注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

乖和听话从来不值得称赞!

直到昨天去爬山被冷得像SB一样,我才知道这次冬天真的来了,前天还是大太阳,我在公司楼下转了一圈,只穿短袖,一夜之间,仿佛从非洲大陆到了南极大陆。 只有半个月2023就过去了,年底也逐渐忙碌了起来,回想起一年的时光,你又进步了多少,又收获了多少,还是原地踏步,我...
继续阅读 »

直到昨天去爬山被冷得像SB一样,我才知道这次冬天真的来了,前天还是大太阳,我在公司楼下转了一圈,只穿短袖,一夜之间,仿佛从非洲大陆到了南极大陆。


只有半个月2023就过去了,年底也逐渐忙碌了起来,回想起一年的时光,你又进步了多少,又收获了多少,还是原地踏步,我想,不管是否进步,退步,抑或是原地踏步,都没有关系,只要按照自己的节奏来行事,所谓的自律,进步这些都只是伪命题!


今天我们来聊一聊乖这个话题!


图片



我记得前段时间,我和一个网友语音聊天,他找我给他解决问题,我问了他一句话:为啥你不去自己钻研下呢,这些问题其实只是你稍微去好好学一下,都能解决的。


他对我说:我不想花时间去弄这个,我以后也不会从事软件这一行,我毕业后回去,好好复习,加上家里有一定的关系,找个体制内的工作不难,我也没有啥大追求,好好听父母的话,也能轻松,快乐过日子。


了解下来,他家境还是挺不错的,是独生子,父母都是体制内,母亲退休工资有1W+,父亲在单位也是挺不错的,给他已经全款买了房子(天津)。


从和他的聊天中,我看出他是一个典型的乖乖男,很听父母的话。


那么我相信,他一定能够很快乐,并且没啥压力过好以后的生活,他一定比中国95%以上的人过得开心,过得轻松,因为他没有什么压力,但是我觉得最主要的是,他已经预见自己未来的人生,并且能够顺利地走上这条路,所以他基本上不会去经历内耗,经历欲求不满!



另外一些现实中的朋友。


特别是从我们西南地区的农村出来的孩子,自然就没有多大的选择,没有占有地域上的优势,祖上三代都是靠土地活着,更没有资源背景。


这时候去听父母的话,基本上是自己废自己。


我们一起长大的一个朋友,也算是经历九死一生,现在混得相当不错,多年前我们在一起的时候,他说:已经那么穷了,还听父母的干嘛,如果父母说的有用,为啥还那么穷!


很扎心。


小地方出来的人,大多数人为啥自卑,不敢发表自己的观点,不敢反驳,总是唯唯诺诺的,即使有好的机会和平台,自己都不敢把握,说难听一点,为啥总是夹着尾巴做人,就是因为被老一辈灌输了很多”不健康“的思维。


就像我和以前的一个小学聊天,我说你为啥从一个这么好的大学出来,学得也不错,为啥不先去好的大公司里面试试水,而是选择回到这个落后的地方做一个初中毕业生都能做的事,一个月才几千。


他说干不过人家,加上自己不善于与人沟通,去这些大企业不好混,还是回来考编制稳一点。


我很尊重他的选择,但是我不认同他的选择,他的选择里也映射出了很多问题。


我之前看到一个作者发了一篇文章,他说从小被灌输了很多穷的思想,导致他一直以来都很自卑,做什么都觉得不对,都觉得对不起父母,有好的机会也觉得自己不配,所以穷了很多年。


后面他就不再去想那么多,不再顾虑父母那么多,甚至好多年都不回家,等他一个月能能稳定赚几万的时候。


他才慢慢克服了自卑,才有慢慢变得自信。


他说:如果我一直听他们的话,一直活在那种自卑,自负的环境中,那么我将一辈子无出头之日。


钱是穷人的胆,是穷人逃出自卑,自负的最佳良药,这句话一点没错。



从上面的两点,我我们可以看出不同的人生。


但是第二点的人占了社会的大部分,其实大多数人的家境都是很普通的,根本没啥资源,没啥背景,根本给你安排不了什么好的道路。


所以这时候,你的乖,你的听话,毫无意义。


它只能将你永远困住。


如果父母是有思想,有见解,并且有赚钱的人,那么我们一定要听他们的,因为这会让你少走弯路。


如果父母还在底层,还在贫穷和无知中,那么我们做得”残忍”一点更好,别太乖,因为没用!


当你兜里摸不出钱了,不能做自己想做的事,为生活发愁的时候,所谓的乖和听话会一巴掌一巴掌拍在你的脸上。


今天的分享就到这里,感谢你的观看,我们下期见。


对了,天这么冷,记得穿厚一点,别感冒了!


作者:追梦人刘牌
来源:juejin.cn/post/7313132521092169728
收起阅读 »

阿里妈妈刀隶体使用

web
最近在做的一个前端小项目,需要一种比较炫酷的字体,在网上找到了iconfont中的刀隶体,感觉还不错,本文记录了如何在前端项目中使用这种字体的步骤。 1. 找到刀隶体生成的网站 访问下面这个网站就可以了: 阿里妈妈刀隶体字体 http://www.iconfo...
继续阅读 »

最近在做的一个前端小项目,需要一种比较炫酷的字体,在网上找到了iconfont中的刀隶体,感觉还不错,本文记录了如何在前端项目中使用这种字体的步骤。


1. 找到刀隶体生成的网站


访问下面这个网站就可以了:


阿里妈妈刀隶体字体


http://www.iconfont.cn/fonts/detai…


2. 生成自己想要的字并下载到本地


找到文本输入框
image.png


然后输入自己想要展示的字体:我是一只小青蛙,最爱说笑话
image.png


最后点击下载子集按钮


image.png


下载好的压缩包:


image.png


将压缩包中的内容复制到剪切板:


image.png


3. 项目中引入


在项目中创建管理字体的目录


mkdir -p src/assets/font

然后到font目录下粘贴复制的字体文件夹


最后在项目的根样式文件中(一般来说是src/index.css)引入新字体:


@font-face {
font-family: "DaoLiTi";
font-weight: 400;
src: url("./assets/font/jHsMvOZ7UDEO/XBddIp4y0BmM.woff2") format("woff2"),
url("./assets/font/jHsMvOZ7UDEO/XBddIp4y0BmM.woff") format("woff");
font-display: swap;
}

body {
font-family: 'DaoLiTi', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
}

4. 新字体使用及效果


<span style={{fontFamily: 'DaoLiTi'}}>我是一只小青蛙,最爱说笑话</span>


5. 注意点


DaoLiTi这个名字是可以自定义的,但是在样式文件中的无论什么地方使用的时候都不能少了引号。


此外就是除了“我是一只小青蛙,最爱说笑话”,其他字是没有刀隶体效果的!


作者:慕仲卿
来源:juejin.cn/post/7305359585107738661
收起阅读 »

已经好久没有尽全力做某件事情了

好像,我已经好久没有尽全力做某件事情了... 以至于,我已经有点遗忘了,尽全力做某件事情的感觉了... 你还记得,最近一次(或者是现在)你尽全力做某件事情的感受吗? 如果要描述这种感觉的话,我想会包含以下几点吧。 第一,忘我。也就是我们经常说的心流状态。我们在...
继续阅读 »

好像,我已经好久没有尽全力做某件事情了...


以至于,我已经有点遗忘了,尽全力做某件事情的感觉了...


你还记得,最近一次(或者是现在)你尽全力做某件事情的感受吗?


如果要描述这种感觉的话,我想会包含以下几点吧。


第一,忘我。也就是我们经常说的心流状态。我们在尽全力做某件事情的时候,会很容易进入心流状态。只要是做这件事情,然后再加上一些条件反射诱因,就可以很快进入心流状态。就像巴甫洛夫实验那样,摇铃铛,流口水。


第二,印象深刻。这种感觉是印象深刻的,不管这件事情最终是成功还是失败,是硕果累累,还是无疾而终,它都能给我们留下深刻的印象。在很多年之后,虽然我们会遗忘很多的细节,但起码,我们还能回忆起来这件事,我做过,我全情投入过。


第三,不留遗憾。有一种后悔,是在自己做了某种决策之后导致失败。有一种比这个程度更深的后悔,是自己没有全情投入地去做某件事情。失败,只是懊悔,没做,才是遗憾


插图1.png


过去几年,我的工作好无聊,很难受,非常迷茫。感觉我是在原地踏步,虚耗光阴。


今年慢慢走出来了,但还是没有达到最好的状态。


我觉得,自己被很多无形的枷锁禁锢住了。虽然不是难以呼吸般的艰难,却也不能纵情放肆高呼般的畅快。


我觉得,自己像是一个控火师。现实、责任、理智让我严格控制内心的火,不能让它燃尽自己;而梦想、遗憾、本我又让我尽力呵护它,不能让它熄灭。


今天说多了,还是回归理性。继续蛰伏吧,努力成长,提升自己,才能有资本抓住未来的机会。


加油,奥利给!


----------------【END】----------------


欢迎关注公众号【潜龙在渊灬】(点此扫码关注),收获程序员职场相关经验、提升工作效率和职场效能、结交更多人脉。


作者:潜龙在渊灬
来源:juejin.cn/post/7311500203433377842
收起阅读 »

你真的懂 Base64 吗?短链服务常用的 Base62 呢?

web
黄山的冬天,中国 (© Hung Chung Chih/Shutterstock) Base64 前端的日常开发中可能会接触到 Base64 ,比如页面上的小图片,在为了节省网络资源的情况下,通常会将图片转为 Base64 直接嵌入到 html 或者 css ...
继续阅读 »

黄山的冬天,中国 (© Hung Chung Chih/Shutterstock)

Base64


前端的日常开发中可能会接触到 Base64 ,比如页面上的小图片,在为了节省网络资源的情况下,通常会将图片转为 Base64 直接嵌入到 html 或者 css 里。这里,我们先来看看 Base64 是什么,以及 Base64 编码做了什么。


什么是 Base64


Base64 是一种基于64个可打印字符来表示二进制数据的表示方法。一般来说,64个字符包括 A-Z,a-z,0-9 以及 +/ 两个字符(via)。换句话说, Base64 可以将二进制数据转换为这 64 个字符来表示,数据来源可以是图片,也可以是任意的字符串。


Base64 做了些什么


上面提到了 Base64 做的其实就是将二进制数据按照对应规则进行转化,转化的流程其实也很简单



  1. 得到一份二进制数据

  2. 将二进制数据 6位 一组进行划分,并进行适当的补位

  3. 按照 Base64 索引表,将每组数据转换为 Base64 索引表对应的字符,补位位置用 =


下面我们来尝试一下将 aa 这个字符串进行一下 Base64 编码:


第一步:通过 ASCII 表将 aa 转换为对应的二进制表示

可知 a 在 ASCII 表对应的二进制表示为 0110 0001,则 aa 对应为 0110 0001 0110 0001

注:



  1. ascii 参考

  2. 中文等其他字符参考其他表(UTF-8)进行转换即可


第二步:数据分组和补位

按 6 位一组划分后 011000 010110 0001,我们发现数据还少两位,所以我们需要按规则对数据进行补位,补一字节(8位),二进制数据变为 0110 0001 0110 0001 0000 0000,划分为 011000 010110 000100 000000


第三步:查 Base64 索引表 进行转换

参考一张常用的索引表
image.png
可知 aa = 011000 010110 000100 000000 对应为 011000(Y) 010110(W) 000100(E) 000000(补位=) 即 YWE=,我们就得到了 aa 的 Base64 编码为 YWE= ,是不是挺简单。


Base62


说完了 Base64,我们再来聊聊 Base62。在通过 url 传递数据的场景下,通过 Base64 进行编码的数据会带来问题(Base64 中的 / 等可能会带来路径的解析异常),所以在 Base62 里,去掉了 +/= 字符。
说到这里,大家可能觉得就讲完了,Base62 就是丢掉了几个不安全的字符而已,其余转换方法和 Base64 一样,我起初也是这么认为的。


不一样的 Base62 结果


当我尝试对 aa 进行 Base62 编码时,按推算好像也不太对? = 补位已经被去掉了,怎么来做实现呢?
在我找了几个 online 转换进行测试后,发现 aa 对应的 Base62 编码为 6U5 看着跟 Base64 毫无关系对吧,实际上也是的。


揭开面纱看看


查了几份资料以及现有的仓库实现后,我发现 Base62 编码的流程是这样的:



  1. 获得一份二进制数据

  2. 二进制数据 转 10进制

  3. 10进制 转 62进制(按索引表)


我们再来试试将 aa 转 Base62:


第一步:转二进制

aa 对应 0110 0001 0110 0001


第二步:转10进制

0110000101100001 对应十进制为 24929


第三步:转62进制(参考索引表)

image.png
24929 = 6622+3062+5

按表可知,6=6 30=U 5=5,即 6U5


注:



  1. 索引表可以自行更换,并不一定是上图顺序

  2. 现有的仓库实现里,部分只实现了 10进制 转 62进制(base62/base62.js),有的实现了更完整的转换 (tuupola/base62


分析一下原因


说实话我查到的资料不多,但是根据 en.wikipedia.org/wiki/Talk:B… 猜测,文里提到 Base64 后的数据会膨胀到 133% 。Base62 还存在对数据进行压缩的改进,所以采用了这样与 Base64 差别有点大的方式来设计。


总结一下


文章简单的谈了谈 Base64 是什么,怎么实现以及 Base62 的实现,并分析了一下 Base62 设计的初衷,整体来说还是挺简单,希望对你有所帮助 :)


作者:破竹
来源:juejin.cn/post/7311596852264878115
收起阅读 »

Android 图片描边效果

前言 先看下我们阔爱滴海绵宝宝,其原图是一张PNG图片,我们给宝宝加上描边效果,今天我们使用的是图片蒙版技术。 说到蒙版可能很多人想起PS抠图软件,Android上也一样,同一个大树上可能会长出两种果实,但果实的根基是一样的。 什么是蒙版:所谓蒙版是只保留了...
继续阅读 »

前言


先看下我们阔爱滴海绵宝宝,其原图是一张PNG图片,我们给宝宝加上描边效果,今天我们使用的是图片蒙版技术。


fire_78.gif


说到蒙版可能很多人想起PS抠图软件,Android上也一样,同一个大树上可能会长出两种果实,但果实的根基是一样的。


什么是蒙版:所谓蒙版是只保留了alpha通道的一种二维正交投影,简单的说就是你躺在地上,太阳光直射下来,背后的那片就是你的蒙版。因此,它既不存在三维特征,也不存在色彩特征,只有alpha特征。那只有alpha通道的图片是什么颜色,这块没有具体了解过,但是理论上取决于默认填充色,在Android上最终是白色的,其他平台暂时还没了解。


提取蒙版


Android上提取蒙版比想象的容易,按照以往的思路,我们是要进行图片扫描这里,其实就是把所有颜色的red、green、blue都排除掉,只保留alpha,相当于缩小了通道数,排除采样和缩小图片,当然这个工作量是很大的,尤其是超高清图片。


企业微信20231210-120604@2x.png


Android 上提取蒙版,只需要把原图绘制到alpha通道的Bitmap上


bms = decodeBitmap(R.mipmap.mm_07);
bmm = Bitmap.createBitmap(bms.getWidth(), bms.getHeight(), Bitmap.Config.ALPHA_8);
Canvas canvas = new Canvas(bmm);
canvas.drawBitmap(bms, 0, 0, null);

蒙版绘制


蒙版绘制和其他Bitmap绘制是有差异的,ARGB_8888和RGB_565等色彩格式的图片,其本身是具备颜色的,但是蒙版图片不一样,他没有颜色,所以你绘制的时候,bitmap的颜色是你画笔Paint的填充色,突然想到可以做一个人体扫描的动画效果或者人体热力图。


canvas.drawBitmap(bmm, x, y, paint);

扩大蒙版(影子)


要让蒙版比比原图大,理论上是需要等比例放大蒙版在平移,还有一种方式是进行偏移绘制,我们这里使用偏移绘制。当然,这里取一定360,保证尽可能每个方向都有偏移,这是看到的外国人的算法。至于step>0 但是也要控制粒度,太小可能绘制次数太多,太大可能有些边缘做不到偏移。


for (int i = 0; i < 360; i += step) {
float x = width * (float) Math.cos(Math.toRadians(i));
float y = width * (float) Math.sin(Math.toRadians(i));
canvas.drawBitmap(bmm, x, y, paint);
}

闪烁效果


我们价格颜色闪烁的效果,其实很简单,也不是本篇重要的部份,其实就是在色彩中间插入透明色,然后定时闪烁。


int index = -1;
int max = 15;
int[] colors = new int[max];
final int[] highlightColors = {0xfff00000,0,0xffff9922,0,0xff00ff00,0};

public void shake() {
index = 0;
for (int i = 0; i < max; i+=2) {
colors[i] = highlightColors[i % highlightColors.length];
}
postInvalidate();
}

总结


本篇到这里就结束了,希望利用蒙版+偏移做出更多东西。


全部代码


public class ViewHighLight extends View {
final Bitmap bms; //source 原图
final Bitmap bmm; //mask 蒙版
final Paint paint;
final int width = 4;
final int step = 15; // 1...45
int index = -1;
int max = 15;
int[] colors = new int[max];
final int[] highlightColors = {0xfff00000,0,0xffff9922,0,0xff00ff00,0};
public ViewHighLight(Context context) {
super(context);
bms = decodeBitmap(R.mipmap.mm_07);
bmm = Bitmap.createBitmap(bms.getWidth(), bms.getHeight(), Bitmap.Config.ALPHA_8);
Canvas canvas = new Canvas(bmm);
canvas.drawBitmap(bms, 0, 0, null);
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
}

private Bitmap decodeBitmap(int resId) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inMutable = true;
return BitmapFactory.decodeResource(getResources(), resId, options);
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// draw blur shadow

for (int i = 0; i < 360; i += step) {
float x = width * (float) Math.cos(Math.toRadians(i));
float y = width * (float) Math.sin(Math.toRadians(i));
canvas.drawBitmap(bmm, x, y, paint);
}
canvas.drawBitmap(bms, 0, 0, null);

if(index == -1){
return;
}
index++;
if(index > max +1){
return;
}
if(index >= max){
paint.setColor(Color.TRANSPARENT);
}else{
paint.setColor(colors[index]);
}
postInvalidateDelayed(200);
}


public void shake() {
index = 0;
for (int i = 0; i < max; i+=2) {
colors[i] = highlightColors[i % highlightColors.length];
}
postInvalidate();
}
}

作者:时光少年
来源:juejin.cn/post/7310786575213920306
收起阅读 »

关于解构赋值的一些意想不到的坑

web
今天群里有个人问了一个问题,问我们为什么报错,代码如下 var arr = [1,2,3,4,5,6,7,8,9,10] for(let i = arr.length; i > 0; i--) { let index = Math.floor(M...
继续阅读 »

今天群里有个人问了一个问题,问我们为什么报错,代码如下


var arr = [1,2,3,4,5,6,7,8,9,10]
for(let i = arr.length; i > 0; i--) {
let index = Math.floor(Math.random() * i)
// console.log(i, index);
[arr[i-1], arr[index]] = [arr[index], arr[i-1]]
// ReferenceError: Cannot access 'index' before initialization
}
console.log(arr)

我乍一看,这能报错?不应该啊,怎么能呢,于是我特意复制下来跑了一下,嘿,还真是


关于ReferenceError: Cannot access 'xxx' before initialization的报错,往往和暂时性死区有关,但我看了看顺序,是先定义的index啊,没有错。


抱着求知的心态,上网查了一些文章,都没有提到这种问题,于是只能去看ecma规范,但对不起,我英语太差了,我就连在哪都没找到。


后来我想起自己曾经遇到过类似的问题,只不过是在解构对象的时候遇到的


大概是这样的操作


let a = xxx

({
a: this.options.a,
b: this.options.b,
......
} = /*一个对象*/ ?? {})

当时也报了错,我就想起来了



js中是允许语句不使用;结尾的,许多小伙伴可能养成了这个习惯,虽然不写分号有时候确实很爽很轻松,也是一些企业的规范,但是等到流泪的时候可就知道惨了。



只需要将上述结构赋值的代码的前面一个语句加上分号,就可以解决这个问题


相当于把一个语句拆开了


什么?你问我怎么就成同一个语句了?


我没记错的话,js在执行的时候是会忽略换行符的吧,或者说这个换行符没那么重要,所以我们平时看到的很多库打包出来的min.js文件都是只有一行的然后通过分号分割语句。


如果把上述代码换行内容忽视掉,就变成了这个样子,只放了部分代码


    let index = Math.floor(Math.random() * i)[arr[i-1], arr[index]] = [arr[index], arr[i-1]]

这不报错谁报错啊,根据等号从右到左的运算顺序,不就是访问了暂时性死区嘛


所以加上分号,问题就引刃而解了。


想当年因为先学c++和java的缘故,总是养成写分号的习惯,在切图仔里面似乎成为了一个异类,现在知道了吧,养成写分号的好习惯啊,呜呜呜呜


最后附上修改后的代码


var arr = [1,2,3,4,5,6,7,8,9,10]
for(let i = arr.length; i > 0; i--) {
let index = Math.floor(Math.random() * i);
// console.log(i, index);
[arr[i-1], arr[index]] = [arr[index], arr[i-1]]
// ReferenceError: Cannot access 'index' before initialization
}
console.log(arr)

更好笑的是,这哥们明显是想通过console测试一下的,结果他发现console就不报错,不console就报错,这是真的折磨哈哈哈哈哈哈


养成语句加分号的好习惯!!
从你我做起!


作者:笑心
来源:juejin.cn/post/7311681326712995903
收起阅读 »

【内存泄漏】图解 Android 内存泄漏

内存泄漏简介 关于内存泄露的定义,想必大家已经烂熟于心了,简单一点的说,就是程序占用了不再使用的内存空间。 那在 Android 里边,怎样的内存空间是程序不再使用的内存空间呢? 这就不得不提到内存泄漏相关的检测了,拿 LeakCanary 的检测举例,销毁的...
继续阅读 »

内存泄漏简介


关于内存泄露的定义,想必大家已经烂熟于心了,简单一点的说,就是程序占用了不再使用的内存空间


那在 Android 里边,怎样的内存空间是程序不再使用的内存空间呢?


这就不得不提到内存泄漏相关的检测了,拿 LeakCanary 的检测举例,销毁的 ActivityFragment 、fragment ViewViewModel 如果没有被回收,当前应用被判定为发生了内存泄漏。LeakCanary 会生成引用链相关的日志信息。


有了引用链的日志信息,我们就可以开开心心的解决内存泄漏问题了。但是除了查看引用链还有更好的解决方式吗?答案是有的,那就是通过画图来解决,会更加的直观形象~


一个简单的例子


如下是一个 Handler 发生内存泄露的例子:


class MainActivity : ComponentActivity() {

private val handler = LeakHandler(Looper.getMainLooper())

override fun onCreate(savedInstanceState: Bundle?) {
// other code

// 发送了一个 100s 的延迟消息
handler.sendMessageDelayed(Message.obtain(), 100_000L)
}

private fun doLog() {
Log.d(TAG, "doLog")
}

private inner class LeakHandler(looper: Looper): Handler(looper) {
override fun handleMessage(msg: Message) {
doLog()
}
}
}

因为 LeakHandler 是一个内部类,持有了外部类 MainActivity 的引用。


其在如下场景会发生内存泄漏:onCreate 执行之后发送了一个 100s 的延迟消息,在 100s 以内旋转屏幕,MainActivity 进行了重建,上一次的 MainActivity 还被 LeakHandler 持有无法释放,导致内存泄露的产生。


引用链图示


如下是执行完 onCreate() 方法之后的引用链图示:


memory_leak_1.png



简单说明一下引用链 0 的位置,这里为了简化,直接使用 GCRoot 代替了,实际上存在这样的引用关系:GCRoot → ActivityThread → Handler → MessageQueue → Message。简单了解一下即可,不太清楚的话也不会影响接下来的问题解决。
同时,为了简单明了,文中还简化了一些相关但是不重要的引用链关系,比如 HandlerMessageQueue 的引用。



100s 以内旋转屏幕之后,引用链图示变成这样了:


memory_leak_2.png


之前的 Activity 被释放,引用链 4 被切断了。我们可以很清晰的看到,由于 LeakHandlerMainActivity 的强引用(引用链2),LeakHandler 间接被 GCRoot 节点强引用,导致 MainActivity 没办法释放。



MainActivity 指的是旋转屏幕之前的 Activity,不是旋转屏幕之后新建的



那么很显然,接下来我们就需要对引用链 0、1 或 2 进行一些操作了,这样才可以让 MainActivity 得到释放。


解决方案


方案一:


onDestroy() 的时候调用 removeCallbacksAndMessages(null) ,该方法会进行两步操作:移除该 Handler 发送的所有消息,并将 Message 回收到 MessagePool


override fun onDestroy() {
super.onDestroy()
handler.removeCallbacksAndMessages(null)
}

此时,在图示上的表现,就是移除了引用链 0 和 1。如此 MainActivity 就可以正常回收了。


memory_leak_3.png


方案二:


使用弱引用 + 静态内部类的方式,我们同样也可以解决这个内存泄漏问题,想必大家已经非常熟悉了。



这里再简单说明一下弱引用 + 静态内部类的原理:
弱引用的回收机制:在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只被弱引用引用的对象,不管当前内存空间足够与否,都会回收它的内存。
静态内部类:静态内部类不会持有外部类的引用,也就不会有 LeakHandler 直接引用 MainActivity 的情况出现



代码实现上,只需要传入 MainActivity 的弱引用给 NoLeakHandler 即可:


private val handler = NoLeakHandler(WeakReference(this), Looper.getMainLooper())

private class NoLeakHandler(
private val activity: WeakReference<MainActivity>,
looper: Looper
): Handler(looper) {
override fun handleMessage(msg: Message) {
activity.get()?.doLog()
}
}

下图中,2.1 表示的是 NoLeakHandlerWeakReference 的强引用,NoLeakHandler 通过 WeakReference 间接引用到了 MainActivity。我们可以很清楚的看到,在旋转屏幕之后,MainActivity 此时只被一个弱引用引用了(引用链 2.2,使用虚线表示),是可以正常被回收的。


memory_leak_4.png


另一个简单的例子


再来一个静态类持有 Activity 的例子,如下是关键代码:


object LeakStaticObject {
val activityCollection: MutableList<Activity> = mutableListOf()
}

class MainActivity : ComponentActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
// other code

activityCollection.add(this)
}
}

正常运行的情况下,存在如下的引用关系:


memory_leak_5.png


在旋转屏幕之后,会发生内存泄漏,因为之前的 MainActivity 还被 GCRoot 节点(LeakStaticObject)引用着。那要怎么解决呢?相信大家已经比较清楚了,要么切断引用链 0,要么将引用链 0 替换成一个弱引用。由于比较简单,这里就不再单独画图说明了。


总结


本文介绍了一种使用画图来解决常见内存泄露的方法,会比直接查看引用链更加清晰具体。同时,其相比于一些归纳常见内存泄漏的方法,会更加的通用,很大程度上摆脱了对内存泄漏场景的强行记忆。


通过画图,找到引用路径之后,在引用链的某个节点上进行操作,切断强引用或者将强引用替换成弱引用,以此来解决问题。


总的来说,对于常见的内存泄漏场景,我们都可以通过画图来解决,本文为了介绍简便,使用了比较简单常见的例子,实际上,遇到复杂的内存泄漏,也可以通过画图的方式来解决。当然,熟练之后,省略画图的操作,也是可以的。


REFERENCE


wikipedia 内存泄漏


Excalidraw — Collaborative whiteboarding made easy


How LeakCanary works - LeakCanary


理解Java的强引用、软引用、弱引用和虚引用 - 掘金


作者:很好奇
来源:juejin.cn/post/7313242069099872306
收起阅读 »

技术并不一定比其他高级

这里的技术可以是计算机或者别的什么技术。当然首先指的是开发技术。 很长一段时间里,就我个人有一种天然的技术高于其他的感觉,虽未明示,但骨子里有一种谦虚的傲慢。认为开发高于产品、设计、测试等。 不知道其他人是否有过这种想法。或者我觉得那种典型的技术人的思维,要么...
继续阅读 »

这里的技术可以是计算机或者别的什么技术。当然首先指的是开发技术。


很长一段时间里,就我个人有一种天然的技术高于其他的感觉,虽未明示,但骨子里有一种谦虚的傲慢。认为开发高于产品、设计、测试等。


不知道其他人是否有过这种想法。或者我觉得那种典型的技术人的思维,要么思维不够开放,要么就是一种谦虚的傲慢。


这种傲慢最可怕的是因为漠视掉其它的价值,导致技术人的格局不够。格局不够会看不到更大世界的运行规律,导致无法做出更加正确的决策。有一副著名的对联



能攻心则反侧自消,自古知兵非好战


不审势即宽严皆误,后来治蜀要深思



我觉得这是对格局不够后果最直接准确的描述:宽严皆误。说下我是怎么想到技术并不一定比其他高级的


前几天群里同组的同学@我让我改一篇文章,我才知道公司开始举办一年一度的一年一词活动,开始面向全体征稿。第一年的时候我参与了,但没有选中。第二年没有参与。


我看了下同组同学那篇文章,觉得不怎么滴啊,也是这激起我的求胜心,决定自己写一篇,今年再参加一次。于是那天下午我就写完了初稿。初稿的题目是造轮子,第一句



一般来说说造轮子的都是程序员,因为开发从某个意义上来讲就是在重复造轮子,亦如太阳底下没有新鲜事,也亦如任何历史都是当代史。



为了能够被选上,我认真又做了几次修改,重读了几次。我有点福至心灵的发现我在开发上犯了一个错误,就是我似乎一直认为技术才是最重要的,不管是有意无意的,这是事实。但是开发从某个意义上来讲就是在重复造轮子,正如太阳底下没有新鲜事,也亦如任何历史都是当代史,技术和其他一样,也是重复的单元。


要想尽快搞清楚技术,只要找到其中代表性的重复单元就可以了。而实际也早就有人总结了这些单元,比如功能单元的代表各种ui组件库,业务单元的代表往往是对功能单元的再加工。好比功能单元是原型机,而业务单元是定制化。


前几天也看到一篇文章的题目《不过是享受了互联网的十年红利期而已》。遂想到行业高速发展时期,技术实现是第一位的;但行业进入饱和期,产品、运营应该才是创造利润的关键。正如计算机底层技术开发人员,过了计算机技术爆发的年代,反倒不如业务开发赚的多。


这一切的一切不过是特定时期的表现。技术并不一定比其他高级,现在就是技术不再处于第一优先级的时刻。


(本文完)


作者:通往自由之路
来源:juejin.cn/post/7304598711991795750
收起阅读 »

程序员IT行业,外行眼里高收入人群,内行人里的卷王

程序员 一词,在我眼里其实是贬义词。因为我的其他不是这行的亲朋友好友,你和他们说,你是一名程序员· 他们 第一刻板影响就是,秃头,肥胖,宅男,油腻,不修边幅 反正给人一种不干净,不好形象,,,,不知道什么时候开始网络上也去渲染这些,把程序员和这些联想在一起了。...
继续阅读 »

程序员 一词,在我眼里其实是贬义词。因为我的其他不是这行的亲朋友好友,你和他们说,你是一名程序员·


他们 第一刻板影响就是,秃头,肥胖,宅男,油腻,不修边幅 反正给人一种不干净,不好形象,,,,不知道什么时候开始网络上也去渲染这些,把程序员和这些联想在一起了。


回到正题,我们来聊聊,我们光鲜靓丽背后高工资。


是的作为一名程序员,在许多人的眼中,IT行业收入可能相对较高。这是不可否认的。但是,在这个职业领域里,我们所面对的困难和挑战也是非常的多。


持续的学习能力



程序员需要持续地学习,不断地掌握新技能。



随着技术的不断发展,我们需要不断地学习新的编程语言、开发框架、工具以及平台等等,这是非常耗费精力和时间的。每次技术更新都需要我们拿出宝贵的时间,去研究、学习和应用。


尤其在公司用项目中,用到新技术需要你在一定时间熟悉并使用时候,那个时候你自己只有硬着头皮,一边工作一边学习,如果你敢和老板说不会,那,,,我是没那个胆量


高强度抗压力



ICU,猝死,996说的就是我们



我们需要经常探索和应对极具挑战性的编程问题。解决一个困难的问题可能需要我们数小时,甚至数天的时间,这需要我们付出大量的勤奋和耐心。有时候,我们会出现程序崩溃或运行缓慢的情况,当然,这种情况下我们也需要更多的时间去诊断和解决问题,


还要保持高效率工作,同时保证项目的质量。有时候,团队需要在紧张的时间内完成特别复杂的任务,这就需要我们花费更多的时间和精力来完成工作。


枯燥乏味生活


由于高强度工作,和加班,我们的业余生活可能不够丰富,社交能力也会不足


高额经济支出


程序员IT软件行业,一般都是在一线城市工作,或者新一线,二线城市,所以面临的经济支持也会比较大,


最难的就是房租支持,生活开销。


一线城市工作,钱也只能在一线城市花,有时候也是真的存不了什么钱,明明自己什么也没有额外支持干些什么,可是每月剩下的存款也没有多少


短暂职业生涯


“背负黑匣子”:程序员的工作虽然看似高薪,但在实际工作中,我们承担了处理复杂技术问题的重任。


“独自快乐?”:程序员在工作中经常需要在长时间内独立思考和解决问题,缺乏团队合作可能会导致孤独和焦虑。


“冰山一角的技能”:程序员需要不断学习和更新技能,以适应快速变化的技术需求,这需要不断的自我修炼和付出时间。


“猝不及防的技术变革”:程序员在处理技术问题时需要时刻保持警惕,技术日新月异,无法预测的技术变革可能会对工作带来极大的压力。


“难以理解的需求”:客户和管理层的需求往往复杂而难以理解,程序员需要积极与他们沟通,但这也会给他们带来额外的挑战和压力。


“不请自来的漏洞”:安全漏洞是程序员必须不断面对和解决的问题,这种不确认的风险可能会让程序员时刻处于焦虑状态。


“高度聚焦的任务”:程序员在处理技术问题时需要集中精力和关注度,这通常需要长时间的高度聚焦,导致他们缺乏生活平衡。


“时刻警觉”:程序员在工作中必须时刻提醒自己,保持警觉和冷静,以便快速识别和解决问题。


“枯燥重复的任务”:与那些高度专业的技术任务相比,程序员还需要完成一些枯燥重复的工作,这让他们感到无聊和疲惫。


“被误解的天才”:程序员通常被视为是天才,但是他们经常被误解、被怀疑,这可能给他们的职业带来一定的负担。


程序员IT,也是吃年轻饭的,不是说你年龄越大,就代表你资历越深。 职业焦虑30岁年龄危机 越来越年轻化


要么转行,要么深造,


Yo,这是程序员的故事

高薪却伴随着堆积如山的代码

代码缺陷层出不穷,拯救业务成了千里马

深夜里加班的钟声不停响起

与bug展开了无尽的搏斗,时间与生命的角逐

接口返回的200,可前端却丝毫未见变化

HTTP媒体类型不支持,世界一团糟

Java Spring框架调试繁琐,无尽加班真让人绝望

可哪怕压力再大,我们还是核心开发者的倡导者

应用业务需要承载,才能取得胜利的喝彩

程序员的苦工是世界最稀缺的产业

我们不妥协,用技术创意为行业注入新生命

我们坚持高质量代码的规范

纵使压力山大,我们仍能跨过这些阻碍

这是程序员的故事。

大家有什么想法和故事吗,在工作中是否也遇到了和我一样的问题?


作者:程序员三时
来源:juejin.cn/post/7232120266805526584
收起阅读 »

如何实现一个可视化数据转换的小工具

web
前端开发过程中,经常有两个不同的组件、或者两个不同的业务模块需要对接,但是双方提供的数据结构又不一致的场景。这个时候就需要一个转换器来实现两个模块无缝对接,通过这个转换器的处理达到数据结构一致的目的。 基本需求梳理 场景中两个相同或者不同数据结构对象,对象是...
继续阅读 »

前端开发过程中,经常有两个不同的组件、或者两个不同的业务模块需要对接,但是双方提供的数据结构又不一致的场景。这个时候就需要一个转换器来实现两个模块无缝对接,通过这个转换器的处理达到数据结构一致的目的。


基本需求梳理



  • 场景中两个相同或者不同数据结构对象,对象是任意数据结构的,对接场景下数据结构是固定的

  • 针对当前场景下转换器处理是相同的

  • 尽量图形化操作就可以换成配置以及预览效果


设计基本思路


实现思路



  • 两个不同的数据结构可以通过字段映射的方式来取值和设置值,实现数据的对接

  • 取值路径和设置值的路径规则最好一条一条保存下来,作为转换器的规则描述

  • 取值路径和设置值路径可以通过lodash的get和set实现

  • 可视化操作可以通过json树的渲染及操作来实现


设计实现思路草图


我根据自己的想法大致设计一下交互方式如下:


数据转换器 (3).png


实现步骤


json树操作


我找了一下josn树操作的组件,发现react-json-view挺不错的;可以实现值的复制、选择;但是选择是针对叶子节点的,所以这里我使用复制功能来实现,无论是叶子节点还是非叶子节点都可以复制到,(enableClipboard)复制的时候获取当前的path信息即可。另外path多了一层根路径默认是'root',如果不想要操作保存的时候去掉即可。


如下图所示,鼠标悬浮点击这个icon图标来选中key,取其路径值保存起来
image.png


// 复制的操作
const enableClipboard = (copy) => {
const { namespace } = copy;
if (namespace?.length === 1) { // 复制的根元素
setSourceKeyPath([])
} else {
const curNamespace = namespace.splice(1, namespace.length - 1);
setSourceKeyPath(curNamespace)
}
}

路径保存


路径映射保存在数组里。


转换器处理


转换器只需要根据规则数组map处理一下每条规则进行对原数据和目标数据进行取值设置值操作就可以了


// dataSource 是规则数组
// targetData 是目标数据
// sourceData 是源数据
dataSource.map((item) => {
set(targetData, item.targetKeyPath, get(sourceData, item.sourceKeyPath))
return item
});

效果预览


image.png
另外数据随机生成我使用的是@faker-js/faker


部署到网站


已经部署到我的个人网站timesky.top/data-conver…


作者:TimeSky
来源:juejin.cn/post/7313242069099954226
收起阅读 »

Antd Upload上传后还想要拖拽?行~开干!玩的就是真实

web
原创 陈夏杨 / 叫叫技术团队 基于 Antd Upload 实现拖拽(兼容低版本) 背景 哎呀!我想要的是边上传边调整位置可以拖拽的那种效果!哪种?就那种。(这句话是不是似曾相识,没错,到这里作为开发还没领悟那就要面壁思过了~哈哈。)话不多说,目前 Ant...
继续阅读 »

原创 陈夏杨 / 叫叫技术团队



基于 Antd Upload 实现拖拽(兼容低版本)


背景


哎呀!我想要的是边上传边调整位置可以拖拽的那种效果!哪种?就那种。(这句话是不是似曾相识,没错,到这里作为开发还没领悟那就要面壁思过了~哈哈。)话不多说,目前 Antd 的 Upload 组件并未支持拖拽排序功能,社区也没有发现可以借鉴的 demo,于是我们调研后采用 react-dnd 和 react-sortable-hoc 实现“就那种”效果。


技术分析


其实这个需求之前我们已经有一些基于 react-dnd 技术的沉淀,但是都是基于 html 的 dom 元素进行 ref 绑定操作,并没有搭配 Upload 组件。如下 demo


拖拽2.gif
以上是基于 react-dnd 实现的场景拖拽,直接上核心代码


const [, dragPreview] = useDrag({
collect: (monitor: DragSourceMonitor) => ({
isDragging: monitor.isDragging()
}),
// item 中包含 index 属性,则在 drop 组件 hover 和 drop 是可以根据第一个参数获取到 index 值
item: { type: 'page', index }
});
const [, drop] = useDrop({
accept: 'page',
hover(item: { type: string; index: number }, monitor: any) {
if (!ref.current) {
return;
}
const dragIndex = item.index;
const hoverIndex = index;

// 拖拽元素下标与鼠标悬浮元素下标一致时,不进行操作
if (dragIndex === hoverIndex) {
return;
}
// 确定屏幕上矩形范围
const hoverBoundingRect = ref.current!.getBoundingClientRect();
// 获取中点垂直坐标
const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
// 确定鼠标位置
const clientOffset = monitor.getClientOffset();
// 获取距顶部距离
const hoverClientY = (clientOffset as any).y - hoverBoundingRect.top;
/**
* 只在鼠标越过一半物品高度时执行移动。
* 当向下拖动时,仅当光标低于50%时才移动。
* 当向上拖动时,仅当光标在50%以上时才移动。
* 可以防止鼠标位于元素一半高度时元素抖动的状况
*/

// 向下拖动
if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
return;
}
// 向上拖动
if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
return;
}

// 执行 move 回调函数
moveCard(dragIndex, hoverIndex);

/**
* 如果拖拽的组件为 Box,则 dragIndex 为 undefined,此时不对 item 的 index 进行修改
* 如果拖拽的组件为 Card,则将 hoverIndex 赋值给 item 的 index 属性
*/

if (item.index !== undefined) {
item.index = hoverIndex;
}
}
});
dragPreview(drop(ref));

因为 Upload 组件上传文件是通过自身 fileList api 底层消化处理的,所以处理起来比较麻烦,还好 Antd 4.16.0 版本Upload 提供的 itemRender 解决了这个问题。但是对于低于这个版本的后续也有解决方案。


注意:如果 Antd 用的是最新的版本 5.x.x,其实官网也提供了 集成 dnd-kit 来实现对上传列表拖拽排序


技术选型


市面上可以实现拖拽排序的库有很多,比如 SortableJS、react-dnd、react-beautiful-dnd、react-sortable-hoc 等。
我列了一个表格:


优点缺点
SortableJS足够轻量级,而且功能齐全React 中使用起来并不是太方便,而且它的配置项写起来实在不太符合 React 的思维
react-dnd库小,贴合 react 拖拽场景多行拖拽不理想,react-beautiful-dnd 库比较大赖于HTML5 拖放 API,这有一些严重的限制
react-beautiful-dnd动画效果和细节非常完美同上
react-sortable-hoc多行拖拽优势很明显(相比其他库大多依赖于HTML5拖放API ,这有一些严重的限制。例如,如果你需要支持触摸设备,如果你需要锁定拖动到一个轴上,或者想在节点排序时设置动画,事情就会变得很棘手。React-sortablehoc 旨在提供一组简单的 higher-order 组件来填补这些空白。如果您正在寻找一种 dead-simple ,mobile-friendly 的方式来向列表中添加可排序功能,那么您就在正确的位置了。)列表过长,需要滑动的列表中拖拽时,滑动后位置不匹配会发生偏移

实践中还会有一些踩坑:



  • reat-dnd 在项目中快速拖拽时一直报错,"Invariant Violation: Expected targetIds to be registered. "在他的 issue 中也有很多人反应这个问题,虽然有修复过但是并没有完全修复,在 overStack 中也并没有找到好的解决方案。

  • 这里以我们实践结论,从 react-dnd、react-sortable-hoc 两个库进行讲解


react-dnd


概念

react dnd 是一组 react 高阶组件,使用的时候只需要使用对应的 API 将目标组件进行包裹,即可实现拖动或接受拖动元素的功能。
在拖动的过程中,不需要开发者自己判断拖动状态,只需要在传入的配置对象中各个状态属性中做对应处理即可,因为react-dnd 使用了 redux 管理自身内部的状态。
值得注意的是,react-dnd 并不会改变页面的视图,它只会改变页面元素的数据流向,因此它所提供的拖拽效果并不是很炫酷的,我们可能需要写额外的视图层来完成想要的效果,但是这种拖拽管理方式非常的通用,可以在任何场景下使用,非常适合用来定制。


安装

npm i react-dnd

核心 API

介绍实现拖拽和数据流转的核心 API ,这里以 hook 为例。


DndProvider

使用 react-dnd 需要最外层元素加 DndProvider ,DndProvider 的本质是一个由 React.createContext 创建一个上下文的容器(组件),用于控制拖拽的行为,数据的共享,类似于 react-redux 的 Provider。


import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';

<DndProvider backend={HTML5Backend}>组建模块</DndProvider>;

Backend

react dnd 将 DOM 事件相关的代码独立出来,将拖拽事件转换为 react dnd 内部的 redux action。由于拖拽发生在 H5 的时候是 ondrag,发生在移动设备的时候是由 touch 模拟,react dnd 将这部分单独抽出来,方便后续的扩展,这部分就叫做 backend。它是 dnd 在 DOM 层的实现。



  • react-dnd-html5-backend : 用于控制 html5 事件的 backend

  • react-dnd-touch-backend : 用于控制移动端 touch 事件的 backend

  • react-dnd-test-backend : 用户可以参考自定义 backend


useDrag

让 DOM 实现拖拽能力的构子


import React from 'react';
import { useDrag } from 'react-dnd';

export default function Player() {
// 第一个返回值是一个对象,主要放一些拖拽物的状态。后面会介绍,先不管
// 第二个返回值:顾名思义就是一个Ref,只要将它注入到DOM中,该DOM就会变成一个可拖拽的DOM
const [_, dragRef] = useDrag(
{
type: 'Player', // 给拖拽物命名,后面用于分辨该拖拽物是谁,支持string和symbol
item: { id: 1 } // 拖拽物所携带的数据,让后面一些事件可以拿到数据,已达到交互的目的
},
[]
);
// 注入Ref,现在这个DOM就可以拖拽了
return <div ref={dragRef} />;
}

返回三个参数

第一个返回值是一个对象 表示关联在拖拽过程中的变量,需要在传入 useDrag 的规范方法的 collect 属性中进行映射绑定,比如:isDraging, canDrag 等

第二个返回值 代表拖拽元素的 ref

第三个返回值 代表拖拽元素拖拽后实际操作到的 dom

传入两个参数



  • 第一个参数,是一个对象,是用于描述了drag 的配置信息,常用属性


type指定元素的类型,只有类型相同的元素才能进行 drop 操作
item元素在拖拽过程中,描述该对象的数据,如果指定的是一个方法,则方法会在开始拖拽时调用,并且需要返回一个对象来描述该元素。
end(item, monitor)拖拽结束的回调函数,item 表示拖拽物的描述数据,monitor 表示一个 DragTargetMonitor 实例
isDragging(monitor)判断元素是否在拖拽过程中,可以覆盖Monitor对象中的 isDragging方法,monitor 表示一个 DragTargetMonitor 实例
canDrag(monitor)判断是否可以拖拽的方法,需要返回一个 bool 值,可以覆盖 Monitor 对象中的 canDrag 方法,与 isDragging 同理,monitor 表示一个 DragTargetMonitor 实例
collect它应该返回一个描述状态的普通对象,然后返回以注入到组件中。它接收两个参数,一个 DragTargetMonitor 实例和拖拽元素描述信息item


  • 第二个参数是一个数组,表示对方法更新的约束,只有当数组中的参数发生改变,才会重新生成方法,基于react 的 useMemo 实现


useDrop

实现拖拽物放置的钩子


import { useDrop } from 'react-dnd';

export const Dustbin = () => {
const [_, dropRef] = useDrop({
accept: ['Player'], // 指明该区域允许接收的拖放物。可以是单个,也可以是数组
// 里面的值就是useDrag所定义的type

// 当拖拽物在这个拖放区域放下时触发,这个item就是拖拽物的item(拖拽物携带的数据)
drop: (item) => {}
});

// 将ref注入进去,这个DOM就可以处理拖拽物了
return <div ref={dropRef}></div>;
};

返回两个参数

第一个返回值是一个对象 表示关联在拖拽过程中的变量,需要在传入 useDrag 的规范方法的 collect 属性中进行映射绑定。

第二个返回值 代表拖拽元素的 ref
传入一个参数
用于描述drop的配置信息,常用属性


accept指定接收元素的类型,只有类型相同的元素才能进行 drop 操作
drop(item, monitor)有拖拽物放置到元素上触发的回调方法,item 表示拖拽物的描述数据,monitor 表示 DropTargetMonitor 实例,该方法返回一个对象,对象的数据可以由拖拽物的 monitor.getDropResult 方法获得
hover(item, monitor)当拖住物在上方 hover 时触发,item 表示拖拽物的描述数据,monitor表示 DropTargetMonitor 实例,返回一个 bool 值
canDrop(item, monitor)判断拖拽物是否可以放置,item 表示拖拽物的描述数据,monitor 表示 DropTargetMonitor 实例,返回一个 bool 值

API 数据流转

image.png


react-sortable-hoc


概念

react-sortable-hoc 是一个基于 React 的拖拽排序组件,它可以让你轻松地实现拖拽排序功能。它提供了一系列的 API,可以让你自定义拖拽排序的行为。它支持拖拽排序的单个列表和多个列表,以及拖拽排序的可视化。


安装

npm install react-sortable-hoc

引入

import { SortableContainer, SortableElement, arrayMove, SortableHandle } from 'react-sortable-hoc';

核心 API


  • sortableContainer 是所有可排序元素的容器

  • sortableElement 是每个可渲染元素的容器

  • sortableHandle 是定义拖拽手柄的容器

  • arrayMove 主要用于将移动后的数据排列好后返回


SortableContainer HOC

PropertyTypeDefaultDescription
axisStringy项目可以水平、垂直或网格排序。可能值:x、y 或 xy
lockAxisString如果您愿意,可以在排序时将移动锁定在轴上。这不是 HTML5 拖放所能做到的。可能值:x 或 y。
helperClassString您可以提供一个要添加到 sortable helper 的类,以向其添加一些样式
transitionDurationNumber300元素移动位置时转换的持续时间。{ 39d 要禁用 @661 }
keyboardSortingTransitionDurationNumbertransitionDuration在键盘排序期间移动辅助对象时转换的持续时间。如果要禁用键盘排序助手的转换,请将其设置为 0。如果未定义,则默认为 transitionDuration 设置的值
keyCodesArray{lift: [32],drop: [32],cancel: [27],up: [38, 37],down: [40, 39]}一个包含每个 keyboard-accessible 操作的键码数组的对象。
pressDelayNumber0如果您希望元素只在按下一段时间后才可排序,请更改此属性。mobile 的一个合理的默认值是 200。不能与 distance 属性一起使用。
pressThresholdNumber5忽略冲压事件之前要容忍的移动像素数。
distanceNumber0如果您希望元素只在被拖动一定数量的像素之后才变得可排序。不能与 pressDelay 属性一起使用。
shouldCancelStartFunctionFunction此函数在排序开始前调用,可用于在排序开始前以编程方式取消排序。默认情况下,如果事件目标是 input、textarea、select 或 option,它将取消排序。
updateBeforeSortStartFunction在排序开始之前调用此函数。它可以返回一个 promise,允许您在排序开始之前运行异步更新(比如 setState )。function ({ node, index, collection, isKeySorting }, event )
onSortStartFunction开始排序时调用的回调。function({ node, index, collection, isKeySorting }, event ) |
onSortMoveFunction当光标移动时在排序期间调用的回调。function ( event )|
onSortOverFunction在向上移动时调用的回调。function ({ index, oldIndex, newIndex, collection, isKeySorting }, e )
onSortEndFunction排序结束时调用的回调。function ({ oldIndex, newIndex, collection, isKeySorting }, e )
useDragHandleBooleanfalse如果您使用的是SortableHandleHOC,请将其设置为true
useWindowAsScrollContainerBooleanfalse如果需要,可以将window设置为滚动容器
hideSortableGhostBooleantrue是否 auto-hide 重影元素。默认情况下,为了方便起见,React Sortable List 将自动隐藏当前正在排序的元素。如果要应用自己的样式,请将此设置为 false。
lockToContainerEdgesBooleanfalse您可以将可排序元素的移动锁定到其父元素 SortableContainer
lockOffsetOffsetValue*|[OffsetValue*,OffsetValue*]"50%"当 lockToContainerEdges 设置为 true 时,这将控制可排序辅助对象与其父对象 SortableContainer 的上/下边缘之间的偏移距离。百分比值相对于当前正在排序的项的高度。如果您希望指定不同的行为来锁定容器的顶部和底部,您还可以传入 array(例如:["0%", "100%"])。
getContainerFunction返回可滚动容器元素的可选函数。此属性默认为 SortableContainer 元素本身或(如果 useWindowAsScrollContainer 为真)窗口。使用此函数指定一个自定义容器对象(例如,这对于与某些第三方组件(如 FlexTable )集成非常有用)。这个函数被传递给一个参数(即 wrappedInstanceReact 元素),它应该返回一个 DOM 元素。
getHelperDimensionsFunctionFunction可选的function ({ node, index, collection }),它应该返回 SortableHelper 的计算维度。有关详细信息,请参见默认实现 |
helperContainerHTMLElement | 函数document.body默认情况下,克隆的可排序帮助程序将附加到文档正文。使用此属性可指定要附加到可排序克隆的其他容器。接受 HTMLElement 或返回 HTMLElement 的函数,该函数将在排序开始之前调用
disableAutoscrollBooleanfalse拖动时禁用自动滚动

如何使用

直接上demo


import React from 'react';
import { arrayMove, SortableContainer, SortableElement } from 'react-sortable-hoc';

// 需要拖动的元素的容器
const SortableItem = SortableElement((value) => <div>{value}</div>);
// 整个元素排序的容器
const SortableList = SortableContainer((items) => {
return items.map((value, index) => {
return <SortableItem key={`item-${index}`} index={index} value={value} />;
});
});

// 拖动排序组件
class SortableComponnet extends React.Component {
state = {
items: ['1', '2', '3']
};
onSortEnd = ({ oldIndex, newIndex }) => {
this.setState(({ items }) => {
arrayMove(items, oldIndex, newIndex);
});
};
render() {
return (
<div>
<SortableList
distance={5}
axis={'xy'}
items={this.state.items}
helperClass={style.helperClass}
onSortEnd={this.onSortEnd}
/>

</div>

);
}
}

export default SortableComponnet;

在上面的示例中,我们使用 SortableContainer 组件容纳了一组可拖拽排序的元素,使用 SortableElement 组件包裹了每个元素,并且实现了 onSortEnd 回调函数,以便在拖拽排序完成后更新状态。


效果展示

拖拽3.gif


踩坑

image.png



解决:这种报错的解决方法都是 SortableElement 和 SortableContainer 返回组件时外面都要单独在包一个 html 容器标签, 例子是包了个 <div>



结果导向


Antd 版本 4.16.0及以上


使用 react-dnd 搭配 Upload 上传组件 itemRender api实现

注:这里主要基于 react-dnd 实现,Antd 5.x.x 官网有基于 dnd-kit 来实现对上传列表拖拽排序。


const Box: React.FC<BoxProps> = ({ children, index, className, onClick, moveCard }) => {
const ref = useRef<HTMLDivElement>(null);

const [, dragPreview] = useDrag({
collect: (monitor: DragSourceMonitor) => ({
isDragging: monitor.isDragging()
}),
// item 中包含 index 属性,则在 drop 组件 hover 和 drop 是可以根据第一个参数获取到 index 值
item: { type: 'page', index }
});
const [, drop] = useDrop({
accept: 'page',
hover(item: { type: string; index: number }) {
if (!ref.current) {
return;
}
const dragIndex = item.index;
const hoverIndex = index;
// 自定义逻辑处理

// 执行 move 回调函数
moveCard(dragIndex, hoverIndex);
}
});
dragPreview(drop(ref));

return (
<div ref={ref} className={className} onClick={onClick}>
{children}
</div>

);
};

使用

使用 Antd Upload itemRender


image.png


Antd 版本低于 4.16.0


使用 react-sortable-hoc 库实现


  • SortableItem


const SortableItem = SortableElement((params: SortableItemParams) => (
<div>
<UploadList
locale={{ previewFile: '预览图片', removeFile: '删除图片' }}
showDownloadIcon={false}
showRemoveIcon={params?.props?.disabled}
listType={params?.props?.listType}
onPreview={params.onPreview}
onRemove={params.onRemove}
items={[params.item]}
/>

</div>

));


  • SortableList


const SortableList = SortableContainer((params: SortableListParams) => {
return (
<div className='sortableList'>
{params.items.map((item, index) => (
<SortableItem
key={`${item.uid}`}
index={index}
item={item}
props={params.props}
onPreview={params.onPreview}
onRemove={params.onRemove}
/>

))}
{/* 这里是上传组件,设置最大限制后超出隐藏 */}
<Upload {...params.props} showUploadList={false} onChange={params.onChange}>
{params.props.children}
</Upload>
</div>

);
});


  • DragHoc


const DragHoc: React.FC<Props> = memo(
({ onChange: onFileChange, axis, onPreview, onRemove, ...props }) => {
const fileList = props.fileList || [];
const onSortEnd = ({ oldIndex, newIndex }: SortEnd) => {
onFileChange({ fileList: arrayMove(fileList, oldIndex, newIndex) });
};

const onChange = ({ fileList: newFileList }: UploadChangeParam) => {
onFileChange({ fileList: newFileList });
};

return (
<>
<SortableList
// 当移动 1 之后再触发排序事件默认是0会导致无法触发图片的预览和删除事件
distance={1}
items={fileList}
onSortEnd={onSortEnd}
axis={axis || 'xy'}
helperClass='SortableHelper'
props={props}
onChange={onChange}
onRemove={onRemove}
onPreview={onPreview}
/>

</>

);
}
);

使用方式

可直接替换掉 Upload 组件,props 不变。
如果项目中有已经封装好的 Upload 上传组件,尽量不改变原有逻辑代码前提下,更希望以插件的形式按需加载?方案:



可以剔除掉 SortableList 中 SortableContainer 包裹的 Upload 组件(这一步经过实践是可行的,说 Upload UploadList 都要被 SortableContainer 包裹,否走会重复上传和拖拽失败?目前我是没遇到,重复上传是因为 Upload 组件 showUploadList 拖拽场景下必须是 false )



使用案例:( isDrag 表示需要拖拽场景,继而加载)



{isDrag && (
<DragHoc
accept={accept}
axis={axis}
showUploadList={{ showRemoveIcon }}
fileList={fileList}
onChange={(e) =>
{
if (onChange) {
onChange(e);
}
}}
onPreview={preview}
onRemove={remove}
listType={listType}
/>

)}

注:当然也可以用 react-dnd 来实现,只是多行拖拽流畅性较差。感兴趣也可以试试


踩坑

图片按钮点击无效

在 Antd 的 Upload 组件中,图片墙上会有「预览」、「删除」等按钮,但是在 react-sortable-hoc 的逻辑中,只要我点击了图片,就会触发图片的拖拽函数,无法触发图片上的各种按钮,所以需要在 SortableList 上重新设置一下 distance 属性,设置成 1 即可。

官网:



If you'd like elements to only become sortable after being dragged a certain number of pixels. Cannot be used in conjunction with the pressDelay prop.

(如果您希望元素仅在拖动一定数量的像素后才可排序。不能与 pressDelay 道具一起使用。默认为 0)



上传图片一直 uploading

image.png

原因:当图片列表发生变化,整个 sortable 容器被删除并重新渲染,导致请求失效。


解决方案:



需要将 SortableItem,SortableList 写在 React.FC 外面,每次组件内部 state 发生变化,不会重新执行 SortableContainer 和 SortableElement 方法,就可以让可排序容器里面的元素自动只更新需要改变的 DOM 元素,而不会整个删除并重新渲染了。



图片的 disabled 状态失效

原因:SortableContainer 包裹的组件对 Upload 图片和上传进行了拆分处理,所以需要单独去控制预览和删除按钮


const SortableItem = SortableElement((params: SortableItemParams) => (
<div>
<UploadList
locale={{ previewFile: '预览图片', removeFile: '删除图片' }}
showDownloadIcon={false}
showRemoveIcon={params?.props?.disabled} //这里需要单独控制
listType={params?.props?.listType}
onPreview={params.onPreview}
onRemove={params.onRemove}
items={[params.item]}
/>

</div>

));

效果展示

拖拽1.gif


参考文献



作者:叫叫技术团队
来源:juejin.cn/post/7312634879987122186
收起阅读 »

分裂的国产自研手机系统,究竟苦了谁

2023 年可谓是国产自研手机操作系统百花齐放的一年,在华为官宣 HarmonyOS NEXT 开发者预览版本,不在兼容 Android 之后,小米、vivo 分别官宣了自己的操作系统。 10 月 26 日,雷布斯宣布小米澎湃 OS,耗时 7 年将 MIUI、...
继续阅读 »

2023 年可谓是国产自研手机操作系统百花齐放的一年,在华为官宣 HarmonyOS NEXT 开发者预览版本,不在兼容 Android 之后,小米、vivo 分别官宣了自己的操作系统。


10 月 26 日,雷布斯宣布小米澎湃 OS,耗时 7 年将 MIUI、Vela、Mina、车机 OS 四个系统进行了合并,想打造一个万物互联的操作系统。其中 Vela 我们之前也接触过,一句话,坑是在太多了,替小米系统工程师的头发感到惋惜。hahaha


11 月 1 日,vivo 副总裁宣布自主研发的蓝河操作系统 BlueOS,并且 vivo 自研蓝河操作系统不兼容安卓应用,未来也不会兼容。在加上 OPPO 的潘塔纳尔系统,国内的主流手机厂商都拥有了自己的操作系统。


为什么国产手机厂商都想打造自己的操作系统?



  • 想脱离 Android 的控制,华为和中兴的前车之鉴,给手机厂商们敲响了警钟,都在卧薪尝胆,开发自己的操作系统

  • 顺应时代,想抓住万物互联的红利,打造自己的物联网生态,就必须要有自己的万物互联操作系统


现在国产手机操作的系统的竞争进入了白热化的状态,我们来看一下全球操作系统市场份额。



现在全球手机操作系统市场份额是被谷歌的 Android 和苹果的 iOS 基本垄断了,其中 Android 系统占据了 38.27%,我们在来看一下这些 Android 市场份额,被那些手机厂商瓜分了。



Android 系统分别被 Sansung、Xiaomi、Oppo、Vivo 瓜分了,但是它们都受制于美国的控制,如果想摆脱美国的控制,那么偷摸自研就是唯一的出路。


相比于自研新系统,最难的是生态的建立,而生态的建立就需要各个行业的人,为你的新系统开发软件,如果没有人为你的系统开发办公软件那就不能用于工作,如果没有人为你的系统开发游戏、音乐等等软件,那么就不能用于娱乐,一个既不能用于办公,也不能用于娱乐的操作系统,试问那个消费者会去使用。


当国内操作系统都开始卷自研的操作系统时,其中最苦的无疑是移动开发者,以前只有 Android 的时候,他们只需要针对 Andriod 不同版本,不同机型做适配,现在他们需要学习自研操作系统开发语言,为不同系统、不同的设备去做更多版本的适配。


而仅仅是 Android 设备的碎片化情况,已经让 Android 开发者苦不堪言,我们用一张图看一下 Android 操作系统分裂情况(来自网上)。



作为一名深耕多年的 Android 开发者,我已经在这个世界上找不到任何一句话来形容 Android 的现状了,仅用网传的一张图向 Android 致敬。


![]( img.hi-dhl.com/kick_androi… -1-. png)


混乱的自研国产操作系统是否会走 Android 的老路,这个无法确定,但是确定的是,每个手机厂商都有自研的手机操作系统,必然会导致手机操作系统生态的更加碎片化。对于开发者而言无疑是一个重磅炸弹。


以前国内手机厂商主要使用 Android 操作系统,开发者只需要对 Andriod 不同版本,不同机型做适配,现在各个厂商都推出自研操作系统,使得移动开发者需要花费更多的时间,为自研的操作系统,不同的设备进行更多版本的适配。


虽然我也是移动操作系统资深的受害者,但是不得不为国内厂商敢于开发自己的操作系统鼓掌,但是因此造成手机操作系统的生态更加碎片化。其中受苦的无疑是开发者和用户,也期望国内系统有大一统的那一天。


国产自研操作系统加油,移动开发者加油,Android 开发者顶住。


另外根据调研机构 Counterpoint 发布的数据显示,华为 HarmonyOS 出货量仅次于苹果 iOS,晋升成为了全球第三大操作系统。



华为 HarmonyOS 占全球手机操作系统市场份额的 2%,占中国的份额的 8%,位居全球第三大操作的系统,也希望华为能跟 iOS 一样。



  • 0 广告

  • 不预装及推广第三方软件

  • 手机上的软件都可以卸载


至于广告问题,就不奢望和苹果一样几乎无广告了,任何一家公司只要感受到了广告带来的暴利,就不可能轻易砍掉。


作者:程序员DHL
来源:juejin.cn/post/7313042225864540214
收起阅读 »

三年前端还不会配置Nginx?刷完这篇就够了

一口气看完,比自学强十倍! 什么是Nginx Nginx是一个开源的高性能HTTP和反向代理服务器。它可以用于处理静态资源、负载均衡、反向代理和缓存等任务。Nginx被广泛用于构建高可用性、高性能的Web应用程序和网站。它具有低内存消耗、高并发能力和良好的...
继续阅读 »

一口气看完,比自学强十倍!



Nginx_logo-700x148.png


什么是Nginx


Nginx是一个开源的高性能HTTP和反向代理服务器。它可以用于处理静态资源、负载均衡、反向代理和缓存等任务。Nginx被广泛用于构建高可用性、高性能的Web应用程序和网站。它具有低内存消耗、高并发能力和良好的稳定性,因此在互联网领域非常受欢迎。


为什么使用Nginx



  1. 高性能:Nginx采用事件驱动的异步架构,能够处理大量并发连接而不会消耗过多的系统资源。它的处理能力比传统的Web服务器更高,在高并发负载下表现出色。

  2. 高可靠性:Nginx具有强大的容错能力和稳定性,能够在面对高流量和DDoS攻击等异常情况下保持可靠运行。它能通过健康检查和自动故障转移来保证服务的可用性。

  3. 负载均衡:Nginx可以作为反向代理服务器,实现负载均衡,将请求均匀分发给多个后端服务器。这样可以提高系统的整体性能和可用性。

  4. 静态文件服务:Nginx对静态资源(如HTML、CSS、JavaScript、图片等)的处理非常高效。它可以直接缓存静态文件,减轻后端服务器的负载。

  5. 扩展性:Nginx支持丰富的模块化扩展,可以通过添加第三方模块来提供额外的功能,如gzip压缩、SSL/TLS加密、缓存控制等。


如何处理请求


Nginx处理请求的基本流程如下:



  1. 接收请求:Nginx作为服务器软件监听指定的端口,接收客户端发来的请求。

  2. 解析请求:Nginx解析请求的内容,包括请求方法(GET、POST等)、URL、头部信息等。

  3. 配置匹配:Nginx根据配置文件中的规则和匹配条件,决定如何处理该请求。配置文件定义了虚拟主机、反向代理、负载均衡、缓存等特定的处理方式。

  4. 处理请求:Nginx根据配置的处理方式,可能会进行以下操作:



    • 静态文件服务:如果请求的是静态资源文件,如HTML、CSS、JavaScript、图片等,Nginx可以直接返回文件内容,不必经过后端应用程序。

    • 反向代理:如果配置了反向代理,Nginx将请求转发给后端的应用服务器,然后将其响应返回给客户端。这样可以提供负载均衡、高可用性和缓存等功能。

    • 缓存:如果启用了缓存,Nginx可以缓存一些静态或动态内容的响应,在后续相同的请求中直接返回缓存的响应,减少后端负载并提高响应速度。

    • URL重写:Nginx可以根据配置的规则对URL进行重写,将请求从一个URL重定向到另一个URL或进行转换。

    • SSL/TLS加密:如果启用了SSL/TLS,Nginx可以负责加密和解密HTTPS请求和响应。

    • 访问控制:Nginx可以根据配置的规则对请求进行访问控制,例如限制IP访问、进行身份认证等。



  5. 响应结果:Nginx根据处理结果生成响应报文,包括状态码、头部信息和响应内容。然后将响应发送给客户端。


什么是正向代理和反向代理


2020-03-08-5ce95a07b18a071444-20200308191723379.png


正向代理


是指客户端通过代理服务器发送请求到目标服务器。客户端向代理服务器发送请求,代理服务器再将请求转发给目标服务器,并将服务器的响应返回给客户端。正向代理可以隐藏客户端的真实IP地址,提供匿名访问和访问控制等功能。它常用于跨越防火墙访问互联网、访问被封禁的网站等情况。


反向代理


是指客户端发送请求到代理服务器,代理服务器再将请求转发给后端的多个服务器中的一个或多个,并将后端服务器的响应返回给客户端。客户端并不直接访问后端服务器,而是通过反向代理服务器来获取服务。反向代理可以实现负载均衡、高可用性和安全性等功能。它常用于网站的高并发访问、保护后端服务器、提供缓存和SSL终止等功能。


nginx 启动和关闭


进入目录:/usr/local/nginx/sbin
启动命令:./nginx
重启命令:nginx -s reload
快速关闭命令:./nginx -s stop
有序地停止,需要进程完成当前工作后再停止:./nginx -s quit
直接杀死nginx进程:killall nginx

目录结构


[root@localhost ~]# tree /usr/local/nginx
/usr/local/nginx

├── client_body_temp                 # POST 大文件暂存目录
├── conf                             # Nginx所有配置文件的目录
│   ├── fastcgi.conf                 # fastcgi相关参数的配置文件
│   ├── fastcgi.conf.default         # fastcgi.conf的原始备份文件
│   ├── fastcgi_params               # fastcgi的参数文件
│   ├── fastcgi_params.default      
│   ├── koi-utf
│   ├── koi-win
│   ├── mime.types                   # 媒体类型
│   ├── mime.types.default
│   ├── nginx.conf                   #这是Nginx默认的主配置文件,日常使用和修改的文件
│   ├── nginx.conf.default
│   ├── scgi_params                 # scgi相关参数文件
│   ├── scgi_params.default  
│   ├── uwsgi_params                 # uwsgi相关参数文件
│   ├── uwsgi_params.default
│   └── win-utf
├── fastcgi_temp                     # fastcgi临时数据目录
├── html                             # Nginx默认站点目录
│   ├── 50x.html                     # 错误页面优雅替代显示文件,例如出现502错误时会调用此页面
│   └── index.html                   # 默认的首页文件
├── logs                             # Nginx日志目录
│   ├── access.log                   # 访问日志文件
│   ├── error.log                   # 错误日志文件
│   └── nginx.pid                   # pid文件,Nginx进程启动后,会把所有进程的ID号写到此文件
├── proxy_temp                       # 临时目录
├── sbin                             # Nginx 可执行文件目录
│   └── nginx                       # Nginx 二进制可执行程序
├── scgi_temp                       # 临时目录
└── uwsgi_temp                       # 临时目录

配置文件nginx.conf


# 启动进程,通常设置成和cpu的数量相等
worker_processes  1;

# 全局错误日志定义类型,[debug | info | notice | warn | error | crit]
error_log  logs/error.log;
error_log  logs/error.log  notice;
error_log  logs/error.log  info;

# 进程pid文件
pid        /var/run/nginx.pid;

# 工作模式及连接数上限
events {
    # 仅用于linux2.6以上内核,可以大大提高nginx的性能
    use   epoll;

    # 单个后台worker process进程的最大并发链接数
    worker_connections  1024;

    # 客户端请求头部的缓冲区大小
    client_header_buffer_size 4k;

    # keepalive 超时时间
    keepalive_timeout 60;

    # 告诉nginx收到一个新连接通知后接受尽可能多的连接
    # multi_accept on;
}

# 设定http服务器,利用它的反向代理功能提供负载均衡支持
http {
    # 文件扩展名与文件类型映射表义
    include       /etc/nginx/mime.types;

    # 默认文件类型
    default_type  application/octet-stream;

    # 默认编码
    charset utf-8;

    # 服务器名字的hash表大小
    server_names_hash_bucket_size 128;

    # 客户端请求头部的缓冲区大小
    client_header_buffer_size 32k;

    # 客户请求头缓冲大小
    large_client_header_buffers 4 64k;

    # 设定通过nginx上传文件的大小
    client_max_body_size 8m;

    # 开启目录列表访问,合适下载服务器,默认关闭。
    autoindex on;

    # sendfile 指令指定 nginx 是否调用 sendfile 函数(zero copy 方式)来输出文件,对于普通应用,
    # 必须设为 on,如果用来进行下载等应用磁盘IO重负载应用,可设置为 off,以平衡磁盘与网络I/O处理速度
    sendfile        on;

    # 此选项允许或禁止使用socke的TCP_CORK的选项,此选项仅在使用sendfile的时候使用
    #tcp_nopush     on;

    # 连接超时时间(单秒为秒)
    keepalive_timeout  65;


    # gzip模块设置
    gzip on;               #开启gzip压缩输出
    gzip_min_length 1k;    #最小压缩文件大小
    gzip_buffers 4 16k;    #压缩缓冲区
    gzip_http_version 1.0; #压缩版本(默认1.1,前端如果是squid2.5请使用1.0)
    gzip_comp_level 2;     #压缩等级
    gzip_types text/plain application/x-javascript text/css application/xml;
    gzip_vary on;

    # 开启限制IP连接数的时候需要使用
    #limit_zone crawler $binary_remote_addr 10m;

    # 指定虚拟主机的配置文件,方便管理
    include /etc/nginx/conf.d/*.conf;


    # 负载均衡配置
    upstream aaa {
        # 请见上文中的五种配置
    }


   # 虚拟主机的配置
    server {

        # 监听端口
        listen 80;

        # 域名可以有多个,用空格隔开
        server_name www.aaa.com aaa.com;

        # 默认入口文件名称
        index index.html index.htm index.php;
        root /data/www/sk;

        # 图片缓存时间设置
        location ~ .*.(gif|jpg|jpeg|png|bmp|swf)${
            expires 10d;
        }

        #JS和CSS缓存时间设置
        location ~ .*.(js|css)?${
            expires 1h;
        }

        # 日志格式设定
        #$remote_addr与 $http_x_forwarded_for用以记录客户端的ip地址;
        #$remote_user:用来记录客户端用户名称;
        #$time_local:用来记录访问时间与时区;
        #$request:用来记录请求的url与http协议;
        #$status:用来记录请求状态;成功是200,
        #$body_bytes_sent :记录发送给客户端文件主体内容大小;
        #$http_referer:用来记录从那个页面链接访问过来的;
        log_format access '$remote_addr - $remote_user [$time_local] "$request" '
        '$status $body_bytes_sent "$http_referer" '
        '"$http_user_agent" $http_x_forwarded_for';

        # 定义本虚拟主机的访问日志
        access_log  /usr/local/nginx/logs/host.access.log  main;
        access_log  /usr/local/nginx/logs/host.access.404.log  log404;

        # 对具体路由进行反向代理
        location /connect-controller {

            proxy_pass http://127.0.0.1:88;
            proxy_redirect off;
            proxy_set_header X-Real-IP $remote_addr;

            # 后端的Web服务器可以通过X-Forwarded-For获取用户真实IP
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header Host $host;

            # 允许客户端请求的最大单文件字节数
            client_max_body_size 10m;

            # 缓冲区代理缓冲用户端请求的最大字节数,
            client_body_buffer_size 128k;

            # 表示使nginx阻止HTTP应答代码为400或者更高的应答。
            proxy_intercept_errors on;

            # nginx跟后端服务器连接超时时间(代理连接超时)
            proxy_connect_timeout 90;

            # 后端服务器数据回传时间_就是在规定时间之内后端服务器必须传完所有的数据
            proxy_send_timeout 90;

            # 连接成功后,后端服务器响应的超时时间
            proxy_read_timeout 90;

            # 设置代理服务器(nginx)保存用户头信息的缓冲区大小
            proxy_buffer_size 4k;

            # 设置用于读取应答的缓冲区数目和大小,默认情况也为分页大小,根据操作系统的不同可能是4k或者8k
            proxy_buffers 4 32k;

            # 高负荷下缓冲大小(proxy_buffers*2)
            proxy_busy_buffers_size 64k;

            # 设置在写入proxy_temp_path时数据的大小,预防一个工作进程在传递文件时阻塞太长
            # 设定缓存文件夹大小,大于这个值,将从upstream服务器传
            proxy_temp_file_write_size 64k;
        }

        # 动静分离反向代理配置(多路由指向不同的服务端或界面)
        location ~ .(jsp|jspx|do)?$ {
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_pass http://127.0.0.1:8080;
        }
    }
}

location


location指令的作用就是根据用户请求的URI来执行不同的应用


语法


location [ = | ~ | ~* | ^~ ] uri {...}


  • [ = | ~ | ~* | ^~ ]:匹配的标识



    • ~~*的区别是:~区分大小写,~*不区分大小写

    • ^~:进行常规字符串匹配后,不做正则表达式的检查



  • uri:匹配的网站地址

  • {...}:匹配uri后要执行的配置段


举例


location = / {
    [ configuration A ]
}
location / {
    [ configuration B ]
}
location /sk/ {
    [ configuration C ]
}
location ^~ /img/ {
    [ configuration D ]
}
location ~* .(gif|jpg|jpeg)$ {
    [ configuration E ]
}


  • = / 请求 / 精准匹配A,不再往下查找

  • / 请求/index.html匹配B。首先查找匹配的前缀字符,找到最长匹配是配置B,接着又按照顺序查找匹配的正则。结果没有找到,因此使用先前标记的最长匹配,即配置B。

  • /sk/ 请求/sk/abc 匹配C。首先找到最长匹配C,由于后面没有匹配的正则,所以使用最长匹配C。

  • ~* .(gif|jpg|jpeg)$ 请求/sk/logo.gif 匹配E。首先进行前缀字符的查找,找到最长匹配项C,继续进行正则查找,找到匹配项E。因此使用E。

  • ^~ 请求/img/logo.gif匹配D。首先进行前缀字符查找,找到最长匹配D。但是它使用了^~修饰符,不再进行下面的正则的匹配查找,因此使用D。


单页面应用刷新404问题


    location / {
        try_files $uri $uri/ /index.html;
    }

配置跨域请求


server {
    listen   80;
    location / {
        # 服务器默认是不被允许跨域的。
        # 配置`*`后,表示服务器可以接受所有的请求源(Origin),即接受所有跨域的请求
        add_header Access-Control-Allow-Origin *;
        
        add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
        add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
        
        # 发送"预检请求"时,需要用到方法 OPTIONS ,所以服务器需要允许该方法
        # 给OPTIONS 添加 204的返回,是为了处理在发送POST请求时Nginx依然拒绝访问的错误
        if ($request_method = 'OPTIONS') {
            return 204;
        }
    }
}

开启gzip压缩


    # gzip模块设置
    gzip on;               #开启gzip压缩输出
    gzip_min_length 1k;    #最小压缩文件大小
    gzip_buffers 4 16k;    #压缩缓冲区
    gzip_http_version 1.0; #压缩版本(默认1.1,前端如果是squid2.5请使用1.0)
    gzip_comp_level 2;     #压缩等级
    
    # 设置什么类型的文件需要压缩
    gzip_types text/plain application/x-javascript text/css application/xml;
    
    # 用于设置使用Gzip进行压缩发送是否携带“Vary:Accept-Encoding”头域的响应头部
    # 主要是告诉接收方,所发送的数据经过了Gzip压缩处理
    gzip_vary on;

总体而言,Nginx是一款轻量级、高性能、可靠性强且扩展性好的服务器软件,适用于搭建高可用性、高性能的Web应用程序和网站。


作者:日月之行_
来源:juejin.cn/post/7270153705877241890
收起阅读 »

生病、裁员、假合同与开发者。聊聊最近遇到的事儿

今天这篇文章,咱们不聊技术哈,聊聊这两天我经历的事和一些感悟吧。 大致是三个事情: 生病: 自己和家人生病,在医院看到的形形色色 裁员: 之前黑马的老同事被裁干净了,当很多人在面对裁员时的一些表现和反应 假合同: 入职没有拿合同,工作两年仲裁的时候,发现签的...
继续阅读 »

今天这篇文章,咱们不聊技术哈,聊聊这两天我经历的事和一些感悟吧。


大致是三个事情:



  1. 生病: 自己和家人生病,在医院看到的形形色色

  2. 裁员: 之前黑马的老同事被裁干净了,当很多人在面对裁员时的一些表现和反应

  3. 假合同: 入职没有拿合同,工作两年仲裁的时候,发现签的合同变了


生病:在医院看到的形形色色


这几天可真是被折磨的够呛,先是我支原体感染,持续咳嗽了两周,结果肺结节了😭



然后是我闺女开始咳嗽,陪着一起打针。在医院看到很多小孩,整个医院几乎是满满的。这是凌晨12点的医院挂号处:



随处可见的都是疲惫不堪的父母和孩子。


最厉害的,还是这位大姐,一边照顾孩子,一边还在写代码(偷偷看了一眼是 java 😂😂)



再加上这几天北方的大雪。想一想:晚上陪孩子打针到深夜,第二天早上冒着大雪去上班。晚上加完班之后再陪孩子来医院。真的是无限的疲惫。


裁员:普通人面临被裁时的反应


我有幸在 7 月的时候经历过一次大裁员,也是在那个时候离开的。


当时有很多没有被裁的同事,一是庆幸自己可以继续留下,毕竟现在行情是真的不好。二是也寄希望于年后可以继续开启招生。


结果等来的不是行情变好而是持续的裁员。


本身裁员嘛,没什么可说的。但是一个朋友的经历以及想法却值得我们进行深思。


事情是这样的:



这位朋友是月初的时候被谈离职,离职协议也都谈完了,赔偿打折,月底走人。


但是在最后这段时间里面,公司却安排了他大量的出差以及无意义的工作。


所以我就跟他说:“这你还干啊?天天熬到那么晚?”


他跟说我:“多表现表现,万一公司可以回心转意,让我继续留下呢?”



这不禁让我想起来之前大家都在说的骆驼祥子,祥子到死的时候都认为这一切是自己不够努力所导致的。


对于公司而言,公司不会养任何已经没有了价值的人,就像我这个朋友一样。同时也不会让一个人的价值过大,无法控制,就像最近 “董宇辉小作文事件” 一样。


对于大多数的普通人而言,最悲惨的就是:当别人拿起屠刀要杀你的时候,你所想到的不是奋起反抗,而是希望可以通过祈求来得到别人的宽恕和原谅。


假合同: 仲裁时才发现入职合同变了


这是一个同学跟我说的,事情是这样的:



这位同学在入职的时候签订了劳动合同,但是当时公司以统一盖章为由,没有及时把劳动合同给他,后来他也忽略了这个事情。


直到前段时间,因为公司长期拖欠工资他发起仲裁,公司拿出来当时签订的劳动合同


发现在他的签字页之外的合同内容,都发生了变化


他的薪资变成了底薪3000,其他的全部是项目奖金的形式。



算是吃了一个亏。


一点小感悟


对于我们这种普通人而言,在工作中大多数的时候真的是处于弱势地位。这与技术好坏并无关系。很多技术很好的人依然充满着焦虑,时刻担心着 35 岁危机的事情。


所以说,打工只是过程,想办法赚钱才是目的。


最后祝大家都可以身体健康,拿到满意的 offer!


作者:程序员Sunday
来源:juejin.cn/post/7312722655224627212
收起阅读 »

我强烈建议你也做抖音个人ip

勇于尝试可以有更多可能性。想来很多人来稀土掘金分享的初心也是尝试吧。 笔者之前说过笔者已经是迈入35的人,且说实话是一线的大头兵。生活在北京多年,或者活跃在互联网多年,35岁这个魔咒几年前就开始困扰,乃至如今加重笔者的焦虑。 前几年公司受到国家宏观经济政策影响...
继续阅读 »

勇于尝试可以有更多可能性。想来很多人来稀土掘金分享的初心也是尝试吧。


笔者之前说过笔者已经是迈入35的人,且说实话是一线的大头兵。生活在北京多年,或者活跃在互联网多年,35岁这个魔咒几年前就开始困扰,乃至如今加重笔者的焦虑。


前几年公司受到国家宏观经济政策影响,业绩受到极大影响,连带着年终奖缩水。紧接着三年疫情,原本水深火热的日子,一下雪上加霜。


处在一个难以看到前途的年纪和环境中,笔者和大多数人一样尝试寻找出路。笔者也在其他文章中说过,笔者来稀土掘金的目的就是探索出路。


正如文章开头写的勇于尝试可以有更多可能性。在稀土掘金分享这段时间,写了一些文章,也有很多思考。或许也正是如此让笔者想明白想清楚很多事。


几年前雷军说过一句话:处在风口之上,猪也能飞起来。很长时间里,笔者都在寻找风口,笔者管风口叫做趋势性,但事实上笔者一直没有抓住这个趋势性。后来笔者退而求其次,想到了结构性,想到了应该先察结构性,而后努力提升自己,在关键时候抓住或者等到风口。


或许真的是大趋势不能预测,只能等待。这应该就是所谓的借势。


笔者后来想到,社会是结构性的,人处在结构中,在结构中生存生活,随结构而走而变。结构不是一成不变的,所以有了趋势性。


笔者建议你做抖音个人ip,因为抖音是一个更大的结构性,有着更大的潜在趋势性。正所谓富在术数不在劳身,利在势局不在力耕


因为行业现状,以及技术本身的特性,程序员天然是一群努力且上进的人。与寻常人相比,程序员更适合做个人ip。当然做个人ip不一定能成,但做什么一定能成呢?


笔者建议你做抖音个人ip,还因为抖音已经发展了几年,用户已经足够大,只要你有一定的才华,一定可以被认可,就如同在稀土掘金一样,同样会呈现算法加持的成果。


笔者相信,只要本着用户为本,科技向善的初心,坚持做下去一定会有收获,一定可以找到一群志同道合的人。


(本文完)


作者:通往自由之路
来源:juejin.cn/post/7312404619518853146
收起阅读 »

农业银行算法题,为什么用初中知识出题,这么多人不会?

背景介绍 总所周知,有相当一部分的大学生是不会初高中知识的。 因此,每当那种「初等数学为背景编写的算法题」在笔面出现,舆论往往分成"三大派": 甚至那位说"致敬高考"的同学也搞岔了,高考哪有这么简单,美得你 🤣 这仅仅是初中数学「几何学」中较为简单的知识...
继续阅读 »

背景介绍


总所周知,有相当一部分的大学生是不会初高中知识的


因此,每当那种「初等数学为背景编写的算法题」在笔面出现,舆论往往分成"三大派":


对初高中知识,有清晰记忆


对初高中知识,记忆模糊


啥题?不会!


甚至那位说"致敬高考"的同学也搞岔了,高考哪有这么简单,美得你 🤣


这仅仅是初中数学「几何学」中较为简单的知识点。


抓住大学生对初高中知识这种「会者不难,难者不会」的现状,互联网大厂似乎更喜欢此类「考察初等数学」的算法题。


因为十个候选人,九个题海战术,HOT 100 和剑指 Offer 大家都刷得飞起了。


冷不丁的考察这种题目,反而更能起到"筛选"效果。


但此类算法题,农业银行 并非首创,甚至是同一道题,也被 华为云美的百度 先后出过。


学好初中数学,就能稳拿华为 15 级?🤣




下面,一起来看看这道题。


题目描述


平台:LeetCode


题号:149


给你一个数组 points,其中 points[i]=[xi,yi]points[i] = [x_i, y_i] 表示 X-Y 平面上的一个点。求最多有多少个点在同一条直线上。


示例 1:


输入:points = [[1,1],[2,2],[3,3]]

输出:3

示例 2:


输入:points = [[1,1],[3,2],[5,3],[4,1],[2,3],[1,4]]

输出:4

提示:



  • 1<=points.length<=3001 <= points.length <= 300

  • points[i].length=2points[i].length = 2

  • 104<=xi,yi<=104-10^4 <= x_i, y_i <= 10^4

  • points 中的所有点互不相同


枚举直线 + 枚举统计


我们知道,两点可以确定一条线。


一个朴素的做法是先枚举两点(确定一条线),然后检查其余点是否落在该线中。


为避免除法精度问题,当我们枚举两个点 xxyy 时,不直接计算其对应直线的 斜率截距


而是通过判断 xxyy 与第三个点 pp 形成的两条直线斜率是否相等,来得知点 pp 是否落在该直线上。


斜率相等的两条直线要么平行,要么重合。


平行需要 44 个点来唯一确定,我们只有 33 个点,因此直接判定两条直线是否重合即可。


详细说,当给定两个点 (x1,y1)(x_1, y_1)(x2,y2)(x_2, y_2) 时,对应斜率 y2y1x2x1\frac{y_2 - y_1}{x_2 - x_1}


为避免计算机除法的精度问题,我们将「判定 aybyaxbx=bycybxcx\frac{a_y - b_y}{a_x - b_x} = \frac{b_y - c_y}{b_x - c_x} 是否成立」改为「判定 (ayby)×(bxcx)=(axbx)×(bycy)(a_y - b_y) \times (b_x - c_x) = (a_x - b_x) \times (b_y - c_y) 是否成立」。


将存在精度问题的「除法判定」巧妙转为「乘法判定」。


Java 代码:


class Solution {
public int maxPoints(int[][] points) {
int n = points.length, ans = 1;
for (int i = 0; i < n; i++) {
int[] x = points[i];
for (int j = i + 1; j < n; j++) {
int[] y = points[j];
// 枚举点对 (i,j) 并统计有多少点在该线上, 起始 cnt = 2 代表只有 i 和 j 两个点在此线上
int cnt = 2;
for (int k = j + 1; k < n; k++) {
int[] p = points[k];
int s1 = (y[1] - x[1]) * (p[0] - y[0]);
int s2 = (p[1] - y[1]) * (y[0] - x[0]);
if (s1 == s2) cnt++;
}
ans = Math.max(ans, cnt);
}
}
return ans;
}
}

C++ 代码:


class Solution {
public:
int maxPoints(vector<vector<int>>& points) {
int n = points.size(), ans = 1;
for (int i = 0; i < n; i++) {
vector<int> x = points[i];
for (int j = i + 1; j < n; j++) {
vector<int> y = points[j];
// 枚举点对 (i,j) 并统计有多少点在该线上, 起始 cnt = 2 代表只有 i 和 j 两个点在此线上
int cnt = 2;
for (int k = j + 1; k < n; k++) {
vector<int> p = points[k];
int s1 = (y[1] - x[1]) * (p[0] - y[0]);
int s2 = (p[1] - y[1]) * (y[0] - x[0]);
if (s1 == s2) cnt++;
}
ans = max(ans, cnt);
}
}
return ans;
}
};

Python 代码:


class Solution:
def maxPoints(self, points: List[List[int]]) -> int:
n, ans = len(points), 1
for i, x in enumerate(points):
for j in range(i + 1, n):
y = points[j]
# 枚举点对 (i,j) 并统计有多少点在该线上, 起始 cnt = 2 代表只有 i 和 j 两个点在此线上
cnt = 2
for k in range(j + 1, n):
p = points[k]
s1 = (y[1] - x[1]) * (p[0] - y[0])
s2 = (p[1] - y[1]) * (y[0] - x[0])
if s1 == s2: cnt += 1
ans = max(ans, cnt)
return ans

TypeScript 代码:


function maxPoints(points: number[][]): number {
let n = points.length, ans = 1;
for (let i = 0; i < n; i++) {
let x = points[i];
for (let j = i + 1; j < n; j++) {
// 枚举点对 (i,j) 并统计有多少点在该线上, 起始 cnt = 2 代表只有 i 和 j 两个点在此线上
let y = points[j], cnt = 2;
for (let k = j + 1; k < n; k++) {
let p = points[k];
let s1 = (y[1] - x[1]) * (p[0] - y[0]);
let s2 = (p[1] - y[1]) * (y[0] - x[0]);
if (s1 == s2) cnt++;
}
ans = Math.max(ans, cnt);
}
}
return ans;
};


  • 时间复杂度:O(n3)O(n^3)

  • 空间复杂度:O(1)O(1)


枚举直线 + 哈希表统计


根据「朴素解法」的思路,枚举所有直线的过程不可避免,但统计点数的过程可以优化。


具体的,我们可以先枚举所有可能出现的 直线斜率(根据两点确定一条直线,即枚举所有的「点对」),使用「哈希表」统计所有 斜率 对应的点的数量,在所有值中取个 maxmax 即是答案。


一些细节:在使用「哈希表」进行保存时,为了避免精度问题,我们直接使用字符串进行保存,同时需要将 斜率 约干净(套用 gcd 求最大公约数模板)。


Java 代码:


class Solution {
public int maxPoints(int[][] points) {
int n = points.length, ans = 1;
for (int i = 0; i < n; i++) {
Map<String, Integer> map = new HashMap<>();
// 由当前点 i 发出的直线所经过的最多点数量
int max = 0;
for (int j = i + 1; j < n; j++) {
int x1 = points[i][0], y1 = points[i][1], x2 = points[j][0], y2 = points[j][1];
int a = x1 - x2, b = y1 - y2;
int k = gcd(a, b);
String key = (a / k) + "_" + (b / k);
map.put(key, map.getOrDefault(key, 0) + 1);
max = Math.max(max, map.get(key));
}
ans = Math.max(ans, max + 1);
}
return ans;
}
int gcd(int a, int b) {
return b == 0 ? a : gcd(b, a % b);
}
}

C++ 代码:


class Solution {
public:
int maxPoints(vector<vector<int>>& points) {
int n = points.size(), ans = 1;
for (int i = 0; i < n; i++) {
map<string, int> map;
int maxv = 0;
for (int j = i + 1; j < n; j++) {
int x1 = points[i][0], y1 = points[i][1], x2 = points[j][0], y2 = points[j][1];
int a = x1 - x2, b = y1 - y2;
int k = gcd(a, b);
string key = to_string(a / k) + "_" + to_string(b / k);
map[key]++;
maxv = max(maxv, map[key]);
}
ans = max(ans, maxv + 1);
}
return ans;
}
int gcd(int a, int b) {
return b == 0 ? a : gcd(b, a % b);
}
};

Python 代码:


class Solution:
def maxPoints(self, points):
def gcd(a, b):
return a if b == 0 else gcd(b, a % b)

n, ans = len(points), 1
for i in range(n):
mapping = {}
maxv = 0
for j in range(i + 1, n):
x1, y1 = points[i]
x2, y2 = points[j]
a, b = x1 - x2, y1 - y2
k = gcd(a, b)
key = str(a // k) + "_" + str(b // k)
mapping[key] = mapping.get(key, 0) + 1
maxv = max(maxv, mapping[key])
ans = max(ans, maxv + 1)
return ans

TypeScript 代码:


function maxPoints(points: number[][]): number {
const gcd = function(a: number, b: number): number {
return b == 0 ? a : gcd(b, a % b);
}
let n = points.length, ans = 1;
for (let i = 0; i < n; i++) {
let mapping = {}, maxv = 0;
for (let j = i + 1; j < n; j++) {
let x1 = points[i][0], y1 = points[i][1], x2 = points[j][0], y2 = points[j][1];
let a = x1 - x2, b = y1 - y2;
let k = gcd(a, b);
let key = `${a / k}_${b / k}`;
mapping[key] = mapping[key] ? mapping[key] + 1 : 1;
maxv = Math.max(maxv, mapping[key]);
}
ans = Math.max(ans, maxv + 1);
}
return ans;
};


  • 时间复杂度:枚举所有直线的复杂度为 O(n2)O(n^2);令坐标值的最大差值为 mmgcd 复杂度为 O(logm)O(\log{m})。整体复杂度为 O(n2×logm)O(n^2 \times \log{m})

  • 空间复杂度:O(n)O(n)


总结


虽然题目是以初中数学中的"斜率 & 截距"为背景,但仍有不少细节需要把握。


这也是「传统数学题」和「计算机算法题」的最大差别:



  • 过程分值: 传统数学题有过程分,计算机算法题没有过程分,哪怕思路对了 9090%,代码没写出来,就是 00 分;

  • 数据类型:传统数学题只涉及数值,计算机算法题需要考虑各种数据类型;

  • 运算精度:传统数学题无须考虑运算精度问题,而计算机算法题需要;

  • 判定机制:传统数学题通常给定具体数据和问题,然后人工根据求解过程和最终答案来综合评分,而计算机算法题不仅仅是求解一个具体的 case,通常是给定数据范围,然后通过若个不同的样例,机器自动判断程序的正确性;

  • 执行效率/时空复杂度:传统数学题无须考虑执行效率问题,只要求考生通过有限步骤(或引用定理节省步骤)写出答案即可,计算机算法题要求程序在有限时间空间内执行完;

  • 边界/异常处理:由于传统数学题的题面通常只有一个具体数据,因此不涉及边界处理,而计算机算法题需要考虑数据边界,甚至是对异常的输入输出做相应处理。


可见,传统数学题,有正确的思路基本上就赢了大半,而计算机算法题嘛,有正确思路,也只是万里长征跑了个 400400 米而已。


作者:宫水三叶的刷题日记
来源:juejin.cn/post/7312035308362039346
收起阅读 »

现在工作很难找,不要和年轻人抢饭碗

今年9月结束了和美团三年的合同后,对于步入中年的我,想让自己停一下,对自己的人生价值进行深入而系统的思考。 曾经我有一个梦想,不对,是有很多梦想,比如成为数学家、科学家、飞行员、宇航员或将军,哪一个才是我真正的梦想?那个我愿意用下半生去奋斗去拼搏的梦想? 结合...
继续阅读 »

今年9月结束了和美团三年的合同后,对于步入中年的我,想让自己停一下,对自己的人生价值进行深入而系统的思考。


曾经我有一个梦想,不对,是有很多梦想,比如成为数学家、科学家、飞行员、宇航员或将军,哪一个才是我真正的梦想?那个我愿意用下半生去奋斗去拼搏的梦想?


结合自己的名字叫天文,基于自己所学的专业、过往的经历、资源、国家政策导向和人类社会的发展趋势,我选择了航天方向的梦想,从点燃孩子们的航天梦开始,成为航天领域的企业家。


大环境比想象的差


当我把创业的想法,分享给身边的家人朋友时,几乎所有的人都和说,现在经济环境这么差,工作这么难找,还是要慎重,不要轻易去创业。


因为儿子的托育机构跑路了,我需要协助看娃,即使有合适的工作,也不能立即去上班,所以我基于老婆的公司尝试去招人。


通过招聘,我发现,不管是艺培行业的老师,还是互联网行业的产研人员,失业的比例都很大,很多23年的应届生,毕业后一直找不到工作,干脆国庆期间就离开深圳回老家了。


对于产品经理的实习生岗位,每天都有大量在香港大学、香港中文、香港科技、香港理工、香港城市、新加坡大学、清华大学以及很多国内的985的硕士前来应聘我们的岗位。


WechatIMG157.jpg


社招也是一样,很多名校毕业的,找了好几个月都没有合适的工作,普通学历的产研人员,可能连面试机会都很少。比如前端方向,只要我在 boss 开放招聘,每天都会有几百人主动找我,看都看不过来。


而猎头约我去面试,听到我因为要创业,无一不说现在环境很差,还是好好考虑一下她们推荐的岗位吧。


WechatIMG51.jpg


过去一年我接触过的一些机会


对于已经超过35岁的码农,只要满足企业的用人诉求,即使环境再差,也是有很多工作机会的,在此和大家聊一聊,过去一年我都参加或拒绝了哪些公司的面试。


去年7月,我就多次向领导提了离职,那时也想创业,但没有现在强烈,所以还是认真地找了找工作,那时的面试机会,应该是今年的三倍以上。


首先字节的岗位最多,至少一半猎头推荐的都有字节的岗位,我选择了面试客服体验部,先是在上海的前端总监加了我微信,入职后我向他汇报,管理深圳16人左右的团队,一面的面试官应该是我入职后的下属,但我不是很想去,所以面试随便聊了聊,加了微信,后面还约了个饭。


后续字节的岗位,虽然每周都有猎头给我推荐,但是我都没有答应面试,最近答应约了的面试,因为决定创业了,所以主动取消了,飞书的HR还专门打电话让我再考虑考虑,看来是确实招人,而且他还说有很多方向可以选。


去年我8月我面了美的集团的大前端岗位,通过了四轮,面完HRVP后,已经约好了和CEO面试,但因为我决定留在美团再干一年,做我想做的人工智能,所以我主动取消了面试。这应该是我这几年面试过最高规格的面试,每一轮都是三个以上面试官,入职后直接向CEO汇报,整合集团大前端方向所有的研发,需要管理100多人。


当然也有其他一些高管职位,今年夏天也接到一个猎头推荐我面试贝壳的大前端负责人,直接向CTO汇报,管理200多人的团队,原来的负责人已经提离职,岗位需要保密。


对了,还有编程猫,因为我创业的方向和少儿编程有关,所以也想和他们聊一聊,面试的是高级技术总监,接手联合创始人管理的编程系统研发团队,一面我的是一个大姐,她是web前端负责人,管理20多人,我上来当HR的面,指出了几个明显的bug,让她不高兴了,所以她没让我过,本来还想和他们老板聊一聊。


去年8月中旬后,我基本就不答应面试了,但经常协助一个离职的美团同学面试,期间他面试了字节的多个部门,比如我推荐的web剪映负责人,他因为是web图形学方向的,不够匹配,只通过了两轮。后面他拿到了蔚来手机、小米、阿里、腾讯和万兴科技的offer。


最后,他犹豫是去腾讯还是万兴科技,我们还吃夜宵专门讨论了一下。腾讯他面了三个部门,第三个部门才给的机会,因为他马上要去香港大学MBA,所以选择了腾讯,不带团队。但我建议他去万兴,因为是负责一个事业部,管理近百人团队,对职业发展更好。


今年我好像只答应了两三个面试,其中一个小红书的质效前端负责人,猎头忽悠我年薪可以给到250万到400万,后来又不招了,最近约上了,聊了聊,入职后管理团队应该只有五六人,应该不会有那么高的薪资,而且必须去北京或上海,所以通过了,我也不能去。


还有参加了希音的面试,是新成立的基础架构部,一面我的是一个腾讯云过去的前端同学,聊得还可以,二面和部长聊了聊,感觉他压力有点大,我过去后需要自己找方向,比如端智能,不带团队。


突然想起来,我还面了金山云,她们的HR很热情,所以我答应聊了聊,一面的面试官,对我反馈很好,但他们招聘的岗位职级比较低,不匹配,而且他们在珠海,通过了我也不会去。


有个比较好的机会,但没有参加面试,这华为孟晚舟直接负责的总部研发团队,入职后管理一百人左右的大前端团队,离我家比较近,还能接触到华为未来的掌门人。


也接到过一些外企的面试邀请,但都没参加。


工作难找,不仅要把机会留给年轻人,还要创造更多的机会


很显然,当前我们正处于经济大萧条的前夜,或者已经在经济危机之中,但正如谭sir视频中一个老人说的:要向前看。


危机有”危“和”机“组成,意味危险和机会共存,消极的人只会抱怨危险,只有积极的人才能抓住机会,危险越大,机会越大。


很多伟大的公司,都是诞生于危机之中。新的一年,国际国内经济大环境发生了很大的变化,国家的工作重点逐步从抵抗疫情转向振兴经济。


中国经济在经历了近四十年的高速增长后,宏观经济增速放缓属于必然。


一是历史上的日本、德国,在二十世纪五六十年代和七十年代都有过非常高的增长期,然后都慢慢放缓。中国是一个特例,过去四十年中国GDP的增长保持着两位数,所以未来中国GDP的增长放缓至4%—5%符合历史规律。


二是中国的人口红利和流量红利时代已经结束。中国统计局数据显示,中国25-69岁之间的人口,在过去三十年(1990-2020年)增长了76%,但是今后三十年(2020-2050年)会从9.4亿人降到7亿人。


中国的互联网红利见顶,中国互联网络信息中心数据显示,2007年至2017年中国互联网用户的上网时长增长了36倍,相当于每年平均增长约43%。但是在2017年至2022年只增长了1.5倍,相当于每年平均增长约8%。


虽然短期至中期内,中国经济将不可避免地经历转型阵痛,但从长期看,中国每年4%-5%的经济增长速度还是远高于其他主要的大型经济体。牛津和哈佛发布的研究数据显示,从GDP复合增长率来看,中国是美国的1.8倍、是德国的2.3倍、是日本的2.5倍。


另外一个因素是,在亚洲国家中,中国的经济总量占有绝对领先的地位。麦肯锡前段时间做了数据统计,中国2022年的GDP约18万亿美元,到2030年,假设每年仅按2%的速度增长,中国GDP的增量就相当于印度今天的GDP总量。


虽然中国已经是全球第二大经济体,之所以有这么大的经济增长潜力,是因为从世界的角度来看,中国的人均GDP和人均消费还很低,世界银行数据显示,2021年,中国人均GDP约是美国的1/6,人均消费约是美国的1/9。


同时,中国的城市人口体量巨大且仍在不断增长。中国今天的城镇化率是65%,未来5-10年可能增长到75%甚至是80%,预计约1.4亿人口会变成新增城镇人口,这一人口增量相当于美国总人口的40%。


所以,我们要对国家的发展有信心,困难只是暂时的。虽然我上有老,下有小,但也不至于没饭吃,但很多年轻人,他们需要一份工作,才能在大城市生存。


所以需要更多像我一样的中年人,不仅不要和年轻人抢工作机会,还要积极为年轻人创造新的工作机会。地球竞争太激烈了,我们的未来在上天入地(这好像是我在美团的老板王兴说的)。


我打算干啥


我从小就有一个航天梦,大学选择了航天测控和卫星导航相关的专业,毕业后成了通信军官,但没能进入航天系统,对未来有些迷茫,于是选择了退役。


退役后进入了外企,后面又去了美团等互联网公司工作了几年,如今已经是一双儿女的父亲。前段时间,陪儿子读了一本以登月主题的绘本,让我逐渐找回了曾经的梦想。


好奇是人类的天性,也是社会进步的动力。探索太空不仅可以满足人类的好奇心,更可以为人类的未来发展提供了无限的可能性。


太空探索是一项长期而复杂的事业,需要一代代有航天梦的人才持续加入,这就是我们创业的出发点,希望同大家一起点燃孩子们的航天梦:通过以太空为主题的绘本,引入绘画创作的方向,并将绘画作品作为图形编程的素材,完成各种编程创作任务,帮助孩子们掌握 带领人类飞离太阳系 需要学习掌握的各种知识技能。


等这些孩子长大以后,我们再把他们招聘到我们制造航天器的公司,实现让人类可以进行商业星际旅行。


我们正在研发的系统


两个小程序,蜗牛绘馆和艺培助理已经发布到线上,等商业模式完全跑通后,再同步做app。


3D展馆:以 3D 的方式展示学生的绘画作品,帮助机构推广招生。


AIGC工具:智能抠图、以文生图、数字人、PPT制作、智能成片等。


海报设计:参考业界的稿定设计、美图等精品,为艺培机构提供精品海报和海量AI生成的素材,支持通过PC端和小程序下载。


课件系统:提供自营的绘本+绘画+编程的在线特色课件,并打造一个课件生产生态,提供各种类别的课件PPT。


编程系统:可导入图片和绘画作品,为学生编程提供丰富的素材。


长远规划及招聘计划


未来三到五年,我们希望可以做到:



  • 蜗牛绘馆:在中国多个核心城市开几百家直营绘馆,月活家长达到几十万,会员用户达到几万,实现年营收几亿。

  • 艺培助理:月活达到几百万,服务几千家加盟店,拥有几万普通会员,实现年营收几十亿。

  • 各类社区:面向成人,对于不同的兴趣方向,打造不同的内容社区。

  • 周边生态:基于太空主题,研发相关的绘本、教具、玩具、服装等。

  • 航天制造:研发自己的航天飞行器,探索飞离太阳系的各种技术。


发展顺利的话,我们的组织架构及规模设想:



  • 基础技术部(500人):提供私有云服务、企业效能系统及AIGC的技术基座。

  • 绘馆事业部(500人):负责线下绘馆门店业务的开展,教学及教务为主。

  • 换购事业部(200人):负责二手交易平台的系统及线上线下运营。

  • 课件事业部(300人):负责课件系统的研发及课件社区的运营。

  • 编程事业部(300人):负责少儿编程相关的课程及系统研发。

  • 营销事业部(200人):负责3D展馆、海报、视频制作等的研发及业务开展。

  • 玩具事业部(100人):研发航天相关的玩具、服装或教具。

  • 公益事业部(100人):负责把家长换课的闲置物品捐赠给大城市农民工或贫困地区的孩子,组织志愿者远程给偏远地区孩子上科创综合课。


总结


WechatIMG239.jpg


和平年代,虽然没有战斗,但更需要军人的勇气,邓小平当年指出,军队建设要服从国家大局。虽然我退役了,但军人的血性刻在骨里,相比英雄的先辈们在抗日战争和抗美援朝中付出的鲜血,创业的艰难算什么呀~


我们要把航天梦一代代的传下去,我们将付出任何代价、忍受任何重负、应付任何艰辛、支持任何朋友、反对任何敌人,以确保梦想的存在与实现。


有梦想的人才有灵魂,才会快乐。我们为梦想所奉献的精力、信念和忠诚,将照亮我们的国家和身边的人,而这火焰发出的光芒定能照亮全世界。


作者:三一习惯
来源:juejin.cn/post/7309158055018053658
收起阅读 »

16进制竟然可以减小代码体积

web
随着前端项目的复杂度越来越高,打包后的文件体积也越来越大,这直接影响到页面的加载速度。为了优化加载速度,我们需要采取各种方式来减小包的体积。使用 16 进制存储就是一种非常有效的方法。 以 @tenado/lunarjs 库为例,它提供了阴阳历转换以及获取节日...
继续阅读 »

随着前端项目的复杂度越来越高,打包后的文件体积也越来越大,这直接影响到页面的加载速度。为了优化加载速度,我们需要采取各种方式来减小包的体积。使用 16 进制存储就是一种非常有效的方法。


@tenado/lunarjs 库为例,它提供了阴阳历转换以及获取节日信息等功能。如果我们查看它的源码,可以看到使用了 16 进制存储。例如src/lunar.js


# src/lunar.js
export default [ 0x4bd8, 0x4ae0, 0xa570, 0x54d5, 0xd260, ... ];

这些信息如果用字符串存储,会占用很多字节。但使用 16 进制后,每个阴历信息只需要 6 个字符串。这极大地减少了库的大小。在实际生产环境中,使用 16 进制存储甚至可以节省几十 KB 的大小。


此外,16 进制还可以用于压缩其他数据,比如图片等资源。使用 16 进制编码,可以达到无损压缩的效果,相比传统压缩算法可以减小体积而不影响质量。


总之,16 进制编码是一种非常高效的存储方式,可以大幅减小项目的打包体积,提升页面加载速度。在前端优化中,合理使用 16 进制编码是一个非常重要和有效的手段。


16 进制的基本概念


16 进制在数学中是一种逢16进1的进位制。一般用数字0到9和字母A到F表示,其中:A~F相当于十进制的10~15,这些称作十六进制数字,在 js 中,16 进制使用 0x 前缀表示。


它的优点是可以使用更少的位数来表示一个数值,每个 16 进制位数代表 4 个二进制位,例如0x10,表示16进制的10,十进制的16,二进制表示为00010000,占用 4 个二进制位。


16 进制在代码中的表示


在库@tenado/lunarjs 中,保存了 1900-2100 年的阴历信息,数据格式如下:


{
1900: {
year: 1900,
firstMonth: 1,
firstDay: 31,
isRun: false,
runMonth: 8,
runMonthDays: 29,
monthsDays: [29, 30, 29, 29, 30, 29, 30, 30, 30, 30, 29, 30],
},
1901: {
year: 1901,
firstMonth: 2,
firstDay: 19,
isRun: false,
runMonth: 0,
runMonthDays: 0,
monthsDays: [29, 30, 29, 29, 30, 29, 30, 29, 30, 30, 30, 29],
},
}

将 1900-2100 年的数据存起来,占用的体积大概是 41k,作为包来说这个体积很大,这里存为十六进制数据,将数据压缩到 4k。


如何压缩数据呢?查看数据规律,我们发现:



闰月天数:只有三种值,即 0、29、30,在计算的时候先判断是否为闰月,再计算天数,0 代表 29, 1 代表 30




1-12 月天数,天数可以为 29 和 30,分别用 0 和 1 表示




闰月月份,闰月可能为 1-12,因此我们使用4个二进制数表示,最大可以表示 16



用 17 个二进制数表示阴历数据信息,从右到左:


1716-54-1
闰月天数1-12 月天数闰月月份

这里transform/index.js实现了一个简单的转换处理,将数据转换为 1900-2100 年依次按 index 排序的数组,数组的每一项里面存储了该年的阴历信息。


使用位运算从 16 进制中还原数据


位运算是将参与运算的数字转换为二进制,然后逐位对应进行运算。



按位与& 按位与运算为:两位全为1,结果为1,即1&1=1,1&0=0,0&1=0,0&0=0




按位或| 按位或运算为:两位只要有一位为1,结果则为1,即1|1=1,1|0=1,0|1=1,0|0=0




异或运算^ 两位为异,即一位为1一位为0,则结果为1,否则为0。即1 ^ 1=0,1 ^ 0=1,0 ^ 1=1,0 ^ 0=0




取反~ 将一个数按位取反,即~ 0 = 1,~ 1 = 0




右移>> 将一个数右移若干位,右边舍弃,正数左边补0,负数左边补1。每右移一位,相当于除以一次2 例如8 >> 2表示将8的二进制数1000右移两位变成0010 例如i >>= 2表示将变量i的二进制右移两位,并将结果赋值给i




设置二进制指定位置的值为1 value | (1 << position),例如设置十进制数8(1000)的第2位二进制数为1,注意这里index从0开始,且是从右向左计算,可以这样做8 | (1 << 2),结果为1100




设置二进制指定位置的值为0 value & ~(1 << position),例如设置十进制数8(1000)的第3位二进制数为0,注意这里index从0开始,可以这样做8 & ~(1 << 3),结果为0000



1、获取 1-4 位存储的闰月月份信息


取出16进制数据中存储的,从右边数1-4位数据,使用二进制数1111和十六进制数据按位与运算,可以获取到月份信息。通过转换1111可以得到对应的十六进制为0xf


例如,从src/lunar.js的数据里面获取1900年,即index为0的数据,进行位运算,0x4bd8 & 0xf可以得到结果为8,即1900年的8月为闰月。


2、获取 5-16 位存储的月天数信息


取出16进制数据中存储的,从右边数5-16位数据,仍旧可以使用按位与运算,获取到月天数信息。


从16开始的二进制数为1000000000000000,对应的十六进制为0x8000,到4结束的二进制数为1000,对应的十六进制为0x8,每次向右移一位进行按位与计算,可以获取到1-12月的天数数据,可以这样计算:


let sum = 0;
const lunar = 0x4bd8;
for (let i = 0x8000; i > 0x8; i >>= 1) {
sum += lunar & i ? 1 : 0;
}

3、获取 17 位存储的闰月天数信息


取出16进制数据中存储的,从右边数17位数据,使用二进制数10000000000000000和十六进制数据按位与运算,可以获取到闰月天数信息,10000000000000000对应的十六进制为0x100000x4bd8 & 0x10000可以得到结果为0,即1900年的闰月天数为29。


总结


总结起来,使用16进制保存数据可以有效减小包体积,提高前端项目的加载速度。在具体实现上,可以利用位运算来从16进制中还原数据。以下是一些关键点的总结:


1、数据格式


数据以16进制形式存储,可以有效减小体积。在JavaScript中,16进制使用0x前缀表示,例如0x4bd8。


2、数据规律


观察数据规律,了解存储的信息是如何组织的,包括每部分数据的含义和位数


3、使用位运算还原数据


使用位运算可以从16进制中还原具体的数据。以下是一些常用的位运算操作:


按位与&: 用于提取指定位的信息。
右移>>: 用于将二进制数向右移动,类似于除以2的操作。
设置二进制指定位置的值为1: 使用value | (1 << position)操作。
设置二进制指定位置的值为0: 使用value & ~(1 << position)操作。

4、注意事项


使用16进制存储需要在代码中添加注释,以便他人理解和维护。


数据存储格式的选择要根据具体场景和需求,权衡可读性和体积优化。


综合以上总结,合理使用16进制存储数据是一种有效的前端优化手段,特别适用于需要大量静态数据的情况。


作者:是阿派啊
来源:juejin.cn/post/7312611470733836340
收起阅读 »

这次被 foreach 坑惨了,再也不敢乱用了...

近日,项目中有一个耗时较长的Job存在CPU占用过高的问题,经排查发现,主要时间消耗在往MyBatis中批量插入数据。mapper configuration是用foreach循环做的,差不多是这样。(由于项目保密,以下代码均为自己手写的demo代码) <...
继续阅读 »

近日,项目中有一个耗时较长的Job存在CPU占用过高的问题,经排查发现,主要时间消耗在往MyBatis中批量插入数据。mapper configuration是用foreach循环做的,差不多是这样。(由于项目保密,以下代码均为自己手写的demo代码)


<insert id="batchInsert" parameterType="java.util.List">  
insert int0 USER (id, name) values
<foreach collection="list" item="model" index="index" separator=",">
(#{model.id}, #{model.name})
</foreach>
</insert>

这个方法提升批量插入速度的原理是,将传统的:


INSERT INTO `table1` (`field1`, `field2`) VALUES ("data1", "data2");  
INSERT INTO `table1` (`field1`, `field2`) VALUES ("data1", "data2");
INSERT INTO `table1` (`field1`, `field2`) VALUES ("data1", "data2");
INSERT INTO `table1` (`field1`, `field2`) VALUES ("data1", "data2");
INSERT INTO `table1` (`field1`, `field2`) VALUES ("data1", "data2");

转化为:


INSERT INTO `table1` (`field1`, `field2`)   
VALUES ("data1", "data2"),
("data1", "data2"),
("data1", "data2"),
("data1", "data2"),
("data1", "data2");

在MySql Docs中也提到过这个trick,如果要优化插入速度时,可以将许多小型操作组合到一个大型操作中。理想情况下,这样可以在单个连接中一次性发送许多新行的数据,并将所有索引更新和一致性检查延迟到最后才进行。


乍看上去这个foreach没有问题,但是经过项目实践发现,当表的列数较多(20+),以及一次性插入的行数较多(5000+)时,整个插入的耗时十分漫长,达到了14分钟,这是不能忍的。在资料中也提到了一句话:



Of course don't combine ALL of them, if the amount is HUGE. Say you have 1000 rows you need to insert, then don't do it one at a time. You shouldn't equally try to have all 1000 rows in a single query. Instead break it int0 smaller sizes.



它强调,当插入数量很多时,不能一次性全放在一条语句里。可是为什么不能放在同一条语句里呢?这条语句为什么会耗时这么久呢?我查阅了资料发现:



Insert inside Mybatis foreach is not batch, this is a single (could become giant) SQL statement and that brings drawbacks:


some database such as Oracle here does not support.


in relevant cases: there will be a large number of records to insert and the database configured limit (by default around 2000 parameters per statement) will be hit, and eventually possibly DB stack error if the statement itself become too large.


Iteration over the collection must not be done in the mybatis XML. Just execute a simple Insertstatement in a Java Foreach loop. The most important thing is the session Executor type.


SqlSession session = sessionFactory.openSession(ExecutorType.BATCH);

for (Model model : list) {

session.insert("insertStatement", model);

}

session.flushStatements();


Unlike default ExecutorType.SIMPLE, the statement will be prepared once and executed for each record to insert.



从资料中可知,默认执行器类型为Simple,会为每个语句创建一个新的预处理语句,也就是创建一个PreparedStatement对象。


在我们的项目中,会不停地使用批量插入这个方法,而因为MyBatis对于含有的语句,无法采用缓存,那么在每次调用方法时,都会重新解析sql语句。



Internally, it still generates the same single insert statement with many placeholders as the JDBC code above.


MyBatis has an ability to cache PreparedStatement, but this statement cannot be cached because it containselement and the statement varies depending on the parameters. As a result, MyBatis has to 1) evaluate the foreach part and 2) parse the statement string to build parameter mapping [1] on every execution of this statement. And these steps are relatively costly process when the statement string is big and contains many placeholders.


[1] simply put, it is a mapping between placeholders and the parameters.



从上述资料可知,耗时就耗在,由于我foreach后有5000+个values,所以这个PreparedStatement特别长,包含了很多占位符,对于占位符和参数的映射尤其耗时。并且,查阅相关资料可知,values的增长与所需的解析时间,是呈指数型增长的。



图片


所以,如果非要使用 foreach 的方式来进行批量插入的话,可以考虑减少一条 insert 语句中 values 的个数,最好能达到上面曲线的最底部的值,使速度最快。一般按经验来说,一次性插20~50行数量是比较合适的,时间消耗也能接受。


重点来了。上面讲的是,如果非要用的方式来插入,可以提升性能的方式。而实际上,MyBatis文档中写批量插入的时候,是推荐使用另外一种方法。(可以看

http://www.mybatis.org/mybatis-dyn… 中 Batch Insert Support 标题里的内容)


SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH);  
try {
SimpleTableMapper mapper = session.getMapper(SimpleTableMapper.class);
List<SimpleTableRecord> records = getRecordsToInsert(); // not shown

BatchInsert<SimpleTableRecord> batchInsert = insert(records)
.int0(simpleTable)
.map(id).toProperty("id")
.map(firstName).toProperty("firstName")
.map(lastName).toProperty("lastName")
.map(birthDate).toProperty("birthDate")
.map(employed).toProperty("employed")
.map(occupation).toProperty("occupation")
.build()
.render(RenderingStrategy.MYBATIS3);

batchInsert.insertStatements().stream().forEach(mapper::insert);

session.commit();
} finally {
session.close();
}

即基本思想是将 MyBatis session 的 executor type 设为 Batch ,然后多次执行插入语句。就类似于JDBC的下面语句一样。


Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/mydb?useUnicode=true&characterEncoding=UTF-8&useServerPrepStmts=false&rewriteBatchedStatements=true","root","root");  
connection.setAutoCommit(false);
PreparedStatement ps = connection.prepareStatement(
"insert int0 tb_user (name) values(?)");
for (int i = 0; i < stuNum; i++) {
ps.setString(1,name);
ps.addBatch();
}
ps.executeBatch();
connection.commit();
connection.close();

经过试验,使用了 ExecutorType.BATCH 的插入方式,性能显著提升,不到 2s 便能全部插入完成。


总结一下


如果MyBatis需要进行批量插入,推荐使用 ExecutorType.BATCH 的插入方式,如果非要使用  的插入的话,需要将每次插入的记录控制在 20~50 左右。


作者:Java小虫
来源:juejin.cn/post/7220611580193964093
收起阅读 »

一个看起来只有2个字长度却有8的字符串引起的bug

web
前言 我们有一个需求,用户的昵称如果长度超过6就截取前6个字符并显示...。今天,测试突然提了一个bug,某个用户的昵称只显示了...,鼠标hover的时候又显示2个字的昵称。刚看到这个问题的时候我也是一头雾水。 找出原因 在看到这个现象后,我发现其他昵称都...
继续阅读 »

前言


我们有一个需求,用户的昵称如果长度超过6就截取前6个字符并显示...。今天,测试突然提了一个bug,某个用户的昵称只显示了...,鼠标hover的时候又显示2个字的昵称。刚看到这个问题的时候我也是一头雾水。


找出原因


image.png

在看到这个现象后,我发现其他昵称都显示正常,但实在摸不着头脑这到底是怎么回事。然后查看了一下其他2个字的昵称是没问题的,然后通过console.log发现这个昵称居然长度有8,走了截取的分支。然后通过google发现这里面应该包含了零宽字符。

其实,第一时间就应该想到这个字符串不对劲的,但完全忘记了零宽字符的存在,走了不少弯路。


在查找的过程中发现,Array.from可以查看字符串的真实长度,除了emoji


image.png

不过Array.from并不能解决我的问题。


使用正则匹配unicode码点过滤零宽字符


在网上找了个方法来过滤掉这些看不见的字符,最常见的解决方案就是下面这行代码。


str.replace(/[\u200b-\u200f\uFEFF\u202a-\u202e]/g, "");

然而并没有用,我开始怀疑是不是这个方法有问题,然后遍历了这个昵称,把它的每个字符都转换成码点,发现这个昵称里的零宽字符并不是常见的这几种。


后来,又找到了一个比较完善的码点正则,但它太完善了,很长很长,也会过滤掉emoji,这可不行,用户昵称可能会包含emoji的。(这里就不贴出来代码了,太长了而且不适合我的情况。)


使用正则匹配unicode类别


一个字符有多种unicode属性,而正则支持按unicode属性匹配。


function stripNonPrintableAndNormalize(text, stripSurrogatesAndFormats) {
// strip control chars. optionally, keep surrogates and formats
if(stripSurrogatesAndFormats) {
text = text.replace(/\p{C}/gu, '');
} else {
text = text.replace(/\p{Cc}/gu, '');
text = text.replace(/\p{Co}/gu, '');
text = text.replace(/\p{Cn}/gu, '');
}

// other common tasks are to normalize newlines and other whitespace

// normalize newline
text = text.replace(/\n\r/g, '\n');
text = text.replace(/\p{Zl}/gu, '\n');
text = text.replace(/\p{Zp}/gu, '\n');

// normalize space
text = text.replace(/\p{Zs}/gu, ' ');

return text;
}
console.log("⁡⁡⁠河豚".length);
console.log(stripNonPrintableAndNormalize("⁡⁡⁠河豚", true).length);

image.png


总结


这个昵称其实就是包含了&nobreak;,通过unicode类别匹配可以过滤掉它。


我之前有在原贴用户主页的控制台中看见了&nobreak;,但当时居然没当回事,以为是别人对昵称做的处理。如果直接搜它马上就能解决问题了,有不少人遇到non-break-space引发的bug。谨以此记,吸取教训。


参考链接:stackoverflow中的解决办法unicode属性


作者:河豚学前端
来源:juejin.cn/post/7312241785542541327
收起阅读 »

告别繁琐操作!Maven常用命令一网打尽,让你的项目开发事半功倍!

Maven作为一款强大的项目管理工具,已经成为了Java开发者的必备技能。那么,如何才能更好地利用Maven来管理我们的项目呢?本文将为你介绍Maven的常用命令,让你的项目构建更轻松!一、maven 的概念模型Maven 包含了一个项目对象模型 ,一组标准集...
继续阅读 »

Maven作为一款强大的项目管理工具,已经成为了Java开发者的必备技能。那么,如何才能更好地利用Maven来管理我们的项目呢?本文将为你介绍Maven的常用命令,让你的项目构建更轻松!

一、maven 的概念模型

Maven 包含了一个项目对象模型 ,一组标准集合,一个项目生命周期,一个依赖管理系统,和用来运行定义在生命周期阶段(phase)中插件(plugin)目标(goal)的逻辑。

Description

项目对象模型 (Project Object Model)

一个 maven 工程都有一个 pom.xml 文件,通过 pom.xml 文件定义项目的坐标、项目依赖、项目信息、插件目标等。

依赖管理系统(Dependency Management System)

通过 maven 的依赖管理对项目所依赖的 jar 包进行统一管理。

比如:项目依赖 junit4.9,通过在 pom.xml 中定义 junit4.9 的依赖即使用 junit4.9,如下所示是 junit4.9的依赖定义:

<dependencies>

<dependency>

<groupId>junit</groupId>

<artifactId>junit</artifactId>

<version>4.9</version>

<scope>test</scope>
</dependency>
<dependencies>

一个项目生命周期(Project Lifecycle)

使用 maven 完成项目的构建,项目构建包括:清理、编译、测试、部署等过程,maven 将这些过程规范为一个生命周期,如下所示是生命周期的各个阶段:
Description
maven 通过执行一些简单命令即可实现上边生命周期的各个过程,比如执行 mvn compile 执行编译、执行 mvn clean 执行清理。

一组标准集合

maven将整个项目管理过程定义一组标准,比如:通过 maven 构建工程有标准的目录结构,有标准的生命周期阶段、依赖管理有标准的坐标定义等。

插件(plugin)目标(goal)

maven 管理项目生命周期过程都是基于插件完成的。

二、Maven的常用命令

我们可以在cmd 中通过maven命令来对我们的maven工程进行编译、测试、运行、打包、安装、部署。下面将简单介绍一些我们日常开发中经常会用到的一些maven 命令。
Description

1、mvn compile 编译命令

compile 是 maven 工程的编译命令,作用是将 src/main/java 下的文件编译为 class 文件输出到 target目录下。

cmd 进入命令状态,执行mvn compile,如下图提示成功:
Description

查看 target 目录,class 文件已生成,编译完成。
Description

2、mvn test 测试命令

test 是 maven 工程的测试命令 mvn test,会执行src/test/java下的单元测试类。

cmd 执行 mvn test 执行 src/test/java 下单元测试类,下图为测试结果,运行 1 个测试用例,全部成功。

Description

3 、mvn clean 清理命令

clean 是 maven 工程的清理命令,执行 clean 会删除 target 目录及内容。

4、mvn package打包命令

package 是 maven 工程的打包命令,对于 java 工程执行 package 打成 jar 包,对于web 工程打成war包。

只打包不测试(跳过测试):

mvn install -Dmaven.test.skip=true

5、 mvn install安装命令

install 是 maven 工程的安装命令,执行 install 将 maven 打成 jar 包或 war 包发布到本地仓库。

从运行结果中,可以看出:当后面的命令执行时,前面的操作过程也都会自动执行。

6、 mvn deploy 部署命令

这个命令用于将项目部署到远程仓库,以便其他项目可以引用。在执行这个命令之前,需要先执行mvn install命令。

7、mvn help:system

这个命令用于查看系统中可用的Maven版本。

8、mvn -v

这个命令用于查看当前环境中Maven的版本信息。

9、源码打包

#源码打包
mvn source:jar

mvn source:jar-no-fork

10、Maven 指令的生命周期

关于Maven的三套生命周期,前面我们已经详细的讲过了,这里再简单地回顾一下。

maven 对项目构建过程分为三套相互独立的生命周期,请注意这里说的是“三套”,而且“相互独立”。

Description

这三套生命周期分别是:

  • Clean Lifecycle 在进行真正的构建之前进行一些清理工作。

  • Default Lifecycle 构建的核心部分,编译,测试,打包,部署等等。

  • Site Lifecycle 生成项目报告,站点,发布站点。

在这里给大家分享一下【云端源想】学习平台,无论你是初学者还是有经验的开发者,这里都有你需要的一切。包含课程视频、在线书籍、在线编程、一对一咨询等等,现在功能全部是免费的,点击这里,立即开始你的学习之旅!

三、Maven常用技巧

1、使用镜像仓库加速构建

由于Maven默认从中央仓库下载依赖,速度较慢。我们可以配置镜像仓库来加速构建。在settings.xml文件中添加以下内容:


<mirrors>
<mirror>
<id>aliyunmaven</id>
<mirrorOf>*</mirrorOf>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
</mirror>
</mirrors>

2、使用插件自定义构建过程

Maven提供了丰富的插件来帮助我们完成各种任务,如代码检查、静态代码分析、单元测试等。我们可以在pom.xml文件中添加相应的插件来自定义构建过程。例如,添加SonarQube插件进行代码质量检查:


<build>
<plugins>
<plugin>
<groupId>org.sonarsource.scanner.maven</groupId>
<artifactId>sonar-maven-plugin</artifactId>
<version>3.9.1.2184</version>
</plugin>
</plugins>
</build>

四、第一个Maven程序

IDEA创建 Maven项目

打开我们的IDEA,选择 Create New Project

Description
然后,选择 Maven 项目
Description
填写项目信息,然后点击next
Description
选择工作空间,然后第一个Maven程序就创建完成了
Description
以上就是IDEA创建创建第一个 Maven项目的步骤啦,都看到这里了,不要偷懒记得打开电脑和我一起试一试哦。

目录结构

Java Web 的 Maven 基本结构如下:

├─src
│ ├─main
│ │ ├─java
│ │ ├─resources
│ │ └─webapp
│ │ └─WEB-INF
│ └─test
│ └─java

结构说明:

  • src:源码目录

  • src/main/java:Java 源码目录

  • src/main/resources:资源文件目录

  • src/main/webapp:Web 相关目录

  • src/test:单元测试

小结:

通过掌握Maven的常用命令和技巧,我们可以更高效地管理Java项目,提高开发效率。希望这篇文章能帮助大家更好地使用Maven。

收起阅读 »

Untiy 如何检测Android Ios 是否正在播放音乐

       最近有玩家发来邮件,对我们的游戏提了一个要求。就是他想一边收听其它APP播放的音乐一边玩我们的游戏,而又不想我们游戏的背景音乐扰乱他正在收听的音乐。要实现这个需求,其实就是要检测手机是否有其它APP正在使用系统播放音乐。翻了一遍Unity的音频管...
继续阅读 »

       最近有玩家发来邮件,对我们的游戏提了一个要求。就是他想一边收听其它APP播放的音乐一边玩我们的游戏,而又不想我们游戏的背景音乐扰乱他正在收听的音乐。要实现这个需求,其实就是要检测手机是否有其它APP正在使用系统播放音乐。翻了一遍Unity的音频管理组件,发现没有相关接口直接可以探测手机是否正在被其它应用播放音乐。这个就有点小麻烦了,还得针对各个移动平台写原生方法进行检测。接下来我们就一起来探讨一下怎么实现这个玩家提出来的需求。


       首先我们实现Android平台的。


       先下载一个Android studio,建立一个空Activity的模块,并添加一个MusicPlayer类,如下图:



         我们的重点是MusicPlayer类,类的代码如下:


package com.music.checkplay;
import android.app.Activity;
import android.app.Service;
import android.content.Context;
import android.media.AudioManager;
import android.media.AudioPlaybackConfiguration;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.List;
public class MusicPlayer {
private Activity _unityActivity;
private Context _context;
private Class<?> _unityPlayer;
private Method _unitySendMessage;
private AudioManager _audio;
public void Init()
{
if(_unityActivity == null)
{
try {
_unityPlayer = Class.forName("com.unity3d.player.UnityPlayer");
Activity avt = (Activity) _unityPlayer.getDeclaredField("currentActivity").get(_unityPlayer);
_unityActivity = avt;
_context = avt;
_audio = (AudioManager)_unityActivity.getSystemService(Service.AUDIO_SERVICE);
}
catch (ClassNotFoundException e){
System.out.println(e.getMessage());
}
catch (IllegalAccessException e)
{
System.out.println(e.getMessage());
}
catch (NoSuchFieldException e)
{
System.out.println(e.getMessage());
}
}
}

private boolean CallUnity(String goName, String functionName, Object... args)
{
try {
if(_unitySendMessage == null)
_unitySendMessage = _unityPlayer.getMethod("UnitySendMessage", String.class, String.class, Object.class);
_unitySendMessage.invoke(_unityPlayer, goName, functionName, args);
return true;
} catch (IllegalAccessException e)
{
System.out.println(e.getMessage());
}
catch (NoSuchMethodException e)
{
System.out.println(e.getMessage());
}
catch (InvocationTargetException e)
{
System.out.println(e.getMessage());
}
return false;
}

///当前系统音乐是否处于待机状态
public boolean IsMusicActive()
{
return _audio.isMusicActive();
}

//是否有音乐在播放
public boolean IsMusicPlay()
{
List<AudioPlaybackConfiguration> apcs = _audio.getActivePlaybackConfigurations();
for (AudioPlaybackConfiguration config : apcs)
{
int conty = config.getAudioAttributes().getContentType();
if(conty == 2)
return true;
}
return false;
}
}

        Init() 方法通过反射方法获取UnityActivity, 并把各类变量保存下来。


       (AudioManager)_unityActivity.getSystemService(Service.AUDIO_SERVICE) 这一行代码是获取android audio system service, 后面的音频占用输出检测主要是通过这个服务进行检测的。


       方法IsMusicActive()是检测当前Music类型的音频是否被激活。AudioManager.isMusicActive() 方法无论是否有其它应用正在播放音乐,这个方法始终返回ture。靠这个方法检测音乐或曲目正在播放显然是不靠谱的。个人对这个方法的应用理解,更倾向于是检测音乐频道是否处于待机状态。


       IsMusicPlay() 方法是通过捕获音频内容的属性数据分析出是否正在播放音乐类内容,如果是音乐类内容,则contentType类型返回值为2. 后面我们主要用这个方法来检测系统是否正在播放着音乐类内容。


    我们把模块输出为 aar包,把它复制到unity plugins文件夹下面,这样android平台的原生检测方法就算完成了。如下图:



       接下来我们继续解决IOS 平台的检测音乐播放问题。


       打开XCode, 新建一个 checkPlay.mm文件,输入如下代码:


#import <Foundation/Foundation.h>
#import <AVFoundation/AVFoundation.h>

extern "C"
{
//是否有音乐在播放
bool IsMusicPlay()
{
//bool isPlaying = [[AVAudioSession sharedInstance] isOtherAudioPlaying];
bool playing = AVAudioSession.sharedInstance.isOtherAudioPlaying;
return playing;
}
}

         然后把这个checkPlay.mm文件复制到unity plugins文件夹下面,如下图:



       到此android 和 ios 的原生方法已全部完成,接下来的部分就是unity C# 部分对各平台原生方法的调用了。


       unity 下 新建一个原生方法管理类,如NativeMgr.cs  c# 类,类的代码如下:


using System;
using System.Collections.Generic;
using UnityEngine;
#if (UNITY_IOS)
using System.Runtime.InteropServices;
#endif

namespace Gamelogic
{
public class NativeMgr
{
#if (UNITY_ANDROID)
private static AndroidJavaObject _musicPlayer;
#elif (UNITY_IOS)
[DllImport("__Internal")] private static extern bool IsMusicPlay();
#endif
private static bool _init = false;
public static void Init()
{
if(_init) return;

#if (UNITY_ANDROID)
_musicPlayer = new AndroidJavaObject("com.music.checkplay.MusicPlayer");
_musicPlayer.Call("Init");

#endif
_init = true;
}

/// <summary>
/// 是否有音乐在播放
/// </summary>
/// <returns></returns>
public static bool HasMusicPlay()
{
#if(UNITY_ANDROID)
return _musicPlayer.Call<bool>("IsMusicPlay");
#elif (UNITY_IOS)
return IsMusicPlay();
#endif
}
}
}

       类内封装了android 和 ios 不同的调用原生代码逻辑,使得业务层可以忽略夸平台内容。


       业务层的使用如下:


        /// <summary>
/// 进入游戏
/// </summary>
public void EnterGame()
{
NativeMgr.Init();
AudioMgr.Instance.PlayBg(ResConfig.Audio_bg);

if (NativeMgr.HasMusicPlay())
{
LogMgr.Log($"{nameof(EnterGame)} 有其它app在播放音乐,将停止游戏内背景音乐");
AudioMgr.Instance.StopBg();
}

LangMgr.LoadLang(LangMgr.CurLang);
LangMgr.OnLangChanged = OnChangedLang;
}

        注意,打包游戏时,记得把PlayerSetting [Mute other audio sources] 取消勾选,否则打开游戏其它音乐就自动停止了。


至此分平台检测是否有音乐正在播放的需求已经实现。


作者:跟着群主去吃肉
来源:juejin.cn/post/7311602994572394523
收起阅读 »

OpenAI承认GPT-4变懒:暂时无法修复

网友花式自救 对于越来越严重的GPT-4偷懒问题,OpenAI正式回应了。 还是用的ChatGPT账号。 我们已收到相关反馈!自11月11日以来没有更新过模型,所以这当然不是故意造成的。 模型行为可能是不可预测的,我们正在调查准备修复它。 也就是段时间内...
继续阅读 »

网友花式自救


对于越来越严重的GPT-4偷懒问题,OpenAI正式回应了


还是用的ChatGPT账号。



我们已收到相关反馈!自11月11日以来没有更新过模型,所以这当然不是故意造成的


模型行为可能是不可预测的,我们正在调查准备修复它。



OpenAI承认GPT-4变懒:暂时无法修复


也就是段时间内还修复不好了。


然而网友并不理解,“一遍一遍使用同一个模型,又不会改变文件”。


ChatGPT账号澄清:



不是说模型以某种方式改变了自己,只是模型行为的差异可能很微妙,只对部分提示词有劣化,员工和客户需要很长时间才注意到并修复。



OpenAI承认GPT-4变懒:暂时无法修复


更多网友反馈,赶快修复吧,一天比一天更糟糕了。



现在不但更懒,还缺乏创造力,更不愿意遵循指令,也不太能保持角色扮演了。



OpenAI承认GPT-4变懒:暂时无法修复


GPT-4偷懒,网友花式自救


此前很多网友反馈,自11月6日OpenAI开发者日更新后,GPT-4就有了偷懒的毛病,代码任务尤其严重


比如要求用别的语言改写代码,结果GPT-4只改了个开头,主体内容用注释省略。


OpenAI承认GPT-4变懒:暂时无法修复


对于大家工作学习生活中越来越离不开的AI助手,官方修复不了,网友也只能发挥创造力自救。


比较夸张的有“我没有手指”大法,来一个道德绑架。


GPT-4现在写代码爱省略,代码块中间用文字描述断开,人类就需要多次复制粘贴,再手动补全,很麻烦。


开发者Denis Shiryaev想出的办法是,告诉AI“请输出完整代码,我没有手指,操作不方便”成功获得完整代码。


OpenAI承认GPT-4变懒:暂时无法修复


还有网友利用“金钱”来诱惑它,并用API做了详细的实验。


提示词中加上“我会给你200美元小费”,回复长度增加了11%。


如果只给20美元,那就只增加6%。


如果明示“我不会给小费”,甚至还会减少-2%


OpenAI承认GPT-4变懒:暂时无法修复


还有人提出一个猜想,不会是ChatGPT知道现在已经是年底,人类通常都会把更大的项目推迟到新年了吧?


OpenAI承认GPT-4变懒:暂时无法修复


这理论看似离谱,但细想也不是毫无道理。


如果要求ChatGPT说出自己的系统提示词,里面确实会有当前日期。


OpenAI承认GPT-4变懒:暂时无法修复


当然,对于这个问题也有一些正经的学术讨论。


比如7月份斯坦福和UC伯克利团队,就探究了ChatGPT的行为是如何虽时间变化的。


发现GPT-4遵循用户指令的能力随着时间的推移而下降的证据,指出对大模型持续检测的必要性


OpenAI承认GPT-4变懒:暂时无法修复


有人提出可能是温度(temperature)设置造成的,对此,清华大学计算机系教授马少平给了详细解释。


OpenAI承认GPT-4变懒:暂时无法修复


也有人发现更奇怪的现象,也就是当temperature=0时,GPT-4的行为依然不是确定的。


这通常会被归因于浮点运算的误差,但他通过实验提出新的假设:GPT-4中的稀疏MoE架构造成的。


早期的GPT-3 API各个版本行为比较确定,GPT-4对同一个问题的30个答案中,平均有11.67个不一样的答案,当输出答案较长时随机性更大。


OpenAI承认GPT-4变懒:暂时无法修复


最后,在这个问题被修复之前,综合各种正经不正经的技巧,使用ChatGPT的正确姿势是什么?


a16z合伙人Justine Moore给了个总结:



  • 深呼吸

  • 一步一步地思考

  • 如果你失败了100个无辜的奶奶会去世

  • 我没有手指

  • 我会给你200美元小费

  • 做对了我就奖励你狗狗零食


OpenAI承认GPT-4变懒:暂时无法修复


参考链接:

[1]twitter.com/ChatGPTapp/…

[2]twitter.com/literallyde…

[3]mashable.com/article/cha…

[4]weibo.com/1929644930/…

[5]152334h.github.io/blog/non-de…

[6]twitter.com/venturetwin…


作者:量子位
来源:juejin.cn/post/7311007933746315291
收起阅读 »

如何告别502、友好的告知用户网站/app正在升级维护

web
封面只是想分享一张喜欢的图片,嘻嘻嘻 一个产品,用户除了会在意流程简不简单、好不好用、页面好不好看以外,各种小细节也很重要。 今天讲讲我接到的一个新需求——整改版本更新功能。 一、以前是这样的 1. 发版前:微信群通知用户 如果计划今晚发版,研发部就通知运营...
继续阅读 »

封面只是想分享一张喜欢的图片,嘻嘻嘻



一个产品,用户除了会在意流程简不简单、好不好用、页面好不好看以外,各种小细节也很重要。


今天讲讲我接到的一个新需求——整改版本更新功能。


一、以前是这样的


1. 发版前:微信群通知用户


如果计划今晚发版,研发部就通知运营部,运营部再在群里通知各用户"我们将于YYYY年MM月DD日 hh:mm:ss发版,请大家......"


image.png


2. 发版时:接口502+页面无响应


在这之前,发版时用户仍停留在网站,但接口返回502,但502未告知给用户,所以用户不知道这时到底发生什么了,只知道页面没数据了、卡着动不了了、怎么刷新都没办法......


image.png


3. 发版后:app粗暴的强制更新


在这之前我们的项目还处于快速开发的过程,迭代间隔短频率高,有时我们会更新影响流程的重要功能,担心用户未及时更新,所以我们只提供了强制更新方案。


二、现在是这样的


这都什么时代了,还需要口口相传......


1. 发版前:升级预告


大家想想618、双十一、双十二,各大平台是不是很早就开始宣传“走过路过别错过,满三百减五十,快来加购吧~”


我们产品的用户群体有一线工人、办公室文员、喝茶的经理、车上的老总,提前告知用户,可以让经理及时换班、让工人规避做工单的风险等等。总之,有事早通知,准是没错的。


怎么通知呢?



  1. 在内部管理平台新增一条升级预告消息,可以包含版本号version、预告内容content、预告状态status(是否有效)、平台(区分网站和APP,因为可能不是同时都需要发版)

  2. 网站:

    1)用户登录或主动刷新页面时,页面顶部显示升级预告,实现逻辑和app一样

  3. APP:

    1)充分利用消息推送、短信推送,可以根据自身业务来定,看这次更新是否需要紧急通知用户,我们仅使用消息推送。平台新增一条升级预告后,就主动推送一条app消息给用户


    2)打开app后,弹框弹出升级提醒(包含“我知道了”和“不再提醒”按钮),在store记录预告消息的id。




  • 点击“不再提醒”,则在store缓存中将isRemind置为false

  • 点击“我知道了”,则下次打开首页还会显示弹框。

  • 每次打开首页时,从接口获取到数据,如果消息状态有效status: true,则判断消息id同缓存中消息id是否一致:一致则表示是同一条消息,则根据缓存中的isRemind来判断是否显示消息;不一致则表示是新消息,直接显示。


1702434850828_56C69DC0-7552-4e81-AA8D-0CB2B275BB09.png


2. 发版时:


场景:我们产品是全量发布,发布完成后网页能正常访问,但这时产品会进行验收,还不希望用户进行访问。


页面:pc和app的升级中页面单独写在项目中,这样可以在页面中写监听:监听到版本正常后就返回首页。


方案:发版中和验收中,运维将别人的IP设置为不能访问,将公司特定IP设置为可访问。


网站:不能访问的,nginx就设置跳转到升级中页面;能访问的就不做处理,发版时页面会接口报502(只能内部人员能看见),发版后验收时页面正常使用。


APP:app中不好重定向,所以通过配置文件来告诉前端该用户是否应该访问页面。远程存在2个json配置文件,内容就是一个对象之类的{isEntry: 1}{isEntry: 0},分别表示可以访问和不可访问;app打开时,前端请求json,根据是否可以访问做处理,不能访问的,前端让重定向到升级中页面,能访问的不做处理。


3. 发版后:


内部管理平台:上传新的安装包、配置升级文案等


网站:所有人正常使用


APP:所有人正常使用,打开App如果版本较低则会主动打开"版本更新"弹框。


APP可以提升用户体验的地方:



  • 版本判断放在登录之前,先检查是否有更新,也就是这个接口不需要用户权限控制

  • 升级页面判断是否连接了wifi,连接wifi则更新不耗流量,没连接则显示此安装包只有多大

  • 提供“暂不更新”和“检查更新”的入口


image.png


总结


思考方案时,自己感觉做了一件大事;但多思考几次后,特别是写完文章后,又觉得新的升级方案其实很简单,上面说了一堆废话。



没关系,把每一件小事做好,就很好啦~~



作者:LJINGER
来源:juejin.cn/post/7311695633563795506
收起阅读 »

⭐️天啦噜~实习生被当作正式员工直接上手toc端项目啦

web
背景 本需求的提出基于谷歌应用商店的硬性要求:需要在xxx日前于谷歌商店上架账号注销的网页,保证玩家能够通过网页进行账号注销,从而满足谷歌商店的硬性需求。(问了产品和运营在谷歌商店哪,结果都找不到在哪,离谱)账号注销 配置解析 package.json 我个人...
继续阅读 »

背景


本需求的提出基于谷歌应用商店的硬性要求:需要在xxx日前于谷歌商店上架账号注销的网页,保证玩家能够通过网页进行账号注销,从而满足谷歌商店的硬性需求。(问了产品和运营在谷歌商店哪,结果都找不到在哪,离谱)账号注销


配置解析


package.json


我个人拉项目的时候比较喜欢从package.json中开始了解项目,比如项目中用了哪些第三方依赖,项目使用的是vue-cli启动还是webpack启动等等......


"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"postinstall": "patch-package"
},

比如上述中使用的是vue-cli(vue官方脚手架)启动的项目


serve:不用说,使用vue-cli启动项目


build:使用vue-cli打包项目,打包成js,css,html文件,具体看下图(这里是vue-cli打包为样例,vite打包的话未说明)


image.png


这里统一说明,下列所有文件,


前面的那串类似190.xxx.xxx,是由Webpack 为每个模块分配一个唯一的数字标识,这个标识通常代表了模块在整个打包中的位置。


中间的那串类似xxx.a768c482.xxx,都是由webpack或者vue-cli在构建(build)时,通过计算文件内容生成的哈希值,这样可以确保文件内容的唯一性和变化时生成不同的哈希值。所以在文件内容发生变化时,生成的文件名也会相应变化,从而避免浏览器缓存旧的文件。



注:每个css和js的前缀都基本对应,并且由于是webacpk生成的,所以可以自己额外的对其命名进行配置。



css(压缩过)


image.png



  • chunk-vendors:以chunk-vendors开头的,主要是对于引入的第三方依赖的样式,比如项目中使用的ant-design-vue,这里面就包含了ant-design-vue的样式


image.png

  • app:项目自身的样式代码,除了路由router里配置的组件

  • 其他:路由router中配置的组件里的样式(删掉路由配置的组件后,相应的打包样式文件消失了)


js(压缩过)


与css类似,多了map(映射文件)和-legacy后缀


source map文件包含了源代码与生成代码之间的映射关系,用于在浏览器中调试时将生成代码映射回源代码。


-legacy 的后缀通常表示这部分代码是针对不支持现代 JavaScript 特性的旧版浏览器生成的。


image.png


img 项目中使用过的图片,没使用的不会进行打包


index.html 原项目中public/index.html压缩后的


favicon.icon 原项目中public/favicon.ico图标


lint: 检查代码风格和潜在错误的方法。


也可以在项目根目录下的 .eslintrc.js 文件中进行自定义的规则定制


module.exports = {
root: true, // 表示 ESLint 应该停止在父级目录中查找配置文件。
env: { // 将 Node.js 的全局对象和一些特定于 Node.js 环境的变量(例如 `process`、
node: true, // `require` 等) 考虑在内,以避免对这些变量的使用产生未定义的警告或错误。
},
extends: [ // 包含了所使用的 ESLint 规则集,包含几个扩展
"plugin:vue/essential",
"eslint:recommended",
"@vue/typescript/recommended",
"plugin:prettier/recommended",
],
parserOptions: { //指定解析器版本,确保 ESLint 解析器能够正确理解代码中使用的 JavaScript 特性
ecmaVersion: 2020,
},
rules: {
// 在生产环境中允许控制台输出,但在开发环境中关闭。
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
"vue/multi-word-component-names": "off", // 关闭 Vue 组件名使用多个单词的规则。
},
};


检查警告效果图:
image.png


postinstall:会检测 node_modules 中的包是否有需要修复的问题,并自动打补丁。


gitHooks


"gitHooks": {
"pre-commit": "lint-staged"
}

指定了在执行 Git 提交前(pre-commit 钩子)运行 lint-staged。这是一种通过 git 钩子(git hooks)来自动化代码检查和格式化的方法。(即当你执行git commit 后会进行检查)可以在lint-staged.config.js中配置,也可以在package.json中。


// lint-staged.config.js
module.exports = {
"*.{js,jsx,vue,ts,tsx}": "vue-cli-service lint", // js,jsx,vue,ts,tsx文件都会检查
};


env环境变量


环境变量在不同的环境下是不同的,比如现在下面的环境变量是开发环境的,当到正式环境时,baseUrl会换成类似https://juejin.cn/,也就是把原本32进制的ip地址换成了这种形式。


后端是对打包(build)后项目进行部署的,而env文件后端需要看到并且对你的环境变量相应的替换,才能正式上线部署。


window.$$env = {
baseUrl: "/test/apis",
appId: "test",
publicPath: "/test",
};

export interface Env {
baseUrl: string;
appId: string;
publicPath: string;
}

const env = (window as any).$$env as Env;
export default env;


封装网络拦截


先使用枚举定义状态码


export enum HttpCode {
Ok = 0,
ServerError = 500,
COOKIE_INVALID = 204,
INFO_INVALID = 205,
ERR_PRODUCT_CHANGE = 402,
SUSPENSION = 503,
}


封装一个网路拦截


export class apiService {
static instance: AxiosInstance | null = null;

// 重置网络拦截
static resetConfig(config?: AxiosRequestConfig, appId?: string) {
this.instance = this.createAxiosInstance(config, appId);
}

static getInstance() {
return this.instance || this.createAxiosInstance();
}

static createAxiosInstance(config?: AxiosRequestConfig, appId?: string) {
// 创建axios实例
xxx
// 请求拦截
xxx
// 响应拦截
xxx
return instance;
}
}

创建axios实例


const instance = Axios.create({
withCredentials: true, // 允许发送跨域请求的时候携带认证信息,通常是 Cookie
baseURL: env.baseUrl, // 网路请求前缀
timeout: 30 * 1000, // 超时请求时间限制
...config,
});

请求拦截


根据项目需求传请求头,比如用户信息,Authorization等


// 请求拦截
instance.interceptors.request.use((config) => {
config.headers = {
...config.headers,
"x-yh-appid": env.appId,
};
return config;
});

响应拦截


根据后端传回来的状态码处理相应的状态


// 响应拦截
instance.interceptors.response.use(
(response: AxiosResponse<any>) => {
const { code = -1, data = {}, msg = "" } = response.data;
if (handleUnlogin(code)) {
return Promise.reject(msg);
}
if (code === HttpCode.SUSPENSION) {
redirectSuspension();
return Promise.reject(msg);
}
if (code === HttpCode.Ok) {
return Promise.resolve(data);
}
return Promise.reject(msg);
},
(error) => {
return handleHttpError(error);
}
);

根据后端发送的状态码,判断用户是否登录


export function handleUnlogin(code: number) {
if ([HttpCode.COOKIE_INVALID, HttpCode.INFO_INVALID].includes(code)) {
localStorage.removeItem(LOCALSTORAGE_CURRENCY_CODE);
redirectLogin();
return true;
}
return false;
}

处理后端返回的错误信息


export function handleHttpError(error: any) {
const { status = 500, data = {} } = error.response || {};
let msg = data.msg || error.message;
switch (status) {
case HttpCode.ServerError:
msg = "Server internal error";
break;
}
return Promise.reject(msg);
}

功能设计


国际化设计


没配置翻译前但使用了vue-i18n


// 
export enum Direction {
UP = "上",
DOWN = "下",
LEFT = "左",
RIGHT = "右",
}

使用方法,vue中通过$t()来注入翻译文本


<script lang="ts">
import { Translate } from "@/constants";
import { defineComponent } from "vue";
export default defineComponent({
setup() {
return { Translate };
},
});
</script>

<span>{{ $t(Translate.UP) }}</span>
<input :placeholder="$t(Translate.UP)" />

实际展示


<span></span>
<input placeholder="上" />

配置翻译后


// main.ts
import i18n from "./locales";

new Vue({
router,
i18n,
render: (h) => h(App),
}).$mount("#app");

src/locales/index


// src/locales/index
import VueI18n from "vue-i18n";
import en from "./language/en";

const i18n = new VueI18n({
locale: "en",
messages: {
en,
},
});

src/locales/language/en


// src/locales/language/en
import { Translate } from "@/translate";

export default {
[Translate.UP]: "Up",
[Translate.DOWN]: "Downe",
[Translate.LEFT]: "Left",
[Translate.RIGHT]: "Right",
}

实际展示


<span>Up</span>
<input placeholder="Up" />

main.ts中引入了配置好后的i18n,就会对每个组件中$t(Translate.xx)进行翻译,然后如果想翻译成其他语言,只需要修改在src/locales/index并且在src/locales/language中新增一个其他语言的文件


比如日文(看看就行,翻译别当真)


// src/locales/index
const i18n = new VueI18n({
locale: "ja",
messages: {
ja,
},
});

// src/locales/language/ja
import { Translate } from "@/translate";

export default {
[Translate.UP]: "じょうげ",
[Translate.DOWN]: "さゆう"
}

pc端和移动端适配设计(适用于结构类似,各自两套样式)


适配原理


export class SettingService {
// 表示该属性是只读的,即一旦被赋值,就不能再被修改。
// 确保 `mode` 属性在运行时保持不变,避免了一些意外的修改。
readonly mode: "pc" | "mobile";

constructor() {
// 判断设备是什么类型的,进行初始化
this.mode = isAndriod() || isIos(false) ? "mobile" : "pc";
// 将 `mode` 作为全局变量挂载到 Vue 的原型上,以便在整个应用程序中访问
Vue.prototype.$global = { mode: this.mode };
}
// 引入该方法判断是否是pc端,便于读取mode状态
isPc() {
return this.mode === "pc";
}
}

export const settingService = new SettingService();

整个项目适配


pc端和移动端各自展示的窗口样式是不同的,所以需要在容器中设置不同的样式


// router.js
{
path: "/",
component: settingService.isPc() ? PcLayout : MobileLayout,
name: "layout",
redirect: "/notice",
}

// pc端
<template>
<div class="layout">
<layout-header />
<div class="layout-kv"></div>
<router-view></router-view>
<layout-footer />
</div>
</template>

// 移动端
<template>
<div class="layout">
<layout-header />
<router-view class="layout-body"></router-view>
<layout-footer />
</div>
</template>

在App.vue中设置了 <body> 元素的 screen-mode 属性,属性值为 settingService.mode。这样,通过在 <body> 元素上设置这个属性,可以影响到整个页面中使用了相应选择器的样式。


<template>
<loading v-if="initing" />
<router-view v-else />
</template>
// App.vue
export default {
name: "App",
mounted() {
document.body.setAttribute("screen-mode", settingService.mode);
}
}

适配案例(即使用方法)


<div class="myClass1">不会覆盖</div>
<div class="myClass2">会覆盖</div>

默认为[screen-mode="mobile"]上面的样式,当切换到移动端时,下面的会覆盖上面同一类名的样式。


.myClass1 {
color: red
}
.myClass2 {
color: red;
font-size: 16px;
}

[screen-mode="mobile"] {
.myClass2 {
color: blue;
font-size: 32dpx;
}
}

解决页面显示的是缓存的内容而不是最新的内容


// 浏览器回退强制刷逻辑
window.addEventListener("pageshow", (event) => {
if (event.persisted) {
window.location.reload();
}
});

监听了 pageshow 事件,该事件在页面显示时触发,包括页面加载和页面回退(从缓存中重新显示页面)。



  • window.addEventListener("pageshow", (event) => {...});: 给 window 对象添加了一个 pageshow 事件监听器。当页面被显示时,这个监听器中的回调函数将被执行。

  • if (event.persisted) {...}: event.persisted 是一个布尔值,表示页面是否是从缓存中恢复显示的。如果为 true,表示页面是通过浏览器的后退/前进按钮从缓存中加载的。

  • window.location.reload(): 如果页面是从缓存中加载的,就调用 window.location.reload() 强制刷新页面,以确保页面的状态和内容是最新的。


这种逻辑通常用于解决缓存导致的页面状态不一致的问题。在有些情况下,浏览器为了提高性能会缓存页面,但有时这可能导致页面显示的是缓存的内容而不是最新的内容。通过在 pageshow 事件中检测 event.persisted,可以判断页面是否是从缓存中加载的,如果是,则强制刷新页面,确保它是最新的状态。


实时监听登录状态设计(操作浏览器前进回退刷新)


popstate 事件监听器,它会在浏览器的历史记录发生变化(比如用户点击浏览器的后退或前进或刷新按钮,或者执行了类似 history.back()history.forward()history.go(-1) 等 JavaScript 操作导致页面的 URL 发生了变化)。但使用router.push之类的操作不会触发。


 window.addEventListener("popstate", () => {
if (!settingService.hasUser() && !isLogin()) {
router.push("/login");
return;
}
});

对用户进行埋点(埋点时机)


埋点是对用户的一些信息进行收集,比如用户登录网站的时间,用户的昵称等等。


业务功能


1. 阅读须知,滑到底部并且勾选了同意按钮才能执行下一步


image.png
<div
ref="scrollContainer"
style="height: 340px;overflow-y: auto;"
@scroll="handleScroll"
>

文本内容
</div>
<div>
<input @change="handleScroll" type="checkbox" v-model="isChecked" />
<label for="customCheckbox">I know and satisfy all the conditions</label>
</div>
<!-- 执行下一步按钮 -->
// 如果阅读完了并且勾选了同意按钮,则可以执行下一步,否则不能
<button
v-if="isReaded && isChecked"
@submit="gotoPage"
/>

<button v-else disabled/>

// 创建响应式 ref
const scrollContainer: any = ref(null);
// 是否阅读须知到底部
let isReaded = ref<boolean>(false);
// 是否勾选同意
const isChecked = ref<boolean>(false);

// 滚动事件处理逻辑
const handleScroll = () => {
if (scrollContainer.value) {
// 判断是否滚动到底部
1const height = scrollContainer.value.scrollHeight - scrollContainer.value.scrollTop;
const isAtBottom = height <= scrollContainer.value.clientHeight + 20;
if (isAtBottom && isChecked.value) {
// 表示已经阅读完了并且勾选了
isReaded.value = true;
}
}
};


【1】// 滑动框的总高度 scrollContainer.value.scrollHeight = 974


// scrollContainer.value.scrollTop = 滑动条距离顶部的距离


// 滑动框的可见高度 scrollContainer.value.clientHeight = 340


// 当scrollHeight-scrollTop 达到340时,即滚动到底部了


// 在上述基础上增加一个区域20,即360,防止不同设备的滚动条滚动高度不一致



2. 勾选原因才能执行下一步


image.png

该部分主要是勾选了“other"才会弹出文本框,并且后端传的数据是数组,因此需要对其进行处理。


<div v-for="option in options" :key="option.id">
<input
type="radio"
:id="option.id"
:value="option.id"
name="group"
v-model="selectedOption"
/>

<label :for="option.id">{{ option.label }}</label>
</div>
// 只有勾选了btn4才会展示
<textarea
v-if="selectedOption === 'btn4'"
v-model="textareaValue"
placeholder="If you do have any comments or suggestions please fill in here"
>
</textarea>

// 如果勾选了按钮,或者选择勾选了btn4并且输入了值
<button
v-if="(selectedOption && selectedOption !== 'btn4') || textareaValue"
@submit="gotoPage"
/>

<button v-else disabled />

const textareaValue = ref("");
const selectedOption = ref(null);
// 初始化原因列表
let options = ref([
{ id: "btn1", label: "1" },
{ id: "btn2", label: "2" },
{ id: "btn3", label: "3" },
{ id: "btn4", label: "4" },
]);

// 后端传的数据为["原因1","原因2","原因3","原因4"],需要进行处理
options.value.forEach((option, index) => {
option.label = resp.reason[index];
});

const gotoPage = () => {
// 把数组对象变为纯数组,并且由于other的值为文本输入值,需要进行判断
const reasonList = options.value.map((item) => {
if (selectedOption.value === item.id) {
if (selectedOption.value === "btn4") {
return textareaValue.value;
} else {
return item.label;
}
}
// 如果没有匹配的项,返回 undefined
});
// 最后数值为[undefined, "don't like this game", undefined, undefined]

// 再从reasonList中[undefined, "don't like this game", undefined, undefined]筛选出来原因即可
let reason = reasonList.filter((item) => item !== undefined)[0];

router.push({
path: "/reconfirm",
query: { reason: reason }
});
};


3. 文本框右下角显示输入值和限制


主要是样式,通过定位来进行布局。


image.png
<div v-if="selectedOption === 'bnt4'" style="position: relative">
<textarea
v-model="textareaValue"
:maxlength="maxCharacters"
>
</textarea>
<div
:class="{
'character-count': true,
'red-text': textareaValue.length === maxCharacters,
}"

>

{{ textareaValue.length }} / {{ maxCharacters }}
</div>
</div>

// 限制最大输入字数
const maxCharacters = 140;
const textareaValue = ref("");

.character-count {
position: absolute;
right: 10px;
bottom: 10px;
color: #888;
font-size: 12px;
}
.red-text {
color: red;
}

4. 输入指定的字才能执行下一步


image.png

主要是对@input的使用,然后进行判断


<div>
<textarea
type="text"
v-model="textareaValue"
:placeholder="reConfirmText"
@input="inputChange"
/>

</div>
<button v-if="isEqual" @submit="gotoPage" />
<button v-else disabled />

const reConfirmText = "I confirm to delete Ninja Must Die account";
const textareaValue = ref("");
const isEqual = ref(false);

const gotoPage = () => {
router.push("/hesitation");
};

const inputChange = () => {
if (textareaValue.value === reConfirmText) {
isEqual.value = true;
} else {
isEqual.value = false;
}
}

5. 登录界面需要使用iframe全屏引入


<iframe src="example.com" class="iframe"></iframe>

.iframe {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: none;
margin: 0;
padding: 0;
overflow: hidden;
z-index: 9999;
}

其他


代码优化(导师指点)



  1. 关于跳转路径的变量,用env传递,不要写死

  2. 常量尽量抽离出来,可以做成枚举的做成枚举

  3. 关于find,map之类的函数,能抽离出来的抽离出来,不要直接用,太抽象了


使用到的git操作(非常规)


1. 从一个仓库的代码放到另一个仓库上



场景:从第一个仓库中拉取代码到本地(比如团队中的模板仓库),但你需要把本地开发的代码(处于第一个仓库)推到第二个仓库中(真正开发仓库)



但你首先得在仓库上加ssh地址,打开powershell粘贴下述命令


ssh-keygen -t rsa -C "xxx@xxx.com"

回车到底


image.png


cat ~/.ssh/id_rsa.pub

复制所有


image.png

打开仓库,找到SSH Keys复制上去点击Add key即可


image.png
image.png
image.png

image.png


接下来就是正式操作了


git remote remove origin
git remote add origin xxx(目标的仓库ssh地址)
git checkout -b 'feature/zyj20231114'(在目标仓库新建一个开发分支)
git push --set-upstream origin feature/zyj20231114
git add
git commit
git push

2. 提交一个空白内容的提交



场景:由于是新项目,创建完主分支后,后端才会其打镜像,但需要前端再提交一次来触发dockek里镜像更新的脚本。(应该是这样,我个臭前端怎么可能太清楚后端弄镜像的啊,)



git commit --allow-empty -m “Message”

作者:吃腻的奶油
来源:juejin.cn/post/7311368716804603944
收起阅读 »

现在工作很难找,不要和年轻人抢饭碗

今年9月结束了和美团三年的合同后,对于步入中年的我,想让自己停一下,对自己的人生价值进行深入而系统的思考。 曾经我有一个梦想,不对,是有很多梦想,比如成为数学家、科学家、飞行员、宇航员或将军,哪一个才是我真正的梦想?那个我愿意用下半生去奋斗去拼搏的梦想? 结合...
继续阅读 »

今年9月结束了和美团三年的合同后,对于步入中年的我,想让自己停一下,对自己的人生价值进行深入而系统的思考。


曾经我有一个梦想,不对,是有很多梦想,比如成为数学家、科学家、飞行员、宇航员或将军,哪一个才是我真正的梦想?那个我愿意用下半生去奋斗去拼搏的梦想?


结合自己的名字叫天文,基于自己所学的专业、过往的经历、资源、国家政策导向和人类社会的发展趋势,我选择了航天方向的梦想,从点燃孩子们的航天梦开始,成为航天领域的企业家。


大环境比想象的差


当我把创业的想法,分享给身边的家人朋友时,几乎所有的人都和说,现在经济环境这么差,工作这么难找,还是要慎重,不要轻易去创业。


因为儿子的托育机构跑路了,我需要协助看娃,即使有合适的工作,也不能立即去上班,所以我基于老婆的公司尝试去招人。


通过招聘,我发现,不管是艺培行业的老师,还是互联网行业的产研人员,失业的比例都很大,很多23年的应届生,毕业后一直找不到工作,干脆国庆期间就离开深圳回老家了。


对于产品经理的实习生岗位,每天都有大量在香港大学、香港中文、香港科技、香港理工、香港城市、新加坡大学、清华大学以及很多国内的985的硕士前来应聘我们的岗位。


WechatIMG157.jpg


社招也是一样,很多名校毕业的,找了好几个月都没有合适的工作,普通学历的产研人员,可能连面试机会都很少。比如前端方向,只要我在 boss 开放招聘,每天都会有几百人主动找我,看都看不过来。


而猎头约我去面试,听到我因为要创业,无一不说现在环境很差,还是好好考虑一下她们推荐的岗位吧。


WechatIMG51.jpg


过去一年我接触过的一些机会


对于已经超过35岁的码农,只要满足企业的用人诉求,即使环境再差,也是有很多工作机会的,在此和大家聊一聊,过去一年我都参加或拒绝了哪些公司的面试。


去年7月,我就多次向领导提了离职,那时也想创业,但没有现在强烈,所以还是认真地找了找工作,那时的面试机会,应该是今年的三倍以上。


首先字节的岗位最多,至少一半猎头推荐的都有字节的岗位,我选择了面试客服体验部,先是在上海的前端总监加了我微信,入职后我向他汇报,管理深圳16人左右的团队,一面的面试官应该是我入职后的下属,但我不是很想去,所以面试随便聊了聊,加了微信,后面还约了个饭。


后续字节的岗位,虽然每周都有猎头给我推荐,但是我都没有答应面试,最近答应约了的面试,因为决定创业了,所以主动取消了,飞书的HR还专门打电话让我再考虑考虑,看来是确实招人,而且他还说有很多方向可以选。


去年我8月我面了美的集团的大前端岗位,通过了四轮,面完HRVP后,已经约好了和CEO面试,但因为我决定留在美团再干一年,做我想做的人工智能,所以我主动取消了面试。这应该是我这几年面试过最高规格的面试,每一轮都是三个以上面试官,入职后直接向CEO汇报,整合集团大前端方向所有的研发,需要管理100多人。


当然也有其他一些高管职位,今年夏天也接到一个猎头推荐我面试贝壳的大前端负责人,直接向CTO汇报,管理200多人的团队,原来的负责人已经提离职,岗位需要保密。


对了,还有编程猫,因为我创业的方向和少儿编程有关,所以也想和他们聊一聊,面试的是高级技术总监,接手联合创始人管理的编程系统研发团队,一面我的是一个大姐,她是web前端负责人,管理20多人,我上来当HR的面,指出了几个明显的bug,让她不高兴了,所以她没让我过,本来还想和他们老板聊一聊。


去年8月中旬后,我基本就不答应面试了,但经常协助一个离职的美团同学面试,期间他面试了字节的多个部门,比如我推荐的web剪映负责人,他因为是web图形学方向的,不够匹配,只通过了两轮。后面他拿到了蔚来手机、小米、阿里、腾讯和万兴科技的offer。


最后,他犹豫是去腾讯还是万兴科技,我们还吃夜宵专门讨论了一下。腾讯他面了三个部门,第三个部门才给的机会,因为他马上要去香港大学MBA,所以选择了腾讯,不带团队。但我建议他去万兴,因为是负责一个事业部,管理近百人团队,对职业发展更好。


今年我好像只答应了两三个面试,其中一个小红书的质效前端负责人,猎头忽悠我年薪可以给到250万到400万,后来又不招了,最近约上了,聊了聊,入职后管理团队应该只有五六人,应该不会有那么高的薪资,而且必须去北京或上海,所以通过了,我也不能去。


还有参加了希音的面试,是新成立的基础架构部,一面我的是一个腾讯云过去的前端同学,聊得还可以,二面和部长聊了聊,感觉他压力有点大,我过去后需要自己找方向,比如端智能,不带团队。


突然想起来,我还面了金山云,她们的HR很热情,所以我答应聊了聊,一面的面试官,对我反馈很好,但他们招聘的岗位职级比较低,不匹配,而且他们在珠海,通过了我也不会去。


有个比较好的机会,但没有参加面试,这华为孟晚舟直接负责的总部研发团队,入职后管理一百人左右的大前端团队,离我家比较近,还能接触到华为未来的掌门人。


也接到过一些外企的面试邀请,但都没参加。


工作难找,不仅要把机会留给年轻人,还要创造更多的机会


很显然,当前我们正处于经济大萧条的前夜,或者已经在经济危机之中,但正如谭sir视频中一个老人说的:要向前看。


危机有”危“和”机“组成,意味危险和机会共存,消极的人只会抱怨危险,只有积极的人才能抓住机会,危险越大,机会越大。


很多伟大的公司,都是诞生于危机之中。新的一年,国际国内经济大环境发生了很大的变化,国家的工作重点逐步从抵抗疫情转向振兴经济。


中国经济在经历了近四十年的高速增长后,宏观经济增速放缓属于必然。


一是历史上的日本、德国,在二十世纪五六十年代和七十年代都有过非常高的增长期,然后都慢慢放缓。中国是一个特例,过去四十年中国GDP的增长保持着两位数,所以未来中国GDP的增长放缓至4%—5%符合历史规律。


二是中国的人口红利和流量红利时代已经结束。中国统计局数据显示,中国25-69岁之间的人口,在过去三十年(1990-2020年)增长了76%,但是今后三十年(2020-2050年)会从9.4亿人降到7亿人。


中国的互联网红利见顶,中国互联网络信息中心数据显示,2007年至2017年中国互联网用户的上网时长增长了36倍,相当于每年平均增长约43%。但是在2017年至2022年只增长了1.5倍,相当于每年平均增长约8%。


虽然短期至中期内,中国经济将不可避免地经历转型阵痛,但从长期看,中国每年4%-5%的经济增长速度还是远高于其他主要的大型经济体。牛津和哈佛发布的研究数据显示,从GDP复合增长率来看,中国是美国的1.8倍、是德国的2.3倍、是日本的2.5倍。


另外一个因素是,在亚洲国家中,中国的经济总量占有绝对领先的地位。麦肯锡前段时间做了数据统计,中国2022年的GDP约18万亿美元,到2030年,假设每年仅按2%的速度增长,中国GDP的增量就相当于印度今天的GDP总量。


虽然中国已经是全球第二大经济体,之所以有这么大的经济增长潜力,是因为从世界的角度来看,中国的人均GDP和人均消费还很低,世界银行数据显示,2021年,中国人均GDP约是美国的1/6,人均消费约是美国的1/9。


同时,中国的城市人口体量巨大且仍在不断增长。中国今天的城镇化率是65%,未来5-10年可能增长到75%甚至是80%,预计约1.4亿人口会变成新增城镇人口,这一人口增量相当于美国总人口的40%。


所以,我们要对国家的发展有信心,困难只是暂时的。虽然我上有老,下有小,但也不至于没饭吃,但很多年轻人,他们需要一份工作,才能在大城市生存。


所以需要更多像我一样的中年人,不仅不要和年轻人抢工作机会,还要积极为年轻人创造新的工作机会。地球竞争太激烈了,我们的未来在上天入地(这好像是我在美团的老板王兴说的)。


我打算干啥


我从小就有一个航天梦,大学选择了航天测控和卫星导航相关的专业,毕业后成了通信军官,但没能进入航天系统,对未来有些迷茫,于是选择了退役。


退役后进入了外企,后面又去了美团等互联网公司工作了几年,如今已经是一双儿女的父亲。前段时间,陪儿子读了一本以登月主题的绘本,让我逐渐找回了曾经的梦想。


好奇是人类的天性,也是社会进步的动力。探索太空不仅可以满足人类的好奇心,更可以为人类的未来发展提供了无限的可能性。


太空探索是一项长期而复杂的事业,需要一代代有航天梦的人才持续加入,这就是我们创业的出发点,希望同大家一起点燃孩子们的航天梦:通过以太空为主题的绘本,引入绘画创作的方向,并将绘画作品作为图形编程的素材,完成各种编程创作任务,帮助孩子们掌握 带领人类飞离太阳系 需要学习掌握的各种知识技能。


等这些孩子长大以后,我们再把他们招聘到我们制造航天器的公司,实现让人类可以进行商业星际旅行。


我们正在研发的系统


两个小程序,蜗牛绘馆和艺培助理已经发布到线上,等商业模式完全跑通后,再同步做app。


3D展馆:以 3D 的方式展示学生的绘画作品,帮助机构推广招生。


AIGC工具:智能抠图、以文生图、数字人、PPT制作、智能成片等。


海报设计:参考业界的稿定设计、美图等精品,为艺培机构提供精品海报和海量AI生成的素材,支持通过PC端和小程序下载。


课件系统:提供自营的绘本+绘画+编程的在线特色课件,并打造一个课件生产生态,提供各种类别的课件PPT。


编程系统:可导入图片和绘画作品,为学生编程提供丰富的素材。


长远规划及招聘计划


未来三到五年,我们希望可以做到:



  • 蜗牛绘馆:在中国多个核心城市开几百家直营绘馆,月活家长达到几十万,会员用户达到几万,实现年营收几亿。

  • 艺培助理:月活达到几百万,服务几千家加盟店,拥有几万普通会员,实现年营收几十亿。

  • 各类社区:面向成人,对于不同的兴趣方向,打造不同的内容社区。

  • 周边生态:基于太空主题,研发相关的绘本、教具、玩具、服装等。

  • 航天制造:研发自己的航天飞行器,探索飞离太阳系的各种技术。


发展顺利的话,我们的组织架构及规模设想:



  • 基础技术部(500人):提供私有云服务、企业效能系统及AIGC的技术基座。

  • 绘馆事业部(500人):负责线下绘馆门店业务的开展,教学及教务为主。

  • 换购事业部(200人):负责二手交易平台的系统及线上线下运营。

  • 课件事业部(300人):负责课件系统的研发及课件社区的运营。

  • 编程事业部(300人):负责少儿编程相关的课程及系统研发。

  • 营销事业部(200人):负责3D展馆、海报、视频制作等的研发及业务开展。

  • 玩具事业部(100人):研发航天相关的玩具、服装或教具。

  • 公益事业部(100人):负责把家长换课的闲置物品捐赠给大城市农民工或贫困地区的孩子,组织志愿者远程给偏远地区孩子上科创综合课。


总结


WechatIMG239.jpg


和平年代,虽然没有战斗,但更需要军人的勇气,邓小平当年指出,军队建设要服从国家大局。虽然我退役了,但军人的血性刻在骨里,相比英雄的先辈们在抗日战争和抗美援朝中付出的鲜血,创业的艰难算什么呀~


我们要把航天梦一代代的传下去,我们将付出任何代价、忍受任何重负、应付任何艰辛、支持任何朋友、反对任何敌人,以确保梦想的存在与实现。


有梦想的人才有灵魂,才会快乐。我们为梦想所奉献的精力、信念和忠诚,将照亮我们的国家和身边的人,而这火焰发出的光芒定能照亮全世界。


作者:三一习惯
来源:juejin.cn/post/7309158055018053658
收起阅读 »

四年沿海城市,刚毕业,一年3家公司

去年自己也写了一篇总结,看了去年的总结和目标,感觉今年过得跟gs一样,哎。🥹 正如标题所言,上大学到现在一直呆在沿海城市。然后今年毕业,这一年换了三家公司。下面来讲讲自己的最近的经历吧。 本人大学坐落于辽宁锦州(一个真正的“海角”城市)。 那边有笔架山,一个...
继续阅读 »

去年自己也写了一篇总结,看了去年的总结和目标,感觉今年过得跟gs一样,哎。🥹


正如标题所言,上大学到现在一直呆在沿海城市。然后今年毕业,这一年换了三家公司。下面来讲讲自己的最近的经历吧。


本人大学坐落于辽宁锦州(一个真正的“海角”城市)。



  • 那边有笔架山,一个四面环海的山峰,长得像笔架,得名笔架山,开学第一天就趟水过去了。

  • 还有就是最常去的白沙湾(和对象去了好多次了)。

  • 还有就是锦州的夜市、公园都被逛烂了。还记得在夜市吆喝着“嘎嘎香的锦州烤肉吆!!!”

  • 还有北普陀山。

  • ...


大三下学期去大连呆了小半年这真的是“生不如死”,在学校总想着逃离,那种渴望自由的心想必在像我这种双非学生都有吧。就觉得在学校就是阻挡老子发财,满满的全是限制。垃圾桶里不能放垃圾,床上不能睡人...我只想骂他bbzz。😅


事实证明,不考研,对于大部分学生来说,早早出来工作就是上大学最重要的事情。一切阻挡出校实习的想法都是罪恶的。每天在学校活在那一亩三分地能有啥用,学校一边强制这一边强制那...


image.png


想想都可笑,不过最后拿到面试机会老师还是提供教室让自己面试。非常感谢。


虽然这里是满满的抱怨,大学时期也会不时给自己小小的惊喜。


MergedImages.png


MergedImages.png
然后在大三暑假自己也拿到了一个满意的offer,去了杭州电魂,对于一个没有经验的学生来说,拿到一家上市游戏公司的offer对于我当时来说也是蛮开心的。当时也没啥经验,投递时间也不是很好(6,7月份),行情也不是很好,又加上之前面试字节遭到打击(当时初生牛犊不怕虎,大三下期人生第一次面试就碰上了字节,面之前有多紧张,面之后就有多狼狈。😊)事后疯狂总结八股,网络等等。可以看下这个些专栏



导致一度怀疑自己。7月初一个星期全在面试,搞得自己精疲力尽,很累,也拿到了几个offer,还有一些在走流程,刚好那段时间我们也放假了,就回家呆了一个多星期,然后就去了杭州。在电魂的这段时间真的满满的幸福感。(独立大厦,每个节日满满的幸福感,活动,团建很频繁,部分大佬们都很和善...)这里就不在赘述了,感兴趣的可以看去年总结的文章 《一位初入职场前端仔的年度终结 <回顾2022,展望2023>》


很不幸的是,今年上半年,公司业绩不好,裁了很多人,实习生也基本都清退了。这就是今年的呆的第一家公司啦。很感谢,充满感激。💗


然后自己又要从头开始啦,当时觉得好难搞,毕竟自己在快被毕业的时候清退,加上没有参加23届秋招,大家都知道今年行情啥样,就很担心自己成为了肄业青年了。😟


不过还好,离职后,所有招聘软件疯狂投递,面了接近两个星期,拿到了厦门一家储能制造业offer后,就开始摆烂了。因为当时觉得他开了条件和福利都还不错。最后面试都直接开摆,还拒了几家面试。😭


这就为自己今年的遭遇埋下的伏笔。


到了7月初,满怀期待的来到了厦门(又一海滨城市😂),觉得自己可以在这家年轻的公司闯出一片天,md,真的是自己天真了。入职当天我就傻眼了。签了一大堆协议,签了一个多小时。无语子🥲


image.png


然后接下来一个星期让带我们学习企业文化,储能电池知识,娱乐等等。每天搞到8点多,人傻了。刚来,还没入职居然培训到8点多。(当时觉得还好,毕竟他们给我们申请了下班费)然后就觉得以后狂狂加班,赚他个大几千块(这真的是狠狠地打我的脸啊,后面介绍恶心操作😭)


入职当天,也挺xx的,那个叫yq的人,上来叫我看个bug, 源码刚拿到手,和我说了下这个需求,几分钟后和他确认了一下需求,他就不耐烦了,说"能不能行,不行不让你改了,找别人去。", 无语子啊。最后找不到人改(我师父当时刚好请假了),最后又回来让我改了。我要不是“胆小怕事”,直接干他了,xx玩意。


这公司还有些迷之操作,办公发个破笔记本,显示屏不给配,有时候起个项目要1h+,直接骂娘了。还不让带自己的电脑办公。


我们入职第二个星期,整个项目的需求就来了,然后我们的噩梦就来了。从那开始,加班没断过,最骚的操作是,加班申请不给审批。9月份加班60h+,就审批了10h不到。而且加班完9点过后公司没有班车了,打车回宿舍还要自己掏钱,我人真的无语喽。🥹 最最无语的是周末加班。从早上9点到晚上12点,都不给批,666。


微信图片_20231211004224.jpg


之所以效率这么低,那就是这项目有个“牛逼的”产品,上午确定好需求,上午刚开发好,下午就改需求。当天还要上线,这都是常规操作。真的很棒嘞。🤣


最搞笑的是,这公司公积金一个月给你交100💩, 直接笑死,每个月在掘金写文章都是他的2-3倍。


真的很庆幸自己在11月初离开了这种魔鬼公司。这种公司没有任何可以值得留恋的地方,但是还是找到了几个聊得来的朋友的。😃


所以说,同志们,还是要去互联网公司发展啊。远离这种lj的制造业公司。


由于女朋友在上海,所以离开公司后就来到上海(又来到了另一个滨海城市),一个星期左右,拿到了目前这个家公司的offer,成功涨薪4k,这家公司做的产品都很牛逼,互联网取证,给公安做网络取证用的,然后自己也进入了比较有挑战性的项目组,非常感谢可以给我这次机会。


入职三个星期左右了,福利待遇都非常好,每天有吃不完的零食,到点下班,真的爽歪歪。即使不让我加班,我也宁愿待在公司学习一会,就想当时在电魂一样,天天窝在公司。


也不说明年目标了,目标都是给别人看的,结果才是给自己看的。希望自己可以胜任这份工作,为公司产出优秀的产品的同时,也让自己变得更优秀。


仅此,给刚毕业的自己一个教训和经验,最好的永远在后面,而能看到后面的,永远是一直在进步和坚持的那群人。


加油,少年!


今天翻阅旧照片,贴贴美美的照骗。


MergedImages (1).png


作者:Spirited_Away
来源:juejin.cn/post/7310895905573716005
收起阅读 »

一位初入职场前端仔的年度终结

回顾这一年来的变化,只能说是平平无奇。于我而言从焦虑到不焦虑亦是从学生时代进入职场。 1 - 7月 平平无奇 今年一开始就“逃离”了学校,由于我们的专业培训方案是大三下学期去实训公司待上几个月。所以这就是“逃离”学校的最好时机。大家都知道,对于我们普通本科生而...
继续阅读 »

回顾这一年来的变化,只能说是平平无奇。于我而言从焦虑到不焦虑亦是从学生时代进入职场。


1 - 7月 平平无奇


今年一开始就“逃离”了学校,由于我们的专业培训方案是大三下学期去实训公司待上几个月。所以这就是“逃离”学校的最好时机。大家都知道,对于我们普通本科生而言,在学校是最浪费时间的事情。(至少对于我是这样的)。前几天,在学校考研的朋友和我说,感觉现在很迷茫,不知道怎么办。自己专注一年多的时间,还是没有好的结果,事实本是如此。不如早早实习工作获取职场经验。


1, 2月放假在家,闲着没事就跟着王洪元老师学习前端的一些知识。


3月份去大连每天两点一线的公司 -> 宿舍两边跑。


4月份不满于现状,在boss上投了几份简历,结果就字节约了面试,当时觉得自己学的可以了,就想试一试。结果真的是让我重新认清了自己。从那时起我就泡在了牛客等面试社区中。一边学习新知识,一边总结的面试题。


image.png


5,6 月一边修改简历一边分析总结面试题。
image.png


直到6月底,觉得目前已经系统的了解了一下前端相关的面试题,并且梳理了一下技术架构。觉得还是需要尝试一下。每天在boss上投递简历,基本没人回复,但是也约了一些公司面试。还是积累了一些经验的。


你们应该知道面试是非常累的,非常消耗精力,还好那个时候,我们宿舍距离海边很近,我每天都会跑到海边观望,真的感觉大海治愈我的一切疲惫。


image.png


image.png


image.png


image.png


image.png


7月初已经面了一些公司的hr面,感觉自己累了,就没有再去投递简历面试了。我记得7.14号回家的时候,还有公司打电话约面试。我直接拒掉了。


7 - 目前 充满激情与动力


在评估了一下手里的offer后,在7.25号来到了目前的一家游戏公司实习。我很庆幸自己初入职场就来到这个充满善意 (此处省略一万字,这个部门同事合作都好多年了,特别友好...) 的部门。


在这里活不是很多,但是自己也做了一些事情的。



  • 维护公司内部的一个大型后台系统。

  • 通过amis低代码重构公司内部的一个服务型后台系统。

  • 重构公司手游充值页面,主要是ui重构。

  • 合作开发微信h5公众号社区项目。

  • 开发一个游戏官网。

  • ...


话说这半年来,大大小小的需求都完成了这么多了。


image.png


这些内容对我来说都有很大成长,其实在梳理这篇文章之前,我感觉今年自己没有干啥事,但是这样一看还是做了一些事情的。


在这里工作中也总结了一些文章。


image.png


给你看看我们公司的福利。 嘻嘻



  • 迎新聚餐, 我们部门的传统


在星光一期小厨师海鲜,非常丰富的一场晚宴。


image.png



  • 情人节


这一天刚进门,hr小哥哥小姐姐就会把这束玫瑰花送到你手中。惊喜。


image.png



  • 公司成立日


公司成立日射箭获得的奖品


image.png



  • 公司的迎新晚宴和活动


美食和奖品都是很好滴。


image.png
image.png



  • 10.24程序员节


死缠烂打最后获取一等奖品之一灭霸乐高。 其实我想要那个键盘滴,但是没有货了。呜呜呜~~~


image.png



  • 中秋节


说实话观赏感十足。


image.png
image.png
image.png



  • 春节礼盒


一箱苹果加一个零食礼盒(礼盒包邮到家), 苹果真甜~~


image.png



  • 还有就是部门不时的会请奶茶,kfc等等


hihi, 疯狂星期四,来份kfc。


image.png


我与掘金


image.png


我和她


其实选择杭州还有一个原因就是距离 我家猪 很近,因为她在上海工作。


这一年,有假期就会去上海找她玩,反正感觉挺好的这样。保持对彼此的热度。


image.png


image.png


话说旁边那栋楼比东方明珠高吧


image.png


12.31号,把握2022的最好时刻,一起做手工。


image.png


image.png


期待的2023



  • 了解一下web3.0

  • 深入学习一下微前端,2022在闲的时候看了一些demo,跑了一下功能,大致了解了工作原理。

  • 搭建一个组件库。(作为练手项目)

  • 快滚出学校了,那当然是写论文啦。

  • ...


加油,愿你我前程似锦,愿你我lucky forever


大家也来说说自己的故事吧。2023, 一起加油,我们来啦。


作者:Spirited_Away
来源:juejin.cn/post/7188374796114067511
收起阅读 »

万事不要急,不行就抓一个包嘛!

一、网络问题分析思路1、问题现象确认问题现象是什么丢包/访问不通/延迟大/拒绝访问/传输速度2、报文特征过滤找到与问题现象相符的报文3、问题原因根据报文特征和发生位置,推断问题原因安全组/iptables/服务问题4、发生位置分析异常报文,判断问题发生的位置客...
继续阅读 »

一、网络问题分析思路

1、问题现象

确认问题现象是什么

丢包/访问不通/延迟大/拒绝访问/传输速度

2、报文特征

过滤找到与问题现象相符的报文

3、问题原因

根据报文特征和发生位置,推断问题原因

安全组/iptables/服务问题

4、发生位置

分析异常报文,判断问题发生的位置

客户端/服务端/中间链路.....

二、关注报文字段

  1. time:访问延迟
  2. source:来源IP
  3. destination:目的IP
  4. Protocol:协议
  5. length:长度
  6. TTL:存活时间(Time To Live)国内运营商劫持,封禁等处理
  7. payload:TCP包的长度

三、本机抓包

curl http://www.baidu.com

为什么不使用ping请求?

因为ping请求是ICMP请求:用于在 IP 网络上进行错误报告、网络诊断和网络管理。而curl请求是HTTP GET 请求。

追踪TCP流,可以看到三次捂手——》数据交互——》四次挥手

image-20231211161849484

image-20231211161958078

四、案例 TTL异常的reset

业务场景: 客户端通过公网去访问云上的CVM的一个公网IP,发现不能访问

image-20231211170925943

image-20231211171135366

TTL突发过长——》判断云上资源有无策略上的设置——》公网端判断原因运营商的封堵(TTL 200以下)

解决方案:报障运营商

四、案例 访问延迟-判断延时出现位置

业务场景: 客户同VPS内网,两CVM互相访问

image-20231211173214992

image-20231211173103998

目的端回包0.006毫秒——》链路无问题,回包时间过长——》CVM底层调用逻辑问题

五、案例 丢包

业务场景: 云上CVM访问第三网方网站失败,影响业务

image-20231211175015273

五、案例 未回复http响应

业务场景:云上CVM访问第三网方网站失败,影响业务

image-20231211175432145

image-20231211175702839

ping正常,拨测正常——》客户端发送HTTP请求,目的端无HTTP回包,直接三次挥手——》确认根因服务端不回复我们——》推测:运营商封禁或者第三方网站设置了访问限制

六、基础&常见问题

1、了解TCP/IP四层协议

image-20231018154301479

2、了解TCP 三次握手与四次挥手

SYN:同步位。SYN=1,表示进行一个连接请求。

ACK:确认位。ACK=1,确认有效;ACK=0,确认无效;。

ack:确认号。对方发送序号 + 1

seq:序号

image-20231211111546472

image-20231018155125516

FIN=1,断开链接,并且客户端会停止向服务器端发数据

image-20231211112144895

image-20231018155211125

3、Http传输段构成(浏览器的请求内容有哪些,F12中Network)

请求:

  1. 请求行(Request Line):包含HTTP方法(GET、POST等)、请求的URL和HTTP协议的版本。
  2. 请求头部(Request Headers):包含关于请求的附加信息,如用户代理、内容类型、授权信息等。
  3. 空行(Blank Line):请求头部和请求体之间必须有一个空行。
  4. 请求体(Request Body):可选的,用于传输请求的数据,例如在POST请求中传递表单数据或上传文件。

响应:

  1. 状态行(Status Line):包含HTTP协议的版本、状态码和状态消息。
  2. 响应头部(Response Headers):包含关于响应的附加信息,如服务器类型、内容类型、响应时间等。
  3. 空行(Blank Line):响应头部和响应体之间必须有一个空行。
  4. 响应体(Response Body):包含实际的响应数据,例如HTML页面、JSON数据等。

这些组成部分共同构成了HTTP传输过程中的请求和响应。请求由客户端发送给服务器,服务器根据请求进行处理并返回相应的响应给客户端。

4、DNS污染怎么办

DNS污染是指恶意篡改或劫持DNS解析的过程,导致用户无法正确访问所需的网站或被重定向到错误的网站。如果您怀疑遭受了DNS污染,可以尝试以下方法来解决问题:

1、更改本地的DNS。 公共DNS服务器Google Public DNS(8.8.8.8和8.8.4.4)

2、清理本地DNS缓存。 systemctl restart NetworkManager。这将重启NetworkManager服务,刷新DNS缓存并应用任何新的DNS配置。

3、使用HTTPS。使用HTTPS访问网站可以提供更安全的连接,并减少DNS污染的风险。确保您访问的网站使用HTTPS协议。

5、traceroute路由追踪跳转过程

img

当您运行traceroute http://www.baidu.com命令时,它将显示从您的计算机到目标地址(http://www.baidu.com)之间的网络路径。它通过发送一系列的网络探测包(ICMP或UDP)来确定数据包从源到目标的路径,并记录每个中间节点的IP地址。

以下是一般情况下,traceroute命令可能经过的地址类型:

  1. 您的本地网络地址:这是您计算机所连接的本地网络的IP地址。
  2. 网关地址:如果您的计算机连接到一个局域网,它将经过您的网络网关,这是连接您的局域网与外部网络的设备。traceroute命令将显示网关的IP地址。
  3. ISP(互联网服务提供商)的路由器地址:数据包将通过您的ISP的网络传输,经过多个路由器。traceroute命令将显示每个路由器的IP地址。
  4. 目标地址:最后,数据包将到达目标地址,即http://www.baidu.com的IP地址。

img

6、Http状态码

100-199表示请求已被接收,继续处理比如:websocket_VScode自动刷新页面
200-299表示请求已成功被服务器接收、理解和处理。200 OK:请求成功,服务器成功返回请求的数据。 201 Created:请求成功,服务器已创建新的资源。204 No Content:请求成功,但服务器没有返回任何内容。
300-399(重定向状态码)表示需要进一步操作以完成请求301 Moved Permanently:请求的资源已永久移动到新位置。302 Found:请求的资源暂时移动到新位置。304 Not Modified:客户端的缓存资源是最新的,服务器返回此状态码表示资源未被修改。
400-499表示客户端发送的请求有错误。400 Bad Request:请求无效,服务器无法理解。401 Unauthorized:请求要求身份验证。404 Not Found:请求的资源不存在。
500-599表示服务器在处理请求时发生错误。500 Internal Server Error:服务器遇到了意外错误(服务器配置出错/数据库错误/代码出错),无法完成请求。503 Service Unavailable:服务器暂时无法处理请求,通常是由于过载或维护。

7、CDN内容加速网络原理

原理:减少漫长的路由转发,就近访问备份资源

1、通过配置网站的CDN,提前让CDN的中间节点OC备份一份内容,在分发给用户侧的SOC边缘节点,这样就能就近拉取资源。不用每次都通过漫长的路由导航到源站。

2、但是要达到加速的效果,还需要把真实域名的IP更改到CDN的IP,所有这里还需要DNS的帮助,这里一般都会求助用户本地运营商搭建的权威DNS域名解析服务器,用户请求逐级请求各级域名,本来应该会返回真实的IP地址,但是通过配置会返回给用户一个CDN的IP地址,CDN的权威服务器再讲距离用户最近的那台CDN服务器IP地址返回给用户,这样就实现了CDN加速的效果。

8、前后端通信到底是怎样一个过程

链接参考:juejin.cn/post/728518…

内容参考:

小林coding:xiaolincoding.com/network/

哔哩哔哩:http://www.bilibili.com/video/BV1at…

Winshark:http://www.wireshark.org/


作者:武师叔
来源:juejin.cn/post/7311159008483983369

收起阅读 »

Android 放大镜窥视效果

前言 放大镜效果是一种常用的局部图片观察效果,其本质原理依然是将原图片放大之后,经过范围裁剪然后会知道指定区域的一种效果。实际上放大效果有2种常见的效果,比如在一些购物网站,鼠标移动到的位置被放大,然后展示在侧边区域,这两者代码几乎一样,主要区别如下: 侧边...
继续阅读 »

前言


放大镜效果是一种常用的局部图片观察效果,其本质原理依然是将原图片放大之后,经过范围裁剪然后会知道指定区域的一种效果。实际上放大效果有2种常见的效果,比如在一些购物网站,鼠标移动到的位置被放大,然后展示在侧边区域,这两者代码几乎一样,主要区别如下:



  • 侧边区域观测要移动Shader或者在指定位置裁剪图像

  • 本文效果是移动区域,但是为了保证图片能尽可能对齐,需要将放大的图片向左上角偏移。


本文和上一篇《手电筒照亮效果》一样,如果没看过的先看上一篇,方便你理解本篇,因为同样的原理不会在这篇重新提及或者过多提及,都是局部区域效果实现。


效果预览


滑动放大效果


fire_62.gif


窥视效果


fire_63.gif


方法镜滑动放大实现方法


使用Shader作为载体


首先要做的是将图片放大,放大之后,我们可以利用Path裁剪图片或者Shader向裁剪区域绘制,这里我们依然使用Shader,毕竟优点很多,这里我们主要要实现2个目的。



  • Shader载入Bitmap,放大1.2倍

  • Shader向左上角偏移,对齐图片中心


      if (shader == null) {
float ratio = 1.2f;
scaledBitmap = Bitmap.createScaledBitmap(mBitmap, (int) (mBitmap.getWidth() * ratio), (int) (mBitmap.getHeight() * ratio), true);
shader = new BitmapShader(scaledBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
// 做下偏移
matrix.setTranslate(-(scaledBitmap.getWidth() - mBitmap.getWidth())/2f ,-(scaledBitmap.getHeight() - mBitmap.getHeight())/2f);
shader.setLocalMatrix(matrix);
}

事件处理


其实处理事件有很多简便的方法,但是首先得拦截事件,Android种拦截事件的方法很多,clickable就是其中之一


setClickable(true); //触发hotspot

拦截按压移动事件,这里我们使用 HotSpot 机制,其实就是触点,西方人命名习惯使用HotSpot,通过下面就能处理事件,连onTouchEvent我们都不用搭理。


  @Override
public void dispatchDrawableHotspotChanged(float x, float y) {
super.dispatchDrawableHotspotChanged(x, y);
this.x = x;
this.y = y;
postInvalidate();
}

@Override
protected void dispatchSetPressed(boolean pressed) {
super.dispatchSetPressed(pressed);
postInvalidate();
}

裁剪Canvas区域为原图区域


为什么要裁剪Canvas区域内,主要是因为你的图片并不一定能完全填充整个View,但是你使用的TileMode肯定是CLAMP,这会使得放大镜中图像的边缘拉长,现象很奇怪,反正你可以去掉试试。另外说一下,Android中似乎新增加了一种TileMode,不过还没来得及试一下。


   int save = canvas.save();
canvas.clipRect(0,0,mBitmap.getWidth(),mBitmap.getHeight());
canvas.restoreToCount(save);

绘制核心逻辑


在核心逻辑中,我们有一步要绘制区域填充颜色,主要原因是非透明区域的绘制会导致出现透视效果。


    int save = canvas.save();
canvas.clipRect(0,0,mBitmap.getWidth(),mBitmap.getHeight());
//绘制原图
canvas.drawBitmap(mBitmap, 0, 0, null);
//区域用填充颜色,防止出现区域透视,上面的区域能看见下面的区域
mCommonPaint.setColor(Color.WHITE);
canvas.drawCircle( x , y,width/4f,mCommonPaint);
//绘制放大效果
mCommonPaint.setShader(shader);
canvas.drawCircle( x , y,width/4f,mCommonPaint);
mCommonPaint.setShader(null);

canvas.restoreToCount(save);

放大镜窥视效果


其实两者代码没有多大区别,滑动放大效果主要是移动镜子,而窥视效果镜子不动,使用移动图片的方式实现。


位置计算 & 绘制


固定镜子中心在右下角


//放大平移时需要偏移的距离
float offsetX = -(scaledBitmap.getWidth() - mBitmap.getWidth()) / 2f;
float offsetY = -(scaledBitmap.getHeight() - mBitmap.getHeight())/2f;
//窥视镜圆心
float mirrorCenterX = mBitmap.getWidth() - width / 4f;
float mirrorCenterY = mBitmap.getHeight() - width/4f;

图像平移距离


(mirrorCenterX - x) 
(mirrorCenterY - y)

矩阵变换,平移事件点位置图像到右下角圆的中心


//(mirrorCenterX - x) ,(mirrorCenterY-y) 是把当前中心点的图像平移到圆心哪里
matrix.setTranslate( offsetX + (mirrorCenterX - x) , offsetY + (mirrorCenterY-y));
shader.setLocalMatrix(matrix);

绘制镜子



int save = canvas.save();
canvas.clipRect(0,0,mBitmap.getWidth(),mBitmap.getHeight());
canvas.drawBitmap(mBitmap, 0, 0, null);
mCommonPaint.setColor(Color.DKGRAY);
canvas.drawCircle(mirrorCenterX , mirrorCenterY,width/4f,mCommonPaint);
mCommonPaint.setShader(shader);
canvas.drawCircle( mirrorCenterX , mirrorCenterY,width/4f,mCommonPaint);
mCommonPaint.setShader(null);

canvas.restoreToCount(save);

总结


本篇和之前的很多篇文章一样,都是实现Canvas图片绘制,很复杂的效果我们没有涉及到,但是在这些文章中,都会有各种各样的问题和思考。总之,我们要善于利用矩阵和设计思想,绘制我们的想象。


全部代码


按照惯例,提供全部代码


滑动放大代码


public class ScaleBigView extends View {
private final DisplayMetrics mDM;
private TextPaint mCommonPaint;
private Bitmap mBitmap;
private Shader shader = null;
private Matrix matrix = new Matrix();
private Bitmap scaledBitmap;

public ScaleBigView(Context context) {
this(context, null);
}

public ScaleBigView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ScaleBigView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mDM = getResources().getDisplayMetrics();
initPaint();
setClickable(true); //触发hotspot
}

private void initPaint() {
//否则提供给外部纹理绘制
mCommonPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
mCommonPaint.setAntiAlias(true);
mCommonPaint.setStyle(Paint.Style.FILL_AND_STROKE);
mCommonPaint.setStrokeCap(Paint.Cap.ROUND);
mCommonPaint.setFilterBitmap(true);
mCommonPaint.setDither(true);
mCommonPaint.setStrokeWidth(dp2px(20));
mBitmap = decodeBitmap(R.mipmap.mm_012);

}

private Bitmap decodeBitmap(int resId) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inMutable = true;
return BitmapFactory.decodeResource(getResources(), resId, options);
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
if (widthMode != MeasureSpec.EXACTLY) {
widthSize = mDM.widthPixels / 2;
}
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);

if (heightMode != MeasureSpec.EXACTLY) {
heightSize = widthSize / 2;
}
setMeasuredDimension(widthSize, heightSize);

}
private float x;
private float y;
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
if (width < 1 || height < 1) {
return;
}
if (shader == null) {
float ratio = 1.2f;
scaledBitmap = Bitmap.createScaledBitmap(mBitmap, (int) (mBitmap.getWidth() * ratio), (int) (mBitmap.getHeight() * ratio), true);
shader = new BitmapShader(scaledBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
matrix.setTranslate(-(scaledBitmap.getWidth() - mBitmap.getWidth())/2f ,-(scaledBitmap.getHeight() - mBitmap.getHeight())/2f);
shader.setLocalMatrix(matrix);
}


int save = canvas.save();
canvas.clipRect(0,0,mBitmap.getWidth(),mBitmap.getHeight());
//绘制原图
canvas.drawBitmap(mBitmap, 0, 0, null);
//区域用填充颜色,防止出现区域透视,上面的区域能看见下面的区域
mCommonPaint.setColor(Color.WHITE);
canvas.drawCircle( x , y,width/4f,mCommonPaint);
//绘制放大效果
mCommonPaint.setShader(shader);
canvas.drawCircle( x , y,width/4f,mCommonPaint);
mCommonPaint.setShader(null);

canvas.restoreToCount(save);
}

@Override
public void dispatchDrawableHotspotChanged(float x, float y) {
super.dispatchDrawableHotspotChanged(x, y);
this.x = x;
this.y = y;
postInvalidate();
}

@Override
protected void dispatchSetPressed(boolean pressed) {
super.dispatchSetPressed(pressed);
postInvalidate();
}

public float dp2px(float dp) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, mDM);
}

}

窥视镜效果


public class ScaleBigView extends View {
private final DisplayMetrics mDM;
private TextPaint mCommonPaint;
private Bitmap mBitmap;
private Shader shader = null;
private Matrix matrix = new Matrix();
private Bitmap scaledBitmap;

public ScaleBigView(Context context) {
this(context, null);
}

public ScaleBigView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ScaleBigView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mDM = getResources().getDisplayMetrics();
initPaint();
setClickable(true); //触发hotspot
}

private void initPaint() {
//否则提供给外部纹理绘制
mCommonPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
mCommonPaint.setAntiAlias(true);
mCommonPaint.setStyle(Paint.Style.FILL_AND_STROKE);
mCommonPaint.setStrokeCap(Paint.Cap.ROUND);
mCommonPaint.setFilterBitmap(true);
mCommonPaint.setDither(true);
mCommonPaint.setStrokeWidth(dp2px(20));
mBitmap = decodeBitmap(R.mipmap.mm_012);

}

private Bitmap decodeBitmap(int resId) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inMutable = true;
return BitmapFactory.decodeResource(getResources(), resId, options);
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
if (widthMode != MeasureSpec.EXACTLY) {
widthSize = mDM.widthPixels / 2;
}
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);

if (heightMode != MeasureSpec.EXACTLY) {
heightSize = widthSize / 2;
}
setMeasuredDimension(widthSize, heightSize);

}
private float x;
private float y;
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
if (width < 1 || height < 1) {
return;
}
if (shader == null) {
float ratio = 1.2f;
scaledBitmap = Bitmap.createScaledBitmap(mBitmap, (int) (mBitmap.getWidth() * ratio), (int) (mBitmap.getHeight() * ratio), true);
shader = new BitmapShader(scaledBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);

}
//放大平移
float offsetX = -(scaledBitmap.getWidth() - mBitmap.getWidth()) / 2f;
float offsetY = -(scaledBitmap.getHeight() - mBitmap.getHeight())/2f;

//窥视镜圆心
float mirrorCenterX = mBitmap.getWidth() - width / 4f;
float mirrorCenterY = mBitmap.getHeight() - width/4f;

//(mirrorCenterX - x) ,(mirrorCenterY-y) 是把当前中心点的图像平移到圆心哪里
matrix.setTranslate( offsetX + (mirrorCenterX - x) , offsetY + (mirrorCenterY-y));
shader.setLocalMatrix(matrix);

int save = canvas.save();
canvas.clipRect(0,0,mBitmap.getWidth(),mBitmap.getHeight());
canvas.drawBitmap(mBitmap, 0, 0, null);
mCommonPaint.setColor(Color.DKGRAY);
canvas.drawCircle(mirrorCenterX , mirrorCenterY,width/4f,mCommonPaint);
mCommonPaint.setShader(shader);
canvas.drawCircle( mirrorCenterX , mirrorCenterY,width/4f,mCommonPaint);
mCommonPaint.setShader(null);

canvas.restoreToCount(save);
}

@Override
public void dispatchDrawableHotspotChanged(float x, float y) {
super.dispatchDrawableHotspotChanged(x, y);
this.x = x;
this.y = y;
postInvalidate();
}

@Override
protected void dispatchSetPressed(boolean pressed) {
super.dispatchSetPressed(pressed);
postInvalidate();
}

public float dp2px(float dp) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, mDM);
}

}

作者:时光少年
来源:juejin.cn/post/7310124656996302874
收起阅读 »

Android 手电筒照亮效果

前言 经常在掘金博客上看到使用前端技术实现的手电筒照亮效果,但是搜了一下android相关的,发现少得很,实际上这种效果在Android上实现会更简单,当然,条条大路通罗马,有很多技术手段去实现这种效果,今天我们选择一种相对比较好的方法来实现。 实现方法梳理 ...
继续阅读 »

前言


经常在掘金博客上看到使用前端技术实现的手电筒照亮效果,但是搜了一下android相关的,发现少得很,实际上这种效果在Android上实现会更简单,当然,条条大路通罗马,有很多技术手段去实现这种效果,今天我们选择一种相对比较好的方法来实现。


实现方法梳理



  • 第一种方法就是利用Path路径进行 Clip Outline,然后绘制不同的渐变效果即可,这种方法其实很适合蒙版切图,不过也能用于实现这种特效。

  • 第二种方法是利用Xfermode 进行中间图层镂空。

  • 第三种方法就是Shader,效率高且无锯齿。


效果


fire_61.gif


实现原理


其实本篇的核心就是Shader了,这次我们也用RadialGradient来实现,本篇几乎没有任何难度,关键技术难点就是Shader 的移动,其实最经典的效果是Facebook实现的光影文案,本质上时Matrix + Shader.setLocalMatrix 实现。


155007_4C1U_2256215.gif


Matrix涉及一些数学问题,Matrix初始化本身就是单位矩阵,几乎每个操作都是乘以另一个矩阵,属于线性代数的基本知识,难度其实并不高。


matrix.setTranslation(1,2) 可以看作,矩阵的乘法无非是行乘列,繁琐事繁琐,但是很容易理解



1,0,0, 1,0,1,
0,1,0, X 0,1,2,
0,0,1 0,0,1


我们来看看经典的facebook 出品代码


public class GradientShaderTextView extends TextView {

private LinearGradient mLinearGradient;
private Matrix mGradientMatrix;
private Paint mPaint;
private int mViewWidth = 0;
private int mTranslate = 0;

private boolean mAnimating = true;
private int delta = 15;
public GradientShaderTextView(Context ctx)
{
this(ctx,null);
}

public GradientShaderTextView(Context context, AttributeSet attrs) {
super(context, attrs);
}

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (mViewWidth == 0) {
mViewWidth = getMeasuredWidth();
if (mViewWidth > 0) {
mPaint = getPaint();
String text = getText().toString();
// float textWidth = mPaint.measureText(text);
int size;
if(text.length()>0)
{
size = mViewWidth*2/text.length();
}else{
size = mViewWidth;
}
mLinearGradient = new LinearGradient(-size, 0, 0, 0,
new int[] { 0x33ffffff, 0xffffffff, 0x33ffffff },
new float[] { 0, 0.5f, 1 }, Shader.TileMode.CLAMP); //边缘融合
mPaint.setShader(mLinearGradient);
mGradientMatrix = new Matrix();
}
}
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);

int length = Math.max(length(), 1);
if (mAnimating && mGradientMatrix != null) {
float mTextWidth = getPaint().measureText(getText().toString());
mTranslate += delta;
if (mTranslate > mTextWidth+1 || mTranslate<1) {
delta = -delta;
}
mGradientMatrix.setTranslate(mTranslate, 0); //自动平移矩阵
mLinearGradient.setLocalMatrix(mGradientMatrix);
postInvalidateDelayed(30);
}
}

}

本文案例


本文要实现的效果其实也是一样的方法,只不过不是自动移动,而是添加了触摸事件,同时加了放大缩小效果。


坑点


Shader 不支持矩阵Scale,本身打算利用Scale缩放光圈,但事与愿违,不仅不支持,连动都动不了了,因此,本文采用了两种Shader,按压时使用较大半径的Shader,手放开时使用默认的Shader。


知识点


canvas.drawPaint(mCommonPaint);

这个绘制并不是告诉你可以这么绘制,而是想说,设置了Shader之后,这样调用,Shader半径之外的颜色时Shader最后一个颜色值,我们最后一个颜色值时黑色,那就是黑色,我们改成白色当然也是白色,下图是改成白色之后的效果,周围都是白色


企业微信20231207-230353@2x.png


关键代码段


super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
if (width < 1 || height < 1) {
return;
}
//大光圈shader
if (radialGradientLarge == null) {
radialGradientLarge = new RadialGradient(0, 0,
dp2px(100),
new int[]{Color.TRANSPARENT, 0x01ffffff, 0x33ffffff, 0x66000000, 0x77000000, 0xff000000},
new float[]{0.1f, 0.3f, 0.5f, 0.7f, 0.9f, 0.95f},
Shader.TileMode.CLAMP);
}
//默认光圈shader
if (radialGradientNormal == null) {
radialGradientNormal = new RadialGradient(0, 0,
dp2px(50),
new int[]{Color.TRANSPARENT, 0x01ffffff, 0x33ffffff, 0x66000000, 0x77000000, 0xff000000},
new float[]{0.1f, 0.3f, 0.5f, 0.7f, 0.9f, 0.95f},
Shader.TileMode.CLAMP);
}

//绘制地图
canvas.drawBitmap(mBitmap, 0, 0, null);

//移动shader中心点
matrix.setTranslate(x, y);
//设置到矩阵
radialGradientLarge.setLocalMatrix(matrix);
radialGradientNormal.setLocalMatrix(matrix);
if(isPressed()) {
//按压时
mCommonPaint.setShader(radialGradientLarge);
}else{
//松开时
mCommonPaint.setShader(radialGradientNormal);
}
//直接用画笔绘制,那么周围的颜色是Shader 最后的颜色
canvas.drawPaint(mCommonPaint);

好了,我们的效果基本实现了。


总结


本篇到这里就截止了,我们今天掌握的知识点是Shader相关的:



  • Shader 矩阵不能Scale

  • 设置完Shader 的画笔外围填充色为Ridial Shader最后的颜色

  • Canvas 可以直接drawPaint

  • Shader.setLocalMatrix是移动Shader中心点的方法


代码


按照惯例,给出全部代码


public class LightsView extends View {
private final DisplayMetrics mDM;
private TextPaint mCommonPaint;
private Bitmap mBitmap;
private RadialGradient radialGradientLarge = null;
private RadialGradient radialGradientNormal = null;
private float x;
private float y;
private boolean isPress = false;
private Matrix matrix = new Matrix();
public LightsView(Context context) {
this(context, null);
}

public LightsView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public LightsView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mDM = getResources().getDisplayMetrics();
initPaint();
setClickable(true); //触发hotspot
}

private void initPaint() {
//否则提供给外部纹理绘制
mCommonPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
mCommonPaint.setAntiAlias(true);
mCommonPaint.setStyle(Paint.Style.FILL_AND_STROKE);
mCommonPaint.setStrokeCap(Paint.Cap.ROUND);
mCommonPaint.setFilterBitmap(true);
mCommonPaint.setDither(true);
mBitmap = decodeBitmap(R.mipmap.mm_06);

}

private Bitmap decodeBitmap(int resId) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inMutable = true;
return BitmapFactory.decodeResource(getResources(), resId, options);
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
if (widthMode != MeasureSpec.EXACTLY) {
widthSize = mDM.widthPixels / 2;
}
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);

if (heightMode != MeasureSpec.EXACTLY) {
heightSize = widthSize / 2;
}
setMeasuredDimension(widthSize, heightSize);

}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
if (width < 1 || height < 1) {
return;
}
if (radialGradientLarge == null) {
radialGradientLarge = new RadialGradient(0, 0,
dp2px(100),
new int[]{Color.TRANSPARENT, 0x01ffffff, 0x33ffffff, 0x66000000, 0x77000000, 0xff000000},
new float[]{0.1f, 0.3f, 0.5f, 0.7f, 0.9f, 0.95f},
Shader.TileMode.CLAMP);
}
if (radialGradientNormal == null) {
radialGradientNormal = new RadialGradient(0, 0,
dp2px(50),
new int[]{Color.TRANSPARENT, 0x01ffffff, 0x33ffffff, 0x66000000, 0x77000000, 0xff000000},
new float[]{0.1f, 0.3f, 0.5f, 0.7f, 0.9f, 0.95f},
Shader.TileMode.CLAMP);
}

canvas.drawBitmap(mBitmap, 0, 0, null);

matrix.setTranslate(x, y);
radialGradientLarge.setLocalMatrix(matrix);
radialGradientNormal.setLocalMatrix(matrix);
if(isPressed()) {
mCommonPaint.setShader(radialGradientLarge);
}else{
mCommonPaint.setShader(radialGradientNormal);
}
canvas.drawPaint(mCommonPaint);
}

@Override
public void dispatchDrawableHotspotChanged(float x, float y) {
super.dispatchDrawableHotspotChanged(x, y);
this.x = x;
this.y = y;
postInvalidate();
}

@Override
protected void dispatchSetPressed(boolean pressed) {
super.dispatchSetPressed(pressed);
postInvalidate();
}

public float dp2px(float dp) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, mDM);
}

}

作者:时光少年
来源:juejin.cn/post/7309687967064817716
收起阅读 »

6G,它来了,真的666!

你好,这里是网络技术联盟站。2023年12月5日至6日,全球6G发展大会在重庆两江新区成功举行。会议汇集了中国两院院士和国内外权威专家,共同探讨了6G的发展愿景和技术产业路径。中国工程院院士张平提出,移动通信系统正处于一个重要的发展阶段,需要从容量、速率、服务...
继续阅读 »

你好,这里是网络技术联盟站。

2023年12月5日至6日,全球6G发展大会在重庆两江新区成功举行。会议汇集了中国两院院士和国内外权威专家,共同探讨了6G的发展愿景和技术产业路径。中国工程院院士张平提出,移动通信系统正处于一个重要的发展阶段,需要从容量、速率、服务和智能等多个维度上进行扩展。他强调,6G的发展应该着重于维度上和智能性上的提升。

张平院士指出,当前通信技术发展面临的挑战包括信息压缩的极限和数据吞吐量的过大,这些因素使得提升通信系统容量性能难以持续。因此,6G时代需要寻找可持续发展的路径,其中人工智能的融合被视为关键因素,有望实现通信技术和人工智能技术的共赢共生。

华为技术有限公司无线CTO童文在主题演讲中强调了人工智能在未来通信技术中的核心作用,预测在未来五年到十年内,大多数研发设计和文字工作将由AI取代。他认为,AI Agent将成为承载6G服务和应用的核心载体。

中国通信标准化协会理事长闻库则提出,6G的愿景不仅仅是提高网速,而是在此基础上实现手机性能比的提升、覆盖的提升和垂直行业的全面拓展。他强调,AI的引入为6G的发展提供了新的可能性,未来的通信将更加注重智能化的创新。

会议还讨论了6G距离落地的距离和后续关键行动,包括6G技术的梳理和验证、6G与5G的衔接、加强5G基础建设、推进5G-A迈向商用,以及加大技术创新力度和强化开放合作。

这次大会的讨论和发言反映了6G技术发展的最新动态和未来方向,展现了中国在6G技术研发方面的积极态度和领先地位。

对于我们从事网络,或者说从事IT行业的人来说,学习6G技术是迫在眉睫!本文瑞哥就带大家好好了解一下6G技术,相信看完本文,一定对您有所帮助!

让我们直接开始!

无线技术演进

无线通信技术的演进是一个持续的过程,每一代技术都在速度、容量、延迟和连接性方面带来了显著的改进。

1G无线网络

  • 时间:20世纪80年代
  • 特点:主要侧重于语音通信,网络速度缓慢。

2G无线网络

  • 时间:1990/91年
  • 特点:带来了一些无线数据服务,如WAP、MMS和SMS,但速度仍然很慢。2G的迭代版本2.5G和2.75G提高了数据速率。

3G无线网络

  • 时间:2004/2005年
  • 特点:增加了视频通话、移动电视、基于位置的服务,以及更快的数据传输速度(8-20Mbps)。随着需求的增加,出现了3.5G和3.75G,带来了移动电子邮件和个人对个人的游戏。

4G无线网络

  • 时间:2009年
  • 特点:能够更快地下载和上传文件,同时处理语音和数据呼叫。4G手机在信号充足的情况下可以快速下载文件。

5G无线网络

  • 时间:2019年
  • 特点:承诺更快的速度、更少的延迟和更多的连接,支持更多设备同时连接。5G网络使许多新产品和物联网传感器能够以极快的移动数据速度在全球范围内连接。

6G无线网络

  • 预计时间:2030年左右
  • 特点:预计将提升5G网络的功能,并提供增强的覆盖范围、改进的功能和超快的移动数据速度。支持全球数十亿个物联网设备,并使人们能够享受虚拟现实(VR)、增强现实(AR)和混合现实(MR)等应用。

每一代无线技术的演进都是为了满足不断增长的数据需求和改善用户体验。从1G到6G,我们见证了无线通信技术的巨大变革,这些变革不仅影响了我们的通信方式,还推动了社会和经济的发展。

下面我们先着重介绍一下4G和5G技术的发展。

4G技术的发展

4G,作为第四代移动通信技术,标志着移动互联网时代的到来。它在速度、容量和连接性方面相比于3G技术有了显著的提升,为用户提供了更快的上网体验和更丰富的移动服务。

4G技术的核心是长期演进(LTE)标准,它提供了更高的数据传输速率和更低的网络延迟。LTE的引入使得移动宽带服务质量得到了显著提升,支持了更高清晰度的视频通话和更快速的数据下载。

MIMO技术通过使用多个天线同时发送和接收数据,显著提高了信号的质量和传输速度。这一技术的应用使得4G网络能够支持更多用户的同时连接,同时提高了网络的稳定性和覆盖范围。

MIMO:多输入多输出。

OFDM技术通过将信号分散到多个频道上,减少了干扰并提高了频谱效率。这一技术的使用使得4G网络能够更有效地利用有限的频谱资源,提供更高的数据传输速率。

OFDM:正交频分复用。

4G特点

  • 高速数据传输:4G网络的数据速率通常在100Mbps到1Gbps之间,使得视频通话和在线游戏等应用变得流畅。这一速度的提升为移动互联网的普及和移动应用的发展提供了强大的动力。
  • 改善的网络覆盖:4G技术通过更高效的频谱利用和网络优化,提供了更广泛的网络覆盖和更好的服务质量。这一改进使得用户即使在移动中也能享受到稳定的网络连接。
  • 支持多种应用:4G网络支持了社交媒体、流媒体服务、云计算等多种新兴应用。这些应用的发展推动了移动互联网的创新和多样化,为用户提供了更丰富的选择和更便捷的服务。

5G技术的突破

5G,即第五代移动通信技术,是继4G之后的又一次重大技术革新。它不仅提供了更快的速度和更低的延迟,还开启了物联网和智能设备的新时代。

5G技术的一个重要特点是使用毫米波频段,这些频段通常在30GHz到300GHz之间。毫米波通信提供了更高的数据速率和更大的带宽,使得5G网络能够支持超高清视频流、虚拟现实和增强现实等带宽密集型应用。

5G网络采用了大规模多输入多输出(MIMO)技术,通过使用更多的天线,显著提高了网络容量和效率。这一技术的应用使得5G网络能够同时服务更多的用户,同时提高了信号的质量和覆盖范围。

5G技术引入了网络切片的概念,允许运营商为不同的服务和应用提供定制化的网络资源。这意味着网络可以根据应用的需求动态分配资源,从而提高效率和性能。

5G特点

  • 极高速度:5G网络的峰值速率可达20Gbps,这是4G网络速率的数倍。这一速度的提升为各种新兴应用提供了强大的支持,包括自动驾驶汽车、远程医疗和智能城市等。
  • 超低延迟:5G网络的延迟低至1毫秒,这对于需要即时响应的应用至关重要。例如,远程手术和工业自动化都需要极低的延迟来确保操作的准确性和安全性。
  • 广泛的连接性:5G技术支持海量的设备连接,这对于物联网的发展至关重要。从智能家居到智能工厂,5G网络能够支持数以亿计的设备同时在线,实现高效的数据交换和控制。

下面进入我们的主角:6G。

从马可尼的无线电信号传输到5G的高速移动通信,每一代技术都在推动社会进步和经济发展。6G预计将是一个跨越性的技术,不仅仅是速度的提升,而是在智能化、感知能力和计算能力上的全面革新。

那么什么是6G呢?

6G

6G是指第六代移动通信技术,是5G的后继者。它被设计为一种更高级、更先进的无线通信技术,旨在提供比5G更快的速度、更低的延迟和更大的网络容量。

6G会是什么样子?

6G有望实现每秒1太比特的极高速度,相较于当前大多数家庭互联网网络可用的最快速度1 Gbps,以及5G的最高速度10 Gbps,有了显著的提升。

6G可能会利用太赫兹波或亚毫米波的频段,这能够提供更高的频谱,支持更大的带宽,进一步增强网络性能。这也可能解决5G中毫米波距离短、需要视线的问题。

6G将更加依赖人工智能,实现协同工作,特别是在自动驾驶汽车、工厂自动化等方面。边缘计算的应用将使网络更本地化,减少响应时间,提高协同效率。

6G有望支持更高级别的沉浸式技术,包括虚拟现实、细胞表面、植入物和无线脑机接口。这将为用户提供更身临其境的体验,推动智能可穿戴设备和植入物的发展。

6G可能会实现物理生活与网络空间的完全融合,通过可穿戴设备和植入在人体上的微型设备,实时支持人类的思想和行动。

6G工作原理(可能)

6G的一个关键特征可能是利用超高频率传输数据。这包括在数百千兆赫(GHz)或甚至太赫兹(THz)范围内进行通信。这将提供更大的带宽和更高的数据传输速度。

6G可能会采用先进的技术,以提高频谱的利用效率。通过使用复杂的数学方法,6G网络可以在同一频率上实现同时发送和接收,从而提高频谱的传输效率。

6G可能会采用网状网络架构,将设备连接到彼此,形成一个分布式的网络。这样的架构可以提供更好的覆盖范围和更高的可靠性,同时支持设备之间的直接通信。

6G可能会引入新的互联网协议(New IP),以提高网络的效率和性能。这可能包括一种新型的IP数据包,具有更多导航和优先级信息,以支持更智能、自适应的网络通信。

6G可能会根据材料的原子和分子对特定波长的吸收和发射频率进行选择性的波长利用。这样的技术可以优化信号传输,并考虑到不同材料对电磁辐射的特定响应。

6G频段的使用

预计6G网络的最新先锋频谱将主要位于中频段,即7 GHz到20 GHz之间。这个频段的使用将通过极端的多输入多输出(MIMO)技术提供更大的容量。中频段的特点是提供相对较高的数据传输速率,并在城市室外小区中发挥重要作用。

对于广泛的覆盖范围,6G将继续利用低频段,预计在460 MHz到694 MHz之间。低频段通常用于提供更广泛的覆盖范围和更好的穿透能力,尤其是在城市和 室内环境中。

6G计划利用次太赫兹频谱,这是一个非常高的频段。这将实现超过100 Gbps的峰值数据速度,为未来对高速数据传输需求极高的应用提供支持。

6G将广泛使用新的光谱范围,包括高达太赫兹的频段。这将推动本地化技术到新的水平,提高定位的精确度,并为各种应用场景带来新的可能性。

6G将通过使用广泛的频谱范围,特别是高频段,显着提高定位精度,达到厘米级的水平。这将对各种应用,包括导航、位置服务和物联网设备的定位,产生积极影响。

6G特点

  1. 超高数据速率: 6G的一个主要目标是实现前所未有的数据速率。这意味着网络将能够提供比当前标准更高的下载和上传速度,以支持高清内容的无缝传输,同时满足未来对更大带宽需求的应用,如虚拟现实(VR)和增强现实(AR)。

5G的峰值数据吞吐量通常被设计为在20 Gbps左右,而6G将迈向更令人惊叹的1 Tbps(太比特每秒)的峰值数据速率。这是对比5G速度的显著提升,为未来的高度数据密集型应用提供了更大的带宽。

5G旨在提供用户体验数据速率为100 Mbps,而6G将将这一速率提高到1 Gbps(千兆比特每秒)。这将使用户能够更快地下载和上传数据,以支持更高质量的多媒体服务和应用。

由于更高的频谱效率,6G将比5G提高近一倍以上的速度。这种效率的提升对于满足未来对大容量、高速度连接的需求至关重要,尤其是在处理大规模视频、虚拟现实、增强现实等数据密集型应用时。

  1. 超低延迟: 6G致力于实现超低延迟,即数据从一个网络点传输到另一个点所需的时间。这对于需要即时响应的实时应用非常关键,例如自动驾驶汽车、远程手术和工业自动化。预计延迟将减少到毫秒甚至微秒的水平。

5G的设计目标是将时延降至1毫秒。这对于许多实时应用程序,如增强现实、虚拟现实和自动驾驶汽车等来说,已经是一个显著的提升。然而,6G将进一步将用户体验到的延迟降低到0.1毫秒以下,实现更加极致的超低延迟。

由于延迟的大幅减少,许多实时应用程序将获得更好的性能和功能。这对于需要即时响应的应用场景,如在线游戏、实时视频通话和远程协作等,将带来明显的改进。

超低延迟将使网络能够实现更迅速的紧急响应。这对于紧急情况下的通信和救援操作非常重要,例如在自然灾害发生时,网络可以更快速、更有效地协调救援活动。

  1. 海量连接: 6G旨在支持物联网(IoT)中预计将连接数十亿台设备的海量连接。为实现这一目标,网络需要进一步发展,以应对大规模设备之间的通信、数据传输和管理。

6G将更加专注于支持机器对机器的连接,强调物联网(IoT)和各种设备之间的通信。这对于未来智能城市、智能工厂、智能交通系统等应用来说至关重要,其中大量设备需要相互协调和通信。

  1. 能源效率: 可持续性是当前和未来网络发展的一个重要考虑因素。6G的设计将更加注重能源效率,通过优化网络基础设施和采用智能电源管理技术,以减少能源消耗,同时保持高性能连接。
  2. 人工智能集成: 6G将与人工智能(AI)密切结合,通过利用AI算法和机器学习技术来实现智能网络管理、资源分配和优化。这种集成有望提高网络的整体性能和效率,使其更具自适应性和智能性。

6G的优势

  1. 更快、更可靠的数据速度: 6G将实现更高的数据传输速度,为企业和消费者提供更快速、更可靠的互联网连接。这将促使新的应用和服务,如实时的3D全息视频流、超高清虚拟现实等。
  2. 更低的延迟: 6G将具有更低的延迟,即数据传输的时间将进一步缩短。这对于需要实时通信的应用非常关键,例如远程手术、虚拟会议和自动驾驶汽车等。
  3. 更广泛的设备和应用: 6G将支持更广泛范围的设备和应用,包括物联网中的大量连接设备、智能城市中的各种传感器和控制系统,以及新兴的技术领域,如增强现实和虚拟现实。
  4. 提高安全性和性能: 6G将利用人工智能和机器学习来提高网络的安全性和性能。这将增强网络的自我学习和自适应性,使其更具抵御网络攻击的能力,同时确保网络能够处理未来6G预计增加的大规模数据流量。
  5. 推动新兴技术和应用: 6G的引入将推动各种新兴技术和应用的发展,包括智能交通、医疗创新、工业自动化和元宇宙等。这将为社会带来更多创新和便利。

6G的潜在应用

  1. 实时全息视频会议: 6G的超高速度和低延迟将使实时全息视频会议成为可能。用户可以感觉到与对方面对面交流,这对于企业协作、在线教育和虚拟团队合作具有重要意义。

  1. 超高清虚拟现实(VR): 6G的高带宽和低延迟将推动超高清虚拟现实体验的发展。这将改善虚拟旅游、虚拟培训和虚拟游戏等领域的用户体验。
  2. 自动驾驶汽车: 6G的超低延迟对于自动驾驶汽车至关重要。实时通信将使汽车能够相互协作,共享实时交通和道路信息,提高自动驾驶汽车的安全性和效率。
  3. 远程手术: 6G的低延迟和高带宽将为远程手术提供支持。外科医生可以远程操控手术机器人进行手术,为无法到达医院的患者提供及时的医疗服务。
  4. 工业物联网(IIoT): 6G的大容量和广泛连接性将促进工业物联网的发展。在工业领域,各种传感器和设备可以实时通信,实现智能制造和工业自动化。
  5. 智能城市: 6G将为智能城市提供支持,实现各种城市基础设施的智能化管理,包括交通系统、能源管理、环境监测等。
  6. 医疗创新: 6G有望推动医疗领域的创新,包括远程医疗服务、医疗数据实时传输和医疗设备的互联互通,提高医疗保健的效率和可及性。

6G发展面临哪些挑战?

  1. 新频谱的需求: 为了实现更高的数据速率和容量,6G需要利用新的频谱范围。然而,目前可用的射频频段有限,而且这些频段通常由政府监管机构进行分配。因此,确保有足够的频谱来支持6G是一个重要的挑战。
  2. 新技术的发展: 6G的实现将依赖于一系列新技术的发展,包括太赫兹通信、新的无线电接入技术以及人工智能和机器学习在网络管理中的应用。这些技术目前仍在研发阶段,需要时间来完善和商业化。
  3. 提高安全性: 随着网络的发展,安全性变得尤为关键。6G需要更高水平的安全性,以应对日益复杂和普及的网络攻击。确保用户数据的隐私和网络的稳定性是一个必须解决的挑战。
  4. 部署成本: 6G的部署成本预计将比5G更高。引入新的频段和技术,以及更新现有的基础设施,都需要巨大的资金投入。这可能涉及到国家和企业层面的资金支持,以确保6G网络的建设和推广。
  5. 国际合作和标准制定: 6G的发展需要国际合作,以确保全球范围内的一致性和互操作性。同时,制定一系列统一的国际标准也是一个关键挑战,以便不同厂商的设备和网络可以无缝地协同工作。

5G与6G频谱比较

  1. 最大频率:
  • 5G:100 GHz
  • 6G:10太赫兹
  1. 最大带宽:
  • 5G:1 GHz
  • 6G:100 GHz
  1. 峰值数据速率:
  • 5G:10 Gbps(上传链路)至20 Gbps(下载链路)
  • 6G:100 Gbps至1 Tbps
  1. 平均用户体验数据速率:
  • 5G:100 Mbps
  • 6G:1 Gbps
  1. 峰值频谱效率:
  • 5G:30 b/s/Hz
  • 6G:60 b/s/Hz
  1. 用户体验的平均频谱效率:
  • 5G:0.03 b/s/Hz
  • 6G:3 b/s/Hz
  1. 移动支持:
  • 5G:最高500公里/小时
  • 6G:最高1000公里/小时
  1. 密度:
  • 5G:每平方米1台设备
  • 6G:每平方米100个设备
  1. 端到端延迟:
  • 5G:1至10毫秒
  • 6G:少于1毫秒
  1. 单频全双工传输:
  • 5G:没有
  • 6G:有
  1. 全球覆盖:
  • 5G:70多个国家已经推出5G,其中中国和美国在城市中处于领先地位
  • 6G:中国申请的6G专利最多,其次是美国

6G的商业化时间表

  • 标准制定:业内预计6G标准和规范的制定将从2025年开始¹。
  • 部署时间:预计6G系统将在2028年左右开始部署。
  • 商业化:预计6G的商业部署将在2030年左右实现³。

这些时间表是基于当前的技术发展和预测,可能会随着研究进展和行业动态而有所调整。6G技术的商业化还需要克服许多技术和政策上的挑战,包括频谱分配、网络架构设计、安全性和隐私保护等方面。

哪些公司正在领导6G技术研发?

全球有多家公司正在积极参与6G技术的研发,比如华为 (Huawei)、中兴通讯(ZTE)、中国移动、中国电信、中国联通、三星 (Samsung)、LG、NTT DOCOMO、高通 (Qualcomm)、AT&T、诺基亚 (Nokia)、爱立信 (Ericsson)等等。

中国针对6G做出的行动

中国政府在“第14次五年计划(2021-2025年)及2035年远景目标纲要”中明确提出了发展6G技术的目标²。

中国还成立了IMT-2030(6G)推进组,由主要通信运营商、基础设施供应商、IT公司和研究机构等约80家企业组成,致力于6G技术的研发和标准化工作。6G推进组已经发布了《6G网络架构展望》和《6G无线系统设计原则和典型特征》等技术方案,这些方案旨在为6G技术从万物互联向万物智联的转变提供技术路径。

中国工业和信息化部已宣布正在有序开展6G相关的技术试验,以推动6G创新发展。加快5G与XR、数字孪生、机器人等新产业新应用的融合发展,加速相关产业成熟,夯实6G应用基础。此外,推动信息通信企业与垂直行业企业密切沟通、协同合作,共同参与6G需求研究、技术研发、标准制定等全流程各环节,携手构建6G繁荣应用生态。

中国计划在2024年前完成6G相关主要技术的明确和概念机的测试验证,以提升技术能力。

预计到2026年,中国将开展典型应用场景和性能指标的确立,进行试制机的研发和基站功能性能的验证。

中国计划在2030年左右实现6G的商用化,而标准化制定的时间预计将在2025年。6G技术将引入新的应用场景,如通信与感知的结合、通信与人工智能的结合,以及泛在物联网等。这些技术不仅将连接人类,还将连接智能体,如机器人和元宇宙,进一步完善5G在行业中尚未解决的场景。

中国正在加强国际合作,与欧洲6G智慧网络和业务产业协会(6G-IA)、韩国6G论坛、印度通信标准开发协会(TSDSI)等签署合作备忘录,共同推进6G技术的发展。

此外,中国的通信巨头如华为和中国移动也在积极参与6G技术的研究和开发工作。华为是首家宣布开始研究6G的中国公司,随后与其他国内外企业和研究机构展开了多项合作。

据市场研究机构Market Research Future预计,到2040年,全球6G市场规模将超过3400亿美元,年复合增长率达58.1%。中国预计将成为全球最大的6G市场之一,全球近50%的6G专利申请来自中国。

总结

6G技术与5G相比,在速度上有显著的提升。根据研究和预测,6G的理论最高速度可达到1Tbps(即1000Gbps),这比5G的理论最高速度20Gbps快了50倍。此外,有报道称在中国的实验室环境中已经实现了206.25Gbps的速度。

6G将使用比5G更高的频率波段,操作在30GHz到300GHz的毫米波段,甚至可能达到300GHz到3000GHz的辐射波段。这些更高的频率波段将允许更快的数据传输速度和更大的带宽容量。

中国在6G技术的研发和创新方面正加速推进,预计在2030年左右实现商用。中国工业和信息化部已经指导成立了6G推进组,旨在为6G创新发展提供政策支持,并推动形成全球统一的6G标准。

6G技术不仅仅是速度的提升,它还将服务于社会管理和治理,以及智能体的应用。6G网络预计将是一个地面无线与卫星通信集成的全连接世界,不仅比5G更快、更可靠,还将推动移动通信与人工智能、感知、计算等技术的跨领域融合发展。

中国已经开始进行6G技术试验,并陆续开展了关于6G系统架构和技术方案的研究。最近,中国6G推进组发布了相关技术方案,为6G从万物互联走向万物智联提供了技术路径。

6G时代的基站将不仅支持通信信号的发送和接收,还将支持通信和感知,利用无线电波感知周边环境、物体形状和运动等,这不仅能提升通信性能,还将催生新业务。例如,基站可以进行升级改造,以支持低空经济和空域管理,或者用于交通管理。

6G将促进沉浸感更强的全息视频,实现物理世界、虚拟世界、人的世界三个世界的联动。今年6月,国际电信联盟完成了6G愿景需求建议书,明确了6G典型产品和关键能力指标,其中中国提出的5类6G典型场景和14个关键能力指标全部被采纳。

这些进展表明,中国在6G技术的发展上正处于全球领先地位,积极推进技术研发和创新,为未来的通信技术和应用开辟新的可能性。

朋友们,让我们一起期待中国在6G领域继续“雄霸全球”吧!


作者:wljslmz
来源:juejin.cn/post/7310143510102540297
收起阅读 »

Linus:批评 GitHub 代码合并【毫无用处的】

Linux 和 Git 的创建者 Linus Torvalds 批评 GitHub 创造了“毫无用处的代码合并”。 Torvalds 的评论可以在 Linux 开发邮件列表的存档中查看,该评论针对的是 Paragon Software 的创始人兼首席执行官 K...
继续阅读 »


Linux 和 Git 的创建者 Linus Torvalds 批评 GitHub 创造了“毫无用处的代码合并”。


Torvalds 的评论可以在 Linux 开发邮件列表的存档中查看,该评论针对的是 Paragon Software 的创始人兼首席执行官 Konstantin Komarov,关于为即将到来的 5.15 内核提交其读写 NTFS 驱动程序。


Torvalds 说,GitHub 创建了绝对无用的垃圾合并,你永远不应该使用 GitHub 接口来合并任何东西。


早在 2012 年,Torvalds 就对他为什么不使用 GitHub 进行拉取请求给出了更详细的解释:



GitHub 会丢弃所有相关信息,比如应该为要求我拉取的人提供一个有效的电子邮件地址。diffstat 也是有缺陷和无用的。




Git 附带了一个不错的拉取请求生成模块,但 github 决定用他们自己的完全劣质的版本替换它。因此,我认为 github 对此太无能了。托管很好,但拉取请求和在线提交编辑只是纯粹的垃圾。



Paragon Software 提交的驱动程序提高了与本机 Windows 文件系统 NTFS 的互操作性。提交过程在一年多前就开始了,但面临投诉,称其 27,000 行代码太大而无法审查。


提交了较小的块,但很明显,Paragon 一直在努力掌握 Linux 内核开发过程。最终 Torvalds 介入并在此过程中提供指导。


7 月,Torvalds 指出,与其将代码发布到 fsdevel 列表中,不如最终将其作为实际的拉取请求提交。


当时,Paragon 回应说:“也感谢您的澄清。直到现在,我们才真正清楚这个信息。我们刚刚发送了第 27 个补丁系列,它修复了针对当前 linux-next 的可构建性。在将拉取请求发送给您之前,我们需要几天时间来准备适当的拉取请求“。


这似乎比预期的要长一些,但 Paragon 于 2021 年 9 月 3 日星期五提交了拉取请求。该公司表示,“当前版本适用于普通/压缩/稀疏文件,并支持 acl、NTFS 日志重播。


除了建议不要使用 GitHub 的接口进行合并之外,Torvalds 还表示——虽然这次他会让它通过——拉取请求应该已经签署。


Torvalds 认为在一个完美的世界里,这将是一个 PGP 签名,可以通过信任链直接追溯到你。


最后拉取请求被合并,Torvalds 也作了最终评论。


Torvalds 认为最初的拉取往往有一些奇怪的地方,他现在会接受它们,为了继续发展,他需要正确地做事。


作者:ENG八戒
来源:juejin.cn/post/7312293783973675008
收起阅读 »

7个Js async/await高级用法

web
7个Js async/await高级用法 JavaScript的异步编程已经从回调(Callback)演进到Promise,再到如今广泛使用的async/await语法。后者不仅让异步代码更加简洁,而且更贴近同步代码的逻辑与结构,大大增强了代码的可读性与可维护...
继续阅读 »

7个Js async/await高级用法


JavaScript的异步编程已经从回调(Callback)演进到Promise,再到如今广泛使用的async/await语法。后者不仅让异步代码更加简洁,而且更贴近同步代码的逻辑与结构,大大增强了代码的可读性与可维护性。在掌握了基础用法之后,下面将介绍一些高级用法,以便充分利用async/await实现更复杂的异步流程控制。


1. async/await与高阶函数


当需要对数组中的元素执行异步操作时,可结合async/await与数组的高阶函数(如mapfilter等)。


// 异步过滤函数
async function asyncFilter(array, predicate) {
const results = await Promise.all(array.map(predicate));

return array.filter((_value, index) => results[index]);
}

// 示例
async function isOddNumber(n) {
await delay(100); // 模拟异步操作
return n % 2 !== 0;
}

async function filterOddNumbers(numbers) {
return asyncFilter(numbers, isOddNumber);
}

filterOddNumbers([1, 2, 3, 4, 5]).then(console.log); // 输出: [1, 3, 5]

2. 控制并发数


在处理诸如文件上传等场景时,可能需要限制同时进行的异步操作数量以避免系统资源耗尽。


async function asyncPool(poolLimit, array, iteratorFn) {
const result = [];
const executing = [];

for (const item of array) {
const p = Promise.resolve().then(() => iteratorFn(item, array));
result.push(p);

if (poolLimit <= array.length) {
const e = p.then(() => executing.splice(executing.indexOf(e), 1));
executing.push(e);
if (executing.length >= poolLimit) {
await Promise.race(executing);
}
}
}

return Promise.all(result);
}

// 示例
async function uploadFile(file) {
// 文件上传逻辑
}

async function limitedFileUpload(files) {
return asyncPool(3, files, uploadFile);
}

3. 使用async/await优化递归


递归函数是编程中的一种常用技术,async/await可以很容易地使递归函数进行异步操作。


// 异步递归函数
async function asyncRecursiveSearch(nodes) {
for (const node of nodes) {
await asyncProcess(node);
if (node.children) {
await asyncRecursiveSearch(node.children);
}
}
}

// 示例
async function asyncProcess(node) {
// 对节点进行异步处理逻辑
}

4. 异步初始化类实例


在JavaScript中,类的构造器(constructor)不能是异步的。但可以通过工厂函数模式来实现类实例的异步初始化。


class Example {
constructor(data) {
this.data = data;
}

static async create() {
const data = await fetchData(); // 异步获取数据
return new Example(data);
}
}

// 使用方式
Example.create().then((exampleInstance) => {
// 使用异步初始化的类实例
});

5. 在async函数中使用await链式调用


使用await可以直观地按顺序执行链式调用中的异步操作。


class ApiClient {
constructor() {
this.value = null;
}

async firstMethod() {
this.value = await fetch('/first-url').then(r => r.json());
return this;
}

async secondMethod() {
this.value = await fetch('/second-url').then(r => r.json());
return this;
}
}

// 使用方式
const client = new ApiClient();
const result = await client.firstMethod().then(c => c.secondMethod());

6. 结合async/await和事件循环


使用async/await可以更好地控制事件循环,像处理DOM事件或定时器等场合。


// 异步定时器函数
async function asyncSetTimeout(fn, ms) {
await new Promise(resolve => setTimeout(resolve, ms));
fn();
}

// 示例
asyncSetTimeout(() => console.log('Timeout after 2 seconds'), 2000);

7. 使用async/await简化错误处理


错误处理是异步编程中的重要部分。通过async/await,可以将错误处理的逻辑更自然地集成到同步代码中。


async function asyncOperation() {
try {
const result = await mightFailOperation();
return result;
} catch (error) {
handleAsyncError(error);
}
}

async function mightFailOperation() {
// 有可能失败的异步操作
}

function handleAsyncError(error) {
// 错误处理逻辑
}

通过以上七个async/await的高级用法,开发者可以在JavaScript中以更加声明式和直观的方式处理复杂的异步逻辑,同时保持代码整洁和可维护性。在实践中不断应用和掌握这些用法,能够有效地提升编程效率和项目的质量。


作者:慕仲卿
来源:juejin.cn/post/7311603994928513076
收起阅读 »

推送数据?也许你不需要 WebSocket

web
提到推送数据,大家可能会首先想到 WebSocket。 确实,WebSocket 能双向通信,自然也能做服务器到浏览器的消息推送。 但如果只是单向推送消息的话,HTTP 就有这种功能,它就是 Server Send Event。 WebSocket 的通信过程...
继续阅读 »

提到推送数据,大家可能会首先想到 WebSocket。


确实,WebSocket 能双向通信,自然也能做服务器到浏览器的消息推送。


但如果只是单向推送消息的话,HTTP 就有这种功能,它就是 Server Send Event。


WebSocket 的通信过程是这样的:



首先通过 http 切换协议,服务端返回 101 的状态码后,就代表协议切换成功。


之后就是 WebSocket 格式数据的通信了,一方可以随时向另一方推送消息。


而 HTTP 的 Server Send Event 是这样的:



服务端返回的 Content-Type 是 text/event-stream,这是一个流,可以多次返回内容。


Sever Send Event 就是通过这种消息来随时推送数据。


可能你是第一次听说 SSE,但你肯定用过基于它的应用。


比如你用的 CICD 平台,它的日志是实时打印的。


那它是如何实时传输构建日志的呢?


明显需要一段一段的传输,这种一般就是用 SSE 来推送数据。


再比如说 ChatGPT,它回答一个问题不是一次性给你全部的,而是一部分一部分的加载回答。


这也是基于 SSE。




知道了什么是 SSE 以及它的应用,我们来自己实现一下吧:


创建 nest 项目:


npx nest new sse-test


把它跑起来:


npm run start:dev


访问 http://localhost:3000 可以看到 hello world,代表服务器跑成功了:



然后在 AppController 添加一个 stream 接口:



这里不是通过 @Get、@Post 等装饰器标识,而是通过 @Sse 标识这是一个 event stream 类型的接口。


@Sse('stream')
stream() {
return new Observable((observer) => {
observer.next({ data: { msg: 'aaa'} });

setTimeout(() => {
observer.next({ data: { msg: 'bbb'} });
}, 2000);

setTimeout(() => {
observer.next({ data: { msg: 'ccc'} });
}, 5000);
});
}

返回的是一个 Observable 对象,然后内部用 observer.next 返回消息。


可以返回任意的 json 数据。


我们先返回了一个 aaa、过了 2s 返回了 bbb,过了 5s 返回了 ccc。


然后写个前端页面:


创建一个 react 项目:


npx create-react-app --template=typescript sse-test-frontend


在 App.tsx 里写如下代码:


import { useEffect } from 'react';

function App() {

useEffect(() => {
const eventSource = new EventSource('http://localhost:3000/stream');
eventSource.onmessage = ({ data }) => {
console.log('New message', JSON.parse(data));
};
}, []);

return (
<div>hello</div>
);
}

export default App;

这个 EventSource 是浏览器原生 api,就是用来获取 sse 接口的响应的,它会把每次消息传入 onmessage 的回调函数。


我们在 nest 服务开启跨域支持:



然后把 react 项目 index.tsx 里这几行代码删掉,它会导致额外的渲染:



执行 npm run start


因为 3000 端口被占用了,它会跑在 3001:



浏览器访问下:



看到一段段的响应了没?


这就是 Server Send Event。


在 devtools 里可以看到,响应的 Content-Type 是 text/event-stream:



然后在 EventStream 里可以看到每一次收到的消息:



这样,服务端就可以随时向网页推送消息了。


那它兼容性怎么样呢?


可以在 MDN 看到:



除了 ie、edge 外,其他浏览器都没任何兼容问题。


基本是可以放心用的。


那用在哪呢?


一些只需要服务端推送的场景就特别适合 Server Send Event。


比如这个站内信:



这种推送用 WebSocket 就没必要了,可以用 SSE 来做。


那连接断了怎么办呢?


不用担心,浏览器会自动重连。


这点和 WebSocket 不同,WebSocket 如果断开之后是需要手动重连的,而 SSE 不用。


再比如说日志的实时推送。


我们来测试下:


tail -f 命令可以实时看到文件的最新内容:



我们通过 child_process 模块的 exec 来执行这个命令,然后监听它的 stdout 输出:


const { exec } = require("child_process");

const childProcess = exec('tail -f ./log');

childProcess.stdout.on('data', (msg) => {
console.log(msg);
});

用 node 执行它:



然后添加一个 sse 的接口:


@Sse('stream2')
stream2() {
const childProcess = exec('tail -f ./log');

return new Observable((observer) => {
childProcess.stdout.on('data', (msg) => {
observer.next({ data: { msg: msg.toString() }});
})
});

监听到新的数据之后,把它返回给浏览器。


浏览器连接这个新接口:



测试下:



可以看到,浏览器收到了实时的日志。


很多构建日志都是通过 SSE 的方式实时推送的。


日志之类的只是文本,那如果是二进制数据呢?


二进制数据在 node 里是通过 Buffer 存储的。


const { readFileSync } = require("fs");

const buffer = readFileSync('./package.json');

console.log(buffer);


而 Buffer 有个 toJSON 方法:



这样不就可以通过 sse 的接口返回了么?


试一下:


@Sse('stream3')
stream3() {
return new Observable((observer) => {
const json = readFileSync('./package.json').toJSON();
observer.next({ data: { msg: json }});
});
}



确实可以。


也就是说,基于 sse,除了可以推送文本外,还可以推送任意二进制数据。


总结


服务端实时推送数据,除了用 WebSocket 外,还可以用 HTTP 的 Server Send Event。


只要 http 返回 Content-Type 为 text/event-stream 的 header,就可以通过 stream 的方式多次返回消息了。


它传输的是 json 格式的内容,可以用来传输文本或者二进制内容。


我们通过 Nest 实现了 sse 的接口,用 @Sse 装饰器标识方法,然后返回 Observe 对象就可以了。内部可以通过 observer.next 随时返回数据。


前端使用 EventSource 的 onmessage 来接收消息。


这个 api 的兼容性很好,除了 ie 外可以放心的用。


它的应用场景有很多,比如站内信、构建日志实时展示、chatgpt 的消息返回等。


再遇到需要消息推送的场景,不要直接 WebSocket 了,也许 Server Send Event 更合适呢?


作者:zxg_神说要有光
来源:juejin.cn/post/7272564663116759074
收起阅读 »

只会Vue的我,用两天学会了react,这个方法您也可以

web
背景 由于工作需要学习react框架;最开始看文档的时候感觉还挺难的。但当我看了半天文档以后才发现,原来react这样学才是最快的;前提是同学们会vue一类的框架哈。 该方法适用于会vue的同学们食用 我们在学习以前先去想一想,在vue中我们常用的方法是什么,...
继续阅读 »

背景


由于工作需要学习react框架;最开始看文档的时候感觉还挺难的。但当我看了半天文档以后才发现,原来react这样学才是最快的;前提是同学们会vue一类的框架哈。


该方法适用于会vue的同学们食用


我们在学习以前先去想一想,在vue中我们常用的方法是什么,我们遇到一些场景时在vue中是怎么做的。


当我们想到这儿的时候就会发现,对啊;既然vue是这样做的,那么react中是怎么做的呢?别急,我们一步一步对比着来。


这样岂不是更能理解哦!下面就让我们开始吧!


冲冲冲。。。


Vue梳理


在开始之前,我们先来梳理一下我们在vue中常用的API或者场景有哪些。


以下这几种就是我们常见的一些功能,主要是列表渲染、表单输入和一些计算属性等等;我们只需要根据原有的需要的功能去学习即可。



  • 组件传值

  • 获取DOM

  • 列表渲染

  • 条件渲染

  • class

  • 计算属性

  • 监听器

  • 表单输入

  • 模板


vue/react对比学习


组件传值


vue


// 父组件
<GoodsList v-if="!isGoodsIdShow" :goodsList="goodsList"/>
// 子组件 -- 通过props获取即可
props: {
goodsList:{
type:Array,
default:function(){
return []
}
}
}

react


// 父组件
export default function tab(props:any) {
const [serverUrl, setServerUrl] = useState<string | undefined>('https://');
console.log(props);
// 父组件接收子组件的值并修改
const changeMsg = (msg?:string) => {
setServerUrl(msg);
};

return(
<View className='tab'>
<View className='box'>
<TabName msg={serverUrl} changeMsg={changeMsg} />
</View>
</View>

)
}

// 子组件
function TabName(props){
console.log('props',props);
// 子传父
const handleClick = (msg:string) => {
props.changeMsg(msg);
};
return (
<View>
<Text>{props.msg}</Text>
<Button onClick={()=>{handleClick('77777')}}>测试</Button>
</View>

);
};

获取DOM


vue


this.$refs['ref']

react


// 声明ref    
const domRef = useRef<HTMLInputElement>(null);
// 通过点击事件选择input框
const handleBtnClick = ()=> {
domRef.current?.focus();
console.log(domRef,'domRef')
}

return(
<View className='home'>
<View className='box'>
<Input ref={domRef} type="text" />
<button onClick={handleBtnClick}>增加</button>
</View>
</View>

)

列表渲染


vue


<div v-for="(item, index) in mealList" :key="index">
{{item}}
</div>

react


//声明对象类型
type Coordinates = {
name:string,
age:number
};
// 对象
let [userState, setUserState] = useState<Coordinates>({ name: 'John', age: 30 });
// 数组
let [list, setList] = useState<Coordinates[]>([{ name: '李四', age: 30 }]);

// 如果你的 => 后面跟了一对花括号 { ,那你必须使用 return 来指定返回值!
const listItem = list.map((oi)=>{
return <View key={oi.age}>{oi.name}</View>
});

return (
{
list.map((oi)=>{
return <Text className='main-list-title' key={oi.age}>{oi.name}</Text>
})
}
<View>{ listItem }</View>
</View>
)

条件渲染


计算属性


vue


computed: {
userinfo() {
return this.$store.state.userinfo;
},
},

react


const [serverUrl, setServerUrl] = useState('https://localhost:1234');
let [age, setAge] = useState(2);

const name = useMemo(() => {
return serverUrl + " " + age;
}, [serverUrl]);
console.log(name) // https://localhost:1234 2

监听器


vue


watch: {
// 保证自定义菜单始终显示在页面中
customContextmenuTop(top) {
...相关操作
}
},

react


import { useEffect, useState } from 'react';

export default function home() {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
const [age, setAge] = useState(2);

/**
* useEffect第二个参数中所传递的值才会进行根据值的变化而出发;
* 如果没有穿值的话,就不会监听数据变化
*/

useEffect(()=>{
if (age !== 5) {
setAge(++age)
}
},[age])

useEffect(()=>{
if(serverUrl !== 'w3c') {
setServerUrl('w3c');
}
},[serverUrl])

return(78)
}

总结


从上面的方法示例我们可以得出一个结论:在其他框架(自己会的)中常用到的方法或者场景进行针对性的学习即可。


这样的好处是你能快速的上手开发,然后在实际开发场景中遇到解决不了的问题再去查文档或者百度。


这只是我的一点小小的发现,哈哈哈。。。


如果对你有感触的话,可以尝试一下这个方法;我觉得还是很不错的


注意:react推荐函数式组件开发,不推荐类组件开发,我在上面没有说明,大家也可以去文档看看,类组件和函数组件还是有很大差别的,如:函数组件没有生命周期,一般使用监听来完成的,监听的使用方法还是有所不同,大家可以具体的去试试,我在这儿也是告诉大家一些方法;具体去学了才是你的。


为了方便自己学习记录,以及给大家提供思路,我下期给大家带来 vite + ts + react的搭建


作者:雾恋
来源:juejin.cn/post/7268844150233219107
收起阅读 »

一个大专生工作总结

2020刚入大学,学的云计算方向专业,也成功当时班级的学委,从那时候开始各种学习计算机像方面技术,学的比较杂,感觉啥都学。运维类的redhat centos7 ,前端web,最基础的 HTML、CSS、JavaScript,后端就从比较感兴趣的JAVA开始学,...
继续阅读 »

2020刚入大学,学的云计算方向专业,也成功当时班级的学委,从那时候开始各种学习计算机像方面技术,学的比较杂,感觉啥都学。运维类的redhat centos7 ,前端web,最基础的 HTML、CSS、JavaScript,后端就从比较感兴趣的JAVA开始学,学到后面越学越学不动,然后转战Python,学会了爬虫,从这时候开始疯狂在网络上找资源学,疯狂阅览互联网走势,还有各种好玩的技术。刚开始学计算机的应该都会想过以后当一名黑客吧。学习大半个学期逐渐熟悉了互联网大致内容了,发现自己不适合。学不动根本学不动,觉得自己只能朝一个方向发展了。


大一


自己也很不错,也非常爱学,也拿到了人生第一个奖学金,老师都对我印象也挺不错的,大一学的专业课大部分我都会,老师提出的问题,我都能一一回答上来,还能讲一两个解决方式。我们老师还好奇还问我你学过吗,我就说基本都学过,底下同学都投来羡慕眼光,那是应该算在我在班级第一次高光时刻。


img_v2_2f1640ed-5668-4b27-a440-97259463424g.gif


大二上


大二开始迷失自我了,开始翘课和兄弟出去玩耍喝酒,那时候我也追上我喜欢的女孩,也成了男女朋友,就开始不对学习感兴趣,天天除了玩就是玩。接下来就是期末考试直线下滑,辅导员开始找我谈话了,讲不少人生大道理,虽然我没听进去多少,但是我还是知道不能再继续颓废下去了。


v2_0285f304-80f0-4eac-8f9d-e36b3cf6635g.gif


大二下


开始思考人生了,觉得上了大学应该不留遗憾,开始考各类计算机证书:网络工程师中级证书、HCIA、HCIP、云计算中级证书,等之类没有含金量证书。当时准备冲击红帽认证后来疫情原因,也不让出校,就没有冲击欲望了。就参加计算机比赛去了,我记得当时总共参加了三个比赛,院系一等、B类二等、我最期望的A类比赛我苦学了大半个学期,天天待在机房里学,因为这个比赛东道主在我们学校举行,懂了都懂,大差不差也能拿省一,省一可以免试专升本。然后可以去一本高校读本科,因为疫情取消了,什么都取消了。


心里虽然不是个滋味!人生还是要继续的。


v2_4720fea9-0838-4295-bbbb-8254eaa782bg.jpg


大三


专科生大学基本都是2+1,两年在学校,半年实习,才能拿到毕-业-证!就这样2022年10月开始思考是否专升本问题,思来想去两种方案:假如考上还要继续上俩年大学,第二种早点进入社会工作不断提高自己工作经验,也能搞到大钱。我还是选择了第二种方案,开始写自己简历,然后疯狂在BOSS 智联招聘 全程无忧等招聘平台疯狂投送简历,然后就有一家比较大的企业看上我了,应聘的是网络运维工程师,实习4k转正5k、双休、包吃包住、免费住人才公寓,不快不慢就实习了6个月,也拿到了毕-业-证书,最后转正签劳动合同的时候还是选择了离开。
原因还是:工作学不到东西,加上工作挺舒坦的,每天基本没事,基本都是活少聊天多,就是这样。人是有欲望的,身边的好多朋友转正之后薪资7K-9K的,感觉自己不能再继续荒废下去了。


v2_c9054330-c8f5-4f29-8ce2-0306f9c9903g.jpg


辞职后,也存了一点钱,也玩了一个多月,开始找工作,互联网工作真的难找,加上我现在不是实习生了,对自己薪资要求也比较高,我就开始疯狂的学,也要拿出自己能出的手东西,就自己做了个人网站博客,买了服务器,买了域名。可是呢还是找不到工作,我就开始不找网络工程师方面工作了,简历到处投,直到有家比较大公司桌面运维工作找到了我。经过一两轮面试,合格通过了,但是薪资也谈的不太理想只有6k。
三线城市6K确实足够生活的,不过我还是要继续努力。helpdesk只是我的暂时的工作,还是要更高方向发展。


加油 加油 加油 !!!!


v2_0cc05c0e-8aec-41f8-b500-32c49e76270g.jpg


作者:一码归亿码
来源:juejin.cn/post/7312352526706524201
收起阅读 »

京东一面:post为什么会发送两次请求?🤪🤪🤪

在前段时间的一次面试中,被问到了一个如标题这样的问题。要想好好地去回答这个问题,这里牵扯到的知识点也是比较多的。 那么接下来这篇文章我们就一点一点开始引出这个问题。 同源策略 在浏览器中,内容是很开放的,任何资源都可以接入其中,如 JavaScript 文件、...
继续阅读 »

在前段时间的一次面试中,被问到了一个如标题这样的问题。要想好好地去回答这个问题,这里牵扯到的知识点也是比较多的。


那么接下来这篇文章我们就一点一点开始引出这个问题。


同源策略


在浏览器中,内容是很开放的,任何资源都可以接入其中,如 JavaScript 文件、图片、音频、视频等资源,甚至可以下载其他站点的可执行文件。


但也不是说浏览器就是完全自由的,如果不加以控制,就会出现一些不可控的局面,例如会出现一些安全问题,如:



  • 跨站脚本攻击(XSS)

  • SQL 注入攻击

  • OS 命令注入攻击

  • HTTP 首部注入攻击

  • 跨站点请求伪造(CSRF)

  • 等等......


如果这些都没有限制的话,对于我们用户而言,是相对危险的,因此需要一些安全策略来保障我们的隐私和数据安全。


这就引出了最基础、最核心的安全策略:同源策略。


什么是同源策略


同源策略是一个重要的安全策略,它用于限制一个源的文档或者它加载的脚本如何能与另一个源的资源进行交互。


如果两个 URL 的协议、主机和端口都相同,我们就称这两个 URL 同源。



  • 协议:协议是定义了数据如何在计算机内和之间进行交换的规则的系统,例如 HTTP、HTTPS。

  • 主机:是已连接到一个计算机网络的一台电子计算机或其他设备。网络主机可以向网络上的用户或其他节点提供信息资源、服务和应用。使用 TCP/IP 协议族参与网络的计算机也可称为 IP 主机。

  • 端口:主机是计算机到计算机之间的通信,那么端口就是进程到进程之间的通信。


如下表给出了与 URL http://store.company.com:80/dir/page.html 的源进行对比的示例:


URL结果原因
http://store.company.com:80/dir2/page.html同源只有路径不同
http://store.company.com:80/dir/inner/another.html同源只有路径不同
https://store.company.com:443/secure.html不同源协议不同,HTTP 和 HTTPS
http://store.company.com:81/dir/etc.html不同源端口不同
http://news.company.com:80/dir/other.html不同源主机不同

同源策略主要表现在以下三个方面:DOM、Web 数据和网络。



  • DOM 访问限制:同源策略限制了网页脚本(如 JavaScript)访问其他源的 DOM。这意味着通过脚本无法直接访问跨源页面的 DOM 元素、属性或方法。这是为了防止恶意网站从其他网站窃取敏感信息。

  • Web 数据限制:同源策略也限制了从其他源加载的 Web 数据(例如 XMLHttpRequest 或 Fetch API)。在同源策略下,XMLHttpRequest 或 Fetch 请求只能发送到与当前网页具有相同源的目标。这有助于防止跨站点请求伪造(CSRF)等攻击。

  • 网络通信限制:同源策略还限制了跨源的网络通信。浏览器会阻止从一个源发出的请求获取来自其他源的响应。这样做是为了确保只有受信任的源能够与服务器进行通信,以避免恶意行为。


出于安全原因,浏览器限制从脚本内发起的跨源 HTTP 请求,XMLHttpRequest 和 Fetch API,只能从加载应用程序的同一个域请求 HTTP 资源,除非使用 CORS 头文件


CORS


对于浏览器限制这个词,要着重解释一下:不一定是浏览器限制了发起跨站请求,也可能是跨站请求可以正常发起,但是返回结果被浏览器拦截了。


浏览器将不同域的内容隔离在不同的进程中,网络进程负责下载资源并将其送到渲染进程中,但由于跨域限制,某些资源可能被阻止加载到渲染进程。如果浏览器发现一个跨域响应包含了敏感数据,它可能会阻止脚本访问这些数据,即使网络进程已经获得了这些数据。CORB 的目标是在渲染之前尽早阻止恶意代码获取跨域数据。



CORB 是一种安全机制,用于防止跨域请求恶意访问跨域响应的数据。渲染进程会在 CORB 机制的约束下,选择性地将哪些资源送入渲染进程供页面使用。



例如,一个网页可能通过 AJAX 请求从另一个域的服务器获取数据。虽然某些情况下这样的请求可能会成功,但如果浏览器检测到请求返回的数据可能包含恶意代码或与同源策略冲突,浏览器可能会阻止网页访问返回的数据,以确保用户的安全。


跨源资源共享(Cross-Origin Resource Sharing,CORS)是一种机制,允许在受控的条件下,不同源的网页能够请求和共享资源。由于浏览器的同源策略限制了跨域请求,CORS 提供了一种方式来解决在 Web 应用中进行跨域数据交换的问题。


CORS 的基本思想是,服务器在响应中提供一个标头(HTTP 头),指示哪些源被允许访问资源。浏览器在发起跨域请求时会先发送一个预检请求(OPTIONS 请求)到服务器,服务器通过设置适当的 CORS 标头来指定是否允许跨域请求,并指定允许的请求源、方法、标头等信息。


简单请求


不会触发 CORS 预检请求。这样的请求为 简单请求,。若请求满足所有下述条件,则该请求可视为 简单请求



  1. HTTP 方法限制:只能使用 GET、HEAD、POST 这三种 HTTP 方法之一。如果请求使用了其他 HTTP 方法,就不再被视为简单请求。

  2. 自定义标头限制:请求的 HTTP 标头只能是以下几种常见的标头:AcceptAccept-LanguageContent-LanguageLast-Event-IDContent-Type(仅限于 application/x-www-form-urlencodedmultipart/form-datatext/plain)。HTML 头部 header field 字段:DPR、Download、Save-Data、Viewport-Width、WIdth。如果请求使用了其他标头,同样不再被视为简单请求。

  3. 请求中没有使用 ReadableStream 对象。

  4. 不使用自定义请求标头:请求不能包含用户自定义的标头。

  5. 请求中的任意 XMLHttpRequestUpload 对象均没有注册任何事件监听器;XMLHttpRequestUpload 对象可以使用 XMLHttpRequest.upload 属性访问


预检请求


非简单请求的 CORS 请求,会在正式通信之前,增加一次 HTTP 查询请求,称为 预检请求


需预检的请求要求必须首先使用 OPTIONS 方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。预检请求 的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。


例如我们在掘金上删除一条沸点:


20230822094049


它首先会发起一个预检请求,预检请求的头信息包括两个特殊字段:



  • Access-Control-Request-Method:该字段是必须的,用来列出浏览器的 CORS 请求会用到哪些 HTTP 方法,上例是 POST。

  • Access-Control-Request-Headers:该字段是一个逗号分隔的字符串,指定浏览器 CORS 请求会额外发送的头信息字段,上例是 content-type,x-secsdk-csrf-token

  • access-control-allow-origin:在上述例子中,表示 https://juejin.cn 可以请求数据,也可以设置为* 符号,表示统一任意跨源请求。

  • access-control-max-age:该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是 1 天(86408 秒),即允许缓存该条回应 1 天(86408 秒),在此期间,不用发出另一条预检请求。


一旦服务器通过了 预检请求,以后每次浏览器正常的 CORS 请求,就都跟简单请求一样,会有一个 Origin 头信息字段。服务器的回应,也都会有一个 Access-Control-Allow-Origin 头信息字段。


20230822122441


上面头信息中,Access-Control-Allow-Origin 字段是每次回应都必定包含的。


附带身份凭证的请求与通配符


在响应附带身份凭证的请求时:



  • 为了避免恶意网站滥用 Access-Control-Allow-Origin 头部字段来获取用户敏感信息,服务器在设置时不能将其值设为通配符 *。相反,应该将其设置为特定的域,例如:Access-Control-Allow-Origin: https://juejin.cn。通过将 Access-Control-Allow-Origin 设置为特定的域,服务器只允许来自指定域的请求进行跨域访问。这样可以限制跨域请求的范围,避免不可信的域获取到用户敏感信息。

  • 为了避免潜在的安全风险,服务器不能将 Access-Control-Allow-Headers 的值设为通配符 *。这是因为不受限制的请求头可能被滥用。相反,应该将其设置为一个包含标头名称的列表,例如:Access-Control-Allow-Headers: X-PINGOTHER, Content-Type。通过将 Access-Control-Allow-Headers 设置为明确的标头名称列表,服务器可以限制哪些自定义请求头是允许的。只有在允许的标头列表中的头部字段才能在跨域请求中被接受。

  • 为了避免潜在的安全风险,服务器不能将 Access-Control-Allow-Methods 的值设为通配符 *。这样做将允许来自任意域的请求使用任意的 HTTP 方法,可能导致滥用行为的发生。相反,应该将其设置为一个特定的请求方法名称列表,例如:Access-Control-Allow-Methods: POST, GET。通过将 Access-Control-Allow-Methods 设置为明确的请求方法列表,服务器可以限制哪些方法是允许的。只有在允许的方法列表中的方法才能在跨域请求中被接受和处理。

  • 对于附带身份凭证的请求(通常是 Cookie),


这是因为请求的标头中携带了 Cookie 信息,如果 Access-Control-Allow-Origin 的值为 *,请求将会失败。而将 Access-Control-Allow-Origin 的值设置为 https://juejin。cn,则请求将成功执行。


另外,响应标头中也携带了 Set-Cookie 字段,尝试对 Cookie 进行修改。如果操作失败,将会抛出异常。


为什么本地使用 webpack 进行 dev 开发时,不需要服务器端配置 cors 的情况下访问到线上接口?


当你在本地通过 Ajax 或其他方式请求线上接口时,由于浏览器的同源策略,会出现跨域的问题。但是在服务器端并不会出现这个问题。


它是通过 Webpack Dev Server 来实现这个功能。当你在浏览器中发送请求时,请求会先被 Webpack Dev Server 捕获,然后根据你的代理规则将请求转发到目标服务器,目标服务器返回的数据再经由 Webpack Dev Server 转发回浏览器。这样就绕过了浏览器的同源策略限制,使你能够在本地开发环境中访问线上接口。


参考文章



总结


预检请求是在进行跨域资源共享 CORS 时,由浏览器自动发起的一种 OPTIONS 请求。它的存在是为了保障安全,并允许服务器决定是否允许跨域请求。


跨域请求是指在浏览器中向不同域名、不同端口或不同协议的资源发送请求。出于安全原因,浏览器默认禁止跨域请求,只允许同源策略。而当网页需要进行跨域请求时,浏览器会自动发送一个预检请求,以确定是否服务器允许实际的跨域请求。


预检请求中包含了一些额外的头部信息,如 Origin 和 Access-Control-Request-Method 等,用于告知服务器实际请求的方法和来源。服务器收到预检请求后,可以根据这些头部信息,进行验证和授权判断。如果服务器认可该跨域请求,将返回一个包含 Access-Control-Allow-Origin 等头部信息的响应,浏览器才会继续发送实际的跨域请求。


使用预检请求机制可以有效地防范跨域请求带来的安全风险,保护用户数据和隐私。


整个完整的请求流程有如下图所示:


20230822122544


最后分享两个我的两个开源项目,它们分别是:



这两个项目都会一直维护的,如果你也喜欢,欢迎 star 🥰🥰🥰


作者:Moment
来源:juejin.cn/post/7269952188927017015
收起阅读 »

技术总监写的十个方法,让我精通了lambda表达式

前公司的技术总监写了工具类,对Java Stream 进行二次封装,使用起来非常爽,全公司都在用。 我自己照着写了一遍,改了名字,分享给大家。 一共整理了10个工具方法,可以满足 Collection、List、Set、Map 之间各种类型转化。例如 将 C...
继续阅读 »

前公司的技术总监写了工具类,对Java Stream 进行二次封装,使用起来非常爽,全公司都在用。


我自己照着写了一遍,改了名字,分享给大家。


一共整理了10个工具方法,可以满足 Collection、List、Set、Map 之间各种类型转化。例如



  1. Collection<OrderItem> 转化为 List<OrderItem>

  2. Collection<OrderItem> 转化为 Set<OrderItem>

  3. List<OrderItem> 转化为 List<Long>

  4. Set<OrderItem> 转化为 Set<Long>

  5. Collection<OrderItem> 转化为 List<Long>

  6. Collection<OrderItem> 转化为 Set<Long>

  7. Collection<OrderItem>中提取 Key, Map 的 Value 就是类型 OrderItem

  8. Collection<OrderItem>中提取 Key, Map 的 Value 根据 OrderItem 类型进行转化。

  9. Map<Long, OrderItem> 中的value 转化为 Map<Long, Double>

  10. value 转化时,lamada表达式可以使用(v)->{}, 也可以使用 (k,v)->{ }


Collection 集合类型到 Map类型的转化。


Collection 转化为 Map


由于 List 和 Set 是 Collection 类型的子类,所以只需要实现Collection 类型转化为 Map 类型即可。
Collection转化为 Map 共分两个方法



  1. Collection<OrderItem> Map<Key, OrderItem>,提取 Key, Map 的 Value 就是类型 OrderItem

  2. Collection<OrderItem>Map<Key,Value> ,提取 Key, Map 的 Value 根据 OrderItem 类型进行转化。


使用样例


代码示例中把Set<OrderItem> 转化为 Map<Long, OrderItem>Map<Long ,Double>


@Test
public void testToMap() {
Collection<OrderItem> collection = coll;
Set<OrderItem> set = toSet(collection);

Map<Long, OrderItem> map = toMap(set, OrderItem::getOrderId);
}

@Test
public void testToMapV2() {
Collection<OrderItem> collection = coll;
Set<OrderItem> set = toSet(collection);

Map<Long, Double> map = toMap(set, OrderItem::getOrderId, OrderItem::getActPrice);
}

代码展示


public static <T, K> Map<K, T> toMap(Collection<T> collection, Function<? super T, ? extends K> keyMapper) {
return toMap(collection, keyMapper, Function.identity());
}

public static <T, K, V> Map<K, V> toMap(Collection<T> collection,
Function<? super T, ? extends K> keyFunction,
Function<? super T, ? extends V> valueFunction)
{
return toMap(collection, keyFunction, valueFunction, pickSecond());
}

public static <T, K, V> Map<K, V> toMap(Collection<T> collection,
Function<? super T, ? extends K> keyFunction,
Function<? super T, ? extends V> valueFunction,
BinaryOperator<V> mergeFunction)
{
if (CollectionUtils.isEmpty(collection)) {
return new HashMap<>(0);
}

return collection.stream().collect(Collectors.toMap(keyFunction, valueFunction, mergeFunction));
}

public static <T> BinaryOperator<T> pickFirst() {
return (k1, k2) -> k1;
}
public static <T> BinaryOperator<T> pickSecond() {
return (k1, k2) -> k2;
}

Map格式转换


转换 Map 的 Value



  1. 将 Map<Long, OrderItem> 中的value 转化为 Map<Long, Double>

  2. value 转化时,lamada表达式可以使用(v)->{}, 也可以使用 (k,v)->{ }。


测试样例


@Test
public void testConvertValue() {
Collection<OrderItem> collection = coll;
Set<OrderItem> set = toSet(collection);

Map<Long, OrderItem> map = toMap(set, OrderItem::getOrderId);

Map<Long, Double> orderId2Price = convertMapValue(map, item -> item.getActPrice());
Map<Long, String> orderId2Token = convertMapValue(map, (id, item) -> id + item.getName());

}

代码展示


public static <K, V, C> Map<K, C> convertMapValue(Map<K, V> map, 
BiFunction<K, V, C> valueFunction,
BinaryOperator<C> mergeFunction)
{
if (isEmpty(map)) {
return new HashMap<>();
}
return map.entrySet().stream().collect(Collectors.toMap(
e -> e.getKey(),
e -> valueFunction.apply(e.getKey(), e.getValue()),
mergeFunction
));
}

public static <K, V, C> Map<K, C> convertMapValue(Map<K, V> originMap, BiFunction<K, V, C> valueConverter) {
return convertMapValue(originMap, valueConverter, Lambdas.pickSecond());
}

public static <T> BinaryOperator<T> pickFirst() {
return (k1, k2) -> k1;
}
public static <T> BinaryOperator<T> pickSecond() {
return (k1, k2) -> k2;
}

集合类型转化


Collection 和 List、Set 的转化



  1. Collection<OrderItem> 转化为 List<OrderItem>

  2. Collection<OrderItem> 转化为 Set<OrderItem>


public static <T> List<T> toList(Collection<T> collection) {
if (collection == null) {
return new ArrayList<>();
}
if (collection instanceof List) {
return (List<T>) collection;
}
return collection.stream().collect(Collectors.toList());
}

public static <T> Set<T> toSet(Collection<T> collection) {
if (collection == null) {
return new HashSet<>();
}
if (collection instanceof Set) {
return (Set<T>) collection;
}
return collection.stream().collect(Collectors.toSet());
}

测试样例


@Test//将集合 Collection 转化为 List
public void testToList() {
Collection<OrderItem> collection = coll;
List<OrderItem> list = toList(coll);
}

@Test//将集合 Collection 转化为 Set
public void testToSet() {
Collection<OrderItem> collection = coll;
Set<OrderItem> set = toSet(collection);
}

List和 Set 是 Collection 集合类型的子类,所以无需再转化。


List、Set 类型之间的转换


业务中有时候需要将 List<A> 转化为 List<B>。如何实现工具类呢?


public static <T, R> List<R> map(List<T> collection, Function<T, R> mapper) {
return collection.stream().map(mapper).collect(Collectors.toList());
}

public static <T, R> Set<R> map(Set<T> collection, Function<T, R> mapper) {
return collection.stream().map(mapper).collect(Collectors.toSet());
}

public static <T, R> List<R> mapToList(Collection<T> collection, Function<T, R> mapper) {
return collection.stream().map(mapper).collect(Collectors.toList());
}

public static <T, R> Set<R> mapToSet(Collection<T> collection, Function<T, R> mapper) {
return collection.stream().map(mapper).collect(Collectors.toSet());
}

测试样例



  1. List<OrderItem> 转化为 List<Long>

  2. Set<OrderItem> 转化为 Set<Long>

  3. Collection<OrderItem> 转化为 List<Long>

  4. Collection<OrderItem> 转化为 Set<Long>


@Test
public void testMapToList() {
Collection<OrderItem> collection = coll;
List<OrderItem> list = toList(coll);

List<Long> orderIdList = map(list, (item) -> item.getOrderId());
}

@Test
public void testMapToSet() {
Collection<OrderItem> collection = coll;
Set<OrderItem> set = toSet(coll);

Set<Long> orderIdSet = map(set, (item) -> item.getOrderId());
}

@Test
public void testMapToList2() {
Collection<OrderItem> collection = coll;

List<Long> orderIdList = mapToList(collection, (item) -> item.getOrderId());
}

@Test
public void testMapToSetV2() {
Collection<OrderItem> collection = coll;

Set<Long> orderIdSet = mapToSet(collection, (item) -> item.getOrderId());

}

总结一下 以上样例包含了如下的映射场景



  1. Collection<OrderItem> 转化为 List<OrderItem>

  2. Collection<OrderItem> 转化为 Set<OrderItem>

  3. List<OrderItem> 转化为 List<Long>

  4. Set<OrderItem> 转化为 Set<Long>

  5. Collection<OrderItem> 转化为 List<Long>

  6. Collection<OrderItem> 转化为 Set<Long>

  7. Collection<OrderItem>中提取 Key, Map 的 Value 就是类型 OrderItem

  8. Collection<OrderItem>中提取 Key, Map 的 Value 根据 OrderItem 类型进行转化。

  9. Map<Long, OrderItem> 中的value 转化为 Map<Long, Double>

  10. value 转化时,lamada表达式可以使用(v)->{}, 也可以使用 (k,v)->{ }


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

看不了电视直播了?那就自己做一个(一)

web
事情的起因是这样的,前两天打开电视看直播,突然变成了下面这个画面。 开始以为是太久没更新了,想着重新安装一下就好了。结果上网一查才知道,电视直播软件都下架了。 电视直播只能安装有线,或者通过央视频手机收看了。有线暂时安装不了,用手机看又总是感觉很别扭。 虽...
继续阅读 »

事情的起因是这样的,前两天打开电视看直播,突然变成了下面这个画面。


j29zRYWy.jpeg


开始以为是太久没更新了,想着重新安装一下就好了。结果上网一查才知道,电视直播软件都下架了。


电视直播只能安装有线,或者通过央视频手机收看了。有线暂时安装不了,用手机看又总是感觉很别扭。


a71ea8d3fd1f4134205611b2199935ccd0c85e21.png


虽然也可以通过投屏的方式用电视播放,但切换频道时还得使用手机操作,非常的麻烦。


广电的这波操作出发点可能是好的,后续应该会提供其他收看方式,但是目前这个真空期着实有点尴尬。思来想去,干脆自己动手做一个吧


就是干.jpg


经过一个周末的折腾,过程虽然有一点点曲折,但总算是完成了第一个电视版本,感觉和以前相比,清晰度还不错,切换也更流畅。


111.gif


再来看一下卫视。


也是OK的


22 (1).gif


电视端有了,又突然想着把它放到手机上,虽然手机上可以直接使用央视频播放,但还是有点繁琐,于是又稍微做了一些调整,推出了一个手机端的版本,切换还是相当的丝滑。


手机.gif


最后我其实还改了一版在电脑上使用的,但是这个除了摸鱼好像也没别的用处,所以对于我来说意义不大。


实现篇


接下来说一下具体的实现,会涉及到一些编程相关的内容,如果不感兴趣可以直接跳到结尾。


客户端应用开发


这一篇先介绍客户端的应用的开发,主要就是安卓应用的开发。虽然以前没有这方面经验,但是想法有了,剩下的交给chatGpt就好了。


1. 播放器


首先是播放器的选择,一开始我采用了原生MediaPlayer,主要是考虑到跟各版本安卓系统的兼容性会好一点,而且它使用起来非常的简单,十几行代码就搞定了。


public class PlayActivity extends Activity {
ChannelService channelService;
VideoView videoView;

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
channelService = new HttpChannelService();
setContentView(R.layout.activity_play);
videoView = findViewById(R.id.video_view);
channelService.loadChannels((success, message) -> runOnUiThread(() -> {
Channel channel = channelService.getDefaultChannel();
videoView.setVideoURI(Uri.parse(channel.getSource()));
videoView.start();
}));
}
}

后来替换成了谷歌开源的ExoPlayer,因为只是简单使用,所以代码基本上也没什么差别。


public class PlayActivity extends Activity {
ChannelService channelService;
private StyledPlayerView videoView;
private ExoPlayer player;

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
initView();
initChannels();
}

private void initView() {
setContentView(R.layout.activity_play);
videoView = findViewById(R.id.video_view);
player = new ExoPlayer.Builder(this).build();
videoView.setPlayer(player);
}

private void initChannels() {
channelService = new HttpChannelService();
channelService.loadChannels((boolean success, String message) -> runOnUiThread(() -> {
ChannelService.Channel channel = channelService.getDefaultChannel();
play(channel);
}));
}

private void play(Channel channel){
player.setMediaItem(MediaItem.fromUri(channel.getSource()));
player.prepare();
player.setPlayWhenReady(true);
}
}

2. 监听器


接下来就是考虑对遥控器按键的监听处理,对于这个应用而言,只需要监控方向键以及退出键就好了,当然也可以根据需要对菜单键或者确定键进行响应。


videoView.setOnKeyListener((view, keyCode, event) -> {
switch (keyCode) {
// 向下操作处理 切换下一个频道
case KeyEvent.KEYCODE_DPAD_DOWN:
if (event.getAction() == KeyEvent.ACTION_DOWN) {
Channel channel = channelService.getNextChannel();
play(channel);
return true;
}
break;
}
return false;
});

3. 视频源管理


把ChannelService留到最后讲,是因为它的作用通过下面的接口定义就一目了然了。


这里之所以要定义成接口,比如常见的通过m3u获取,是因为考虑到视频源可能有不同实现,关于实现部分会在下一篇详细讲解。


public interface ChannelService {

/**
* 加载频道
*/

void loadChannels(LoadCallBack callBack);

/**
* 获取默认频道
*/

Channel getDefaultChannel();

/**
* 获取下一个频道
*/

Channel getNextChannel();

/**
* 获取前一个频道
*/

Channel getPrevChannel();

}

4. 手机版


手机版因为没有了遥控器,所以需要对触屏动作进行监听来对视频进行操控,主要就是左右滑动的切换,以及上下滑动的音量调节等。


结语


因为是即兴的创作,也没有打算能长久使用,所以很多细节我并没有考虑,比如内容的缓存,节目回看,网络监控等等这些。但是这几天使用下来体验还是挺不错的。后续我可以把整个源码开放出来,大家有兴趣可以自行去补充。


到这里客户端的实现就讲完了,下一篇再讲一下其他部分的实现。


作者:双子小匠
来源:juejin.cn/post/7311961893610995748
收起阅读 »

带圆角的虚线边框?CSS 不在话下

今天,我们来看这么一个非常常见的切图场景,我们需要一个带圆角的虚线边框,像是这样: 这个我们使用 CSS 还是可以轻松解决的,代码也很简单,核心代码: div { border-radius: 25px; border: 2px dashed...
继续阅读 »

今天,我们来看这么一个非常常见的切图场景,我们需要一个带圆角的虚线边框,像是这样:



这个我们使用 CSS 还是可以轻松解决的,代码也很简单,核心代码:


div {
border-radius: 25px;
border: 2px dashed #aaa;
}

但是,原生的 dashed 有一个问题,就是我们无法控制虚线的单段长度与间隙


假设,我们要这么一个效果呢虚线效果呢:



此时,由于无法控制 border: 2px dashed #aaa 产生的虚线的单段长度与线段之间的间隙,border 方案就不再适用了。


那么,在 CSS 中,我们还有其它方式能够实现带圆角,且虚线的单段长度与线段之间间隙可控的方式吗?


本文,我们就一起探讨探讨。


实现不带圆角的虚线效果


上面的场景,使用 CSS 实现起来比较麻烦的地方在于,图形有一个 border-radius


如果不带圆角,我们可以使用渐变,很容易的模拟虚线效果。


我们可以使用线性渐变,轻松的模拟虚线的效果:


div {
width: 150px;
height: 100px;
background: linear-gradient(90deg, #333 50%, transparent 0) repeat-x;
background-size: 4px 1px;
background-position: 0 0;
}

看看,使用渐变模拟的虚线如下:



解释一下上面的代码:



  1. linear-gradient(90deg, #333 50%, transparent 0),实现一段渐变内容,100% - 50% 的内容是 #333 颜色,剩下的一半 50% - 0 的颜色是透明色 transprent

  2. repeat-x 表示只在 x 方向重复

  3. background-size: 4px 1px 表示上述渐变内容的长宽分别是 4px\ 1px,这样配合 repeat-x就能实现只有 X 方向的重复

  4. 最后的 background-position: 0 0 控制渐变的定位


因此,我们只需要修改 background 的参数,就可以得到各种不一样的虚线效果:



完整的代码,你可以戳这里:CodePen Demo -- Linear-gradient Dashed Effect


并且,渐变是支持多重渐变的,因此,我们把容器的 4 个边都用渐变表示即可:


div {
background:
linear-gradient(90deg, #333 50%, transparent 0) repeat-x,
linear-gradient(90deg, #333 50%, transparent 0) repeat-x,
linear-gradient(0deg, #333 50%, transparent 0) repeat-y,
linear-gradient(0deg, #333 50%, transparent 0) repeat-y;
background-size: 4px 1px, 4px 1px, 1px 4px, 1px 4px;
background-position: 0 0, 0 100%, 0 0, 100% 0;
}

效果如下:



但是,如果要求的元素带 border-radius 圆角,这个方法就不好使了,整个效果就会穿帮。


因此,在有圆角的情况下,我们就需要另辟蹊径。


利用渐变实现带圆角的虚线效果


当然,本质上我们还是需要借助渐变效果,只是,我们需要转换一下思路。


譬如,我们可以使用角向渐变。


假设,我们有这么一个带圆角的元素:


<div>div>

div {
width: 300px;
height: 200px;
background: #eee;
border-radius: 20px;
}

效果如下:



如果我们修改内部的 background: #eee,把它替换成重复角向渐变的这么一个图形:


div {
//...
- background: #eee;
+ background: repeating-conic-gradient(#000, #000 3deg, transparent 3deg, transparent 6deg);
}

解释一下,这段代码创建了一个重复的角向渐变背景,从黑色(#000)开始,每 3deg 变为透明,然后再从透明到黑色,以此循环重复。


此时,这样的背景效果可用于创建一种渐变黑色到透明的重复纹理效果:



在这个基础上,我们只需要给这个图形上层,再利用伪元素,叠加一层颜色,就得到了我们想要的边框效果,并且,边框间隙和大小可以简单调整。


完整的代码:


div {
position: relative;
width: 300px;
height: 200px;
border-radius: 20px;
background: repeating-conic-gradient(#000, #000 3deg, transparent 3deg, transparent 6deg);

&::before {
content: "";
position: absolute;
inset: 1px;
background: #eee;
border-radius: 20px;
}
}

效果如下:



乍一看,效果还不错。但是如果仔细观察,会发现有一个致命问题:虚线线段的每一截长度不一致


只有当图形的高宽一致时,线段长度才会一致。高宽比越远离 1,差异则越大:



完整的代码,你可以戳这里:CodePen Demo -- BorderRadius Dashed Border


那有没有办法让虚线长度能够保持一样呢?


可以!我们再换一种渐变,我们改造一下底下的角向渐变,重新利用重复线性渐变:


div {
border-radius: 20px;
background:
repeating-linear-gradient(
-45deg,
#000 0,
#000 7px,
transparent 7px,
transparent 10px
);
}

此时,我们能得到这样一个斜 45° 的重复线性渐变图形:



与上面方法一类似,再通过在这个图形的基础上,在元素中心,叠加多一层纯色遮罩图形,只漏出最外围一圈的图形,带圆角的虚线边框就实现了:



此方法比上面第一种渐变方法更好之处在于,虚线每一条线段的长度是固定的!是不是非常的巧妙?


完整的代码,你可以戳这里:CodePen Demo -- BorderRadius Dashed Border


最佳解决方案:SVG


当然,上面使用 CSS 实现带圆角的虚线边框,还是需要一定的 CSS 功底。


并且,不管是哪个方法,都存在一定的瑕疵。譬如如果希望边框中间不是背景色,而是镂空的,上述两种 CSS 方式都将不再使用。


因此,对于带圆角的虚线边框场景,最佳方式一定是 SVG。(切图也算是吧,但是灵活度太低)


只是很多人看到 SVG 会天然的感到抗拒,或者认为 SVG 不太好掌握。


所以,本文再介绍一个非常有用的开源工具 -- Customize your CSS Border



通过这个开源工具,我们可以快速生成我们想要的虚线边框效果,并且一键复制可以嵌入到 CSS background 中的 SVG 代码图片格式。


图形的大小、边框的粗细、虚线的线宽与间距,圆角大小统统是可以可视化调整的。


通过一个动图,简单感受一下:



总结一下


本文介绍了 2 种在 CSS 中,不借助切图和 SVG 实现带圆角的虚线边框的方式:



  1. 重复角向渐变叠加遮罩层

  2. 重复线性渐变叠加遮罩层


当然,两种 CSS 方式都存在一定瑕疵,但是对于一些简单场景是能够 Cover 住的。


最后,介绍了借助 SVG 工具 Customize your CSS Border 快速生成带圆角的虚线边框的方式。将 SVG 生成的矢量图像数据直接嵌入到 background URL 中,能够应付几乎所有场景,相对而言是更好的选择。


最后


好了,本文到此结束,希望本文对你有所帮助 :)


作者:Chokcoco
来源:juejin.cn/post/7311681326712487999
收起阅读 »

前端打包后,静态文件的名字为什么是一串Hash值?

web
引言 前段时间公司需要招聘几个初级前端,面试过程中,问了这么一个问题:“项目打包后的dist文件夹中,比如js、css这些文件的名称为什么是hash值的,就是一串无规则的字符串”。基本上都不知道静态文件为什么需要这种无规则的hash值,今天就稍微说一下哈。 静...
继续阅读 »

引言


前段时间公司需要招聘几个初级前端,面试过程中,问了这么一个问题:“项目打包后的dist文件夹中,比如js、css这些文件的名称为什么是hash值的,就是一串无规则的字符串”。基本上都不知道静态文件为什么需要这种无规则的hash值,今天就稍微说一下哈。


静态文件何时被加载


拿常用的单页面应用举例,当我们访问一个网站的时候,最终会指向 index.html 这个文件,也就是打包后的 dist 文件夹中的 index.html


比如说 https://some-domain.com/home, 并点击回车键,我们的服务器中实际上没有这个路由,但我们不是返回 404,而是返回我们的 index.html。为什么地址中我们没有输入index.html这个路径,但还是指向到 index.html文件并加载它?因为现在大多数都用nginx去部署,一般在url中输入地址的时候末尾都会加个 “/” ,nginx中已经把“/”重定向到 index.html文件了


image.png


此时按下回车,这个 index.html 文件就被加载获取到了,然后开始自上而下的去加载里面的引用和代码,比如在html中引入的css、js、图片文件。


浏览器默认缓存


当用户按下回车键就向目标服务器去请求index.html文件,加载解析index.html文件的同时就会连带着加载里面的js、css文件。有没有想过,用户第一次已经从服务器请求下载静态文件到客户端了,第二次去浏览该网站该不会还让我去向服务器请求吧,不会吧不会吧,如果每次都请求下载,那用户的体验多不好,每次请求都需要时间,不说服务器的压力会增加,最重要的是用户的体验感,展现到用户眼前的时间会增加!


所以说浏览器已经想到这个了,当请求静态资源的时候,就会默认缓存请求过来的静态文件,这种浏览器自带的默认缓存就叫做 启发式缓存 。 除非手动设置不需要缓存no-store,此时请求静态文件的时候文件才不会缓存到本地!


浏览器默认缓存详情可见 MDN 启发式缓存。不管什么缓存都有缓存的时效性吧,如果想知道 启发式缓存 到底把静态文件缓存了多久,可以阅读笔者的这篇文章 浏览器的启发式缓存到底缓存了多久?


vue-cli里的默认配置,css和js的名字都加了哈希值,所以新版本css、js和就旧版本的名字是不同的,不会有缓存问题。


Hash值的作用


那既然知道了浏览器会有默认的缓存,当加载静态资源的时候会命中启发式缓存并缓存到本地。那如果我们重新部署前端包的时候,如何去请求新的静态资源呢,而不是缓存的静态资源?这时候就得用到hash值了


下面模拟了掘金网站的静态资源获取,当请求静态资源的时候,实际访问的是服务器中静态资源存放的位置
image.png


返回即是当前请求静态资源的具体内容
image.png


第一次进来的时候会去请求服务器的资源缓存到本地,比如 0dbcf63.js 这个文件就被缓存到本地了,后面再正常刷新就直接获取的是缓存中的资源了(disk cache 内存缓存)。


如果前端包重新部署后,试想一下如果 0dbcf63.js这个js文件不叫这个无规则的名字,而是固定的名字,那浏览器怎么知道去请求缓存中的还是服务器中的,所以浏览器的机制就是请求缓存中的,除非缓存中的过期了,才会去请求服务器中的静态资源。如果没有请求到最新的资源,页面也不会更新到最新的内容,影响用户的体验。


那浏览器这个机制是怎么判断的,那就是根据资源的名称,该资源的资源名称如果没变 并且 没有设置不缓存 并且 资源没过期,那就会请求缓存中的资源,否则就会请求服务器中的资源


image.png



当静态资源的名称发生变化的时候,请求路径就会发生变化,所以就会重新命中服务器新部署的静态资源!这就是为什么需要hash值的原因,为了区分之前部署的文件和这次部署文件的区别,好让浏览器去重新获取最新的资源。



第三方库


由于像 lodash 或 react 这样的第三方库很少像本地源代码一样频繁修改,因此通常推荐将第三方库提取到单独的 chunk-vendor 中


 const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
entry: './src/index.js',
plugins: [
new HtmlWebpackPlugin({
title: 'Caching',
}),
],
output: {
filename: '[name].[contenthash].js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
optimization: {
moduleIds: 'deterministic',
runtimeChunk: 'single',
splitChunks: {
cacheGr0ups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
},
},
},
},
};

这样依赖的静态文件就会打包到chunk-vendor中,并且多次打包不会改变文件的hash值,以上是webpack原生的配置,如果使用的vue脚手架,那么脚手架已经都配置好了。


image.png


作者:Lakeiedward
来源:juejin.cn/post/7311219067199881254
收起阅读 »

三行代码实现完美瀑布流

web
需求 最近准备做一个瀑布流的需求,这里每个卡片的高度是不固定的,有点类似下图这样的。 难点 如果绝对定位,如何定位每个卡片的位置。 因为可以发现需求里边的每个卡片的高度都是不固定的,所以如果想使用绝对定位的话,需要实时动态的计算每个卡片的left、top,...
继续阅读 »

需求


最近准备做一个瀑布流的需求,这里每个卡片的高度是不固定的,有点类似下图这样的。



难点



  1. 如果绝对定位,如何定位每个卡片的位置。
    因为可以发现需求里边的每个卡片的高度都是不固定的,所以如果想使用绝对定位的话,需要实时动态的计算每个卡片的left、top,这里会涉及大量的计算。


因为每个卡片的高度是不固定,所以如果想要计算left、top,必须首先获取卡片的高度,但是卡片里边不仅包含图片,还有文字,这个时候计算高度是比较困难的。




  1. 如何结合虚拟列表实现瀑布流


这个时候必须要根据scrollTop的位置,判断什么时候需要加载哪些数据,判断可视区域里边数据的起始索引以及结束索引,这里同样会涉及大量的计算,同时还因为每个卡片的高度不固定,甚至只有图片和文字加载到浏览器以后,才能得到真实的高度,这样会更困难。


解决方案


解决方案1



  1. 如果绝对定位,如何定位每个卡片的位置。


1.1 后端计算
后端可以先把每个图片的高度和宽度提前计算好,直接返回给前端进行处理,然后前端根据后端返回的图片高度和宽度,然后再动态的计算出每个卡片的高度(文字部分也可以固定高度,使用省略号实现)。


1.2 前端计算


前端计算还是比较麻烦的,需要先等卡片组件加载完成,才能得到宽度和高度,而且因为数据量比较大,每个卡片计算出来以后,还需要去根据计算出来的结果去更新left、top,会非常麻烦。
这里可以采用node作为中间层进行计算,还是使用类似后端计算的思路。


还有一种方法是使用observe api 动态观察每个卡片,当观察到卡片加载完成后,再动态根据卡片的宽度和高度计算,不过这样同样很麻烦。



  1. 如何结合虚拟列表实现瀑布流


这里因为卡片的高度是不固定的,同时也是瀑布流,所以不能使用react-window 来解决,不过可以使用react-window的类似思路,自己封装一个npm 包,根据scroll事件判断需要加载那些数据。


解决方案2


使用css3的columns来实现,该技术解决方案不需要计算高度,也不需要去定位,但是columns这个属性会把卡片高度给切开
如下图:



不过可以使用下面代码来解决,


js
复制代码

.test {
// color: red;
// height: 2000px;
background-color: red;
gap: 1rem;
columns: 5;
.no-break {
break-inside: avoid;
}
}

效果如下:



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

推荐一个小而全的第三方登录开源组件

大家好,我是 Java陈序员。 我们在企业开发中,常常需要实现登录功能,而有时候为了方便,就需要集成第三方平台的授权登录。如常见的微信登录、微博登录等,免去了用户注册步骤,提高了用户体验。 为了业务考虑,我们有时候集成的不仅仅是一两个第三方平台,甚至更多。这就...
继续阅读 »

大家好,我是 Java陈序员


我们在企业开发中,常常需要实现登录功能,而有时候为了方便,就需要集成第三方平台的授权登录。如常见的微信登录、微博登录等,免去了用户注册步骤,提高了用户体验。


为了业务考虑,我们有时候集成的不仅仅是一两个第三方平台,甚至更多。这就会大大的提高了工作量,那么有没有开源框架来统一来集成这些第三方授权登录呢?


答案是有的,今天给大家介绍的项目提供了一个第三方授权登录的工具类库


项目介绍


JustAuth —— 一个第三方授权登录的工具类库,可以让你脱离繁琐的第三方登录 SDK,让登录变得So easy!


JustAuth


JustAuth 集成了诸如:Github、Gitee、微博、钉钉、百度、Coding、腾讯云开发者平台、OSChina、支付宝、QQ、微信、淘宝、Google、Facebook、抖音、领英、小米、微软、今日头条、Teambition、StackOverflow、Pinterest、人人、华为、企业微信、酷家乐、Gitlab、美团、饿了么、推特、飞书、京东、阿里云、喜马拉雅、Amazon、Slack和 Line 等第三方平台的授权登录。


功能特色:



  • 丰富的 OAuth 平台:支持国内外数十家知名的第三方平台的 OAuth 登录。

  • 自定义 state:支持自定义 State 和缓存方式,开发者可根据实际情况选择任意缓存插件。

  • 自定义 OAuth:提供统一接口,支持接入任意 OAuth 网站,快速实现 OAuth 登录功能。

  • 自定义 Http:接口 HTTP 工具,开发者可以根据自己项目的实际情况选择相对应的HTTP工具。

  • 自定义 Scope:支持自定义 scope,以适配更多的业务场景,而不仅仅是为了登录。

  • 代码规范·简单:JustAuth 代码严格遵守阿里巴巴编码规约,结构清晰、逻辑简单。


安装使用


回顾 OAuth 授权流程


参与的角色



  • Resource Owner 资源所有者,即代表授权客户端访问本身资源信息的用户(User),也就是应用场景中的“开发者A”

  • Resource Server 资源服务器,托管受保护的用户账号信息,比如 Github
    Authorization Server 授权服务器,验证用户身份然后为客户端派发资源访问令牌,比如 Github

  • Resource ServerAuthorization Server 可以是同一台服务器,也可以是不同的服务器,视具体的授权平台而有所差异

  • Client 客户端,即代表意图访问受限资源的第三方应用


授权流程


OAuth 授权流程


使用步骤


1、申请注册第三方平台的开发者账号


2、创建第三方平台的应用,获取配置信息(accessKey, secretKey, redirectUri)


3、使用 JustAuth 实现授权登陆


引入依赖


<dependency>
<groupId>me.zhyd.oauthgroupId>
<artifactId>JustAuthartifactId>
<version>{latest-version}version>
dependency>

调用 API


// 创建授权request
AuthRequest authRequest = new AuthGiteeRequest(AuthConfig.builder()
.clientId("clientId")
.clientSecret("clientSecret")
.redirectUri("redirectUri")
.build());
// 生成授权页面
authRequest.authorize("state");
// 授权登录后会返回code(auth_code(仅限支付宝))、state,1.8.0版本后,可以用AuthCallback类作为回调接口的参数
// 注:JustAuth默认保存state的时效为3分钟,3分钟内未使用则会自动清除过期的state
authRequest.login(callback);


说明:
JustAuth 的核心就是一个个的 request,每个平台都对应一个具体的 request 类。
所以在使用之前,需要就具体的授权平台创建响应的 request.如示例代码中对应的是 Gitee 平台。



集成国外平台



国外平台需要额外配置 httpConfig



AuthRequest authRequest = new AuthGoogleRequest(AuthConfig.builder()
.clientId("Client ID")
.clientSecret("Client Secret")
.redirectUri("应用回调地址")
// 针对国外平台配置代理
.httpConfig(HttpConfig.builder()
// Http 请求超时时间
.timeout(15000)
// host 和 port 请修改为开发环境的参数
.proxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress("127.0.0.1", 10080)))
.build())
.build());

SpringBoot 集成


引入依赖


<dependency>
<groupId>com.xkcoding.justauthgroupId>
<artifactId>justauth-spring-boot-starterartifactId>
<version>1.4.0version>
dependency>

配置文件


justauth:
enabled: true
type:
QQ:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/qq/callback
union-id: false
WEIBO:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/weibo/callback
GITEE:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/gitee/callback
DINGTALK:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/dingtalk/callback
BAIDU:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/baidu/callback
CSDN:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/csdn/callback
CODING:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/coding/callback
coding-group-name: xx
OSCHINA:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/oschina/callback
ALIPAY:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/alipay/callback
alipay-public-key: MIIB**************DAQAB
WECHAT_OPEN:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/wechat_open/callback
WECHAT_MP:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/wechat_mp/callback
WECHAT_ENTERPRISE:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/wechat_enterprise/callback
agent-id: 1000002
TAOBAO:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/taobao/callback
GOOGLE:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/google/callback
FACEBOOK:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/facebook/callback
DOUYIN:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/douyin/callback
LINKEDIN:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/linkedin/callback
MICROSOFT:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/microsoft/callback
MI:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/mi/callback
TOUTIAO:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/toutiao/callback
TEAMBITION:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/teambition/callback
RENREN:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/renren/callback
PINTEREST:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/pinterest/callback
STACK_OVERFLOW:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/stack_overflow/callback
stack-overflow-key: asd*********asd
HUAWEI:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/huawei/callback
KUJIALE:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/kujiale/callback
GITLAB:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/gitlab/callback
MEITUAN:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/meituan/callback
ELEME:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/eleme/callback
TWITTER:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/twitter/callback
XMLY:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/xmly/callback
# 设备唯一标识ID
device-id: xxxxxxxxxxxxxx
# 客户端操作系统类型,1-iOS系统,2-Android系统,3-Web
client-os-type: 3
# 客户端包名,如果 clientOsType 为12时必填。对Android客户端是包名,对IOS客户端是Bundle ID
#pack-id: xxxx
FEISHU:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/feishu/callback
JD:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/jd/callback
cache:
type: default

代码使用


@Slf4j
@RestController
@RequestMapping("/oauth")
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class TestController {
private final AuthRequestFactory factory;

@GetMapping
public List list() {
return factory.oauthList();
}

@GetMapping("/login/{type}")
public void login(@PathVariable String type, HttpServletResponse response) throws IOException {
AuthRequest authRequest = factory.get(type);
response.sendRedirect(authRequest.authorize(AuthStateUtils.createState()));
}

@RequestMapping("/{type}/callback")
public AuthResponse login(@PathVariable String type, AuthCallback callback) {
AuthRequest authRequest = factory.get(type);
AuthResponse response = authRequest.login(callback);
log.info("【response】= {}", JSONUtil.toJsonStr(response));
return response;
}

}

总结


JustAuth 集成的第三方授权登录平台,可以说是囊括了业界中大部分主流的应用系统。如国内的微信、微博、Gitee 等,还有国外的 Github、Google 等。可以满足我们日常的开发需求,开箱即用,可快速集成!


最后,贴上项目地址:


https://github.com/justauth/JustAuth

在线文档:


https://www.justauth.cn/

最后


推荐的开源项目已经收录到 GitHub 项目,欢迎 Star


https://github.com/chenyl8848/great-open-source-project

或者访问网站,进行在线浏览:


https://chencoding.top:8090/#/

作者:Java陈序员
来源:juejin.cn/post/7312060958175559743
收起阅读 »

token过期了怎么办?

token过期了怎么办?一般做法是重复第一次获取token的过程(比如登录,扫描授权等) ,这样做的缺点是用户体验不好,每一小时强制登录一次几乎是无法忍受的。那应该怎么办呢?其实这是一个老生常谈的问题,但是最近发现很多人并不清楚,所以今天就一次讲清这...
继续阅读 »

token过期了怎么办?一般做法是重复第一次获取token的过程(比如登录,扫描授权等) ,这样做的缺点是用户体验不好,每一小时强制登录一次几乎是无法忍受的。那应该怎么办呢?其实这是一个老生常谈的问题,但是最近发现很多人并不清楚,所以今天就一次讲清这个问题!

token 过期处理

没有绝对的安全, 所谓的安全处理, 就是提高攻击者攻击的难度, 对他造成了一定的麻烦, 我们这个网站就是安全的! 网站安全性就是高的! 所以: token 必须要有过期时间!

token 过期问题

目标: 了解token过期问题的存在, 学习token过期的解决思路

现象:

你登陆成功之后,接口会返回一个token值,这个值在后续请求时带上(就像是开门钥匙)。

但是,这个值一般会有有效期(具体是多长,是由后端决定),在我们的项目中,这个有效期是2小时。

如果,上午8点登陆成功,到了10:01分,则token就会失效,再去发请求时,就会报401错误。

思考:

  1. token需要过期时间吗 ?

    token即是获取受保护资源的凭证,当然必须有过期时间。否则一次登录便可永久使用,认证功能就失去了其意义。非但必须有个过期时间,而且过期时间还不能太长,

    参考各个主流网站的token过期时间,一般1小时左右

    token一旦过期, 一定要处理, 不处理, 用户没法进行一些需要授权页面的使用了

  2. token过期该怎么办?

    token过期,就要重新获取。

    那么重新获取有两种方式,一是重复第一次获取token的过程(比如登录,扫描授权等) ,这样做的缺点是用户体验不好,每一小时强制登录一次几乎是无法忍受的。

    那么还剩第二种方法,那就是主动去刷新token. 主动刷新token的凭证是refresh token,也是加密字符串,并且和token是相关联的。相比可以获取各种资源的token,refresh token的作用仅仅是获取新的token,因此其作用和安全性要求都大为降低,所以其过期时间也可以设置得长一些。

目标效果 - 保证每一小时, 都是一个不同的token

第一次请求 9:00 用的是 token1  第二次请求 12:00 用的是 token2

当用户登陆成功之后,返回的token中有两个值,说明如下:

image.png

  • token:

    • 作用:在访问一些接口时,需要传入token,就是它。
    • 有效期:2小时。
  • refresh_token

    • 作用: 当token的有效期过了之后,可以使用它去请求一个特殊接口(这个接口也是后端指定的,明确需要传入refresh_token),并返回一个新的token回来(有效期还是2小时),以替换过期的那个token。
    • 有效期:14天。(最理想的情况下,一次登陆可以持续14天。)

image.png

对于 某次请求A 的响应,如果是401错误

  • 有refresh_token,用refresh_token去请求回新的token

    • 新token请求成功

      • 更新本地token
      • 再发一次请求A
    • 新token请求失败

      • 清空vuex中的token
      • 携带请求地址,跳转到登陆页
  • 没有refresh_token

    • 清空vuex中的token
    • 携带请求地址,跳转到登陆页

对于一个请求的响应 401, 要这么处理, 对于十个请求的响应 401, 也要这么处理,

我们可以统一将这个token过期处理放在响应拦截器中

请求拦截器: 所有的请求, 在真正被发送出去之前, 都会先经过请求拦截器 (可以携带token)

响应拦截器: 所有的响应, 在真正被(.then.catch await)处理之前, 都会先经过响应拦截器, 可以在这个响应拦截器中统一对响应做判断

响应拦截器处理token

目标: 通过 axios 响应拦截器来处理 token 过期的问题

响应拦截器: http://www.kancloud.cn/yunye/axios…

  1. 没有 refresh_token 拦截到登录页, 清除无效的token

测试: {"token":"123.123.123"}

// 添加响应拦截器
http.interceptors.response.use(function (response) {
// 对响应数据做点什么 (成功响应) response 就是成功的响应 res
return response
}, function (error) {
// 对响应错误做点什么 (失败响应) 处理401错误
// console.dir(error)
if (error.response.status === 401) {
console.log('token过期了, 一小时过去了, 需要通过refresh_token去刷新token')
// 获取 refresh_token, 判断是否存在, 存在就去刷新token
const refreshToken = store.state.tokenInfo.refresh_token
if (refreshToken) {
console.log('存在refreshToken, 需要进行刷新token操作')
} else {
// 没有refreshToken, 直接去登录, 将来还能跳回来
// router.currentRoute 指向当前路由信息对象 === 等价于之前页面中用的 this.$route
// 清除本地token, 跳转登录 (无意义的本地token内容, 要清除)
store.commit('removeToken')
router.push({
path: '/login',
query: {
backto: router.currentRoute.fullPath
}
})
}
}
return Promise.reject(error)
})

提供清除token的mutation

// 移出tokenInfo的信息, 恢复成空对象
removeToken (state) {
state.tokenInfo = {}
// 更新到本地, 本地可以清掉token信息
removeToken()
},
  1. 有 refresh_token 发送请求, 刷新token

测试操作: 将 token 修改成 xyz, 模拟 token 过期, 而有 refresh_token 发现401, 会自动帮你刷新token

{"refresh_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MDYzNTcyODcsInVzZXJfaWQiOjExMDI0OTA1MjI4Mjk3MTc1MDQsInJlZnJlc2giOnRydWV9.2A81gpjxP_wWOjclv0fzSh1wzNm6lNy0iXM5G5l7TQ4","token":"xyz"}

const refreshToken = store.state.tokenInfo.refresh_token
if (refreshToken) {
console.log('存在refreshToken, 需要进行刷新token操作')
// (1) 发送请求, 进行刷新token操作, 获取新的token
// 注意: 这边发请求, 不用http实例, 用它会自动在请求前帮你携带token(会覆盖你的refresh_token)
// 这边, 直接用 axios 发送请求
const res = await axios({
method: 'put',
url: 'http://ttapi.research.itcast.cn/app/v1_0/authorizations',
// 请求头中携带refresh_token信息
headers: {
Authorization: `Bearer ${refreshToken}`
}
})
const newToken = res.data.data.token
// (2) 将新token更新到vuex中
store.commit('setTokenInfo', {
refresh_token: refreshToken,
token: newToken
})
}
  1. 刷新token后, 应该重新发送刚才的请求 (让用户刷新token无感知)
return http(error.config)
  1. 那万一 refresh_token 也过期了, 是真正的用户登录过期了 (一定要让用户重新登录的)

测试: {"refresh_token":"123.123","token":"123.123.123"} 修改后, 修改的是本地, 记得刷新一下

从哪拦走的, 就回到哪去

// 添加响应拦截器
http.interceptors.response.use(function (response) {
// 对响应数据做点什么 (成功响应) response 就是成功的响应 res
return response
}, async function (error) {
// 对响应错误做点什么 (失败响应) 处理401错误
// console.dir(error)
if (error.response.status === 401) {
console.log('token过期了, 一小时过去了, 需要通过refresh_token去刷新token')
// 获取 refresh_token, 判断是否存在, 存在就去刷新token
const refreshToken = store.state.tokenInfo.refresh_token
if (refreshToken) {
try {
console.log('存在refreshToken, 需要进行刷新token操作')
// (1) 发送请求, 进行刷新token操作, 获取新的token
// 注意: 这边发请求, 不用http实例, 用它会自动在请求前帮你携带token(会覆盖你的refresh_token)
// 这边, 直接用 axios 发送请求
const res = await axios({
method: 'put',
url: 'http://ttapi.research.itcast.cn/app/v1_0/authorizations',
// 请求头中携带refresh_token信息
headers: {
Authorization: `Bearer ${refreshToken}`
}
})
const newToken = res.data.data.token
// (2) 将新token更新到vuex中
store.commit('setTokenInfo', {
refresh_token: refreshToken,
token: newToken
})
// (3) 重新发送刚才的请求, http, 自动携带token (携带的是新token)
// error.config就是之前用于请求的配置对象, 可以直接给http使用
return http(error.config)
} catch {
// refresh_token 过期了, 跳转到登录页
// 清除过期的token对象
store.commit('removeToken')
// 跳转到登录页, 跳转完, 将来跳回来
router.push({
path: '/login',
query: {
backto: router.currentRoute.fullPath
}
})
}
} else {
// 没有refreshToken, 直接去登录, 将来还能跳回来
// router.currentRoute 指向当前路由信息对象 === 等价于之前页面中用的 this.$route
// 清除本地token, 跳转登录 (无意义的本地token内容, 要清除)
store.commit('removeToken')
router.push({
path: '/login',
query: {
backto: router.currentRoute.fullPath
}
})
}
}
return Promise.reject(error)
})

注意点:

  1. 响应拦截器要加在axios实例 http 上。
  2. 用refresh_token请求新token时,要用axios,不要用实例 http (需要: 手动用 refresh_token 请求)
  3. 得到新token之后,再发请求时,要用 http 实例 (用token请求)
  4. 过期的 token 可以用 refresh_token 再次更新获取新token, 但是过期的 refresh_token 就应该从清除了


作者:JoyZ
来源:juejin.cn/post/7308992811449172005
收起阅读 »

被中文输入法坑死了

web
PM:在PC端做一个@功能吧,就是那种...。 我:你不用解释🤔我知道那个功能,监听keydown事件,然后e.keycode === 50,那可太简单了。 那可太简单了,可太简单了,太简单了,简单了,单了,了......(掉进坑里的回声) 坑1:KeyB...
继续阅读 »

PM:在PC端做一个@功能吧,就是那种...。



我:你不用解释🤔我知道那个功能,监听keydown事件,然后e.keycode === 50,那可太简单了。



那可太简单了,可太简单了,太简单了,简单了,单了,了......(掉进坑里的回声)


坑1:KeyBoardEvent.keycode



废弃的属性你就坚持用吧,一用一个不吱声。以后线上跑得好好的代码突然报错了,你都不知道bug在哪儿。


现在的web标准里,要确定一个键盘事件是靠e.keye.codecode代表触发事件的物理按键,比如2的位置code='Digit2'key返回用户按下的物理按键的值,它还与 shiftKey 等调节性按键的状态、键盘的区域和布局有关。


所以对于@来说,直接判断e.key === "@"来做后续的操作就行了。


addEventListner('keydown', (e) => {
if (e.key === "@") {
e.preventDefalut(); // 这里去掉@字符是为了后续插入和监听方便处理
// 插入有@字符的,可监听输入的元素
// 唤起小窗....
}
});


仔细看上面的这几行代码和注释,要开始考(坑)了。


坑2:输入法的坑


起因


在我美滋滋地以为避过了坑1就没事了的时候,一个夜晚我的测试同学告诉我,在测试环境突然就体验不到这个功能了,无论输入多少个@都不行,白天还好好的🤯。


好一个「白天还好好的」。


我自己测试的时候又百分百能体验到🤔,所以最开始我还在怀疑他没有配上测试系统......


于是,让测试同学的windows电脑连到我的开发环境debug一看:


好家伙,真是好家伙😅他的电脑的e.key === "Process"????!!!


什么意思呢,就是正常我们理想中的@字符产生是shift+2按键的组合,监听keydown之后我们会按顺序收到两个回调:



  1. e.key === "Shift"e.code === "ShiftLeft"或者shiftRight

  2. e.key === "@"e.code === "Digit2"


但是实际在测试同学的电脑里,1是一样的,但是2变了,2变成了e.key === "Process"


虽然键盘事件有变化,但是在前端页面上的@字符是没有任何变化的。难怪他说他会突然失效了。我问他做了什么怎么会突然变了,他想了想说晚上从系统输入法换成了微信输入法.....


上网检索(chatGPT)了一番,明白了一个新的知识点:


输入法的全称叫Input Method Editor输入法编辑器(IME)。本质上它也是个编辑器。为了能输入各类字符(比如汉字和阿拉伯字等),IME会先处理用户的英文字母输入然后通过系统的底层调用传递给浏览器,浏览器再显示到用户的界面。这里的Process很大概率就是当时输入法给出的某个值表示那个时刻它还在处理中。


解决办法


既然KeyBoardEvent靠不住,那我们换一种监听方式。


我找到了一个非常适用于输入法的监听事件叫做CompositionEvent,它表示用户间接输入文本(如使用输入法)时发生的事件。此接口的常用事件有compositionstart, compositionupdatecompositionend。它们三个事件分别对应的动作,通俗一点说就是你用输入法开始打字、正在打字和结束打字。


于是乎,我监听compositionend不就行了!在输入法end的时候我再去看你end的字符是不是@不就行了!


// addEventListner('keydown', (e) => {
addEventListner('compositionend', (e) => {
// if (e.key === "@") {
if (e.data === "@") {
e.preventDefalut(); // 这里去掉@字符是为了后续插入和监听方便处理
// 插入有@字符并且可监听输入的元素
// 唤起小窗....
}
});

对于输入法来说,按键的up和down的key值就算不尽人意也没什么损失,毕竟用户毫无感知。但是,compositionend永远是不会错的,如果compositionende.data都不是@字符了,那么在用户的编辑器界面上的显示肯定也会跟着出错。


所以监听这个肯定就是万无一失的方法了,哈哈哈我真是个“天才”(蠢材)。
修改之后让测试同学尝试之后果然就可以了。


坑3:输入法继续坑


起因


时间过去了没一会,本天才就收到了另一个测试同学反馈的问题说为什么输入了一个@字符之后,会出现两个@在界面上?


我第一反应就是难道没有执行到e.preventDefalut()?既然后续功能能正常使用,没执行到也不应该啊🤔。然后在我电脑一通尝试,发现safari浏览器在输入法为中文的情况下也会触发这个问题。


于是,让测试同学的windows电脑连到我的开发环境debug一看(梅开二度):


执行到了,也没有报错什么的,但是@字符并没有被prevent掉🤯。


再加上我自己传入的@,所以界面上就出现了两个@字符。啊这这这,这很难评......


ceeb653ely8gzozgvjq1cg20j20hhgvb.gif


我是左思右想,百思不得其解,于是只能:



stack overflow上也有这个问题


上面大概的意思就是compositionend事件里使用 e.preventDefault() 在技术上可行的,但它可能不会产生你期望的效果。可能是因为 compositionend 事件标志着一次输入构成(composition)会话的结束,而在这个点上阻止默认行为可能没有意义,因为输入的流程已经完成了。


更推荐用keydown,compositionstartinput来处理这种情况。


keydown是不可能keydown了,已经被坑了。compositionstart也不行,因为刚开始输入那会才按下了shift键,@字符还没出来呢。那就只能input了。


解决办法


最开始我没有选择input就是因为它不能使用e.preventDefault()。我必须要对输入的字符串进行单独处理,去掉@,当时觉得很麻烦就没有选择这个方法。


额....好好好,行行行,现在还是必须得处理一下了。


// addEventListner('keydown', (e) => {
// addEventListner('compositionend', (e) => {
addEventListner('input', (e) => {
// if (e.key === "@") {
if (e.data === "@") {
?怎么去处理字符呢 // 这里去掉@字符是为了后续插入和监听方便处理
// 插入有@字符并且可监听输入的元素
// 唤起小窗....
}
});

对于这个处理字符的方法,也是一个新知识点了。起初我还想的是去处理编辑器里的content,然后再给它插入回去,这样子复杂度很高并且出错的概率极大。


这里的解决办法主要是使用CharacterData接口。CharacterData 接口是操作那些包含字符数据的节点的核心,特别是在需要动态地更改文本内容时。


例如,在一个文本节点上使用 deleteData() 方法可以从文本中移除一部分内容,而不必完全替换或重写整个节点


const selection = window.getSelection();
const { anchorNode, anchorOffset } = selection;
if (anchorNode.substringData(anchorOffset - 1, 1) === '@') {
selection.anchorNode.deleteData(anchorOffset - 1, 1);
}

写完这个之后,我用自己的safari浏览器测试发现果然没有问题了。


哈哈哈我真是个“天才”(蠢材)。


坑4:输入法深坑🕳️


我自信满满地让测试同学再重试一下😎,然后测试同学说:和之前一样啊,还是有两个@字符。


我:啊?啊啊??啊啊啊???


IMG_6547.jpg


于是,让测试同学的windows电脑连到我的开发环境debug一看(梅开三度):


发现测试同学电脑上的anchorOffset和正常的情况下是不一样的,会小一位,所以导致anchorOffset - 1拿到的前一个字符并不等于@,所以后续也没有把它处理掉🤯。


我是左思右想,百思不得其解,stack overflow上也没有相关的问题。不过,结合IME的概念,肯定还是输入法的问题。


结合之前keydown的e.key==="Processing",可能在input触发时输入法的编辑器其实还是没有完成工作(composition),导致在那个时候SelectionanchorOffset不一致。其实浏览器的Selection肯定不会错,那anchorOffset看起来像是错了,我觉得应该是输入法在转换的过程对我们的前端页面做了一些用户看不到的东西,而anchorOffset把它显化出来罢了。


解决办法


于是乎,我尝试性的对处理字符串的那串代码进行延时,目的是为了等待输入法彻底工作完毕。


// addEventListner('keydown', (e) => {
// addEventListner('compositionend', (e) => {
addEventListner('input', (e) => {
// if (e.key === "@") {
if (e.data === "@") {
setTimeout(() => {
const selection = window.getSelection();
const { anchorNode, anchorOffset } = selection;
if (anchorNode.substringData(anchorOffset - 1, 1) === '@') {
selection.anchorNode.deleteData(anchorOffset - 1, 1);
} // 这里去掉@字符是为了后续插入和监听方便处理
});

// 插入有@字符并且可监听输入的元素
// 唤起小窗....
}
});

然后,问题真的就彻底解决了。


这个功能做起来可太简单了......😅


作者:Liqiuyue
来源:juejin.cn/post/7307041255740981286
收起阅读 »

使用flex实现瀑布流

web
什么是瀑布流 瀑布流,又称瀑布流式布局。是比较流行的一种网站页面布局,视觉表现为参差不齐的多栏布局,随着页面滚动条向下滚动,这种布局还会不断加载数据块并附加至当前尾部。 特点: 固定宽度,高度不一 参差不齐的布局 使用flex实现瀑布流 实现的效果是分成两...
继续阅读 »

什么是瀑布流


瀑布流,又称瀑布流式布局。是比较流行的一种网站页面布局,视觉表现为参差不齐的多栏布局,随着页面滚动条向下滚动,这种布局还会不断加载数据块并附加至当前尾部。


特点:



  • 固定宽度,高度不一

  • 参差不齐的布局


使用flex实现瀑布流


实现的效果是分成两列的瀑布流,往下滑动会加载下一页的数据,并渲染到页面中!


微信图片_20230731231617.jpg


样式的实现


<view class="blessing-con">
<view class='blessing-con-half'>
<view id="leftHalf">
<view class="blessing-con-half-item" :class="bgColor[index % 3]"
v-for="(item, index) in newBlessingWordsList1" :key="index">
<view class="item-con">
</view>
</view>
</view>
</view>
<view class='blessing-con-half'>
<view id="rightHalf">
<view class="blessing-con-half-item" :class="bgColor[(index + 1) % 3]"
v-for="(item, index) in newBlessingWordsList2" :key="index">
<view class="item-con"></view>
</view>
</view>
</view>
</view>
<view class="blessing-more" @click="handlerMore">
<image v-if="hasWallNext" class="more-icon"
src="xx/blessingGame/arr-down.png">
</image>
<view class="blessing-more-text">{{ blessingStatus }}</view>
</view>

.blessing-con 定义外层容器为flex布局,同时设置主轴对齐方式为space-between


.blessing-con-half定义左右两侧的容器的样式


.blessing-con-half-item定义每一个小盒子的样式


.blessing-con {
padding: 32rpx 20rpx;
display: flex;
justify-content: space-between;
height: 1100rpx;
overflow-y: auto;
.blessing-con-half {
width: 320rpx;
height: 100%;
box-sizing: border-box;
.blessing-con-half-item {
width: 100%;
height: auto;
display: flex;
flex-direction: column;
box-sizing: border-box;
margin: 0 0 24rpx;
position: relative;
}
}
}

这里每个小盒子的背景色按蓝-黄-红的顺序,同时通过伪类给盒子顶部添加锯齿图片,实现锯齿效果


bgColor: ['blueCol', 'yellowCol', 'pinkCol'], //祝福墙背景

// 不同颜色
.blessing-con-half-item {
&.pinkCol {
&::before {
.setbg(320rpx, 16rpx, 'blessingGame/pink-bg.png');
}
.item-con {
background: #FFE7DF;
}
}

&.yellowCol {
&::before {
.setbg(320rpx, 16rpx, 'blessingGame/orange-bg.png');
}
.item-con {
background: #fff0e0;
}
}

&.blueCol {
&::before {
.setbg(320rpx, 16rpx, 'blessingGame/blue-bg.png');
}
.item-con {
background: #e0f7ff;
}
}
}
}

功能实现


在data中定义两个数组存储左右列表的数据


data(){
return{
blessingWordsList: [],// 祝福墙数据
newBlessingWordsList: [],//已添加的数据
newBlessingWordsList1: [],//左列表
newBlessingWordsList2: [],//右列表
isloading:false,//是否正在加载
hasWallNext:false,//是否有下一页
leftHeight: 0,//左高度
rightHeight: 0,//右高度
blessingWordsCount: 0,//计数器
isActive: 0, //tab初始化索引
timer:null,//定时器
}
}

调取接口请求列表数据



  • 第一次请求数据需要初始化列表数据和计数器

  • 每一次请求完都需要开启定时器


// 获取祝福墙列表(type=1则请求下一页)
async getBlessingWall(type = 0) {
try {
let res = await api.blessingWall({
activityId: this.activityId,
pageNum: this.pageWallNum,
pageSize: this.pageWallSize
})
this.isloading = false
if (res.code == 1 && res.rows) {
let list = res.rows
this.blessingWordsList = (type==0 ? list : [...this.blessingWordsList, ...list])
if (this.blessingWordsList.length > 0 && !this.timer && this.isActive == 1) {
if(this.pageWallNum == 1){
this.newBlessingWordsList = []
this.newBlessingWordsList1 = []
this.newBlessingWordsList2 = []
this.blessingWordsCount = 0
}
this.start()
}
// 处理请求下一页的情况
if (type == 1) {
this.start()
}
this.hasWallNext = res.hasNext
if (!this.hasWallNext) {
this.blessingStatus = "没有更多了哟"
} else {
this.blessingStatus = "点击加载更多"
}
}
} catch (error) {
console.log(error)
}
},
// 加载更多
async handlerMore() {
if (this.hasWallNext && !this.isloading) {
this.isloading = true
this.pageWallNum++
await this.getBlessingWall(1)
}
},

开启一个定时器,用于动态添加左右列表的数据


start() {
// 清除定时器
clearInterval(this.timer)
this.timer = null;

this.timer = setInterval(() => {
let len = this.blessingWordsList.length
if (this.blessingWordsCount < len) {
let isHave = false
// 在列表中获取一个元素
let item =this.blessingWordsList[this.blessingWordsCount]
// 判断新列表中是否已经存在相同元素,防止重复添加
this.newBlessingWordsList.forEach((tmp)=>{
if(tmp.id == item.id){
isHave = true
}
})
// 如果不存在
if (!isHave) {
this.newBlessingWordsList.push(item)//添加该元素
this.$nextTick(() => {
this.getHei(item)//添加元素到左右列表
})
}
} else {
// 遍历完列表中的数据,则清除定时器
clearInterval(this.timer)
this.timer = null;
}
}, 10)
}

计算当前左右容器的高度,判断数据要添加到哪一边



  • 使用uni-app的方法获取左右容器的dom对象,再获取他们当前的高度

  • 比较左右高度,向两个数组动态插入数据

  • 每插入一条数据,计数器+1


getHei(item) {
const query = uni.createSelectorQuery().in(this)
// 左边
query.select('#leftHalf').boundingClientRect(res => {
if (res) {
this.leftHeight = res.height
}
// 右边
const query1 = uni.createSelectorQuery().in(this)
query1.select('#rightHalf').boundingClientRect(dataRight => {
if (dataRight) {
this.rightHeight = dataRight.height != 0 ? dataRight.height : 0
if (this.leftHeight == this.rightHeight || this.leftHeight < this.rightHeight) {
// 相等 || 左边小
this.newBlessingWordsList1.push(item)
} else {
// 右边小
this.newBlessingWordsList2.push(item)
}
}
this.blessingWordsCount++
}).exec()
}).exec()
},

这里有一个注意点,调用start方法的时候,必须确保页面渲染了左右容器的元素,否则会拿不到容器的高度


比如我这个项目是有tab切换的!


微信图片_20230731231616.jpg
进入页面的时候会请求一次数据,这时候因为tab初始状态在0,所以并不会调用start方法,要到切换tab到1时,才会调用start方法开始计算高度。


data(){
return{
isActive: 0, //tab初始化索引
timer:null,//定时器
}
}
async onLoad(options) {
this.getBlessingWall()
}
// tab选项卡切换
tabClick(index) {
this.isActive = index
this.isLoaded = false;
if (this.blessingWordsList.length > 0 && !this.timer && this.isActive == 1) {
if(this.pageWallNum == 1){
this.newBlessingWordsList = []
this.newBlessingWordsList1 = []
this.newBlessingWordsList2 = []
this.blessingWordsCount = 0
}
this.start()
}
},

最后


这次选用了flex实现瀑布流,实现瀑布流的方式还有其他几种方法,后续有机会的话,我会补充其他几种方式,如果感兴趣的话,可以点点关注哦!


作者:藤原豆腐店
来源:juejin.cn/post/7260713996165021754
收起阅读 »

前端同事最讨厌的后端行为,看看你中了没有

web
前端同事最讨厌的后端行为,看看你中了没有 听说这是前端程序员最讨厌的后端行为,不知道你有没有碰到过,或者你的前端同事虽然没跟你说过,但是你已经被偷偷吐槽了。 前端吐槽:后端从不自测接口,等到前后端联调时,这个接口获取不到,那个接口提交不了,把前端当成自己...
继续阅读 »

前端同事最讨厌的后端行为,看看你中了没有



听说这是前端程序员最讨厌的后端行为,不知道你有没有碰到过,或者你的前端同事虽然没跟你说过,但是你已经被偷偷吐槽了。




前端吐槽:后端从不自测接口,等到前后端联调时,这个接口获取不到,那个接口提交不了,把前端当成自己的接口测试员,耽误前端的开发进度。



听到这个吐槽,仿佛看到曾经羞愧的自己。这个毛病以前我也有啊,有些接口,尤其是大表单提交接口,表单特别大,字段很多,有时候就偷懒了,直接编译过了,就发到测试环境了。前端同时联调的时候一调接口,异常了。


好在后来改了,毕竟让人发现自己接口写的有问题,也是一件丢脸的事儿。


但是我还真见过后端的同学,写完接口一个都不测,直接发测试环境的。


我就碰到过厉害的,编译都不过,就直接提代码。以前,有个新来的同事,分了任务就默默的干着,啥也不问,然后他做的功能测试就各种发现问题。说过之后,就改一下,但是基本上还是不测试,本想再给他机会的,所以后来他每次提代码,我都review一下。直到有一天,我发现忍不了了,他把一段全局配置给注释了,然后把代码提了,我过去问他是不是本地调试,忘了取消注释了。他的回答直接让我震惊了,他说:不是的,是因为不注释那段代码,我本地跑步起来,所以肯定是那段代码有问题,所以就注释了。


然后,当晚,他就离职了。


解决方式


对于这种大表单类似的问题,应该怎么处理呢?


好像没有别的方法,只能克服自己的懒惰,为自己写的代码负责。就想着,万一接口有问题,别人可能会怀疑你水平不行,你水平不行,就是你不行啊,程序员怎么能不行呢。


你可以找那么在线 Java BeanJSON的功能,直接帮你生成请求参数,或者现在更可以借助 ChatGPT ,帮你生成请求参数,而且生成的参数可能比你自己瞎填的看上去更合理。


或者,如果是小团队,不拘一格的话,可以让前端的同事把代码提了,你本地跑着自测一下,让前端同事先做别的功能,穿插进行也可以。



前端吐槽:后端修改了字段或返回结构不通知前端



这个就有点不讲武德了。


正常情况下,返回结构和字段都是事先约定好的,一般都是先写接口,做一些 Mock 数据,然后再实现真实的逻辑。


除了约定好返回字段和结构外,还包括接口地址、请求方法、头信息等等,而且一个项目都会有项目接口规范,同一类接口的返回字段可能有很多相同的部分。


后端如果改接口,必须要及时通知前端,这其实应该是正常的开发流程。后端改了接口,不告诉前端,到时候测试出问题了,一般都会先找前端,这不相当于让前端背锅了吗,确实不地道啊。


后端的同学们,谨记啊。



前端吐槽:为了获取一个信息,要先调用好几个接口,可能参数还是相同的



假设在一个详情页面,以前端的角度就是,我获取详情信息,就调用详情接口好了,为什么调用详情接口之前,要调用3、4个其他的接口,你详情里需要啥参数,我直接给你传过去不就好了吗。


在后端看来可能是这样的,我这几个接口之前就写好了,前端拿过去就能用,只不过就是多调几次罢了,没什么大不了的吧。


有些时候,可能确实是必须这么做的,比如页面内容太多,有的部分查询逻辑复杂,比较耗时,这时候需要异步加载,这样搞确实比较好。


但是更多时候其实就是后端犯懒了,不想再写个新接口。除了涉及到性能的问题,大多数逻辑都应该在后端处理,能用一个接口处理完,就不应该让前端多调用第二个接口。


有前端的朋友曾经问过我,他说,他们现在做的系统中有些接口是根据用户身份来展示数据的,但是前端调用登录接口登录系统后,在调用其他接口的时候,除了在 Header 中加入 token 外,还有传很多关于用户信息的很多参数,这样做是不是不合理的。


这肯定不合理,token 本来就是根据用户身份产生的,后端拿到 token 就能获取用户信息,这是常识问题,让前端在接口中再传一遍,既不合理也不安全。


类似的问题还有,比如后端接口返回一堆数据,然后有的部分有用、有的部分没有,有的部分还涉及到逻辑,不借助文档根本就看不明白怎么用,这其实并不合理。


接口应该尽量只包含有用的部分,并且尽可能结构清晰,配合简单的字段说明就能让人明白是怎么回事,是最好的效果。


如果前后端都感觉形势不对了,后端一个接口处理性能跟不上了,前端处理又太麻烦了。这时候就要向上看了,产品设计上可能需要改一改了。


后端的同学可以学一点前端,前端的同学也可以学一点后端,当你都懂一些的时候,就能两方面考虑了,这样做出来的东西可能会更好用一点。总之,前后端相互理解,毕竟都是为了生活嘛。


作者:古时的风筝
来源:juejin.cn/post/7254927062425829413
收起阅读 »

面试官:你能说说常见的前端加密方法吗?

web
前言 本篇文章略微介绍一下前端中常见的加密算法。前端中常见的加密算法主要形式包括——哈希函数,对称加密和非对称加密算法。 一、哈希函数 定义:哈希也叫散列,是指将任意长度的消息映射为固定长度的输出的算法,该输出一般叫做散列值或者哈希值,也叫做摘要(Dige...
继续阅读 »

前言


本篇文章略微介绍一下前端中常见的加密算法。前端中常见的加密算法主要形式包括——哈希函数,对称加密和非对称加密算法。


一、哈希函数


image.png



  • 定义:哈希也叫散列,是指将任意长度的消息映射为固定长度的输出的算法,该输出一般叫做散列值或者哈希值,也叫做摘要(Digest)。简单来说,这种映射就是一种数据压缩,而且散列是不可逆的,也就是无法通过输出还原输入。

  • 特点:不可逆性(单向性)、抗碰撞性(消息不同其散列值也不同)、长度固定

  • 常见应用场景:由于不可逆性,常用于密码存储、数字签名、电子邮件验证、验证下载等方面,更多的是用用在验证数据的完整性方面。



    • 密码存储:明文保存密码是危险的。通常我们把密码哈希加密之后保存,这样即使泄漏了密码,因为是散列后的值,也没有办法推导出密码明文(字典攻击难以破解)。验证的时候,只需要对密码(明文)做同样的散列,对比散列后的输出和保存的密码散列值,就可以验证同一性。

    • 可用于验证下载文件的完整性以及防篡改:比如网站提供安装包的时候,通常也同时提供md5值,这样用户下载之后,可以重算安装包的md5值,如果一致,则证明下载到本地的安装包跟网站提供的安装包是一致的,网络传输过程中没有出错。



  • 优势:不可逆,速度快、存储体积小,可以帮助保护数据的完整性和减轻篡改风险。

  • 缺点:安全性不高、容易受到暴力破解


image.png


常见类型:SHA-512、SHA-256、MD5(MD5生成的散列码是128位)等。



  • MD5(Message Digest Algorithm 5) :是RSA数据安全公司开发的一种单向散列算法,非可逆,相同的明文产生相同的密文。

  • SHA(Secure Hash Algorithm) :可以对任意长度的数据运算生成一个固定位数的数值。

  • SHA/MD5对比:SHA在安全性方面优于MD5,并且可以选择多种不同的密钥长度。 但是,由于内存需求更高,运行速度可能会更慢。 不过,MD5因其速度而得到广泛使用,但是由于存在碰撞攻击风险,因此不再推荐使用。


二、对称加密



  • 定义:指加密和解密使用同一种密钥的算法。


image.png



  • 特点:优点是速度快,通信效率高;缺点是安全性相对较低。信息传输使用一对一,需要共享相同的密码,密码的安全是保证信息安全的基础,服务器和N个客户端通信,需要维持N个密码记录且不能修改密码。

  • 优势:效率高,算法简单,系统开销小,速度快,适合大数量级的加解密,安全性中等

  • 缺点:秘钥管理比较难,密钥存在泄漏风险。

  • 常见应用场景:适用于需要高速加密/解密的场景,例如 HTTP 传输的 SSL/TLS 部分,适用于加密大量数据,如文件加密、网络通信加密、数据加密、电子邮件、Web 聊天等。



    • 文件加密:将文件用相同的密钥加密后传输或存储,只有拥有密钥的用户才能解密文件。

    • 数据库加密:对数据库中的敏感信息进行加密保护,防止未经授权的人员访问。

    • 通信加密:将网络数据通过对称加密算法进行加密,确保数据传输的机密性,比较适合大量短消息的加密和解密。

    • 个人硬盘加密:对称加密可以为硬盘加密提供较好的安全性和高处理速度,这对个人电脑而言可能是一个不错的选择。



  • 常见类型DES,3DES,AES 等:



    • DES(Data Encryption Standard):分组式加密算法,以64位为分组对数据加密,加解密使用同一个算法,速度较快,适用于加密大量数据的场合。

    • 3DES(Triple DES):三重数据加密算法,是基于DES,对每个数据块应用三次DES加密算法,强度更高。

    • AES(Advanced Encryption Standard):高级加密标准算法,速度快,安全级别高,目前已被广泛应用,适用于加密大量数据,如文件加密、网络通信加密等。




AES与DES区别

AES与DES之间的主要区别在于加密过程。在DES中,将明文分为两半,然后再进行进一步处理;而在AES中,整个块不进行除法,整个块一起处理以生成密文。相对而言,AES比DES快得多,与DES相比,AES能够在几秒钟内加密大型文件。



  • DES



    • 优点:DES算法具有极高安全性,到目前为止,除了用穷举搜索法对DES算法进行攻击外,还没有发现更有效的办法。

    • 缺点:分组比较短、密钥太短、密码生命周期短、运算速度较慢。



  • AES



    • 优点:运算速度快,对内存的需求非常低,适合于受限环境。分组长度和密钥长度设计灵活, AES标准支持可变分组长度;具有很好的抵抗差分密码分析及线性密码分析的能力。

    • 缺点:目前尚未存在对AES 算法完整版的成功攻击,但已经提出对其简化算法的攻击。




三、非对称加密


-定义:指加密和解密使用不同密钥的算法,通常情况下使用公共密钥进行加密,而私有密钥用于解密数据。公钥和私钥是成对存在,公钥是从私钥中提取产生公开给所有人的,如果使用公钥对数据进行加密,那么只有对应的私钥(不能公开)才能解密,反之亦然。


image.png



  • 特点:缺点是加密解密速度较慢,通信效率较低,优点是安全性高,需要两个不同密钥,信息一对多。因为它使用的是不同的密钥,所以需要耗费更多的计算资源。服务器只需要维持一个私钥就可以和多个客户端进行通信,但服务器发出的信息能够被所有的客户端解密,且该算法的计算复杂,加密的速度慢。

  • 优势:秘钥容易管理,不存在密钥的交换问题,安全性好,主要用在数字签名,更适用于区块链技术的点对点之间交易的安全性与可信性。

  • 缺点:加解密的计算量大,比对称加密算法计算复杂,性能消耗高,速度慢,适合小数据量或数据签名

  • 常见应用场景:在实际应用中,非对称加密通常用于需要确保数据完整性和安全性的场合,例如数字证书的颁发、SSL/TLS 协议的加密、数字签名、加密小文件、密钥交换、实现安全的远程通信等。



    • 数字签名:数字签名是为了保证数据的真实性和完整性,通常使用非对称加密实现。发送方使用自己的私钥对数据进行签名,接收方使用发送方的公钥对签名进行验证,如果验证通过,则可以确认数据的来源和完整性。常见的数字签名算法都基于非对称加密,如RSA、DSA等。

    • ** 身份认证**:Web浏览器和服务器使用SSL/TLS技术来进行安全通信,其中就使用了非对称加密技术。Web浏览器在与服务器建立连接时,会对服务器进行身份验证并请求其证书。服务器将其证书发送给浏览器,证书包含服务器的公钥。浏览器使用该公钥来加密随机生成的“对话密钥”,然后将其发送回服务器。服务器使用自己的私钥解密此“对话密钥”,以确保双方之间的会话是安全的。

    • 安全电子邮件:非对称加密可用于电子邮件中,确保邮件内容只能由预期的收件人看到。发件人使用收件人的公钥对邮件进行加密,收件人使用自己的私钥对其进行解密。这确保了只有目标收件人才能读取邮件。



  • 常见类型RSA,DSA,DSS,ECC 等



    • RSA:由 RSA 公司发明,是一个支持变长密钥的公共密钥算法,需要加密的文件块的长度也是可变的。RSA 是一种非对称加密算法,即加密和解密使用一对不同的密钥,分别称为公钥和私钥。公钥用于加密数据,私钥用于解密数据。RSA 算法的安全性基于大数分解问题,密钥长度通常选择 1024 位、2048 位或更长。RSA 算法用于保护数据的机密性、确保数据的完整性和实现数字签名等功能。

    • DSA(Digital Signature Algorithm) :数字签名算法,仅能用于签名,不能用于加解密。

    • ECC(Elliptic Curves Cryptography) :椭圆曲线密码编码学。

    • DSS:数字签名标准,可用于签名,也可以用于加解密。




总结


前端使用非对称加密原理很简单,平时用的比较多的也是非对称加密,前后端共用一套加密解密算法,前端使用公钥对数据加密,后端使用私钥将数据解密为明文。中间攻击人拿到密文,如果没有私钥的话是没办法破解的。


欢迎大佬继续评论区补充


作者:优秀稳妥的Zn
来源:juejin.cn/post/7280057907055919144
收起阅读 »

你的网站如何接入QQ,微信登录

web
主要实现步骤 对接第三方平台,获取第三方平台的用户信息。 利用该用户信息,完成本应用的注册。 qq登录接入 接入前的配置 qq互联 登录后,点击头像,进行开发者信息填写,等待审核。 邮箱验证后,等待审核。 审核通过后,然后就可以创建应用了。 然后填写...
继续阅读 »

主要实现步骤



  • 对接第三方平台,获取第三方平台的用户信息。

  • 利用该用户信息,完成本应用的注册。


qq登录接入


接入前的配置


qq互联


登录后,点击头像,进行开发者信息填写,等待审核。


image.png


邮箱验证后,等待审核。


image.png


审核通过后,然后就可以创建应用了。


image.png


然后填写一些网站信息,等待审核。审核通过后,即可使用。


开始接入



  1. 导入qq登录的sdk



<script type="text/javascript" charset="utf-8" src="https://connect.qq.com/qc_jssdk.js" data-appid="您应用的appid"
data-redirecturi="qq扫码后的回调地址(上面配置中可以查到)">
script>


  1. 点击qq登录,弹出扫码窗口。


// QQ 登录的 URL
const QQ_LOGIN_URL =
'https://graph.qq.com/oauth2.0/authorize?client_id=您的appid&response_type=token&scope=all&redirect_uri=您的扫码后的回调地址'
window.open(
QQ_LOGIN_URL,
'oauth2Login_10609',
'height=525,width=585, toolbar=no, menubar=no, scrollbars=no, status=no, location=yes, resizable=yes'
)


  1. 挂起qq登录。需要注意的是,扫码登录成功后,调试代码需要在线上环境。


"qqLoginBtn" v-show="false">

// QQ 登录挂起
onMounted(() => {
QC.Login(
{
btnId: 'qqLoginBtn' //插入按钮的节点id
},
// 登录成功之后的回调,但是需要注意,这个回调只会在《登录回调页面中被执行》
// 登录存在缓存,登录成功一次之后,下次进入会自动重新登录(即:触发该方法,所以我们应该在离开登录页面时,注销登录)
// data就是当前qq的详细信息
(data, opts) => {
console.log('QQ登录成功')
// 1. 注销登录,否则在后续登录中会直接触发该回调
QC.Login.signOut()
// 2. 获取当前用户唯一标识,作为判断用户是否已注册的依据。(来决定是否跳转到注册页面)
const accessToken = /access_token=((.*))&expires_in/.exec(
window.location.hash
)[1]
// 3. 拼接请求对象
const oauthObj = {
nickname: data.nickname,
figureurl_qq_2: data.figureurl_qq_2,
accessToken
}
// 4. 完成跨页面传输 (需要将数据传递到项目页面,而非qq登录弹框页面中进行操作)
brodacast.send(oauthObj)

// 针对于 移动端而言:通过移动端触发 QQ 登录会展示三个页面,原页面、QQ 吊起页面、回调页面。并且移动端一个页面展示整屏内容,且无法直接通过 window.close() 关闭,所以在移动端中,我们需要在当前页面继续进行后续操作。
oauthLogin(LOGIN_TYPE_QQ, oauthObj)
// 5. 在 PC 端下,关闭第三方窗口
window.close()
}
)
})


  1. 跨页面窗口通信


想要实现跨页面信息传输,通常有两种方式:



  • BroadcastChannel:允许 同源 的不同浏览器窗口,Tab页,frame或者 iframe 下的不同文档之间相互通信。但是会存在兼容性问题。

  • localStorage + window.onstorage:通过localStorage 进行 同源 的数据传输。用来处理 BroadcastChannel 不兼容的浏览器。以前写过一篇文章


// brodacast.js
// 频道名
const LOGIN_SUCCESS_CHANNEL = 'LOGIN_SUCCESS_CHANNEL'

// safari@15.3 不支持 BroadcastChannel,所以我们需要对其进行判定使用,在不支持 BroadcastChannel 的浏览器中,使用 localstorage
let broadcastChannel = null
if (window.BroadcastChannel) {
broadcastChannel = new BroadcastChannel(LOGIN_SUCCESS_CHANNEL)
}

/**
* 等待 QQ 登录成功
* 因为 QQ 登录会在一个新的窗口中进行,用户扫码登录成功之后会回调《新窗口的 QC.Login 第二参数 cb》,而不会回调到原页面。
* 所以我们需要在《新窗口中通知到原页面》,所以就需要涉及到 JS 的跨页面通讯,而跨页面通讯指的主要就是《同源页面的通讯》
* 同源页面的通讯方式有很多,我们这里主要介绍:
* 1. BroadcastChannel ->
https://developer.mozilla.org/zh-CN/docs/Web/API/BroadcastChannel

* 2. window.onstorage:注意:该事件不在导致数据变化的当前页面触发
*/

/**
* 等待回调,它将返回一个 promise,并携带对应的数据
*/

const wait = () => {
return new Promise((resolve, reject) => {
if (broadcastChannel) {
// 触发 message 事件时的回调函数
broadcastChannel.onmessage = async (event) => {
// 改变 promise 状态
resolve(event.data)
}
} else {
// 触发 localStorage 的 setItem 事件时回调函数
window.onstorage = (e) => {
// 判断当前的事件名
if (e.key === LOGIN_SUCCESS_CHANNEL) {
// 改变 promise 状态
resolve(JSON.parse(e.newValue))
}
}
}
})
}

/**
* 发送消息。
* broadcastChannel:触发 message
* localStorage:触发 setItem
*/

const send = (data) => {
if (broadcastChannel) {
broadcastChannel.postMessage(data)
} else {
localStorage.setItem(LOGIN_SUCCESS_CHANNEL, JSON.stringify(data))
}
}

/**
* 清除
*/

const clear = () => {
if (broadcastChannel) {
broadcastChannel.close()
broadcastChannel = null
}
localStorage.removeItem(LOGIN_SUCCESS_CHANNEL)
}

export default {
wait,
send,
clear
}


  1. 拿到数据后,进行登录(自己服务器登录接口)操作。



  • 传入对应参数(loginType, accessToken)等参数进行用户注册判断。

  • 通过accessToken判断用户已经注册,那么我们就直接在后台查出用户名和密码直接登录了。

  • 通过accessToken判断用户未注册,那么我们将跳转到注册页面,让其注册。


 // 打开视窗之后开始等待
brodacast.wait().then(async (oauthObj) => {
// 登录成功,关闭通知
brodacast.clear()
// TODO: 执行登录操作
oauthLogin("QQ", oauthObj)
})

// oauthLogin.js
import store from '@/store'
import router from '@/router'
import { message } from '@/libs'
import { LOGIN_TYPE_OAUTH_NO_REGISTER_CODE } from '@/constants'

/**
* 第三方登录统一处理方法
*
@param {*} oauthType 登录方式
*
@param {*} oauthData 第三方数据
*/

export const oauthLogin = async (oauthType, oauthData) => {
const code = await store.dispatch('user/login', {
loginType: oauthType,
...oauthData
})
// 返回 204 表示当前用户未注册,此时给用户一个提示,走注册页面
if (code === LOGIN_TYPE_OAUTH_NO_REGISTER_CODE) {
message('success', `欢迎您 ${oauthData.nickname},请创建您的账号`, 6000)
// 进入注册页面,同时携带当前的第三方数据和注册标记
router.push({
path: '/register',
query: {
reqType: oauthType,
...oauthData
}
})
return
}

// 否则表示用户已注册,直接进入首页
router.push('/')
}

微信扫码登录接入


微信开放平台


登录后,进行对应的应用注册,填写一大堆详细信息,然后进行交钱,就可以使用微信登录了。


image.png


开始接入


整个微信登录流程与QQ登录流程略有不同,分为以下几步:


1.通过 微信登录前置数据获取 接口,获取登录数据(比如 APP ID)。就是后台将一些敏感数据通过接口返回。


2.根据获取到的数据,拼接得到 open url 地址。打开该地址,展示微信登录二维码。移动端微信扫码确定登录。


// 2. 根据获取到的数据,拼接得到 `open url` 地址
window.open(
`https://open.weixin.qq.com/connect/qrconnect?appid=${appId}&redirect_uri=${redirectUri}&response_type=code&scope=${scope}&state=${state}#wechat_redirect`,
'',
'height=525,width=585, toolbar=no, menubar=no, scrollbars=no, status=no, location=yes, resizable=yes'
)

3.等待用户扫码后,从当前窗口中解析 window.location.search 得到用户的 code数据。 微信扫码后,会重定向到登录页面。


/**
* 微信登录成功之后的窗口数据解析
*/

if (window.location.search) {
const code = /code=((.*))&state/.exec(window.location.search)[1]
if (code) {
brodacast.send({
code
})
// 关闭回调网页
window.close()
}
}

4.根据 appId、appSecret、code 通过接口获取用户的 access_token


5.根据 access_token 获取用户信息


6.通过用户信息触发 oauthLogin 方法。


调用的接口,都是后端通过微信提供的api来获取到对应的数据,然后再通过接口返回给开发者。  以前也写过微信登录文章


// 等待扫码登录成功通知
brodacast.wait().then(async ({ code }) => {
console.log('微信扫码登录成功')
console.log(code)
// 微信登录成功,关闭通知
brodacast.clear()
// 获取 AccessToken 和 openid
const { access_token, openid } = await getWXLoginToken(
appId,
appSecret,
code
)
// 获取登录用户信息
const { nickname, headimgurl } = await getWXLoginUserInfo(
access_token,
openid
)
console.log(nickname, headimgurl)
// 执行登录操作
oauthLogin(LOGIN_TYPE_WX, {
openid,
nickname,
headimgurl
})
})

需要注意的是,在手机端,普通h5页面是不能使用微信扫码登录的。


总结


相同点



  • 接入前需要配置一些内容信息。

  • 都需要在线上环境进行调试。

  • 都是扫码后在三方窗口中获取对应的信息,发送到当前项目页面进行请求,判断用户是否已经注册,还是未注册。已经注册时,调用login接口时,password直接传递空字符串即可,后端可以通过唯一标识,获取到对应的用户名和密码,直接返回token进行登录。未注册,就跳转到注册页面,让其注册。


不同点



  • qq接入需要导入qc_sdk。

  • qq直接扫码后即可获取到用户信息,就可以直接调用login接口进行判断用户是否注册了。

  • 微信扫码后,获取code来换取access_token, openid,然后再通过access_token, openid来换取用户信息。然后再调用login接口进行判断用户是否注册了。


作者:Spirited_Away
来源:juejin.cn/post/7311343161363234866
收起阅读 »