注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

环信FAQ

环信FAQ

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

摸鱼时间打造一款产品

辞职 我辞职了 拿上水杯,挎起背包,工位被我丢在了身后,一阵清风过后,我便离开了这度过一年半载的地方 辞职的原因很简单,公司快没钱了,要么同公司共进退,要么离开,于是我选择了离开 公司的待遇不算好也不算差,工资不算满意,但至少双休不加班。平时开发阶段末尾还比较...
继续阅读 »

辞职


我辞职了


拿上水杯,挎起背包,工位被我丢在了身后,一阵清风过后,我便离开了这度过一年半载的地方


辞职的原因很简单,公司快没钱了,要么同公司共进退,要么离开,于是我选择了离开


公司的待遇不算好也不算差,工资不算满意,但至少双休不加班。平时开发阶段末尾还比较闲,大把摸鱼时间,逛逛各种论坛,掘金、知乎、github不亦乐乎,现在看来公司倒闭和我不无关系


久而久之,不免有些无聊。论坛里充斥着灌水文章,看多了属实是食之无味。于是为了打发时间,只能写一写自己的项目。一想到老板在为我打工,敲打键盘的双手便愈发轻盈了


未曾设想的道路


大半年前为了记录学习一项技能到底要花多少时间,我开了个新坑,做一款计时软件,记录某个时间段发生的事情


和以往一样,最初只是打算随便写写,写个基础功能完事。但在使用的过程中,越来越多的需求在脑海中构建。编程最有趣的便是创造感,你能感受到自己在创建一个新的世界,在创造的过程中时间飞速流逝,一转眼便度过了无聊的一天


激情是短暂的,生活是漫长的。和往常一样,功能在逐步完成,但我的兴趣在逐渐减少。没有添加柴薪,火便只能渐渐势弱


此时两个选择摆在面前,一个便是不再更新下去,毕竟要做的已经完成了,再去寻找下一个打发时间的事情就好了。另一个则是保持兴趣继续做下去


兴趣?利益?


做一个开源软件,如果能收获社区的掌声想必是件自豪的事情。但如果只有掌声,久而久之开源作者可能会陷入自己到底为了什么才做这件事的思维泥潭。有的人失去了兴趣便离开了,有的人发出了声音希望得到一些回馈


兴趣可以支撑人前行,但又有多少人能不求回报去做一件事?不可否认,曾经幻想过做出爆红的软件,然后不用打工,财富自由这样的白日梦。虽然不能一步登天,但我想借助它向前一步


审视一下目前的状况,如果要供用户使用,一个简单的计时功能加上记录,未免太过单薄。这么简单的功能实在谈不上什么竞争力,实现成本过低,而且我相信人们更愿意使用移动app,而不是在pc上去使用这个功能。我需要一个特定于pc且有实在价值的功能,很快我便找到了,它既满足前面的要求,又契合软件的主题


广告恐怕是最理想的获利方式,不会影响用户使用,也不用去考虑升级版之类的的东西。虽然不知道具体能有多少收入,但希望起码能够抵消掉域名的费用


有了继续前进的目标,这艘小船便能扬帆远航


但眼下的问题很严重,我在技术选型上摔了个大跟头


重头再来


好的开始是成功的一半,但没有人能预料到未来会发生什么


使用vue3为前端,我直接选择了webview方向的跨端框架


在以go为后端的wails和rust为后端tauri中,我选择了go。之前学习过一段时间的rust,深知学习的难度。而且在最初的预想中,我只是打算做个简单的计时软件,使用go也只是做一下数据库操作。不久后就完成了最初的一版,但在后续的尝试中,发现wails的生态还是太小了,很多基础的功能都需要自己实现。这时再看看tauri就显得很香了,各种插件和前端的绑定,再加上go并没有用得多么称手,于是只能长痛不如短痛了


ui框架的选择上我也犯了同样的问题。开始是偏向于material design这种风格,选择了vuetify,这个框架当时我看了很久,做的时候已经要到v3正式版本了。本来以为没问题,但后续使用时过于难受,此时文档基本没怎么更新,issue也被各种bug塞满了。只能快刀斩乱麻,换了习惯的ant-design-vue,风格区别很大,但改改样式也能用。quasar同样在我的考虑范围内,但更加小众,目前是不打算换了,在tauri v2移动端正式版后,再做尝试


为什么最开始没有选择做移动端?功能契合,使用起来也更方便。一方面是我的主要技能栈是js,另一方面重新学移动端过于不切实际,为一个八字没一撇的项目去学实在没有必要。flutter我之前也学过,试着写了一点,但还是不如js来得舒服


回过头来,发现走了很多弯路,但不去尝试只站在远处观望,永远也不会有结果。颠颠撞撞重头再来


编程之外


我一直把时间花在了代码之上,但想要做一款产品还远远不够,它迫使我不得不将视角转向那些我不曾关注的角落


UI可谓是产品的脸面,用户的第一印象便停留在了logo和界面上,虽然使用了风格统一的组件库,但将他们组合在一起的时候未必能将它们严丝合缝。目前只能说是勉强能看,日后再做修改


说明文档带领用户快速理解程序的运作,由于用户没有设计者的前提条件,很多理所当然也就需要一一记录


想要完善功能,bug和feature的反馈也要做指引,方便接收用户意见,确定前进路线


说明文档


参考vite的官网,使用vitepress,写markdown就可以了,还可以配上vue组件,还算方便


部署上选择了netlify,可以换自己的域名,还可以自动更新ssl证书


本来以为部署很麻烦的,结果一个小时左右就全部搞定,包括在namesilo上买域名,然后在netlify部署、配置


拥抱AI


在完成这些工作的过程中,有不少地方借助了AI,可以说很大程度加快了进程


编码上,由于我完全不懂windows编程和勉强会点rust语法,想要完成监听系统上的应用状态这项功能,根本就无从谈起。要花大量时间去学习的话,反而和我利用碎片时间进行编程相冲突了。况且在new bing的帮助下,我完成一个简单的函数就要花费数个小时的尝试。new bing根据我的需求返回了相关的api参考,但很多时候返回的代码并不能直接运行,有着这样那样的问题,需要去修正。很难想象仅凭我一人去翻找资料何时才能完成这冰山一角


在这个过程中,new bing最大的帮助就是提供了关键词。很多时候,你知道一个事物,想用自己的语言需要一长串词语去描述,但过去的搜索引擎并不能理解这些,而且就算把描述输入进去,也会因为过多的关键字导致答案被淹没在茫茫的网页之中。这就造成了一个困境,我不知道它叫什么,所以我要去搜索,但搜索的时候要知道它叫什么


在netlify配置域名,我输入了如何去配置,new bing给出了关键的name servers,省去了花时间去到处去找教程


为应用绘制一个logo,很显然我并没有这个能力,使用Bing Image Creator,一段描述就能生成


这些都是一些无关紧要,琐碎的事情,我只想获取结果,把精力留在我擅长的事情上。试想一下,我一个人去实现要花掉多少时间?最终能实现吗?部分功能交给其他人,又要用什么去换取?


计划


讲到这里如果有兴趣了解一下的话,可以移步仓库地址,但目前的功能我只能说很少,而且还可能出现问题,我提前声明一下。说明文档见此处,需要看清警告提示


为什么这个时候来写这篇文章来介绍呢,主要是辞职了也没事干,已经做了大半年就整理了一下。原本是半年后再辞职的,但计划赶不上变化,只能提前放出来看看情况


还有一项没能赶上的便是广告,使用的是google adsense,但提交申请后便石沉大海。尽管提前做了申请,但已经过去了几个星期。可能是开始建站时随意申请被驳回的缘故,久久没有反应


最后


辞职其实还有一个原因,就是累了,光是待在公司什么都不干,也能感觉到劳累。工作小憩之余,在过道眺望远方时,我一直想问自己究竟在干些什么。我想做出改变,想去尝试新的东西,体验另外一种生活


想过很多次,以后也许不会从事编程的工作,但又有什么选择呢。我希望是创作,而不是枯燥的重复劳作,但我很清楚这不是换一个职业就能改变的问题,终究是实力的问题。编程很有趣,但在公司并不是如此


现在我已经度过了一周的悠闲时光了,白天在家看看书,傍晚下楼走走,看着面向我驶过的匆忙下班的人流,感叹这也是自己前不久的模样。我背朝着喧闹,走上凉爽的林荫道,晚风吹过,天

作者:hanaTsuk1
来源:juejin.cn/post/7256879435340890172
边挂着一轮淡淡的月牙

收起阅读 »

@Contended注解有什么用?

@Contended是Java 8中引入的一个注解,用于减少多线程环境下的“伪共享”现象,以提高程序的性能。要理解@Contended的作用,首先要了解一下什么是伪共享(False Sharing)。1. 什么是伪共享?伪共享(False Sharing)是多...
继续阅读 »

@Contended是Java 8中引入的一个注解,用于减少多线程环境下的“伪共享”现象,以提高程序的性能。

要理解@Contended的作用,首先要了解一下什么是伪共享(False Sharing)。

1. 什么是伪共享?

伪共享(False Sharing)是多线程环境中的一种现象,涉及到CPU的缓存机制和缓存行(Cache Line)。

现代CPU中,为了提高访问效率,通常会在CPU内部设计一种快速存储区域,称为缓存(Cache)。CPU在读写主内存中的数据时,会首先查看该数据是否已经在缓存中。如果在,就直接从缓存读取,避免了访问主内存的耗时;如果不在,则从主内存读取数据并放入缓存,以便下次访问。

缓存不是直接对单个字节进行操作的,而是以块(通常称为“缓存行”)为单位操作的。一个缓存行通常包含64字节的数据。

在多线程环境下,如果两个或更多的线程在同一时刻分别修改存储在同一缓存行的不同数据,那么CPU为了保证数据一致性,会使得其他线程必须等待一个线程修改完数据并写回主内存后,才能读取或者修改这个缓存行的数据。尽管这些线程可能实际上操作的是不同的变量,但由于它们位于同一缓存行,因此它们之间就会存在不必要的数据竞争,这就是伪共享。

伪共享会降低并发程序的性能,因为它会增加缓存的同步操作和主内存的访问。解决伪共享的一种方式是尽量让经常被并发访问的变量分布在不同的缓存行中,例如,可以通过增加无关的填充数据,或者利用诸如Java的@Contended注解等工具。

2. @Contended注解是什么?

@Contended 是Java 8引入的一个注解,设计用于减少多线程环境下的伪共享(False Sharing)问题以提高程序性能。

伪共享是现代多核处理器中一个重要的性能瓶颈,它发生在多个处理器修改同一缓存行(Cache Line)中的不同数据时。缓存行是内存的基本单位,一般为64字节。当一个处理器读取主内存中的数据时,它会将整个缓存行(包含需要的数据)加载到本地缓存(L1,L2或L3缓存)中。如果另一个处理器修改了同一缓存行中的其他数据,那么原先加载到缓存中的数据就会变得无效,需要重新从主内存中加载。这会增加内存访问的延迟,降低程序性能。

@Contended注解可以标注在字段或者类上。它能使得被标注的字段在内存布局上尽可能地远离其他字段,使得被标注的字段或者类中的字段分布在不同的缓存行上,从而减少伪共享的发生。

例如,考虑以下代码:

public class Foo {
@Contended
long x;
long y;
}

在这里,x@Contended注解标记,所以xy可能会被分布在不同的缓存行上,这样如果多个线程并发访问xy,就不会引发伪共享。

需要注意的是,@Contended是JDK的内部API,它在Java 8中引入,但在默认情况下是不开放的,要使用需要添加JVM参数-XX:-RestrictContended,并且在编译时需要使用--add-exports java.base/jdk.internal.vm.annotation=ALL-UNNAMED。此外,过度使用@Contended可能会浪费内存,因为它会导致大量的内存空间被用作填充以保持字段间的距离。所以在使用时需要谨慎权衡内存和性能的考虑。

3. 简单案例

在Java 8及以上版本中,@Contended注解是属于jdk的内部API,因此在正常情况下使用时需要打开开关-XX:-RestrictContended才能正常使用。同时需要注意的是,@Contended在JDK 9以后的版本中可能无法正常工作,因为JDK 9开始禁止使用Sun的内部API。

以下是一个@Contended注解的简单使用案例:

import jdk.internal.vm.annotation.Contended;

public class ContendedExample {

@Contended
volatile long value1 = 0L;

@Contended
volatile long value2 = 0L;

public void increaseValue1() {
value1++;
}

public void increaseValue2() {
value2++;
}

public static void main(String[] args) {
ContendedExample example = new ContendedExample();

Thread thread1 = new Thread(() -> {
for (int i = 0; i < 1000000; i++) {
example.increaseValue1();
}
});

Thread thread2 = new Thread(() -> {
for (int i = 0; i < 1000000; i++) {
example.increaseValue2();
}
});

thread1.start();
thread2.start();

try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println("value1: " + example.value1);
System.out.println("value2: " + example.value2);
}
}

这个例子中定义了两个使用了@Contended注解的volatile长整型字段value1value2。两个线程分别对这两个字段进行增加操作。因为这两个字段使用了@Contended注解,所以他们会被分布在不同的缓存行中,减少了因伪共享带来的性能问题。但由于伪共享的影响在实际运行中并不容易直接观察,所以这个例子主要展示了@Contended注解的使用方式,而不是实际效果。


作者:一只爱撸猫的程序猿
链接:https://juejin.cn/post/7255604228956110904
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

多页应用和单页应用的对比

在前端开发中,多页应用(MPA,Multi-Page Application)和单页应用(SPA,Single-Page Application)是两种不同的应用构建方式,它们的主要区别在于如何处理页面切换。多页应用(MPA)的主要特点是每个页面都是独立的 H...
继续阅读 »

在前端开发中,多页应用(MPA,Multi-Page Application)和单页应用(SPA,Single-Page Application)是两种不同的应用构建方式,它们的主要区别在于如何处理页面切换。

多页应用(MPA)的主要特点是每个页面都是独立的 HTML 文件,页面的切换通过重新加载整个页面来实现。在 MPA 中,每个页面都有自己的控制器和视图,因此需要每个页面单独加载所需的资源,如样式表、脚本和图片等。这种架构通常适用于传统的 Web 应用程序,具有稳定的 URL 和 SEO 优化的能力。

单页应用(SPA)的主要特点是在一个 HTML 文件中加载应用程序的所有资源,如 JavaScript、CSS 和 HTML。在 SPA 中,所有页面切换都是在客户端完成的,不需要重新加载整个页面,而是通过 AJAX(FETCH) 和 JavaScript 动态地更新 DOM。由于只需要加载一次资源,因此可以提高应用程序的性能和响应速度。但是,这种架构可能不太适合需要 SEO 优化的网站,因为搜索引擎爬虫通常不能很好地处理 SPA。

多页应用(MPA):


  1. 区别:多页应用是由多个页面组成的应用程序,每个页面都有自己的 URL。在浏览器中导航时,页面会重新加载。每个页面的内容和数据都是从服务器请求的。

  2. 优点:

  • 对搜索引擎优化(SEO)更友好,因为每个页面都有一个独立的 URL。
  • 适用于传统的网站架构,易于实现。
  • 页面加载速度较快,因为用户只加载当前访问页面的资源。
  1. 缺点:
  • 页面间的跳转和响应速度较慢,因为每次跳转都需要重新加载页面。
  • 前后端耦合度较高,需要处理多个页面的模板和资源。
  • 前端代码复用率较低,因为每个页面可能都需要编写独立的代码。

单页应用(SPA):


  1. 区别:单页应用是只有一个页面的应用程序,其中所有内容和数据都通过 AJAX 请求从服务器获取。页面不会重新加载,而是通过 JavaScript 动态更新内容。

  2. 优点:

  • 用户体验更好,页面间跳转和响应速度快,类似于原生应用。
  • 前后端分离,有利于团队协作和开发效率的提升。
  • 前端代码复用率高,可以使用组件和模块化的方式构建应用。
  1. 缺点:
  • 对搜索引擎优化(SEO)不友好,因为只有一个 URL,部分搜索引擎爬虫可能无法解析 JavaScript 生成的内容。
  • 首次加载时间较长,因为需要加载整个应用的资源和代码。
  • 对于大型应用,维护和管理可能会变得复杂。

多页应用程序示例:电子商务网站

电子商务网站是一个典型的多页应用程序,它通常有多个页面,如首页、产品列表、产品详情、购物车、结账和付款等。

每个页面都有自己的 URL,因此用户可以通过书签或链接直接访问页面。每个页面都是独立的 HTML 文件,包含自己的控制器和视图,因此可以单独加载和缓存。

但是,每次用户访问新页面时,需要重新加载整个页面,这可能会影响页面切换的速度。

单页应用程序示例:后台管理系统

现在多数后台管理系统是用Vue、React等框架开发的单页应用。

它的所有功能都包含在一个 HTML 文件中,包括 JavaScript、CSS 和 HTML。页面切换是通过 AJAX 和 JavaScript 动态地更新 DOM 实现的,不需要重新加载整个页面。

由于只需要加载一次资源,因此页面切换速度非常快。但是,由于所有内容都在一个页面中,因此首次加载可能需要一些时间。此外,由于没有单独的页面,因此不太适合需要 SEO 优化的网站。


根据项目需求和目标,可以选择使用多页应用还是单页应用。一般来说,对于需要优化搜索引擎排名的网站,可以选择多页应用;对于需要提供流畅的用户体验和快速响应的应用,可以选择单页应用。


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

烟雨蒙蒙的三月

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

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

你是否也会像我一样焦虑

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

要跳出当前的圈子吗

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

会有春天吗

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

这个文章应该有个结尾

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


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

探索Flutter包体优化

前言在产品的运营期间,我们发现Android用户对App容量大小的敏感度明显高于iOS用户。所以,为了提升产品在市场中竞争力,进一步提升产品的下载量,我们需要对基于Flutter开发的Android客户端进行包体优化。经过调研,我们发现Flutter经过多年的...
继续阅读 »

前言

在产品的运营期间,我们发现Android用户对App容量大小的敏感度明显高于iOS用户。

所以,为了提升产品在市场中竞争力,进一步提升产品的下载量,我们需要对基于Flutter开发的Android客户端进行包体优化。

经过调研,我们发现Flutter经过多年的发展后,DevTools已经完善,用较低的低成本即可满足需求。

下面我们会从分析开始,了解不同构成部分的容量分布情况。再对构成部分的特性来决定最终如何进行包体优化。

生成分析报告

首先,在build产物时,我们可以加上 --analyze-size 标识来生成分析报告。apk 和 appbundle 还可以指定产物的平台架构,如:--target-platform android-arm,分析实际机型的情况。

# 变异产物
# Specify one of android-arm, android-arm64, or android-x64 in the --target-platform flag.
flutter build apk --analyze-size --target-platform android-arm64
flutter build appbundle --analyze-size --target-platform android-arm64

apk 分析日志

app-release.apk (total compressed)                                         33 MB
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
assets/
dexopt 1 KB
flutter_assets 5 MB
classes.dex 7 MB
lib/
arm64-v8a 18 MB
Dart AOT symbols accounted decompressed size 8 MB
package:flutter 3 MB
package:image 799 KB
dart:core 325 KB
package:xxx_xxx_flutter 293 KB
dart:ui 279 KB
dart:typed_data 226 KB
dart:io 212 KB
dart:collection 177 KB
package:vector_graphics_compiler 170 KB
dart:async 154 KB
package:flutter_localizations 129 KB
package:flutter_spinkit 99 KB
package:extended_image 93 KB
dart:ffi 85 KB
package:petitparser 73 KB
dart:convert 62 KB
package:archive 62 KB
package:dio 57 KB
package:vector_math 54 KB
package:riverpod 49 KB
AndroidManifest.xml 3 KB
res/
-j.png 4 KB
1k.png 33 KB
1r.png 91 KB
2M.png 3 KB
33.9.png 2 KB
3m.png 1 KB
4C.png 1 KB
4F.png 10 KB
AQ.png 33 KB
Bj.png 29 KB
CG.png 23 KB
Ch.png 98 KB
D2.png 14 KB
E7.png 98 KB
EA.png 98 KB
ER.9.png 2 KB
FM.9.png 1 KB
Fo.png 29 KB
G1.png 2 KB
J6.9.png 2 KB
JO.png 29 KB
Mr.9.png 1 KB
NF.png 6 KB
Ni.png 33 KB
ON.png 91 KB
Pi.9.png 3 KB
Q11.9.png 3 KB
RX.png 16 KB
S9.png 2 KB
SD.png 5 KB
Tu.png 10 KB
Vq.png 1 KB
Vw.png 71 KB
WR.png 30 KB
XU.png 1 KB
Yj.png 4 KB
_C.png 3 KB
_f.png 2 KB
bI.png 3 KB
color-v23 2 KB
color 16 KB
dn.png 2 KB
e1.xml 1 KB
eB.9.png 2 KB
fM.png 5 KB
fR.png 67 KB
gV.9.png 1 KB
jy.png 8 KB
nN.png 4 KB
qt.png 2 KB
tj.9.png 2 KB
u3.png 3 KB
wi.9.png 2 KB
wi1.9.png 1 KB
wn.png 10 KB
x4.png 3 KB
yT.png 1 KB
zT.png 91 KB
resources.arsc 926 KB
client_analytics.proto 1 KB
dc/
a.gz 37 KB
kotlin/
collections 1 KB
kotlin.kotlin_builtins 5 KB
ranges 1 KB
reflect 1 KB
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
A summary of your APK analysis can be found at: /Users/xxx/.flutter-devtools/apk-code-size-analysis_01.json

To analyze your app size in Dart DevTools, run the following command:
dart devtools --appSizeBase=apk-code-size-analysis_01.json

appbundle 分析日志

app-release.aab (total compressed)                                         18 MB
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
BUNDLE-METADATA/
assets.dexopt 1 KB
com.android.tools.build.libraries 8 KB
com.android.tools.build.obfuscation 628 KB
base/
assets 5 MB
dex 3 MB
lib 8 MB
Dart AOT symbols accounted decompressed size 8 MB
package:flutter 3 MB
package:image 799 KB
dart:core 325 KB
package:xxx_xxx_flutter 293 KB
dart:ui 279 KB
dart:typed_data 226 KB
dart:io 212 KB
dart:collection 177 KB
package:vector_graphics_compiler 170 KB
dart:async 154 KB
package:flutter_localizations 129 KB
package:flutter_spinkit 99 KB
package:extended_image 93 KB
dart:ffi 85 KB
package:petitparser 73 KB
dart:convert 62 KB
package:archive 62 KB
package:dio 57 KB
package:vector_math 54 KB
package:riverpod 49 KB
manifest 4 KB
res 1 MB
resources.pb 292 KB
root 51 KB
META-INF/
UPLOAD.SF 46 KB
UPLOAD.RSA 1 KB
MANIFEST.MF 34 KB
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
A summary of your AAB analysis can be found at: /Users/xxx/.flutter-devtools/aab-code-size-analysis_01.json

To analyze your app size in Dart DevTools, run the following command:
dart devtools --appSizeBase=aab-code-size-analysis_01.json
✓ Built build/app/outputs/bundle/release/app-release.aab (18.2MB).

解读分析报告

使用DevTools解读报告

这里以 VS Code DevTools 举例(Install and run DevTools from VS Code

  • shift + cmd + p打开命令面板,再输入Open DevTools,打开面板。

  • 选择App Size Tooling,打开,并选择需要分析的文件,路径在生成报告时会提供(如:/Users/xxx/.flutter-devtools/aab-code-size-analysis_01.json)

202306271030501687833050QkTyxa.jpg

以上图为例,appbundle在未压缩的情况下, 体积为23.1MB,其中base占据了主要的容量。

base中占据主要大小的分别有lib (8MB)、assest (5MB)、dex (3MB)、res (1.2MB)。

分析内容物可以得出:

  • lib,Flutter端的代码实现以及依赖库。
  • assets,Flutter端使用到的资源文件。
  • dex,原生端的代码实现以及依赖库。
  • res,原生端使用到的资源文件。

除去这些主要的文件内容,剩余的多位配置文件,所占容量的比例也不大,可优化空间不大。

可探索的优化方向

第一步,通过优化素材可以直接减少包体内容的大小。

  • 降低视频素材的分辨率和质量

  • 多使用jpg、svg等高压缩率的图片素材

  • 使用IconFont替换icon的图片素材

  • 使用网络素材,替换本地素材,比如使用网络图片/视频,网络素材包等。

  • 使用GoogleFonts替代本地Fonts。

第二步,减少不必要的代码模块。

  • 检查 pubspec.yaml 并删除未使用的库/包。
  • 对第三方库进行精简定制,再引入使用,删除不需要的功能。
  • 使用Flutter提供的精简代码功能,对产物代码进行重定义。

下面介绍下Flutter提供的精简代码功能。

精简代码

在build产物时,添加 --obfuscate 和 --split-debug-info 来实现对代码的混淆,以及对代码的精简。

  • --split-debug-info ,提取调试信息,实现精简代码,可单独使用。
  • --obfuscate,开启代码混淆,提高代码反编译门槛。
# 指定android-arm64平台,在当前路径输出符号表(如`app.android-arm64.symbols`)
flutter build appbundle --target-platform android-arm64 --split-debug-info=.

未精简的日志

flutter build appbundle --target-platform android-arm64                           

Running Gradle task 'bundleRelease'...
Font asset "MaterialIcons-Regular.otf" was tree-shaken, reducing it from 1645184 to 3124 bytes (99.8% reduction). Tree-shaking can be disabled by providing the --no-tree-shake-icons flag when building your app.
Running Gradle task 'bundleRelease'... 26.7s
✓ Built build/app/outputs/bundle/release/app-release.aab (18.2MB).

精简后的日志

flutter build appbundle --target-platform android-arm64 --split-debug-info=. 

Running Gradle task 'bundleRelease'...
Font asset "MaterialIcons-Regular.otf" was tree-shaken, reducing it from 1645184 to 3124 bytes (99.8% reduction). Tree-shaking can be disabled by providing the --no-tree-shake-icons flag when building your app.
Running Gradle task 'bundleRelease'... 56.6s
✓ Built build/app/outputs/bundle/release/app-release.aab (17.6MB).

开启混淆的日志

flutter build appbundle --target-platform android-arm64 --split-debug-info=.  --obfuscate

Running Gradle task 'bundleRelease'...
Font asset "MaterialIcons-Regular.otf" was tree-shaken, reducing it from 1645184 to 3124 bytes (99.8% reduction). Tree-shaking can be disabled by providing the --no-tree-shake-icons flag when building your app.
Running Gradle task 'bundleRelease'... 57.5s
✓ Built build/app/outputs/bundle/release/app-release.aab (17.6MB).

读取混淆的堆栈跟踪

如需要调试被混淆的应用程序创建的堆栈跟踪,请遵循以下步骤将其解析为可读的内容:

  • 找到与应用程序匹配的符号文件。例如,在 Android arm64 设备崩溃时,需要 app.android-arm64.symbols文件。

  • 向 flutter symbolize 命令提供堆栈跟踪(存储在文件中)和符号文件。例如:

flutter symbolize -i <stack trace file> -d out/android/app.android-arm64.symbols

总结

在素材和代码模块的两个方向上进行包体优化,是比较容易操作,并且不会影响App稳定性的方案。

在出包的工作流中,我们会启用精简代码以及代码混淆,优化产物中代码所占的体积。

排查已经引入的依赖库,看是否存在未使用或者已经弃用的多余依赖。同时分析占容量较大的三方依赖,判断是否需要通过精简定制来降低容量。

最终是排查素材,筛选体积,对不达标的素材进行重新制作,或者寻找替代方案,比如改用网络资源。

参考

Obfuscating The Flutter App

Mastering Dart & Flutter DevTools — Part 3: App Size Tool

Reducing Flutter App Size

Using the app size tool


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

客户端日志&埋点&上报的线程安全问题

引子如果设计一个客户端埋点上报库,日志的完整性、高效传输、日志的及时性都是需要考量的点。其中“高效传输”除了采用更高效的序列化方案、压缩日志、还包含减少通信次数。若每产生一条日志就上报一次就浪费流量了。通常的做法是“批量上报”,即先将日志堆积在内存中,数量达到...
继续阅读 »

引子

如果设计一个客户端埋点上报库,日志的完整性、高效传输、日志的及时性都是需要考量的点。

其中“高效传输”除了采用更高效的序列化方案、压缩日志、还包含减少通信次数。若每产生一条日志就上报一次就浪费流量了。通常的做法是“批量上报”,即先将日志堆积在内存中,数量达到阈值时才触发一次上报。

批量上传 V1.0

假设埋点上报的实现如下:

object EasyLog {
var maxSize = 50
// 用于堆积日志的列表
private val logs = mutableListOf<Any>()
fun log(any: Any){
logs.add(any)
if(logs.size() >= maxSize) {
uploadLogs(logs)
logs.clear()
}
}
}

这样实现存在多线程安全问题,当log()被多线程并发访问时,共享变量logs并不是线程安全的。在多线程环境下调用 ArrayList.add() 会发生数据损坏,因为 ArrayList.add() 实现如下:

public boolean add(E e) {
ensureCapacityInternal(size + 1);
elementData[size++] = e;
return true;
}

其中的前两句都不是线程安全的。

第一句是扩容,重新申请内存并将原数组拷贝至新数组。如果两个线程发现容量不足同时进行扩容,因为拷贝数组过程不是原子的,若被打断,则已复制的内容可能会被覆盖。

第二句是索引自增,++ 操作也不是原子的,多线程环境下可能发生现有数据被覆盖。

使用@Synchronized注解可以解决该问题:

object EasyLog {
@Synchronized
fun log(any: Any){}
}

相当于为整段代码套上 synchronized,同一瞬间只有一个线程可以输出日志。

性能更好的做法是使用线程安全的容器,比如ConcurrentLinkedQueue,它使用无锁机制实现线程安全的并发读写,关于它源码级别的分析可以点击面试题 | 徒手写一个非阻塞线程安全队列

批量上传 V2.0 —— 调控日志生产消费速度

埋点上报场景中,日志的生产的速度远大于消费速度(上传是耗时操作)。这样用于堆积日志的容器可能无限增长,内存有爆炸的风险。

得使用一种机制调控日志生产和消费速度。比如,丢弃新/旧日志、暂停生产。

Kotlin 中的 Channel 提供了需要的所有功能,包括线程安全以及调控生产消费速度。

使用 Channel 重构如下:

object EasyLog {
// 容量为50的 Channel
private val channel = Channel<Any>(50)
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
private var maxSize = 50
private val logs = mutableListOf<Any>()
init {
//新起协程作为日志消费者
scope.launch { channel.consumeEach { innerLog(it) } }
}

fun log(any: Any){
// 新起协程作为日志生产者
scope.launch { channel.send(any) }
}

private fun innerLog(any: Any){
logs.add(any)// 堆积日志
if(logs.size() >= maxSize) {// 日志数量超阈值后上传
uploadLogs(logs)
logs.clear()
}
}
}

每一条新日志都会转发到 Channel 上,在 Channel 另一头有一个单独的协程消费日志。

Channel 就像一个队列,生产者队尾插入,消费者队头取出。它是一个线程安全的容器,多线程并发写没有问题,而且现在只有一个消费者,所以消费日志的代码不会有线程安全问题。就好比四面八方涌入的购票者只能在一个窗口前排队。将并行问题串行化是实现线程安全的一种方法。

Channel 的构造方法可传入三个参数:

public fun <E> Channel(
// 缓存大小
capacity: Int = RENDEZVOUS,
// 溢出策略
onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND,
// 如何处理未传递元素
onUndeliveredElement: ((E) -> Unit)? = null
): Channel<E>

缓冲大小表示生产速度大于消费速度时最多缓存的元素数。当缓存满后继续产生的元素会触发溢出策略,默认策略是BufferOverflow.SUSPEND,表示挂起生产者,而不是阻塞,生产者线程可以继续运行。这是 Channel 相较于 Java 阻塞队列的优势。

批量上传 V3.0 —— 延迟去小尾巴

若客户端产生了49条日志,应用被杀,那这49条日志就丢了,因为还未达到50条上传阈值。为了确保日志的完整性,不得不对每一条日志进行持久化。然后在下一次应用启动时从磁盘读取并上传之。

这样依然不能满足日志的及时性,比如该用户一周之后才启动应用。

需要一种机制及时处理日志的小尾巴(未达批量阈值的日志):当每一条日志到达时,开启倒计时,如果倒计时归零前无新日志请求,则将已堆积日志批量上传,否则关闭前一个倒计时,开启新的倒计时。

日志&埋点&上报(一)中使用 Handler 实现了这套机制。

这次换用协程实现:

object EasyLog {
// 容量为50的 Channel
private val channel = Channel<Any>(50)
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
private var maxSize = 50
private val logs = mutableListOf<Any>()
// 冲刷job
private var flushJob: Job? = null
init {
scope.launch { channel.consumeEach { innerLog(it) } }
}

fun log(any: Any){
scope.launch { channel.send(any) }
}

private fun innerLog(any: Any){
logs.add(log)
flushJob?.cancel() // 取消上一次倒计时
// 若日志数量达到阈值,则直接冲刷,否则延迟冲刷
if (logs.size() >= maxSize) {
flush()
} else {
flushJob = delayFlush()
}
}

// 冲刷:上传内存中堆积的批量日志
private fun flush() {
uploadLogs(logs)
logs.clear()
}

// 延迟冲刷
private fun delayFlush() = scope.launch {
delay(5000)// 延迟5秒,如果没有新日志产生,则冲刷
flush()
}
}

批量上报 V4.0 —— 并行问题串行化

上述代码存在多线程安全问题。

因为延迟冲刷时新起一个协程,导致 logs.clear() 和 logs.add() 可能并发执行。

为了复现这个多线程问题,写了一个压测demo:

// 线程安全的自增 Int 值
val count = AtomicInteger(0)
// 用于验证多线程问题的 Int 数组,初始值都是 -1
private val testArray = Array(100) { -1 }
val list = mutableListOf<Int>()
var flushJob: Job? = null

// 模拟并发生产日志
repeat(100) {
scope.launch(Dispatchers.Default) {
delay((50L..10_00L).random())
channel.send(count.getAndIncrement())
}
}

// 模拟串行消费日志
scope.launch {
channel.consumeEach {
delay((50L..200L).random())
log(it)
}
}
//模拟日志库入口方法
private fun log(value: Int) {
list.add(value)
flushJob?.cancel()
if (list.size >= 5) {// 5条为一批
flush()
} else {
flushJob = delayFlush()
}
}

// 延迟冲刷
fun delayFlush() = scope.launch {
delay(100)
flush()
}

// 冲刷
private fun flush() {
// 以批量日志的值赋值测试数组
list.forEach { testArray[it.value] = it.value }
list.clear()
}

检测方法是并发输出100条日志(内容为一个自增序号0-99),同时声明一个大小为100的数组,所有元素初始化为-1,每当日志被批量处理时将序号对应的数组索引值置为序号值。所有日志处理完毕后,检测数组中是否有-1,若有则表示复现多线程问题,检测代码如下:

val isOk = testArray.any { it == -1 }

若数组中所有的值都不是 -1,则表示 0-100 的日志都被正确地处理了。

运行 demo,isOk 是 false。。。。继续运行了 n 次,居然全都是 false。。。。开始怀疑我对多线程的认知,然后就问 ChatGPT:“理论上这段代码会发生线程安全问题,但是我的测试代码没有复现这个问题,如何才能复现?”

这段代码中的线程安全问题可能不容易复现,因为涉及到多个线程和协程同时运行,而且延迟时间是随机的。要复现这个问题,可以尝试以下方法:

  1. 增加重复次数:增加 repeat(100) 中的数字,这将增加线程安全问题发生的可能性。
  2. 减小延迟时间范围:将 delay((50L..10_00L).random()) 和 delay((50L..200L).random()) 中的延迟时间范围减小,以便让线程和协程更密集地运行。

尽管这些方法可以增加线程安全问题发生的可能性,但并不能保证一定能复现问题。线程安全问题的复现通常取决于操作系统、硬件和运行时环境等多种因素。

听了他的建议调整了 delay 的时间间隔,并增加次数到1000次,多线程问题浮现了:

java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.next(ArrayList.java:860)
at test.taylor.com.taylorcode.concurrent.ConcurrentInitActivity.flush(ConcurrentInitActivity.kt:134)
at test.taylor.com.taylorcode.concurrent.ConcurrentInitActivity.log(ConcurrentInitActivity.kt:66)

在遍历列表的同时增删列表元素就会发生这个问题。即使没有发生上述 crash,也可能会打印出 isOk=true,表示array中有元素为-1。

使用线程安全的ConcurrentLinkedQueue作为容器,仍然无法解决问题,因为它的clear()方法是弱一致性的,它需要遍历整个队列,但遍历是基于快照机制的。

最后还是运用 “将并行问题串行化” 来解决这个多线程问题:

// 单线程 Dispatcher
val logDispatcher = Executors.newFixedThreadPool(1).asCoroutineDispatcher()

// 在单线程上延迟冲刷
fun delayFlush() = scope.launch(logDispatcher) {
delay(50)
flush()
}

// 在单线程上消费日志
scope.launch(logDispatcher) {
channel.consumeEach {
delay((25L..100L).random())
log(it)
}
}

构建一个单独的线程,使得日志的消费和冲刷都在该线程进行。

单线程会降低性能吗? 不会,因为延迟冲刷是挂起剩余的代码,而不会阻塞线程。在单线程上延迟冲刷就好比使用 Handler.postDelay() 将冲刷逻辑排到主线程消息队列的末尾。


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

熟读代码简洁之道,为什么我还是选择屎山

前言前几天我写了一篇Vue2屎山代码汇总,收到了很多人的关注;这说明代码简洁这仍然是一个程序员的基本素养,大家也都对屎山代码非常关注;但是关注归关注,执行起来却非常困难;我明明知道这段代码的最佳实践,但是我就是不那样写,因为我有很多难言之隐;没有严格的卡口没有...
继续阅读 »

前言

前几天我写了一篇Vue2屎山代码汇总,收到了很多人的关注;这说明代码简洁这仍然是一个程序员的基本素养,大家也都对屎山代码非常关注;但是关注归关注,执行起来却非常困难;我明明知道这段代码的最佳实践,但是我就是不那样写,因为我有很多难言之隐;

没有严格的卡口

没有约束就没有行动,比方说eslint,eslint只能减少很少一部分屎山,而且如果不在打包机器上配置eslint的话那么eslint都可以被绕过;对我个人而言,实现一个需求,当然是写屎山代码要来的快一些,我写屎山代码能够6点准时下班,要是写最佳实践可能就要7点甚至8点下班了,没有人愿意为了代码整洁度而晚一点下班的。

没有CodeReview,CodeReview如果不通过会被打回重新修改,直到代码符合规范才能提交到git。CodeReview是一个很好地解决团队屎山代码的工具,只可惜它只是一个理想。因为实际情况是根本不可能有时间去做CodeReview,连基本需求都做不完,如果去跟老板申请一部分时间来做CodeReview,老板很有可能会对你进行灵魂三连问:你做为什么要做CodeReivew?CodeReview的价值是什么?有没有量化的指标?对于屎山代码的优化,对于开发体验、开发效率、维护成本方面,这些指标都非常难以衡量,它们对于业务没有直接的价值,只能间接地提高业务的开发效率,提高业务的稳定性,所以老板只注重结果,只需要你去实现这个需求,至于说代码怎么样他并不关心;

没有代码规约

大厂一般都有代码规约,比如:2021最新阿里代码规范(前端篇)百度代码规范

但是在小公司,一般都没有代码规范,也就是说代码都无章可循;这种环境助长了屎山代码的增加,到头来屎山堆得非常高了,之后再想去通过重构来优化这些屎山代码,这就非常费力了;所以要想优化屎山代码光靠个人自觉,光靠多读点书那是没有用的,也执行不下去,必须在团队内形成一个规范约定,制定规约宜早不宜迟

没有思考的时间

另外一个造成屎山代码的原因就是没时间;产品经理让我半天完成一个需求,老大说某个需求很紧急,要我两天内上线;在这种极限压缩时间的需求里面,确实没有时间去思考代码怎么写,能cv尽量cv;但是一旦养成习惯,即使后面有时间也不会去动脑思考了;我个人的建议是不要总是cv,还是要留一些时间去思考代码怎么写,至少在接到需求到写代码之前哪怕留个5分钟去思考,也胜过一看到需求差不多就直接cv;

框架约束太少

越是自由度高的框架越是容易写出屎山代码,因为很多东西不约束的话,代码就会不按照既定规则去写了;比如下面这个例子: stackblitz.com/edit/vue-4a…

这个例子中父组件调用子组件,子组件又调用父组件,完全畅通无阻,完全可以不遵守单向数据流,这样的话为了省掉一部分父子组件通信的逻辑,就直接调用父组件或者子组件,当时为了完成需求我这么做了,事后我就后悔了,极易引起bug,比如说下一次这个需求要改到这一部分逻辑,我忘记了当初这个方法还被父组件调用,直接修改了它,于是就引发线上事故;最后自己绩效不好看,但是全是因为自己当初将父子组件之间耦合太深了;

自己需要明白一件事情那就是框架自由度越高,越需要注意每个api调用的方式,不能随便滥用;框架自由不自由这个我无法改变,我只能改变自己的习惯,那就是用每一个api之前思考一下这会给未来的维护带来什么困难;

没有代码质量管理平台

没有代码质量管理平台,你说我写的屎山,我还不承认,你说我代码写的不好,逻辑不清晰,我反问你有没有数据支撑

但是当代码质量成为上线前的一个关键指标时,每个人都不敢懈怠;常见的代码质量管理平台有SonarQubeDeepScan,这些工具能够继承到CI中,成为部署的一个关键环节,为代码质量保驾护航;代码的质量成为了一个量化指标,这样的话每个人的代码质量都清晰可见

最后

其实看到屎山代码,每一个人都应该感到庆幸,这说明有很多事情要做了,有很多基建可以开展起来;推动团队制定代码规约、开发eslint插件检查代码、为框架提供API约束或者部署一个代码质量管理平台,这一顿操作起来绩效想差都差不了;


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

感觉要学的东西真的好多,时长迷茫、焦虑,你有这种感觉吗

我的感受我有过,就拿前端来说,这些年前端飞速发展,各种新知识层出不穷,能学的东西真的好多好多,时不时就会感慨:“吾生也有涯,而知也无涯。 以有涯随无涯,殆已。”越学越累,没头。更累的是你费半天劲学完的东西不久就忘了,记了笔记也没用,总有种白学了的感觉。与同事对...
继续阅读 »

我的感受

我有过,就拿前端来说,这些年前端飞速发展,各种新知识层出不穷,能学的东西真的好多好多,时不时就会感慨:“吾生也有涯,而知也无涯。 以有涯随无涯,殆已。”越学越累,没头。

更累的是你费半天劲学完的东西不久就忘了,记了笔记也没用,总有种白学了的感觉。

与同事对话

当我把这种感觉跟同事说的时候,他说:“我真的很佩服你的毅力,我做不到你那样,我只学用的到的东西,其余时间更喜欢玩玩游戏、陪陪家人。”

当我还在回味他说的这句话时,他又来了一句,狠狠地刺痛了我,他以开玩笑的语气说:“你学那么多,也没见你工资比我高哇,你。。。你好像比我低吧。。。”。我。。。好心痛。。。事实确实如此。

为何有学习焦虑

步入社会后我们的学习还是要更多的坚持学以致用,说白了,学就是为了用,而不是为了自我感动。

其实,很多人并不喜欢学习,他们之所以拼命学,是为了缓解焦虑,对抗危机,但如果你学的东西给你创造了不了任何价值,只能让你沉浸在不停地学学学,忘忘忘的痛苦中,这样只能白白蹉跎岁月。

怎么缓解

这个时候或许你可以停下来,仔细想想自己当前学的是不是能尽快用到实践中,给自己带来价值。

我感觉,你学习焦虑的原因并不是因为你掌握的知识不够,而是你掌握的知识并没有给你带来任何价值,说白了就是它无法给你带来钱!

你焦虑的是你没钱,怎么有钱?给别人创造价值呀;怎么创造价值?给别人解决问题呀;怎么解决问题?把你学的东西赶紧用出来哇。用呀,赶紧用。用不了?用不了你学它干啥?赶紧去学能用的去呀!

可能有些人会认为这么说有点太功利了,这就仁者见仁智者见智吧。


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

程序员更需要钝感力

程序员更需要钝感力,别让自己太疲惫程序员每个人都是聪明的,都是反应灵敏的。 一个同事说过这么一句话:"都当的了程序员,大家都是聪明人,不过有些事不好意思点破罢了,他要是真的太过分,就真的撕破脸!"。讲一个大多数同学都会遇到的事,之前在一家单位随着部门发展,空降...
继续阅读 »

程序员更需要钝感力,别让自己太疲惫

程序员每个人都是聪明的,都是反应灵敏的。 一个同事说过这么一句话:"都当的了程序员,大家都是聪明人,不过有些事不好意思点破罢了,他要是真的太过分,就真的撕破脸!"。

讲一个大多数同学都会遇到的事,之前在一家单位随着部门发展,空降了个老大哥(前端领导)。怎么说呢那个时候自己陷入了一个极其痛苦的时期,他呢每天都很闲不干活,活呢都交给我跟另外一个哥们干,而且功劳还是他的。我很不爽,当我跟我对象说这个事的时候,她说谁让人家是领导呢!!现在再总结的时候发现好像都释怀了,那个时候每天晚上失眠睡不着。

那个时候痛苦的点是:

  1. 身为领导不作为。
  2. 身为领导技术能力不行。
  3. 把我的产出都当作自己的汇报给领导。

其实那个时候或多或少会跟他对着干,我对象也跟我聊过这个事说不要跟领导对着干,人家是领导不干活就不干活了。好像确实没落到什么好处,最后还是人家啥活不干,痛苦的还是自己。还是要学会向上管理。 

都是打工的,工期是按照人日排的,就是换个领导,怎么着你都要干活的啊!谁让你不是领导呢。 

到现在听到有吐槽领导会不干活、技术能力不行、功劳是自己的,锅都是我的一些吐槽。我都会笑笑不说什么,其实反过来想领导的位置不允许犯错,一般当领导的年龄都很大了(可能不是那么容易找到那么符合预期的工作了),假如再犯一些低级的错误,会被领导的领导不信任等。你如果能跟领导相处很好,就是犯错了锅是你的,最后你得绩效还是好的。

领导积极管理组织分享、一块搞些新奇的东西也好,不作为也好,干好自己的工作积极响应,自己擅长的领域就好好发挥,管他结果是啥呢,大家都聪明人,都看得明白的。有时间了就学点新东西,都是打工的,没必要搞得都心里不舒服,凡事多多人情世故下 哈哈 真不爽了离开就是了。

钝感力这个词其实跟我们常说的看开点、别往心里去、别想太多、不要太敏感等等有异曲同工之妙。

我们都需要顿感力,凡事别较真,在《高敏感是种天赋》,作者伊尔斯·桑德把高敏感比作是种天赋,是种天生具有责任感的人(任何事情都是会提前规划,反复练习不允许自己失误,努力会把事情做好),容易产生共鸣的人(不至于在工作中需要浪费太多时间沟通事情怎么做,我稍微一说你就能get到)等。

顿感力

渡边淳一再顿感力书中把顿感力解释为“迟钝的力量”,既从容面对生活中的挫折和伤痛。作者认为钝感是一种才能,一种能让人们的才华开花结果、发扬广大的力量。迟钝虽然给人一种木讷的负面印象,但钝感力确实我们赢得美好生活的手段和智慧

作者从以下几点讲了顿感力的优势:

1、从蚊子叮皮肤 展示钝感力的皮肤优势。

2、从同事被领导严格批评到我们都心疼他 到他第二天没有一点事 “白担心了”。

3、从文艺写作沙龙“石之会”结识优秀青年作家在第一次给主编投稿受打击就一蹶不振 最后销声匿迹 。

4、从“沾沾自喜”“得意忘形”到“再接再厉”。

5、从视觉过好,导致眼睛过累。

6、从听觉过好产生幻听。

7、从嗅觉过好到食物的怪味。

8、从味觉的过好到吃不到常人认为很好吃的辣度咸度。

9、从触觉的灵敏到身体感受到剧烈的疼痛。

10、从身体的关节疼痛能预知天气状况。

11、从“善睡的能人”到获得美丽 健康长寿。

一千个读者就有一千个哈姆雷特,每个人都会有不同的观点和感受, 也有一些书评如下:

读者一:

认为“钝感力”并不是什么新鲜玩意,中华五千年文化博大精深。用汉语来形容就是,一曰大智若愚,二曰难得糊涂。

大智若愚者,拥有大智慧,看起来却很笨拙,表面上给人一种木纳、不善言辞、反应迟钝的感觉,似乎不够机灵,实则心中有数,把握有度,巧藏于拙,遇事不急不躁,内心清楚明白,甚至洞若观火。

难得糊涂者,不是自我欺骗,不是装腔作势,而是表面上嘻嘻哈哈,看似糊涂,实则看透世事,看透人生,落得个与世无争,悠闲自得。

读者二:
我觉得挺好的。有了钝感力,生活会幸福一些。但是我觉得真正的钝感力,来自于自信,而自信来自于认知能力。如果我们的认知达到一定高度,钝感力自然形成。

读者三:
人生遇到的烦事50%可用四个字解决:关我屁事!
那么剩下的50%,亦可以用四个字解决:关你屁事!

如何培养顿感力呢?

1、迅速忘却不快之事;
2、认定目标,即使失败仍要继续挑战;
3、坦然面对流言蜚语;
4、对嫉妒与嘲讽心怀感激之心;
5、面对表扬,甘之如饴,但不得寸进尺,不得意忘形

敏感度检测

都读到这里了,可以做个敏感度检测摘录至《高敏感是种天赋》,作者伊尔斯·桑德书中。

主要用于高度敏感群体。每个问题有五个选项,每个数字代表问题的描述与自己的符合情况,0-4分别是:

  • 0=完全不符合
  • 1=有一点儿符合
  • 2=基本符合
  • 3=较为符合
  • 4=非常符合
  1. 美妙的音乐会让我非常激动。()
  2. 我总是花比别人更多的精力去预测未来可能出现的问题,并做好充分的准备。()
  3. 我很擅长发现新的可能和选择。()
  4. 我很容易兴奋,总是有很多主意。()
  5. 我知道生活不止我们看见和听见的一切。
  6. 我的疼痛阈限很低。()
  7. 我常常觉得对别人来说很容易的事情,对我来说却太沉重。()
  8. 每天我需要一点时间独处。()
  9. 如果我跟别人连续相处两到三个小时,中间几乎没有休息,这会让我非常疲惫。()
  10. 预感到冲突即将出现,我会提前逃跑。()
  11. 面对愤怒,即使不是针对我,也会让我倍感压力。()
  12. 别人的痛苦会深深地影响到我。()
  13. 每件事我总是竭尽全力,以避免不愉快的事情或者失误的发生。()
  14. 我富有创造力。()
  15. 艺术性的工作有时会给我带来深深的快乐。()
  16. 面对多重任务,我的阈限比别人要低。比如,我很难做到一边上网,一边与人交谈。()
  17. 我不喜欢待在刺激过多的地方,比如游乐场、大型超市、运动会。
  18. 在电视上看到的暴力图片会影响我很久。□
  19. 我会花比别人更多的时间来思考。□
  20. 我很擅长感知动物和植物的生长状态。□
  21. 身处美丽的自然环境中时,我整个身体里都洋溢着幸福。□
  22. 我触须灵敏,能轻易感知到他人的心理状态。□
  23. 我很容易感到愧疚。□
  24. 我工作的时候,如果有人看我,我会很有压力。□
  25. 我有一双敏锐的眼睛,能一眼看透别人的心理活动。□
  26. 我很容易受到惊吓。□
  27. 我能给别人提供倾情陪伴和有意义的友谊。□
  28. 那些似乎不会打扰别人的声音却给我带来很大的困扰。□
  29. 我非常直观。□
  30. 我很享受独自一人的感觉。□
  31. 多数时间我都是一个明智的决断者,但有时也会是冲动型,追求速度。□
  32. 喧闹的声音,刺激的味道和强烈的光线都会影响到我。□
  33. 我对在安静平和的环境中休息的需求比别人更大。□
  34. 我很难从饥饿和寒冷中转移注意力。□
  35. 我很容易哭泣。□
  • 1-35题总分合计()
  1. 我喜欢毫无准备地体验新事物。□
  2. 当我在某些方面比别人更聪明,我会感觉很棒。□
  3. 社交不会让我疲惫。如果气氛足够好,我可以一直在活动现场待下去,甚至不用独处休息。□
  4. 我喜欢野外生存一类的夏令营。□
  5. 我喜欢在压力下工作。□
  6. 如果别人不舒服,我倾向于认为这是他们自己的错。□
  7. 我总是充满能量,我的心情很少受到周围的事情的影响。□
  8. 我常常是最后一个离开派对的人。□
  9. 我习惯船到桥头自然直,很少担心。
  10. 我喜欢跟朋友一起在度假小屋度过周末,并不需要独自待着。□
  11. 朋友出其不意地拜访我,我会非常惊喜。□
  12. 我能应对睡眠很少的状况。□
  13. 我喜欢放鞭炮。□
  • 36-48题总分合计()

第一组题目包括1-35。将你的答案加起来求和,如果所有问题你都选1,那么合计应该是35。

第二组题目包括36-48。将你的答案加起来求和,如果所有问题你都选2,那么合计应该是26。

接着,用第一组的总分,减去第二组的总分,以上述例子为例,答案应该是9。

最后的分数则是你的敏感分数,应该是介于52-140之间的某个值。得分越高,敏感程度越高。如果你的分数超过了60,那么你可能就是一个高度敏感型的人。

对测试结果进行解释常常需要谨慎。当我们用该测试结果描述一个人时,它肯定不是非常全面的。还有许多方面未被纳入考虑。并且测试结果还会受到你测试当天的心情的影响。你可以将该测试视为一个大概的参考,而不必过分去强调它。

结语

高敏感、迟钝也罢,怎么都是自己,试着跟自己和解,适当时候不要对自己要求太高,先暂时做好眼前事。 阅己、越己、悦己、乐己八个字送给各种程序员,工作只是生活一部分而已。尤其是在现在行情不好的时候,在工作的时候做好自己的本分工作,失业的也不要想太多,就当给自己放了个假期,后面有你大展拳脚的时候。加油哦


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

人走茶凉?勾心斗角?职场无友谊?

你和同事之间存在竞争关系要不要把工作关系维护成伙伴关系明枪暗箭防不胜防背后捅刀子往往最不设防大家是否在职场上交友是有也遇到过以上困扰呢?不要在职场上交“朋友”,而是要寻找“盟友”。这两者的区别在于应对策略:我们会愿意为“朋友”牺牲自己的利益,像是一张年卡。而结...
继续阅读 »

你和同事之间存在竞争关系

要不要把工作关系维护成伙伴关系

明枪暗箭防不胜防

背后捅刀子往往最不设防

大家是否在职场上交友是有也遇到过以上困扰呢?

不要在职场上交“朋友”,而是要寻找“盟友”。

这两者的区别在于应对策略:

我们会愿意为“朋友”牺牲自己的利益,像是一张年卡。

而结交“盟友”就是为了一起争取更多利益,《孔乙己》说得好:“这次是现钱,酒要好。”

所以,在职场上的“受欢迎”和社交场、朋友圈上的“受欢迎”之间有着本质的区别:

你和你的同事未必真心喜欢彼此,但在日常相处当中能够客气、友善地交往。

大家需要寻找盟友时会第一个想到你,在争斗冲突时会尽量绕开你,这就是一种非常理想的“受欢迎”状态。 不要在职场上寻求友谊和爱,这件事是不对的。

在这里给大家列出一个在职场上受欢迎的清单。

1.实力在及格线以上

这是一切的前提。职场新人要“先活下来,再做兄弟”,稳住了工作能力这个基本面,才有资格和同事谈交情。

实力不够的人会拖累整个团队、增加所有人的工作量,大家恨都来不及,绝对不会和他称兄道弟。

实力强可以表现为实力本身,在初级职位上,也可以表现为潜力。

极少数特别强大的人可能从一开始就能很好地完成工作,但是大部分人在新加入一个团队时都需要经过一段时间的磨合,在这个过程中有欠缺和不足都是正常的,你所表现出来的敬业精神、学习能力和进步的速度才是大家对你进行评价的关键。

刚入职的新人,对于要做的事情完全没有概念,但是为人极勤奋又上进,给他布置的任务会完成得特别扎实,每一天都在飞快地进步。这样的人在职场上永远都能收获一大把来自他人的橄榄枝。

2.比较高的自尊水平

高自尊的人对自己评价高,要求也高,又能够带着欣赏的眼光去看周围的人,他们不光是很好的父母、伴侣和朋友,同时也是职场上最好的结盟对象。

高自尊的人往往拥有很多优秀的品质,同时他们也能够理解“大局”,和他们合作不用在鸡毛蒜皮的细节上纠缠推诿,可以把精力全部用来开疆拓土,极大地降低团队的内耗。

如果你是一个高自尊的人,在日常生活中表现出了自律和很好的品行,就会收获高自尊同类的赞赏。有些低自尊的人可能会认为你的言行是在“装X”,别犹豫,把他们从你的结交名单当中划掉,高自尊会帮你筛掉一批最糟糕的潜在合作者。

如果你是一个部门的领导者,记得要维护高自尊的下属,他们都是潜在的优秀带队者,给他们一个位子就可以坐上来自己动,给他们一点精神鼓励和支持,他们就会变得无所不能。

即使高自尊的手下可能某些地方让你感到嫉妒或者冒犯(这是常见的,嫉妒是每个人都一定会有的情感),也绝对不要默许或者纵容低自尊的妄人跑去伤害他们,否则会伤了大家的心,事业就难以成功了。

“朕可以敲打丞相,但你算什么东西”就是对这种低自尊妄人最好的态度。

3.嘴严,可靠

在任何一个群体当中,多嘴多舌的人都不会受到尊重,而在职场上,嘴不严尤其危险。

如果你是一个爱说是非的人,围绕在你周围的只会是一帮同样没正事、低级趣味的家伙。你会被打上“不可靠”的标记,愿意和你交流的人越来越少,大家等着看你什么时候因为多嘴闯祸,而强者根本不会和你为伍。

有些同学曾经给我留言说,自己很内向,不知道如何跟同事拉近关系。内向的人最适合强调自己的“嘴严”和“可靠”,在职场上,这两项品质远比“能说会道”更让人喜欢。

4.随和,有分寸

体面的人不传闲话,也不会轻易对旁人发表议论。

“思想可以特立独行,生活方式最好随大流”,这是对自己的要求,而他人的生活方式是不是合理,不是我们能评价的。

哪怕是最亲近的人,都未必能知晓对方的全部经历和心里藏着的每一件小事。在职场上大家保持着客气有礼的距离,就更不可能了解每个人做事的出发点和逻辑,“看不懂”是正常的,但是完全没有必要“看不惯”。如果还要大发议论,把自己的“看不惯”到处传播,你的伙伴就只会越来越少。

有人说在北上广深这样的大城市,人和人之间距离遥远,缺人情味,太冷漠。

这不是冷漠,而是对“和自己不一样”的宽容,这份宽容就是我们在向文明社会靠拢的标志。

5.懂得如何打扮

还记得斯大林的故事吗?在他离开校园之后,从头到脚都经过精心设计,不是为了精神好看,而是要让自己看起来就像一位投身革命事业的进步青年。

有句老话叫做“先敬罗衣后敬人”,本意是讽刺那些根据衣饰打扮来评价一个人的现象。我们自己在做判断的时候要尽量避免受到这类偏见的影响,但是对他人可能存在的偏见一定要心中有数。人是视觉动物,穿着打扮是“人设(人物设定)”的一部分,在我们开口说话之前,衣饰鞋袜就已经传达了无数信息。

想要成为职场当中受欢迎的人,穿着打扮的风格就要和公司的调性保持一致,最安全的做法是向你的同事靠拢。

在一个风格统一的群体当中,“与众不同”这件事自带攻击性。如果在事业单位之类的上年纪同事比较多的地方上班,马卡龙色的衣服和颜色夸张的口红,最好等到下班时间再上身。

这不是压抑天性,而是自我保护和职业精神。

6.和优秀的人站在一起

在职场上,优秀的人品质都是相似的:勤奋,自律,不断精进。如果发现了这样的同事,就要尽量和他们保持良好关系。

但是,单纯的日常沟通并不足以让你们成为盟友,正式结盟往往是通过利益交换和分享:当你遇到棘手的工作任务,就可以主动邀请对方共同跟进,同时将一部分利益让出去。愉快的合作是关系飞跃的最好契机。

优秀的人能认可的,通常也都是自己的同类。如果你能获得他们的称许和背书,在同事当中的地位自然会有所提升。

7.知道如何求助

前两天有一位关系户同学留言说,自己即将去实习,因为家人的关系可以得到一些行业资深专家的指点,问自己应该如何表现,是不是不懂就要问,像“好奇宝宝”一样,对方就会觉得自己好学上进。

我告诉她说,不要上去就问,有任何疑惑都先用搜索引擎找一下答案,如果找不出来,再带着你搜到的细节去询问那些资深前辈。

互联网时代有个很大的变化,就是人们获取信息的成本大大降低。善用搜索引擎寻找答案,就能更快、更精准、更全面地找到自己想要的东西,这种方式比跑到对方工位边用嘴问效率高得多。

凡事都问,只会让人觉得你的文字阅读能力有限,同时既不把自己的时间当回事,也不尊重别人的时间。尤其对方还是行业中的专家,他们的时间一定比实习生的宝贵多了。如果网上找不到答案,再带着细节去仔细咨询,这样的请教才是高效的,才能证明你是一个“好学上进”的人。

职场不是校园,不会再有一群老师专门负责手把手地教你,不轻易占用其他同事的时间会让你成为一个自立、有分寸、受尊重的人。毕业之后,你取得进步的速度、最终的上升空间,都和使用搜索引擎寻找答案的能力呈正相关。

8.技巧地送出小恩小惠

小恩小惠带两个“小”字,并不意味着这是一种微末小技。事实上,即使是最普通的零食,只要讲究得法,都可以送到人心里。

你的同事当中有没有因为宗教信仰而忌口的情况?

甲和乙爱吃辣,丙和丁爱吃甜,是不是两种口味都来上一点?

要留心同事的自我暴露,最好是用一个小本本记下来,关键时刻可能派上大用场。大家都是成年人,不会像孩子一样轻易被小恩小惠打动,打动我们的往往是“你把我放在心上”的温暖。

9.良好的情绪管理能力

很多时候这是个隐藏特征,但是自带“一票否决”属性:平时表现得沉着稳重,周围同事们不会有特别明显的感觉,然而歇斯底里和失控只要有一次,之前苦心经营的人设就会全面崩塌。情绪不稳定的人一般没人敢惹,但是也没人会在意了:你会被视为一个“病人”,很难再有大的发展。

已经发泄出去的情绪不能收回来,这个时候不要反复陷入纠结和悔恨,待在情绪里不出来,钱花出去了就不要去想,不要去比价。

如果情绪失控了,应该立刻做到的是原谅自己,然后考虑如何不再有下一次失控。要知道大多数人一辈子都至少会换三四次工作,了不起是换个地方,重新再来。

有的人特别幸运,天生长得好看,容易被人喜欢。

如果不是让人眼前一亮的高颜值人士,就不要太心急了。

成为一个自律、行为可以预期的人,也能慢慢地被别人喜欢。

人生很长,被人喜欢这件事,我们不用赶时间。


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

Vite 开发环境为何这么快?

web
本文只是笔者作为一个初学者,在学习中与看了诸多业界的优秀实践文章之后的思考和沉淀,如果你在看的过程中觉得有些不妥的地方,可以随时和我联系,一起探讨学习。 提到 Vite,第一个想到的字就是 快,到底快在哪里呢?为什么可以这么快? 本文从以下几个地方来讲 快...
继续阅读 »

本文只是笔者作为一个初学者,在学习中与看了诸多业界的优秀实践文章之后的思考和沉淀,如果你在看的过程中觉得有些不妥的地方,可以随时和我联系,一起探讨学习。



提到 Vite,第一个想到的字就是 ,到底快在哪里呢?为什么可以这么快?
本文从以下几个地方来讲



  • 快速的冷启动: No Bundle + esbuild 预构建

  • 模块热更新:利用浏览器缓存策略

  • 按需加载:利用浏览器 ESM 支持


Vite 本质上是一个本地资源服务器,还有一套构建指令组成。



  • 本地资源服务器,基于 ESM 提供很多内建功能,HMR 速度很快

  • 使用 Rollup 打包你的代码,预配件了优化的过配置,输出高度优化的静态资源


快递的冷启动


No-bundle


在冷启动开发者服务器时,基于 Webpack 这类 bundle based 打包工具,启动时必须要通过 依赖收集、模块解析、生成 chunk、生成模块依赖关系图,最后构建整个应用输出产物,才能提供服务。


这意味着不管代码实际是否用到,都是需要被扫描和解析。


image.png


而 Vite 的思路是,利用浏览器原生支持 ESM 的原理,让浏览器来负责打包程序的工作。而 Vite 只需要在浏览器请求源码时进行转换并按需提供源码即可。


这种方式就像我们编写 ES5 代码一样,不需要经过构建工具打包成产物再给浏览器解析,浏览器自己就能够解析。


image.png
与现有的打包构建工具 Webpack 等不同,Vite 的开发服务器启动过程仅包括加载配置和中间件,然后立即启动服务器,整个服务启动流程就此结束。


Vite 利用了现代浏览器支持的 ESM 特性,在开发阶段实现了 no-bundle 模式,不生成所有可能用到的产物,而是在遇到 import 语句时发起资源文件请求。


当 Vite 服务器接收到请求时,才对资源进行实时编译并将其转换为 ESM,然后返回给浏览器,从而实现按需加载项目资源。而现有的打包构建工具在启动服务器时需要进行项目代码扫描、依赖收集、模块解析、生成 chunk 等操作,最后才启动服务器并输出生成的打包产物。


正是因为 Vite 采用了 no-bundle 的开发模式,使用 Vite 的项目不会随着项目迭代变得庞大和复杂而导致启动速度变慢,始终能实现毫秒级的启动。


esbuild 预构建


当然这里的毫秒级是有前提的,需要是非首次构建,并且没有安装新的依赖,项目代码中也没有引入新的依赖。


这是因为 Vite 的 Dev 环境会进行预构建优化。
在第一次运行项目之后,直接启动服务,大大提高冷启动速度,只要没有依赖发生变化就会直接出发热更新,速度也能够达到毫秒级。


这里进行预构建主要是因为 Vite 是基于浏览器原生**支持 **ESM 的能力实现的,但要求用户的代码模块必须是ESM模块,因此必须将 commonJSUMD 规范的文件提前处理,转化成 ESM 模块并缓存入 node_modules/.vite


在转换 commonJS 依赖时,Vite 会进行智能导入分析,即使模块导出时动态分配的,具名导出也能正常工作。


// 符合预期
import React, { useState } from 'react'

另一方面是为了性能优化


为了提高后续页面加载的性能,Vite 将那些具有许多内部模块的 ESM 依赖转为单个模块。


比如我们常用的 lodash 工具库,里面有很多包通过单独的文件相互导入,而 lodash-es这种 ESM 包会有几百个子模块,当代码中出现 import { debounce } from 'lodash-es'发出几百个 HTTP 请求,这些请求会造成网络堵塞,影响页面的加载。


通过将 lodash-es 预构建成一个单独模块,只需要一个 HTTP 请求。


那么如果是首次构建呢?Vite 还能这么快吗?


在首次运行项目时,Vite 会对代码进行扫描,对使用到的依赖进行预构建,但是如果使用 rollup、webpack 进行构建同样会拖累项目构建速度,而 Vite 选择了 esbuild 进行构建。



btw,预构建只会在开发环境生效,并使用 esbuild 进行 esm 转换,在生产环境仍然会使用 rollup 进行打包。



生产环境使用 rollup 主要是为了更好的兼容性和 tree-shaking 以及代码压缩优化等,以减小代码包体积


为什么选择 esbuild?


esbuild 的构建速度非常快,比 Webpack 快非常多,esbuild 是用 Go 编写的,语言层面的压制,运行性能更好


image.png


核心原因就是 esbuild 足够快,可以在 esbuild 官网看到这个对比图,基本上是 上百倍的差距。


前端的打包工具大多数是基于 JavaScript 实现的,由于语言特性 JavaScript 边运行边解释,而 esbuild 使用 Go 语言开发,直接编译成机器语言,启动时直接运行即可。


更多关于 Go 和 JavaScript 的语言特性差异,可以检索一下。


不久前,字节开源了 Rspack 构建工具,它是基于 Rust 编写的,同样构建速度很快



  • Rust 编译生成的 Native Code 通常比 JavaScript 性能更为高效,也意味着 rspack 在打包和构建中会有更高的性能。

  • 同时 Rust 支持多线程,意味着可以充分利用多核 CPU 的性能进行编译。而 Webpack 受限于 JavaScript 对多线程支持较弱,导致很难进行并行计算。


不过,Rspack 的插件系统还不完善,同时由于插件支持 JS 和 rust 编写,如果采用 JS 编写估计会损失部分性能,而使用 rust 开发,对于开发者可能需要一定的上手成本


image.png


同时发现 Vite 4 已经开始增加对 SWC 的支持,这是一个基于 Rust 的打包器,可以替代 Babel,以获取更高的编译性能。


**Rust 会是 JavaScript 基建的未来吗?**推荐阅读:zhuanlan.zhihu.com/p/433300816


模块热更新


主要是通过 WebSocket 创建浏览器和服务器的通信监听文件的改变,当文件被修改时,服务端发送消息通知客户端修改相应的代码,客户端对应不同的文件进行不同的操作的更新。


WebpackVite 在热更新上有什么不同呢?


Webpack: 重新编译,请求变更后模块的代码,客户端重新加载


Vite 通过监听文件系统的变更,只对发生变更的模块重新加载,只需要让相关模块的 boundary 失效即可,这样 HMR 更新速度不会因为应用体积增加而变慢,但 Webpack 需要经历一次打包构建流程,所以 HMR Vite 表现会好于 Webpack


核心流程


Vite 热更新流程可以分为以下:



  1. 创建一个 websocket 服务端和client文件,启动服务

  2. 监听文件变更

  3. 当代码变更后,服务端进行判断并推送到客户端

  4. 客户端根据推送的信息执行不同操作的更新


image.png


创建 WebSocket 服务


在 dev server 启动之前,Vite 会创建websocket服务,利用chokidar创建一个监听对象 watcher 用于对文件修改进行监听等等,具体核心代码在 node/server/index 下


image.png


createWebSocketServer 就是创建 websocket 服务,并封装内置的 close、on、send 等方法,用于服务端推送信息和关闭服务



源码地址:packages/vite/src/node/server/ws.ts



image.png


执行热更新


当接受到文件变更时,会执行 change 回调


watcher.on('change', async (file) => {
file = normalizePath(file)
// invalidate module graph cache on file change
moduleGraph.onFileChange(file)

await onHMRUpdate(file, false)
})

当文件发生更改时,这个回调函数会被触发。file 参数表示发生更改的文件路径。


首先会通过 normalizePath 将文件路径标准化,确保文件路径在不同操作系统和环境中保持一致。


然后会触发 moduleGraph 实例上的 onFailChange 方法,用来清空被修改文件对应的 ModuleNode 对象的 transformResult 属性,**使之前的模块已有的转换缓存失效。**这块在下一部分会讲到。



  • ModuleNode 是 Vite 最小模块单元

  • moduleGraph 是整个应用的模块依赖关系图



源码地址:packages/vite/src/node/server/moduleGraph.ts



onFileChange(file: string): void {
const mods = this.getModulesByFile(file)
if (mods) {
const seen = new Set<ModuleNode>()
mods.forEach((mod) => {
this.invalidateModule(mod, seen)
})
}
}

invalidateModule(
mod: ModuleNode,
seen: Set<ModuleNode> = new Set(),
timestamp: number = Date.now(),
isHmr: boolean = false,
hmrBoundaries: ModuleNode[] = [],
): void {
...
// 删除平行编译结果
mod.transformResult = null
mod.ssrTransformResult = null
mod.ssrModule = null
mod.ssrError = null
...
mod.importers.forEach((importer) => {
if (!importer.acceptedHmrDeps.has(mod)) {
this.invalidateModule(importer, seen, timestamp, isHmr)
}
})
}

可能会有疑惑,Vite 在开发阶段不是不会打包整个项目吗?怎么生成模块依赖关系图


确实是这样,Vite 不会打包整个项目,但是仍然需要构建模块依赖关系图,当浏览器请求一个模块时



  • Vite 首先会将请求的模块转换成原生 ES 模块

  • 分析模块依赖关系,也就是 import 语句的解析

  • 将模块及依赖关系添加到 moduleGraph

  • 返回编译后的模块给浏览器


因此 Vite 的 Dev 阶段时动态构建和更新模块依赖关系图的,无需打包整个项目,这也实现了真正的按需加载。


handleHMRUpdate


在 chokidar change 的回调中,还执行了 onHMRUpdate 方法,这个方法会调用执行 handleHMRUpdate 方法


handleHMRUpdate 中主要会分析文件更改,确定哪些模块需要更新,然后将更新发送给浏览器。


浏览器端的 HMR 运行时会接收到更新,并在不刷新页面的情况下替换已更新的模块。



源码地址:packages/vite/src/node/server/hmr.ts



export async function handleHMRUpdate(
file: string,
server: ViteDevServer,
configOnly: boolean,
): Promise<void> {
const { ws, config, moduleGraph } = server
// 获取相对路径
const shortFile = getShortName(file, config.root)
const fileName = path.basename(file)
// 是否配置文件修改
const isConfig = file === config.configFile
// 是否自定义插件
const isConfigDependency = config.configFileDependencies.some(
(name) => file === name,
)
// 环境变量文件
const isEnv =
config.inlineConfig.envFile !== false &amp;&amp;
(fileName === '.env' || fileName.startsWith('.env.'))
if (isConfig || isConfigDependency || isEnv) {
// auto restart server
...
try {
await server.restart()
} catch (e) {
config.logger.error(colors.red(e))
}
return
}
...
// 如果是 Vite 客户端代码发生更改,强刷
if (file.startsWith(normalizedClientDir)) {
// ws full-reload
return
}
// 获取到文件对应的 ModuleNode
const mods = moduleGraph.getModulesByFile(file)
...
// 调用所有定义了 handleHotUpdate hook 的插件
for (const hook of config.getSortedPluginHooks('handleHotUpdate')) {
const filteredModules = await hook(hmrContext)
...
}
// 如果是 html 文件变更,重新加载页面
if (!hmrContext.modules.length) {
// html file cannot be hot updated
if (file.endsWith('.html')) {
// full-reload
}
return
}

updateModules(shortFile, hmrContext.modules, timestamp, server)
}


  • 配置文件更新、.env更新、自定义插件更新都会重新启动服务 reload server

  • Vite 客户端代码更新、index.html 更新,重新加载页面

  • 调用所有 plugin 定义的 handleHotUpdate 钩子函数

  • 过滤和缩小受影响的模块列表,使 HMR 更准确。

  • 返回一个空数组,并通过向客户端发送自定义事件来执行完整的自定义 HMR 处理

  • 插件处理更新 hmrContext 上的 modules

  • 如果是其他情况更新,调用 updateModules 函数


流程图如下


image.png


updateModules 中主要是对模块进行处理,生成 updates 更新列表,ws.send 发送 updates 给客户端


ws 客户端响应


客户端在收到服务端发送的 ws.send 信息后,会进行相应的响应


当接收到服务端推送的消息,通过不同的消息类型做相应的处理,比如 updateconnectfull-reload 等,使用最频繁的是 update(动态加载热更新模块)和 full-reload (刷新整个页面)事件。



源码地址:packages/vite/src/client/client.ts



image.png


在 update 的流程里,会使用 Promise.all 来异步加载模块,如果是 js-update,及 js 模块的更新,会使用 fetchUpdate 来加载


if (update.type === 'js-update') {
return queueUpdate(fetchUpdate(update))
}

fetchUpdate 会通过动态 import 语法进行模块引入


浏览器缓存优化


Vite 还利用 HTTP 加速整个页面的重新加载。
对预构建的依赖请求使用 HTTP 头 max-age=31536000, immutable 进行强缓存,以提高开发期间页面重新加载的性能。一旦被缓存,这些请求将永远不会再次访问开发服务器。


这部分的实现在 transformMiddleware 函数中,通过中间件的方式注入到 Koa dev server 中。



源码地址:packages/vite/src/node/server/middlewares/transform.ts



若需要对依赖代码模块做改动可手动操作使缓存失效:


vite --force

或者手动删除 node_modules/.vite 中的缓存文件。


总结


Vite 采用 No Bundleesbuild 预构建,速度远快于 Webpack,实现快速的冷启动,在 dev 模式基于 ES module,实现按需加载,动态 import,动态构建 Module Graph。


在 HMR 上,Vite 利用 HTTP 头 cacheControl 设置 max-age 应用强缓存,加速整个页面的加载。


当然 Vite 还有很多的不足,比如对 splitChunks 的支持、构建生态 loader、plugins 等都弱于 Webpack。不过 Vite 仍然是一个非常好的构建工具选择。在不少应用中,会使用 Vite 来进行开发环境的构建,采用 Webpack5 或者其他 bundle base 的工具构建生产环境。


参考文章


zhuanlan.zhihu.com/p/467

325485

收起阅读 »

前端入门Docker最佳实践

本地安装及相关库 下载 Docker Desktop 双击安装即可。 作用: 打包:就是把你软件运行所需的依赖、第三方库、软件打包到一起,变成一个安装包 分发:你可以把你打包好的“安装包”上传到一个镜像仓库,其他人可以非常方便的获取和安装 部署:拿着“安...
继续阅读 »

本地安装及相关库



  1. 下载 Docker Desktop 双击安装即可。

  2. 作用:



  • 打包:就是把你软件运行所需的依赖、第三方库、软件打包到一起,变成一个安装包

  • 分发:你可以把你打包好的“安装包”上传到一个镜像仓库,其他人可以非常方便的获取和安装

  • 部署:拿着“安装包”就可以一个命令运行起来你的应用,自动模拟出一摸一样的运行环境,不管是在 Windows/Mac/Linux。


启动报错解决



  • 报错截图


image.png



  • 解决方法:


控制面板->程序->启用或关闭 windows 功能,开启 Windows 虚拟化和 Linux 子系统(WSL2)


image.png


命令行安装 Linux 内核


wsl --install -d Ubuntu


设置开机启动 Hypervisor


bcdedit /set hypervisorlaunchtype auto


设置默认使用版本2


wsl --set-default-version 2


查看 WSL 是否安装正确


wsl --list --verbose


应该如下图,可以看到一个 Linux 系统,名字你的不一定跟我的一样,看你安装的是什么版本。


并且 VERSION 是 2


image.png


切换镜像加速源


镜像加速器镜像加速器地址
Docker 中国官方镜像registry.docker-cn.com
DaoCloud 镜像站f1361db2.m.daocloud.io
Azure 中国镜像dockerhub.azk8s.cn
科大镜像站docker.mirrors.ustc.edu.cn
阿里云ud6340vz.mirror.aliyuncs.com
七牛云reg-mirror.qiniu.com
网易云hub-mirror.c.163.com
腾讯云mirror.ccs.tencentyun.com

"registry-mirrors": ["https://registry.docker-cn.com"]


image.png


目录挂载:



  • 使用 Docker 运行后,我们改了项目代码不会立刻生效,需要重新buildrun,很是麻烦。

  • 容器里面产生的数据,例如 log 文件,数据库备份文件,容器删除后就丢失了。


挂载方式



  • bind mount 直接把宿主机目录映射到容器内,适合挂代码目录和配置文件。可挂到多个容器上

  • volume 由容器创建和管理,创建在宿主机,所以删除容器不会丢失,官方推荐,更高效,Linux 文件系统,适合存储数据库数据。可挂到多个容器上

  • tmpfs mount 适合存储临时文件,存宿主机内存中。不可多容器共享。


文档参考:docs.docker.com/storage/


多容器通信


项目往往都不是独立运行的,需要数据库、缓存这些东西配合运作。


文档参考:docs.docker.com/engine/refe…


创建一个名为test-net的网络:


docker network create test-net


运行 Redis 在 test-net 网络中,别名redis


docker run -d --name redis --network test-net --network-alias redis redis:latest


Docker-Compose



  • 如果你是安装的桌面版 Docker,不需要额外安装,已经包含了。

  • 如果是没图形界面的服务器版 Docker,你需要单独安装 安装文档

  • 运行docker-compose检查是否安装成功


要把项目依赖的多个服务集合到一起,我们需要编写一个docker-compose.yml文件,描述依赖哪些服务


参考文档:docs.docker.com/compose/


docker-compose.yml 文件所在目录,执行:docker-compose up就可以跑起来了。


命令参考:docs.docker.com/compose/ref…


常用命令


docker ps 查看当前运行中的容器


docker images 查看镜像列表


docker rm container-id 删除指定 id 的容器


docker stop/start container-id 停止/启动指定 id 的容器


docker rmi image-id 删除指定 id 的镜像


docker volume ls 查看 volume 列表


docker network ls 查看网络列表


编译 docker build -t test:v1 . -t 设置镜像名字和版本号


在后台运行只需要加一个 -d 参数docker-compose up -d


查看运行状态:docker-compose ps


停止运行:docker-compose stop


重启:docker-compose restart


重启单个服务:docker-compose restart service-name


进入容器命令行:docker-compose exec service-name sh


查看容器运行log:docker-compose logs [service-na

作者:StriveToY
来源:juejin.cn/post/7256607606465380411
me]

收起阅读 »

拒绝复杂 if-else,前端策略模式实践

设计模式的重要性 为什么要学习和使用设计模式,我觉得原因主要有两点 解除耦合:设计模式的目的就是把 “不变的” 和 “可变的” 分离开,将 “不变的” 封装为统一对象,“可变的” 在具体实例中实现 定义统一标准:定义一套优秀代码的标准,相当于一份实现优秀代码...
继续阅读 »

设计模式的重要性


为什么要学习和使用设计模式,我觉得原因主要有两点



  1. 解除耦合:设计模式的目的就是把 “不变的” 和 “可变的” 分离开,将 “不变的” 封装为统一对象,“可变的” 在具体实例中实现

  2. 定义统一标准:定义一套优秀代码的标准,相当于一份实现优秀代码的说明书


在前端开发过程中面对复杂场景能能够更清晰的处理代码逻辑,其中策略模式在我的前端工作中的应用非常多,下面就展开讲讲策略模式在前端开发的具体应用


策略模式基础


策略模式的含义是:定义了一系列的算法,并将每个算法封装起来,使它们可以互相替换


我个人对于策略模式的理解,就是将原来写在一个函数中一整套功能,拆分为一个个独立的部分,从而达到解耦的目的。所以策略模式最好的应用场景,就是拆解 if-else,把每个 if 模块封装为独立算法


在面向对象的语言中,策略模式通常有三个部分



  • 策略(Strategy):实现不同算法的接口

  • 具体策略(Concrete Strategy):实现了策略定义的接口,提供具体的算法实现

  • 上下文(Context):持有一个策略对象的引用,用一个ConcreteStrategy 对象来配置,维护一个对 Strategy 对象的引用


这么看定义可能不太直观,这里我用 TS 面向对象的方式实现的一个计算器的策略模式例子说明一下


// 第一步: 定义策略(Strategy)
interface CalculatorStrategy {
calculate(a: number, b: number): number;
}

// 第二步:定义具体策略(Concrete Strategy)
class AddStrategy implements CalculatorStrategy {
calculate(a: number, b: number): number {
return a + b;
}
}

class SubtractStrategy implements CalculatorStrategy {
calculate(a: number, b: number): number {
return a - b;
}
}

class MultiplyStrategy implements CalculatorStrategy {
calculate(a: number, b: number): number {
return a * b;
}
}

// 第三步: 创建上下文(Context),用于调用不同的策略
class CalculatorContext {
private strategy: CalculatorStrategy;

constructor(strategy: CalculatorStrategy) {
this.strategy = strategy;
}

setStrategy(strategy: CalculatorStrategy) {
this.strategy = strategy;
}

calculate(a: number, b: number): number {
return this.strategy.calculate(a, b);
}
}

// 使用策略模式进行计算
const addStrategy = new AddStrategy();
const subtractStrategy = new SubtractStrategy();
const multiplyStrategy = new MultiplyStrategy();

const calculator = new CalculatorContext(addStrategy);
console.log(calculator.calculate(5, 3)); // 输出 8

calculator.setStrategy(subtractStrategy);
console.log(calculator.calculate(5, 3)); // 输出 2

calculator.setStrategy(multiplyStrategy);
console.log(calculator.calculate(5, 3)); // 输出 15

前端策略模式应用


实际上在前端开发中,通常不会使用到面向对象的模式,在前端中应用策略模式,完全可以简化为两个部分



  1. 对象:存储策略算法,并通过 key 匹配对应算法

  2. 策略方法:实现 key 对应的具体策略算法


这里举一个在最近开发过程应用策略模式重构的例子,实现的功能是对于不同的操作,处理相关字段的联动,在原始代码中,对于操作类型 opType 使用大量 if-else 判断,代码大概是这样的,虽然看起来比较少,但是每个 if 里面都有很多处理逻辑的话,整体的可读性的就会非常差了


export function transferAction() {
actions.forEach((action) => {
const { opType } = action

// 展示 / 隐藏字段
if (opType === OP_TYPE_KV.SHOW) { }
else if (opType === OP_TYPE_KV.HIDE) {}
// 启用 / 禁用字段
else if (opType === OP_TYPE_KV.ENABLE) { }
else if (opType === OP_TYPE_KV.DISABLE) {}
// 必填 / 非必填字段
else if (opType === OP_TYPE_KV.REQUIRED) { }
else if ((opType === OP_TYPE_KV.UN_REQUIRED) { }
// 清空字段值
else if (opType === OP_TYPE_KV.CLEAR && isSatify) { }
})
}

在使用策略模式重构之后,将每个 action 封装进单独的方法,再把所用的算法放入一个对象,通过触发条件匹配。这样经过重构后的代码,相比于原来的 if-else 结构更清晰,每次只要找到对应的策略方法实现即可。并且如果后续有扩展,只要继续新的增加策略方法就好,不会影响到老的代码


export function transferAction( /* 参数 */ ) {
/**
* @description 处理字段显示和隐藏
*/

const handleShowAndHide = ({ opType, relativeGroupCode, relativeCode }) => {}

/**
* @description // 启用、禁用字段(支持表格行字段的联动)
*/

const handleEnableAndDisable = ({ opType, relativeGroupCode, relativeCode }) => {}

/**
* @description 必填 / 非必填字段(支持表格行字段的联动)
*/

const handleRequiredAndUnrequired = ({ opType, relativeGroupCode, relativeCode }) => {}

/**
* @description 清空字段值
*/

const handleClear = ({ opType, relativeGroupCode, relativeCode }) => {}

// 联动策略
const strategyMap = {
// 显示、隐藏
[OP_TYPE_KV.SHOW]: handleShowAndHide,
[OP_TYPE_KV.HIDE]: handleShowAndHide,
// 禁用、启用
[OP_TYPE_KV.ENABLE]: handleEnableAndDisable,
[OP_TYPE_KV.DISABLE]: handleEnableAndDisable,
// 必填、非必填
[OP_TYPE_KV.REQUIRED]: handleRequiredAndUnrequired,
[OP_TYPE_KV.UN_REQUIRED]: handleRequiredAndUnrequired,
// 清空字段值
[OP_TYPE_KV.CLEAR]: handleClear,
}

// 遍历执行联动策略
actions.forEach((action) => {
const { opType, relativeGroupCode, relativeCode, value } = action

if (strategyMap[opType]) {
strategyMap[opType]({ /* 入参 */ })
}
})
}

总结


策略模式的优点在于:代码逻辑更清晰,每个策略对对应一个实现方法;同时遵循开闭原则,新的策略方法无需改变已有代码,所以非常适合处理或重构复杂逻辑的 if-else


在前端开发过程中,不需要遵循面向对象的应用方式,只需要通过对象存储策略算法,通过 key 匹配具体策略实现,就可以实

作者:WujieLi
来源:juejin.cn/post/7256721204300202042
现一个基础的策略模式

收起阅读 »

nest.js 添加 swagger 响应数据文档

web
基本使用 通常情况下,在 nest.js 的 swagger 页面文档中的响应数据文档默认如下 此时要为这个控制器添加响应数据文档的话,只需要先声明 数据的类型,然后通过@ApiResponse 装饰器添加到该控制器上即可,举例说明 todo.entity....
继续阅读 »

基本使用


通常情况下,在 nest.js 的 swagger 页面文档中的响应数据文档默认如下



此时要为这个控制器添加响应数据文档的话,只需要先声明 数据的类型,然后通过@ApiResponse 装饰器添加到该控制器上即可,举例说明


todo.entity.ts


@Entity('todo')
export class TodoEntity {
@Column()
@ApiProperty({ description: 'todo' })
value: string

@ApiProperty({ description: 'todo' })
@Column({ default: false })
status: boolean
}

todo.controller.ts


  @Get()
@ApiOperation({ summary: '获取Todo详情' })
@ApiResponse({ type: [TodoEntity] })
async list(): Promise<TodoEntity[]> {
return this.todoService.list();
}


@Get(':id')
@ApiOperation({ summary: '获取Todo详情' })
@ApiResponse({ type: TodoEntity })
async info(@IdParam() id: number): Promise<TodoEntity> {
return this.todoService.detail(id);
}

此时对应的文档数据如下显示


image-20230718012234692


如果你想要自定义返回的数据,而不是用 entity 对象的话,可以按照如下定义


todo.model.ts


export class Todo {
@ApiProperty({ description: 'todo' })
value: string

@ApiProperty({ description: 'todo' })
status: boolean
}

然后将 @ApiResponse({ type: TodoEntity }) 中的 TodoEntity 替换 Todo 即可。


自定义返回数据


然而通常情况下,都会对返回数据进行一层包装,如


{
"data": [
{
"name": "string"
}
],
"code": 200,
"message": "success"
}

其中 data 数据就是原始数据。要实现这种数据结构字段,首先定义一个自定义类用于包装,如


export class ResOp<T = any> {
@ApiProperty({ type: 'object' })
data?: T

@ApiProperty({ type: 'number', default: 200 })
code: number

@ApiProperty({ type: 'string', default: 'success' })
message: string

constructor(code: number, data: T, message = 'success') {
this.code = code
this.data = data
this.message = message
}
}

接着在定义一个拦截器,将 data 数据用 ResOp 包装,如下拦截器代码如下


transform.interceptor.ts


export class TransformInterceptor implements NestInterceptor {
constructor(private readonly reflector: Reflector) {}

intercept(
context: ExecutionContext,
next: CallHandler<any>,
): Observable<any> {
return next.handle().pipe(
map(data => {
const response = context.switchToHttp().getResponse<FastifyReply>()
response.header('Content-Type', 'application/json; charset=utf-8')
return new ResOp(HttpStatus.OK, data ?? null)
}),
)
}
}

此时返回的数据都会转换为 { "data": { }, "code": 200, "message": "success" } 的形式,这部分不为就本文重点,就不赘述了。


回到 Swagger 文档中,只需要 @ApiResponse({ type: TodoEntity }) 改写成 @ApiResponse({ type: ResOp<TodoEntity> }),就可以实现下图需求。


image-20230718012618710


自定义 Api 装饰器


然后对于庞大的业务而言,使用 @ApiResponse({ type: ResOp<TodoEntity> })的写法,肯定不如@ApiResponse({ type: TodoEntity })来的高效,有没有什么办法能够用后者的方式,却能达到前者的效果,答案是肯定有的。


这里需要先自定义一个装饰器,命名为 ApiResult,完整代码如下


import { Type, applyDecorators, HttpStatus } from '@nestjs/common'
import { ApiExtraModels, ApiResponse, getSchemaPath } from '@nestjs/swagger'

import { ResOp } from '@/common/model/response.model'

const baseTypeNames = ['String', 'Number', 'Boolean']

/**
* @description: 生成返回结果装饰器
*/

export const ApiResult = <TModel extends Type<any>>({
type,
isPage,
status,
}: {
type?: TModel | TModel[]
isPage?: boolean
status?: HttpStatus
}
) =>
{
let prop = null

if (Array.isArray(type)) {
if (isPage) {
prop = {
type: 'object',
properties: {
items: {
type: 'array',
items: { $ref: getSchemaPath(type[0]) },
},
meta: {
type: 'object',
properties: {
itemCount: { type: 'number', default: 0 },
totalItems: { type: 'number', default: 0 },
itemsPerPage: { type: 'number', default: 0 },
totalPages: { type: 'number', default: 0 },
currentPage: { type: 'number', default: 0 },
},
},
},
}
} else {
prop = {
type: 'array',
items: { $ref: getSchemaPath(type[0]) },
}
}
} else if (type) {
if (type && baseTypeNames.includes(type.name)) {
prop = { type: type.name.toLocaleLowerCase() }
} else {
prop = { $ref: getSchemaPath(type) }
}
} else {
prop = { type: 'null', default: null }
}

const model = Array.isArray(type) ? type[0] : type

return applyDecorators(
ApiExtraModels(model),
ApiResponse({
status,
schema: {
allOf: [
{ $ref: getSchemaPath(ResOp) },
{
properties: {
data: prop,
},
},
],
},
}),
)
}

其核心代码就是在 ApiResponse 上进行扩展,这一部分代码在官方文档: advanced-generic-apiresponse 中提供相关示例,这里我简单说明下


{ $ref: getSchemaPath(ResOp) } 表示原始数据,要被“塞”到那个类下,而第二个参数 properties: { data: prop } 则表示 ResOpdata 属性要如何替换,替换的部分则由 prop 变量决定,只需要根据实际需求构造相应的字段结构。


由于有些 类 没有被任何控制器直接引用, SwaggerModule 目前还无法生成相应的模型定义,所以需要 @ApiExtraModels(model) 将其额外导入。


此时只需要将 @ApiResponse({ type: TodoEntity }) 改写为 @ApiResult({ type: TodoEntity }),就可达到最终目的。


不过我还对其进行扩展,使其能够返回分页数据格式,具体根据实际数据而定,演示效果如下图:


image-20230718023729609


导入第三方接口管理工具


通过上述的操作后,此时记下项目的 swagger-ui 地址,例如 http://127.0.0.1:5001/api-docs, 此时再后面添加-json,即 http://127.0.0.1:5001/api-docs-json 所得到的数据便可导入到第三方的接口管理工具,就能够很好的第三方的接口协同,接口测试等功能。


image-20230718022612215


image-20230718022446188

收起阅读 »

echarts+dataV实现中国在线选择省市区地图

web
echarts+dataV实现中国在线选择省市区地图 利用 dataV 的地图 GEO JSON数据配合 echarts 和 element-china-area-data实现在线选择 省市区 地图 效果预览 可以通过自行选择省市区在线获取地图数据,配合 e...
继续阅读 »

echarts+dataV实现中国在线选择省市区地图


利用 dataV 的地图 GEO JSON数据配合 echarts 和 element-china-area-data实现在线选择 省市区 地图


效果预览




可以通过自行选择省市区在线获取地图数据,配合 echarts 渲染出来。


实现思路


先通过 regionData 中的数据配合组件库的 级联选择器 进行 省市区 的展示和选择,同时拿到选中 省市区 的 value 值去请求 dataV 中的 GEO JSON 数据


regionData 中的 省市区 数据结构为


elementChinaAreaData.regionData = [{
label: "北京市",
value: "11",
children: [{…}]
}, {
label: 'xxx',
value: 'xxx',
children: [{...}]
}]



  1. dataV 地图数据请求地址为 https://geo.datav.aliyun.com/areas_v3/bound/100000_full.json 其中https://geo.datav.aliyun.com/areas_v3/bound/请求地址前缀不变, 100000_full是全国地图数据的后缀,每个 省市区 后缀不同

  2. regionData 中的 value 值逻辑是


省级为 2                 广东省 value  44
市级为 4 广州市 value 4401
区/县级为 6 天河区 value 440106
直辖市为 2 北京市 value 11
直辖市-市辖区级为 4 北京市-市辖区 value 1101

但是 dataV 后缀长度都是 6 位,好在不足 6 位的只需要用 0 补齐就可以和 dataV 请求后缀联动起来



  1. 直辖市 和 直辖市-市辖区是指同个地址,只是 regionData 多套了一层,所以应该请求同一个 value 后缀的地址,这里以直辖市的为准,下列是需要转换的直辖市,重庆市同时含有 区和县,但是 dataV 中没有做区分,regionData 又有做区分,统一成重庆市总的地图数据


const specialMapCode = {
'1101': '11', // 北京市-市辖区
'1201': '12', // 天津市-市辖区
'3101': '31', // 上海市-市辖区
'5001': '50', // 重庆市-市辖区
'5002': '50' // 重庆市-县
}


  1. dataV 请求地址到 区/县 级后不需要加 _full 后缀如 广东省-广州市-天河区 的请求地址为 https://geo.datav.aliyun.com/areas_v3/bound/440106.json


接合这几点我们可以缕清 regionDatadataV 数据接口地址的关系了,如果 value 长度不足 6 位,如 省/市 则需要用 0 补齐到 6位,且需要加 _full,而 县/区 则不用补 0 和 _full,直辖市-市辖区 则需要进行特殊处理,也只有四个直辖市,处理难度不大,下列代码是级联选择器选择后的处理逻辑,很好理解


const padMapCode = value => {
let currValue = specialMapCode[value] || value
return currValue.length < 6 ? currValue += `${'0'.repeat(6 - currValue.length)}_full` : currValue
}

其它页面展示代码请参考源码


源码


echart-China-map

作者:半生瓜i
来源:juejin.cn/post/7256610327131258940

收起阅读 »

Blitz:可以实时聊天的协同白板

web
书接上文 之前在 跨平台渲染引擎之路:类 Figma 的无限画布 中提到了想落地无限画布场景的渲染 SDK,最近业余时间基本都在折腾类似的事情,不过在形式和顺序上有些调整,首先先看下目前还比较粗糙的项目预览。 预览 项目地址:Blitz 体验地址:Blitz...
继续阅读 »

书接上文


之前在 跨平台渲染引擎之路:类 Figma 的无限画布 中提到了想落地无限画布场景的渲染 SDK,最近业余时间基本都在折腾类似的事情,不过在形式和顺序上有些调整,首先先看下目前还比较粗糙的项目预览。


预览


preview.gif


项目地址:Blitz


体验地址:Blitz - A collaborative whiteboard with chat functionality


目前前端项目直接运行后,协同编辑与音视频部分连接的是我个人的服务器,配置比较低,可能会出现不稳定的情况,后续会再做一些服务自动重启之类的保障措施,也会在 server 模块提供本地运行的机制,感兴趣的可以先 Star 关注一下~


项目目标


产品向



  1. 类似 Canva/Figma 的白板应用

  2. 支持多人实时协作,包括编辑、评论等

  3. 支持音视频聊天、实时通信、投屏等


技术向



  1. 覆盖前端客户端与后端服务器完整链路

  2. 矢量绘制、文字排版与渲染、特效、音视频等实现原理

  3. 集成一些流行或前沿的技术尝鲜,比如 AIGC、抠图、超分等

  4. 调研子技术点目前常见的技术选型,研究第三方库与自主实现/优化的方案


落地思路


最开始提到的落地思路上的调整主要是在以下几个方面上:


第一是在项目形式上,原计划只想搞几个按钮来进行操作,结果发现这太过简陋了,要做更复杂的功能和性能测试就很不方便,同时自己后续想进一步落地像文字、路径等引擎之上的元素级别的能力,因此考虑直接搭建一个类似 Canva 或 Figma 的白板应用,并逐渐迭代到可真正供用户使用的状态。


第二是在引擎开发的节奏上,上次调研后发现,在前端 Pixi.js 的性能其实已经算是 Top 级别的了,像 WASM 方向的 CanvasKit 也只能是不相上下(当然 CanvasKit 的定位其实是不太一样的),如果按照 Pixi 的设计重新用 C++ 或 Rust 实现一遍,那亲测性能是有 30% 左右的提升的,所以考虑先直接用 Pixi.js 作为第一步的引擎,后续逐步替换成 C++ 版本。


第三是会在画布之上从编辑器的角度将整个生态搭建起来,比如更多的元素、特效,以及实时协作、通信等,并且集成一些自己感兴趣的或者看到的有意思的第三方能力,跟市面上已有的产品形成差异化,比如在白板协作的同时能够进行视频聊天、视角跟随等。


因此,在落地顺序上会先搭建一个可以满足最小主流程的编辑器,并在其上逐渐补充能力和优化架构,这过程中会使用到许多优秀的第三方项目来先快速满足需求,最后再将第三方的内容逐渐替代成原生能力(如果第三方存在问题或自己的确有这个能力的话~)。


这项目不仅涉及渲染等多媒体技术,也是一个自己用来学习从前端到后端完整技术栈的项目,欢迎大家一起交流讨论,有想要的功能或者新奇的想法更可以提出来,一起共建或者我来尝试集成到项目中看看~。


编辑器


介绍一下目前编辑器已支持的能力所涉及的技术选型,以及落地过程相关知识点的整理。


无限画布


目前我是直接使用 Pixi 作为渲染引擎,因此只需要将视图中的 传递给 Pixi 的 Application 即可。不过现实情况下我们不可能创建一个无限大的 canvas,因此一般需要自定义一个 Viewport 的概念,用于模拟出无限缩放倍数以及无边界移动的效果。


社区中已经有一个 pixi-viewport 的项目支持类似效果了,但是实际使用过程中,发现其所使用的 Pixi 是之前的版本,与最新版本结合使用时会报错,另外我预先考虑考虑是将用户操作事件进行统一的拦截、分发和处理,该项目会把视口上的事件都接管过去,与我的设计思路相悖,不过 Viewport 的复杂度也不高,因此这部分目前是直接自主实现的。


代码文件:Viewport.ts


画笔


线条的绘制最开始使用的是 Paper.js ,效果和性能方式都很优异,而且也能完全满足后面形状、路径元素的实现,在交互上 Paper 也完整支持了基于锚点调整路径等操作。不过在引入 Pixi 后就需要考虑二者如何交互,目前为了先跑通最小流程先使用的 Pixi 的 Graphics ,后面在完成二者的兼容设计后大概率还是会换回来的。


代码文件:Brush.ts


交互


编辑器用户界面框架是基于 Vue3 落地的,在 UI 组件和风格上采用的是 Element-Plus 为主,这部分前端同学应该属于驾轻就熟的,目前只实现了简单的包围盒展示以及移动元素的操作。


用户认证


用户认证目前直接使用的是第三方的 Authing 身份云 ,不仅能够支持用户名、邮箱、手机等注册方式,微信、Github等第三方身份绑定也是齐全的,并且提供了配套的 UI 组件,拆箱即用,在眼下阶段可以节省很大的成本,帮助聚焦在其他核心能力的开发上。


协同编辑


协同算法目前主流的就是 OT 和 CRDT ,二者都有许多的论文和应用实践,我目前的方案是直接使用 Y.js 项目,目前还只是简单接入,后续这个模块的设计将参考 liveblocks 进行。


CRDT 在多人编辑场景的适用性方面,首先最终一致性保障上肯定是没问题的,同时 FigmaRoom.shPingcode WIKI 等成熟项目也都正在使用,Y.js 的作者在 Are CRDTs suitable for shared editing? 文章中也做了很多说明。从个人的使用体验来说,在工程应用方面,Y.js 相比自己基于 OT 的原理进行实现而言成本大大降低了,只需要进行数据层的 Binding 即可,至于内存方面的问题,其实远没有想象中那么严重,也有许多优化手段,综合评估来看,对于个人或小团队,以及新项目来说,Y.js 是一个相对更好的选择。


协同编辑是很大的一个课题,@doodlewind 的 探秘前端 CRDT 实时协作库 Yjs 工程实现 , @pubuzhixing 的 多人协同编辑技术的演进 都是很好的学习文章,后面自己有更多心得收获的话再进行分享。


代码文件:WhiteBoard.ts


音视频会议


音视频会议的技术实现方案是多样的,你可以选择直接基于 WebRTC 甚至更底下的协议层完全自主实现,也可以采用成熟的流媒体服务器和配套客户端 SDK 进行接入,前者更适合系统学习,但是我觉得后者会更平滑一些,不仅能够先快速满足项目需求,也能够从成熟的解决方案中学习成功经验,避免自己重复走弯路。


媒体服务器的架构目前主要有 Mesh、MCU、SFU 三种。纯 Mesh 方案无法适应多人视频通话,也无法实现服务端的各种视频处理需求,SFU 相比于 MCU,服务器的压力更小(纯转发,无转码合流),灵活性更好,综合看比较适合目前我的诉求。


在 SFU 的架构方向下,被 Miro 收购的 MediaSoup 是综合社区活跃性、团队稳定性、配套完善度等方面较好的选择。在编辑器侧 MediaSoup 提供了 mediasoup-client ,基于该 SDK 可以实现房间连接、音视频流获取与传输、消息通信等能力。


代码文件:VideoChat.ts


服务器


HTTPS 证书


MediaSoup 的服务端访问需要通过 HTTPS 协议,另外处于安全考虑,也建议前端与后端通信统一走 HTTPS ,证书的申请我是用的 Certbot ,按官方教程走就行,非常简单,如果遇到静态页面 HTTPS 访问异常的话,可以参考 该文章 调整下 Nginx 配置看看。


协同编辑


协同编辑我采用的是 Hocuspocus 的解决方案,其除了提供客户端的 Provider 外,也提供了对应服务端的 SDK,按照官网教程直接使用即可,也比较简单。


不过因为项目使用的是 HTTS 协议,因此 WebScoket 也需要使用 WSS 协议才行,Hocuspocus 没有提供这部分的封装,需要自己通过 Express 中转一层,这部分参考 express-ws 的 https 实现即可。


代码文件:server 目录下的 whiteboard.ts


音视频会议


MediaSoup 官方提供了一套基本可以拆箱即用的 demo ,目前我是直接将其 server 模块的代码改了改直接部署在了后端上。主要的修改点就是在端口、证书等配置上,另外项目编译的时候可能会有一些 TS 的 Lint 错误,视情况修改或跳过即可。可以直接参考这两篇文章:mediasoup部署mediasoup错误解决


代码文件:后面熟悉了该模块代码后再整理到项目里面,目前与官方无太大差异


CI/CD


这部分会等到项目的架构相对稳定,功能相对完整后再落地,特别是 CI 属于项目质量保障的重要一环,为了项目达成用户可用的目标是一定要做的。


下一步


从上述内容看其实目前我们也还不算完成了最小流程的编辑体验,比如作图记录的保存就没有做,而且已实现的能力都比较简陋,问题较多,因此下一个项目计划节点中会做的是:



  • 保存/读取作图记录

  • 元素缩放/旋转/删除

  • 导出作图结果

  • 音视频聊天的一些能力补充

  • 支持图片/文字元素

  • 快捷键


同时会将项目的代码框架再做一些完善,修复一些问题,届时会再结合过程中的技术点或浅或深地做一些分享。


最后


目前项目还处于最最开始的起步阶段,架构设计、代码规范等都存在暴力实现的情况,不过我基本会保持每天抽时间更新的状态(通过 Github记录也能看出来),计划今年内能够实现定好的项目目标。


在过程中会阶段性分享自己做的技术选型还有一些技术原理、优化细节等,目前我的路径大体是从第三方到自主实现,因此分享上也会是从浅到深,从大框架到具体技术这么一个节奏。之前我对前端/后端开发其实接触很少,所以许多知识都需要现学现用,比如这次的 Vue、Nginx 等等,欢迎看到的朋友有任何问题或者建议可以一起交流,甚至一起共建项目~


最后对 Blitz 感兴趣的可以点个 S

作者:格子林ll
来源:juejin.cn/post/7256393626681540645
tar ,万分感谢~

收起阅读 »

手撸一个 useLocalStorage

web
前言 最近在用 vue3 + typeScript + vite 重构之前的代码,想着既然都重写了那何不大刀阔斧的改革,把复杂的逻辑全部抽象成独立的 hook,不过官方称之为“组合式函数”(Composables),好家伙写着写着就陷入 “hook 陷阱” 了...
继续阅读 »

前言


最近在用 vue3 + typeScript + vite 重构之前的代码,想着既然都重写了那何不大刀阔斧的改革,把复杂的逻辑全部抽象成独立的 hook,不过官方称之为“组合式函数”(Composables),好家伙写着写着就陷入 “hook 陷阱” 了,啥都想用 hook 实现(自己强迫自己的那种🙃),下笔之前会先去vueuse上看看有没有现成可用的,没有就自己撸一个。


但回过头来发现有些地方确实刻意为之了,导致用起来不是那么爽,比如写了一个 usePxToRem hook,作用是把 px 转换为 rem,用法如下


import { usePxToRem } from './usePxToRem'

const { rem } = usePxToRem('120px')

初看确实没问题,但如果此时有两个px需要转换怎么办,下面这样写肯定不行的,会提示变量rem已经被定义了,不能重复定义。


import { usePxToRem } from './usePxToRem'

const { rem } = usePxToRem('120px')
const { rem } = usePxToRem('140px')

像这样变通下也是勉强能解决的。


import { usePxToRem } from './usePxToRem'

const { rem: rem1 } = usePxToRem('120px')
const { rem: rem2 } = usePxToRem('140px')
console.log(rem1, rem2)

但是总感觉有点麻烦不够优雅,重新思考下这个需求,好像不需要响应式,是不是更适合用函数 convertPxToRem 解决,所以说写着写着就掉进了 hook 陷阱了😂。


正文


扯远了回到正题,开发中经常需要操作 localStorage,直接用原生也没啥问题,如果再简单封装一下就更好了,用起来方便多了。


export function getLocalStorage(key: string, defaultValue?: any) {
const value = window.localStorage.getItem(key)

if (!value) {
if (defaultValue) {
window.localStorage.setItem(key, JSON.stringify(defaultValue))
return defaultValue
} else {
return ''
}
}

try {
const jsonValue = JSON.parse(value)
return jsonValue
} catch (error) {
return value
}
}

export function setLocalStorage(key: string, value: any) {
window.localStorage.setItem(key, JSON.stringify(value))
}

export function removeLocalStorage(key: string) {
window.localStorage.removeItem(key)
}

假设有个需求在页面上实时显示 localStorage 里的值,那么必须单独设置一个变量接收 localStorage 的值,然后一边修改变量一边设置 localStorage,这样写起来就有点繁琐了。


<template>
<div>
{{ user }}
</div>
</template>

<script setup lang="ts">
import { ref } from 'vue';
import { getLocalStorage, setLocalStorage } from './localStorage';

const user = ref('')
user.value = getLocalStorage('user', '张三')
user.value = '李四'
setLocalStorage('user', user.value)
</script>

我想要的效果是一步搞定,像下面这样,是不是很优雅。


import { useLocalStorage } from './useLocalStorage'

const user = useLocalStorage('user', '张三')
user.value = '李四'

第一想法是从 vueues 上找现成的,毕竟这个需求太通用了,useLocalStorage 确实很好用,然后就在想能不能学习 vueuse 自己实现一个简单的 useLocalStorage,正好锻炼下。


第一步搭框架实现基本功能。


import { ref, watch } from "vue";

export function useLocalStorage(key: string, defaultValue: any) {
const data = ref<any>()

// 读取 storage
try {
data.value = JSON.parse(window.localStorage.getItem(key) || '')
} catch (error) {
data.value = window.localStorage.getItem(key)
} finally {
if (!data.value) {
data.value = defaultValue
}
}

// 上面只是读取 storage,并没有把更新后的值写入到 storage 中
// 接下来监听 data,每次更新都更新 storage 中值
watch(() => data.value, () => {
if (data.value === null) {
// 置为null表明要清空该值了
window.localStorage.removeItem(key)
} else {
if (typeof data.value === 'object') {
window.localStorage.setItem(key, JSON.stringify(data.value))
} else {
window.localStorage.setItem(key, data.value)
}
}
}, {
immediate: true
})

return data
}

虽然基本功能实现了,但有个问题,比如定义了一个 number 类型的 count 变量,正常情况下只能赋值数字,但这里赋值为字符串也是允许的,因为 data 设置 any 类型了,接下来想办法把类型固定住,比如一开始赋值为 number,后续更新只能是 number 类型,避免误操作。此时就不能使用 any 类型了,需要用范型来约束返回值了,至于范型是啥,请移步这里


我们约定好默认值 defaultValue 的类型就是接下来要操作的类型,稍作调整如下,这样返回值 datadefaultValue 的类型就一致了。


import { ref, watch } from "vue"
import type { Ref } from 'vue'

export function useLocalStorage<T>(key: string, defaultValue: T) {
const data = ref() as Ref<T>

// 读取 storage
try {
data.value = JSON.parse(window.localStorage.getItem(key) || '')
} catch (error) {
data.value = window.localStorage.getItem(key) as T
} finally {
if (!data.value) {
data.value = defaultValue
}
}

// 上面只是读取 storage,并没有把更新后的值写入到 storage 中
// 接下来监听 data,每次更新都更新 storage 中值
watch(() => data.value, () => {
if (data.value === null) {
// 置为null表明要清空该值了
window.localStorage.removeItem(key)
} else {
if (typeof data.value === 'object') {
window.localStorage.setItem(key, JSON.stringify(data.value))
} else {
window.localStorage.setItem(key, data.value as string)
}
}
}, {
immediate: true
})

return data
}

继续举例子看看,会发现IDE报错了,提示不能将类型“string”分配给类型“number”,至此改造第一步算是完成了。


const count = useLocalStorage('count', 1);
count.value = 2
count.value = '3'

image.png


来试试删除 count,IDE又报错了,提示不能将类型“null”分配给类型“number”,确实有道理。


image.png


那来点暴力的,在定义 data 的时候给一个 null 类型,就像这样 const data = ref() as Ref<T | null>,那么 count.value = null 就不会报错了,也能清空了。不过当我们这样写的时候问题又来了,count.value += 1,IDE会提示 “count.value”可能为 “null” ,确实在定义的时候给了一个 null 类型,那该怎么办呢?


可以用 get set 实现,在 get 的时候返回当前类型,在 set 的时候可以设置 null,然后 count.value 在设置的时候可以为 null 或者 number,在读取的时候只是 number 了。


type RemovableRef<T> = {
get value(): T
set value(value: T | null)
}

const data = ref() as RemovableRef<T>

至此一个简单的 useLocalStorage 算是实现了,顺便聊聊自己在开发 hook 时一些心得体验。



  1. 不要把所有功能写到一个 hook 中,这样没有任何意义,一定要一个功能一个 hook,功能越单一越好

  2. 有时候 hook 在初始化的时候需要传递一些参数,如果这些参数是给 hook 中某个函数使用的,那么最好是在调用该函数的时候传参,这样可以多次调用传不同的
    作者:胡先生
    来源:juejin.cn/post/7256620538092290107
    参数。

收起阅读 »

工信部又出新规!爬坑指南

一、背景 工信部最近发布了新的入网要求,明确了app进网检测要求的具体变化,主要涉及到一些app权限调用,个人信息保护,软件升级以及敏感行为。为了不影响app的正常运行,依据工信部的文件进行相关整改,下文将从5个方向来阐述具体的解决思路。 二、整改 2.1 个...
继续阅读 »

一、背景


工信部最近发布了新的入网要求,明确了app进网检测要求的具体变化,主要涉及到一些app权限调用,个人信息保护,软件升级以及敏感行为。为了不影响app的正常运行,依据工信部的文件进行相关整改,下文将从5个方向来阐述具体的解决思路。


二、整改


2.1 个人信息保护


2.1.1 基本模式(无权限、无个人信息获取模式)


这次整改涉及到最大的一个点就是基本模式,基本模式指的是在用户选择隐私协议弹窗时,不能点击“不同意”即退出应用,而是需要给用户提供一个除联网功能外,无任何权限,无任何个人信息获取的模式且用户能正常使用。


这个说法有点抽象,我们来看下友商已经做好的案例。


腾讯视频



从腾讯视频的策略来看,用户第一次使用app,依旧会弹出一个“用户隐私协议”弹窗供用户选择,但是和以往不同的是,“不同意”按钮替换为了“不同意并进入基本功能模式”,用户点击“不同意并进入基本功能模式”则进入到一个简洁版的页面,只提供一些基本功能,当用户点击“进入全功能模式”,则再次弹出隐私协议弹窗。当杀死进程后,再次进入则直接进入基本模式。


网易云音乐



网易云音乐和腾讯视频的产品策略略有不同,在用户在一级授权弹窗点击“不同意”,会跳转至二级授权弹窗,当用户在二级弹窗上点击“不同意,进入基本功能模式”,才会进入基本功能页面,在此页面上点击“进入完整功能模式”后就又回到了二级授权页。当用户杀死进程,重新进入app时,还是会回到一级授权页。


网易云音乐比腾讯视频多了一个弹窗,也只是为了提升用户进入完整模式的概率,并不涉及到新规。


另外,B站、酷狗音乐等都已经接入了基本模式,有兴趣的伙伴可以自行下载体验。


2.1.2 隐私政策内容


如果app存在读取并传送用户个人信息的行为,需要检查其是否具备用户个人信息收集、使用规则,并明确告知读取和传送个人信息的目的、方式和范围。


判断权限是否有读取、修改、传送行为,如果有,需要在隐私协议中明文告知。


举个例子,app有获取手机号码并且保存在服务器,需要在协议中明确声明:读取并传送用户手机号码。


2.2 app权限调用


2.2.1 应用内权限调用



  1. 获取定位信息和生物特征识别信息


在获取定位信息以及生物特征识别信息时需要在调用权限前,单独向用户明示调用权限的目的,不能用系统权限弹窗替代。



如上图,申请位置权限,需要在申请之前,弹出弹窗供用户选择,用户同意调用后才可以申请位置权限。



  1. 其他权限


其他权限如上面隐私政策一样,需要在调用时,声明是读取、修改、还是传送行为,如下图



2.3 应用软件升级


2.3.1 更新


应用软件或插件更新应在用户授权的情况下进行,不能直接更新,另外要明确告知用户此行为包含下载和安装。


简单来说,就是在app进行更新操作时,需要用弹窗告知用户,是否更新应用,更新的确认权交给用户,并且弹窗上需要声明此次更新有下载和安装两个操作。如下图



2.4 应用签名


需要保

作者:付十一
来源:juejin.cn/post/7253610755126476857
证签名的真实有效性。

收起阅读 »

2023年年中总结-逃离大厂后的生活真的是我想要的吗?

最近看到某象笔记大裁员的新闻,害怕笔记都寄了,开始转移笔记,转移的过程中复盘了毕业以来近五年的工作记录,做个五年的复盘。如果说要给文章起个标题,那就起本来三年前想写的一篇找工作总结--95后程序员逃离大厂,但又过了三年,现在的标题可能要加上--逃离大厂后的生活...
继续阅读 »

image.png


最近看到某象笔记大裁员的新闻,害怕笔记都寄了,开始转移笔记,转移的过程中复盘了毕业以来近五年的工作记录,做个五年的复盘。如果说要给文章起个标题,那就起本来三年前想写的一篇找工作总结--95后程序员逃离大厂,但又过了三年,现在的标题可能要加上--逃离大厂后的生活真的是我想要的吗?


上午看到一个大佬的文章,上边有一个观点感觉很适合拖延症患者,大概意思就是不管做什么,先做五分钟,于是周一开始想写的文章,终于在周五有了开始。


大学还没毕业的时候,正值互联网发展巅峰的几年,当时感觉形势一片大好,大三的时候同学们都纷纷开始找大厂的工作,大四签了大厂之后,本以为可以在大厂退休,上班之前确实没有签几年合同的概念,直到签合同的那天才知道原来第一次合同是签三年。


入职大厂后的生活确实像个螺丝钉,周围大佬真的很多,每天听同事讲他们的朋友们财富自由,感觉每天的生活没有什么实感,每天早上起床,吃早饭,上班摸会🐟,,和同事吃午饭,遛弯,午休,然后下午开始和PM斗争,到了晚上开始coding。。。每天的工作都是重复的,生活也是,看着室友和对象一起在北京买了房落了户,每个月背上不少的房贷,我常常想这是不是以后的我的生活。


在每日每夜的重复之后,在刷到某脉上一片逃离大厂的帖子之后,我越来越焦虑,生怕失去这份工作,害怕被裁员,于是开始寻找下一个目标,进国企央企,找一份“稳定”的工作。


找工作的第一步果然还是刷题,背八股文,笔记准备了很多,在找工作的时候,投了很多公司,当时的想法是找一个二线城市的稳定工作,然后定居。当时投简历的策略是,先投一些小厂攒攒面经,记录下面试时面试官可能会遇到的问题,基本每场面试我都会录音,结束后会复盘下自己的表现,然后下一次面试根据经验再做调整,因为当时更想找一份减少coding的工作,所以八股文准备的其实没有特别好。


后面也确实找到了一个看起来很稳定的二线某金融行业,生活也变的正常了起来。目前的生活就是平淡到我基本回忆不起来去年都做了什么,大厂的生活也确实离我越来越远,所以说人不能太闲,今年我又重新思考了起来,我究竟想要什么样的人生?


复盘笔记时,对过去五年的笔记做了一个简单的分析。在大厂的几年,有一些好的习惯,比如看到一些技术文档,会存下来放在笔记里,有空的时候会学习一下;刚毕业的几年每周都会对自己的工作做记录,复盘。在之前室友的带动下,还开始学习着记账,管理自己的支出和收入;年初会给自己定一个目标,到每个季度写OKR的时候会复盘一下。。。最近几年的笔记减少了技术学习,不过保留了工作总结复盘的习惯。从近几年的工作复盘中明显可见,技术基本没有成长,但是确实在工作中拓宽了一些眼界。


总结下近几年的生活,离开大厂后,确实发现,大厂是有“大厂光环”存在的,我们在工作中迅速取得的一些进步,可能只是因为我们站在了巨人的肩膀上,除去大厂的平台,我们还需要不断探索我们自身如何能在不同的平台迅速的成长。


平淡的上半年在总结中一笔带过了,希望下半年技术上能有所成长,能适当的输出一些技术文档,也能在学习的过程中找到适合自己的成长方向。最后,记录下最近最大的感悟,生活是座围城,城里的人想逃出来,城外的人想冲进去。


作者:除夕tete
来源:juejin.cn/post/7255475756321669177
收起阅读 »

揭秘 html2Canvas:打印高清 PDF 的原理解析

web
1. 前言 最近我需要将网页的DOM输出为PDF文件,我使用的技术是html2Canvas和jsPDF。具体流程是,首先使用html2Canvas将DOM转化为图片,然后将图片添加到jsPDF中进行输出。 const pdf = new jsPDF({    ...
继续阅读 »

1. 前言


最近我需要将网页的DOM输出为PDF文件,我使用的技术是html2Canvas和jsPDF。具体流程是,首先使用html2Canvas将DOM转化为图片,然后将图片添加到jsPDF中进行输出。


const pdf = new jsPDF({     
unit: 'pt',    
format: 'a4',    
orientation: 'p',
});
const canvas = await html2canvas(element,
{
onrendered: function (canvas) {    
document.body.appendChild(canvas);  
}
}
);
const canvasData = canvas.toDataURL('image/jpeg', 1.0);
pdf.addImage(canvasData, 10, 10);
pdf.save('jecyu.pdf');

遇到了图片导出模糊的问题,解决思路是:



  1. 先html2canvas 转成高清图片,然后再传一个 scale 配置:


scale: window\.devicePixelRatio \* 3// 增加清晰度


  1. 为了确保图片打印时不会变形,需要按照 PDF 文件的宽高比例进行缩放,使其与 A4 纸张的宽度一致。因为 A4 纸张采用纵向打印方式,所以以宽度为基准进行缩放。


// 获取canavs转化后的宽度 
const canvasWidth = canvas.width;
// 获取canvas转化后的高度
const canvasHeight = canvas.height;
// 高度转化为PDF的高度 const height = (width / canvasWidth) \* canvasHeight;
// 1 比 1 进行缩放
pdf.addImage(data, 'JPEG', 0, 0, width, height);
pdf.save('jecyu.pdf');

要想了解为什么这样设置打印出来的图片变得更加清晰,需要先了解一些有关图像的概念。


2. 一些概念


2.1 英寸


F2FDB01D-EAF3-4056-BFB0-A2615285F55C.png

英寸是用来描述屏幕物理大小的单位,以对角线长度为度量标准。常见的例子有电脑显示器的17英寸或22英寸,手机显示器的4.8英寸或5.7英寸等。厘米和英寸的换算是1英寸等于2.54厘米。


2.2 像素


像素是图像显示的基本单元,无法再分割。它是由单一颜色的小方格组成的。每个点阵图都由若干像素组成,这些小方格的颜色和位置决定了图像的样子。


image.png

图片、电子屏幕和打印机打印的纸张都是由许多特定颜色和位置的小方块拼接而成的。一个像素通常被视为图像的最小完整样本,但它的定义和上下文有关。例如,我们可以在可见像素(打印出来的页面图像)、屏幕上的像素或数字相机的感光元素中使用像素。根据上下文,可以使用更精确的同义词,如像素、采样点、点或斑点。


2.3 PPI 与 DPI


PPI (Pixel Per Inch):每英寸包括的像素数,用来描述屏幕的像素密度。


DPI(Dot Per Inch):即每英寸包括的点数。   


在这里,点是一个抽象的单位,可以是屏幕像素点、图片像素点,也可以是打印的墨点。在描述图片和屏幕时,通常会使用DPI,这等同于PPI。DPI最常用于描述打印机,表示每英寸打印的点数。一张图片在屏幕上显示时,像素点是规则排列的,每个像素点都有特定的位置和颜色。当使用打印机打印时,打印机可能不会规则地打印这些点,而是使用打印点来呈现图像,这些打印点之间会有一定的空隙,这就是DPI所描述的:打印点的密度。


30E718E3-8D78-4759-8D3A-A2E428936DF7.png


在这张图片中,我们可以清晰地看到打印机是如何使用墨点打印图像的。打印机的DPI越高,打印出的图像就越精细,但同时也会消耗更多的墨点和时间。


2.4 设备像素


设备像素(物理像素)dp:device pixels,显示屏就是由一个个物理像素点组成,屏幕从工厂出来那天物理像素点就固定不变了,也就是我们经常看到的手机分辨率所描述的数字。


DF7FDA29-CBFC-41DD-8AD3-E4BB480C322F.png
一个像素并不一定是小正方形区块,也没有标准的宽高,只是用于丰富色彩的一个“点”而已。


2.5 屏幕分辨率


屏幕分辨率是指一个屏幕由多少像素组成,常说的分辨率指的就是物理像素。手机屏幕的横向和纵向像素点数以 px 为单位。


CB3C41C3-C15B-40E5-9724-60E7639A1B65.png

iPhone XS Max 和 iPhone SE 的屏幕分辨率分别为 2688x1242 和 1136x640。分辨率越高,屏幕上显示的像素就越多,单个像素的尺寸也就越小,因此显示效果更加精细。


2.6 图片分辨率


在我们所说的图像分辨率中,指的是图像中包含的像素数量。例如,一张图像的分辨率为 800 x 400,这意味着图像在垂直和水平方向上的像素点数分别为 800 和 400。图像分辨率越高,图像越清晰,但它也会受到显示屏尺寸和分辨率的影响。


如果将 800 x 400 的图像放大到 1600 x 800 的尺寸,它会比原始图像模糊。通过图像分辨率和显示尺寸,可以计算出 dpi,这是图像显示质量的指标。但它还会受到显示屏影响,例如最高显示屏 dpi 为 100,即使图像 dpi 为 200,最高也只能显示 100 的质量。


可以通过 dpi 和显示尺寸,计算出图片原来的像素数


719C1F90-0990-499B-AD74-0ED41A8825FD.png

这张照片的尺寸为 4x4 英寸,分辨率为 300 dpi,即每英寸有 300 个像素。因此它的实际像素数量是宽 1200 像素,高 1200 像素。如果有一张同样尺寸(4x4 英寸)但分辨率为 72 dpi 的照片,那么它的像素数量就是宽 288 像素,高 288 像素。当你放大这两张照片时,由于像素数量的差异,可能会导致细节的清晰度不同。


怎么计算 dpi 呢?dpi = 像素数量 / 尺寸


举个例子说明:


假设您有一张宽为1200像素,高为800像素的图片,您想将其打印成4x6英寸的尺寸。为此,您可以使用以下公式计算分辨率:宽度分辨率 = 1200像素/4英寸 = 300 dpi;高度分辨率 = 800像素/6英寸 = 133.33 dpi。因此,这张图片的分辨率为300 dpi(宽度)和133.33 dpi(高度)。需要注意的是,计算得出的分辨率仅为参考值,实际的显示效果还会受到显示设备的限制。


同一尺寸的图片,同一个设备下,图片分辨率越高,图片越清晰。  


A790AD33-7440-458B-8588-F32827C533BD.png


2.7 设备独立像素


前面我们说到显示尺寸,可以使用 CSS 像素来描述图片在显示屏上的大小,而 CSS 像素就是设备独立像素。设备独立像素(逻辑像素)dip:device-independent pixels,独立于设备的像素。也叫密度无关像素。


为什么会有设备独立像素呢?


智能手机的发展非常迅速。几年前,我们使用的手机分辨率非常低,例如左侧的白色手机,它的分辨率只有320x480。但是,随着科技的进步,低分辨率手机已经无法满足我们的需求了。现在,我们有更高分辨率的屏幕,例如右侧的黑色手机,它的分辨率是640x960,是白色手机的两倍。因此,如果在这两个手机上展示同一张照片,黑色手机上的每个像素点都对应白色手机上的两个像素点。


image.png

理论上,一个图片像素对应1个设备物理像素,图片才能得到完美清晰的展示。因为黑色手机的分辨率更高,每英寸显示的像素数量增多,缩放因素较大,所以图片被缩小以适应更高的像素密度。而在白色手机的分辨率较低,每英寸显示的像素数量较少,缩放因素较小,所以图片看起来相对较大。


为了解决分辨率越高的手机,页面元素越来越小的问题,确保在白色手机和黑色手机看起来大小一致,就出现了设备独立像素。它可以认为是计算机坐标系统中得到一个点,这个点代表可以由程序使用的虚拟像素。


例如,一个列表的宽度 300 个独立像素,那么在白色手机会用 300个物理像素去渲染它,而黑色手机使用 600个物理像素去渲染它,它们大小是一致的,只是清晰度不同。


那么操作系统是怎么知道 300 个独立像素,应该用多少个物理像素去渲染它呢?这就涉及到设备像素比。


2.8 设备像素比


设备像素比是指物理像素和设备独立像素之间的比例关系,可以用devicePixelRatio来表示。具体而言,它可以按以下公式计算得出。


设备像素比:物理像素 / 设备独立像素 // 在某一方向上,x 方向或者 y 方向

在JavaScript中,可以使用window.devicePixelRatio获取设备的DPR。设备像素比有两个主要目的:



  • 1.保持视觉一致性,以确保相同大小的元素在不同分辨率的屏幕上具有一致的视觉大小,避免在不同设备上显示过大或过小的问题。

  • 2.支持高分辨率屏幕,以提供更清晰、更真实的图像和文字细节。


开发人员可以使用逻辑像素来布局和设计网页或应用程序,而不必考虑设备的物理像素。系统会根据设备像素比自动进行缩放和适配,以确保内容的一致性和最佳显示效果。


3. 分析原理


3.1 html2canvas 整体流程


在使用html2canvas时,有两种可选的模式:一种是使用foreignObject,另一种是使用纯canvas绘制。


使用第一种模式时,需要经过以下步骤:首先将需要截图的DOM元素进行克隆,并在过程中附上getComputedStyle的style属性,然后将其放入SVG的foreignObject中,最后将SVG序列化成img的src(SVG直接内联)。


img.src = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(new XMLSerializer().serializeToString(svg)); 4.ctx.drawImage(img, ....)

第二种模式是使用纯Canvas进行截图的步骤。具体步骤如下:



  1. 复制要截图的DOM,并将其附加样式。

  2. 将复制的DOM转换为类似于VirtualDOM的对象。

  3. 递归该对象,根据其父子关系和层叠关系计算出一个renderQueue。

  4. 每个renderQueue项目都是一个虚拟DOM对象,根据之前获取的样式信息,调用ctx的各种方法。


6C07042D-923E-4FFA-9C79-FF924D3E8512.png


3.2 分析画布属性 width、height、scale


通常情况下,每个位图像素应该对应一个物理像素,才能呈现完美清晰的图片。但是在 retina 屏幕下,由于位图像素点不足,图片就会变得模糊。


为了确保在不同分辨率的屏幕下输出的图片清晰度与屏幕上显示一致,该程序会取视图的 dpr 作为默认的 scale 值,以及取 dom 的宽高作为画布的默认宽高。这样,在 dpr 为 2 的屏幕上,对于 800 * 600 的容器画布,通过 scale * 2 后得到 1600 * 1200 这样的大图。通过缩放比打印出来,它的清晰度是跟显示屏幕一致的。


0A21E97D-FE73-47D9-9CE8-058696CCE58C.png


假设在 dpr 为 1 的屏幕,假如这里 scale 传入值为 2,那么宽、高和画布上下文都乘以 2倍。


A3086809-FA99-42A4-8C9E-8BA6C21395E4.png


为什么要这样做呢?因为在 canvas 中,默认情况下,一个单位恰好是一个像素,而缩放变换会改变这种默认行为。比如,缩放因子为 0.5 时,单位大小就变成了 0.5 像素,因此形状会以正常尺寸的一半进行绘制;而缩放因子为 2.0 时,单位大小会增加,使一个单位变成两个像素,形状会以正常尺寸的两倍进行绘制。


如下例子,通过放大倍数绘制,输出一张含有更多像素的大图


// 创建 Canvas 元素 
const canvas = document.createElement('canvas');
canvas.width = 200;
canvas.height = 200;

// 获取绘图上下文
const ctx = canvas.getContext('2d');
// 绘制矩形
ctx.fillStyle = 'red';
ctx.fillRect(50, 50, 100, 100);
document.body.appendChild(canvas)

//== 放大2倍画布 ==//
const canvas2 = document.createElement('canvas'); //
// 改变 Canvas 的 width 和 height
canvas2.width = 400;
canvas2.height = 400;
const ctx2 = canvas2.getContext('2d');
// 绘制矩形
ctx2.scale(2, 2);
// 将坐标系放大2倍,必须放置在绘制矩形前才生效
ctx2.fillStyle = 'blue';
ctx2.fillRect(50, 50, 100, 100);
document.body.appendChild(canvas2)

3.3 为什么 使用 dpr * 倍数进行 scale


在使用html2Canvas时,默认会根据设备像素比例(dpr)来输出与屏幕上显示的图片清晰度相同的图像。但是,如果需要打印更高分辨率的图像,则需要将dpr乘以相应的倍数。例如,如果我们想要将一张800像素宽,600像素高,72dpi分辨率的屏幕图片打印在一张8x6英寸,300dpi分辨率的纸上,我们需要确保图片像素与打印所需像素相同,以保证清晰度。


步骤 1: 将纸的尺寸转换为像素


可以使用打印分辨率来确定转换后的像素尺寸。


假设打印分辨率为 300 dpi,纸的尺寸为 8x6 英寸,那么:


纸的宽度像素 = 8 英寸 * 300 dpi = 2400 像素


纸的高度像素 = 6 英寸 * 300 dpi = 1800 像素


步骤 2: 计算图片在纸上的实际尺寸


将图片的尺寸与纸的尺寸进行比例缩放,以确定在纸上的实际打印尺寸


图片在纸上的宽度 = (图片宽度 / 屏幕像素每英寸) * 打印分辨率


图片在纸上的高度 = (图片高度 / 屏幕像素每英寸) * 打印分辨率


图片在纸上的宽度 = (800 / 72) * 300 = 3333.33 像素(约为 3334 像素)


图片在纸上的高度 = (600 / 72) * 300 = 2500 像素


步骤 3: 调整图片大小和打印分辨率


根据计算出的实际尺寸,可以将图片的大小调整为适合打印的尺寸,并设置适当的打印分辨率。


图片在纸上的宽度为 3334 像素,高度为 2500 像素。


也就是说,在保持分辨率为 72 dpi 的情况下,需要把原来 800*600 的图片,调整像素为 3334 * 2500。如果是位图直接放大,就会变糊。如果是矢量图,就不会有问题。这也是 html2Canvas 最终通过放大 scale 来提高打印清晰度的原因。


在图片调整像素为 *3334 * 2500,虽然屏幕宽高变大了,但通过打印尺寸的换算,最终还是 6 8 英寸,分辨率 为 300dpi。


在本案例中,我们需要打印出一个可以正常查看的 pdf,对于 A4尺寸,我们可以用 pt 作为单位,其尺寸为 595pt * 841pt。 实际尺寸为  595/72 = 8.26英寸,841/72 =  11.68英寸。为了打印高清图片,需要确保每英寸有300个像素,也就是8.26 * 300 = 2478像素,11.68 * 300 = 3504 像素,也就是说 canvas 转出的图片必须要这么大,最终打印的像素才这么清晰。


而在绘制 DOM 中,由于调试时不需要这么大,我们可以缩放比例,比如缩小至3倍,这样图片大小就为826像素 * 1168像素。如果高度超过1168像素,则需要考虑分页打印。


下面是 pt 转其他单位的计算公式


function convertPointsToUnit(points, unit) {   
// Unit table from <https://github.com/MrRio/jsPDF/blob/ddbfc0f0250ca908f8061a72fa057116b7613e78/jspdf.js#L791>  
var multiplier;  
switch(unit) {    
case 'pt'
multiplier = 1;         
break;    
case 'mm'
multiplier = 72 / 25.4
break;    
case 'cm'
multiplier = 72 / 2.54
break;    
case 'in'
multiplier = 72;        
break;    
case 'px'
multiplier = 96 / 72;   
break;    
case 'pc'
multiplier = 12;        
break;    
case 'em'
multiplier = 12;        
break;    
case 'ex'
multiplier = 6;
break;
default:      
throw ('Invalid unit: ' + unit);  
}  
return points \* multiplier; }

4. 扩展


4.1 为什么使用大图片 Icon 打印出来还模糊


在理论上,一个位图像素应该对应一个物理像素,这样图片才能完美清晰地展示。在普通屏幕上,这没有问题,但在Retina屏幕上,由于位图像素点不足,图片会变得模糊。


EE3424CB-9DEB-4F55-B4A7-89736725C0E1.jpg


所以,对于图片高清问题,比较好的方案是两倍图片(@2x)


如:200x300(css pixel)img标签,就需要提供 400x600 的图片


如此一来,位图像素点个数就是原来的 4 倍,在 retina 屏幕下,位图像素个数就可以跟物理像素个数


形成 1:1 的比例,图片自然就清晰了(这也解释了为啥视觉稿的画布需要 x2


这里还有另一个问题,如果普通屏幕下,也用了两倍图片 ,会怎么样呢?


很明显,在普通屏幕下(dpr1),200X300(css pixel)img 标签,所对应的物理像素个数就是 200x300 个。而两倍图的位图像素。则是200x300*4,所以就出现一个物理像素点对应4个位图像素点,所以它的取色也只能通过一定的算法(显示结果就是一张只有原像素总数四分之一)


我们称这个过程叫做(downsampling),肉眼看上去虽然图片不会模糊,但是会觉得图片缺失一些锐利度。


11465C2B-AEBB-472D-9CFD-8922ADB72E5F.jpg


通常在做移动端开发时,对于没那么精致的app,统一使用 @2x 就好了。


10C2665B-6F90-4317-8AE9-A380F9F0ABA3.png


上面 100x100的图片,分别放在 100x100、50x50、25x25的 img 容器中,在 retina 屏幕下的显示效果


条码图,通过放大镜其实可以看出边界像素点取值的不同:




  • 图片1,就近取色,色值介于红白之间,偏淡,图片看上去可能会模糊(可以理解为图片拉伸)。




  • 图片2,没有就近取色,色值要么红,要么是白,看上去很清晰。




  • 图片3,就近取色,色值位于红白之间,偏重,图片看上去有色差,缺失锐利度(可以理解为图片挤压)。




要想大的位图 icon 缩小时保证显示质量,那就需要这样设置:


img {     
image-rendering:-moz-crisp-edges;    
image-rendering:-o-crisp-edges;    
image-rendering:-webkit-optimize-contrast;    
image-rendering: crisp-edges;    
-ms-interpolation-mode: nearest-neighbor;    
-webkit-font-smooting:  antialiased;
}

5. 总结


本文介绍了如何通过使用 html2Canvas 来打印高清图片,并解释了一些与图片有关的术语,包括英寸、像素、PPI 与 DPI、设备像素、分辨率等,同时逐步分析了 html2Canvas 打印高清图片的原理。



demo: github.com/jecyu/pdf-d…



参考资料


收起阅读 »

uni-app下App转微信小程序的操作经验

web
背景 就是老板觉得 app 比较难以开展,需要开发小程序版本方便用户引入; 个人觉得,我们的产品更偏向B端产品,需要公司整体入住,而不是散兵游勇的加入,没必要进行这样的引流,奈何我不是老板,那就干。 目前已经有二十几个页面及即时通信模块,已经可以稳定运行;...
继续阅读 »

背景



  1. 就是老板觉得 app 比较难以开展,需要开发小程序版本方便用户引入;

    1. 个人觉得,我们的产品更偏向B端产品,需要公司整体入住,而不是散兵游勇的加入,没必要进行这样的引流,奈何我不是老板,那就干。

    2. 目前已经有二十几个页面及即时通信模块,已经可以稳定运行;



  2. 后续新开发的功能要兼容到App和微信小程序;

  3. 同时还要按照新的ui进行修改页面样式。


关于APP代码转小程序的方案研究



  1. App的开发方案uni-app,本来就是留了兼容的方案的,但是目前有很多的业务,需要逐步测试优化;

  2. 原始开发过程一般以h5为基础,然后兼容app的各种版本;

  3. 开发过程,代码管理的考虑是需要切出一个新的打包小程序分支,这样对于基础的更新仍然在app端首先兼容开发,后续合并到具体的端开发分支上,然后做兼容问题处理,具体的分支如下:

    1. ft/base分支,仍旧以原本的App开发分支为准;

    2. ft/app分支,用做App的开发兼容测试;

      1. ft/app_android_qa,app的安卓端测试分支‘

      2. ...



    3. ft/mp分支,用做微信小程序开发兼容测试;




按着官方指导文档进行修改,对可预知的问题进行修改



  1. App正常,小程序、H5异常的可能性

    1. 代码中使用了App端特有的plus、Native.js、subNVue、原生插件等功能,如下的地点坐标获取功能;



  2. 微信小程序开发注意

    1. 这里一个很重要的问题,需要对原始的项目进行分包,不然是绝对不能提交发布的;




地点坐标获取功能


本次开发中的地理位置选择功能,在App下使用了原生的高德地图服务,在小程序下边就需要改成腾讯地图的位置选择服务uni.chooseLocation


高德地图、腾讯地图以及谷歌中国区地图使用的是GCJ-02坐标系,还好这两个使用的坐标系是一致的,否则就需要进行坐标的转换;


关联的bug报错:getLocation:fail the api need to be declared in the requiredPrivateInfos field in app.json/ext.json


// manifest.json,如下两个平台不需要同时配置
{
// App应用,使用高德地图
"sdkConfigs": {
"geolocation": {
"amap": {
"__platform__": ["ios", "android"],
"appkey_ios": "",
"appkey_android": ""
}
},
"maps": {
"amap": {
"appkey_ios": "",
"appkey_android": ""
}
},
},

// 在小程序下使用地图选择,使用腾讯地图
"mp-weixin": {
"permission": {
"scope.userLocation": {
"desc": "你的位置信息将用于小程序位置接口的效果展示"
}
},
// 这里的配置是有效的
"requiredPrivateInfos": ["getLocation", "chooseLocation"],
}
}


契约锁



  1. 契约锁,app下使用的是webview直接打开签订的合同;

  2. 但是在小程序,需要引用契约锁的小程序插件页面;


// App下打开webview进行操作
await navTo('/pages/common/webview');
export const navTo = (url, query) => {
if (query) {
url += `?query=${encodeURIComponent(JSON.stringify(query))}`;
}
return new Promise((resolve, reject) => {
uni.navigateTo({
url,
success: (res) => {
resolve(res);
},
fail: (e) => {
reject(e);
},
});
});
};

// 微信小程序下的处理方式
// 如下打开插件页面
const res = await wx.navigateTo({
url: `plugin://qyssdk-plugin/${pageType}?ticket=${ticket}&hasCb=true&env=${baseUrl.qys_mp_env}`,
success(res) {},
fail(e) {},
});

微信小程序分包



  1. 原始的App版本是在pages下边进行平铺的,没任何分包;

  2. 小程序每个分包不能大于2M,主包也不能大于2M,分包总体积不超过 20M;

  3. 在小程序下,分包:

    1. 主包,包括基础的一些配置,资源文件等,还要包括几个tab页面;

    2. 分包,按照业务模块进行划分:

      1. "root": "pages/authenticate"

      2. "root": "pages/team",

      3. "root": "pages/salary",

      4. "root": "pages/employ",

      5. ...





  4. 分包之后需要相应的修改页面跳转的地址,当前版本主要在pages.json里边进行划分,所以需要修改的跳转地址并不是很多;


压缩资源文件大小



  1. 对static目录进行整理;

    1. 压缩图片文件;

    2. 对于不着急展示的图片采用远端加载的方式;



  2. 删除不需要的资源,如一些不兼容微信端的组件、不再用的组件等;


视频模块nvue页面的重写



  1. 原本的组件不支持小程序,后续只能重新写这块;

  2. 删除原本的App视频模块nvue页面;


即时通信模块的业务修改



  1. 这块的核心是推送即时消息,在小程序下很容易收不到,最后的方案是做一个新的页面,去提示下载打开App操作;

  2. 删除原本的App即时通信所引入的各种资源文件;


整体ui的修改



  1. 修改基础的样式定义变量;

    1. 修改uni.scss文件,修改为新的ui风格;



  2. 对硬页面的ui逐步修改;


小程序的按需注入


小程序配置:lazyCodeLoading,在 mp-weixin 下边配置;


直接运行代码,对着bug进行逐步修改


在开发工具中运行,查看控制台以及小程序评分、优化体验等的提示进行。


Error: 暂不支持动态组件[undefined],Errors compiling template: :style 不支持 height: ${scrollHeight}px 语法


其实就是 style 的一种写法的问题,语法问题:


:style="{height: `${scrollHeight}px`}">


:style="`height: ${scrollHeight}px`" => :style="{height: `${scrollHeight}px`}"


http://test.XXX.com 不在以下 request 合法域名列表中


配置request合法域名的问题,参考文档:developers.weixin.qq.com/miniprogram…,添加后正常。


Unhandled promise rejection


当 Promise 的状态变为 rejection 时,我们没有正确处理,让其一直冒泡(propagation),直至被进程捕获。这个 Promise 就被称为 unhandled promise rejection。


Error: Compile failed at pages/message/components/Chat.vue






只能删除后使用v-if进行判断展示;


无效的 page.json ["titleNView"]


也就是这里的头信息不能支持这个配置,直接删除。


代码质量的问题 / 代码优化


common/vendor 过大的问题



  1. uni-app 微信小程序 vendor.js 过大的处理方式和分包优化

    1. 使用运行时代码压缩;

      1. HBuilder 直接开启压缩,但是这样会编译过程变慢;

      2. cli 创建的项目可以在 package.json 中添加参数–minimize





  2. vendor.js 过大的处理方式

    1. 开启压缩;

    2. 分包,对一些非主包引用的资源引用位置进行修改;




总结



  1. 方向很重要,预先的系统选型要多考虑以后的需要,不要太相信老板的话,可能开始说不要,后边就要了;

  2. uni-app框架下,兼容多端的修改还是容易处理的,一般只会发生几类问题,有时候看起来很严重,其实并不严重;


以上只是个人见解,请指教

作者:qiuwww
来源:juejin.cn/post/7255879340223184956

收起阅读 »

如何做一份干净的Git提交记录

背景 毕业工作有一些年头了,之前在写工作代码或者给开源项目贡献的时候,提交代码都不是很规范,甚至可以说十分的随意,想到什么就提交什么,根本没有管理提交记录的概念或者想法(当你身边的人都不怎么在意的时候,你也很难在意的)。工作了一段时间之后,对代码提交规范的要求...
继续阅读 »

背景


毕业工作有一些年头了,之前在写工作代码或者给开源项目贡献的时候,提交代码都不是很规范,甚至可以说十分的随意,想到什么就提交什么,根本没有管理提交记录的概念或者想法(当你身边的人都不怎么在意的时候,你也很难在意的)。工作了一段时间之后,对代码提交规范的要求高了不少,因为我发现,当我把这件事情做好的时候,会让工作变得顺畅一些。有一天解决倒腾git解决了一些问题,就想到把这些东西沉淀下来,分享出去。所以写下了这篇文章。倒也不是多么高大上的代码技巧或者炫技,只是分享之前遇到的一些git commit问题,和分享一些见解。


cases and thoughts


1. 处理业务开发代码和测试联调代码


想象一下这样一个场景:有一个需求开发完了,准备和其他同事联调这个需求。谨慎起见,联调期间希望可以观测到这个需求运行过程中的大部分细节。比如上下游的输入输出是什么样的,运行到某段代码他的表现应该是什么样的。如果没有测试到位,就上线了,一不小心造成了什么事故,问题可就大了。另外,如果在测试过程中遇到了肉眼看不出来的bug,这时候也需要打一些日志去分析。


这个时候问题就来了,调试日志代码一般情况下是不能上线的,毕竟一个系统在关键的地方打上日志方便线上排查bug就好,到处打日志估计服务器也很难顶得住。这个时候怎么办呢?我会通过下面的例子来演示下面解决的方法。


首先我们假设有一个仓库。叫做git-experiment.并且当前有一个提交,是一个名为Feature-1的需求的相关开发改动。通过查看git提交日志我们可以看到:


image-20230623172359302


下面让我们看看具体怎么解决这个问题。


1.1 直接在这个分支上写,merge进master之前去掉这些提交记录


第一个做法其实比较好理解,就直接在原来的分支上加嘛,测试完了把这些关于测试的提交记录去掉就好。下面这幅图模拟的情景是:



  1. 加上了打印log逻辑。

  2. 在测试中发现了问题,又添加了一个commit来修复这个bug。


image-20230623172557907


测试结束,这个时候我们看到上面这样一份提交记录,应该怎么办呢,用git revert?是不太行的喔,因为revert会生成一个新的提交记录。像下面这样子。


image-20230623175716868


这样子虽然代码没了,但是这份提交记录看起来就会很奇怪。很明显,在这个场景中,我们实际上只需要关于Feature-1代码改动和关于bug fix的代码改动。这个时候推荐用git rebase。比如在上面这个case中,可以运行:


git rebase -i HEAD~2

这时候我们将看到下面这个景象,这些你挑选好的commit会倒序排列在你面前让你操作。


image-20230623181437659


d代表要放弃这个commit,pick代表保留。确认好你的操作之后退出。可能会遇到代码冲突,解决冲突之后可以执行git rebase --continue。完事之后我们可以看到。


image-20230623181551019


Perfect,这就是我们想要的。但是,这个方法的缺点就是在原来的分支上引入了新的不确定性,并且在去除日志代码的时候,最好期待没有冲突出现,如果出现了冲突,一个不小心把业务需求的代码搞掉了,那就美滋滋了。这种做法相当于把所有的风险都集中在一个分支上面。


1.2 切换一个分支,在新分支上写测试相关的代码


这里介绍第二种方法。我们可以在原来的基础上切换出一个新的分支来,专门用来做测试相关的工作,原有的分支专注在业务开发和bug fix。


image-20230623184507882


然后给这个分支加上测试相关的日志打印代码。


image-20230623185128561


这时候可以看一下这个分支上面的提交日志。


image-20230623185158854


这时候我们就可以拿这个分支去部署,然后和别人联调测试了。如果在测试过程中发现了bug,需要修复怎么办?这个好办,在原来的分支上做bug fix,然后测试分支cherry-pick bug fix的commit就好了。让我们来看看。


image-20230623185610302


commit bug fix的代码后,切换到测试分支,cherry pick这个commit。


image-20230623190717668


有冲突解决冲突。弄完之后让我们来看看这个分支的log。


image-20230623190652166


这样你就可以继续用这个分支做bug fix之后的测试啦。这样做的好处就是,你的开发分支始终是安全且干净的。冲突都在测试分支解决。


2. 让每一个commit都有意义


相信大家都听过这样的一句话,"代码是写给人看的,其次它能在电脑上跑起来。"。无论是工作还是开源项目。如果你写的代码会给别人仔细的review,那么让别人知道你的代码代表什么,要做什么,就显得尤为关键。不要因为做这个需要时间就不去做,如果别人没看懂你的commit或者需要你修改一下commit信息,这些现在省下来的时间后面还是会还回去的。就比如上面这个例子的最终状态是这样的:


image-20230623181551019


实际上,我个人认为这两个commit应该合并成一个,其实看代码的人看到这个bug fix的commit还会好奇这个提交的上下文是什么,可能脑海里会涌现出这样一个问题:bug是什么?不要让别人想太多,如果他很困惑,他就会来问你。最好可以通过commit信息告诉他你做了什么。


说到这里你就会想了,我把所有代码都弄成一个commit,再给上比较好的commit信息是不是就好了。看情况吧,如果一个需求你写了好多代码,上千行这样子。这时候我认为应该拆分一下commit,比如这个时候你可以拆分成好几个小的commit,再分别给提供commit信息。看代码的人可以通过你的commit信息拆解你的需求,细分到每一个小需求去审视你的代码,这也节约了他人的时间。


总结


上面是在我在工作中遇到的一些关于代码提交的问题和想法, 在这里分享给到大家。希望对大家有所帮助。


作者:陪计算机走过漫长岁月
来源:juejin.cn/post/7255967761804951589
收起阅读 »

什么是护网行动?

一、什么是护网行动? 护网行动是以公安部牵头的,用以评估企事业单位的网络安全的活动。 具体实践中。公安部会组织攻防两方,进攻方会在一个月内对防守方发动网络攻击,检测出防守方(企事业单位)存在的安全漏洞。 通过与进攻方的对抗,企事业单位网络、系统以及设备等的安全...
继续阅读 »

一、什么是护网行动?


护网行动是以公安部牵头的,用以评估企事业单位的网络安全的活动。


具体实践中。公安部会组织攻防两方,进攻方会在一个月内对防守方发动网络攻击,检测出防守方(企事业单位)存在的安全漏洞。


通过与进攻方的对抗,企事业单位网络、系统以及设备等的安全能力会大大提高。


“护网行动”是国家应对网络安全问题所做的重要布局之一。“护网行动”从2016年开始,随着我国对网络安全的重视,涉及单位不断扩大,越来越多的单位都加入到护网行动中,网络安全对抗演练越来越贴近实际情况,各机构对待网络安全需求也从被动构建,升级为业务保障刚需。


二、护网分类


护网一般按照行政级别分为国家级护网、省级护网、市级护网;除此之外,还有一些行业对于网络安全的要求比较高,因此也会在行业内部展开护网行动,比如金融行业。


三、护网的时间


不同级别的护网开始时间和持续时间都不一样。以国家级护网为例,一般来说护网都是每年的7、8月左右开始,一般持续时间是2~3周。省级大概在2周左右,再低级的就是一周左右。2021年比较特殊,由于是建党100周年,所有的安全工作都要在7月之前完成,所有21年的护网在4月左右就完成了。


四、护网的影响


护网是政府组织的,会对所参与的单位进行排名,在护网中表现不佳的单位,未来评优评先等等工作都会受到影响。并且护网是和政治挂钩的,一旦参与护网的企业、单位的网络被攻击者打穿,领导都有可能被撤掉。比如去年的一个金融证券单位,网络被打穿了,该单位的二把手直接被撤职。整体付出的代价还是非常严重的。


五、护网的规则


**1、**红蓝对抗


护网一般分为红蓝两队,做红蓝对抗(网上关于红蓝攻防说法不一,这里以国内红攻蓝防为蓝本)。


红队为攻击队,红队的构成主要有“国家队”(国家的网安等专门从事网络安全的技术人员)、厂商的渗透技术人员。其中“国家队”的占比大概是60%左右,厂商的技术人员组成的攻击小队占比在40%左右。一般来说一个小队大概是3个人,分别负责信息收集、渗透、打扫战场的工作。


蓝队为防守队,一般是随机抽取一些单位参与。


2、蓝队分数


蓝队初始积分为10000分,一旦被攻击成功就会扣相应的分。每年对于蓝队的要求都更加严格。2020年以前蓝队只要能发现攻击就能加分,或者把扣掉的分补回来;但是到了2021年,蓝队必须满足及时发现、及时处置以及还原攻击链才能少扣一点分,不能再通过这个加分了。唯一的加分方式就是在护网期间发现真实的黑客攻击。


3、红队分数


每只攻击队会有一些分配好的固定的目标。除此之外,还会选取一些目标放在目标池中作为公共目标。一般来说红队都会优先攻击这些公共目标,一旦攻击成功,拿到证据后,就会在一个国家提供的平台上进行提交,认证成功即可得分。一般来说,提交平台的提交时间是9:00——21:00,但是这并不意味着过了这段时间就没人攻击了。实际上红队依然会利用21:00——9:00这段时间进行攻击,然后将攻击成果放在白天提交。所以蓝队这边需要24小时进行监守防护。


六、什么是红队?


红队是一种全范围的多层攻击模拟,旨在衡量公司的人员和网络、应用程序和物理安全控制,用以抵御现实对手的攻击。


在红队交战期间,训练有素的安全顾问会制定攻击方案,以揭示潜在的物理、硬件、软件和人为漏洞。红队的参与也为坏的行为者和恶意内部人员提供了机会来破坏公司的系统和网络,或者损坏其数据。


6.1、红队测试的意义


1. 评估客户对威胁行为的反应能力。


2. 通过实现预演(访问CEO电子邮件、访问客户数据等)来评估客户网络的安全态势。


3. 演示攻击者访问客户端资产的潜在路径。


我们认为站在红队的角度来说,任何网络安全保障任务都会通过安全检测的技术手段从寻找问题的角度出发,发现系统安全漏洞,寻找系统、网络存在的短板缺陷。红队安全检测方会通过使用多种检测与扫描工具,对蓝方目标网络展开信息收集、漏洞测试、漏洞验证。尤其是在面向规模型企业时,更会通过大规模目标侦查

等快速手段发现系统存在的安全问题,其主要流程如下:


1、大规模目标侦查


红方为了快速了解蓝方用户系统的类型、设备类型、版本、开放服务


类型、端口信息,确定系统和网络边界范围,将会通过Nmap、端口扫描与服务识别工具,甚至是使用ZMap、MASScan等大规模快速侦查工具了解用户网络规模、整体服务开放情况等基础信息,以便展开更有针对性的测试。


2、口令与常用漏洞测试


红方掌握蓝方用户网络规模、主机系统类型、服务开放情况后,将会使用Metasploit或手工等方式展开针对性的攻击与漏洞测试,其中包含:各种Web应用系统漏洞,中间件漏洞,系统、应用、组件远程代码执行漏等,同时也会使用Hydra等工具对各种服务、中间件、系统的口令进行常用弱口令测试,最终通过技术手段获得主机系统或组件权限。


3、权限获取与横向移动


红方通过系统漏洞或弱口令等方式获取到特定目标权限后,利用该主机系统权限、网络可达条件进行横向移动,扩大战果控制关键数据库、业务系统、网络设备,利用收集到的足够信息,最终控制核心系统、获取核心数据等,以证明目前系统安全保障的缺失。


红队充当真实且有动力的攻击者。大多数时候,红队攻击范围很大,整个环境都在范围内,他们的目标是渗透,维持持久性、中心性、可撤退性,以确认一个顽固的敌人能做什么。所有策略都可用,包括社会工程。最终红队会到达他们拥有整个网络的目的,否则他们的行动将被捕获,他们将被所攻击网络的安全管理员阻止,届时,他们将向管理层报告他们的调查结果,以协助提高网络的安全性。


红队的主要目标之一是即使他们进入组织内部也要保持隐身。渗透测试人员在网络上表现不好,并且可以很容易的被检测到,因为他们采用传统的方式进入组织,而红队队员是隐秘的、快速的,并且在技术上具备了规避AV、端点保护解决方案

、防火墙和组织已实施的其他安全措施的知识。


七、什么是蓝队


蓝队面临的更大挑战,是在不对用户造成太多限制的情况下,发现可被利用的漏洞,保护自己的领域。


1. 弄清控制措施


对蓝队而言最重要的,是了解自身环境中现有控制措施的能力,尤其是在网络钓鱼和电话钓鱼方面。有些公司还真就直到正式对抗了才开始找自家网络中的防护措施。


2. 确保能收集并分析数据


因为蓝队的功效基于收集和利用数据的能力,日志管理工具,比如Splunk,就特别重要了。另一块能力则是知道如何收集团队动作的所有数据,并高保真地记录下来,以便在复盘时确定哪些做对了,哪些做错了,以及如何改进。


3. 使用适合于环境的工具


蓝队所用工具取决于自身环境所需。他们得弄清“这个程序在干什么?为什么它会试图格式化硬盘?”,然后加上封锁非预期动作的技术。测试该技术是否成功的工具,则来自红队。


4. 挑有经验的人加入团队


除了工具,蓝队最有价值的东西,是队员的知识。随着经验的增长,你会开始想“我见过这个,那个也见过,他们做了这个,还做了那个,但我想知道这里是否有个漏洞。”如果你只针对已知的东西做准备,那你对未知就毫无准备。


5. 假定会有失败


提问,是通往探索未知的宝贵工具。别止步于为今天已存在的东西做准备,要假定自己的基础设施中将会有失败。


最好的思路,就是假设终将会有漏洞,没什么东

作者:网络安全在线
来源:juejin.cn/post/7255281894911606843
西是100%安全的。

收起阅读 »

培训班出来入职两个月感受分享

前言 大家好,好久不见,一晃距离上次发文章又快两个月过去了,不是我工作忙没时间写,确实是我太懒了不想写。上次发文章还是我刚入职发的心得,都是我自己真实的经历,我也偶尔来到这看看大家的评论,大家的每条评论我都看到了,因为太懒了没有一个个回复,不好意思。 关于评论...
继续阅读 »

前言


大家好,好久不见,一晃距离上次发文章又快两个月过去了,不是我工作忙没时间写,确实是我太懒了不想写。上次发文章还是我刚入职发的心得,都是我自己真实的经历,我也偶尔来到这看看大家的评论,大家的每条评论我都看到了,因为太懒了没有一个个回复,不好意思。


关于评论有很多对我上次发的文章说到的项目情况和数据库表感到怀疑,觉得我是在说谎,也有的说我就是培训机构故意瞎编文章骗人培训。针对这个我今天有空现在正好写篇文章说明下:


关于上篇文章提到的表数量真假


上篇文章是我刚入职写的,我说入职的公司数据库表将近有上千张。在这里我说声对不起,由于我那时刚入职确实不了解,老大给我在Navicat上连接了四个数据库连接,两个本地,两个线上,我当时就一个个打开大致的浏览了一下表,自己估算的这四个连接里表加起来是有将近上千张,我以为都是这个项目的表,就直接在文章里说了项目这么多表。


后来才知道本地和线上,这个项目分集团和工厂两套系统,功能大致一样也有很多不一样的,集团系统会下发指令到工厂系统,都是微服务结构,每个模块单独的服务,每个模块单独的数据库,集团和工厂数据库表加起来差不多四五百张表吧,我也不知道算不算多。下面放几张项目和数据库图片让你们看看,由于公司项目我就打码了


1689479021337.jpg


1689478827647.jpg


1689477878476.jpg


关于我入职后情况感受


在我入职这两个月时间里,我自己感觉确实有一些成长,从刚开始的恐惧,到现在的放松。刚开始给我安排的第一个业务任务就是,做一个上传Excel或PDF单子,存到minio中,然后读出单子上填写的工时和库存在页面显示出来,然后判断库存不够发邮件通知采购。我当时不知道怎么下手,领导说你这两年经验这对你来说应该不难吧?我只能自己慢慢摸索,我先chatGPT在哪搞半天怎么上传文件,领导后来看到骂了我一顿说这上传要你自己写吗?工具类都有封装的方法直接调用就存到minio了,让我想办法怎么读出单子上的数据,我百度了一天才读出单子上的数据,然后就是发邮件提醒采购,我又在那百度怎么发邮件,同事说都有的,让我看看他们代码怎么发的,我看了之后他们用kafka异步在那发邮件,我搞了好久才搞明白kafka怎么用的。总共搞了快一个星期才基本搞好,我本地用apiPost测了没问题,我就推到线上了,结果线上别人测了有问题页面数据没有显示我读出来的值,我读出来后修改了数据库值,页面刷新应该会读出来的啊,但是就是没出来,我本地测接口看数据库数据确实改了啊,为什么页面就是不变,搞了好久领导过来又骂我一顿说你看不懂代码吗?这读的缓存你看不到?让我更新数据库后清除Redis缓存,最后终于搞好了。


就这样我刚开始真的煎熬,好在领导和同事人都很好,对我帮助很大,我也慢慢的熟练写出功能了。


我现在的状况


我现在是独立负责一个模块,对这个模块也比较熟悉了,业务需求也能基本按规定时间完成了。我现在周末会在家继续学习新知识,昨天周六在家学了一天的工作流activiti7,在B站看的8个小时的视频教程搞了一天搞完了,今天准备做个小项目专门练习一下工作流。


image.png
嗯,就这样吧,我去吃饭了,吃完饭回来继续学习了。学

作者:学习编程的小江
来源:juejin.cn/post/7255968185681215525
无止境,大家一起加油

收起阅读 »

熟读代码简洁之道,为什么我还是选择屎山

web
前言 前几天我写了一篇Vue2屎山代码汇总,收到了很多人的关注;这说明代码简洁这仍然是一个程序员的基本素养,大家也都对屎山代码非常关注;但是关注归关注,执行起来却非常困难;我明明知道这段代码的最佳实践,但是我就是不那样写,因为我有很多难言之隐; 没有严格的卡口...
继续阅读 »

前言


前几天我写了一篇Vue2屎山代码汇总,收到了很多人的关注;这说明代码简洁这仍然是一个程序员的基本素养,大家也都对屎山代码非常关注;但是关注归关注,执行起来却非常困难;我明明知道这段代码的最佳实践,但是我就是不那样写,因为我有很多难言之隐;


没有严格的卡口


没有约束就没有行动,比方说eslint,eslint只能减少很少一部分屎山,而且如果不在打包机器上配置eslint的话那么eslint都可以被绕过;对我个人而言,实现一个需求,当然是写屎山代码要来的快一些,我写屎山代码能够6点准时下班,要是写最佳实践可能就要7点甚至8点下班了,没有人愿意为了代码整洁度而晚一点下班的。


没有CodeReview,CodeReview如果不通过会被打回重新修改,直到代码符合规范才能提交到git。CodeReview是一个很好地解决团队屎山代码的工具,只可惜它只是一个理想。因为实际情况是根本不可能有时间去做CodeReview,连基本需求都做不完,如果去跟老板申请一部分时间来做CodeReview,老板很有可能会对你进行灵魂三连问:你做为什么要做CodeReivew?CodeReview的价值是什么?有没有量化的指标?对于屎山代码的优化,对于开发体验、开发效率、维护成本方面,这些指标都非常难以衡量,它们对于业务没有直接的价值,只能间接地提高业务的开发效率,提高业务的稳定性,所以老板只注重结果,只需要你去实现这个需求,至于说代码怎么样他并不关心;


没有代码规约


大厂一般都有代码规约,比如:2021最新阿里代码规范(前端篇)百度代码规范


但是在小公司,一般都没有代码规范,也就是说代码都无章可循;这种环境助长了屎山代码的增加,到头来屎山堆得非常高了,之后再想去通过重构来优化这些屎山代码,这就非常费力了;所以要想优化屎山代码光靠个人自觉,光靠多读点书那是没有用的,也执行不下去,必须在团队内形成一个规范约定,制定规约宜早不宜迟


没有思考的时间


另外一个造成屎山代码的原因就是没时间;产品经理让我半天完成一个需求,老大说某个需求很紧急,要我两天内上线;在这种极限压缩时间的需求里面,确实没有时间去思考代码怎么写,能cv尽量cv;但是一旦养成习惯,即使后面有时间也不会去动脑思考了;我个人的建议是不要总是cv,还是要留一些时间去思考代码怎么写,至少在接到需求到写代码之前哪怕留个5分钟去思考,也胜过一看到需求差不多就直接cv;


框架约束太少


越是自由度高的框架越是容易写出屎山代码,因为很多东西不约束的话,代码就会不按照既定规则去写了;比如下面这个例子:
stackblitz.com/edit/vue-4a…


这个例子中父组件调用子组件,子组件又调用父组件,完全畅通无阻,完全可以不遵守单向数据流,这样的话为了省掉一部分父子组件通信的逻辑,就直接调用父组件或者子组件,当时为了完成需求我这么做了,事后我就后悔了,极易引起bug,比如说下一次这个需求要改到这一部分逻辑,我忘记了当初这个方法还被父组件调用,直接修改了它,于是就引发线上事故;最后自己绩效不好看,但是全是因为自己当初将父子组件之间耦合太深了;


自己需要明白一件事情那就是框架自由度越高,越需要注意每个api调用的方式,不能随便滥用;框架自由不自由这个我无法改变,我只能改变自己的习惯,那就是用每一个api之前思考一下这会给未来的维护带来什么困难;


没有代码质量管理平台


没有代码质量管理平台,你说我写的屎山,我还不承认,你说我代码写的不好,逻辑不清晰,我反问你有没有数据支撑


但是当代码质量成为上线前的一个关键指标时,每个人都不敢懈怠;常见的代码质量管理平台有SonarQubeDeepScan,这些工具能够继承到CI中,成为部署的一个关键环节,为代码质量保驾护航;代码的质量成为了一个量化指标,这样的话每个人的代码质量都清晰可见


最后


其实看到屎山代码,每一个人都应该感到庆幸,这说明有很多事情要做了,有很多基建可以开展起来;推动团队制定代码规约、开发eslint插件检查代码、为框架提供API约束或者部署一个代码质量管理平台,这一顿操作起

作者:蚂小蚁
来源:juejin.cn/post/7255686239756533818
来绩效想差都差不了;

收起阅读 »

程序员提高效率的办法

最重要的-利用好工具 🔧 工欲善其事必先利其器,利用好的知识外脑来帮助自己,学会使用AI大模型 比如Chagpt等; http://www.dooocs.com/chatgpt/REA… 1. 早上不要开会 📅 每个人一天是 24 小时,时间是均等的,但是时间...
继续阅读 »

最重要的-利用好工具 🔧


工欲善其事必先利其器,利用好的知识外脑来帮助自己,学会使用AI大模型 比如Chagpt等;
http://www.dooocs.com/chatgpt/REA…


1. 早上不要开会 📅


每个人一天是 24 小时,时间是均等的,但是时间的价值却不是均等的,早上 1 小时的价值是晚上的 4 倍。为什么这么说?


因为早晨是大脑的黄金时间,经过一晚上的睡眠,大脑经过整理、记录、休息,此时的状态是最饱满的,适合专注度高的工作,比如编程、学习外语等,如果把时间浪费在开会、刷手机等低专注度的事情上,那么就会白白浪费早上的价值。


2. 不要使用番茄钟 🍅


有时候在专心编程的时候,会产生“心流”,心流是一种高度专注的状态,当我们专注的状态被打破的时候,需要 15 分钟的时候才能重新进入状态。


有很多人推荐番茄钟工作法,设定 25 分钟倒计时,强制休息 5 分钟,之后再进入下一个番茄钟。本人在使用实际使用这种方法的时候,经常遇到的问题就是刚刚进入“心流”的专注状态,但番茄钟却响了,打破了专注,再次进入这种专注状态需要花费 15 分钟的时间。


好的替换方法是使用秒表,它跟番茄钟一样,把时间可视化,但却是正向计时,不会打破我们的“心流”,当我们编程专注度下降的时候中去查看秒表,确定自己的休息时间。


3. 休息时间不要玩手机 📱


大脑处理视觉信息需要动用 90% 的机能,并且闪烁的屏幕也会让大脑兴奋,这就是为什么明明休息了,但是重新回到工作的时候却还是感觉很疲惫的原因。


那么对于休息时间内,我们应该阻断视觉信息的输入,推荐:



  • 闭目养神 😪

  • 听音乐 🎶

  • 在办公室走动走动 🏃‍♂️

  • 和同事聊会天 💑

  • 扭扭脖子活动活动 💁‍♂️

  • 冥想 or 正念 🧘


4. 不要在工位上吃午饭 🥣


大脑经过一早上的编程劳累运转之后,此时的专注度已经下降 40%~50%,这个时候我们需要去重启我们的专注度,一个好的方法是外出就餐,外出就餐的好处有:




  • 促进血清素分泌:我们体内有一种叫做血清素的神经递质,它控制着我们的睡眠和清醒,外出就餐可以恢复我们的血清素,让我们整个人神经气爽:



    • 日光浴:外出的时候晒太阳可以促进血清素的分泌

    • 有节奏的运动:走路是一种有节奏的运动,同样可以促进血清素分泌




  • 激发场所神经元活性:场所神经元是掌控场所、空间的神经细胞,它存在于海马体中,外出就餐时场所的变化可以激发场所神经元的活性,进而促进海马体活跃,提高我们的记忆力




  • 激活乙酰胆碱:如果外出就餐去到新的餐馆、街道,尝试新的事物的话,可以激活我们体内的乙酰胆碱,它对于我们的“创作”和“灵感”起到非常大的作用。




5. 睡午觉 😴


现在科学已经研究表现,睡午觉是非常重要的一件事情,它可以:



  • 恢复我们的身体状态:26 分钟的午睡,可以让下午的工作效率提升 34%,专注力提升 54%。

  • 延长寿命:中午不睡午觉的人比中午睡午觉的人更容易扑街

  • 预防疾病:降低老年痴呆、癌症、心血管疾病、肥胖症、糖尿病、抑郁症等


睡午觉好处多多,但也要适当,15 分钟到 30 分钟的睡眠最佳,超过的话反而有害。


6. 下午上班前运动一下 🚴


下午 2 点到 4 点是人清醒度最低的时候,10 分钟的运动可以让我们的身体重新清醒,提高专注度,程序员的工作岗位和场所如果有限,推荐:



  • 1️⃣ 深蹲

  • 2️⃣ 俯卧撑

  • 3️⃣ 胯下击掌

  • 4️⃣ 爬楼梯(不要下楼梯,下楼梯比较伤膝盖,可以向上爬到顶楼,再坐电梯下来)


7. 2 分钟解决和 30 秒决断 🖖


⚒️ 2 分钟解决是指遇到在 2 分钟内可以完成的事情,我们趁热打铁把它完成。这是一个解决拖延的小技巧,作为一个程序员,经常会遇到各种各样的突发问题,对于一些问题,我们没办法很好的决策要不要立即完成,2 分钟解决就是一个很好的辅助决策的办法。


💣 30 秒决断是指对于日常的事情,我们只需要用 30 秒去做决策就好了,这源于一个“快棋理论”,研究人员让一个著名棋手去观察一盘棋局,然后分别给他 30 秒和 1 小时去决定下一步,最后发现 30 秒和 1 小时做出的决定中,有 90% 都是一致的。


8. 不要加班,充足睡眠 💤


作为程序员,我们可能经常加班到 9 点,到了宿舍就 10 点半,洗漱上床就 12 点了,再玩会儿手机就可以到凌晨 2 、3 点。


压缩睡眠时间,大脑就得不到有效的休息,第二天的专注度就会降低,工作效率也会降低,这就是一个恶性循环。


想想我们在白天工作的时候,其实有很多时间都是被无效浪费的,如果我们给自己强制设定下班时间,创新、改变工作方式,高效率、高质量、高密度的完成工作,那是否就可以减少加班,让我们有更多的自由时间去学习新的知识技术,进而又提高我们的工作效率,形成一个正向循环。


9. 睡前 2 小时 🛌




  1. 睡前两小时不能做的事情:



    • 🍲 吃东西:空腹的时候会促进生长激素,生长激素可以提高血糖,消除疲劳,但如果吃东西把血糖提高了,这时候生长激素就停止分泌了

    • 🥃 喝酒

    • ⛹️ 剧烈运动

    • 💦 洗澡水过高

    • 🎮 视觉娱乐(打游戏,看电影等)

    • 📺 闪亮的东西(看手机,看电脑,看电视)

    • 💡 在灯光过于明亮的地方




  2. 适合做的事情



    • 📖 读书

    • 🎶 听音乐

    • 🎨 非视觉娱乐

    • 🧘‍♂️ 使身体放松的轻微运动




10. 周末不用刻意补觉 🚫


很多人以周为单位进行休息,周一到周五压缩睡眠,周末再补觉,周六日一觉睡到下午 12 点,但这与工作日的睡眠节奏相冲突,造成的后果就是星期一的早上起床感的特别的厌倦、焦躁。


其实周末并不需要补觉,人体有一个以天为单位的生物钟,打破当前的生物钟周期,就会影响到下一个生物钟周期,要调节回来也需要花费一定时间。


我们应该要以天为单位进行休息,早睡早起,保持每天的专注度。


参考


以上大部分来源于书籍 《为什么精英都是时间

作者:dooocs
来源:juejin.cn/post/7255189463747543095
控》,作者桦泽紫苑;

收起阅读 »

如何给你的个人博客添加点赞功能

web
最近在重构博客,想要添加一些新功能。平时有看 Josh W. Comeau 的个人网站,他的每篇文章右侧都会有一个心形按钮,用户通过点击次数来表达对文章的喜爱程度。让我们来尝试实现这个有趣的点赞功能吧! 绘制点赞图标 点赞按钮的核心是 SVG 主要由两部分组...
继续阅读 »

最近在重构博客,想要添加一些新功能。平时有看 Josh W. Comeau 的个人网站,他的每篇文章右侧都会有一个心形按钮,用户通过点击次数来表达对文章的喜爱程度。让我们来尝试实现这个有趣的点赞功能吧!


image.png


绘制点赞图标


点赞按钮的核心是 SVG 主要由两部分组成:



  • 两个爱心形状 ❤️ 的 path ,一个为前景,一个为背景

  • 一个遮罩 mask ,引用 rect 作为遮罩区域


首先使用 defs 标签定义一个 id 为 heart 的爱心形状元素,在后续任何地方都可以使用 use 标签来复用这个 “组件”。


其次使用 mask 标签定义了一个 id 为 mask 的遮罩元素,通过 rect 标签设置了一个透明的矩形作为遮罩区域。


最后使用一个 use 标签引用了之前定义的 heart 图形元素作为默认的初始颜色,使用另一个 use 标签,同样引用 heart 图形元素,并使用 mask 属性引用了之前定义的遮罩元素,用于实现填充颜色的遮罩效果。


点赞动画


接下来实现随着点赞数量递增时爱心逐渐被填充的效果,我们可以借助 CSS 中 transfrom 的 translateY 属性来完成。设置最多点击次数(这里我设置为 5 次)通过 translateY 来移动遮罩的位置完成填充,也就是说,读者需要点击 5 次才能看到完整的红色爱心形状 ❤️ 的点赞按钮。


除此之外我们还可以为点赞按钮添加更有趣交互效果:



  1. 每次点击时右侧会出现『 +1 』字样

  2. 用户在点击第 3 次的时候,填充爱心形状 ❤️ 点赞按钮的同时,还会向四周随机扩散 mini 爱心 💗


这里可以用 framer-motion 来帮助我们实现动画效果。


animate([
...sparklesReset,
['button', { scale: 0.9 }, { duration: 0.1 }],
...sparklesAnimation,
['.counter-one', { y: -12, opacity: 0 }, { duration: 0.2 }],
['button', { scale: 1 }, { duration: 0.1, at: '<' }],
['.counter-one', { y: 0, opacity: 1 }, { duration: 0.2, at: '<' }],
['.counter-one', { y: -12, opacity: 0 }, { duration: 0.6 }],
...sparklesFadeOut,
])

这样就完成啦,使劲儿戳下面的代码片段试试效果:



数据持久化


想要让不同用户看到一致的点赞数据,我们需要借助数据库来保存每一个用户的点赞次数和该文章的总获赞次数。每当用户点击一次按钮,就会发送一次 POST 请求,将用户的 IP 地址和当前点赞的文章 ID (这里我使用的文章标题,可以替换为任意文章唯一标识) 存入数据库,同时返回当前的用户合计点赞次数和该文章的总获赞次数


export async function POST(req: NextRequest, { params }: ParamsProps) {
const res = await req.json()
const slug = params.slug
const count = Number(res.count)
const ip = getIP(req)
const sessionId = slug + '___' + ip

try {
const [post, user] = await Promise.all([
db.insert(pages)
.values({ slug, likes: count })
.onConflictDoUpdate({
target: pages.slug,
set: { likes: sql`pages.likes + ${count}` },
})
.returning({ likes: pages.likes }),
db.insert(users)
.values({ id: sessionId, likes: count })
.onConflictDoUpdate({
target: users.id,
set: { likes: sql`users.likes + ${count}` },
})
.returning({ likes: users.likes })
])
return NextResponse.json({
post_likes: post[0].likes || 0,
user_likes: user[0]?.likes || 0
});
} catch (error) {
return NextResponse.json({ error }, { status: 400 })
}
}

同理,当用户再次进入该页面时,发起 GET 请求,获取当前点赞状态并及时渲染到页面。


回顾总结


点赞功能在互联网应用中十分广泛,自己手动尝试实现这个功能还是挺有趣的。本文从三方面详细介绍了这一实现过程:



  • 绘制点赞图标:SVG 的各种属性应用

  • 点赞动画:framer-motion 动画库的使用

  • 数据持久化:数据库查询


如果这篇文章对你有帮助,记得点赞!


本文首发于我的个人网站 leonf

ong.me

收起阅读 »

我教你怎么在Vue3实现列表无限滚动,hook都给你写好了

web
先看成果 无限滚动列表 无限滚动列表(Infinite Scroll)是一种在网页或应用程序中加载和显示大量数据的技术。它通过在用户滚动到页面底部时动态加载更多内容,实现无缝的滚动体验,避免一次性加载所有数据而导致性能问题。供更流畅的用户体验。但需要注意在实...
继续阅读 »

先看成果


动画.gif

无限滚动列表


无限滚动列表(Infinite Scroll)是一种在网页或应用程序中加载和显示大量数据的技术。它通过在用户滚动到页面底部时动态加载更多内容,实现无缝的滚动体验,避免一次性加载所有数据而导致性能问题。供更流畅的用户体验。但需要注意在实现时,要考虑合适的加载阈值、数据加载的顺序和流畅度,以及处理加载错误或无更多数据的情况,下面我们用IntersectionObserver来实现无线滚动,并且在vue3+ts中封装成一个可用的hook


IntersectionObserver是什么



IntersectionObserver(交叉观察器)是一个Web API,用于有效地跟踪网页中元素在视口中的可见性。它提供了一种异步观察目标元素与祖先元素或视口之间交叉区域变化的方式。
IntersectionObserver的主要目的是确定一个元素何时进入或离开视口,或者与另一个元素相交。它在各种场景下非常有用,例如延迟加载图片或其他资源,实现无限滚动等。



这里用一个demo来做演示


动画.gif

demo代码如下,其实就是用IntersectionObserver来对某个元素做一个监听,通过siIntersecting属性来判断监听元素的显示和隐藏。


 const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
console.log('元素出现');
} else{
console.log('元素隐藏');
}
});
});
observer.observe(bottom);


无限滚动实现


下面我们开始动手


1.数据模拟


模拟获取数据,比如分页的数据,这里是模拟的表格滚动的数据,每次只加载十条,类似于平时的翻页效果,这里写的比较简单,
在这里给它加了一个最大限度30条,超过30条就不再继续增加了


<template>
<div ref="container" class="container">
<div v-for="item in list" class="box">{{ item.id }}</div>
</div>

</template>
<script setup lang="ts">

const list: any[] = reactive([]);
let idx = 0;

function getList() {
return new Promise((res) => {
if(idx<30){
for (let i = idx; i < idx + 10; i++) {
list.push({ id: i });
}
idx += 10
}
res(1);
});
</script>

2.hook实现


import { createVNode, render, Ref } from 'vue';
/**
接受一个列表函数、列表容器、底部样式
*/

export function useScroll() {
// 用ts定义传入的三个参数类型
async function init(fn:()=>Promise<any[] | unknown>,container:Ref) {
const res = await fn();
}
return { init }
}


执行init就相当于加载了第一次列表 后续通过滚动继续加载列表


import { useScroll } from "../hooks/useScroll.ts";
onMounted(() => {
const {init} = useScroll()
//三个参数分别是 加载分页的函数 放置数据的容器 结尾的提示dom
init(getList,container,bottom)
});

3.监听元素


export function useScroll() {
// 用ts定义传入的三个参数类型
async function init(fn:()=>Promise<any[] | unknown>,container:Ref,bottom?:HTMLDivElement) {
const res = await fn();
// 使用IntersectionObserver来监听bottom的出现
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
fn();
console.log('元素出现');
} else{
console.log('元素隐藏');

}
});
});
observer.observe(bottom);
}
return { init }
}

4.hook初始化


获取需要做无限滚动的容器 这里我们用ref的方式来直接获取到dom节点 大家也可以尝试下用getCurrentInstance这个api来获取到


整个实例,其实就是类似于vue2中的this.$refs.container来获取到dom节点容器


根据生命周期我们知道dom节点是在mounted中再挂载的,所以想要拿到dom节点,要在onMounted里面获取到,毕竟没挂载肯定是拿不到的嘛



const container = ref<HTMLElement | null>(null);
onMounted(() => {
const vnode = createVNode('div', { id: 'bottom',style:"color:#000" }, '到底了~');
render(vnode, container.value!);
const bottom = document.getElementById('bottom') as HTMLDivElement;
// 用到的是createVNode来生成虚拟节点 然后挂载到容器container中
const {init} = useScroll()
//三个参数分别是 加载分页的函数 放置数据的容器 结尾的提示dom
init(getList,container,bottom)
});

这部分代码是生成放到末尾的dom节点 封装的init方法可以自定义传入末尾的提示dom,也可以不传,封装的方法中有默认的dom


优化功能


1.自定义默认底部提示dom


async function init(fn:()=>Promise<any[] | unknown>,container:Ref,bottom?:HTMLDivElement) {
const res = await fn();
// 如果没有传入自定义的底部dom 那么就生成一个默认底部节点
if(!bottom){
const vnode = createVNode('div', { id: 'bottom',style:"color:#000" }, '已经到底啦~');
render(vnode, container.value!);
bottom = document.getElementById('bottom') as HTMLDivElement;
}
// 使用IntersectionObserver来监听bottom的出现
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
fn();
console.log('元素出现');
} else{
console.log('元素隐藏');

}
});
});
observer.observe(bottom);
}

完整代码


import { createVNode, render, Ref } from 'vue';
/**
接受一个列表函数、列表容器、底部样式
*/

export function useScroll() {
async function init(fn:()=>Promise<any[] | unknown>,container:Ref,bottom?:HTMLDivElement) {
const res = await fn();
// 生成一个默认底部节点
if(!bottom){
const vnode = createVNode('div', { id: 'bottom' }, '已经到底啦~');
render(vnode, container.value!);
bottom = document.getElementById('bottom') as HTMLDivElement;
}
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
fn();
}
});
});
observer.observe(bottom);
}
return { init }
}


<template>
<div ref="container" class="container">
<div v-for="item in list" class="box">{{ item.id }}</div>
</div>

</template>
<script setup lang="ts">
import { onMounted, createVNode, render, ref, reactive } from 'vue';
import { useScroll } from "../hooks/useScroll.ts";
const list: any[] = reactive([]);
let idx = 0;
function getList() {
return new Promise((res,rej) => {
if(idx<=30){
for (let i = idx; i < idx + 10; i++) {
list.push({ id: i });
}
idx += 10
res(1);
}
rej(0)
});
}

const container = ref<HTMLElement | null>(null);
onMounted(() => {
const vnode = createVNode('div', { id: 'bottom' }, '到底了~');
render(vnode, container.value!);
const bottom = document.getElementById('bottom') as HTMLDivElement;
const {init} = useScroll()
init(getList,container,bottom)
});

</script>
<style scoped>
.container {
border: 1px solid black;
width: 200px;
height: 100px;
overflow: overlay
}

.box {
height: 30px;
width: 100px;
background: red;
margin-bottom: 10px
}
</style>

作者:一只大加号
来源:juejin.cn/post/7255149657769066551
>
收起阅读 »

作为开发人员,如何一秒洞悉文件结构?

web
曾经在处理复杂的文件结构时感到束手无策吗?别担心,说一个真正的解决方案——JavaScript中的tree-node包。它能以一种惊人的方式展示文件和文件夹的层次结构,让你瞬间掌握复杂的项目布局。 背景 在一个新项目中,你可能会面对各种文件,包括HTML、CS...
继续阅读 »

b60632618f4042c9a5aed99a0d176157.jpeg


曾经在处理复杂的文件结构时感到束手无策吗?别担心,说一个真正的解决方案——JavaScript中的tree-node包。它能以一种惊人的方式展示文件和文件夹的层次结构,让你瞬间掌握复杂的项目布局。


背景


在一个新项目中,你可能会面对各种文件,包括HTML、CSS、JavaScript、配置文件等等。起初,你可能不清楚这些文件的具体作用和位置,感到无从下手。而随着项目的发展,文件数量可能会急剧增加,你可能会渐渐迷失在文件的迷宫中,忘记了某个文件的用途或者它们之间的关联。


正是在这样的背景下,tree-node包闪亮登场!它为你呈现出一个惊人的树状结构,展示了项目中各个文件和文件夹之间的层次关系。通过运行简单的命令,你就能立即获得一个清晰而易于理解的文件结构图。无论是文件的嵌套层级、文件之间的依赖关系,还是文件夹的组织结构,一目了然。


一键安装,瞬间拥有超能文件管理能力!


无需复杂的步骤或繁琐的设置,只需在命令提示符或终端中输入一行命令,即可全局安装tree-node包:


npm install -g tree-node-cli

震撼视觉展示


tree-node包不仅仅是文件管理工具,它能以惊人的树状结构展示方式,为你带来震撼的视觉体验。使用treee命令,它能够在屏幕上呈现令人惊叹的文件和文件夹布局。无论是开发项目还是设计项目,你都能一目了然地了解整个文件结构。


示例: 假设你的项目文件结构如下:


- src
- js
- app.js
- css
- styles.css
- theme.css
- index.html
- public
- images
- logo.png
- banner.png
- index.html
- README.md

通过执行以下命令:


treee -L 3 -I "node_modules|.idea|.git" -a --dirs-first

你将获得一个惊艳的展示结果:


.
├───src
│ ├───js
│ │ └───app.js
│ ├───css
│ │ ├───styles.css
│ │ └───theme.css
│ └───index.html
├───public
│ ├───images
│ │ ├───logo.png
│ │ └───banner.jpg
│ └───index.html
└───README.md

这个直观的展示方式帮助你迅速理解整个文件结构,无需手动遍历文件夹层级。你可以清楚地看到哪些文件和文件夹属于哪个层级,方便你快速导航和查找所需资源,你也可以在上面注释文件的作用。


自定义控制


tree-node包提供了强大的自定义功能,让你对文件结构拥有绝对掌控。只需重新执行treee命令,tree-node-cli会自动展示最新的文件结构。再通过设置参数,你可以控制显示的层级深度、忽略特定文件夹,并决定是否显示隐藏文件。


配置参数:


-V, --version             输出版本号
-a, --all-files 打印所有文件,包括隐藏文件
--dirs-first 目录在前,文件在后
-d, --dirs-only 仅列出目录
-I, --exclude [patterns] 排除与模式匹配的文件。用 | 隔开,用双引号包裹。 例如 “node_modules|.git”
-L, --max-depth <n> 目录树的最大显示深度
-r, --reverse 按反向字母顺序对输出进行排序
-F, --trailing-slash 为目录添加'/'
-h, --help 输出用法信息

例如,使用以下命令可以显示三级深度的文件结构,并排除node_modules、.idea、objects和.git文件夹,同时显示所有文件,包括以点开头的隐藏文件:(这几个配置是最常见的,我基本是直接复制粘贴拿来就用


treee -L 3 -I "node_modules|.idea|objects|.git" -a --dirs-first


  • -L 3:指定路径的级别为3级。

  • -I "node_modules|.idea|objects|.git":忽略文件夹(正则表达式匹配。.git会匹配到.gitignore)。

  • -a:显示所有文件(默认前缀有"."的不会显示,例如".bin")。

  • --dirs-first:目录在前,文件在后(默认是字母排序)。


tree-node-cli的自定义控制没有繁琐的配置和操作,只需几个简单的参数设置执行命令,你就能根据自己的需求,定制化你的文件展示方式。


灵活应对文件变动


tree-node-cli不仅可以帮助你展示当前的文件结构,还可以灵活应对文件的变动。当你新增或删除了JS文件时,只需重新执行treee命令,tree-node-cli会自动更新并展示最新的文件结构。


示例:
假设在项目中新增了一个名为utils.js的JavaScript文件。只需在终端中切换到项目文件夹路径,并执行以下命令:


treee -L 3 -I "node_modules|.idea|objects|.git" -a --dirs-first

tree-node-cli将重新扫描文件结构,并在展示中包含新添加的utils.js文件:


.
├───src
│ ├───js
│ │ ├───utils.js
│ │ └───app.js
│ ├───css
│ │ ├───styles.css
│ │ └───theme.css
│ └───index.html
├───public
│ ├───images
│ │ ├───logo.png
│ │ └───banner.jpg
│ └───index.html
└───README.md

同样,如果你删除了一个文件,tree-node-cli也会自动更新并将其从展示中移除。


总结


不管你是开发者、设计师还是任何需要处理复杂文件结构的人,tree-node包都将成为你的得力助手。它简化了文件管理手动操作过程,提供了震撼的视觉展示,让你能够轻松地理解和掌握项目的文件结构。你还有更好的文件管理方法吗,欢迎在评论区分享你对文件管理的更好方法,让我们共同探讨文件管理的最佳实践。


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

CSS实现0.5px的边框的两种方式

web
方式一 <style> .border { width: 200px; height: 200px; position: relative; } .border::before { content: ""; position: abs...
继续阅读 »

方式一


<style>
.border {
width: 200px;
height: 200px;
position: relative;
}
.border::before {
content: "";
position: absolute;
left:0;
top: 0;
width: 200%;
height: 200%;
border: 1px solid blue;
transform-origin: 0 0;
transform: scale(0.5);
}
</style>

<div class="border"></div>

方式二


<style>
.border {
width: 200px;
height: 200px;
position: relative;
}
.border::before {
position: absolute;
box-sizing: border-box;
content: " ";
pointer-events: none;
top: -50%;
right: -50%;
bottom: -50%;
left: -50%;
border: 1px solid blue;
transform: scale(0.5);
}
</style>

<div class="border"></div>
作者:很晚很晚了
来源:juejin.cn/post/7255147749360156730

收起阅读 »

基于 Tauri, 我写了一个 Markdown 桌面 App

web
本文视频地址 前言 大家好,我是小马。 去年,我开发了一款微信排版编辑器 MDX Editor。它可以自定义组件、样式,生成二维码,代码 Diff 高亮,并支持导出 Markdown 和 PDF 等功能。然而,作为一个微信排版编辑器,它的受众面比较有限,并不适...
继续阅读 »

本文视频地址


前言


大家好,我是小马。


去年,我开发了一款微信排版编辑器 MDX Editor。它可以自定义组件、样式,生成二维码,代码 Diff 高亮,并支持导出 Markdown 和 PDF 等功能。然而,作为一个微信排版编辑器,它的受众面比较有限,并不适用于每个人。因此,我基于该编辑器开发了 MDX Editor 桌面版,它支持 Mac、Windows 和 Linux,并且非常轻量,整个应用的大小只有 7M。现在,MDX Editor 桌面版已经成为我的创作工具。如果你对它感兴趣,可以在文末获取。


演示


技术选型


开发 MDX Editor 桌面 App,我使用了如下核心技术栈:




  • React (Next.js)




  • Tauri —— 构建跨平台桌面应用的开发框架




  • Tailwind CSS —— 原子类样式框架,支持深色皮肤




  • Ant Design v5 —— 使用"Tree"组件管理文档树




功能与实现


1. MDX 自定义组件


MDX 结合了 Markdown 和 JSX 的优点,它让你可以在 Markdown 文档中直接使用 React 组件,构建复杂的交互式文档。如果你熟悉 React,你可以在 "Config" 标签页中自定义你的组件;如果你不是一个程序员,你也可以基于现有模板进行创作。例如,模板中的 "Gallery" 组件实际上就是一个 "flex" 布局。


代码



function Gallery({children}) {

return <div className="flex gallery">

{children}

</div>


}


文档写作


预览效果


2. 深色皮肤


对于笔记软件来说,深色皮肤已经成为一个不可或缺的部分。MDX Editor 使用 Tailwind CSS 实现了深色皮肤。



3. 多主题


编辑器内置了 10+个文档主题和代码主题,你可以点击右上方的设置按钮进行切换。



4. 本地文件管理


桌面 App 还支持管理本地文件。你可以选择一个目录,或者将你的文档工作目录拖入编辑器,便能够实时地在编辑器中管理文档。



当我在开发这个功能之前,我曾担心自己不熟悉 Rust,无法完成这个功能。但是,熟悉了 Tauri 文档之后,我发现其实很简单。Tauri 提供了文件操作的 API,使得我们不需要编写 Rust 代码,只需要调用 Tauri API 就能完成文件管理。


import { readTextFile, BaseDirectory } from '@tauri-apps/api/fs';

// 读取路径为 `$APPCONFIG/app.conf` 的文本文件

const contents = await readTextFile('app.conf', { dir: BaseDirectory.AppConfig });


文档目录树采用了 Ant Design 的 Tree 组件实现,通过自定义样式使其与整体皮肤风格保持一致,这大大减少了编码工作量。


5. 文档格式化


在文档写作的过程中,格式往往会打断你的创作思路。虽然 Markdown 已经完全舍弃了格式操作,但有时你仍然需要注意中英文之间的空格、段落之间的空行等细节。MDX Editor 使用了 prettier 来格式化文档,只需按下 command+s 就能自动格式化文档。



最后


如果你对这个编辑器感兴趣,可以在 Github 下载桌面版体验。如果你对实现过程感兴趣,也可以直接查看源码。如果您有任何好的建议,可以在上面提出 Issues,或者关注微信公众号 "JS

作者:狂奔滴小马
来源:juejin.cn/post/7255189463746986039
酷" 并留言反馈。

收起阅读 »

一名4年前端打工仔的年中总结

2023年中总结 时光飞逝,感觉只是一转眼,2023年进度条已过半,相信各位掘友也会这样觉得吧。回顾过去的半年,我都做些什么事情呢?今天就给各位分享一下我的年中总结,分为两部分来总结(工作 和 个人)。 工作 2023年年初,各位应该大部分人都是新冠初愈吧,反...
继续阅读 »

2023年中总结


时光飞逝,感觉只是一转眼,2023年进度条已过半,相信各位掘友也会这样觉得吧。回顾过去的半年,我都做些什么事情呢?今天就给各位分享一下我的年中总结,分为两部分来总结(工作 和 个人)。


工作


2023年年初,各位应该大部分人都是新冠初愈吧,反正我是,也不再疫情管控了,本以为好日子来了。然而...

两极反转


网络上大厂裁员,裁线,减产...,各种消息铺天盖地,再加上AI遍地开花的冲击,有人为了博眼球,蹭热度直接搞出了什么前端已死,唯恐天下不乱的言论,本就内卷的互联网行业变得更加内卷。


个人建议有看这些混淆视听文章的时间,还不如想想怎么学点东西,想想怎么搞钱!


笔者上半年工作是这样的:



  1. 带不来成就感的项目

  2. 狭窄到看不到的上升空间

  3. 频繁的出差

  4. 1薪都没有的年终(22年还有2薪)

  5. 非常好的领导的离开

  6. 当然,没有出过意外的加薪也没有了

  7. 团队获奖——“王牌战队”


我为啥不换工作?哈哈,怪自己借口太多。


个人


在工作中,不难看出几乎不能带给我什么技术上或者其他方面能力的成长了,那只有通过自己去学习了。这半年我做了以下几件事情:


掘金专栏


✅ 完成了[我的专栏——前端需要掌握的设计模式]。(juejin.cn/column/7195…)
image.png


音乐小项目


✅ 为了熟悉Vue3 CompositionAPI以及小程序,我用业余时间完成了一个小的音乐项目,PC端


image.png


前端性能优化学习


✅ 还学习了前端性能优化,整理了一些笔记到个人的仓库
image.png
我的博客


Vue3源码学习


✅ 除了以上这些,我还在学习Vue3的源码,都在进阶Vue.js中,还在努力更新中。
image.png


参与金石计划分奖金


✅ 参与掘金的金石计划获得200+奖金,虽然不多,但是还是挺开心的。
image.png


最近参加的一次:
image.png


和对象打赌


✅ 除此之外还和对象打了一个赌,对象天天念叨减肥,我说如果在年底能够从120 -> 105斤,我就转10000🧧给她,否则明年就去领证😁,不知道会是怎么样,期待吧!反正都不亏。


总结


总的来说,2023上半年对我而言还算是充实的吧。在个人方面,我通过掘金文章专栏、个人音乐项目、前端性能优化学习以及Vue3源码学习,开拓了自己的知识与技能,养成持续学习的习惯很重要。在工作方面,虽然没有挑战、加薪。但是把出差当成公费旅游还行。


后续的规划:



  1. 对象工资已经比我高了,金9银10搏一搏,看能不能变🏍。

  2. 持续学习、多参与到开源
    作者:Lvzl
    来源:juejin.cn/post/7255189526775644215
    项目。

收起阅读 »

用Echarts打造自己的天气预报!

web
前言 最近刚刚学习了Echarts的使用,于是想做一个小案例来巩固一下。项目效果如下图所示: 话不多说,开始进入实战。 创建项目 这里我们使用vue-cli来创建脚手架: vue create app 这里的app是你要创建的项目的名称,进入界面我们选择安装...
继续阅读 »

前言


最近刚刚学习了Echarts的使用,于是想做一个小案例来巩固一下。项目效果如下图所示:


0.png


话不多说,开始进入实战。


创建项目


这里我们使用vue-cli来创建脚手架:
vue create app


这里的app是你要创建的项目的名称,进入界面我们选择安装VueRouter,然后就可以开始进行开发啦。


页面自适应实现


我们这个项目实现了一个页面自适应的处理,实现方式很简单,我利用了一个第三方的库,可以将项目中的px动态的转化为rem,首先我们要安装一个第三方的库
npm i lib-flexible
安装完成后,我们需要在 main.js中引入
import 'lib-flexible/flexible'
还要在项目中添加一个配置文件postcss.config.js,文件内容如下:


module.exports = {
plugins: {
autoprefixer: {},
"postcss-pxtorem": {
"rootValue": 37.5,
"propList": ["*"]
}
}
}

上述代码是一个 PostCSS 的配置示例,用于自动添加 CSS 属性的前缀和将像素单位转换为 rem 单位。


其中



  • autoprefixer 是一个 PostCSS 插件,用于根据配置的浏览器兼容性自动添加 CSS 属性的前缀,以确保在不同浏览器中的兼容性。

  • postcss-pxtorem 是另一个 PostCSS 插件,用于将像素单位转换为 rem 单位,以实现页面在不同设备上的自适应效果。在上述配置中,rootValue 设置为 37.5,这意味着 1rem 会被转换为 37.5px。propList 设置为 ["*"] 表示所有属性都要进行转换。


这样,我们在项目中任何一个地方写px,都会动态的转化成为rem,由于rem是一个中相对于根元素字体大小的CSS单位,可以根据根元素的字体大小进行动态的调整,达到我们一个也买你自适应的目的。


实时时间效果实现


在项目的左上角有一个实时显示的时间,我们是如何做到的呢?首先我们在数据源中定义一个loalTime字段,用来装我们的时间,然后可以通过 new Date() 函数返回当前的时间对象,但这个对象我们是无法直接使用的,需要通过toLocaleTimeString() 函数处理,将 Date 对象转换为本地时间的格式化字符串。


methods{
getLocalTime() {
return new Date().toLocaleTimeString();
},
}

仅仅是这样的话,我们获取的时间是不会动的,怎么让他动起来呢,答案是使用定时器:


created() {
setInterval(() => {
this.localTime = this.getLocalTime();
}, 1000);
},

我们使用了一个setInterval定时器函数,让他每秒钟触发一次,然后将返回的时间赋值给我们的数据源中的localTime,同时将他放在created这个生命周期中,确保一开始就能运行,这样,我们就得到了一个可以随当前时间变化的时间。


省市选择组件实现


这个功能自己实现较为麻烦,我们选择使用第三方的组件库,这里我们选择的是Vant,这是一个轻量级,可靠的移动端组件库,我们首先需要安装他


npm i vant@latest-v2 -S


由于我们使用Vue2进行开发,所以需要指定其版本,然后就是导入所以有组件:


import Vant from 'vant'; 
import 'vant/lib/index.css';
Vue.use(Vant);

由于我们只是在本地开发,所以我们选择导入所有组件,在正式开发中可以选择按需引入来达到性能优化的目的。


准备工作完毕,导入我们需要的组件:


<van-popup v-model="show" position="bottom" :style="{ height: '30%' }">
<van-area
title="标题"
:area-list="areaList"
visible-item-count="4"
@cancel="show = false"
columns-num="2"
@confirm="selectCity"
/>

</van-popup>

这里我们通过show的值来控制的组件的显示与否,点击确认按钮后,会执行selectVCity方法,该方法会将我们选择的省市返回,格式为一个包含地区编码和地区名称的一个对象数组。


天气信息的获取


我们获取天气的信息主要依靠高德地图提供的api来实现,高德地图为我们提供了很多丰富的地图功能,包括了实时天气和天气预报功能,首先我们要注册一下,成为开发者,并获取自己的密钥和key。


最后在index.html中引入:


<script type="text/javascript">
window._AMapSecurityConfig = {
securityJsCode: '你的密钥',
}
</script>
<script type="text/javascript" src="https://webapi.amap.com/maps?v=2.0&key=你的key"></script>

就可以进行开发了。我们首先需要在项目开始加载的时候显示我们当地的信息,所以需要获取我们的当前所处环境的IP地址,所以高德也为我们提供了方法:


initMap() {
let that = this;
AMap.plugin("AMap.CitySearch", function () {
var citySearch = new AMap.CitySearch();
citySearch.getLocalCity(function (status, result) {
if (status === "complete" && result.info === "OK") {
// 查询成功,result即为当前所在城市信息
// console.log(result.city);
that.getWeatherData(result.city);
}
});
});
},

通过AMap.CitySearch插件我们可以很容易的获取到我们当前的IP地址,然后将我们获取到的IP地址传入到getWeatherData() 方法中去获取天气信息,需要注意的是,因为要求项目一启动就获取信息,所以这个方法也是需要放在created这个生命周期中的。然后就是获取天气信息的方法:


getWeatherData(cityName) {
let that = this;
AMap.plugin("AMap.Weather", function () {
//创建天气查询实例
var weather = new AMap.Weather();

//执行实时天气信息查询
weather.getLive(cityName, function (err, data) {
console.log(err, data);
that.mapData = data;
});

//执行实时天气信息查询
weather.getForecast(cityName, function (err, data) {
that.futureMapData = data.forecasts;
console.log(that.futureMapData);

// 每天的温度
that.seriesData = [];
that.seriesNightData = [];
data.forecasts.forEach((item) => {
that.seriesData.push(item.dayTemp);
that.seriesNightData.push(item.nightTemp);
});

that.$nextTick(() => {
that.initEchart();
});
});
});
},

通过这个方法,我们只需要传入城市名就可以很轻松的获取到我们需要的天气信息,并同步到我们的数据源中,然后将其渲染到页面中去。


数据可视化的实现


面对一堆枯燥的数据,我们很难提起兴趣,这时候,数据可视化的重要性就体现出来了,数据可视化是指使用图表、图形、地图、仪表盘等可视化工具将大量的数据转化为具有可读性和易于理解的图像形式的过程。通过数据可视化,可以直观地呈现数据之间的关系、趋势、模式和异常,从而帮助人们更好地理解和分析数据。


而Echarts就是这样一个基于 JavaScript 的开源可视化图表库,里面有非常多的图表类型可供我们使用,这里我们使用比较简单的折线统计图来展示数据。


首先也是安装依赖


npm i echarts


然后就是在项目中引入


import * as echarts from "echarts";


然后就可以进行开发啦,现在页面中准备好一个容器,方便承载我们的图表


<div class="echart-container" ref="echartContainer"></div>


然后就是根据我们获取到的数据进行绘制:


initEchart() {
// 基于准备好的dom,初始化echarts实例
let myChart = echarts.init(this.$refs.echartContainer);

// 绘制图表
let option = {
title: {
text: "ECharts 入门示例",
},
tooltip: {},
xAxis: {
data: ["今天", "明天", "后天", "三天后"],
axisTick: {
show: false,
},
axisLine: {
lineStyle: {
color: "#fff",
},
},
},
yAxis: {
min: "-10",
max: "50",
interval: 10,
axisLine: {
show: true,
lineStyle: {
color: "#fff",
},
},
splitLine: {
show: true,
lineStyle: {
type: "dashed",
color: ["red", "green", "yellow"],
},
},
},
series: [
{
name: "白天温度",
type: "line",
data: this.seriesData,
},
{
name: "夜间温度",
type: "line",
data: this.seriesNightData,
lineStyle: {
color: "red",
},
},
],
};
myChart.setOption(option);
},

一个图表中有非常多的属性可以控制它的不同形态,具体的不过多阐述,可以查看Echarts的参考文档,然后我们就得到一个非常美观的折线统计图。同时不能忘记和省市区选择器进行联动,当我们切换省市的时候,手动触发一次绘制,并且将我们选择的城市传入,这样,我们就得到了一个可以实时获取全国各地天气的小demo。


以上就是主要功能的具体实现方法:代码地址


作者:严辰
来源:juejin.cn/post/7255161684526940220
>欢迎大家和我交流!

收起阅读 »

通过调试技术,我理清了 b 站视频播放很快的原理

web
b 站视频播放的是很快的,基本是点哪就播放到哪。 而且如果你上次看到某个位置,下次会从那个位置继续播放。 那么问题来了:如果一个很大的视频,下载下来需要很久,怎么做到点哪个位置快速播放那个位置的视频呢? 前面写过一篇 range 请求的文章,也就是不下载资源的...
继续阅读 »

b 站视频播放的是很快的,基本是点哪就播放到哪。


而且如果你上次看到某个位置,下次会从那个位置继续播放。


那么问题来了:如果一个很大的视频,下载下来需要很久,怎么做到点哪个位置快速播放那个位置的视频呢?


前面写过一篇 range 请求的文章,也就是不下载资源的全部内容,只下载 range 对应的范围的部分。


那视频的快速播放,是不是也是基于 range 来实现的呢?


我们先复习下 range 请求:



请求的时候带上 range:



服务端会返回 206 状态码,还有 Content-Range 的 header 代表当前下载的是整个资源的哪一部分:



这里的 Content-Length 是当前内容的长度,而 Content-Range 里是资源总长度和当前资源的范围。


更多关于 Range 的介绍可以看这篇文章:基于 HTTP Range 实现文件分片并发下载!


那 b 站视频是不是用 Range 来实现的快速播放呢?


我们先在知乎的视频试一下:


随便打开一个视频页面,比如这个:



然后打开 devtools,刷新页面,拖动下进度条,可以看到确实有 206 的状态码:



我们可以在搜索框输入 status-code:206 把它过滤出来:



这是一种叫过滤器的技巧:



可以根据 method、domain、mime-type 等过滤。




  • has-response-header:过滤响应包含某个 header 的请求




  • method:根据 GET、POST 等请求方式过滤请求




  • domain: 根据域名过滤




  • status-code:过滤响应码是 xxx 的请求,比如 404、500 等




  • larger-than:过滤大小超过多少的请求,比如 100k,1M




  • mime-type:过滤某种 mime 类型的请求,比如 png、mp4、json、html 等




  • resource-type:根据请求分类来过滤,比如 document 文档请求,stylesheet 样式请求、fetch 请求,xhr 请求,preflight 预检请求




  • cookie-name:过滤带有某个名字的 cookie 的请求




当然,这些不需要记,输入一个 - 就会提示所有的过滤器:



但是这个减号之后要去掉,它是非的意思:



和右边的 invert 选项功能一样。


然后点开状态码为 206 的请求看一下:




确实,这是标准的 range 请求。


我点击进度条到后面的位置,可以看到发出了新的 range 请求:



那这些 range 请求有什么关系呢?


我们需要分析下 Content-Range,但是一个个点开看不直观。


这时候可以自定义显示的列:


右键单击列名,可以勾选展示的 header,不过这里面没有我们想要的 header,需要自定义:



点击 Manage Header Columns



添加自定义的 header,输入 Content-Range:



这时候就可以直观的看出这些 range 请求的范围之间的关系:



点击 Content-Range 这一列,升序排列。


我们刷新下页面,从头来试一下:


随着视频的播放,你会看到一个个 range 请求发出:



这些 range 请求是能连起来的,也就是说边播边下载后面的部分。


视频进度条这里的灰条也在更新:



当你直接点击后面的进度条:



观察下 range,是不是新下载的片段和前面不连续了?


也就是说会根据进度来计算出 range,再去请求。


那这个 range 是完全随意的么?


并不是。


我们当前点击的是 15:22 的位置:



我刷新下页面,点击 15:31 的位置:



如果是任意的 range,下载的部分应该和之前的不同吧。


但是你观察下两次的 range,都是 2097152-3145727


也就是说,视频分成多少段是提前就确定的,你点击进度条的时候,会计算出在哪个 range,然后下载对应 range 的视频片段来播放。


那有了这些视频片段,怎么播放呢?


浏览器有一个 SourceBuffer 的 api,我们在 MDN 看一下:



大概是这样用的:



也就是说,可以一部分一部分的下载视频片段,然后 append 上去。


拖动进度条的时候,可以把之前的部分删掉,再 append 新的:



我们验证下,搜索下代码里是否有 SourceBuffer:


按住 command + f 可以搜索请求内容:



可以看到搜索出 3 个结果。


在其中搜索下 SourceBuffer:



可以看到很多用到 SourceBuffer 的方法,基本可以确认就是基于 SourceBuffer 实现的。


也就是说,知乎视频是通过 range 来请求部分视频片段,通过 SourceBuffer 来动态播放这个片段,来实现的快速播放的目的。具体的分段是提前确定好的,会根据进度条来计算出下载哪个 range 的视频。


那服务端是不是也要分段存储这些视频呢?


确实,有这样一种叫做 m3u8 的视频格式,它的存储就是一个个片段 ts 文件来存储的,这样就可以一部分一部分下载。



不过知乎没用这种格式,还是 mp4 存储的,这种就需要根据 range 来读取部分文件内容来返回了:



再来看看 b 站,它也是用的 range 请求的方式来下载视频片段:



大概 600k 一个片段:


下载 600k 在现在的网速下需要多久?这样播放能不快么?


相比之下,知乎大概是 1M 一个片段:



网速不快的时候,体验肯定是不如 b 站的。


而且 b 站用的是一种叫做 m4s 的视频格式:



它和 m3u8 类似,也是分段存储的,这样提前分成不同的小文件,然后 range 请求不同的片段文件,速度自然会很快。


然后再 command + f 搜索下代码,同样是用的 SourceBuffer:



这样,我们就知道了为什么 b 站视频播放的那么快了:


m4s 分段存储视频,通过 range 请求动态下载某个视频片段,然后通过 SourceBuffer 来动态播放这个片段。


总结


我们分析了 b 站、知乎视频播放速度很快的原因。


结论是通过 range 动态请求视频的某个片段,然后通过 SourceBuffer 来动态播放这个片段。


这个 range 是提前确定好的,会根据进度条来计算下载哪个 range 的视频。


播放的时候,会边播边下载后面的 range,而调整进度的时候,也会从对应的 range 开始下载。


服务端存储这些视频片段的方式,b 站使用的 m4s,当然也可以用 m3u8,或者像知乎那样,动态读取 mp4 文件的部分内容返回。


除了结论之外,调试过程也是很重要的:


我们通过 status-code 的过滤器来过滤除了 206 状态码的请求。



通过自定义列在列表中直接显示了 Content-Range:



通过 command + f 搜索了响应的内容:



这篇文章就是对这些调试技巧的综合运用。


以后再看 b 站和知乎视频的时候,你会不会想起它是基于 range 来实现的分段下载和播放呢?



更多调试技术可以看我的调试小册《前端调试通关秘籍》


作者:zxg_神说要有光
来源:juejin.cn/post/7255110638154072120

收起阅读 »

flutter 极简的网络请求 - Retrofit 文档记录

前言对于Retrofit插件说实话之前是不太了解的,后来偶然发现了它,感觉还是比较惊艳的。主要工作流程就是注解、生成,通过定义简化通用请求方法的繁杂工作。(ps: json_serializable、freezed 和 最新的Riverpo...
继续阅读 »

前言

对于Retrofit插件说实话之前是不太了解的,后来偶然发现了它,感觉还是比较惊艳的。主要工作流程就是注解、生成,通过定义简化通用请求方法的繁杂工作。(ps: json_serializablefreezed 和 最新的Riverpod也是类似的工作方式,但Riverpod理解要稍微复杂一些。)

优秀插件太多感觉都有点看不完了,但是一聊到能减少重()复()工()作()那高低肯定是要上车了。 (●'◡'●)

插件Git地址:retorfit.dart

一、Retrofit 文档记录

主要目的还是记录一下,方便后续使用的时候查看。

1、添加插件引用

dependencies:
dio: any
retrofit: '>=4.0.0 <5.0.0'
logger: any #for logging purpose
json_annotation: ^4.8.1

dev_dependencies:
retrofit_generator: '>=7.0.0 <8.0.0' // required dart >=2.19
build_runner: '>=2.3.0 <4.0.0'
json_serializable: ^6.6.2

2、定义请求使用

import 'package:dio/dio.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:retrofit/retrofit.dart';

part 'example.g.dart';

@RestApi(
// 请求域名
baseUrl: 'https://5d42a6e2bc64f90014a56ca0.mockapi.io/api/v1/',
// 数据解析方式,默认为json
parser: Parser.JsonSerializable,
)
abstract class RestClient {
// 标准的构建方式
// dio: 传入发起网络请求的对象
// baseUrl: 请求域名,优先级高于注解
factory RestClient(Dio dio, {String baseUrl}) = _RestClient;

// 1、添加请求方式注解,接口地址
// 2、定义返回值类型(可以是任意类型,也可以是定义好的model),请求方法名,请求参数(后面会提到)
@GET('/tasks')
Future<List<Task>> getTasks();
}

example.g.dart 是脚本生成的具体实现文件;

@RestApi(baseUrl:...) 添加注解及部分配置参数;

Future<List<Task>> getTasks(...) 定义请求的返回值、参数值、请求类型与接口地址;

这个文件请求方法配置,按规范书写就可以了。

3、执行编译脚本

# dart
dart pub run build_runner build

# flutter
flutter pub run build_runner build

// 个人更建议使用 watch 命令
// 该命令监听输入,可以实时编译最新的代码,不用每次修改之后重复使用 build 了
flutter pub run build_runner watch

4、基本使用

import 'package:dio/dio.dart';
import 'package:logger/logger.dart';
import 'package:retrofit_example/example.dart';

final logger = Logger();

void main(List<String> args) {
final dio = Dio(); // Provide a dio instance
dio.options.headers['Demo-Header'] = 'demo header'; // config your dio headers globally
final client = RestClient(dio);

client.getTasks().then((it) => logger.i(it));
}

5、更多的请求方式

  @GET('/tasks/{id}')
Future<Task> getTask(@Path('id') String id);

@GET('/demo')
Future<String> queries(@Queries() Map<String, dynamic> queries);

@GET('https://httpbin.org/get')
Future<String> namedExample(
@Query('apikey') String apiKey,
@Query('scope') String scope,
@Query('type') String type,
@Query('from') int from);

@PATCH('/tasks/{id}')
Future<Task> updateTaskPart(
@Path() String id, @Body() Map<String, dynamic> map);

@PUT('/tasks/{id}')
Future<Task> updateTask(@Path() String id, @Body() Task task);

@DELETE('/tasks/{id}')
Future<void> deleteTask(@Path() String id);

@POST('/tasks')
Future<Task> createTask(@Body() Task task);

@POST('http://httpbin.org/post')
Future<void> createNewTaskFromFile(@Part() File file);

@POST('http://httpbin.org/post')
@FormUrlEncoded()
Future<String> postUrlEncodedFormData(@Field() String hello);

6、在方法中额外添加请求头

  @GET('/tasks')
Future<Task> getTasks(@Header('Content-Type') String contentType);

-- or --

import 'package:dio/dio.dart' hide Headers;

@GET('/tasks')
@Headers(<String, dynamic>{
'Content-Type': 'application/json',
'Custom-Header': 'Your header',
})
Future<Task> getTasks();

官方后续文档就不在这里复述了,下面记录一下我自己的使用方式。

二、Retrofit 个人使用

如官方文档所述,Retrofit的使用本就十分简单,这里更多的是对请求使用的归纳。

1、创建请求体

创建文件api_client.dart,该文件主要是对请求方法的编写。

...
@RestApi()
abstract class ApiClient {
factory ApiClient(Dio dio, {String baseUrl}) = _ApiClient;
// 定义请求方法
...
}

2、创建dio请求拦截

创建文件interceptor.dart,文件主要是对发起请求响应结果的通用处理,简化我们在使用过程中,重复的处理公共模块。


class NetInterceptor extends Interceptor {
NetInterceptor();

@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
// 在这里可以配置请求头,设置公共参数
final token = UserService.to.token.token;
if (token.isNotEmpty) {
options.headers['Authorization'] = token;
}
handler.next(options);
}

@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
// 预处理请求返回结果,处理通用的错误信息
// 包括不限于数据格式错误、用户登录失效、 需单独处理的额外约定错误码
Map dataMap;
if (response.data is Map) {
dataMap = response.data;
} else if (response.data is String) {
dataMap = jsonDecode(response.data);
} else {
dataMap = {'code': 200, 'data': response.data, 'message': 'success'};
}

if (dataMap['code'] != 200) {
if (dataMap['code'] == 402 || dataMap['code'] == 401) {
// _ref.read(eventBusProvider).fire(AppNeedToLogin());
}
handler.reject(
DioError(
requestOptions: response.requestOptions,
error: dataMap['message'],
),
true,
);
return;
}
response.data = dataMap['result'];
handler.next(response);
}
}

3、独立请求参数类

创建文件params.dart,文件主要目的是存放请求类型参数(毕竟官方推荐使用具体类型作为结构参数,非Map),当然也可以直接使用我们请求结果的数据模型作为请求参数(但是不是所有的方法都合适),简单的参数还是不用写这,个人感觉还是太复杂了,不够简洁。

具体没啥说的,直接使用json_serializable就可以了,当然也可以手写,这里推荐一下 VS Code插件 - Dart Data Class Generator,直接定义好属性之后直接通过提示扩展对应的方法就好了。

import 'package:json_annotation/json_annotation.dart';
part 'params.g.dart';

@JsonSerializable()
class TokenParams {
@JsonKey(name: 'client_id')
final String clientId;

TokenParams(this.clientId);
factory TokenParams.fromJson(Map<String, Object?> json) =>
_$TokenParamsFromJson(json);
Map<String, dynamic> toJson() => _$TokenParamsToJson(this);
}

4、添加请求桥接(独立基础请求配置)

创建实际请求类repository.dart,简化实际使用


class NetRepository {
/// 独立请求体
static ApiClient client = ApiClient(
Dio(BaseOptions())
..interceptors.addAll([
LogInterceptor(
requestBody: true,
responseBody: true,
),
NetInterceptor(),
]),
baseUrl: _devDomain.host,
);

/// 如果域名不一致可以独立创建,方便区分
static ApiClient user...
static ApiClient company...

}

final _devDomain = AppDomain(
host: 'https://api.apiopen.top/api',
pcHost: 'http://www.xxx.com ',
);

// 定义域名配置,用类的形式只是为了更好的管理和使用
// 当然这里也可以直接换成枚举、常量字符串等等,看个人编写习惯
class AppDomain {
/// 接口域名
final String host;

/// 电脑端地址
final String pcHost;

/// final String host1;
/// final String host2;
/// ...

AppDomain({
required this.host,
required this.pcHost,
});
}

5、使用案例

这里是搬用上一篇 一站式刷新和加载 的使用场景,其他地方放使用类似。


@override
FutureOr fetchData(int page) async {
try {
final data = await NetRepository.client.videoList(page, 20);
await Future.delayed(const Duration(seconds: 1));
if (tag) {
endLoad(data.list as List<VideoList>, maxCount: data.total);
} else {
tag = true;
endLoad([], maxCount: data.total);
}
} catch (e) {
formatError(e);
}
}

总结

如果使用这个请求库的话,可以极大的简化我们样板式代码的书写,还是值得推荐的。 一切的一切就是都是为了更简单,也算是为了尽量少写没用的代码而努力。

毕竟不想当将军的士兵不是好士兵,不想写代码的程序猿才是好猿。 ( ̄▽ ̄)"

附Demo地址: boomcx/template_getx


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

一场关于”职责边界不明确“引起的争论

起因有人的地方就会有江湖,有江湖的地方就会有人情。程序员这个职业,虽然天天面对电脑需要足够理性,但是公司技术边界问题在IT行业经常是模糊和感性的。对于大公司而言,规范和制度可能定义得比较明确,这样的争论会相对较少;对于小公司而言,部门和团队的职责定义得没有这么...
继续阅读 »

起因

有人的地方就会有江湖,有江湖的地方就会有人情。

程序员这个职业,虽然天天面对电脑需要足够理性,但是公司技术边界问题在IT行业经常是模糊和感性的。对于大公司而言,规范和制度可能定义得比较明确,这样的争论会相对较少;对于小公司而言,部门和团队的职责定义得没有这么清晰,存在很多这种模糊的边界;人情不好,此时跨部门合作就会经常出现这种扯皮的事情。

实际案例如下:

产品提出了一个需求:网约车行业,乘客预约用车时,需要对接到预约订单的司机进行履约监控,在司机忘记履约或者履约不及时的时候,系统要能够识别,并通知司机。
团队划分:派单团队和司机团队。
派单团队:从领域划分的角度,派单侧负责给乘客找司机。找到司机后,这个司机能不能去服务这个乘客,应该属于司机团队来监控,而且这个属于对司机行为的履约监控,跟派单应该没有关系。
司机团队:从领域划分的角度,司机侧只负责司机维度的管理,不负责司机和乘客双边关系的监控,司机接到订单后,属于司机和订单双边履约关系的监控,应该还是派单团队负责。

争论

对于司机的履约监控,这本身是一个非常大的方向。由哪边的团队负责开发,也意味着后续相关的维护和开发都会有较大的工作量。

从目前领域划分的角度来看,这块其实属于模糊的边界。

  • 从司机的视角来看: 就是对司机的履约行为,进行监控。
  • 从派单的视角来看: 就是对派单后,司机和乘客的关系是否合理,进行监控。

那双方各执一词,谁也无法说服谁,最后衍生到组织结构的问题。

  • 司机侧:不能只从派单的视角来看,要从整体来看,但是这个整体是什么却是模糊的。又扯到这个需求对司机团队没有价值,对派单团队价值更大。。。
  • 派单侧:产品文档已经定义的很明确,这个属于司机侧的监控,而且这个完全属于派单后的司机行为,跟派单确实毫无关系。

当两边都争执不下时,就只能请架构部门的人来进行协调。其实架构部门的人,对两边的业务都不是熟悉,他的立场就代表那个所谓的“江湖”。

最后架构部门的人站在了司机侧的立场:

  • 派单侧要对派单结果负责,订单派完后司机能不能履约,属于派单后续的闭环行为。

这个结论最终也没有完全说服我,因为司机所有的后续结果都是派单这个底层能力支持的,这么说司机侧后续的所有监控都要派单来负责。

反思

关于边界模糊的问题,有没有更好的解决思路?

  • 人情上解决,有人的地方就有江湖。多建立良好的合作关系,从日常的工作中去努力,在力所能及的范围内多给别人帮助。
  • 制度上解决,向上推动这种模糊边界的划分问题,帮助公司建立更规范的制度,这种方式其实体现了个人能力的不足。
  • 思想上解决,放大思考,模糊的边界后续对哪个团队有更大的收益;或者说后续是否可以进一步成为团队的核心能力。

总结

基于不确定性的问题,此次自己得到经验和教训。

  • 职场上永远要保持理性,争吵不会解决问题。
  • 模糊的问题,更需要你提前去思考,为自己的团队争取利益。
  • 职场上要多去思考,慎重表达,没有思考清楚,就不要表达。
  • 更多的去关注人,表达者最重要的是要关注对象的心理。

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

搞懂ThreadLocal

前言ThreadLocal可以说是面试的常客了,虽然在日常开发中用到的次数并不多,但因为其在Handler、ActivityThread中都发挥着重要的作用,使得面试官在问其他问题的时候会顺便考查一下ThreadLocal。为了彻底理清其逻辑,这里系统的整理一...
继续阅读 »

前言

ThreadLocal可以说是面试的常客了,虽然在日常开发中用到的次数并不多,但因为其在Handler、ActivityThread中都发挥着重要的作用,使得面试官在问其他问题的时候会顺便考查一下ThreadLocal。为了彻底理清其逻辑,这里系统的整理一遍ThreadLocal结构,帮助自己理解。

一、概述

在分析ThreadLocal之前先不要看源码,我们先来大致建立起关于ThreadLocal整体的认知。

TheadLocal工具涉及到的几个类:Thread、ThreadLocal、ThreadLocalMap,对于它们之间的关系我们可以这样简单理解:每个Thread对象都拥有一个独属于自己的Map容器-ThreadLocalMap,这里我们先把它理解为HashMap,该容器的作用是存储和维护独属于本线程的值,而它的key值就是TheadLocal对象,value值就是我们需要存储的Object。

这就是ThreadLocal工具的结构,所以在ThreadLocal工具中真正重要的是ThreadLocalMap,它才是存储线程独有数据的地方。

图片出处

二、ThreadLocal是什么

看了概述之后你其实已经对ThreadLocal有了一个大致的认知了,但是仅仅这些还不够,还需要更加深入的了解ThreadLocal。

ThreadLocal,即线程的本地变量,设计目的是为了让线程中拥有属于自己的变量,主要用于线程间数据隔离,是用来解决线程安全性问题的一个工具。它相当于为每一个线程都开辟了一块内存空间,用来存储共享变量的副本,每个线程访问共享变量时只能去访问和操作自己共享变量的副本,从而避免多线程竞争同一个共享数据,保证了在多线程环境下各个线程里的变量相对独立于其他线程内的变量。

在这里所谓开辟的内存空间就是 ThreadLocalMap,共享变量就是 ThreadLocal,共享变量的副本就是存储到ThreadLocalMap中的key。

//创建一个ThreadLocal共享变量
static final ThreadLocal<String> sThreadLocal = new ThreadLocal<String>();

创建一个ThreadLocal修饰的共享变量,当线程访问该共享变量时,这个线程就会在自己的成员变量ThreadLocalMap中保存一份数据副本,多个线程操作这个变量的时候,实际是在操作自身线程本地内存里面的变量,从而起到线程隔离的作用,避免了并发场景下的线程安全问题。

三、Thread源码分析

class Thread implements Runnable {
...
ThreadLocal.ThreadLocalMap threadLocals = null;
...
}

每个线程都有一个成员变量-ThreadLocalMap,但是该变量并没有设置引用,也就是说内存并没有为它分配空间,它的引用实际是在ThreadLocal#set方法中设置的,这样的话,虽然每个Thread对象都会有一个ThreadLocalMap变量,但是只有在使用ThreadLocal工具实现线程数据隔离的时候才会实例化,不使用则不会实例化,避免了内存占用。

四、ThreadLocal源码分析

既然每个Thread对象都有一个属于自己的容器ThreadLocalMap,那么对于数据的管理无外乎添加、获取、删除,也就是就是set、get、remove,但是这些操作并不是线程直接对ThreadLocalMap进行,而是通过ThreadLocal来间接实现的,ThreadLocalMap是ThreadLocal的静态内部类

1、ThreadLocal#set()

    public void set(T value) {
//获取当前线程对象
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);//this表示当前ThreadLocal对象
else
createMap(t, value);
}

//获取thread对象的成员变量ThreadLocalMap
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}

首先会获取当前线程的变量ThreadLocalMap,如果该变量为null,那么会调用createMap方法初始化ThreadLocalMap,如果不为null,则调用ThreadLocalMap#set方法将数据存储起来。

2、ThreadLocal#get()

    public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}

private T setInitialValue() {
T value = initialValue();//initialValue方法会返回null
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}

与set方法同理,首先会获取ThreadLocalMap,根据ThreadLocalMap是否为null来进行操作。如果不为null,则根据key值-ThreadLocal对象直接从ThreadLocalMap中取值并返回。如果为null,则调用setInitialValue方法,该方法逻辑几乎和set方法相同,不同的是value值为null,所以最终返回的也是null。

从上面的方法中我们可以看到不管是set方法还是get方法,都会先获取当前的Thread对象,然后获取Thread对象的成员变量ThreadLocalMap,最终对Map进行操作,这样也就保证了所有操作都是作用在Thread对象的同一个ThreadLocalMap上。

五、ThreadLocalMap

static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;

Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}

ThreadLocal没有直接使用HashMap而是自己重新开发了一个 map,最主要的作用是让它的key为虚引用类型,这样当ThreadLocal对象销毁时,多个持有其引用的线程不会影响它的回收。 ThreadLocalMap是一个很像HashMap的数据结构,但他并没有实现 Map接口,而且它的 Entry是继承WeakReference的,也没有 next 指针,所以不存在链表。对于hash冲突,采用的是开放地址法来进行解决。 ThreadLocaMap的扩容机制也不同于HashMap,ThreadLocalMap的扩容阈值是长度的2/3,当表中的元素数量达到阈值时,不会立即进行扩容,而是会触发一次rehash操作清除过期数据,如果清除过期数据之后元素数量大于等于总容量的3/4才会进行真正意义上的扩容。

六、ThreadLocal的内存泄漏

我们都知道内存泄漏必然和对象的引用有关,先来看一下ThreadLocal的引用关系图。

image.png

Thread中的成员变量ThreadLocalMap,它里面的key指向ThreadLocal成员变量,并且是一个弱引用。

1、为什么Entry的key使用弱引用?

如果 Entry 的key为强引用,则会导致ThreadLocal对象在被创建它的线程销毁时,由于ThreadLocalMap的持有而导致ThreadLocal对象无法被回收,进而导致严重的内存泄漏问题,因此Eetry的key被声明为弱引用来避免这种问题

2、ThreadLocal弱引用下为什么会导致内存泄漏?

所谓弱引用,是指对象允许在这种引用关系存在的情况下被GC回收。

前面也说过,ThreadLocalMap中的key是一个弱引用,当ThreadLocal变量被设置为null,即此时ThreadLocal对象仅有一个弱引用-key,而没有任何外部强引用关系。发生一次系统GC后,ThreadLocal对象会被GC回收,key的引用就变成一个null,导致这部分内存永远无法被访问,造成内存泄漏的问题。因此这些value就会一直存在一条强引用链: Thread变量 -> Thread对象 -> ThreaLocalMap -> Entry -> value -> Object 无法回收,造成内存泄漏。

所以说,从ThreadLocal本身的设计来看,是一定存在内存泄漏的。有的朋友可能会说不会出现内存泄漏啊,如果线程被回收了,线程里面的成员变量也都会被回收,也就不存在内存泄漏了,这是不对的。首先,在线程执行期间,始终有一块无法访问的内存被占用。其次,我们在实际开发中多数情况下使用线程池,而线程池是重复利用的,线程池不会销毁线程,那么线程中会一直存在这种类型的value,导致内存泄漏。

image.png

3、如何避免内存泄漏

既然已经知道弱引用下内存泄漏的原因,那么解决方案也就很清晰了,将不再被使用的Entry及时从线程的ThreadLocalMap中删除,或者延长ThreadLocal的生命周期。

而删除不再使用的Entry有两种方式。

  • 主动清除:使用完ThreadLocal后,手动调用ThreadLocal#remove()方法,将Entry从ThreadLocalMap中删除。
  • 条件触发清除:当然,为了避免内存泄漏的问题,ThreadLocal也做了一些工作。ThreadLocalMap拥有自动清除机制去清除过期Entry,当调用ThreadLocalMapget()、set()对数据进行读写时,都会触发对Entry里面key为null的数据的清除。

我们也能看到系统自动清除是需要一定的触发条件的,不能完全避免内存泄漏,所以正确的做法是调用ThreadLocal#remove()主动清除。

还可以将ThreadLocal声名为private static,使它的生命周期与线程保持一致,保证一直存在与之关联的强引用。

总的来说,有两个方法可以避免内存泄漏

  1. 每次使用完ThreadLocal之后,主动调用remove()方法移除数据。
  2. 扩大成员变量ThreadLocal的作用域,把ThreadLocal声名为private static,使它无法被GC回收。这种方法虽然避免了key为null的情况,但是如果后续线程不再继续访问这个key,也就会导致这个内存一直占用不被释放,最后造成内存溢出的问题。

所以说来说去,最好的方式还是在使用完之后,调用remove方法去移除掉这个数据

七、总结

  • ThreadLocal为每一个线程创建一个ThreadLocalMap,用于存储独属于线程自己的数据。
  • ThreadLocal的设计并不是为了解决并发问题,而是解决变量在线程内部的共享问题,线程内部可以访问独属于自己的变量。
  • 因为每个线程都只会访问自己ThreadLocalMap 保存的变量,所以不存在线程安全问题。
  • 为了避免ThreadLocal造成的内存泄漏,最好在每次使用完ThreadLocal之后,主动调用remove()方法移除数据。

个人能力经验有限,文章如有错误,还望指正。


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

Git 回退到指定版本

Git
在 Git 中,我们可以使用多种方法回退代码到指定版本,包括使用 reset 命令、使用 revert 命令、使用 checkout 命令等。下面分别介绍这些方法。方法一: 使用 git r...
继续阅读 »

在 Git 中,我们可以使用多种方法回退代码到指定版本,包括使用 reset 命令、使用 revert 命令、使用 checkout 命令等。下面分别介绍这些方法。

方法一: 使用 git reset 命令

命令

git reset

命令可以将当前分支的 HEAD 指针指向指定的提交,从而回退代码到指定版本。

该命令有三种模式:--soft--mixed 和 --hard。它们的区别在于回退代码的程度不同。

  • --mixed (默认):将 HEAD 指针和暂存区都回退到指定提交,但不改变工作区的内容。
  • --soft 仅将 HEAD 指针回退到指定提交,不改变暂存区和工作区的内容。
  • --hard 将 HEAD 指针、暂存区和工作区都回退到指定提交,会丢失最新的代码修改,慎用。

示例

# 查看提交历史
git log

# 回退到指定提交(使用 --soft 模式)
git reset --soft <commit>

# 查看状态
git status

# 提交回退后的代码
git commit -m "回退到 <commit>"

# 推送到远程仓库
git push origin <branch>

我们首先使用 git log 命令查看提交历史,找到要回退到的提交的 SHA-1 值。

然后使用 git reset 命令回退代码到指定提交,

这里使用了 --soft 模式,这样暂存区和工作区的内容不会改变,只是 HEAD 指针指向了指定提交。

接着我们使用 git status 命令查看当前状态,确认回退操作是否正确。

最后,我们使用 git commit 命令提交回退后的代码,并使用 git push 命令将代码推送到远程仓库。

方法二:使用 git revert 命令

命令

git revert 命令可以将指定提交的修改反向应用到当前分支上,相当于撤销指定提交的修改。

这种方式比使用 git reset 命令更加安全,因为它不会改变提交历史,而是创建一个新的提交来撤销之前的修改。

示例

# 查看提交历史
git log

# 撤销指定提交
git revert <commit>

# 提交撤销操作
git commit -m "回退到版本 <commit>"

# 推送到远程仓库
git push origin <branch>

我们首先使用 git log 命令查看提交历史,找到要回退的提交的 SHA-1 值。

然后使用 git revert 命令撤销指定提交的修改,这样会创建一个新的提交来撤销之前的修改。

接着我们使用 git commit 命令提交撤销操作,

并使用 git push 命令将代码推送到远程仓库。

方法三:使用 git checkout 命令

命令

git checkout 命令可以将当前分支的 HEAD 指针指向指定的提交,并将工作区的内容替换成指定提交的内容。这种方式不改变提交历史,但会直接覆盖工作区的内容,慎用。

示例

# 查看提交历史
git log

# 切换到指定提交
git checkout <commit>

# 提交回退后的代码
git commit -m "回退到版本 <commit>"

# 切回到原来的分支
git checkout <branch>

# 推送到远程仓库
git push origin <branch>

我们首先使用 git log 命令查看提交历史,找到要回退的提交的 SHA-1 值。

然后使用 git checkout 命令切换到指定提交,这样工作区的内容就会被直接替换成指定提交的内容。

接着我们使用 git commit 命令提交回退后的代码,并使用 git checkout 命令切回到原来的分支。

最后,我们使用 git push 命令将代码推送到远程仓库。

改之后git push上去远程仓库的命令行 以及 报错的相关解决办法

当我们改完代码后,想要将代码推送到远程仓库时,可以使用以下命令:

# 推送当前分支到远程仓库
git push origin <branch>

其中,<branch> 表示当前分支的名称,例如 master。这个命令会将本地分支的提交推送到远程仓库,并将远程分支更新为与本地分支一致。

如果在推送代码时出现错误,可以根据错误提示进行相应的解决办法。

常见的错误及其解决办法如下:

  • error: failed to push some refs to 'git@github.com:<username>/<repository>.git':这个错误通常是由于本地分支和远程分支的提交历史不一致导致的。解决办法是先执行 git pull 命令将远程分支的代码拉取到本地,然后再执行 git push 命令推送代码。

  • error: src refspec <branch> does not match any:这个错误通常是由于本地分支不存在或者拼写错误导致的。解决办法是先执行 git branch 命令查看本地分支列表,确认分支名是否正确,如果不存在则需要先创建分支。

  • error: failed to push some refs to 'git@github.com:<username>/<repository>.git':这个错误通常是由于权限不足导致的。解决办法是确认当前用户是否有权限推送代码到远程仓库,如果没有则需要联系管理员进行授权。

总结

总之,回退代码和推送代码都是 Git 中非常常见的操作,掌握这些操作可以帮助我们更加高效地进行开发和协作。


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

我在数据中台建设和落地的一些经验总结

软件工程师罗小东,多年平台架构设计和落地经验,这里从智慧型项目、数字化项目进行数据治理建设的一些经验总结。概述针对于中小型团队和当前接触到的大部分项目来说,很少有非常大的数据治理需求,特别是互联网型的PB级数据。在大部分情况下,数据量在TB级或亿级级别较多。相...
继续阅读 »

软件工程师罗小东,多年平台架构设计和落地经验,这里从智慧型项目、数字化项目进行数据治理建设的一些经验总结。

概述

针对于中小型团队和当前接触到的大部分项目来说,很少有非常大的数据治理需求,特别是互联网型的PB级数据。在大部分情况下,数据量在TB级或亿级级别较多。相对于PPT级别的方法论,会更加注重于实际运用,为了应对这些场景,在以下几个方面进行了考虑和优化:

  • 是否真的需要建立一个Hadoop体系的数据仓库

  • 针对于中小型客户数据治理需求怎么建设

  • 怎么样针对当前的项目进行数据资源管理

  • 后期的数据治理和各个数据治理维护怎么做

在真正理解项目需求、精细化管理以及灵活选择数据治理工具和技术的基础上,能够更好地应对不同场景下的数据治理需求。不同项目不同架构,我有我思。

过程建设

许多客户都有数字化建设的需求,但不同的场景需要使用不同的技术方案,在具体的建设过程中,整理的一些思路:

  • 首先要充分了解客户的业务场景和需求,从而选择最适合的技术方案。

  • 在建设过程中,要注重数据质量和服务,以确保数据的准确性和能力体现。

  • 合理规划数据治理流程,包括数据采集、清洗、转换、存储等环节,并通过数据可视化手段展示数据治理效果,提高数据治理成效。

  • 对于不同的项目规模和预算成本,选择不同方案,优化算法和调整计算引擎,减少资源和成本。

针对不同的客户场景,规划合理的数据治理流程。

是否真的需要建立一个全套的数据仓库体系

针对于不同的场景,对于数据治理,需要根据具体场景来选择合__适的方案

前期的搭建方式

目前在搭建数据治理平台时,开始我们使用的是CDH做为数据仓库底座,通常使用Hadoop体系的数据仓库平台,并按照ODS/DWD/ADS等层级进行划分,通过Kettle/Filebeat/Sqoop等方式抽取数据进行离线计算,使用Hive做为数据仓库,我们的工程师在这块上也有多年的治理经验,计算引擎使用的偏向于Spark,数据建模和维护也是按通用的数据标准处理,这个有前期多个项目里面基本上都是,有一些项目会运行在K8S上。

这个过程消耗的资源较多,而且计算引擎和计算过程比较统一,特别是Spark计算的时候,消耗大量的内存资源。而对于一般中小型项目,或者一般的客户来说,这个资源的建设会成本过高,特别是在数据治理这块并不是要求特别高的时候。

客户数据治理成本高

一些客户可能并不理解数据治理的成本和价值,除了政务型项目或不缺费用的项目,很难落地,没有达到预期的数据运营效果。

比如一个智慧社区项目,在这块上的数据仓库主要存储的数据在几个方面,用户行为、IOT数据采集、还有视频流数据的存储(只存储主键祯数据),另外就是一些业务系统的数据采集存储,针对于以上数据的分析,与AI结合,提供出API服务能力,在这些数据中,超过一定生命周期的会做清理,最后评估出来10年左右180T存储,而这个过程中,大部分是冷数据。

最后建设使用的方案是云厂家的一体机来进行管理,但是这个成本是极高的,类似于这样的数据场景,遇到的比较多,最后在考虑一个问题,是否需要这么重的数据仓库。

针对于中小型客户数据治理需求怎么建设的

建立一个轻量级的数据场景,以更好地满足不同项目的需求。建设轻量级数据治理平台

建设轻量级数据治理平台,是优化数据管理和维护成本的方法之一。目前大数据套件较多,学习成本较高,对中小型团队而言,这一成本占比较大。因此,需要采取有效措施降低人员培训成本和管理维护成本。

将多个工具整合为轻量级数据中台,使用minio分布式存储、Clickhouse数据仓库、kettle抽取工具和kafka数据总线等技术统一数据治理,适配各类规模的企业需求。在数据清理和转换后,将数据存储到ODS层,非结构化和半结构化数据存储在分布式存储和ES中,并根据生命周期规划定期清理不必要的数据,只保留有价值的数据和流程相关数据。

此外,针对人员培训,设计系统化的培训课程和多种灵活的培训方式,以提高员工的数据管理和分析能力。对于团队管理和维护,可以建立数据治理的文化氛围,鼓励全员参与,同时引入自动化工具和脚本,减少人工操作和管理成本。

通过以上措施,项目可以建设出高效、灵活的数据治理平台,降低人员培训和管理成本,提高数据治理能力和业务价值体现 ,实现项目的业务需求和决策目标。

怎么样针对当前的项目进行数据资源管理

建设通用的数据治理能力组件和平台组件,以便根据具体项目需求进行选择和组合,实现对数据资源的有效管理。

针对当前的项目进行数据资源管理,可以建设一套通用的数据治理能力组件和平台组件。这些组件可用于多种场景下的数据治理工作,如:

  • 数据上报服务:供政务、个人、单位等通用型用户使用的通用数据采集上报平台,支持非技术型人员和部门进行数据入仓。

  • 数据总线服务:连接数据平台中不同组件和子系统的核心组件,实现数据的快速传输和交换,并统一集成数据主题管理。

  • 主数据管理服务:帮助企业确保数据质量、提高业务流程效率,并为数据分析和决策提供支持,促进企业内部数据的标准化、管理和共享。

  • 数据集成服务:提供在线设置ETL作业、转换任务的定时运行策略,监控任务的执行情况,查看任务执行日志的功能,强有力地支撑后续的数据开发、数据挖掘。

  • 数据开发服务:向数据开发工程师提供拖拉拽控件的方式,设计复杂的工作流有向无环图,挖掘出有商业价值的数据。

  • 数据安全网关:提供数据交换、数据共享、数据开放的平台,包含网关接口安全、接口权限认证、黑名单管理、Oauth2接口认证等功能,向组织内各个部门提供支持。

这些数据治理能力组件和平台组件可根据具体项目需求进行选择和组合,实现对数据资源的有效管理,我们采用灵活的数据治理方案,根据项目大小和需求,选择相应的数据治理工具和技术。

在提供工具的同时,针对于业务的个性化要求和业务开发需求,比如报表、大屏、还有数据服务使用等,当前是让ISV团队进行处理,而这个过程由中台团队提供技术支持和培训,而数据治理套件不对客户。

后期的数据治理和各个数据治理维护怎么做

建立一套完善的数据治理流程和规范,包括数据质量控制、数据安全保护、数据持续更新等方面的要求

实现数据治理和各个数据治理维护的目标,包括数据流程标准化、人员技术培训、数据指标采集等。在实际应用过程中,需要根据企业的具体需求和情况,适当调整和优化数据治理策略,以提高数据质量和效率,为项目的发展提供有力支撑。

  • 数据流程标准化:通过数据总线服务连接数据平台中的不同组件和子系统,以便实现数据的快速传输和交换,并统一集成数据主题管理。建立标准化的数据流程,包括数据采集、清洗、存储、转换等环节,并确保每个环节都符合相关标准和规范。

  • 人员技术培训:利用主数据管理服务对企业内部数据进行标准化、管理和共享,确保数据质量和提高业务流程效率。同时,为各个层次的员工提供有针对性、系统化的培训课程,提高他们的数据管理和分析能力。

  • 数据指标采集:使用数据集成服务在线设置ETL作业和转换任务的定时运行策略,监控任务的执行情况和查看任务执行日志的功能。确保多种数据格式和来源的数据经过清洗、转换后能够及时有效地送达组织的数据仓库,并为后续的数据开发和挖掘提供支持。

  • 数据治理目标达成:使用数据开发服务向数据开发工程师提供拖拉拽式的控件,设计复杂的工作流图,挖掘出有商业价值的数据,帮助企业实现对数据的全面管控和治理。同时,使用数据安全网关进行数据交换、共享和开放的管理,确保数据的安全性和防止潜在的风险。

同时实现对数据的全面管控和治理,确保数据的质量和安全,提高数据开发和分析的效率和准确性,从而更好地支撑企业的业务需求和决策,提供出数据服务和治理。

总结

数据治理是数字化建设中非常重要的一环。在进行数据治理时,我们需要根据不同的业务场景和需求,选择最适合的数据治理方案,包括选择不同的组件组装和数据存储方式等。对于轻量级数据管理平台和重量级数据管理平台,我们可以针对具体情况进行选择,权衡成本与效益,以满足客户实际需求。在整个数据治理过程中,我们还需要注重客户成本的管理,确保项目的落地和实际效果,并且不断优化数据治理流程,需要积极参与业务需求分析和技术选型,确保数据治理方案符合客户需求和行业标准。

过程考虑不同的场景选择不同的数据治理方案和组件组装,根据实际情况选择轻量级或重量级数据中台,注重客户成本管理和实际效果,以满足客户需求并推动数字中台建设。

以上为在大中小型项目中的数据治理经验输出,提供一些参考。


作者:软件工程师_罗小东
链接:https://juejin.cn/post/7238978524030861371
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

一次查找分子级Bug的经历,过程太酸爽了

bug
在软件开发的世界里,偶尔会出现一些非常隐蔽的 Bug,这时候工程师们像探险家一样,需要深入代码的丛林,寻找隐藏在其中的“幽灵宝藏”。前段时间,我和我的团队也踏上了这样一段刺激、有趣的探险之旅。最近繁忙的工作告一段落,我总算轻松下来了,想趁这个机会,跟大家分享我...
继续阅读 »

在软件开发的世界里,偶尔会出现一些非常隐蔽的 Bug,这时候工程师们像探险家一样,需要深入代码的丛林,寻找隐藏在其中的“幽灵宝藏”。前段时间,我和我的团队也踏上了这样一段刺激、有趣的探险之旅。

最近繁忙的工作告一段落,我总算轻松下来了,想趁这个机会,跟大家分享我们的这次“旅途”。

01 引子

我是 ShowMeBug 的 CEO 李亚飞,是一个古老的 Ruby 工程师。由于 2019 年招聘工程师的噩梦经历,我立志打造一个真实模拟工作场景的 IDE,用来终结八股文、算法横行的技术招聘时代。

这个云上的 IDE 引擎,我称之为轻协同 IDE 引擎——因为它不是为了繁杂重度的工作场景准备的,而是适应于大部分人的习惯、能快速上手熟悉、加载速度快、能协同(面试用)、低延迟感,让用户感受非常友好 

多环境启动与切换 

为了达成秒级启动环境的性能要求,我们设计了一套精巧的分布式文件系统架构,其核心是一个可以瞬间复制大量小文件的写时复制 (COW) 技术。IO 吞吐能达到几万人同时在线,性能绝对是它的一大优势。

我们对此信心满满,然而没想到,很快就翻车了。

02 探险启程

2023 年 1 月,北方已经白雪皑皑,而深圳却仍难以感受到冬天的寒意。

我和我的团队在几次打开文件树的某个文件时,会显得有点慢——当时没有人在意,按照常规思路,“网速”背了这个锅。事后我们复盘才发现,这个看似微不足道的小问题,其实正是我们开始这次探险之旅的起点。

1 月底,南方的寒意缓缓侵入。这时候我们的轻协同 IDE 引擎已经开始陆续支持了 Vue2、Vue3、React、Django、Rails 等框架环境,一开始表现都很棒,加载和启动速度都很快。但是,跑了一段时间,我们开始察觉,线上环境就出现个别环境(Rails 环境)启动要 20-30s 才能完成

虽然其他环境仍然保持了极快的加载和启动速度,但敏锐的第六感告诉我,不行,这一定有什么猫腻,如果不立即行动,势必会对用户体验带来很不好的影响。于是,我开始安排团队排查眼前这个不起眼的问题,我们的探险之旅正式开始。

03 初露希望

湿冷的冬季,夜已深,我和我们的团队依旧坐在电脑前苦苦探索,瑟瑟发抖。

探险之旅的第一站,就是老大难的问题:定位Bug。目前只有某一个环境启动很慢,其他的环境都表现不错。大家想了很多办法都没有想明白为什么,甚至怀疑这个环境的模板是不是有问题——但把代码放在本地启动,最多就2秒。

哎,太诡异了。我们在这里卡了至少一周时间,不断追踪代码,分析日志文件,尝试各种方案,都没有弄清楚一个正常的程序启动为什么会慢。我们一度陷入了疲惫和焦虑的情绪中。

Debug 是种信仰,只有坚信自己能找到 Bug,才有可能找到 Bug。

软件开发界一直有一个低级 Bug 定律:所有诡异的问题都来自一个低级原因。在这“山重水复疑无路”之际,我们决定重新审视我们的探险路径:为什么只有 Rails 更慢,其他并不慢?会不会只是一个非常微小的原因而导致?

这时候,恰好有一个架构师朋友来访,向我们建议,可以用 perf 火焰图分析看看 Rails 的启动过程。

perf火焰图实例

当我们用 perf 来分析时,惊讶地发现:原来 Rails 的启动要加载更多的文件! 紧接着,我们又重新用了一个文件读写监控的工具:fatrace,通过它,我们看到 Rails 每次启动需要读写至少 5000 个文件,但其他框架并不需要。

这才让我们突然意识到,会不会是文件系统读写速度不及预期,导致了启动变慢。

04 Bug现身

为了搞清楚是不是文件系统读写速度的问题,我急需一个测试 IO 抖动的脚本。我们初步估算一下,写好这个脚本需要好几个小时的时间。

夜已深,研发同学都陆续下班了。时间紧迫!我想起了火爆全球的 ChatGPT,心想,不如让它写一个试试。

测试 IO 抖动的脚本 

Cool,几乎不需要改动就能用,把代码扔在服务器开跑,一测,果然发现问题:每一次文件读写都需要 10-20ms 才能完成 。实际上,一个优秀的磁盘 IO 读写时延应该在亚毫级,但这里至少慢了 50 倍。 

Bingo,如同“幽灵宝藏”一般的分子级 Bug 逐渐显现,问题的根因已经确认:过慢的磁盘 IO 读写引发了一系列操作变慢,进而导致启动时间变得非常慢 

更庆幸的是,它还让我们发现了偶尔打开文件树变慢的根本原因,这也是整个系统并发能力下降的罪魁祸首 

05 迷雾追因

看到这里,大家可能会问,这套分布式文件系统莫非一直这么慢,你们为什么在之前没有发现?

非也,早在项目开始的时候,这里的时延是比较良好的,大家没有特别注意这个 IOPS 性能指标,直到我们后面才留意到,系统运行超过一个月时,IO 读写时延很容易就进入到卡顿的状态,表现就是文件系统所在主机 CPU 忽高忽低,重启就会临时恢复。

此时,探险之旅还没结束。毕竟,这个“幽灵宝藏”周围依旧笼罩着一层迷雾。

我们继续用 fatrace(监控谁在读写哪个 IO)监控线上各个候选人答题目录的 IO读写情况,好家伙,我们发现了一个意外的情况:几乎每一秒都有一次全量的文件 stats 操作 (这是一个检测文件是否有属性变化的 IO 操作)!

也就是说,比如有 1000 个候选人正在各自的 IDE 中编码,每个候选人平均有 300 个文件,就会出现每秒 30 万的 IO 操作数!

我们赶紧去查资料,根据研究数据显示,一个普通的 SSD 盘的 IOPS 最高也就到 2-3 万 。于是,我们重新测试了自己分布式文件系统的 IOPS 能力,结果发现也是 2-3 万 。

那这肯定远远达不到我们理想中的能力级别。

这时,问题更加明确:某种未知的原因导致了大量的 IOPS 的需求,引发了 IO 读写时延变长,慢了大约几十倍

06 接近尾声

我和我的团队继续深究下去,问题已经变得非常明确了:

原来,早在去年 12 月,我们上线一个监听文件增删的变化来通知各端刷新的功能。

最开始我们采用事件监听 (fswatch event),因为跨了主机,所以存在 1-2s 的延迟。研发同学将其改为轮询实现的方案,进而引发了每秒扫描目录的 stats 行为。

当在百人以下访问时,IOPS 没有破万,还足够应对。但一旦访问量上千,便会引发 IO 变慢,进而导致系统出现各种异常:间歇导致某些关键接口 QPS 变低,进而引发系统抖动

随着“幽灵宝藏”显露真身,这次分子级 Bug 的探险之旅也已经接近尾声。团队大 呼:这过程实在太酸爽了!

07 技术无止境

每一个程序员在成长路上,都需要与 Bug 作充足的对抗,要么你勇于探索,深入代码的丛林,快速定位,挖到越来越丰富的“宝藏”,然后尽情汲取到顶级的知识,最终成为高手;或者被它打趴下, 花费大量时间都找不到问题的根源,成为芸芸众生中的一人。

当然,程序员的世界中,不单单是 Debug。

当我毕业 5 年之后,开始意识到技术的真正价值是解决真正的社会问题。前文中我提到,由于我发现技术招聘真是一个极其痛苦的事:特别花面试官的时间,却又无法有效分析出候选人的技术能力,所以创立 ShowMeBug 来解决这个问题:用模拟实战的编程环境,解决科学评估人才的难度

这个轻协同 IDE 技术从零开发,支持协同文件树、完全自定义的文件编辑器、协同的控制台 (Console) 与终端 (Shell),甚至直接支持 Ctrl+P 的文件树搜索,不仅易于使用,又强大有力。

但是这还不够。要知道,追求技术精进是我们技术人的毕生追求。对于这个轻协同IDE,我们追求三个零:零配置、零启动、零延迟。其中,零启动就是本文所追求的极限:以最快的速度启动环境和切换环境

因此,探险之旅结束后,我们进一步改进了此文件系统,设定 raid 的多磁盘冗余,采用高性能 SSD,同时重新制定了新磁盘架构参数,优化相关代码,最终大幅提升了分布式文件系统的稳定性与并发能力。

截止本文结尾,我们启动环境的平均速度为 1.3 秒,切换环境速度进入到亚秒级,仅需要 780ms。目前在全球范围的技术能力评估赛道 (TSA) 中,具备 1-2 年的领先性

08 后记

正当我打算结束本文时,我们内部的产品吐槽群信息闪烁,点开一看:嚯,我们又发现了新 Bug。

立夏已至,我们的探险之旅又即将开始。


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

一位双非本科大二生的前端学习求职之路

心路历程在上大学之前,对自己的未来有各种展望,想着到底该选什么专业未来做什么好呢?然后抱着志愿书没日没夜的看,看别人的专业介绍视频。最后终于确定了——数字媒体技术但是双非本科,学校教的内容是真的稀碎,学习写代码,不如软工计科这些硬专业,学艺术,又不如美院专业的...
继续阅读 »

心路历程

在上大学之前,对自己的未来有各种展望,想着到底该选什么专业未来做什么好呢?然后抱着志愿书没日没夜的看,看别人的专业介绍视频。最后终于确定了——数字媒体技术

但是双非本科,学校教的内容是真的稀碎,学习写代码,不如软工计科这些硬专业,学艺术,又不如美院专业的人,对于三维来说,又没有这种设计和创新的思维。

最后加入了学校的一个信息工程学院的工作室,决定了以后当个前端码农。

学习过程

大一上学期

在刚开始便是从htmlcssJavaScript三大件开始,大概用了一个学期,起码对这三个有了稍微初步熟练的了解,在b站上看完了黑马前端的几个教程,也看了部分js高级、es6的内容。寒假开始学习Vue2,看的是codewhy在18年的视频,案例是移动端购物街的开发,当时codewhy的课程,也是非常的折磨,可能好几集里面,改bug了就占了其中一大部分。但是总归也是在大一下学期的期中左右看完了。

大一下学期

这时候学长他们有一个uniapp的外包项目,就直接拉着我开干,然后还好,uniapp和Vue2差别不是特别大,也是第一次做实战的项目,踩了很多坑,但是也算是做的差不多,但是很炸裂的是甲方跑路了,我们做了一半最后也不了了之。

大二上学期

主要是学习微信小程序开发,也做了一个课程项目,是一个购物商城。然后接了工作室两个外包项目,一个是钉钉宜搭应用开发,这部分主要是统筹其他两个同学在做,另一个是微信小程序,配合微信小程序云开发,因为要做这个,也顺带把云开发自学了,因为没什么时间学习,所以都是看着文档一点点摸索。不过最后也是独立完成了一个小应用交差了。但是甲方又拖欠,现在已经快一年了也没到账。

然后在21年十月左右,参加了阿里巴巴终端练习生计划,跟学长组了队,做的是一个清单项目,是一个跨web端、APP、小程序的一个清单类应用,完成度还蛮高的。然后我是负责用uniapp做移动端的主要功能开发,但是毕竟是清单,真正的技术再怎么难也难不到哪里去,主要是web端他们的代码比较优雅,而且技术比较新,然后被队友带着,只以几分之差,获得了第二名,还有个奖杯和证书。

1689142578324.jpg

1689142605063.jpg

寒假

一个17的学长刚好联系到我,准备做一个代码生成器,功能大概是选择字段和类型,可以自动生成假数据,也可以进行语言的转换,比如Typescript和java的。那时候想着用最新的技术来实现,突击学习了Vue3Typescript,看的是慕课网的知乎着也项目,但是内容大部分是讲的自定义组件的开发,对于当时的我来说,是真的很难啃,更何况对Vue3也不熟悉。后面没看完便在寒假开始了这个项目的开发,因为没接触过真实的项目架构,然后代码也没有拆分,基本上是一页梭哈。所以单文件代码量来到了800+,那做起来更是很难受了。虽然代码很乱,但起码也算完成了。

做完这个项目后,刚好字节青训营也开始报名了。这次是自己单打独斗去参加,当的是队长,找了几个大三和已经实习的大四大佬们一起做,当时选的题目是使用SSR的仿掘金网站,当时在选题时,因为队友基本上是React技术栈,最后便确定用Next.js做,也恰好有大佬带着,把整个项目的结构给做好了,然后我当时主要是文章详情页的开发,还有用Strapi做一些接口数据。最后也算完整的完成了。虽然有很多需要改进的地方,但是也算是运气好,拿了四等奖,还有优秀营员。

大二下学期

在下学期重新的去系统学习Vue3,因为技术不太扎实,然后也顺带学习了Typescriptpinia等等最新的技术。直到大二下学期期中才学习完。然后同时也接了国企的一个钉钉宜搭的开发,也在低代码这里,花了不少的时间,在期末也算把这个做完了。

并且在老师和工作室同学的带领下,制作了云易学教学平台,技术栈也是用到了最新的vue3ts,最主要的功能是通过websock配合后端的接口实现了在网页通过ssh控制台的操控方式,创建docker实例,比如可以在web端真正的操作MySQL数据库,这次因为有了一次开发之后,这次就熟练多了。最后通过了省赛和国赛,省赛公费去了广外,拿了省一回来。然后国赛的答辩也在几天后。

求职之路

在大二下的期末,便想着找实习,然后开始看面经。但是因为是大二的,只能实习两个月,很多公司因为这个没给我机会,投了400份后找到了一家在北京的线上实习,主要是用uniappunicloud做全栈开发,然后便在开始恶补uniclod的内容,大概花了一星期,做完了他们的笔试便入职了。但是,里面的坑是真的非常多,一来就让我做上一个实习生遗留下来的项目,然后总共有两个实习生做过这个,最后听hr说,两个实习生都离职了。那个项目做的是真的一言难尽,很多变量用拼音命名,页面的组件也不拆分,然后unicloud明明已经可以用前端拉取所有数据了,他还是用云函数来拉取数据库的内容,然后还有各种eventbus瞎用,也有两个人做过的痕迹,因为有重复的逻辑代码,项目文件也是全是first、second、third这样的命名方式。然后很多页面类似的,也是直接复制,然后在复制后的修改,用不到的也没有删除,最后整个项目跑起来,一步一个报错。我花了整整两个星期,去试图理解这坨不可名状之物,然后在这个基础上去改bug,最后也是凭借着我花足够多的时间去想办法重构,让部分bug缓解了。

不仅如此,这个公司也是让我很难评价,我遇到一个微信登录时好时坏的问题,跟我对接的技术跟我说,是很容易解决的,看文档就可以了,文档的内容就是很基础的修改一些配置,然而这些配置我早就试过了,但是我还是根据文档操作了之后,仍然会遇到时好时坏的问题。我认为这个时好时坏的问题多半是云空间或者是appid或者小程序密钥的问题,但是技术一直打太极,说文档就可以解决啦,本来是几分钟就可以解决的,都跑过好几遍啦。确实是几分钟问题,我在我这里用自己的小程序id和云空间,直接是几分钟就好了,但是遇到时好时坏的情况我真的没办法自己去解决了,更何况这个代码不是我从头开始做的,我不知道上一个人做了哪些操作,最后在这两个星期,被折磨的压力很大。而且这个项目做完之后,也只有400,期间不断的增加新需求,增加新的bug修改。让我狠狠的体会了一波社会的险恶,最后直接一分钱没拿离职了。

未来展望

现在已经是大二暑假了,之前阿里训练营的一个队友,也进入了字节实习,另一位跟我一样的大二女生,也找到了一家北京的实习,有时候真的会有很重的无力感和迷茫,不知道该何去何从,不知道如何下手。

在剩下来的一年好好沉淀吧,最近开始算法的学习,还有准备开始React的系统性学习,在接下来的时间也会在这里更新自己的算法学习,还有一些技术内容,虽然我的实力远不如掘金的大佬们,但是如果能帮助到一些同样迷茫的大学生,那我认为就有意义了吧。


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

写"简单"而不是"容易"的代码

简单 vs 容易简单 == 容易?在大多数人的第一印象中,这两个词好像是等同的,我们在很多场景下会相互替换这两个词而表达出相同的意思,比如下面这些:“高数题很简单/容易” “开车很容易/简单” “C语言写起来很简单/容易”当然 “抗原测试阳性也很容易...
继续阅读 »

简单 vs 容易

简单 == 容易?

在大多数人的第一印象中,这两个词好像是等同的,我们在很多场景下会相互替换这两个词而表达出相同的意思,比如下面这些:

“高数题很简单/容易” “开车很容易/简单” 


image

“C语言写起来很简单/容易

当然 “抗原测试阳性也很容易/简单”...

对于以上这些句子我想大家都是认同的......吧?好吧,或许前三个问题可能有不同的意见,但在第四个问题上我们应该还是能达成一致的(如果还是有疑问,也许去医院溜达一圈可以改变你的想法,即使全程佩戴口罩;))。

正如上面的句子所示,随意更换结尾的 简单 和 *容易 *两个词,好像依然可以得出一样的含义,这并不会带来理解上的偏差,所以我们可以就此推出结论 简单 == 容易 吗?或许我们可以从另一个角度再来看下这个问题

从其他语言体系中收获一些启发:

Simple:

image

在英文单词中 Simple** **的词根是sim 和 plex, 可以理解是一层,一圈,至于plex 中所代表的折叠和扭曲的含义,当只有一层或者一圈时,实际上也就是没有折叠,不扭曲了,这个词的反义词是complex,意思是编织在一起或者折叠在一起

image

因此对应到软件开发过程中,当我们追求简单的事物时,其中最关键的一点就是,我们希望它是专注于某一方面的,我们不想看到它和其他事物交织在一起,当然,这并不意味着我们需要太过追求单一一个,相反 简单* *的关键无关乎数量,更多的是追求是更少的交织,或者是没有交织,这一点才是重中之重,正如我们上面描述的一样,事物是否是交织重叠的,只要进去看下就知道了,它是客观的,是可以深入去研究的,而这种客观也是后面我们区分于 **容易 **的核心所在

Easy:

image

来源于拉丁语动词adjacere(附近放置),其现在分词为adjacens(附近的,手边的,方便的,英语adjacent的词源,adjective的间接词源),进入古法语后有名词aise(英语ease的词源),派生了动词aisier(轻松、随意放置),其过去分词aisie进入盎格鲁-诺曼底语中为aise,进入英语为easy

而 **Easy *就有趣了,其最初是来源于拉丁词 ***adjacens(附近的,手边的,方便的)一词。***靠近是一个很有意思的概念,*我们可以从下面几个方面来理解下它

  1. 物理意义上的靠近

这个东西就在你附近,触手可得,不需要骑车,或是开车去

  1. 靠近我们已知的东西,或者换个词:熟悉

对于我们来说俄语很难吗?当然是的,不过对于俄国人来说他们可能并不会这么觉得,无非是他们相对来说更熟悉罢了

  1. 靠近我们的能力

手里有一把锤子,看什么都像钉子,当我们说这个东西很容易的时候,很大程度上是因为我们已经想到了一些功能相似的东西

所以,simple** == easy** 吗?不,easy 是相对的, 弹钢琴和俄语对我来说真的很难,但是对其他人来说却很容易。不像 *simple,它是客观的,*我们总是可以进去看看,寻找是否存在重叠和交叉,而 easy 总是要问一句,对谁来说容易,对谁来说很难?

为什么需要简单,而不是容易

许多时候我们说的简单,都是从自身出发的,其实更应该用词为 容易;而真正的 **简单 **,是不和别的东西耦合的,独立的东西。多数时候我们在产品开发过程中的冲突在于,产品经理会说自己的设计是简单的(Easy),但是开发同学认为复杂(不符合自己开发的 Easy),但是却忽略了,我们真正需要的是 Simple

这样说你会发现,我们做的许多事情,往往都是从 easy 开始,而不是 simple 。 easy 开始的速度很快,但是随着项目的扩展,复杂度越来越高,速度慢慢就掉下来了 —— 想想每次重构代码的痛苦吧。而 simple 则刚开始并没有太快的速度,因为需要定义许多的东西,抽象归纳许多对象,但后续推进则是越来越快 —— 因为结构清晰构件完备,只需要理解有限的上下文就可以完成模块的修改或扩展。

因为限制

任何事情都是有限制的:

  1. 我们只能让我们理解的东西变可靠

  2. 我们只能在同时思考很少的一些事情

  3. 互相纠缠的事情我们只能把它们放在一起来思考

  4. 复杂性会降低我们的理解

我们怎么可能制造出我们不了解的可靠产品,当我们在某些系统上,想在未来使事情变得更加灵活、可扩展和动态时,我们将在理解它们的行为并确保它们正确的能力上做出权衡。但是对于我们想要确保正确的事情,我们都将受到限制,受限于对它的理解。

而且我们的理解力是很有限的,举个例子,你一次能在空中保持多少个球,或者你一次能记住多少件事?数量有限,而且数量很少,对吧?所以我们只能考虑一些事情,当事情交织在一起时,我们就失去了独立对待它们的能力。

image

image

抛球讲解:http://www.matrix67.com/blog/archiv…

因此,每次我们需要理解软件的一个新部分,并且它与另一件事相关联时,我就不得不将另一件事拉入脑海,因为我们无法在没有另一件事的情况下考虑这件事。这就是他们交织在一起的本质。因此,每一次交织都会增加这种负担,而这种负担是我们可以考虑的事物数量的组合。因此,从根本上说,这种复杂性,这种将事物编织在一起,将极大的限制我们理解系统的能力

简单带来的收益

《针织城堡》 《积木城堡》

  • 容易理解

理解代码想表达什么,而不是写的是什么

  • 容易改变

你能想象在一个针织的城堡上做改动吗?

  • 容易debug

  • 灵活性

容易代码的产生

随着时间的拉长和各种各样因素的干扰,代码慢慢就脱离了我们的掌控,当回过神来,看着日积月累的代码库,我们会发现它已大到经难以撼动了,只能寄希望于它不会哪一天突然炸开,或赶在它炸开前把它扔出去。

坏味道

image

image

读侦探小说时,透过一些神秘的文字猜测故事情节是一种很棒的体验;但如果是在阅读代码,这样的体验就不怎么好了。我们也许会幻想自己是《名侦探柯南》中的柯南,但我们写下的代码应该直观明了,代码中最重要的一环就是好的名字。

然而,很遗憾,命名是编程中最难的事情之一,所以我们需要深思熟虑如何给函数、模块、变量和类命名,使它们能清晰地表明自己的功能和用法,很多情况下我们不愿意给程序元素改名,觉得不值得费这个劲,但好的名字能节省未来用在猜谜上的大把时间。

重复代码

如果你在一个以上的地点看到相同的代码结构,那么可以肯定,设法将它们合而为一,程序会变得更好。一旦有重复代码存在,阅读这些重复的代码时你就必须加倍仔细,留意其间细微的差异。如果要修改重复代码,你必须找出所有的副本来修改。

过长函数

函数越长,就越难理解,从已有的经验中可以看出,活得最长、最好的程序,其中的函数往往都比较短。初次接触到这种代码库的程序员常常会觉得“计算都没有发生”——程序里满是无穷无尽的委托调用。但和这样的程序共处几年之后,你就会明白这些小函数的价值所在。间接性带来的好处,更好的阐释力、更易于复用、更多的选择,都是由小函数来支持的。

过长参数列表

把函数所需的所有东西都以参数的形式传递进去。这可以理解,因为除此之外就只能选择全局数据,而全局数据很快就会变成邪恶的东西。但过长的参数列表本身也经常令人迷惑,使用它的人必须小心再小心。尤其在参数末尾出现了布尔类型的参数时,更容易引起人的误解,传true或false会有什么不同吗?

发散式变化

我们希望软件能够更容易被修改——毕竟软件本来就该是“软”的。一旦需要修改,我们希望能够跳到系统的某一点,只在该处做修改。如果不能做到这一点,这可不是一个好的信号。

重复的switch

在不同的地方反复使用同样的switch 逻辑(可能是以switch/case语句的形式,也可能是以连续的if/else语句的形 式)。重复的switch的问题在于:每当你想增加一个选择分支时,必须找到所有 的switch,并逐一更新。

“所有条件逻辑都应该用多态取代,绝大多数if语句都应该被扫进历史的垃圾桶”,这不免有些矫枉过正了

过大的类

一个类过大往往是因为想利用单个类做太多事情,其内往往就会出现太多字段。一旦如此,重复代码也就接踵而至了

注释

当然,并不是说你不该写注释,之所以要在这里提到注释是因为注释往往被当做除臭剂来使用了,大多数情况下注释的大量出现是是因为代码本身已经很糟糕了

有意义的注释应该更多关注无法通过代码本身表达的内容,除了用来记述将来的打算之外,注释还可以用来标记你并无十足把握的区域。你可以在注释里写下自己“为什么做某某事”,这类信息可以帮助将来的修改者理解代码所没有表达出来的细节

保持简单的代码

 

什么是重构

如何保证代码不随着时间和迭代而慢慢腐坏呢? 恐怕没有比重构更有效的方法了吧,但是重构一词近些年被用的太广泛了,很多人用“重构”这个词来指代任何形式的代码清理,但实际上用“结构调整”来泛指对代码库进行的各种形式的重新组织或清理更准确一点,而重构则是特定的一类“结构调整”,整体而言,经过重构之后的代码所做的事应该与重构之前大致一样:

 

重构(名词):对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本

重构(动词):使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构

也就是说,如果你在重构过程中发现了一个bug,那么重构之后它应该依然存在(当然,你可以在重构后顺手就修改掉它),同样,我们也需要把它跟性能优化也区分下开,他们两个也很像,都是在不改变程序的可观测行为下做的改动,两者的差别在于,重构的目的是为了让代码更容易理解和更容易修改,最终可能使程序更快了,也可能更慢了,而性能优化时,我们只关注让程序运行的更快,最终可能使得代码变得更难以理解和维护了,当然这点也是在我们预期之内的。

为何要重构

 

重构并不是万能药,它只是一种工具,帮助我们达到以下目的方式中的一种

▪  保持软件的设计

如果没有重构,程序的内部设计会逐渐腐败,当我们经常只为了短期目的而修改代码时,往往会忽略掉或者说没有理解整体的设计,于是代码会逐渐失去其结构,我们会越来越难以通过阅读代码来理解原来的设计,而随着代码结构的流失,我们也将越来越难以维护其设计意图,导致更快的代码腐败,所以,经常性的重构有助于我们维护代码应有的形态

▪ 使软件更容易理解

机器并不关心程序的样子,它只是按照我们的指示精确执行罢了,但别忘记了,除了我们自己和机器外,代码还有其他的读者,几个月后可能会有另一个程序员尝试读懂你的代码并做一些修改,他才是重要的那一个,相比于机器是否多消耗一个时钟周期,如果一个程序员需要花费一周时间才能读懂你的代码并修改才更要命呢。

重构可以帮我们让代码更易读。开始进行重构前,代码可以正常运行,但结构不够理想。在重构上花一点点时间,就可以让代码更好地表达自己的意图——更清晰地说出我想要做的。

▪ 提高编程速度

归结到最后其实可以总结为一点:重构帮我们更快速地开发程序

我们很容易得出这些好处:改善设计、提升可读性、减少bug,这些都是在提高质量。但花在重构上的时 间,难道不是在降低开发速度吗?

我们在软件开发中经常能碰到这种场景,一个团队,一开始的迭代和进展都很快,但是如今想要添加一个新的功能需要的时间就长的多了,bug排查和修复也越来越慢,代码库一个补丁摞一个补丁,需要细致的“考古”工作才能搞懂系统是如何工作的,这些负担会不断的拖累我们的开发速度,直到最后恨不得重写系统。

 

有些团队则不同,他们添加新功能的速度越来越快,因为他们可以利用已有的功能快速构建新功能,两者

的主要区别就在于软件的内部质量,良好的内部质量可以让我们轻易找到在哪里以及如何修改,良好的模块划分可以使我们只需要理解代码的一小块就可以做出修改,引入bug的可能性也会变小,即使有了bug,我们也可以很快的找出来并修复掉,最终我们的代码会演变成一个平台,在其上,我们可以很容易的构造其领域相关的新功能

 

 

何时重构

▪ 添加新功能

重构的最佳时机就在添加新功能之前,当要添加一个功能时,我们一般都会先看下代码库中已有的内容,此时经常可以发现,有些函数或者代码只要稍微调整下结构,就能使我们添加新功能变得更容易,可能只是一个函数的参数不太一样或是代码中一些字面量不太一样,如果不先重构,就只能把这段代码复制过来,修改几个值,这样就产生了重复的代码,更麻烦的是,一旦后续需要调整这块逻辑,我就需要同时修改这两处(希望我还能想起来有两处需要修改)

就好像要去东边的上海,你不一定会直接向东开,而是先向北开去上高速,后者会使你更快到达目的地。

▪ 使代码更易懂

在做改动前你需要先先理解代码在做什么,然后才能修改,这段代码可能是自己写的,也可能是别人写的,一旦你需要花费很大精力思考代码在做什么时,这就是一个好的时机了,“这个变量名称代表这个意思”,“这段逻辑是这样工作的”......我们通常无法把太多的细节长时间留存在脑海里,为什么不把他转移到代码本身,使其一目了然呢?如果把对代码的理解植入代码中,这份知识会保存得更久,其他人也能获得同等的收益。

不仅如此,长远来看的话,当代码变得更易理解时,我们通常能够看到之前设计中没有看见的问题,如果没有做前面的重构,也许我们永远也看不到这些设计问题,因为我们不够聪明,无法在脑海中推演所有的变化。

▪ 有计划的重构

上面的重构时机都是夹杂在我们日常的功能开发中的,重构本身就是我们开发新功能或者修复bug的一环,但有时候,因为快速的功能迭代而忽视了代码的设计,问题就会在某些区域逐渐累积长大,最终需要专门花些时间来解决,但这种有计划的重构应该很少,大部分重构应该是不起眼的、见机行事的。

▪ 何时不应该重构

虽然我们提倡重构,但确实有一些不值得重构的情况。

如果看见一块凌乱的代码,但并不需要修改它,那么就不需要重构它。 如果丑陋的代码能被隐藏在一个函数或者API之下,我们可以暂时容忍它继续保持丑陋。只有当我们需要理解其工作原理时,对其进行重构才有价值。

另一种情况是,如果重写比重构还容易,就别重构了,当然在这之前我们总是需要花些时间尝试的。

重构的挑战

 

▪ 延缓新功能开发

从上面的讨论中其实我们已经得到这个问题的答案了,重构的意义不在于炫技或是把代码库打磨的闪闪发光,而是纯粹从经济角度出发的考量。我们之所以重构,因为它能让我们更快,添加功能更快,修复bug更快。如果有人告诉我们“重构会拖慢进度”,我们应该坚信,他说的一定是别的东西,而不是重构,重构的唯一目的就是让我们开发更快,用更少的工作量创造更大的价值

▪ 代码归属

很多重构可能不仅涉及一个模块的改动,同时也会影响其他一些模块或者系统,代码的所有权边界会妨碍我们的一些重构,例如修改某个模块暴露出去的函数命名,不仅实现方需要修改,调用方也需要修改,尤其是涉及到暴露给其他团队的api,情况可能会更复杂,很有可能根本不知道调用法有哪些

当然这并不会完全阻止我们,只是受到很多限制罢了,比如我们可以同时保留老的函数签名和api,使其内部调用重构后的实现,等到确认所有调用方都修改后,再删除老的声明,当然也可能选择永久保留老的声明。

▪ 分支合并

大多数情况下,我们都是多个人同时维护一个代码仓库的,现代便利的仓库分支管理工具很好的支持了我们的团队协作,使我们可以更快的完成产品的开发,我们通常会从主干上拉取一个功能分支进行开发,直到功能上线时才会合并会主干,以保证主干不被功能分支所影响,这存在一个问题,我们的功能分支存在的时间越久,和主干的差异就会越大,尤其是当多个人同时在进行不同的功能分支开发时情况会更加复杂,当你因为重构修改了一个函数的命名,而其他人在新加代码中又使用了它,当代码集成时,问题就来了,而且随着功能分支存在时间的增加,这种痛苦也会不断的增加。

▪ 测试

重构的一个重要特征就是--不会改变程序可观察的行为,我们不能寄希望于“只要我足够小心,就不会破坏任何东西”,但万一我们犯了个错误怎么办?或许应该把万一两个字去掉,人总是会犯错误的,关键是在于如何快速的发现错误,要做到这一点,我们就需要一套能够快速测试的代码组件,所以大多数情况下如果我们想要重构,我们就需要先有可以自测试的代码,一旦能够自测试,我们就可以使用很小的步子进行前进,一旦测试失败,我们只需要执行退回到上一次可以成功运行的状态即可。

▪ 遗留代码

大多数情况下,有一大笔遗产是件好事,但从程序员的角度来看就不同了。遗留代码往往很复杂,测试又不足,而且最关键的是,是别人写的。重构能很好的帮助我们理解系统,理顺代码的逻辑,但是关键点在于遗留系统多半没测试。如果你面对一个庞大而又缺乏测试的遗留系统,很难安全地重构清理它。

对于这个问题,显而易见的答案就是“没测试就加测试”,说起来轻松,做起来可就不轻松了,一个系统只有在一开始设计时就考虑到了测试,添加测试才会容易,可要是如此的话系统早就该有测试了,还需要现在才开始加吗?

但是,无论如何,就像《整洁代码之道》中所说的那样:“让营地比你来时更干净些”。

如何安全的重构

▪ TDD

▪ 自动化重构


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

再学http-为什么文件上传要转成Base64?

1 前言最近在开发中遇到文件上传采用Base64的方式上传,记得以前刚开始学http上传文件的时候,都是通过content-type为multipart/form-data方式直接上传二进制文件,我们知道都通过网络传输最终只能传输二进制流,所以毫无疑问他们本质...
继续阅读 »

1 前言

最近在开发中遇到文件上传采用Base64的方式上传,记得以前刚开始学http上传文件的时候,都是通过content-type为multipart/form-data方式直接上传二进制文件,我们知道都通过网络传输最终只能传输二进制流,所以毫无疑问他们本质上都是一样的,那么为什么还要先转成Base64呢?这两种方式有什么区别?带着这样的疑问我们一起来分析下。

2 multipart/form-data上传

先来看看multipart/form-data的方式,我在本地通过一个简单的例子来查看http multipart/form-data方式的文件上传,html代码如下

<!DOCTYPE html>
<html>
<head>
<title>上传文件示例</title>
<meta charset="UTF-8">
<body>
<h1>上传文件示例</h1>
<form action="/upload" method="POST" enctype="multipart/form-data">
<label for="file">选择文件:</label>
<input type="file" id="file" name="file"><br>
<label for="tx">说明:</label>
<input type="text" id="tx" name="remark"><br><br>
<input type="submit" value="上传">
</form>
</body>
</html>

页面展示也比较简单

image.png

选择文件点击上传后,通过edge浏览器f12进入调试模式查看到的请求信息。
请求头如下 image.png 在请求头里Content-Type 为 multipart/form-data; boundary=----WebKitFormBoundary4TaNXEII3UbH8VKo,刚开始看肯定有点懵,不过其实也不复杂,可以简单理解为在请求体里要传递的参数被分为多部份,每一部分通过分解符boundary分割,就比如在这个例子,表单里有file和remark两个字段,则在请求体里就被分为两部分,每一部分通过boundary=----WebKitFormBoundary4TaNXEII3UbH8VKo来分隔(实际上还要加上CRLF回车换行符,回车表示将光标移动到当前行的开头,换行表示一行文本的结束,也就是新文本行的开始)。需要注意下当最后一部分结尾时需要加多两个"-"结尾。
我们继续来看请求体

image.png 第一部分是file字段部分,它的Content-Type为image/png,第二部分为remark字段部分,它没有声明Content-Type,则默认为text/plain纯文本类型,也就是在例子中输入的“测试”,到这里大家肯定会有个疑问,上传的图片是放在哪里的,这里怎么没看到呢?别急,我猜测是浏览器做了特殊处理,请求体里不显示二进制流,我们通过Filder抓包工具来验证下。

image.png 可以看到在第一部分有一串乱码显示,这是因为图片是二进制文件,显示成文本格式自然就乱码了,这也证实了二进制文件也是放在请求体里。后端使用框架springboot通过MultipartFile接受文件也是解析请求体的每一部分最终拿到二进制流。

@RestController
public class FileController {
// @RequestParam可接收Content-Type 类型为:multipart/form-data 
// 或 application/x-www-form-urlencoded 请求体的内容
@PostMapping("/upload")
public String upload(@RequestParam("file") MultipartFile file) {
return "test";
}
}

到此multipart/form-data方式上传文件就分析完了,关于multipart/form-data官方说明可参考 RFC 7578 - Returning Values from Forms: multipart/form-data (ietf.org)

3 Base64上传

在http的请求方式中,文件上传只能通过multipart/form-data的方式上传,这样一来就会有比较大的限制,那有没其他方式可以突破这一限制,也就是说我可以通过其他的请求方式上传,比如application/json?当然有,把文件当成一个字符串,和其他普通参数没什么两样,我们可以通过其他任意请求方式上传。如果转成了字符串,那上传文件就比较简单了,但问题是我们怎么把二进制流转成字符串,因为这里面可能会有很多“坑”,业界一般的做法是通过Base64编码把二进制流转成字符串,那为什么不直接转成字符串而要先通过Base64来转呢?我们下面来分析下。

3.1 Base64编码原理

在分析原理之前,我们先来回答什么是Base64编码?首先我们要知道Base64只是一种编码方式,并不是加解密算法,因此Base64可以编码,那也可以解码,它只是按照某种编码规则把一些不可显示字符转成可显示字符。这种规则的原理是把要编码字符的二进制数每6位分为一组,每一组二进制数可对应Base64编码的可打印字符,因为一个字符要用一个字节显示,那么每一组6位Base64编码都要在前面补充两个0,因此总长度比编码前多了(2/6) = 1/3,因为6和8最小公倍数是24,所以要编码成Base64对字节数的要求是3的倍数(24/8=3字节),对于不足字节的需要在后面补充字节数,补充多少个字节就用多少个"="表示(一个或两个),这么说有点抽象,我们通过下面的例子来说明。
我们对ASCII码字符串"AB\nC"(\n和LF都代表换行)进行Base64编码,因为一共4字节,为了满足是3的倍数需要扩展到6个字节,后面补充了2个字节。

image.png

表3.1

转成二级制后每6位一组对应不同颜色,每6位前面补充两个0组成一个字节,最终Base64编码字符是QUIKQw==,Base64编码表大家可以自行网上搜索查看。

image.png 我们通过运行程序来验证下

image.png 最终得出的结果与我们上面推理的一样。

3.2 Base64编码的作用

在聊完原理之后,我们继续来探讨文件上传为什么要先通过Base64编码转成字符串而不直接转成字符串?一些系统对特殊的字符可能存在限制或者说会被当做特殊含义来处理,直接转成普通字符串可能会失真,因此上传文件要先转成Base64编码字符,不能把二进制流直接字符串。

另外,相比较multipart/form-data Base64编码文件上传比较灵活,它不受请求类型的限制,可以是任何请求类型,因为最终就是一串字符串,相当于请求的一个参数字段,它不像二进制流只能限定multipart/form-data的请求方式,日常开发中,我们用的比较多的是通过apllication/json的格式把文件字段放到请求体,这种方式提供了比较便利的可操作性。

4 总结

本文最后再来总结对比下这两种文件上传的方式优缺点。
(1)multipart/form-data可以传输二进制流,效率较高,Base64需要编码解码,会耗费一定的性能,效率较低。
(2)Base64不受请求方式的限制,灵活度高,http文件二进制流方式传输只能通过multipart/form-data的方式,灵活度低。
因为随着机器性能的提升,小文件通过二进制流传输和字符串传输,我们对这两种方式时间延迟的感知差异并不那么明显,因此大部分情况下我们更多考虑的是灵活性,所以采用Base64编码的情况也就比较多。


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

懒汉式逆向APK

通过各方神仙文档,以及多天调试,整理了这篇极简反编译apk的文档(没几个字,吧).轻轻松松对一个apk(没壳的)进行逆向分析以及调试.其实主要就是4个命令.准备下载apktool下载Android SDK Build-Tools,其中对齐和签名所需的命令都在此...
继续阅读 »

通过各方神仙文档,以及多天调试,整理了这篇极简反编译apk的文档(没几个字,吧).轻轻松松对一个apk(没壳的)进行逆向分析以及调试.其实主要就是4个命令.

准备

  1. 下载apktool
  2. 下载Android SDK Build-Tools,其中对齐和签名所需的命令都在此目录下对应的版本的目录中,比如我的在D:\sdk\build-tools\30.0.3目录下,可以将此目录加入环境变量中,后续就可以直接使用签名和对齐所需的命令了
  3. 可选,下载jadx-gui,可查看apk文件,并可导出为gralde项目供AS打开

流程

  1. 解压apk: apktool d C:\Users\CSP\Desktop\TEMP\decompile\test.apk -o C:\Users\CSP\Desktop\TEMP\decompile\test,第一个参数是要解压的apk,第二个参数(-o后面)是解压后的目录

  2. 修改: 注意寄存器的使用别错乱,特别注意,如果需要使用更多的寄存器,要在方法开头的.locals x或.registers x中对x+1

    • 插入代码:在idea上使用java2smali插件先生成smali代码,可复制整个.smali文件到包内,或者直接复制smali代码,注意插入后修改包名;
    • 修改代码:需要熟悉smali语法,可自行百度;
    • 修改so代码,需要IDA,修改完重新保存so文件,并替换掉原so文件,注意如有多个架构的so,需要都进行修改并替换;
    • 删除代码:不建议,最好逻辑理清了再删,但千万别删一半;
    • 资源:修改AndroidManifest.xml,可在application标签下加入android:debuggable="true",重新打包后方可对代码进行调试;
  3. 重打包apktool b C:\Users\CSP\Desktop\TEMP\decompile\test -o C:\Users\CSP\Desktop\TEMP\decompile\test_b.apk,第一个参数是要进行打包的目录文件,第二个参数(-o后面)是重新打包后的apk路径.重新打包成功,会出现Press any key to continue ...

  4. 对齐zipalign -v 4 C:\Users\CSP\Desktop\TEMP\decompile\test_b.apk C:\Users\CSP\Desktop\TEMP\decompile\test_b_zipalign.apk,第一个参数是需要进行对齐的apk路径,第二个参数是对齐后的apk路径.对齐成功,会出现Verification succesful

  5. 签名apksigner sign -verbose --ks C:\Users\CSP\Desktop\软件开发\反编译\mykeys.jks --v1-signing-enabled true --v2-signing-enabled true --ks-key-alias key0 --ks-pass pass:mykeys --key-pass pass:mykeys --out C:\Users\CSP\Desktop\TEMP\decompile\test_b_sign.apk C:\Users\CSP\Desktop\TEMP\decompile\test_b_zipalign.apk,第一个参数(--ks后面)是密钥路径,后面跟着是否开启V1、V2签名,在后面跟着签名密码,最后两个参数(--out后面)是签名后的apk路径以及需要签名的apk(注意需对齐)路径.签名成功,会出现Signed

  6. 安装adb install C:\Users\CSP\Desktop\TEMP\decompile\test_b_sign.apk

  7. 调试: 用jdax将apk导出为gradle项目,在AS中打开,即可通过attach debugger的方式对刚重新打包的项目进行调试.注意,调试时因为行号对不上,所以只能在方法上打上断点(菱形图标,缺点,运行速度极慢)

  8. 注意事项:

    • 上述命令中,将目录和项目'test'改成自己的目录和项目名即可;
    • apktool,zipalign,apksigner,adb命令需加入环境变量,否则在各自的目录下./xxx 去执行命令;
    • zipalign,apksigner所需的运行文件在X:XX\sdk\build-tools\30.0.3目录下;
    • 使用apksigner签名,对齐操作必须在签名之前(推荐使用此签名方式);
    • 新版本Android Studio生成的签名密钥,1.8版本JDK无法使用,我是安装了20版本的JDK(AS自带的17也行)

假懒

为了将懒进行到底,写了个bat脚本(需要在test文件目录下):

::关闭回显
@echo off
::防止中文乱码
chcp 65001
title 一键打包

start /wait apktool b C:\Users\CSP\Desktop\TEMP\decompile\test -o C:\Users\CSP\Desktop\TEMP\decompile\test_b.apk
start /b /wait zipalign -v 4 C:\Users\CSP\Desktop\TEMP\decompile\test_b.apk C:\Users\CSP\Desktop\TEMP\decompile\test_b_zipalign.apk
start /b /wait apksigner sign -verbose --ks C:\Users\CSP\Desktop\软件开发\反编译\mykeys.jks --v1-signing-enabled true --v2-signing-enabled true --ks-key-alias key0 --ks-pass pass:mykeys --key-pass pass:mykeys --out C:\Users\CSP\Desktop\TEMP\decompile\test_b_sign.apk C:\Users\CSP\Desktop\TEMP\decompile\test_b_zipalign.apk

大家将此脚本复制进bat文件,即可一键输出.

不过目前略有瑕疵:1.重新打包需要新开窗口,并且完成后还需手动关闭;2.关闭后还要输入'N'才能进行后续的对齐和签名操作有无bat大神帮忙优化下/(ㄒoㄒ)/~~!

-------更新

真懒

对于'假懒'中的打包脚本,会有2个瑕疵,使得不能将懒进行到底.经过查找方案,便有了以下'真懒'的方案,使得整个打包可以真正一键执行:

::关闭回显
@echo off
::防止中文乱码
chcp 65001
title 一键打包

call apktool b C:\Users\CSP\Desktop\TEMP\decompile\test -o C:\Users\CSP\Desktop\TEMP\decompile\test_b.apk
call zipalign -v 4 C:\Users\CSP\Desktop\TEMP\decompile\test_b.apk C:\Users\CSP\Desktop\TEMP\decompile\test_b_zipalign.apk
del test_b.apk
call apksigner sign -verbose --ks C:\Users\CSP\Desktop\软件开发\反编译\mykeys.jks --v1-signing-enabled true --v2-signing-enabled true --ks-key-alias key0 --ks-pass pass:mykeys --key-pass pass:mykeys --out C:\Users\CSP\Desktop\TEMP\decompile\test_b_sign.apk C:\Users\CSP\Desktop\TEMP\decompile\test_b_zipalign.apk
del test_b_zipalign.apk

echo 打包结束
echo 输出文件是-----test_b_sign.apk

pause

可以看到,把start换成了call,并且删除了重新打包和对齐后的文件,只留下最后签完名的文件

image.png

到此够了吗?不够,因为运行第一个apktool b命令时,重新打包完,会被pasue,让你按一个按键再继续.

image.png

这当然不行,这可不算一键,那么我们找到apktool的存放路径,打开apktool.bat,找到最后一行

image.png

就是这里对程序暂停了,那么就把这一行删了,当然最好是注释了就行,在最前面rem即可对命令进行注释,处理完之后,再重新运行我们的'一键打包.bat'脚本,这时候在中途重新打包后就不会出现'Press any key to continue...'了,即可一键实现打包-对齐-签名的流程了( •̀ ω •́ )y.

当然,如果想使脚本到处运行,可以给脚本增加一个变量,在运行前通过环境变量的形式把要打包的目录路径加入进来,这个大家可以自行尝试.

最后,感谢大家的阅读.这里面有对smali和so的修改,有机会和时间,我也会继续分享!!!


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

Parcelable为什么速度优于 Serializable ?

在Android开发中,我们有时需要在组件之间传递对象,比如Activity之间、Service之间以及进程之间。传递对象的方式有三种:将对象转换为Json字符串通过Serializable序列化通过Parcelable序列化 1、什么是序列化  序...
继续阅读 »

在Android开发中,我们有时需要在组件之间传递对象,比如Activity之间、Service之间以及进程之间。
传递对象的方式有三种:

  • 将对象转换为Json字符串
  • 通过Serializable序列化
  • 通过Parcelable序列化 

1、什么是序列化

微信截图_20230619105720.png

  序列化:简单来说,就是将实例的状态转化为可以存储或者传输的形式的过程
  反序列化:反过来再把这种形式还原成实例的过程就叫做反序列化

  这种可以传输或者存储的形式,可以是二进制流,也可以是字符串,可以被存储到文件,也可以通过各种协议被传输。

2、Serializable 的实现原理

   Serializable 是 Java 平台中用于对象序列化和反序列化的接口。,它是一个空接口,没有定义任何方法,它仅仅只起到了标记作用。通过实现它,Java 虚拟机可以识别该类是可以进行序列化和反序列化操作的。 

2.1 Serializable 序列化的使用

将一个对象序列化写入文件:

public class User implements Serializable {

private String name;
private String email;

public User(String name, String email) {
this.name = name;
this.email = email;
}

/***** get set方法省略 *****/
}
File file = new File("write.txt");

//序列化写入文件
FileOutputStream fileOutputStream = new FileOutputStream(file);
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(new User("李四", "lisi@qq.com"));
objectOutputStream.flush();

//读取文件反序列化
FileInputStream fileInputStream = new FileInputStream(file);
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
User user = (User) objectInputStream.readObject();

序列化写入文件的结果:
image.png

2.2 Serializable 的关键方法ObjectOutputStream() 和 writeObject()

  那么对于一个只是实现了一个空接口的实例,ObjectOutputStream是如何做到知道这个类中都有哪些属性结构的呢?并且是如何获取它们的值的呢?
  我们来看一下 ObjectOutputStream的源码实现,在它的构造方法中主要做两件事:

  • 创建一个用于写入文件的Stream
  • 写入魔数和版本号
public ObjectOutputStream(OutputStream out) throws IOException {
verifySubclass();
bout = new BlockDataOutputStream(out);//创建Stream
...
writeStreamHeader();//写入魔数和版本号
...
}

  再来看 writeObject() 方法,writeObject的核心是调用 writeObject0()方法,在writeObject0中通过 ObjectStreamClass desc = ObjectStreamClass.lookup(cl, true) 创建出一个原始实例的描述信息的实例,即desc。desc这个描述信息中就包括原始类的属性结构和属性的值。接着再根据实例的类型调用对应的方法将实例的类名和属性信息写入到输出流;字符串、数组、枚举和一般的实例写入的逻辑也是不同的。


image.png

2.3 性能分析

  很明显在序列化的过程中,写输出流的过程肯定不存在输出瓶颈,复杂操作集中在如何去解析原始对象的结构,如何读取它的属性。所以要把重点放在ObjectStreamClass这个类是如何的被创建出来的。
  我们分析lookup方法,发现创建过程会先去读取缓存。如果发现已经解析并且加载过相同的类,那么就直接返回。在没有缓存的情况下,才会根据class去创建新的ObjectStreamClass实例。

    static ObjectStreamClass lookup(Class<?> cl, boolean all) {
...
Reference<?> ref = Caches.localDescs.get(key);//读取缓存
if (ref != null) {
entry = ref.get();
}


if (entry instanceof ObjectStreamClass) {
return (ObjectStreamClass) entry;
}


...

if (entry == null) {
entry = new ObjectStreamClass(cl);//没有缓存
}

if (entry instanceof ObjectStreamClass) {
return (ObjectStreamClass) entry;
}
}

  在创建过程中,类名name是通过class实例调用反射API来获取的。再通过getDeclaredSUID 方法提取到serialVersionUID 字段信息。如果没有配置,getSerialVersionUID 方法会通过 computeDefaultSUID 生成一个默认的序列号。
  接下来就会去获取属性以及计算属性值的偏移量。

    private ObjectStreamClass(final Class<?> cl) {
name = cl.getName();//类名

suid = getDeclaredSUID(cl);//提取 serialVersionUID 字段信息


fields = getSerialFields(cl);//获取属性 即通过反射获取该类所有需要被序列化的Field
computeFieldOffsets();//计算属性值的偏移量
}

  我们再来看一下读取属性信息的代码 getSerialFields(),首先系统会判断我们是否自行实现了字段序列化 serialPersistentFields 属性,否则走默认序列化流程,既忽律 static、transient 字段。

    private static ObjectStreamField[] getSerialFields(Class<?> cl)
throws InvalidClassException
{
ObjectStreamField[] fields;
if (Serializable.class.isAssignableFrom(cl) &&
!Externalizable.class.isAssignableFrom(cl) &&
!Proxy.isProxyClass(cl) &&
!cl.isInterface())
{
if ((fields = getDeclaredSerialFields(cl)) == null) {
fields = getDefaultSerialFields(cl);//默认序列化字段规则
}
Arrays.sort(fields);
} else {
fields = NO_FIELDS;
}
return fields;
}

  然后在getDefaultSerialFields 中使用了大量的反射API,最后把属性信息构建成了ObjectStreamField的实例。

    private static ObjectStreamField[] getDefaultSerialFields(Class<?> cl) {
Field[] clFields = cl.getDeclaredFields();//获取当前类的所有字段
ArrayList<ObjectStreamField> list = new ArrayList<>();
int mask = Modifier.STATIC | Modifier.TRANSIENT;

for (int i = 0; i < clFields.length; i++) {
if ((clFields[i].getModifiers() & mask) == 0) {
//将其封装在ObjectStreamField中
list.add(new ObjectStreamField(clFields[i], false, true));
}
}
int size = list.size();
return (size == 0) ? NO_FIELDS :
list.toArray(new ObjectStreamField[size]);
}

  到这里我们会发现Serializable 整个计算过程非常复杂,而且因为存在大量反射和 GC 的影响,序列化的性能会比较差。另外一方面因为序列化文件需要包含的信息非常多,导致它的大小比 Class 文件本身还要大很多,这样又会导致 I/O 读写上的性能问题。

  总结,实现Serializable接口后 ,Java运行时将使用反射来确定如何编组和解组对象。所以我们可以认定这些反射操作就是影响 Serializable 性能的一个重要的因素,同时会创建大量临时对象并导致相当多的垃圾收集。但是因为这些反射,所以Serializable的使用非常简单。

3、Parcelable的实现原理

  在Android中提供了一套机制,可以将序列化之后的数据写入到一个共享内存。其他进程就可以通过Parcel来读取这块共享内存中的字节流,并且反序列化成实例。Parcelable相对于Serializable的使用相对复杂一些。
微信截图_20230629112841.png

3.1 Parcelable 序列化的使用

public class User implements Parcelable {

private String name;

private String email;

//反序列化
protected User(Parcel in) {
name = in.readString();
email = in.readString();
}


public static final Creator<User> CREATOR = new Creator<User>() {
@Override
public User createFromParcel(Parcel in) {
return new User(in);
}

@Override
public User[] newArray(int size) {
return new User[size];
}
};

@Override
public int describeContents() {
return 0;
}

// 用于序列化
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(name);
dest.writeString(email);
}
}
User user = new User();
Bundle params = new Bundle();
params.putParcelable("user", user);

Bundle arguments = getArguments();
personBean = arguments.getParcelable("user");

  实现 Parcelable 接口,需要实现writeToParcel的方法以提供序列化时,将数据写入Parcel的代码。除此之外还要提供一个Creator以及一个参数是Parcel类型的构造方法,用来反序列化。
  序列化和反序列化每个字段的代码统一由使用者自己来实现。这样一来在序列化和反序列化的过程中,就不必再去关心实例的属性结构和访问权限。这些都由开发者自己来实现。所以能够避免大面积的使用反射的情况,算是牺牲了一定的易用性来提升运行时的效率。当然了这个易用性我们也可以通过parcelize的方式来弥补。此外,Parcelable还有一个优点,就是它可以手动控制序列化和反序列化的过程。这意味着我们可以选择只序列化对象的部分字段,或者在反序列化的时候对字段进行一些额外的处理。这种灵活性使得Parcelable在某些特定的场景下更加有用。
  虽然Parcelable的设计初衷并不是像Serializable那样,基于输入流和输出流的操作,而是基于共享内存的概念。但Parcelable是支持让我们获取到序列化之后的data数组的。这样一来,我们就可以同样把序列化后的信息写入到文件中。

        //序列化写入byte[]
Parcel parcel = Parcel.obtain();
parcel.setDataPosition(0);
new User().writeToParcel(parcel, 0);
byte[] bytes = parcel.marshall();
parcel.recycle();

//从byte数组反序列化
Parcel parcel = Parcel.obtain();
parcel.unmarshall(bytes, 0, bytes.length);
parcel.setDataPosition(0);
new User(parcel);

3.2 Intent、Bundle中传值对比

public class Intent implements Parcelable, Cloneable { }

public final class Bundle extends BaseBundle implements Cloneable, Parcelable { }

  在安卓平台最经常用到序列化的情况,是通过Intent传值。我们可以看到,无论是Intent还是Bundle,其实都是Parcelable的实现类。
  那么当Intent或者Bundle被序列化的时候,它们内部的Serializable是如何被处理的呢?
  通过代码可以看到,在Parcel的writeSerializable方法中,还是会先把Serializable转化成Byte数组。然后再通过writeByteArray去写入到Parcel中。

    public final void writeSerializable(@Nullable Serializable s) {
if (s == null) {
writeString(null);
return;
}
String name = s.getClass().getName();
writeString(name);

ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(s);
oos.close();

writeBytea

  所以在Intent传值的场景下,Parcelable也是存在速度优势的。因为Parcelable就是正常的基于writeToParcel的方法中的逻辑去进行序列化的。而Serializable要先通过ObjectOutputStream把对象转换成ByteArray,再通过writeByteArray写入Parcel。

  • Parcelable

  调用对象内部实现的writeToParcel 方法,通过一些write方法直接写入Parcel。

  • Serializable

  通过ObjectOutputStream把对象转换成ByteArray,再通过writeByteArray写入Parcel。

  但是有些情况下不一定Parcelable更快。
  之前在看Serializable源码的时候,我们发现ObjectStreamClass是存在缓存机制的。所以在一次序列化的过程中,如果涉及到大量相同类型的不同实例序列化,比如一个实例反复嵌套自己的类型,或者是在序列化大数组的情况下。Serializable的性能还是存在优势的。 

4、Parcelable为什么速度优于Serializable?

  • Parcelable
    1. 对象自行实现出入口方法,避免使用反射的情况。
    2. 二进制流存储在连续内存中,占用空间更小。
    3. 牺牲易用性(kotlin的Parcelize 可以弥补),换取性能。
  • Serializable
    1. 用反射获取类的结构和属性信息,过程中会产生中间信息。
    2. 有缓存结构,在解析相同类型的情况下,能复用缓存。
    3. 性能在可接受的范围内,易用性较好。

  不能抛开应用场景谈技术方案,在大多数场景下Parcelable确实存在性能优势,而Serializable的性能缺陷主要来自反射构建ObjectStreamClass类型的描述信息。在构建ObjectStreamClass类型的描述信息的过程中,是有缓存机制的。所以能够大量复用缓存的场景下,Serializable反而会存在性能优势。 Parcelable原本在易用性上是存在短板的,但是kotlin的Parcelize 很好的弥补了这个缺点。


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

从面试官角度分析:介绍一下Android中的Context?

Context是什么Context的结构Context的注意事项问题正解:一、Context是什么Context 是 Android 中用的十分常见的一种概念,常被翻译成上下文,这个概念在其他的技术中也有运用。Android 官方对它的解释,可以理解为应用程序...
继续阅读 »
  1. Context是什么
  2. Context的结构
  3. Context的注意事项

问题正解:

一、Context是什么

Context 是 Android 中用的十分常见的一种概念,常被翻译成上下文,这个概念在其他的技术中也有运用。Android 官方对它的解释,可以理解为应用程序环境中全局信息的接口,它整合了相当多系统级的服务,可以用来得到应用中的类、资源,以及可以进行应用程序级的调起操作,比如启动 Activity、Service等等,而且 Context 这个类是 抽象abstract 的,不含有具体的函数实现。

二、Context结构

Context 是维持 Android 程序中各组件能够正常工作的一个核心功能类。

Context 本身是一个抽象类,其主要实现类为 ContextImpl,另有直系子类两个:

  • ContextWrapper
  • ContextThemeWrapper

这两个子类是 Context 的代理类,它们继承关系如下:

image.png

ContextImpl类介绍

ContextImpl 是 Context API 的十分常见实现,它为 Activity 和其他应用程序组件提供基本上下文对象,说白了就是 ContextImpl 实现了抽象类的方法,我们在使用 Context 的时候的方法就是它实现的。

ContextWrapper类介绍

ContextWrapper 类代理 Context 的实现,将其所有调用简单地代理给另一个 Context 对象(ContextImpl),可以被分类为修饰行为而不更改原始 Context 的类,其实就 Context 类的修饰类。真正的实现类是 ContextImpl,ContextWrapper 里面的方法执行也是执行 ContextImpl 里面的方法。

ContextThemeWrapper

就是一个带有主题的封装类,比 ContextWrapper 多了主题,它的一个直接子类就是 Activity。

通过Context的继承关系图结合我们几个开发中比较常见的类,Activity、Service、Application,所以Context 一共有三种类型,分别是 Application、Activity 和Service,他们分别承担不同的责任,都属于 Context,而他们具有 Context 的功能则是由ContextImpl 类实现的。

三、Context的数量

其实根据上面的 Context 类型我们就已经可以得出答案了。Context 一共有 Application、Activity 和 Service 三种类型对象,因此一个应用程序中Context 数量的计算公式就可以这样写:

Context数量 = Activity数量 + Service数量 + 1

上面的1代表着 Application 的数量,因为一个应用程序中可以有多个 Activity 和多个 Service,但是只能有一个 Application。

四、Context注意事项

Context 如果使用不恰当很容易引起内存泄露问题。

最简单的例子比如说使用了 Context 的错误的单例模式:

public class Singleton {
  private static Singleton instance;
  private Context mContext;

  private Singleton(Context context) {
      this.mContext = context;
  }

  public static Synchronized Singleton getInstance(Context context) {
      if (instance == null) {
          instance = new Singleton(context);
      }
      return instance;
  }
}

上述代码中,我们使得了一个静态对象持有 Context 对象,而静态数据的生命一般是长于普通对象的,因此当 Context 被销毁(例如假设这里持有的是 Activity 的上下文对象,当 Activity 被销毁的时候),因为 instance 仍然持有 Context 的引用,导致 Context 虽然被销毁了但是却无法被GC机制回收,因为造成内存泄露问题。

而一般因为Context所造成的内存泄漏,基本上都是 Context 已经被销毁后,却因为被引用导致GC回收失败。但是 Application 的 Context 对象却会随着当前进程而一直存在,所以使用 Context 是应当注意:

  • 当 Application 的 Context 能完成需要的情况下,并且生命周期长的对象,优先使用 Application 的 Context。
  • 不要让生命周期长于 Activity 的对象持有到 Activity 的引用。
  • 尽量不在 Activity 中使用非静态内部类,因为非静态内部类会隐式持有外部类实例的引用,如果使用静态内部类,将外部实例引用作为弱引用持有。
五、如何正确回复以上面试题
  1. 面试官:Android 中有多少类型的 Context,它们有什么区别?

回答总共有 Activity 、Service、Application 这些 Context 。

共同点:它们都是 ContextWrapper 的派生类,而 ContextWrapper 的成员变量 mBase 可以用来存放系统实现的 ContextImpl,这样我们在执行如 Activity 的 Context 方法时,都是通过静态代理的方式最终执行到 ContextImpl 的方法。我们调用 ContextWrapper 的 getBaseContext 方法就能得到 ContextImpl 的实例。

不同点:它们有各自不同的生命周期;在功能上,只有 Activity 显示界面,正因为如此,Activity 继承的是 ContextThemeWrapper 提供一些关于主题、界面显示的能力,间接继承了 ContextWrapper ;而 Applicaiton 、Service 都是直接继承 ContextWrapper ,所以我们注意一点,但凡跟 UI 有关的,都应该用 Activity 作为 Context 来处理,不然要么会报错,要么 UI 会使用系统默认的主题。

  1. 面试官:一个APP应用里有几个 Context 呢?

Context 一共有 Application 、Activity 和 Service 三种类型,因此一个应用程序中 Context 数量的计算公式就可以这样写:

Context 数量 = Activity 数量 + Service 数量 + 1

上面的1代表着 Application 的数量,因为一个App中可以有多个Activity和多个 Service,但是只能有一个 Application。

  1. 面试官:Android 开发过程中,Context 有什么用?

Context 就等于 Application 的大管家,主要负责:

  • 四大组件的信息交互,包括启动 Activity、Broadcast、Service,获取 ContentResolver 等。
  • 获取系统/应用资源,包括 AssetManager、PackageManager、Resources、System Service 以及 color、string、drawable 等。
  • 文件,包括获取缓存文件夹、删除文件、SharedPreference 相关等。
  • 数据库(SQLite)相关,包括打开数据库、删除数据库、获取数据库路径等。

其它辅助功能,比如配置 ComponentCallbacks,即监听配置信息改变、内存不足等事件的发生

  1. 面试官:ContextImpl 实例是什么时候生成的,在 Activity 的 onCreate 里能拿到这个实例吗?

可以。我们开发的时候,经常会在 onCreate 里拿到 Application,如果用 getApplicationContext 取,最终调用的就是 ContextImpl 的 getApplicationContext 方法,如果调用的是 getApplication 方法,虽然没调用到 ContextImpl ,但是返回 Activity 的成员变量 mApplication 和 ContextImpl 的初始化时机是一样的。 再说下它的原理,Activity 真正开始启动是从 ActivityThread.performLaunchActivity 开始的,这个方法做了这些事:

  • 通过 ClassLoader 去加载目标 Activity 的类,从而创建 对象。
  • 从 packageInfo 里获取 Application 对象。
  • 调用 createBaseContextForActivity 方法去创建 ContextImpl。
  • 调用 activity.attach ( contextImpl , application) 这个方法就把 Activity 和 Application 以及 ContextImpl 关联起来了,就是上面结论里说的时机一样。
  • 最后调用 activity.onCreate 生命周期回调。

通过以上的分析,我们知道了 Activity 是先创建类,再初始化 Context ,最后调用 onCreate , 从而得出问题的答案。不仅 Activity 是这样, Application 、Service 里的 Context 初始化也都是这样的。

  1. 面试官:ContextImpl 、ContextWrapper、ContextThemeWrapper 有什么区别?
  • ContextWrapper、ContextThemeWrapper 都是 Context 的代理类,二者的区别在于 ContextThemeWrapper 有自己的 Theme 以及 Resource,并且 Resource 可以传入自己的配置初始化。
  • ContextImpl 是 Context 的主要实现类,Activity、Service 和 Application 的 Base Context 都是由它建立的,即 ContextWrapper 代理的就是 ContextImpl 对象本身。
  • ContextImpl 和 ContextThemeWrapper 的主要区别是, ContextThemeWrapper 有 Configuration 对象,Resource 可以根据这个对象来初始化。
  • Service 和 Application 使用同一个 Recource,和 Activity 使用的 Resource 不同。
  1. 面试官:Activity Context、Service Context、Application Context、Base Context 有什么区别?
  • Activity、Service 和 Application 的 Base Context 都是由 ContextImpl 创建的,且创建的都是 ContextImpl 对象,即它们都是 ContextImpl 的代理类 。
  • Service 和 Application 使用相同的Recource,和 Activity 使用的 Resource 不同。
  • getApplicationContext 返回的就是 Application 对象本身,一般情况下它对应的是应用本身的 Application 对象,但也可能是系统的某个 Application。
  1. 面试官:为什么不推荐使用 BaseContext?
  • 对于 Service 和 Application 来说,不推荐使用 Base Context,是担心用户修改了 Base Context 而导致出现错误。
  • 对于 Activity 而言,除了担心用户的修改之外,Base Context 和 Activity 本身对于 Reource 以及 Theme 的相关行为是不同的(如果应用了 Configuration 的话),使用 Base Context 可能出现无法预期的现象。
  1. 面试官:ContentProvider 里的 Context 是什么时候初始化的呢?

ContentProvider 不是 Context ,但是它有一个成员属性 mContext ,是通过构造函数传入的。那么这个问题就变成了,ContentProvider 什么时候创建。应用创建 Application 是通过执行 ActivityThread.handleBindApplication 方法,这个方法的相关流程有:

  • 创建 Application
  • 初始化 Application 的 Context
  • 执行 installContentProviders 并传入刚创建好的 Application 来创建 ContentProvider
  • 执行 Application.onCreate

得出结论,ContentProvider 的 Context 是在 Applicaiton 创建之后,但是 onCreate 方法调用之前初始化的。

  1. 面试官:BroadcastReceiver 里的 Context是哪来的?

广播接收器,分动态注册和静态注册。

  • 动态注册很简单,在调用 Context.registerReceiver 动态注册 BroadcastReceiver 时,会生成一个 ReceiverDispatcher 会持有这个 Context ,这样当有广播分发到它时,执行 onReceiver 方法就可以把 Context 传递过去了。当然,这也是为什么不用的时候要 unregisterReceiver 取消注册,不然这个 Context 就泄漏了哦。
  • 静态注册时,在分发的时候最终执行的是 ActivityThread.handleReceiver ,这个方法直接通过 ClassLoader 去创建一个 BroadcastReceiver 的对象,而传递给 onReceiver 方法的 Context 则是通过 context.getReceiverRestrictedContext() 生成的一个以 Application 为 mBase 的 ContextWrapper。注意这边的 Context 不是 Application 。


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

收起狭隘,换位思考,心胸放开!

当你越证明自己是对的时候,其实你已经错了一半,因为如果你真的是对的,不用你来证明,人只有经历越多,跳的坑越多,在做事时才会有更宽广的视角,在面对每一个问题时才会发现原因所在,一个人越自大,越觉得自己是对的,那么其实你就越肤浅,离正确就越来越远,做人最难的就是站...
继续阅读 »

当你越证明自己是对的时候,其实你已经错了一半,因为如果你真的是对的,不用你来证明,人只有经历越多,跳的坑越多,在做事时才会有更宽广的视角,在面对每一个问题时才会发现原因所在,一个人越自大,越觉得自己是对的,那么其实你就越肤浅,离正确就越来越远,做人最难的就是站在不同的视角去看待问题!

收起自己的狭隘

我时常在提醒自己,当你面对一件事的时候,如果你没有身入其中,那么你就不要用你有限的认知去评判,刚毕业实习的时候,公司用的后端语言是nodejs,前端用layui,虽然我自认为我JS写的还行,nodejs也不成问题,但是就是有一种抗拒,因为我主要还是写Java嘛,所以内心就有点看不上,那时候我觉得你用nodejs,他妈的连sql都要写在代码里,为啥不用Java,SpringBoot+Mybatis Plus一把梭,上去就是干,多么方便啊,还有你为啥不用Vue,Vue那么牛逼你不用你要用layui,你们都干了些什么啊!

先为自己的自以为是,自己的那狭隘而又自我感觉良好的思想说声抱歉,我们总是有一个通病,觉得要按照自己想的来才行,其实这恰恰显示出了自己的弱,自己的视野太狭隘,对于技术栈,公司有的产品和项目是和一些单位和公司有挂钩或者合作的,所以对于语言和开发框架也是有一定的讲究,还有像数据库字段,对于很多政府项目,字段命名使用拼音,有些朋友就嗷嗷直叫,就觉得这他妈什么设计啊,还不如我来干,我也曾说过,后来我学会了闭嘴。

如果你没能力制定规则,那么你就遵守规则,而不是自己只能扛100斤,非要说能媲美霸王举鼎!

站在别人的角度去看问题

在职场和生活中,很多人总是自已为是,总觉得自己来做这件事,一定比别人做得好,还有些人喜欢马后炮,别人没有很好的完成一件事,它总会说:“你看,当时我已经说是这样做,那样做,这个问题你早就应该考虑到”,这一系列话在职场和生活中,听到的太多太多了,我以前也是这样。

正式参加工作后,接手了一些代码,让人欲哭无泪,于是就和同事和朋友说:“这傻逼怎么写的代码啊,写成这个鸟样,我他妈也是服了,真sb”,后来,因为工期还有一些原因,自己也写了很多垃圾,翻出来看脸都是红的,那么,下一个人来接手的时候,自己也会被骂!

我们总是觉得自己能做得很好,作为上级,当手下工作完成得不好时,会觉得手下无能,其实何不反思一下自己,手下无能,那么证明自己的分配和对手下的一些交代一定存在很大的问题,那么作为手下,又会觉得领导不行,领导没能力,但是,何不反思一下自己做事是否认真思考过,反应过!

没有谁是正确的,也没谁是错误的,在任何事发生的时候,甩锅不仅不能解决问题,反而会使问题严重,有一句一将无能,累死三军,但是我也觉得一卒无能,拖垮三军,当然这个无能不单单指能力,还指人品,道德,担当等等,一个人是走不远的,一行人才能走得更远。

心胸宽广

“大肚能容,容天下难容之事; 开口便笑,笑世间可笑之人”,心胸宽广是一个人的魅力,总想搞谁,让谁难看,给别人扣帽子,不行君子之举,这样的人可能在暂时获取到了利益恩惠,但是也会失去很多,有很多东西是靠你的人品,道德,态度等积累起来的,在生活和工作中,要有宽广的胸怀,这样不仅能减轻你80%的心理负担,还能在无形之中给你带来你意想不到的收获!


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

一个艰难就业的23年应届生的2022年

自我介绍我的家乡是浙江-宁波-余姚,是一名就读于一所位于宁波-慈溪(学校:笑死,这就我一所大学,你直接报我名字得了)的双非独立学院的软件工程专业的23年应届生,7到10月有在南京实习,现在是孤身一人在杭州实习的社恐前端实习生,前端练习时长一年半,擅长唱、跳、r...
继续阅读 »

自我介绍

我的家乡是浙江-宁波-余姚,是一名就读于一所位于宁波-慈溪(学校:笑死,这就我一所大学,你直接报我名字得了)的双非独立学院的软件工程专业的23年应届生,7到10月有在南京实习,现在是孤身一人在杭州实习的社恐前端实习生,前端练习时长一年半,擅长唱、跳、rap... 还只擅长Vue的渣渣前端程序猿,有兴趣可以关注我的公众号程序猿青空,23年开始我会时不时分享各种优秀文章、学习资源、学习课程,探索初期,还请多多关照。这篇文章会是我公众号的第一篇文章,主要对我这一年来的经历做一个简单的流水账总结,涉及到恋爱、租房、学习、工作等各方面内容,希望这份经验对你也能有所帮助。

学习

大二下半年的时候分流,自主报名到了我们学校的产业学院——企业和学校联合创办的培养应用型人才的学院。我文科相当薄弱,埋头考研会相当痛苦,也很清楚自己做不来官僚主义那一套,公职也不是适合我的职业(没错我对公职有偏见),很坚定就业这条路。因为还没有毕业,我的身份归根结底就是一个双非下流本科的一名大学生,为了避免自己毕业即失业,看当时产业学院的宣传也不错就去了。

事实上因为产业学院刚创办不久,而且并不是所有人来到这里都是为了就业的,也有可能是为了学分、助学金等其他方面的原因,课程设计、师资力量、同学质量等各方面都良莠不齐、鱼龙混杂。每门课程的期末大作业基本都是一个小项目,大三一年里两个期末都有为了大作业通宵的几天,再加上1500💰凑活过的生活费,死贵的电费和食堂伙食费,在这里学习和生活有时候还蛮辛苦的。好在我很清楚自己应该做什么,天赋不够,努力来凑,本来起跑线就低,更应该比别人卷一点。当然我也不是那种能够没日没夜卷的人(👀),关注了鱼皮,加入了他的知识星球,在星球天天学习健身(没错我还健身💪)打卡的flag没两个礼拜就立不住了,知识付费的事咱也没少干,就是说能一直坚持下来的着实不多,咱也明白咱就是个普通人,逆袭这种事确实还是很难做到的,我这人还是比较佛系的。

大三这一年我用一年的时间从零学前端,自认为还算是没有辜负自己,这一年时间的学习也还算有成果,虽然没法和卷王们争第一,也能跟在他们后面做个万年老二(😭呜呜呜)。下半年开始实习后更别说了,新的技术栈的学习基本就停滞了。实习前我还天真的以为能有更多的时间学习,正相反,比在学校学的更少,因为下班到家七八点,生活琐事会比在学校里多得多,而且我下班后还要花一个多钟头健身,再加上忙碌一天后更无心学习,只想躺平。

下半年做过的最卷的事也就参与了字节青训营,课题选择了前端监控平台,可惜的就是没能在青训营期间完成(😭呜呜呜,队友都摆烂了),当然也就没有结营证书。但我也不甘心就这样算罢,这个项目我就自己拉出来,作为我的毕业设计去完成它。解决实习期间学习效率低的最好办法就是在公司学习一些对公司业务有关或者优化公司项目的知识,名正言顺地摸鱼。我是Vue入门的,这一年里也一直守着Vue,来年第一季度目标就是学习React和Nest,开发一个自己的数据聚合的网站,能变现就最好了(😎欸嘿)。

生活&实习

大三下,也就是今年上半年,为了冲刺暑期实习,也就没去做兼职了,感叹本就艰难的生活的同时,殊不知这是为数不多还能自己自由掌控的日子了(😥我哭死)。其实我开始准备实习还是挺晚了,再加上期末没有太多时间,准备并不是太充分,没有太多自信心,投了几家大厂,不是没回应,就是笔试挂,就有点望而却步。

在我一个大佬同学的介绍下,面试了一家南京的小厂,过程很顺利,实习薪资给的也很可观,当时就没考虑那么多,就选择接受offer了(后来在杭州实习认识了几个小伙伴,才学了没几个月,暑假就面试进了独角兽企业,我那个时候确实应该再多投一投的)。刚开始的想法是第一次出门实习,有份经验就可以,在什么城市没关系,然而事实是工作上确实没什么关系,生活上关系可大了。7月13日第一次一个人拎上行李,义无反顾地去了南京,以为自己终于能够大展拳脚,再不济也能够在公司有所贡献,然而现实总是没那么理想。

上路

因为一个人前往外地工作,第一件事情便是租房,为了省点钱就托南京实习公司的一个同事看房子,因为他的房租到期也要找房子就顺便可以租在一起,有个照应。然而实际上因为是第一次出远门工作和生活,一切和自己的理想差距显然大了许多:因为不是自己实地看的房,而且也是第一次租房,虽然房租只有850💰,但是也可能因为是夏季大家都开空调,差不多50多💰一个礼拜的电费和其他乱七八糟的费用,一个月光租房子就差不多得1200💰,并不算贵,但是性价比极低;我的房间没地方晒衣服,只能晒在那个同事的房间的阳台,作为一个社恐患者,每次去都要做很多心理斗争(他会不会睡了,他会不会在忙....🙃);桌上只能堪堪放下我的显示器和笔记本,鼠标活动范围极小;床应该是睡过好几个租客了,明显的不舒服;吃的方面因为有点水土不服不能随便乱吃,同时也是为了省钱所以选择自己做饭,因此还得购置很多厨具调味品等等,一次性的开销💰不小;回学校的频率比我想象的高,因此来回车费也成为一大负担;当时租房合同是同事代签的,他签了一年,我那时候也不懂也没问,再加上当时换工作离开的比较急,没时间找转租,违约金直接血亏1700💰。

日常挤地铁

生活的种种问题都还能接受或者解决,然而工作方面,因为进入公司的时间段比较特殊再加上疫情影响,在南京实习的三个月里,我始终没有能够在技术上得到足够的提升,再加上与公司和领导的气场不合,使得我在公司整天如坐针毡,甚至有点无所事事(总之就是过的很不开心),虽然有不低的实习薪资,但是我始终没法在那里躺平。因此在中秋决定参与秋招,开始寻找第二份实习工作。

然而今年找工作并不简单,因为频繁发作的疫情,再加上互联网行业这些年的发展,行业的形势非常的严峻,各大公司都削减了HC(head count,人头数,就是最终录用的人数,肯定有小伙伴不懂这个词,我一开始就不懂🤏),作为一个民本23年应届生,在今年的秋招着实很难找到一份理想的工作。那段时间的想法就是尽快找到下一份工作(急急急急急急,我是急急国王),找到一份离家近、工资高、平台大至少满足两个的工作。从9月10日中秋就开始投出第一份简历,到10月19日确定来到杭州的一家四五百人的SaaS公司,这期间投出过几百份简历,得到的回应却寥寥无几,这是一段非常难忘的经历。

这一个月里每一天都在为找工作烦恼,一开始专注于线上面试,却始终的得不到理想工作的认可,持续的碰壁使得开始怀疑自己这些年的学习,自己的选择是不是错了,是不是自己能力确实没法满足他们的要求(被ktv了),后来也决定不放过线下面试的机会,顶着疫情在南京、杭州、家、学校几地频繁奔波,在杭州线下面试的那一天还是顶着自己身体上的各种不适(持续拉肚子,全身酸痛,萎靡不振),仍然要拿出饱满的精神去面对面试,好在当时就获得了面试官也是现在的leader的认可,简直就是久旱逢甘霖,虽然并不是直接发的offer,但是也是十分有信心。杭州比起南京的工作,实习薪资低了很多,但是因为线下面试,对于当时感受到的公司的氛围十分的心动,也就放弃了其他小公司更高薪资的offer,决定了自己的第二份实习工作。

又上路啦

换工作又是换城市,所以又需要租房搬家,购置各种必需品,又是一大笔开销,在还没进公司前始终在担忧自己先择了薪资更低的工作,到时候会不会付出了这么多,结果又远不如预期让自己更痛苦。不过在经过了一个月左右实习后,我在杭州的公司工作的感受让我相信自己的选择没有错。

10月23日我再一次拖着一大堆行李开始了迁徙,本来打算先简单看房子,先回家住几天再自驾,拖着行李回来看房子签合同,所以我把被子等一些大件的行李都寄回家了,但是这次进入杭州后就黄🐎了(之前几地来回跑黄都没黄一下),只能多看几套房子然后就签下来,好在当天就看到一个自己满意的,10几平,押一付一,一个月算上水电差不多也就1300💰,不至于睡大街,但是我没有被子,当时杭州刚开始降温,温度也就个位数,但是买被子太亏了,之后用不上,就买了床毛毯,多盖几件衣服,凑活过了两天(真的凑活,冷的雅痞)。

杭州租的房

11月1日正式入职,正式开启了在杭州的工作生活,有条不紊的入职手续,时长1周的实习生培训,认识了许多和我一起实习的小伙伴,刚进来还赶上公司的双十一活动,让我对未来的工作生活充满希望。

双十一零食自助

第一月开始接触了一些简单的业务,重新开始了健身,第二个月就参与开发了一个简单的项目,还封装了公共组件、开发了简单的提高开发效率的脚手架工具,我终于能够继续有条不紊运转了。

在南京实习的期间除了参加了字节青训营和准备面试而巩固基础外,专业上可以说是没有丝毫提升,不过生活经验确实收获满满,坚定了自己的目标,职业生涯规划更加清晰,为了达到目标去学会自律。这几个月的开销给自己和父母都增添了不小得负担,好在现在稳定下来勉强能够在杭州自给自足,生活重新步入正轨,比起在南京,杭州的生活更加得心应手。但是并不是说南京不好,南京是一个非常优雅的城市,这里有他躺在超市里超乖的猫猫,超治愈

超乖的猫猫

离开南京前我也花时间去好好游玩了两天(去了一些免费的博物馆,景点)。

忘记叫啥地了

比起杭州,我认为南京更适合生活,我只是去到了一个不适合我的公司和因为经验不足吃了不少亏才离开了这个城市。我很珍惜在杭州的这份工作,也非常享受现在忙碌充实的生活,我也希望自己的能力能够不断得到认可,继续探索自己的人生价值。

感情

呜呜呜,鼠鼠该死啊,鼠鼠长了个恋爱脑,但是好在现在穷的雅痞,我还社恐,可以心无旁骛地工作学习(搞💰)。出来实习没几个礼拜就跟在一起一年的女孩子分手了,其实在上半年因为我们对未来规划的分歧就吵过架,她想留在慈溪,而我更向往大城市(当然不止这一点原因啦),那个时候我就很清楚这段感情肯定没法坚持很久,下半年又异地,在各自的城市实习,天天吵架,自然而然就吵分了,累觉不爱。我深知自己不是啥好男人(男人没一个好东西),还没有资本,毕业前绝对要水泥封心(做杭州第一深情)。

其实我家离学校很近,但是从念大学开始还是很少回家了,在学校里没有什么感觉,直到独自出门在外工作才知道在家真好,爸爸妈妈真好(我是妈宝男,呜呜呜😭),看这篇文章的小伙伴不要再随便跟爸爸妈妈撒气了哦。家里的老人只剩下奶奶独自在乡下了,以后一定要多打电话。

展望

在未来的一年中,希望自己能够吸收已经犯过的错误的经验,保质保量地完成未来的各项工作,作为一名程序员最重要的最重要的就是自我驱动,持续学习,通过不断学习才能够在未来的工作中创造更多的价值,以下是我23年的一些计划

学习

  • 这个月先抓紧时间把自己的毕设解决,写复盘的分享博客,之后顺利毕业
  • 上半年学习React,Nest,开发一个数据聚合分享平台,同样做分享
  • 运营自己的博客和各平台账号,不说多少粉丝,能坚持不凉就行,争取每周一个博客
  • 每季至少阅读一本书,学习一个技术栈
  • 坚持自己的每日计划和每月复盘总结(包含年中和年终总结)

工作

  • 因为现在常态化了,不知道今年的就业形势会是什么样的,着实不想再像去年那样被支配了,所以还是希望得到自己满意的薪资的前提下在这里转正,但愿不要出什么幺蛾子吧
  • 继续卷进部门更深层业务,目标负责6个项目
  • 学习更多优化开发效率和质量的技术栈,明年就简单定个两个的目标吧,要求不高

生活

  • 我真的超级想买机车的,但是杭州主城区禁摩,所以先23年下半年花时间考个D照,看情况决定买个机车还是电驴
  • 3月份房租到期了,看房肯定又要放进日程了,看看到时候有没有合租的小伙伴吧,如果有人有兴趣到时候可以分享一下杭州租房经验
  • 健身肯定是要继续的,有一说一我肉体确实没啥天赋(也可能是吃得不够多),健身更多的是一种生活态度吧
  • 我是一个很不喜欢打电话的人,尤其是和长辈,感觉没话聊,但是老人家接到自己孩子的电话,知道孩子过得不错,真的会很开心。明年定个小目标,一个月给奶奶打一通电话。

2022年好像所有人都过的很艰难,或许所有人都想离开浪浪山,但是也不要忘记看看浪浪山的风景,让我们一起加油吧。最后再打个广告,关注公众号程序猿青空,免费领取191本计算机领域黑皮书电子书,更有集赞活动免费挑选精品课程(各个领域的都有),不定期分享各种优秀文章、学习资源、学习课程,能在未来(因为现在还没啥东西)享受更多福利。


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

改行后我在做什么?(2022-9-19日晚)

闲言碎语今天回了趟家里,陪父母一起吃了个饭。父母照例是在唠叨,这个年纪了还不结婚,也没个稳定的工作,巴拉巴拉的一大堆。吃完饭我匆匆的就回到了我租住的地方。在现阶段,其实我对于父母所诉说的很多东西,我都是认同的。但在我这个年纪,这个阶段,看似有很多选择,但其实我...
继续阅读 »

闲言碎语

今天回了趟家里,陪父母一起吃了个饭。父母照例是在唠叨,这个年纪了还不结婚,也没个稳定的工作,巴拉巴拉的一大堆。吃完饭我匆匆的就回到了我租住的地方。在现阶段,其实我对于父母所诉说的很多东西,我都是认同的。

但在我这个年纪,这个阶段,看似有很多选择,但其实我没有选择。能做的也只是多挣点钱。

在这个信息爆炸的时代,我们知道更高的地方在哪里。但当你想要再往上走一步的时候,你发现你的上限,其实从出生或从你毕业的那一刻就已经注定了。可能少部分人通过自身的努力,的确能突破壁垒达到理想的高度。但这只是小概率事件罢了。在我看来整个社会的发展,其实早就已经陷入了一种怪圈。

在我,早些年刚刚进入社会的时候。那时的想法特别简单。就想着努力工作,努力提升自身的专业素养。被老板赏识,升职加薪成为一名管理者。如果,被淘汰了那应该是自己不够优秀,不够努力,专业技能不过硬,自己为人处事不够圆滑啥的。

内卷这个词语引爆网络的时候;当35岁被裁员成为常态的时候。再回头看我以前的那些想法那真的是一个笑话。(我觉得我可能是在为自己被淘汰找借口)

当前的状态

游戏工作室的项目,目前基本处于停滞的状态。我不敢加机器也不敢关机。有时候我都在想,是不是全中国那3-4亿的人都在搞这个?一个国外的游戏,金价直接拉成这个逼样。

汽配这边的话,只能说喝口稀饭。(我花了太多精力在游戏工作室上了)

梦想破灭咯

其实按照正常情况来说,游戏工作室最开始的阶段,我应该是能够稍微挣点钱的。我感觉我天时、地利、人和。我都占的。现在来看的话,其实我只占了人和。我自己可以编码,脚本还是从驱动层模拟键鼠,写的一套脚本。这样我都没赚钱,我擦勒。

接下来干嘛

接下来准备进厂打螺丝。(开玩笑的) 还是老老实实跟着我弟学着做生意吧。老老实实做汽配吧!在这个时代,好像有一技之长(尤其是IT)的人,好像并不能活得很好。除非,你这一技之长,特别特别长。(当下的中国不需要太多的这类专业技术人员吧。)

我感受到的大环境

我身边有蛮多的大牛。从他们的口中和我自己看到的。我感觉在IT这个领域,国内的环境太恶劣了。在前端,除开UI库,我用到的很多多的库全是老外的。为什么没有国人开源呢?因为,国人都忙着996了。我们可以在什么都不知道的情况下,通过复制粘贴,全局搜索解决大部分问题。 机械视觉、大数据分析、人工智能 等很多东西。这一切的基石很多年前就有了,为什么没人去研究他?为什么我们这波人,不断的在学习:这样、那样的框架。搭积木虽然很好玩。但创造一个积木,不应该也是一件更有挑战性的事情么?

在招聘网站还有一个特别奇怪的现象。看起来这家公司是在招人,但其实是培训机构。 看起来这家公司正儿八经是在招聘兼职,但其实只想骗你去办什么兼职卡。看起来是在招送快递,送外卖的,招聘司机的,但其实只是想套路你买车。我擦勒。这是怎样的一个恶劣的生存环境。这些个B人就不能干点,正经事?

卖菜的、拉车的、搞电商的、搞短视频、搞贷款的、卖保险的、这些个公司市值几百亿。很难看到一些靠创新,靠创造,靠产品质量,发展起来的公司。


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

实习到毕业一年的回忆:工作旅程

前两天和实习那会的同事一起吃饭,聊到了他们那会刚毕业两三年的工作收入,问完我之后说,“你刚毕业一年的起点太高了,税后五位数,而且还是大专学历,这在外面根本找不到薪资那么多的工作”,“这一切还是得感谢你们几个人,如果不是前年你们收留了我,估计我都不干这行跑去流水...
继续阅读 »

前两天和实习那会的同事一起吃饭,聊到了他们那会刚毕业两三年的工作收入,问完我之后说,“你刚毕业一年的起点太高了,税后五位数,而且还是大专学历,这在外面根本找不到薪资那么多的工作”,“这一切还是得感谢你们几个人,如果不是前年你们收留了我,估计我都不干这行跑去流水线拧螺丝了”,我点头说道。

21年六月在学校投了上百份简历,面试收到了几个offer,但是实习工资给的太少,不是2.5k或者3k,这对于那时年少轻狂的我怎么可能接受呢,果断拒绝,快月底临近毕业找不到工作的我越来越慌了,后来约了一家线上面试并且通过了,实习工资150一天,正常每个月能拿3.3k,有节假日的情况下只能拿到不到2.8k的可怜工资。但命运真的很神奇,因为这家实习公司,结识了能够在职场上帮助到我的良师益友。

实习公司所在的写字楼

21年十月认识了一位朋友介绍的女生,可能是好久没和女生接触过,我变得不怎么会和女生聊天了,只记得我和她打了两个月的王者,基本上天天玩,还都是玩的人机,后来不知道啥原因就凉凉了,当然两个月也没见过面。当然因为这个事搞的我心烦意乱,工作没法工作,21年底,22年初,也就是元旦期间,我向公司提出离职,电话裸辞,直接就不去公司了,给老板整的一脸懵逼。22年一月中旬,公司聚餐邀请了已经离职的我,晚上酒喝起兴的我,在同事的劝说下,我向老板表明了我想回到公司的意向,后来如愿以偿的回到了公司,此时,我的工资不是150一天了,而且达到了惊人的4.5k每月。

上班路上的金鸡湖大道

22年六月临近毕业,在实习公司沉淀了一年,我觉得时机已经成熟是时候走了,鼓起勇气和老板说了离职,老板同意了。这个时候我还不知道未来的一年,我还会和他们经常聚餐,一起聊行业、工作、生活。甚至今天的这份工作也得益于他们。

离职后,准备去南京发展,当时在常州的同学那暂住了几天,闲的没事干就投了几份简历玩玩,面试了两家都收到了offer,一家给政府做erp系统的公司给了7.5k,另一家是上市公司的外包给了8k,随后我就不想去南京了,选择了那家外包公司,在那前几个月基本上天天没事,过的相当的安逸,每天晚上下班后,5:30准备到球场,后来我换了个组长,我开始做MES系统了,第一个系统我身份是打杂的,给另一个同事当助手,后来做的系统,我开始当主力开发。22年底,工作干的十分不顺心,萌生了离职的想法,向外包公司的部门经理提了涨薪,他只给涨500块钱,我觉得也没必要留下了,所性直接离职,此时我还没有转正,所以我直接在一周到走人。

再次离职后,我选择回到老家休息一段时间,思考一下第二年该去往何处。在家乡待了近四十天,基本上没有碰过电脑,我到处的玩,打球,打游戏,泡澡,感觉已经废了。

过年前几天,我开始慌了,于是我重新打开我的小米笔记本,打开了熟悉又陌生的IDEA,学习了几个开源框架,背了一些面试题,准备年后去外地找工作。

CIM开源框架

大年初三,我早早的买好火车票去往常州,准备在常州找一份工作,可惜我找了近一周,一份工作也没有找到,于是我将目光看向南京和老东家所在的苏州。我联系了实习公司一个同事现在所在的公司,于是他将我内推到了现在的这个公司,他向上面的人担保我肯定没有问题,所以我直接跳过了面试,也就是在这个公司,因为我代码写的好,所以我两次加薪达到了税后五位数。

23年五月二十号,公司安排我去西安出差两周,这是我人生第一次出差,见到了网络上所谓的甲方,值得我纪念一下。

飞机上的云层 仓库

如今,那位内推我的同事,也就是我第一份实习公司的同事,他要走了,去了一家做大数据的公司,领导让我开始学习做管理,以后带新人做项目,我只能说尽力而为。

对于像我这样学历不高的人而言,个人觉得代码不是技术架构,而是人情世故,人脉是人生宝贵的一笔财富。

浪子花梦

上班摸鱼写于2023年7月12日11点。


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