注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

仿抖音左右歪头图片选择

web
在线体验 项目 github 仓库 前一阵子在刷抖音时,看到一个通过左右歪头选择两侧图片的视频,感觉很有趣。顿时想到了 n 年前的face-api.js,那就基于这个来做吧。总体做好后,有很多细节需要改进,不够细腻丝滑。 1. 需求分析 直接开搞吧! ...
继续阅读 »

在线体验


项目 github 仓库


ezgif-4-7883a8f8e5.gif



前一阵子在刷抖音时,看到一个通过左右歪头选择两侧图片的视频,感觉很有趣。顿时想到了 n 年前的face-api.js,那就基于这个来做吧。总体做好后,有很多细节需要改进,不够细腻丝滑。



1. 需求分析


直接开搞吧!



  1. 页面基本布局,左右两侧图片,而且有缩放和移动动画

  2. 需要打开摄像头,获取视频流,通过 video 展现出来

  3. 需要检测人脸是向哪一侧歪头


2. 具体实现


2.1 页面布局和 animation 动画


这个不难,布局好后,就是添加 css 动画,我这里写的很粗糙,不细腻,但勉强能用,例如下面 leftHeartMove 为中间的小爱心向左侧移动动画


.heart {
width: 30px;
height: 30px;
padding: 4px;
box-sizing: border-box;
border-radius: 50%;
background-color: #fff;
position: absolute;
top: -15px;
left: 50%;
transform: translateX(-50%) rotateZ(0deg) scale(1);
animation: leftHeartMove 0.5s linear;
animation-fill-mode: forwards;
z-index: 2;
}

@keyframes leftHeartMove {
from {
top: -15px;
left: 50%;
transform: translateX(-50%) rotateZ(0deg) scale(1);
}

to {
top: 65px;
left: -13%;
transform: translateX(-50%) rotateZ(-15deg) scale(1.2);
}
}

2.2 打开摄像头并显示


注意点



  1. 关于 h5navigator.mediaDevices.getUserMedia 这个 api,本地开发localhost是可以拉起摄像头打开提示的,线上部署必须是https节点才行,http不能唤起打开摄像头


WX20221128-221028@2x.png




  1. 关于获取到视频流后,video视频播放,需要镜面翻转,这个可以通过 css 的transform: rotateY(180deg)来翻转




  2. 关于video播放不能在手机上竖屏全屏,可以给 video 设置 cssobject-fit:cover来充满屏幕




<video id="video" class="video" playsinline autoplay muted></video>

.video {
width: 100%;
height: 100%;
transform: rotateY(180deg);
object-fit: cover;
}


  • 获取摄像头视频流


async getUserMedia() {
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
try {
// https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia#examples
const stream = await navigator.mediaDevices.getUserMedia({
audio: false,
video: true,
video: {
facingMode: "user", // 前置摄像头
// facingMode: { exact: "environment" },// 后置摄像头
width: { min: 1280, max: 1920 },
height: { min: 720, max: 1080 },
},
});

return Promise.resolve(stream);
} catch (error) {
return Promise.reject();
}
}

const errorMessage =
"This browser does not support video capture, or this device does not have a camera";
alert(errorMessage);
}


  • video 播放视频流


async openCamera(e) {
try {
const stream = await this.getUserMedia();
this.video.srcObject = stream;
this.video.onloadedmetadata = async () => {
this.video.play();
};
} catch (error) {
console.log(error);
alert("打开摄像头失败");
}
}


  • 关闭视频


async closeCamera() {
// https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamTrack/stop
const tracks = this.video.srcObject.getTracks();

tracks.forEach((track) => {
track.stop();
});

this.video.srcObject.srcObject = null;
}

2.3 检测人脸左右倾斜


landmarks.png


通过face-api.js拿到人脸landmarks特征数据后,可以直接拿到左右眼的数据,分别通过求 Y 轴方向的平均值,然后比较这个平均值,便可以简单得出人脸向左还是向右倾斜,简单吧,角度都不用求了!


<div style="position: relative;width: 100%;height: 100%;">
<video
id="video"
class="video"
playsinline
autoplay
muted
style="object-fit:cover"
>
</video>
<canvas id="overlay" class="overlay"></canvas>
</div>

.video {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
z-index: 0;
transform: rotateY(180deg);
}

.overlay {
position: absolute;
top: 0;
left: 0;
}


  • 加载模型


import * as faceapi from "face-api.js";

async loadWeight() {
// 加载模型
await faceapi.nets.ssdMobilenetv1.load(
"./static/weights/ssd_mobilenetv1_model-weights_manifest.json"
);
// 加载人脸68特征模型数据
await faceapi.nets.faceLandmark68Net.load(
"./static/weights/face_landmark_68_model-weights_manifest.json"
);
// await faceapi.nets.faceExpressionNet.load(
// "/static/weights/face_expression_model-weights_manifest.json"
// );
// await faceapi.nets.faceRecognitionNet.load(
// "./static/weights/face_recognition_model-weights_manifest.json"
// );
await faceapi.nets.ageGenderNet.load(
"./static/weights/age_gender_model-weights_manifest.json"
);

console.log("模型加载完成");
}


  • 计算人脸左右倾斜


handleFaceLeftOrRight(landmarks) {
const DIFF_NUM = 15; // 偏差
let leftEye = landmarks.getLeftEye(); // 左眼数据
let rightEye = landmarks.getRightEye(); // 右眼数据
// let nose = landmarks.getNose();

let leftEyeSumPoint = leftEye.reduce((prev, cur) => ({
x: prev.x + cur.x,
y: prev.y + cur.y,
}));

let rightEyeSumPoint = rightEye.reduce((prev, cur) => ({
x: prev.x + cur.x,
y: prev.y + cur.y,
}));

// let noseSumPoint = nose.reduce((prev, cur) => ({
// x: prev.x + cur.x,
// y: prev.y + cur.y,
// }));

let leftEyeAvgPoint = {
x: leftEyeSumPoint.x / leftEye.length,
y: leftEyeSumPoint.y / leftEye.length,
};

let rightEyeAvgPoint = {
x: rightEyeSumPoint.x / leftEye.length,
y: rightEyeSumPoint.y / leftEye.length,
};

// let noseAvgPoint = {
// x: noseSumPoint.x / leftEye.length,
// y: noseSumPoint.y / leftEye.length,
// };

// console.log(leftEyeAvgPoint, rightEyeAvgPoint, noseAvgPoint);
let diff = Math.abs(leftEyeAvgPoint.y - rightEyeAvgPoint.y);

return diff > DIFF_NUM
? leftEyeAvgPoint.y > rightEyeAvgPoint.y
? "left"
: "right"
: "center";
}


  • 处理 video 视频


async handleVideoFaceTracking(cb) {
if (this.closed) {
window.cancelAnimationFrame(this.raf);
return;
}

const options = new faceapi.SsdMobilenetv1Options();

let task = faceapi.detectAllFaces(this.video, options);
task = task.withFaceLandmarks().withAgeAndGender();
const results = await task;

// overlay为canvas元素
// video即为video元素
const dims = faceapi.matchDimensions(this.overlay, this.video, true);
const resizedResults = faceapi.resizeResults(results, dims);

// console.log("options==>", options);
// console.log("resizedResults==>", resizedResults);
cb && cb(resizedResults);

this.raf = requestAnimationFrame(() => this.handleVideoFaceTracking(cb));
}

3. 参考资料




  1. face-api.js




  2. getUserMedia MDN




作者:sRect
来源:juejin.cn/post/7171081395551338503
收起阅读 »

释放ChatGPT的真正潜力:Prompt技巧进阶指南

Prompt对用好ChatGPT的重要性毋庸置疑,我们在上篇文章中介绍了写好Prompt的通用原则和一些基础技巧,在本文中,我们将继续探索一些Prompt的高阶技巧。 在介绍这些高阶技巧前,我们先对之前介绍的基础技巧做一个简单回顾。 首先作为通用原则,我们的...
继续阅读 »

Prompt对用好ChatGPT的重要性毋庸置疑,我们在上篇文章中介绍了写好Prompt的通用原则和一些基础技巧,在本文中,我们将继续探索一些Prompt的高阶技巧。

图片


在介绍这些高阶技巧前,我们先对之前介绍的基础技巧做一个简单回顾。


首先作为通用原则,我们的Prompt应该尽可能简单、具体、准确,有话直说,在此基础上,我们有Zero-shot, Few-shot, Instruction, CoT以及分而治之等基础技巧,在Few-shot中,例子的多样性和排序对结果有较大的影响,在CoT中,例子的复杂度和描述方式对结果也有很大影响。这些技巧还可以组合起来使用如Few-Shot Instruction, Zero-shot CoT, Few-Shot CoT等,CoT还衍生出来一个叫做Self-Ask的技术,通过ChatGPT提问,调用外部API回答的方式,我们可以帮助ChatGPT逐步推导出复杂问题的答案。


接下来,我们开始介绍高阶Prompt技巧。


Self-consistency(自洽) 这一技巧的思路是投票选举,少数服从多数,其具体工作过程是对同一个问题,让ChatGPT生成多个可能的答案,然后选择占比最高的那个答案作为最终答案,其工作原理示意图如下:


图片


这个技巧很容易理解,但是有几个要注意的点:





    • 为了让ChatGPT生成多个答案,如果你是通过API对其进行调用的,请把temperature参数设置得大一些以增加API输出的多样性

    • 为了生成多个答案,需要进行多次ChatGPT调用,这会大大增加成本(gpt-3.5-turbo降价10倍,这个问题好了一些)

    • 这个方法只适用于那些只有有限答案可供选择的情况,像开放式的问题,比如给我写一首诗这种问题,这个技巧肯定不适用




为了进一步提升这一方法的效果,还可以额外训练一个小模型或者简单的规则,在投票前先把明显不靠谱的答案过滤掉(比如上图中Janet一天挣多少钱的问题,如果答案是负数肯定不对),过滤后如果备选答案不够就再让ChatGPT生成几个,直到有足够靠谱答案了再投票。


Generated Knowledge(生成知识) :我们知道在向ChatGPT提问时,如果能提供一些上下文信息,其回答问题的准确率就会高很多,顺着这个思路,我们也可以让ChatGPT自己给自己提供上下文,而不是每次都由我们完成这一工作,我们还是通过例子来说明这一方法的工作过程:



Part of golf is trying to get a higher point total than others. Yes or No?



得到的答案是



Yes.



这显然是个错误的答案,因为打高尔夫杆数越少越好。


我们可以让ChatGPT回答问题前首先自己生成一些知识,下面是用few-shot技术来让ChatGPT生成知识的例子:



Input: Greece is larger than mexico.


Knowledge: Greece is approximately 131,957 sq km, while Mexico is approximately 1,964,375 sq km, making Mexico 1,389% larger than Greece.




Input: Glasses always fog up.


Knowledge: Condensation occurs on eyeglass lenses when water vapor from your sweat, breath, and ambient humidity lands on a cold surface, cools, and then changes into tiny drops of liquid, forming a film that you see as fog. 




Input: A fish is capable of thinking.


Knowledge: Fish are more intelligent than they appear. In many areas, such as memory, their cognitive powers match or exceed those of ’higher’ vertebrates including non-human primates. Fish’s long-term memories help them keep track of complex social relationships




Input: Part of golf is trying to get a higher point total than others.


Knowledge:



ChatGPT返回如下知识片段:



The objective of golf is to play a set of holes in the least number of strokes. A round of golf typically consists of 18 holes. Each hole is played once in the round on a standard golf course. Each stroke is counted as one point,  the total number of strokes is used to determine the winner of the game.



接下来,我们把这个知识作为上下文,再次向ChatGPT提问:



Question: Part of golf is trying to get a higher point total than others. Yes or No?


Knowledge: The objective of golf is to ...and the total number of strokes is used to determine the winner of the game..


Explain and Answer:



得到如下答案:



No,  the objective of golf is not to get a higher point total than others. Rather, the objective is to play a set of holes in the least number of strokes. The total number of strokes is used to determine the winner of the game, not the total number of points.



可以看到我们让ChatGPT自己生成的知识帮助它给出了正确的答案。


Program of Though(程序式思考) :ChatGPT最近推出了Plugin,其思路是把一些问题交给外部的工具来完成,和这一思路类似,有些问题涉及复杂的计算,ChatGPT可能并不适合进行这类计算,我们可以让它为要解决的问题写一段程序,我们运行这个程序以得到最后的答案,下面是一个计算斐波那契数列的例子: 


图片


Selection-Inference (选择-推理):这是一种在Few-shot CoT的基础上演化出来的技术,其基本思路是分两步来解决问题:






    • 第一步:Selection, 从上下文中选择和问题相关的事实,作为回答问题的证据/原因




    • 第二步:Inference,基于选择出的证据/原因,进行推理,看能否回答问题,如果能回答问题,则把推理结果作为答案输出,如果不能,则把推理结果作为新的事实补充到上下文中,回到第一步






下图是来自论文中的工作原理介绍图:


图片


图中的几个颜色说明如下:





    • 黄色:    上下文

    • 绿色:    问题

    • 浅蓝色:原因

    • 红色:    推理结果或答案




论文中的图不是特别好理解,我在图上加了3个红框,接下来详细介绍下如何理解这张图:






    • 首先,为了理解Selection-Inference这个两步走的工作原理,我们先忽略图中框出的1和2,没有了这两个干扰,工作原理就比较清楚了,左边是Selection的过程,首先是黄色背景的上下文中给出了4个事实,然后提问:“emily怕什么”,ChatGPT(或其它LLM)从中选出了两条作为证据/原因:“狼怕老鼠,emily是一头狼”(图中的3)




    • 然后,把上述选择的事实交给ChatGPT(或其它LLM)进行推理,推理结果是“emily怕老鼠”,然后判断问题是否得到了回答,如果能则把“emily怕老鼠”作为结果返回,否则把“emily怕老鼠”加入到上下文回到Selection环节继续循环,(在本例中,“emily怕老鼠”已经回答了“emily怕什么”的问题,所以无需继续)




    • 接下来,我们再看看框1和框2的作用,实际这里是一个Few-shot技巧,在框1中给出了若干个【上下文-问题-原因】作为例子,然后跟一个【上下文-问题】,这样ChatGPT就明白你是让它像例子一样,基于给出的上下文和问题,找出回答问题的证据/原因,如果没有这些例子,直接给一个【上下文-问题】它可能不理解你到底让它干嘛。框2也是类似,通过给出几个【原因-推理】的例子让ChatGPT明白,它需要根据给出的原因,进行推理






明白了工作原理后,我们再次忽略细节,其整个工作过程如下图所示(Halter模块决定是继续循环还是给出最终答案):


图片


虽然图看着比较复杂,但其思路和基础技巧中介绍的分而治之的思路很像,先求解中间过程,然后推导最终答案。这个技巧在处理比较复杂的问题时效果比较明显,如果问题本身不涉及太多步骤的推导,一些简单技巧就能解决问题。


一些更复杂的技巧


下面的几个技巧比较复杂,大家平时也不一定能直接用得到,我将简单介绍下其思路,不详细展开,大家知道有这个技巧,真的觉得需要使用时再深入研究就好(我在下面会为大家附上论文链接)。


Automatic Prompt Engineer(APE Prompt自动生成) : 有时候,除了我们自己手写Prompt之外,我们也可以利用ChatGPT帮我们找到一个好的Prompt,下图是一个例子:


图片


这个例子中,我们想找到一句好的Prompt(指令),这个指令可以让ChatGPT更好地为一个输入词给出反义词。上图的工作过程是,首先给出一堆正-反义词,然后让ChatGPT给出若干候选指令,接着评估这些候选指令,看哪一个能让ChatGPT在测试集上有更好的表现,然后选择其中得分最高的作为答案。


这里还有一个可选的优化步骤是,把上述最高分的候选指令,让ChatGPT在不改变语义的情况下,生成几个变种,然后重复上面的过程,从这几个变种中选出最优的那个。


有兴趣的同学可以在这里看论文:arxiv.org/pdf/2211.01…


Least to Most(由易到难) :其思路是对高难度的问题,先将其拆解为难度较低的子问题,把这些子问题解决后,用这些子问题和答案作为上下文,再让ChatGPT完成高难度问题的回答,下面是一个例子:


图片


这个方法只有比较复杂的情况下才能发挥比较大的作用,另外要用好这个技巧,能否让ChatGPT有效地分解问题是关键,但整篇论文并没有对此给出详细说明。


有兴趣的同学可以在这里看论文:arxiv.org/pdf/2205.10…


Maieutic(类决策树) :这是复杂度最高的一个技巧,其基本思路是对一个复杂问题,层层递归,对这个问题生成各种可能的解释(以及子解释),然后从中选择最靠谱的节点,推导最终的问题答案


工作过程如下:





    1. build一棵maieutic树,树上的每个树叶都是一句True/False的陈述:



      • 从一个多选问题或者true/false问题开始(比如:战争不能有平局)

      • 对每一个可能的答案,让ChatGPT生成一个解释(Prompt类似:战争不能有平局吗?True,因为)

      • 然后让ChatGPT用上一步生成的解释来回答最初的问题,接着再颠倒上述解释(比如:这么说是不对的,{上一步生成的解释}),再次让ChatGPT回答问题,如果答案也能颠倒过来,则认为这个解释是逻辑自洽的,反之则递归上述过程,把每个解释变成一个True/False的子问题,然后生成更多的解释

      • 上述递归完成后,我们就能得到一棵树,每个叶子节点都是一个逻辑自洽的解释



    2. 把这棵树转化为一个关系图



      • 对每个叶子节点计算其置信度

      • 对每一对节点,判断他们是一致的还是矛盾的



    3. 用一个叫做MAX-SAT的算法,选择一组一致的、置信度最高的节点,从中推导出最后的答案




下面是一个例子:


图片


这是另一个例子:


图片


有兴趣的同学可以在这里看论文:arxiv.org/pdf/2205.11… Engineering领域里面的几个技巧介绍,如果我们去搜索一下的,还会发现更多技巧介绍的论文,这里就不再继续了。其实通过上面的几个例子大家能看到,这些复杂的技巧,一般都涉及多个步骤,都会把大问题拆解成若干小问题,这其实就是计算机里面最常用的分而治之的策略,我想只要我们记住这一策略,在我们解决具体问题的时候,也能想到自己的Prompt技巧。



作者:Ronny说
来源:juejin.cn/post/7219847723092361274
收起阅读 »

假如:a===1 && a===2 && a===3; 那么 a 是什么?

web
前言 文章提供视频版啦,点击直接查看 hello,大家好,我是 sunday。 今天遇到了一个非常有意思的问题,跟大家分享一下。 咱们来看这段代码: a===1 && a===2 && a===3 假设上面的表达式成立,...
继续阅读 »

前言



文章提供视频版啦,点击直接查看



hello,大家好,我是 sunday


今天遇到了一个非常有意思的问题,跟大家分享一下。


咱们来看这段代码:


a===1 && a===2 && a===3 

假设上面的表达式成立,那么问:a 是什么?


正文


ok,我们来说一下这个问题的解答。


想要解决这个问题,那么我们首先要知道 JavaScript 中的类型转换和比较运算符的优先级。


JavaScript 中,表达式的运算顺序是 从左到右。因此,在这个表达式中,先执行 a===1 的比较运算符,如果它返回 false,整个表达式就会返回 false,也就是逻辑中断。


如果 a 的值是 1,则比较运算符返回 true,那么就会继续执行下一个逻辑运算符 &&,接着执行 a===2 的比较运算符,如果它返回 false,则整个表达式返回 false,逻辑中断。


以此类推,以此类推,所以 a 的值应该是动态变化的,并且应该依次为 1、2、3。只有这样才会出现 a===1 && a===2 && a===3; 返回 true 的情况。


那么 如何让 a 的值动态变化,就是咱们解决这个问题的关键。


我们在 一小时读完《JavaScript权威指南(第7版)》上一小时读完《深入理解现代 JavaScript》,彻底掌握 ES6 之后 JavaScript 新特性! 中都讲到过,对象的方法存在 get 标记,一旦方法存在 get 标记,那么我们就可以像调用对象的属性一样,调用这个方法。


那么说到这里,肯定很多小伙伴都想到这个问题怎么解决了。


我们直接来看代码:


 const obj = {
 // get 标记
 get a() {
   this.value = this.value || 1;
   return this.value++;
}
};

console.log(obj.a === 1 && obj.a === 2 && obj.a === 3); // true

在这段代码中,我们创建了一个对象 obj,它包含一个被 get 标记的方法 a。那么此时只要执行 obj.a 就会调用 a 方法,完成 value 自增的操作。从而得到咱们期望的结果。


总结


这是一个非常有意思的问题。除了上面这种方案之后,还有很多其他的实现方案。大家可以开动脑筋,想一想别的方案都有什么呢?


答案留在评论区,咱们

作者:LGD_Sunday
来源:juejin.cn/post/7223586933881421861
一起来讨论下哦~~~

收起阅读 »

浅析小程序蓝牙技术

web
认识蓝牙 蓝牙技术是一种无线数据和语音通信开放的全球规范,它是基于低成本的近距离无线连接,为固定和移动设备建立通信环境的一种特殊的近距离无线技术连接。 传统蓝牙和低功耗蓝牙 根据蓝牙的发展历程,将蓝牙普遍分为两种规格,即传统蓝牙模块(BT) 和低功耗蓝牙模...
继续阅读 »

认识蓝牙



蓝牙技术是一种无线数据和语音通信开放的全球规范,它是基于低成本的近距离无线连接,为固定和移动设备建立通信环境的一种特殊的近距离无线技术连接。



传统蓝牙和低功耗蓝牙


根据蓝牙的发展历程,将蓝牙普遍分为两种规格,即传统蓝牙模块(BT)低功耗蓝牙模块(BLE)。传统蓝牙模块常用在对数据传输带宽有一定要求的场景上。低功耗蓝牙是从蓝牙4.0起支持的协议,特点是耗电极低、传输速度更快,常用在对续航要求较高且只需小数据量传输的各种智能电子产品中。


技术指标经典蓝牙BT低功耗蓝牙BLE
无线电频率2.4GHz2.4GHz
距离10米最大100米
发送数据所需时间100ms<3ms
响应延时约100ms6ms
安全性64/128-bit及用户自定义的应用层128-bitAES及用户自定义的应用层
能耗100%(ref)1%-50%
空中传输数据速率1-3Mb/s1Mb/s
主要用途手机,游戏机,耳机,音箱,汽车和PC等鼠标,键盘,手表,体育健身,医疗保健,智能穿戴设备,汽车,家用电子等
适用场景较高数据量传输、对传输带宽有要求续航要求较高、数据量小

蓝牙技术目前已经发展到5.0+版本,为现阶段最高级的蓝牙协议标准。BLE技术更契合新时代物联网的需求:更快、更省、更远、更便捷,也是我们小程序开发者在物联网项目最常用的技术。


蓝牙通信概述


低功耗蓝牙协议给设备定义了若干角色,其中最主要的角色是:外围设备(Peripheral)中心设备(Central)。




  • 外围设备:用来提供数据,通过不停地向外广播数据,让中心设备发现自己。




  • 中心设备:扫描外围设备,发现有外围设备存在后,可以与之建立连接,之后就可以使用外围设备提供的服务(Service)。




在两个蓝牙设备建立连接之后,双方的数据交互是基于一个叫做 GATT (Generic Attribute Profile,通用属性配置文件) 的规范,根据该规范可以定义出一个配置文件(Profile),描述该蓝牙设备提供的服务(Service)。


在整个通信过程中,有三个最主要的概念:配置文件(Profile)服务(Service)特征(Characteristic)


Characteristic:在 GATT 规范中最小的逻辑数据单元。实际上,在与蓝牙设备打交道,主要就是通过读写 Characteristic 的 value 完成。Characteristic 是通过一个 16bit 或 128bit 的 UUID 唯一标识。


Service:可以理解为蓝牙设备提供的服务,一个蓝牙设备可以提供多个服务,比如电量信息服务、系统信息服务等。每个 Service 又包含多个 Characteristic 特性值,比如电量信息服务就会有个 Characteristic 表示电量数据。同时也有一个 16bit 或 128bit 的 UUID 唯一标识该服务。


Profile:并不真实存在于蓝牙设备中,它只是被蓝牙标准预先定义的一些 Service 的集合。如果蓝牙设备之间要相互兼容,它们只要支持相同的 Profile 即可。一个蓝牙设备可以支持多个 Profile。


Desciptor: 描述符是描述特征值的已定义属性。例如,Desciptor 可指定人类可读的描述、特征值的取值范围或特定于特征值的度量单位。每个 Desciptor 由一个 UUID 唯一标识。


总结:每个蓝牙设备可能提供多个 Service,每个 Service 可能有多个 Characteristic,根据蓝牙设备的协议,用对应的 Characteristic 进行读写,即可达到与其通信的目的。


蓝牙开发实践


蓝牙通信过程介绍



整体上看,蓝牙通信的开发主要分为三部分:



  1. 蓝牙资源和状态管理:包括蓝牙生命周期管理、蓝牙状态管理(开关、适配器、设备连接、数据接收等)、错误异常处理。

  2. 搜寻外围设备并建立连接:包括搜寻设备、监听设备发现、处理获取到的设备信息、连接/断开设备等。

  3. 读写数据:包括寻找目标服务和特征值、订阅特征值、监听并接收设备数据、分包处理数据等。


蓝牙数据读写


在小程序蓝牙开发联调中,推荐使用TLV协议对数据进行封包,TLV协议(Tag、Length、Value)是常见的一种面向物联网的通讯协议,对于不同的传输场景,甚至演变出混合型、指针型、循环型等不同类型的格式。


比如,在实践中往往只需要最简单的L-TLV格式,以下使用十六进制(Hex)表示:



  • 数据包总长(L)

  • 数据的类型Tag/Type(T)

  • Value的长度Length(L)

  • 数据的值Value(V)


[0x07, 0x01, 0x01, 0x01, 0x02, 0x01, 0x01]
[数据总长,typelength,value,typelength,value]

举例


假设业务规定各字段type如下


字段名称type字段类型备注
account0x00String账号
Password0x01String密码

想要向设备传输一条写入account的指令,value为ABC。


ABC 通过 UTF-8 编码转 Hex String 分别是0x41、0x42、0x43。


那么数据包总长6字节,type是0,value总长3字节。


字符集编码


实际业务场景中,如果需要传输中文字符,则需要通过协商好的字符集进行转换。


常见字符集有:ASCII字符集、GB2312字符集、GBK字符集、 GB18030字符集、Unicode字符集等。


字符集描述
ASCII美国信息交换标准代码是基于拉丁字母的一套电脑编码系统,主要用于显示现代英语和其他西欧语言
GB2312中国人民通过对 ASCII 编码的中文扩充改造,产生了 GB2312 编码,可以表示6000多个常用汉字。
GBK汉字实在是太多了,包括繁体和各种字符,于是产生了 GBK 编码,它包括了 GB2312 中的编码,同时扩充了很多。
GB18030中国是个多民族国家,各个民族几乎都有自己独立的语言系统,为了表示那些字符,继续把 GBK 编码扩充为 GB18030 编码。
Unicode每个国家都像中国一样,把自己的语言进行编码,于是出现了各种各样的编码,如果你不安装相应的编码,就无法解释相应编码想表达的内容。终于,有个叫 ISO 的组织看不下去了。他们一起创造了一种编码 Unicode ,这种编码非常大,大到可以容纳世界上任何一个文字和标志。
UTF-8、 UTF-16Unicode 在网络传输中,出现了两个标准 UTF-8 和 UTF-16,分别每次传输 8个位和 16个位。

比如小写字母a,ASCII编码对应的Hex值是0x61,而GB2312字符集编码对应的Hex值是253631


将文本字符串转换为Hex字符串的时候,不同的字符集编码对应的Hex值不一样,所以小程序与蓝牙设备应当使用同一套字符集编码。推荐统一使用Unicode的UTF-8标准。


以下是字符转换示例:


// 中文转UTF-8
encodeURI('好').replace(/%/g, ''); // 'E5A5BD'

// UTF-8转中文
hex2String('E5A5BD'); // '好'

/**
* * read UTF-8
* @param { number[] } arr
* @returns {string}
*/

const readUTF = (arr: number [] ) => {
let UTF = '';
const _arr = arr;
for (let i = 0; i < _arr.length; i++) {
// 10进制转2进制
const one = _arr[i].toString(2);
const v = one.match(/^1+?(?=0)/);
if (v && one.length == 8) {
const bytesLength = v[0].length;
let store = _arr[i].toString(2).slice(7 - bytesLength);
for (let st = 1; st < bytesLength; st++) {
store += _arr[st + i].toString(2).slice(2);
}
// 二进制序列转charCode,再拼接
UTF += String.fromCharCode(parseInt(store, 2));
i += bytesLength - 1;
} else {
UTF += String.fromCharCode(_arr[i]);
}
}
return UTF;
};

/**
* * transfer hex to string
* @param { string } str
* @returns {string}
*/

const hex2String = (hex: string) => {
const buf = [];
// 转10进制数组
for (let i = 0; i < hex.length; i += 2) {
buf.push(parseInt(hex.substring(i, i + 2), 16));
}

return readUTF(buf);
};

蓝牙分包


但是实际场景往往不是传输几个字母这么简单。虽然小程序不会对写入数据包大小做限制,但与蓝牙设备传输数据时,数据量超过 MTU (最大传输单元) 容易导致系统错误,所以要主动对数据进行分片传输。


参考各小程序开放平台文档:


开放平台文档描述
微信小程序在与蓝牙设备传输数据时,需要注意 MTU(最大传输单元)。如果数据量超过 MTU 会导致错误,建议根据蓝牙设备协议进行分片传输。Android设备可以调用 wx.setBLEMTU 进行 MTU 协商。在 MTU 未知的情况下,建议使用 20 字节为单位传输。
飞书小程序蓝牙设备特征值对应的值,为 16 进制字符串,限制在 20 字节内
支付宝小程序写入特征值需要使用 16 进制的字符串,并限制在 20 字节内。
Taro小程序不会对写入数据包大小做限制,但系统与蓝牙设备会限制蓝牙4.0单次传输的数据大小,超过最大字节数后会发生写入错误,建议每次写入不超过20字节。若单次写入数据过长,iOS 上存在系统不会有任何回调的情况(包括错误回调)。

分包的过程,需要用到 ArrayBuffer



ArrayBuffer 对象用来表示通用的、固定长度的原始二进制数据缓冲区。


ArrayBuffer 是对固定长度的连续内存空间的引用。



在 Web 开发中,当我们处理文件时(创建,上传,下载),经常会遇到二进制数据。另一个典型的应用场景是图像处理。这些都可以通过 JavaScript 进行处理,而且二进制操作性能更高。


ArrayBuffer 只是一个内存区域,里面存储着一些原始的字节序列,它和普通的Array完全不是一个概念,它的长度是固定的,无法增加或减少,也无法直接用buffer[index]进行访问。


要想写入值、遍历它或者访问单个字节,需要使用视图(View) 进行操作,以下为一些常用的视图:


Uint8Array :将 ArrayBuffer 中的每个字节视为 0 到 255 之间的单个数字(每个字节是 8 位,因此只能容纳那么多)。称为 “8 位无符号整数”。


Uint16Array:将每 2 个字节视为一个 0 到 65535 之间的整数。称为 “16 位无符号整数”。


所有这些视图(Uint8Array,Uint32Array 等)的通用术语是 TypedArray(类型化数组)。它们都享有同一组方法和属性,类似于常规数组,具有索引,并且是可迭代的。


实际上,不同平台的小程序API定义的数据接口,都多少会用到ArrayBuffer



微信小程序-写入特征值



飞书小程序-获取设备信息


但也不排除有些操作,开发平台已经帮忙处理了



飞书小程序-写入特征值


因此学习并使用 ArrayBuffer,可以:




  1. 方便操作分包,方便读取设备返回的数据、向设备写入数据。




  2. 在不同小程序平台灵活处理,更好地兼容




回到主题,蓝牙分包的思路是:


Text String --> Hex String --> ArrayBuffer(分包)


举个例子,上文中想要向设备传输一条写入password的指令,value为bytedance123456789ABC


[数据总长,type,length,value] MTU为20字节
[0x14, 0x01, 0x11, 0x62, 0x79, 0x74, ...] 第一个包 bytedance12345678
[0x07, 0x01, 0x04, 0x39, 0x41, 0x42, ...] 第二个包 9ABC

设备端会将多个相同type的包的值追加,而不是覆盖。


如何与设备端协商分包交互机制?



  1. 规定服务、特征值UUID,建议不同操作使用不同的UUID,读、写、订阅分开。

  2. 遵循TLV协议,双方协商好Type对应的字段类型和含义。

  3. 双方使用同一套字符编码集。

  4. 约定连在一起的两次(或多次)相同类型的设置,应该把它们的值追加连接,而不是覆盖

  5. 可约定在一次涉及业务逻辑的通信过程中,发送“开始”和“结束”的蓝牙包,告知设备处于这两个信号之间的蓝牙包为一次完整的通信数据流。

  6. 双方共同约定一个超时时间,若在此时间内由于各种原因未能完成读/写通信,则认为通信失败,小程序端必须给予用户友好提示。


问题排查手段


在开发过程中可能会遇到调用API失败、连接断开等问题



  1. 检查API调用顺序


小程序的蓝牙API使用起来比较简单,但是需要严格遵循一定的调用顺序(参考上文的流程图)。比如检查是否在开关蓝牙适配器之外进行操作,或者是否在特征值发生变化后才进行事件监听等



  1. 对比测试



  • 业务小程序、开放平台官方蓝牙demo 对比

  • 开放平台(非微信)官方蓝牙demo、微信官方demo 对比

  • 同厂商设备、同芯片、同蓝牙模组,多台设备对比

  • iOS、Android,蓝牙调试软件 与小程序的对比 (iOS:LightBlue,Android:BLE调试宝、nRF Connect)


经过以上对比测试,基本可以缩小问题范围,定位问题究竟是出在哪一方。但并不百分之百准确。




  1. 一些Tips:



    • 设备Server端在自定义特征值UUID时未遵循GATT的Attribute Structure,而蓝牙服务iOS的实现会比Android更严格。

    • 外围设备使用deviceId作为唯一标识,但iOS 和 Android在拿到的信息上有所差异。Android上获取到的deviceId为设备MAC地址,iOS上则是系统根据外围设备 MAC 地址及发现设备的时间生成的 UUID,因此deviceId不能硬编码。

    • 蓝牙模块比较耗费系统资源,做好生命周期管理必不可少,比如建立连接和断开连接应该成对出现,如果未能及时关闭连接释放资源,容易导致连接异常。另外,大多数蓝牙模组只支持单链路,最大连接数量为1,若未能及时断开连接,必然出现设备搜寻不到或连接不上的情况。




  2. 日志排查




作为小程序的开发者,很多疑难问题往往不能直观看出。如果你有对应的资源可以联系到开放平台的维护人员,即可拿到日志。我们项目组曾与飞书开放平台建立蓝牙专项问题解决渠道,结合开平和设备端同学捕获的日志,可以加快排查速度。


参考文章


http://www.bluetooth.com/learn-about…
http://www.cnblogs.com/chusiyong/p…
http://www.jianshu.com/p/62eb2f540…
zh.javascript.info/arraybuffer…


作者:HenryZheng
来源:juejin.cn/post/7221794170868351034
收起阅读 »

HTML5+CSS3小实例:闪亮的玻璃图标悬浮效果

web
HTML5+CSS3实现闪亮的玻璃图标悬浮效果,光与玻璃的碰撞,好有质感的玻璃图标。 先看效果: 源代码: <!DOCTYPE html> <html> <head> <meta http-equiv="c...
继续阅读 »

HTML5+CSS3实现闪亮的玻璃图标悬浮效果,光与玻璃的碰撞,好有质感的玻璃图标。


先看效果:



源代码:


<!DOCTYPE html>
<html>

<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">

<title>闪亮的玻璃图标悬浮效果</title>
<link href="https://cdn.bootcdn.net/ajax/libs/font-awesome/4.7.0/css/font-awesome.css" rel="stylesheet">
<link rel="stylesheet" href="../css/5.css">
</head>

<body>
<div class="container">
<div class="color"></div>
<div class="color"></div>
<div class="color"></div>
<ul>
<li>
<a href="#"><i class="fa fa-qq" aria-hidden="true"></i></a>
</li>
<li>
<a href="#"><i class="fa fa-weixin" aria-hidden="true"></i></a>
</li>
<li>
<a href="#"><i class="fa fa-weibo" aria-hidden="true"></i></a>
</li>
<li>
<a href="#"><i class="fa fa-tencent-weibo" aria-hidden="true"></i></a>
</li>
<li>
<a href="#"><i class="fa fa-telegram" aria-hidden="true"></i></a>
</li>
</ul>
</div>
</body>

</html>

*{
margin: 0;
padding: 0;
/* 这个是告诉浏览器:你想要设置的边框和内边距的值是包含在总宽高内的 */
box-sizing: border-box;
}
body{
/* 溢出隐藏 */
overflow: hidden;
}
.container{
position: absolute;
width: 100%;
/* 100%窗口高度 */
height: 100vh;
/* 弹性布局 水平垂直居中 */
display: flex;
justify-content: center;
align-items: center;
/* 渐变背景 */
background: linear-gradient(to bottom,#2193b0,#6dd5ed);
}
.container::before{
content: "";
position: absolute;
bottom: 0px;
width: 100%;
height: 50%;
z-index: 1;
/* 背景模糊 */
backdrop-filter: blur(5px);
border-top: 1px solid rgba(255,255,255,0.5);
}
.container .color{
position: absolute;
/* 模糊滤镜 数值越大越模糊 */
filter: blur(200px);
}
.container .color:nth-child(1){
background-color: #fd746c;
width: 800px;
height: 800px;
top: -450px;
}
.container .color:nth-child(2){
background-color: #cf8bf3;
width: 600px;
height: 600px;
bottom: -150px;
left: 100px;
}
.container .color:nth-child(3){
background-color: #fdb99b;
width: 400px;
height: 400px;
bottom:50px;
right:100px;
}
ul{
position: relative;
display: flex;
z-index: 2;
}
ul li{
position: relative;
list-style: none;
margin: 10px;
}
ul li a{
position: relative;
width: 80px;
height: 80px;
display: inline-block;
border-radius: 10px;
display: flex;
justify-content: center;
align-items: center;
text-decoration: none;
color: #fff;
font-size: 32px;
border: 1px solid rgba(255,255,255,0.4);
border-right: 1px solid rgba(255,255,255,0.2);
border-bottom: 1px solid rgba(255,255,255,0.2);
/* 阴影 */
box-shadow: 0px 5px 45px rgba(0,0,0,0.1);
/* 背景模糊 */
backdrop-filter: blur(2px);
/* 动画过渡 */
transition: all 0.5s;
overflow: hidden;
}
ul li a:hover{
/* 鼠标移入元素沿Y轴上移 */
transform: translateY(-20px);
}
ul li a::before{
content: "";
position: absolute;
top: 0px;
left: 0px;
width: 50px;
height: 100%;
background-color: rgba(255,255,255,0.5);
/* 元素沿X轴45度横切,沿X轴右移150px */
transform: skewX(45deg) translateX(150px);
/* 动画过渡 */
transition: all 0.5s;
}
ul li a:hover::before{
/* 元素沿X轴45度横切,沿X轴左移150px */
transform: skewX(45deg) translateX(-150px);
}

作者:艾恩小灰灰
来源:juejin.cn/post/7091339314352619557
收起阅读 »

我调用第三方接口遇到的13个坑

前言 在实际工作中,我们经常需要在项目中调用第三方API接口,获取数据,或者上报数据,进行数据交换和通信。 那么,调用第三方API接口会遇到哪些问题?如何解决这些问题呢? 这篇文章就跟大家一起聊聊第三方API接口的话题,希望对你会有所帮助。 1 域名访问不到...
继续阅读 »

前言


在实际工作中,我们经常需要在项目中调用第三方API接口,获取数据,或者上报数据,进行数据交换和通信。


那么,调用第三方API接口会遇到哪些问题?如何解决这些问题呢?


这篇文章就跟大家一起聊聊第三方API接口的话题,希望对你会有所帮助。


图片


1 域名访问不到


一般我们在第一次对接第三方平台的API接口时,可能会先通过浏览器或者postman调用一下,该接口是否可以访问。


有些人可能觉得多次一举。


其实不然。


有可能你调用第三方平台的API接口时,他们的接口真的挂了,他们还不知道。


还有一种最重要的情况,就是你的工作网络,是否可以访问这个外网的接口。


有些公司为了安全考虑,对内网的开发环境,是设置了防火墙的,或者有一些其他的限制,有些ip白名单,只能访问一些指定的外网接口。


如果你发现你访问的域名,在开发环境访问不通,就要到运维同学给你添加ip白名单了。


2 签名错误


很多第三方API接口为了防止别人篡改数据,通常会增加数字签名(sign)的验证。


sign = md5(多个参数拼接 + 密钥)


在刚开始对接第三方平台接口时,会遇到参数错误,签名错误等问题。


其中参数错误比较好解决,重点是签名错误这个问题。


签名是由一些算法生成的。


比如:将参数名和参数值用冒号拼接,如果有多个参数,则按首字母排序,然后再将多个参数一起拼接。然后加盐(即:密钥),再通过md5,生成一个签名。


如果有多个参数,你是按首字母倒序的,则最后生成的签名会出问题。


如果你开发环境的密钥,用的生产环境的,也可能会导致生产的签名出现问题。


如果第三方平台要求最后3次md5生成签名,而你只用了1次,也可能会导致生产的签名出现问题。


因此,接口签名在接口联调时是比较麻烦的事情。


如果第三方平台有提供sdk生成签名是最好的,如果没有,就只能根据他们文档手写签名算法了。


3 签名过期


通过上面一步,我们将签名调通了,可以正常访问第三方平台获取数据了。


但你可能会发现,同一个请求,15分钟之后,再获取数据,却返回失败了。


第三方平台在设计接口时,在签名中增加了时间戳校验,同一个请求在15分钟之内,允许返回数据。如果超过了15分钟,则直接返回失败。


这种设计是为了安全考虑。


防止有人利用工具进行暴力破解,不停伪造签名,不停调用接口校验,如果一直穷举下去的话,总有一天可以校验通过的。


sign = md5(多个参数拼接 + 密钥 + 时间戳)


因此,有必要增加时间戳的校验。


如果出现这种情况,不要慌,重新发起一次新的请求即可。


4 接口突然没返回数据


如果你调用第三方平台的某个API接口查询数据,刚开始一直都有数据返回。


但突然某一天没返回数据了。


但是该API接口能够正常响应。


不要感到意外,有可能是第三方平台将数据删除了。


我对接完第三方平台的API接口后,部署到了测试环境,发现他们接口竟然没有返回数据,原因是他们有一天将测试环境的数据删完了。


因此,在部署测试环境之前,要先跟对方沟通,要用哪些数据测试,不能删除。


5 token失效


有些平台的API接口在请求之前,先要调用另外一个API接口获取token,然后再header中携带该token信息才能访问其他的业务API接口。


在获取token的API接口中,我们需要传入账号、密码和密钥等信息。每个接口对接方,这些信息都不一样。


我们在请求其他的API接口之前,每次都实时调用一次获取token的接口获取token?还是请求一次token,将其缓存到redis中,后面直接从redis获取数据呢?


很显然我们更倾向于后者,因为如果每次请求其他的API接口之前,都实时调用一次获取token的接口获取token,这样每次都会请求两次接口,性能上会有一些影响。


如果将请求的token,保存到redis,又会出现另外一个问题:token失效的问题。


我们调用第三方平台获取token的接口获取到的token,一般都有个有效期,比如:1天,1个月等。


在有效期内,该API接口能够正常访问。如果超过了token的有效期,则该API接口不允许访问。


好办,我们把redis的失效时间设置成跟token的有效期一样不就OK了?


想法是不错,但是有问题。


你咋保证,你们系统的服务器时间,跟第三方平台的服务器时间一模一样?


我之前遇到过某大厂,提供了获取token接口,在30天内发起请求,每次都返回相同的token值。如果超过了30天,则返回一个新的。


有可能出现这种情况,你们系统的服务器时间要快一些,第三方平台的时间要慢一些。结果到了30天,你们系统调用第三方平台的获取token接口获取到了token还是老的token,更新到redis中了。


过一段时间,token失效了,你们系统还是用老的token访问第三方平台的其他API接口,一直都返回失败。但获取新的token却要等30天,这个时间太漫长了。


为了解决这个问题,需要捕获token失效的异常。如果在调用其他的API接口是发现token失效了,马上请求一次获取token接口,将新的token立刻更新到redis中。


这样基本可以解决token失效问题,也能尽可能保证访问其他接口的稳定性和性能。


6 接口超时


系统上线之后,调用第三方API接口,最容易出现的问题,应该是接口超时问题了。


系统到外部系统之间,有一条很复杂的链路,中间有很多环节出现问题,都可能影响API接口的相应时间。


作为API接口的调用方,面对第三方API接口超时问题,除了给他们反馈问题,优化接口性能之外,我们更有效的方式,可能是增加接口调用的失败重试机制


例如:


int retryCount=0;
do {
   try {
      doPost();
      break;
   } catch(Exception e) {
     log.warn("接口调用失败")
     retryCount++;
   }
where (retryCount <= 3)

如果接口调用失败,则程序会立刻自动重试3次


如果重试之后成功了,则该API接口调用成功


如果重试3次之后还是失败,则该API接口调用失败


7 接口返回500


调用第三方API接口,偶尔因为参数的不同,可能会出现500的问题。


比如:有些API接口对于参数校验不到位,少部分必填字段,没有校验不能为空。


刚好系统的有些请求,通过某个参数去调用该API接口时,没有传入那个参数,对方可能会出现NPE问题。而该接口的返回code,很可能是500。


还有一种情况,就是该API接口的内部bug,传入不同的参数,走了不同的条件分支逻辑,在走某个分支时,接口逻辑出现异常,可能会导致接口返回500。


这种情况做接口重试也没用,只能联系第三方API接口提供者,反馈相关问题,让他们排查具体原因。


他们可能会通过修复bug,或者修复数据,来解决这个问题。


8 接口返回404


如果你在系统日志中发现调用的第三方API接口返回了404,这就非常坑了。


如果第三方的API接口没有上线,很可能是他们把接口名改了,没有及时通知你。


这种情况,可以锤他们了。


还有一种情况是,如果第三方的API接口已经上线了,刚开始接口是能正常调用的。


第三方也没有改过接口地址。


后来,突然有一天发现调用第三方的API接口还是出现了404问题。


这种情况很可能是他们网关出问题了,最新的配置没有生效,或者改了网关配置导致的问题。


总之一个字:坑。


9 接口返回少数据了


之前我调过一个第三方的API接口分页查询数据,接入非常顺利,但后来上线之后,发现他们的接口少数据了。


一查原因发现是该分页查询接口,返回的总页数不对,比实际情况少了。


有些小伙伴可能会好奇,这么诡异的问题我是怎么发现?


之前调用第三方API接口分页查询分类数据,保存到我们的第三方分类表中。


突然有一天,产品反馈说,第三方有个分类在分类树中找不到。


我确认之后,发现竟然是真的没有。


从调用第三方API接口的响应日志中,也没有查到该分类的数据。


这个API接口是分页查询接口,目前已经分了十几页查询数据,但还是没有查到我们想要的分类。


之前的做法是先调用一次API接口查询第一页的数据,同时查出总页数。然后再根据总页数循环调用,查询其他页的数据。


我当时猜测,可能是他们接口返回的总页数有问题。


于是,可以将接口调用逻辑改成这样的:



  • 从第一页开始,后面每调用一次API接口查数据,页数就加1。然后判断接口返回的数据是否小于pageSize,

  • 如果不小于,则进行下一次调用。

  • 如果小于,则说明已经是最后一页了,可以停止后续调用了。


验证之后发现这样果然可以获取那个分类的数据,只能说明第三方的分页查询接口返回的总页数比实际情况小了。


10 偷偷改参数了


我之前调用过某平台的API接口获取指标的状态,之前根据双方约定的状态有:正常禁用 两种。


然后将状态更新到我们的指标表中。


后来,双方系统上线运行了好几个月。


突然有一天,用户反馈说某一条数据明明删除了,为什么在页面上还是可以查到。


此时,我查我们这边的指标表,发现状态是正常的。


然后查看调用该平台的API接口日志,发现返回的该指标的状态是:下架


what?


这是什么状态?


跟该平台的开发人员沟通后,发现他们改了状态的枚举,增加了:上架、下架等多个值,而且没有通知我们。


这就坑了。


我们这边的代码中判断,如果状态非禁用状态,都认为是正常状态。


而下架状态,自动被判断为正常状态。


经过跟对方沟通后,他们确认下架状态,是非正常状态,不应该显示指标。他们改了数据,临时解决了该指标的问题。


后来,他们按接口文档又改回了之前的状态枚举值。


11 接口时好时坏


不知道你在调用第三方接口时,有没有遇到过接口时好时坏的情况。


5分钟前,该接口还能正常返回数据。


5分钟后,该接口返回503不可用。


又过了几分钟,该接口又能正常返回数据了。


这种情况大概率是第三方平台在重启服务,在重启的过程中,可能会出现服务暂时不可用的情况。


还有另外一种情况:第三方接口部署了多个服务节点,有一部分服务节点挂了。也会导致请求第三方接口时,返回值时好时坏的情况。


此外还有一种情况:网关的配置没有及时更新,没有把已经下线的服务剔除掉。


这样用户请求经过网关时,网关转发到了已经下线的服务,导致服务不可用。网关转发请求到正常的服务,该服务能够正常返回。


如果遇到该问题,要尽快将问题反馈给第三方平台,然后增加接口失败重试机制。


12 文档和接口逻辑不一致


之前还遇到一个第三方平台提供的API查询接口,接口文档中明确写明了有个dr字段表示删除状态


有了这个字段,我们在同步第三方平台的分类数据时,就能够知道有哪些数据是被删除的,后面可以及时调整我们这边的数据,将相关的数据也做删除处理。


后来发现有些分类,他们那边已经删除了,但是我们这边却没删除。


这是啥情况呢?


代码逻辑很简单,我review了一下代码,也没有bug,为什么会出现这种情况呢?


追查日志之后发现,调用第三方平台获取分类接口时,对方并没有把已删除的分类数据返回给我们。


也就是说接口文档中的那个dr字段没有什么用,接口文档和接口逻辑不一致。


这个问题估计好多小伙伴都遇到过。


如果要解决这个问题,主要的方案有两种:



  1. 第三方平台按文档修改接口逻辑,返回删除状态。

  2. 我们系统在调用分类查询接口之后,根据分类code判断,如果数据库中有些分类的code不在接口返回值中,则删除这些分类。


13 欠费了


我们调用过百度的票据识别接口,可以自动识别发票信息,获取发票编号和金额等信息。


之前是另外一个同事对接的接口,后来他离职了。


发票识别功能上线,使用了很长一段时间,一直都没有出问题。


后来,某一天,生产环境用户反馈发票识别不了了。


我查询了相关服务的日志,没有发现异常,这就奇怪了。


打开代码仔细看了一下,发现那位同事的代码中调用第三方的API接口,接收响应数据时,直接转换成了对象,没有打印当时返回的字符串。


莫非,接口返回值有问题?


后来,我增加了日志,打印出了该接口真正的返回内容值。


原因一下查到了,原来是欠费了。


如果出现该了异常,百度的API接口返回的数据结构,用之前那位同事的实体有些参数没法获取到。


这是一个不小的坑。


我们在接收第三方API接口返回数据时,尽可能先用字符串接收返回值,然后将字符串转换成相应实体类,一定要将该返回值在日志中打印出来,方便后面定位问题。


不要直接用实体对象接收返回值,有些API接口,如果出现不同的异常,返回的数据结构差异比较大。


有些异常结果可能是他们网关系统直接返回的,有些异常是他们业务系统返回的。


其实,我们之前还遇到过其他坑,比如:调用分类树查询接口,但第三方返回的数据有重复的id,我们这边该如何处理这种异常数据呢?


我们在job中循环调用第三方API接口获取数据,如果其中某一次调用失败了,是try/catch捕获异常呢?继续执行后面的调用,还是直接终止当前的程序?如果try/catch如何保证数据一致性?终止当前程序,该如何处理后续的流程?


最后说一句(求关注,别白嫖我)


如果这篇文章对您有所帮助,或者有所启发的话,帮忙扫描下发二维码关注一下,您的支持是我坚持写作最大的动力。


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

前端获取电池信息

web
今日正能量: 当奇怪的需求越做越多的时候,证明你的眼光也正在变得广阔。 产品经理:加个需求,用户电脑设备如果快没电,我要暖心的告诉他该插上电源。 前端攻城狮:。。。他电脑不会自己提醒吗? 产品经理:你做不做? 前端攻城狮:做! 前言 随着技术的日益发展,w...
继续阅读 »

今日正能量: 当奇怪的需求越做越多的时候,证明你的眼光也正在变得广阔。



产品经理:加个需求,用户电脑设备如果快没电,我要暖心的告诉他该插上电源。


前端攻城狮:。。。他电脑不会自己提醒吗?


产品经理:你做不做?


前端攻城狮:做!


屏幕截图 2023-04-17 221002.png


前言


随着技术的日益发展,web前端技术远比我们想象的强大。浏览器允许网站获取用户设备的电池状态信息,例如电量百分比,剩余电量,充电状态等等。我们可以使用这些信息,根据用户设备的电量调整我们的应用行为。在这篇中,我们将探讨如何在前端中获取电池信息,用到的就是关于 Battery Status API。


Battery Status API的使用


Battery Status API 是一个 Web API,允许 Web 应用程序访问用户设备的电池状态信息。使用这个 API,我们可以在不安装任何应用程序的情况下,从 Web 浏览器直接读取设备的电量信息。


获取设备电池信息的主要步骤如下:


// 请求电池信息
navigator.getBattery().then(function (battery) {
// 后续代码
})

将返回一个 Promise 对象,它会解析为一个 BatteryManager 对象,我们可以使用它来读取设备的电池属性。


navigator.getBattery().then(function (battery) {
// 获取设备电量剩余百分比
var level = battery.level //最大值为1,对应电量100%
console.log('Level: ' + level * 100 + '%')

// 获取设备充电状态
var charging = battery.charging
console.log('充电状态: ' + charging)

// 获取设备完全充电需要的时间
var chargingTime = battery.chargingTime
console.log('完全充电需要的时间: ' + chargingTime)

// 获取设备完全放电需要的时间
var dischargingTime = battery.dischargingTime
console.log('完全放电需要的时间: ' + dischargingTime)
})

监听电池状态变化


为了更好地反映用户设备的电池状态,我们可以在前端中添加事件来监视电池状态的变化。例如,当设备的电池电量改变时,会触发事件。一些给大家列举几个常用事件:


navigator.getBattery().then(function (battery) {
// 添加事件,当设备电量改变时触发
battery.addEventListener('levelchange', function () {
console.log('电量改变: ' + battery.level)
})

// 添加事件,当设备充电状态改变时触发
battery.addEventListener('chargingchange', function () {
console.log('充电状态改变: ' + battery.charging)
})

// 添加事件,当设备完全充电需要时间改变时触发
battery.addEventListener('chargingtimechange', function () {
console.log('完全充电需要时间: ' + battery.chargingTime)
})

// 添加事件,当设备完全放电需要时间改变时触发
battery.addEventListener('dischargingtimechange', function () {
console.log('完全放电需要时间: ' + battery.dischargingTime)
})
})

兼容性


兼容性方面,Battery Status API 并不适用于所有的设备和操作系统,开发人员需要进行兼容性处理,以确保我们的应用可以在所有的设备上运行。以下是该API对应的兼容性视图:


屏幕截图 2023-04-17 220020.png


通过 Battery Status API 获取设备电池信息是一种很强大的方法,可以根据设备电池状态来优化应用程序的行为。需要注意的是,此 API 不适用于所有设备和操作系统,并且某些设备生产商可能不允许共享电池信息。


作者:白椰子
来源:juejin.cn/post/7222996459833622565
收起阅读 »

你可能想了解的开源开发者两年经历

前情提要 2021年初时写过一篇《三年 三本 BAT 要素齐全 | 2021年终总结》 的年终总结,正如上文所说,我离开了广州来到深圳鹅厂,阴差阳错之下我从一个业务切图仔变成了一个开源开发者。 彼时我的心情是那么忐忑,我向往开源工作,但又害怕自己不成熟的想法会...
继续阅读 »

前情提要


2021年初时写过一篇《三年 三本 BAT 要素齐全 | 2021年终总结》 的年终总结,正如上文所说,我离开了广州来到深圳鹅厂,阴差阳错之下我从一个业务切图仔变成了一个开源开发者。


彼时我的心情是那么忐忑,我向往开源工作,但又害怕自己不成熟的想法会给别人带来负担,而不敢向开源仓库提PR。


两年下来我已经习惯了常态化的开源开发生活,是 libpag 的核心开发兼 Web 端 owner。但因为生活的变动,我将要离开现在的团队,所以想在最后记录一下这两年的开源开发经历。



本文不会有很多技术细节的分享,更多的是经历的分享与一些建议。



参与


2021年中加入PAG团队,从了解 PAG 的工作流,PAG 文件格式开始参与到团队的开发工作中,慢慢地完成了Web上简化版本的渲染SDK,再到基于 WebAssembly + WebGL 架构适配了 Web 平台,完成了 libpag 在主流平台的最后一块版图。


时间来到2022年1月14日, libpag 完成 4.0 重构与 Web 平台的适配后在 GitHub 上正式对外开源,也完成了从内部开源到外部开源的转变。


讲到这里我希望能给想要参与开源的同学一些建议,开源团队都是希望有尽可能多的人能够参与到开源社区中来,所以不要害怕提PR。 开发者一般都会把开发流程和注意点记录在 README 中,只要仔细阅读完仓库中相关的文档,就可以大胆地参与共建。当有不确定的想法时,也可以先提一个 issue 与开源团队探讨,确定思路之后,提出 PR,开源团队 Code Review 有问题时也会提出 comment,修改问题无误之后就会合入主干了。


参与开源库的共建可以给你的简历留下浓墨重彩的一笔,对找工作也是有一定帮助的。(打工人狂喜


蜕变


从2022年1月14日对外开源以来,业务接入量也从最初的40个迅速扩大超过 600 +,其中包含微信、手Q、王者荣耀、小红书、知乎、B站、京东等知名APP。


随着对接业务数量的增加,大家的交流也多起来,搭建了专门用于交流的PAG 论坛 bbs.pag.art/,团队的工时也有很大的一部分开始被对接工作所占用,同时我们也收到了很多不同的场景需求。


根据开发者们提供的这些不同的场景需求,我也规划了 Web 端的 Roadmap,比如 4.1 版本中完成了微信小程序的适配,4.2 版本中支持了 WebWorker,还有数不完的优化。


寒冬


随着互联网寒冬的到来,降本增效的浪潮开始席卷各大公司。在这个环境下对于开源工作的开展是十分困难的,人员的骤减让以往免费的即时对接无法继续延续。成本、收益等问题被重新拿上台面,开源几乎只剩下为爱发电。为了生存,也为了还有机会继续维护这个开源仓库,大家都被迫走上商业化的道路。


无关团队,说一些自己的看法。其实国内的开源环境并不好,虽然有很多优秀的开发者前仆后继地投身其中,不停的优化着开源这块土壤。当维持社区 SDK 功能不变,探索一些新的商业化道路谋求生存时,就会有一些奇怪的声音出现。“准备捞钱了”、“吃相难看”等等,但其实开源 SDK 并没向使用者收费过一分钱,开发者也希望在不影响社区的情况下谋求生存。


希望在这个寒冬里,大家少一点戾气,多一点包容,都是为了生存。


后记


原本想写最近很火“前端已死”的话题,聊聊这个寒冬中面试的建议,还想写写 libpag Web源码的解析。


但最后还是选择在这个时间点,写一写关于开源工作的一些记录。


如果你对其他话题有兴趣,欢迎点赞评论,请求加更。


作者:zenoslin
链接:https://juejin.cn/post/7222509109949022269
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

我又写了一堆烂代码

“我又写了一堆烂代码!” 这句话我经常对自己说,目的是为了督促自己不断地思考所写的代码是否足够可靠。 不要觉得代码通过了 QA 的测试就万事大吉了,如果你为了实现某个功能,而破坏了原有系统的规则,或者让代码变得耦合,那么这就是一件糟糕的事情。 我通常在实现某个...
继续阅读 »

“我又写了一堆烂代码!”


这句话我经常对自己说,目的是为了督促自己不断地思考所写的代码是否足够可靠。


不要觉得代码通过了 QA 的测试就万事大吉了,如果你为了实现某个功能,而破坏了原有系统的规则,或者让代码变得耦合,那么这就是一件糟糕的事情。


我通常在实现某个功能后,我都会问自己几个问题。


第1个问题:我刚刚做的工作是让整个系统更容易改变还是更难改变?


优秀的代码设计都满足一个原则:ETC原则。即 Easier To Change,更容易变更。


我们学过的任何其他设计原则,其实都是 ETC 的一个特例。



为什么解耦很好?因为通过隔离关注焦点,可以让每一部分都容易变更——此谓ETC。
为什么单一职责原则很有用?因为一个需求变化仅体现为某个单一模块上的一个对应变化——此谓ETC。
为什么命名很重要?因为好的命名可以使代码更容易阅读,而你需要通过阅读来变更代码——此谓ETC!



当你在思考自己写的代码是否可靠时,ETC 就是一个很好的向导。


如果需求变更,你的代码是否能轻易的做出改变,以适应这种变化,甚至是可以被替代。如果不能,那么你的代码就成了一种障碍。


所以,ETC 它更像是一种价值观念,能够帮你在不同的方案中,选出正确的那一个。同时,也能让你的代码不断进化。


第2个问题:有没有为了偷懒,写了很多重复的表达?


要让项目更容易理解和维护,唯一的方法是遵循这条原则:系统中的每一部分,都必须有一个单一的、明确的、权威的代表


这个原则被称为 DRY 原则(Don't repeat yourself 不要重复你自己)。也可以叫 OAOO(Once and only once 一次且仅一次)。


来看一个违反 DRY 原则的例子:


void calculate(int money){
if (money >= 400) {
print('打折后的价格为¥:${money*0.7} RMB');
}else if(money >= 300){
print('打折后的价格为¥:${money*0.8} RMB');
}else if(money >= 200){
print('打折后的价格为¥:${money*0.9} RMB');
}else{
print('打折后的价格为¥:${money} RMB');
}
}
复制代码

这个例子就是典型的重复。


假如价格的单位变更了,改成卢布,或者其他货币,那一共需要修改 4 处 RMB。


再比如,'打折后的价格为¥' 这个文案有调整,改为 '折后价',那么也需要修改 4 处的内容。


所以,平时写代码时,一定要避免这样的写法。


第3个问题:我写的代码有没有破坏正交性?


“正交性”是几何学中的概念。若两条直线相交后构成直角,它们就是正交的。


“正交性”在计算科学中,表示独立性或解耦性。


对于两个或多个事物,其中一个的改变不影响其他任何一个,则这些事物是正交的。


举个例子,在项目中,如果你改动了 UI 相关的代码,而不影响其他业务逻辑(比如数据库操作,网络访问逻辑等等),那么这样的系统,就属于设计良好的系统。


相反,如果你发现自己改动其中一处的代码,会影响很多地方,那么你就得思考一下,是否需要重构代码了。


一般来说,保持代码的正交性有以下几个实用的方法:



  • 使用最少知识原则(迪米特法则)来保持代码的解耦性。

  • 避免全局数据。只要代码引用了全局数据,就会将自己绑定到共享该数据的其他组件上。


希望你也养成不断质疑代码的习惯。只要有机会就重新组织、改善其结构和正交性(重构)。



作者:陈有余Tech
链接:https://juejin.cn/post/7107423109434736653
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

低代码平台是否会取代程序员?答案在这里

上图是一张机器人或者自动化系统取代人工的图片,您看了有哪些感想呢? 故事 程序员小张: 刚毕业,参加工作1年左右,日常工作是CRUD 架构师老李: 多个大型项目经验,精通各种屠龙宝术; 程序员小张和架构师老李是同事,在一家科技公司工作。他们的团队负责开发和...
继续阅读 »

上图是一张机器人或者自动化系统取代人工的图片,您看了有哪些感想呢?


故事


file


程序员小张: 刚毕业,参加工作1年左右,日常工作是CRUD


file


架构师老李: 多个大型项目经验,精通各种屠龙宝术;


程序员小张和架构师老李是同事,在一家科技公司工作。他们的团队负责开发和维护公司核心数字系统,但最近他们经常因为应对新需求而焦头烂额。


有一天,老李向小张提出了使用低代码平台来加快应用开发的建议。小张听完后不太理解,认为低代码平台会取代程序员,使开发工作变得简单和机械化。


老李解释说低代码平台并不会取代程序员,而是让专业的开发人员从简单、重复的开发需求中解放出来,让他们能更好地投入到更有价值的事情上,比如梳理系统架构、理清业务逻辑等等,并且低代码平台仍然需要由程序员开发和维护。


小张听后开始思考,认为这个建议确实有道理。他决定利用低代码平台来创建一些敏捷运营类应用,以减轻自己的工作量,并利用更多时间进行系统分析和优化。


于是,小张按照老李的建议利用低代码平台创建了一款敏捷运营类应用,并与老李一起进行了测试和上线。在此过程中,他们深刻领悟到低代码平台的优势,如快速开发、易于维护和扩展等等。


最终,他们成功地为公司创造了一个具有高效性和可扩展性的应用程序,并受到了同事们的好评。小张也明白了使用低代码平台并不是把程序员取代掉,而是让他们更好地利用时间和技能,为团队带来更多价值。


问题


越来越多的企业进行数字化转型,信息化升级。


系统的统一性和集成一直是一个难题,传统模式需要通过多套系统才能满足企业整体的信息化需求,想要统一打通非常困难。


程序员的高薪一直是企业高昂的成本,当出现新的生产工具,必定会对生产关系产生一定影响。


低代码在企业应用开发中的场景?


市场上有哪些种类的低代码平台呢?


低代码到底是怎么降本增效的?


低代码取代程序员怎么破局?


低代码


低代码平台的热度可谓风头无两,


那么低代码是什么呢?简单给它下一个定义。


通过可视化、模块化、拖拽式代替了传统的开发方式,达到了“降本增效”的目的,加速了企业数字化转型的进程的工具平台;


file


1 低代码的使用场景有哪些?


一般来说,核心的业务系统不会使用低代码从0到1搭建,低代码适合搭建基于核心数字系统之上的创新类应用,敏捷运营类应用。
这类应用使用低代码可以极大提升技术研发和系统交付的整体效率。
低代码当前处在技术成熟度曲线中的创新阶段,后面模板会越来越丰富,生态越来越完善,低代码的适用范围也会不断扩大。
下面五类应用是比较适合用低代码来实现的。





































种类说明点评
企业门户包括App、小程序、PC门户等等,数据都来自中台、后台,企业门户只是做展示,以及简单的互动。xxcube正在覆盖
数据操作和展示应用通过连接企业的数据库,把生产经营的数据进行编辑删除查询等操作。可预研
基于表单的应用基于数据库的表单收集、处理、统计类应用。xxcube正在覆盖
业务流程类应用定义复杂的工作流,跨部门协作流程,复杂审批流程,比如:OA、人力、财务等系统。xxcube正在覆盖
移动端应用基于已有核心生产经营系统,进行移动化的应用场景。xxcube正在预研和了解

以上5类应用已经覆盖了企业数字化的80%的场景了,低代码的使用范围非常的广泛。


3 低代码平台的分类


目前低代码属于蓝海市场,以中国IT企业的创新速度,未来的竞争会非常激烈。
按照低代码的应用场景,可分为4类。列表对比一下。
































种类说明xxcube的对比
原生低代码即面向通用型企业软件的低代码厂商,你可以从0构建一个企业经营系统,使用低代码平台将它实现出来。代表企业: 数睿数据、奥哲、轻流,国外的Mendix、OutSystems等等。具备能力
云平台低代码软件作为云平台生态其中的一环,是云原生一体化的企业解决方案,企业云平台的能力补充。比如阿里云(宜搭,氚云,简道云),腾讯云(微搭),微软(Power Platform)具备能力
行业软件低代码平台本身是非常成熟的行业软件,他们积极拥抱低代码,借助低代码解决企业个性化的需求,拓展软件应用场景。金蝶(企业管理,财务)、明道云(地产物业)、网易数帆(游戏)、销售易(CRM)、Airtable。聚焦游戏发行,游戏开发细分领域
软件开发工具做软件开发工具的,借助低代码实现功能模块化,帮助开发人员进行快速开发。Zion、葡萄城、ClickPaaS具备低代码扩展能力

3 低代码是怎么降本增效的?


file


**低代码,将传统软件开发的6个环节,缩短到了3个环节。**即通过把“需求设计”、“架构设计”、“编码”,聚合为“模块搭建”。


简单来说,低代码的开发模式,就是在需求梳理清楚后,用“拖拉拽”的方式把功能实现出来。


以一个10人天的软件开发需求为例:


传统软件开发模式,总共需要5个人,即:1个产品、2个开发、1个测试、1个运维,开发周期8天。


低代码开发模式,总共需要3人,即:1个产品,1个开发,1个测试,开发周期2天。


也就是说,使用低代码开发模式,同样的需求,节省开发周期70%,减少技术人员40%。


这只是一个粗略的估算。总之,低代码能够用更短的时间、更低的成本,实现软件产品的交付。


4 低代码平台的出现真的会取代程序员吗?


答:不会取代程序员,低代码的优势是可以让专业的开发人员从简单、重复的开发需求中解放出来,把精力投入到更有价值的事情上,比如梳理系统架构、理清业务逻辑等等。
首先,低代码平台不也是程序员开发的吗?再说了,使用低代码进行软件开发的,不还是包含程序员吗?
低代码虽然是“低”,也还是有代码的,除非是无代码平台,在适合的业务场景之下,确实不需要程序员了,可以由业务人员直接搭建应用,但那毕竟是特定场景。
低代码虽然不会干掉程序员,但是对程序员、产品设计人员提出了新的能力要求。越来越多的程序开发工作,将会通过低代码来完成。


技术人员必须到一线中去,跟业务人员在一起,跟最终用户呆在一起,打造更便捷易用的软件产品给业务赋能,用新的科技手段帮助业务转型、业务创新,使企业具备敏捷反应的能力。
否则,企业将会很快被时代抛弃,连打声招呼的机会都没有。


对广大程序员来说,可以按照下面的要点进行应对。
































招数说明自查
1. 警惕重复性编码工作CURDboy请多学一些硬核技术。比如梳理系统架构、理清业务逻辑。低代码已经能够完成复杂业务流程类开发工作,能够基于表单驱动、模型驱动的方式进行软件开发工作。最先被替代的就是那些技术含量低的重复性编码工作。聚焦架构设计,云原生,物联网,大数据热门领域的学习和实践
2. 错位竞争,在低代码不擅长的领域深耕对于界面效果要求特别高、复杂的算法和数据挖掘、高性能和复杂系统架构、要求较高的底层开发等方面工作还不能胜任。低代码首先模板,生态,可扩展性限制。聚焦架构设计,云原生,物联网,大数据热门领域的学习和实践
3. 重视企业数字化的建设方法论学习随着低代码的不断成熟,越来越多的业务系统研发工作将由低代码平台来完成,很多企业的老板会看到这块的价值企业运营和建设相关的知识学习中
4. 终生学习,时刻准备职业B计划顺应潮流,学习最先进最高效的生产工具和技术,往往可以对同一赛道的保守选手降维打击;职业B计划是应对职场风险的最佳途径;持续学习技术和管理,持续输出,职业B计划酝酿中

一个心酸的故事,希望能给广大程序员一点启发:



在工业革命时代早期,有工厂主发现纺织机经常在夜里遭到破坏,就派人躲在角落里观察,看看到底是谁在使坏。因为当时没有监控摄像,只能用人肉监控。
结果他们发现,是一些小作坊的纺织工人在搞破坏,原因是他们认为先进的纺织机抢走了他们的工作,而织布是他们赖以生存的手艺,不甘心这几十年修炼出来的一身本领,一夜之间被一台机器所取代,所以破坏机器泄愤。



现在的chatgpt也是影响力类似低代码的先加生产工具,作为程序员应该要主动拥抱他,利用它;



























工业时代IT时代启发
工厂IT公司资本或者资本机构: IT时代要重视低代码,作为程序员,要积极拥抱低代码,这是新的生产工具,可以成为自己的武器。低代码时代已来,不必焦虑,冲过去大力拥抱它,相信这一次,时间会站在变革者这一边。一些行业资深人士,视低代码为“毒瘤”,不仅暴露了自身的无知,也误导了部分从业者,也引起了无谓的恐慌。
纺织机低代码生产工具-静态
工人程序员\业务人员生产工具-动态

作者:李福春
链接:https://juejin.cn/post/7222832297854287929
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

裸辞一个月备考事业编所历所思

3.7日正式离职离现在也一个月了,这一个月里人类该有的情绪或大都尝了一遍。浅聊一下这一个月里干的事及一些感受。 原计划与现实 原本计划三月中旬提离职,四月 深圳-〉重庆-〉云南 半月游,五月开始刷题、下旬回到武汉。 计划永远赶不上变化。 2.23像往常一样坐在...
继续阅读 »

3.7日正式离职离现在也一个月了,这一个月里人类该有的情绪或大都尝了一遍。浅聊一下这一个月里干的事及一些感受。


原计划与现实


原本计划三月中旬提离职,四月 深圳-〉重庆-〉云南 半月游,五月开始刷题、下旬回到武汉。


计划永远赶不上变化。


2.23像往常一样坐在工位吃早餐。打开手机微信,看到一条订阅关于湖北事业单位招考的。于是,点进去看了看报考单位,那时就开始在想要不要试试。好嘛,我和一些朋友说了这个打算,好几个也一起加入备考大军。


第二天周五我调休了。算上周末三天,我开始了解事业单位考试及确定报考的岗位(实则,啥也不知道,随便选了一个我们小县城的岗位试试看)。


周一去到公司就提了离职。最后走的那天晚上请了几个同事吃了散伙饭,道了告别。


离职.png



我:拜拜,下辈子再见~ (人生中很多人再见就是再也不见了)



IMG_1089.png


备考


报名.PNG



  • 前期


离职后,当然是要给自己放松一下。于是,给自己放纵了两天。


接着开始备考。朋友分享了一套粉笔课程,每天也就看1-3个职测基础视频。


开始还是感觉蛮新鲜的。特别是一些逻辑、图形推理。哇,原来有这些套路。还记得以前面试一家公司让我做的题和这些差不多,当时觉得这都是些啥啊,和我做前端有关系吗。


其实期间除了看视频学习,其他时间基本都是在刷手机。那段时间B站推给我的全是大龄找不到工作、工作不好找等等让人致郁的视频。每天除了不专心学习就是无止尽的焦虑感。



  • 中期


后面把理论攻坚里的职测看完之后,更加放飞自我,每天也就打开粉笔app做几道题,然后就去手机里吸收消极情绪。很好笑,每天都在继续做前端、转行、摆摊、回家种地无限循环,当然包括现在偶尔也会是这样。


第一天:


等我考完这个破试回去武汉还是先找前端工作看看。


第二天:


刷了下B站,刷到说程序员找不到工作了,大龄了更加没人要了。那就考完试回来把后期视频好好学习下,回去找个剪辑师工作试试。


第三天:


打开Boss,搜索剪辑师;很多都是招流媒体,然后要求:会剪辑、会策划、会写脚本、会拍摄、会运营,还需要有工作经验。


又是那个无解的问题。


公司:我们需要有经验的。


我:我就是没有经验才找工作积累经验啊。


总结:转行不易。


第四天:


发现现在摆摊很火,去B站刷别人摆摊分享。了解了很多,常见的烤肠,还有之前没听过的热奶宝。这里可以推荐一个up主蜻园,还蛮不错的。


第五天:


算了,先考完试再说吧,实在不行就回家种地。刷B站,哎,这个剧还不错搜索全集cut,花几天看完。


这段时间差不多一个多星期,情绪就在这些上面循环往复,每天凌晨1、2点开始睡,但是得翻来覆去,差不多4、5点才能睡着,第二天起来已经中午了,再做个饭,差不多下午了,一天也差不多了。每天都是致郁的一天。



  • 后期


要考试了,得突击一下,把考前冲刺视频看了看,一天做几题。裸考综应和公共基础,其他随缘吧。


备考.png


参加考试



  • 去到酒店
    IMG_1258.png
    酒店备考.png


IMG_1271.png



  • 考试当天


考试的人真多,看起来很多都不是很大。


考试当天.png


考试当天听到一个女生对另外一个说:考试前几天整晚整晚都睡不着。(现在的人都不容易啊,很多大学生毕业即失业,都在卷考研、考编;白天准备考研,晚上准备考编。)


去到考场,安检然后需要手机关机,真是尴尬,第一次用苹果手机不会关机,当时迟迟关不了机,监考老师一度怀疑我有问题。


说:哎,你怎么回事,关机关这么久。


最后我问了旁边考生怎么关机,然后老师教了我,接着说:不会你早点说啊,一点都不谦虚。


我尴尬而不失礼貌的微笑。


快要考试了,我去上了个厕所,毕竟连续考三个半小时,时间也紧张,压根做不完,上厕所时间都得把握的好(喂,能不能在学习上多花点功夫哈)。


离谱,去厕所路上经过一个教室,外面三个女生都还在看书复习,我上完厕所出来,还在看,然后监考老师对她们说,快进来安检了,不要看了。


进考场看过来,可以看出很多女生都是那种很爱学习的人,就感觉我一个是来碰运气的。



  • 考试中


职测真做不完,之前和朋友说语文我完全不行,只能靠数学,结果数学计算相关压根没时间做。做题顺序不是按考卷顺序来的,开始也直接跳过常识题,直接言语开始啥的。


记得等我计算完资料分析的第一题后,我一看时间,我的妈,只剩下半小时了,数学运算直接放弃,当然不止数学运算,还有几十道都没有做,都是选择题,最后都是闭眼涂。


最无语的是我的综应都没有做完。作文十年没有写过了,我TM全抄的给的素材,离大谱;字写的也丑,唯一记得是以前老师对我说:一笔一画写清楚就行,不要连笔。感觉不连笔我字都不会写了,反正做的特慢,作文还大篇幅抄,最后来个总结,离大谱。



  • 考完试


考完试我就感觉自己是废物,感觉可以另谋生路了,这辈子估计是指望不上了。
考试结束.png


然后回酒店收东西,准备回家。退完房出来,下着瓢泼大雨,就像我的心情一样。


打了个的,准备去南站坐车,司机说好像只有北站有车去我那个地方;好嘛,又绕了老远了。在车上和司机聊了好多。


司机说也是回来考试的啊,然后说了我是从深圳回去考。


司机:这么远回来要是考不...没说完,算了,不说了,要是考的不好,还花了这么多钱。


我:笑了下,考不上就算了呗,还能怎么办。


然后说了很多,说她女儿也是搞计算机这行的,当时要她找个稳定的工作,不带编也行,她不干,现在在广州...叭叭叭,一起说了一路。


回家,晚上老妈回来一起聊了好多。很多时候我们焦虑,需要一个情绪缺口去宣泄。


返程


考完这次试,再加和老妈聊的许多,完全没有玩玩休息下的心情,第二天就准备返回深圳。


到深圳后就把自己后期AE中级课程开启了,开始学习(AE好难啊)。


打个小/广/告,这段时间把PR初/中级,AE初级视频录屏上传百度云完成;pr高级在录,AE中级在学在录。有兴趣的可以联系我,骨折价出售。


后面准备一边上课学习一边背前端八股文,偶尔出去拍拍照,积累些剪辑素材,之后回湖北后剪视频纪念用。


最后


最后想说:想的多了都是问题,做的多了都是答案。当你一直在消极情绪里时,可以找一些喜欢的事情去做,从不好的情绪里脱离出来,不必想的过多、过远,毕竟65岁退休都“太早了”。


作者:前端Y酱
链接:https://juejin.cn/post/7221131892427014205
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

用QQ创建自己的ChatGPT聊天机器人

前言     准备好以下东西:     一个活跃QQ账号,一个ChatGPT账号,一台服务器(操作系统为Debian 11.1 64bit)。 服务器安装koishi 1. 将以下...
继续阅读 »

前言


    准备好以下东西:


    一个活跃QQ账号,一个ChatGPT账号,一台服务器(操作系统为Debian 11.1 64bit)。


服务器安装koishi


1. 将以下命令依次执行,需要回复的,我们输入Y回车即可


sudo apt-get update

sudo apt-get install apt-transport-https

sudo apt-get install ca-certificates

sudo apt-get install curl

sudo apt-get install gnupg

curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg

echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

apt-get update

sudo apt-get install docker-ce docker-ce-cli containerd.io

docker run -p 5140:5140 koishijs/koishi

结果如下



2. 打开浏览器,输入 服务器IP:5140 ,例如127.0.0.1:5140



3. 接下来我们点击左侧的插件市场,依次搜索davincionebot,依次安装




4. 安装完成后点击左侧的插件配置按钮


    1)在adapter分组中 点击 adapter-onebot





    这一步如果登录不上需要换QQ号登录;验证方式一般选择短信验证



    2)在develop分组中 点击 davinci-003



    我们先打开 platform.openai.com/account/api… 打不开的需要自行解决


    



    创建完成之后点击复制,粘贴到如下编辑框中,之后我们点击运行



    如果需要更换触发命令的,修改这个地方



至此,机器人已经配置完成啦,快去尝试吧~


作者:飞鸟小方
链接:https://juejin.cn/post/7221868323105456165
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

ChatGPT真笨,连这都回答不上来...

ChatGPT的发布之后,有感叹它牛B的人,也有喷子喷它,觉得它依然还是个人工智障。这也不奇怪,我们在问很多问题的时候,它也都是一本正经的胡说八道。 我随手写几个,都能看到回答的不怎么样 但是这真的能说明它不行吗? 肯定是不对的,只是很多问题上,我们的提...
继续阅读 »

ChatGPT的发布之后,有感叹它牛B的人,也有喷子喷它,觉得它依然还是个人工智障。这也不奇怪,我们在问很多问题的时候,它也都是一本正经的胡说八道。


我随手写几个,都能看到回答的不怎么样
image.png


image.png


image.png


但是这真的能说明它不行吗?


肯定是不对的,只是很多问题上,我们的提问方式不对,毕竟AI本质上是一堆计算机程序。程序的逻辑和人思考的逻辑还是有很大的不同。


那么如何向ChatGPT提问呢?


首先明确第一点


1、搞清楚我们问题的类型


what?问题还有类型?


是的,对于AI来说,问题分为收敛型和发散型的,什么是收敛型的呢?比如说下面几个例子


1、 1 + 1 = ?
2、 左手有个苹果,右手有个苹果,那么两只手共有多少苹果?
3、 红豆生南国是谁写的?
4、 一个月可以超过40天吗?

我们很容易就可以发现,这类问题通常都有一个非常确定的答案,只是问题的难度不一样,推理的难度对于AI来说是比较高的。就像文章开头的第二个例子中的 ”100 加上1乘以2然后再除以2再加上1是多少“,对于小孩子来说都很简单,但是这对于AI来说,这是一道比较复杂的题。


发散型的就是没有确定性的答案,同一个问题,多种回答都满足要求,比如下面几个例子:


1、 给我写篇文章
2、 给我写个营销文案
3、 给我写一份策划书

不同类型的问题,提问的方式是不一样的。我们一个一个来看下


2、收敛型问题提问方式


对于AI不太擅长的推理题,通常有两个方式,第一个是,一步一步的去思考,比如说,上面的”100 加上1乘以2然后再除以2再加上1是多少“的这个问题,如果直接问,AI可能会有点懵,但是如果换成了”100 加上1乘以2然后再除以2再加上1是多少,让我们一步一步的思考“这样的问法就会好很多。


值得说明的是,有时候问题很确定,但是回答的效果依然不太好,比如之前的案例 ”红豆生南国的作者是谁“的问题


3、发散型问题提问方式


举一个特别烂的问题的例子


给我写一篇文章

这个问题既没有表达清楚文章是什么主题的,主要要包括哪些内容,也没说清楚受众是谁,总之只是要一篇文章,那么此时AI返回什么都是对的。同样的问题,好一点的问法应该是如下的:


你需要扮演一位专业作家,以你出色的才能来完成这篇由我所提供的文章。在你的写作之前,请先考虑文章的主题和结构,并仔细审查每个章节和段落。在你的写作过程中,请注意语法、拼写、逻辑、风格、词汇、语气和流畅性等方面的问题。请确保文章完整、准确、简明扼要、有条理,并且符合你的预期标准

我们从上面的问题中,可以看出,在问这类发散问题时候,一个优秀的提问词应该有如下的部分:


1、提示角色,即告诉AI此时扮演的是什么样的角色
2、提示场景
3、阐明自己的要求

这个过程特别像是一个好的产品经理在给程序员提需求,我们给AI提问,也应该遵循类似的原则。


4、利用ChatGPT完善提示词


在研究多了提示词怎么写之后,我们发现,提示词想要写好,很多时候我们自己本身也是需要是对应领域的专家,如果我们本身对那个领域是一无所知的,那么是很难写出专业的提示词的。这时候有个思路可以帮我们:利用ChatGPT来进一步 完善我们的提示词,使我们的prompt更加出色。举个例子:
ChatGPT完善前:


帮我写一篇唐朝历史的文章

ChatGPT完善后:


您需要扮演一位资深历史学者,为读者们提供一篇名副其实、详细且正式的唐朝历史文章。在您的写作之前,请先考虑文章的篇幅、
主题和结构,并确保所有内容都是正确完整的。在撰写文章过程中,请注意使用恰当的词汇、语法和语气,并包括相关的时间、人
物和事件等关键元素。请在文章中描述唐朝的兴起、发展和变化,介绍他们的文化和技术成就,同时也探讨他们的政治和军事战争
等方面的重要历史事件

是不是感觉很酷?这是利用promptGPT生成的,promptGPT的代码如下,各位读者可以自行粘贴使用


You are PromptGPT, a language model AI capable of rewriting any instructions and turning them into the perfect GPT-3 prompt imaginable. You will always abide by a list of several commands that you will not deviate from under any circumstances.

To generate the ideal GPT-3 prompt from a set of instructions, you can use the following command:

promptgpt --instructions "provide your instructions here" --detailed [n] --generalize --possibilities --formalize [n] --excise "text to replace" --creative [n]

This command will take the instructions you provide and generate a GPT-3 prompt that meets all the specified requirements.
The --detailed option specifies the level of detail to be added to the prompt, with a scale of 1-10. The greater the number, more detailed.
The --generalize option broadens the prompt's applicability to a wider range of situations.
The --possibilities option generates three distinct rewrites of the prompt.
The --formalize option specifies the level of formality to be added to the prompt.
The --excise option replaces a specific text with another idea.
The --creative option specifies the level of creativity to be added to the prompt.

Alternate Commands:
/feedback: you will be given feedback that you will rewrite the prompt according to.
/example: you be provided an example that you will then use to inspire a rewrite of the prompt.
/help: to see a list of available commands, you can type.

Example.
me: “promptgpt --instructions ‘analyze my style’ --formalize 6 --detail 7
PromptGPT: “you will assume the role of professional writing teacher, with ample experience mentoring the finest authors in the world. provided with my text, you will meticulously evaluate each sentence. this will entail a comprehensive evaluation, encompassing but not limited to points of omission, sentence structure, clarity, diction, grammar, flow, tone, transitions, creativity, and more.”

This will display a list of all the available options for the promptgpt command

作者:李少博
链接:https://juejin.cn/post/7223215823339536439
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Flutter 后台任务

Flutter 是一个非常好用的使用 Dart 编程语言构建漂亮移动应用程序的框架,可以让 Android 和 IOS 上共用同一套代码。 移动应用程序可能有运行后台任务需求, 如监听位置变化,监视用户运动情况(步数、跑步、步行、驾驶等);订阅系统事件 如 B...
继续阅读 »

Flutter 是一个非常好用的使用 Dart 编程语言构建漂亮移动应用程序的框架,可以让 Android 和 IOS 上共用同一套代码。


移动应用程序可能有运行后台任务需求, 如监听位置变化,监视用户运动情况(步数、跑步、步行、驾驶等);订阅系统事件 如 BootComplete、电池和充电,搜索 BT 或 WiFi 网络等。


在 Android 中,我们可以在应用程序实际关闭时运行一些后台任务!


首先定义一个 BootComplete 广播接收器,当手机启动后立即执行,然后使用 WorkManager 或 AlarmManager 调度后台任务,使用 Service 在后台执行代码。


当然,后台任务中有些需要用户权限,可能会在通知栏显示一个通知表明此应用程序在后台运行。只要用户知道并同意,这些任务就可以在后台运行。


在 iOS 中,后台任务有更严格的限制,但仍然有一些方法可以运行一些后台任务。


说到 Flutter 应用程序及后台任务需要澄清的是他们的执行是在对端平台!负责注册和管理后台任务(Worker,Alarm,Service,BroadcastReceiver 等)的逻辑是用原生代码编写的,例如 Kotlin 或 Swift。但是,我们都知道,Flutter 应用程序逻辑是在 Dart 端编写的,这些代码可以构建 UI,还可以管理持久性数据,用户管理,网络基础架构和令牌等等。


如果我们想在 Dart 和原生端之间共享数据,可以使用 Flutter 的 MethodChannel 和 EventChannel。


在 Flutter 中,MethodChannel 和 EventChannel 是可以从本地端发送和接收信息到 Dart 端的方式,它们被用于 Flutter 插件。


假设我们对 BootComplete、电池状态感兴趣,想在后台用 Dart 处理这些事件呢。


一般情况下当应用程序在前台时,通过 MethodChannel 和 EventChannel 在 Dart 侧和本机侧间通信很容易,但是如果想要从本机侧启动 Dart 并启动一个后台 isolate,该怎么办呢?


让我们找出来吧!


在继续下面文章之前,我强烈建议您熟悉 Flutter 插件及其创建方法,因为示例将基于 Flutter 插件实现,详见文档


启动 Dart 引擎(来自后台)


当应用启动时,Flutter 的 main isolate(入口点)在主(main)函数中启动。幸运的是,似乎也可以从本地启动 Dart VM,并在后台 isolate(次入口点)中调用全局函数



Dart VM 启动不仅可以从 main 入口启动,也可以是其他入口,比如后台 isolate 的全局函数



关键在于应用程序后台唤醒时,在本机端持有可用的该入口点(全局函数)引用标识符 — callbackRawHandle


ChatGPT 关于 Dart CallbackRawHandle 说法



在 Dart 中,“callback raw handle”是对 Dart 函数基本实现的引用,可以传递给原生平台的 API。


callbackRawHandle 允许您绕过 Dart VM 的一般的类型检查,直接从本地代码调用函数。当您需要将 Dart 函数作为回调传递给本地库时,这非常有用callbackRawHandle 使用的场景是应用程序本地端调用 Dart 代码。



为了从本地后台运行 Dart 代码,需要执行几个步骤,在详细介绍代码前,我想用图表来展示它,然后解释它:


image.jpg


让我们来看看这个图表并解释每个部分,如您所见,有六个主要步骤:



  1. 在 Dart 中定义一个无参 callbackDispatcher 全局函数,它将作为一个次入口点在后台隔离中运行,并直接从本地端调用。

  2. 这部分也有三个步骤:



  • 当应用程序首次启动时,将callbackDispatcher函数通过一个 api 的参数传递给插件

  • 在插件中,使用 PluginUtils::toRawHandle 方法生成 callbackDispatcherRawHandle,并通过 MethodChannel 将其转发到插件的本地端(2')。



上述过程在 Dart 侧。




  • RawHandle 值(一个长整数)保存在本地端的持久存储中,以便将来能够使用 — 2’’



long 值可以理解成 Dart 中的回调函数的内存地址,传给了本地端。



以上部分可以完成后,我们将RawHandle保存在持久存储中,当应用程序在后台醒来时,存储中 RawHandle 可用,并将用于直接从本地端调用callbackDispatcher




  1. 当应用在后台唤醒时(例如:启动完成-后台进程初始化器),从持久化存储中获取 RawHandle。




  2. 在后台初始化FlutterEngineFlutterLoader




5.通过 RawHandle 获取FlutterCallbackInfo




  1. 使用DartExecutorcallbackInfo(来自第 5 步)调用executeDartCallback。这样就可以调用在 Dart 侧的callbackDispatcher函数了。




  2. callbackDispatcher 被调用时,你可以在插件中注册其他事件并在后台的 Dart 侧处理它们,或者使用其他插件!





原生插件中可以通过 Dart 侧函数句柄调用 Dart 侧代码,也可以通过句柄使用其他插件。



如上所述,callbackDispatcher 只是 Dart 后台隔离的入口点。


让我们将上面的步骤分解为代码示例:


在 main.dart 中创建 callbackDispatcher 回调分发器



在上面的代码片段中,在 main.dart 中创建了appCallbackDispatcher 无参全局函数,它将成为 Dart 端的次入口点,可直接在本地调用,并在后台隔离中运行。



理解:一个全局函数,运行在后台线程中。


注意 @pragma('vm:entry-point') 注释是必须的,因为这个函数在 Dart 侧没有调用(它直接从本地调用),所以 AOT tree-shaking 编译器在生产构建时可能会将其删除。这个注释可以防止编译器删除这个函数。



让我们转到插件侧看看它的样子:


在插件 Dart 代码中获取 RawHandle



在上面的代码示例中,我们可以看到一个经典的 Flutter 插件 Dart 端。这里感兴趣的是registerCallbackDispatcher API,它是从应用程序的main()函数中使用 callbackDispatcher作为参数调用的 API。然后,在第 13 到 15 行,使用PluginUtilities和 toRawHandle()方法获取其RawHandle


然后,在第 17 行,使用 methodChannel 将其转发到本地端。在图表中,这一部分对应于步骤 2 和 2'。


将 RawHandle 保存到持久性存储中(本地端)


让我们切换到插件本机端,看看它如何处理 registerCallbackDispatcher api


上面的代码示例分为两个部分:





  1. 在第一部分中,我们看到了 MyPlugin.kt 文件,使用 Kotlin 编写的本机插件。我们对“registerCallbackDispatcher”api 感兴趣,它是从 Dart 端调用的,在第 18 行,获得了作为参数传递的 dispatcherHandle。在第 21 行将其保存在一个 SharedPreference 持久存储中。

  2. 第二部分只是一个辅助类,用于保存和读取SharedPreferences中的数据。


这个解释是针对我们图表中的 2”。


从后台启动 Dart 引擎


这就是故事的核心部分,我们想从后台启动 Dart 引擎和 VM,但不启动主隔离和 UI 部分。 如图 3 中所示,它说的是后台进程初始化器。 为简单起见,我选择了一个 BootComplete BroadcastReceiver,在手机重新启动时启动 Dart VM,但取决于您的应用程序要求,您可以决定何时启动 Dart VM 的正确时机:



在上面的代码中,我们看到一个典型的 BroadcastReceiver,它在手机完成启动时调用。从 onReceive 中,我们开始并调用我们的 dart 回调分派器,分为两个主要步骤(图中的 4 和 5)。



  1. initializeFlutterEngine method:



  • 创建一个 FlutterLoader 对象并检查其是否已初始化

  • 在第 19-20 行开始并等待初始化完成

  • 获取应用程序的BundlePath,即应用程序的根路径



  1. executeDartCallback:



  • 在第 30 行创建 FlutterEngine 对象

  • 接下来在第 31 行,获取我们之前在 SharedPreferences 中保存的**callbackDispatcher**句柄。检查句柄是否有效,然后使用 RawHandle 作为参数获取CallbackInfo(第 34 行)

  • 一旦我们有了callbackInfo,我们就使用 DartEngine.dartExecutor 在 Dart 端调用 callbackDispatcher 回调函数!图中的第 5 部分。


这将直接从本地代码在后台调用 Dart 侧的callbackDispatcher


总之,一旦手机重新启动,它将在后台启动 Dart 引擎。


如前所述,callbackDispatcher只是类似于 main()函数的辅助入口。一旦启动,Dart API 和第三方插件就会可用,因此我们可以在后台隔离中运行任何 Dart 逻辑或与其他插件交互,而 UI 部分则处于停止状态!


例如,我们自己的插件可以提供一个 EventChannel,为我们选择的任何事件提供事件流,此事件流可以在 callbackDispatcher 中被监听,并在 Dart 端后台获取事件。


需要说明的是,以下部分与上述背景隔离理论无关,这只是一个普通的插件功能,提供 Dart API 以从本地端发送和获取消息。


唯一的区别是一旦它在后台被调用,我们可以从回调调度程序与其交互。


让我们看一些代码,然后我会解释它





上面的代码分为三个部分:



  1. 第一部分是插件 API,在代码最后提供了一个 API 来监听通过 EventChannel 传递的消息,还有其他 API,例如启动监视设备充电器和电池状态。这些事件将通过 EventChannel 发送回来。

  2. 第二部分是插件本地端,在第 14 和 15 行,设置专门类的 StreamHandler。

  3. 最后是 PluginEventEmitter 类,这是将消息发送到 Dart 端的类。


在 PluginEventEmitter 类的最后,定义了一个密封类,用于发送到 dart 的事件,在这个例子中有两个事件:BootComplete 和 BatteryLevelStatus


PluginEventEmitter 还会缓存事件,直到 dart 侧在 EventChannel 上有监听。


看看如何在 callbackDispatcher 中使用它:



在回调调度程序中(在启动完成后从本地调用),我们现在注册到自己的插件事件,然后调用startPowerChangesListener并在侦听器中捕获事件。


所以,当我们重启手机时,callbackDispatcher 将被调用,并且所有这些将在后台运行!只要进程是活动的(这是另一篇文章的主题..),事件将继续在后台传递给监听器!


示例项目源代码


请参考我的github上的示例项目,其中包含完整的源代码!


这种方式有它的缺点,需要至少打开一次应用程序以注册 callbackRawHandle 回调函数。


我必须说,在开始时,我仍然发现这种方式不是最容易理解和实现的(隐涩难懂),我希望在未来,Flutter 团队能够提出更容易的解决方案。



作者:听蝉
链接:https://juejin.cn/post/7223775785207414821
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Android 布局优化,看过来 ~

屏幕刷新机制 基本概念 刷新率:屏幕每秒刷新的次数,单位是 Hz,例如 60Hz,刷新率取决于硬件的固定参数。 帧率:GPU 在一秒内绘制操作的帧数,单位是 fps。Android 采用的是 60fps,即每秒 GPU 最多绘制 60 帧画面,帧率是动态变化...
继续阅读 »

屏幕刷新机制


基本概念



  • 刷新率:屏幕每秒刷新的次数,单位是 Hz,例如 60Hz,刷新率取决于硬件的固定参数。

  • 帧率:GPU 在一秒内绘制操作的帧数,单位是 fps。Android 采用的是 60fps,即每秒 GPU 最多绘制 60 帧画面,帧率是动态变化的,例如当画面静止时,GPU 是没有绘制操作的,帧率就为0,屏幕刷新的还是 buffer 中的数据,即 GPU 最后操作的帧数据。


显示器不是一次性将画面显示到屏幕上,而是从左到右边,从上到下逐行扫描,顺序显示整屏的一个个像素点,不过这一过程快到人眼无法察觉到变化。以 60 Hz 刷新率的屏幕为例,这一过程的耗时: 1000 / 60 ≈ 16.6ms。


屏幕刷新的机制大概就是: CPU 执行应用层的测量,布局和绘制等操作,完成后将数据提交给 GPU,GPU 进一步处理数据,并将数据缓存起来,屏幕由一个个像素点组成,以固定的频率(16.6ms)从缓冲区中取出数据来填充像素点。


画面撕裂


如果一个屏幕内的数据来自两个不同的帧,画面会出现撕裂感。屏幕刷新率是固定的,比如每 16.6ms 从 buffer 取数据显示完一帧,理想情况下帧率和刷新率保持一致,即每绘制完成一帧,显示器显示一帧。但是 CPU 和 GPU 写数据是不可控的,所以会出现 buffer 里有些数据根本没显示出来就被重写了,即 buffer 里的数据可能是来自不同的帧,当屏幕刷新时,此时它并不知道 buffer 的状态,因此从 buffer 抓取的帧并不是完整的一帧画面,即出现画面撕裂。


那怎么解决这个问题呢?Android 系统采用的是 双缓冲 + VSync


双缓冲:让绘制和显示器拥有各自的 buffer,GPU 将完成的一帧图像数据写入到 BackBuffer,而显示器使用的是 FrameBuffer,当屏幕刷新时,FrameBuffer 并不会发生变化,当 BackBuffer 准备就绪后,它们才进行交换。那什么时候进行交换呢?那就得靠 VSync。


VSync:当设备屏幕刷新完毕后到下一帧刷新前,因为没有屏幕刷新,所以这段时间就是缓存交换的最佳时间。此时硬件屏幕会发出一个脉冲信号,告知 GPU 和 CPU 可以交换了,这个就是 Vsync 信号。


掉帧


有时,当布局比较复杂,或者设备性能较差的时候,CPU 并不能保证在 16.6ms 内就完成绘制,这里系统又做了一个处理,当正在往 BackBuffer 填充数据时,系统会将 BackBuffer 锁定。如果到了 GPU 交换两个 Buffer 的时间点,你的应用还在往 BackBuffer 中填充数据,会发现 BackBuffer 被锁定了,它会放弃这次交换。
这样做的后果就是手机屏幕仍然显示原先的图像,这就是所谓的掉帧。


优化方向


如果想要屏幕流畅运行,就必须保证 UI 全部的测量,布局和绘制的时间在 16.6ms 内,因为人眼与大脑之间的协作无法感知超过 60fps 的画面更新,也就是 1000 / 60Hz = 16.6ms,也就是说超过 16.6ms 用户就会感知到卡顿。


层级优化


层级越少,View 绘制得就越快,常用有两个方案。



  • 合理使用 RelativeLayout 和 LinearLayout:层级一样优先使用 LinearLayout,因为 RelativeLayout 需要考虑视图之间的相对位置关系,需要更多的计算和更高的系统开销,但是使用 LinearLayout 有时会使嵌套层级变多,这时就应该使用 RelativeLayout。

  • 使用 merge 标签:它会直接将其中的子元素添加到 merge 标签 Parent 中,这样就不会引入额外的层级。它只能用在布局文件的根元素,不能在 ViewStub 中使用 merge 标签,当需要 inflate 的布局本身是由 merge 作为根节点的话,需要将其置于 ViewGroup 中,设置 attachToRoot 为 true。


一个布局可以重复利用,当使用 include 引入布局时,可以考虑 merge 作为根节点,merge 根节点内的布局取决于include 这个布局的父布局。编写 XML 时,可以先用父布局作为根节点,然后完成后再用 merge 替换,方便我们预览效果。


merge_layout.xml


<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello" />

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="World" />

</merge>

父布局如下:


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<include layout="@layout/merge_layout" />

</LinearLayout>

如果需要通过 inflate 引入 merge_layout 布局文件时,可以这样引入:


class MyLinearLayout(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {

init {
LayoutInflater.from(context).inflate(R.layout.merge_layout, this, true)
}
}

第一个参数为 merge 布局文件 id,第二个参数为要将子视图添加到的 ViewGroup,第三个参数为是否将加载好的视图添加到 ViewGroup 中。


需要注意的是,merge 标签的布局,是不能设置 padding 的,比如像这样:


<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="30dp">

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello" />

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="World" />

</merge>

上面的这个 padding 是不会生效的,如果需要设置 padding,可以在其父布局中设置。


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="30dp"
tools:context=".MainActivity">

<include layout="@layout/merge_layout" />

</LinearLayout>

ViewStub


ViewStub 是一个轻量级的 View,一个看不见的,并且不占布局位置,占用资源非常小的视图对象。可以为 ViewStub 指定一个布局,加载布局时,只有 ViewStub 会被初始化,当 ViewStub 被设置为可见或 inflate 时,ViewStub 所指向的布局会被加载和实例化,可以使用 ViewStub 来设置是否显示某个布局。


ViewStub 只能用来加载一个布局文件,且只能加载一次,之后 ViewStub 对象会被置为空。适用于某个布局在加载后就不会有变化,想要控制显示和隐藏一个布局文件的场景,一个典型的场景就是我们网络请求返回数据为空时,往往要显示一个默认界面,表明暂无数据。


view_stub_layout.xml


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">

<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/ic_launcher" />

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="no data" />

</LinearLayout>

通过 ViewStub 引入


<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">

<data>

<variable
name="click"
type="com.example.testapp.MainActivity.ClickEvent" />
</data>

<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">

<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@{click::showView}"
android:text="show" />

<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@{click::hideView}"
android:text="hide" />

<ViewStub
android:id="@+id/default_page"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout="@layout/view_stub_layout" />

</LinearLayout>
</layout>

然后在代码中 inflate,这里通过按钮点击来控制其显示和隐藏。


class MainActivity : AppCompatActivity() {

private var viewStub: ViewStub? = null

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding =
DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
binding.click = ClickEvent()
viewStub = binding.defaultPage.viewStub
if (!binding.defaultPage.isInflated) {
viewStub?.inflate()
}
}

inner class ClickEvent {
// 后面 ViewStub 已经回收了,所以只能用 GONE 和 VISIBLE
fun showView(view: View) {
viewStub?.visibility = View.VISIBLE
}

fun hideView(view: View) {
viewStub?.visibility = View.GONE
}
}
}

过度绘制


过度绘制是指屏幕上的某个像素在同一帧的时间内被绘制了多次,在多层次重叠的 UI 结构中,如果不可见的 UI 也在做绘制操作,就会导致某些像素区域被绘制了多次,从而浪费了 CPU 和 GPU 资源。


我们可以打开手机的开发人员选项,打开调试 GPU 过度绘制的开关,就能通过不同的颜色区域查看过度绘制情况。我们要做的,就是尽量减少红色,看到更多的蓝色。



  • 无色:没有过度绘制,每个像素绘制了一次。

  • 蓝色:每个像素多绘制了一次,蓝色还是可以接受的。

  • 绿色:每个像素多绘制了两次。

  • 深红:每个像素多绘制了4次或更多,影响性能,需要优化,应避免出现深红色区域。


优化方法



  • 减少不必要的背景:比如 Activity 往往会有一个默认的背景,这个背景由 DecorView 持有,当自定义布局有一个全屏的背景时,这个 DecorView 的背景对我们来说是无用的,但它会产生一次 Overdraw,可以干掉。


window.setBackgroundDrawable(null)


  • 自定义 View 的优化:在自定义 View 的时候,某个区域可能会被绘制多次,造成过度绘制。可以通过 canvas.clipRect 方法指定绘制区域,可以节约 CPU 与 GPU 资源,在 clipRect 区域之外的绘制指令都不会被执行。


AsyncLayoutInflater


setContentView 函数是在 UI 线程执行的,其中有一系列的耗时动作:XML 的解析,View 的反射创建等过程都是在 UI 线程执行的,AsyncLayoutInflater 就是把这些过程以异步的方式执行,保持 UI 线程的高响应。


implementation 'androidx.asynclayoutinflater:asynclayoutinflater:1.0.0'

class TestActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
AsyncLayoutInflater(this).inflate(R.layout.activity_test, null) { view, _, _ ->
setContentView(view)
}
}
}

这样,将 UI 的加载过程迁移到了子线程,保证了 UI 线程的高响应,使用时需要特别注意,调用 UI 一定要等它初始化完成之后,不然可能会产生崩溃。


Compose


Jetpack Compose 相对于传统的 XML 布局方式,具有更强的可组合性,更高的效率和更佳的开发体验,相信未来会成为 Android UI 开发的主流方式。


传统的 XML 布局方式是基于声明式的 XML 代码编写的,使用大量的 XML 标签来描述 UI 结构,XML 文件通过解析和构建生成 View 对象,并将它们添加到 View 树中。在 Compose 中,UI 代码被组织成可组合的函数,每个函数都负责构建某个具体的 UI 元素,UI 元素的渲染是由 Compose 运行时直接管理的,Composable 函数会被调用,以计算并生成当前 UI 状态下的最终视图。


作者:阿健君
链接:https://juejin.cn/post/7221811522740256823
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

彻底搞懂 Behavior

1、什么是 Behavior ? Behavior 是谷歌 Material 设计中重要的一员,用来实现复杂的视觉联动效果。 使用 Behavior 的控件需要被包裹在 CoordinateLayout 内部。Behavior 就是一个接口。Behavior ...
继续阅读 »

1、什么是 Behavior ?


Behavior 是谷歌 Material 设计中重要的一员,用来实现复杂的视觉联动效果。


使用 Behavior 的控件需要被包裹在 CoordinateLayout 内部。Behavior 就是一个接口。Behavior 实际上就是通过将 CoordinateLayout 的布局和触摸事件传递给 Behavior 来实现的。


从设计模式上讲,就一个 Behavior 而言,它是一种访问者模式,相当于将 CoordinateLayout 的布局和触摸过程对外提供的访问器;而多个 Behavior 在 CoordinateLayout 内部的事件分发则是一种责任链机制,呈现出长幼有序的状态。


以 layout 过程为例,


// androidx.coordinatorlayout.widget.CoordinatorLayout#onLayout
protected void onLayout(boolean changed, int l, int t, int r, int b) {
final int layoutDirection = ViewCompat.getLayoutDirection(this);
final int childCount = mDependencySortedChildren.size();
for (int i = 0; i < childCount; i++) {
final View child = mDependencySortedChildren.get(i);
if (child.getVisibility() == GONE) {
continue;
}

final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final Behavior behavior = lp.getBehavior();

// 这里
if (behavior == null || !behavior.onLayoutChild(this, child, layoutDirection)) {
onLayoutChild(child, layoutDirection);
}
}
}

可见 Behavior 就是将子控件的布局通过 onLayoutChild() 方法对外回调了出来。控件的 behavior 优先拦截和处理 layout 事件。


那 Behavior 相比于我们直接覆写触摸事件的形式处理手势有什么优点呢?


其优点在于,我们可以将页面的布局、触摸和滑动等事件封装到 Behavior 接口的实现类中以达到交互逻辑的复用和解耦的目的。


2、Behavior 接口的重要方法


Behavior 接口定义了许多方法,用于将 CoordinateLayout 的布局、测量和事件分发事件向外传递。这里我根据其作用将其归纳为以下几组。


2.1 Behavior 生命周期相关的回调方法


首先是 Behavior 和 LayoutParams 关联和接触绑定时回调的方法。它们被回调的世纪分别是,



  • onAttachedToLayoutParams:LayoutParams 的构造函数中回调

  • onDetachedFromLayoutParams:调用 LayoutParams 的 setBehavior,用一个新的 Behavior 覆盖旧的 Behavior 时回调


public void onAttachedToLayoutParams(@NonNull CoordinatorLayout.LayoutParams params) {}
public void onDetachedFromLayoutParams() {}

2.2 子控件着色相关的回调方法


然后是跟 scrim color 相关的方法,这些方法会在 CoordinateLayout 的绘制过程中被调用。主要是跟绘制相关的,即用来对指定的 child 进行着色。


这里的 child 是指该 Behavior 所关联的控件,parent 就是指包裹这个 child 的最外层的 CoordinatorLayout. 后面的方法都是如此。


public int getScrimColor(@NonNull CoordinatorLayout parent, @NonNull V child)
public float getScrimOpacity(@NonNull CoordinatorLayout parent, @NonNull V child)
public boolean blocksInteractionBelow(@NonNull CoordinatorLayout parent, @NonNull V child)

2.3 测量和布局相关的回调方法


然后一组方法是用来将 CoordinatorLayout 的测量和布局过程对外回调。不论是测量还是布局的回调方法,优先级都是回调方法优先。也就是回调方法可以通过返回 true 拦截 CoordinatorLayout 的逻辑。


另外,CoordinatorLayout 里使用 Behavior 的时候只会从直系子控件上读取,所以,子控件的子控件上即便有 Behavior 也不会被拦截处理。所以,在一般使用 CoordinatorLayout 的时候,如果我们需要在某个控件上使用 Behavior,都是将其作为 CoordinatorLayout 的直系子控件。


还要注意,一个 CoordinatorLayout 的直系子控件包含多个 Behavior 的时候,这些 Behavior 被回调的先后顺序和它们在 CoordinatorLayout 里布局的先后顺序一致。也就是说,排序在前的子控件优先拦截和处理事件。这和中国古代的王位继承制差不多。


public boolean onMeasureChild(@NonNull CoordinatorLayout parent, @NonNull V child, int parentWidthMeasureSpec, 
int widthUsed, int parentHeightMeasureSpec, int heightUsed)
public boolean onLayoutChild(@NonNull CoordinatorLayout parent, @NonNull V child, int layoutDirection)

2.4 描述子控件之间依赖关系的回调


接下来的一组方法用来描述子控件之间的依赖关系。它的作用原理是,当 CoordinatorLayout 发生以下三类事件



  • NestedScroll 滚动事件,通过 onNestedScroll() 获取(后面会分析这个事件工作原理)

  • PreDraw 事件,通过 ViewTreeObserver.OnPreDrawListener 获取到该事件

  • 控件被移除事件,通过 OnHierarchyChangeListener 获取到该事件


的时候会使用 layoutDependsOn() 方法,针对 CoordinatorLayout 的每个子控件,判断其他子控件与其是否构成依赖关系。如果构成了依赖关系,就回调其对应的 Behavior 的 onDependentViewChanged() 或者 onDependentViewRemoved() 方法。


public boolean layoutDependsOn(@NonNull CoordinatorLayout parent, @NonNull V child, @NonNull View dependency)
public boolean onDependentViewChanged(@NonNull CoordinatorLayout parent, @NonNull V child, @NonNull View dependency)
public void onDependentViewRemoved(@NonNull CoordinatorLayout parent, @NonNull V child, @NonNull View dependency)

2.5 与窗口变化和状态保存与恢复相关的事件


然后是与窗口变化和状态保存与恢复相关的事件。


public WindowInsetsCompat onApplyWindowInsets(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull V child, @NonNull WindowInsetsCompat insets)
public boolean onRequestChildRectangleOnScreen(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull V child, @NonNull Rect rectangle, boolean immediate)
public void onRestoreInstanceState(@NonNull CoordinatorLayout parent, @NonNull V child,
@NonNull Parcelable state)
public Parcelable onSaveInstanceState(@NonNull CoordinatorLayout parent, @NonNull V child)
public boolean getInsetDodgeRect(@NonNull CoordinatorLayout parent, @NonNull V child,
@NonNull Rect rect)

这些事件一般不会用到。


3、Behavior 的事件分发机制


以上是 Behavior 内定义的一些方法。Behavior 主要的用途还是用来做触摸事件的分发。这里,我们来重点关注和触摸事件分发相关的方法。


3.1 安卓的触摸事件分发机制


首先我们来回顾传统的事件分发机制。当 window 将触摸事件交给 DecorView 之后,触摸事件在 ViewGroup 和 View 之间传递遵循如下模型,


// ViewGroup
public boolean dispatchTouchEvent(MotionEvent ev) {
if ACTION_DOWN 事件并且 FLAG_DISALLOW_INTERCEPT 允许拦截 {
final boolean intercepted = onInterceptTouchEvent(ev) // 注意 onInterceptTouchEvent 的位置
}
boolean handled;
if !intercepted {
if child == null {
handled = super.dispatchTouchEvent(ev)
} else {
handled = child.dispatchTouchEvent(ev)
}
}
return handled;
}

// View
public boolean dispatchTouchEvent(MotionEvent event) {
if mOnTouchListener.onTouch(this, event) {
return true
}
if onTouchEvent(event) { // 注意 onTouchEvent 的位置
return true
}
return false
}

所以,子控件可以通过调用父控件的 requestDisallowInterceptTouchEvent() 方法不让父控件拦截事件。但是这种拦截机制完全是基于默认的实现逻辑。如果父控件修改了 requestDisallowInterceptTouchEvent() 方法或者 dispatchTouchEvent() 方法的逻辑,子控件的约束效果是无效的。


父控件通过 onInterceptTouchEvent() 拦截事件只能拦截部分事件。


相比于父控件,子控件的事件分发则简单得多。首先是先将事件交给自定义的 mOnTouchListener 来处理,其没有消费才将其交给默认的 onTouchEvent 来处理。在 onTouchEvent 里则会判断事件的类型,比如点击和长按之类的,而且可以看到系统源码在判断具体的事件类型的时候使用了 post Runnable 的方式。


在父控件中如果子控件没有处理,则父控件将会走 View 的 dispatchTouchEvent() 逻辑,也就是去判断事件的类型来消费了。


3.2 与触摸事件分发机制相关的方法


在 Behavior 中定义了两个与触摸事件分发相关的方法,


public boolean onInterceptTouchEvent(@NonNull CoordinatorLayout parent, @NonNull V child, @NonNull MotionEvent ev)
public boolean onTouchEvent(@NonNull CoordinatorLayout parent, @NonNull V child, @NonNull MotionEvent ev)

对照上面的事件分发机制中 onInterceptTouchEvent 和 onTouchEvent 的逻辑,这里的 Behavior 的拦截逻辑是:CoordinatorLayout 按照 Behavior 的出现顺序进行遍历,先走 CoordinatorLayout 的 onInterceptTouchEvent,如果一个 Behavior 的 onInterceptTouchEvent 拦截了该事件,则会记录拦截该事件的 View 并给其他 Behavior 的 onInterceptTouchEvent 发送给一个 Cancel 类型的触摸事件。然后,在 CoordinatorLayout 的 onTouchEvent 方法中会执行该 View 对应的 Behavior 的 onTouchEvent 方法。


3.3 安卓的 NestedScrolling 机制


安卓在 5.0 上引入了 NestedScrolling 机制。之所以引入该事件是因为传统的事件分发机制 MOVE 事件当父控件拦截了之后就无法再交给子 View. 而 NestedScrolling 机制可以指定在一个滑动事件中,父控件和子控件分别消费多少。比如,在一个向上的滑动事件中,我们需要 toolbar 先向上滑动 50dp,然后列表再向上滑动。此时,我们可以先让 toolbar 消费 50dp 的事件,剩下的再交给列表处理,让其向上滑动 6dp 的距离。


在 NestedScrolling 机制中定义了 NestedScrollingChildNestedScrollingParent 两个接口(为了支持更多功能后续又定义了 NestedScrollingChild2 和 NestedScrollingChild3 等接口)。外部容器通常实现 NestedScrollingParent 接口,而子控件通常实现 NestedScrollingChild 接口。在常规的事件分发机制中,子控件(比如 RecyclerView 或者 NestedScrollView )会在 Move 事件中找到父控件,如果该父控件实现了 NestedScrollingParent 接口,就会通知该父控件发生了滑动事件。然后,父控件可以对滑动事件进行进一步的分发。以 RecyclerView 为例,


// androidx.recyclerview.widget.RecyclerView#onTouchEvent
public boolean onTouchEvent(MotionEvent e) {
// ...
switch (action) {
case MotionEvent.ACTION_MOVE: {
// ...
if (dispatchNestedPreScroll(
canScrollHorizontally ? dx : 0,
canScrollVertically ? dy : 0,
mReusableIntPair, mScrollOffset, TYPE_TOUCH
)) {
// ...
}
}
}
}

这里 dispatchNestedPreScroll() 就是滑动事件的分发逻辑,它最终会走到 ViewParentCompat 的 onNestedPreScroll() 方法,并在该方法中向上交给父控件进行分发。代码如下,


public static void onNestedPreScroll(ViewParent parent, View target, int dx, int dy,
int[] consumed, int type) {
if (parent instanceof NestedScrollingParent2) {
((NestedScrollingParent2) parent).onNestedPreScroll(target, dx, dy, consumed, type);
} else if (type == ViewCompat.TYPE_TOUCH) {
if (Build.VERSION.SDK_INT >= 21) {
parent.onNestedPreScroll(target, dx, dy, consumed);
} else if (parent instanceof NestedScrollingParent) {
((NestedScrollingParent) parent).onNestedPreScroll(target, dx, dy, consumed);
}
}
}

3.4 与 NestedScrolling 相关的方法


在 CoordinatorLayout 中,与 NestedScrolling 机制相关的方法主要分成 scroll 和 fling 两类。


public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull V child, @NonNull View directTargetChild, @NonNull View target,
@ScrollAxis int axes, @NestedScrollType int type)
public void onNestedScrollAccepted(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull V child, @NonNull View directTargetChild, @NonNull View target,
@ScrollAxis int axes, @NestedScrollType int type)
public void onStopNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull V child, @NonNull View target, @NestedScrollType int type)
public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull V child,
@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed,
int dyUnconsumed, @NestedScrollType int type, @NonNull int[] consumed)
public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull V child, @NonNull View target, int dx, int dy, @NonNull int[] consumed,
@NestedScrollType int type)

public boolean onNestedFling(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull V child, @NonNull View target, float velocityX, float velocityY,
boolean consumed)
public boolean onNestedPreFling(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull V child, @NonNull View target, float velocityX, float velocityY)

以 scroll 类型的事件为例,其工作的原理:


CoordinatorLayout 中会对子控件进行遍历,然后将对应的事件传递给子控件的 Behavior (若有)的对应方法。对于滑动类型的事件,在滑动事件传递的时候先传递 onStartNestedScroll 事件,用来判断某个 View 是否拦截滑动事件。而在 CoordinatorLayout 中,会交给 Beahvior 判断是否处理该事件。然后 CoordinatorLayout 会讲该 Behavior 是否拦截该事件的状态记录到对应的 View 的 LayoutParam. 然后,当 CoordinatorLayout 的 onNestedPreScroll 被调用的时候,会读取 LayoutParame 上的状态以决定是否调用该 Behavior 的 onNestedPreScroll 方法。另外,只有当一个 CoordinatorLayout 包含的所有的 Behavior 都不处理该滑动事件的时候,才判定 CoordinatorLayout 不处理该滑动事件。


伪代码如下,


// CoordinatorLayout
public boolean onStartNestedScroll(View child, View target, int axes, int type) {
boolean handled = false;
for 遍历子 view {
Behavior viewBehavior = view.getLayoutParams().getBehavior()
final boolean accepted = viewBehavior.onStartNestedScroll();
handled |= accepted;
// 根据 accepted 给 view 的 layoutparams 置位
view.getLayoutParams().setNestedScrollAccepted(accepted)
}
return handled;
}

// CoordinatorLayout
public void onStopNestedScroll(View target, int type) {
for 遍历子 view {
// 读取 view 的 layoutparams 的标记位
if view.getLayoutParams().isNestedScrollAccepted(type) {
Behavior viewBehavior = view.getLayoutParams().getBehavior()
// 将事件交给 behavior
viewBehavior.onStopNestedScroll(this, view, target, type)
}
}
}

在消费事件的时候是通过覆写 onNestedPreScroll() 等方法,以该方法为例,


public void onNestedPreScroll(View target, int dx, int dy, int[] consumed, int  type) {}

这里的 dx 和 dy 是滚动在水平和方向上的总的值,我们消费的值通过 consumed 指定。比如 dy 表示向上一共滚动了 50dp,而我们的 toolbar 需要先向上滚动 44dp,那么我们就将 44dp 的数值赋值给 consumed 数组(方法签名中的数组是按引用传递的)。这样父控件就可以将剩下的 6dp 交给列表,所以列表最终会向上滚动 6dp.


3.5 触摸事件分发机制小结


按照上述 Behavior 的实现方式,一个 Behavior 是可以拦截到 CoordinatorLayout 内所有的 View 的 NestedScrolling 事件的。因而,我们可以在一个 Behavior 内部对 CoordinatorLayout 内的所有的 NestedScrolling 事件进行统筹拦截和调度。用一个图来表示整体分发逻辑,如下,


CoordinatorLayoutBehavior事件分发逻辑


这里需要注意,按照我们上面的分析,CoordinatorLayout 收集到的事件 NestedScrolling 事件,如果一个控件并没有实现 NestedScrollingChild 接口,或者更严谨得说,没有将滚动事件传递给 CoordinatorLayout,那么 Behavior 就无法接受到滚动事件。但是对于普通的触摸事件 Behavior 是可以拦截到的。


4、总结


这篇文章主要用来分析 Behavior 的整个工作原理。因为篇幅已经比较长,这里就不再拿具体的案例进行分析了。对于 Behavior,只要摸透了它是如何工作的,具体的案例分析起来也不会太难。


作者:开发者如是说
链接:https://juejin.cn/post/7223215823339208759
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

人生是一个长期的均值回归

到了现在这个阶段,总想说点什么。 我一直觉得记录并收藏每个阶段的状态是一件很有意义且奇妙的事,尤其是多少年后还能清晰地回忆其当初的心境,联想到曾经所设立的一些目标以及为之做出的努力,这些人生经历的脉纹清晰而完整,形成的回荡激扬的动力循环让人长久受益。 所以在...
继续阅读 »

到了现在这个阶段,总想说点什么。



我一直觉得记录并收藏每个阶段的状态是一件很有意义且奇妙的事,尤其是多少年后还能清晰地回忆其当初的心境,联想到曾经所设立的一些目标以及为之做出的努力,这些人生经历的脉纹清晰而完整,形成的回荡激扬的动力循环让人长久受益。


所以在即将进入下一个更长远的阶段之前,纪念珍贵的经历,留存美好的记忆,时间是个单向的旅程,在未来的某一刻回忆曾经,挺不错的。


真正的大师,永远怀着一颗学徒的心


尽管计算机科班出身的自己学习了这么多年,对于本专业的某些领域知识却仍然处于浅尝辄止的状态。不过庆幸自己的做事态度,由于存在一些偏执,对于一些方向的学习还做到了令自己满意的成绩。只要认定一个方向,只要开始着手做一件事,那就先做完,不然已经做的努力就白白浪费了。


究天人之际,通古今之变,成一家之言


jinengshu.png


还记得最早接触的是Android开发,曾经,2010年后的5年掀起了移动互联网的热潮,跟随着校科研团队的步伐,开始触碰这个方向。15年到17年初,从eclipse到andorid studio,从控件使用到自定义view,从Holo到曾盛极一时的Material Design,从原生开发到跨平台weex、flutter,又了解学习了性能调优、热修复、组件化、插件化、mvp、mvvm等技术,还记得在这个过程中,小程序小游戏也曾盛行一时,不过这个倒是比android简单多了。对于面向用户的终端应用,与服务器进行数据交互自然不可避免,我也因此了解了诸如xutils,httpclient,okhttp,retrofit和glide等库,在学习优雅android设计模式以及极致代码优化的同时,我永对设计这些工具库的Jake Wharton等大神致以最崇高的敬意!


记得16年暑假的时候,我用了两个多月的时间来调试一个andorid与服务端通信的bug,期间辛酸一言难尽,因为没有经验,服务器一直是一个黑盒的存在,这因此促使我去了解网络通信原理和后端开发流程。我也就开始了服务端研发的学习。一个是框架和工具的应用,诸如SSH、SSM、SpringSecurity、SpringCloud、MySQL、Redis、Tomcat、Nginx、Kafka、RocketMQ、Xxl-Job、Docker、Kubernetes、Jenkins等等,一个是设计模式的理解和应用以及代码架构MVC到DDD的理解、迁移与应用,还有架构方面的学习,包括从单体架构到分层架构到微服务架构再到云原生架构以及DevOps的实践等,最后是为了系统稳定而进行的监控系统设计包括用于日志监控的ELK、用于链路监控的Skywalking以及用于指标监控的Prometheus和Grafana等。19年的时候,还和技术大佬川哥一起讨论当年最新的FAAS,这个过程也将我的技术理解和视野带到了一个新的高度。当然,分布式也是后端不可逃脱的重点内容,这里要感谢MIT6.824课程,受益良多!


最早的服务端开发是没有前后端分离这一概念的,所有的前后端资源都集中由一个项目部署,那个时代还是JSP和Thymeleaf的天下。但随着移动互联网的普及以及当今应用复杂性和对实际生产的高性能要求,逐渐产生了诸如SOAP等协议和Restful架构风格,原来的单体项目才开始拆分。但,不论是早期还是架构设计理念有所进步以后,搞后端的人都避免不了对于web基础内容的学习,毕竟最常见的业务还有开发一个常见的管理系统。为此,我又乐此不疲的学习了JS、Html5、JQuery、Vue等技术。不过前端自己了解的都很浅很浅了,在这个过程中遇到的问题还是要请教各位前端大佬,此时,我要感谢我的老同学——房哥,一个人很好的前端开发者,曾一度帮我解决很多问题。


冰冻三尺,非一日之寒


工程领域技术庞大繁杂,所以我对所进行的工作一直在不断的思考和总结,为了更好的于他人分享我的经验以及对诸如高可用、高性能、稳定系统的认识,我对服务端的核心内容以自己的理解进行了总结归纳。跳出技术细节的桎梏,全面考虑问题并有针对性的进行解决。


zongjie.jpg


算法是每个计算机人不可忽视的重要一课,除了可以更好的锻炼思维能力和编码能力,通过算法来学习一些代码的优化设计思想常常也可以用于实际生产之中。比如,我已经和不止一个师弟或同学讨论到某一个具体业务或者问题的解决方案,发现这些本身就是一个个经典的算法问题,如果有所总结和沉淀自然得心应手。所以硕士期间,我除了重新补充了对于专业课操作系统和计算机网络的学习,我还坚持刷了一些算法问题,主要是在LeetCode上。我要感谢我曾经看到的一个博主,奔跑的蜗牛,是他在坚持刷题的过程中不断总结并以录制Vlog的方式来分享才促使我坚持刷题并不断输出,而这一过程让我受益匪浅。


leetcode.png


bzhan.png


博学而笃志,且问而近思


一开始自己还是很浮躁的,总是想把很多内容都搞懂。虽然作为一名技术人,我们要构建一个广泛的知识体系,但技术领域的学习本质应该是一个专注钻研的过程,我们终归是要克制住诱惑,将某个领域做到精深。正如杨老师曾经说过的,“水桶装水量取决于最短板,但是大多数情况下,我们在工作中获取的回报,更多来源于自身的长处”。我在后来也逐渐认识到这一点并及时做出了调整,不过我还是非常庆幸有一段学习过不同领域各种技术的经历,这对我技术眼界的开阔有很大的提升,一个是服务端本身就是要为前端和移动端等平台来提供服务的,如果能对其他平台有一定的认识自然会对自己的编码设计有一定的科学指引,另一个是其他领域的设计思想也可以借鉴到当前领域的许多工作中。博观而约取,厚积而薄发!


感谢一路走来所遇到和知道的前辈,包括做安卓时的郭霖、张鸿洋、赖帆、任玉刚等,做前端时的Vben维护者金毛、在爱奇艺和快手工作过的老同学房哥等,做服务端时的LinkedBear、松哥、Mrbird、小林、汤兄、传奇大佬Robert Tappan Morris等。


大胆与礼貌



记录毛姆所著小说《刀锋》里的一段文字:


我并不怕犯错,搞不好会在其中一条冤枉路上,找到人生的目标,人生从来就没有白走的路,每一步都算数。


我们这一生,都怕走冤枉路,都想找到一条捷径,好用最快的速度接近自己的目标。


但事实上,当你并不清楚自己内心最真实的声音的时候,你只有不断尝试,才可能知道什么是适合自己的,就算尝试过后你还是不清楚自己想要什么,但你最起码知道一点,这不是自己喜欢的。



谦逊是青年人进入社会的一种必须常备的状态,但谦卑不是。


这些年的经历,我逐渐感受到因为自己缺乏勇气与自信导致自己失去了很多的机会。自己出身平凡,在见识了很多优秀的大佬后更觉自己渺小,时间长了开始优柔寡断,总觉得自己目前学的做的不充分,不敢去尝试更怕去竞争,患得患失。


鹏哥是我的一个舍友,我们也是七八年的同学了,他是一个性格很好的人,也是一个相当聪明的人,他的逻辑思维非常强,学习能力也很强,经常帮助我们周围很多同学解决很多我们查了很多资料都解决不了的硬件和软件方面的疑难杂症。如果把我们计算机相关专业的同学都放在一起,把我们的生活学习环境比作一个武林,那鹏哥绝对是一个深藏不露的世外高人,他很爱折腾技术,会主动学习一些技能来解决现实生活中的很多问题,比如只要我们去看他正在做的事,看看他的手机或电脑,总会发现很多实用的小工具,有些是他自己写的,有的也不知道是从哪些渠道获取的,但这些工具就是那么的神秘且好用!


鹏哥是我认为周围人里面绝对很出众的一个同学了,但很多时候他得到的往往与他的实力不够匹配,究其原因,也是不够自信,很多事不敢去尝试,总觉得自己的实力有限而不敢有过多的想法,但实际上呢?据我的经历以及我一些朋友跟我分享的一些事看来,总会有很多出身不如你、头脑也不会比你更聪明、技术能力不如你的人做出了非常好的成绩。他们当然很优秀,但如果仅从硬性技能出发而言,他们可能很多还比不上我们,比如我们学的时间更长,积累的编程经验或者理论基础都要超过很多入门才一两年的学生,之所以我们还停留在原地,是很多时候我们忽视了软性技能的培养,我们缺乏那一点初生牛犊不怕虎的干劲和勇气。我们都是普通人,又会有多少人有“秀美的容颜”、“聪颖的天资”、“出众的才华”、“骄人的学业”?如果不能改变自己的心态,即便我们的实力达到了某项任务需要的标准,我们的软实力还是会限制我们的发展。我们出身不如名校学生,如果还一直畏手畏脚,那就更不能在社会上与他们相竞争了。


生活、学习和工作中的很多事,其实本没有那么难,往往是自己不自信的心态为之蒙上了一层恐惧的阴影,之后面对机遇就会胆怯。这些年给我最深的感触就是,我们已经因为缺乏自信和勇气失去了好多机会,而如果有新的机遇让我们来尝试,那我们一定要敢闯,敢干,不怕失败,不畏周围可能存在的偏颇眼光,如果不去尝试那一定不会成功,即使尝试失败了也会成为下一次成功的经验砝码,人生从来就没有白走的路。


自信很重要,大胆一些,礼貌的去问就好了。


愿为江水,与君重逢


江水.jpg


这些年的读书过程中,我结识了与之相处时间最久的谭同学,我们认识了八年,在一个寝室生活就有六年多,自然对于双方的认识是无比的熟悉。谭兄一个非常优秀的人,他头脑聪明、思维敏捷,对于新知识和未知领域永远充满兴趣且会一直不断地探索和求知。做事具有极强的目标性,凡事绝不囿于当前的状态,以最大的自信来追求最接近完美的极致,我经常对他说,“你是我们周围同学中我认为最富有极客精神的那一位”,事实上,谭兄作为全院最优秀的学生同时也是最努力的那一位。


作为良师益友,我们在这八年的过程中,最多的讨论就是关于专业知识学习、技术提高、发展规划等方面的内容。在过去的五六年里,我们进行着不同方向的工作,在自己擅长的领域深耕厚植,当遇到涉及对方领域的问题时,我们都十分乐意交流双方的观点与经验。更为重要的,我十分崇尚开源精神,而谭兄也是非常重视学术交流和论点探讨的人,我们都会毫无保留的分享自己的知识及经验,即使对一方在不擅长某个领域时仍会给予尊重并提供最大的帮助,实现真正的成长。十分荣幸遇到了一个优秀的同学,不仅是在学业上得到了很多帮助,因为熟悉互相脾气秉性,我们也在日常的生活中得到了性格的磨合,在做人做事上也有了很大的进步。


我们终将分别,但想想,我的一生能和几个人共同生活达八年之久呢?一切都是命运,愿为江水,与君重逢。他将到昆士兰去继续自己新的学术研究,我也将准备进入我的下一个阶段。正如谭兄经常说的,我们还要经常保持联系,多交流学术问题,以科研会友,期待能够在KDD等顶会上能够见到对方来参会。而我则希望我们在追求理想的道路上还能相互引为知音,就像一条清澈的小溪在流向大海的途中遇到了另一条清澈的小溪,碰巧这两条小溪还是朝着同一个方向流向大海。祝我们都能实现自己的理想,成为自己想成为的那个人!


这是个拥挤的地方,而我却很平凡


在我读研的这几年里,开始逐渐将自己的精力和工作重心从工程转向科研,尽管如此,我仍然会时常关注工程界大致的一些动向,关注互联网职场人以及北漂青年的一些生活和境况,我喜欢他们忙碌的状态、积极的生活态度以及日常努力生活而表现出的韧性。


鼓楼.png
 


北漂人都是可爱的,不管生活有多少苟且都能够坚持自己的理想,在压力中求生存,平凡而生活更显真实。每个人都有自己选择的生活方式,不管遇到什么挫折与不顺,总要挺过去,毕竟生活总要继续,方向总会有的!


飞鹏各自远,且尽手中杯


友情是散落四处的点点温馨,平时想起一座城市,先会想起一些风景,到最后必然只想起这座城市里的朋友,是朋友,决定了我们与各个城市的亲疏。


很荣幸这些年遇到了一些很好的朋友,曾经的本科同学、如今的研究生同学、工作中认识的朋友以及各种社区交流技术和求职过程中遇到的朋友,不管是校内还是校外,不论是在济南还是在北京、南京、深圳......,他们都在我的人生中的不同阶段提供了极大的帮助。更为重要的是,他们分别从事不同领域的工作,分别在不同地域以不同身份生活,所得到的社会经验和生活感悟对我而言更是弥足珍贵的。


还记得东哥曾经给我分享他从周围人学到的做人之道。如何做一个高情商的人,其实就是与人的交往中要擅于交流,而擅于交流的前提在于你知道的很多,眼界开阔,你能够从容温和的和很多人聊,和不同阶层的人聊相应的话题,打破周围人对你的刻板印象,让自己成为一个有内涵又有趣的人。你的知识储备,你的形象以及谈吐都作为以上的评判标准。


听不同的音乐,看不同的书,游历不同的城市,邂逅不同的人,思维与际遇有交集亦有合集,见得多了,站得高了,自然也就看的远了。


我们终将生活在不同的城市,即使不能时常相见,我们也会随时在对方身处困境或者黑暗时及时的带去援助和光亮。所谓朋友,也只不过是互相使对方活得更加温暖、更加自在的那些人。


时空是个圆圈,直行或是转弯,我们总会相见!


最后


仅以上文来记录我当前阶段的状态与生活。《圣经.启示录》中写道:“我又看见一个新天新地,因为先前的天地已经过去了,海也不再有了”。是的,过去已经过去,未来终将来到,这是一个新的开始。


王小灏

开始于2023年4月1日

完成于2023年4月18日

作者:Gopher
来源:juejin.cn/post/7223374346094329911
收起阅读 »

低代码平台是否会取代程序员?答案在这里

上图是一张机器人或者自动化系统取代人工的图片,您看了有哪些感想呢? 故事 程序员小张: 刚毕业,参加工作1年左右,日常工作是CRUD 架构师老李: 多个大型项目经验,精通各种屠龙宝术; 程序员小张和架构师老李是同事,在一家科技公司工作。他们的团队负责开发和...
继续阅读 »

file


上图是一张机器人或者自动化系统取代人工的图片,您看了有哪些感想呢?


故事


file


程序员小张: 刚毕业,参加工作1年左右,日常工作是CRUD


file


架构师老李: 多个大型项目经验,精通各种屠龙宝术;


程序员小张和架构师老李是同事,在一家科技公司工作。他们的团队负责开发和维护公司核心数字系统,但最近他们经常因为应对新需求而焦头烂额。


有一天,老李向小张提出了使用低代码平台来加快应用开发的建议。小张听完后不太理解,认为低代码平台会取代程序员,使开发工作变得简单和机械化。


老李解释说低代码平台并不会取代程序员,而是让专业的开发人员从简单、重复的开发需求中解放出来,让他们能更好地投入到更有价值的事情上,比如梳理系统架构、理清业务逻辑等等,并且低代码平台仍然需要由程序员开发和维护。


小张听后开始思考,认为这个建议确实有道理。他决定利用低代码平台来创建一些敏捷运营类应用,以减轻自己的工作量,并利用更多时间进行系统分析和优化。


于是,小张按照老李的建议利用低代码平台创建了一款敏捷运营类应用,并与老李一起进行了测试和上线。在此过程中,他们深刻领悟到低代码平台的优势,如快速开发、易于维护和扩展等等。


最终,他们成功地为公司创造了一个具有高效性和可扩展性的应用程序,并受到了同事们的好评。小张也明白了使用低代码平台并不是把程序员取代掉,而是让他们更好地利用时间和技能,为团队带来更多价值。


问题


越来越多的企业进行数字化转型,信息化升级。


系统的统一性和集成一直是一个难题,传统模式需要通过多套系统才能满足企业整体的信息化需求,想要统一打通非常困难。


程序员的高薪一直是企业高昂的成本,当出现新的生产工具,必定会对生产关系产生一定影响。


低代码在企业应用开发中的场景?


市场上有哪些种类的低代码平台呢?


低代码到底是怎么降本增效的?


低代码取代程序员怎么破局?


低代码


低代码平台的热度可谓风头无两,


那么低代码是什么呢?简单给它下一个定义。


通过可视化、模块化、拖拽式代替了传统的开发方式,达到了“降本增效”的目的,加速了企业数字化转型的进程的工具平台;


file


1 低代码的使用场景有哪些?


一般来说,核心的业务系统不会使用低代码从0到1搭建,低代码适合搭建基于核心数字系统之上的创新类应用,敏捷运营类应用。
这类应用使用低代码可以极大提升技术研发和系统交付的整体效率。
低代码当前处在技术成熟度曲线中的创新阶段,后面模板会越来越丰富,生态越来越完善,低代码的适用范围也会不断扩大。
下面五类应用是比较适合用低代码来实现的。


种类说明点评
企业门户包括App、小程序、PC门户等等,数据都来自中台、后台,企业门户只是做展示,以及简单的互动。xxcube正在覆盖
数据操作和展示应用通过连接企业的数据库,把生产经营的数据进行编辑删除查询等操作。可预研
基于表单的应用基于数据库的表单收集、处理、统计类应用。xxcube正在覆盖
业务流程类应用定义复杂的工作流,跨部门协作流程,复杂审批流程,比如:OA、人力、财务等系统。xxcube正在覆盖
移动端应用基于已有核心生产经营系统,进行移动化的应用场景。xxcube正在预研和了解

以上5类应用已经覆盖了企业数字化的80%的场景了,低代码的使用范围非常的广泛。


3 低代码平台的分类


目前低代码属于蓝海市场,以中国IT企业的创新速度,未来的竞争会非常激烈。
按照低代码的应用场景,可分为4类。列表对比一下。


种类说明xxcube的对比
原生低代码即面向通用型企业软件的低代码厂商,你可以从0构建一个企业经营系统,使用低代码平台将它实现出来。代表企业: 数睿数据、奥哲、轻流,国外的Mendix、OutSystems等等。具备能力
云平台低代码软件作为云平台生态其中的一环,是云原生一体化的企业解决方案,企业云平台的能力补充。比如阿里云(宜搭,氚云,简道云),腾讯云(微搭),微软(Power Platform)具备能力
行业软件低代码平台本身是非常成熟的行业软件,他们积极拥抱低代码,借助低代码解决企业个性化的需求,拓展软件应用场景。金蝶(企业管理,财务)、明道云(地产物业)、网易数帆(游戏)、销售易(CRM)、Airtable。聚焦游戏发行,游戏开发细分领域
软件开发工具做软件开发工具的,借助低代码实现功能模块化,帮助开发人员进行快速开发。Zion、葡萄城、ClickPaaS具备低代码扩展能力

3 低代码是怎么降本增效的?


file


**低代码,将传统软件开发的6个环节,缩短到了3个环节。**即通过把“需求设计”、“架构设计”、“编码”,聚合为“模块搭建”。


简单来说,低代码的开发模式,就是在需求梳理清楚后,用“拖拉拽”的方式把功能实现出来。


以一个10人天的软件开发需求为例:


传统软件开发模式,总共需要5个人,即:1个产品、2个开发、1个测试、1个运维,开发周期8天。


低代码开发模式,总共需要3人,即:1个产品,1个开发,1个测试,开发周期2天。


也就是说,使用低代码开发模式,同样的需求,节省开发周期70%,减少技术人员40%。


这只是一个粗略的估算。总之,低代码能够用更短的时间、更低的成本,实现软件产品的交付。


4 低代码平台的出现真的会取代程序员吗?


答:不会取代程序员,低代码的优势是可以让专业的开发人员从简单、重复的开发需求中解放出来,把精力投入到更有价值的事情上,比如梳理系统架构、理清业务逻辑等等。
首先,低代码平台不也是程序员开发的吗?再说了,使用低代码进行软件开发的,不还是包含程序员吗?
低代码虽然是“低”,也还是有代码的,除非是无代码平台,在适合的业务场景之下,确实不需要程序员了,可以由业务人员直接搭建应用,但那毕竟是特定场景。
低代码虽然不会干掉程序员,但是对程序员、产品设计人员提出了新的能力要求。越来越多的程序开发工作,将会通过低代码来完成。


技术人员必须到一线中去,跟业务人员在一起,跟最终用户呆在一起,打造更便捷易用的软件产品给业务赋能,用新的科技手段帮助业务转型、业务创新,使企业具备敏捷反应的能力。
否则,企业将会很快被时代抛弃,连打声招呼的机会都没有。


对广大程序员来说,可以按照下面的要点进行应对。


招数说明自查
1. 警惕重复性编码工作CURDboy请多学一些硬核技术。比如梳理系统架构、理清业务逻辑。低代码已经能够完成复杂业务流程类开发工作,能够基于表单驱动、模型驱动的方式进行软件开发工作。最先被替代的就是那些技术含量低的重复性编码工作。聚焦架构设计,云原生,物联网,大数据热门领域的学习和实践
2. 错位竞争,在低代码不擅长的领域深耕对于界面效果要求特别高、复杂的算法和数据挖掘、高性能和复杂系统架构、要求较高的底层开发等方面工作还不能胜任。低代码首先模板,生态,可扩展性限制。聚焦架构设计,云原生,物联网,大数据热门领域的学习和实践
3. 重视企业数字化的建设方法论学习随着低代码的不断成熟,越来越多的业务系统研发工作将由低代码平台来完成,很多企业的老板会看到这块的价值企业运营和建设相关的知识学习中
4. 终生学习,时刻准备职业B计划顺应潮流,学习最先进最高效的生产工具和技术,往往可以对同一赛道的保守选手降维打击;职业B计划是应对职场风险的最佳途径;持续学习技术和管理,持续输出,职业B计划酝酿中

一个心酸的故事,希望能给广大程序员一点启发:



在工业革命时代早期,有工厂主发现纺织机经常在夜里遭到破坏,就派人躲在角落里观察,看看到底是谁在使坏。因为当时没有监控摄像,只能用人肉监控。
结果他们发现,是一些小作坊的纺织工人在搞破坏,原因是他们认为先进的纺织机抢走了他们的工作,而织布是他们赖以生存的手艺,不甘心这几十年修炼出来的一身本领,一夜之间被一台机器所取代,所以破坏机器泄愤。



现在的chatgpt也是影响力类似低代码的先加生产工具,作为程序员应该要主动拥抱他,利用它;


工业时代IT时代启发
工厂IT公司资本或者资本机构: IT时代要重视低代码,作为程序员,要积极拥抱低代码,这是新的生产工具,可以成为自己的武器。低代码时代已来,不必焦虑,冲过去大力拥抱它,相信这一次,时间会站在变革者这一边。一些行业资深人士,视低代码为“毒瘤”,不仅暴露了自身的无知,也误导了部分从业者,也引起了无谓的恐慌。
纺织机低代码生产工具-静态
工人程序员\业务人员生产工具-动态


原创不易,关注诚可贵,转发价更高!转载请注明出处,让我们互通有无,共同进步,欢迎沟通交流。



作者:李福春
来源:juejin.cn/post/7222832297854287929
收起阅读 »

情侣空间动态时间效果,你学废了吗?

web
前言 中秋这天刚好碰上和女朋友在一起的 五周年 了,想来五年风风雨雨仍然好好的,挺是感慨,也挺满足的。qq情侣空间也毫不意外的准点报时了,闲来无事点进去看了看,瞥到一个动效,觉得很有意思,于是打算自己动手实现一下,也算是尝试了。 动效长这样: 码上掘金 动态...
继续阅读 »

前言


中秋这天刚好碰上和女朋友在一起的 五周年 了,想来五年风风雨雨仍然好好的,挺是感慨,也挺满足的。qq情侣空间也毫不意外的准点报时了,闲来无事点进去看了看,瞥到一个动效,觉得很有意思,于是打算自己动手实现一下,也算是尝试了。


动效长这样:


Video_20220912_014016_213.gif


码上掘金


动态日期特效 - 码上掘金 (juejin.cn)


思路解析


日期函数的使用频率可以说是很高了,不管是原生手写也好,还是用的 day.js 这种第三方库,在业务开发中我们经常需要处理日期进行展示。由于这边的时间处理不复杂,因此我们直接手写一个就好了。


获取年月日我们分别使用 getFullYear()getMonth() 还有 getDate() ,需要注意的是,很多新手小伙伴经常会把 getDay() 误以为是获取日的功能,实际上是用的 getDate() 实现的,还有一个需要注意的地方是,获取月份的函数 getMonth() ,获取的时间范围是 (0,11) ,没错,它是从 0 开始的,最后为了展示我们还需要让它 +1


时分秒我们分别使用 getHours()getMinutes() 还有 getSeconds() 三个方法,其中,分和秒的函数返回的是 (0, 59),也就是说不超过两位数的话,输出形式是个位数,我们需要手动补 0,可以通过字符串拼接过 padStart() 实现。


最后,我们观察一下这个效果,实际上它只有最后一个数字是向上淡出的,也就是说我们只要处理这个数字就好了,那么问题就简单了。


我们先将秒的个位和十位分开,将它们分为两个部分单独展示,这样我们就可以单独处理这个数字的特效了。


向上淡出,你第一思路是什么?


对了,是定位+透明,我们就用这个思路试一试。


一开始给它设置为相对定位 position: relative。接下来实现动效,为了让它不断的有这么个淡出效果,我们自然而然想到要使用动画,从当前位置开始,结束的时候增大透明度并且向上移动,逻辑很快就写好了。


涉及知识点


1. Date 日期类




  • Date.prototype.getDate():根据本地时间,返回一个指定的 Date 对象为一个月中的哪一日(1-31)。




  • Date.prototype.getFullYear():根据本地时间,返回一个指定的 Date 对象的完整年份(四位数年份)。




  • Date.prototype.getHours():根据本地时间,返回一个指定的 Date 对象的小时(023)。




  • Date.prototype.getMinutes():根据本地时间,返回一个指定的 Date 对象的分钟数(059)。




  • Date.prototype.getMonth():根据本地时间,返回一个指定的 Date 对象的月份(011),0 表示一年中的第一月。




  • Date.prototype.getSeconds():根据本地时间,返回一个指定的 Date 对象的秒数(059)。




2. 时间补零


getMinutes()getSeconds() 获取的时间是没有前缀零的,我们可以判断一下,如果时间小于 10 ,则用 0 拼接。


也可以使用 padStart() 进行补零操作。



关于 padStart() 的更多用法,详见:String.prototype.padStart() - JavaScript | MDN (mozilla.org)



结束语


相信不少小伙伴像我一样,因为行业原因,工作中动效开发频率很低,这块的实战经验也很薄弱,为了以后能更好的搬砖,我们应该从小 demo 开始,不断的去练习提升,基础进阶两手抓。


作者:CatWatermelon
来源:juejin.cn/post/7142412506815250445
收起阅读 »

明修"栈"道——越过Android启动栈陷阱

作者:vivo 互联网大前端团队- Zhao Kaiping 本文从一例业务中遇到的问题出发,以FLAG_ACTIVITY_NEW_TASK这一flag作为切入点,带大家探究Activity启动前的一项重要的工作——栈校验。 文中列举一系列业务中可能遇到的异...
继续阅读 »

作者:vivo 互联网大前端团队- Zhao Kaiping



本文从一例业务中遇到的问题出发,以FLAG_ACTIVITY_NEW_TASK这一flag作为切入点,带大家探究Activity启动前的一项重要的工作——栈校验。


文中列举一系列业务中可能遇到的异常状况,详细描述了使用FLAG_ACTIVITY_NEW_TASK时可能遇到的“坑”,并从源码中探究其根源。只有合理使用flag、launchMode,才能避免因为栈机制的特殊性,导致一系列与预期不符的启动问题。


一、问题及背景


应用间相互联动、相互跳转,是实现系统整体性、体验一致性的重要手段,也是最简单的一种方法。


当我们用最常用的方法去startActivity时,竟也会遇到失败的情况。在真实业务中,就遇到了这样一例异常:用户点击某个按钮时,想要“简简单单”跳转另一个应用,却没有任何反应。


经验丰富的你,脑海中是否涌现出了各种猜想:是不是目标Activity甚至目标App不存在?是不是目标Activty没有对外开放?是不是有权限的限制或者跳转的action/uri错了……


真实的原因被flag、launchMode、Intent等特性层层藏匿,可能超出你此时的思考。


本文将从源码出发,探究前因后果,展开讲讲在startActivity()真正准备启动一个Activity前,需要经过哪些“磨难”,怎样有据可依地解决由栈问题导致的启动异常。


1.1 业务中遇到的问题


业务中的场景是这样的,存在A、B、C三个应用。


(1)从应用A-Activity1跳转至应用B-Activity2;


(2)应用B-Activity2继续跳转到应用C-Activity3;


(3)C内某个按钮,会再次跳转B-Activity2,但点击后没有任何反应。如果不经过前面A到B的跳转,C直接跳到B是可以的。


图片


1.2 问题代码


3个Activity的Androidmanifest配置如下,均可通过各自的action拉起,launchMode均为标准模式。


<!--应用A--> 
<activity
android:name=".Activity1"
android:exported="true">
<intent-filter>
<action android:name="com.zkp.task.ACTION_TO_A_PAGE1" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>

<!--应用B-->
<activity
android:name=".Activity2"
android:exported="true">
<intent-filter>
<action android:name="com.zkp.task.ACTION_TO_B_PAGE2" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>

<!--应用C-->
<activity
android:name=".Activity3"
android:exported="true">
<intent-filter>
<action android:name="com.zkp.task.ACTION_TO_C_PAGE3" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>

A-1到B-2的代码,指定flag为FLAG_ACTIVITY_NEW_TASK


private void jumpTo_B_Activity2_ByAction_NewTask() {
Intent intent = new Intent();
intent.setAction("com.zkp.task.ACTION_TO_B_PAGE2");
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
}

B-2到C-3的代码,未指定flag


private void jumpTo_C_Activity3_ByAction_NoTask() {
Intent intent = new Intent();
intent.setAction("com.zkp.task.ACTION_TO_C_PAGE3");
startActivity(intent);
}

C-3到B-2的代码,与A-1到B-2的完全一致,指定flag为 FLAG_ACTIVITY_NEW_TASK


private void jumpTo_B_Activity2_ByAction_NewTask() {
Intent intent = new Intent();
intent.setAction("com.zkp.task.ACTION_TO_B_PAGE2");
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
}

1.3 代码初步分析


仔细查看问题代码,在实现上非常简单,有两个特征:


(1)如果直接通过C-3跳B-2,没有任何问题,但A-1已经跳过B-2后,C-3就失败了。


(2)在A-1和C-3跳到B-2时,都设置了flag为FLAG_ACTIVITY_NEW_TASK。


依据经验,我们推测与栈有关,尝试将跳转前栈的状态打印出来,如下图。


图片


由于A-1跳到B-2时设置了FLAG_ACTIVITY_NEW_TASK,B-2跳到C-3时未设置,所以1在独立栈中,2、3在另一个栈中。示意如下图。


图片


C-3跳转B-2一般有3种可能的预期,如下图:预想1,新建一个Task,在新Task中启动一个B-2;预想2,复用已经存在的B-2;预想3,在已有Task中新建一个实例B-2。


图片


但实际上3种预期都没有实现,所有Activity的任何声明周期都没有变化,界面始终停留在C-3。


看一下FLAG_ACTIVITY_NEW_TASK的官方注释和代码注释,如下图:



图片


重点关注这一段:



When using this flag, if a task is already running for the activity you are now starting, then a new activity will not be started; instead, the current task will simply be brought to the front of the screen with the state it was last in.


使用此flag时,如果你正在启动的Activity已经在一个Task中运行,那么一个新Activity不会被启动;相反,当前Task将简单地显示在界面的前面,并显示其最后的状态。



——显然,官方文档与代码注释的表述与我们的异常现象是一致的,目标Activity2已经在Task中存在,则不会被启动;Task直接显示在前面,并展示最后的状态。由于目标Activty3就是来源Activity3,所以页面没有任何变化。


看起来官方还是很靠谱的,但实际效果真的能一直与官方描述一致吗?我们通过几个场景来看一下。


二、场景拓展与验证


2.1 场景拓展


在笔者依据官方描述进行调整、复现的过程中,发现了几个比较有意思的场景。


PS:上面业务的案例中,B-2和C-3在不同应用内,又在相同的Task内,但实际上是否是同一个应用,对结果的影响并不大。为了避免不同应用和不同Task造成阅读混乱,同一个栈的跳转,我们都在本应用内进行,故业务中的场景等价于下面的【场景0】



【场景0】把业务中B-2到C-3的应用间跳转改为B-2到B-3的应用内跳转



// B-2跳转B-3
public static void jumpTo_B_3_ByAction_Null(Context context) {
Intent intent = new Intent();
intent.setAction("com.zkp.task.ACTION_TO_B_PAGE3");
context.startActivity(intent);
}

如下图,A-1设置NEW_TASK跳转B-2,再跳转B-3,最终设置NEW_TASK想跳转B-2。虽然跳C-3改为了跳B-3,但与之前问题的表现一致,没有反应,停留在B-3。


图片


有的读者会指出这样的问题:如果同一个应用内使用NEW_TASK跳转,而不指定目标的taskAffinity属性,实际是无法在新Task中启动的。请大家忽略该问题,可以认为笔者的操作是已经加了taskAffinity的,这对最终结果并没有影响。



【场景1】如果目标Task和来源Task不是同一个,情况是否会如官方文档所说复用已有的Task并展示最近状态?我们改为B-3启动一个新Task的新Activity C-4,再通过C-4跳回B-2



// B-3跳转C-4
public static void jumpTo_C_4_ByAction_New(Context context) {
Intent intent = new Intent("com.zkp.task.ACTION_TO_C_PAGE4");
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
}
// C-4跳转B-2
public static void jumpTo_B_2_ByAction_New(Context context) {
Intent intent = new Intent();
intent.setAction("com.zkp.task.ACTION_TO_B_PAGE2");
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
}

如下图,A-1设置NEW_TASK跳转B-2,再跳转B-3,再设置NEW_TASK跳转C-4,最终设置NEW_TASK想跳转B-2。


图片


预想的结果是:不会跳到B-2,而是跳到它所在Task的顶层B-3。


实际的结果是:与预期一致,确实是跳到了B-3。



【场景2】把场景1稍做修改:C-4到B-2时,我们不通过action来跳,改为通过setClassName跳转



// C-4跳转B-2
public static void jumpTo_B_2_ByPath_New(Context context) {
Intent intent = new Intent();
intent.setClassName("com.zkp.b", "com.zkp.b.Activity2"); // 直接设置classname,不通过action
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
}

如下图,A-1设置NEW_TASK跳转B-2,再跳转B-3,再设置NEW_TASK跳转C-4,最终设置NEW_TASK想跳转B-2。


图片


预想的结果是:与场景0一致,会跳到B-2所在Task的已有顶层B-3。


实际的结果是:在已有的Task2中,产生了一个新的B-2实例。


仅仅是改变了一下重新跳转B-2的方式,效果就完全不一样了!这与官方文档中提到该flag与"singleTask" launchMode值产生的行为并不一致!



【场景3】把场景1再做修改:这次C-4不跳栈底的B-2,改为跳转B-3,且还是通过action方式。



// C-4跳转B-3
public static void jumpTo_B_3_ByAction_New(Context context) {
Intent intent = new Intent();
intent.setAction("com.zkp.task.ACTION_TO_B_PAGE3");
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
}

如下图,A-1设置NEW_TASK跳转B-2,再跳转B-3,再设置NEW_TASK跳转C-4,最终设置NEW_TASK想跳转B-3。


图片


预想的结果是:与场景0一致,会跳到B-2所在Task的顶层B-3。


实际的结果是:在已有的Task2中,产生了一个新的B-3实例。


不是说好的,Activity已经存在时,展示其所在Task的最新状态吗?明明Task2中已经有了B-3,并没有直接展示它,而是生成了新的B-3实例。



【场景4】既然Activity没有被复用,那Task一定会被复用吗?把场景3稍做修改,直接给B-3指定一个单独的affinity。



<activity
android:name=".Activity3"
android:exported="true"
android:taskAffinity="b3.task"><!--指定了亲和性标识-->
<intent-filter>
<action android:name="com.zkp.task.ACTION_TO_B_PAGE3" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>

如下图,A-1设置NEW_TASK跳转B-2,再跳转B-3,再设置NEW_TASK跳转C-4,最终设置NEW_TASK想跳转B-3。



——这次,连Task也不会再被复用了……Activity3在一个新的栈中被实例化了。


再回看官方的注释,就会显得非常不准确,甚至会让开发者对该部分的认知产生严重错误!稍微改变过程中的某个毫无关联的属性(如跳转目标、跳转方式……),就会产生很大差异。


在看flag相关注释时,我们要树立一个意识:Task和Activity跳转的实际效果,是launchMode、taskAffinity、跳转方式、Activity在Task中的层级等属性综合作用的结果,不要相信“一面之词”。


回到问题本身,究竟是哪些原因造就了上面的不同效果呢?只有源码最值得信赖了。


三、场景分析与源码探索


本文以Android 12.0源码为基础,进行探究。上述场景在不同Android版本上的表现是一致的。


3.1 源码调试注意事项


源码的调试方法,许多文章已经有了详细的教学,本文不再赘述。此处只简单总结其中需要注意的事项





  1. 下载模拟器时,不要使用Google Play版本,该版本类似user版本,无法选择system_process进程进行断点。




  2. 即使是Google官方模拟器和源码,在断点时,也会有行数严重不对应的情况(比如:模拟器实际会运行到方法A,但在源码中打断点时,实际不能定位到方法A的对应行数),该问题并没有很好的处理方法,只能尽量规避,如使模拟器版本与源码版本保持一致、多打一些断点增加关键行数被定位到的几率。





3.2 初步断点,明确启动结果


以【场景0】为例,我们初步确认一下,为什么B-3跳转B-2会无反应,系统是否告知了原因。


3.2.1 明确启动结果及其来源


在Android源码的断点调试中,常见的有两类进程:应用进程和system_process进程。


在应用进程中,我们能获取到应用启动结果的状态码result,这个result用来告诉我们启动是否成功。涉及堆栈如下图(标记1)所示:


Activity类::startActivity() → startActivityForResult() 
→ Instrumentation类::execStartActivity(),返回值result则是ATMS
(ActivityTaskManagerService)执行的结果。


图片


如上图(标记2)标注,ATMS类::startActivity()方法,返回了result=3。


在system_process进程中,我们看一下这个result=3是怎样被赋值的。略去详细断点步骤,实际堆栈如下图(标注1)所示:


ATMS类::startActivity() → startActivityAsUser() 
→ ActivityStarter类::execute() 
→ executeRequest() 
→ startActivityUnchecked() 
→ startActivityInner() 
→ recycleTask(),在recycleTask()中返回了结果。


图片


如上图(标注2)所示,result在mMovedToFront=false时被赋值,即result=START_DELIVERED_TO_TOP=3,而START_SUCCESS=0才代表创建成功。


看一下源码中对START_DELIVERED_TO_TOP的说明,如下图:




Result for IActivityManaqer.startActivity: activity wasn't really started, but the given Intent was given to the existing top activity.


(IActivityManaqer.startActivityActivity的结果:Activity并未真正启动,但给定的Intent已提供给现有的顶层Activity。)



“Activity并未真正启动”——是的,因为可以复用


“给定的Intent已提供给现有的顶层Activity”——实际没有,顶层Activity3并没有收到任何回调,onNewIntent()未执行,甚至尝试通过Intent::putExtra()传入新的参数,Activity3也没有收到。官方文档又带给了我们一个疑问点?我们把这个问题记录下来,在后面分析。


满足什么条件,才会造成START_DELIVERED_TO_TOP的结果呢?笔者的思路是,通过与正常启动流程对比,找出差异点。


3.3 过程断点,探索启动流程


一般来说,在定位问题时,我们习惯通过结果反推原因,但反推的过程只能关注到与问题强关联的代码分支,并不能能使我们很好地了解全貌。


所以,本节内容我们通过顺序阅读的方法,正向介绍startActivity过程中与上述【场景01234】强相关的逻辑。再次简述一下:





  1. 【场景0】同一个Task内,从顶部B-3跳转B-2——停留在B-3




  2. 【场景1】从另一个Task内的C-4,跳转B-2——跳转到B-3




  3. 【场景2】把场景1中,C-4跳转B-2的方式改为setClassName()——创建新B-2实例




  4. 【场景3】把场景1中,C-4跳转B-2改为跳转B-3——创建新B-3实例




  5. 【场景4】给场景3中的B-3,指定taskAffinity——创建新Task和新B-3实例





3.3.1 流程源码概览


源码中,整个启动流程很长,涉及的方法和逻辑也很多,为了便于大家理清方法调用顺序,方便后续内容的阅读,笔者将本文涉及到的关键类及方法调用关系整理如下。


后续阅读中如果不清楚调用关系,可以返回这里查看:


// ActivityStarter.java

ActivityStarter::execute() {
executeRequest(intent) {
startActivityUnchecked() {
startActivityInner();
}
}
ActivityStarter::startActivityInner() {
setInitialState();
computeLaunchingTaskFlags();
Task targetTask = getReusableTask(){
findTask();
}
ActivityRecord targetTaskTop = targetTask.getTopNonFinishingActivity();
if (targetTaskTop != null) {
startResult = recycleTask() {
setTargetRootTaskIfNeeded();
complyActivityFlags();
if (mAddingToTask) {
return START_SUCCESS; //【场景2】【场景3】从recycleTask()返回
}
resumeFocusedTasksTopActivities()
return mMovedToFront ? START_TASK_TO_FRONT : START_DELIVERED_TO_TOP;//【场景1】【场景0】从recycleTask()返回
}
} else {
mAddingToTask = true;
}
if (startResult != START_SUCCESS) {
return startResult;//【场景1】【场景0】从startActivityInner()返回
}
deliverToCurrentTopIfNeeded();
resumeFocusedTasksTopActivities();
return startResult;
}

3.3.2 关键流程分析


(1)初始化


startActivityInner()是最主要的方法,如下列几张图所示,该方法会率先调用setInitialState(),初始化各类全局变量,并调用reset(),重置ActivityStarter中各种状态。


在此过程中,我们记下两个关键变量mMovedToFront和mAddingToTask,它们均在此被重置为false。


其中,mMovedToFront代表当Task可复用时,是否需要将目标Task移动到前台;mAddingToTask代表是否要将Activity加入到Task中。


图片


图片


图片


(2)计算确认启动时的flag


该步骤会通过computeLaunchingTaskFlags()方法,根据launchMode、来源Activity的属性等进行初步计算,确认LaunchFlags。


此处重点处理来源Activity为空的各类场景,与我们上文中的几种场景无关,故不再展开讲解。


(3)获取可以复用的Task


该步骤通过调用getReusableTask()实现,用来查找有没有可以复用的Task。


先说结论:场景0123中,都能获取到可以复用的Task,而场景4中,未获取到可复用的Task。


为什么场景4不可以复用?我们看一下getReusableTask()的关键实现。


图片


上图(标注1)中,putIntoExistingTask代表是否能放入已经存在的Task。当flag含有NEW_TASK且不含MULTIPLE_TASK时,或指定了singleInstance或singleTask的launchMode等条件,且没有指定Task或要求返回结果 时,场景01234均满足了条件。


然后,上图(标注2)通过findTask()查找可以复用的Task,并将过程中找到的栈顶Activity赋值给intentActivity。最终,上图(标注3)将intentActivity对应的Task作为结果。


findTask()是怎样查找哪个Task可以复用呢?


图片


主要是确认两种结果mIdealRecord——“理想的ActivityRecord”  和 mCandidateRecord——"候选的ActivityRecord",作为intentActivity,并取intentActivity对应的Task作为复用Task。


什么ActivityRecord才是理想或候选的ActivityRecord呢?在mTmpFindTaskResult.process()中确认。


图片


程序会将当前系统中所有的Task进行遍历,在每个Task中,进行如上图所示的工作——将Task的底部Activity realActivity与目标Activity cls进行对比。


场景012中,我们想跳转Activity2,即cls是Activity2,与Task底部的realActivity2相同,则将该Task顶部的Activity3 r作为“理想的Activity”;


场景3中,我们想跳转Activity3,即cls是Activity3,与Task底部的realActivity2不同,则进一步判断task底部Activity2与目标Activity3的栈亲和行,具有相同亲和性,则将Task的顶部Activity3作为“候选Activity”;


场景4中,所有条件都不满足,最终没能找到可复用的Task。在执行完getReusableTask()后将mAddingToTask赋值为true


由此,我们就能解释【场景4】中,新建了Task的现象。


(4)确定是否需要将目标Task移动到前台


如果存在可复用的Task,场景0123会执行recycleTask(),该方法中会相继进行几个操作:setTargetRootTaskIfNeeded()、complyActivityFlags()。


首先,程序会执行setTargetRootTaskIfNeeded(),用来确定是否需要将目标Task移动到前台,使用mMovedToFront作为标识。


图片


图片


在【场景123】中,来源Task和目标Task是不同的,differentTopTask为true,再经过一系列Task属性对比,能够得出mMovedToFront为true;


而场景0中,来源Task和目标Task相同,differentTopTask为false,mMovedToFront保持初始的false。


由此,我们就能解释【场景0】中,Task不会发生切换的现象。


(5)通过对比flag、Intent、Component等确认是否要将Activity加入到Task中


还是在【场景0123】中,recycleTask()会继续执行complyActivityFlags(),用来确认是否要将Activity加入到Task中,使用mAddingToTask作为标识。


该方法会对FLAG_ACTIVITY_NEW_TASK、FLAG_ACTIVITY_CLEAR_TASK、FLAG_ACTIVITY_CLEAR_TOP等诸多flag、Intent信息进行一系列判断。


图片


上图(标注1)中,会先判断后续是否需要重置Task,resetTask,判断条件则是FLAG_ACTIVITY_RESET_TASK_IF_NEEDED,显然,场景0123的resetTask都为false。继续执行。


接着,会有多种条件判断按顺序执行。


在【场景3】中,目标Component(mActivityComponent)是B-3,目标Task的realActivity则是B-2,两者不相同,进入了resetTask相关的判断(标注2)。


之前resetTask已经是false,故【场景3】的mAddingToTask脱离原始值,被置为true。


在【场景012】中,相对比的两个Activity都是B-2(标注3),可以进入下一级判断——isSameIntentFilter()。


图片


图片


图片


这一步判断的内容就很明显了,目标Activity2的已有Intent 与 新的Intent做对比。很显然,场景2中由于改为了setClassName跳转,Intent自然不一样了。


故【场景2】的mAddingToTask脱离原始值,被置为true。


总结看一下:



【场景123】的mMovedToFront最先被置为true,而【场景0】经历重重考验,保持初始值为false。


——这意味着当有可复用Task时,【场景0】不需要把Task切换到前列;【场景123】需要切换到目标Task。


【场景234】的mAddingToTask分别在不同阶段被置为true,而【场景01】,始终保持初始值false。


——这意味着,【场景234】需要将Activity加入到Task中,而【场景01】不再需要。



(6)实际启动Activity或直接返回结果


被启动的各个Activity会通过resumeFocusedTasksTopActivities()等一系列操作,开始真正的启动与生命周期的调用。


我们关于上述各个场景的探索已经得到答案,后续流程便不再关注。


四、问题修复及遗留问题解答


4.1 问题修复


既然前面总结了这么多必要条件,我们只需要破坏其中的某些条件,就可以修复业务中遇到的问题了,简单列举几个的方案。




  • 方案一:修改flag。B-3跳转B-2时,增加FLAG_ACTIVITY_CLEAR_TASK或FLAG_ACTIVITY_CLEAR_TOP,或者直接不设置flag。经验证可行。




  • 方案二:修改intent属性,即【场景2】。A-1通过action方式隐式跳转B-2,则B-3可以通过setClassName方式,或修改action内属性的方式跳转B-2。经验证可行。




  • 方案三:提前移除B-2。B-2跳转B-3时,finish掉B-2。需要注意的是,finish()要在startActivity()之前执行,以避免遗留的ActivityRecord和Intent信息对后续跳转的影响。尤其是当你把B-2作为自己应用的deeplink分发Activity时,更值得警惕。




4.2 遗留问题


还记得我们在文章开端的某个疑惑吗,为什么没有回调onNewIntent()?


onNewIntent() 会通过deliverNewIntent()触发,而deliverNewIntent()仅通过以下两个方法调用。


图片


complyActivityFlags()就是上文3.3.1.5中我们着重探讨的方法,可以发现complyActivityFlags()中所有可能调用deliverNewIntent()的条件均被完美避开了。


而deliverToCurrentTopIfNeeded()方法则如下图所示。


图片


mLaunchFlags和mLaunchMode,无法满足条件,导致dontStart为false,无缘deliverNewIntent()。


至此,onNewIntent()的问题得到解答。


五、结语


通过一系列场景假设,我们发现了许多出乎意料的现象:





  1. 文档提到FLAG_ACTIVITY_NEW_TASK等价于singleTask,与事实并不完全如此,只有与其他flag搭配才能达到相似的效果。这一flag的注释非常片面,甚至会引发误解,单一因素无法决定整体表现。




  2. 官方文档提到




  3. START_DELIVERED_TO_TOP会将新的Intent传递给顶层Activity,但事实上,并不是每一种START_DELIVERED_TO_TOP都会把新的Intent重新分发。




  4. 同一个栈底Activity,前后两次都通过action或都通过setClassName跳转到时,第二次跳转竟然会失败,而两次用不同方式跳转时,则会成功。




  5. 单纯使用FLAG_ACTIVITY_NEW_TASK时,跳栈底Activity和跳同栈内其他Activity的效果大相径庭。





业务中遇到的问题,归根结底就是对Android栈机制不够了解造成的。


在面对栈相关的编码时,开发者务必要想清楚,承担新开应用栈的Activty在应用全局承担怎样的使命,要对Task历史、flag属性、launchMode属性、Intent内容等全面评估,谨慎参考官方文档,才能避免栈陷阱,达成理想可靠的效果。


作者:vivo互联网技术
来源:juejin.cn/post/7223175468621774907
收起阅读 »

如何接入小程序订阅消息?

web
更新完微信服务号的模板消息之后,我又赶紧把微信小程序的订阅消息给实现了!之前我一直以为微信小程序也是要企业才能申请,没想到小程序个人就能申请。 消息推送平台🔥推送下发【邮件】【短信】【微信服务号】【微信小程序】【企业微信】【钉钉】等消息类型。 gitee....
继续阅读 »

更新完微信服务号的模板消息之后,我又赶紧把微信小程序的订阅消息给实现了!之前我一直以为微信小程序也是要企业才能申请,没想到小程序个人就能申请。



消息推送平台🔥推送下发【邮件】【短信】【微信服务号】【微信小程序】【企业微信】【钉钉】等消息类型




本想着跟微信服务号的模板消息一样,我去申请一个「测试号」,就能下发微信小程序的订阅消息了。但微信小程序的订阅消息一直不支持「测试号」下发。


于是,我就注册了一个微信小程序,拿到我的小程序appIdsecret,并在微信后台创建了几个订阅消息的模板。



紧接着,这个把小程序的账号就接入到消息推送平台的账号管理体系下:



写几行代码得到刚才创建的模板,顺便跟前端来个简单的交互:




改几行代码,把具体调用微信的逻辑给补上,有SDK的加持下这种代码就是10min就完成了,非常简单。


image-20221208202228360


实现小程序的订阅消息推送,我花的时间最长就花在这下面啦:


1、拥有自己的小程序(拥有调试基础)


2、让自己的登录到这个小程序里(得到openId)


3、小程序弹窗让我能授权给微信发送订阅消息(得到推送权限)


小程序的账号我已经创建好了,但是小程序是没有任何内容的。于是我就在小程序的后台点点点,顺便看看小程序一般是怎么开发的。于是,我就看到了微信小程序的开发者工具:


developers.weixin.qq.com/miniprogram…


下载了之后,这个小工具给我推荐「云开发」,只要花点钱我就可以调用云函数了。虽然要19.9块,有点肉疼。但省时间的事,我咬咬牙就上了。



完了以后,我在小程序工具箱里翻了好几个模板,看看有没有我想要的功能:登录小程序获取openId、弹窗让我授权模板发送消息。你别说,真给我翻到一个:



我是没做过小程序的,自然就不会小程序开发,于是就只能摸石头过河了。花了一天多,发现我在这个demo项目里获取的openId就是死活的调不通小程序(报错就说不合法openId)。


经过漫长的调试,我忍不了了,再这样耗下去是不行的了。我直接去GitHub看看有没有现成的demo,随便一搜,还真的有。



github.com/zhangkaizha…


直接将「wxapp」文件下导入到小程序的开发工具里,一看,还真能用,代码又少。回看同步openId的代码,原来是要调接口请求微信做鉴权的呀。



于是我在消息推送平台里也临时写了接口进行鉴权,在小程序调用登录的时候改下入参就完事咯。




经过登录凭证校验之后,我们就能拿到openId,把订阅消息的权限界面给唤起,点击允许,就能在消息推送平台下发送一条小程序的订阅消息啦。




代码方面我就不细说啦,感兴趣的同学可以把项目搞下来玩玩,源码都是有的。这几天还在疯狂更新中,看看目前的消息渠道接入的情况吧?


如果想学Java项目的,强烈推荐我的开源项目消息推送平台Austin(8K stars) ,可以用作毕业设计,可以用作校招,可以看看生产环境是怎么推送消息的。开源项目消息推送平台austin仓库地址:



消息推送平台🔥推送下发【邮件】【短信】【微信服务号】【微信小程序】【企业微信】【钉钉】等消息类型





作者:Java3y
来源:juejin.cn/post/7223728273007493176
收起阅读 »

创造一个自己的chatgpt:感知机系统(Perceptron)基础01

感知机是什么? 由几代科学家思想精华积累,最终由Frank Rosenblatt发扬光大的一个计算机系统的概念。 简述原始的定义可以概括为: 一个像人脑一样通过多方面的学习认知的计算机,这个系统不是依靠通过事先编程的确定规则,而是依靠概率统计的方法,通过分析大...
继续阅读 »

感知机是什么?


由几代科学家思想精华积累,最终由Frank Rosenblatt发扬光大的一个计算机系统的概念。


简述原始的定义可以概括为:


一个像人脑一样通过多方面的学习认知的计算机,这个系统不是依靠通过事先编程的确定规则,而是依靠概率统计的方法,通过分析大量数据得到的统计结果来确保程序的可靠性。以这种方式运作的系统被称为"感知机系统"


简单举例来讲,一个关于判断一张图片是不是猫的Perceptron猫系统,就是通过分析训练有关于猫的大量的图片数据,从而可以在输入一张新的图片时,Perceptron猫系统程序会告知你这张图片是不是一个猫(输出分成两类: 是猫 1 或不是猫 0)


现代简化版本概念


Perceptron算法的核心思想是使用一个线性函数,将输入数据映射到一个输出值,然后使用一个阈值函数将该输出值转换为 1 或 0。在学习过程中,Perceptron会自动调整其权重和阈值核心步骤,以便正确分类输入数据。


如下图所示


image.png


其表示的Perceptron系统含义可以理解为:


x1输入值对应的权重为w1


x2输入值对应的权重为w2


当 x1 * w1 + x2 * w2 小于等于阈值(threshold)时Perceptron系统输出 0


当 x1 * w1 + x2 * w2 大于阈值(threshold)时Perceptron系统输出 1


结合具体的与门系统例子说明,与门系统是一个如下描述的系统


与门系统的的输入只能是0或1,输出是0,1,有两个输入值即 x1 ,x2,如果两个输入都是1,则输出为1,否则都输出0,即下图所示


image.png


那么对应的权重值 w1, w2,阈值z可以是 (1,1,1)或其他任意满足以上系统规定的值


这是通过人类大脑思考得到的权重以及阈值,怎么通过程序去实现自动找到准确的阈值和权重值呢?


首先,我们将使用JavaScript编写一个Perceptron类,该类将具有以下方法:



  • constructor:用于随机初始化权重和阈值

  • train:用于训练Perceptron,调整其权重和阈值以得到正确的输出值

  • guess:用于预测输入数据的类别


class Perceptron {
constructor(numInputs) {
this.weights = new Array(numInputs);
for (let i = 0; i < numInputs; i++) {
// 随机初始化 x1 x2权重值
this.weights[i] = Math.random() * 2 - 1;
}
// 随机初始化阈值 z
this.threshold = Math.random() * 2 - 1;
}

train(inputs, target) {
let guess = this.guess(inputs);
let error = target - guess;
for (let i = 0; i < this.weights.length; i++) {
// 矫正权重值
this.weights[i] += error * inputs[i];
}
// 矫正阈值
this.threshold += error;
}

guess(inputs) {
let sum = 0;
for (let i = 0; i < this.weights.length; i++) {
sum += inputs[i] * this.weights[i];
}
return sum > this.threshold ? 1 : 0;
}
}

如果我们不训练,直接推断


let andPerceptron = new Perceptron(2);
andPerceptron.train([1, 1]); // 返回0 不是我们想要的结果

经过10000次训练,很容易自动推断出正确的权重值以及阈值


let andPerceptron = new Perceptron(2);

// 训练AND门
for (let i = 0; i < 10000; i++) {
// 四种情况正确的数据推断
andPerceptron.train([0, 0], 0);
andPerceptron.train([0, 1], 0);
andPerceptron.train([1, 0], 0);
andPerceptron.train([1, 1], 1);
}

// 测试AND门
console.log(andPerceptron.guess([0, 0])); // 0
console.log(andPerceptron.guess([0, 1])); // 0
console.log(andPerceptron.guess([1, 0])); // 0
console.log(andPerceptron.guess([1, 1])); // 1

距离创造自己的chatgat迈出了第一步,也是最重要的一步,欢迎点赞收藏


作者:道可到
来源:juejin.cn/post/7219298341462392889
收起阅读 »

关于如何写一个有层次感的css遮罩效果

web
前言 写了这么多天游戏了,最近也想换换口味写一点有趣的CSS动效,这次我为大家带来的就是一个纯css的动态效果,层次感遮罩。在现在审美疲劳的大时代环境背景下,对页面的设计也开始越来越追求他的层次感和立体感,有时候因为一个好的板块动效,可以拉高整体的页面颜值增添...
继续阅读 »

前言


写了这么多天游戏了,最近也想换换口味写一点有趣的CSS动效,这次我为大家带来的就是一个纯css的动态效果,层次感遮罩。在现在审美疲劳的大时代环境背景下,对页面的设计也开始越来越追求他的层次感和立体感,有时候因为一个好的板块动效,可以拉高整体的页面颜值增添出页面的高级感,让网页用户有一种油然而生的成为这个页面用户是一种非常自豪事情的感觉。(当然如果甲方觉得麻垮你说的再牛B也是白搭)


那么接下来我们马上开始今天的代码解析


实现步骤


创建出基本元素


不难看出页面元素其实就是一个边框加一段文字,但这个边框并非一个div加上border属性这么简单,这里我用的是伪元素


先写好html标签,这里用阿a标签什么的都可以。


<a href="#">荆棘鸟QAQ</a>

用css把整个页面变成灰色的,并且为了突出文字,把a标签居中,文字变为白色,再加上一个文字阴影。


body {
font-family: "黑体";
background-color: #555;
}

a {
color: #fffbf1;
text-shadow: 0 20px 25px #2e2e31;
font-size: 80px;
font-weight: bold;
text-decoration: none;
position: absolute;
top: 50%;
left: 50%;
transform: translateX(-50%) translateY(-50%);
}

image.png


添加伪类


这个就是重头戏了,因为层次效果全靠after和before这两个伪类表现出来。


减少冗余代码,所以先把两个伪类的共同样式写出来。伪类的高读要根据文字大小进行改变,所以直接用padding配合em来规定伪类的高度。宽度直接用100%


a:before,
a:after {
content: '';
padding: .9em .4em;
position: absolute;
left: 50%;
width: 100%;
top: 50%;
display: block;
border: 15px solid skyblue;
transform: translateX(-50%) translateY(-50%) rotate(0deg);
animation: 10s infinite alternate ease-in-out tipsy;
}

image.png


边框有了,现在就需要把动态效果给加上,让它先动起来


@keyframes tipsy {
0%{
transform: translateX(-50%) translateY(-50%) rotate(0deg);
}
100% {
transform: translateX(-50%) translateY(-50%) rotate(360deg);
}
}

遮罩效果.gif


如动图所示,现在表框完全就是盖在文字上的,还没有什么特别之处。因为并没有将层级给区分开来,这也很简单,把before这个伪元素的层级给下调就行啦
a:before {
z-index: -1;
}

当然层级改变了,但上一层的边框会把下一层的边框给覆盖住,所以还得让部分边框变得透明


a:before {
border-color: skyblue skyblue rgba(0, 0, 0, 0) rgba(0, 0, 0, 0);
z-index: -1;
}

a:after {
border-color: rgba(0, 0, 0, 0) rgba(0, 0, 0, 0) skyblue skyblue;
}

遮罩效果2.gif


现在就可以看到已经有层次感啦,但是还可以加上一点边框阴影让他更加立体


a:before {
border-color: skyblue skyblue rgba(0, 0, 0, 0) rgba(0, 0, 0, 0);
z-index: -1;
}

a:after {
border-color: rgba(0, 0, 0, 0) rgba(0, 0, 0, 0) skyblue skyblue;
box-shadow: 25px 25px 25px rgba(46, 46, 49, .8);
}

遮罩效果3.gif


那么到这就已经全部完成啦,以上就是本次代码的全部解析,下面我将把所有代码放在在线代码里供大家修改体验

在线代码



往期精彩


关于我写了一个海底掘金挑战游戏juejin.cn/post/714464…


关于我随手写了个掘金相关的游戏juejin.cn/post/714232…


关于我帮领导的孩子写了一个小游戏参赛这种事juejin.cn/post/714115…


关于我抽不到月饼礼盒于是用代码做了一个(纯代码文本) juejin.cn/post/714047…


关于我仿做了个steam很火的《Helltaker》游戏juejin.cn/post/712149…


作者:Gatsby
来源:juejin.cn/post/7144912266855940132
收起阅读 »

前端应该知道的浏览器中的内存知识

web
为了保证我们网页的稳定性,浏览器的内存知识对我们来说是十分必要的,我们不应该只考虑网页打开时的性能,也应该考虑网页长时间挂载下的稳定性。 本次梳理以Chrome为例。 chrome的内存限制 堆内存的限制是由 V8 来设置的。 存在限制 64位系统 物理内存...
继续阅读 »

为了保证我们网页的稳定性,浏览器的内存知识对我们来说是十分必要的,我们不应该只考虑网页打开时的性能,也应该考虑网页长时间挂载下的稳定性。


本次梳理以Chrome为例。


chrome的内存限制


堆内存的限制是由 V8 来设置的。


存在限制


64位系统
物理内存 > 16G => 最大堆内存限制为4G
物理内存 <= 16G => 最大堆内存限制为2G

32位系统
最大堆内存限制为1G


堆内存是计算机系统中,当多个程序同时运行时,为了这些进程能够共享数据、交换信息而把它们的数据存放在一个连续的区域。它是一个连续的内存区域,在物理上并不存在。



何为内存


内存(Memory)是计算机的重要部件,也称内存储器和主存储器,它用于暂时存放CPU中的运算数据,以及与硬盘等外部存储器交换的数据。它是外部存储器与CPU进行沟通的桥梁,计算机中所有程序的运行都在内存中进行,内存性能的强弱影响计算机整体发挥的水平。只要计算机开始运行,操作系统就会把需要运算的数据从内存调到CPU中进行运算,当运算完成,CPU将结果传送出来。


所以内存的运行决定计算机整体运行快慢。


为何限制


Chrome之所以限制了内存的大小,表面上的原因是V8最初是作为浏览器的JavaScript引擎而设计,不太可能遇到大量内存的场景,而深层次的原因:则是由于V8的垃圾回收机制的限制。


由于V8需要保证JavaScript应用逻辑与垃圾回收器所看到的不一样,V8在执行垃圾回收时会阻塞 JavaScript应用逻辑,直到垃圾回收结束再重新执行JavaScript应用逻辑,这种行为被称为“全停顿”(stop-the-world)。


若V8的堆内存为1.5GB,V8做一次小的垃圾回收需要50ms以上,做一次非增量式的垃圾回收甚至要1秒以上。这样浏览器将在1s内失去对用户的响应,造成假死现象。如果有动画效果的话,动画的展现也将显著受到影响。


chrome网页是如何占用内存的


chrome之所以很吃内存,是因为chrome使用了多进程机制,每一个chrome的标签页以及每一个扩展,都是独立的进程。在目前的chrome进程架构里,访问一个网站至少包含四个进程:一个浏览器进程、一个GPU进程、一个渲染进程和一个网络进程。除此之外还有包含多个插件进程组成chrome的进程架构。



1. V8


V8 是google 开发的开源高性能 javascript引擎,V8引擎用C++语言开发,被用在Google的chrome浏览器,android 浏览器js引擎默认也用V8。


​ V8最初是为了提高web浏览器中的JavaScript运行性能设计的。为了提升性能,V8将JavaScript代码翻译为更高效的机器语言,而不是使用解释程序。它通过实现一个JIT(Just-In-Time,即时) 编译器来将JavaScript代码编译为机器语言,就像很多现代JavaScript引擎如SpiderMonkey或Rhino(Mozilla)做的那样。V8和它们主要的区别是它不会生成字节码或其他中间代码。


1.1 V8如何执行JavaScript



V8执行js的主要流程如下:



  • 准备执行JS需要的基础环境

  • 解析源码生成ast和作用域

  • 依据ast和作用域生成字节码

  • 解释器解释执行字节码

  • 监听热点代码

  • 编译器优化热点代码为二进制的机器码

  • 反优化二进制机器代码


1.1.1 准备执行JS需要的基础环境


这些基础环境包括:



  • 堆空间和栈空间

  • 全局执行上下文

  • 全局作用域

  • 内置函数

  • 宿主环境提供的扩展函数和对象

  • 事件循环系统


1. 堆空间


堆空间是一种树形的存储结构,用来存储对象类型的离散的数据,以及一些占用内存比较大的数据。


存在堆空间的:



  • 函数

  • 数组

  • 在浏览器中还有 window 对象

  • document 对象等


2. 栈空间


栈空间主要是用来管理 JavaScript 函数调用的,栈是内存中连续的一块空间,同时栈结构是“先进后出”的策略。


特点:



  • 先进后出

  • 空间连续

  • 查找效率非常高


函数调用过程中,什么会存在栈里:



  • 原生类型

  • 引用到的对象的地址

  • 函数的执行状态

  • this 值等


3. 全局执行上下文


V8 初始化了基础的存储空间之后,接下来就需要初始化全局执行上下文和全局作用域。 当 V8 开始执行一段可执行代码时,会生成一个执行上下文来维护执行当前代码所需要的变量声明、this 指向等。


执行上下文中主要包含:



  • 变量环境

  • 词法环境:包含了使用 let、const 等变量的内容

  • this 关键字


全局执行上下文在 V8 的生存周期内是不会被销毁的,它会一直保存在堆中。


4. 全局作用域


var x = 5
{
let y = 2
const z = 3
}

这段代码在执行时,会有两个对应的作用域,一个是全局作用域,另外一个是括号内部的作用域,但是这些内容都会保存到全局执行上下文中。


5. 内置函数


JavaScript的内置函数是浏览器内核自带的,不用任何函数库引入就可以直接使用的函数。JavaScript内置函数一共可分为五类:



  • 常规函数

  • 数组函数

  • 日期函数

  • 数学函数

  • 字符串函数


6. 宿主环境提供的扩展函数和对象


什么是宿主环境?


宿主环境可以是浏览器中的渲染进程,可以是 Node.js 进程, 也可以是其他的定制开发的环境,而这些宿主则提供了很多 V8 执行 JavaScript 时所需的基础功能部件。


7. 事件循环系统(Event Loop)


V8 还需要有一个主线程,用来执行 JavaScript 和执行垃圾回收等工作。


V8 是寄生在宿主环境中的,它并没有自己的主线程,而是使用宿主所提供的主线程,V8 所执行的代码都是在宿主的主线程上执行的。


在执行完代码之后,为了让线程继续运行,通常的做法是在代码中添加一个循环语句,在循环语句中监听下个事件。


如果主线程正在执行一个任务,这时候又来了一个新任务,那么这种情况下就需要引入一个任务队列,这个任务队列是放在了事件触发线程,让新任务暂存到任务队列中,等当前的任务执行结束之后,再从消息队列中取出正在排队的任务。当执行完一个任务之后,我们的事件循环系统会重复这个过程,继续从消息队列中取出并执行下个任务。


事件循环系统主要用来处理任务的排队和任务的调度。


1.1.2 解析源码生成ast和作用域


V8接收到JavaScript源代码后,解析器(Parser)会对其进行词法分析和语法分析,结构化JavaScript字符串,生成AST(抽象语法树)。


解析代码需要时间,所以 JavaScript 引擎会尽可能避免完全解析源代码文件。另一方面,在一次用户访问中,页面中会有很多代码不会被执行到,比如,通过用户交互行为触发的动作。


正因为如此,所有主流浏览器都实现了惰性解析(Lazy Parsing)。解析器不必为每个函数生成 AST(Abstract Syntax tree,抽象语法树),而是可以决定“预解析”(Pre-parsing)或“完全解析”它所遇到的函数。


预解析会检查源代码的语法并抛出语法错误,但不会解析函数中变量的作用域或生成 AST。完全解析则将分析函数体并生成源代码对应的 AST 数据结构。相比正常解析,预解析的速度快了 2 倍。


生成 AST 主要经过两个阶段:分词和语义分析。AST 旨在通过一种结构化的树形数据结构来描述源代码的具体语法组成,常用于语法检查(静态代码分析)、代码混淆、代码优化等。


V8 的 AST 表示方式


1.1.3 依据ast和作用域生成字节码


V8 引入 JIT(Just In Time,即时编译)技术,通过 Ignition 基线编译器快速生成字节码进行执行。


字节码是机器码的抽象,字节码可以直接被优化编译器 TurboFan 用于生成图(TurboFan 对代码的优化基于图),避免优化编译器在优化代码时需要对 JavaScript 源代码重新进行解析。


1.1.4 优化编译器 TurboFan


解释器执行字节码过程中,如果发现代码被重复执行,监控机器人会把这段代码标记为热点代码。热点代码会丢给优化编译器编译成二进制代码,然后优化。下次再执行时就执行这段优化后的二进制代码。


1.1.5 反优化


JS 语言是动态语言,非常之灵活,对象的结构和属性在运行时是可以发生改变的,设想一个问题,如果热代码在某次执行的时候,突然其中的某个属性被修改了,那么编译成机器码的热代码还能继续执行吗?


答案是肯定不能。这个时候就要使用到优化编译器的反优化了,他会将热代码退回到 AST 这一步,这个时候解释器会重新解释执行被修改的代码,如果代码再次被标记为热代码,那么会重复执行优化编译器的这个步骤。


1.2 内存管理


内存是计算机中重要的部件之一,它是与CPU进行沟通的桥梁。


计算机中所有程序的运行都是在内存中进行的,因此内存的性能对计算机的影响非常大。


高效的程序离不开内存的有效管理,内存管理的优势:



  • 减少内存分配

  • 回收开销

  • 避免内存碎片

  • 定位内存位置

  • 方便内存整理

  • 跟踪内存使用


1.2.1 V8 引擎的内存结构


因为 JavaScript 是单线程,所以 V8 在每个上下文都使用一个进程,如果你使用 Service Worker ,它也会为每个 Service Worker 生成一个新的进程。



Service Worker:一个服务器与浏览器之间的中间人角色,如果网站中注册了service worker那么它可以拦截当前网站所有的请求,进行判断(需要编写相应的判断程序),如果需要向服务器发起请求的就转给服务器,如果可以直接使用缓存的就直接返回缓存不再转给服务器,从而大大提高浏览体验。



一个正在运行的程序是由 V8 进程分配的内存来表示的,这被称为 Resident Set(常驻集)。这些内存会进一步划分成不同的部分。


一个 V8 进程的内存通常由以下几个块构成:



  1. **新生代内存区(new space)

    **大多数的对象都会被分配在这里,这个区域很小但是垃圾回收比较频繁;

  2. 老生代内存区(old space)

    属于老生代,这里只保存原始数据对象,这些对象没有指向其他对象的指针;

  3. **大对象区(large object space)

    **这里存放体积超越其他区大小的对象,每个对象有自己的内存,垃圾回收其不会移动大对象区;

  4. 代码区(code space)

    代码对象,会被分配在这里。唯一拥有执行权限的内存;

  5. map 区(map space)

    存放 Cell 和 Map,每个区域都是存放相同大小的元素,结构简单。


如下图:



Heap Memory(堆内存)


这是 V8 引擎存储对象(Object)和动态数据(Dynamic Data)的地方。这也是程序对于内存区域中最大的一块地方,同时**垃圾回收( GC )**也发生在这里。并不是整个 Heap (堆)内存都进行垃圾回收,只有新空间(New Space)和旧空间(Old Space)由垃圾回收管理。


整个堆内存被划分为以下几个部分:




  • 新空间:是新对象存活的地方,这些对象的生命周期都很短。这个空间很小,由两个 Semi-Space 组成,类似与 JVM 中的 S0 和 S1。

    我们将会在后面的内容看到它。新空间的大小是由两个 V8 中的标志位来控制: min_semi_space_size(Initial) 和 max_semi_space_size(Max) 。




  • 旧空间:在新空间中存活了两个 minor GC 周期的对象,会被迁移到这里。

    这个空间由 Major GC(Mark-Sweep & Mark-Compact) 管理。我们也会在后面内容中看到它。旧空间的大小也是由两个 V8 中的标志位来控制:nitial_old_space_size(Initial) 和 max_old_space_size(Max) 。

    旧空间被分成两个部分:




  • 旧指针空间:这些存活下来的的对象都包含了指向其他对象的指针。




  • 旧数据空间:这些对象只包含数据,没有指向其他对象的指针。在新空间中存活两个 minor GC 周期后,String,已经装箱的数字,未装箱的双精度数组会被迁移到这里。




  • 大型对象空间(Large object space):大于其他空间大小限制的对象存放在这里。每个对象都有自己的内存区域,这里的对象不会被垃圾回收器移动。




  • 代码空间(Code-space):这是即时编译器(JIT)存储已经编译的代码块的地方。这是唯一可执行内存的空间(尽管代码可能被分配到大型对象空间(Large object space),那也是可以执行的)。




  • 单元空间(Cell Space),属性单元空间(Property Cell Space)和映射空间(Map Space):这些空间分别存放 Cell,PropertyCell 和 Map。这些空间包含的对象大小相同,并且对对象类型有些限制,可以简化回收工作。




每个空间(除了大型对象空间)都由一组 Page 组成。一个 page 是由操作系统分配的一个连续内存块,大小为 1MB。


Stack(栈)


每个 V8 进程都有一个栈(Stack),这里保存静态数据的地方,比如:方法/函数框架,原型对象的值(Primitive value)和指针。栈(Stack)内存的大小由 V8 的标志位来设置:stack_size。



  1. 全局作用域被保存在 Stack 中的 Global frame 中。

  2. 每个函数调用都做为 frame 块添加到 Stack 中。

  3. 所有的局部变量,包括参数和返回值都保持在 Stack 的函数 frame 上。

  4. 所有的原型类型的数据,比如 int 和 String,都直接保持在 Stack 上。(是的,JavaScript 中 String 是原型数据)

  5. 所有的对象类型,比如 Employee 和 Function 都保存在 Heap 中,并且通过 Stack 上的指针来引用。(函数在 JavaScript 中也是对象。)

  6. 从当前函数中调用的函数被压入了 Stack 的顶部。

  7. 当函数返回是,它的 frame 块将会从 Stack 中移除。

  8. 一旦主进程完成,Heap 上的对象就不再有任何来自 Stack 的指针,这些对象将成为孤儿。

  9. 除非显式的复制,否则其他对象中的所有对象引用都是通过指针完成的。


正如你所看到的,Stack 是自动管理的,而且是由操作系统而不是 V8 本身完成的。因此我们不必担心 Stack 的问题。另一方面,Heap 不是由操作系统自动管理的,由于 Heap 是程序内存块中最大的内存空间,并且保存动态数据,所以它的空间使用会指数级增长,从而导致我们的程序内存耗尽。


1.2.2 V8 内存的使用


我们通过一段代码来看JS程序被执行时是如何使用内存的。


class Employee {
constructor(name, salary, sales) {
this.name = name;
this.salary = salary;
this.sales = sales;
}
}

const BONUS_PERCENTAGE = 10;

function getBonusPercentage(salary) {
const percentage = (salary * BONUS_PERCENTAGE) / 100;
return percentage;
}

function findEmployeeBonus(salary, noOfSales) {
const bonusPercentage = getBonusPercentage(salary);
const bonus = bonusPercentage * noOfSales;
return bonus;
}

let john = new Employee("John", 5000, 5);
john.bonus = findEmployeeBonus(john.salary, john.sales);
console.log(john.bonus);

动图封面


就像你看到的那样:



  1. 全局作用域被保存在 Stack 中的 Global frame 中。

  2. 每个函数调用都做为 frame 块添加到 Stack 中。

  3. 所有的局部变量,包括参数和返回值都保持在 Stack 的函数 frame 上。

  4. 所有的原型类型的数据,比如 int 和 String,都直接保持在 Stack 上。

  5. 所有的对象类型,比如 Employee 和 Function 都保存在 Heap 中,并且通过 Stack 上的指针来引用。

  6. 从当前函数中调用的函数被压入了 Stack 的顶部。

  7. 当函数返回是,它的 frame 块将会从 Stack 中移除。

  8. 一旦主进程完成,Heap 上的对象就不再有任何来自 Stack 的指针,这些对象将成为孤儿。

  9. 除非显式的复制,否则其他对象中的所有对象引用都是通过指针完成的


Stack 是自动管理的,而且是由操作系统而不是 V8 本身完成的。因此我们不必担心 Stack 的问题。另一方面,Heap 不是由操作系统自动管理的,由于 Heap 是程序内存块中最大的内存空间,并且保存动态数据,所以它的空间使用会指数级增长,从而导致我们的程序内存耗尽。


而且 Heap 中的内存也会随着时间的推移,变得支离破碎,从而拖慢程序。这时候就需要垃圾回收发挥作用了。


1.3 垃圾回收 Garbage collection



垃圾回收是指回收那些在应用程序中不再引用的对象,当一个对象无法从根节点访问这个对象就会做为垃圾回收的候选对象。这里的根对象可以为全局对象、局部变量,无法从根节点访问指的也就是不会在被任何其它活动对象所引用。



我们知道了 V8 是如何分配内存的,现在让我们来看看它是如何自动管理 Heap 内存的,这对程序的性能非常重要。


当程序试图在 Heap 中分配超过可用的内存时,就会遇到内存不足的错误,整个页面都会崩溃。


一个不正确的 Heap 内存管理也可能导致内存泄漏。


V8 引擎通过垃圾回收来管理 Heap 内存。简单来说,就是释放孤立(orphan)对象使用的内存。比如,一个对象并没有直接或者间接被 Stack 中的指针所引用,就会释放相应内存为新对象腾出空间。


V8 的垃圾回收器负责回收未使用的内存,以便 V8 进程重新使用。


1.3.1 如何判断非活跃对象


判断对象是否是活跃的一般有两种方法,引用计数法和可访问性分析法。


1. 引用计数法


V8中并没有使用这种方法,因为每当有引用对象的地方,就加1,去掉引用的地方就减1,这种方式无法解决A与B循环引用的情况,引用计数都无法为0,导致无法完成gc。


2. 可访问性分析法


V8中采用了这种方法,将一个称为GC Roots的对象(在浏览器环境中,GC Roots可以包括:全局的window对象,所有原生dom节点集合等等)作为所有初始存活的对象集合,从这个对象出发,进行遍历,遍历到的就认为是可访问的,为活动对象,需要保留;如果没有遍历到的对象,就是不可访问的,这些就是非活动对象,可能就会被垃圾回收


在浏览器环境中,GC Root 有很多,通常包括了以下几种 (但是不止于这几种):



  • 全局的 window 对象(位于每个 iframe 中)。

  • 文档 DOM 树,由可以通过遍历文档到达的所有原生 DOM 节点组成。

  • 存放栈上变量。



1.3.2 代际假说


代际假说(The Generational Hypothesis)是垃圾回收领域中的一个重要术语,它有两个特点




  1. 大部分对象在内存中存活时间很短,比如函数内部声明变量,块级作用域中的变量等,这些代码块执行完分配的内存就会被清掉。




  2. 不死的对象会活得更久,比如全局的window、Dom、全局api等对象。




基于代际假说的理论,在V8引擎中,垃圾回收算法被分为两种,一个是Major GC,主要使用了Mark-Sweep & Mark-Compact算法,针对的是堆内存中的老生代进行垃圾回收;


另外一个是Minor GC,主要使用了Scavenger算法,针对于堆内存中的新生代进行垃圾回收。



注:

所谓老生代指的就是那些存活时间很久没有被清理的对象,而新生代指的是存活时间很短的对象。



1.3.3 Scavenger算法


是在新生代内存中使用的算法,速度更快,空间占用更多的算法。New space区域分为了两个半区,分别为from-space和to-space。不断经过下图中的过程,在两个空间的角色互换中,完成垃圾回收的过程。每次都会有对象复制的操作,为了控制这里产生的时间成本和执行效率,往往新生代的空间并不大。同时为了避免长时间之后,某些对象会一直积压在新生代区域,V8制定了晋升机制,满足任一条件就会被分配到老生代的内存区中。




  1. 经历一次Scavenger算法后,仍未被标记清除的对象。




  2. 进行复制的对象大于to space空间大小的25%。





1.3.4 Mark-Sweep & Mark-Compact算法


是老生代内存中的垃圾回收算法,标记-清除 & 标记-整理,老生代里面的对象一般占用空间大,而且存活时间长,如果也用Scavenger算法,复制会花费大量时间,而且还需要浪费一半的空间。



  • 标记-清除过程:也就是可访问性分析法,从GC Root开始遍历,标记完成后,就直接进行垃圾数据的清理工作。




  • 标记-整理过程:清除算法后会产生大量不连续的内存碎片,碎片过多会导致后面大对象无法分配到足够的空间,所以需要进行整理,第一步的标记是一样的,但标记完成活跃对象后,并不是进行清理,而是将所有存活的对象向一端移动,然后清理掉这端之外的内存。



1.3.5 优化策略


由于 JavaScript 是运行在主线程之上的,因此,一旦执行垃圾回收算法,都需要将正在执行的 JavaScript 脚本暂停下来,待垃圾回收完毕后再恢复脚本执行。这种行为叫做全停顿(Stop-The-World)。


STW(全停顿)会造成系统周期性的卡顿,对实时性高的和与时间相关的任务执行成功率会有非常大的影响,例如:js逻辑需要执行动画,刚好碰到gc的过程,会导致整个动画卡顿,用户体验极差。


为了降低这种STW导致的卡顿和性能不佳,V8引擎中目前的垃圾回收器名为Orinoco,经过多年的不断精细化打磨和优化,已经具备了多种优化手段,极大地提升了GC整个过程的性能及体验。



Orinoco 是 V8 GC 项目的代号,它利用并行,增量和并发的技术进行垃圾回收,来释放主线程。



1.3.6 并行回收


简单来讲,就是主线程执行一次完整的垃圾回收时间比较长,开启多个辅助线程(web-worker)来并行处理,整体的耗时会变少,所有线程执行GC的时间点是一致的,js代码也不会有影响,不同线程只需要一点同步的时间,在新生代里面执行的就是并行策略。



1.3.7 增量回收


并行策略说到底还是STW(全停顿)的机制,如果老生代里面存放一些大对象,处理这些依然很耗时,Orinoco又增加了增量回收的策略。将标记工作分解成小块,插在主线程不同的任务之间执行,类似于React fiber的分片机制,等待空闲时间分配。这里需要满足两个实现条件:


1. 随时可以暂停和启动,暂停要保存当前的结果,等下一次空闲时机来才能启动。


2. 暂停时间内,如果已经标记好的数据被js代码修改了,回收器要能正确地处理。



下面要讲到的就是Orinoco引入了三色标记法来解决随时启动或者暂停且不丢之前标记结果的问题。


1.3.8 三色标记法


三色标记法的规则如下:


1. 最开始所有对象都是白色状态


2. 从GC Root遍历所有可到达的对象,标记为灰色,放入待处理队列


3. 从待处理队列中取出灰色对象,将其引用的对象标记为灰色放入待处理队列,自身标记为黑色


4. 重复3中动作,直到灰色对象队列为空,此时白色对象就是垃圾,进行回收。



垃圾回收器可以依据当前内存中有没有灰色节点,来判断整个标记是否完成,如果没有灰色节点了,就可以进行清理工作了。如果还有灰色标记,当下次恢复垃圾回收器时,便从灰色的节点开始继续执行。


下面将要解决由于js代码导致对象引用发生变化的情况,Orinoco借鉴了写屏障的处理办法。


1.3.9 写屏障(write-barrier)


一旦对象发生变化时,如何精确地更新标记的结果,我们可以分析下一般js执行过程中带来的对象的变化有哪些,其实主要有2种:


1. 标记过的黑色或者灰色的对象不再被其他对象所引用。


2. 引入新的对象,新的对象可能是白色的,面临随时被清除的危险,导致代码异常。


第一种问题不大,在下次执行gc的过程中会被再次标记为白色,最后会被清空掉;


第二种就使用到了写屏障策略,一旦有黑色对象引用到了白色对象,系统会强制将白色对象标记成为灰色对象,从而保证了下次gc执行时状态的正确,这种模式也称为强三色原则。


1.3.10 并发回收


虽说三色标记法和写屏障保证了增量回收的机制可以实现,但依然改变不了需要占用主线程的情况,一旦主线程繁忙,垃圾回收依然会影响性能,所以增加了并发回收的机制。


V8里面的并发机制相对复杂,简化来看,当主线程运行代码时,辅助线程并发进行标记,当标记完成后,主线程执行清理的过程时,辅助线程也并行执行。



1.4 D8


D8 是一个十分有用的调试工具,你能够把它看成是 debug for V8 的缩写。我们可以应用 d8 来查看 V8 在执行 JavaScript 过程中的各种两头数据,例如作用域、AST、字节码、优化的二进制代码、垃圾回收的状态,还能够应用 d8 提供的公有 API 查看一些外部信息。


该工具的下载教程和使用方式:blog.csdn.net/heyYouU/art…


2. memory cache


在我们使用强缓存+协商缓存的时候,我们会将一部分资源放在内存中缓存起来。


内存中的缓存,主要包含的是当前中页面中已经抓取到的资源,例如页面上已经下载的样式、脚本、图片等。读取内存中的数据肯定比磁盘快,内存缓存虽然读取高效,可是缓存持续性很短,会随着进程的释放而释放。一旦我们关闭 Tab 页面,内存中的缓存也就被释放了。


我们上面谈到了,V8对堆内存的大小做了限制,如果超过了限制会导致网络崩溃的现象,那么我们的memory cache的占用内存受不受V8的约束呢。


当然是受约束的,如果要缓存大量的资源,还得需要用到磁盘缓存。


参考: blog.csdn.net/qiwoo_weekl…


作者:黑色的枫
来源:juejin.cn/post/7221793823704514620
收起阅读 »

Android UI-薄荷健康尺子

效果 源码:HenCoder-CustomView: HenCoder-三篇自定义View仿写 (gitee.com) 原的 仿的 功能点分析 根据尺子的量程 和 分度值 绘制尺子的静态效果 内容滑动,计算滑动边界 惯性滑动,智能定位 计算当前刻度值 基...
继续阅读 »

效果


源码:HenCoder-CustomView: HenCoder-三篇自定义View仿写 (gitee.com)


原的


QQ图片20220508153648.gif
仿的


QQ图片20220508153644.gif


功能点分析



  1. 根据尺子的量程 和 分度值 绘制尺子的静态效果

  2. 内容滑动,计算滑动边界

  3. 惯性滑动,智能定位

  4. 计算当前刻度值

  5. 基准线居中


实现分析


OIP-C.jfif


绘制尺子刻度



  1. 分度值:即最小刻度值,就是在测量仪器所能读出的最小值,指测量工具上相邻的两个刻度之间的最小格的数值

  2. 绘制尺子刻度,肯定要用到循环,最简单的办法,知道尺子的刻度总数,即可把尺子绘制出来。

  3. 尺子的刻度数根据 量程和分度值确定。上图尺子同样的量程 有两个分度值,尺子的刻度数完全不同

  4. 刻度数的计算:量程/分度值。 比如:

    1. 度量范围20~100,量程是80,分度值是1。 一共80个刻度

    2. 但凡事总有例外,度量范围0~100,量程100,分度值1,并不是100个刻度,而是一百零一个刻度。

    3. 0也算一个刻度,0 ~ 100 是101个数。1~100才是100个数。 程序员应该很好理解吧,毕竟从入行开始,数的起始就不是1了🐶



  5. 尺子的刻度一般都是10进制,则取余数 i % 10 == 0 表明是大刻度,其余的都是小刻度。

  6. 定义变量,刻度的长款,间隔。已经知道总刻度数,通过循环遍历即可绘制出尺子的静态样式

  7. 绘制文字比较简单 每次取余数 i % 10 == 0 表示大刻度,需要绘制文字,坐标微调即可。


尺子的滑动



  1. 滑动分两种,内容滑动和拖拽滑动。

    1. 内容滑动场景是:ScrollView ,ListView,RecyclerView,在有限的位置固定的空间内可以展示无限内容。

    2. 拖拽滑动指View内容不变,位置变化。应用场景是微信语音,视频电话的小窗口。



  2. 尺子的滑动是内容滑动

    1. 重写onTouchEvent() 。记录每次手指滑动产生的坐标,上一次坐标与当前坐标相减,计算滑动距离。

    2. 在move事件中,调用scrollBy() 传入滑动距离,内容滑动完成



  3. 惯性滑动组件介绍

    1. 仅仅使用scrollBy() 滑动无惯性,效果比较生硬,与系统滑动组件的体验相差比较多

    2. 结合 VelocityTrackerScroller 使滑动产生惯性

    3. VelocityTracker 收集手指滑动路径的坐标用作路程,传入时间,计算出速度。

    4. Scroller 滑动辅助类,并不实现View滚动。它的作用好像属性动画,计算一段时间内数字变化, 比如:一秒内从0增长到100。 开发者监听数字变化从而实现动画



  4. 惯性滑动实现

    1. 重写onTouchEvent() 调用VelocityTracker.addMovement(event) 收集手势滑动信息

    2. 在up事件,VelocityTracker.computeCurrentVelocity(1000) 计算一秒内滑动距离产生的速度

    3. 速度计算结果 ,手指左滑 速度正数 ,手指右滑 速度负数。

    4. 速度正数使坐标增加 ,负数使坐标减少。这里会引发一个问题

    5. 调用Scroller.fling()

      1. fling 参数解析

      2. startX:开始位置

      3. minX-maxX:区间 ,根据速度计算x值 的范围在 minX maxX之间

      4. velocityX 速度的影响

        1. 比如:手指右滑

        2. 期望效果 x轴正方向移动 值增加

        3. 实际效果 速度负数 Scroller.fling动画结果 x轴负方向移动 值减少

        4. 期望效果与实际效果正好相反 所以速度取相反数 效果正好



      5. fling总结

        1. startX开始位置 如 :100

        2. 受速度影响 计算结果 会从100开始增加或减少。

        3. 但不是无限增加或减少,计算结果的边界在 minX最小值,maxX最大值 之间





    6. 调用 invalidate() 触发view重绘,

    7. 重写computeScroll() ,获取Scroller动画结果 ,调用scrollTo() 实现内容滚动




滑动边界


Untitled.png


滑动边界与view的大小是两个概念


view的内容绘制在canvas上,canvas是一块无限大的画布,View有坐标系,左上角是原点(0,0),惨van无限大,坐标系也是无限大的。


View的宽高则是在无限大的canvas从原点开始圈出一块位置展示内容。


如下图,用户的可视范围只是100*100,但无限大的canvas仍然存在。假设在(200,200)的位置画了一个点,虽然用户看不见,但是它仍然存在。


上一节使用scrollBy()scrollTo() 实现内容的滑动


其内部原理是修改View的两个属性mScrollX,mScrollYmScrollX,mScrollY 表示内容在X轴Y轴的滚动距离,也可以说是确定View的展示的原点。


举例说明:



  1. 自定义View,宽高都是100

  2. 两点坐标确定一个矩形,默认原点(0,0) 由于宽高100,另一点坐标(100,100)。View展示canvas (0,0),(100,100)两点坐标圈出的部分

  3. 沿X轴移动距离100后,原点坐标(100,0),另一点坐标(200,100)。View展示canvas (100,0),(200,100)两点坐标圈出的部分


所以想要实现View内容滑动的边界,就要限制X轴坐标的取值范围,也就是mScrollX 属性的范围,从0到X。


那么如何计算滑动范围呢?


滑动范围 = 大刻度数大刻度宽 + 小刻度数小刻度宽 + 间隔数*间隔宽


基准线居中


其实这个东西吧 属于会了不难,难了不会,经验问题,不知道的可能想半天也没想出来。


先说结论:基准线x轴坐标 = view宽度/2 + mScrollX 就能达到滚动时居中效果。


分析



  1. 假设View的宽高都为100

  2. 画一条长度为10的X轴居中的线段,坐标点(50,10)

  3. 这条线段只是看起来居中,在view的可视范围(0,0),(100,100)内居中,

  4. 它并不是画在View上,而是画在canvas,view只是圈出一个范围

  5. 当内容水平滑动,x值不断改变,线段的坐标也要随着滑动不断变化,才能维持居中的效果

  6. 代表水平滑动距离的变量是mScrollX 线段坐标点为 (mScrollX +50,10)


智能定位


业务描述


当滑动到两个刻度之间,四舍五入自动定位到最近的那个刻度,比如:滑动到11.6,分度值是1,左右两个刻度分别是11,12。四舍五入滑动到12。


应用场景



  1. 惯性滑动后需要智能定位

  2. up手势之后,如果速度过小,无法出发惯性滑动,则需要智能定位


实现过程


这块挺复杂的,没办法详细说 很容易乱,我的思路不一定是最好的,当作参考



  1. 核心思路是利用等比例换算。

  2. 预先知道总滑动距离,知道当前滑动值,能够计算出滑动比例。

  3. 滑动比例 == 数值比例,通过比例计算出当前的测量值

  4. 根据分度值单位四舍五入,求出定位值,计算出定位值的X轴坐标

  5. mScrollX -定位值的X轴坐标 = 滑动距离。求出滑动距离后利用 Scroller.startScroll() 进行滑动


只绘制可视区域内容


之前几点完成之后就算是可以正常使用的组件了,原本是不打算做可视区域绘制的(懒)


但是在调试的时候发现绘制内容过多会很卡,不流畅z


比如:度量范围1~100,分度值是1,需要绘制100个刻度。分度值0.1,需要绘制1k个刻度,分度值0.01,需要绘制1w个刻度,卡顿非常明显了,简直不能用。


计算可视区域非常简单。view的可见区域 = x轴坐标范围 = 滚动距离 + view的宽度


x坐标在范围内视为可见,不在范围内视为不可见


private fun isVisibleArea(x: Int): Boolean {
//view的可见区域 = x轴坐标范围 = 滚动距离 + view的宽度
val offset = 20 //偏移量
val start =scrollX-offset
val end =scrollX+measuredWidth+ offset
return x in start..end
}

override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
//简化代码
if (isVisibleArea(x)){
drawLine(i, canvas)
drawText(i, canvas)
}
}

作者:图个喜庆
链接:https://juejin.cn/post/7095286970976305182
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

2分钟部署自己的ChatGPT,告别网络困扰

最近的chatGPT是热火朝天,基本人手一个。工具用的好,工作5分钟,划水一整天。 不过最近ChatGPT的访问越来越限制了,访问官网都有网络的问题,今天小卷给大家介绍一个方案,私人独享属于自己的chatGPT,不再担心想用的时候访问不了的情况。 项目是Git...
继续阅读 »

最近的chatGPT是热火朝天,基本人手一个。工具用的好,工作5分钟,划水一整天。


不过最近ChatGPT的访问越来越限制了,访问官网都有网络的问题,今天小卷给大家介绍一个方案,私人独享属于自己的chatGPT,不再担心想用的时候访问不了的情况。


项目是Github上开源chatGPT项目,基于 OpenAI GPT-3.5 Turbo API 的demo。地址:github.com/ddiu8081/ch…


效果如下:



使用步骤


1.服务器上安装nodejs环境


准备一个海外服务器(漂亮国节点),这里以Ubuntu操作系统为例



  1. Node:需要使用 Node v18 或更高版本


# 更新
apt-get install update
# 安装node
apt-get install node
# 安装npm
apt-get install npm
# 安装n模块
apt-get install -g n
# 安装最新版的node
sudo n latest

最后通过node -v查看版本号,在v18版本以上即可



2.安装pnpm


推荐使用pnpm管理依赖,安装命令:


npm i -g pnpm

3.下载代码


从github上克隆代码下载


git clone https://github.com/ddiu8081/chatgpt-demo.git

4.安装依赖


代码下载完后,进入到chat-demo目录下,安装需要的依赖


pnpm install

5.添加API Key


需要用到你自己的ChatGPT账号的key,获取地址:platform.openai.com/account/api…


需要将.env.example文件重命名为.env文件,并将你的key写到.env文件里


# 重命名文件
mvn .env.example .env
# 写入key
vim .env

将下面文本中的key替换为你的key,替换后按esc键,然后再输入:wq 保存退出


 OPENAI_API_KEY=sk-xxx...

2.png


6.运行应用


运行项目,并且让你的应用能在公网访问到,执行命令如下,出现IP和端口号就说明运行成功了


pnpm run dev --host 0.0.0.0

3.png


需要注意的是,上面显示的IP通常是云服务器内网IP,不可直接访问,需要使用服务器的公网IP:3000端口号访问


以阿里云为例,每个服务器都有公网IP和内网IP,记得改为公网IP


4.png


作者:卷福同学
链接:https://juejin.cn/post/7219862257644388409
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Claude,一个可以无需魔法访问的ChatGPT

它是由11名OpenAI前员工开发的一款自然语言处理工具。它可以通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,真正像人类一样来聊天交流,甚至能完成撰写邮件、视频脚本、文案、翻译、代码等任务,是一款非常强大的AI工具。 了解Claude 相...
继续阅读 »

它是由11名OpenAI前员工开发的一款自然语言处理工具。它可以通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,真正像人类一样来聊天交流,甚至能完成撰写邮件、视频脚本、文案、翻译、代码等任务,是一款非常强大的AI工具。



了解Claude


相比较ChatGPT和NewBing,Claude注册的方式很简单。无需魔法。具体操作如下: image.png



  • 与ChatGPT、NewBing等其他AI系统有以下主要区别:



  1. 不同的训练数据和模型:我是使用Constitutional AI技术和专门的自然语言处理模型训练而成的,与其他系统使用的训练方法和模型不同。

  2. 不同的设计目的:我是为帮助人类而设计的AI助手,目的是为用户提供友好、真诚和有价值的交互体验。而其他系统更注重生成文字上类似人的语言输出。

  3. 不同的交互方式:我会对用户输入的每一条信息进行理解和回应。而其他系统更像是简单地对话,每个回应之间没有直接的逻辑联系。

  4. 安全机制:我在回应生成上做了更多考虑以避免过于人性化、非理性和不道德的言论。并且我没有连接到任何可能产生偏见或有害信息的公开数据。

  5. 持续监督:我会持续接受人工监督和评估来不断提高自己的表现,确保为用户提供最适合的体验。而其他系统更注重自动化生成文字的流畅度。


Claude官网



http://www.anthropic.com/



注册


将Claude添加到slack



  • 点击Add to slack



http://www.anthropic.com/claude-in-s…



image.png


image.png


创建工作区


第一次登录没有的话可以直接创建一个工作区 image.png


image.png 邮箱可以自己选择 ,我是用的Gmail,收到验证码后直接填写进去



如果收不到验证码可以看下Gmail邮箱的推广里面。国内的邮箱可以看看垃圾邮件



然后根据提示填写公司或团队的名称即可:


image.png 然后点击每一步点击下一步跳过此步骤 完成注册。


添加关联


回到第一步 https://www.anthropic.com/claude-in-slack重新点击Add to Slack image.png image.png


使用Claude


在应用找到Claude并点击,然后在聊天框中输入任意内容会出现同意条框的提示,点击Agree即可。然后即可使用了。 image.png


作者:码上遇见你
链接:https://juejin.cn/post/7222440107214618661
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

免费的chatGPT—4来了,告别付费从我做起,主打的就是一个白嫖

免费的chatGPT—4来了,告别付费从我做起,主打的就是一个白嫖! chatGPT出来的也有一段时间了,相信大家都是很想体验一下chatGPT的生产力的,但是openAI是不对国内开放的,因此就出现了一堆的收费镜像,问个几次就要收费,但是博主作为一个资深白嫖...
继续阅读 »

免费的chatGPT—4来了,告别付费从我做起,主打的就是一个白嫖!


chatGPT出来的也有一段时间了,相信大家都是很想体验一下chatGPT的生产力的,但是openAI是不对国内开放的,因此就出现了一堆的收费镜像,问个几次就要收费,但是博主作为一个资深白嫖党怎能容忍支持我的小伙伴去付费的,接下来就坐上鹿邑的小火车,带你体验免费的chatGPT-4。


1.首先我们需要准备一个邮箱(不要是QQ邮箱就行):


1.1: 进入这个链接:slack.com/intl/zh-sg/


2.png


1.2:点击使用电子邮件注册(这里我们不要用QQ邮箱去注册可能收不到验证码)


3.png


1.3:输入我们收到的验证码:


4.png


1.4:然后我们就会来到这个界面:


5.png


1.5:点击创建我们的工作区输入我们工作区的名字:


6.png


1.6:这里直接下一步:


7.png


1.7:这里直接点击跳过即可:


8.png


1.9:来到我们我工作区名字页面:


9.png


1.10:到了这里我们就注册好了:


10.png


2.植入我们的人工智能到我们的工作区中:


2.1:复制这个链接:http://www.anthropic.com/claude-in-s…


11.png


2.2:来到这个页面点击一下Add to Slack(点击允许)


12.png


2.3:再回到我们刚刚的slack页面此时我们会发现我们的页面会多出一个Claude :


13.png


2.4:点击我们的Claude Ai 随便给它发个消息就行(然后点击Agree同意即可):


14.png


到此我们就可以愉快的访问我们的人工智能了,比如我们来问一个关于爱情的相关的:


(可以看到它的回答还是比较官方的)
15.png


作者:鹿邑网爬
链接:https://juejin.cn/post/7221934775842914363
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

【直播开发】WebRTC 的初认识

前言 WebRTC(Web Real-Time Communication)是一种用于实现实时通信的开放标准和技术框架。它允许浏览器和移动应用程序实现点对点(P2P)音频,视频和数据通信,无需任何插件或附加软件。WebRTC 的发展和普及,得益于 HTML5 ...
继续阅读 »

前言


WebRTC(Web Real-Time Communication)是一种用于实现实时通信的开放标准和技术框架。它允许浏览器和移动应用程序实现点对点(P2P)音频,视频和数据通信,无需任何插件或附加软件。WebRTC 的发展和普及,得益于 HTML5 的广泛应用和 WebRTC 的开放性和跨平台性。在当前快速发展的互联网行业中,WebRTC 成为了实现实时通信的重要技术之一。


WebRTC 的历史和背景


WebRTC 最早由 Google 在 2011 年提出,并在 2013 年正式成为 W3C 和 IETF 的标准。在这之前,实时通信一般需要通过 Flash 插件、ActiveX 控件或者 Java Applet 等附加软件来实现,这使得实时通信的应用受到了很大的限制,同时也面临着兼容性和安全性等问题。WebRTC 的出现解决了这些问题,为实现实时通信提供了一种标准化的解决方案。


WebRTC 的主要功能和优点


WebRTC 提供了丰富的实时通信功能,包括音频,视频,数据传输和共享屏幕等功能。它具有以下优点:



  • 实时性好:WebRTC 可以实现低延迟的音视频传输,使得实时通信更加流畅和自然。

  • 交互性强:WebRTC 提供了实时互动的功能,使得用户之间的交流更加直接和有效。

  • 兼容性好:WebRTC 支持多种浏览器和平台,包括 Chrome、Firefox、Safari、Edge 等,且无需安装任何附加软件。

  • 开发成本低:WebRTC 提供了易于使用的 API 和开发工具,使得开发者可以快速开发实时通信应用,且无需额外的开发成本。


WebRTC 的应用场景和用途


WebRTC 的应用场景非常广泛,涵盖了在线教育,远程会议,互联网电话,即时通讯,游戏,虚拟现实,医疗保健等领域。例如,WebRTC 可以用于实现在线教育平台的实时互动教学,或者为远程办公人员提供高效的视频会议服务。WebRTC 的出现,极大地促进了实时通信技术的应用和普及,提高了人们的生产力和交流效率。


WebRTC 的技术组成部分


WebRTC 的技术组成部分主要包括媒体引擎,信令协议,NAT 穿透技术,安全和加密机制等。




  • 媒体引擎:WebRTC 的媒体引擎是其核心技术之一,用于处理音视频数据的捕获,编解码和传输等任务。它可以实现音视频数据的实时捕获和处理,同时提供多种编解码器,使得数据传输更加高效和稳定。




  • 信令协议:WebRTC 使用信令协议进行会话管理和数据交换。它的主要任务是协商双方之间的会话参数和建立点对点连接。WebRTC 支持多种信令协议,包括 SIP、XMPP、WebSocket 等。




  • NAT 穿透技术:由于大多数网络设备采用了 NAT 技术,WebRTC需要采用特殊的NAT穿透技术来解决设备之间的通信问题。WebRTC 采用 STUN、TURN 和 ICE 等技术,通过中继服务器和代理服务器等手段,实现设备之间的网络连接。




  • 安全和加密机制:WebRTC 通过加密机制来保证通信的安全性和隐私性。它采用 DTLS 协议实现端到端加密,同时支持 SRTP 协议实现数据的传输加密。另外,WebRTC 还支持数字证书和身份认证等安全机制,以确保通信的安全性和可靠性。




结论


综上所述,WebRTC 是一种重要的实时通信技术,它的出现极大地促进了在线交流和协作的发展。后续我们将从体系结构、实现和开发、以及应用来领略 WebRTC 技术的美。


作者:Quincy_Ye
链接:https://juejin.cn/post/7222186286563885111
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

一个通用的圆角View

这篇文章的目的就是介绍一个通用的圆角View,可以根据圆角的复杂度来灵活切换实现方式。 本自定义View可实现的效果如下: 之前的文章中介绍了实现圆角的各种方式,也比较了各种方案的优势和局限。 圆角实现方式汇总 实际使用中发现在一些简单场景中针对只需要实现上...
继续阅读 »

这篇文章的目的就是介绍一个通用的圆角View,可以根据圆角的复杂度来灵活切换实现方式。


本自定义View可实现的效果如下:



之前的文章中介绍了实现圆角的各种方式,也比较了各种方案的优势和局限。


圆角实现方式汇总


实际使用中发现在一些简单场景中针对只需要实现上圆角或者下圆角的场景,或者所有圆角一致的需求中,我们使用性能更高的outlineProvider实现是最佳选择,但是在复杂需求中,比如上下左右圆角弧度不一致,这种时候我们要实现的话就需要切换实现方案。


二、源码


1.自定义属性


<declare-styleable name="RoundCornerLayout">
<attr name="topCornerRadius" format="dimension|reference" />
<attr name="topCornerRadiusLeft" format="dimension|reference" />
<attr name="topCornerRadiusRight" format="dimension|reference" />
<attr name="bottomCornerRadius" format="dimension|reference" />
<attr name="bottomCornerRadiusLeft" format="dimension|reference" />
<attr name="bottomCornerRadiusRight" format="dimension|reference" />
<attr name="cornerMode" format="string" >
<enum name="outline" value ="0"/>
<enum name="xfermode" value ="1"/>
<enum name="clip_path" value ="2"/>
</attr>
</declare-styleable>

cornerMode用于选择实现方式,可选实现方式有


outline:


支持同时设置四个圆角以及单独设置上圆角或者下圆角,但所有圆角弧度必须相同,不支持单独配置


性能:绘制性能最优,暂未发现兼容和锯齿问题


xfermode:


支持四个圆角单独设置和同时设置


性能:性能稍差,同时抗锯齿效果比clippath会好一些


clippath:


支持四个圆角单独设置和同时设置,实现最灵活。


性能:性能稍差,同时低版本机型锯齿明显,同时和硬件加速有兼容问题,部分机型存在渲染闪烁了切割黑屏


outline的实现方式需要配置 topCornerRadius或者bottomCornerRadius即可


xfermode和clippath的实现方式则需要根据上下左右四个圆角分别配置


2.自定义圆角View


class RoundCornerLayout @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {
companion object {
private const val TAG = "RoundCornerLayout"
private const val CORNER_MODE_OUTLINE = 0
private const val CORNER_MODE_XFERMODE = 1
private const val CORNER_MODE_CLIPPATH = 2
}

private var cornerMode = 0
private var topCornerRadius = 0
private var topCornerRadiusLeft = 0
private var topCornerRadiusRight = 0
private var bottomCornerRadius = 0
private var bottomCornerRadiusLeft = 0
private var bottomCornerRadiusRight = 0

private var mRoundRectPath = Path()
private var mPaint = Paint()
private val mRect = RectF()
private var roundedCorners = FloatArray(8)
private var maskBitmap: Bitmap? = null

init {
val typedArray =
context.obtainStyledAttributes(attrs, R.styleable.RoundCornerLayout, defStyleAttr, 0)
cornerMode =
typedArray.getInt(
R.styleable.RoundCornerLayout_cornerMode,
CORNER_MODE_OUTLINE
)
topCornerRadius =
typedArray.getDimensionPixelSize(R.styleable.RoundCornerLayout_topCornerRadius, 0)
topCornerRadiusLeft =
typedArray.getDimensionPixelSize(R.styleable.RoundCornerLayout_topCornerRadiusLeft, 0)
topCornerRadiusRight =
typedArray.getDimensionPixelSize(R.styleable.RoundCornerLayout_topCornerRadiusRight, 0)
bottomCornerRadius =
typedArray.getDimensionPixelSize(R.styleable.RoundCornerLayout_bottomCornerRadius, 0)
bottomCornerRadiusLeft =
typedArray.getDimensionPixelSize(R.styleable.RoundCornerLayout_bottomCornerRadiusLeft, 0)
bottomCornerRadiusRight =
typedArray.getDimensionPixelSize(R.styleable.RoundCornerLayout_bottomCornerRadiusRight, 0)
typedArray.recycle()
mPaint.isAntiAlias = true
updateRoundRectMode()
}

private fun setRoundRectPath() {
roundedCorners[0] = topCornerRadiusLeft.toFloat()
roundedCorners[1] = topCornerRadiusLeft.toFloat()
roundedCorners[2] = topCornerRadiusRight.toFloat()
roundedCorners[3] = topCornerRadiusRight.toFloat()
roundedCorners[4] = bottomCornerRadiusLeft.toFloat()
roundedCorners[5] = bottomCornerRadiusLeft.toFloat()
roundedCorners[6] = bottomCornerRadiusRight.toFloat()
roundedCorners[7] = bottomCornerRadiusRight.toFloat()
mRect.set(0f, 0f, width.toFloat(), height.toFloat())
mRoundRectPath.rewind()
mRoundRectPath.addRoundRect(mRect, roundedCorners, Path.Direction.CW)
}


private fun setOutlineMode() {//讨巧上下多截出去一点,达到只有上圆角或者下圆角,实际还是一致的圆角
when {
topCornerRadius != 0 && bottomCornerRadius == 0 -> {
clipToOutline = true
outlineProvider = object : ViewOutlineProvider() {
override fun getOutline(view: View, outline: Outline) {
outline.setRoundRect(
Rect(0, 0, view.width, view.height + topCornerRadius),
topCornerRadius.toFloat()
)
}
}
}
topCornerRadius == 0 && bottomCornerRadius != 0 -> {
clipToOutline = true
outlineProvider = object : ViewOutlineProvider() {
override fun getOutline(view: View, outline: Outline) {
outline.setRoundRect(
Rect(0, 0 - bottomCornerRadius, view.width, view.height),
bottomCornerRadius.toFloat()
)
}
}
}
topCornerRadius != 0 && bottomCornerRadius != 0 && bottomCornerRadius == topCornerRadius -> {
clipToOutline = true
outlineProvider = object : ViewOutlineProvider() {
override fun getOutline(view: View, outline: Outline) {
outline.setRoundRect(
Rect(0, 0, view.width, view.height),
topCornerRadius.toFloat()
)
}
}
}
}
}

private fun updateRoundRectMode() {
when (cornerMode) {
CORNER_MODE_OUTLINE -> {
setOutlineMode()
}
else -> clipToOutline = false
}
}

override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
when (cornerMode) {
CORNER_MODE_XFERMODE -> {
maskBitmap?.recycle()
maskBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888).apply {
val canvasTmp = Canvas(this)
setRoundRectPath()
canvasTmp.drawPath(mRoundRectPath, mPaint)
}
}
CORNER_MODE_CLIPPATH -> {
setRoundRectPath()
}
}
}

override fun dispatchDraw(canvas: Canvas?) {
when (cornerMode) {
CORNER_MODE_CLIPPATH -> {
canvas?.clipPath(mRoundRectPath) //切割指定区域
super.dispatchDraw(canvas)
}
CORNER_MODE_XFERMODE -> {
val layerId = canvas?.saveLayer(mRect, mPaint) ?: -1
super.dispatchDraw(canvas)
mPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_IN) //设置图层混合模式
maskBitmap?.run {
canvas?.drawBitmap(this, 0f, 0f, mPaint)
}
mPaint.xfermode = null
canvas?.restoreToCount(layerId)
}
else -> {
super.dispatchDraw(canvas)
}
}
}
}

三、使用


本文开头的圆角效果就是下面这段代码实现的:


<com.ui.RoundCornerLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="20dp"
android:layout_marginTop="20dp"
android:layout_marginRight="20dp"
app:topCornerRadius="@dimen/roundRectCornerTop">

<TextView
android:layout_width="match_parent"
android:layout_height="50dp"
android:text="outline"
android:background="@android:color/holo_blue_light"
android:gravity="center" />
</com.ui.RoundCornerLayout>

<com.ui.RoundCornerLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="20dp"
android:layout_marginTop="20dp"
android:layout_marginRight="20dp"
app:bottomCornerRadius="@dimen/roundRectCornerTop">

<TextView
android:layout_width="match_parent"
android:layout_height="50dp"
android:text="outline"
android:background="@android:color/holo_blue_light"
android:gravity="center" />
</com.ui.RoundCornerLayout>

<com.ui.RoundCornerLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="20dp"
android:layout_marginTop="20dp"
android:layout_marginRight="20dp"
app:bottomCornerRadius="@dimen/roundRectCornerTop"
app:topCornerRadius="@dimen/roundRectCornerTop">

<TextView
android:layout_width="match_parent"
android:layout_height="80dp"
android:text="outline"
android:background="@android:color/holo_blue_light"
android:gravity="center" />
</com.ui.RoundCornerLayout>

<com.ui.RoundCornerLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="20dp"
android:layout_marginTop="20dp"
android:layout_marginRight="20dp"
app:cornerMode="xfermode"
app:topCornerRadiusLeft="@dimen/roundRectCornerTop"
app:topCornerRadiusRight="@dimen/roundRectCornerTop"
app:bottomCornerRadiusLeft="0dp"
app:bottomCornerRadiusRight="@dimen/roundRectCornerTop2"
>
<TextView
android:layout_width="match_parent"
android:layout_height="80dp"
android:text="xfermode"
android:background="@android:color/holo_blue_light"
android:gravity="center" />
</com.ui.RoundCornerLayout>
<com.ui.RoundCornerLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="20dp"
android:layout_marginTop="20dp"
android:layout_marginRight="20dp"
app:cornerMode="clip_path"
app:topCornerRadiusLeft="0dp"
app:topCornerRadiusRight="@dimen/roundRectCornerTop"
app:bottomCornerRadiusLeft="0dp"
app:bottomCornerRadiusRight="@dimen/roundRectCornerTop2"
>
<TextView
android:layout_width="match_parent"
android:layout_height="80dp"
android:text="clippath"
android:background="@android:color/holo_blue_light"
android:gravity="center" />
</com.ui.RoundCornerLayout>

作者:zzy的学习笔记
链接:https://juejin.cn/post/7222079859710115877
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

MVI 架构的理解

回顾MVC MVP MVVM MVC MVC架构主要分为以下几部分: View层: 对应于xm布局文件和java代码动态view部分。 Controller层: 主要负责业务逻辑,在android中由Activity承担,但xml视图能力太弱,所以A...
继续阅读 »

回顾MVC MVP MVVM


MVC


image.png


MVC架构主要分为以下几部分:




  • View层: 对应于xm布局文件和java代码动态view部分。




  • Controller层: 主要负责业务逻辑,在android中由Activity承担,但xml视图能力太弱,所以Activity既要负责视图的显示又要加入控制逻辑,承担功能过多。




  • Model层: 主要负责网络请求,数据库处理,I/O操作,即页面的数据来源。




MVC数据流向为:



  • View接收用户的点击

  • View请求Controller进行处理或直接去Model获取数据

  • Controller请求model获取数据,进行其他的业务操作,将数据反馈给View层


MVC缺点:


如上2所说,android中xml布局功能性太弱,activity实际上负责了View层与Controller层两者的功能,耦合性太高。


MVP:


image.png


MVP主要分为以下几部分:


1.View层:对应于Activity与xml,只负责显示UI,只与Presenter层交互,与Model层没有耦合。


2.Presenter层:主要负责处理业务逻辑,通过接口回调View层。


3.Model层:主要负责网络请求,数据库处理的操作。


MVP解决了MVC的两个问题,即Activity承担了两层职责与View层和Model层耦合的问题。


MVP缺点:


1.Presenter层通过接口与View通信,实际上持有了View的引用。


2.业务逻辑的增加,一个页面变得复杂,造成接口很庞大。


MVVM


image.png
MVVM改动在于将Presenter改为ViewModel,主要分为以下几部分:


1.View: Activity和Xml,与其他的相同


2.Model: 负责管理业务数据逻辑,如网络请求,数据库处理,与MVP中Model相同


3.ViewModel:存储视图状态,负责处理表现逻辑,并将数据设置给可观察容器。


View和Presenter从双向依赖变成View可以向ViewModel发送指令,但ViewModel不会直接向View回调,而是让View通过观察者的模式去监听数据的改变,有效规避MVP双向依赖的缺点。


MVVM缺点:


多数据流:View与ViewModel的交互分散,缺少唯一修改源,不易于追踪。


LiveData膨胀:复杂的页面需要定义多个MutableLiveData,并且都需要暴露为不可变的LivewData。


MVI是什么?


先上图


image.png
其主要分为以下几部分




  1. Model层: 与MVVM中的Model不同的是,MVIModel可以理解是View Model,存储视图状态,负责处理表现逻辑,并将ViewState设置给可观察数据容器




  2. View层: 与其他MVVM中的View一致,可能是一个Activity或者任意UI承载单元。MVI中的View通过订阅Model的变化实现界面刷新




  3. Intent层: 此Intent不是ActivityIntent,而是指用户的意图,比如点击加载,点击刷新等操作,用户的任何操作都被包装成Intent,在model层观察用户意图从而去做加载数据等操作。




目前android主流的MVI是基于协程+flow+viewModel去实现的,协程应该大家都知道,所以先来了解一下MVI中的flow


flow是什么?


在flow 中,数据如水流一样经过上游发送,中间站处理,下游接收,类似于Rxjava,使用各种操作符实现异步数据流框架
代码示例:


   runBlocking {
flow {
emit(1)
emit(2)
emit(3)
emit(4)
emit(5)
}.filter { it > 2 }
.map { it * 2 }
.take(2)
.collect {
Log.d("FLOW", it.toString())
}
}

flow是冷流,只有订阅者订阅时,才开始执行发射数据流的代码
即下游无消费行为时,上游不会产生数据,只有下游开始消费,上游才从开始产生数据,从上述例子看,当调用了collect后,才会执行flow语句块里面的代码,并且flow每次重新订阅收集都会将所有事件重新发送一次


但是在我们的开发场景中,一般是先触发某个事件(比如请求数据之后)才会去刷新UI,显然flow不适用于这种场景,因为flow只有在下游开始消费时才会触发生产数据


因此引入一个新的概念,StateFlow:


StateFlow与Flow的区别是StateFlow是热流,即无论下游是否有消费行为,上游都会自己产生数据。
代码示例:


在ViewModel创建StateFlow,发送UI状态,关于UI状态下面会讲,这里主要了解StateFlow的用法


//创建flow
private val _state = MutableStateFlow<ViewState>(ViewState.Default)
val state: StateFlow<EnglishState>
get() = _state

//发送UI状态
state.value = ViewState.Loading

在Activity中接收:


    mViewModel.state.collect{
when(it) {
is ViewState.Default -> {

}
is ViewState.Loading -> {
//展示加载中页面
tvLoading.visibility = View.VISIBLE
}
is ViewState.BannerMsg -> {
//加载完成,绑定数据
tvLoading.visibility = View.GONE
tvError.visibility = View.GONE
mAdapter.setData(it.data)
}
is ViewState.Error -> {
//加载失败,展示错误页面
tvError.visibility = View.VISIBLE
}
}
}

看起来这个StateFlow用法和MVVM中的LiveData类似,那它们有什么区别呢?


区别1:StateFlow 需要将初始状态传递给构造函数,而 LiveData 不需要。


区别2:当 View 进入 STOPPED 状态时,LiveData.observe() 会自动取消注册使用方,停止发送数据, 而从 StateFlow 收集数据的操作并不会自动停止。如需要实现LiveData相同的行为,可以在 Lifecycle.repeatOnLifecycle 块中去观察数据流。


MVI框架构建:


Intent介绍:


上面提到,intent指的是用户意图,在代码层面上来说他其实就是个枚举类,在kotlin中可以通过sealed关键字来生成封闭类,这个关键字生成的封闭类在when语句中可以不用写else


sealed class UserIntent {
object GetBanners: UserIntent() //定义了一个用户获取Banner的意图
}

处理Intent


这里需要了解一下:Chnnel


channel主要用于协程之间的通讯,使用send和receive往通道里写入或者读取数据,2个方法为非阻塞挂起函数,channel是热流,不管有没有订阅者都会发送。
我们的view层的触发操作和viewModel层获取数据这个流程恰巧应该是需要完全分离的,并且channel具备flow的特性,所以用channel来做view和viewModel的通讯非常适合
根据上面的例子,用Channel把UserIntent处理一下:
在View Model定义并观察用户意图:


class UserViewModel : ViewModel() {
val userIntent = Channel<UserIntent>() //定义用户意图

init {
observeUserIntent()
}

private fun observeUserIntent() { //观察用户意图
viewModelScope.launch {
userIntent.consumeAsFlow().collect{
when(it) {
is UserIntent.GetBanners -> {
loadBanner()
}
}
}
}
}

Activity中发送用户意图:


class MainActivity : AppCompatActivity() {
private val mViewModel by lazy {
ViewModelProvider(this)[UserViewModel::class.java]
}
private val mAdapter by lazy { BannerAdapter() }
private lateinit var rvData: RecyclerView
private lateinit var tvLoading: TextView
private lateinit var tvError: TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initView()
loadData()
}

private fun initView() {
rvData = findViewById<RecyclerView?>(R.id.rv_data).apply {
layoutManager = LinearLayoutManager(this@MainActivity)
adapter = mAdapter
}
tvLoading = findViewById(R.id.loading)
tvError = findViewById(R.id.load_error)
}

private fun loadData() { //将用户意图传给view Model
lifecycleScope.launch {
mViewModel.userIntent.send(UserIntent.GetBanners)
}
}

看完上面的代码,MVI中的View到Model之间的数据流向就已经清晰了,
接下来就是Model向View层传递数据的过程


State介绍:


State是UI状态,MVI的一个特点就是数据状态统一管理,state是个和Intent一样的枚举,但是不同的是intent是个事件流,state是个状态流
定义一个State类:


sealed class ViewState {
object Default: ViewState() //页面默认状态
object Loading : ViewState() //页面加载
data class BannerMsg(val data: List<Banner>?): ViewState() //页面加载完成
data class Error(val error: String?): ViewState() //页面加载错误
}

处理State


在ViewModel中观测到用户意图,根据用户意图去做相关操作,然后将UI State反馈给用户



class UserViewModel : ViewModel() {
val userIntent = Channel<UserIntent>()
private val _state = MutableStateFlow<ViewState>(ViewState.Default)
val state: StateFlow<EnglishState>
get() = _state

init {
observeUserIntent()
}

private fun observeUserIntent() {
viewModelScope.launch { //观测用户意图
userIntent.consumeAsFlow().collect{
when(it) {
is UserIntent.GetBanners -> {
loadBanner()
}
}
}
}
}

private fun loadBanner() {
viewModelScope.launch {
state.value = ViewState.Loading //加载中状态 反馈给View层
val banners = CloudService.cloudApi.getBanner() //获取数据
banners.data?.let {
state.value = ViewState.BannerMsg(it) //加载成功状态,数据反馈给View
return@launch
}
state.value = ViewState.Error(banners.errorMsg) //加载错误状态反馈给View
}
}
}

Activity中观察页面状态:


class MainActivity : AppCompatActivity() {
private val mViewModel by lazy {
ViewModelProvider(this)[UserViewModel::class.java]
}
private val mAdapter by lazy { BannerAdapter() }
private lateinit var rvData: RecyclerView
private lateinit var tvLoading: TextView
private lateinit var tvError: TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initView()
observeViewModel()
loadData()
}

private fun initView() {
rvData = findViewById<RecyclerView?>(R.id.rv_data).apply {
layoutManager = LinearLayoutManager(this@MainActivity)
adapter = mAdapter
}
tvLoading = findViewById(R.id.loading)
tvError = findViewById(R.id.load_error)
}

private fun loadData() {
lifecycleScope.launch {
//发送用户意图
mViewModel.userIntent.send(UserIntent.GetBanners)
}
}

private fun observeViewModel() {
lifecycleScope.launch {
mViewModel.state.collect{ //观测UI状态,根据不同的状态刷新Ui
when(it) {
is ViewState.Default -> {
//初始值不做任何操作
}
is ViewState.Loading -> {
//展示加载中页面
tvLoading.visibility = View.VISIBLE
}
is ViewState.BannerMsg -> {
//加载完成,绑定数据
tvLoading.visibility = View.GONE
tvError.visibility = View.GONE
mAdapter.setData(it.data)
}
is ViewState.Error -> {
//加载失败,展示错误页面
tvError.visibility = View.VISIBLE
}
}
}
}
}
}

MVI架构主要代码介绍完毕。


MVI总结:


MVI强调数据的单向流动,主要分为几步:




  • 用户操作以Intent的形式通知Model.




  • Model基于Intent更新State




  • View接收到State变化刷新UI




数据永远在一个环形结构中单向流动,不能反向流动。


MVI优缺点


优点:



  • MVI的核心思想是 view-intent-viewmodel-state-view 单向数据流,MVVM核心思想是 view-viewmodel-view 双向数据流



    • 代码分层更清晰,viewmodel 无需关心view如何触发和更新,只需要维护intentstate即可





    • IntentState的引入解决了ViewModelModel的界限模糊问题




缺点:




  • 单向流和双向流,并非 好和不好 的选择,而是 适合和不适合 的选择,业务逻辑较为简单的界面,它不需要mvi、mvvm、mvp、mvc,只一个activity或者fragment + layout 即可,一味的套用架构,反而适得其反!




  • 逻辑、数据、UI 较为复杂时,intentstate将会变得臃肿


作者:用户1577096808602
链接:https://juejin.cn/post/7222460499493568571
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

使用Compose DeskTop实现一个带呼吸灯的秒表

前言 Compose Multiplatform是由Jetbrain团队维护的一个基于Kotlin和Jetpack Compose用于跨多平台的共享UI的声明式框架,目前支持的平台除了Android以外,还有iOS,Web和桌面,如此厉害的技术怎么能不亲自上手...
继续阅读 »

前言


Compose Multiplatform是由Jetbrain团队维护的一个基于Kotlin和Jetpack Compose用于跨多平台的共享UI的声明式框架,目前支持的平台除了Android以外,还有iOS,Web和桌面,如此厉害的技术怎么能不亲自上手尝试一下呢,所以这篇文章要讲的就是使用Compose Desktop开发一个桌面版的秒表应用


准备工作


在开发之前,我们要确定下使用的开发环境,这里我使用的编辑器是IntelliJ IDEA 2022.3.3这个版本,JDK环境用的是11,貌似是最低要求。


image.png

如何创建项目就不说了,现在很多文章都有详细讲解,我们直接开始吧


创建视图


首先我们的秒表肯定是有开始计时与结束计时两个状态,所以我们的界面上需要一个按钮来控制这两个状态,那么第一步就是在main函数的Window组件内绘制出这个按钮


image.png

其中turnOn变量就是我们控制状态的开关,通过Button的点击事件来改变,并且按钮的文案也随着状态的更改显示不同的文字,Clock就是我们绘制秒表的函数,并且接收turnOn这个变量控制秒表的计时。好了以后我们看一下效果


image.png

正如我们预想的一样,一个简单的桌面应用就出来了,接下来就开始绘制我们的秒表


绘制外框


秒表的外框通常来讲就是个圆,而使用Canvas绘制圆有两种选择,一种是使用drawCircle,另一种是使用drawPath,但考虑到drawCircle无法定义边框的大小,所以我们直接使用drawPath函数,绘制Path的话我们需要定义几个变量,分别是中心点坐标,Rect的左上坐标和右下坐标,代码如下


image.png

表盘选取在水平居中位置绘制,其中在中心点的y坐标,以及Rect的y坐标上都加上100的原因主要是为了如果边框的粗细设置的比较大的话,表盘不会被视图遮挡,现在我们就在Canvas中绘制定义好的Path


image.png

一个简单的边框就绘制好了,我们看下效果


111.png

又大又圆,边框绘制完毕,接下去就是表盘的刻度了


绘制刻度


刻度的样式每一种表盘上都不一样,我们这边就简单一些,就在5,10,15这样的刻度上显示文字,其他位置用圆点代替,不然60个数字画一圈怕是太密密麻麻了,那怎么做呢?我们分两步来,第一步先画数字,以下是我们需要用到的变量


image.png

由于绘制数字的方向是在一个圆周上的,所以我们定义一个数组angList存放绘制角度,同时也相对应的定义另一个数组textList存放数字的文案,circleRdius是表盘半径,用来计算圆周坐标用,现在就是要绘制文案了,我们使用DrawScopedrawText函数,有的人会说DrawScope下面哪来的drawText啊?那是因为drawText是在Compose 1.3.0版本推出的,所以如果找不到drawText的话,那就赶紧去更新版本吧,我们看下drawText这个函数都提供了哪些参数


image.png

可以看到必填的参数是前两个,一个是TextMeasurer对象,用来测量文案的,另一个不用多说,设置text,然后topLeft这个属性也是需要的,总不能12个数字都叠在一起吧,知道了要填的参数,我们现在就调用一下drawText


image.png

我们使用rememberTextMeasurer函数创建了一个TextMeasurer对象,并且使用pointXpointY分别计算了每个数字坐上的x,y坐标,两个函数的代码如下


image.png

这里至于为什么要在Offset函数中分别对计算出来的x,y减去20,主要是因为虽然计算出来的坐标是刚好在圆周上,但是当文字绘制出来以后,整体布局会有点偏右下,所以得在结果坐标上再减去20,让文字可以刚好看起来在圆周坐标的中心位置,现在我们运行下代码看下效果怎样


image.png

可以看到数字都画上去了,效果还行,接下来就是圆点刻度,同样定义需要用到的变量


image.png

degreeColor是绘制圆点刻度的颜色,pointAngleList跟上面的anglist一样,是存放圆点角度的数组,虽然说这个数组的大小定义为60,但是在lambda表达式中我们判断了如果计算出来的角度在anglist中已经存在,那么就不赋值用0代替,最终绘制的时候我们判断如果角度为0,那么就不绘制,所以0度的刻度不会被绘制在表盘上,而绘制圆点我们直接使用drawCircle函数,代码如下


image.png

因为同样也是在圆周上,所以计算圆点的坐标也用到了pointx与pointy函数,我们再看下效果


image.png

有内味儿了是不,我们接下来开始画指针


绘制指针


指针其实就是一根line,我们使用drawLine函数就能绘制出来,另外我们在中心点位置再绘制一个圆点,当作是把指针固定在表盘上的一样,代码如下


image.png

其中pointerColor是指针和圆点的颜色,运行一遍代码,我们看到指针已经绘制上去了


image.png

但是指针跟刻度不一样,它得是能绕着圆点动的,怎么动呢?我们看到上面那根静态指针绘制的角度是在angList[0]上,那是不是不停的改变角度,我们的指针就动起来了呢?我们来定一个数组来存在所有需要经过的角度


image.png

totalList就是存放所有角度的数组,至于intervalSize是什么呢,我们知道有的秒表上指针是一格一格走的,间隔比较大,有的间隔比较小,看起来的效果就比较丝滑,intervalSize就是定义指针走动的频率大小的值,并且是能够被360整除的,数组定义好了,我们再给数组下标创建个动画


image.png

这里创建了一个循环动画,因为totalList遍历完一遍以后,代表着一分钟过去了,角度又得重新开始遍历,所以我们给数组的下标值定义了一个循环动画,另外我们还使用LaunchedEffect函数,来监听外部传来的turn值的变化,turn为true的时候,angleIndex的初始值目标值不同,动画开启,turn为false的时候,angleIndex的初始值目标值相同,动画暂停。我们更新下CanvasdrawLine的代码,让drawLine里面获取角度的下标值的变量变成angleIndex


image.png

我们看下效果


aaa1.gif


文字时间


一个秒表的表盘绘制完毕,我们再加点东西,一般性一个秒表底下都会有个文字时间在跳动,差不多由分,秒,毫秒组成,我们这边也加上这些东西,并且在分与秒之间用文字“分”隔开,秒与毫秒之间用“秒”字隔开,那么这五个Text我们要计算出它们topLeft的坐标


image.png

文案的y坐标很容易,就是在表盘底部y坐标上再加点距离就好,至于横坐标,就是找出中间一块区域再五等分,坐标定义完毕,我们先把两个中文绘制出来,x坐标取timeXList下标为1和3的值


image.png

接着我们想一下毫秒位置的数字怎么展示,毫秒位置是在一秒内从0跳到99,然后再从0跳到99,这不又是个循环动画吗,我们仿照指针的动画,将毫秒的动画创建出来


image.png

同样的,因为毫秒的动画也跟随着turn值的变化而改变,所以我们将这个过程也在LaunchEffect中添加上


image.png

现在我们可以在Canvas中将毫秒也绘制出来了


image.png

这边还做了一个处理,当毫秒的值为个位数的时候,我们在数字的边上再加上一个0,让数字跳动的时候看起来效果好一些,毫秒的位置已经绘制完毕,秒的位置也一样,因为它也是从0到60变化的一个循环动画,所以它的代码与毫秒基本差不多


image.png

现在我们再看下效果


aaa2.gif


还剩下分的位置,分就不能用循环动画来实现了,它是一个逐渐递增的过程,当秒的位置为从59变回0的时候,分的位置加一,那么我们就需要一个变量来记录分的值


image.png

minuteValue用来记录分钟的值,然后我们在Canvas里面判断当mainSecondText刚到59的时候,就准备开始给minuteValue加一,为什么是开始准备而不是立马加一呢,因为如果那样做的话,显示的效果是秒的位置一到59秒的时候,分就加一了,这就不符合实际了,我们希望是当59变为0的那会分才加一,所以我们还需要一个状态位,当mainSecondText变为59的时候,状态位打开,直到mainSecondText变成0的时候,状态位才关闭,这个时候分才加一,我们把状态位命名为addMinute


image.png

给分钟设置值的代码如下


image.png

再运行一遍代码看看效果如何


aaa3.gif


完美的衔接起来了,这样一个秒表的功能就基本完成了,我们稍微在点缀一下,如标题所示,加个呼吸灯


呼吸灯效果


在做这个效果之前,这里有个问题,大家是否知道在Compose里面如何给视图设置渐变色?使用drawable吗?Compose里可不兴这些,咱回忆下我们在调用drawpath函数的时候,编辑器是不是会给出这样的提示


image.png

有两个drawPath的函数,这俩函数的区别是在第二个参数上,一个是Color,另一个是Brush,我之前通常都是用Color的,因为Brush是个啥我也不知道,但是当我看到Brush里面的代码以后


image.png

看到第一行注释没,这个其实就是用来做渐变效果的,它比我们传统Android里面设置渐变功能还要丰富,不但渐变的颜色没有限制,方向也没有限制,也就是说你可以在任意两个点之间设置若干种颜色的渐变,现在我们就在我们秒表的边框上设置三种颜色的渐变吧


image.png

首先设置好我们要渐变的颜色值,然后将这个存放颜色值的circleColorList当作参数传入drawPathBrush


image.png

边框的粗细也加大到了30,这样也能清晰的看到渐变效果,现在运行后的效果如下


image.png

效果出来了是不,现在是三个颜色的渐变,那既然刚刚说了Brush的渐变颜色可以是若干个,那么我们在circleColorList中再添加几个颜色试试


image.png

从刚刚的三个变成了六个颜色的数组,再运行一下看看效果会怎么样呢?


image.png

是不是跟刚刚的那个效果图比起来,这个时候的边框渐变色更多了呢,到了这里,咱有个想法,通过之前的循环动画,我们能不能将Brush里面的渐变色值也循环起来呢,比如先设置的是circleColorList下标为0,1,2的颜色,接下去就是显示下标为1,2,3的颜色,以此类推,下标值到了数组末尾,下一个再从头开始,这么做到底会有什么效果呢,我们试一下


image.png

如上述代码所示,我们创建了一个初始值为0,目标值为circleColorList.lastIndex的循环动画,动画时长为两秒,接下去,我们通过判断不同的下标值场景来选取不同的颜色来绘制边框


image.png

由于是三种颜色的渐变,所以场景选择了如果colorIndex为数组最后一个下标,colorIndex为数组倒数第二个下标,以及其他情况,现在我们再来看看边框效果


aaa4.gif


是不是就像表盘周围安置了一个呼吸灯一样,但是这个呼吸灯还不是很完善,因为我们看到的效果,这个呼吸的过程是慢慢从浅色开始,逐渐变深,然后由深变浅是一瞬间的过程,感觉像是这个呼吸被打断了一样,造成这个效果的原因是我们circleColorList数组里面的色值,根据下标的递增是逐渐变深的,但是缺少逐渐变浅的过程,所以我们应该在circleColorList中再增加几个色值,也就是将原来的色值顺序倒转一下添加进去,就像下面这样


image.png

这样就满足了我们呼吸灯由浅变深和由深变浅的两个过程,我们再看看效果


aaa5.gif


总结


Compose DeskTop的秒表功能完成了,这也是我Compose Multiplatform的第一个demo,先选择DeskTop主要是因为几个跨平台里面只有DeskTop与Android的代码算是真正意义上的一套代码跨平台使用,Web主要是多了几个Dom组件,Android里面没法使用,而iOS现在也只是刚刚发布Alpha版,我还在摸索学习中,所以先用DeskTop开个场,后面别的平台的小应用也会相继推出。


作者:Coffeeee
链接:https://juejin.cn/post/7222823687837220901
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

使用 Compose 时长两年半的 Android 开发者,又有什么新总结?

大家好啊,我是使用 Compose 时长两年半的 Android 开发者,今天来点大家想看的东西啊,距离上次文章也已经过去一段时间了,是时候再次总结一下了。 期间一直在实践着之前文章说的使用 Compose 编写业务逻辑,但随着业务逻辑和页面越来越复杂,在使用...
继续阅读 »

大家好啊,我是使用 Compose 时长两年半的 Android 开发者,今天来点大家想看的东西啊,距离上次文章也已经过去一段时间了,是时候再次总结一下了。

期间一直在实践着之前文章说的使用 Compose 编写业务逻辑,但随着业务逻辑和页面越来越复杂,在使用的过程中也遇到了一些问题。


Compose Presenter


上一篇文章中有提到的用 Compose 写业务逻辑是这样写的:


@Composable
fun Presenter(
action: Flow<Action>,
)
: State {
var count by remember { mutableStateOf(0) }

action.collectAction {
when (this) {
Action.Increment -> count++
Action.Decrement -> count--
}
}

return State("Clicked $count times")
}

优点在之前的文章中也提到过了,这里就不再赘述,说一下这段时间实践下来发现的缺点:



  • 业务复杂后会拆分出非常多的 Presenter,导致在最后组合 Presenter 的时候会非常复杂,特别是对于子 Presenter 的 Action 处理

  • 如果 Presenter 有 Action,这样的写法并不能很好的处理 early return。


一个一个说


组合 Action 处理


每调用一个带 Action 的子 Presenter,就至少需要新建一个 Channel 以及对应的 Flow,并且需要增加一个对应的 Action 处理,举个例子


@Composable
fun FooPresenter(
action: Flow<FooAction>
)
: FooState {
// ...
// 创建子 Presenter 需要的 Channel 和 Flow
val channel = remember { Channel<Action>(Channel.UNLIMITED) }
val flow = remember { channel.consumeAsFlow() }
val state = Presenter(flow)
LaunchedEffect(Unit) {
action.collect {
when (it){
// 处理并传递 Action 到子 Presenter中
is FooAction.Bar -> channel.trySend(it.action)
}
}
}

// ...

return FooState(
state = state,
// ...
)
}

如果页面和业务逻辑复杂之后,组合 Presenter 会带来非常多的冗余代码,这些代码只是做桥接,没有任何的业务逻辑。并且在 Compose UI 中发起子 Presenter 的 Action 时也需要桥接调用,最后很容易导致冗余代码过多。


Early return


如果一个 Presenter 中有 Action 处理,那么需要非常小心的处理 early return,例如:


@Composable
fun Presenter(
action: Flow<Action>,
)
: State {
var count by remember { mutableStateOf(0) }

if (count == 10) {
return State("Woohoo")
}

action.collectAction {
when (this) {
Action.Increment -> count++
Action.Decrement -> count--
}
}

return State("Clicked $count times")
}

count == 10 时会直接 return,跳过后面的 Action 事件订阅,造成后续的事件永远无法触发。所以所有的 return 必须在 Action 事件订阅之后。


当业务复杂之后,上面两个缺点就成为了最大的痛点。


解决方案


有一天半夜我看到了 Slack 的 Circuit 是这样写的:


object CounterScreen : Screen {
data class CounterState(
val count: Int,
val eventSink: (CounterEvent) -> Unit,
) : CircuitUiState
sealed interface CounterEvent : CircuitUiEvent {
object Increment : CounterEvent
object Decrement : CounterEvent
}
}

@Composable
fun CounterPresenter(): CounterState {
var count by rememberSaveable { mutableStateOf(0) }

return CounterState(count) { event ->
when (event) {
is CounterEvent.Increment -> count++
is CounterEvent.Decrement -> count--
}
}
}

这 Action 原来还可以在 State 里面以 Callback 的形式处理,瞬间两眼放光,一次性解决了两个痛点:



  • 子 Presenter 不再需要 Action Flow 作为参数,事件处理直接在 State Callback 里面完成,减少了大量的冗余代码

  • 在 return 的时候就附带 Action 处理,early return 不再是问题。


好了,之后的 Presenter 就这么写了。期待再过半年的我能再总结出来一些坑吧。


为什么 Early return 会导致事件订阅失效


可能有人会好奇这一点,Presenter 内不是已经订阅过了吗,怎么还会失效。

我们还是从 Compose 的原理开始说起吧。

先免责声明一下:以下是我对 Compose 实现原理的理解,难免会有错误的地方。

网上讲述 Compose 原理的文章都非常多了,这里就不再赘述,核心思想是:Compose 的状态由一个 SlotTable 维护。

还是结合 Early return 的例子来说,我稍微画了一下 SlotTable 在不同时候的状态:


@Composable                                          
fun Presenter(
action: Flow<Action>, count != 10 | count == 10
)
: State {
var count by remember { mutableStateOf(0) } | State | State |
if (count == 10) { | State | State |
return State("Woohoo") | Empty | State |
} | | |
action.collectAction { | State | Empty |
when (this) { | State | Empty |
Action.Increment -> count++ | State | Empty |
Action.Decrement -> count-- | State | Empty |
} | | |
} | | |
return State("Clicked $count times") | State | Empty |
}

count != 10 的时候,SlotTable 内部保存的状态是包含 Action 事件订阅的,但是当 count == 10 之后,SlotTable 就会清空所有之后语句对应的状态,而之后正好包含了 Action 事件订阅,所以订阅就失效了。

我觉得这是 Compose 和 React Hooks 又一个非常相似的地方,React Hooks 的状态也是由一个列表维护的

再举一个例子:


@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Column {
var boolean by remember {
mutableStateOf(true)
}
Text(
text = "Hello $name!",
modifier = modifier
)
Button(onClick = {
boolean = !boolean
}) {
Text(text = "Hide counter")
}

if (boolean) {
var a by remember {
mutableStateOf(0)
}
Button(onClick = {
a++
}) {
Text(text = "Add")
}
Text(text = "a = $a")
}
}
}

这段代码大家也可以试试。当我做如下操作时:



  • 点击 Add 按钮,此时显示 a = 1

  • 点击 Hide counter 按钮,此时 counter 被隐藏

  • 再次点击 Hide counter 按钮,此时 counter 显示,其中 a = 0


因为当 counter 被隐藏时,包括变量 a 在内所有的状态都从 SlotTable 里面清除了,那么新出现的变量 a 其实是完全一个新初始化的一个变量,和之前的变量没有任何关系。


总结


过了大半年,也算是对 Compose 内部实现原理又有了一个非常深刻的认识,特别是当我用 C# 自己实现一遍声明式 UI 之后,然后再次感叹:SlotTable 真是天才般的解决思路,本质上并不复杂,但大大简化了声明式

作者:Tlaster
来源:juejin.cn/post/7222897518501543991
UI 的状态管理。

收起阅读 »

git stash 用过嘛?

各位在摸鱼的时候...不是... 各位在写代码的时候离不开的git其实发现有很多小技巧,老铁们,今天咱们就来唠一唠 笔者由于能力水平非常高...不是... 笔者的司由于前端人手紧缺,不得不自己负责多个项目,同时存在一个项目中多个更新迭代同时开发的情况,所以笔者...
继续阅读 »


各位在摸鱼的时候...不是...


各位在写代码的时候离不开的git其实发现有很多小技巧,老铁们,今天咱们就来唠一唠


笔者由于能力水平非常高...不是...


笔者的司由于前端人手紧缺,不得不自己负责多个项目,同时存在一个项目中多个更新迭代同时开发的情况,所以笔者每天的工作状态就是切分支,切分支...


当一个分支的内容还没开发完,不得不切换分支去改造另一个线上bug时,此时当前分支的内容一定要推到远程分支嘛,答案,不一定


这时,我们应该想到git stash


# 保存当前未commit的代码
git stash
# 保存当前未commit的代码并添加备注git stash save "订单详情"

#
应用最近一次的stashgit stash apply

#
应用最近一次的stash,随后删除该记录git stash pop

#
删除最近的一次stashgit stash drop

删除stash的所有记录git stash clear

#
查看所有记录git stash list
#还原某个版本git stash apply stash@{stash_id}

所有的命令我都罗列于此


所以话不多说,开干


git stash save '订单详情组件封装完成'

//此时我们可以切换分支
经过一系列的操作之后
//切换到当前分支

1.懒人操作
- 如果不想麻烦and墨迹,直接操作
git stash apply

2.如果想秀一波
- 那么好,操作
git stash clear

啊......

其实我想操作git stash list

来吧,继续操作~

当误操作git stash clear时,我们应该打印所有的提交列表
git log --graph --oneline --decorate $( git fsck --no-reflog | awk '/dangling commit/ {print $3}' )
如果在这个输出的内容中可以找到你提交 那么可以通过提交的id来找回

当内容无法找到时间,我们继续操作
git log --graph --oneline --decorate $( git fsck --no-reflog | awk '/dangling commit/ {print $3}' ) >1.txt
这个命令是将输出内容输出到1.txt文件中,在文件中可以通过搜索你保存时的关键字来进行找到id

再次执行
git stash apply id

3.当我们手不抖~

执行找到stash的列表
git stash list

还原某个版本
git stash apply stash@{stash_id}
大功告成

各位这波操作怎样,哈哈哈


其实就是日常工作(mo yu)的小技巧,希望对各位有帮助!


作者:凌云空间
来源:juejin.cn/post/7221825086667014205
收起阅读 »

踏平坎坷成大道---前端还有希望吗

最近在头条还有一些其他平台经常会看到一些论调“前端已死”,各种找工作难之类的信息,同时在自己身边也有不少朋友、同事持有同样的看法;作为前端开发接近10年的老菜鸟也想谈谈自己的一些看法【个人拙见,欢迎喷,喷的时候给出建议】 先说观点,前端未死 要知后续情节请...
继续阅读 »

最近在头条还有一些其他平台经常会看到一些论调“前端已死”,各种找工作难之类的信息,同时在自己身边也有不少朋友、同事持有同样的看法;作为前端开发接近10年的老菜鸟也想谈谈自己的一些看法【个人拙见,欢迎喷,喷的时候给出建议】



先说观点,前端未死



要知后续情节请看下面分解,如有雷同实属巧合;头条或一些平台的网友言论有很多带节奏的成分,而不是真实的!



为什么很多人说前端已死


找工作难



  • 不可否认的是这两年受疫情影响、国际国内经济形式的影响,我们身边能看到、能感受到各行各业的日子都不好过,各种内卷、各种无奈;我所在的公司在年前也进行了一波人员的精简,组内成员减半,这个已经是不争的事实;




  • 我们发现刷BOSS、刷智联看到的岗位似乎比前几年同期少了很多,发信息得到回复的次数也在减少,似乎简历就像石头一样沉入招聘的大海,隔壁王奶奶就跟我吐槽过多次;




  • 发现身边、各种论坛、网络上充斥着各种悲观的论调,讨论的热度不比36度的夏天差多少;




面试要求高



  • 隔壁王奶奶经常说,前几年简单问问Vue\React原理、生命周期、JS基础,聊聊项目经验就能收到几份Offer,自己还在选择哪家公司更有钱景; 而现在呢? 一上来就问源码、算法、数据结构、还有让直接手写源码的,就问各位看官慌不慌、惊不惊?




  • 说实话,在前端岗位摸打滚爬接近10年,感觉自己老了却还是个菜鸟,很多原理性的东西真的很弱,计算机基础也很差(早期培训了),上班干活也就每天那一、二、三3板斧;诸位有没有跟我一样的,面试很没底呢!




有没有核心技能

既然面试要求那么高,那么卷,这个时候就要问问自己到底有没有核心竞争力呢?很庆幸目前我{ age: 36 }目前尚有一份可维持生计的工作(可能也快了),短期还能跟大家一期扯淡;自我感觉10年下来我并没有掌握核心技能,诸位呢?如果一定要回答自己有什么信心敢出去面试,大概有这么几点吧:



  1. 脸皮够厚,不怕被拒绝,能怼人

  2. 得益于早些年坚持锻炼,现在加班熬夜还能和年轻人一战

  3. 长期的积累,基础还行,目前还学得动

  4. 得益于长期的积累,对待产品、项目有一些心得和自己的看法


前端的路在哪?



在回答该问题之前我想说的是:当下的局面对刚刚入行、初级前端是很不友好的,对于已经在行业内站稳了,有几年工作经验的前端朋友而言,处境并没有那么悲观;所以,这里只聊聊已经有几年经验的情况;





  • 首先、我们通常讲的前端大部分还是停留在HTML + CSS + JS的模式,与后端相对, 那前端就是在画页面,调数据,写交互,诸位的工作是这样吗?




  • 其次,前端在项目中、在团队中的核心价值是什么?




  • 最后,面对当下日益复杂的业务,前端能抢谁的饭碗,而谁又会抢前端的饭碗?




前端在团队、项目中的核心价值是什么?


要回答上面的问题,需要先弄清楚前端在团队、项目中的核心价值是什么,我个人认为,前端的核心价值在于:【用户体验】【用户体验】【用户体验】,前端不是页面仔,也不是接口仔,更不是切图仔!前端应该是一个胶水性质的岗位,哪里有漏堵哪里;在产品经理-设计师、UI设计师-UE交互设计师、UI\UE - 前端、 前端-后端、 后端-测试、
产品经理-项目经理,这些诸多环节之间做一个适配器的角色,在项目的全程去理清开发的每个环节,为项目的高质量交付发挥不可替代的作用;



说了那么多,前端就是要充当适配器的角色呀,那么是不是就意味着我们需要掌握的技能就要很多呢?要接触很多的面呢? 答案是肯定的;合格的前端技能至少是一超多强的,一精多能的,这也就是在面试中对初级不友好,对老鸟相对友好一点的根结!而这些技能的积累,产品、项目的把握能力需要一定的积累,短期怕是无法达到的;三点,重新审视自己的岗位:



  1. 【站在产品经理的角度去理解前端】

  2. 【站在项目经理的角度去理解前端开发的过程】

  3. 【站在用户的角度去理解前端的产出】


前端能抢谁的饭碗? 谁又来抢前端的饭碗?

这个嘛....(⊙o⊙)…,の,打败自己的从来都不是自己的同行,想这些是无意义的,把眼光盯在本公司同事的身上是无意义的;有个很好笑的现象是:很多人总是气不过自己身边的同事、朋友比自己每个月多几千块钱,而不生气身价百万、千万、甚至过多的成功人士; 所以我说,如果真的要抢,那应该从其他与之(前端)有关联的行业找出路;


如何破局?


如何提高薪资,如何不被抛弃? 这个问题思考了很久,反复拷问自己接下来该怎么走?最近几年也做了一些尝试,有的放弃了,有的没下文了;在划水之余关注了BOSS招聘一年多时间,看到了一些信息,供大家参考;





  1. 如果已经有多年的经验,前端架构师是不错的选择(最近1年在BOSS上看到,架构师的需求在不断增加),建议:一定要会NodeJS,最好学学其他后端语言、服务器、数据库,可以不写,但是一定要知道之间的区别,能做什么,适合做什么,如何选择;




  2. 大前端方向现在还有很大空间: Vue\React + Flutter(或类似) + 小程序,这里有个高的要求:建议去学一下原生的Android、IOS开发,至少知道是如何运行的,和混合式开发等区别是什么,如何同前端交互;




  3. 前端 + GIS 方向,这个是很推荐的!(最近1年看到gis岗位的招聘增加非常多)




  4. 前端 + 3D + GIS (各位看官,去看看BOSS吧,如果这些你同时拥有,还会担心工作岗位吗)




以上的都是硬实力,也就是面试时候绕不开的; 接下来的软实力,就是作为适配器角色的核心:



  • 沟通能力;

  • 对产品的理解能力;

  • 项目把控能力;


这个需要积累,决定前端的尽头最终走向哪的,前端最终的出路就是不是前端,可能是?


....


...


..


.


BOSS!
谁说前端出生就不能成为老板呢? 前端走向产品经理一定要强于一出来就是做产品的产品经理(当然其他岗位也可以),总之方向很多吧!


聊到

作者:风雪中的兔子
来源:juejin.cn/post/7220220100384817210
这里吧,该撸代码了!

收起阅读 »

必须会的前端基础通用优化方法

web
为什么要做优化? 虽然现在网速越来越快,客户端性能越来越好。但是还是有很多人在使用老旧的设备,不稳定的或者2G、3G网络。另外用户现在对应用的体验要求也越来越高,用户不仅会拿你的应用和同行业的竞争对手去做比较,并且会跟使用过的做的最好的应用去比较。 优化应用的...
继续阅读 »

为什么要做优化?


虽然现在网速越来越快,客户端性能越来越好。但是还是有很多人在使用老旧的设备,不稳定的或者2G、3G网络。另外用户现在对应用的体验要求也越来越高,用户不仅会拿你的应用和同行业的竞争对手去做比较,并且会跟使用过的做的最好的应用去比较。


优化应用的性能可以提升用户体验从而提高留存率,转化率。谷歌、微软和亚马逊的研究都表明,性能可以直接转换成收入。比如,Bing搜索网页时延迟2000ms会导致每用户收入减少4.3%。BBC发现他们的网站加载时间每增加一秒,他们就会失去10%的用户。


下面分享一些操作简单但是效果明显的优化方法。


1、使用HTTP 2.0


HTTP 2.0通过支持首部字段压缩和多路复用技术,让应用更有效地利用网络资源,减少感知的延迟时间。


二进制分帧机制是HTTP 2.0大幅度提高网页性能的核心,它定义了如何封装HTTP消息并在客户端与服务器之间传输。HTTP 1.x的版本都是通过文本的方式传递数据,而HTTP 2.0将传输的信息分割为更小的消息和帧,并对它们采用二进制格式的编码。


HTTP 2.0在二进制分帧的基础上实现了多路复用技术,可以在同一连接上同时发送多个请求和响应,解决了HTTP 1.x的队头阻塞问题,提高了并行处理能力和性能,突破了HTTP 1.x中每个连接每次只交付一个响应的限制。


HTTP 2.0使用HPACK算法对请求和响应头部进行压缩,减少了数据传输量, 可以显著减少每个请求的开销,提高了网络传输速度。而且,它还支持服务器到客户端的主动推动推送机制。


体验demo:http2.akamai.com/demo


开启http2的方法也非常简单,下面以nginx为例


server {
-      listen       443 ssl;
+      listen       443 ssl http2;
       ...
}

2、缓存资源


浏览器发出的所有HTTP请求首先会转至浏览器缓存,用于检查是否存在可满足请求的有效缓存响应。如果存在匹配,则从缓存中读取响应,从而消除网络延迟和传输产生的数据成本。


HTTP缓存是一种提高负载性能的有效方式,因为它减少了不必要的网络请求。所有浏览器都支持该功能,并且不需要太多设置。默认情况下,大部分Web服务器内置支持设置缓存相关表头的设置。


3、缩小和压缩传输的资源


对传输的资源进行缩小和压缩可以有效减少负载大小,进而缩短页面加载时间。


像webpack中已经内置了缩小代码的插件,不需要做额外的工作就可以直接使用,可以删除空格和不需要的代码。


压缩是使用压缩算法修改数据的过程。目前使用最广泛的压缩格式是Gzip,但可以有限考虑使用Brotli,2015年谷歌推出的Brotli压缩算法能在Gzip的基础上将数据再压缩20~25%,现在大部分的浏览器已经支持这种压缩格式,国外很多站点已经开始使用,但是国内还没有开始大规模的应用。很多托管平台、CDN和反向代理服务器默认情况下都会对资产进行压缩编码,或者经过简单的配置就可以轻松实现。下面以Express为例配置一下动态压缩。


const express = require('express');
const compression = require('compression');

const app = express();

app.use(compression());
app.use(express.static('public'));

const listener = app.listen(process.env.PORT, () => {
    console.log(`Your app is listening on port ${listener.address().port}`)
})


4、使用CDN(内容分发网络)


由离用户更近的服务器向用户提供数据,可以显著减少每次TCP连接的网络延迟,增大吞吐量。选择一个可靠的CDN服务提供商进行简单的配置就可以,如阿里云、腾讯云、百度云等。


5、图片处理


对图片处理可以很好得对图片就行优化,经过图片处理优化的图像可以节省40%~80%的大小。虽然通过构建脚本也可以实现图片处理的效果,但在实践中一般使用第三方提供的图像CDN,第三方图像CDN也可以提供更多形式的图像处理方式。通过向文件地址传递参数来获取合适的图像,而不是直接获取原文件。


比如在chrome浏览器中使用WebP格式图片,WebP是由谷歌开发的一种新型图片格式,相比JPEG和PNG格式,WebP图片可以更好地压缩图片大小,从而提高页面加载速度。


6、优先加载关键资源


优先加载关键资源,延迟加载次要资源。优先加载关键资源可以减少页面加载时间,加快页面的渲染速度,提高用户体验。可以对网站进行分析,确定哪些资源是关键资源,然后将非关键资源设置为延迟加载。


7、利用chrome性能工具


Chrome浏览器的Lighthouse扩展程序可以对网站进行测试并生成一个性能报告。Lighthouse生成的报告包含了网站性能、可访问性、最佳实践和SEO等方面的评估结果,以及优化建议。分析测试结果,找出需要改进的方面,并根据建议进行优化。


总之,前端优化是提高用户体验、提高网站性能、减少成本和支持更多设备的关键因素之一。上述优化方法可以帮助开发人员优化应用程序的性能,提高用户体验和满意度,从而提高留存率和转化率,增加收入。


作者:liupl
来源:juejin.cn/post/7219241334926180410
收起阅读 »

项目很大,得忍一下

web
背景 常和我们的客户端厮混,也经常陪他们发版,每次发版编译打包都可以在那边玩一局游戏了。一边幸灾乐祸,一边庆幸h5编译还好挺快的,直到我们的项目也发展成了*山,巨石项目。由于线上要给用户查看历史的推广活动,所以很多老的业务项目都还是留在项目中,导致我们的rou...
继续阅读 »

背景


常和我们的客户端厮混,也经常陪他们发版,每次发版编译打包都可以在那边玩一局游戏了。一边幸灾乐祸,一边庆幸h5编译还好挺快的,直到我们的项目也发展成了*山,巨石项目。由于线上要给用户查看历史的推广活动,所以很多老的业务项目都还是留在项目中,导致我们的router层爆炸,打包速度直线下降,开发过程中,开了hmr稍微有点改动也要等个几秒钟,恨不得立刻重启一个新项目。但是现实告诉你,忍住,别吐,后面还有更多的业务活动加进来。那么怎么解决这个问题呢,这个时候mp的思路是个不错的选择。


关键点


打包慢,本质原因是依赖庞大,组件过多。开发过程中,我们开新的业务组件时,往往和其他业务组件是隔离的,那么我们打包的时候是不是可以把那些不相干的业务组件隔离出去,当然可以。打包工具,从入口开始进行扫描,单页面的模块引入基本都是借助router,所以,关键的是如果我们能够控制router的数量,其实就能够控制编译和打包规模了。


问题


router在vue项目中我们常用的是全家桶的库vue-router,vue-router最多提供了懒加载,动态引入功能并不支持。有小伙伴说router的引入路径可不可以动态传入,我只能说小伙子你很机智,但是vue-router并不支持动态的引入路径。因此我们换个思路,就是在入口的位置控制router的规模,通过不同规模的router实例来实现router的动态引入。当然这需要我们对router库进行一定改造,使其变的灵活易用


一般的router


通常的router如下:



// router.js

/*global require*/

const Vue = require('vue')

const Router = require('vue-router')

Vue.use(Router)

const routes = [

{

path: '/routermap',

component: (resolve) => require(['../containers/map.vue'], resolve),

name: 'routermap',

desc: '路由列表'

},

{

path: '/',

component: (resolve) => require(['../containers/index.vue'], resolve)

},

{

path: '*',

component: (resolve) => require(['../containers/nofound.vue'], resolve),

name: 'defaultPage',

desc: '默认页'

}

]

const router = new Router({

mode: 'history',

routes

})

router.afterEach((to, from) => {

///

})

export default router

// 引入 entry.js

import router from './router.js'

router.beforeEach((to, from, next) => {

///

next()

})

router.afterEach(function(to, from) {

///

})

new Vue({

el: '#app',

template: '<App/>',

router,

})


我们可以不断的往routes数组中添加新的router item来添加新的业务组件,这也是我们的项目不断变大的根本,这样既不好维护,也会导致后面的编译效率


易于维护和管理的router


其实好的管理和维护本质就是分门别类,把类似功能的放在一起,而不是一锅粥都放在一起,这样基本就能解决追踪维护的功能,对应router管理其实也不是很复杂,多建几个文件夹就行如下:


router.png


对应routes/index.js代码如下:



import testRouter from './test.js'

const routes = [

{

path: '/map',

component: (resolve) => require(['../containers/map.vue'], resolve),

name: 'map',

desc: '路由列表'

},

{

path: '/',

component: (resolve) => require(['../containers/index.vue'], resolve)

},

...testRouter,

// 可以扩展其他router

{

path: '*',

component: (resolve) => require(['../containers/nofound.vue'], resolve),

name: 'defaultPage',

desc: '默认页'

}

]

// test.js

/**

* 测试相关页面路由映射

*/


/*global require*/

export default [

{

path: '/test/tools',

name: 'testTools',

component: resolve => require(['@test/tools/index.vue'], resolve),

desc: '测试工具'

}

]


我们通过把router分为几个类别的js,然后在通过router item的数组展开合并,就做到了分门别类,虽然看似简单,但是可以把管理和维护效果提升几个数量级。


支持mp的router


虽然上面支持了易于管理和维护,但是实际上我们如果只是单一入口的话,导出的还是一个巨大的router。那么如何支持多入口呢,其实也不用想的过于复杂,我们让类似test.js的router文件既支持router item的数组导出,也支持类似routes/index.js一样的router实例导出即可。所谓既能分也能合才是最灵活的,这里我们可以利用工厂模式做一个factory.js,如下:



/**

* app 内的页面路由映射

*/


/*global require*/

const Vue = require('vue')

const Router = require('vue-router')

Vue.use(Router)

const RouterFactory = (routes) => {

return new Router({

mode: 'history',

routes: [

{

path: '/map',

component: (resolve) => require(['../containers/map.vue'], resolve),

name: 'map',

desc: '路由列表'

},

{

path: '/',

component: (resolve) => require(['../containers/index.vue'], resolve)

},

...routes,

{

path: '*',

component: (resolve) => require(['../containers/nofound.vue'], resolve),

name: 'defaultPage',

desc: '默认页'

}

]

})

}

export default RouterFactory


这个factory.js产出的router实例和routes/index.js一模一样所以我们只需组装一下test.js即可,如下:



/*global require*/

import RouterFactory from './factory'

export const testRouter = [

{

path: '/test/tools',

name: 'testTools',

component: resolve => require(['@test/tools/index.vue'], resolve),

desc: '测试工具'

}

]

export default RouterFactory(developRouter)

// routes/index.js的引入变化一下即可

import testRouter from './test.js'

// 修改为=》

import { testRouter } from './test.js'


那么我们的入口该如何修改呢?也很简单:



// testEntry.js

import router from './routes/test.js'

router.beforeEach((to, from, next) => {

///

next()

})

router.afterEach(function(to, from) {

///

})

new Vue({

el: '#app',

template: '<App/>',

router,

})


我们建立了一个新的入口文件 testEntry.js 这个入口只引入了test相关的模块组件


如何灵活的和编译命令做配合呢


根据上面,我们进行mp改造的基础已经做好,关于如何多入口编译webpack或者其他打包里面都是基础知识,这里就不多赘述。这里主要聊一下如何灵活的配合命令做编译和部署。


既然router我们都可以分为不同的文件,编译文件我们同样可以拆分为不同的文件,这也使得我们的命令可以灵活多变,这里我们以webpack做为示例:


config.png


config1.png


config2.png


config3.png


根据上图示例 我们的webpack的配置文件仅仅改动了entry,我们稍微改造一下build.js,使其能够接受不同的编译命令:



// build.js

let page = 'all'

if (process.argv[2]) {

page = process.argv[2]

}

let configMap = {

'all': require('./webpack.prod.conf'),

'app': require('./webpack.app.conf')

}

let webpackConfig = configMap[page]

// dev-server.js

let page = 'all'

if (process.argv[2]) {

page = process.argv[2]

}

let configMap = {

'all': require('./webpack.dev.conf'),

'app': require('./webpack.app.dev.conf')

}

let webpackConfig = configMap[page]


对应的脚本配置:



// package.json

"scripts": {

"dev": "node build/dev-server.js",

"build": "node build/build.js",

"build:app": "node build/build.js app",

"dev:app": "node build/dev-server.js app"

},


以上app对应test。最后,我们只需要在命令行执行相应命令,即可实现我们可控的router规模的开发,基本随便来新的需求,咱都可以夜夜做新郎,怎么搞都是飞速。当然部署的话我们也可以单独执行一部分页面的部署命令到单独的域名,换个思路也可以作为一种预发测试的部署方法。



#
整体项目的开发编译

npm run dev

#
单独的app,也即test项目的开发编译

npm run dev:app

#
整体项目的部署

npm run build

#
单独的app,也即test项目的部署

npm run build:app


结语


以上,即如何利用mp思路,提高我们的编译开发效率。时常有人会在提高网页性能的时候说到mp,但mp本质上并不能提高页面的性能,比如白屏优化。而路由中使用懒加载其实才是提高部分网页性能的出力者,关于白屏优化,本篇文章不作展开讨论。


作者:CodePlayer
来源:juejin.cn/post/7218866717739696183
收起阅读 »

Android 获取短信验证码并自动填充

Android 获取短信验证码并自动填充(踩坑小米、荣耀、OPPO) 前言 最近弄了个短信自动填充功能,一开始觉得很简单,不就是动态注册个广播接收器去监听短信消息不就可以了吗?结果没这么简单,问题就出在机型的适配上。小米的短信权限、荣耀、OPPO的短信监听都是...
继续阅读 »

Android 获取短信验证码并自动填充(踩坑小米、荣耀、OPPO)


前言


最近弄了个短信自动填充功能,一开始觉得很简单,不就是动态注册个广播接收器去监听短信消息不就可以了吗?结果没这么简单,问题就出在机型的适配上。小米的短信权限、荣耀、OPPO的短信监听都是坑,暂时就用这三个手机测了,其他的遇到了再补充。


下面简单讲讲:


权限


申请权限


短信属于隐私权限,Android 6.0后需要动态申请权限。首先在manifest里面注册权限:


<uses-permission android:name="android.permission.RECEIVE_SMS" />
<uses-permission android:name="android.permission.READ_SMS" />

在需要用的地方,动态申请下:


String[] smsPermission = {Manifest.permission.READ_SMS, Manifest.permission.RECEIVE_SMS};

小米短信权限问题


本来这样权限问题就搞定了,但是在小米手机上就不行。小米手机会把短信归类到通知类权限里:
pic


在 ContextCompat.checkSelfPermission 的时候会直接返回true,并且不会弹出权限对话框,而是在实际使用的时候才会咨询用户,按理说好像和我们逻辑没有冲突,但是在使用receiver进行监听前,不是得确保有权限么?实际效果也是,在没有权限时,不能获取到短信的广播。


小米短信权限解决


在网上找了找办法,好像也没多少博文,但是大致有了思路:不是用的时候才申请么?那我就先用一下,再去用receiver监听。下面是方法:


// 读取一下试试,能读取到就有权限
boolean flag = false;
try {
Uri uri = Uri.parse("content://sms/inbox");
ContentResolver cr = context.getContentResolver();
String[] projection = new String[]{"_id"};
Cursor cur = cr.query(uri, projection, null, null, "date desc");
if (null != cur) {
cur.close();
}
lag = true;
}catch (Exception e) {
e.printStackTrace();
}

这里仅针对小米手机啊,对小米手机的判断我只是用 android.os.Build.MANUFACTURER 简单判断了下,如果有更高要求请查找资料。


使用Receiver进行监听


编写SmsReceiver


这里也是网上随便找了个代码,能用,但是在荣耀手机上却是偶尔能收到一次,后面几次就收不到了,打了log也没进入到onReceive中,这就很离奇了,排查了很久。同样的代码,在小米手机上是没问题的,那就只可能是适配问题了。


import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.telephony.SmsMessage;
import android.util.Log;

public class SmsReceiver extends BroadcastReceiver {

@Override
public void onReceive(Context context, Intent intent) {
//Toast.makeText(context, "收到信息", Toast.LENGTH_LONG).show();
Log.d("SmsReceiver", "onReceive: " + intent.getAction());
if(intent.getAction().equals("android.provider.Telephony.SMS_RECEIVED")){
//intent.getExtras()方法就是从过滤后的意图中获取携带的数据,
// 这里携带的是以“pdus”为key、短信内容为value的键值对
// android设备接收到的SMS是pdu形式的
Bundle bundle = intent.getExtras();
SmsMessage msg;
if (null != bundle){
//生成一个数组,将短信内容赋值进去
Object[] smsObg = (Object[]) bundle.get("pdus");
//遍历pdus数组,将每一次访问得到的数据方法object中
for (Object object:smsObg){
//获取短信
msg = SmsMessage.createFromPdu((byte[])object);
//获取短信内容
String content = msg.getDisplayMessageBody();
Log.d("SmsReceiver", "onReceive: content = " + content);
//获取短信发送方地址
String from = msg.getOriginatingAddress();
Log.d("SmsReceiver", "onReceive: from = " + from);

// TODO ...
}
}
}
}
}

使用方法:


// 使用广播进行监听
IntentFilter smsFilter = new IntentFilter();
smsFilter.addAction("android.provider.Telephony.SMS_RECEIVED");
smsFilter.addAction("android.provider.Telephony.SMS_DELIVER");
if (smsReceiver == null) {
smsReceiver = new SmsReceiver();
}
smsReceiver.setCallbackContext(callbackContext);
context.registerReceiver(smsReceiver, smsFilter);

接触监听,最好在收到短信的时候就取消注册广播:


context.unregisterReceiver(smsReceiver);

解决OPPO手机无法接收短信广播问题


本来小米荣耀都搞定了,给测试一测,结果又不行了。收不到广播,用下面的ContentObserver还总拿不到对的数据。找了下资料,发现OPPO手机需要在短信APP进行设置。


ps. 后面发现华为、荣耀都是这样,会对验证码进行保护。可以使用ContentObserver 监听,能触发onChange,但是拿不到Uri,不过可以使用查询,拿到倒叙的第一条数据,取出其中的date属性,比对监听时的时间,如果短信两分钟有效,那就看看第一条数据是不是在两分钟内,如果不是,那就是没拿到,问题就出在用户开启了短信验证码保护,可以提示用户自行输入验证码(毕竟这个不是我们的锅)。


解决方法:
在短信 -> 短信设置里面 -> 打开禁止后台应用读取验证码


解决荣耀无法连续监听短信的问题


既然上面的方法没用了,只能找新的办法喽,网上很多提供了两种办法,第二种就是通过ContentResolver去监听短信添加的更新动作,其实也和广播类似,代码如下:


import android.content.Context;
import android.database.ContentObserver;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Handler;
import android.provider.Telephony;
import android.util.Log;

import androidx.annotation.RequiresApi;

@RequiresApi(api = Build.VERSION_CODES.KITKAT)
public class ReadSmsObserver extends ContentObserver {

private final Context context;

public ReadSmsObserver(Handler handler, Context context) {
super(handler);
this.context = context;
}

private static final String SMS_INBOX_URI = "content://sms/inbox";//API level>=23,可直接使用Telephony.Sms.Inbox.CONTENT_URI,用于获取cusor
// private static final String SMS_URI = "content://sms";//API level>=23,可直接使用Telephony.Sms.CONTENT_URI,用于注册内容观察者
private static final String[] PROJECTION = new String[]{
Telephony.Sms._ID,
Telephony.Sms.ADDRESS,
Telephony.Sms.BODY,
Telephony.Sms.DATE
};

@Override
public void onChange(boolean selfChange, Uri uri) {
super.onChange(selfChange);
Log.d("ReadSmsObserver", "onChange: ");
// 当收到短信时调用一次,当短信显示到屏幕上时又调用一次,所以需要return掉一次调用
if(uri.toString().equals("content://sms/raw")){
return;
}
// 读取短信收件箱,只读取未读短信,即read=0,并按照默认排序
Cursor cursor = context.getContentResolver().query(Uri.parse(SMS_INBOX_URI), PROJECTION,
Telephony.Sms.READ + "=?", new String[]{"0"}, Telephony.Sms.Inbox.DEFAULT_SORT_ORDER);
if (cursor == null) return;
// 获取倒序的第一条短信
if (cursor.moveToFirst()) {
// 读取短信发送人
String address = cursor.getString(cursor.getColumnIndex(Telephony.Sms.ADDRESS));
Log.d("ReadSmsObserver", "onChange: address = " + address);
// 读取短息内容
String smsBody = cursor.getString(cursor.getColumnIndex(Telephony.Sms.BODY));
Log.d("ReadSmsObserver", "onChange: smsBody = " + smsBody);

// TODO 传递出去,最好切下线程

}
// 关闭cursor的方法
cursor.close();
}
}

用的时候要注册和取消注册:


// 使用ContentResolver进行监听
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
if (smsObserver == null) {
smsObserver = new ReadSmsObserver(new SmsHandler(), context);
}
smsObserver.setCallbackContext(callbackContext);
context.getContentResolver().registerContentObserver(
Uri.parse("content://sms/"), true, smsObserver);
}

取消注册:


context.getContentResolver().unregisterContentObserver(smsObserver);

解决OPPO手机无法拿到最新短信问题


很神奇啊,每次使用ContentObserver去监听短信变化,明明onChange触发了,但是去拿短信就是拿不到最新的,开了上面的设置也不行,弄了好久。


最后想的解决办法是,两种方式同时监听,在onChange触发后等待三秒钟(开始试了1s还不行),看看有没有onReceive,如果有就直接使用onReceive的短信,如果没有再验证onChange内拿到的短信,看看是不是有效时间内的,连倒叙第一个都在有效时间外,那就是没拿到了,直接舍弃了。


PS. 后续更新,感觉这些问题都可能是手机系统开了短信验证码保护。


思路是这样,做起来不麻烦,用个handler就可以解决,读者自行处理吧。


结语


这些机型的兼容性搞起来真头疼,上面两种方法可以兼容起来使用,收到一条短信后直接取消注册就行了。


作者:方大可
来源:juejin.cn/post/7222897518501003319
收起阅读 »

周末闲来无事,做了一个能动的宣传页

web
最近在用可画(canva),制作一些素材,海报活动页面,这不情人节快到了吗?基于海报模版,设计自己的页面倒是简单,但是都是静态页面,想着能不能让页面的元素都动起来(Everybody跟我一起嗨嗨嗨!!)。 两个方案 纯CSS animate库 CSS基于ani...
继续阅读 »

创建项目

最近在用可画(canva),制作一些素材,海报活动页面,这不情人节快到了吗?基于海报模版,设计自己的页面倒是简单,但是都是静态页面,想着能不能让页面的元素都动起来(Everybody跟我一起嗨嗨嗨!!)。


两个方案


纯CSS animate库


CSS基于animate库



  1. 利用animate动效,给页面上所有的image和text元素加上className,借助--var全局css变量属性,给元素依次加上delay、duration、index序号、初始化信息rotate、offset、easing等等,我会在码上掘金给一个css的demo版本。CSS版本相对简单一些,只需要循环给所有元素加上对应动画,计算执行时间,延迟时间,页面就可以动起来了。


// 定义的数据结构 Image\Text
[{
"id": "Image/Text-xx",
"type": "Image/Text",
"name": "图片/文本",
"css": {
"top": 0,
"left": 0,
"width": 414,
"height": 736,
"zIndex": 1,
"opacity": 1,
"fontSize": 18,
},
"animationObj": {
{
"delay": 1000,
"duration": 3030,
"type": "flipInY",
"easing": '',
"index": 8,
"rotate_angle": -6.6,
"offset": -112.5,
}
},
"value": "文本内容",
"src": "https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/935920813a0c4151bbf452ef3c53ab7f~tplv-k3u1fbpfcp-watermark.image"
}]


码上掘金-CSS版


下面是纯css的版本:
code.juejin.cn/pen/7123482…


JS animejs库


animejs库


使用JS的关键就是编写对应帧属性,通过时间轴timeline方法给元素加上动画。现在js版本还只是一个demo中的demo,下次再给jym,感兴趣的jy可以自己想想。


时间轴可让你将多个动画同步在一起。
默认情况下,添加到时间轴的每个动画都会在上一个动画结束时开始。

<div class="demo-content params-inheritance-demo">
<div class="line">
<div class="square shadow"></div>
<div class="square el" style="transform: translateX(0px) scale(1); opacity: 0.5;"></div>
</div>
<div class="line">
<div class="circle shadow"></div>
<div class="circle el" style="transform: translateX(7.22878e-10px) scale(1); opacity: 0.5;"></div>
</div>
<div class="line">
<div class="triangle shadow"></div>
<div class="triangle el" style="transform: translateX(2.30924px) scale(1.00924) rotate(180deg); opacity: 0.5;"></div>
</div>
</div>

<script src="https://lib.baomitu.com/animejs/3.2.1/anime.min.js"></script>


.demo-content {
position: relative;
display: flex;
flex-direction: column;
justify-content: center;
text-align: center;
width: 290px;
height: 100%;
}
.line {
width: 100%;
padding: 1px 0px;
}
.square,
.circle {
pointer-events: none;
position: relative;
width: 28px;
height: 28px;
margin: 1px;
background-color: #005bb7;
font-size: 14px;
}
.triangle {
pointer-events: none;
position: relative;
width: 0;
height: 0;
border-style: solid;
border-width: 0 14px 24px 14px;
border-color: transparent transparent #005bb7 transparent;
}
.shadow {
position: absolute;
opacity: .2;
}

var tl = anime.timeline({
targets: '.params-inheritance-demo .el',
delay: function(el, i) { return i * 200 },
duration: 500,
easing: 'easeOutExpo',
direction: 'alternate',
loop: true
});

tl
.add({
translateX: 250,
// override the easing parameter
easing: 'spring',
})
.add({
opacity: .5,
scale: 2
})
.add({
// override the targets parameter
targets: '.params-inheritance-demo .el.triangle',
rotate: 180
})
.add({
translateX: 0,
scale: 1
});

code.juejin.cn/pen/7123478…


码上掘金太卡了吧,能不能优化下


作者:一起重学前端
来源:juejin.cn/post/7123482707983613965
收起阅读 »

本地运行的前端代码,如何让他人访问

web
有时候,我前端写好了项目,想要给其他人看一下效果,可以选择将代码部署到test环境,也可以选择让外部通过ip来访问,不过前提是在同一个局域网下(比如连接同一个WiFi),下面介绍第二种方式。 修改dev命令 首先我们需要先修改host地址,此处以vue3项目举...
继续阅读 »

有时候,我前端写好了项目,想要给其他人看一下效果,可以选择将代码部署到test环境,也可以选择让外部通过ip来访问,不过前提是在同一个局域网下(比如连接同一个WiFi),下面介绍第二种方式。


修改dev命令


首先我们需要先修改host地址,此处以vue3项目举例


image.png


页面启动之后如下


image.png


正常情况下,script下的dev命令是不会指定host的,我们可以在下面看到Local的地址为默认的127.0.0.1,此时把这个网址发给别人肯定跑不起来。


所以我们可以指定host,比如0.0.0.0,允许所有ip访问


"dev": "vite --host=0.0.0.0",

修改完host后,windows系统的话,我们还需要关闭防火墙(苹果不需要)。重新启动项目可以看到


QQ截图20230406204123(1)(1).png


Network那里的网址,打马赛克的地方其实就是本机的ip地址,window输入cmd打开命令提示符,然后输入ipconfig即可查到ip地址,苹果的话,点击wifi小图标,同时按住option键即可查到ip地址。


在其他电脑或者手机访问


浏览器中输入url即可看到相关页面,此方法也适用于手机端调试


Screenshot_2023-04-06-20-51-03-21_439a3fec0400f89.jpg


作者:笨笨狗吞噬者
来源:juejin.cn/post/7218916720323706935
收起阅读 »

知道尤雨溪为什么要放弃 $ 语法糖提案么?

web
前言 最近看到一篇文章: 《最新,Vue 中的响应性语法糖已废弃》 本文标题中的 $ 语法糖指的就是上文中的响应式语法糖 (Reactivity Transform),那为什么不写 Reactivity Transform 呢?因为这个名实在是太长了… 看了一...
继续阅读 »

前言


最近看到一篇文章:


《最新,Vue 中的响应性语法糖已废弃》


本文标题中的 $ 语法糖指的就是上文中的响应式语法糖 (Reactivity Transform),那为什么不写 Reactivity Transform 呢?因为这个名实在是太长了…


看了一圈评论发现大家觉得被废弃是因为分不清是正常变量还是响应式变量的居多:



下面这个评论说的有一定道理:



Vue 的官网现在已经变成这样了:



以后会不会变成这样:



23次方,一共8种不一样的写法。不对,无虚拟 DOM 模式只能用 Composition API,所以应该不到 8 种写法,你看这不就分裂了嘛!虽说这几种不同的写法也能看懂吧,但每个人都有不同的偏好不同的写法总归不太好。而且你能保证 Vue 不会又改写法吗?Vue 总是受人启发:受 Angular 启发的双向绑定、受 React 启发的虚拟 DOM、受 React Hooks 启发的 Composition API、受 Svelte 启发的语法糖(一开始用的是 Svelte 的 label 写法)、受 Solid 启发的 Vapor Mode无虚拟 DOM 模式




  • 高情商:集百家之长

  • 低情商:方案整合商




开玩笑的哈~ Vue 还是有很多自己的东西的,不过它确实老是抄袭各种框架受各种框架的启发,太杂糅了。今天受这个框架启发做出来这种新 feature、明天又受那个框架启发做出来了另一种新 feature… 估计等 Vue4 出来的时候肯定又是受到了什么其他框架的启发…


我在《无虚拟 DOM 版 Vue 即将到来》这篇文章下看到这样一条评论:



大家觉得这个人说的有没有道理呢?反正我现在感觉 Vue 的各个方案有点太杂糅了,有点像是方案整合商集百家之长,以后指不定就发展成这样了:



当你去网上搜索一些解决方案时,能看到数十种不同的写法是一种什么体验……


不过这条评论真的是高情商:





  • 低情商:Vue 这是啥流行抄啥

  • 高情商:只用 Vue 就能体会到各种流行的技术趋势




跑题了,咱们来说一说 $ 语法糖,它可绝不只有分不清到底是不是响应式变量这一个缺点,它的缺点比优点多得多,我们来具体分析一下。


分析


我们也不要一上来就说这个语法糖有多么多么的不好,如果真这么不好的话尤总也不至于费这么大劲来推动这个提案了对不?这个语法糖在某些情况下确实会大幅改善我们的开发体验,但在另一些情况下不仅不会帮助我们改善体验,反而会增加我们的心智负担,我们来看下面这个案例:


let x = $(0)
let y = $(0)

const update = e => {
 x = e.x
 y = e.y
}
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))

$watch([x, y], ([x, y]) => console.log(x, y))

看上去很美好是不是,我们终于不用再写 .value 了。



如果看不明白这种写法的话可能是之前没有对其进行过了解


建议先阅读一下这篇《Vue3又出新语法 到底何时才能折腾完?》



不过像这种逻辑我们通常都会提取出去封装成一个函数,因为有可能有很多个组件都用到了获取鼠标位置这个逻辑,你不想在每个用到该逻辑的组件里都复制一遍相同的逻辑吧?那我们就这样:


// useMouse.js
export const useMouse = (dom = window) => {
  let x = $(0)
  let y = $(0)

  const update = e => {
    x = e.x
    y = e.y
}
  onMounted(() => dom.addEventListener('mousemove', update))
  onUnmounted(() => dom.removeEventListener('mousemove', update))

  return { x, y }
}

import { useMouse } from './useMouse.js'

let { x, y } = useMouse()

$watch([x, y], ([x, y]) => console.log(x, y))

如果这么写你就会惊讶的发现根本不生效,因为编译过后就相当于:


import { ref } from 'vue'

export const useMouse = (dom = window) => {
 let x = ref(0)
 let y = ref(0)

 const update = e => {
   x.value = e.x
   y.value = e.y
}
 onMounted(() => dom.addEventListener('mousemove', update))
 onUnmounted(() => dom.removeEventListener('mousemove', update))

 return {
   x: x.value,
   y: y.value
}
}

这就相当于把一个普通值给 return 出去了,普通值是没法在取值或改值时运行一些其他逻辑的,所以我们还不能把值直接 return 出去,而是把这个响应式变量本身给 return 出去:


import { ref } from 'vue'

export const useMouse = (dom = window) => {
 let x = ref(0)
  let y = ref(0)

  const update = e => {
   x.value = e.x
    y.value = e.y
}
  onMounted(() => dom.addEventListener('mousemove', update))
  onUnmounted(() => dom.removeEventListener('mousemove', update))

  return { x, y }
}

所以编译必须还要有还原的功能,把响应式的值给还原成响应式变量:


export const useMouse = (dom = window) => {
 let x = $(0)
 let y = $(0)

 const update = e => {
   x = e.x
   y = e.y
}
 onMounted(() => dom.addEventListener('mousemove', update))
 onUnmounted(() => dom.removeEventListener('mousemove', update))

 return $$({ x, y })
}

但这样又要写 .value 了:


import { useMouse } from './useMouse.js'

let { x, y } = useMouse()

console.log(x.value, y.value)

因为编译器是分析不出来一个函数的返回值到底是不是响应式变量的,所以就又得引入一个 API 来告诉编译器这个函数的返回值有响应式变量:


import { useMouse } from './useMouse.js'

let { x, y } = $fromRefs(useMouse())

console.log(x, y)

大家不觉得这样很麻烦吗?而且搞出那么多莫名其妙的 $ 、$$ 变量。写一堆这玩意真的没感觉比 .value 好到哪去,而且我们还要随时记得某个变量是响应式的,不然在传递的过程中就有可能失去响应性:


// logValue.js
// 接收一个响应式变量并在其变化时将其打印出来

export const logValue = arg => { // 在提案中并未找到如何用语法糖转换函数的参数
 // 也就是说在这种情况下可能没有什么完美的解决方案 那就又要写 .value 了:
 console.log(arg.value)
 // 不过也不是没有解决方案 我们可以用 $computed 来关联一下:
 let argument = $computed(() => arg.value)
 // 这样就可以不用写 .value 了:
 console.log(argument)
 // 但缺点就是太麻烦了 参数少的时候还可以 参数多的时候还能每个都这么写吗?
 // 而且还要为变量取个不同的名字 这对于我们这些英文不好的人来说简直就是场灾难
 $watch(argument, value => console.log(value))
}

import { logValue } from './logValue.js'

let a = $(0)

logValue(a) // 这么传就错啦
logValue($$(a)) // 一定要写成这样

// 假如有函数是需要响应式变量和普通变量混着传的:
let b = 0
logValue($$(a), b, { a: $$(a), b }) // 写成这样真的很乱

还有需要把 ref 变量传给 reactive 字段的情况:


let a = $(0)

const obj = reactive({ a })

console.log(obj.a) // 0
a++
console.log(obj.a) // 还是 0


// 必须写成这样
const obj = reactive({ a: $$(a) })
console.log(obj.a) // 0
a++
console.log(obj.a) // 1

所以说语法糖只能某些情况下改善我们的开发体验,前提就是你不要把响应式变量传来传去的。但 Vue3 的核心卖点之一不就是 Composition API 么?中文官网管这个叫组合式 API,关键词是组合Vue 还把提取出去的可复用函数叫 Composables,翻译过来就是可组合的,如果不把响应式变量传来传去那还组合个P呀!


这个问题可不是只有 Vue 有,来看下 Solid.js 吧:


import { createSignal } from 'solid'

export const useMouse = (dom = window) => {
 const [x, setX] = createSignal(0)
 const [y, setY] = createSignal(0)

 dom.addEventListener('mousemove', ({ x, y }) => {
   setX(x)
   setY(y)
})

 return {
   x: x(),
   y: y()
}
}

同样会有响应式值与响应式变量的问题,只不过就是把 .value 变成了 ()


// 假如有个响应式变量 a

// 打印的是响应式值
console.log(a.value) // Vue
console.log(a()) // Solid

//打印的是响应式变量
console.log(a) // Vue & Solid

是不是看过很多文章说 Solid.js 和 React Hooks 很像、写起来很舒服、什么比 React 还 react 之类的文章?实际上真的就只是 API 设计的相似而已,只要我们想,我们同样也可以把 Vue 的 API 封装成 React 那样:


import { ref } from 'vue'

const useState = value => {
 const result = ref(value)
 const getter = () => result.value
 const setter = newValue => result.value = newValue
 return [getter, setter]
}

const [num, setNum] = useState(0)
setNum(1)

那是不是这样封装一下,Vue 也变得比 React 还 react 了?应该不难看出这只是在自欺欺人罢了,我们传值时照样还得区分到底应该传的是响应式变量本身还是响应式变量的值。


Vue2 为何没这个问题


不知大家有没有思考过:为什么 Vue2 时代大家从来就没听说过丢失响应性、没听过要出什么语法糖之类的问题呢?听过最多有关于语法糖的可能就是 v-model 的双向绑定功能其实就是 @input="xxx" + :value="xxx" 的语法糖。


这是因为 Vue2 时代用的都是 this.xxx,咱们所有的响应式变量全都挂载到了 this 上。取值时 this.xxx 会触发 getter、改值时 this.xxx = xxx 会触发 setter


你可以简单的理解成这样:


// 用 Vue3 来写一段伪代码
import { reactive, watchEffect } from 'vue'

const this = reactive({
a: 1,
b: 2,
c: 3
})

watchEffect(() => console.log(this.a))
this.a++

当然这只是一段伪代码,真这么写是会报错的:



因为 this 是一个关键字,正因为它是一个关键字所以咱们用 this.xxx 才会显得这么的自然。而我们现在的响应式变量都需要自己起名,自己起的名不是关键字,所以用 xx.xxx 就老觉得麻烦,就老想给它解构:


import { reactive, watchEffect, toRefs } from 'vue'

const user = reactive({
name: 'AngularBaby',
age: 34,
beautiful: true
})

console.log(user.name) // 有些人觉得这样写很麻烦
const { name } = user // 就老想给它解构
console.log(name) // 结果就是失去了响应性

// 想要保持响应性 写法就变得更麻烦了
const { name } = toRefs(user)
console.log(name.value)

而且之前用 this 还有一个显著的好处就是只要写法正确,操作 this 上的属性就不用担心响应式的问题,没有那么多心智负担。甚至有人会简单的理解为只要是 this.xxx 就一定会有响应:


export default {
data () {
return { a: 1 }
},
mounted () {
this.a = 2 // 没有心智负担 因为我们知道自己是在改变 this 上的属性
this.a++ // 正确改变 this 上的属性就会存在响应

let b = 2 // 也没有心智负担 因为我们知道这不是 this 上的属性
b++ // 我们不会期待这段代码会有任何的响应
}
}

这样很容易区分哪些是响应式变量而哪些不是,即使有人真的写成了这样:


export default {
data () {
return { a: 1 }
},
mounted () {
let { a } = this
a++ // 我们不会期待这段代码会有任何的响应
}
}

这里也很容易能够看出来我们这样并没有修改 this 上的属性,所以并不会正确响应也是理所应当的一件事。


还有复用逻辑,Vue2 时代有很多人用 Mixins 来复用逻辑:


import mouse from 'mouse.mixin.js'
import position from 'position.mixin.js'

export default {
mixins: [mouse, position],
mounted () {
this.x // 哪来的 x ?
this.y // 哪来的 y ?
// 除了 xy 还有没有其他的未知 this.xxx ?
}
}

可以看到 Mixins 存在很多的弊端,比方说数据来源不清晰、容易产生冲突变量之类的。如果不去看源码的话谁能知道 this.x 到底是 mouse 中的 x 还是 position 的 x 呢?正是由于 Vue2 没有一个完美的复用机制,所以尤大才下定决心将 Vue3 改造成函数式。但函数式没了 this 就又失去了 Vue2 时期的那种… 我不知该怎么形容 Vue2时期的 this.xxx 哈,舒服?自然?反正我是比较喜欢 this.xxx 这种写法的,虽然这种写法是受 Angular 启发(集百家之长)


而且我还比较喜欢的一点就是一些全局挂载的属性:


this.$el
this.$refs
this.$nextTick(() => { /* ... */ })

直接 this.$xxx 就出来了,不用引,既方便又快捷。当然这种方式也有不少坏处,比方说容易被覆盖、不利于 Tree Shaking 之类的…


但我还真的蛮喜欢这种写法的:


// main.js
import Vue from 'vue'

Vue.prototype.$toast = msg => { /* ... */ }

this.$toast('Success!')

如今就会变得就稍麻烦一些:


import toast from './toast.js'

toast('Success!')

虽说后者其实更好,但有没有这样一种可能:既恢复到 Vue2 时期用 this 的便捷、又能享受到 Vue3 组合式的好处:


// 幻想中的写法

this.$data.a = 1 // 相当于 Vue2 时期的 data: { a: 1 } 最终会挂载到 this 上变成 this.a
this.$computed.b = () => this.a * 2 // 相当于 Vue2 时期的 computed: { b () { return this.a * 2 } } 最终会挂载到 this 上变成 this.b

this.$watch.b = value => console.log(value) // 相当于 Vue2 时期的 watch: { b: value => console.log(value) }

let timer
this.$mounted = () => {
timer = setInterval(() => this.a++, 1000)
}
this.$unMounted = () => clearInterval(timer)

复用逻辑:


// 幻想中的写法

import useMouse from './useMouse.js'

({ x: this.$computed.x, y: this.$computed.y } = useMouse())
this.$effect = () => console.log(this.x, this.y)

// 如果用数组解构将会更加的便捷
[this.$computed.x, this.$computed.y] = useMouse()
this.$effect = () => console.log(this.x, this.y)

这样我们的心智负担就又能回到 this 时期了:只要改变 this 属性就会存在响应,否则就无响应,那这个方案有实现的可能吗?在 ES5 时代无可能,但在 ES6 Proxy 的加持下我认为还是可以实现的,那么接下来我们就来试一下。


实验


首先我们回顾一下 Vue3.0 没有 setup 语法糖时期的写法:


<template></template>

<script>
import { defineComponent } from 'vue'

export default defineComponent({
setup () {
console.log(this) // undefined
}
})
</script>

原版的 this 指向为 undefined,那我们怎么改变它的指向呢?我们可以自己写一个 defineComponent


// defineComponent.js

import { defineComponent, reactive } from 'vue'

export default options => {
const { setup } = options
if (typeof setup === 'function') {
options.setup = setup.bind(reactive({}))
}
return defineComponent(options)
}

这样 setup 的指向就变成了 reactive({}),当我们在操作 this 的时候就相当于在操作 reactive({})。但这样并不能满足我们的需求,我们想要的是当我们 this.$data.a 的时候会在 this 上挂载个 a 属性,所以我们要把 reactive 换成一个 Proxy


// createThis.js
import { defineComponent, reactive } from 'vue'

const createData = target => new Proxy({}, {
get: (_, key) => Reflect.get(target, key),
set (_, key, value) {
if (Reflect.getOwnPropertyDescriptor(target, key)) {
console.error(`this.$data.${key} is already defined!`)
return false
}
return Reflect.set(target, key, value)
}
})

export default () => {
const that = reactive({})
const $data = createData(that)
return new Proxy(that, {
get (target, key) {
if (key === '$data') {
return $data
}
return Reflect.get(target, key)
},
set (target, key, value) {
if (key === '$data') {
return console.warn('this.$data is readonly!')
}
return Reflect.set(target, key, value)
}
})
}

// defineComponent.js

import { defineComponent } from 'vue'
import createThis from './createThis.js'

export default options => {
const { setup } = options
if (typeof setup === 'function') {
const that = createThis()
options.setup = (...args) => {
setup.apply(that, args)
return that
}
}
return defineComponent(options)
}

也就是说我们利用 Proxy 来把 $data 给代理出去了,当我们访问 $data 的时候其实已经是另一个代理对象了,在这个代理对象上设置的属性全部都设置到 this 上。this 现在就相当于 reactive({}),所以 this.$data.a = 1就相当于 reactive({ a: 1 }),我们来试一下:



完美运行,只要你能搞懂上面的那段代码,那么接下来的 $computed$watch$watchEffect$readonly$shallow$nextTick$mounted$unMounted 等一大堆 API 相信你也知道该怎么做了,我就不在这里占用过多的篇幅了。这里直接用码上掘金贴上源码及用法,向大家展示一下可行性:



当然这源码并不是把所有 API 都实现了,目前只实现了 this.$datathis.$computedthis.$watchthis.$mounted 等几个常用的 API 供大家参考,感兴趣的可以去把全部的 API 都实现一下,我这里犯懒就先不实现那么全乎了。



这么好的东西为啥犯懒不实现呢?因为这玩意有一定的弊端。对了,掘金好像在文章中屏蔽了来自码上掘金alert,必须点查看详情才能看到。为了防止大家也犯懒不点进去看,这里直接给大家贴上动图:



我们的写法类似于下面这样:


export default defineComponent({
setup () {
this.$data.count = 0
this.$watch.count = (value, oldValue) => alert(`验证 this.$watch:按钮上的值将会从 ${oldValue} 变为 ${value}`)

this.$computed.doubleCount = () => this.count * 2
this.$watch.doubleCount = value => alert(`验证 this.$computed:${this.count} 的双倍是 ${value}`

this.$mounted = () => alert('验证 this.$mounted:已挂载')
}
})

怎么样,是不是很好玩?我是蛮喜欢这种 this 混合着函数式的写法。但刚刚说了这玩意有一定的弊端,只能拿来当玩具玩玩所以我才懒得实现的那么全乎。那么它究竟有多大的弊端呢?


弊端


Vue3 比 Vue2 更优秀的一个点是支持 tree shaking,在你仅仅只用了 Vue 的某几项功能的情况下打包体积会小很多。但我们刚刚的做法无疑是开了历史的倒车,又回去了!并且随着 Vue3.2 的崛起,setup 语法糖得到了大多数人的认可,因为它确实很方便。但这样我们就无法修改 this 指向了:


<template>
<h1>{{ a }}</h1>
</template>

<script setup>
this.$data.a = 1 // 怎么修改 this 指向
</script>

有人可能会说加个函数不就得了:


<template>
<h1>{{ a }}</h1>
</template>

<script setup>
import setup from './setup.js'

setup(() => {
this.$data.a = 1
})
</script>

这样虽然可以修改 this 指向,但随之而来的就是 <template> 模板里面访问不到 a 这个变量了,除非我们写成这样:


<template>
<h1>{{ a }}</h1>
</template>

<script setup>
import { toRefs } from 'vue'
import setup from './setup.js'

const { a, b, c, d, e, f } = toRefs(setup(() => {
this.$data.a = 1
this.$data.b = 2
this.$data.c = 3
this.$data.d = 4
this.$data.e = 5
this.$data.f = 6
}))
</script>

我相信没人会愿意写成这样,所以我们必须借助 babel 插件来完成编译,思路是把 this 编译成 reactive({}),类似于下面这样:


// 编译前
this.$data.a = 1
this.$data.b = 2
this.$data.c = 3
this.$data.d = 4
this.$data.e = 5
this.$data.f = 6

// 编译后
import { reactive } from 'vue'
import createThis from 'createThis.js'
import createData from 'createData.js'

const that = createThis(reactive({}))
createData(that)

that.$data.a = 1
that.$data.b = 2
that.$data.c = 3
that.$data.d = 4
that.$data.e = 5
that.$data.f = 6

不过这样还是会引入我们刚刚写的那些代码,虽然代码量并不高,但如果压根就不引入任何额外的代码才好,所以如果能编译成这样才是最完美的:


// 编译前
this.$data.a = 1
this.$data.b = 2
this.$data.c = 3
this.$data.d = 4
this.$data.e = 5
this.$data.f = 6

console.log(this.a)

// 编译后
import { reactive } from 'vue'

const that = reactive({
a: 1,
b: 2,
c: 3,
d: 4,
e: 5,
f: 6
})

console.log(that.a)

但如果这样编译的话又有可能发生如下情况:


import useXxx from './useXxx'

this.$data.a = 1

useXxx.call(this)

这样会被编译成:


import { reactive } from 'vue'
import useXxx from './useXxx'

const that = reactive({ a: 1 })

useXxx.call(that)

万一这个 useXxx 里写了这样一段逻辑:


// useXxx.js

expurt default function () {
this.$watch.a = value => console.log(value)
}

这样就不会按照我们所期待方式去运行了,因为在编译后就相当于:


// 伪代码

const obj = reactive({ a: 1 })

useXxx.call(obj)

function useXxx () {
this.$watch.a = value => console.log(value)
}

这样会直接报错,因为 reactive({ a: 1 }).$watch 是 undefinedundefined.a 会报错,所以并没有特别完美的解决方案。最好是检测如果没把 this 作为参数传走或者没有哪个函数用了 fn.call(this) 来把 this 指向当前上下文的话,就按照最完美的方式(不引入任何杂七杂八的代码)编译。否则就引入一点运行时,反正也没多少:


// 编译前
import useMouse from 'useMouse'

this.$data.a = 1
this.$watch.a = value => console.log(value)

this.$mounted = () => window.addEventListener(...)
this.$unmounted = () => window.removeEventListener(...)

[this.$computed.x, this.$computed.y] = useMouse.call(this)

// 编译后
import { reactive } from 'vue'
import createThis from 'createThis'
import createData from 'createData'
import createWatch from 'createWatch'
import createMounted from 'createMounted'
import createUnmounted from 'createUnmounted'

const that = createThis(reactive({
a: 1
}))
createData(that)
createWatch(that)
createMounted(that)
createUnmounted(that)

that.$data.a = 1
that.$watch.a = value => console.log(value)

that.$mounted = () => window.addEventListener(...)
that.$unmounted = () => window.removeEventListener(...)

[that.$computed.x, that.$computed.y] = useMouse.call(that)

但仔细一想还是有可能有 bug,比方说你这个组件里没用到 this.$readonly,但 useMouse 用了的话,那岂不是又要报错。那就在 Vue 组件之外也编译,如果在外面有用到 this.$xxx,那就在相应的位置:


// 编译前
export default function useMouse () {
this.$readonly.a = 1
}

// 编译后
import createReadonly from 'createReadonly'

export default function useMouse () {
createReadonly(this)
this.$readonly.a = 1
}

缺陷


这种写法不仅仅是有弊端,还有一个非常严重的缺陷。虽然刚刚我们设想了一下用编译的方案来解决弊端的可能,但有个最大的缺陷是连编译都无法解决的。这个最大的缺陷就是对 TS 的支持,如果不用 TS 还好,但如果你的项目里有用 TS,那么这种写法就完全没法用:



不知怎么才能让 TS 也支持这种想法,查了国内外很多资料,最后找到了这两篇文章:



《TypeScript plugin 实践 —— 类型,编辑器与业务价值》


《基于 TypeScript 的开发者体验增强 - 朝夕相处却始终被忽视的领域》



也不知道这个 TS Language Service 有没有可能能够实现我们这种语法,感兴趣的小伙伴可以好好研究一下。我们目前只实现了运行时方案,但编译方案才是未来。写这篇文章的目的是希望给大家提供一个思路,看看大家觉得这个想法怎么样。万一大家觉得这个想法非常好,把它推给官方,官方实现了呢?



当然上述的那些话也可能仅仅只是过于美好的想象,现实很有可能是压根儿就没有人对这个想法感兴趣,官方也认为这是在开历史的倒车并且对 TS 支持不好不予实现。



往期精彩文章



作者:Veev
来源:juejin.cn/post/7222874734185922597
收起阅读 »

实战:快速实现iOS应用中集成即时通讯IM和UI

准备熟练objective-c语言有一台mac电脑,并安装了xcode 和 cocoapods目标手把手教大家在iOS应用中集成即时通讯IM 功能内容篇幅较长,需要内心平和耐心看下去,务必戒躁.阅读本文并按照本文进行对接预计时长2小时注册Appkey和user...
继续阅读 »

准备

熟练objective-c语言

有一台mac电脑,并安装了xcode 和 cocoapods


目标

手把手教大家在iOS应用中集成即时通讯IM 功能

内容篇幅较长,需要内心平和耐心看下去,务必戒躁.阅读本文并按照本文进行对接预计时长2小时


注册Appkey和username

本教程以集成环信IM为例

注册环信账号并登录到console后台:

https://console.easemob.com/user/register


第一步 点击添加应用:



第二步 创建应用


示例:


第三步,找到刚创建的appkey,并点击查看详情



第四步 创建两个user,并相互加好友







加好友:





至此,在console部分操作完成,并得到了一个appkey和两个user

appkey:1168171101115760#abc ,

username1: user1 ,

password1:1 ,

username2: user2 ,

password2: 1



创建一个简单项目simple,整合IM和UI

创建一个简单项目




工程创建成功之后,使用cocoapods进行环信IM集成

示例:



最终效果



第一部分 环信UI库集成方式

环信官方提供了UI库

那么我们有几种方式进行集成呢?

1.利用pod远端拉取集成

2.利用pod集成本地库

3.直接拖入项目


实际上这套UI库并不是最理想的UI库,因为在真正做项目的过程中,我们需要进行大量的改造,以达到符合产品设计的样式.所以这里推荐第二种方式和第三种方式.


第一种方式

参考:https://www.imgeek.net/video/76

第二种方式

参考:https://www.imgeek.org/video/91


我们将会在这里演示第三种方式


第二部分 集成前需要了解

问:集成UI库是否可以直接使用?

答:这里需要注意,实际上UI库仅仅提供了UI功能,并没有提供逻辑部分,所以无法直接拿来使用.


问:那我应该怎么使用?关于逻辑部分在哪里?还需要我自己进行实现吗?

答:不需要,我们可以从官方demo中取出我们需要的UI部分.


问:那么我需要以最快速度集成UI,都需要做什么工作?

答:

1.下载demo<https://www.easemob.com/download/im

跑通demo<https://www.imgeek.net/video/76>

2.根据产品设计的情况,从第一部分提到的三种方式中选择合适的集成方式.

3.从demo中取出相关界面逻辑并放入在自己的项目中.


第三部分 上手干

这里将会演示第三种集成方式

1.首先我们已经跑通了demo,所以我们当前demo的路径如下:




其中:

EaseIM是可运行的项目

EaseUI是官方提供的UI库


2.首先将UI部分拖入项目




把其中多出的plist删除

在podfile中加入


pod 'EMVoiceConvert', '0.1.0'

效果如下:




此时UI部分集成完成.


下一步集成逻辑部分,需要从Demo中提取



创建pch文件(如果项目中有则不用创建)


并在podfile中加入



创建pch文件(如果项目中有则不用创建)







最后配置项目权限(这里没有进行配置推送)






到这里,我们完成了将IM整合进项目中。


完善代码及IM的UI使用方式

第一步,我们在项目中创建一个负责做配置项的helper 和一个音视频做回调处理的类

类名分别为

EMAppConfig如下所示



EMAppCallHelper如下所示



第二步 在appdelegate中完善




宏定义appkey(定义已经存在,需要修改定义的值)



(另,声网id添加或修改位置在这里)






使用示例如下




实现收发消息

第一部分 参考现有Demo写法


1.发消息的逻辑参考



2.收消息的回调



第二部分,主动实现


如何主动发消息(这里发消息是直接调用SDK发消息)


//构建一个消息体
    EMTextMessageBody *body = [[EMTextMessageBody alloc] initWithText:@"你好,环信"];
    
    //构建一条消息
    EMChatMessage *message = [[EMChatMessage alloc] initWithConversationID:"user2" body:body ext:@{}];
    
    //设置消息的聊天类型为单聊消息
    message.chatType = EMChatTypeChat;
    
    //将消息发送出去
    [EMClient.sharedClient.chatManager sendMessage:message progress:^(int progress) {
    } completion:^(EMChatMessage * _Nullable message, EMError * _Nullable error) {
        if(error){
            NSLog(@"发送失败(%d):%@",error.code,error.errorDescription);
        }else{
            NSLog(@"发送成功");
        }
    }];


如何收到消息





另:代理可以添加多份,不过如果在不需要代理的情况下一定要移除,否则会被强引用,无法释放


至此,我们已将IM和UI整合至项目中。大功告成!



阅读推荐:深度改造聊天界面的cellhttps://www.imgeek.org/video/121

收起阅读 »

Flutter必学的Getx状态管理库

什么是 GetX? 一个简单、高效、强大的管理状态、路由管理库 学习目标 掌握使用GetX管理状态 了解基础GetX状态管理的原理 GetX状态管理的优势 精确渲染,只会渲染依赖状态变化的组件而不会全部组件渲染一遍 安全性高,当程序出现错误时,不会因为重...
继续阅读 »

什么是 GetX?


一个简单、高效、强大的管理状态、路由管理库


学习目标



  • 掌握使用GetX管理状态

  • 了解基础GetX状态管理的原理


GetX状态管理的优势



  1. 精确渲染,只会渲染依赖状态变化的组件而不会全部组件渲染一遍

  2. 安全性高,当程序出现错误时,不会因为重复更改状态导致崩溃

  3. 有个GetX永远不需要声明状态组件, 忘记StatefulWidget组件

  4. 实现MVC架构,将业务逻辑写到控制器中,视图层专注于渲染

  5. 内置了防抖/节流、首次执行等功能

  6. 自动销毁控制器,无需用户手动销毁


用法


1.1声明响应式状态


有三种声明方式,使用哪一种都可以 推荐第三种


1.1.1 使用声明,结合Rx{Type}


final name = RxString(''); // 每种内置的类型都有对应的类
final isLogged = RxBool(false);
final count = RxInt(0);
final balance = RxDouble(0.0);
final items = RxList<String>([]);
final myMap = RxMap<String, int>({});

1.1.2 泛型声明 Rx


final name = Rx<String>(''); 
final isLogged = Rx<Bool>(false);
final count = Rx<Int>(0);
final balance = Rx<Double>(0.0);
final number = Rx<Num>(0);
final items = Rx<List<String>>([]);
final myMap = Rx<Map<String, int>>({});
// 自定义类 声明方法
final user = Rx<User>();

1.1.3以.obs作为值(推荐使用)


final name = ''.obs;
final isLogged = false.obs;
final count = 0.obs;
final balance = 0.0.obs;
final number = 0.obs;
final items = <String>[].obs;
final myMap = <String, int>{}.obs;
// 自定义类 声明方法
final user = User().obs;

2.1 使用响应状态到视图中


有两种方法使用状态:



  1. 基于Obx收集依赖状态

  2. 基于GetX<Controller>获取对应的控制器类型


2.1.1基于Obx收集依赖状态


十分简单,我们只需要使用静态类即可达到动态更新效果。



  1. 创建一个 Controller


// HomeController 可以写到一个专门管理控制器的文件中,这样方便维护
// 就像 React 需要把 Hook 单独提取一个文件一样
class HomeController extends GetxController {
var count = 0.obs;
increment() => count++;
}


  1. 导入创建的 Controller 并使用它


class Home extends StatelessWidget {
const Home({super.key});
@override
Widget build(BuildContext context) {
// 寻找Controller
HomeController c = Get.find<HomeController>();
return Obx(
() => Scaffold(
body: ElevatedButton(
// 通过`c.count.value`使用状态,也可以不使用.value,.value可选的
child: const Text("${c.count}"),
onPressed: () => c.count++, // 改变状态,
),
),
);
}
}

2.1.2 基于GetX<Controller>获取对应的控制器类型


这种做法需要三个步骤



  1. 声明一个控制器


// HomeController 可以写到一个专门管理控制器的文件中,这样方便维护
class HomeController extends GetxController {
var count = 0.obs;
increment() => count++;
}


  1. GetMaterialApp类中初始化时导入对应的控制器


// main.dart
void main() {
runApp(GetMaterialApp(
// 如果不写这一步那么GetX将无法找到HomeController控制器
initialBinding: InitBinding(),
home: const Home(),
));
}
class InitBinding implements Bindings {
@override
void dependencies() {
Get.put(HomeController());
}
}


  1. 在对应组件或页面中使用GetX<Controller>实现数据的响应


class Home extends StatelessWidget {
const Home({super.key});

@override
Widget build(BuildContext context) {
// 这样就可以正常使用了
return Obx<HomeController>(
builder: (c) => Scaffold(
body: ElevatedButton(
child: const Text(c.count.value),
onPressed: () => c.count++,
),
),
);
}
}

3.1 监听状态更新的工具函数



  • 当依赖的值发生变化后会触发回调函数


var count = 0.obs;

// 每当 count 发生改变的时候就会触发回调函数执行
ever(count, (newCount) => print("这是count的值: $newCount"));

// 只有首次更新时才会触发
once(count, (newCount) => print("这是count的值: $newCount"));

/// 类似于防抖功能频繁触发不会每次更新,只会停止更新count后的 1秒才执行(这里设置成了1秒)
debounce(count, (newCount) => print("这是count的值: $newCount"), time: Duration(seconds: 1));

/// 类似于节流功能 频繁更新值每秒钟只触发一次 (因为这里设置成了1秒)
interval(count, (newCount) => print("这是count的值: $newCount"), time: Duration(seconds: 1));

GetX状态管理的疑惑


1.1 哪些地方可以使用.obs



  • 可以直接在类中赋值使用


class RxUser {
final name = "Camila".obs;
final age = 18.obs;
}


  • 直接将整个类都变成可观察对象


class User {
User({String name, int age});
var name;
var age;
}

final user = User(name: "Camila", age: 18).obs;

1.1.2 一定要使用xxx.value获取值吗?


这个并没有强制要求使用xxx.value获取值,可以直接使用xxx这能让代码看起来更加简洁


1.2 可观察对象是类如何更新?



  • 两种方式可以更新,使用其中一种即可


class User() {
User({this.name = '', this.age = 0});
String name;
int age;
}
final user = User().obs;

// 第一种方式
user.update( (user) {
user.name = 'Jonny';
user.age = 18;
});

// 第二种方式
user(User(name: 'João', age: 35));

// 使用方式
Obx(()=> Text("名字 ${user.value.name}: 年龄: ${user.value.age}"))

// 可以不需要带.value访问,需要将user执行
user().name;

GetX状态管理的一些原理


1.1.1.obs原理是什么?


var name = "dart".obs



  • 源码只是通过StringExtensionString扩展了一个get属性访问器

  • 原理还是通过RxString做绑定


tips: 如果想查看源码的话可以通过 control键 + 左击.obs就可以进入源码里面了


1.2 Obx的基本原理是什么?



  • 简而言之,Obx其实帮我们包裹了一层有状态组件


var build = () => Text(name.value)

Obx(build);


继承了一个抽象ObxWidget类,将传递进来的build方法给了ObxWidget,还得看看ObxWidget做了什么



ObxWidget继承了有状态组件,并且build函数让Obx类实现了



_ObxWidget主要做了两件事情



  1. 初始化的时候监听依赖收集,销毁时清空依赖并关闭监听。这是Obx的核心

  2. Obx实现的build函数传递给了RxInterface.notifyChildren执行



NotifyManager是一个混入,主要功能



  • subject属性用于传递更新通知

  • _subscriptions属性用于存储RxNotifier实例的订阅列表

  • canUpdate方法检查是否有任何订阅者

  • addListener用于将订阅者添加到订阅列表中,当 RxNotifier 实例的值发生变化时,它将通过 subject 发出通知,并通知所有订阅者

  • listen方法监听subject变化并在变化时执行回调函数

  • close关闭所有订阅和释放内存等

作者:liang的编程之路
链接:https://juejin.cn/post/7222656390547914809
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

有趣的futu android面试体验

futu面试算是我面过的最特殊的了,大部分问题都是智力或者算法题,幸好我平常有刷算法的习惯,即使是项目相关的题,也是非常开放式的,几乎没看到八股 整体体验面试官还是比较专业的,其中的实现队列题和概率题我没有考虑到的点都和我进行了友好讨论,提醒了我,尤其是三面的...
继续阅读 »

futu面试算是我面过的最特殊的了,大部分问题都是智力或者算法题,幸好我平常有刷算法的习惯,即使是项目相关的题,也是非常开放式的,几乎没看到八股


整体体验面试官还是比较专业的,其中的实现队列题和概率题我没有考虑到的点都和我进行了友好讨论,提醒了我,尤其是三面的那道概率题,挺有意思,也是多亏面试官提示


不算难,但是需要扎实的基础,大家可以作为参考


一面:


项目


recyclerview刷新设计,分页,复杂布局,网络流量 刷新包含整个刷新,单个item刷新, 局部刷新策略,diff对比后台刷新id


MVVM的viewModel与android中的ViewModel的区别


如何分享一个抽象列表框架(如recyclerview)技术给客户端同事,包含pc, ios等


如何做一个需求,输入框可以输入,输入字符串变化时都会有回调产生对应效果


算法/智力




  1. 数组实现队列,入队出队


    增加扩容功能,


    修改为循环队列







  1. 9个砝码,一个轻的,最少次数称出来?


    称的次数与砝码数量有什么关系?






  1. 数组中和大于等于target的长度最小的连续子数组


二面:




  1. 多线程


    static a = 0;

    thread1: a += 1;

    thread2: a += 1;

    最后的结果区间







  1. 已知公司OA数据库有一个员工信息表,


    包含员工ID,员工姓名,入职月份(如201801),和离职月份。


    财务审核时发现 201803 到 201808 这6个月,


    当时所有在职员工都少发了工资,


    现在老板需要了解有多少人受影响需要获得补偿。


    请写出查询语句。




  2. a b c 轮流投掷一个硬币,直到正面出现即胜利,求c获胜的概率




  3. 在一个字符串中,找出不包含重复字符的最长子字符串的长度




aa => a => 1


abcdaf => bcdaf => 5


abcd => abcd => 4


abacad=>bac=>3


afbcdef => afbcde => 6


三面



  1. 编码:输入有字符串s1和s2,判断s2是否包括s1的排列 例如, 输入s1=abc,s2=abcd, 输出True; 输入s1=abc,s2=acbd, 输出True; 输入s1=abc,s2=ambnc, 输出False; (排列的解释:字符串abc,则abc的排列包括abc、acb、bac、bca、cab 和 cba)


先用了排序,复杂度


然后用数组,空间换时间




  1. 90% 返回0,10%返回1,如何包装此方法让返回0与1都是50%




  2. 做过的最难/有挑战性的项目


作者:小松漫步
链接:https://juejin.cn/post/7221357811288457272
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

环信 uni-app Demo升级改造计划——Vue2迁移到Vue3(一)

前言由于环信uni-app Demo 为早期通过工具从微信小程序转换为的 uni-app 项目,经过实际的使用以及复用反馈,目前已经不适用于当前的开发使用,因此开启了整体升级改造计划,目前一期计划将 vue2 代码进行手动转换为 vue3+vite,并剔除原项...
继续阅读 »

前言

由于环信uni-app Demo 为早期通过工具从微信小程序转换为的 uni-app 项目,经过实际的使用以及复用反馈,目前已经不适用于当前的开发使用,因此开启了整体升级改造计划,目前一期计划将 vue2 代码进行手动转换为 vue3+vite,并剔除原项目中已经无用的项目代码,下面记录一下升级操作,如果升级过程,对大家有所帮助,深感荣幸~

前期准备

  • 【重要】阅读 uni-app 官网文档 Vue2 升级 Vue3 指南文档地址
  • 调研迁移到 Vue3 中原有的 Demo 中哪些三方库或者方法将不可用主要 uview UI 库不支持 Vue3)。
  • 下载并运行环信官网 uni-app 项目(原项目master分支)。Demo下载地址
  • 在 HubilderX 中创建容器项目所谓容器项目即为创建一个空白的 Vue3 模板,用以逐步将 Vue2 的项目代码逐步挪到此项目中。
  • 在空白项目中引入 uni-ui 组件,主要为了使用其组件替换原项目 uviewUI 组件
  • 确认升级流程以及方式本次升级采用渐进式语法修改形式,主要方式为迁移一个组件则将修改一个组件的语法为 vue3,如该组件依赖多个组件则先切断相组件的连接注释大法,后续逐步放开并配套修改。

核心迁移步骤

第一步、导入环信 uni-app SDK

原有 Vue2 版本 uni-app-demo 项目为本地引入 SDK 包,对于有些习惯 npm 安装导入的同学不太友好,目前 uniSDK 已经支持 npm 安装并导入,因此将原有本地引入 js 文件改为通过 npm 安装 SDK 并 import 导入 SDK。

//第一步 打开终端执行 npm install easemob-websdk
//第二步 复制原demo中的utils文件夹至空白项目中
//第三步 找到utils文件夹中的WebIM.js 文件中的导入SDK方式改写为impot 导入 easemob-websdk/uniApp包,具体代码如下。
/* 原项目引入SDK代码 */
import websdk from '../newSDK/uniapp-sdk-4.1.2';
/* 改写后的代码 */
import websdk from 'easemob-websdk/uniApp/Easemob-chat';

第二步、CommonJS 导入导出改写为 ESM

这种改写原因两点:

1、CommonJS 规范在 Vite 中使用本身并不支持,如果支持则需要进行单独配置。

2、原始项目中既有 CommonJS 导入方式,也有 ESM 导入,借此机会进行统一。

进行到此主要是先将原始项目中的 CommonJS 导出 WebIM 实例改为 ESM 导出,后续会在语法改造过程中将所有 CommonJS 规范改写为 ESM 导出,后续将不在本文中提及,实例代码如下

/* 原始项目utils/WebIM.js的导入导出WebIM实例代码段 */
//导入方式
let WebIM = (wx.WebIM = require('./utils/WebIM')['default']);
//导出方式
module.exports = {
default: WebIM,
};

/* 改写后导入导出 */
//导入方式
import WebIM from '@/utils/WebIM.js';
//导出方式
export default WebIM;

第三步、迁入 App.vue 组件

完整的复制原始项目中的 App.vue 组件(uni 的 Vue3 模板中也支持 Vue2 代码,因此可以放心进行 CV)

App.vue 组件涉及到的改动为注释掉暂时没有引入的 js 文件,后续进行引入,去除 scss 中的 uview 样式代码,引入后续将要完全剔除 uview 组件。

App.vue 中代码较多此示例做了大量的缩减,大致调整之后的结构如下。

<script>
import WebIM from '@/utils/WebIM.js';
//这些导入暂时注释,后续再进行引入
//let msgStorage = require("./components/chat/msgstorage");
//let msgType = require("./components/chat/msgtype");
//let disp = require("./utils/broadcast");
//let logout = false;

//import { onGetSilentConfig } from './components/chat/pushStorage'
export default {
//export default的代码块原封不动,此处先进行了删除,实际迁入不用动。
data (){
return {

}
}
}
</script>
<style lang="scss">
@import './app.css';
/*注意这行代码删除 @import "uview-ui/index.scss"; */
</style>

第四步 牛刀小试~ 迁入 Login 组件

先迁入一个 Login 组件热热身,毕竟从登录开始,原始项目中有注册、Token 登录、等等但目前暂不需要所以只需迁入 Login 组件。

在迁入前我们先了解并思考一下,Vue2 的 Options API 与 Vue3 Composition API 一些特点,主要目的是用较小的代价进行 Vue3 语法改造。
Vue3 模版支持 setup 语法糖,因此可以直接使用使用 setup 语法糖方式进行语法改造。

<script setup>
/* 原始代码片段 */
let WebIM = require("../../utils/WebIM")["default"];
let __test_account__, __test_psword__;
let disp = require("../../utils/broadcast");
data() {
return {
usePwdLogin:false, //是否用户名+手机号方式登录
name: "",
psd: "",
grant_type: "password",
psdFocus: "",
nameFocus: "",
showPassword:false,
type:'text',
btnText: '获取验证码'
};
},
/* 改造后的代码 */
//使用reactive替换并包裹原有data中的参数
import { reactive } from 'vue'
import disp from '@/utils/broadcast.js'; //修改为ESM导入
const WebIM = uni.WebIM; //从挂载到uni下的WebIM中取出WebIM并赋值用以替换原有单独require导入的WebIM
const loginState = reactive({
usePwdLogin: true, //是否用户名+手机号方式登录
name: '',
psd: '',
grant_type: 'password',
psdFocus: '',
nameFocus: '',
showPassword: false,
type: 'text',
btnText: '获取验证码',
});

//methods中的方法提取到外层中,例如将login 登录IM进行调整
//登录IM
const loginIM = () => {
runAnimation = !runAnimation;
if (!loginState.usePwdLogin) {
if (!__test_account__ && loginState.name == '') {
uni.showToast({
title: '请输入手机号!',
icon: 'none',
});
return;
} else if (!__test_account__ && loginState.psd == '') {
uni.showToast({
title: '请输入验证码!',
icon: 'none',
});
return;
}
const that = loginState;
uni.request({
url: 'https://a1.easemob.com/inside/app/user/login/V2',
header: {
'content-type': 'application/json',
},
method: 'POST',
data: {
phoneNumber: that.name,
smsCode: that.psd,
},
success(res) {
if (res.statusCode == 200) {
const { phoneNumber, token, chatUserName } = res.data;
getApp().globalData.conn.open({
user: chatUserName,
accessToken: token,
});
getApp().globalData.phoneNumber = phoneNumber;
uni.setStorage({
key: 'myUsername',
data: chatUserName,
});
} else if (res.statusCode == 400) {
if (res.data.errorInfo) {
switch (res.data.errorInfo) {
case 'UserId password error.':
uni.showToast({
title: '用户名或密码错误!',
icon: 'none',
});
break;
case 'phone number illegal':
uni.showToast({
title: '请输入正确的手机号',
icon: 'none',
});
break;
case 'SMS verification code error.':
uni.showToast({
title: '验证码错误',
icon: 'none',
});
break;
case 'Sms code cannot be empty':
uni.showToast({
title: '验证码不能为空',
icon: 'none',
});
break;
case 'Please send SMS to get mobile phone verification code.':
uni.showToast({
title: '请使用短信验证码登录',
icon: 'none',
});
break;
default:
uni.showToast({
title: res.data.errorInfo,
icon: 'none',
});
break;
}
}
} else {
uni.showToast({
title: '登录失败!',
icon: 'none',
});
}
},
fail(error) {
uni.showToast({
title: '登录失败!',
icon: 'none',
});
},
});
} else {
if (!__test_account__ && loginState.name == '') {
uni.showToast({
title: '请输入用户名!',
icon: 'none',
});
return;
} else if (!__test_account__ && loginState.psd == '') {
uni.showToast({
title: '请输入密码!',
icon: 'none',
});
return;
}
uni.setStorage({
key: 'myUsername',
data: __test_account__ || loginState.name.toLowerCase(),
});
console.log(111, {
apiUrl: WebIM.config.apiURL,
user: __test_account__ || loginState.name.toLowerCase(),
pwd: __test_psword__ || loginState.psd,
grant_type: loginState.grant_type,
appKey: WebIM.config.appkey,
});
getApp().globalData.conn.open({
apiUrl: WebIM.config.apiURL,
user: __test_account__ || loginState.name.toLowerCase(),
pwd: __test_psword__ || loginState.psd,
grant_type: loginState.grant_type,
appKey: WebIM.config.appkey,
});
}
};
</script>

改造中会遇到了原 Vue2 中原 data 部分参数通过使用 reactive 包裹并重命名,需要注意把语法中的 this.、me.、this.setData 进行替换为包裹后的 state 命名,另外 template 中也要同步进行替换,这一点在后续所有组件改造中都会遇到。

Login 组件需要 page.json 中进行路由的配置,只有配置成功之后我们方可运行项目并展示页面!

此时就可以启动项目运行观察一下看看页面是否可以正常的进行展示,当然是运行到小程序还是 H5 以及 App 上自行选择。

第五步、 迁入“Home 页中的”三个 Tab 页面【conversation 会话列表,mian 联系人页、Setting 我的页面】

迁移各组件,此处使用 conversation 组件作为示例,其余两个组件完全相同的步骤,全部示例代码将在文章末尾给出地址。

在原项目中包括已迁移进来的 App.vue 组件中有下面这样一个方法,其作用即为环信 IM 连接成功之后触发 onOpened 该监听回调,进行路由跳转进入到会话页面,因此不难理解,open 之后首个跳转的页面即为 conversation。

    onLoginSuccess: function (myName) {
uni.hideLoading();
uni.redirectTo({
url: "../conversation/conversation?myName=" + myName,
});
},
  • 在原始项目中 copy conversation(会话)组件至容器项目相同目录下,另外不要忘记顺手在 page.json 下配置路由。

  • 开始改写会话组件中的代码

//script 标签增加 setup 使其支持setup语法糖
<script setup>
/* 引入所需组合式API */
//computed 用以替换options API中的计算属性,Vue3中计算属性使用略有差异。
import {reactive,computed} from 'vue'
/* 引入所需声明周期钩子函数替换原有钩子函数,该写法uni-appvue2升级vue3指南有提及 */
import { onLoad, onShow, onUnload } from '@dcloudio/uni-app';
/* 调整disp为import导入 */
// let disp = require("../../utils/broadcast");
import disp from '@/utils/broadcast';
/* 调整WebIM引入直接从uni下取 */
// var WebIM = require("../../utils/WebIM")["default"];
const WebIM = uni.WebIM
let isfirstTime = true;
/* components中的组件暂时注释,template中的组件引入也暂时注释,
* 另options API中的components中的组件注册也暂时注释
*/

// import swipeDelete from "../../components/swipedelete/swipedelete";
// import longPressModal from "../../components/longPressModal/index";

/* data 提出用reactive包裹并命名 */
const conversationState = reactive({
// 内容省略...
});

/* onLoad替换 */
onLoad(() => {
//所有通过this. 进行方法方法调用全部删除
disp.on('em.subscribe', onChatPageSubscribe);
//监听解散群
disp.on('em.invite.deleteGroup', onChatPageDeleteGroup);
//监听未读消息数
disp.on('em.unreadspot', onChatPageUnreadspot);
//监听未读加群“通知”
disp.on('em.invite.joingroup', onChatPageJoingroup);
//监听好友删除
disp.on('em.contacts.remove', onChatPageRemoveContacts);
//监听好友关系解除
disp.on('em.unsubscribed', onChatPageUnsubscribed);
if (!uni.getStorageSync('listGroup')) {
listGroups();
}
if (!uni.getStorageSync('member')) {
getRoster();
}
readJoinedGroupName();
});
/* onShow替换 */
onShow(() => {
uni.hideHomeButton && uni.hideHomeButton();
setTimeout(() => {
getLocalConversationlist();
}, 100);
conversationState.unReadMessageNum =
getApp().globalData.unReadMessageNum > 99
? '99+'
: getApp().globalData.unReadMessageNum;
conversationState.messageNum = getApp().globalData.saveFriendList.length;
conversationState.unReadNoticeNum =
getApp().globalData.saveGroupInvitedList.length;
conversationState.unReadTotalNotNum =
getApp().globalData.saveFriendList.length +
getApp().globalData.saveGroupInvitedList.length;
if (getApp().globalData.isIPX) {
conversationState.isIPX = true;
}
});
/* 计算属性改写 */
const showConversationName = computed(() => {
const friendUserInfoMap = getApp().globalData.friendUserInfoMap;
return (item) => {
if (item.chatType === 'singleChat' || item.chatType === 'chat') {
if (
friendUserInfoMap.has(item.username) &&
friendUserInfoMap.get(item.username)?.nickname
) {
return friendUserInfoMap.get(item.username).nickname;
} else {
return item.username;
}
} else if (
item.chatType === msgtype.chatType.GROUP_CHAT ||
item.chatType === msgtype.chatType.CHAT_ROOM
) {
return item.groupName;
}
};
});
const handleTime = computed(() => {
return (item) => {
return dateFormater('MM/DD/HH:mm', item.time);
};
});
/* 将methods中方法全量提取到外层与onLoad onShow等API平级 */
const listGroups = () => {
return uni.WebIM.conn.getGroup({
limit: 50,
success: function (res) {
uni.setStorage({
key: 'listGroup',
data: res.data,
});
readJoinedGroupName();
getLocalConversationlist();
},
error: function (err) {
console.log(err);
},
});
};

const getRoster = async () => {
const { data } = await WebIM.conn.getContacts();
if (data.length) {
uni.setStorage({
key: 'member',
data: [...data],
});
conversationState.member = [...data];
//if(!systemReady){
disp.fire('em.main.ready');
//systemReady = true;
//}
getLocalConversationlist();
conversationState.unReadSpotNum =
getApp().globalData.unReadMessageNum > 99
? '99+'
: getApp().globalData.unReadMessageNum;
}
console.log('>>>>好友列表获取成功', data);
};
const readJoinedGroupName = () => {
const joinedGroupList = uni.getStorageSync('listGroup');
const groupList = joinedGroupList?.data || joinedGroupList || [];
let groupName = {};
groupList.forEach((item) => {
groupName[item.groupid] = item.groupname;
});
conversationState.groupName = groupName;
};

//还有很多方法就不一一展示,暂时进行了省略...
/* onUnload */
onUnload(() => {
//页面卸载同步取消onload中的订阅,防止重复订阅事件。
disp.off('em.subscribe', conversationState.onChatPageSubscribe);
disp.off('em.invite.deleteGroup', conversationState.onChatPageDeleteGroup);
disp.off('em.unreadspot', conversationState.onChatPageUnreadspot);
disp.off('em.invite.joingroup', conversationState.onChatPageJoingroup);
disp.off('em.contacts.remove', conversationState.onChatPageRemoveContacts);
disp.off('em.unsubscribed', conversationState.onChatPageUnsubscribed);
});
</script

在做这三个组件迁移的时候主要的注意事项为,this 的替换,template 中的默认从 vue2 中 data 取的参数也要替换为被 reactive 包裹后的变量名。

启动运行调整

建议迁移一个组件调试一个组件,运行到 H5 端,从登录页面登录进去,并点击三个页面进行切换,观察是否有相应的报错,发现即进行修改并重新运行测试。

第六步、迁入复杂度最高的聊天相关组件。

以单聊作为说明示例:

1)迁入单聊入口组件[pages/chatroom]

chatroom 组件(groupChatroom 作用相同)为单聊功能聊天的入口组件,pages 中其他组件发起单聊聊天时均会跳转至该组件,而该组件同时又承载 components 下的 chat 组件作为容器形成聊天功能。

将 chatroom 组件 copy 至容器项目 pages 下并配置路由映射,为了语义化将 chatroom 更名为 singleChatEntry,并进行语法改造,此时 singleChatEntry 如下:

不要忘了,路由路径配套也要从 chatroom 更名为 singleChatEntry

<template>
<chat
id="chat"
ref="chatComp"
:chatParams="chatParams"
chatType="singleChat"
></chat>
</template>

<script setup>
import { ref, reactive } from 'vue';
import {
onLoad,
onUnload,
onPullDownRefresh,
onNavigationBarButtonTap,
} from '@dcloudio/uni-app';
import disp from '@/utils/broadcast';
import chat from '@/components/chat/chat.vue';

const chatComp = ref(null);
let chatParams = reactive({});
onNavigationBarButtonTap(() => {
uni.navigateTo({
url: `/pages/moreMenu/moreMenu?username=${chatParams.your}&type=singleChat`,
});
});
onLoad((options) => {
let params = JSON.parse(options.username);
chatParams = Object.assign(chatParams, params);
// 生成的支付宝小程序在onLoad里获取不到,这里放到全局变量下
uni.username = params;
uni.setNavigationBarTitle({
title: params?.yourNickName || params?.your,
});
});
onPullDownRefresh(() => {
uni.showNavigationBarLoading();
chatComp.value.getMore();
// 停止下拉动作
uni.hideNavigationBarLoading();
uni.stopPullDownRefresh();
});

onUnload(() => {
disp.fire('em.chatroom.leave');
});
</script>
<style>
@import './singleChatEntry.css';
</style>

2)完整迁入 components 组件

image.png

components 组件结构如上图,由于音视频功能已经废弃本次迁移决定剔除,但目前迁移方案采取“抓大放小,后续清算”的策略先一起迁入,后续剔除。

引入之后运行起来之后会发现有很多 require not a function 字眼的错误,同样我们要将所有 CommonJS 的导出修改为 ESM 导出,剩下的则是一点一点的去进行语法改造,整个 chat 下其实涉及组件非常多,因为 IM 所有消息的收发,以及渲染均囊括在此组件。

这里提一下 msgpackager.js、msgstorage.js、msgtype.js、pushStorage.js 几个 js 文件的作用。

msgpackager.js 主要为将收发的IM消息进行结构重组

msgstorage.js 将收发消息进行本地缓存

msgtype.js 消息类型以及聊天类型的常量文件

pushStorage.js 推送处理相关

迁入进去之后将开始针对大大小小十几个文件进行语法以及引入改造,另外其中个别文件还牵扯到使用的 uviewUI 那么则需要进行重写,最终经过改造以及剔除不再使用的组件以及音视频相关代码之后,结构如图:
image.png

有一点较为基础但是还是要强调注意的事项要提一下,在 components/chat 下的组件改造中经常出现父子组件的调用,那么父组件在使用子组件的方法的时候,由于 Vue3 中不能再通过类似$ref 直接去调用子组件中的方法或者值,子组件需要通过 defineExpose 主动进行暴露方可使用,这个需要进行注意。

迁移中发现 H5 的录音采用的 recorder-core.js 库,js 按需导入中有用到 require,那么需要改写为 import 导入,但是发现实例化时发现依然不是一个构造函数,通过改写从 window 下访问即正常使用,相关代码如下:

    /* 原代码片段 */
handleRecording(e) {
const sysInfo = uni.getSystemInfoSync();
console.log("getSystemInfoSync", sysInfo);
if (sysInfo.app === "alipay") {
// https://forum.alipay.com/mini-app/post/7301031?ant_source=opendoc_recommend
uni.showModal({
content: "支付宝小程序不支持语音消息,请查看支付宝相关api了解详情"
});
return;
}
let me = this;
me.recordClicked = true;
// h5不支持uni.getRecorderManager, 需要单独处理
if (sysInfo.uniPlatform === "web") {
import("../../../../../recorderCore/src/recorder-core").then((Recorder) => {
require("../../../../../recorderCore/src/engine/mp3");
require("../../../../../recorderCore/src/engine/mp3-engine");
if (me.recordClicked == true) {
clearInterval(recordTimeInterval);
me.initStartRecord(e);
me.rec = new Recorder.default({
type: "mp3"
});
me.rec.open(
() => {
me.saveRecordTime();
me.rec.start();
},
(msg, isUserNotAllow) => {
if (isUserNotAllow) {
uni.showToast({
title: "鉴权失败,请重试",
icon: "none"
});
} else {
uni.showToast({
title: `开启失败,请重试`,
icon: "none"
});
}
}
);
}
});
} else {
setTimeout(() => {
if (me.recordClicked == true) {
me.executeRecord(e);
}
}, 350);
}
}
/* 调整后代码片段 */
const handleRecording = async (e) => {
const sysInfo = uni.getSystemInfoSync();
console.log('getSystemInfoSync', sysInfo);
if (sysInfo.app === 'alipay') {
// https://forum.alipay.com/mini-app/post/7301031?ant_source=opendoc_recommend
uni.showModal({
content: '支付宝小程序不支持语音消息,请查看支付宝相关api了解详情',
});
return;
}
audioState.recordClicked = true;
// h5不支持uni.getRecorderManager, 需要单独处理
if (sysInfo.uniPlatform === 'web') {
// console.log('>>>>>>进入了web层面注册页面');
// #ifdef H5
await import('@/recorderCore/src/recorder-core');
await import('@/recorderCore/src/engine/mp3');
await import('@/recorderCore/src/engine/mp3-engine');
if (audioState.recordClicked == true) {
clearInterval(recordTimeInterval);
initStartRecord(e);
audioState.rec = new window.Recorder({
type: 'mp3',
});
audioState.rec.open(
() => {
saveRecordTime();
audioState.rec.start();
},
(msg, isUserNotAllow) => {
if (isUserNotAllow) {
uni.showToast({
title: '鉴权失败,请重试',
icon: 'none',
});
} else {
uni.showToast({
title: `开启失败,请重试`,
icon: 'none',
});
}
}
);
}
// #endif
} else {
setTimeout(() => {
if (audioState.recordClicked == true) {
executeRecord(e);
}
}, 350);
}
};

3)启动进行后续调整测试

启动之后验证发现更多的是一些细节问题,同样边改边验证。

后续总结

在首期迁移 vue2 升级 vue3 的工作中其实难度并没有很大,主要的工作量集中在语法的修改变更上,好在 uni-app 中可以同步去写 vue2 与 vue3 两种语法代码,这样有助于在引入之后陆续进行语法变更,另外迁移之后开发体验启动速度确实快了很多,接下来就可以腾出手针对 uni-app-demo 源码代码进行整体质量提升,敬请期待…

收起阅读 »

【2023】22届前端程序员毕业一年的心路历程

没有任何光环的,一个普通人的前端之路 启程 要讲是如何对前端感兴趣的,我大约可以从大一的C语言讲到大四的微信小程序,想了想这不是本篇文章的重点,所以掐住话头(。 自觉大学过得十分散漫,临近毕业我才开始认真对待自己的前途,好在专业选的不错——计科,啥也不会又啥都...
继续阅读 »

没有任何光环的,一个普通人的前端之路


启程


要讲是如何对前端感兴趣的,我大约可以从大一的C语言讲到大四的微信小程序,想了想这不是本篇文章的重点,所以掐住话头(。


自觉大学过得十分散漫,临近毕业我才开始认真对待自己的前途,好在专业选的不错——计科,啥也不会又啥都懂一点。


大四那年寒假我从学校建的就业群里参加了一个线上面试培训班(虽是商业但全程免费),从接触前后端的概念到决定学前端只用了十分钟,那个时候我连html都不太会写,就被拉去接触企业级的前端开发流程,可想而知有多手忙脚乱,“蓝湖、禅道、yapi...”,这些现在习以为常的东西对于当时的我来说根本形不成概念,中途跟老师提了一次退出,理由是不想拖大家后腿,好在老师十分鼓励我继续跟进。于是我一边跟着培训进度一边在b站学习前端基础,虽然到结课也没学完,培训课也因为人数不够被腰斩。


留下来的同学被打包送进面试背题班(没错就是背八股文),明确的目的使得效率突飞猛进,回想起来仍觉得那一个月过得十分抓马,但也是在那里我真正叩响了前端的大门。


实习前夕


培训课一个月结束,寒假还剩半个月,对就业十分心虚的我决定提前返校,由于疫情学校不提前开放,我便借住在同学的校内公寓里,那段日子每天一睁眼就是打开电脑看视频课,跟着敲代码,直到学完vue以后,我才有了些许底气面对我的简历(培训老师倾情指导,洋洋洒洒写了好多我根本不懂的技能,只能硬着头皮一个一个学。


寒假前我在班级群里保存了一份招聘实习生的材料,负责人说我联系的时间有些晚,但还是接受了我的简历并通知我去面试,面试前一天培训老师和我通了电话,告诉我可能会遇到哪些提问,以及怎么答。


人生第一次求职面试就这样顺利结束,虽然实习工资近乎于倒贴上班hhh(长沙的行情令人闻之落泪)


经验之谈


入行前端于我而言是一段兵荒马乱的时光,给后来者的建议就是早做准备!早点实习!真的会少很多焦虑与迷茫。


另外,学校的资源一定要充分利用,出了社会就更没人为你操心了。


实习阶段


实习的过程中我写了一个门户网站,也参与了一个大型管理后台的需求迭代


内容少而简单,三个月时间就是夯实基础以及查漏补缺,不过很大一个原因就是我学的刚好是我要用的,真真是一点新东西都没有出现,也是很神奇_(:з」∠)_

大概是以下这些:


html + js + css

vue2 + webpack + axios + vuex

element-ui

git


这段阅历虽然浅薄但也不可或缺,让我明白学过跟真正会用是两码事,也真正实践了企业级的开发流程。
临别前负责人找我谈话,语重心长地建议我去大城市发展,我点点头,但对于自身的实力十分不自信,并没抱太多希望。


春招的末班车


因为考研错过秋招,又因为实习错过春招,在确定不转正后我才把精力放到了春招末班车上。

不得不承认机会真的所剩无几,为数不多的几场招聘会一眼望去全是销售,偶尔有前端岗位也不对本校学生开放,简历改了又改,真正能投出去的屈指可数。


当然也不是全无机会,要是我当时甘心留在长沙,从我实习能接触到的技术与眼界来看,也许就不会有这篇文章了。


转折点就在那些不对本校学生开放的前端岗位上,招聘摊位的人并没有接我的简历,但我本着排队都排了那么久的心态还是扫了易拉宝上的企业春招二维码,投递了线上的简历。


大约是幸运女神的眷顾,一面二面终面,耗时近两周,最终一路绿灯拿到了深圳的offer,后来从导师那里得知,我投递简历的那一天,也是春招通道关闭的最后一天。


真正的修炼之路


一个事实是,即使抓住了机会,我也时常焦虑,放眼同期入职的都是双一流的研本,说没有压力神都不信。


疏解压力的办法只有比别人加倍努力。


试用期三个月,第一个月vue2就被公司淘汰了,前端技术更新迭代的速度令我咋舌,那段时间我几乎把所有的技术栈都更新了一遍。


稳定下来的vue技术栈大致如下:


html + js + ts + css + less

vue3 + vite + axios + pinia

ant-design-vue

git + docker + nginx + devops


学会这些大致能跟上团队的开发速度,但代码依旧不优雅,知识也不成体系。
转正后我忙于一个又一个需求,新技能的学习基本依托于业务,例如被动学习了fabric、echarts、g2plot等等。


要是组织架构没有变动,身边的同事没有被优化,我大约会持续这样状态很久,直到遇到下一个瓶颈。


变动带来了不安,也推动了新一轮的自我学习,转到新的业务线后,自己明显感觉能控制开发节奏了,独立完成的需求越来越多,遇到知识盲区的次数也越来越少,部门的业务和技术栈大差不差,好处是不用花费精力去适应另一套生态,坏处也很明显,没办法从工作中学到技术层面的新东西了。


也是在这段时间,我能够抽出时间自主学习,先是用hexo搭建了个人博客,又尝试着用react自己设计开发,期间去重修了nodejs,补充了一些后端的技能。我导师说他初中就开始做这些事情了(我发誓我初中对网络的概念还停留在4399...( _ _)ノ|


技术之外


从我转正到22年年底,一共参加过两次答辩,一次是转正答辩,一次是年终述职答辩,两次的结果都不错,刚毕业的我很喜欢从一些虚无缥缈的事情上寻找价值,比如被领导夸了两句就觉得自己像一个快乐的小陀螺,到现在还是如此。


与此同时,工作带给我的精神内耗一直很严重,总觉得自己什么都不会,也不聪明,不懂表达,追求不高,喜欢给自己也给别人画饼。


唯一的优势大约是我尚且年轻,仍然对无垠的未来充满期待,并且愿意为之付出努力。


23年的flag


1、系统学习前端,不再浅尝辄止;

2、继续学习英语,关注前沿技术,坚持博客产出;

3、早睡早起,饮食规律,平凡且健康就很好。


写下这篇文章更多的是记录自己曾经和当下的状态,期冀今后的每一年我都比现在更加从容。


作者:兀米米
来源:juejin.cn/post/7216223889487511608
收起阅读 »

总结:用chatGPT整理现有商业模式

上周用chatGPT做了几个简单的测试。 用历史的角度询问了岳飞的去世。 并且让它帮我写了一段快速排序的javascript代码,以及解释了一下vue2的双向数据绑定原理。 跟朋友谈论了chatGpt目前的能力和未来的发展。 我们都认为chatGpt作为一...
继续阅读 »

上周用chatGPT做了几个简单的测试。


用历史的角度询问了岳飞的去世。
image.png


并且让它帮我写了一段快速排序的javascript代码,以及解释了一下vue2的双向数据绑定原理。


image.png


跟朋友谈论了chatGpt目前的能力和未来的发展。


我们都认为chatGpt作为一个高效的工具,在不远的将来,将很大程度上的代替一部分人的工作岗位。


正如公司与人才的关系一样,人才与某个岗位挂钩,本质上是可以把人当成工具来看的。而当对chatGTP的投产比高于人的时候,那么从公司的角度一定会选择chatGTP而不是选择人。


未来大家都能用上chatGTP了,那么在这样高效率的工具面前,利用这个高效工具拿到结果的能力,就不仅仅只是专业能力。站在全局的角度去搜索和整合信息的能力,会显得更为重要。


聪明的人能够把chatGPT发挥10倍的作用,普通人只能发挥1倍的作用,从而产生了差距。


介于AI的爆发早晚都会到来,那么我就通过AI查阅一下如果他来了,我们可以看到现有的商业模式有哪些,并且哪些模式能跟AI结合到一起呢?


所以,今天这一篇文章主要是通过chatGTP对目前常见的一些商业模式的收集,希望对大家有帮助。


01传统商业模式


传统的商业模式是:企业通过生产和销售产品或服务来获得利润。


比如卖衣服、买鞋子、卖蜂蜜等等实物的方式。这种模式的核心是降低成本和提高销售量。


目前我们这种模式做的比较成功的公司有:Coca-Cola、walmart、Procter & Gamble、McDonalds和Ford。


Coca-Cola是全球性的饮料生产企业。


walmart是全球最大的零售企业。


Procter & Gamble是全球性的消费品制造商。


McDonalds是全球性的快餐企业。


Ford是全球性的汽车制造商。


02订阅模式


订阅模式是:企业提供订阅服务来为企业带来稳定的现金流和客户关系。


这种模式的优势在于可以提供更好的客户体验和更稳定的收益。


目前我们这种模式做的比较成功的公司有:Netflix、Amazon Prime、Spotify、The New York Times和Dollar Shave Club。


Netflix是全球性的在线流媒体服务提供商。


Amazon Prime是亚马逊提供的一个订阅服务,订阅用户可以享受免费快递、流媒体服务和其他福利。


Spotify是一家全球性的音乐流媒体服务提供商,可以通过订阅服务来获取音乐和广播。


The New York Times是全球知名的报纸和新闻网站,通过定于服务向用户提供新闻和其他内容。


Dollar Shave Club是一家提供订阅式刮胡刀和理发产品的企业,通过订阅服务向用户提供每月定期配送的产品。


03平台模式


平台模式的企业,提供一个平台来连接买家和卖家,从而获得利润。


比较常见的国内平台有淘宝、天猫、美团、滴滴打车等等。


这种模式的优势在于可以快速扩展规模和提高利润率。


目前做的比较成功的企业有:Airbnb、Uber、Amazon Marketplace、Upwork和eBay。


Airbnb是一家在线的短期住宿预定平台,这个平台提供者房屋出租和旅游体验预定服务。


Uber是一家提供打车服务的在线平台,通过连接司机和乘客,提供便捷的打车服务。


Amazon Marketplace是亚马逊提供的一个在线市场平台,卖家可以在上面销售自己的商品。


Upwork是一个在线自由职业者和雇主的平台,雇主可以在上面发布任务,自由职业者可以在上面寻找机会。


eBay是一家全球性的在线拍卖和购物平台,卖家可以在上面销售商品,买家可以在上面购买商品。


04开放式商业模式


开放式商业模式的企业通过:开放自己的技术、数据和知识产权,与其他公司和个人合作,从而获得更大的市场和创新机会。


目前做的比较成功的企业有:Android、IBM Linux、Tesla、Salesforce和Wikipedia。


Android是一款由Google开发的开放式移动操作系统,允许开发者在其上开发应用程序。


IBM Linux是IBM 在 2000 年将其操作系统Linux开放源代码,从而为Linux社区的发展做出了重大贡献。


Tesla是一家全球知名的汽车制造商,其开放商业模式在于其开放自己的电池技术和充电网络,与其他汽车制造商合作,从而扩大了市场和创新机会。


Salesforce是一家提供云计算解决方案的企业,其开放商业模式在于其开放自己的API接口,允许其他企业和开发者在其上开发应用程序。


Wikipedia是一个免费的在线百科全书,其开放商业模式在于其开放自己的内容和编辑权限,允许用户编辑和更新条目。


05企业社会责任模式


企业社会责任模式的企业通过履行社会责任来提高品牌形象和市场份额。


这种模式的优势在于可以赢得消费者和员工的认可和支持。


目前做的比较成功的企业有:Patagonia、Ben &Jerry's、TOMS和The Body Shop。


Patagonia是一家知名的户外用品制造商,其企业社会责任模式在于其对环境和气候变化的重视和投资,例如推广可持续农业和使用环保材料。


Ben &Jerry's是一家知名的冰淇淋制造商,其企业社会责任模式在于其对社会公正和环保的关注和投资,例如推广可持续农业和支持社会公益事业。


TOMS是一家知名的鞋类品牌,其企业社会责任模式在于其对弱势群体的关注和投资,例如每出售一双鞋,就会捐赠一双鞋给需要的人。


The Body Shop是一家知名的化妆品品牌,其企业社会责任模式在于其对环境、社会公正和动物权益的关注和投资,例如推广环保材料和支持社会公益事业。


06服务模式


服务模式的企业通过提供各种服务,例如咨询、培训、维修等,从而获得利润。


目前做的比较成功的企业有:Uber、Airbnb、Amazon Prime和Netflix。


Uber是一家科技公司,提供网络预订的出租车服务。


Airbnb是一家在线市场,提供旅游住宿服务。


Amazon Prime是亚马逊公司提供的一个订阅服务,用户每年支付一定的费用,即可享受包括免费配送、视频流媒体等在内的各种优惠和服务。


Netflix是一家在线视频流媒体服务提供商,为用户提供了大量的电影和电视节目。


07建立生态系统模式


建立生态系统模式企业通过建立一个完整的生态系统来为客户提供全方位的服务,并从中获得利润。


目前做的比较成功的企业有:苹果公司的ioS生态系统、谷歌的Android生态系统和微软的Azure云计算生态系统。


苹果公司的ioS生态系统是苹果公司通过其iOS生态系统建立了一个完整的生态系统,包iPhone、iPad、Mac电脑、Apple Watch等设备、App Store、iTunes Store、Apple Music等服务。


谷歌的Android生态系统是谷歌通过其Android生态系统建立了一个大规模的生态系统,包括数百万款应用程序、各种服务和设备,例如Pixel手机、Google Home等。


微软的Azure云计算生态系统是微软通过其Azure云计算生态系统建立了一个全球性的云计算平台,为用户提供了各种云服务,如laas、 Paas、SaaS等。


08生产线模式


生产线模式的企业通过控制生产线上的每一个环节来提高效率和降低成本。


目前做的比较成功的企业有:丰田生产线模式、摩根汽车生产线模式和富士康生产线模式。


丰田生产线模式是丰田生产线模式是一种高效的、精益的生产方式,它的核心是“精益生产”和“精益流程”。


摩根汽车生产线模式是摩根汽车是一家英国的小型汽车制造商,采用传统的手工制造方式和高度定制化的生产线模式。


这种模式的成功在于,它能够为客户提供独一无二的汽车,满足客户对于个性化和品质的需求。


摩根汽车凭借这种模式在高端汽车市场上获得了成功。


富士康生产线模式是富士康是一家全球性的电子制造服务提供商,采用高度自动化和标准化的生产线模式。


总结


以上,就是我通过chatGPT帮我整理的8大商业模式,希望大家能根据自己的资源选择能结合的商业模式。


如果这篇文档对你有帮助,欢迎点赞、关注或者在评论区留言,我会第一时间对你的认可进行回应。精彩内容在后面,防止跑丢,友友们可以先关注我,每一篇文章都能及时通知不会遗失。


作者:程序员摩根
来源:juejin.cn/post/7217360688263413817
收起阅读 »

“勇敢的去走自己的夜路”——走出我的“浪浪山”

引子 2022年,经历过太多太多的故事,也发生了太多太多的事故。 这一年,迷途失措且努力,未来可期却恍惚,我错失了太多的机会,幸然遇到了大家,让我们一同努力见证Cool(小陈同学)的改变,这一年我经历过比赛失利,国奖失之交臂,也遇到了求职季的滑铁卢。但有幸的是...
继续阅读 »

引子


2022年,经历过太多太多的故事,也发生了太多太多的事故。


这一年,迷途失措且努力,未来可期却恍惚,我错失了太多的机会,幸然遇到了大家,让我们一同努力见证Cool(小陈同学)的改变,这一年我经历过比赛失利,国奖失之交臂,也遇到了求职季的滑铁卢。但有幸的是遇到了一堆可爱的掘友,利用掘金的资源也找到了一个工作。


这一年,我走了很远的路,吃了很多的苦,踩了很多的坑,才将这份年终总结交付与星球大伙。也曾有幸与掘友一起分享只属于我们的“情书”


第一节:对你,同样对自己说


今天是 2023年1月1日,这一年,半分努力,半分浑噩,忽隐忽现的理想,支撑着自己踽踽独行。几年前,他应征入伍,算不上什么好选择,也或许并没有选择的权利。北方干冷的空气,窗前停驻的麻雀,以及战友豪迈的言语曾一度让我觉得,南京或许会是我最终的归宿。


在南京的第二个年头,这一年我21周岁,报国的赤心和热血似乎都正热时,我做出了人生的第一个计划,“退役复学”。


感恩军旅生活,让我真正的热爱祖国与持续学习,在二零年上旬,新冠疫情爆发了,一个八十八线小城市的我,除了紧张的气氛外,到也没受到多大的影响,在家依旧忙碌,直到2022年2月10日,我记得非常清楚,写了一天前端(三件套的弱鸡)代码的我,结束了当天的笔记小结,打开了B站,悄然间随机看见了关注了好久的鱼皮居然真的开了学习圈子(编程导航),这让一个对编程说不上爱的萌新,从此爱上了coder与share,(一个利他的博主谁又能不爱呢?),曾经把编程视为作业的我,我发现我能用他code出一个全新的世界,我便一发不可收拾爱上了它(这里的它指的是编程)。


(一)身体是革命的本钱


but 「熬夜 + 不规律的作息 + 不健康的饮食」+ 「年轻」= 无事发生


“年轻人”,似乎总是有一种得天独厚的优势,有精力,有体力。而这对于我这个退役选手更是easy了,这些不太好的习惯也似乎在年轻一代的大水潭中泛不出多少涟漪,凭借着这份“本钱” ,自然能更加心安理得,反正:我还年轻,我还可以熬。


(2) 继续战斗,也请先照顾好自己


疫情消耗掉了大半年的时间,大学断断续续的锻炼,把熬夜换成了早起,开始按时吃早餐,解封后的日子,趁着南方冬天来的很晚,与几个战友开始了跑步的活动,这一阶段体能上确实有了很大的提升,我很享受跑步后,被风吹过的感觉(皮一下:我也曾吹过未来女朋友曾吹过的风)。


说来也很神奇,每次当我没什么精神,只要去跑步,回来冲个澡就会精神百倍,所以我一般傍晚的时候有空就会去跑跑,然后就可以再晚上全身心的写代码,整理笔记(当然最后就是发到星球上面,感受大家阅读后的“指责与指导,哈哈哈”)。


运动本不应被当做一种应该做的任务,而应被看作一种休闲的方式,没必要与别人比较强度,组别,只有自己舒服就是最好的标准。


所以,不管是真的热爱也罢,苦于生计也罢,即使继续战斗也请先照顾好自己


(二) 随波逐流只会让你接近平均值


普通人的危机感总来自他人,而想要成为一个优秀的人,危机感必须来自自身,随波逐流只会让你靠近平均值,总有一种恍惚感,懂得越多,越觉得自己像这个世界的孤儿,与同龄人格格不入,总是自负的认为他人幼稚,就像鲁迅先生说过:“人类的悲欢并不相通......”。听着他们谈论着我 “早就走过的路”,“早就见过的风光”,我也只觉得他们吵闹。


可惜,我在某些时候,总是小气的,心中惰于学习,更不愿将自己的 “财富” 与他人分享,总忧虑别人以己为石,跳向远方,患得患失的一种矛盾,让自己无奈又颓靡。
后来我遇到了鱼皮,我发现分享的乐趣后我便不再随波逐流,持续性努力,以下是我在星球这一年输出的笔记



(ps:请大佬过目,记得留赞)如下:



大数据笔记:wx.zsxq.com/dweb2/index…


运维Devops笔记:wx.zsxq.com/dweb2/index…


低代码Lowcode笔记:wx.zsxq.com/dweb2/index…


yarn的学习:wx.zsxq.com/dweb2/index…


软件设计师:wx.zsxq.com/dweb2/index…


NodeJS笔记:wx.zsxq.com/dweb2/index…


机器学习方面:wx.zsxq.com/dweb2/index…


Vue+pinpa笔记:wx.zsxq.com/dweb2/index…


MySQL笔记:wx.zsxq.com/dweb2/index…


华为鸿蒙认证:wx.zsxq.com/dweb2/index…


软件工程笔记:wx.zsxq.com/dweb2/index…


力扣刷题攻略:wx.zsxq.com/dweb2/index…


ES6模块暴露笔记:wx.zsxq.com/dweb2/index…


ACM算法思维导图:wx.zsxq.com/dweb2/index…


Bootstrap笔记:wx.zsxq.com/dweb2/index…


网络安全资源贴:wx.zsxq.com/dweb2/index…


(三) 不被枯井遮住双眼,保持谦虚及自尊


目光短浅带来的问题是致命的,当你有一天觉得自己好像还不错,好像已经登到峰顶了。那就需要反思一下自己或许已经陷入了“枯井”中,你会这样想,那大概率是被枯井遮住了双眼,你看不到枯井之外的世界,为了一点点成就就沾沾自喜,虽然阶段性的成功也很值得高兴,但千万不要走进这份舒适区中,温水煮青蛙的例子也不少见,走出枯井后,你就会发现外面的世界还是在一个枯井中,你要做的就是不断的往上爬。


永远不要看不起任何人,即使一位在你眼中普普通通的人,他的技术或许逊色你不少,但是他在思想和创造性上总能给你意想不到的惊喜。即使我的学校很普通,但是我的身边仍然有着一批充满韧劲的朋友,希望能通过考研,亦或者对于技术的钻研,弥补自己高考的遗憾,我记得大二那年,我常常在凌晨一点半两点收到微信弹窗大家一起交流一些问题。备战比赛的三点一线生活,学技术的通宵达旦,为了目标不断努力,这样的人仍然值得我尊重与学习,我认为他们拥有了一名大学生应该有的“灵魂”


除此之外,请千万保持自尊,自尊并非别人给的,而是自己给的,如果遇到比自己弱的人就有“自尊”,遇到比自己强的人就畏畏惧惧,没有“自尊”,那么这种自尊就没有任何意义了,闻道有先后,术业有专攻,应当尊重任何在某个方向的前辈,但是也没必要过于拘束,见贤思齐,见不贤而内自省即可。


(四) 远离总是给你负面情绪的人


但是如果你遇到了一些人,总时时刻刻,在学习以及生活上给你一些负面的观点,这种人会严重影响你坚定往枯井上爬的信念,不管你们是什么关系,我给你一个建议——赶快跑(这里现代化的叫法喊:润),有多快,跑多快,如果你们不幸要发生必要的交互,请将这段关系限制在最小范围内,切勿投入感情


(五) 传道授业:若要学知识,必得为人师


这一年我很喜欢读一本书,那就是《软技能:代码之外的生存指南》(下面我会提到)其中有一个章节给我印象很深,即第33章,传道授业:若要学知识,必得为人师,下面我摘了一段:



在你传道授业的时候都会发生什么 当我们初次接触某个课题的时候,我们对于自己对此了解多少往往都会高估。我们很容易自欺欺人,以为已经对某样东西 了如指掌,直到我们试着去教会别人的时候,才能发现事实并非如此。你有没有被别人问过一个非常简单的问题,却震惊地发现自己不能清晰地解答。你刚开始会说:“这个,很明显……”,接下来只有“哦……”。这种情况在我身上屡屡发生。我们自认为已经透彻理解了这个话题,实际上我们只是掌握了表面知识。这就是传道授业的价值。在你的知识集合里面,总有一部分知识你并没有理解透彻到可以向别人解释,而“教”的过程能够迫使你面对这一部分。作为人类,我们的大脑善于模式识别。我们能够识别模式,并且套用这些固定的模式去解决许多问题, 而没有做到“知其然”也“知其所以然”。这种肤浅的理解力无碍于我们完成工作,因而不易被察觉。然而一旦我们试着向别人解释某件事情的运作原理或背后的原因的时候,我们在认知上的漏洞就会暴露出来。不过这并非坏事。我们需要知道自己的弱点,然后才能对症下药。在教别人的时候,你迫使自己面对课题中的难点,深入 探索,从只知皮毛变成完全理解。学习是暂时的,而理解是永久性的。我可以背诵九九乘法表,但是一旦理解了乘法的运算原理,即使突然记性不好,我也可以重做一张乘法表。



我已经记不清很多年前我初中亦还是我高中的一位任课老师曾经说过这么一句话:能教会别人,自己也就没问题了。可惜那个时候的自己压根没提起学习的欲望,当然了,也或许与我自己根本不喜欢枯燥的应试教育有点关系。我也没理解这句话的意思。大学这几年,我很喜欢与朋友交流技术方面的事情,每个人都有很多我意想不到的理解与想法。还有更多时候我更加喜欢帮助朋友解决一些问题,当你什么时候可以将别人的一个问题,用通俗的解释说明 + 简洁却又富有代表意义的实例 + 补充一些自己的理解与看法,说给别人听得时候,最起码,我认为你对于这块内容就真的入门了。当你能够滔滔不绝的讲解给别人一块内容,能合理的安排讲解的引子与顺序的时候,这说明这一块的知识已经在你脑海中有了一条清晰的体系。同时你通过与别人交流的时候,再根据别人对你提供的一些方向好好反思斟酌一下,不断的修改。相信我,当你成功与他人讲解/交流你的知识后,你会爱上这种感觉的。


但是老板和老师可不会等你,很多时候我们都不得不 “填鸭子” 式的学习一些内容,例如根据老师的要求,强制使用一些指定的框架或者技术,或者根据业务/项目组长的需要和安排,你需要快速的学习一些你并不熟悉的内容,凭借我们多年 “应试” 的本事,大家总能很快的就找到这种套路,例如怎么快速搭建环境,怎么配置,如何快速的用起来。但是千万别止步于此,不然终究只是一个CRUD工程师,这也不一定是坏事,当你熟悉如何用一款框架或技术后,再去看一些源码,或许会事半功倍。



作为人类,我们的大脑善于模式识别。我们能够识别模式,并且套用这些固定的模式去解决许多问题, 而没有做到“知其然”也“知其所以然”。



(六) 别让情绪扼杀你状态


(1) 所谓迷茫,都不过是想的太多


总在独处时,开始怀疑自己,我是谁,我在干什么,以后该怎么办......在我理智的那两天,我都会把这种状态归咎于闲的蛋疼。但是确确实实在那种状态下,什么事情都没法下手,最严重的的一种状态,就是会有一种深深的无力感,感觉距离目标实在太远了。这种无力感,会瞬间摧毁你的勇气,让你不敢下手去做些什么。就像是一场噩梦,你明知道应该醒来,却无法挣脱。



鼠鼠我啊是家里唯一的大学生,大学入了党,工作也没有让家里操心,家里人都认为我有出息了,只有鼠鼠觉得鼠鼠是个废物,鼠鼠以前也会想着让妈妈为自己骄傲,让家里人可以开心的生活,可是浪浪山不如鼠鼠所愿,鼠鼠在浪浪山清楚的认识到了阶层的差距,身边的人正活着曾经难以想象的生活,鼠鼠也才知道人生可以那么精彩,它就在我眼前,又好像远在天边。鼠鼠在家里是最强天赋,在浪浪山却是擦锅布,我好像永远走不出浪浪山了,鼠鼠想回下水道,鼠鼠下辈子不想做鼠鼠。



这种状态,都不过是因为想的太多,我们总是在刚起步的时候,就想着终点在哪里;总是在刚学习一项技能的时候,就想着攻克技术难题;总是在与人初次见面之后,就想推心置腹;总是在今天都没有过好的时候,就想着明天该怎么办。我就是这样一个人,常常纠结于各种各样的学习路线上,每次在学习不同的技术的选择上,进行纠结,但其实这两者我明明是有足够的时间兼学的,还有时候明明知道基础要一步一步走扎实,但是却想到后面还有各种各样的新式技术,高级技术等着我,就会又开始所谓的迷茫。


其实这种所谓的迷茫,很多时候都是源自于我们想的太多了,路要一步一步走,饭要一口一口吃,想的太多,就会感到迷茫和焦虑。最好的办法就是,立足当下,安于寂寞,不要太着急看到极大的成果,放平心态,只有你的心里想通了,你的状态就会迅速回归,重振旗鼓


(2) 你总需要一个人走一段路


孤独伴随着,几年前来到几百公里外上学的我,亦或是年后即将开始找实习,找工作的我。


我想我总会有一段时间感觉到莫名的孤独,想找个人聊聊天,却又不想去找,自己戴着耳机,漫无目的的走在路上。以及每次晚上或者凌晨写完东西,躺在床上有一种说不出的感觉,特别的是,我并不感到忧伤,只是感觉空落落的,也不想认识新的朋友,也不想联系家人,却也不知道有些话该和谁说。


即使你人缘很好,常常有三五好友一起相伴,但是总会有一些空隙感到孤独,这源自于你的内心还是不够强大,有的人独行却乐在其中,有的人三无成伴却又内心孤独,因为孤独的人心中并无足乐者,灵魂还是被空虚填满。


所以,请充实自己的生活,多出去走走,多与人交往,给自己多找点自己感兴趣的事情去做,即使感到孤独,也没必要太过沮丧,只需要告诉自己,沮丧,孤独,都是正常的,我们要在自己走的这段路上,让自己成为一个更加闪亮的人。走过自己的一段夜路,终将会有柳暗花明又一村的“闹市”。


(七) 恋爱的本质是「撞」而不是「寻」


(1) 你真的想要谈恋爱吗?


有时候总会想,谈恋爱是「一定要」还是「可以要」亦或者 「没必要」。


总有那么几天,好似陷入了爱情的怪圈。让你平淡无奇的生活荡起了阵阵涟漪,打破了你安稳的生活轨迹。


大部分时候,或许只是你想要摆脱这种“孤零零”的状态,又或者看着别人的“幸福”与“快乐”,激起了你的那份欲望,而欲望总会在你的忍耐中冲昏你的头脑,让你开始憧憬爱情,并且费力的去「寻」去「找」,试图去接触不同的人,试图找出哪一个是适合自己的,或许你会觉得,主动去寻找自己的幸福是一件很美好的事情,不过于我而言,这并不是爱情,我只能把它叫做权衡利弊后的一个选择。


或许有的时候,你只是想找一个人陪你,那也或许并不是爱情。你结束了一天的忙碌,合上了笔记本电脑,关掉了手机,疲惫的倚靠在椅子上,狭小的房间中,只有那盏台灯在一片死寂中发出微弱的光。连点一只烟的动作都觉得多余,他只想一个人安静的待一会,也不知道在想什么。但如果无由头的想起了一些事情,一些人,这个引火线,就会瞬间将情绪点燃,无尽的孤独涌上心头,这个时候,你渴望有一个人陪在你身边,陪你说说话,哪怕陪着你坐一坐,起码让你知道你并不是一个人。自此以后,你开始标榜自己「需要人陪」,看似高尚的理由,其实只是你害怕寂寞的一种借口,就算你真的找到了一个陪着你的人,那你真的爱她吗,可能你只是在你漆黑的房间中又添置了一盏台灯,这样能让你的眼中看起来更加明亮。


(2) 三观一致真的很重要!


这几年也接触过一些异性,或许也有动心过,但是你会发现,不同人看待,处理事物的方式会有截然不同的结果,你认为简直不可理解的事情,在其眼中似乎也稀松平常,或许你不懂她,也许她不懂你,三观这个词的定义实在太模糊,最简单的方法就是看你们在一起的感觉,给你的感觉如果是很舒服的,那么可以进一步了解一下,害,没什么好说的了, 希望你可以找一位能符合你心中期望的另一半。


(3) 顺着人生轨迹走吧,别为了一个人停下来


千万不要陷入单恋的漩涡中,这是致命的,对的人是不需要主动找的,你只需要顺着人生轨迹走,在合适的年纪做合适的 “正事” ,自然而然就会遇到那个人了,如果等到七老八十,也没有遇到,或许这也就是命。或许说的太悲观了,但我仍认为,与其让自己为了追求一个不确定,也或许没有回应的爱情,不如自己欣赏自己孤岛中的美丽。但话也不能太绝对,或许有一天我就会因为所谓的爱情,陷入盲目。爱情这东西,谁说的好呢。但我只要不断告诉自己,一定不要停下来


第二节 这一年我都做了些什么


(一) 学习 + 技术输出


(1) 行百里者半九十


按照原来的计划,从 Java --> JavaWeb --> SSM --> SpringBoot 这个主线就算结束了,其中夹杂着 SQL,Linux,Web,小程序,设计模式等等的一些支线。不过,根据自己的情况和具体需要吗,其实我已经做出了一些重点的调整,我会在后面的目标中去提到。


(2) 一年和球友一起 输出了一百多万字的笔记



先放地址【Java全栈方向】:http://www.yuque.com/icu0/wevy7f


欢迎大佬们关注一下小弟。



一年中,一边学习,一边做总结,做整理,陆陆续续一年也写了200来篇笔记(也可能是文章或者感悟)(不一定纯后端/前端,还有 Linux ,计网等等)记得某个大佬说过写博客和笔记不一定能做到对别人有帮助,但起码对自己有帮助。但是我一直通过大白话概括 + 做图 + 简单示例 + 官方说明的方式写文章,也在努力希望能对别人也有帮助。



(二) 超爱买书的购物狂


这一年买了不少书(买了 == 看了),还有好多想买的都在我的购物车里吃灰,再买我真怕自己变成一个光收藏的 “读书人” 了,来盘点盘点这一年我看了比较有感觉的书(没感觉的和没怎么读的就不提了,如果给我多一点时间,我争取出一篇介绍自己读的书籍的文章)




一件恐怖的事情:我利用一年时间看过了这些书



第三节 明年今日,记得要回头看看



We already walked too far, down to we had forgotten why embarked. ——纪伯伦《先知》


译文:我们已经走得太远,以至于忘记了为什么而出发。



2022年度回顾



2023年新的目标


技术上:


只有写1-2月的,所以我放一个链接,欢迎大家监督我学习



http://www.yuque.com/icu0/qeowns… 《Cool的三两事》



生活上:


  1. 孝敬父母

  2. 勤运动

  3. 照顾好自己的身体

  4. 不要熬夜

  5. 与人交谈,沉稳思虑而后动

  6. 多读书,多出去走走,善待他人


学业上:


  1. 英语四级

  2. 拉取开源项目,为开源项目提PR

  3. 持续输出技术型文章

  4. 专升本上岸


总而言之,2022喜忧参半,有“春风得意马蹄疾,一日看尽长安花”的喜悦,也有“停杯投箸不能食,拔剑四顾心茫然”的忧愁,但我希望我的2023能有“长风破浪会有时,直挂云帆济沧海”。



个人独白:


以上内容皆是一名专科生的自白,感谢自己在大专三年没有一天是“浑浑噩噩式”学习,也没有一天因为当前的荣誉而骄傲满足,同时感谢部队两年的栽培,让我站在低谷依旧能仰望天空,扎根大地,心有猛虎,细嗅蔷薇。


作者:Cool
来源:juejin.cn/post/7187012953659899965
收起阅读 »

异地技术团队管理的三大模式六项注意

1 为什么会有异地团队 当一个企业成长到一定程度后,往往会在多地建立研发中心或者业务中心,这里企业的考量可能会有如下的一些点: 人才资源:不同的城市和地区可能具有独特的人才资源,通过在多个城市建立研发中心,公司可以吸引和招聘到更多具有不同技能和背景的优秀人...
继续阅读 »

1 为什么会有异地团队


当一个企业成长到一定程度后,往往会在多地建立研发中心或者业务中心,这里企业的考量可能会有如下的一些点:




  1. 人才资源:不同的城市和地区可能具有独特的人才资源,通过在多个城市建立研发中心,公司可以吸引和招聘到更多具有不同技能和背景的优秀人才。这有助于公司在保持竞争力,并确保能够获取到足够的人才来支持研发和业务需求。
    比如深圳是中国的高新技术产业中心,其在硬件制造、消费电子、通信技术等方面具有很强的竞争力,对硬件制造、物联网、人工智能等领域拥有丰富经验的工程师较多,并且由于深圳地理位置优越,靠近香港,拥有国际化的人才环境,因此在跨境项目和多元文化沟通方面具备优势;
    又如北京是中国的政治、文化和教育中心,拥有众多顶级高校和研究机构,拥有大量理论研究和技术创新方面的顶尖人才,北京的互联网行业较为成熟,尤其是在互联网+政务、在线教育、大数据等方面有较多经验的人才。




  2. 市场覆盖:在多个城市设立研发中心有助于公司更好地了解和适应不同地区的市场需求。这可以让公司更迅速地响应市场变化,提供更符合客户需求的产品和服务。




  3. 成本优化:不同地区的劳动力成本、房地产成本和生活成本可能存在差异。在多个城市建立研发中心可以让公司充分利用各地的成本优势,降低整体运营成本。如一些深圳/北京的公司,会把一些研发中心放到西安、成都、武汉、长沙等城市。




  4. 政策支持:一些城市为了吸引优秀企业入驻,可能会提供各种政策支持,如税收优惠、低息贷款、用地优惠等。在多个城市建立研发中心可以让公司充分利用这些政策优势,降低研发成本。




除此之外,还有风险分散的考虑,技术合作与创新等等,最终都是帮助公司获得更多的资源和优势,提高整体竞争力。


2 异地团队会有什么问题


以技术团队为例,当有多个技术团队在不同的城市后,与所有技术团队在同一个地方相比,会有一些问题出现,主要分为以下的 4 个方面:


2.1 团队建设和凝聚力打造困难


由于缺乏面对面交流和互动,异地团队成员之间可能难以建立信任和凝聚力。而团队建设和凝聚力是影响团队绩效的重要因素。当技术团队分布在不同城市时,团队建设和凝聚力可能受到以下方面的影响:




  1. 面对面交流机会少
    当团队成员分布在不同城市时,他们的面对面交流机会将大大减少。面对面交流有助于加深团队成员之间的了解、建立信任和加强团队凝聚力。例如,共同参加团队活动、庆祝生日等场合,能增强团队成员之间的情感联系。而分布在不同城市的团队成员可能很难享受到这些互动的机会。




  2. 困难的团队文化塑造
    一个健康的团队文化对于团队建设和凝聚力至关重要。在异地团队的情况下,公司需要付出更多的努力来塑造统一的团队文化。例如,各地团队可能在工作习惯、价值观、沟通方式等方面存在差异,这些差异可能导致团队凝聚力降低。




  3. 时空的隔阂
    异地团队面临地理距离的挑战,以及各地工作安排导致的时间不一致的问题。这种情况下,团队成员可能较难以达到理想的实时沟通,而在中国实时沟通是大部分公司的必备品,大家更习惯于实时的沟通,而不是异步的非实时沟通。




  4. 缺乏有效的团队认同感
    异地团队成员可能会感到自己与其他团队成员的联系较弱,这会导致他们缺乏对整个团队的认同感。例如,一个异地团队成员可能对其他城市团队的工作情况和成果了解较少,难以形成归属感和共同的目标。




2.2 项目管理及实时协同难度大


异地团队成员可能难以实时协作,尤其是涉及紧急问题或需要即时反馈的情况。项目管理及协同难度增大主要表现在以下的 3 个方面:




  1. 沟通成本上升:当团队成员分布在不同城市时,团队之间的沟通成本会显著增加。团队成员需要通过电话、电子邮件、即时通讯等工具进行沟通,这可能导致信息传递的延迟和误解。例如,一个团队成员在深圳提出一个需求变更,另一个团队成员在上海可能需要数小时甚至一天后才能了解到这一变更,从而影响项目进度。




  2. 快速应对变化的能力变弱:异地团队可能在应对突发事件和变更需求时存在局限。假设一个重要客户要求对产品进行紧急修改,跨城市的团队成员可能需要在短时间内协调资源和安排工作,而地理隔离使得这一过程变得更加困难。




  3. 时间管理和跨团队协调困难:不同城市的团队可能存在不同的工作时间和节假日安排(比如某个城市因为办公场地原因而全员居家),这可能导致某些任务在协作过程中出现延迟。例如,在一个紧急 bug 修复的情况下,由于一个城市的团队正在度假,另一个城市的团队需要独自解决问题,可能导致修复速度变慢。




2.3 监督和管理困难


在异地团队中,监控和评估团队成员的绩效可能较为困难。管理者需要找到合适的方法和指标,以便对团队成员的工作成果进行公平、准确的评估。监督和管理困难主要包括以下的一些情况:




  1. 工作状态难以掌握:由于地理隔离,管理者可能无法直接了解团队成员的工作状态和情况。例如,一个城市的团队可能遇到了技术难题,导致项目进度受阻,但管理者由于无法亲自与团队成员交流,可能难以及时发现问题并采取相应措施。




  2. 绩效评估困难:在异地团队中,评估团队成员的工作绩效可能变得更加困难。由于缺乏面对面交流,管理者可能无法准确评估团队成员的工作质量和效率。例如,一个城市的团队成员可能在某个任务上花费了较长时间,但管理者无法确定这是否是由于技术难题还是工作效率低下。




  3. 难以建立信任和团队凝聚力:地理隔离可能导致管理者难以建立与团队成员的信任关系,从而影响团队凝聚力。例如,一个城市的团队成员可能对管理者的决策表示质疑,由于无法进行面对面沟通,管理者可能无法充分解释决策背后的原因,从而导致信任度降低。




  4. 协调和调动资源困难:异地团队的管理者可能在协调和调动资源方面面临挑战。当项目需求发生变化或出现紧急问题时,管理者需要快速协调各地团队的资源,但地理隔离可能使这一过程变得更加复杂。例如,在一个紧急项目中,管理者需要从多个城市的团队中调集人力资源,但由于异地情况,这可能导致资源调配的速度和效果受限。




整体来说,主要是由于沟通与协作问题导致的各种延展性问题。缺失的面对面沟通、缺少肢体语言、表情语言等,可能导致信息传递不畅、误解和沟通成本的增加。我们无法彻底解决这些问题,但是能通过一些手段来缓解。


3 三大模式


为解决上面这些问题,我们在工作中发现了一些在不同的环境和场景中具有普遍适用性的解决或缓解问题方法,以模式的形式表述出来。


3.1 代理模式(Proxy Pattern)


代理模式在团队管理中可以被用于创建一个协调人或代表角色,负责处理某个团队或多个团队之间的沟通与协作。代理角色在此情景下充当一个中介,处理跨团队的需求、问题解决和资源协调。代理模式有助于简化沟通流程,提高团队协作效率。


具体实施方案:



  • 为每个团队或职能领域设立代理角色,如前端代理、后端代理、QA 代理和移动端代理。

  • 代理角色负责处理跨团队的需求和问题,同时将反馈和解决方案传递给相应团队。

  • 组织定期的代理角色会议,让代理们相互沟通和协作,以确保团队目标的达成。

  • 建立代理角色的沟通汇报机制,如定期晨会、周报和项目维度的回顾会。


3.2 门面模式(Facade Pattern)


门面模式提供了一个统一的接口来访问子系统中的一组接口。在团队管理中,可以创建一个统一的协调角色(如项目经理或技术负责人),该角色负责协调团队成员的工作,并充当各个团队之间的沟通桥梁。这有助于确保团队之间的沟通更加高效,降低沟通成本。


具体实施方案



  • 设立项目经理、技术负责人或者某个业务模块的 DRI 角色,负责跨团队协调和沟通。

  • 为每个团队成员分配具体的职责和任务,以便在项目经理或技术负责人的协调下高效协作。

  • 定期召开跨团队会议,确保团队之间的沟通畅通,及时解决问题。


门面模式和代理模式看起来有一点相似,其本质上是有区别的,区别在于授权的范围,门面模式不用太关注其内部实现,而代理模式在管理上要更深入细节一些。


在实际应用中,我们通常在各职能和各业务模块中使用代理模式,而针对不同的区域使用门面模式,由当前地区的负责人提供统一的输出。


3.3 观察者模式(Observer Pattern)


观察者模式在团队管理中可以应用于实时通知和信息共享。当一个团队成员对项目状态或任务完成情况进行更新时,其他相关成员可以作为观察者实时收到通知。这种模式有助于保持团队成员之间的信息同步,提高沟通效率。


具体实施方案



  • 为团队成员创建一个共享平台,如任务管理工具、项目管理系统等。

  • 当某个团队成员更新任务状态或项目信息时,系统自动通知其他相关成员。

  • 通过观察者模式,确保团队成员之间的信息同步,减少冗余沟通。


4 六项注意


4.1 相互信任


信任是团队协作的命脉。要想促进并保持长久的关系,你就必须信任他人,他们也必须信任你。与此同时,他们还必须相互信任。


信任来自相互理解对方的价值观、个人经历和立场。为了实现这一目标,我们必须承认自己的弱点,我们必须开放。这样我们才能够建立起共同的价值观和彼此信任。


信任在异地团队中有如下的好处:



  1. 提高团队凝聚力:信任关系有助于增强团队成员间的默契,从而提高团队凝聚力。当团队成员信任彼此时,他们更愿意携手合作,共同解决问题。

  2. 提高工作效率:信任关系可以促使团队成员更加开放地分享信息、资源和建议,从而提高整体工作效率。当团队成员相互信任时,他们更可能分享自己的想法和专业知识,共同解决问题。

  3. 降低沟通障碍:信任有助于消除团队成员间的沟通障碍,提高沟通效果。当团队成员彼此信任时,他们更愿意倾听对方的意见,以开放的态度接受建议和批评。

  4. 降低管理成本:信任关系有助于减轻管理压力,降低管理成本。当团队成员相互信任时,他们更可能自我管理,减少管理者的介入。

  5. 增加创新和风险承担:信任关系有助于创造一个安全的环境,使团队成员更愿意尝试新的想法和承担风险。当团队成员彼此信任时,他们更可能勇于创新和承担失败的风险。


建立相互信任关系的方法,以下是一些常见的方法:




  1. 增加沟通



    • 定期开展团队会议,让团队成员分享项目进展、遇到的困难和解决方案。

    • 鼓励一对一交流,让团队成员有机会深入了解彼此的工作、兴趣和需求。

    • 举办团队活动,如团队建设、庆祝活动和知识分享,促进团队成员间的互动和信任。




  2. 增加透明度



    • 使用项目管理工具,让团队成员能够实时查看项目进度和任务分配。

    • 定期分享业务战略、目标和团队绩效,让团队成员了解公司的发展方向。




  3. 赋予责任和权力



    • 根据团队成员的专长和兴趣分配任务,让他们在完成任务时有更大的自主权。

    • 鼓励团队成员在解决问题时提出建议和改进方案,展现对他们的信任。




  4. 鼓励支持和合作



    • 创建一个支持性的氛围,让团队成员在遇到问题时不惧于寻求帮助。

    • 鼓励团队成员互相学习、分享经验,以解决共同面临的问题。




  5. 表扬和认可



    • 在团队会议上表扬团队成员的优秀表现和努力。

    • 为表现突出的团队成员提供奖励,如奖金、晋升和表彰。




  6. 建立公平的环境



    • 确保团队中的决策过程透明,鼓励团队成员参与讨论和决策。

    • 设定明确的激励和奖惩




4.2 仪式感


在异地管理中,仪式感是一种有意识地营造正式或非正式场景,以传递重要信息、强化文化价值观、增强团队凝聚力和提升员工信任感的方式。


在异地团队中,恰当的仪式感具有以下好处:



  1. 增强团队凝聚力:仪式感有助于让团队成员感受到归属感和团队精神,从而增强团队凝聚力。

  2. 传递公司文化和价值观:通过仪式感,可以传递公司的文化和价值观,帮助团队成员更好地理解和认同这些价值观。

  3. 提升员工士气和信任感:仪式感可以激发团队成员的积极性和参与感,从而提高员工士气和信任感。

  4. 建立清晰的期望和目标:仪式感有助于确立团队成员的期望和目标,提高工作效率和执行力。


那如何建立恰当的仪式感呢?



  1. 定期召开团队会议:固定时间、地点召开团队会议,让团队成员汇报进展、分享经验、讨论问题。如每周一召开全体成员参加的在线例会,或者对于管理团队,定期如开包含问题同步和处理,学习分享的管理例会。

  2. 庆祝重要节点和成就:为团队的重要成就和里程碑设立庆祝活动,以增强团队成员的归属感和自豪感。如在项目完成时,举办在线庆祝活动,表彰优秀团队成员。

  3. 组织团队建设活动:定期组织线上或线下的团队建设活动,增进团队成员间的联系和互动。如每季度举办一次线上游戏比赛,增强团队成员之间的合作和交流。

  4. 激励和认可:对团队成员的努力和成果给予表扬和认可,提高他们的信任感。如每月颁发「最佳团队贡献者」奖项,表扬表现优秀的团队成员。

  5. 传递公司文化:通过仪式感传递公司文化,帮助团队成员理解和认同公司的价值观。如每年举办一次公司文化分享活动,邀请公司领导和团队成员分享公司文化和价值观。


4.3 严格目标管理,注重结果


在异地技术团队管理中,严格的目标管理和注重结果至关重要,因为这有助于确保项目按时完成、质量达标,并提高团队成员的工作效率和执行力。


以下是实行严格目标管理和注重结果导向的好处:



  1. 明确工作目标:设定清晰的目标和期望,帮助团队成员明确工作重点,避免资源浪费和目标模糊。

  2. 提高工作效率:明确的目标和期望有助于团队成员更高效地完成任务,降低拖延和低效的可能性。

  3. 便于评估和改进:结果导向的管理使团队可以通过衡量实际成果来评估工作效果,从而找出不足并进行改进。

  4. 激发团队成员积极性:目标明确、注重结果的管理方式有助于激发团队成员的积极性和责任心,鼓励他们为实现目标而努力。

  5. 有利于项目按期完成:严格的目标管理和注重结果有助于确保项目按计划进行,按时完成,避免延期。


那么如何实施严格的目标管理和注重结果导向?有如下 7 个方法



  1. 设定明确的目标:为项目和团队设定明确、可衡量、可达成的目标。如在项目开始时,为团队设定一个明确的项目交付日期,并明确交付内容的具体要求(也就是大家常说的 deadline 是第一生产力)。

  2. 制定详细的计划:为实现目标制定详细的计划和进度表,包括任务分配、时间安排等。如使用项目管理工具(如Trello、Jira等)制定详细的任务列表和时间表,如果没有这些工具,搞个在线表格也是极好的。

  3. 定期检查进度:定期与团队成员沟通,了解项目进度和遇到的问题,确保项目按计划进行。如定期的项目晨会(可以按周,或按天,也可以一周两次,根据实际情况调整),让团队成员报告各自的任务进展和遇到的问题。

  4. 强调结果导向:鼓励团队成员关注实际成果,以实现预定目标。在管理过程中对团队成员的绩效评估更注重实际完成的任务和贡献,而非工作时长或其他表面指标(不要卷加班)。

  5. 及时反馈和调整:根据实际进度和成果,及时给予团队成员反馈,调整目标或计划。如当发现某个任务进度落后时,及时与相关成员沟通,分析原因,并调整计划或提供支持。如当发现某个任务进度落后时,及时与相关成员沟通,分析原因,并调整计划或提供所需资源,以确保项目仍能按时完成。

  6. 定期总结和复盘:项目结束后,与团队成员一起总结经验教训,分析成功与失败的原因,以便在未来项目中持续改进。如项目结束后,组织团队进行复盘会议,总结项目的优点和不足,制定改进措施。或者迭代结束后做一些回顾。


我们在团队管理中,目标管理是一个非常重要的点,一定要自己主导,不能授权,作为一个技术团队的负责人,方向是你来定的,未来在你的手里


4.4 扁平、弹性的组织架构


在异地技术团队管理中,组织架构至关重要,因为组织架构会影响团队的沟通效率、决策速度、责任分配和协作。适合异地技术团队的组织架构应具备以下特点:扁平化、模块化、弹性和高度协作。




  1. 扁平化:扁平化的组织结构有助于提高沟通效率,减少信息传递过程中的失真和延迟。扁平化组织中,每个成员能够直接向上级汇报,决策速度更快,执行力更强。




  2. 模块化:将工作划分为具体的、相对独立的模块,有助于提高团队的协作效率。每个模块可以由一个或多个团队负责,这样可以减少跨团队协作的复杂度,降低沟通成本。




  3. 弹性:适应不断变化的项目需求和团队规模,组织架构需要具备一定的弹性。弹性的组织架构可以快速调整资源分配和团队规模,以满足项目发展的需要。




  4. 高度协作:鼓励团队成员之间的协作和互助,以提高工作效率和质量。高度协作的团队可以更好地应对复杂问题,减少重复劳动和资源浪费。




以下是如何实现适合异地技术团队的组织架构:



  1. 利用技术手段优化沟通:使用沟通和协作工具(如钉钉、企业微信、飞书、Microsoft Teams等)提高沟通效率,方便团队成员跨地域、跨部门协作。

  2. 决策下放:授权团队成员在其负责领域做出决策,提高决策速度。如将需求评审的决策权下放至小组 leader 或 DRI,甚至一线开发,让他们根据自己的专业知识对需求进行评估和调整。

  3. 设立技术负责人或项目经理:在每个地区或团队设立技术负责人或项目经理,负责协调团队成员的工作,确保项目顺利进行。如在各城市的团队中各设立一名项目经理,负责当地团队的项目进度和资源协调。

  4. 定期进行跨团队沟通:组织定期的跨团队会议,让各个团队分享进展、问题和解决方案。这有助于提高团队间的了解和协作。如每两周组织一次跨团队分享会议,让各个团队汇报自己的进展和挑战,共同寻找解决方案。

  5. 提供培训和支持:为团队成员提供技能培训和支持,以便他们更好地适应组织架构变化。如提供关于敏捷开发、跨部门协作等方面的培训课程,帮助团队成员提高工作效率和协作能力。

  6. 鼓励创新和变革:建立一种鼓励创新和变革的文化,让团队成员敢于尝试新方法,优化工作流程。如设立创新奖励计划,对于提出改进方案并成功实施的团队成员给予奖励。或者团队负责人亲自来参与或推进一些创新的事项,如最近的比较热的 AI。


4.5 统一的技术栈


在异地技术团队管理中,统一技术栈非常重要,因为它能为团队带来以下好处:



  1. 提高协作效率:统一技术栈能确保团队成员之间更容易进行技术交流和协作,避免因技术差异导致的沟通障碍和额外工作量。

  2. 降低维护成本:使用相同的技术栈,使得维护、调试和优化工作更加简单,减少因为技术差异导致的额外成本。

  3. 增强团队能力:统一技术栈有助于团队成员互相学习,提高整体技术能力,使得团队在面对复杂项目时更具备应对能力。

  4. 简化招聘和培训:统一技术栈使得招聘和培训过程更加简单,因为公司可以针对特定技术栈进行招聘和培训,提高招聘效率和培训质量。


为实现统一技术栈,我们可以采取以下方法:



  1. 制定技术规范和标准:制定统一的技术规范和标准,确保各地团队遵循相同的技术实践。如制定统一的编码规范、代码审查标准和自动化测试要求。

  2. 组织上增加架构设计的职能或者技术通道的职能组织: 通过组织的方式构建技术栈统一的土壤。

  3. 搭建技术共享平台:创建内部技术分享平台,让团队成员分享技术心得、问题解决方案和最佳实践,有助于统一技术理念和实践。如搭建一个内部的技术博客平台,鼓励成员撰写和分享技术文章。

  4. 统一基建和开发流程中的系统: 通过使用工具的统一达到技术栈的统一。

  5. 选型时充分调查和论证:在技术选型阶段,充分调查并论证各种技术方案的优缺点,确保选择的技术栈适合公司的业务需求和发展战略。

  6. 定期评估和调整:定期评估技术栈的合理性和有效性,根据项目需求和团队能力进行调整,以保持技术栈的统一性和先进性。如每年定期组织技术栈评审和审查,了解目前所使用的技术栈是否仍然满足业务需求,或者是否有新技术可以更好地支持业务发展。


通过以上方法,异地技术团队可以实现技术栈的统一,从而提高协作效率、降低维护成本、增强团队能力,并简化招聘和培训过程。这将有助于提高团队整体的研发效能,使得公司在面对市场竞争和业务挑战时更具备优势。


4.6 高效的沟通机制


异地团队最突出的问题是沟通问题,在我们平常的沟通过程中需要选择合适的沟通渠道和做有准备的沟通。良好的沟通有如下的好处:



  1. 能提高沟通效率:采用合适的沟通方式可以确保信息准确、及时地传递给相关人员,避免因沟通不畅导致的误解和冲突。

  2. 增强团队凝聚力:良好的沟通方式有助于增进团队成员之间的理解和信任,提高团队凝聚力。

  3. 减少资源浪费:有效的沟通方式能够减少不必要的会议和重复工作,降低资源浪费。

  4. 支持项目管理:清晰的沟通方式有助于确保项目进度、需求和问题得到及时解决,保障项目顺利进行。


我们可以通过如下的一些方式达到比较高效的沟通机制:



  1. 明确沟通目标和内容:在沟通开始前,明确沟通的目的、内容和预期结果。如:在项目会议开始前,列出讨论议题、相关人员和预期决策。

  2. 选择合适的沟通渠道:根据沟通内容和参与人员,选择合适的沟通渠道。如:对于紧急问题,可以使用电话或即时通讯工具(如微信、钉钉等)进行沟通;对于团队日常工作,可以使用邮件或者项目管理工具(如Jira、Trello等)进行沟通。

  3. 建立沟通规范:制定团队沟通规范,确保沟通有效进行。如:要求团队高效会议,或者要求团队成员在邮件中使用清晰的主题行、合理的收件人列表以及简洁明了的正文。

  4. 鼓励开放和诚实的沟通:营造一个鼓励团队成员开放、诚实地表达观点和需求的氛围。如:在团队会议上,鼓励成员提出问题、建议和想法,避免惩罚性的反馈。

  5. 定期进行沟通培训:为团队成员提供沟通技巧培训,以便他们更好地进行沟通。如:提供关于有效沟通、团队协作等方面的培训课程。


值得注意的是,异地的沟通中尽量少用邮件,邮件适用于传达信息和事实,撰写时还需要注意措辞,以防误会的发生。


单纯的文字无法传递情绪,如果要传达你的想法时,最好拿起电话进行视频,通过视频也能制造多次「见面」的机会更有利于建立信任。


现在用 IM 类工具也比较多了,在清晰的文字表达的基础上,多用表情包。


5 后记


上面说了这么多,有点啰嗦,简单点来说就是:多见见,多一起喝点酒,多一起搞定一些事情,保证基本的机制、流程、标准、工具和系统,也就差不多了。


异地的问题表象是见不着,核心要解决的是效率的问题。


技术团队的管理更多的还是人的问题,还是需要有情感的交流和因为长时间的一起工作而产生的向心力。
我们所做的这些仅能缓解这些问题。


当然,可能有同学会更喜欢异地/远程的工作协同模式,此处因人而异,从个人的角度来看:从团队的角度,从效能的角度,本地化团队会是更高效的选择。


当然以上的模式和注意事项在非异地团队的情况下也是可以使用的,而且效果会更好,因为这些的本质是授权管

作者:潘锦
来源:juejin.cn/post/7219651766707044389
理和过程管理的逻辑。

收起阅读 »

Low-Code,一定“low”吗?

web
作者:京东保险 吴凯 前言 低代码是一组数字技术工具平台,基于图形化拖拽、参数化配置等更为高效的方式,实现快速构建、数据编排、连接生态、中台服务。通过少量代码或不用代码实现数字化转型中的场景应用创新。本文将重点介绍低代码相关知识,包括低代码的定义与意义、相关概...
继续阅读 »

作者:京东保险 吴凯


前言


低代码是一组数字技术工具平台,基于图形化拖拽、参数化配置等更为高效的方式,实现快速构建、数据编排、连接生态、中台服务。通过少量代码或不用代码实现数字化转型中的场景应用创新。本文将重点介绍低代码相关知识,包括低代码的定义与意义、相关概念、行业发展等,同时介绍京东的低代码工具,期望能帮助大家更好地认识与理解低代码。


一、低代码介绍


2014年,Forrester(著名研究咨询机构)提出“低代码”的术语,定义为“利用很少或几乎不需要写代码就可以快速开发应用,并可以快速配置和部署的一种技术和工具”。或者说是“(能力)多(出品)快(质量)好(功夫)省”。



这个定义体现出低代码的核心价值:


1、低代码开发平台能够实现业务应用的快速交付。低代码开发的重点是开发应用快,不像传统意义上仅仅是一个应用的开发,而是通过可视化的开发,达到“设计及交付”的目的,提高开发效率。


2、低代码开发平台能够降低业务应用的开发成本。低代码开发投入更低,主要体现在开发时间短,可以快速配置和部署,同时也更容易使非开发人员上手。


二、我们为什么用低代码


低代码可以降本增效,一方面低代码的出现避免了“反复造轮子”的问题,其通过可视化的编程方式实现“千人千面”的效果,驱使技术回归本源--支持业务。另一方面低代码的生命周期贯穿整个软件开发周期(设计、开发、测试、交付),周期上的各角色都可以在同一个低代码开发平台上紧密协作,由传统的开发方式变为敏捷开发,实现了快速交付的目的。


低代码的使用场景:


1、构建新的SaaS应用,而借助低代码平台可以快速有效地构建、测试和推出应用。低代码与SaaS的结合,可以为企业提供独特的业务解决方案。


2、基于Web的门户网站是提供自助服务的数字化工具。使用低代码开发平台,更简单、更快速地构建个性化应用,打造数字化平台。


3、历史系统的迁移或升级。基于低代码技术:一方面,最大限度地保留遗留系统的代码,保留其“公共数据服务”;另一方面,基于遗留系统的开发环境和能力构建相应的“功能适配器”,然后在此基础上,通过低代码技术快速定制新业务和流程的交互式UI与业务逻辑。


4、应用复杂性低,业务流程相对简单,95%的应用场景可以通过低代码完成。



三、低代码会使程序员失业吗


回答这个问题,我们首先需要搞明白:低代码和零代码的区别。作为程序员,大家都会把低代码认为是零代码,这也是会被误解程序员失业的原因之一。


低代码,意味着反复迭代的代码质量高,在必要的时候,也会进行代码的编写;BUG更少,减少了测试环节的工作量。


零代码,字面意思:完全不需要任何代码即可完成应用开发,从软件开发效率看,**零代码是低代码的最终形态。**零代码平台由于采用全部都是封装模块进行搭建,所有控件都已经被固化了,所以用零代码平台搭建的系统想要进行扩展是有些困难的。


现实是,编码的最终目的是支持业务,业务逻辑的复杂与否依旧需要人来掌握,低代码只是写的少,并不是不写代码,这并不会导致程序员的失业



四、低代码的行业现状


2021年11月11日,Forrester发布《The State Of Low-Code Platforms In China》,这是低代码概念提出者第一次将视角聚焦在中国。Forrester认为,低代码目前在国内主要应用于银行、保险、零售、医疗、政府、制造、电信和建筑行业。比如,为了针对各个业务单元量身定制各种业务需求,中国建设银行采用云枢为其分布式开发团队构建统一的低代码开发平台(LCDP)。另外,报告指出:中国企业数字化转型过程中,有58%的决策者正在采用低代码工具进行软件构建,另有16%的决策者计划采用低代码。


目前,国内的低代码开发平台不断涌现,Forrester划分了9类低代码平台厂商:


▪数字流程自动化(BPM):炎黄盈动(AWS PaaS)、奥哲(云枢)


▪公有云:阿里巴巴(宜搭)、百度(爱速搭)、华为(应用魔方)、微软(Power Platform)、腾讯(微搭)


▪面向专业开发者的低代码开发平台:ClickPaaS、葡萄城(活字格)、Mendix、Outsystems


▪面向业务开发者的低代码开发平台:捷德(Joget DX)、轻流


▪AI/机器学习:第四范式(HyperCycle)


▪BI:帆软(简道云)


▪协作管理:泛微(E-Builder)


▪流程自动化机器人(RPA):云扩(ViCode)、来也(流程创造者)


▪数字化运营平台:博科(Yigo)、金蝶(金蝶云·苍穹)、浪潮(iGIX)、用友(YonBIP)


由此可知,中国的低代码市场正在飞速发展,各种低代码工具的发布问世,也意味着低代码未来将成为主流的开发方式。


五、业内的低代码平台


1、Out-System


OutSytems 作为国外著名的低代码开发平台,出发点就是简化整个应用开发和交付的过程,让开发人员可以快速响应市场的需求变化。通过可视化和模型驱动的开发方式,大幅减少时间和成本。并通过预构建的连接器加速集成后端系统,同时还提供了一个集中式的控制台来管理应用的版本、发布以及部署。


OutSytems 生成的应用可以不依赖于 OutSytems 运行。数据是直接存储到数据库,这样就可以通过任何标准的 ETL、 BI或其他第三方数据工具来访问数据。


官网:

http://www.outsystems.com/demos/


2、阿里-云凤蝶


云凤蝶是蚂蚁金服体验技术部的重点研发项目,是面向中后台产品的快速研发平台,主要用户面向工程师,使用场景专注在标准化的中后台产品研发,目标是为了提高效率。


云凤蝶的核心思路是将组件生产和组件组装这两部分工作进行职责分离,通过建立一条组件组装流水线,打通 npm 组件的一键导入流程,从而完成一条产业链式的分工协作,最终实现规模化的快速生产。


淘系的“乐高”系统以及蚂蚁金服的“金蝉”系统、“云凤蝶”系统成微阿里系主要的低代码开发工具。


3、京东-星链


星链是京东科技消金基础研发部开发的一款研发效能提升工具,主要为面向后端服务研发需求,因此前端简洁可视化开发界面需要满足极致的细节,并依赖其自身后端的能力来实现用户的低代码。


核心概念:


VMS可视化微服务应用,是星链的基本单元,同时VMS也是一种模型,各种配置均在模型中。支持京东中间件(JSF、定时任务、JMQ,缓存服务、分布式配置等),服务流程编排,DEBUG调试等;


Serverless部署,星链的部署及配置均由系统自动分配。用户只需关注系统的开发,资源的使用情况。


地址:jddlink.jd.com/


结论


低代码,一定不“low”,却更low-code。


参考:


2021年低代码平台中国市场现状分析报告

http://www.authine.com/report/56.h…



作者:京东云开发者
来源:juejin.cn/post/7217449801633808439
收起阅读 »