注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

Demo体验

Demo体验

场景Demo,开箱即用
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

CSDN 搬运了 Github 所有项目,骚操作一波接一波

最近几天,CSDN 旗下的代码平台 GitCode 在未获得任何作者授权的情况下,批量搬运了 Github 上的几乎所有开源项目:GitCode 不仅把项目所有信息都搬运到自己平台上,还给每个开发者、组织都创建了主页。如果作者想要编辑和操作自己的主页和项目,只...
继续阅读 »

最近几天,CSDN 旗下的代码平台 GitCode 在未获得任何作者授权的情况下,批量搬运了 Github 上的几乎所有开源项目:


GitCode 不仅把项目所有信息都搬运到自己平台上,还给每个开发者、组织都创建了主页。如果作者想要编辑和操作自己的主页和项目,只能使用 Github 授权登录并创建 GitCode 平台的账号才能操作。


GitCode 甚至把项目 README 中的 github 字样都替换成了gitcode...

CSDN 的骚操作远不止这些,它们甚至创建了一批 CSDN 小号,并使用 AI 发布大量 GitCode 项目的相关内容,以进行引流。大家都知道,CSDN 的内容在很多搜索引擎中的权重是比较高的,这一骚操作就回导致搜索结果又多了很多垃圾信息。


AI 盛行的今天,大模型需要使用大量互联网信息进行训练。而 CSDN 用 AI 生成垃圾内容发布到网络上,多多少少会对大模型的质量产生影响,大模型又会生成更多垃圾内容,最终形成恶性循环,想想都可怕。

最搞笑的是,GitCode 在搬运 Github 项目时似乎没有做筛选,搬运了很多违法、违规的项目(懂得都懂,导致网站短暂 404,真是搬起石头砸了自己的脚。


此事发生后,很多开发者都出来声讨 GitCode,并要求其删除账号和项目:


最后不得不吐槽一句,这么一个半成品网站(网站随处可见的Bug),就不要拿出来搞事情了,很难看的。

有网友整理了 CSDN 的五宗罪:


图源:
https://github.com/Catherina0/evil-CSDN

作者:极速星空4DO

来源:www.toutiao.com/article/7384999821570064950/

收起阅读 »

你想活出怎样的人生?

hi~好久不见,距离上次发文隔了有段时间了,这段时间,我是裸辞去感受了一下前端市场的水深火热,那么这次咱们不聊技术,就说一说最近这段时间的经历和一些感触吧。 先说一下自己的个人情况,目前做前端四年,双非本,非科班,技术栈Vue和小程序,读过源码,刷过算法,写过...
继续阅读 »

hi~好久不见,距离上次发文隔了有段时间了,这段时间,我是裸辞去感受了一下前端市场的水深火热,那么这次咱们不聊技术,就说一说最近这段时间的经历和一些感触吧。


先说一下自己的个人情况,目前做前端四年,双非本,非科班,技术栈Vue和小程序,读过源码,刷过算法,写过开源,工作地点在武汉。


我是在三月初裸辞向公司提的离职,并在四月初离开。在做出裸辞这个决定之前,其实也是犹豫了好久,因为在上家公司做开发还是很愉快的,同时看网上大家对于如今的市场行情评价都是寒气逼人,所以对于这次的裸辞我思考了有半年之久。


我的想法有几个点:



  1. 上家公司整体规模偏小,而且项目的复杂度并不太高,技术上的成长主要靠个人,所以如果在这里继续做下去,技术,眼界,薪资可能都会比较受限,越往后越会出现技术不匹配年限的问题。如果公司一旦出现了点什么问题,那么个人在市面上可选择的岗位就会十分受限。

  2. 互联网下行的情况在前两年就已经出现了,然而每年又都会有一大批新的大学生加入到这个行业,那么可能真的今年就是往后十年中最好的一年了,之后一定是会越来越卷的。

  3. 对自己的技术还算是有些信心,觉得不至于会找不到合适的工作。


综合考虑了以上几点,决定就勇敢一次,迈出这一步,不论后面的结果如何都是自己的选择。


面试


然后,就聊一聊最近这段时间面试的感触吧。先说结论,别的城市倒不清楚,就只说武汉,行情的确是有些差的,主要体现在小公司开不起价,大点公司(武汉其实也没什么大公司)又很难过简历筛,再加之岗位有限,所以整体的感受就是水深火热


从三月中下旬开始投递简历,一直到五月底决定去向,这期间在招聘软件上打了上百次招呼,拿到十二个面试机会,通过的有七家,最终选择了离家还算比较近,工作流程以及规模还不错的一家公司入了职。


这段时间可以说是要比平时上班还要累的,工作日每天起来就会去刷一刷招聘软件,去看看有没有新出的职位可以聊一下的,但渐渐的就会发现,招聘软件翻来覆去就那么几家公司,还都是常年招聘的,新出的机会可能要好久才会遇到一次。


能约到面试的几天心态还会好一些,可一旦连续几天没有约到面试,投递简历都石沉大海,那个时候内心就会开始有些焦虑,很容易会想要不要随便找一家将就下得了,但好在每次有这种想法的时候,都会有新的面试邀约出现,也算是挺幸运的了。而且根据每次面试的过程来看,目前我点的技能点是完全够用了的,甚至面一些小公司的时候,有时能清晰的感受到在吊打面试官,这也算是无形中增加了我的信心吧,能够让我继续战斗下去~ 而且也非常感谢在找工作时给我鼓励的掘友,当时面了一家公司,而面试官是一位掘友的朋友,可能下去后面试官和掘友提起了我的面试,晚上在掘金收到了掘友的私信,说我的技术一定没问题的,而且算法可以,一定要去投一投大公司~ 当天收到私信时,可以说真的是热泪盈眶,感受到了寒冬中的小小温暖,真的非常感谢~


然后说一下面试体验吧,面试体验真的和公司规模成正比的。


窒息的面试体验


我面的这几家,有一些小公司的面试官或者hr真的各种作妖:



  • 有的时候吊打了面试官,然后hr来谈薪想压价,拿什么压我都能理解,毕竟公司给到hr的预算可能有限,但是拿技术来压,真就不理解,面试官都没说什么,甚至当场说技术确实很不错,然后一个hr来尝试根据之前做的项目找漏洞去聊技术,聊复杂度去压价,真的是让人难以理解。

  • 有的公司则是非常的小,然后面试官应该就是公司领导吧,给了一份笔试题,做完后去面试,笔试当时做了15分钟,面试只12分钟,而面试的时候在刚进行2分钟我就已经想结束面试直接走人了,面试官就是对着他出的一份稀烂的笔试题一个个问,我也一个个给他答,每答一个他都先把你的答案给否定,然后尝试从回答中找漏洞,没有找到那就再问一个他自己现编的很奇怪的问题,真就离谱,也真是我素质还算好,没有当场去怼他,当时面的12分钟真的是折磨

  • 再不然有些面试官,就是简历也不细看,就会去问一些冷门API的用法,这一家当时我已经面到后期了,见了形形色色的面试官,所以也不惯着,直接就问他,你问这个有什么用呢?你是想招干活的人,还是想招可培养的人?那你面试问一个API能问出来什么呢?


愉快的面试体验


说完了小公司的体验,再说一些体验还不错的面试吧,一个体验比较好的面试给人的感觉就是,对方是能把我掌握的技术深度和广度都给探到,并且双方面试过程更像是探讨的过程



  • 有的面试官会在听你介绍项目难点以及解决方案的时候,逐步的引导你去思考出更优的解决方案

  • 有的面试官则会给你一种感觉就是,这个面试官真的很大佬,比如我遇到的一个面试官精通源码,虽然我也看过并且写过源码文章,但在很多细节的地方还是会有所遗忘,在面试的过程中,有的地方思路乱了,面试官则会在我把我知道的都讲完之后,去完整的给梳理一次思路,并说明整个的运行流程。


这两种面试官其实都有一个共同的点,就是他是在找你技术的深度和解决问题的能力,让你尽可能的展示自己,而不是对着一份面试题或者就是想刁难你找优越感


最后的选择


最终,选择的这家,其实薪资上的涨幅很小,但工作强度会比上一家大上不少。面了2个月,这个过程很累,我也没有太多的能量去接着去面试了,而这家公司整体面试体验给我的感觉还可以,就先入职看看喽~


然后,关于自己的职业发展,目前其实是有些迷茫的,刚入行前端的时候,感觉当时的机会还是很多的,能看到很多大厂的招聘要求以及结合一些在网上看到的一些大佬的经历,然后我就做出了规划:去研究源码和算法参与一些开源,当工作经验够3年之后,去尝试投递一下大厂,看一看新的机会。可是现在,当经验,技能可以达到要求之后,市场却凉下来了,不是92的学历或者大厂的履历,连简历筛都很难过的去,小一点的公司也想用较低的工资去招一个经验丰富的人,然后面试就还会问对加班的看法,甚至有的还会问无效加班接不接受,感觉整个市场都是一个让人无法理解的样子


最后


上面聊了这么多,不管怎样,也确实是当前武汉前端求职环境的现状大佬当然无所畏惧),所以,如果有朋友还跃跃欲试想换个环境,那我建议也是,如果可以的话找好再走,不要着急但这个问题的点就在于,很多公司会要求线下面试,就算线上面试,时间安排其实也会很不方便),可以投递一下先试试水,感受一下市场。但如果是有自己的规划或者实在是想要换个环境的朋友,可以根据我上面说的,只要能做好心理预期可能会连续打招呼两三天,甚至一周都没有回应),确保自己的心态稳定因为这本来就不是个人的问题,我们能做的就是把所掌握的技术准备充分就可以了),其实也可以一试,机会是有的,但是不多,需要自己去争取,并把握住


最后的最后,关于起这个标题,其实是我在一开始写这篇文章的时候脑海中就浮现的宫崎骏的这个电影和这句话。。。关于这个电影,网上有很多的评价,有的人会觉得这个电影不知道到底想说些什么,教会我们些什么。那有没有可能,老爷子其实也没打算教我们什么,当下的环境已经塞给我们太多东西,可以单纯的感受一下宫崎骏为我们创造的奇幻世界也是挺好的~ 你想活出怎样的人生其实都没有问题,或奋斗,或躺平,或去大城市,或留在小城市都只是一个选择,一种体验而已,没什么对错之分。所以这句话是在问掘友,也是在问我自己吧~


后续的个人规划,其实我也还没有很明确,现阶段,打算先继续搞一搞自己感兴趣的技术吧,不管环境怎样,个人的状态怎样,只要是在向前的,我想总归是好的吧,后续也会继续输出一些有意思的内容,掘友们共勉~


作者:沽汣
来源:juejin.cn/post/7376177615441117238
收起阅读 »

移动前端混合开发技术演进之路

本文是azuo和萌妹俩技术创作之旅的第15篇原创文章,内容创作@azuo😄,精神支持@大头萌妹😂 前言:本文主要探讨了移动混合开发( Hybrid APP) 开发的技术演进历程,将阐述了webview(H5)、React Native、小程序技术等在其中所扮...
继续阅读 »

本文是azuo和萌妹俩技术创作之旅的第15篇原创文章,内容创作@azuo😄,精神支持@大头萌妹😂



前言:本文主要探讨了移动混合开发( Hybrid APP) 开发的技术演进历程,将阐述了webview(H5)、React Native、小程序技术等在其中所扮演的关键角色及带来的变革。原生能力缺失、长时间白屏、用户操作响应不及时等web开发的问题是如何被解决的?


一、诞生背景


早期移动应用开发,由于机器硬件性能的方面影响,为了更好的用户体验(操作响应、流畅度和原生的能力),主要集中在原生应用开发上。


1.1 原生开发的缺点


原生应用开发周期和更新周期长,也逐渐在快速的迭代的互联网产品产生矛盾。


缺点:



  • 开发周期长:开发调试需要编译打包,动辄就需要几分钟甚至十几分钟,相比H5的亚秒级别的热更能力,是在太长了;

  • 更新周期长:正常的发版需要用户手动更新,无法做到H5这种发布即更新的效率。

  • 使用前需要安装;

  • 需要多端开发;(Android和iOS两端开发人力成本高)


1.2 web开发的缺点


原生应用的研发效率问题,也逐渐在快速的迭代的互联网产品产生矛盾。这时候,开发人就自然而然的想到web技术能力,快速开发和发版生效和跨平台能力。


web技术开发的H5界面,相比原生应用,缺点也很明显:



  1. 缺少系统的提供原生能力;

  2. 页面白屏时间长(原生基本可以做到1秒内,h5普遍在2秒以上);

  3. 用户操作响应不及时(动画卡、点击没有反应);


把Native开发和web开发的优缺点整合一下,就诞生了Hybrid App。Hybrid App技术从诞生到现在一直在解决这3个问题。


二、 提供原生能力


JSBridge技术是由 Hybrid 鼻祖框架phoneGap带到开发者的视野中,解决了第一个问题。它通过webview桥接(JSBridge)的方式层解决web开发能力不足的问题,让web页面可以用系统提供原生能力。


2.1 技术原理


Android原生开发提供了各种view控件(类比Dom元素:div、canvas、iframe),其中就用一个webview(类比iframe)。JSBridge 就像其名称中的『Bridge』的意义一样,是 Native 和非 Native 之间的桥梁,它的核心是 构建 Native 和非 Native 间消息通信的通道,而且是 双向通信的通道


image.png


双向通信的通道:



  • JS 向 Native 发送消息 : 调用相关功能、通知 Native 当前 JS 的相关状态等。

  • Native 向 JS 发送消息 : 回溯调用结果、消息推送、通知 JS 当前 Native 的状态等。


2.2 实现细节


Android可以通过webview将一些原生的Java方法注入到window上供Javascript调用。Javascript也可以直接在window上挂着全局对象给webview执行。


2.2.1  JavaScript 调用 Native


Android 可以采用下面的方式:


public class JSBridgeActivity extends Activity{ 
private WebView Wv;

@Override
publicvoidonCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
Wv = (WebView)findViewById(R.id.webView);
Wv.getSettings().setJavaScriptEnabled(true);
// 4.2 使用 @JavascriptInterface
Wv.addJavascriptInterface(new JavaScriptInterface(this), "nativeBridge");
// TODO 显示 WebView
}
}


public class JavaScriptInterface{
@JavascriptInterface
public void postMessage(String webMessage){
// Native 逻辑
}
}

前端调用方式:


// android会在window上注入nativeBridge对象
window.nativeBridge.postMessage(message);

native层除了上述方式被Javascript调用,还有可以拦截alert、confirm、console的日志输出、请求URL(伪协议)等方式,来的获取到Javascript调用native的意图。


2.2.2 Native 调用 JavaScript


相比于 JavaScript 调用 Native, Native 调用 JavaScript 较为简单, WebView 组件,都以子组件的形式存在于 View/Activity 中,直接调用相应的 API 即可(类比浏览器的window中的原生方法)。


// android 4.4之前
webView.loadUrl("javascript:"+javascriptString)

// android 4.4之后
webView.evaluateJavascript(
javaScriptString, // js表达式
new ValueCallback<String>() { // 表达式的值通过回调给native
@Override
public void onReceiveValue(String value){
// 鉴权拦截,一般估计页面域名白名单的方式
JSONObject json = new JSONObject(value)
switch(json.bridgeName){
// 处理
}

}
}
);

2.3  JSBridge 接口


JSBridge 技术是对JavaScript 和 Native之间的封装成JS SDK方便前端JS调用,主要功能有两个:调用 Native和 接收Native 被调。


(function () {
var id = 0,
callbacks = {};


window.JSBridge = {
// 调用 Native
invoke: function(bridgeName, callback, data) {
// 判断环境,获取不同的 nativeBridge
var thisId = id ++; // 获取唯一 id
callbacks[thisId] = callback; // 存储 Callback
nativeBridge.postMessage(JSON.stringify{
bridgeName: bridgeName,
data: data || {},
callbackId: thisId // 传到 Native 端
});
},
receiveMessage: function(msg) {
var bridgeName = msg.bridgeName,
data = msg.data || {},
callbackId = msg.callbackId; // Native 将 callbackId 原封不动传回
// 具体逻辑
// bridgeName 和 callbackId 不会同时存在
if (callbackId) {
if (callbacks[callbackId]) { // 找到相应句柄
callbacks[callbackId](msg.data); // 执行调用
}
} elseif (bridgeName) {


}
}
};
})();

JSBridge通过建立一个通信桥梁,使得JavaScript和原生代码可以相互调用,实现高效的数据传输和交互。这个过程是跨线程异步调用的,数据传输一般会经过两次序列化(还有提升的空间)


三、解决白屏


3.1 白屏产生的原因


原生APP安装后启动页面,在正常情况是不用再从网络获取资源,只需要请求后端接口获取数据就可以完成渲染了,网页不需要安装才,每次打开web页面都会从远程服务加载资源后,再请求后端数据后才能渲染。在用户等待资源加载过程和浏览器渲染未完成中,就会出现白屏。造成白屏的主要原因 -- 资源网络加载


首屏渲染SSR.drawio.png


3.2 离线包技术


离线包主要是识别特定url地址(通常是url参数=离线批次id,即:_bid=1221)后保存到用户手机硬盘。用户下次打开H5页面就可以不用走网络请求。离线包一包也会提供预下载能力,保证首次打开H5页面也可以获得收益。



离线包是完整的资源分发系统,需要一个完整的技术团队来建设和维护的。



3.2.1 离线包分发过程


分发流程中主要涉及4种角色:



  • 离线配置平台:配置平台可以提供离线配置能力、离线包管理(上传、禁用、清空)、离线包使用统计、离线包准入审核(自动(包大小限制)+人工(解决特殊case))

  • 离线配置服务: 配置服务主要提供服务层能力,实现离线配置服务,离线包更新服务,离线资源长传下载服务、离线资源使用统计服务

  • 离线SDK: 端内接入离线SDK,SDK主要与离线配置服务进行交互,完成离线资源的管理和接入配置能力

  • Native侧 : 实现拦截请求在特定的协议下接入离线资源


image.png


3.2.2 离线包加载过程


离线包的加载流程


image.png


3.2.3 拦截实现细节


实现WebViewClient: 继承WebViewClient类,并重写shouldInterceptRequest方法。这个方法会在WebView尝试加载一个URL时被调用,你可以在这里检查请求的URL,并决定是否拦截这个请求。


public class MyWebViewClient extends WebViewClient {  
private InputStream getOfflineResource(String url) {
// ... 你的实现代码 ...
return null; // 示例返回null,实际中应该返回InputStream
}

@Override
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
String url = request.getUrl().toString();

// 检查这个URL是否在你的离线包中
InputStream inputStream = getOfflineResource(url);
if (inputStream != null) {
// 如果在离线包中找到了资源,就返回一个WebResourceResponse对象
return new WebResourceResponse(
"text/html", // MIME类型,这里以HTML为例
"UTF-8", // 编码
inputStream
);
}
// 如果没有在离线包中找到资源,就返回null,让WebView按照默认的方式去加载这个URL
// 走网络请求获取
}
}

// 在你的Activity或Fragment中
WebView webView = findViewById(R.id.webview);
webView.setWebViewClient(new MyWebViewClient());

3.3 服务端渲染(SSR )


在3.1 白屏产生的原因,影响白屏的因素是JS和CSS资源和数据请求。如果,html请求得到的内容中直接包含首屏内容所需要内联的CSS和Dom结构。


首屏渲染.drawio (4).png


SSR通过在服务端(BFF)直接完成有内容的HTML组装。webview获取到html内容就可以直接渲染。减少白屏时间和不可交互时间。


3.3.1 增量更新和并行请求


SSR将本来一个简单框架HTML,增加了首屏内容所需要的完整CSS和Dom内容。这样的话,HTML请求的包体积就增大了多。其中:



  • 跟版本相关的样式文件CSS (变更频率低)

  • 跟用户信息相关的Dom内容(变更频率高)


HTML根据内容变更频率进行页面分割如下:


<!DOCTYPE html>
<html lang="en">
<head>
<title>OPPO用户体验评价</title>
<meta charset="UTF-8">
<script content="head">window._time = Date.now()</script>
<meta name="renderer" content="webkit|chrome">
<meta name="format-detection" content="telephone=no" />
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="x5-orientation" content="portrait">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-COMPATIBLE" content="IE=Edge,chrome=1">
<meta name="nightmode" content="disable">
<meta name="color-scheme" content="light">
<!-- css内联内容开始 -->
<style>
/*http://www.xxx.com/wj-prod/style.css*/
/**
* 替换url的css内容,内容比较多
*/

</style>
<!-- css内联内容结束 -->
</head>
<body>
<!-- dom内容开始 -->
<div id="app">
<!-- 拼接好的html结果 -->
<div>
<span></span>
</div>
</div>
<!-- dom内容结束 -->
<!-- 数据内容开始 -->
<script content="page-data">
// 直出的数据,方便vue、react等框架回填状态,声明式UI才必须
window.syncData = {/**服务端获取的数据**/}
</script>
<!-- 数据内容结束 -->
<script crossorigin="anonymous" src="//cdn.xxx.com/wj-prod/client.bundle.js?_t=1"></script>
</body>
</html>

客户端和BFF层大概工作流程如下:


image.png


首屏渲染.drawio (5).png


手机QQ将这套方案开源了:github.com/Tencent/Vas… (我曾经也是这套方案的参与者和使用者)


3.4 总结


为了更快的渲染出页面,发展了离线包技术、服务器端渲染(SSR)、Webview启动并行等一系列的技术方案,这些技术可以单个使用,也可以组合使用。



  • 对于首次加载的页面,使用服务器端渲染(SSR)和Webview启动并行,是可以很好的解决白屏问题,适用H5活动页面。

  • 对于二次加载的页面,使用离线包技术、服务器端渲染(SSR)和Webview启动并行,可以在不经过网络请求也可以展示页面,适用固定入口客户端页面;


四、解决卡顿


使用过程发现H5网页相比于原生页面,更容卡顿,甚至造成页面卡死的问题。这个章节就主要解决为啥浏览器渲染的H5会比原生卡?Hybrid开发用哪些技术如何解决这个问题?


4.1 浏览器渲染的慢


浏览器技术的发展历程已有超过30年的历史,Chrome内核有超过2400万行代码,有很重的历史包袱。


4.1.2 渲染流程


浏览器渲染页面使用了多线程的架构,发生卡顿的主要原因在:渲染线程和JS引擎线程,他两是互斥的,Javascript长时间执行会导致渲染线程无法工作。
image.png


GUI渲染线程(GUI Thread):



  1. 负责渲染浏览器界面。

  2. 解析HTML、CSS,构建DOM树和CSS规则树,并合成渲染树。

  3. 布局(Layout)和渲染(Paint)页面内容。

  4. 与JS引擎线程互斥,当JS引擎线程执行时GUI渲染线程被挂起,GUI更新会被保存在一个队列中,等JS引擎空闲时立即执行。


JS引擎线程(JS Engine Thread):



  1. 也称为JS内核(在Chrome中为V8)。

  2. 负责解析和执行JavaScript代码。

  3. 单线程设计,JS运行过长会阻塞GUI渲染。


事件触发线程(Event Dispatch Thread):



  1. 用于控制事件循环。

  2. 当事件(如点击、鼠标移动等)被触发时,该线程会将事件放到对应的事件队列中,等待JS引擎线程处理。


合成器线程(Compositor Thread)和光栅线程(Raster Thread):



  1. 这两个线程在渲染器进程中运行,以高效流畅地渲染页面。

  2. 合成器线程负责将不同的图层组合成最终用户看到的页面。

  3. 光栅线程则负责将图层内容转换为位图,以便在屏幕上显示。


以用户点击操作为例:


image.png


如果界面的刷新帧率是60帧,在不掉帧的情况。执行时间只有 1000 ms / 60 = 16.66 ms。上图中间的JS引擎线程和渲染线程的执行是串行,而且不能超过16.66 ms。(留给JS引擎和渲染线程执行的时间本身不多,60帧只有有16ms,120帧只有8ms)这就是浏览器为啥比原生渲染卡。


4.2 声明式UI


浏览器渲染慢的主要原因是JS引擎线程和渲染进程的执行互斥, 那么,最简单解决方式就是将渲染线程改造按照帧率来调度,不再等JS引擎线程全部执行完再去渲染。但是,由于浏览器最初涉及的JS引擎线程是为了应对命令式UI渲染方案,命令式UI对界面的修改是不可预测。


4.2.1 命令式UI


命令式UI关注于如何达到某个特定的用户界面状态,通过编写具体的操作指令来直接操纵界面元素。关注于操作步骤和过程,需要编写具体的代码来实现每个步骤。


// dom找到需要变更的节点
const list = document.querySelector('#content')
// 修改样式
list.style.display = 'none'
// 增加内容
list.innerHTML += `<div class="item">列表内容</div>`

优点: 是入门简单,讲究一个精确控制直接操作。


缺点: 直接操作界面,带来对UI界面渲染的不可以预测性;


4.2.1 声明式UI


声明式UI(Declarative UI)是一种用户界面编程范式,它关注于描述UI的期望状态,而不是直接编写用于改变UI的命令。在声明式UI中,开发者通过声明性的方式定义UI的结构、样式和行为,而具体的渲染和更新工作则由框架或库自动完成。


声明式UI编程范式:


image.png


function List(people) {
const listItems = people.map(person =>
<li key={person.id}>
<img
src={getImageUrl(person)}
alt={person.name}
/>

<p>
<b>{person.name}</b>
{' ' + person.profession + ' '}
known for {person.accomplishment}
</p>
</li>

);
return <ul>{listItems}</ul>;
}

优点: 入门难度有所增加,代码更加简洁,带来更高和可维护性,可以直接根据数据预测UI更新


缺点: 入门难度有所增加,灵活性没有命令式UI高;


4.2.1 虚拟DOM


声明式UI强调数据驱动UI更新,一般声明式UI框架中,都还会引入虚拟DOM技术。虚拟DOM(Virtual DOM)是一种在前端开发中广泛使用的技术,它通过JavaScript对象来模拟真实的DOM结构,从而优化Web应用程序的性能和渲染效率。



  • 核心思想:将页面的状态抽象为JavaScript对象表示,避免直接操作真实的DOM,从而提高性能和渲染效率。

  • 工作流程:



    • 初始渲染:首先,通过JavaScript对象(虚拟DOM)表示整个页面的结构。这个虚拟DOM是一个轻量级的映射,保存着真实DOM的层次结构和信息。

    • 更新状态:当应用程序的状态发生变化时,如用户交互或数据更新,虚拟DOM会被修改。这个过程操作的是内存中的JavaScript对象,而不是直接操作真实的DOM。

    • 生成新的虚拟DOM:状态变化后,会生成一个新的虚拟DOM,反映更新后的状态。

    • 对比和更新:通过算法(如Diff算法)将新的虚拟DOM与旧的虚拟DOM进行对比,找出它们之间的差异。

    • 生成变更操作:根据对比结果,找出需要更新的部分,并生成相应的DOM操作(如添加、删除、修改节点等)。

    • 应用变更:将生成的DOM操作应用到真实的DOM上,只更新需要变更的部分,而不是整个页面重新渲染。




virtual-dom为例,虚拟Dom的渲染流程大致如下:


import h from 'virtual-dom/h'
import diff from 'virtual-dom/diff'
import patch from 'virtual-dom/patch'

// 第一步:定义渲染函数,UI = F( state)中的f,
// 开发人员编写渲染模版(react对于是jsx,vue对应的template),由构建工具生成;
function render(count) {
return h('text', { attributes: { count } }, [String(count)])
}

// 第二步:初始化vtree
let tree = render(count) // We need an initial tree

// UI变更
setTimeout(function () {
// 第三步:更新state,重新生成vtree
count++
const newTree = render(count)

// 第四步:对比新旧vtree的差异
const patches = diff(tree, newTree)
console.info('patches', patches)

// 第五步:增量更新dom
// patch(rootNode, patches)

tree = newTree
}, 1000)

相比于命令式UI的开发,声明式UI和虚拟DOM技术结合后,UI渲染过程表示用简单的数据结构就可以表述(第四步骤得到结果序列化),能序列化的好处就是可以很简单完成跨线程处理。


4.3 React Native


声明式UI和虚拟DOM是由React带到开发的视野中。虚拟DOM除了提供声明式UI的高性能渲染能力,它还有一个强大的能力--抽象能力。



4.3.1 组件抽象


在开发者的代码与实际的渲染之间加入一个抽象层,这就可以带来很多可能性。对于React Native 渲染实现:



  • 在IOS平台中则调用Objective-C 的API 去渲染iOS 组件;

  • 在Android平台则调用Java API 去渲染Android 组件,而不是渲染到浏览器DOM 上。


image.png


React Native的渲染是使用不同的平台UI Manager 来渲染UI。因此,React Native对UI开发的基础组件进行整合和对应


React NativeAndroid ViewIOS ViewWeb Dom
<view><ViewGr0up><UIView<div>
<Text><TextView><UITextView><p>
<Image><ImageView><UIImageView><img>

4.3.2 样式渲染


组件结构通过抽象的基础可以完成每个平台的转换。UI界面开发出来结构还需要样式编写。React Native引用了Yoga。Yoga是 C语言写的一个 CSS3/Flexbox 的跨平台 实现的Flexbox布局引擎,意在打造一个跨iOS、Android、Windows平台在内的布局引擎,兼容Flexbox布局方式,让界面布局更加简单。


4.3.3 线程模型


在React Native中,渲染由一个JS线程和原生线程。JS线程负责解析和执行JavaScript代码,而原生线程则负责渲染界面和执行原生操作。JS执行的结果(dom diff)异步通知原生层。


image.png


4.3.3 总结


React Native借助虚拟DOM的抽象能力,把逻辑层的JS代码执行单独抽到JS引擎中执行,不再与UI渲染互斥,可以留更多时间给UI渲染线程。


UI渲染相比浏览器渲染性能提升主要在两点:



  • JS层不再互斥UI渲染;

  • UI渲染由浏览器渲染改成原生渲染;


UI放到Natie层渲染,逻辑放在JS层执行,Natice层与JS层通过JSBridge(24年底会默认替换成JSI,以提高数据通信性能,有兴趣可以去了解)进行通信。


Weex和快应用的实现原理跟React Native类似,主要的差异是在编写声明式UI的DSL,这里就不一一讲解


4.4 微信小程序


微信小程序是从公众号的H5演变而来的。2015年微信对外发布JS-SDK(JS Bridge)提供微信的原生能力(类似早期的phoneGap的),解决了移动网页能力不足的问题。但是,页面加载白屏、网页安全和卡顿问题依旧没被解决。


微信在2017年设计一个全新的系统来解决这些问题,它需要使得所有的开发者都能做到:



  • 快速的加载

  • 更强大的能力

  • 原生的体验

  • 易用且安全的微信数据开放

  • 高效和简单的开发


4.4.1 双线程架构


有了虚拟DOM这个抽象层,UI界面开发的的逻辑层和视图层可以分离。小程序的渲染层和逻辑层分别由两个线程管理(视图层是 WebView,逻辑层是 JS 引擎


image.png



  • 视图层主要负责页面的渲染,每一个页面Page View对应一个Webview(不能超过10个页面栈)。

  • 逻辑层负责js的执行,一个JS执行的沙箱环境;


微信小程序的双线程有如下主要优点:



  1. javascript脚本执行不会抢占ui渲染资源,使整体页面渲染更快;

  2. 每个PageView是由一个webview单独渲染,页面切换效果上更接近原生,比公众号h5网页浏览体验要好;

  3. 安全管控,独立的沙箱环境运行javascript逻辑代码,避免了浏览器的开放api操作dom、跳转页面等,更加安全。


4.4.2 开发的DSL


小程序包含一个描述整体程序的 app 和多个描述各自页面的 page。


个小程序主体部分由三个文件组成,必须放在项目的根目录,如下:


文件必需作用
app.js小程序逻辑
app.json小程序公共配置
app.wxss小程序公共样式表

一个小程序页面由四个文件组成,分别是:


文件类型必需作用
js页面逻辑
wxml页面结构
json页面配置
wxss页面样式表

WXML和WXSS是微信官方创造的DSL,需要进行编译后才能被Webview解析执行。可以从微信开发者工具包文件中找到 wcc 和 wcsc 两个编译工具



  • wcc 编译器可以将 wxml 文件编译成 JS 文件

  • wcsc 编译器可以将 wxss 文件编译成 JS 文件。


 
WXML(WeiXin Markup Language)是框架设计的一套标签语言,结合基础组件事件系统,可以构建出页面的结构。(类比虚拟DOM中的Render函数)


<!--wxml-->
<view>
<text class="text">{{message}}</text>
</view>

将wcc拷贝到当前的index.wxml同级目录, 执行


./wcc -js index.wxml >> wxml.js

将wxml.js的内容复制到浏览器的console中执行后,输入:


$gwx('index.wxml')({
message: 'hello world'
})

可以获得vtree:


{
"tag": "wx-page",
"children": [
{
"tag": "wx-view",
"attr": {},
"children": [
{
"tag": "wx-text",
"attr": {
"class": "text"
},
"children": [
"hello world"
],
"raw": {},
"generics": {}
}
],
"raw": {},
"generics": {}
}
]
}

WXSS (WeiXin Style Sheets)是一套样式语言,用于描述 WXML 的组件样式。(跟CSS类似,增加了rpx相对尺寸,可以参考REM的响应式布局)


page{
display:flex;
background-color: #fff;
}
.wrap{
width:320rpx;
height: 200rpx;
}
.text{
color:red;
font-size:12px
}

将wcsc拷贝到当前的index.wxss同级目录, 执行


./wcsc -js index.wxss >> wxss.js

最后将wxss.js的内容拷贝到浏览器去运行,即可得到:


image.png


(page的样式转化成了body,rpx转成px)


4.4.3 逻辑层和渲染层


逻辑层主要执行app.js和每个页面Page构造器。最终将Page中data修改后的结果通过setData同步给渲染进程。


image.png


逻辑层是一个沙箱的执行环境,该环境不存在DOM API、window、document等对象API和全局对象。换句话来说,小程序相比传统H5是更加安全。小程序中访问用户相关信息是不能像H5直接调用浏览器API,需要经过用户授权才或者由用户操作触发才可以被调用。


小程序的渲染层是在webview执行的,主要将运行wxml和wxss编译后的代码;



  • wxss文件编译成js,之后后会往head中插入style样式

  • wxml编译成声明式UI的render函数,接受逻辑层的data来更新vtree,dom diff ,增量更新dom


render函数中的data由逻辑层调用setData跨线程传给渲染层, 渲染层相比传统的浏览器渲染页面少了渲染前的data生成。相比React Native,渲染层仍然会执行JS(主要虚拟Dom更新)。


image.png


逻辑层和渲染层的在不同平台的实现方式:


运行环境逻辑层渲染层
iOSJavaScriptCoreWKWebView
AndroidV8XWeb(腾讯自研,基于Mobile Chrome内核)
PCChrome内核Chrome内核
小程序开发工具NW.jsChrome WebView

4.4.4 Skyline渲染引擎


小程序早期的渲染层是使用webview,每个PageView对一个webview,内存开销是很多。



Skyline渲染引擎其实可以被看作一个被优化后的webview,并在其内置了更加优秀的动画系统、跨线程传说方案



微信增加了渲染引擎 Skyline,其使用更精简高效的渲染管线,并带来诸多增强特性,让 Skyline 拥有更接近原生渲染的性能体验。


image.png


Skyline 创建了一条渲染线程来负责 Layout, Composite 和 Paint 等渲染任务,并在 AppService 中划出一个独立的上下文,来运行之前 WebView 承担的 JS 逻辑、DOM 树创建等逻辑。这种新的架构相比原有的 WebView 架构,有以下特点:



  • 界面更不容易被逻辑阻塞,进一步减少卡顿

  • 无需为每个页面新建一个 JS 引擎实例(WebView),减少了内存、时间开销

  • 框架可以在页面之间共享更多的资源,进一步减少运行时内存、时间开销

  • 框架的代码之间无需再通过 JSBridge 进行数据交换,减少了大量通信时间开销


 Skyline 的首屏时间比 WebView 快 66%


image.png


Skyline 的内存占用比 WebView 减少 50%


image.png


详细可以参考:developers.weixin.qq.com/miniprogram…


4.4.5 总结


微信小程序采用双线程的架构方案,即解决web困扰已久的安全问题,而且也在一定程度上优化了页面渲染性能。虚拟DOM的抽象能力,使得PageView可以是WebView、React-Native-Like、Flutter 等来渲染


微信小程序也有类似离线包的技术,将用户访问的小程序缓存在微信APP的安装目录中,来解决页面白屏问题。首次加载白屏问题通过native层loading页面来遮盖,因此,小程序首次使用也会有2到3秒的加载过程(小程序分包要求,加载包不能超过2M,加载时间可以做到可控😄)。。


4.5 总结


React Native、Weex、微信小程序、快应用等技术,提供了一整套开发完备的技术和工具来实现混合开发。包括不限于:



  • 平台提供基础UI组件为基础;

  • 声明式UI作为首选,虚拟DOM的抽象能力,UI渲染框架可以多层级多语言实现;

  • 双线程和JSBridge(JSI),使得JS逻辑执行和UI渲染分离;

  • 完整工具类,编译、打包、HMR;

  • 分包,一个应用可以由多个模块包组成;

  • 亚秒级别的热更新能力;


后面出现的Flutter、ArkUI框架也基本围绕这些技术理念进行整合(当然还有编译技术的优化JIT向AOT,带来更快的启动速度)。


(Flutter、ArkTS带来更快的启动速度的技术方案后面再补到文章内吧)


五、发展历程


混合开发的发展史是一段技术革新和演进的过程,它标志着移动应用开发从单一平台向跨平台、高效率的方向转变。


image.png



  • JSBridge让JavaScript拥有原生能力,JSI等技术让JavaScript直面C++,带来更加高效的传输速度;

  • 离线包技术,兼顾加载和留存,SRR仍是很有效优化首屏速度的手段;

  • 分包技术是提高加载速度和开发效率;

  • 声明式U开发范式,加上虚拟Dom抽象能力,解偶上层开发与底层渲染框架,新的渲染框架不断涌现;

  • JSCore引擎的双线程架构,打破逻辑层和UI层间的互斥,即解决Web困扰已久的安全问题,也缓解浏览器渲染性能问题;


作者:azuo
来源:juejin.cn/post/7382051737362284559
收起阅读 »

扫码出入库与web worker

web
我为什么会用到这个呢,那还得从最近项目的一个扫码出入库的需求说起,之前客户的扫码出入库都是c端的,在效率方面没有明显的问题,但是后面这个项目的升级,就把c端的扫码部分摞到了B端了 大体需求是这样,客户用无线扫码枪扫回运单上的条码,然后扫码枪使用HID键盘模式(...
继续阅读 »

我为什么会用到这个呢,那还得从最近项目的一个扫码出入库的需求说起,之前客户的扫码出入库都是c端的,在效率方面没有明显的问题,但是后面这个项目的升级,就把c端的扫码部分摞到了B端了


大体需求是这样,客户用无线扫码枪扫回运单上的条码,然后扫码枪使用HID键盘模式(扫码枪相当于一个键盘),在一个一直聚焦的输入框输入扫到的条码,然后我这边监听到条码调接口录入库,成功后再语音播报扫码结果,同时刷新结果,刷新统计信息。


听上去很简单是不是,想象是美好的,可现实就残酷了,在初始版的时候,功能是做出来了,本地出入库都没问题,但是发到生产就悲催了,乱七八糟的问题


比如



  • 1.扫码枪精度的问题,扫码识别率低下,扫10次才能正确识别1次

  • 2.扫出来的码,断码,原本以为扫码枪扫一次就等同于我复制一个条码进输入框,可结果是扫码枪一次扫入,输入框接受的条码就像一个字符串流一样,一个字符一个字符进入的,这就导致中间间隔稍微长一点,就被错误的识别为另外一个条码(扫码是多个码连续扫入的)

  • 3.语音播报延迟,经常会有语音播放不出或者播放一半,这个..


这个就很让人无语,明明本地啥问题也没有


第一个问题,扫码枪精度,确实是有,因为我做的时候拿的扫码枪是一个有线的扫码枪,那识别率才叫一个高,准确率差不多95%,几乎没遇到解码啥的问题,可换成无线的扫码枪就傻眼,第一个问题就很烦,想到几千个客户没办法统一更换扫码枪,于是就想想优化一下条码编码呢,
我这边条码是用的jsBarcode组件,默认的编码类型CODE128,嗯~~问题会不会出在条码规范上呢


我去查了一下,条码的编码规范大致有以下几种


条码类型类别描述常见应用编码长度
UPC-A1D通用产品代码,常见于零售业零售商品12位数字
UPC-E1DUPC-A的压缩版本小型零售商品6位数字
EAN-131D欧洲商品编号,国际通用图书、零售商品13位数字
EAN-81DEAN-13的压缩版本小型商品8位数字
Code 391D可变长度,包含字母、数字和特殊字符工业、政府可变长度
Code 1281D高密度条码,表示所有128个ASCII字符物流、运输可变长度
Interleaved 2 of 5 (ITF)1D数字条码,每两个数字组成一对交错编码分销、仓储偶数位数字
QR Code2D可存储大量数据,包括文字、数字、二进制数据和汉字支付、信息分享、广告可变长度
Data Matrix2D高密度编码,适用于小型物品标识电子元器件、医疗设备可变长度
PDF4172D可编码大量数据身-份-正件、运输标签可变长度
Aztec Code2D高容错性,适用于票务和登机牌票务、登机牌可变长度

我这里着重说说CODE39和CODE128;我发现CODE39生成的条码比CODE128生成的长很多,我这把无线扫码枪扫很久都扫不出来,识别超慢,这个很奇怪,之前客户C端系统找技术查了一下,编码规范是CODE39,我就懵逼了,都是CODE39,为啥我们生成的码就识别这么慢,捣鼓了很久也没个结果,如果有哪位知道的可以给我说一下,就索性放弃这种编码模式,改用CODE128吧,查了一下,这是一种效率更高的编码方式,CODE39条码较长的主要原因在于它的编码效率较低,每个字符占用的空间较大,而CODE128通过更加紧凑和高效的编码方式,能够在同样的内容下生成更短的条码,于是撺掇同事把所有的条码都用CODE128生成,至此,扫码枪识别效率低的问题算事过去了


然后就是第二点,扫出来的码,断码问题,这个也因为换了短码好那么一点,可扫出来也经常有解码内容变长,的问题,暂时还在想办法优化


最后就是语音播报延迟,卡壳,甚至没有语音的情况,这个问题比较恼火,我这边组件是使用的开源库howler.js,这个库的优点就是兼容性好,可以播放包括mp3, opus, ogg, wav, aac, m4a, m4b, mp4, webm, 等多种格式,而且还支持分轨sprite播放,这个是我的最初的代码


import config from "./config";
import "./lib/howler.min";
const ENV = import.meta.env;

class VoiceReport {
public list = [];
constructor() {
this.initVoice();
}
// 目录放在@/assets/voice/ 下面
public voiceList: any = import.meta.globEager("@/assets/voice/*.mp3");
public voiceNameList = Object.keys(this.voiceList);
// 初始化语音播报器列表
initVoice = () => {
config.forEach((v) => {
const item = {
name: `Ref${v.codeType}${v.codeKey}`,
code: v.codeKey,
codeName: v.codeName,
voice: "",
};
const voiceIndex = this.voiceNameList.findIndex((voice) =>
String(voice).includes(v.codeKey)
);
if (voiceIndex > -1) {
item.path = this.voiceNameList[voiceIndex];
item.voice = this.voiceList[this.voiceNameList[voiceIndex]];
}
this.list.push(item);
});
};
// 播放
play = (code: string) => {
const Stream = this.list.find((v) => v.code == code);
let StreamVoide = null;
if (ENV?.DEV) {
StreamVoide = Stream?.path;
} else {
StreamVoide = Stream?.voice?.default;
}
// 提供的条码不在列表中
if (!StreamVoide) return;
try {
const sound = new Howl({
src: [StreamVoide],
volume: 1.0,
html5: true,
onplayerror: (e) => {
console.log("error", e);
},
});
sound.play();
} catch (e) {
console.log(e);
}
};
}

export default VoiceReport;


这个倒是能放,可能不能优化呢


我首先想到的是就从播放器本身优化呢,我想着会不会是加载的延迟或者加载文件过多,想着将所有的文件进行合并,再生成sprite信息,弄是弄了,可是不论如何就是load报错,我再把这个多个mp3合并成一个文件@/assets/voice/fullStack.mp3,进行生成sprite,来加载,加载是加载上来了,可同样遇到播放错误,播放的track根本不是我期望的那个


这个是错误代码:



import config from "./config";
import "./lib/howler.min";
import fullVoice from "@/assets/voice/fullStack.mp3";
const ENV = import.meta.env;

class player {
public list: any = [];
public player: any = {};
constructor() {
this.initVoice();
}
// 目录放在@/assets/voice/ 下面
public voiceList: any = import.meta.globEager("@/assets/voice/*.mp3");
public fullVoice: any = fullVoice;
public voiceNameList = Object.keys(this.voiceList);
public sprite: any = {};
public streamVoide: any = [];
// 时间戳转换为秒
timeStringToSeconds = (timeStr: string) => {
const parts = timeStr.split(":");
const hours = parseInt(parts[0]);
const minutes = parseInt(parts[1]);
const seconds = parseInt(parts[2]);

return hours * 3600 + minutes * 60 + seconds;
}
// 初始化语音播报器列表
initVoice = () => {
config.forEach((v, index) => {
const item = {
name: `Ref${v.codeType}${v.codeKey}`,
code: v.codeKey,
codeName: v.codeName,
voice: {},
path: "",
duration: this.timeStringToSeconds(v.duration ?? 0) * 1000,
durationStart: 0,
durationEnd: 0,
};
item.durationStart = !index ? 0 : this.list[index - 1].durationEnd;
item.durationEnd = item.durationStart + item.duration;
this.sprite[v.codeKey] = [item.durationStart, item.durationEnd];
const voiceIndex = this.voiceNameList.findIndex((voice) =>
String(voice).includes(v.codeKey)
);
if (voiceIndex > -1) {
item.path = this.voiceNameList[voiceIndex];
item.voice = this.voiceList[this.voiceNameList[voiceIndex]];
}
this.list.push(item);
/* eslint-disable */
// @ts-ignore
this.streamVoide.push(ENV?.DEV ? item.path : item.voice?.default);
});
/* eslint-disable */
// @ts-ignore
this.player = new Howl({
src: this.streamVoide,
volume: 1.0,
html5: true,
sprite: this.sprite,
onplayerror: (e: any) => {
console.log("play error", e);
},
onload: (e: any) => {
console.log("error", e);
}
});
window.player = this.player;
console.log(this.sprite, this.player, fullVoice)
};
// 播放
play = (code: string) => {
try {
this.player.play(code);
} catch (e) {
console.log(e);
}
};
}

export default player;

到现在还在持续找解决方案中,
最后,不得不把希望寄托在异步任务请求导致阻塞主线程这个猜想上,因为每完成一次扫码,会发起三个请求



  • 入库请求

  • 刷新结果列表请求

  • 刷新统计请求


这么多请求一起,接口稍微一慢就有可能导致播放卡顿的问题
这个在我经过一段时间的搜索之后发现,发现webworker可以处理这个问题


web worker

根据MDN的说法

Web Workers 是 Web 内容在后台线程中运行脚本的一种简单方法。工作线程可以在不干扰用户界面的情况下执行任务。此外,他们还可以使用 fetch() 或 XMLHttpRequest API 发出网络请求。创建后,工作人员可以通过将消息发布到该代码指定的事件处理程序来向创建它的 JavaScript 代码发送消息(反之亦然)。


既然是独立于主线程之外的一个,那就不可避免的会遇到身份验证和通信的问题,对于发起的请求没有携带身份信息,这个好办,就自己在封装一个axios方法fetch,将身份信息传过去ok,这里主要贴一下worker的内容,也很简单


import type { WorkerMessageDataType } from "../types/types";
import fetch from "@/utils/fetch";
import { throttle } from "lodash";
let Ajax: any = null;

// 从主线程接受数据
self.onmessage = function (e: WorkerMessageDataType) {
console.log("Worker: 收到请求", e);
const type = e.data?.type || "";
const data = e.data?.data || {};
// 一定要初始化
if (type == "init") {
const headers: any = e.data?.headers;
Ajax = fetch(headers);
}
// 请求刷新统计数据
if (type == "refreshScanCountData") refreshScanCountData();
// 请求刷新列表扫码结果
if (type == "refreshDataList") refreshDataList();
// 请求入库
if (type == "checkAddIntoStock") checkAddIntoStock(data);
};

// 向主线程发送数据
const sedData = (type: string, data: object) => {
const param = {
type,
data: data || {},
};
self.postMessage(param);
};

// 刷新统计数据,查询统计信息api
const refreshScanCountData = throttle(() => {
Ajax({
method: "post",
url: `/api/CountStatistics`,
data: {},
}).then((res: any) => {
sedData("refreshScanCountData", res);
});
}, 500);

// 刷新扫码结果数据
const refreshDataList = throttle(() => {
Ajax({
method: "post",
url: `/api/scanToStorage/page`,
data: {},
}).then((res: any) => {
sedData("refreshDataList", res);
});
}, 500);

// 请求入库
const checkAddIntoStock = (data: { barcode: string; [x: string]: any }) => {
Ajax({
method: "post",
url: `/api/scanToStorage`,
data,
})
.then((res: any) => {
// 刷新统计数据
refreshScanCountData();
// 刷新列表
refreshDataList();
sedData("checkAddIntoStock", {
barcode: data.barcode,
...res,
status: true,
});
})
.catch(() => {
sedData("checkAddIntoStock", {
barcode: data.barcode,
status: false,
});
});
};

在主线程页面写一个方法,初始化一下这个worker


// 加载worker
const initWorker = () => {
const headers = {
Authorization: "bearer " + sessionStorage.getItem("token"),
token: sessionStorage.getItem("token"),
currRoleId: sessionStorage.getItem("roleId"),
};
// 初始化,加入身份信息
WebWorker.postMessage({ type: "init", headers });
// 从worker接受消息
WebWorker.onmessage = (e) => {
console.log("Main script: Received result", e.data);
const type = e.data?.type || "";
const data = e.data?.data || {};

// 异步更新统计信息
if (type == "refreshScanCountData") {
ScanCountData.value = data;
}
// 刷新表格数据
if (type == "refreshDataList") {
dataTable.value.updateData(data);
}
};
};


这样就可以了,即便是这样,依然还有好多问题没解决,这个是我的第一篇文章,难免有错误疏漏,这个需求并没结束,我还会持续跟进更新的


作者:kiohang
来源:juejin.cn/post/7380342160581492747
收起阅读 »

用空闲时间做了一个小程序-二维码生成器

web
一直在摸鱼中赚钱的大家好呀~ 先向各位鱼友们汇报一下情况,目前小程序已经有900+的鱼友注册使用过。虽然每天都有新的鱼友注册,但是鱼友增长的还很缓慢。自从国庆前的文字转语音的工具上线到现在已经将近有1个月没有更新小程序了。但是今天终终终终终于又有个小工具上线了...
继续阅读 »

一直在摸鱼中赚钱的大家好呀~


先向各位鱼友们汇报一下情况,目前小程序已经有900+的鱼友注册使用过。虽然每天都有新的鱼友注册,但是鱼友增长的还很缓慢。自从国庆前的文字转语音的工具上线到现在已经将近有1个月没有更新小程序了。但是今天终终终终终于又有个小工具上线了,希望这个小工具可以帮助到更多的鱼友们(没错就是你们)


这次更新的工具是一个二维码生成器,虽然很多小程序存在这个工具,但是本人也是想尝试一下实现这个工具。老规矩,先来看下知名UI设计师设计的页面。







同样在工具tab页中增加了二维码生成器模块。从UI图中可以看出第一个表单页面不是很难,就是一个文本框、两个颜色选择、一个图片上传。这个页面我在开发中也是很快就完成了,没有什么技术含量。


当我做到颜色选择弹窗的时候是想从网上找一个现成的插件。但是找了半天没有找到合适的,只能自己手动开发一个。既然要做颜色选择器的功能就要先了解一下颜色的两种格式 (我这边的实现就这两种格式)


颜色的HEX格式

颜色的HEX格式是#+六位数字/字母,其中六位数字/字母是一种十六进制的表达方式。这六位分别两个一组,从左到右分别表示绿00表示最小,十进制是0FF表示最大,十进制是255。通俗点讲,某个颜色的数值越大,包含这个颜色就越多。如:#000000-黑色、#FFFFFF-白色、#FF0000-红色、#00FF00-绿色、#0000FF-蓝色。


颜色的RGB格式

颜色的RGB格式是rgb(0-255,0-255,0-255), 其中0-255就是HEX格式的十进制表达方式。这三个数值从左到右分别表示绿0表示最小;255表示最大。通俗点讲,某个颜色的数值越大,包含这个颜色就越多。如:rgb(0,0,0)-黑色、rgb(255,255,255)-白色、rgb(255,0,0)-红色、rgb(0,255,0)-绿色、rgb(0,0,255)-蓝色。


有了上面的概念,我的思路也就出来了。让用户分别选择这三种颜色的数值,然后通过用户选择的三种颜色的数值转成目标颜色,就可以完成颜色选择的功能。思路出来了之后就告知了UI,然后按照我的思路将效果图出了出来 (没错,就是先实现后出图)。实现中主要使用了vant-ui组件库的popupslider两个组件 (聪明人都喜欢用现成的)。贴一下部分实现代码:


show="{{ show }}" 
title="展示弹出层"
position="bottom"
bind:close="cancelHandle"
custom-style="background-color: #F3F3F9;border-radius: 40rpx 40rpx 0rpx 0rpx;"
root-portal>
class="color-popup">
class="popup-header flex flex_j_c--space-between flex_a_i--center">
class="flex-item_f-1">
class="title flex-item_f-1">{{ title }}
class="flex-item_f-1 flex flex_j_c--flex-end">
name="cross" size="32rpx" bind:tap="cancelHandle" />


class="color-picker" wx:for="{{ pickers }}" wx:key="index" wx:if="{{ index !== 3 }}">
class="color-picker-label">{{ item.label }}
class="flex flex_a_i--center">
class="slider-wrap flex-item_f-1 {{ item.field }}">
value="{{ item.value }}" min="{{ 0 }}" max="{{ 255 }}" data-index="{{ index }}" bind:change="changeHandle" bind:drag="changeHandle" custom-class="slider" bar-height="60rpx" active-color="transparent" use-button-slot>
class="slider-button" slot="button">


class="slider-value">{{ item.value }}


class="color-preview-box flex flex_a_i--center">
class="preview-box-wrap">
class="preview-box" style="background-color: {{ rgbaStyle }};">
class="preview-label">颜色预览

class="presets-box-wrap flex-item_f-1 flex flex_j_c--space-between">
class="presets-box flex flex_j_c--center flex_a_i--center {{ rgbaStyle === item.rgbaStyle ? 'active' : '' }}" wx:for="{{ presets }}" wx:key="index" style="background-color: {{ item.rgbaStyle }};" data-row="{{ item }}" bind:tap="chooseHandle">
class="active-box">



class="confirm-wrap flex">
class="hex-box flex flex_a_i--center flex_j_c--space-between">
#
{{ hex }}

class="confirm-button-box flex-item_f-1">
type="primary" custom-class="confirm-button" bind:click="confirmHandle" round>确定





import { rgb2Hex } from '../../utils/util'

const presets = [
[0, 0, 0, 255], [102, 102, 102, 255],
[0, 95, 244, 255], [100, 196, 102, 255],
[247, 206, 70, 255], [235, 77, 61, 255],
]

Component({
options: {
addGlobalClass: true
},
properties: {
show: {
type: Boolean,
value: false
},
title: {
type: String,
value: ''
},
value: {
type: Array,
value: [0, 0, 0, 255],
observer: function(val) {
const { pickers } = this.data
if(val.length) {
this.setData({
pickers: pickers.map((item, index) => {
return {...item, value: val[index]}
}),
})
this.setColor(val)
} else {
this.setData({
pickers: pickers.map((item, index) => {
return {...item, value: index === 3 ? 255 : 0}
}),
})
const rgba = [0, 0, 0, 255]
this.setColor(rgba)
}
}
}
},
data: {
pickers: [
{ field: 'r', label: '红色', value: 0 },
{ field: 'g', label: '绿色', value: 0 },
{ field: 'b', label: '蓝色', value: 0 },
{ field: 'a', label: '透明度', value: 255 },
],
rgba: [],
hex: '',
rgbaStyle: '',
presets: [
...presets.map(rgba => {
return {
rgba,
rgbaStyle: `rgba(${ rgba.join(',') })`
}
})
]
},
methods: {
changeHandle(e) {
const { detail, currentTarget: { dataset: { index } } } = e
const key = `pickers[${ index }].value`
this.setData({
[key]: typeof detail === 'object' ? detail.value : detail
})
const rgba = this.data.pickers.map(item => item.value)
this.setColor(rgba)
},
chooseHandle(e) {
const { rgba } = e.currentTarget.dataset.row
this.setData({
pickers: this.data.pickers.map((item, index) => {
return {...item, value: rgba[index]}
}),
})
this.setColor(rgba)
},
// 设置颜色
setColor(rgba) {
const hex = rgb2Hex(...rgba)
const rgbaStyle = `rgba(${ rgba.join(',') })`
this.setData({ rgba, hex: hex.replace('#', ''), rgbaStyle })
},
confirmHandle(e) {
this.triggerEvent('confirm', { rgba: this.data.rgba, rgbaStyle: this.data.rgbaStyle })
},
cancelHandle() {
this.triggerEvent('cancel')
},
}
})

到此颜色选择器的组件已经实现了,还剩下一个预览下载的页面。我这边的实现并不是直接页面跳转,因为这边预览之后返回是希望还保留预览之前的数据的。如果直接离开当前页面并清除了数据,不符合用户预期的。所以使用了一个假页。微信小程序提供了一个 page-container 的页面容器,效果类似于 popup 弹出层,页面内存在该容器时,当用户进行返回操作,关闭该容器不关闭页面。


如果二维码中含中文的静态码使用微信扫描后是无法正常展示内容的(后期安排上二维码解析的功能)


感谢大家观看我今日的水文,文笔实在是不行,欢迎鱼友们给小程序提提意见,或者有什么有趣的想法也可以与楼主提一提。最后希望大家到我的小程序来多坐坐。





作者:拖孩
来源:juejin.cn/post/7384350475736989731
收起阅读 »

多级校验、工作流,这样写代码才足够优雅!

责任链模式,简而言之,就是将多个操作组装成一条链路进行处理。 请求在链路上传递,链路上的每一个节点就是一个处理器,每个处理器都可以对请求进行处理,或者传递给链路上的下一个处理器处理。 责任链模式的应用场景,在实际工作中,通常有如下两种应用场景。 操作需要经...
继续阅读 »

责任链模式,简而言之,就是将多个操作组装成一条链路进行处理。


请求在链路上传递,链路上的每一个节点就是一个处理器,每个处理器都可以对请求进行处理,或者传递给链路上的下一个处理器处理。


图片


责任链模式的应用场景,在实际工作中,通常有如下两种应用场景。



  • 操作需要经过一系列的校验,通过校验后才执行某些操作。

  • 工作流。企业中通常会制定很多工作流程,一级一级的去处理任务。


下面通过两个案例来学习一下责任链模式。


案例一:创建商品多级校验场景


以创建商品为例,假设商品创建逻辑分为以下三步完成:


①创建商品、


②校验商品参数、


③保存商品。


第②步校验商品又分为多种情况的校验,必填字段校验、规格校验、价格校验、库存校验等等。


这些检验逻辑像一个流水线,要想创建出一个商品,必须通过这些校验。如下流程图所示:


图片


图片


伪代码如下:


创建商品步骤,需要经过一系列的参数校验,如果参数校验失败,直接返回失败的结果;通过所有的参数校验后,最终保存商品信息。


图片


图片


如上代码看起来似乎没什么问题,它非常工整,而且代码逻辑很清晰。



PS:我没有把所有的校验代码都罗列在一个方法里,那样更能产生对比性,但我觉得抽象并分离单一职责的函数应该是每个程序员最基本的规范!



但是随着业务需求不断地叠加,相关的校验逻辑也越来越多,新的功能使代码越来越臃肿,可维护性较差。


更糟糕的是,这些校验组件不可复用,当你有其他需求也需要用到一些校验时,你又变成了Ctrl+C , Ctrl+V程序员,系统的维护成本也越来越高。如下图所示:


图片


图片


伪代码同上,这里就不赘述了。


终于有一天,你忍无可忍了,决定重构这段代码。


使用责任链模式优化:创建商品的每个校验步骤都可以作为一个单独的处理器,抽离为一个单独的类,便于复用。


这些处理器形成一条链式调用,请求在处理器链上传递,如果校验条件不通过,则处理器不再向下传递请求,直接返回错误信息;若所有的处理器都通过检验,则执行保存商品步骤。


图片


图片


案例一实战:责任链模式实现创建商品校验


UML图:一览众山小


图片


图片


AbstractCheckHandler表示处理器抽象类,负责抽象处理器行为。其有3个子类,分别是:



  • NullValueCheckHandler:空值校验处理器

  • PriceCheckHandler:价格校验处理

  • StockCheckHandler:库存校验处理器


AbstractCheckHandler 抽象类中, handle()定义了处理器的抽象方法,其子类需要重写handle()方法以实现特殊的处理器校验逻辑;


protected ProductCheckHandlerConfig config 是处理器的动态配置类,使用protected声明,每个子类处理器都持有该对象。


该对象用于声明当前处理器、以及当前处理器的下一个处理器nextHandler,另外也可以配置一些特殊属性,比如说接口降级配置、超时时间配置等。


AbstractCheckHandler nextHandler 是当前处理器持有的下一个处理器的引用,当前处理器执行完毕时,便调用nextHandler执行下一处理器的handle()校验方法;


protected Result next() 是抽象类中定义的,执行下一个处理器的方法,使用protected声明,每个子类处理器都持有该对象。


当子类处理器执行完毕(通过)时,调用父类的方法执行下一个处理器nextHandler。


HandlerClient 是执行处理器链路的客户端,HandlerClient.executeChain()方法负责发起整个链路调用,并接收处理器链路的返回值。


商品参数对象:保存商品的入参


ProductVO是创建商品的参数对象,包含商品的基础信息。


并且其作为责任链模式中多个处理器的入参,多个处理器都以ProductVO为入参进行特定的逻辑处理。


实际业务中,商品对象特别复杂。咱们化繁为简,简化商品参数如下:


/**
 * 商品对象
 */

@Data
@Builder
public class ProductVO {
    /**
     * 商品SKU,唯一
     */

    private Long skuId;
    /**
     * 商品名称
     */

    private String skuName;
    /**
     * 商品图片路径
     */

    private String Path;
    /**
     * 价格
     */

    private BigDecimal price;
    /**
     * 库存
     */

    private Integer stock;
}

抽象类处理器:抽象行为,子类共有属性、方法


AbstractCheckHandler:处理器抽象类,并使用@Component注解注册为由Spring管理的Bean对象,这样做的好处是,我们可以轻松的使用Spring来管理这些处理器Bean。


/**
 * 抽象类处理器
 */

@Component
public abstract class AbstractCheckHandler {

    /**
     * 当前处理器持有下一个处理器的引用
     */

    @Getter
    @Setter
    protected AbstractCheckHandler nextHandler;


    /**
     * 处理器配置
     */

    @Setter
    @Getter
    protected ProductCheckHandlerConfig config;

    /**
     * 处理器执行方法
     * @param param
     * @return
     */

    public abstract Result handle(ProductVO param);

    /**
     * 链路传递
     * @param param
     * @return
     */

    protected Result next(ProductVO param) {
        //下一个链路没有处理器了,直接返回
        if (Objects.isNull(nextHandler)) {
            return Result.success();
        }

        //执行下一个处理器
        return nextHandler.handle(param);
    }

}

在AbstractCheckHandler抽象类处理器中,使用protected声明子类可见的属性和方法。


使用 @Component注解,声明其为Spring的Bean对象,这样做的好处是可以利用Spring轻松管理所有的子类,下面会看到如何使用。


抽象类的属性和方法说明如下:



  • public abstract Result handle():表示抽象的校验方法,每个处理器都应该继承AbstractCheckHandler抽象类处理器,并重写其handle方法,各个处理器从而实现特殊的校验逻辑,实际上就是多态的思想。

  • protected ProductCheckHandlerConfig config:表示每个处理器的动态配置类,可以通过“配置中心”动态修改该配置,实现处理器的“动态编排”和“顺序控制”。配置类中可以配置处理器的名称、下一个处理器、以及处理器是否降级等属性。

  • protected AbstractCheckHandler nextHandler:表示当前处理器持有下一个处理器的引用,如果当前处理器handle()校验方法执行完毕,则执行下一个处理器nextHandler的handle()校验方法执行校验逻辑。

  • protected Result next(ProductVO param):此方法用于处理器链路传递,子类处理器执行完毕后,调用父类的next()方法执行在config 配置的链路上的下一个处理器,如果所有处理器都执行完毕了,就返回结果了。


ProductCheckHandlerConfig配置类 :


/**
 * 处理器配置类
 */

@AllArgsConstructor
@Data
public class ProductCheckHandlerConfig {
    /**
     * 处理器Bean名称
     */

    private String handler;
    /**
     * 下一个处理器
     */

    private ProductCheckHandlerConfig next;
    /**
     * 是否降级
     */

    private Boolean down = Boolean.FALSE;
}

子类处理器:处理特有的校验逻辑


AbstractCheckHandler抽象类处理器有3个子类分别是:



  • NullValueCheckHandler:空值校验处理器

  • PriceCheckHandler:价格校验处理

  • StockCheckHandler:库存校验处理器


各个处理器继承AbstractCheckHandler抽象类处理器,并重写其handle()处理方法以实现特有的校验逻辑。


NullValueCheckHandler:空值校验处理器。针对性校验创建商品中必填的参数。如果校验未通过,则返回错误码ErrorCode,责任链在此截断(停止),创建商品返回被校验住的错误信息。注意代码中的降级配置!


super.getConfig().getDown()是获取AbstractCheckHandler处理器对象中保存的配置信息,如果处理器配置了降级,则跳过该处理器,调用super.next()执行下一个处理器逻辑。


同样,使用@Component注册为由Spring管理的Bean对象,


/**
 * 空值校验处理器
 */

@Component
public class NullValueCheckHandler extends AbstractCheckHandler{

    @Override
    public Result handle(ProductVO param) {
        System.out.println("空值校验 Handler 开始...");
        
        //降级:如果配置了降级,则跳过此处理器,执行下一个处理器
        if (super.getConfig().getDown()) {
            System.out.println("空值校验 Handler 已降级,跳过空值校验 Handler...");
            return super.next(param);
        }
        
        //参数必填校验
        if (Objects.isNull(param)) {
            return Result.failure(ErrorCode.PARAM_NULL_ERROR);
        }
        //SkuId商品主键参数必填校验
        if (Objects.isNull(param.getSkuId())) {
            return Result.failure(ErrorCode.PARAM_SKU_NULL_ERROR);
        }
        //Price价格参数必填校验
        if (Objects.isNull(param.getPrice())) {
            return Result.failure(ErrorCode.PARAM_PRICE_NULL_ERROR);
        }
        //Stock库存参数必填校验
        if (Objects.isNull(param.getStock())) {
            return Result.failure(ErrorCode.PARAM_STOCK_NULL_ERROR);
        }
        
        System.out.println("空值校验 Handler 通过...");
        
        //执行下一个处理器
        return super.next(param);
    }
}

PriceCheckHandler:价格校验处理。


针对创建商品的价格参数进行校验。这里只是做了简单的判断价格>0的校验,实际业务中比较复杂,比如“价格门”这些防范措施等。


/**
 * 价格校验处理器
 */

@Component
public class PriceCheckHandler extends AbstractCheckHandler{
    @Override
    public Result handle(ProductVO param) {
        System.out.println("价格校验 Handler 开始...");

        //非法价格校验
        boolean illegalPrice =  param.getPrice().compareTo(BigDecimal.ZERO) <= 0;
        if (illegalPrice) {
            return Result.failure(ErrorCode.PARAM_PRICE_ILLEGAL_ERROR);
        }
        //其他校验逻辑...

        System.out.println("价格校验 Handler 通过...");

        //执行下一个处理器
        return super.next(param);
    }
}

StockCheckHandler:库存校验处理器。


针对创建商品的库存参数进行校验。


/**
 * 库存校验处理器
 */

@Component
public class StockCheckHandler extends AbstractCheckHandler{
    @Override
    public Result handle(ProductVO param) {
        System.out.println("库存校验 Handler 开始...");

        //非法库存校验
        boolean illegalStock = param.getStock() < 0;
        if (illegalStock) {
            return Result.failure(ErrorCode.PARAM_STOCK_ILLEGAL_ERROR);
        }
        //其他校验逻辑..

        System.out.println("库存校验 Handler 通过...");

        //执行下一个处理器
        return super.next(param);
    }
}

客户端:执行处理器链路


HandlerClient客户端类负责发起整个处理器链路的执行,通过executeChain()方法。


如果处理器链路返回错误信息,即校验未通过,则整个链路截断(停止),返回相应的错误信息。


public class HandlerClient {

  public static Result executeChain(AbstractCheckHandler handler, ProductVO param) {
      //执行处理器
      Result handlerResult = handler.handle(param);
      if (!handlerResult.isSuccess()) {
          System.out.println("HandlerClient 责任链执行失败返回:" + handlerResult.toString());
          return handlerResult;
      }
      return Result.success();
  }
}

以上,责任链模式相关的类已经创建好了。


接下来就可以创建商品了。


创建商品:抽象步骤,化繁为简


createProduct()创建商品方法抽象为2个步骤:①参数校验、②创建商品。


参数校验使用责任链模式进行校验,包含:空值校验、价格校验、库存校验等等,只有链上的所有处理器均校验通过,才调用saveProduct()创建商品方法;否则返回校验错误信息。


createProduct()创建商品方法中,通过责任链模式,我们将校验逻辑进行解耦。createProduct()创建商品方法中不需要关注都要经过哪些校验处理器,以及校验处理器的细节。


/**
 * 创建商品
 * 
@return
 */

@Test
public Result createProduct(ProductVO param) {

    //参数校验,使用责任链模式
    Result paramCheckResult = this.paramCheck(param);
    if (!paramCheckResult.isSuccess()) {
        return paramCheckResult;
    }

    //创建商品
    return this.saveProduct(param);
}

参数校验:责任链模式


参数校验paramCheck()方法使用责任链模式进行参数校验,方法内没有声明具体都有哪些校验,具体有哪些参数校验逻辑是通过多个处理器链传递的。如下:


/**
 * 参数校验:责任链模式
 * 
@param param
 * 
@return
 */

private Result paramCheck(ProductVO param) {

    //获取处理器配置:通常配置使用统一配置中心存储,支持动态变更
    ProductCheckHandlerConfig handlerConfig = this.getHandlerConfigFile();

    //获取处理器
    AbstractCheckHandler handler = this.getHandler(handlerConfig);

    //责任链:执行处理器链路
    Result executeChainResult = HandlerClient.executeChain(handler, param);
    if (!executeChainResult.isSuccess()) {
        System.out.println("创建商品 失败...");
        return executeChainResult;
    }

    //处理器链路全部成功
    return Result.success();
}

paramCheck()方法步骤说明如下:


👉 步骤1:获取处理器配置。


通过getHandlerConfigFile()方法获取处理器配置类对象,配置类保存了链上各个处理器的上下级节点配置,支持流程编排、动态扩展。


通常配置是通过Ducc(京东自研的配置中心)、Nacos(阿里开源的配置中心)等配置中心存储的,支持动态变更、实时生效。


基于此,我们便可以实现校验处理器的编排、以及动态扩展了。


我这里没有使用配置中心存储处理器链路的配置,而是使用JSON串的形式去模拟配置,大家感兴趣的可以自行实现。


/**
 * 获取处理器配置:通常配置使用统一配置中心存储,支持动态变更
 * @
return
 */

private ProductCheckHandlerConfig getHandlerConfigFile() {
    //配置中心存储的配置
    String configJson = "{"handler":"nullValueCheckHandler","down":true,"next":{"handler":"priceCheckHandler","next":{"handler":"stockCheckHandler","next":null}}}";
    //转成Config对象
    ProductCheckHandlerConfig handlerConfig = JSON.parseObject(configJson, ProductCheckHandlerConfig.class);
    return handlerConfig;
}

ConfigJson存储的处理器链路配置JSON串,在代码中可能不便于观看,我们可以使用json.cn等格式化看一下,如下,配置的整个调用链路规则特别清晰。


图片


图片


getHandlerConfigFile()类获到配置类的结构如下,可以看到,就是把在配置中心储存的配置规则,转换成配置类ProductCheckHandlerConfig对象,用于程序处理。



注意,此时配置类中存储的仅仅是处理器Spring Bean的name而已,并非实际处理器对象。



图片


图片


接下来,通过配置类获取实际要执行的处理器。


👉 步骤2:根据配置获取处理器。


上面步骤1通过getHandlerConfigFile()方法获取到处理器链路配置规则后,再调用getHandler()获取处理器。


getHandler()参数是如上ConfigJson配置的规则,即步骤1转换成的ProductCheckHandlerConfig对象;


根据ProductCheckHandlerConfig配置规则转换成处理器链路对象。代码如下:


 * 使用Spring注入:所有继承了AbstractCheckHandler抽象类的Spring Bean都会注入进来。Map的Key对应Bean的name,Value是name对应相应的Bean
 */
@Resource
private Map handlerMap;

/**
 * 获取处理器
 * 
@param config
 * 
@return
 */

private AbstractCheckHandler getHandler (ProductCheckHandlerConfig config) {
    //配置检查:没有配置处理器链路,则不执行校验逻辑
    if (Objects.isNull(config)) {
        return null;
    }
    //配置错误
    String handler = config.getHandler();
    if (StringUtils.isBlank(handler)) {
        return null;
    }
    //配置了不存在的处理器
    AbstractCheckHandler abstractCheckHandler = handlerMap.get(config.getHandler());
    if (Objects.isNull(abstractCheckHandler)) {
        return null;
    }
    
    //处理器设置配置Config
    abstractCheckHandler.setConfig(config);
    
    //递归设置链路处理器
    abstractCheckHandler.setNextHandler(this.getHandler(config.getNext()));

    return abstractCheckHandler;
}

👉 👉 步骤2-1:配置检查。


代码14~27行,进行了配置的一些检查操作。如果配置错误,则获取不到对应的处理器。代码23行handlerMap.get(config.getHandler())是从所有处理器映射Map中获取到对应的处理器Spring Bean。



注意第5行代码,handlerMap存储了所有的处理器映射,是通过Spring @Resource注解注入进来的。注入的规则是:所有继承了AbstractCheckHandler抽象类(它是Spring管理的Bean)的子类(子类也是Spring管理的Bean)都会注入进来。



注入进来的handlerMap中 Map的Key对应Bean的name,Value是name对应的Bean实例,也就是实际的处理器,这里指空值校验处理器、价格校验处理器、库存校验处理器。如下:


图片


图片


这样根据配置ConfigJson(👉 步骤1:获取处理器配置)中handler:"priceCheckHandler"的配置,使用handlerMap.get(config.getHandler())便可以获取到对应的处理器Spring Bean对象了。


👉 👉 步骤2-2:保存处理器规则。


代码29行,将配置规则保存到对应的处理器中abstractCheckHandler.setConfig(config),子类处理器就持有了配置的规则。


👉 👉 步骤2-3:递归设置处理器链路。


代码32行,递归设置链路上的处理器。


//递归设置链路处理器 abstractCheckHandler.setNextHandler(this.getHandler(config.getNext()));

这一步可能不太好理解,结合ConfigJson配置的规则来看,似乎就很很容易理解了。


图片


图片


由上而下,NullValueCheckHandler 空值校验处理器通过setNextHandler()方法设置自己持有的下一节点的处理器,也就是价格处理器PriceCheckHandler。


接着,PriceCheckHandler价格处理器,同样需要经过步骤2-1配置检查、步骤2-2保存配置规则,并且最重要的是,它也需要设置下一节点的处理器StockCheckHandler库存校验处理器。


StockCheckHandler库存校验处理器也一样,同样需要经过步骤2-1配置检查、步骤2-2保存配置规则,但请注意StockCheckHandler的配置,它的next规则配置了null,这表示它下面没有任何处理器要执行了,它就是整个链路上的最后一个处理节点。


通过递归调用getHandler()获取处理器方法,就将整个处理器链路对象串联起来了。如下:


图片


图片



友情提示:递归虽香,但使用递归一定要注意截断递归的条件处理,否则可能造成死循环哦!



实际上,getHandler()获取处理器对象的代码就是把在配置中心配置的规则ConfigJson,转换成配置类ProductCheckHandlerConfig对象,再根据配置类对象,转换成实际的处理器对象,这个处理器对象持有整个链路的调用顺序。


👉 步骤3:客户端执行调用链路。


public class HandlerClient {

  public static Result executeChain(AbstractCheckHandler handler, ProductVO param) {
      //执行处理器
      Result handlerResult = handler.handle(param);
      if (!handlerResult.isSuccess()) {
          System.out.println("HandlerClient 责任链执行失败返回:" + handlerResult.toString());
          return handlerResult;
      }
      return Result.success();
  }
}

getHandler()获取完处理器后,整个调用链路的执行顺序也就确定了,此时,客户端该干活了!


HandlerClient.executeChain(handler, param)方法是HandlerClient客户端类执行处理器整个调用链路的,并接收处理器链路的返回值。


executeChain()通过AbstractCheckHandler.handle()触发整个链路处理器顺序执行,如果某个处理器校验没有通过!handlerResult.isSuccess(),则返回错误信息;所有处理器都校验通过,则返回正确信息Result.success()


总结:串联方法调用流程


基于以上,再通过流程图来回顾一下整个调用流程。


图片


图片


测试:代码执行结果


场景1:创建商品参数中有空值(如下skuId参数为null),链路被空值处理器截断,返回错误信息


//创建商品参数
ProductVO param = ProductVO.builder()
      .skuId(null).skuName("华为手机").Path("http://...")
      .price(new BigDecimal(1))
      .stock(1)
      .build();

测试结果


图片


图片


场景2:创建商品价格参数异常(如下price参数),被价格处理器截断,返回错误信息


ProductVO param = ProductVO.builder()
      .skuId(1L).skuName("华为手机").Path("http://...")
      .price(new BigDecimal(-999))
      .stock(1)
      .build();

测试结果


图片


图片


场景 3:创建商品库存参数异常(如下stock参数),被库存处理器截断,返回错误信息。


//创建商品参数,模拟用户传入
ProductVO param = ProductVO.builder()
      .skuId(1L).skuName("华为手机").Path("http://...")
      .price(new BigDecimal(1))
      .stock(-999)
      .build();

测试结果


图片


图片


场景4:创建商品所有处理器校验通过,保存商品。


![15](C:\Users\18796\Desktop\文章\15.png)![15](C:\Users\18796\Desktop\文章\15.png)![15](C:\Users\18796\Desktop\文章\15.png)![15](C:\Users\18796\Desktop\文章\15.png)//创建商品参数,模拟用户传入
ProductVO param = ProductVO.builder()
      .skuId(1L).skuName("华为手机").Path("http://...")
      .price(new BigDecimal(999))
      .stock(1).build();

测试结果


图片


责任链的优缺点


图片


图片


作者:程序员蜗牛
来源:juejin.cn/post/7384632888321179659
收起阅读 »

dockerhub国内镜像站集体下线?别慌,教你丝滑拉取镜像~

web
前言想必大家都听说了,国内镜像站几乎都用不了,对于开发者来说,无疑是个不好的消息。在docker pull时直接超时失败,拉取不下来镜像。那么有没有什么办法解决呢?有!还不止一种。通过docker配置文件配置可用的国内镜像源设置代理自建镜像仓库方法1已经不太好...
继续阅读 »

前言

想必大家都听说了,国内镜像站几乎都用不了,对于开发者来说,无疑是个不好的消息。在docker pull时直接超时失败,拉取不下来镜像。那么有没有什么办法解决呢?有!还不止一种。

  1. 通过docker配置文件配置可用的国内镜像源
  2. 设置代理
  3. 自建镜像仓库

方法1已经不太好使了,能找到可用的不多,有的还存在没有最新的镜像问题。

方法2可行,不过得要有科学上网的工具,再会一点配置代理的知识,操作起来稍稍复杂。

本文主要介绍第三种方法,上手快,简单,关键还0成本!

准备工作

  1. 登录阿里云,找到容器镜像服务,创建一个个人版实例。(第一次使用的话,会让设置访问密码。记住,后面会用)
  2. 找到仓库管理-命名空间,新建一个命名空间且设置为公开

微信截图_20240626174632.png 3.不要创建镜像仓库,回到访问凭证

可以看到,如下2个信息,一个是你的阿里云用户名,一个是你的仓库地址(后面有用)

sudo docker login --username=阿里云用户名 registry.cn-beijing.aliyuncs.com

github配置

  1. fork项目,地址: docker_image_pusher

(感谢tech-shrimp提供的工具)

  1. 在fork后的项目中通过Settings-Secret and variables-Actions-New Repository secret路径,配置4个环境变量
  • ALIYUN_NAME_SPACE-命名空间
  • ALIYUN_REGISTRY_USER-阿里云用户名
  • ALIYUN_REGISTRY_PASSWORD-访问密码
  • ALIYUN_REGISTRY-仓库地址

企业微信截图_20240626203514.png

3.配置要拉取的镜像 打开项目images.txt,每一行配置一个镜像,格式:name:tag 比如

企业微信截图_20240626213138.png

提交修改的文件,则会自动在Actions中创建一个workflow。等待片刻即可(1分钟左右)

企业微信截图_20240626212730.png

5.回到阿里云容器镜像服务控制台-镜像仓库

企业微信截图_20240626213555.png

可以看到镜像已成功拉取并同步到你自己的仓库中。

测试效果

我自己操作了下把nginx的镜像给拉了过来,找台服务器测试一下速度

演示.gif 哈哈!这速度杠杠的吧! 用这个方式的好处是,借助github的action机制,直接从dockerhub上拉取任何你想要的镜像,也不用担心国内镜像站版本更新不及时的问题。再从自建的仓库中pull下来就可以啦! 如果有小伙伴没捣鼓成功的,可以留言给我。


作者:临时工
来源:juejin.cn/post/7384623060199473171
收起阅读 »

微信小程序全新渲染引擎Skyline(入门篇)

web
前言 最近看小程序文档的时候发现了 swiper 组件新增了 Skyline 特有的属性,直接使用竟然没有效果。 不信邪的我打算来研究研究究竟什么是 Skyline!经过一系列文档阅读与实践,长时间闭门造车的我打开了新世界的大门,我惊讶的发现 Skyline...
继续阅读 »

前言


最近看小程序文档的时候发现了 swiper 组件新增了 Skyline 特有的属性,直接使用竟然没有效果。



不信邪的我打算来研究研究究竟什么是 Skyline!经过一系列文档阅读与实践,长时间闭门造车的我打开了新世界的大门,我惊讶的发现 Skyline 引擎很可能是微信小程序未来发展的重点方向,有着更类似原生的交互体验,新增的特性让人连连称叹,特以此文来总结性地介绍一下 Skyline。


双线程模型


了解 Skyline 之前,我们有必要重新复习一下什么是小程序的双线程模型。


如官方文档所言,小程序的运行环境分成渲染层和逻辑层,其中 WXML 模板和 WXSS 样式工作在渲染层,JS 脚本工作在逻辑层。小程序的渲染层和逻辑层分别由2个线程管理:



  • 渲染层的界面使用了WebView 进行渲染,一个小程序存在多个界面,所以渲染层存在多个WebView线程;

  • 逻辑层采用JsCore线程运行JS脚本。


这两个线程的通信会经由微信客户端(原生) 做中转,逻辑层发送网络请求也经由微信客户端 (原生) 转发,有了微信小程序客户端 (原生) 作为媒介系统,使得我们开发者能够专注于数据与逻辑。


如上所述,小程序的通信模型如下图所示。



什么是 Skyline 引擎


前文提到,基于 WebView 和原生控件混合渲染的方式,小程序优化扩展了 Web 的基础能力,所以小程序相对于普通的Web页面有着更为良好的性能与体验。


由于 Web 在移动端的表现与原生应用仍有一定差距,亦或许是 Web 的优化遇到了瓶颈,为了进一步优化小程序性能,微信在 WebView 渲染之外新增了一个渲染引擎,也就是我们本文的重磅主角: Skyline,它使用更精简高效的渲染管线,并带来诸多增强特性,让 Skyline 拥有更接近原生渲染的性能体验。


Skyline 引擎 vs Webview 引擎


我们知道:WebView 的 JS 逻辑、DOM 树创建、CSS 解析、样式计算、Layout、Paint (Composite) 都发生在同一线程,在 WebView 上执行过多的 JS 逻辑可能阻塞渲染,导致界面卡顿,大致流程如下图所示。



但是,在 Skyline 环境下改变了这个情况,它创建了一条渲染线程来负责计算图层布局,图层的绘制以及整合图层页面等渲染任务,并在 AppService 中划出一个独立的上下文,来运行之前 WebView 承担的 JS 逻辑、DOM 树创建等逻辑。



据官方统计数据表明,Skyline 与 WebView 性能相比,具有如下优势:


Skyline 的首屏时间比 WebView 快约 66%



单个页面 Skyline 的占用比 WebView 减少约 35%


单个页面 Skyline 的占用比 WebView 减少 35%,两个页面 Skyline 的内存占用比 WebView 减少 50%,随着打开的页面变多,内存差距越明显。



Skyline 引擎的优点



  • 界面更不容易被逻辑阻塞,进一步减少卡顿

  • 无需为每个页面新建一个 JS 引擎实例(WebView),减少了内存、时间开销

  • 框架可以在页面之间共享更多的资源,进一步减少运行时内存、时间开销

  • 框架的代码之间无需再通过 JSBridge 进行数据交换,减少了大量通信时间开销

  • 保持和原有架构的兼容性,基于 WebView 环境的小程序代码基本上无需任何改动即可直接在新的架构下运行


更多Skyline的特性更新请详见Skyline 渲染引擎 / 概览 / 特性 | 微信开放文档


Skyline 引擎的缺点



  • WXS效率可能有所下降 (WXS 由于被移到 AppService 中,虽然逻辑本身无需改动,但询问页面信息等接口会变为异步,效率也可能有所下降)


但是,也不必过多的担心,微信推出了新的 Worklet 机制,它比原有的 WXS 更靠近渲染流程,用以高性能地构建各种复杂的动画效果。


Skyline 引擎的使用


前文提到,我想使用 swiper 组件新增的 Skyline 特有属性无果,是因为我没有完成 Skyline的配置。如果想要使用 Skyline引擎,我们可以按页面级别来选择性的配置是走 Skyline 引擎或是 Webview 引擎来渲染。


// page.json
{
"renderer": "skyline"
}

// page.json
{
"renderer": "webview"
}

配置完成之后,我们就可以愉快的使用 Skyline 专有的新特性了。


Skyline 引擎的兼容性


我们可能会担心开启了 Skyline 的渲染模式会不会带来兼容性问题。官方表示:



所以我们完全可以放下对兼容性的顾虑,拥抱新的 Skyline 引擎,让大部分的用户优先体验到新一代微信小程序的渲染技术,做第一批吃螃蟹的人!对于我们开发者而言,有必要深入了解一下Skyline引擎的更新带来了哪些开发层面的变化与创新,毕竟,吃螃蟹的人会越来越多嘛。


后记


感谢您的阅读,本文仅为微信小程序 Skyline 引擎的入门介绍篇,后续会持续更新有关 Skyline 引擎相关实际操作及使用的文章,如有兴趣,欢迎持续关注。


作者:阿李贝斯
来源:juejin.cn/post/7298927261210361882
收起阅读 »

2024年令人眼前一亮的Web框架

web
本文翻译自 dev.to/wasp/web-fr… 感谢您的阅读! 介绍 2024年正向我们走来,我们怀着满腔热情为新的一年制定计划,探索未来一年可以学习或实现的目标。此时此刻,正是探寻来年值得学习的框架、理解其功能和特色的最佳时刻。我们以2023年JS 新...
继续阅读 »

本文翻译自 dev.to/wasp/web-fr…

感谢您的阅读!



介绍


2024年正向我们走来,我们怀着满腔热情为新的一年制定计划,探索未来一年可以学习或实现的目标。此时此刻,正是探寻来年值得学习的框架、理解其功能和特色的最佳时刻。我们以2023年JS 新星名单为指引,力求保持客观公正的态度。对于每一个特色框架,我们都将突出其最大的优势,使您能够全面理解它们的优点,从而选择适合自己的框架进行尝试!


HTMX - 返璞归真🚲


htmx-演示


为谁而设:



  • 你希望减少JavaScript的编写量

  • 你希望代码更简单,以超媒体为中心


HTMX在2023年迅速走红,过去一年间在GitHub上赢得了大量星标。HTMX并非普通的JS框架。如果你使用HTMX,你将大部分时间都花在超媒体的世界中,以与我们通常对现代Web开发的JS密集型视角完全不同的视角看待Web开发。HTMX利用HATEOAS(Hypermedia作为应用程序状态的引擎)的概念,使开发人员能够直接从HTML访问浏览器功能,而不是使用Javascript。


此外,它还证明了通过发布令人惊叹的表情符号并以口碑作为主要营销手段,你可以获得人气和认可。不仅如此,你还可能成为HTMX的CEO!它吸引了许多开发人员尝试这种构建网站的方法,并重新思考他们当前的实践。所有这些都使2024年对于这个库的未来发展充满了激动人心的可能性。


Wasp - 全栈,开箱即用🚀


开放SaaS


为谁而设:



  • 你希望快速构建全栈应用

  • 你希望在一个出色的一体化解决方案中继续使用React和Node.js,而无需手动挑选堆栈的每一部分

  • 你希望获得一个为React和Node.js预配置的免费SaaS模板—— Open SaaS


对于希望简单轻松地全面控制其堆栈的工具的用户,无需再寻找!Wasp是一个有主见的全栈框架,利用其编译器以快速简便的方式为你的应用创建数据库、后端和前端。它使用React、Node.js和Prisma,这些都是全栈Web开发人员正在使用的一些最著名的工具。


Wasp的核心是main.wasp文件,它作为你大部分需求的一站式服务。在其中,你可以定义:



  • 全栈身份验证

  • 数据库架构

  • 异步作业,无需额外的基础设施

  • 简单且灵活的部署

  • 全栈类型安全

  • 发送电子邮件(Sendgrid、MailGun、SMTP服务器等)

  • 等等……


最酷的事情是?经过编译器步骤后,你的Wasp应用程序的输出是一个标准的React + Vite前端、Node.js后端和PostgreSQL数据库。从那里,你可以使用单个命令轻松将一切部署到Fly.io等平台。


尽管有些人可能会认为Wasp的有主见立场是负面的,但它却是Wasp众多全栈功能的驱动力。使用Wasp,单个开发人员或小型团队启动全栈项目变得更加容易,尤其是如果你使用预制的模板或OpenSaaS作为你的SaaS起点。由于项目的核心是定义明确的,因此开始一个项目并可能在几天内创建自己的全栈SaaS变得非常容易!


此外,还有一点很酷的是,大多数Web开发人员对大多数现有技术的预先存在的知识仍然在这里适用,因为Wasp使用的技术已经成熟。


Solid.js - 一流的reactivity库 ↔️


扎实的例子


适合人群:



  • 如果你希望代码具有高响应性

  • 现有的React开发人员,希望尝试一种对他们来说学习曲线较低的高性能工具


Solid.js是一个性能很高的Web框架,与React有一些相似之处。例如,两者都使用JSX,采用基于函数的组件方法,但Solid.js不使用虚拟DOM,而是将你的代码转换为纯JavaScript。然而,Solid.js因其利用信号、备忘录和效果实现细粒度响应性的方法而更加出名。信号是Solid.js中最简单、最知名的基本元素。它们包含值及其获取和设置函数,使框架能够观察并在DOM中的确切位置按需更新更改,这与React重新渲染整个组件的方式不同。


Solid.js不仅使用JSX,还对其进行了增强。它提供了一些很酷的新功能,例如Show组件,它可以启用JSX元素的条件渲染,以及For组件,它使在JSX中更轻松地遍历集合变得更容易。另一个重要的是,它还有一个名为Solid Start的元框架(目前处于测试版),它使用户能够根据自己的喜好,使用基于文件的路由、操作、API路由和中间件等功能,以不同的方式渲染应用程序。


Astro - 静态网站之王👑


天文示例


适合人群:



  • 如果您需要一款优秀的博客、CMS重型网站工具

  • 需要一个能够集成其他库和框架的框架


如果您在2023年构建了一个内容驱动的网站,那么很有可能您选择了Astro作为首选框架来实现这一目标!Astro是另一个使用不同架构概念来脱颖而出的框架。对于Astro来说,这是岛屿架构。在Astro的上下文中,岛屿是页面上的任何交互式UI组件,与静态内容的大海形成鲜明对比。由于这些岛屿彼此独立运行,因此页面可以有任意数量的岛屿,但它们也可以共享状态并相互通信,这非常有用。


关于Astro的另一个有趣的事情是,他们的方法使用户能够使用不同的前端框架,如React、Vue、Solid来构建他们的网站。因此,开发人员可以轻松地在其当前知识的基础上构建网站,并利用可以集成到Astro网站中的现有组件。


Svelte - 简单而有效🎯


精简演示


适合人群:



  • 您希望学习一个简单易上手的框架

  • 追求简洁且代码执行速度快的开发体验


Svelte是另一个尝试通过尽可能直接和初学者友好的方式来简化和加速Web开发的框架。它是一个很容易学习的框架,因为要使一个属性具有响应性,您只需声明它并在HTML模板中使用它。 每当在JavaScript中程序化地更新值时(例如,通过触发onClick事件按钮),它将在UI上反映出来,反之亦然。


Svelte的下一步将是引入runes。runes将是Svelte处理响应性的方式,使处理大型应用程序变得更加容易。类似于Solid.js的信号,符文通过使用类似函数的语句提供了一种直接访问应用程序响应性状态的方式。与Svelte当前的工作方式相比,它们将允许用户精确定义整个脚本中哪些部分是响应性的,从而使组件更加高效。类似于Solid和Solid Start,Svelte也有其自己的框架,称为SvelteKit。SvelteKit为用户提供了一种快速启动其由Vite驱动的Svelte应用程序的方式。它提供了路由器、构建优化、不同的渲染和预渲染方式、图像优化等功能。


Qwik - 非常快🚤


qwik演示


适合人群:



  • 如果您想要一个高性能的Web应用

  • 现有的React开发人员,希望尝试一种高性能且学习曲线平缓的框架


最后一个但同样重要的框架是Qwik。Qwik是另一个利用JSX和函数组件的框架,类似于Solid.js,为基于React的开发人员提供了一个熟悉的环境,以便尽快上手。正如其名字所表达的,Qwik的主要目标是实现您应用程序的最高性能和最快执行速度。


Qwik通过利用可恢复性(resumability)的概念来实现其速度。简而言之,可恢复性基于在服务器上暂停执行并在客户端上恢复执行而无需重新播放和下载全部应用程序逻辑的想法。这是通过延迟JavaScript代码的执行和下载来实现的,除非有必要处理用户交互,这是一件非常棒的事情。它使整体速度提高,并将带宽降低到绝对最小值,从而实现近乎瞬间的加载。


结论


在我们所提及的所有框架和库中,最大的共同点是它们的熟悉度。每个框架和库都试图以构建在当前知识基础上的方式吸引潜在的新开发者,而不是做一些全新的事情,这是一个非常棒的理念。


当然,还有许多我们未在整篇文章中提及但值得一提的库和框架。例如,Angular 除了新的标志和文档外,还包括信号和新的控制流。还有 Remix,它增加了对 Vite、React Server Components 和新的 Remix SPA 模式的支持。最后,我们不能忘记 Next.js,它在过去几年中已成为 React 开发者的默认选择,为新的 React 功能铺平了道路。


作者:腾讯TNTWeb前端团队
来源:juejin.cn/post/7339830464000213027
收起阅读 »

我们都被困在系统里

前言 Hi 你好,我是东东拿铁,一个正在探索个人IP的后端程序员。 2020年外卖最火热的时候,有一篇文章《外卖骑手,困在系统里》。 作为一个互联网从业人员,我之前从未有机会体会到,当每一个工作都要被时间和算法压榨时,我会是一种怎样的感受。 而最近的一段经历...
继续阅读 »

前言


Hi 你好,我是东东拿铁,一个正在探索个人IP的后端程序员。


2020年外卖最火热的时候,有一篇文章《外卖骑手,困在系统里》。



作为一个互联网从业人员,我之前从未有机会体会到,当每一个工作都要被时间和算法压榨时,我会是一种怎样的感受。


而最近的一段经历,我感觉也被困在系统里了。


起因


如果你是一个研发人员,免不了要值班、处理线上问题。当然这都很正常,每个系统都有bug或者咨询类的问题。


由于我们面临的客户比较多,加上系统有一些易用性的问题或bug,提出来的问题不少。


公司有一项政策,当客服人员提交工单之后,系统对每一个单子有超时时间,如果超出一定时间你还未提交,罚款50元。


挺奇葩的,谁能保证1个小时就一定能排查出问题呢?


于是就会有一个场景,如果赶上问题多,一下子来5、6个工单,恰巧遇到不容易排查的耽误时间的话,处理后面的工单,都面临着超时的压力。



之前同事们对值班这件事,充满了怨言,大多都会吐槽几个点



  1. 系统bug太多了,又是刚刚某某需求改出来的问题

  2. 需求设计不合理,很多奇怪的操作导致了系统问题

  3. 客服太懒了,明明可以自己搜,非得提个工单问

  4. 基础设施差,平台不好用


我不太爱吐槽,但当工单一下子来的太多的时候,我不由自主的陷入机械的处理问题中,压缩思考的时间,只求不要超时就好。


明明系统有很多问题需要解决、流程也有很多可以优化,可当系统给到我们的压力越来越多时,我们便不去思考,陷入只有吐槽、怨言和避免罚款的状态。


当陷入了系统的支配,只能被动接受,甚至有了一些怨言的时候,我意识到,这样的状态,是有问题的。


被困住的打工人


外卖员为什么不遵守交通规则呢?


外卖小哥为了多赚钱、避免处罚,我之前也很不理解,为什么为了避免处罚,连自己的生命安全都可以置之不顾。



但转念一想,我们虽然不用在马路上奔波,可受到“系统”的压力,可是一点也不比外卖员少。


大家一定有过类似的经历:你骑车或者开车去上班,距离打卡时间所剩无几,你在迟到的边缘疯狂试探,可能多一个红绿灯,你就赶不上了,这时候你会不会狠踩几脚油门、闯一个黄灯,想要更快一点呢?


但随着裁员、降本增效、各类指标的压力越来越大,我们被迫不停的内卷,不断压榨自己,才能满足职场要求越来越严格的“算法”,比如,每半年一次的绩效考核,月度或者季度的OKR、KPI,还有处理不完的线上问题、事故,充斥在我们的脑海里面。


其实我们何尝不是“外卖员”呢?外卖员是为了不被扣钱,我们是为了年终奖、晋升罢了。


所以回过头来看,其实我们早早的就被困在“系统”中了,为了满足系统的要求,我们不得不埋头苦干,甚至加班透支身体,作出很多非常短线思维的事情。


但为什么,我之前从来没有过被困住的感觉,为什么我现在才回过神来,意识到这个问题呢?


我想,大概是越简单的事情,你作出的反应就越快、越激烈。而越复杂、时间越长的事情,你作出的反应就越缓慢,甚至忽略掉。


比如上班即将迟到的你,你会立刻意识到,迟到可能会受到处罚。但是年终评估你的绩效目标时,你或许只有在最后的几个月才会意识到,某某事情没完成,年终奖或许要少几个月而感到着急。


积极主动


最近正好在读《高效能人士的七个习惯》,其中第一个习惯就是积极主动


书中说到:人性的本质是主动而非被动的,人类不仅能针对特定环境选择回应方式,更能主动创造有利的环境。


我们面对的问题可以分为三类:



  • 可直接控制的(问题与自身的行为有关)

  • 可间接控制的(问题与他人的行为有关)

  • 无法控制的(我们无能为力的问题,例如我们的过去或现实的环境)


对于这三类问题,积极主动的话,应该如何加以解决呢。


可直接控制的问题


针对可直接控制的问题,可以通过培养正确习惯来解决。


从程序员角度来看,线上bug多,可以在开发前进行技术设计,上线前进行代码CR,自动化测试,帮助自己避免低级的问题。


面对处理工单时咨询量特别多的问题,随手整理个文档出来,放到大家都可以看到的地方。


可间接控制的


对于可间接控制的,我们可以通过改进施加影响的方法来解决。


比如流程机制的不合理,你可以通过向上反馈的方式施加影响,提出自己的建议而不是吐槽。


无法控制的


对于无法控制的,我们要做的就是改变面部曲线,以微笑、真诚与平和来接受现实。


虽然反馈问题的人或许能力参差不齐,导致工单量很多,但我们意识到这一点是无法避免的,不如一笑而过,这样才不至于被问题左右。


说在最后


好了,文章到这里就要结束了。


最近由于值班的原因,陷入了一段时间的无效忙碌中,每一天都很累,几乎抽不出时间来思考,所以更新的频率也降下来了。


但还好,及时的意识到问题,把最近的一点思考分享出来,希望我们每个人都不会被“系统”困住。




作者:东东拿铁
来源:juejin.cn/post/7385098943942656054
收起阅读 »

第一次使用缓存,因为没预热,翻车了

缓存不预热会怎么样?我帮大家淌了路。缓存不预热会导致系统接口性能下降,数据库压力增加,更重要的是导致我写了两天的复盘文档,在复盘会上被骂出了翔。 悲惨的上线时刻 事情发生在几年前,我刚毕业时,第一次使用缓存内心很激动。需求场景是虚拟商品页面需要向用户透出库存状...
继续阅读 »

缓存不预热会怎么样?我帮大家淌了路。缓存不预热会导致系统接口性能下降,数据库压力增加,更重要的是导致我写了两天的复盘文档,在复盘会上被骂出了翔。


悲惨的上线时刻


事情发生在几年前,我刚毕业时,第一次使用缓存内心很激动。需求场景是虚拟商品页面需要向用户透出库存状态,提单时也需要校验库存状态是否可售卖。但是由于库存状态的计算包含较复杂的业务逻辑,耗时比较高,在500ms以上。如果要在商品页面透出库存状态那么商品页面耗时增加500ms,这几乎是无法忍受的事情。


如何实现呢?最合适的方案当然是缓存了,我当时设计的方案是如果缓存有库存状态直接读缓存,如果缓存查不到,则计算库存状态,然后加载进缓存,同时设定过期时间。何时写库存呢? 答案是过期后,cache miss时重新加载进缓存。 由于计算逻辑较复杂,库存扣减等用户写操作没有同步更新缓存,但是产品认可库存状态可以有几分钟的状态不一致。为什么呢?


因为仓库有冗余库存,就算库存状态不一致导致超卖,也能容忍。同时库存不足以后,需要运营补充库存,而补充库存的时间是肯定比较长的。虽然补充库存完成几分钟后,才变为可售卖的,产品也能接受。 梳理完缓存的读写方案,我就沉浸于学习Redis的过程。


第一次使用缓存,我把时间和精力都放在Redis存储结构,Redis命令,Redis为什么那么快等方面的关注。如饥似渴的学习Redis知识。


直到上线阶段我也没有意识到系统设计的缺陷。


代码写的很快,测试验证也没有问题。然而上线过程中,就开始噼里啪啦的报警,开始我并没有想到报警这事和我有关。直到有人问我,“XXX,你是不是在上线库存状态的需求?”。


我人麻了,”怎么了,啥事”,我颤抖的问


“商品页面耗时暴涨,赶紧回滚”。一个声音传来


“我草”,那一瞬间,我的血压上涌,手心发痒,心跳加速,头皮发麻,颤抖的手不知道怎么在发布系统点回滚,“我没回滚过啊,咋回滚啊?”


“有降级开关吗”? 一个声音传来。


"没写..."。我回答的时候觉得自己真是二笔,为啥没加降级啊。(这也是复盘被骂的重要原因)


那么如何对缓存进行预热呢?


如何预热缓存


灰度放量


灰度放量实际上并不是缓存预热的办法,但是确实能避免缓存雪崩的问题。例如这个需求场景中,如果我没有放开全量数据,而是选择放量1%的流量。这样系统的性能不会有较大的下降,并且逐步放量到100%。


虽然这个过程中,没有主动同步数据到缓存,但是通过控制放量的节奏,保证了初始化缓存过程中,不会出现较大的耗时波动。


例如新上线的缓存逻辑,可以考虑逐渐灰度放量。


扫描数据库刷缓存


如果缓存维度是商品维度或者用户维度,可以考虑扫描数据库,提前预热部分数据到缓存中。


开发成本较高。除了开发缓存部分的代码,还需要开发扫描全表的任务。为了控制缓存刷新的进度,还需要使用线程池增加并发,使用限流器限制并发。这个方案的开发成本较高。


通过数据平台刷缓存


这是比较好的方式,具体怎么实现呢?


数据平台如果支持将数据库离线数据同步到Hive,Hive数据同步到Kafka,我们就可以编写Hive SQL,建立ETL任务。把业务需要被刷新的数据同步到Kafka中,再消费Kafka,把数据写入到缓存中。在这个过程中通过数据平台控制并发度,通过Kafka 分片和消费线程并发度控制 缓存写入的速率。


这个方案开发逻辑包括ETL 任务,消费Kafka写入缓存。这两部分的开发工作量不大。并且相比扫描全表任务,ETL可以编写更加复杂的SQL,修改后立即上线,无需自己控制并发、控制限流。在多个方面ETL刷缓存效率更高。


但是这个方案需要公司级别支持 多个存储系统之间可以进行数据同步。例如mysql、kafka、hive等。


除了首次上线,是否还有其他场景需要预热缓存呢?


需要预热缓存的其他场景


如果Redis挂了,数据怎么办


刚才提到上线前,一定要进行缓存预热。还有一个场景:假设Redis挂了,怎么办?全量的缓存数据都没有了,全部请求同时打到数据库,怎么办。


除了首次上线需要预热缓存,实际上如果缓存数据丢失后,也需要预热缓存。所以预热缓存的任务一定要开发的,一方面是上线前预热缓存,同时也是为了保证缓存挂掉后,也能重新预热缓存。


假如有大量数据冷启动怎么办


假如促销场景,例如春节抢红包,平时非活跃用户会在某个时间点大量打开App,这也会导致大量cache miss,进而导致雪崩。 此时就需要提前预热缓存了。具体的办法,可以考虑使用ETL任务。离线加载大量数据到Kafka,然后再同步到缓存。


总结



  1. 一定要预热缓存,不然线上接口性能和数据库真的扛不住。

  2. 可以通过灰度放量,扫描全表、ETL数据同步等方式预热缓存

  3. Redis挂了,大量用户冷启动的促销场景等场景都需要提前预热缓存。


作者:五阳
来源:juejin.cn/post/7277461864349777972
收起阅读 »

半夜被慢查询告警吵醒,limit深度分页的坑

故事梅雨季,闷热的夜,令人窒息,窗外一道道闪电划破漆黑的夜幕,小猫塞着耳机听着恐怖小说,辗转反侧,终于睡意来了,然而挨千刀的手机早不振晚不振,偏偏这个时候振动了一下,一个激灵,没有按捺住对内容的好奇,点开了短信,卧槽?告警信息,原来是负责的服务出现慢查询了。小...
继续阅读 »

故事

梅雨季,闷热的夜,令人窒息,窗外一道道闪电划破漆黑的夜幕,小猫塞着耳机听着恐怖小说,辗转反侧,终于睡意来了,然而挨千刀的手机早不振晚不振,偏偏这个时候振动了一下,一个激灵,没有按捺住对内容的好奇,点开了短信,卧槽?告警信息,原来是负责的服务出现慢查询了。小猫想起来,今天在下班之前上线了一个版本,由于新增了一个业务字段,所以小猫写了相关的刷数据的接口,在下班之前调用开始刷历史数据。

考虑到表的数据量比较大,一次性把数据全部读取出来然后在内存里面去刷新数据肯定是不现实的,所以小猫采用了分页查询的方式依次根据条件查询出结果,然后进行表数据的重置。没想到的是,数据量太大,分页的深度越来越深,渐渐地,慢查询也就暴露出来了。

慢查询告警

强迫症小猫瞬间睡意全无,翻起来打开电脑开始解决问题。

那么为什么用使用limit之后会出现慢查询呢?接下来老猫和大家一起来剖析一下吧。

剖析流程

limit分页为什么会变慢?

在解释为什么慢之前,咱们来重现一下小猫的慢查询场景。咱们从实际的例子推进。

做个小实验

假设我们有一张这样的业务表,商品Product表。具体的建表语句如下:

CREATE TABLE `Product` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`type` tinyint(3) unsigned NOT NULL DEFAULT '1' ,
`spuCode` varchar(50) NOT NULL DEFAULT '' ,
`spuName` varchar(100) NOT NULL DEFAULT '' ,
`spuTitle` varchar(300) NOT NULL DEFAULT '' ,
`channelId` bigint(20) unsigned NOT NULL DEFAULT '0',
`sellerId` bigint(20) unsigned NOT NULL DEFAULT '0'
`mallSpuCode` varchar(32) NOT NULL DEFAULT '',
`originCategoryId` bigint(20) unsigned NOT NULL DEFAULT '0' ,
`originCategoryName` varchar(50) NOT NULL DEFAULT '' ,
`marketPrice` decimal(10,2) unsigned NOT NULL DEFAULT '0.00',
`status` tinyint(3) unsigned NOT NULL DEFAULT '1' ,
`isDeleted` tinyint(3) unsigned NOT NULL DEFAULT '0',
`timeCreated` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`timeModified` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) ,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `uk_spuCode` (`spuCode`,`channelId`,`sellerId`),
KEY `idx_timeCreated` (`timeCreated`),
KEY `idx_spuName` (`spuName`),
KEY `idx_channelId_originCategory` (`channelId`,`originCategoryId`,`originCategoryName`) USING BTREE,
KEY `idx_sellerId` (`sellerId`)
) ENGINE=InnoDB AUTO_INCREMENT=12553120 DEFAULT CHARSET=utf8mb4 COMMENT='商品表'

从上述建表语句中我们发现timeCreated走普通索引。 接下来我们根据创建时间来执行一下分页查询:

当为浅分页的时候,如下:

select * from Product where timeCreated > "2020-09-12 13:34:20" limit 0,10

此时执行的时间为: "executeTimeMillis":1

当调整分页查询为深度分页之后,如下:

select * from Product where timeCreated > "2020-09-12 13:34:20" limit 10000000,10

此时深度分页的查询时间为: "executeTimeMillis":27499

此时看到这里,小猫的场景已经重现了,此时深度分页的查询已经非常耗时。

剖析一下原因

简单回顾一下普通索引和聚簇索引

我们来回顾一下普通索引和聚簇索引(也有人叫做聚集索引)的关系。

大家可能都知道Mysql底层用的数据结构是B+tree(如果有不知道的伙伴可以自己了解一下为什么mysql底层是B+tree),B+tree索引其实可以分为两大类,一类是聚簇索引,另外一类是非聚集索引(即普通索引)。

(1)聚簇索引:InnoDB存储表是索引组织表,聚簇索引就是一种索引组织形式,聚簇索引叶子节点存放表中所有行数据记录的信息,所以经常会说索引即数据,数据即索引。当然这个是针对聚簇索引。

02.png

由图可知在执行查询的时候,从根节点开始共经历了3次查询即可找到真实数据。倘若没有聚簇索引的话,就需要在磁盘上进行逐个扫描,直至找到数据为止。显然,索引会加快查询速度,但是在写入数据的时候,由于需要维护这颗B+树,因此在写入过程中性能也会下降。

(2)普通索引:普通索引在叶子节点并不包含所有行的数据记录,只是会在叶子节点存本身的键值和主键的值,在检索数据的时候,通过普通索引子节点上的主键来获取想要找到的行数据记录。

03.png

由图可知流程,首先从非聚簇索引开始寻找聚簇索引,找到非聚簇索引上的聚簇索引后,就会到聚簇索引的B+树上进行查询,通过聚簇索引B+树找到完整的数据。该过程比较专业的叫法也被称为“回表”。

看一下实际深度分页执行过程

有了以上的知识基础我们再来回过头看一下上述深度分页SQL的执行过程。 上述的查询语句中idx_timeCreated显然是普通索引,咱们结合上述的知识储备点,其深度分页的执行就可以拆分为如下步骤:

1、通过普通索引idx_timeCreated,过滤timeCreated,找到满足条件的记录ID;

2、通过ID,回到主键索引树,找到满足记录的行,然后取出展示的列(回表);

3、扫描满足条件的10000010行,然后扔掉前10000000行,返回。

结合看一下执行计划:

04.png

原因其实很清晰了: 显然,导致这句SQL速度慢的问题出现在第2步。其中发生了10000010次回表,这前面的10000000条数据完全对本次查询没有意义,但是却占据了绝大部分的查询时间。

再深入一点从底层存储来看,数据库表中行数据、索引都是以文件的形式存储到磁盘(硬盘)上的,而硬盘的速度相对来说要慢很多,存储引擎运行sql语句时,需要访问硬盘查询文件,然后返回数据给服务层。当返回的数据越多时,访问磁盘的次数就越多,就会越耗时。

替换limit分页的一些方案。

上述我们其实已经搞清楚深度分页慢的原因了,总结为“无用回表次数过多”。

那怎么优化呢?相信大家应该都已经知道了,其核心当然是减少无用回表次数了。

有哪些方式可以帮助我们减少无用回表次数呢?

子查询法

思路:如果把查询条件,转移回到主键索引树,那就不就可以减少回表次数了。 所以,咱们将实际的SQL改成下面这种形式:

select * FROM Product where id >= (select p.id from Product p where p.timeCreated > "2020-09-12 13:34:20" limit 10000000, 1) LIMIT 10;

测试一下执行时间: "executeTimeMillis":2534

我们可以明显地看到相比之前的27499,时间整整缩短了十倍,在结合执行计划观察一下。

05.png

我们综合上述的执行计划可以看出,子查询 table p查询是用到了idx_timeCreated索引。首先在索引上拿到了聚集索引的主键ID,省去了回表操作,然后第二查询直接根据第一个查询的 ID往后再去查10个就可以了!

显然这种优化方式是有效的。

使用inner join方式进行优化

这种优化的方式其实和子查询优化方法如出一辙,其本质优化思路和子查询法一样。 我们直接来看一下优化之后的SQL:

select * from Product p1 inner join (select p.id from Product p where p.timeCreated > "2020-09-12 13:34:20" limit 10000000,10) as p2 on p1.id = p2.id

测试一下执行的时间: "executeTimeMillis":2495

06.png

咱们发现和子查询的耗时其实差不多,该思路是先通过idx_timeCreated二级索引树查询到满足条件的主键ID,再与原表通过主键ID内连接,这样后面直接走了主键索引了,同时也减少了回表。

上面两种方式其核心优化思想都是减少回表次数进行优化处理。

标签记录法(锚点记录法)

我们再来看下一种优化思路,上述深度分页慢原因我们也清楚了,一次性查询的数据太多也是问题,所以我们从这个点出发去优化,每次查询少量的数据。那么我们可以采用下面那种锚点记录的方式。类似船开到一个地方短暂停泊之后继续行驶,那么那个停泊的地方就是抛锚的地方,老猫喜欢用锚点标记来做比方,当然看到网上有其他的小伙伴称这种方式为标签记录法。其实意思也都差不多。

这种方式就是标记一下上次查询到哪一条了,下次再来查的时候,从该条开始往下扫描。我们直接看一下SQL:

select * from Product p where p.timeCreated > "2020-09-12 13:34:20" and id>10000000 limit 10

显然,这种方式非常快,耗时如下: "executeTimeMillis":1

但是这种方式显然是有缺陷的,大家想想如果我们的id不是连续的,或者说不是自增形式的,那么我们得到的数据就一定是不准确的。与此同时咱们也不能跳页查看,只能前后翻页。

当然存在相同的缺陷,我们还可以换一种写法。

select * from Product p where p.timeCreated > "2020-09-12 13:34:20" and id between 10000000 and 10000010  

这种方式也是一样存在上述缺陷,另外的话更要注意的是between ...and语法是两头都是闭区域间。上述语句如果ID连续不断地情况下,咱们最终得到的其实是11条数据,并不是10条数据,所以这个地方还是需要注意的。

存入到es中

上述罗列的几种分页优化的方法其实已经够用了,那么如果数据量再大点的话咋整,那么我们可能就要选择其他中间件进行查询了,当然我们可以选择es。那么es真的就是万能药吗?显然不是。ES中同样存在深度分页的问题,那么针对es的深度分页,那么又是另外一个故事了,这里咱们就不展开了。

写到最后

那么半夜三更爬起来优化慢查询的小猫究竟有没有解决问题呢?电脑前,小猫长吁了一口气,解决了! 我们看下小猫的优化方式:

select * from InventorySku isk inner join (select id from InventorySku where inventoryId = 6058 limit 109500,500 ) as d on isk.id = d.id

显然小猫采用了inner join的优化方法解决了当前的问题。

相信小伙伴们后面遇到这类问题也能搞定了。


作者:程序员老猫
来源:juejin.cn/post/7384652811554308147
收起阅读 »

零成本搭建个人图床服务器

前言 图床服务器是一种用于存储和管理图片的服务器,可以给我们提供将图片上传后能外部访问浏览的服务。这样我们在写文章时插入的说明图片,就可以集中放到图床里,既方便多平台文章发布,又能统一管理和备份。 当然下面通过在 GitHub 上搭建的图床,不光不用成本,而且...
继续阅读 »

前言


图床服务器是一种用于存储和管理图片的服务器,可以给我们提供将图片上传后能外部访问浏览的服务。这样我们在写文章时插入的说明图片,就可以集中放到图床里,既方便多平台文章发布,又能统一管理和备份。


当然下面通过在 GitHub 上搭建的图床,不光不用成本,而且还能上传视频或音乐。操作方法和以前在 GitHub 上搭建静态博客类似,但是中间会多一些一些工具介绍和技巧。


流程



  • 创建仓库

  • 设置仓库

  • 连接仓库

  • 应用 Typora


创建仓库


创建仓库和平时的代码托管一样,添加一个 public 权限仓库,用默认的 main 分支。当然也可以提前创建一个目录,但是根目录最好有一个 index.html。



设置仓库


设置仓库主要是添加提交 Token,和配置 GitHub Pages 参数。而这两小步的设置,在前面文章 "Hexo 博客搭建" 有比较详细介绍,所以这里就稍微文字带过了。


Token 生成


登陆 GitHub -> Settings -> Developer settings -> Personal access tokens -> Tokens (classic),然后点击 "Generate new token",填写备注和过期时间,权限主要勾选 "repo"、"workflow"、"user"。最后生成 "ghp_" 前缀的字符串就是 Token 了,复制并保存下来。


GitHub Pages 配置


进入仓库页 -> Settings -> Pages,设置 Branch,指定仓库的分支和分支根目录,Source 选择 "Deploy from a branch",最后刷新或者重新进入,把访问链接地址复制保存下来。



连接仓库


连接可以除了 API 方式,也可以用第三方的工具,比如 "PicGo"。工具位置自行搜索哈,下面以他为例,演示工具的连接配置、文件上传和访问测试。


连接配置


找到 "图床设置" -> "GitHub",下面主要填写仓库名(需带上账户名),分支名(默认 main 即可),Token(上面生成保存下来的),存储路径(后带斜杠)可以填写已存在,如果不存在则在仓库根目录下新建。



文件上传


文件格式除了下面指定的如 Markdown、HTML、URL 外,还能上传图片音乐视频等(亲测有效)。点击 "上传区",将文件直接拖动到该窗口,提示上传成功后,进入 GitHub 仓库下查看是否存在。 



访问测试


访问就是能将仓库里的图片或视频以外链的方式展示,就像将文件放在云平台的存储桶一样。将前面 GitHub Pages 开启的链接复制下来,然后拼接存储路径和文件名就可以访问了。



应用 Typora


Typora 通过 PicGo 软件自动上传图片到 GitHub 仓库中。打开 Typora 的文件 -> 偏好设置 -> 图像 -> 上传图片 -> 配置 PicGo 路径,然后指定一下 PicGo 的安装位置。 



开始使用


可以点击 "验证图片上传选项",验证成功就代表已经将 Typora 的图标上传到仓库,也可以直接将图片复制到当前 md 文档位置。



![image-20240608145607117](https://raw.githubusercontent.com/z11r00/zd_image_bed/main/img/image-20240608145607117.png)

上传成功后会将返回一个如上面的远程链接,并且无法打开和显示,这是就要在 PicGo 工具的图床设置中。将自己 GitHUb 上的域名设定为自定义域名,格式 "域名 / 仓库名", 在 Typora 上传图片后重启就可展示了。


image-20240612104856943


作者:北桥苏
来源:juejin.cn/post/7384320850722553867
收起阅读 »

12306全球最大票务系统与Gemfire介绍

全球最大票务系统 自2019年12月12日发售春运首日车票,截至2020年1月9日,12306全渠道共发售车票4.12亿张,日均售票能力达到了2000万张,平均一年售出30亿张火车票,也就是说12306已经发展成全球交易量最大的实时票务系统。 12306发布数...
继续阅读 »

全球最大票务系统


自2019年12月12日发售春运首日车票,截至2020年1月9日,12306全渠道共发售车票4.12亿张,日均售票能力达到了2000万张,平均一年售出30亿张火车票,也就是说12306已经发展成全球交易量最大的实时票务系统。


12306发布数据显示,2020年春运期间,40天的春运期间,12306最高峰日网站点击量为1495亿次,这相当于每个中国人一天在12306上点击了100次,平均每秒点击量为170多万次。而全球访问量最大的搜索引擎网站, 谷歌日访问量也不过是56亿次,一个12306的零头。 再看一下大家习惯性做对比的淘宝,2019年双十一当天,淘宝的日活跃用户为4.76亿,相当于每个人也在淘宝上点击300多次,才能赶上12306的峰值点击量。


上亿人口,40天时间,30亿次出行,12306之前,全球没有任何一家公司和产品接手过类似的任务。这个网站是在数亿人苛刻的目光中,做一件史无前例却又必须成功的事情。


历史发展


10年前铁道部顶着重重压力决心要解决买车票这个全民难题,2010年春运首日12306网站开通并试运行,2011年12月23日网站正式上线,铁道部兑现了让网络售票覆盖所有车次的承诺,不料上线第一天,全民蜂拥而入,流量暴增,网站宕机,除此之外,支付流程繁琐支付渠道单一,各种问题不断涌现,宕机可能会迟到,但永远不会缺席,12306上线的第二年,网站仍然难以支撑春运的巨大流量,很多人因为网站的各种问题导致抢票失败,甚至耽误了去线下买票的最佳时机,铁道部马不解鞍听着批评,一次又一次给12306改版升级,这个出生的婴儿几乎是在骂声中长大的。2012年9月,中秋国庆双节来临之前,12306又一次全站崩溃,本来大家习以为常的操作,却被另一个消息彻底出炉,这次崩溃之前,铁道部曾花了3.3亿对系统进行升级,中标的不是IBM惠普EMC等大牌厂商,而是拥有国字号背景的太极股份和同方股份,铁道部解释说3.3亿已经是最低价了,但没人能听进去,大家只关心他长成了什么样,没人关心他累不累,从此之后,铁道部就很少再发声明了。


2013年左右,各种互联网公司表示我行我上,开发了各种抢票网站插件。当时360浏览器靠免费抢票创下国内浏览器使用率的最高纪录,百度猎豹搜狗UC也纷纷加入,如今各类生活服务APP,管他干啥的,都得植入购票抢票功能和服务,12306就这样被抢票软件围捕了。不同的是,过去抢票是免费,现在由命运馈赠的火车票,都在明面上标好了价格,比如抢票助力包,一般花钱买10元5份,也可以邀请好友砍一刀,抢票速度上,分为低快高级光速VIP等等速度,等级越高就越考验钱包。


2017年12306上线了选座和接续换乘功能,从此爱人可以自由抢靠窗座,而且夹在两人之间坐立不安,换乘购票也变得简单。2019年上线官方捡漏神器候补购票功能,可以代替科技黄牛,自动免费为旅客购买退票余票。......


阿里云当时主要是给他们提供虚拟机服务,主要是做IaaS这一层,就是基础设施服务这一层,2012年熟悉阿里云历史的应该都知道,那个时候阿里云其实还是很小的一个厂商,所以不要盲目夸大阿里云在里面起的作用。


技术难点


1、巨大流量,高请求高并发。


2、抢票流量。每天放出无数个爬虫机器人,模拟真人登陆12306,不间断的刷新网站余票,这会滋生很多的灰色流量,也会给12306本身的话造成非常大的压力。


3、动态库存。电商的任务是购物结算,库存是唯一且稳定的,而12306每卖出一张车票,不仅要减少首末站的库存,还要同时减少一趟列车所有过路站的。



以北京西到深圳福田的G335次高铁为例,表面上看起来中间有16个车站及16个SKU,但实际上不同的起始站都会产生新的SKU。我们将所有起始和终点的可能性相加,就是16+15+14一直加到一,一共136个SKU,而每种票对应三种座位,所以一共是408个商品。然后更复杂的是用户每买一张票会影响其他商品的库存,假如用户买了一张北京西的高碑店东的票,那北京始发的16个SKU库存都要减一,但是它并不影响非北京始发车票的库存,
更关键的是这些SKU间有的互斥,有的不互斥,优先卖长的还是优先卖短程的呢,每一次火车票的出售都会引发连锁变化,让计算量大大增加,如果再叠加当前的选座功能,计算数量可能还要再翻倍,而这些计算数据需要在大量购票者抢票的数秒,甚至数毫秒内完成,难度可想而知有多多大。



4、随机性。你永远都不知道哪一个人会在哪一天,去到哪一个地点,而双十一的预售和发货,其实已经提前准备了一个月,甚至几个月,并不是集中在双十一那天爆发的那一天。所以必须要有必须要有动态扩容的能力。


读扩散和写扩散


上面说的动态库存,就比如 A -> B -> C -> D 共 4 个车站,假如乘客买了 B -> C 的车票,那么同时会影响到 A->C,A->D,B->C,B->D,涉及了多个车站的排列组合,这里计算是比较耗费性能的。


那么这里就涉及到了 “读扩散” 和 “写扩散” 的问题,在 12 年的时候,12306 使用的就是读扩散,也就是在扣减余票库存的时候,直接扣减对应车站,而在查询的时候,进行动态计算。而写扩散就是在写的时候,就动态计算每个车站应该扣除多少余票库存,在查询的时候直接查即可。


12306本身他其实是读的流量远远大于写的流量,我个人是认为写扩散其实会更好一点。


Pivotal Gemfire


Redis 在互联网公司中使用的是比较多的,而在银行、12306 很多实时交易的系统中,很多采用 Pivotal Gemfire作为解决方案。Redis 是开源的缓存解决方案,而 Pivotal Gemfire 是商用的,我们在互联网项目中为什么使用 Redis 比较多呢,就是因为 Redis 是开源的,不要钱,开源对应的也就是稳定性不是那么的强,并且开源社区也不会给你提供解决方案,毕竟你是白嫖的,而在银行以及 12306 这些系统中,它们对可靠性要求非常的高,因此会选择商用的 Pivotal Gemfire,不仅性能强、高可用,而且 Gemfire 还会提供一系列的解决方案,据说做到了分布式系统中的 CAP


12306 的性能瓶颈就在于余票的查询操作上,上边已经说了,12306 是采用读扩散,也就是客户买票之后,扣减库存只扣减对应车站之间的余票库存,在读的时候,再来动态的计算每个站点应该有多少余票,因此读性能是 12306 的性能瓶颈


当时 12306 也尝试了许多其他的解决方案,比如 cassandra 和 mamcached,都扛不住查询的流量,而使用 Gemfire 之后扛住了流量,因此就使用了 Gemfire。2012年6月一期先改造12306的主要瓶颈——余票查询系统。 9月份完成代码改造,系统上线。2012年国庆,又是网上订票高峰期间,大家可以显著发现,可以登录12306,虽然还是很难订票,但是查询余票很快。2012年10月份,二期用GemFire改造订单查询系统(客户查询自己的订单记录)2013年春节,又是网上订票高峰期间,大家可以显著发现,可以登录12306,虽然还是很难订票,但是查询余票很快,而且查询自己的订票和下订单也很快。


技术改造之后,在只采用10几台X86服务器实现了以前数十台小型机的余票计算和查询能力,单次查询的最长时间从之前的15秒左右下降到0.2秒以下,缩短了75倍以上。 2012年春运的极端高流量并发情况下,系统几近瘫痪。而在改造之后,支持每秒上万次的并发查询,高峰期间达到2.6万个查询/秒吞吐量,整个系统效率显著提高;订单查询系统改造,在改造之前的系统运行模式下,每秒只能支持300-400个查询/秒的吞吐量,高流量的并发查询只能通过分库来实现。改造之后,可以实现高达上万个查询/秒的吞吐量,而且查询速度可以保障在20毫秒左右。新的技术架构可以按需弹性动态扩展,并发量增加时,还可以通过动态增加X86服务器来应对,保持毫秒级的响应时间。


通过云计算平台虚拟化技术,将若干X86服务器的内存集中起来,组成最高可达数十TB的内存资源池,将全部数据加载到内存中,进行内存计算。计算过程本身不需要读写磁盘,只是定期将数据同步或异步方式写到磁盘。GemFire在分布式集群中保存了多份数据,任何一台机器故障,其它机器上还有备份数据,因此通常不用担心数据丢失,而且有磁盘数据作为备份。GemFire支持把内存数据持久化到各种传统的关系数据库、Hadoop库和其它文件系统中。大家知道,当前计算架构的瓶颈在存储,处理器的速度按照摩尔定律翻番增长,而磁盘存储的速度增长很缓慢,由此造成巨大高达10万倍的差距。这样就很好理解GemFire为什么能够大幅提高系统性能了。Gemfire 的存储和计算都在一个地方,它的存储和实时计算的性能目前还没有其他中间件可以取代。


但是 Gemfire 也存在不足的地方,对于扩容的支持不太友好的,因为它里边有一个 Bucket 类似于 Topic 的概念,定好 Bucket 之后,扩容是比较难的,在 12306 中,也有过测试,需要几十个T的内存就可以将业务数据全部放到内存中来,因此直接将内存给加够,也就不需要很频繁的扩容。


12306业务解决方案


当然在优化中,我们靠改变架构加机器可以提升速度效率,剩下的也需要业务上的优化。


1、验证码。如果说是淘宝啊这种网站,他用这种验证码,用12306的验证码,可能大家都不会用了,对不对,但是12306他比较特殊,因为铁路全国就他一家,所以说他可以去做这个事情,他不用把用户体验放在第一位
。他最高的优先级是怎么把票给需要的人手上。


当然这个利益的确是比较大,所以也会采用这种人工打码的方式,可以雇一批大学生去做这个验证码识别。


2、候补。候补车票其实相当于整个系统上,它是一个异步的过程,你可以在这里排队,后面的话也没有抢到票,后面再通知你。


3、分时段售票。对于抢票来说,瞬时抢票会导致对服务器有瞬间很大的压力,因此从业务设计上来说需要将抢票的压力给分散开,比如今天才开启抢15天之后的车票。2点抢票,3点抢票等等。


总结


只有程序员才知道,一个每天完成超过1500万个订单,承受近1500亿次点击的系统到底有多难,在高峰阶段的时候,平均每秒就要承受170多万次的点击,面对铁路运输这种特殊的运算模式,也能够保证全国人民在短时间内抢到回家的票,12306就是在无数国人的苛责和质疑中,创造了一个世界的奇迹。


12306除了技术牛,还有着自己的人情关怀,系统会自动识别购票者的基本信息,如果识别出订单里有老人会优先给老人安排下铺儿童和家长会尽量安排在邻近的位置,12306 在保证所有人都能顺利抢到回家的票的同时,还在不断地增加更多的便利,不仅在乎技术问题,更在乎人情异味,12306可能还不够完美,但他一直在努力变得更好,为我们顺利回家提供保障,这是背后无数程序员日夜坚守的结果,我们也应该感谢总设计师单杏花女士,所以你可以调侃,可以批评,但不能否认12306背后所做出的所有努力!


作者:jack_xu
来源:juejin.cn/post/7381747852831653929
收起阅读 »

秒懂双亲委派机制

前言 最近有位小伙伴问了我一个问题:JDBC为什么会破坏双亲委派机制? 这个问题挺有代表性的。 双亲委派机制是Java中非常重要的类加载机制,它保证了类加载的完整性和安全性,避免了类的重复加载。 这篇文章就跟大家一起聊聊,Java中类加载的双亲委派机制到底是怎...
继续阅读 »

前言


最近有位小伙伴问了我一个问题:JDBC为什么会破坏双亲委派机制?


这个问题挺有代表性的。


双亲委派机制是Java中非常重要的类加载机制,它保证了类加载的完整性和安全性,避免了类的重复加载。


这篇文章就跟大家一起聊聊,Java中类加载的双亲委派机制到底是怎么回事,有哪些破坏双亲委派机制的案例,为什么要破坏双亲委派机制,希望对你会有所帮助。


1 为什么要双亲委派机制?


我们的Java在运行之前,首先需要把Java代码转换成字节码,即class文件。


然后JVM需要把字节码通过一定的方式加载到内存中的运行时数据区


这种方式就是类加载器(ClassLoader)。


再通过加载、验证、准备、解析、初始化这几个步骤完成类加载过程,然后再由jvm执行引擎的解释器和JIT即时编译器去将字节码指令转换为本地机器指令进行执行。


我们在使用类加载器加载类的时候,会面临下面几个问题:



  1. 如何保证类不会被重复加载?类重复加载会出现很多问题。

  2. 类加载器是否允许用户自定义?

  3. 如果允许用户自定义,如何保证类文件的安全性?

  4. 如何保证加载的类的完整性?


为了解决上面的这一系列的问题,我们必须要引入某一套机制,这套机制就是:双亲委派机制


2 什么是双亲委派机制?


接下来,我们看看什么是双亲委派机制。


双亲委派机制的基本思想是:当一个类加载器试图加载某个类时,它会先委托给其父类加载器,如果父类加载器无法加载,再由当前类加载器自己进行加载。


这种层层委派的方式有助于保障类的唯一性,避免类的重复加载,并提高系统的安全性和稳定性。


在Java中默认的类加载器有3层:



  1. 启动类加载器(Bootstrap Class Loader):负责加载 %JAVA_HOME%/jre/lib 目录下的核心Java类库,比如:rt.jar、charsets.jar等。它是最顶层的类加载器,通常由C++编写。

  2. 扩展类加载器(Extension Class Loader):负责加载Java的扩展库,一般位于/lib/ext目录下。

  3. 应用程序类加载器(Application Class Loader):也称为系统类加载器,负责加载用户类路径(ClassPath)下的应用程序类。


用一张图梳理一下,双亲委派机制中的3种类加载器的层次关系:图片


但这样不够灵活,用户没法控制,加载自己想要的一些类。


于是,Java中引入了自定义类加载器。


创建一个新的类并继承ClassLoader类,然后重写findClass方法。


该方法主要是实现从那个路径读取 ar包或者.class文件,将读取到的文件用字节数组来存储,然后可以使用父类的defineClass来转换成字节码。


如果想破坏双亲委派的话,就重写loadClass方法,否则不用重写。


类加载器的层次关系改成:图片


双亲委派机制流程图如下:图片


具体流程大概是这样的:



  1. 需要加载某个类时,先检查自定义类加载器是否加载过,如果已经加载过,则直接返回。

  2. 如果自定义类加载器没有加载过,则检查应用程序类加载器是否加载过,如果已经加载过,则直接返回。

  3. 如果应用程序类加载器没有加载过,则检查扩展类加载器是否加载过,如果已经加载过,则直接返回。

  4. 如果扩展类加载器没有加载过,则检查启动类加载器是否加载过,如果已经加载过,则直接返回。

  5. 如果启动类加载器没有加载过,则判断当前类加载器能否加载这个类,如果能加载,则加载该类,然后返回。

  6. 如果启动类加载器不能加载该类,则交给扩展类加载器。扩展类加载器判断能否加载这个类,如果能加载,则加载该类,然后返回。

  7. 如果扩展类加载器不能加载该类,则交给应用程序类加载器。应用程序类加载器判断能否加载这个类,如果能加载,则加载该类,然后返回。

  8. 如果应用程序类加载器不能加载该类,则交给自定义类加载器。自定义类加载器判断能否加载这个类,如果能加载,则加载该类,然后返回。

  9. 如果自定义类加载器,也无法加载这个类,则直接抛ClassNotFoundException异常。


这样做的好处是:



  1. 保证类不会重复加载。加载类的过程中,会向上问一下是否加载过,如果已经加载了,则不会再加载,这样可以保证一个类只会被加载一次。

  2. 保证类的安全性。核心的类已经被启动类加载器加载了,后面即使有人篡改了该类,也不会再加载了,防止了一些有危害的代码的植入。


3 破坏双亲委派机制的场景


既然Java中引入了双亲委派机制,为什么要破坏它呢?


答:因为它有一些缺点。


下面给大家列举一下,破坏双亲委派机制最常见的场景。


3.1 JNDI


JNDI是Java中的标准服务,它的代码由启动类加载器去加载。


但JNDI要对资源进行集中管理和查找,它需要调用由独立厂商在应用程序的ClassPath下的实现了JNDI接口的代码,但启动类加载器不可能“认识”这些外部代码。


为了解决这个问题,Java后来引入了线程上下文类加载器(Thread Context ClassLoader)。


这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置。


如果创建线程时没有设置,他将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。


有了线程上下文加载器,JNDI服务就可以使用它去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载的动作,这样就打破了双亲委派机制。


3.2 JDBC


原生的JDBC中Driver驱动本身只是一个接口,并没有具体的实现,具体的实现是由不同数据库类型去实现的。


例如,MySQL的mysql-connector.jar中的Driver类具体实现的。


原生的JDBC中的类是放在rt.jar包,是由启动类加载器进行类加载的。


在JDBC中需要动态去加载不同数据库类型的Driver实现类,而mysql-connector.jar中的Driver实现类是用户自己写的代码,启动类加载器肯定是不能加载的,那就需要由应用程序启动类去进行类加载。


为了解决这个问题,也可以使用线程上下文类加载器(Thread Context ClassLoader)。


3.3  Tomcat容器


Tomcat是Servlet容器,它负责加载Servlet相关的jar包。


此外,Tomcat本身也是Java程序,也需要加载自身的类和一些依赖jar包。


这样就会带来下面的问题:



  1. 一个Tomcat容器下面,可以部署多个基于Servlet的Web应用,但如果这些Web应用下有同名的Servlet类,又不能产生冲突,需要相互独立加载和运行才行。

  2. 但如果多个Web应用,使用了相同的依赖,比如:SpringBoot、Mybatis等。这些依赖包所涉及的文件非常多,如果全部都独立,可能会导致JVM内存不足。也就是说,有些公共的依赖包,最好能够只加载一次。

  3. 我们还需要将Tomcat本身的类,跟Web应用的类隔离开。


这些原因导致,Tomcat没有办法使用传统的双亲委派机制加载类了。


那么,Tomcat加载类的机制是怎么样的?


图片



  • CommonClassLoader:是Tomcat最基本的类加载器,它加载的类可以被Tomcat容器和Web应用访问。

  • CatalinaClassLoader:是Tomcat容器私有的类加载器,加载类对于Web应用不可见。

  • SharedClassLoader:各个Web应用共享的类加载器,加载的类对于所有Web应用可见,但是对于Tomcat容器不可见。

  • WebAppClassLoader:各个Web应用私有的类加载器,加载类只对当前Web应用可见。比如不同war包应用引入了不同的Spring版本,这样能加载各自的Spring版本,相互隔离。


3.4 热部署


由于用户对程序动态性的追求,比如:代码热部署、代码热替换等功能,引入了OSGi(Open Service Gateway Initiative)。


OSGi中的每一个模块(称为Bundle)。


当程序升级或者更新时,可以只停用、重新安装然后启动程序的其中一部分,对企业来说这是一个非常诱人的功能。


OSGi的Bundle类加载器之间只有规则,没有固定的委派关系。


各个Bundle加载器是平级关系。


不是双亲委派关系。




作者:苏三说技术
来源:juejin.cn/post/7383894631312769074
收起阅读 »

ThreadLocal不香了,ScopedValue才是王道

ThreadLocal的缺点在Java中,当多个方法要共享一个变量时,我们会选择使用ThreadLocal来进行共享,比如:  以上代码将字符串“dadudu”通过设置到ThreadLocal中,从而可以做到在main()方法中赋值,在a(...
继续阅读 »

ThreadLocal的缺点

在Java中,当多个方法要共享一个变量时,我们会选择使用ThreadLocal来进行共享,比如:  以上代码将字符串“dadudu”通过设置到ThreadLocal中,从而可以做到在main()方法中赋值,在a()b()方法中获取值,从而共享值。

生命在于思考,我们来想想ThreadLocal有什么缺点:

  1. 第一个就是权限问题,也许我们只需要在main()方法中给ThreadLocal赋值,在其他方法中获取值就可以了,而上述代码中a()b()方法都有权限给ThreadLocal赋值,ThreadLocal不能做权限控制。
  2. 第二个就是内存问题,ThreadLocal需要手动强制remove,也就是在用完ThreadLocal之后,比如b()方法中,应该调用其remove()方法,但是我们很容易忘记调用remove(),从而造成内存浪费

ScopedValue

而JDK21中的新特性ScopedValue能不能解决这两个缺点呢?我们先来看一个ScopedValue的Demo: 

首先需要通过ScopedValue.newInstance()生成一个ScopedValue对象,然后通过ScopedValue.runWhere()方法给ScopedValue对象赋值,runWhere()的第三个参数是一个lambda表达式,表示作用域,比如上面代码就表示:给NAME绑定值为"dadudu",但是仅在调用a()方法时才生效,并且在执行runWhere()方法时就会执行lambda表达式。

比如上面代码的输出结果为: 

从结果可以看出在执行runWhere()时会执行a()a()方法中执行b()b()执行完之后返回到main()方法执行runWhere()之后的代码,所以,在a()方法和b()方法中可以拿到ScopedValue对象所设置的值,但是在main()方法中是拿不到的(报错了),b()方法之所以能够拿到,是因为属于a()方法调用栈中。

所以在给ScopedValue绑定值时都需要指定一个方法,这个方法就是所绑定值的作用域,只有在这个作用域中的方法才能拿到所绑定的值。

ScopedValue也支持在某个方法中重新开启新的作用域并绑定值,比如: 

以上代码中,在a()方法中重新给ScopedValue绑定了一个新值“xiaodudu”,并指定了作用域为c()方法,所以c()方法中拿到的值为“xiaodudu”,但是b()中仍然拿到的是“dadudu”,并不会受到影响,以上代码的输出结果为: 

甚至如果把代码改成: 

以上代码在a()方法中有两处调用了c()方法,我想大家能思考出c1c2输出结果分别是什么: 

所以,从以上分析可以看到,ScopedValue有一定的权限控制:就算在同一个线程中也不能任意修改ScopedValue的值,就算修改了对当前作用域(方法)也是无效的。另外ScopedValue也不需要手动remove,关于这块就需要分析它的实现原理了。

实现原理

大家先看下面代码,注意看下注释: 

执行main()方法时,main线程执行过程中会执行runWhere()方法三次,而每次执行runWhere()时都会生成一个Snapshot对象,Snapshot对象中记录了所绑定的值,而Snapshot对象有一个prev属性指向上一次所生成的Snapshot对象,并且在Thread类中新增了一个属性scopedValueBindings,专门用来记录当前线程对应的Snapshot对象。

比如在执行main()方法中的runWhere()时:

  1. 会先生成Snapshot对象1,其prev为null,并将Snapshot对象1赋值给当前线程的scopedValueBindings属性,然后执行a()方法
  2. 在执行a()方法中的runWhere()时,会先生成Snapshot对象2,其prevSnapshot对象1,并将Snapshot对象2赋值给当前线程的scopedValueBindings属性,使得在执行b()方法时能从当前线程拿到Snapshot对象2从而拿到所绑定的值,runWhere()内部在执行完b()方法后会取prev,从而取出Snapshot对象1,并将Snapshot对象1赋值给当前线程的scopedValueBindings属性,然后继续执行a()方法后续的逻辑,如果后续逻辑调用了get()方法,则会取当前线程的scopedValueBindings属性拿到Snapshot对象1,从Snapshot对象1中拿到所绑定的值就可以了,而对于Snapshot对象2由于没有引用则会被垃圾回收掉。

所以,在用ScopedValue时不需要手动remove。

好了,关于ScopedValue就介绍到这啦,下次继续分享JDK21新特性,欢迎大家关注我的公众号:Hoeller,第一时间接收我的原创技术文章,谢谢大家的阅读。


作者:IT周瑜
来源:juejin.cn/post/7287241480770928655
收起阅读 »

开发经理:谁在项目里面用Stream. paraller()直接gun

大家好,我是小玺,今天给大家分享一下项目中关于Stream.parallel() 碰到的坑。 Stream.parallel() 是Java 8及以上版本引入的Stream API中的一个方法,它用于将一个串行流转换为并行流。并行流可以在多个处理器上同时执行操...
继续阅读 »

大家好,我是小玺,今天给大家分享一下项目中关于Stream.parallel() 碰到的坑。


Stream.parallel() 是Java 8及以上版本引入的Stream API中的一个方法,它用于将一个串行流转换为并行流。并行流可以在多个处理器上同时执行操作,从而显著提高对大量数据进行处理的性能。


踩坑日记


某个大型项目,晚上十一点多有个用户对小部分数据进行某项批量操作后,接口大半天没有反应最后返回超时报错,但是过了一段时间后,出现了部分数据被修改成功,部分数据则没有反应。用户立马跳起来,打电话投诉到公司领导层,于是乎领导层对上至开发经理和PM,下至小开发进行会议批斗,要求马上排查并解决问题,毕竟项目这么大,当初也是要求测试做过压测的,怎么出现这么大的生产事故。


1712648893920.png


于是乎开发和实施运维分头行事,开发人员排查问题,实施人员先把问题数据维护好,不能应该用户使用。一群开发也是很疑惑,开发和测试环境都没法复现出问题,简单过一下代码也没看出个所以然,由于时间问题,不得不呼叫一手开发经理帮忙看看,开发经理后台接口看完Stream.parallel()进行的操作代码立马就炸了,git看了下提交人【会笑】,把这个开发从头到脚喷了一遍。


在对会笑单独进行了长达半小时的“耐心教育”后(ps:问题安排另一名开发同事修复),开发经理给团队的所有后端开发人员又都教育了一遍。原来会笑在用并行流的时候,没有考虑线程池配置和事务问题,把一堆数据进行了批量更新,Stream.parallel()并行流默认使用的是ForkJoinPool.commonPool()作为线程池,该线程池默认最大线程数就是CPU核数。


1712648957687.png


雀食对于一些初中级开发来说,开发过程中往往喜欢用一些比较新颖的写法来实现但是对新语法又是一知半解的,Stream.parallel()作为Java的新特性,也就成了其中一个反面教材。如果操作数据量不大的情况,其实没有必要用到Stream.parallel(),效率反而会变差。


注意事项



  1. 线程安全:并行流并不能保证线程安全性,因此,如果流中的元素是共享资源或操作本身不是线程安全的,你需要确保正确同步或使用线程安全的数据结构。

  2. 数据分区:Java的并行流机制会自动对数据进行分区,但在某些情况下,数据分区的开销可能大于并行带来的收益,特别是对于小规模数据集。

  3. 效率考量:并非所有的流操作都能从并行化中受益,有些操作(如短流操作或依赖于顺序的操作)并行执行反而可能导致性能下降。而且,过多的上下文切换也可能抵消并行带来的优势。

  4. 资源消耗:并行流默认使用的线程池大小可能与机器的实际物理核心数相适应,但也可能与其他并发任务争夺系统资源。

  5. 结果一致性:并行流并不保证执行的顺序性,也就是说,如果流操作的结果依赖于元素的处理顺序,则不应该使用并行流。

  6. 事务处理:在涉及到事务操作时,通常需要避免在并行流中直接处理,如上述例子所示,应当将事务边界放在单独的服务方法内,确保每个线程内的事务独立完成。


Tips:线程数可以通JVM启动参数-Djava.util.concurrent.ForkJoinPool.common.parallelism=20进行修改


作者:小玺
来源:juejin.cn/post/7355431482687864883
收起阅读 »

记一次难忘的json反序列化问题排查经历

前言 最近我在做知识星球中的商品秒杀系统,昨天遇到了一个诡异的json反序列化问题,感觉挺有意思的,现在拿出来跟大家一起分享一下,希望对你会有所帮助。 案发现场 我最近在做知识星球中的商品秒杀系统,写了一个filter,获取用户请求的header中获取JWT的...
继续阅读 »

前言


最近我在做知识星球中的商品秒杀系统,昨天遇到了一个诡异的json反序列化问题,感觉挺有意思的,现在拿出来跟大家一起分享一下,希望对你会有所帮助。


案发现场


我最近在做知识星球中的商品秒杀系统,写了一个filter,获取用户请求的header中获取JWT的token信息。


然后根据token信息,获取到用户信息。


在转发到业务接口之前,将用户信息设置到用户上下文当中。


这样接口中的业务代码,就能通过用户上下文,获取到当前登录的用户信息了。


我们的token和用户信息,为了性能考虑都保存到了Redis当中。


用户信息是一个json字符串。


当时在用户登录接口中,将用户实体,使用fastjson工具,转换成了字符串:


JSON.toJSONString(userDetails);

保存到了Redis当中。


然后在filter中,通过一定的key,获取Redis中的字符串,反序列化成用户实体。


使用的同样是fastjson工具:


JSON.parseObject(json, UserEntity.class);

但在反序列化的过程中,filter抛异常了:com.alibaba.fastjson.JSONException: illegal identifier : \pos 1, line 1, column 2{"accountNonExpired":true,"accountNonLocked":true,"authorities":[{"authority":"admin"}],"credentialsNonExpired":true,"enabled":true,"id":13,"password":"$2a$10$o3XfeGr0SHStAwLuJRW6y.kE0UTerQfv3SXrAcVLuJ6M3hEsC9RKe","roles":["admin"],"username":"admin"}


2 分析问题


我刚开始以为是json数据格式有问题。


将json字符串复制到在线json工具:http://www.sojson.com,先去掉化之后,再格式数据,发现json格式没有问题:![图片](p3-juejin.byteimg.com/tos-cn-i-k3…)


然后写了一个专门的测试类,将日志中打印的json字符串复制到json变量那里,使用JSON.parseObject方法,将json字符串转换成Map对象:


public class Test {

    public static void main(String[] args) {
        String json = "{"accountNonExpired":true,"accountNonLocked":true,"authorities":[{"authority":"admin"}],"credentialsNonExpired":true,"enabled":true,"id":13,"password":"$2a$10$o3XfeGr0SHStAwLuJRW6y.kE0UTerQfv3SXrAcVLuJ6M3hEsC9RKe","roles":["admin"],"username":"admin"}";
        Map map = JSON.parseObject(json, Map.class);
        // 输出解析后的 JSON 对象
        System.out.println(map);
    }
}

执行结果:


{password=$2a$10$o3XfeGr0SHStAwLuJRW6y.kE0UTerQfv3SXrAcVLuJ6M3hEsC9RKe, credentialsNonExpired=true, roles=["admin"], accountNonExpired=true, id=13, authorities=[{"authority":"admin"}], enabled=true, accountNonLocked=true, username=admin}

竟然转换成功了。


这就让我有点懵逼了。。。


为什么相同的json字符串,在Test类中能够正常解析,而在filter当中却不行?


当时怕搞错了,debug了一下filter,发现获取到的json数据,跟Test类中的一模一样:图片


带着一脸的疑惑,我做了下面的测试。


8000页BAT大佬写的刷题笔记,让我offer拿到手软


莫非是反序列化工具有bug?


3 改成gson工具


我尝试了一下将json的反序列化工具改成google的gson,代码如下:


 Map map = new Gson().fromJson(userJson, Map.class);

运行之后,报了一个新的异常:com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected BEGIN_OBJECT but was STRING at line 1 column 2 path $


这里提示json字符串中包含了:$


$是特殊字符,password是做了加密处理的,里面包含$.,这两种特殊字符。


为了快速解决问题,我先将这两个特字符替换成空字符串:


json = json.replace("$","").replace(".","");

日志中打印出的json中的password,已经不包含这两个特殊字符了:


2a10o3XfeGr0SHStAwLuJRW6ykE0UTerQfv3SXrAcVLuJ6M3hEsC9RKe

但调整之后代码报了下面的异常:com.google.gson.JsonSyntaxException: com.google.gson.stream.MalformedJsonException: Expected name at line 1 column 2 path $.


跟刚刚有点区别,但还是有问题。


4 改成jackson工具


我又尝试了一下json的反序列化工具,改成Spring自带的的jackson工具,代码如下:


ObjectMapper objectMapper = new ObjectMapper();
try {
    Map map = objectMapper.readValue(json, Map.class);
catch (JsonProcessingException e) {
    e.printStackTrace();
}

调整之后,反序列化还是报错:com.fasterxml.jackson.core.JsonParseException: Unexpected character ('' (code 92)): was expecting double-quote to start field name


3种反序列化工具都不行,说明应该不是fastjson的bug导致的当前json字符串,反序列化失败。


到底是什么问题呢?


5 转义


之前的数据,我在仔细看了看。


里面是对双引号,是使用了转义的,具体是这样做的:"


莫非还是这个转义的问题?


其实我之前已经注意到了转义的问题,但使用Test类测试过,没有问题。


当时的代码是这样的:


public class Test {

    public static void main(String[] args) {
        String json = "{"accountNonExpired":true,"accountNonLocked":true,"authorities":[{"authority":"admin"}],"credentialsNonExpired":true,"enabled":true,"id":13,"password":"$2a$10$o3XfeGr0SHStAwLuJRW6y.kE0UTerQfv3SXrAcVLuJ6M3hEsC9RKe","roles":["admin"],"username":"admin"}";
        Map map = JSON.parseObject(json, Map.class);
        // 输出解析后的 JSON 对象
        System.out.println(map);
    }
}

里面也包含了一些转义字符。


我带着试一试的心态,接下来,打算将转义字符去掉。


看看原始的json字符串,解析有没有问题。


怎么去掉转义字符呢?


手写工具类,感觉不太好,可能会写漏一些特殊字符的场景。


8000页BAT大佬写的刷题笔记,让我offer拿到手软


我想到了org.apache.commons包下的StringEscapeUtils类,它里面的unescapeJava方法,可以轻松去掉Java代码中的转义字符。


于是,我调整了一下代码:


json = StringEscapeUtils.unescapeJava(json);
JSON.parseObject(json, UserEntity.class);

这样处理之后,发现反序列化成功了。


总结


这个问题最终发现还是转义的问题。


那么,之前Test类中json字符串,也使用了转义,为什么没有问题?


当时的代码是这样的:


public class Test {

    public static void main(String[] args) {
        String json = "{"accountNonExpired":true,"accountNonLocked":true,"authorities":[{"authority":"admin"}],"credentialsNonExpired":true,"enabled":true,"id":13,"password":"$2a$10$o3XfeGr0SHStAwLuJRW6y.kE0UTerQfv3SXrAcVLuJ6M3hEsC9RKe","roles":["admin"],"username":"admin"}";
        Map map = JSON.parseObject(json, Map.class);
        System.out.println(map);
    }
}

但在filter中的程序,在读取到这个json字符串之后,发现该字符串中包含了``转义符号,程序自动把它变成了\


调整一下Test类的main方法,改成三个斜杠的json字符串:


public static void main(String[] args) {
    String json = "{\"accountNonExpired\":true,\"accountNonLocked\":true,\"authorities\":[{\"authority\":\"admin\"}],\"credentialsNonExpired\":true,\"enabled\":true,\"id\":13,\"password\":\"$2a$10$o3XfeGr0SHStAwLuJRW6y.kE0UTerQfv3SXrAcVLuJ6M3hEsC9RKe\",\"roles\":[\"admin\"],\"username\":\"admin\"}";
    Map map = JSON.parseObject(json, Map.class);
    System.out.println(map);
}

执行结果:Exception in thread "main" com.alibaba.fastjson.JSONException: illegal identifier : \pos 1, line 1, column 2{"accountNonExpired":true,"accountNonLocked":true,"authorities":[{"authority":"admin"}],"credentialsNonExpired":true,"enabled":true,"id":13,"password":"$2a$10$o3XfeGr0SHStAwLuJRW6y.kE0UTerQfv3SXrAcVLuJ6M3hEsC9RKe","roles":["admin"],"username":"admin"}抛出了跟文章最开始一样的异常。


说明其实就是转义的问题。


之前,我将项目的日志中的json字符串,复制到idea的Test的json变量中,当时将最外层的双引号一起复制过来了,保存的是1个斜杠的数据。


这个操作把我误导了。


而后面从在线的json工具中,把相同的json字符串,复制到idea的Test的json变量中,在双引号当中粘贴数据,保存的却是3个斜杠的数据,它会自动转义。


让我意识到了问题。


好了,下次如果遇到类似的问题,可以直接使用org.apache.commons包下的StringEscapeUtils类,先去掉转义,再反序列化,这样可以快速解决问题。


此外,这次使用了3种不同的反序列化工具,也看到了其中的一些差异。


作者:苏三说技术
来源:juejin.cn/post/7385081262871003175
收起阅读 »

当了程序员之后?(真心话)

分享是最有效的学习方式。 博客:blog.ktdaddy.com/ 地铁上刷到一个话题,觉得挺有意思的,如下。 看到很多朋友在下面吐槽,有说加班是真的多,有说找对象是真的难,有说程序员爱穿格子衫是假爱背电脑是真的等等,大家吐槽得都挺欢乐的。 老猫也开始复...
继续阅读 »

分享是最有效的学习方式。
博客:blog.ktdaddy.com/



地铁上刷到一个话题,觉得挺有意思的,如下。


1709213979295.png


看到很多朋友在下面吐槽,有说加班是真的多,有说找对象是真的难,有说程序员爱穿格子衫是假爱背电脑是真的等等,大家吐槽得都挺欢乐的。


1709824704115.png


老猫也开始复盘这些年的经历,更多想聊的可能还是一个后端程序员的真实感悟。


入行


俗话说“男怕入错行,女怕嫁错郎。”相信很多朋友在进入一个行业之前都是深思熟虑的,亦或者是咨询过一些人,亦或者是查阅了挺多资料。然而老猫入行则相当奇葩,不是蓄谋已久,而是心血来潮。


一切都得从一部电视剧开始,不晓得大家有没有看过这部电视剧,佟丽娅主演的“我的经济适用男”。


1709215705556.png


12年的一部电视剧,挺老了,主要女主放弃富二代的追求和"成熟稳重老实巴交的IT男"好上了的桥段。当时心智单纯的老猫可谓看的是热血沸腾啊。一拍桌子,“发可油,劳资今后就要当那个男主,这结局多好啊,抱得美人归啊这是,我要学IT!”。当时老猫的专业是电子信息类的专业,后来基本就放弃了本专业,大学基本逃课就跑去学软件去了。


就这么上了贼船,一晃十年过去了。多年前,躲在实验室里,开发了一个简单的坦克大战的游戏,感觉自己是最牛逼的,子弹爱怎么飞怎么飞,坦克能开多块就开多快,那时候觉得自己就是这个IT世界的主角,“control evety thing”。在这样一个程序的世界里,所有的事儿都是自己说了算。


踏上社会后,遭遇社会惨无人道地毒打之后,发现要做的就是提升造火箭吹牛逼的能力,工作中是个crud-boy。键盘上磨损最严重的那几个键是“ctrl”,“c”,“v”,“x”。当年那个意气风发的少年已经不复存在,我是一个弱鸡螺丝钉。


1709217726156.png


工作十年


大部分后端程序员也主要是围绕着业务在转,所以crud可能占了大部分时间。


话虽如此,但还是有点除此以外的收获,这些收获甚至潜移默化地影响着我的生活。


技术日新月异,今天这个框架,明天那个架构,今天这种实现牛逼,明天那种部署更6等等,到头来发现自己一直都是在追着技术跑。也确实如果不奔跑的话,可能就会被淘汰。作为程序员来说适应变化也是程序员的一种品质,但是老猫觉得具备下面这些可能会更加重要一些,这些可能也是唯一不变的。


抽象思维很重要


第一次听到“架构师”这个职位的时候,觉得那一定是一个需要超强技术能力的人才能胜任的岗位。


后来才发现原来架构师也分种类,“业务架构”,“技术架构”等等。再后来发现无论哪种架构,其实他们身上会有一种共同的东西,那就是优秀的抽象思维。


啥是抽象思维?百度百科上是这么说的:


抽象思维,又称词的思维或者逻辑思维,是指用词进行判断、推理并得出结论的过程。
抽象思维以词为中介来反映现实。这是思维的最本质特征,也是人的思维和动物心理的根本区别。

说的比较官方,甚至有点不好懂。


大家以前上语文课的时候,有没有做过阅读理解,老师在讲课的时候常常我们概述一下这段文字到底讲了什么东西,越精简越好,可能要求20个字以内。其实这个过程就是在锻炼咱们的抽象思维能力以及概括能力。


在软件后端领域,当业务传达某一个需求的时候,往往需要产品去提炼一道,那么此时就是产品抽象,继而产品又将需求传达给相关的研发负责人,研发负责人设计出相关的实现模型,那么这又是技术抽象,这些抽象的过程就是将复杂的业务流程和逻辑转化为可管理和可重用的组件的过程。它的目的是简化系统的实现,聚焦于应用程序的核心功能,同时隐藏不必要的细节。抽象后设计出各种基础能力,通过对基础能力的组合和拼接,支持复杂多变的业务逻辑和业务形态。


gw1.png


具备抽象思维,能够让我们从复杂的业务中迅速切入业务关键点。在生活中可能表现在透过现象看到本质,或者碰到问题能够快速给出有效解决方案或思路。例如老猫上次遇到的“真-丢包事件”。


分层思维很重要


说到分层思维,应该准确地来说是建立在能够清晰的抽象出事务本质的基础上,而后再去做分层。


很多地方都会存在分层思想。生活中就有,大家双休日没事的时候估计会逛商场,商城的模式一般就是底层停车场,一层超市,二层卖服装的,三层儿童乐园,卖玩具的,四层吃饭看电影娱乐的等等。


再去聊到技术上的分层思想,例如OSI七层模型,大家在面试的时候甚至都碰到过。


gw2.png


抛开这些,其实我们对自己当前负责的一些业务,一些系统也需要去做一些分层划分,这些分层可以让我们更好地看清业务系统之间的关系。例如老猫之前梳理的一张图。


gw3.png


通过这样的分层梳理,我们可能更好地理解当前的系统组成以及层级关系。(备注一下,老猫这里画图工具用的还是wps绘制的)。


结构化思维很重要


结构化思维又是咋回事儿?
不着急,打个比方,咱们看下面一组数据:
213421790346567560889
现在有个要求,咱们需要记下这些数字,以及出现的次数。短时间内想要记住可能比较困难
如果我们把这些数字的内容调整下,变成下面这样:
00112233445566778899
是不是清晰了很多?


所谓的结构化思维,就是从无序到有序的一种思考过程,将搜集到的信息、数据、知识等素材按一定的逻辑进行分析、整理,呈现出有序的结构,继而化繁为简。有结构的信息更适合大脑记忆和理解。


人类大脑在处理信息的时候,有两个特点:


第一,不能一次太多,太多信息会让我们的大脑觉得负荷过大;乔治·米勒在他的论文《奇妙的数字7±2》中提出,人类大脑短期记忆无法一次容纳7个以上的记忆项目,比较容易记住的是3个项目,当然最容易的是1个。


第二,喜欢有规律的信息。有规律的信息能减少复杂度,Mitchell Waldrop在《复杂》一书中,提出一种用信息熵来进行复杂性度量的方法,所谓信息熵就是一条信息包含信息量的大小。举个例子,假设一条消息由符号A、C、G和T组成。如果序列高度有序,很容易描述,例如“A A A A A A A … A”,则熵为零。而完全随机的序列则有最大熵值。


ccfc037aa9b4e852ef2a16f8e58c4a86.png


老猫在写文章的时候喜欢先列一下要写的提纲,然后再跟着提纲一点一点的往下写,写定义,写实现,写流程。


虽然本文偷了个懒,没有写思维导图,老猫一般再聊到干货的时候都会和大家先列一下提纲。这种提纲其实也是结构化的一种。当我们遇到复杂系统需求的时候,咱们不妨先列个提纲,将需要做的按照自己定义好的顺序罗列好,这样解决起来会更加容易一些。


太过理性可能也不好


程序员做久了,做一件事情的时候都会去想着先做什么然后做什么一步一步,有时候会显得过于机械,不知变通,
有时候可能也会太过较真,大直男显得情商比较低,会多多少少给别人带去一些不便,记得在银行办理业务的时候会指出业务员说话的逻辑漏洞,然后不停地追问,最终可能导致业务员尴尬地叫来业务经理解释等等。


程序员思维做事情,可能在日常生活中比较严谨,但是很多时候还是会显得比较死板。


总结


以上是老猫觉得除了技术以外,觉得一个后端程序员应该具备的一些思考方式以及工作方式,当然也可能只是老猫的方法论,如果大家有其他的工作领悟,也欢迎大家留言,大家一起分享一下经验。


作者:程序员老猫
来源:juejin.cn/post/7343493283073507379
收起阅读 »

Flutter桌面应用开发:深入Flutter for Desktop

Flutter 是一个开源的 UI 工具包,用于构建高性能、高保真、多平台的应用程序,包括移动、Web 和桌面。 安装和环境配置 安装Prerequisites: Java Development Kit (JDK): 安装JDK 8或更高版本,因为Flutt...
继续阅读 »

Flutter 是一个开源的 UI 工具包,用于构建高性能、高保真、多平台的应用程序,包括移动、Web 和桌面。


安装和环境配置


安装Prerequisites:


Java Development Kit (JDK): 安装JDK 8或更高版本,因为Flutter要求JDK 1.8或更高。配置环境变量JAVA_HOME指向JDK的安装路径。
Flutter SDK:


下载Flutter SDK:


访问Flutter官方网站下载适用于Windows的Flutter SDK压缩包。
解压并选择一个合适的目录安装,例如 C:\src\flutter
将Flutter SDK的bin目录添加到系统PATH环境变量中。例如,添加 C:\src\flutter\bin


Git:


如果还没有安装Git,可以从Git官网下载并安装。
在安装过程中,确保勾选 "Run Git from the Windows Command Prompt" 选项。


Flutter Doctor:


打开命令提示符或PowerShell,运行 flutter doctor 命令。这将检查你的环境是否完整,并列出任何缺失的组件,如Android Studio、Android SDK等。


Android Studio (如果计划开发Android应用):


下载并安装Android Studio,它包含了Android SDK和AVD Manager。
安装后,通过Android Studio设置向导配置Android SDK和AVD。
确保在系统环境变量中配置了ANDROID_HOME指向Android SDK的路径,通常是\Sdk


iOS Development (如果计划开发iOS应用):


你需要安装Xcode和Command Line Tools,这些只适用于macOS。
在终端中运行xcode-select --install以安装必要的命令行工具。


验证安装:


运行 flutter doctor --android-licenses 并接受所有许可证(如果需要)。
再次运行 flutter doctor,确保所有必需的组件都已安装并配置正确。


开始开发:


创建你的第一个Flutter项目:flutter create my_first_app
使用IDE(如VS Code或Android Studio)打开项目,开始编写和运行代码。


基础知识


在Flutter桌面应用开发中,Dart语言是核心。基础Flutter应用展示来学习Dart语言魅力:


import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}

class MyHomePage extends StatefulWidget {
MyHomePage({Key? key, required this.title}) : super(key: key);

final String title;

@override
_MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;

void _incrementCounter() {
setState(() {
// This call to setState tells the Flutter framework that something has
// changed in this State object, which causes it to re-build the widget.
_counter++;
});
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}

导入库:



  • import 'package:flutter/material.dart';: 导入Flutter的Material库,包含了许多常用的UI组件。


主入口点:


void main() => runApp(MyApp());: 应用的主入口点,启动MaterialApp。


MyApp StatelessWidget:


MyApp是一个无状态的Widget,用于配置应用的全局属性。


MyHomePage StatefulWidget:



  • MyHomePage是一个有状态的Widget,它有一个状态类_MyHomePageState,用于管理状态。

  • title参数在构造函数中传递,用于初始化AppBar的标题。


_MyHomePageState:



  • _counter变量用于存储按钮点击次数。

  • _incrementCounter方法更新状态,setState通知Flutter需要重建Widget。

  • build方法构建Widget树,根据状态_counter更新UI。


UI组件:



  • Scaffold提供基本的布局结构,包括AppBar、body和floatingActionButton。

  • FloatingActionButton是一个浮动按钮,点击时调用_incrementCounter。

  • Text组件显示文本,AppBar标题和按钮点击次数。

  • ColumnCenter用于布局管理。


Flutter应用


创建项目目录:


选择一个合适的位置创建一个新的文件夹,例如,你可以命名为my_flutter_app。


初始化Flutter项目:


打开终端或命令提示符,导航到你的项目目录,然后运行以下命令来初始化Flutter应用:


   cd my_flutter_app
flutter create .

这个命令会在当前目录下创建一个新的Flutter应用。


检查项目:


初始化完成后,你应该会看到以下文件和文件夹:



  • lib/:包含你的Dart代码,主要是main.dart文件。

  • pubspec.yaml:应用的配置文件,包括依赖项。

  • android/ios/:分别用于Android和iOS的原生项目配置。


运行应用:


为了运行应用,首先确保你的模拟器或物理设备已经连接并准备好。然后在终端中运行:


   flutter run

这将构建你的应用并启动它在默认的设备上。


编辑代码:


打开lib/main.dart文件,这是你的应用的入口点。你可以在这里修改代码以自定义你的应用。例如,你可以修改MaterialApphome属性来指定应用的初始屏幕。


热重载:


当你修改代码并保存时,可以使用flutter pub get获取新依赖,然后按r键(或在终端中输入flutter reload)进行热重载,快速查看代码更改的效果。


布局和组件


Flutter提供了丰富的Widget库来构建复杂的布局。下面是一个使用Row, Column, Expanded, 和 ListView的简单布局示例,展示如何组织UI组件。


import 'package:flutter/material.dart';

void main() {
runApp(MyApp());
}

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Desktop Layout Example',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(
appBar: AppBar(title: Text("Desktop App Layout")),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
RaisedButton(onPressed: () {}, child: Text('Button 1')),
RaisedButton(onPressed: () {}, child: Text('Button 2')),
],
),
SizedBox(height: 20),
Expanded(
child: ListView.builder(
itemCount: 20,
itemBuilder: (context, index) {
return ListTile(title: Text('Item $index'));
},
),
),
],
),
),
);
}
}

Column和Row是基础的布局Widget,Expanded用于占据剩余空间,ListView.builder动态构建列表项,展示了如何灵活地组织UI元素。


状态管理和数据流


在Flutter中,状态管理是通过Widget树中的状态传递和更新来实现的。最基础的是使用StatefulWidgetsetState方法,但复杂应用通常会采用更高级的状态管理方案,如ProviderRiverpodBloc


import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

class Counter with ChangeNotifier {
int _count = 0;

int get count => _count;

void increment() {
_count++;
notifyListeners();
}
}

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => Counter(),
child: MaterialApp(
home: Scaffold(
body: Center(
child: Consumer(
builder: (context, counter, child) {
return Text(
'${counter.count}',
style: TextStyle(fontSize: 24),
);
},
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
Provider.of(context, listen: false).increment();
},
child: Icon(Icons.add),
),
),
),
);
}
}

状态管理示例引入了Provider库,ChangeNotifier用于定义状态,ChangeNotifierProvider在树中提供状态,Consumer用于消费状态并根据状态更新UI,Provider.of用于获取状态并在按钮按下时调用increment方法更新状态。这种方式解耦了状态和UI,便于维护和测试。


路由和导航


Flutter使用Navigator进行页面间的导航。


import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Navigation Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
initialRoute: '/',
routes: {
'/': (context) => HomePage(),
'/details': (context) => DetailsPage(),
},
);
}
}

class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Home Page')),
body: Center(
child: ElevatedButton(
child: Text('Go to Details'),
onPressed: () {
Navigator.pushNamed(context, '/details');
},
),
),
);
}
}

class DetailsPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Details Page')),
body: Center(child: Text('This is the details page')),
);
}
}

MaterialApproutes属性定义了应用的路由表,initialRoute指定了初始页面,Navigator.pushNamed用于在路由表中根据名称导航到新页面。这展示了如何在Flutter中实现基本的页面跳转逻辑。


响应式编程


Flutter的UI是完全响应式的,意味着当状态改变时,相关的UI部分会自动重建。使用StatefulWidgetsetState方法是最直接的实现方式。


import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: CounterPage(),
);
}
}

class CounterPage extends StatefulWidget {
@override
_CounterPageState createState() => _CounterPageState();
}

class _CounterPageState extends State<CounterPage> {
int _counter = 0;

void _incrementCounter() {
setState(() {
_counter++;
});
}

@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}

StatefulWidgetsetState的使用体现了Flutter的响应式特性。当调用_incrementCounter方法更新_counter状态时,Flutter框架会自动调用build方法,仅重绘受影响的部分,实现了高效的UI更新。这种模式确保了UI始终与最新的状态保持一致,无需手动管理UI更新逻辑。


平台交互


Flutter提供了Platform类来与原生平台进行交互。


import 'package:flutter/foundation.dart';

void main() {
runApp(MyApp());
}

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Platform.isAndroid ? AndroidScreen() : DesktopScreen(),
);
}
}

class AndroidScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Text('This is an Android screen');
}
}

class DesktopScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Text('This is a Desktop screen');
}
}

性能优化


优化主要包括减少不必要的渲染、使用高效的Widget和数据结构、压缩资源等。例如,使用const关键字创建常量Widget以避免不必要的重建:


class MyWidget extends StatelessWidget {
const MyWidget({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
return Container(
color: Colors.blue,
child: const Text('Optimized Widget', style: TextStyle(fontSize: 24)),
);
}
}

调试和测试


Flutter提供了强大的调试工具,如热重载、断点、日志输出等。测试方面,可以使用test包进行单元测试和集成测试:


import 'package:flutter_test/flutter_test.dart';

void main() {
test('Counter increments correctly', () {
final counter = Counter(0);
expect(counter.value, equals(0));
counter.increment();
expect(counter.value, equals(1));
});
}

打包和发布


发布Flutter应用需要构建不同平台的特定版本。在桌面环境下,例如Windows,可以使用以下命令:


flutter build windows

这将生成一个.exe文件,可以分发给用户。确保在pubspec.yaml中配置好应用的元数据,如版本号和描述。


Flutter工作原理分析


Flutter Engine:



  • Flutter引擎是Flutter的基础,它负责渲染、事件处理、文本布局、图像解码等功能。引擎是用C++编写的,部分用Java或Objective-C/Swift实现原生平台的接口。

  • Skia是Google的2D图形库,用于绘制UI。在桌面应用中,Skia直接与操作系统交互,提供图形渲染。

  • Dart VM运行Dart代码,提供垃圾回收和即时编译(JIT)或提前编译(AOT)。


Flutter Framework:



  • Flutter框架是用Dart编写的,它定义了Widget、State和Layout等概念,以及动画、手势识别和数据绑定等机制。

  • WidgetsFlutterBinding是框架与引擎的桥梁,它实现了将Widget树转换为可绘制的命令,这些命令由引擎执行。


Widgets:



  • Flutter中的Widget是UI的构建块,它们是不可变的。StatefulWidget和State类用于管理可变状态。

  • 当状态改变时,setState方法被调用,导致Widget树重新构建,进而触发渲染。


Plugins:



  • 插件是Flutter与原生平台交互的方式,它们封装了原生API,使得Dart代码可以访问操作系统服务,如文件系统、网络、传感器等。

  • 桌面应用的插件需要针对每个目标平台(Windows、macOS、Linux)进行实现。


编译和运行流程:



  • 使用flutter build命令,Dart代码会被编译成原生代码(AOT编译),生成可执行文件。

  • 运行时,Flutter引擎加载并执行编译后的代码,同时初始化插件和设置渲染管线。


调试和热重载:



  • Flutter支持热重载,允许开发者在运行时快速更新代码,无需重新编译整个应用。

  • 调试工具如DevTools提供了对应用性能、内存、CPU使用率的监控,以及源代码级别的调试。


性能优化:



  • Flutter通过AOT编译和Dart的垃圾回收机制来提高性能。

  • 使用const关键字创建Widget可以避免不必要的重建,减少渲染开销。


作者:天涯学馆
来源:juejin.cn/post/7378015213347913791
收起阅读 »

一条SQL 最多能查询出来多少条记录?

问题 一条这样的 SQL 语句能查询出多少条记录? select * from user 表中有 100 条记录的时候能全部查询出来返回给客户端吗? 如果记录数是 1w 呢? 10w 呢? 100w 、1000w 呢? 虽然在实际业务操作中我们不会这么干,...
继续阅读 »

问题


一条这样的 SQL 语句能查询出多少条记录?


select * from user 

表中有 100 条记录的时候能全部查询出来返回给客户端吗?


如果记录数是 1w 呢? 10w 呢? 100w 、1000w 呢?


虽然在实际业务操作中我们不会这么干,尤其对于数据量大的表不会这样干,但这是个值得想一想的问题。


寻找答案


前提:以下所涉及资料全部基于 MySQL 8


max_allowed_packet


在查询资料的过程中发现了这个参数 max_allowed_packet



上图参考了 MySQL 的官方文档,根据文档我们知道:



  • MySQL 客户端 max_allowed_packet 值的默认大小为 16M(不同的客户端可能有不同的默认值,但最大不能超过 1G)

  • MySQL 服务端 max_allowed_packet 值的默认大小为 64M

  • max_allowed_packet 值最大可以设置为 1G(1024 的倍数)


然而 根据上图的文档中所述



The maximum size of one packet or any generated/intermediate string,or any parameter sent by the mysql_smt_send_long_data() C API function




  • one packet

  • generated/intermediate string

  • any parameter sent by the mysql_smt_send_long_data() C API function


这三个东东具体都是什么呢? packet 到底是结果集大小,还是网络包大小还是什么? 于是 google 了一下,搜索排名第一的是这个:



根据 “Packet Too Large” 的说明, 通信包 (communication packet) 是



  • 一个被发送到 MySQL 服务器的单个 SQL 语句

  • 或者是一个被发送到客户端的单行记录

  • 或者是一个从主服务器 (replication source server) 被发送到从属服务器 (replica) 的二进制日志事件。


1、3 点好理解,这也同时解释了,如果你发送的一条 SQL 语句特别大可能会执行不成功的原因,尤其是insert update 这种,单个 SQL 语句不是没有上限的,不过这种情况一般不是因为 SQL 语句写的太长,主要是由于某个字段的值过大,比如有 BLOB 字段。


那么第 2 点呢,单行记录,默认值是 64M,会不会太大了啊,一行记录有可能这么大的吗? 有必要设置这么大吗? 单行最大存储空间限制又是多少呢?


单行最大存储空间


MySQL 单行最大宽度是 65535 个字节,也就是 64KB 。无论是 InnoDB 引擎还是 MyISAM 引擎。



通过上图可以看到 超过 65535 不行,不过请注意其中的错误提示:“Row size too large. The maximum row size for the used table type, not counting BLOBs, is 65535” ,如果字段是变长类型的如 BLOB 和 TEXT 就不包括了,那么我们试一下用和上图一样的字段长度,只把最后一个字段的类型改成 BLOB 和 TEXT


mysql> CREATE TABLE t (a VARCHAR(10000), b VARCHAR(10000),
c VARCHAR(10000), d VARCHAR(10000), e VARCHAR(10000),
f VARCHAR(10000), g TEXT(6000)) ENGINE=InnoDB CHARACTER SET latin1;
Query OK, 0 rows affected (0.02 sec)

可见无论 是改成 BLOB 还是 TEXT 都可以成功。但这里请注意,字符集是 latin1 可以成功,如果换成 utf8mb4 或者 utf8mb3 就不行了,会报错,仍然是 :“Row size too large. The maximum row size for the used table type, not counting BLOBs, is 65535.” 为什么呢?


因为虽然不包括 TEXT 和 BLOB, 但总长度还是超了!


我们先看一下这个熟悉的 VARCHAR(255) , 你有没有想过为什么用 255,不用 256?



在 4.0 版本以下,varchar(255) 指的是 255 个字节,使用 1 个字节存储长度即可。当大于等于 256 时,要使用 2 个字节存储长度。所以定义 varchar(255) 比 varchar(256) 更好。


但是在 5.0 版本以上,varchar(255) 指的是 255 个字符,每个字符可能占用多个字节,例如使用 UTF8 编码时每个汉字占用 3 字节,使用 GBK 编码时每个汉字占 2 字节。



例子中我们用的是 MySQL8 ,由于字符集是 utf8mb3 ,存储一个字要用三个字节, 长度为 255 的话(列宽),总长度要 765 字节 ,再加上用 2 个字节存储长度,那么这个列的总长度就是 767 字节。所以用 latin1 可以成功,是因为一个字符对应一个字节,而 utf8mb3 或 utf8mb4 一个字符对应三个或四个字节,VARCHAR(10000) 就可能等于要占用 30000 多 40000 多字节,比原来大了 3、4 倍,肯定放不下了。


另外,还有一个要求,列的宽度不要超过 MySQL 页大小 (默认 16K)的一半,要比一半小一点儿。 例如,对于默认的 16KB InnoDB 页面大小,最大行大小略小于 8KB。


下面这个例子就是超过了一半,所以报错,当然解决办法也在提示中给出了。


mysql> CREATE TABLE t4 (
c1 CHAR(255),c2 CHAR(255),c3 CHAR(255),
c4 CHAR(255),c5 CHAR(255),c6 CHAR(255),
c7 CHAR(255),c8 CHAR(255),c9 CHAR(255),
c10 CHAR(255),c11 CHAR(255),c12 CHAR(255),
c13 CHAR(255),c14 CHAR(255),c15 CHAR(255),
c16 CHAR(255),c17 CHAR(255),c18 CHAR(255),
c19 CHAR(255),c20 CHAR(255),c21 CHAR(255),
c22 CHAR(255),c23 CHAR(255),c24 CHAR(255),
c25 CHAR(255),c26 CHAR(255),c27 CHAR(255),
c28 CHAR(255),c29 CHAR(255),c30 CHAR(255),
c31 CHAR(255),c32 CHAR(255),c33 CHAR(255)
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC DEFAULT CHARSET latin1;
ERROR 1118 (42000): Row size too large (> 8126). Changing some columns to TEXT or BLOB may help.
In current row format, BLOB prefix of 0 bytes is stored inline.

那么为什么是 8K,不是 7K,也不是 9K 呢? 这么设计的原因可能是:MySQL 想让一个数据页中能存放更多的数据行,至少也得要存放两行数据(16K)。否则就失去了 B+Tree 的意义。B+Tree 会退化成一个低效的链表。


你可能还会奇怪,不超过 8K ?你前面的例子明明都快 64K 也能存下,那 8K 到 64K 中间这部分怎么解释?


答:如果包含可变长度列的行超过 InnoDB 最大行大小, InnoDB 会选择可变长度列进行页外存储,直到该行适合 InnoDB ,这也就是为什么前面有超过 8K 的也能成功,那是因为用的是VARCHAR这种可变长度类型。



当你往这个数据页中写入一行数据时,即使它很大将达到了数据页的极限,但是通过行溢出机制。依然能保证你的下一条数据还能写入到这个数据页中。


我们通过 Compact 格式,简单了解一下什么是 页外存储行溢出


MySQL8 InnoDB 引擎目前有 4 种 行记录格式:



  • REDUNDANT

  • COMPACT

  • DYNAMIC(默认 default 是这个)

  • COMPRESSED


行记录格式 决定了其行的物理存储方式,这反过来又会影响查询和 DML 操作的性能。



Compact 格式的实现思路是:当列的类型为 VARCHAR、 VARBINARY、 BLOB、TEXT 时,该列超过 768byte 的数据放到其他数据页中去。



在 MySQL 设定中,当 varchar 列长度达到 768byte 后,会将该列的前 768byte 当作当作 prefix 存放在行中,多出来的数据溢出存放到溢出页中,然后通过一个偏移量指针将两者关联起来,这就是 行溢出机制



假如你要存储的数据行很大超过了 65532byte 那么你是写入不进去的。假如你要存储的单行数据小于 65535byte 但是大于 16384byte,这时你可以成功 insert,但是一个数据页又存储不了你插入的数据。这时肯定会行溢出!



MySQL 这样做,有效的防止了单个 varchar 列或者 Text 列太大导致单个数据页中存放的行记录过少的情况,避免了 IO 飙升的窘境。


单行最大列数限制


mysql 单表最大列数也是有限制的,是 4096 ,但 InnoDB 是 1017



实验


前文中我们疑惑 max_allowed_packet 在 MySQL8 的默认值是 64M,又说这是限制单行数据的,单行数据有这么大吗? 在前文我们介绍了行溢出, 由于有了 行溢出 ,单行数据确实有可能比较大。


那么还剩下一个问题,max_allowed_packet 限制的确定是单行数据吗,难道不是查询结果集的大小吗 ? 下面我们做个实验,验证一下。


建表


CREATE TABLE t1 (
c1 CHAR(255),c2 CHAR(255),c3 CHAR(255),
c4 CHAR(255),c5 CHAR(255),c6 CHAR(255),
c7 CHAR(255),c8 CHAR(255),c9 CHAR(255),
c10 CHAR(255),c11 CHAR(255),c12 CHAR(255),
c13 CHAR(255),c14 CHAR(255),c15 CHAR(255),
c16 CHAR(255),c17 CHAR(255),c18 CHAR(255),
c19 CHAR(255),c20 CHAR(255),c21 CHAR(255),
c22 CHAR(255),c23 CHAR(255),c24 CHAR(255),
c25 CHAR(255),c26 CHAR(255),c27 CHAR(255),
c28 CHAR(255),c29 CHAR(255),c30 CHAR(255),
c31 CHAR(255),c32 CHAR(192)
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC DEFAULT CHARSET latin1;


经过测试虽然提示的是 Row size too large (> 8126) 但如果全部长度加起来是 8126 建表不成功,最终我试到 8097 是能建表成功的。为什么不是 8126 呢 ?可能是还需要存储一些其他的东西占了一些字节吧,比如隐藏字段什么的。


用存储过程造一些测试数据,把表中的所有列填满


create
definer = root@`%` procedure generate_test_data()
BEGIN
DECLARE i INT DEFAULT 0;
DECLARE col_value TEXT DEFAULT REPEAT('a', 255);
WHILE i < 5 DO
INSERT INTO t1 VALUES
(
col_value, col_value, col_value,
col_value, REPEAT('b', 192)
);
SET i = i + 1;
END WHILE;
END;


max_allowed_packet 设置的小一些,先用 show VARIABLES like '%max_allowed_packet%'; 看一下当前的大小,我的是 67108864 这个单位是字节,等于 64M,然后用 set global max_allowed_packet =1024 将它设置成允许的最小值 1024 byte。 设置好后,关闭当前查询窗口再新建一个,然后再查看:



这时我用 select * from t1; 查询表数据时就会报错:



因为我们一条记录的大小就是 8K 多了,所以肯定超过 1024byte。可见文档的说明是对的, max_allowed_packet 确实是可以约束单行记录大小的。


答案


文章写到这里,我有点儿写不下去了,一是因为懒,另外一个原因是关于这个问题:“一条 SQL 最多能查询出来多少条记录?” 肯定没有标准答案


目前我们可以知道的是:



  • 你的单行记录大小不能超过 max_allowed_packet

  • 一个表最多可以创建 1017 列 (InnoDB)

  • 建表时定义列的固定长度不能超过 页的一半(8k,16k...)

  • 建表时定义列的总长度不能超过 65535 个字节


如果这些条件我们都满足了,然后发出了一个没有 where 条件的全表查询 select * 那么.....


首先,你我都知道,这种情况不会发生在生产环境的,如果真发生了,一定是你写错了,忘了加条件。因为几乎没有这种要查询出所有数据的需求。如果有,也不能开发,因为这不合理。


我考虑的也就是个理论情况,从理论上讲能查询出多少数据不是一个确定的值,除了前文提到的一些条件外,它肯定与以下几项有直接的关系



  • 数据库的可用内存

  • 数据库内部的缓存机制,比如缓存区的大小

  • 数据库的查询超时机制

  • 应用的可用物理内存

  • ......


说到这儿,我确实可以再做个实验验证一下,但因为懒就不做了,大家有兴趣可以自己设定一些条件做个实验试一下,比如在特定内存和特定参数的情况下,到底能查询出多少数据,就能看得出来了。


虽然我没能给出文章开头问题的答案,但通过寻找答案也弄清楚了 MySQL 的一些限制条件,并加以了验证,也算是有所收获了。


参考



作者:xiaohezi
来源:juejin.cn/post/7255478273652834360
收起阅读 »

使用uniapp制作安卓app容器

1. 背景项目需要做一个安卓app,而且不需要上架应用市场,部门也没有安卓开发,想着就套个webview就行了吧。没有选择react native之类的是因为这些工具需要安装很多环境工具,我只是开发一个壳子没必要这么复杂。用webview也方便快速修复页面问题...
继续阅读 »

1. 背景

项目需要做一个安卓app,而且不需要上架应用市场,部门也没有安卓开发,想着就套个webview就行了吧。没有选择react native之类的是因为这些工具需要安装很多环境工具,我只是开发一个壳子没必要这么复杂。

webview也方便快速修复页面问题。

所以最后选择了uniapp,但是uniapp本身就是套在一个大的webview下的, 所以再套一个webview难免会有一些意想不到的问题,下面就是一些踩过的坑记录。

2. 项目初始化

新建项目就默认模板就行,我只需要壳子。

image.png 启动了之后可以看到有两个调试工具

image.png

第一个就是网页上常用的vue调试工具,可以看到vue组件属性啥的,第二个就是类似chrome的控制台,但是无法查看元素,还有就是必须让设备和电脑在同一个网段下才行,不然连接不上。

hbuilder的控制台本身也有一些输出,比如页面的console

image.png

但是这里输出对象的时候不是很方便查看,如果你需要的话就打开上面说的第二个调试工具。

3. webview使用

整个项目很简单,大概就这样一个页面

<template>
<web-view :src='PROJECT_PATH' @message="onMessage">web-view>
template>
<script>
// ...
script>

3.1 网页与app通信

这是最重要的一个功能,可以参考官方文档

网页和app交互总结起来就是这两点:

  • 网页 -> APPwindow.uni.postMessage();
  • APP -> 网页webview.evalJS()

3.1.1. 网页 -> APP

首先要在项目中引入uni.webview.js,这个就相当于jsbridge,可以让网页操作uniapp

初始化完成后会在window上挂载一个uni对象,通过uni.postMessage就能往app发送消息,app中监听onMessage就行。

这里有几个小坑:

  1. 发送的格式window.uni.postMessage({ data: 数据 }),必须要有个字段data,这样app才能收到数据。源码

image.png 2. 发送的数据不需要序列化成字符串,uniapp会转换json。 3. appmessage事件中接收到事件参数应该这样解构

function onMessage(e) {
const {
type,
data
} = e.detail.data[0]
}

3.1.2. APP -> 网页

app向网页传输消息就直接调用网页的js就行了。这里我统一封装了一个函数:

// app向网页发送消息
const deliverMessage = (msg) => {
// 调用webview中的deliverMessage函数
// 这个函数是我在网页挂载的一个全局函数,调用deliverMessage后会触发页面中的一些事件
currentWebview.evalJS(`deliverMessage(${JSON.stringify(msg)})`)
}

上面的代码例子中出现的currentWebview需要我们自己去获取。

// vue2中
const rootWebview = this.$scope.$getAppWebview()
this.currentWebview = rootWebview.children()[0]

// vue3中
import {
getCurrentInstance,
ref,
} from "vue";
const currentWebview = ref(null)
const vueInstance = getCurrentInstance()
const rootWebview = vueInstance.proxy.$scope.$getAppWebview()
currentWebview.value = rootWebview.children()[0]

这里也有一个坑,rootWebview.children()如果你一渲染就获取是无法获取到webview实例的,具体原因没有深入研究,估计是异步的原因

这里提供两个思路:

  1. 加一个定时器,延迟获取webview,这个方法虽然听起来不保险,但是实际测试还是挺稳当的。关键是简单
setTimeout(() => {
currentWebview.value = rootWebview.children()[0]
}, 1000)
  1. 你要是觉得定时器不保险,那就使用plusapi手动创建webview。但是消息处理这块比较麻烦。官网参考
<template>

template>
// 我这里vue3为例
onMounted(() => {
plus.globalEvent.addEventListener('plusMessage', ({data: {type, args}}) => {
// 是网页调用uni的api
if(type === 'WEB_INVOKE_APPSERVICE') {
const {data: {name, arg}} = args
// 是发送消息事件
if(name === 'postMessage') {
// arg就是传过来的数据
}
}
})
const wv = plus.webview.create("", "webview", {
'uni-app': 'none',
})
wv.loadURL(网页地址)
rootWebview.append(wv);
})

plus.globalEvent.addEventListener这个是翻源码找到的,主要是我不想改uni.webview.js的源码,所以只有找到正确的监听事件。

WEB_INVOKE_APPSERVICEuniapp内部定义的一个名字,反正就是用来交互操作的命名空间。

这样基础的互操作就有了。

3.1.3. 整个流程

  1. 网页调用window.uni.postMessage({ data }) => app监听(用组件的onMessage或者自定义的globalEvent
  2. app调用网页定义的函数deliverMessage并传递参数,网页中的deliverMessage内部处理监听
// 网页中的deliverMessage
window.deliverMessage = (msg) => {
// 触发网页注册的监听器
eventListeners.forEach((listener) => {

});
};

3.2. 返回拦截

默认情况下,手机按下返回键,app会响应提示是否退出,但是实际我需要网页进入二级路由的时候,按下手机返回键是返回上一级路由而不是退出。当路由是一级路由时才提示是否退出app

import {
onBackPress,
onShow,
} from '@dcloudio/uni-app'
// 页面当前的路由信息
const pageRoute = shallowRef()
onBackPress(() => {
// tab页正常app返回逻辑
if (pageRoute.value?.isTab) {
return false
} else {
// 二级路由拦截app返回
return true
}
})

pageRoute是页面当前路由信息,页面通过监听路由变化触发routeChange事件,将路由信息传给app。当按下返回键的时候,判断当前路由配置是不是tab页,如果是就正常退出,不是就拦截返回。

4. 总结

有了通信功能,很多操作就可以实现了,比如获取设备safeArea,获取设备联网状态等等。


作者:头上有煎饺
来源:juejin.cn/post/7313740940773097482

收起阅读 »

跟一位 40+ 岁的同学沟通之后,差点泪崩

个人情况 这位同学咱们叫他【大哥】吧。 大哥 今年 42 岁了,03 年毕业,专科学历(那个时候的专科学历还是很值钱的),从毕业之后一直都在做开发相关的工作。接触过 c、c#、.net、java、android、前端 整个的技术栈还是非常丰富的(毕竟 20多年...
继续阅读 »



个人情况


这位同学咱们叫他【大哥】吧。


大哥 今年 42 岁了,03 年毕业,专科学历(那个时候的专科学历还是很值钱的),从毕业之后一直都在做开发相关的工作。接触过 c、c#、.net、java、android、前端 整个的技术栈还是非常丰富的(毕竟 20多年 的工作经验)。


期间也进入过一些大厂,比如:微博、阿里 等。目前在北方的某二线城市,刚经历了裁员。



现在的行情说起“裁员”大家不要感觉很丢人,很多时候被裁员并不是因为你的个人能力不行,仅仅只是因为 公司盈利下降,甚至持续亏损 所导致的。



目前,找工作接近 3 个月,面试寥寥无几,拿到的几个 offer 也都薪资跌幅巨大(40% 以上)。


大哥 一直认为自己是非常努力的那批人(不努力当初也进不了大厂),一直在苦心钻研技术,并且尝试各种架构以及解决方案,甚至为此放弃了一些陪伴家人的时间。


同时,因为一直在关注技术,大哥 也出现了一些 程序员常见的问题 就是 不善与人交流、不善于表达自己的感受。


成长的模式


每个人都想变得更好,但只有一些人有勇气做出改变


对于很多人来说,我们都喜欢循规蹈矩,按照固定的方式进行生活。如果你是学生,那么每天都会重复 上课、下课、打游戏的生活。如果你是职场人,每天都会重复 上班、下班、加班 的生活。这会让你在短时间内找到自己的价值,或者找到你认为的价值。


这样的生活很轻松,但却会让我们陷入到一个固化的模式之中。世界是不断变化的,一旦变化来临(裁员),那么我们会变得无所是从。


所以,不要陷入固化的模式之中


如果你是一名学生,你希望取得好成绩,同时也保持社交生活。你会想出去,和朋友喝咖啡,打球、谈恋爱,尽情享受青春岁月。如果你是一名职业人士,固定的薪水会让你变得懒惰。你醒来,去上班,开始做任务,到了一天结束时,你就没有精力拿起笔记本电脑做其他事情了。写作、游戏或者其他的等爱好都会被束之高阁。


你看,这是另一种模式。它会让你拥有 更多的可变性,从而开始适应变化!


关于努力的一些谬论


我见过很多 35+ 以上的开发者,有的现在已经可以按照自己的期望进行生活,但是更多的目前依然无法选择自己的生活方式。


其实大家都很努力,但是结果却截然不同。


网上有各种信息告诉我们需要努力,但是却从没有告诉我们 应该如何选择努力的方向


从而导致很多同学会认为,身为程序员,我们只需要专注技术就可以了。殊不知 “学的数理化,走遍天下都不怕” 的时代已经过去了。



努力是对的,但是我们需要找到正确的方向。“方向不对,努力白费” 这句话,大家应该都非常熟悉



所以,尝试寻找自己适合的方向,不要 尽信 一些 “鸡汤文章”。如果你没有找到自己的路,而是遵循别人设定的规则,当结果没有出现时,你就会认为这是自己的错,从而陷入内耗。


殊不知,很大的可能是因为 这些并不适合你。就像你明明是一个人,却非要学习如何挥动翅膀一样。


尽信书,不如无书。找到自己适合的方式。



  1. 先想清楚,你想要成为一个什么样的人

  2. 然后明白,这样的人需要具备什么能力

  3. 对你来说,如何可以练习拥有这些能力

  4. 坚持或者放弃,取决于你的目标是否发生了变化


每个人都需要寻找自己的平衡


生活会变得越来越好,这句话其实是 错误的


生活一定是一个波浪,起起伏伏才是常态:



在完善自己的过程中,也要尝试接受自己


很多同学会在自己没有达到期望的时候 “惩罚自己”,认为这是自己的错误,或者是能力问题。从而更加逼迫自己努力。


但是逼迫来的努力,结果通常不会太好。就像 如果你把减肥认为是一个痛苦的过程,那么你就很难减肥成功一样


所以,尽量尝试获得 即时反馈



  • 当你获得一些成就时,为自己庆祝一下。找一些朋友进行分享

  • 不要过分执着于结果,只要航线没有偏离即可

  • 培养一些好奇心,很多东西都是有关联的,不要闭门造车

  • 多思考"为什么",寻找一些事物的本质原因,同时也提醒自己 不要忘记自己为什么开始~

作者:程序员Sunday
来源:juejin.cn/post/7379865729024737292
收起阅读 »

前任开发在代码里下毒了,支付下单居然没加幂等

故事又是一个风和日丽没好的一天,小猫戴着耳机,安逸地听着音乐,撸着代码,这种没有会议的日子真的是巴适得板。不料祸从天降,组长火急火燎地跑过来找到了小猫。“快排查一下,目前有A公司用户反馈积分被多扣了”。小猫回忆了一下“不对啊,这接口我也没动过啊,前几天对外平台...
继续阅读 »

故事

又是一个风和日丽没好的一天,小猫戴着耳机,安逸地听着音乐,撸着代码,这种没有会议的日子真的是巴适得板。

不料祸从天降,组长火急火燎地跑过来找到了小猫。“快排查一下,目前有A公司用户反馈积分被多扣了”。

小猫回忆了一下“不对啊,这接口我也没动过啊,前几天对外平台的老六直接找我要个支付接口,我就给他了的,以前的代码,我都没有动过的......”。

于是小猫一边疑惑一边翻看着以前的代码,越看脸色越差......

42175B273A64E95B1B5B66D392256552.jpg

小猫做的是一个标准的积分兑换商城,以前和客户合作的时候,客户直接用的是小猫单位自己定制的h5页面。这次合作了一家公司有点特殊,由于公司想要定制化自己个性化的H5,加上本身A公司自己有开发能力,所以经过讨论就以接口的方式直接将相关接口给出去,A客户H5开发完成之后自己来对接。

慢慢地,原因也水落石出,之前好好的业务一直没有问题是因为商城的本身H5页面做了防重复提交,由于量小,并且一般对接方式用的都是纯H5,所以都没有什么问题,然后这次是直接将接口给出去了,完了接口居然没有加幂等......

小猫躺枪,数据订正当然是少不了了,事故报告当然也少不了了。

正所谓前人挖坑,后人遭殃,前人锅后人背。

聊聊幂等

接口幂等梗概

这个案例其实就是一个典型的接口幂等案例。那么老猫就和大家从以下几个方面好好剖析一下接口幂等吧。

interfacemd.png

什么是接口幂等

比较专业的术语:其任意多次执行所产生的影响均与第一次执行的影响相同。 大白话:多次调用的情况下,接口最终得到的结果是一致的。

那么为什么需要幂等呢?

  1. 用户进行提交动作的时候,由于网络波动等原因导致后端同步响应不及时,这样用户就会一直点点点,这样机会发生重复提交的情况。
  2. 分布式系统之间调用的情况下,例如RPC调用,为了防止网络波动超时等造成的请求失败,都会添加重试机制,导致一个请求提交多次。
  3. 分布式系统经常会用到消息中间件,当由于网络原因,mq没有收到ack的情况下,就会导致消息的重复投递,从而就会导致重复提交行为。
  4. 还有就是恶意攻击了,有些业务接口做的比较粗糙,黑客找到漏洞之后会发起重复提交,这样就会导致业务出现问题。打个比方,老猫曾经干过,邻居小孩报名了一个画画比赛,估计是机构培训发起的,功能做的也差,需要靠投票赢得某些礼品,然后老猫抓到接口信息之后就模拟投票进行重复刷了投票。

那么哪些接口需要做幂等呢?

首先我们说是不是所有的接口都需要幂等?是不是加了幂等就好呢?显然不是。 因为接口幂等的实现某种意义上是要消耗系统性能的,我们没有必要针对所有业务接口都加上幂等。

这个其实并不能做一个完全的定义说哪个就不用幂等,因为很多时候其实还是得结合业务逻辑一起看。但是其中也是有规律可循的。

既然我们说幂等就是多次调用,接口最终得到结果一致,那么很显然,查询接口肯定是不要加幂等的,另外一些简单删除数据的接口,无论是逻辑删除还是物理删除,看场景的情况下其实也不用加幂等。

但是大部分涉及到多表更新行为的接口,咱们最好还是得加上幂等。

接口幂等实战方案

前端防抖处理

前端防抖主要可以有两种方案,一种是技术层面的,一种是产品层面的:

  1. 技术层面:例如提交控制在100ms内,同一个用户最多只能做一次订单提交的操作。
  2. 产品层面:当然用户点击提交之后,按钮直接置灰。

基于数据库唯一索引

  1. 利用数据库唯一索引。我们具体来看一下流程,咱们就用小猫遇到的例子。如下:

unique-key.png

过程描述:

  • 建立一张去重表,其中某个字段需要建立唯一索引,例如小猫这个场景中,咱们就可以将订单提交流水单号作为唯一索引存储到我们的数据库中,就模型上而言,可以将其定义为支付请求流水表。
  • 客户端携带相关流水信息到后端,如果发现编号重复,那么此时就会插入失败,报主键冲突的错误,此时我们针对该错误做一下业务报错的二次封装给到客户另一个友好的提示即可。

数据库乐观锁实现

什么是乐观锁,它假设多用户并发的事务在处理时不会彼此互相影响,各事务能够在不产生锁的情况下处理各自影响的那部分数据。 说得直白一点乐观锁就是一个马大哈。总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,只在更新的时候会判断一下在此期间别人有没有去更新这个数据。

例如提交订单的进行支付扣款的时候,本来可能更新账户金额扣款的动作是这样的:

update Account set balance = balance-#{payAmount} where accountCode = #{accountCode}

加上版本号之后,咱们的代码就是这样的。

update Account set balance = balance-#{payAmount},version=version +1 where accountCode = #{accountCode} and version = #{currVersion}

这种情况下其实就要求客户端每次在请求支付下单的时候都需要上层客户端指定好当前的版本信息。 不过这种幂等的处理方式,老猫用的比较少。

数据库悲观锁实现

悲观锁的话具有强烈的独占和排他特性。大白话谁都不信的主。所以我们就用select ... for update这样的语法进行行锁,当然老猫觉得单纯的select ... for update只能解决同一时刻大并发的幂等,所以要保证单号重试这样非并发的幂等请求还是得去校验当前数据的状态才行。就拿当前的小猫遇到的场景来说,流程如下:

pessimistic.png

begin;  # 1.开始事务
select * from order where order_code='666' for update # 查询订单,判断状态,锁住这条记录
if(status !=处理中){
//非处理中状态,直接返回;
return ;
}
## 处理业务逻辑
update order set status='完成' where order_code='666' # 更新完成
update stock set num = num - 1 where spu='xxx' # 库存更新
commit; # 5.提交事务

这里老猫一再想要强调的是在校验的时候还是得带上本身的业务状态去做校验,select ... for update并非万能幂等。

后端生成token

这个方案的本质其实是引入了令牌桶的机制,当提交订单的时候,前端优先会调用后端接口获取一个token,token是由后端发放的。当然token的生成方式有很多种,例如定时刷新令牌桶,或者定时生成令牌并放到令牌池中,当然目的只有一个就是保住token的唯一性即可。

生成token之后将token放到redis中,当然需要给token设置一个失效时间,超时的token也会被删除。

当后端接收到订单提交的请求的时候,会先判断token在缓存中是否存在,第一次请求的时候,token一定存在,也会正常返回结果,但是第二次携带同一个token的时候被拒绝了。

流程如下:

token.png

有个注意点大家可以思考一下: 如果用户用程序恶意刷单,同一个token发起了多次请求怎么办? 想要实现这个功能,就需要借助分布式锁以及Lua脚本了,分布式锁可以保证同一个token不能有多个请求同时过来访问,lua脚本保证从redis中获取令牌->比对令牌->生成单号->删除令牌这一系列行为的原子性。

分布式锁+状态机(订单状态)

现在很多的业务服务都是分布式系统,所以就拿分布式锁来说,关于分布式锁,老猫在此不做赘述,之前老猫写过redis的分布式锁和实现,还有zk锁和实现,具体可见链接:

当然和上述的数据库悲观锁类似,咱们的分布式锁也只能保证同一个订单在同一时间的处理。其次也是要去校订单的状态,防止其重复支付的,也就是说,只要支付的订单进入后端,都要将原先的订单修改为支付中,防止后续支付中断之后的重复支付。

在上述小猫的流程中还没有涉及到现金补充,如果涉及到现金补充的话,例如对接了微信或者支付宝的情况,还需要根据最终的支付回调结果来最终将订单状态进行流转成支付完成或者是支付失败。

总结

在我们日常的开发中,一些重要的接口还是需要大家谨慎对待,即使是前任开发留下的接口,没有任何改动,当有人咨询的时候,其实就要好好去了解一下里面的实现,看看方案有没有问题,看看技术实现有没有问题,这应该也是每一个程序员的基本素养。

另外的,在一些重要的接口上,尤其是资金相关的接口上,幂等真的是相当的重要。小伙伴们,你们觉得呢?如果大家还有好的解决方案,或者有其他思考或者意见也欢迎大家的留言。


作者:程序员老猫
来源:juejin.cn/post/7324186292297482290
收起阅读 »

Node.js 正在衰退吗?通过一些关键指标告诉你事实如何!

web
关于 “Node.js 凉了吗?” 类似话题大家平常在某乎上也有看到过。 近日 Node.js 官方 Twitter 上转载了一则帖子,看来国外也有此讨论。Node.js TSC 成员 & fastifyjs 首席维护者 @Matteo Collin...
继续阅读 »

关于 “Node.js 凉了吗?” 类似话题大家平常在某乎上也有看到过。



近日 Node.js 官方 Twitter 上转载了一则帖子,看来国外也有此讨论。Node.js TSC 成员 & fastifyjs 首席维护者 @Matteo Collina 对此进行了回复,表示关于 Node.js 衰退的传言被大大夸大了。Node.js 不仅不会消失,而且正在积极进化以满足现代 Web 开发的需求



以下内容翻译自 @Matteo Collina 的博文


在过去的 15 年里,Node.js 一直是 Web 开发的基石。自 2009 年发布以来,它从一个简单的小众技术,发展到如今支持超过 630 万个网站、无数的 API,并被财富 500 强中的 98% 所使用。


作为一个强大的开源运行时环境,Node.js 非常适合数字化转型的挑战。基于熟悉的 JavaScript 基础,Node.js 拥有轻量且事件驱动的架构,这使其非常适合构建可扩展的实时应用程序,能够处理大量并发请求——这是当今 API 驱动世界的关键需求。


结合其活跃且不断增长的开源社区以及 OpenJS 基金会的强力支持,Node.js 已成为当代 Web 开发的支柱。


但最近,有关 Node.js 衰落的传言开始流传。这些说法有多少可信度呢?


在这篇博客中,我们将深入探讨一些关键指标,这些指标描绘了一个繁荣的 Node.js 生态系统,并展现了其光明的未来。我们还将看看已经发布并即将在 Node.js 上推出的主要功能。


技术是永无止境的循环


有些人可能认为新技术不可避免地会使旧技术过时。但事实上,进步往往是建立在现有基础之上的。以 COBOL 为例,这种编程语言创建于 1959 年,今天仍在积极使用。虽然它可能不是前沿 Web 开发的首选,但 COBOL 在银行、金融和政府机构的核心业务系统维护中仍然至关重要。根据最新的 Tiobe 指数,COBOL 正在上升,其受欢迎程度在 Ruby 和 Rust 之间。其持久的相关性突显了一个关键点:技术进步并不总是意味着抛弃过去。


COBOL 正在崛起(来源: tiobe.com/tiobe-index)


让我们考虑另一个 Web 开发领域的老将:jQuery。这款 JavaScript 库比 Node.js 早三年发布,拥有令人印象深刻的使用统计数据——超过 95% 的 JavaScript 网站和 77% 的所有网站都在使用它。jQuery 的持久受欢迎程度表明,技术的年龄并不一定决定其相关性。就像 jQuery 一样,Node.js 尽管更年轻,但也有潜力保持其作为 Web 开发人员宝贵工具的地位。


94.4% 支持 JS 的网站都使用了 jQuery -(来源: w3techs.com/technologies/overview/javascrip..)


Node.js 目前的势头


根据 StackOverflow 的调查,Node.js 是最受欢迎的技术。这种成功依赖于 Node.js 和 npm 注册表的强大组合。这个创新的二人组解决了大规模软件复用的挑战,这是以前无法实现的。


来源:StackOverflow


因此,预先编写的代码模块的使用激增,巩固了 Node.js 作为开发强国的地位。



Readable-stream 的下载量从 2022 年的略高于 30 亿增长到 2023 年的接近 70 亿,意味着使用量在三年内翻了一番。


Node.js 的总下载量:Node.js 每月有高达 1.3 亿的下载量。


然而,理解这一数字包含什么很重要。这些下载量中的很大一部分实际上是头文件。在 npm i 命令期间,这些头文件是临时下载的,用于编译二进制插件。编译完成后,插件会存储在系统上供以后使用。


来源:nodedownloads.nodeland.dev


按操作系统划分的下载量中,Linux 位居榜首。这是有道理的,因为 Linux 通常是持续集成(CI)的首选——软件在开发过程中经过的自动化测试过程。虽然 Linux 主导 CI,但开源项目(OSS)通常在 Windows 上进行额外测试以确保万无一失。


这种高下载量的趋势转化为实际使用。在 2021 年,Node.js 二进制文件的下载量为 3000 万到 2024 年这一数字跃升至 5000 万。在 2023 年,Docker Hub 上的 Node.js 镜像获得了超过 8 亿次下载,提供了 Node.js 在生产环境中使用情况的宝贵洞察。


保持应用程序安全:更新你的 Node.js 版本


许多开发人员和团队无意中让他们的应用程序面临风险,因为他们没有更新 Node.js。以下是保持最新版本的重要性。


Node.js 提供了长期支持(LTS)计划,以确保关键应用程序的稳定性和安全性。然而,版本最终会到达其生命周期的终点,这意味着它们不再接收安全补丁。使用这些过时版本构建的应用程序将面临攻击风险。


例如,Node.js 版本 14 和 16 现在已经被弃用。尽管如此,这些版本每月仍有数百万次下载 —— Node 16 在 2 月份被下载了 2500 万次,而 Node 14 则约为 1000 万次*。令人震惊的是,一些开发人员甚至在使用更旧的版本,如 Node 10 和 12。


LTS 计划


好消息是:更新 Node.js 很容易。推荐的方法是每隔两个 LTS 版本进行升级。例如,如果你当前使用的是 Node.js 16(已不再支持),你应该迁移到最新的 LTS 版本,即目前的 Node.js 20。不要让过时的软件使你的应用程序暴露于安全威胁中。


Node.js 努力确保你的安全


Node.js 非常重视安全性。安全提交会由 Node 技术指导委员会(TSC)进行彻底评估,以确定其有效性。该团队努力确保快速响应时间,目标是在提交报告后 5 天内做出初步响应,通常在 24 小时内实现。


初次响应平均时间


安全修复每季度批量发布。去年,TSC 总共收到了 80 个提交。


Node.js 安全提交


没有 Open Source Security Foundation(OpenSSF)的支持,这种对安全性的承诺是不可能实现的。通过 OpenSSF 领导的 Alpha-Omega 项目,由微软、谷歌和亚马逊资助,Node.js 获得了专门用于提高其安全态势的拨款。该项目于 2022 年启动,旨在通过促进更快的漏洞识别和解决,使关键的开源项目更加安全。这一合作以及 Node.js 对安全工作的专门资金,展示了其保护用户安全的强烈承诺。


安全工作总资金


近年来发布的主要功能


让我们来看看过去几年引入的一些功能。


ESM


Node.js 已经采用了 ECMAScript 模块(ESM)。ESM 提供了一种现代的代码结构方式,使其更清晰和易于维护。


ESM 的一个关键优势是能够在 import 语句中显式声明依赖项。这改善了代码的可读性,并帮助你跟踪项目的依赖关系。因此,ESM 正迅速成为新 Node.js 项目的首选模块格式。


以下是如何在 Node 中使用 ESM 模块的演示:


// addTwo.mjs
function addTwo(num) {
return num + 2;
}

export { addTwo };

// app.mjs
import { addTwo } from './addTwo.mjs';

// 打印:6
console.log(addTwo(4));

线程


Node 还推出了工作线程,允许用户将复杂的计算任务卸载到独立的线程。这释放了主线程来处理用户请求,从而带来更流畅和响应更快的用户体验。


const {
Worker,
isMainThread,
setEnvironmentData,
getEnvironmentData,
} = require('node:worker_threads');

if (isMainThread) {
setEnvironmentData('Hello', 'World!');
const worker = new Worker(__filename);
} else {
console.log(getEnvironmentData('Hello')); // 打印“World!”。
}

Fetch


Node.js 现在内置了 Fetch API 的实现,这是一种现代且符合规范的方式来通过网络获取资源。这意味着你可以编写更清晰和一致的代码,而不必依赖外部库。


Node.js 还引入了几个与 Fetch 一起的新功能,以增强 Web 平台的兼容性。这些功能包括:



  • Web Streams:高效处理大数据流,而不会使应用程序不堪重负。

  • FormData:轻松构建和发送表单数据用于 Web 请求。

  • StructuredClone():创建复杂数据结构的深拷贝。

  • textEncoder() 和 textDecoder():无缝处理文本编码和解码任务。

  • Blob:表示各种用途的原始二进制数据。


结合 Fetch,这些新增功能使你能够在 Node.js 环境中完全构建现代 Web 应用程序。


const res = await fetch('https://example.com');
const json = await res.json();
console.log(json);

Promises


Node.js 提供了内置的 Promise 功能,提供了一种更清晰和结构化的方式来处理异步任务的结果(成功或失败)。


与回调地狱相比,使用 Promises 可以编写更自然、更易于理解的代码。


以下是使用 fs/promises 模块中的 readFile 方法的实际示例,展示了 Promises 如何简化异步文件读取:


import { readFile } from 'node:fs/promises';

try {
const filePath = new URL('./package.json', import.meta.url);
const contents = await readFile(filePath, { encoding: 'utf8' });
console.log(contents);
} catch (err) {
console.error(err.message);
}

Node 独有的核心模块


Node.js 引入了核心模块和用户引入模块的明确区分,使用 "node:" 前缀来标识核心模块


这个前缀像是一个标签,立即将模块标识为 Node.js 的核心构建块。这种区分有几个好处:



  • 减少混淆:不再将核心模块误认为是用户创建的模块。

  • 简化选择:使用 "node:" 前缀轻松选择所需的特定核心模块。


这种变化还防止用户使用可能与未来核心模块冲突的名称注册到 npm 注册表中,如下所示:


import test from 'node:test';
import assert from 'node:assert';

Watch


在引入此功能之前,nodemon 是文件更改监视中最流行的包。


现在,--watch 标志提供了:



  • 自动文件监视:它监视您导入的文件,准备在发生任何更改时立即采取行动。

  • 即时重启:每当修改监视的文件时,Node.js 自动重启,确保您的应用程序反映最新更新。

  • 测试协同作用:--watch 标志与测试运行器友好地协作,在文件更改后自动重新运行测试。这使得开发工作流程变得流畅,提供持续反馈。

  • 为了更精细的控制,--watch-path 标志允许您指定要监视的确切文件。


AsyncLocalStorage


AsyncLocalStorage 允许在 Web 请求或任何其他异步持续时间内存储数据。它类似于其他语言中的线程本地存储。


AsyncLocalStorage 增强了开发人员创建像 React 服务器组件这样的功能,并作为 Next.js 请求存储的基础。这些组件简化了 React 应用程序的服务器端渲染,最终提高了开发者体验。


import http from 'node:http';
import { AsyncLocalStorage } from 'node:async_hooks';

const asyncLocalStorage = new AsyncLocalStorage();

function logWithId(msg) {
const id = asyncLocalStorage.getStore();
console.log(`${id !== undefined ? id : '-'}:`, msg);
}

let idSeq = 0;
http.createServer((req, res) => {
asyncLocalStorage.run(idSeq++, () => {
logWithId('start');
// Imagine any chain of async operations here
setImmediate(() => {
logWithId('finish');
res.end();
});
});
}).listen(8080);

http.get('http://localhost:8080');
http.get('http://localhost:8080');
// 输出:
// 0: start
// 1: start
// 0: finish
// 1: finish

WebCrypto


这个标准化的 API 在 Node.js 环境中直接提供了强大的加密工具集。


使用 WebCrypto,您可以利用以下功能:



  • 密钥生成:创建强大的加密密钥以保护您的数据。

  • 加密和解密:对敏感信息进行加密,以安全存储和传输,并在需要时解密。

  • 数字签名:签署数据以确保真实性并防止篡改。

  • 哈希:生成数据的唯一指纹以进行验证和完整性检查。


通过将 WebCrypto 集成到您的 Node.js 应用程序中,您可以显著增强其安全性,并保护用户数据。


const { subtle } = require('node:crypto').webcrypto;

(async function () {
const key = await subtle.generateKey({
name: 'HMAC',
hash: 'SHA-256',
length: 256
}, true, ['sign', 'verify']);

const enc = new TextEncoder();
const message = enc.encode('I love cupcakes');

const digest = await subtle.sign({
name: 'HMAC'
}, key, message);
})();

实用工具


Node 开始提供了许多实用工具。其核心团队认为用户不应该安装新模块来执行基本实用程序。其中一些实用程序包括以下内容。


Utils.ParseArgs()


Node.js 提供了一个名为 Utils.ParseArgs() 的内置实用程序(或来自 node 模块的 parseArgs 函数),简化了解析应用程序中的命令行参数的任务。这消除了对外部模块的需求,使您的代码库更精简。


那么,Utils.ParseArgs() 如何帮助?它接受传递给您的 Node.js 脚本的命令行参数,并将它们转换为更可用的格式,通常是一个对象。这个对象使得在代码中访问和利用这些参数变得容易。


import { parseArgs } from 'node:util';

const args = ['-f', '--bar', 'b'];
const options = {
foo: {
type: 'boolean',
short: 'f',
},
bar: {
type: 'string',
},
};

const {
values,
positionals,
} = parseArgs({ args, options });

console.log(values, positionals);
// 输出:[Object: null prototype] { foo: true, bar: 'b' } []

单一可执行应用程序


单个可执行应用程序使得通过 Node 分发应用程序成为可能。这在构建和分发 CLI 到用户时非常强大。


这个功能将应用程序代码注入到 Node 二进制文件中。可以分发二进制文件而不必安装 Node/npm。目前仅支持单个 CommonJS 文件。


为了简化创建单个可执行文件,Node.js 提供了一个由 Postman Labs 开发的辅助模块 postject。


权限系统


Node.js 进程对系统资源的访问以及可以执行的操作可以通过权限来管理。还可以通过权限管理其他模块可以访问的模块。


process.permission.has('fs.write');
// true
process.permission.deny('fs.write', '/home/user');

process.permission.has('fs.write');
// true
process.permission.has('fs.write', '/home/user');
// false

测试运行器


它使用 node:test、--test 标志和 npm test。它支持子测试、skip/only 和生命周期钩子。它还支持函数和计时器模拟;模块模拟即将推出。


它还通过 --experimental-test-coverage 提供代码覆盖率和通过 -test-reporter 和 -test-reporter-destination 提供报告器。基于 TTY,默认为 spec、TAP 或 stdout。


import test from 'node:test';
import test from 'test';

test('synchronous passing test', (t) => {
// This test passes because it does not throw an exception.
assert.strictEqual(1, 1);
});

test('synchronous failing test', (t) => {
// This test fails because it throws an exception.
assert.strictEqual(1, 2);
});

test('asynchronous passing test', async (t) => {
// This test passes because the Promise returned by the async
// function is settled and not rejected.
assert.strictEqual(1, 1);
});

test('asynchronous failing test', async (t) => {
// This test fails because the Promise returned by the async
// function is rejected.
assert.strictEqual(1, 2);
});

test('failing test using Promises', (t) => {
// Promises can be used directly as well.
return new Promise((resolve, reject) => {
setImmediate(() => {
reject(new Error('this will cause the test to fail'));
});
});
});

test('callback passing test', (t, done) => {
// done() is the callback function. When the setImmediate() runs, it invokes
// done() with no arguments.
setImmediate(done);
});

test('callback failing test', (t, done) => {
// When the setImmediate() runs, done() is invoked with an Error object and
// the test fails.
setImmediate(() => {
done(new Error('callback failure'));
});
});

require(esm)


一个新的标志已经发布,允许开发者同步地引入 ESM 模块。


'use strict';

const { answer } = require('./esm.mjs');
console.log(answer);


另外,一个新的标志 --experimental-detect-module 允许 Node.js 检测模块是 commonJS 还是 esm。这个新标志简化了在 JavaScript 中编写 Bash 脚本。


WebSocket


WebSocket 是 Node.js 最受欢迎的功能请求之一。这个功能也是符合规范的。



为 Node.js 做贡献


作为一种开源技术,Node.js 主要由志愿者和协作者维护。由于 Node.js 的受欢迎程度不断提高,维护工作也越来越具有挑战性,需要更多的帮助。


Node.js 核心协作者维护 nodejs/node GitHub 仓库。Node.js 核心协作者的 GitHub 团队是 @nodejs/collaborators。协作者具有:



  • 对 nodejs/node 仓库的提交访问权限

  • 对 Node.js 持续集成(CI)作业的访问权限


无论是协作者还是非协作者都可以对 Node.js 源代码提出修改建议。提出修改建议的机制是 GitHub 拉取请求(pull request)。协作者审查并合并(land)拉取请求。


在拉取请求能够合并之前,必须得到两个协作者的批准。(如果拉取请求已经开放超过 7 天,一个协作者的批准就足够了。)批准拉取请求表示协作者对变更负责。批准必须来自不是变更作者的协作者。


如果协作者反对提出的变更,则该变更不能合并。例外情况是,如果 TSC 投票批准变更,尽管存在反对意见。通常,不需要涉及 TSC。


通常,讨论或进一步的更改会导致协作者取消他们的反对。


从根本上说,如果您想对 Node.js 的未来有发言权,请开始贡献!



总结


关于 Node.js 衰退的传言被大大夸大了。深入研究这些指标后,可以清楚地看到:Node.js 不仅不会消失,而且正在积极进化以满足现代 Web 开发的需求


凭借庞大的用户基础、繁荣的开源社区和不断创新的功能,Node.js 仍然是一个强大而多功能的平台。最近增加的 ESM、工作线程、Fetch API 和内置模块表明了它在技术前沿保持领先的承诺。


此外,Node.js 通过专门的团队和严格的流程优先考虑安全性。它的开放协作模式欢迎像您这样的开发人员的贡献,确保平台的光明未来。


因此,无论您是经验丰富的开发人员还是刚刚起步,Node.js 都为构建可扩展和高效的 Web 应用程序提供了一个有力的选择。丰富的资源、活跃的社区和对持续改进的承诺使其成为您下一个项目的坚实基础


参考:



作者:五月君
来源:juejin.cn/post/7379667550505304075
收起阅读 »

uni-app利用renderjs实现截取视频第一帧画面作为封面图

web
需求背景 如下图,使用 uni-app 做 app 时,要上传图片和视频,这里选择图片和视频分别使用的 uni.chooseImage 和 uni.chooseVideo,上传使用的 uni.uploadFile,问题就是这些 API 还有上传成功后服务器返回...
继续阅读 »



需求背景


如下图,使用 uni-app 做 app 时,要上传图片和视频,这里选择图片和视频分别使用的 uni.chooseImageuni.chooseVideo,上传使用的 uni.uploadFile,问题就是这些 API 还有上传成功后服务器返回内容中都没有提供视频封面图,于是只能使用一个固定的图片来充当视频封面,但是这样用户体验很不好


image.png


解决思路


在获取到视频链接后,如果我们可以让视频在后台自动播放,出现第一帧画面后再将它给停掉,在这个过程中利用 canvas 截取到视频播放的第一帧画面保存起来,那不就可以作为视频封面了吗?没那么容易,平时在 H5 环境中,到目前为止就行了,但问题是,现在我这里是 App,然后 uni-app 自带的 video 组件没法截取画面,而 App 环境又没法用 H5 环境的 video 标签,它甚至没有 document 对象, 技术框架上不兼容, 那怎么办?


这时候就需要用到 renderjs 了,毕竟它的核心作用之一就是 “在视图层操作dom,运行 for webjs库”。


那思路就有了,在 renderjs 模块中监听原始模块中的文件列表,当更改时(新增、删除),在 renderjs 中动态创建 video 元素,让它自动静音播放,使用 canvas 截取第一帧画面后销毁 video 元素并将图片传递给原始模块,原始模块将其设置为对应视频的封面


代码逻辑


<template>
<view :prop="canvasList" :change:prop="canvas.getVideoCanvas">
<view v-for="(item,index) in fileList" :key="index">
<image v-if="item.type===0" :src="item.url" @click="previewImage(item.url)">image>
<view v-else @click="previewVideoSrc = item.url">

<image mode="widthFix" :src="item.cover">image>

<u-icon class="play-icon" name="play-right-fill" size="30" color="#fff">u-icon>
view>
view>
<view class="preview-full" v-if="previewVideoSrc!=''">
<video :autoplay="true" :src="previewVideoSrc" :show-fullscreen-btn="false">
<cover-view class="preview-full-close" @click="previewVideoSrc=''"> ×
cover-view>
video>
view>
view>
template>

<script>
import { deepClone } from '@/utils'
// 原始模块
export default {
data() {
return {
previewVideoSrc: '', // 预览视频url
fileList: [
{ url: '', type: 0 },
{ url: '', type: 1 },
{ url: '', type: 1 },
] // 真正用来展示和传递的文件列表,type: 0代表图片,1代表视频
}
},
computed: {
// 用于 renderjs 模块监听,不用 fileList 是因为后续还有更改它(为其内部元素添加 cover )属性
// 监听 fileList 然后又更改它会导致循环递归,这里使用 deepClone 也是为了让 canvasList 不与
// fileList 产生关联
canvasList() {
return deepClone(this.fileList)
}
},
methods: {
// 预览图片
previewImage(url) {
uni.previewImage({
urls: [url]
});
},
// 生成视频封面
getVideoPoster({ index, cover }) {
this.$set(this.fileList[index], 'cover', cover)
},
}
}
script>
<script module="canvas" lang="renderjs">
// renderjs 模块
export default {
methods: {
getVideoCanvas(nV, oV, ownerInstance) {
if(oV !== undefined && Array.isArray(nV) && nV.length > 0) {
nV.forEach((item, index) => {
// 如果是视频
if(item.type == 1) {
// 防止一次性执行过多逻辑导致卡顿
setTimeout(() => {
// 创建video标签
let video = document.createElement("video")
// 设置为自动播放和静音
video.setAttribute('autoplay', 'autoplay')
video.setAttribute('muted', 'muted')
// 设置播放源
video.innerHTML = ''
// 创建 canvas 元素和 2d 画布
let canvas = document.createElement('canvas')
let ctx = canvas.getContext('2d')
// 监听 video 的 canplay 事件
video.addEventListener('canplay', function () {
// 设置宽高
let anw = document.createAttribute("width");
anw.nodeValue = 80;
let anh = document.createAttribute("height");
anh.nodeValue = 80;
canvas.setAttributeNode(anw);
canvas.setAttributeNode(anh);
// 画布渲染
ctx.drawImage(video, 0, 0, 80, 80);
// 生成 base64 图片
let base64 = canvas.toDataURL('image/png')
// 暂停并销毁 video 元素
video.pause()
video.remove();
// 传递数据给逻辑层
ownerInstance.callMethod('getVideoPoster', {
index,
cover: base64
})
}, false)
}, index * 120)
}
})
}
}
}
}
script>

成果展示


image.png


还有另一个地方,之前就是这样的,都是用的默认图片当作封面:


image.png


经过处理后就是这样啦:


image.png


7.gif


作者:鹏北海
来源:juejin.cn/post/7322762833690066981
收起阅读 »

2023年给一位团队成员绩效“打c”的经历

2023年作为疫情开放的第一个年份,国家整体的经济形势还处于低迷阶段。IT行业同样如此,各公司都保持着降本增效的节奏,企业招聘人才的需求明显放缓,有人力缺口的岗位都优先考虑内部人才的转岗。在这样的大环境下,IT行业的从业者们想要离职会更加谨慎,不会再如以前那样...
继续阅读 »

2023年作为疫情开放的第一个年份,国家整体的经济形势还处于低迷阶段。IT行业同样如此,各公司都保持着降本增效的节奏,企业招聘人才的需求明显放缓,有人力缺口的岗位都优先考虑内部人才的转岗。在这样的大环境下,IT行业的从业者们想要离职会更加谨慎,不会再如以前那样“老子干的不爽,就离职换个公司”。


即便如此,每个公司依然还会有一些人,总过得浑浑噩噩,需要别人踢一脚就走一步,不去主动承担和思考事情,只能做一些确定性很强和设计好的工作。2023年,我们团队就有这样的一名成员,最终不好的绩效,也只能落到他头上。


然而,最终和他进行绩效约谈的时候,他却完全不认可,表现出非常激烈的逆反情绪,认为他的职级不需要去承担过多的事情。而我作为他的直接管理者,这次约谈的过程显然不够成功,现在整体再复盘一下这个过程。


前提背景


员工背景: 进入公司已经超过三年,近一年由于部门变动转入我的团队。本身的职级较低,由于之前工作平平没有太多起色,所以一直也没有得到过晋升,而年龄却已经越来越大。


进入我的团队后,团队内年龄与其不相上下的成员,职级已比他高出较多。从而,也引起了他心态的失衡,总觉得公司亏欠他的,他的能力不应该得不到晋升。所以在工作时,只愿做自己职级内的事情,也不愿意承担更多。


过程管控


我作为他的直接管理者,发现问题后私下跟他聊过。跟他说过几次,他做事情太被动,工作时对外沟通经常带着个人情绪,需要更加积极正面的去承担事情。为了打消他的顾虑,也跟他说明了,只要你的工作能力有所提升,对团队有所帮助,我会尽量帮助你晋升。


然而,一个人心态的问题,是一个历史长期积累的过程。他并没有因为和我的几次沟通,就打破了自己的认知,对外依旧较封闭,对内能力又显得不足。而他自己却认识不到,总认为他在当前的职级上,已经足够了,除非公司让其晋升,不然他也不会付出更多。


为了打破这种僵硬的局面,作为管理者我安排了一项稍有困难的任务给他,这既是机会也是挑战。第一、让其认识到自身的不足;第二、如果他能够较好的完成任务,也就为后面的晋升提供了保障。


也就是这么一次任务,不但目的没有达到,最后还惹得双方都陷入了僵局。



这项任务还未开始3个月前,我就跟他说:要开始熟悉相关的业务和代码了,后面会有大的项目变更,需要提前做好准备。


前期我并没有明确说明,要交付什么产物。更多的是,给他自己空间,让他在一个相对宽松的时间内,把整体的业务和细节都了解清楚,能够在组内进行一次分享。


任务我已经给出去了,在项目开始前将近3个月的时间, 他并没有给到我任何反馈,也没有交付任何相关的文档。


随着时间的推移,项目开始启动了,基于这样的工作态度和结果,我本不打算让他再负责这个项目。但是上级管理者,也希望能够给予他一次机会,做好了能够为后面的晋升,提供较好的铺垫。


就是这样的安排,由于他前期没有较好的准备,后面在落地方案评审时漏洞百出,导致项目出现了延期的风险。所以最终不得不由我直接来接管项目,重新分配和协调各个研发人员,最终确保了项目的质量和进度。



约谈结果


对于这样的团队成员,既不能给团队带来正向的帮助,也不能让其自身得到成长。这是一个双输的局面,管理者要让团队保持正向的发展,就必须要勇于去解决这样的问题。


所以年度的绩效考核,就必须亮明你的态度,即使公司没有淘汰的指标,你也要去做那个坏人,把不合适的人从团队清除掉。只是,我没有想到这个过程如此艰难。


下面从他的视角,来反驳这个结果的几个观点:



  • 他的职级,只需要配合做好相应的开发任务就可以,不需要去主导事情。

  • 那个有挑战的项目,不管过程怎样,结果是好的,项目按时按质的上线了。

  • 给他不好的绩效,需要参照公司的标准,给出明确的原因。


然后,带着强烈的情绪说要去投诉,甚至要上升到CTO、CEO 那边。 投诉没有问题,我也表明了态度, 你可以向上申请表达自己的诉求, 但是我也会持有自己的观点和建议。


最终,当然也不会因为他的申诉就改变结果。只是,这个现状本应该在管理的过程中,就应该让其感知到,不要等到最后的环节,才让双方都陷入难堪的局面。


反思总结


作为一个管理者,要面对各式各样的研发人员。有的人优秀,上来就能够跟你站在一个视角看问题;有的人有潜力,需要你给出机会和试错空间,让其成长;有的人就该辞退,针对这些人,你尤其要做好备战。作为管理者,既要有开放和怀柔的心态去留住人才,也要有铁血的手腕去清退团队的毒瘤。


清退毒瘤,是一项艰难但必要的任务,如何去做呢?



  1. 评估情况: 评估对团队的影响,是否对团队的合作和效率产生负面影响,是否违反了团队的价值观和行为准则。要确保有足够的证据来支持你的决定。

  2. 沟通和反馈: 与他进行一对一沟通,明确表达你对他们行为的关注,并提供具体的例子。给予他们改进的机会,并讨论如何改变。

  3. 制定行动计划: 如果没有改善他们的行为,你需要制定一个行动计划。包括培训和指导。

  4. 寻求支持: 寻求其他团队成员和上级的支持。


作者:云游者
来源:juejin.cn/post/7341368001203699747
收起阅读 »

Stream很好,Map很酷,但答应我别用toMap()

在 JDK 8 中 Java 引入了让人欲罢不能的 stream 流处理,可以说已经成为了我日常开发中不可或缺的一部分。 当完成一次流处理之后需要返回一个集成对象时,已经肌肉记忆的敲下 collect(Collectors.toList()) 或者 colle...
继续阅读 »

JDK 8Java 引入了让人欲罢不能的 stream 流处理,可以说已经成为了我日常开发中不可或缺的一部分。


当完成一次流处理之后需要返回一个集成对象时,已经肌肉记忆的敲下 collect(Collectors.toList()) 或者 collect(Collectors.toSet())。你可能会想,toListtoSet 都这么便捷顺手了,当又怎么能少得了 toMap() 呢。


答应我,一定打消你的这个想法,否则这将成为你噩梦的开端。


image.png


什么?你不信,没有什么比代码让人更痛彻心扉,让我们直接上代码。


让我们先准备一个用户实体类。


@Data
@AllArgsConstructor
public class User {

private int id;

private String name;
}

假设有这么一个场景,你从数据库读取 User 集合,你需要将其转为 Map 结构数据,keyvalue 分别为 useridname


很快,你啪的一下就写出了下面的代码:


public class UserTest {
@Test
public void demo() {
List<User> userList = new ArrayList<>();
// 模拟数据
userList.add(new User(1, "Alex"));
userList.add(new User(1, "Beth"));

Map<Integer, String> map = userList.stream()
.collect(Collectors.toMap(User::getId, User::getName));
System.out.println(map);
}
}

运行程序,你已经想好了开始怎么摸鱼,结果啪的一下 IllegalStateException 报错就拍你脸上,你定睛一看怎么提示 Key 值重复。


image.png


作为优秀的八股文选手,你清楚的记得 HashMap 对象 Key 重复是进行替换。你不信邪,断点一打,堆栈一看,硕大的 uniqKeys 摆在了面前,凭借四级 424 分的优秀战绩你顿时菊花一紧,点开一看,谁家好人 map key 还要去重判断啊。


image.png


好好好,这么玩是吧,你转身打开浏览器一搜,原来需要自己手动处理重复场景,啪的一下你又重新改了一下代码:


public class UserTest {
@Test
public void demo() {
List<User> userList = new ArrayList<>();
// 模拟数据
userList.add(new User(1, "Alex"));
userList.add(new User(2, null));

Map<Integer, String> map = userList.stream()
.collect(Collectors.toMap(User::getId, User::getName, (oldData, newData) -> newData));
System.out.println(map);
}
}

再次执行程序,你似乎已经看到知乎的摸鱼贴在向你招手了,结果啪的一下 NPE 又拍在你那笑容渐渐消失的脸上。



静下心来,本着什么大风大浪我没见过的心态,断点堆栈一气呵成,而下一秒你又望着代码陷入了沉思,我是谁?我在干什么?




鼓起勇气,你还不信今天就过不去这个坎了,大手一挥,又一段优雅的代码孕育而生。


public class UserTest {
@Test
public void demo() {
List<User> userList = new ArrayList<>();
// 模拟数据
userList.add(new User(1, "Alex"));
userList.add(new User(1, "Beth"));
userList.add(new User(2, null));

Map<Integer, String> map = userList.stream()
.collect(Collectors.toMap(
User::getId,
it -> Optional.ofNullable(it.getName()).orElse(""),
(oldData, newData) -> newData)
);
System.out.println(map);
}
}

优雅,真是太优雅了,又是 Stream 又是 Optional,可谓是狠狠拿捏技术博文的 G 点了。


image.png


这时候你回头一看,我需要是什么来着?这 TM 不是一个循环就万事大吉了吗,不信邪的你回归初心,回归了 for 循环的怀抱,又写了一版。


public class UserTest {
@Test
public void demo() {
List<User> userList = new ArrayList<>();
// 模拟数据
userList.add(new User(1, "Alex"));
userList.add(new User(1, "Beth"));
userList.add(new User(2, null));

Map<Integer, String> map = new HashMap<>();
userList.forEach(it -> {
map.put(it.getId(), it.getName());
});
System.out.println(map);
}
}

看着运行完美无缺的代码,你一时陷入了沉思,数分钟过去了,你删除了 for 循环,换上 StreamOptional 不羁的外衣,安心的提交了代码,这口细糠一定也要让好同事去尝一尝。


image.png


作者:烽火戏诸诸诸侯
来源:juejin.cn/post/7383643463534018579
收起阅读 »

写了一个责任链模式,bug 无数...

责任链模式是一种行为设计模式, 允许你将请求沿着处理者链进行发送。 收到请求后, 每个处理者均可对请求进行处理, 或将其传递给链上的下个处理者。 使用场景 责任链的使用场景还是比较多的: 多条件流程判断:权限控制 ERP 系统流程审批:总经理、人事经理、项...
继续阅读 »

责任链模式是一种行为设计模式, 允许你将请求沿着处理者链进行发送。


收到请求后, 每个处理者均可对请求进行处理, 或将其传递给链上的下个处理者。


图片


使用场景


责任链的使用场景还是比较多的:



  • 多条件流程判断:权限控制

  • ERP 系统流程审批:总经理、人事经理、项目经理

  • Java 过滤器的底层实现 Filter


如果不使用该设计模式,那么当需求有所改变时,就会使得代码臃肿或者难以维护,例如下面的例子。


| 反例


假设现在有一个闯关游戏,进入下一关的条件是上一关的分数要高于 xx:



  • 游戏一共 3 个关卡

  • 进入第二关需要第一关的游戏得分大于等于 80

  • 进入第三关需要第二关的游戏得分大于等于 90


那么代码可以这样写:


//第一关
public class FirstPassHandler {
    public int handler(){
        System.out.println("第一关-->FirstPassHandler");
        return 80;
    }
}

//第二关
public class SecondPassHandler {
    public int handler(){
        System.out.println("第二关-->SecondPassHandler");
        return 90;
    }
}

//第三关
public class ThirdPassHandler {
    public int handler(){
        System.out.println("第三关-->ThirdPassHandler,这是最后一关啦");
        return 95;
    }
}

//客户端
public class HandlerClient {
    public static void main(String[] args) {

        FirstPassHandler firstPassHandler = new FirstPassHandler();//第一关
        SecondPassHandler secondPassHandler = new SecondPassHandler();//第二关
        ThirdPassHandler thirdPassHandler = new ThirdPassHandler();//第三关

        int firstScore = firstPassHandler.handler();
        //第一关的分数大于等于80则进入第二关
        if(firstScore >= 80){
            int secondScore = secondPassHandler.handler();
            //第二关的分数大于等于90则进入第二关
            if(secondScore >= 90){
                thirdPassHandler.handler();
            }
        }
    }
}

那么如果这个游戏有 100 关,我们的代码很可能就会写成这个样子:


if(第1关通过){
    // 第2关 游戏
    if(第2关通过){
        // 第3关 游戏
        if(第3关通过){
           // 第4关 游戏
            if(第4关通过){
                // 第5关 游戏
                if(第5关通过){
                    // 第6关 游戏
                    if(第6关通过){
                        //...
                    }
                }
            }
        }
    }
}

这种代码不仅冗余,并且当我们要将某两关进行调整时会对代码非常大的改动,这种操作的风险是很高的,因此,该写法非常糟糕。


| 初步改造


如何解决这个问题,我们可以通过链表将每一关连接起来,形成责任链的方式,第一关通过后是第二关,第二关通过后是第三关....


这样客户端就不需要进行多重 if 的判断了:


public class FirstPassHandler {
    /**
     * 第一关的下一关是 第二关
     */

    private SecondPassHandler secondPassHandler;

    public void setSecondPassHandler(SecondPassHandler secondPassHandler) {
        this.secondPassHandler = secondPassHandler;
    }

    //本关卡游戏得分
    private int play(){
        return 80;
    }

    public int handler(){
        System.out.println("第一关-->FirstPassHandler");
        if(play() >= 80){
            //分数>=80 并且存在下一关才进入下一关
            if(this.secondPassHandler != null){
                return this.secondPassHandler.handler();
            }
        }

        return 80;
    }
}

public class SecondPassHandler {

    /**
     * 第二关的下一关是 第三关
     */

    private ThirdPassHandler thirdPassHandler;

    public void setThirdPassHandler(ThirdPassHandler thirdPassHandler) {
        this.thirdPassHandler = thirdPassHandler;
    }

    //本关卡游戏得分
    private int play(){
        return 90;
    }

    public int handler(){
        System.out.println("第二关-->SecondPassHandler");

        if(play() >= 90){
            //分数>=90 并且存在下一关才进入下一关
            if(this.thirdPassHandler != null){
                return this.thirdPassHandler.handler();
            }
        }

        return 90;
    }
}

public class ThirdPassHandler {

    //本关卡游戏得分
    private int play(){
        return 95;
    }

    /**
     * 这是最后一关,因此没有下一关
     */

    public int handler(){
        System.out.println("第三关-->ThirdPassHandler,这是最后一关啦");
        return play();
    }
}

public class HandlerClient {
    public static void main(String[] args) {

        FirstPassHandler firstPassHandler = new FirstPassHandler();//第一关
        SecondPassHandler secondPassHandler = new SecondPassHandler();//第二关
        ThirdPassHandler thirdPassHandler = new ThirdPassHandler();//第三关

        firstPassHandler.setSecondPassHandler(secondPassHandler);//第一关的下一关是第二关
        secondPassHandler.setThirdPassHandler(thirdPassHandler);//第二关的下一关是第三关

        //说明:因为第三关是最后一关,因此没有下一关
        //开始调用第一关 每一个关卡是否进入下一关卡 在每个关卡中判断
        firstPassHandler.handler();

    }
}

| 缺点


现有模式的缺点:



  • 每个关卡中都有下一关的成员变量并且是不一样的,形成链很不方便

  • 代码的扩展性非常不好,最新设计模式面试题整理好了


| 责任链改造


既然每个关卡中都有下一关的成员变量并且是不一样的,那么我们可以在关卡上抽象出一个父类或者接口,然后每个具体的关卡去继承或者实现。


有了思路,我们先来简单介绍一下责任链设计模式的基本组成:



  • 抽象处理者(Handler)角色: 定义一个处理请求的接口,包含抽象处理方法和一个后继连接。

  • 具体处理者(Concrete Handler)角色: 实现抽象处理者的处理方法,判断能否处理本次请求,如果可以处理请求则处理,否则将该请求转给它的后继者。

  • 客户类(Client)角色: 创建处理链,并向链头的具体处理者对象提交请求,它不关心处理细节和请求的传递过程。


图片


public abstract class AbstractHandler {

    /**
     * 下一关用当前抽象类来接收
     */

    protected AbstractHandler next;

    public void setNext(AbstractHandler next) {
        this.next = next;
    }

    public abstract int handler();
}

public class FirstPassHandler extends AbstractHandler{

    private int play(){
        return 80;
    }

    @Override
    public int handler(){
        System.out.println("第一关-->FirstPassHandler");
        int score = play();
        if(score >= 80){
            //分数>=80 并且存在下一关才进入下一关
            if(this.next != null){
                return this.next.handler();
            }
        }
        return score;
    }
}

public class SecondPassHandler extends AbstractHandler{

    private int play(){
        return 90;
    }

    public int handler(){
        System.out.println("第二关-->SecondPassHandler");

        int score = play();
        if(score >= 90){
            //分数>=90 并且存在下一关才进入下一关
            if(this.next != null){
                return this.next.handler();
            }
        }

        return score;
    }
}

public class ThirdPassHandler extends AbstractHandler{

    private int play(){
        return 95;
    }

    public int handler(){
        System.out.println("第三关-->ThirdPassHandler");
        int score = play();
        if(score >= 95){
            //分数>=95 并且存在下一关才进入下一关
            if(this.next != null){
                return this.next.handler();
            }
        }
        return score;
    }
}

public class HandlerClient {
    public static void main(String[] args) {

        FirstPassHandler firstPassHandler = new FirstPassHandler();//第一关
        SecondPassHandler secondPassHandler = new SecondPassHandler();//第二关
        ThirdPassHandler thirdPassHandler = new ThirdPassHandler();//第三关

        // 和上面没有更改的客户端代码相比,只有这里的set方法发生变化,其他都是一样的
        firstPassHandler.setNext(secondPassHandler);//第一关的下一关是第二关
        secondPassHandler.setNext(thirdPassHandler);//第二关的下一关是第三关

        //说明:因为第三关是最后一关,因此没有下一关

        //从第一个关卡开始
        firstPassHandler.handler();

    }
}

| 责任链工厂改造


对于上面的请求链,我们也可以把这个关系维护到配置文件中或者一个枚举中。我将使用枚举来教会大家怎么动态的配置请求链并且将每个请求者形成一条调用链。


图片


public enum GatewayEnum {
    // handlerId, 拦截者名称,全限定类名,preHandlerId,nextHandlerId
    API_HANDLER(new GatewayEntity(1"api接口限流""cn.dgut.design.chain_of_responsibility.GateWay.impl.ApiLimitGatewayHandler"null2)),
    BLACKLIST_HANDLER(new GatewayEntity(2"黑名单拦截""cn.dgut.design.chain_of_responsibility.GateWay.impl.BlacklistGatewayHandler"13)),
    SESSION_HANDLER(new GatewayEntity(3"用户会话拦截""cn.dgut.design.chain_of_responsibility.GateWay.impl.SessionGatewayHandler"2null)),
    ;

    GatewayEntity gatewayEntity;

    public GatewayEntity getGatewayEntity() {
        return gatewayEntity;
    }

    GatewayEnum(GatewayEntity gatewayEntity) {
        this.gatewayEntity = gatewayEntity;
    }
}

public class GatewayEntity {

    private String name;

    private String conference;

    private Integer handlerId;

    private Integer preHandlerId;

    private Integer nextHandlerId;
}

public interface GatewayDao {

    /**
     * 根据 handlerId 获取配置项
     * 
@param handlerId
     * 
@return
     */

    GatewayEntity getGatewayEntity(Integer handlerId);

    /**
     * 获取第一个处理者
     * 
@return
     */

    GatewayEntity getFirstGatewayEntity();
}

public class GatewayImpl implements GatewayDao {

    /**
     * 初始化,将枚举中配置的handler初始化到map中,方便获取
     */

    private static Map gatewayEntityMap = new HashMap<>();

    static {
        GatewayEnum[] values = GatewayEnum.values();
        for (GatewayEnum value : values) {
            GatewayEntity gatewayEntity = value.getGatewayEntity();
            gatewayEntityMap.put(gatewayEntity.getHandlerId(), gatewayEntity);
        }
    }

    @Override
    
public GatewayEntity getGatewayEntity(Integer handlerId) {
        return gatewayEntityMap.get(handlerId);
    }

    @Override
    
public GatewayEntity getFirstGatewayEntity() {
        for (Map.Entry entry : gatewayEntityMap.entrySet()) {
            GatewayEntity value = entry.getValue();
            //  没有上一个handler的就是第一个
            if (value.getPreHandlerId() == null) {
                return value;
            }
        }
        return null;
    }
}

public class GatewayHandlerEnumFactory {

    private static GatewayDao gatewayDao = new GatewayImpl();

    // 提供静态方法,获取第一个handler
    public static GatewayHandler getFirstGatewayHandler() {

        GatewayEntity firstGatewayEntity = gatewayDao.getFirstGatewayEntity();
        GatewayHandler firstGatewayHandler = newGatewayHandler(firstGatewayEntity);
        if (firstGatewayHandler == null) {
            return null;
        }

        GatewayEntity tempGatewayEntity = firstGatewayEntity;
        Integer nextHandlerId = null;
        GatewayHandler tempGatewayHandler = firstGatewayHandler;
        // 迭代遍历所有handler,以及将它们链接起来
        while ((nextHandlerId = tempGatewayEntity.getNextHandlerId()) != null) {
            GatewayEntity gatewayEntity = gatewayDao.getGatewayEntity(nextHandlerId);
            GatewayHandler gatewayHandler = newGatewayHandler(gatewayEntity);
            tempGatewayHandler.setNext(gatewayHandler);
            tempGatewayHandler = gatewayHandler;
            tempGatewayEntity = gatewayEntity;
        }
    // 返回第一个handler
        return firstGatewayHandler;
    }

    /**
     * 反射实体化具体的处理者
     * 
@param firstGatewayEntity
     * 
@return
     */

    private static GatewayHandler newGatewayHandler(GatewayEntity firstGatewayEntity) {
        // 获取全限定类名
        String className = firstGatewayEntity.getConference();
        try {
            // 根据全限定类名,加载并初始化该类,即会初始化该类的静态段
            Class clazz = Class.forName(className);
            return (GatewayHandler) clazz.newInstance();
        } catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
            e.printStackTrace();
        }
        return null;
    }

}

public class GetewayClient {
    public static void main(String[] args) {
        GetewayHandler firstGetewayHandler = GetewayHandlerEnumFactory.getFirstGetewayHandler();
        firstGetewayHandler.service();
    }
}

设计模式有很多,责任链只是其中的一种,我觉得很有意思,非常值得一学。设计模式确实是一门艺术,仍需努力呀!




作者:程序员蜗牛
来源:juejin.cn/post/7383643463534067731
收起阅读 »

如果失业了,我们还能干啥?

这个事其实一直存在脑子的。为啥呢?因为我们听到太多了,太多了,35岁是个坎。事实上,找工作也是如此,很多行业都是有年龄限制的。找不到自己原来的行业的工作了。那就只有转行了。   对于我们这种菜鸟级别人,现实是残酷的。转行又谈何容易呀?但是真的到那一天,地步了,...
继续阅读 »

这个事其实一直存在脑子的。为啥呢?因为我们听到太多了,太多了,35岁是个坎。事实上,找工作也是如此,很多行业都是有年龄限制的。找不到自己原来的行业的工作了。那就只有转行了。


  对于我们这种菜鸟级别人,现实是残酷的。转行又谈何容易呀?但是真的到那一天,地步了,也不得不转。这不仅仅是我一个的想法,同事也是,群里的网友也是。于是乎,我们失业了,我们能干啥?经常被讨论起来。


   我也经常观察和想一些可行的。太远太陌生的咱也想不到。我想到的是开滴滴,顺风车,送外卖,送快递,干工地,开一个小餐馆,干保安,干搬运,干家政服务,干修理,洗空调。最后就是回老家养牛养鸡养鸭养猪之类。


  我先说几个我亲眼看到的,我觉得是非常可行的。


    之前公司有一个小小的箱子需要扔掉,然后叫了物业过来。大概是50x50x80这么大小。你们可知道这么一点东西,扔掉要多少钱么?100块。听到简直不敢相信。还有换灯泡,物业过来帮忙换多少钱一个?50元。就那么一两分钟的事。如果你不愿意,那只有自己换了。所以公司一个都没有叫物业做。扔箱子交给收废品的,换灯炮就我们男同事换。


     到了现在的公司,于是又遇到相同的事,这次换一个灯泡,你们听了都会惊讶的。400多一个。真的贵得离谱。只是咱没有工具,还有公司不允许,不然我就能干好。


    空调原来是要洗的,不过之前是不知道怎么洗,现在看了他们洗一次,知道非常简单。洗一台大概30分钟。收费是50到70元一台。真的很容易!


所以我把这些看到的分享到一个群里。说以后咱干这个!


这些天我还拍到一些服务图片。


一 收费服务


image.png


二 拆卸代扔服务


image.png


他们收费都比较贵,那咱比他们便宜三分之一?是不是可以把业务接过来?


   这些肯定比干工地轻松一些,而且赚得不比工地少。只要把服务干好了,回头客,口碑好了,不愁没有活。也不会存在所谓失业了。


三 其他大佬建议


image.png


image.png


四 其他人总结


4686dc607d2bcaa4f136e1245041a3f.jpg


五 最新发现——流动小贩


这是一个卖鞋子的小贩,生意真的好。60元一双。我都买了两双。


image.png


image.png


以前还发现卖衣服,包包的,接近过年边,到一些服装,箱包批发市场进货,成包,成捆的。因为都是一些外贸尾货,退单,样式不错,价格便宜,质量也不错。然后以一个大家可以接受的价格卖。比如30元,60元,90元等等。老人,年轻人都爱。因为大家没有那么讲究的。讲究的是实用。


  有时候自己焦虑,是因为害怕,习惯了熟悉路径。不愿意改变罢了。其实都是未必要的。


  正所谓车到山前必有路,船到桥头自然直,一切顺其自然!只要自己不懒,不要所谓的面子,一生还是可以顺顺当当的。


  当然,如果有厉害的高人指引,带路,贵人相助,那肯定可以过得更好。那是另当别论了。


  这些就是我当前想到的,了解到的。


作者:大巨头
来源:juejin.cn/post/7286762580877901865
收起阅读 »

微信小程序区分环境开发 and 合理绕过官方上线审核

web
前言:首先说明一点,虽然绕过官方审核,是不推荐的行为,但是实际的项目开发中,难免会有一些需求或功能在发布上线时,会被官方拒绝。例如类目不对的情况,由于企业性质或其他原因,无法申请相关类目要求的资质;或者申请资质办理难度大、所需时间漫长,无法在上线节点前申请完成...
继续阅读 »

前言:

首先说明一点,虽然绕过官方审核,是不推荐的行为,但是实际的项目开发中,难免会有一些需求或功能在发布上线时,会被官方拒绝。

例如类目不对的情况,由于企业性质或其他原因,无法申请相关类目要求的资质;或者申请资质办理难度大、所需时间漫长,无法在上线节点前申请完成,但是实际业务中确实有此需求。

这就需要在上线时先合理绕过官方审核,以期能顺利发布成功,不耽误业务使用。

一、背景和问题描述

很多开发者在开发项目的时候发现,上线微信小程序最难的不是开发阶段,而是微信审核机制。因为微信为了自身平台规避法律风险,开发的很多功能需要提供相关的正件或者资质,就像前面所说,相关的资质办理难度大,或者一般的公司根本办不下来。那么绕过审核就是一个很重要的上线技巧。
我们之前开发的一个微信小程序,涉及一些视频,发布审核时,被官方认定需要补充“教育服务-在线视频课程类目”。如下图所示:

image.png

但是我们项目中的视频内容是关于“用车知识的介绍和使用须知”,并不属于教育类视频或直播课程,而且我们也拥有“教育服务 > 在线教育”的服务类目,可能跟“在线视频课程”类目不一样。

image.png

可是实际业务中确实需要此功能,那么该如何顺利上线呢?

二、解决思路

因为需要此功能,那么:

  1. 体验版环境下必须能正常展示,才能让测试同事正常测试。
  2. 在提交审核时,即在开发版环境下,此模块需要隐藏,才能绕过官方审核,使审核通过。
  3. 在发布审核成功后,即在正式版环境下,此模块需正常展示,可供用户使用。

三、解决方案

我这边实现了两种解决方法,供大家参考:

方案一

核心: 使用 wx.getAccountInfoSync()

功能描述: 获取当前账号信息。线上小程序版本号仅支持在正式版小程序中获取,开发版和体验版中无法获取。

可参考微信小程序官方文档: 获取当前账号信息:Object wx.getAccountInfoSync()

image.png

具体使用方法如下:

  1. 在小程序项目的app.js文件中的onLaunch中获取小程序账号信息:

image.png

onLaunch: function () {
//启动时动态获取小程序的 appid
const accountinfo = wx.getAccountInfoSync()

wx.setStorageSync('miniProgram', accountinfo.miniProgram)
},
  1. 然后在需要做判断的模块的页面获取miniProgram,我这边是在展示视频模块入口页面获取:
  • js文件中获取账号信息的值:

image.png

data: {
miniProgram: wx.getStorageSync('miniProgram'),
},
  • html文件中进行判断:

image.png

注:我是使用miniProgram.version的值进行判断的。
因为此值是线上小程序版本号,只有在线上环境中才会有值,所以只会在线上环境中展示,提交审核的开发环境中看不到此模块。
而在体验版环境下,我不会加wx:if="{{miniProgram.version}}"这个代码,只有在提交审核时加上。缺点就是需要改动代码,但是能完美避开审核,使审核顺利通过。

方案二

核心: 使用小程序视频插件。
优点: 完美继承完美继承小程序原生的所有特性和事件。不用改代码。

后期我们开发了一个小程序的视频插件,在展示视频的页面中,使用视频插件代替。这样也能完美通过审核。

image.png

这个小程序视频插件作用是,专门为没有视频播放资质的小程序提供视频播放功能,解决视频播放资质问题。

思路来源于官方解答:

image.png

涉小程序插件功能介绍: developers.weixin.qq.com/miniprogram…

涉小程序类目资质、适用范围参考:developers.weixin.qq.com/miniprogram…

以上,希望对大家有帮助!


作者:小蹦跶儿
来源:juejin.cn/post/7340154170234552370
收起阅读 »

跟骑手学习送外卖,这家具身智能公司的机器人已经上岗挣钱了

你点过无人机送的外卖吗? 在深圳、上海等一线城市,让无人机给自己送个外卖已经不是什么新鲜事。但它送的方式可能和你想的不太一样。 想象中的无人机送外卖 be like: 而现实中的无人机送外卖 be like: 也就是说,它不会把外卖直接送到你家阳台,而是和...
继续阅读 »

你点过无人机送的外卖吗?


在深圳、上海等一线城市,让无人机给自己送个外卖已经不是什么新鲜事。但它送的方式可能和你想的不太一样。


想象中的无人机送外卖 be like:


图片


而现实中的无人机送外卖 be like:


图片


也就是说,它不会把外卖直接送到你家阳台,而是和你家有一段距离的外卖柜。你需要下楼走一段距离才能拿到。于是,有些网友发出灵魂追问:「你猜我为什么点外卖?」


所以,现在问题就变成了:从家到外卖柜这段距离怎么办?解决思路也很简单:让一个送货机器人帮你送完这段路。


这是具身智能机器人公司推行科技(Infermove)最近放出来的一段视频。从中可以看出,在无人机到达指定地点后,送货机器人可以把货「拿」过来,放到自己的「肚子」里,然后再送到指定小区、写字楼的指定楼层,实现无缝接驳。


其实,除了帮无人机送剩下的路程,它还能自己 cover 全程。在过去的 18 个月里,推行科技的机器人已经帮山姆会员店等商家送了几万单货。要知道,这些店铺和目的地之间往往隔了几条街,因此机器人需要在非机动车道上和人、自行车、电动车一起穿行、过马路,还要自己进小区、坐电梯,把外卖、商品送到用户手里。为了适应接驳无人机等更复杂的工作,推行科技给这些机器人安上了手臂,这样它们就能完成拿取包装袋、按电梯、推拉门等需要上肢才能完成的任务。


难得的是,在和人类骑手一致的考核制度下,这些机器人的履约率(按时送达的百分比)已达 98.5%,因此拿到的报酬已经可以覆盖自身的成本,做到了单个机器人盈亏平衡。这在还没进入大规模落地阶段的具身智能领域是非常稀有的。


为了了解这个机器人背后的技术和创业思路,机器之心和推行科技创始人卢鹰翔、龙禹含展开了深入对谈。他们指出,让机器人在充满变数的开放物理世界中穿行并不是一件简单的事。为了克服其中的困难,他们走了一条类似于特斯拉的数据驱动路线,利用自研的「骑手影子系统」在短时间内获取了大量高质量数据,因此机器人的表现才能如此出色。未来,他们还将在自然语言、多模态等方向持续迭代,让这个机器人更加实用。


走进开放物理世界,机器人如何工作?  


机器之心:能否简单介绍一下,公司现在在做一件什么事,长期愿景是什么?


卢鹰翔我们希望以数据驱动的方式,打造出可以在开放物理世界中自主移动的机器人。具体而言,我们是通过利用人类驾驶的两轮电瓶车、电动轮椅等产生的驾驶数据,用模仿学习和强化学习的方法,来逐步实现一款能够应对开放物理世界的硬件无关(hardware-agnostic)的具身智能产品。


我们开始行动的第一步就是解决「数据从哪来」的问题。21 年创业之初我们先是搭建了一套基于轮椅平台的「端到端」算法架构,利用轮椅驾驶数据训练末端移动机器人,并在硅谷进行了 8 公里的路测。后来我们意识到末端物流场景是更高效的数据来源,于是开始打造「骑手影子系统」,利用末端物流场景下的骑手骑行数据和机器人产品落地数据构建双数据闭环


目前我们在末端物流场景已经落地了 18 个月,比如给苏州、深圳的山姆会员店等前置仓做物流配送。我们的机器人和公路无人配送车有一个很显著的区别。无人配送车只完成运输任务的中间一段,不会进入小区、商场、写字楼等场所,如果用来进行外卖、商超等本地生活类配送,两端都需要有人参与。相比之下,我们的物流机器人以做到「门到门」的配送为设计目标。比如对于我们合作的奶茶门店,我们的机器人会开进商场,停在柜台前等待装单,装单之后离开商场,跨过两条街,驶入写字楼或小区,然后自己找到电梯、坐电梯上到具体的楼层,把货物送达指定地点。这在许多场景下已经非常贴近骑手的服务能力。所以我们做的事情更多的是属于具身智能这个范畴。


到了去年底、今年初这个时间,我们发现落地环境给我们提出了一些更高的要求。一是特定场所进一步的通达,像操作按钮或开关、按电梯。二是外卖等常见商品的抓取、捡拾。三是打开有把手的推拉门等交互场景。


在这些需求的驱动下,我们开始有针对性地研发上肢能力。这和其他具身智能领域的公司可能有所不同,他们有些会去优化做菜、叠衣服等上肢能力,而我们是根据常见的客户需求有针对性地去解决上述几个问题。


机器之心:利用您提到的上肢能力,你们研发了什么产品?


卢鹰翔:今年 618,我们落地了一款具备上肢操作能力的物流机器人。它的下半身是一个带有装载能力的移动机器人本体,上半身支持三维世界的单臂交互能力。


这个机器人首先用于支持无人机的外卖配送接驳。无人机的降落地点通常和顾客还有一段距离,这个机器人首先要能够把无人机卸下来的货物装进自己的货仓,然后至少要坐一次电梯。有些电梯可能没有梯控,需要手动按按钮。机器人的上肢就是在这些场景中发挥作用。


无人机接驳是个新场景,其实在目前已有的场景中,我们也可以利用这个上肢去干两件事情。一是我们会在它的上面整合一个 RFID(射频识别)芯片,让机器人自己刷卡进小区,而不是依赖保安手动操作。二是在取货人迟迟不来的情况下,让机器人主动把货物从「肚子」里拿出来,放到架子、门口等指定地点,就像骑手放外卖一样。这样可以省去大量的等待时间,提高配送效率。


机器之心:这个机器人可以上台阶吗?它是不是只能送一些设施比较好的小区?


卢鹰翔:这里面其实涉及到三个问题。


第一个问题:能不能上台阶?我们现在的这款物流机器人是不能上台阶的,因为它下面是四个轮子。这是从经济角度考虑做出的一个选择,因为四轮底盘目前是最成熟、最常见的。不过这个轮子经过了特殊设计,有一定的越障能力,能跨越 7 厘米以内的单级台阶或凹陷。


此外,我刚才提到一个概念,叫硬件无关(hardware-agnostic)。其实我们这个系统也成功适配过一些异形底盘,比如四足、双轮足,这些底盘是可以上楼梯的,但可能没有那么稳定。所以,要不要让机器人上台阶其实是取决于我们客户的需求,如果客户想用四条腿的机器狗送外卖或快递,而且愿意接受它的价格,那么我们在技术上是可以打磨的


第二个问题:我们的机器人可以到达什么样的环境?其实我们国家去年出台了一部《无障碍环境建设法》,它对于公共场所提出的要求是:两条腿能到的地方,轮椅都要能到。这部法律不仅要求所有增量的公共场所、建筑物都要满足无障碍要求,目前已有的存量场所也要逐渐完成合规改造。这对于我们来说是一个有利的环境,因为我们机器人的设计尺寸参照的是电动轮椅的国家标准,所以轮椅能到的地方,我们基本上都能到


第三个问题:到不了的地方怎么办?我们现在的应用场景本质上是人机混合,而不是有你无我的一种局面。就是说一个货仓会部署一部分机器人,一部分骑手,大家一起接单。系统在派单的时候会进行一些目的地的筛选。而且这个筛选系统本就存在,不需要额外的开发成本。


从自动驾驶到具身智能,挑战升维


机器之心:公司现在的人才配置是怎样的?这些人才搭建起了一个怎样的技术栈?


卢鹰翔:我们的团队其实是自动驾驶、机器人、机器学习、机械等各个专业背景的人组合起来的一个团队。创始团队成员之前都在硅谷做自动驾驶,就是 L4、Robotaxi 这些方向,之前我们负责研发的车型还拿到了加州政府发放的第二块可以无安全员上路的 Robotaxi 牌照,第一块发给了 Waymo。我们的思路是搭建一套数据驱动的技术栈,类似于美国的特斯拉和英国的 Wayve。受到他们的启发,我们研发了一套「骑手影子系统」,利用骑手驾驶的两轮电瓶车来获取用于算法迭代的训练数据,目的是实现机器人在开放物理世界而不只是公路上的自主移动能力。这种算法架构的好处是性能的天花板非常高,理论上可以无限拟人。


机器之心:公司很多人才都是自动驾驶出身的,这和其他很多具身智能公司的班底其实很相似。能否谈一下,从单纯做自动驾驶扩展到交互维度更高的具身智能,你们遇到了哪些新的挑战? 


卢鹰翔:第一个挑战是环境的不规律。与公路上的自动驾驶汽车相比,我们机器人面临的物理环境是非结构化的,规律性更差。我们知道,公路是按照严格的国家标准来修筑的,但当我们去解决一个开放物理世界中的自主移动问题的时候,这个有利的条件就不存在了。我们现在的落地环境主要是城市,尚有一些建筑规范。但我们落地的其他场景,比如农村,规律性要更差。未来,我们可能还要扩展到野外。


第二个挑战是规则的缺失。公路上有明确的交通规则,也有交警来维持秩序,这相当于人为地让大家的行为变得有规律。这对于机器人来说是非常有利的一个客观条件。但在具身智能所面对的开放物理世界,交通参与者变得更加复杂,包括骑各种车的人甚至宠物,他们的行为要更加随机。


第三个挑战是辅助工具的缺失。公路交通有成熟的生态,所以有一些辅助工具被开发出来,比如百度地图,它可以告诉你前方堵车或施工,请绕行。但开放的物理世界中就缺乏这样的工具。


要解决前两个问题,我们需要大量的训练数据。但是这类数据是非常稀缺的。我们知道,ChatGPT 利用的是人类过去几十年积攒下来的互联网数据。物理世界的数据可能在有了自动驾驶这样的行业之后才被系统地收集,这和互联网数据完全不在一个量级。而我们想要的开放物理世界的训练数据就更稀缺了。针对这个数据获取难题,我们最初的想法是利用人驾驶的电动轮椅来获取众包数据。在接触到末端物流场景和客户之后,我们逐渐迭代成现在这种利用骑手载具,也就是骑手驾驶的电瓶车来获取。


打破数据魔咒杀手锏 ——「量大管饱」的骑手影子系统


机器之心:能否详细介绍一下你们的数据获取思路?


卢鹰翔:在数据获取层面,市面上有几种不同的思路,多数情况下这些思路是并存的。各家公司可能会以不同的比例去选择一种组合方式。


首先说仿真数据。有一部分公司会比较认同仿真数据的价值,比如去年 Hint0n 以顾问身份加入的 Vayu Robotics 机器人公司。我们也用仿真数据,有自己的仿真模拟器。但相比之下,我们更看重真实数据,我们认为真实数据的价值是无可替代的。仿真数据对于我们来说主要是在真实数据的基础上降本增效。


真实数据的获取也分为两种,一种是 on policy 的,一种是 off policy 的。on policy 数据就是部署的机器人在每天使用过程中产生的数据。这种数据目前是非常稀缺且昂贵的,因为它要在机器人落地之后才会有,这就会变成一个「先有鸡还是先有蛋」的问题。所以我们就要突破这个技术瓶颈,实现对 off policy 的数据的利用能力。


简单来说就是,如果只是利用我们部署在山姆的一些机器人来获取数据,它的效率非常低,成本也很高。但是,如果能利用骑手驾驶电瓶车产生的数据,还有一些电动轮椅产生的数据,我们的系统就能够在短时间内获取大量数据,而且这些数据的营养也很丰富。


作为一家看重仿真数据的公司,Vayu Robotics 也是认同真实数据的价值的。他们会在硅谷雇佣一些骑手,产生一些真实世界的数据,然后在这个基础上利用仿真模拟器去训练。


但这方面我们存在一些国情优势。我国是一个非机动车大国,一方面,这意味着我们机器人的应用场景会比较大、比较丰富,覆盖各个城市的大街小巷。另一方面,这也意味着我们的骑手产生的数据是量大管饱的。相比之下,美国的一些公司就不太容易大量获取这类数据,需要请一些专业的人,以高昂的成本去采集。


机器之心:您说的「量大管饱」是怎样一个概念?


卢鹰翔:我这里有一些数据。中国骑手平均每人每天会跑 100 到 200 公里。我们在苏州一个普通超市落地的前置仓,一般配备 15 到 20 个骑手。这些骑手一个月产生的数据轻轻松松就会超过 10 万公里,一年肯定可以超过百万公里,通常可以接近 200 万公里。


作为对比,国内最头部的做 Robotaxi 的 L4 公司,自成立以来积累的数据基本上也只有几百万公里,像 Waymo 这样的全球头部公司也就两千万公里。当然,里程数是一个比较简单的维度。但在这个简单的维度上,我们利用骑手影子系统仅在单一前置仓落地不到两年所产生的数据量,就相当于一家国内头部自动驾驶公司自成立以来的路测积累总和


我们还有一个对比对象,就是特斯拉。他们在 2014 年就推出了第一款搭载 Autopilot 软硬件的车型,开始收集驾驶数据。截至今年初特斯拉推出V12.3,他们在过去十年间一共积累了将近20亿公里人类驾驶数据用于智能驾驶系统的训练,在全球范围内也称得上遥遥领先。而对于中国的600万活跃骑手群体而言,20亿公里只是他们一两天跑的量,我们叫「中国骑手一天,特斯拉汽车十年」。这就是所谓的量大管饱。可以说,骑手影子系统为我们迭代产品提供了非常可靠的数据保障。


但除了量大管饱,骑手影子系统产生的数据还有一些优势。第一是成本。我们是让骑手在送单的过程中积累数据,这对于他们来说没有边际成本,我们的成本也非常低。第二是数据的丰富度。骑手的数据是在真实的生产环境中产生的,而且越是经济发达、人口密集、接近城市中心的地方,它产生的数据就越多。这些数据包含一年四季、各种天气状况。它本身的复杂度、代表度都很好,避免了高度同质化的情况。


所以,无论是从数量、质量还是成本来说,这个系统产生的数据都符合「好数据」的标准。目前,我们已经开始和一些销售电动两轮车的主机厂合作,打算在印度部署这个系统,这也是一个量大管饱的环境。


机器之心:能否详细介绍一下「 骑手影子系统」的技术细节?


卢鹰翔:这个系统主要通过一套车载硬件采三种数据。一是环境数据,即通过摄像头采集路况、障碍物等视觉数据。二是定位数据,通过比较便宜的 RTK 来采集。三是操作数据,即骑手在某种特定情况下进行了什么样的操作,比如踩油门、刹车或者左拐右拐。在采到这些数据后,我们就通过模仿学习和强化学习的方式,让模型去学习人类的行为,逐渐向人类行为靠拢。


机器之心:这个系统能让机器人知道实时路况?


卢鹰翔:是的,因为末端道路的通行能力会非常频繁地发生变化,解决机器人末端移动不仅要解决 AI 问题,还要解决情报问题。就像老司机也需要百度地图来提示前方道路有堵车一样。比如说,在非机动车道上,我们经常会遇到两个拦路桩,它们将道路分成三条。通常中间的那条最好走。但如果临时出现一个商贩占据了中间这条路,开始在那里卖红薯,这条路就走不通了。这个时候,机器人需要提前知道怎么选择最佳路线。而经过这里的骑手自然会做出应变,比如他可能说「师傅能不能让一让」,如果商贩让开了,机器人就能知道这条路是可以通行的。如果不让,骑手就会选择一条次优路线,机器人也能知道。完成这些只需要骑手实时回传 RTK 定位数据。这和百度地图实时提醒前方堵车的原理是相似的。


不仅已落地,还能盈亏平衡


机器之心:刚才提到,去年,图灵奖得主 Hint0n 加入了一家名叫 Vayu Robotics 的机器人公司。在您看来,这家公司有哪些吸引 Hint0n 的特点?


卢鹰翔:当时 Hint0n 自己发了一个帖子来阐述他加入 Vayu 的原因,就是看中了末端物流这个场景的高安全性和可落地性。


我们知道,Hint0n 非常关注 AI 安全。他在帖子里提到,这个送货机器人的动能只有汽车的 1%。拿我们这个机器人来说,它的极限动能也就 500 焦耳,这相当于一个 70 公斤的人从一把椅子高的地方跌落产生的能量。所以如果这个机器人不小心撞到人,它至多把人撞疼,不会撞伤,容错率很高。


图片


图片


高安全性带来的是高可落地性。我们知道,像 Waymo 这样的公司在 Robotaxi 方面已经做得非常好了,平均五万公里左右才接管一次,但距离大规模落地似乎还是遥遥无期。其中一个很大的原因就是它的场景容错率太低了。而 Vayu 和我们选的都是一些高容错率的场景。除了末端物流,其实我们还落地了一些类似场景,比如帮机场驱鸟、帮鱼塘抛洒鱼料。从技术路线上来讲,大家都不约而同地看好这个路线。但相比之下,我们在数据上具备一定的国情优势。


机器之心:你们的机器人盈亏情况如何?  


卢鹰翔:我们可以达到单个机器人的盈亏平衡。


我们落地的末端物流主要是外卖和商超两大块,客户分别是国内在这两个场景市占率最高的两大平台。


商超领域我们其实跑得挺成熟的,比如在苏州,我们给山姆送了 18 个月,累计送了 3 万多单。这 3 万多单累计下来是盈亏平衡的。我可以分享几个数据。第一个是平均效率,国内骑手平均每天送 35 到 40 单,我们的机器人平均每天可以送 20 单,相当于两台机器人可以干一个人的活儿。第二个是履约率,即有多少单是按时、无损送达的,这个数值可能更有意义。通常来讲,我们机器人的履约率可以达到 98.5%,按照达达对于骑手的考核标准,这可以达到 A 级(以 98% 为界)。在这个场景中,我们的机器人和骑手是在一个地方排队的,不需要前置仓为它们配备额外的人力。考核标准也和骑手一样。


外卖是一个比商超更有挑战性的领域。它是多点对多点的配送,也要保证时效。在这个场景中,我们的机器人和人的考核标准也是一样的,超时或出现其他问题也要扣钱。


在跟人类骑手进行平等的奖惩考核的情况下,机器人挣到的钱可以覆盖它的成本,包括折旧、电费、维修费、管理员工资等等。在具身智能产品还没有大规模量产的当下,这种盈亏平衡的情况是非常稀有的。


未来迭代方向:上肢、自然语言和多模态


机器之心:现在,这个机器人拥有上肢了,交互变得更加复杂,你们遇到了哪些新的挑战? 


龙禹含:最大的一个挑战还是数据问题。当机器人的能力扩展到上肢,它的数据是更加稀缺的,全球的科研机构、公司都在花很大的力气去收集数据。但即便如此,数据的多样性依然不足,实际训练出来的模型泛化性也不是很强。比如谷歌的 RT 项目,在做厨房场景时,他们有一个机器人数据厨房,专门用来收集数据。但离开这个厨房进入到真实场景后,他们机器人的成功率还是会大幅下降。


不过,我们机器人的动作相对来说没有那么复杂,比如不用去学叠衣服等涉及柔性物体的动作,也不会像谷歌那样有很多步骤。它的动作基本上可以拆解为一些子问题,比如操作电梯的按钮、操作货物包装袋、拉开门让底盘出去等。在拆解出这些子问题后,我们就可以专门去收集这些场景的数据,然后利用一些模仿学习的算法去学习,让这件事情跑起来。在跑起来之后,我们的机器人会看到一些成功的案例,也会看到一些失败的案例。在看过各种各样的包装袋、门、电梯之后,它的能力就会逐步提升。


机器之心:现在具身智能的一大方向是让机器人听懂自然语言,甚至基于多模态信息来进行推理决策,推行科技在这方面有没有一些计划?


卢鹰翔让机器人听懂自然语言这件事情肯定会去做,而且已经在我们的规划之中,下一代产品就会具备这样一个能力。本身我们机器人产品的应用场景就比较贴近人的日常生活,直接用自然语言交互将是非常实用的一个功能。


龙禹含:关于多模态,其实我们的机器人现在已经在用多模态大模型了。即使是完成刚才提到的按电梯按钮、取货、开关门这样的操作,如果想达到一个比较好的泛化能力,现在最稳定的路径就是利用大模型的多模态能力。


目前我们机器人里的多模态大模型主要用于解决一些视觉问题,比如物体识别、目标物估计。这有别于传统的自动驾驶,后者只针对某些类别,比如汽车、行人、电动车,去做识别。我们的机器人要识别不同样子、不同位置的电梯按钮,不同形状的纸袋、塑料袋以及不同类别的门,它面对的要求更高了,所以我们用多模态大模型来解决这些问题。


机器之心:很多人认为,人形机器人会是具身智能的最终形态,您怎么看?推行科技是否有必要去做人形机器人?


卢鹰翔:说人形机器人会是具身智能的最终形态,这背后的主要逻辑是:目前人类生存的物理世界,比如房子,本身是为人类躯体设计的,所以人形机器人会具备最广泛的通用性。但我们认为,碳基智能和硅基智能之间有一个很大的区别。碳基智能只能支持特定的躯体,比如一个人的大脑只能驱动一个人,一个狗的大脑只能驱动一只狗。但硅基智能可以同时支持多种形态,比如一套智能驾驶系统可以装在本田的车上,也可以装到丰田的车上。所以硅基智能本身不太受具体形态的限制


在认识到这个区别后,我们认为,具身智能不一定非要定义一个最终形态,比如变成人形去适应人类的生存环境。反之,它可以是环境本身。也就是说,它不一定非要去一辆汽车、一幢房子、一条生产线上去工作,它可以是这个汽车、房子、生产线本身。它可以同时存在多种物理形态。


具体到产品开发思路上,我们不会跟风去做一个人形机器人,而是根据客户、场景的需求来决定把机器人做成什么样子,比如它按电梯或者开门需要一只手,我们就给它安一只手。


龙禹含:我补充一下。其实在产品迭代的过程中,我们考虑过两种方向,一种是比较贴近于人的方向,一种就是现在这种方向。我们之所以做出现在这种选择,其实主要是考虑这个产品需要大规模在实际场景中落地。如果做成接近于人的形态,还要在非机动车道上达到接近骑手的速度,我们觉得是不适配的。而且还存在交规风险和居民、客户接受度的风险。未来,我们还是会根据客户的需求以及成本等因素来选择合适的形态。


数据驱动贯穿始终


机器之心:前段时间,李飞飞教授创立了一个空间智能公司,您如何看待这个方向?


卢鹰翔:在看到新闻后,我们也做了一些调研,就是研究李飞飞教授这个公司具体要做什么。我们问了她实验室的学生,结果学生暂时也不太清楚。考虑到李飞飞教授之前一个非常重要的贡献是 ImageNet,而具身智能领域现在既没有特别好的训练数据集,也没有特别成熟的预训练模型,所以我们猜测,她这个新公司可能会在数据方向做一些事情,比如三维场景中人和机器之间相互关系的数据的收集,然后用这些数据去辅助机器人基础大模型的训练。


机器之心:李飞飞等具身智能领域的研究者有没有给你们的创业之路提供一些启发?


龙禹含:数据魔咒已经成为当前具身智能领域的一个共识。李飞飞等研究者给我们的启发,就是要尽快去实际场景中获得更多高质量的数据,而且是用商业化的方式低成本地去获取,然后再反过来推动技术的进一步发展和落地。这是我们在创立推行科技之初就确立的思路。在具身智能领域,这个思路已经被李飞飞教授这样的业界前辈反复印证。这让我们在这个方向的努力变得更加坚定。


作者:机器之心
来源:juejin.cn/post/7383957030345670666
收起阅读 »

HTTP3为什么抛弃了经典的TCP,转而拥抱 QUIC 呢

我们在看一些关于计算机网络的数据或文章的时候,最常听到的就是 TCP、UDP、HTTP 这些,除此之外,我们或多或少可能听过 QUIC这个东西,一般跟这个词一起出现的是 HTTP3,也就是HTTP协议的3.0版本,未来2.x 版本的升级方案。QUIC...
继续阅读 »

我们在看一些关于计算机网络的数据或文章的时候,最常听到的就是 TCP、UDP、HTTP 这些,除此之外,我们或多或少可能听过 QUIC这个东西,一般跟这个词一起出现的是 HTTP3,也就是HTTP协议的3.0版本,未来2.x 版本的升级方案。

QUIC 由 Google 主导设计研发。我们都知道 HTTP 协议是应用层协议,在传输层它使用的是 TCP 作为传输协议,当然这仅仅是对于 HTTP/1 和 HTTP/2 而言的。而 QUIC 的设计的对标协议就是 TCP ,也就是说将来只要能使用 TCP 的地方,都可以用 QUIC 代替。

Google 最开始的目的就是为了替换 HTTP 协议使用的 TCP 协议,所以最开始的名字叫做 HTTP over QUIC,后来由 IETF 接管后更名为 HTTP/3。所以,现在说起 HTTP/3 ,实际指的就是利用 QUIC 协议的版本。

TCP 不好吗,为什么还要 QUIC

TCP 协议作为传输层最负盛名的协议,可谓是久经考验。只要一说到 TCP ,我们都能说出来它是一种面向连接的、可靠的、基于字节流的传输层通信协议。

TCP 通过三次握手的方式建立连接,并且通过序号、ACK确认、丢包重传以及流量控制、拥塞控制等各种繁琐又细致的方式来保证它的可靠性。

关于 TCP 的更多细节,有兴趣的可以读读我之前写的《轻解计算机网络》里的 一个网管的自我修养之TCP协议

看上去很完美了,那为什么还要重新搞个 QUIC 出来呢,而且还被作为下一代 HTTP 的实现协议。确实不存在完美的协议,TCP 协议在整个发展过程中经过了多次改进,但是由于牵扯到互联网世界浩如烟海的各种设备,每次更新、修改都要考虑兼容问题,历史包袱太重,以至于尾大不掉。

所以为了弥补 TCP 的不足,在 TCP 上直接修改不太可能,那最好的方案就是重新开发一套协议。这种协议要吸收 TCP 的精华,又要解决 TCP 的不足,这就是 QUIC 出现的意义。

TCP 的问题-队头阻塞

时至今日,互联网上大多数网站都已经支持 HTTP/2 协议了,你可以在浏览器开发者工具中看一下网络请求,其中的 Protocol 表示网络请求采用的协议。

image.png

HTTP/2的一个主要特性是使用多路复用(multiplexing),因而它可以通过同一个TCP连接发送多个逻辑数据流。复用使得很多事情变得更快更好,它带来更好的拥塞控制、更充分的带宽利用、更长久的TCP连接————这些都比以前更好了,链路能更容易实现全速传输。标头压缩技术也减少了带宽的用量。

采用HTTP/2后,浏览器对每个主机一般只需要 一个 TCP连接,而不是以前常见的六个连接。

如下图所示,HTTP/2 在使用 TCP 传输数据的时候,可以在一个连接上传输两个不同的流,红色是一个流,绿色是另外一个流,但是仍然是按顺序传输的,假设其中有一个包丢了,那整个链路上这个包后面的部分都要等待。

image.png

这就造成了阻塞,虽然一个连接可传多个流,但仍然存在单点问题。这个问题就叫做队头阻塞。

QUIC 如何解决的

TCP 这个问题是无解的,QUIC 就是为了彻底解决这个问题。

如下图所示,两台设备之间建立的是一个 QUIC 连接,但是可以同时传输多个相互隔离的数据流。例如黄色是一个数据流,蓝色是一个数据流,它俩互不影响,即便其中一个数据流有丢包的问题,也完全不会影响到其他的数据流传输。

这样一来,也就解决了 TCP 的队头阻塞问题。

image.png

为什么要基于 UDP 协议

QUIC 虽然是和TCP 平行的传输协议,工作在传输层,但是其并不是完全采用全新设计的,而是对 UDP 协议进行了包装。

UDP 是无连接的,相对于 TCP 来说,无连接就是不可靠的,没有三次握手,没有丢包重传,没有各种各样的复杂算法,但这带来了一个好处,那就是速度快。

而 QUIC 为了达到 TCP 的可靠性,所以在 UDP 的基础上增加了序号机制、丢包重传等等 UDP 没有而 TCP 具有的特性。

既然这么多功能都做了,还差一个 UDP 吗,完全全新设计一个不好吗,那样更彻底呀。

之所以不重新设计应该有两个原因:

  1. UDP 本身就是非常经典的传输层协议,对于快速传输来说,其功能完全没有问题。
  2. 还有一个重要的原因,前面也说到了,互联网上的设备太多,而很多设备只认 TCP 和 UDP 协议,如果设计一个完全全新的协议,很难实施。

QUIC 协议

不需要三次握手

QUIC 建立连接的速度是非常快的,不需要 TCP 那样的三次握手,称之为 0-RTT(零往返时间)及 1-RTT(1次往返时间)。

QUIC 使用了TLS 1.3传输层安全协议,所以 QUIC 传输的数据都是加密的,也就是说 HTTP/3 直接就是 HTTPS 的,不存在 HTTP 的非加密版本。

正是因为这样,所以,QUIC 建立连接的过程就是 TLS 建立连接的过程,如下图这样,首次建立连接只需要 1-RTT。

image.png

而在首次连接建立之后,QUIC 客户端就缓存了服务端发来的 Server Hello,也就是加密中所需要的一些内容。在之后重新建立连接时,只需要根据缓存内容直接加密数据,所以可以在客户端向服务端发送连接请求的同时将数据也一并带过去,这就是 0-RTT 。

连接不依靠 IP

QUIC 在建立连接后,会为这个连接分配一个连接 ID,用这个 ID 可以识别出具体的连接。

假设我正在家里用 WIFI 发送请求,但是突然有事儿出去了,马上切换到了蜂窝网络,那对于 QUIC 来说就没有什么影响。因为这个连接没有变,所以仍然可以继续执行请求,数据该怎么传还怎么传。

而如果使用的是 TCP 协议的话,那只能重新建立连接,重传之前的数据,因为 TCP 的寻址依靠的是 IP 和 端口。

未来展望

随着 QUIC 协议的不断完善和推广,其应用场景将更加广泛,对互联网传输技术产生深远的影响。未来的互联网,将是 QUIC 和 HTTP3 主导的时代。

要知道,HTTP/1 到 HTTP/2,中间用了整整 16 年才完成,这还只是针对协议做改进和优化,而 QUIC 可谓是颠覆性的修改,可想而知,其过程只会更加漫长。



作者:古时的风筝
来源:juejin.cn/post/7384266820466180148
收起阅读 »

微信小程序:uniapp解决上传小程序体积过大的问题

web
概述 在昨天的工作中遇到了一个微信小程序上传代码过大的情况,在这里总结一下具体的解决步骤,首先介绍一下,技术栈是使用uniapp框架+HBuilderX的开发环境。 错误提示 真机调试,提示包提交过大,不能正常生成二维码,后续上传代码更是不可能了,减少包中的...
继续阅读 »

概述


在昨天的工作中遇到了一个微信小程序上传代码过大的情况,在这里总结一下具体的解决步骤,首先介绍一下,技术栈是使用uniapp框架+HBuilderX的开发环境。


错误提示


图4.png


真机调试,提示包提交过大,不能正常生成二维码,后续上传代码更是不可能了,减少包中的体积顺着这条思路去解决问题。


1.静态图片资源改变成网络请求的方式


问题5.png


我们使用的初衷是,把图片加载在static本地,缓存在本地,以便提升更快的响应速度,第一步剥离大的图片更换成网络请求,顺着编辑器提示去处理。


2.对小程序进行分包


小程序主包最大可以加载到1.5M,加载所有的依赖和插件不能大于2M,小程序中有个解决办法是对小程序进行分包处理,使每个包保持在2M的大小,主包和分包之间直接进行跳转,分包和分包不能跳转。


"optimization" : {
"subPackages" : true
},

进行了拆包还是没有解决问题,分包的作用主要运行的是代码,也就是说代码要尽量的小,多了需要进行分解。


3.压缩vendor.js


昨天真正的定位问题是vendor.js 1.88M ,小程序开发代码工具-详情-代码依赖分析中查看,解决vendor.js才是根本的解决之道。


使用HBuilderX打包上传来解决问题,HBuilderX -> 发行 -> 小程序(微信),操作的过程失败了一次,是因为需要注意的是需要绑定开发者后台的地方,开发管理->开发设置->小程序代码上传下载小程序代码上传密钥和绑定IP白名单,这个需要管理员同意。


问题6.png


最后包的体积从12.88M压缩到了4.16M,问题得以解决。


作者:stark张宇
来源:juejin.cn/post/7282363816020508733
收起阅读 »

uni-app app端 人脸识别

web
在听到人脸识别,连忙去看看,去阿里 腾讯 看他们的人脸识别方法,官方sdk什么的。 到后来,需求确定了,拍照(照片)上传,后台去识别是不是本人,这一瞬间从天堂到地狱,放着官方那么好的方法。 用照片,还的自己去写,去实现。 下面为大家提供一个 uni-app 自...
继续阅读 »

在听到人脸识别,连忙去看看,去阿里 腾讯 看他们的人脸识别方法,官方sdk什么的。


到后来,需求确定了,拍照(照片)上传,后台去识别是不是本人,这一瞬间从天堂到地狱,放着官方那么好的方法。


用照片,还的自己去写,去实现。


下面为大家提供一个 uni-app 自动拍照 上传照片 后端做匹配处理。


参考插件市场的 ext.dcloud.net.cn/plugin?id=4…


在使用前 先去manifest.json 选择APP模块配置, 勾选直播推流



直接采用nvue开发,直接使用live-pusher组件进行直播推流,如果是vue开发,则需要使用h5+的plus.video.LivePusher对象来获取


nuve js注意事项


注意nuve 页面 main.js 的封装函数 。无法直接调用(小程序其他的端没有测试)


在APP端 this.api报错,显示是undefined,难道nvue页面,要重新引入api文件


在APP端,main.js中挂载Vuex在nvue页面无法使用this.$store.state.xxx


简单粗暴点直接用uni.getStorageSync 重新获取一遍


//获取用户数据 userInfo在Data里定义


this``.userInfo = uni.getStorageSync(``'userInfo'``)


nuve css注意事项


单位只支持px


其他的em,rem,pt,%,upx 都不支持


需要重新引入外部css


不支持使用 import 的方式引入外部 css


<``style src="@/common/test.css"></``style``>


 默认flex布


display: flex; //不需要写
//直接用下面的标签
flex-direction: column;
align-items: center;
justify-content: space-between;

页面样式


<view class="live-camera" :style="{ width: windowWidth, height: windowHeight }">
<view class="title">
{{second}}秒之后开始识别
</view>
<view class="preview" :style="{ width: windowWidth, height: windowHeight-80 }">
<live-pusher id="livePusher" ref="livePusher" class="livePusher" mode="FHD" beauty="1" whiteness="0"
aspect="2:3" min-bitrate="1000" audio-quality="16KHz" :auto-focus="true" :muted="true"
:enable-camera="true" :enable-mic="false" :zoom="false" @statechange="statechange"
:style="{ width: cameraWidth, height: cameraHeight }">
</live-pusher>

<!--提示语-->
<cover-view class="remind">
<text class="remind-text" style="">{{ message }}</text>
</cover-view>

<!--辅助线-->
<cover-view class="outline-box" :style="{ width: windowWidth, height: windowHeight-80 }">
<cover-image class="outline-img" src="../../static/idphotoskin.png"></cover-image>
</cover-view>
</view>
</view>

JS部分


<script>
import operate from '../../common/operate.js'
import api from '../../common/api.js'
export default {
data() {
return {
//提示
message: '',
//相机画面宽度
cameraWidth: '',
//相机画面宽度
cameraHeight: '',
//屏幕可用宽度
windowWidth: '',
//屏幕可用高度
windowHeight: '',
//流视频对象
livePusher: null,
//照片
snapshotsrc: null,
//倒计时
second: 0,
ifPhoto: false,
// 用户信息
userInfo: []
};
},
onLoad() {
//获取屏幕高度
this.initCamera();
//获取用户数据
this.userInfo = uni.getStorageSync('userInfo')
setTimeout(() => {
//倒计时
this.getCount()
}, 500)
},
onReady() {
// console.log('初始化 直播组件');
this.livePusher = uni.createLivePusherContext('livePusher', this);
},
onShow() {
//开启预览并设置摄像头
/*
* 2023年12月28日
* 在最新的APP上面这个周期 比onReady 直播初始要早执行
* 故而第二次进入页面 相机启动失败
* 把该方法 移步到 onReady 即可
*/


this.startPreview();

},
methods: {
//获取屏幕高度
initCamera() {
let that = this
uni.getSystemInfo({
success: function(res) {
that.windowWidth = res.windowWidth;
that.windowHeight = res.windowHeight;
that.cameraWidth = res.windowWidth;
that.cameraHeight = res.windowWidth * 1.5;
}
});
},
//启动相机
startPreview() {
this.livePusher.startPreview({
success(res) {
console.log('启动相机', res)
}
});
},
//停止相机
stopPreview() {
let that = this
this.livePusher.stopPreview({
success(res) {
console.log('停止相机', res)
}
});
},
//摄像头 状态
statechange(e) {
console.log('摄像头', e);
if (this.ifPhoto == true) {
//拍照
this.snapshot()
}
},
//抓拍
snapshot() {
let that = this
this.livePusher.snapshot({
success(res) {
that.snapshotsrc = res.message.tempImagePath;
that.uploadingImg(res.message.tempImagePath)
}
});
},
// 倒计时
getCount() {
this.second = 5
let timer = setInterval(() => {
this.second--;
if (this.second < 1) {
clearInterval(timer);
this.second = 0
this.ifPhoto = true
this.statechange()
}
}, 1000)
},
// 图片上传
uploadingImg(e) {
let url = e
// console.log(url);
let that = this
uni.uploadFile({
url: operate.api + 'api/common/upload',
filePath: url,
name: 'file',
formData: {
token: that.userInfo.token
},
success(res) {
// console.log(res);
let list = JSON.parse(res.data)
// console.log(list);
that.request(list.data.fullurl)
}
})
},
//验证请求
request(url) {
let data = {
token: this.userInfo.token,
photo: url
}
api.renzheng(data).then((res) => {
// console.log(res);
operate.toast({
title: res.data.msg
})
if (res.data.code == 1) {
setTimeout(() => {
operate.redirectTo('/pages/details/details')
}, 500)
}
if (res.data.code == 0) {
setTimeout(() => {
this.anew(res.data.msg)
}, 500)
}
})
},
// 认证失败,重新认证
anew(msg) {
let that = this
uni.showModal({
content: msg,
confirmText: '重新审核',
success(res) {
if (res.confirm) {
// console.log('用户点击确定');
that.getCount()
} else if (res.cancel) {
// console.log('用户点击取消');
uni.navigateBack({
delta: 1
})
}
}
})
},
}
};
</script>

css 样式


<style lang="scss">
// 标题
.title {
font-size: 35rpx;
align-items: center;
justify-content: center;
}

.live-camera {
.preview {
justify-content: center;
align-items: center;

.outline-box {
position: absolute;
top: 0;
left: 0;
bottom: 0;
z-index: 99;
align-items: center;
justify-content: center;

.outline-img {
width: 750rpx;
height: 1125rpx;
}
}

.remind {
position: absolute;
top: 880rpx;
width: 750rpx;
z-index: 100;
align-items: center;
justify-content: center;

.remind-text {
color: #dddddd;
font-weight: bold;
}
}
}
}
</style>


作者:虚乄
来源:juejin.cn/post/7273126566459719741
收起阅读 »

35岁,是终点?还是拐点?

35岁,是终点还是拐点,取决于我们对生活和事业的态度、目标以及行动。这个年龄可以看作是一个重要的转折点,具有多重意义和可能性。 很多人在35岁时,已经在自己的职业生涯中建立了一定的基础,可能达到了管理层或专家级别。如果你还是一个基层员工,那你要反思一下,你的...
继续阅读 »

35岁,是终点还是拐点,取决于我们对生活和事业的态度、目标以及行动。这个年龄可以看作是一个重要的转折点,具有多重意义和可能性。



很多人在35岁时,已经在自己的职业生涯中建立了一定的基础,可能达到了管理层或专家级别。如果你还是一个基层员工,那你要反思一下,你的职业生涯规划可能出了问题,工作能力与人情世故为什么都没有突破?是否在某个领域深耕多年?



有些人可能会选择在这个年龄段重新评估自己的职业,考虑转型或创业,寻找新的挑战和机遇。这是个不错的想法,基于过往积累的经验和能力,现在自媒体发达,个人的创业成本低到0也可以创业,就是你能坚持多久,给自己多长时间的规划,又想达成怎样的目标。



35岁通常是家庭责任较重的时期,可能要照顾孩子和父母,家庭生活会影响个人的时间和精力分配。这是35岁中年油腻大叔最难的地方,上有老,下有小,自己还是很渺小。这是最痛苦的,家里顶梁柱,连倒下的资格都没有。



在自我认知层面,35岁的人通常对自己的优缺点、兴趣爱好有更清晰的认识,知道自己想要什么,不想要什么。所以这个年龄段的人可能会更加关注健康和自我提升,进行一些以前没有时间或精力去做的事情,如学习新技能、锻炼身体等。


举个例子,现在的 AI 和鸿蒙,热得烫手,是程序员学习的新方向,不同于区块链、AR/VR这些事件,AI 落地各行各业产品,未来是 XXX+AI 的时代,不管你正在做 JAVA 后端开发,还是即将学习 JAVA 开发,你都逃脱不了 AI。



鸿蒙就不用说了,国产操作系统,多少年了,国人终于可以用上自己的操作系统,这不是事件,不是概念,是必然趋势,一个新的技术领域将要开启,我是很坚信的,拒绝反驳。



经过多年的生活和工作经历,相信大多数人已经积累了较为丰富的经验和智慧,对待问题更加冷静和理性。


35岁既不是终点,也不是绝对的拐点,而是人生旅途中一个重要的里程碑。它为未来的发展提供了丰富的经验和更明确的方向。关键在于如何利用过去的积累,进行自我调整和规划,为未来的生活和事业开辟新的道路。以上是 V哥的浅见,突出想到这些问题,就记录了下来,你有什么见解,咱们评论区讨论,拍砖请下手轻点,欢迎关注威哥爱编程,程序员路上愿与你成为一起前行的基友。


作者:威哥爱编程
来源:juejin.cn/post/7383342927509471283
收起阅读 »

Jquery4.0发布!下载量依旧是 Vue 的两倍!

web
前言 大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心~ 背景 其实在去年,Jquery 就宣布了要发布 4 版本 可以看到,Jquery 在五天前发布了 4 版本 Jquery4.0 更新了啥? 接下来说一下到...
继续阅读 »

前言


大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心~


背景


其实在去年,Jquery 就宣布了要发布 4 版本



可以看到,Jquery 在五天前发布了 4 版本




Jquery4.0 更新了啥?


接下来说一下到底更新了啥?


弃用了 1x 和 2x 版本,废弃一些方法


这意味着不再去兼容低版本了,未来 Jquery 将着力于发展新的版本,弃用了一些方法



  • jQuery.cssNumber

  • jQuery.cssProps

  • jQuery.isArray

  • jQuery.parseJSON

  • jQuery.nodeName

  • jQuery.isFunction

  • jQuery.isWindow

  • jQuery.camelCase

  • jQuery.type

  • jQuery.now

  • jQuery.isNumeric

  • jQuery.trim

  • jQuery.fx.interval


Typescript 重构


看过 Jquery 源码的都知道,以前 Jquery 是用 JavaScript 写的,现在新版本是采用 Typescript 重构的,提高整体代码的可维护性


对新特性的支持


jQuery 4.0 将添加对新的 JavaScript 特性的支持,包括:



  • async/await

  • Promise

  • Optional Chaining

  • Nullish Coalescing


优化性能



  • 优化 DOM 操作

  • 改进事件处理

  • 优化 Ajax 请求

  • 增强兼容性


增强兼容性



  • 支持 Internet Explorer 11 和更高版本

  • 支持 Edge 浏览器

  • 支持 Safari 浏览器


FormData 支持


jQuery.ajax 添加了对二进制数据的支持,包括 FormData。


此外,jQuery 4.0 还删除了自动 JSONP 升级、将 jQuery source 迁移至 ES 模块;以及添加了对 Trusted Types 的支持,确保以 TrustedHTML 封装的 HTML 能以不违反 require-trusted-types-for 内容安全策略指令的方式用作 jQuery 操作方法的输入。


由于删除了 Deferreds 和 Callbacks(现在压缩后不到 20k 字节),jQuery 4.0.0 的 slim build 变得更加小巧。


还有人用 Jquery 吗?


随着现在前端发展的迅速,越来越多人投入了 React、Vue 的怀抱,这意味着越来越少人用 Jquery 了,而且用 Jquery 的基本都是老项目,老项目都是求稳的,所以也不会去升级 Jquery


所以我不太看好 Jquery 后续的发展趋势,虽然曾经它真的帮助了我们很多


虽然如此,现阶段 NPM 上,Jquery 的下载量依旧是 Vue 的两倍



作者:Sunshine_Lin
来源:juejin.cn/post/7362727170039070771
收起阅读 »

适配最新微信小程序隐私协议开发指南,兼容uniapp版本

web
前一阵微信小程序官方发布了一个用户隐私保护指引填写说明,说是为了规范开发者的用户个人信息处理行为,保障用户合法权益,小程序、插件中涉及处理用户个人信息的开发者,均需补充相应用户隐私保护指引。 估计是有啥政策强制要求,微信自己也还没想好要咋实现就匆匆发布了第一版...
继续阅读 »

前一阵微信小程序官方发布了一个用户隐私保护指引填写说明,说是为了规范开发者的用户个人信息处理行为,保障用户合法权益,小程序、插件中涉及处理用户个人信息的开发者,均需补充相应用户隐私保护指引。


估计是有啥政策强制要求,微信自己也还没想好要咋实现就匆匆发布了第一版,然后不出意外各种问题,终于在2023年8月22发布了可以正常接入调试的版本。


逛开发者社区很多人在吐槽这个东西,按照现在的实现方式微信完全可以自己在它的框架层实现,非得让开发者多此一举搞个弹窗再去调它的接口通知它。


吐槽归吐槽,代码还是要改的不是,毕竟不改9月15号之后相关功能就直接挂了!时间紧任务重下面直说干货。


准备工作



  • 小程序后台设置用户隐私保护指引,需要等待审核通过:设置-基本设置-服务内容声明-用户隐私保护指引

  • 小程序的基础库版本从 2.32.3 开始支持,所以要选这之后的版本

  • 在 app.json 中加上这个设置 " usePrivacyCheck" : true,在2023年9月15号之前需要自己手动加这个设置,15号之后平台就强制了


具体步骤可以参考官方给的开发文档,里面也有官方提供的 demo 文件。



原生小程序适配代码


直接参考的官方给的 demo3 和 demo4 综合修改出的版本,通过组件的方式引用,所有相关处理逻辑全部放到了 privacy 组件内部,其他涉及到隐私接口的页面只需在 wxml 里引用一下就行了,其他任何操作都不需要,组件内部已经全部处理了。


网上有其他人分享的,要在页面 onLoad、onShow 里获取是否有授权这些,用下面的代码这些都不需要,只要页面里需要隐私授权,引入 privacy 组件后,用户触发了隐私接口时会自动弹出此隐私授权弹窗。



长话短说这一步你总共只需做2个步骤:



  1. 新建一个 privacy 组件:privacy.wxml、privacy.wxss、privacy.js、privacy.json,完整代码在下方

  2. 在涉及隐私接口的页面引入 privacy 组件,如果使用的页面比较多,可以直接在 app.json 文件里通过 usingComponents 全局引入


privacy.wxml


<view wx:if="{{innerShow}}" class="privacy">
<view class="privacy-mask" />
<view class="privacy-dialog-wrap">
<view class="privacy-dialog">
<view class="privacy-dialog-header">用户隐私保护提示</view>
<view class="privacy-dialog-content">感谢您使用本小程序,在使用前您应当阅读井同意<text class="privacy-link" bindtap="openPrivacyContract">《用户隐私保护指引》</text>,当点击同意并继续时,即表示您已理解并同意该条款内容,该条款将对您产生法律约束力;如您不同意,将无法继续使用小程序相关功能。</view>
<view class="privacy-dialog-footer">
<button
id="btn-disagree"
type="default"
class="btn btn-disagree"
bindtap="handleDisagree"
>不同意</button>
<button
id="agree-btn"
type="default"
open-type="agreePrivacyAuthorization"
class="btn btn-agree"
bindagreeprivacyauthorization="handleAgree"
>同意并继续</button>
</view>
</view>
</view>
</view>

privacy.wxss


.privacy-mask {
position: fixed;
z-index: 5000;
top: 0;
right: 0;
left: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.2);
}

.privacy-dialog-wrap {
position: fixed;
z-index: 5000;
top: 16px;
bottom: 16px;
left: 80rpx;
right: 80rpx;
display: flex;
align-items: center;
justify-content: center;
}

.privacy-dialog {
background-color: #fff;
border-radius: 32rpx;
}

.privacy-dialog-header {
padding: 60rpx 40rpx 30rpx;
font-weight: 700;
font-size: 36rpx;
text-align: center;
}

.privacy-dialog-content {
font-size: 30rpx;
color: #555;
line-height: 2;
text-align: left;
padding: 0 40rpx;
}

.privacy-dialog-content .privacy-link {
color: #2f80ed;
}

.privacy-dialog-footer {
display: flex;
padding: 20rpx 40rpx 60rpx;
}

.privacy-dialog-footer .btn {
color: #FFF;
font-size: 30rpx;
font-weight: 500;
line-height: 100rpx;
text-align: center;
height: 100rpx;
border-radius: 20rpx;
border: none;
background: #07c160;
flex: 1;
margin-left: 30rpx;
justify-content: center;
}

.privacy-dialog-footer .btn::after {
border: none;
}

.privacy-dialog-footer .btn-disagree {
color: #07c160;
background: #f2f2f2;
margin-left: 0;
}

privacy.js


let privacyHandler
let privacyResolves = new Set()
let closeOtherPagePopUpHooks = new Set()

if (wx.onNeedPrivacyAuthorization) {
wx.onNeedPrivacyAuthorization(resolve => {
if (typeof privacyHandler === 'function') {
privacyHandler(resolve)
}
})
}

const closeOtherPagePopUp = (closePopUp) => {
closeOtherPagePopUpHooks.forEach(hook => {
if (closePopUp !== hook) {
hook()
}
})
}

Component({
data: {
innerShow: false,
},
lifetimes: {
attached: function() {
const closePopUp = () => {
this.disPopUp()
}
privacyHandler = resolve => {
privacyResolves.add(resolve)
this.popUp()
// 额外逻辑:当前页面的隐私弹窗弹起的时候,关掉其他页面的隐私弹窗
closeOtherPagePopUp(closePopUp)
}

closeOtherPagePopUpHooks.add(closePopUp)

this.closePopUp = closePopUp
},
detached: function() {
closeOtherPagePopUpHooks.delete(this.closePopUp)
}
},
pageLifetimes: {
show: function() {
this.curPageShow()
}
},
methods: {
handleAgree(e) {
this.disPopUp()
privacyResolves.forEach(resolve => {
resolve({
event: 'agree',
buttonId: 'agree-btn'
})
})
privacyResolves.clear()
},
handleDisagree(e) {
this.disPopUp()
privacyResolves.forEach(resolve => {
resolve({
event: 'disagree',
})
})
privacyResolves.clear()
},
popUp() {
if (this.data.innerShow === false) {
this.setData({
innerShow: true
})
}
},
disPopUp() {
if (this.data.innerShow === true) {
this.setData({
innerShow: false
})
}
},
openPrivacyContract() {
wx.openPrivacyContract({
success: res => {
console.log('openPrivacyContract success')
},
fail: res => {
console.error('openPrivacyContract fail', res)
}
})
},
curPageShow() {
if (this.closePopUp) {
privacyHandler = resolve => {
privacyResolves.add(resolve)
this.popUp()
// 额外逻辑:当前页面的隐私弹窗弹起的时候,关掉其他页面的隐私弹窗
closeOtherPagePopUp(this.closePopUp)
}
}
}
}
})

privacy.json


{
"component": true,
"usingComponents": {}
}

uniapp版本


uniapp 版本也可以直接用上面的代码,新建的 privacy 组件放到微信小程序对应的 wxcompnents 目录下,这个目录下是可以直接放微信小程序原生的组件代码的,因为目前只有微信小程序有这个东西,后期还可能随时会更改,所以没必要再额外去封装成 vue 组件了。


页面引用组件的时候直接用条件编译去引用:


{
// #ifdef MP-WEIXIN
"usingComponents": {
"privacy": "/wxcomponents/privacy/privacy"
}
// #endif
}

在 vue 页面里使用组件也要用条件编译:


<template>
<view>
<!-- #ifdef MP-WEIXIN -->
<privacy />
<!-- #endif -->
</view>
</template>

注意uniapp官方目前还没有来适配微信这,目前开发调试 usePrivacyCheck 这个设置放到 page.json 文件里无效的,要放到 manifest.json 文件的 mp-weixin 下面:


{
"name" : "uni-plus",
"appid" : "__UNI__3C6F1BF",
"mp-weixin" : {
"appid" : "wx123456789",
"__usePrivacyCheck__" : true
}
}

作者:cafehaus
来源:juejin.cn/post/7272276908381175819
收起阅读 »

扒一扒uniapp是如何做ios app应用安装的

为何要扒 因为最近有移动端业务的需求,用uniapp做了ios、Android双端的app应用,由于没有资质上架AppStore和test flight,所以只能使用苹果的超签(需要ios用户提供uuid才能加入测试使用,并且只支持100人安装使用)。打包出来...
继续阅读 »

为何要扒


因为最近有移动端业务的需求,用uniapp做了ios、Android双端的app应用,由于没有资质上架AppStore和test flight,所以只能使用苹果的超签(需要ios用户提供uuid才能加入测试使用,并且只支持100人安装使用)。打包出来生成的是一个ipa包,并不能直接安装,要通过爱思助手这类的应用装一下ipa包。但交付到客户手上就有问题了,还需要电脑连接助手才能安装,那岂不是每次安装新版什么的,都要打开电脑搞一下。因此,才有了这次的扒一扒,目标就是为了解决只提供一个下载链接用户即可下载,不用再通过助手类应用安装ipa包。




开干


官方模板




先打开uniapp云打包一下项目看看


image-20230824112232275.png




复制地址到移动端浏览器打开看看


image-20230824112410817.png


这就对味了,都知道ios是不能直接打开ipa文件进行安装的,接下来就研究下这个页面的执行逻辑。




开扒




F12打开choromdevtools,ctrl+s保存网页html。


image.png


保存成功,接下来看看html代码(样式代码删除了)


    <!DOCTYPE html>
<!-- saved from url=(0077)https://ide.dcloud.net.cn/build/download/2425a4b0-4229-11ee-bd1b-67afccf2f6a7 -->
<html>
   <head>
       <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  <meta name="viewport" content="initial-scale=1.0, maximum-scale=1.0, user-scalable=no, width=device-width">
  <meta name="apple-mobile-web-app-capable" content="yes">
  <meta name="apple-mobile-web-app-status-bar-style" content="black">
 </head>

<body>
<br><br>
   <center>
       <a class="button" href="itms-services://?action=download-manifest&amp;url=https://ide.dcloud.net.cn/build/ipa-xxxxxxxxxxx.plist">点击安装</a>
   </center>
   <br><br>
   <center>注意:只提供包名为io.dcloud.appid的包的直接下载安装,如果包名不一致请自行搭建下载服务器</center>
</body>
</html>



解析




从上面代码可以看出,关键代码就一行也就是a标签的href地址("itms-services://?action=download-manifest&url=ide.dcloud.net.cn/build/ipa-x…")


先看看itms-services是什么意思,下面是代码开发助手给的解释


image-20230824113418246.png


大概意思就是itms-services是苹果提供给开发者一个的更新或安装应用的协议,用来做应用分发的,需要指向一个可下载的plist文件地址。




什么又是plist呢,这里再请我们的代码开发助手解释一下


image-20230824113748570.png


对于没接触过ios相关开发的,连plist文件怎么写都不知道,既然如此,那接下来就来扒一下dcloud的pilst文件,看看官方是怎么写的吧。




打开浏览器,copy一下刚刚扒下来的html文件下a标签指向的地址,复制url后面plist文件的下载地址粘贴到浏览器保存到桌面。


image-20230824114108792.png


访问后会出现


image-20230824115354028.png




别担心,这时候直接按ctrl+s可以直接保存一个plist.xml文件,也可以打开devtools查看网络请求,找到ipa开头的请求


image-20230824115609551.png


直接新建一个plist文件,cv一下就好,我这里就选择保存它的plist.xml文件,接下来康康文件里到底是什么


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>items</key>
<array>
<dict>
<key>assets</key>
<array>
<dict>
<key>kind</key>
<string>software-package</string>
<key>url</key>
<string>https://bdpkg-aliyun.dcloud.net.cn/20230824/xxxxx/Pandora.ipa?xxxxxxxx</string>
</dict>
      <dict>
<key>kind</key>
<string>display-image</string>
<key>needs-shine</key>
<false/>
<key>url</key>
<string>https://qiniu-web-assets.dcloud.net.cn/unidoc/zh/uni.png</string>
</dict>
</array>
<key>metadata</key>
<dict>
<key>kind</key>
<string>software</string>
<key>bundle-identifier</key>
<string>xxxxx</string>
<key>title</key>
<string>HBuilder手机应用</string>
</dict>
</dict>
</array>
</dict>
</plist>

直接抓重点,这里存你存放ipa包的地址


image-20230824120013828.png


这里改你应用的昵称


image-20230824120453368.png


这里改图标


image-20230824120509797.png


因篇幅限制,想了解plist的自行问代码助手或者搜索引擎。




为我所用


分析完了,如何为我所用呢,首先按照分析上扒下来的plist文件修改下自身应用的信息,并且需要服务器存放ipa文件,这里我选择了unicloud,开发者可以申请一个免费的空间(想了解更多的自己去dcloud官网看看,说多了有打广告嫌疑),替换好大概如下:


image-20230824155040313.png


将plist文件放到服务器上后,拿到plist的下载地址,打开扒下来的html,将a标签上的url切换成plist文件的下载地址,如图:


image-20230824155306228.png


可以把页面上没用的信息都删掉,保存,再把html放到服务器上,用户访问这个地址,就可以直接下载描述文件安装ipa包应用了(记得需要添加用户的uuid到开发者账号上),其实至此需求已经算是落幕了,但转念想想还是有点麻烦,于是又优化了一下,将a标签中的href信息,直接加载到二维码上供用户扫描便可直接下载,相对来说更方便一点,于是我直接打开草料,生成了一个二维码,至此,本次扒拉过程结束,需求落幕!


作者:廿一c
来源:juejin.cn/post/7270799565963149324
收起阅读 »

在微信小程序里使用rpx,被坑了😕 | 不同设备表现不同

web
小小需求 实现一个 Tooltip,就是那种很简单有一个按钮,按一下就会出现或消失气泡的组件,就是下面这个效果。 放个按钮、加个点击事件、做好气泡的定位好像就搞定了。然鹅,事情没有发展的这么顺利。 开发 组件结构 按照上面说的过程,放好按钮,添加好点击事...
继续阅读 »

小小需求


实现一个 Tooltip,就是那种很简单有一个按钮,按一下就会出现或消失气泡的组件,就是下面这个效果。


gif


放个按钮、加个点击事件、做好气泡的定位好像就搞定了。然鹅,事情没有发展的这么顺利。


开发


组件结构


按照上面说的过程,放好按钮,添加好点击事件。比较复杂的地方就是处理气泡的定位,气泡需要进行绝对定位,让它脱离文档流,不能在隐藏或偏移的时候还占个坑(需要实现那种浮动的效果)。还有气泡有个小三角,这个三角也是需要额外处理定位的。于是设计了组件的结构如下:
old.png
Tooltip 包着 Button 显示在界面上,设置定位属性,让它可以成为子元素定位的基准元素。然后创建 Prompt,它会相对于 Tooltip 进行定位,Prompt 中的小三角形则相对于 Prompt 进行定位。定位的具体数值则根据元素的尺寸和想放置的位置进行定位。在这个例子中就是实现气泡在按钮下方居中显示, Prompt 偏移数值计算如下:

水平偏移需要考虑 Tooltip 和 Prompt 的宽度,偏移的距离就是两者宽度之差的一半。


move


left=width(Tooltip)/2width(Prompt)/2left = width(Tooltip) / 2 - width(Prompt) / 2

垂直偏移则要考虑 Tooltip、Prompt 和 小三角的高度,计算方法类似。(偷懒不做动图了)

top=height(Tooltip)+hegiht(::before_border)+height(gap)top = height(Tooltip) + hegiht(::before\_border) + height(gap)

小三角相对于气泡的偏移也是类似的计算方法,总之能够根据元素的尺寸让偏移刚好能够居中。


代码


代码如下:(wxml 和 wxss 没有高亮,用 html 和 css 格式代替了)


<view class="tooltip" >
<button size="mini" bindtap="handleTooltipShow">TOOLTIP</button>
<view wx:if="{{showTooltip}}" class="prompt-container">
<view class="prompt-title">这是弹窗标题</view>
<view class="prompt-content">这是弹窗内容,啊吧啊吧吧</view>
</view>
</view>

.tooltip{
display: flex;
align-items: center;
position:relative;
width:350rpx;
height: 64rpx;
}
.prompt-container{
background-color: rgba(0,0,0,0.7);
border-radius: 16rpx;
color: #fff;
width: 200rpx;

position:absolute;
padding: 20rpx;
top: 80rpx;
left: 55rpx;
}
.prompt-container::before{
position:absolute;
content:'';
width: 0rpx;
height: 0rpx;
border: 14rpx solid transparent;
border-bottom-color:rgba(0,0,0,0.7);
top: -28rpx;
left: 103rpx;
}
.prompt-title{
font-size: 26rpx;
font-weight: bold;
}
.prompt-content{
font-size: 24rpx;
}

踩坑


美滋滋呀,这就做完了,在 iPhone 12 mini 模拟器上看起来丝毫没有问题,整个气泡内容看起来是那么完美~


12mini


换个最豪(ang)华(gui)的机型(iPhone 14 Pro Max)看看,大事不妙,怎么小三角和气泡之间出现了一条缝,再看看代码按按计算器,算的尺寸没有任何问题呀!但是展示就是变成了这样:


14pm


爬坑


打开调试器一顿猛调试,发现了一些不对劲,下面慢慢说。


关于 rpx


微信小程序提供了个特殊的单位 rpx,代码中也都是使用这个单位进行开发,据说是能够方便开发者在不同尺寸设备上实现自适应布局。
放一张官方文档截图:
wxwd.png
它的意思就是,它把所有的屏幕宽度都设置为 750rpx,不管这个设备真实的宽度有几个设备独立像素(就是宽度有多少 px)。开发者只需要使用 rpx 为单位,小程序会帮你把 rpx 转成 px,听起来是不是很方便很友好~(但并不是🌚)


试试这个公式是不是真的


根据图中提供的转换方式 1rpx=(screenWidth/750)×px1rpx = (screenWidth / 750) \times px
用上面那个例子中的 Tooltip 组件来进行验证,手动算一下在设备上得到的 px 值是不是真的能用上面的公式计算出来。


设备屏幕尺寸 / 750组件理论尺寸真实尺寸
iPhone 12 mini375 / 750 = 0.5Tooltip175 × 32175 × 32
iPhone 14 Pro Max428 / 750 = 0.57Tooltip199.5 × 36.48199× 36

看起来真实的计算会直接省略小数点后的值,直接进行取整。这可能是导致我们的预期和真实展示有偏差的原因。再看看其他组件的尺寸计算:


设备屏幕尺寸 / 750组件理论尺寸真实尺寸
iPhone 12 mini375 / 750 = 0.5Prompt120120
::before14 × 1414 × 14
iPhone 14 Pro Max428 / 750 = 0.57Prompt136.8136
::before15.96 × 15.9614 × 14

因为高度是根据文字自适应的,所以这里没有计算 Prompt 的高度。但依然可以从表中看出,rpx 到 px 的转换并不是简单的直接取整,不然 iPhone 14 Pro Max 中的 ::before 应该尺寸为 15 × 15。至于到底是所有 rpx 到 px 转换都有隐藏的规则,还是伪元素的尺寸转换和其他元素不统一,还是 border 尺寸计算比较特殊,我们也无从得知,官方也没有相关说明。


小三角相对于气泡的偏移


其实在这个例子中,我们最关心的就是这个小三角相对于气泡的偏移是不是符合预期,整体气泡居中与否那么小的差别我们几乎看不出来,但是这个小三角偏离气泡这段距离,搁谁都无法接受。
那着重看下这个小三角的偏移我们是怎么做的,小三角的尺寸完全是由边构成的,完整的矩形尺寸是 28rpx×28rpx28rpx × 28rpx,我们向上的偏移需要设置成整个矩形的尺寸,也就是 top:28rpxtop: -28rpx,这样才能让下半部分的小三角完全展示出来。理论上来说,尺寸和偏移都设置 rpx 为单位,如果使用统一的转换规则,那肯定也是没问题的,既然出现了问题肯定是两者的计算不是那么的统一。我们看到实际的结果,尺寸计算是不符合我们的预期的,那么就猜测偏移可能是按照公式计算的。可以来验证一下,计算得到的值 1515 和真实值 1414 相差 1px1px,我打算放个高度为 1px1px 的长条在这个缝隙里,看看是不是刚好塞进去。
test.png

竟然真的刚好塞进去了,这说明我的猜测应该没有错,偏移的计算在我们预期中,小三角向上移动了 15px。但不能进行更多的验证了,再猜我就要把这个规律猜出来了(手动狗头🤪)。
总之就是,盒子尺寸的计算和偏移距离的计算用的不是一个规律,这就是坑之所在。


解决方案


针对当前需求


我们可以避开这个坑,让小三角相对于气泡不要产生偏移,而是能死死的贴在气泡上。想要达到这样的效果,我们需要修改一下布局结构,改成下面这个样子:


new.png
让气泡整体和小三角形成兄弟关系,那他俩就不会分离了,然后整体的偏移让他们的父节点 Prompt 来决定。


代码如下:


<view class="tooltip" >
<button size="mini" bindtap="handleTooltipShow">TOOLTIP</button>
<view wx:if="{{showTooltip}}" class="prompt-container">
<view class="prompt-triangle"></view>
<view class="prompt-text-container">
<view class="prompt-title">这是弹窗标题</view>
<view class="prompt-content">这是弹窗内容,啊吧啊吧吧</view>
</view>
</view>
</view>

.tooltip{
display: flex;
align-items: center;
position:relative;
width:350rpx;
height: 64rpx;
}
.prompt-container{
position:absolute;
top: 80rpx;
left: 55rpx;
}
.prompt-triangle{
position:relative;
content:'';
width: 0rpx;
height: 0rpx;
border: 14rpx solid transparent;
border-bottom-color:rgba(0,0,0,0.7);
left: 103rpx;
}
.prompt-text-container{
background-color: rgba(0,0,0,0.7);
border-radius: 16rpx;
color: #fff;
width: 200rpx;
padding: 20rpx;
}
.prompt-title{
font-size: 26rpx;
font-weight: bold;
}
.prompt-content{
font-size: 24rpx;
}


通用方法


除了上面避坑的方法,还有一个方法就是进行一个填坑。自定义实现一个 rpx2px 方法,动态的根据设备来进行 px 值的计算,再通过内联样式传递给元素。


function rpx2px(rpx){
return ( wx.getSystemInfoSync().windowWidth / 750) * rpx
}
Page({
data: {
top: rpx2px(100), // 类似这样定义一个状态,通过内联样式传入
},
})

<view class="tooltip" >
<button size="mini" bindtap="handleTooltipShow">TOOLTIP</button>
<view wx:if="{{showTooltip}}" class="prompt-container">
<view class="prompt-triangle" style="top:{{top}}px"></view>
<!-- 注意上面👆这里添加了 style 内联样式 -->
<view class="prompt-text-container">
<view class="prompt-title">这是弹窗标题</view>
<view class="prompt-content">这是弹窗内容,啊吧啊吧吧</view>
</view>
</view>
</view>

这样子不管是什么尺寸什么偏移都按照统一的规则进行换算,妈妈再也不同担心我被坑啦~~~


总结


虽然解法看起来如此简单,但是爬坑的过程真是无比艰难,各种猜测和假设,虽然一些得到了验证,但最终也是无法猜透小程序的心~~ 只能自己避坑和填坑,按需选择吧。


作者:用户9787521254131
来源:juejin.cn/post/7257516901843763257
收起阅读 »

Moka Ascend 2024|势在·人为,技术创新,激发企业管理内在效能

2024年6月19日,Moka 在深圳举办的 Moka Ascend 2024 产品发布会圆满落幕。此次大会以「势在·人为」为主题,汇聚了来自不同企业的管理者、人力资源专家和技术创新者,共同探讨和分享他们在人力资源管理、组织效能提升以及全球化招聘等方面的最新实...
继续阅读 »

2024年6月19日,Moka 在深圳举办的 Moka Ascend 2024 产品发布会圆满落幕。此次大会以「势在·人为」为主题,汇聚了来自不同企业的管理者、人力资源专家和技术创新者,共同探讨和分享他们在人力资源管理、组织效能提升以及全球化招聘等方面的最新实践和深刻见解。

Moka 联合创始人兼 CEO 李国兴在题为「势在必行:激发管理效率的内在势能」的主题演讲中,正式发布「Moka 人效管理解决方案」,以回应当前企业对经营和管理的诉求。要看清楚「人效」,管理者不应该只是得到事后汇报和对补救过程做出决策,而是真的需要一个内嵌于HR业务中,涵盖企业人力资源管理全流程,具备人力资源全面效能的工具。「Moka 人效管理解决方案」从成本到人效,从规划到落地,让决策更有依据,让管理更有保障,为企业健康发展[ 控本增效 ],帮助企业更清晰而全面地看到人力资源效能的运作情况,也让过程中的管理价值不断重复循环起来,「看得清」才能更好的「管得住」。

今年1月初,Moka 携手钉钉,推出「钉钉人事旗舰版」,致力于解决中大型企业在 HR 软件的使用过程中面临的系统割裂、数据孤岛等问题。此产品一经推出,对于整个行业来说,都是让人眼前一亮的「HR SaaS 新物种」。

钉钉生态业务部总经理陈霆旭与李国兴一起就合作后的进展和成绩进行了分享。在阐述钉钉开放生态战略合作的构想时,陈霆旭提到,在人事场景方面,Moka 是理想的合作伙伴,我们双方通过深度融合的方式,创新性地满足了客户需求。「钉钉人事旗舰版」之所以能被市场快速验证和接纳,本质还为了更好满足客户价值,大家在价值链上找到了更精细的分工和更清晰的边界,干自己更擅长的事,钉钉有非常丰富的IM、OA、文档、音视频等协同办公能力,也让企业的组织数字化实现零门槛,Moka在人事领域也有非常深的积淀,有了这种强强融合共建的产品能给员工和HR带来前所未有的创新体验,为客户带来「1+1>2」的价值和效果。

除了「看得清」,「看得远」成为企业不断进步与更新的动力。新一代大模型时代到来,AI 技术越来越多的可能性呈现在大众面前。Moka 合伙人兼 CTO 刘洪泽在大会上分享了 Moka 在 AI 领域的技术创新和应用实践。以 AI 原生为理念,深入业务场景,Moka Eva 的能力不断进化,不仅完成了从智能面试到智能招聘解决方案的升级,同时也赋能于全新功能「SmartPractice」,集成多国家和地区招聘最佳实践,智能辅助HR全球招聘,全流程提升招聘效率与招聘质量。

同时,AI 亦全面赋能 Moka 技术平台,构建新一代底层平台能力。伴随着新加坡数据中心的投入使用,以及一直以来与合作伙伴的生态共融,Moka 将与更多客户实现合作共赢,不断向打造世界级HR产品的愿景靠近。

企业成长是个超长周期的动态过程,过程中企业会面临市场内卷、业务难管等挑战,针对企业发展难题,各行业有成功实践经验的管理者提出新解法。

本次大会亦邀请了多位企业管理者分享了他们在人效管理、本地化人才战略和全球化招聘方面的深刻见解和实践经验。来自卓尔数科的CHO屠俊强调了在不确定性环境下,"1+3"模型发挥着重要作用,在应用SOTE模型的同时,构建组织力、业务力和影响力,实现业务与组织管理的深度融合。KLOOK 客路旅行的中国区人力资源总监戴良辰则分享了如何通过业务共创和团队成长两大维度,落地本地招聘,推动业务快速发展。

此外,本次大会还举办了题为”无界之帆:全球化招聘的挑战和实践“的圆桌论坛,由李国兴主持,邀请了携程集团招聘运营负责人梁昊耘、AfterShip Global HRD Flora Zeng和海辰储能招聘总监金一凡,共同探讨海外团队组建的挑战与经验,如何在全球范围内构建雇主品牌,以及如何利用系统和工具优化招聘流程。这些分享不仅展现了中国企业在全球化浪潮中的积极作为,也为其他企业提供了宝贵的参考和启示。

势在人为,破局而出。Moka 未来仍会继续坚持「全员体验更好」的产品理念,响应客户需求,持续优化产品与服务,助力企业实现更高效、更智能的人事管理。同时,Moka 也将积极与各方共同探索人事管理新机遇,在动态的变化中寻求可期待的攀升,保持始终向上的能量。

收起阅读 »

我是如何解决职场内卷、不稳定、没前景的

大家好,我卡颂。 我的读者大部分是职场人,在经济下行期,大家普遍反映混职场艰难。 再深究下,发现造成职场艰难的原因主要有三个: 内卷:狼多肉少 不稳定:裁员总是不期而遇 没前景:明知过几年会被优化,但无法改变 本文根据我的个人经历以及大量案例走访,得出一套...
继续阅读 »

大家好,我卡颂。


我的读者大部分是职场人,在经济下行期,大家普遍反映混职场艰难。


再深究下,发现造成职场艰难的原因主要有三个:



  1. 内卷:狼多肉少

  2. 不稳定:裁员总是不期而遇

  3. 没前景:明知过几年会被优化,但无法改变


本文根据我的个人经历以及大量案例走访,得出一套切实可行的不内卷、高稳定、有前景的职业发展路径。


推荐职场发展遇到卡点的同学阅读。


造成三个问题的原因


要知道问题的解法,首先得了解问题成因。


造成内卷的原因


传统职业发展路径中的目标通常需要竞争,比如:



  • 升职加薪的名额有限

  • 跳槽时目标公司的 HC 有限

  • 在职学历提升的录取人数有限


更有甚者,在行业下行期,一些负面的名额也需要竞争(让自己不被选中),比如:



  • 绩效打C

  • 裁员的名额


问题的本质并不在个人身上,而在依附于公司的职业发展路径上。


我们从小接受的普鲁士教育的目的,是通过考试不断筛选守规则且有专业技能的公民


职场可以理解为这种筛选模式的延续,所以面对职场内卷时,大部分人不会认为这是模式的问题,而是从自身找原因(我是不是还不够努力?)


所以,内卷的本质原因是 —— 传统职业发展路径在有限的资源下充满竞争,有竞争就有加剧内卷。


造成“不稳定”的原因


试问,什么样的工作才是稳定的工作?很多同学会脱口而出:国企、考编。


这些传统意义的铁饭碗稳定的原因是 —— 他们依附于一个稳定的平台。


而造成打工人职业发展不稳定的原因正是如此 —— 他们依附于某个公司。


有人会说:如果公司不行了我可以跳槽啊。


问题是,你跳槽的目的地不也是一家公司嘛。再发散点讲,你职业生涯的每一步都依附于公司。



所以,不稳定的本质原因是 —— 传统职业发展路径都依附于公司存在。在经济下行期,公司这个载体并不稳定。


造成“没前景”的原因


前段时间,我和一个金融业的前辈聊天,他在5年前就做到行长的level,政治斗争失败后离职,现在作为普通职员,在一家银行做执行层面的工作。


见多了这样的例子,不禁让我思考 —— 什么样的工作才是有前景的工作?


我的结论是 —— 在不内卷且稳定的基础上,有复利的工作。


可以做一辈子,且做的越久越吃香的工作老中医就是这样的一份工作:



  • 不内卷:患者是冲着他来的,即使市面上有很多其他中医,也不会对他造成影响

  • 稳定:不会失业

  • 有复利:随着年龄增长,会更有竞争力


抛开现象看本质,老中医这类工作的核心模式是 —— 以专家影响力为基础形成的圈层生意。


剖析“老中医模式”


老中医模式为什么能做到不内卷、高稳定、有前景?让我们一条条分析。


如何做到“不内卷”?


上文提到,内卷源于有限资源下的竞争。所以,不内卷的核心是创造新价值,而不是占有已有资源


新价值可以在“没有人被剥夺价值”的情况下被创造,就像老中医在治好一个病人时,不会有另一个病人为此蒙受损失。


如何做到“高稳定”?


传统职业生涯的不稳定性来源于依附于公司,而老中医并不依附于医院(即使在医院坐诊,也是医院沾了老中医的光)。


那么,老中医依附于什么呢?答案是 —— 中医行业。


中国人几千年来对中医形成的认可,使得依附于行业的中医会获得极大的稳定性,你体会下两者的区别:



  • 依附于公司:我是xx医院的中医

  • 依附于行业:我是xx领域名中医


背后的本质原因是 —— 依附于公司,你的收益完全来源于公司(xx医院的中医,患者是冲着医院来的)。


而依附于行业,你的收益则完全来自行业受众(xx领域名中医,患者是冲着他的领域专业度来的)。


把鸡蛋从公司这个篮子拿出来,放到千千万万从业者的篮子里,这就是稳定性的由来。


如何做到“有前景”?


在不内卷、高稳定性的基础上,随着时间、阅历增长,一名中医的含金量会指数增长。


一位40岁的中医和一位80岁的中医,在老百姓的认知中,后者显然比前者可信赖度高了不止一倍。因为老百姓潜意识里会认为 —— 年龄越大,知识、经验越丰富。


这就是知识的复利


WIN事业工作法


根据老中医模式的特点,我们可以总结一条全新的职业发展路径:



  1. 智慧(Wisdom):以“领域专家”为目标,构建领域智慧

  2. 影响力(Influence):围绕领域智慧打造行业影响力

  3. 圈层(Network):运用影响力吸引关注者,形成圈层


这就是W.I.N事业工作法,一种不内卷、高稳定、有前景的职业发展路径。


在这条新职业路径中:



  1. 没有占有已有资源,而是通过构建领域智慧创造新的价值

  2. 没有依附于公司,而是依附于行业,成为领域专家

  3. 没有将鸡蛋放在公司的篮子里,而是将鸡蛋放在圈层受众手中


总结


传统职业发展路径三大问题的主要成因是:



  1. 内卷:在有限的资源下充满竞争,有竞争就有加剧内卷

  2. 不稳定:依附于公司,公司在行业下行期不稳定

  3. 没前景:内卷且不稳定,还没有复利效应


为了克服这三点,一种可行的方式是“借鉴老中医的职业发展路径”。


这种新职业发展路径被称为W.I.N事业工作法,包括三个步骤:



  1. 智慧(Wisdom):以“领域专家”为目标,构建领域智慧

  2. 影响力(Influence):围绕领域智慧打造行业影响力

  3. 圈层(Network):运用影响力吸引关注者,形成圈层


关于工作法更详细的内容已经开源为免费的电子书《W.I.N事业工作法》,推荐对职业发展有卡点的同学阅读。


作者:魔术师卡颂
来源:juejin.cn/post/7376586649981780005
收起阅读 »

写html页面没意思,来挑战chrome插件开发

web
谷歌浏览器插件开发是指开发可以在谷歌浏览器中运行的扩展程序,可以为用户提供额外的功能和定制化的体验。谷歌浏览器插件通常由HTML、CSS和JavaScript组成,非常利于前端开发者。 开发者可以利用这些技术在浏览器中添加新的功能、修改现有功能或者与网页进行交...
继续阅读 »

谷歌浏览器插件开发是指开发可以在谷歌浏览器中运行的扩展程序,可以为用户提供额外的功能和定制化的体验。谷歌浏览器插件通常由HTML、CSS和JavaScript组成,非常利于前端开发者。
开发者可以利用这些技术在浏览器中添加新的功能、修改现有功能或者与网页进行交互。


要开发谷歌浏览器插件,开发者通常需要创建一个包含*清单文件(manifest.json)、背景脚本(background script)、内容脚本(content script)*等文件的项目结构。清单文件是插件的配置文件,包含插件的名称、版本、描述、权限以及其他相关信息。背景脚本用于处理插件的后台逻辑,而内容脚本则用于在网页中执行JavaScript代码。


谷歌浏览器插件可以实现各种功能,例如添加新的工具栏按钮、修改网页内容、捕获用户输入、与后台服务器进行通信等。开发者可以通过谷歌浏览器插件API来访问浏览器的各种功能和数据,实现各种定制化的需求。
插件开发涉及的要点:


image.png


基础配置


开发谷歌浏览器插件,最重要的文件 manifest.json


{
"name": "Getting Started Example", // 插件名称
"description": "Build an Extension!", // 插件描述
"version": "1.0", // 版本
"manifest_version": 3, // 指定插件版本,这个很重要,指定什么版本就用什么样的api,不能用错了
"background": {
"service_worker": "background.js" // 指定background脚本的路径
},
"action": {
"default_popup": "popup.html", // 指定popup的路径
"default_icon": { // 指定popup的图标,不同尺寸
"16": "/images/icon16.png",
"32": "/images/icon32.png",
"48": "/images/icon48.png",
"128": "/images/icon128.png"
}
},
"icons": { // 指定插件的图标,不同尺寸
"16": "/images/icon16.png",
"32": "/images/icon32.png",
"48": "/images/icon48.png",
"128": "/images/icon128.png"
},
"permissions": [],// 指定应该在脚本中注入那些变量方法,后文再详细说
"options_page": "options.html",
"content_scripts": [ // 指定content脚本配置
{
"js": [ "content.js"], // content脚本路径
"css":[ "content.css" ],// content的css
"matches": ["<all_urls>"] // 对匹配到的tab起作用。all_urls就是全部都起作用
}
]
}


  • name: 插件名称


manifest_version:对应chrome API插件版本,浏览器插件采用的版本,目前共2种版本,是2和最新版3



  • version: 本插件的版本,和发布相关

  • action:点击图标时,设置一些交互

    • default_icon:展示图标

      • 16、32、48、128



    • default_popup:popup.html,一个弹窗页面

    • default_title:显示的标题



  • permissions:拥有的权限

    • tabs:监听浏览器tab切换事件



  • options_ui

  • background:

    • service_worker:设置打开独立页面




官方实例


官方教程


打开pop弹窗页面


设置action的default_popup属性


{
"name": "Hello world",
"description": "show 'hello world'!",
"version": "1.0",
"manifest_version": 3,
"action": {
"default_popup": "popup.html",
"default_icon": {
"16": "/images/icon16.png",
"32": "/images/icon32.png",
"48": "/images/icon48.png",
"128": "/images/icon128.png"
}
},
"permissions":["tabs", "storage", "activeTab", "idle"],
"background": {
"service_worker": "background.js"
},
"content_scripts": [
{
"js": [ "content.js"],
"css":[ "content.css" ],
"matches": ["<all_urls>"]
}
]
}

创建popup.html


<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>显示出hello world</title>
<link rel="stylesheet" type="text/css" href="popup.css">
</head>

<body>
<h1>显示出hello world</h1>
<button id="clickBtn">点击按钮</button>
<script src="popup.js"></script>
</body>
</html>

文件可以通过链接引入css、js。


body {
width: 600px;
height: 300px;
}
h1 {
background-color: antiquewhite;
font-weight: 100;
}


console.log(document.getElementById('clickBtn'));
document.getElementById('clickBtn').addEventListener('click', function () {
console.log('clicked');
});

点击插件图标


点击图标可以看到如下的popup的页面。


image.png


调试popup.js的方法



  • 通过弹窗,在弹窗内部点击右键,选择审查内容
    image.png

  • 通过插件图标,进行点击鼠标右键,选择审查弹出内容
    image.png


通过background打开独立页面


基于backgroundservice_workerAPI可以打开一个独立后台运行脚本。此脚本会随着插件安装,初始化执行一次,然后一直在后台运行。可以用来存储浏览器的全局状态数据。
background脚本是长时间运行在后台,随着浏览器打开就运行,直到浏览器关闭而结束运行。通常把需要一直运行的、启动就运行的、全局公用的数据放到background脚本。


chrome.action.onClicked.addListener(function () {
chrome.tabs.create({
url: chrome.runtime.getURL('newPage.html')
});
});


为了打开独立页面,需要修改manifest.json


{
"name": "newPage",
"description": "Demonstrates the chrome.tabs API and the chrome.windows API by providing a user interface to manage tabs and windows.",
"version": "0.1",
"permissions": ["tabs"],
"background": {
"service_worker": "service-worker.js"
},
"action": {
"default_title": "Show tab inspector"
},
"manifest_version": 3
}

为了实现打开独立页面,在manifest.json中就不能在配置 action:default_popup
newPage.js文件中可以使用*chrome.tabs*和chrome.windowsAPI;
可以使用 chrome.runtime.getUrl 跳转一个页面。


chrome.runtime.onInstalled.addListener(async () => {
chrome.tabs.create(
{
url: chrome.runtime.getURL('newPage.html'),
}
);
});

content内容脚本


content-scripts(内容脚本)是在网页上下文中运行的文件。通过使用标准的文档对象模型(DOM),它能够读取浏览器访问的网页的详细信息,可以对打开的页面进行更改,还可以将DOM信息传递给其父级插件。内容脚本相对于background还是有一些访问API上的限制,它可以直接访问以下chrome的API



  • i18n

  • storage

  • runtime:

    • connect

    • getManifest

    • getURL

    • id

    • onConnect

    • onMessage

    • sendMessage




content.js运行于一个独立、隔离的环境,它不会和主页面的脚本或者其他插件的内容脚本发生冲突
有2种方式添加content脚本


在配置中设置


"content_scripts": [
{
"js": [ "content.js"],
"css":[ "content.css" ],
"matches": ["<all_urls>"]
}
]

content_scripts属性除了配置js,还可以设置css样式,来实现修改页面的样式。
matches表示需要匹配的页面;
除了这3个属性,还有



  • run_at: 脚本运行时刻,有以下3个选项

    • document_idle,默认;浏览器会选择一个合适的时间注入,并是在dom完成加载

    • document_start;css加载完成,dom和脚本加载之前注入。

    • document_end:dom加载完成之后



  • exclude_matches:排除匹配到的url地址。作用和matches相反。


动态配置注入


在特定时刻才进行注入,比如点击了某个按钮,或者指定的时刻
需要在popup.jsbackground.js中执行注入的代码。


chrome.tabs.executeScript(tabs[0].id, {
code: 'document.body.style.backgroundColor = "red";',
});

也可以将整个content.js进行注入


chrome.tabs.executeScript(tabs[0].id, {
file: "content.js",
});

利用content制作一个弹窗工具


某天不小心让你的女神生气了,为了能够道歉争取到原谅,你是否可以写一个道歉信贴到每一个页面上,当女神打开网站,看到每个页面都会有道歉内容。


image.png


道歉信内容自己写哈,这个具体看你的诚意。
下面设置2个按钮,原谅和不原谅。 点击原谅,就可以关闭弹窗。 点击不原谅,这个弹窗调整css布局位置继续显示。(有点像恶意贴片广告了)


下面设置content.js的内容


let newDiv = document.createElement('div');
newDiv.innerHTML = `<div id="wrapper">
<h3>小仙女~消消气</h3>
<div><button id="cancel">已消气</button>
<button id="reject">不原谅</button></div>
</div>`
;
newDiv.id = 'newDiv';
document.body.appendChild(newDiv);
const cancelBtn = document.querySelector('#cancel');
const rejectBtn = document.querySelector('#reject');
cancelBtn.onclick = function() {
document.body.removeChild(newDiv);
chrome.storage.sync.set({ state: 'cancel' }, (data) => {
});
}
rejectBtn.onclick = function() {
newDiv.style.bottom = Math.random() * 200 + 10 + "px";
newDiv.style.right = Math.random() * 800 + 10 + "px";
}
// chrome.storage.sync.get({ state: '' }, (data) => {
// if (data.state === 'cancel') {
// document.body.removeChild(newDiv);
// }
// });

content.css布局样式


#newDiv {
font-size: 36px;
color: burlywood;
position: fixed;
bottom: 20px;
right: 0;
width: 300px;
height: 200px;
background-color: rgb(237, 229, 216);
text-align: center;
z-index: 9999;
}

打开option页面


options页,就是插件的设置页面,有2个入口



  • 1:点击插件详情,找到扩展程序选项入口


image.png



  • 2插件图标,点击右键,选择 ‘选项’ 菜单


image.png


可以看到设置的option.html页面


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>插件的option配置</title>
</head>
<body>
<h3>插件的option配置</h3>
</body>
</html>

此页面也可以进行js、css的引入。


替换浏览器默认页面


override功能,是可以替换掉浏览器默认功能的页面,可以替换newtab、history、bookmark三个功能,将新开页面、历史记录页面、书签页面设置为自定义的内容。
修改manifest.json配置


{
"chrome_url_overrides": {
"newtab": "newtab.html",
"history": "history.html",
"bookmarks": "bookmarks.html"
}
}

创建一个newtab的html页面


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h1>new tab</h1>
</body>
</html>

插件更新后,点开新的tab,就会出现我们自定义的页面。第一次的情况会让用户进行选择,是进行更换还是保留原来的配置。


image.png
很多插件都是使用newtab进行自定义打开的tab页,比如掘金的浏览器插件,打开新页面就是掘金网站插件


页面之间进行数据通信


image.png
如需将单条消息发送到扩展程序的其他部分并选择性地接收响应,请调用 runtime.sendMessage()tabs.sendMessage()。通过这些方法,您可以从内容脚本向扩展程序发送一次性 JSON 可序列化消息,或者从扩展程序向内容脚本发送。如需处理响应,请使用返回的 promise。
来源地址:developer.chrome.com/docs/extens…


content中脚本发送消息


chrome.runtime.sendMessage只能放在content的脚本中。


(async () => {
const response = await chrome.runtime.sendMessage({greeting: "hello"});
// do something with response here, not outside the function
console.log(response);
})();

其他页面发送消息


其他页面需向内容脚本发送请求,请指定请求应用于哪个标签页,如下所示。此示例适用于 Service Worker、弹出式窗口和作为标签页打开的 chrome-extension:// 页面


(async () => {
const [tab] = await chrome.tabs.query({active: true, lastFocusedWindow: true});
const response = await chrome.tabs.sendMessage(tab.id, {greeting: "hello"});
// do something with response here, not outside the function
console.log(response);
})();

接收消息使用onMessage


在扩展程序和内容脚本中使用相同的代码


chrome.runtime.onMessage.addListener(
function(request, sender, sendResponse) {
console.log(sender.tab ?
"from a content script:" + sender.tab.url :
"from the extension");
if (request.greeting === "hello")
sendResponse({farewell: "goodbye"});
}
);

添加右键菜单


创建菜单


首先在manifest.json的权限中添加配置


{
"permissions": ["contextMenus"]
}


background.js中添加创建菜单的代码


let menu1 = chrome.contextMenus.create({
type: 'radio', // 可以是 【normal、checkbox、radio】,默认是normal
title: 'click me',
id: "myMenu1Id",
contexts:['image'] // 只有是图片时,菜显示
}, function(){

})

let menu2 = chrome.contextMenus.create({
type: 'normal', // 可以是 【normal、checkbox、radio】,默认是normal
title: 'click me222',
id: "myMenu222Id",
contexts:['all'] //所有类型都显示
}, function(){

})

let menu3 = chrome.contextMenus.create({
id: 'baidusearch1',
title: '使用百度搜索:%s',
contexts: ['selection'], //选择页面上的文字
});

// 删除一个菜单
chrome.contextMenus.remove('myMenu222Id'); // 被删除菜单的id menuItemId
// 删除所有菜单
chrome.contextMenus.removeAll();

// 绑定菜单点击事件
chrome.contextMenus.onClicked.addListener(function(info, tab){
if(info.menuItemId == 'myMenu222Id'){
console.log('xxx')
}
})

以下是其他可以使用的api


// 删除某一个菜单项
chrome.contextMenus.remove(menuItemId);
// 删除所有自定义右键菜单
chrome.contextMenus.removeAll();
// 更新某一个菜单项
chrome.contextMenus.update(menuItemId, updateProperties);
// 监听菜单项点击事件, 这里使用的是 onClicked
chrome.contextMenus.onClicked.addListener(function(info, tab)) {
//...
});

绑定点击事件,发送接口请求


首先需要在manifest.jsonhosts_permissions中添加配置


{
"host_permissions": ["http://*/*", "https://*/*"]
}

创建node服务器,返回json数据


// server.mjs
const { createServer } = require('node:http');
const url = require('url');

const server = createServer((req, res) => {
var pathname = url.parse(req.url).pathname;

if (pathname.includes('api')) {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.write(
JSON.stringify({
name: 'John Doe',
age: 30,
})
);
res.end();
} else {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Hello World!\n' + pathname);
}
});

server.listen(8080, '127.0.0.1', () => {
console.log('Listening on 127.0.0.1:8080');
});

编辑background.js文件


// 插件右键快捷键
// 点击右键进行选择
chrome.contextMenus.onClicked.addListener(function (info, tab) {
if (info.menuItemId === 'group1') {
console.log('分组文字1', info);
}
if (info.menuItemId === 'group2') {
console.log('分组文字2');
}
// 点击获取到数据
if (info.menuItemId === 'fetch') {
console.log('fetch 获取数据');
const res = fetch('http://localhost:8080/api', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
}).then((res) => {
console.log(res, '获取到http://localhost:8080/api接口数据');
chrome.storage.sync.set({ color: 'red' }, function (err, data) {
console.log('store success!');
});
});
}
// 创建百度搜索,并跳转到搜索结果页
if (info.menuItemId === 'baidusearch1') {
// console.log(info, tab, "baidusearch1")
// 创建一个新的tab页面
chrome.tabs.create({
url:
'https://www.baidu.com/s?ie=utf-8&wd=' + encodeURI(info.selectionText),
});
}
});

// 创建右键快捷键
chrome.runtime.onInstalled.addListener(function () {
// Create one test item for each context type.
let contexts = [
'page',
'selection',
'link',
'editable',
'image',
'video',
'audio',
];
// for (let i = 0; i < contexts.length; i++) {
// let context = contexts[i];
// let title = "Test '" + context + "' menu item";
// chrome.contextMenus.create({
// title: title,
// contexts: [context],
// id: context,
// });
// }

// Create a parent item and two children.
let parent = chrome.contextMenus.create({
title: '操作数据分组',
id: 'parent',
});
chrome.contextMenus.create({
title: '分组1',
parentId: parent,
id: 'group1',
});
chrome.contextMenus.create({
title: '分组2',
parentId: parent,
id: 'group2',
});
chrome.contextMenus.create({
title: '获取远程数据',
parentId: parent,
id: 'fetch',
});

// Create a radio item.
chrome.contextMenus.create({
title: '创建单选按钮1',
type: 'radio',
id: 'radio1',
});
chrome.contextMenus.create({
title: '创建单选按钮2',
type: 'radio',
id: 'radio2',
});

// Create a checkbox item.
chrome.contextMenus.create({
title: '可以多选的复选框1',
type: 'checkbox',
id: 'checkbox',
});
chrome.contextMenus.create({
title: '可以多选的复选框2',
type: 'checkbox',
id: 'checkbox2',
});

// 在title属性中有一个%s的标识符,当contexts为selection,使用%s来表示选中的文字
chrome.contextMenus.create({
id: 'baidusearch1',
title: '使用百度搜索:%s',
contexts: ['selection'],
});

// Intentionally create an invalid item, to show off error checking in the
// create callback.
chrome.contextMenus.create(
{ title: 'Oops', parentId: 999, id: 'errorItem' },
function () {
if (chrome.runtime.lastError) {
console.log('Got expected error: ' + chrome.runtime.lastError.message);
}
}
);
});

点击鼠标右键,效果如下


image.png


image.png


如果在页面选择几个文字,那么就显示出百度搜索快捷键,


image.png


缓存,数据存储


首先在manifest.json的权限中添加storage配置


{
"permissions": ["storage"]
}

chrome.storage.sync.set({color: 'red'}, function(){
console.log('background js storage set data ok!')
})

然后就可以在content.js或popup.js中获取到数据


// 这里的参数是,获取不到数据时的默认参数
chrome.storage.sync.get({color: 'yellow'}, function(){
console.log('background js storage set data ok!')
})

tabs创建页签


首先在manifest.json的权限中添加tabs配置


{
"permissions": ["tabs"]
}

添加tabs的相关操作


chrome.tabs.query({}, function(tabs){
console.log(tabs)
})
function getCurrentTab(){
let [tab] = chrome.tabs.query({active: true, lastFocusedWindow: true});
return tab;
}

notifications消息通知


Chrome提供chrome.notifications的API来推送桌面通知;首先在manifest.json中配置权限


{
"permissions": [
"notifications"
],
}

然后在background.js脚本中进行创建


// background.js
chrome.notifications.create(null, {
type: "basic",
iconUrl: "drink.png",
title: "喝水小助手",
message: "看到此消息的人可以和我一起来喝一杯水",
});


devtools开发扩展工具


在manifest中配置一个devtools.html


{
"devtools_page": "devtools.html",
}

devtools.html中只引用了devtools.js,如果写了其他内容也不会展示


<!DOCTYPE html>
<html lang="en">
<head> </head>
<body>
<script type="text/javascript" src="./devtools.js"></script>
</body>
</html>

创建devtools.js文件


// devtools.js
// 创建扩展面板
chrome.devtools.panels.create(
// 扩展面板显示名称
"DevPanel",
// 扩展面板icon,并不展示
"panel.png",
// 扩展面板页面
"Panel.html",
function (panel) {
console.log("自定义面板创建成功!");
}
);

// 创建自定义侧边栏
chrome.devtools.panels.elements.createSidebarPane(
"Sidebar",
function (sidebar) {
sidebar.setPage("sidebar.html");
}
);

然后在创建自定的Panel.html和sidebar.html页面。


相关代码下载


作者:北鸟南游
来源:juejin.cn/post/7350571075548397618
收起阅读 »

程序员提高效率的 10 个方法

1. 早上不要开会 📅 每个人一天是 24 小时,时间是均等的,但是时间的价值却不是均等的,早上 1 小时的价值是晚上的 4 倍。为什么这么说? 因为早晨是大脑的黄金时间,经过一晚上的睡眠,大脑经过整理、记录、休息,此时的状态是最饱满的,适合专注度高的工作,比...
继续阅读 »

1. 早上不要开会 📅


每个人一天是 24 小时,时间是均等的,但是时间的价值却不是均等的,早上 1 小时的价值是晚上的 4 倍。为什么这么说?


因为早晨是大脑的黄金时间,经过一晚上的睡眠,大脑经过整理、记录、休息,此时的状态是最饱满的,适合专注度高的工作,比如编程、学习外语等,如果把时间浪费在开会、刷手机等低专注度的事情上,那么就会白白浪费早上的价值。


2. 不要使用番茄钟 🍅


有时候在专心编程的时候,会产生“心流”,心流是一种高度专注的状态,当我们专注的状态被打破的时候,需要 15 分钟的时候才能重新进入状态。


有很多人推荐番茄钟工作法,设定 25 分钟倒计时,强制休息 5 分钟,之后再进入下一个番茄钟。本人在使用实际使用这种方法的时候,经常遇到的问题就是刚刚进入“心流”的专注状态,但番茄钟却响了,打破了专注,再次进入这种专注状态需要花费 15 分钟的时间。


好的替换方法是使用秒表,它跟番茄钟一样,把时间可视化,但却是正向计时,不会打破我们的“心流”,当我们编程专注度下降的时候中去查看秒表,确定自己的休息时间。


3. 休息时间不要玩手机 📱


大脑处理视觉信息需要动用 90% 的机能,并且闪烁的屏幕也会让大脑兴奋,这就是为什么明明休息了,但是重新回到工作的时候却还是感觉很疲惫的原因。


那么对于休息时间内,我们应该阻断视觉信息的输入,推荐:



  • 闭目养神 😪

  • 听音乐 🎶

  • 在办公室走动走动 🏃‍♂️

  • 和同事聊会天 💑

  • 扭扭脖子活动活动 💁‍♂️

  • 冥想 or 正念 🧘


4. 不要在工位上吃午饭 🥣


大脑经过一早上的编程劳累运转之后,此时的专注度已经下降 40%~50%,这个时候我们需要去重启我们的专注度,一个好的方法是外出就餐,外出就餐的好处有:



  • 促进血清素分泌:我们体内有一种叫做血清素的神经递质,它控制着我们的睡眠和清醒,外出就餐可以恢复我们的血清素,让我们整个人神经气爽:

    • 日光浴:外出的时候晒太阳可以促进血清素的分泌

    • 有节奏的运动:走路是一种有节奏的运动,同样可以促进血清素分泌



  • 激发场所神经元活性:场所神经元是掌控场所、空间的神经细胞,它存在于海马体中,外出就餐时场所的变化可以激发场所神经元的活性,进而促进海马体活跃,提高我们的记忆力

  • 激活乙酰胆碱:如果外出就餐去到新的餐馆、街道,尝试新的事物的话,可以激活我们体内的乙酰胆碱,它对于我们的“创作”和“灵感”起到非常大的作用。


5. 睡午觉 😴


现在科学已经研究表现,睡午觉是非常重要的一件事情,它可以:



  • 恢复我们的身体状态:26 分钟的午睡,可以让下午的工作效率提升 34%,专注力提升 54%。

  • 延长寿命:中午不睡午觉的人比中午睡午觉的人更容易扑街

  • 预防疾病:降低老年痴呆、癌症、心血管疾病、肥胖症、糖尿病、抑郁症等


睡午觉好处多多,但也要适当,15 分钟到 30 分钟的睡眠最佳,超过的话反而有害。


6. 下午上班前运动一下 🚴


下午 2 点到 4 点是人清醒度最低的时候,10 分钟的运动可以让我们的身体重新清醒,提高专注度,程序员的工作岗位和场所如果有限,推荐:



  • 1️⃣ 深蹲

  • 2️⃣ 俯卧撑

  • 3️⃣ 胯下击掌

  • 4️⃣ 爬楼梯(不要下楼梯,下楼梯比较伤膝盖,可以向上爬到顶楼,再坐电梯下来)


7. 2 分钟解决和 30 秒决断 🖖


⚒️ 2 分钟解决是指遇到在 2 分钟内可以完成的事情,我们趁热打铁把它完成。这是一个解决拖延的小技巧,作为一个程序员,经常会遇到各种各样的突发问题,对于一些问题,我们没办法很好的决策要不要立即完成, 2 分钟解决就是一个很好的辅助决策的办法。


💣 30 秒决断是指对于日常的事情,我们只需要用 30 秒去做决策就好了,这源于一个“快棋理论”,研究人员让一个著名棋手去观察一盘棋局,然后分别给他 30 秒和 1 小时去决定下一步,最后发现 30 秒和 1 小时做出的决定中,有 90% 都是一致的。


8. 不要加班,充足睡眠 💤


作为程序员,我们可能经常加班到 9 点,到了宿舍就 10 点半,洗漱上床就 12 点了,再玩会儿手机就可以到凌晨 2、3 点。


压缩睡眠时间,大脑就得不到有效的休息,第二天的专注度就会降低,工作效率也会降低,这就是一个恶性循环。


想想我们在白天工作的时候,其实有很多时间都是被无效浪费的,如果我们给自己强制设定下班时间,创新、改变工作方式,高效率、高质量、高密度的完成工作,那是否就可以减少加班,让我们有更多的自由时间去学习新的知识技术,进而又提高我们的工作效率,形成一个正向循环。


9. 睡前 2 小时 🛌



  1. 睡前两小时不能做的事情:

    • 🍲 吃东西:空腹的时候会促进生长激素,生长激素可以提高血糖,消除疲劳,但如果吃东西把血糖提高了,这时候生长激素就停止分泌了

    • 🥃 喝酒

    • ⛹️ 剧烈运动

    • 💦 洗澡水温过高

    • 🎮 视觉娱乐(打游戏,看电影等)

    • 📺 闪亮的东西(看手机,看电脑,看电视)

    • 💡 在灯光过于明亮的地方



  2. 适合做的事情

    • 📖 读书

    • 🎶 听音乐

    • 🎨 非视觉娱乐

    • 🧘‍♂️ 使身体放松的轻微运动




10. 周末不用刻意补觉 🚫


很多人以周为单位进行休息,周一到周五压缩睡眠,周末再补觉,周六日一觉睡到下午 12 点,但这与工作日的睡眠节奏相冲突,造成的后果就是星期一的早上起床感的特别的厌倦、焦躁。


其实周末并不需要补觉,人体有一个以天为单位的生物钟,打破当前的生物钟周期,就会影响到下一个生物钟周期,要调节回来也需要花费一定时间。


我们应该要以天为单位进行休息,早睡早起,保持每天的专注度。


参考


以上大部分来源于书籍 《为什么精英都是时间控》,作者桦泽紫苑,是一个脑神经专家。


作者:吴楷鹏
来源:juejin.cn/post/7253605936144285757
收起阅读 »

我转产品了-前端转产品是一种什么样的体验

程序之路 入门前端的 3 年,前端技术从 pug/handlebars/jquery 制作各种企业官网,再到 gulp/vue/react/webpack 的工程化开发后台管理、 webapp 。然后是 node/express/koa ,开始涉及全栈。 代码...
继续阅读 »

程序之路


入门前端的 3 年,前端技术从 pug/handlebars/jquery 制作各种企业官网,再到 gulp/vue/react/webpack 的工程化开发后台管理、
webapp 。然后是 node/express/koa ,开始涉及全栈。
代码管理工具也从 svn 到 git ,然后制定提交规范,分支管理规范,结合 gitflow/githook 以及各种 lint 保证团队开发风格及可维护
性。
产品发布的方式从 ftp 上传,到 npm/nodejs/shell 脚本,然后再到 jenkins/docker/git 多分支多环境部署。


从第 3 年之后就感觉技术没什么提升了 ,后面都是在各个小作坊担任前端组长角色(其实感觉就是救火队长),哪里项目急去哪里,哪里有难题去哪里。实际比 UI、比测试、比实习产品的地位还低,基本没有话语权。



为什么转产品


严格来说,并不是专门的喜欢产品这个职位,而是希望了解产品经理所做的事。因为在软件开发的工作里,工作的内容和返工程序大大取决与产品对用户需求的理解能力,业务熟悉能力。而作为前端,经常只集中精力在处理页面还原、交互实现、数据对接、浏览器兼容等工作上面。对整个系统的业务逻辑是比较片面的。


如果对用户需求和产品业务有所了解,那可能在开发之前就能发现需求上的不必要性,发现设计上的错误,而减少程序开发的返工率。


总的说来,是期望:



  • 拒绝无效编程

  • 深入理解业务

  • 培养跨部门沟通能力

  • 培养产品设计能力


是否适合转产品


根据上面所说的几点理解,我自身而言并不拒绝,这是在心理方面。


在能力方面,我认为我是可以去学习和培养得到这份能力的。因为自己做的一个程序库 demo,得到了第一份前端工作。前端工作 2 年后,老板尝试让我做产品,并在过程中得到老板的一些建议。1、做产品就不要去考虑程序实现;2、如果自己是对的,就要去坚持,争执得面红耳赤也没有关系。对于这两点建议现在我是如何理解的,后面我会讲。


在习惯方面,我经常会吐槽 xx 产品应如何实现,经常觉得 xx 产品很难用,也经常自己开发 xx 小工具。当然这里我想说:人人都是产品经理,我是认可这句话的。因为产品的受众就是大众,而大众的感受就是产品。至于我自己的 xx 小工具,当然也会被吐槽,不过我觉得这并不影响“喜欢做产品”这个习惯,而做出好产品,是在做产品的过程中去获得的能力。


如何得到这份工作


严格意义上的这份工作,大家都知道一般而言薪资是比开发要低一些的。我说下我能给到的:



  • 接受作为入门岗产品的薪资,不考虑自己的开发经验的工资

  • 能陪开发一起加班,一起赶项目

  • 能在与客户的需求讨论阶段,通过自己的开发经验给出符合客户所需和较低开发成本的解决方案

  • 能处理好产品核心的工作,例如需求文档、原型设计等(仅限于我当前对产品职务的了解)

  • 必要时可为前端团队提供技术方案


真正意义的产品岗的入门工作


我入职这家公司,公司管理层有征求我的意见。问公司现在缺产品,把我拉来填这个位置,问我的想法之类。我接受之后,在这个公司的职位就正式为产品了。


前期的工作,是与另一个兼职的产品去客户现场去了解需求,我做会议纪要,每场会我都在。


领导的意思是,因为兼职的那个产品可能会照顾不到。所以期望我今后能全权接受他手上的项目和往后的产品项目。


另一个公司的项目是两个团队开发的,公司一个团队,公司外部一个团队。这个项目有二期,计划我来接手二期。因为一期临近上线,把我接去做测试,说是我也刚好可以熟悉一下这个项目。虽然在之前我的理解中,产品就是产品,测试就是测试,心里多少有一点抵触。但想到确实在测试过程中多少可以增加对系统的了解,也坦然接受了。


然后在临近上线时,客户认为当前的产品流程不符合需要。需要修改流程,还要增加一个额外的流程。本来项目时间有所有延后,又加上客户添加需求,所以双方决定延后半月上线,但要添加新的流程以及再加一个二期功能。


这个二期功能中有一个拓客功能就是我将来要设计的模块。现在相当于我要提前介入。


不过好在这个系统的客户都还比较好相处,在客户现场做测试、改 BUG、讨论需求的这几天里,经常各种好吃好喝的东西都拿过来。饭点也问大家的口味情况,不重样的给大家点餐。系统有不少的问题,客户也没发脾气(这个我至今没理解)。


一个拓客模块原型


在我的构思中,是打算把整个拓客功能高度抽象化,尽量减少与原系统的耦合度。希望将来其他系统能便于复用这个模块,因为拓客功能是面向 C 端程序常见需求,并且流程也容易标准化。


所以构思了很多东西。


当与客户讲了这些东西之后,客户表示很多东西都有考虑到位。当然也有客户的自己侧重点的东西和必要上的东西的考量,这些东西在前期可能作为产品是比较难感知到的。



与客户讨论需求的部分心得


心得来源于分歧。


虽然这次需求沟通总的来说达到了自己的预期。但这边负责人后面批评了我,所为什么我要给客户讲这么多东西?为什么要答应他们?我们做不完!


我说我没有答应他们什么,我只是尽可能的去了解客户想做的,和让客户知道我想做的。后面我有意识到,由于这个模块是在这半月之类要临时加上去的,负责人害怕客户会认为我给他讲的那些功能就是这半月之类要上的功能。


所以,在这种情况下,在与客户表达功能的同时,要避免客户对功能产生错误的预期。


所以我后面单独找客户聊了,由于时间紧迫,之前给他讲的那些功能并不能完全实现。然后给他展示我这边能给到的一个满足他拓客条件的简化版本。客户表示理解,欣然接受,这个简化版本也与团队进行了同步,没什么问题。


另外,对于一个功能的实现,有很多做法和分支。我们不用一开始做得很细,当与客户沟通,得到客户想做的方向之后(当然客户想做的方向不一定正确,而如何能提前知道客户的方向不正确,这可能是更上一层的能力,比客户更了解客户所面临的问题)。


一个需求文档


拓客所处的项目第一期进行了近一年左右,神奇的是居然没有还没有需求文档。现在项目要上线了,负责人要去找客户结账了才想到要这文档。然后这文档让我来写,对于半路介入这个项目并且刚试岗这个职位的我来说简直头皮发麻。因为据我了解需求文档这东西巨细无遗,需要深入到系统的每个流程和细节。


谁让我现在是这个角色,我不入地狱谁入地狱?随后我反手就找公司把公司的需求文档模板发我一下。模板发了,但我一眼看过去,只知道需要填些什么内容,像是一个骷髅,却想不有内容的样子应会是怎样的,不知道一个有血肉甚至是有灵魂的样子是怎样的。


然后又让公司把以前的其他项目发我一份。然后公司随手发我一份,我打开一看,好家伙,161 页,部分内容如下:



以我之前的了解,需求文档这东西主要是用于验收的(实际开发中需求文档根本来不及跟上需求的变化)。而验收时为了表达工作量,需求文档通常都是内容越多越好。


所以这真也是个体力活。


为了让需求文本能与现有的实现相符合,我打开了现在的系统,现在的系统有些流程还跑不通,然后又根据我的之前的测试结果和现有原型的理解,进行梳理,先把页面和功能拉出来,大概如下:


# 后台管理系统
- 登录
- 用户名
- 密码
- 验证码
- 记住密码
- 系统管理
- 区域架构
- 展开和折叠
- 上级区域
- 名称
- 排序
- 状态是启用还是停用
- 区域层级
- 搜索 -- 名称、层级、状态
......
......
......

# 小程序
- 推广中心
- 统计面板
- 奖励总金额 -- 考虑隐私问题暂不展示应邀人员的细目
- 注册人数 -- 考虑隐私问题暂不展示应邀人员的细目
- 去提现 -- 跳转到体现页面
- 去提现
- 展示总的可提现金额
- 输入想提现的金额发起提现申请
- 展示提现申请记录

- 登录
- 有手机号时授权登录
- 无需要号时通过验证码登录,并进行实名认证
- 设置安全密码
......
......
......


然后根据页面和功能点去展开描述。具了解,需求文档需要包含以下内容:


- 产品概述
- 功能概述
- 用户需求
- 功能分析
- 非功能性需求
- 界面设计
- 数据需求
- 约束和假设

而在功能需求中,有几点是常见的:


- 功能概述
- 功能分析
- 界面设计
- 数据需求

看起来就是功能概括是怎样的?功能具体是怎样的?界面怎样的?数据库设计是怎样的?


很明显,数据库设计这个我暂时细致不了,而且我看现有的需求文档中也不是每个功能都把数据库设计放上去的。总之我认为,能基本把功能描述清楚,看起来够分量就行啦。


那么基于上面我列出的功能结构,例如:


- 登录
- 用户名
- 密码
- 验证码
- 记住密码

是很容易能推导出来:


- 功能概述
- 功能分析
- 界面设计

这东西的:


### 功能概述
本功能旨在提供用户登录系统的功能,包括输入用户名、密码和验证码,并提供记住密码的选项。

### 功能分析
用户登录功能主要涉及以下几个要素:

1. 用户名:用户需要输入其注册时使用的用户名。
2. 密码:用户需要输入与用户名对应的密码。密码应该以安全的方式进行存储和传输,例如使用哈希算法进行加密。
3. 验证码:为了增加登录的安全性,可以添加验证码功能,要求用户输入验证码。验证码通常是由字母和数字组成的随机字符串,用于验证用户的真实性。
4. 记住密码:提供一个选项,让用户选择是否记住密码。如果用户选择记住密码,下次登录时系统会自动填充用户名和密码。

### 界面设计
用户登录界面应包含以下元素:

- 用户名输入框:用于输入用户名。
- 密码输入框:用于输入密码。密码应以隐藏或替代字符的形式显示。
- 验证码输入框:用于输入显示的验证码。
- 验证码图片:用于显示验证码的图像,以便用户看到并输入。
- 记住密码复选框:用于让用户选择是否记住密码。
- 登录按钮:用户点击此按钮以提交登录表单并尝试登录系统。


然后我就以这种方式完成了 98 页的需求文档,这样应该能先交差一版了。


image.png


作者:四叶草会开花
来源:juejin.cn/post/7283766477802864675
收起阅读 »

到底怎样配色才能降低图表的可读性?

web
点赞 + 关注 + 收藏 = 学会了 本文简介 在数据可视化的世界里,图表是我们最常用的语言。但你是否曾被一张图表的配色误导? 配色方案的选择往往被看作是一种艺术,但其实它更是一门科学。 文章将带你一探究竟,哪些配色选择实际上会削弱图表的表达力,甚至误导读者。...
继续阅读 »

点赞 + 关注 + 收藏 = 学会了


本文简介


在数据可视化的世界里,图表是我们最常用的语言。但你是否曾被一张图表的配色误导?


配色方案的选择往往被看作是一种艺术,但其实它更是一门科学。


文章将带你一探究竟,哪些配色选择实际上会削弱图表的表达力,甚至误导读者。


过于丰富的颜色


我管理着10家酒店。以下是这10家酒店在2023年里的收入数据。


1月2月3月4月5月6月7月8月9月10月11月12月
酒店A134501360013200135001370013350136501340013800132001360013700
酒店B680071007300690072007400700073007500760071007200
酒店C152001490015100148001500014700152001490015100148001500014700
酒店D830085008100840086008200850083008600840085008600
酒店E118001200012200121001190012100122001200011800121001190012000
酒店F790081007700830078008400800082008100830084008200
酒店G146501440014700145001480014400146001470014500144001460014800
酒店H550057005800560059005750580059505600590057005800
酒店I143001400014200141001430014200140001410014300142001410014300
酒店J960094009800950097009600990094009800950097009600

我想按月对比酒店G酒店I的收入,并且能直观的知道这两家酒店在所有酒店中的收入属于什么水平。


如果按下图这样展示,对吗?


01.png


粗略一看,这图的数据还挺丰富的,色彩也挺吸引眼球。但你花了多久才找到酒店G酒店I


我们使用 Echarts 等图表库时,通常都会在页面中展示图例。如果想看酒店G酒店I的数据,那我们把其他酒店的数据隐藏掉就行了。


02.png


这样确实能很直观的看到酒店G酒店I的收入趋势和对比。


但把其他酒店的数据隐藏了,又观察不到这两家酒店在所有酒店中的收入水平。


更好的做法是将其他酒店的颜色设置为灰色。


03.png


灰色是一个不起眼的颜色,非常适合用来展示“背景信息”,它不像其他颜色那样吸引眼球。


在上面这个例子中,灰色的主要作用是描述“大环境”,用来凸显想要强调的信息。


但在实际项目中,如果页面的背景色不是白色,又想做到上面这个例子的效果,那可以在页面背景色的基础上往“白色”或者“黑色”方向调色。


04.png


比如,圆点是页面的背景色,红框部分就是可以选择的“背景信息”的颜色。


现在回过头来看看为什么会出现色彩丰富的图表。


我猜有两种可能。


一是项目需求,比如做To G的大屏项目,通常需要炫酷的特效和丰富的色彩去吸引甲方眼球。


二是设计工具或者前端的图表库默认提供了丰富的颜色,开发者只管把数据丢给图表库使用默认的配色去渲染。


配色始终不如一


同一个数据,在不同页中使用了不同的配色方案。用户会觉得你的产品很不专业,也很难培养用户习惯和对品牌的认知。


举个例子,在下方这个图中,顶部的柱状图和下方3个折线图的配色完全不一样。


05.png


反传统的配色


我们的产品支持微信和支付宝这两个支付方式,我们都知道支付宝的主色是蓝色,微信的主色是绿色。


在统计支付来源的数据时,如果出现反传统的配色就会影响用户对数据的理解。


06.png


再错得离谱点的话,可能会将支付宝和微信的主色对掉。


07.png




IMG_0393.GIF


点赞 + 关注 + 收藏 = 学会了


作者:德育处主任
来源:juejin.cn/post/7383268946819792911
收起阅读 »

浅谈放弃出国读研回忆录

那件事回想起来依然内心波澜起伏,那是2017年的冬天,那天风很大,信息工程学院2017级研究生群突然发了一条公告,关于《瑞典林奈大学双学位硕士项目推荐的通知会》,辅导员说感兴趣的同学可以去参加一下宣讲会。看完消息后,我久久不能平复。感谢qq群一直保存着群文档,...
继续阅读 »

那件事回想起来依然内心波澜起伏,那是2017年的冬天,那天风很大,信息工程学院2017级研究生群突然发了一条公告,关于《瑞典林奈大学双学位硕士项目推荐的通知会》,辅导员说感兴趣的同学可以去参加一下宣讲会。看完消息后,我久久不能平复。感谢qq群一直保存着群文档,现在我还能找到它。


mmexport1718958470859.jpg


真要从头说起,那是从我17年的春天成功收到上海一所高校的研究生录取通知书开始的。外地朋友不太理解从河南考到上海有多难,谈高考只有河南人自己懂。家人朋友为我感到骄傲,说我前途无量,以后在上海混出人头地了,别忘记曾经的河南四线城市的这帮老朋友。那年我真的以为考上了一线城市的研究生了,我就真的前途似锦了。经历了没有任何想法无所事事的暑假后(总结起来就干了一件事-35天拿了驾-照),2017年9月10号正式开学,然后命运的齿轮随着我乘坐的从河南开往上海的那趟火车开始转动了。我拎着大包小包的行李箱,随着校友的指引来到了研究生公寓,三人一间,有空调有独卫有阳台,比起我在河南的大学宿舍条件,档次直接提升了一个level,那会我别提有多新鲜了,新的面孔,新的环境,还是在一线魔都,这让我一个出身在河南落后小县城的普通家庭出身的女孩子对一切都感到新奇和欣喜。没有晚自习我就办了一张健身卡每天晚上去锻炼,周末没事就去市中心逛街,魔都的繁华,让我像刘姥姥进大观园一样。就连偶尔去泡的学校图书馆都比本科学校的长的像马桶的图书馆看起来更气派,甚至还有自己的独立自习室,每天跟同门师兄弟几人有说有笑的共同学习,那段时光大概是我到上海以后最无忧无虑的日子了。


面对当时研究生群里发的出国留学项目,我激动又向往,开始认真了解了起来,首先,学校的位置在典瑞,学校qs排名百度上说一百多位,还算可以,最后也是最重要的:学费和生活费


截屏2024-06-21 16.56.15.png
截屏2024-06-21 16.56.35.png

(107000学年 + 40320 * 2学期 + 机票)* 2年 约等于 40w


给大家科普一下19年的40w+什么概念,那会所有城市房子价格疯涨,相当于二线城市一套房子首付,普通家庭的全部家底,一个计算机专业的研究生省吃俭用996加班至少2-3年攒下的全部积蓄。


看完学费后犹豫了,父亲在电话里说支持我,就算是砸锅卖铁也愿意。我知道父亲就算是找亲戚借钱也真的能做到。但是我考虑了很久,花光家里的钱出国读书后,我知道眼界+经历是非常珍贵的,但是代价要牺牲父母的所有积蓄,更何况我还不是独生女,家里还有一个同胞在读书,怎么办,怎么选择?我做不到那么果断和自私。


可是机会真的很难得,读了国内也保留学籍,回国可以拿国内国外双硕士学位,3年拿双硕士性价比非常高,过了这村就没了这店了,这让我一个出身在18线小县城的穷学生非常心动又纠结。努力改变,但是面对经济状况,望而却步。辗转反侧了几周后,最后我默默的选择了放弃。安慰自己说父母不容易,操劳了一辈子,不让家里人负担太重。别的同学留学,可能是家里伸伸手就能够的着,我留学,是父母削尖脑袋往上挤才能做得到。这就是差距,我的自尊心,敏感又脆弱,面对现实,卑微又无奈。


后来在魔都打拼了多年,买了房,买了车,定居。今天上班开车路过家附近一所国际学校时勾起了自己陈年往事,如今回想起来,依然双眼湿润,内心久久不能平复。依然不禁遐想,假如当初咬咬牙,选择出国读书了会怎么样?是不是工资比现在高?圈子和眼界比现在精彩,人生体验会不会更圆满,等等一切遗憾的疑问句.......


但过去了终究是过去了,现在看这区区40万,并不是大数字,但多对于当时一个穷学生来说,简直就是一块沉重的大石头压在父母的肩膀上,压弯了腰,压不断精神的脊柱但能压断身体的脊椎。至少现在家里的同胞也成功毕业结了婚,对于父母来说,没有任何负担了。如果回到过去,我依然坚持自己的选择:放弃出国读研。


我对自己暗暗发誓说:我一定不会让自己的孩子经历我曾经经历的这些,存够至少100-200万再生孩子,让下一代有做选择的资本,而不是被现实所禁锢住飞翔的翅膀。不让自己的出身局限了刻在骨子里的认知,至少不要像我这样自卑无助,能给下一代提供自由发挥的环境,引导式教育,希望孩子勇敢自信独立,大胆的体验人生。


我想每一位当父母的都想给自己的孩子最好的,都希望下一代更有出息。在育儿的这条路上理解父母,成为父母。


作者:为了WLB努力
来源:juejin.cn/post/7382879981527400467
收起阅读 »

勇敢的人先拿到结果

上周许久未见的大学学长叫我出去喝酒,他这次来贵阳是为开分店的事情而来的,他比我高一个年级,在我毕业的时候,他就自己开始做生意了,短短两三年,到现在他已经开了七八个分店了,还在不断发展,并且加盟的人也不少,平均下来,现在每个月的收入也是很可观的。 对于我们这种末...
继续阅读 »

上周许久未见的大学学长叫我出去喝酒,他这次来贵阳是为开分店的事情而来的,他比我高一个年级,在我毕业的时候,他就自己开始做生意了,短短两三年,到现在他已经开了七八个分店了,还在不断发展,并且加盟的人也不少,平均下来,现在每个月的收入也是很可观的。


对于我们这种末流二本院校毕业的学生,特别还是在贵州这个经济相对比较落后的地区,拿到这个成绩还是挺厉害的,并且这个收入并不是固定的,还是不断增长。


学长是学市场营销的,这也算是个天坑专业,所以那会他就知道自己将来肯定是从事不了这个行业的,所以自己就在宿舍开了一个小卖部,每天下课后就骑着电瓶车去送货,虽然每个月赚不了多少钱,但是对于做生意这一块,他的思维肯定是得到了锻炼。


因为我们是在广西读书,所以螺蛳粉就比较多,在毕业后,他就去柳州考察做螺蛳粉,联系好各种渠道后,回到贵州就直接开干。


因为那会贵州的各个市里面卖螺蛳粉的还很少,并且没有特色和品牌效应,所以自己就先设计名称,logo,最后先开了一个店铺,自己亲自下厨,因为比较有特色,一个月直接干到了全市螺蛳粉餐饮销量的第二名。


随后又开了第二家,第三家......别人在看到他赚了钱后,其它市区的人也纷纷向他学习,他自己就收加盟费用,现在他要做的事情就是玩,还有考察门店,然后扩展。


从他的事迹中,我说两个点。


勇于放弃


对于很多人而言,读书的目的就是为了找一份稳定的工作,最好是体制内。


如果你读完大学后出去做销售,做生意,那么对于你身边的很多人而言,他们会觉得你这个大学白读了,因为在他们眼中,只有坐在办公室里面才是最体面了。


你和他说做生意,创业这些东西,他会给你说:这些不稳,以后没有退休工资。


但是如果你真听他们的,那么后面后悔的一定是你。


就像学长,如果他也和别人一样毕业后回到自己那地方加入考编大军,那么他现在肯定和别人一样,也在背书,焦虑,但是他选择了其它的路。


这时候有些人就会抬杠:考上了就能吃一辈子,而你做生意如果运气不好那么就直接亏光,到时候你就知道编制的香了。


这也是很多人的通病。


我觉得如果一件事情你看不到希望,就别过于去迷恋它,舍不得它,不然会被它束缚,比如学历,经验等等。


敢想敢干


可能你会觉得他家里应该有底子的,不然毕业后怎么就能开店。


但是我们问一下自己,就算你家里有底子,毕业后就给你十万块让你开店,你觉得你行吗?恐怕大部分人都不知道自己该做什么吧。


首先躬身入局本身就是一件很难的事情,我们多数人能够拼命上班,但是如果让你脱离平台去自己干一件事就比登天还难。


因为你在公司有别人给你安排好,你去做就行了,换句话来说,你就是个干苦力的,真让你去谈判,去闯市场,大多数人是没这个能力的。


这也是一种损失厌恶心态,因为你怕自己花时间去做,到后面不仅亏了钱,还把自己弄得很累,而安安稳稳打工不一样,它是“稳赚不赔”的。


但是这个世界上很难有稳赚不赔的东西,就说安安稳稳打工拿工资,但是工资不高,那一定是在亏着走的,除非你觉得自己的时间毫无价值,那么就是赚的。


作者:苏格拉的底牌
来源:juejin.cn/post/7340898858556178432
收起阅读 »

解决小程序web-view两个恶心问题

web
1.web-view覆盖层问题 问题由来 web-view 是一个 web 浏览器组件,可以用来承载网页的容器,会自动铺满整个页面。 所以这得多恶心。。。不仅铺满,还覆盖了普通的标签,调z-index都无解。 解决办法 web-view内部使用cover-...
继续阅读 »

1.web-view覆盖层问题


问题由来


web-view 是一个 web 浏览器组件,可以用来承载网页的容器,会自动铺满整个页面。



所以这得多恶心。。。不仅铺满,还覆盖了普通的标签,调z-index都无解。



解决办法


web-view内部使用cover-view,调整cover-view的样式即可覆盖在web-view上。


cover-view


覆盖在原生组件上的文本视图。


app-vue和小程序框架,渲染引擎是webview的。但为了优化体验,部分组件如map、video、textarea、canvas通过原生控件实现,原生组件层级高于前端组件(类似flash层级高于div)。为了能正常覆盖原生组件,设计了cover-view。


支持的平台:


AppH5微信小程序支付宝小程序百度小程序

具体实现


<template>
<view>
<web-view :src="viewUrl" v-if="viewUrl" >
<cover-view class="close-view" @click="closeView()">
<cover-image class="close-icon" src="../../static/design/close-icon.png"></cover-image>
</cover-view>
</web-view>
</view>

</template>

.close-view{
position: fixed;
z-index: 99999;
top: 30rpx;
left: 45vw;

.close-icon{
width: 100rpx;
height: 80rpx;
}
}

代码说明:这里的案例是一个关闭按钮图标悬浮在webview上,点击图标可以关闭当前预览的webview。


注意


仅仅真机上才生效,开发者工具上是看不到效果的,如果要调整覆盖层的样式,可以先把web-view标签注释了,写完样式没问题再释放web-view标签。


2.web-view导航栏返回


问题由来



  • 小程序端 web-view 组件一定有原生导航栏,下面一定是全屏的 web-view 组件,navigationStyle: custom 对 web-view 组件无效。


场景


用户在嵌套的webview里填写表单,不小心按到导航栏的返回了,就全没了。


解决办法


使用page-container容器,点击到返回的时候,给个提示。


page-container


页面容器。


小程序如果在页面内进行复杂的界面设计(如在页面内弹出半屏的弹窗、在页面内加载一个全屏的子页面等),用户进行返回操作会直接离开当前页面,不符合用户预期,预期应为关闭当前弹出的组件。 为此提供“假页”容器组件,效果类似于 popup 弹出层,页面内存在该容器时,当用户进行返回操作,关闭该容器不关闭页面。返回操作包括三种情形,右滑手势、安卓物理返回键和调用 navigateBack 接口。


具体实现


<template>
<view>
<web-view :src="viewUrl" v-if="viewUrl" >
<cover-view class="close-view" @click="closeView()">
<cover-image class="close-icon" src="../../static/design/close-icon.png"></cover-image>
</cover-view>
</web-view>
<!--这里这里,就这一句-->
<page-container :show="isShow" :overlay="false" @beforeleave="beforeleave"></page-container>
</view>

</template>

export default {
data() {
return {
isShow: true
}
},
methods: {
beforeleave(){
this.isShow = false
uni.showToast({
title: '别点这里',
icon: 'none',
duration: 3000
})
}
}
}


结语


算是小完美的解决了吧,这里记录一下,看看就行,勿喷。


作者:世界哪有真情
来源:juejin.cn/post/7379960023407198220
收起阅读 »

React Native新架构:恐怖的性能提升

web
自2018年以来,React Native团队一直在重构其核心架构,以便开发者能够创建更高质量更好性能的体验。最近在 React Native 的官网看到他们在安利他们的新的架构,本文将我所了解到的一些皮毛带给大家。以浅薄的见解来揭示其所带来的显著的性能改进,...
继续阅读 »

新架构


自2018年以来,React Native团队一直在重构其核心架构,以便开发者能够创建更高质量更好性能的体验。最近在 React Native 的官网看到他们在安利他们的新的架构,本文将我所了解到的一些皮毛带给大家。以浅薄的见解来揭示其所带来的显著的性能改进,并探讨为何以及如何过渡到这一新架构。


为什么需要新的架构?


多年来,使用React Native构建应用遇到了一些不可避免的限制。比如:React Native的布局和动画效果可能不如原生应用流畅,JavaScript和原生代码之间的通信效率低下,序列化和反序列化开销大,以及无法利用新的React特性等。这些限制在现有架构下无法解决,因此新的架构应运而生。新的架构提升了React Native在数个方面的能力,使得一些之前无法实现的特性和优化成为可能。


同步布局和效果


对比下老的架构(左边)和新的架构(右边)的效果:


React


构建自适应的UI体验通常需要测量视图的大小和位置并进行调整。在现有架构中,使用onLayout事件获取布局信息可能导致用户看到中间状态或视觉跳跃。而在新架构下,useLayoutEffect可以同步获取布局信息并更新,让这些中间状态彻底消失。可以明显看到不会存在跟不上的情况。


function ViewWithTooltip() {
const targetRef = React.useRef(null);
const [targetRect, setTargetRect] = React.useState(null);

useLayoutEffect(() => {
targetRef.current?.measureInWindow((x, y, width, height) => {
setTargetRect({ x, y, width, height });
});
}, [setTargetRect]);

return (
<>
<View ref={targetRef}>
<Text>一些内容,显示一个悬浮提示Text>

View>
<Tooltip targetRect={targetRect} />

);
}

支持并发渲染和新特性


可以看到新架构支持了并发渲染的效果对比,左边是老架构,右边是新架构:


并发渲染特性


新架构支持React 18及之后版本的并发渲染和新特性,例如Suspense数据获取和Transitions。这使得web和原生React开发之间的代码库和概念更加一致。同时,自动批处理减少了重绘的次数,提升了UI的流畅性。


function TileSlider({ value, onValueChange }) {
const [isPending, startTransition] = useTransition();

return (
<>
<View>
<Text>渲染 {value} 瓷砖Text>

<ActivityIndicator animating={isPending} />
View>
<Slider
value={value}
minimumValue={1}
maximumValue={1000}
step={1}
onValueChange={newValue =>
{
startTransition(() => {
onValueChange(newValue);
});
}}
/>

);
}

快速的JavaScript/Native接口


新架构移除了JavaScript和原生代码之间的异步桥接,取而代之的是JavaScript接口(JSI)。JSI允许JavaScript直接持有C++对象的引用,从而大大提高了调用效率。这使得像VisionCamera这样处理实时帧的库能够高效运行,消除大量序列化的开销。


JSI


VisionCamera 的地址是:github.com/mrousavy/re…


目前多达6K+的star,这个在 React Native 上的份量还是响当当的,可以看到它明显是用上了 JSI 了,向先驱们致敬。
VisionCamera


启用新架构的期望


尽管新架构提供了显著的改进,启用新架构并不一定会立即提升应用的性能。你的代码可能需要重构以利用新的功能,如同步布局效果或并发特性。或许,我认为,React Native 可能会同步出一些工具来帮助我们更好的迁移。比如配套的 eslint 插件,提示更优的建议写法等等。


现在是否应该使用新架构?


目前新架构仍被视为实验性,在2024年末发布的React Native版本中将成为默认设置。对于大多数生产环境应用,建议等待正式发布。库维护者则可以尝试启用并确认其用例被覆盖。另外看到react-native-vision-camera 这个库的 issue 下面反馈,JSI 目前还是存在一些坑需要爬的,所以要尝鲜的话,还是要有心理准备。
还是有坑在


通过详细介绍新架构的一系列优势和实际应用,我们可以看到React Native的未来发展前景。尽早了解和适应这些变化,一旦新架构正式发布,我们就能更好地利用React Native的潜力,为用户提供更好的体验。更好的产品体验,意味着产品的竞争力也会更强。


作者:brzhang
来源:juejin.cn/post/7377277576651898899
收起阅读 »