注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

当遇到需要在Activity间传递大量的数据怎么办?

在Activity间传递的数据一般比较简单,但是有时候实际开发中也会传一些比较复杂的数据,尤其是面试问道当遇到需要在Activity间传递大量的数据怎么办? Intent 传递数据的大小是有限制的,它大概能传的数据是1M-8K,原因是Binder锁映射的内存大...
继续阅读 »

在Activity间传递的数据一般比较简单,但是有时候实际开发中也会传一些比较复杂的数据,尤其是面试问道当遇到需要在Activity间传递大量的数据怎么办?


Intent 传递数据的大小是有限制的,它大概能传的数据是1M-8K,原因是Binder锁映射的内存大小就是1M-8K.一般activity间传递数据会要使用到binder,因此这个就成为了数据传递的大小的限制。那么当activity间要传递大数据采用什么方式呢?其实方式很多,我们就举几个例子给大家说明一下,但是无非就是使用数据持久化,或者内存共享方案。一般大数据的存储不适宜使用SP, MMKV,DataStore。


Activity之间传递大量数据主要有如下几种方式实现:
  • LruCache
  • 持久化(sqlite、file等)
  • 匿名共享内存

使用LruCache

LruCache是一种缓存策略,可以帮助我们管理缓存,想具体了解的同学可以去Glide章节中具体先了解下。在当前的问题下,我们可以利用LruCache存储我们数据作为一个中转,好比我们需要Activity A向Activity B传递大量数据,我们可以Activity A先向LruCache先写入数据,之后Activity B从LruCache读取。


首先我们定义好写入读出规则:

public interface IOHandler {
   //保存数据
   void put(String key, String value);
   void put(String key, int value);
   void put(String key, double value);
   void put(String key, float value);
   void put(String key, boolean value);
   void put(String key, Object value);

   //读取数据
   String getString(String key);
   double getDouble(String key);
   boolean getBoolean(String key);
   float getFloat(String key);
   int getInt(String key);
   Object getObject(String key);
}

我们可以根据规则也就是接口,写出具体的实现类。实现类中我们保存数据使用到LruCache,这里面我们一定要设置一个大小,因为内存中数据的最大值是确定,我们保存数据的大小最好不要超过最大值的1/8.

LruCache<String, Object> mCache = new LruCache<>( 10 * 1024*1024);

写入数据我们使用比较简单:

@Override
public void put(String key, String value) {
   mCache.put(key, value);
}

好比上面写入String类型的数据,只需要接收到的数据全部put到mCache中去。


读取数据也是比较简单方便:

@Override
public String getString(String key) {
   return String.valueOf(mCache.get(key));
}

持久化数据

那就是sqlite、file等方式。将需要传递的数据写在临时文件或者数据库中,再跳转到另外一个组件的时候再去读取这些数据信息,这种处理方式会由于读写文件较为耗时导致程序运行效率较低。这种方式特点如下:


优势:


(1)应用中全部地方均可以访问


(2)即便应用被强杀也不是问题了


缺点:


(1)操做麻烦


(2)效率低下


匿名共享内存

在跨进程传递大数据的时候,我们一般会采用binder传递数据,但是Binder只能传递1M一下的数据,所以我们需要采用其他方式完成数据的传递,这个方式就是匿名共享内存。


Anonymous Shared Memory 匿名共享内存」是 Android 特有的内存共享机制,它可以将指定的物理内存分别映射到各个进程自己的虚拟地址空间中,从而便捷的实现进程间内存共享。


Android 上层提供了一些内存共享工具类,就是基于 Ashmem 来实现的,比如 MemoryFile、 SharedMemory。



今日分享到此结束,对你有帮助的话,点个赞再走呗,每日一个面试小技巧




关注公众号:Android老皮

解锁  《Android十大板块文档》 ,让学习更贴近未来实战。已形成PDF版



内容如下



1.Android车载应用开发系统学习指南(附项目实战)

2.Android Framework学习指南,助力成为系统级开发高手

3.2023最新Android中高级面试题汇总+解析,告别零offer

4.企业级Android音视频开发学习路线+项目实战(附源码)

5.Android Jetpack从入门到精通,构建高质量UI界面

6.Flutter技术解析与实战,跨平台首要之选

7.Kotlin从入门到实战,全方面提升架构基础

8.高级Android插件化与组件化(含实战教程和源码)

9.Android 性能优化实战+360°全方面性能调优

10.Android零基础入门到精通,高手进阶之路


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

三分钟教会你微信炸一炸,满屏粑粑也太可爱了!

相信这个特效你和你的朋友(或对象)一定玩过 当你发送一个便便的表情,对方如果扔一个炸弹表情,就会立刻将这个便便炸开,实现满屏粑粑的“酷炫”画面。可谓是“臭味十足”,隔着屏幕都能感受到来自微信爸爸的满满恶意。 不清楚大家对这个互动设计怎么看,反正一恩当时是喜欢的...
继续阅读 »

相信这个特效你和你的朋友(或对象)一定玩过


当你发送一个便便的表情,对方如果扔一个炸弹表情,就会立刻将这个便便炸开,实现满屏粑粑的“酷炫”画面。可谓是“臭味十足”,隔着屏幕都能感受到来自微信爸爸的满满恶意。


不清楚大家对这个互动设计怎么看,反正一恩当时是喜欢的不行,拉着朋友们就开始“炸”得不亦乐乎。


同样被虏获芳心的设计小哥哥在玩到尽兴后,突然灵感大发,连夜绘制出了设计稿,第二天就拉上产品和研发开始脑暴。


“微信炸💩打动我的一点是他满屏的设计,能够将用户强烈的情绪抒发出来;同时他能够与上一条表情进行捆绑,加强双方的互动性。”设计小哥哥声情并茂道。


“所以,让我们的表情也‘互动’起来吧!”


这不,需求文档就来了:



改掉常见的emoji表情发送方式,替换为动态表情交互方式。即,

当用户发送或接收互动表情,表情会在屏幕上随机分布,展示一段时间后会消失。

用户可以频繁点击并不停发送表情,因此屏幕上的表情是可以非常多且重叠的,能够将用户感情强烈抒发。


(暂用微信的聊天界面进行解释说明,图1为原样式,图2是需求样式)



这需求一出,动态表情在屏幕上的分布方案便引起了研发内部热烈讨论:当用户点击表情时,到底应该将表情放置在屏幕哪个位置比较好呢?


最直接的做法就是完全随机方式:取0到屏幕宽度和高度中随机值,放置表情贴纸。但这么做的不确定因素太多,比如存在一定几率所有表情都集中在一个区域,布局边缘化以及最差的重叠问题。因此简单的随机算法对于用户的体验是无法接受的。




我们开始探索新的方案:

因为目前点的选择依赖于较多元素,比如与屏幕已有点的间距,与中心点距离以及屏幕已有点的数目。因此最终决定采用点权随机的方案,根据上述元素决策出屏幕上可用点的优度,选取优度最高的插入表情。


基本思路


维护对应屏幕像素的二维数组,数组元素代指新增图形时,图形中心取该点的优度。

采用懒加载的方式,即每次每次新增图形后,仅记录现有方块的位置,当需要一个点的优度时再计算。


遍历所有方块的位置,将图形内部的点优度全部减去 A ,将图形外部的点按到图形的曼哈顿距离从 0 到 max (W,H),映射,减 0 到 A * K2。

每次决策插入位置时,随机取 K + n * K1 个点,取这些点中优度最高的点为插入中心

A, K, K1, K2 四个常数可调整



一次选择的复杂度是 n * randT,n 是场上方块数, randT 是本次决策需要随机取多少个点。 从效率和 badcase来说,这个方案目前最优。





代码展示


```cpp
#include <iostream>
#include <vector>
using namespace std;

const int screenW = 600;
const int screenH = 800;

const int kInnerCost = 1e5;
const double kOuterCof = .1;

const int kOutterCost = kInnerCost * kOuterCof;

class square
int x1;
int x2;
int y1;
int y2;
};

int lineDist(int x, int y, int p){
if (p < x) {
return x - p;
} else if (p > y) {
return p - y;
} else {
return 0;
}
}

int getVal(const square &elm, int px, int py){
int dx = lineDist(elm.x1, elm.x2, px);
int dy = lineDist(elm.y1, elm.y2, py);
int dist = dx + dy;
constexpr int maxDist = screenW + screenH;
return dist ? ( (maxDist - dist) * kOutterCost / maxDist ) : kInnerCost;
}

int getVal(const vector<square> &elmArr, int px, int py){
int rtn = 0;
for (auto elm:elmArr) {
rtn += getVal(elm, px, py);
}
return rtn;
}

int main(void){

int n;
cin >> n;

vector<square> elmArr;
for (int i=0; i<n; i++) {
square cur;
cin >> cur.x1 >> cur.x2 >> cur.y1 >> cur.y2;
elmArr.push_back(cur);
}


for (;;) {
int px,py;
cin >> px >> py;
cout << getVal(elmArr, px, py) << endl;
}

}

优化点

  1. 该算法最优解偏向边缘。因此随着随机值设置越多,得出来的点越偏向边缘,因此随机值不能设置过多。
  2. 为了解决偏向边缘的问题,每一个点在计算优度getVal需要加上与屏幕中心的距离 * n * k3

效果演示


最后就是给大家演示一下最后的效果啦!


圆满完成任务,收工,下班!


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

为什么我在公司里访问不了家里的电脑?

本文为掘金社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究! 上篇文章「为什么我们家里的IP都是192.168开头的?」提到,因为IPv4地址有限,最大42亿个。为了更好的利用这有限的IP数量,网络分为局域网和广域网,将IP分为了私有I...
继续阅读 »

本文为掘金社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!



上篇文章「为什么我们家里的IP都是192.168开头的?」提到,因为IPv4地址有限,最大42亿个。为了更好的利用这有限的IP数量,网络分为局域网和广域网,将IP分为了私有IP和公网IP,一个局域网里的N多台机器都可以共用一个公网IP,从而大大增加了"可用IP数量"。


收发数据就像收发快递


当我们需要发送网络包的时候,在IP层,需要填入源IP地址,和目的IP地址,也就是对应快递的发货地址和收货地址。


IP报头里含有发送和接收IP地址


但是我们家里的局域网内,基本上都用192.168.xx.xx这样的私有IP


如果我们在发送网络包的时候,这么填。对方在回数据包的时候该怎么回?毕竟千家万户人用的都是192.168.0.1,网络怎么知道该发给谁?


所以肯定需要将这个192.168.xx私有IP转换成公有IP


因此在上篇文章最后,留了这么个问题。局域网内用的是私有IP,公网用的都是公有IP。一个局域网里的私有IP想访问局域网外的公有IP,必然要做个IP转换,这是在哪里做的转换呢?


私有IP和公有IP在哪进行转换


答案是NAT设备,全称Network Address Translation,网络地址转换。基本上家用路由器都支持这功能。


我们来聊下它是怎么工作的。


NAT的工作原理


为了简单,我们假设你很富,你家里分到了一个公网IP地址 20.20.20.20,对应配到了你家自带NAT功能的家用路由器上,你家里需要上网的设备有很多,比如你的手机,电脑都需要上网,他们构成了一个局域网,用的都是私有IP,比如192.168.xx。其中你在电脑上执行ifconfig命令,发现家里的电脑IP是192.168.30.5。 你要访问的公网IP地址是30.30.30.30


于是就有下面这样一张图


内网IP访问公网IP


当你准备发送数据包的时候,你的电脑内核协议栈就会构造一个IP数据包。这个IP数据包报头里的发送端IP地址填的就是192.168.30.5接收端IP地址就是30.30.30.30。将数据包发到NAT路由器中。


此时NAT路由器会将IP数据包里的源IP地址修改一下,私有IP地址192.168.30.5改写为公网IP地址20.20.20.20,这叫SNATSource Network Address Translation,源地址转换)。并且还会在NAT路由器内部留下一条 192.168.30.5 -> 20.20.20.20的映射记录,这个信息会在后面用到。之后IP数据包经过公网里各个路由器的转发,发到了接收端30.30.30.30,到这里发送流程结束。


SNAT


如果接收端处理完数据了,需要发一个响应给你的电脑,那就需要将发送端IP地址填上自己的30.30.30.30,将接收端地址填为你的公网IP地址20.20.20.20,发往NAT路由器。NAT路由器收到公网来的消息之后,会检查下自己之前留下的映射信息,发现之前留下了这么一条 192.168.30.5 -> 20.20.20.20记录,就会将这个数据包的目的IP地址修改一下,变成内网IP地址192.168.30.5, 这也叫DNATDestination Network Address Translation,目的地址转换)。 之后将其转发给你的电脑上。


DNAT


整个过程下来,NAT悄悄的改了IP数据包的发送和接收端IP地址,但对真正的发送方和接收方来说,他们却对这件事情,一无所知


这就是NAT的工作原理。




NAPT的原理


到这里,相信大家都有一个很大的疑问。


局域网里并不只有一台机器,局域网内 每台机器都在NAT下留下的映射信息都会是 192.168.xx.xx -> 20.20.20.20,发送消息是没啥事,但接收消息的时候就不知道该回给谁了。


NAT的问题


这问题相当致命,因此实际上大部分时候不会使用普通的NAT


那怎么办呢?


问题出在我们没办法区分内网里的多个网络连接。


于是乎。


我们可以加入其他信息去区分内网里的各个网络连接,很自然就能想到端口。


但IP数据包(网络层)本身是没有端口信息的。常见的传输层协议TCP和UDP数据报文里才有端口的信息。


TCP报头有端口号


UDP报头也有端口号


于是流程就变成了下面这样子。


当你准备发送数据包的时候,你的电脑内核协议栈就会先构造一个TCP或者UDP数据报头,里面写入端口号,比如发送端口是5000,接收端口是3000,然后在这个基础上,加入IP数据报头,填入发送端和接收端的IP地址。


那数据包长这样。


数据包的构成


假设,发送端IP地址填的就是192.168.30.5接收端IP地址就是30.30.30.30


将数据包发到NAT路由器中。


此时NAT路由器会将IP数据包里的源IP地址和端口号修改一下,从192.168.30.5:5000改写成20.20.20.20:6000。并且还会在NAT路由器内部留下一条 192.168.30.5:5000 -> 20.20.20.20:6000的映射记录。之后数据包经过公网里各个路由器的转发,发到了接收端30.30.30.30:3000,到这里发送流程结束。


NAPT发送数据


接收端响应时,就会在数据包里填入发送端地址是30.30.30.30:3000,将接收端20.20.20.20:6000,发往NAT路由器。NAT路由器发现下自己之前留下过这么一条 192.168.30.5:5000 -> 20.20.20.20:6000的记录,就会将这个数据包的目的IP地址和端口修改一下,变回原来的192.168.30.5:5000。 之后将其转发给你的电脑上。


NAPT接收数据


如果局域网内有多个设备,他们就会映射到不同的公网端口上,毕竟端口最大可达65535,完全够用。这样大家都可以相安无事。


像这种同时转换IP和端口的技术,就是NAPT(Network Address Port Transfer , 网络地址端口转换 )。


看到这里,问题就来了。


那这么说只有用到端口的网络协议才能被NAT识别出来并转发?


但这怎么解释ping命令?ping基于ICMP协议,而ICMP协议报文里并不带端口信息。我依然可以正常的ping通公网机器并收到回包。


ping报头


事实上针对ICMP协议,NAT路由器做了特殊处理。ping报文头里有个Identifier的信息,它其实指的是放出ping命令的进程id


对NAT路由器来说,这个Identifier的作用就跟端口一样。


另外,当我们去抓包的时候,就会发现有两个Identifier,一个后面带个BE(Big Endian),另一个带个LE(Little Endian)


其实他们都是同一个数值,只不过大小端不同,读出来的值不一样。就好像同样的数字345,反着读就成了543。这是为了兼容不同操作系统(比如linux和Windows)下大小端不同的情况。


1667783441963


内网穿透是什么


看到这里,我们大概也发现了。使用了NAT上网的话,前提得内网机器主动请求公网IP,这样NAT才能将内网的IP端口转成外网IP端口


反过来公网的机器想主动请求内网机器,就会被拦在NAT路由器上,此时由于NAT路由器并没有任何相关的IP端口的映射记录,因此也就不会转发数据给内网里的任何一台机器。


举个现实中的场景就是,你在你家里的电脑上启动了一个HTTP服务,地址是192.168.30.5:5000,此时你在公司办公室里想通过手机去访问一下,却发现访问不了。


那问题就来了,有没有办法让外网机器访问到内网的服务?


有。


大家应该听过一句话叫,"没有什么是加中间层不能解决的,如果有,那就再加一层"。


放在这里,依然适用。


说到底,因为NAT的存在,我们只能从内网主动发起连接,否则NAT设备不会记录相应的映射关系,没有映射关系也就不能转发数据。


所以我们就在公网上加一台服务器x,并暴露一个访问域名,再让内网的服务主动连接服务器x,这样NAT路由器上就有对应的映射关系。接着,所有人都去访问服务器x,服务器x将数据转发给内网机器,再原路返回响应,这样数据就都通了。这就是所谓的内网穿透


像上面提到的服务器x,你也不需要自己去搭,已经有很多现成的方案,花钱就完事了,比如花某壳。


内网穿透


到这里,我们就可以回答文章标题的问题。


为什么我在公司里访问不了家里的电脑?


那是因为家里的电脑在局域网内,局域网和广域网之间有个NAT路由器。由于NAT路由器的存在,外网服务无法主动连通局域网内的电脑。


两个内网的聊天软件如何建立通讯


好了,问题就叒来了。


我家机子是在我们小区的局域网里,班花家的机子也是在她们小区的局域网里。都在局域网里,且NAT只能从内网连到外网,那我电脑上登录的QQ是怎么和班花电脑里的QQ连上的呢?


两个局域网内的服务无法直接连通


上面这个问法其实是存在个误解,误以为两个qq客户端应用是直接建立连接的。


然而实际上并不是,两个qq客户端之间还隔了一个服务器。


聊天软件会主动与公网服务器建立连接


也就是说,两个在内网的客户端登录qq时都会主动向公网的聊天服务器建立连接,这时两方的NAT路由器中都会记录有相应的映射关系。当在其中一个qq上发送消息时,数据会先到服务器,再通过服务器转发到另外一个客户端上。反过来也一样,通过这个方式让两台内网的机子进行数据传输。


两个内网的应用如何直接建立连接


上面的情况,是两个客户端通过第三方服务器进行通讯,但有些场景就是要抛开第三端,直接进行两端通信,比如P2P下载,这种该怎么办呢?


这种情况下,其实也还是离不开第三方服务器的帮助。


假设还是A和B两个局域网内的机子,A内网对应的NAT设备叫NAT_A,B内网里的NAT设备叫NAT_B,和一个第三方服务器server


流程如下。


step1和2: A主动去连server,此时A对应的NAT_A就会留下A的内网地址和外网地址的映射关系,server也拿到了A对应的外网IP地址和端口。


step3和4: B的操作和A一样,主动连第三方server,NAT_B内留下B的内网地址和外网地址的映射关系,然后server也拿到了B对应的外网IP地址和端口。


step5和step6以及step7: 重点来了。此时server发消息给A,让A主动发UDP消息到B的外网IP地址和端口。此时NAT_B收到这个A的UDP数据包时,这时候根据NAT_B的设置不同,导致这时候有可能NAT_B能直接转发数据到B,那此时A和B就通了。但也有可能不通,直接丢包,不过丢包没关系,这个操作的目的是给NAT_A上留下有关B的映射关系


step8和step9以及step10: 跟step5一样熟悉的配方,此时server再发消息给B,让B主动发UDP消息到A的外网IP地址和端口。NAT_B上也留下了关于A到映射关系,这时候由于之前NAT_A上有过关于B的映射关系,此时NAT_A就能正常接受B的数据包,并将其转发给A。到这里A和B就能正常进行数据通信了。这就是所谓的NAT打洞


step11: 注意,之前我们都是用的UDP数据包,目的只是为了在两个局域网的NAT上打个洞出来,实际上大部分应用用的都是TCP连接,所以,这时候我们还需要在A主动向B发起TCP连接。到此,我们就完成了两端之间的通信。


NAT打洞


这里估计大家会有疑惑。


端口已经被udp用过了,TCP再用,那岂不是端口重复占用(address already in use)?

其实并不会,端口重复占用的报错常见于两个TCP连接在不使用SO_REUSEADDR的情况下,重复使用了某个IP端口。而UDP和TCP之间却不会报这个错。之所以会有这个错,主要是因为在一个linux内核中,内核收到网络数据时,会通过五元组(传输协议,源IP,目的IP,源端口,目的端口)去唯一确定数据接受者。当五元组都一模一样的时候,内核就不知道该把数据发给谁。而UDP和TCP之间"传输协议"不同,因此五元组也不同,所以也就不会有上面的问题。五元组


NAPT还分为好多种类型,上面的nat打洞方案,都能成功吗?

关于NAPT,确实还细分为好几种类型,比如完全锥形NAT和限制型NAT啥的,但这并不是本文的重点。所以我就略过了。我们现在常见的都是锥形NAT。上面的打洞方案适用于大部分场景,这其中包括限制最多的端口受限锥形NAT


1668247032737


总结



  • IPV4地址有限,但通过NAT路由器,可以使得整个内网N多台机器,对外只使用一个公网IP,大大节省了IP资源。

  • 内网机子主动连接公网IP,中间的NAT会将内网机子的内网IP转换为公网IP,从而实现内网和外网的数据交互。

  • 普通的NAT技术,只会修改网络包中的发送端和接收端IP地址,当内网设备较多时,将有可能导致冲突。因此一般都会使用NAPT技术,同时修改发送端和接收端的IP地址和端口

  • 由于NAT的存在,公网IP是无法访问内网服务的,但通过内网穿透技术,就可以让公网IP访问内网服务。一波操作下来,就可以在公司的网络里访问家里的电脑。


最后留个问题,有了NAT之后,原本并不富裕的IPv4地址突然就变得非常够用了。


那我们为什么还需要IPv6?


另外IPv6号称地址多到每粒沙子都能拥有自己的IP地址,那我们还需要NAT吗?


最后


最近原创更文的阅读量稳步下跌,思前想后,夜里辗转反侧。


我有个不成熟的请求。





离开广东好长时间了,好久没人叫我靓仔了。


大家可以在评论区里,叫我一靓仔吗?


最近评论区里叫我diao毛的兄弟越来越多了。


so emo. 哪有什么diao毛,在你面前的,不过是一个漂泊在外,思念故乡的可怜打工人而已。


所以。


我这么善良质朴的愿望,能被满足吗?


别说了,一起在知识的海洋里呛水吧


作者:小白debug
来源:juejin.cn/post/7170850066473680927
收起阅读 »

一个月后,我们又从 MySQL 双主切换成了主 - 从

一、遇到的坑 一个月前,我们在测试环境部署了一套 MySQL 高可用架构,也就是 MySQL 双主 + Keepalived 的模式。 在这一个月遇到了很多坑: 因为两个 MySQL 节点都可以写入,极其容易造成主键重复,进而导致主从同步失败。 同步失败后,...
继续阅读 »

一、遇到的坑


一个月前,我们在测试环境部署了一套 MySQL 高可用架构,也就是 MySQL 双主 + Keepalived 的模式。


在这一个月遇到了很多坑



  • 因为两个 MySQL 节点都可以写入,极其容易造成主键重复,进而导致主从同步失败。

  • 同步失败后,Slave_SQL_Thread 线程就停了,除非解决了同步的错误,才能继续进行同步。

  • 同步失败的错误,不会只有一条记录有问题,往往是一大片的同步问题。

  • 两个节点互相缺少对方的数据。

  • 主从的同步延迟,切换到新主库后,数据不是最新。

  • 当出现不一致时,无法确定以哪个库为准。


造成上面问题的主要原因就是因为两个节点都支持写入 + 双主可以随时切换。


解决这种问题的方案有 改进自增主键的步长(影响未评估),使用 GTID 方案(未验证)。即使这样,双主同步的风险还是有,而且不同步后,如何处理是个大难题。


那么回到我们最初的想法:为什么会选择双主?


最开始的目的就是为了高可用。双主就是说有一台 MySQL 节点挂了,另外一台能够顶上,对于用户来说是无感的,给运维人员一定的缓冲时间来排查 MySQL 故障。另外老的主节点恢复后,不用改配置就能立即成为从节点。


经过这一个月的 MySQL 双主模式的试运行,最后我们还是决定切换到 MySQL 主 - 从模式。


双主模式就是两个节点即是主节点也是从节点,那我们现在切换到一主一从模式,就可以认为是降级。接下来我们聊聊双主换成主从的思路和步骤。


二、双主降为主从


双主模式


双主模式的原理图如下:



两个主节点,都安装了 KeepAlived 高可用组件,对外提供了一个 VIP,只有一个节点接管 VIP,客户端访问的请求都是到这个 VIP,另外一个节点处于待机状态。


主从模式


和双主不一样的地方如下,从节点是只读的。



一主一从是主从模式中的一种,具有以下特点:



  • 一个主节点,一个从节点,主节点提供给客户端访问,从节点只通过主节点的 binlog 进行数据同步。

  • 从节点是只读的。从节点可以作为只读节点提供类似报表查询等耗时读操作。

  • 主节点宕机后,从节点成为主节点,也是高可用的一种方案。


相对于双主的高可用方案,不同之处如下:



  • 主从切换需要用脚本将从库设置为可读可写。

  • 主从切换后,需要将从库设置为不同步老主库。

  • 主从切换后,老的主库恢复后,需要人工设置为只读,且开启同步新主库的功能。


这样来看,主从模式在异常情况下,多了些人工操作。


在异常情况下,主从切换一般是这样处理的:通过脚本监测主节点是否宕机,如果主库宕机了,则从库自动切换为新的主库,待老主库恢复后,就作为从库同步新主库数据,新主库上的 Keepalived 接管 VIP。


目前改为主从模式有两种方式:



  • 简单方式:人工切换模式,主节点故障后需要人工切换主从。

  • 复杂方式:高可用方式,主节点故障后,主从自动切换,读写分离自动切换。


本篇只涉及简单方式,复杂方式的原理和配置步骤放到下篇专门讲解。


三、改为主从的简单方式


简单方式的主从切换流程如下:



和双主模式的主从切换的区别是,从节点是只读的,Keepalived 没有启动,需要人工操作主从切换和启动 Keepalived。


修改配置的步骤如下


① 为了避免从节点上的 Keepalived 自动接管 VIP 的情况出现,将从节点的 Keepalived 停止,如果遇到主节点故障,则需要人工干预来进行主从切换。从节点切换为主节点后,重新启动从节点 Keepalived。


systemctl status keepalived

② 保留主节点的 Keepalived,保证 MySQL 的连接信息都不需要变。


③ 主节点 node1 停用 MySQL 的同步线程。


STOP SLAVE

④ 从节点 node2 设置 MySQL 为只读模式。


# 修改 my.cnf 文件read_only = 1

⑤ 移除主节点 node1 同步 node2 MySQL 的权限。


⑥ 从节点 node1 的开机启动项中移除 keepalived 服务自启动。


# 修改启动项配置sudo vim /etc/rc.local# 移除以下脚本systemctl start keepalived

四、总结


双主高可用的坑确实比较多,没有 MySQL 的硬核知识真的很难搞定。笔者在这一个月的实践中,深刻体会到了双主同步的难点所在,最后还是选择了一主一从的模式。


另外因为最开始的配置都是双主模式下的,所以要修改一些配置,来改为主从模式。因项目时间比较紧,目前采取的是非高可用的主从模式。


作者:优雅程序员阿鑫
来源:juejin.cn/post/7136841690802814989
收起阅读 »

所有开发者注意,苹果审核策略有变

iOS
访问敏感数据的 App 新规 苹果最近在 Apple Developer 上发了篇新闻公告,对需要访问用户敏感数据的 App 增加了审核要求。 这件事的缘由是苹果发现有一小部分 API 可能会被开发者滥用,通过信息指纹收集有关用户设备的信息。 早在今年 6 月...
继续阅读 »


访问敏感数据的 App 新规


苹果最近在 Apple Developer 上发了篇新闻公告,对需要访问用户敏感数据的 App 增加了审核要求。


这件事的缘由是苹果发现有一小部分 API 可能会被开发者滥用,通过信息指纹收集有关用户设备的信息。


早在今年 6 月的 WWDC23 上苹果就宣布,开发人员需要在其应用程序的隐私清单中声明使用某些 API 的原因,目前正式放出了这份需要声明的 API 列表。


新规详情


从今年(2023年)秋天开始,大概是 9 月中旬左右,如果你将你的 App 上传到 App Store Connect,你的应用程序使用到了需要声明原因的 API(也包括你引入的第三方 SDK),但是你没有在隐私清单文件中添加原因,那么 Apple 会给你发送一封警告性的邮件。


从 2024 年春季开始,大概是 3 月左右,没有在隐私清单文件中说明使用原因的 App 将会被拒审核。


需要声明原因的 API 有哪些?


1、NSUserdefaults 相关 API


这个 API 是被讨论最多争议最大的,因为几乎每个 App 都会用到,而且因为有沙盒保护,每个 app 的存储空间是隔离的,这都要申报理由,的确十分荒谬。


2、获取文件时间戳相关的 API

  • creationDate
  • modificationDate
  • fileModificationDate
  • contentModificationDateKey
  • creationDateKey
  • getattrlist(::::_:)
  • getattrlistbulk(::::_:)
  • fgetattrlist(::::_:)
  • stat
  • fstat(::)
  • fstatat(::::)
  • lstat(::)
  • getattrlistat(::::::)

3、获取系统启动时间的 API


大多数衡量 App 启动时间的 APM 库会用到这个 API。

  • systemUptime
  • mach_absolute_time()

4、磁盘空间 API

  • volumeAvailableCapacityKey
  • volumeAvailableCapacityForImportantUsageKey
  • volumeAvailableCapacityForOpportunisticUsageKey
  • volumeTotalCapacityKey
  • systemFreeSize
  • systemSize
  • statfs(::)
  • statvfs(::)
  • fstatfs(::)
  • fstatvfs(::)
  • getattrlist(::::_:)
  • fgetattrlist(::::_:)
  • getattrlistat(::::::)

5、活动键盘 API


这个 API 可以来确定当前用户文本输入的主要语言,有些 App 可能会用来标记用户。

  • activeInputModes


如何在 Xcode 中配置


由于目前 Xcode 15 正式版还没有发布,下边的操作是在 Beta 版本进行的。


在 Xcode 15 中隐私部分全部归类到了一个后缀为 .xcprivacy 的文件中,创建项目时默认没有生成这个文件,我们先来创建一下。


打开项目后,按快捷键 Command + N 新建文件,在面板中搜索 privacy,选择 App Pirvacy 点击下一步创建这个文件。



这个文件是个 plist 格式的面板,默认情况下长这样:



然后点击加号,创建一个 Privacy Accessed API TypesKey,这是一个数组,用来包含所有你 App 使用到需要申明原因的 API。



在这个数组下继续点击加号,创建一个 Item,会看到两个选项:

  • Privacy Accessed API Type:用到的 API 类型
  • Privacy Accessed API Reasons:使用这个 API 的原因(也是个数组,因为可能包含多个原因)



这两个 Key 都创建出来,然后在 Privacy Accessed API Type 一栏点击右侧的菜单,菜单中会列出上边提到的所有 API,选择你需要申报的 API,我这里就拿 UserDefault 来举例:



然后在 Privacy Accessed API Reasons 一览中点击加号,在右侧的选项框中选择对应的原因,每个 API 对应的原因都会列出来,可以到苹果的官方文档上查看这个 API 的原因对应的是哪个,比如 UserDefault 对应的是 CA92.1,我这里就选择这个:



到此,申报原因就完成了,原因不需要自己填写,直接使用苹果给出的选项就可以了,还是蛮简单的。


参考资料


[1]公告原文: developer.apple.com/news/?id=z6…


[2]需要在 App 内声明的 API 列表: developer.apple.com/documentati…


[3]API 列表对应的原因: developer.apple.com/documentati…


作者:杂雾无尘
来源:juejin.cn/post/7267091810379759676
收起阅读 »

懂点心理学 - 踢猫效应

懂点心理学,生活工作两不误~ 什么是踢猫效应 某公司董事长为了重整公司事务,许诺自己将早到晚归。有一次,他在家看报太入迷以至于忘记了时间,为了不迟到,他在公路上超速驾驶,结果被警察开了罚单,最后还耽误了时间。这位老董愤怒之极,回到办公室,为了转移他人的注意,...
继续阅读 »

pexels-yana-kangal-17591717.jpg


懂点心理学,生活工作两不误~


什么是踢猫效应



某公司董事长为了重整公司事务,许诺自己将早到晚归。有一次,他在家看报太入迷以至于忘记了时间,为了不迟到,他在公路上超速驾驶,结果被警察开了罚单,最后还耽误了时间。这位老董愤怒之极,回到办公室,为了转移他人的注意,他将销售经理叫到办公室训斥了一顿。


销售经理挨训之后,气急败坏地走出办公室,将秘书叫到自己的办公室并对他挑剔一顿。


秘书无言无故被人挑剔,自然是一肚子气,故意找接线员的茬儿。


接线员无可奈何垂头丧气地回到家,对着自己的儿子大发雷霆。


儿子莫名其妙地被父亲痛斥之后,也很恼火,便将自己家里的猫狠狠地踢了一脚。



可怜的猫🥺。


提猫效应指的是坏情绪的传达,并且承受最多最痛的人,是弱小的群体。


其他案例


笔者在工作中也会遇到这样的人:



**主管,因为任务的延期完成或者任务出现问题,并没能完成上面的 OKR。受到经理的警告,然后 TA 会找茬自己手下的人,对其一顿 PUA。有时候,TA 还直接跟经理火拼,美其名曰为公司着想,吵着吵着就哭起来,然后任性连休几天假...



这是一种病态的社会表现。也是个人情绪无法抑制的无良的宣泄渠道。更是一种伤害集体利益和他人利益的一种行为。


当然,现实生活中还有很多这种鲜活的案例,比如:


所谓的专家,建议老百姓城市买房去库存,买车带动经济,再开车回农村耕田。升斗小民们可真是惨啊~


如何应对



  1. 认清自己的角色:自己的角色是相对,有时候你是弱者,有时候你是施暴者。我们要正确认识到自己当前所处的角色,尊重善待无辜的人和动物。

  2. 情绪管理:人不是神,会有喜怒哀乐愁,适当给自己放松一下,拥抱下大自然,请别压抑情绪,更不能为了释放情绪而转嫁情绪给他人。「亲人最容易受伤」

  3. 规范和法律:当自己的权益受到了侵害,需要我们拿起法律的武器。比如,公司无缘无故裁了你,但是又没有给赔偿你,甚至在推荐信上恶意差评你。


参考


收起阅读 »

都用HTTPS了,还能被查出浏览记录?

大家好,我卡颂。 最近,群里一个刚入职的小伙因为用公司电脑访问奇怪的网站,被约谈了。他很困惑 —— 访问的都是HTTPS的网站,公司咋知道他访问了啥? 实际上,由于网络通信有很多层,即使加密通信,仍有很多途径暴露你的访问地址,比如: DNS查询:通常DN...
继续阅读 »

大家好,我卡颂。


最近,群里一个刚入职的小伙因为用公司电脑访问奇怪的网站,被约谈了。他很困惑 —— 访问的都是HTTPS的网站,公司咋知道他访问了啥?



实际上,由于网络通信有很多层,即使加密通信,仍有很多途径暴露你的访问地址,比如:




  • DNS查询:通常DNS查询是不会加密的,所以,能看到你DNS查询的观察者(比如运营商)是可以推断出访问的网站




  • IP地址:如果一个网站的IP地址是独一无二的,那么只需看到目标 IP地址,就能推断出用户正在访问哪个网站。当然,这种方式对于多网站共享同一个IP地址(比如CDN)的情况不好使




  • 流量分析:当访问一些网站的特定页面,可能导致特定大小和顺序的数据包,这种模式可能被用来识别访问的网站




  • cookies或其他存储:如果你的浏览器有某个网站的cookies,显然这代表你曾访问过该网站,其他存储信息(比如localStorage)同理




除此之外,还有很多方式可以直接、间接知道你的网站访问情况。


本文将聚焦在HTTPS协议本身,聊聊只考虑HTTPS协议的情况下,你的隐私是如何泄露的。


欢迎围观朋友圈、加入人类高质量前端交流群,带飞


HTTPS简介


我们每天访问的网站大部分是基于HTTPS协议的,简单来说,HTTPS = HTTP + TLS,其中:




  • HTTP是一种应用层协议,用于在互联网上传输超文本(比如网页内容)。由于HTTP是明文传递,所以并不安全




  • TLS是一种安全协议。TLS在传输层对数据进行加密,确保任何敏感信息在两端(比如客户端和服务器)之间安全传输,不被第三方窃取或篡改




所以理论上,结合了HTTPTLS特性的HTTPS,在数据传输过程是被加密的。但是,TLS建立连接的过程却不一定是加密的。


TLS的握手机制


当我们通过TLS传递加密的HTTP信息之前,需要先建立TLS连接,比如:




  • 当用户首次访问一个HTTPS网站,浏览器开始查询网站服务器时,会发生TLS连接




  • 当页面请求API时,会发生TLS连接




建立连接的过程被称为TLS握手,根据TLS版本不同,握手的步骤会有所区别。



但总体来说,TLS握手是为了达到三个目的:




  1. 协商协议和加密套件:通信的两端确认接下来使用的TLS版本及加密套件




  2. 验证省份:为了防止“中间人”攻击,握手过程中,服务器会向客户端发送其证书,包含服务器公钥和证书授权中心(即CA)签名的身份信息。客户端可以使用这些信息验证服务器的身份




  3. 生成会话密钥:生成用于加密接下来数据传输的密钥




TLS握手机制的缺点


虽然TLS握手机制会建立安全的通信,但在握手初期,数据却是明文发送的,这就造成隐私泄漏的风险。


在握手初期,客户端、服务端会依次发送、接收对方的打招呼信息。首先,客户端会向服务端打招呼(发送client hello信息),该消息包含:




  • 客户端支持的TLS版本




  • 支持的加密套件




  • 一串称为客户端随机数client random)的随机字节




  • SNI等一些服务器信息




服务端接收到上述消息后,会向客户端打招呼(发送server hello消息),再回传一些信息。


其中,SNIServer Name Indication,服务器名称指示)就包含了用户访问的网站域名。


那么,握手过程为什么要包含SNI呢?


这是因为,当多个网站托管在一台服务器上并共享一个IP地址,且每个网站都有自己的SSL证书时,那就没法通过IP地址判断客户端是想和哪个网站建立TLS连接,此时就需要域名信息辅助判断。


打个比方,快递员送货上门时,如果快递单只有收货的小区地址(IP地址),没有具体的门牌号(域名),那就没法将快递送到正确的客户手上(与正确的网站建立TLS连接)。


所以,SNI作为TLS的扩展,会在TLS握手时附带上域名信息。由于打招呼的过程是明文发送的,所以在建立HTTPS连接的过程中,中间人就能知道你访问的域名信息。


企业内部防火墙的访问控制和安全策略,就是通过分析SNI信息完成的。



虽然防火墙可能已经有授信的证书,但可以先分析SNI,根据域名情况再判断要不要进行深度检查,而不是对所有流量都进行深度检查



那么,这种情况下该如何保护个人隐私呢?


Encrypted ClientHello


Encrypted ClientHelloECH)是TLS1.3的一个扩展,用于加密Client Hello消息中的SNI等信息。


当用户访问一个启用ECH的服务器时,网管无法通过观察SNI来窥探域名信息。只有目标服务器才能解密ECH中的SNI,从而保护了用户的隐私。



当然,对于授信的防火墙还是不行,但可以增加检查的成本



开启ECH需要同时满足:




  • 服务器支持TLSECH扩展




  • 客户端支持ECH




比如,cloudflare SNI测试页支持ECH扩展,当你的浏览器不支持ECH时,访问该网站sni会返回plaintext



对于chrome,在chrome://flags/#encrypted-client-hello中,配置ECH支持:



再访问上述网站,sni如果返回encrypted则代表支持ECH


总结


虽然HTTPS连接本身是加密的,但在建立HTTPS的过程中(TLS握手),是有数据明文传输的,其中SNI中包含了服务器的域名信息。


虽然SNI信息的本意是解决同一IP下部署多个网站,每个网站对应不同的SSL证书,但也会泄漏访问的网站地址


ECH通过对TLS握手过程中的敏感信息(主要是SNI)进行加密,为用户提供了更强的隐私保护。


作者:魔术师卡颂
来源:juejin.cn/post/7264753569834958908
收起阅读 »

被一个问题卡了近两天,下班后我哭了。。。

写在前面 好像很久没有更文了,感觉有很多想写的,但却又不知道该写些什么了。。。 近阶段,整个人的状态都好,本计划这月想给自己充电,做一些自己想做的事,结果真的就是事与愿违吧。 好像每个人都一样,都是为了生活而疲于奔命,依然忙碌于各种事情之间。 整个过程 没经过...
继续阅读 »

写在前面


好像很久没有更文了,感觉有很多想写的,但却又不知道该写些什么了。。。


近阶段,整个人的状态都好,本计划这月想给自己充电,做一些自己想做的事,结果真的就是事与愿违吧。


好像每个人都一样,都是为了生活而疲于奔命,依然忙碌于各种事情之间。


整个过程


没经过深思熟虑的计划制定


两周前,组内同事想让我帮忙做冒烟测试脚本,原因是因为每次发版测试的时间耗时特别长,所以在结束批量测试工具的开发工作后,我便主动和领导请缨做冒烟测试脚本的开发工作。



和领导说,脚本开发需要5天,整个冒烟测试每次需要大约5分钟!



领导听完很吃惊,我自己说应该差不多吧。


迷之自信?


可能很多同学也会和我的领导一样吃惊,为什么?


系统发版后的回归测试,就测试场景和流程来看,工作量肯定不小,姑且不说技术问题,就业务流程的梳理就很费时间了。


而我却说整个过程只需要五天,可见我是多想证明自己了


其实不然,我自己还是有一些考量的,才说出五天,原因有两个:



  • 因为信任,所以备受期待,同事信任我,真的感觉自己被需要,并且想为团队贡献出一份自己的力量;

  • 因为之前做过测试环境的性能测试脚本,以为很多接口可以直接拿来就用(我天真了,因为改了不少,需要重做)。


理性永远在给感性收拾烂摊子


整个系统总共6个测试流程,也就是说我每天要完成1.2个流程的脚本开发。


我特别喜欢现在团队的氛围,第一天到下班点时,差一个模块就完成了一个流程。


所以在责任心的驱使下,心想加个班吧,今天能赶出来这个模块,明天其他的流程就能复用了。


一切看似很好,也正是这个模块把我彻底卡住了,我遇到了一个让我很抓狂的问题:



打个类比,比如发起申请接口,申请成功了,到领导审批,点击同意的时候报错,而发起申请这个接口却不报错,你在页面同样的操作,领导同意却是正常好用的。



被问题卡住,心态开始崩盘


这个问题,我反复查了近两天......


这期间我积极的找开发同事帮忙排查问题,并确认是否是我的入参不对导致节点数据不正确。


由于开发同事比较忙,能帮我排查问题的时间有限,所以只有在开发稍微有点时间,才能帮忙排查联调。


也正因为开发同事的尽心尽力帮忙,几次下来,让我感觉离问题根源好像又进了一步。


也知道为什么不能审批了,因为虽然请求成功了,但是没走业务逻辑,导致部分数据还是默认值,所以审批报错。


关于入参的排查,暂时告一段落了,因为数据状态不对,无法进行审批,意味着还是没有解决问题。


到这已经是第三天了,一个流程都没整完,感觉整个人都不好了,心态有点崩了......


于是向领导说明原因,领导了解后,并说先把耗时最长的做完,虽然没那么大压力,但是心里还是有些深深地自责。


我还是没忍住,终于哭了出来......


距离周五晚上发版测试还有两天,这个问题不解决,怎么也说不过去,心里一直憋着这个劲特别难受。


当时的想法,真的是谁能帮帮我,帮帮我行么?


但是我也不知道该找谁帮忙,谁又能帮助我?


为什么?说是业务问题吧?还不算?技术问题吧,入参还查不出来啥问题?真的就是进退两难!


因为开发太忙,实在没时间,暂时也没想到什么好的解决办法,我就先下班回了家。


把车停好后,习惯性地给女友打了电话,那天还是我的生日,再加上那阶段烦心事特别多,说着说着我哭了出来,突然感觉好无助而且很没用,最后彻底哭了出来,为什么就那么难?


我以为我很颓废,今天我才知道,原来我早废了。


因为烦心事特别多,导致整个人都不好了,哭出来后,感觉真的很舒服,而且整个人平和了许多。


没人能教你,只有自己能拯救自己


回到家后,搭建好环境,改用工具进行测试,使用jmeter+fiddler抓包开始,重新调接口来模拟测试,结果居然成功了,真的很意外,难道是我代码写的有问题?


第二天上班,我把自己代码接口调用及入参与昨天做好的jmeter脚本一一对照,发现入参一模一样,这让我产生了怀疑,是我封装的工具类有问题?


我代码走的HTTP协议,而jmeter脚本是HTTPS协议才成功的。


这让我想到,可能我的httpclient需要走HTTPS协议请求会让接口调用后,数据应该会正常显示吧。


有了思路,就开始找httpclient如何进行HTTPS请求的相关文章。


经过一番搜索,找到的重点都是围绕使用ssl和根证书的使用的代码片段,我又对httpclient底层封装进行改造,改造完再次使用封装工具类调用接口,结果还是数据状态不对,我真的彻底绝望了。


于是,我又去找到了强哥(我北京的同事),强哥说你干嘛自己封装,用hutool呀。


我照着强哥的思路,又去照着hutool中的工具类,开始写demo,逐一调用接口,结果竟然成功了,这让我欣喜若狂,真的好用。


于是,我对写好的demo,再次进行封装,也就是hutool中的工具类封装,封装好后,再次使用封装好的工具类调用,结果数据状态又不对了。


我真的服了,这是玩我吗?分开就好使,封装就不行。


有的同学说了,应该是你封装的有问题,那为什么其他模块都好用,就这个模块不行?


后来,我灵机一动,那就都对分开可用这部分代码进行简单封装,保证流程跑通就行,算是退而求其次的解决方法,虽然,它很low,但是能用。


也正因为这个临时解决方案,助力我在周五发版前成功的让同事用上了,一个流程的冒烟测试,跑完这一个流程仅需113秒,比手动回归快了近10倍的时间。


写在最后


整个过程让我记忆深刻,在此特别记录一下,真的是头一次被问题卡的这么难受,那种既生气,又干不掉难题的感觉,太难受了!


你有被难题阻塞,一直无法继续下去的情况吗?

作者:软件测试君
来源:juejin.cn/post/7135473631559811080
欢迎文末给我留言哦!

收起阅读 »

鹅厂组长,北漂 10 年,有房有车,做了一个违背祖宗的决定

前几天是 10 月 24 日,有关注股票的同学,相信大家都过了一个非常难忘的程序员节吧。在此,先祝各位朋友们身体健康,股票基金少亏点,最重要的是不被毕业。 抱歉,当了回标题党,不过在做这个决定之前确实纠结了很久,权衡了各种利弊,就我个人而言,不比「3Q 大战」...
继续阅读 »

前几天是 10 月 24 日,有关注股票的同学,相信大家都过了一个非常难忘的程序员节吧。在此,先祝各位朋友们身体健康,股票基金少亏点,最重要的是不被毕业。


抱歉,当了回标题党,不过在做这个决定之前确实纠结了很久,权衡了各种利弊,就我个人而言,不比「3Q 大战」时腾讯做的「艰难的决定」来的轻松。如今距离这个决定过去了快 3 个月,我也还在适应着这个决定带来的变化。


按照工作汇报的习惯,先说结论:



在北漂整整 10 年后,我回老家合肥上班了



做出这个决定的唯一原因:



没有北京户口,积分落户陪跑了三年,目测 45 岁之前落不上



户口搞不定,意味着孩子将来在北京只能考高职,这断然是不能接受的;所以一开始是打算在北京读几年小学后再回老家,我也能多赚点钱,两全其美。


因为我是一个人在北京,如果在北京上小学,就得让我老婆或者让我父母过来。可是我老婆的职业在北京很难就业,我父母年龄大了,北京人生地不熟的,而且那 P 点大的房子,住的也憋屈。而将来一定是要回去读书的,这相当于他们陪着我在北京折腾了。


或者我继续在北京打工赚钱,老婆孩子仍然在老家?之前的 6 年基本都是我老婆在教育和陪伴孩子,我除了逢年过节,每个月回去一到两趟。孩子天生过敏体质,经常要往医院跑,生病时我也帮不上忙,所以时常被抱怨”丧偶式育儿“,我也只能跟渣男一样说些”多喝热水“之类的废话。今年由于那啥,有整整 4 个多月没回家了,孩子都差点”笑问客从何处来“了。。。


5月中旬,积分落户截止,看到贴吧上网友晒出的分数和排名,预计今年的分数线是 105.4,而实际分数线是 105.42,比去年的 100.88 多了 4.54 分。而一般人的年自然增长分数是 4 分,这意味着如果没有特殊加分,永远赶不上分数线的增长。我今年的分数是 90.8,排名 60000 左右,每年 6000 个名额,即使没有人弯道超车,落户也得 10 年后了,孩子都上高一了,不能在初二之前搞到户口,就表示和大学说拜拜了。


经过我的一番仔细的测算,甚至用了杠杆原理和人品守恒定理等复杂公式,最终得到了如下结论:



我这辈子与北京户口无缘了



所以,思前想后,在没有户口的前提下,无论是老婆孩子来北京,还是继续之前的异地,都不是好的解决方案。既然将来孩子一定是在合肥高考,为了减少不必要的折腾,那就只剩唯一的选择了,我回合肥上班,兼顾下家里。


看上去是个挺自然的选择,但是:



我在腾讯是组长,团队 20 余人;回去是普通工程师,工资比腾讯打骨折



不得不说,合肥真的是互联网洼地,就没几个公司招人,更别说薪资匹配和管理岗位了。因此,回合肥意味着我要放弃”高薪“和来之不易的”管理“职位,从头开始,加上合肥这互联网环境,基本是给我的职业生涯判了死刑。所以在 5 月底之前就没考虑过这个选项,甚至 3 月份时还买了个显示器和 1.6m * 0.8m 的大桌子,在北京继续大干一场,而在之前的 10 年里,我都是用笔记本干活的,从未用过外接显示器。


5 月初,脉脉开始频繁传出毕业的事,我所在的部门因为是盈利的,没有毕业的风险。但是营收压力巨大,作为底层的管理者,每天需要处理非常非常多的来自上级、下级以及甲方的繁杂事务,上半年几乎都是凌晨 1 点之后才能睡觉。所以,回去当个普通工程师,每天干完手里的活就跑路,貌似也不是那么不能接受。毕竟自己也当过几年 leader 了,leader 对自己而言也没那么神秘,况且我这还是主动激流勇退,又不是被撸下来的。好吧,也只能这样安慰自己了,中年人,要学会跟自己和解。后面有空时,我分享下作为 leader 和普通工程师所看到的不一样的东西。


在艰难地说服自己接受之后,剩下的就是走各种流程了:

1. 5月底,联系在合肥工作的同学帮忙内推;6月初,通过面试。我就找了一家,其他家估计性价比不行,也不想继续面了
2. 6月底告诉总监,7月中旬告诉团队,陆续约或被约吃散伙饭
3. 7月29日,下午办完离职手续,晚上坐卧铺离开北京
4. 8月1日,到新公司报道

7 月份时,我还干了一件大事,耗时两整天,历经 1200 公里,不惧烈日与暴雨,把我的本田 125 踏板摩托车从北京骑到了合肥,没有拍视频,只能用高德的导航记录作为证据了:




这是导航中断的地方,晚上能见度不行,在山东花了 70 大洋,随便找了个宾馆住下了,第二天早上出发时拍的,发现居然是水泊梁山附近,差点落草为寇:




骑车这两天,路上发生了挺多有意思的事,以后有时间再分享。到家那天,是我的结婚 10 周年纪念日,我没有提前说我要回来,更没说骑着摩托车回来,当我告诉孩子他妈时,问她我是不是很牛逼,得到的答复是:



我觉得你是傻逼



言归正传,在离开北京前几天,我找团队里的同学都聊了聊,对我的选择,非常鲜明的形成了两个派系:

1. 未婚 || 工作 5 年以内的:不理解,为啥放弃管理岗位,未来本可以有更好的发展的,太可惜了,打骨折的降薪更不能接受

2. 已婚 || 工作 5 年以上的:理解,支持,甚至羡慕;既然迟早都要回去,那就早点回,多陪陪家人,年龄大了更不好回;降薪很正常,跟房价也同步,不能既要又要

确实,不同的人生阶段有着不同的想法,我现在是第 2 阶段,需要兼顾家庭和工作了,不能像之前那样把工作当成唯一爱好了。


在家上班的日子挺好的,现在加班不多,就是稍微有点远,单趟得 1 个小时左右。晚上和周末可以陪孩子玩玩,虽然他不喜欢跟我玩🐶。哦,对了,我还有个重要任务 - 做饭和洗碗。真的是悔不当初啊,我就不应该说会做饭的,更不应该把饭做的那么好吃,现在变成我工作以外的最重要的业务了。。。


比较难受的是,现在公司的机器配置一般,M1 的 MBP,16G 内存,512G 硬盘,2K 显示器。除了 CPU 还行,内存和硬盘,都是快 10 年前的配置了,就这还得用上 3 年,想想就头疼,省钱省在刀刃上了,属于是。作为对比,腾讯的机器配置是:



M1 Pro MBP,32G 内存 + 1T SSD + 4K 显示器


客户端开发,再额外配置一台 27寸的 iMac(i9 + 32G内存 + 1T SSD)



由奢入俭难,在习惯了高配置机器后,现在的机器总觉得速度不行,即使很多时候,它和高配机没有区别。作为开发,尤其是客户端开发,AndroidStudio/Xcode 都是内存大户,16G 实在是捉襟见肘,非常影响搬砖效率。公司不允许用自己的电脑,否则我就自己买台 64G 内存的 MBP 干活用了。不过,换个角度,编译时间变长,公司提供了带薪摸鱼的机会,也可以算是个福利🐶


另外,比较失落的就是每个月发工资的日子了,比之前少了太多了,说没感觉是不可能的,还在努力适应中。不过这都是小事,毕竟年底发年终奖时,会更加失落,hhhh😭😭😭😭


先写这么多吧,后面有时间的话,再分享一些有意思的事吧,工作上的或生活上的。


遥想去年码农节时,我还在考虑把房子从昌平换到海淀,好让孩子能有个“海淀学籍”,当时还做了点笔记:




没想到,一年后的我回合肥了,更想不到一年后的腾讯,股价竟然从 500 跌到 206 了(10月28日,200.8 了)。真的是世事难料,大家保重身体,好好活着,多陪陪家人,一起静待春暖花开💪🏻💪🏻


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

程序员崩溃的40个瞬间!!!太形象了,你遇到几个?

说到程序员,在外界眼里他们是掌控代码的大神他们是改变世界的王者其实程序员的工作不容易不信就来看看程序员崩溃的各种瞬间——1.公司实习生找bug2.在调试时,将断点设置在错误的位置3.当我有一个很棒的调试想法4.偶然间看到自己多年前写的代码5.当我第一次启动我的...
继续阅读 »

说到程序员,在外界眼里

他们是掌控代码的大神

他们是改变世界的王者

其实程序员的工作不容易

不信

就来看看程序员崩溃的各种瞬间——

1.公司实习生找bug


2.在调试时,将断点设置在错误的位置


3.当我有一个很棒的调试想法


4.偶然间看到自己多年前写的代码


5.当我第一次启动我的单元测试


6.数据库的delete语句忘了使用限定词where


7.明明是个小bug,但就是死活修不好


8.当我尝试调整生产数据库中的一些东西时

动图封面

9.好像真的没人发现我产品里的bug


10.下班前我还有一项任务没有完成


11.产品还没测试直接投入生产时


12.调试过多线程的都会懂


13.当我以为已捕获了所有可能的异常...的时候


14.当我试图清理几行所谓的旧代码


15.当有人让我帮他调试代码时


16.程序员第一次向老板演示项目


17.当你看到你几个月没碰过的代码


18.接到产品经理电话的我睡意全无


19.测试的时候一切ok,真正上线的时候……


20.作为一个程序员,拷问灵魂的时刻到了


21.当年学C语言的过程


22.当前端程序员想改后台代码时,后台程序员的样子


23.调试bug


24.正在调试,突然内存溢出了


25.需求文档又改了


26.苦逼的后端工程师


27.后端工程师做UI的活


28.在生产环境做hotfix


29.刚调稳定的系统,公司叕空降了一位架构师,又双叕要重构现有系统……


30.当程序员听客户说还在用IE时


31.功能先上了再说


32.新手程序员第一次做项目的过程


33.零错误零警告一次编译通过


34.春节前后上班写代码状态是这样的


35.被老板委派接手刚刚离职同事的项目...


36.准备下班的时候,测试又提bug过來了…


37.测试刚写完的代码


38.当我以为我修复了一个bug


39.程序员新手尝试新框架的时候


40.当我第一次测试我的代码时


41.我设计的接口和别人调用我的接口(好疼)


42.高级开发人员作为一个团队进行编程时


43.不小心碰到了遗留代码,真惨


本文转自知乎专栏 互联网视界https:/

收起阅读 »

Hook神器—Frida安装

什么是Frida Frida is Greasemonkey for native apps, or, put in more technical terms, it’s a dynamic code instrumentation toolkit. It ...
继续阅读 »

什么是Frida



Frida is Greasemonkey for native apps, or, put in more technical terms, it’s a dynamic code instrumentation toolkit. It lets you inject snippets of JavaScript into native apps that run on Windows, Mac, Linux, iOS and Android. Frida is an open source software.



frida 是平台原生 appGreasemonkey ,说的专业一点,就是一种动态插桩工具,可以插入一些代码到原生 app 的内存空间去,(动态地监视和修改其行为),这些原生平台可以是WinMacLinuxAndroid 或者 iOS 。而且 frida 还是开源的。


Frida安装


Python3安装


安装Frida之前需要电脑有Python环境的,Python3的安装可以参考这篇文章。Python安装之后,还要检查是否安装了pip3,如果没有安装的话,需要自行查找安装的方法。


安装Frida

1.安装frida

pip3 install frida

2.安装frida-tools

pip3 install frida-tools

3.安装objection

pip3 install objection

执行完以上命令就完成了frida的安装,上面的命令安装的都是最新版本的


安装frida-server


安装frida-server之前需要知道Android手机的cpu架构,命令如下

adb shell getprop ro.product.cpu.abi 

还要知道电脑安装的frida的版本,frida-server的版本要与电脑端的frida版本相同,查看电脑端的frida版本的命令如下,

frida --version

知道了Android手机的cpu架构和frida的版本,到github下载相应版本的frida-server,github地址点击这里




测试是否安装成功


启动frida-server

  1. 将下载的frida-server压缩包解压
  2. 解压后的文件push到手机
  3. 启动frida-server服务

上面步骤的对应命令如下

$ adb root 
$ adb push frida-server-16.0.8-android-arm /data/local/tmp
$ adb shell
$ su
$ cd /data/local/tmp
$ chmod 777 /data/local/tmp/frida-server-16.0.8-android-arm
$ ./frida-server-16.0.8-android-arm

端口映射


启动frida-server之后还要进行端口映射,否则电脑无法连接到手机

adb forward tcp:27042 tcp:27042
adb forward tcp:27043 tcp:27043

查看进程


上面的步骤都完成后,就可以执行下面的命令,获取手机当前的进程信息

frida-ps -U

如果能看到你手机的进程信息,如图




则说明你的frida和frida-server安装配置成功。


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

教你如何实现接口防刷

教你如何实现接口防刷 前言 我们在浏览网站后台的时候,假如我们频繁请求,那么网站会提示 “请勿重复提交” 的字样,那么这个功能究竟有什么用呢,又是如何实现的呢? 其实这就是接口防刷的一种处理方式,通过在一定时间内限制同一用户对同一个接口的请求次数,其目的是为了...
继续阅读 »

教你如何实现接口防刷


前言


我们在浏览网站后台的时候,假如我们频繁请求,那么网站会提示 “请勿重复提交” 的字样,那么这个功能究竟有什么用呢,又是如何实现的呢?


其实这就是接口防刷的一种处理方式,通过在一定时间内限制同一用户对同一个接口的请求次数,其目的是为了防止恶意访问导致服务器和数据库的压力增大,也可以防止用户重复提交。


思路分析


接口防刷有很多种实现思路,例如:拦截器/AOP+Redis、拦截器/AOP+本地缓存、前端限制等等很多种实现思路,在这里我们来讲一下 拦截器+Redis 的实现方式。


其原理就是 在接口请求前由拦截器拦截下来,然后去 redis 中查询是否已经存在请求了,如果不存在则将请求缓存,若已经存在则返回异常。具体可以参考下图




具体实现



注:以下代码中的 AjaxResult 为统一返回对象,这里就不贴出代码了,大家可以根据自己的业务场景来编写。



编写 RedisUtils

import com.apply.core.exception.MyRedidsException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
* Redis工具类
*/
@Component
public class RedisUtils {

@Autowired
private RedisTemplate<String, Object> redisTemplate;

/****************** common start ****************/
/**
* 指定缓存失效时间
*
* @param key 键
* @param time 时间(秒)
* @return
*/
public boolean expire(String key, long time) {
try {
if (time > 0) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}

/**
* 根据key 获取过期时间
*
* @param key 键 不能为null
* @return 时间(秒) 返回0代表为永久有效
*/
public long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}

/**
* 判断key是否存在
*
* @param key 键
* @return true 存在 false不存在
*/
public boolean hasKey(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}

/**
* 删除缓存
*
* @param key 可以传一个值 或多个
*/
@SuppressWarnings("unchecked")
public void del(String... key) {
if (key != null && key.length > 0) {
if (key.length == 1) {
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete(CollectionUtils.arrayToList(key));
}
}
}
/****************** common end ****************/


/****************** String start ****************/

/**
* 普通缓存获取
*
* @param key 键
* @return 值
*/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}

/**
* 普通缓存放入
*
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}

/**
* 普通缓存放入并设置时间
*
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
public boolean set(String key, Object value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}

/**
* 递增
*
* @param key 键
* @param delta 要增加几(大于0)
* @return
*/
public long incr(String key, long delta) {
if (delta < 0) {
throw new MyRedidsException("递增因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, delta);
}

/**
* 递减
*
* @param key 键
* @param delta 要减少几(小于0)
* @return
*/
public long decr(String key, long delta) {
if (delta < 0) {
throw new MyRedidsException("递减因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, -delta);
}
/****************** String end ****************/
}

定义Interceptor

import com.alibaba.fastjson.JSON;
import com.apply.common.utils.redis.RedisUtils;
import com.apply.common.validator.annotation.AccessLimit;
import com.apply.core.http.AjaxResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Objects;

/**
* @author Bummon
* @description 重复请求拦截
* @date 2023-08-10 14:14
*/
@Component
public class RepeatRequestIntercept extends HandlerInterceptorAdapter {

@Autowired
private RedisUtils redisUtils;

/**
* 限定时间 单位:秒
*/
private final int seconds = 1;

/**
* 限定请求次数
*/
private final int max = 1;

public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//判断请求是否为方法的请求
if (handler instanceof HandlerMethod) {
String key = request.getRemoteAddr() + "-" + request.getMethod() + "-" + request.getRequestURL();
Object requestCountObj = redisUtils.get(key);
if (Objects.isNull(requestCountObj)) {
//若为空则为第一次请求
redisUtils.set(key, 1, seconds);
} else {
response.setContentType("application/json;charset=utf-8");
ServletOutputStream os = response.getOutputStream();
AjaxResult<Void> result = AjaxResult.error(100, "请求已提交,请勿重复请求");
String jsonString = JSON.toJSONString(result);
os.write(jsonString.getBytes());
os.flush();
os.close();
return false;
}
}
return true;
}

}

然后我们 将拦截器注册到容器中

import com.apply.common.validator.intercept.RepeatRequestIntercept;
import com.apply.core.base.entity.Constants;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
* @author Bummon
* @description
* @date 2023-08-10 14:17
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {

@Autowired
private RepeatRequestIntercept repeatRequestIntercept;

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(repeatRequestIntercept);
}
}

我们再来编写一个接口用于测试

import com.apply.common.validator.annotation.AccessLimit;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
* @author Bummon
* @description
* @date 2023-08-10 14:35
*/
@RestController
public class TestController {

@GetMapping("/test")
public String test(){
return "SUCCESS";
}

}

最后我们来看一下结果是否符合我们的预期:


1秒内的第一次请求:




1秒内的第二次请求:




确实已经达到了我们的预期,但是如果我们对特定接口进行拦截,或对不同接口的限定拦截时间和次数不同的话,这种实现方式无法满足我们的需求,所以我们要提出改进。


改进


我们可以去写一个自定义的注解,并将 secondsmax 设置为该注解的属性,再在拦截器中判断请求的方法是否包含该注解,如果包含则执行拦截方法,如果不包含则直接返回。


自定义注解 RequestLimit

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* @author Bummon
* @description 幂等性注解
* @date 2023-08-10 15:10
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RequestLimit {

/**
* 限定时间
*/
int seconds() default 1;

/**
* 限定请求次数
*/
int max() default 1;

}

改进 RepeatRequestIntercept

/**
* @author Bummon
* @description 重复请求拦截
* @date 2023-08-10 15:14
*/
@Component
public class RepeatRequestIntercept extends HandlerInterceptorAdapter {

@Autowired
private RedisUtils redisUtils;

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//判断请求是否为方法的请求
if (handler instanceof HandlerMethod) {
HandlerMethod hm = (HandlerMethod) handler;
//获取方法中是否有幂等性注解
RequestLimit anno = hm.getMethodAnnotation(RequestLimit.class);
//若注解为空则直接返回
if (Objects.isNull(anno)) {
return true;
}
int seconds = anno.seconds();
int max = anno.max();
String key = request.getRemoteAddr() + "-" + request.getMethod() + "-" + request.getRequestURL();
Object requestCountObj = redisUtils.get(key);
if (Objects.isNull(requestCountObj)) {
//若为空则为第一次请求
redisUtils.set(key, 1, seconds);
} else {
//限定时间内的第n次请求
int requestCount = Integer.parseInt(requestCountObj.toString());
//判断是否超过最大限定请求次数
if (requestCount < max) {
//未超过则请求次数+1
redisUtils.incr(key, 1);
} else {
//否则拒绝请求并返回信息
refuse(response);
return false;
}
}
}
return true;
}

/**
* @param response
* @date 2023-08-10 15:25
* @author Bummon
* @description 拒绝请求并返回结果
*/
private void refuse(HttpServletResponse response) throws IOException {
response.setContentType("application/json;charset=utf-8");
ServletOutputStream os = response.getOutputStream();
AjaxResult<Void> result = AjaxResult.error(100, "请求已提交,请勿重复请求");
String jsonString = JSON.toJSONString(result);
os.write(jsonString.getBytes());
os.flush();
os.close();
}

}

这样我们就可以实现我们的需求了。


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

Mac开发环境配置看这一篇就够了

iOS
前言 从 macOS Catalina 开始,Mac 使用 zsh 作为默认登录 Shell 和交互式 Shell。当然你也可以修改默认Shell,但一般没这个必要。而实际开发中经常会遇到一些环境问题导致的报错,下面我们就讲一下一些常用库的环境配置以及原理。 ...
继续阅读 »

前言


macOS Catalina 开始,Mac 使用 zsh 作为默认登录 Shell 和交互式 Shell。当然你也可以修改默认Shell,但一般没这个必要。而实际开发中经常会遇到一些环境问题导致的报错,下面我们就讲一下一些常用库的环境配置以及原理。


一、Homebrew


作为Mac上最常用的包管理器,Homebrew可以称为神器,用它来管理Mac上的依赖环境便捷又省心。


1. 安装


这里我们直接在终端执行国人写的一键安装脚本,换源(官方源的速度你懂的)啥的都直接安排上了。

/bin/zsh -c "$(curl -fsSL https://gitee.com/cunkai/HomebrewCN/raw/master/Homebrew.sh)"



这里我们选择1、中科大下载源就好了,按照提示输入并耐心等待安装完成。






最后一步重载配置文件我们执行source ~/.zshrc,重载用户目录下的.zshrc


到这里我们可以执行brew -v测试一下Homebrew的安装结果:

~:~$brew -v
Homebrew 3.6.21-26-gb0a74e5
Homebrew/homebrew-core (git revision 4fbf6930104; last commit 2023-02-08)
Homebrew/homebrew-cask (git revision cbce859534; last commit 2023-02-09)

有版本号输出说明已经安装完成了。


2. 卸载


直接在终端执行一键脚本即可

复制代码
/bin/zsh -c "$(curl -fsSL https://gitee.com/cunkai/HomebrewCN/raw/master/HomebrewUninstall.sh)"

3. 常用命令

/// 安装
brew install FORMULA|CASK...

/// 搜索
brew search TEXT|/REGEX/

/// 卸载包
brew uninstall FORMULA|CASK...

/// 查看安装列表
brew list [FORMULA|CASK...]

/// 查看包信息
brew info [FORMULA|CASK...]

/// 查看哪些包可以更新
brew outdated

/// 更新指定包(安装新包,但旧包依旧保留)
brew upgrade [FORMULA|CASK...]

/// 更新Homebrew
brew update

/// 清理旧版本和缓存
brew cleanup # 清理所有包的旧版本
brew cleanup [FORMULA ...] # 清理指定包的旧版本
brew cleanup -n # 查看可清理的旧版本包,不执行实际操作

/// 锁定不想更新的包(因为update会一次更新所有的包的,当我们想忽略的时候可以使用这个命令)
brew pin [FORMULA ...] # 锁定某个包
brew unpin [FORMULA ...] # 取消锁定

/// 软件服务管理
brew services list # 查看使用brew安装的服务列表
brew services run formula|--all # 启动服务(仅启动不注册)
brew services start formula|--all # 启动服务,并注册
brew services stop formula|--all # 停止服务,并取消注册
brew services restart formula|--all # 重启服务,并注册

二、Ruby

1. 安装



其实Mac系统默认已经有Ruby的环境了,在终端中执行ruby -v查看版本号。

~:~$ruby -v
ruby 2.6.3p62 (2019-04-16 revision 67580) [universal.x86_64-darwin20]

本地ruby版本有点低了,这里我们使用Homebrew来更新,

brew install ruby

执行结束后默认会将最新版本的ruby安装到/usr/local/Cellar/目录下。


我们查看一下当前的ruby版本:

~:~$ruby -v
ruby 2.6.3p62 (2019-04-16 revision 67580) [universal.x86_64-darwin20]

好像版本并未发生变化,why? 这里主要是因为Shell环境中并没有读到最新的ruby路径,我们可以再编辑一下用户目录下的环境配置文件~/.zshrc,新增ruby的路径并写入环境变量:

# 环境变量配置
export RUBY=/usr/local/Cellar/ruby/3.2.0/bin
export GEMS=/usr/local/lib/ruby/gems/3.2.0/bin

# 写入环境变量
export PATH=$RUBY:$GEMS:$PATH

这里先添加上面的内容然后执行source ~/.zshrc,后面会讲到Shell环境配置相关的内容。


再次查看ruby版本:

~:~$ruby -v
ruby 3.2.0 (2022-12-25 revision a528908271) [x86_64-darwin20]

此时可以看到ruby已经升级到最新的3.2.0版本。


当然我们还可以执行which ruby查看当前的ruby的具体路径:

~:~$which ruby
/usr/local/Cellar/ruby/3.2.0/bin/ruby

从结果可以看出当前使用的ruby正是我们在.zshrc中配置的路径。


2. Gem换源


Gemruby的包管理器,一些ruby库我们需要使用Gem来安装,但Gem官方源速度拉胯,这里我们需要替换为国内源。

/// 添加国内源并删除官方源
gem sources --add https://gems.ruby-china.com/ --remove https://rubygems.org/

/// 查看当前源地址
gem sources -l

查看当前源,确认已替换为国内源即可。

~:~$gem sources -l
*** CURRENT SOURCES ***

https://gems.ruby-china.com/

3. 常用包安装

/// cocoapods安装
gem install cocoapods

/// fastlane安装
gem install fastlane

耐心等待安装完成后我们可以测试一下:

~:~$pod --version
1.11.3

~:~$fastlane --version
fastlane installation at path:
/usr/local/lib/ruby/gems/3.2.0/gems/fastlane-2.211.0/bin/fastlane
-----------------------------
[✔] 🚀
fastlane 2.211.0

从结果可以看出cocoapodsfastlane都安装完成了。


三、Python

1. 使用Xcode自带Python库(推荐)



其实Xcode命令行工具自带了python库,项目中需要执行python脚本的优先使用这个会更合适,因为Xcode编译项目时会优先使用这个python库,Mac中仅使用这一个版本可以避免一些多python版本环境问题导致的报错。


根据当前Xcode命令行工具中的python版本,这里我们需要在~/.zshrc中添加相关配置并执行source ~/.zshrc重载配置:

# 环境变量配置
export PYTHON=/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.8/Python3/bin

# 写入环境变量
export PATH=$PYTHON:$PATH

# 别名
alias python=python3
alias pip=pip3

这里使用别名以便于执行python命令时使用的是python3, 查看一下版本,结果也符合预期。

~:~$python --version
Python 3.8.9

2. 使用Homebrew安装


这里我们直接执行:

brew install python

耐心等待安装完成,其实Homebrew会将Python安装到/usr/local/Cellar/目录下,并在/usr/local/bin目录创建了链接文件。这里我们需要在~/.zshrc中添加相关配置并执行source ~/.zshrc重载配置:

# 环境变量配置
export SBIN=/usr/local/bin:/usr/local/sbin

# 写入环境变量
export PATH=$SBIN:$PATH

# 别名
alias python=python3
alias pip=pip3

查看一下版本,已经升级到最新版:

~:~$python --version
Python 3.10.10

3. pip换源


pippython的包管理器,我们可以使用它来安装一些python库。我们可以更换一个国内源来提升下载速度:

/// 查看当前源
pip config list

/// 替换为清华大学源
pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple

/// 还原为默认源
pip config unset global.index-url


4. 常用包安装

/// openpyxl安装
pip install openpyxl

安装速度非常快:

~:~$pip install openpyxl
Collecting openpyxl
Using cached openpyxl-3.1.0-py2.py3-none-any.whl (250 kB)
Requirement already satisfied: et-xmlfile in /usr/local/lib/python3.10/site-packages (from openpyxl) (1.1.0)
Installing collected packages: openpyxl
Successfully installed openpyxl-3.1.0

四、Shell环境配置

1. zsh的配置文件.zshrc



macOS Catalina 开始,Mac 使用 zsh 作为默认shell,而它的配置文件是用户目录下的.zshrc文件,所以我们之前在定义环境变量时都会编辑这个文件。每次打开终端时都会读取这个配置文件,如果需要在当前的shell窗口读取最新的环境配置则需要执行source ~/.zshrc,这也是之前我们编辑该文件后重载配置的原因(为了让最新的配置生效😁)。


2. 定义环境变量(全局变量)

export RUBY=/usr/local/Cellar/ruby/3.2.0/bin

其实我们之前在讲Ruby的安装时已经在~/.zshrc文件中定义过全局变量,语法就是在一个变量名前面加上export关键字。这里我们可以在终端输出一下这个变量:

~:~$echo $RUBY
/usr/local/Cellar/ruby/3.2.0/bin

变量的值可以正常输出,这也意味着这样的变量在当前shell程序中全局可读。


3. 写入环境变量


常见的环境变量:

  • CDPATH:冒号分隔的目录列表,作为cd命令的搜索路径
  • HOME:当前用户的主目录
  • PATHshell查找命令的目录列表,由冒号分隔
  • BASH:当前shell实例的全路径名
  • PWD:当前工作目录

这里重点关注一下PATH变量,当我们在shell命令行界面中输入一个外部命令时,shell必须搜索系统来找到对应的程序。PATH环境变量定义了用于进行命令和程序查找的目录:

echo $PATH

某些时候我们执行命令会遇到command not found这样的报错,比如:

~:~$hi
zsh: command not found: hi

这是因为PATH中的目录并没有包含hi命令,所以我们执行hi就报错。同理,当我们在配置环境时,某些库的目录需要被写入到PATH中,比如:

# 环境变量配置
export SBIN=/usr/local/bin:/usr/local/sbin
export HOMEBREW=/usr/local/Homebrew/bin
export RUBY=/usr/local/Cellar/ruby/3.2.0/bin
export GEMS=/usr/local/lib/ruby/gems/3.2.0/bin

# 写入环境变量
export PATH=$SBIN:$HOMEBREW:$RUBY:$GEMS:$PATH

这样当我们执行具体的命令时,shell才能够正确的访问。




  • 附.zshrc常见配置

    # 环境变量配置
    export SBIN=/usr/local/bin:/usr/local/sbin
    export HOMEBREW=/usr/local/Homebrew/bin
    export RUBY=/usr/local/Cellar/ruby/3.2.0/bin
    export GEMS=/usr/local/lib/ruby/gems/3.2.0/bin

    # 写入环境变量
    export PATH=$SBIN:$HOMEBREW:$RUBY:$GEMS:$PATH

    # 别名
    alias python=python3
    alias pip=pip3

    # 编码
    export LC_ALL=en_US.UTF-8
    export LANG=en_US.UTF-8

    # 控制PS1信息
    PROMPT='%U%F{51}%1~%f%u:~$'

    # 镜像源
    export HOMEBREW_BOTTLE_DOMAIN=https://mirrors.ustc.edu.cn/homebrew-bottles



五、参考文档


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

收起阅读 »

类似chat-gpt的打字机效果

类似chat-gpt的打字机效果 展示效果: 实现思路:只要控制显示内容的长度就行了,每次加一点显示内容,然后一直播放闪烁动画,加载完了就停掉动画。 结论:单个字逐渐加载 + 闪烁动画 = 打字机效果 闪烁动画实现 通过css实现.cursor { ...
继续阅读 »

类似chat-gpt的打字机效果


展示效果:




实现思路:只要控制显示内容的长度就行了,每次加一点显示内容,然后一直播放闪烁动画,加载完了就停掉动画。


结论:单个字逐渐加载 + 闪烁动画 = 打字机效果


闪烁动画实现


通过css实现

.cursor {
position: absolute;
display: inline-block;
width: 2px;
height: 16px;
background-color: #000;
animation: blink 1s infinite;
transform: translate(2px, 3px);
}

@keyframes blink {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0;
}
}

展现效果:




完整代码

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>打字机效果</title>
<style>
.cursor {
position: absolute;
display: inline-block;
width: 2px;
height: 16px;
background-color: #000;
animation: blink 1s infinite;
transform: translate(2px, 3px);
}

@keyframes blink {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0;
}
}
.box1 {
line-height: 22px;
width: 300px;
font-size: 16px;
padding: 10px;
border: 1px solid pink;
margin-bottom: 10px;
min-height: 100px;
}
</style>
</head>
<body>
<div class="box1">
<span class="cursor"></span>
</div>
<button class="btn">添加文字</button>
<script>
const randomTextArr = ["萨嘎", '三', "agas", '大厦', '阿萨法施工', 'saf', '啊', '收到', '三个哈哈哈', '阿事实上事实上事实上', '事实上事实上少时诵诗书', '叫哦大家搜狗号度搜化工三打哈干撒的很尬山东干红手打很尬搜哈', '时间几节课MVvvvvvvvvvv啪啪啪啪啪啪PPT科技我IQ和瓦暖气,你', '撒啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊就你跟贵公司懂法守法收入与武器我先把发发花洒就你跟贵公司懂法守法收入与武器我先把发发花洒'];
const box1 = document.querySelector('.box1');
const btn = document.querySelector('.btn');
let showText = '';
let addTextArr = [];
let timer = null;

btn.onclick = () => {
getRandomText();
updateText();
}

function getRandomText() {
const randomTextArrLength = randomTextArr.length;
let randomNum = Math.random();
let addText = randomTextArr[Math.floor(Math.random() * randomTextArrLength)];
addTextArr.push(addText);
console.log(addText)
}

function updateText() {
let index = 0;
if (!timer) {
timer = setInterval(() => {
if (addTextArr.length > 0) {
if (index < addTextArr[0].length) {
box1.innerHTML = showText + addTextArr[0][index] + `<span></span>`;
showText += addTextArr[0][index];
index ++;
} else {
index = 0;
box1.innerHTML = showText;
addTextArr.shift();
}
} else {
clearInterval(timer);
timer = null;
}
}, 50)
}
}
</script>
</body>
</html>

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

让你兴奋不已的13个CSS技巧🤯

快来免费体验ChatGpt plus版本的,我们出的钱 体验地址:chat.waixingyun.cn 可以加入网站底部技术群,一起找bug,另外新版作图神器已上线 cube.waixingyun.cn/home 1.使用边框绘制一个三角形 在某些情况下,例如...
继续阅读 »

快来免费体验ChatGpt plus版本的,我们出的钱 体验地址:chat.waixingyun.cn 可以加入网站底部技术群,一起找bug,另外新版作图神器已上线 cube.waixingyun.cn/home


1.使用边框绘制一个三角形


在某些情况下,例如在工具提示中添加箭头指针时,如果你只需要简单的三角形,那么加载图片可能会过度。


仅使用CSS,您就可以通过边框创建一个三角形。


这是一个相当老的技巧。理想情况下,你会在一个宽度和高度都为零的元素上设置边框。所有的边框颜色都是透明的,除了那个将形成箭头的边框。例如,要创建一个向上指的箭头,底部边框是有颜色的,而左边和右边是透明的。无需包括顶部边框。边框的宽度决定了箭头的大小

.upwards-arrow {
width: 0;
height: 0;
border-left: 20px solid transparent;
border-right: 20px solid transparent;

border-bottom: 20px solid crimson;
}

这将创建一个像下面所示的向上指的箭头:




事例地址:codepen.io/chriscoyier…


2.交换元素的背景


z-index 属性规定了元素如何堆叠在其他定位元素上。有时,你可能会设置一个 z-index 属性让子元素的层级较低,结果却发现它隐藏在其父元素的背景之后。为了防止这种情况,你可以在父元素上创建一个新的堆叠上下文,防止子元素隐藏在其后面。创建堆叠上下文的一种方法是使用 isolation: isolate CSS样式声明。


我们可以利用这种堆叠上下文技术来创建悬停效果,该效果可以交换按钮的背景。例如:

button.join-now {
cursor: pointer;
border: none;
outline: none;
padding: 10px 15px;

position: relative;
background-color: #5dbea3;
isolation: isolate; /* If ommitted, child pseudo element will be stacked behind */
}

button.join-now::before {
content: "";
position: absolute;
background-color: #33b249;
top: 0;
left: 100%;
right: 0;
bottom: 0;
transition: left 500ms ease-out;

z-index: -1;
}

button.join-now:hover::before {
left: 0;
}

上述代码在鼠标悬停时交换了 button 的背景。背景的变化不会干扰前景的文本,如下面的gif所示:




3.将元素居中


可能,你已经知道如何使用 display: flex;display: grid; 来居中元素。然而,另一种不太受欢迎的在x轴上居中元素的方法是使用 text-align CSS属性。这个属性在居中文本时就能直接使用。要想在DOM中也居中其他元素,子元素需要有一个 inline 的显示。它可以是 inline-block 或任何其他内联...

div.parent {
text-align: center;
}

div.child {
display: inline-block;
}

4.药丸💊形状按钮


可以通过将按钮的边框半径设置为非常高的值来制作药丸形状的按钮。当然,边框半径应该高于按钮的高度。

button.btn {
border-radius: 80px; /* value higher than height of the button */
padding: 20px 30px;
background-color: #fdd835;
border: none;
color: black;
font-size: 20px;
}



按钮的高度可能会随着设计的改变而增加。因此,你会发现将 border-radius 设置为非常高的值是很方便的,这样无论按钮是否增大,你的css都能继续工作。


5.轻松为你的网站添加美观的加载指示器


对于开发者来说,将注意力转移到为你的网站创建一个美观的加载指示器上往往是一项乏味的任务。这种关注力更好地用于构建项目的其他重要部分,这些部分值得我们去关注。


当你在阅读时,很可能你也觉得这是个令人烦恼的难题。这就是为什么我花时间为你消除这个障碍,并精心准备了一个装有加载指示器的库,让你可以在你的梦想项目中“即插即用”。这是一个完整的集合,你只需要挑选出那个能点燃你心中火花💖的。只需看看这个库的简单用法,源代码在Github上可用。别忘了给个星星⭐


地址:http://www.npmjs.com/package/rea…




6.简易暗色或亮色模式


您只需要几行CSS代码,就可以在我们的网站上启用深色/浅色模式。您只需让浏览器知道,您的网站可以在系统的深色/浅色模式下正确显示。

html {
color-scheme: light dark;
}

注意: color-scheme 属性可以设置在除 html 之外的任何DOM元素上。


然后通过我们的网站设置控制背景颜色和文字颜色的变量,通过检查浏览器支持使其更加防弹:

html {
--bg-color: #ffffff;
--txt-color: #000000;
}

@supports (background-color: Canvas) and (color: CanvasText) {
:root {
--bg-color: Canvas;
--txt-color: CanvasText;
}
}

注意:如果你不在元素上设置 background-color ,它将继承浏览器定义的与深色/浅色主题匹配的系统颜色。这些系统颜色在不同的浏览器之间可能会有所不同。


明确设置 background-color 可以与 prefers-color-scheme 结合使用,以提供与浏览器默认设置不同的颜色阴影。


以下是暗/亮模式的实际应用。用户的偏好在暗模式和亮模式之间进行模拟。




7.使用省略号( ... )截断溢出的文本


这个技巧已经存在一段时间,用于美观地修剪长文本。但你可能仍然错过了它。你只需要以下的CSS:

p.intro {
width: 300px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

只需实施以下规则:



  • 明确的宽度,因此剪裁的边界将永远被达到。

  • 浏览器会将超出元素宽度的长文本进行换行。所以你需要阻止这种情况: white-space: nowrap; 。

  • 溢出的内容应被剪裁: overflow: hidden; 。

  • 当文本即将被剪切时,用省略号( ... )填充字符串: text-overflow: ellipsis; 。


结果看起来像这样:




8.将长文本截断为若干行


这与上述技巧略有不同。这次,文本被剪裁,将内容限制为一定的行数。

p.intro {
width: 300px;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3; /* Truncate when no. of lines exceed 3 */
overflow: hidden;
}

输出看起来像这样:




9. 停止过度劳累自己写作 toprightbottomleft


在处理定位元素时,你通常会编写如下代码:

.some-element {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}

这可以通过使用 inset 属性来简化:

.some-element {
position: absolute;
inset: 0;
}

或者,如果你对 toprightbottomleft 有不同的值,你可以按照如下的顺序分别设置它们: inset: -10px 0px -10px 0px这种简写方式与margin 的工作方式相同。


10.提供优化过的图片


请尝试在浏览器的开发者工具中将网络速度调整到较慢,然后访问一个由高清图片组成的网站,比如 unsplash。这就是你的网站访客在网络速度较慢的地理区域尝试欣赏你的高清内容时所经历的痛苦。


但你可以通过 image-set CSS 技巧提供一种解救方法。


可以为浏览器提供选项,让它决定最适合用户设备的图片。例如:

.banner {
background-image: url("elephant.png"),
background-image: -webkit-image-set(
url("elephant.webp") type("image/webp") 1x,
url("elephantHD.webp") type("image/webp") 2x,
url("elephant.png") type("image/png") 1x,
url("elephantHD.png") type("image/png") 2x
);
}

上述代码将设置元素的背景图像。


如果支持 -webkit-image-set ,那么背景图像将会是一种优化的图像,也就是说,这将是一种支持的MIME类型的图像,且更适合用户设备的分辨率能力。


例如:由于更高质量的图像直接与更大的尺寸成正比,所以在网络状况差的情况下使用高分辨率设备的用户,会促使浏览器决定提供支持的低分辨率图像。让用户等待高清图像加载是不合逻辑的。


11. 计数器


你不必纠结于浏览器如何渲染编号列表。你可以利用 counters() 实现你自己的设计。以下是操作方法:

ul {
margin: 0;
font-family: sans-serif;

/* Define & Initialize Counter */
counter-reset: list 0;
}

ul li {
list-style: none;
}

ul li:before {
padding: 5px;
margin: 0 8px 5px 0px;
display: inline-block;
background: skyblue;
border-radius: 50%;
font-weight: 100;
font-size: 0.75rem;

/* Increment counter by 1 */
counter-increment: list 1;
/* Show incremented count padded with `.` */
content: counter(list) ".";
}



12.表单验证视觉提示


仅使用CSS,您就可以向用户显示有关表单输入有效性的视觉提示。我们可以在表单元素上使用 :valid:invalid CSS伪类,当其内容验证成功或失败时,应用适当的样式。


请考虑以下HTML页面结构:

<!-- Regex in pattern attribute means input can accept `firstName Lastname` (whitespace sepearated names) -->
<!-- And invalidates any other symbols like `*` -->
<input
type="text"
pattern="([a-zA-Z0-9]\s?)+"
placeholder="Enter full name"
required
/>
<span></span>

<span> 将用于显示验证结果。以下的CSS根据其验证结果来设置输入框的样式:

input + span {
position: relative;
}

input + span::before {
position: absolute;
right: -20px;
bottom: 0;
}

input:not(:placeholder-shown):invalid {
border: 2px solid red;
}

input:not(:placeholder-shown):invalid + span::before {
content: "✖";
color: red;
}

input:not(:placeholder-shown):valid + span::before {
content: "✓";
color: green;
}

地址:codepen.io/hane-smitte…


13. 一键选择文本


这个技巧主要是为了提升网站用户的复制和粘贴体验。使用 user-select: all ,可以通过一键实现简单的文本选择。所有位于该元素下方的文本节点都会被选中。


另一方面,可以使用 user-select: none; 来禁用文本选择。禁用文本选择的另一种方法是将文本放在 ::before::after CSS伪元素的 content: ''; 属性中。


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

你大脑中的画面,现在可以高清还原了

前言 AI 直接把你脑中的创意画出来的时刻,已经到来了。 本文转载自机器之心 仅用于学术分享,若侵权请联系删除 欢迎关注公众号CV技术指南,专注于计算机视觉的技术总结、最新技术跟踪、经典论文解读、CV招聘信息。 CV各大方向专栏与各个部署框架最全教程整理 【...
继续阅读 »

前言 AI 直接把你脑中的创意画出来的时刻,已经到来了。



本文转载自机器之心


仅用于学术分享,若侵权请联系删除


欢迎关注公众号CV技术指南,专注于计算机视觉的技术总结、最新技术跟踪、经典论文解读、CV招聘信息。


CV各大方向专栏与各个部署框架最全教程整理


【CV技术指南】CV全栈指导班、基础入门班、论文指导班 全面上线!!


近几年,图像生成领域取得了巨大的进步,尤其是文本到图像生成方面取得了重大突破:只要我们用文本描述自己的想法,AI 就能生成新奇又逼真的图像。


但其实我们可以更进一步 —— 将头脑中的想法转化为文本这一步可以省去,直接通过脑活动(如 EEG(脑电图)记录)来控制图像的生成创作。


这种「思维到图像」的生成方式有着广阔的应用前景。例如,它能极大提高艺术创作的效率,并帮助人们捕捉稍纵即逝的灵感;它也有可能将人们夜晚的梦境进行可视化;它甚至可能用于心理治疗,帮助自闭症儿童和语言障碍患者。


最近,来自清华大学深圳国际研究生院、腾讯 AI Lab 和鹏城实验室的研究者们联合发表了一篇「思维到图像」的研究论文,利用预训练的文本到图像模型(比如 Stable Diffusion)强大的生成能力,直接从脑电图信号生成了高质量的图像。



论文地址:arxiv.org/pdf/2306.16…


项目地址:github.com/bbaaii/Drea…


方法概述


近期一些相关研究(例如 MinD-Vis)尝试基于 fMRI(功能性磁共振成像信号)来重建视觉信息。他们已经证明了利用脑活动重建高质量结果的可行性。然而,这些方法与理想中使用脑信号进行快捷、高效的创作还差得太远,这主要有两点原因:


首先,fMRI 设备不便携,并且需要专业人员操作,因此捕捉 fMRI 信号很困难;


其次,fMRI 数据采集的成本较高,这在实际的艺术创作中会很大程度地阻碍该方法的使用。


相比之下,EEG 是一种无创、低成本的脑电活动记录方法,并且现在市面上已经有获得 EEG 信号的便携商用产品。


但实现「思维到图像」的生成还面临两个主要挑战:


1)EEG 信号通过非侵入式的方法来捕捉,因此它本质上是有噪声的。此外,EEG 数据有限,个体差异不容忽视。那么,如何从如此多的约束条件下的脑电信号中获得有效且稳健的语义表征呢?


2)由于使用了 CLIP 并在大量文本 - 图像对上进行训练,Stable Diffusion 中的文本和图像空间对齐良好。然而,EEG 信号具有其自身的特点,其空间与文本和图像大不相同。如何在有限且带有噪声的 EEG - 图像对上对齐 EEG、文本和图像空间?


为了解决第一个挑战,该研究提出,使用大量的 EEG 数据来训练 EEG 表征,而不是仅用罕见的 EEG 图像对。该研究采用掩码信号建模的方法,根据上下文线索预测缺失的 token。


不同于将输入视为二维图像并屏蔽空间信息的 MAE 和 MinD-Vis,该研究考虑了 EEG 信号的时间特性,并深入挖掘人类大脑时序变化背后的语义。该研究随机屏蔽了一部分 token,然后在时间域内重建这些被屏蔽的 token。通过这种方式,预训练的编码器能够对不同个体和不同脑活动的 EEG 数据进行深入理解。


对于第二个挑战,先前的解决方法通常直接对 Stable Diffusion 模型进行微调,使用少量噪声数据对进行训练。然而,仅通过最终的图像重构损失对 SD 进行端到端微调,很难学习到脑信号(例如 EEG 和 fMRI)与文本空间之间的准确对齐。因此,研究团队提出采用额外的 CLIP 监督,帮助实现 EEG、文本和图像空间的对齐。


具体而言,SD 本身使用 CLIP 的文本编码器来生成文本嵌入,这与之前阶段的掩码预训练 EEG 嵌入非常不同。利用 CLIP 的图像编码器提取丰富的图像嵌入,这些嵌入与 CLIP 的文本嵌入很好地对齐。然后,这些 CLIP 图像嵌入被用于进一步优化 EEG 嵌入表征。因此,经过改进的 EEG 特征嵌入可以与 CLIP 的图像和文本嵌入很好地对齐,并更适合于 SD 图像生成,从而提高生成图像的质量。


基于以上两个精心设计的方案,该研究提出了新方法 DreamDiffusion。DreamDiffusion 能够从脑电图(EEG)信号中生成高质量且逼真的图像。



具体来说,DreamDiffusion 主要由三个部分组成:


1)掩码信号预训练,以实现有效和稳健的 EEG 编码器;


2)使用预训练的 Stable Diffusion 和有限的 EEG 图像对进行微调;


3)使用 CLIP 编码器,对齐 EEG、文本和图像空间。


首先,研究人员利用带有大量噪声的 EEG 数据,采用掩码信号建模,训练 EEG 编码器,提取上下文知识。然后,得到的 EEG 编码器通过交叉注意力机制被用来为 Stable Diffusion 提供条件特征。



为了增强 EEG 特征与 Stable Diffusion 的兼容性,研究人员进一步通过在微调过程中减少 EEG 嵌入与 CLIP 图像嵌入之间的距离,进一步对齐了 EEG、文本和图像的嵌入空间。


实验与分析


与 Brain2Image 对比


研究人员将本文方法与 Brain2Image 进行比较。Brain2Image 采用传统的生成模型,即变分自编码器(VAE)和生成对抗网络(GAN),用于实现从 EEG 到图像的转换。然而,Brain2Image 仅提供了少数类别的结果,并没有提供参考实现。


鉴于此,该研究对 Brain2Image 论文中展示的几个类别(即飞机、南瓜灯和熊猫)进行了定性比较。为确保比较公平,研究人员采用了与 Brain2Image 论文中所述相同的评估策略,并在下图 5 中展示了不同方法生成的结果。


下图第一行展示了 Brain2Image 生成的结果,最后一行是研究人员提出的方法 DreamDiffusion 生成的。可以看到 DreamDiffusion 生成的图像质量明显高于 Brain2Image 生成的图像,这也验证了本文方法的有效性。



消融实验


预训练的作用:为了证明大规模 EEG 数据预训练的有效性,该研究使用未经训练的编码器来训练多个模型进行验证。其中一个模型与完整模型相同,而另一个模型只有两层的 EEG 编码层,以避免数据过拟合。在训练过程中,这两个模型分别进行了有 / 无 CLIP 监督的训练,结果如表 1 中 Model 列的 1 到 4 所示。可以看到,没有经过预训练的模型准确性有所降低。



mask ratio:本文还研究了用 EEG 数据确定 MSM 预训练的最佳掩码比。如表 1 中的 Model 列的 5 到 7 所示,过高或过低的掩码比会对模型性能都会产生不利影响。当掩码比为 0.75 达到最高的整体准确率。这一发现至关重要,因为这表明,与通常使用低掩码比的自然语言处理不同,在对 EEG 进行 MSM 时,高掩码比是一个较好的选择。


CLIP 对齐:该方法的关键之一是通过 CLIP 编码器将 EEG 表征与图像对齐。该研究进行实验验证了这种方法的有效性,结果如表 1 所示。可以观察到,当没有使用 CLIP 监督时,模型的性能明显下降。实际上,如图 6 右下角所示,即使在没有预训练的情况下,使用 CLIP 对齐 EEG 特征仍然可以得到合理的结果,这凸显了 CLIP 监督在该方法中的重要性。



欢迎关注公众号CV技术指南,专注于计算机视觉的技术总结、最新技术跟踪、经典论文解读、CV招聘信息。


计算机视觉入门1v3辅导班


【技术文档】《从零搭建pytorch模型教程》122页PDF下载


QQ交流群:470899183。群内有大佬负责解答大家的日常学习、科研、代码问题。


其它文章


中科院自动化所发布FastSAM | 精度相当,速度提升50倍!!!


大核卷积网络是比 Transformer 更好的教师吗?ConvNets 对 ConvNets 蒸馏奇效


MaskFormer:将语义分割和实例分割作为同一任务进行训练


CVPR 2023 VAND Workshop Challenge零样本异常检测冠军方案


视觉魔法师:开启语义分割的奇幻之旅


沈春华团队最新 | SegViTv2对SegViT进行全面升级,让基于ViT的分割模型更轻更强


刷新20项代码任务SOTA,Salesforce提出新型基础LLM系列编码器-解码器Code T5+


可能95%的人还在犯的PyTorch错误


从DDPM到GLIDE:基于扩散模型的图像生成算法进展


CVPR最佳论文颁给自动驾驶大模型!中国团队第一单位,近10年三大视觉顶会首例


最新轻量化Backbone | FalconNet汇聚所有轻量化模块的优点,成就最强最轻Backbone


ReID专栏(二)多尺度设计与应用


ReID专栏(一) 任务与数据集概述


libtorch教程(三)简单模型搭建


libtorch教程(二)张量的常规操作


libtorch教程(一)开发环境搭建:VS+libtorch和Qt+libtorch


NeRF与三维重建专栏(三)nerf_pl源码部分解读与colmap、cuda算子使用


NeRF与三维重建专栏(二)NeRF原文解读与体渲染物理模型


NeRF与三维重建专栏(一)领域背景、难点与数据集介绍


异常检测专栏(三)传统的异常检测算法——上


异常检测专栏(二):评价指标及常用数据集


异常检测专栏(一)异常检测概述


BEV专栏(二)从BEVFormer看BEV流程(下篇)


BEV专栏(一)从BEVFormer深入探究BEV流程(上篇)


可见光遥感图像目标检测(三)文字场景检测之Arbitrary


可见光遥感目标检测(二)主要难点与研究方法概述


可见光遥感目标检测(一)任务概要介绍


TensorRT教程(三)TensorRT的安装教程


TensorRT教程(二)TensorRT进阶介绍


TensorRT教程(一)初次介绍TensorRT


AI最全资料汇总 | 基础入门、技术前沿、工业应用、部署框架、实战教程学习


计算机视觉入门1v3辅导班


计算机视觉交流群


聊聊计算机视觉入门


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

阿里又又发布了一个“AI神器”

历史回顾 上一次阿里发布通义千问,犹在昨天,结果,阿里又发布了一件AI神器,该神器实用性极强,据说背后依然采用阿里的通义千问大模型。 不了解的可以看下我的历史文章 阿里的通义千问,震惊到我了 最近一直在整理AIGC方面的东西分享给大家,当然编程也不会落下,欢迎...
继续阅读 »

历史回顾


上一次阿里发布通义千问,犹在昨天,结果,阿里又发布了一件AI神器,该神器实用性极强,据说背后依然采用阿里的通义千问大模型。


不了解的可以看下我的历史文章 阿里的通义千问,震惊到我了


最近一直在整理AIGC方面的东西分享给大家,当然编程也不会落下,欢迎关注,让我们在AI的道路上越走越远


他来了,讯飞星火迈着矫健的步伐向我们走来了


免费搭建个人stable-diffusion绘画(非本地、干货教程)


阿里给“打工”朋友送上“节日礼物”




六一儿童节当天,阿里就给所有“打工”的大朋友送上了一份“节日礼物”


6月1日上午,阿里云发布了面向音视频内容的AI新品“通义听悟”,并正式公测



【通义听悟】 推荐给你~ tingwu.aliyun.com/u/14xZ00303… 工作学习AI助手,依托大模型,为每一个人提供全新的音视频体验。点击链接立即注册,公测期免费体验。



通义千问、通义听悟 这哥俩现在所处环境不同,定位不同,功能不同 但依赖大模型是相同的


这是阿里通义大模型最新的应用进展,也是国内首个开放公测的大模型应用产品。


根据阿里云智能CTO周靖人介绍,“通义听悟”是依托通义千问大模型和音视频AI模型的AI助手,可进行实时语音识别,实现对话的实时记录、多语言翻译、发言总结、提取PPT、内容规整等。


对我们打工人有什么用


会议神器




当领导在上面夸夸其谈的时候,你的会议纪要可谓是错乱无章,这会儿通义听悟就上线了,你只需要录音




或者我们本地上传




支持区分多人对话,然后开始转写


值得一提的是, “听悟”可以根据AI转写,提取这场说话内容的关键词,给出全文摘要。




视频总结神器




不同于传统的实时会议速记转写,如今面向C端提供视频转写服务的应用尚在少数。而如今的通义听悟,则从纯粹的音频转写,延伸到了音视频领域,融合了十多项AI新功能。


“通义听悟”我个人认为最大的实用功能是:可以设置插件,无论看视频、看直播,还是开会,点开听悟插件,就能实现音视频的实时转录和翻译。




其实看到这里,可以感受到,这不只是说对打工人的福利,也是对于学生党的福利,比如我们上课,课后复盘总结




最后再提一点阿里的生态,他们将数据存储和阿里云盘打通 这点是值得表扬的,在阿里云盘中,用户可以一键转写云盘中的文件,在云盘内在线播放视频时,能够实时生成字幕。


还能帮我们什么


通义听悟未来还有更多基于大模型的功能上线。比如,对视频中出现的PPT,AI能够基于通义千问大模型做到一键提取,而用户也能向AI助手针对多个音视频内容进行提问、让听悟概括特定段落等等。


值得注意的是,听悟目前针对一些细分场景中提供了不同的部署形态,如浏览器插件。在Chrome安装听悟插件后,听悟在无字幕视频中就可以实时生成双语悬浮字幕。二转写结果可下载为字幕文件,方便新媒体从业者视频后期制作




通义千问Chrome插件示意图,近期该功能将上线,可以持续关注 我后续给大家做详细介绍,不过我们可以先感受下




钉钉的在线会议模块“钉闪记”,同样集成了听悟。在会议结束后,钉闪记所能够输出的也不再是纯粹的速记,而是包含重点摘要的完整文档,可以有效地提升公司内部工作效率。甚至,在开会时,AI可以代为记录会议、整理要点。


未来一段时间还将在夸克APP、阿里云盘等端口提供服务


总结


这一番体验下来总体的效果还是可以的




从通义听悟中可以看出,国内大模型厂商除了在底层大模型搭建上快马加鞭外,AI应用也已经成为他们必须抓住的机遇——AI音视频转写、内容理解等功能,背后意味着通用能力,厂商们可以覆盖包括开会、上课、培训、面试、直播、看视频、听播客等音视频场景,嵌入到不同的应用软件当中。


今天的分享就到这里,我们的AI绘画系列正在慢慢搭建,对AI有兴趣的可以关注公众号(微信公众号搜索 1点东西) ,我们会持续输出AIGC类好玩的工具和想法,立志让每个人都能感受AI,利用AI找寻更多可能性


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

App 备案的复杂情绪:某些海外的独立 app 要和我们告别了

最近国内 App 备案的消息引发了大家热烈的讨论,其实对于国内的正规开发者而言其实影响没那么大,只是多了一道行政程序。有媒体报道说这样做是为了打击网络欺诈。对于作为独立开发者的我而言,app 要求备案我是完全理解的。只是希望能让备案维持一个低门槛,不要出现地方...
继续阅读 »

最近国内 App 备案的消息引发了大家热烈的讨论,其实对于国内的正规开发者而言其实影响没那么大,只是多了一道行政程序。有媒体报道说这样做是为了打击网络欺诈。对于作为独立开发者的我而言,app 要求备案我是完全理解的。只是希望能让备案维持一个低门槛,不要出现地方部门在执行中为了图省事增加额外的制度成本。


关于备案一个焦点问题是海外的 app 怎么办。参照历史经验,海外的一些独立 app 可能要跟国区告别了。备案对于海外的独立开发者而言还是不太好操作的,除非 AppStore 可以提供足够的帮助。但是我个人的观点,AppStore 只是一个发行商。替开发者备案是一个重运营的体力活,从商业角度 apple 实在是没动力做这个事情。何况如果海外 app 能赚钱,他们自然有能力和动力去完成备案。总的来说对海外的独立开发者而言,增加了不少门槛。




还有一个坏消息:如果一个 app 提供的是订阅服务,在订阅期间停止服务苹果会给用户退款。所以海外的 app 在国区下架以后,如果提供的是订阅服务,就不只是损失国内市场。苹果给用户退款,开发者可能还要贴钱给苹果。不过我个人觉得一个 app 如果能在中国赚到钱,似乎完全有动力找一个本地代理解决一些备案的事情。也有不少海外 app 接入的是支付集成方案,也许支付的解决方案提供商会有兴趣提供国内的备案服务(stripe?)。


但是也许这个对国内的一些开发者而言有一个小小的利好。如果国区大量海外 app 下架,国内的 app 市场就空出了不少市场。虽然这个并不是我所期待的,但是在商言商这个就是事实。也许 AppStore 会再现 copy to china 的情况,做一个高仿的海外 app 上架国区。道德上这样当然是要被人谴责的,但是国内现状就有不少安卓的开发者 copy 优质的 iOS 独立 app 到安卓市场。真很难评。


再说监管的执行问题。Apple 因为对自己的 app 分发一直有严格的管理,前几年就开始收紧了企业证书,很容易满足监管要求。加上 apple 又是一家守法的外企,相信在要求 app 在信息里填上备案号就可以了。但是安卓因为可以比较自由的安装 apk 的包,主流安卓手机厂商又都是中国的企业,我觉得未来如果要收紧监管,要求国内安卓手机接入 apk 安装认证,只有有备案号的 app 才能安装在手机上也不是不可能。到时候如果海外 app 不仅不能从应用商店下,自己下载的 apk 也不能安装恐怕会是一个沉重的打击。


更加严格的监管,对于会被电信诈骗骗到的小白用户是有好处的。但是我想对于另外一头对 app 有自主辨别能力的自由派用户而言就相当不友好了。我觉得简单的抱怨有点肤浅,而且伤身体。还是找到一个和现实世界妥协的方式吧。


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

如今的网络投票还有意义吗?完全就是比哪家预算高吧

最近老板的老婆在参加一项什么“指尖博物馆”的评选,因此老板每天在群里给我们发链接让我们投票。我们兢兢业业每天一投,甚至还相互提醒,完全把这个当成事业在努力。结果,对面直接不按常理出牌,一夜就刷了300票,我们一早来,发现这个数据,面面相觑:这绝对是刷票了吧! ...
继续阅读 »

最近老板的老婆在参加一项什么“指尖博物馆”的评选,因此老板每天在群里给我们发链接让我们投票。我们兢兢业业每天一投,甚至还相互提醒,完全把这个当成事业在努力。结果,对面直接不按常理出牌,一夜就刷了300票,我们一早来,发现这个数据,面面相觑:这绝对是刷票了吧!


这就是现在网络投票的现状,可能一开始开启网络刷票的人并没有想到会变成这样。本以为这将是民意的体现,具有公平、公正、透明的特质。而且它不需要场地、设备、人力,更不需要大量资金,极大地节省了人力成本。最重要的是,它能通过网络迅速传播活动信息、扩大影响规模,最后的数据一导出,妥妥一大亮点。


事与愿违,网络投票中的刷票行为,让这原本可以最大体现民意的设置,成了黑灰产牟利的“利器”。“某某评选开始啦,请动动你的手指为我们投上宝贵的一票”“本次评选活动对我们很重要,我们需要您的支持,鞠躬感谢”……每次朋友圈看到这样的话,我都心有戚戚:发了也没用呀,对面可能直接机器刷票了!


网络刷票的两种形式


网络刷票背后有一群分工明确的黑灰产业。所谓黑灰产是指利用计算机、网络等手段,基于各类漏洞,通过恶意程序、木马病毒、网络、电信等形式,以非法盈利为目的规模化、组织化、分工明确的群体组织。
图片


网络刷票形式,主要有人工刷票与机器刷票两种。


人工刷票:就是一些空闲时间多的人士,以帮助参赛选手投票来获得“佣金”为主要工作。这些人多数为兼职投票手,外界称之为投票水军。


**技术刷票:**通过抓包工具分析提交投票时所产生的数据,然后使用脚本程序批量提交数据的刷票方式。


通过分析可以发现,网络刷票黑灰产揽客主要分为三步:第一步,在网上搜索这类评选活动,通过冒充主办方或媒体等方式,拨打参选对象单位公开的办公电话,索要参选者私人联系方式。第二步,抛诱饵强卖。会以“专业低价、保证安全”、“先刷后付”等话术来诱骗参选人交钱刷票,不管参选人是否同意,团伙都会假意进行少量刷票,为下一步诈骗进行铺垫。第三步,威胁+诈骗。即便参选者交了一部分的预付金,诈骗团伙也不会再帮你刷票了。而是以此为要挟不断加价,等到活动快结束时,再以冲击排名为由诈骗一笔。


此外,为了吸引参与者自发的去拉票刷票,商家往往会将投票配合奖励去吸引投票用户。由此经引起“羊毛党”注意,出现组队集团化刷票薅礼品的现象。


如何防止网络刷票


毫无疑问,网络刷票对于发起者(商家)、参与者、投票用户这三方来说,都是会造成损失的。我们需要采取一定的措施来防止黑灰产进行刷票。


比较简单的方式有以下集中:


1. 验证码: 在投票页面中加入验证码功能,通过输入验证码来确认投票者是真实的人类用户,而不是机器人或其他自动程序。这里可以使用顶象的免费验证码,因此就不展开叙述了。


2. IP地址限制: 通过限制相同IP地址的投票次数,可以防止同一IP地址的用户对同一选项进行多次投票。P地址限制可以通过服务器端脚本语言(如PHP、Python等)或者Web服务器(如Apache、Nginx等)来实现。


<?php
// 获取投票者的IP地址
$ip = $_SERVER['REMOTE_ADDR'];

// 设置IP地址的投票次数上限为10次
$vote_limit = 10;

// 判断IP地址的投票次数是否已经超过上限
if(get_votes_count($ip) >= $vote_limit){
// 如果超过了上限,提示投票失败
echo "投票失败,您已经超过投票次数限制!";
}else{
// 如果没有超过上限,进行投票操作
do_vote();
// 记录投票记录
record_vote($ip);
// 提示投票成功
echo "投票成功,谢谢您的支持!";
}

// 获取指定IP地址的投票次数
function get_votes_count($ip){
// 连接数据库
$conn = mysqli_connect('localhost', 'username', 'password', 'database');
// 查询指定IP地址的投票次数
$result = mysqli_query($conn, "SELECT COUNT(*) AS count FROM votes WHERE ip='$ip'");
// 获取投票次数
$row = mysqli_fetch_assoc($result);
$count = $row['count'];
// 关闭数据库连接
mysqli_close($conn);
return $count;
}

// 记录投票记录
function record_vote($ip){
// 连接数据库
$conn = mysqli_connect('localhost', 'username', 'password', 'database');
// 插入投票记录
mysqli_query($conn, "INSERT INTO votes (ip) VALUES ('$ip')");
// 关闭数据库连接
mysqli_close($conn);
}

// 进行投票操作
function do_vote(){
// TODO:进行投票操作
}
?>


3. Cookie限制: 通过在用户浏览器中设置Cookie,可以限制同一浏览器的用户对同一选项进行多次投票。


<?php
// 获取投票者的Cookie
$cookie_name = "voted";
$voted = isset($_COOKIE[$cookie_name]) ? $_COOKIE[$cookie_name] : 0;

// 设置Cookie的过期时间为1天
$expire_time = time() + 86400;

// 判断投票者是否已经投过票
if($voted){
// 如果已经投过票,提示投票失败
echo "投票失败,您已经投过票!";
}else{
// 如果还没有投票,进行投票操作
do_vote();
// 设置投票者的Cookie
setcookie($cookie_name, 1, $expire_time);
// 提示投票成功
echo "投票成功,谢谢您的支持!";
}

// 进行投票操作
function do_vote(){
// TODO:进行投票操作
}
?>


另外,在用户打开页面时,Cookie已经配置在用户浏览器中了,因为示例代码中的setcookie()函数会在服务器响应中设置Cookie并发送给客户端浏览器,当用户打开页面时,浏览器会检查本地是否存在该网站的Cookie,并将其附加在该请求中一同发送给服务器,以便服务器识别该用户的身份,或者保存一些用户相关的数据。因此,在示例代码中,当用户访问该网站时,Cookie已经生效并配置在用户浏览器中了。


4. 人工审核: 对投票结果进行人工审核,通过人工审核来确认投票者的身份和投票行为的真实性。但是,该方法需要投入较大的人力和时间成本,不适合大规模的投票活动。


结语


在2021年1月8日,国家互联网信息办公室公布《互联网信息服务管理办法(修订草案征求意见稿)》。对互联网信息发布、保存及个人信息安全保护等方面作出规定,并对日益泛滥的刷票、刷量、刷评论及制作虚假账号给出了处罚细则。其中特别第二十五条特提到,任何组织和个人不得以营利为目的或为获取其他非法利益,实施下列行为,扰乱网络秩序:
(一)明知是虚假信息而发布或者有偿提供信息发布服务的;
(二)为他人有偿提供删除、屏蔽、替换、下沉信息服务的;
(三)大量倒卖、注册并提供互联网信息服务账号,被用于违法犯罪的;
(四)从事虚假点击、投票、评价、交易等活动,破坏互联网诚信体系的。


希望在此基础上,网络刷票情况能够得到一定的缓解。<

作者:昀和
来源:juejin.cn/post/7225875600644735037
/p>

以上。

收起阅读 »

职场上有什么谎言?

努力干活就能赚多点钱 职场中最大的谎言可能是“工作越忙就能赚到更多的钱”。虽然在某些情况下这是真实的,但很多时候这只是一种误解。长时间超负荷工作可能会导致身体和心理健康问题,甚至影响到家庭生活和人际关系。此外,很多公司并不愿意在工作强度过高的员工身上支付额外的...
继续阅读 »

努力干活就能赚多点钱


职场中最大的谎言可能是“工作越忙就能赚到更多的钱”。虽然在某些情况下这是真实的,但很多时候这只是一种误解。长时间超负荷工作可能会导致身体和心理健康问题,甚至影响到家庭生活和人际关系。此外,很多公司并不愿意在工作强度过高的员工身上支付额外的报酬,而是更倾向于平衡员工的工作与生活,提高员工的幸福感和满意度。因此,新进职场人士应该认识到,在职场中坚持适度的工作量、良好的工作习惯和优秀的职业素养才是取得成功的重要因素。


我都是为你好


“我都是为你好”可能是一种常见的谎言,在不同情境下被使用。在某些情况下,这可能真诚地表达出对他人的关心和照顾,但在其他情况下,这也可能成为掩盖自己私人动机或者行为错误的借口。因此,在职场和日常生活中,我们需要学会审视这句话所蕴含的背后意图,并判断其是否真实可信。同时,我们也应该秉持着开放、坦诚、尊重和理解的态度,与他人进行良好的沟通和相处,以建立健康、和谐的人际关系。


他做得比你好,向他好好学习


“他做得比你好,向他好好学习”是一句非常有益的建议,可以让人们从成功的经验中汲取营养,不断提高自己的能力和水平。在职场中,人们面对不同的工作任务和挑战,而且每个人的工作方式、思维模式和经验都不同,因此,我们应该善于借鉴他人的优点和长处,吸取别人的经验和教训,不断完善自己的职业素养和技能。然而,这并不意味着要完全依赖和模仿别人,而是应该在合适的时机,根据自身实际情况和需要,加以改进和创新,开拓自己的专业视野和发展空间。


在职场中,有些人可能会通过拍马屁、拉关系等不正当手段来获取自己的利益或者提高自己的地位。然而,这种做法可能会导致负面后果和损失,例如破坏工作团队的合作氛围、损害自己的职业形象和信誉等。因此,我们应该始终保持清醒和冷静的头脑,不受拍马屁等诱惑,专注于自己的工作和职责,努力提高自己的专业水平和职业素养。同时,我们也应该与他人建立良好的人际关系,以合理、公正、透明的方式展示自己的才华和成果,赢得别人的尊重和信任,并在适当的时刻借助他人的力量来实现共同的目标。


公司不怎么赚钱,理解一下,行情好了加工资


如果公司在过去设定了一些目标和承诺,但无法兑现或者没有达到预期的结果,那么这就是一种失信行为。画饼充当推销手段,可能会对员工、客户和利益相关方造成误导和不良影响,并破坏公司的商誉和形象。因此,公司应该根据市场实际情况和自身能力水平,制定合理、可行的计划和策略,避免过于浮夸和虚幻的承诺,注重落实和执行,加强与员工、客户和社会各方的沟通和互动,建立坦诚、透明的企业文化和价值观念。同时,员工也应该保持客观、谨慎、理性的态度,不盲目追求高回报或者虚假宣传,始终以个人职业道德和职责为先,为公司和自

作者:象骑士
来源:juejin.cn/post/7213636024102469693
己的未来发展负责任。

收起阅读 »

糟了,生产环境数据竟然不一致,人麻了!

大家好,我是冰河~~ 今天发现Mysql的主从数据库没有同步 先上Master库: mysql>show processlist; 查看下进程是否Sleep太多。发现很正常。 show master status; 也正常。 mysql> sh...
继续阅读 »

大家好,我是冰河~~


今天发现Mysql的主从数据库没有同步


先上Master库:


mysql>show processlist;

查看下进程是否Sleep太多。发现很正常。


show master status;

也正常。


mysql> show master status;
+-------------------+----------+--------------+-------------------------------+
| File | Position | Binlog_Do_DB | Binlog_Ignore_DB |
+-------------------+----------+--------------+-------------------------------+
| mysqld-bin.000001 | 3260 | | mysql,test,information_schema |
+-------------------+----------+--------------+-------------------------------+
1 row in set (0.00 sec)

再到Slave上查看


mysql> show slave status\G                                                

Slave_IO_Running: Yes
Slave_SQL_Running: No

可见是Slave不同步


解决方案


下面介绍两种解决方法


方法一:忽略错误后,继续同步


该方法适用于主从库数据相差不大,或者要求数据可以不完全统一的情况,数据要求不严格的情况


解决:


stop slave;

#表示跳过一步错误,后面的数字可变
set global sql_slave_skip_counter =1;
start slave;

之后再用mysql> show slave status\G 查看


mysql> show slave status\G
Slave_IO_Running: Yes
Slave_SQL_Running: Yes

ok,现在主从同步状态正常了。。。


方式二:重新做主从,完全同步


该方法适用于主从库数据相差较大,或者要求数据完全统一的情况


解决步骤如下:


1.先进入主库,进行锁表,防止数据写入


使用命令:


mysql> flush tables with read lock;

注意:该处是锁定为只读状态,语句不区分大小写


2.进行数据备份


#把数据备份到mysql.bak.sql文件


mysqldump -uroot -p -hlocalhost > mysql.bak.sql

这里注意一点:数据库备份一定要定期进行,可以用shell脚本或者python脚本,都比较方便,确保数据万无一失。


3.查看master 状态


mysql> show master status;
+-------------------+----------+--------------+-------------------------------+
| File | Position | Binlog_Do_DB | Binlog_Ignore_DB |
+-------------------+----------+--------------+-------------------------------+
| mysqld-bin.000001 | 3260 | | mysql,test,information_schema |
+-------------------+----------+--------------+-------------------------------+
1 row in set (0.00 sec)

4.把mysql备份文件传到从库机器,进行数据恢复


scp mysql.bak.sql root@192.168.128.101:/tmp/

5.停止从库的状态


mysql> stop slave;

6.然后到从库执行mysql命令,导入数据备份


mysql> source /tmp/mysql.bak.sql

7.设置从库同步,注意该处的同步点,就是主库show master status信息里的| File| Position两项


change master to master_host = '192.168.128.100', master_user = 'rsync',  master_port=3306, master_password='', master_log_file =  'mysqld-bin.000001', master_log_pos=3260;

8.重新开启从同步


mysql> start slave;

9.查看同步状态


mysql> show slave status\G  

Slave_IO_Running: Yes
Slave_SQL_Running: Yes

10.回到主库并执行如下命令解除表锁定。


UNLOCK TABLES;

好了,今天就到这儿吧,小伙伴们点赞、收藏、评论,一键三连走起呀,我是冰河,我们下期见~

作者:冰_河
来源:juejin.cn/post/7221858081495203897
~

收起阅读 »

某用户说他付钱了订单状态未修改

背景 某项目中有一个收费业务,生成订单,订单状态为待支付。在移动端上打开该功能时可查看到待支付的订单,然后用户可以对待支付的订单进行支付。但偶尔出现客户反馈支付后订单还是待支付的状态,导致用户无法继续使用接下来的功能,导致用户的体验行特别差。重新梳理一遍,排查...
继续阅读 »



背景


某项目中有一个收费业务,生成订单,订单状态为待支付。在移动端上打开该功能时可查看到待支付的订单,然后用户可以对待支付的订单进行支付。但偶尔出现客户反馈支付后订单还是待支付的状态,导致用户无法继续使用接下来的功能,导致用户的体验行特别差。重新梳理一遍,排查该问题。


涉及服务:



  • server1 主服务业务,提供所有业务的服务;

  • m-server:提供移动端 H5 视图和部分业务功能,部分功能业务都直接请求 server1;


总体架构


2022-09-02-10-11-16-image.png


总体流程:



  • 用户打开移动端应用时,由 m-server 提供页面视图;

  • 移动端相关业务数据由 server1 提供,即用户请求时,会到 m- server 再由其转发到 server1 服务上;

  • 相关支付业务由 m-server 服务与微信支付交互,支付完成后再由 m-server 与 server1 交互,同步订单的状态;


详细支付时序图:


支付流程的时序图,可以参考微信的官网:pay.weixin.qq.com/wiki/doc/ap…


支付流程:



  • 用户点击支付时,向 m-server 发起支付,然后生成订单

  • 向微信支付发起生成预付单

  • 点击微信支付的支付,此时会与微信支付进行验证支付授权权限

  • 微信返回支付授权,然后用户输入密码,确认支付,向微信支付服务提交授权

  • 微信支付返回支付结果给用户,并发微信消息提醒,同时会向 m-server 异步通知支付结果

  • m-server 接收到支付结果将同步给 server ,然后server 变更订单状态结果

  • m-server 显示最后结果给用户,如支付成功的订单详情


订单状态同步设计:


2022-09-02-10-34-24-image.png


订单状态流程:



  • server1 生成订单并记录到 db 中

  • m-server 从 server 中获取到订单的列表

  • m-server 接收到微信支付成功时,就会告知 server 支付成功,然后由 server 将订单状态修改为已支付


问题分析


已支付成功了,但订单状态却还是未支付成功?


首先,订单的状态由待支付到支付成功,必须是由微信支付服务异步通知 m-server 支付成功,然后再由 m-server 通知 server1 去修改订单状态。


所以,无论 m-server 还是 server1 服务在支付期间发生抖动都可能导致支付成功的信息成功通知给 server1 ,从而导致订单状态修改失败。还有一种可能性,微信支付服务可能没有异步通知。毕竟是第三方发起通知,所以也可能发生未通知情况。


优化方案


为了保证订单状态最终结果状态一致性,需要增加服务高可用,且可以支持自动重新发送订单状态变更的请求,及时重发重试。


详细设计


2022-09-02-10-45-00-image.png



  • m-server 确认支付时也将订单信息进行存储,状态为待支付

  • m-server 接收到微信支付成功通知后,就转发告知server1 服务

  • server1 修改订单状态进行响应,m-server 接收到响应进行删除或者修改订单状态(可按需进行),m-server 这里订单信息没有用了就也可以删除

  • 同时开启一个异步轮询 m-server 存储的订单信息,对于订单状态是待支付的,进行重发重试。这个过程需要先和微信支付服务确认确实是已支付,然后再将信息重新发送 server1,告知将订单状态调整为已支付


// 定时轮询订单信息状态
func notifyAuto() {
// 异步定时监控订单状态的变化
var changeOrder = func() {
result := orderService.getUnPayOrder(0) // 获取当前未支付状态的订单
if len(result) > 0 {
for v := range result {
status := wechat.getOrderStatus(v.orderId)
if status == 1 { // 订单在微信上时已支付的,需重新调用server 修改订单状态
orderService.sendOrderFinish(v.orderId)
}
}
}
}

go func() {
time.AfterFunc(time.Minute*10, changeOrder)
}()
}
复制代码
作者:小雄Ya
来源:juejin.cn/post/7138609603356901413
>
收起阅读 »

1.0 除 0 没抛出错误,我差点被输送社会

简言 在项目中,我使用了错误的除法,除零没有抛出错误,程序运行正常,但是数据异常。组长说要把我输送社会,老板说还可以挽留一下,我们来复盘一下这次错误。 先让我们来试一试 public class TestDouble { public static v...
继续阅读 »

简言


在项目中,我使用了错误的除法,除零没有抛出错误,程序运行正常,但是数据异常。组长说要把我输送社会,老板说还可以挽留一下,我们来复盘一下这次错误。


先让我们来试一试


public class TestDouble {   
public static void main(String[] args) {
System.out.println(1.0 / 0);
}
}

你认为的我认为的它应该会抛出 ArithmeticException 异常


但是它现在输出了 Infinity



为什么呢?


Double 数据类型支持无穷大


还有其他类型支持吗?


有,还有 Float


下面我们来查看 Double 源码,可以看到


/** 
* 一个常数,保持类型的正无穷大
*/

public static final double POSITIVE_INFINITY = 1.0 / 0.0;
/**
* 一个常数,保持类型的负无穷大
*/

public static final double NEGATIVE_INFINITY = -1.0 / 0.0;
/**
* 一个常数,非数值类型
*/

public static final double NaN = 0.0d / 0.0;

下面来试验下 0.0/0 与 -1.0/0


Double 正无穷 = 1.0 / 0;
Double 负无穷 = -1.0 / 0;
System.out.println("正无穷:" + 正无穷);
System.out.println("负无穷:" + 负无穷);
Double 非数值 = 0.0 / 0;
System.out.println("非数值 0.0/0 ->" + 非数值);

输出:


正无穷:Infinity
负无穷:-Infinity
非数值 0.0/0 ->NaN

对无穷大进行运算


下面来测试对 Float 类型与 Doubloe 类型无穷大进行运算


public static void testFloatInfinity() {
Float infFloat = Float.POSITIVE_INFINITY;
Double infDouble = Double.POSITIVE_INFINITY;
System.out.println("infFloat + 5 = " + (infFloat + 5));
System.out.println("infFloat - infDouble = " + (infFloat - infDouble));
System.out.println("infFloat * -1 = " + (infFloat * -1));
}

输出:


infFloat + 5 = InfinityinfFloat - infDouble = NaNinfFloat * -1 = -Infinity

可以注意到 1,3 行运算符合我们的预计结果


ps: Infinity- Infinity 的结果不是数字类型


对这些值进行判断


public static void checkFloatInfinity() {   
Double 正无穷 = 1.0 / 0;
Double 负无穷 = -1.0 / 0;
Double 非数值 = 0.0 / 0;
System.out.println("判断正无穷: " + Double.isInfinite(正无穷));
System.out.println("判断负无穷: " + (Double.NEGATIVE_INFINITY == 负无穷));
System.out.println("判断非数值(==): " + (Double.NaN == 非数值));
System.out.println("判断非数值(isNaN): " + Double.isNaN(非数值));
}

输出:


判断正无穷: true
判断负无穷: true
判断非数值(==): false
判断非数值(isNaN): true


ps: 判断 NaN 不要使用 ==


作者:程序员鱼丸
来源:juejin.cn/post/7135621128818524174

收起阅读 »

谈谈国内前端的三大怪啖

web
因为工作的原因,我和一些外国前端开发有些交流。他们对于国内环境不了解,有时候会问出一些有趣的问题,大概是这些问题的启发,让我反复在思考一些更为深入的问题。 今天聊三个事情: 小程序 微前端 模块加载 小程序 每个行业都有一把银座,当坐上那把银座时,做什么...
继续阅读 »

因为工作的原因,我和一些外国前端开发有些交流。他们对于国内环境不了解,有时候会问出一些有趣的问题,大概是这些问题的启发,让我反复在思考一些更为深入的问题。


今天聊三个事情:



  • 小程序

  • 微前端

  • 模块加载


小程序



每个行业都有一把银座,当坐上那把银座时,做什么便都是对的。



“我们为什么需要小程序?”


第一次被问到这个问题,是因为一个法国的同事。他被派去做一个移动端业务,刚好那个业务是采用小程序在做。于是一个法国小哥就在被痛苦的中文文档和黑盒逻辑中来回折磨着 🤦。


于是,当我们在有一次交流中,他问出了我这个问题:我们为什么需要小程序?


说实话,我试图解释了 19 年国内的现状,以及微信小程序推出时候所带来的便利和体验等等。总之,在我看来并不够深刻的见解。


即便到现在为止,每次当我使用小程序的时候,依旧会复现这个问题。在 ChatGPT 11 月份出来的时候,我也问了它这个很有国内特色的问题:





看起来它回答的还算不错,至少我想如果它来糊弄那些老外,应该会比我做的更好些。


但如果扪心自问,单从技术上来讲。以上这些事情,一定是有其他方案能解决的。


所以从某种程度上来看,这更像是一场截胡的商业案例:


应用市场

全世界的互联网人都知道应用市场是非常有价值的事情,可以说操作系统最值钱的部分就在于他们构建了自己的应用市场。


只要应用在这里注册发行,雁过拔毛,这家公司就是互联网世界里的统治阶级,规则的制定者。


反之则需要受制于人,APP 做的再大,也只是应用市场里的一个应用,做的好与坏还得让应用商店的评判。


另外,不得不承认的是,一个庞大如苹果谷歌这样的公司,他们的应用商店对于普通国内开发者来说,确实是有门槛的。


在国内海量的 APP 需求来临之前,能否提供一个更低成本的解决方案,来消化这些公司的投资?


毕竟不是所有的小企业都需要 APP,其实他们大部分需求 Web 就可以解决,但是 Web 没牌面啊,做 Web 还得砸搜索的钱才有流量。(某度搜索又做成那样...)


那做操作系统?太不容易,那么多人都溺死在水里了,这水太深。


那有没有一种办法可以既能构建生态,又有 APP 的心智,还能给入驻企业提供流量?


于是,在 19 年夏天,深圳滨海大厦下的软件展业基地里,每天都在轮番播放着,做 XX小程序,拥抱下一个风口...


全新体验心智

小程序用起来挺方便的。


你有没有想过,这些美妙感觉的具体都来自哪些?以及这些真的是 Web 技术本身无法提供的吗?



  1. 靠谱感,每个小程序都有约束和规范,于是你可以将他们整整齐齐的陈放在你的列表里,仿佛你已经拥有了这一个个精心雕琢的作品,相对于一条条记不住的网页地址和鱼龙混杂的网页内容来说,这让你觉得小程序更加的有分量和靠谱。

  2. 安全感,沉浸式的头部,没有一闪而过的加载条,这一切无打扰的设计,都让你觉得这是一个在你本地的 APP,而不是随时可能丢失网页。你不会因为网速白屏而感到焦虑,尽管网络差的时候,你的 KFC 依旧下不了单 😂

  3. 沉浸感,我不知道是否打开网页时顶部黑黑的状态栏是故意留下的,还是不小心的... 这些限制都让用户非常强烈的意识到这是一个网页而不是 APP,而小程序中虽然上面也存在一个空间的空白,但是却可以被更加沉浸的主题色和氛围图替代。网页这个需求做不了?我不信。


H5小程序


  1. 顺滑感,得益于 Native 的容器实现,小程序在所有的视图切换时,都可以表现出于原生应用一样的顺滑感。其实这个问题才是在很多 Hybrid 应用中,主要想借助客户端来处理和解决的问题。类似容器预开、容器切换等技术是可以解决相关问题的,只是还没有一个标准。


我这里没有提性能,说实话我不认为性能在小程序中是有优势的(Native 调用除外,如地图等,不是一个讨论范畴)。作为普通用户,我们感受到的只是离线加载下带来的顺滑而已。


而上述提到的许多优势,这对于一个高品质的 Web 应用来说是可以做得到的,但注意这里是高品质的 Web 应用。而这种“高品质”在小程序这里,只是入驻门槛而已。


心智,这个词,听起来很黑话,但却很恰当。当小程序通过长期这样的筛选,所沉淀出来一批这样品质的应用时。就会让每个用户即便在还没打开一个新的小程序之前,也有不错体验的心理预期。这就是心智,一种感觉跟 Web 不一样,跟 APP 有点像的心智。


打造心智,这件事情好像就是国内互联网企业最擅长做的事情,总是能从一些细微的差别中开辟一条独立的领域,然后不断强化灌输本来就不大的差异,等流量起来再去捞钱变现。




我总是感觉现在的互利网充斥着如何赚钱的想法,好像永远赚不够。“赚钱”这个事情,在这些公司眼里就是圈人圈地抢资源,看看谁先占得先机,别人有的我也得有,这好像是最重要的事情。


很少有企业在思考如何创造些没有的市场,创造些真正对技术发展有贡献,对社会发展有推动作用的事情。所以在国内互联网圈也充斥着一种奇怪的价值观,有技术的不一定比赚过钱的受待见。


管你是 PHP 还是 GO,管你是在做游戏还是直播带货,只要赚到钱就是高人。并且端的是理所应当、理直气壮,有些老板甚至把拍摄满屋子的程序员为自己打工作为一种乐趣。什么情怀、什么优雅、什么愿景,人生就俩字:搞钱。


不是故意高雅,赚钱这件事情本身不寒碜,只是在已经赚到盆满钵满、一家独大的时候还在只是想着赚更多的钱,好像赚钱的目的就是为了赚钱一样,这就有点不合适。企业到一定程度是要有社会责任的,龙头企业每一个决定和举措,都有会影响接下来的几年里这个行业的价值观走向。



当然也不是完全没有好格局的企业,我非常珍惜每一个值得尊重的中国企业,来自一个蔚来车主。



小程序在商业上固然是成功的,但吃的红利可以说还是来自 网页 到 应用 的心智变革。将本来流向 APP 的红利,截在了小程序生态里。


但对于技术生态的发展却是带来了无数条新的岔路,小程序的玩法就决定了它必须生长在某个巨型应用里面,不论是用户数据获取、还是 API 的调用,其实都是取决于应用容器的标准规范。


不同公司和应用之间则必然会产生差异,并且这种差异是墒增式的差异,只会随着时间的推移越变越大。如果每个企业都只关注到自己业务的增长,无外部约束的话,企业必然会根据自己的业务发展和政策需要,选择成本较低的调整 API,甚至会故意制造一些壁垒来增加这种差异。


小程序,应该是 浏览器 与 操作系统 的融合,这本应该是推动这两项技术操刀解决的事情。


微前端


qiankun、wujie、single-spa 是近两年火遍前端的技术方案,同样一个问题:我们为什么需要微前端?


我不确定是否每个在使用这项技术的前端都想清楚了这个问题,但至少在我面试过的候选人中,我很少遇到对自己项目中已经在使用的微前端,有很深的思考和理解的人。


先说下我的看法:



  1. 微前端,重在解决项目管理而不在用户体验。

  2. 微前端,解决不了该优化和需要规范的问题。

  3. 微前端,在挽救没想清楚 MPA 的 SPA 项目。


没有万能银弹



银色子弹(英文:Silver Bullet),或者称“银弹”“银质子弹”,指由纯银质或镀银的子弹。在欧洲民间传说及19世纪以来哥特小说风潮的影响下,银色子弹往往被描绘成具有驱魔功效的武器,是针对狼人、吸血鬼等超自然怪物的特效武器。后来也被比喻为具有极端有效性的解决方法,作为杀手锏、最强杀招、王牌等的代称。



所有技术的发展都是建立在前一项技术的基础之上,但技术依赖的选择过程中一定需要保留第一性原理的意识。


当 React、Vue 兴起,当 打包技术(Webpack) 兴起,当 网页应用(SPA) 兴起,这些杰出的技术突破都在不同场景和领域中给行业提供了新的思路、新的方案。


不知从何时开始,前端除了 div 竟说不出其他的标签(还有说 View 的),项目中再也不会考虑给一个通用的 class 解决通用样式问题。


不知从何时开始,有没有权限需要等到 API 请求过后才知道,没有权限的话再把页面跳转过去申请。


不知从何时开始,大家的页面都放在了一个项目里,两个这样的巨石应用融合竟然变成了一件困难的事。


上面这些不合理的现状,都是在不同的场景下,不思考适不适合,单一信奉 “一招吃遍天” 下演化出的问题。


B 端应用,是否应该使用 SPA? 这其实是一个需要思考的问题。


微前端从某种程度上来讲,是认准 SPA 了必须是互联网下一代应用标准的产物,好像有了 SPA 以后,MPA 就变得一文不值。甭管页面是移动端的还是PC的;甭管页面是面对 C 端的还是 B 端的;甭管一个系统是有 20 个页面还是 200 个页面,一律行这套逻辑。


SPA 不是万能银弹,React 不是万能银弹,Tailwind 不是万能银弹。在新技术出现的时候,保持热情也要保持克制。



ps. 我也十分痛恨 React 带来的这种垄断式的生态,一个 React 组件将 HTML 和 Style 都吃进去,让你即使在做一个移动端的纯展示页面时,也需要背上这些称重的负担。



质疑 “墨守成规”,打开视野,深度把玩,理性消费。


分而治之


分治法,一个很基本的工程思维。


在我看来在一个正常商业迭代项目中的主要维护者,最好不要超过 3 个人,注意是主要维护者(Maintainer) 。


你应该让每个项目都有清晰的责任人,而不是某行代码,某个模块。责任人的理解是有归属感,有边界感的那种,不是口头意义上的责任人。(一些公司喜欢搞这种虚头巴脑的事情,什么连坐…)


我想大部分想引入微前端的需求都是类似 如何更好的划分项目边界,同时保留更好的团队协同。


比如 导航菜单 应该是独立收口独立管理的,并且当其更新时,应该同时应用于所有页面中。类似的还有 环境变量、时区、主题、监控及埋点。微前端将这些归纳在主应用中。


而具体的页面内容,则由对应的业务进行开发子应用,最后想办法将路由关系注册进主应用即可。


当然这样纯前端的应用切换,还会出现不同应用之间的全局变量差异、样式污染等问题,需要提供完善的沙箱容器、隔离环境、应用之间通信等一系列问题,这里不展开。


当微前端做到这一部分的时候,我不禁在想,这好像是在用 JavaScript 实现一个浏览器的运行容器。这种本应该浏览器本身该做的事情,难道 JS 可以做的更好?


只是做到更好的项目拆分,组织协同的话,引入后端服务,由后端管控路由表和页面规则,将页面直接做成 MPA,这个方案或许并不比引入微前端成本高多少。


体验差异


从 SPA 再回 MPA,说了半天不又回去了么。


所以不防想想:在 B端 业务中使用 SPA 的优势在哪里?


流畅的用户体验:

这个话题其实涵盖范围很广,对于 SPA 能带来的 “流畅体验”,对于大多数情况下是指:导航菜单不变,内容变化 发生变化,页面切换中间不出现白屏


但要做到这个点,其实对于 MPA 其实并没有那么困难,你只需要保证你的 FCP 在 500ms 以内就行。



以上的页面切换全部都是 MPA 的多页面切换,我们只是简单做了导航菜单的 拆分 和 SWR,并没有什么特殊的 preload、prefetch 处理,就得到了这样的效果。


因为浏览器本身在页面切换时会在 unload 之前先 hold 当前页面的视图不变,发起一下一个 document 的请求,当页面的视图渲染做到足够快和界面结构稳定就可以得到这样的效果。



这项浏览器的优化手段我找了很久,想找一篇关于它的博文介绍,但确实没找到相关内容,所以 500ms 也是我的一个大致测算,如果你知道相关的内容,可以在评论区补充,不胜感激。



所以从这个角度来看,浏览器本身就在尽最大的努力做这些优化,并且他们的优化会更底层、更有效的。


离线访问 (PWA)

SPA 确实会有更好的 PWA 组织能力,一个完整的 SPA 应用甚至可以只针对编译层做改动就可以支持 PWA 能力。


但如果看微前端下的 SPA 应用,需要支持 PWA 那就同样需要分析各子应用之间的元数据,定制 Service Worker。这种组织关系和定制 SW,对于元数据对于数据是来自前端还是后端,并不在意。


也就是说微前端模式下的 PWA,同样的投入成本,把页面都管理在后端服务中的 MPA 应用也是可以做到相同效果的。


项目协同、代码复用

有人说 SPA 项目下,项目中的组件、代码片段是可以相互之间复用的,在 MPA 下就相对麻烦。


这其实涉及到项目划分的领域,还是要看具体的需求也业务复杂度来定。如果说整个系统就是二三十个页面,这做成 SPA 使用前端接管路由高效简单,无可厚非。


但如果你本身在面对的是一个服务于复杂业务的 B 端系统,比如类似 阿里云、教务系统、ERP 系统或者一些大型内部系统,这种往往需要多团队合作开发。这种时候就需要跟完善的项目划分、组织协同和系统整合的方案。


这个时候 SPA 所体现出的优势在这样的诉求下就会相对较弱,在同等投入的情况下 MPA 的方案反而会有更少的执行成本。



也不是所有项目一开始就会想的那么清楚,或许一开始的时候就是个简单的 SPA 项目,但是随着项目的不断迭代,才变成了一个个复杂的巨石应用,现在如果再拆出来也会有许多迁移成本。引入微前端,则可以...



这大概是许多微前端项目启动的背景介绍,我想说的是:对于屎山,我从来不信奉“四两拨千斤”


如果没有想好当下的核心问题,就引入新的“银弹”解决问题,只会是屎山雕花。


项目协同,抽象和复用这些本身不是微前端该解决的问题,这是综合因素影响下的历史背景问题。也是需要一个个资深工程师来把控和解决的核心问题,就是需要面对不同的场景给出不同的治理方案。


这个道理跟防沙治沙一样,哪有那么多一蹴而就、立竿见影的好事。


模块加载


模块加载这件事情,从玉伯大佬的成名作 sea.js 开始就是一个非常值得探讨的问题。在当时 jQuery 的时代里,这是一个绝对超前的项目,我也在实际业务中体会过在无编译的环境下 sea.js 的便捷。



实际上,不论是微前端、低代码、会场搭建等热门话题离不开这项技术基础。


import * from * 我们每天都在用,但最终的产物往往是一个自运行的 JS Bundle,这来自于 Webpack、Vite 等编译技术的发展。让我们可以更好的组织项目结构,以构建更复杂的前端应用。


模块的概念用久了,就会自然而然的在遇到浏览器环境中,遇到动态模块加载的需求时,想到这种类似模块加载的能力。


比如在遇到会场千奇百怪的个性化营销需求时,能否将模块的 Props 开放出来,给到非技术人员,以更加灵活的方式让他们去做自由组合。


比如在低代码平台中,让开发者自定义扩展组件,动态的将开发好的组件注册进低代码平台中,以支持更加个性的需求。


在万物皆组件的思想影响下,把一整个完整页面都看做一个组件也不是不可以。于是在一些团队中,甚至提倡所有页面都可以搭建、搭建不了的就做一个大的页面组件。这样及可以减少维护运行时的成本,又可以统一管控和优化,岂不美哉。


当这样大一统的“天才方案”逐渐发展成为标准后,也一定会出现一些特殊场景无法使用,但没关系,这些天才设计者肯定会提供一种更加天才的扩展方案出来。比如插件,比如扩展,比如 IF ELSE。再后来,就会有性能优化了,而优化的 追赶对象 就是用原来那种简单直出的方案。


有没有发现,这好像是在轮回式的做着自己出的数学题,一道接一道,仿佛将 1 + 1的结论重新演化了一遍。



题外话,我曾经自己实现过一套通过 JSON Schema 描述 React 结构的 “库” ,用于一个低代码核心层的渲染。在我的实现过程中,我越发觉得我在做一件把 JSX 翻译成 JS 的事情,但 JSX 或者 HTML 本身不就是一种 DSL 么。为什么一定要把它翻译成 JSON 来传输呢?或者说这样的封装其本身有意义么?这不就是在做 PHP、ASP 直接返回带有数据的 HTML Ajax 一样的事情么。




传统的浏览器运行环境下要实现一个模块加载器,无非是在全局挂载一个注册器,通过 Script 插入一段新的 JS,该 JS 通过特殊的头尾协议,将运行时的代码声明成一个函数,注册进事先挂载好的注册器。


但实际的实现场景往往要比这复杂的多,也有一些问题是这种非原生方式无法攻克的问题。比如全局注册器的命名冲突;同模块不同版本的加载冲突;并发加载下的时序问题;多次模块加载的缓存问题 等等等等等...


到最后发现,这些事情好像又是在用 JS 做浏览器该做的事情。然而浏览器果然就做了,<script type="module"></script>,Vite 就主要采用这种模式实现了它 1 年之内让各大知名项目切换到 Vite 的壮举。


“但我们用不了,有兼容性问题。”


哇哦,当我看着大家随意写出的 display: grid 样式定义,不禁再次感叹人们对未知的恐惧。



import.meta 的兼容性是另外一个版本,是需要 iOS12 以上,详情参考:caniuse.com/?search=imp…


试想一下,现在的低代码、会场搭建等等各类场景的模块加载部分,如果都直接采用 ESM 的形式处理,这对于整个前端生态和开发体验来说会有多么大的提升。


模块加载,时至今日,本来就已经不再需要 loader。 正如 seajs 中写的:前端模块化开发那点历史



历史不是过去,历史正在上演。随着 W3C 等规范、以及浏览器的飞速发展,前端的模块化开发会逐步成为基础设施。一切终究都会成为历史,未来会更好。



结语


文章的结尾,我想感叹另外一件事,国人为什么一定要有自己的操作系统?为什么一定需要参与到一些规范的制定中?


因为我们的智慧需要有开花的土壤。如果没有自己掌握核心技术,就是只能在问题出现的时候用另类的方式来解决。最后在一番折腾后,发现更底层的技术只要稍稍一改就可以实现的更好。这就像三体中提到的 “智子” 一样,不断在影响着我们前进的动力和方向。


不论是小程序、微前端还是模块加载。试想一下,如果我们有自己的互联网底蕴,能决定或者影响操作系统和浏览器的底层能力。这些 “怪啖” 要么不会出现,要么就是人类的科技创新。



希望未来技术人不用再追逐 Write Once, Run Everywhere 的事情...



作者:YeeWang
来源:juejin.cn/post/7267091810366488632
收起阅读 »

刚来公司就接了一个不发版直接改代码的需求

前言 前几天突然接到一个技术需求,想要做一个功能。前端有一个表单,在页面上可以直接写 java 代码,写完后就能保存到数据库,并且这个代码实时生效。这岂非是不用发版就可以随时改代码了吗?而且有bug也不怕,随时改。 适用场景:代码逻辑需要经常变动的业务。 核...
继续阅读 »

前言


前几天突然接到一个技术需求,想要做一个功能。前端有一个表单,在页面上可以直接写 java 代码,写完后就能保存到数据库,并且这个代码实时生效。这岂非是不用发版就可以随时改代码了吗?而且有bug也不怕,随时改。



适用场景:代码逻辑需要经常变动的业务。


核心思想



  • 页面改动 java 代码字符串

  • java 代码字符串编译成 class

  • 动态加载到 jvm



实现重点


JDK 提供了一个工具包 javax.tools 让使用者可以用简易的 API 进行编译。



这些工具包的使用步骤:



  1. 获取一个 javax.tools.JavaCompiler 实例。

  2. 基于 Java 文件对象初始化一个编译任务 CompilationTask 实例。

  3. 因为JVM 里面的 Class 是基于 ClassLoader 隔离的,所以编译成功之后可以通过自定义的类加载器加载对应的类实例

  4. 使用反射 API 进行实例化和后续的调用。


1. 代码编译


这一步需要将 java 文件编译成 class,其实平常的开发过程中,我们的代码编译都是由 IDEA、Maven 等工具完成。


内置的 SimpleJavaFileObject 是面向源码文件的,而我们的是源码字符串,所以需要实现 JavaFileObject 接口自定义一个 JavaFileObject。


public class CharSequenceJavaFileObject extends SimpleJavaFileObject {

public static final String CLASS_EXTENSION = ".class";

public static final String JAVA_EXTENSION = ".java";

private static URI fromClassName(String className) {
try {
return new URI(className);
} catch (URISyntaxException e) {
throw new IllegalArgumentException(className, e);
}
}

private ByteArrayOutputStream byteCode;
private final CharSequence sourceCode;

public CharSequenceJavaFileObject(String className, CharSequence sourceCode) {
super(fromClassName(className + JAVA_EXTENSION), Kind.SOURCE);
this.sourceCode = sourceCode;
}

public CharSequenceJavaFileObject(String fullClassName, Kind kind) {
super(fromClassName(fullClassName), kind);
this.sourceCode = null;
}

public CharSequenceJavaFileObject(URI uri, Kind kind) {
super(uri, kind);
this.sourceCode = null;
}

@Override
public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
return sourceCode;
}

@Override
public InputStream openInputStream() {
return new ByteArrayInputStream(getByteCode());
}

// 注意这个方法是编译结果回调的OutputStream,回调成功后就能通过下面的getByteCode()方法获取目标类编译后的字节码字节数组
@Override
public OutputStream openOutputStream() {
return byteCode = new ByteArrayOutputStream();
}

public byte[] getByteCode() {
return byteCode.toByteArray();
}
}

如果编译成功之后,直接通过 CharSequenceJavaFileObject#getByteCode()方法即可获取目标类编译后的字节码对应的字节数组(二进制内容)


2. 实现 ClassLoader


因为JVM 里面的 Class 是基于 ClassLoader 隔离的,所以编译成功之后得通过自定义的类加载器加载对应的类实例,否则是加载不了的,因为同一个类只会加载一次。


主要关注 findClass 方法


public class JdkDynamicCompileClassLoader extends ClassLoader {

public static final String CLASS_EXTENSION = ".class";

private final static Map<String, JavaFileObject> javaFileObjectMap = new ConcurrentHashMap<>();

public JdkDynamicCompileClassLoader(ClassLoader parentClassLoader) {
super(parentClassLoader);
}


@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
JavaFileObject javaFileObject = javaFileObjectMap.get(name);
if (null != javaFileObject) {
CharSequenceJavaFileObject charSequenceJavaFileObject = (CharSequenceJavaFileObject) javaFileObject;
byte[] byteCode = charSequenceJavaFileObject.getByteCode();
return defineClass(name, byteCode, 0, byteCode.length);
}
return super.findClass(name);
}

@Override
public InputStream getResourceAsStream(String name) {
if (name.endsWith(CLASS_EXTENSION)) {
String qualifiedClassName = name.substring(0, name.length() - CLASS_EXTENSION.length()).replace('/', '.');
CharSequenceJavaFileObject javaFileObject = (CharSequenceJavaFileObject) javaFileObjectMap.get(qualifiedClassName);
if (null != javaFileObject && null != javaFileObject.getByteCode()) {
return new ByteArrayInputStream(javaFileObject.getByteCode());
}
}
return super.getResourceAsStream(name);
}

/**
* 暂时存放编译的源文件对象,key为全类名的别名(非URI模式),如club.throwable.compile.HelloService
*/

void addJavaFileObject(String qualifiedClassName, JavaFileObject javaFileObject) {
javaFileObjectMap.put(qualifiedClassName, javaFileObject);
}

Collection<JavaFileObject> listJavaFileObject() {
return Collections.unmodifiableCollection(javaFileObjectMap.values());
}
}

3. 封装了上面的 ClassLoader 和 JavaFileObject


public class JdkDynamicCompileJavaFileManager extends ForwardingJavaFileManager<JavaFileManager> {

private final JdkDynamicCompileClassLoader classLoader;
private final Map<URI, JavaFileObject> javaFileObjectMap = new ConcurrentHashMap<>();

public JdkDynamicCompileJavaFileManager(JavaFileManager fileManager, JdkDynamicCompileClassLoader classLoader) {
super(fileManager);
this.classLoader = classLoader;
}

private static URI fromLocation(Location location, String packageName, String relativeName) {
try {
return new URI(location.getName() + '/' + packageName + '/' + relativeName);
} catch (URISyntaxException e) {
throw new IllegalArgumentException(e);
}
}

@Override
public FileObject getFileForInput(Location location, String packageName, String relativeName) throws IOException {
JavaFileObject javaFileObject = javaFileObjectMap.get(fromLocation(location, packageName, relativeName));
if (null != javaFileObject) {
return javaFileObject;
}
return super.getFileForInput(location, packageName, relativeName);
}

/**
* 这里是编译器返回的同(源)Java文件对象,替换为CharSequenceJavaFileObject实现
*/

@Override
public JavaFileObject getJavaFileForOutput(Location location, String className, JavaFileObject.Kind kind, FileObject sibling) throws IOException {
JavaFileObject javaFileObject = new CharSequenceJavaFileObject(className, kind);
classLoader.addJavaFileObject(className, javaFileObject);
return javaFileObject;
}

/**
* 这里覆盖原来的类加载器
*/

@Override
public ClassLoader getClassLoader(Location location) {
return classLoader;
}

@Override
public String inferBinaryName(Location location, JavaFileObject file) {
if (file instanceof CharSequenceJavaFileObject) {
return file.getName();
}
return super.inferBinaryName(location, file);
}

@Override
public Iterable<JavaFileObject> list(Location location, String packageName, Set<JavaFileObject.Kind> kinds, boolean recurse) throws IOException {
Iterable<JavaFileObject> superResult = super.list(location, packageName, kinds, recurse);
List<JavaFileObject> result = new ArrayList<>();
// 这里要区分编译的Location以及编译的Kind
if (location == StandardLocation.CLASS_PATH && kinds.contains(JavaFileObject.Kind.CLASS)) {
// .class文件以及classPath下
for (JavaFileObject file : javaFileObjectMap.values()) {
if (file.getKind() == JavaFileObject.Kind.CLASS && file.getName().startsWith(packageName)) {
result.add(file);
}
}
// 这里需要额外添加类加载器加载的所有Java文件对象
result.addAll(classLoader.listJavaFileObject());
} else if (location == StandardLocation.SOURCE_PATH && kinds.contains(JavaFileObject.Kind.SOURCE)) {
// .java文件以及编译路径下
for (JavaFileObject file : javaFileObjectMap.values()) {
if (file.getKind() == JavaFileObject.Kind.SOURCE && file.getName().startsWith(packageName)) {
result.add(file);
}
}
}
for (JavaFileObject javaFileObject : superResult) {
result.add(javaFileObject);
}
return result;
}

/**
* 自定义方法,用于添加和缓存待编译的源文件对象
*/

public void addJavaFileObject(Location location, String packageName, String relativeName, JavaFileObject javaFileObject) {
javaFileObjectMap.put(fromLocation(location, packageName, relativeName), javaFileObject);
}
}

4. 使用 JavaCompiler 编译并反射生成实例对象


public final class JdkCompiler {

static DiagnosticCollector<JavaFileObject> DIAGNOSTIC_COLLECTOR = new DiagnosticCollector<>();

@SuppressWarnings("unchecked")
public static <T> T compile(String packageName,
String className,
String sourceCode) throws Exception {
// 获取系统编译器实例
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
// 设置编译参数
List<String> options = new ArrayList<>();
options.add("-source");
options.add("1.8");
options.add("-target");
options.add("1.8");
// 获取标准的Java文件管理器实例
StandardJavaFileManager manager = compiler.getStandardFileManager(DIAGNOSTIC_COLLECTOR, null, null);
// 初始化自定义类加载器
JdkDynamicCompileClassLoader classLoader = new JdkDynamicCompileClassLoader(Thread.currentThread().getContextClassLoader());

// 初始化自定义Java文件管理器实例
JdkDynamicCompileJavaFileManager fileManager = new JdkDynamicCompileJavaFileManager(manager, classLoader);
String qualifiedName = packageName + "." + className;
// 构建Java源文件实例
CharSequenceJavaFileObject javaFileObject = new CharSequenceJavaFileObject(className, sourceCode);
// 添加Java源文件实例到自定义Java文件管理器实例中
fileManager.addJavaFileObject(
StandardLocation.SOURCE_PATH,
packageName,
className + CharSequenceJavaFileObject.JAVA_EXTENSION,
javaFileObject
);
// 初始化一个编译任务实例
JavaCompiler.CompilationTask compilationTask = compiler.getTask(
null,
fileManager,
DIAGNOSTIC_COLLECTOR,
options,
null,
Collections.singletonList(javaFileObject)
);
Boolean result = compilationTask.call();
System.out.println(String.format("编译[%s]结果:%s", qualifiedName, result));
Class<?> klass = classLoader.loadClass(qualifiedName);
return (T) klass.getDeclaredConstructor().newInstance();
}
}

完成上面工具的搭建之后。我们可以接入数据库的操作了。数据库层面省略,只展示 service 层


service 层:


public class JavaService {

public Object saveAndGetObject(String packageName,String className,String javaContent) throws Exception {
Object object = JdkCompiler.compile(packageName, className, javaContent);
return object;
}

}

测试:


public class TestService {

public static void main(String[] args) throws Exception {
test();
}

static String content="package cn.mmc;\n" +
"\n" +
"public class SayHello {\n" +
" \n" +
" public void say(){\n" +
" System.out.println(\"11111111111\");\n" +
" }\n" +
"}";

static String content2="package cn.mmc;\n" +
"\n" +
"public class SayHello {\n" +
" \n" +
" public void say(){\n" +
" System.out.println(\"22222222222222\");\n" +
" }\n" +
"}";

public static void test() throws Exception {
JavaService javaService = new JavaService();
Object sayHello = javaService.saveAndGetObject("cn.mmc", "SayHello", content);
sayHello.getClass().getMethod("say").invoke(sayHello);

Object sayHello2 = javaService.saveAndGetObject("cn.mmc", "SayHello", content2);
sayHello2.getClass().getMethod("say").invoke(sayHello2);
}
}

我们在启动应用时,更换了代码文件内存,然后直接反射调用对象的方法。执行结果:


可以看到,新的代码已经生效!!!



注意,直接开放修改代码虽然方便,但是一定要做好安全防护


作者:女友在高考
来源:juejin.cn/post/7134155429147312141

收起阅读 »

你身边的那些'加班文化'

说起来加班,别管什么80后,90后,00后,内心肯定都是非常抵制的,朝九晚六,平衡工作生活,想必是大多数人的梦想。 为什么突然想写一篇这样的文章,这两天确实有几件事情触动了我。 1.同事猝死 曾任职的一家公司,一名同事于4月1日猝死。以前总是看网上那些猝死的案...
继续阅读 »

说起来加班,别管什么80后,90后,00后,内心肯定都是非常抵制的,朝九晚六,平衡工作生活,想必是大多数人的梦想。

为什么突然想写一篇这样的文章,这两天确实有几件事情触动了我。


1.同事猝死


曾任职的一家公司,一名同事于4月1日猝死。以前总是看网上那些猝死的案例,感慨加班猝死,不曾想这件事生生的发生在自己身边,这名同事是一名销售,具体猝死缘由(前)公司并没有多谈,但我想一定是和熬夜加班息息相关。


image.png


这家公司的加班文化,我深有体会,去年11月初,各个项目组长在工作群先后通知强制要求加班,并不给你机会以及理由请假,反正加班给出的理由就是冲刺冲刺。


1.png


当时有同事用dingding和其他同事讨论加班的各种不情愿,聊天记录居然被公司扒出来并认为其散播负面言论,邮件全体对其进行警告,我大为震惊,当然这名同事后来也进入了裁员名单。


4.png


警告信发出之后越来越多的同事周末有一天不能来公司办公,其实他们在家根本也没闲着,这时候大老板急了,又发了这样的一封邮件。


5.png


(我看到这封邮件的时候,心里的想法就是作为公司的领头人,这格局不是一般的小,仅代表个人意见)

显然这封邮件是给那些不坚持10 10 7的人看的,对于那些不加班的人等待着的自然是优化。就这样还是有些同事继续坚持着10 10 7,在元旦之前大家终于把所有项目全部成功交付,本以为等待着大家的是老板分享喜悦的成果,结果等来的却是3/2的裁员,后来大家想明白了,就是催促着大家感觉把项目做完,裁掉你们。其中有位同事,前一天通宵加完班,第二天就被裁了,那时候整个办公室一片哗然。


2.持续加班住进icu


image.png


image.png


朋友的同事,据说刚刚升为一名奶爸,每天十点前不曾下班,大家问他为何这么拼,给出的理由是我刚刚生了娃,买了房子,工作一点不敢懈怠,怕惹得领导不高兴随时让我滚蛋。

是啊,谁想加班,谁愿意加班天天守着个破电脑,可是生活让我们不得不加班,不得不向一些不合理的规定屈服。


3.中国电科龙哥


这两天龙哥火遍了各个社交网络,龙哥痛批强制加班员工事情为什么能火,为什么传播速度这么快,我猜测是龙哥一人之言说出了很多打工人的心声,引起网友一片共鸣。


不过央视网的这文字怕是惹得大家一顿p喽。


720635d7223ec0b82485f989ae38034.jpg


职场人偶尔加下班,没有任何问题,毕竟工作中肯定会遇到一些紧急事件需要及时处理,作为公司员工,加班赶下进度也是应该的,可是长期被自愿加班就不合时宜了,毕竟身体是革命的本钱,一旦因为长期加班,出现问题就得不偿失了,所以加班文化不可取,尤其是超

作者:zhouzhouya
来源:juejin.cn/post/7218735340043092028
长加班更不应该存在。

收起阅读 »

项目中前端如何实现无感刷新 token!

前一阵遇到了一个问题,线上平台有时会出现用户正在使用的时候,突然要用户去进行登录,这样会造成很不好的用户体验,但是当时一直没有好的思路因此搁置了下来;通过零散时间查询资料以及思考,最终解决了这个问题,接下来跟大家分享一下! 环境请求采用的 Axios V1.3...
继续阅读 »

前一阵遇到了一个问题,线上平台有时会出现用户正在使用的时候,突然要用户去进行登录,这样会造成很不好的用户体验,但是当时一直没有好的思路因此搁置了下来;通过零散时间查询资料以及思考,最终解决了这个问题,接下来跟大家分享一下!


环境

  1. 请求采用的 Axios V1.3.2。
  2. 平台的采用的 JWT(JSON Web Tokens) 进行用户登录鉴权。
    (拓展:JWT 是一种认证机制,让后台知道该请求是来自于受信的客户端;更详细的可以自行查询相关资料)

问题现象


线上用户在使用的时候,偶尔会出现突然跳转到登录页面,需要重新登录的现象。


原因

  1. 突然跳转到登录页面,是由于当前的 token 过期,导致请求失败;在 axios 的响应拦截axiosInstance.interceptors.response.use中处理失败请求返回的状态码 401,此时得知token失效,因此跳转到登录页面,让用户重新进行登录。
  2. 平台目前的逻辑是在 token 未过期内,用户登录平台可直接进入首页,无需进行登录操作;因此就存在该现象:用户打开平台,由于此时 token 未过期,用户直接进入到了首页,进行其他操作。但是在用户操作的过程中,token 突然失效了,此时就会出现突然跳转到登录页面,严重影响用户的体验感!
    注:目前线上项目中存在数据大屏,一些实时数据的显示;因此存在用户长时间停留在大屏页面,不进行操作,查看实时数据的情况

切入点

  1. 怎样及时的、在用户感知不到的情况下更新token
  2. 当 token 失效的情况下,出错的请求可能不仅只有一个;当失效的 token 更新后,怎样将多个失败的请求,重新发送?

操作流程


好了!经过了一番分析后,我们找到了问题的所在,并且确定了切入点;那么接下来让我们实操,将问题解决掉。

前要:

1、我们仅从前端的角度去处理。

2、后端提供了两个重要的参数:accessToken(用于请求头中,进行鉴权,存在有效期);refreshToken(刷新令牌,用于更新过期的 accessToken,相对于 accessToken 而言,它的有效期更长)。


1、处理 axios 响应拦截


注:在我实际的项目中,accessToken 过期后端返回的 statusCode 值为 401,需要在axiosInstance.interceptors.response.useerror回调中进行逻辑处理

// 响应拦截
axiosInstance.interceptors.response.use(
(response) => {
return response;
},
(error) => {
let {
data, config
} = error.response;
return new Promise((resolve, reject) => {
/**
* 判断当前请求失败
* 是否由 toekn 失效导致的
*/
if (data.statusCode === 401) {
/**
* refreshToken 为封装的有关更新 token 的相关操作
*/
refreshToken(() => {
resolve(axiosInstance(config));
});
} else {
reject(error.response);
}
})
}
)


  1. 我们通过判断statusCode来确定,是否当前请求失败是由token过期导致的;

  2. 使用 Promise 处理将失败的请求,将由于 token 过期导致的失败请求存储起来(存储的是请求回调函数,resolve 状态)。理由:后续我们更新了 token 后,可以将存储的失败请求重新发起,以此来达到用户无感的体验


补充:


现象:在我过了几天登录平台的时候发现,refreshToken过期了,但是没有跳转到登录界面
原因

1、当refreshToken过期失效后,后端返回的状态码也是 401

2、发起的更新token的请求采用的也是处理后的axios,因此响应失败的拦截,对更新请求同样适用

问题:

这样会造成,当refreshToken过期后,会出现停留在首页,无法跳转到登录页面。

解决方法

针对这种现象,我们需要完善一下axios中响应拦截的逻辑

axiosInstance.interceptors.response.use(
(response) => {
return response;
},
(error) => {
let {
data, config
} = error.response;
return new Promise((resolve, reject) => {
/**
* 判断当前请求失败
* 是否由 toekn 失效导致的
*/
if (
data.statusCode === 401 &&
config.url !== '/api/token/refreshToken'
) {
refreshToken(() => {
resolve(axiosInstance(config));
});
} else if (
data.statusCode === 401 &&
config.url === '/api/token/refreshToken'
) {
/**
* 后端 更新 refreshToken 失效后
* 返回的状态码, 401
*/
window.location.href = `${HOME_PAGE}/login`;
} else {
reject(error.response);
}
})
}
)

2、封装 refreshToken 逻辑


要点:



  1. 存储由于token过期导致的失败的请求。
  2. 更新本地以及axios中头部的token
  3. 当 refreshToken 刷新令牌也过期后,让用户重新登录
// 存储由于 token 过期导致 失败的请求
let expiredRequestArr: any[] = [];

/**
* 存储当前因为 token 失效导致发送失败的请求
*/
const saveErrorRequest = (expiredRequest: () => any) => {
expiredRequestArr.push(expiredRequest);
}

// 避免频繁发送更新
let firstRequre = true;
/**
* 利用 refreshToken 更新当前使用的 token
*/
const updateTokenByRefreshToken = () => {
firstRequre = false;
axiosInstance.post(
'更新 token 的请求',
).then(res => {
let {
refreshToken, accessToken
} = res.data;
// 更新本地的token
localStorage.setItem('accessToken', accessToken);
// 更新请求头中的 token
setAxiosHeader(accessToken);
localStorage.setItem('refreshToken', refreshToken);

/**
* 当获取了最新的 refreshToken, accessToken 后
* 重新发起之前失败的请求
*/
expiredRequestArr.forEach(request => {
request();
})
expiredRequestArr = [];
}).catch(err => {
console.log('刷新 token 失败err', err);
/**
* 此时 refreshToken 也已经失效了
* 返回登录页,让用户重新进行登录操作
*/
window.location.href = `${HOME_PAGE}/login`;
})
}

/**
* 更新当前已过期的 token
* @param expiredRequest 回调函数,返回由token过期导致失败的请求
*/
export const refreshToken = (expiredRequest: () => any) => {
saveErrorRequest(expiredRequest);
if (firstRequre) {
updateTokenByRefreshToken();
}
}

补充:


问题:

1、怎么能保证当更新token后,在处理存储的过期请求时,此时没有过期请求还在存呢?;万一此时还在expiredRequestArr推失败的请求呢?

解决方法
我们需要调整一下更新 token的逻辑,确保当前由于过期失败的请求都接收到了,再更新token然后重新发起请求。


最终结果:

// refreshToken.ts

/**
* 功能:
* 用于实现无感刷新 token
*/
import { axiosInstance, setAxiosHeader } from "@/axios"
import { CLIENT_ID, HOME_PAGE } from "@/systemInfo"

// 存储由于 token 过期导致 失败的请求
let expiredRequestArr: any[] = [];

/**
* 存储当前因为 token 失效导致发送失败的请求
*/
const saveErrorRequest = (expiredRequest: () => any) => {
expiredRequestArr.push(expiredRequest);
}

/**
* 执行当前存储的由于过期导致失败的请求
*/
const againRequest = () => {
expiredRequestArr.forEach(request => {
request();
})
clearExpiredRequest();
}

/**
* 清空当前存储的过期请求
*/
export const clearExpiredRequest = () => {
expiredRequestArr = [];
}

/**
* 利用 refreshToken 更新当前使用的 token
*/
const updateTokenByRefreshToken = () => {
axiosInstance.post(
'更新请求url',
{
clientId: CLIENT_ID,
userName: localStorage.getItem('userName')
},
{
headers: {
'Content-Type': 'application/json;charset=utf-8',
'Authorization': 'bearer ' + localStorage.getItem("refreshToken")
}
}
).then(res => {
let {
refreshToken, accessToken
} = res.data;
// 更新本地的token
localStorage.setItem('accessToken', accessToken);
localStorage.setItem('refreshToken', refreshToken);
setAxiosHeader(accessToken);
/**
* 当获取了最新的 refreshToken, accessToken 后
* 重新发起之前失败的请求
*/
againRequest();
}).catch(err => {
/**
* 此时 refreshToken 也已经失效了
* 返回登录页,让用户重新进行登录操作
*/
window.location.href = `${HOME_PAGE}/login`;
})
}

let timer: any = null;
/**
* 更新当前已过期的 token
* @param expiredRequest 回调函数,返回过期的请求
*/
export const refreshToken = (expiredRequest: () => any) => {
saveErrorRequest(expiredRequest);
// 保证再发起更新时,已经没有了过期请求要进行存储
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
updateTokenByRefreshToken();
}, 500);
}
// 响应拦截 区分登录前
axiosInstance.interceptors.response.use(
(response) => {
return response;
},
(error) => {
let {
data, config
} = error.response;
return new Promise((resolve, reject) => {
/**
* 判断当前请求失败
* 是否由 toekn 失效导致的
*/
if (
data.statusCode === 401 &&
config.url !== '/api/token/refreshToken'
) {
refreshToken(() => {
resolve(axiosInstance(config));
});
} else if (
data.statusCode === 401 &&
config.url === '/api/token/refreshToken'
) {
/**
* 后端 更新 refreshToken 失效后
* 返回的状态码, 401
*/
clearExpiredRequest();
window.location.href = `${HOME_PAGE}/login`;
} else {
reject(error.response);
}
})
}
)

补充


感谢很多朋友提出了很多更好的方法;我写这篇文章主要是为了分享一下,恰好这种问题推到了我(前端工程师)身上,我是怎样处理的;虽然有可能在一些朋友看来很低级,但它确是我实际工作中碰到的问题,每一个问题的出现解决后都对自身是一种成长,通过分享的方式来巩固自己,也希望能对他人有一些帮助!


总结


经过一波分析以及操作,我们最终实现了实际项目中的无感刷新token,最主要的是有效避免了:用户在平台操作过程中突然要退出登录的现象(尤其是当用户进行信息填写,突然要重新登录,之前填写的信息全部作废,是很容易让人发狂的)。

其实回顾一下,技术上并没有什么难点,只是思路上自己是否能够想通、自洽。人是一棵会思想的芦苇,我们要有自己的思想,面对问题,有自己的思考。

希望我们能在技术的路上走的越来越远,与君共勉!!!


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

iOS 组件间通信,另一种与众不同的实现方式

iOS
本文已参与「新人创作礼」活动,一起开启掘金创作之路。 组件间通信,但凡大一点的项目都会做模块化开发,必然会遇到兄弟组件解耦、通信问题。 那如何不互相依赖模块,又可以相互传输消息呢?网上的方案是有很多了,比如:URL 路由target-actionprotoco...
继续阅读 »

本文已参与「新人创作礼」活动,一起开启掘金创作之路。


组件间通信,但凡大一点的项目都会做模块化开发,必然会遇到兄弟组件解耦、通信问题。


那如何不互相依赖模块,又可以相互传输消息呢?网上的方案是有很多了,比如:

  1. URL 路由
  2. target-action
  3. protocol


iOS:组件化的三种通讯方案 这篇写的挺不错,没了解的同学可以看一下



也有很多第三方组件代表,MGJRouterCTMediatorBeeHiveZIKRouter 等(排名不分前后[手动狗头])。


但他们或多或少都有各自的优缺点,这里也不展开说,但基本上的有这么几种问题:

  1. 使用起来比较繁琐,需要理解成本,开发起来也需要写很多冗余代码。
  2. 基本都需要先注册,再实现。那就无法保证代码一定存在实现,也无法保证实现是否跟注册出现不一致(当然你可以增加一些校验手段,比如静态检测之类的)。这一点在比较大型的项目里都是很痛的,要不就不敢删除历史代码来积债,要不就是莽过去,测试或者线上出现问题[手动狗头]。
  3. 如果存在 Model 需要传递,要不下沉到公共模块,要不就是转 NSDictionary。还是公共层积债或者模型变更导致运行时出问题。

那有没有银弹呢?这就是本次要讲的实现方式,换个角度解决问题。


与众不同的方案


通过上述的问题,想一下我们想要的实现是什么样:

  1. 不需要增加开发成本,也不需要理解整体的实现原理。
  2. 由组件提供方提供,先有实现再有定义,保证 API 是完全可用的,如果实现发生变更,调用方会编译时报错(问题暴露前置)。且其他模块不依赖但又可以准确调用到这个方法。
  3. 各类模型在模块内是正常使用的,且对外暴露也是可以正常使用的,但又不用去下沉在公共模块。

是不是感觉要求很过分?就像一个渣男既不想跟你结婚,又想跟你生孩子[手动狗头] 。


但能不能实现呢,确实是可以的。但解决办法不在 iOS 本身,而在 codegen。铺垫到这里,我们来看看具体实现。


GDAPI 原理


在笔者所在的稿定,之前用的是 CTMediator 方案做组件间通信,当然也就有上面的那些问题,甚至线上也出现过因为 Protocol 找不到 Mediator 导致的线上 crash。


为了解决定义和实现不匹配的问题,我们希望定义一定要有实现,实现一定要跟定义一致。


那是否就可以换个思路,先有实现,再有定义,从实现生成定义。


这点参考了 JAVA 的注解机制,我们定义了一个宏 GDM_EXPORT_MODULE(),用于说明哪些方法是需要开发给其他模块使用的。

// XXLoginManager.h

/// 判断是否登陆
- (BOOL)isLogin GDM_EXPORT_MODULE();

这样在组件开发方就完成了 API 开放,剩下的工作就是如何生成一个调用层代码。


调用层代码其实也就是 CTMediator 的翻版,通过 iOS 的运行时反射机制去寻找实现类

// XXService.m

static id<GDXXXAPI> _mXXXService = nil;
+ (id<GDXXXAPI>)XXXService {
if (_mXXXService == nil) {
_mXXXService = [self implementorOfName:@"GDXXXManager"];
}
return _mXXXService;
}

我们把这些生成的方法调用,生成到一个 GDAPI 模块统一存储,当然这个模块除了上述模块的 Service 层是要有具体的 .m 来做落地,其他都是 .h 的头文件。


那调用侧只需要 pod 增加依赖 s.dependency 'GDAPI/XXXXService' 即可调用到具体实现了

@import GDAPI;

...

bool isLogin = [GDAPI.XXService isLogin];


这里肯定有同学会问,生成过程呢???


笔者是用 Ruby 代码实现了整个 codegen 过程,当时没选择 Python 主要是为了跟 cocoapods 使用相同的开发语言,易于做侵入设计,但其实用其他语言都没问题,通过 shell 脚本做中转即可。




这里源码有些定制化实现,放出来现在也是徒增大家烦恼,所以讲一下生成关键过程:

  1. 遍历组件所在目录,取出所有的 .h 文件,缓存在 Map<文件路径,文件内容>(一级缓存)
  2. 解析存在 GDM_EXPORT_MODULE() 的方法,将方法的名称、参数、注释通过正则手段分解成相应的属性,存储到 Map<模块名,API 模型列表> (二级缓存)
  3. 对于每一个 API 模型进行进一步解析,解析入参和出参,判断参数类型是否为自定义类型(模型、代理、枚举、包括复杂的 NSArray<CustomModel *> * 等),如果有存在,则遍历一级缓存,找到自定义类型的定义,生成对应的 Model -> Procotol 等,且存储在多个 Map 中 Map<类名/代理名/枚举名,具体解析后的模型>(三级缓存)
  4. 有了 AST 生成就变得很简单,模版代码 + 模版输出即可


有了上述各种模型,就差不多完成了 AST (抽象语法树) 的生成过程,至于为什么是用的正则而不是 iOS 的 AST 工具,主要原因是想做的很轻,尽量减少大家的构建时长,不要通过编译来实现。0



可以看到已经有大量模块生成了相应的 GDAPI




执行时长在 2S 左右,因为有一个预执行的过程,来做组件项目化,这个也算是特殊实现了。
实质上执行也就 1S 即可。


还有一点要说的是执行时机是在 pod install / update 之前,这个是通过 hooks cocoapods 的执行过程做到的。


一些难点


嵌套模型


上面虽然粗略的讲了下 Model / Procotol 会生成 Protocol,但其实这一部分确实是最困难的,也是因为历史积债问题,下沉在公共模块的庞大的模型在各个组件里传输。


那要把它完全的 API 化,就需要对它的属性进行递归解析,生成完全符合的 protocol


例如:

... 举例为伪代码,OC 代码确实很啰嗦

class A extends B {
C c;

NSArray<D> d;
}

/// 测试
- (void)test:(A *)a GDM_EXPORT_MODULE();

生成结果就如下图(伪代码):


@protocol GDAPI_A {
NSObject<GDAPI_C> c;

NSArray<NSObject<GDAPI_D>> d;
}

@protocol GDAPI_B {
}

@protocol GDAPI_C {
}

@protocol GDAPI_D {
}

以及调用服务

@protocol GDXXXAPI <NSObject>
/// 测试
- (void)test:(NSObject<GDAPI_A, GDAPI_B>)a;


这个在落地过程中坑确实非常多。


B 模块想创建 A 模块的模型


当然这个是很不合理的,但现实中确实很多这样的历史问题。


当然也不能用模型下沉开倒车,那解决上用了一个巧劲

/// 创建 XX
- (XXXModel *)createXXX GDM_EXPORT_MODULE();

提供一个创建模型的 API 给外部使用,这样对于 Model 的管理还是在模块内,外部模块使用上从 new XXX() 改为 [GDAPI.XXService createXX]; 即可。


零零碎碎


用正则判断抓取 AST,在一些二三方库中也是很常见的,但来处理 OC 确实挺痛苦的,再加上历史代码很多没什么规范,空格、注释各式各样,写个通用的适配算是比较耗时的。


还有就是一些个性化的兼容,也存在一些硬编码的情况,比如有些组件去关联到的 Model 在 framework 中,维护一个对应表,用 @class 来兼容解决。


后续


篇(jing)幅(li)有限,就不再展开说明,这个实现思路影响了笔者后续的很多开发过程,有兴趣可以看下笔者 Flutter 的文章,里面也是 codegen 的广泛运用。


如果有任何问题,都可以评论区一起讨论。


手敲不易,如果对你学习工作上有所启发,请留个赞, 感谢阅读 ~~


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

Swift是时候使用Codable了

用不起: 苹果发布Swift支持Codable已经有一定历史年限了,为什么还用不起来,无非就是苹果的Codable太强势了, 比如模型里的定义比数据返回的json多一个key,少一个key,key的值类型不匹配(如定义为String,返回的是Int),苹果老子...
继续阅读 »

用不起:


苹果发布Swift支持Codable已经有一定历史年限了,为什么还用不起来,无非就是苹果的Codable太强势了,


比如模型里的定义比数据返回的json多一个key,少一个key,key的值类型不匹配(如定义为String,返回的是Int),苹果老子直接掀桌子,整个模型为nil。这。。。


而且模型的属性想要默认值,无。。。




你牛,牛到大家不知道怎么用


于是网络一边夸他Codable好用,一边真正工程开发中却还用不起来。


搞起来:


最近研究网上有没有好用的Codable库的时候,找到了这个。2021 年了,Swift 的 JSON-Model 转换还能有什么新花样github.com/iwill/ExCod…


经过他的封装,把苹果包装的服服帖帖。经测试,解决如下问题:

  1. 多一个key
  2. 少一个key
  3. key的类型不匹配的时候,自动做类型转换
  4. 默认值处理好。 



他的模型定义可以简化为:

struct testModel: ExAutoCodable {
@ExCodable
var courseId: Int = -1
@ExCodable
var totalSectionCount: Int = -1 // 总的章节
@ExCodable
var courseImageUrl: String = ""
@ExCodable
var tudiedSectionCount: Int = 0 // 已经学习章节
}

既然他这么好,那就用起来啰喂,,,,等等,等等


定义模型这样,竟然不行:

struct testModel: ExAutoCodable {
@ExCodable
var jumpParam: [String: Any]? = [:]

@ExCodable
var matchs: [Any] = []
}

苹果老子说Any不支持Codable???转模型的时候,这个全是空,nil。


一看工程,基本每个模型的定义都有这个呀,全有Any的定义,懵逼


研究起来:


通过研究stackoverflow.com/questions/4…, 发现可以给Any封装一个支持Codable的类型,比如AnyCodable这样。然后模型里面用到Any的,全部给换成AnyCodable。




模型改为如下,使用AnyCodable

struct testModel: ExAutoCodable {
@ExCodable
var jumpParam: [String: AnyCodable]? = [:]

@ExCodable
var matchs: [AnyCodable] = []
}

AnyCodable.swift代码如下:

//
// AnyCodable.swift
//
// 因为Any不支持Codable,但是模型里面经常会用到[String: Any]。
// 所以添加类AnyCodable,代替Any,来支持Codable, 如:[String: AnyCodable]。
// https://stackoverflow.com/questions/48297263/how-to-use-any-in-codable-type

import Foundation

public struct AnyCodable: Decodable {
var value: Any

struct CodingKeys: CodingKey {
var stringValue: String
var intValue: Int?
init?(intValue: Int) {
self.stringValue = "\(intValue)"
self.intValue = intValue
}
init?(stringValue: String) { self.stringValue = stringValue }
}

init(value: Any) {
self.value = value
}

public init(from decoder: Decoder) throws {
if let container = try? decoder.container(keyedBy: CodingKeys.self) {
var result = [String: Any]()
try container.allKeys.forEach { (key) throws in
result[key.stringValue] = try container.decode(AnyCodable.self, forKey: key).value
}
value = result
} else if var container = try? decoder.unkeyedContainer() {
var result = [Any]()
while !container.isAtEnd {
result.append(try container.decode(AnyCodable.self).value)
}
value = result
} else if let container = try? decoder.singleValueContainer() {
if let intVal = try? container.decode(Int.self) {
value = intVal
} else if let doubleVal = try? container.decode(Double.self) {
value = doubleVal
} else if let boolVal = try? container.decode(Bool.self) {
value = boolVal
} else if let stringVal = try? container.decode(String.self) {
value = stringVal
} else {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "the container contains nothing serialisable")
}
} else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Could not serialise"))
}
}
}

extension AnyCodable: Encodable {
public func encode(to encoder: Encoder) throws {
if let array = value as? [Any] {
var container = encoder.unkeyedContainer()
for value in array {
let decodable = AnyCodable(value: value)
try container.encode(decodable)
}
} else if let dictionary = value as? [String: Any] {
var container = encoder.container(keyedBy: CodingKeys.self)
for (key, value) in dictionary {
let codingKey = CodingKeys(stringValue: key)!
let decodable = AnyCodable(value: value)
try container.encode(decodable, forKey: codingKey)
}
} else {
var container = encoder.singleValueContainer()
if let intVal = value as? Int {
try container.encode(intVal)
} else if let doubleVal = value as? Double {
try container.encode(doubleVal)
} else if let boolVal = value as? Bool {
try container.encode(boolVal)
} else if let stringVal = value as? String {
try container.encode(stringVal)
} else {
throw EncodingError.invalidValue(value, EncodingError.Context.init(codingPath: [], debugDescription: "The value is not encodable"))
}
}
}
}

这个结合Excodable,经过测试,完美。数据转换成功。


如果模型的定义忘记了,还是定义为Any呢。 再给Excodable库里面的源码,做安全检查,修改代码如下:

public extension Encodable {
func encode(to encoder: Encoder, nonnull: Bool, throws: Bool) throws {
var mirror: Mirror! = Mirror(reflecting: self)
while mirror != nil {
for child in mirror.children where child.label != nil {
try (child.value as? EncodablePropertyWrapper)?.encode(to: encoder, label: child.label!.dropFirst(), nonnull: false, throws: false)
// 注意:Any不支持Codable, 可以使用AnyCodable代替。
// 注意枚举类型,要支持Codable
assert((child.value as? EncodablePropertyWrapper) != nil, "模型:\(mirror)里面的属性:\(child.label) 需要支持 Encodable")
}
mirror = mirror.superclassMirror
}
}
}

public extension Decodable {
func decode(from decoder: Decoder, nonnull: Bool, throws: Bool) throws {
var mirror: Mirror! = Mirror(reflecting: self)
while mirror != nil {
for child in mirror.children where child.label != nil {
try (child.value as? DecodablePropertyWrapper)?.decode(from: decoder, label: child.label!.dropFirst(), nonnull: false, throws: false)
// 注意:Any不支持Codable, 可以使用AnyCodable代替。
// 注意枚举类型,要支持Codable
assert((child.value as? DecodablePropertyWrapper) != nil, "模型:\(mirror)里面的属性:\(child.label) 需要支持 Decodable")
}
mirror = mirror.superclassMirror
}
}
}

嗯,这下模型如果定义为Any,可以在运行的时候报错,提醒要改为AnyCodable。


能愉快的编码了。。。


不过总感觉还差点东西。


再研究起来:


找到这个 github.com/levantAJ/An…


可以实现

let dictionary: [String: Any] = try container.decode([String: Any].self, forKey: key)
let array: [Any] = try container.decode([Any].self, forKey: key)

通过自定义[String: Any]和[Any]的解码,实现Any的Codble。


是否可以把这个合并到Excodable里面吧,从而什么都支持了,666。


在Excodable里面提issues,作者回复有空可以弄弄。


我急用呀,那就搞起来。


花了九牛二虎,终于搞出下面兼容代码:

// Make `Any` support Codable, like: [String: Any], [Any]
fileprivate protocol EncodableAnyPropertyWrapper {
func encode<Label: StringProtocol>(to encoder: Encoder, label: Label, nonnull: Bool, throws: Bool) throws
}
extension ExCodable: EncodableAnyPropertyWrapper {
fileprivate func encode<Label: StringProtocol>(to encoder: Encoder, label: Label, nonnull: Bool, throws: Bool) throws {
if encode != nil { try encode!(encoder, wrappedValue) }
else {
let t = type(of: wrappedValue)
if let key = AnyCodingKey(stringValue: String(label)) {
if (t is [String: Any].Type || t is [String: Any?].Type || t is [String: Any]?.Type || t is [String: Any?]?.Type) {
var container = try encoder.container(keyedBy: AnyCodingKey.self)
try container.encodeIfPresent(wrappedValue as? [String: Any], forKey: key)
} else if (t is [Any].Type || t is [Any?].Type || t is [Any]?.Type || t is [Any?]?.Type) {
var container = try encoder.container(keyedBy: AnyCodingKey.self)
try container.encodeIfPresent(wrappedValue as? [Any], forKey: key)
}
}
}
}
}
fileprivate protocol DecodableAnyPropertyWrapper {
func decode<Label: StringProtocol>(from decoder: Decoder, label: Label, nonnull: Bool, throws: Bool) throws
}
extension ExCodable: DecodableAnyPropertyWrapper {
fileprivate func decode<Label: StringProtocol>(from decoder: Decoder, label: Label, nonnull: Bool, throws: Bool) throws {
if let decode = decode {
if let value = try decode(decoder) {
wrappedValue = value
}
} else {
let t = type(of: wrappedValue)
if let key = AnyCodingKey(stringValue: String(label)) {
if (t is [String: Any].Type || t is [String: Any?].Type || t is [String: Any]?.Type || t is [String: Any?]?.Type) {
let container = try decoder.container(keyedBy: AnyCodingKey.self)
if let value = try container.decodeIfPresent([String: Any].self, forKey: key) as? Value {
wrappedValue = value
}
} else if (t is [Any].Type || t is [Any?].Type || t is [Any]?.Type || t is [Any?]?.Type) {
let container = try decoder.container(keyedBy: AnyCodingKey.self)
if let value = try container.decodeIfPresent([Any].self, forKey: key) as? Value {
wrappedValue = value
}
}
}
}
}
}

再在他用的地方添加

// MARK: - Encodable & Decodable - internal

public extension Encodable {
func encode(to encoder: Encoder, nonnull: Bool, throws: Bool) throws {
var mirror: Mirror! = Mirror(reflecting: self)
while mirror != nil {
for child in mirror.children where child.label != nil {
if let wrapper = (child.value as? EncodablePropertyWrapper) {
try wrapper.encode(to: encoder, label: child.label!.dropFirst(), nonnull: false, throws: false)
} else { //添加
try (child.value as? EncodableAnyPropertyWrapper)?.encode(to: encoder, label: child.label!.dropFirst(), nonnull: false, throws: false)
}
}
mirror = mirror.superclassMirror
}
}
}

public extension Decodable {
func decode(from decoder: Decoder, nonnull: Bool, throws: Bool) throws {
var mirror: Mirror! = Mirror(reflecting: self)
while mirror != nil {
for child in mirror.children where child.label != nil {
if let wrapper = (child.value as? DecodablePropertyWrapper) {
try wrapper.decode(from: decoder, label: child.label!.dropFirst(), nonnull: false, throws: false)
} else { //添加
try (child.value as? DecodableAnyPropertyWrapper)?.decode(from: decoder, label: child.label!.dropFirst(), nonnull: false, throws: false)
}
}
mirror = mirror.superclassMirror
}
}
}


完美:


综上,终于可以让Excodable库支持[String: Any]和[Any]的Codable了,撒花撒花。


从而模型定义这样,也能自动编解码:

struct testModel: ExAutoCodable {
@ExCodable
var jumpParam: [String: Any]? = [:]

@ExCodable
var matchs: [Any] = []
}

针对这个库的更新修改,改到这github.com/yxh265/ExCo…


也把对应的更新提交给Excodable的作者了,期待合并。
(作者iwill说,用ExCodable提供的 ExCodableDecodingTypeConverter 协议来实现是否可行。
我看了,因为Any不支持Codable,所以要想用ExCodableDecodingTypeConverter协议,也得要大改。也期待作者出马添加这个功能。)


最后的使用方法:


引入如下:

pod 'ExCodable', :git => 'https://github.com/yxh265/ExCodable.git', :commit => '4780fb8'

模型定义:

struct TestStruct: ExAutoCodable {
@ExCodable // 字段和属性同名可以省掉字段名和括号,但 `@ExCodable` 还是没办法省掉
var int: Int = 0
@ExCodable("string", "str", "s", "nested.string") // 支持多个 key 以及嵌套 key 可以这样写
var string: String? = nil
@ExCodable
var anyDict: [String: Any]? = nil
@ExCodable
var anyArray: [Any] = []
}

编解码:

let test = TestStruct(int: 304, string: "Not Modified", anyDict: ["1": 2, "3": "4"], anyArray: [["1": 2, "3": "4"]])
let data = try? test.encoded() as Data?
let copy1 = try? data?.decoded() as TestStruct?
let copy2 = data.map { try? TestStruct.decoded(from: $0) }
XCTAssertEqual(copy1, test)
XCTAssertEqual(copy2, test)

引用:


2021 年了,Swift 的 JSON-Model 转换还能有什么新花样


github.com/iwill/ExCod…


stackoverflow.com/questions/4…


stackoverflow.com/questions/4…


Property wrappers in Swift和Codable


作者:清点游玩
链接:https://juejin.cn/post/7168748765946806303
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

iOS应用内弹窗通知怎么实现?其实很简单,这样,这样,再这样.....你学会了么?

iOS
携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第29天,点击查看活动详情。 项目背景 消息通知可以及时地将状态、内容的更新触达到用户,用户则可以根据收到的消息做后续判断。这是最常见的信息交换方式的产品设计。 而顶部向下弹出的消息通知本质上...
继续阅读 »

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第29天,点击查看活动详情


项目背景


消息通知可以及时地将状态、内容的更新触达到用户,用户则可以根据收到的消息做后续判断。这是最常见的信息交换方式的产品设计。


而顶部向下弹出的消息通知本质上是根据条件触发的“中提醒”通知类型,示例:每次在网购时,支付成功后在App会展示消息通知。


因此本章中,我们就来试试使用SwiftUI来实现应用内弹窗通知交互。


项目搭建


首先,创建一个新的SwiftUI项目,命名为NotificationToast


消息弹窗样式

我们构建一个新的视图NotificationToastView,然后声明好弹窗视图的内容变量,示例:

struct NotificationToastView: View {
    var notificationImage: String
    var notificationTitle: String
    var notificationContent: String
    var notificationTime: String

    var body: some View {
        //弹窗样式
    }
}

上述代码中,我们声明了4个String类型的变量:notificationImage图标信息、notificationTitle标题信息、notificationContent内容信息、notificationTime推送时间。


然后我们构建样式内容,示例:

HStack {
    Image(notificationImage)
        .resizable()
        .aspectRatio(contentMode: .fit)
        .frame(width: 60)
        .clipShape(Circle())
        .overlay(Circle().stroke(Color(.systemGray5), lineWidth: 1))
    VStack(spacing: 10) {
        HStack {
            Text(notificationTitle)
                .font(.system(size: 17))
                .foregroundColor(.black)
            Spacer()
            Text(notificationTime)
                .font(.system(size: 14))
                .foregroundColor(.gray)
        }
        Text(notificationContent)
            .font(.system(size: 14))
            .foregroundColor(.black)
            .lineLimit(4)
            .multilineTextAlignment(.leading)
    }
}
.padding()
.frame(minWidth: 10, maxWidth: .infinity, minHeight: 10, maxHeight: 80)
.background(.white)
.cornerRadius(8)
.shadow(color: Color(.systemGray4), radius: 5, x: 1, y: 1)
.padding()

上述代码中,我们构建了样式排布,Image使用notificationImage图片信息变量,它和其他元素是HStack横向排布关系。


右边则是HStack横向排布的notificationTitle标题变量的文字和notificationTime推送时间的文字,使用Spacer撑开。


而底下是notificationContent内容信息,它和标题信息及推送时间信息是VStack纵向排布。


我们在ContentView中展示看看效果,示例:

NotificationToastView(notificationImage: "me", notificationTitle: "文如秋雨", notificationContent: "一只默默努力变优秀的产品汪,独立负责过多个国内细分领域Top5的企业级产品项目,擅长B端、C端产品规划、产品设计、产品研发,个人独立拥有多个软著及专利,欢迎产品、开发的同僚一起交流。", notificationTime: "2分钟前")


消息弹窗交互


交互方面,我么可以做个简单的交互,创建一个按钮,点击按钮时展示消息弹窗,消息弹窗显示时等待2秒后自动消失。


实现逻辑也很简单,我们可以让弹窗加载的时候在视图之外,然后点击按钮的时候,让消息弹窗从下往下弹出,然后等待2秒后再回到视图之外


首先我们声明一个偏移量,定义消息弹窗的初始位置,示例:

@State var offset: CGFloat = -UIScreen.main.bounds.height / 2 - 80

然后给弹窗视图加上偏移量和动画的修饰符,示例:

ZStack {
    NotificationToastView(notificationImage: "me", notificationTitle: "文如秋雨", notificationContent: "一只默默努力变优秀的产品汪,独立负责过多个国内细分领域Top5的企业级产品项目,擅长B端、C端产品规划、产品设计、产品研发,个人独立拥有多个软著及专利,欢迎产品、开发的同僚一起交流", notificationTime: "2分钟前")
        .offset(x: 0, y: offset)
        .animation(.interpolatingSpring(stiffness: 120, damping: 10))
    Button(action: {
        if offset <= 0 {
            offset += 180
            DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
                self.offset -= 180
            }
        }
    }) {
        Text("弹出通知")
    }
}

上述代码中,我们让NotificationToastView弹窗视图偏移位置Y轴为我们声明好的变量offset位置,然后使用ZStack叠加展示一个按钮,当我们offset在视图外时,点击按钮修改偏移量的位置为180,然后调用成功后等待2秒再扣减偏移量回到最初的位置


项目预览


我们看看最终效果。


恭喜你,完成了本章的全部内容!

快来动手试试吧。

如果本专栏对你有帮助,不妨点赞、评论、关注~

作者:文如秋雨
链接:https://juejin.cn/post/7136104673248804878
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

最新前端技术趋势

前端的车轮滚滚向前,轮子造的越来越圆,速度造的越来越快,每个人都在适应这个轮子的节奏,稍微不注意就会被甩出车轮之外。狼狈不堪之外还会发自心底的大喊一声:别卷了!! 话虽这么说,但现实就是这样,无论是客观还是主观因素都不得不让你继续的往前走。既然是往前走, 那么...
继续阅读 »

前端的车轮滚滚向前,轮子造的越来越圆,速度造的越来越快,每个人都在适应这个轮子的节奏,稍微不注意就会被甩出车轮之外。狼狈不堪之外还会发自心底的大喊一声:别卷了!!


话虽这么说,但现实就是这样,无论是客观还是主观因素都不得不让你继续的往前走。既然是往前走,
那么能知道一些前面有啥东西岂不是更好,也许能少走弯路。



自己对前端23年大概的技术做了一些展望,想到什么写什么。毕竟谁都不知道会不会突然间又出了个frontEndGPT打翻了所有人的饭碗。



1 AI


最先说的肯定是AI,22年末,23年初的chatgpt让AI话题火的一塌糊涂,同时也被认为是一次重大的技术革新,技术革新带来的就是重塑,一切都要被重塑,你的职业,你的工作。
视觉层面的stable diffusionmidjourney已经对设计师产生了重大影响,而涉及到视觉ui层面的话,前端肯定是绕不开的部分。虽然目前没有直接的对前端产生影响,但下面的就一个就不只是对前端了,而是对整个程序员都产生了影响。

  • copilot


Your AI pair programmer. Trained on billions of lines of code, GitHub Copilot turns natural language prompts into coding suggestions across dozens of languages.


看看醒目的文字就知道,程序员或多或少都会被影响了。


还有什么CodeWhisperer, Cursor等也都是AI辅助编程。


更有FrontyLocofy等将图片AI分析为HTML文档,无代码快速建站,figma快速解析成代码等,虽然是提效不少,但谁能说这些不是对前端的一种重塑呢。


2 主流框架


随着React,Vue等框架进一步的普及,现在前端想要脱离它们的场景越来越少了。那么它们的下一步规划,也会对我们产生一些不小的影响。


react


react 18以后,react似乎是对create-react-app这种项目启动方式也不怎么主推了,毕竟速度摆在那里,没有任何优势。而使用直接竞争对手的产品似乎又不太合适,而直接说又不用又显得跟开源精神不吻合(当然竞争对手的一些基本特性也确实和现有的构建思想不太吻合)。


他们似乎采取一种围魏救赵的方式,着重宣传next的方式。next不仅仅是一个ssr的框架,同时它也支持csr,ssg等不同的方式(next13开始,对于客户端组件和服务端组件可以有了比较好的区分)。同时next与react有着千丝万缕的联系,而next正在进行一个新的构建工具的替换。
next采用了turbopack,也是Webpack作者TobiasKoppers的作品,官方说它更新速度比Vite也要快10倍、比Webpack快700倍


而react也应该大概率会引入turbopack(当然它如果继续搞前端脚手架的话)。当然也可能会直接使用next环境。


至于快多少,以及评价基准等我们可以看下turbopack真比Vite快10倍吗?


next


next最新版本也加入了很多的特性,比如server component理念,约定式路由的更改,流式渲染,客户端组件与服务端组件分离更简单,更好的构建速度等等功能。可以让开发体验,用户体验更好,性能也会有响应的提升。


vite


这个不用说了,优秀的构建速度以及越来越丰富的社区,让其在22年有了很大提升。随着浏览器的逐步升级,23年vite肯定也会是重大的一年。


webpack


虽然5有了好多的功能提升,不过速度似乎一直是一个绕不过去的坎。就连作者也已经开始搞turbopack了,虽然加入swc能让编译有很大提升,但是目前从我身边的人的了解看,越来越多的人开始转向vite等其它方式了


turbopack


是webpack作者去的新公司开发的一款基于rust的打包工具。官方明确说明就是为了替换webpack。 同时强调webpack是这十年最火的工具,那turbopack就定位成未来几年的工具。由于作者和webpack, Vercel, next, React这些之间千丝万缕的联系,很难不说未来React也许会和这个打包工具绑定上。下面是官方提供的速度参考




而除了turbopack外,同一团队还在做Turborepo


这是一款项目管理的工具,最主要的面向场景是Monorepo这种复杂的多项目管理


Monorepo有很多优势,但是在多个项目中会有很多复杂的构建过程和相对闭塞的构建步骤,每一次上线都是耗时严重。所以Turborepo是为了解决这个问题出现的,让一些构建重复的构建步骤提炼出来,基于整个Monorepo项目的维度来管理多个子项目。


同时对于单个CI的构建步骤,解决每台机器,每个人都要单独构建的问题,还提炼了类似store的方案,脱离了构建环境,只跟项目绑定,当然这个方案是否使用要看你自己,毕竟原理是把构建产物放到第三方储存,而第三方又不是一个类似npm的开源机构。


3 服务端


node从7,8年前的爆火,到现在的不温不火,前端语言介入服务端这个命题似乎现在是越来越清晰了。那就是定位,可以做网关,可以做转发,可以做一些数据代理合并,定位清晰node依然有自己的使用场景。
node也马上到了20版本,迎来了一些特性

  • esm的更好支持,

  • 测试功能更加丰富

  • V8 引擎更新至 11.3,与Chromium113版本大部分相同

  • 支持以虚拟机的方式,动态运行js代码

  • WebAssembly的支持(实验性的)


于此同时,node曾经的作者Ryan Dahl几年前搞的Deno似乎没有太多的消息,似乎在向商业化方向前进。打造出Deno Deploy及其即时边缘渲染SSR框架Deno Fresh


4 其它


WebAssembly, 元框架,ts,微前端?


引用:


http://www.infoq.cn/article/9qu…
turbo.build/pack/docs/w…
http://www.robinwieruch.de/web-develop…


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

北美 2023 被裁员的感悟

不贩卖焦虑,就事论事,希望能帮助到有需要的朋友。 很多人觉得在裁员之前是没有任何迹象的,其实真的不是这样。 公司在裁员的过程中有很多要裁员的迹象,我会在另外一篇文章中对我遇到的一些裁员信号设置一些雷区和警告,当你遇到上面的这些信号的时候,直觉告诉你需要马上考虑...
继续阅读 »

不贩卖焦虑,就事论事,希望能帮助到有需要的朋友。


很多人觉得在裁员之前是没有任何迹象的,其实真的不是这样。


公司在裁员的过程中有很多要裁员的迹象,我会在另外一篇文章中对我遇到的一些裁员信号设置一些雷区和警告,当你遇到上面的这些信号的时候,直觉告诉你需要马上考虑寻找下一个替代方案了。


因为当这些信号的任何一个或者多个同时出现的时候就意味着裁员在进行中了,通常会在 3 到 6 个月左右发生。




在公司的职位


在被裁公司的职位是 Tech Lead。


虽然这个职位并不意味着你对其他同事而言能够获得更多的有效信息,但是通常可能会让自己与上级有更好的沟通管道。


但是,非常不幸的是这家公司的沟通渠道非常有问题。


因为负责相关开发部分的副总是从 PHP 转 Java 的,对 Java 的很多情况都不非常明确,所以他非常依赖一个架构师。


但,公司大部分人都认为这个架构师的要求是错误的,并且是非常愚蠢的。


比如说要求代码必须要放在一行上面,导致代码上面有不少行甚至超过了 1000 个字符。


所有开发都对这个要求非常不理解,并且多次提出这种要求是无理并且愚蠢的,我们组是对这个要求反应最激烈,并且抵触最大的(也有可能是因为我的原因,我不希望在明显错误的地方让步;我可以让步,但是需要给一个能说服的理由)。


然而,这个所谓的架构师就利用 PR 合并的权力,不停的让我们的组员进行修改。


裁员之前


正是因为在公司的职位和上面说到的和架构师直接的冲突。


在 6 个月之前,我就已经和组里的同事说要准备进行下一步了,你们该面试的就面试了,不要拖延。


在这个中间过程中,我的领导还找我谈过一次。领导的意思就是他非常同意我们的有关代码 PR 的要求,也觉得这些要求都是狗屁。


但,负责开发的副总,认为我们组现在是所有组里面最差的。


可能当时没有认真理解这句话的意思,我们组从所有组里面最好的,变成最差的只用了 2 周(一个 Sprint)的时间。


在这次谈话后,我更加坚信让我的组员找下一家的信息了,对他们中途要面试其他公司我都放开一面。


非常不幸的,我自己那该死的拖延症,我是把我自己的简历准备好了,但是还没有来得及投就等来了真正裁员的这一天。


深刻的教训和学到的经验:


如果公司的运营或者管理让感觉到不舒服,并且已经有开始寻找下家的想法的时候,一定要尽快准备,马上实施,不要拖延


这就是我在上面标黑马上的原因。


裁员过程


裁员过程非常简单和迅速,并且在毫不知情的情况下进行。


在周四的时候,公司的高层提示所有的会议今天全部取消,并且把应该 11 点开的全公司会议提前到了 9 点。


因为很多人都没有准备,所以很多人也没有参加。


后来才知道,9 点就是宣布裁员的开始,事后知道裁员比率为 40%。


然后就是各个部门找自己的被裁的员工开会,这个会议通常首先是一个 Zoom 的 Group 会议,说了一堆屁话,就是什么这是不得已的决定呀,什么乱七八糟的东西。


当然,在这个时候你还需要或者期待公司给你什么合理的理由呢?


然后就是 P&C 人员说话,基本上就是每个人 15 分钟的时间单独 Zoom。


这个 15 分钟,主要就是读下文件了,至于 2 个会议上是不是开摄像头,随意。


你愿意开也行,不愿意开也行,反正上面的所有人都心不在焉。我是懒得开,因为和你谈话的人,你都根本不认识。


第二个会议就是 P&C,这个会议主要就是和你说说被裁员后可以有的一些福利和什么的,对我个人来说我更关注的是补偿。


至于 401K 和里面的资金都是可以自行转的,也没啥需要他们说的,了解到补偿就是 6 周工资,不算多也凑合能接受。


负责裁员的人说,还有什么需要问的,我简单的回答了下 All Set 然后 have a nice day 就好了。毕竟他们只是具体做事的人,问他们也问不出个所以然,这有啥的。


裁员之后


裁员之后,感觉所有认识的被裁的同事都是懵的。


开完 15 分钟的 P&C 会议后,基本上电脑和邮箱马上就不能用了。公司貌似说电脑可以自己留着,但是上面的数据会被远程清理掉。


留在公司里面的东西会有人收拾后寄到家里。


我在公司里的办公桌就属于离职型办公桌,简单的来说,上面只有一台不属于我的显示器,另外就是从其他地方拿过来的一盒消毒湿巾,公司里面压根没有我需要的东西。


很多人认为公司禁用账户有点太不讲人情,其实从技术层面来说根本没有什么,因为所有的管理都是 LDAP,直接在 LDAP 上禁用你账户就好了,没啥稀奇的。


中午的时候,被裁的同事都互相留下了手机号码,感觉大家因为我在裁员列表里面感觉有点扯。另外更扯的同事在这个公司工作了 7 年了,也在列表里面(所有 PHP 的基础架构都是他写的和建立的)。


虽然最开始和这个同事有过一些摩擦,但是这个印度的同事真的挺好的,我们都觉得他挺不错,也愿意和他一起共事。


很多人,包括我。都对这个同事感觉不值,也觉得这很扯。


奈何公司的选择就是一些阿谀奉承,天天扯淡的人,比如说那个奇葩的架构师。


没多久,被裁的同事建了一个群,然后把我给拉进去了,主要还是我们组里面的同事,大家希望能够分享一些面试经验和机会,偶尔吐槽下。


在晚上的时候,突然收到另外一个同事的 LinkedIn 好友邀请,他不在这次裁员内。


但是他也被降职了,他本来是 Sr 开发人员和小组长,后来被提拔成架构师了,现在连小组长都不是了。


他和我说,如果需要帮助的话,他会尽量帮忙,并且还给他的一些曾经的招聘专员账号推送给了我。


我也非常感谢他们,虽然经历过,但是也收获了一些朋友,虽然说在美国职场比较难收获朋友,但是也并不是完全这样的。


没有了利益的纠葛,更容易说点实话。


http://www.ossez.com/t/topic/144…


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

何不食肉糜?

21年的时候,微博上有过一番口诛笔伐,就是就是管清友建议刚开始工作的年轻人就近租房不要把时间浪费在上班的路上,要把时间利用起来投资自己,远比省下的房租划算。 视频见这里:http://www.bilibili.com/video/BV1Bb… 当时我印象非常...
继续阅读 »

21年的时候,微博上有过一番口诛笔伐,就是就是管清友建议刚开始工作的年轻人就近租房不要把时间浪费在上班的路上,要把时间利用起来投资自己,远比省下的房租划算。


视频见这里:http://www.bilibili.com/video/BV1Bb…



当时我印象非常深刻,微博评论是清一色的 “何不食肉糜”,或者说“房租你付?”


可能是因为这件事情的刺激,管清友后来才就有了“我特别知道年轻人建议专家不要建议”的言论。


对还是错?


在我看来,管清友的这个建议可以说是掏心掏肺,非常真诚,他在视频里也说了,他是基于很多实际案例才说的这些话,不是说教。


为什么我这么肯定呢?


很简单,我就是代表案例。


我第一家公司在浦东陆家嘴,四号线浦东大道地铁站旁边,我当时来上海的时候身无分文,借的家里的钱过来的,我是贫困家庭。


但,为了节约时间,我就在公司附近居住,步行五分钟,洗头洗澡都是中午回住的地方完成,晚上几乎都是11:00之后回去,倒头就睡,因为时间可以充分利用。


节约的时间,我就来研究前端技术,写代码,写文章,做项目,做业务,之前的小册(免费章节,可直接访问)我也提过,有兴趣的可以去看看。


现在回过头来看那段岁月,那是充满了感激和庆幸,自己绝对做了一个非常正确的决定,让自己的职业发展后劲十足。


所以,当看到管清友建议就近租房的建议,我是非常有共鸣的,可惜世界是参差的,管清友忽略了一个事实,那就是优秀的人毕竟是少数,知道如何主动投资自己的人也是凤毛麟角,他们根本就无法理解。


又或者,有些人知道应该要投资自己,但是就是做不到,毕竟辛苦劳累,何苦呢,做人,不就是应该开心快乐吗?


说句不好听的,有些人的时间注定就是不值钱的。


工作积极,时间长是种优势?


一周前,我写了篇文章,谈对“前端已死”的看法,其中提到了“团队下班最晚,工作最积极”可以作为亮点写在简历里。


结果有人笑出了声。



好巧的是,管清友的租房建议也有人笑了,出没出声并不知道。



也有人回复“何不食肉糜”。


这有些出乎我的意料,我只是陈述一个简单的事实,却触动了很多人的敏感神经。


我突然意识到,有些人可能有一个巨大的认知误区,就是认为工作时长和工作效率是负相关的,也就是那些按时下班的是效率高,下班晚的反而是能力不足,因为代码不熟,bug太多。



雷军说你说的很有道理,我称为“劳模”是因为我工作能力不行。


你的leader也认为你说的对,之前就是因为我每天准时下班,证明了自己的能力,所以自己才晋升的。


另外一个认知误区在于,把事实陈述当作目标指引。


如果你工作积极,是那种为自己而工作的人,你就在简历中体现,多么正常的建议,就好比,如果你是北大毕业的,那你就在简历中体现,没任何问题吧。


我可没有说让你去拼工作时长,装作工作积极,就好比我没有让你考北大一样。


你就不是这种类型的人,对吧,你连感同身受都做不到,激动个什么呢,还一大波人跟着喊666。


当然,我也理解大家的情绪,我还没毕业的时候,也在黑心企业待过,钱少事多尽煞笔,区别在于,我相对自驱力和自学能力强一些,通过自己的努力跳出了这个循环。


但大多数人还是被工作和生活推着走,所以对加班和内卷深恶痛绝,让本就辛苦的人生愈发艰难,而这种加班和内卷并没有带来收入的提升。


那问题来了,有人通过努力奋斗蒸蒸日上,有人的辛苦努力原地踏步,同样的,有的人看到建议觉得非常有用,有的人看到建议觉得何不食肉糜,区别在哪里呢?


究竟是资本作恶呢?还是自己能力不足呢?


那还要建议吗?


管清友不再打算给年轻人建议了,我觉得没必要。


虽然,大多数时候,那些听得进去建议的人大多不需要建议,而真正需要建议的又听不进,但是,那么多年轻人,总有一部分潜力股,有一些真正需要帮助的人。


他们可能因为环境等原因,有短暂的迷茫与不安,但是,来自前人发自真心的建议或许可以让他们坚定自己前进方向,从而走出不一样的人生。


就像当年我被乔布斯的那些话语激励过那般。


所以,嘲笑之人任其笑之,只要能帮助到部分人,那就有了价值。


因此,我不会停止给出那些我认为对于成长非常有帮助的建议。


(完)


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

放心,前端死不了

前言 今年春天莫名冒出一个“前端已死”的话题,搞得同行们人人自危。 这个话题和当下的环境是分不开的,如果环境很好,工作好找,那这个话题也就不攻自破了,不会流传开来。 网络上的任何事情都可以在《乌合之众》书中找到答案。大众言论没有理性,全是极端,要么封神,要么踩...
继续阅读 »

前言


今年春天莫名冒出一个“前端已死”的话题,搞得同行们人人自危。


这个话题和当下的环境是分不开的,如果环境很好,工作好找,那这个话题也就不攻自破了,不会流传开来。


网络上的任何事情都可以在《乌合之众》书中找到答案。大众言论没有理性,全是极端,要么封神,要么踩死。


今天能找到不错的工作,就感觉自己马上就要晋升 CEO 迎娶白富美了。但一旦明天情况不太好,就感觉自己马上要死了。


“前端已死”这个话题就属于后者。



原文地址 juejin.cn/post/722432… ,非作者允许不得转载



互联网行业会持续发展


前端技术是依托于互联网行业的,只要行业还在,它就会有用武之地,就会有价值。塞班已死,因为诺基亚不在了。


虽然现在电脑手机早已普及完成,增长率没那么大了,但互联网在各个领域的应用却不断更新升级。它已经嵌入到我们生活的各个领域了,就像水和电一样。


有互联网行业,就需要各种 toC 的页面、公众号、小程序,各种 toB 的管理系统和富客户端应用。就需要前端开发技术,需要前端人才。


你可能盲从“前端已死”,但你绝不会同意“互联网将死”,这就很矛盾 —— 当然我们生活、网络处处都矛盾。


递弱代偿的基本规律


递弱代偿,是我国哲学家王东岳提出的事物发展理论。对于这世界的任何事物,只要是发展,就一定会“递弱代偿”。


举个例子,低等的单细胞生物,一个细胞即可完成吃喝拉撒各种功能。这就很像小创业公司,或者一个行业的发展初期,都是全能型人才,拿起键盘写代码,抄起拖把扫厕所。个体很厉害,但整体很低级。


再往下发展到多细胞生物,开始有了细胞功能的分化,有的专门呼吸、有的专门消化。这就像初具规模的小公司,行业开始有了发展和进化,人才开始分角色了。有的人专门写代码,有的人专门扫卫生


再慢慢发展 N 步,到了高级哺乳动物,体内细胞分为上百种,严格划分了各种职能,最终拼接为一个高级的整体。公司和行业也一样,当前互联网从业人员的角色有几十种,大家各司其职,才做成了几万人的大公司。


不光互联网行业,像其他常见的行业,工业、汽车、医疗,这些成熟的领域,哪些不是严格细分角色的?


就连现在的网络诈骗,也都是有规模、有组织、有上下线的。


所以,互联网行业要发展,就一定会遵循这个规律,继续细分角色,前端永远都是一个重要角色。


如果今年前端被全栈替代了,明年运维也被全栈替代了,后年产品也被全栈替代了 …… 那就违背这个哲学规律了,那就说明互联网会倒退 —— 这显然不对


PS:发展不一定是线性的,它可能是起伏的,有强增长期,一定就有缓和期 —— 但整体一定是继续增长的。


前端没有可替代技术


Android 和 IOS 被小程序替代了很多,但前端目前没有发现什么可替代技术,反而前端早已慢慢的占据其他技术领域,如客户端、服务端。


新的技术如 Flutter WebAssembly 都是对当下前端技术的补充,真实 TS 也不是为了替代 ES ,它也是一个补充和备选方案。


浏览器 + HTML CSS JS + HTTP ,目前没有任何技术可以替代它,反而它们正在加速进化和完善。Vue React 小程序,目前也已经牢牢占据了高地,前端技术的范式早已形成。


ChatGPT 代替不了程序员


无论网上怎么宣传,当下所有的民生领域的人工智能,包括智能驾驶,严格来说都是:人工智能辅助


辅助,就是为人服务的。为人的自主思考能力和创造力服务,不是为机械重复的工作服务。


ChatGPT 本质上就是一个搜索引擎的二次封装,它更能理解你的输入意图,它更精确的帮你拼接返回结果。但它就是一个辅助工具,用好了可以帮你提升效率,但前提是得有人去用它。


你可以想一下日常的工作内容,开会,沟通,扯皮这些,AI 能参与进来吗?

它能理解你的业务需求吗?

它能帮你完成复杂模块的开发和联调吗?

它能帮你和其他同事沟通吗?


它能做的只是帮你写一部分固定的代码和文档,仅此而已。

现在有搜索引擎,我们说“面向 google 编程”,以后可能就变为“面向 ChatGPT 编程”,提升效率。


大环境总会有起伏,这是常态


大概从 15-16 年开始,当时随便培训一下,出来就一万多的工资(北京为例),直到前两年,行业的增长率特别快。这一代人也吃到了行业的红利,多少挣了点钱。


但无论哪个行业,都不能指望他一直这么增长,有增长就有缓和。再牛的人也不能影响到大环境,庄稼不收年年种,三穷三富活到老。


缓和期过去,还会迎来下一轮增长期。


总结

  • 互联网行业会持续发展

  • 发展就要递弱代偿,细分行业角色

  • 前端没有可替代的技术,是一个重要角色


所以,前端死不了,只是最近因环境原因暂时蛰伏,发展缓慢而已。


当下建议


有工作的就先稳住,千万别裸辞。哪怕工作不顺利,毕竟拿人钱受人管。


已经裸辞或者被毕业的,也不要过于焦虑,即便工作机会少这也不是你的问题,人斗不过大环境的。该学习就学习,该面试还是去面试,把自己该做的做了就是,其他的就别管了,淡定。


之前上班加班忙成狗,现在难得有时间,正好自由安排做一点自己之前想做的事。毕竟人生苦短,不是光为了工作,每个人都有自己独有的爱好。


也许过了这一阵,你再想这么自由就没机会了。有些事儿,现在不做,这辈子也许就不再有机会和动力了。


如果你想反驳我的观点


以你想的为准,你说的对。


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

2023 了,不会还要做官网吧!

web
随着 AIGC 的兴起,移动互联网仿佛一瞬间也不香了,吃到红利的头部应用还在稳坐泰山,后起者在各种 xx 已死的声浪中,不知所措,信心尽失。流量的走势左右了太多人的思绪,太多的决策,所以当你在 2023 年,PC 互联网已经凉到不能再凉的时候,接到一个官网的需...
继续阅读 »

随着 AIGC 的兴起,移动互联网仿佛一瞬间也不香了,吃到红利的头部应用还在稳坐泰山,后起者在各种 xx 已死的声浪中,不知所措,信心尽失。流量的走势左右了太多人的思绪,太多的决策,所以当你在 2023 年,PC 互联网已经凉到不能再凉的时候,接到一个官网的需求,你会怎么去做呢,耐心看完,希望对你有所帮助。


官网开发作为前端的传统艺能已经诞生很久了,与现在主流的 react/vuecsr 框架开发不同,官网对 seo 有一定要求,所以咱们的网页不再是开局一个空 html,渲染全靠 js 了。传统的官网开发,一般采用 WordPress 建站,很好的技术栈 php + html/css/js 前后台一把梭,方便快捷,架构成熟。不过当遇到公司的前后端技术栈是 java + react 时,这个方案显然对开发人员并不友好,作为一名思维跳脱,勇于承担的前端,灵光乍现,我们是不是可以自己独立的完成整个官网呢。


在掘金上搜了搜,居然还有现成的课程,看看时间还挺新的,不如就从它入手吧。




作为网站尊贵的会员,我用兑换券换到了这门课程,开始了如饥似渴的学习。文章内容还是比较浅显易懂的,跟着操作下一个网站的雏形就出现了。课程中的技术栈主要采用 next.js + strapi 完成对官网的开发,相信各位看官并不陌生,业界知名的 react ssr 框架和 headless cms 系统,基础的介绍就不多说了,来分享点实操中得到经验教训吧。


next.js 相关


收获到的知识


1. 如何调试 next.js 服务端渲染的逻辑


之前,我只会使用 console.log 在启动的命令行里去判断代码的问题,现在我知道了使用 cross-env NODE_OPTIONS='--inspect' next dev 命令,他会开启一个 nodejsinspect 调试窗口,因为 next.js 的系统对象都很复杂,层级深,如果 console.log 的话很觉得很烦人,通过如上的方式就能便捷。






2. 生成有参数的静态页面的方法


其实像官网流量挺小的,没有必要走静态导出的 ssg ,使用 ssr 也完全没有问题,但是鉴于对 k8s 扩容,服务器费用之类的知识不太了解,官网的迭代频率也很低,所以本次采用了 ssg 的方案,要求所有子页面都要生成, 这就不得不提到内置的 api名为 getStaticPaths了。他会在打包阶段,先从一个列表接口拿到所有的id,再执行 getStaticProps 中的逻辑,拿到所有详情数据。比如我们的故事详情页,共有两条数据,id为1,2,执行完后,就会在对应目录下生成两个 html






得到的经验教训


1. 移动端适配


一个简易的判断移动端的代码很容易写,先不纠结完备性,大致如下:

/mobile|android|iphone|ipad|phone/i.test(window.navigator.userAgent.toLowerCase())

然而在 next.jsssg 方案中你却很难拿到,因为 render 里不能有对 window 的直接使用。我想了很多取巧的方法,但是都不行,如果是 ssr 还可以用请求头的 user-agent 来判断,作为属性透传到组件中,如课程一样。但 ssg 你只能使用响应式,同时渲染2套布局,才是最简单的做法。仔细想想,因为 html 字符串的构建是在 build 的时候就已经决定了,所以不能动态的拼接成不同的 dom 也就很好理解了。



PS: 感谢 @wonderL17 指出,也可以使用nginx配置的方式,将对应路径转发到为移动端生成的页面。
如果页面复用逻辑多,且能很好支持响应式,可以使用响应式。如果完全是另一套风格,则可以使用配置ng方式去处理,更为优秀,代码会更好维护且生成的页面更小。



2. api目录的处理


next.js 中的 api 目录可以承担接口的功能,课程中在 api 层进行了一次中转,我对此呈保留意见,想法如下:

  • 如果 strapi 不能直接支持一定的并发量,那么使用 api 来代理请求一样不能达到。

  • 中间层越多,可能出现的协作编码问题就越多,比如 strapi 返回的数据不符合预期,有些人会去修改 strapi,有些人会去改 next.js 的 api,这样会造成项目维护的不统一。


其实简单的对 strapi 加个跨域的配置,就能很好的直接在每个组件内使用,这对于一般的官网是完全可以满足的,而且代码清晰整洁。


strapi 相关


strapi 暂时没有给我带来新的知识,因为没有对他的原理进行研究,最核心的功能,编辑类型,即可生成对应的增删改查界面是最值得学习的部分,可是也是他最成熟的部分,开发嘛,能用就行,所以只有使用上的踩坑记录。但不得不说,strapi 的模块设计的挺不错的(除开dashboard ui定制外),有兴趣的可以了解下代码原理和设计思路。


得到的经验教训


1. 数据库(!!!!!)


请一定不要用 sqlite,不然你会哭的。因为项目肯定是需要多人协作的,强大如你 merge 代码对你来说轻而易举,但是你会 merge db 文件吗?相信我,你不会。


所以不要等项目已成,你才想起来去换个数据库,那么你即将面临,如果将 sqlite 数据导出成 postgresql 或者 mysql 数据的问题,听上去并不难吧,我花了 1~2 天时间才把这个无意义的工作做完。虽然能查到很多方案,但开源的不好用,付费的不想试,还是老老实实导出 sql,再导入是最快的。(不详述了,都是辛酸,你懂的,装了一堆东西,要么环境问题,要么功能问题,迟迟无法完成迁移的痛)


所以一开始就选好数据库,很关键,这样既方便了协作开发,又方便了后续使用。因为 4.x? 具体是几不清楚,relations 排序不生效(postgresql | mysql),sqlite 没有这个问题,所以替换数据库后,升级下 strapi,我升级的是4.10.6,顺便可以把依赖里的 sharp 删掉,有时候要装很久。


2. OSS(!!!!)


oss 一样至关重要,如果你使用了 strapi 的媒体库功能,请一定先配置好自定义的 oss,使用这个库就好了 strapi-provider-upload-oss,配置起来很方便,如果你头铁,一开始就是不配,你将面临,将上传好的图片删除,再重新上传一遍,没错他没有批量替换的功能,只能手动操作,我也没有花很久,1个小时,重新换了70~80张图片(关键素材上次用完还删了)。


配置如下:

// config/plugins.js

module.exports = ({ env }) => ({
upload: {
config: {
provider: "strapi-provider-upload-oss", // full package name is required
providerOptions: {
accessKeyId: "xxxx", // 用你的 oss 配置把 xxx 换掉,但千万别上传到开源库里(如github, gitee)哦
accessKeySecret: "xxx",
region: "xxx",
bucket: "xxx",
uploadPath: "/strapi/static",
baseUrl: "xxx",
timeout: 3000,
secure: true,
},
},
},
});

3. 数据的格式化(!!!)


课程中自己定义 removeTime, removeAttrsAndId 等方法,对数据进行处理,可能写的比较早,还没有成熟的转换库,这里介绍下 strapi-plugin-transformer,可以快速的去掉没必要的层级结构和一些属性,相当好用。配置如下:

// config/plugins.js

module.exports = ({ env }) => ({
transformer: {
enabled: true,
config: {
responseTransforms: {
removeAttributesKey: true,
removeDataKey: true,
},
},
},
});

这个操作可以避免对数据进行过多的处理,也就意味着 src/api/xx 里的代码,你基本不需要手动修改了,大大提升了后台配置的开发效率。C端对数据的处理也更简单了


4. 一些小的注意点

  • config 目录下添加插件,需要创建 plugins.js 文件,少写了 s 会导致插件不生效。(PS:嗯,就是粗心的我建了 plugin.js 还怪人家插件不好使~)

  • .cache 还挺有用的,一些修改不生效,可以试试 build 后再重启试试。


一些拓展能力


strapi 的初始状态很难满足直接交付给运营配置,最大的坑点在于类型定义是英文的,还没有层级结构,这里参考了这篇文章提到的方案:juejin.cn/post/721922…,来对 dashboard 进行定制。功能主要分为3个步骤,patch-package 使用参考原文章即可。

  • 类型汉化


只需要修改 admin/app.js 即可


  • 类型层级&排序:.cache/admin/src/content-manger/pages/App/LeftMenu 文件

// 目录排序,1,1.1,2,2.1
function compareDirectories(formatter, dir1, dir2) {
// 提取目录中的数字和点号
const regex = /(\d+|\.)+/g;
const arr1 = dir1.match(regex);
const arr2 = dir2.match(regex);

if (!arr1 || !arr2) return formatter.compare(dir1, dir2);

// 比较每个部分的数字
for (let i = 0; i < Math.max(arr1.length, arr2.length); i++) {
const num1 = parseFloat(arr1[i]) || 0; // 如果无法解析为数字,默认为0
const num2 = parseFloat(arr2[i]) || 0;

if (num1 < num2) {
return -1;
} else if (num1 > num2) {
return 1;
}
}

// 如果所有部分都相同,则按照长度进行比较
if (arr1.length < arr2.length) {
return -1;
} else if (arr1.length > arr2.length) {
return 1;
}

return 0; // 目录完全相同
}



// Sort correctly using the language
// 这条注释下,用目录排序的方法替代原有 sort

// SubNavLink 内,使用 dangerouslySetInnerHTML 体现目录层级
<span dangerouslySetInnerHTML={{__html: link.title.replace(/([0-9]+.)/g, (a, b, index) => {
if (index <= 1) return '';

return '   ';
})}}></span>


这样你就能得到一个这样的配置目录




  • 项目部署

因为我这边项目是 ssg,修改完内容是需要触发流水线重新部署的,如果像想避免这个工作,可以增加一些按钮,触发 webhook。这样只要排版是够用的,就不需要开发介入了。


如下示例,修改的是 .cache/admin/src/pages/HomePage/index.js




以上改动想要生效,记得用 patch-package,这也是我说他的定制模块不友好的原因~


总结


next.jsstapi 的学习,踩坑,使用经验就是这么多了,起初发起这个项目,是因为我们官网的框架有些老了,用的 fis3,有时候法务、市场同学来找替换素材,都是没什么工作难度的事儿,浪费时间,也整的挺烦的,所以借着机会升级了一下,以后就事半功倍了。


至于在当今这个时代,官网的 seo 是否还有意义,是否还能够为公司带来不俗的自然增量,这个我最感兴趣的事儿,迟迟没能发起。


因为在公司,这是涉及很多部门(品牌,公关,法务,市场,产品,设计)的事儿,普通开发并不能调动资源,我也很期待,如果有幸能发起一个这样的项目,并不断通过技术上的优化为业务带来新的增量,那里可能会用到更多的贴合业务的 ssr 技术,到时候有机会再和大家分享下~


但我也想对技术感兴趣的朋友说,底层的技术重构也是很有魅力的一件事儿,尽管没有业务方的支持,如果你持之以恒的来做,复刻原产品,也能让这个产品在技术层面上焕然一新。


谨以此文,与君共勉!


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

你在公司混的差,可能和组织架构有关!

如果你接触过公司的面试工作,一定见过很多来自大公司的渣渣。这些人的薪资和职位,比你高出很多,但能力却非常一般。 如果能力属实,我们大可直接把这些大公司的员工打包接收,也免了乱七八糟的面试工作。但可惜的是,水货的概率通常都比较大,新的公司也并不相信他们的能力。尤...
继续阅读 »

如果你接触过公司的面试工作,一定见过很多来自大公司的渣渣。这些人的薪资和职位,比你高出很多,但能力却非常一般。


如果能力属实,我们大可直接把这些大公司的员工打包接收,也免了乱七八糟的面试工作。但可惜的是,水货的概率通常都比较大,新的公司也并不相信他们的能力。尤其是这两年互联网炸了锅,猪飞的日子不再,这种情况就更加多了起来。


反过来说也一样成立,就像是xjjdog在青岛混了这么多年,一旦再杀回北上广,也一样是落的下乘的评价。


除了自身的努力之外,你在上家公司混的差,还与你在组织架构中所处于的位置和组织架构本身有关。


一般公司会有两种组织架构方式:垂直化划分层级化划分


1. 垂直划分


垂直划分,多以业务线为模型进行划分。各条业务线共用公司行政资源,相互之间关联不大。


各业务线之间,内部拥有自治权。


image.png


如上图所示,公司共有四个业务线。




  • 业务线A,有前端和后端开发。因为成员能力比较强,所以没有测试运维等职位;




  • 业务线B倡导全栈技能,开发后台前端一体化;




  • 业务线C的管理能力比较强,仅靠少量自有研发,加上大量的外包,能够完成一次性工作。




  • 业务线D是传统的互联网方式,专人专岗,缺什么招什么,不提倡内部转岗




运行模式




  1. 业务线A缺人,缺项目,与业务线BCD无任何关系,不允许借调




  2. 业务线发展良好,会扩大规模;其他业务线同学想要加入需要经过复杂的流程,相当于重新找工作




  3. 业务线发展萎靡,会缩减人员,甚至会整体砍掉。优秀者会被打散吸收进其他业务线




好处




  1. 业务线之间存在竞争关系,团队成员有明确的奋斗目标和危机意识




  2. 一条业务线管理和产品上的失败,不会影响公司整体运营




  3. 可以比较容易的形成单向汇报的结构,避免成本巨大且有偏差的多重管理




  4. 便于复制成功的业务线,或者找准公司的发展重点




坏处




  1. 对业务线主要分管领导的要求非常高




  2. 多项技术和产品重复建设,容易造成人员膨胀,成本浪费




  3. 部门之间隔阂加大,共建、合作困难,与产品化相逆




  4. 业务线容易过度自治,脱离掌控




  5. 太激进,大量过渡事宜需要处理




修订


为了解决上面存在的问题,通常会有一个协调和监管部门,每个业务线,还需要有响应的协调人进行对接。以以往的观察来看,效果并不会太好。因为这样的协调,多陷于人情沟通,不好设计流程规范约束这些参与人的行为。


image.png


在公司未摸清发展方向之前,并不推荐此方式的改革。它的本意是通过竞争增加部门的进取心,通过充分授权和自治发挥骨干领导者的作用。但在未有成功案例之前,它的结果变成了:寄希望于拆分成多个小业务线,来解决原大业务线存在的问题。所以依然是处于不太确定的尝试行为。


2. 水平划分


水平划分方式,适合公司有确定的产品,并能够形成持续迭代的团队。


它的主要思想,是要打破“不会做饭的项目经理不是好程序员”的思维,形成专人专业专岗的制度。


这种方式经历了非常多的互联网公司实践,可以说是最节约研发成本,能动性最高的组织方式。主要是因为:




  • 研发各司其职,做好自己的本职工作可以避免任务切换、沟通成本,达到整体最优




  • 个人单向汇报,组织层级化,小组扁平化。“替领导负责,就是替公司负责”




  • 任何职位有明确的JD,可替换性高,包括小组领导




这种方式最大的问题就是,对团队成员的要求都很高。主动性与专业技能都有要求,需要经过严格的面试筛选。


坏处




  • 是否适合项目类公司,存疑




  • 存在较多技术保障部门,公共需求 下沉容易造成任务积压




  • 需要对其他部门进行整合,才能发挥更大的价值




分析


image.png


如上图,大体会分为三层。




  • 技术保障,保障公司的底层技术支撑,问题处理和疑难问题解决。小组多但人少,职责分明




  • 基础业务,公司的旗舰业务团队,需求变更小但任何改动都非常困难。团队人数适中




  • 项目演化,纯项目,可以是一锤子买卖,也可以是服务升级,属于朝令夕改类需求的聚居地。人数最多




可以看到项目演化层,多是脏活,有些甚至是尝试性的项目-----这是合理的。




  1. 技术保障和基础业务的技术能力要求高,业务稳定,适合长期在公司发展,发展属性偏技术的人群,流动性小,招聘困难




  2. 项目演化层,业务多变,项目奖金或者其他回报波动大,人员流动性高,招聘容易




成功的孵化项目,会蜕变成产品,或者基础业务,并入基础业务分组。


从这种划分可以看出,一个人在公司的命运和发展,在招聘入职的时候就已经确定了。应聘人员可以根据公司的需求进行判断,提前预知自己的倾向。


互联网公司大多数将项目演化层的人员当作炮灰,因为他们招聘容易,团队组件迅速,但也有很多可能获得高额回报,这也是很多人看中的。


3.组合


组合一下垂直划分和层级划分,可以是下面这种效果。


image.png


采用层级+垂直方式进行架构。即:首选层级模式,然后在项目演化层采用垂直模式,也叫做业务线,拥有有限的自治权。


为每一个业务线配备一个与下层产品化或者技术保障对接的人员。


绩效方面,上层的需求为下层的实现打分。基础业务和技术保障,为绿色的协调人员打分。他们的利益是一致的。


End


大公司出来的并不一定是精英,小公司出来的也并不一定是渣渣。这取决于他在公司的位置和所从事的内容。核心部门会得到更多的利益,而边缘的尝试性部门只能吃一些残羹剩饭。退去公司的光环,加上平庸的项目经历,竞争力自然就打上一个折扣。


以上,仅限IT行业哦。赵家人不在此列。


作者:小姐姐味道
来源:juejin.cn/post/7203651773622452261
收起阅读 »

解决扫码枪因输入法中文导致的问题

web
问题 最近公司项目上遇到了扫码枪因搜狗/微软/百度/QQ等输入法在中文状态下,使用扫码枪扫码会丢失字符的问题 思考 这种情况是由于扫码枪的硬件设备,在输入的时候,是模拟用户键盘的按键来实现的字符输入的,所以会触发输入法的中文模式,并且也会触发输入法的自动联想。...
继续阅读 »

问题


最近公司项目上遇到了扫码枪因搜狗/微软/百度/QQ等输入法在中文状态下,使用扫码枪扫码会丢失字符的问题


思考


这种情况是由于扫码枪的硬件设备,在输入的时候,是模拟用户键盘的按键来实现的字符输入的,所以会触发输入法的中文模式,并且也会触发输入法的自动联想。那我们可以针对这个来想解决方案。


方案一


首先想到的第一种方案是,监听keydown的键盘事件,创建一个字符串数组,将每一个输入的字符进行比对,然后拼接字符串,并回填到输入框中,下面是代码:


function onKeydownEvent(e) {
this.code = this.code || ''
const shiftKey = e.shiftKey
const keyCode = e.code
const key = e.key
const arr = ['Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', 'Z', 'X', 'C', 'V', 'B', 'N', 'M', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-']
this.nextTime = new Date().getTime()
const timeSpace = this.nextTime - this.lastTime
if (key === 'Process') { // 中文手动输入
if (this.lastTime !== 0 && timeSpace <= 30) {
for (const a of arr) {
if (keyCode === 'Key' + a) {
if (shiftKey) {
this.code += a
} else {
this.code += a.toLowerCase()
}
this.lastTime = this.nextTime
} else if (keyCode === 'Digit' + a) {
this.code += String(a)
this.lastTime = this.nextTime
}
}
if (keyCode === 'Enter' && timeSpace <= 30) {
if (String(this.code)) {
// TODO
dosomething....
}
this.code = ''
this.nextTime = 0
this.lastTime = 0
}
}
} else {
if (arr.includes(key.toUpperCase())) {
if (this.lastTime === 0 && timeSpace === this.nextTime) {
this.code = key
} else if (this.lastTime !== 0 && timeSpace <= 30) {
// 30ms以内来区分是扫码枪输入,正常手动输入时少于30ms的
this.code += key
}
this.lastTime = this.nextTime
} else if (arr.includes(key)) {
if (this.lastTime === 0 && timeSpace === this.nextTime) {
this.code = key
} else if (this.lastTime !== 0 && timeSpace <= 30) {
this.code += String(key)
}
this.lastTime = this.nextTime
} else if (keyCode === 'Enter' && timeSpace <= 30) {
if (String(this.code)) {
// TODO
dosomething()
}
this.code = ''
this.nextTime = 0
this.lastTime = 0
} else {
this.lastTime = this.nextTime
}
}
}


这种方案能解决部分问题,但是在不同的扫码枪设备,以及不同输入法的情况下,还是会出现丢失问题


方案二


使用input[type=password]来兼容不同输入的中文模式,让其只能输入英文,从而解决丢失问题


这种方案网上也有不少的参考

# 解决中文状态下扫描枪扫描错误

# input type=password 取消密码提示框


使用password密码框确实能解决不同输入法的问题,并且Focus到输入框,输入法会被强制切换为英文模式


添加autocomplete="off"autocomplete="new-password"属性


官方文档:
# 如何关闭表单自动填充


但是在Chromium内核的浏览器,不支持autocomplete="off",并且还是会出现这种自动补全提示:


image.png


上面的属性并没有解决浏览器会出现密码补全框,并且在输入字符后,浏览器还会在右上角弹窗提示是否保存:


image.png


先解决密码补全框,这里我想到了一个属性readonly,input原生属性。input[type=password]readonly
时,是不会有密码补全的提示,并且也不会弹窗提示密码保存。


那好,我们就可以在输入前以及输入完成后,将input[type=password]立即设置成readonly


但是需要考虑下面几种情况:



  • 获取焦点/失去焦点时

  • 当前输入框已focus时,再次鼠标点击输入框

  • 扫码枪输出完成最后,输入Enter键时,如果清空输入框,这时候也会显示自动补全

  • 清空输入框时

  • 切换离开页面时


这几种情况都需要处理,将输入框变成readonly


我用vue+element-ui实现了一份,贴上代码:


<template>
<div class="scanner-input">
<input class="input-password" :name="$attrs.name || 'one-time-code'" type="password" autocomplete="off" aria-autocomplete="inline" :value="$attrs.value" readonly @input="onPasswordInput">
<!-- <el-input ref="scannerInput" v-bind="$attrs" v-on="$listeners" @input="onInput"> -->
<el-input ref="scannerInput" :class="{ 'input-text': true, 'input-text-focus': isFocus }" v-bind="$attrs" v-on="$listeners">
<template v-for="(_, name) in $slots" v-slot:[name]>
<slot :name="name"></slot>
</template>
<!-- <slot slot="suffix" name="suffix"></slot> -->
</el-input>
</div>
</template>

<script>
export default {
name: 'WispathScannerInput',
data() {
return {
isFocus: false
}
},
beforeDestroy() {
this.$el.firstElementChild.setAttribute('readonly', true)
this.$el.firstElementChild.removeEventListener('focus', this.onPasswordFocus)
this.$el.firstElementChild.removeEventListener('blur', this.onPasswordBlur)
this.$el.firstElementChild.removeEventListener('blur', this.onPasswordClick)
this.$el.firstElementChild.removeEventListener('mousedown', this.onPasswordMouseDown)
this.$el.firstElementChild.removeEventListener('keydown', this.oPasswordKeyDown)
},
mounted() {
this.$el.firstElementChild.addEventListener('focus', this.onPasswordFocus)
this.$el.firstElementChild.addEventListener('blur', this.onPasswordBlur)
this.$el.firstElementChild.addEventListener('click', this.onPasswordClick)
this.$el.firstElementChild.addEventListener('mousedown', this.onPasswordMouseDown)
this.$el.firstElementChild.addEventListener('keydown', this.oPasswordKeyDown)

const entries = Object.entries(this.$refs.scannerInput)
// 解决ref问题
for (const [key, value] of entries) {
if (typeof value === 'function') {
this[key] = value
}
}
this['focus'] = this.$el.firstElementChild.focus.bind(this.$el.firstElementChild)
},
methods: {
onPasswordInput(ev) {
this.$emit('input', ev.target.value)
if (ev.target.value === '') {
this.$el.firstElementChild.setAttribute('readonly', true)
setTimeout(() => {
this.$el.firstElementChild.removeAttribute('readonly')
})
}
},
onPasswordFocus(ev) {
this.isFocus = true
setTimeout(() => {
this.$el.firstElementChild.removeAttribute('readonly')
})
},
onPasswordBlur() {
this.isFocus = false
this.$el.firstElementChild.setAttribute('readonly', true)
},
// 鼠标点击输入框一瞬间,禁用输入框
onPasswordMouseDown() {
this.$el.firstElementChild.setAttribute('readonly', true)
},
oPasswordKeyDown(ev) {
// 判断enter键
if (ev.key === 'Enter') {
this.$el.firstElementChild.setAttribute('readonly', true)
setTimeout(() => {
this.$el.firstElementChild.removeAttribute('readonly')
})
}
},
// 点击之后,延迟200ms后放开readonly,让输入框可以输入
onPasswordClick() {
if (this.isFocus) {
this.$el.firstElementChild.setAttribute('readonly', true)
setTimeout(() => {
this.$el.firstElementChild.removeAttribute('readonly')
}, 200)
}
},
onInput(_value) {
this.$emit('input', _value)
},
getList(value) {
this.$emit('input', value)
}
// onChange(_value) {
// this.$emit('change', _value)
// }
}
}
</script>

<style lang="scss" scoped>
.scanner-input {
position: relative;
height: 36px;
width: 100%;
display: inline-block;
.input-password {
width: 100%;
height: 100%;
border: none;
outline: none;
padding: 0 16px;
font-size: 14px;
letter-spacing: 3px;
background: transparent;
color: transparent;
// caret-color: #484848;
}
.input-text {
font-size: 14px;
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
pointer-events: none;
background-color: transparent;
::v-deep .el-input__inner {
// background-color: transparent;
padding: 0 16px;
width: 100%;
height: 100%;
}
}

.input-text-focus {
::v-deep .el-input__inner {
outline: none;
border-color: #1c7af4;
}
}
}
</style>


至此,可以保证input[type=password]不会再有密码补全提示,并且也不会再切换页面时,会弹出密码保存弹窗。
但是有一个缺点,就是无法完美显示光标。如果用户手动输入和删除,使用起来会有一定的影响。


我想到过可以使用模拟光标,暂时不知道可行性。有哪位同学知道怎么解决的话,可以私信我,非常感谢


作者:香脆又可口
来源:juejin.cn/post/7265666505102524475
收起阅读 »

假如面试官让你讲一下新版雪花算法

这是一篇由 Seata 的官网上一篇叫做“关于新版雪花算法的答疑”的文章引发的思考。 seata.io/zh-cn/blog/… 还是有点意思的,结合自己的理解和代码,加上画几张图来拆解一下 Seata 里面的“改良版雪花算法”。 虽然是在 Seata ...
继续阅读 »

这是一篇由 Seata 的官网上一篇叫做“关于新版雪花算法的答疑”的文章引发的思考。



seata.io/zh-cn/blog/…



image.png


还是有点意思的,结合自己的理解和代码,加上画几张图来拆解一下 Seata 里面的“改良版雪花算法”。


虽然是在 Seata 里面看到的,但是本篇文章的内容和 Seata 框架没有太多关系,反而和数据库的基础知识有关。


所以,即使你不了解 Seata 框架,也不影响你阅读。


当你理解了这个类的工作原理之后,你完全可以把这个只有 100 多行的类搬运到你的项目里面,然后就变成你的了。


dddd(懂的都懂)!



先说问题



雪花算法的使用场景:


如果你的项目中涉及到需要一个全局唯一的流水号,比如订单号、流水号之类的,又或者在分库分表的情况下,需要一个全局唯一的主键 ID 的时候,就需要一个算法能生成出这样“全局唯一”的数据。


一般来说,我们除了“全局唯一”这个基本属性之外,还会要求生成出来的 ID 具有“递增趋势”,这样的好处是能减少 MySQL 数据页分裂的情况,从而减少数据库的 IO 压力,提升服务的性能。


此外,在当前的市场环境下,不管你是啥服务,张口就是高并发,我们也会要求这个算法必须得是“高性能”的。


雪花算法,就是一个能生产全局唯一的、递增趋势的、高性能的分布式 ID 生成算法。


关于雪花算法的解析,网上相关的文章比雪花还多,这个玩意,应该是“面试八股文”中重点考察模块,分布式领域中的高频考题之一,如果是你的盲区的话,可自行去了解一下。


比如一个经典的面试题就是:


雪花算法最大的缺点是什么?


背过题的小伙伴应该能立马答出来:时钟敏感


因为在雪花算法中,由于要生成单调递增的 ID,因此它利用了时间的单调递增性,所以是强依赖于系统时间的。


如果系统时间出现了回拨,那么生成的 ID 就可能会重复。


而“时间回拨”这个现象,是有可能出现的,不管是人为的还是非人为的。


当你回答出这个问题之后,面试官一般会问一句:


那如果真的出现了这种情况,应该怎么办呢?


很简单,正常来说只要不是不是有人手贱或者出于泄愤的目的进行干扰,系统的时间漂移是一个在毫秒级别的极短的时间。


所以可以在获取 ID 的时候,记录一下当前的时间戳。然后在下一次过来获取的时候,对比一下当前时间戳和上次记录的时间戳,如果发现当前时间戳小于上次记录的时间戳,所以出现了时钟回拨现象,对外抛出异常,本次 ID 获取失败。


理论上当前时间戳会很快的追赶上上次记录的时间戳。


但是,你可能也注意到了,“对外抛出异常,本次 ID 获取失败”,意味着这段时间内你的服务对外是不可使用的。


比如,你的订单号中的某个部分是由这个 ID 组成的,此时由于 ID 生成不了,你的订单号就生成不了,从而导致下单失败。


再比如,在 Seata 里面,如果是使用数据库作为 TC 集群的存储工具,那么这段时间内该 TC 就是处于不可用状态。


你可以简单的理解为:基础组件的错误导致服务不可用。



再看代码



基于前面说的问题,Seata 才提出了“改良版雪花算法”。



seata.io/zh-cn/blog/…



图片


在介绍改良版之前,我们先把 Seata 的源码拉下来,瞅一眼。


在源码中,有一个叫做 IdWorker 的类:



io.seata.common.util.IdWorker



再来看一下它的提交记录:


图片


2020 年 5 月 4 日第一次提交,从提交时的信息可以看出来,这是把分布式 ID 的生成策略修改为 snowflake,即雪花算法。


同时我们也能在代码中找到前面提到的“对外抛出异常,本次 ID 获取失败”相关代码,即 nextId  方法,它的比较方式就是用当前时间戳和上次获取到的时间戳做对比:



io.seata.common.util.IdWorker#nextId



图片


这个类的最后一次提交是 2020 年 12 月 15 日:


图片


这一次提交对于 IdWorker 这个类进行了大刀阔斧的改进,可以看到变化的部分非常的多:


图片


我们重点关注刚刚提到的 nextId 方法:


图片


整个方法从代码行数上来看都可以直观的看到变化,至少没有看到抛出异常了。


这段代码到底是怎么起作用的呢?


首先,我们得理解 Seata 的改良思路,搞明白思路了,再说代码就好理解一点。


在前面提到的文章中 Seata 也说明了它的核心思路:


图片


原版的雪花算法 64 位 ID 是分配这样的:


图片


可以看到,时间戳是在最前面的,因为雪花算法利用了时间的单调递增的特性。


所以,如果前面的时间戳一旦出现“回退”的情况,即打破了“时间的单调递增”这个前提条件,也就打破了它的底层设计。


它能怎么办?


它只能给你抛出异常,开始摆烂了。


可以看到节点 ID 长度为二进制的 10 位,也就是说最多可以服务于 1024 台机器,所以你看 Seata 最开始提交的版本里面,有一个在 1024 里面随机的动作。


因为算法规定了,节点 ID 最多就是 2 的 10 次方,所以这里的 1024 这个值就是这样来的:


图片


包括后面有大佬觉得用这个随机算法一点都不优雅,就把这部分改成了基于 IP 去获取:


图片


看起来有点复杂,但是我们仔细去分析最后一行:



return ((ipAddressByteArray[ipAddressByteArray.length - 2] & 0B11) << Byte.SIZE) + (ipAddressByteArray[ipAddressByteArray.length - 1] & 0xFF);



变量 & 0B11 运算之后的最大值就是 0B11 即 3。


Byte.SIZE = 8。


所以,3 << 8,对应二进制 1100000000,对应十进制 768。


变量 & 0xFF 运算之后的最大值就是 0xFF 即 255。


768+255=1023,取值范围都还是在 [0,1023] 之间。


然后你再看现在最新的算法里面,官方的老哥们认为获取 IP 的方式不够好:


图片


所以又把这个地方从使用 IP 地址改成了获取 Mac 地址。


图片


最后一行是这样的:



return ((mac[4] & 0B11) << 8) | (mac[5] & 0xFF);



那么理论上的最大值就是 768 | 255 ,算出来还是 1023。


所以不管你怎么玩出花儿来,这个地方搞出来的数的取值范围就只能是 [0,1023] 之间。


别问,问就是规定里面说了,算法里面只给节点 ID 留了 10 位长度。


最后,就是这个 12 位长度的序列号了:


图片


这个玩意没啥说的,就是一个单纯的、递增的序列号而已。


既然 Seata 号称是改良版,那么具体体现在什么地方呢?


简单到你无法想象:


图片


是的,仅仅是把时间戳和节点 ID 换个位置就搞定了。


然后每个节点的时间戳是在 IdWorker 初始化的时候就设置完成了,具体体现到代码上是这样的:



io.seata.common.util.IdWorker#initTimestampAndSequence



图片


在获取 ID 的过程中,只有在初始化的时候获取过一次系统时间,之后和它就再也没有关系了。


所以,Seata 的分布式 ID 生成器,不再依赖于时间。


然后,你再想想另外一个问题:


由于序列号只有 12 位,它的取值范围就是 [0,4095]。如果我们序列号就是生成到了 4096 导致溢出了,怎么办呢?



很简单,序列号重新归 0,溢出的这一位加到时间戳上,让时间戳 +1。


那你再进一步想想,如果让时间戳 +1 了,那么岂不是会导致一种“超前消费”的情况出现,导致时间戳和系统时间不一致了?


朋友,慌啥啊,不一致就不一致呗,反正我们现在也不依赖于系统时间了。


然后,你想想,如果出现“超前消费”,意味着什么?


意味着在当前这个毫秒下,4096 个序列号不够用了。


4096/ms,约 400w/s。


你啥场景啊,怎么牛逼?


(哦,原来是面试场景啊,那懂了~)


另外,官网还抛出了另外一个问题:这样持续不断的"超前消费"会不会使得生成器内的时间戳大大超前于系统的时间戳,从而在重启时造成 ID 重复?


你想想,理论上确实是有可能的。假设我时间戳都“超前消费”到一个月以后了。


那么在这期间,你服务发生重启时我会重新获取一次系统时间戳,导致出现“时间回溯”的情况。


理论上确实有可能。



但是实际上...


看看官方的回复:


图片



别问,问就是不可能,就算出现了,最先崩的也不是我这个地方。



说了这么多其实就记住住这个图,就完事了:


图片


那么问题又来了:


改良版的算法是单调递增的吗?



在单节点里面,它肯定是单调递增的,但是如果是多个节点呢?


在多个节点的情况下,单独看某个节点的 ID 是单调递增的,但是多个节点下并不是全局单调递增。


因为节点 ID 在时间戳之前,所以节点 ID 大的,生成的 ID 一定大于节点 ID 小的,不管时间上谁先谁后。



这一点我们也可以通过代码验证一下,代码的意思是三个节点,每个节点各自生成 5 个 ID:


图片


从输出来看,一眼望去,生成的 ID 似乎是乱序的,至少在全局的角度下,肯定不是单调递增的:


但是我们把输出按照节点 ID 进行排序,就变成了这样,单节点内严格按照单调递增,没毛病:


图片


而在原版的雪花算法中,时间戳在高位,并且始终以系统时钟为准,每次生成的时候都会严格和系统时间进行对比,确保没有发生时间回溯,这样可以保证早生成的 ID 一定小于晚生成的 ID ,只有当 2 个节点恰好在同一时间戳生成 ID 时,2 个 ID 的大小才由节点 ID 决定。


这样看来,Seata 的改进算法是不是错的?


先别急,继续往下看。



分析一波



分析之前,先抛出官方的回答:


图片


先来一个八股文热身:


请问为什么不建议使用 UUID 作为数据库的主键 ID ?


就是为了避免触发 MySQL 的页分裂从而影响服务性能。


比如当前主键索引的情况是这样的:


图片


如果来了一个  433,那么直接追加在当前最后一个记录 432 之后即可。


图片


但是如果我们要插入一个 20 怎么办呢?


那么数据页 10 里面已经放满了数据,所以会触发页分裂,变成这样:


图片


进而导致上层数据页的分裂,最终变成这样的一个东西:


图片


上面的我们可以看出页分裂伴随着数据移动,所以我们应该尽量避免。


理想的情况下,应该是把一页数据塞满之后,再新建另外一个数据页,这样 B+ tree 的最底层的双向链表永远是尾部增长,不会出现上面画图的那种情况:在中间的某个节点发生分裂。


那么 Seata 的改良版的雪花算法在不具备“全局的单调递增性”的情况下,是怎么达到减少数据库的页分裂的目的的呢?


我们还是假设有三个节点,我用 A,B,C 代替,在数值上 A < B < C,采用的是改良版的雪花算法,在初始化的情况下是这样的。


图片


假设此时,A 节点申请了一个流水号,那么基于前面的分析,它一定是排在 A-seq1 之后,B-seq1 之前的。


但是这个时候数据页里面的数据满了,怎么办?


分裂呗:


图片


又来了 A-seq3 怎么办?


问题不大,还放的下:


图片


好,这个时候数据页 7 满了,但是又来了 A-seq4,怎么办?


只有继续分裂了:


图片


看到这个分裂的时候,你有没有嗦出一丝味道,是不是有点意思了?


因为在节点 A 上生成的任何 ID 都一定小于在节点 B 上生成的任何 ID,节点 B 和节点 C 同理。


在这个范围内,所有的 ID 都是单调递增的:


图片


而这样的范围最多有多少个?


是不是有多少个节点,就有多少个?


那么最多有多少个节点?


图片


2 的 10 次方,1024 个节点。


所以官方的文章中有这样的一句话:



新版算法从全局角度来看,ID 是无序的,但对于每一个 workerId,它生成的 ID 都是严格单调递增的,又因为 workerId 是有限的,所以最多可划分出 1024 个子序列,每个子序列都是单调递增的。



经过前面的分析,每个子序列总是单调递增的,所以每个子序列在有限次的分裂之后,最终都会达到稳态。


或者用一个数学上的说法:该算法是收敛的。


再或者,放个图看看:


图片


我想说作者画的时候尽力了,至于你看懂看不懂的,就看天意了。


页分裂


前面写的所有内容,你都能在官网上前面提到的两个文章中找到对应的部分。


但是关于页分裂部分,官方并没有进行详细说明。本来也是这样的,人家只是给你说自己的算法,没有必要延伸的太远。


“刚才说到了页分裂”,以面试官的嘴脸怎么可能放过你,“展开讲讲?”


链接已放,自行展开:



mysql.taobao.org/monthly/202…



还是先搞个图:


图片


问,在上面的这个 B+ tree 中,如果我要插入 9,应该怎么办?


因为数据页中已经没有位置了,所以肯定要触发页分裂。


会变成这样:


图片


这种页分裂方式叫做插入点(insert point)分裂。


其实在 InnoDB 中最常用的是另外一种分裂方式,中间点(mid point)分裂。


如果采用中间点(mid point)分裂,上面的图就会变成这样:


图片


即把将原数据页面中的 50% 数据移动到新页面,这种才是普遍的分裂方法。


这种分裂方法使两个数据页的空闲率都是 50%,如果之后的数据在这两个数据页上的插入是随机的话,那么就可以很好地利用空闲空间。


但是,如果后续数据插入不是随机,而是递增的呢?


比如我插入 10 和 11。


插入 10 之后是这样的:


图片


插入 11 的时候又要分页了,采用中间点(mid point)分裂就变成了这样:


图片


你看,如果是在递增的情况下,采用中间点(mid point)分裂,数据页 8 和 20 的空间利用率只有 50%。


因为数据的填充和分裂的永远是右侧页面,左侧页面的利用率只有 50%。


所以,插入点(insert point)分裂是为了优化中间点(mid point)分裂的问题而产生的。


InnoDB 在每个数据页上专门有一个叫做 PAGE_LAST_INSERT 的字段,记录了上次插入位置,用来判断当前插入是是否是递增或者是递减的。


如果是递减的,数据页则会向左分裂,然后移动上一页的部分数据过去。


如果判定为递增插入,就在当前点进行插入点分裂。


比如还是这个图:


图片


上次插入的是记录 8,本次插入 9,判断为递增插入,所以采用插入点分裂,所以才有了上面这个图片。


好,那么问题就来了,请听题:


假设出现了这种情况,阁下又该如何应对?


图片


在上面这个图的情况下,我要插入 10 和 9:


当插入 10 的时候,按 InnoDB 遍历 B+ tree 的方法会定位到记录 8,此时这个页面的 PAGE_LAST_INSERT 还是 8。所以会被判断为递增插入,在插入点分裂:


图片


同理插入 9 也是这样的:


图片


最终导致 9、10、11 都独自占据一个 page,空间利用率极低。


问题的根本原因在于每次都定位到记录 8(end of page),并且都判定为递增模式。


哦豁,你说这怎么办?


答案就藏在这一节开始的时候放的链接中:


图片


前面所画的图都是在没有并发的情况下展开的。


但是在这个部分里面,牵扯到了更为复杂的并发操作,同时也侧面解释了为什么 InnoDB 在同一时刻只能有有一个结构调整(SMO)进行。


这里面学问就大了去了,有兴趣的可以去了解一下 InnoDB 在 B+ tree 并发控制上的限制,然后再看看 Polar index 的破局之道。


反正我是学不动了。


哦,对了。前面说了这么多,还只是聊了页分裂的情况。


有分裂,就肯定有合并。


那么什么时候会触发页合并呢?


页合并会对我们前面探讨的 Seata 的改良版雪花算法带来什么影响呢?


别问了,别问了,学不动了,学不动了。


作者:essenceNow
来源:juejin.cn/post/7265641370495451148
收起阅读 »

iOS:零碎整理iOS音视频开发API

在ios开发过程中,音频经常会用到,而音频根据使用场合分为音效和音乐,音效一般只播放1~2秒ios音效支持的格式: ios 支持的音频格式有:aac、alac、he-aac、iLBc、IMA4、Linea PCM、MP3、CAF,其中,aac、alac、he-...
继续阅读 »

在ios开发过程中,音频经常会用到,而音频根据使用场合分为音效和音乐,音效一般只播放1~2秒

  • ios音效支持的格式: ios 支持的音频格式有:aac、alac、he-aac、iLBc、IMA4、Linea PCM、MP3、CAF,其中,aac、alac、he-aac、mp3、caf支持硬件解码,其他只支持软件解码, 软件界面因为比较耗电,所以,我们在开发过程中,经常采用的是caf、mp3

  • 音频库: AVFoundation.framework

代码

// 打开资源
NSURL* url =[[NSBundle mainBundle]URLForResource:@"m_03" withExtension:@"wav"];
SystemSoundID soundID;
AudioServicesCreateSystemSoundID((__bridge CFURLRef)(url), &soundID);
// 播放音效
AudioServicesPlaySystemSound(self.soundID);
// 删除音效
AudioServicesDisposeSystemSoundID(self.soundID);
  • 框架

  • 加载音乐资源并播放

AVAudioPlayer* player = musicDict[fileName];
if (!player) {
NSURL* url = [[NSBundle mainBundle] URLForResource:fileName withExtension:nil];
NSCAssert(url != nil, @"fileName not found musics");

NSError* error;
player = [[AVAudioPlayer alloc]initWithContentsOfURL:url error:&error];
if (error) {
NSLog(@"load music error");
return;
}
[musicDict setObject:player forKey:fileName];
}
if (player.isPlaying == NO) {
[player play];
}
  • 暂停 停止操作

[player pause];// 暂停
[player stop];// 停止
[player isplaying];// 是否在播放

好了,现在能播放音乐了,但我们在看其他的应用的时候,一般当应用切换到后台的时候也能播放音乐,那这个又是如何实现的呢?这个只要设置音频的后台播放,具体为:

1> 在后台开启一个任务

- (void)applicationDidEnterBackground:(UIApplication *)application
{
// 开启后台任务,让音乐继续播放
[application beginBackgroundTaskWithExpirationHandler:nil];
}

  2> 设置项目配置文件


   3> 设置音频链接会话,这个主要告诉设备如何处理音频事件的

1234// 设置音频会话类型``   ``AVAudioSession* session = [AVAudioSession sharedInstance];``   ``[session setCategory:AVAudioSessionCategorySoloAmbient error:``nil``];``   ``[session setActive:``YES error:``nil``];

这里有很多会话类型,如果想详细了解,可参考:blog.csdn.net/daiyelang/a…

现在应该可以播放音乐了。


作者:会飞的金鱼
链接:https://juejin.cn/post/7238110426147373112
来源:稀土掘金
收起阅读 »

iOS中UICollectionView的item增删、拖拽和排序动画

iOS
效果图 这个是前段时间项目新增的一个功能,刚刚开始组员是用UIScrollView + UIView 实现的,但这种实现方式属实是有点low,后续闲暇时笔者用UICollectionView简单实现了下。 思路 简单理一下思路,首先是把整个页面先布局出来,这...
继续阅读 »

效果图




这个是前段时间项目新增的一个功能,刚刚开始组员是用UIScrollView + UIView 实现的,但这种实现方式属实是有点low,后续闲暇时笔者用UICollectionView简单实现了下。


思路


简单理一下思路,首先是把整个页面先布局出来,这里涉及到一个UICollectionViewSection背景色的问题,有需要的可以点这里,有详细的介绍。移动动画也很简单,先获取起点cell和终点cell,再新建一个动画的AnimationItem,根据获取的起点和终点cell动画就行,最后再实现拖拽排序效果。


大致总结一下:

  • 布局

  • 移动动画

  • 拖拽排序


下面就根据思路一步步来。我们一定要有一个意识,不管是多么复杂的动画,只要把它分解开来,按步骤一步一步实现就很简单。


实现


我们就跟上面的思路一步一步实现。


布局


首先想到肯定是新建一个Model来管理这个数据,新建Model也要有点技巧。

struct ItemModel {
    var section: Int = -1 // cell的section索引
    var item: Int = -1 // cell的item索引
    var name: String = "" // 名称
    var isAdded: Bool = false // 是否添加到首页应用(第0区)
    var id: String { // 唯一标识,可以用这个来命名图片的名称,也可以用来作判断
        get {
            "\(section)_\(item)"
        }
    }
    init(){}
}

看得出来,ItemModel的属性section + item = IndexPath,可以根据 model 知道当前cell的所在位置了。


笔者这用的是 struct ,感觉用 class 会更好点,因为后续会改变数组中Model的属性值。已经写了就懒得再改了。


存在2个数组数据:

  • var editItems = [ItemModel](),由前一页传入的、可编辑、拖拽的数据,位于UICollectionView的第0个Section。

  • var datas = [[ItemModel]](),按照Section的顺序,存放所有的数据。


注意:Section要从1开始,因为第0个Section是可以编辑拖拽的区域。


datas中存放全部的数据:

for i in 0..<names.count {
let subNames = names[i]
var items = [ItemModel]()
for j in 0..<subNames.count {
var model = ItemModel()
model.section = i+1 // 注意这里的Section要从1开始
model.item = j
model.name = subNames[j]
model.isAdded = editItems.contains(where: { $0.id == model.id})
items.append(model)
}
datas.append(items)
}

根据数据布局UICollectionView


移动动画


移动只要2个操作,添加应用和删除应用。


添加


笔者这里规定了最多可以添加8个应用。


大致思路:

  • 获取当前点击的 cell,为了得到其坐标作为动画起始位置

  • 在 collectionView 中插入一个空白的 cell 占位,此举是为了增加或减少行数的动画过渡更自然;对应也应该在 editItems 中添加一个空白的 model 作为数据源,等移动动画结束后再给model重新赋值。

  • 获取新插入的空白 cell,为了得到其坐标作为动画的结束位置

  • 生成动画的 cell,起始 -> 结束 动画。

  • 更新数据,刷新


删除


思路与添加雷同,且比之更简单


具体的思路和步骤,代码中都有一步步的注释,可自行查阅。


拖拽排序


这个拖拽排序,在iOS11之前的比较麻烦,都是靠自己计算,这里也简单说下思路:


iOS11.0之前的实现思路

  1. 在UICollectionView上添加一个长按的手势

  2. 在UICollectionView上面添加一个浮动隐藏的cell,便于拖拽

  3. 通过长按操作找到需要被拖动的cellA

  4. 通过拖动cellA找到找到和它交换位置的cellB

  5. 交换cellA和cellB的位置

  6. 替换数据源,把起始位置的数据模型删除,然后将起始位置的数据模型插入到拖拽位置


这种比较复杂的是结合位置判断需要交换的cell。但是在iOS11之后,UICollectionView新增了dragDelegatedropDelegate,用来实现拖拽排序的效果。


dragDelegate、dropDelegate


直接上代码:

collectionView.dragDelegate = self
collectionView.dropDelegate = self
collectionView.dragInteractionEnabled = true
collectionView.reorderingCadence = .immediate
collectionView.isSpringLoaded = true

  • dragInteractionEnabled 属性要设置为 true,才可以进行 drag 操作。此属性在 iPad 默认是 true,在 iPhone 默认是 false。

  • reorderingCadence 重排序节奏,可以调节集合视图重排序的响应性。

  • UICollectionViewReorderingCadenceImmediate 默认值。当开始移动的时候就立即回流集合视图布局,实时的重新排序。

  • UICollectionViewReorderingCadenceFast 快速移动,不会立即重新布局,只有在停止移动的时候才会重新布局

  • UICollectionViewReorderingCadenceSlow 停止移动再过一会儿才会开始回流,重新布局

  • isSpringLoaded 弹性加载效果,也可以使用代理方法:func collectionView(_ collectionView: UICollectionView, shouldSpringLoadItemAt indexPath: IndexPath, with context: UISpringLoadedInteractionContext) -> Bool。

需要实现UICollectionViewDropDelegateUICollectionViewDragDelegate协议方法。下面是常用的几个方法,按照调用的先后顺序说明一下:

/*
* 识别到拖动,一次拖动一个;若一次拖动多个,则需要选中多个
* 提供一个给定 indexPath 的可进行 drag 操作的 item
* NSItemProvider, 拖放处理时,携带数据的容器,通过对象初始化,该对象需满足 NSItemProviderWriting 协议
*/
func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem]

/*
* 使用自定义预览,如果该方法没有实现或者返回nil,那么整个 cell 将用于预览
* UIDragPreviewParameters有2个属性:backgroundColor设置背景颜色;visiblePath设置视图的可见区域
* 笔者使用这个方法除去了拖拽过程中item的阴影
*/
func collectionView(_ collectionView: UICollectionView, dragPreviewParametersForItemAt indexPath: IndexPath) -> UIDragPreviewParameters?

/*
* 开始拖拽后,继续添加拖拽的任务,处理雷同`itemsForBeginning`方法
*/
func collectionView(_ collectionView: UICollectionView, itemsForAddingTo session: UIDragSession, at indexPath: IndexPath, point: CGPoint) -> [UIDragItem]

/*
* 拖拽开始,可自行处理
*/
func collectionView(_ collectionView: UICollectionView, dragSessionWillBegin session: UIDragSession)

/*
* 判断对应的 item 能否被执行drop会话,是否能放置
*/
func collectionView(_ collectionView: UICollectionView, canHandle session: UIDropSession) -> Bool

/*
* 处理拖动中放置的策略,此方法会 频繁调用,在此方法中应尽可能减少工作量。
* 四种分别:move移动;copy拷贝;forbidden禁止,即不能放置;cancel用户取消。
* 效果一般使用2种:.insertAtDestinationIndexPath 挤压移动;.insertIntoDestinationIndexPath 取代。
* 在某些情况下,目标索引路径可能为空(比如拖到一个没有cell的空白区域)你可以通过 session.locationInView 做你自己的命中测试
*/
func collectionView(_ collectionView: UICollectionView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UICollectionViewDropProposal

/*
* 当drop会话进入到 collectionView 的坐标区域内就会调用
*/
func collectionView(_ collectionView: UICollectionView, dropSessionDidEnter session: UIDropSession)

/*
* 结束放置时的处理
* 如果该方法不做任何事,将会执行默认的动画
*/
func collectionView(_ collectionView: UICollectionView, performDropWith coordinator: UICollectionViewDropCoordinator)

/*
* 拖拽开始,可自行处理
*/
func collectionView(_ collectionView: UICollectionView, dragSessionDidEnd session: UIDragSession)

/*
* 当dropSession 完成时会被调用,不管结果如何。一般进行清理或刷新操作
*/
func collectionView(_ collectionView: UICollectionView, dropSessionDidEnd session: UIDropSession)

这里大概的拖拽动画就差不多了,代码中会有更详细的注释。


总结


将一个大功能拆分成一个个小模块,按部就班一步一步实现就不难了。这里只是中间囊括了各种小动画和刷新,设计好思路,不行就多试几次肯定可以。


代码自取:RCDragDropAnimation




若存在什么不对的地方,欢迎指正!


作者:云层之上
链接:https://juejin.cn/post/7246777949100933177
来源:稀土掘金
收起阅读 »

iOS 中如何精准还原 Sketch 线性渐变效果

iOS
背景 这样的渐变效果当然用切图是可以方便的实现,但切图不够灵活,而且会增加包大小 那如何用代码实现呢? 首先看下 iOS 中渐变的几个参数 colors startPoint endPoint locations colors 很好获取,其他三个参数怎么...
继续阅读 »

背景




这样的渐变效果当然用切图是可以方便的实现,但切图不够灵活,而且会增加包大小


那如何用代码实现呢?


首先看下 iOS 中渐变的几个参数



  • colors

  • startPoint

  • endPoint

  • locations


colors 很好获取,其他三个参数怎么办呢,似乎只能看图猜出个大概?


猜想


众所周知,sketch 有一键导出标注的插件 ,但是只能获取到 locations 信息

background-image: linear-gradient(-73deg, #361CE6 0%, #7DA7EB 50%, #96A4FF 100%);

并且这个 -73deg 对于 iOS 中的 startPoint``endPoint 来说还不太友好,需要经过一番转换。


这个时候心中有个想法💡,这个插件能导出这些信息应该是对 sketch 的源文件进行了解析,那么 sketch 的源文件是个什么样的文件呢,会不会像 .ipa 那样是个压缩包呢?


实践


file 命令可以查看文件的信息

file Test.sketch

输出如下结果

Test.sketch: Zip archive data, at least v2.0 to extract, compression method=deflate

可以看到这确实是一个压缩包

那就可以用 unzip 命令来解压一下

unzip Test.sketch -d ./temp

👀看看解压出了个啥呢?

.
├── document.json
├── meta.json
├── pages
│   └── 7832D4DC-A896-40BE-8F96-45850CE9FC53.json
├── previews
│   └── preview.png
└── user.json

有 json 文件!欣喜若狂😁!!!最终在 pages 这个目录下的 json 文件找到了想要的东西

{
"_class": "gradient",
"elipseLength": 0,
"from": "{1.1356274384397782, 0.99999999999999978}",
"gradientType": 0,
"to": "{-0.13533980933892775, -0.49069446290249097}",
"stops": [
{
"_class": "gradientStop",
"position": 0,
"color": {
"_class": "color",
"alpha": 1,
"blue": 0.903056932532269,
"green": 0.1092045213150163,
"red": 0.2098672970162421
}
},
{
"_class": "gradientStop",
"position": 0.4973543951952161,
"color": {
"_class": "color",
"alpha": 1,
"blue": 0.9204804793648098,
"green": 0.6532974892326747,
"red": 0.4919794574547816
}
},
{
"_class": "gradientStop",
"position": 1,
"color": {
"_class": "color",
"alpha": 1,
"blue": 1,
"green": 0.6418734727143864,
"red": 0.5896740424923781
}
}
]
}

结论

  • from 是 startPoint

  • to 是 endPoint

  • stops 中的 position 是 locations


Tips: UI 给的 sketch 文件可能图层太多,json 文件会非常大,打开比较卡,可以把图层复制到自己新建的 sketch 文件中再解压


作者:LittleYuuuuu
链接:https://juejin.cn/post/7222179242946641978
来源:稀土掘金
收起阅读 »

iOS17适配指南之UIContentUnavailableView(一)

iOS
介绍 新增视图,表示内容不可达,特别适用于没有数据时的占位视图。 UIContentUnavailableConfigurationUIContentUnavailableView 的配置参数,用于设置不可达时的占位内容。既可以使用 UIKit,又可以使用 S...
继续阅读 »

介绍


新增视图,表示内容不可达,特别适用于没有数据时的占位视图。


UIContentUnavailableConfiguration

  • UIContentUnavailableView 的配置参数,用于设置不可达时的占位内容。

  • 既可以使用 UIKit,又可以使用 SwiftUI。

  • 系统提供了 3 种配置,分别为empty()、loading()与search()。

  • UIViewController 增加了一个该类型的参数contentUnavailableConfiguration,用于设置view内容不可达时的占位内容。


案例一

import UIKit

class ViewController: UIViewController {
lazy var tableView: UITableView = {
let tableView = UITableView(frame: UIScreen.main.bounds, style: .plain)
tableView.dataSource = self
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "abc")
return tableView
}()
// UIContentUnavailableView
lazy var unavailableView: UIContentUnavailableView = {
var config = UIContentUnavailableConfiguration.empty()
// 配置内容
config.text = "暂无数据"
config.textProperties.color = .red
config.secondaryText = "正在加载数据..."
config.image = UIImage(systemName: "exclamationmark.triangle")
config.imageProperties.tintColor = .red
var buttonConfig = UIButton.Configuration.filled()
buttonConfig.title = "加载数据"
config.button = buttonConfig
config.buttonProperties.primaryAction = UIAction(title: "") { _ in
self.loadData()
}
var backgroundConfig = UIBackgroundConfiguration.listPlainCell()
backgroundConfig.backgroundColor = .systemGray6
config.background = backgroundConfig
// 创建UIContentUnavailableView
let unavailableView = UIContentUnavailableView(configuration: config)
unavailableView.frame = UIScreen.main.bounds
return unavailableView
}()
var content: [String] = []

override func viewDidLoad() {
super.viewDidLoad()

view.addSubview(tableView)
if content.isEmpty {
view.addSubview(unavailableView)
}
}

func loadData() {
content = ["iPhone 12 mini", "iPhone 12", "iPhone 12 Pro", "iPhone 12 Pro Max",
"iPhone 13 mini", "iPhone 13", "iPhone 13 Pro", "iPhone 13 Pro Max",
"iPhone 14", "iPhone 14 Plus", "iPhone 14 Pro", "iPhone 14 Pro Max"]
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.tableView.reloadData()
self.unavailableView.removeFromSuperview()
}
}
}

// MARK: - UITableViewDataSource
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return content.count
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "abc", for: indexPath)
cell.textLabel?.text = content[indexPath.row]
cell.imageView?.image = UIImage(systemName: "iphone")
return cell
}
}

效果:


案列二

import UIKit

class ViewController: UIViewController {
lazy var emptyConfig: UIContentUnavailableConfiguration = {
var config = UIContentUnavailableConfiguration.empty()
config.text = "暂无数据"
config.image = UIImage(systemName: "exclamationmark.triangle")
return config
}()

override func viewDidLoad() {
super.viewDidLoad()

contentUnavailableConfiguration = emptyConfig
}

// MARK: - 更新UIContentUnavailableConfiguration
override func updateContentUnavailableConfiguration(using state: UIContentUnavailableConfigurationState) {
// 切换
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
let loadingConfig = UIContentUnavailableConfiguration.loading()
self.contentUnavailableConfiguration = loadingConfig
}
// 移除
DispatchQueue.main.asyncAfter(deadline: .now() + 6) {
self.contentUnavailableConfiguration = nil
self.view.backgroundColor = .systemTeal
}
}
}


作者:YungFan
链接:https://juejin.cn/post/7257732392541634621
来源:稀土掘金

收起阅读 »

你还在傻傻的打开页面输用户名和密码?来我教你实现自动化

web
背景 Hello~,大家好! 本文和各位分享一个有趣的事情! 我司主要的客户是银行,随着银行对信息安全越来越重视,我司积极配合银行防范信息安全,因此我司产品都从之前的外网开发引入了深信服的云桌面开发。由于笔者用的 Mac 电脑,云桌面 win7,天差地别的体验...
继续阅读 »

背景


Hello~,大家好! 本文和各位分享一个有趣的事情!


我司主要的客户是银行,随着银行对信息安全越来越重视,我司积极配合银行防范信息安全,因此我司产品都从之前的外网开发引入了深信服的云桌面开发。由于笔者用的 Mac 电脑,云桌面 win7,天差地别的体验我就先忍了😭,但是从 Mac 电脑桌面到进入云桌面,需要登录一些莫名其妙的 VPN、网址,这个过程是令人反感的、重复的、也是非常恶心的,每天都要这样、要等待很久......🤮



  1. 打开Mac上的xxxTrust并登录

  2. 打开Chrome

  3. 打开xxx网址

  4. 输入用户名、密码(不能自动填充那种)

  5. 点击登录

  6. 进入云桌面资源页面,点击一个资源,会自动调起 Mac 上安装的一个什么 VDIxxx 的软件

  7. 成功进入云桌面


我丢,各位来说说,这个过程是不是很恶心。作为一个技术人,我们不能忍受这种机械式的操作,我们要去做出改变。不能让自己一直做这些重复的恶心操作,于是我就想着 能不能像我打开 Mac 桌面的一个应用那样,中间的步骤自动完成,直到进入云桌面? 这就是笔者本文的主题。


实现自动化


其实笔者也没有一步到位——能不能像我打开 Mac 桌面的一个应用那样,中间的步骤自动完成,直到进入云桌面?。实现过程中,有一些新想法,接下来就分享一下我从开始有这个想法到实现的过程:


ConsoleSnippets


一开始: 我已经登录了 xxxTrust,笔者在浏览器已经打开了xxx网址,只是不想输密码,我就想起浏览器开发者工具 ConsoleSnippets,可以在里面写一些脚本,然后可以快捷执行:


image.png

打开控制台 -> 快捷键 command + p -> 输入!,选择执行哪个 Snippets -> 回车。看下效果:
1.gif


感觉还行是吧,那接着来,我们现在进入了资源管理的界面,接下来需要手动点击打开一个云桌面的资源,同样地,接着建一个 Snippets


image.png

看下效果:
1.gif


OK!成功进入,但是现在还需要我们手动去执行脚本,而且要执行两个。于是就有了新的想法。


篡改猴


能不能在对应的页面自动执行上面写好的脚本?


此时笔者想到了一个 Chrome 插件 —— 篡改猴,也叫油猴脚本,简单理解它的作用就是可以在你指定的网页中执行你写入的脚本。那就装一个呗!(Chrome商店需要🪜,可直接使用Edge浏览器)


image.png

打开管理面板,新建两个脚本:
image.png


编辑:
截屏2023-08-11 11.46.22.png


截屏2023-08-11 12.43.48.png

然后改一下设置,在 document-end 执行我们的脚本,此时可能还获取不到dom,因此使用 setTimeout
截屏2023-08-11 12.45.57.png
再来看下效果:


1.gif
鼠标一下没动哦!你以为这就完了?并没有,接着看。


自动化打开应用


能不能自动打开xxxTrustChrome,并自动打开登录的网址?


咱们一开始就说了,在访问网址之前,还需要链接VPN、打开浏览器,那就来吧!


截屏2023-08-11 13.21.17.png

在Mac上有两个东西可以完成自动操作:


截屏2023-08-11 13.22.20.png
通过自动操作配置出来的,也支持转变成快捷指令。我理解这俩应该是差不多的东西,来看下我们如何实现:


image.png
操作步骤:

  1. 打开 xxxTrust

  2. 通过 AppleScript,设置延时6秒,因为打开上面的 app 过后,有一个自动登录的过程,我们设置的长一点

  3. 打开 Chrome

  4. 打开登录网址


配置自动化操作和快捷指令都是可视化的,很容易上手,在此不过多介绍,感兴趣的掘友可自行探索哦!至于AppleScript,我只能告诉你是 GPT 教我这么写的。


最后一步就是把这个快捷指令发布到桌面:
截屏2023-08-11 13.33.00.png


看下最终的效果:
1.gif


这不,又多了几分钟摸鱼时间!😂


总结


笔者通过真实的一个场景,借助 Snippets篡改猴快捷指令自动操作 等工具实现自动化完成进入云桌面的一系列流程。笔者只实现了 Mac 的,windows 系统肯定也有类似的工具等待各位去探索(比如 python 脚本应该就能实现打开应用等操作)。


除此之外呢,还想表达一个观点就是——我们应该把那些机械式的活交给机械去做,比如在平时的开发中,总是CRUD?能不能高效CRUD?对吧!把这些对自己能力提升没有意义的工作,想办法用程序去实现了,岂不是美滋滋!


好了,本次分享就到此结束了,感谢阅读哦!


如果本文对你有一点点帮助,点个赞支持一下吧,你的每一个【】都是我创作的最大动力 ^_^


作者:Lvzl
来源:juejin.cn/post/7265744160694911028
收起阅读 »

代码中被植入了恶意删除操作,太狠了!

背景 在交接的代码中做手脚进行删库等操作,之前只是网上听说的段子,没想到上周还真遇到了,并且亲自参与帮忙解决。 事情是这样的,一老板接手了一套系统,可能因为双方在交接时出现了什么不愉快的事情,对方不提供源代码,只是把生产环境的服务器打了一个镜像给到对方。 对方...
继续阅读 »

背景


在交接的代码中做手脚进行删库等操作,之前只是网上听说的段子,没想到上周还真遇到了,并且亲自参与帮忙解决。


事情是这样的,一老板接手了一套系统,可能因为双方在交接时出现了什么不愉快的事情,对方不提供源代码,只是把生产环境的服务器打了一个镜像给到对方。


对方拿到镜像恢复之后,系统起来怎么也无法正常处理业务,于是就找到我帮忙看是什么原因。经过排查,原来交接的人在镜像中做了多处手脚,多处删除核心数据及jar包操作。下面来给大家细细分析排查过程。


排查过程


由于只提供了镜像文件,导致到底启动哪些服务都是问题。好在是Linux操作系统,镜像恢复之后,通过history命令可以查看曾经执行了哪些命令,能够找到都需要启动哪些服务。但服务启动之后,业务无法正常处理,很多业务都处于中间态。


原本系统是可以正常跑业务的,打个镜像之后再恢复就不可以了?这就奇怪了。于是对项目(jar包或war)文件进行排查,查看它们的修改时间。


在文件的修改时间上还真找到了一些问题,发现在打镜像的两个小时前,项目中一个多个项目底层依赖的jar包被修改过,另外还有两个class文件被修改过。


于是,就对它们进行了重点排查。首先反编译了那两个被修改过的class文件,在代码中找到了可疑的地方。


可疑代码


在两个被修改的类中都有上述代码。最开始没太留意这段代码,但直觉告诉我不太对,一个查询业务里面怎么可能出现删除操作呢?这太不符合常理了。


于是仔细阅读上述代码,发现上述红框中的代码无论何时执行最终的结果都是id=1。你是否看出来了?问题就出在三目表达式上,无论id是否为null,id被赋的值都是1。看到这里,也感慨对方是用心了。为了隐藏这个目的,前面写了那么多无用的代码。


但只有这个还不是什么问题,毕竟如果只是删除id为1的值,也只是删除了一条记录,影响范围应该有限。


紧接着反编译了被修改的jar包,依次去找上述删除方法的底层实现,看到如下代码:


删除操作


原来前面传递的id=1是为了配合where条件语句啊,当id=1被传递进来之后,就形成了where 1=1的条件语句。这个大家在mybatis中拼接多条件语句时经常用到。结果就是一旦执行了上述业务逻辑,就会触发删除T_QUART_DATA全表数据的操作。


T_QUART_DATA表中是用于存储触发定时任务的表达式,到这里也就明白了,为啥前面的业务跑不起来,全部是中间态了。因为一旦在业务逻辑中触发开关,把定时任务的cron表达式全部删除,十多个定时任务全部歇菜,业务也就跑步起来了。


找到了问题的根源,解决起来就不是啥事了,由于没有源代码,稍微费劲的是只能把原项目整个反编译出来,然后将改修改地方进行了修改。


又起波折


本以为到此问题已经解决完毕了,没想到第二天又出现问题了,项目又跑不起来了。经过多方排查和定位,感觉还有定时任务再进行暗箱操作。


于是通过Linux的crontab命令查看是否有定时任务在执行,执行crontab -ecrontab -l,还真看到有三个定时任务在执行。跟踪到定时任务执行的脚本中,而且明目张胆的起名deleteXXX:


删除脚本


而在具体的脚本中,有如下执行操作:


删除核心依赖包


这下找到为什么项目中第二天为啥跑不起来了,原来Linux的定时任务将核心依赖包删除了,并且还会去重启服务。


为了搞破坏,真是煞费苦心啊。还好的是这个jar包在前一天已经反编译出来了,也算有了备份。


小结


原本以为程序员在代码中进行删库操作或做一些其他小手脚只是网络上的段子,大多数人出于职业操守或个人品质是不会做的。没想到这还真遇到了,而且对方为了隐藏删除操作,还做了一些小伪装,真的是煞费苦心啊。如果有这样的能力和心思,用在写出更优秀的代码或系统上或许更好。


当然,不知道他们在交接的过程中到底发生了什么,竟然用这样的方式对待昔日合作的伙伴。之所以写这篇文章,是想让大家学习如何排查代码问题的过程,毕竟用到了不少知识点和技能,但这并不是教大家如何去做手脚。无论怎样,最起码的职业操守还是要有

作者:程序新视界
来源:juejin.cn/post/7140066341469290532
的,这点不接受反驳。

收起阅读 »

为什么App独立开发最好别做日记、记账

我在前几天发了条帖子说(新人)独立开发选择笔(日)记、记账、Todo主流三件套之一等于加速死亡。引发了一点点🤏🏻争议。刚好我在选择 app 方向的时候也深思过这个问题,所以我展开讲讲,分享一下我的想法。 认清自己的定位 在独立开发刚开始做,羽翼未丰的时候,你对...
继续阅读 »

我在前几天发了条帖子说(新人)独立开发选择笔(日)记、记账、Todo主流三件套之一等于加速死亡。引发了一点点🤏🏻争议。刚好我在选择 app 方向的时候也深思过这个问题,所以我展开讲讲,分享一下我的想法。


认清自己的定位


在独立开发刚开始做,羽翼未丰的时候,你对自己的定位就应该是一个游击队队员。这也是独立开发的天然优势,因为小,所以灵活机动。游击战的核心奥义是流动性和速决性。因为你没有一个根据地需要防守,你可以找到一个薄弱的地方进攻。因为这个地方薄弱,所以你集中优势兵力可以很快的决胜。因为你小,所以赚到一笔是一笔。因为你是在边缘薄弱的地方建立的优势,这个小细分市场的利润不足以吸引大部队来,你就有了自己的根据地。你想想红军的根据地都是在什么地方,都是在两省交界的山里。可没听说根据地在上海、北京的。


举个例子,一个小业务,几个月工作量,可以有10万利润。对于一个互联网团队是看不上的。一个完整的互联网团队,一个CEO,一个产品,一个设计,两个研发(前端+后端),一个测试,加上行政,加上公司的运营费用。这一套组合拳摊下来,为了组织的长期利益,短期的一个20万利润的项目他们是看不上的。但是如果你是独立开发,两个人搞两个月,赚10万,你做不做?或者说你能做吗?你能做的,只是赚的不多。但是公司这个事情就没法做。这就是大象踩不死蚂蚁的道理。有人觉得就算两个人两个月赚10万,一个月人均25000也不是什么大钱。确实不是很多的钱,但这也是独立开发普遍心态上的一个问题:你想要的太多。平民独立开发早期最关键的是找到一个持续赚钱的方式,你有实力让自己活下来,后期就可以慢慢发展。而不是一上来就觉得一年要赚100万,然后以一个游击队的姿态去攻打大城市。你要赚大钱,就要有对应的能力。但是很多起步的独立开发者并没有这样的能力,那就是一个不匹配的目标了。


局部创新在笔记领域不是决定性的优势


很多人觉得我做笔记,我有一个想法,我有一个痛点,市面上的笔记没有。我进行了一个局部创新,可能是更好看一点点的设计,或者一个特别的笔记格式。我有一个局部优势,我可以做,我入场。


第一个问题:某些品类的局部优势没有决定性。换一句话说,也许你的确解决了一个其他笔记没有的痛点,但是这个不能转化成你的产品整体优势。不足以转化成足够的收入。


如果你解决了一个普遍痛点,一个高商业价值的痛点,有什么理由现有的成熟笔记app不做呢?请问你作为一个独立开发者开发这个功能要做多久?你有没可能一年里没有任何风声开发一个杀手功能,出来的时候瞬间占领用户心智,用户蜂拥而来?在一个成熟品类里,这样很难的。诺曼底登陆不是游击队能做到的。你要是有这个能力和信心,你就不应该做独立开发,你应该成立公司高举高打。真实的现状是如果你的一个功能受欢迎,成熟的笔记团队花一两个月也就做出来了。


而且笔记类还有一个特点:他有数据积累。用户价值=新体验-旧体验-替换成本。假设你的新体验确实有优势,大于现有产品的旧体验,但是笔记类的替换成本是很高的。我已经在这里积累了好几年的数据,一个新的、个人的、上线没多久的数据积累类,替换成本是很高的。我需要同时愿意抛弃我的旧数据,还需要对你建立信任。 所以除非领头的app犯错误,否则后来的小app是没有机会超过他的。因为他有先发优势,他可以在看到你之后,完善自己,提高自己的旧体验。你不能静态的认为头部的成功的app会停在那里。


成熟品类的高入局门槛


笔记类app自 AppStore 开始有以来,就有人做了。这意味着在你的 app 上线之前,高付费意愿的用户已经付费购买过了。这意味着,如果你要获得存量用户,你需要超越现有品类的成功 app,你需要有更好的整体体验,你还需要有足够全面的运营能力。让这些买了其他 app 的用户愿意选择你。你做笔记 app,一上线,真正的笔记用户心里已经有一个对标的门槛了。本来你在农村盖个楼,是个一层土楼也可以,是个木楼也行,大家都认可是个楼。你在市中心盖一个土楼,大家就会觉得你简陋,功能不全了。用户心智里已经把这个品类的成熟应用当做了一个基线。


再说新的用户。新的用户如果要用一个笔记,上 AppStore 搜,上社交平台搜,肯定看到的大量都是成熟 app 的推荐。他们本来就是历经时代活下来的,自然是得到了用户的认可。既然存在了这么久,自然知道他们的人就多。所以你在自然新增流量上也很劣势。


所以你做一个成熟品类,意味着你要成功,你就必须有一个极具说服力的产品优势,加足够好的产品质量,加足够高效的运营。


我有两个理财项目,一个项目利润年化10%,一个项目利润年后1%,你选哪个?你既然都是在做新产品,为什么要选一个更难的赛道?是你觉得自己命中注定要开发一个笔记app,还是你贫瘠的想象力只想到了做笔记?你确定是在100个产品评估出的最好的方向是笔记?


对比一个新市场,比如最近很火的套壳的 gpt 应用。这是一个全新品类,意味着用户心里是没有对标品的。当用户下载你的 app 时,他不会期待你应该有怎样的功能,他没有对比的对象。这个品类里因为没有头部应用,大家搜索的时候也就看到什么下什么。你会有自然新增流量。因为大家都是差不多时间起步的,其他app不会领先很多,意味着你也有可能建立产品优势,至少你没什么太大的劣势,更有希望建立用户口碑。


长线和短线


投资理财主要有两个流派:长线和短线。长线就关注这个企业未来长期的发展,关注企业的价值。如果这个企业是低估的,就可以持有,因为他们评估出未来这个企业会成长。短期的波动对他们并不构成干扰。大概就是大家口中说的价值投资吧。还有一类是短线,他们不关注未来的情况,买入一只股票只关心这只股票下个月会不会涨,明天会不会涨。因此这个股票的已经100倍PE对他们也没影响,后面有人接盘就可以了。量化交易大多是这样的短线逻辑,于是他们有着高换手率。


长线和短线都是合理的策略。最大的问题是,你不能用抱着长线的心态做短线。这大概就是接盘股民的心态吧,他们持有一只股票的时候觉得这个企业未来会成长。但是短期市场遇冷跌了20%,看到很多人抛了,他们就觉得受不了了,于是割肉离场。买的时候听的是做长线人的意见,卖的时候是跟着做短线的人卖的。


把这个逻辑放到 app 上也是这样的。笔记 app 是一个长线价值,越到后面越值钱,做的越久产品优势越大,用户粘性越高。但是你抱着短线的心态进来做,做了4 个月,收入用户都没起色,你怀疑自己,团队开始有意见,于是你就放弃了。这就是问题所在了,大多数独立开发者没有耐心在一个赛道持续亏损做两年,不具备做长线的心态和财力。


所以我建议独立开发起步的时候多关注短线价值。就是你投入三五个月,能有起色的方向。做三五个月,要不就要赚钱,要不就要有用户口碑。一鸟在手胜过二鸟在林。等你解决了起步的时候生存问题,再考虑农村包围城市的问题。


谈谈番茄钟


也有人觉得番茄钟是独立开发的重灾区。虽然番茄钟似乎是一个红海了,但是我却觉得番茄钟反而是一个可以做的赛道。不知道大家有没有留意一下这个有这个功能的app,似乎万物都可以番茄钟。谜底时钟有番茄钟,滴答清单有番茄钟。番茄钟在设计上可以极简,可以是我的番茄,可以是小鸡,可以是面条,可以是像素。


image.png
IMG\_5978.png
IMG\_5979.png
IMG\_5980.png

那么为什么番茄钟可以呢?


番茄钟替换成本低。因为是及时性的工具类,历史积累的番茄时间统计并不太重要。在功能操作上也很简单,核心交互就是点击一下开始计时,25分钟后提醒你要休息5分钟。很容易可以完成一个基础任何。


设计差异化可以成为付费点,市场容量大。 因为交互很简单,所以做出突出的设计成本可以接受。因为这个品类里,设计可以成为卖点,设计又是一个各有所好的事情,因此天然会有很多不同的需求。某个番茄钟可能做的很好用,很好看,但是他不可能吃掉全部用户。一件短袖设计的再好看,也不可能让所有人都买单。


由此我们可以得出一个结论:番茄钟有短线价值。并且番茄钟引入养成和成就体系以后,有可能变成一个长线产品。如果你非要卷,去卷番茄钟吧。


最后


地上有两张钱,一张100,一张10块,你先捡哪张?我想结果是不言而喻的。独立开发者的一个生态位优势就是可以在一个小领域建立极高的人效比。这个小领域有两个可能:一个是这个领域太小太细分,只能容得下小团队做(高人效);这个领域是新的,还没人知道这里行不行,于是小成本的独立开发先做了(高灵活)。建议各位独立开发者如果要做死亡加速三件套的产品的话三思而后行。


作者:没故事的卓同学
来源:juejin.cn/post/7265967971162898487
收起阅读 »

万能的异步处理方案

异步处理通用方案 前言 良好的系统设计必须要做到开闭原则,随着业务的不断迭代更新,核心代码也会被不断改动,出错的概率也会大大增加。但是大部分增加的功能都是在扩展原有的功能,既要保证性能又要保证质量,我们往往都会使用异步线程池来处理,然而却增加了很多不确定性因素...
继续阅读 »

异步处理通用方案


前言


良好的系统设计必须要做到开闭原则,随着业务的不断迭代更新,核心代码也会被不断改动,出错的概率也会大大增加。但是大部分增加的功能都是在扩展原有的功能,既要保证性能又要保证质量,我们往往都会使用异步线程池来处理,然而却增加了很多不确定性因素。 由此我设计了一套通用的异步处理SDK,可以很轻松的实现各种异步处理


目的


通过异步处理不仅能够保证方法能够得到有效的执行而且不影响主流程


更重要的是各种兜底方法保证数据不丢失,从而达到最终一致性\color{red}最终一致性


优点


无侵入设计,独立数据库,独立定时任务,独立消息队列,独立人工执行界面(统一登录认证)


使用spring事务事件机制,即使异步策略解析失败也不会影响业务


如果你的方法正在运行事务,会等事务提交后再处理事件


就算事务提交了,异步策略解析失败了,我们还有兜底方案执行(除非数据库有问题,消息队列有问题,方法有bug)


组件


kafka 消息队列


xxl job 定时任务


mysql 数据库


spring 切面


vue 界面


设计模式


策略


模板方法


动态代理


流程图


image.png


数据库脚本


CREATE TABLE `async_req` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`application_name` varchar(100) NOT NULL DEFAULT '' COMMENT '应用名称',
`sign` varchar(50) NOT NULL DEFAULT '' COMMENT '方法签名',
`class_name` varchar(200) NOT NULL DEFAULT '' COMMENT '全路径类名称',
`method_name` varchar(100) NOT NULL DEFAULT '' COMMENT '方法名称',
`async_type` varchar(50) NOT NULL DEFAULT '' COMMENT '异步策略类型',
`exec_status` tinyint NOT NULL DEFAULT '0' COMMENT '执行状态 0:初始化 1:执行失败 2:执行成功',
`exec_count` int NOT NULL DEFAULT '0' COMMENT '执行次数',
`param_json` longtext COMMENT '请求参数',
`remark` varchar(200) NOT NULL DEFAULT '' COMMENT '业务描述',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_applocation_name` (`application_name`) USING BTREE,
KEY `idx_exec_status` (`exec_status`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='异步处理请求';

CREATE TABLE `async_log` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`async_id` bigint NOT NULL DEFAULT '0' COMMENT '异步请求ID',
`error_data` longtext COMMENT '执行错误信息',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_async_id` (`async_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='异步处理日志';

异步策略


image.png


安全级别


image.png


执行状态


image.png


流程图


image.png


image.png
image.png

apollo 配置


# 开关:默认关闭
scm.async.enabled=true

# 数据源 druid
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/fc_async?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowMultiQueries=true&rewriteBatchedStatements=true
spring.datasource.username=user
spring.datasource.password=xxxx
spring.datasource.filters=config
spring.datasource.connectionProperties=config.decrypt=true;config.decrypt.key=yyy
#静态地址
spring.resources.add-mappings=true
spring.resources.static-locations=classpath:/static/


# 以下配置都有默认值
# 核心线程数
async.executor.thread.corePoolSize=10
# 最大线程数
async.executor.thread.maxPoolSize=50
# 队列容量
async.executor.thread.queueCapacity=10000
# 活跃时间
async.executor.thread.keepAliveSeconds=600

# 执行成功是否删除记录:默认删除
scm.async.exec.deleted=true

# 自定义队列名称前缀:默认应用名称
scm.async.topic=应用名称

# 重试执行次数:默认5次
scm.async.exec.count=5

# 重试最大查询数量
scm.async.retry.limit=100

# 补偿最大查询数量
scm.async.comp.limit=100

用法


1,异步开关
scm.async.enabled=true

2,在需要异步执行的方法加注解 (必须是spring代理方法)
@AsyncExec(type = AsyncExecEnum.SAVE_ASYNC, remark = "数据字典")

3,人工处理地址
http://localhost:8004/async/index.html

注意


1,应用名称
spring.application.name

2,队列名称
${scm.async.topic:${spring.application.name}}_async_queue
自定义topic:scm.async.topic=xxx

3,自己业务要做幂等

4,一个应用公用一个队列
自产自消

5,定时任务
异步重试定时任务(2分钟重试一次,可配置重试次数)
异步补偿定时任务(一小时补偿一次,创建时间在一小时之前的)

效果展示


image.png


image.png


作者:三火哥
来源:juejin.cn/post/7266087843239084090
收起阅读 »

电话背调,我给他打了8分

前段时间招聘的一位开发,待了两三周,拿到了京东的offer,离职了。在离职的后一天,接到了他新公司的背调电话,几乎每项都给他打了8分。这个分数打的有点虚,单纯只是为了不影响他下家的入职。 离职之前,收到他在飞书上查看电话号码的消息,大概也猜到是在填写背调人信息...
继续阅读 »

前段时间招聘的一位开发,待了两三周,拿到了京东的offer,离职了。在离职的后一天,接到了他新公司的背调电话,几乎每项都给他打了8分。这个分数打的有点虚,单纯只是为了不影响他下家的入职。


离职之前,收到他在飞书上查看电话号码的消息,大概也猜到是在填写背调人信息,但自始至终,他也没打一声招呼,让给个好评。


离职最后一天,办完手续,没跟任何人打一个招呼,不知什么时候就消失了。


当初他刚入职一周时,其实大家都已经看出他在沟通上有很大问题,还想着如何对他有针对性的安排工作和调整,发挥他的长处,避免他的短处。但没想到这么快就离职了。在他提离职时,虽没过多挽留,但给了一些过来人的建议,很明显也听不进去。


站在旁观者的角度来看,他的职业生涯或即将面临到的事几乎能看得清清楚楚,但他有自己的坚持,别人是没办法的。


就着这事,聊聊最近对职场上关于沟通的一些思考:


第一,忌固执己见


职场中最怕遇到的一种人就是固执己见的人。大多数聪明人,在遇到固执己见的人时,基本上都会在三言两语之后停止与其争辩。因为,人一旦在自己的思维层次形成思维闭环,是很难被说服的。


而对于固执己见的人,失去的是新的思维、新的思想、纠错学习的机会,甚至是贵人的相助。试想一下,本来别人好像给你提建议,指出一条更好的路,结果换来的是争辩,是抬杠,聪明人都会敬而远之,然后默默地在旁边看着你掉坑里。


真正牛的人,基本上都是兼听则明,在获得各类信息、建议之后,综合分析,为己所用。


第二,不必说服,尊重就好


站在另外一个方面,如果一件事与己无关,别人有不同的意见,或者这事本身就是别人负责,那么尊重就好,不必强行说服对方,不必表现自己。


曾看到两个都很有想法的人,为一件事争论好几天,谁也无法说服谁。一方想用权力压另一方,另一方也不care,把简单的事情激化,急赤白脸的。


其实争论的核心只是展现形式不同而已,最终只是在争情绪、争控制感、争存在感而已,大可不必。


对于成年人,想说服谁都非常难的。而工作中的事,本身就没有对错,只有优劣,大多数时候试一下就知道了。


有句话说的非常好,“成年人的世界只做筛选,不做教育”。如果说还能做点什么,那就是潜移默化的影响别人而已。


第三,不懂的领域多听少说


如果自己对一个领域不懂,最好少发表意见,多虚心学习、请教即可。任正非辞退写《万言书》的员工的底层逻辑就是这个,不懂,不了解情况,还草率提建议,只是哗众取宠、浪费别人时间。


如果你不懂一个领域,没有丰富的背景知识和基础理论支撑,在与别人沟通的过程中,强行提建议,不仅露怯,还会惹人烦。即便是懂,也需要先听听别人的看法和视角解读。


站在另一个角度,如果一个不懂的人来挑战你的权威,质疑你的决定,笑一笑就好,不必与其争辩。


郭德纲的一段相声说的好:如果你跟火箭专家说,发射火箭得先抱一捆柴,然后用打火机把柴点着,发射火箭。如果火箭专家看你一眼,就算他输。


第四,没事多夸夸别人


在新公司,学到的最牛的一招就是夸人。之前大略知道夸人的效果,但没有太多的去实践。而在新公司,团队中的几个大佬,身体力行的在夸人。


当你完成一件事时,夸“XXX,真牛逼!”,当你解决一个问题时,夸“还得是XXX,不亏是这块的专家”。总之,每当别人有好的表现时,总是伴随着夸赞和正面响应。于是整个团队的氛围就非常好。


这事本身也不需要花费什么成本,就是随口一句话的事,而效果却非常棒。与懂得“人捧人,互相成就彼此,和气生财”的人相处,是一种非常愉悦的体验。


前两天看到一条视频,一位六七岁的小姑娘指派正在玩游戏的父亲去做饭,父亲答应了。她妈妈问:你是怎么做到的?她说:夸他呀。


看看,这么小的小孩儿都深谙的人性,我们很多成人却不懂,或不愿。曾经以为开玩笑很好,现在发现“夸”才是利器,同时一定不要开贬低性的玩笑。


其实,职场中还有很多基本的沟通规则,比如:分清无效沟通并且及时终止谈话、适当示弱、认真倾听,积极反馈、少用反问等等。


当你留意和思考这些成型的规则时,你会发现它们都是基于社会学和心理学的外在呈现。

作者:程序新视界
来源:juejin.cn/post/7265978883123298363
很有意思,也很有用。

收起阅读 »

AI欣赏-街头少女🔥🔥🔥

描述 💯💯💯 你更喜欢哪一位选手呢? 自己制作的一些AI绘画关键词,一是为了保存下来,二是分享给大家看看,可以收集一些意见和建议、想法等等。所以欢迎大家踊跃发言! 本次文章的描述主题如下:一个女孩在街头大笑,湿透的,辫子,写实风格的,电影级别的,HDR的。Lo...
继续阅读 »

描述


💯💯💯 你更喜欢哪一位选手呢?


自己制作的一些AI绘画关键词,一是为了保存下来,二是分享给大家看看,可以收集一些意见和建议、想法等等。所以欢迎大家踊跃发言!


本次文章的描述主题如下:一个女孩在街头大笑,湿透的,辫子,写实风格的,电影级别的,HDR的。

  • Lora:无

  • Embeddings:ng_deepnegative_v1_75t [1a3e]


Prompt



a young woman, street, laughing, ponytails, (hdr:1.3), (muted colors:1.2), dramatic, complex background, cinematic, filmic, (rutkowski, artstation:0.8), soaking wet,



Negative Prompt



(nsfw:2),Multiple people,easynegative,(worst quality:2),(low quality:2),lowres,(monochrome:1.4),(grayscale:1.4),big head,severed legs,short legs,missing legs,acnes,skin blemishes,age spot,backlight,(ugly:1.4),(duplicate:1.4),(morbid:1.2),(mutilated:1.2),mutated hands,(poorly drawn hands:1.4),blurry, (bad anatomy:1.4),(bad proportions:1.4),(disfigured:1.4),(unclear eyes:1.4),bad hands, bad tooth,missing fingers,extra digit,bad body,NG_DeepNegative_V1_75T,glans,EasyNegative:0.5,gross proportions.short arm,(missing arms:1.4),missing thighs,missing calf,mutation,duplicate,morbid,mutilated,poorly drawn cloth,strange finger,bad finger,(mutated hands and fingers:1.4),(text:1.4), bad-artist, bad_prompt_version2, bad-hands-5, bad-image-v2-39000,



基础配置




生成图的效果展示


选手1




选手2




选手3




选手4




选手5




选手6




选手7




选手8




选手9




投票


🌺 请开始诸位的投票吧!评论区见!!


作者:襄垣
链接:https://juejin.cn/post/7223267912727298103
来源:稀土掘金
收起阅读 »

iPhone两秒出图,目前已知的最快移动端Stable Diffusion模型来了

Stable Diffusion (SD)是当前最热门的文本到图像(text to image)生成扩散模型。尽管其强大的图像生成能力令人震撼,一个明显的不足是需要的计算资源巨大,推理速度很慢:以 SD-v1.5 为例,即使用半精度存储,其模型大小也有 1.7...
继续阅读 »

Stable Diffusion (SD)是当前最热门的文本到图像(text to image)生成扩散模型。尽管其强大的图像生成能力令人震撼,一个明显的不足是需要的计算资源巨大,推理速度很慢:以 SD-v1.5 为例,即使用半精度存储,其模型大小也有 1.7GB,近 10 亿参数,端上推理时间往往要接近 2min。


为了解决推理速度问题,学术界与业界已经开始对 SD 加速的研究,主要集中于两条路线:(1)减少推理步数,这条路线又可以分为两条子路线,一是通过提出更好的 noise scheduler 来减少步数,代表作是 DDIM [1],PNDM [2],DPM [3] 等;二是通过渐进式蒸馏(Progressive Distillation)来减少步数,代表作是 Progressive Distillation [4] 和 w-conditioning [5] 等。(2)工程技巧优化,代表作是 Qualcomm 通过 int8 量化 + 全栈式优化实现 SD-v1.5 在安卓手机上 15s 出图 [6],Google 通过端上 GPU 优化将 SD-v1.4 在三星手机上加速到 12s [7]。


尽管这些工作取得了长足的进步,但仍然不够快。


近日,Snap 研究院推出最新高性能 Stable Diffusion 模型,通过对网络结构、训练流程、损失函数全方位进行优化,在 iPhone 14 Pro 上实现 2 秒出图(512x512),且比 SD-v1.5 取得更好的 CLIP score。这是目前已知最快的端上 Stable Diffusion 模型!



论文地址:arxiv.org/pdf/2306.00…
Webpage: snap-research.github.io/SnapFusion


核心方法


Stable Diffusion 模型分为三部分:VAE encoder/decoder, text encoder, UNet,其中 UNet 无论是参数量还是计算量,都占绝对的大头,因此 SnapFusion 主要是对 UNet 进行优化。具体分为两部分:(1)UNet 结构上的优化:通过分析原有 UNet 的速度瓶颈,本文提出一套 UNet 结构自动评估、进化流程,得到了更为高效的 UNet 结构(称为 Efficient UNet)。(2)推理步数上的优化:众所周知,扩散模型在推理时是一个迭代的去噪过程,迭代的步数越多,生成图片的质量越高,但时间代价也随着迭代步数线性增加。为了减少步数并维持图片质量,我们提出一种 CFG-aware 蒸馏损失函数,在训练过程中显式考虑 CFG (Classifier-Free Guidance)的作用,这一损失函数被证明是提升 CLIP score 的关键!


下表是 SD-v1.5 与 SnapFusion 模型的概况对比,可见速度提升来源于 UNet 和 VAE decoder 两个部分,UNet 部分是大头。UNet 部分的改进有两方面,一是单次 latency 下降(1700ms -> 230ms,7.4x 加速),这是通过提出的 Efficient UNet 结构得到的;二是 Inference steps 降低(50 -> 8,6.25x 加速),这是通过提出的 CFG-aware Distillation 得到的。VAE decoder 的加速是通过结构化剪枝实现。




下面着重介绍 Efficient UNet 的设计和 CFG-aware Distillation 损失函数的设计。


(1)Efficient UNet


我们通过分析 UNet 中的 Cross-Attention 和 ResNet 模块,定位速度瓶颈在于 Cross-Attention 模块(尤其是第一个 Downsample 阶段的 Cross-Attention),如下图所示。这个问题的根源是因为 attention 模块的复杂度跟特征图的 spatial size 成平方关系,在第一个 Downsample 阶段,特征图的 spatial size 仍然较大,导致计算复杂度高。




为了优化 UNet 结构,我们提出一套 UNet 结构自动评估、进化流程:先对 UNet 进行鲁棒性训练(Robust Training),在训练中随机 drop 一些模块,以此来测试出每个模块对性能的真实影响,从而构建一个 “对 CLIP score 的影响 vs. latency” 的查找表;然后根据该查找表,优先去除对 CLIP score 影响不大同时又很耗时的模块。这一套流程是在线自动进行,完成之后,我们就得到了一个全新的 UNet 结构,称为 Efficient UNet。相比原版 UNet,实现 7.4x 加速且性能不降。


(2)CFG-aware Step Distillation


CFG(Classifier-Free Guidance)是 SD 推理阶段的必备技巧,可以大幅提升图片质量,非常关键!尽管已有工作对扩散模型进行步数蒸馏(Step Distillation)来加速 [4],但是它们没有在蒸馏训练中把 CFG 纳入优化目标,也就是说,蒸馏损失函数并不知道后面会用到 CFG。这一点根据我们的观察,在步数少的时候会严重影响 CLIP score。


为了解决这个问题,我们提出在计算蒸馏损失函数之前,先让 teacher 和 student 模型都进行 CFG,这样损失函数是在经过 CFG 之后的特征上计算,从而显式地考虑了不同 CFG scale 的影响。实验中我们发现,完全使用 CFG-aware Distillation 尽管可以提高 CLIP score, 但 FID 也明显变差。我们进而提出了一个随机采样方案来混合原来的 Step Distillation 损失函数和 CFG-aware Distillation 损失函数,实现了二者的优势共存,既显著提高了 CLIP score,同时 FID 也没有变差。这一步骤,实现进一步推理阶段加速 6.25 倍,实现总加速约 46 倍。


除了以上两个主要贡献,文中还有对 VAE decoder 的剪枝加速以及蒸馏流程上的精心设计,具体内容请参考论文。


实验结果


SnapFusion 对标 SD-v1.5 text to image 功能,目标是实现推理时间大幅缩减并维持图像质量不降,最能说明这一点的是下图:




该图是在 MS COCO’14 验证集上随机选取 30K caption-image pairs 测算 CLIP score 和 FID。CLIP score 衡量图片与文本的语义吻合程度,越大越好;FID 衡量生成图片与真实图片之间的分布距离(一般被认为是生成图片多样性的度量),越小越好。图中不同的点是使用不同的 CFG scale 获得,每一个 CFG scale 对应一个数据点。从图中可见,我们的方法(红线)可以达到跟 SD-v1.5(蓝线)同样的最低 FID,同时,我们方法的 CLIP score 更好。值得注意的是,SD-v1.5 需要 1.4min 生成一张图片,而 SnapFusion 仅需要 1.84s,这也是目前我们已知最快的移动端 Stable Diffusion 模型!


下面是一些 SnapFusion 生成的样本:




更多样本请参考文章附录。


除了这些主要结果,文中也展示了众多烧蚀分析(Ablation Study)实验,希望能为高效 SD 模型的研发提供参考经验:


(1)之前 Step Distillation 的工作通常采用渐进式方案 [4, 5],但我们发现,在 SD 模型上渐进式蒸馏并没有比直接蒸馏更有优势,且过程繁琐,因此我们在文中采用的是直接蒸馏方案。




(2)CFG 虽然可以大幅提升图像质量,但代价是推理成本翻倍。今年 CVPR’23 Award Candidate 的 On Distillation 一文 [5] 提出 w-conditioning,将 CFG 参数作为 UNet 的输入进行蒸馏(得到的模型叫做 w-conditioned UNet),从而在推理时省却 CFG 这一步,实现推理成本减半。但是我们发现,这样做其实会造成图片质量下降,CLIP score 降低(如下图中,四条 w-conditioned 线 CLIP score 均未超过 0.30, 劣于 SD-v1.5)。而我们的方法则可以减少步数,同时将 CLIP score 提高,得益于所提出的 CFG-aware 蒸馏损失函数!尤其值得主要的是,下图中绿线(w-conditioned, 16 steps)与橙线(Ours,8 steps)的推理代价是一样的,但明显橙线更优,说明我们的技术路线比 w-conditioning [5] 在蒸馏 CFG guided SD 模型上更为有效。




(3)既有 Step Distillation 的工作 [4, 5] 没有将原有的损失函数和蒸馏损失函数加在一起,熟悉图像分类知识蒸馏的朋友应该知道,这种设计直觉上来说是欠优的。于是我们提出把原有的损失函数加入到训练中,如下图所示,确实有效(小幅降低 FID)。




总结与未来工作


本文提出 SnapFusion,一种移动端高性能 Stable Diffusion 模型。SnapFusion 有两点核心贡献:(1)通过对现有 UNet 的逐层分析,定位速度瓶颈,提出一种新的高效 UNet 结构(Efficient UNet),可以等效替换原 Stable Diffusion 中的 UNet,实现 7.4x 加速;(2)对推理阶段的迭代步数进行优化,提出一种全新的步数蒸馏方案(CFG-aware Step Distillation),减少步数的同时可显著提升 CLIP score,实现 6.25x 加速。总体来说,SnapFusion 在 iPhone 14 Pro 上实现 2 秒内出图,这是目前已知最快的移动端 Stable Diffusion 模型。


未来工作:


1.SD 模型在多种图像生成场景中都可以使用,本文囿于时间,目前只关注了 text to image 这个核心任务,后期将跟进其他任务(如 inpainting,ControlNet 等等)。




  1. 本文主要关注速度上的提升,并未对模型存储进行优化。我们相信所提出的 Efficient UNet 仍然具备压缩的空间,结合其他的高性能优化方法(如剪枝,量化),有望缩小存储,并将时间降低到 1 秒以内,离端上实时 SD 更进一步。




参考文献


[1] Denoising Diffusion Implicit Models, ICLR’21


[2] Pseudo Numerical Methods for Diffusion Models on Manifolds, ICLR’22


[3] DPM-Solver: A Fast ODE Solver for Diffusion Probabilistic Model Sampling in Around 10 Steps, NeurIPS’22


[4] Progressive Distillation for Fast Sampling of Diffusion Models, ICLR’22


[5] On Distillation of Guided Diffusion Models, CVPR’23


[6] http://www.qualcomm.com/news/onq/20…


[7] Speed Is All You Need: On-Device Acceleration of Large Diffusion Models via GPU-Aware Optimizations, CVPR’23 Workshop


作者:机器之心
链接:https://juejin.cn/post/7244452476191850557
来源:稀土掘金
收起阅读 »

使用脚本更新 macOS 壁纸,让你每天看到不同的美景🖼️

在macOS系统中,我们可以轻松地更换桌面壁纸。但是,如果你每天都想要一张新的壁纸,手动更换就会变得十分繁琐。幸运的是,我们可以使用bash脚本和unsplash API自动更新壁纸。 步骤1:获取unsplash API密钥 首先,你需要注册一个unspla...
继续阅读 »

在macOS系统中,我们可以轻松地更换桌面壁纸。但是,如果你每天都想要一张新的壁纸,手动更换就会变得十分繁琐。幸运的是,我们可以使用bash脚本和unsplash API自动更新壁纸。


步骤1:获取unsplash API密钥


首先,你需要注册一个unsplash账户,并申请一个API密钥。这个API密钥将允许你通过编程方式访问unsplash图片库。


步骤2:编写bash脚本


创建一个新的文本文件,然后在其中添加以下代码:

#!/bin/bash

# set the unsplash API access key
access_key="YOUR_UNSPLASH_API_ACCESS_KEY"

# define the query to search for wallpaper images
query="nature"

# search for a random wallpaper image
result=$(/usr/bin/curl -s -H "Authorization: Client-ID $access_key" "https://api.unsplash.com/photos/random?query=$query")

# extract the image URL from the JSON response
image_url=$(echo "$result" | /opt/homebrew/bin/jq -r '.urls.full')

# download the image
/usr/bin/curl -s "$image_url" > ~/Pictures/wallpaper.jpg

# set the image as the desktop wallpaper
osascript -e "tell application \"Finder\" to set desktop picture to POSIX file \"$HOME/Pictures/wallpaper.jpg\""

这段代码会使用unsplash API搜索与“nature”相关的随机图片,并将其下载到“~/Pictures/wallpaper.jpg”文件中。然后,它会使用AppleScript将下载的图片设置为桌面壁纸。


步骤3:运行bash脚本


将文件保存为“update-wallpaper.sh”,然后打开终端并导航到该文件所在的目录。运行以下命令以使脚本可执行:

chmod +x update-wallpaper.sh

现在,你可以通过在终端中输入以下命令来运行脚本:

./update-wallpaper.sh

步骤4:设置定时任务

脚本依赖:curl、jq、bash、unsplash,使用 which 获取路径,然后替换脚本里的curl和jq。

which curl
which jq

你可以将该脚本设置为定时任务,以便每天自动更新壁纸。打开“终端”并输入以下命令以编辑cron定时任务:

crontab -e

然后,添加以下行:

0 9 * * * /path/to/update-wallpaper.sh

这将在每天上午9点运行该脚本。

现在,你可以坐下来,放松一下,让你的macOS自动更新壁纸。享受吧!

作者:FreeCultureBoy
链接:https://juejin.cn/post/7226301946839089211
来源:稀土掘金

收起阅读 »