注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

因为山就在那里

第一次爬北京的山,看到了不一样的风景,也收获到了意外的体验。虽说是3A级景区,但这次给我的体验远不止3A,恰巧赶上了日落,路上遇到了小猫,2000多米的爬山体验,给我的感觉是,棒极了。 千里之行,始于足下 也许是最近在学校待久了吧,课堂上一些公式化的学习生活...
继续阅读 »


第一次爬北京的山,看到了不一样的风景,也收获到了意外的体验。虽说是3A级景区,但这次给我的体验远不止3A,恰巧赶上了日落,路上遇到了小猫,2000多米的爬山体验,给我的感觉是,棒极了。




千里之行,始于足下


也许是最近在学校待久了吧,课堂上一些公式化的学习生活有时让人觉得些许枯燥,刚好最近特别想去爬山,想去那就去呗!于是乎,我咨询了一下经常在北京各大山区骑行游览的同学,他随手推荐了离学校不远的蟒山。我上地图搜了一下,在网上简略地看了一下攻略,就决定出发了。没想到的是,另外有两位同学也打算出去走走,我说,那就一起呗,反正也不远。就这样,我收获了两个小伙伴。出发前,我东西收拾得差不多了,但我依然还在想是不要再准备些什么?思索了一下,我觉得不用再考虑太多了,千里之行,始于足下,想做就直接去做先,直接行动起来!




山路崎岖,拥抱自然


在到达正式爬山之前,其实还是经历了许多的小插曲。当天我们三人是坐地铁到达关东站,然后骑着共享单车去到蟒山国家森林公园的入口。但是骑到一半突然觉出似乎山区附近没有停车点,于是本来已经快到山脚下的我们决定原路返回一段距离去最近的停车点。好在最近停车点不远,也没有迂回多少。之后我们开始等公交,可等了一会儿也没见车来,我索性提出直接打车,然后刚打车就有师傅接单了,发现路费也不贵,其实也就10来分钟的路程,不过上山有点坡度,所以骑车可能会久些。就这样我们来到了入口。检完票进去后就面临路线的选择,我想着先从山脚走起吧,就先往文化广场的方向走去。蟒山森林公园其实好多小景点都别有一番风味。


一走进森林公园,给我的感觉就是:“清爽”!在这里,你可以深吸一口气,不用担心烟尘啥的,这里的空气让人很舒服。旁边这些小树林也别有也一番风味,阳关透过枝叶映射在地面上,一条一条的,很有感觉。最重要的是,这里人流很少,是真正的清净!路上有很多的休息椅子,累了也能随时坐会儿,开始给我一种真正世外桃源的感觉。





在体验一番过后,突然来到了观蟒台,说实话, 我好像没咋观出哪里是蟒,可能是还在蟒山脚的缘故,看得不是很开阔,但是风景还是不错的。之后我们开始往山上的方向走去,途中经过了蟒山叠翠、碑林、然后就到了文化广场,广场上面有个水池(忘记叫什么了)。这些景点我个人感觉不是很惊艳,但也算是有该有的吧,水池是个环形的,能看到山的倒影,也算还行吧。




山不向我,我向山去


经过这些小景点的兜兜转转后,我们才算最终踏上了通向山顶的道路,也能明显地感到坡道有些坡度了。我们往五观台的方向爬去,五观台,听说是观五样东西,一个是京城什么的,一个是水库什么的,其他几个也记不太清了,反正就是看五样不同的景观。开始上去的时候有路牌提示,说据顶点还有2000来米,大约需要2个小时,当时看了还是有些吃惊的,竟然这么高吗?没事,反正我也没咋去过,就直接上呗。于是,一个接着一个的台阶就这样在我的脚下流过。



开始的山景似乎没特别惊艳的,因为也许还没到半山腰,景色不是很丰富,但也有不同的植被,也能看到一些小山的景色。一开始觉得没什么难爬的,可能会稍微耗点体力,但是走了一小阵后发现确实有些小累,于是我们开始稍微歇会儿。歇会儿过后又继续前进,路上也碰到了一些来爬山的人,不过也不多。我们一路走着,路上也有不一样的景色,当我们走着走着时,发现太阳似乎快要落山了,有一个角度望去特别的好看,突然的这样的景色也让我给惊艳到了。





不知为什么,从此刻起,突然觉得现在山上的景色好好看,让原本些许累的我们也放松了一下,脚上的累也消了不少。就这样,我们一边欣赏着夕阳,一边继续前行。路上还遇上了一只“学长”,“学长”挺胖挺可爱的。



太阳马上就落山了,我们也目睹了这场绝美的日落







其实太阳落得很快,估计就几秒的时间,可能美好的东西总是很短暂吧!这也让我想起了“夕阳无限好,只是近黄昏”,但是呢,我又觉着“莫道桑榆晚,为霞尚满天”,虽然从岁数来说,放在我们身上不太合适吧,但是我觉得从做事角度,其实还是要怀抱希望,做自己热爱的,现在出发去做,不会太迟。我没有用手机录下它日落那一瞬,而更多是用眼睛去感受了。


半山腰有些拥挤,到山顶看看




太阳已经落山了,但距离山顶还有一段距离,考虑到待会还要下山,心里有些犹豫。但是呢,山顶就在眼前了,总得去看看吧,反正现在也不算太晚,路上还有些人,于是我们就一鼓作气,冲向了山顶。



可能是冬天+北方吧,天黑得比较快,这会儿月亮也出来了,人们也点起了灯火,从山顶上望去,夜景还是挺好看的。




天黑了,也不敢待太久,稍微看看后就下山去了,幸好我们有三个人,不然我还真不敢上来。


总结


本次爬山给我的收获还是挺大的,在山里面,花花草草,虫虫鸟鸟,一山一木,一水一露,让人感到十分清净。我觉得的是,得顺其自然,做你想做的事,有想法就直接行动;做你热爱的事情,喜欢才会有动力;做能让你感到开心和快乐的事情,不必背着自己的内心,做一些不开心不快乐的事。很喜欢的的一句话:“为什么要登山?因为山就在那里”。是的,山就在那里,它们不会走动,你去它那,它就会给你反馈,给你舒心。也许它们不会说话,但是它又无时无刻不在与你互动呢?偶遇的绝美日落,半山腰出现的“可爱学长”,问鼎山峰后的夜景,这才是真正的交流,所以,喜欢就去做呗,不用太在意结果,路途中的美景本身就是一种收获,在过程中,也许就能找到答案了。


作者:椰佬小KK
来源:juejin.cn/post/7302371147978113078
收起阅读 »

虽然是我遇到的一个棘手的生产问题,但是我写出来之后,就是你的了。

你好呀,是歪歪。 前几天,就在大家还沉浸在等待春节到来的喜悦氛围的时候,在一个核心链路上的核心系统中,我踩到一个坑的一比的坑,要不是我沉着冷静,解决思路忙中有序,处理手段雷厉风行,把它给扼杀在萌芽阶段了,那这玩意肯定得引发一个比较严重的生产问题。 从问题出现到...
继续阅读 »

你好呀,是歪歪。


前几天,就在大家还沉浸在等待春节到来的喜悦氛围的时候,在一个核心链路上的核心系统中,我踩到一个坑的一比的坑,要不是我沉着冷静,解决思路忙中有序,处理手段雷厉风行,把它给扼杀在萌芽阶段了,那这玩意肯定得引发一个比较严重的生产问题。


从问题出现到定位到这个问题的根本原因,我大概是花了两天半的时间。


所以写篇文章给大家复盘一下啊,这个案例就是一个纯技术的问题导致的,和业务的相关度其实并不大,所以你拿过去直接添油加醋,稍微改改,往自己的服务上套一下,那就是你的了。


我再说一次:虽然现在不是你的,但是你看完之后就是你的了,你明白我意思吧?



表象


事情是这样的,我这边有一个服务,你可以把这个服务粗暴的理解为是一个商城一样的服务。有商城肯定就有下单嘛。


然后接到上游服务反馈,说调用下单接口偶尔有调用超时的情况出现,断断续续的出现好几次了,给了几笔流水号,让我看一下啥情况。当时我的第一反应是不可能是我这边服务的问题,因为这个服务上次上线都至少是一个多月前的事情了,所以不可能是由于近期服务投产导致的。


但是下单接口,你听名字就知道了,核心链接上的核心功能,不能有一点麻痹大意。


每一个请求都很重要,客户下单体验不好,可能就不买了,造成交易损失。


交易上不去营业额就上不去,营业额上不去利润就上不去,利润上不去年终就上不去。


想到这一层关系之后,我立马就登陆到服务器上,开始定位问题。


一看日志,确实是我这边接口请求处理慢了,导致的调用方超时。


为什么会慢呢?



于是按照常规思路先根据日志判断了一下下单接口中调用其他服务的接口相应是否正常,从数据库获取数据的时间是否正常。


这些判断没问题之后,我转而把目光放到了 gc 上,通过监控发现那个时间点触发了一次耗时接近 1s 的 full gc,导致响应慢了。


由于我们监控只采集服务近一周的 gc 数据,所以我把时间拉长后发现 full gc 在这一周的时间内出现的频率还有点高,虽然我还没定位到问题的根本原因,但是我定位到了问题的表面原因,就是触发了 full gc。


因为是核心链路,核心流程,所以此时不应该急着去定位根本原因,而是先缓解问题。


好在我们提前准备了各种原因的应急预案,其中就包含这个场景。预案的内容就是扩大应用堆内存,延缓 full gc 的出现。


所以我当即进行操作报备并联系运维,按照紧急预案执行,把服务的堆内存由 8G 扩大一倍,提升到 16G。


虽然这个方法简单粗暴,但是既解决了当前的调用超时的问题,也给了我足够的排查问题的时间。



定位原因


当时我其实一点都不慌的,因为问题在萌芽阶段的时候我就把它给干掉了。



不就是 full gc 吗,哦,我的老朋友。


先大胆假设一波:程序里面某个逻辑不小心搞出了大对象,触发了 full gc。


所以我先是双手插兜,带着监控图和日志请求,闲庭信步的走进项目代码里面,想要凭借肉眼找出一点蛛丝马迹......



没有任何收获,因为下单服务涉及到的逻辑真的是太多了,服务里面 List 和 Map 随处可见,我很难找到到底哪里是大对象。


但是我还是一点都不慌,因为这半天都没有再次发生 Full GC,说明此时留给我的时间还是比较充足的,


所以我请求了场外援助,让 DBA 帮我导出一下服务的慢查询 SQL,因为我想可能是从数据库里面一次性取的数据太多了,而程序里面也没有做控制导致的。


我之前就踩过类似的坑。


一个根据客户号查询客户有多少订单的内部使用接口,接口的返回是 List<订单>,看起来没啥毛病,对不对?


一般来说一个个人客户就几十上百,多一点的上千,顶天了的上万个订单,一次性拿出来也不是不可以。


但是有一个客户不知道咋回事,特别钟爱我们的平台,也是我们平台的老客户了,一个人居然有接近 10w 的订单。


然后这么多订单对象搞到到项目里面,本来响应就有点慢,上游再发起几次重试,直接触发 Full gc,降低了服务响应时间。


所以,经过这个事件,我们定了一个规矩:用 List、Map 来作为返回对象的时候,必须要考虑一下极端情况下会返回多少数据回去。即使是内部使用,也最好是进行分页查询。


好了,话说回来,我拿到慢查询 SQL 之后,根据几个 Full gc 时间点,对比之后提取出了几条看起来有点问题的 SQL。


然后拿到数据库执行了一下,发现返回的数据量其实也都不大。


此刻我还是一点都不慌,反正内存够用,而且针对这类问题,我还有一个场外援助没有使用呢。


第二天我开始找运维同事帮我每隔 8 小时 Dump 一次内存文件,然后第三天我开始拿着内存文件慢慢分析。


但是第二天我也没闲着,根据现有的线索反复分析、推理可能的原因。


然后在观看 GC 回收内存大小监控的时候,发现了一点点端倪。因为触发 Full GC 之后,发现被回收的堆内存也不是特别多。


当时就想到了除了大对象之外,还有一个现象有可能会导致这个现象:内存泄露。


巧的是在第二天又发生了一次 Full gc,这样我拿到的 Dump 文件就更有分析的价值了。基于前面的猜想,我分析的时候直接就冲着内存泄漏的方向去查了。


我拿着 5 个 Dump 文件,分析了在 5 个 Dump 文件中对象数量一直在增加的对象,这样的对象也不少,但是最终定位到了 FutureTask 对象,就是它:



找到这玩意了再回去定位对应部分的代码就比较容易。


但是你以为定位了代码就完事了吗?


不是的,到这里才刚刚开始,朋友。


因为我发现这个代码对应的 Bug 隐藏的还是比较深的,而且也不是我最开始假象的内存泄露,就是一个纯粹的内存溢出。


所以值得拿出来仔细嗦一嗦。


示例代码


为了让你沉浸式体验找 BUG 的过程,我高低得给你整一个可复现的 Demo 出来,你拿过去就可以跑的那种。


首先,我们得搞一个线程池:



需要说明一下的是,上面这个线程池的核心线程数、最大线程数和队列长度我都取的 1,只是为了方便演示问题,在实际项目中是一个比较合理的值。


然后重点看一下线程池里面有一个自定义的叫做 MyThreadFactory 的线程工厂类和一个自定义的叫做 MyRejectedPolicy 的拒绝策略。


在我的服务里面就是有这样一个叫做 product 的线程池,用的也是这个自定义拒绝策略。


其中 MyThreadFactory 的代码是这样的:



它和默认的线程工厂之间唯一的区别就是我加了一个 threadFactoryName 字段,方便给线程池里面的线程取一个合适的名字。


更直观的表示一下区别就是下面这个玩意:



原生:pool-1-thread-1

自定义:product-pool-1-thread-1



接下来看自定义的拒绝策略:



这里的逻辑很简单,就是当 product 线程池满了,触发了拒绝策略的时候打印一行日志,方便后续定位。


然后接着看其他部分的代码:



标号为 ① 的地方是线程池里面运行的任务,我这里只是一个示意,所以逻辑非常简单,就是把 i 扩大 10 倍。实际项目中运行的任务业务逻辑,会复杂一点,但是也是有一个 Future 返回。


标号为 ② 的地方就是把返回的 Future 放到 list 集合中,在标号为 ③ 的地方循环处理这个 list 对象里面的 Future。


需要注意的是因为实例中的线程池最多容纳两个任务,但是这里却有五个任务。我这样写的目的就是为了方便触发拒绝策略。


然后在实际的项目里面刚刚提到的这一坨逻辑是通过定时任务触发的,所以我这里用一个死循环加手动开启线程来示意:



整个完整的代码就是这样的,你直接粘过去就可以跑,这个案例就可以完全复现我在生产上遇到的问题:


public class MainTest {

    public static void main(String[] args) throws Exception {

        ThreadPoolExecutor productThreadPoolExecutor = new ThreadPoolExecutor(1,
                1,
                1,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(1),
                new MyThreadFactory("product"),
                new MyRejectedPolicy());

        while (true){
            TimeUnit.SECONDS.sleep(1);
            new Thread(()->{
                ArrayList<Future<Integer>> futureList = new ArrayList<>();
                //从数据库获取产品信息
                int productNum = 5;
                for (int i = 0; i < productNum; i++) {
                    try {
                        int finalI = i;
                        Future<Integer> future = productThreadPoolExecutor.submit(() -> {
                            System.out.println("Thread.currentThread().getName() = " + Thread.currentThread().getName());
                            return finalI * 10;
                        });
                        futureList.add(future);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
                for (Future<Integer> integerFuture : futureList) {
                    try {
                        Integer integer = integerFuture.get();
                        System.out.println(integer);
                        System.out.println("future.get() = " + integer);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }

    }

    static class MyThreadFactory implements ThreadFactory {
        private static final AtomicInteger poolNumber = new AtomicInteger(1);
        private final ThreadGr0up group;
        private final AtomicInteger threadNumber = new AtomicInteger(1);
        private final String namePrefix;
        private final String threadFactoryName;

        public String getThreadFactoryName() {
            return threadFactoryName;
        }

        MyThreadFactory(String threadStartName) {
            SecurityManager s = System.getSecurityManager();
            group = (s != null) ? s.getThreadGr0up() :
                    Thread.currentThread().getThreadGr0up();
            namePrefix = threadStartName + "-" +
                    poolNumber.getAndIncrement() +
                    "-";
            threadFactoryName = threadStartName;
        }

        public Thread newThread(Runnable r) {
            Thread t = new Thread(group, r,
                    namePrefix + threadNumber.getAndIncrement(),
                    0);
            if (t.isDaemon())
                t.setDaemon(false);
            if (t.getPriority() != Thread.NORM_PRIORITY)
                t.setPriority(Thread.NORM_PRIORITY);
            return t;
        }
    }

    public static class MyRejectedPolicy implements RejectedExecutionHandler {

        @Override
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (e.getThreadFactory() instanceof MyThreadFactory) {
                MyThreadFactory myThreadFactory = (MyThreadFactory) e.getThreadFactory();
                if ("product".equals(myThreadFactory.getThreadFactoryName())) {
                    System.out.println(THREAD_FACTORY_NAME_PRODUCT + "线程池有任务被拒绝了,请关注");
                }
            }
        }
    }
}

你跑的时候可以把堆内存设置的小一点,比如我设置为 10m:



-Xmx10m -Xms10m



然后用 jconsole 监控,你会发现内存走势图是这样的:



哦,我的老天爷啊,这个该死的图,也是我的老伙计了,一个缓慢的持续上升的内存趋势图, 最后疯狂的触发 gc,但是并没有内存被回收,最后程序直接崩掉:



这绝大概率就是内存泄漏了啊。


但是在生产上的内存走势图完全看不出来这个趋势,我前面说了,主要因为 GC 情况的数据只会保留一周时间,所以就算把整个图放出来也不是那么直观。


其次不是因为我牛逼嘛,萌芽阶段就干掉了这个问题,所以没有遇到最后频繁触发 gc,但是没啥回收的,导致 OOM 的情况。


所以我再带着你看看另外一个视角,这是我真正定位到问题的视角。就是分析内存 Dump 文件。


分析内存 Dump 文件的工具以及相关的文章非常的多,我就不赘述了,你随便找个工具玩一玩就行。我这里主要是分享一个思路,所以就直接使用 idea 里面的 Profiler 插件了,方便。


我用上面的代码,启动起来之后在四个时间点分别 Dump 之后,观察内存文件。内存泄露的思路就是找文件里面哪个对象的个数和占用空间是在持续上升嘛,特别是中间还发生过 full gc,这个过程其实是一个比较枯燥且复杂的过程,在生产项目中可能会分析出很多个这样的对象,然后都要到代码里面去定位相关逻辑。


但是我这里极大的简化了程序,所以很容易就会发现这个 FutureTask 对象特别的抢眼,数量在持续增加,而且还是名列前茅的:



然后这个工具还可以看对象占用大小,大概是这个意思:



所以我还可以看看在这几个文件中 FutureTask 对象大小的变化,也是持续增加:



就它了,准没错。


好,问题已经能复现了,GC 图和内存 Dump 的图也都给你看了。


到这里,如果有人已经看出来问题的原因了,可以直接拉到文末点个赞,感谢大佬阅读我的文章。


如果你还没看出端倪来,那么我先给你说问题的根本原因:



问题的根本原因就出在 MyRejectedPolicy 这个自定义拒绝策略上。



在带你细嗦这个问题之前,我先问一个问题:



JDK 自带的线程池拒绝策略有哪些?



这玩意,老八股文了,存在的时间比我从业的时间都长,得张口就来:



  • AbortPolicy:丢弃任务并抛出 RejectedExecutionException 异常,这是默认的策略。

  • DiscardOldestPolicy:丢弃队列最前面的任务,执行后面的任务

  • CallerRunsPolicy:由调用线程处理该任务

  • DiscardPolicy:也是丢弃任务,但是不抛出异常,相当于静默处理。


然后你再回头看看我的自定义拒绝策略,是不是和 DiscardPolicy 非常像,也没有抛出异常。只是比它更高级一点,打印了一点日志。


当我们使用默认的策略的时候:



或者我们把框起来这行代码粘到我们的 MyRejectedPolicy 策略里面:



再次运行,不管是观察 gc 情况,还是 Dump 内存,你会发现程序正常了,没毛病了。


下面这个走势图就是在拒绝策略中是否抛出异常对应的内存走势对比图:



在拒绝策略中抛出异常就没毛病了,为啥?



探索


首先,我们来看一下没有抛出异常的时候,发生了什么事情。


没有抛出异常时,我们前面分析了,出现了非常多的 FutureTask 对象,所以我们就找程序里面这个对象是哪里出来的,定位到这个地方:



future 没有被回收,说明 futureList 对象没有被回收,而这两个对象对应的 GC Root 都是new 出来的这个线程,因为一个活跃线程是 GC Root。


进一步说明对应 new 出来的线程没有被回收。


所以我给你看一下前面两个案例对应的线程数对比图:



没有在拒绝策略中抛出异常的线程非常的多,看起来每一个都没有被回收,这个地方肯定就是有问题的。


然后随机选一个查看详情,可以看到线程在第 39 行卡着的:



也就是这样一行代码:



这个方法大家应该熟悉,因为也没有给等待时间嘛,所以如果等不到 Future 的结果,线程就会在这里死等。


也就导致线程不会运行结束,所以不会被回收。


对应着源码说就是有 Future 的 state 字段,即状态不正确,导致线程阻塞在这个 if 里面:



if 里面的 awaitDone 逻辑稍微有一点点复杂,这个地方其实还有一个 BUG,在 JDK 9 进行了修复,这一点我在之前的文章中写过,所以就不赘述了,你有兴趣可以去看看:《Doug Lea在J.U.C包里面写的BUG又被网友发现了。》


总之,在我们的案例下,最终会走到我框起来的代码:



也就是当前线程会在这里阻塞住,等到唤醒。


那么问题就来了,谁来唤醒它呢?


巧了,这个问题我之前也写过,在这篇文章中,有这样一句话:《关于多线程中抛异常的这个面试题我再说最后一次!》



如果子线程捕获了异常,该异常不会被封装到 Future 里面。是通过 FutureTask 的 run 方法里面的 setException 和 set 方法实现的。在这两个方法里面完成了 FutureTask 里面的 outcome 变量的设置,同时完成了从 NEW 到 NORMAL 或者 EXCEPTIONAL 状态的流转。



带你看一眼 FutureTask 的 run 方法:



也就是说 FutureTask 状态变化的逻辑是被封装到它的 run 方法里面的。


知道了它在哪里等待,在哪里唤醒,揭晓答案之前,还得带你去看一下它在哪里诞生。


它的出生地,就是线程池的 submit 方法:



java.util.concurrent.AbstractExecutorService#submit




但是,朋友,注意,我要说但是了。


首先,我们看一下当线程池的 execute 方法,当线程池满了之后,再次提交任务会触发 reject 方法,而当前的任务并不会被放到队列里面去:



也就是说当 submit 方法不抛出异常就会把正常返回的这个状态为 NEW 的 future 放到 futureList 里面去,即下面编号为 ① 的地方。然后被标号为 ② 的循环方法处理:



那么问题就来了:被拒绝了的任务,还会被线程池触发 run 方法吗?


肯定是不会的,都被拒绝了,还触发个毛线啊。


不会被触发 run 方法,那么这个 future 的状态就不会从 NEW 变化到 EXCEPTION 或者 NORMAL。


所以调用 Future.get() 方法就一定一直阻塞。又因为是定时任务触发的逻辑,所以导致 Future 对象越来越多,形成一种内存泄露。


submit 方法如果抛出异常则会被标号为 ② 的地方捕获到异常。


不会执行标号为 ① 的地方,也就不会导致内存泄露:



道理就是这么一个道理。


解决方案


知道问题的根本原因了,解决方案也很简单。


定位到这个问题之后,我发现项目中的线程池参数配置的并不合理,每次定时任务触发之后,因为数据库里面的数据较多,所以都会触发拒绝策略。


所以首先是调整了线程池的参数,让它更加的合理。当时如果你要用这个案例,这个地方你也可以包装一下,动态线程池,高大上,对吧,以前讲过。


然后是调用 Future.get() 方法的时候,给一个超时时间,这样至少能帮我们兜个底。资源能及时释放,比死等好。


最后就是一个教训:自定义线程池拒绝策略的时候,一定一定记得要考虑到这个场景。


比如我前面抛出异常的自定义拒绝策略其实还是有问题的,我故意留下了一个坑:



抛出异常的前提是要满足最开始的 if 条件:



e.getThreadFactory() instanceof MyThreadFactory



如果别人误用了这个拒绝策略,导致这个 if 条件不成立的话,那么这个拒绝策略还是有问题。


所以,应该把抛出异常的逻辑移到 if 之外。


同时在排查问题的过程中,在项目里面看到了类似这样的写法:



不要这样写,好吗?


一个是因为 submit 是有返回值的,你要是不用返回值,直接用 execute 方法不香吗?


另外一个是因为你这样写,如果线程池里面的任务执行的时候出异常了,会把异常封装到 Future 里面去,而你又不关心 Future,相当于把异常给吞了,排查问题的时候你就哭去吧。


这些都是编码过程中的一些小坑和小注意点。


反转


这一小节的题目为什么要叫反转?


因为以上的内容,除了技术原理是真的,我铺垫的所有和背景相关的东西,全部都是假的。



整篇文章从第二句开始就是假的,我根本就没有遇到过这样的一个生产问题,也谈不上扼杀在摇篮里,更谈不上是我去解决的了。


但是我在开始的时候说了这样一句话,也是全文唯一一句加粗的话:



虽然现在不是你的,但是你看完之后就是你的了,你明白我意思吧?



所以这个背景其实我前几天看到了“严选技术”发布的这篇文章《严选库存稳定性治理系列:一个线程池拒绝策略引发的血案》


看完他们的这篇文章之后,我想起了我之前写过的这篇文章:《看起来是线程池的BUG,但是我认为是源码设计不合理。》


我写的这篇就是单纯从技术角度去解析的这个问题,而“严选技术”则是从真实场景出发,层层剥茧,抵达了问题的核心。


但是这两篇文章遇到的问题的核心原因其实是一模一样的。


我在我的文章中的最后就有这样一段话:



巧了,这不是和“严选技术”里面这句话遥相呼应起来了吗:



在我反复阅读了他们的文章,了解到了背景和原因之后,我润色了一下,写了这篇文章来“骗”你。


如果你有那么几个瞬间被我“骗”到了,那么我问你一个问题:假设你是面试官,你问我工作中有没有遇到过比较棘手的问题?


而我是一个只有三年工作经验的求职者。


我用这篇文章中我假想出来的生产问题处理过程,并辅以技术细节,你能看出来这是我“包装”的吗?


然后在描述完事件之后,再体现一下对于事件的复盘,可以说一下基于这个事情,后面自己对监控层面进行了丰富,比如接口超时率监控、GC 导致的 STW 时间监控啥的。然后也在公司内形成了“经验教训”文档,主动同步给了其他的同事,以防反复踩坑,巴拉巴拉巴拉...


反正吧,以后看到自己觉得好的案例,不要看完之后就完了,多想想怎么学一学,包装成自己的东西。


这波包装,属于手摸手教学了吧?


求个赞,不过分吧?


作者:why技术
来源:juejin.cn/post/7186512174779465765
收起阅读 »

学习能力必然是职场的核心能力

最近新工作的编程语言换为了Golang,同时也在面试招聘相关岗位的人才。通过简历面试(别人的经历),以及自己的亲身学习经历,真切的感受到学习能力将是未来的一大竞争力。 从面试方面来看,大多数人工作稳定之后便失去了学习能力,以为现在的工作可以长久的干下去。结果,...
继续阅读 »

最近新工作的编程语言换为了Golang,同时也在面试招聘相关岗位的人才。通过简历面试(别人的经历),以及自己的亲身学习经历,真切的感受到学习能力将是未来的一大竞争力。


从面试方面来看,大多数人工作稳定之后便失去了学习能力,以为现在的工作可以长久的干下去。结果,互联网的风停下来之后,市场的需求变了,从单一的编程语言、单一业务的能力变成更加综合的能力,需要的人逐渐变为T型人才甚至π型人才。此时,学习能力就变得更加重要。否则,面临的只能是市场的淘汰。


下面分享一下自己最近三周学习Golang的一些经验和方法,大家可以拿来借鉴的其他学习方面上:


第一、实践。任何的学习都离不开实践。不能够运用到实践中的学习大概率是无效学习,而实践也是学习最有效的手段。在刚开学学习Golang时,找了一份基础语法的文档,花一两个小时看了一遍,知道常见的语法结构怎么用的,便开始搭建项目,写业务功能。其实这样的效果最快,以具体的功能实践来驱动学习,同时把对这方面的手感和思路锻炼出来。


第二、系统学习。单纯动手实践的过程中会掺杂着业务逻辑的实现,学习效率和范围上会有一些局限,属于用到什么学什么,缺点是不够系统。这时还需要一两本书,通读全书,帮助系统的了解这门语言(或某个行业)是怎么运作的,整个生态是什么样的,底层逻辑是怎样的,以便查漏补缺。在系统学习这块,建议以书籍为主,书籍的优势就是方便、快捷、系统、准确。


第三、交流。之前找一个懂的大佬请教和交流不是那么容易。但随着AI的发展,交流形式不仅仅限于大佬了,也可以是GPT。GPT最强大的能力是无所不知,知无不言。当然,对于它提供的结果也需要辩证的去看,某些地方可能会有错误,但大方向基本上是没错的,再辅以佐证,基本上能够解决80%的问题。


如果有机会参与面试,无论是作为面试官或者被面试者,都是一个交流的过程。在相互沟通的过程中了解市场需要什么,市场流行什么。


最后,针对某些问题,还是得去跟大佬交流才行,交流的过程中会碰撞出很多火花来。比如,不断的迭代某个算法,学到更好的实现方式,了解到你不知道的知识点等。曾经,一个字符串截取的功能,与大佬交流了三次,升级了三版,也学到了不同的API的使用方法和特性。


第四,输出。检验是否学会的一个标准就是你能否清晰的给别人描述出来,让别人听得懂。这一条是否很耳熟?对,它就是费曼学法,世界公认的最快的学习法。如果没办法很好的表达,说明这块掌握的还不是很清楚。当然,这个过程中也属于交流,也会拿到别人的反馈,根据别人的反馈来认识到自己的掌握程度和薄弱点。


第五,利用别人的时间。个人的时间总是有限的,不可能什么事情都自己做,也不可能都亲手验证。而作为管理者,最大的技能之一就是靠别人、靠团队来实现目标。那么,一个技术方案是否可行,是否有问题,也可以交给别人来调研、实践、验证。这样,可以让学习的效率并行起来。


另外,我们可能都听说过“一万小时定律”,这个概念是极具迷惑性的,会让你觉得学习任何东西都需要花费大量的时间的。其实不然,一万小时定律指的是学习一个复杂的领域并且成为这个领域的专家。


而我们在生活和实践的过程中,往往不需要什么方面都成为专家,只需要知道、掌握或会用某一领域的知识即可。对于入门一个新领域,一般来说,可能只需要20小时、100小时不等,没有想象中那么难。对于一个懂编程语言的人来说,从零学习另外一门语言,一般也就一两周时间就可以上手了。因此,我们不要对此产生畏惧心理。


上面讲的是学习方法,但最根本的是学习的意愿。你是选择花一年时间学习一门技术,然后重复十年,还是愿意每年都不断的学习迭代自己?两者的结果差距超乎你的想象。


作者:程序新视界
来源:juejin.cn/post/7257285697382449189
收起阅读 »

使用单例模式管理全局音频

引言 在现代Web应用中,音频播放是一项常见的功能需求。为了更好地管理全局音频,确保在页面切换、隐藏等情况下能够得到良好的用户体验,我们需要一种可靠的音频管理方案。本文将详细介绍一种基于单例模式的全局音频管理器,使用TypeScript语言和Howler库实现...
继续阅读 »

引言


在现代Web应用中,音频播放是一项常见的功能需求。为了更好地管理全局音频,确保在页面切换、隐藏等情况下能够得到良好的用户体验,我们需要一种可靠的音频管理方案。本文将详细介绍一种基于单例模式的全局音频管理器,使用TypeScript语言和Howler库实现。


背景


在开发Web应用时,往往需要在全局范围内管理音频播放。这可能涉及到多个组件或页面,需要一种机制来确保音频播放的一致性和稳定性。单例模式是一种设计模式,通过保证类只有一个实例,并提供一个全局访问点,来解决这类问题。


单例模式的优势


避免多次实例化


单例模式确保一个类只有一个实例存在,避免了不同部分对同一个资源进行多次实例化的情况。在音频管理器的场景下,如果允许多个实例存在,可能导致不同部分播放不同的音频,或者相互之间干扰。


全局访问点


通过单例模式,我们可以在整个应用中通过一个全局访问点获取音频管理器的实例。这使得在不同组件或模块中都能方便地调用音频管理器的方法,实现全局统一的音频控制。


统一状态管理


单例模式有助于统一状态管理。在音频管理器中,通过单例模式,我们可以确保整个应用中只有一个状态(例如是否正在播放、页面是否可见等)被正确地管理和维护。


技术实现


类结构与构造函数


首先,让我们看一下AudioManager的类结构。它包含一个私有静态实例,一个私有音频对象,以及一些控制音频播放状态的属性。构造函数是私有的,确保只能通过静态方法getInstance来获取实例。


class AudioManager {
private static instance: AudioManager;
private sound: Howl | undefined;
private isPlaying: boolean;
private isPageVisible: boolean;

private constructor() {
// 构造函数逻辑
}

// 其他方法和事件处理逻辑
}


构造函数中,我们初始化了一些基本属性,如isPlaying(是否正在播放)和isPageVisible(页面是否可见)。同时,通过visibilitychange事件监听页面可见性的变化,调用handleVisibilityChange方法处理相应逻辑。


单例模式实现


接下来,我们看一下如何通过单例模式确保只有一个AudioManager实例存在。


public static getInstance(): AudioManager {
if (!AudioManager.instance) {
AudioManager.instance = new AudioManager();
}
return AudioManager.instance;
}


通过getInstance方法,我们能够获取到AudioManager的唯一实例。在这个方法内部,我们检查instance是否已经存在,如果不存在,则创建一个新的实例。这确保了在应用中任何地方获取到的都是同一个实例。


页面可见性处理


在构造函数中,我们通过visibilitychange事件监听页面可见性的变化,并在handleVisibilityChange方法中处理相应逻辑。


private handleVisibilityChange(): void {
this.isPageVisible = !document.hidden;

if (this.isPageVisible) {
this.resume();
} else {
this.pause();
}
}


这部分逻辑确保了当页面不可见时暂停音频播放,页面重新可见时恢复播放状态,从而提升用户体验。


音频播放控制


play、stop、pause、resume等方法用于控制音频的播放状态。


public play(url: string): void {
// 音频播放逻辑
}

public stop(): void {
// 音频停止逻辑
}

public pause(): void {
// 音频暂停逻辑
}

public resume(): void {
// 音频恢复播放逻辑
}


在play方法中,我们通过Howler库创建一个新的音频对象,设置其来源和播放结束的回调函数。其他方法则用于停止、暂停和恢复音频的播放。


使用示例


全部代码:


import { Howl } from 'howler';

class AudioManager {
private static instance: AudioManager;
private sound: Howl | undefined;
private isPlaying: boolean;
private isPageVisible: boolean;

private constructor() {
this.isPlaying = false;
this.isPageVisible = !document.hidden;

document.addEventListener('visibilitychange', () => {
this.handleVisibilityChange();
});
}

public static getInstance(): AudioManager {
if (!AudioManager.instance) {
AudioManager.instance = new AudioManager();
}
return AudioManager.instance;
}

private handleVisibilityChange(): void {
this.isPageVisible = !document.hidden;

if (this.isPageVisible) {
this.resume();
} else {
this.pause();
}
}

public play(url: string): void {
if (this.isPlaying) {
this.stop();
}

this.sound = new Howl({
src: [url],
onend: () => {
// 音频播放结束时的回调
this.isPlaying = false;
// 在这里可以添加其他处理逻辑,例如停止或切换到下一个音频
}
});

this.sound.play();
this.isPlaying = true;
}

public stop(): void {
if (this.sound) {
this.sound.stop();
this.isPlaying = false;
}
}

public pause(): void {
if (this.sound && this.sound.playing()) {
this.sound.pause();
}
}

public resume(): void {
if (this.sound && this.isPlaying && this.isPageVisible) {
this.sound.play();
}
}

public getSound(): Howl | undefined {
return this.sound;
}
}

export default AudioManager.getInstance();


最后,让我们看一下如何在应用中使用这个全局音频管理器。


import AudioManager from './AudioManager';

// 播放音频
AudioManager.play('https://example.com/audio.mp3');

// 暂停音频
AudioManager.pause();

// 恢复音频
AudioManager.resume();

// 停止音频
AudioManager.stop();


通过引入AudioManager并调用其方法,我们可以方便地在应用中管理全局音频,而无需关心实例化和状态管理的细节。


应用场景


多页面应用


在多页面应用中,全局音频管理器的单例模式特性尤为重要。不同页面可能需要协同工作,确保用户在浏览不同页面时音频状态的一致性。


// 在页面1中播放音频
AudioManager.play('https://example.com/audio1.mp3');

// 切换到页面2,音频状态保持一致
AudioManager.resume();


组件化开发


在组件化开发中,不同组件可能需要协同工作以实现统一的音频控制。单例模式确保了所有组件共享同一个音频管理器实例,避免了冲突和不一致的问题。


// 在组件A中播放音频
AudioManager.play('https://example.com/audioA.mp3');

// 在组件B中暂停音频,整体状态保持一致
AudioManager.pause();


页面可见性


通过监听页面可见性的变化,我们确保在用户切换到其他标签页或最小化应用时,音频能够自动暂停,节省系统资源。


// 页面不可见时,自动暂停音频
// 页面重新可见时,自动恢复播放

结语


通过单例模式,我们实现了一个可靠的全局音频管理器,有效解决了在Web应用中音频播放可能遇到的问题。通过对代码逻辑的详细解释,我们希望读者能够更深入地理解这一设计模式的应用,从而在实际项目中更好地运用和扩展。同时,使用Howler库简化了音频操作的复杂性,使得开发者能够更专注于业务逻辑的实现。希望本文对您理解和使用单例模式管理全局音频有所帮助。


作者:一码平川哟
来源:juejin.cn/post/7303797715392479284
收起阅读 »

[compose] 仿刮刮乐效果

web
需求 下班路上新开了一家彩-票店,路过时总是心痒,本着小D怡情的心态,偶尔去刮几张,可是随着时间久了,发现也花了不少钱,看网上有人开发电子木鱼,突然奇想,为什么不做一张电子彩-票。 分析 传统View,网上有很多解决方案,大多数是通过混合模式进行两个图层的合并...
继续阅读 »

需求


下班路上新开了一家彩-票店,路过时总是心痒,本着小D怡情的心态,偶尔去刮几张,可是随着时间久了,发现也花了不少钱,看网上有人开发电子木鱼,突然奇想,为什么不做一张电子彩-票。


分析


传统View,网上有很多解决方案,大多数是通过混合模式进行两个图层的合并。


大致思路:

1、使用onDraw()方法中的Canvas绘制底层中奖层

2、在上面绘制一个蒙版层Bitmap, 在蒙版层Bitmap里面,放置一个新的Canvas

3、绘制一个灰色的矩阵,绘制一个path,将paint的Xfermode设置为 PorterDuff.Mode.DST_IN
4、手指移动时,更新Path路径


Compose实现

1、通过实现DrawModifier,重写draw() 方法

2、绘制原始内容层,drawContent()
3、绘制蒙版和手势层,


//配置画笔 blendMode = Xfermode
private val pathPaint = Paint().apply {
alpha = 0f
style = PaintingStyle.Stroke
strokeWidth = 70f
blendMode = BlendMode.SrcIn
strokeJoin = StrokeJoin.Round
strokeCap = StrokeCap.Round
}

drawIntoCanvas {
//设置画布大小尺寸
val rect = Rect(0f, 0f, size.width, size.height)
//从原始画布层,转换一个新的画布层
it.saveLayer(rect, layerPaint)
//设置新画布大小尺寸
it.drawRect(rect, layerPaint)
startPath.lineTo(moveOffset.x, moveOffset.y)
//绘制手指移动path
it.drawPath(startPath, pathPaint)
it.restore()
}

完整代码


fun ScrapeLayePage(){
var linePath by remember { mutableStateOf(Offset.Zero) }
val path by remember { mutableStateOf(Path()) }
Column(modifier = Modifier
.fillMaxWidth()
.pointerInput("dragging") {
awaitEachGesture {
while (true) {
val event = awaitPointerEvent()
when (event.type) {
//按住时,更新起始点
Press -> {
path.moveTo(
event.changes.first().position.x,
event.changes.first().position.y
)
}
//移动时,更新起始点 移动时,记录路径path
Move -> {
linePath = event.changes.first().position
}
}
}
}
}
.scrapeLayer(path, linePath)
) {
Image(
modifier = Modifier.fillMaxWidth(),
painter = painterResource(id = R.mipmap.cat),
contentDescription = ""
)
Text(text = "这是一只可爱的猫咪~~")
}
}

fun Modifier.scrapeLayer(startPath: Path, moveOffset: Offset) =
this.then(ScrapeLayer(startPath, moveOffset))

class ScrapeLayer(private val startPath: Path, private val moveOffset: Offset) : DrawModifier {

private val pathPaint = Paint().apply {
alpha = 0f
style = PaintingStyle.Stroke
strokeWidth = 70f
blendMode = BlendMode.SrcIn
strokeJoin = StrokeJoin.Round
strokeCap = StrokeCap.Round
}

private val layerPaint = Paint().apply {
color = Color.Gray
}

override fun ContentDrawScope.draw() {
drawContent()
drawIntoCanvas {
val rect = Rect(0f, 0f, size.width, size.height)
//从当前画布,裁切一个新的图层
it.saveLayer(rect, layerPaint)
it.drawRect(rect, layerPaint)
startPath.lineTo(moveOffset.x, moveOffset.y)
it.drawPath(startPath, pathPaint)
it.restore()
}
}
}

图片.png

参考资料



作者:Gx
来源:juejin.cn/post/7303075105390133259
收起阅读 »

技术管理经验

做了几年的技术管理,简单几点经验可以分享交流一下,管理比起程序还复杂很多,因为一个面向人,一个面向机器,不能以相同的方式来思考,重点是找到平衡,不能用一种方式解决所有问题;好的管理者日常要看的面相非常多,尤其是一、二线带团队,不仅要关注好每个人,同时还要能做好...
继续阅读 »

做了几年的技术管理,简单几点经验可以分享交流一下,管理比起程序还复杂很多,因为一个面向人,一个面向机器,不能以相同的方式来思考,重点是找到平衡,不能用一种方式解决所有问题;好的管理者日常要看的面相非常多,尤其是一、二线带团队,不仅要关注好每个人,同时还要能做好承上启下的作用,要做到面面俱到难度十分高,以下就根据几个维度来分享我自己的经验。


做一个自己都喜欢的团队


每个人带的团队都会有自己的风格,我对自己的要求就是创造自己都愿意待的团队,至于什么是我自己喜欢的团队,简单归类了几个要点,有目标、有空间、有坚持,有明确且有挑战的目标,有足够的空间和舞台来发挥所长,有耐心能坚持做正确的事,能有机会和一群伙伴来达成目标真的是再幸福不过的事,这过程肯定会遇到无数的问题,只要确认不断保持向前,坚持就是胜利✌️


找到共同的目标,坚持做对的事


很多时候团队会存在支持多个目标/业务,这时候就必须类似技术抽象一样的来找到团队内的最大公约数,不一定只有一个但必须要有,核心点还是找到共同的目标,有了共同目标、愿景后可以更好的凝聚方向。


如果一个管理者只是在做分配资源的事,那么还不如不要这个管理者,毕竟传声筒的工作完全可以交给系统解决,例如团队 OKR 只是把每个小方向聚合,那么团队大概率是没方向,等待着别人来推进。


换位思考,保持沟通,面对的是人不是机器


任何沟通最重要的就是换位思考,如果能做到这件事大概率沟通上可以减少很多问题;沟通的过程重点应该是通,关键点不是自己说了多少或做了多少,而是对方获得了多少,多理解对方背后考量的因素,去解决问题而不是解决出问题的人。


过去常听到对于技术人的评价是 “无法沟通”,这种情况大概率是双方没找到频率,每个人都有自己习惯的沟通方式,作为管理者应该要在团队内帮助大家达成一种沟通默契,不应该要求所有人都能使用同一种方式来交流,以及该有的底线划清楚;非常多优秀的人是很有个性的,懂得如何把这样的人摆好位置,会比起把这样的人驯服更有意义,只要在影响范围可控的情况下,应该多花点时间思考在整体排兵布阵。


团队成长 > 个人成长


带团队后最大的不同点就在于需要思考的是整个团队的产出,而不是自己的产出;尤其是原先自己是同团队最优秀的成员因此被提拨出来作管理,自己原先是其他人的 150% 以上的贡献度,但开始带团队后必须要能接受自己的产出会大幅下降,可能自己剩下 50%,团队其他人提升 20%,达到团队整体产出提升。


放权、给予试错空间很重要,想要让团队成员成长势必要让他逐步能承担起更重要的责任与任务;以战养兵是在工作中很好的方案,过程给予适度的辅助与容错,很多错没经历过是不会懂,读万卷书不如行万里路。


建团队,招聘 培养 汰换


建团队是每个管理者都会面对的问题,团队人才密度、人才水平都会决定采用不同的管理方式,其中招聘我认为是最重要的,这关卡的越严格后面就会越轻松,强烈建议这关必须把握好底线,水桶永远会从最低的那一边开始漏水。


招聘是一门大学问,坊间也有很多相关的书籍介绍,并且有专门的服务和职位,在这就以用人的部分来谈谈,一般我自己决定用人就两个原则,“我是否愿意汇报给他/她?”或 “我是否愿意把背后交给他/她?” 这两个问题概括大量的信息和判断,包含当前/未来是否需要这样的能力、团队匹配度、潜力和领导力等,团队整体布局上最好能保持更多元,并且别怕团队成员能力超越自己,就像曾子说的 “用師者王,用友者霸,用徒者亡。”


培养简单可以分为两块专业和软素质,专业部分培养一般相对容易,刚好是相同专业可以直接指导,如果是不同的领域可以找外部进来帮忙,软素质部分大多还是依靠团队日常培养,这块重点主要还是言传身教,身为主管日常的一举一动都会成为团队同学的标杆,因此必须对于自己的言行有一定的要求,尤其是公开场合。


汰换对管理者来说是个很重要的责任,尤其在面对这个环节一定要慎重的考量,要对于每个汰换最好都能有记录留底,帮助自己复盘;一般来说必须找到当前的瓶颈,究竟是卡在事还是人,如果是卡在人,也必须分析清楚原因,因为造成团队不稳定因素?潜力?阻碍团队发展?包含考虑这个瓶颈是不是自己。


1 to 10,10 to N,定义规则


团队 <10 的时候可与所有团队成员都非常的亲近,能明确的知道每个人在做些什么,甚至是团队成员家里的猫受伤了都能知道,在这个时期管理的负担并不是特别大,至少能有一半的时间能在执行日常工作,这时期可能会出现蜡烛两头烧的情况,必须要能尽快的找到平衡点,不需要扣每个细节,但要抓准方向。


团团在 >10 后在 <=15 可能整体变化不大,不过可以很明显的感受到管理成本提升,这个时期会慢慢的开始发现到靠着过去手把手了解细节方式精力会严重不足,需要靠着各种规则、规范来达到找寻最大公约数。


随着管理的范围越大会发现想要知道每个同学的情况不现实,靠着规则来减少管理精力,引导团队走向正确的方向,这其中会有很多的权衡,出现很多错误解读或过度解读信息的问题,通过不断的调整来达到平衡,然而这个平衡是来自于成熟度及规模。


Netflix 的团队有出了一本《零规则》,描述的是大多更像是以原则来取代规则减少理解成本,相信每个人都能按照规则走,再定期抽查的机制保障,从书中来看是可行的,为了适配不同地区文化不同也做了适度的调整,是一种感觉可行的模式,但还是要根据当前团队情况调整。


绩效管理


绩效只是管理的工具和手段,通过这个工具来帮助团队发展和个人提升,让每个同学知道自己阶段的优缺点以和校正方向,让好的更好,阶段性不好的同学能找到方向,达到个人和团队的成长。


绩效管理一定要记得 “功在平时”,绝对不能把绩效管理作为周期性的工作处理,日常付出的越多在绩效评分的时候问题就越少,同时也能帮助绩效沟通上更顺畅,把事情放到最后一刻很容易造成意外,一个不小心就成了事故。


不能忘本


大多技术管理者都是从 IC 开始,保持手感很重要,随着管理的幅度增加会大量降低自己的时间,最好还是能保持着写代码的手感,过往的经验能帮助增加判断力,但技术是不断在翻新,缺少了手感短期可能问题不大,要是习惯了会逐步脱节,严重点就会出现 “技术官僚” 问题,毕竟迭代这么快,哪怕知道原理也容易出现多个类似产品出现后该如何更好的判断问题。


多体验自己做出来的东西,感受技术带来的价值,相信自己每一行代码都在提升人的生活水平,而不是完成一个个任务;技术是个很不同的工种,除了使用工具还能够创造新的工具,为别人提升效率同时也能为自己提升效率。


小结


修身 齐家 治国 平天下,先能以身作则才能更好的要求其他人,让更多人能一起朝着相同方向前进,就像优秀的技术管理者大多也会是个优秀的 IC;保持同理心,多换位思考,帮助更多人激发潜力,各自发挥所长并找到其中的平衡点,这门学问就像是艺术一样的有趣。


到今天仍然觉得自己不是个合格的管理者,还有很多事要学,还有很多要改,没办法做到完美,只能尽可能在前往更好的路上加速前进着,欢迎各种交流讨论,互相学习🤝


作者:iskenhuang
来源:juejin.cn/post/7268301435066368035
收起阅读 »

程序员接外包的三个原则以及有意思的讨论

原则一:乙方来做决策 最终拍板人是谁?是甲方,如果你非要抢板子,那你以后就没有甲方了 但是,如果甲方也觉得“我花钱了,当然要听我的(那些只对上级负责又不能拍板的底层打工人,总是这样认为)”,那这种甲方的项目你就不要接 因为在这种甲方眼里,你只是“施工方”,他...
继续阅读 »

原则一:乙方来做决策



  • 最终拍板人是谁?是甲方,如果你非要抢板子,那你以后就没有甲方了

  • 但是,如果甲方也觉得“我花钱了,当然要听我的(那些只对上级负责又不能拍板的底层打工人,总是这样认为)”,那这种甲方的项目你就不要接

  • 因为在这种甲方眼里,你只是“施工方”,他们即不需要你的经验价值,更不会为你的经验买单。所以这种甲方你会做得很累,当他们觉得“你的工作强度不足以匹配付给你的费用时(他们总这样觉得)”,他们就会不停地向你提出新的开发需求

  • 所以,你要尽量找那种尊重你经验价值,总是向你请教,请你帮他做决策的甲方


原则二:为甲方护航



  • 甲方未来会遇到什么问题,你们双方其实都不知道,所以你需要一边开发一边解决甲方实际遇到的问题。

  • 因此,不要为了完成合同上的工作内容而工作,要为甲方遇到的实际问题,为了甲方的利益而开发,要提前做好变通的准备。

  • 永远不要觉得你把合同上的功能列表做完,你就能休息了。你要解决的真正问题是,为甲方护航,直至甲方可以自己航行。


原则三:不做没有用户的项目



  • 如果甲方的项目没有太多用户使用,这种项目就不要接。

  • 除了代码的累计经验,还有一种经验也很重要,那就是“了解用户的市场经验”

  • 只有真正面对有实际用户的项目,你才能有“解决市场提出的问题”的经验,而不是停留在“解决甲方提出的问题”

  • 拥有市场经验,你就会有更高的附加价值,再配上尊重你经验价值的甲方,你就会有更高的收入

  • 永远记住:真正愿意在你身上花钱的甲方,他的目的一定是为了让你帮他赚钱!


以上只是我根据自己经验的一家之言,可能对我有用,不一定对别人也有用。肯定还有很多有价值的原则,希望大家根据自己的经验一起来分享。


下面是一些有意思的讨论


原则 2 、3 都是虚的,就不讨论了。

只说原则一:


一般而言,甲方跟你对接的,一定不是老板。

所以他的核心目的一定是项目实施成功。

但项目是不是真的能给企业带来效益,其实是优先级特别低的一个选项。


拿日常生活举个例子。夫妻、情侣之间,你媳妇儿托你办个事,比如让你买个西瓜。

你明知道冬天的西瓜又贵又不好吃,你会怎么办?


A ,买西瓜,回去一边吃西瓜一起骂水果摊老板没良心。

B ,给你媳妇儿上农业课。然后媳妇儿让你跪搓衣板。

C ,水果摊老板训你一顿,以冬天吃白菜豆腐好为由,非卖你一颗大白菜。你家都不敢回。


这里面,你就是那个甲方对接人。你怎么选?


所以乙方一定不能做决策。乙方做决策的结果,就是甲方对接人被利益集团踹开或者得罪甲方对接人,最终导致项目失败




我也来说三个原则

1.要签合同,合同越细越好;

2.要给订金,订金越多越好;

3.尾款不结不给全部源码。




原则一:外包大部分就是苦力活,核心有价值的部分有自己公司的人干轮不到外包,你不干有的是人干,不会有人尊重你经验价值,甲方说怎么干就怎么干,写到合同里,按合同来,没甲方懂自家业务,别替甲方做决策,万一瞎建议导致项目出现大问题,黄了,外包钱都拿不回来


原则二:给多少钱办多少事,如果甲方给钱痛快,事少,可以看自己良心对甲方多上点心,否则别给自己加戏,不然很可能把自己感动了,甲方却想着好不容易碰上这么个人,白嫖


原则三:不做没有用户的项目,太片面,不是所有外包项目都是面对海量用户,但是做所有外包项目都是为了赚钱,假如有个富二代两三万找你做个毕设,简单钱多不用后续维护,这种接不接?假如某工厂几十万定制内部系统,可能只有几个人用,这种接不接


总之外包就是赚个辛苦钱,别指望这个来提升自己技术和自我价值,外包行业水太深,你这几个原则都太理想化




某富豪要盖一栋私人别墅,招建筑工人,现在缺一名搅拌水泥的工人,

找到了张三,张三说我去过很多工地,啥活儿都干过,经验极其丰富,我可以指导一切事物,我再给你兼职当个总设计师吧,一切事物听我的决策没错。

我每天做完我的本职工作搅拌水泥砂浆,我还能熬夜给建筑设计布局,风水,房间规划,材料采购等等,我啥都会,直接干到建筑完工

富豪很感兴趣,说那你来吧,我盖的是自己住的别墅,张三一听连连摆手:你是盖私人别墅啊?不行不行,我不去了,我以前盖的都是高楼大厦,住户多对我技术水平有严峻的考验,做成了对我有很大提高,私人别墅才几个人用,对我职业生涯一点帮助都没




永远不要接外包

这才是正确的答案

做私活的时间

不如自己休息休息,陪陪家人




屁事真多,有钱就行了,管他项目有没有人,人家产品低能你还得兜底,接外包考虑的是能不能满足需求。啥条件啊还能挑三拣四,给多少钱干多少活。 招投标接的 30 万以上的项目才有可能考虑你说的这些东西。





呵呵 我的意见是:



  1. 给钱就做(前提是合规,不是合理),先给定金,拿到定金开工。

  2. 遇到扯皮,就停止开发。

  3. 要有空闲的时间,偶尔做做(上面说的对:永远不要做外包)。


展开来说,做外包的长期收益很低。就当临时玩一下,所以给钱就做,不管你的需求合理不合理,比如甲方想给智障人士开发一款数独小游戏,好,给钱,签合同,支付定金,开工。


开工了3天,甲方突然说,那个我想加个魔方游戏。不好意思,不行,立即停止开发,开始和甲方掰扯,如果掰扯不明白,就终止合同,如果掰扯明白就继续。


不说了,我要和甲甲甲甲甲方掰扯去了。


作者:Data_Adventure
来源:juejin.cn/post/7256590619412676663
收起阅读 »

再高级的打工人也只是打工人!

再高级的打工人也只是打工人! OpenAI CEO 奥特曼被罢免的事情人尽皆知「虽然,今天又复职了。。」,我们能从中学到什么呢? CEO 也能被裁,这应该是最近几年被裁名单里面,职级最高的一个人了吧。你再也不用担心给别人说自己被裁有多丢人了,因为 CEO 都...
继续阅读 »

再高级的打工人也只是打工人!


OpenAI CEO 奥特曼被罢免的事情人尽皆知「虽然,今天又复职了。。」,我们能从中学到什么呢?



CEO 也能被裁,这应该是最近几年被裁名单里面,职级最高的一个人了吧。你再也不用担心给别人说自己被裁有多丢人了,因为 CEO 都会被裁。


另外,我想说,再高级的打工人也只是打工人!


作为打工人,被裁,尤其是这几年,是在是再正常不过的事情。尽管 CEO 的罢免和总裁的辞职对于外界来说可能是一个令人震惊的消息,但在公司内部,这些变动可能只是公司内部的调整和重组的一部分。


因为,再高级的打工人对于整个公司来说,都不是不可替代的。甚至,连公司的老板也不是不可替代的。国内很多大公司都开始这么培养接班人,离开了老板,公司照常运转的公司,才是一个体制健全的公司,这样的公司才能实现基业长青!当然,老板的不同在于,自己能决定自己什么时候离开。


不管是谁,当公司想裁你的时候,你就不再重要,就跟我上次被裁一样,上午还在写需求、改 bug,下午 6 点多通知被裁,第二天就不用来了!虽然这个对比优点不恰当,毕竟我对于公司来说只是一个普通打工人。


一声被裁,马上就得搬着东西走人。



当然,对于奥特曼(Sam Altman)这样的人来说,被裁不像我们大多数人一样需要考虑下一份工作、生活有没有保障、房贷能不能还上等一系列生存问题。


但是这件事也提醒了我们,无论我们的职位有多高级,我们仍然处于职场中不可忽视的变化之中。因此,我们应该时刻保持敏感和警觉,不断更新自己的知识和技能,以应对潜在的职业变动和挑战。


尤其是对于很多大厂中低层管理者而言,放弃了技术,管理能力也不见得有竞争力,毕竟国内大部分管理者都是野路子,没有经过系统的学习与培训。


如果你放弃了技术,之后因为各种原因被裁,那你可能还不如底层员工,他们还能更好的获取下一份工作,毕竟金字塔的底层岗位最多。


那我们能做什么呢?


做一家「一人企业」。什么是「一人企业」,就是把自己当做一个企业、一个产品来运营,来推销,把自己的能力最大化。


什么人适合做「一人企业」呢?借用饭大的话来说:主业稳定、有时间&有乐趣、不躺平&能利他。



所以,我不是在劝你无脑去干,而是满足了这三个条件,你再去干,你也应该是去干。


当自己的老板!


作者:伍六七AI编程
来源:juejin.cn/post/7302644330883727360
收起阅读 »

一个30岁专科java猿自白,对苍白的现实不妥协!!!

🙈 序言 时光弹指一挥,三十而立的年纪,生活在自己眼里,可以说一地鸡毛… 我通过这篇文章来分享我的经历,在软件行业栉风沐雨的六年… 🚂 茫 16年专科毕业,确切的说是在英雄联盟游戏里毕业,夙兴夜寐的三年,在游戏、吃喝上忙碌,一周7天,五个通宵。就这样,从学校出...
继续阅读 »


🙈 序言
时光弹指一挥,三十而立的年纪,生活在自己眼里,可以说一地鸡毛…
我通过这篇文章来分享我的经历,在软件行业栉风沐雨的六年…


🚂 茫
16年专科毕业,确切的说是在英雄联盟游戏里毕业,夙兴夜寐的三年,在游戏、吃喝上忙碌,一周7天,五个通宵。就这样,从学校出来,开始做销售,第一份工作,卖鞋油,给别人擦鞋,三月后,去了企业管理咨询公司,卖课,做了半年,兜里比脸还干净,又去做保险,更惨淡,就这样,17年,踏上北漂之路!


image.png


🪂 17 入行


17年三月,清楚记得自己报道的日子。七天的基础班,倒数第一名的成绩,差点把我劝退,硬着头皮坚持,扛下来了,我是最笨的人,但也是最努力的人,培训的那半年,每天早八点,晚12的敲代码。只因为,这是自己唯一的退路!半年后,我是班里第二个找到工作的人,入职某外包公司,现在想想自己那时候的选择,还是有一些遗憾。


image.png


☠ 18 踩坑最多的一年
拿到第一个月工资的时候,简直不敢相信,激动的和家里人分享。如果不做这个行业,我觉得月薪1万1很可能就是做梦,现在我也能理解到认知那句话了。这算是第一次找到了自信,在工作上有了结果。工作没想象的难,但也非常难受,学习能力方面确实和本科是有差距,纯野路子,没有什么专业基础,为此,18年,加了很多班,通宵达旦的修复很多自己写的Bug,后来,能力得到认可,当然,这是一点一滴的积累,血和泪只有自己知道。


🔥 19 裸辞
18年上半年买了房子,开始背负房贷,又贷款买了车位,这个时候每月贷款就需要还6K,去掉房租,生活费,身无分文,很多个夜晚挣扎着无法入睡,在压力驱使下,不得不找一个薪水更高一些的工作,找工作的过程对我的打击很大,找了将近一个月,才得到一份工作,比原来高2K,14薪,不过是内部员工,这一点是值得欣慰的。在找工作的过程中能明显感觉到社会对专科的歧视很大,因为有很多聊的很好的场面,但都是因为学历失之交臂!
这一年,我成了开发组长,管理四五个人开发,说组长感觉又不太合适,因为自己的开发任务也比较大,就算这样,也还好,因为房贷压力小了,能腾出手忙一点发展的事儿了,这个时候,没有时间谈个人的私事,这一年,工作中忙碌,兼顾生活,贷款,家庭,遇到了很多研发方面的困难,不过都一一克服了,还获得了客户的好评,这儿就不细说了,后续博客会写一部分。


🤡 20 裸辞
这一年,房子交房了,要装修,除了贷款的压力,还得装修房子,在工作上,已顺风顺水,有啥困难都能解决,个人能力团队也都很认可,但薪水成了一个生活发展的阻碍,为此向公司提了一下涨薪,被驳回了。然后我又提出裸辞,公司开始打算涨薪挽留,我拒绝了!然后我又去了外包公司,只为多拿点钱,让压力小一些,把家里的事情处理完。


下图来纪念以下公司的前台!!!


image.png


🦧 21 沉淀
裸辞也算非常顺利,半月的时间就接上了,薪资的话18K。工作很轻松,很多任务都能快速高效完成,在这个空闲的时间里,也没有闲着,会读读书,思考一下人生的意义!以下是我读完的书!


image.png


读书给我带来了一些动力和精神的慰藉,我也没有想到,一个大学不务正业的人能够静下来看书看到一千多个小时!我读的书没有太多技术方面的,在那个时候最大的作用应该是抵抗那些来自生活得压力和孤独吧。这一年的收获和前几年相比来说,应该是人更平和了,不在那么功利和浮躁,开始看淡一些物质相关的东西!


🧸 22 被解雇
作为一个外包公司,服务的项目做完了,又赶上疫情,被解雇也算是顺理成章的事情,这个时候也没有埋怨,也赶上春节,在家里呆了半个月,就开始投递简历,心里想的是在奋斗一年,找个离家近一点的工作。这个时候的就业环境已经不太乐观了,就学历门槛就很卡了,复习了几天,然后面了三四家吧,就又踏上北漂征程,这次也是外包公司,薪资22K。
三月入职,9月晋升,薪水涨到24.3K,工作上没怎么加过班,工作时间内也都能很出色的完成任务,这一年的总结就是遇到了很多优秀的人,学到了很多方法论,自己更能独立解决问题,在研发层面能够结合业务提出更多的解决方案,的确,这一年做方案的能力很强了。


🎨 23 前篇裸辞
23年五月,通过了PMP项目管理考试,顺利获得证书。作为一个山东人,深受孔孟思想的影响,不孝有三,无后为大!如今也快30岁了,别说结婚,对象的影子也没见!这个压力也慢慢加到了生活了。当我感受到痛苦和不想漂泊的时候,今年五月底,我递交了辞职申请!
离职需要在呆一个月,处理一下自己手头的尾部工作和交接事宜,7月1号,踏上了回家的征程。我很感谢这个公司,让我经历压力没那么大了,房子装修完了,就剩贷款了!
用此图来纪念一下!!!


image.png


🎨 23 中篇挣扎
回到家,作为一个驾-照拿了八年没摸过车的人,这个时候买了车,找了个教练,练了练车,差不多了,就开始闭关一月,学习,看视频,属于恶补吧。
8月开始投递简历,本是泰安人,想在家乡谋一份差事,为家乡发展做点贡献,自己钱包也多一点,这个双赢的机会在我面了两三家公司后打消了念头。
开始投递济南的公司,面了一家,项目经理职位,16k,就去工作了,也是一家外包公司。局方公司背景还是不错的!
工作了三个月,我又裸辞了,原因是太累,在提升自己层面上,得到的比较少。因为有贷款的压力,我辞职是做了很大心理建设的!


image.png


🔅 23 后篇 展望
辞职后,无缝衔接了一家公司,就是专门做研发,薪资12K,大小周。没有嫌弃,没有抱怨。目前自己的想法很简单,就是打造自己的产品,能够拿得出手一些东西,比如博客,比如项目,比如情感的一些价值!以前的自己好高骛远,现在的自己更脚踏实地,一步一个脚印,把自己的价值分享出来,成就别人,也成就自己!


⚜ 未来我会怎么做



  • 做好自己的本职工作-核心

  • 少花一点时间放在娱乐上

  • 多做一些有价值有产出的事儿 比如博客 比如读书分享 比如研究新项目

  • 跟随趋势走 拥抱变化

  • 更好的利用技术而不是沉迷技术

  • 更平和的心态处事

  • 简单的态度生活处世 己所不欲 勿施于人


作者:码农佩奇
来源:juejin.cn/post/7303720074842652709
收起阅读 »

微信小程序记住密码,让登录解放双手

web
密码是用户最重要的数据,也是系统最需要保护的数据,我们在登录的时候需要用账号密码请求登录接口,如果用户勾选记住密码,那么下一次登录时,我们需要将账号密码回填到输入框,用户可以直接登录系统。我们分别对这种流程进行说明: 记住密码 在请求登录接口成功后,我们需要判...
继续阅读 »

密码是用户最重要的数据,也是系统最需要保护的数据,我们在登录的时候需要用账号密码请求登录接口,如果用户勾选记住密码,那么下一次登录时,我们需要将账号密码回填到输入框,用户可以直接登录系统。我们分别对这种流程进行说明:


记住密码


在请求登录接口成功后,我们需要判断用户是否勾选记住密码,如果是,则将记住密码状态账号信息存入本地。
下次登录时,获取本地的记住密码状态,如果为true则获取本地存储的账号信息,将信息回填登录表单。

在这里插入图片描述

在这里插入图片描述


密码加密


我在这里例举两种加密方式MD5Base64
MD5:
1、不可逆
2、任意长度的明文字符串,加密后得到的固定长度的加密字符串
3、实质是一种散列表的计算方式


Base64:
1、可逆性
2、可以将图片等二进制文件转换为文本文件
3、可以把非ASCII字符的数据转换成ASCII字符,避免不可见字符
4、实质是 一种编码格式,如同UTF-8


我们这里使用Base64来为密码做加密处理。


npm install --save js-base64

引入Base64


// js中任意位置都可引入
let Base64 = require('js-base64').Base64;

可以通过encodedecode对字符串进行加密和解密


let Base64 = require('js-base64').Base64;

let pwd = Base64.encode('a123456');
console.log(pwd); // YTEyMzQ1Ng==

let pws2 = Base64.decode('YTEyMzQ1Ng==');
console.log(pwd2); // a123456

到这里我们对密码的简单加密和解密就完成了。
需要注意的是,Base64是可以解密的,所以单纯使用Base64进行加密是不安全的,所以我们要对Base64进行二次加密操作,生成一个随机字符串 + Base64的加密字符。


/***
*
@param {number} num 需要生成多少位随机字符
*
@return {string} 生成的随机字符
*/

const randomString = (num) => {
let str = "",
arr = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'];
let index = null;
for (let i = 0; i < num; i++) {
index = Math.round(Math.random() * (arr.length - 1));
str += arr[index];
}
return str;
}

调用randomString函数,根据你传入的数字来生成指定长度的随机字符串,然后将随机字符串与Base64生成的随机字符凭借,完成对密码的二次加密。


let pwd = randomWord(11) + Base64.encode(password); // J8ndUzNIPTtYTEyMzQ1Ng==

到这里就完成了密码加密操作。
在用户登录时,将账号密码存入本地,存入本地方式有很多,例如:CookieslocalStoragesessionStorage等,关于使用方法网上有很多,这里我们使用微信小程序的存储方式wx.setStorageSyn


// 我们这里使用微信小程序的存储方式wx.setStorageSync
let account = {
username: 'test‘,
password: pwd
}
wx.setStorageSync('
account', account)

在这里插入图片描述


二次登录


用户勾选记住密码后,第二次进入系统,直接从本地获取账号密码,对密码进行解密后回填到表单。
先判断用户是否勾选记住密码,然后对密码进行解密。


init() {
let state = wx.getStorageSync('rememberMe')
if (state) {
let account = wx.getStorageSync('account')
let Base64 = require('js-base64').Base64;
let pwd = Base64.decode(account.password.slice(11))
this.setData({
username: account.username,
password: pwd
})
}
this.setData({ rememberMe: state })
}

将解密后的数据回显到表单上,用户就可以直接登录了。


最后


关于记住密码业务,需要保证用户的密码是加密存储,这里用的是微信小程序示例,在web上的流程也是如此,你可以在vue项目中使用本文提到的方法。


作者:DCodes
来源:juejin.cn/post/7303739766106472488
收起阅读 »

登录是前端做全栈的必修课

web
如何在前端实现自动或无感化的登录态管理,包括用户注册、登录、接口校验登录态以及实现自动化请求时自动携带访问令牌。我们将探讨两种常见的实现方式:使用 HTTP Cookie 和前端存储和发送访问令牌。 1. 注册和登录 首先,用户需要通过注册和登录来获取访问令牌...
继续阅读 »

如何在前端实现自动或无感化的登录态管理,包括用户注册、登录、接口校验登录态以及实现自动化请求时自动携带访问令牌。我们将探讨两种常见的实现方式:使用 HTTP Cookie 和前端存储和发送访问令牌。


1. 注册和登录


首先,用户需要通过注册和登录来获取访问令牌。


1.1 注册接口


在注册接口中,用户提供必要的注册信息(如用户名和密码),服务器对用户进行验证并创建用户账户。


示例代码(Node.js + Express):


// 注册接口
app.post('/register', async (req, res) => {
try {
const { username, email, password } = req.body;

// 检查用户名和邮箱是否已被注册
if (users.some(user => user.username === username)) {
return res.status(400).json({ error: '用户名已被注册' });
}

if (users.some(user => user.email === email)) {
return res.status(400).json({ error: '邮箱已被注册' });
}

// 使用bcrypt对密码进行哈希处理
const hashedPassword = await bcrypt.hash(password, 10);

// 创建新用户对象
const user = {
id: Date.now().toString(),
username,
email,
password: hashedPassword
};

// 将用户信息存储到数据库
users.push(user);

// 创建访问令牌
const token = jwt.sign({ userId: user.id }, 'secretKey');

res.status(201).json({ message: '注册成功', token });
} catch (error) {
res.status(500).json({ error: '注册失败' });
}
});

1.2 登录接口


在登录接口中,用户提供登录凭据(如用户名和密码),服务器验证凭据的正确性并颁发访问令牌。


示例代码(Node.js + Express):


app.post('/login', (req, res) => {
// 获取登录凭据
const { username, password } = req.body;

// 在此处进行用户名和密码的验证,如检查用户名是否存在、密码是否匹配等

// 验证成功,颁发访问令牌
const token = createAccessToken(username);

// 将访问令牌写入 Cookie
res.cookie('token', token, {
httpOnly: true,
secure: true, // 仅在 HTTPS 连接时发送 Cookie
sameSite: 'Strict' // 限制跨站点访问,提高安全性
});

// 返回登录成功的响应
res.status(200).json({ message: '登录成功' });
});

2. 接口校验登录态


在需要校验登录态的受保护接口中,服务器将校验请求中的登录凭据(Cookie 或访问令牌)的有效性。


示例代码(Node.js + Express):


app.get('/protected', (req, res) => {
// 从请求的 Cookie 中提取访问令牌
const token = req.cookies.token;

// 或从请求头部中提取访问令牌,如果采用前端存储和发送访问令牌方式
// const token = req.headers.authorization.split(' ')[1]; // 示例代码,需根据实际情况进行解析

// 检查访问令牌的有效性
if (!token) {
return res.status(401).json({ error: '未提供访问令牌' });
}

try {
// 验证访问令牌
const decoded = verifyAccessToken(token);

// 在此处进行更详细的用户权限校验等操作

// 返回受保护资源
res.status(200).json({ message: '访问受保护资源成功' });
} catch (error) {
res.status(401).json({ error: '无效的访问令牌' });
}
});

3. 自动化登录态管理


要实现自动或无感化的登录态管理,前端需要在每个请求中自动携带访问令牌(Cookie 或请求头部)。


3.1 使用 HTTP Cookie


当使用 HTTP Cookie 时,浏览器会自动将 Cookie 包含在每个请求的头部中,无需手动设置。


示例代码(前端使用 JavaScript):


// 发送请求时,浏览器自动携带 Cookie
fetch('/protected');

3.2 前端存储和发送访问令牌


当使用前端存储和发送访问令牌时,前端需要在每个请求的头部中手动设置访问令牌。


示例代码(前端使用 JavaScript):


// 从存储中获取访问令牌
const token = localStorage.getItem('token');

// 设置请求头部
const headers = {
'Authorization': `Bearer ${token}`
};

// 发送请求时,手动设置请求头部
fetch('/protected', { headers });

在上述示例代码中,我们使用了前端的 localStorage 来存储访问令牌,并在发送请求时手动设置了请求头部的 Authorization 字段。


请注意,无论使用哪种方式,都需要在服务器端进行访问令牌的验证和安全性检查,以确保请求的合法性和保护用户数据的安全。


补充说明:



  • createUser:自定义函数,用于创建用户账户并将其保存到数据库或其他持久化存储中。

  • createAccessToken:自定义函数,用于创建访问令牌。

  • verifyAccessToken:自定义函数,用于验证访问令牌的有效性。


写在最后


文章旨在答疑扫盲,内容简明扼要方便学习了解,请确保在实际应用中采取适当的安全措施来保护用户的登录凭据和敏感数据,保持学习,共勉~


作者:vin_zheng
来源:juejin.cn/post/7303463043249635362
收起阅读 »

从小米14安装不上应用说起【适配64位】

一、原因 某天早上,同事突然对我说我换了小米14pro手机但是安装不了公司的打卡软件,怎么办呀。一时间,我也不知道原因,看到给我发的安装不上的截图陷入了沉思。随即打开在git仓库里找到这个项目,到本地编译打开,开始思考解决办法。 二、解决思路 从网上查询了一番...
继续阅读 »

一、原因


某天早上,同事突然对我说我换了小米14pro手机但是安装不了公司的打卡软件,怎么办呀。一时间,我也不知道原因,看到给我发的安装不上的截图陷入了沉思。随即打开在git仓库里找到这个项目,到本地编译打开,开始思考解决办法。


二、解决思路


从网上查询了一番,小米14pro 只支持安装64位的应用,可能是老项目没有做64位的适配。等到项目编译好,打开模块下的build.gradle文件,果然如此,没有做64位的适配。


ndk {
abiFilters 'armeabi', "x86", "x86_64"
}

针对64位做适配,一般都是适配so库,一般来说,如果你的项目中没有使用到so库或者C,C++代码,都是支持64位的。
这里再做下说明,ABI是Application Binary Interface(应用程序二进制接口)的缩写,在Android中,它指的是Android操作系统与设备硬件供应商提供的库之间的接口。ABI定义了程序各个组件在二进制级别上的交互方式,其中一个重要的方面是处理器使用的指令集架构。Android支持多种ABI以适应具有不同硬件架构的设备。



  • ARM(Advanced RISC Machine):

    • ARM是移动设备中常见的架构。

    • 变体:armv5、armv7-A、armv8-A(64位)等。



  • x86:

    • x86是台式机和笔记本电脑中常见的架构。

    • 变体:x86、x86_64(64位)。



  • MIPS(Microprocessor without Interlocked Pipeline Stages):

    • MIPS架构在过去在一些Android设备中被广泛使用,但近年来变得不那么常见。




好了,回归到正题,就要针对项目中这种情况处理so库了,因为这个老项目是从其他项目演变过来的,用不到这些so库,所以我的解决办法就是全部删除掉(当然要对项目中的源代码进行处理),再进行打包处理。
如果你们处理项目中的没有兼容的so库,推荐一个检测插件EasyPrivacy,接入这个就可以方便查看那些so库没有做适配了。
找到没有适配的so库之后,需要找到提供者获取最新兼容的so库或者找到相关官网看是否提供64位的so库。当然代码中还需要进行处理。


ndk {
abiFilters 'armeabi-v7a', 'arm64-v8a', "x86", "x86_64"
}

这样子打包的时候,在apk中的libs文件夹下就会出现四个对应的文件夹,里面就是对应的so库了。但是这样会增大包的体积。在android{}中配置如下代码,这样子打包之后就会出现四种包对应不同架构的包,这样子包的体积也会减小。


splits {
abi {
enable true
reset()
include 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64' //select ABIs to build APKs for
universalApk true //generate an additional APK that contains all the ABIs
}
}

回归正题,当我把打好的包发给同事的时候,同事告诉我还是不行。思来想去,排除64位的问题,那么剩下的只有Android版本的问题了。新出的手机肯定是搭载最新的Android版本,目前Android 14 有什么新变动还没有了解到。
image.png
从官网看到,映入眼前的第一条就是这个,检查项目中的代码,发现targetSdkVersion 还是16,怪不得安装不上,至此所有问题解决。


作者:风罗伊曼
来源:juejin.cn/post/7303741345323221044
收起阅读 »

陪伴了 14 年的 API 下线了

hi 大家好,我是 DHL。就职于美团、快手、小米。 Android 开发者应该深有感触,近几年 Android 每次的更新,对开发者的影响都非常的大,而这次 Android 14 的更新,直接让陪伴我们多年的老朋友 overridePendingTransi...
继续阅读 »

hi 大家好,我是 DHL。就职于美团、快手、小米。


Android 开发者应该深有感触,近几年 Android 每次的更新,对开发者的影响都非常的大,而这次 Android 14 的更新,直接让陪伴我们多年的老朋友 overridePendingTransition 下线。


这篇文章主要想介绍一下我们的老朋友 overridePendingTransition,它陪伴了我们度过了 14 年,如今也完成了它的使命,现已功成身退,这个方法在 Android 14 中被废弃了。



在 2009 年的时候,正式将 overridePendingTransition 添加到 Android Eclair(2.0) 源码中,Android 开发者对它应该有一种熟悉有陌生的感觉吧,我们刚开始学 Android 写 Activity 跳转动画的时候,都接触过这个。


Intent intent = new Intent(B.this, C.class);
startActivity(intent);
overridePendingTransition(R.anim.fade_in, R.anim.fade_out);

这段代码对每个 Android 同学都非常熟悉,而且至今在项目里面,到处都有它的身影。如果我们要为 Antivity 添加进入或者退出动画,那么只需要在 startActivity() 或者 finish() 方法之后立即调用 overridePendingTransition 即可。


14 年后的今天,Android 14 的横空出世 overridePendingTransition 也完成了它的使命,在 Android 14 的源码中正式被废弃了,感兴趣的小伙伴,可以打开 Android 14 的源码看一下。



当得知它都被废弃了,确实感到有些意外,源码中推荐我们使用新方法 overrideActivityTransition 代替 overridePendingTransition


我还以为是什么更好的方法,结果推荐的方法更加的难用,为了一个虚有其表的功能,废弃了这个 API,还给开发者增加了很大的负担。


按照 Android 官方的解释和源码中的说明,废弃掉这个方法是因为在 Android 14 中引入了新的返回动画,而 overrideActivityTransition 方法不能和它很好的做兼容,所以需要用新的方法去替换。


什么是新的返回动画


比如使用返回手势可以在应用后面显示主屏幕的动画预览。



小伙伴们一起来评评这个功能实用性怎么样,为了这个功能废弃掉我们的老朋友,如果是你,你会这么做吗?另外我们在看看新的 API 的设计。



新的 API 相比于旧 API 多了一个参数 overrideType,一起来看看源码中是如何描述这个参数 overrideType


For example, if we want to customize the opening transition when launching 
Activity B which gets started from Activity A, we should call this method inside
onCreate with overrideType = OVERRIDE_TRANSITION_OPEN because the Activity B
will on top of the task. And if we want to customize the closing transition when
finishing Activity B and back to Activity A, since B is still is above A, we
should call this method in Activity B with overrideType = OVERRIDE_TRANSITION_CLOSE.

If an Activity has called this method, and it also set another activity animation
by Window#setWindowAnimations(int), the system will choose the animation set from
this method.

翻译一下就是,每次想使用过渡动画,都必须告诉系统 overrideType 使用什么参数,比如当我们从 Activity A 打开 Activity B 时,需要使用参数 overrideType = OVERRIDE_TRANSITION ,当我们从 Activity B 返回到 Activity A 时,需要使用参数 overrideType = OVERRIDE_TRANSITION_CLOSE


这个参数不是应该由系统自动来处理吗,开发者只需要关心参数 enterAnimexitAnim 即可,这明显没有带来任何好处,还给开发者增加了很多负担。


这只是其中一个改变,Android 开发者应该都深有感触,每次 Android 的更新,都有一堆无用的改变,还给开发者增加了很多负担,每次的适配都是一堆体力活,这样就导致了 App 对 SDK 的版本产生了强烈的依赖。


不过好在有经验的开发者,经历过一次有一次的适配之后,积累了经验,在新的项目中,会对大部分 Android API 进行封装,如果 API 有大的变化,不需要对整个项目进行修改。




全文到这里就结束了,感谢你的阅读,坚持原创不易,欢迎在看、点赞、分享给身边的小伙伴,我会持续分享原创干货!!!




我开了一个云同步编译工具(SyncKit),主要用于本地写代码,同步到远程设备,在远程设备上进行编译,最后将编译的结果同步到本地,代码已经上传到 Github,欢迎前往仓库 hi-dhl/SyncKit 查看。



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

你知道为什么template中不用加.value吗?

web
Vue3 中定义的ref类型的变量,在setup中使用这些变量是需要带上.value才可以访问,但是在template中却可以直接使用。 询其原因,可能会说 Vue 自动进行ref解包了,那具体如何实现的呢? proxyRefs Vue3 中有有个方法prox...
继续阅读 »

Vue3 中定义的ref类型的变量,在setup中使用这些变量是需要带上.value才可以访问,但是在template中却可以直接使用。


询其原因,可能会说 Vue 自动进行ref解包了,那具体如何实现的呢?


proxyRefs


Vue3 中有有个方法proxyRefs,这属于底层 API 方法,在官方文档中并没有阐述,但是 Vue 里是可以导出这个方法。


例如:


<script setup>
import { onMounted, proxyRefs, ref } from "vue";

const user = {
name: "wendZzoo",
age: ref(18),
};
const _user = proxyRefs(user);

onMounted(() => {
console.log(_user.name);
console.log(_user.age);
console.log(user.age);
});
</script>

上面代码定义了一个普通对象user,其中age属性的值是ref类型。当访问age值的时候,需要通过user.age.value,而使用了proxyRefs,可以直接通过user.age来访问。



这也就是为何template中不用加.value的原因,Vue3 源码中使用proxyRefs方法将setup返回的对象进行处理。


实现proxyRefs


单测


it("proxyRefs", () => {
const user = {
name: "jack",
age: ref(10),
};
const proxyUser = proxyRefs(user);

expect(user.age.value).toBe(10);
expect(proxyUser.age).toBe(10);

proxyUser.age = 20;
expect(proxyUser.age).toBe(20);
expect(user.age.value).toBe(20);

proxyUser.age = ref(30);
expect(proxyUser.age).toBe(30);
expect(user.age.value).toBe(30);
});

定义一个age属性值为ref类型的普通对象userproxyRefs方法需要满足:



  1. proxyUser直接访问age是可以直接获取到 10 。

  2. 当修改proxyUserage值切这个值不是ref类型时,proxyUser和原数据user都会被修改。

  3. age值被修改为ref类型时,proxyUseruser也会都更新。


实现


既然是访问和修改对象内部的属性值,就可以使用Proxy来处理getset。先来实现get


export function proxyRefs(objectWithRefs) {
return new Proxy(objectWithRefs, {
get(target, key) {}
});
}

需要实现的是proxyUser.age能直接获取到数据,那原数据target[key]ref类型,只需要将ref.value转成value


使用unref即可实现,unref的实现参见本专栏上篇文章,文章地址:mp.weixin.qq.com/s/lLkjpK9TG…


get(target, key) {
return unref(Reflect.get(target, key));
}

实现set


export function proxyRefs(objectWithRefs) {
return new Proxy(objectWithRefs, {
get(target, key) {
return unref(Reflect.get(target, key));
},
set(target, key, value) {},
});
}

从单侧中可以看出,我们是测试了两种情况,一种是修改proxyUserageref类型, 一种是修改成不是ref类型的,但是结果都是同步更新proxyUseruser。那实现上也需要考虑这两种情况,需要判断原数据值是不是ref类型,新赋的值是不是ref类型。


使用isRef可以判断是否为ref类型,isRef的实现参见本专栏上篇文章,文章地址:mp.weixin.qq.com/s/lLkjpK9TG…


set(target, key, value) {
if (isRef(target[key]) && !isRef(value)) {
return (target[key].value = value);
} else {
return Reflect.set(target, key, value);
}
}

当原数据值是ref类型且新赋的值不是ref类型,也就是单测中第 1 个情况赋值为 10,将ref类型的原值赋值为valueref类型值需要.value访问;否则,也就是单测中第 2 个情况,赋值为ref(30),就不需要额外处理,直接赋值即可。


验证


执行单测yarn test ref



作者:wendZzoo
来源:juejin.cn/post/7303435124527333416
收起阅读 »

【Java集合】单列集合Set:HashSet与LinkedHashSet详解,为什么它比List接口更严格?

前言 上篇我们介绍了单列集合中常用的list接口,本篇我们来聊聊单列集合中的另外一个重要接口Set集合。1、Set 介绍java.util.Set接口和java.util.List接口一样,同样实现了Collection接口,它与Collection...
继续阅读 »

前言 上篇我们介绍了单列集合中常用的list接口,本篇我们来聊聊单列集合中的另外一个重要接口Set集合。

1、Set 介绍

java.util.Set接口和java.util.List接口一样,同样实现了Collection接口,它与Collection接口中的方法基本一致,并没有对Collection接口进行功能上的扩充,只是比Collection接口更加严格了。


与List接口不同的是,Set接口中元素无序,并且都会以某种规则保证存入的元素不出现重复,这里的某种规则,我们在后面中给大家揭秘,大家不要着急。
1 无序
2 不可重复

它没有索引,所以不能使用普通for 循环进行遍历。

Set 集合 遍历元素的方式  迭代器,增强for

来,我们通过案例练习来看看


//创建集合对象

HashSet hs = new HashSet();

//使用Collection的方法添加元素
hs.add("hello");
hs.add("world");
hs.add("java");
//无法添加,在执行这行代码时,会报错误
hs.add("world");
//遍历
for(String s : hs) {
System.out.println(s);
}

Set接口类型,定义变量,Collection的常用方法 add()没有报错,说明Set 完全实现了Collection中的方法;
在添加代码 hs.add("world");无法加入,验证了 Set的不可重复;
多次运行遍历发现,输入的顺序是改变的,说明Set是无序的。

Set集合有多个实现子类,这里我们介绍其中的java.util.HashSet、java.util.LinkedHashSet这两个集合。

2、HashSet 集合介绍

通过java文档,我们知道java.util.HashSet是Set接口的一个实现类

  • 它所存储的元素是不可重复的

  • 元素都是无序的(即存取顺序不一致)

  • 没有索引,没有带索引的方法,也不能使用普通for循环遍历

  • java.util.HashSet  是由哈希表(实际上是一个 HashMap 实例)支持,换句话说它的底层的实现数据结构是 哈希表结构,而哈希表结构的特点是查询速度非常快。

我们先来使用一下HashSet集合,体验一下,在进行讲解:

    public class Demo1Set {

public static void main(String[] args) {

//创建集合对象
HashSet hs = new HashSet();

//添加元素
hs.add("hello");
hs.add("world");
hs.add("java");

hs.add("world");

//使用增强for遍历
for(String s : hs) {
System.out.println(s);
}

}
}

输出结果如下
world
java
hello

***发现world 单词只存储了一个,集合中没有重复元素***

3、HashSet集合存储数据的结构

3.1 哈希表数据结构

我们在前面的文章中,已经介绍过基本的数据结构,大家可以回顾以下。

哈希表是什么呢?简单的理解,在Java中,哈希表有两个版本,在jdk1.8 之前,哈希表 = 数组+链表 ,而在 jdk1.8 之后,哈希表 = 数组+链表+红黑树(一种特殊的二叉树) 我们在这里对哈希表的讲述,为了便于我们学习,只需要记住它在计算机中的结构图即可。

你还在苦恼找不到真正免费的编程学习平台吗?可以试试云端源想!课程视频、在线书籍、在线编程、实验场景模拟、一对一咨询…无论你是初学者还是有经验的开发者,这里都有你需要的一切。最重要的是,所有资源完全免费!点击这里,立即开始你的学习之旅!


再一个,现在我们大多使用的jdk版本是1.8之后的,所以我们讲解的哈希表结构是 第二个版本。 废话不多说,来看看图吧:

  1. 我们在之前已经介绍过数组和链表的结构图,所以我们在这里就来简单介绍一下红黑树结构。

我们在生活中树的结构,树根-树枝-叶子。计算机世界的树,刚好与我们现实中的树成镜像相反,树根在上,树枝在下。那每个树枝上只有不超过两个叶子的就叫做  二叉树,而红黑树就是我们一颗特殊的二叉树,结构就是这样: image

  1. 说完红黑二叉树呢,我们来看看我们的哈希表结构图:

    1. 有数组

    2. 有链表

    3. 有红黑树

    image

    3.2 哈希值

    我们刚刚通过哈希表的介绍,知道 元素在结构中的存放位置,是根据hash值来决定的,而 链表的长度达到某些条件就可能触发把链表演化为一个红黑树结构。那么hash值是什么呢?说白了就是个数字,只不过是通过一定算法计算出来的。
    接下来我们介绍一下:

    • 哈希值:是JDK 根据对象地址或者字符串或者数字算出来的 int 类型的数值

    • 如何获取哈希值?

    在java基础中,我们学习过 Object类是所有类的基类,每个类都默认继承Object类。通过API 文档查找,有个方法 public int hashCode():返回对象的哈希码值

    我们看下这个方法的使用

    首先定义一个person类

    具有两个属性,设置getset方法,设置构造函数

       public class Demo2HashCode {

    public static void main(String[] args) {

    String str1 = "hello";
    String str2 = new String("hello");

    System.out.println("str1 hashcode =" + str1.hashCode());
    System.out.println("str2 hashcode =" + str2.hashCode());

    //通过上下两段代码的对比,我们可以知道 String 类重写了HashCode()方法。

    Student student = new Student("玛丽", 20);
    Student student2 = new Student("沈腾", 30);
    System.out.println("student hashcode =" + student.hashCode());
    System.out.println("student2 hashcode =" + student2.hashCode());

    }
    }

    好,我们了解了hash值概念和获取方式,那我们就来看看元素事怎么加入到 hashset中的

    3.3  添加元素过程

    那么向 HashSet集合中,添加一个元素时,到底执行了哪些流程呢? 首先我们在实例化HashSet 对象,同过api文档,在调用空参构造器后,创建了一个长度为16的数组。 其次在调用add方法之后,它的执行流程如下:

    1. 根据对象的哈希值计算存储位置

          如果当前位置没有元素则直接存入
      如果当前位置有元素存在,则进入第二步
    2. 当前元素的元素和已经存在的元素比较哈希值如果哈希值不同,则将当前元素进行存储如果哈希值相同,则进入第三步

    3. 通过equals()方法比较两个元素的内容如果内容不相同,则将当前元素进行存储如果内容相同,则不存储当前元素流程图来表示:

    4. image

    了解了元素添加的原理后,在添加元素的过程中,如果说某一条链表达到一定数量,就会触发条件,去演化为一条红黑树,防止我们学起来太吃力,在这里不做深入探究。 总而言之,JDK1.8引入红黑树大程度优化了HashMap的性能,那么对于我们来讲保证HashSet集合元素的唯一,其实就是根据对象的hashCode和equals方法来决定的。如果我们往集合中存放自定义的对象,那么保证其唯一,就必须复写hashCode和equals方法建立属于当前对象的比较方式。

    4、HashSet 存储自定义类型元素

        //优化后的student
    public class Student {
    private String name;
    private int age;

    public Student(String name, int age) {
    this.name = name;
    this.age = age;
    }

    public String getName() {
    return name;
    }

    public void setName(String name) {
    this.name = name;
    }

    public int getAge() {
    return age;
    }

    public void setAge(int age) {
    this.age = age;
    }

    @Override
    public String toString() {
    return "Student{" +
    "name='" + name + '\'' +
    ", age=" + age +
    '}';
    }

    @Override
    public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof Student)) return false;

    Student student = (Student) o;

    if (age != student.age) return false;
    return name.equals(student.name);
    }

    @Override
    public int hashCode() {
    int result = name.hashCode();
    result = 31 * result + age;
    return result;
    }
    }


    public class Demo3HashSet {

    public static void main(String[] args) {

    HashSet students = new HashSet<>();

    Student student = new Student("玛丽", 20);
    Student student2 = new Student("沈腾", 30);
    Student student3 = new Student("沈腾", 30);

    students.add(student);
    students.add(student2);
    students.add(student3);

    for (Student studentObj : students) {
    System.out.println("Hashset 元素=" + studentObj);
    }
    }
    }

    执行结果
    Hashset 元素=Student{name='玛丽', age=20}
    Hashset 元素=Student{name='沈腾', age=30}

    5、LinkedHashSet

    我们知道HashSet 保证元素的唯一,可以元素存放进去是没有顺序的,那么我们有没有办法保证有序呢? 打开API文档,我们查看 HashSet下面有一个子类 java.util.LinkedHashSet,这个名字听起来和我们之前学过的LinedList  有点像呢。通过文档,LinkedHashSet 具有可预知迭代顺序的 Set 接口的哈希表和链接列表实现。此实现与 HashSet 的不同之外在于,后者维护着一个运行于所有条目的双重链接列表。 简单的理解为:在进行集合添加元素的同时,不仅经过程序的执行形成一个哈希表结构,还维护了一个记录插入前后顺序的双向链表。 我们通过案例来感受一下:

        public class Demo4LinkedHashSet {

    public static void main(String[] args) {

    // Set demo = new HashSet<>();

    Set demo = new LinkedHashSet<>();
    demo.add("hello");
    demo.add("world");
    demo.add("ni");
    demo.add("hao");
    demo.add("hello");

    for (String content : demo) {
    System.out.println("---" + content);
    }
    }
    }

    执行结果:
    第一次:
    ---world
    ---hao
    ---hello
    ---ni

    第二次:
    ---hello
    ---world
    ---ni
    ---hao

    小节

    到这里我们已经讲完了单列集合的两种基本接口,分别是List 和 Set,然后通过一些简单的案例,我们也初步了解了它们的使用,其实大家可能还是有些懵逼的;但是在这里,还是要多练习,多看看别人的代码,看看别的优秀的代码,是在怎样的场景下使用的,是怎么互相转换使用的;希望在大家阅读这个系列文章的时候,一方面使一些刚学习的新人,有所帮助;一方面为一些已经工作了一段时间的朋友,温故而知新;更重要的一点,让大家知道,技术的出现,是为了解决问题而创造出来的,他本来就是为了解决生活的难题,只不过使用了一些好的,快捷的方式而已。
    可能文章中有些地方讲的不恰当,大家可以私信,探讨探讨,互相提高。本篇就到这里,happy ending!

      收起阅读 »

      软件设计中你考虑过重试了吗?

      你好,我是刘牌! 人生做事情失败了,拍拍裤子,站起来再试试,那么为啥软件中请求失败了为何就放弃了,而不是不再试试呢! 前言 今天分享一下重试操作,我们知道网络是不可靠的,那么在进行网络请求时,难免会出现请求失败,连接失败等情况,为了保证软件的稳定性和良好的...
      继续阅读 »

      你好,我是刘牌!



      人生做事情失败了,拍拍裤子,站起来再试试,那么为啥软件中请求失败了为何就放弃了,而不是不再试试呢!



      前言


      今天分享一下重试操作,我们知道网络是不可靠的,那么在进行网络请求时,难免会出现请求失败,连接失败等情况,为了保证软件的稳定性和良好的体验,很多时候我们不应该将程序内部出现的问题都抛出给用户,而是应该尽最大可能将软件内部不可抗拒的问题在程序内部处理掉,那么很多时候我们会采取重试操作。


      背景和问题


      程序产生网络故障或者其他一些故障是无法避免的,可能因为一些原因导致某些服务在短时间或者一段时间断连,可能是服务器负载过高而导致的,也可能是数据库导致故障从而影响服务,也可能是GC过于频繁而导致服务很不稳定等等,总之,导致服务不可用的因素很多很多。


      对于程序的出错,如果不属于业务上的异常,不应该抛给用户,比如抛出“无法连接远程服务”,“服务器负载过高”,“数据库异常”这类异常给用户实际上没有任何意义,反而会影响用户用户体验,因为用户并不关心这些,他们也读不懂这些领域词汇,所以应该去避免这些问题。


      解决方案


      程序发生异常是无法避免的,我们只有采取一些补救措施,在最大程度上提高程序的稳定性和用户体验,对于程序产生的问题,有一些可能只是瞬时的,系统能够很快恢复,有一些需要一定的时间,而有一些需要介入人工,所以需要花费的时间更多,那么就需要根据不同的情况来处理,下面对其进行分类。


      取消


      当系统中的异常是暂时无法处理的,这时候就应该直接取消任务,因为如果不取消,而是让用户一直等待,那么就会导致用户的操作无法进行下一步,而是一直等待,用户体验就会变得很差,这时候应该给用户友好的提示,提醒他们稍后再进行办理,浪费别人的时间等于谋财害命。


      重试


      如果请求因为网络原因或者服务短暂的不可用,这种故障时间很短,很快就能恢复,比如一些服务是多实例部署,刚好请求到的那个服务出现网络故障而没能请求成功,如果我们直接返回异常,那么肯定不合适,因为其他服务实例还能提供服务,所以应该对请求进行重试,重试后可能请求到了其他正常的服务,即使请求到了之前的服务,那么可能它已经恢复好了,能够正常提供服务了,这里重试是没有时间间隔的,是不间断地请求,直到请求成功,这种适用于服务很够很快恢复的场景。


      间隔重试


      间隔重试就是不会一下进行重试,而是隔一个时间段再进行重试,比如一些服务因为过于繁忙导致负载过高而暂时对外提供服务,那么这时候如果不断发起重试,只会导致服务负载更高,我们应该隔个时间段再进行重试,让服务处理堆积的任务,等服务负载降下来再重试,这个时间间隔需要我们进行考量,按照合适的值去设置,比如1s,这完全根据实际场景去衡量。


      上面对三种方案进行描述,我们只描述了重试,但是重试次数也是我们要去考量的一个值,如果一个服务20s才恢复,那么我们重试20秒肯定不太合适,不过也要看具体业务,面向客户的话肯定大多客户接受不了,这时候我们应该设置重试次数,比如重试了三次还不能成功,那么久取消任务,而不是一直重试下去。



      重试次数也要根据实际情况来设置,如果一直重试,而服务一直无法恢复,那么也会消耗资源,并且用户导致用户请求一直在等待,用户体验不好,设置设置次数过少,那么可能会导致没有足够重试,从而导致浪费了一些重试次数,最后还没有成功,如下,第三次就重试成功,如果设置为两次,那么前两次没有成功就返回,用户还需重新再发起请求。



      从上面可以看出,这些设置都没有黄金值,而是需要我们根据业务和不断地测试才能找出合适的值。


      怎么重试,参数怎么管理


      上面对重试进行一些理论的讲解,那么在实际场景中我们应该如果去做呢,首先要考虑我们的业务中是否真的有必要重试,如果没必要,那么就完全没必要去增加复杂度,如果需要,那么就需要进行良好的设计,保证其优雅和扩展性。


      不同的业务有不同的重试逻辑,所以我们需要在不同的地方实现不同的逻辑,但是重试次数和重试时间间隔这些参数应该是需要可动态配置的,比如今天服务负载过高,那么时间间隔可以设置稍微长一点,次数可以设置多一点,然后负载较低的时候,参数可以设置小一点,这些配置信息可以写入配置中心中。


      也有一些重试框架供我们使用,比如spring-retry,我们可以借助一些框架来管理我们的重试任务,更方便管理。


      总结


      以上对重试的一些介绍就完了,我们介绍了重试的场景,重试产生的背景,还有一些解决方案,还对重试的一些管理进行介绍,重试的方案很多,实现方式也有很多,它不是固定的技术,而是一种思想,也是我们在软件设计中应该考虑的一个点,它能提高软件的稳定性和用户体验,但是也需要我们进行考量。



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



      作者:刘牌
      来源:juejin.cn/post/7238230111941689400
      收起阅读 »

      简历中的项目经历可以怎么写?

      概述 工作这10多年来,也经常做招聘的工作,面试过的人超过50人次了,而看过的候选人的简历则有几百份了,但是清晰且能突出重点的简历,确实很少遇到。这里基本可以说明一个问题,很多候选人是不太清楚如何写出一份好的简历的。 下面基于简历中的项目经历,重点铺开说一下。...
      继续阅读 »

      概述


      工作这10多年来,也经常做招聘的工作,面试过的人超过50人次了,而看过的候选人的简历则有几百份了,但是清晰且能突出重点的简历,确实很少遇到。这里基本可以说明一个问题,很多候选人是不太清楚如何写出一份好的简历的。


      下面基于简历中的项目经历,重点铺开说一下。在社招中,项目经历面试官重点考察的地方。


      写项目经历需要注意的地方


      项目经历是介绍你实战经历的地方,同时也能反映你对已掌握的技能的使用情况。对于应聘偏技术类的岗位来说,这块非常的重要。


      下面会以支付中心作为例子进行阐述。




      • 项目背景,也即是你一定要非常清楚启动这个项目的缘由是啥。如果这个都说不清楚的话,那说明,你真的就是埋头干活,偏执行的角色。对项目并没有一个整体的认识。就算你只是这个项目的普通参与者,也需要主动的去了解和理解该项目立项的原因。有个注意的地方是,项目背景的文字描述不要太长,一两句就可以了。比如说:当前支付中心耦合在订单系统中,为了提升支付模块的稳定性、维护性、性能和扩展性,需要将支付模块独立出来,统一为其他内部系统提供支付能力;




      • 项目功能介绍,介绍一下这个项目能做什么,有什么核心模块,需要应付什么量级的流量。以支付中心为例子:为内部的订单系统提供支付能力,对内提供了微信、支付宝、抖音、海外、信用卡、钱包、礼品卡以及组合支付的支付、回调、退款、查询、业务对账等能力。平时需要应付每秒1万的支付请求。




      • 技术架构设计,这里考察的是技术选型的严谨性和模块设计的合理性。如果项目用到了RabbitMQ、Redis、Kafka等一些技术,你自己心里一定有个底,就是当时为什么选用这些技术,是经过深思熟虑的吗?是经过了很多轮的技术栈对比后决定使用的吗。也即是技术选型是一个严谨的论证的一个过程。而设计这块,则要说清楚模块划分的缘由以及解决方案。还是以支付中心为例子:通过支付网关,对外提供统一的接口,而内部则通过支付路由模块,进行具体的支付方式路由,并把单独的支付方式,以物理单元进行隔离,避免各种支付方式在出故障时,相互影响。为了应付高频的支付动作,采用数据库分库的方式缓解写的压力。




      • 我负责的模块,如果你参与的项目是部门核心项目,但是自己参与的模块确是边缘模块或者只是参与了很小的一部分,虽然你也能在这个项目里,得到成长。但是那是称不上个人亮点的。因为面试官会更倾向于:你为这个项目做了什么贡献,因为你,项目有了什么好的改变和突破性进展。因此,做项目的时候,不妨跟自己的领导多反馈一下,希望能独立主导一些重要的模块。如果领导觉得当前的你还无法独立hold住重要的模块,你也不要气馁,平时多多提升自己,争取后续能主导一些重要模块。这个真的很重要,为了将来的自己,你必须得这么做。在做项目的时候,如果你长期一直起着螺丝钉的作用的话,对你极其不利,甚至可以说,你是在浪费时间。




      • 难点和踩过的坑,难点也即是亮点。在你负责的模块里,具体的难点是什么,你是通过什么方案解决的。而解决的过程中,又遇到什么大坑?怎么优化的。这个其实是一种引导,把面试官引入到你自己比较熟悉又印象深刻的领域,如果你准备充分的话,是能给面试官一个好的印象的,是能加分的。同时能解决掉难点,对自身成长也是有利的,且还能说明的你韧性不错,有追求。




      • 取得的成效,不能只是重视过程,而不重视结果,这是不可取的。你需要用结果和数据体现你的价值。比如说,支付中心上线后,你负责的业务模块,慢调用和慢SQL消失了,接口响应速度提升了10倍,上线半年,无任何大故障。等等。




      项目经历写几个合适?


      如果按照上面的的方式来书写项目的话,那每个项目的文字描述是不短的,一个项目的描述就大概要占用半页了。因此,简历里的项目不能太多,2到3个就可以了。项目主要在精不在多,把自己负责比较多的且能作为自己的一个亮点的核心项目,说清楚道明白,更为重要。


      现在的你应该做什么?


      赶紧好好总结一些当前和之前做过的项目,按照上面列的方式,好好梳理和思考一下,提炼一些重要的内容出来。争取能作为自己履历的亮点。如果你发现到目前为止,还没有能为自己带来竞争力的项目,那赶紧好好反思一下,赶紧争取去做。


      小结


      如果你不是什么名人或者知名大佬,学历和履历也一般般,那么你只能通过曾经做过好的项目来增强自己的竞争力了。HR也会通过你的项目经历来了解你的能力。项目经历一定要真实,要突出亮点和难点,并说清楚自己在项目起到什么作用。


      作者:SamDeepThinking
      来源:juejin.cn/post/7200953096893136955
      收起阅读 »

      一体多面:哪有什么DO、BO、DTO,只不过是司空见惯的日常

      1 分层疑问 无论DDD还是MVC模式构建项目,势必涉及到工程结构的分层,每一层由于定位不同,所以访问的对象也不同,那么对象在每一层传递时就会涉及到对象的转换,这时有人会产生以下疑问: 对象种类多,增加理解成本 对象之间转换,增加代码成本 编写代码时有时不同...
      继续阅读 »

      1 分层疑问


      无论DDD还是MVC模式构建项目,势必涉及到工程结构的分层,每一层由于定位不同,所以访问的对象也不同,那么对象在每一层传递时就会涉及到对象的转换,这时有人会产生以下疑问:



      • 对象种类多,增加理解成本

      • 对象之间转换,增加代码成本

      • 编写代码时有时不同层对象几乎一样


      即使有这样的疑问,我也认为分层是必要的,所以本文我们尝试回答上述疑问。




      2 通用分层模型


      两种通用模型是MVC和DDD,我之前在文章《DDD理论建模与实现全流程》也详细讨论DDD建模和落地全流程,本文只涉及对象的讨论,所以会对模型有所简化。




      2.1 模型分类



      • 数据对象:DO(data object)

      • 业务对象:BO(business object)

      • 视图对象:VO(view object)

      • 数据传输对象:DTO(data transfer object)

      • 领域对象:DMO(domain object)

      • 聚合对象:AGG(aggregation)




      2.2 MVC


      MVC模型总体分为三层:



      • 持久层(persistence)

      • 业务层(business)

      • 表现层(presentation/client)


      每一层使用对象:



      • 持久层

        • 输入对象:DO

        • 输出对象:DO



      • 业务层

        • 输入对象:BO

        • 输出对象:BO



      • 表现层

        • 输入对象:VO/DTO

        • 输出对象:VO/DTO






      2.3 DDD


      DDD模型总体分为四层:



      • 基础设施层(infrastructure)

      • 领域层(domain)

      • 应用层(application)

      • 外部访问层(presentation/client)


      每一层使用对象:



      • 基础设施层

        • 输入对象:DO

        • 输出对象:DO



      • 领域层

        • 输入对象:DMO

        • 输出对象:DMO



      • 应用层

        • 输入对象:AGG

        • 输出对象:DTO



      • 外部访问层

        • 输入对象:VO/DTO

        • 输出对象:VO/DTO






      3 生活实例


      这些对象看起来比较复杂,理解成本很高,好像是为了分层硬造出来的概念。其实不然,这些对象在生活中司空见惯,只不过大家并没有觉察。我们设想有一家三口,小明、小明爸爸和小明妈妈,看看这些对象是怎么应用在生活中的。




      3.1 MVC


      3.1.1 数据对象(DO)


      数据对象作用是直接持久化至数据库,是最本质的一种对象,这就像小明在卧室中穿着背心睡觉,这接近于人最本质的一种状态,小明此时是一种数据对象。




      3.1.2 业务对象(BO)


      小明起床走出卧室,这时小明就不仅仅是他自己了,他还多了很多身份,例如儿子、学生、足球队队员,不同身份输入和输出信息是不一样的。作为儿子要回应家长的要求,作为学生要回应老师的要求,作为足球队员要回应教练的要求。作为小明从数据对象,在不同的身份场景中变成了不同的业务对象。




      3.1.3 视图对象/数据传输对象(VO/DTO)


      小明吃完早饭准备去上学,但是嘴角粘上了饭粒,出门前要把饭粒擦掉。数据传输对象需要注意简洁性和安全新,最重要的是只携带必要信息,而不应该携带不必须要信息,所以此时小明变成了视图对象。




      3.2 DDD


      3.2.1 领域对象(DMO)


      领域对象要做到领域完备,从本质上来说与业务对象相同,但是通常使用充血模型,业务对象通常使用贫血模型。




      3.2.2 聚合对象(AGG)


      学校要开家长会要求小明、小明妈妈和小明爸爸全部参加,其中小明负责大扫除,小明妈妈负责出黑板报,小明爸爸负责教小朋友们踢足球。此时学校和家长联系时是以家庭为单位的,家庭就是聚合对象。




      4 一体多面


      通过上述实例我们看到,即使是同一个自然人,在不同的场景下也有不同的身份,不同的身份对他的要求是不同的,输入和输出也是不同的。这就是一体多面。


      同理对于同一个对象,即使其在数据库只有一条数据,但是由于场景的不同,输入和输出也是不同的,所以有了各种看似复杂的对象。我们再回看上面三个问题,可以尝试给出本文的答案:


      对象种类多,增加理解成本:这是必须要付出的成本,小明不能嘴角挂着饭粒去上学


      对象之间转换,增加代码成本:这是必须要付出的成本,不同角色切换时必须要付出切换成本,小明不能用回应足球队教练的输出回应老师或者老师,这是截然不同的角色


      编写代码时有时不同层对象属性几乎一样:小明作为一个自然人,他自身固有特性也是相同的,所以表现在对象上是很多属性是一致的。但是不同的角色是有不同要求的,所以为了一些细微的差别,也是要新增对象的,这是必要的代价


      作者:JAVA前线
      来源:juejin.cn/post/7302740437529395211
      收起阅读 »

      Mysql升级后字符编码引起的血泪教训

      描述 现在大部分企业所使用的MySQL数据库相信都已经从5.7升级到了8,性能也得到了大幅度的提升 MySQL 8.0对于数据管理带来了很多改变,使得MySQL成为一个更强大、更灵活和更易于使用的数据库管理系统。 MySQL 8.0提供了更好的JSON支持...
      继续阅读 »

      描述


      现在大部分企业所使用的MySQL数据库相信都已经从5.7升级到了8,性能也得到了大幅度的提升


      MySQL 8.0对于数据管理带来了很多改变,使得MySQL成为一个更强大、更灵活和更易于使用的数据库管理系统。




      1. MySQL 8.0提供了更好的JSON支持,包括更快的JSON函数和表达式,以及新的JSON数据类型和索引。




      2. MySQL 8.0引入了窗口函数,这些函数可以用来计算分析函数的结果并根据指定的排序规则进行分组。




      3. MySQL 8.0提供了更好的空间数据支持,包括新的空间数据类型和函数,例如ST_Distance_Sphere函数,它可以计算两个点之间的球面距离。




      4. MySQL 8.0提供了更好的安全性,包括更安全的默认配置、更严格的密码策略、更多的SSL/TLS选项等。




      5. MySQL 8.0提供了更好的性能,包括新的索引算法、更好的查询优化器、更好的并发控制等。




      MySQL5.7


      查看版本号

      image.png


      查看编码格式

      image.png


      从结果可以看出,MySQL8默认字符编码为utf8mb4


      查看排序规则

      image.png


      从结果可以看出,MySQL8默认排序规则为utf8mb4_general_ci


      总结

      MySQL5.7 默认字符编码是utf8mb4,默认排序规则是utf8mb4_general_ci


      MySQL8


      查看版本号

      image.png


      查看编码格式

      image.png


      “character_set_client” 表示客户端字符集


      “character_set_connection” 表示连接字符集


      “character_set_server” 表示服务器字符集


      从结果可以看出,MySQL8默认字符编码为utf8mb4


      查看排序规则

      image.png


      从结果可以看出,MySQL8默认排序规则为utf8mb4_0900_ai_ci


      总结

      MySQL8 默认字符编码是utf8mb4,默认排序规则是utf8mb4_0900_ai_ci


      utf8 与 utf8mb4 区别




      1. 存储字符范围不同:



        • utf8 编码最多能存储 3 个字节的 Unicode 字符,支持的 Unicode 范围较窄,无法存储一些辅助平面字符(如 emoji 表情)。

        • utf8mb4 编码最多能存储 4 个字节的 Unicode 字符,支持更广泛的 Unicode 范围,包括了 utf8 所不支持的一些特殊字符和 emoji 表情等。




      2. 存储空间不同:



        • utf8 编码时,字符长度可以是最多 3 个字节。

        • utf8mb4 编码时,字符长度可以是最多 4 个字节。




      3. 对于存储 Emoji 和特殊字符的支持:



        • utf8mb4 能够存储和处理来自辅助平面的字符,包括emoji表情,这些字符需要使用 4 个字节来编码。而 utf8 不支持这些字符。




      utf8mb4_general_ci 与 utf8mb4_0900_ai_ci 区别




      1. utf8mb4_general_ci



        • 这是MySQL中较为通用的字符集和校对规则。

        • utf8mb4 是一种用于存储 Unicode 字符的编码方式,支持更广泛的字符范围,包括 emoji 等。

        • general_ci 是一种排序规则,对字符进行比较和排序时不区分大小写,对于大多数情况来说是足够通用的。




      2. utf8mb4_0900_ai_ci



        • 这是MySQL 8.0.0 版本后引入的校对规则。

        • 0900 表示MySQL 8.0.0 版本。

        • ai_ci 是指采用 accent-insensitive 方式,即对于一些有重音符号的字符,排序时会忽略重音的存在。




      主要区别在于排序规则的不同。utf8mb4_0900_ai_ci 在排序时会对重音符号进行忽略,所以某些含有重音符号的字符在排序时可能会与 utf8mb4_general_ci 有所不同。


      索引不生效问题


      表结构

      CREATE TABLE `user` (
      `id` bigint NOT NULL COMMENT '主键',
      `username` varchar(50) NOT NULL DEFAULT '' COMMENT '名称',
      `password` varchar(50) NOT NULL DEFAULT '' COMMENT '密码',
      `store_id` bigint NOT NULL DEFAULT 0 COMMENT '门店id',
      `is_delete` int NOT NULL DEFAULT '0' COMMENT '是否删除',
      PRIMARY KEY (`id`),
      KEY `idx_store_id` (`store_id`)
      ) ENGINE=InnoDB CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='用户表';


      CREATE TABLE `user_role` (
      `id` bigint NOT NULL COMMENT '主键',
      `user_id` bigint NOT NULL DEFAULT 0 COMMENT '用户id',
      `role_id` bigint NOT NULL DEFAULT 0 COMMENT '角色id',
      `is_delete` int NOT NULL DEFAULT '0' COMMENT '是否删除',
      PRIMARY KEY (`id`),
      KEY `idx_user_id` (`user_id`),
      KEY `idx_role_id` (`role_id`)
      ) ENGINE=InnoDB CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户角色关系表';


      查询

      SELECT DISTINCT
      t1.id,
      t1.username
      FROM
      user t1
      JOIN user_role t2 ON t2.user_id = t1.id
      WHERE
      t1.is_delete = 0
      and t2.is_delete = 0
      and t1.store_id = 2
      AND t2.role_id NOT IN (9, 6)


      执行计划

      企业微信截图_c83704fd-f85a-4dc7-901f-00a9cf35857e.png


      通过执行计划发现明明字段上加了索引,为什么索引没有生效


      explain format = tree 命令

      企业微信截图_e26332e8-cad7-42fc-bfb7-7c06fbadf26b.png


      问题找到了


      (convert(t2.user_id using utf8mb4) = t1.id))



      在回头看看表结构

      image.png


      为什么会不一致呢?

      mysql5.7 升级之前 两个表都是 CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci


      mysql5.7 升级到 mysql8 后,user_role 更新过表结构


      修改表排序规则


      ALTER TABLE user CHARACTER COLLATE = utf8mb4_0900_ai_ci;



      image.png


      再次查看执行计划

      企业微信截图_5a4e736a-a9b1-413a-b517-17e552d1b783.png


      企业微信截图_a97f807a-8c3b-4a8e-ad2f-9ad47a6f398e.png


      总结

      开发一般都不太注意表结构的字符编码和排序规则,数据库升级一定要先统一字符编码和排序规则


      查询的问题


      由于先发布应用,后执行的脚步,没有通知测试所以没有生产验证,导致第二天一大早疯狂报警


      image.png


      一看就是两个表字段排序规则不一致导致的


      只能修改表结构排序规则 快速解决


      总结


      升级MySQL是一个常见的操作,但在升级过程中可能会遇到各种问题。本文主要介绍排序规则不一致导致的问题,希望能对大家在升级MySQL时有所帮助。在进行任何升级操作之前,务必备份数据库,以防数据丢失。同时,建议定期对数据库进行性能优化,以提高系统的高可用。


      作者:三火哥
      来源:juejin.cn/post/7303349226066444288
      收起阅读 »

      直播点赞喷射表情效果实现

      web
      最近在线看直播年会。有一个点赞的按钮,点击点赞按钮喷射表情,表情在屏幕上向上浮动之后消失。觉得这个效果挺具有代表性,所以想实现一下。 找了一个别人的效果图 就来实现这个效果。 写一个点赞按钮 <style> .like-box { ...
      继续阅读 »

      最近在线看直播年会。有一个点赞的按钮,点击点赞按钮喷射表情,表情在屏幕上向上浮动之后消失。觉得这个效果挺具有代表性,所以想实现一下。


      找了一个别人的效果图


      点赞.gif


      就来实现这个效果。


      写一个点赞按钮


        <style>
      .like-box {
      width: 48px;
      height: 48px;
      border-radius: 50%;
      background-color: #ddd;
      position: relative;
      top: 360px;
      display: flex;
      align-items: center;
      justify-content: center;
      left: 300px;
      cursor: pointer;
      }

      .like-box i {
      font-size: 25px;
      }
      </style>
      <div class="like-box" id="like-box">
      <i class="icon-dianzan iconfont"></i>
      </div>

      1700297207118.png


      其中中间的图标用的是阿里巴巴矢量图标库的图标字体。


      动态创建表情


      动态表情用一个div表示。背景是表情图片。有6个备选表情


      image.png


      div样式


       .like-box div {
      position: absolute;
      width: 48px;
      height: 48px;
      background-image: url("./public/images/bg1.png");
      background-size: cover;
      z-index: -1;
      }

      使用js创建表情图标,并插入到点赞div


      const likeBox = document.getElementById('like-box') // id为like-box
      const createFace = function () {
      const div = document.createElement('div')
      return div
      }

      likeBox.addEventListener('click', function () {
      const face = createFace()
      likeBox.appendChild(face)
      })

      实现表情动画效果


      从最终效果图中可以看出,最终效果是由多个表情组成,更准确的说是由多个不同运动轨迹表情实现。所以关键是表情的轨迹实现。


      而在运动轨迹过程中,有大小缩放效果、有淡出效果。所以至少有三个animation。


      实现缩放效果


      使用animation实现缩放效果,添加animation样式


          @keyframes scale {
      0% {
      transform: scale(0.3);
      }

      100% {
      transform: scale(1.2);
      }
      }

      当动态创建表情div时,将缩放效果添加到div上,添加后效果


      缩放效果.gif


      实现淡出效果


      使用animation实现淡出效果,添加animation样式


          @keyframes opacity {
      0% {
      top: 0;
      }

      10% {
      top: -10px;
      }

      75% {
      opacity: 1;
      top: -180px;

      }

      100% {
      top: -200px;
      opacity: 0;
      }
      }

      当动态创建表情div时,将淡出效果添加到div上,添加后具体效果


      淡入淡出.gif


      实现不同轨迹效果


      单一轨迹

      创建单一轨迹样式效果


       @keyframes swing_1 {
      0% {}

      25% {
      left: 0;
      }

      50% {
      left: 8px;
      }

      75% {
      left: -15px;
      }

      100% {
      left: 15px;
      }
      }

      当动态创建表情div时,将单一轨迹样式效果添加到div上,添加后具体效果


      单一轨迹.gif


      多轨迹

      多轨迹有点麻烦,但是也不是很麻烦。具体思路是创建多个轨迹样式,然后在动态创建表情时给表情div随机添加各种轨迹样式,添加后具体效果


      多轨迹.gif


      最终js代码


      const likeBox = document.getElementById('like-box')

      const createFace = function () {
      // 随机表情
      const face = Math.floor(Math.random() * 6) + 1;
      // 随机轨迹
      const trajectory = Math.floor(Math.random() * 11) + 1; // bl1~bl11

      const div = document.createElement('div')

      div.className = `face${face} trajectory${trajectory}`;
      return div
      }


      likeBox.addEventListener('click', function () {
      const face = createFace()
      likeBox.appendChild(face)
      })

      移除产生的表情div


      为了避免一直添加div,乃至最后降低动画性能,需要等animation结束后移除动画div


      const likeBox = document.getElementById('like-box')

      const createFace = function () {
      const face = Math.floor(Math.random() * 6) + 1;
      const trajectory = Math.floor(Math.random() * 11) + 1; // bl1~bl11

      const div = document.createElement('div')

      div.className = `face${face} trajectory${trajectory}`;
      // 移除div
      div.addEventListener("animationend", () => {
      if(likeBox.contains(div)){
      likeBox.removeChild(div)
      }
      });
      return div
      }

      likeBox.addEventListener('click', function () {
      const face = createFace()
      likeBox.appendChild(face)
      })

      总结


      所有的效果实现都通过css的animation实现。实际还可以使用canvas实现。关键是实现的思路。


      核心思路:使用css的animation实现,基于对动画效果的拆分;拆出单一效果,之后所有效果同时发挥作用。


      最近想到一件事,初高中学到的数学知识应该可以应用在开发中。有些前端效果实际就是数学知识的一种应用。更准确的说是高中数学函数图形以及几何图形那块,这两块都有曲线的函数或者方程表示。


      如果要实现的效果是曲线或者轨迹的话,完全可以考虑它的坐标关系是不是数学中学到的。进而知道关系,进而开发出效果。


      我一直想把中学和大学的知识应用,我想上面就是应用的一个点。


      代码地址:github.com/zhensg123/r…


      (本文完)


      参考文章


      H5 直播的疯狂点赞动画是如何实现的?(附完整源码)


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

      我们工作快乐吗

      工作满意度是幸福感的重要预测指标。对于工作有多满意,很大程度上也决定了对人生有多满意。有研究表明,在幸福感的所有预测指标中,工作仅次于婚姻。盖洛普公司主导了一次针对全球十几万人的调查研究,包括了从原始部落到现代文明的不同工作形态,结果表明,不管在地球的哪个角落...
      继续阅读 »

      工作满意度是幸福感的重要预测指标。对于工作有多满意,很大程度上也决定了对人生有多满意。有研究表明,在幸福感的所有预测指标中,工作仅次于婚姻。盖洛普公司主导了一次针对全球十几万人的调查研究,包括了从原始部落到现代文明的不同工作形态,结果表明,不管在地球的哪个角落,「一份有意义的工作可以显著地预测幸福感。」


      在希腊神话里有这样一个故事,一个叫西西弗斯的国王,他自作聪明,戏弄了死神和冥王等众神。众神把他抓到阴间,给他一个恶劣的惩罚,他需要每天把一块大石头从山脚推到山顶,每当他正好要到达山顶的时候,大石就会滑落回山脚,西西弗斯只有顶着烈日,日复一日地将石头从推到山顶,如此循环往复。「西西弗斯这个故事代表了没有意义工作的极致,也是一项非常残酷的惩罚。」


      著名的富士康连续自杀事件,在工厂车间,几百个工人,日复一日地做着重复而简单的工作,不就是西西弗斯的神话在现实中发生?在无休止的加班和重复劳动中,不知道工作的意义是什么,如此麻木而紧张的工作状态,导致了最终的悲剧。


      上班如上坟:最糟糕的工作状态


      现在有一句话描述的特别贴切——“上班如上坟”,这句话非常形象地表达出了最糟糕的工作状态。


      马云曾经说过一个深受大家认同的观点,一个人离职,原因无外乎两点:



      • 钱没给够

      • 受委屈了


      其实真实发生的离职情况来看,第二点的因素占绝大多数,当我们工作中遇到了很多不快乐的事情时,就会触发大脑的情绪思维,从而产生了离职的念头。


      在互联网行业,大多数产生这种“上班如上坟”的状态是以下几种情况:



      • 「内卷」,一种是**「工作时长」上的卷,一天有做不完的工作,干不完的活,每天工作可能超过12个小时,甚至没有周末,类似于富士康的模式,简单低效且麻木,没有基本的休息时间保障;另一种是「内心」上的卷,可能是过高的要求,无法完成的目标,带来的无尽压力,产生了「绝望感和窒息感」**,长期在这个状态下非常容易出现心理问题。

      • 「得不到认可」,一直被领导PUA,这会导致一种无论如何努力都不被认可的无力感,产生非常大的精神内耗,会怀疑自己和否定自己,有很大的负面情绪产生。


      一旦达到“上班如上坟”这个状态,实际上已经是非常糟糕的情况了。往往这种状态下,大家会选择摆烂,消极对待工作,逃避,有离职冲动等等,工作和生活都会受到极大影响。


      金钱不过老六


      在正式工作之前,我们对于薪资有无比强烈的渴求,那时的offer选择真的很简单,哪一家给的钱多就去。在工作之后,很多公司挖人的手段也是以高薪涨幅为第一吸引力,在环境比较好的时候,往往跳槽才是获得更高薪酬的最有效手段。


      大多数打工人面临的是生存问题,大城市高额的生活成本,房贷车贷子女教育,是压在每个人心头的大山,尤其在深圳这样的地方,无数人在这个地方有着共同的目标——搞钱,金钱的重要性不言而喻。


      但是,我们是否过于重视金钱而忽视了其他?在赫兹伯格的研究中,「金钱排在成就、认可、工作本身、责任、晋升之后」,在第6位,「前5项因素才是真正驱动员工的动力所在」


      在赫兹伯格提出的著名的双因素理论中,金钱属于保健因素,它是必要的保障,只会让员工产生不满的情绪,而无法得到满意的情绪:


      image


      image


      实际上,我们在跳槽拿到高薪的最快乐的阶段,是在拿到Offer后的那段时间,以及拿到第一个月的薪水的时候,其他的时间的情绪感受都与金钱本身关系不大了。


      物质资源永远是有限的,在大幅度提高的情况下才能产生激励的效果。钱并不是万能的,从员工的角度来说,个人的身心健康应该是更重要的东西。


      快乐工作的价值


      谷歌在早些时候平均年薪仅在美国科技公司中排12位,但是却获得了最佳雇主排行榜的榜首。谷歌对员工开出了丰厚的条件:



      • 免费用餐

      • 班车接送

      • 股票期权

      • 发放IPad

      • 社交活动,比如瑜伽、葡萄酒、足球比赛、露天烧烤、吉他等

      • 健身房、免费按摩、免费理发、体能训练等等


      这些福利并非浪费,盖洛普指出,员工满意度提高5%,连带提升11.9%的外部客户满意度,同时企业效益也会提升2.5%


      后来这一类的福利策略也被其他公司纷纷效仿。快乐工作的第一步是需要有一个舒心的环境,让大家能够对公司满意,才能更加自发地了解探寻工作的意义。


      Denison Consulting公司调查显示,员工心情不愉快的企业年销售额仅增长了0.1%,而心情愉快的同期增长15.1%。企业若能将员工满意度提高20%,便可将财务绩效提升42%


      快乐工作,会让企业产率、质量、销售额、客户满意度、创新性、适应性都有一定的提升,降低员工缺勤率、跳槽率、紧张与疲劳感,这是雇主与雇员的双赢策略。


      「我们需要追求的终极目标是,让工作成为一种享受。」


      管理是控制还是服务


      相信绝大多数的管理者认为管理是服务,但是在实际行为中却不一定,比如:



      • 不应该对员工说“谢谢”,这种假客气只会产生距离感?

      • 不需要征询员工意见,我对结果要负责应该我说了算?

      • 成功来自于能力和实力,与其他因素无关?

      • 无法容忍下属的挑战,有悖于合作的伦理并产生糟糕的示范效应?

      • 每个人都得为自己的情绪负责,管理还有更重要的事情要做?

      • 保持开放的沟通固然重要,但是很容易让员工自以为是,需要保持距离?

      • 比起目标、绩效、团队的前途,友善和蔼的风格只会让权威旁落?

      • 每天压力这么大,谁能有时间去培养员工提升他们的能力呢?

      • 授权并不靠谱,现在这个环境,更适合各司其职,稳稳当当?

      • 说实话,所谓的愿景和将来就是忽悠,我如何与员工谈论自己都不相信的未来?


      这些问题能够帮助我们反思,自己究竟是在控制还是在服务,作为管理者,一个细微的举动可能在员工看来就会引起不小的情绪波动。


      我们冷漠吗


      工作氛围的糟糕,上班如上坟,十有八九跟管理者有很大的关系,更多的时候,冷漠与氛围直接相关,比如:



      • 同事见面都会热切的打招呼吗?

      • 同事们互相信任,困惑、焦虑和感受都能相互分享吗?

      • 大家在各种沟通场合中能够畅所欲言吗?是否藏着掖着?

      • 吃饭时会形成若干个自发团体吗?有人形单影只吗?

      • 下班之后同事还有可能有聚会吗?

      • 团队稳定性很高吗?团建大家都积极参与吗?

      • 新人容易融入吗?会有计划培养新人吗?

      • 奖励时常发生吗?无论是物质上的还是精神上的

      • 大家认可自己的工作是有价值的吗?

      • 领导的压力和情绪会透传给下属吗?

      • 领导愿意和员工单独交流吗?

      • 领导与员工相处舒服吗?

      • 领导善于夸奖人吗?


      这些问题有助于判别团队的氛围如何,一个好的工作氛围,才能享受工作,激发更大的动力,这样的团队才能创造更大的价值。


      内心驱动模式


      从个人内心的动力上,大概可以分为三类驱动模式:


      恐惧驱动



      • 害怕最亲近的人离开?

      • 害怕失去工作,失去工作能力?

      • 害怕失去健康?

      • 害怕不成功,窝窝囊囊过一辈子?

      • 害怕资产缩水,辛辛苦苦的钱贬值?

      • 害怕晚年凄凉,孤苦伶仃?

      • 害怕被领导同事瞧不起,不被认可?

      • 害怕家庭解体?

      • 害怕死亡?


      责任驱动



      • 要对工作负责,我做事是有品质的

      • 要对同事负责,不能给别人带来麻烦

      • 要对领导负责,不能辜负他的信任

      • 要对父母负责,懂得报恩

      • 要对那一位负责,已经是利益共同体

      • 要对孩子负责,作好的榜样

      • 要对银行负责,需要还贷

      • 要对朋友负责,有真心的朋友不容易

      • 要对国家负责,民族的脊梁

      • 要对自己负责,希望回首往事不留遗憾


      梦想驱动



      • 我想赚很多钱,可以衣食无忧想要什么就有什么

      • 我想成为大家都喜欢的人,因为我而快乐

      • 我想周游世界,看看外面的风景

      • 我想健康长寿,尽量活得久一点

      • 我想每天能够幸福满足,活出质感

      • 我想好日子延续下去,不要大起大落要平安喜乐

      • 我想大家都能够快快乐乐在一起

      • 我想做自己喜欢的事,可以不要为了钱而工作

      • 我想帮助更多需要帮助的人,人本来应该善良


      在工作上,「责任驱动、梦想驱动占据主导地位的人是非常稀缺的」,作为一个管理者,如果因为自己,把这类人变成了行尸走肉,只剩下恐惧驱动,真的是最大的过错,令人发指,天理难容!


      个人感悟


      上班如上坟这种状态,十有八九都是跟管理者有很大的关系。在目前互联网增速放缓的今天,物质金钱上已经无法带来激励的时候,这类问题尤为突出。若工作氛围很糟糕,更是雪上加霜,团队没有凝聚力,无法获得更高的产出,导致更多的裁员,形成恶性循环。


      在目前的大环境下,企业面对生存压力,沉重的指标压在管理者的身上,更需要管理者能够创造快乐的工作氛围,即使在互联网这个不得不加班的大环境下,也能够让工作成为一种享受,不要再让员工产生额外的内耗,驱动团队创造更大的价值,共克时艰。


      作者:孟健
      来源:juejin.cn/post/7292442438249791538
      收起阅读 »

      前段时间面试了一些人,有这些槽点跟大家说说​

      前段时间组里有岗位招人,花了些时间面试,趁着周末把过程中的感悟和槽点总结成文和大家讲讲。简历书写和自我介绍今年的竞争很激烈:找工作的人数量比去年多、平均质量比去年高。裸辞的慎重,要做好和好学校、有大厂经历人竞争的准备去年工作经历都是小公司的还有几个进了面试,今...
      继续阅读 »

      前段时间组里有岗位招人,花了些时间面试,趁着周末把过程中的感悟和槽点总结成文和大家讲讲。

      简历书写和自我介绍

      1. 今年的竞争很激烈:找工作的人数量比去年多、平均质量比去年高。裸辞的慎重,要做好和好学校、有大厂经历人竞争的准备

      1. 去年工作经历都是小公司的还有几个进了面试,今年基本没有,在 HR 第一关就被刷掉了

      2. 这种情况的,一定要走内推,让内推的人跟 HR 打个招呼:这人技术不错,让用人部门看看符不符合要求

      3. 用人部门筛简历也看学历经历,但更关注这几点:过去做了什么项目、项目经验和岗位对不对口、项目的复杂度怎么样、用到的技术栈如何、他在里面是什么角色

      4. 如果项目经历不太出彩,简历上可以补充些学习博客、GitHub,有这两点的简历我都会点开仔细查看,印象分会好很多

      5. 现在基本都视频面试,面试的时候一定要找个安静的环境、体态认真的回答。最好别用手机,否则会让人觉得不尊重!

      6. 我面过两个神人,一个在马路上边走边视频;另一个聊着聊着进了卫生间,坐在马桶上和我讲话(别问我怎么知道在卫生间的,他努力的声音太大了。。。)

      7. 自我介绍要自然一点,别像背课文一样好吗亲。面试官不是考你背诵,是想多了解你一点,就当普通聊天一样自然点

      8. 介绍的时候不要过于细节,讲重点、结果、数据,细节等问了再说

      9. 准备介绍语的时候问问自己,别人可以得到什么有用的信息、亮点能不能让对方快速 get 到

      10. 实在不知道怎么介绍,翻上去看第 4 点和第 5 点

      11. 出于各种原因,很多面试官在面试前没看过你的简历,在你做自我介绍时,他们也在一心二用 快速地浏览你的简历。所以你的自我介绍最好有吸引人的点,否则很容易被忽略

      12. 你可以这样审视自己的简历和自我介绍:

        a. 整体:是否能清晰的介绍你的学历、工作经历和技能擅长点

        b. 工作经历:是否有可以证明你有能力、有结果的案例,能否从中看出你的能力和思考

        c. 技能擅长点:是否有岗位需要的大部分技能,是否有匹配工作年限的复杂能力,是否有区别于其他人的突出点

      面试问题

      1. 根据公司规模、岗位级别、面试轮数和面试官风格,面试的问题各有不同,我们可以把它们简单归类为:项目经历、技能知识点和软素质

      2. 一般公司至少有两轮技术面试 + HR 面试,第一轮面试官由比岗位略高一级的人担任,第二轮面试官由用人部门领导担任

      3. 不同轮数考察侧重点不同。第一轮面试主要确认简历真实性和基础技术能力,所以主要会围绕项目经历和技能知识点;第二轮面试则要确认这个人是否适合岗位、团队,所以更偏重过往经历和软素质

      项目经历

      项目经历就是我们过往做过的项目。

      项目经历是最能体现一个程序员能力的部分,因此面试里大部分时间都在聊这个。

      有朋友可能会说:胡说,为什么我的面试大部分时候都是八股文呢?

      大部分都是八股文有两种可能:要么是初级岗位、要么是你的经历没什么好问的。哦还有第三种可能,面试官不知道问什么,从网上搜的题。

      在项目经历上,面试者常见的问题有这些:

      1. 不重要的经历占比过多(比如刚毕业的时候做的简单项目花了半页纸)

      2. 经历普通,没有什么亮点(比如都是不知名项目,项目周期短、复杂度低)

      3. 都是同质化的经历,看不出有成长和沉淀(比如都是 CRUD、if visible else gone)

      出现这种情况,是因为我们没有从面试官的角度思考,不知道面试的时候对方都关注什么。

      在看面试者的项目经历时,面试官主要关注这三点:

      1. 之前做的项目有没有难度

      2. 项目经验和当前岗位需要的是否匹配

      3. 经过这些项目,这个人的能力有哪些成长

      因此,我们在日常工作和准备面试时,可以这样做:

      1. 工作时有意识地选择更有复杂度的,虽然可能花的时间更多,但对自己的简历和以后发展都有好处

      2. 主动去解决项目里的问题,解决问题是能力提升的快车道,解决的问题越多、能力会越强

      3. 解决典型的问题后,及时思考问题的本质是什么、如何解决同一类问题、沉淀为文章、记录到简历,这些都是你的亮点

      4. 经常复盘,除了公司要求的复盘,更要做自己的复盘,复盘这段时间里有没有成长

      5. 简历上,要凸显自己在项目面试的挑战、解决的问题,写出自己如何解决的、用到什么技术方案

      6. 投简历时,根据对方业务类型和岗位要求,适当的调整项目经历里的重点,突出匹配的部分

      7. 面试时,要强调自己在项目里的取得的成果、在其中的角色、得到什么可复制的经验

      技能知识点

      技能知识点就是我们掌握的编程语言、技术框架和工具。

      相较于项目经历,技能知识点更关键,因为它决定了面试者是否能够胜任岗位。

      在技能知识点方面,面试者常见的问题有这些:

      1. 不胜任岗位:基础不扎实,不熟悉常用库的原理

      2. 技术不对口:没有岗位需要的领域技术

      3. 技术过剩:能力远远超出岗位要求


      第一种情况就是我们常说的“技术不行”。很多人仅仅在工作里遇到不会的才学习,工作多年也没有自己的知识体系,在面试的时候很容易被基础知识点问倒,还给自己找理由说“我是高级开发还问这么细节的,面试官只会八股文”。框架也是浅尝辄止,会用就不再深入学了,这在面试的时候也很容易被问住。

      第二种情况,是岗位工作内容属于细分领域,但面试者不具备这方面的经验,比如音视频、跨端等。为了避免这种情况,我们需要打造自己的细分领域技能,最好有一个擅长的方向,越早越好。

      第三种情况简单的来说就是“太贵了”。有时候一些资深点的开发面试被挂掉,并不是因为你的能力有问题,而是因为岗位的预算有限。大部分业务需求都是增删改查和界面展示,并不需要多复杂的经验。这种情况下,要么再去看看更高级的岗位,要么降低预期。

      在我面试的人里,通过面试的都有这些特点:

      1. 技术扎实:不仅仅基础好,还有深度

      2. 解决过复杂的问题:项目经验里除了完成业务需求,也有做一些有挑战的事

      有些人的简历上只写项目经历不写技能知识点,对此我是反对的,这样做增加了面试官了解你的成本。问项目经历的目的还是想确认你有什么能力,为什么不直接明了的写清楚呢?

      软素质

      这里的「软素质」指面试时考察的、技术以外的点。

      程序员的日常工作里,除了写代码还需要做这些事:

      1. 理解业务的重点和不同需求的核心点,和其他同事协作完成

      2. 从技术角度,对需求提出自己的思考和建议,反馈给其他人

      3. 负责某个具体的业务/方向,成为这个方面所有问题的处理者


      因此,面试官或者 HR 还会考察这些点,以确保面试者具备完成以上事情的能力:

      1. 理解能力和沟通表达能力

      2. 业务能力

      3. 稳定性


      第一点是指面试者理解问题和讲清楚答案的能力。遇到过一些面试者,面试的时候过于紧张,讲话都讲不清楚,这种就让人担心“会不会是个社恐”、“工作里该不会也这样说不清楚吧”;还有的人爱抢答,问题都没听明白就开始抢答,让人怀疑是不是性格太急躁太自大;还有的人过于能讲,但讲不到重点,东扯西扯,让人对他的经历和理解能力产生了怀疑。

      第二点是指在实现业务目标的过程中可以提供的能力。 业务发展是需要团队共同努力的,但有的人从来没这么想过,觉得自己上班的任务就是写代码,来什么活干什么活,和外包一样。

      业务发展中可能有各种问题。定方向的领导有时候会过于乐观、跨部门协作项目可能会迟迟推进不动、产品经理有时候也会脑子进水提无用需求、质量保障的测试同学可能会大意漏掉某个细节测试。这个时候,程序员是否能够主动站出来出把力,帮助事情向好的方向发展,就很重要了。

      遇到过一些面试者,在一家公司干了好几年,问起来业务发展情况语焉不详,让人感觉平时只知道写代码;还有的面试者,说起业务问题抱怨指责一大堆,“领导太傻逼”、“产品经理尽提蠢需求”,负能量满满😂。

      第三点是指面试者能不能在一家公司长久干下去。 对于级别越高的人,这点要求就越高,因为他的离开对业务的发展会有直接影响。即使级别不高,频繁换工作也会让人对你有担心:会不会抗压能力很差、会不会一不涨工资就要跑路。一般来说,五年三跳就算是临界线,比这个频繁就算是真的“跳的有点多”。

      针对以上这三点,我们可以这样做:

      1. 面试时调整心态,当作普通交流,就算不会也坦然说出,不必过于紧张

      2. 回答问题时有逻辑条理,可以采用类似总分总的策略

      3. 工作时多关注开发以外的事,多体验公司产品和竞品,在需求评审时不摸鱼、多听听为什么做、思考是否合理、提出自己的想法

      4. 定好自己的职业规划(三年小进步、五年大进步),在每次换工作时都认真问问自己:下一份工作能否帮助自己达到目标

      总结

      好了,这就是我前段时间面试的感悟和吐槽。

      总的来说,今年找工作的人不少,市面上的岗位没有往年那么多。如果你最近要换工作,最好做足准备。做好后面的规划再换、做好准备再投简历、经历整理清楚再面试。


      作者:拭心又在思考了我的天
      来源:mp.weixin.qq.com/s/TvfKTXUHZAR37f9oi3W12w

      收起阅读 »

      为什么手机厂商都纷纷入局自研操作系统?

      时间线 2020 年 9 月 10 日,华为召开了开发者大会,正式推出了 HarmonyOS 2.0 系统,并宣布为开发者提供完整分布式设备与应用开发生态。 2023年10月17日,小米集团首席执行官雷军正式公布了这款操作系统,并称该系统将逐步接替之前小米开...
      继续阅读 »

      时间线


      2020 年 9 月 10 日,华为召开了开发者大会,正式推出了 HarmonyOS 2.0 系统,并宣布为开发者提供完整分布式设备与应用开发生态。


      image.png


      2023年10月17日,小米集团首席执行官雷军正式公布了这款操作系统,并称该系统将逐步接替之前小米开发的MIUI系统,成为小米智能硬件互联生态的统一计算平台。首款搭载HyperOS的智能手机为小米14系列手机。10月20日、23日,MIUI的微信、微博等公众平台账号陆续将MIUI改为其中文名“小米澎湃OS”。


      image.png


      2023年11月3日消息,在此前举行的 2023 vivo 开发者大会上,vivo 已经宣布首款搭载蓝河操作系统的设备将是 vivo WATCH 3 手表。vivo 副总裁周围在接受媒体采访时明确表示,vivo 自研蓝河操作系统不兼容安卓应用。


      image.png


      以上这几年国内著名手机厂商公布自研操作系统的时间线。笔者最近比较对国内操作系统的发展动向比较感兴趣,所以就开始了解收集这方面的信息,想通过这篇文章帮我看清操作系统发展脉络或者说补充一些相关认知盲区。


      先用一张图厘清操作系统发展脉络


      大家应该都知道华为、小米和Vivo分别是国内三个著名的Android手机厂商。国内移动互联网快速的发展,也是得益于Android的开放性,国内手机厂商基本都是基于ASOP(Android Open Source Project)魔改源码逐渐形成自己的定制操作系统,比如HarmonyOS、小米的MIUI和Vivo的OriginOS。如下图所示:


      Xnip2023-11-20_19-34-03.jpg


      iOS 是由苹果开发的移动操作系统。苹果最早于2007年1月9日的Macworld大会上公布这个系统,最初是设计给iPhone使用的,后来陆续套用到iPod touch、iPad上。iOS与苹果的macOS操作系统一样,属于类Unix的商业操作系统。苹果凭借它极具竞争力的软硬件创新产品,妥妥是一方巨头,跟Android开放生态不一样,苹果的操作系统并不开源。


      但从2023年国内手机厂商公布的操作系统来看,从曾经套壳Android逐渐演变成去安卓化趋势。那为啥会有这种趋势发生,或者说这真的是未来操作系统发展趋势吗,国内厂商可能会面临什么样的挑战?这个问题的答案我们需要时间去验证,我们只能先做些畅想。


      国内手机厂商是如何发展起来的?


      国内手机厂商的发展主要经历了以下几个阶段:



      1. 初步阶段(2000-2003年):在这个阶段,国内手机市场刚刚起步,消费者对手机的需求逐渐增加。一些国内厂商开始生产手机,主要以低端和中端产品为主,如联想、TCL等。

      2. 品牌竞争阶段(2004-2006年):随着消费者对手机品牌的关注度增加,一些知名品牌开始崛起,如华为、中兴、酷派等。这些品牌通过自主研发、产品创新和技术积累,逐渐在中高端市场占据一席之地。

      3. 智能手机时代(2007-2010年):智能手机的兴起,为国产手机厂商提供了新的发展机遇。在这个阶段,小米、OPPO、vivo等厂商迅速崛起,通过高性价比、良好的用户体验和市场营销策略,逐渐在国内市场占据重要地位。

      4. 全球化竞争(2011年至今):随着国内手机市场的逐渐饱和,国产手机厂商开始将目光投向海外市场,如印度、东南亚等新兴市场。此外,厂商也在积极布局5G、物联网等新兴领域,以寻求新的增长点。


      Android系统最早于2008年9月23日正式发布。进入中国市场的时间则是在2009年左右,随着第一款搭载Android系统的手机HTC Dream(在中国市场称为G1)的上市,Android系统开始在中国市场逐渐普及。


      华为、小米和vivo这些手机厂商开始使用Android系统的时间如下:



      1. 华为:华为早在2009年就推出了第一款搭载Android系统的智能手机——华为U8220。此后,华为逐步发展成为全球领先的手机厂商之一,并在Android系统上推出了自家的EMUI(Emotion UI)用户界面。

      2. 小米:小米成立于2010年4月,同年8月推出了基于Android系统的MIUI定制系统。2011年8月,小米发布了第一款搭载Android系统的手机——小米1。凭借高性价比和出色的用户体验,小米迅速崛起成为国内外知名的手机品牌。

      3. vivo:vivo成立于2009年,2011年推出了第一款搭载Android系统的智能手机——vivo X1。随后,vivo在Android系统上推出了自家的Funtouch OS用户界面,并逐步发展成为市场份额较高的手机厂商之一。


      从上面我们回顾了下国内手机厂商的发展历史,可以说没有Android系统进入国内市场,中国的智能手机时代历史进程就不可能这么快,国内手机厂商也正是抓住了这波机遇完成了国产化,并且发展至今。


      更具象化分析智能手机厂商发展


      从我个人的分析理解,国内手机厂商发展分为以下几个阶段:



      1. 第一阶段:本土化定制,实现自主可控
        这个阶段国内手机厂商主要关注于对Android系统进行本土化定制,通过开发自家的用户界面(如华为的EMUI、小米的MIUI、vivo的Funtouch OS等),满足国内用户的需求和使用习惯。同时,厂商也开始关注硬件研发和生产,逐步减少对外部供应商的依赖,提高自主可控能力。

      2. 第二阶段:自家产品生态化拓展建设
        在第二阶段,国内手机厂商开始关注产品生态的构建,将手机作为核心,围绕其开发各类智能硬件和应用服务,如智能家居、可穿戴设备、云服务等。这有助于提高用户粘性,形成闭环的生态系统,从而提升品牌竞争力和市场份额。

      3. 第三阶段:公布自研操作系统,引入AI大模型完善产品体验
        在第三阶段,国内手机厂商开始研发自家的操作系统,如华为的鸿蒙OS(HarmonyOS),以降低对Android系统的依赖,提高自主创新能力。同时,厂商也开始引入人工智能技术,通过AI大模型优化产品性能、提升用户体验,为未来5G、物联网等新技术应用做好准备。


      目前手机厂商已经来到第三阶段,自华为公布鸿蒙系统之后,小米和Vivo也相继公布自家的自研操作系统,自主创新似乎是国内厂商不约而同的战略共识。


      避免被“卡脖子”再发生


      2019年5月15日,华为被美国正式制裁,截止目前,已经过去了4年。那么这些年华为到底被制裁了什么?


      华为受到的制裁主要包括以下几个方面:



      1. 美国实体清单:2019年5月,美国政府将华为及其关联公司列入实体清单,禁止美国企业与华为进行商业往来,除非获得特别许可。这意味着华为无法从美国供应商处购买关键零部件和技术,如芯片、软件等。

      2. 芯片供应限制:2020年5月,美国政府进一步收紧对华为的制裁,要求全球芯片制造商在向华为供应美国技术含量超过25%的产品时,必须获得美国政府的许可。这使得华为的芯片供应受到严重影响,尤其是高端芯片。

      3. 软件限制:华为受到制裁后,谷歌停止为华为新款手机提供GMS(谷歌移动服务),包括谷歌应用商店、Gmail、YouTube等关键应用。尽管华为推出了自家的HMS(华为移动服务)替代GMS,但在全球范围内,GMS仍然具有很高的市场需求和用户粘性。

      4. 网络设备限制:美国政府将华为视为国家安全威胁,禁止美国电信运营商使用联邦补贴购买华为的网络设备。此外,美国还在全球范围内施压,敦促其他国家在5G网络建设中排除华为。

      5. 金融制裁:美国政府还对华为进行金融制裁,限制华为及其关联公司在美国的金融交易,使其在全球范围内融资和开展业务受到限制。


      这些制裁对华为的业务造成了很大影响,尤其是在芯片供应、软件服务和5G网络设备方面。然而,华为在此期间加大了自主研发投入,努力寻求突破和替代方案。


      华为是如何应对技术封锁的?



      • 推出麒麟9000S芯片突破5G芯片断供


      image.png
      今年爆火的Mate60系列手机搭载就是这款芯片,华为终于扬眉吐气了一番。



      • 推出自研鸿蒙全场景分布式操作系统


      image.png



      • 推出HMS 替代 GMS


      image.png


      当然还有很多自主创新的突破,从这些战略手段来看,华为确实非常值得尊敬的企业,至少给国内众多科技企业打了个样。


      从华为被制裁的案例来看,国内其他一众手机厂商谁都无法预测自己可能是下一个华为。那么实现自主可控就显得非常迫切了,除非国内一众厂商放弃海外市场这块蛋糕,但这看起来是不太可能的事情。


      万物互联生态蛋糕


      我们知道不管是华为还是小米,它们的产品可不仅仅只是手机和PC,还有全场景的IoT智能硬件(包括智能家居、可穿戴设备,平板等等),未来还有智能驾驶汽车业务,比如现在小米正在如火如荼正在进行的造车运动。


      小米IoT平台设备接入数据
      image.png
      小米IoT万物互联布局
      image.png


      小米智能汽车
      image.png


      面对如此庞大的设备和连接数,小米需要一套融合的系统框架统一支持全生态设备与应用,要成为未来百亿设备、百亿连接,万物互联的公有底座。这也能理解这为什么小米会推出澎湃OS来替代MIUI,未来构建万物互联的大格局所做出的战略性布局。


      然而国内手机厂商的产品布局大致相同,属于强竞争关系,虽说鸿蒙系统较早就发布,但从商业竞争来看,这是另外一个层面的自主可控,让小米、Vivo这些厂商投入到鸿蒙怀抱目前来看是不太现实的,毕竟谁都不想当谁的小弟,或许形成操作系统联盟生态更合适。


      写在最后


      从最开始时间线开始我们了解到国产厂商纷纷入局自研操作系统,然后通过一张图理解了操作系统的发展脉络,再了解到国产手机厂商是如何发展起来的,Android的进入让国产手机进入智能化时代,我们再具象化理解目前如华为、小米和Vivo这些手机厂商发展的几个阶段。从华为被美国制裁,国内厂商引发的担忧和战略布局万物互联生态,最终为了能够实现自主可控,摆脱外部依赖的风险,纷纷入局自研操作系统。从各家厂商分别分布自己家的自研操作系统,说明这件事相比于自研芯片的难度要小很多。从操作系统发展脉络来看至少是站在巨人的肩膀上,完全自主产权这样的说法听听就好。要撼动Android和iOS的地位,要看开发者买不买单,这么多年所沉淀下来的生态要是这么容易就能够替代,那么当年Windows Phone就不会落寞而去,相比于微软大家觉得我们的差距还有多大呢。


      作者:巫山老妖
      来源:juejin.cn/post/7303413519907586057
      收起阅读 »

      虽然炒股赚了十多万,但差点倾家荡产!劝你别入坑

      今天,五阳哥不打算聊技术,而是聊一下炒股的话题。我自认为在这方面有发言权,自述一个程序员的炒股经历。 2019年,我开始涉足股市,在2021年中旬为了购房,将持有的股票全部卖出,赚了十多万元。在最高峰时期,我获利超过了二十多万元,但后来又回吐了一部分利润。虽然...
      继续阅读 »

      今天,五阳哥不打算聊技术,而是聊一下炒股的话题。我自认为在这方面有发言权,自述一个程序员的炒股经历。


      2019年,我开始涉足股市,在2021年中旬为了购房,将持有的股票全部卖出,赚了十多万元。在最高峰时期,我获利超过了二十多万元,但后来又回吐了一部分利润。虽然我的炒股成绩不是最出色的,但也超过了很多人。因为大多数股民都是亏损的,能够在股市长期盈利的人真的是凤毛麟角。


      股市中普遍流传的七亏二平一赚的说法并不只是传闻,事实上,现实中的比例更加残酷,能够长期赚钱的人可能连10%都达不到。


      接下来,我想谈谈我的炒股经历和心路历程,与大家分享一下我的内心体验,为那些有意向或正在炒股的朋友提供一些参考。希望劝退大家,能救一个是一个!


      本文倒叙描述,先聊聊最后的疯狂和偏执!


      不甘失败,疯狂上杠杆


      股市有上涨就有下跌,在我卖出以后,股市继续疯涨了很多。当时长春高新,我是四百一股买入,六百一股就卖出了,只赚了2万。可是在我卖出去的两个月以后,它最高涨到了一千。相当于我本可以赚六万,结果赚了两万就跑了。


      我简直想把大腿拍烂了,这严重的影响了我的认知。我开始坚信,这只股票和公司就是好的,非常牛,是我始乱终弃,我不应该早早抛弃人家。 除了悔恨,我还在期盼它下跌,好让我再次抄底,重新买入,让我有重新上车的机会!


      终于这只股票后来跌了10%,我觉得跌的差不多了,于是我开始抄底买入!抄底买入的价格在900一股(复权前)。


      没想到,这次抄底是我噩梦的开始。我想抄他的底,他想抄我的家!


      image.png


      这张图,完美的诠释了我的抄底过程。地板底下还有底,深不见底,一直到我不再敢抄底为止。一直抄到,我天天睡不着觉!


      当时我九百多一股开始抄底买入,在此之前我都是100股,后来我开始投入更多的资金在这只股票上。当时的我 定下了规矩,鸡蛋不能放在一个篮子里;不能重仓一只股票,要分散投资;这些道理我都明白,但是真到了节骨眼上,我不想输,我想一把赢回来,我要抄底,摊平我的成本。


      正所谓:高位加仓,一把亏光。之前我赚的两万块钱,早就因为高位加仓,亏回去了。可是我不甘心输,我想赢回来。当时意识不到也不愿意承认:这就是赌徒心理。


      后来这只股票,从1000,跌倒了600,回调了40%。而我已经被深深的套牢。当时我盈利时,只买了1股。等我被套牢时,持有了9股。 按照1000一股,就是九十万。按照600一股,就是54万。


      我刚毕业,哪来的那么多钱!


      我的钱,早就在800一股的时候,我就全投进去了,我认为800已经算是底了吧,没想到股价很快就击穿了800。


      于是我开始跟好朋友借钱。一共借了10万,商量好借一年,还他利息。后来这10万块钱,也禁不住抄底,很快手里没钱了,股价还在暴跌。我已经忘记当时亏多少钱了,我当时已经不敢看账户了,也不敢细算亏了多少钱!


      于是,我又开始从支付宝和招商银行借贷,借钱的利率是相当高的,年利息在6%以上。当时一共借了30万。但是股价还不见底,我开始焦虑的睡不着觉。


      不光不见底,还在一直跌,我记得当时有一天,在跌了很多以后,股价跌停 -10%。当时的我已经全部资金都投进去了,一天亏了5万,我的小心脏真的要受不了了。跌的我要吐血! 同事说,那天看见我的脸色很差,握着鼠标手还在发抖!


      跌成这样,我没有勇气打开账户…… 我不知道什么时候是个头,除了恐惧只有恐惧,每天活在恐惧之中。


      我盘算了一下,当时最低点的我,亏了得有二十多万。从盈利六万,一下子到亏二十多万。只需要一个多月的时间。


      我哪里经历过这些,投资以来,我都是顺风顺水的,基本没有亏过钱,从来都是挣钱,怎么会成这个样子。


      当时的我,没空反思,我只希望,我要赚回来!我一定会赚回来,当时能借的支付宝和招行都已经借到最大额度了…… 我也没有什么办法了,只能躺平。


      所以股价最低点的时候,基本都没有钱加仓。


      侥幸反弹,但不忍心止盈


      股价跌了四个月,这是我人生极其灰暗的四个月。后来因为种种原因,股价涨回来了,当时被传闻的事情不攻自破,公司用实际的业绩证明了自己。


      股价开始慢慢回暖,后来开始凶猛的反弹,当时的我一直认为:股价暴跌时我吃的所有苦,所有委屈,我都要股市给我补回来!


      后来这段时间,股价最高又回到了1000元一股(复权前)。最高点,我赚了二十多万,但是我不忍心止盈卖出。


      我觉得还会继续涨,我还在畅想:公司达到,万亿市值。


      我觉得自己当时真的 失了智了。


      结婚买房,卖在最高点


      这段时间,不光股市顺丰顺水,感情上也比较顺利,有了女朋友,现在是老婆了。从那时起,我开始反思自己的行为,我开始意识到,自己彻彻底底是一个赌徒。


      因为已经回本了,也赚了一点钱,我开始不断的纠结要不要卖出,不再炒股了。


      后来因为两件事,第一件是我姐姐因为家里要做小买卖,向我借钱。 当时的我,很纠结,我的钱都在股市里啊,借她钱就得卖股票啊,我有点心疼。奈何是亲姐,就借了。


      后来我盘算着,不对劲。我还有贷款没还呢,一共三十万。我寻思,我从银行借钱收6%的利息,我借给别人钱,我一分利息收不到。 我TM 妥妥的冤大头啊。


      不行,我要把贷款全部还上,我Tm亏大了,于是我逐渐卖股票。一卖出便不可收拾。


      我开始担心,万一股价再跌回去,怎么办啊。我和女朋友结婚时,还要买房,到时候需要一大笔钱,万一要是被套住了,可怎么办啊!


      在这这样的焦虑之下,我把股票全部都卖光了!


      冥冥之中,自有天意。等我卖出之后的第二周,长春高新开启了下一轮暴跌,而这一轮暴跌之后,直至今日,再也没有翻身的机会。从股价1000元一股,直至今天 300元一股(复权前是300,当前是150元)。暴跌程度大达 75%以上!


      image.png


      全是侥幸


      我觉得我是幸运的,如果我迟了那么一步!假如反应迟一周,我觉得就万劫不复。因为再次开启暴跌后,我又会开始赌徒心理。


      我会想,我要把失去的,重新赢回来!我不能现在卖,我要赢回来。再加上之前抄底成功一次,我更加深信不疑!


      于是我可能会从1000元,一路抄底到300元。如果真会如此,我只能倾家荡产!


      不是每个人都有我这么幸运,在最高点,跑了出去。 雪球上之前有一个非常活泼的用户, 寒月霖枫,就是因为投资长春高新,从盈利150万,到亏光100万本金,还倒欠银行!


      然而这一切,他的家人完全不知道,他又该如何面对家人,如何面对未来的人生。他想自杀,想过很多方式了结。感兴趣的朋友可以去 雪球搜搜这个 用户,寒月霖枫。


      我觉得 他就是世界上 另一个自己。我和他完全类似的经历,除了我比他幸运一点。我因为结婚买房和被借钱,及时逃顶成功,否则我和他一样,一定会输得倾家荡产!


      我觉得,自己就是一个赌狗!


      image.png


      image.png


      然而,在成为赌狗之前,我是非常认真谨慎对待投资理财的!


      极其谨慎的理财开局


      一开始,我从微信理财通了解到基金,当时2019年,我刚毕业两年,手里有几万块钱,一直存在活期账户里。其中一个周末,我花时间研究了一下理财通,发现有一些债券基金非常不错。于是分几批买了几个债券基金,当时的我对于理财既谨慎又盲目。


      谨慎的一面是:我只敢买债券基金,就是年利息在 5%上下的。像股票基金这种我是不敢买的。


      盲目的一面是:我不知道债券基金也是风险很大的,一味的找利息最多的债券基金。


      后来的我好像魔怔了,知道了理财这件事,隔三差五就看看收益,找找有没有利息更高的债券基金。直到有一天,我发现了一个指数基金,收益非常稳定。


      是美股的指数基金,于是我买了1万块钱,庆幸的是,这只指数基金,三个月就赚了八百多,当时的我很高兴。那一刻,我第一次体会到:不劳而获真的让人非常快乐!


      如饥似渴的学习投资技巧


      经过一段时间的理财,我对于理财越来越熟悉。


      胆子也越来越大,美股的指数基金赚了一点钱,我害怕亏回去,就立即卖了。卖了以后就一直在找其他指数基金,这时候我也在看国内 A股的指数基金,甚至行业主题的基金。


      尝到了投资的甜头以后,我开始花更多的时间用来 找基。我开始从方方面面评估一只基金。


      有一段时间,我特别自豪,我在一个周末,通过 天天基金网,找到了一个基金,这只基金和社保投资基金的持仓 吻合度非常高。当时的我思想非常朴素, 社保基金可是国家队,国家管理的基金一定非常强,非常专业,眼光自然差不了。这只基金和国家队吻合度如此高,自然也差不了。


      于是和朋友们,推荐了这只基金。我们都买了这只基金,而后的一个月,这只基金涨势非常喜人,赚了很多钱,朋友们在群里也都感谢我,说我很厉害,投资眼光真高!


      那一刻,我飘飘然……


      我开始投入更多的时间用来理财。下班后,用来学习的时间也不学习了,开始慢慢的过度到学习投资理财。我开始不停地 找基。当时研究非常深入,我会把这只基金过往的持仓记录,包括公司都研究到。花费的时间也很多。


      我也开始看各种财经分析师对于股市的分析,他们会分析大盘何时突破三千点,什么时候股市情绪会高昂起来,什么行业主题会热门,什么时候该卖出跑路了。


      总之,投资理财,可以学习的东西多种多样!似乎比编程有趣多了。


      换句话说:我上头了


      非常荒谬的炒股开局


      当时我还是非常谨慎地,一直在投资基金,包括 比较火爆的 中欧医疗创新C 基金,我当时也买了。当时葛兰的名气还很响亮呢。后来股市下行,医疗股票都在暴跌,葛兰的基金 就不行了,有句话调侃:家里有钱用不完,中欧医疗找葛兰。腰缠万贯没人分,易方达那有张坤。


      由此可见,股市里难有常胜将军!


      当时的我,进入股市,非常荒谬。有一天,前同事偷偷告诉我,他知道用友的内幕,让我下午开盘赶紧买,我忙追问,什么内幕,他说利润得翻五倍。 我寻思一下,看了一眼用友股票还在低位趴着,心动了。于是我中午就忙不迭的线上开户,然后下午急匆匆的买了 用友。 事后证明,利润不光没有翻五倍,还下降了。当然在这之前,我早就跑了,没赚着钱,也没咋亏钱。


      当时的我,深信不疑这个假的小道消息,恨不得立即买上很多股票。害怕来不及上车……


      自从开了户,便一发不可收拾,此时差2个月,快到2019年底!席卷全世界的病毒即将来袭


      这段时间,股市涨势非常好,半导体基金涨得非常凶猛! 我因为初次进入股市,没有历史包袱,哪个股票是热点,我追哪个,胆子非常大。而且股市行情非常好,我更加相信,自己的炒股实力不凡!


      换句话说:越来越上头,胆子越来越大。 学习编程,学个屁啊,炒股能赚钱,还编个屁程序。


      image.png


      刚入股市,就赶上牛市,顺风顺水


      2019年底到2020年上半年,A股有几年不遇的大牛市,尤其是半导体、白酒、医疗行业行情非常火爆。我因为初入股市,没有历史包袱,没有锚点。当前哪个行业火爆,我就买那个,没事就跑 雪球 刷股票论坛的时间,比上班的时间还要长。


      上班摸鱼和炒股 是家常便饭。工作上虽然不算心不在焉,但是漫不经心!


      image.png


      在这之前,我投入的金额不多。最多时候,也就投入了10万块钱。当时基金收益达到了三万块。我开始飘飘然。


      开始炒股,也尝到了甜头,一开始,我把基金里的钱,逐渐的转移到股市里。当时的我给自己定纪律。七成资金投在基金里,三成资金投在股市里。做风险平衡,不能完全投入到风险高的股市里。


      我自认为,我能禁得住 炒股这个毒品。


      但是逐渐的,股票的收益越来越高,这个比例很快就倒转过来,我开始把更多资金投在股市中,其中有一只股票,我非常喜欢。这只股票后来成为了很多人的噩梦,成为很多股民 人生毁灭的导火索!


      长春高新 股票代码:000661。我在这只股票上赚的很多,后来我觉得股市涨了那么多,该跌了吧,于是我就全部卖出,清仓止盈。 当时的我利润有六万,我觉得非常多了,我非常高兴。


      其中 长春高新 一只股票的利润在 两万多元。当时这是我最喜欢的一只股票。我做梦也想不到,后来这只股票差点让我倾家荡产……


      当时每天最开心的事情就是,打开基金和证券App,查看每天的收益。有的时候一天能赚 两千多,比工资还要高。群里也非常热闹,每个人都非常兴奋,热烈的讨论哪个股票涨得好。商业互吹成风……


      换句话说:岂止是炒股上头,我已经中毒了!


      image.png


      之后就发生了,上文说的一切,我在抄底的过程中,越套越牢……


      总结


      以上都是我的个人真实经历。 我没有谈 A 股是否值得投资,也不评论当前的股市行情。我只是想分享自己的个人炒股经历。


      炒股就是赌博


      我想告诉大家,无论你在股市赚了多少钱,迟早都会还回去,越炒股越上头,赚的越多越上头。


      赌徒不是一天造成的,谁都有赢的时候,无论赚多少,最终都会因为人性的贪婪 走上赌徒的道路。迟早倾家荡产。即使你没有遇到长春高新,也会有其他暴跌的股票等着你!


      什么🐶皮的价值投资! 谈价值投资,撒泡尿照照自己,你一个散户,你配吗?


      漫漫人生路,总会错几步。股市里错几步,就会让你万劫不复!



      ”把钱还我,我不玩了“




      ”我只要把钱赢回来,我就不玩了“



      这都是常见的赌徒心理,奉劝看到此文的 程序员朋友,千万不要炒股和买基金。


      尤其是喜欢打牌、打德州扑克,喜欢买彩-票的 赌性很强的朋友,一定要远离炒股,远离投资!


      能救一个是一个!


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

      这样解释shift,面试官直接起立!

      面试官提问 面试官:候选人你好,请解释下面的现象: 数组1000个元素,shift操作和对象delete操作的benchmark 差不多。 数组10w个元素时,shift操作和对象delete操作的benchmark 相差巨大。 场景如下: 数组1000个...
      继续阅读 »

      面试官提问


      面试官:候选人你好,请解释下面的现象:



      1. 数组1000个元素,shift操作和对象delete操作的benchmark 差不多。

      2. 数组10w个元素时,shift操作和对象delete操作的benchmark 相差巨大。


      场景如下:
      数组1000个元素时:shift和delete相差无几
      63368106fc02fd76c9cd6cc951dc064.png


      数组100000个元素时,shift比delete慢了100倍
      a120b0387b1118d83ffbc4ac0b3f052.png


      开始表演!


      候选人:总的来说,这是因为数组在1k长度时,v8引擎能申请到一段连续内存做shift运算,在利用L1缓存的优势下,速度能和object的delete有得一比。


      顺便一提,利用L1缓存思想做性能优化,也是最近游戏界中ECS架构为何能获得如此高的关注度的原因之一。


      我们回到咱们前端来讲,口说无凭,眼见为实,让我们直接打开V8引擎源码一睹为快。


      第一步、我们找到[].shift相关v8引擎源码


      我们可以看到ArrayPrototypeShift代码会尝试执行TryFastArrayShift函数,若该函数抛出Slow或者Runtime标志,则运行相应的逻辑。


      image.png


      第二步、我们进入 TryFastArrayShift 这个函数继续看,这个函数有两个逻辑:



      1. 若没有连续内存,则抛出Slow

      2. 若数组长度>100,则抛出Runtime



      回到我们案例:我们数组长度如果在1000,则抛出的是Runtime标志;如果在10W,则抛出的是Slow标志。



      image.png


      第三步、查看Slow和Runtime的逻辑。




      • slow对应的GenericArrayShift函数逻辑如下 :



        • 先把数组转换为对象

        • 再遍历对象的key,每一个key都往前移一位。(这也是ECMAScript-262规范定义的算法)
          image.png




      • runtime对应的ArrayShift函数逻辑如下:



        1. 申请连续内存

        2. 遍历并移位

          image.png




      最后总结,从上面三步我们确认了V8引擎的执行逻辑:



      1. 10W数据,很难申请到连续内存,通常就无法利用L1缓存,导致比较卡慢

      2. 1000数据,较容易申请到连续内存,通常能利用到L1缓存,速度较快。

        1. 100长度以下的数组,直接走C++逻辑。

        2. 100长度以上的数组,走汇编逻辑加速。




      这也就说明了为什么1000数据和10W数据执行上有一定差异的原因。



      注意:ArrayShift函数源码可能有误,也请大佬指点。


      PS:这个ArrayShift源码不在v8中,而在汇编中。


      PS:这是因为buitin的代码需要在vs上编译出来才能查到相关代码的引用地址,目前比较忙,先分享思路,我后续会进行更新订正。



      小结


      上诉面试经历是我刚编的,希望能来更多的朋友讨论,毕竟大家对v8源码或多或少有一点陌生的畏难心理,我也如此。本问题实际是来自我交流群的某一次讨论。


      不过道理是真的:


      平时遇到某些问题,咱们也不妨从V8源码入手分析。


      有时候源码比各类文章讲得更清晰。


      日积月累,总会有一些意想不到的收获。


      参考文章



      作者:尘码在划水
      来源:juejin.cn/post/7302330573382107148
      收起阅读 »

      Altman王者归来!强势要求解散董事会,OpenAI终极宫斗一触即发

      【新智元导读】 董事会打脸了!Altman众望所归上演王者归来,戴着访客证出现在OpenAI总部,并且强势要求董事会解散。CEO的复仇之路反转再反转,双方目前仍在对峙。 从被扫地出门到王者回归,乔布斯用了12年,而Sam Altman,仅仅用了两天。 现在,A...
      继续阅读 »
      【新智元导读】 董事会打脸了!Altman众望所归上演王者归来,戴着访客证出现在OpenAI总部,并且强势要求董事会解散。CEO的复仇之路反转再反转,双方目前仍在对峙。

      从被扫地出门到王者回归,乔布斯用了12年,而Sam Altman,仅仅用了两天。


      现在,Altman已经以胜利者的姿态重返OpenAI探讨自己的去留问题,并且对董事会提出了新的要求——


      「更换现有的董事会成员,并且得到证明自己并无过错的声明。」


      简单来说就是,我可以回来,但你们得走。

      图片临时CEO Mira Murati、首席战略官Jason Kwon、首席运营官Brad Lightcap,都站在了Altman这一边,希望董事会辞职。董事们让步了,原则上同意辞职,但还未正式执行,正在评估新董事的人选。截止发稿时,双方还在僵持中。但Altman,应该是已经掌握了主动权。


      「王者回归」之路



      当地时间周日,六小时之前,Sam Altman po出自己佩戴OpenAI访客证进入大楼的照片,皱着眉、眼神复杂地望向镜头,同时打下这样一句话——



      这是我第一次,也是最后一次,戴上OpenAI的访客证。



      图片而在Altman被离职的同时也一起辞职的OpenAI总裁Greg Brockman,也和Altman一起与OpenAI展开了谈判。上周六,四人董事会将Altman无情踢出之后,又在周日反悔了,跪求Altman重返OpenAI。原因一方面是金主爸爸们给董事会的压力,另一方面,则是大量员工的追随和支持。董事会是让步了,但Altman却未必会接受了。现在,他手头的选择很多,如果回OpenAI,他就要求重新设立新董事会;或者,他甚至可以带着大批愿意离职的前员工,直接另起炉灶创立新公司。从这里也能看出,真正让Altman不可替代的,是OpenAI顶级科学家对他的无限忠诚。他们,才是OpenAI的中流砥柱,也是ChatGPT的核心贡献者。


      金主之怒


      据彭博社报道,微软CEO纳德拉对于董事会的行为非常愤怒。据悉,他在事件爆发后一直和Altman保持着联系,并且保证会支持他。要知道,微软是OpenAI最大的投资者,投入了130亿美元,拥有OpenAI Global LLC 49%的股份。图片与此同时,OpenAI最主要的风投支持者们,包括其第二大股东Thrive Capital、Tiger Global、Khosla Ventures以及Sequoia Capital,都表示希望Altman回归。而且,无论Altman接下来要做什么,他们都会给予支持。图片这不由得让人想起硅谷的另一起著名事件——众所周知,史蒂夫·乔布斯在1985年的时候被自己亲手创立的苹果解雇。随后,他创立了NeXT,一家生产高端计算机的公司。而彼时的苹果,已经风雨飘摇。1997年,乔布斯正式回归。很快,他就把苹果从一个苦苦挣扎的科技公司转变为一个全球巨头。图片


      员工纷纷表态


      另一创始人、OpenAI总裁及董事会主席Greg Brockman,在第一时间辞职,坚决表示自己和Altman同进退。图片随着事情的发酵,Altman发推表示:我太爱OpenAI团队了。图片同时,大量OpenAI的核心员工和高管,都转发了Altman的推特,纷纷po出爱心,表示支持。图片这些OpenAI核心员工对于Altman的支持,似乎在告诉董事会,开了他,OpenAI很有可能面临大量的员工流失。图片而这些人,正是OpenAI能够走到今天,成为科技圈最受瞩目,甚至能够改变科技行业未来的公司的原因。图片为了安抚员工,OpenAI和Altman展开复职谈判之后,OpenAI高管在一份发给员工的备忘录中称,他们对Altman和Brockman的回归「非常乐观」。


      董事会被架到火上烤


      现在,Altman复职谈判最大的障碍是,他希望能够解散炒掉他的董事会,并引入新的董事会成员。对此,原董事会很有可能不得不重新发表一个声明,推翻原本炒掉Altman的声明,为Altman平反。这样的话,他们不但把自己架到了火上烤,还让所有人都有理由对董事会的「合法性」提出质疑。图片根据外媒报道,如果董事会真的重组,新加入董事会的成员可能会包括:Salesforce Inc.前联席首席执行官Bret Taylor。图片以及另一位来自微软的高管。而推动前董事会裁掉Altman的OpenAI首席科学家Ilya,能否继续留在董事会之中,就不得而知了。毕竟,矛盾的地方在于,不久前的开发者大会已经充分昭示了Altman的商业野心。而董事会成员,尤其以Ilya为主,则对AI的安全性产生了担忧。对此,马斯克也不忘趁此时机倒油,表达对Ilya的支持,同时也就间接表达了对Altman的质疑。



      我十分担心。Ilya有良好的道德观,他并不是一个追求权力的人。除非他认为绝对必要,否则他绝不会采取如此激进的行动。



      图片


      虽然,董事会还在犹豫,但如果Altman真要决定创办新公司,必定有一大批员工会忠心追随。


      太长不看版


      总结一下就是,在过去短短几天内,OpenAI就发生了一系列惊天大动荡——



      • OpenAI发布公告,宣布解除Sam Altman CEO和Greg Brockman董事会主席的职务。



      • CTO Mira Murati被任命为临时CEO。



      • 很快,Brockman也发帖表示已经辞职。



      • Altman和Brockman发表联合声明,对董事会的做法表示「震惊和悲痛」。


      图片



      • 三位高级研究员Jakob Pachocki、Aleksander Madry和Szymon Sidor,纷纷辞职表示抗议。



      • 据了解,首席科学家Ilya Sutskever在解雇Altman的过程中发挥了关键作用。



      • 第二天,OpenAI似乎迫于压力,又想让Altman回归CEO职位。



      • 对此,Altman提出了自己的条件,包括要求解雇他的董事会成员辞职。而董事会则犹豫不决。



      • 有媒体报道称,如果未能达成协议,将会有大批员工辞职。


      Altman有意成立新的AI公司



      据知情人士透露,Altman正计划成立一家新的人工智能企业。而OpenAI前总裁Greg Brockman有望加入该公司。不过,目前我们还不清楚这家企业的具体情况。图片


      与此同时,关于Altman在开发人工智能方面的雄心壮志和更多细节也已浮出水面。不久前,他与包括芯片设计公司Arm在内的半导体高管进行了讨论,商讨如何尽早设计出新的芯片,为OpenAI这样的大语言模型公司降低成本。


      图片


      据彭博社报道,Altman计划创办的这家芯片公司,将会打造类似于谷歌TPU(张量处理单元)的人工智能芯片。为此,Altman一直在中东为这个代号为Tigris的项目筹集资金。


      TPU等定制设计的芯片被认为有朝一日有可能超越英伟达制造的人工智能加速器。人工智能公司都对其梦寐以求,但开发一款AI芯片需要巨量的时间和资源。


      图片


      不过公司尚未成立,与投资者的谈判也还处于早期阶段。但无论新公司采取何种形式,Altman和Brockman都可以避免重复目前在OpenAI遇到的问题。除了这家芯片公司,Altman还一直在为他与苹果公司前设计总监Jony Ive合作开发的一款人工智能硬件设备筹集资金。


      图片


      据一位知情人士透露,最近几个月,Altman定期参加孙正义在加州Woodside豪宅举行的晚宴,与微软首席执行官Satya Nadella等其他科技高管讨论人工智能、芯片和其他科技话题的未来。或许,正是Altman的野心和副业,使他与董事会本已紧张的关系变得更加复杂。


      竞争对手急于挖角


      这边Altman前脚刚走,Cohere和Adept等竞争对手已经开始在OpenAI挖人了,而谷歌DeepMind也收到了来自OpenAI员工的新简历。


      这些举动表明了Altman下台后OpenAI面临的风险:四位高管的离职有可能引发一连串的辞职潮,使其难以维持去年的高速发展。随着Altman考虑回归,许多高管也在考虑回归。


      图片


      Adept是一家估值10亿美元的初创公司,它正在创建一个人工智能模型,可以在用户的电脑上为他们完成任务。


      该公司的代表在Altman被解雇后24小时内,联系了OpenAI的多名现任工程师和研究人员。


      一位知情人士表示,OpenAI的一些员工在董事会发表声明解雇Altman后的几个小时内,向谷歌的人工智能实验室DeepMind提交了简历。OpenAI的主要创业公司竞争对手在LinkedIn上发布了一则招聘信息,称其正在招聘多名技术项目经理。


      图片


      而Cohere公司的联合创始人兼首席执行官Aidan Gomez则在公司的招聘页面上发布了一个链接,指出该公司正在招聘「机器学习技术人员」。


      图片


      代码生成初创公司Replit的创始人兼首席执行官Amjad Masad也同样在OpenAI宣布领导层变动三小时后发布了公司招聘页面的链接。


      图片


      ——看起来像是落井下石?不过这种相互挖角的事情对于OpenAI等公司可能也习以为常。OpenAI成立于2015年,目前有700多名员工,其中一些人就是从谷歌、Meta和Stripe等大型科技公司挖来的。去年他们高调聘用的一些员工包括特斯拉自动驾驶汽车前主管Andrej Karpathy和Stripe隐私与数据保护前主管EmmaRedmond。据知情人士透露,OpenAI最近通过提供数百万美元的股票套餐来吸引谷歌的员工。毫不夸张地说,Altman在从其他公司招募人才方面发挥了关键作用。


      网友吃瓜整活乐开了花


      微软在幕后发大力了,现在看看OpenAI谁说了算。图片Altman亮出了自己的最后王牌,「权力转换卡」!图片相同的姿势,相同的结局,只是Altman效率高太多了。图片和一年前马老板收购推特后相似的场景再次上演!图片


      参考资料:


      time.com/6337449/ope…


      http://www.theinformation.com/articles/al…


      http://www.theinformation.com/articles/op…


      http://www.theverge.com/2023/11/19/…


      http://www.theverge.com/2023/11/19/…


      作者:新智元
      来源:juejin.cn/post/7303423871708987427
      收起阅读 »

      无感刷新,我想说说这三种方案

      web
      现在当你想去找一个无感刷新的方案的时候,搜出来的大多都是教你在aioxs的相应拦截器里面去截取当前请求的config。然后当token刷新后再去请求失败的接口。首先声明,这个方案完全没有任何问题。只是有可以优化的地方,这个优化的地方可以在我将要写的第二种方案中...
      继续阅读 »

      现在当你想去找一个无感刷新的方案的时候,搜出来的大多都是教你在aioxs的相应拦截器里面去截取当前请求的config。然后当token刷新后再去请求失败的接口。首先声明,这个方案完全没有任何问题。只是有可以优化的地方,这个优化的地方可以在我将要写的第二种方案中得到解决。


      准备工作


      接口服务


      在实行方案之前需要准备好相关的接口服务,我会用node写一些登录刷新和正常的业务接口, 点这里查看


      简单介绍下准备的接口及其作用



      • /login: 模拟登录并返回tokenrefreshToken

      • /refreshToken: 当token过期,请求这个接口会获得新的tokenrefreshToken,接口需要传入通过/login接口或者/refreshToken接口返回的refreshToken。当refreshToken也判断过期,就只能去登陆页。

      • /test1: 模拟正常的业务请求接口,会验证token是否有效及是否过期,过期返回401

      • /test2: 模拟正常的业务请求接口,会验证token是否有效及是否过期,过期返回401

      • /test3: 模拟正常的业务请求接口,会验证token是否有效及是否过期,过期返回401


      token的过期时间设置为5秒。


      axios的封装


      我们使用axios都会进行二次封装,都会在拦截器里面处理一些逻辑。这里给出一个最基本的封装,后面都会用到。


      import axios from "axios";

      export const service = axios.create({
        timeout: 1000 * 30,
        baseURL: "http://192.168.0.102:9001",
      });

      service.interceptors.request.use(
        (config) => {
          let token = localStorage.getItem("token");
          config.headers["Authorization"] = token;
          return config;
        },
        (err) => Promise.reject(err)
      );

      service.interceptors.response.use(
        (res) => {
          return res.data;
        },
        (err) => {
          return Promise.reject(err);
        }
      );

      export const get = (url, params) => {
        return new Promise((resolve, reject) => {
          service
            .get(url, { params })
            .then((res) => {
              resolve(res);
            })
            .catch((err) => {
              reject(err);
            });
        });
      };

      export const post = (url, data = {}) => {
        return new Promise((resolve, reject) => {
          service
            .post(url, data)
            .then((res) => {
              resolve(res);
            })
            .catch((err) => {
              reject(err);
            });
        });
      };

      首先还是介绍下最广泛的使用方案


      在axios的响应拦截器里面处理


      这种方案的工作流程都是在相应拦截器里面处理的,当判断到接口401,则请求刷新token接口,并改变一个状态来表明当前正在执行刷新token,这样可以避免多个请求同时401的时候所导致的一次401就会请求一次刷新token接口。接着将紧随着报401的接口保存起来,等到刷新token接口成功后再去执行这些失败的接口。完整代码,在上文的二次封装的axios基础上进行更改。


      import axios from "axios";
      ----------------------------------------------------------------新增
      import { refreshToken } from "@/api/login";
      ----------------------------------------------------------------新增

      export const service = axios.create({
        timeout: 1000 * 30,
        baseURL: "http://192.168.0.102:9001",
      });

      service.interceptors.request.use(
        (config) => {
          let token = localStorage.getItem("token");
          config.headers["Authorization"] = token;
          return config;
        },
        (err) => Promise.reject(err)
      );

      ----------------------------------------------------------------新增
      let inRefreshing = false; // 当前是否正在请求刷新token
      let wating = []; // 报401的接口 加入等待列表 刷新接口成功后统一请求
      ----------------------------------------------------------------新增

      service.interceptors.response.use(
        (res) => {
          return res.data;
        },
        (err) => {
      ----------------------------------------------------------------新增
          let { config } = err.response;

          if (inRefreshing) { // 刷新token正在请求,把其他的接口加入等待数组
            return new Promise((resolve) => {
              wating.push({
                config,
                resolve,
              });
            });
          }

          if (err?.response?.status === 401) {
            inRefreshing = true;

            refreshToken({ refreshToken: localStorage.getItem("refreshToken") }).then(
              (res) => {
                const { success, response } = res;
                if (success) {
                  inRefreshing = false;

                  const { token, refreshToken } = response;
                  localStorage.setItem("token", token);
                  localStorage.setItem("refreshToken", refreshToken);

      // 刷新token请求成功,等待数据的失败接口重新发起请求
                  wating.map(({ config, resolve }) => {
                    resolve(service(config));
                  });
                  wating = []; // 请求完之后清空等待请求的数组

                  return service(config); // 当前接口重新发起请求
                } else {
                  // 刷新token失败  重新登录
                }
              }
            );
          }
      ----------------------------------------------------------------新增
          return Promise.reject(err);
        }
      );

      export const get = (url, params) => {
        return new Promise((resolve, reject) => {
          service
            .get(url, { params })
            .then((res) => {
              resolve(res);
            })
            .catch((err) => {
              reject(err);
            });
        });
      };

      export const post = (url, data = {}) => {
        return new Promise((resolve, reject) => {
          service
            .post(url, data)
            .then((res) => {
              resolve(res);
            })
            .catch((err) => {
              reject(err);
            });
        });
      };

      有些同学可能对上面代码有些疑问,感觉wating里面的数据不对,比如同时发送三个接口报401,wating只会加入两条接口数据,还有一条在哪儿。其实就在第60行返回了。


      然后再对接口进行简单的封装


      import { post } from "./index";

      export const login = () => post("/login");
      export const refreshToken = (data) => post("/refreshToken", data);

      export const test1 = () => post("/test1");
      export const test2 = () => post("/test2");
      export const test3 = () => post("/test3");

      接下来看结果。点击按钮会发送三条接口请求。

      第一次点击发送请求token还未过期,第二次点击发送请求token已过期。
      1.gif


      完美,没啥问题!因为上面给大家展示的是最简单的axios封装,所以在响应拦截器里面加上一些接口重发的逻辑好像没啥问题。但是如果还有对接口数据的加密解密过程呢?还有控制加载的时候loading窗完美展示的问题呢?这样一看在二次封装的axios里面加上接口重发就有点儿太多了。再比如说我用的不是axios怎么办,是不是又得根据请求的插件去改一些东西。


      那么与axios拦截器解耦就成了需要做的事了。


      与axios拦截器解耦


      我们需要创建一个构造函数,这个构造函数会将报401的接口收集起来,等到刷新token接口成功后再去请求


      import { refreshToken } from "@/api/login";

      function requestCollection() {
        let inRefreshing = false; // 当前是否正在请求刷新token
        let wating = []; // 报401的接口 加入等待列表 刷新接口成功后统一请求

        return (request) => {
          return new Promise((resolve) =>
            request()
              .then((res) => {
                resolve(res);
              })
              .catch(async (err) => {
                if (err.response.status == 401) {
                  if (inRefreshing) {
                    // 加入等待执行的数组
                    return wating.push({
                      request,
                      resolve,
                    });
                  }

                  inRefreshing = true;

                  await RT();

                  wating.map(({ resolve, request }) => {
                    resolve(request());
                  });
                  wating = [];

                  return resolve(request());
                }
              })
          );
        };
      }

      const RT = () => {
        return new Promise((resolve) =>
          refreshToken({
            refreshToken: localStorage.getItem("refreshToken"),
          }).then((res) => {
            const { success, response } = res;
            if (success) {
              const { token, refreshToken } = response;
              localStorage.setItem("token", token);
              localStorage.setItem("refreshToken", refreshToken);

              resolve();
            }
          })
        );
      };

      export default requestCollection;

      其实内在的处理逻辑与第一种大同小异,主要在于将这一部分逻辑抽离出来。


      使用


      import { post } from "./index2";
      import requestCollection from "./RequestCollection";

      const check = new requestCollection();

      export const login = () => check(() => post("/login"));
      export const refreshToken = (data) => check(() => post("/refreshToken", data));

      export const test1 = () => check(() => post("/test1"));
      export const test2 = () => check(() => post("/test2"));
      export const test3 = () => check(() => post("/test3"));

      效果演示


      2.gif


      这种方案存在的一个小问题
      因为需要对接口进行一层包裹,所以如果你的项目已经运行有一段时间了突然来个需求说想要个无感刷新token,那其实这个方案会随着你的项目的大小而加大你的工作量。


      上述两种方案都存在的不足
      当我们都在讨论无感刷新token的方案都是如何接口重发的时候,大家都忽略了一个问题,那就是接口重发的过程是发送了原本两倍+1的接口数量。如果这个接口本来就慢,恰好碰上了401,那用户就得花约两倍时间去等一个结果,相信你不愿意等,我也不愿意等。


      那么被大家诟病会造成一定性能浪费的定时任务刷新就可以解决这个问题,并且定时任务刷新也是解耦于二次封装的axios。 其实都到了2023年,一个定时任务会对最终用户所使用的页面造成卡顿的影响可以说完全感知不到。但是我也不推荐大家无意识的随便使用定时器或者闭包一类能通过一点点累加所造成的内存泄露的性能问题。


      定时任务刷新


      这个就不给代码了,主要注意几个点就行



      • 后端需要配合返回一个token的过期时间

      • 定时任务全局存在

      • 刷新token成功后需要更新过期时间重新计算


      总结


      其实上面三种方案都有自己的好处和缺点,无论你使用哪一种方案,都没有问题。这些需要你结合自己的项目来选择适合你的方案。


      作者:谁是克里斯
      来源:juejin.cn/post/7302404170412802074
      收起阅读 »

      为什么我不建议中小企业使用 TypeScript

      web
      此博客内容,包含【极端的个人主观因素、极端的个人主观因素、极端的个人主观因素】,如不喜欢,请轻喷...... 不知道从什么时候开始,前端开发者中出现了一种 唯 TS 至上论 的思想。 如果你的项目中使用的 是JavaScript 而 不是TypeScript,...
      继续阅读 »

      此博客内容,包含【极端的个人主观因素、极端的个人主观因素、极端的个人主观因素】,如不喜欢,请轻喷......


      不知道从什么时候开始,前端开发者中出现了一种 唯 TS 至上论 的思想。


      如果你的项目中使用的 是JavaScript不是TypeScript,那么就会被打上 很low 的标签。同时也会被立刻质疑:“你为什么没有使用 TS?”


      我为什么一定要使用 TypeScript 呢?


      TypeScript 真的有那么的完美,值得我们在任何的场景下都优先使用吗? 恐怕不是的


      任何的一门技术都是一把双刃剑,它在带来一定优势的同时,必然也会带来一定的不便性。


      所以,咱们今天就来聊一聊:“为什么我不建议中小企业使用 TypeScript!”


      01:不要让 TS 沦为 个人KPI


      有很多中小企业的 “技术Leader”,本身并没有对 TS 进行过深入的了解。只不过是因为老板的一句:“我听说人家现在都在用 TS 啦?” 而强行在团队中推行 TS 。完全不考虑团队目前的技术方向以及团队的加班时长,这是 不可取的


      所以 不要让 TS 沦为 个人KPI


      当你想要在团队中推行 TS 时,你应该首先评估团队中是否有人使用过 TS,研究下大家学习 TS 所需要花费的时间。如果团队中都不熟悉 TS ,并且你也没有令人信服的理由,就不要强制团队使用 TS 啦。


      02:大多数的中小企业很难适应它


      说真的,“你(中小企业)花了多少钱招人,你自己心里没点 B 数吗?” 怎么着?还真打算拿着买“粉条”的钱去买“鱼翅”不成?


      面试的时候,想尽办法的压薪资。工作的时候,又期望大家为你发挥出远超TA当前薪资的能力,好处都让你占了呗?


      所以,别那么天真了!你的开发人员现阶段真的很难适应 TS。


      如果,你真的想要在团队中推行 TS,并且希望它可以为你带来好的结果。


      那么 请先培训你的团队!


      拿出一定的时间和金钱,来提升你团队的技术能力和技术深度。为他们提供学习 TS 所需要的时间和课程,询问团队的意见,正确的评估你们团队的技能组合。否则你的 自私 决定,只会损害你们团队的利益,最终也会损害到你自己的利益。


      03:容易出现 “伪 TS”


      所有以 .ts 结尾的文件,都是使用了 TypeScript 的。


      由此,项目中就有可能会出现大量的 “伪 TS”。也就是:“以 .ts 结尾的文件,但是内容都是 js。”


      这样的项目,除了可以让你们老板拿出去“吹牛逼”之外,对技术个人是毫无意义的。



      当然,让老板可以拿出去 “吹牛逼” 对很多 “所谓的 Leader” 而言,就是TA们价值的体现



      04:更容易出现屎山


      其实 “伪 TS” 还好,因为它毕竟 不需要 我们花费更多的 时间和头发 来了解它的心路历程。


      而比 “伪 TS” 更可怕的是:屎山一样的 TS 代码。


      相信我,一旦 TS 屎山起来,那个味道要比 JS 重的多!


      不知道大家有没有接手过一些 “所谓的 TS 项目”。我有幸在多个 训练营的同学 那里见到过很多次。


      过度的类型声明、过度的类型封装,以及那些明明被定义但是 “好像” 从来都没有使用过的属性们。偏偏你还不敢动它们。就问你晕不晕。


      将来当你想要去定义一个属性时,发现好像已经有了一个类似的,但是你又不确定的时候怎么办呢?最安心的办法就是 “再创建一个”。


      所以,我曾经有幸在一个接口中见到了这样的代码(以下为伪代码):


      interface User {
      name: string;
      name2: string;
      username: string;
      username2: any;
      }


      就问你刺激不刺激。


      通常情况下,当我们遇到这样的代码时,根据 “尽量遵守前人代码习惯” 的规范下,很快这里就可能会出现 name3、name4 以及 name-next...


      总结


      任何的一门技术都是一把双刃剑,它在带来一定优势的同时,必然也会带来一定的不便性。


      所以,我们真的没有必要去跟风追逐所谓的强类型。适合自己团队的,才是最好的!



      这是第二次发了,虽然我啥也没改......



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

      Git 提交竟然还能这么用?

      大家好,我是鱼皮。Git 是主流的代码版本控制系统,是团队协作开发中必不可少的工具。 这篇文章,主要是给大家分享 Git 的核心功能 提交(Commit)的作用,帮助大家更好地利用 Git 这一工具来提高自己的开发工作效率。 什么是 Git 提交? Git 提...
      继续阅读 »

      大家好,我是鱼皮。Git 是主流的代码版本控制系统,是团队协作开发中必不可少的工具。


      这篇文章,主要是给大家分享 Git 的核心功能 提交(Commit)的作用,帮助大家更好地利用 Git 这一工具来提高自己的开发工作效率。


      什么是 Git 提交?


      Git 提交是指将你的代码保存到 Git 本地存储库,就像用 Word 写长篇论文时进行保存文件一样。每次 Git 提交时都会创建一个唯一的版本,除了记录本次新增或发生修改的代码外,还可以包含提交信息,来概括自己这次提交的改动内容。


      如下图,就是一次 Git 提交:



      Git 提交的作用


      Git 提交有很多作用,我将它分为 基础用法其他妙用


      基本作用


      历史记录


      Git 提交最基本的作用就是维护项目的历史记录。每次提交都会记录代码库的状态,包括文件的添加、修改和删除;还包括一些提交信息,比如提交时间、描述等。这使得我们可以通过查看所有的历史提交来追溯项目的开发进度和历程,了解每个提交中都发生了什么变化。


      比如查看我们编程导航文档网站项目的提交记录,能看到我是怎么一步一步构建出这个文档网站的:



      开源地址:github.com/liyupi/code…




      在企业开发中,如果一个人写了 Bug,还死不承认,那么就可以搬出 Git 提交记录,每一行代码是谁提交的都能很快地查出来,谨防甩锅!


      版本控制


      另一个 Git 提交的基本作用是版本控制。每个提交都代表了代码库的一个版本,这意味着开发者可以随时切换代码版本进行开发,恢复旧版本的代码、或者撤销某次提交的代码改动。


      推荐新手使用可视化工具而不是 Git 命令进行版本的切换和撤销提交,在不了解 Git 工作机制的情况下使用命令操作很容易出现问题。


      如下图,在 JetBrains 系列开发工具中,右键某个提交,就可以切换版本或撤销提交了:



      代码对比


      你可以轻松地查看两个提交之间的所有代码更改,便于快速了解哪些部分发生了变化。这对于解决代码冲突、查找错误或审查代码非常有帮助。


      在 JetBrains 系列开发工具中,只需要选中 2 个提交,然后点右键,选择 Compare Versions 就能实现代码对比了:



      改动了哪些代码一目了然:



      一般情况下,如果我们因为某次代码改动导致项目出现了新的 Bug。通过这种方式对比本次改动的所有代码,很快就能发现 Bug 出现的原因了。


      其他妙用


      除了基本作用外,Git 提交还有一些妙用~


      记录信息


      像上面提到的,Git 提交不仅能用于记录代码更改,我们还可以在提交信息中包含有关这次更改的重要信息。比如本次改动代码的介绍、代码更改的原因、相关的任务(需求单)或功能等。可以简单理解为给本次工作写总结和描述。


      如果提交信息编写得非常清晰完善,那么项目的团队成员可以更容易地理解每个提交,甚至能做到 “提交即文档”,提高协作和项目维护效率。


      正因如此,很多团队会定制自己的提交信息规范,比如之前我在鹅厂的时候,每次提交都建议带上需求单的地址,便于了解这次提交是为了完成什么需求。


      这里给大家推荐一种很常用的提交信息规范 —— 约定式提交,每次提交信息都需要遵循以下的结构:



      《约定式提交》文档:http://www.conventionalcommits.org/zh-hans/v1.…



      <类型>[可选 范围]: <描述>

      [可选 正文]

      [可选 脚注]


      当然,这种方式有利有弊,可能有同学会觉得 “我注释都懒得写,你还让我写提交信息?” 这取决于你们项目的规模和紧急程度等因素,反正团队内部保持一致就好。


      像我在用 Git 开发个人项目时,也不是每次都写很详细的提交信息的。但是带 编程导航 的同学从 0 开发项目时,每场直播写的代码都会单独作为一次提交,如下图:



      是不是很清晰呢?这样做的好处是,大家想获取某场直播对应的中间代码(而不是最终的成品代码)时,只需要点击某次提交记录就可以获取到了,很方便。



      如果你的提交信息写得非常标准、统一结构,那么甚至还可以用程序自动读取所有的提交信息,生成日志、或者输出提交报告。


      自动化构建部署


      大厂研发流程中,一般都是使用 CI / CD(持续集成和持续部署)平台,以流水线的形式自动构建部署项目的。


      Git 提交可以和 CI / CD 平台进行集成,比如自动监视代码库中的提交,并在每次提交后自动触发构建和部署任务。一个典型的使用场景是,每次代码开发完成后,先提交代码到测试分支,然后 CI / CD 平台监测到本次提交,并立即在测试环境中构建和部署,而不需要人工操作,从而提交效率。


      GitHub Actions 和 GitHub Webhooks 都可以实现上述功能,感兴趣的同学可以尝试下。



      GitHub Actions 文档教程:docs.github.com/zh/actions/…




      检验项目真假


      最后这个点就比较独特了,那就是面试官可以通过查看 Git 的提交记录来判断你的项目真假、是不是自己做的。


      比如我收到一些同学的简历中,有的开源项目看起来感觉很厉害,但是点进仓库看了下提交记录,发现寥寥无几,甚至有的只有 1 次!像下图这样:



      那么这个项目真的是他自己从 0 开始做的么?答案就显而易见了。


      如果真的是你自己用心做的项目,提交记录绝对不止 1 次,而且面试官能够通过提交记录很清晰地了解到你的项目开发周期。


      像我的 yuindex Web 终端项目一样,这才是比较真实、有说服力的:



      其他人也能从你的提交记录中,感受到你对项目的用心程度。


      讲到这里,是不是有些同学恍然大悟,知道为啥自己的项目明明开源了,但是没有收到面试邀请、或者被面试官觉得项目不真实了?


      实践


      以上就是本次分享,Git 提交的实践其实非常简单,我建议大家每次做新项目时,无论大小,都用 Git 来托管你的项目,并且每开发完一个功能或解决 Bug,都进行一次提交。等项目完成后回过头来看这些提交记录,都是自己宝贵的财富。


      作者:程序员鱼皮
      来源:juejin.cn/post/7303349108845920306
      收起阅读 »

      大型 APP 的性能优化思路

      做客户端开发都基本都做过性能优化,比如提升自己所负责的业务的速度或流畅性,优化内存占用等等。但是大部分开发者所做的性能优化可能都是针对中小型 APP 的,大型 APP 的性能优化经验并不会太多,毕竟大型 APP 就只有那么几个,什么是大型 APP 呢?以飞书来...
      继续阅读 »

      做客户端开发都基本都做过性能优化,比如提升自己所负责的业务的速度或流畅性,优化内存占用等等。但是大部分开发者所做的性能优化可能都是针对中小型 APP 的,大型 APP 的性能优化经验并不会太多,毕竟大型 APP 就只有那么几个,什么是大型 APP 呢?以飞书来说,他的业务有 im,邮箱,日历,小程序,文档,视频会议……等等,包体积就有大几百 M,像这种业务非常多且复杂的 APP 都可以认为是大型 APP。所以我在这篇文章主要讲一下大型 APP 是怎么做性能优化的,给大家在性能优化一块提供一个新的视角和启发。在这篇文章中,我主要会讲一下这两个主题:




      1. 大型 app 相比于中小型 app 在做性能优化时的异同点




      2. 大型 app 性能优化的思路




      大型和小型应用性能优化的异同点


      1.1 相同点


      性能优化在本质上以及在优化维度上都是一样的。性能优化的本质是合理且充分的使用硬件资源,让程序表现的更好;并且都需要基于应用层、系统层、硬件层三个维度来进行优化


      whiteboard_exported_image.png


      1.2 不同点


      针对系统层和硬件层的优化都是一样,有区别的主要是针对应用层的优化。


      中小型 app 做业务和做性能优化的往往是同一个人,在做优化的时候,只需要考虑单个业务最优即可,我们只需要给这些业务充足的硬件资源(比如给更多的内存资源:缓存更多的数据,给更多的 cpu 资源:用更多的线程来执行业务逻辑,给更多的磁盘资源:缓存足够的本地数据),并且合理的使用就能让业务表现的更好。只要这些单个的业务性能表现好,那么这款 app 的整体性能品质是不错


      whiteboard_exported_image-2.png


      和中小型 APP 不同的是,大型 APP 业务多且复杂,各个业务的团队很可能都不在一个部门,不在同一个城市。在这种情况下,如果每个业务也去追求自己业务的性能最优,同样是在自己的业务中使用更多的线程,使用更多的缓存,使用更多 cpu 的方式来使自己业务表现更好,那么就会导致 APP 整体的性能急剧劣化。因此大型 APP 需要有一个专门团队来做性能优化的,这个团队需要脱离某一个具体业务,站在全局的视角来让 APP 整体表现更优。


      whiteboard_exported_image-3.png


      大型应用性能优化方案


      总的来说由于资源是有限的,在中小型 APP 上,业务少,资源往往是充足的,我们做性能优化时往往考虑的是怎么将资源充分的发挥出来,而在大型 APP 上,资源往往是不足的,做性能优化时需要考虑即能充分发挥硬件资源,又需要进行合理的分配,当我们站在全局的视角来进行资源分配时,就需要考虑到这三个点:



      1. 如何管控业务对资源的使用

      2. 如何度量业务对资源的消耗

      3. 如何让业务在资源紧张时做出更优的策略


      下面我会针对速度及流畅性优化、内存优化这两个方向,讲一讲针对这这三点的体现。


      2.1 速度和流畅性优化:如何管控业务对资源的使用


      在速度和流畅性方向,中小型 APP 只需要分析和优化主路径的耗时逻辑;将同步任务尽量优化成异步任务;多进行预加载等方案,即能起很好的优化效果。但是对于大型 APP 来说,异步任务往往非常多,cpu 往往都是打满的情况,这种情况下主路径得不到足够的 cpu 资源,导致速度变慢。所以大型 app 中一般都会对业务的异步任务,如启动阶段的预加载进行管控,因此需要预加载框架或者类似的框架,来收敛、管控、以及调度所有业务的预加载任务。我们来看一下在大型 APP 中,通用的预加载框架是怎么做的。


      2.1.1 预加载框架


      想要管控业务的预加载任务,我们需要考虑这两个点:




      1. 预加载任务的添加方式




      2. 预加载任务调度和管理的机制




      3. 预加载任务的添加方式




      首先要将业务的预加载任务和业务进行解耦,要能做到即使该预加载任务不执行,也不会影响到业务的正常使用,并且将预加载任务封装成粒度最小的 task,然后直接将这些 task 丢到到预加载框架中,我们可以通过单例提供一个 addPreloadTask 方法,业务方只需要调用该接口,并传入预加载任务 task 以及一些属性及配置参数即可。将预加载任务添加到预加载框架后,业务方就不需要进行任何其他操作了,是否执行、什么时候执行,都交给预加载框架来管理。


      whiteboard_exported_image-4.png




      1. 预加载任务调度时机




      那么预加载框架对于添加进来的 task 如何调度呢?这就是一个预加载框架复杂的的地方的,我们可以有很多策略,比如这三种:



      1. 关键节点调度策略:比如各个生命周期阶段,页面渲染完成阶段等去执行,也可以在任务添加进来后立刻执行。

      2. 性能调度策略:比如判断 cpu 是否忙碌,温度是否过高,内存是否充足等,只有在性能较好的情况下才进行调度

      3. 用户行为调度策略:如果做的更复杂一些,还可以结合用户的行为指标,如该业务用户是否会使用,如果某一个用户从来不适用这个 app 里的这个功能,那么改业务添加进来的预加载任务就可以完全舍弃到,这里面可以用一些端智能的方案来精细化的控制预加载任务的调度


      每种调度策略不是单独执行的,我们可以将各种策略整合起来,形成一套完善的调度策略。


      whiteboard_exported_image-5.png


      2.2 速度和流畅性优化:如何让业务在资源紧张时做出更优的策略


      上面提到的是站在全局的视角,如何管控预加载任务的,除了预加载任务,还有很多其他的异步任务我们都可以用一些框架来规范化的管控起来,这里再举一个例子,对于大型 APP 来说,业务在使用的过程中很容易出现因为 cpu 或内存不足导致卡顿,响应慢等性能问题,所以在做性能优化时,是需要推动业务方在资源不足时,做出相应策略的,这个时候我们就需要降级框架来处理了。降级框架需要解决这两个问题:



      1. 性能指标的采集

      2. 降级任务的调度


      2.2.1 降级框架




      1. 性能指标的采集




      想要再资源紧张时让业务做出优化策略,那么对资源紧张的判断就是必不可少的一步。我们一般通过在程序运行过程中,采集设备性能指标来判断资源是否紧张,最基本的性能指标有 cpu 使用率,温度,Java 内存,机型等,除机型外其他性能指标一般都是以固定的频率进行采集,如 cpu 使用率可以 10s 采集一次,温度可以 30s 采集一次,java 内存可以 1 分钟采集一次,采集的频率需要考虑对性能的影响以及指标的敏感度,比如 cpu 的使用率采集,需要读取 proc/stat 的文件并解析,是有一定性能损耗的,所以我们在采集时,不能太频繁;温度的变化是比较慢的,我们采集的频率也可以长一些。降级框架需要整合这些性能指标的采集,减少各个业务自己采集造成不必要的性能损耗。


      当降级框架采集到性能指标,并判断当前资源异常时,通用的做法是通知各个业务,业务收到通知后再进行降级。比如系统的 lowmemorykiller 机制,都是采用通知的方式。


      whiteboard_exported_image-6.png


      但是在大型 APP 中,仅仅将触发性能阈值的通知给到各个业务方,效果不会太好,因为业务方可能并不会去响应通知,或者个别业务响应了,但是其他业务不响应,依然效果不佳。无法管控业务是否进行降级,这显然不符合在大型 APP 做性能优化的思路,那么我们要怎么做呢?




      1. 降级任务的调度




      添加任务:我们依然可以推动各个业务将降级的逻辑封装在 task 中,并且注册到降级框架中,并由降级框架来进行调度和管理。因为往降级框架注册 task 时,需要带上业务的名称,所以我们能也能清楚的知道,那些业务有降级处理逻辑,哪些业务没有,对于没有注册的业务,需要专门推动进行降级响应以及 task 的注册。


      调度任务:和预加载框架一样,对于注册进来的 task,降级框架的任务调度要考虑清楚调度的时机,以 cpu 使用率为例,不同的设备下的阈值也是不一样的,高端机型可能 cpu 的使用率在 70%以上,app 还是流畅的,但是低端机在 50%以上就开始卡顿了,因此不同的机型需要根据经验值或者线上数据设置一个合理的阈值。当 cpu 到达这个阈值时,降级框架便开始执行注册到 cpu 列表中的降级任务,在执行降级任务时,不需要将队列里的 task 全部执行,我们可以分批执行,如果执行到某一批降级 task 时,cpu 恢复到阈值以下了,后面的降级 task 就可以不用在执行了。可以看到,通过降级框架,我们就可以站在全局的维度,去进行更好的管控,比如我们可以度量业务做降级任务的效果,给到一个评分,对于效果不好的,可以推动优化。


      whiteboard_exported_image-7.png


      2.3 内存优化:如何度量业务对资源的消耗


      上面两个例子将的是在大型 app 中,如何管控业务对资源的使用,以及如何让业务在资源紧张时做出更优的策略的思路,我接着基于内存优化的方向,讲一讲如何度量业务对资源的消耗。


      当 app 运行过程中,往往只能获得整体的内存的数据占用,没法获的各个业务消耗了多少内存的,因为各个业务的数据都是放在同一个堆中的,对于小型 app 来说这种情况并不是问题,因为就那么几个业务在使用内存,但是对于大型 app 来说就是一个问题了,有些业务为了自己性能指标能更好,会占用更多的内存,导致整体的内存占用过高。所以我们需要弄清每个业务到底使用了多少内存才能推动业务进行优化。


      whiteboard_exported_image-10.png


      我们可以线下通过分析 hprof 文件或者其他调试的方式来弄清楚每个 app 的内存占用,但是很多时候没有充足的时间在版本都去统计一下,或者即使统计了,也可能因为路径没覆盖全导致数据不准确。所以我们最好能通过线上监控的方式,就能统计到业务的内存消耗,并且在内存消耗异常的时候进行上报。


      我在这里介绍一种思路。大部分的业务都是以 activity 呈现的,所以我们可以监听全局的 activity 创建,在业务的 onCreate 最前面统计一下 java 和 native 内存的大小,作为这个业务启动时的基准内存。然后在 acitvity 运行过程中,固定采集在当前 activity 下的内存并减去 onCreate 时的基准内存,我们就能度量出当前业务的一个内存消耗情况了。在该 acitvity 结束后,我们可以主动触发一下 gc,然后在和前面的基准内存 diff 一下,也能统计出该业务结束后的增量内存,理想情况下,增量内存应该是要小于零的,由于 gc 需要 cpu 资源,所以我们只需要开取小部分的采样率即可。


      whiteboard_exported_image-8.png


      当我们能在运行过程中,统计各个业务的内存消耗,那么就可以推动内存消耗高的业务进行优化,或者当某个版本的某个业务出现较大的劣化时,触发报警等。


      除了上面提到的思路,我们也可以统计在业务使用过程中的触顶次数,计算出一个触顶率的指标,触顶及 java 内存占用达到一个阈值,比如 80%,我们就可以认为触顶了,对于触顶次数高的业务,同样也可以进行异常上报,然后推动业务方进行修改。这些数据和指标的统计,都是无侵入的,所以并不需要我们了解业务的细节。


      如果我们想做的更细一些,还可以 hook 图片的创建,hook 集合的 add,remove 等方法,当监控到大图片和大集合时,打印堆栈,并将关键信息上报。在飞书,如果是低端机中,图片如果占用内存过大的,都会在 hook 方法中进行一些压缩或者降低质量的兜底处理。


      总结


      除了速度及流畅性,内存方向的优化外,还有其他方向的优化,如包体积,稳定性,功耗等,在大型 APP 上都要基于管控业务对资源的使用;度量业务对资源的消耗;让业务在资源紧张时做出更优的策略这三个方向去进行优化,这里我就不再一一展开讲了。


      whiteboard_exported_image-9.png


      当然我这里讲的优化思路并不是大型 app 做性能优化的全部,我讲的只是在做大型 app 的性能时相比于中小型 app 需要额外做的,并且也是效果最好的优化,这些方案在中小型 app 上可能并不需要。除了我这篇文章讲的内容外,还有很多优化的方案,这些方案不管是在大型 app 还是中小型 app 上都是通用的,比如深入了解业务,基于业务逻辑去做分析和优化,抓 trace,分析 trace 等等,或者基于系统层或者硬件层去做一些优化等等,这里就不再展开讲了。


      作者:helson赵子健
      来源:juejin.cn/post/7302740437529853963
      收起阅读 »

      鸿蒙开发,对于前端开发来说,究竟是福是祸呢?

      提前声明: 先说好哈,不要一上来就开喷,好吧,不感兴趣的话你可以不用看下文直接划走,直接喷俺有点承受不住,心脏不好。如果你感兴趣,你可以先把这篇文章看完,看完后感觉俺讲的还挺有道理的那就不喷,如果讲的你认为啥也不是,那就往死里喷,喷不动了俺也加入。 唠叨唠叨 ...
      继续阅读 »

      提前声明: 先说好哈,不要一上来就开喷,好吧,不感兴趣的话你可以不用看下文直接划走,直接喷俺有点承受不住,心脏不好。如果你感兴趣,你可以先把这篇文章看完,看完后感觉俺讲的还挺有道理的那就不喷,如果讲的你认为啥也不是,那就往死里喷,喷不动了俺也加入。


      唠叨唠叨


      最近,鸿蒙开发的风头也吹到俺这里了,于是我便上网看了看,就以俺的知识面去聊一聊鸿蒙,究竟是个啥,有啥用呢。


      在此之前,咱们可以先看个视频来大致了解一下鸿蒙系统究竟是干啥的,有啥好处:鸿蒙的官方定义哔哩哔哩bilibili(该视频为黑马的课程视频,原视频没暂时没找到,可跳到 03:46~12:1713:27~19:35 两个时间段)。


      如果你看了这个视频的话,相信你对鸿蒙也有了一定的了解了。


      为啥我想说鸿蒙呢



      最近一段时间,总是有人在说一些(俺认为哈,别人我就管不着了哈)有些莫名其妙的话术:什么前端以死呀、鸿蒙就是个安卓套壳呀、前端的春天要来了呀、等等之类的。是真的死了吗,俺不这样认为,只是技术门槛提高了而已,毕竟市场他是活的,人它也是活的,是活的话他就有变的时候,你的技术不变,不去进行升级的话,那就会被现有的市场所淘汰。优胜劣汰这个道理俺相信你们每个人都懂,只是有些人不想去面对而已,仅此而已。



      鸿蒙系统又是个啥


      俺简单来说哈,其实就一句话:鸿蒙系统是全场景 、面向未来、万物物联的


      如果这句话比较难理解,或者俺通过一张图让你更直观一点:


      Snipaste_2023-11-15_17-23-22.png


      如果你还是不理解的话,可以去华为官网看看官方对于鸿蒙系统的解释。


      那鸿蒙系统的特点有啥



      1. 统一OS,弹性部署


      一套操作系统,满足大大小小所有设备的需求,小到耳机,大到车机,智慧屏,手机等,让不同设备使用同一语言无缝沟通。



      1. 硬件互助,资源共享


      搭载 HarmonyOS 的每个设备都不是孤立的,在系统层让多终端融为一体,成为“超级终端”,终端之间能力互助共享,带来无缝协同体验。手机可以连接一切设备,可以将这些设备看作一个整体,如当手机需要操作自家的音响时,直接在手机上简单动一动手指头就行了,就像操作手机上的喇叭一样方便。



      1. 一次开发,多端部署


      开发者基于分布式应用框架,开发者只需要写一次逻辑代码,就可以部署在多种终端上,在多个设备上安装了。



      1. 应用自由跨端


      HarmonyOS 原子化服务是轻量化服务的新物种,它提供了全新的服务和交互方式,可分可合,可流转,支持免安装等特性,能够让应用化繁为简,让服务触手可及。



      • 咱们来以一个例子理解一下:


      假设咱们要用安卓操作系统去控制一台音响,这台音响有切歌功能、点歌功能、语音聊天功能,现在俺有点寂寞,需要音响陪我聊会天,俺只需要音响的语音聊天功能,但你必须要下载他的完整APP,并不能俺需要用啥功能就下载啥功能。而鸿蒙系统就可以做到。



      1. 用“简单”激活你的设备智能


      HarmonyOS 是新一代智能终端操作系统。为不同设备的智能化、互联与协同提供了统一的语言。设备可实现一碰入网,无屏变有屏,操作可视化,一键直达原厂服务等全新功能。通过简单而智能的服务,实现设备智能化产业升级。


      用安卓操作系统时,你需要下载设备对应的APP才能控制该设备,而鸿蒙操作系统,你直接将手机与设备上的芯片碰一碰,就可以直接通过手机来使用设备了。


      小提示: 俺家也没几个鸿蒙相关的设备,具体的俺也不是特别清楚,这些都是俺从网上了解到的。手机能连接上设备的前提是该设备的厂家与华为达成了合作才行吧(好像是这样的)。但俺用的是华为手机,路由器也是华为的,就这两个华为设备从俺的体验上来说哈,那还是不错的。


      可以与安卓做下对比



      1. 内核方面的对比


      安卓系统:


      是基于linux的宏内核设计 ,宏内核包含了操作系统绝大多数的功能和模块,而且这些功能和模块都具有最高的权限,只要一个模块出错,整个系统就会崩溃,这也是安卓系统容易崩溃的原因。好处就是系统开发难度低。


      鸿蒙系统:


      是微内核设计:微内核仅包括了操作系统必要的功能模块(任务管理、内存分配等)处在核心地位具有最高权限,其他模块不具有最高权限,也就是说其他模块出现问题,对于整个系统的运行是没有阻碍的。微内核稳定性很高。而且鸿蒙系统包含了两个内核,如果是手机APP是基于Linux内核,而如果是其他的硬件是基于LiteOS内核



      1. 运行速度的对比


      安卓系统:


      安卓程序不能与系统底层直接进行通信活动,是运行在虚拟机上的。如果虚拟机出了问题话的那系统就是卡住。


      鸿蒙系统:


      鸿蒙系统中的方舟编译器解决了这个问题的,任何由编译器编译的软件,是直接运行在操作系统中的,可以直接与系统底层进行通信。鸿蒙的运行速度更快



      1. 作为手机操作系统的对比


      安卓和鸿蒙都是基于安卓开源项目(AOSP)进行开发的。


      而安卓开源平台是可以在开源许可下自由使用和修改的。国内外很多手机厂商都根据这套代码制定了自己的操作系统,比如:三星、小米、魅族等。而华为也是基于这套开源体系,制定了鸿蒙操作系统。


      鸿蒙操作系统的构成:



      HarmonyOS = 安卓开放平台的开源代码 - GMS - 安卓UI + HMS + 鸿蒙UI + 分布式软总线 + 以Ability为核心的应用开发框架。




      1. 连接其他设备的对比


      安卓系统:


      安卓手机连接其他设备,不管从 app 开发方面,还有使用方面都非常麻烦,而且如果换一个第三方设备,还需要把发现,配对,连接,组合,验证的过程再次操作一遍。


      鸿蒙系统:


      但是鸿蒙就非常简单了,从 app 开发方面,只要写很少的代码就可以调用第三方硬件,从使用的角度来讲,不管是多少设备连在一起,鸿蒙的终极目标是都能像使用一台设备那样简单。


      那鸿蒙究竟是不是安卓的套壳呢



      网上有很多人说鸿蒙就是安卓的套壳,还用人说人家搞安卓开发的都是这样认为的。都不太看好鸿蒙,不要跟风,好吧。别人说是就是呀。你真的有去认真了解过吗。经过俺的一番捯饬后,俺大致的讲讲俺的理解哈。



      其实吧,为啥有这么多人说鸿蒙就是安卓的套壳呢,归根结底呀,是这两家的 “祖宗” 其实是一家人,也就是安卓和鸿蒙都是基于安卓开源项目 AOSP 进行开发的。而且 AOSP 里的代码,是全球很多开发者共同维护开发的,华为也是该代码的提供者之一,任何人都是可以在开源许可协议下去自由使用和二次修改的。而华为也是基于这套开源体系,制定了鸿蒙操作系统。这就是为啥都说鸿蒙是安卓的套壳的原因了。


      小提示: 可能会有人问俺 AOSP 又是啥东东,俺在网上找了一篇文章,你可以看看,了解一下:鸿蒙系统不是安卓系统?AOSP 为你揭秘! (baidu.com)


      所以呢,不是套壳、不是套壳、不是套壳重要的事说三遍哈。你要是还是那样认为那话,那俺只能说,我嘞个豆!!!


      就国家政策和市场形式


      其实从央视力挺华为就可以看出了,我国对鸿蒙系统还是相当重视的(网传,鸿蒙系统会上交给国家,俺也不知道是真是假)。


      就俺认为哈,代码这玩意都是老外搞出来的,一个操作系统能难倒他们,只是安卓和ios这两家独大,资历雄厚。可能有国外有好的操作系统出现,只是还没广为人知就已经被资本扼杀在摇篮里了。这又有谁知道呢。当然了这写只是俺的猜测而已。


      如果一个国家的操作系统多了,其实也不利于社会的管理和发展,国家一定会主推一个操作系统,然后其他系统为辅,从而形成 “百家争鸣” 的形式。


      另外哈,俺在招聘网上也查了看了一下,鸿蒙开发相关岗位的薪资大都与安卓开发平齐甚至有的还比安卓开发相关岗位的薪资要高得多(俺看到一家的鸿蒙开发的薪资,18~30K 16薪,说实话哈,俺是真的心动了)。


      声明一下: 以上有关的国家和社会的话术,都是俺自我认为的、理解的,请广大网友不必太纠结其对错,更不要上升到国家层面去给予评论和回复。谢谢!


      回归主题


      回归主题: 鸿蒙开发,对于前端开发来说,究竟是福是祸呢?


      看个人理解吧,俺认为哈,是福(俺已经开始学了)。就国内哈,如果明年华为推出的 HarmonyOS Next 将真的彻底抛弃 AOSP (华为的这个决定很大胆,这也是大部分的安卓开发者头痛的事,所以才会非常反感鸿蒙)。也就是说,明年,所有的安卓应用将不能在华为手机上使用,要想使用的话,就必须采用鸿蒙原生开发将应用改为鸿蒙应用程序。那你想想哈,我国有多少个应用,又有几个是用鸿蒙原生开发的或重构的,你再想想哈,这么多应用都要重构,那是不是这工作量非常之巨大,这么巨大的工作量,那公司是不是要招鸿蒙开发相关岗位了,薪资给少了你肯定不愿意去呀,那它公司又急需呀,那他的薪资待遇会不会被提高。那如果你会的话那你是不是就能上,那样的话害怕找不到工作。


      这就是相当于前端开发的一个红利期,而且这个红利期至少会持续两三年势头不会淡。其实俺说它是前端开发的春天的话也不为过,至少是在国内哈,国外俺就不知道啦。


      当然这还得等到明年华为推出的 HarmonyOS Next 是否真的彻底抛弃 AOSP ,如果是的话,那俺的认为就是对的。如果是假的话,那此上的一切都免谈,都是瞎扯淡。


      上手试试



      小提示哈: 如果你看完了上面的内容,你发现对鸿蒙开发产生了一定的好奇,你可以直接去官网注册个账号HarmonyOS应用开发官网 - 华为HarmonyOS打造全场景新服务,实名认证一下(俺建议采用银行卡的方式认证,这样通过认证更快),然后里面有在线的视频课程,它会带你具体了解如何开发鸿蒙原生应用。下面的内容你就可以忽略了。




      Snipaste_2023-11-15_18-26-04.png


      俺电脑上没装v16.19.1版本的node:俺用的是16.20.1的,不知道行不行,再装一个吧:


      Snipaste_2023-11-15_18-34-21.png


      路径与编译工具的安装地址是一致的:


      Snipaste_2023-11-15_18-37-10.png


      点击next,如果出现报红,选择第二个就可以了哈。


      Snipaste_2023-11-15_18-42-37.png


      创建个应用:


      Snipaste_2023-11-15_18-46-29.png


      Snipaste_2023-11-15_18-48-55.png


      创建第一个应用 FirstApp


      Snipaste_2023-11-15_18-55-34.png


      咻咻等待一下的啦,让项目配置一下资源。


      Snipaste_2023-11-15_19-04-17.png


      第一次运行会有上图的提示信息,将其 × 了就可以看到 Hello World 效果了


      Snipaste_2023-11-15_19-07-53.png


      小改一下:


      Snipaste_2023-11-15_19-09-37.png


      使用模拟器:


      Snipaste_2023-11-15_19-11-51.png


      Snipaste_2023-11-15_19-15-31.png


      登录后,选着P50机型模拟器调试:


      Snipaste_2023-11-15_19-42-35.png



      也不知道为啥,有时候就是无法用P50机型模拟器调试。后来俺还是用了本地模拟器。



      作者:doudou_sir
      来源:juejin.cn/post/7302254338855338003
      收起阅读 »

      高效案例检索工具,Alpha案例库智慧检索成为律师检索工具首选

      “工欲善其事,必先利其器。”当今,律界同仁需要权衡的问题早已不是“要不要”使用法律科技,而是如何高质量、高效率地使用法律科技工具。在业内人士看来,随着人工智能技术的不断发展,法律行业科技化将成为不可逆转的趋势。从目前国内律所引入科技工具的现状来看,Alpha法...
      继续阅读 »

      “工欲善其事,必先利其器。”当今,律界同仁需要权衡的问题早已不是“要不要”使用法律科技,而是如何高质量、高效率地使用法律科技工具。在业内人士看来,随着人工智能技术的不断发展,法律行业科技化将成为不可逆转的趋势。从目前国内律所引入科技工具的现状来看,Alpha法律智能操作系统具有较高的使用率,该系统在律师日常法律检索和律所管理方面都产生了巨大的“动能”。其官网链接:法律检索阿尔法

      最新的数据显示,业已有多达15W法律人长期使用Alpha。平台为满足众多律所的需求,更是做到了“天”级更新,每年斥资千万研发经费用以丰富系统的功能。当前,该工具的功能模块几乎覆盖到了法律工作的各个领域,被形象地称为律师工作的“得力干将”,特别是其中大数据赋能的“秒”级法律检索功能,更是极大地提升了律师的工作效率。


      法律检索阿尔法


      比如,在检索“单位分的房子,离婚时如何分割?”这一问题时,Alpha可以智能化地对“法院认为”板块中的“房改房”或“福利分房”关键词进行有效检索,在短短几秒钟的时间内就会得出相关案例,法规等相关内容。帮助律师们有效地缩小了检索范围,进一步提高了检索的精确度。此外,Alpha针对多个关键词之间“或”的逻辑关系,也可以通过深入的“思考”决定是否修改为“且”,用以满足某些案情的复杂性要求。


      法律检索阿尔法


      如果说Alpha的高级检索已经达到了全面的效果,那么其“智慧检索”功能则更是深入到“地毯式细致检索”的程度。目前,Alpha的“智慧检索”通过精准全面的标签设置,做到了检索的“主动化”,它能够依据技术与算法,科学设计标签的产生规则,从而为律师以“点选标签”的方式进行创新检索打下了坚实的基础。它分类细致、检索便捷,更重要的是它依据强大的算法,抓住了万变之“宗”,能够在纷繁复杂的社会生活中总结出各个领域的法律纠纷焦点问题,帮助律师快速提升理论运用的能力,在实务工作中长足进步。这一功能让众多的律师,尤其是经验不足的新手律师受益颇深。


      法律检索阿尔法


      在生成检索报告方面,Alpha也堪称是一把好手。针对日常的“案例检索”,律师可以直接下载文书列表,将所需文书的“法院认为”“裁判结果”等重要部分截取下来,用以快速浏览,并进行初步法律分析。若需要完整文书,也可以选择下载完整版。在自定义检索报告功能的助力下,律师们还可以设置报告封面、导出格式等,真正做到了“一气呵成”,一键生成。

      作为一款集法律全面检索、可视化分析、报告制作、文书模板等强悍功能于一体的工具,Alpha可以帮助法律人全方面、多维度、高效率地开展工作,以科技为律所发展插上有力的翅膀!

      Alpha系统官网:https://www.icourt.cc/product/alpha


      收起阅读 »

      半年120GC盘红盘,电脑救赎之旅

      工作开始,一直被程序猿=修电脑的,厌恶感十足,很可惜打脸来的太快,经历了公司换新电脑,坏了几次,再到工作变动自己搞了新电脑,直到红盘,被迫开启了自己的电脑救赎之旅,本次以这次被迫优化为例,主要想说的是搞程序的解决红盘的问题。 装系统     之前配备的电脑新拆...
      继续阅读 »

      工作开始,一直被程序猿=修电脑的,厌恶感十足,很可惜打脸来的太快,经历了公司换新电脑,坏了几次,再到工作变动自己搞了新电脑,直到红盘,被迫开启了自己的电脑救赎之旅,本次以这次被迫优化为例,主要想说的是搞程序的解决红盘的问题。


      装系统


          之前配备的电脑新拆就一直有莫名重启,移动电源无法使用的情况,随后到了彻底无法忍受的程度,你能想象,你正敲着代码毫无征兆就重启吗,还是一天10几次那种,好在常年随时保存的好习惯避免了重大事故,于是开始了反反复复的系统重装,直到确认是硬件问题,拿去报修,完了之后,当系统安装已经成为了本能,让我彻底的不再恐惧重装系统这个事儿,你们应该都懂,作为一名开发,系统重装这个事儿到底是有多恐惧、

          在这之后,由于工作变动的原因,又仓促的买了新电脑,安装已经驾轻就熟,大概是对纯净版,专业版系统的执着,微PE工具箱让我感觉到装系统和纯净版是一个让人愉悦的事儿,夹带私货的系统让人头皮发麻,我大概经过了这个几个阶段,
      系统安装->升级专业版->C盘分区扩容->提权administrator(普通用户对权限引起的bug和问题深度受害者)->驱动/开发工具安装,一顿操作搞下来,我的C盘大概控制在了40G以内,其他软件的安装严格的装在了指定盘符,结果半年多下来,我的120G的C盘坎坎到今天只剩下了5个G,可能这个就是全栈加全干带来的深度后果吧。


      C盘红盘过程


           像是npm 、cargo、vs、maven、Android Studio、python等工具C盘盘符产生缓存的问题我已经极度的进行了处理,但从120G-40G=80G的过程,你应该能想象我这大半年都经历了啥、找的c盘批处理完全解决不了我的困难,至于现有的C盘瘦身啥的软件要么收费要么夸大,感觉跟搞程序用的电脑不吻合,完全解决不了啥问题,没办法,我只能挨个的分析文件夹,定位这些内容产生自哪里。


      @echo off

      echo 正在清除系统垃圾文件,请稍等……

      del /f /s /q %systemdrive%\*.tmp

      del /f /s /q %systemdrive%\*._mp

      del /f /s /q %systemdrive%\*.log

      del /f /s /q %systemdrive%\*.gid

      del /f /s /q %systemdrive%\*.chk

      del /f /s /q %systemdrive%\*.old

      del /f /s /q %systemdrive%\recycled\*.*

      del /f /s /q %windir%\*.bak

      del /f /s /q %windir%\prefetch\*.*

      rd /s /q %windir%\temp & md %windir%\temp

      del /f /q %userprofile%\cookies\*.*

      del /f /q %userprofile%\recent\*.*

      del /f /s /q “%userprofile%\Local Settings\Temporary Internet Files\*.*”

      del /f /s /q “%userprofile%\Local Settings\Temp\*.*”

      del /f /s /q “%userprofile%\recent\*.*”

      echo 一键清理系统垃圾完成!

      echo. & pause

      优化前C盘只剩下5个G,没有先见之明,只能把优化后的结果贴图了,从D盘应该看得出端倪
      image.png


      在这个截图之前,C盘的用户数据大约是60多个G,20多G的优化空间也从此处来的
      image.png




      C盘救赎



      • C:\Users\Administrator\AppData\Local如果作为前端,大概率空间的占用是从这里开始的
        image.png

      • 如果作为服务端



      1. JAVA检查一下你的maven包缓存位置,我开始就设置了,因此没有占据C盘空间,很多都放在D盘了。

      2. c# 检查一下nuget的包缓存,可能会很大

      3. python的可以检查一下pip相关的包

      4. 总之现在的开发工具都很热衷于你的用户数据存储包,你如果分析一下c盘空间会有惊喜发现



      • 我之前也搜过很多教程,然并卵,很多压根没考虑过C盘用户数据造成的问题。


           整的比较简单,算是个程序猿群体避免重装系统的一个方案吧,求人不如求己,实在是在重装的边缘挣扎的老哥,可以尝试从这几个方面去做处理,尝试把相关的批处理删除加到以上的脚本中,做个自己的专清小工具,别被各种C盘瘦身垃圾软件折磨了。


      结束


      马上2023,还是在修电脑的过程中度过,为程序奋斗的一年画上句号、祝愿各位的电脑利器更加畅快随心,在新的一年里创造新的,专属于自己的程序传奇。
      ----------最后,新年快乐!!!


      作者:沈二到不行
      来源:juejin.cn/post/7183343983907569720
      收起阅读 »

      别把这些 Redis 操作写到生产环境

      软件工程师在开发前要提前注意规避对 Redis 性能有影响的操作,避免走“先污染后治理”的老路。如下是整理出来6条会导致 Redis 性能下降的原因,尽量避免这些操作出现在生产环境中。 1. 大键和大值 存储大键或大值可能会消耗更多的内存,并且在 Redis ...
      继续阅读 »

      软件工程师在开发前要提前注意规避对 Redis 性能有影响的操作,避免走“先污染后治理”的老路。如下是整理出来6条会导致 Redis 性能下降的原因,尽量避免这些操作出现在生产环境中。


      1. 大键和大值


      存储大键或大值可能会消耗更多的内存,并且在 Redis 进行网络和磁盘 I/O 操作时可能会增加延迟。


      创建一个大键和大值:


      SET bigkey "a".repeat(5242880)  # 创建一个5MB的大值

      2. 阻塞操作


      某些 Redis 命令,如 BLPOPBRPOPBRPOPLPUSH,可能会阻塞 Redis 进程。同样,Lua 脚本执行时间过长也可能导致阻塞。


      如下 BLPOP 操作会阻塞 Redis 直到有元素被推入列表或者超时:


      BLPOP mylist 0  # 0表示无限期等待

      3. 过期键的处理


      如果有大量的键同时过期,Redis 的性能可能会受到影响,因为 Redis 需要在后台清理这些过期的键。


      创建一个大量即将过期的键:


      for i in range(100000):
      EXPIRE key{i} 10 # 10秒后过期

      4. 持久化


      Redis 提供了两种持久化选项——RDB 和 AOF。RDB 是将当前进程数据生成快照保存的方式,而 AOF 是记录服务器收到的每一条写命令。频繁的持久化操作可能会增加磁盘 I/O 负载,从而影响性能。


      启用 AOF 持久化并配置为每次有数据修改都立即写入磁盘(可能会影响性能):


      CONFIG SET appendonly yes
      CONFIG SET appendfsync always

      5. 使用复杂度高的命令


      KEYSSMEMBERSHGETALL 这样的命令可能需要扫描整个集合,当数据集大时,它们可能会导致 Redis 暂时停止处理其他请求。


      KEYS 命令,它会扫描整个键空间:


      KEYS *

      6. 内存使用过高


      如果 Redis 服务器的内存使用接近或达到了其最大值,性能可能会受到影响。此外,如果你的数据集大于可用内存,那么操作系统可能会开始进行分页,这会大大降低 Redis 的性能。


      使用 INFO memory 命令可以查看 Redis 的内存使用情况:


      INFO memory

      作者:Light_Tree
      来源:juejin.cn/post/7248286946573205565
      收起阅读 »

      前端程序媛的编程之旅:第一卷,一年的萌新探索与成长

      写在前面: 不知不觉已工作一年有余,好像毕业后的时光是一匹发疯的马,用尽全身气力疯狂向前,人生前半段是缓慢成长,后半程却加速变老。我以为越长大越沉默,没想到我还是一以贯之的废话一大堆。他日再回首时,恐已不复如今的心境,生活是值得记录的,无论好坏呀~ 正文: 校...
      继续阅读 »

      写在前面:


      不知不觉已工作一年有余,好像毕业后的时光是一匹发疯的马,用尽全身气力疯狂向前,人生前半段是缓慢成长,后半程却加速变老。我以为越长大越沉默,没想到我还是一以贯之的废话一大堆。他日再回首时,恐已不复如今的心境,生活是值得记录的,无论好坏呀~


      正文:


      校招篇:面试官,你好


      校招的记忆已然有点模糊了,有点后悔之前怎么没有记录生活。但我记得每次面试时的自我介绍,开场白的第一句话就是面试官,你好。我校招那一年互联网寒冬还不太寒,至少我周围熟悉的同学基本都手握好多大厂offer,所以当时有点小沮丧,总觉得自己菜菜的。也曾想过先去北京工作,不过考虑到我的性格以及离家近,最终还是选择来到成都。其实从小到大我没有什么目标,只是跟着周围同学朋友的节奏学习与生活,不掉队一路走到了现在,我不知道我喜欢什么,只是明确的知道不接受什么,排除掉不接受的,其他范围内我都可。


      工作篇:我的前端初体验


      依稀记得入职第一天,leader来楼下门口接的我,然后参加早会自我介绍与相互介绍,从此就正式开启了我的搬砖之路。刚入职时有好多不懂的,不懂开会时说的许多缩略词(连PM、PRD、RD啥的都不知道),还不懂从哪里开始看项目代码,对于提交代码、联调、部署、Nginx配置等环节,我就如同面对迷雾,一片茫然。挺感谢leader和同事们的指导与帮助,不仅仅是工作上的,还有帮助我融入团队,因为刚毕业时的我有点轻微社恐,不敢也不好意思多说话(然而现在笑声最大的就是我哈哈哈哈),虽然社恐不过我也是爱笑的社恐,即使很多不会,每天也是笑嘻嘻的,后来年终述职时同事的评价,以及晋升的其中一条理由,都是说我心态好哈哈哈哈哈哈。虽然网上挺多骂我司的,但从我自己的角度来看,我司的应届生培养确实还行,会给你充足的时间学习与成长,长期有耐心不仅仅是空口号。入职3个月时,我每天的工作还是培训和学习,除了已定的学习计划,mentor也会问我还有啥想学的嘛,可以调整。


      当我害怕自己做不好时,同事:领导问啥都要说“好,能做到”,反正工作又不是造火箭,不难的,而且不会就问嘛。结果真的不难耶哈哈哈,人最大的恐惧是对未知的恐惧。当我出现了线上事故时,同事:这有啥,我以前还有更严重的,大家都是这么过来的。当我提前想工作跳槽时,同事:我是一年一跳,自从帮leader筛选简历后,才发现我还可以,你也要相信你是比较靠前的。还有好多诸如此类的对话,缓解了我的焦虑与迷茫,偷偷在这里谢谢大家呀。


      后来我才开始慢慢接触项目,从最开始修改一个小bug,到后来完成一个小模块的开发,再到现在可以独立负责一个项目的开发上线。与刚入职时相比,现在更大的感受可能是更从容(也可能是开始摆烂哈哈哈),不再害怕新的任务不会怎么办,不再担心我的提问在别人看来是否有点笨笨的,不再纠结晚上早10分钟走会不会不太好……原来技术方案评审、CodeReview、述职等都没有想象中的可怕,很多事经历后才发现不过如此,一切都没关系哒。


      生活篇:程序媛的日常


      来成都后,经历了第一次震感强烈的地震,经历了高温限电于是居家办公,也见证了从全民防疫到全面放开。新冠全面放开后,leader说:大家如果身体有不适的就请假,别硬撑着,这只是一份工作而已。其他同事基本请了将近2周的假,我申请了一周,其实我只发烧了一天就没啥症状了,中间还去了一趟都江堰玩。后来的二阳、三阳大家都波澜不惊了。


      程序员只是一份工作,并不是生活的全部,周围同事有沉迷于养花的,有每周都骑行一天的,也有喜欢跳舞的……至于我,喜欢美食,喜欢小裙子,喜欢一切可爱的东东(๑• . •๑)。成都真的有好多好吃的,至少很对我的胃口,来了以后胖了8斤多呜呜呜。


      还有一个绕不开的话题,可能是我一生最悲伤的事情之一,工作一年时我的奶奶去世了,我哭了好久好久,现在提起仍然很难过,个中辛酸就不在此赘述了。


      其实还经历了好多事,但有些事只适合收藏。凡所发生,必有来意,不管开心还是难过,都想记下那时的感受,等我老了再来看,当时开心的事我是否依然会笑,当初觉得悲痛万分的坎坷,是否又轻舟已过万重山了(^-^)


      最后:


      这篇博客是我工作总结系列的第一篇,我希望通过这个平台,记录我在编程之旅中的点点滴滴,记录代码之外的日常生活。如果通过分享我的经历和感受,或多或少能够帮助和鼓励更多的人,那就更好啦。


      我想以喜欢的史铁生的一段话结尾~~
      我一直要活到我能够历数前生,你能够与我一同笑看,所以死与你我从不相干。


      作者:正是江南好时节
      来源:juejin.cn/post/7302276173789954098
      收起阅读 »

      如何实现一个下班倒计时程序

      Hello伙伴们,好几天不见啦。最近也是晚上打球太累,加上一直在研究mybatis的多租户问题,简直是没有太多的精力了。正好周六的晚上有一点点的空隙,就是洗完澡之后,顿时觉得整个人轻松下来了。有伙伴跟我一样的感受吗? 话不多说,现在我们来开始今天的主题:《如何...
      继续阅读 »

      Hello伙伴们,好几天不见啦。最近也是晚上打球太累,加上一直在研究mybatis的多租户问题,简直是没有太多的精力了。正好周六的晚上有一点点的空隙,就是洗完澡之后,顿时觉得整个人轻松下来了。有伙伴跟我一样的感受吗?


      话不多说,现在我们来开始今天的主题:《如何实现一个桌面倒计时程序》。


      身为打工人,一定是想着下班的那一刻吧。就像我昨天和我的伙伴开玩笑说:一个月就盼望着发工资的那一天。shigen找到了一段程序来实现下班倒计时,一起来看看实现的效果吧:


      倒计时应用


      页面上动态的显示当前时间和剩余时间,假设shigen的文章要在今天的23点写完,那么我还剩2小时25分钟的准备时间。是不是挺神奇的,另外,还可以实现到点了自动关机。人走电脑关,看看老板还有什么理由让你去加班🤫🤫🤫。


      那就上今天的代码吧:


       # -*- encoding: utf-8 -*-
       __date__ = '2023/11/18 19:27:08'
       
       """
       距离下班时间倒计时
       """

       
       import time
       from tkinter import *
       
       
       def refresh_current_time():
           """刷新当前时间"""
           clock_time = time.strftime('%Y-%m-%d %H:%M:%S')
           curr_time.config(text=clock_time)
           curr_time.after(1000, refresh_current_time)
       
       
       def refresh_down_time():
           """刷新倒计时时间"""
           # 当前时间戳
           now_time = int(time.time())
           # 下班时间时分秒数据过滤
           work_hour_val = int(work_hour.get())
           if work_hour_val > 23:
               down_label.config(text='小时的区间为(00-23)')
               return
           work_minute_val = int(work_minute.get())
           if work_minute_val > 59:
               down_label.config(text='分钟的区间为(00-59)')
               return
           work_second_val = int(work_second.get())
           if work_second_val > 59:
               down_label.config(text='秒数的区间为(00-59)')
               return
           # 下班时间转为时间戳
           work_date = str(work_hour_val) + ':' + str(work_minute_val) + ':' + str(work_second_val)
           work_str_time = time.strftime('%Y-%m-%d ') + work_date
           time_array = time.strptime(work_str_time, "%Y-%m-%d %H:%M:%S")
           work_time = time.mktime(time_array)
           if now_time > work_time:
               down_label.config(text='已过下班时间')
               return
           # 距离下班时间剩余秒数
           diff_time = int(work_time - now_time)
           while diff_time > -1:
               # 获取倒计时-时分秒
               down_minute = diff_time // 60
               down_second = diff_time % 60
               down_hour = 0
               if down_minute > 60:
                   down_hour = down_minute // 60
                   down_minute = down_minute % 60
               # 刷新倒计时时间
               down_time = str(down_hour).zfill(2) + '时' + str(down_minute).zfill(2) + '分' + str(down_second).zfill(2) + '秒'
               down_label.config(text=down_time)
               tk_obj.update()
               time.sleep(1)
               if diff_time == 0:
                   # 倒计时结束
                   down_label.config(text='已到下班时间')
                   # 自动关机,定时一分钟关机,可以取消
                   # down_label.config(text='下一分钟将自动关机')
                   # os.system('shutdown -s -f -t 60')
                   break
               diff_time -= 1
       
       
       # 程序主入口
       if __name__ == "__main__":
           # 设置页面数据
           tk_obj = Tk()
           tk_obj.geometry('400x280')
           tk_obj.resizable(0, 0)
           tk_obj.config(bg='white')
           tk_obj.title('倒计时应用')
           Label(tk_obj, text='下班倒计时', font='宋体 20 bold', bg='white').pack()
           # 设置当前时间
           Label(tk_obj, font='宋体 15 bold', text='当前时间:', bg='white').place(x=50, y=60)
           curr_time = Label(tk_obj, font='宋体 15', text='', fg='gray25', bg='white')
           curr_time.place(x=160, y=60)
           refresh_current_time()
           # 设置下班时间
           Label(tk_obj, font='宋体 15 bold', text='下班时间:', bg='white').place(x=50, y=110)
           # 下班时间-小时
           work_hour = StringVar()
           Entry(tk_obj, textvariable=work_hour, width=2, font='宋体 12').place(x=160, y=115)
           work_hour.set('18')
           # 下班时间-分钟
           work_minute = StringVar()
           Entry(tk_obj, textvariable=work_minute, width=2, font='宋体 12').place(x=185, y=115)
           work_minute.set('00')
           # 下班时间-秒数
           work_second = StringVar()
           Entry(tk_obj, textvariable=work_second, width=2, font='宋体 12').place(x=210, y=115)
           work_second.set('00')
           # 设置剩余时间
           Label(tk_obj, font='宋体 15 bold', text='剩余时间:', bg='white').place(x=50, y=160)
           down_label = Label(tk_obj, font='宋体 23', text='', fg='gray25', bg='white')
           down_label.place(x=160, y=155)
           down_label.config(text='00时00分00秒')
           # 开始计时按钮
           Button(tk_obj, text='START', bd='5', command=refresh_down_time, bg='green', font='宋体 10 bold').place(x=150, y=220)
           tk_obj.mainloop()

      代码就是简简单单的204行,要实现到点自动关机的伙伴可以把63-64行的代码注释打开即可。


      那最后总结一下吧,为什么shigen会选取这个程序作为今天的分享呢?



      1. 跨平台。首先python是跨平台的,其次tkinter也是跨平台的,意味着在常见的操作系统都可以执行这个代码,实现倒计时的效果;

      2. 新思路。其实shigen之前也做了一个类似的桌面时钟效果,做的更加酷炫一点的话,其实可以当作屏保了;

      3. 小工具的改造。其实shigen的mac上也有很多的小工具,但是都是在命令行执行的,改在了GUI界面岂不是更加的nice和方便,也实现傻瓜式操作。

      作者:shigen01
      来源:juejin.cn/post/7302348032543899660
      收起阅读 »

      透过线上问题谈横向能力

      主题    昨天遇到了一个比较有意思的服务器问题,然而就这么发生了,有些异形问题在多方协作模式下发生,协调甩锅、“坐等靠”,证明等乱七八糟的内容太消磨热情,本次通过一个异常的问题排查分享,来谈谈横向能力的问题,谈谈看法 起因   事情的起因是这样,运维的堡垒机...
      继续阅读 »

      主题


         昨天遇到了一个比较有意思的服务器问题,然而就这么发生了,有些异形问题在多方协作模式下发生,协调甩锅、“坐等靠”,证明等乱七八糟的内容太消磨热情,本次通过一个异常的问题排查分享,来谈谈横向能力的问题,谈谈看法


      起因


        事情的起因是这样,运维的堡垒机访问发布一直未发生任何问题,但节后,C系统出现统一认证异常的情况,但其他集成系统均无任何问题,堡垒机同样,这个C系统是正常的、出现了一方正常其他方不正常的情况,一般规律,软件出现问题,这个情况会在某些场景下复现,出现异常情况.


      注: 这里补充一下,统一认证起到的作用是保证内部系统的统一,同样承担着接入外部系统以及接入外部认证逻辑的作用,而此时的异常系统为自身系统、因此才有了排查的前提
      认证的逻辑,基本上是通过,统一存储服务域下的localStorage,通过ifame隐藏打开指定页,通过postMessage 进行唯一交换进行的(唯一交换的好处是不用源头统一,不用处理乱七八糟的不一致问题)
      image.png


      经过


        基于以上前提,经过异常反馈的情况和代码的确认,剔除了自身逻辑的问题(之所以要确认一次,是为了核实),怀疑问题出自于服务器网络问题,了解到之前一直没有任何问题,C系统中间经过发布,而且都笃定之前没出现过此种情况,因此有了退版本的情况,但情况依然不正常,又找到了我这里,此时有了远程的可调试环境,此时观测到一个特殊标记,在域下明明有token的情况下,通过这种方式获取不到、又核实了其他系统均正常。


        此时的问题已基本定位到服务器上了,但如何在当前条件下快速的验证证明此项结论的正确性,是个问题(业主方服务器相关已经过沟通,绕进了鸡生蛋、蛋生鸡的循环种)。


         过程大略是经过nginx的重新代理访问,达到切换服务的目的,通过路径配置进行更改验证,一番操作下来,验证了外部认证逻辑的正确性,但又产生了跨域问题,原因是C系统的发布用的是绝对地址,配置有服务指定访问地址,源码替换,重新docker镜像发布之后,添加服务的nginx访问后,问题得到了解决,当然过程回溯很容易,中间也是各波折。


      如何证明


         说实话,这个问题,我也属于第一次遇到,怀疑可能是安全策略的问题导致的,两个服务器的连通性是没有任何问题的,期间怀疑过nginx配置问题,docker问题,就也不知道怎么证明服务器的什么因素影响到了无法有效获取localStorage,有知道的老哥,也还请赐教、


      回溯


        这个问题,现在分析来看,涉及到前端、后端、运维的三重交接处,nginx这个问题,经常被甩来甩去,均认为是运维的职责、而此项问题,运维也无法跟踪排查得出结论,试错的条件和成本较大(并非别人想不到),但问题需要解决,此时的情况下,最小化的验证和解决问题就需要有横向的能力去综合评判、某方面缺失,很容问题就变成三方证明,这个问题不是我的问题,结果显而易见,就跟死锁一样得不到解决,当然这只是很小的一方面、


      分析


        每个人的想法和态度,会随着不同阶段变化,在某一个情况下希望努力学习新的知识内容用以变化成薪资、某个阶段需要安稳,又可能在某个阶段变成需要休息和清闲,但这一切的选择,有个前提是事前的努力,运气和时代的洪流并不是一直都有,求而不得是常态,为啥都经历过高考,却想不明白,更高的分数才有更多的选择,未来也才有更多的机会、至于年龄大转行这个问题,这是个人姿态的问题,积极向上总是好的,丧着也不是不能过,各自容忍,毕竟过的都是自己的人生,不是他人的,姿态和态度总要有,那么,你的态度又是啥?


      作者:沈二到不行
      来源:juejin.cn/post/7288678134076162111
      收起阅读 »

      Android进阶宝典 -- App线上网络问题优化策略

      在我们App开发过程中,网络是必不可少的,几乎很难想到有哪些app是不需要网络传输的,所以网络问题一般都是线下难以复现,一旦到了用户手里就会碰到很多疑难杂症,所以对于网络的监控是必不可少的,针对用户常见的问题,我们在实际的项目中也需要添加优化策略。 1 网络的...
      继续阅读 »

      在我们App开发过程中,网络是必不可少的,几乎很难想到有哪些app是不需要网络传输的,所以网络问题一般都是线下难以复现,一旦到了用户手里就会碰到很多疑难杂症,所以对于网络的监控是必不可少的,针对用户常见的问题,我们在实际的项目中也需要添加优化策略。


      1 网络的基础优化


      对于一些主流的网络请求框架,像OkHttp、Retrofit等,其实就是对Http协议做了封装,我们在使用的时候常见的就是POST或者GET请求,如果我们是做客户端开发,知道这些基本的内容好像也可以写代码,但是真正碰到了线上网络问题,反而摸不到头脑,其实最大的问题还是对于网络方面的知识储备不足,所以文章的开始,我们先来点基础的网络知识。


      1.1 网络连接的类型


      其实对于网络的连接,我们常见的就是向服务端发起请求,服务端返回对应的响应,但是在同一时刻,只能有一个方向的数据传输,这种连接方式称为半双工通信。


      类型描述举例
      单工在通信过程中,数据只能由一方发送到另一方常见的例如UDP协议;Android广播
      半双工在通信过程中,数据可以由一方A发送到另一方B,也可以从一方B发送到另一方A,但是同一时刻只能存在一方的数据传输常见的例如Http协议
      全双工在任意时刻,都会存在A到B和B到A的双向数据传输常见的例如Socket协议,长连接通道

      所以在Http1.0协议时,还是半双工的协议,因为默认是关闭长连接的,如果需要支持长连接,那么就需要在http头中添加字段:“Connection:Keep-Alive”;在Http 1.1协议时,默认是开启了长连接,如果需要关闭长连接,那么需要添加http请求头字段:“Connection:close”.


      那么什么时候或者场景下,需要用到长连接呢?其实很简单,记住一点即可,如果业务场景中对于消息的即时性有要求时,就需要与服务端建立长连接,例如IM聊天,视频通话等场景。


      1.2 DNS解析


      如果伙伴们在项目中有对网络添加trace日志,除了net timeout这种超时错误,应该也看到过UnknowHostException这种异常,这是因为DNS解析失败,没有解析获取到服务器的ip地址。


      像我们在家的时候,手机或者电脑都会连接路由器的wifi,而路由器是能够设置dns服务器地址的,


      image.png


      但是如果设置错误,或者被攻击篡改,就会导致DNS解析失败,那么我们app的网络请求都会出现异常,所以针对这种情况,我们需要加上自己的DNS解析策略。


      首先我们先看一个例子,假设我们想要请求百度域名获取一个数据,例如:


      object HttpUtil {

      private const val BASE_URL = "https://www.baidu.comxx"

      fun initHttp() {
      val client = OkHttpClient.Builder()
      .build()
      Request.Builder()
      .url(BASE_URL)
      .build().also {

      kotlin.runCatching {
      client.newCall(it).execute()
      }.onFailure {
      Log.e("OkHttp", "initHttp: error $it ")
      }

      }
      }
      }

      很明显,百度的域名是错误的,所以在执行网络请求的时候就会报错:


      java.net.UnknownHostException: Unable to resolve host "www.baidu.comxx": No address associated with hostname

      所以一旦我们的域名被劫持修改,那么整个服务就会处于宕机的状态,用户体感就会很差,因此我们可以通过OkHttp提供的自定义DNS解析器来做一个小的优化。


      public interface Dns {
      /**
      * A DNS that uses {@link InetAddress#getAllByName} to ask the underlying operating system to
      * lookup IP addresses. Most custom {@link Dns} implementations should delegate to this instance.
      */

      Dns SYSTEM = hostname -> {
      if (hostname == null) throw new UnknownHostException("hostname == null");
      try {
      return Arrays.asList(InetAddress.getAllByName(hostname));
      } catch (NullPointerException e) {
      UnknownHostException unknownHostException =
      new UnknownHostException("Broken system behaviour for dns lookup of " + hostname);
      unknownHostException.initCause(e);
      throw unknownHostException;
      }
      };

      /**
      * Returns the IP addresses of {@code hostname}, in the order they will be attempted by OkHttp. If
      * a connection to an address fails, OkHttp will retry the connection with the next address until
      * either a connection is made, the set of IP addresses is exhausted, or a limit is exceeded.
      */

      List<InetAddress> lookup(String hostname) throws UnknownHostException;
      }

      我们看下源码,lookup方法相当于在做DNS寻址,一旦发生异常那么就会抛出UnknownHostException异常;同时内部还定义了一个SYSTEM方法,在这个方法中会通过系统提供的InetAddress类进行路由寻址,同样如果DNS解析失败,那么也会抛出UnknownHostException异常。


      所以我们分两步走,首先使用系统能力进行路由寻址,如果失败,那么再走自定义的策略。


      class MyDNS : Dns {


      override fun lookup(hostname: String): MutableList<InetAddress> {

      val result = mutableListOf<InetAddress>()
      var systemAddressList: MutableList<InetAddress>? = null
      //通过系统DNS解析
      kotlin.runCatching {
      systemAddressList = Dns.SYSTEM.lookup(hostname)
      }.onFailure {
      Log.e("MyDNS", "lookup: $it")
      }

      if (systemAddressList != null && systemAddressList!!.isNotEmpty()) {
      result.addAll(systemAddressList!!)
      } else {
      //系统DNS解析失败,走自定义路由
      result.add(InetAddress.getByName("www.baidu.com"))
      }

      return result
      }
      }

      这样在www.baidu.comxx 解析失败之后,就会使用www.baidu.com 域名替换,从而避免网络请求失败的问题。


      1.3 接口数据适配策略


      相信很多伙伴在和服务端调试接口的时候,经常会遇到这种情况:接口文档标明这个字段为int类型,结果返回的是字符串“”;或者在某些情况下,我需要服务端返回一个空数组,但是返回的是null,对于这种情况,我们在数据解析的时候,无论是使用Gson还是Moshi,都会解析失败,如果处理不得当,严重的会造成崩溃。


      所以针对这种数据格式不匹配的问题,我们可以对Gson简单做一些适配处理,例如List类型:


      class ListTypeAdapter : JsonDeserializer<List<*>> {
      override fun deserialize(
      json: JsonElement?,
      typeOfT: Type?,
      context: JsonDeserializationContext?
      )
      : List<*> {
      return try {
      if (json?.isJsonArray == true) {
      Gson().fromJson(json, typeOfT)
      } else {
      Collections.EMPTY_LIST
      }
      } catch (e: Exception) {
      //
      Collections.EMPTY_LIST
      }
      }
      }

      如果json是List数组类型数据,那么就正常将其转换为List数组;如果不是,那么就解析为空数组。


      class StringTypeAdapter : JsonDeserializer<String> {

      override fun deserialize(
      json: JsonElement?,
      typeOfT: Type?,
      context: JsonDeserializationContext?
      )
      : String {
      return try {
      if (json?.isJsonPrimitive == true) {
      Gson().fromJson(json, typeOfT)
      } else {
      ""
      }
      } catch (e: Exception) {
      ""
      }
      }
      }

      对于String类型字段,首先会判断是否为基础类型(String,Number,Boolean),如果是基础类型那么就正常转换即可。


      GsonBuilder()
      .registerTypeAdapter(Int::class.java, IntTypeAdapter())
      .registerTypeAdapter(String::class.java, StringTypeAdapter())
      .registerTypeAdapter(List::class.java, ListTypeAdapter())
      .create().also {
      GsonConverterFactory.create(it)
      }

      这样在创建GsonConverterFactory时,就可以使用我们的策略来进行数据适配,但是在测试环境下,我们不建议这样使用,因为无法发现服务端的问题,在上线之后为了规避线上问题可以使用此策略。


      2 HTTPS协议


      http协议与https协议的区别,就是多了一个“s”,可别小看这一个“s”,它能够保证http数据传输的可靠性,那么这个“s”是什么呢,就是SSL/TLS协议。


      image.png


      从上图中看,在进入TCP协议之前会先走SSL/TLS协议.


      2.1 对称加密和非对称加密


      既然Https能保证传输的可靠性,说明它对数据进行了加密,以往http协议数据的传输都是明文传输,数据极容易被窃取和冒充,因此后续优化中,对于数据进行了加密传输,才有了Https协议诞生。


      常见的加密手段有两种:对称加密和非对称加密。


      2.1.1 对称加密


      首先对称加密,从名字就能知道具体的原理,看下图:


      image.png


      对称加密和解密的密钥是一把钥匙,需要双方约定好,发送方通过秘钥加密数据,接收方使用同一把秘钥解密获取传递的数据。


      所以使用对称加密非常简单,解析数据很快,但是安全性比较差,因为双方需要约定同一个key,key的传输有被劫持的风险,而统一存储则同样存在被攻击的风险。


      所以针对这种情况,应运而生出现了非对称加密。


      2.1.2 非对称加密


      非对称加密会有两把钥匙:私钥 + 公钥,对于公钥任何人都可以知道,发送方可以使用公钥加密数据,而接收方可以用只有自己知道的私钥解密拿到数据。


      image.png


      那么既然公钥所有人都知道,那么能够通过公钥直接推算出私钥吗?答案是目前不可能,未来可能会,得看全世界的密码学高手或者黑客能否解决这个问题。


      总结一下两种加密方式的优缺点:


      加密类型优点缺点
      对称加密流程简单,解密速度快不安全,秘钥管理有风险
      非对称加密私钥只有自己知道流程繁琐,解密速度慢

      2.2 公钥的安全保障


      通过2.1小节对于非对称加密的介绍,虽然看起来安全性更高了一些,但是对于公钥的传递有点儿太理想化,我们看下面的场景。


      image.png


      如果公钥在传输的过程中被劫持,那么发送方拿到的是黑客的公钥,后续所有的数据传输都被劫持了,所以问题来了,如何保证发送方拿到的公钥一定是接收方的?


      举个简单的例子:我们在马路上捡到了一张银行卡,想把里面的钱取出来,那么银行柜台其实就是接收方,银行卡就是公钥,那么银行就会直接把钱给我们了吗?肯定不可以,要么需要身-份-证,要么需要密码,能够证明这个银行卡是我们自己的,所以公钥的安全性保证就是CA证书(可以理解为我们的身-份-证)。


      那么首先接收方需要办一张身-份-证,需要通过CA机构生成一个数字签名,具体生成的规则如下:


      image.png


      那么最终发送给接收方的就是如下一张数字证书,包含的内容有:数字签名 + 公钥 + 接收方的个人信息等。


      image.png


      那么发送方接收到数字证书之后,就会检查数字证书是否合法,检测方式如下:


      image.png


      如果不是办的假证,这种可能性几乎为0,因为想要伪造一个域名的数字签名,根本不可能,CA机构也不是吃干饭的,所以只要通过证书认证了,那么就能保证公钥的安全性。


      image.png


      2.3 Https的传输流程


      其实一个Https请求,中间包含了2次Http传输,假如我们请求http://www.baidu.com 具体流程如下:


      (1)客户端向服务端发起请求,要访问百度,那么此时与百度的服务器建立连接;


      (2)此时服务端有公钥和私钥,公钥可以发送给客户端,然后给客户端发送了一个SSL证书,其中包括:CA签名、公钥、百度的一些信息,详情可见2.2小节最后的图;


      (3)客户端在接收到SSL证书后,对CA签名解密,判断证书是否合法,如果不合法,那么就断开此次连接;如果合法,那么就生成一个随机数,作为数据对称加密的密钥,通过公钥加密发送到服务端。


      (4)服务端接收到了客户端加密数据后,通过私钥解密,拿到了对称加密的密钥,然后将百度相关数据通过对称加密秘钥加密,发送到客户端。


      (5)客户端通过解密拿到了服务端的数据,此次请求结束。


      其实Https请求并不是完全是非对称加密,而是集各家之所长,因为对称加密密钥传递有风险,因此前期通过非对称加密传递对称加密密钥,后续数据传递都是通过对称加密,提高了数据解析的效率。


      但是我们需要了解的是,Https保障的只是通信双方当事人的安全,像测试伙伴通过Charles抓包这种中间人攻击方式,还是会导致数据泄露的风险,因为通过伪造证书或者不受信任的CA就可以实现。


      作者:layz4android
      来源:juejin.cn/post/7276368438146924563
      收起阅读 »

      GPT 深夜变天,福利碎片逐渐出现

      11月6日首次 OpenAI 大会,是由 OpenAI 创始人 山姆.阿特曼 进行分享新版本 GPT-4.0 Turbo ,以及未来蓝图。 11 月 18 日凌晨,OpenAI 突然发布一则官方声明,宣布 Sam Altman 经过慎重的...
      继续阅读 »

      11月6日首次 OpenAI 大会,是由 OpenAI 创始人 山姆.阿特曼 进行分享新版本 GPT-4.0 Turbo ,以及未来蓝图。


      图片


      11 月 18 日凌晨,OpenAI 突然发布一则官方声明,宣布 Sam Altman 经过慎重的审查程序后离开公司,首席技术官 Mira Murati 作为临时 CEO 暂代工作。


      以下是官方声明的全文翻译:



      首席技术官 Mira Murati 被任命为临时 CEO,继续领导 OpenAI;Sam Altman 离开公司。


      寻找长期继任者的工作将持续进行。


      OpenAI 公司的董事会今天宣布:Sam Altman将辞去 CEO 职务并离开董事会。公司的首席技术官 Mira Murati 将担任临时CEO,立即生效。


      Mira 作为 OpenAI 领导团队的成员已有五年,她在 OpenAI 发展成为全球 AI 领导者的过程中发挥了关键作用。她具备独特的技能,对公司的价值观、运营和业务有深刻的理解,并已经领导公司的研究方向、产品和安全功能。鉴于她在公司各方面的长期任期和紧密参与,包括她在 AI 治理和政策方面的经验,董事会认为她非常适合担任这一职务,并在寻找未来的长期 CEO 的过程中预期实现无缝过渡。


      Altman 先生的离职是在董事会经过深思熟虑的审查过程后决定的,董事会认为他在与董事会的沟通中未能始终保持坦诚,从而妨碍了董事会履行职责的能力。董事会不再相信他有能力继续领导 OpenAI。


      董事会在一份声明中表示:「OpenAI 的设立旨在推进我们的使命:确保通用人工智能造福全人类。董事会仍然全力致力于实现这一使命。我们感谢 Sam 对 OpenAI 创立和发展的许多贡献。同时,我们认为在前进的过程中需要新的领导。作为公司研究、产品和安全功能的领导者,Mira 非常适合担任临时 CEO。我们对她在过渡期间领导 OpenAI 的能力充满信心。」


      OpenAI 董事会由 OpenAI 首席科学家 Ilya Sutskever、独立董事 Quora CEO Adam D'Angelo、科技企业家 Tasha McCauley 和乔治城安全和新兴技术中心的 Helen Toner 组成。


      作为这次过渡的一部分,Greg Brockman 将辞去董事会主席职务,但将继续在公司担任职务,向 CEO 汇报。


      OpenAI 成立于 2015 年,是一家非营利组织,其核心使命是确保通用人工智能造福全人类。2019 年,OpenAI 进行了重组,以确保公司在追求这一使命的同时,保留非营利组织的使命、治理和监督。董事会的多数成员是独立


      ,独立董事不持有 OpenAI 的股权。尽管公司经历了快速增长,但推进 OpenAI 的使命和维护其章程原则仍然是董事会的基本治理责任。





      图片


      虽然有些委婉,但不免让人感觉有在“宫变”的感觉。毕竟是一手创办的产品,近年的影响力还是非常大的,里面所蕴含的投资价值还是巨大的。


      不免会产生内部利益的分歧。


      虽然如此,最近几天也给我们放出了一些福利碎片。


      例如: ?model=gpt-4-gizmo 参数可以体验GPT4.0的BUG,虽然短暂后被修复,但也可以体验到他的功能的强大。


      另外一个就是,最近发现注册账号上已经方便了很多,不再需要手机号注册,仅仅只需要邮箱就可以。


      这里我也立马给我星球的伙伴进行了分享,解决了他们因账号问题带来的困惑。并且目前也有一些 GPT 的账号挂在商铺里出售作为一个变现,甚至是按月付费体验。


      图片


      我也给我的伙伴同事们注册了账号,趁着账号注册方便,小伙伴也可以体验一波,也不清楚是短时开放还是长期的,只能跟着走吧。


      GPT 注册地址:chat.openai.com/


      当然,注册需要“科学上网”,并且受一些 IP 限制,出现不受支持等等。


      图片


      作者:桑小榆呀
      来源:juejin.cn/post/7302338286768521268
      收起阅读 »

      为什么说https比http安全?

      web
      前言 在互联网时代,我们每天都在进行着与网络有关的活动,而网络安全问题也因此成为大家越来越关注的话题。http协议作为最常用的网络传输协议之一,其设计缺陷让黑客攻击变得更加容易。相比之下,https协议通过加密通信,能够更有效地保护用户隐私和数据安全。 本文将...
      继续阅读 »

      前言


      在互联网时代,我们每天都在进行着与网络有关的活动,而网络安全问题也因此成为大家越来越关注的话题。http协议作为最常用的网络传输协议之一,其设计缺陷让黑客攻击变得更加容易。相比之下,https协议通过加密通信,能够更有效地保护用户隐私和数据安全。


      本文将为您介绍什么是https,为什么它比http更安全,帮助您更好地了解网络安全问题。


      什么是https


      httpshttp的加强版(HTTP+SSL),因为http特性是明文传输,因此到每个传输的环节中数据都有可能被第三方篡改的可能,也就是我们所说是中间人攻击。为了数据的安全,提出了https这个方案


      但它不是一个新的协议,原理上是在httptcp层之间建立一个中间层(也叫安全层),在不像之前http一样,直接进行数据通信,这个中间层会对数据进行加密处理,将加密后的数据给TCPTCP再将数据包进行解密处理才能传给上游的http



      http是位于OSI网络模型中的应用层




      SSL(Secure Sockets Layer 安全套接字协议),及其继任者传输层安全(Transport Layer Security,TLS)是为网络通信提供安全及数据完整性的一种安全协议



      20230112112331


      在采用了SSL后,http就拥有了https的加密,证书,完整性保护功能。


      换句话说,安全性是由SSL来保证的


      SSL/TLS


      SSL 即 安全套接层(Secure Sockets Layer),在OSI模型中处于第5层。在1999SSL更名为TLS(传输层安全),正式标准化。


      TLS中使用了两种加密技术,分别是对称加密和非对称加密



      提到 TLS ,就要说下 OpenSSL ,它是一个开源密码学程序库和工具包,支持了所有公开的加密算法和协议,许多应用软件都适用它作为底层库来实现 TLS 功能,例如 Apache、Nginx等。



      加密技术


      对称加密 Symmetric Cryptography


      对称加密常见的加密算法有:DESAESIDEA


      这个很好理解,对称加密指的是加密和解密的方式都是使用同一把钥匙(密保)


      缺点:



      • 服务器端也会把密钥提供给对方进行解密,如果密钥传递的过程中被窃取那就没有加密的意义了


      非对称加密 Asymmetric Cryptography


      非对称加密常见的算法有:RSADSADH


      非对称加密会有两把解密的密钥分别是ABA加密后的数据包只能通过密钥B来解密,反之,B加密的数据包只能通过A来解密


      其中,A是公钥,B是私钥,这两把钥匙是不一样,公钥可以给任何人使用,私钥则必须保密。


      这样子做可以防止密钥被攻击者窃取后用来获取数据


      缺点:



      • 公钥是公开的,攻击者可以截获公钥后解密服务器发送过来的密钥

      • 公钥不包含服务器信息,使用这个方案无法确保服务器身份的合法性,存在中间人攻击风险

      • 使用非对称加密在数据加密解密过程需要消耗一定时间,降低了数据传输效率


      hash算法


      例如sha256sha1md5这些用来确定数据的完整性,是否有被篡改过,主要用来生成签名信息。


      混合加密


      HTTPS采用的是混合加密方案(即:对称加密和非对称加密的结合)


      非对称加密的安全性比较高,但是解密速度会比较慢。


      当数据进行第一次通信时,使用非对称加密算法(解决安全性问题)交互对称密钥,在这之后的每一次通信都采用对称加密来进行交互。这样子性能和安全也可以得到均衡。


      混合加密总用了4把钥匙



      • 非对称加密A+私钥B

      • 对称加密私钥C和私钥D



      内容传输时使用对称加密,证书验证阶段使用非对称加密



      HTTPS工作过程



      1. 客户端发起一个网络请求。

      2. 服务器将自己的信息以数字证书的方式给了客户端(证书里面含有密钥公钥,地址,证书颁发机构等信息),其中的公钥是用来加密信息的。

      3. 当客户端接收到这个信息之后,会验证证书的完整性。(当证书有效继续下一步,否则显示警告信息)

      4. 客户端生成一个对称密钥并用第二步中的证书公钥进行加密发送给服务器端,

      5. 服务器用私钥解密获取对此密钥。(也证明了服务器是私钥的持有者)

      6. 接下来的通话使用该密钥进行通讯。


      20230409202418


      HTTPS运行原理


      浏览器拿到证书后,会先读取issuer(发布机构),然后在操作系统中内置的受信任的发布机构中查找证书,是否匹配,如果没有找到证书,说明证书有问题,如果找到了,就会拿上级证书的公钥去解密本级证书,得到数字指纹hash,然后对本级证书进行数字摘要算法(证书中提供的指纹加密算法)计算结果,与解密得到的指纹对比。如果一样,说明证书没有被修改过。公钥可以放心使用,可以开始握手通信了。


      证书从哪里来



      • 在安装系统的时候,受信任的证书发布机构的数字证书就已经被微软安装在操作系统中


      20230409202800


      什么时候证书不可信



      • 证书不是权威CA颁发(一些企业贪图便宜使用盗版证书,没有经过CA认证,也就无法通过使用浏览器内置CA公钥进行验证)

      • 证书过期

      • 证书部署错误,例如证书和域名信息不匹配


      HTTPS优劣势


      优势



      • 提高Web数据安全性

      • 加密用户数据

      • 提高搜索引擎排序

      • 浏览器不会出现非“不安全”警告

      • 提高用户对站点的信赖

      • 增加了中间人攻击成本


      缺点



      • https协议在握手时耗时会大一些,影响整体加载速度

      • 客户端和服务器端会使用更大的性能来处理数据加解密

      • SSL证书需要支付一定的费用来获取

      • 也不是绝对的安全,当网站被攻击,服务器被劫持时,HTTPS起不到任何作用

      • SSL证书通常需要绑定IP,不能在同一IP上绑定多个域名,IPv4资源不可能支撑这个消耗


      关于数字证书认证


      结合了两种加密方式可以实现数据的加密传输,但安全性还远远不够


      如果攻击者采用了DNS劫持,将目标地址替换成攻击者指定的地址,之后服务器再伪造一份公钥和私钥,也能对数据进行处理,而客户端此时也不知道正在访问一个危险的服务器


      HTTPS在混合加密的基础上,再追加了数字证书认证步骤,目的就是为了让服务器证明自己的身份


      在传输过程中,服务器运营者需要向第三方认证机构(CACertificate Authority)获取授权,在认证通过之后CA会给服务器颁发数字证书


      这个证书的作用就是用来证明服务器身份,还有就是把公钥传递给客户端


      当客户端获取到数字证书后,会读取其明文内容,CA在对数字证书签名时会保存一个hash函数,这个函数是用来计算明文的内容得到数据A,然后用公钥解密明文内容得到数据B,再对这两份数据进行对比,如果一致就代表认证合法。


      为什么要使用https?


      它们之间有什么区别吗?


      通过上面的介绍,我们可以了解到http在传输过程是明文的,数据容易被黑客截取或者篡改,这会导致用户信息泄露,而https通过ssl进行通讯加密处理,就算被截取了,也无法解读数据


      另外,除了安全性方面,httpshttp还有以下区别:



      • 由于https需要对数据进行加解密,所以会增加服务器和客户端的消耗更多的性能资源来处理,同时也增加了响应速度

      • https需要申请证书和验证,http则不需要



      作者:_island
      来源:juejin.cn/post/7220619478979182648
      收起阅读 »

      语雀 P0 事故复盘,这 9 个字亮了!

      大家好,我是鱼皮。 最近语雀不是出了个号称 “载入史册” 的 P0 级事故嘛 —— 连续宕机 7 个多小时无法使用,作为一个大厂知名产品,这个修复速度属实让人无法理解。要命的是我们公司的知识库也是放在语雀上的,导致那天下午大家摸鱼很愉快。 很快,官方就发布了《...
      继续阅读 »

      大家好,我是鱼皮。


      最近语雀不是出了个号称 “载入史册” 的 P0 级事故嘛 —— 连续宕机 7 个多小时无法使用,作为一个大厂知名产品,这个修复速度属实让人无法理解。要命的是我们公司的知识库也是放在语雀上的,导致那天下午大家摸鱼很愉快。


      很快,官方就发布了《故障公告》。有一说一,这个公告写得还是挺不错的,时间线梳理的很清楚。而且起码没有把责任归咎于 “网络原因”,还以为又是某个地区的网线被挖断了呢。



      故障公告原文:mp.weixin.qq.com/s/WFLLU8R4b…



      也有同学看了的语雀故障公告文章,发现改进措施这一段中提到了 “可监控,可灰度,可回滚” 这 9 个字,我觉得这确实是全文的核心亮点了,把事故复盘总结地很精辟。



      但是这 9 个字到底是什么意思呢?鱼皮给大家解读一下。


      如何保证系统发布的稳定性?


      首先,这几点都是企业正式线上项目需要重点关注的能力,所以大家在校自学时一般是很少能接触到的。



      但如果你知道并实践过这些,前途不可限量啊!



      可监控


      可监控是指能够实时地收集和展示系统运行时的数据和指标,以便开发和运维同学可以及时发现系统问题、更快进行故障排查和性能调优。需要监控的信息可以包括系统性能指标(内存、CPU、带宽等)、业务日志、错误信息等。


      还有一个与之相关的术语 “可观测性”,就是指一个系统状态对开发维护者的透明程度。举个例子,我不需要每次打开服务器看日志或者用什么 jmap 命令分析 gc,而是直接通过一个面板整体查看系统的状态,甚至是自动提示问题和解决方案。


      AIOps 智能运维也是现在很流行的一种技术,用 AI 帮忙运维诊断系统,大大提高开发运维效率。


      可灰度


      指灰度发布能力(又叫金丝雀发布)。将系统的新版本全量部署给所有用户之前,先仅对一小部分用户进行试用。这样可以通过收集这部分用户的反馈和监控数据就能评估新版本的稳定性,并及时进行调整和修复,从而减少对全体用户的潜在风险。


      灰度发布又有很多策略。比如经典的按流量阶段性发布,先随机给 5% 的用户使用新版本,验证没问题后,再给 20%、50%、75% 的用户使用新版本逐渐放量,直到覆盖 100% 的用户。


      还有很多策略,列举几个常见的:


      1)按照用户的业务属性灰度,比如 VIP 用户先用、老用户先用。


      2)按人群灰度,比如特定地域、特定年龄、特定偏好、特定客户端的用户。


      3)按渠道灰度,比如通过某平台注册的用户先体验等等。


      灰度做的好,可以避免很多线上问题,及时控制影响。因此很多知名产品发布时都会采用灰度或者内测的策略,这也就是为什么有些同学能第一时间体验到微信新功能,有些同学却没有。


      可回滚


      就像 Git 版本控制系统回滚写错的代码一样,系统的版本也是可以回滚的。


      线上系统出现问题时,可以将已经部署的新版本回退到之前的稳定版本。这样做可以快速恢复系统,减少对用户的影响,并给开发同学足够的时间来排查和修复问题。而不是线上一直故障,每分钟都是损失。


      最后


      咱也不是阿里内部的同学,说实话我不相信阿里内部没有统一的监控平台、灰度发布和部署管理平台。估计是部门自治或者人员不规范的操作导致的吧。(毕竟一个实习生说不定就能干崩一家公司)


      总之,上面讲的这些特性都是为了在软件开发和发布过程中提高系统的稳定性、可靠性和可维护性。


      想要实践上面这几点其实也很简单,直接用微信云托管平台就好了。我之前直播时录制过一套微信云托管的实践教程,大家如果需要的话,可以评论 “需要教程” 让我看看大家对这方面的需求,有必要的话回头给大家发出来~


      作者:程序员鱼皮
      来源:juejin.cn/post/7302230973738893349
      收起阅读 »

      为什么算法复杂度分析,是学算法最核心的一步

      基本介绍 算法复杂度这个概念,是算法中比较重要的一个核心的点。假设你现在要去分辨一串代码写的好与坏,那么是不是就得需要有一个可以衡量的标准,而算法复杂度的分析,就是一把标准之尺,有了这把尺子,你就能分辨出那些写的糟糕的代码,同时你也知道了要怎样去优化这段代码。...
      继续阅读 »

      基本介绍


      算法复杂度这个概念,是算法中比较重要的一个核心的点。假设你现在要去分辨一串代码写的好与坏,那么是不是就得需要有一个可以衡量的标准,而算法复杂度的分析,就是一把标准之尺,有了这把尺子,你就能分辨出那些写的糟糕的代码,同时你也知道了要怎样去优化这段代码。


      而目前我们常用的分析法,也就是大O表示法


      常见复杂度


      我们看一下 下面的代码


         function fn(n) {
      let m = 0
      console.log(m)
      for (let i = 0; i <= n; i = ++ ) {
      m += i
      m--
      }
      }

      我们假设 一行代码的执行的消耗时间是 1run_time 那么以此推导上面代码执行的时间消耗是(3n + 2)run_time 那么用大O表示法就是O(3n + 2)。


      ps:本文中中次的概念对应 每行代码 而不是整个代码片段


      大O表示法,并不会具体分析出每行代码执行花费的时间,他是一个粗略的抽象的统计概念,主要是是表示的某段代码的,所消耗的(时间/空间)增长趋势


      O表示是 总耗时 和总次数的一个比值,可以简单理解为 每一次代码执行所需要花费的耗时,也就是 总时间/总次数 = 每次执行需要消耗的平均时长。


      那么刚刚的O(3n + 2) 其实就是 (3n + 2) * 每次代码需要消耗的平均时长,那么就可以得出一个公式 T(n) = O(代码执行的总次数)


      其中 T(n) 表示的是 整段代码执行需要的总耗时


      在大O表示法中,常数,低阶,系数,在表示的时候是可以直接忽略统计的的,那么最后实际表示的复杂度就是O(n) 了


      我们再来看下面的代码


       function fn(n) {
      let aa = 0
      let bb = 0

      for (let i = 0; i < n; ++i) {
      aa += i
      }

      for (let i = 0; i < n; ++i) {
      for (let k = 0; k < n; ++i) {
      bb += k
      }
      }

      }

      前两行代码 很好看出来 就是个2,第一个for循环的消耗是 2n 第二个for循环 消耗是n的二次方那么实际用大O 表示就是 O(2 + 2n + n²) 最后表示的时候取3块代码中增长趋势最大的也就是O(n²)


      O(logn) O(nlogn)


      理解了上面分析的内容之后,这两个 O(logn), O(nlogn) 复杂度就很容易去学会了


       function fn(n) {
      let m = 0
      for (let i = 0; i < n; i *= 2) {
      m++
      }
      }

      我们来假设 n 是8 那么 2³ 就是8 那么也就是 2的x次方就是 n 那么用大O 表示法就是O(log2ⁿ)


      function fn(n) {
      let m = 0
      for (let i = 0; i < n; i *= 3) {
      m++
      }
      }

      那么上面这段代码就很容易看出来是O(log3ⁿ) 了 我们忽略他的底数,都统一表示O(logn)


      在这基础上O(nlogn) 就更好理解了,它表示的就是 一段 执行n遍的 logn复杂度的代码,我们把上面的代码稍稍修改一下


       function fn(n) {
      for(let j =0;j<n;j++){
      let m = 0
      for (let i = 0; i < n; i *= 2) {
      m++
      }
      }
      }

      空间复杂度


      其实空间复杂度 和时间复杂度 计算方式是一模一样的,只不过是着重的点不一样,当你回了时间复杂度的计算,空间复杂对你来说就是张飞吃豆芽了


         function fn(n) {
      let m = []
      for (let i = 0; i <= n; i = ++ ) {
      m.push(i)
      }
      }

      这块代码我们关注他的空间使用 就知道 是O(n)了


      案例分析


      我们来举个前端中一个经典的复杂度优化的例子,react,vue 他们的diff算法。


      要知道目前最好的 两棵树的完全比较,复杂度也还是O(n³) ,这对频繁触发更新的情况,是一个严重的瓶颈。 同样的问题也存在于 react中 useEffect 和useMemo 的dep 以及 memo 的props。


      所以他们都将比较操作 只停留在了当前的一层,比如diff只比较 前后同一层级的节点变化,不同层级的变化比对在出发更新时做出决定,这样就可以始终把复杂度维持在O(n)



      结语


      其实你分析出来的复杂度不等于,代码真实的复杂度,不管是大O表示法也好 还是别的表示法也好,都是针对代码复杂度分析的一个抽象工具,比如有一段处理分页的代码的业务逻辑,你清清楚楚的知道,目前是不允许改变分页大小的,也就是每次调用最多传进来的只有10 条数据,但是代码写的复杂度是O(n²) 这时候其实是没有多大的影响的,但是假设你现在写了一个无线滚动的功能,每次加载还都需要对所有的数据做O(n²)的操作,那么这时候,你就需要去想想怎么做优化了


      作者:烟花易冷人憔悴
      来源:juejin.cn/post/7302644330883612672
      收起阅读 »

      从canvas到B站弹幕

      web
      canvas是HTML自带的一个用于绘制图形的标签,它身上的API太多了,本文会介绍几个常见的属性,以及应用到B站的实现 Canvas 我们在body中放一个canvas标签,然后在Script中添加属性 <body> <canvas i...
      继续阅读 »

      canvas是HTML自带的一个用于绘制图形的标签,它身上的API太多了,本文会介绍几个常见的属性,以及应用到B站的实现



      Canvas


      我们在body中放一个canvas标签,然后在Script中添加属性


      <body>
      <canvas id="canvas" width="300" height="300"></canvas>
      <script>
      let canvas = document.getElementById("canvas")
      let ctx = canvas.getContext("2d")
      ctx.fillStyle = "green"
      ctx.fillRect(10,10,55,50)
      </script>
      </body>

      let ctx = canvas.getContext("2d")其实就是对canvas实例化一个对象。先得到一只2维的画笔,接下来的操作都是针对这只画笔


      ctx.fillStyle = "green"给这只画笔沾上墨水,否则怎么画都画不出


      fillRext用来画填充矩形。其中四个参数分别为左上角坐标,和右下角坐标,此时效果如下


      1.png


      	<script>
      let canvas = document.getElementById("canvas")
      let ctx = canvas.getContext("2d")
      ctx.strokeRect(10,10,55,55)
      </script>

      strokeRect是用来画矩形的,只有边框,不会进行填充,stroke这个单词可能大家只知道有中风的意思,其实还有笔画,轻拭的意思,此时效果如下


      2.png


      再来一个自定义描边


      	<script>
      let canvas = document.getElementById("canvas")
      let ctx = canvas.getContext("2d")
      ctx.beginPath()
      ctx.moveTo(10, 10)
      ctx.lineTo(10, 55)
      ctx.lineTo(55, 10)
      ctx.closePath()
      ctx.stroke()
      </script>

      beginPath就是让画笔落在纸上


      moveTo接收的一个起始位置坐标


      两个lineTo是终点坐标


      closePath将所有点连接起来,stroke开始画,一定要有stroke,否则没有效果


      3.png


      当然,你也可以对其填充


              ctx.beginPath()
      ctx.moveTo(10, 10)
      ctx.lineTo(10, 55)
      ctx.lineTo(55, 10)
      ctx.fill()

      默认颜色黑色


      4.png


      当然,你也可以画贝塞尔曲线(bezierCurve):不规则的曲线,这个内容我这里不做介绍,方法可以网上自寻搜索


      再来画个圆


          let canvas = document.getElementById("canvas")
      let ctx = canvas.getContext("2d")
      ctx.arc(50,50,40,0,2 * Math.PI)
      ctx.stroke()

      arc方法用于画圆或圆弧,前两个参数为圆心坐标,第三个参数为圆的半径,第四个参数是起始角度(通常为0,三点钟方向),最后一个参数为终止角度。


      5.png


      绘制文本


          let canvas = document.getElementById("canvas")
      let ctx = canvas.getContext("2d")
      ctx.font = '50px sans-serif'
      ctx.fillText('床前明月光',10, 100)

      fillText中后两个参数为起始坐标,strokeText绘制的是空心字


      6.png


      如果两个fillText的起始坐标一样,就可以重叠在一起,我现在再加一句同其实坐标的文字


      7.png


      B站弹幕其实就是用的画布,但是实现起来还是比较困难,为了方便文章排版,注释都放在了代码里面


      b站弹幕


      html部分


      <!DOCTYPE html>
      <html lang="en">
      <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Document</title>
      <style>
      #canvas {
      position: absolute;
      }
      /* 宽高不在css设置,是因为是和视频一起变化,动态的,用js */
      </style>
      </head>
      <body>
      <div class="wrap">
      <h1>Peaky Blinders</h1>
      <div class="main">
      <canvas id="canvas"></canvas>
      <video src="./video.mp4" id="video" controls="true" width="720" height="480"></video>
      </div>
      <div class="content">
      <input type="text" id="text">
      <input type="button" value="发弹幕" id="btn">
      <!-- 取色器 -->
      <input type="color" id="color">
      <!-- 控制弹幕的大小 -->
      <input type="range" id="range" max="40" min="20">
      </div>
      </div>
      <script src="./index.js"></script>
      <!-- 执行js 后加载js js可能是本地文件,也可能是在线地址,如果放在开头,执行这个的时候会堵住html的加载,甚至会报错,读取canvas都不知道,如果保证js内容在页面加载完毕后执行也可以放在前面,就是window.onload这个方法 -->
      </body>
      </html>

      js部分


      // 这个js代码很难写,一般高级程序员才会这样写
      // window.onload = function(){}
      // 1.读取用户内容 2.把内容颜色大小放到画布上,绘制
      // 历史弹幕,数组(里面放对象)还要接受新的弹幕,到了时间就绘制,要递归
      let data = [
      { value: 'By order of the peaky bliears', color: 'red', fontSize: 22, time: 5 },
      { value: 'No Fucking Fighting', color: 'green', fontSize: 30, time: 10},
      { value: 'Fucking Shelby', color: 'black', fontSize: 22, time: 22}
      ]
      // 整理弹幕数据,弹幕的y,历史弹幕问题 形参跟外面的一样没毛病,辨识度更高,代码太多了abc是啥都不知道,都可以 ,形参可以默认值,万一没有传值呢
      function CanvasBarrage(canvas, video, opts = {}){
      if(!canvas || !video) return
      this.video = video
      this.canvas = canvas
      // 伪代码 canvas 宽高 和 video宽高保持一致
      // canvas.width = style.width style读取宽高,js设置宽高
      this.canvas.width = video.width
      this.canvas.height = video.height
      // 获取画布
      this.ctx = canvas.getContext("2d")
      // 初始化代码
      // 没有认为修改弹幕的设置,默认值
      let defOpts = {
      color: '#e91e63',
      fontSize: 20,
      speed: 1.5,
      // 透明度
      opacity: 0.5,
      data: []
      //value和time不需要默认值
      }
      Object.assign(this, defOpts, opts)
      // 视频播放,弹幕才会进行绘制
      this.isPaused = true
      // 默认暂停
      // 获取到所有的弹幕 map(返回一个新的数组)里面是箭头函数(把item交给一个新的箭头函数) map循环了,每个弹幕都被修饰了一下
      this.barrages = this.data.map((item) => new Barrage(item, this))
      this.render()
      }
      Barrage.prototype.init = function(){
      // 左边是自己新建的右边是传进来的,如果第一个是没有的,就是给出的默认的颜色
      this.color = this.obj.color || this.context.color
      this.speed = this.obj.speed || this.context.speed
      this.opacity = this.obj.opacity || this.context.opacity
      this.fontSize = this.obj.fontSize || this.context.fontSize

      let p = document.createElement('p')
      // 让字体大小等于设置的大小
      p.style.fontSize = this.fontSize + 'px'
      p.innerHTML = this.value
      document.body.appendChild(p)
      // 右边是获取这个容器的宽度
      this.width = p.offsetWidth
      // 放完之后要删掉
      document.body.removeChild(p)
      // 设置弹幕的位置
      this.x = this.context.canvas.width
      // y的高度是随机值
      this.y = this.context.canvas.height * Math.random()
      // 弹幕可能上下方超出边界
      if(this.y < this.fontSize){
      this.y = this.fontSize
      }else if(this.y > this.context.canvas.height - this.fontSize){
      this.y = this.context.canvas.height - this.fontSize
      }
      }

      // Barrage 修饰一条弹幕 为箭头函数那里服务 (实例对象,this对象)
      function Barrage(obj, context){
      this.value = obj.value
      this.time = obj.time
      // 挂在构造函数中后面更方便
      this.obj = obj
      this.context = context
      }

      CanvasBarrage.prototype.clear = function(){
      this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
      }
      // 将这条弹幕会绘制在画布上
      Barrage.prototype.renderEach = function(){
      // canvas绘制过程
      // 设置画布的文字字体,字号
      // 设置画布的文字颜色
      // 绘制颜色
      this.context.ctx.font = `${this.fontSize}px Arial`
      this.context.ctx.fillStyle = this.color
      this.context.ctx.fillText(this.value, this.x, this.y)
      }
      // 将弹幕绘制到画布上
      CanvasBarrage.prototype.renderBarrages = function(){
      // 伪代码 拿到视频播放的当前时间,根据时间绘制
      let time = this.video.currentTime
      // 遍历所有的弹幕
      this.barrages.forEach(function(barrage){
      // 出屏之外之后就不用再操作了
      if(time >= barrage.time && !barrage.flag){
      // 这属性没有就是false 这个操作就是为了防止放过了的弹幕不需要初始化
      if(!barrage.isInit){
      barrage.init()
      barrage.isInit = true
      }
      // 控制弹幕左移
      barrage.x -= barrage.speed
      // rednerEach相当于ctx.fillstyle
      barrage.renderEach()
      // 弹幕是有长度的
      if(barrage.x < -barrage.width){
      barrage.flag = true
      }
      }
      })
      }
      // 这里就是render ,把弹幕弄到画布中
      CanvasBarrage.prototype.render = function(){
      // 清除画布,习惯问题
      this.clear()
      // 要先绘制才能操作画笔,并且要向左移动
      this.renderBarrages()
      // 播放状态才能移动
      if(!this.isPaused){
      // setInterval这里不用,下面定时器的更高级,16.7ms(内定时间)之后就执行一次,递归之后就是一直循环下去
      requestAnimationFrame(this.render.bind(this))
      // bind(this)以后再讲
      }
      }
      // 添加新的弹幕
      CanvasBarrage.prototype.add = function(obj){
      // barrages是终极数组,data修饰之后的
      // this.barrages16.7ms之后也会重修渲染一次
      this.barrages.push(new Barrage(obj,this))
      }
      // 传的参数是canvas和video dom结构 opts是一个对象含value color time fontSize 这个会替代掉,合并对象,相同的覆盖,不同的加进去
      let canvas = document.getElementById('canvas')
      // video知道此时视频多少秒
      let video = document.getElementById('video')
      // $没有意义,区分罢了
      let $text = document.getElementById('text')
      let $btn = document.getElementById('btn')
      let $color = document.getElementById('color')
      let $range = document.getElementById('range')
      // 整理弹幕的实例对象
      // 对象里key和value可以直接由{data: data}变成{data}
      let canvasBarrage = new CanvasBarrage(canvas, video, {data})
      // play是播放,处理所有弹幕实例对象
      video.addEventListener('play',function(){
      canvasBarrage.isPaused = false
      // 处理每一条弹幕,canvasBarrage相当于一个管家
      canvasBarrage.render()
      })

      function send(){
      // 读取文本内容
      let value = $text.value
      // video 自带一个属性读取时间
      let time = video.currentTime
      let color = $color.value
      let fontSize = $range.value
      // 把上面的内容整理成一个对象,交给函数去操作
      let obj = {
      value: value,
      color: color,
      fontSize: fontSize,
      time: time
      }
      // 多么希望add可以把obj放进去,接收新的弹幕,处理弹幕再走一遍send
      canvasBarrage.add(obj)
      }
      $btn.addEventListener('click', send)
      $text.addEventListener('keyup',function(e){

      console.log(e);
      if(e.keyCode === 13){
      send()
      }

      })


      1. 数据结构:

        • data 数组包含表示单个弹幕项的对象。每个对象具有诸如 value(文本内容)、colorfontSizetime(显示时间)等属性。



      2. CanvasBarrage 类:

        • CanvasBarrage 是一个构造函数,用于初始化弹幕系统。

        • 它接受一个画布元素、一个视频元素和可选的配置选项。

        • 默认选项(defOpts)包括诸如 colorfontSizespeedopacitydata 等属性。

        • 根据提供的数据创建了一个 Barrage 对象的数组。

        • render 方法负责在画布上渲染和动画弹幕。

        • clear 方法在渲染之前清除画布。



      3. Barrage 类:

        • Barrage 是用于单个弹幕项的构造函数。

        • 它接受一个对象(obj)和一个上下文(context),即 CanvasBarrage 的实例。

        • init 方法使用属性如 colorspeedopacityfontSizewidthxy 初始化弹幕项。

        • renderEach 方法在画布上渲染单个弹幕项。



      4. 渲染和动画:

        • renderBarrages 方法负责根据当前视频时间渲染所有弹幕项。

        • render 方法使用 requestAnimationFrame 不断调用自身以进行连续动画。

        • add 方法允许向系统添加新的弹幕项。



      5. 事件监听器:

        • 对视频的 play 事件监听器触发在视频播放时渲染弹幕。

        • 对按钮($btn)的 click 事件监听器触发 send 函数以添加新的弹幕。

        • 对文本输入框($text)的 keyup 事件监听器在按下Enter键时触发 send 函数。



      6. 用户输入处理:

        • send 函数读取输入值(文本、时间、颜色、fontSize)并创建一个新的弹幕对象,然后将其添加到弹幕系统中。



      7. 初始化:

        • 使用画布、视频和提供的数据创建了 CanvasBarrage 的实例。



      8. 使用:

        • 当视频播放时,弹幕系统开始渲染,并且用户可以使用提供的输入元素添加新的弹幕




      效果如下:


      效果.gif




      如果觉得本文对你有帮助的话,可以给个免费的赞吗[doge] 还有可以给我的gitee链接codeSpace: 记录coding中的点点滴滴 (gitee.com)点一个免费的star吗[星星眼]


      作者:Dolphin_海豚
      来源:juejin.cn/post/7302310196311719988
      收起阅读 »

      风控规则引擎(一):Java 动态脚本

      风控规则引擎(一):Java 动态脚本 日常场景 共享单车会根据微信分或者芝麻分来判断是否交押金 汽车租赁公司也会根据微信分或者芝麻分来判断是否交押金 在一些外卖 APP 都会提供根据你的信用等级来发放贷款产品 金融 APP 中会根据很复杂规则来判断用户是否...
      继续阅读 »

      风控规则引擎(一):Java 动态脚本


      日常场景



      1. 共享单车会根据微信分或者芝麻分来判断是否交押金

      2. 汽车租赁公司也会根据微信分或者芝麻分来判断是否交押金

      3. 在一些外卖 APP 都会提供根据你的信用等级来发放贷款产品

      4. 金融 APP 中会根据很复杂规则来判断用户是否有借款资格,以及贷款金额。


      在简单的场景中,我们可以通过直接编写一些代码来解决需求,比如:


      // 判断是否需要支付押金
      return 芝麻分 > 650

      这种方式代码简单,如果规则简单且不经常变化可以通过这种方式,在业务改变的时候,重新编写代码即可。


      在金融场景中,往往会根据不同的产品,不同的时间,对接的银行等等多个维度来配置规则,单纯的直接编写代码无法满足业务需求,而且编写代码的方式对于运营人员来说无论实时性、可视化都很欠缺。


      在这种情况往往会引入可视化的规则引擎,允许运营人员可以通过可视化配置的方式来实现一套规则配置,具有实时生效、可视化的效果。减少开发和运营的双重负担。


      这篇主要介绍一下如何实现一个可视化的表达式的定义和执行。


      表达式的定义


      在上面说到的使用场景中,可以了解中至少需要支持布尔表达式。比如



      1. 芝麻分 > 650

      2. 居住地 不在 国外

      3. 年龄在 18 到 60 之间

      4. 名下无其他逾期借款


      ...


      在上面的例子中,可以将一个表达式分为 3 个部分



      1. 规则参数 (ruleParam)

      2. 对应的操作 (operator)

      3. 对应操作的阈值 (args)


      则可以将上面的布尔表达式表示为



      1. 芝麻分 > 650


      {
      "ruleParam": "芝麻分",
      "operator": "大于",
      "args": ["650"]
      }


      1. 居住地 不在 国外


      {
      "ruleParam": "居住地",
      "operator": "位于",
      "args": ["国内"]
      }


      1. 年龄在 18 到 60 之间


      {
      "ruleParam": "年龄",
      "operator": "区间",
      "args": ["18""60"]
      }


      1. 名下无其他逾期借款


      {
      "ruleParam": "在途逾期数量",
      "operator": "等于",
      "args": ["0"]
      }

      表达式执行


      上面的通过将表达式使用 json 格式定义出来,下面就是如何在运行中动态的解析这个 json 格式并执行。


      有了 json 格式,可以通过以下方式来执行对应的表达式



      1. 因为表达式的结构已经定义好了,可以通过手写代码来判断所有的情况实现解释执行, 这种方案简单,但增加操作需要修改对应的解释的逻辑, 且性能低


      /*
      {
      "ruleParam": "在途逾期数量",
      "operator": "等于",
      "args": ["0"]
      }
      */

      switch(operator) {
      case "等于":
      // 等于操作
      break;
      case "大于":
      // 等于操作
      break;
      ...
      }



      1. 在第一次得到 json 字符串的时候,直接将其根据不同的情况生成对应的 java 代码,并动态编译成 Java Class,方便下一次执行,该方案依然需要处理各种情况,但因为在第一次编译成了 java 代码,性能和直接编写 java 代码一样




      2. 使用第三方库实现表达式的执行




      使用第三方库实现动态表达式的执行


      在 Java 中有很多表达式引擎,常见的有



      1. jexl3

      2. mvel

      3. spring-expression

      4. QLExpress

      5. groovy

      6. aviator

      7. ognl

      8. fel

      9. jsel


      这里简单介绍一下 jexl3 和 aviator 的使用


      jexl3 在 apache commons-jexl3 中,该表达式引擎比较符合人的书写习惯,其会判断操作的类型,并将参数转换成对应的类型比如 3 > 4 和 "3" > 4 这两个的执行结果是一样的


      aviator 是一个高性能的 Java 的表达式类型,其要求确定参数的类型,比如上面的 "3" > 4 在 aviator 是无法执行的。


      jexl3 更适合让运营手动编写的情况,能容忍一些错误情况;aviator 适合开发来使用,使用确定的类型参数来提供性能


      jexl3 使用


      加入依赖


      <dependency>
      <groupId>org.apache.commons</groupId>
      <artifactId>commons-jexl3</artifactId>
      <version>3.2.1</version>
      </dependency>

      // 创建一个带有缓存 jexl 表达式引擎,
      JexlEngine JEXL = new JexlBuilder().cache(1000).strict(true).create();

      // 根据表达式字符串来创建一个关于年龄的规则
      JexlExpression ageExpression = JEXL.createExpression("age > 18 && age < 60");

      // 获取需要的参数,java 代码太长了,简写一下
      Map<String, Object> parameters parameters = {"age": 30}

      // 执行一下
      JexlContext jexlContext = new MapContext(parameters);

      boolean result = (boolean) executeExpression.evaluate(jexlContext);

      以上就会 jexl3 的简单使用


      aviator


      引入依赖


      <dependency>
      <groupId>com.googlecode.aviator</groupId>
      <artifactId>aviator</artifactId>
      <version>5.3.1</version>
      </dependency>

      Expression ageExpression = executeExpression = AviatorEvaluator.compile("age > 18 && age < 60");

      // 获取需要的参数,java 代码太长了,简写一下
      Map<String, Object> parameters parameters = {"age": 30}

      boolean result = (boolean) ageExpression.execute(parameters);

      注意 aviator 是强类型的,需要注意传入 age 的类型,如果 age 是字符串类型需要进行类型转换


      性能测试


      不同表达式引擎的性能测试


      Benchmark                                         Mode  Cnt           Score           Error  Units
      Empty thrpt 3 1265642062.921 ± 142133136.281 ops/s
      Java thrpt 3 22225354.763 ± 12062844.831 ops/s
      JavaClass thrpt 3 21878714.150 ± 2544279.558 ops/s
      JavaDynamicClass thrpt 3 18911730.698 ± 30559558.758 ops/s
      GroovyClass thrpt 3 10036761.622 ± 184778.709 ops/s
      Aviator thrpt 3 2871064.474 ± 1292098.445 ops/s
      Mvel thrpt 3 2400852.254 ± 12868.642 ops/s
      JSEL thrpt 3 1570590.250 ± 24787.535 ops/s
      Jexl thrpt 3 1121486.972 ± 76890.380 ops/s
      OGNL thrpt 3 776457.762 ± 110618.929 ops/s
      QLExpress thrpt 3 385962.847 ± 3031.776 ops/s
      SpEL thrpt 3 245545.439 ± 11896.161 ops/s
      Fel thrpt 3 21520.546 ± 16429.340 ops/s
      GroovyScript thrpt 3 91.827 ± 106.860 ops/s

      总结


      这是写的规则引擎的第一篇,主要讲一下



      1. 如何讲一个布尔表达式转换为 json 格式的定义方便做可视化存储和后端校验

      2. 如何去执行一个 json 格式的表达式定义


      在这里也提供了一些不同的表达式引擎和性能测试,如果感兴趣的可以去尝试一下。


      下一篇主要讲一下在引擎里面规则参数、操作符是如何设计的,也讲一下可视化圆形的设计


      作者:双鬼带单
      来源:juejin.cn/post/7302805039450210313
      收起阅读 »

      技术大佬问我 订单消息重复消费了 怎么办?

      技术大佬 :佩琪,最近看你闷闷不乐了,又被虐了? 佩琪:(⊙o⊙)…,又被大佬发现了。这不最近出去面试都被揉捏的像一个麻花了嘛 技术大佬 :哦,这次又是遇到什么难题了? 佩琪: 简历上的技术栈里,不是写了熟悉kafka中间件嘛,然后就被面试官炮轰了里面的细节 ...
      继续阅读 »

      技术大佬 :佩琪,最近看你闷闷不乐了,又被虐了?


      佩琪:(⊙o⊙)…,又被大佬发现了。这不最近出去面试都被揉捏的像一个麻花了嘛


      技术大佬 :哦,这次又是遇到什么难题了?


      佩琪: 简历上的技术栈里,不是写了熟悉kafka中间件嘛,然后就被面试官炮轰了里面的细节


      佩琪: 其中面试官给我印象深刻的一个问题是:你们的kafka消息里会有重复消息的情况吗?如果有,是怎么解决重复消息的了?


      技术大佬 :哦,那你是怎么回答的了?


      佩琪:我就是个crud boy,根本不知道啥是重复消息,所以就回答说,没有


      技术大佬 :哦,真是个诚实的孩子;然后了?


      佩琪:然后面试官就让我回家等通知了,然后就没有然后了。。。。


      佩琪:对了大佬,只是简单的接个订单消息,为啥还会有重复的订单消息了?


      技术大佬 :向上抬了抬眼睛,清了清嗓子,面露自信的微笑回答道:以我多年的经验,这里面大概有2个原因:一个是重复发送,一个是重复消费


      写作 (6).png


      佩琪哦哦,那重复发送是啥了?


      技术大佬 :重试发送是从生产端保证消息不丢失,但不保证消息不会重发;业界还有个专业术语定义这个行为叫做"at least once"。在重试的时候,如果发送消息成功,在记录成功前程序崩了/或者因为网络问题,导致消息中间件存储了消息,但是调用方失败了,调用方为了保证消息不丢,会再次重发这个失败的消息。具体详情可参见上篇文章《kafka 消息“零丢失”的配方》


      佩琪重复消费又是啥了?


      技术大佬 :重复消费,是指消费端重复消费。举个例子吧, 比如我们一般在消费消息时,都建议处理完业务后,手工提交offset;但是在提交offset的时候,因为某些原因程序崩了。再次重启消费者应用后,会继续消费上次未提交的消息,像下面这段代码


        while(true){
      consumer.poll(); // ①拉取消息
      XXX // ② 业务处理;
      consumer.commit(); //③提交消费位移
      }

      在第三步,提交位移的时候,程序突然崩了(jvm crash)或者网络闪断了;再次消费时,会拉取未提交的消息重复执行第二步的业务处理。


      佩琪:哦哦,原来是这样。我就写了个消费者程序,咋这么多的“技术坑”在这里面。请问大佬,解决重复消费的底层逻辑是啥了?


      技术大佬 : 两个字:幂等。 即相同的请求参数,请求1次,和请求100W次,得到的结果和对业务的影响是一样的。比如:同一个订单消息消费一次,然后进行积分累加;和同一个订单的消息重复消费10次,进行积分累加,最后效果是一样的,总积分不能变,不能变。 如果同一个订单消费了10次,积分也给累加了10次,这种就不叫幂等了。


      佩琪:哦哦。那实现幂等的核心思想和通用做法又是什么了?


      技术大佬 :其实也挺简单 存储+唯一key 。在进行业务处理时,查询下是否已处理过这个唯一key的消息;如果存在就不进行后续业务处理;如果不存在就继续后续业务的处理吧。


      佩琪摸了摸头,唯一key是个啥了?


      技术大佬 :唯一key是消息里业务数据的唯一标识; 比如对于某些业务场景是订单号;某些业务场景是订单号+订单状态;


      佩琪存储又是指什么了?


      技术大佬 :一般指的就是存储引擎了;比如业界常用的mysql,redis;或者不怎么常用的mongo,hbase等。


      佩琪对了大佬,目前业界有哪些幂等解决方案?


      技术大佬常用的大概有两种方式:强校验和弱校验


      佩琪强校验是什么了?能具体说说吗?


      技术大佬 :强校验其实是以数据库做为存储;唯一key的存储+业务逻辑的处理放入一个事务里;要么同时成功,要么同时失败。举个例子吧,比如接收到 用户订单支付消息后;根据订单号+状态,累加用户积分;先查询一把流水表,发现有这个订单的处理记录了,直接忽略对业务的处理;如果没有则进行业务的操作 ,然后把订单号和订单状态做为唯一key,插入流水表,最后做为一个整体的事务进行提交;


      整体流程图如下:


      写作 (4).png
      待做


      佩琪大佬好强。能具体说说你的这种方案有什么优缺点吗?


      技术大佬 :给你一张图,你学习下?


      优点缺点
      在并发情况下,只会严格执行一次。数据库唯一性+事务回滚能保证业务只执行一次; 不会存在幂等校验穿透的问题处理速度较慢: 处理性能上和后续的redis方案比起来,慢一个数量级。毕竟有事务加持;另外插入唯一数据时极大可能读磁盘数据,进行唯一性校验
      可提供查询流水功能:处理流水记录的持久化,在某些异常问题排查情况下,还能较为方便的提供查询记录历史数据需要额外进行清理:如果采用mysql进行存储,历史记录数据的清理,需要自己单独考虑和处理,但这应该不是个事儿
      实现简单:实现难度还是较为简单的,一个注解能包裹住事务+回滚
      适用资金类业务:非常适合涉及资金类业务的防重;毕竟涉及到钱,不把数据持久化和留痕,心理总是不踏实
      架构上简约:架构上也简单,大多数业务系统都需要依赖数据库中间件吧。

      佩琪: 果然是大佬,请收下我的打火机。


      佩琪弱弱的问下,那弱校验是什么了?


      技术大佬 :其实是用redis进行唯一key存储和防重。比如订单消息,订单号是一条数据的唯一标识吧。然后使用lua脚本设置该消息正在消费中;此时重复消息来,进行相同的设置,发现该订单号,已经被标识为正在处理中,那这条消息放入延时队列中,延时重试吧;如果发现已经消费成功,则直接返回,不执行业务了;业务执行完,设置该key执行成功。
      大概过程是这样的
      写作 (5).png


      佩琪那用redis来进行防重,会存在什么问题吗?


      技术大佬 : 可能会存在防重数据的丢失吧,最后带来防不了重。


      佩琪redis为什么会丢防重数据了?


      技术大佬 : 数据丢失一部分是因为redis自身的原因,因为它把数据放入了内存中;虽然提供了异步复制机制+高可用方案,但还是不能100%保证数据不丢失。


      技术大佬 : 另外一个原因是,数据过期清理后,可能还有极低的概率处理相同的消息,此时就防不了重了。


      佩琪那能不设置过期时间吗?


      技术大佬 : 额,除非家里有矿。


      技术大佬 : redis毕竟是用内存进行存储,存储容量比起硬盘来小很多,存储单位是G(硬盘线上存储单位是T开始),而且价格比起硬盘来又贵多个数量级;属于骄贵性存储;所以为了节约存储空间,一般都会设置一个较短的过期时间,进行数据的淘汰;而这个较短过期时间,是根据业务情况进行定义,有5分钟的,有10分钟的。


      技术大佬 : 在说了,这些防重key是不太具备业务属性和高频率访问特性的,不属于热点数据,为啥还要一直放到缓存里了???


      佩琪:果然是大佬,请再次收下我的打火机。用redis来做防重,缺点这么的多,那为什么还要用redis来进行防重了?


      技术大佬 :你不觉得它的优点也很多吗。用它,主要是利用redis操作数据速度快,性能高;并且还能自动清理过期数据的特性,简直不要太方便;另外做防重的目标是啥了?还不是为了那些少数异常情况下产成的重复数据对其过滤;所以引入redis做防重,是为了大多数情况下,降低对业务性能的伤害;从而在性能和数据准确性中间取了个平衡。


      技术大佬 : 建议对处理的及时性有一定要求,并且非资金类业务;比如消费下单消息,然后发送通知等业务使用吧。


      技术大佬 :我知道你想要问什么了?我这里画了图,列了下优缺点,你拿去看看?


      优点缺点
      处理速度快因为数据有过期时间和redis自身特性;防重数据有丢失可能性,结果就是有不能防重的风险
      无需自动清理唯一key记录实现上比起数据库,稍显复杂,需要写lua脚本;但学过编程的,相信我半天时间熟悉语法+写这个lua脚本应该是没问题的
      消息一定能消费成功架构上稍显复杂,为了保证一定能消费成功,引入了延时队列

      佩琪:嘿嘿大佬,我听说防重最好的是用布隆过滤器,占用空间小,速度很快,为啥不用布隆过滤器了?


      技术大佬 :不使用redis布隆过滤器,主要是 redis 布隆过滤器特性会导致,某些消息会被漏掉。因为布隆过滤器底层逻辑是,校验一个key如果不存在,绝对不会存在;但是某个key如果存在,那么他是可能存在,又可能不存在。所以这会导致防重查询不准确,最终导致漏消息,这太不能接受了。


      技术大佬 :还有个不算原因的原因,是redis 4.0之前的版本还都不支持布隆过滤器了。


      佩琪大佬 redis我用过, redis 有个setnx,既能保证并发性,又能进行唯一key存储,你为啥不用了?


      技术大佬 :不使用它,主要是redis的 setnx操作和后续的业务执行,不是一个事务单元;即可能setnx成功了,后续业务执行时进程崩溃了,然后在消息重试的时候,又发现setnx里有值了,最终会导致消费失败的消息重试时,会被过滤,造成消息丢失情况。所以才引入了redis lua+延时消息。在lua脚本里记录消费业务的执行状态,延时消息保证消息一定不会丢失。


      佩琪我想偷个懒有现成的框架吗?


      技术大佬 :有的。实现核心的幂等key的设置和校验lua脚本。



      1. lua代码如下:


      local status = redis.call('get',KEYS[1]);
      if status == nil //不存在,则redis放入唯一key和过期时间
      then
      redis.call('SETEX',KEYS[1],ARGV[1],1)
      return "2" //设置成功
      else //存在,返回处理状态
      return status
      end


      1. 消费者端的使用,伪代码如下


      //调用lua脚本,获得处理状态
      String key = null; //唯一id
      int expiredTimeInSeconds = 10*60; //过期时间
      String status = evalScript(key,expiredTimeInSeconds);

      if(status.equals("2")){//设置成功,继续业务处理
      //业务处理
      }

      if(status.equals("1")){ //已在处理中
      //发送到延时队列吧
      }

      if(status.equals("3")){ //已处理成功
      //什么都不做了
      }


      总结




      1. 生产端的重复发送和消费端的重复消费导致消息会重




      2. 解决消息重复消费的底层逻辑是幂等




      3. 实现幂等的核心思想是:唯一key+存储




      4. 有两种实现方式:基于数据库强校验和基于redis的弱校验。




      感悟


      太难了


      为了保证上下游消息数据的完整性;引入了重试大法和手工提交offerSet等保证数据完整性解决手段;
      可引入了这些解决手段后;又带来了数据重复的问题,数据重复的问题,是可以通过幂等来解决的。


      太难了


      作为应用层开发的crud boy的我,深深的叹了口气,开发的应用要在网络,主机,操作系统,中间件,开发人员写个bug等偶发性问题出现时,还需要保证上层应用数据的完整性和准确性。


      此时佩琪头脑里突然闪现过一道灵光,业界有位大佬曾说过:“无论什么技术方案,都有好的一面,也有坏的一面。而且,每当引入一个新的技术方案解决一个已有的技术问题时,这个新的方案会带来更多的问题,问题就像一个生命体一样,它们会不断的繁殖和进化”。在消息防丢+防重的解决方案里,深感到这句话的哲理性。


      原创不易,请 点赞,留言,关注,转载 4暴击^^


      作者:程序员猪佩琪
      来源:juejin.cn/post/7302023698721570857
      收起阅读 »

      重要提醒!第三方 Cookie 即将被禁用

      web
      Chrome 浏览器计划从 2024 年第一季度开始禁用 1% 用户的第三方 Cookie,以方便测试,然后在 2024 年第三季度逐步覆盖到 100% 用户。Chrome 推出了一系列API,为诸如身份验证、广告和欺诈检测等用例提供了以隐私为重点的替代方案。...
      继续阅读 »

      chrome-4.webp


      Chrome 浏览器计划从 2024 年第一季度开始禁用 1% 用户的第三方 Cookie,以方便测试,然后在 2024 年第三季度逐步覆盖到 100% 用户。Chrome 推出了一系列API,为诸如身份验证、广告和欺诈检测等用例提供了以隐私为重点的替代方案。


      本文将带您了解禁用时间表,建议您立即采取行动,以确保您的网站做好准备。


      1.禁用时间表


      privacysandbox.com 的时间轴上,可以看到 2023 年第四季度和 2024 年第一季度将有两个里程碑,作为 Chrome 辅助测试模式的一部分。该测试主要针对测试隐私沙盒相关性和测量 API 的组织,但作为测试的一部分,将对 1% 的 Chrome 稳定版用户禁用第三方 cookies。


      配图:时间表


      这意味着从 2024 年开始,即使您没有积极参与 Chrome 浏览器辅助测试,您也可以预期在您的网站上看到越来越多的 Chrome 浏览器用户禁用了第三方 Cookie。这一测试周期将持续到 2024 年第三季度,届时将禁用所有 Chrome 浏览器用户的第三方 Cookie。


      2.需要做哪些准备?


      为确保您的网站在没有第三方 cookie 的情况下可以正常运行,需要做好以下准备:



      • 梳理第三方 cookie 的使用情况。

      • 进行破坏测试。

      • 对于存储在每个网站上的跨站点 cookie(例如嵌入式 cookie),请考虑使用 CHIPS 进行分区。

      • 对于在一小组相关联网站之间的跨站点 cookie,请考虑使用相关网站集。

      • 对于其他第三方 cookie 的使用情况,请迁移到相关的 Web API。


      2.1.梳理第三方 cookie 的使用情况


      Chrome开发者工具的网络面板显示请求中设置和发送的Cookie。在应用程序面板中,在存储下可以看到Cookie标题。您可以浏览每个访问的站点存储的Cookie,作为页面加载的一部分。您可以按照SameSite列进行排序,以将所有 Cookie分组。


      第三方 cookie 可以通过其 SameSite= 值来识别。您应该搜索代码以查找将 SameSite 属性设置为此值的实例。如果您在 2020 年左右之前对添加 SameSite= 到您的 Cookie 进行了更改,那么这些更改可能是一个很好的起点。


      Chrome DevTools 网络面板显示根据请求设置和发送的 cookie。在 Application 面板中,您可以在 Storage 下看到 Cookie。您可以浏览为页面加载过程中访问的每个站点存储的 cookie。您可以按列 SameSite 排序以对所有 值的 cookie 进行分组。


      从 Chrome 118 开始,DevTools 问题选项卡显示了重大更改问题:"跨站点上下文中发送的 Cookie 将在未来的 Chrome 版本中被阻止", 该问题列出了当前页面可能受影响的 cookie。


      chrome-issue-cookie.png


      如果您发现第三方设置的 cookie,您应该与这些提供商核实,看看他们是否有逐步淘汰第三方 cookie 的计划。例如,您可能需要升级正在使用的库的版本、更改服务中的配置选项,或者如果第三方正在自行处理必要的更改,则不采取任何操作。


      2.2.进行破坏测试


      您可以使用 --test-third-party-cookie-phaseout 命令行标志或从Chrome 118开始,使用 chrome://flags/#test-third-party-cookie-phaseout 启用。这将设置 Chrome 阻止第三方 cookie,并确保新功能和缓解措施处于活动状态,以最佳模拟淘汰后的状态。


      您也可以尝试通过 chrome://settings/cookies 阻止第三方 cookie 进行浏览,但请注意,该标志也确保了新功能和更新功能的启用。阻止第三方cookie是检测问题的好方法,但并不一定能验证您已经修复了问题。


      如果您为您的网站保持一个活跃的测试套件,那么您应该进行两次并行运行:一次是使用常规设置运行的Chrome,一次是使用启用--test-third-party-cookie-phaseout 标志启动的相同版本的 Chrome。第二次运行中的任何测试失败而第一次运行中没有的都是需要调查的第三方cookie依赖的好候选项。请确保报告您发现的问题。


      一旦您确定了存在问题的cookie并了解了它们的用例,您可以通过以下选项来选择必要的解决方案。


      2.3.将 Partitioned cookie 与 CHIPS 结合使用


      如果您的第三方 Cookie 在与顶级站点进行 1:1 嵌入的上下文中使用,则可以考虑使用带有独立分区状态的 Cookie(CHIPS)的分区属性,以允许使用每个站点使用的单独的 Cookie 进行跨站点访问。


      partitioned.png


      要实现 CHIPS,您需要将 Partitioned 属性添加到您的 Set-Cookie 头中:


      通过设置 Partitioned,该网站选择将 cookie 存储在由顶级网站分隔的单独的 cookie 存储区。在上面的示例中,cookie 来自store-finder.site,该网站托管了一个店铺地图,用户可以保存他们喜欢的店铺。通过使用 CHIPS,当 brand-a.site 嵌入store-finder.site 时,fav_store cookie 的值为123。然后,当 brand-b.site 也嵌入 store-finder.site 时,他们将设置并发送自己分隔的fav_store cookie 实例,例如值为456。


      这意味着嵌入式服务仍然可以保存状态,但没有允许跨站点跟踪的共享跨站点存储。


      潜在的使用案例:第三方聊天嵌入、第三方地图嵌入、第三方支付嵌入、子资源内容分发网络(CDN)负载均衡、无头内容管理系统提供商、用于提供不受信任的用户内容的沙盒域名、使用 Cookie 进行访问控制的第三方 CDN、需要在请求中添加 Cookie 的第三方 API 调用、按发布商进行状态范围的嵌入广告。


      2.4.使用相关网站集


      当仅在一小部分相关网站上使用第三 Cookie 时,您可以考虑使用相关网站集合(RWS),以便在这些定义的网站上上下文中允许跨站点访问该Cookie。


      要实施 RWS,您需要定义并提交网站组。为确保这些网站之间存在有意义的关联,有效集合的策略要求按以下方式对这些网站进行分组:具有可见关联的相关网站(如公司产品的变体)、服务域(如 API、CDN)或国家代码域(如 .uk、.jp)。


      RWS.png


      网站可以使用 Storage Access API 来请求跨站点的 Cookie 访问权限,使用 requestStorageAccess() 方法或使用requestStorageAccessFor() 方法委派访问权限。当网站在同一组中时,浏览器会自动授予访问权限,并且跨站点的 Cookie将可用。


      这意味着相关网站的组仍然可以在有限的上下文中使用跨站点的 Cookie,但不会冒着以允许跨站点追踪的方式在不相关的站点之间共享第三方cookie的风险。


      潜在的用例包括:特定于应用程序的域,特定于品牌的域,特定于国家的域,用于提供不受信任的用户内容的沙盒域,用于API的服务域,CDN。


      2.5.迁移到相关的 Web API


      CHIPS 和 RWS 能够在保护用户隐私的同时实现特定类型的跨站点 Cookie访问,但是其他使用第三方 Cookie 的实例必须迁移到以隐私为重点的替代方案。


      Privacy Sandbox 提供了一系列针对特定用例的专用API,无需使用第三方cookie:



      • 联合身份管理(FedCM)允许用户登录到站点和服务。

      • 私有状态令牌通过在站点之间交换有限的、非识别信息,实现反欺诈和反垃圾邮件功能。

      • 主题功能实现基于兴趣的广告和内容个性化。

      • 受保护的受众功能实现再营销和自定义受众。

      • 属性报告功能实现广告展示和转化的测量。


      此外,Chrome 还支持 Storage Access API (SAA),用于用户交互框架中的使用。SAA 已经在 Edge,Firefox 和 Safari 上得到支持。我们认为它在保持用户隐私的同时,仍然能够实现关键的跨站点功能,并具有跨浏览器的兼容性。


      请注意,Storage Access API (SAA) 将向用户显示浏览器权限提示。为了提供最佳的用户体验,只有在调用 requestStorageAccess() 的站点与嵌入页面进行交互并之前在顶级上下文中访问过第三方站点时,才会提示用户。成功授权将允许该站点在 30 天内跨站点访问 Cookie。可能的用例包括认证的跨站点嵌入,如社交网络评论小部件、支付提供商、订阅视频服务。


      如果您仍然有未被这些选项覆盖的第三方 Cookie 用例,您应该向 Chrome 团队报告该问题,并考虑是否有不依赖于启用跨站点跟踪的功能的替代实现。


      作者:FED实验室
      来源:juejin.cn/post/7302330573381156876
      收起阅读 »

      程序员必看几大定律(2),你中招了吗?

      1 洛克定律 确定目标,专注行动! 这条定律,我相信做过几年的程序员感受都会特别深! 除非你写的代码没什么难度,否则思考或者说设计才是程序员最经常要做的事,只有想清楚了,动手写代码才会顺畅! 不然你会发现自己经常在写一会->想一会->改一会 中...
      继续阅读 »

      1 洛克定律



      确定目标,专注行动!



      这条定律,我相信做过几年的程序员感受都会特别深!


      除非你写的代码没什么难度,否则思考或者说设计才是程序员最经常要做的事,只有想清楚了,动手写代码才会顺畅! 不然你会发现自己经常在写一会->想一会->改一会 中循环往复,浪费不少时间和精力。


      我自己总结了一下程序员的几个阶段:




      1. 初期: 以学习技术为主,写代码过程中花费时间最多的是:


        很多语法不懂要查、被一些低级错误耽误,调试很久可能发现只是单词拼错、完全没思路,只能疯狂搜索或者求助前辈。




      2. 中期: 常用技术已经熟悉,业务也逐渐深入,花费时间最多的是:


        某些代码因为经常copy,虽然已经写了几十次了,每次用的时候总是想不起来,还是要经常查。


        花更多的时间思考业务逻辑、思考代码结构。




      3. 后期: 我也还没达到,只能自己脑补一下:


        代码敲的更少了,主要负责项目管理,系统设计,系统架构。


        百科全书级的人物,能够解决绝大部分问题,当新人向你询问自己也曾犯过的错误时,也会感慨:时光不再了啊!!




      2 相关定律



      条条大路通罗马,万事万物皆有联系!



      不知道你们是不是跟我曾经一样,有某一段时期,痴迷于各种技术栈。


      我之前在刚接触前端,学习vue的时候,对前端充满了兴趣,不仅要会element-ui,也要会ant-design,不管是less,scss我都想要会,各种实用的工具库我也要会,vue2还不过瘾,vue3也得会,还看了react,nodejs,express,nuxt,next,ts,webpack,vite,rollup......


      花了不少时间看了各种技术栈的文档,也实际动手搞了点小东西。


      不过说真的,收获其实不多,即使当时感觉收获很多知识,但是因为这些额外的知识在工作中不常使用,过一段时间就忘了差不多了。


      后来我是想通了,与其这样囫囵吞枣,不如深入现有经常接触的知识,我就不信了,如果我把vue2彻底搞懂了,学vue3还不是轻轻松松,学react应该也只是分分钟的事。


      于是我就从各种源码开始看起,从axios到vuex,vue-router,vue2的源码也看了一部分了,不过最近工作忙,已经停了几周没看了,但是收获还是很多的,确实更让我坚信了:


      条条大路通罗马!!


      感兴趣的小伙伴也可以进入我的主页,里面有不少源码的阅读解析。


      3 奥卡姆剃刀定律



      把握关键,化繁为简



      不知不觉,已经从当初的菜鸟逐渐变成现在的老鸟,工作也从繁复的coding中挣脱出来了一些些。


      现在越来越觉得设计才是软件开发的精髓


      而一个好的设计,一定不能太过复杂!


      听一位领导讲过:公司推广过不少,不乏设计精细,十分深入业务理解的系统,但是真正能够推广让用户爱用的系统,一定不能太复杂,必要的时候也需要为用户的便利做出一些牺牲。


      4 墨菲定律



      如果事情有变坏的可能,不管这种可能性有多小,它总会发生。



      不知道是谁瞎传的,我一直以为墨菲定律说的是:你越害怕的事情,越容易发生!


      直到我看完墨菲定律才发现自己真的是被这句话给耽误了好久。




      墨菲定律对于程序员来说,我觉得意义是很重大的。


      因为如果代码存在bug,那么就一点有变坏的可能,不管这种可能性有多小,它总会发生!


      也就是说,只要存在bug,它早晚都是会被触发的!!


      不知道有多少人跟我曾经一样,抱着侥幸的心理,有时候即使知道某段代码存在bug,但是因为触发条件十分苛刻,就不去管它!!


      只是我的心里还是会隐隐担心着某天会不会被人发现。




      作为程序员,一定要保持代码的严谨性,对自己的代码负责。


      任何人都无法保证自己不出错。


      但是至少,如果已经有我们知道的问题,不要因为麻烦,不要因为侥幸,而去逃避它!


      或许你们不相信,当我抱着这种消除自己所有已知隐患的态度写代码之后,反而觉得整个人都轻松了不少。


      脑袋里没有那么多要惦记的事情,事情做完就真的等于事情做完了!如果真的出现自己也没想到的问题,那我也已经尽力了,再解决就是了。


      5 酝酿效应



      灵感来自偶然!



      不知道你们是否也有遇到过这样的场景:


      一整天都被某个问题困扰,百思不得其解!!


      回去睡一觉,第二天再一想这个问题,我去,答案怎么就直接有了!!


      那我昨天为什么会困扰这么久呢?




      这其实就跟酝酿效应有关系!


      当我们百思不得其解的时候,往往代表着脑袋的思考方向就进入了死胡同,这时候再怎么往里投入时间,投入精力都进展甚微。


      而当我们放下这件事,去做其他事情的时候,我们的潜意识并没有忘记这件事,还是在帮助我们思考,只不过不是继续往死胡同里走了,用一个词来形容,那就是酝酿


      所以当我们回过头再思考的时候,常常发现有心栽花花不开,无心插柳柳成荫柳暗花明又一村这样的现象!


      所以如果你们看到程序员老哥们正在喝茶发呆,一定要小心,他们一定不是表面看过去的这么简单,他们的脑海可能正在疯狂酝酿着一段绝佳的代码!!


      作者:林劭敏
      来源:juejin.cn/post/7302249949215408167
      收起阅读 »

      浅谈一下滴滴实习

      在租房躺尸好几天了,自从周一从滴滴离职就一直待在租房打游戏,瞬间没有工作的负担是真的彻底让我释放了心中的欲望,实际上游戏纯属发泄欲望和转移注意力的工具,我是一个喜欢瞎想的人(这可能就是我胖不起来的主要原因),放纵完实在是太无聊了,想写点什么。 说实话我这文章写...
      继续阅读 »

      在租房躺尸好几天了,自从周一从滴滴离职就一直待在租房打游戏,瞬间没有工作的负担是真的彻底让我释放了心中的欲望,实际上游戏纯属发泄欲望和转移注意力的工具,我是一个喜欢瞎想的人(这可能就是我胖不起来的主要原因),放纵完实在是太无聊了,想写点什么。
      说实话我这文章写得毫无章法,完全是想到哪里写到哪里,也不想去分门别类了,我觉得真实的想法最重要,如果有语义错误就略过吧哈哈。


      说说业务


      对这段实习做一个小小的总结,先说一说业务吧,我所在的是滴滴商旅的一个技术部门,主要负责企业版这块的业务,我去的第一天上午看团队规范文档,下午跑项目看代码,第二天接需求,当然是比较简单的需求,后面陆陆续续做了滴滴企业版的小部分 pc 端官网和大部分移动端官网,如果你现在用手机搜滴滴企业版,那么你看到的页面大概率就是我做的,除此以外还有一个经典后台管理项目,其实项目用的技术栈都还好,没有说很有难度的,对于业务来说我觉得最难的应该就是项目的搭建和部署,然后就是技术方案,开发代码确实是最基础的事情了,这几个月完成的代码量并不大,这也完全在意料之中,实习生嘛,能确保自学到东西就行,当自学到一定的程度会很迷茫,不知道下一个进阶的领域是什么,但是在这段时间我逐渐感觉前端的一个瓶颈就在前端工程化,其实早就在学了,但是没有实际的项目经验加上网上教程比较匮乏,大多是讲解 webpack 的基本使用甚至一度让大部分人认为 webpack 就是前端工程化,如果有后端基础我觉得理解工程化那就太简单了,只不过可惜的是参与前端开发的大多是后端经验为 0 的同学,因此对于常年在浏览器玩 js 的我们很难理解在编译阶段能做的一些工作的意义所在,不管是现在的 Node 或是 Go 和 Rust,其实都可以作为一个深入挖掘的方向,至少我感觉业务是真的很无聊,偶尔当玩具写写还行,每天写真的没意思。


      除了业务以外认知到一些原来不知道的职场"内幕"。


      第一点:面试冷知识

      走之前组内一直在招社招的员工,当面试官的兄弟和我说了我才知道,原来面试通一个部门甚至是同一个面试官可能真的会因为面试官心情或者其余外在因素决定你面试是否通过,比如最近部门比较忙,那可能面试也就比较水一点,大概考察没问题就直接过了没有那么多的时间去做横向比较(那我面的部门基本都还是比较闲啊哈哈),又或者是面试官看你比较顺眼性格也比较符合他的要求,大概率会给一些比较简单的题,这些都会影响面试官的判断从而决定你是否能通过面试最终拿 offer,所以经过这件事之后看开了很多,如果原来你一直不理解平时技术没你好的同学最后能拿到同公司或者同部门 offer,现在应该慢慢也就看开了,一旦挂了及时投递下一个部门,这不一定是自己的原因。


      第二点:大厂其实不全是 996

      不要被危言耸听,这其实大概率取决于你的部门而非公司,我在的部门经过这两年的形势成功变得小而精,小组的氛围很好,平时开发大家都合作得很开心,不管是导师还是 leader,休息了也会偶尔一起打打游戏,在这个部门我感觉挺好至少没有看到所谓的大厂 996,基本上大家 10 点来,最晚 8 点也都走了。离职的前一天刚好赶上了部门团建于是狠狠地去蹭了一只烤全羊,leader 把商旅的大 leader 请过来了,我对大领导的刻板印象是电视里那种懂得都得,但是没想到和我想的完全相反,大家举杯畅饮吹牛逼,欢声笑语,挺好,,后来想想有可能是因为大家都是技术出身很多时候也都很讨厌那一套,这也是我对互联网最满意的一点,凭本事吃饭,对于出身不是那么地依赖,也不是尔诈我虞,阿谀奉承。


      第三点:学会装菜,不要没事找事

      作为实习生,懂的都懂,其实在哪里都一样,如果你太着急表现自己,别人就会觉得你过分刺毛,能装菜的地方千万别装逼,艹,我感觉我就是傻逼了,这也许也是我离开的原因之一,作为实习生老老实实完成自己的工作就好,能够保质保量完成任务对于导师来说基本就差不多了,至于一些 pua 话术里面说的额外价值,我觉得对于没有转正的实习生来说毫无意义,反而会自找麻烦,因为并不会因为你原本安排 2 天切完的图你一天切完导师就给你放松自学,很多时候你做的事情是否有意义完全取决于你的导师是否愿意安排有意义的工作给你,所幸我在滴滴完整地参与了项目的技术方案到代码编写直至最后部署上线,里面沉淀了我自己的思考,经过这段实习确实让我受益匪浅。


      最后一点是软实力

      我觉得这也是我在这段实习中收获到的最重要的东西之一:"学会总结,及时复盘",每次周会给导师和兄弟们讲方案总是要准备很久,会去看很多的自己不知道的东西,以此来让我写的东西显得足够的高大上,记得有一次上线官网出问题了,意料之中做了一个复盘,倒不是说学到了什么代码层面的东西,更多的是让我了解了整个项目从开发到部署上线的流程,这个远远比写代码有意义,不得不说这极大地培养了我的能力,包括新技术的敏感程度,技术的深度以及口才,总结出来的东西一方面加深了自己的记忆和理解,往小了说,让我可以在以后的技术面试中就这段经历侃侃而谈,往大了说,这个让我学会从更高的视角去看问题,不再是盯着代码的一亩三分地,更多的是学会从项目的技术架构层面去看问题,第二是学会表现自己并且及时纠正自己的错误,没错,就是给别人看,自己瞎学总结是没有意义的,你是一个无比努力的人,可是大家不知道那也毫无意义,他能知道的仅仅是你能写上简历的东西,只有向别人更好的展示自己,下一次面试官看到你才会觉得你是一个善于总结和反思的人,程序员这一行也是这样,其实参与一个开源项目远远比你基础扎实更让人刮目相看,尽管你只是为一个看起来无比高大上的开源切了图,对我自己来说我只是把曾经在 wps 或者 typora 的写作 转移到了掘金或者 github,内容并没有太大的变化,这样的事情何乐而不为呢。


      最后做一个收尾。


      这两天想去北京附近转转,今天跑到了天安门,还是想吐槽地铁站一些人地素质问题,经典的钱包鼓起来了素质教育没跟上来,或者换句话说富起来很多并非接受过良好教育的一批人。


      马上快开学了,要回学校拿保研名额,说实话我到现在都不确定哪条路是对了,大厂?还是保研?还是国企公务员?谁知道呢,每个人有每个人的说法,老员工会劝你保研进编制,新员工会劝你尽早进大厂捞钱,每个人追求的往往都是目前最缺失的,也许正是因为未来充满未知所以才无限期待,不然像我这样躺尸一周那该多无聊,脑袋都睡麻木了,这两周陆陆续续也面试了四五家公司,不得不说有大厂背书投简历就是好使,曾经拒绝过我的那些公司都拿到了 offer 然后全给拒了哈哈,不为别的就是解气,基本都是一些 b 格还比较高的独角兽公司,比如教育,云服务器,游戏行业等等,大厂我肯定还是没这个底气的哈哈我依然是大部分大厂的舔狗,不过结果不算坏,下一站是老铁厂了。


      不知道是否会有之前一起工作的兄弟看到这篇文章,如果认为我有说得不恰当的地方欢迎指正。


      作者:雨赎
      来源:juejin.cn/post/7268289776867934266
      收起阅读 »

      华为鸿蒙app开发,真的遥遥领先?

      前言 最近刷头条,刷到很多开始鸿蒙系统app开发者说 鸿蒙系统要崛起了 属于国家意志。于是我也在周五空闲时间去华为官网学习一下,体验一下遥遥领先的感觉。 developer.huawei.com/ 官网下载下载DevEco Studio 下载流程就不用细说了 ...
      继续阅读 »

      前言


      最近刷头条,刷到很多开始鸿蒙系统app开发者说 鸿蒙系统要崛起了 属于国家意志。于是我也在周五空闲时间去华为官网学习一下,体验一下遥遥领先的感觉。
      developer.huawei.com/ 官网下载下载DevEco Studio


      下载流程就不用细说了 借鉴一下别人的文章,主要核心在于按照官网学习了一个ToDo的例子


      鸿蒙OS应用开发初体验


      启动页面


      image.png


      Setup


      image.png


      image.png



      HarmonyOS-SDK:鸿蒙操作系统软件开发工具包



      • Previewer:预览器

      • Toolchains:工具链



      OpenHarmony-SDK:开源鸿蒙操作系统软件开发工具包




      • ArkTS:鸿蒙生态的应用开发语言。

      • JS:JavaScript

      • Previewer:预览器

      • Toolchains:工具链



      image.png


      Create Project


      image.png image.png


      配置工程


      image.png 项目名称、包名、存储路径、编译SDK版本、模型,语言、设备类型等。


      工程目录结构


      image.png



      • AppScope:存放应用全局所需要的资源文件。

      • entry:应用主模块,存放HarmonyOS应用的代码、资源等。

      • on_modules:工程依赖包,存放工程依赖的源文件。

      • build-profile.json5是工程级配置信息,包括签名、产品配置等。

      • hvigorfile.ts是工程级编译构建任务脚本,hvigor是基于任务管理机制实现的一款全新的自动化构建工具,主要提供任务注册编排,工程模型管理、配置管理等核心能力。

      • oh-package.json5是工程级依赖配置文件,用于记录引入包的配置信息


      TODO例子


      这里我我干掉了初始代码 实现了一个TODO例子 源码贴出来了


      image.png



      import ArrayList from '@ohos.util.ArrayList'
      @Entry
      @Component
      struct Index {
      @State message: string = 'Hello World'
      private taskList:Array<String>=[
      '吃饭',
      '睡觉',
      '遛娃',
      '学习'
      ]
      build() {
      Column() {
      Text('待办')
      .fontSize(50)
      .fontWeight(FontWeight.Bold)
      .align(Alignment.Start)
      ForEach(this.taskList,(item)=>{
      ToDoItem({ content: item })
      })

      }.height('100%')
      .width('100%')
      .backgroundColor('#e6e6e6')

      }
      }

      @Component
      struct ToDoItem {
      private content: string;
      @State isComplete: boolean = false;
      @State isClicked: boolean = false;
      build() {
      Row() {
      Image($r('app.media.app_icon'))
      .width(20)
      .margin(10)
      Text(this.content)
      .fontSize(20)
      .fontWeight(FontWeight.Bold)
      .fontColor(Color.Black)
      .opacity(this.isComplete ? 0.4 : 1)
      .decoration({ type: this.isComplete ? TextDecorationType.Underline : TextDecorationType. })
      }.borderRadius(24)
      .width('100%')
      .padding(20)
      .backgroundColor(this.isClicked ? Color.Gray : Color.White)
      .margin(10)
      .onClick(
      ()=>{
      this.isClicked = true; // 设置点击状态为true
      setTimeout(() => {
      this.isClicked = false; // 0.5秒后恢复点击状态为false
      }, 500);
      this.isComplete=!this.isComplete
      }
      )
      }
      }

      总结


      在模拟器上 点击啥的效果还好 但是在我华为p40上的真机运行效果真的点击效果响应太慢了吧。本人也是华为手机的爱好者,但这一次真的不敢苟同谁敢用这样的平台开发app。有深入学习的大佬指点一下,望花粉勿喷。


      作者:阡陌昏晨
      来源:juejin.cn/post/7302070112639385651
      收起阅读 »

      鸿蒙OS应用开发初体验

      什么是HarmonyOS? HarmonyOS(鸿蒙操作系统)是华为公司开发的一款基于微内核的分布式操作系统。它是一个面向物联网(IoT)时代的全场景操作系统,旨在为各种类型的设备提供统一的操作系统平台和开发框架。HarmonyOS 的目标是实现跨设备的无缝协...
      继续阅读 »

      什么是HarmonyOS?


      HarmonyOS(鸿蒙操作系统)是华为公司开发的一款基于微内核的分布式操作系统。它是一个面向物联网(IoT)时代的全场景操作系统,旨在为各种类型的设备提供统一的操作系统平台和开发框架。HarmonyOS 的目标是实现跨设备的无缝协同和高性能。


      DevEco Studio



      对标Android Studio,开发鸿蒙OS应用的IDE。



      启动页面


      image.png


      Setup


      image.png


      image.png



      HarmonyOS-SDK:鸿蒙操作系统软件开发工具包



      • Previewer:预览器

      • Toolchains:工具链



      OpenHarmony-SDK:开源鸿蒙操作系统软件开发工具包




      • ArkTS:鸿蒙生态的应用开发语言。

      • JS:JavaScript

      • Previewer:预览器

      • Toolchains:工具链



      image.png


      Create Project


      image.png
      image.png


      配置工程


      image.png
      项目名称、包名、存储路径、编译SDK版本、模型,语言、设备类型等。


      工程目录结构


      image.png



      • AppScope:存放应用全局所需要的资源文件。

      • entry:应用主模块,存放HarmonyOS应用的代码、资源等。

      • on_modules:工程依赖包,存放工程依赖的源文件。

      • build-profile.json5是工程级配置信息,包括签名、产品配置等。

      • hvigorfile.ts是工程级编译构建任务脚本,hvigor是基于任务管理机制实现的一款全新的自动化构建工具,主要提供任务注册编排,工程模型管理、配置管理等核心能力。

      • oh-package.json5是工程级依赖配置文件,用于记录引入包的配置信息。


      Device Manager


      image.png


      创建好的模拟器会出现在这里。
      image.png


      启动模拟器之后,会在设备列表中出现。


      image.png


      编译运行


      image.png
      编译运行,可以从通知栏看到输出的文件并不是apk,而是hap(Harmony Application Package的缩写)。是鸿蒙操作系统设计的应用程序包格式。


      image.png
      .hap 文件包含了应用程序的代码、资源和元数据等信息,用于在 HarmonyOS 设备上安装和运行应用程序。


      image.png


      整体开发流程跟Android基本无差,所以熟悉Android开发的同学上手基本没啥难度。


      ArkTS



      ArkTS是鸿蒙生态的应用开发语言。它在保持TypeScript(简称TS)基本语法风格的基础上,对TS的动态类型特性施加更严格的约束,引入静态类型。同时,提供了声明式UI、状态管理等相应的能力,让开发者可以以更简洁、更自然的方式开发高性能应用。
      developer.harmonyos.com/cn/develop/…



      最简单例子:


      @Entry
      @Component
      struct Index {
      @State message: string = 'Hello World'

      build() {
      Row() {
      Column() {
      Text(this.message)
      .fontSize(50)
      .fontWeight(FontWeight.Bold)
      }
      .width('100%')
      }
      .height('100%')
      }
      }

      看起来非常简洁,采用的是声明式UI,写过Flutter的同学对声明式UI应该不会陌生。从最简单的例子初步了解下基本语法:



      • 装饰器,用于装饰类、结构、方法以及变量,并赋予其特殊的含义。如@Entry、@Component、@State都是装饰器。

      • 自定义组件:可复用的UI单元,可组合其他组件,如上述被@Component装饰的stuct Index。

      • UI 描述:以声明式的方式来描述UI的结构,如上述的build()方法中的代码块。

      • 系统组件:ArkUI框架中默认内置的基础和容器组件,可直接被开发者调用,比如示例中的Row、Column、Text。

      • 属性方法:组件可以通过链式调用配置多项属性,如fontSize()、width()、height()、backgroundColor()等。

      • 事件方法:组件可以通过链式调用设置多个事件的响应逻辑,本例代码不涉及,可以进一步学习文档。


      这里就不是Android熟悉的java或kotlin语言了,编程语言变成了类JavaScript的前端语言,这意味着我们需要适应用前端的思想去开发鸿蒙应用,比如状态管理。


      总结


      本文纯初体验遥遥领先背后的鸿蒙操作系统,基于开发者平台提供的IDE、鸿蒙生态的开发语言ArkTS,通过模拟器运行起来了鸿蒙OS版HelloWorld。对于已经有移动开发经验的同学来说上手可以说非常快,官方文档也非常详尽,ArkTS语法也非常简洁易学,如果大家对华为生态的应用开发感兴趣或者想深入学习借鉴华为做OS和物联网的思路,鸿蒙系统就是一个标杆。


      作者:巫山老妖
      来源:juejin.cn/post/7295576148363886631
      收起阅读 »