注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

环信FAQ

环信FAQ

集成常见问题及答案
RTE开发者社区

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

说说今年的秋招的情况与感受

目前秋招已经过了一段时间,说下我看到的情况和整体感受,仅限于计算机相关专业。 另外,我感受秋招的视角比较特别,今年做了半年的Java面试辅导副业,我是以一种“秋招面试陪跑”的”准老师“身份切入的。 时隔五年,又跟着十几个同学重新经历了一次秋招,有点儿“爷青回”...
继续阅读 »

目前秋招已经过了一段时间,说下我看到的情况和整体感受,仅限于计算机相关专业。


另外,我感受秋招的视角比较特别,今年做了半年的Java面试辅导副业,我是以一种“秋招面试陪跑”的”准老师“身份切入的。


时隔五年,又跟着十几个同学重新经历了一次秋招,有点儿“爷青回”的感觉。


对比去年,依然普天同庆(ai hong bian ye)


很多同学觉得今年疫情阴霾散去,经济复苏,形势一片转好,他们甚至动了冲一冲大厂的念头。但真的到了秋招的时候才发现,24秋招 = 没有迪子的23秋招,转而发下重誓,不进体制誓不为人。


不得不说,那些年,我们一起甩锅的疫情,这次终于自证清白了。


其实,经历了过往十年移动互联网的高速发展,目前流量红利已经见顶,进入到了增长受限的存量时代,这些互联网大厂对于人才的需求远不如以前那么强烈了,甚至从去年就开始一波又一波的“去肥增瘦”。



说完了需求侧,我们再用一张图来说说供给侧,2024年高校毕业生人数达到了1187万人,大概是1000万多点儿的国内毕业生和100多万海归毕业生的总盘子。


理性地思考一下,需求侧的日益饱和 + 供给侧的井喷之势 + 选专业时的追涨杀跌 + 转码时的后知后觉,今年还能形势转好?


我信你个鬼!


再说几个方面的细节:




  • 今年无论你是多牛逼的硕,只要本科不是211 985,有的大厂连笔试机会都不给。




  • 今年的大厂面试官,会因为你没有大厂实习经历而挂掉你,不再培养优秀人才了,希望开箱即用。




  • 今年的技术面真难,如果你回答不好生产环境的压测方案,以及流量激增100倍的解决方案,会直接挂掉你。




  • 今年力扣的算法原题越来越少了,今年的八股文考查源码的越来越多了。




  • 今年貌似没有985保底公司。




  • 3个985本硕目前投了七八十家公司,约面的只有十家,其他的均显示“简历评估中”,感觉企业根本不着急。




你大爷还是你大爷


那种学历牛逼,有大厂实习经历,有参赛获奖经历,技术功底扎实行业内的牛逼人才,依然是大厂offer收割机。



对于这类同学,只要在面试准备期别走偏,只要能正常发挥应有水平,只要别中二地犯了面试官的忌讳,他们能完全摆脱大环境萧条的左右。


接下来他们要做的事情就是选择取舍了,有句话说得还是很有道理的,“选择大于努力,命运大于选择”。


强烈建议,其中的那些能力出众、足够努力,但不善于选择的小镇做题家们,这个时候一定要多问问人,做到谋定后动,行且坚毅。


颈部同学受影响最大


除非整个行业团灭,否则头部同学永远都是稳如泰山的,而离头部同学差一个档位的颈部同学,则受影响最大。


颈部同学的人物画像大概是:



  • 技术储备出色,也有实习经历,但学历并没那么出色的同学。

  • 985本硕,技术储备一般,无实习经历,项目经历出自黑马或尚硅谷的同学。

  • 211本硕,技术储备尚可,有些中小厂实习经历的同学。

  • 名校海归硕,技术储备与中国式校招不match,边吃凉面边扳正认知的同学。

  • 985本硕,技术储备出色,也有实习经历,但沟通能力存在硬伤的同学。


这类同学是过往互联网黄金十年、企业人才扩招的最大受益者,也是现在行业萧条的最大受害者,跟几年前的学长学姐进行比较,则成为了他们最大的精神内耗。


他们会被面试官花式吊打屡屡凉面,他们所泡的池子是汪洋大海,他们阅尽千帆归来却依然0 offer,他们拿到offer的档位和数量会直线下降,他们拿到offer的薪资也没能实现倒挂上届。



有人会说,如果颈部都被影响了,那中部和尾部的同学不是影响更大了吗?


这个未必,中尾部的同学没有那么强的比较心理,在性格上更加随遇而安和知足常乐,甚至早早做好了“大不了转行,干啥不是干”的准备。


这就验证了,忧天的往往不是杞人。


逆商和复盘能力的最好考查


高考虽然可以复读,但浪费一年大好时光的成本过于庞大,因此其“一战定天下”的属性更加强烈。


而秋招面试,你甚至可以在前面挂99次,但只要有一次面试发挥出色,拿到了心仪公司的offer,你就是100%成功的。


因此,在不断的“凉面”和“挂面”中保持心态平和,不抛弃不放弃,认真做好复盘总结,不断完善自己的知识体系,不断提升自己的认知层次,你下次的面试成功率是会叠加的。


记住,乾坤未定,你我皆是黑马,秋招是对逆商和复盘能力的最好考查。



一场与面试官的心理博弈


高考是拿到考卷后的解题模式,秋招虽然也有笔试,但其只是敲门砖,绝不是终极态。


终极态是在两三个小时的面试过程中,迅速得到几个陌生面试官的肯定与认可,这是有很多前期工作需要准备的,往往会涉及到候选人与面试官的心理猜析和博弈,面试话题和节奏控制与反控制。



如果你设计合理,那么面试官会在不知不觉中陷入到你提前安排好的布局中。


这里举一个简历当中项目选型的例子:


有些同学为了充分体现其做的项目有技术含量,硬往简历上放手写RPC框架、消息队列、分布式缓存、仿滴滴打车,仿MySQL RDBMS之类的。


这有些乍一看挺唬人,但其实给自己埋下了不小的坑。因为这种颇具技术含量的项目,最大的问题就是它的深度和广度不收敛,你很难hold住。


所以,除非你确实深谙此道,否则就等着被各家公司的面试官,以各种姿势花式吊打吧。


而那些聪明的,善于与面试官博弈的同学,早就准备好了几个有些难度、但技术深度和广度可控的项目。在面试的时候,他就可以顺理成章地将面试官带入到自己所熟悉的八股文技术点中,最终成为了offer收割机。


写给那些心态崩了的同学




  • 有人说,秋招让他明白,读书的目的只是为了换文凭;




  • 有人说,秋招让他明白,人真的要学会接受自己的普通,要学会取悦自己;




  • 有人说,秋招让他明白,倘若是因为读书而耽误了正事,那么读书就是玩物丧志;




  • 有人说,秋招是应届生的头等大事,一旦错过了秋招,我的人生完蛋了;




对于那些心态崩了的同学,我要说的是:


人生中最辉煌的时刻,绝对不是你功成名就的那天,而是你坠入绝望之谷后,重新燃起挑战人生的欲望,再次义无反顾地踏上征程的那天。


作者:库森学长
来源:juejin.cn/post/7279313746450530315
收起阅读 »

为什么5.225.toFixed(2)!=5.23,令人摸不着头脑的银行家舍入法

web
前言 很多时候,我们在程序中计算数字,得到的结果也许并不和我们想象得一样,在我们大多数人的认知里几乎都是四舍五入法,但是程序中所呈现的好像并不是我们想要的结果。 今天就谈谈程序中的那匪夷所思得银行家舍入法(也会涉及到数字精度问题) 什么是银行家舍入法 银行家舍...
继续阅读 »

前言


很多时候,我们在程序中计算数字,得到的结果也许并不和我们想象得一样,在我们大多数人的认知里几乎都是四舍五入法,但是程序中所呈现的好像并不是我们想要的结果。

今天就谈谈程序中的那匪夷所思得银行家舍入法(也会涉及到数字精度问题)


什么是银行家舍入法


银行家舍入法,也称为四舍六入五留双或四舍六入五成双,是一种在计算机科学和金融领域广泛使用的舍入方法。


具体操作步骤如下:



  1. 如果被修约的数字小于5,则直接舍去;

  2. 如果被修约的数字大于5,则进行进位;

  3. 如果被修约的数字等于5,则需要查看5前面的数字。如果5前面的数字是奇数,则进位;如果5前面的数字是偶数,则舍去5,即修约后末尾数字都成为偶数。特别需要注意的是,如果5的后面还有不为0的任何数,则无论5的前面是奇数还是偶数,均应进位。


以上可以看出银行家舍入法得规则,当为5时,并不是所有得都会向前进一位,所以就可以知道5.225.toFixed(2)为什么不等于5.23了


举例


在浏览器的控制台中,我们可以试着打印一下


image.png
这个时候我们可以看到,哎,好像是符合我们的所认知得四舍五入法了,但是紧接着


image.png
这里看出,怎么又变成这样的了,这还是银行家舍入法呀,为了更严谨再试一下5前面为奇数时得结果


image.png
这里结果又变了,反而是整数大于等于4得正常了,但是小于4得又有些失常了,反而整数为1得总是按照咱们预想的结果在进行,这种结果让我大脑一片混乱,所以这到底是什么原因,导致结果不像是银行家舍入法,也不像是四舍五入法


在我掉了一花西币的头发后,终于想通了,是程序中的精度问题,我们所写的数字并不是表面那么纯粹,再次打印一下看看


image.png
现在可以清楚看出,我们所写的简单的数字后面并不见简单,之所以1.235和1.225使用toFiexd的时候都准确的四舍五入了,都是因为他的后面是多出来了0.0000000000几的数字,然而2.235就没有那么幸运了,所以2.235的0.005就被舍弃了!


解决方法


先说一种可行但不完全可行的解决方法,就是使用Math.round()
首先这个方法确实是js中提供的真正含义上的四舍五入的方法。


image.png
哎,这么一看,确实可行,既然简单的可以,那我们就试着进行复杂运算一下,再保留一下两位小数试试看


image.png
呕吼,错了,按我们正常来算应该是9.77,但却得到了9.76。

要知道程序中存在着精度问题,再我们算来这个式子的结果应该是9.765,但是在程序看来


image.png
可以说是无限趋近于9.765但还没有达到,然后就在Math.round这个方法中给舍弃掉了,这个方法似乎不完全可行


那么另外一招就是可行但有隐式风险的方式,就是在我们所算出来的结果后面添加0.0000000001,这样再让我们看一下结果


image.png
这样可以看出,无论使用哪种方法,都能达到我们所需的结果了,即使使用toFixed有了银行家舍入法的规则,依旧可以按我们所想的一样进行四舍五入,因为当我们加了0.000000001后,即使最后一位等于5了,5后面还有数字,它就会向前进一位,那如果说加了这0.000000001正好等于5然后又触发了银行家舍入法的规则,那只能说算你倒霉,这就是我说为什么会有隐式风险,有风险但很小。


当然还有一个方法就是自己写一个方法来解决这个问题


//有的时候也许传的参数就是计算过后的,无线趋近于5的数,可以根据需求来判断是否传入第二个参数
Number.prototype.myToFixed = function (n, d) {
//进来之后转为字符串 字符串不存在精度问题
const str = this.toString();
const dotIndex = str.indexOf(".");
//如果没有小数点传进来的就是整数,直接使用toFixed传出去
if (dotIndex === -1) {
return this.toFixed(n);
}
//当为小数的时候
const intStr = str.substring(0, dotIndex);
const decStr = str.substring(dotIndex + 1, str.length).split("");
//当大于5时,就进一
if (decStr[n] >= 5) {
decStr[n - 1] = Number(decStr[n - 1]) + 1;
const dec = decStr.slice(0, n).join("");
return `${intStr}.${dec}`;
} else {
//否则小于五时 先判断是否有第二个参数
if (d) {
//如果有就截取到第二个参数的位置
const newDec = decStr.splice(n, n + d);
let nineSum = 0;
//遍历循环有多少个9
for (let index = 0; index < newDec.length; index++) {
if (index != 0 && newDec[index] == 9) {
nineSum++;
}
}
//判断四舍五入后面的位置 是否为四 并且是否除了4之后全是9 或者 9的位数大于第二个传的参数
if (newDec[0] == 4 && (nineSum >= newDec.length - 2 || nineSum >= d)) {
//条件成立 就按5进一
decStr[n - 1] = Number(decStr[n - 1]) + 1;
const dec = decStr.slice(0, n).join("");
return `${intStr}.${dec}`;
} else {
//不成立则舍一
const dec = decStr.slice(0, n).join("");
return `${intStr}.${dec}`;
}
} else {
//没有第二个参数,小于五直接舍一
const dec = decStr.slice(0, n).join("");
return `${intStr}.${dec}`;
}
}
};

我们再进行测试一下


image.png


image.png
这样就是我们想要的结果了


总结


在程序中,银行家舍入法和数字的精度问题很多时候都会遇见,不论前端还是后端,然而处理这些数据也是比较头疼的事,我所讲的这些也许不能满足所有情况,但大多数情况都是可以处理的。


如果是相对于银行里这种对数字比较敏感的环境,这些参数的处理还需要更加谨慎的处理


写的如有问题,欢迎提出建议


作者:iceCode
来源:juejin.cn/post/7280430881952759862
收起阅读 »

你知道抖音的IP归属地是怎么实现的吗

1.背景 最近刷抖音发现上线了 IP 属地的功能,小伙伴在发表动态、发表评论以及聊天的时候,都会显示自己的 IP 属地信息,其核心意义是让用户更具有真实性,减少虚假欺骗事件。正好最近本人开发获取客户端ip,做一些接口限流,黑白名单等需求功能,顺路就研究了一下怎...
继续阅读 »

1.背景


最近刷抖音发现上线了 IP 属地的功能,小伙伴在发表动态、发表评论以及聊天的时候,都会显示自己的 IP 属地信息,其核心意义是让用户更具有真实性,减少虚假欺骗事件。正好最近本人开发获取客户端ip,做一些接口限流,黑白名单等需求功能,顺路就研究了一下怎么解析IP获取归属地问题。


接下来,就着重讲解一下Java后端怎么实现IP归属地的功能,其实只需要以下两大步骤:


2.获取客户端ip接口


做过web开发都知道,无论移动端还是pc端的请求接口都会被封装成为一个HttpServletRequest对象,该对象包含了客户端请求信息包括请求的地址,请求的参数,提交的数据等等。


如果服务器直接把IP暴漏出去,那么request.getRemoteAddr()就能拿到客户端ip。


但目前流行的架构中,基本上服务器都不会直接把自己的ip暴漏出去,一般前面还有一层或多层反向代理,常见的nginx居多。 加了代理后,相当于服务器和客户端中间还有一层,这时·request.getRemoteAddr()拿到的就是代理服务器的ip了,并不是客户端的ip。所以这种情况下,一般会在转发头上加X-Forwarded-For等信息,用来跟踪原始客户端的ip。


X-Forwarded-For: 这是一个 Squid 开发的字段,只有在通过了HTTP代理或者负载均衡服务器时才会添加该项。 格式为X-Forwarded-For:client1,proxy1,proxy2,一般情况下,第一个ip为客户端真实ip,后面的为经过的代理服务器ip。 上面的代码注释也说的很清楚,直接截取拿到第一个ip。 Proxy-Client-IP/WL- Proxy-Client-IP: 这个一般是经过apache http服务器的请求才会有,用apache http做代理时一般会加上Proxy-Client-IP请求头,而WL-Proxy-Client-IP是他的weblogic插件加上的头。这种情况也是直接能拿到。 HTTP_CLIENT_IP: 有些代理服务器也会加上此请求头。 X-Real-IP: nginx一般用这个。


但是在日常开发中,并没有规范规定用以上哪一个头信息去跟踪客户端,所以都有可能,只能一一尝试,直到获取到为止。代码如下:


@Slf4j
public class IpUtils {

   private static final String UNKNOWN_VALUE = "unknown";
   private static final String LOCALHOST_V4 = "127.0.0.1";
   private static final String LOCALHOST_V6 = "0:0:0:0:0:0:0:1";

   private static final String X_FORWARDED_FOR = "X-Forwarded-For";
   private static final String X_REAL_IP = "X-Real-IP";
   private static final String PROXY_CLIENT_IP = "Proxy-Client-IP";
   private static final String WL_PROXY_CLIENT_IP = "WL-Proxy-Client-IP";
   private static final String HTTP_CLIENT_IP = "HTTP_CLIENT_IP";

   private static final String IP_DATA_PATH = "/Users/shepherdmy/Desktop/ip2region.xdb";
   private static  byte[] contentBuff;
 
  /**
    * 获取客户端ip地址
    * @param request
    * @return
    */
   public static String getRemoteHost(HttpServletRequest request) {
       String ip = request.getHeader(X_FORWARDED_FOR);
       if (StringUtils.isNotEmpty(ip) && !UNKNOWN_VALUE.equalsIgnoreCase(ip)) {
           // 多次反向代理后会有多个ip值,第一个ip才是真实ip
           int index = ip.indexOf(",");
           if (index != -1) {
               return ip.substring(0, index);
          } else {
               return ip;
          }
      }
       ip = request.getHeader(X_REAL_IP);
       if (StringUtils.isNotEmpty(ip) && !UNKNOWN_VALUE.equalsIgnoreCase(ip)) {
           return ip;
      }
       if (StringUtils.isBlank(ip) || UNKNOWN_VALUE.equalsIgnoreCase(ip)) {
           ip = request.getHeader(PROXY_CLIENT_IP);
      }
       if (StringUtils.isBlank(ip) || UNKNOWN_VALUE.equalsIgnoreCase(ip)) {
           ip = request.getHeader(WL_PROXY_CLIENT_IP);
      }
       if (StringUtils.isBlank(ip) || UNKNOWN_VALUE.equalsIgnoreCase(ip)) {
           ip = request.getRemoteAddr();
      }

       if (StringUtils.isBlank(ip) || UNKNOWN_VALUE.equalsIgnoreCase(ip)) {
           ip = request.getHeader(HTTP_CLIENT_IP);
      }

       if (StringUtils.isBlank(ip) || UNKNOWN_VALUE.equalsIgnoreCase(ip)) {
           ip = request.getRemoteAddr();
      }
       return ip.equals(LOCALHOST_V6) ? LOCALHOST_V4 : ip;
  }

}


项目推荐:基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba企业级系统架构底层框架封装,解决业务开发时常见的非功能性需求,防止重复造轮子,方便业务快速开发和企业技术栈框架统一管理。引入组件化的思想实现高内聚低耦合并且高度可配置化,做到可插拔。严格控制包依赖和统一版本管理,做到最少化依赖。注重代码规范和注释,非常适合个人学习和企业使用


Github地址github.com/plasticene/…


Gitee地址gitee.com/plasticene3…


微信公众号Shepherd进阶笔记


交流探讨群:Shepherd_126



3.获取ip归属地


通过上面我们就能获取到客户端用户的ip地址,接下来就可以通过ip解析获取归属地了。


如果我们在网上搜索资料教程,大部分都是说基于各大平台(eg:淘宝,新浪)提供的ip库进行查询,不过不难发现这些平台已经不怎么维护这个功能,现在处于“半死不活”的状态,根本不靠谱,当然有些平台提供可靠的获取ip属地接口,但是收费、收费、收费


本着作为一个程序员的严谨:“能白嫖的就白嫖,避免出现要买的是你,不会用也是你的尴尬遭遇”。扯远了言归正传,为了寻求可靠有效的解决方案,只能去看看github有没有什么项目能满足需求,果然功夫不负有心人,发现一个宝藏级项目:ip2region,一个准确率 99.9% 的离线 IP 地址定位库,0.0x 毫秒级查询,ip2region.db 数据库只有数 MB的项目,提供了众多主流编程语言的 xdb 数据生成和查询客户端实现,这里只能说:开源真香,开源万岁。


3.1 Ip2region 特性


标准化的数据格式


每个 ip 数据段的 region 信息都固定了格式:国家|区域|省份|城市|ISP,只有中国的数据绝大部分精确到了城市,其他国家部分数据只能定位到国家,其余选项全部是0。


数据去重和压缩


xdb 格式生成程序会自动去重和压缩部分数据,默认的全部 IP 数据,生成的 ip2region.xdb 数据库是 11MiB,随着数据的详细度增加数据库的大小也慢慢增大。


极速查询响应


即使是完全基于 xdb 文件的查询,单次查询响应时间在十微秒级别,可通过如下两种方式开启内存加速查询:



  1. vIndex 索引缓存 :使用固定的 512KiB 的内存空间缓存 vector index 数据,减少一次 IO 磁盘操作,保持平均查询效率稳定在10-20微秒之间。

  2. xdb 整个文件缓存:将整个 xdb 文件全部加载到内存,内存占用等同于 xdb 文件大小,无磁盘 IO 操作,保持微秒级别的查询效率。


IP 数据管理框架


v2.0 格式的 xdb 支持亿级别的 IP 数据段行数,region 信息也可以完全自定义,例如:你可以在 region 中追加特定业务需求的数据,例如:GPS信息/国际统一地域信息编码/邮编等。也就是你完全可以使用 ip2region 来管理你自己的 IP 定位数据。


99.9% 准确率


数据聚合了一些知名 ip 到地名查询提供商的数据,这些是他们官方的的准确率,经测试着实比经典的纯真 IP 定位准确一些。


ip2region 的数据聚合自以下服务商的开放 API 或者数据(升级程序每秒请求次数 2 到 4 次):



备注:如果上述开放 API 或者数据都不给开放数据时 ip2region 将停止数据的更新服务。


3.2 整合Ip2region客户端进行查询


提供了众多主流编程语言的 xdb 数据生成和查询客户端实现,已经集成的客户端有:java、C#、php、c、python、nodejs、php扩展(php5 和 php7)、golang、rust、lua、lua_c,nginx。这里讲一下java的客户端。


首先我们需要引入依赖:


<dependency>
 <groupId>org.lionsoul</groupId>
 <artifactId>ip2region</artifactId>
 <version>2.6.5</version>
</dependency>

接下来我们需要先去下载数据文件ip2region.xdb到本地,然后基于数据文件进行查询,下面查询方法文件路径改为你本地路径即可,ip2region提供三种查询方式:


完全基于文件的查询


import org.lionsoul.ip2region.xdb.Searcher;
import java.io.*;
import java.util.concurrent.TimeUnit;

public class SearcherTest {
   public static void main(String[] args) {
       // 1、创建 searcher 对象
       String dbPath = "ip2region.xdb file path";
       Searcher searcher = null;
       try {
           searcher = Searcher.newWithFileOnly(dbPath);
      } catch (IOException e) {
           System.out.printf("failed to create searcher with `%s`: %s\n", dbPath, e);
           return;
      }

       // 2、查询
       try {
           String ip = "1.2.3.4";
           long sTime = System.nanoTime();
           String region = searcher.search(ip);
           long cost = TimeUnit.NANOSECONDS.toMicros((long) (System.nanoTime() - sTime));
           System.out.printf("{region: %s, ioCount: %d, took: %d μs}\n", region, searcher.getIOCount(), cost);
      } catch (Exception e) {
           System.out.printf("failed to search(%s): %s\n", ip, e);
      }

       // 3、关闭资源
       searcher.close();
       
       // 备注:并发使用,每个线程需要创建一个独立的 searcher 对象单独使用。
  }
}

缓存 VectorIndex 索引


我们可以提前从 xdb 文件中加载出来 VectorIndex 数据,然后全局缓存,每次创建 Searcher 对象的时候使用全局的 VectorIndex 缓存可以减少一次固定的 IO 操作,从而加速查询,减少 IO 压力。


import org.lionsoul.ip2region.xdb.Searcher;
import java.io.*;
import java.util.concurrent.TimeUnit;

public class SearcherTest {
   public static void main(String[] args) {
       String dbPath = "ip2region.xdb file path";

       // 1、从 dbPath 中预先加载 VectorIndex 缓存,并且把这个得到的数据作为全局变量,后续反复使用。
       byte[] vIndex;
       try {
           vIndex = Searcher.loadVectorIndexFromFile(dbPath);
      } catch (Exception e) {
           System.out.printf("failed to load vector index from `%s`: %s\n", dbPath, e);
           return;
      }

       // 2、使用全局的 vIndex 创建带 VectorIndex 缓存的查询对象。
       Searcher searcher;
       try {
           searcher = Searcher.newWithVectorIndex(dbPath, vIndex);
      } catch (Exception e) {
           System.out.printf("failed to create vectorIndex cached searcher with `%s`: %s\n", dbPath, e);
           return;
      }

       // 3、查询
       try {
           String ip = "1.2.3.4";
           long sTime = System.nanoTime();
           String region = searcher.search(ip);
           long cost = TimeUnit.NANOSECONDS.toMicros((long) (System.nanoTime() - sTime));
           System.out.printf("{region: %s, ioCount: %d, took: %d μs}\n", region, searcher.getIOCount(), cost);
      } catch (Exception e) {
           System.out.printf("failed to search(%s): %s\n", ip, e);
      }
       
       // 4、关闭资源
       searcher.close();

       // 备注:每个线程需要单独创建一个独立的 Searcher 对象,但是都共享全局的制度 vIndex 缓存。
  }
}

缓存整个 xdb 数据


我们也可以预先加载整个 ip2region.xdb 的数据到内存,然后基于这个数据创建查询对象来实现完全基于文件的查询,类似之前的 memory search。


import org.lionsoul.ip2region.xdb.Searcher;
import java.io.*;
import java.util.concurrent.TimeUnit;

public class SearcherTest {
   public static void main(String[] args) {
       String dbPath = "ip2region.xdb file path";

       // 1、从 dbPath 加载整个 xdb 到内存。
       byte[] cBuff;
       try {
           cBuff = Searcher.loadContentFromFile(dbPath);
      } catch (Exception e) {
           System.out.printf("failed to load content from `%s`: %s\n", dbPath, e);
           return;
      }

       // 2、使用上述的 cBuff 创建一个完全基于内存的查询对象。
       Searcher searcher;
       try {
           searcher = Searcher.newWithBuffer(cBuff);
      } catch (Exception e) {
           System.out.printf("failed to create content cached searcher: %s\n", e);
           return;
      }

       // 3、查询
       try {
           String ip = "1.2.3.4";
           long sTime = System.nanoTime();
           String region = searcher.search(ip);
           long cost = TimeUnit.NANOSECONDS.toMicros((long) (System.nanoTime() - sTime));
           System.out.printf("{region: %s, ioCount: %d, took: %d μs}\n", region, searcher.getIOCount(), cost);
      } catch (Exception e) {
           System.out.printf("failed to search(%s): %s\n", ip, e);
      }
       
       // 4、关闭资源 - 该 searcher 对象可以安全用于并发,等整个服务关闭的时候再关闭 searcher
       // searcher.close();

       // 备注:并发使用,用整个 xdb 数据缓存创建的查询对象可以安全的用于并发,也就是你可以把这个 searcher 对象做成全局对象去跨线程访问。
  }
}

3.3 springboot整合示例


首先我们也需要像上面一样引入maven依赖。然后就可以基于上面的查询方式进行封装成工具类了,我这里选择了上面的第三种方式:缓存整个 xdb 数据


@Slf4j
public class IpUtils {
   private static final String IP_DATA_PATH = "/Users/shepherdmy/Desktop/ip2region.xdb";
   private static  byte[] contentBuff;

   static {
       try {
           // 从 dbPath 加载整个 xdb 到内存。
           contentBuff = Searcher.loadContentFromFile(IP_DATA_PATH);
      } catch (IOException e) {
           e.printStackTrace();
      }
  }
 
     /**
    * 根据ip查询归属地,固定格式:中国|0|浙江省|杭州市|电信
    * @param ip
    * @return
    */
   public static IpRegion getIpRegion(String ip) {
       Searcher searcher = null;
       IpRegion ipRegion = new IpRegion();
       try {
           searcher = Searcher.newWithBuffer(contentBuff);
           String region = searcher.search(ip);
           String[] info = StringUtils.split(region, "|");
           ipRegion.setCountry(info[0]);
           ipRegion.setArea(info[1]);
           ipRegion.setProvince(info[2]);
           ipRegion.setCity(info[3]);
           ipRegion.setIsp(info[4]);
      } catch (Exception e) {
           log.error("get ip region error: ", e);
      } finally {
           if (searcher != null) {
               try {
                   searcher.close();
              } catch (IOException e) {
                   log.error("close searcher error:", e);
              }
          }
      }
       return ipRegion;
  }

}

作者:shepherd111
来源:juejin.cn/post/7280118836685668367
收起阅读 »

Metal每日分享,不同色彩空间转换滤镜效果

iOS
本案例的目的是理解如何用Metal实现色彩空间转换效果滤镜,转换在不同色彩空间生成的图像; Demo HarbethDemo地址iDay每日分享文档地址 实操代码// 色彩空间转换滤镜 let filter = C7ColorSpace.init(with:...
继续阅读 »

本案例的目的是理解如何用Metal实现色彩空间转换效果滤镜,转换在不同色彩空间生成的图像;




Demo



实操代码

// 色彩空间转换滤镜
let filter = C7ColorSpace.init(with: .rgb_to_yuv)

// 方案1:
ImageView.image = try? BoxxIO(element: originImage, filters: [filter, filter2, filter3]).output()

// 方案2:
ImageView.image = originImage.filtering(filter, filter2, filter3)

// 方案3:
ImageView.image = originImage ->> filter ->> filter2 ->> filter3

效果对比图


  • 不同参数下效果

    实现原理


  • 过滤器


这款滤镜采用并行计算编码器设计.compute(kernel: type.rawValue)

/// 色彩空间转换
public struct C7ColorSpace: C7FilterProtocol {

public enum SwapType: String, CaseIterable {
case rgb_to_yiq = "C7ColorSpaceRGB2YIQ"
case yiq_to_rgb = "C7ColorSpaceYIQ2RGB"
case rgb_to_yuv = "C7ColorSpaceRGB2YUV"
case yuv_to_rgb = "C7ColorSpaceYUV2RGB"
}

private let type: SwapType

public var modifier: Modifier {
return .compute(kernel: type.rawValue)
}

public init(with type: SwapType) {
self.type = type
}
}

  • 着色器

每条通道乘以各自偏移求和得到Y,用Y作为新的像素rgb;

kernel void C7ColorSpaceRGB2Y(texture2d<half, access::write> outputTexture [[texture(0)]],
texture2d<half, access::read> inputTexture [[texture(1)]],
uint2 grid [[thread_position_in_grid]]) {
const half4 inColor = inputTexture.read(grid);

const half Y = half((0.299 * inColor.r) + (0.587 * inColor.g) + (0.114 * inColor.b));
const half4 outColor = half4(Y, Y, Y, inColor.a);

outputTexture.write(outColor, grid);
}

// See: https://en.wikipedia.org/wiki/YIQ
kernel void C7ColorSpaceRGB2YIQ(texture2d<half, access::write> outputTexture [[texture(0)]],
texture2d<half, access::read> inputTexture [[texture(1)]],
uint2 grid [[thread_position_in_grid]]) {
const half4 inColor = inputTexture.read(grid);

const half3x3 RGBtoYIQ = half3x3({0.299, 0.587, 0.114}, {0.596, -0.274, -0.322}, {0.212, -0.523, 0.311});
const half3 yiq = RGBtoYIQ * inColor.rgb;
const half4 outColor = half4(yiq, inColor.a);

outputTexture.write(outColor, grid);
}

kernel void C7ColorSpaceYIQ2RGB(texture2d<half, access::write> outputTexture [[texture(0)]],
texture2d<half, access::read> inputTexture [[texture(1)]],
uint2 grid [[thread_position_in_grid]]) {
const half4 inColor = inputTexture.read(grid);

const half3x3 YIQtoRGB = half3x3({1.0, 0.956, 0.621}, {1.0, -0.272, -0.647}, {1.0, -1.105, 1.702});
const half3 rgb = YIQtoRGB * inColor.rgb;
const half4 outColor = half4(rgb, inColor.a);

outputTexture.write(outColor, grid);
}

// See: https://en.wikipedia.org/wiki/YUV
kernel void C7ColorSpaceRGB2YUV(texture2d<half, access::write> outputTexture [[texture(0)]],
texture2d<half, access::read> inputTexture [[texture(1)]],
uint2 grid [[thread_position_in_grid]]) {
const half4 inColor = inputTexture.read(grid);

const half3x3 RGBtoYUV = half3x3({0.299, 0.587, 0.114}, {-0.299, -0.587, 0.886}, {0.701, -0.587, -0.114});
const half3 yuv = RGBtoYUV * inColor.rgb;
const half4 outColor = half4(yuv, inColor.a);

outputTexture.write(outColor, grid);
}

kernel void C7ColorSpaceYUV2RGB(texture2d<half, access::write> outputTexture [[texture(0)]],
                                texture2d<half, access::read> inputTexture [[texture(1)]],
                                uint2 grid [[thread_position_in_grid]]) {
    const half4 inColor = inputTexture.read(grid);

    const half3x3 YUVtoRGB = half3x3({1.0, 0.0, 1.28033}, {1.0, -0.21482, -0.38059}, {1.0, 2.21798, 0.0});
    const half3 rgb = YUVtoRGB * inColor.rgb;
    const half4 outColor = half4(rgb, inColor.a);

    outputTexture.write(outColor, grid);
}

色彩空间


  • YIQ

在YIQ系统中,是NTSC(National Television Standards Committee)电视系统标准;


  • Y是提供黑白电视及彩色电视的亮度信号Luminance,即亮度Brightness;
  • I代表In-phase,色彩从橙色到青色;
  • Q代表Quadrature-phase,色彩从紫色到黄绿色;



转换公式如下:




  • YUV

YUV是在工程师想要在黑白基础设施中使用彩色电视时发明的。他们需要一种信号传输方法,既能与黑白 (B&W) 电视兼容,又能添加颜色。亮度分量已经作为黑白信号存在;他们将紫外线信号作为解决方案添加到其中。

由于 U 和 V 是色差信号,因此在直接 R 和 B 信号上选择色度的 UV 表示。换句话说,U 和 V 信号告诉电视在不改变亮度的情况下改变某个点的颜色。
或者 U 和 V 信号告诉显示器以牺牲另一种颜色为代价使一种颜色更亮,以及它应该移动多少。
U 和 V 值越高(或负值越低),斑点的饱和度(色彩)就越高。
U 值和 V 值越接近零,颜色偏移越小,这意味着红、绿和蓝光的亮度会更均匀,从而产生更灰的点。
这是使用色差信号的好处,即不是告诉颜色有多少红色,而是告诉红色比绿色或蓝色多多少。
反过来,这意味着当 U 和 V 信号为零或不存在时,它只会显示灰度图像。
如果使用 R 和 B,即使在黑白场景中,它们也将具有非零值,需要所有三个数据承载信号。
这在早期的彩色电视中很重要,因为旧的黑白电视信号没有 U 和 V 信号,这意味着彩色电视开箱后只会显示为黑白电视。
此外,黑白接收器可以接收 Y' 信号并忽略 U 和 V 颜色信号,使 YUV 向后兼容所有现有的黑白设备、输入和输出。
如果彩色电视标准不使用色差信号,这可能意味着彩色电视会从 B& 中产生有趣的颜色 W 广播,否则需要额外的电路将黑白信号转换为彩色。
有必要为色度通道分配较窄的带宽,因为没有可用的额外带宽。
如果某些亮度信息是通过色度通道到达的(如果使用 RB 信号而不是差分 UV 信号,就会出现这种情况),黑白分辨率就会受到影响。

YUV 模型定义了一个亮度分量 (Y),表示物理线性空间亮度,以及两个色度分量,分别称为 U(蓝色投影)和 V(红色投影)。它可用于在 RGB 模型之间进行转换,并具有不同的颜色空间




转换公式如下:




最后


  • 慢慢再补充其他相关滤镜,喜欢就给我点个星🌟吧。

✌️.


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

烟雨蒙蒙的三月

金三银四好像失效了 从去年下半年开始,互联网寒冬就总是萦绕在耳边。大量的公司倒闭、裁员。本以为等疫情过,等春天,等金三银四,一切又会变得好起来。但是站在这个本应该金光闪闪的时刻,却是无人问津。那个我们希望的春天似乎没有到来。 你是否也会像我一样焦虑 从业...
继续阅读 »
金三银四好像失效了


从去年下半年开始,互联网寒冬就总是萦绕在耳边。大量的公司倒闭、裁员。本以为等疫情过,等春天,等金三银四,一切又会变得好起来。但是站在这个本应该金光闪闪的时刻,却是无人问津。那个我们希望的春天似乎没有到来。



你是否也会像我一样焦虑


从业六年多,但是最近将近两年都在中间件行业,技术、履历都算不上优秀。年纪每一年都在增长,而我有随着年纪一起快速增长吗?我想是没有的。年后公司部门会议说到部门发展,领导说我们的产品越发稳定,对于一个中间件来说,客户需要的就是稳定,太多功能对于他们来说是无用的。这就意味着我们的产品到头了。但是这个产品到头我们再做什么呢?没人给我们答案。



要跳出当前的圈子吗


如果在这里看不见曙光,那么在别的地方是不是能有希望呢?春天,万物复苏,楼下如枯骨林立的一排排树干纷纷长出绿芽,迸发生机。我是否可以像沉寂了一整个冬天的枯树一样迎来自己的春天呢?目前看来也是没有的。投了很多家的简历,犹如石沉大海了无音讯。不知道对于别人来说是怎样,但是对我而言,这个三月并不是春天。



会有春天吗


去年我第一次为开源社区贡献了自己的代码,我觉得我变得更好了。疫情也在年底宣布画上句号,春天似乎真的要来了。物理上的春天是到来了,可是那个我们期盼的春天它真的会到来吗?总是在期盼等一等一切就会好转,因为除了等,我们似乎也并没有太多的选择。时代的轮盘一直运转,无数人的命运随之沉浮。我们更多的只能逆来顺受,接受它的变化,并随之拥抱它。可是未知的未来总是让人充满惶恐,看看自己再过两年就三十了,未婚、未育。在本就三十五岁魔咒的行业,总是惴惴不安。我总是思考如果被这个行业抛弃,不做开发我又能做什么呢?如果是你,这个答案会是什么呢?



这个文章应该有个结尾


文章总是需要结尾的,生活不是。生活还需要继续,每个人的答案都需要自己去寻找。在茫然无措的时刻,只能自己去寻找一些解药,在心绪不宁的时候,学习什么也是学不进去的。最近在看《我的解放日记》,能够缓解一些我的焦虑情绪。如果你也需要一些治愈系的剧,也可以去看看它。时代的浪潮推着我们往前,我们惶恐不安,手足无措,这都不是我们的错,我们只能尽力做好能做的。但是那些决定命运的瞬间几乎都是我们不能选择的。能活着就已经很不错了。


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

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

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

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


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


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


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


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


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


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


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


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


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


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


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


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


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

如何治愈拖延症

如何治愈拖延症 背景 最近发现我的拖延症很严重了😭😭,看了一下我的抖音主页,我已经很久没有去跑步了。最近的一次跑步的记录停留在了8月23日,周三。我的这篇文章写在周天的上午,掐指一算,已经有三天晚上没有跑步了。我不大喜欢给自己找借口,没有行动就是没有行动。 ...
继续阅读 »

如何治愈拖延症


背景


最近发现我的拖延症很严重了😭😭,看了一下我的抖音主页,我已经很久没有去跑步了。最近的一次跑步的记录停留在了8月23日,周三。我的这篇文章写在周天的上午,掐指一算,已经有三天晚上没有跑步了。我不大喜欢给自己找借口,没有行动就是没有行动。




就拿我昨天晚上来说吧,吃完饭已经是8点了,这个点没啥问题。和家里通了半小时的电话之后,发现手机没电了,于是又在充电。等到九点的时候,电池的电量还在30%左右,我知道我的手机电池不大行,不足以支撑一个小时,于是就放弃了😅。


但是当早上我坐在电脑前的时候,发现昨天的好多事情都没有完成,今天的事情又得往后推了。越堆积越是多,都喘不过气来了🤥。



哈哈🤭🤭,也不好意思让大家看到下周的推文内容啦,算是提前剧透了😎





我就不断的在思考,为什么我的执行力不行了。我觉得我的代言词就是:一个有思想有行动力的程序员。现在看来,我是一个懒惰、带有严重的拖延症的程序员了。不行,这个问题得治,不然我会更加的焦虑,堆积更多的任务导致更低的效率。


分析


结合这个低效率的周末,我反思了我为什么效率这么低。


🕢推迟开始


我发现我总喜欢做todo list,但是很少去看,也很少去核对一下我当前的进度。总觉得一天的时间很长,我可以先去做别的事情,比如碎片化的短视频、吃吃吃、发呆。于是一件件的本在计划中的事情被不断的推迟了。


⏲时间管理困难


从我8:00起来到晚上的凌晨入睡,减去我个人清洁、做饭、午睡,我剩下的时间大约是10个小时。但是,我一对比下来,我的时间利用率仅仅是40%,相当于我只有4个小时是在满满当当的学习的。我之前的ipad在的时候,我会用潮汐这个软件把我的时间分割成一个小时一个小时的。现在没了,我发现我的时间规划真的出了大问题。


🤖自我控制力下降


我觉得最近一年的时间,我真的太放松自我了。我的技术成长、学习上长进也是微乎其微。我总结下来就是因为我的自控力太差了,或者说没有承受着外界的干扰。因为一个短视频就可以刷上一个小时的短视频,因为一个好物就会不断的逛购物软件......碎片化的时间消耗,最终导致了效率低下。


解决方案


针对以上我总结的问题,我决定对症下药。


🧾明确的计划


我觉得我明确的计划真的很必要。就像我公众号shigen里面给自己定的一个目标一样:



2023年的8月开始,我先给自己定一个小目标:公众号文章不停更





“不停更”的意思是我每天都要更新文章。我的推文里还带了“新闻早知道”栏目,我哪天没更新或者说更新晚了,我就觉得目标没有实现了,新闻也没什么意义了。我觉得日常的计划和这个目标的设定和实现有着相似的地方,我要把我的计划和目标更明确一点。🤔🤔比方说我今天要干嘛,我完成了怎么样了。


优先级


事情分清楚轻重缓急,我记得我在实习的时候,就有一次因为项目要上线和我一点不大紧要的事情次序搞混了,导致晚上加班上线。现在的我也是,很多重要的事情也是放到了最后做甚至只延期了。所以,我的行动之前,得先做最要紧的事情。但是也会混杂一些个人的情绪在里边,比方说明明一件事情很重要,但是自己就是不想做或者说觉得事情很简单,我先做最有意思的事情。很多时候都是这样的,兴趣和意义占据了主导因素,优先级反而不是那么重要了。


抗拒干扰


手机就在我的边上,这很难不因为一个消息或者一个发愣就去拿起手机,一旦拿起来就放不下了。所以,我觉得最好就是把它放在我的抽屉里,然后眼不见就不去想它了。


奖励惩罚机制


最后,我觉得奖罚分明也挺重要的。在这里,我也想起了我在一线的时候,我周末总会有一天去我住的地方隔壁去逛超市,每次的消费金额大约在100-150左右。但是我出去的前提是我的学习目标完成了或者代码写完了。我现在却相反,目标缺少了一个验收和奖惩的过程。我觉得和我更喜欢宅有一点关系了,所以,我也得奖励我自己一下:目标完成了可以去逛超市消费🛒,也可以去骑行🚲;但是没完成,健腹轮😭😭安排上!


好了,以上就是我对于最近的拖延症的分析和解决方式的思考了。也欢迎伙伴们在评论区交流一下自己对于拖延症的看法。


shigen一起,每天不一样!


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

Swift - LeetCode - 二叉树的所有路径

iOS
题目 给你一个二叉树的根节点 root,按 任意顺序,返回所有从根节点到叶子节点的路径。 叶子节点 是指没有子节点的节点。 示例 1: 输入:root = [1,2,3,null,5]输出:["1->2->5","1->3"] 示例 2:...
继续阅读 »

题目


给你一个二叉树的根节点 root,按 任意顺序,返回所有从根节点到叶子节点的路径。


叶子节点 是指没有子节点的节点。


示例 1:



  • 输入:root = [1,2,3,null,5]
  • 输出:["1->2->5","1->3"]


示例 2:



  • 输入:root = [1]
  • 输出:["1"]


提示:


  • 树中节点的数目在范围 [1, 100] 内
  • -100 <= Node.val <= 100

方法一:深度优先搜索


思路及解法


最直观的方法是使用深度优先搜索。在深度优先搜索遍历二叉树时,我们需要考虑当前的节点以及它的孩子节点。


  • 如果当前节点不是叶子节点,则在当前的路径末尾添加该节点,并继续递归遍历该节点的每一个孩子节点。
  • 如果当前节点是叶子节点,则在当前路径末尾添加该节点后我们就得到了一条从根节点到叶子节点的路径,将该路径加入到答案即可。

如此,当遍历完整棵二叉树以后我们就得到了所有从根节点到叶子节点的路径。当然,深度优先搜索也可以使用非递归的方式实现,这里不再赘述。


代码

class Solution {
func binaryTreePaths(_ root: TreeNode?) -> [String] {
var paths: [String] = []
constructPaths(root, "", &paths)
return paths
}

func constructPaths(_ root: TreeNode?, _ path: String, _ paths: inout [String]) {
if nil != root {
var path = path
path += String(root!.val)
if nil == root?.left && nil == root?.right {
paths.append(path)
} else {
path += "->"
constructPaths(root?.left, path, &paths)
constructPaths(root?.right, path, &paths)
}
}
}
}

复杂度分析

  • 时间复杂度:(2),其中  表示节点数目。在深度优先搜索中每个节点会被访问一次且只会被访问一次,每一次会对  变量进行拷贝构造,时间代价为 (),故时间复杂度为 (2)

  • 空间复杂度:(2),其中  表示节点数目。除答案数组外我们需要考虑递归调用的栈空间。在最坏情况下,当二叉树中每个节点只有一个孩子节点时,即整棵二叉树呈一个链状,此时递归的层数为 ,此时每一层的  变量的空间代价的总和为 (=1)=(2) 空间复杂度为 (2)。最好情况下,当二叉树为平衡二叉树时,它的高度为 log,此时空间复杂度为 ((log)2)


方法二:广度优先搜索


思路及解法


我们也可以用广度优先搜索来实现。我们维护一个队列,存储节点以及根到该节点的路径。一开始这个队列里只有根节点。在每一步迭代中,我们取出队列中的首节点,如果它是叶子节点,则将它对应的路径加入到答案中。如果它不是叶子节点,则将它的所有孩子节点加入到队列的末尾。当队列为空时广度优先搜索结束,我们即能得到答案。


代码

class Solution {
func binaryTreePaths(_ root: TreeNode?) -> [String] {
var paths: [String] = []
if nil == root {
return paths
}
var node_queue: [TreeNode] = []
var path_queue: [String] = []

node_queue.append(root!)
path_queue.append(String(root!.val))

while !node_queue.isEmpty {
let node: TreeNode? = node_queue.removeFirst()
let path: String = path_queue.removeFirst()

if nil == node?.left && nil == node?.right {
paths.append(path)
} else {
if nil != node?.left {
node_queue.append(node!.left!)
path_queue.append(path + "->" + String(node!.left!.val))
}

if nil != node?.right {
node_queue.append(node!.right!)
path_queue.append(path + "->" + String(node!.right!.val))
}
}
}
return paths
}
}

复杂度分析


  • 时间复杂度:(2),其中  表示节点数目。分析同方法一。

  • 空间复杂度:(2),其中  表示节点数目。在最坏情况下,队列中会存在  个节点,保存字符串的队列中每个节点的最大长度为 ,故空间复杂度为 (2)


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

交互小组件 — iOS 17

iOS
作为一名 iOS 开发人员,该平台有一些令人兴奋的特性和功能值得探索。 其中,小部件是我的最爱。 小部件已成为 iOS 和 macOS 体验中不可或缺的一部分,并且随着 SwiftUI 中引入的最新功能,它们现在变得更加强大。 在本文中,我们将探讨如何通过交互...
继续阅读 »

作为一名 iOS 开发人员,该平台有一些令人兴奋的特性和功能值得探索。 其中,小部件是我的最爱。 小部件已成为 iOS 和 macOS 体验中不可或缺的一部分,并且随着 SwiftUI 中引入的最新功能,它们现在变得更加强大。 在本文中,我们将探讨如何通过交互性和动画使小组件变得栩栩如生,使它们更具吸引力和视觉吸引力。 我们将深入探讨动画如何与小组件配合使用的细节,并展示新的 Xcode Preview API,它可以实现快速迭代和自定义。 此外,我们将探索如何使用熟悉的控件(如 Button 和 Toggle)向小部件添加交互性,并利用 App Intents 的强大功能。 那么让我们开始吧!


小部件中的交互性
小部件在单独的进程中呈现,它们的视图代码仅在归档期间运行。 为了使小组件具有交互性,我们可以使用 Button 和 Toggle 等控件。 但是,由于 SwiftUI 不会在应用程序的进程空间中执行闭包或改变绑定,因此我们需要一种方法来表示可由小部件扩展执行的操作。 App Intents 为此提供了一个解决方案,允许我们定义可由系统调用的操作。 通过导入 SwiftUI 和 AppIntents,我们可以使用接受 AppIntent 作为参数的 Button 和 Toggle 初始值设定项来执行所需的操作。


现在我们要为现有项目创建小组件。




相应地命名它。 请注意,禁用两个复选框




现在我将使用清单和按钮重写现有代码。

struct Provider: TimelineProvider {  
func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry( checkList: Array(ModelData.shared.items.prefix(3)))
}

func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
let entry = SimpleEntry(checkList: Array(ModelData.shared.items.prefix(3)))
completion(entry)
}

func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
//var entries: [SimpleEntry] = []

// Generate a timeline consisting of five entries an hour apart, starting from the current date.
let data = Array(ModelData.shared.items.prefix(3))
let entries = [SimpleEntry(checkList: data)]

let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
}

struct SimpleEntry: TimelineEntry {
var date: Date = .now

var checkList: [ProvisionModel]
}

struct InteractiveWidgetEntryView : View {
var entry: Provider.Entry

var body: some View {
VStack(alignment: .leading, spacing: 5.0) {
Text("My List")
if entry.checkList.isEmpty{
Text("You've bought all🏆")
}else{
ForEach(entry.checkList) { item in
HStack(spacing: 5.0){

Image(systemName: item.isAdded ? "checkmark.circle.fill":"circle")
.foregroundColor(.green)


VStack(alignment: .leading, spacing: 5){
Text(item.itemName)
.textScale(.secondary)
.lineLimit(1)
Divider()
}
}
}
}
}
.containerBackground(.fill.tertiary, for: .widget)
}
}

struct InteractiveWidget: Widget {
let kind: String = "InteractiveWidget"

var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider()) { entry in
InteractiveWidgetEntryView(entry: entry)
}
.configurationDisplayName("My Widget")
.description("This is an example widget.")
}
}


提供的代码在 iOS 或 macOS 应用程序中使用 SwiftUI 定义小部件。 让我们分解代码并解释每个部分:

  1. Provider:该结构体符合TimelineProvider协议,负责向widget提供数据。 它包含三个功能:
    • placeholder(in:):此函数返回一个占位符条目,表示首次添加小部件时的外观。 它使用派生自 ModelData.shared.items 的清单数组创建 SimpleEntry。
    • getSnapshot(in:completion:):此函数生成一个表示小部件当前状态的快照条目。 它使用派生自 ModelData.shared.items 的清单数组创建 SimpleEntry。
    • getTimeline(in:completion:):此函数生成小部件的条目时间线。 它使用派生自 ModelData.shared.items 的清单数组创建 SimpleEntry 实例的数组,并返回包含这些条目的时间线。
    1. SimpleEntry:此结构符合 TimelineEntry 协议,表示小部件时间线中的单个条目。 它包含一个表示条目日期的日期属性和一个 checkList 属性,后者是一个 ProvisionModel 项的数组。
    2. InteractiveWidgetEntryView:此结构定义用于显示小部件条目的视图层次结构。 它采用 Provider.Entry 类型的条目作为输入。 在 body 属性内部,它创建一个具有对齐和间距设置的 VStack。 它显示一个标题,并根据 checkList 数组是否为空,显示一条消息或迭代该数组以显示每个项目的信息。
    3. InteractiveWidget:该结构定义小部件本身。 它符合Widget协议并指定了Widget的种类。 它提供了一个 StaticConfiguration,其中包含一个 Provider 实例作为数据提供者,并提供一个 InteractiveWidgetEntryView 作为每个条目的视图。 它还设置小部件的显示名称和描述。
    4. Preview:此代码块用于在开发过程中预览小部件的外观。 它为 .systemSmall 大小的小部件创建预览,并提供 SimpleEntry 实例作为条目。 总的来说,此代码设置了一个使用 SwiftUI 框架显示清单的小部件。 小部件的数据由 Provider 结构提供,条目的视图由 InteractiveWidgetEntryView 结构定义。 InteractiveWidget 结构配置小部件并提供用于开发目的的预览。


还有按钮动作!


Apple 为此推出了 AppIntents!


我已经创建了视图模型和应用程序意图。

struct ProvisionModel: Identifiable{  
var id: String = UUID().uuidString
var itemName: String
var isAdded: Bool = false

}

class ModelData{
static let shared = ModelData()

var items: [ProvisionModel] = [.init(
itemName: "Orange"
), .init(
itemName: "Cheese"
), .init(
itemName: "Bread"
), .init(
itemName: "Rice"
), .init(
itemName: "Sugar"
), .init(
itemName: "Oil"
), .init(
itemName: "Chocolate"
), .init(
itemName: "Corn"
)]
}

提供的代码包括两个数据结构的定义:ProvisionModel 和 ModelData。 以下是每项的解释:


ProvisionModel:该结构表示清单中的一个供应项。 它符合可识别协议,该协议要求它具有唯一的标识符。 它具有以下属性:


id:一个字符串属性,保存使用 UUID 生成的唯一标识符。 每个 ProvisionModel 实例都会有一个不同的 id。


itemName:表示供应项目名称的字符串属性。


isAdded:一个布尔属性,指示该项目是否已添加到清单中。 它使用默认值 false 进行初始化。


ModelData:此类充当数据存储和单例,提供对供应项的共享访问。 它具有以下组件:
共享:ModelData 类型的静态属性,表示类的共享实例。 它遵循单例模式,允许跨应用程序访问同一实例。


items:一个数组属性,包含表示供应项的 ProvisionModel 实例。 该数组使用一组预定义的项目进行初始化,每个项目都使用特定的 itemName 进行初始化。 ModelData.shared 实例提供对此数组的访问。
总的来说,此代码为清单应用程序设置了数据模型。 ProvisionModel 结构定义每个供应项的属性,包括其唯一标识符以及是否已添加到清单中。 ModelData 类提供对供应项列表的共享访问,并遵循单例模式以确保访问和修改数据的一致性。


现在是 appIntent 的时候了!

struct MyActionIntent: AppIntent{  

static var title: LocalizedStringResource = "Toggle Task State"
@Parameter(title: "Task ID")
var id: String
init(){

}

init(id: String){
self.id = id
}

func perform() async throws -> some IntentResult {
if let index = ModelData.shared.items.firstIndex(where: { $0.id == id }) {
ModelData.shared.items[index].isAdded.toggle()

let itemToRemove = ModelData.shared.items[index]
ModelData.shared.items.remove(at: index)

DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
ModelData.shared.items.removeAll(where: { $0.id == itemToRemove.id })
}

print("Updated")
}

return .result()
}
}

提供的代码定义了一个名为 MyActionIntent 的结构,该结构符合 AppIntent 协议。 此结构表示在清单应用程序中切换任务状态的意图。 以下是对其组成部分的解释:


title(静态属性):该属性表示操作意图的标题。 它的类型为 LocalizedStringResource,它是用于本地化目的的本地化字符串资源。


id(属性装饰器):该属性用@Parameter装饰,表示需要切换的任务的ID。


init():这是结构的默认初始化程序。 它不执行任何特定的初始化。


init(id: String):此初始化程序允许您使用特定任务 ID 创建 MyActionIntent 实例。


Perform()(方法):AppIntent 协议需要此方法,并执行与 Intent 相关的操作。
以下是其实施细目:
它检查 ModelData.shared.items 数组中是否存在与意图中提供的 ID 匹配的任务。


如果找到匹配项,它将使用toggle() 方法切换任务的isAdded 属性。 这会改变任务的状态。
然后,它创建一个局部变量 itemToRemove 来存储切换的任务。
使用remove(at:)方法和找到任务的索引从ModelData.shared.items数组中删除任务。
延迟 2 秒后,使用removeAll(where:) 和检查匹配 ID 的闭包从 ModelData.shared.items 数组中删除 itemToRemove。


最后,“Updated”被打印到控制台。
return .result():该语句返回一个IntentResult实例,表示intent的完成,没有任何具体的结果值。
总的来说,此代码定义了一个意图,用于执行切换清单中任务状态的操作。 它访问 ModelData 的共享实例,以根据提供的 ID 查找和修改任务。


现在是时候用 AppIntents 替换图像了

Button(intent: MyActionIntent(id: item.id)) {  
Image(systemName: item.isAdded ? "checkmark.circle.fill":"circle")
.foregroundColor(.green)
}
.buttonStyle(.plain)


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

看完这位小哥的GitHub,我沉默了

就在昨天,一个名为win12的开源项目一度冲上了GitHub的Trending热榜。 而且最近项目的Star量也在飙升,目前已经获得了2.2k+的Star标星。 出于好奇,点进去看了看。好家伙,项目README里写道这是一个14岁的初中生所打造的开源项目。...
继续阅读 »

就在昨天,一个名为win12的开源项目一度冲上了GitHub的Trending热榜。



而且最近项目的Star量也在飙升,目前已经获得了2.2k+的Star标星。



出于好奇,点进去看了看。好家伙,项目README里写道这是一个14岁的初中生所打造的开源项目。


即:在网页端实现了Windows 12的UI界面和交互效果。


这里也放几张图片感受一下。

登录页面

开始菜单

资源管理器

设置

终端命令行

AI Copilot

其他应用



这个项目的灵感来源于作者之前看到 Windows 12 概念版后深受启发,于是决定做一个Windows12网页版(就像之前的 Windows 11 网页版一样),可以让用户在网络上预先体验 Windows 12。


可以看到,这个项目是一个前端开源项目,而且由标准前端技术(HTML,JS,CSS)来实现,下载代码,无需安装,打开desktop.html即可。



项目包含:

  • 精美的UI设计
  • 流畅丰富的动画
  • 各种高级的功能(相较于网页版)

不仅如此,作者团队对于该项目的后续发展还做了不少规划和畅想。


  • 项目规划


  • 项目畅想


刚上面也说了,项目README里写道该项目的作者是一位14岁的初中生,网名叫星源,曾获得CSP普及组一等奖和蓝桥杯国赛三等奖。


作者出生于2009年,在成都上的小学和初中,目前刚上初三。


这样来看的话,虽说作者年龄很小,不过接触计算机和编程应该非常早,而且对计算机领域的知识和技术应该有着非常浓厚的兴趣。


从作者的个人主页里能看到,技术栈这块涉猎得还挺广泛。



作者自己表示如今上初三了,对于win12这个项目也不会做什么功能的更新了,后续的维护更新将交给其他贡献者成员。



文章的结尾也附上Windows 12网页版体验地址:tjy-gitnub.github.io/win12/desktop.html,感兴趣的同学可以自行体验。


聊到这里不得不说,人与人之间的差距确实挺大的。就像这位小哥,才14岁就已经精通前端技术了。


而14岁的我,当年在干嘛呢?


我想了又想。。


额,我好像在网吧里玩红警。。(手动doge)


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

className 还能这么用,你学会了吗

抛出问题 className大家都用过吧,用它在react项目中设置样式。它的用法很简单,除了可以设置一个样式外,react中也可以使用className引入多个类样式。 这次在写项目的时候,碰到一个非常小但是当时却一直解决不了的问题。后面在复盘的时候将它解决...
继续阅读 »

抛出问题


className大家都用过吧,用它在react项目中设置样式。它的用法很简单,除了可以设置一个样式外,react中也可以使用className引入多个类样式。


这次在写项目的时候,碰到一个非常小但是当时却一直解决不了的问题。后面在复盘的时候将它解决了。问题大致是这样的:


有两个活动页,每个活动页上都有一个活动规则图标来弹出活动规则,活动规则图标距离顶部会有一个值。现在问题就是这个活动规则在这两个活动页距离顶部的这个值是不一样的,但是我已经将这个活动规则图标做成了组件,并在这两个活动页里都调用了它,从而导致两个页面的样式会相同。如下图所示:




解决问题


这个问题不算很大,但是属于细节问题。就和我的组长所说的一样,一个项目应该要做到先完成再完美。所以我当时的解决方法是再写一个活动规则组件,只是将距离顶部的值做出修改即可。效果确实是达到了,不过在最后复盘代码的时候,组长注意到了这两个组件,并开始询问我为什么这样做。


组长:Rule_1Rule_2这两个组件是什么意思,我看它们没有很大的区别呀。


我便简单说了一下缘由。


组长接着说:你忘了组件是什么吗?一个CSS样式值不同就大费周章地新增一个组件,这岂不是太浪费了。再去想想其他方案。


通过这一番谈话我想起了组件化思想的运用,发现之前解决的这个小问题解决的并不够好。于是,我就带着组件化思想又来重新完善它。


我重新写了一个demo代码,将主要内容和问题在demo代码中体现出来。下面是原版活动规则组件demo代码,之后的代码都是基于demo代码完成的

import React from "react";
import "./index.css";
const Header = ({ onClick }) => {
return (
<>
<div className="container_hd">
<div
className='affix'
onClick={onClick}
></div>
</div>
</>
);
};
export default Header;

组件化思想


我自己问自己:既然已经写好了一个活动规则组件,为什么仅仅因为一个样式值的不同而去新增一个功能一样的组件?很显然,这种方法是最笨的方案。既然是组件,那就应该要有复用性,或者说只需在原有的基础上稍加改动就可达到效果。


这是样式的问题,因此要从根本上解决问题。单纯地修改 CSS 样式肯定不行,因为两个页面两个不同的样式。


className 运用


className 就不用多介绍了,经常能使用,咱们直接来看如何解决问题。在这里我定义了一个 Value 值,用来区分是在哪个页面的,比如分别有提交页和成功页,我在成功页设置一个 Value 值,,然后将 Value 值传入到活动规则组件,那么在活动规则组件里只需要判断 Value 值是否等于成功页的 Value 值即可。在 className 处做一个三元判断,如下所示:

className={`affix_${Value === "0" ? "main" : "submit"}`}

相当于如果Value等于0的时候类名为affix_main,否则为affix_submit。最后再css将样式完善即可。完整代码可以参考如下:

  • 成功页组件
import Header from "./components/Header";

const Success = () => {
const Value = "0";
return (
<div style={{ backgroundColor: "purple", width: "375px", height: "670px" }}>
<Header Value={Value}></Header>
</div>
);
};

export default Success;

  • 活动规则组件
import React from "react";
import "./index.css";
const Header = ({ onClick, Value }) => {
return (
<>
<div className="container_hd">
<div
className={`affix_${Value === "0" ? "main" : "submit"}`}
onClick={onClick}
></div>
</div>
</>
);
};
export default Header;

  • 活动规则组件样式
.container_hd {
width: 100%;
}
.affix_main {
position: absolute;
top: 32px;
right: -21px;
z-index: 9;
width: 84px;
height: 26px;
background: url('./assets/rule.png');
background-size: contain;
background-repeat: no-repeat;
}
.affix_submit {
position: absolute;
top: 12px;
right: -21px;
z-index: 9;
width: 84px;
height: 26px;
background: url('./assets/rule.png');
background-size: contain;
background-repeat: no-repeat;
}



通过对比效果图可以看出,两者的效果确实发生变化。完成之后,我心里在想:为什么当时就没想出这个简单易行的方案呢?动态判断并设置类名,至少比最开始的新增一个组件的方法高级多了。


总结问题


对于这个问题的解决就这样告一段落了,虽然看起来比较简单(一个动态设置类名),但是通过这个className的灵活使用,让我对className的用法有了更进一步的掌握,也不得不感叹组件化思想的广泛运用,这里最大程度地将组件化思想通过className 发挥出来。


因此,希望通过这个问题,来学会className的灵活用法,并理解好组件化思想。当然如果大家还有更好的解决方案的话,欢迎在评论区告诉我。


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

35岁又如何,脚步不停歇,道阻且长,行之将至

前言 公司总会有业务的调整,而自己也会随着业务调整不得不做出一些改变。过去的半年,偶尔会找不到努力的方向。每到需要总结的时候,也总会感叹是否又荒废了时光。在即将快35岁的年纪,发现需要重新审视一下自己,让自己能有进一步的提升,保持足够的竞争力。 思考 所以从...
继续阅读 »


前言


公司总会有业务的调整,而自己也会随着业务调整不得不做出一些改变。过去的半年,偶尔会找不到努力的方向。每到需要总结的时候,也总会感叹是否又荒废了时光。在即将快35岁的年纪,发现需要重新审视一下自己,让自己能有进一步的提升,保持足够的竞争力。



思考


所以从自身多个维度出发,查阅自己的现状和不足,确定好目标以及一些计划。主要包括以下方面,如果大家有类似的疑惑,也可以按照这些方面提升自己。


image.png



技术


     技术是技术人的立身之本,没有技术,其他的都是空谈。


image.png



基础能力


内容现状目标
包括但不限于
- 计算机基础知识(网络、操作系统)
- 编程语言基础和进阶
- ....
评分: 良好
作为计算机专业出身的小镇做题家,学校学习时课程学的还算不错,但是到了工作中,只能用良好评价。最近半年频繁接触的像TCP、UDP的内容,分分钟让自己怀疑大学时期究竟有没有认真学习。
目标:优秀
基础知识的掌握当然要牢固,这是一个程序员专业度的体验。所以,针对遇到的问题,一定要刨根问底,去探究深层的原因。在探索和解惑的过程中,实际也会涉及对基础知识的检阅。


算法


内容现状目标
包括但不限于
- 计算机数据结构与算法
- 常用算法(Leecode题)
- ...
评分:较差
对于算法的考核,在面试中越来越重要,几乎每家公司都会有算法题。 我个人的算法偏差,有些题目只能靠死记硬背,这也间接导致影响了几次面试中的评价。
目标:良好
基于我在算法方面的实际能力,我觉得做到优秀有一些困难。但是对于常见的面试题目和解题方法,一定要掌握


架构


内容现状目标
包括但不限于
- 数据库
- 中间件
- 分布式理论和实践
- 微服务
- ...
评分:良好
每一个程序员都有架构师的梦。但是落实到实际工作中,真正的架构师岗位很少。不过幸运的是,我们所负责的模块,还是有一些机会的。
目标:优秀
对于架构相关的内容要掌握,在自己负责的内容中勇于尝试。做好时刻要进行架构设计的准备。


产品


作为技术人员,与产品应该是统一战线。有些时候,我们很容易陷入技术实现的细节中,而忽略了产品需求的合理性。


image.png



产品思维和数据洞察


内容现状目标
包括但不限于
- 以产品思维分析需求,提出合理合理意见和建议
- 需求的数据收集
- 功能上线后的数据分析
- ....
评分:差
对于我自己,我也是非常缺乏产品化思维的,很多时候还是以功能出发,并未将其产品化。在数据层面,也缺少足够的敏感度。
目标: 良好
要做到优秀非常困难,毕竟我也不打算转产品(手动狗头)。但是还是期望自己能有产品思维,多参与评审设计。同时也要提升自己的数据敏感度,能从数据中分析产品需求。


项目管理


内容现状目标
包括按不限于
- 需求分析
- 需求跟进
- 各方协调
评分: 差
在上一家公司,绝大多数会有专职的PM或者PMO进行项目管理,到后半段进行敏捷迭代以后,会参与部分项目管理的职责。等来到现在的公司,很多事情需要技术牵头处理,所以作为技术也要有项目管理的能力。
目标: 良好
能够作为PM进行项目牵头跟进,遇到问题多向前思考几步,这一方面对于技术人员的软素质提升也是非常有帮助的。


沟通


 在公司里面,我们有很大一部分时间都是在沟通,沟通也是软素质的一种体现。


image.png



沟通技巧


内容现状目标

- 观点表达要清晰
- 要学会倾听
评分:良好目标:优秀
希望自己更要学会倾听,在和别人沟通时,一定不要基于表达自己的观点,尤其是自己非常擅长的领域,也要克制自己急于表达观点。此外,对于一些事情的表达,切记不要斩钉截铁的回复。


情绪管理


内容现状目标
做好情绪管理,避免在沟通中引入情绪,影响沟通的效果。评分:良好
有时候会情绪化解决问题
目标:优秀
最近也在练习冥想,尽量控制自己的情绪表达。


分享


如果需要深刻掌握某个知识,一般可以按照以下步骤,阅读-> 笔记-> 总结-> 写作->分享。
当可以把知识能够分享给其他人,知识才真正属于了自己。


image.png



阅读


内容现状目标

- 技术或非技术书籍
- 技术博客(推荐medium)
- ...
评分:良好
目前会按照一定的计划读一些书籍,每周也会读几篇博客。
目标:优秀
但是这里有一点需要额外注意,那就是英文文章的阅读一定要加强。


写作


内容现状目标

- 对于自己要经常找的内容,要统一记录,快速查找
- 做好总结,选用适当的方式描述(视频、音频、或者图表)
评分:较差
很多时候,对于看到的内容,遇到的问题,总结不够及时,后续反复来找。
目标:良好
该记录的地方一定记录。
要学会用好Xmind等神器。


分享会


内容现状目标

- 团队分享
- 部门分享
- 公司分享
- ...
评分:差
面对面的分享参与非常非常少。
目标:良好
有机会一定要参与,因为每一次参与,也是督促自己认真整理,以及校验自己学习成果的时候。


管理


关于管理,我几乎0经验,只有之前敏捷团队的一些经验。当然,这是不是说明我进步空间大。



目标制定


内容现状目标

- 明确目标
- 目标清单
- 明确计划
- 目标验收
- ....
评分:较差
某些事情的处理,缺乏计划。
目标:良好
对于目标,我觉得可大可小,也可能不是管理目标,但是希望自己针对后面每个工作都按照目标、计划等内容列出来,逐步锻炼吧。


思考和创新



思考


内容现状目标

- 深度思考
- 抽象思考
- 系统思考
- ....
评分:较差目标:良好
遇到问题,三思而后行,尝试往前想3步,利用各种思考方式思考问题。多阅读、多提问、多交流。


创新


内容现状目标

- 技术创新
- 业务创新
评分: 差
对于我自己,很容易墨守成规,不是很容易变通,所以创新方面很弱。
目标:良好这个还是很困难的,无论是技术创新还是业务创新,如果没有足够的涉猎都不足以支撑。但是还是要提醒自己,这是自己非常薄弱的点。


健康


内容现状目标
身体是革命的本钱,在透支身体加班的同时,还是要记得锻炼身体。评分: 良好
近期北京天气开始变好,早晚不是很热,我也开始骑行通勤上班。骑行时可以让自己从另外一个视角看这个城市,真的很舒服。不过由于单趟通勤要20KM,往返40,加上自己比较菜,所以每周目前基本节奏是周一骑到公司,周二骑回家,周三休息一天,周四再骑到公司,周五骑回家。还不能天天骑,慢慢加油吧。
健康工作50年!!!

image.png



后记


从上面的这些维度分析以后,知道自己还有哪些方面需要进一步提升。所以,我每周都会把这周在这些方面所做的内容记录下来。在日常工作中,也会留意这些内容。


image.png


总而言之,还是继续加油吧!


作者:wowojyc艺超
来源:juejin.cn/post/7276352518262947900
收起阅读 »

硬盘坏了,一气之下用 js 写了个恢复程序

web
硬盘坏了,一气之下写了个恢复程序 师傅拯救无望 硬盘已经寄过去超过一周了,一问竟然是还没开始弄??? 再过一周,上来就问我分几个区?我要恢复哪些数据?我要恢复的数据在哪个位置? 那好吧,既然给了钱师傅也都放弃了,我也没什么好寄托希望的了。况且经过这三个...
继续阅读 »

硬盘坏了,一气之下写了个恢复程序


师傅拯救无望


硬盘已经寄过去超过一周了,一问竟然是还没开始弄???


2023-03-24-14-15-16.png


再过一周,上来就问我分几个区?我要恢复哪些数据?我要恢复的数据在哪个位置?


2023-03-24-14-18-50.png


2023-03-24-14-19-30.png


2023-03-24-14-20-05.png


那好吧,既然给了钱师傅也都放弃了,我也没什么好寄托希望的了。况且经过这三个星期的缓解,心情已经平复了很多,就像时光,回不来了就是回不来了。


自救之路


在把硬盘寄过去的时间里,等待师傅的修复结果的时间里,我并没有闲着(在摸鱼)。


经过调研,数据恢复方法通常有:



  • 硬件损坏,对坏的盘进行修复

  • 误删或逻辑错误等,文件扫描修复

  • git 重置恢复


很明显,这些都不适用于我现在的场景。因为师傅能不能修好是未知的,我只是数据盘没了,系统盘还在。由于 vscode 的数据目录空间占比较小,就没有搬迁到数据盘里,这刚好可以为恢复代码提供了可能。


这是因为新版 vscode 有一个时间线功能,这个时间线数据是默认存储在用户目录下的。


我从 C:/Users/love/AppData/Roaming/Code/User/History 目录中确实找到了很多名为 entries.json 的文件,结构如下:


{
// 配置版本
"version": 1,
// 原来文件所在位置
"resource": "file:///d%3A/git2/cloudcmd/.madrun.mjs",
// 文件历史
"entries": [
{
// 历史文件存储的名称
"id": "YFRn.mjs",
"source": "工作区编辑",
// 修改的时间
"timestamp": 1656583915880
},
{
"id": "Vfen.mjs",
"timestamp": 1656585664751
},
]
}

通过上面的文件大概可以看到,每一个时间点的文件都保存在另一个随机命名的文件里。而网上的方法基本都是自己一个个手动到目录里去根据最新的 id 去找对应的文件内容,然后创建文件并把内容复制出来。


这个过程恢复一两个文件还好,但我这可是要恢复整个 git 工作区,大概有几十个项目上千个文件。


这时候当然是在网上找找有没有什么 vscode 数据恢复 相关的工具,很遗憾找了大半天都没有找到。


气死我了,一气之下就自己写个!


恢复程序开发步骤


毕竟只要数据在磁盘上,无非就是一个文件读取操作的问题,还要拿在这水文章,见谅见谅。


首先考虑需求:



  • 我要实现一个自动扫描 vscode 数据目录

  • 然后以原始的目录结构还原出来,不需要我自己去创建文件夹和文件

  • 如果还原的文件最新的那份不是我想要的,我还能根据时间线进行对比和选择

  • 扫描出来有N个项目时,我可以指定只还原某此项目

  • 我可以搜索文件、目录名或文件内容进行还原

  • 为了方便,我还要一个看起来不太丑的操作界面


大概就上面这些吧。


然后考虑实现:


我要实现一个自动扫描 vscode 数据目录


要的就是我自己连数据目录和恢复地址也不需要填写,就能自动恢复的那种。那么就让程序来自动查找数据目录。经过调研,各版本的 vscode 的数据目录一般保存在这些地方:


参考: stackoverflow.com/a/72610691


  - win -- C:\Users\Mark\AppData\Roaming\Code\User\History
- win -- C:\Users\Mark\AppData\Roaming\Code - Insiders\User\History
- /home/USER/.config/VSCodium/User/History/
- C:\Users\USER\AppData\Roaming\VSCodium\User\History

大概有上面这些路径,当然不排除使用者故意把默认位置修改掉这种边缘情况,或者使用者就只想扫描某个数据目录的情况,所以我也要支持手动输入目录:


  let { historyPath, toDir } = req.body
const homeDir = os.userInfo().homedir
const pathList = [
historyPath,
`${homeDir}/AppData/Roaming/Code/User/History/`,
`${homeDir}/AppData/Roaming/Code - Insiders/User/History/`,
`${homeDir}/AppData/Roaming/VSCodium/User/History`,
`${homeDir}/.config/VSCodium/User/History/`,
]
historyPath = (() => {
return pathList.find((path) => path && fs.existsSync(path))
})()
toDir = toDir || normalize(`${process.cwd()}/re-store/`)

然后以原始的目录结构还原出来……


这就需要解析扫描到的时间线文件 entries.json 了。我们先把解析结果放到一个 list 中,以下是一个完整的解析方法。


然后再把列表转换为树型,与硬盘上的状态对应起来,这样便于调试数据和可视化。


function scan({ historyPath, toDir } = {}) {
const gitRoot = `${historyPath}/**/entries.json`

fs.existsSync(toDir) === false && fs.mkdirSync(toDir, { recursive: true })
const globbyList = globby.sync([gitRoot], {})

let fileList = globbyList.map((file) => {
const data = require(file)
const dir = path.parse(file).dir
// entries.json 地址
data.from = file
data.fromDir = dir
// 原文件地址
data.resource = decodeURIComponent(data.resource).replace(
/.*?\/\/\/(.*$)/,
`$1`
)
// 原文件存储目录
data.resourceDir = path.parse(data.resource).dir
// 恢复后的完整地址
data.rresource = `${toDir}/${data.resource.replace(/:\//g, `/`)}`
// 恢复后的目录
data.rresourceDir = `${toDir}/${path
.parse(data.resource)
.dir.replace(/:\//g, `/`)}
`

const newItem = [...data.entries].pop()
// 创建文件所在目录
fs.mkdirSync(data.rresourceDir, { recursive: true })
const binary = fs.readFileSync(`${dir}/${newItem.id}`, {
encoding: `binary`,
})
fs.writeFileSync(data.rresource, binary, { encoding: `binary` })
return data
})

const tree = pathToTree(fileList, { key: `resource` })
return tree
}

为了方便,我还要一个看起来不太丑的操作界面


我们要把文件树的形式展示出来,还要方便切换。后面决定使用 macos 的文件管理器风格,大概如下。


image.png


如果还原的文件最新的那份不是我想要的,我还能根据时间线进行对比和选择


理论上这里应该要做一个像 vscode 对比文件那样,有代码高亮功能,并且把有差异的字符高亮出来。


实际上,这个需求得加钱。


2023-03-24-15-09-25.png


由于界面是在浏览器里的,需要自动打开,浏览器与系统交互需要一个接口,所以我们使用 opener 来自动打开浏览器。


使用 get-port 来自动生成接口服务的端口,避免使用时出现占用。


  const opener = require(`opener`)
const { portNumbers, default: getPort } = await import(`get-port`)
const port = await getPort({ port: portNumbers(3000, 3100) })
const server = express()
server.listen(port, `0.0.0.0`, () => {
const link = `http://127.0.0.1:${port}`
opener(link)
})

封装成工具,我为人人


理论上我根本不需要什么 UI 界面,也不需要配置,因为我的文件都恢复出来了我还花时间去搞毛线?


实际上,万一别人也有这个恢复文件的需要呢?那么他只要运行下面这条命令代码就能立刻恢复到当前目录啦!


npx vscode-file-recovery

这就是恢复后的文件在硬盘里的样子啦:


2023-03-24-15-22-23.png


所有代码位于:



建议收藏,以备不时之需。/手动狗头


作者:程序媛李李李李李蕾
来源:juejin.cn/post/7213994684262826040
收起阅读 »

写在入职九周年这天,讲讲这些年的心路历程

往前翻翻才意识到已经很长很长时间没有写文章,大概具体有多长?感觉上有一光年那么长。 今天,刚好是入职九周年,竟然遇到周末,省了奶茶钱。是的,没错,我在一个窝里面趴了九年。 这些年,彷徨过,迷茫过,孤独过,也充满热血的奋斗过,激情的追求过,有犹豫,有脆弱,也有失...
继续阅读 »

往前翻翻才意识到已经很长很长时间没有写文章,大概具体有多长?感觉上有一光年那么长。


今天,刚好是入职九周年,竟然遇到周末,省了奶茶钱。是的,没错,我在一个窝里面趴了九年。


这些年,彷徨过,迷茫过,孤独过,也充满热血的奋斗过,激情的追求过,有犹豫,有脆弱,也有失落。在悠悠岁月中,能及时不断做出调整,让自己学会享受工作带来的乐趣,学会慢慢成长。在当下浮躁的时代,写些闲言碎语,给诸君放松下心情,缓解压力。


过去的那些年


入职那天,阳光明媚,清风柔和,大厦旁边的道路开满迎春花,连空气都是甜的,让人不由自主地深呼吸,可以闻到花香草香,还有阳光的味道。


那一天也是为数不多来上班较早的一天,哦吼有大草坪,哦哟还有篮球场,这楼还波浪线,牛批,B座这个大厅有点大,有点豪华,牛批牛批,头顶上这看着怎么像熊掌,12345,设计真不孬啊。慢慢的大厅上聚集了很多人,有点吵,咋也没人组织一下呢,大家都很随意的站着等待,陆陆续续有员工来上班。


“XXX”,听到有人喊我名字,吓我一跳,还以为偷看小姐姐被发现了。


“站到靠近电梯入口的最右一列,第一个位置上”,“XX,去站在他后面”,“大家按我叫名字的顺序排好队,咱们准备上楼了”。


那会儿,AI 还不会人脸识别过闸机。


呦呵,这公司真牛批,还有扶手电梯。跟着带路的同学来到三楼五福降中天会议室,一个挺老大的屋子,还有各种数不过来的高大上仪器电子设备,一周后,也是在这里,我和厂长面对面聊聊人生。坐稳扶好后,HR 同学开始入职培训,我摸摸新电脑,摸摸工卡牌,心里美滋滋,想到未来几年,将在这样美妙的环境中度过,喜不胜收,甚至我都闻到了楼下食堂啵啵鱼的香味。


培训刚结束。


“我叫到名字的同学,跟我来。XXX,XX……”,纳尼???中午还管饭??这福利也太好了吧,真不用吧,我自己能找到食堂,再说,你知道我喜欢吃什么吗?“跟着我走,咱们去北门做班车,去另一个办公楼,你们的工位不在这儿。”不在这?还坐班车?what?被外包了?她刚刚喊我了吗???差不多六七个同学,跟着楼长鱼贯而出,下楼,走小路,几分钟后,上了班车。司机大哥,一脚油门,就带着我远离啵啵鱼。


大约10分钟,也有可能是5分钟,或者15分钟,按照现在萝卜快跑无人车的速度,是7分钟。来到一栋圆了咕咚的,长的像长南瓜的楼,它有一个很科技感的名字,“首创空间”,就是这个空间,不仅给我带来技术的成长,还有十几斤的未来多年甩也甩不掉的肥肉。没有啵啵鱼的日子,相见便成为世上最奢侈的愿望。


大约是两年后的初春 ,准确的说,不到两年,记得是二、三月份,北京的PM2.5比较严重的时候,鼻子还不会过敏,也没学会发炎,眼睛也不知道怎么迎风流泪,总之,我们要搬家了。


“科技园”\color{#333333}“科技园”,听听,听听,多好的名字,长得像无穷大与莫比乌斯环,楼顶带跑道,位置也牛批,毗邻猪厂、鹅厂、渣浪,北邻联想,西靠壹号院,远眺百望山,低头写代码,啧啧,美滋滋的日子又来了。当时还没有共享单车,晚饭时蹭着班车和一群小伙伴过去看新工位,喏,不错不错,挺大,位置离厕所不远,不错不错,会议室安静舒适好多个,不错不错。重点来了,食堂大的离谱,还有很多美食,连吃几个月,基本不重样。吃过几次啵啵鱼,与大厦简直天壤之别,怀念。


机会说来就来,几个月后的一天,发生了一件大事。我回到了梦开始的地方,那让人朝思暮想的啵啵鱼,那让人魂牵梦绕的味道,那让人无法忘怀的美妙口感。


清醒一下.gif


命运说变就变,国庆休假回来,食堂换了运营商,我他么……¥#%@%#!@#@!&%


一直没变的:不忘初心,砥砺前行。


曾经觉得自己无所不能,可以改变世界,总幻想像蝴蝶一样扇扇翅膀,亚马逊的雨林就会刮起大风。食堂吃的多了,越来越认识到自己的影响力微乎其微,我们能做到,是把交代的工作做好,做到极致,应该就是对公司最大的回馈了,也对得起日渐增多的白发。


早些年,搞视频直播,课程学习,每天研究各种编解码技术,与视频流打交道,看过不少底层技术原理书籍,探索低延迟的 P2P 技术,枯燥,乏味,也跟不上时代变化,觉得自己会的那些早晚被淘汰,技术乏陈革新的速度超乎想象,而你所负责的,恰恰不是那些与时代贴合度较高的业务,边缘化。


怎么破?


从来没有人限制你,不允许你去学习。\color{red}从来没有人限制你,不允许你去学习。


因为恰巧在做课程的直播、录播,需要特别关注课程内容,主要担心出现线上问题,刚好利用这个契机,了解到很多跨专业,跨部门的业务,当时给自己的宗旨是,“只要有时间,就去听课”,“凡是所学,皆有收获”。前后积累近千小时的学习时长,现在想想,觉得都有些不可能,怎么做到的,是人吗?这是人干的事?


日常工作,专心不摸鱼,积极努力提高工作效率,解决研发任务,配合 peer 做好产品协同。晚饭后,专心研究 HTML大法,通勤路上手机看文档,学 api 用法,学习各种牛批的框架,技巧,逛各大论坛,写博客做积累,与各种人物斯比,每天晚上十点,跑步半小时,上床睡觉,生活很规律。


机缘巧合下,我终于从一个小坑,成功跳到一个大坑,并至今依然在坑中。那天,我想起了啵啵鱼。


16797732_0_final.png


可爱小熊猫,AI 还不会画牙、画手的阶段


一直在变的


团队在变,用两只手数了数,前前后后换了七次 leader,管理风格比五味杂陈还多一味,有的事无巨细,有的不闻不问,有的给你空间让你发挥,有的完全帮不上忙。怎么破?尊重,学习,并努力适应,不断调整心态,适应环境的变化。


业务在变,这么多年数过来,参与过的产品没有一百也有八十了,真正能够长期坚守下来的产品不多,机会可遇不可求,能得一二,实属幸运。把一款产品从零做到一,很容易;再做到十,很难但能够完成;再从十到百,几乎不可能。互联网公司怎么会有这样的产品存在,少之又少。


技能在变,经历过前端技术栈井喷的同学都深有体会,学不动的感受。


时代在变,社会在变,人心也在改变。


曾经多次想过换个环境,换一个坑趴着,毕竟很多机会还是很诱人的。印象最深的一次,是在某年夏天,对手头的工作实在是感到无聊。由于前一年小伙伴们的共同努力,产品架构设计相当完美,今年的工作接近于智力劳动转变为纯人力的重复的机械的体力劳动,对产品建设渐失激情,每天如同行尸走肉般的敲键盘,突然意识到,自己到了职业发展瓶颈期。如何抉择,走或留,临门一脚的事,至于这一脚踢向何方,还未知。


忧思几天后,去找 leader 沟通,好家伙,他让我呆在这里别动,帮他稳住团队,他要撤,一两个月的事。好家伙,你不殿后掩护我们,自己先撂了,还说可以试试带团队,我说大哥,也没几个人呀。他说你还能招兵买马,试试新的角色,体会下不同的视角,很好的机会。坑,绝对的大坑,我他么竟然义不容辞的答应了。


好在,不枉大家这么多年的认可,团队战斗力很强大。


你觉得什么是幸福



  • 有独处的时间

  • 有生活的追求

  • 工作能给你带来乐趣


颐和园.jpg


静悄悄的圆明园东路


前些日子,给娃拿药请了半天假,工作日人不多,十点多就完事了,看看时间地铁回去差不多到公司刚好中午饭。医院出来看到很多小黄车,美团那种新式的自行车,看着很不错,还没体验过,特别想兜几圈。查地图,距离公司有22公里,按照骑行的速度推算,70分钟也差不多到了。打定主意后,书包里翻出俩水煮蛋(鬼知道我为什么早上去公司拿了俩鸡蛋)和一瓶水(鬼使神差的早上往书包放的),算是吃过早饭了。于是一个人,一条狗,开局一把刀,沿着滨河路,经过木樨地,二里河,中关村南大街,北大街,信息路,上地西路回来了。您还别说,就是一个地道。竟然还路过玉渊潭,还遇到了封路限行,静悄悄的圆明园东路,过国图,还有数不清的大学,附中,有那么一瞬间好想回母校去看看,总之,重点是顺路吃到心心念的煎饼果子。


路上给媳妇打电话,这小妞竟然说我疯了,疯了?你懂个屁,这叫幸福。


人生的乐趣


人生的乐趣何在?你的答案和我的答案可能不一样,作为打工人,我知道,肯定不是工作。但似乎又不能没有工作,不工作我们怎么活着?怎么在这个社会上,换取资源,立足于当下,着眼于未来。说回工作,最后悔的事,曾经有那么一小段,人际关系没有处理好,可能造成误会,当时来自于我对某些事情的不表态,默许的态度,十周年前修复它。最快乐的时光,是和大家一起沉浸在技术点的探讨,Bug的跟进定位,发现问题解决问题的成就感;参与产品的规划,出谋划策,影响他人;挑灯夜战,专注于产品的 DDL,为上线争分夺秒的努力前行,感受团队的力量。


这个春天,爬过许多京郊的小山头,站在山顶,凝视着壮丽的景色,总以为自己是秦始皇。不惑之前,去征服贡嘎雪山。


总之,故事太多讲也讲不完,作为一个九年的老东西,我是不会爆金币的。


到结尾了,给点建议吧


建议?给不了给不了,我自己还没活明白。


历史的滚滚车轮中,每个生命都很渺小,时代一直在变,抓住机遇,让自己成长,多读书,沉下心,慢慢来。


16795669_0_final.png


作者:水鳜鱼肥
来源:juejin.cn/post/7222509109948989501
收起阅读 »

我们都有美好的未来

从善待厂毕业了,年终没有,季度奖也没有,好在N+1还有 同一个组的小伙伴吃了最后一顿散伙饭 后来,陆陆续续知道了其他人的动向 继博去了楼上的一家公司,做农民工讨薪的app,再后来听说快成为第一批用户了 阿森去了本地的一个大厂,每天10点他会跟我们讲他下班了 添...
继续阅读 »

从善待厂毕业了,年终没有,季度奖也没有,好在N+1还有


同一个组的小伙伴吃了最后一顿散伙饭


后来,陆陆续续知道了其他人的动向


继博去了楼上的一家公司,做农民工讨薪的app,再后来听说快成为第一批用户了


阿森去了本地的一个大厂,每天10点他会跟我们讲他下班了


添总去了城里,每天朝九晚六


浩宇回了内蒙去放羊


沐川转行不写Java了


文强回了重庆,住了院,听说因为工作生了一场病


我们都还在,都还有美好的未来


作者:think123
来源:juejin.cn/post/7154257335878189087
收起阅读 »

跨域漏洞,我把前端线上搞崩溃了

web
最近迁移前端资源到云厂商时,我遇到了一个奇怪的问题 —— 线上环境某个 CSS 资源跨域了,后果直接导致项目崩溃不可用。大领导直接找到了我的工位,激动的心,颤抖的手,问题原因就是找不到!!! 很奇怪,我已经提前设置了资源允许跨域访问,提测环境、测试环境也已经...
继续阅读 »

最近迁移前端资源到云厂商时,我遇到了一个奇怪的问题 —— 线上环境某个 CSS 资源跨域了,后果直接导致项目崩溃不可用。大领导直接找到了我的工位,激动的心,颤抖的手,问题原因就是找不到!!!


WX20230807-141353@2x.png


很奇怪,我已经提前设置了资源允许跨域访问,提测环境、测试环境也已经正常运行很久了,理论上不应该出现跨域问题。而且更奇怪的是,这个问题只出现在某个 CSS 文件上。


建议大家在阅读本文时结合目录一起查看。本文详细介绍了从跨域问题发现到跨域问题解决的整个过程,文章还简要提到了前端资源链路。结合上下文来看,对处理前端跨域问题具有一定的参考价值。希望对大家有所帮助。


什么是跨域问题?


跨域及其触发条件


跨域是指在 web 开发中,一个网页的源(origin)与另一个网页的源不同,即它们的协议、域名或端口至少有一个不同。跨域问题是由于浏览器的同源策略而产生的,它限制了一个网页中加载的资源只能来自同一个源,以防止恶意网站在未经允许的情况下访问其他网站的数据。


以下情况会触发跨域问题:



  1. 不同域名:当页面的域名与请求的资源的域名不一致时,会触发跨域问题,如从 example.com 页面请求资源来自 api.example.net

  2. 不同协议:如果页面使用了 https 协议加载,但试图请求非 https 资源,也会触发跨域问题。

  3. 不同端口:如果页面加载的是 example.com:3000,但试图请求资源来自 example.com:4000,同样会触发跨域问题。

  4. 不同子域名:即使是不同的子域名也会被认为是不同的源。例如,subdomain1.example.comsubdomain2.example.com 是不同的源。


image.png


跨域问题会影响到浏览器执行以下操作:



  • JavaScript的XMLHttpRequest或Fetch API请求其他源的资源。

  • 通过<img><link><script>等标签加载其他源的资源。

  • 使用CORS(跨源资源共享)机制实现跨域数据传输。


解决跨域的方法


解决跨域问题的方法有多种,具体的选择取决于你的应用场景。以下是一些常见的跨域解决方法:



  1. 跨域资源共享(CORS) :CORS是一种标准机制,通过在服务器端设置响应头来允许或拒绝跨域请求。这是解决跨域问题的最常见方法。

    • 在服务器端设置响应头中的Access-Control-Allow-Origin字段来指定允许访问的域名或使用通配符*表示允许所有域名访问。

    • 其他相关的CORS头,如Access-Control-Allow-MethodsAccess-Control-Allow-Headers,用于控制允许的HTTP方法和请求头。



  2. JSONP(JSON with Padding): 通过动态创建 <script> 标签来实现跨域请求的技术。服务器端返回的数据被包装在一个函数调用中,该函数名由客户端指定。虽然 JSONP 简单易用,但只支持GET请求,由于安全性较差(容易受到跨站脚本攻击),存在安全风险。
    // 客户端代码
    function handleResponse(data) {
    console.log('Received data:', data);
    }

    const script = document.createElement('script');
    script.src = 'https://example.com/api/data?callback=handleResponse';
    document.head.appendChild(script);


  3. 代理服务器:设置一个位于同一域的代理服务器,将跨域请求代理到目标服务器,并将响应返回给客户端。这个方法需要服务器端的额外配置。

  4. 跨文档消息传递: 使用window.postMessage()方法,可以在不同窗口或iframe之间进行跨域通信。

  5. WebSocket: WebSocket是一种双向通信协议,不受同源策略的限制。通过WebSocket,客户端和服务器可以建立持久连接进行跨域通信。

  6. Nginx反向代理: 使用 Nginx 或其他反向代理服务器可以将跨域请求转发到目标服务器,同时解决跨域问题。这种方法适用于前端无法直接控制目标服务器的情况。


每种方法都有其适用的场景和安全考虑,具体的选择取决于项目的需求和架构。


背景与跨域设置


image.png


项目背景介绍


最近我负责了一个前端迁移第三方云(阿里云)的工作,由于这是一个多项目组合成的微前端项目,我考虑在前端迁移中,尽可能统一各个应用项目流程、规范和技术。一是形成统一的规范和方式,二是团队项目各负责人按照方案迁移各自项目时避免因各自不一致导致出现问题。


而在这其中就存在着资源存储和加载不一致的情况,我遇到了三种不同的方法:




  1. 直接使用云存储提供的资源地址


    这是一种常见方式,但也伴随着一些潜在问题。首先,访问云资源可能会有一定的延迟,尤其对于大型文件或数据集。其次,公共云资源地址可能存在安全性和隐私风险,特别是涉及敏感信息时。此外,直接使用OSS资源地址可能导致资源管理分散,难以跟踪和监控资源的使用情况,也可能限制了一些高级功能的实现,如CDN缓存控制、分布式访问控制以及资源日志记录。


    https://company.oss-cn-beijing-internal.company.com/productdata/gateway/prod/assets/index-a12abcbf.css



  2. 使用配置了云存储的CDN域名地址


    这种方式是我比较推荐的方式,然而团队在配置这块链路上存在一些潜在问题。首先 CDN 请求打到了团队内部的一个老服务器,目前老服务器的稳定性不如预期,稳定性方面较差,出现过故障,影响用户体验。前端迁移到第三方云的主要目的之一就是解耦该服务,提供更稳定的前端资源环境。此外,该服务器与其他服务器存在依赖关系,增加了项目的复杂性和不稳定性,解耦是必要的。并且使用这个 CDN 的项目很多,随着时间推移,项目的增加可能会使得该资源地址的维护变得相当复杂。


    https://static.company.com/productdata/gateway/prod/assets/index-a12abcbf.css



  3. 直接加载服务器内部的前端资源


    直接加载服务器内部的前端资源是通过网站域名的代理访问服务器内部资源的一种方法。有几个项目使用这个种方式,这种方式具备简单、快捷等优势。然而,这种方式可能引入网络延迟和性能影响,因为资源请求需要经过团队内部的服务器。同时,存在单点故障的风险,如果内部服务器发生故障或不可用,将导致网站前端资源无法加载,可能对用户造成不便。它也依赖于团队内部的网络环境,需要保持网络的稳定性和可靠性以确保资源能够顺利加载。




为了统一这三种方式并规避潜在问题,我想到了一个综合性的前端资源请求链路方案。通过将 OSS 存储桶、CDN 和网关服务器相互结合,以提升资源分发速度和安全性,同时减轻 OSS 服务器的负载。此外,我还将所有资源引用集中到一个配置文件中,位于网关服务器,以便轻松进行维护和跟踪。(这里只是简要介绍,我将在后续文章分享详细细节


然而,在初步方案制定后,也需要考虑如何处理在同源策略下可能出现的跨域问题。


image.png


前端静态资源跨域设置


我在OSS存储桶的跨域设置中配置了允许跨域头,使得网页可以通过浏览器访问OSS资源,免受同源策略的限制。


image.png


为什么我会选择在 OSS 存储桶配置呢?


主要因为这个存储桶非常整洁,只有两个项目在使用,已经提前简单配置了跨域处理。而且这两个项目后续会按照前端迁移方案进行统一迁移处理,因此我认为直接在 OSS 存储桶配置跨域会更为简洁和可维护,我还和 SRE 老师调整了一下配置(然而,没想到恰恰因为我的这个行为,导致后面出现了跨域问题)。


此外,为了确保安全性,我采取了以下措施:



  • 将项目单独、分批迁移到阿里云 OSS。

  • 在网关服务器中使用nginx进行项目正则匹配,每次迁移就开放一个项目。
    location ~ ^/(gateway|message)/prod/

  • 项目在提测、测试环境都各自运行一段时间(有的甚至在1~2个月)。

  • 在未迁移到正式环境前,各项目按照各自排期计划进行过多次发版。


这些措施是为了确保有问题,可以在提测环境、测试环境中暴露出来。然而,在迁移第3个项目到正式服环境时,出现了问题。。。


奇怪的CSS资源跨域问题


为什么只有某个CSS文件受影响?


跨域问题通常由浏览器的同源策略引起,该策略限制了来自不同源的资源之间的交互。


如果资源有跨域问题,不应该只有某个CSS文件出现跨域问题呀?


3p55k2cus.png


分析后,我发现浏览器中 CSS 资源的返回头中缺少 CORS 头信息,截图如下:


image.png


正常情况下,应该是下图这样:


image.png


这时候我在想不应该呀,我已经在源站 OSS 存储桶配置了允许跨域头,这里的返回头中应用是要携带的,而且别的文件(如html、js)返回头中都是携带了允许跨域,但是为什么只有这个 CSS 资源的就没有呢?





需要注意的是,通常情况下,HTML 文件本身不受同源策略的限制,因此可以从不同源加载 CSS 文件。但如果 CSS 文件中包含引用其他跨域资源(如字体、图片等),那么同源策略仍然会生效,需要特别注意处理这些跨域资源的加载。


问题的深层原因分析


image.png


排除了自身导致的问题


面对这样一个看似简单的跨域问题,我做了一系列的排查和解决过程。首先,我排除了浏览器缓存、资源代码方面以及浏览器本身的问题,并同 SRE 老师否定了前端资源链路(如OSS、CDN)配置错误的可能性。随后,我们还仔细查看了网关日志,但未能发现问题。


一直没找到导致跨域问题出现的原因,我们也想到了直接在网关服务器或 CDN 中强制加入允许跨域头。然而我们一讨论,发现不行,因为 OSS 中已经配置了跨域,强制加入允许跨域头,会出现双重跨域问题;如果移除 OSS 中跨域头也不行,因为已经有两个项目已经直接引用阿里云地址,移除后那两个项目也会出现跨域问题。





寻求阿里云 CDN 运维工程师的帮助


结合我们自己的分析,我们认为是前端资源请求链路的哪个环节出现了问题,但是迟迟找不到原因,于是我们咨询了阿里云 CDN 运维工程师,因为阿里云 CDN 的日志隔天才出来,所以借此希望通过阿里云 CDN 运维老师能够查看下当天的 CDN 日志,从而找到问题。查看日志后,阿里云 CDN 运维老师也只是给出了日志显示一切正常,但随后我们继续沟通。


随后,给到了我们一个关键点:“OSS要能响应跨域头,请求头必须携带 Origin 请求头”。阿里云 CDN 运维老师也说传任何值都可以,但是我多次查看到浏览器请求已经携带了 Origin 请求头。如下图:


image.png


这就奇怪了!此时测试环境提测环境又无法复现 CORS 跨域问题,我们又不能直接在生产环境调试这个问题。


借助工具复现问题


于是我在思考是否能够在提测环境模拟出加载有问题资源的场景。我想到了可以通过拦截浏览器对提测环境的资源请求地址,并将其代理到具有问题的资源地址上来实现这个目的。为了实现这一方案,我使用了一个名为 GoRes 的谷歌浏览器插件。


image(2).png


成功复现,见下图:


3p55k2cus.png


随后,在多次代理调试中,我发现只有在正式服这个项目的资源地址中出现了这个问题。我和 SRE 老师一起再次确认了提测环境、测试环境和正式环境中各自网关服务器和 CDN 域名等的差异性,当然还是没发现问题!





问题逐渐浮现出水面


经过综合分析,我们怀疑 CDN 缓存可能是导致问题的原因。然而,我们无法直接查看缓存的资源,只能再次联系阿里云 CDN 的运维老师。经过多次沟通,我们得知如果客户端在第一次请求 CDN 时没有携带 Origin 请求头,CDN 就不会将 Origin 请求头传递到 OSS,OSS 因此不会响应跨域头,而后续 CDN 便会将没有跨域头的资源内容缓存下来。


这时我才意识到,OSS 内部存在着对 Origin 辨别的跨域处理机制。而在此之前,上传代码资源到 OSS 后,由于是正式环境,为了安全起见测试资源是否上传成功,我直接在浏览器中访问了一个 CSS 文件地址(当时请求到了资源,我还信心满满,丝毫没有注意到还有这么一个坑),但这一步的操作却间接成为了导致跨域问题出现的导火索


通常情况下,当网页加载跨域资源时,由于违反了同源策略,浏览器会自动添加源 Origin 到资源的请求头中。然而,由于我直接请求了 CSS 资源地址,未触发同源策略,浏览器也就没有自动添加 Origin 请求头,导致请求到的 OSS 资源中没有跨域头配置。这也就是为什么 CDN 缓存了没有跨域头的资源。


在网页加载资源时,由于 CDN 缓存了没有跨域头的资源,无论你如何请求,只要 CDN 缓存一直存在,浏览器加载的就是没有跨域头的资源。 因此,这也导致了资源跨域问题的出现。



本来是为了谨慎一点,提前验证资源是否已上传成功的操作,没想到却成为了跨域问题出现的导火索!!!



image.png


这个问题的教训很深刻,让我们意识到必须在向 OSS 请求资源时强制添加 Origin 请求头,以确保今后前端资源的稳定性。否则,这个问题将成为一个定时炸弹。我们决定在网关服务器上分工合作解决这个问题,以杜绝类似情况再次发生。这个经验教训也提醒了我和SRE老师要更加谨慎地处理类似操作,以避免潜在的问题。


如何稳定解决跨域问题


尽管我们已经找到了问题的根源,但是不排除是不是还有其他类似问题,为了保险起见,我决定还是缩小影响范围。在确保测试无问题后,逐步放开所有项目。


SRE 老师负责处理向 OSS 传递Origin请求头的部分,而我负责处理 Nginx location 的正则匹配项目的部分。以下是我们的网关服务器配置:


location ~ ^/(message|dygateway|logcenter)/tice/ { 
set $cors_origin "";
if ($http_origin = "" ) {
set $cors_origin 'static.example.com';
}
if ($http_origin != "") {
set $cors_origin $http_origin;
}
proxy_set_header Origin $cors_origin;
}



  • location ~ ^/(message|dygateway)/tice/:这是一个正则匹配,能更容易地添加或移除项目。




  • proxy_set_header Origin $cors_origin;:如果请求中包含 Origin 头部,它会被直接传递给 OSS;如果没有,它会被设置为一个值后再传递给 OSS。




配置完成后,直接在浏览器中请求下面这个资源地址,你会发现请求头并没有添加上去。这并不是配置出错,而是因为上面我们提到的CDN不仅缓存了资源,还缓存了请求头。


https://static.company.com/productdata/gateway-admin/prod/assets/index-a12abcbf.css

所以我们在这个资源的地址后面拼接了参数,相当于是请求新的 CDN 资源地址,此时可以发现跨域头已经添加上了。


https://static.company.com/productdata/gateway-admin/prod/assets/index-a12abcbf.css?abc=111

image.png


接下来就是在真实项目中测试下,首先在 CDN 后台刷新了有问题的项目资源文件目录,清除掉有跨域问题 CDN 资源缓存后。然后重新刷新浏览器,此时这个文件就成功加上了跨域的请求头,页面访问也正常了。





image.png


image.png


后面我又测试了多次,跨域问题彻底解决。为了避免以后出现类型的问题,所以我又整理了跨域资源共享(CORS)方案,希望对大家有用,请大家接着往下看。


跨域资源共享方案


image.png


跨域资源共享方案是解决前端资源跨域问题的最常见方法,可维护性强,配置简单,可以说这是业界普遍处理前端资源跨域的方式。下面我们将深入探讨三种不同的 CORS 配置方案,并分析各自的优缺点。


OSS存储桶配置跨域


我们都知道 OSS(对象存储服务)是阿里云提供的海量、安全、低成本、高可靠的云存储服务。但其实 OSS 也能设置跨域处理,可以让客户端前端应用从其他域名请求资源。


实施步骤:



  1. 登录阿里云控制台,找到对应的OSS存储桶。

  2. 进入存储桶的管理界面,选择“跨域设置”。

  3. 添加CORS规则,指定允许的来源、方法、头信息等。


image.png


优点:



  • 简单易用:配置简单,通过图形界面即可完成。

  • 安全性高:可以灵活控制允许访问的来源,减少安全风险。


缺点:



  • 依赖云服务商:此方法只适用于使用阿里云OSS的情况,不适用于其他云服务商或自建服务器。


注意:OSS存储桶配置完成跨域后,需要在请求 OSS 存储桶资源时,在请求头中配置 Origin。因为 OSS 内部的机制是 OSS 响应跨域头的前提是必须要携带源站Origin请求头。 建议大家强制配置必传 Origin 请求头,否则容易出现我这次的问题。使用OSS存储桶配置跨域制定方案时,可以参考我在上面的处理:“如何稳定解决跨域问题”。


网关服务器配置跨域


在网关服务器配置跨域,网关服务器通常配置了 Nginx 反向代理服务器。通过配置 Nginx location,可以实现对特定域名的允许跨域支持。


实施步骤:



  1. 修改nginx配置文件(通常位于/etc/nginx/nginx.conf),添加CORS相关配置。

  2. 配置Access-Control-Allow-OriginAccess-Control-Allow-MethodsAccess-Control-Allow-Headers等头信息。


location / {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET,POST,OPTIONS,PUT,DELETE';
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
}

优点:



  • 灵活性高:可以自由配置适应特定需求。

  • 适用性广:适用于各种服务器环境,不依赖特定云服务商。


缺点:



  • 配置复杂:需要熟悉nginx配置。


CDN配置跨域


CDN(内容分发网络)是一种通过将内容缓存到全球各地节点,加速用户访问速度的网络服务。但也能通过 CDN 配置 CORS,可以在边缘节点处实现跨域。


实施步骤:



  1. 登录CDN服务提供商的控制台,找到相应CDN加速域名。

  2. 进入域名配置界面,找到CORS配置选项。

  3. 添加CORS规则,指定允许的来源、方法、头信息等。


image.png


优点:



  • 高性能:CDN 服务通常提供全球分发,可以加速跨域请求,提供更好的性能。

  • 规模化:适用于大规模的Web应用,可支持高并发的跨域请求。


缺点:



  • 成本:使用 CDN 服务可能会产生额外的费用,特别是对于大量的数据传输。

  • 配置复杂性:相对于 OSS 或 Nginx,CDN 的配置可能会更为复杂,需要在控制台进行详细的设置。


注意:腾讯云 CDN 中有专门针对跨域设置的勾选项,只需要选中保存就行。


三种跨域处理方案各有优缺点,选择合适的方案取决于具体的业务需求和技术栈。我上面所说的也只供大家参考,毕竟 CDN、存储桶这种很大程度受限于云平台,这也是我把允许跨域配置在网关服务器的原因之一。可以综合考虑选择合适的方案或者结合多种方案来实现跨域资源共享。


u=2094032080,194978745&fm=30&app=106&f=JPEG.jpeg


结语


前端资源加载问题往往受多种因素的影响,包括 CDN 配置、资源请求链路、云存储配置等。因此,需要全面分析并综合考虑可能出现问题的任何风险点。也要合理使用浏览器插件工具、网络抓包工具和服务器日志分析等工具,可以帮助我们更快速地诊断和解决问题。如果问题复杂或涉及云服务配置,与云厂商的支持团队联系可以提供专业的帮助。


这是我关于资源跨域的一篇文章,里面关于定位问题和跨域方案希望对您有所帮助和参考。如果您需要进一步的协助或有任何问题,请随时提问!


作者:Sailing
来源:juejin.cn/post/7279429009796546623
收起阅读 »

问个事,我就用Tomcat,不用Nginx,行不行!

只用Tomcat,不用Nginx搭建Web服务,行不行?我曾经提出的愚蠢问题,今天详细给自己解释下,为什么必须用Nginx! 不用Nginx,只用Tomcat的Http请求流程 浏览器处理一个Http请求时,会首先通过DNS服务器找到域名关联的IP地址,然后请...
继续阅读 »

只用Tomcat,不用Nginx搭建Web服务,行不行?我曾经提出的愚蠢问题,今天详细给自己解释下,为什么必须用Nginx!


不用Nginx,只用Tomcat的Http请求流程


浏览器处理一个Http请求时,会首先通过DNS服务器找到域名关联的IP地址,然后请求到对应的IP地址。以阿里云域名管理服务为例,一个域名可以最多绑定三个IP地址,这三个IP地址需要是公网IP地址,所以首先需要在三个公网Ip服务器上部署Tomcat实例。


此时我将面临的麻烦如下



  1. 由于DNS域名管理绑定的IP地址有限,最多三个,你如果想要扩容4台Tomcat,是不支持的。无法满足扩容的诉求

  2. 如果你有10个服务,对应10套Tomcat集群,就需要10 * 3台公网Ip服务器。成本还是蛮高的。

  3. 10个服务需要对应10个域名,分别映射到对应的Tomcat集群

  4. 10个域名我花不起这个钱啊!(其实可以用二级域名配置DNS映射)

  5. 公网服务器作为接入层需要有防火墙等安全管控措施,30台公网服务器,网络安全运维,我搞不定。

  6. 公网IP地址需要额外从移动联通运营商或云厂商购买,30个公网IP价格并不便宜。

  7. 前后端分离的情况,Tomcat无法作为静态文件服务器,只能用Nginx或Apache


以上几个问题属于成本、安全、服务扩容等方面。


如果Tomcat服务发布怎么办


Tomcat在服务发布期间是不可用的,在发布期间Http请求打到发布的服务器,就会失败。由于DNS 最多配置3台服务器,也就是发布期间是 1/3 的失败率。 我会被老板枪毙,用加特林


DNS不能自动摘掉故障的IP地址吗?


不能,DNS只是负责解析域名对应的IP地址,他并不知道对应的服务器状态,更不会知道服务器上Tomcat的状态如何。DNS只是解析IP,并没有转发Http请求,所以压根不知道哪台服务器故障率高。更无法自动摘掉IP地址。


我能手动下掉故障的IP地址吗?


这个我能,但是还是会有大量请求失败。以阿里云为例,配置域名映射时,我可以下掉对应的IP地址,但需要指定域名映射的缓存时间,默认10分钟。换句话说,就算你在上线前,摘掉了对应的IP,依然要等10分钟,所有的客户端才会拿到最新的DNS解析地址。


那么把TTL缓存时间改小,可以吗? 可以的,但是改小了,就意味更多的请求被迫从DNS服务器拿最新的映射,整体请求耗时增加,用户体验下降!被老板发现,会骂我。


节点突然挂掉怎么办?


虽然可以在DNS管理后台手动下掉IP地址,但是节点突然宕机、Tomcat Crash等因素导致的突然故障,我是来不及下掉对应IP地址的,我只能打电话告诉老板,“线上服务崩了,你等我10分钟改点东西”。


如果这时候有个软件能 对Tomcat集群健康检查和故障重试,那就太好了。


恰好,这是 Nginx 的长处!


Nginx可以健康检查和故障重试


而Tomcat没有。


例如有两台Tomcat节点,在Nginx配置故障重试策略


upstream test {
server 127.0.0.1:8001 fail_timeout=60s max_fails=2; # Server A
server 127.0.0.1:8002 fail_timeout=60s max_fails=2; # Server B
}

当A节点出现 connect refused时(端口关闭或服务器挂了),说明服务不可用,可能是服务发布,也可能是服务器挂了。此时nginx会把失败的请求自动转发到B节点。 假设第二个请求 请求到A还是失败,正好累计2个失败了,那么Nginx会自动把A节点剔除存活列表 60 秒,然后继续把请求2 转发到B节点进行处理。60秒后,再次尝试转发请求到A节点…… 循环往复,直至A节点活过来……


而这一过程客户端是感知不到失败的。因为两次请求都二次转发到B节点成功处理了。客户端并不会感知到A节点的处理失败,这就是Nginx 反向代理的好处。即客户端不用直连服务端,加了个中间商,服务端的个别节点宕机或发布,对客户端都毫无影响。


而Tomcat只是Java Web容器,并不能做这些事情。


10个服务,10个Tomcat集群,就要10个域名,30个公网IP吗?


以阿里云为例,域名管理后台是可以配置二级域名映射,所以一个公网域名拆分为10个二级域名就可以了。


所以只用Tomcat,不用Nginx。需要1个公网域名,10个二级域名,30台服务器、30个公网IP。


当我和老板提出这些的时候,他跟我说:“你XX疯了,要不滚蛋、要不想想别的办法。老子没钱,你看我脑袋值几个钱,拿去换公网IP吧”。


image.png


心里苦啊,要是能有一个软件,能帮我把一个域名分别映射到30个内网IP就好了。


恰好 Nginx可以!


Nginx 虚拟主机和反向代理


例如把多个二级域名映射到不同的文件目录,例如



  1. bbs.abc.com,映射到 html/bbs

  2. blog.abc.com 映射到 html/blog


http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
server {
listen 80;
server_name http://www.abc.com;
location / {
root html/www;
index index.html index.htm;
}
}

server {
listen 80;
server_name bbs.abc.com;
location / {
root html/bbs;
index index.html index.htm;
}
}

server {
listen 80;
server_name blog.abc.com;
location / {
root html/blog;
index index.html index.htm;
}
}
}

例如把不同的二级域名或者URL路径 映射到不同的 Tomcat集群



  1. 分别定义 serverGroup1、serverGroup2 两个Tomcat集群

  2. 分别把路径group1、group1 反向代理到serverGroup1、serverGroup2


upstream serverGroup1 {                    # 定义负载均衡设备的ip和状态
server 192.168.225.100:8080 ; # 默认权重值为一
server 192.168.225.101:8082 weight=2; # 值越高,负载的权重越高
server 192.168.225.102:8083 ;
server 192.168.225.103:8084 backup; # 当其他非backup状态的server 不能正常工作时,才请求该server,简称热备
}

upstream serverGroup2 { # 定义负载均衡设备的ip和状态
server 192.168.225.110:8080 ; # 默认权重值为一
server 192.168.225.111:8080 weight=2; # 值越高,负载的权重越高
server 192.168.225.112:8080 ;
server 192.168.225.113:8080 backup; # 当其他非backup状态的server 不能正常工作时,才请求该server,简称热备
}

server { # 设定虚拟主机配置
listen 80; # 监听的端口
server_name picture.itdragon.com; # 监听的地址,多个域名用空格隔开
location /group1 { # 默认请求 ,后面 "/group1" 表示开启反向代理,也可以是正则表达式
root html; # 监听地址的默认网站根目录位置
proxy_pass http://serverGroup1; # 代理转发
index index.html index.htm; # 欢迎页面
deny 127.0.0.1; # 拒绝的ip
allow 192.168.225.133; # 允许的ip
}
location /group2 { # 默认请求 ,后面 "/group2" 表示开启反向代理,也可以是正则表达式
root html; # 监听地址的默认网站根目录位置
proxy_pass http://serverGroup2; # 代理转发
index index.html index.htm; # 欢迎页面
deny 127.0.0.1; # 拒绝的ip
allow 192.168.225.133; # 允许的ip
}

error_page 500 502 503 504 /50x.html;# 定义错误提示页面
location = /50x.html { # 配置错误提示页面
root html;
}
}

经过以上的教训,我再也不会犯这么愚蠢的错误了,我需要Tomcat,也需要Nginx。


当然如果钱足够多、资源无限丰富,公网IP、公网服务器、域名无限…… 服务发布,网站崩溃,无动于衷,可以不用Nginx。


作者:他是程序员
来源:juejin.cn/post/7280088532377534505
收起阅读 »

别再用 float 布局了,flex 才是未来!

web
大家好,我是树哥! 前面一篇文章整体介绍了 CSS 的布局知识,其中说到 float 布局是 CSS 不断完善的副产物。而在 2023 年的今天,flex 这种布局方式才是未来!那么今天我们就来学习下 flex 弹性布局。 什么是 Flex 布局? 在经过了长...
继续阅读 »

大家好,我是树哥!


前面一篇文章整体介绍了 CSS 的布局知识,其中说到 float 布局是 CSS 不断完善的副产物。而在 2023 年的今天,flex 这种布局方式才是未来!那么今天我们就来学习下 flex 弹性布局。


什么是 Flex 布局?


在经过了长达 10 年的发展之后,CSS3 才终于迎来了一个简单好用的布局属性 —— flex。Flex 布局又称弹性布局,它使用 flexbox 属性使得容器有了弹性,可以自动适配各种设备的不同宽度,而不必依赖于传统的块状布局和浮动定位。


举个很简单地例子,如果我们想要实现一个很简单左侧定宽,右侧自适应的导航布局,如下图所示。


-w1239


在没有 flex 之前,我们的代码是这么写的。


<div>
<h1>4.1 两栏布局 - 左侧定宽、右侧自适应 - float</h1>
<div class="container">
<div class="left41"></div>
<div class="right41"></div>
</div>
</div>

/** 4.1 两栏布局 - 左侧定宽、右侧自适应 - float **/
.left41 {
float: left;
width: 300px;
height: 500px;
background-color: pink;
}
.right41 {
width: 100%;
height: 500px;
background-color: aquamarine;
}

这种方式不好的地方在于,我们还需要去理解 float 这个概念。一旦需要理解 float 这个概念,我们就会拖出一大堆概念,例如文档流、盒子模型、display 等属性(虽然这些东西确实应该学)。但对于 flex 来说,它就很简单,只需要设置一个伸缩系数即可,如下代码所示。


<div>
<h1>4.2 两栏布局 - 左侧定宽、右侧自适应 - flex</h1>
<div class="container42">
<div class="left42"></div>
<div class="right42"></div>
</div>
</div>

.container42 {
display: flex;
}
.left42 {
width: 300px;
height: 500px;
background-color: pink;
}
.right42 {
flex: 1;
width: 100%;
height: 500px;
background-color: aquamarine;
}

上面的代码里,我们只需要将父级容器设置为 flex 展示形式(display: flex),随后在需要自动伸缩的容器里设置属性即可。上面代码中的 flex: 1 表示其占据所有其他当行所剩的空间。通过这样的方式,我们非常方便地实现了弹性布局。


当然,上面只是一个最简单的例子,甚至还不是很能体现出 flex 的价值。flex 除了在响应式布局方面非常方便之外,它在对齐等方面更加方便,能够极大地降低学习成本、提高工作效率。


Flex 核心概念


对于 Flex 布局来说,其有几个核心概念,分别是:主轴与交叉轴、起始线和终止线、Flex 容器与 Flex 容器项。


主轴和交叉轴


在 Flex 布局中有一个名为 flex-direction 的属性,可以取 4 个值,分别是:



  • row

  • row-reverse

  • column

  • column-reverse


如果你选择了 row 或者 row-reverse,那么主轴(Main Axis)就是横向的 X 轴,交叉轴(Cross Axis)就是竖向的 Y 轴,如下图所示。


主轴是横向的X轴,交叉轴是竖向的Y轴


如果你选择了 column 或者 column-reverse,那么主轴(Main Axis)就变成是竖向的 Y 轴,交叉轴(Cross Axis)就是横向的 X 轴,如下图所示。


主轴是竖向的Y轴,交叉轴是横向的X轴


起始线和终止线


过去,CSS 的书写模式主要被认为是水平的,从左到右的。但现代的布局方式涵盖了书写模式的范围,所以我们不再假设一行文字是从文档的左上角开始向右书写的。


对于不同的语言来说,其书写方向不同,例如英文是从左到右,但阿拉伯文则是从右到左。那么对于这两种语言来说,其xx会有所不同 TODO。举个简单的例子,如果 flex-direction 是 row ,并且我是在书写英文。由于英文是从左到右书写的,那么主轴的起始线是左边,终止线是右边,如下图所示。


-w557


但如果我在书写阿拉伯文,由于阿拉伯文是从右到左的,那么主轴的起始线是右边,终止线是左边,如下图所示。


-w541


在 Flex 布局中,起始线和终止线决定了 Flex 容器中的 Flex 元素从哪个方向开始排列。 举个简单例子,如果我们通过 direction: ltr 设置了文字书写方向是从左到右,那么起始线就是左边,终止线就是右边。此时,如果我们设置的 flex-direction 值是 row,那么 Flex 元素将会从左到右开始排列。但如果我们设置的 flex-direction 值是 row-reverse,那么 Flex 元素将会从右到左开始排列。


在上面的例子中,交叉轴的起始线是 flex 容器的顶部,终止线是底部,因为两种语言都是水平书写模式。但如果有一种语言,它的书写形式是从底部到顶部,那么当设置 flex-direction 为 column 或 column-reverse 时,也会有类似的变化。


Flex 容器与 Flex 元素


我们把一个容器的 display 属性值改为 flex 或者 inline-flex 之后,该容器就变成了 Flex 容器,而容器中的直系子元素就会变为 flex 元素。如下代码所示,parent 元素就是 Flex 容器,son 元素就是 Flex 元素。


<style>
#parent {
display: flex;
}
</style>
<div id="parent">
<div id="son"></div>
</div>

Flex 核心属性


对于 Flex 来说,它有非常多的用法,但核心属性却相对较少。这里我只简单介绍几个核心属性,如果你想了解更多 Flex 的属性,可以去 Mozilla 官网查询,这里给个传送门:flex 布局的基本概念 - CSS:层叠样式表 | MDN


对于 Flex 布局来说,其核心属性有如下几个:



  1. flex-direction 主轴方向

  2. flex 伸缩系数及初始值

  3. justify-content 主轴方向对齐

  4. align-items 交叉轴方向对齐


flex-direction 主轴方向


如上文所介绍过的,flex-direction 定义了主轴的方向,可以取 4 个值,分别是:



  • row 默认值

  • row-reverse

  • column

  • column-reverse


一旦主轴确定了,交叉轴也确定了。主轴和交叉轴与后续的对齐属性有关,因此弄懂它们非常重要!举个很简单的例子,如下的代码将展示下图的展示效果。


.box {
display: flex;
flex-direction: row-reverse;
}

<div class="box">
<div>One</div>
<div>Two</div>
<div>Three</div>
</div>

-w538


如果你将 flex-direction 改成 column-reverse,那么将会变成如下的效果,如下图所示。


-w541


flex 伸缩系数及初始值


前面说到 Flex 布局可以很方便地进行响应式布局,其实就是通过 flex 属性来实现的。flex 属性其实是 flex-grow、flex-shrink、flex-basis 这三个参数的缩写形式,如下代码所示。


flex-grow: 1;
flex-shrink: 1;
flex-basis: 200px;
/* 上面的设置等价于下面 flex 属性的设置 */
flex: 1 1 200px;

在考虑这几个属性的作用之前,需要先了解一下 可用空间 available space 这个概念。这几个 flex 属性的作用其实就是改变了 flex 容器中的可用空间的行为。


假设在 1 个 500px 的容器中,我们有 3 个 100px 宽的元素,那么这 3 个元素需要占 300px 的宽,剩下 200px 的可用空间。在默认情况下,flexbox 的行为会把这 200px 的空间留在最后一个元素的后面。


-w537


如果期望这些元素能自动地扩展去填充满剩下的空间,那么我们需要去控制可用空间在这几个元素间如何分配,这就是元素上的那些 flex 属性要做的事。


flex-basis


flex-basis 属性用于设置 Flex 元素的大小,其默认值是 auto。此时浏览器会检查元素是否有确定的尺寸,如果有确定的尺寸则用该尺寸作为 Flex 元素的尺寸,否则就采用元素内容的尺寸。


flex-grow


flex-grow 若被赋值为一个正整数,flex 元素会以 flex-basis 为基础,沿主轴方向增长尺寸。这会使该元素延展,并占据此方向轴上的可用空间(available space)。如果有其他元素也被允许延展,那么他们会各自占据可用空间的一部分。


举个例子,上面的例子中有 a、b、c 个 Flex 元素。如果我们给上例中的所有元素设定 flex-grow 值为 1,容器中的可用空间会被这些元素平分。它们会延展以填满容器主轴方向上的空间。


但很多时候,我们可能都需要按照比例来划分剩余的空间。此时如果第一个元素 flex-grow 值为 2,其他元素值为 1,则第一个元素将占有 2/4(上例中,即为 200px 中的 100px), 另外两个元素各占有 1/4(各 50px)。


flex-shrink


flex-grow 属性是处理 flex 元素在主轴上增加空间的问题,相反 flex-shrink 属性是处理 flex 元素收缩的问题。如果我们的容器中没有足够排列 flex 元素的空间,那么可以把 flex 元素 flex-shrink 属性设置为正整数,以此来缩小它所占空间到 flex-basis 以下。


与flex-grow属性一样,可以赋予不同的值来控制 flex 元素收缩的程度 —— 给flex-shrink属性赋予更大的数值可以比赋予小数值的同级元素收缩程度更大。


justify-content 主轴方向对齐


justify-content 属性用来使元素在主轴方向上对齐,它的初始值是 flex-start,即元素从容器的起始线排列。justify-content 属性有如下 5 个不同的值:



  • flex-start:从起始线开始排列,默认值。

  • flex-end::从终止线开始排列。

  • center:在中间排列。

  • space-around:每个元素左右空间相等。

  • space-between:把元素排列好之后,剩余的空间平均分配到元素之间。


各个不同的对齐方式的效果如下图所示。


flex-start:


-w454


flex-end:


-w444


center:


-w449


space-around:


-w442


space-between:


-w453


align-items 交叉轴方向对齐


align-items 属性可以使元素在交叉轴方向对齐,它的初始值是 stretch,即拉伸到最高元素的高度。align-items 属性有如下 5 个不同的值:



  • stretch:拉伸到最高元素的高度,默认值。

  • flex-start:按 flex 容器起始位置对齐。

  • flex-end:按 flex 容器结束为止对齐。

  • center:居中对齐。

  • baseline:始终按文字基线对齐。


各个不同的对齐方式的效果如下图所示。


stretch:


-w448


flex-start:


-w439


flex-end:


-w438


center:


-w444


要注意的事,无论 align-items 还是 justify-content,它们都是以主轴或者交叉轴为参考的,而不是横向和竖向为参考的,明白这点很重要。


Flex 默认属性


由于所有 CSS 属性都会有一个初始值,所以当没有设置任何默认值时,flex 容器中的所有 flex 元素都会有下列行为:



  • 元素排列为一行 (flex-direction 属性的初始值是 row)。

  • 元素从主轴的起始线开始。

  • 元素不会在主维度方向拉伸,但是可以缩小。

  • 元素被拉伸来填充交叉轴大小。

  • flex-basis 属性为 auto。

  • flex-wrap 属性为 nowrap。


弄清楚 Flex 元素的默认值有利于我们更好地进行布局排版。


实战项目拆解


看了那么多的 Flex 布局知识点,总感觉干巴巴的,是时候来看看别人在项目中是怎么用的了。


-w1290


上面是我在 CodePen 找到的一个案例,这样的一个布局就是用 Flex 布局来实现的。通过简单的分析,其实我们可以拆解出其 Flex 布局方法,大致如下图所示。


-w1297


首先整体分为两大部分,即导航栏和内容区域,这部分的主轴纵向排列的(flex-direction: column),如上图红框部分。随后在内容区域,又将其分成了左边的导航栏和右边的内容区域,此时这块内容是横向排列的(flex-direction: row),如下上图蓝框部分。


剩下的内容布局也大致类似,其实就是无限套娃下去。由于偏于原因,这里就不继续深入拆解了,大致的布局思路已经说得很清楚了。


有了 Flex 布局之后,貌似布局也变得非常简单了。但纸上得来终觉浅,还是得自己实际动手练练才知道容易不容易,不然就变成纸上谈兵了!


总结


看到这里,关于 Flex 布局的核心点就介绍得差不多了。掌握好这几个核心的知识点,开始去实践练习基本上没有什么太大的问题了。剩下的一些比较小众的属性,等用到的时候再去查查看就足够了。


接下来更多的时间,就是找多几个实战案例实践,唯有实践才能巩固所学知识点。后面有机会,我将分享我在 Flex 布局方面的项目实践。


如果这篇文章对你有帮助,记得一键三连支持我!


参考资料



作者:树哥聊编程
来源:juejin.cn/post/7280054182996033548
收起阅读 »

看完这位小哥的GitHub,我沉默了

web
就在昨天,一个名为win12的开源项目一度冲上了GitHub的Trending热榜。 而且最近项目的Star量也在飙升,目前已经获得了2.2k+的Star标星。 出于好奇,点进去看了看。好家伙,项目README里写道这是一个14岁的初中生所打造的开源项目。...
继续阅读 »

就在昨天,一个名为win12的开源项目一度冲上了GitHub的Trending热榜。



而且最近项目的Star量也在飙升,目前已经获得了2.2k+的Star标星。



出于好奇,点进去看了看。好家伙,项目README里写道这是一个14岁的初中生所打造的开源项目。


即:在网页端实现了Windows 12的UI界面和交互效果。


这里也放几张图片感受一下。



  • 登录页面




  • 开始菜单




  • 资源管理器




  • 设置




  • 终端命令行




  • AI Copilot




  • 其他应用



这个项目的灵感来源于作者之前看到 Windows 12 概念版后深受启发,于是决定做一个Windows12网页版(就像之前的 Windows 11 网页版一样),可以让用户在网络上预先体验 Windows 12。


可以看到,这个项目是一个前端开源项目,而且由标准前端技术(HTML,JS,CSS)来实现,下载代码,无需安装,打开desktop.html即可。



项目包含:



  • 精美的UI设计

  • 流畅丰富的动画

  • 各种高级的功能(相较于网页版)


不仅如此,作者团队对于该项目的后续发展还做了不少规划和畅想。



  • 项目规划




  • 项目畅想



刚上面也说了,项目README里写道该项目的作者是一位14岁的初中生,网名叫星源,曾获得CSP普及组一等奖和蓝桥杯国赛三等奖。


作者出生于2009年,在成都上的小学和初中,目前刚上初三。


这样来看的话,虽说作者年龄很小,不过接触计算机和编程应该非常早,而且对计算机领域的知识和技术应该有着非常浓厚的兴趣。


从作者的个人主页里能看到,技术栈这块涉猎得还挺广泛。



作者自己表示如今上初三了,对于win12这个项目也不会做什么功能的更新了,后续的维护更新将交给其他贡献者成员。



文章的结尾也附上Windows 12网页版体验地址:tjy-gitnub.github.io/win12/desktop.html,感兴趣的同学可以自行体验。


聊到这里不得不说,人与人之间的差距确实挺大的。就像这位小哥,才14岁就已经精通前端技术了。


而14岁的我,当年在干嘛呢?


我想了又想。。


额,我好像在网吧里玩红警。。(手动doge)


作者:CodeSheep
来源:juejin.cn/post/7275978708644151354
收起阅读 »

蒙提霍尔问题

最近看韩国电视剧【D.P:逃兵追缉令】里面提到一个有趣的数学概率游戏 -> 蒙提霍尔问题 意思是:参赛者会看见三扇门,其中一扇门的里面有一辆汽车,选中里面是汽车的那扇门,就可以赢得该辆汽车,另外两扇门里面则都是一只山羊,让你任意选择其中一个,然后打开其余...
继续阅读 »



最近看韩国电视剧【D.P逃兵追缉令】里面提到一个有趣的数学概率游戏 -> 蒙提霍尔问题


意思是:参赛者会看见三扇门,其中一扇门的里面有一辆汽车,选中里面是汽车的那扇门,就可以赢得该辆汽车,另外两扇门里面则都是一只山羊,让你任意选择其中一个,然后打开其余两个门中的一个并且是山羊(去掉一个错误答案),这时,让你重新选择。那么你是会坚持原来的选择,还是换选另外一个未被打开过的门呢?


大家可以想一想如果是自己,我们是会换还是不会换?


好了,我当时看到后感觉很有意思,所以我简单写了一套代码,源码贴在下面,大家可以验证一下,先告诉大家,换赢得汽车的概率是2/3,不换赢得汽车的概率是1/3

<header>
<h1>请选择换不换?</h1><button class="refresh">刷新</button>
</header>
<section>
<div class="box">
<h2>1</h2>
<canvas width="300" height="100"></canvas>
<div class="prize">奖品</div>
</div>
<div class="box">
<h2>2</h2>
<canvas width="300" height="100"></canvas>
<div class="prize">奖品</div>
</div>
<div class="box">
<h2>3</h2>
<canvas width="300" height="100"></canvas>
<div class="prize">奖品</div>
</div>
</section>
<span>请选择号码牌</span>
<select name="" id="">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>
<button class="confirm">确认</button>
<span class="confirm-text"></span>
<span class="opater">
<button class="change">换</button>
<button class="no-change">不换</button>
</span>
<p>
<strong>游戏规则:</strong>
<span>
上面有三个号码牌,其中一个号码牌的里面有汽车,选中里面是汽车的号码牌,
你就可以赢得该辆汽车,另外两个号码牌里面则都是一只山羊,
你任意选择其中一个,然后打开其余两个号码牌中的一个并且是山羊(去掉一个错误答案),
这时,你有一个重新选择的机会,你选择换还是不换?
</span>
</p>
.prize {
width: 300px;
height: 100px;
background-color: pink;
font-size: 36px;
line-height: 100px;
text-align: center;
position: absolute;
}

canvas {
position: absolute;
z-index: 2;
}

section {
display: flex;
}

.box {
width: 300px;
height: 200px;
cursor: pointer;
}

.box+.box {
margin-left: 8px;
}

header {
display: flex;
align-items: center;
}

header button {
margin-left: 8px;
height: 24px;
}
p {
width: 400px;
background-color: pink;
}
function shuffleArray(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
}
function getRandomNumber() {
return Math.random() > 0.5 ? 1 : 2;
}
let a1 = [0, 1, 2]
let i1 = undefined
let i2 = undefined
let isChange = false
const opater = document.querySelector('.opater')
opater.style.display = 'none'
// 随机一个奖品
const prizes = document.querySelectorAll('.prize')
let a0 = [0, 1, 2]
a0 = shuffleArray(a0)
a0.forEach((v,i) => {
const innerText = !!v ? '山羊' : '汽车'
prizes[i].innerText = innerText
})

const canvas = document.querySelectorAll('canvas')
const confirmText = document.querySelector('.confirm-text')
canvas.forEach(c => {
// 使用canvas实现功能
// 1. 使用canvas绘制一个灰色的矩形
const ctx = c.getContext('2d')
ctx.fillStyle = '#ccc'
ctx.fillRect(0, 0, c.width, c.height)
// 2. 刮奖逻辑
// 鼠标按下且移动的时候,需要擦除canvas画布
let done = false
c.addEventListener('mousedown', function () {
if (i1 === undefined) return alert('请先选择号码牌,并确认!')
if (!isChange) return alert('请选择换不换!')
done = true
})
c.addEventListener('mousemove', function (e) {
if (done) {
// offsetX 和 offsetY 可以获取到鼠标在元素中的偏移位置
const x = e.offsetX - 5
const y = e.offsetY - 5
ctx.clearRect(x, y, 10, 10)
}
})
c.addEventListener('mouseup', function () {
done = false
})
})
const confirm = document.querySelector('.confirm')
const refresh = document.querySelector('.refresh')
confirm.onclick = function () {
let select = document.querySelector('select')
const options = Array.from(select.children)
confirmText.innerText = `您选择的号码牌是${select.value},请问现在换不换?`
// 选择后,去掉一个错误答案
// i1是下标
i1 = select.value - 1
// delValue是值
let delValue = undefined
// 通过下标找值
if (a0[i1] === 0) {
delValue = getRandomNumber()
} else {
delValue = a0[i1] === 1 ? 2 : 1
}
// 通过值找下标
i2 = a0.indexOf(delValue)
// 选择的是i1, 去掉的是
const ctx = canvas[i2].getContext('2d')
ctx.clearRect(0, 0, 300, 100)
options.map(v => v.disabled = true)
confirm.style.display = 'none'
opater.style.display = 'inline-block'
}
const change = document.querySelector('.change')
const noChange = document.querySelector('.no-change')
change.onclick = function () {
isChange = true
const x = a1.filter(v => v !== i1 && v !== i2)
confirmText.innerText = `您确认选择的号码牌是${x[0] + 1},请刮卡!`
opater.style.display = 'none'
}
noChange.onclick = function () {
isChange = true
confirmText.innerText = `您确认选择的号码牌是${i1 + 1},请刮卡!`
opater.style.display = 'none'
}
refresh.onclick = function () {
window.location.reload()
}

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

什么是 HTTP 长轮询?

什么是 HTTP 长轮询? Web 应用程序最初是围绕客户端/服务器模型开发的,其中 Web 客户端始终是事务的发起者,向服务器请求数据。因此,没有任何机制可以让服务器在没有客户端先发出请求的情况下独立地向客户端发送或推送数据。 为了克服这个缺陷,Web 应用...
继续阅读 »

什么是 HTTP 长轮询?


Web 应用程序最初是围绕客户端/服务器模型开发的,其中 Web 客户端始终是事务的发起者,向服务器请求数据。因此,没有任何机制可以让服务器在没有客户端先发出请求的情况下独立地向客户端发送或推送数据。


为了克服这个缺陷,Web 应用程序开发人员可以实施一种称为 HTTP长轮询的技术,其中客户端轮询服务器以请求新信息。服务器保持请求打开,直到有新数据可用。一旦可用,服务器就会响应并发送新信息。客户端收到新信息后,立即发送另一个请求,重复上述操作。


什么是 HTTP 长轮询?


那么,什么是长轮询?HTTP 长轮询是标准轮询的一种变体,它模拟服务器有效地将消息推送到客户端(或浏览器)。


长轮询是最早开发的允许服务器将数据“推送”到客户端的技术之一,并且由于其寿命长,它在所有浏览器和 Web 技术中几乎无处不在。即使在一个专门为持久双向通信设计的协议(例如 WebSockets)的时代,长轮询的能力仍然作为一种无处不在的回退机制占有一席之地。


HTTP 长轮询如何工作?


要了解长轮询,首先要考虑使用 HTTP 的标准轮询。


“标准”HTTP 轮询


HTTP 轮询由客户端(例如 Web 浏览器)组成,不断向服务器请求更新。


一个用例是想要关注快速发展的新闻报道的用户。在用户的浏览器中,他们已经加载了网页,并希望该网页随着新闻报道的展开而更新。实现这一点的一种方法是浏览器反复询问新闻服务器“内容是否有任何更新”,然后服务器将以更新作为响应,或者如果没有更新则给出空响应。浏览器请求更新的速率决定了新闻页面更新的频率——更新之间的时间过长意味着重要的更新被延迟。更新之间的时间太短意味着会有很多“无更新”响应,从而导致资源浪费和效率低下。




上图:Web 浏览器和服务器之间的 HTTP 轮询。服务器向立即响应的服务器发出重复请求。


这种“标准”HTTP 轮询有缺点:


  • 更新请求之间没有完美的时间间隔。请求总是要么太频繁(效率低下)要么太慢(更新时间比要求的要长)。
  • 随着规模的扩大和客户端数量的增加,对服务器的请求数量也会增加。由于资源被无目的使用,这可能会变得低效和浪费。

HTTP 长轮询解决了使用 HTTP 进行轮询的缺点


  1. 请求从浏览器发送到服务器,就像以前一样
  2. 服务器不会关闭连接,而是保持连接打开,直到有数据供服务器发送
  3. 客户端等待服务器的响应。
  4. 当数据可用时,服务器将其发送给客户端
  5. 客户端立即向服务器发出另一个 HTTP 长轮询请求


上图:客户端和服务器之间的 HTTP 长轮询。请注意,请求和响应之间有很长的时间,因为服务器会等待直到有数据要发送。


这比常规轮询更有效率。


  • 浏览器将始终在可用时接收最新更新
  • 服务器不会被永远无法满足的请求所搞垮。

长轮询有多长时间?


在现实世界中,任何与服务器的客户端连接最终都会超时。服务器在响应之前保持连接打开的时间取决于几个因素:服务器协议实现、服务器体系结构、客户端标头和实现(特别是 HTTP Keep-Alive 标头)以及用于启动的任何库并保持连接。


当然,许多外部因素也会影响连接,例如,移动浏览器在 WiFi 和蜂窝连接之间切换时更有可能暂时断开连接。


通常,除非您可以控制整个架构堆栈,否则没有单一的轮询持续时间。


使用长轮询时的注意事项


在您的应用程序中使用 HTTP 长轮询构建实时交互时,需要考虑几件事情,无论是在开发方面还是在操作/扩展方面。


  • 随着使用量的增长,您将如何编排实时后端?
  • 当移动设备在WiFi和蜂窝网络之间快速切换或失去连接,IP地址发生变化时,长轮询会自动重新建立连接吗?
  • 通过长轮询,您能否管理消息队列并如何处理丢失的消息?
  • 长轮询是否提供跨多个服务器的负载平衡或故障转移支持?

在为服务器推送构建具有 HTTP 长轮询的实时应用程序时,您必须开发自己的通信管理系统。这意味着您将负责更新、维护和扩展您的后端基础设施。


服务器性能和扩展


使用您的解决方案的每个客户端将至少每 5 分钟启动一次与您的服务器的连接,并且您的服务器将需要分配资源来管理该连接,直到它准备好满足客户端的请求。一旦完成,客户端将立即重新启动连接,这意味着实际上,服务器将需要能够永久分配其资源的一部分来为该客户端提供服务。当您的解决方案超出单个服务器的能力并且引入负载平衡时,您需要考虑会话状态——如何在服务器之间共享客户端状态?您如何应对连接不同 IP 地址的移动客户端?您如何处理潜在的拒绝服务攻击?


这些扩展挑战都不是 HTTP 长轮询独有的,但协议的设计可能会加剧这些挑战——例如,您如何区分多个客户端发出多个真正的连续请求和拒绝服务攻击?


消息排序和排队


在服务器向客户端发送数据和客户端发起轮询请求之间总会有一小段时间,数据可能会丢失。


服务器在此期间要发送给客户端的任何数据都需要缓存起来,并在下一次请求时传递给客户端。




然后出现几个明显的问题:


  • 服务器应该将数据缓存或排队多长时间?
  • 应该如何处理失败的客户端连接?
  • 服务器如何知道同一个客户端正在重新连接,而不是新客户端?
  • 如果重新连接花费了很长时间,客户端如何请求落在缓存窗口之外的数据?

所有这些问题都需要 HTTP 长轮询解决方案来回答。


设备和网络支持


如前所述,由于 HTTP 长轮询已经存在了很长时间,它在浏览器、服务器和其他网络基础设施(交换机、路由器、代理、防火墙)中几乎得到了无处不在的支持。这种级别的支持意味着长轮询是一种很好的后备机制,即使对于依赖更现代协议(如 WebSockets )的解决方案也是如此。


众所周知,WebSocket 实现,尤其是早期实现,在双重 NAT 和某些 HTTP 长轮询运行良好的代理环境中挣扎。


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

🐞 如何成为一名合格的“中级开发”

嗨,大家好!这里是道长王jj~ 🎩🧙‍♂️ 在这个系列里面的上一篇文章中,我跟大家分享了怎么做一个专业的开发者,还有工作中要注意什么事情。 这是我们人生很重要的一步,因为只有学会怎么开始,才能慢慢变优秀,才能一步步往上进步。 如果你是第一次看这个系列,我强烈建...
继续阅读 »

嗨,大家好!这里是道长王jj~ 🎩🧙‍♂️


在这个系列里面的上一篇文章中,我跟大家分享了怎么做一个专业的开发者,还有工作中要注意什么事情。


这是我们人生很重要的一步,因为只有学会怎么开始,才能慢慢变优秀,才能一步步往上进步。


如果你是第一次看这个系列,我强烈建议你回去看看我之前写的两篇文章,说不定能对你有帮助。



  1. 🎖️怎么知道我的能力处于什么水平?我该往哪里努力?
  2. 🚗我毕业/转行了,怎么适应我的第一份开发工作?


其实我想写这篇文章已经很久了,可是一直想不出来怎么写,找了很多资料也没用。


确实憋不出来,中间还水了一篇“JavaScript冷饭”文章。天可是天天炒冷饭不好吃啊,写那些水文总会心生愧疚,感觉对不起你们哈哈。


今天,我们继续聊一聊,当我们进入这个角色一两年后,该怎么摆脱“初级”头衔,迈入“中级”阶段呢?😎



注意事项:


我接下来提及的内容可能很多大佬跟我的意见是不同的。


也有可能我的知识有限,我只涵盖了前端开发工程师的部分,对其他岗位的开发工程师不了解,可能我说的指标并不一定能和贵公司考核时所授予给的职称相对应。


我这里说的是衡量开发人员技能、知识和整体能力的一般指标


它会根据所在的领域而变化,比如前端、后端、数据等等都不太一样。


虽然具体的工具、技术甚至架构知识可能有所不同,但是我说的一般原则应该是可以广泛适用的。


如果觉得我说错了,请在评论区交流。😊



🎖️ 中级开发的显著特点:“骄傲”


当你到了中级水平,你心里一定有一个想法。那就是:


我已经学会了我现在做的事情,以及要用的所有东西了!


再说得清楚一点就是:


“我已经完全会用JavaScript了,我对HTML很熟悉了,我对数据库没问题!”


“我已经完全会用Vue了,我也会用Angular开发”


这个时候的“中级开发”,觉得他已经有了这个领域需要的能力了。



我肯定每个人到了中级阶段后肯定会有这种感觉。


可能你觉得我要说的是开玩笑,但是大部分的“中级开发”肯定都经历过这个事情。



当然啦,我想表达的“骄傲”不是贬义词。


因为这个阶段只是我们成长中必须经历的一个阶段。这真的不是一件坏事。


“骄傲”不是一件坏事


我们小时候我们都会觉得,爸爸妈妈什么都不知道,我们才更明白


类似的,当你真正进入进入“中级开发“这个角色,你大概率的就会产生这类“骄傲的情绪”。


当你拥有“骄傲”,你才开始真正走自己的路。这个时候你才真正开始独立思考。


这意味着你已经积累了足够的知识和经验,可以继续精进设计模式、最佳实践等这些学科以拔高你的知识。


简单的东西已经不能吸引你了。


🚩 中级开发应该掌握什么?


现在你是中级开发了,你需要看看自己是不是能做到下面这些事情。


这些“新”的东西可以让中级开发更有经验,也更能帮助团队。


编程能力:


  1. 很清楚不同的系统(API、模块、包等)怎么互相连接
  2. 熟练使用编程工具(IDE、GIT等)
  3. 知道怎么实现一般的需求
  4. 遇到bug的时候,知道从哪里找原因和解决办法
  5. 知道怎么优化代码和重构代码
  6. 知道怎么提高性能
  7. 知道怎么用面向对象的程序设计
  8. 知道常用的软件架构模式(MVC、MVVM、MVP、MVI等)
  9. 知道编程语言的一些特点(函数式编程)
  10. 知道怎么部署系统应用
  11. 知道怎么用数据库索引
  12. 知道怎么用数据库表迁移
  13. 知道怎么用数据库分片技术

社会能力:


  1. 可以偶尔跟产品经理(客户)沟通
  2. 是团队的主力

开始优雅:


  1. 代码模块开始按照设计模式来写
  2. 对烂代码有敏感度和重构能力

等等


📌 对中级开发的一些建议


也许现在在读文章的你已经是一位中级开发的存在了,我现在有一些建议想要分享给你!


找一个自己感兴趣的开发者社区加入


为什么我们常说“好的团队创造个人”呢


因为当你真的参与到了重要或高价值的项目时,你真的比一个人漫无目的地学习更快地获得经验。


而且当你真正在团队中贡献力量地时候,你地团队,你的组长,你的领导都会知道,把事情交给你,你就能把自己做好。


在这个过程中,你能积累经验并在你的团队中声名鹊起(这不是名气,而是知名度),那么当新的机会出现时,你就能很快地把握住。


跳出舒适区


跟我上一篇提到的给初级开发的建议类似,你一定要经常的跳出自己的舒适区,不然你不会有毅力坚持学习。


而且,特别是在互联网行业,学习能力是个硬性指标,如果无法坚持下去,很容易就会被淘汰。


这样做可以开阔你的眼界,让你的知识面更广。最终,你会逐渐掌握开发的技巧,面对这些全新的知识领域时,能更快、更准确地找到重点并掌握它们。


但是只要你坚持下去,未来的你一定会与其他人拉开差距。


找到你的导师


这一点在上一篇我也强调过了。你的开发生涯,不能只靠你自己摸索。


你需要有人给你提供想法并能够从中学习。特别是在“中级开发”阶段。


导师可以帮助你不会在某些技术问题或者人生问题上钻牛角尖,他可以拉你一把,避免你浪费很多时间。


这个人可以是你团队中的某个人。


也可以是网络上开发者社区中认识的某位博主。


找到你信任的人(或者更可能是一群人),你可以跟他们问问题和说想法!


找到可以指导你的导师,让你能够突破当前的认知。你的未来将逐步变得清晰起来。


持续学习


这个没什么好说的,在这内卷的社会中,如果没有润的资本和能力,不如在持续学习中等待破局的机会!




🎉 你觉得怎么样?这篇文章可以给你带来帮助吗?当你处于这个阶段时,你发现什么对你帮助最大?如果你有任何疑问或者想进一步讨论相关话题,请随时发表评论分享您的想法,让其他人从中受益。🚀✨


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

iOS文件系统

iOS
沙盒机制 概念 iOS 沙盒机制是一种安全策略,它将每个应用程序的数据和资源隔离在一个专用目录中,限制了应用程序访问其他应用程序或系统文件的能力,从而保护了用户数据和系统安全. 目录结构 For security purposes, an iOS app’s...
继续阅读 »

沙盒机制


概念


iOS 沙盒机制是一种安全策略,它将每个应用程序的数据和资源隔离在一个专用目录中,限制了应用程序访问其他应用程序或系统文件的能力,从而保护了用户数据和系统安全.


目录结构



For security purposes, an iOS app’s interactions with the file system are limited to the directories inside the app’s sandbox directory. During installation of a new app, the installer creates a number of container directories for the app inside the sandbox directory. Each container directory has a specific role. The bundle container directory holds the app’s bundle, whereas the data container directory holds data for both the app and the user. The data container directory is further divided into a number of subdirectories that the app can use to sort and organize its data. The app may also request access to additional container directories—for example, the iCloud container—at runtime.



👆大概内容讲的是出于安全考虑,iOS应用只能在当前APP的沙盒目录下与文件系统进行交互(读取、创建、删除等).在APP安装时,系统会在当前APP的沙盒目录下创建不同类型不同功能的容器目录(Bundle、Data、iCloud).Data Container又进一步被划分为Documents、Library、temp、System Data.沙盒目录结构如下图所示:




各目录描述如下图所示:




下面介绍一些关于文件系统中常用的API.


创建NSFileManager

// 1.创建实例
NSFileManager *fileManager = [[NSFileManager alloc] init];

// 2.获取单例
NSFileManager *fileManager = [NSFileManager defaultManager];

// 3.自定义
自定义NSFileManager实现一些自定义功能.

NSFileManager有一个delegate(NSFileManagerDelegate)属性,实现该协议的对象能够对文件的拷贝、移动等操作做更多的逻辑处理,同时能在这些操作发生错误时做一些容错的处理.

// 该协议用于控制文件/文件夹,是否允许移动、删除、拷贝.以及允许这些操作发生错误时进行额外的处理.
@protocol NSFileManagerDelegate <NSObject>

// 控制是否允许该操作:
// 以下每种方法都有一个NSURL和NSString的版本,URL和Path同作用的方法只会调用一次,并且优先调用URL的方法.如果两个方法都没实现,则实现系统的默认值YES.
// 其中srcURL/srcPath分别代表原(文件/文件夹)路径URL(file://xxx)/路径(xxx). xxx为完整路径.
// 其中dstURL/dstPath分别代表目标(文件/文件夹)路径URL(file://xxx)/路径(xxx). xxx为完整路径.

// 错误处理:
// 以下每种方法都有一个NSURL和NSString的版本,URL和Path同作用的方法只会调用一次,并且优先调用URL的方法.如果两个方法都没实现,则不处理错误.

@optional

/// Moving an Item

// 在移动文件/文件夹前调用,控制是否允许移动操作
// 如果两个方法都没有实现,系统默认YES即允许移动.
- (BOOL)fileManager:(NSFileManager *)fileManager shouldMoveItemAtURL:(NSURL *)srcURL toURL:(NSURL *)dstURL;
- (BOOL)fileManager:(NSFileManager *)fileManager shouldMoveItemAtPath:(NSString *)srcPath toPath:(NSString *)dstPath;

// 移动失败时调用 处理错误信息
- (BOOL)fileManager:(NSFileManager *)fileManager shouldProceedAfterError:(NSError *)error movingItemAtURL:(NSURL *)srcURL toURL:(NSURL *)dstURL;
- (BOOL)fileManager:(NSFileManager *)fileManager shouldProceedAfterError:(NSError *)error movingItemAtPath:(NSString *)srcPath toPath:(NSString *)dstPath;

/// Copy an Item

// 在拷贝文件/文件夹前调用,控制是否允许拷贝操作
// 如果两个方法都没有实现,系统默认YES即允许拷贝.
- (BOOL)fileManager:(NSFileManager *)fileManager shouldCopyItemAtURL:(NSURL *)srcURL toURL:(NSURL *)dstURL;
- (BOOL)fileManager:(NSFileManager *)fileManager shouldCopyItemAtPath:(NSString *)srcPath toPath:(NSString *)dstPath;

// 拷贝失败时调用 处理错误信息
- (BOOL)fileManager:(NSFileManager *)fileManager shouldProceedAfterError:(NSError *)error copyingItemAtPath:(NSString *)srcPath toPath:(NSString *)dstPath;
- (BOOL)fileManager:(NSFileManager *)fileManager shouldProceedAfterError:(NSError *)error copyingItemAtURL:(NSURL *)srcURL toURL:(NSURL *)dstURL;

/// Delete an Item

// 在拷贝文件/文件夹前调用,控制是否允许删除操作
// 如果两个方法都没有实现,系统默认YES即允许删除.
- (BOOL)fileManager:(NSFileManager *)fileManager shouldRemoveItemAtPath:(NSString *)path;
- (BOOL)fileManager:(NSFileManager *)fileManager shouldRemoveItemAtURL:(NSURL *)URL;

// 删除失败时调用 处理错误信息
- (BOOL)fileManager:(NSFileManager *)fileManager shouldProceedAfterError:(NSError *)error removingItemAtPath:(NSString *)path;
- (BOOL)fileManager:(NSFileManager *)fileManager shouldProceedAfterError:(NSError *)error removingItemAtURL:(NSURL *)URL;

@end

考虑线程安全的问题,如果需要实现NSFileManagerDelegate协议,通常是新创建NSFileManager实例或是继承NSFileManager后新建实例,确保一个一个实例仅对应一个delegate.


创建文件/文件夹

/// 文件

// 创建文件,会覆盖已存在的文件内容.
// path: 文件路径.
// data: 文件内容.
// attr: 文件属性.例如:设置文件夹可读写权限、文件夹创建日期中.传入nil,则使用系统默认的配置.
// return: YES:文件存在或创建成功. NO:文件创建失败
- (BOOL)createFileAtPath:(NSString *)path
contents:(NSData *)data
attributes:(NSDictionary<NSFileAttributeKey, id> *)attr;

/// 文件夹

// url: 文件夹路径URL.
// createIntermediate: 是否自动创建目录中不存在的中间目录,如果设置为NO,仅仅会创建路径的最后一级目录,若任意中间路径不存在,该方法会返回NO.同时如果任意中间目录是文件也会失败.
// attributes: nil,则使用系统默认的配置.
// error: 错误信息.
// YES: 文件夹创建成功.YES: createIntermediates为YES且文件夹已存在. NO: 错误发生.
- (BOOL)createDirectoryAtURL:(NSURL *)url
withIntermediateDirectories:(BOOL)createIntermediates
attributes:(NSDictionary<NSFileAttributeKey, id> *)attributes
error:(NSError * _Nullable *)error;

// 同上
- (BOOL)createDirectoryAtPath:(NSString *)path
withIntermediateDirectories:(BOOL)createIntermediates
attributes:(NSDictionary<NSFileAttributeKey, id> *)attributes
error:(NSError * _Nullable *)error;


其他方式写入文件:NSData、NSString、All kinds Of Collections (写入plist).


删除文件/文件夹


删除操作是指将文件/文件夹从指定目录移除掉.

// 以file://xxx的形式删除文件/文件夹.
// srcURL: 原文件/文件夹目录URL
// dstURL: 目标目录URL.
// error: 错误信息.
// return: YES: 移动成功或URL为nil或delegate停止删除操作. NO: 错误发生.
- (BOOL)removeItemAtURL:(NSURL *)URL
error:(NSError * _Nullable *)error;
// 同上,不过传入的是xxx完整路径.
- (BOOL)removeItemAtPath:(NSString *)path
error:(NSError * _Nullable *)error;

// 将文件/文件夹移入到废纸篓,适用于Macos.iOS上使用会失败.
- (BOOL)trashItemAtURL:(NSURL *)url
resultingItemURL:(NSURL * _Nullable *)outResultingURL
error:(NSError * _Nullable *)error;

// 注意:
// 1.如果删除的是文件夹,则会删除文件夹中所有的内容.
// 2.删除文件前会调用delegate的-[fileManager:shouldRemoveItemAtURL:]或-[fileManager:shouldRemoveItemAtPath:]方法,用于控制能否删除.如果都没有实现,则默认可以删除.
// 3.删除失败时会调用delegate的-[fileManager:shouldProceedAfterError:removingItemAtURL:]或-[fileManager:shouldProceedAfterError:removingItemAtPath:]方法,用于处理错误.
// 如果2个方法都没有实现则删除失败.并且删除方法会返回相应的error信息.
// 方法返回YES会认为删除成功,返回NO则删除失败,删除方法接收error信息.

文件/文件夹是否存在


// path: 文件/文件夹路径.
// isDirectory: YES: 当前为文件夹. NO: 当前为文件.
// return: YES: 文件/文件夹存在. NO: 文件/文件夹不存在.
- (BOOL)fileExistsAtPath:(NSString *)path
isDirectory:(BOOL *)isDirectory;

// 同上,不过不能判断当前是文件还是文件夹.
- (BOOL)fileExistsAtPath:(NSString *)path;

遍历文件夹


有时候我们并不知道文件的具体路径,此时就需要遍历文件夹去拼接完整路径.

/// 浅度遍历: 返回当前目录下的文件/文件夹(包括隐藏文件).
// 默认带上了options: NSDirectoryEnumerationSkipsSubdirectoryDescendants.
// NSDirectoryEnumerationIncludesDirectoriesPostOrder无效,因为不会遍历子目录.
- (nullable NSArray<NSString *> *)contentsOfDirectoryAtPath:(NSString *)path
error:(NSError **)error

// url: 文件路径.
// keys: 预请求每个文件属性的key数组.如果不想预请求则传入@[],传nil会有默认的预请求keys.
// options: 遍历时可选掩码.
// error: 错误信息.
- (nullable NSArray<NSURL *> *)contentsOfDirectoryAtURL:(NSURL *)url
includingPropertiesForKeys:(nullable NSArray<NSURLResourceKey> *)keys
options:(NSDirectoryEnumerationOptions)mask
error:(NSError **)error

/// 深度遍历(递归遍历): 返回当前目录下的所有文件/文件夹(包括隐藏文件).
- (nullable NSDirectoryEnumerator<NSString *> *)enumeratorAtPath:(NSString *)path
- (nullable NSDirectoryEnumerator<NSURL *> *)enumeratorAtURL:(NSURL *)url
includingPropertiesForKeys:(nullable NSArray<NSURLResourceKey> *)keys
options:(NSDirectoryEnumerationOptions)mask
errorHandler:(nullable BOOL (^)(NSURL *url, NSError *error))handler
- (NSArray<NSString *> *)subpathsOfDirectoryAtPath:(NSString *)path
error:(NSError * _Nullable *)error;
- (NSArray<NSString *> *)subpathsAtPath:(NSString *)path;

获取文件夹

// iOS基本上都是使用NSUserDomainMask.

// 获取指定目录类型和mask的文件夹.类似于NSSearchPathForDirectoriesInDomains方法.
// 常用的directory:
// NSApplicationSupportDirectory -> Library/Application Support.
// NSCachesDirectory -> /Library/Caches.
// NSLibraryDirectory -> /Library.
- (NSArray<NSURL *> *)URLsForDirectory:(NSSearchPathDirectory)directory
inDomains:(NSSearchPathDomainMask)domainMask

// 获取指定directory & domainMask下的文件夹.
// domain: 不能传入NSAllDomainsMask.
// url: 仅当domain = NSUserDomainMask & directory = NSItemReplacementDirectory时有效.
// shouldCreate: 文件不存在时 是否创建.
- (NSURL *)URLForDirectory:(NSSearchPathDirectory)directory
inDomain:(NSSearchPathDomainMask)domain
appropriateForURL:(NSURL *)url
create:(BOOL)shouldCreate
error:(NSError * _Nullable *)error;
// 同上
FOUNDATION_EXPORT NSArray<NSString *> *NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory directory, NSSearchPathDomainMask domainMask, BOOL expandTilde);
// 获取/temp目录
FOUNDATION_EXPORT NSString *NSTemporaryDirectory(void);
// 获取沙盒根目录
FOUNDATION_EXPORT NSString *NSHomeDirectory(void);

设置和获取文件/文件夹属性

// 获取指定目录下文件/文件夹的属性[NSFileAttributeKey].(https://developer.apple.com/documentation/foundation/nsfileattributekey).
- (nullable NSDictionary<NSFileAttributeKey, id> *)attributesOfItemAtPath:(NSString *)path
error:(NSError **)error;

// 为指定目录文件/文件夹设置属性.
- (BOOL)setAttributes:(NSDictionary<NSFileAttributeKey, id> *)attributes ofItemAtPath:(NSString *)path error:(NSError * _Nullable *)error;

// 基于NSDictionary提供的便利方法 //
@interface NSDictionary<KeyType, ObjectType> (NSFileAttributes)

// 文件大小
- (unsigned long long)fileSize;

// 文件创建日期
- (nullable NSDate *)fileCreationDate;
// 文件修改日期
- (nullable NSDate *)fileModificationDate;

// 文件类型.NSFileAttributeType
- (nullable NSString *)fileType;

// 文件权限掩码
- (NSUInteger)filePosixPermissions; // 位掩码 可见文件 _s_ifmt.h eg:S_IRWXU
/* File mode */
/* Read, write, execute/search by owner */
#define S_IRWXU 0000700 /* [XSI] RWX mask for owner */
#define S_IRUSR 0000400 /* [XSI] R for owner */
#define S_IWUSR 0000200 /* [XSI] W for owner */
#define S_IXUSR 0000100 /* [XSI] X for owner */
/* Read, write, execute/search by group */
#define S_IRWXG 0000070 /* [XSI] RWX mask for group */
#define S_IRGRP 0000040 /* [XSI] R for group */
#define S_IWGRP 0000020 /* [XSI] W for group */
#define S_IXGRP 0000010 /* [XSI] X for group */
/* Read, write, execute/search by others */
#define S_IRWXO 0000007 /* [XSI] RWX mask for other */
#define S_IROTH 0000004 /* [XSI] R for other */
#define S_IWOTH 0000002 /* [XSI] W for other */
#define S_IXOTH 0000001 /* [XSI] X for other */

// 当前文件/文件夹所处的文件系统编号
- (NSInteger)fileSystemNumber;
这两个方法可以拼接文件的引用URL -> file:///.file/id=fileSystemNumber.fileSystemFileNumber
// 当前文件/文件夹在文件系统中的编号
- (NSUInteger)fileSystemFileNumber;

// 是否是隐藏文件
- (BOOL)fileExtensionHidden;
...

@end

移动文件/文件夹


移动操作是将文件从一个位置移动到另一个位置.

// 以file://xxx的形式移动文件/文件夹.
// srcURL: 原文件/文件夹位置URL.
// dstURL: 目标位置URL.
// error: 错误信息.
// return: YES: 移动成功或manager的delegate停止移动操作. NO: 错误发生.
- (BOOL)moveItemAtURL:(NSURL *)srcURL
toURL:(NSURL *)dstURL
error:(NSError * _Nullable *)error;
// 同上,不过传入的是xxx完整路径.
- (BOOL)moveItemAtPath:(NSString *)srcPath
toPath:(NSString *)dstPath
error:(NSError * _Nullable *)error;

// 注意:
// 1.srcURL/srcPath、dstURL/dstPath任意为nil会crash.
// 2.如果目标目录文件已存在会被覆盖.
// 3.移动文件前会调用delegate的-[fileManager:shouldMoveItemAtURL:toURL:]或-[fileManager:shouldMoveItemAtPath:toPath:]方法,用于控制能否移动.如果都没有实现,则默认可以移动.
// 4.移动失败时会调用delegate的-[fileManager:shouldMoveItemAtURL:toURL:]或-[fileManager:shouldMoveItemAtPath:toPath:]方法,用于处理错误.
// 如果2个方法都没有实现则移动失败.并且移动方法会返回相应的error信息.
// 方法返回YES会认为移动成功,返回NO则移动失败,移动方法接收error信息.

拷贝文件/文件夹


拷贝操作是将原文件从一个位置copy到另一个位置,类似于复制粘贴.

// 以file://xxx的形式拷贝文件/文件夹.
// srcURL: 原文件/文件夹/位置URL.
// dstURL: 目标位置URL.
// error: 错误信息.
// return: YES: 拷贝成功或manager的delegate停止拷贝操作. NO: 错误发生.
- (BOOL)copyItemAtURL:(NSURL *)srcURL
toURL:(NSURL *)dstURL
error:(NSError * _Nullable *)error;
// 同上,不过传入的是xxx完整路径.
- (BOOL)copyItemAtPath:(NSString *)srcPath
toPath:(NSString *)dstPath
error:(NSError * _Nullable *)error;

// 注意:
// 1.srcURL/srcPath、dstURL/dstPath任意为nil会crash.
// 2.如果目标目录已经存在则会发生错误.
// 3.拷贝文件前会调用delegate的-[fileManager:shouldCopyItemAtURL:toURL:]或-[fileManager:shouldCopyItemAtPath:toPath:]方法,用于控制能否拷贝.如果都没有实现,则默认可以拷贝.
// 4.拷贝失败时会调用delegate的-[fileManager:shouldProceedAfterError:copyingItemAtURL:toURL:]或-[fileManager:shouldProceedAfterError:copyingItemAtPath:toPath:]方法,用于处理错误.
// 如果2个方法都没有实现则拷贝失败.并且拷贝方法会返回相应的error信息.
// 方法返回YES会认为拷贝成功,返回NO则拷贝失败,拷贝方法接收error信息.

参考资料


  1. developer.apple.com/library/arc…
  2. developer.apple.com/documentati…

好物推荐


  1. OpenSim:用于快速定位模拟器中项目沙盒目录.

  2. Flex:用于真机或模拟器Debug环境下调试.


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

大厂人,大厂魂,大厂都是人上人?

大厂,一个贴在互联网人灵魂深处的标签,四舍五入,是互联网人的第二学历。 本文写给所有向往大厂,被大厂(或大厂人) PUA,逃离大厂的人。 Chapter 1: 千万人往 很多人(包括我)选择大厂的理由,无非是下面几点: 更完善的公司制度和明确的上升通道更复杂的...
继续阅读 »

大厂,一个贴在互联网人灵魂深处的标签,四舍五入,是互联网人的第二学历。


本文写给所有向往大厂,被大厂(或大厂人) PUA,逃离大厂的人。


Chapter 1: 千万人往


很多人(包括我)选择大厂的理由,无非是下面几点:


  1. 更完善的公司制度和明确的上升通道
  2. 更复杂的业务/技术问题
  3. 更成熟的基础建设
  4. 更高的工资

一言以蔽之,就是更高的成长空间。当然,也不排除为了工牌(大厂光环)入职的人,尤其是做公众号的、卖课的、运作人设的(绝没有骂我自己的意思)。


不过,大厂也带来了另一面,那就是:


  1. 高情商:「向上管理」;低情商:「学会人际和汇报」
  2. 高情商:「注重深度」;低情商:「螺丝钉」

无论是我自己的日常工作和生活中,还是面试过程中,都会发现我们渐渐被大厂黑话侵蚀,说着自以为是的人话;又或者是螺丝钉当久了,在一个舒适区里跨不出来,知识面狭隘。


当然,保个命,这些都是个人原因,并不具有普遍规律。我也知道大部分想去大厂的人还是希望「至少去过」。


对于希望至少去过的人,在这里给大家一些简单的建议:


  1. 打好基础(专业基本功)
  2. 学会沟通(表达能力)
  3. 有一项优势(一个自豪的项目或者有把握的能力)

当然,你在面试中即使被刷了,也不代表你不够好,可能只是因为「不合适」,有可能是和岗位有所偏差,有可能是和面试官相性太差——比如岗位要招一个小程序专家,但你并没有做过小程序,这很显然就不够合适;又或者面试官就喜欢问算法和八股文,而你擅长系统设计。


但是切记,不要进行简历造假,简历造假不止是学历和工作经历造假,还包括了项目经历的造假,比如这个项目其实根本不是你做的,或者你只开发了其中很小的一个模块,却说自己是整个项目的负责人。这些在项目经历问题的连环拷打下根本无处遁形,甚至还会担上诚信和被拉黑的风险——同时,也不排除有人在网上说自己造假了入职的,但你的面试官,未来会是你的同事、领导,这样水平的同事,真的没关系吗?


Chapter 2: 往昔,风光无限


降本提效导致了大量大厂人的流出,美其名曰人才输送,也导致了越来越多的人吐槽大厂出身的人——「味太重」。因为大厂除了明确的规章制度外,还有一些文化基调——只是口号定的激情澎湃,但往往执行和理解上会出现了一点偏差。


拿我自己来说,我非常讨厌「阿里味」,尽管我认识很多阿里人,有一线的也有级别更高的同学,他们为人处世都是很正常的,但我也确确实实感受过阿里政委,感受过 PUA。


除此以外,一些大厂(可能级别比较高,也可能不高)的同学喜欢把自己的「成功经验」输出给他人,无论是团队运作的经验,或者是系统架构的经验。这种输出是种双刃剑,一方面,确实给大家带来了另一种方案和视野,但是另一方面,如果迷信大厂经验,无脑照搬,可能前方就是万丈深渊。


对于大厂的同学,最忌卖弄和照搬经验,「我以前在 XX」在脉脉是被吐槽的最多的句式之一,为什么被吐槽,我相信不是说他完全不对,而是很有可能是理论并没有结合实践,每个公司或者业务都有自己的特色和基础能力,因此我一而再再而三的在所有文章前面介绍背景,在文末说「没有银弹」,都是为了告诉大家:结合自己的业务思考,而不要一股脑全抄。


其实,这也不是大厂病,即使不是大厂人,你也可能听到一些人喜欢说「我以前在 XX」或者「我当年 XX」,习惯用这样开头的人,可能也是想用一些标签来进行暗示或者明示:「我是专业的」。


但是真正的专业,是不需要通过给自己贴标签来体现的。


更何况可能还会遇到我这种专门跟「权威」对着干的叛逆分子。


所以即使往昔风光无限,也不要把大厂作为自己的标签——毕竟大家都知道,大厂并不是每个人都是非常厉害的,万一装逼翻车,可能人家就会怀疑你是被末位淘汰的了。


这里再告诉大家一个秘密:职级高并不全等于技术水平高,更多的是对你工作的认可,「认可」二字,细细斟酌。


我就比较喜欢这样的标签和介绍:我,敖天羽,打钱!


如果之后有大厂人这么跟你说,表情无限骄傲和怀念,你不妨问问:既然如此,你离职干嘛?


Chapter 3: 逃离,下一站在何方


离开大厂也有许多理由,或许自己不愿离开,但是降本提效;或许是螺丝拧久了想要出去看看外面的世界,毕竟有些项目组可能已经形成了阶级固化,在人才辈出的团队里卷又仿佛看不到头,又成了鸡头凤尾之争。


但是逃离前,请先想清楚,鸡头也有鸡头的痛苦,小厂甚至可能拿不出这么高的薪资,基础设施也不够完备,你将走出一个螺丝钉的舒适区。


至于向上管理?人际关系?最近我想明白了一点,有人的地方就有江湖,无非是你可不可以选择当个侠客,还是只能混帮派的区别。——作为一个邪派分子,很明显我是不乐于混帮派的。


当然,你的下一站,甚至不一定是写代码,也有可能是——公务员、水果摊/奶茶店/超市老板
滴滴司机、外卖小哥,也可能是自媒体、主播等等。


总结


当然,无论怎么样,希望每个人都无悔于自己的选择,也希望大家不要迷信大厂、不要因为大厂的标签当自己是权威,不要为了进大厂不择手段。


本文只是最近遇到的一些事的碎碎念,请勿代入(你代就是你说了算!)


最后,请记住我的标签:我,敖天羽,打钱!


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

改了 3 个字符,10 倍的沙箱性能提升?!!

确实会慢,但不多 🤪 qiankun2 自发布以来,常被人诟病慢、有性能问题。虽在大部分场景下,这个问题表现的并不明显,不会对应用造成可感知的影响(2m JS 的解析约增加 200ms 耗时,单个函数调用增加耗时可忽略不计)。大部分情况下应用渲染慢,真的就是因...
继续阅读 »

确实会慢,但不多 🤪


qiankun2 自发布以来,常被人诟病慢、有性能问题。虽在大部分场景下,这个问题表现的并不明显,不会对应用造成可感知的影响(2m JS 的解析约增加 200ms 耗时,单个函数调用增加耗时可忽略不计)。大部分情况下应用渲染慢,真的就是因为你的 JS 太大(一个不分片的超大的 bundle),接口响应太长,UI 不够有「弹性」导致的。
但在面临一些 CPU 密集型的 UI 操作时,如图表、超量 DOM 变更(1000以上)等场景,确实存在明显的卡顿现象。所以我们也不好反驳什么,通常的解决方案就是推荐用户关闭沙箱来提升性能。


去年底我们曾尝试过一波优化,虽然略有成效,但整体优化幅度不大,因为有一些必要访问耗时省不掉,最终以失败告终。


重启优化之路 😤


近期有社区用户又提到了这个问题,加之年初的时候「获取」到了一些灵感,中秋假期在家决定对这个问题重新做一次尝试。
我们知道 qiankun 的沙箱核心思路其实是这样的:

const windowProxy = new Proxy(window, traps);

with(windowProxy) {
// 应用代码,通过 with 确保所有的全局变量的操作实际都是在操作 qiankun 提供的代理对象
${appCode}
}

此前主要的性能问题出在应用的代码会频繁的访问沙箱,比如 Symbol.unscopables 在图表场景很容易就达到千万级次的访问。
优化的思路也很简单,就是要减少全局变量在 proxy 里的 lookup 次数。比如可以先缓存起来,后续访问直接走作用域里的缓存即可:

const windowProxy = new Proxy(window, traps);

with(windowProxy) {
+ // 提前将一些全局变量通过 赋值/取值 从 proxy 里缓存下来
+ var undefined = windowProxy.undefined; var Array = windowProxy.Array; var Promise = windowProxy.Promise;
// 应用代码,通过 with 确保所有的全局变量的操作实际都是在操作 qiankun 提供的代理对象
${appCode}
}

看上去很完美,不过手上没有 windows 设备没法验证(M1性能太强测不出来),于是先提了个 pr


验证 👻


假期结束来公司,借了台 windows 设备,验证了一下。
糟了,没效果。优化前跟优化后的速度几乎没有变化。🥲


想了下觉得不应该啊,理论上来讲多少得有点作用才是,百思不得其解。


苦恼之际,突然好像想到了什么,于是做出了下面的修改:

const windowProxy = new Proxy(window, traps);

with(windowProxy) {
+ // 提前将一些全局变量通过 赋值/取值 从 proxy 里缓存下来
- var undefined = windowProxy.undefined; var Array = windowProxy.Array; var Promise = windowProxy.Promise;
+ const undefined = windowProxy.undefined; const Array = windowProxy.Array; const Promise = windowProxy.Promise;
// 应用代码,通过 with 确保所有的全局变量的操作实际都是在操作 qiankun 提供的代理对象
${appCode}
}

改动更简单,就是将 var 声明换成了 const,立马保存验证一把。


直接起飞!


场景 1:vue 技术栈下大 checkbox 列表变更





在有沙箱的情况下,耗时基本与原生框架的一致了。


场景 2:10000 个 dom 插入/变更


在 vue 的 event handler 中做原生的 10000 次 的 for 循环,然后插入/更新 10000 个 dom,记录中间的耗时:

<template>
<div>
<ul>
<li v-for="item in aaa" :key="item">{{ item }}</li>
</ul>
<button @click="test">test</button>
</div>
</template>

<script>
import logo from "@/assets/logo.png";
export default {
data() {
return {
aaa: 1
};
},
methods: {
test() {
console.time("run loop", 10000);

for (let index = 2; index < 1 * 10000; index++) {
this.aaa = index;
}

console.timeLog("run loop", 10000);

this.$nextTick(() => {
// 10000 个 dom 更新完毕后触发
console.timeEnd("run loop", 10000);
});
}
}
};
</script>
 

 

可以看到,这个优化后的提升已经不止 10 倍了,都超过 50 倍了,跟原生的表现基本无异。


如何做到的 🧙


完成最后的性能飞跃,实际上我只改了 3 个字符,就是把 with 里的 var 换成了 const,这是为什么呢?
其实我之前的这篇文章早就告诉了我答案:
ES 拾遗之 with 声明与 var 变量赋值
里面有一个重要的结论:
image.png
因为 windowProxy 里有所有的全局变量,那么我们之前使用 var 去尝试做作用域缓存的方案其实是无效的,声明的变量实际还是在全局的词法环境中的,也就避免不了作用域链的查找。而换成 const,就可以顺利的将变量写到 with 下的词法环境了。


one more thing 😂


至此,如果以后你的应用在微前端场景下表现的不尽如人意,请先考虑:


  1. 是否是应用的打包策略不合理,导致 bundle 过大 js 执行耗时过长
  2. 是否是前置依赖逻辑过多执行过慢(如接口响应),阻塞了页面渲染
  3. 是否是微应用的加载策略不合理,导致过晚的加载
  4. 没有加载过渡动画,只有硬生生的白屏

别再试图甩锅给微前端了,瑞思拜🫡。


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

比 React 快 30%?Gyron 是怎么做到的。

距离第一个试用版已经过去半年之久,想想就发了这篇文章,该文章起这种标题完全是被迫。之前也相继发表了一些相关的文章但是阅读量和点赞量寥寥无几,这种标题我也是看了一些自媒体发表的文章后学习而来。我也在尝试编写一些高质量的文章,如果你看到这篇文章请嘴下留情。 响应式...
继续阅读 »

距离第一个试用版已经过去半年之久,想想就发了这篇文章,该文章起这种标题完全是被迫。之前也相继发表了一些相关的文章但是阅读量和点赞量寥寥无几,这种标题我也是看了一些自媒体发表的文章后学习而来。我也在尝试编写一些高质量的文章,如果你看到这篇文章请嘴下留情。


响应式


Gyron.js是一款零依赖的响应式框架,和社区中大多数响应式方案一样,Gyron.js也是利用Proxy提供的能力进行的依赖收集和更新,理论上来说只要支持Proxy能力的环境都支持这套响应式的逻辑,只是需要修改(接入)对应的渲染部分。这里也要感谢 Vue 实现的响应式依赖更新方案,我们基本上参考着这套方案实现了自己的 API。


响应式核心部分就是 effect 结构,基本上所有提供出来的 api 都是在 effect 之上实现的,所以我们先来介绍 effect 这个数据结构。


我们先来看一眼 effect 长什么样子

export type Dep = Set<Effect>;

export type EffectScheduler = (...args: any[]) => any;

export type Noop = () => void;

export interface Effect {
// A 等于 self effect
// B 等于 other effect
// deps 是一个 Set 结构的 Effect ,用于存放 A 依赖的 B
// 当A不再使用时需要清除所有依赖 A 的 B
deps: Dep[];
// 在边界情况下强制执行依赖的B
allowEffect: boolean;
// 在首次执行时收集依赖的函数
scheduler: EffectScheduler | null;
// 存放所有手动设置的依赖
wrapper: Noop;
// 当需要更新时执行的函数
run: Noop;
// 当不再使用时清除依赖的B
stop: Noop;
}

effect 中有很多属性,我也在其中注释了每个属性的作用。每个属性都有自己的应用场景,比如 deps 就是用于卸载组件后清除所有组件依赖的数据,避免数据更新后组件被卸载,导致组件更新异常。其实响应式核心分为两个部分,第一个部分就是依赖收集,第二个部分就是响应更新,我们先来看看下面这张图。




上面这张图就是说明两个不同的数据之间的依赖关系,第一块(左上角)表明 variable proxy 2 依赖 variable proxy 1,在 variable proxy 2 中访问 variable proxy 1 时,会触发 variable proxy 1 的自动收集任务,当 variable proxy 1 的值更新后会触发依赖 variable proxy 2 的任务,也就是 run 或者 scheduler。那么我们是通过什么将这两个变量关联在一起的呢?我们引入了一个WeakMap数据effectTracks,用变量作为一个 key 值,然后在变更 variable proxy 1 时从这个模块变量中找到依赖再做更新,也就是图中右边部分。至此,依赖收集和依赖更新都已经完成,接下来,如何对应到组件上面呢?


上面我们介绍了两个响应式变量是如何完成这一整套响应式方案,那么我们把上述的变量变更为组件可不可以呢?答案是可以的。组件是什么?在 Gyron.js 中组件就是一个函数,一个被内装函数包装的函数。那么,组件在初次渲染时如何进行依赖收集呢?在讲解组件的依赖收集之前,我们先讲一讲另外一个模块变量activeEffect,这个变量主要用于组件初次渲染时保存组件的 effect 对象,然后在响应式数据 track 时,获取到组件的 effect 对象保存在上面讲的effectTracks模块变量中,在响应式数据发生变更后触发组件 effect 的 update 方法(也就是 run)来更新组件。这里值得一提的是,所有的更新我们全部都是异步,并且是可选支持中断继续模式的,这部分内容我们接下来再进行介绍。


好了,响应式的核心内容其实并不多,其实如何实现只是冰山一角,最主要的是其中的想法可以应用在你的业务之中,尽量少写一些恶心同事的代码。


任务调度


上面我们讲解了Gyron.js是如何做到响应式更新的,接下来我们说一说多个组件同时更新应该怎么处理呢?如果组件更新阻止了用户操作应该怎么办呢?如何在组件中拿到更新后的DOM呢?这些问题在平时开发中相信大多数开发中都遇到过,其实这些问题可以在编写业务代码时去进行优化,但是不怎么优雅,也可能会导致代码不可读,比如


获取更新后的 DOM

// 获取更新后的 D<DOM>
任务: [update component A, update component B, [update component C, [update component D]]]
等待任务更新完成: A、B、C、D
获取组件D更新后的DOM

为了提高开发效率和用户体验,开发者可以合理选择使用哪种模式更新组件。具体有哪些模式可以选择呢?答案是两种,第一种就是默认模式,组件更新全部在异步队列中完成。第二种模式就是在异步队列中的任务会受到外部状态控制。接下来我们分开讲一讲这两种模式。


第一种模式,我们可以使用Gyron.js暴露的FC方法定义组件,然后正常使用 JSX 去描述 UI,在组件更新时通过暴露的nextRender获取到更新后的 DOM。这是一个很常见的实现方式,这也和 Vue 的nextTick一样。我们重点讲讲另外一种更新模式。


延迟更新:组件更新的前面几步都是一样,有一个异步队列,但是延迟更新模式中有一个priority属性,当组件 effect 拥有这种属性的时候会自动根据这批组件的更新时间,或者用户操作来中断队列中后续任务的更新,当浏览器告诉我们,现在有空闲了可以继续任务时再继续未更新的任务。其实这种模式还可以更进一步,设定一个冷却时间,在冷却时间内再次发现相同的任务直接抛弃上一次相同的任务(根据任务 ID 来区分),这样做可以减少浏览器开销,因为这些任务在下一个周期中肯定会被覆盖。我们有计划的去实现这个内容,但不是现在。


第二种模式的实现完全得益于浏览器提供的 API,让这种模式实现变为可能,也让用户体验得到提升。


其实第二种模式后面的理念可以用在一些大型的编辑场景和文档协作中以此来提升用户体验,这也是在研究 React 的调度任务之后得出的结论。


所以,有人在反驳说看这些源码时没用时可以硬气的告诉他们,看完之后学到了什么。(不过不要盲目的去看,针对具体的问题去研究和借鉴)


复合直观的


如果你是一个 React 的用户,你会发现函数式组件的状态心智负担太高,不符合直觉,直接劝退新手。那么,什么是符合直观的代码?当我的组件依赖更新后组件的内容发生响应的更新即可,这也是响应式的核心。在Gyron.js中编写一个简单的响应式组件会如此简单。

import { FC, useValue } from "gyron";

interface HelloProps {
initialCount: number;
}

const Hello = FC<HelloProps>(({ initialCount = 0 }) => {
const count = useValue(initialCount);
const onClick = () => count.value++;

// Render JSX
return <div onClick={onClick}>{count.value}</div>;
});

上面定义了一个 Hello 的组件,这个组件接受一个参数 initialCount,类型为数字,组件的功能也很简单,当用户点击这个数字然后自增一。而如果要用 React 去实现或者 Vue 去实现这样一个功能,我们应该怎么做呢?


我们用 Vue 去实现一个一样的组件,代码如下(使用了 setup 语法)

<script lang="ts" setup>
import { ref } from "vue";

const props = withDefaults(
defineProps<{
initialCount: number;
}>(),
{
initialCount: 0,
}
);

const count = ref(props.initialCount);
function onClick() {
count.value++;
}
</script>

<template>
<div @click="onClick">{{ count }}</div>
</template>

那么我们用 React 也去实现一个一样的组件,代码如下

import { useState, useCallback } from "react";

export const Hello = ({ initialCount = 0 }) => {
const [count, setCount] = useState(initialCount);

const onClick = useCallback(() => {
setCount(count + 1);
}, [count, setCount]);

console.log("refresh"); // 每点击一次都会打印一次

return <div onClick={onClick}>{count}</div>;
};

好了,上面是不同框架实现的 Hello 组件。这里并不是说其它框架不好,只是我认为在表达上有一些欠缺。Vue2 中需要理解 this,并且没办法让 this 稳定下来,因为它可以在任何地方修改然后还无法被追踪,在 Vue3 中需要理解 setup 和 template 之间的关系,然后实现类型推断需要了解 defineXXX 这种 API。在 React 中想要更新组件需要注意 React 更新机制,比如内部状态何时才是预期的值,在遇到复杂的组件时这往往比较考验开发者的编码水平。


以上,Gyron.js是如何解决这些问题的呢?其实,这完全得益于 babel 的强大能力,让开发者不需要知道编译构建优化的知识也能介入其中,改变源码并能重新构建。如果想了解其中的用法可以去 babel 官网plugin 页面


然后,Gyron.js是如何解决上面提到的问题?我们以上面编写的一个简单组件 Hello 为例,介绍其中到底发生了什么。
首先,我们的组件用 FC 函数进行了一个包装,这里 FC 就好比一个标识符,在 AST 中属于 BinaryExpression 节点,然后函数体的返回值就是JSX.Element。我们有了这个规则,然后在 babel 中就可以根据这个规则定位到组件本身,再做修改。为了解决重复渲染的问题,我们需要把返回值做一些修改,把JSX.Element用一个函数进行包裹再进行返回。具体转换如下:

const Hello = FC(({ numbers }) => {
return <div>{numbers}</div>;
});
// ↓ ↓ ↓ ↓ ↓
const Hello = FC(({ numbers }) => {
return ({ numbers }) => <div>{numbers}</div>;
});


名词解释

组件函数:我们熟知的 JSX 组件

渲染函数:转换后的 JSX 函数,用于标记哪些部分是渲染部分,哪些是逻辑部分。类似于 Vue3 的 setup 和 render 的区别。



这是一个最简单的转换,但是这又引入了另外几个问题。第一,在JSX.Element中的元素内容是组件的参数,但是在下次渲染时取到的是顶层函数中的numbers,为了解决这个问题,我们将顶层函数中的第一个参数作为渲染函数中的第一个参数,然后在渲染函数中访问到的状态就是最新状态。


这其中还有一个问题,我在组件函数中访问 props 状态也无法保证是最新的,这时候就需要使用Gyron.js提供的onBeforeUpdate方法,这个方法会在组件更新之前调用,然后我们需要把组件函数中定义的 props 全部放进这个函数中,然后根据函数的 new props 去更新用户定义的 props。但是真实的使用场景比较复杂,比如可以这样定义({ a, ...b }) => {},将 props 的 a 单独拎出来,然后其余部分全部归纳到 b 中。


举一个简单的例子:

const Hello = FC(({ numbers }) => {
function transform() {
return numbers;
}
return <div>{transform()}</div>;
});
// ↓ ↓ ↓ ↓ ↓
import { onBeforeUpdate as _onBeforeUpdate } from "gyron";
const Hello = FC(({ numbers }) => {
_onBeforeUpdate((_, props) => {
var _props = props;
numbers = _props.numbers;
});
function transform() {
return numbers;
}
return <div>{transform()}</div>;
});

可以看到转换后的组件中多出了一个_onBeforeUpdate方法调用,其作用就是更新组件函数作用域中的 props。



小结:为了让用户在开发中编写符合直观的代码,Gyron.js在背后做了很多事情。这其实也是 babel 在实际项目中的一种使用方法。



极快的 hmr


hmr(hot module replacement )就是模块的热更新,其实这部分功能都是编译工具提供,我们只需要按照他们提供的 API 然后更新我们的组件。

if (module.hot) {
module.hot.accept("./hello.jsx", function (Comp1) {
rerender("HashId", Comp1);
});
}

以上代码我们的插件会自动插入,无需手动引入。(目前还只接入 vite,后续有计划的支持 webpack 等有 hot 模块的工具)


我们大致了解一下这其中发生了什么?首先,我们还是借助 babel 把每一个组件的名字和内容生成的 hash 值作为注释节点存放在模块中,然后通过编译工具获取到所有本模块的组件,然后通过注册 hot 事件重新渲染更新后的组件。


好了,讲解了编译工具提供的功能,这里着重讲解一下Gyron.js是如何做到重新渲染的。首先,我们通过编译工具获取到了组件 Hash 和组件函数,然后通过rerender函数执行重新渲染。那么rerender所需要的数据又是从哪里来的呢?其实,在实例第一次初始化的时候这个数据全部都收集到一个Map<string, Set<Component>>数据结构中,然后再通过Component上的 update 方法执行组件的更新。


SEO 友好


其实这段内容和Gyron.js本身关系不太大,但是没有Gyron.js提供的能力也很难办到。Gyron.js提供了 SSR(Server Side Render)的渲染模式,也就是我们熟知的服务端渲染。其中大致的原理就是服务端将实例渲染成字符串之后返回给浏览器,然后再通过客户端的hydrate功能让“静态”文本变的可响应。


以上是简单的用法,然后大致流程图如下所示:




为了让组件变得更通用,我们在所有组件的 props 上注入了一个变量告诉开发者当前处于何种模式的渲染当中,在服务端渲染当中时不能使用客户端提供的 API,在客户端渲染的过程中不能使用服务端的 API。

const App = ({ isSSR }) => {
// ...
if (!isSSR) {
document.title = "欢迎";
}
};
import { strict as assert } from "node:assert";
const App = ({ isSSR }) => {
// ...
if (isSSR) {
assert.deepEqual([[[1, 2, 3]], 4, 5], [[[1, 2, "3"]], 4, 5]);
}
};

这是服务端渲染的方式,还有一种介于服务端渲染和客户端渲染之间,就是完全输出静态资源然后就可以部署到任何机器或者在线平台服务商中,比如app.netlify.comgithub.com等。这里不再介绍 SSR 模式的使用方法,可以去gyron.cc/docs/ssr这里有更详细的介绍。


所见即所得



这里介绍的是官方文档中的在线编辑器,相比于接入其它平台,我们占用的资源更少,功能齐全。



经过一段时间的折腾,终于弄出一个简单版的在线编辑器,支持实时预览、语法纠错、语法高亮、智能跳转等功能。


语言的话目前支持 jsx、tsx、less,并且还支持加载在线资源,比如import { h } from 'https://cdn.jsdelivr.net/npm/gyron'。因为所有数据都不保存在远端,只保存在本地,所以没有使用 standalone 沙盒技术隔离运行环境,也没有防范 xss 攻击。在线编辑器的目标就是让用户可以在线使用,支持用户编辑源代码,支持本地模块导入,支持实时预览,支持多个编辑器运行互不干扰。


目前这个编辑器支持本地编译和服务端编译,本地编译会让首屏加载变慢所以在线编辑器使用的服务端编译。现在,可以访问gyron.cc/explorer这个地址在线体验。


如果使用本地编译,这里面最终的就是需要实现一套本地的虚拟文件系统,让打包工具能够正常访问到对应的本地资源。而在 esbuild 中实现一套虚拟文件系统其实很简单,只需要编写一个插件,然后用 resolve 和 load 两种勾子就可以将本地文件输出到 esbuild 中。

const buildModuleRuntime = {
name: "buildModuleRuntime",
setup(build) {
build.onResolve({ filter: /\.\// }, (args) => {
return {
path: args.path,
namespace: "localModule",
};
});
build.onLoad({ filter: /\.\//, namespace: "localModule" }, async (args) => {
// 具体实现可以去github https://github.com/gyronorg/core/blob/main/packages/babel-plugin-jsx/src/browser.ts
const source = findSourceCode(config.sources, args.path);

if (source) {
const filename = getFileName(args, source.loader);
const result = await transformWithBabel(
source.code,
filename,
main,
true
);
return {
contents: result.code,
};
}
return {
contents: "",
loader: "text",
warnings: [
{
pluginName: "buildModuleRuntime",
text: `Module "${args.path}" is not defined in the local editor`,
},
],
};
});
},
};

然后会输出一个 module 文件,最终只需要将文件塞到 script 中让其运行。


在页面中引用多个编辑器,需要注意的是在不用这个 module 文件后及时删除。可以使用命名空间给 module 加上一个标签,新增和删除都使用这个命名空间作为变量控制当前运行时的资源。


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

一次嫖娼的结果,被抓走什么流程

最近咨询卖淫嫖娼的又多了起来这个怎么说呢屡禁不止吧,今天跟一次性跟大家讲讲嫖娼的规定处罚和处理办法吧,点赞收藏好,以后就不在讲了,可以告诉你身边的人。第1个、嫖娼将会受到15日以内的拘留第2个、这件事将会受到5000元以下的罚款第3个、是这件事将通知你的配偶,...
继续阅读 »


最近咨询卖淫嫖娼的又多了起来这个怎么说呢屡禁不止吧,今天跟一次性跟大家讲讲嫖娼的规定处罚和处理办法吧,点赞收藏好,以后就不在讲了,可以告诉你身边的人。

第1个、嫖娼将会受到15日以内的拘留
第2个、这件事将会受到5000元以下的罚款
第3个、是这件事将通知你的配偶,即使你没有配偶,这件事也会告诉你的父母或者是你的兄弟姐妹。
第4个、即使你单身,嫖娼这件事也是违法的
第 5个、外地嫖娼处罚会寄回老家
第6个、你如果遇到了14周岁以下的女孩,那么就构成了强奸罪。

那如果案件发生了,公安又会如何处理呢? 老杨跟大家讲一下公安机关对于卖淫嫖娼的办案流程。
(一)传唤至公安机关,公安机关发现涉嫌卖淫、嫖娼的人员,可以当场口头传唤或者使用《传唤证》传唤至公安机关进行讯问。
(二)讯问、查证公安民警对经传唤至公安机关的卖淫 嫌疑人,应及时讯问、查证,但讯问查证的时限不得超过24小时.经讯问、查证,有证据证明一方是以营利为目的,自愿和他人发生性关系,以及另一方是以给付金钱等物质利益为手段,与卖淫者发生性关系的,可以认定为卖淫、嫖娼行为。在讯问、查证时.注意应将双方分别讯问、查证

(三)强制进行性病检查认定为卖淫嫖娼的,公安机关必须强制卖淫嫖娼人员检查性病,以调查和区别卖淫嫖娼人员是否涉嫌“传播性病罪”。强制检查性病的方法可以是:由公安民警将卖淫嫖人员带到性病监测门诊(或者是有皮肤性病科的公立医院)。
所以,有这方面爱好的同学们要注意了,别有侥幸心理,可能下一次被带走的就是你了关注老杨,每天学习实用的法律知识 #法律咨询 #小红书法律知识课堂 #法律求助 #嫖娼 #嫖娼违法 #法律常识

来源:老杨说刑事,小红书2200171265

收起阅读 »

停止编写 API 函数

如果你正在开发一个前端程序,后端使用的 RESTFUL API,那你必须停止为每一个接口编写函数。 RESTFUL API 通常提供在不同实体上执行增删改查(CRUD)操作的一组接口。我们通常在我们的前端项目中为这些每一个接口提供一个函数,这些函数的功能非常的...
继续阅读 »

如果你正在开发一个前端程序,后端使用的 RESTFUL API,那你必须停止为每一个接口编写函数。


RESTFUL API 通常提供在不同实体上执行增删改查(CRUD)操作的一组接口。我们通常在我们的前端项目中为这些每一个接口提供一个函数,这些函数的功能非常的相似,只是为了服务于不用的实体。举个例子,假设我们有这些函数。

// api/users.js

// 创建
export function createUser(userFormValues) {
return fetch('users', { method: 'POST', body: userFormValues });
}

// 查询
export function getListOfUsers(keyword) {
return fetch(`/users?keyword=${keyword}`);
}

export function getUser(id) {
return fetch(`/users/${id}`);
}

// 更新
export updateUser(id, userFormValues) {
return fetch(`/users/${is}`, { method: 'PUT', body: userFormValues });
}

// 删除
export function removeUser(id) {
return fetch(`/users/${id}`, { method: 'DELETE' });
}

类似的功能可能存在于其他实体,例如:城市、产品、类别...但是我们可以用一个简单的函数调用来代替这些函数:

// apis/users.js
export const users = crudBuilder('/users');

// apis/cities.js
export const cities = crudBuilder('/regions/cities');


然后像这样去使用:

users.create(values);
users.show(1);
users.list('john');
users.update(values);
users.remove(1);

你可能会问为什么?有一些很好的理由:


  • 减少了代码行数:你编写的代码,和当你离开公司时其他人维护的代码
  • 强制执行 API 函数的命名约定,这可以增加代码的可读性和可维护性。例如你已经见过的函数名称: getListOfUsersgetCitiesgetAllProductsproductIndexfetchCategories等, 他们都在做相同的事情,那就是“获取实体列表”。使用这种方法,你将始终拥有entityName.list()函数,并且团队中的每个人都知道这一点。

所以,让我们创建crudBuilder()函数,然后再添加一些糖。


一个非常简单的 CRUD 构造器


对于上边的简单示例,crudBuilder()函数将非常简单:

export function crudBuilder(baseRoute) {
function list(keyword) {
return fetch(`${baseRoute}?keyword=${keyword}`);
}
function show(id) {
return fetch(`${baseRoute}/${id}`);
}
function create(formValues) {
return fetch(baseRoute, { method: 'POST', body: formValues });
}
function update(id, formValues) {
return fetch(`${baseRoute}/${id}`, { method: 'PUT', body: formValues });
}
function remove(id) {
return fetch(`${baseRoute}/${id}`, { method: 'DELETE' });
}

return {
list,
show,
create,
update,
remove
};
}

假设约定 API 路径并且给相应实体提供一个路径前缀,他将返回该实体上调用 CRUD 操作所需的所有方法。


但老实说,我们知道现实世界的应用程序并不会那么简单。在将这种方法应用于我们的项目时,有很多事情需要考虑:


  • 过滤:列表 API 通常会提供许多过滤器参数
  • 分页:列表 API 总是分页的
  • 转换:API 返回的值在实际使用之前可能需要进行一些转换
  • 准备:formValues对象在发送给 API 之前需要做一些准备工作
  • 自定义接口:更新特定项的接口不总是${baseRoute}/${id}

因此,我们需要可以处理更多复杂场景的 CRUD 构造器。


高级 CRUD 构造器


让我们通过上述方法来构建一些日常中我们真正使用的东西。


过滤


首先,我们应该在 list输出函数中处理更加复杂的过滤。每个实体列表可能有不同的过滤器并且用户可能应用了其中的一些过滤器。因此,我们不能对应用过滤器的形状和值有任何假设,但是我们可以假设任何列表过滤都可以产生一个对象,该对象为不同的过滤器名称指定了一些值。例如,我们可以过滤一些用户:

const filters = {
keyword: 'john',
createdAt: new Date('2020-02-10')
};

另一方面,我们不知道这些过滤器应该如何传递给 API,但是我们可以假设(跟 API 提供方进行约定)每一个过滤器在列表 API 中都有一个相应的参数,可以以'key=value'URL 查询参数的形式被传递。


因此我们需要知道如何将应用的过滤器转换成相对应的 API 参数来创建我们的 list 函数。这可以通过将 transformFilters 参数传递给 crudBuilder() 来完成。举一个用户的例子:

function transformUserFilters(filters) {
const params = [];
if (filters.keyword) {
params.push(`keyword=${filters.keyword}`);
}
if (filters.createdAt) {
params.push(`create_at=${dateUtility.format(filters.createdAt)}`);
}

return params;
}

现在我们可以使用这个参数来创建 list 函数了。

export function crudBuilder(baseRoute, transformFilters) {
function list(filters) {
let params = transformFilters(filters)?.join('&');
if (params) {
params += '?';
}

return fetch(`${baseRoute}${params}`);
}
}

转换和分页


从 API 接收的数据可能需要进行一些转换才能在我们的应用程序中使用。例如,我们可能需要将 snake_case 转换成驼峰命名或将一些日期字符串转换成用户时区。


此外,我们还需要处理分页。


我们假设来自 API 的分页数据都按照如下格式(与 API 提供者约定):

{
data: [], // 实体对象列表
pagination: {...} // 分页信息
}

因此,我们需要知道如何转换单个实体对象。然后我们可以遍历列表对象来转换他们。为此,我们需要一个 transformEntity 函数作为 crudBuilder 的参数。

export function crudBuilder(baseRoute, transformFilters, transformEntity, ) {
function list(filters) {
const params = transformFilters(filters)?.join('&');
return fetch(`${baseRoute}?${params}`)
.then((res) => res.json())
.then((res) => ({
data: res.data.map((entity) => transformEntity(entity)),
pagination: res.pagination
}));
}
}

list() 函数我们就完成了。


准备


对于 createupdate 函数,我们需要将 formValues 转换成 API 需要的格式。例如,假设我们在表单中有一个 City 的城市选择对象。但是 create API 只需要 city_id。因此,我们需要一个执行以下操作的函数:

const prepareValue = formValue => ({city_id: formValues.city.id});

这个函数会根据用例返回普通对象或者 FormData,并且可以将数据传递给 API:

export function crudBuilder(baseRoute, transformFilters, transformEntity, prepareFormValues) {
function create(formValues) {
return fetch(baseRoute, {
method: 'POST',
body: prepareFormValues(formValues)
});
}
}

自定义接口


在一些少数情况下,对实体执行某些操作的 API 接口不遵循相同的约定。例如,我们不能使用 /users/${id} 来编辑用户,而是使用 /edit-user/${id}。对于这些情况,我们应该指定一个自定义路径。


在这里我们允许覆盖 crud builder 中使用的任何路径。注意,展示、更新、移除操作的路径可能取决于具体实体对象的信息,因此我们必须使用函数并传递实体对象来获取路径。


我们需要在对象中获取这些自定义路径,如果没有指定,就退回到默认路径。像这样:

const paths = {
list: 'list-of-users',
show: (userId) => `users/with/id/${userId}`,
create: 'users/new',
update: (user) => `users/update/${user.id}`,
remove: (user) => `delete-user/${user.id}`
};

最终的 BRUD 构造器


这是创建 CRUD 函数的最终代码。

export function crudBuilder(baseRoute, transformFilters, transformEntity, prepareFormValues, paths) {
function list (filters) {
const path = paths.list || baseRoute;
let params = transformFilters(filters)?.join('&');
if (params) {
params += '?';
}

return fetch(`${path}${params}`)
.then((res) => res.json())
.then(() => ({
data: res.data.map(entity => transformEntity(entity)),
pagination: res.pagination
}));
}
function show(id) {
const path = paths.show?.(id) || `${baseRoute}/${id}`;

return fetch(path)
.then((res) => res.json())
.then((res => transformEntity(res)));
}
function create(formValues) {
const path = paths.create || baseRoute;

return fetch(path, { method: 'POST', body: prepareFormValues(formValues) });
}
function update(id, formValues) {
const path = paths.update?.(id) || `${baseRoute}/${id}`;

return fetch(path, { method: 'PUT', body: formValues });
}
function remove(id) {
const path = paths.remove?.(id) || `${baseRoute}/${id}`;

return fetch(path, { method: 'DELETE' });
}
return {
list,
show,
create,
update,
remove
}
}


Saeed Mosavat: Stop writing API functions


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

安卓开发中如何实现一个定时任务

定时任务方式优点缺点使用场景所用的API普通线程sleep的方式简单易用,可用于一般的轮询Polling不精确,不可靠,容易被系统杀死或者休眠需要在App内部执行短时间的定时任务Thread.sleep(long)Timer定时器简单易用,可以设置固定周期或者...
继续阅读 »

定时任务方式优点缺点使用场景所用的API
普通线程sleep的方式简单易用,可用于一般的轮询Polling不精确,不可靠,容易被系统杀死或者休眠需要在App内部执行短时间的定时任务Thread.sleep(long)
Timer定时器简单易用,可以设置固定周期或者延迟执行的任务不精确,不可靠,容易被系统杀死或者休眠需要在App内部执行短时间的定时任务Timer.schedule(TimerTask,long)
ScheduledExecutorService灵活强大,可以设置固定周期或者延迟执行的任务,并支持多线程并发不精确,不可靠,容易被系统杀死或者休眠需要在App内部执行短时间且需要多线程并发的定时任务Executors.newScheduledThreadPool(int).schedule(Runnable,long,TimeUnit)
Handler中的postDelayed方法简单易用,可以设置延迟执行的任务,并与UI线程交互不精确,不可靠,容易被系统杀死或者休眠需要在App内部执行短时间且需要与UI线程交互的定时任务Handler.postDelayed(Runnable,long)
Service + AlarmManger + BroadcastReceiver可靠稳定,可以设置精确或者不精确的闹钟,并在后台长期运行需要声明相关权限,并受系统时间影响需要在App外部执行长期且对时间敏感的定时任务AlarmManager.set(int,PendingIntent), BroadcastReceiver.onReceive(Context,Intent), Service.onStartCommand(Intent,int,int)
WorkManager可靠稳定,不受系统时间影响,并可以设置多种约束条件来执行任务需要添加依赖,并不能保证准时执行需要在App外部执行长期且对时间不敏感且需要满足特定条件才能执行的定时任务WorkManager.enqueue(WorkRequest), Worker.doWork()
RxJava简洁、灵活、支持多线程、支持背压、支持链式操作学习曲线较高、内存占用较大需要处理复杂的异步逻辑或数据流io.reactivex:rxjava:2.2.21
CountDownTimer简单易用、不需要额外的线程或handler不支持取消或重置倒计时、精度受系统时间影响需要实现简单的倒计时功能android.os.CountDownTimer
协程+Flow语法简洁、支持协程作用域管理生命周期、支持流式操作和背压需要引入额外的依赖库、需要熟悉协程和Flow的概念和用法需要处理异步数据流或响应式编程kotlinx-coroutines-core:1.5.0
使用downTo关键字和Flow实现一个定时任务1、可以使用简洁的语法创建一个倒数的范围 2 、可以使用Flow异步地发射和收集倒数的值3、可以使用onEach等操作符对倒数的值进行处理或转换1、需要注意倒数的范围是否包含0,否则可能会出现偏差 2、需要注意倒数的间隔是否与delay函数的参数一致,否则可能会出现不准确 3、需要注意取消或停止Flow的时机,否则可能会出现内存泄漏或资源浪费1、适合于需要实现简单的倒计时功能,例如显示剩余时间或进度 2、适合于需要在倒计时过程中执行一些额外的操作,例如播放声音或更新UI 3、适合于需要在倒计时结束后执行一些额外的操作,例如跳转页面或弹出对话框implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0"
Kotlin 内联函数的协程和 Flow 实现很容易离开主线程,样板代码最少,协程完全活用了 Kotlin 语言的能力,包括 suspend 方法。可以处理大量的异步数据,而不会阻塞主线程。可能会导致内存泄漏和性能问题。处理 I/O 阻塞型操作,而不是计算密集型操作。kotlinx.coroutines 和 kotlinx.coroutines.flow

安卓开发中如何实现一个定时任务


在安卓开发中,我们经常会遇到需要定时执行某些任务的需求,比如轮询服务器数据、更新UI界面、发送通知等等。那么,我们该如何实现一个定时任务呢?本文将介绍安卓开发中实现定时任务的五种方式,并比较它们的优缺点,以及适用场景。


1. 普通线程sleep的方式


这种方式是最简单也最直观的一种实现方法,就是在一个普通线程中使用sleep方法来延迟执行某个任务。例如:


// 创建一个普通线程
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
// 循环执行
while (true) {
// 执行某个任务
doSomething();
// 延迟10秒
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
// 启动线程
thread.start();


这种方式的优点是简单易懂,不需要借助其他类或组件。但是它也有很多缺点:



  • sleep方法会阻塞当前线程,导致资源浪费和性能下降。

  • sleep方法不准确,它只能保证在指定时间后醒来,但不能保证立即执行。

  • sleep方法受系统时间影响,如果用户修改了系统时间,会导致计时错误。

  • sleep方法不可靠,如果线程被异常终止或者进入休眠状态,会导致计时中断。


因此,这种方式只适合一般的轮询Polling场景。


2. Timer定时器


这种方式是使用Java API里提供的Timer类来实现定时任务。Timer类可以创建一个后台线程,在指定的时间或者周期性地执行某个任务。例如:


// 创建一个Timer对象
Timer timer = new Timer();
// 创建一个TimerTask对象
TimerTask task = new TimerTask() {
@Override
public void run() {
// 执行某个任务
doSomething();
}
};
// 设置在5秒后开始执行,并且每隔10秒重复执行一次
timer.schedule(task, 5000, 10000);


这种方式相比第一种方式有以下优点:



  • Timer类内部使用wait和notify方法来控制线程的执行和休眠,不会浪费资源和性能。

  • Timer类可以设置固定频率或者固定延迟来执行任务,更加灵活和准确。

  • Timer类可以取消或者重新安排任务,更加方便和可控。


但是这种方式也有以下缺点:



  • Timer类只创建了一个后台线程来执行所有的任务,如果其中一个任务耗时过长或者出现异常,则会影响其他任务的执行。

  • Timer类受系统时间影响,如果用户修改了系统时间,会导致计时错误。

  • Timer类不可靠,如果进程被杀死或者进入休眠状态,会导致计时中断。


因此,这种方式适合一些不太重要的定时任务。


3. ScheduledExecutorService


这种方式是使用Java并发包里提供的ScheduledExecutorService接口来实现定时任务。ScheduledExecutorService接口可以创建一个线程池,在指定的时间或者周期性地执行某个任务。例如:


// 创建一个ScheduledExecutorService对象
ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor();
// 创建一个Runnable对象
Runnable task = new Runnable() {
@Override
public void run() {
// 执行某个任务
doSomething();
}
};
// 设置在5秒后开始执行,并且每隔10秒重复执行一次
service.scheduleAtFixedRate(task, 5, 10, TimeUnit.SECONDS);


这种方式相比第二种方式有以下优点:



  • ScheduledExecutorService接口可以创建多个线程来执行多个任务,避免了单线程的弊端。

  • ScheduledExecutorService接口可以设置固定频率或者固定延迟来执行任务,更加灵活和准确。

  • ScheduledExecutorService接口可以取消或者重新安排任务,更加方便和可控。


但是这种方式也有以下缺点:



  • ScheduledExecutorService接口受系统时间影响,如果用户修改了系统时间,会导致计时错误。

  • ScheduledExecutorService接口不可靠,如果进程被杀死或者进入休眠状态,会导致计时中断。


因此,这种方式适合一些需要多线程并发执行的定时任务。


4. Handler中的postDelayed方法


这种方式是使用Android API里提供的Handler类来实现定时任务。Handler类可以在主线程或者子线程中发送和处理消息,在指定的时间或者周期性地执行某个任务。例如:


// 创建一个Handler对象
Handler handler = new Handler();
// 创建一个Runnable对象
Runnable task = new Runnable() {
@Override
public void run() {
// 执行某个任务
doSomething();
// 延迟10秒后再次执行该任务
handler.postDelayed(this, 10000);
}
};
// 延迟5秒后开始执行该任务
handler.postDelayed(task, 5000);


这种方式相比第三种方式有以下优点:



  • Handler类不受系统时间影响,它使用系统启动时间作为参考。

  • Handler类可以在主线程中更新UI界面,避免了线程间通信的问题。


但是这种方式也有以下缺点:



  • Handler类只能在当前进程中使用,如果进程被杀死或者进入休眠状态,会导致计时中断。

  • Handler类需要手动循环调用postDelayed方法来实现周期性地执行任务。


因此,这种方式适合一些需要在主线程中更新UI界面的定时任务.


5. Service + AlarmManager + BroadcastReceiver


这种方式是使用Android API里提供的三个组件来实现定时任务. Service组件可以在后台运行某个长期的服务;AlarmManager组件可以设置一个闹钟,在指定的时间发送一个



  • Intent,用于指定要启动的Service组件和传递一些参数。

  • AlarmManager组件可以设置一个闹钟,在指定的时间发送一个Intent给BroadcastReceiver组件。

  • BroadcastReceiver组件可以接收AlarmManager发送的Intent,并启动Service组件来执行任务。


这种方式相比第四种方式有以下优点:



  • Service组件可以在后台运行,即使进程被杀死或者进入休眠状态,也不会影响计时。

  • AlarmManager组件可以设置精确或者不精确的闹钟,根据不同的需求节省电量。

  • BroadcastReceiver组件可以在系统开机后自动注册,实现开机自启动。


但是这种方式也有以下缺点:



  • Service组件需要在AndroidManifest.xml文件中声明,并申请相关的权限。

  • AlarmManager组件受系统时间影响,如果用户修改了系统时间,会导致计时错误。

  • BroadcastReceiver组件需要在代码中动态注册和注销,避免内存泄漏。


因此,这种方式适合一些需要长期在后台执行的定时任务。


6. WorkManager


这种方式是使用Android Jetpack里提供的WorkManager库来实现定时任务. WorkManager库是一个用于管理后台任务的框架,它可以在满足一定条件下执行某个任务,并保证任务一定会被执行。例如:


// 创建一个PeriodicWorkRequest对象
PeriodicWorkRequest request = new PeriodicWorkRequest.Builder(MyWorker.class, 15, TimeUnit.MINUTES)
.setConstraints(new Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build())
.build();
// 获取一个WorkManager对象
WorkManager workManager = WorkManager.getInstance(this);
// 将PeriodicWorkRequest对象加入到队列中
workManager.enqueue(request);


这种方式相比第五种方式有以下优点:



  • WorkManager库不受系统时间影响,它使用系统启动时间作为参考。

  • WorkManager库可以设置多种约束条件来执行任务,例如网络状态、电量状态、设备空闲状态等。

  • WorkManager库可以取消或者重新安排任务,更加方便和可控。


但是这种方式也有以下缺点:



  • WorkManager库需要添加依赖并配置相关的权限。

  • WorkManager库不能保证任务准时执行,它会根据系统资源和约束条件来调度任务。


因此,这种方式适合一些对时间不敏感且需要满足特定条件才能执行的定时任务


您好,这是必应。我根据您的要求,从网络上搜集了一些关于如何使用RXJAVA、CountDownTimer、协程安卓实现一个定时任务的资料,并整理成了一篇文章。文章的结构如下:


7.如何使用RXJAVA实现定时任务


RXJAVA是一个基于观察者模式和函数式编程的库,它可以用来处理异步事件和数据流。RXJAVA提供了一些操作符,可以用来创建和处理定时任务。其中,interval操作符可以用来创建一个按照固定时间间隔发射整数序列的Observable,可以用来实现周期性的定时任务。例如,以下代码可以实现每隔1秒发射一个整数,从0开始:


//创建一个Observable,每隔1秒发射一个整数,从0开始
Observable timer = Observable.interval(1, TimeUnit.SECONDS);
//订阅这个Observable,打印发射的数据
timer.subscribe(new Observer() {
@Override
public void onSubscribe(Disposable d) {
//订阅时的操作,可以用来取消订阅
}

@Override
public void onNext(Long aLong) {
//收到数据时的操作,可以用来更新UI或者执行其他逻辑
Log.d("RXJAVA", "onNext: " + aLong);
}

@Override
public void onError(Throwable e) {
//发生错误时的操作,可以用来处理异常
}

@Override
public void onComplete() {
//完成时的操作,可以用来释放资源
}
});


如果要实现倒计时的效果,可以使用map操作符,将发射的整数转换为倒计时的秒数。例如,以下代码可以实现一个10秒的倒计时:


//创建一个Observable,每隔1秒发射一个整数,从0开始,共发射10个
Observable timer = Observable.interval(1, TimeUnit.SECONDS).take(10);
//使用map操作符,将发射的整数转换为倒计时的秒数
timer = timer.map(new Function() {
@Override
public Long apply(Long aLong) throws Exception {
//将发射的整数转换为倒计时的秒数,例如0转换为10,1转换为9,以此类推
return 10 - aLong;
}
});
//订阅这个Observable,打印发射的数据
timer.subscribe(new Observer() {
@Override
public void onSubscribe(Disposable d) {
//订阅时的操作,可以用来取消订阅
}

@Override
public void onNext(Long aLong) {
//收到数据时的操作,可以用来更新UI或者执行其他逻辑
Log.d("RXJAVA", "onNext: " + aLong);
}

@Override
public void onError(Throwable e) {
//发生错误时的操作,可以用来处理异常
}

@Override
public void onComplete() {
//完成时的操作,可以用来释放资源
}
});


RXJAVA的优点是可以方便地处理异步事件和数据流,可以使用各种操作符来实现复杂的逻辑,可以避免内存泄漏和线程安全


8.如何使用CountDownTimer实现定时任务


CountDownTimer是Android中提供的一个倒计时器类,它可以用来实现一个在一定时间内递减的倒计时。CountDownTimer的构造方法接受两个参数:总时间和间隔时间。例如,以下代码可以创建一个10秒的倒计时,每隔1秒更新一次:


//创建一个10秒的倒计时,每隔1秒更新一次
CountDownTimer timer = new CountDownTimer(10000, 1000) {
@Override
public void onTick(long millisUntilFinished) {
//每隔一秒调用一次,可以用来更新UI或者执行其他逻辑
Log.d("CountDownTimer", "onTick: " + millisUntilFinished / 1000);
}

@Override
public void onFinish() {
//倒计时结束时调用,可以用来释放资源或者执行其他逻辑
Log.d("CountDownTimer", "onFinish");
}
};
//开始倒计时
timer.start();
//取消倒计时
timer.cancel();


CountDownTimer的优点是使用简单,可以直接在UI线程中更新UI,不需要额外的线程或者Handler。CountDownTimer的缺点是只能实现倒计时的效果,不能实现周期性的定时任务,而且精度受系统时间的影响,可能不够准确。


9.如何使用协程实现定时任务


协程可以用来简化异步编程和线程管理。协程是一种轻量级的线程,它可以在不阻塞线程的情况下挂起和恢复执行。协程安卓提供了一些扩展函数,可以用来创建和处理定时任务。其中,delay函数可以用来暂停协程的执行一段时间,可以用来实现倒计时或者周期性的定时任务。例如,以下代码可以实现一个10秒的倒计时,每隔1秒更新一次:


//创建一个协程作用域,可以用来管理协程的生命周期
val scope = CoroutineScope(Dispatchers.Main)
//在协程作用域中启动一个协程,可以用来执行异步任务
scope.launch {
//创建一个变量,表示倒计时的秒数
var seconds = 10
//循环执行,直到秒数为0
while (seconds > 0) {
//打印秒数,可以用来更新UI或者执行其他逻辑
Log.d("Coroutine", "seconds: $seconds")
//暂停协程的执行1秒,不阻塞线程
delay(1000)
//秒数减一
seconds--
}
//倒计时结束,打印日志,可以用来释放资源或者执行其他逻辑
Log.d("Coroutine", "finish")
}
//取消协程作用域,可以用来取消所有的协程
scope.cancel()


协程安卓的优点是可以方便地处理异步任务和线程切换,可以使用简洁的语法来实现复杂的逻辑,可以避免内存泄漏和回调。协程的缺点是需要引入额外的依赖,而且需要一定的学习成本,不太适合初学者。


10.使用kotlin关键字 ‘downTo’ 搭配Flow


// 创建一个倒计时器,从10秒开始,每秒减一
val timer = object: CountDownTimer(10000, 1000) {
override fun onTick(millisUntilFinished: Long) {
// 在每个间隔,发射剩余的秒数
emitSeconds(millisUntilFinished / 1000)
}

override fun onFinish() {
// 在倒计时结束时,发射0
emitSeconds(0)
}
}

// 创建一个Flow,用于发射倒数的秒数
fun emitSeconds(seconds: Long): Flow = flow {
// 使用downTo关键字创建一个倒数的范围
for (i in seconds downTo 0) {
// 发射当前的秒数
emit(i.toInt())
}
}


11.kotlin内联函数的协程和 Flow 实现


fun FragmentActivity.timerFlow(
time: Int = 60,
onStart: (suspend () -> Unit)? = null,
onEach: (suspend (Int) -> Unit)? =
null,
onCompletion: (suspend () -> Unit)? =
null
): Job {
return (time downTo 0)
.asFlow()
.cancellable()
.flowOn(Dispatchers.Default)
.onStart { onStart?.invoke() }
.onEach {
onEach?.invoke(it)
delay(
1000L)
}.onCompletion { onCompletion?.invoke() }
.launchIn(lifecycleScope)
}


//在activity中使用
val job = timerFlow(
time = 60,
onStart = { Log.d("Timer", "Starting timer...") },
onEach = { Log.d("Timer", "Seconds remaining: $it") },
onCompletion = { Log.d("Timer", "Timer completed.") }
)

//取消计时
job.cancel()

作者:淘淘养乐多
来源:juejin.cn/post/7270173192789737487
收起阅读 »

趣解设计模式之《小王看病记》

〇、小故事 小王最近参与了公司一个大项目,工作很忙,经常加班熬夜,满负荷工作了2个月后,项目终于如期上线,并且客户反馈也特别的好。老板很开心,知道大家为这个项目付出了很多,所以给全组同事都放了1个星期的假。 小王在项目期间也经常因为饮食不规范而导致胃疼,最近也...
继续阅读 »

〇、小故事


小王最近参与了公司一个大项目,工作很忙,经常加班熬夜,满负荷工作了2个月后,项目终于如期上线,并且客户反馈也特别的好。老板很开心,知道大家为这个项目付出了很多,所以给全组同事都放了1个星期的假。


小王在项目期间也经常因为饮食不规范而导致胃疼,最近也越来越严重了。所以他就想趁着这个假期时间去医院检查一下身体


他来到医院的挂号处,首先缴费挂号,挂了一个检查胃部的诊室。



小王按照挂号信息,来到了诊室,医生简单的询问了一下他的病情,然后给他开了几个需要检查的单子



小王带着医生开具的检查单,就在医院的收费处排队等待着缴费



缴费完毕后,小王就按照医生开的检查项目进行了身体检查……



那么从上面小王的一系列看病流程我们可以发现,这是一系列的处理过程,跟链条一样,即:



挂号——>开检查单——>缴费——>检查——>……



那么对于类似这种的业务逻辑,我们就可以使用一种设计模式来处理,即今天要介绍的——责任链模式


一、模式定义


责任链模式Chain of Responsibility Pattern



使多个对象都有机会处理请求,从而避免了请求的发送者和接受者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有对象处理它为止。



二、模式类图


下面我们再举一个例子,一家公司收到了好多的电子邮件,其中大致分为四类:



CEO处理】公司粉丝发来的邮件。

法律部门处理诽谤公司产品的邮件。

业务部门处理】要求业务合作的邮件。

直接丢弃】其他垃圾邮件。



这里需要CEO先查阅邮件处理,然后再由法务部处理,随后是业务部处理,最后是垃圾邮件执行废弃。根据以上的描述,我们首先需要邮件实体类Email,和用于区分不同处理方式的邮件类型EmailType。对于所有处理者,我们首先创建一个抽象的处理器类AbstractProcessor,再创建四个处理器的实现类,分别是CEO处理器CeoProcessor法务部门处理器LawProcessor业务部门处理器BusinessProcessor垃圾邮件处理器GarbageProcessor。具体类关系如下图所示:



三、模式实现


创建邮件实体类Email.java


@Data
@NoArgsConstructor
@AllArgsConstructor
public class Email {
// 邮件类型
private int type;

// 邮件内容
private String content;
}

创建邮件类型枚举类EmailType.java


public enum EmailType {
FANS_EMAIL(1, "粉丝邮件"),
SLANDER_EMAIL(2, "诽谤邮件"),
COOPERATE_EMAIL(3, "业务合作邮件"),
GARBAGE_EMAIL(99, "垃圾邮件");

public int type;

public String remark;

EmailType(int type, String remark) {
this.type = type;
this.remark = remark;
}
}

创建抽象处理类AbstractProcessor.java


public abstract class AbstractProcessor {

// 责任链中下一个处理节点
private AbstractProcessor nextProcessor;

// 返回的处理结果
private String result;

public final String handleMessage(List emails) {
List filterEmails =
emails.stream().filter(email -> email.getType() == this.emailType()).collect(Collectors.toList());
result = this.execute(filterEmails);
if (this.nextProcessor == null) {
return result;
}
return this.nextProcessor.handleMessage(emails);
}

// 设置责任链的下一个处理器
public void setNextProcessor(AbstractProcessor processor) {
this.nextProcessor = processor;
}

// 获得当前Processor可以处理的邮件类型
protected abstract int emailType();

// 具体处理方法
protected abstract String execute(List emails);
}

创建CEO处理类CeoProcessor.java


public class CeoProcessor extends AbstractProcessor {
@Override
protected int emailType() {
return EmailType.FANS_EMAIL.type; // 处理粉丝来的邮件
}

@Override
protected String execute(List emails) {
if (CollectionUtils.isNotEmpty(emails)) {
System.out.println("-------CEO开始处理邮件-------");
emails.stream().forEach(email ->
System.out.println(email.getContent()));
}
return "任务执行完毕!";
}
}

创建法律部门处理类LawProcessor.java


public class LawProcessor extends AbstractProcessor {

@Override
protected int emailType() {
return EmailType.SLANDER_EMAIL.type; // 处理诽谤类型的邮件
}

@Override
protected String execute(List emails) {
if (CollectionUtils.isNotEmpty(emails)) {
System.out.println("-------法律部门开始处理邮件-------");
emails.stream().forEach(email ->
System.out.println(email.getContent()));
}
return "任务执行完毕!";
}
}

创建业务部门处理类BusinessProcessor.java


public class BusinessProcessor extends AbstractProcessor {

@Override
protected int emailType() {
return EmailType.COOPERATE_EMAIL.type; // 处理合作类型的邮件
}

@Override
protected String execute(List emails) {
if (CollectionUtils.isNotEmpty(emails)) {
System.out.println("-------业务部门开始处理邮件-------");
emails.stream().forEach(email ->
System.out.println(email.getContent()));
}
return "任务执行完毕!";
}
}

创建垃圾邮件处理类GarbageProcessor.java


public class GarbageProcessor extends AbstractProcessor {

@Override
protected int emailType() {
return EmailType.GARBAGE_EMAIL.type; // 处理垃圾类型邮件
}

@Override
protected String execute(List emails) {
if (CollectionUtils.isNotEmpty(emails)) {
System.out.println("-------垃圾开始处理邮件-------");
emails.stream().forEach(email ->
System.out.println(email.getContent()));
}
return "任务执行完毕!";
}
}

创建责任链模式测试类ChainTest.java


public class ChainTest {
// 初始化待处理邮件
private static List emails = Lists.newArrayList(
new Email(EmailType.FANS_EMAIL.type, "我是粉丝A"),
new Email(EmailType.COOPERATE_EMAIL.type, "我要找你们合作"),
new Email(EmailType.GARBAGE_EMAIL.type, "我是垃圾邮件"),
new Email(EmailType.FANS_EMAIL.type, "我是粉丝B"));

public static void main(String[] args) {
// 初始化处理类
AbstractProcessor ceoProcessor = new CeoProcessor();
AbstractProcessor lawProcessor = new LawProcessor();
AbstractProcessor businessProcessor = new BusinessProcessor();
AbstractProcessor garbageProcessor = new GarbageProcessor();

// 设置责任链条
ceoProcessor.setNextProcessor(lawProcessor);
lawProcessor.setNextProcessor(businessProcessor);
businessProcessor.setNextProcessor(garbageProcessor);

// 开始处理邮件
ceoProcessor.handleMessage(emails);
}
}

执行后的结果


-------CEO开始处理邮件-------
我是粉丝A
我是粉丝B
-------业务部门开始处理邮件-------
我要找你们合作
-------垃圾开始处理邮件-------
我是垃圾邮件

Process finished with exit code 0

作者:爪哇缪斯
来源:juejin.cn/post/7277801611996676157
收起阅读 »

微博图床挂了!

一直担心的事情还是发生了。 作为hexo多年的使用者,微博图床一直是我的默认选项,hexo+typora+iPic更是我这几年写文章的黄金组合。而图床中,新浪图床一直都是我的默认选项,速度快、稳定同时支持大图片批量上传更是让其成为了众多图床工具的默认选项。虽然...
继续阅读 »

一直担心的事情还是发生了。


作为hexo多年的使用者,微博图床一直是我的默认选项,hexo+typora+iPic更是我这几年写文章的黄金组合。而图床中,新浪图床一直都是我的默认选项,速度快、稳定同时支持大图片批量上传更是让其成为了众多图床工具的默认选项。虽然今年早些的时候,部分如「ws1、ws2……」的域名就已经无法使用了,但通过某些手段还是可以让其存活的,而最近,所有调用的微博图床图片都无法加载并提示“403 Forbidden”了。




💡Tips:图片中出现的Tengine是淘宝在Nginx的基础上修改后开源的一款Web服务器,基本上,Tengine可以被看作一个更好的Nginx,或者是Nginx的超集,详情可参考👉淘宝Web服务器Tengine正式开源 - The Tengine Web Server



刚得知这个消息的时候,我的第一想法其实是非常生气的,毕竟自己这几年上千张图片都是用的微博图床,如今还没备份就被403了,可仔细一想,说到底还是把东西交在别人手里的下场,微博又不是慈善企业,也要控制成本,一直睁一只眼闭一只眼让大家免费用就算了,出了问题还是不太好怪到微博上来的。


那么有什么比较好的办法解决这个问题呢?


查遍了网上一堆复制/粘贴出来的文章,不是开启反向代理就是更改请求头,真正愿意从根本上解决问题的没几个。


如果不想将自己沉淀的博客、文章托管在印象笔记、notion、语雀这些在线平台的话,想要彻底解决这个问题最好的方式是:自建图床!


为了更好的解决问题,我们先弄明白,403是什么,以及我们存在微博上的图片究竟是如何被403的。


403


百度百科,对于403错误的解释很简单



403错误是一种在网站访问过程中,常见的错误提示,表示资源不可用。服务器理解客户的请求,但拒绝处理它,通常由于服务器上文件或目录的权限设置导致的WEB访问错误。



所以说到底是因为访问者无权访问服务器端所提供的资源。而微博图床出现403的原因主要在于微博开启了防盗链。


防盗链的原理很简单,站点在得知有请求时,会先判断请求头中的信息,如果请求头中有Referer信息,然后根据自己的规则来判断Referer头信息是否符合要求,Referer 信息是请求该图片的来源地址。


如果盗用网站是 https 的 协议,而图片链接是 http 的话,则从 https 向 http 发起的请求会因为安全性的规定,而不带 referer,从而实现防盗链的绕过。官方输出图片的时候,判断了来源(Referer),就是从哪个网站访问这个图片,如果是你的网站去加载这个图片,那么 Referer 就是你的网站地址;你的网址肯定没在官方的白名单内,(当然作为可操作性极强的浏览器来说 referer 是完全可以伪造一个官方的 URL 这样也也就也可以饶过限制🚫)所以就看不到图片了。



解决问题


解释完原理之后我们发现,其实只要想办法在自己的个人站点中设置好referer就可以解决这个问题,但说到底也只是治标不治本,真正解决这个问题就是想办法将图片迁移到自己的个人图床上。


现在的图床工具很多,iPic、uPic、PicGo等一堆工具既免费又开源,问题在于选择什么云存储服务作为自己的图床以及如何替换自己这上千张图片。



  1. 选择什么云存储服务

  2. 如何替换上千张图片


什么是OSS以及如何选择


「OSS」的英文全称是Object Storage Service,翻译成中文就是「对象存储服务」,官方一点解释就是对象存储是一种使用HTTP API存储和检索非结构化数据和元数据对象的工具。


白话文解释就是将系统所要用的文件上传到云硬盘上,该云硬盘提供了文件下载、上传等一列服务,这样的服务以及技术可以统称为OSS,业内提供OSS服务的厂商很多,知名常用且成规模的有阿里云、腾讯云、百度云、七牛云、又拍云等。


对于我们这些个人用户来说,这些云厂商提供的服务都是足够使用的,我们所要关心的便是成本💰。


笔者使用的是七牛云,它提供了10G的免费存储,基本已经够用了。


有人会考虑将GitHub/Gitee作为图床,并且这样的文章在中文互联网里广泛流传,因为很多人的个人站点都是托管在GitHub Pages上的,但是个人建议是不要这么做。


首先GitHub在国内的访问就很受限,很多场景都需要科学上网才能获得完整的浏览体验。再加上GitHub官方也不推荐将Git仓库存储大文件,GitHub建议仓库保持较小,理想情况下小于 1 GB,强烈建议小于 5 GB。


如何替换上千张图片


替换文章中的图片链接和“把大象放进冰箱里”步骤是差不多的



  1. 下载所有的微博图床的图片

  2. 上传所有的图片到自己的图床(xx云)

  3. 对文本文件执行replaceAll操作


考虑到我们需要迁移的文件数量较多,手动操作肯定是不太可行的,因此我们可以采用代码的方式写一个脚本完成上述操作。考虑到自己已经是一个成熟的Java工程师了,这个功能就干脆用Java写了。


为了减少代码量,精简代码结构,我这里引入了几个第三方库,当然不引入也行,如果不引入有一些繁琐而又简单的业务逻辑需要自己实现,有点浪费时间了。


整个脚本逻辑非常简单,流程如下:



获取博客文件夹下的Markdown文件


这里我们直接使用hutool这个三方库,它内置了很多非常实用的工具类,获取所有markdown文件也变得非常容易


/**
* 筛选出所有的markdown文件
*/

public static List<File> listAllMDFile() {
List<File> files = FileUtil.loopFiles(VAULT_PATH);
return files.stream()
.filter(Objects::nonNull)
.filter(File::isFile)
.filter(file -> StringUtils.endsWith(file.getName(), ".md"))
.collect(Collectors.toList());
}

获取文件中的所有包含微博图床的域名


通过Hutools内置的FileReader我们可以直接读取markdown文件的内容,因此我们只需要解析出文章里包含微博图床的链接即可。我们可以借助正则表达式快速获取一段文本内容里的所有url,然后做一下filter即可。


/**
* 获取一段文本内容里的所有url
*
* @param content 文本内容
* @return 所有的url
*/

public static List<String> getAllUrlsFromContent(String content) {
List<String> urls = new ArrayList<>();
Pattern pattern = Pattern.compile(
"\\b(((ht|f)tp(s?)\\:\\/\\/|~\\/|\\/)|www.)" + "(\\w+:\\w+@)?(([-\\w]+\\.)+(com|org|net|gov"
+ "|mil|biz|info|mobi|name|aero|jobs|museum" + "|travel|[a-z]{2}))(:[\\d]{1,5})?"
+ "(((\\/([-\\w~!$+|.,=]|%[a-f\\d]{2})+)+|\\/)+|\\?|#)?" + "((\\?([-\\w~!$+|.,*:]|%[a-f\\d{2}])+=?"
+ "([-\\w~!$+|.,*:=]|%[a-f\\d]{2})*)" + "(&(?:[-\\w~!$+|.,*:]|%[a-f\\d{2}])+=?"
+ "([-\\w~!$+|.,*:=]|%[a-f\\d]{2})*)*)*" + "(#([-\\w~!$+|.,*:=]|%[a-f\\d]{2})*)?\\b");
Matcher matcher = pattern.matcher(content);
while (matcher.find()) {
urls.add(matcher.group());
}
return urls;
}

下载图片


用Java下载文件的代码在互联网上属实是重复率最高的一批检索内容了,这里就直接贴出代码了。


public static void download(String urlString, String fileName) throws IOException {
File file = new File(fileName);
if (file.exists()) {
return;
}
URL url = null;
OutputStream os = null;
InputStream is = null;
try {
url = new URL(urlString);
URLConnection con = url.openConnection();
// 输入流
is = con.getInputStream();
// 1K的数据缓冲
byte[] bs = new byte[1024];
// 读取到的数据长度
int len;
// 输出的文件流
os = Files.newOutputStream(Paths.get(fileName));
// 开始读取
while ((len = is.read(bs)) != -1) {
os.write(bs, 0, len);
}
} finally {
if (os != null) {
os.close();
}
if (is != null) {
is.close();
}
}
}

上传图片


下载完图片后我们便要着手将下载下来的图片上传至我们自己的云存储服务了,这里直接给出七牛云上传图片的文档链接了,文档里写的非常详细,我就不赘述了👇


Java SDK_SDK 下载_对象存储 - 七牛开发者中心


全局处理


通过阅读代码的细节,我们可以发现,我们的方法粒度是单文件的,但事实上,我们可以先将所有的文件遍历一遍,统一进行图片的下载、上传与替换,这样可以节约点时间。


统一替换的逻辑也很简单,我们申明一个全局Map,


private static final Map<String, String> URL_MAP = Maps.newHashMap();

其中,key是旧的新浪图床的链接,value是新的自定义图床的链接。


我们将listAllMDFile这一步中所获取到的所有文件里的所有链接保存于此,下载时只需遍历这个Map的key即可获取到需要下载的图片链接。然后将上传后得到的新链接作为value存在到该Map中即可。


全文替换链接并更新文件


有了上述这些处理步骤,接下来一步就变的异常简单,只需要遍历每个文件,将匹配到全局Map中key的链接替换成Map中的value即可。


/**
* 替换所有的图片链接
*/

private static String replaceUrl(String content, Map<String, String> urlMap) {
for (Map.Entry<String, String> entry : urlMap.entrySet()) {
String oldUrl = entry.getKey();
String newUrl = entry.getValue();
if (StringUtils.isBlank(newUrl)) {
continue;
}
content = RegExUtils.replaceAll(content, oldUrl, newUrl);
}
return content;
}

我们借助commons-lang实现字符串匹配替换,借助Hutools实现文件的读取和写入。


files.forEach(file -> {
try {
FileReader fileReader = new FileReader(file.getPath());
String content = fileReader.readString();
String replaceContent = replaceUrl(content, URL_MAP);
FileWriter writer = new FileWriter(file.getPath());
writer.write(replaceContent);
} catch (Throwable e) {
log.error("write file error, errorMsg:{}", e.getMessage());
}
});

为了安全起见,最好把文件放在新的目录中,不要直接替换掉原来的文件,否则程序出现意外就麻烦了。


接下来我们只需要运行程序,静待备份结果跑完即可。


以上就是本文的全部内容了,希望对你有所帮助


作者:插猹的闰土
来源:juejin.cn/post/7189651446306963514
收起阅读 »

🐞 如何成为一名合格的“中级开发”

嗨,大家好!这里是道长王jj~ 🎩🧙‍♂️ 在这个系列里面的上一篇文章中,我跟大家分享了怎么做一个专业的开发者,还有工作中要注意什么事情。 这是我们人生很重要的一步,因为只有学会怎么开始,才能慢慢变优秀,才能一步步往上进步。 如果你是第一次看这个系列,我强烈建...
继续阅读 »

嗨,大家好!这里是道长王jj~ 🎩🧙‍♂️


在这个系列里面的上一篇文章中,我跟大家分享了怎么做一个专业的开发者,还有工作中要注意什么事情。


这是我们人生很重要的一步,因为只有学会怎么开始,才能慢慢变优秀,才能一步步往上进步。


如果你是第一次看这个系列,我强烈建议你回去看看我之前写的两篇文章,说不定能对你有帮助。


其实我想写这篇文章已经很久了,可是一直想不出来怎么写,找了很多资料也没用。


确实憋不出来,中间还水了一篇“JavaScript冷饭”文章。天可是天天炒冷饭不好吃啊,写那些水文总会心生愧疚,感觉对不起你们哈哈。


今天,我们继续聊一聊,当我们进入这个角色一两年后,该怎么摆脱“初级”头衔,迈入“中级”阶段呢?😎



注意事项:


我接下来提及的内容可能很多大佬跟我的意见是不同的。


也有可能我的知识有限,我只涵盖了前端开发工程师的部分,对其他岗位的开发工程师不了解,可能我说的指标并不一定能和贵公司考核时所授予给的职称相对应。


我这里说的是衡量开发人员技能、知识和整体能力的一般指标


它会根据所在的领域而变化,比如前端、后端、数据等等都不太一样。


虽然具体的工具、技术甚至架构知识可能有所不同,但是我说的一般原则应该是可以广泛适用的。


如果觉得我说错了,请在评论区交流。😊



🎖️ 中级开发的显著特点:“骄傲”


当你到了中级水平,你心里一定有一个想法。那就是:


我已经学会了我现在做的事情,以及要用的所有东西了!


再说得清楚一点就是:


“我已经完全会用JavaScript了,我对HTML很熟悉了,我对数据库没问题!”


“我已经完全会用Vue了,我也会用Angular开发”


这个时候的“中级开发”,觉得他已经有了这个领域需要的能力了。



我肯定每个人到了中级阶段后肯定会有这种感觉。


可能你觉得我要说的是开玩笑,但是大部分的“中级开发”肯定都经历过这个事情。



当然啦,我想表达的“骄傲”不是贬义词。


因为这个阶段只是我们成长中必须经历的一个阶段。这真的不是一件坏事。


“骄傲”不是一件坏事


我们小时候我们都会觉得,爸爸妈妈什么都不知道,我们才更明白


类似的,当你真正进入进入“中级开发“这个角色,你大概率的就会产生这类“骄傲的情绪”。


当你拥有“骄傲”,你才开始真正走自己的路。这个时候你才真正开始独立思考。


这意味着你已经积累了足够的知识和经验,可以继续精进设计模式、最佳实践等这些学科以拔高你的知识。


简单的东西已经不能吸引你了。


🚩 中级开发应该掌握什么?


现在你是中级开发了,你需要看看自己是不是能做到下面这些事情。


这些“新”的东西可以让中级开发更有经验,也更能帮助团队。


编程能力:



  1. 很清楚不同的系统(API、模块、包等)怎么互相连接

  2. 熟练使用编程工具(IDE、GIT等)

  3. 知道怎么实现一般的需求

  4. 遇到bug的时候,知道从哪里找原因和解决办法

  5. 知道怎么优化代码和重构代码

  6. 知道怎么提高性能

  7. 知道怎么用面向对象的程序设计

  8. 知道常用的软件架构模式(MVC、MVVM、MVP、MVI等)

  9. 知道编程语言的一些特点(函数式编程)

  10. 知道怎么部署系统应用

  11. 知道怎么用数据库索引

  12. 知道怎么用数据库表迁移

  13. 知道怎么用数据库分片技术


社会能力:



  1. 可以偶尔跟产品经理(客户)沟通

  2. 是团队的主力


开始优雅:



  1. 代码模块开始按照设计模式来写

  2. 对烂代码有敏感度和重构能力


等等


📌 对中级开发的一些建议


也许现在在读文章的你已经是一位中级开发的存在了,我现在有一些建议想要分享给你!


找一个自己感兴趣的开发者社区加入


为什么我们常说“好的团队创造个人”呢


因为当你真的参与到了重要或高价值的项目时,你真的比一个人漫无目的地学习更快地获得经验。


而且当你真正在团队中贡献力量地时候,你地团队,你的组长,你的领导都会知道,把事情交给你,你就能把自己做好。


在这个过程中,你能积累经验并在你的团队中声名鹊起(这不是名气,而是知名度),那么当新的机会出现时,你就能很快地把握住。


跳出舒适区


跟我上一篇提到的给初级开发的建议类似,你一定要经常的跳出自己的舒适区,不然你不会有毅力坚持学习。


而且,特别是在互联网行业,学习能力是个硬性指标,如果无法坚持下去,很容易就会被淘汰。


这样做可以开阔你的眼界,让你的知识面更广。最终,你会逐渐掌握开发的技巧,面对这些全新的知识领域时,能更快、更准确地找到重点并掌握它们。


但是只要你坚持下去,未来的你一定会与其他人拉开差距。


找到你的导师


这一点在上一篇我也强调过了。你的开发生涯,不能只靠你自己摸索。


你需要有人给你提供想法并能够从中学习。特别是在“中级开发”阶段。


导师可以帮助你不会在某些技术问题或者人生问题上钻牛角尖,他可以拉你一把,避免你浪费很多时间。


这个人可以是你团队中的某个人。


也可以是网络上开发者社区中认识的某位博主。


找到你信任的人(或者更可能是一群人),你可以跟他们问问题和说想法!


找到可以指导你的导师,让你能够突破当前的认知。你的未来将逐步变得清晰起来。


持续学习


这个没什么好说的,在这内卷的社会中,如果没有润的资本和能力,不如在持续学习中等待破局的机会!




🎉 你觉得怎么样?这篇文章可以给你带来帮助吗?当你处于这个阶段时,你发现什么对你帮助最大?如果你有任何疑问或者想进一步讨论相关话题,请随时发表评论分享您的想法,让其他人从中受益。🚀✨


作者:道长王jj
来源:juejin.cn/post/7243203041872412731
收起阅读 »

实现滚动点赞墙

web
需要实现的效果如下: 需要将用户点赞的信息,一条一条的展示在页面顶部,这样的效果有多种实现方式,下面一一来了解一下吧~ 纯css实现 scss如下:(如果要将scss改为less,将$改为@就可以了) 当移动到第8行结束的时候,同屏出现的两行(第9行和第1...
继续阅读 »

需要实现的效果如下:



需要将用户点赞的信息,一条一条的展示在页面顶部,这样的效果有多种实现方式,下面一一来了解一下吧~


纯css实现



scss如下:(如果要将scss改为less,将$改为@就可以了)


当移动到第8行结束的时候,同屏出现的两行(第9行和第10行),就需要结束循环,重头开始了


这是一个上移的动画,动画执行的时间就是8s


itemShowTime+(itemShowTime + (oneCycleItemNum - oneScreenItemNum)(oneScreenItemNum) * (itemShowTime / $oneScreenItemNum)


$itemHeight: 60px; // 单个item的高度

$itemShowTime: 2s; // 单个item从完整出现到消失的时长
$oneCycleItemNum: 8; // 单个循环上移的item条数
$oneScreenItemNum: 2; // 同屏出现的item条数(不能大于 oneCycleItemNum)

$oneCycleItemTime: calc($itemShowTime + ($oneCycleItemNum - $oneScreenItemNum) * ($itemShowTime / $oneScreenItemNum));

@keyframes dynamics-rolling {
from {
transform: translateY(0);
}
to {
transform: translateY(-$itemHeight * $oneCycleItemNum);
}
}
.container {
height: 600px;

animation: dynamics-rolling $oneCycleItemTime linear infinite;
.div {
line-height: 60px;
}
}

.visibleView {
width: 100%;
height: 120px;
overflow: hidden;
background-color: skyblue;

}
.box {
width: 100%;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}

简单的demo:



import React, { useEffect, useRef, useState } from 'react'
import styles from './index.module.scss'

const dataSource = new Array(50).fill(0).map((_, index) => index + 1)

export default function CycleScrollList() {
const [data, setData] = useState(dataSource.slice(0, 10))

return (
<div className={styles.box}>
<div className={styles.visibleView}>
<div className={styles.container}>
{
data.map((item, index) => (
<div key={ index } className={styles.div}>{ item }</div>
))
}
</div>
</div>
</div>

)
}

setInterval监听


css动画是定时的,所以可以定时更新列表内容,但是会有很明显的抖动,效果不太友好,应该是定时器的时间还不能太准




import React, { useEffect, useRef, useState } from 'react'
import styles from './index.module.scss'

const dataSource = new Array(50).fill(0).map((_, index) => index + 1)


export default function CycleScrollList() {
const [data, setData] = useState(dataSource.slice(0, 10))
const nextIndex = useRef(10) // 持续从 dataSource 拿数据的下一个 index

useEffect(() => {
const timer = setInterval(() => {
replaceData()
},4900)

return () => {
clearInterval(timer)
}
}, [])


const replaceData = () => {
let newData = []
if (nextIndex.current-5 < 0) {
newData = [...dataSource.slice(nextIndex.current-5) ,...dataSource.slice(0, nextIndex.current + 5)]
} else {
newData = [...dataSource.slice(nextIndex.current-5, nextIndex.current + 5)]
}
// 使用当前的后半份数据,再从 dataSource 中拿新数据
console.log(newData)
const nextIndexTemp = nextIndex.current + 5
const diff = nextIndexTemp - dataSource.length
if (diff < 0) {
nextIndex.current = nextIndexTemp
} else {
// 一轮数据用完,从头继续
nextIndex.current = diff
}
setData(newData)
}

return (
<div className={styles.box}>
<div className={styles.visibleView}>
<div className={styles.container}>
{
data.map((item, index) => (
<div key={ index } className={styles.div}>{ item }</div>
))
}
</div>
</div>
</div>

)
}

IntersectionObserver监听


监听第5个元素


如果第五个元素可见了,意味着不可见时,需要更换数据了


如果第五个元素不可见了,立刻替换数据


替换的数据如下:



使用IntersectionObserver监听元素,注意页面卸载时,需要去除绑定


tsx如下:



import React, { useEffect, useRef, useState } from 'react'
import styles from './index.module.scss'

const dataSource = new Array(50).fill(0).map((_, index) => index + 1)
const ITEM_5_ID = 'item-5'

export default function CycleScrollList() {
const [data, setData] = useState(dataSource.slice(0, 10))

const intersectionObserverRef = useRef<IntersectionObserver | null>()
const item5Ref = useRef<HTMLDivElement | null>(null)

const nextIndex = useRef(10) // 持续从 dataSource 拿数据的下一个 index
const justVisible5 = useRef<boolean>(false) // 原来是否为可视

useEffect(() => {
intersectionObserverRef.current = new IntersectionObserver((entries) => {
entries.forEach((item) => {
if (item.target.id === ITEM_5_ID) {
// 与视图相交(开始出现)
if (item.isIntersecting) {
justVisible5.current = true
}
// 从可视变为不可视
else if (justVisible5.current) {
replaceData()
justVisible5.current = false
}
}
})
})
startObserver()

return () => {
intersectionObserverRef.current?.disconnect()
intersectionObserverRef.current = null
}
}, [])

const startObserver = () => {
if (item5Ref.current) {
// 对第五个 item 进行监测
intersectionObserverRef.current?.observe(item5Ref.current)
}
}

const replaceData = () => {
let newData = []
if (nextIndex.current-5 < 0) {
newData = [...dataSource.slice(nextIndex.current-5) ,...dataSource.slice(0, nextIndex.current + 5)]
} else {
newData = [...dataSource.slice(nextIndex.current-5, nextIndex.current + 5)]
}
// 使用当前的后半份数据,再从 dataSource 中拿新数据
console.log(newData)
const nextIndexTemp = nextIndex.current + 5
const diff = nextIndexTemp - dataSource.length
if (diff < 0) {
nextIndex.current = nextIndexTemp
} else {
// 一轮数据用完,从头继续
nextIndex.current = diff
}
setData(newData)
}

return (
<div className={styles.box}>
<div className={styles.visibleView}>
<div className={styles.container}>
{
data.map((item, index) => (
index === 4 ?
<div id={ ITEM_5_ID } ref={ item5Ref } key={ index } className={styles.div}>{ item }</div>
:
<div key={ index } className={styles.div}>{ item }</div>
))
}
</div>
</div>
</div>

)
}

scss样式


$itemHeight: 60px; // 单个item的高度

$itemShowTime: 3s; // 单个item从完整出现到消失的时长
$oneCycleItemNum: 5; // 单个循环上移的item条数
$oneScreenItemNum: 3; // 同屏出现的item条数(不能大于 oneCycleItemNum)

$oneCycleItemTime: calc($itemShowTime + ($oneCycleItemNum - $oneScreenItemNum) * ($itemShowTime / $oneScreenItemNum));

@keyframes dynamics-rolling {
from {
transform: translateY(0);
}
to {
transform: translateY(-$itemHeight * $oneCycleItemNum);
}
}
.container {
height: 600px;

animation: dynamics-rolling $oneCycleItemTime linear infinite;
.div {
line-height: 60px;
}
}

.visibleView {
width: 100%;
height: 120px;
overflow: hidden;
background-color: skyblue;

}
.box {
width: 100%;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}

作者:0522Skylar
来源:juejin.cn/post/7278244755825442853
收起阅读 »

用了策略模式之后,再也不用写那么多 if else 了,真香!

web
前言 从我个人理解来看,设计模式其实就藏在我们平时的代码中,只是有人把它们提、炼出来,赋予了一些专业的名词和定义,下面给大家介绍一个日常项目开发中非常实用的设计模式,也就是策略模式。 策略模式的定义 先来看下策略模式的定义:定义一系列的算法,把它们一个个封装起...
继续阅读 »

前言


从我个人理解来看,设计模式其实就藏在我们平时的代码中,只是有人把它们提、炼出来,赋予了一些专业的名词和定义,下面给大家介绍一个日常项目开发中非常实用的设计模式,也就是策略模式


策略模式的定义


先来看下策略模式的定义:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。


简单来说就是有多种选择,然后一般只会选择一种。从代码的角度来说就是,定义一系列的ifelseif,然后只会命中其中一个。


举个例子


话不多说,直接来看例子,比如我们需要计算员工工资,员工工资计算规则如下:



  • 高级工:时薪为25块/小时

  • 中级工:时薪为20块/小时

  • 初级工:时薪为15块/小时


按每天10小时的工作时长来算。


一、第一版实现:


const calculateSalary = function (workerLevel, workHours = 10) {
if (workerLevel === 'high') {
return workHours * 25
}
if (workerLevel === 'middle') {
return workHours * 20
}
if (workerLevel === 'low') {
return workHours * 15
}
}
console.log(calculateSalary('high')) // 250
console.log(calculateSalary('middle')) // 200

这段代码具有明显的缺点:



  • calculateSalary函数庞大,有许多的if else语句,这些语言需要覆盖所有的逻辑分支

  • calculateSalary函数缺乏弹性,如果新增一种员工等级higher,需要修改calculateSalary函数的内部实现,违反开放——封闭原则

  • 算法的复用性差


二、第二版实现(函数组合):


当然,我们可以使用函数组合的方式重构代码,把每一个if中的逻辑单独抽离成一个函数。


const workerLevelHigh = function (workHours) {
return workHours * 25
}

const workerLevelMiddle = function (workHours) {
return workHours * 20
}
const workerLevelLow = function (workHours) {
return workHours * 15
}

const calculateSalary = function (workerLevel, workHours = 10) {
if (workerLevel === 'high') {
return workerLevelHigh(workHours)
}
if (workerLevel === 'middle') {
return workerLevelMiddle(workHours)
}
if (workerLevel === 'low') {
return workerLevelLow(workHours)
}
}
console.log(calculateSalary('high', 10)) // 250
console.log(calculateSalary('middle', 10)) // 200

这样会提高算法的复用性,但这种改善十分有限,calculateSalary函数依旧庞大和缺乏弹性。


三、第三版实现(策略模式):


我们可以把不变的部分和变化的部分拆分开来。



  • 不变的部分:算法的使用方式不变,都是根据某个算法取得计算后的工资数额;

  • 变化的部分:算法的实现。


我们js的对象是key value的形式,这可以帮助我们天然的替换掉if else


因此,我们可以定义对象的两部分:



  • 针对变化的部分,我们可以定义一个策略对象,它封装了具体的算法,负责具体的计算过程

  • 针对不变的部分,我们提供一个Context函数,它接受客户的请求,随后把请求委托给策略对象。


const strategies = {
"high": function (workHours) {
return workHours * 25
},
"middle": function (workHours) {
return workHours * 20
},
"low": function (workHours) {
return workHours * 15
},
}

const calculateSalary = function (workerLevel, workHours) {
return strategies[workerLevel](workHours)
}
console.log(calculateSalary('high', 10)) // 250
console.log(calculateSalary('middle', 10)) // 200

策略模式的优缺点


从我个人在实际项目中的使用来看,策略模式的优缺点如下:


优点:



  • 代码复杂度降低:再也不用写那么多if else了。eslint其中有一项规则配置叫圈复杂度,其中一条分支也就是一个if会让圈复杂增加1,圈复杂度高的代码不易阅读和维护,用策略模式就能很好的解决这个问题;

  • 易于切换、理解和扩展:它将算法封装在独立的strategy中,比如你要在上面代码中加一个等级higherlower,直接更改策略对象strategies就行,十分方便。

  • 复用性高:策略模式中的算法可以复用在系统的其它地方,你只需要用将策略类strategies用export或者module.exports导出,就能在其他地方很方便的复用。


缺点:



  • 增加使用者使用负担:因为大量运用策略模式会在实际项目中堆砌很多策略类或者策略对象,这样项目的新人如果不熟悉这些策略类和策略对象,会增加他们的使用成本和学习成本,前期来说会比看if else更加难懂。


小结


以上就是我个人对策略模式的解读和了解啦,实际上项目中用策略模式的场景还是挺多的,因为在写业务代码中,很容易写出大量的if else,这时候就可以封装为策略模式,方便项目维护和扩展,从我个人的使用体验来看,还是相当香的。


大家喜欢在实际项目使用策略模式么,欢迎留言和讨论~


作者:han_
来源:juejin.cn/post/7279041076273610764
收起阅读 »

27岁程序媛未来的出路到底在哪里?

不太聪明的脑子的思考原因 最近回老家面试了一个工作,发现老家的思想的底层逻辑是:到了这个年纪女性就应该相夫教子,不愿意给女性与男性同等的工资标准或对女性进行培养, 看到他们这种嘴脸真的不想回去,但是目前互联网环境也不好,对未来开始变得迷茫不安, 不过作为i型...
继续阅读 »

不太聪明的脑子的思考原因


最近回老家面试了一个工作,发现老家的思想的底层逻辑是:到了这个年纪女性就应该相夫教子,不愿意给女性与男性同等的工资标准或对女性进行培养,

看到他们这种嘴脸真的不想回去,但是目前互联网环境也不好,对未来开始变得迷茫不安,

不过作为i型人格真的很喜欢这种沉浸式工作,暂时没有换行业的打算,所以还是先从目前做程序出发,去提升自己的能力,争取能再多干个几年,然后回东北老家花几万块买个小房子,开始我的摆烂养老人生
(人生终极目标)


在此总结一下今年上半年的成果和下半年的目标吧~


上半年成果


1.刷力扣拿到排名


摆烂人生是在去年感知到危机的时候结束的,于是开始疯狂刷LeetCode,学习算法,最终的结果是对待代码问题脑子变得灵光了但生活中越发糊涂了,但是目前困难的题还是基本摸不到头绪的状态,好多数学公式也不知道,位运算符也不咋会用,就目前感觉自己还是很差,提升的空间还是非常非常高的

(今年四月拿到排名时截的图)


微信图片_20230823180111.jpg

2.开始准备软考


年初的时候开始考虑考一个专业资格证,于是开始做一些功课,上半年从bilibili上看了一些公开的课先做了初步了解,六月份买了一套课开始进行系统的学习,备战11月的考试


微信图片_20230823180721.png
微信图片_20230823180710.png

3.涨薪


很幸运自己能在目前经济环境下行的情况下没有失业,并且领导对我还算认可,给我们在竞争中留下来的人涨了工资,但说是涨薪,其实最终结果我们未必拿到的多了,因为目前公司效益不景气,如果公司效益持续低迷,年底的14薪必定要打水漂,但是还能稳定的存活下来也算是比较满意了,真心希望公司越来越好,因为我们的老板人真的非常不错(虽然我不接受pua但是发自内心感谢公司)


4.买了自行车开始骑行健身


其实早就想买个自行车,可以骑行上班,周末也可以当运动,不过身边的好多人都不赞同,因为像夏天太热、冬天太冷、刮风下雨都骑不出去,但是最终我还是买了,嘎嘎开心,不过确实影响因素很多最终也没骑过几次哈哈(主要是本人太懒总是找借口不骑车出门)


微信图片_20230823181638.jpg

下半年目标


1.软考通过!


最近还是按照规划的持续学习,每个月给自己定一个总体的目标,然后分到每一天里去,现在距离考试还有两个多月,还是要加油的!


2.争取换一个更高的平台


感觉目前的公司体量还是太小了,做了很多微信小程序,工作对自己的提升已经到达了极限,但是就目前的情况来说,还是对年底的14薪抱有一丝丝幻想,所以这个目标可能在今年年底或者明年去达成


3.持续精进算法


还是在有条不紊的刷LeetCode,给自己的最低要求是每周至少一道中级,保持一个持续学习的状态


4.做一个开源项目


这个规划应该会在11月份开始实施,或者如果突然来了灵感可以立马启动,也是给以后面试提供一个优势条件吧


最后希望还在圈子中的同行们也能越来越好,不管这些努力会不会给自己带来实质性的收益,本质上都是在提升自己,目的其实很简单,就是不被这日新月异的时代所淘汰,
就好像一句金句里描述的那样:我们所做的一切,不是为了改变世界,而是不让世界改变我们!



最后,大家有什么能提升自己的点子也可以给我留言,让我们一起努力吧,加油!


2648ff5ff16d83fcde8e1f6117c4f472.jpeg


作者:毛毛裤
来源:juejin.cn/post/7270403438201356346
收起阅读 »

回乡偶书

离别家乡岁月多,近来人事半消磨。 惟有门前镜湖水,春风不改旧时波。 1200多年前,耄耋诗人告老,从北方回他萧绍一带的家乡。多年后,而立之年的晚辈从萧山出发,一夜辗转赶回北方老家,需要在窄窄几天假期里,拜堂成亲洞房花烛。随后便要返程,收敛和压抑个性,作为被标准...
继续阅读 »

离别家乡岁月多,近来人事半消磨。
惟有门前镜湖水,春风不改旧时波。


1200多年前,耄耋诗人告老,从北方回他萧绍一带的家乡。多年后,而立之年的晚辈从萧山出发,一夜辗转赶回北方老家,需要在窄窄几天假期里,拜堂成亲洞房花烛。随后便要返程,收敛和压抑个性,作为被标准化了的打工人,回归互联网行业最知名几家工厂流水线生产工的身份。


7年前茕茕一人离开时,心意诀决,并无此刻留恋情愫。我想离开熟悉而平平无奇的这里,想在发达的大城市闯荡立足。几年艰苦卓绝,起起伏伏壮阔波澜,如愿在都市有一席之地。当7年后匆匆返乡,成婚祭祖,再要和新妻携手南下时,更多却是不舍。


这是新的开始,但何尝不是结束?告别从小见我长大的故交亲友,告别家乡,人生前几十年的社交圈若即若离渐行渐远。而要去遥远的南方,认识新的朋友,开辟新的章节。


见到了眼熟却不能明确是何亲戚,更不知名姓住址的宾客,当年的中年人苍苍白发垂垂老矣,带着相见不相识的儿童——我更清楚与此同时,当年的许多老人,限于距离限于身体,甚至是生死阴阳,并不能见到。


”而这些复活的情愫仅仅只能引发怀旧的兴致,根本不能重新再去领受。恰如一只红冠如血尾翎如帜的公鸡发现了曾经哺育自己的那只蛋壳,却再也无法重新蜷卧其中体验那蛋壳里头的全部美妙了“(「白鹿原」白孝文回乡)。它要觅食要筑窝,要跳上垛头,引吭高歌。


“近来人事半消磨”,读过多次不能领会。当亲眼看到原本肥胖的人失去意识躺卧病榻,瘦削枯槁。更有比我还小的同村邻里,远房亲戚,二十二三,刚刚毕业,因意外或疾病戛然而止。


“惟有门前镜湖水,春风不改旧时波”。这是一声无可奈何的哀叹?还是看通之后的豁达?也或许兼而有之。作者被称有唐一代,福禄寿考典范。当身经目睹这些,也认同高龄高寿,没有痛苦或较短暂病痛后,在满堂子孙目送中离开,对当事人实在算不得坏事。


image.png


这是被繁忙和xx偷走的几年,大疫过后,这世界和之前已经很不一样了。


作者:fliter
来源:juejin.cn/post/7275592320010272805
收起阅读 »

git merge 和 git rebase的区别

git rebase 让你的提交记录更加清晰可读 git rebase 的使用 rebase 翻译为变基,它的作用和 merge 很相似,用于把一个分支的修改合并到另外一个分支上。 如下图所示,下图介绍了经过 rebase 前后提交历史的变化情况。 现在我们...
继续阅读 »

git rebase 让你的提交记录更加清晰可读


git rebase 的使用


rebase 翻译为变基,它的作用和 merge 很相似,用于把一个分支的修改合并到另外一个分支上。


如下图所示,下图介绍了经过 rebase 前后提交历史的变化情况。



现在我们来用一个例子来解释一下上面的过程。


假设我们现在有2条分支,一个为 master\color{#2196F3}{master} ,一个为 feature/1\color{#2196F3}{feature/1},他们都基于初始的一个提交 add readme\color{#2196F3}{add \ readme} 进行检出分支,之后,master 分支增加了 3.js\color{red}{3.js},和 4.js\color{red}{4.js} 的文件,分别进行了2次提交,feature/1\color{#2196F3}{feature/1} 也增加了 1.js\color{red}{1.js}2.js\color{red}{2.js} 的文件,分别对应以下2条提交记录。


master\color{#2196F3}{master} 分支如下图:



feature/1\color{#2196F3}{feature/1} 分支如下图:



结合起来看是这样的:



此时,切换到 feature/1 分支下,执行 git rebase master ,成功之后,通过 log 查看记录。


如下图所示:可以看到先是逐个应用了 master 分支的更改,然后以 master\color{#2196F3}{master} 分支最后的提交作为基点,再逐个应用 feature/1\color{#2196F3}{feature/1} 的每个更改。



所以,我们的提交记录就会非常清晰,没有分叉,上面演示的是比较顺利的情况,但是大部分情况下,rebase 的过程中会产生冲突的,此时,就需要手动解决冲突,然后使用 git addgit rebase --continue 的方式来处理冲突,完成 rebase,如果不想要某次 rebase 的结果,那么需要使用 git rebase --skip 来跳过这次 rebase


git merge 和 git rebase 的区别


不同于 git rebase的是,git merge 在不是 fast-forward(快速合并)的情况下,会产生一条额外的合并记录,类似 Merge branch 'xxx' into 'xxx' 的一条提交信息。



另外,在解决冲突的时候,用 merge 只需要解决一次冲突即可,简单粗暴,而用 rebase 的时候 ,需要一次又一次的解决冲突。


git rebase 交互模式


在开发中,常会遇到在一个分支上产生了很多的无效的提交,这种情况下使用 rebase 的交互式模式可以把已经发生的多次提交压缩成一次提交,得到了一个干净的提交历史,例如某个分支的提交历史情况如下:



进入交互式模式的方法是执行:


git rebase -i <base-commit>

参数 base-commit 就是指明操作的基点提交对象,基于这个基点进行 rebase 的操作,对于上述提交历史的例子,我们要把最后的一个提交对象 (ac18084\color{#F19E38}{ac18084}) 之前的提交压缩成一次提交,我们需要执行的命令格式是


git rebase -i ac18084

此时会进入一个 vim 的交互式页面,编辑器列出的信息像下列这样。



想要合并这一堆更改,我们要使用 squash 策略进行合并,即把当前的 commit 和它的上一个 commit 内容进行合并, 大概可以表示为下面这样。


pick  ... ...
s ... ...
s ... ...
s ... ...

修改文件后 按下 : 然后 wq 保存退出,此时又会弹出一个编辑页面,这个页面是用来编辑提交的信息,修改为 feat: 更正,最后保存一下,接着使用 git branch 查看提交的 commit 信息,rebase 后的提交记录如下图所示,是不是清爽了很多? rebase 操作可以让我们的提交历史变得更加清晰。




特别注意,只能在自己使用的 feature 分支上进行 rebase 操作,不允许在集成分支上进行 rebase,因为这种操作会修改集成分支的历史记录。



rebase 的风险



patch:【假设本地分支为 dev1,c1 和 c2 是本地往 dev1 分支上做的两次提交】把 dev1 分支上的c1和 c2 “拆”下来,并临时保存成 c1' 和 c2'。git 里将其称为 patch



rebase\color{red}{rebase} 会将当前分支的新提交拆下来,保存成 patch\color{red}{patch},然后合并进其他分支新的 commit\color{red}{commit},最后将 patch\color{red}{patch} 接进当前分支。这是 rebase\color{red}{rebase} 对多条分支的操作。对于单条分支,rebase\color{red}{rebase} 还能够合并多个 commit\color{red}{commit} 单号,将多个提交合并成一个提交。


git rebase -i [commit id]命令能够合并(整改) commit id 之前的所有 commit\color{red}{commit} 单。加上-i选项能够提供一个交互界面,分阶段修改commit信息并 rebase\color{red}{rebase}


但这里就会出现一个问题:如果你合并多个单号时,一不小心合并多了,将别人的提交也合并了,此时你本地的 commit history\color{red}{commit \ history} 和远程仓库的 commit history\color{red}{commit \ history} 不一样了,无论你如何 push\color{red}{push},都无法推送你的代码了。如果你并不记得 rebase\color{red}{rebase} 之前的 HEAD\color{red}{HEAD} 指向的 commit\color{red}{commit}commit ID\color{red}{commit \ ID} 的话,git reflog\color{red}{git \ reflog} 都救不了你。


tips:  你可以 push\color{red}{push} 时带上 f\color{red}{-f} 参数,强制覆盖远程 commit history\color{red}{commit \ history},你这样做估计会被打,因为覆盖之后,团队的其他人的本地 commit history\color{red}{commit \ history} 就与远程的不一样了,都无法推送了。


因此,请保证仅仅对自己私有的提交单进行 rebase\color{red}{rebase} 操作,对于已经合并进远程仓库的历史提交单,不要使用 rebase\color{red}{rebase} 操作合并 commit\color{red}{commit} 单。


作者:d_motivation
来源:juejin.cn/post/7277089907974357052
收起阅读 »

谈谈2年前非科班3年前端经验的我是如何进外企的

先放结论,主要靠下面几点 有进外企的意识,并提前准备 运气 一定的技术水平和从业背景 下面进入正题。 有进外企的意识,并提前准备 去之前的几年,我就在网上刷到过不少外企的分享,类似 朝9晚5,不加班 请假随时请,不需要理由 15 天年假 看病不花钱、有补...
继续阅读 »

先放结论,主要靠下面几点



  • 有进外企的意识,并提前准备

  • 运气

  • 一定的技术水平和从业背景


下面进入正题。


有进外企的意识,并提前准备


去之前的几年,我就在网上刷到过不少外企的分享,类似



  • 朝9晚5,不加班

  • 请假随时请,不需要理由

  • 15 天年假

  • 看病不花钱、有补充商业医疗险

  • 零食、水果,,,,


以及下面的缺点



  • 不容易升高管,天花板低

  • 薪资不太高


考虑到国内的情况,把外企作为 40 岁甚至之后的一个选择,似乎是个不错的方案。


于是,我做了一下粗略的分析,当我准备投外企的时候,我大概需要具备下面的能力



  • 英文读写流利

  • 口语流利最好,最起码要能听懂,说的清楚

  • 技术水平和工作经验要大致匹配


显然其中最大的问题是英文听说读写,于是我把英语融入开发中,刻意的用英文查、答问题,看英文文档,写英文博客,来提升英文水平,所以最终在读写方面提升不少,听说上面,似乎还行。虽然全英面的时候,听力上吃了不少亏。


所以,如果你有这个想法,但是实力还不够,要提前准备,现在英语不行,不代表1-2年后不行。


运气


当时我司的上海分公司还在大规模招人,我运气好,赶上了,换做 2023 年,连 HC 都没了。


但是不是今年的你因此就放弃这个选择了呢?如果你仍然考虑把外企作为一个选项的话,那我的建议是,长期关注,有机会就试试


在一段时间内,外企缩招是很正常的,但如果你周期拉的够长,一边上班,一边关注机会,总会看到机会的。不过,拿到正式 offer 前,要稳住


如果已经离职了,根据自己评估来,外企面试流程2个礼拜都是很正常的事情,提前规划,如果急着找工作的话,这个时候就真的是看运气了。


如果你能把自己安排在面试官差不多准备下决定的时候,轮到你面试,那也是可以增加录取的概率的,就是不太容易。


一定的技术和从业背景


面试总归还是要看从业背景和技术的,如果有外国面试官,而你的公司也在国外有名,那自然是加分项。


如果发现外企的技术栈和国内不太重合,也不要怕,有时候反而是机会。


如果看到技术栈不太符,而你又愿意进这家公司,这个时候你的竞争对手就少了很多。如果你再愿意去多了解一下,优势就更多了。


外企哪里找


我当时是照着 955.WLB 的公司名单,挨个去搜索、尝试的。


上面名单中的外企,我当时就面过 leetcode 和 我司,主要外企招 3 年经验前端的也不是很多,反倒是 5年经验 + Java 系全栈方向更受外企欢迎,机会多不少


上面名单中,不加班的公司还是有一些的。但是如果剔除公积金、社保避税的话(主要是国内公司),选择不多。


作者:xianshenglu
来源:juejin.cn/post/7271900840246689829
收起阅读 »

八百年不面试,一面试就面得一塌糊涂

web
前言 好久没面试了,最近看一些厂开始招人了,于是投了投,没想着过,主要是抱着学习的态度,看看自己哪里不足,没想到自己这么不足。。。 最近面了个试,某大厂,具体是哪个厂大家可以自行猜测,我看看有没有对的,哈哈哈哈 目前是三面,但是估计止步于三面了,然后我稍微整理...
继续阅读 »

前言


好久没面试了,最近看一些厂开始招人了,于是投了投,没想着过,主要是抱着学习的态度,看看自己哪里不足,没想到自己这么不足。。。


最近面了个试,某大厂,具体是哪个厂大家可以自行猜测,我看看有没有对的,哈哈哈哈


目前是三面,但是估计止步于三面了,然后我稍微整理了一下面试题,但是这里只说出我的思考,而不说出答案,至于为啥不写答案,我只能说,我自己不会。。。有些内容我需要进行学习,然后系统地、简单易懂地分享给大家


我说一下我自身的情况:主要技术栈是vue全家桶,算是能深挖的那种,其他的,react、webpack、vite、less、sass、tailwindcss、unocss、Nuxt、node等系列都会,但是说实话我是没法手写出来的,只停留在会用的程度,webgl、canvas等可视化方向还可以,毕竟我之前就是做这个的,算法还算可以,不说精通,但是一般题是可以做出来的,然后在基础方面,也就是js、css这块,我只能说了解吧,因为见过css大神coco这种的,就感觉自己css从熟悉变成了听说。


反正大概的就那样吧,会的比较多,比较杂,但是很多都不精通(不去看源码这种),关键的来了,我对于浏览器这块比较薄弱,计网、操作系统对我来说像噩梦一样,我觉得是我经验少吧,没能在工作中接触到这几个层面,所以我就是真的能答出来,也就是硬背的,并且不能举一反三,这也导致了这次面试的惨败



下面我说一下面试吧


注意,公司的技术栈主要是React(umi)这块,vue很少很少,然后会用node写一些中间件,大部分都是大前端



然后算法问题的话,也不在这里说,主要说一些口述的问题


一面


一面是对我来说最友好的一面了,基本上都是简单的一些基础问题


面试官主要是react技术栈,然后我和他说了,我主要是vue的,vue的原理可以,但是问我react的太深的问题我是不太会的,首先是自我介绍,然后开始问问题



  • pinia和vuex的区别,其实他想问我Redux和Mobx和其他React状态管理的区别,但是奈何我就会这几个,所以他索性问了问了我pinia

  • css实现DOM节点的水平居中有几种方式:我记得我说了四种,flex,text-align,margin,position,应该还有,但是一瞬间的话,脑袋瓦特了

  • 实现一个左右布局,左侧200px,右侧自适应,css写有几种方式:我说了浮动、定位、弹性盒、网格这四种

  • 检测js数据类型,typeof和instanceof区别,instanceof原理:这里我直接手写了instanceof,这个很简单

  • 浏览器输入url,到看到页面会发生什么:我当时懵了,我看过n个面经都说过这个问题,经典八股,但是我就是没背,只能磕磕巴巴说了一些(我八股真的不行,而且我不背这玩意)

  • 用Java的时候,对登录请求进行拦截,怎么处理的:这个很简单哈,为啥问Java,这是因为我简历上有,我之前从事过全栈,然后他就问了一下

  • 函数式编程的副作用是什么

  • 工作的经历,项目问题(这个占据了大部分的时间),其中有个问题可以分享一下,因为我用了wangeditor,他问我wangeditor的内核是什么


一面总体来说是很友好的,而且都答出来了,面试官很礼貌,面试感受非常好,第二天下午的时候通知二面


二面


噩梦的开始



  • 自我介绍

  • 公司项目问题(绝大部分时间)

  • vue、react数据绑定的区别

  • 我想存储一个客户端的数据,前端有哪些存储方式:后来就存储、内存的问题开始展开

  • pinia会进行数据的存储,它最终存在了哪里

  • js的内存是怎么进行管理的

  • 垃圾回收、内存泄漏,什么情况会导致内存泄漏

  • 闭包是什么,应用场景,怎么操作会产生内存泄漏

  • 你在工作时用的哪种协议

  • 除了http还有哪些通信协议(跟前端有关的)

  • websocket通信过程是怎么样的

  • 前端跨域相关问题

  • 代理相关问题

  • 服务和服务之间有没有跨域

  • 前端安全方面有哪些攻击方式

  • 该怎么处理呢

  • node有哪些框架可以处理脚本攻击(或者是库)


有些问题记不清了,后面有一些网络的问题,但是忘了,前面其实还好,而且问题是一步一步衍生出来的,这感觉很好,但是到网络安全这里,我就有点不会了,当时就感觉完犊子了,再见


然后过了三天,hr电话告诉我过了,约了三面,其实是比较吃惊的,我以为已经止步了


三面


最难受的一面



  • 自我介绍

  • 说说最近自己认为最好的项目,然后我说了一些,然后对方:就这?我一时语塞,开始紧张(项目占据了大多数时间)

  • 说说tcp三次挥手,为什么不能两次

  • tcp粘包,讲讲

  • 还有一些计网和操作系统的问题,这里是因为,我根本不会,所以压根没记住问题。。

  • 进程、线程区别,举个生动的例子

  • 讲讲多线程

  • 浏览器的核心线程和核心进程有哪些

  • MySQL的引擎

  • 现在有一个100tb的文件,让你一分钟之内把这个文件遍历出来,怎么做


计网和操作系统一塌糊涂,现在面试还没有反馈,凉凉了,而且看面试官的态度也能看出来是很不满意的


总结


平均时长在45min左右


几乎没问vue的任何问题,这是我最难受的,而且js、ts、css也几乎不问的,反正就是我上面的技术栈几乎一个没问,面试官主要就问你两处:你的工作经验(也就是你曾经的公司项目),以及计网和操作系统


因为我有做一些开源的项目和个人的项目,但是他们更在乎你之前公司的项目是什么样的


我自己的项目比较多,简历就有5.6页,但是没啥用,他们都没问


我也发现自己计网、操作系统这里太薄弱了,有时间还是得系统学习一下的,自己的确在开发中没遇到过这些,欠加思考


希望大家也能重视一下这里吧


作者:Shaka
来源:juejin.cn/post/7273682292538933306
收起阅读 »

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

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

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


悲惨的上线时刻


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


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


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


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


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


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


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


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


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


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


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


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


如何预热缓存


灰度放量


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


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


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


扫描数据库刷缓存


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


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


通过数据平台刷缓存


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


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


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


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


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


需要预热缓存的其他场景


如果Redis挂了,数据怎么办


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


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


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


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


总结



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

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

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


作者:他是程序员
来源:juejin.cn/post/7277461864349777972
收起阅读 »

王兴入局大模型!美团耗资21亿拿下光年之外100%股权

【新智元导读】 正式官宣!美团收购光年之外全部权益,斥资20.65亿。**** 官宣了!美团以20.65亿人民币收购光年之外。 就在刚刚,美团在港交所公告,已订立交易协议收购光年之外的全部权益。 总代价包括现金233,673,600美元;债务承担人民币366,...
继续阅读 »
【新智元导读】 正式官宣!美团收购光年之外全部权益,斥资20.65亿。****

官宣了!美团以20.65亿人民币收购光年之外。


就在刚刚,美团在港交所公告,已订立交易协议收购光年之外的全部权益。


总代价包括现金233,673,600美元;债务承担人民币366,924,000元;现金人民币1.00元。




于公告日期,光年之外的净现金总额约为285,035,563美元。转让协议交割完成后,美团将持有光年之外100%权益。


前几天,光年之外联合创始人王慧文因健康问题暂时离岗引发许多人的关注。


甚至,外界关心诸多的是他的停职对公司造成哪些影响。




美团在公告中对于并购的解释是,通过收购事项获得领先的AGI技术及人才,有机会加强其于快速增长的人工智能行业中的竞争力。


这次,美团出手,意味着光年之外在后续运营有了足够资金支持。


同时,对美团来说,大模型能对未来业务转型也将产生有利帮助。


美团拿下光年之外



其实,在外界看来,美团收购光年之外,就像是板凳钉钉的事。


从感性层面讲,王兴与王慧文是清华的室友,在创业路上并肩作战。王慧文入局大模型后,王兴紧接着应声跟进。


在大模型爆火后,美团CEO王兴也对此表示极大的关注,甚至,在3月份还投资光年之外。


当时,王兴表示「AI大模型让我既兴奋于即将创造出来的巨大生产力,又忧虑它未来对整个世界的冲击。老王和在创业路上同行近二十年,既然他决心拥抱这次大浪潮,那我必须支持。」




从理性层面讲,自2019年美团将战略升级为「零售+科技」后,不论是王兴本人,还是公司来讲,对AI也投入非常大的兴趣。


当前,大模型已经成为兵家必争之地,国内许多头部科技纷纷入局。


据「豹变」独家报道,美团做大模型,已经有2个多月,几乎是与王兴投资光年之外同步进行的。


据称,算法团队正积极扩招,甚至还在筹划成立单独的「平台部门」,帮助美团大模型通过具体的商业化形式落地。


对美团来讲,智能配送系统、外卖无人车等场景,都需要AI驱动。


收购光年之外后,美团能够将大模型的能力,与自家核心业务相结合,比如外卖、本地生活服务等等。




此外,还能够在客服、物流、产品体验等各种场景中实现应用,将大模型能力与场景深度融合。


美团方面表示,并购完成后,将支持光年团队继续在大模型领域进行探索和研究。


所以说,美团的未来还是值得期待的。


而前几日,王慧文病倒的消息,让外界猜测纷纷,比如融资不顺利,或团队组建困难。


有国内媒体澄清道,光年之外A轮融资已经完成一个月,融资到账实际金额远高于外部报道的2.3亿美元,网传的“融资不顺利”消息,属于谣言。


此次美团在港交所的公告,也证实了这一点。


而在人才组队上,光年之外也进展顺利。


在成立后不久,光年之外就以换股形式收购了一流科技,原核心技术团队被保留。


而在两个月内,光年之外的研发团队规模就已经在70人左右,团队在算法等领域,研发经验丰富。


这样一支已经组建成熟的团队,在当下的大模型之战中,无疑属于稀缺资源。


美团选择收购光年之外,显然也是经过深思熟虑。


VC平稳退出



光年之外在6月初刚刚完成了的这笔2.3亿美元的融资,由源码资本领投。


腾讯、五源资本和快手创始人宿华也参与了这次融资。


从港交所披露的信息来看,除了6月初的这轮融资,红杉中国也在前期对光年之外进行了投资。


当王慧文因病离开光年之外的领导岗位之后,这些前期投资的VC都因为这突发的黑天鹅事件,可能面临着投资打水漂的风险。


但是随着美团的出手收购,这些参与光年之外的投资至少能在一定程度上落袋为安。


不用再担心因为被投公司核心创始人离职给被投公司带来的巨大影响。


王慧文辞任董事



此前,大模型创业4个月,王慧文就因身体原因停职休养。


紧接着,美团在港交所公布,王慧文已提出辞去公司非执行董事、公司董事会提名委员会成员和公司授权代表职务,自6月26日起生效。




在王慧文卸任董事后,美团宣布,执行董事穆荣均已获委任为授权代表,自2023年6月26日起生效。


另外,提名委员会将由冷雪松先生和沈向洋博士组成,冷雪松继续担任提名委员会主席。


在另一份公告中,美团更新了董事会成员。王兴和穆荣均担任公司执行董事,沈南鹏为非执行董事,欧高敦、冷雪松、沈向洋是独立非执行董事。



参考资料:


www1.hkexnews.hk/listedco/li…


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

月入5000+?Midjourney制作小红书壁纸实现副业变现

上一篇文章介绍了Midjourney实现副业变现的6种方式,也介绍了小红书壁纸的变现逻辑:第一种是开通店铺,吸引用户进行购买;第二种是引导用户在某个小程序下载图片,用户每下载一张,我们便会有0.3元左右的收益。这一篇开始介绍如何使用Midjourney制作小红...
继续阅读 »

上一篇文章介绍了Midjourney实现副业变现的6种方式,也介绍了小红书壁纸的变现逻辑:第一种是开通店铺,吸引用户进行购买;第二种是引导用户在某个小程序下载图片,用户每下载一张,我们便会有0.3元左右的收益。这一篇开始介绍如何使用Midjourney制作小红书壁纸。


一、制作步骤


使用Midjourney制作小红书壁纸的步骤比较简单,分为5步,其中最关键的步骤就是画出用户喜欢的壁纸


1、绘画壁纸


在开始绘画之前,我们首先要确定好壁纸类型,小红书的壁纸类型有多种,包括剪纸类型、花草类型、梵高类型(使用梵高的风格画各种夜景)、二次元类型等,确定壁纸类型有利于我们吸引垂类粉丝。


以二次元风格壁纸为例,在Midjourney中,输入/image指令,再输入提示词:cute cat swimming underwater,smiling,bright eyes,portrait,dream,a bright color --ar 9:16 --q 2 --niji 5 --v 5.1 --s 750,就可以得到一张好看的壁纸。 



2、添加边框


添加手机边框有两种方式:一种需要熟悉PS软件,使用PSD文件添加;另一种是通过醒图、美图秀秀等APP添加(搜索【手机边框】,找到合适的边框,进行添加)。这一步骤不是必须的,但添加之后,会有仪式感。 



3、发布作品


接下来就可以在小红书发布作品,标题填写【4K高清壁纸】,有利于用户搜索。如果会写文案,也可以进行文案的添加。需要说明的是,一开始不建议开通店铺,等用户数到达一定数量再开通(不会开通小红书店铺的读者,可以留言)。


4、上传图片


如果通过小程序进行变现,需要将生成的壁纸上传到小程序,以XX壁纸小程序为例,抖音搜索或者微信搜索都可以,之后注册,注册完成设置自己的口令,口令尽量以数字为主,这样用户下载起来比较方便。


5、进行引流


如果开通了商城,发布作品时,将商城添加到作品中即可;如果是小程序,需要进行引流,比如,进入粉丝群可以得到高清图片、三连击得到原图壁纸等。


二、发布逻辑


上述5个步骤实现了小红书壁纸的制作以及发布,如何源源不断的画出用户喜欢的壁纸,也是我们需要考虑的问题。


1、模仿


模仿点赞比较高的作品,火过的作品有可能再火一次


2、创新


创新需要具备一定的审美能力,不过没有关系,多画画就有了。


3、追随实事


这和那些口播博主一样,追随社会热点事件,只不过我们追随的是节日氛围,比如在端午节发布和其相关的作品。


4、多发


做过自媒体的都知道,作品是否能火,多少有一点玄学,也就是概率事件。那对抗概率事件最好的武器就是数量和时间,也就是多发布作品,坚持一两个月。


以上四点是做自媒体的通用逻辑,就不再赘述。


三、案例


在小红书中,通过Midjourney制作壁纸变现超过5000元的大有人在,以下便是几个案例:


1、两个月,变现15000元。




2、两个月,变现28000元。




3、一个多月,变现8000元(包含80件商品)。




列举这几个案例,并不是说我们按照上述步骤就一定能达到这样的收入(赚钱本身就不是容易的事),而是想要表达两个意思:第一,在找对标账号的时候,我们就应该找这种变现能力强的账号,跟着他们学习,收获会很大。


第二,任何技术越早开始越好,AI已经是势不可挡,因为它的本质是生产力的提高,而且国家也在鼓励人工智能的发展,作为技术人员,我们有先发优势,当机会来临的时候,我们一定要抓住。


就像在掘金社区,有人是为了分享技术,但有人也是为了吸引粉丝,进行副业变现。当某个行业红利不在的时候,最好的方法是寻找下一个行业红利。


我是阿凯,一位不知名的Midjourney玩家,如果你有任何关于Midjourney的问题,可以进行留言。关注我,下一篇手把手教你使用Midjourney制作抖音壁纸,进行副业变现。


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

浅谈一下滴滴实习

在租房躺尸好几天了,自从周一从滴滴离职就一直待在租房打游戏,瞬间没有工作的负担是真的彻底让我释放了心中的欲望,实际上游戏纯属发泄欲望和转移注意力的工具,我是一个喜欢瞎想的人(这可能就是我胖不起来的主要原因),放纵完实在是太无聊了,想写点什么。 说实话我这文章写...
继续阅读 »

在租房躺尸好几天了,自从周一从滴滴离职就一直待在租房打游戏,瞬间没有工作的负担是真的彻底让我释放了心中的欲望,实际上游戏纯属发泄欲望和转移注意力的工具,我是一个喜欢瞎想的人(这可能就是我胖不起来的主要原因),放纵完实在是太无聊了,想写点什么。
说实话我这文章写得毫无章法,完全是想到哪里写到哪里,也不想去分门别类了,我觉得真实的想法最重要,如果有语义错误就略过吧哈哈。


说说业务


对这段实习做一个小小的总结,先说一说业务吧,我所在的是滴滴商旅的一个技术部门,主要负责企业版这块的业务,我去的第一天上午看团队规范文档,下午跑项目看代码,第二天接需求,当然是比较简单的需求,后面陆陆续续做了滴滴企业版的小部分 pc 端官网和大部分移动端官网,如果你现在用手机搜滴滴企业版,那么你看到的页面大概率就是我做的,除此以外还有一个经典后台管理项目,其实项目用的技术栈都还好,没有说很有难度的,对于业务来说我觉得最难的应该就是项目的搭建和部署,然后就是技术方案,开发代码确实是最基础的事情了,这几个月完成的代码量并不大,这也完全在意料之中,实习生嘛,能确保自学到东西就行,当自学到一定的程度会很迷茫,不知道下一个进阶的领域是什么,但是在这段时间我逐渐感觉前端的一个瓶颈就在前端工程化,其实早就在学了,但是没有实际的项目经验加上网上教程比较匮乏,大多是讲解 webpack 的基本使用甚至一度让大部分人认为 webpack 就是前端工程化,如果有后端基础我觉得理解工程化那就太简单了,只不过可惜的是参与前端开发的大多是后端经验为 0 的同学,因此对于常年在浏览器玩 js 的我们很难理解在编译阶段能做的一些工作的意义所在,不管是现在的 Node 或是 Go 和 Rust,其实都可以作为一个深入挖掘的方向,至少我感觉业务是真的很无聊,偶尔当玩具写写还行,每天写真的没意思。


除了业务以外认知到一些原来不知道的职场"内幕"。


第一点:面试冷知识

走之前组内一直在招社招的员工,当面试官的兄弟和我说了我才知道,原来面试通一个部门甚至是同一个面试官可能真的会因为面试官心情或者其余外在因素决定你面试是否通过,比如最近部门比较忙,那可能面试也就比较水一点,大概考察没问题就直接过了没有那么多的时间去做横向比较(那我面的部门基本都还是比较闲啊哈哈),又或者是面试官看你比较顺眼性格也比较符合他的要求,大概率会给一些比较简单的题,这些都会影响面试官的判断从而决定你是否能通过面试最终拿 offer,所以经过这件事之后看开了很多,如果原来你一直不理解平时技术没你好的同学最后能拿到同公司或者同部门 offer,现在应该慢慢也就看开了,一旦挂了及时投递下一个部门,这不一定是自己的原因。


第二点:大厂其实不全是 996

不要被危言耸听,这其实大概率取决于你的部门而非公司,我在的部门经过这两年的形势成功变得小而精,小组的氛围很好,平时开发大家都合作得很开心,不管是导师还是 leader,休息了也会偶尔一起打打游戏,在这个部门我感觉挺好至少没有看到所谓的大厂 996,基本上大家 10 点来,最晚 8 点也都走了。离职的前一天刚好赶上了部门团建于是狠狠地去蹭了一只烤全羊,leader 把商旅的大 leader 请过来了,我对大领导的刻板印象是电视里那种懂得都得,但是没想到和我想的完全相反,大家举杯畅饮吹牛逼,欢声笑语,挺好,,后来想想有可能是因为大家都是技术出身很多时候也都很讨厌那一套,这也是我对互联网最满意的一点,凭本事吃饭,对于出身不是那么地依赖,也不是尔诈我虞,阿谀奉承。


第三点:学会装菜,不要没事找事

作为实习生,懂的都懂,其实在哪里都一样,如果你太着急表现自己,别人就会觉得你过分刺毛,能装菜的地方千万别装逼,艹,我感觉我就是傻逼了,这也许也是我离开的原因之一,作为实习生老老实实完成自己的工作就好,能够保质保量完成任务对于导师来说基本就差不多了,至于一些 pua 话术里面说的额外价值,我觉得对于没有转正的实习生来说毫无意义,反而会自找麻烦,因为并不会因为你原本安排 2 天切完的图你一天切完导师就给你放松自学,很多时候你做的事情是否有意义完全取决于你的导师是否愿意安排有意义的工作给你,所幸我在滴滴完整地参与了项目的技术方案到代码编写直至最后部署上线,里面沉淀了我自己的思考,经过这段实习确实让我受益匪浅。


最后一点是软实力

我觉得这也是我在这段实习中收获到的最重要的东西之一:"学会总结,及时复盘",每次周会给导师和兄弟们讲方案总是要准备很久,会去看很多的自己不知道的东西,以此来让我写的东西显得足够的高大上,记得有一次上线官网出问题了,意料之中做了一个复盘,倒不是说学到了什么代码层面的东西,更多的是让我了解了整个项目从开发到部署上线的流程,这个远远比写代码有意义,不得不说这极大地培养了我的能力,包括新技术的敏感程度,技术的深度以及口才,总结出来的东西一方面加深了自己的记忆和理解,往小了说,让我可以在以后的技术面试中就这段经历侃侃而谈,往大了说,这个让我学会从更高的视角去看问题,不再是盯着代码的一亩三分地,更多的是学会从项目的技术架构层面去看问题,第二是学会表现自己并且及时纠正自己的错误,没错,就是给别人看,自己瞎学总结是没有意义的,你是一个无比努力的人,可是大家不知道那也毫无意义,他能知道的仅仅是你能写上简历的东西,只有向别人更好的展示自己,下一次面试官看到你才会觉得你是一个善于总结和反思的人,程序员这一行也是这样,其实参与一个开源项目远远比你基础扎实更让人刮目相看,尽管你只是为一个看起来无比高大上的开源切了图,对我自己来说我只是把曾经在 wps 或者 typora 的写作 转移到了掘金或者 github,内容并没有太大的变化,这样的事情何乐而不为呢。


最后做一个收尾。


这两天想去北京附近转转,今天跑到了天安门,还是想吐槽地铁站一些人地素质问题,经典的钱包鼓起来了素质教育没跟上来,或者换句话说富起来很多并非接受过良好教育的一批人。


马上快开学了,要回学校拿保研名额,说实话我到现在都不确定哪条路是对了,大厂?还是保研?还是国企公务员?谁知道呢,每个人有每个人的说法,老员工会劝你保研进编制,新员工会劝你尽早进大厂捞钱,每个人追求的往往都是目前最缺失的,也许正是因为未来充满未知所以才无限期待,不然像我这样躺尸一周那该多无聊,脑袋都睡麻木了,这两周陆陆续续也面试了四五家公司,不得不说有大厂背书投简历就是好使,曾经拒绝过我的那些公司都拿到了 offer 然后全给拒了哈哈,不为别的就是解气,基本都是一些 b 格还比较高的独角兽公司,比如教育,云服务器,游戏行业等等,大厂我肯定还是没这个底气的哈哈我依然是大部分大厂的舔狗,不过结果不算坏,下一站是老铁厂了。


不知道是否会有之前一起工作的兄弟看到这篇文章,如果认为我有说得不恰当的地方欢迎指正。


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

不知什么原因,背调没过?

前两天写了一篇文章《电话背调,我给他打了8分》,跟大家聊了职场中沟通的一些基本原则和经验。背调时,同事没给打招呼,几乎也没什么私交,但出于“不坏别人好事”的原则,给了8分的评价。 在稍微大一些的公司中,背调是非常重要的环节。如果拿到了offer,上家公司已经离...
继续阅读 »

前两天写了一篇文章《电话背调,我给他打了8分》,跟大家聊了职场中沟通的一些基本原则和经验。背调时,同事没给打招呼,几乎也没什么私交,但出于“不坏别人好事”的原则,给了8分的评价。


在稍微大一些的公司中,背调是非常重要的环节。如果拿到了offer,上家公司已经离职,新公司还没入职,背调没通过,那就有点悲催了。所以,今天就跟大家聊聊入职背调中的一些注意事项。


第一,背调日趋严格


整体而言,背调是越来越严格了。当然,每家公司都不是为了背调而背调,这是劳民伤财的事,主要是因为履历包装的情况太严重。特别是有一部分刚毕业为了找到工作,通过简历、履历、学历等途径包装成2-3工作经验的情况时有发生。


还有就是,HR也有考核指标,HR在实际招聘的过程中会踩一些坑,为了避免类似的事情发生,会在既有的经验上进行迭代筛查条件。


一般背调有两种方式:体量小一些的公司,HR会给你留电话的人打电话核实;体量大一些的公司会直接委托三方来进行背调核实。


HR直接打电话的背调相对来说会简单一些,而且会有一些个人风格,我们暂且不提。而背调公司的风格一般比较统一。


第二,背调联系人


背调的过程一般会让写三类联系人:直接领导、人力和同事。大概率会背调之前两家公司的履历。


在填写时,你就需要慎重考虑了,基本上会挨个打电话询问你的情况的。所以,你写谁之前,最好先打个招呼,否则说你的坏话,那你就有些悲催了。像上篇文章中同事那样不打招呼的操作,是强烈不建议的。


另外,你写的这些联系人要能够联系得到才行。如果都联系不上,过的可能性就不大了。


第三,背调的过程


曾经多次作为上级领导参与背调,背调的核心点有几项(他,代表被背调的人):


确认身份:确认你是否是本人,是否是他的上级领导。同时,还会确认他的岗位信息,他是否带下属,下属多少人等。除了电话确认之外,甚至还会要求入职人员跟相关人要公司企业管理软件(钉钉、飞书等)中带有企业名称、填写人姓名的截图证明等。


表现评分:在工作表现、沟通表现等方面会有1-10分,询问各项的表现评分是多少。同时,在问题的涉及上还会有一些交叉认证的小策略。会涉及到:工作表现如何,与大家相处的如何,吃苦耐劳能力如何,抗压能力如何、离职原因是什么、你是否满意他的整体表现、是否有违规操作等等。


交叉确认:除了个人表现的评分确认之外,如果同一个公司的背调,还会交叉确认一下你留的其他人员是否也是这家公司的,是否是对应岗位的。


如果你预留的信息都是真实的,那么不用担心什么,跟填写联系人的打好招呼就行了。如果部分内容有出入,那可要交代清楚了。


另外,在工作中,平时与同事和上下级相处时,保持融洽的关系,留一个联系方式等也有一定的必要性。


第四,其他可能性


除了上面统一的背调流程之外,某些公司还会有更加严格的背调信息。这些信息是否违法违规暂且不说,但是是会出现的。如果你不care这份工作,可以拒绝提供的。


常见的有收入证明、工资流水、社保缴纳、征信报告等。


收入证明一般由上家公司出具并盖章,私企或关系比较好一些,可以适当调整。工资流水可以是银行打印的或下载的电子单据。社保缴纳可以提供查询到的流水。征信报告这个对于部分金融相关的行业会有一定要求,会引导你操作申请一份个人征信报告。


另外还有两项,大多数人可能不知道,但对于高端的一些岗位也会涉及到:HR的圈子和劳动诉讼。


HR是有自己的圈子和人脉的,而且可能比你想象的要广。如果你在上家公司,或者在圈子里名声不好,很可能会被问出来的。这个也没其他办法,自己的个人人设和职业素养问题了。


另外一个就是劳动诉讼,这个也是可以调查出来的,除了有专门的机构可以做这些事之外,某些诉讼可以在企业的“法律诉讼”中查到诉讼的另一方的。当然,如果曾经涉及到刑事案件用人单位也是可以查出来的。


最后


市场越来越卷,而打工人越来越不容易。在日常工作中保持良好的人际关系和职业素养,更多的还是为自己铺好后路。在面试找新工作时保持诚信,尽量避免出现撒一个谎,用一百个谎来圆的情况。


最后,无论怎样,都要有备选方案,既不能丢了西瓜捡了芝麻,更不能最后两手空空。


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

我裸辞了,但是没走成!

人在国企,身不由己!公司福利和薪资还可以,但有个难顶的组长就不可以,说走就走!如果把这个组长换了的话就另说了! 1.为什么突然想不干了? 1.奇葩的新组长 我的前组长辞职了,然后被安排到这个自研项目组,而这个新组长我之前得罪过,老天爷真爱开玩笑! 今年过年前,...
继续阅读 »

人在国企,身不由己!公司福利和薪资还可以,但有个难顶的组长就不可以,说走就走!如果把这个组长换了的话就另说了!


1.为什么突然想不干了?


1.奇葩的新组长


我的前组长辞职了,然后被安排到这个自研项目组,而这个新组长我之前得罪过,老天爷真爱开玩笑!


今年过年前,我主开发的平台要嵌入到他负责的项目里面,一切对接都很顺利,然而某天,有bug,我修复了,在群里面发消息让他合并分支更新一下。他可能没看到,然后我下班后一个小时半,我还在公司,在群里问有没有问题,没回应!


然后我就坐车回家,半路,产品经理组长、大组长和前组长一个个轮流call我,让我处理一下bug,我就很无语!然后我就荣获在家远程办公,发现根本没问题!然后发现是对方没更新的问题!后面我修复完直接私聊他merge分支更新,以免又这样大晚上烦人!
而类似的事情接连发生,第三次之后,我忍不住了,直接微信怼了他,他还委屈自己晚上辛苦加班,我就无语大晚有几个人用,晚上更新与第二天早上更新有什么区别?然后就这样彻底闹掰了!


我就觉得这人很奇葩,有什么问题不能直接跟我沟通,一定要找我的上级一个个间接联系我呢?而且,这更新流程就很有问题,我之前在别的组支援修bug,是大早上发布更新,一整天测试,保证不是晚上的时候出现要紧急处理的问题!


然后,我跟这人有矛盾后,我就没继续对接这个项目了,前组长安排了别人代替我!


结果兜兜转转,竟然调到他这里来!作孽啊!


2.项目组乱糟糟


新项目组可以看出新组长管理水平很糟糕!


新组长给自己的定位是什么都管!产品、前后端、测试、业务等,什么都往自己身上揽!他自己觉得很努力,但他不是那部分的专业人员,并不擅长,偏偏还没那个金刚钻揽一堆瓷器活!老爱提建议!


项目组就两个产品,其中一个是UI设计刚转,还没成长为专业的产品经理,而那个主要负责的产品经理根本忙不过来!


然后,他一个人搞不定,就开始了PUA大法,周会的时候就会说:“希望大家要把这个项目当成自己的事业来奋斗,一起想,更好地做这个产品!”


“这个项目集成了那么多的模块功能,如果大家能够做好,对自己有很大的历练和成长!”


“我们项目是团队的重点项目,好多领导都看好,开发不要仅限于开发,要锻炼产品思维……”


……


简而言之就是,除了本职工作也要搞点产品的工作!


然后建模师开始写市场调研文档,前后端开发除了要敲代码,还得疯狂想新功能。


整个组开始陷入搞新东西的奇怪旋涡!


某次需求评审的时候,因为涉及到大量的文件存储,我提出建议,使用minio,fastdfs,这样就不用每次部署的时候,整体文件还要迁移,结果对方一口拒绝,坚决使用本地存储,说什么不要用XX平台的思想来污染他这个项目,他这个项目就要不需要任何中间件都能部署。


就很无语!那个部署包又大又冗余,微服务都不用,必须要整包部署整套系统,只想要某几个功能模块都不行,还坚持说这样可以快速整包部署比较好!


一直搞新功能的问题就是版本更新频繁!一堆新功能都没测清楚就发布,导致产品质量出现严重问题,用户体验极差!终于用户积攒怨气爆发了,在使用群里面@了我们领导,产品质量问题终于被彻底揭开了遮羞布!


领导开始重视这个产品质量的问题,要求立即整改!


然后这个新组长开始新一轮搞事!


“大家保证新功能进度的同时,顺便测试旧功能,尽量不要出bug!”


意思就是你开发进度也要赶,测试也要搞!


就不能来点人间发言吗?


3.工作压力剧增


前组长是前端的,他带我入门3D可视化的,相处还算融洽!然而他辞职了,去当自由职业者了!


新组长是后端的,后端组长问题就是习惯以后端思维评估前端工作,给任务时间很紧。时间紧就算了,还要求多!


因为我之前主开发的项目是可视化平台,对方不太懂,但不妨碍他喜欢异想天开,加个这个,加个那个,他说一句话,就让你自行想象,研究竞品和评估开发时间!没人没资源,空手套白狼,我当时就很想爆他脑袋了!


我花一个星期集成了可视化平台的SDK,连接入文档都写好了,然后他验收的时候提出一堆动态配置的要求,那么大的可视化平台,他根本没考虑项目功能模块关联性和同步异步操作的问题,他只会提出问题,让你想解决方案!


然后上个月让我弄个web版的Excel表格,我看了好多开源项目,也尝试二开,增加几个功能,但效率真的好低!于是我就决定自己开发一个!


我开发了两个星期,他就问我搞定没?我说基本功能可以,复杂功能还在做!


更搞笑的是,我都开发两个星期了,对方突然中午吃饭的时候微信我,怕进度赶不上,建议我还是用开源的进行二开,因为开源有别人帮忙一起搞。


我就很无语,人家搞的功能又不是一定符合你的需求,开源不等于别人给你干活,大家都是各干各的,自己还得花精力查看别人代码,等价于没事找事,给自己增加工作量!别人开发的有隐藏问题,出现bug排查也很难搞,而自己开发的,代码熟悉,即便有问题也能及时处理!


我就说他要是觉得进度赶不上就派个人来帮忙,结果他说要我写方案文档,得到领导许可才能给人。


又要开发赶进度,又要写文档,哪有那么多时间!最终结果就是没资源,没人手,进度依旧要赶!


因为我主开发的那个可视化平台在公司里有点小名气,好多平台想要嵌入,然后,有别的平台找到他要加上这个可视化平台,但问题是我很忙,又要维护又要开发,哪搞得了那么多?还说这个很赶!赶你个头!明知道时间没有,就别答应啊!工作排期啊!


新组长不帮组员解决问题,反而把问题抛给组员,压榨组员就很让人反感!


2.思考逃离


基于以上种种!我觉得这里不是一个长久之地,于是想要逃离这里!


我联系了认识的其他团队的人,别人表示只要领导愿意放人,他们愿意接收我,然后我去咨询一些转团队的流程,那些转团队成功的同事告诉我,转团队最难的是领导放人这关,而且因为今年公司限制招聘,人手短缺,之前有人提出申请,被拒绝了!并且转团队的交接的一两个月内难免要承受一些脸色!有点麻烦!


我思虑再三,我放弃了转团队这条路,因为前组长走了之后,整个团队只剩下我一个搞3D开发的,估计走不掉!


3.提出辞职


忍了两个月,还是没忍住,工作最重要的是开开心心!赚钱是一回事,要是憋出个心理疾病就是大事了!于是我为了自己的身心健康,决定走人!拜拜了喂!老娘不奉陪了!


周一一大早,我就提交了辞职信,大组长表示很震惊,然后下午的时候,领导和大组长一起来跟我谈话,聊聊我为什么离职?问我有没有意愿当个组长之类的,我拒绝了,我只想好好搞技术!当然我不会那么笨去说别人的坏话得罪人!


我拿前组长当挡箭牌,说自己特别不习惯这个新组长的管理方式!前组长帮我扛着沟通之类的问题,我只要专心搞开发就好了!


最终,我意志坚定地挡住了领导和大组长的劝留谈话,并且开始刷面试题,投简历准备寻找新东家!


裸辞后真的很爽,很多同事得知消息都来关心我什么情况,我心里挺感动的!有人说我太冲动了,可以找好下家再走!但我其实想得很清楚,我没可能要求一个组长委屈自己来适应我,他有他的管理方式,我有我的骄傲!我不喜欢麻烦的事,更不喜欢委屈自己,一个月后走人是最快解决的方案!


4. 转机


其实我的离开带来了一点影响,然后加上新组长那个产品质量问题警醒了领导,然后新组长被调去负责别的项目了,换个人来负责现在的项目组,而这个人就是我之前支援过的项目组组长,挺熟悉的!


新新组长管理项目很有条理也很靠谱,之前支援的项目已经处于稳定运行的状态了,于是让他来接手这个项目!他特意找我谈话,劝我留下来,并且承诺以后我专心搞技术,他负责拖住领导催进度等问题!


我本来主要就是因为新组长的问题才走人的,现在换了个不错的组长!可以啊!还能苟苟!


5.反思


  1. 其实整件事情中,我也有错,因为跟对方闹掰了,就拒绝沟通,所以导致很多问题的发生,如果我主动沟通去说明开发难度的问题,并且争取时间,就不至于让自己处于一个精神内耗的不快乐状态。
  2. 发现问题后,我没有尝试去跟大组长反馈,让大组长去治治对方,或者让大组长帮忙处理这个矛盾,我真的太蠢了!
  3. 我性格其实挺暴躁的,看不顺眼就直接怼,讨厌的人就懒得搭理,这样的为人处世挺不讨喜的,得改改这坏脾气!

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

iOS 内存泄漏排查方法及原因分析

iOS
本文将从以下两个层面解决iOS内存泄漏问题:内存泄漏排查方法(工具)内存泄漏原因分析(解决方案) 在正式开始前,我们先区分两个基本概念: 内存泄漏(memory leak):是指申请的内存空间使用完毕之后未回收。 一次内存泄露危害可以忽略,但若一直...
继续阅读 »

本文将从以下两个层面解决iOS内存泄漏问题:

  • 内存泄漏排查方法(工具)
  • 内存泄漏原因分析(解决方案)


在正式开始前,我们先区分两个基本概念:


  • 内存泄漏(memory leak):是指申请的内存空间使用完毕之后未回收。 一次内存泄露危害可以忽略,但若一直泄漏,无论有多少内存,迟早都会被占用光,最终导致程序crash。(因此,开发中我们要尽量避免内存泄漏的出现)
  • 内存溢出(out of memory):是指程序在申请内存时,没有足够的内存空间供其使用。 通俗理解就是内存不够用了,通常在运行大型应用或游戏时,应用或游戏所需要的内存远远超出了你主机内安装的内存所承受大小,就叫内存溢出。最终导致机器重启或者程序crash


简单来说:





一、排查方法



我们知道,iOS开发有“ARC机制”帮忙管理内存,但在实际开发中,如果处理不好堆空间上的内存还是会存在内存泄漏的问题。如果内存泄漏严重,最终会导致程序的崩溃。



首先,我们需要检查我们的App有没有内存泄漏,并且快速定位到内存泄漏的代码。目前比较常用的内存泄漏的排查方法有两种,都在Xcode中可以直接使用:


  • 第一种:静态分析方法(Analyze
  • 第二种:动态分析方法(Instrument工具库里的Leaks)。一般推荐使用第二种。

1.1 静态内存泄漏分析方法:


  • 第一步:通过Xcode打开项目,然后点击Product->Analyze,开始进入静态内存泄漏分析。 如下图所示:

  • 第二步:等待分析结果。

  • 第三步:根据分析的结果对可能造成内存泄漏的代码进行排查,如下图所示。



PS:静态内存泄漏分析能发现大部分问题,但只是静态分析,并且并不准确,只是有可能发生内存泄漏。一些动态内存分配的情形并没有分析。如果需要更精准一些,那就要用到下面要介绍的动态内存泄漏分析方法(Instruments工具中的Leaks方法)进行排查。



1.2 动态内存泄漏分析方法:



静态内存泄漏分析不能把所有的内存泄漏排查出来,因为有的内存泄漏发生在运行时,当用户做某些操作时才发生内存泄漏。这是就要使用动态内存泄漏检测方法了。



步骤如下:


  • 第一步:通过Xcode打开项目,然后点击Product->Profile,如下图所示:

    • 第二步:按上面操作,build成功后跳出Instruments工具,如上图右侧图所示。选择Leaks选项,点击右下角的【choose】按钮。如下图:

    • 第三步:这时候项目程序也在模拟器或手机上运行起来了,在手机或模拟器上对程序进行操作,工具显示效果如下:


点击左上角的红色圆点,这时项目开始启动了,由于Leaks是动态监测,所以手动进行一系列操作,可检查项目中是否存在内存泄漏问题。如图所示,橙色矩形框中所示绿色为正常,如果出现如右侧红色矩形框中显示红色,则表示出现内存泄漏。



选中Leaks Checks,在Details所在栏中选择CallTree,并且在右下角勾选Invert Call TreeHide System Libraries,会发现显示若干行代码,双击即可跳转到出现内存泄漏的地方,修改即可。


举个例子:



PS:AFHTTPSessionManager内存泄漏是一个很常见的问题:解决方法有两种:点击这里





二、内存泄漏的原因分析


目前,在ARC环境下,导致内存泄漏的根本原因是代码中存在循环引用,从而导致一些内存无法释放,最终导致dealloc()方法无法被调用。主要原因大概有一下几种类型:


2.1 ViewController中存在NSTimer


如果你的ViewController中有NSTimer,那么你就要注意了,因为当你调用

[NSTimer scheduledTimerWithTimeInterval:1.0 
target:self
selector:@selector(updateTime:)
userInfo:nil
repeats:YES];

  • 理由:这时 target: self,增加了ViewController的retain count, 即self强引用timertimer强引用self。造成循环引用。
  • 解决方案:在恰当时机调用[timer invalidate]即可。

2.2 ViewController中的代理delegate



代理在一般情况下,需要使用weak修饰。如果你这个VC需要外部传某个delegate进来,通过delegate+protocol的方式传参数给其他对象,那么这个delegate一定不要强引用,尽量使用weak修饰,否则你的VC会持续持有这个delegate,直到代理自身被释放。



  • 理由:如果代理用strong修饰,ViewController(self)会强引用ViewView强引用delegatedelegate内部强引用ViewController(self)。造成内存泄漏。
  • 解决方案:代理尽量使用weak修饰。

举个例子:代理一般用weak修饰,避免循环引用。

@class QiAnimationButton;
@protocol QiAnimationButtonDelegate <NSObject>

@optional
- (void)animationButton:(QiAnimationButton *)button willStartAnimationWithCircleView:(QiCircleAnimationView *)circleView;
- (void)animationButton:(QiAnimationButton *)button didStartAnimationWithCircleView:(QiCircleAnimationView *)circleView;
- (void)animationButton:(QiAnimationButton *)button willStopAnimationWithCircleView:(QiCircleAnimationView *)circleView;
- (void)animationButton:(QiAnimationButton *)button didStopAnimationWithCircleView:(QiCircleAnimationView *)circleView;
- (void)animationButton:(QiAnimationButton *)button didRevisedAnimationWithCircleView:(QiCircleAnimationView *)circleView;

@end


@interface QiAnimationButton : UIButton

@property (nonatomic, weak) id <QiAnimationButtonDelegate> delegate;

- (void)startAnimation; //!< 开始动画
- (void)stopAnimation; //!< 结束动画
- (void)reverseAnimation; //!< 最后的修改动画

2.3 ViewController中Block



在我们日常开发中,如果block使用不当,很容易导致内存泄漏。



  • 理由:如果block被当前ViewController(self)持有,这时,如果block内部再持有ViewController(self),就会造成循环引用。
  • 解决方案:在block外部对弱化self,再在block内部强化已经弱化的weakSelf

For Example:

    __weak typeof(self) weakSelf = self;

[self.operationQueue addOperationWithBlock:^{

__strong typeof(weakSelf) strongSelf = weakSelf;

if (completionHandler) {

KTVHCLogDataStorage(@"serial reader async end, %@", request.URLString);

completionHandler([strongSelf serialReaderWithRequest:request]);
}
}];

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

如何判断设备是否越狱?

iOS
前言 iPhone 越狱已经不是什么新鲜事,但是越狱之后意味着已经拿到了系统的所有权限,继续在越狱的设备上运行你的程序也就意味着不再安全,因此目前很多主流的 App 都是禁止运行在此类设备上的。 但是怎么判断一个设备是否为越狱的机器呢?今天就来讲讲我所知道的一...
继续阅读 »

前言


iPhone 越狱已经不是什么新鲜事,但是越狱之后意味着已经拿到了系统的所有权限,继续在越狱的设备上运行你的程序也就意味着不再安全,因此目前很多主流的 App 都是禁止运行在此类设备上的。


但是怎么判断一个设备是否为越狱的机器呢?今天就来讲讲我所知道的一些方法。


方法一


检查手机上是否安装了 Cydia,玩越狱的同学肯定都清楚,这个 app 堪称是越狱系统的 App Store,上边可以安装各种正规 App Store 安装不到的软件。Cydia 上除了独立的应用程序之外,更多的包是 iOS 本身和应用程序的扩展、修改和主题。


因此可以说只要是越狱的设备,都会安装这个应用,那我们只需要检测这个应用存不存在就行了。


这里主要用到的方法是用 canOpenURL 是否能打开 cydia:// 这个 URL Scheme。

func isJailBreak() -> Bool {
    return UIApplication.shared.canOpenURL(URL(string: "cydia://")!)
}



这里记得把 cydia 加入到 info.plist 中的 LSApplicationQueriesSchemes 字段里才能正常检测应用是否安装



这种方式简单粗暴,不过不建议用,因为准确度可能不高,一方面 cydia 可能把这个 URL Scheme 改掉防止你检测。另一方面正常手机也可能会有一个 app 的 URL Scheme 叫这个名字,造成误判。


方法二


检测是否存在 MobileSubstrate 动态库,这个库是 cydia 的基石,越狱环境下安装绝大部分插件,必须要有 MobileSubstrate,因此我们只需要判断是否存在这个动态库即可。


我在网上找了一个 c 语言的实现:

bool
isJailBreak(void)
{
    const char *const imageName = "MobileSubstrate";
    if (imageName != NULL) {
        const uint32_t imageCount = _dyld_image_count();
        for (uint32_t iImg = 0; iImg < imageCount; iImg++) {
            const char *name = _dyld_get_image_name(iImg);
            if (strstr(name, imageName) != NULL) {
                return true;
            }
        }
    }
    return false;
}


方法三


还是检测文件,如果越狱的话,设备会创建许多文件,可以使用 FileManager 来检测这些文件是否存在:

func isJailBreak() -> Bool {
#if targetEnvironment(simulator)
    return false
#else
    let files = [
        "/private/var/lib/apt",
        "/Applications/Cydia.app",
        "/Applications/RockApp.app",
        "/Applications/Icy.app",
        "/Applications/WinterBoard.app",
        "/Applications/SBSetttings.app",
        "/Applications/blackra1n.app",
        "/Applications/IntelliScreen.app",
        "/Applications/Snoop-itConfig.app",
        "/bin/sh",
        "/usr/libexec/sftp-server",
        "/usr/libexec/ssh-keysign /Library/MobileSubstrate/MobileSubstrate.dylib",
        "/bin/bash",
        "/usr/sbin/sshd",
        "/etc/apt /System/Library/LaunchDaemons/com.saurik.Cydia.Startup.plist",
        "/System/Library/LaunchDaemons/com.ikey.bbot.plist",
        "/Library/MobileSubstrate/DynamicLibraries/LiveClock.plist",
        "/Library/MobileSubstrate/DynamicLibraries/Veency.plist"
    ]
    return files.contains(where: {
        return FileManager.default.fileExists(atPath: $0)
    })
#endif
}


这里有个条件编译,在模拟器下是不需要检查的。


方法四


越狱之后所有 App 都被授予 root 权限,并且可以修改沙箱之外的文件。利用这个特点,如果我们的 App 可以写入其沙箱之外的文件,则证明该设备已越狱:

func isJailBreak() -> Bool {
    let string = "iOS 新知"
    do {
        try string.write(to: URL(filePath: "/private/myfile.txt"), atomically: true, encoding: .utf8)
        return true
    } catch {
        return false
    }
}


方法五


越狱之后也就意味着 App 可以随意调用系统 API 了,因此我们可以尝试调用系统 API,来查看是否能得到正确结果,以此来判断是否越狱:

func isJailBreak() -> Bool {
    let RTLD_DEFAULT = UnsafeMutableRawPointer(bitPattern: -2)
    let forkPtr = dlsym(RTLD_DEFAULT, "fork")
    typealias ForkType = @convention(c) () -> Int32
    let fork = unsafeBitCast(forkPtr, to: ForkType.self)

    return fork() != -1
}


建议以上五种方法结合使用,提高检测的准确率


检测到越狱设备,禁止使用


如果检测到当前运行环境为越狱设备,可以强制退出 App,以确保安全。强制退出 app 的方法就很多了,可以使用 exit(-1),也可以人为做个数组越界之类的:

// 检测到越狱设备
if isJailBreak() {
    // 退出 app
    exit(-1)
    // 或者数组越界 crash
    // [0][1]
}

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

iOS气泡提示工具BubblePopup的使用

iOS
BubblePopup 气泡弹框,气泡提示框,可用于新手引导,功能提示。 在平时的开发中,通常新手引导页或功能提示页会出现气泡弹窗来做提示。如果遇到了这类功能通常需要花费一定的精力来写这么一个工具的,这里写了一个气泡弹窗工具,希望能帮你提升一些开发效率。 使用...
继续阅读 »

BubblePopup


气泡弹框,气泡提示框,可用于新手引导,功能提示。


在平时的开发中,通常新手引导页或功能提示页会出现气泡弹窗来做提示。如果遇到了这类功能通常需要花费一定的精力来写这么一个工具的,这里写了一个气泡弹窗工具,希望能帮你提升一些开发效率。


使用方法

  • 从gitHub上下载代码到本地,代码地址:github.com/zhfei/Bubbl…
  • 调用BubblePopupManager文件内的单例方法,在指定的页面上添加气泡提示。 普通文本气泡弹窗使用方式如下:
BubblePopupManager.shared.addPopup(toView: self.view, tips: "冒泡弹窗", popupType: .dotLine, positionType: .bottom, popupPoint: nil, linkPoint: CGPoint(x: sender.frame.midX, y: sender.frame.minY), maxWidth: 200.0)


自定义View气泡弹窗使用方式如下:

BubblePopupManager.shared.addPopup(toView: self.view, customContentView: MyContentView(), popupType: .triangle, positionType: .bottom, popupPoint: CGPoint(x: sender.frame.midX, y: sender.frame.minY), linkPoint: nil, maxWidth: 200.0)

注意:自定义内容View只能使用frame布局,不能使用约束。


设计模式


气泡弹窗View的结构设计采用的设计模式为组合模式

把气泡弹窗分为3个部分:气泡背景,气泡指示器,气泡提示内容。


在创建气泡弹窗时,根据子类的自定义实现,将这三部分分别创建并组装到一起。实现了功能的灵活插拔和自定义扩展。


气泡弹窗View类图



气泡弹窗生成算法采用的设计模式为模版方法模式

在气泡构建基类中设置好气泡的构建步骤,把必要的部分或者提供默认实现的部分在父类中提供默认的实现,对其他需要自定义实现的部分,只在父类中写了一个抽象方法,具体实现交给子类自己实现。


虚线气泡弹窗类图



三角形气泡弹窗类图



核心实现

  • BubblePopupManager: 使用气泡弹窗工具的入口,通过它创建并添加一个气泡弹窗到指定的View上。

  • BubblePopupBuilder: 气泡弹窗构建者基类,使用模版方法模式定义了气泡的构建流程,子类可以自定义各自的实现。

  • DotLineBubblePopupBuilder: 虚线气泡弹窗基类,它是基类BubblePopupBuilder的子类,内部包含了虚线气泡弹窗生成时所需要的工具方法和必要属性,方便创建top,bottom,left,right虚线气泡弹窗。

  • TriangleBubblePopupBuilder : 三角形气泡弹窗基类,它是BubblePopupBuilder的子类,内部包含了三角形气泡弹窗生成时所需要的工具方法和必要属性,方便创建top,bottom,left,right三角形气泡弹窗

  • BubblePopup: 气泡弹窗View,它内部使用组合模式将子部件组合起来,组成了一个气泡弹窗。

  • BubbleViewFactory: 气泡弹窗子视图创建工程,用于创建气泡弹窗所需要的子视图,并将各个子视图组装成一个最终的气泡弹窗。


BubblePopupBuilder

BubblePopupBuilder是所有气泡弹窗的公共基类,对于里面定义的属性和方法的功能分别为


  • 属性:属性里保存的是气泡弹窗公共的,必要的数据。
  • 方法:在基类提供的方法中主要用于定义气泡的构建流程。 核心方法如下:
   func setupUI() {
addBubbleContentView(to: bubblePopup)
addBubbleBGView(to: bubblePopup)
updateLayout(to: bubblePopup)
addBubbleFlagView(to: bubblePopup)
}

其中气泡内容展示视图和气泡背景视图有默认实现,子类可以直接使用默认样式。


而气泡标识View和气泡布局方法则需要子类自己实现,因为不同类型的气泡弹窗它们的气泡标识设布局方式是不一样的。


DotLineBubblePopupBuilder

虚线气泡基类DotLineBubblePopupBuilder,它继承自BubblePopupBuilder

  • 属性:增加了虚线弹窗必要的linkPoint属性,即:虚线与气泡弹窗的连接点。 增加了一个坐标系转换懒加载属性,用于将用户设置的屏幕坐标点转成气泡内部的视图坐标系中的点。

  • 重要方法说明:

getDrawDotLineLayerRectParams

用于虚线图层绘制:获取虚线绘制时所需要的绘制元素坐标,如:虚线的开始,结束坐标,连接点圆的直径等。

getDotLineLayerContainerViewFrame

更新虚线容器View的位置大小信息:获取不同情况下的虚线容器Frame。

layoutDotLineBubblePopupView

更新虚线气泡弹窗的frame。

updateBGBubbleViewFrame

更新气泡背景的frame。


这里提供的方法属于工具方法,子类可以通过传递自己的类型来得到对应的结果。这里按道理可以使用设计模式中策略模式来对算法进行封装,如:在基类定义一个抽象方法,将上面则4个工具方法分拆到各自的子类中,让子类在对应的自己的类中实现这个方法。


这里没有这样做原因是:这些方法在子类中的实现代码并不复杂,用一个方法根据条件集中返回是比较方便的,而分拆到不同类中反而很麻烦。所以选择在基类中以方法工具的形式统一放置了。


DotLineTopBubblePopupBuilder

top型虚线气泡弹窗DotLineTopBubblePopupBuilder,它继承自DotLineBubblePopupBuilder,属于一直具体的弹窗类型。


它里面只对下面两个方法进行了重写,根据自己的类型进行子类个性化实现。

override func updateLayout
override func addBubbleFlagView

具体实现如下:

class DotLineTopBubblePopupBuilder: DotLineBubblePopupBuilder {

override func updateLayout(to bubblePopup: BubblePopup) {
layoutDotLineBubblePopupView(bubblePopup: bubblePopup, positionType: .top)
}

override func addBubbleFlagView(to bubblePopup: BubblePopup) {
assert(!self.targetPoint.equalTo(.zero), "气泡提示点无效")

let flagFrame = getDotLineLayerContainerViewFrame(position: .top, targetPoint: self.targetPoint)
let params = getDrawDotLineLayerRectParams(position: .top)
let flagBubbleView = BubbleViewFactory.generateDotLineBubbleFlagView(flagFrame: flagFrame, position: .top, params: params)
bubblePopup.bubbleFlagView = flagBubbleView
bubblePopup.addSubview(flagBubbleView)
}

}

其他bottom, left, right类型相似。


TriangleBubblePopupBuilder

三角形气泡基类TriangleBubblePopupBuilder,它继承自BubblePopupBuilder

  • 属性:相对于基类增加了popupPoint属性,它是三角形顶点指向的坐标点 增加了一个坐标系转换懒加载属性,用于将用户设置的屏幕坐标点转成气泡内部的视图坐标系中的点。

  • 重要方法说明:

getDrawTriangleLayeyRectParams

为三角形图层绘制提供不同气泡类型所需要的绘制元素坐标,如:三角形的三个顶点。

getTriangleLayerContainerViewFrame

获取不同情况下三角形图层容器的Frame,用于更新三角形图层容器View的位置大小。

layoutTriangleBubblePopupView

更新三角形气泡弹窗的frame。

updateTriangleBGBubbleView

更新气泡背景的frame。


三角形弹窗基类TriangleBubblePopupBuilder的设计方式和虚线弹窗基类是一样的。
这里的方法属于工具方法,子类可以通过传递自己的类型来得到对应的结果,通过牺牲一点开发模式的规范化来换取开发效率的提升。


在三角形气泡基类的下面同样有4个子类top,bottom,left ,right进行各种的自定义实现。


TriangleTopBubblePopupBuilder

top型三角形气泡弹窗DotLineTopBubblePopupBuilder,它继承自DotLineBubblePopupBuilder,属于一直具体的弹窗类型。


它里面只对下面这两个方法做了重写,根据自己的类型进行子类个性化实现。

override func updateLayout
override func addBubbleFlagView

具体实现如下:

class TriangleTopBubblePopupBuilder: TriangleBubblePopupBuilder {
override func updateLayout(to bubblePopup: BubblePopup) {
layoutTriangleBubblePopupView(bubblePopup: bubblePopup, positionType: .top)
}
override func addBubbleFlagView(to bubblePopup: BubblePopup) {
assert(!self.targetPoint.equalTo(.zero), "气泡提示点无效")

let flagFrame = getTriangleLayerContainerViewFrame(position: .top, targetPoint: self.targetPoint)
let params = getDrawTriangleLayeyRectParams(position: .top)
let flagBubbleView = BubbleViewFactory.generateTriangleBubbleFlagView(flagFrame: flagFrame, position: .top, params: params)
bubblePopup.bubbleFlagView = flagBubbleView
bubblePopup.addSubview(flagBubbleView)
}
}

其他bottom, left, right类型相似。


弹窗效果展示


三角形气泡弹窗



虚线气泡弹窗



自定义气泡弹窗



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

用一个RecyclerView实现抖音二级评论

前一阵,看到一位掘友分享了一篇文章:Android简单的两级评论功能实现,看得出来,是一位Android萌新记录的学习过程。我当时还留了一条建议: 建议用单RecyclerView+多ItemType+ListAdapter实现,保持UI层的清洁,把逻辑处理...
继续阅读 »

前一阵,看到一位掘友分享了一篇文章:Android简单的两级评论功能实现,看得出来,是一位Android萌新记录的学习过程。我当时还留了一条建议:



建议用单RecyclerView+多ItemType+ListAdapter实现,保持UI层的清洁,把逻辑处理集中在数据源的转换上,比如展开/收起二级评论可以利用flatMap和groupBy等操作符转换



其实我之前在工作中,也曾经做过类似抖音的二级评论的需求。但那个时候自己很菜,还没有用过Kotlin,协程更是没有接触过,这个功能和另一位同事一起开发了两周才搞定。


刚好这个周末没啥事,就想着写一个简单实现抖音二级评论基本功能的Demo。一方面,想试试自己现在开发这样一个需求会是什么样的体验;另一方面,也算是给Android掘友,尤其是萌新,分享一点业务开发的心得。


先上个效果图(没有UI,将就看吧),写代码的整个过程花了4个小时左右,相比当初自己开发需求已经快了很多了哈。



给产品估个两天时间,摸一天半的鱼不过分吧(手动斜眼)



评论.gif


需求拆分


这种大家常用的评论功能其实也就没啥好拆分的了,简单列一下:



  • 默认展示一级评论和二级评论中的热评,可以上拉加载更多。

  • 二级评论超过两条时,可以点击展开加载更多二级评论,展开后可以点击收起折叠到初始状态。

  • 回复评论后插入到该评论的下方。


技术选型


前面我在给掘友的评论中,也提到了技术选型的要点:


单RecyclerView + 多ItemType + ListAdapter


这是基本的UI框架。


为啥要只用一个RecyclerView?最重要的原因,就是在RecyclerView中嵌套同方向RecyclerView,会有性能问题和滑动冲突。其次,当下声明式UI正是各方大佬推崇的最佳开发实践之一,虽然我们没有使用声明式UI基础上开发的Compose/Flutter技术,但其构建思想仍然对我们的开发具有一定的指导意义。我猜测,androidx.recyclerview.widget.ListAdapter可能也是响应声明式UI号召的一个针对RecyclerView的解决方案吧。


数据源的转换


数据驱动UI!


既然选用了ListAdapter,那么我们就不应该再手动操作adapter的数据,再用各种notifyXxx方法来更新列表了。更提倡的做法是,基于data class浅拷贝,用Collection操作符对数据源的进行转换,然后将转换后的数据提交到adapter。为了提高数据转换性能,我们可以基于协程进行异步处理。


graph LR
start[原List] --异步数据处理--> 新List --> stop[ListAdapter.submitList]
stop --> start

要点:



  • 浅拷贝


低成本生成一个全新的对象,以保证数据源的安全性。


data class Foo(val id: Int, val content: String)

val foo1 = Foo(0, "content")
val foo2 = foo1.copy(content = "updated content")


  • Collection操作符


Kotlin中提供了大量非常好用的Collection操作符,能灵活使用的话,非常有利于咱们向声明式UI转型。


前面我提到了groupByflatMap这两个操作符。怎么使用呢?


以这个需求为例,我们需要显示一级评论、二级评论和展开更多按钮,想要分别用一个data class来表示,但是后端返回的数据中又没有“展开更多”这样的数据,就可以这样处理:


// 从后端获取的数据List,包括有一级评论和二级评论,二级评论的parentId就等于一级评论的id
val loaded: List<CommentItem> = ...
val grouped = loaded.groupBy {
// (1) 以一级评论的id为key,把源list分组为一个Map<Int, List<CommentItem>>
(it as? CommentItem.Level1)?.id ?: (it as? CommentItem.Level2)?.parentId
?: throw IllegalArgumentException("invalid comment item")
}.flatMap {
// (2) 展开前面的map,展开时就可以在每级一级评论的二级评论后面添加一个控制“展开更多”的Item
it.value + CommentItem.Folding(
parentId = it.key,
)
}


  • 异步处理


前面我们描述的数据源的转换过程,在Kotlin中,可以简单地被抽象为一个操作:


List<CommentItem>.() -> List<CommentItem>

对于这个需求,数据源转换操作就包括了:分页加载,展开二级评论,收起二级评论,回复评论等。按照惯例,抽象一个接口出来。既然我们要在协程框架下进行异步处理,需要给这个操作加一个suspend关键字。


interface Reducer {
val reduce: suspend List<CommentItem>.() -> List<CommentItem>
}

为啥我给这个接口取名Reducer?如果你知道它的意思,说明你可能已经了解过MVI架构了;如果你还不知道它的意思,说明你可以去了解一下MVI了。哈哈!


不过今天不谈MVI,对于这样一个小Demo,完全没必要上架构。但是,优秀架构为我们提供的代码构建思路是有必要的!


这个Reducer,在这里就算是咱们的小小业务架构了。



  • 异步2.0


前面谈到异步,我们印象中可能主要是网络请求、数据库/文件读写等IO操作。


这里我想要延伸一下。


ActivitystartActivityForResult/onActivityResultDialog的拉起/回调,其实也可以看着是异步操作。异步与是否在主线程无关,而在于是否是实时返回结果。毕竟在主线程上跳转到其他页面,获取数据再回调回去使用,也是花了时间的啊。所以在协程的框架下,有一个更适合描述异步的词语:挂起(suspend)


说这有啥用呢?仍以这个需求为例,我们点击“回复”后拉起一个对话框,输入评论确认后回调给Activity,再进行网络请求:


class ReplyDialog(context: Context, private val callback: (String) -> Unit) : Dialog(context) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.dialog_reply)
val editText = findViewById<EditText>(R.id.content)
findViewById<Button>(R.id.submit).setOnClickListener {
if (editText.text.toString().isBlank()) {
Toast.makeText(context, "评论不能为空", Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
callback.invoke(editText.text.toString())
dismiss()
}
}
}

suspend List<CommentItem>.() -> List<CommentItem> = {
val content = withContext(Dispatchers.Main) {
// 由于整个转换过程是在IO线程进行,Dialog相关操作需要转换到主线程操作
suspendCoroutine { continuation ->
ReplyDialog(context) {
continuation.resume(it)
}.show()
}
}
...进行其他操作,如网络请求
}

技术选型,或者说技术框架,咱们就实现了,甚至还谈到了部分细节了。接下来进行完整实现细节分享。


实现细节


MainActivity


基于上一章节的技术选型,咱们的MainActivity的完整代码就是这样了。


class MainActivity : AppCompatActivity() {
private lateinit var commentAdapter: CommentAdapter

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
commentAdapter = CommentAdapter {
lifecycleScope.launchWhenResumed {
val newList = withContext(Dispatchers.IO) {
reduce.invoke(commentAdapter.currentList)
}
val firstSubmit = commentAdapter.itemCount == 1
commentAdapter.submitList(newList) {
// 这里是为了处理submitList后,列表滑动位置不对的问题
if (firstSubmit) {
recyclerView.scrollToPosition(0)
} else if (this@CommentAdapter is FoldReducer) {
val index = commentAdapter.currentList.indexOf(this@CommentAdapter.folding)
recyclerView.scrollToPosition(index)
}
}
}
}
recyclerView.adapter = commentAdapter
}
}

RecyclerView设置一个CommentAdapter就行了,回调时也只需要把回调过来的Reducer调度到IO线程跑一下,得到新的数据listsubmitList就完事了。如果不是submitList后有列表的定位问题,代码还能更精简。如果有知道更好的解决办法的朋友,麻烦留言分享一下,感谢!


CommentAdapter


别以为我把逻辑处理扔到adapter中了哦!


AdapterViewHolder都是UI组件,我们也需要尽量保持它们的清洁。


贴一下CommentAdapter


class CommentAdapter(private val reduceBlock: Reducer.() -> Unit) :
ListAdapter<CommentItem, VH>(object : DiffUtil.ItemCallback<CommentItem>() {
override fun areItemsTheSame(oldItem: CommentItem, newItem: CommentItem): Boolean {
return oldItem.id == newItem.id
}

override fun areContentsTheSame(oldItem: CommentItem, newItem: CommentItem): Boolean {
if (oldItem::class.java != newItem::class.java) return false
return (oldItem as? CommentItem.Level1) == (newItem as? CommentItem.Level1)
|| (oldItem as? CommentItem.Level2) == (newItem as? CommentItem.Level2)
|| (oldItem as? CommentItem.Folding) == (newItem as? CommentItem.Folding)
|| (oldItem as? CommentItem.Loading) == (newItem as? CommentItem.Loading)
}
}) {

init {
submitList(listOf(CommentItem.Loading(page = 0, CommentItem.Loading.State.IDLE)))
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH {
val inflater = LayoutInflater.from(parent.context)
return when (viewType) {
TYPE_LEVEL1 -> Level1VH(
inflater.inflate(R.layout.item_comment_level_1, parent, false),
reduceBlock
)

TYPE_LEVEL2 -> Level2VH(
inflater.inflate(R.layout.item_comment_level_2, parent, false),
reduceBlock
)

TYPE_LOADING -> LoadingVH(
inflater.inflate(
R.layout.item_comment_loading,
parent,
false
), reduceBlock
)

else -> FoldingVH(
inflater.inflate(R.layout.item_comment_folding, parent, false),
reduceBlock
)
}
}

override fun onBindViewHolder(holder: VH, position: Int) {
holder.onBind(getItem(position))
}

override fun getItemViewType(position: Int): Int {
return when (getItem(position)) {
is CommentItem.Level1 -> TYPE_LEVEL1
is CommentItem.Level2 -> TYPE_LEVEL2
is CommentItem.Loading -> TYPE_LOADING
else -> TYPE_FOLDING
}
}

companion object {
private const val TYPE_LEVEL1 = 0
private const val TYPE_LEVEL2 = 1
private const val TYPE_FOLDING = 2
private const val TYPE_LOADING = 3
}
}

可以看到,就是一个简单的多ItemTypeAdapter,唯一需要注意的就是,在Activity里传入的reduceBlock: Reducer.() -> Unit,也要传给每个ViewHolder


ViewHolder


篇幅原因,就只贴其中一个:


abstract class VH(itemView: View, protected val reduceBlock: Reducer.() -> Unit) :
ViewHolder(itemView) {
abstract fun onBind(item: CommentItem)
}

class Level1VH(itemView: View, reduceBlock: Reducer.() -> Unit) : VH(itemView, reduceBlock) {
private val avatar: TextView = itemView.findViewById(R.id.avatar)
private val username: TextView = itemView.findViewById(R.id.username)
private val content: TextView = itemView.findViewById(R.id.content)
private val reply: TextView = itemView.findViewById(R.id.reply)
override fun onBind(item: CommentItem) {
avatar.text = item.userName.subSequence(0, 1)
username.text = item.userName
content.text = item.content
reply.setOnClickListener {
reduceBlock.invoke(ReplyReducer(item, itemView.context))
}
}
}

也是很简单,唯一特别一点的处理,就是在onClickListener中,让reduceBlockinvoke一个Reducer实现。


Reducer


刚才在技术选型章节,已经提前展示了“回复评论”这一操作的Reducer实现了,其他Reducer也差不多,比如展开评论操作,也封装在一个Reducer实现ExpandReducer中,以下是完整代码:


data class ExpandReducer(
val folding: CommentItem.Folding,
) : Reducer {
private val mapper by lazy { Entity2ItemMapper() }
override val reduce: suspend List<CommentItem>.() -> List<CommentItem> = {
val foldingIndex = indexOf(folding)
val loaded =
FakeApi.getLevel2Comments(folding.parentId, folding.page, folding.pageSize).getOrNull()
?.map(mapper::invoke) ?: emptyList()
toMutableList().apply {
addAll(foldingIndex, loaded)
}.map {
if (it is CommentItem.Folding && it == folding) {
val state =
if (it.page > 5) CommentItem.Folding.State.LOADED_ALL else CommentItem.Folding.State.IDLE
it.copy(page = it.page + 1, state = state)
} else {
it
}
}
}

}

短短一段代码,我们做了这些事:



  • 请求网络数据Entity list(假数据)

  • 通过mapper转换成显示用的Item数据list

  • Item数据插入到“展开更多”按钮前面

  • 最后,根据二级评论加载是否完成,将“展开更多”的状态置为IDLELOADED_ALL


一个字:丝滑!


用于转换EntityItemmapper的代码也贴一下吧:


// 抽象
typealias Mapper<I, O> = (I) -> O
// 实现
class Entity2ItemMapper : Mapper<ICommentEntity, CommentItem> {
override fun invoke(entity: ICommentEntity): CommentItem {
return when (entity) {
is CommentLevel1 -> {
CommentItem.Level1(
id = entity.id,
content = entity.content,
userId = entity.userId,
userName = entity.userName,
level2Count = entity.level2Count,
)
}

is CommentLevel2 -> {
CommentItem.Level2(
id = entity.id,
content = if (entity.hot) entity.content.makeHot() else entity.content,
userId = entity.userId,
userName = entity.userName,
parentId = entity.parentId,
)
}

else -> {
throw IllegalArgumentException("not implemented entity: $entity")
}
}
}
}

细心的朋友可以看到,在这里我顺便也将热评也处理了:


if (entity.hot) entity.content.makeHot() else entity.content

makeHot()就是用buildSpannedString来实现的:


fun CharSequence.makeHot(): CharSequence {
return buildSpannedString {
color(Color.RED) {
append("热评 ")
}
append(this@makeHot)
}
}

这里可以提一句:尽量用CharSequence来抽象表示字符串,可以方便我们灵活地使用Span来减少UI代码。


data class


也贴一下相关的数据实体得了。



  • 网络数据(假数据)


interface ICommentEntity {
val id: Int
val content: CharSequence
val userId: Int
val userName: CharSequence
}

data class CommentLevel1(
override val id: Int,
override val content: CharSequence,
override val userId: Int,
override val userName: CharSequence,
val level2Count: Int,
) : ICommentEntity


  • RecyclerView Item数据


sealed interface CommentItem {
val id: Int
val content: CharSequence
val userId: Int
val userName: CharSequence

data class Loading(
val page: Int = 0,
val state: State = State.LOADING
) : CommentItem {
override val id: Int=0
override val content: CharSequence
get() = when(state) {
State.LOADED_ALL -> "全部加载"
else -> "加载中..."
}
override val userId: Int=0
override val userName: CharSequence=""

enum class State {
IDLE, LOADING, LOADED_ALL
}
}

data class Level1(
override val id: Int,
override val content: CharSequence,
override val userId: Int,
override val userName: CharSequence,
val level2Count: Int,
) : CommentItem

data class Level2(
override val id: Int,
override val content: CharSequence,
override val userId: Int,
override val userName: CharSequence,
val parentId: Int,
) : CommentItem

data class Folding(
val parentId: Int,
val page: Int = 1,
val pageSize: Int = 3,
val state: State = State.IDLE
) : CommentItem {
override val id: Int
get() = hashCode()
override val content: CharSequence
get() = when {
page <= 1 -> "展开20条回复"
page >= 5 -> ""
else -> "展开更多"
}
override val userId: Int = 0
override val userName: CharSequence = ""

enum class State {
IDLE, LOADING, LOADED_ALL
}
}
}

这部分没啥好说的,可以注意两个点:



  • data class也是可以抽象的。但这边我处理不是很严谨,比如CommentItem我把userIduserName也抽象出来了,其实不应该抽象出来。

  • 在基于Reducer的框架下,最好是把data class的属性都定义为val


结语


更多的代码就不贴了,贴太多影响观感。有兴趣的朋友可以移步源码


总结一下实现心得:



  • 数据驱动UI

  • 对业务的精准抽象

  • 对异步的延伸理解

  • 灵活使用Collection操作符

  • 没有UI和PM,写代码真TM爽!


作者:blackfrog
来源:juejin.cn/post/7276808079143190565
收起阅读 »

2022年三蹦子团队生存指南

概述 2022年,这一年,经历了四川有酷热到河里没有水发电而导致停电的夏天🌞,也面对此时冷冷的冬天❄️。 但更深的体会,是咱们这个五菱宏光🚚一样的团队,在上半年时,修修补补还能上秋名山一战,大叫:“输者留下车标”。 结果,下半年校招的应届生一到岗,再加上突发...
继续阅读 »

概述


2022年,这一年,经历了四川有酷热到河里没有水发电而导致停电的夏天🌞,也面对此时冷冷的冬天❄️。


但更深的体会,是咱们这个五菱宏光🚚一样的团队,在上半年时,修修补补还能上秋名山一战,大叫:“输者留下车标”。


1.PNG


结果,下半年校招的应届生一到岗,再加上突发需求增多,整个团队的状态就从四个轱辘变成三个轱辘了。而这种三个轱辘的车,北京叫它“三蹦子”,要是后面有棚子能坐人的话,在四川这种车也被叫作“火三轮”。


2.PNG


有一段对三蹦子的描述如下:三轮车前部为驾驶位,后部是车厢,厢体一般为金属制半圆形,可以并排乘坐两个人,车厢上可安装防雨篷,后部车厢下面装有弹簧和两个轮子


就只看这个三蹦子的介绍,就知道这东西只有三个轮子,跑起来不那么靠谱。如果一个团队也这样三个轮子运转的话,估计也不那么靠谱了。


团队


为避免对号入座,人物描写有部分润色,非 100% 人物原本特征


观海👨🏻‍🦲


三蹦子团队的 leader 是观海,作为团队 leader,他负责规划开发计划,需求交付迭代这些琐事。


面对团队躺平的、划水的人,他需要经常苦口婆心地给团队讲:《高效沟通的方法》《有计划的安排工作》 以及 《程序员脱发防治》。他还得时不时威胁一下团队各位成员,敲打敲打大家,让大家多发挥一下主观能动性、把各自工作职责内的事尽量做好一点,让各自纸面的 KPI 好看点,但就是这样做,也仅能维持团队的不散架以及保持自身为数不多的头发👨🏻‍🦲。


阿呜🐕


而我,阿呜,在公司里的职称,说起来是高级研发工程师,当然观海也是高工的,只是他是领导,不能一起论。


其实本来我不是高工,但是他们说,每个团队都需要一个兜底的高级程序员。我也不是谦虚,我说你们另请高明吧。他们说组织上已经决定了,你们团队的高工就是你了。于是,我就成为了高工。


我顺便还得兼职团队的系统工程师。原因是负责各个团队的、真正的系统工程师全都跑去给客户写 ppt 了,每个团队自身的需求分析、系统设计的任务都得研发人员自己搞了。


就这样我不仅要完成自己的研发任务,还要作为类似备份人员对团队其他人员的工作兜底,还得参与相关的设计工作。真的是一个人当两个半人用,还只发一个人的工资。给老板点个👍。


鱿鱼🦑


团队里另一位,我的徒弟,鱿鱼,在来我司之前,用了三年时间,干倒闭了三家公司。


作为一名中级研发工程师,他已经成功的从四大天坑之一的环境工程转行进入了计算机行业。甚至我还记得当年面试他时,他说他发誓绝不回去干环境,要一直做一名研发。


鱿鱼,他为人勤奋,但缺少足够的研发经验,一个原因是非科班欠缺了很多基础知识,二个原因是在之前的公司做研发时,他只接触了非常简单的 CRUD,甚至没有接触过如何做需求分析和设计。这导致很多时候,我把评审过的详细设计文档交到他手上时,他依然会有很多疑问,严重拖慢了自己的开发进度⏰。


年轻人👦🏻


而观海的徒弟,年轻人,作为一名入职一年的助理研发工程师,为人聪明,计算机专业知识扎实,虽然还没有丰富的软件设计经验,但编码经验已经能应对80%的情况了,是团队里安排工作时唯一能让观海放心的人。


他目前最让人津津乐道的成绩是在实习三个月期间,连续拿到了三个优秀评价,而他能拿到这个成绩的原因也在于入职第一周,就在对 ELK 没有任何相关经验的基础上,凭测试提供的些微线索,成功定位并解决了组件升级导致的日志采集策略差异问题。


自此,年轻人在整个部门一炮而红,接着在试用期的三个月里连续解决多个问题单,迅速上手了工作。


时间最终会让他成为一名优秀的工程师。


三板斧🪓🪓🪓


团队里的反面人物,三板斧,一位干了八年研发的中级研发工程师。为何有八年的研发经验、入职我司两年了还是中级呢?


因为他的工作态度让部门老大不同意他的晋升。三板斧,不论是做需求还是改问题单,他上来不做任何分析沟通,直接就对其他研发测试同事连续三个输出:“这个以前就是这样设计的。”🪓“这个不是我写的。”🪓“为什么你认为是问题?”🪓


就这一气呵成的输出,经常气得测试找他们老大过来投诉。


而他做需求开发的话,如果他能拖到月底最后一刻交付代码,那他就绝对不会从月初开始实现需求。而另一方面,面对客户去解决客户问题时,他对客户真的是贴心的“有问必答”,主动告诉客户这就是个 bug,甚至给客户深入讲解设计缺陷,导致客户听完后若有所思,转身就给公司发起一个产品问题投诉。于是整个部门收到的产品投诉,这一年属于三板斧“创造”的投诉能占到 40% 到 50%,部门老大看到他时,都经常玩变脸🌚。


皇上👑


今年下半年入职的应届生,皇上,00 后应届生。他有着典型 00 后的特点,有自己的想法,不顺从“权威”,但也有着小年轻特有的缺点,缺乏用于工作的方式方法。


领导把他安排给我带,我一看,这不活脱脱就是《甄嬛传》里的皇帝吗?👑 皇上是计算机专业毕业的,属于难得的科班,标准的根正苗红。但是我总觉得他大学里是水货,要不然也写不出如下的怪异代码。


public boolean test(boolean flag){
……
if(flag){
return true;
}else if(!flag){
return false;
}
}

问题❓


这样的团队有什么问题,我总结了一下比较核心的几个问题:




  • 问题1:团队人员有人存在明显短板,有人总是躺平,有人总给团队捅娄子




  • 问题2:专职人员缺失(比如系统设计师),导致研发人员的工作职责混乱且效率低下




  • 问题3:新人融入周期长,适应能力、学习能力差,拖累团队整体效率




这些问题对这个三蹦子团队来说是致命的💀。那么最终团队又是怎么没散架,而生存了下来呢?


生存指南📖


首先,针对个别人躺平或者捅娄子的情况,观海在每月任务规划时,就让我提前评估是否可以兜底相关任务,不能兜底的就考虑延期或者拆分需求。


在开发周期中,通过每日晨会对团队成员进度进行评审,并按照检视时间点定时检视成员的当前成果,避免有人真的划水、躺平。


如果在这种情况下,依然出现超进度风险,就由我或者其他成员接手兜底,避免无法交付的风险,同时给予当期划水、躺平的人员较差的考评。


针对专职人员缺失的问题,一方面面对现实,由团队自我协助,通力完成设计,以达成交付。同时让团队研发人员分别主导不同的需求的设计,其他人员辅助参与,锻炼研发人员设计能力;另一方面,在核心需求的开发中,观海会向领导要求系统设计师必须回归本职参与设计。


而新人的培养,则按预估新人能力不足的情况,将其定位为完全不懂计算机的人,让新人主要参与非困难问题单的定位与修改,以达到熟悉项目代码、学习优秀代码、学会计算机逻辑思维的目的。同时辅以试用期考评成绩进行鞭策,推动新人尽快完成从学生到职场打工人的蜕变。


以上措施,虽然也是螺蛳壳里做道场,不能真的解决问题,但尽量公平的让团队成员各司其职,顺便再化阻力为助力,锻炼了团队成员的能力。


但是到了年底,我终于还是忍不住,给观海提了一个一直想问的问题:“像三板斧这样的人,一直这要划水,真的行吗?”


观海给我一个神秘的微笑😄,道:“团队有这样的人存在,不就能证明其他人工作的成绩吗?只有对比才能看出我们工作的价值呀。”


说完,他拍拍我的肩膀,顶着地中海飘然远去,离我在原地久久思考……🤔


作者:阿呜的边城
来源:juejin.cn/post/7175410777447202874
收起阅读 »