注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

程序员能有什么好玩意?

从业10年了,看看一枚老成员都有什么好玩意(有个人的、同事的、公司的……)。【多图预警!!!摸鱼预警!!!】 桌面预警 桌面上放了个二层小架子,之前还有个盆栽的,可惜死掉了,悼念&缅怀+1。 喷雾预警 好几年前的习惯,之前是理肤泉的喷雾。当年的我还是...
继续阅读 »

从业10年了,看看一枚老成员都有什么好玩意(有个人的、同事的、公司的……)。【多图预警!!!摸鱼预警!!!


桌面预警


桌面上放了个二层小架子,之前还有个盆栽的,可惜死掉了,悼念&缅怀+1。


image.png


喷雾预警


好几年前的习惯,之前是理肤泉的喷雾。当年的我还是很暴躁的,需要一点水分帮我降降温,不过,当编程没有啥思路的时候,喷一喷感觉还不错。


image.png


养生预警


西洋参


有个同事是吉林的,某一天送给我一个山货大礼包,其中就有这瓶西洋参参片。偶尔会取几片泡水,当然喝茶的时候更多一些。【咖啡基本是戒了】


image.png


手串


年前,我领导说想弄个串儿盘着,防止老年痴呆。


我就买了些散珠自己串了些串,团队内,每人分了一串儿。


自己也留了些手串,每天选一串佩戴,主要是绕指柔的玩法。


image.png


image.png


image.png


茶事


喝茶也又些年头了,喝过好喝的,也扔过不好喝的。最近主要喝云南大白,家里的夫人也比较喜欢,


香道


疫情的风刮过来,听说艾草的盘香可以消毒杀菌,就买了盘香,还有个小香炉。周末在家会点一点,其实没那么好闻,但是仪式感满满的。


手霜


大概是东北恶劣的天气原因,办公室的手霜还是不少的,擦一擦,编码也有了仪式感。


盆栽


公司之前定了好多盆栽,我也选了一盆(其实是产品同学的,我的那盆已经养死了)。


image.png


打印机


家里买了台打印机,主要是打印一些孩子的东西,比如涂鸦的模版、还有孩子的照片。


image.png


工作预警


笔记本


大多用的是Mac,大概也不会换回Windows了。


image.png


耳机


还是用的有线耳机,没赶上潮流。哈哈


image.png


键盘


依然没赶上机械键盘的潮流,用的妙控……


面对疾风吧!


之前客户送的,小摆件。


image.png


证书


证书不少,主要是毕业时候发的,哈哈哈。



  1. 前年,公司组织学习了PMP,完美拿到了毕业后的第一个证书。

  2. 公司组织的活动的证书OR奖杯(干瞪眼大赛、乒乓球大赛、羽毛球大赛等),最贵的奖品应该是之前IDEA PK大赛获得的iwatch。

  3. 年会时发的证书。作为优秀的摸鱼份子,每年收到的表彰并不少,大多是个人的表彰,还有就是团队的证书,当然我更关心证书下面的奖金。

  4. 社区的证书。大致是技术社区的证书,嗯嗯,掘金的就一个,某年的2月优秀创作者,应该是这个。


家里的办公桌


夫人是个文艺女青年,喜欢装点我们的家,家里的办公桌的氛围还是很OK的。当然工作之余,也喜欢和夫人喝点小酒,我喜欢冰白,同好可以探讨哈。


image.png


悲伤的事情


疫情


疫情对我们的生活影响还是比较大的,特别是对我一个大龄程序员而言。


未来


今年打算给家庭计划一些副业,有余力的情况下,能够增加一些收入。人生已经过去了半数,感悟到生命的可贵,感情的来之不易,愿我们身边的人都越来越幸福。


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

2023面试真题之CSS篇

web
恐惧就是这样,你直视它、向前一步的时候它就消失了,选择相信自己能克服一切困难,勇敢向前,直面恐惧,就会发现之前的害怕,变成了心里的能量。大家好,我是柒八九。今天,我们继续2023前端面试真题系列。我们来谈谈关于CSS的相关知识点。如果,想了解该系列的文章,可以...
继续阅读 »

恐惧就是这样,你直视它、向前一步的时候它就消失了,选择相信自己能克服一切困难,勇敢向前,直面恐惧,就会发现之前的害怕,变成了心里的能量。

大家好,我是柒八九

今天,我们继续2023前端面试真题系列。我们来谈谈关于CSS的相关知识点。

如果,想了解该系列的文章,可以参考我们已经发布的文章。如下是往期文章。

文章list

  1. 2023前端面试真题之JS篇

你能所学到的知识点

  1. 盒模型 推荐阅读指数⭐️⭐️⭐️⭐️

  2. CSS的display属性有哪些值 推荐阅读指数⭐️⭐️⭐️⭐️

  3. position 里面的属性有哪些 推荐阅读指数⭐️⭐️⭐️⭐️

  4. flex里面的属性 推荐阅读指数⭐️⭐️⭐️⭐️⭐️

  5. flex布局的应用场景 推荐阅读指数⭐️⭐️⭐️⭐️

  6. CSS的长度单位有哪些 推荐阅读指数⭐️⭐️⭐️⭐️

  7. 水平垂直居中 推荐阅读指数⭐️⭐️⭐️⭐️⭐️

  8. {块级格式化上下文|Block Formatting Context} 推荐阅读指数⭐️⭐️⭐️⭐️⭐️

  9. 层叠规则 推荐阅读指数⭐️⭐️⭐️⭐️⭐️

  10. 重绘和重排 推荐阅读指数⭐️⭐️⭐️⭐️⭐️

  11. CSS引入方式(4种) 推荐阅读指数⭐️⭐️⭐️⭐️

  12. 硬件加速 推荐阅读指数⭐️⭐️⭐️⭐️⭐️

  13. 元素超出宽度...处理 推荐阅读指数⭐️⭐️⭐️⭐️⭐️

  14. 元素隐藏 推荐阅读指数⭐️⭐️⭐️⭐️⭐️

  15. Chrome支持小于12px 的文字 推荐阅读指数⭐️⭐️⭐️⭐️

好了,天不早了,干点正事哇。 img


盒模型

一个盒子由四个部分组成:contentpaddingbordermargin

  1. content

    ,即

    实际内容

    ,显示文本和图像

    • content 属性大都是用在::before/::after这两个伪元素

  2. padding

    ,即内边距,内容周围的区域

    • 内边距是透明

    • 取值不能为负

    • 受盒子的background属性影响

    • padding 百分比值无论是水平还是垂直方向均是相对于宽度计算

  3. boreder,即边框,围绕元素内容的内边距的一条或多条线,由粗细、样式、颜色三部分组成

  4. margin,即外边距,在元素外创建额外的空白,空白通常指不能放其他元素的区域

img

标准盒模型

img

  • 盒子总宽度 = width + padding + border + margin;

  • 盒子总高度 = height + padding + border + margin

也就是,width/height 只是内容宽高,不包含 paddingborder

IE 怪异盒子模型

img

  • 盒子总宽度 = width + margin;

  • 盒子总高度 = height + margin;

也就是,width/height 包含了 paddingborder

更改盒模型

CSS 中的 box-sizing 属性定义了渲染引擎应该如何计算一个元素的总宽度和总高度

box-sizing: content-box|border-box
复制代码
  1. content-box (默认值),元素的 width/height 不包含paddingborder,与标准盒子模型表现一致

  2. border-box 元素的 width/height 包含 paddingborder,与怪异盒子模型表现一致


CSS的display属性有哪些值

CSS display 属性设置元素是否被视为或者内联元素以及用于子元素的布局,例如流式布局网格布局弹性布局

形式上,display 属性设置元素的内部外部的显示类型。

  1. 外部类型设置元素参与流式布局;

  2. 内部类型设置子元素的布局(子元素的格式化上下文

常见属性值(8个)

  1. inline :默认

  2. block

  3. inline-block

  4. flex

  5. grid

  6. table

  7. list-item

  8. 双值的:只有Firefox70支持了这一语法


position 里面的属性有哪些

定义和用法:position 属性规定元素的定位类型。
说明:这个属性定义建立元素布局所用的 定位机制

  • 任何元素都可以定位

  • 绝对或固定元素会生成一个块级框,而不论该元素本身是什么类型。

  • 相对定位元素会相对于它在正常流中的默认位置偏移。

position 有以下可选值:(6个)

img

CSS 有三种基本的定位机制:普通流浮动绝对定位


flex里面的属性

容器的属性 (6个)

  1. flex-direction
    • 决定主轴的方向(即项目的排列方向)

    • row默认值):主轴为水平方向,起点在左端。

    • row-reverse:主轴为水平方向,起点在右端。

    • column:主轴为垂直方向,起点在上沿。

    • column-reverse:主轴为垂直方向,起点在下沿。

  2. flex-wrap
    • flex-wrap属性定义,如果一条轴线排不下,如何换行

    • nowrap:(默认):不换行。

    • wrap:换行,第一行在上方。

    • wrap-reverse:换行,第一行在下方

  3. flex-flow
    • flex-flow属性是flex-direction属性和flex-wrap属性的简写形式,默认值为row nowrap

  4. justify-content
    • justify-content属性定义了项目在主轴上的对齐方式。

    • flex-start默认值):左对齐

    • flex-end:右对齐

    • center: 居中

    • space-between两端对齐,项目之间的间隔都相等。

    • space-around:每个项目两侧的间隔相等。所以,项目之间的间隔比项目与边框的间隔大一倍

  5. align-items
    • align-items属性定义项目在交叉轴上如何对齐。

    • stretch默认值):如果项目未设置高度或设为auto,将占满整个容器的高度

    • flex-start:交叉轴的起点对齐。

    • flex-end:交叉轴的终点对齐。

    • center:交叉轴的中点对齐。

    • baseline: 项目的第一行文字的基线对齐。

  6. align-content
    • align-content属性定义了多根轴线的对齐方式

    • 如果项目只有一根轴线,该属性不起作用。

项目的属性(6个)

  1. order
    • order属性定义项目的排列顺序。

    • 数值越小,排列越靠前,默认为0

  2. flex-grow
    • flex-grow属性定义项目的放大比例

    • 默认为0,即如果存在剩余空间,也不放大

    • 如果所有项目的flex-grow属性都为1,则它们将等分剩余空间(如果有的话)

  3. flex-shrink
    • flex-shrink属性定义了项目的缩小比例

    • 默认为1,即如果空间不足,该项目将缩小。

    • 如果所有项目的flex-shrink属性都为1,当空间不足时,都将等比例缩小

  4. flex-basis
    • flex-basis属性定义了在分配多余空间之前,项目占据的{主轴空间|main size}。

    • 浏览器根据这个属性,计算主轴是否有多余空间。

    • 它的默认值为auto,即项目的本来大小。

  5. flex
    • flex属性是flex-grow, flex-shrinkflex-basis的简写,默认值为0 1 auto后两个属性可选

    • flex: 1 = flex: 1 1 0%

    • flex: auto = flex: 1 1 auto

  6. align-self

flex:1 vs flex:auto

flex:1flex:auto 的区别,可以归结于flex-basis:0flex-basis:auto的区别

当设置为0时(绝对弹性元素),此时相当于告诉flex-growflex-shrink在伸缩的时候不需要考虑我的尺寸

当设置为auto时(相对弹性元素),此时则需要在伸缩时将元素尺寸纳入考虑


flex布局的应用场景

  1. 网格布局

    • Grid- display:flex

    • Grid-Cell - flex: 1; flex:1使得各个子元素可以等比伸缩,flex: 1 = flex: 1 1 0%

  2. 百分比布局

    • img

    • col2 - flex: 0 0 50%;

    • col3 - flex: 0 0 33.3%;

    • img

  3. 圣杯布局

    • 页面从上到下,分成三个部分:头部(header),躯干(body),尾部(footer)。其中躯干又水平分成三栏,从左到右为:导航、主栏、副栏

    • img

    • container - display: flex; - flex-direction: column;- min-height: 100vh;

    • header/footer - flex: 0 0 100px;

    • body - display: flex; - flex:1

    • content - flex: 1;

    • ads/av - flex: 0 0 100px;

    • nav - order: -1;

    • img

  4. 侧边固定宽度

    • 侧边固定宽度,右边自适应

    • img

    • aside1 - flex: 0 0 20%;

    • body1 - flex:1

  5. 流式布局

    • 每行的项目数固定,会自动分行

    • img

    • container2 - display: flex; - flex-flow: row wrap;

    • img


CSS的长度单位有哪些

  1. 相对长度

    • 相对长度单位指的是这个单位没有一个固定的值,它的值受到其它元素属性(例如浏览器窗口的大小、父级元素的大小)的影响,在响应式布局方面相对长度单位非常适用

    • img

  2. 绝对长度

    • 绝对长度单位表示一个真实的物理尺寸,它的大小是固定的,不会因为其它元素尺寸的变化而变化

    • img


水平垂直居中

  1. 宽&高固定

    1. absolute + 负 margin

    2. absolute + margin auto

    3. absolute + calc

  2. 宽&高不固定

    1. absolute + transform: translate(-50%, -50%);

    2. flex布局

    3. grid 布局

宽&高固定

absolute + 负 margin

.parent {
+ position: relative;
}

.child {
width: 300px;
height: 100px;
padding: 20px;

+ position: absolute;
+ top: 50%;
+ left: 50%;
+ margin: -70px 0 0 -170px;
}

复制代码

img

  • 初始位置为方块1的位置

  • 当设置left、top为50%的时候,内部子元素为方块2的位置

  • 设置margin为负数时,使内部子元素到方块3的位置,即中间位置

absolute + margin auto

img

absolute + calc

img


宽&高不固定

absolute + transform: translate(-50%, -50%);

.parent {
position: relative;
}
.child {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
复制代码

flex布局

.parent {
display: flex;
justify-content: center;
align-items: center;
}
复制代码

grid布局

.parent {
display:grid;
}
.parent .child{
margin:auto;
}
复制代码

{块级格式化上下文|Block Formatting Context}

{块级格式化上下文|Block Formatting Context}(BFC),它是页面中的一块渲染区域,并且有一套属于自己的渲染规则:(6个)

  1. 内部的盒子会在垂直方向一个接一个的放置

  2. 对于同一个BFC的俩个相邻的盒子的margin会发生重叠,与方向无关。

  3. 每个元素的左外边距与包含块的左边界相接触(页面布局方向从左到右),即使浮动元素也是如此

  4. BFC的区域不会与float的元素区域重叠

  5. 计算BFC的高度时,浮动子元素也参与计算

  6. BFC就是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面的元素,反之亦然

触发条件 (5个)RFODP

  1. 根元素,即HTML元素

  2. 浮动元素float值为left、right

  3. overflow值不为 visible,为 autoscrollhidden

  4. display的值为inline-block、table、inline-table、flex、inline-flex、grid、inline-grid

  5. position 的值为absolutefixed

应用场景

  1. 防止

    margin

    重叠

    • 将位于同一个BFC的元素,分割到不同的BFC中

  2. 高度塌陷

    • 计算BFC的高度时,浮动子元素也参与计算

    • 子元素浮动

    • 父元素 overflow: hidden;构建BFC

  3. 多栏自适应

    • BFC的区域不会与float的元素区域重叠

    • aside -float:left

    • main -margin-left:aside-width -overflow: hidden构建BFC


层叠规则

所谓层叠规则,指的是当网页中的元素发生层叠时的表现规则。

z-index:z-index属性只有和定位元素(position不为static的元素)在一起的时候才有作用。

CSS3中,z-index已经并非只对定位元素有效,flex盒子的子元素也可以设置z-index属性。

层叠上下文的特性

  • 层叠上下文的层叠水平要比普通元素高

  • 层叠上下文可以阻断元素的混合模式

  • 层叠上下文可以嵌套,内部层叠上下文及其所有元素均受制于外部的层叠上下文

  • 每个层叠上下文和兄弟元素独立

    • 当进行层叠变化或渲染的时候,只需要考虑后代元素

  • 每个层叠上下文是自成体系的,当元素发生层叠的时候,整个元素被认为是在父层叠上下文的层叠顺序中

层叠上下文的创建(3类)

由一些CSS属性创建

  1. 天生派

    • 页面根元素天生具有层叠上下文

    • 根层叠上下文

  2. 正统派

    • z-index值为数值的定位元素的传统层叠上下文

  3. 扩招派

    • 其他CSS3属性

根层叠上下文

指的是页面根元素,页面中所有的元素一定处于至少一个层叠结界中

定位元素与传统层叠上下文

对于position值为relative/absolute的定位元素,当z-index值不是auto的时候,会创建层叠上下文。

CSS3属性(8个)FOTMFIWS

  1. 元素为flex布局元素(父元素display:flex|inline-flex),同时z-index不是auto - flex布局

  2. 元素的opactity值不是1 - {透明度|opactity}

  3. 元素的transform值不是none - {转换|transform}

  4. 元素mix-blend-mode值不是normal - {混合模式|mix-blend-mode}

  5. 元素的filter值不是none - {滤镜|filter}

  6. 元素的isolation值是isolate - {隔离|isolation}

  7. 元素的will-change属性值为上面②~⑥的任意一个(如will-change:opacity

  8. 元素的-webkit-overflow-scrolling设为touch


重绘和重排

页面渲染的流程, 简单来说,初次渲染时会经过以下6步

  1. 构建DOM树;

  2. 样式计算;

  3. 布局定位

  4. 图层分层;

  5. 图层绘制

  6. 合成显示

CSS属性改变时,重渲染会分为回流重绘直接合成三种情况,分别对应从布局定位/图层绘制/合成显示开始,再走一遍上面的流程。

元素的CSS具体发生什么改变,则决定属于上面哪种情况:

  • 回流(又叫重排):元素位置、大小发生变化导致其他节点联动,需要重新计算布局;

  • 重绘:修改了一些不影响布局的属性,比如颜色;

  • 直接合成:合成层transform、opacity修改,只需要将多个图层再次合并,而后生成位图,最终展示到屏幕上;

触发时机

回流触发时机

回流这一阶段主要是计算节点的位置和几何信息,那么当页面布局和几何信息发生变化的时候,就需要回流。

  • 添加或删除可见的DOM元素

  • 元素的位置发生变化

  • 元素的尺寸发生变化(包括外边距、内边框、边框大小、高度和宽度等)

  • 内容发生变化,比如文本变化或图片被另一个不同尺寸的图片所替代

  • 页面一开始渲染的时候(这避免不了)

  • 浏览器的窗口尺寸变化(因为回流是根据视口的大小来计算元素的位置和大小的)

  • 获取一些特定属性的值

    • offsetTop、offsetLeft、 offsetWidth、offsetHeight

    • scrollTop、scrollLeft、scrollWidth、scrollHeight

    • clientTop、clientLeft、clientWidth、clientHeight

    • 这些属性有一个共性,就是需要通过即时计算得到。因此浏览器为了获取这些值,也会进行回流。

重绘触发时机

触发回流一定会触发重绘

除此之外还有一些其他引起重绘行为:

  • 颜色的修改

  • 文本方向的修改

  • 阴影的修改

浏览器优化机制

由于每次重排都会造成额外的计算消耗,因此大多数浏览器都会通过队列存储重排操作并批量执行来优化重排过程。浏览器会将修改操作放入到队列里,直到过了一段时间或者操作达到了一个阈值,才清空队列。

当你获取布局信息的操作的时候,会强制队列刷新,例如offsetTop等方法都会返回最新的数据。

因此浏览器不得不清空队列,触发回流重绘来返回正确的值

减少回流

  1. 对于那些复杂的动画,对其设置 position: fixed/absolute,尽可能地使元素脱离文档流,从而减少对其他元素的影响

  2. 使用css3硬件加速,可以让transformopacityfilters这些动画不会引起回流重绘

  3. 在使用 JavaScript 动态插入多个节点时, 可以使用DocumentFragment.创建后一次插入.

  4. 通过设置元素属性display: none,将其从页面上去掉,然后再进行后续操作,这些后续操作也不会触发回流与重绘,这个过程称为离线操作


CSS引入方式(4种)

  1. 内联方式

    • <div style="background: red"></div>

  2. 嵌入方式

    • HTML 头部中的 <style> 标签下书写 CSS 代码

  3. 链接方式

    • 使用 HTML 头部的 <head> 标签引入外部的 CSS 文件。

    • <link rel="stylesheet" type="text/css" href="style.css">

  4. 导入方式

    • 使用 CSS 规则引入外部 CSS 文件

比较链接方式和导入方式

链接方式(用 link )和导入方式(用 @import)都是引入外部的 CSS 文件的方式

  • link 属于 HTML,通过 <link> 标签中的 href 属性来引入外部文件,而 @import 属于 CSS,所以导入语句应写在 CSS 中,要注意的是导入语句应写在样式表的开头,否则无法正确导入外部文件;

  • @importCSS2.1 才出现的概念,所以如果浏览器版本较低,无法正确导入外部样式文件;

HTML 文件被加载时,link 引用的文件会同时被加载,而 @import 引用的文件则会等页面全部下载完毕再被加载;


硬件加速

浏览器中的层分为两种:渲染层合成层

渲染层

渲染层的概念跟层叠上下文密切相关。简单来说,拥有z-index属性的定位元素会生成一个层叠上下文,一个生成层叠上下文的元素就生成了一个渲染层。

层叠上下文的创建(3类)

由一些CSS属性创建

  1. 天生派

    • 页面根元素天生具有层叠上下文

    • 根层叠上下文

  2. 正统派

    • z-index值为数值的定位元素的传统层叠上下文

  3. 扩招派 (CSS3属性)

    1. 元素为flex布局元素(父元素display:flex|inline-flex),同时z-index不是auto - flex布局

    2. 元素的opactity值不是1 - {透明度|opactity}

    3. 元素的transform值不是none - {转换|transform}

    4. 元素mix-blend-mode值不是normal - {混合模式|mix-blend-mode}

    5. 元素的filter值不是none - {滤镜|filter}

    6. 元素的isolation值是isolate - {隔离|isolation}

    7. 元素的will-change属性值为上面②~⑥的任意一个(如will-change:opacity

    8. 元素的-webkit-overflow-scrolling设为touch

合成层

只有一些特殊的渲染层才会被提升为合成层,通常来说有这些情况:

  1. transform:3D变换:translate3dtranslateZ

  2. will-change:opacity | transform | filter

  3. opacity | transform | fliter 应用了过渡和动画(transition/animation

  4. video、canvas、iframe

硬件加速

浏览器为什么要分层呢?答案是硬件加速。就是给HTML元素加上某些CSS属性,比如3D变换,将其提升成一个合成层,独立渲染

之所以叫硬件加速,就是因为合成层会交给GPU(显卡)去处理,在硬件层面上开外挂,比在主线程(CPU)上效率更高。

利用硬件加速,可以把需要重排/重绘的元素单独拎出来,减少绘制的面积。

避免重排/重绘,直接进行合成,合成层的transformopacity的修改都是直接进入合成阶段的;

  • 可以使用transform:translate代替left/top修改元素的位置;

  • 使用transform:scale代替宽度、高度的修改;


元素超出宽度...处理

单行 (AKA: TWO)

  1. text-overflow:ellipsis:当文本溢出时,显示省略符号来代表被修剪的文本

  2. white-space:nowrap:设置文本不换行

  3. overflow:hidden:当子元素内容超过容器宽度高度限制的时候,裁剪的边界是border box的内边缘

p{
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
width:400px;
}
复制代码

多行

  1. 基于高度截断(伪元素 + 定位)

  2. 基于行数截断()

基于高度截断

关键点 height + line-height + ::after + 子绝父相

核心的css代码结构如下:

.text {
position: relative;
line-height: 20px;
height: 40px;
overflow: hidden;
}
.text::after {
content: "...";
position: absolute;
bottom: 0;
right: 0;
padding: 0 20px 0 10px;
}
复制代码

基于行数截断

关键点:box + line-clamp + box-orient + overflow

  1. display: -webkit-box:将对象作为弹性伸缩盒子模型显示

  2. -webkit-line-clamp: n:和①结合使用,用来限制在一个块元素显示的文本的行数(n)

  3. -webkit-box-orient: vertical:和①结合使用 ,设置或检索伸缩盒对象的子元素的排列方式

  4. overflow: hidden

p {
width: 300px;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
复制代码

元素隐藏

可按照隐藏元素是否占据空间分为两大类(6 + 3)

  1. 元素不可见,不占空间

    (

    3absolute

    +

    1relative

    +

    1script

    +

    1display

    )

    1. <script>

    2. display:none

    3. absolute + visibility:hidden

    4. absolute + clip:rect(0,0,0,0)

    5. absolute + opacity:0

    6. relative+left负值

  2. 元素不可见,占据空间

    (3个)

    1. visibility:hidden

    2. relative + z-index负值

    3. opacity:0

元素不可见,不占空间

<script>

<script type="text/html">
<img src="1.jpg">
</script>
复制代码

display:none

其他特点:辅助设备无法访问,资源加载,DOM可访问

对一个元素而言,如果display计算值是none,则该元素以及所有后代元素都隐藏

.hidden {
display:none;
}
复制代码

absolute + visibility

.hidden{
position:absolute;
visibility:hidden;
}
复制代码

absolute + clip

.hidden{
position:absolute;
clip:rect(0,0,0,0);
}
复制代码

absolute + opacity

.hidden{
position:absolute;
opacity:0;
}
复制代码

relative+负值

.hidden{
position:relative;
left:-999em;
}
复制代码

元素不可见,占据空间

visibility:hidden

visibility 的继承性

  • 父元素设置visibility:hidden,子元素也看不见

  • 但是,如果子元素设置了visibility:visible,则子元素又会显示出来

.hidden{
visibility:hidden;
}
复制代码

relative + z-index

.hidden{
position:relative;
z-index:-1;
}
复制代码

opacity:0

.hidden{
opacity:0;
filter:Alpha(opacity=0)
}
复制代码

总结

最常用的还是display:nonevisibility:hidden,其他的方式只能认为是奇招,它们的真正用途并不是用于隐藏元素,所以并不推荐使用它们。

关于display: nonevisibility: hiddenopacity: 0的区别,如下表所示:

img


Chrome支持小于12px 的文字

Chrome 中文版浏览器会默认设定页面的最小字号是12px,英文版没有限制

原由 Chrome 团队认为汉字小于12px就会增加识别难度

  • 中文版浏览器 与网页语言无关,取决于用户在Chrome的设置里(chrome://settings/languages)把哪种语言设置为默认显示语言

  • 系统级最小字号 浏览器默认设定页面的最小字号,用户可以前往 chrome://settings/fonts 根据需求更改

解决方案(3种)

  1. zoom

  2. transform:scale()

  3. -webkit-text-size-adjust:none

zoom

zoom 可以改变页面上元素的尺寸,属于真实尺寸。

其支持的值类型有:

  • zoom:50%,表示缩小到原来的一半

  • zoom:0.5,表示缩小到原来的一半

.span10{
font-size: 12px;
display: inline-block;
zoom: 0.8;
}
复制代码

transform:scale()

transform:scale()这个属性进行放缩

使用scale属性只对可以定义宽高的元素生效,所以,需要将指定元素转为行内块元素

.span10{
       font-size: 12px;
       display: inline-block;
       transform:scale(0.8);
  }
复制代码

text-size-adjust

该属性用来设定文字大小是否根据设备(浏览器)来自动调整显示大小

属性值:

  • auto默认,字体大小会根据设备/浏览器来自动调整;

  • percentage:字体显示的大小

  • none:字体大小不会自动调整

存在兼容性问题,chrome受版本限制,safari可以


后记

分享是一种态度

全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。

img

作者:前端小魔女
来源:https://juejin.cn/post/7203153899246780453

收起阅读 »

非科班三本程序员入行这几年

一直以来都想写下自己的经历,今天看了下掘金的前辈们写的经历,觉得也有点感慨,恰逢最近工作也不是很如意。也想写点什么,缓解下焦虑紧张的心情。 我09年中考,考了512分,没能过县里的一中公费分数线。家里条件不好,所以去的县二中。一开始我也没想好到底是继续读书还是...
继续阅读 »

一直以来都想写下自己的经历,今天看了下掘金的前辈们写的经历,觉得也有点感慨,恰逢最近工作也不是很如意。也想写点什么,缓解下焦虑紧张的心情。


我09年中考,考了512分,没能过县里的一中公费分数线。家里条件不好,所以去的县二中。一开始我也没想好到底是继续读书还是去打工,家里的姐姐初中毕业就因为家里穷辍学去打工了,老父亲从小也是上过高中的,那个年代上高中还是很少的,可能对读书也有点执念,希望我继续读下去。当时去二中的想法很简单,虽然成绩不是很好,但是觉得年龄还小,出去打工感觉这辈子还有的是时间,不如去高中试下水读的好就继续读,读不好就像老家大多数年轻壮劳力一样,出国打黑工赚点快钱,过个几年回家娶个媳妇,大家不都是这么过的吗?心里暗暗觉得也是不错的。


WechatIMG12.jpeg


09年开始了高中的学习生活,虽然刚开始来的时候是抱着试试的态度,但实际上我也算是个有干啥像啥的本性,在高中一直也都算是努力学习着,虽说高考成绩不尽如意,但也算是尽力了。我们高中是县里的二中,每年的二本上线的学生屈指可数,我一般的考试成绩基本维持在全年级理科13名左右,高考考了457分,当时也纠结要不要复读一年,县一中打电话说我的成绩可以在县一中免费复读,我又抱着要考个好学校的决心决定再试一次,可到了复习班以后,根本没这个心情,复习班在7月就开始了上课,我只读了三天就回家了,当时在复读班静不下心来学习,实在是读不下去。


WechatIMG11.jpeg
12年7月10号左右,跟随着几个高中同学去了市里的工地,干起了暑假工。一边干暑假工,一边想着填报志愿。虽说考的分不高,但是还是决定要继续读书,说不好到底为什么要读书,家里条件又不好,可能冥冥中就是宿命吧,也可能是对打工生涯的一种逃避,也可能是对大学的幻想,也可能是幻想着读了大学就会实现阶级的跨越。


到工地第一天的早晨,早上吃的看不见几个饭粒的稀米粥,不知道食堂老头从哪里搞来的很难吃的馒头,像是假的一样没有一点面的味道,菜就是那种最便宜的萝卜咸菜。这吃食,就算监狱也不过如此吧! 吃饭完,宿舍门口来了一车木板,那种木匠用来支模板的木板子,上面还有好多钉子,我在车上卸货,被扎了好几次,天上还下着濛濛细雨,但工长丝毫没有让我们停下来进屋的意思。就这样顶着雨卸完了一车货。工地的宿舍也是常人无法想象的脏乱差,那种很多年的砖瓦房,里面的床铺能大幅度晃动,我在上铺根本不敢动,上去了就是稳稳的趴着或坐着。宿舍长度大概有两间房那么大,里面还在过道摆满了几根大铁管子,铺下面摆满了垃圾和剩饭,还有写日常的脸盆饭缸等。


宿舍大概有十来个人,我下铺住着的是两个开塔吊的工友,当时听说一天150元,我们这种力工一天80元。门口住着的是小工长,负责给大家分活。里面的有调度塔吊的师傅,有开搅拌机的,还有个老头专门筛沙子,还有个老滑头是在楼上负责清理垃圾的。宿舍的晚上呼噜声震天响,7月的天气要把人蒸熟了,不知道这样的日子我能坚持到多久,感觉这种生活真的是折磨。每天早上七点上班,晚上七点下班,中午休息一个半小时。我在这期间,刚开始几天跟着工长放线,超平,后来跟着大叔开了几天搅拌机,听这个大叔说,他家的孩子也读了大学了,他还给孩子在市里买了楼。我心想我要有这么能挣钱的爸爸就好了。再后来工地打灰,也就是用那种水泥罐车和喷水泥的机器来给楼上供水泥。在打灰时候,我负责拎着那个震动棒的电机,水泥灰喷到哪里,就要用震动棒震一下,确保水泥凝固时没有空心的。震动棒的电机是真的沉啊,还要在水泥路里面来回趟着走,还要再拎着电线。当时我还是个18岁的孩子,感觉人生真的好难啊!再后来工地没活时候跟着大叔去其他工地做售后,給人家的下水道通下水,当时是去了六个核桃的研发单位,里面开着空调,到处飘着核桃露的香味,每罐核桃露打开抽取少部分的样品就倒掉了,我心想要是不到掉让我喝个痛快该有多好啊。在修下水道这几天,庆幸的是这里有空调,比起外面像烤箱一样的日子,感觉更舒服一些。


在工地干了十多天后,实在是受不了工地的生活就回家了。最后报考了省里的一所三本院校,到了大学以后,好像一下没了目标,每天除了上课学习外,也有更多自己的时间了,有的人想去学生会锻炼一下,有的人趁有时间开始谈恋爱,那时候12年手机还没有那么多娱乐的应用,用的最多的还是qq,游戏也只有奋斗小鸟,削水果,赛车等。我刚上学用的酷派的什么3gs,花了300块买的,是同学中最次的那种。心里莫名有一种自卑的感觉。当时觉得学生会真的没啥锻炼人的,像是一种虚假的官职一样,好多学生觉得当上了学生会就是领导,高别人一等。这种做派实在看不惯,也就一直没加入其中。谈恋爱就更没我啥事了,家里穷没自信,虽然有时候也有萌动的时候,但是清醒后,还是觉得谈恋爱不适合我。回顾大学的生活,现在也令人向往,没有赚钱的压力,也没有学习的压力,还有很多随意支配的时间,还有很多同学可以一起陪着玩,也许这就是这辈子最幸福的时光吧。


14年夏天,时间一晃大二结束了,我学的是土木工程,那个暑假在学校附近找了个设计院打算提前感受下工作的生活。我进的组是设计院的结构设计组。相对互联网行业的产品,开发,测试,运营。设计院也有一套的研发流程,有整体方案设计,建筑设计,结构设计,给排水等。虽然每天没有工资,但是也每天都按时上下班,14年的时候,在设计院里已经预感到行业的萧条,我们设计院在我们市里还算不错的私企,但是都没有什么活,好多人工作几年还是拿着一千块的基本工资,住着公司提供的免费宿舍,这样的待遇别说买房娶媳妇了,维持生活都难。那时候开始我就想着要作出点行动了,这个行业已经不行了,不能以后眼看着自己往火堆里跳。大概在设计院呆了一个月后,我回家了,我觉得在设计院没啥前景,学习也没啥太大意思。


到大三结束,好多人开始找实习单位了,也有一些牛鬼蛇神的公司来学校宣讲,看了下没有啥正经工作,除了销售就是些没人去的工作。我内心也有点荒,家里没钱,也没背景,自己这三年更是没学到一点本事。接着开始逛了下人才市场,发现根本没有啥正经岗位,索性直接去了几个工地,问家人要人不,工地的人可能也没见过这样的找工作的,对我们来工地找工作感到很奇怪,没等我们多说就把我们打发走了。恰好后来同学群里说我老家有个人招房屋土地测量的,管吃住还能有1200的实习工资。我心想反正也比呆着强,就去试试吧。到了单位应该是6月17号左右,所在地是一个工厂里面,租了几间办公室和宿舍。做的工作是到农村里面测量每家房屋实际占用的大小,天气很炎热,我干了大概一个月左右吧,就辞职不干了,觉得是在是没啥意思,学不到本事,以后也不可能做这个行业了,当时觉得以后是不会再从事建筑行业了,没有一点希望。


2015年7月13号,我直接买的票从保定到北京报名培训。在北京报名了android开发的培训。当时我一个高中同学在那里,我也算投奔她去的吧。当时我觉得趁现在还没有毕业,还有时间踏踏实实静下心来学点本事,要真等到毕业,那可真的就没时间也没机会了。同时也觉得建筑行业看不到未来,计算机起码是凭个人能力找工作,总不可能啥也不会凭关系进公司吧。之前在设计院实习时候,有个同事是研发经理,大学时候学的是机械的,我想着他都能转行我肯定也可以。还有就是在实习期间,有个考研宿舍可以住,就和计算机专业的几个同学一起住一个宿舍,也算是稍微了解了下这个行业。就这样误打误撞进入了这个行业,有时候甚至觉得冥冥之中是命中注定要从事这一行。有些事真的是说不清楚,可能就是命里的安排,当时报名费是一万五千元,我这么穷的家庭居然也敢报名,能上学都是极限了。可真到报名时候,我爸居然同意了,同意我花钱培训。虽然我本来打算贷款培训,但是需要家里人签字,家里人觉得要真的想培训那就全款交学费吧,在她们印象中,贷款还是太危险了,觉得搞不好就陷入高利贷中一样。


潘家园桥.jpeg


即使站在风口上,也没有一帆风顺的事。找工作入行也同样费尽了周折。12月培训结束后,跟着同一期的同学投简历,觉得自己还是啥也不会,很幼稚的也跟着去面试,以为也许某个傻瓜公司领导能看中我。现在想想自己那时候还是想当然了,以为像培训公司宣传的一样,培训完了就能找到工作,甚至能月薪过万。我想着自己虽然不过万,能找个五千月薪的也行啊,起码能自己养活自己,不用在工地上忍受风吹日晒,而且周六日都不休息。跟着投了一周简历后没有收到任何面试通知,可笑的是,我舍友有面试机会,后来我就和他一起去面试了,只要他收到面试邀请,我也跟着一起去,他面试完了,我也顺便面试一下。结果也可想而知,根本没人要我。


眼看着在这耗下去也不是个办法,吃住用度也比学校贵,就先回学校了。在学校一边复习培训的内容,一边准备学校的考试。想着过了年再去北京试试。


过了年,也就是大四的最后一个学期了。我这时候虽然没有培训的贷款,也没有助学贷款,但是因为买了个新电脑和平时用度比较多,借了点贷款。同时来北京找工作租房子等花费也比较多,找工作也就压力更大了。好在找到了一个工作,在丰台那边,月薪3500,虽然低点,但是总算能入行了,心里很高兴。这家公司基本都是应届生或者没毕业的,做的工作也都不是很懂,都是些很低端的外包工作,我和CTO说我的领导啥也不会,写的东西我觉得还如我写的呢,啥也不是。领导说那要不你来搭个框架?我说我也没这个实力,结果没过几天,我被通知走人。就这样我职业生涯的第一份工作还不到一个月就结束了。后来又在朝阳区找了个月薪4000的工作,同样也是因为能力不足,被老板赶走了,第二份工作也是一个月左右。这时候也快毕业了,开始回学校准备毕业设计,一边准备毕业设计,一边学习编程技能,盼望着毕业后能找个如意的工作,做毕设期间,是最后一段无忧无虑在的大学时光,不在北京那种高节奏环境下,不再想找工作的烦恼,感觉身心很放松,拍毕业照,吃散伙饭,送走一个个同学,大学生活仿佛像一场梦一样,梦醒了,人生的烦恼接踵而至。


本来想记录下最近几年的焦虑,没想到一写起来就铺垫太多了,写了好多关于上学和家庭的琐碎。


16年夏天,大学毕业后,又杀回了北京,虽然接近半年的学习和有小部分的工作经历,但是投简历还是仿佛石沉大海,看着招聘网站上写的要求3年工作经验,仿佛是我无法逾越的鸿沟。我痛苦着,绝望着,也许是毕业与失业的同时遭遇,一个人在出租屋里终于爆发了,一下子开始痛哭流涕,想起了前几天毕业时候有的同学难过的哭了,当时我没哭,这一刻终究还是没忍住。振作起来后,又开始找工作,去了一个清华硕士的创业项目,同样也是做了一周,工资也没要我就又离职了。我觉得这个人也太不靠谱了,招几个人啥都没有就开始创业?更像是忽悠人。这时候我想着既然北京没机会,何妨不去天津试试呢,就这样,我投了几家天津的公司,从北京坐火车去天津面试。恰好有一家要我了,看上去这家公司还挺靠谱的,有宽敞的大会议室,整洁的工位。当时心里很开心,希望这次能稳下来。


入职天津公司后,工作了一年多,到17年10月我裸辞来北京面试了,因为我知道,迟早是要来北京的,和天津的工资差太多了,工作时间越长差距越大,想趁着年轻多在北京赚点钱。就这样又入职了北京一家做零售的小公司,给自动贩卖机上的android系统做售卖。干了三天又跑路了,觉得和互联网行业仿佛是两个行业,公司也小,人也很少,即便稳定,我也不想留下来。过了几天又面试了一家小的创业公司,在慈云寺那边,同样是因为和同事不合,干了将近五个月,觉得老板不靠谱又离职了。


再后来从18年4月到现在又先后入职了三家公司,都算是比较靠谱的公司,年薪也从20+到现在的40+,最近感觉又遇到了瓶颈。虽然这几年一直从事安卓开发。但是说实话没有什么真本事,能力更谈不上有啥大的突破,每次跳槽都是临时背题。本来这篇文章想写写这几年我是怎么混的这么惨的,工作六年多还是没有达到六年经验应有的水平。
WechatIMG19.jpeg
从16年下半年开始就跟着动脑学院的安卓视频课程学习,学的越多,感觉不会的越多,自己基础差,学历低,又不是科班出身,所以压力很大。结果可能是走了很多歪路,到17年下半年,花了很多精力在乱学,没有什么章法,虽然平时也注重安卓的一些基础,但是觉得进大公司还差的太多。后来18年学了很多音视频相关的ffmpeg,C语言,WebRtc,音视频编解码基础,OpenGl,还有黑马程序员的JavaWeb开发等等学的很杂。当时想着虽然主营安卓,但是如果靠着音视频这条专项应该会很吃香吧,结果并没有学到音视频开发应有的高度,Android相关的也没有太大的提高,导致在开发市场并不是很吃香。学JavaWeb主要是想着以后去别的新一线城市能够更有优势。


后来想多学习下Grade.Kotlin等,发现项目中用不到的话,光学效果也不是很好。所以又开始学习计算机基础相关的,这也是受陈皓指点的吧,想成为高手就要从计算机系统,网络,C语言等开始学起,也确实坚持了小半年,看了计算机操作系统,汇编语言,还有些极客时间里面的一些Linux等。也有跟着哈工大的李志军老师学习计算机操作系统。看了这些之后,还是觉得自己真的没办法踏下心来一心学下去,这里有大神坚持下去并有收获的可以指点一下。


WechatIMG14.jpeg
总之吧,入行程序员做Android开发这几年,我不后悔选择这一行,是这份工作让我从一穷二白,到现在靠自己结了婚,也买了房子。但是也有遗憾,遗憾的是没能够像众多优秀的开发者一样,达到行业内较高的高度,现在只能说是一个没有任何核心竞争力的随时可替换的螺丝钉。


现如今还是想找到自己的目标,希望自己能够朝着这个目标不断努力,能够让自己能够在开发的行业里多干几年,尤其是今年这种环境,谁也说不好明天还是不是还在公司。但是什么才是目标呢?每天刷题准备面试让自己保持这种面试的竞争力难道就是目标吗?没完没了的看框架源码,学习新知识这样让自己能够不被这个行业淘汰难道就是普通开发者的目标吗?


还是像那些大神那样下狠功夫把各种基础的计算机理论学透?如果是这样只是学不运用于工作又能有多大的收获呢?


哎 可能是上高中时候被洗脑养成的焦虑的性格,也可能是身处这个行业,这个城市让自己无形之中就会有思想上的包袱。先写到这里吧,后面有新的理解和收获再来补充。


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

工作 7 年的老程序员,现在怎么样了

犹记得高中班主任说:“大家努力考上大学,大学没人管,到时候随便玩”。我估计很多老师都这么说过。 我考上大学(2010年)之前也是这么过的。第一年哥哥给买了个一台华硕笔记本电脑。那个年代买华硕的应该不少,我周边就好几个。有了电脑之后,室友就拉着我一起 cs,四个...
继续阅读 »

犹记得高中班主任说:“大家努力考上大学,大学没人管,到时候随便玩”。我估计很多老师都这么说过。


我考上大学(2010年)之前也是这么过的。第一年哥哥给买了个一台华硕笔记本电脑。那个年代买华硕的应该不少,我周边就好几个。有了电脑之后,室友就拉着我一起 cs,四个人组队玩,那会觉得很嗨,上头。


后来看室友在玩魔兽世界,那会不知道是什么游戏,就感觉很好玩,再后来就入坑了。还记得刚开始玩,完全不会,玩个防骑,但是打副本排DPS,结果还被人教育,教育之后还不听(因为别的职业不会玩),就经常被 T 出组。之后,上课天天看游戏攻略和玩法,或者干脆看小说。前两年就这么过去了


1 跟风考研


大三开始,觉得这么混下去不行了。在豆瓣上找了一些书,平时不上课的时候找个自习室学习。那会家里打电话说有哪个亲戚家的孩子考研了,那是我第一次知道“考研”这个词。那会在上宏微观经济学的课,刚好在豆瓣上看到一本手《牛奶面包经济学》,就在自习室里看。刚好有个同院系的同学在里面准备考研,在找小伙伴一起战斗(毕竟,考研是一场长跑,没有同行者,会很艰难)。我一合计,就加入了他们的小团队。从此成为“中国合伙人”(刚好四个人)中的一员。


我那会也不知道毕业了之后能去哪些公司,能找哪些岗位,对于社会完全不了解,对于考研也是完全不了解。小团队中的三个人都是考金融学,我在网上查,知道了学硕和专硕的区别,也知道专硕学费贵。我家里没钱,大学时期的生活费都是自己去沃尔玛、麦当劳、发传单挣得,大学四年,我在沃尔玛工作超过了 2 年、麦当劳半年,食堂倒盘子半年,中途还去发过传单,暑假还去实习。没钱,他们考金融学专硕,那我就靠经济学学硕吧,学硕学费便宜。


从此开始了考研之路。


2 三次考研


大三的时候,报名不是那么严格,混进去报了名,那会还没开始看书,算是体验了一把考研流程;


还记得那次政治考了 48 分,基本都过了很多学校的单科线,那会就感觉政治最好考(最后发现,还是太年轻)。


大四毕业那年,把所有考研科目的参数书都过了 2 遍,最后上考场,最后成绩也就刚过国家线。


毕业了,也不知道干啥,就听小伙伴的准备再考一次,之前和小伙伴一起来了北京,租了个阳台,又开始准备考研。结果依然是刚过国家线。这一年也多亏了一起来北京的几个同学资助我,否则可能都抗不过考试就饿死街头了。


总结这几次考研经历,失败的最大原因是,我根本不知道考研是为了什么。只是不知道如果工作的话,找什么工作。刚好别人提供了这样一个逃避工作的路,我麻木的跟着走而已。这也是为什么后面两次准备的过程中,一有空就看小说的原因。


但是,现在来看,我会感谢那会没有考上,不然就错过了现在喜欢的技术工作。因为如果真的考上了经济学研究生,我毕业之后也不知道能干啥,而且金融行业的工作我也不喜欢,性格上也不合适,几个小伙伴都是考的金融,去的券商,还是比较了解的。


3 入坑 JAVA 培训


考完之后,大概估了分,知道自己大概率上不了就开始找工作了。那会在前程无忧上各种投简历。开始看到一个做外汇的小公司,因为我在本科在一个工作室做过外汇交易相关的工作,还用程序写了一段量化交易的小程序。


所以去培训了几天,跟我哥借了几千块钱,注册了一个账号,开始买卖外汇。同时在网上找其他工作。


后面看介绍去西二旗的一家公司面试,说我的技术不行,他们提供 Java 培训(以前的套路),没钱可以贷款。


我自己也清楚本科一行 Java 代码没写过,直接工作也找不到工作。就贷款培训了,那会还提供住宿,跟学校宿舍似的,上下铺。


4 三年新手&非全研究生


培训四个月之后,开始找工作。那会 Java 还没这么卷,而且自己还有个 211 学历,一般公司的面试还是不少的。但是因为培训的时候学习不够刻苦(也是没有基础)。最后进了一个小公司,面试要 8000,最后给了 7000。这也是我给自己的最底线工资,少于这个工资就离开北京了,这一年是 2015 年。


这家公司是给政府单位做内部系统的,包括中石油、气象局等。我被分配其中一个组做气象相关系统。第二年末的时候,组内的活对我来说已经没什么难度了,就偷偷在外面找工作,H3C 面试前 3 面都通过了,结果最后大领导面气场不符,没通过。最后被另外一家公司的面试官劝退了。然后公司团建的时候,大领导也极力挽留我,最后没走成。


这次经历的经验教训有 2 个,第 1 个是没有拿到 offer 之前,尽量不要被领导知道。第 2 个是,只要领导知道你要离职,就一定要离职。这次就是年终团建的时候,被领导留下来了。但是第二年以各种理由不给工资。


之前自己就一直在想出路,但是小公司,技术成长有限,看书也对工作没有太大作用,没有太大成长。之后了解到研究生改革,有高中同学考了人大非全。自己也就开始准备非全的考试。最后拿到录取通知书,就开始准备离职了。PS:考研准备


在这家公司马上满 3 年重新签合同的时候,偷偷面试了几家,拿到了 2 个还不错的 offer。第二天就跟直属领导提离职了。这次不管直属领导以及大领导如何劝说,还是果断离职了。


这个公司有两个收获。一个是,了解了一般 Java Web 项目的全流程,掌握了基本开发技能,了解了一些大数据开发技术,如Hadoop套件。另外一个是,通过准备考研的过程,也整理出了一套开发过程中应该先思而后行。只有先整理出


5 五年开发经历


第二家公司是一家央企控股上市公司,市场规模中等。主要给政府提供集成项目。到这家公司第二年就开始带小团队做项目,但是工资很低,可能跟公司性质有关。还好公司有宿舍,有食堂。能省下一些钱。


到这家公司的时候,非全刚好开始上课,还好我们 5 点半就下班,所以我天天卡点下班,大领导天天给开发经理说让我加班。但是第一学期要上课,领导对我不爽,也只能这样了。


后来公司来了一个奇葩的产品经理,但是大领导很挺他,大领导下面有 60 号人,研发、产品、测试都有。需求天天改,还不写在文档上。研发都开发完了,后面发现有问题,要改回去,产品还问,谁让这么改的。


是否按照文档开发,也是大领导说的算,最后你按照文档开发也不对,因为他们更新不及时;不按照文档开发也不对,写了你不用。


最后,研发和产品出差,只能同时去一波人,要是同时去用户现场,会打架。最后没干出成绩,产品和大领导一起被干走了。


后面我们整体调整了部门,部门领导是研发出身。干了几个月之后,领导也比较认可我的能力,让我带团队做一个中型项目,下面大概有 10 号人,包括前后端和算法。也被提升为开发经理。


最后因为工资、工作距离(老婆怀孕,离家太远)以及工作内容等原因,跳槽到了下一家互联网公司。


6 入行互联网


凭借着 5 年的工作经历,还算可以的技术广度(毕竟之前啥都干),985 学校的非全研究生学历,以及还过得去的技术能力。找到了一家知名度还可以的互联网公司做商城开发。


这个部门是公司新成立的部门,领导是有好几家一线互联网经验的老程序员,技术过硬,管理能力强,会做人。组内成员都年轻有干劲。本打算在公司大干一场,涨涨技术深度(之前都是传统企业,技术深度不够,但是广度可以)。


结果因为政策调整,整个部门被裁,只剩下直属领导以及领导的领导。这一年是 2020 年。这个时候,我在这个公司还不到 1 年。


7 再前行


拿着上家公司的大礼包,马上开始改简历,投简历,面试,毕竟还有房贷要还(找了个好老婆,她们家出了大头,付了首付),马上还有娃要养,一天也不敢歇息。


经过一个半月的面试,虽然挂的多,通过的少。最终还是拿了 3 个不错的offer,一个滴滴(滴滴面经)、一个XXX网络(最终入职,薪资跟滴滴基本差不多,技术在市场上认可度也还不错。)以及一个建信金科的offer。


因为大厂部门也和上家公司一样,是新组建的部门,心有余悸。然后也还年轻,不想去银行躺平,也怕银行也不靠谱,毕竟现在都是银行科技公司,干几年被裁,更没有出路。最终入职XXX网络。


8 寒冬


入职XXX网络之后,开始接入公司的各种技术组件,以及看到比较成熟的需求提出、评估、开发、测试、发布规范。也看到公司各个业务中心、支撑中心的访问量,感叹还是大公司好,流程规范,流量大且有挑战性。


正要开心的进入节奏,还没转正呢(3 个月转正),组内一个刚转正的同事被裁,瞬间慌得一批。


刚半年呢,听说组内又有 4 个裁员指标,已经开始准备简历了。幸运的是,这次逃过一劫。


现在已经 1 年多了,在这样一个裁员消息满天飞的年代,还有一份不错的工作,很幸运、也很忐忑,也在慢慢寻找自己未来的路,共勉~


9 总结


整体来看,我对自己的现状还算满意,从一个高中每个月 300 块钱生活费家里都拿不出来;高考志愿填报,填学校看心情想去哪,填专业看专业名字的的村里娃,走到现在在北京有个不错的工作,组建了幸福的家庭,买了个不大不小的房子的城里娃。不管怎么样,也算给自己立足打下了基础,留在了这个有更多机会的城市;也给后代一个更高的起点。


但是,我也知道,现在的状态并不稳固,互联网工作随时可能会丢,家庭成员的一场大病可能就会导致整个家庭回到解放前。


所以,主业上,我的规划就是,尽力提升自己的技术能力和管理能力,争取能在中型公司当上管理层,延迟自己的下岗年龄;副业上,提升自己的写作能力,尝试各种不同的主题,尝试给各个自媒体投稿,增加副业收入。


希望自己永远少年,不要下岗~


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

简历写得好,offer不会少!

转眼时间已经到2月下旬了,按照往年各个公司的招聘进度,估计近期各个公司的春招就会开启了。 春招作为校招生们求职的黄金时间,把握好金三银四的招聘季,不仅可以为金九银十的秋招做好铺垫,运气好的话可以直接通过实习转正一步到位免去秋招。 因此,准备在春招中大显身手...
继续阅读 »

转眼时间已经到2月下旬了,按照往年各个公司的招聘进度,估计近期各个公司的春招就会开启了。




春招作为校招生们求职的黄金时间,把握好金三银四的招聘季,不仅可以为金九银十的秋招做好铺垫,运气好的话可以直接通过实习转正一步到位免去秋招。




因此,准备在春招中大显身手的朋友,也该把写简历提上日程了!




为什么要写简历


简历作为求职者的名片,是HR衡量求职者岗位匹配度的重要因素,也是面试前留给面试官的第一印象




一份内容丰富、排版精美的简历不仅可以增大简历筛选的通过率,也能让面试官在面试中更愿意去挖掘出你的闪光点。




毕竟求职者有没有用心、求职意愿是否强烈都是可以从简历中窥探一二的




与此同时,一份排版混乱、内容随意的简历,如果被石沉大海也就情有可原了。




毕竟简历代表的是一种态度!


如何写简历


简历的目的是向HR和面试官清晰展示自己是否与岗位匹配。




一份简历的制作,通常需要从格式内容两个方面来考虑。


格式


一份还算不错的简历格式应该考虑到如下几个因素:


image.png


最近帮一些同学修改简历,我发现很多同学的简历在排版方面存在如下几个方面的问题:




x 简历色彩太多


理解大家想要突出亮点的心情。但是过于花里胡哨可能会分散阅读者的注意力,从而导致适得其反的结果。




x 内容没有边距


很多同学的经历很丰富,想在简历中充分展现自我,因此密密麻麻写了很多内容。但没有突出亮点,也没有合理的设置边距,这可能会给阅读者带来不好的阅读体验。




x 个人照过于随意


为了在视觉上给招聘官良好的印象,很多同学会放自己的一寸照片在简历上。诚然,良好的求职形象是一个加分项,但是在照片的选择上,大家应该以合适出发,生活照和艺术照确实不太适合在简历上出现。




负责招聘的HR一天可能要阅读上百上千份简历,视觉上的体验也是写简历时需要考虑的因素。




对于理工科的同学们来说,简历排版可能是大家不擅长或者不太care的点,其实目前市面上有很多制作简历的网站,大家可以去这些平台选择合适的模板


内容


如果说简历的格式只是起到印象分的作用,那么简历的内容就是简历能否选中的决定性因素了!




记得之前去线下招聘会应聘时,HR在拿到我的简历后,手上的笔一直在简历上圈重点,包括我的教育背景、求职意向、实习经历、项目经历、获奖情况等等...




现在回想起来,这应该就是大家常说的star法则吧!




因此,强烈建议大家按照star法则来填充自己简历的内容:




image.png
image.png




具体到简历中的每个模块,需要包含的内容有以下几个方面:




01 基本信息&求职意向(非常重要)


基本信息是HR联系求职者的主要途径,而求职意向则是岗位匹配度的重要衡量标准。




02 教育背景


成绩好的同学可以放上自己的绩点&排名,学校好的同学可以标注985、211。




03 自我评价&技术栈


自我评价是从性格方面展示求职者的岗位匹配度,技术栈是从能力上体现求职者实力。




对于致力于从事技术岗位的同学,可以在技术栈部分展示出自己的能力。

比如:熟练掌握原生 HTML、CSS、JavaScript 的使用,熟悉 React 框架、了解 Webpack、 TypeScript,熟练使用代码托管工具 Git等等。




如果有持续更新的技术博客或者开源项目,也可以在这里用数字量化加粗体现访问量和star数...




04 实习经历&项目经历(篇幅至少占3/5)


某种程度上说,有和求职岗位相匹配的大厂实习背景的同学在简历筛选中会更容易通过。




如果没有实习经历,有和求职岗位相匹配的项目经历也是star之一。




因为实习经历本质上也是项目经历。




在表述项目经历时,应重点突出自己的工作内容及成果,按照star原则写出技术难点和技术亮点,并量化展示成果,减少口水话的表述。




05 获奖经历和校园经历


获奖经历是软实力的体现, 如果在求职岗位所在的领域获得过有含金量的证书,会给阅读者留下更好的印象。或者在校期间拿到过学业奖学金,也是软实力的体现哦~




总结


一份好的简历,通常会经历多次打磨。




在这期间,我们也可以根据简历来进行阶段性的有效复盘,找出自己的亮点和待提升点,并在下一个版本中进一步提高




与此同时,建议大家将每个版本的简历保存为word和pdf两个版本。word版本便于进一步修改简历,pdf版本用于求职投递, 防止因设备问题导致格式错乱!


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

程序员35岁不是坎,是一把程序员自己设计的自旋锁

有时候,我是悲伤的,这一路走来,从大专第一次接触电脑,学习编程,到现在这四五年的摸爬滚打,好不容易让自己拥有经验,灵活工作了,却要思考35岁这个程序员的诅咒,确切来说是中国程序员的独有的诅咒。 优秀的程序员思维逻辑严谨,弄清楚需求的本质是每天重复的工作,也是...
继续阅读 »

有时候,我是悲伤的,这一路走来,从大专第一次接触电脑,学习编程,到现在这四五年的摸爬滚打,好不容易让自己拥有经验,灵活工作了,却要思考35岁这个程序员的诅咒,确切来说是中国程序员的独有的诅咒。



优秀的程序员思维逻辑严谨,弄清楚需求的本质是每天重复的工作,也是对工作的态度,那弄清楚诅咒的来源,义不容辞。


被诅咒的35岁




  • 35岁后找不到工作了!

  • 35岁后被开了!

  • 程序员的年轻饭,就是35岁!

  • 昨天群里还有同学在聊,如何利用硬件加Linux绕过35岁的梦魇。



其实有人这么说过,也有公司这么做过,他们认为人在35岁以后,注意,说的是人,这一个庞大的群体,而不单指程序员,他们认为在当今互联网快速发展的时代,程序员在35岁后会有以下限制




  1. 技术变化快:技术的更新换代速度非常快,一些老旧的编程语言和技术已经被新技术所取代,这些新技术可能需要程序员重新学习和适应。所以有些人认为年龄大的程序员可能不如年轻的程序员适应新技术。




  2. 年龄歧视:有些雇主可能会偏向招聘年轻的程序员,认为他们更具有创新和学习新技术的能力。




  3. 职业发展:程序员的职业发展路径通常是从程序员到高级程序员,然后是技术主管或项目经理等。他们认为在这个年龄段还没有对应到这个发展行列中的人能力可能是欠缺的。




  4. 完全是年龄歧视的,他们不会在意你能创造的价值,就是认为你不行。




其实分析上述几个点之后,发现,企业拒绝的不是一个35岁的人,而是一个35岁后不满足需求的人,试着分析一下,企业为什么会这样思考?


知己知彼,百战不殆


经过五六年的开发,我可以从我观察的角度分析一下,为什么会将不满足需求的人加上35岁的枷锁。


其实,一个35岁的程序员自己应该要达到35岁程序员应有的高度。


而企业要求的不是一个未满35岁的程序员,而是拒绝一个35岁了,经验还不满足需求的程序员。


从开发中总结,什么样的程序员在给35的程序员不断自旋枷锁


在从零到一的项目中,新手程序员往往会更加注重技术的应用,对今对技术、对需求、对公司存在敬畏之心,他们在开发当中不会随便的乱用一些技术,他们也对项目的规范存在尊敬的态度。一个经验丰富的程序员。假设他不遵守项目的规范,你给他任意一个需求,他都能轻松的完成,但是他从来不会care设计模式,从来不会思考需求的扩展维护以及健壮。那长此以往下去之后这个项目将会面临以下两个问题。
说白了,一个经验丰富的程序员,如果不听从领导的安排自己又对自己代码的要求特别低,因为他们编写的程序一般会按时按点完成需求,测试仪不会存在大量的bug,所以他们认为他们在公司当中是稳定的存在。这就导致企业认为指挥不了的程序员就是这些年龄大的程序员,就是这些程序员,再给自己的35岁自旋加锁。


从0到1的项目,经验丰富而低要求的程序员更容易造成项目的失败


代码人生中介绍了架构和开发的关系,一个项目在经历一段时间的开发之后,往往体现的是研发的规模、投入的成本、增长的效益都会增大,这也是一个公司发展壮大的必经之路。


从开发的角度分析


一个项目第一个版本的投入,可能是三个程序员在两个月的时间完成了第一个版本的发布,并且第一个版本的发布当中基本覆盖了这个应用的90%的功能,从第二个版本开始,每一次需求的研发,时间都将比第一个版本开发的时间更长,而做的东西更少,并且在这个过程当中开发的人数会逐渐增多,简单来讲,研发的人数和需要研发相同需求的时间看下面这一张表。


image.png


image.png


从企业的角度分析


在他们的眼中,当初三个人两个月开发的代码数量,是我们现在大版本当中,10个人两个月开发数量的好几倍,就在他们眼中是我们开发的生产力太低了,但是在扩张招聘当中,从程序员的角度,招聘的一定是经验丰富的程序员,这就让企业认为我们有经验的程序员,也就是说,年龄大的程序员造成的生产力的底下。


image.png


失败的原因真的是大龄程序员吗?


其实我们简单分析一下就可以知道,造成这个像失败的原因,从技术角度单纯来讲,就是因为架构的失败,或者是没有约束的开发模式造成的,因为我们开发一个需求的时间长短,更多的是在维护之前的代码,一个新的需求的插入需要改动太多太多的代码,屎山的代码也就是这么来的。


打破年龄枷锁,其实企业需要的是这样的程序员


所以我们不能浪费多年的开发经验,时刻谨记导致上述问题的原因,不管你是领导者还是程序员,一定要杜绝上述问题的发生,从企业的角度出发,规范自己的编程行为,从现在开始解掉这个枷锁。晋级的思想系列中会总结更多的技巧,总结更多的经验。
主要还是需要将架构的思想深刻到记忆里,让每一行代码都透露着设计的气息,让代码优雅,让内存合理,让扩展更强,让程序更健壮,努力让自己保持以下状态,也要养成一些好的习惯。



  1. 技术能力和知识面:一个经验丰富的程序员应该掌握广泛的编程语言和开发工具,对计算机科学原理、数据结构和算法等基础知识有深刻的理解。此外,一个架构能力强劲的程序员应该能够将技术知识转化为实用的解决方案,设计出高效、可扩展、可维护的系统。

  2. 代码质量:一个经验丰富的程序员应该写出易读易懂、清晰简洁的代码,并遵守编程规范。一个架构能力强劲的程序员应该对代码结构、模块化、可重用性等方面有很高的要求,避免代码臃肿、不易维护的情况。

  3. 项目经验:一个经验丰富的程序员应该具备多个项目的经验,能够处理项目中出现的各种问题,并能够在团队中合作开发。一个架构能力强劲的程序员应该能够根据项目需求制定适合的架构方案,提高系统性能和可扩展性。

  4. 学习能力和思维方式:一个经验丰富的程序员应该能够持续学习新技术和知识,保持对行业的敏锐度。一个架构能力强劲的程序员应该能够独立思考和解决问题,具有系统化思维方式和架构设计的能力。

  5. 持续学习:程序员需要不断学习新的技术和工具,了解行业最新的趋势和发展方向,以便在架构设计和代码编写中使用最新的技术和最佳实践。

  6. 阅读优秀的代码:阅读优秀的代码可以让程序员学习到别人的优秀经验和架构设计,借鉴别人的思路和方法,以此提高自己的写作和设计能力。

  7. 代码重构:程序员需要对自己的代码进行重构,将代码进行整理、简化和优化,使其更加易读易懂、易于扩展和维护。代码重构可以帮助程序员不断改进自己的代码质量和架构设计。

  8. 设计模式和架构模式:程序员需要学习和掌握各种设计模式和架构模式,以此帮助自己设计出更加稳定和可扩展的系统。

  9. Code Review:让别人对自己的代码进行Review是提高自己的写作和架构能力的一种有效方式,因为Review者可以帮助发现代码中的问题并给出改进意见。

  10. 编写文档:程序员需要编写清晰、易懂的文档,以便让其他人了解自己的代码和架构设计,这可以帮助自己更好地理解自己的设计思路,发现潜在的问题并进行改进。


总结


虽然可能会对年长的程序员造成一些挑战,但这并不意味着35岁是程序员的限制年龄或诅咒。年长的程序员通常具有更多的经验和技能,并且会在其他方面表现更优秀,比如领导能力、项目管理和客户沟通等。


因此,年龄并不应该成为评价程序员的唯一标准。


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

从事架构师岗位快2年了,聊一聊我对架构的一些感受和看法

作者简介 架构师李肯(全网同名) 在深圳白手起家,毕业不到4年实现一线城市核心地段的安家梦,从0开始谱写励志人生!一个专注于嵌入式物联网架构设计的攻城狮,2022年度RT-Thread社区杰出布道者,期待您的支持与关注! 标题:从事架构师岗位快2年了,聊一...
继续阅读 »

作者简介


架构师李肯(全网同名)
在深圳白手起家,毕业不到4年实现一线城市核心地段的安家梦,从0开始谱写励志人生!一个专注于嵌入式物联网架构设计的攻城狮,2022年度RT-Thread社区杰出布道者,期待您的支持与关注!





标题:从事架构师岗位快2年了,聊一聊我对架构的一些感受和看法



职位不分高低,但求每天都能有新的进步,永远向着更高的目标前进。





时间是过得真快,就这么一晃就快过了2年了。自2021年5年以来,跌跌爬爬,在架构师的岗位上也快“混”迹2年了,是时候好好静下心来梳理梳理,好好想一想接下来的路该怎么走,如何才能获得更大的提升,毕竟留给快35岁程序猿的时间已经不多了。


下面,我想结合自己的经历,谈一谈自己对架构师岗位的感受和想法,也是希望这样能够更好地提醒和鞭策自己,时刻不要忘了,你的本职工作是一个【架构师】。


踏上新的征程


时钟拨回到2年前,那是2021年5月的那个夏天,因个人原因我向前东家提出了离开,留下曾经一起奋斗过的小伙伴。


在前东家时,我是挂职【资深嵌入式软件工程师】,也曾带领过一个小团队,7-8人,做出过一些成绩,也曾为公司的业绩扛过一些靶子,但终究还是职业发展的考虑,我选择了退出。


正是有萌生提出之意时,在脉脉上有位HR找到我,说是有个【嵌入式架构师】的岗位要找资深研发,对嵌入式开发的要求比较高,有没有兴趣试试。


说实在,之前我也不知道有公司会专门招聘【嵌入式架构师】这样的岗位,但我是知道一般的研发团队中,总是会有人要负责【架构】的工作,而且肯定得是团队中最资深的那一帮人在搞架构的事情。


就这样抱着好奇心,我就参加了那场面试,没想到的是那场面试非常的愉快,跟我的面试官(也就是我现在的老大)聊得非常投机,再后来工作上也的确给予了最大力度的支持和认可,有一种求得知音的感觉。


就这样,一场很顺利的面试结束,复试了2轮,顺利拿到Offer,于2021年5月份入职。


而在2022年末的特殊时期,我也顺利得到老板的认可,成为了公司的小股东。


祥文可见:成为了公司股东,而我却失眠了!


也是从那时候开始,我萌生了【架构师李肯】这个技术IP。


test


架构是什么?


架构,本文中特指【软件架构】。坦白说,架构是一个比较虚的东西,它不像实物那样看得见摸得着,而是一种抽象的概念在里面。


恰巧,最近ChatGPT不是非常火吗?要不,我们来问一下它,看看它对架构的理解是怎么样的?



提问:架构是什么? 回答:架构是一种分层模型,用于描述软件系统的结构和行为。它定义了软件系统的模块、接口、职责和关系,从而使软件系统可以更好地满足用户的需求。



下面谈一下我对架构的理解:



架构是一种抽象的软件设计,它要着力于用技术的手段解决业务上遇到的可伸缩、可扩展、性能优、易用性等难题。


一般来说,一个优秀的软件架构,必须要满足横向可扩展,纵向可伸缩,系统性能还要能够看得住一定的压力测试,同时,对架构的使用者而言,应该有非常清晰易用的API接口,在整个设计中需要充分考虑架构的易用性。


就拿我现在做的嵌入式系统架构来说,毫无疑问,这里需要的架构就是必须支持各种对接业务的功能模块可以可快速扩展和和裁减,同时对硬件底层的抽象设计足够抽象,以便于架构快速地移植在各个芯片平台上。而且,各个芯片平台的性能特性需要发挥到最佳状态,满足各种商用的需求。另外,对应用开发团队也需要提供非常明确的API接口和文档,以便于基于该软件架构快速输出软件,达到商业落地的目的。



架构师到底是干什么的?


这个问题我相信外行人都比较迷,于是我先用这个问题问了一下ChatGPT,得到的答案是:



提问:架构师到底是干什么的? 回答:架构师是一种计算机技术专家,负责设计和构建软件系统或网络架构,以满足业务需求。架构师需要深入理解业务和技术,以确定系统的需求,并设计合适的架构,以满足这些需求。架构师还负责评估和选择合适的技术,实施和监控系统,以及维护系统的性能和安全性。



嗯,好像有点道理。那么,我顺着这层思路再补充谈一下我的看法。


回到了上面【什么是架构】这个问题后,我再结合我的实际工作内容说说我的这个架构师到底是干嘛的。我分为几个点来阐述:




  1. 架构师首要解决的,就是复杂多变的业务需求中,分析并提取中核心需求,并加以抽象设计,以便于使用软件技术解决这样的需求难题;这就要求架构师一定是对核心业务非常熟悉的,否则可能你设计的架构就偏离了实际业务需求,这无异于纸上谈兵,哪怕架构设计得再优秀,都无法落地。

  2. 对提取的核心需求进行拆解:提取出对应的可扩展性、可伸缩性、易用性、性能要求等各个方面的需求,形成需求矩阵,针对各个需求,分别设计核心架构,满足业务需求。

  3. 架构师可以说是业务需求跟应用开发的一个衔接者,对上是对业务需求团队负责,对下是对应用开发团队负责,这里面的汇总、分析、设计、协调都是必备的技能;他所要解决的是两个团队面临的迫切问题。

  4. 对内,还需要负责核心技术难题的攻关:遇到团队中的疑难技术问题,需要攻关介入的时候,毫无疑问,架构师团队要冲在最前面,为后面的业务团队和应用开发团队铺平前进的道路。

  5. 对外,还需要负责业内核心前沿技术的调研和学习:这里面是一个进阶学习的过程,每个架构师团队必须要保持一定的学习能力,观测业内的技术架构方向,不一定非得追最新最热的技术方向,但至少你要能懂,这项新技术能帮我们解决什么样的问题。也许,将来我们就能用上它。



你的终极目标又是什么?


去年年末的时候,我写过一篇文章,叫 【架构师李肯】带你走进架构师的一天


里面有这么一章节:



正好俗语所说:“不想当将军的士兵,不是一个好的士兵!”


换到我身上,我认为是:“不想当CTO的架构师,不是一个好的架构师!”


我的更远大目标就是,有朝一日,挂帅CTO,至于迎娶白富美的后话,就还是别了,早已心有所属。


回想自己一路升级打怪,从技术小白,踏入助理工程师的岗位,慢慢提升自己,开始走向中级工程师,逐渐可以独立接项目了,锻炼了一些日子,开始担任高级工程师,开始考虑更全面了,需要承担技术攻坚的重任;再后来,跨越到资深工程师,也开始带团队了,不再单兵作战,需要为团队负责到底;而现在,架构师一职,承担的不仅仅是本部门的技术工作,还需要宏观考虑技术的延展性和前瞻性,某些情况下甚至关乎着整条业务线的生存,不可谓影响不大。


而如果走向CTO的岗位,那直接就关系整个公司的技术脉络,这可直接影响公司的存亡,压力和挑战并存。


我有时候也问我自己,准备好了吗?我希望我自己能给出肯定的答案:“ Yes,I’m ready !"


img



回到现实吧,朝着你的目标前进就好了,希望就在眼前。


如果你想成为架构师该如何提升自己?


这个问题可能也是大家比较关心的,同样的,我们也问一下ChatGPT吧。



提问:如果你想成为架构师该如何提升自己? 回答:



  1. 学习计算机基础:要想成为架构师,首先要掌握计算机的基础知识,包括计算机硬件和操作系统等。

  2. 熟悉编程语言:掌握多种编程语言,比如C,C++,Java,Python,Go等。

  3. 学习网络技术:熟悉TCP/IP,HTTP,FTP等网络技术,了解网络安全技术。

  4. 熟悉数据库:学习并掌握多种数据库,包括MySQL,Oracle,MongoDB,Redis等。

  5. 了解框架:掌握各种框架,比如Spring,Hibernate,Struts,MyBatis等。

  6. 学习容器技术:学习容器技术,比如Docker,Kubernetes等,掌握微服务架构。

  7. 了解分布式:学习分布式计算,掌握分布式存储,消息队列,大数据,云计算等技术。

  8. 实践经验:多积累实践经验,学习不同的架构设计思想,不断完善自己的技术水平。



好吧,答案跟我想的完全不太一样。毕竟我目前专注的嵌入式系统架构设计这一块。


但是我认为架构的核心理念是一致的,你需要扎实的基础技术知识来铺垫,最重要的是培养自己敏锐的【架构】思维。


比如用到一个很好用的框架,一个很帮的开源项目,多去想想别人是如何从【架构】去考虑问题的,同时又是如何设计这个【架构】已满足这些需求问题的,多动脑子去思考和整理,慢慢积累起来就会有比较敏锐的【架构】观。


又比如我们学习和使用的Linux系统,它里面可是浓缩了世界各地最优秀的那帮码农的精华,包含了大量的【架构】设计理念,而我们作为它的使用者和开发者,更是应该深入学习它,可以是某一模块的设计,或者某一类模型的设计,等你能把这些架构摸透了,差不多你也就成了架构师。


常言道:【书中自有黄金屋】,我个人觉得看对应领域的专家级书籍,也是一种非常棒的学习方式。站在巨人的肩膀上,可以帮助你爬得更快,升得更高。


经常在后台收到小伙伴的私信,问我有没有在架构方面比较优秀的书籍推荐。


这不,最近刚出了一本书籍,叫《持续架构实践》,它的一推出,立马轰动业界。


作为架构领域的从业者,我第一时间拿到了书本,匆匆看了几章,有种酣畅淋漓的感觉,甚至有种相见恨晚的意味。



软件架构领域正在爆发一场新的革命。Gartner权威发布2023年十大科技趋势之一 “可持续IT架构” ,可持续架构得到越来越多从业人员认同。创建和维护可持续的软件架构对于架构师和工程师而言也是一项巨大的挑战。



感兴趣的朋友,可以多关注一下这本书,尤其是希望从事架构师岗位的小伙伴,也许它能帮你解开很多心中的疑团。


image-20230214123213729


更多关于《持续架构设计》书籍的介绍,请参考社区帖子介绍,详见 bbs.csdn.net/topics/6134…


附图


这里有朋友好奇ChatGPT的玩法,又没有合适的工具来体验,所以来问到我,我用我那8毛钱的Python技术写了一个小工具,只需要输入API-KEY就可以了,不需要代理,也不需要fanqiang,可以试用试用。


image.png
这个小工具,有需要的可以私我,友情共享。


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

改行后我在做什么?(2022-9-19日晚)

闲言碎语 今天回了趟家里,陪父母一起吃了个饭。父母照例是在唠叨,这个年纪了还不结婚,也没个稳定的工作,巴拉巴拉的一大堆。吃完饭我匆匆的就回到了我租住的地方。在现阶段,其实我对于父母所诉说的很多东西,我都是认同的。 但在我这个年纪,这个阶段,看似有很多选择...
继续阅读 »

闲言碎语



今天回了趟家里,陪父母一起吃了个饭。父母照例是在唠叨,这个年纪了还不结婚,也没个稳定的工作,巴拉巴拉的一大堆。吃完饭我匆匆的就回到了我租住的地方。在现阶段,其实我对于父母所诉说的很多东西,我都是认同的。




但在我这个年纪,这个阶段,看似有很多选择,但其实我没有选择。能做的也只是多挣点钱。




在这个信息爆炸的时代,我们知道更高的地方在哪里。但当你想要再往上走一步的时候,你发现你的上限,其实从出生或从你毕业的那一刻就已经注定了。可能少部分人通过自身的努力,的确能突破壁垒达到理想的高度。但这只是小概率事件罢了。在我看来整个社会的发展,其实早就已经陷入了一种怪圈。




在我,早些年刚刚进入社会的时候。那时的想法特别简单。就想着努力工作,努力提升自身的专业素养。被老板赏识,升职加薪成为一名管理者。如果,被淘汰了那应该是自己不够优秀,不够努力,专业技能不过硬,自己为人处事不够圆滑啥的。




内卷这个词语引爆网络的时候;当35岁被裁员成为常态的时候。再回头看我以前的那些想法那真的是一个笑话。(我觉得我可能是在为自己被淘汰找借口)



当前的状态



游戏工作室的项目,目前基本处于停滞的状态。我不敢加机器也不敢关机。有时候我都在想,是不是全中国那3-4亿的人都在搞这个?一个国外的游戏,金价直接拉成这个逼样。




汽配这边的话,只能说喝口稀饭。(我花了太多精力在游戏工作室上了)



梦想破灭咯



其实按照正常情况来说,游戏工作室最开始的阶段,我应该是能够稍微挣点钱的。我感觉我天时、地利、人和。我都占的。现在来看的话,其实我只占了人和。我自己可以编码,脚本还是从驱动层模拟键鼠,写的一套脚本。这样我都没赚钱,我擦勒。



接下来干嘛



接下来准备进厂打螺丝。(开玩笑的)
还是老老实实跟着我弟学着做生意吧。老老实实做汽配吧!在这个时代,好像有一技之长(尤其是IT)的人,好像并不能活得很好。除非,你这一技之长,特别特别长。(当下的中国不需要太多的这类专业技术人员吧。)



我感受到的大环境



我身边有蛮多的大牛。从他们的口中和我自己看到的。我感觉在IT这个领域,国内的环境太恶劣了。在前端,除开UI库,我用到的很多多的库全是老外的。为什么没有国人开源呢?因为,国人都忙着996了。我们可以在什么都不知道的情况下,通过复制粘贴,全局搜索解决大部分问题。 机械视觉、大数据分析、人工智能 等很多东西。这一切的基石很多年前就有了,为什么没人去研究他?为什么我们这波人,不断的在学习:这样、那样的框架。搭积木虽然很好玩。但创造一个积木,不应该也是一件更有挑战性的事情么?




在招聘网站还有一个特别奇怪的现象。看起来这家公司是在招人,但其实是培训机构。 看起来这家公司正儿八经是在招聘兼职,但其实只想骗你去办什么兼职卡。看起来是在招送快递,送外卖的,招聘司机的,但其实只是想套路你买车。我擦勒。这是怎样的一个恶劣的生存环境。这些个B人就不能干点,正经事?




卖菜的、拉车的、搞电商的、搞短视频、搞贷款的、卖保险的、这些个公司市值几百亿。很难看到一些靠创新,靠创造,靠产品质量,发展起来的公司。


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

DIO源码浅析——揭开面纱后是你印象中的样子吗

dio
DIO源码解析 dio是一个强大的Dart Http请求库,支持Restful API、FormData、拦截器、请求取消、Cookie管理、文件上传/下载、超时、自定义适配器等 Dio版本号:4.0.6 基本使用 final dio = Dio(); fin...
继续阅读 »

DIO源码解析


dio是一个强大的Dart Http请求库,支持Restful API、FormData、拦截器、请求取消、Cookie管理、文件上传/下载、超时、自定义适配器等


Dio版本号:4.0.6


基本使用


final dio = Dio();
final result = await dio.get('https://xxxx.ccc');

源码分析


源码分析通常情况下是一个逆推的过程,首先熟悉api的使用,然后通过api的调用思考功能是如何实现的。这里就从Dio()和get()方法作为切入点,看看Dio的内部实现。切忌直接下载源码通读一遍,容易找不到重点。


Dio


查看源码发现Dio是个抽象类,定义了Dio支持的所有功能。有面向对象经验的应该都知道抽象类无法直接实例化,但是这里却可行其实这是dart的factory语法糖,方便开发者使用工厂模式创建对象。


简化的Dio代码,例举出比较具有代表性的属性和方法。


abstract class Dio {
factory Dio([BaseOptions? options]) => createDio(options);
late BaseOptions options;

Interceptors get interceptors;

late HttpClientAdapter httpClientAdapter;

late Transformer transformer;
...
Future<Response<T>> get<T>(
String path, {
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
ProgressCallback? onReceiveProgress,
});

Future<Response<T>> request<T>(
String path, {
data,
Map<String, dynamic>? queryParameters,
CancelToken? cancelToken,
Options? options,
ProgressCallback? onSendProgress,
ProgressCallback? onReceiveProgress,
});
...
}

1. 工厂方法创建Dio对象


factory Dio([BaseOptions? options]) => createDio(options);
这是上面提到的为何抽象类能实例化对象,就是个语法糖起了作用,跟进去发现createDio(options)这个方法定义在entry_stub.dart里,并且是个空实现。先不深究,反正最后的实现要么是DioForBrowser、要么是DioForNative至于加了什么魔法不是本期的重点

2. BaseOptions


options保存通用的请求信息,比如baseUrl、headers、超时时间等参数。用来设置全局配置。

3. Interceptors


这就是所有http请求框架里都会用到的拦截器在Dio里的实现,里面的关键源码一个线性列表存储所有的拦截器,和重写的下标操作符。在发起请求时会使用Interceptors存储的拦截器按顺序进行拦截处理。

4. HttpClientAdapter


HttpClientAdapter是Dio真正发起请求的地方,他是一个抽象类,实现类通过依赖注入注入进来。Dio这里运用了职责分离的思想进行接耦,Dio定义请求方法和请求拦截等操作,使用HttpClientAdapter建立连接发起请求。这样设计的好处在于,如若对网络请求库有改动的需求可以自己实现一个HttpClientAdapter子类进行替换就行,无需改动原有代码。

5. Transformer


Transformer的作用是在请求前后可以对请求参数,和请求结果进行修改。在请求时生效在请求拦截器之后,响应时发生在响应拦截器之前。对于了解过洋葱模型的同学来说,这很好理解,Transformer处于Interceptors的里面一层。

6. 诸如get、post、request...方法


Dio里定义的方法全部都是抽象方法,需要子类来实现。这里的作用是定义一个通用的请求接口,包含http常用的一些方法。

按照程序看完抽象类就该看实现类了,Android Studio里在抽象类Dio的左边有个向下的箭头,点击一下发现有三个子类。


1. DioMixin


DioMixin也是一个抽象类,实现了Dio接口几乎所有的方法,只有两个属性未实现:




  • HttpClientAdapter




  • BaseOptions


    这两个属性交由DioForNative和DioForBrowser各自进行注入。




class DioForBrowser with DioMixin implements Dio {
DioForBrowser([BaseOptions? options]) {
this.options = options ?? BaseOptions();
httpClientAdapter = BrowserHttpClientAdapter();
}
}

class DioForNative with DioMixin implements Dio {
singleton.
DioForNative([BaseOptions? baseOptions]) {
options = baseOptions ?? BaseOptions();
httpClientAdapter = DefaultHttpClientAdapter();
}
}

这个很好理解,因为native和web的发起请求肯定是不一样的。dio默认使用的http_client来自于dart_sdk暂未直接支持web。所以需要通过创建不同的http_client适配web和native。


好了,到这里基本确定DioMixin这个类就是Dio最重要的实现类了。DioForNative和DioForBrowser只是针对不同平台的适配而已。继续分析DioMixin:


同样从get方法开始跟进


  Future<Response<T>> get<T>(
String path, {
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
ProgressCallback? onReceiveProgress,
}) {
return request<T>(
path,
queryParameters: queryParameters,
options: checkOptions('GET', options),
onReceiveProgress: onReceiveProgress,
cancelToken: cancelToken,
);
}

get方法里设置了一下method为‘GET’,然后把参数全数传递给了request方法,继续看看request方法


  Future<Response<T>> request<T>(
String path, {
data,
Map<String, dynamic>? queryParameters,
CancelToken? cancelToken,
Options? options,
ProgressCallback? onSendProgress,
ProgressCallback? onReceiveProgress,
}) async {
options ??= Options();
var requestOptions = options.compose(
this.options,
path,
data: data,
queryParameters: queryParameters,
onReceiveProgress: onReceiveProgress,
onSendProgress: onSendProgress,
cancelToken: cancelToken,
);
requestOptions.onReceiveProgress = onReceiveProgress;
requestOptions.onSendProgress = onSendProgress;
requestOptions.cancelToken = cancelToken;
...
return fetch<T>(requestOptions);
}

request方法里主要干了两件事



  1. 合并BaseOptions和外部传进来的请求参数

  2. 绑定上传、下载、取消等回调到请求对象


然后将处理好的请求参数交给fetch方法。继续跟进(前方高能,fetch方法是dio的核心了)


  Future<Response<T>> fetch<T>(RequestOptions requestOptions) async {
final stackTrace = StackTrace.current;

if (requestOptions.cancelToken != null) {
requestOptions.cancelToken!.requestOptions = requestOptions;
}

//这里是根据请求参数,简单判断下返回的type。意思是T如果声明了类型,要么是普通文本要么是json对象
if (T != dynamic &&
!(requestOptions.responseType == ResponseType.bytes ||
requestOptions.responseType == ResponseType.stream)) {
if (T == String) {
requestOptions.responseType = ResponseType.plain;
} else {
requestOptions.responseType = ResponseType.json;
}
}

//请求拦截包装器:interceptor就是拦截器里的onRequest方法,作为参数传过来
//1.开始分析这个包装器的作用,仅当状态处于next时开始工作
//2.listenCancelForAsyncTask方法作用是,cancelToken的Future和请求的拦截器Future同时执行,cancelToken先执行完成的话就抛出异常终止请求。
//3.创建一个requestHandler,并调用interceptor方法(在request这里就是onRequest方法),然后返回requestHander.future(了解Completer的同学应该都知道,这是可以手动控制future的方法)。这就解释了为何拦截器里的onRequest方法,开发者需要手动调用next等方法进入下一个拦截器。

FutureOr Function(dynamic) _requestInterceptorWrapper(
InterceptorSendCallback interceptor,
) {
return (dynamic _state) async {
var state = _state as InterceptorState;
if (state.type == InterceptorResultType.next) {
return listenCancelForAsyncTask(
requestOptions.cancelToken,
Future(() {
return checkIfNeedEnqueue(interceptors.requestLock, () {
var requestHandler = RequestInterceptorHandler();
interceptor(state.data as RequestOptions, requestHandler);
return requestHandler.future;
});
}),
);
} else {
return state;
}
};
}

//响应拦截包装器:
//实现方式参考_requestInterceptorWrapper基本类似,但是要注意这里放宽了state的条件多了一个resolveCallFollowing,这个后续再讲
FutureOr<dynamic> Function(dynamic) _responseInterceptorWrapper(
InterceptorSuccessCallback interceptor,
) {
return (_state) async {
var state = _state as InterceptorState;
if (state.type == InterceptorResultType.next ||
state.type == InterceptorResultType.resolveCallFollowing) {
return listenCancelForAsyncTask(
requestOptions.cancelToken,
Future(() {
return checkIfNeedEnqueue(interceptors.responseLock, () {
var responseHandler = ResponseInterceptorHandler();
interceptor(state.data as Response, responseHandler);
return responseHandler.future;
});
}),
);
} else {
return state;
}
};
}

// 错误拦截包装器
FutureOr<dynamic> Function(dynamic, StackTrace) _errorInterceptorWrapper(
InterceptorErrorCallback interceptor) {
return (err, stackTrace) {
if (err is! InterceptorState) {
err = InterceptorState(
assureDioError(
err,
requestOptions,
),
);
}

if (err.type == InterceptorResultType.next ||
err.type == InterceptorResultType.rejectCallFollowing) {
return listenCancelForAsyncTask(
requestOptions.cancelToken,
Future(() {
return checkIfNeedEnqueue(interceptors.errorLock, () {
var errorHandler = ErrorInterceptorHandler();
interceptor(err.data as DioError, errorHandler);
return errorHandler.future;
});
}),
);
} else {
throw err;
}
};
}

// Build a request flow in which the processors(interceptors)
// execute in FIFO order.

// Start the request flow
// 初始化请求拦截器第一个元素,第一个InterceptorState的type为next
var future = Future<dynamic>(() => InterceptorState(requestOptions));

// Add request interceptors to request flow
// 这是形成请求拦截链的关键,遍历拦截器的onRequest方法,并且使用_requestInterceptorWrapper对onRequest方法进行包装。
//上面讲到_requestInterceptorWrapper返回的是一个future
//future = future.then(_requestInterceptorWrapper(fun));这段代码就是让拦截器形成一个链表,只有上一个拦截器里的onRequest内部调用了next()才会进入下一个拦截器。
interceptors.forEach((Interceptor interceptor) {
var fun = interceptor is QueuedInterceptor
? interceptor._handleRequest
: interceptor.onRequest;
future = future.then(_requestInterceptorWrapper(fun));
});

// Add dispatching callback to request flow
// 发起请求的地方,发起请求时也处在future链表里,方便response拦截器和error拦截器的处理后续。
//1. reqOpt即,经过拦截器处理后的最终请求参数
//2. _dispatchRequest执行请求,并根据请求结果判断执行resolve还是reject
future = future.then(_requestInterceptorWrapper((
RequestOptions reqOpt,
RequestInterceptorHandler handler,
) {
requestOptions = reqOpt;
_dispatchRequest(reqOpt)
.then((value) => handler.resolve(value, true))
.catchError((e) {
handler.reject(e as DioError, true);
});
}));

//request处理执行完成后,进入response拦截处理器,遍历形成response拦截链表
// Add response interceptors to request flow
interceptors.forEach((Interceptor interceptor) {
var fun = interceptor is QueuedInterceptor
? interceptor._handleResponse
: interceptor.onResponse;
future = future.then(_responseInterceptorWrapper(fun));
});

// 请求拦截链表添加完成后,添加错误的链表
// Add error handlers to request flow
interceptors.forEach((Interceptor interceptor) {
var fun = interceptor is QueuedInterceptor
? interceptor._handleError
: interceptor.onError;
future = future.catchError(_errorInterceptorWrapper(fun));
});

// Normalize errors, we convert error to the DioError
// 最终返回经过了拦截器链表的结果
return future.then<Response<T>>((data) {
return assureResponse<T>(
data is InterceptorState ? data.data : data,
requestOptions,
);
}).catchError((err, _) {
var isState = err is InterceptorState;

if (isState) {
if ((err as InterceptorState).type == InterceptorResultType.resolve) {
return assureResponse<T>(err.data, requestOptions);
}
}

throw assureDioError(
isState ? err.data : err,
requestOptions,
stackTrace,
);
});
}

关于fetch的源码分析在关键点写了注释,各位同学自行享用。像这种比较长的源码分析一直在思考该如何写,分块解析怕代码逻辑关联不上来,索性直接全部拿来写上注释。再通过实例问题解释代码,各位如果有好的建议去分析代码,请在评论区留言


关于拦截器的几个实例问题




  1. 拦截器不手动调用RequestInterceptorHandler.next会怎么样?
    答:根据我们梳理的流程来看,Dio在发起请求时会根据拦截器生成一个future的链表,future只有等到上一个执行完才会执行下一个。如果拦截器里不手动调用next则会停留在链表中的某个节点。




  2. 拦截器onError中可以做哪些操作?




interceptors.forEach((Interceptor interceptor) {
var fun = interceptor is QueuedInterceptor
? interceptor._handleError
: interceptor.onError;
future = future.catchError(_errorInterceptorWrapper(fun));
});

对这段代码进行分析,可以看到onError的执行,是在future链表的catchError(捕获future里的错误)方法中进行的。


onError的方法签名如下
void onError(DioError err,ErrorInterceptorHandler handler)
可以在onError方法调用 next、resolve、reject三个方法处理。

next 使用的completeError,会让future产生一个错误,被catch到交给下一个拦截器处理。


void next(DioError err) {
_completer.completeError(
InterceptorState<DioError>(err),
err.stackTrace,
);
_processNextInQueue?.call();
}

resolve 使用complete会正常返回数据,不会触发catchError,所以跳过后续的onError拦截器


void resolve(Response response) {
_completer.complete(InterceptorState<Response>(
response,
InterceptorResultType.resolve,
));
_processNextInQueue?.call();
}

reject 和next代码类似,但是设置了状态为InterceptorResultType.reject,结合_errorInterceptorWrapper代码看,包装器里只处理err.type == InterceptorResultType.next ||
err.type == InterceptorResultType.rejectCallFollowing条件,其他状态直接抛出异常。所以reject的效果就是抛出错误直接完成请求


  void reject(DioError error) {
_completer.completeError(
InterceptorState<DioError>(
error,
InterceptorResultType.reject,
),
error.stackTrace,
);
_processNextInQueue?.call();
}
//error包装器
FutureOr<dynamic> Function(dynamic, StackTrace) _errorInterceptorWrapper(
InterceptorErrorCallback interceptor) {
return (err, stackTrace) {
if (err is! InterceptorState) {
err = InterceptorState(
assureDioError(
err,
requestOptions,
),
);
}
//仅会处理InterceptorResultType.next和InterceptorResultType.rejectCallFollowing,而reject的类型是reject,所以直接执行elese的throw
if (err.type == InterceptorResultType.next ||
err.type == InterceptorResultType.rejectCallFollowing) {
return listenCancelForAsyncTask(
requestOptions.cancelToken,
Future(() {
return checkIfNeedEnqueue(interceptors.errorLock, () {
var errorHandler = ErrorInterceptorHandler();
interceptor(err.data as DioError, errorHandler);
return errorHandler.future;
});
}),
);
} else {
throw err;
}
};
}


  1. 在onRequest里抛出异常,后续的onRequest和onResponse还会回调吗?

  2. 在onResponse里抛出异常,后续的onResponse还会回调吗?


答:回顾一下请求的流程,发起请求->onRequest(1)->onRequest(2)->onRequest(3)->http请求->onResponse(1)->onResponse(2)->onResponse(3)->catchError(1)->catchError(2)->catchError(3)。

这就很明显无论是onRequest还是onResponse抛出异常都会被catchError(1)给捕获,跳过了后续的onRequest和onResponse。

补充



  • 在requestWrapper、responseWrapper、errorWrapper里都可以看到listenCancelForAsyncTask,第一个参数是cancelToken。这是因为Dio的取消请求是在拦截器里进行的,只要请求还未走完拦截器就可以取消请求。这就有个新的问题,如果咱们未设置拦截器取消请求就无法使用了吗?显然不是。在发起请求的时候还会把cancelToken再次传递进去,监听是否需要取消请求,如果取消的话就关闭连接,感兴趣的同学自行查看相关源码。


  Future<Response<T>> _dispatchRequest<T>(RequestOptions reqOpt) async {
var cancelToken = reqOpt.cancelToken;
ResponseBody responseBody;
try {
var stream = await _transformData(reqOpt);
responseBody = await httpClientAdapter.fetch(
reqOpt,
stream,
cancelToken?.whenCancel,
);
...
}


  • InterceptorResultType在拦截器里发挥的作用,上面也提过了其实就是在调用InterceptorHandler的next,resolve,reject时设着一个标记,用于判断是继续下一个拦截器还是跳过后续拦截方法


2. DioForNative 和 DioForBrowser


下面这段代码是DioMixin发起请求的地方,可以看到真正执行http请求的是HttpClientAdapter的fetch方法。那DioForNative和DioForBrowser是针对不同平台的实现,那最简单的方法就是对fetch方法进行定制就好了。上面也提到了他们各自创建了不同的HttpClientAdapter,感兴趣的同学可以看看BrowserHttpClientAdapter和DefaultHttpClientAdapter


 Future<Response<T>> _dispatchRequest<T>(RequestOptions reqOpt) async {
var cancelToken = reqOpt.cancelToken;
ResponseBody responseBody;
try {
var stream = await _transformData(reqOpt);
responseBody = await httpClientAdapter.fetch(
reqOpt,
stream,
cancelToken?.whenCancel,
);
...
}

总结


Dio也是一个网络封装库,本身并不负责建立http请求等操作。除此之外还集成了请求拦截,取消请求的功能。采用了面向接口的方式,所以替换http请求库代价很小,只需要自己实现HttpClientAdapter替换下即可。


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

Flutter 仿闲鱼动画效果

前言 目前正在做的项目,为了增加用户的体验度,准备增加一些动画效果,其中底部栏中间按钮的点击事件参考了闲鱼的动效,便在此基础上仿写了该动效,并增加了一些新的效果。 动效 闲鱼动效 仿写效果 思路 根据UI的设计图,对每个模块设计好动画效果,本人主要设计了以...
继续阅读 »

前言


目前正在做的项目,为了增加用户的体验度,准备增加一些动画效果,其中底部栏中间按钮的点击事件参考了闲鱼的动效,便在此基础上仿写了该动效,并增加了一些新的效果。


动效


闲鱼动效


闲鱼gif.gif


仿写效果


动效gif.gif


思路


根据UI的设计图,对每个模块设计好动画效果,本人主要设计了以下四个效果。


1、底部返回键旋转动画


底部返回按钮动画其实就是个旋转动画,利用Transform.rotate设置angle的值即可,这里使用了GetX来对angle进行动态控制。


//返回键旋转角度,初始旋转45度,使其初始样式为 +
var angle = (pi / 4).obs;

///关闭按钮旋转动画控制器
late final AnimationController closeController;
late final Animation<double> closeAnimation;

///返回键旋转动画
closeController = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: provider,
);

///返回键旋转动画
closeController = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: provider,
);

///页面渲染完才开始执行,不然第一次打开不会启动动画
WidgetsBinding.instance.addPostFrameCallback((duration) {
closeAnimation =
Tween(begin: pi / 4, end: pi / 2).animate(closeController)
..addListener(() {
angle.value = closeAnimation.value;
});
closeController.forward();
});


///关闭按钮点击事件
void close() {
///反转动画,并关闭页面
Future.delayed(
const Duration(milliseconds: 120), () {
Get.back();
});

closeController.reverse();
}


IconButton(
onPressed: null,
alignment: Alignment.center,
icon: Transform.rotate(
angle: controller.angle.value,
child: SvgPicture.asset(
"assets/user/ic-train-car-close.svg",
width: 18,
height: 18,
color: Colors.black,
),
))

2、底部四个栏目变速上移动画+渐变动画


四个栏目其实就是个平移动画,只不过闲鱼是四个栏目一起平移,而我选择了变速平移,这样视觉效果上会好一点。


//透明度变化
List<AnimationController> opacityControllerList = [];
//上移动画,由于每个栏目的移动速度不一样,需要用List保存四个AnimationController,
//如果想像闲鱼那种整体上移,则只用一个AnimationController即可。
List<AnimationController> offsetControllerList = [];
List<Animation<Offset>> offsetAnimationList = [];

//之所以用addIf,是因为项目中这几个栏目的显示是动态显示的,这里就直接写成true
Column(
children: []
..addIf(
true,
buildItem('assets/user/ic-train-nomal-car.webp',"学车加练","自主预约,快速拿证"))
..addIf(
true,
buildItem('assets/user/ic-train-fuuxn-car.webp',"有证复训","优质陪练,轻松驾车"))
..addIf(
true,
buildItem('assets/user/ic-train-jiaxun-car.webp',"模拟加训","考前加训,临考不惧"))
..addIf(
true,
buildItem('assets/user/ic-train-jiakao-car.webp',"驾考报名","快捷报名无门槛"))
..add(playWidget())
..addAll([
17.space,
]),
)

//仅仅是为了在offsetController全部初始化完后执行play()
Widget playWidget() {
//执行动画
play();
return Container();
}

int i = 0;

Widget buildItem(String img,String tab,String slogan) {
//由于底部栏目是动态显示的,需要在创建Widget时一同创建offsetController和offsetAnimation
i++;
AnimationController offsetController = AnimationController(
duration: Duration(milliseconds: 100 + i * 20),
vsync: this,
);
Animation<Offset> offsetAnimation = Tween<Offset>(
begin: const Offset(0, 2.5),
end: const Offset(0, 0),
).animate(CurvedAnimation(
parent: offsetController,
// curve: Curves.easeInOutSine,
curve: const Cubic(0.12, 0.28, 0.48, 1),
));

AnimationController opacityController = AnimationController(
duration: const Duration(milliseconds: 500),
lowerBound: 0.2,
upperBound: 1.0,
vsync: this);

opacityControllerList.add(opacityController);
offsetControllerList.add(offsetController);
offsetAnimationList.add(offsetAnimation);

return SlideTransition(
position: offsetAnimation,
child: FadeTransition(
opacity: opacityController,
child: Container(
margin: EdgeInsets.only(bottom: 16),
height: 62,
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(12)),
color: const Color(0xfffafafa)),
child:
Row(mainAxisAlignment: MainAxisAlignment.center, children: [
24.space,
Image.asset(img, width: 44, height: 44),
12.space,
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(tab,
style: const TextStyle(
color: Color(0XFF000000),
fontSize: 16,
fontWeight: FontWeight.bold)),
Text(slogan,
style: const TextStyle(
color: Color(0XFF6e6e6e), fontSize: 12)),
]).expanded,
Image.asset("assets/user/ic-train-arrow.webp",
width: 44, height: 44),
17.space
])).inkWell(
onTap: () {},
delayMilliseconds: 50)),
);
}

//执行动画
void play() async {
for (int i = 0; i < offsetControllerList.length; i++) {
opacityControllerList[i].forward();

///栏目正序依次延迟(40 + 2 * i) * i的时间,曲线速率
Future.delayed(Duration(milliseconds: (40 + 2 * i) * i), () {
offsetControllerList[i]
.forward()
.whenComplete(() => offsetControllerList[i].stop());
});
}
}



///关闭按钮点击事件
void close() {
///反转动画,并关闭页面
Future.delayed(
const Duration(milliseconds: 120), () {
Get.back();
});

for (int i = offsetControllerList.length - 1; i >= 0; i--) {
///栏目倒叙依次延迟(40 + 2 * (offsetControllerList.length-1-i)) * (offsetControllerList.length-1-i))的时间
Future.delayed(
Duration(
milliseconds:
(40 + 2 * (offsetControllerList.length-1-i)) * (offsetControllerList.length-1-i)), () {
offsetControllerList[i].reverse();
});
}
opacityTopController.reverse();
}

3、中间图片渐变动画


渐变动画使用FadeTransition即可。


///图片透明度渐变动画控制器
late final AnimationController imgController;

///图片透明度渐变动画
imgController = AnimationController(
duration: const Duration(milliseconds: 500),
lowerBound: 0.0,
upperBound: 1.0,
vsync: provider);
imgController.forward().whenComplete(() => imgController.stop());

///渐变过渡
FadeTransition(
opacity: imgController,
child:
Image.asset("assets/user/ic-traincar-guide.webp"),
),

///关闭按钮点击事件
void close() {
imgController.reverse();
}

4、顶部文案渐变动画+下移动画


///顶部标题下移动画控制器
late final AnimationController offsetTopController;
late final Animation<Offset> offsetTopAnimation;

///顶部标题渐变动画控制器
late final AnimationController opacityTopController;


///顶部标题上移动画
offsetTopController = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: provider,
);
offsetTopController
.forward()
.whenComplete(() => offsetTopController.stop());
offsetTopAnimation = Tween<Offset>(
begin: const Offset(0, -0.8),
end: const Offset(0, 0),
).animate(CurvedAnimation(
parent: offsetTopController,
curve: Curves.easeInOutCubic,
));
offsetTopController
.forward()
.whenComplete(() => offsetTopController.stop());

//UI
SlideTransition(
position: offsetTopAnimation,
child: FadeTransition(
opacity: opacityTopController,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
80.space,
const Text(
'练车指南',
style: TextStyle(
color: Color(0XFF141414),
fontSize: 32,
fontWeight: FontWeight.w800,
),
),
2.space,
const Text('易练只为您提供优质教练,为您的安全保驾护航',
style: TextStyle(
color: Color(0XFF141414),
fontSize: 15)),
],
))),


///关闭按钮点击事件
void close() {
offsetTopController.reverse();
opacityTopController.reverse();

}

5、注销动画


最后,在关闭页面的时候不要忘记注销动画。


///关闭时注销动画
void dispose() {
for (int i = offsetControllerList.length - 1; i > 0; i--) {
offsetControllerList[i].dispose();
}
offsetTopController.dispose();
opacityTopController.dispose();
imgController.dispose();
closeController.dispose();
}

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

Android源码—为什么onResume方法中不可以获取View宽高

前言 有一个经典的问题,我们在Activity的onCreate中可以获取View的宽高吗?onResume中呢? 对于这类八股问题,只要看过都能很容易得出答案:不能。 紧跟着追问一个,那为什么View.post为什么可以获取View宽高? 今天来看看这些问题...
继续阅读 »

前言


有一个经典的问题,我们在Activity的onCreate中可以获取View的宽高吗?onResume中呢?


对于这类八股问题,只要看过都能很容易得出答案:不能


紧跟着追问一个,那为什么View.post为什么可以获取View宽高?


今天来看看这些问题,到底为何?


今日份问题:



  1. 为什么onCreate和onResume中获取不到view的宽高?

  2. 为什么View.post为什么可以获取View宽高?



基于Android API 29版本。



问题1、为什么onCreate和onResume中获取不到view的宽高?


首先我们清楚,要拿到View的宽高,那么View的绘制流程(measure—layout—draw)至少要完成measure,【记住这一点】。


还要弄清楚Activity的生命周期,关于Activity的启动流程,后面单独写一篇,本文会带一部分。


另外布局都是通过setContentView(int)方法设置的,所以弄清楚setContentView的流程也很重要,后面也补一篇。


首先要知道Activity的生命周期都在ActivityThread中, 当我们调用startActivity时,最终会走到ActivityThread中的performLaunchActivity


    private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
……
       Activity activity = null;
       try {
           java.lang.ClassLoader cl = appContext.getClassLoader();
         // 【关键点1】通过反射加载一个Activity
           activity = mInstrumentation.newActivity(
                   cl, component.getClassName(), r.intent);
          ……
      } catch (Exception e) {
           ……
      }

       try {
           ……

           if (activity != null) {
               ……
               // 【关键点2】调用attach方法,内部会初始化Window相关信息
               activity.attach(appContext, this, getInstrumentation(), r.token,
                       r.ident, app, r.intent, r.activityInfo, title, r.parent,
                       r.embeddedID, r.lastNonConfigurationInstances, config,
                       r.referrer, r.voiceInteractor, window, r.configCallback,
                       r.assistToken);

               ……
                 
               if (r.isPersistable()) {
                 // 【关键点3】调用Activity的onCreate方法
                   mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
              } else {
                   mInstrumentation.callActivityOnCreate(activity, r.state);
              }
               ……
          }
           ……
       return activity;
  }

performLaunchActivity中主要是创建了Activity对象,并且调用了onCreate方法。


onCreate流程中的setContentView只是解析了xml,初始化了DecorView,创建了各个控件的对象;即将xml中的 转化为一个TextView对象。并没有启动View的绘制流程


上面走完了onCreate,接下来看onResume生命周期,同样是在ActivityThread中的performResumeActivity


    @Override
   public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
           String reason) {
       ……
       // 【关键点1】performResumeActivity 中会调用activity的onResume方法
       final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
……
         
       final Activity a = r.activity;

       ……
         
       if (r.window == null && !a.mFinished && willBeVisible) {
           r.window = r.activity.getWindow();
           View decor = r.window.getDecorView();
           decor.setVisibility(View.INVISIBLE); // 设置不可见
           ViewManager wm = a.getWindowManager();
           WindowManager.LayoutParams l = r.window.getAttributes();
           a.mDecor = decor;
           ……
             
           if (a.mVisibleFromClient) {
               if (!a.mWindowAdded) {
                   a.mWindowAdded = true;
                 // 【关键点2】在这里,开始做View的add操作
                   wm.addView(decor, l);
              } else {
                   ……
                   a.onWindowAttributesChanged(l);
              }
          }

           
      } else if (!willBeVisible) {
          ……
      }
      ……
  }

handleResumeActivity中两个关键点



  1. 调用performResumeActivity, 该方法中r.activity.performResume(r.startsNotResumed, reason);会调用Activity的onResume方法。

  2. 执行完Activity的onResume后调用了wm.addView(decor, l);,到这里,开始将此前创建的DecorView添加到视图中,也就是在这之后才开始布局的绘制流程



到这里,我们应该就能理解,为何onCreate和onResume中无法获取View的宽高了,一句话就是:View的绘制要晚于onResume。



问题2、为什么View.post为什么可以获取View宽高?


那接下来我们开始看第二个问题,先看看View.post的实现。


    public boolean post(Runnable action) {
       final AttachInfo attachInfo = mAttachInfo;
    // 添加到AttachInfo的Handler消息队列中
       if (attachInfo != null) {
           return attachInfo.mHandler.post(action);
      }

       // 加入到这个View的消息队列中
       getRunQueue().post(action);
       return true;
  }

post方法中,首先判断attachInfo成员变量是否为空,如果不为空,则直接加入到对应的Handler消息队列中。否则走getRunQueue().post(action);


从Attach字面意思来理解,其实就可以知道,当View执行attach时,才会拿到mAttachInfo, 因此我们在onResume或者onCreate中调用view.post(),其实走的是getRunQueue().post(action)


接下来我们看一下mAttachInfo在什么时机才会赋值。


View.java


void dispatchAttachedToWindow(AttachInfo info, int visibility) {
   mAttachInfo = info;
}

dispatch相信大家都不会陌生,分发;那么一定是从根布局上开始分发的,我们可以全局搜索,可以看到



不要问为什么一定是这个,因为我看过,哈哈哈


其实ViewRootImpl就是一个布局管理器,这里面有很多内容,可以多看看。


ViewRootImpl中直接定位到performTraversals方法中;这个方法一定要了解,而且特别长,下面我抽取几个关键点。


    private void performTraversals() {
  ……
  // 【关键点1】分发mAttachInfo
  host.dispatchAttachedToWindow(mAttachInfo, 0);
  ……
       
     //【关键点2】开始测量
     performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
     ……
     //【关键点3】开始布局
     performLayout(lp, mWidth, mHeight);
     ……
     // 【关键点4】开始绘制
     performDraw();
     ……
  }

再强调一遍,这个方法很长,内部很多信息,但其实总结来看,就是View的绘制流程,上面的【关键点2、3、4】。也就是这个方法执行完成之后,我们就能拿到View的宽高了;到这里,我们终于看到和View的宽高相关的东西了。


但还没结束,我们post出去的任务,什么时候执行呢,上面host可以看成是根布局,一个ViewGroup,通过一层一层的分发,最后我们看看View的dispatchAttachedToWindow方法。


 void dispatchAttachedToWindow(AttachInfo info, int visibility) {
    mAttachInfo = info;
    ……
    // Transfer all pending runnables.
    if (mRunQueue != null) {
        mRunQueue.executeActions(info.mHandler);
        mRunQueue = null;
    }
}

这里可以看到调用了mRunQueue.executeActions(info.mHandler);


public void executeActions(Handler handler) {
   synchronized (this) {
       final HandlerAction[] actions = mActions;
       for (int i = 0, count = mCount; i < count; i++) {
           final HandlerAction handlerAction = actions[i];
           handler.postDelayed(handlerAction.action, handlerAction.delay);
      }

       mActions = null;
       mCount = 0;
  }
}

这就很简单了,就是将post中的Runnable,转移到mAttachInfo中的Handler, 等待接下来的调用执行。


这里要结合Handler的消息机制,我们post到Handler中的消息,并不是立刻执行,不要认为我们是先dispatchAttachedToWindow的,后执行的测量和绘制,就没办法拿到宽高。实则不是,我们只是将Runnable放到了handler的消息队列,然后继续执行后面的内容,也就是绘制流程,结束后,下一个主线程任务才会去取Handler中的消息,并执行。


结论



  1. onCreate和onResume中无法获取View的宽高,是因为还没执行View的绘制流程。

  2. view.post之所以能够拿到宽高,是因为在绘制之前,会将获取宽高的任务放到Handler的消息队列,等到View的绘制结束之后,便会执行。



水平有限,若有不当,请指出!!!


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

IM等成熟框架是否可以支持到UE接入应用?

IM 的聊天框架是否可以,作为组件,集成到UE工程中?这个方向,我们可以做出哪些努力?

IM 的聊天框架是否可以,作为组件,集成到UE工程中?这个方向,我们可以做出哪些努力?

勇敢的小伙是回家妥协还是趁年轻再拼一把(我的七年)

你好我是小李,今年还不到23周岁,是一个工作不到4年的前端程序员。你可能好奇为什么一个23岁的人能有3年多的经验,这也是面试时很多HR问我的问题。其实答案很简单,我17岁的时候就开始上班了。讲讲我的故事:我从前是一个不爱学习的坏学生,迷恋游 戏导致从老师口中的...
继续阅读 »

你好

我是小李,今年还不到23周岁,是一个工作不到4年的前端程序员。

你可能好奇为什么一个23岁的人能有3年多的经验,这也是面试时很多HR问我的问题。

其实答案很简单,我17岁的时候就开始上班了。

讲讲我的故事:

我从前是一个不爱学习的坏学生,迷恋游 戏导致从老师口中的优等生变成一个没考上高中的学渣。知道中考结果的时候对我震惊很大,但也在意料之中。该怎么办,去电子厂打工,还是去上职校?

最终选择还是去了济南某3+2大专,因为非常喜欢玩游戏,所以选择了计算机专业。但我选的班级又跟普通3+2大专有所不同,因为他是跟北大某鸟联合创办的就业班。在学校学了很多东西,Java,C#,前端,数据库,设计都有,毕竟外面的培 训机构一般是学三个月,我们学了两年。但当时也是年纪尚小,也没学会多少东西就面临着第一次实习。(我们3+2是上两年学实习一年再上两年)

进入社会开始工作:

第一次步入社会找工作的时光也挺怀念,同学们三五结伴出去,穿着西装打好领带出去面试。没错穿西装,学校强制要求的,一群17 18岁的小孩穿西装去面试。那时候有很多培训机构给以招人的名义让你去面试,然后忽悠要培训,我们回来就把不靠谱的公司写黑板上,当时写了一黑板的公司名称。

后来侥幸让一家算上老板行政共三个人的公司录取,写VB.NET(现在后端技术已经忘干净了)。但干了一周就跑了,一个人当八个人干,天天加班。

然后就开始面试,顺利的被某内的人事忽悠学了设计,学习三个月,两万块钱。因为老师一直催要实习证明,自己比较着急,某内当时以UI设计师的岗位邀我去面试,后来说技术不行,但给你一个内部培训的机会,那个人事给我说了很多,弄得我当时脑子一片空白,糊里糊涂就签了培训合同,后来想明白已经为时已晚。

其实我当时在某内学的还算不错,因为在前两年的课程中学过一些PS的东西,当时想要不以后就转行做设计师吧,就放弃了后两年大专的课程,去考了个成考大专。找工作的时候还算顺利,被一家医疗器械公司录取,做起了平面设计。

在这家医疗器械公司认识了很多朋友,并且现在还有几个一直保持联系,体会到了挣钱的不容易。一次公司要做一个简单的企业宣传网站,知道我是学软件开发的,就安排给了我。当时设计工作很闲,这个网站不需要后端,前端死页面贴几张图那种。这个网站做了三个周左右吧,当时技术菜,并且前端课程中占比很少,就边做边查资料,搞定自适应,调浏览器的兼容性。在做的过程中,突然有了不甘的心理,学了两年的软件就此转行真的不甘心,每天上下班近三个小时的车程就在看前端教学视频(当时看的是渡一教育,那个老师讲的很好)。

随后工作了一段时间就提了离职,离职原因:我要做软件开发,我要当程序员。

离职后找了几家工作,有一家正好招前端和设计的,看我两个都会,把我招了去面试。很巧面试官了解我的学历情况他说他也是北大某鸟的,也是我这种3+2,不过是济南另一个校区的。技术没怎么问,就问我会不会,会不会的我都回答了会,然后济南项目部的老大就问我能不能接受出差,去上海总部,我说能,第一天面试入职第二天早上六点就坐上了去上海的火车(那时我还没18岁)。


坐了12个多小时的火车腰酸背痛,虽然是硬卧但也不舒服。出了车站第一次去大城市,看着外面的灯红酒绿,感叹着十里洋场烟花地的美丽。后来上海的同事说要在站内坐地铁去,就去售票机买了地铁票,坐了人生中的第一次地铁。

出了地铁按着导航到了五角场,晚上的五角场真好看。跟着导航进入了五角场的一个商场,不争气的手机进去就没信号了,说实话当时真的慌,偌大个商场找不到出口在哪,东西南北也分不清,拎着个行李箱满地跑。后来上海的同事来接的我,才安全到公司。

到了公司没有想象的这么好,公司主要做硬件的,各种设备各种工具堆得满办公室都是,每个人就一个小隔间作为办公桌。公司领导看我没吃饭给我订了盒饭,大概到了11点跟着加班的同事回了宿舍。宿舍的环境也是相当的差,马桶是坏的,要自己用舀水冲,幸好住的地方离五角场近,没事可以去那里玩玩。

就这样过了两周吧,老板突然说要拉我去现场,他开车带着我们走了好久,打开地图看居然都出了上海,到了江苏。目的地是江苏的一个汽车组装厂,公司做的物联网系统要放在一辆概念车的展车上,为近期的虹桥车展做准备。组装厂是封闭的,24小时开着灯,不看手机不知道外面是白天还是黑天。每天都有饭送到跟前,睡醒了吃,吃完了干活,累了就睡觉那种。

终于熬到了车展,第一次逛了虹桥会展中心,真的很大,看到很多很帅的车。车展后待了没几天就提了离职,这种睡醒上班,累了就睡的工作太折磨人了。其实一直是想去外滩看看的,但到离职也没去成。

离职后就回了家,在家玩了一段时间又回到了济南,找了一个外包公司的工作。这家外包公司主要用的技术是uniapp框架,员工近百人,入职要求很低,只要是一周内能用uniapp框架做项目就行。我之前看过vue的视频,学起来uniapp也很容易,成功入职。

这里的门槛低,接的项目多,做成的却没几个,平均一个月一个App项目,隔三岔五就有找上门的甲方。最终以公司发不下来工资结束了这份工作。

这时候到了2019年12月份也是武汉疫情爆发的时候,因为是农村,可以出门但不能出村,整天和发小一起打王者,放了个小长假。

年后三月份,看着疫情没有那么严重了,就签了离乡保证书(就是出了当地无论目的地有没有住处能不能找到工作你都不能回来),回了济南。

因为会了uniapp这项技能入职了一家做养老项目的公司。开始后端就两个,前端没有,是一个实施在弄前端,他也会,但会的不多。公司不大,到最后也才不到十个人,前端基本上是我自己在弄,或者我写一个简单的页面让几个实施去改成类似的页面。就这样过了一年,感觉项目没有前景,并且公司一年涨薪50元人民币感觉太少了就离职再次去了上海。

为什么选择上海,对比北京这里住房比较方便,北京住四环五环工作地点却在二环每天要挤地铁太麻烦,对比广东深圳那两个又太远了。


到了上海,第一目的地是北蔡。在家的时候联系了一个租房中介,想在浦东找一份工作。跟着中介到了小区,算是个老破小吧,房子很旧,小区门口的垃圾堆了很多。到了屋子里床垫里面的弹簧都出来了,窗子的玻璃坏了一半,另一半是塑料布糊上去了,风一刮哗哗的响。那也没办法,1900多的房租押一付一还要给中介一千多的中介费,也可能被坑了,但就这样吧,一个外地人到一个城市现找工作只能这样。出门去超市买了床被子还不是棉的,自己带的夏凉被当褥子垫身子底下。三月份的上海到了晚上比北方好不了多少,床垫的弹簧硌的腰疼,冷风通过墙上空调管道开的洞传到屋子里,被子上盖着羽绒服也不顶用,第二天睡醒浑身疼。后来的几天都是穿着一份睡,但也好不了太多。

紧接着就是找工作,不得不说大城市工作机会就是多,但外包外派也多,前两周只考虑浦东和黄浦的工作,面了好多,要么是技术不行被拒要么工资给的低,有几个拿学历低压低工资,但我都没去。到了第三周有点着急,因为就准备找一个月的工作,找不到就回山东,所以把面试范围扩大到了整个上海,只要是不太偏远都行。第二天下午去杨浦面了一家互联网公司,面的时候很不顺利,面技术的时候面了两个多小时。那天就早晨吃了一块面包,面到一半又饿又渴,头晕眼花的。当时我的vue技术也不算是很好,之前主要做uniapp项目了,面完认为自己会凉,可能公司缺人吧,说来试岗一周,可以的话就留下。当时自己感觉这里的办公环境还不错,旁边就是复旦大学,就同意了。

试岗开始是熟悉项目,后来上手做一些东西,前端老大就是面试我的小哥。不懂的他也教我,开始跟他关系不是很好,可能他教我的时候太凶了,我没事就不理他。试岗还算顺利,不是什么很难的东西,但签正式合同的时候我就傻眼了,为什么是一份外派合同,就是说是外派到这个公司的,我问他周围的同事,他们也是这样,我之前是一直抵制进外派的,面试的时候也没跟我说,没办法,在这里待了一周了,每天工作很轻松,外派就外派吧。

每天工作进行着,就是浦东到杨浦通勤时间太久了,就不要租房押金到杨浦复旦大学的对面小区租了一个2360一个月,小区离公司园区就隔着复旦大学。

在公司的工作比较清闲每天准时下班,很少有加班,有很长一段时间没有工作,每天就干坐着翻掘金看技术文档看一天,这段时间对我技术提升很大,小哥有时也会教我一些新东西,慢慢接触发现他人也挺好。当时感觉自己学历不够,报了个网络教育的专升本。周六周天就去外面玩,晚上去黄埔江边吹一吹江风。

突然有一天中午吃饭的时候发现公司园区被封了一栋楼,说是有密接,从这开始上海疫情来了。公司一天天人越来越少,新闻中确诊人数日渐增加。后来上海政府要“鸳鸯锅”式封城法,先封浦东,再封浦西,以黄浦江为界,各封一周。我当时就猜一周是不行的,就准备了两周的泡面零食应对疫情管控。只能说我还是太天真了,疫情封了两个多月,这两个多月我都在吃泡面,一天吃两桶,吃多了不够吃的,买泡面只能通过美团跑腿买,两箱桶面要了我230多块钱。每天核酸检测,抗原检测,新闻上确诊人数疯狂的增加。泡面吃腻了怎么办,就把社区发的油菜叶跟泡面一起泡,但量很少,几次就吃完了。

过了差不多两个月,疫情得到缓解,风险等级也渐渐下降,居家办公的也开始去公司上班了。那时候接近项目交付,开始了加班,有时候加到一两点,但我工作不忙,很多都是后端服务器的问题,我在那加班也只是应对突发问题。后来在上海的那段时间非常想家,我猜应该是因为疫情的太折磨人感觉家无比的温暖,没多久就离职回家。


在家休息了一个多月又回到了济南,找到了一份薪资还算可以的工作,在上海攒了点钱,考了摩托车驾-照,买了辆摩托车。奈何公司产品经理太垃圾,也到了年底,准备离职年后重新找。(谁也不想到最后做出一堆垃圾项目)

时间回到了现在,从老家过完年回济南两周了,年前裸辞的多潇洒年后找工作多狼狈

boos上投了好多,但都是送达。就算是已读不回也行,但大部分都是HR根本没有看。后来在开发群里问了都是这样,很多公司都是在裁人,很少有招的。找了两周工作回我的大部分是外派,弄得我很焦虑,我在想是不是一线城市好找些,要不卖了摩托车再去上海深圳闯一闯又或者回老家找个稳定的工作开始与柴米油盐为伴。

不知不觉写了这么多了,我还以为第一次写不了几个字呢。写了一半的时候跟我妈打了个电话,平时我很少与家人联系,都是她们主动跟我打电话,我妈让我不急慢慢找,打完电话后焦虑的心情放松了下来。

作者:世间有灵
来源:juejin.cn/post/7199235508665008188

收起阅读 »

数据可视化大屏设计器开发-多选拖拽

web
开头本文是数据可视化开始的开发细节第五章。关于画布中的元素的各种鼠标拖拽操作。简单声明 本人只是一个菜鸡,以下方法仅个人思路,如有错误,轻喷🙏🏻 。开头说明 下面所说的元素表示的是组或者组件的简称。开始大屏设计当中,不乏需要调整图表组件的位置和尺寸。 相...
继续阅读 »

开头

本文是数据可视化开始的开发细节第五章。关于画布中的元素的各种鼠标拖拽操作。

简单声明
本人只是一个菜鸡,以下方法仅个人思路,如有错误,轻喷🙏🏻 。

开头说明
下面所说的元素表示的是组或者组件的简称。

开始

大屏设计当中,不乏需要调整图表组件的位置尺寸
相较于网页低代码,图表大屏低代码可能需要更复杂的操作,比如嵌套成组多选单元素拖拽缩放多元素拖拽缩放
并且需要针对鼠标的动作做相应的区分,当中包含了相当的细节,这里就一一做相应的讲解。

涉及的依赖

  • react-rnd
    react-rnd是一个包含了拖拽和缩放两个功能的react组件,并且有非常丰富的配置项。
    内部是依赖了拖拽(react-draggable)和缩放(re-resizable)两个模块。
    奈何它并没有内置多元素的响应操作,本文就是针对它来实现对应的操作。

  • react-selecto
    react-selecto是一个简单的简单易用的多选元素组件。

  • eventemitter3
    eventemitter3是一个自定义事件模块,能够在任何地方触发和响应自定义的事件,非常的方便。

相关操作

多选

画布当中可以通过鼠标点击拖拽形成选区,选区内的元素即是被选中的状态。

这里即可以使用react-selecto来实现此功能。


从图上操作可以看到,在选区内的元素即被选中(会出现黑色边框)。

  import ReactSelecto from 'react-selecto';

 const Selecto = () => {

   return (
     <ReactSelecto
       // 会被选中元素的父容器 只有这个容器里的元素才会被选中  
       dragContainer={'#container'}
       // 被选择的元素的query
       selectableTargets={['.react-select-to']}
       // 表示元素有被选中的百分比为多少时才能被选中
       hitRate={10}
       // 当已经存在选中项时,按住指定按键可进行继续选择  
       toggleContinueSelect={'shift'}
       // 可以通过点击选择元素
       selectByClick
       // 是否从内部开始选择(?)
       selectFromInside
       // 拖拽的速率(不知道是不是这个意思)
       ratio={0}
       // 选择结束
       onSelectEnd={handleSelectEnd}
     ></ReactSelecto>
  );
};
复制代码

这里有几个需要注意的地方。

  • 操作互斥
    画布当中的多选和拖拽都是通过鼠标左键来完成的,所以当一个元素是被选中的时候,鼠标想从元素上开始拖拽选择组件是不被允许的,此时应该是拖拽元素,而不是多选元素。

而元素如果没有被选中时,上面的操作则变成了多选。


  • 内部选中
    画布当中有的概念,它是一个组与组件无限嵌套的结构,并且可以单独选中组内的元素。
    当选中的是组内的元素时,即说明最外层的组是被选中的状态,同样需要考虑上面所说的互斥问题。

单元素拖拽缩放

单元素操作相对简单,只需要简单使用react-rnd提供的功能即可完成。


多元素拖拽缩放

这里就是本文的重点了,结合前面介绍的几个依赖,实现一个简单的多选拖拽缩放的功能。

具体思路

多个元素拖拽,说到底其实鼠标拖拽的还是一个元素,就是鼠标拖动的那一个元素。
其余被选中的元素,仅仅需要根据被拖动的元素的尺寸位置变动来做相应的加减处理即可。

相关问题
  • 信息计算
    联动元素的位置尺寸信息该如何计算。

  • 组件间通信
    因为每一个图表组件并非是单纯的同级关系,如果是通过层层props传递,免不了会有多余的刷新,造成性能问题。
    而通过全局的dva状态同样在更新的时候会让组件刷新。

  • 数据刷新
    图表数据是来自于dva全局的数据,现在频繁自刷新相当于是一直更新全局的数据,同样会造成性能问题。

  • 其他
    一些细节问题

解决方法

  • 信息计算
    关于位置的计算相对简单,只需要单纯的将操作的元素的位置和尺寸差值传递给联动组件即可。

  • 组件间通信
    根据上面问题的解析,可以使用eventemitter3来完成任意位置、层级的数据通信,并且它和react渲染无任何关系。

import { useCallback, useEffect } from 'react'
import EventEmitter from 'eventemitter3'

const eventemitter = new EventEmitter()

const SonA = () => {

 console.log('刷新')

 useEffect(() => {
   const listener = (value) => {
     console.log(value)
  }
   eventemitter.addListener('change', listener)
   return () => {
     eventemitter.removeListener('change', listener)
  }
}, [])

 return (
   <span>son A</span>
)

}

const SonB = () => {

 const handleClick = useCallback(() => {
   eventemitter.emit('change', 'son B')
}, [])

 return (
   <span>
     <button onClick={handleClick}>son B</button>
   </span>
)

}

const Parent = () => {

 return (
   <div>
     <SonA />
     <br />
     <SonB />
   </div>
)

}

运行上面的例子可以发现,点击SonB组件的按钮,可以让SonA接收到来自其的数据,并且并没有触发SonA的刷新。
需要接收数据的组件只需要监听(addListener)指定的事件即可,比如上面的change事件。
而需要发送数据的组件则直接发布(emit)事件即可。
这样就避免了一些不必要的刷新。

  • 数据刷新
    频繁刷新全局数据,会导致所有依赖其数据的组件都会刷新,所以考虑为需要刷新数据的组件在内部单独维护一份状态。
    开始操作时,记录下状态,标识开始使用内部状态表示图表的信息,结束操作时处理下内部数据状态,将数据更新到全局中去。

  import { useMemo, useEffect, useState, useRef } from 'react'
 import EventEmitter from 'eventemitter3'

 const eventemitter = new EventEmitter()

 const Component = (props: {
   position: {left: number, top: number}
}) => {

   const [ position, setPosition ] = useState({
     left: 0,
     top: 0
  })

   const isDrag = useRef(false)

   const dragStart = () => {
     isDrag.current = true
     setPosition(props.position)
  }

   const drag = (position) => {
     setPosition(position)
  }

   const dragEnd = () => {
     isDrag.current = false
     // TODO
     // 更新数据到全局
  }

   useEffect(() => {
     eventemitter.addListener('dragStart', dragStart)
     eventemitter.addListener('drag', drag)
     eventemitter.addListener('dragEnd', dragEnd)
     return () => {
       eventemitter.removeListener('dragStart', dragStart)
       eventemitter.removeListener('drag', drag)
       eventemitter.removeListener('dragEnd', dragEnd)
    }
  }, [])

   return (
     <span
       style={{
         left: (isDrag.current ? position : props.position).left,
         top: (isDrag.current ? position : props.position).top
      }}
     >图表组件</span>
  )

}

上面的数据更新还可以更加优化,对于短时间多次更新操作,可以控制一下更新频率,将多次更新合并为一次。

  • 其他

    • 控制刷新
      这里的控制刷新指的是上述的内部刷新,不需要每次都响应react-rnd发出的相关事件,可以做对应的节流(throttle)操作,减少事件触发频率。

    • 通信冲突问题
      因为所有的组件都需要监听拖拽的事件,包括当前被拖拽的组件,所以在传递信息时,需要把自身的id类似值传递,防止冲突。

    • 组件的缩放属性
      这里是关于前文说到的成组的逻辑相关,因为组存在scaleXscaleY两个属性,所以在调整大小的时候,也要兼顾此属性(本文先暂时不考虑这个问题)。

    • 单元素选中情况
      自定义事件的监听是无差别的,当只选中了一个元素进行拖拽缩放操作时,无须触发相应的事件。

最后的DEMO


成品

其实在之前就已经发现其实react-selecto的作者也有研发其他的可视化操作模块,包括本文所说的多选拖拽的操作,但是奈何无法满足本项目的需求,故自己实现了功能。
如果有兴趣可以去看一下这个成品moveable

总结

通过上面的思路,即可完成一个简单的多元素拖拽缩放的功能,其核心其实就是eventemitter3的自定义事件功能,它的用途在平常的业务中非常广泛。
比如我们完全可以在以上例子的基础上,加上元素拖拽时吸附的功能。

结束

结束🔚。

顺便在下面附上相关的链接。

试用地址
试用账号
静态版试用地址
操作文档
代码地址

作者:写代码请写注释
来源:juejin.cn/post/7202445722972815417

收起阅读 »

手把手教你实现一个自定义 eslint 规则

web
ESlint 概述ESLint 是一个代码检查工具,通过静态的分析,寻找有问题的模式或者代码。默认使用 Espree 解析器将代码解析为 AST 抽象语法树,然后再对代码进行检查。抽象语法树(Abstract Syntax Tree,AST),或简称语法树(S...
继续阅读 »

ESlint 概述

ESLint 是一个代码检查工具,通过静态的分析,寻找有问题的模式或者代码。默认使用 Espree 解析器将代码解析为 AST 抽象语法树,然后再对代码进行检查。

抽象语法树Abstract Syntax Tree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。

随着前端工程化体系的不断发展,Eslint 已经前端工程化不可缺失的开发工具。它解决了前端工程化中团队代码风格不统一的问题,避免了一些由于代码规范而产生的 Bug, 同时它提高了了团队的整体效率。

运行机制

Eslint的内部运行机制不算特别复杂,主要分为以下几个部分:

  • preprocess,把非 js 文本处理成 js

  • 确定 parser(默认是 espree

  • 调用 parser,把源码 parseSourceCodeAST

  • 调用 rules,对 SourceCode 进行检查,返回 linting problems

  • 扫描出注释中的 directives,对 problems 进行过滤

  • postprocess,对 problems 做一次处理

  • 基于字符串替换实现自动 fix

具体描述,这里就不补充了。详细的运行机制推荐大家去学习一下Eslint的底层实现原理和源码。

常用规则

为了让使用者对规则有个更好的理解, Eslint 官方将常用的规则进行了分类并且定义了一个推荐的规则组 "extends": "eslint:recommended"。具体规则详情请见官网

示例规则如下:

  • array-element-newline<string|object>
    "always"(默认) - 需要数组元素之间的换行符
    "never" - 不允许数组元素之间换行
    "consistent" - 数组元素之间保持一致的换行符

配置详解

Eslint 配置我们主要通过.eslintrc配置来描述

extends

extends 的内容为

一个 ESLint配置文件,一旦扩展了(即从外部引入了其他配置包),就能继承另一个配置文件的所有属性(包括rules, plugins, and language option在内),然后通过 merge合并/覆盖所有原本的配置。最终得到的配置是前后继承和覆盖前后配置的并集。

extends属性的值可以是:

  • 定义一个配置的字符串(配置文件的路径、可共享配置的名称,如eslint:recommendedeslint:all)

  • 定义规则组的字符串。plugin:插件名/规则名称 (插件名取eslint-plugin-之后的名称)

 "extends": [
   "eslint:recommended",
   "plugin:react/recommended"
],

parserOptions

指定你想要支持的 JavaScript 语言选项。默认支持 ECMAScript 5 语法。你可以覆盖该设置,以启用对 ECMAScript 其它版本和 JSX 的支持。

"parserOptions": {
 "ecmaVersion": 6,
 "sourceType": "module",
 "ecmaFeatures": {
    "jsx": true
}
}

rules

ESLint 拥有大量的规则。你可以通过配置插件添加更多规则。使用注释或配置文件修改你项目中要使用的规则。要改变一个规则,你必须将规则 ID 设置为下列值之一:

  • "off"0 - 关闭规则

  • "warn"1 - 开启规则,使用警告级别的错误:warn (不会导致程序退出)

  • "error"2 - 开启规则,使用错误级别的错误:error (当被触发的时候,程序会退出)

"plugins": [
 "plugin-demo",
],
"rules": {
 "quotes": ["error", "double"], // 修改eslint recommended中quotes规则
 "plugin-demo/rule1": "error", // 配置eslint-plugin-plugin-demo 下rule1规则
}

对于 Eslint recommended 规则组中你不想使用的规则,也可以在这里进行关闭。

plugin

ESLint 支持使用第三方插件。要使用插件,必须先用 npm进行安装

"plugins": [
  "plugin-demo", // // 配置 eslint-plugin-plugin-demo 插件
],

这里做一下补充,extendsplugin 的区别在于,extendsplugin 的子集。就好比如 Eslint 中除了 recommended 规则组还有其他规则

自定义Eslint插件

团队开发中,我们经常会使用一些 eslint 规则插件来约束代码开发,但偶尔也会有一些个性定制化的团队规范,而这些规范就需要通过一些自定义的 ESlint 插件来实现。

我们先看一段简短的代码:

import { omit } from 'lodash';

上述代码是我们在使用lodash的一个习惯性写法,但是这段代码会导致全量引入lodash,造成工程包体积偏大。

正确的引用方式如下:

import omit from 'lodash/omit';

// 或
import { omit } from 'lodash-es';

我们希望可以通过插件去约束开发者的使用习惯。但是 Eslint 自带的规则对于这个定制化的场景就无法满足了。此时, 就需要去使用 Eslint 提供的开放能力去定制化一个 Eslint 规则。接下来我将从创建到使用去实现一个lodash引用规范的Eslint自定义插件

创建

工程搭建

Eslint 官方提供了脚手架来简化新规则的开发, 如不使用脚手架搭建,只需保证和脚手架一样的结构就可以啦。

创建工程前,先全局安装两个依赖包:

$ npm i -g yo
$ npm i -g generator-eslint

再执行如下命令生成 Eslint 插件工程。

$ yo eslint:plugin

这是一个交互式命令,需要你填写一些基本信息,如下

$ yo eslint:rule
? What is your name? // guming-eslint-plugin-custom-lodash
? What is the plugin ID? // 插件名 (eslint-plugin-xxx)
? Type a short description of this plugin: // 描述你的插件是干啥的
? Does this plugin contain custom ESLint rules? Yes // 是否为自定义Eslint 校验规则
? Does this plugin contain one or more processors? No // 是否需要处理器


接下来我们为插件创建一条规则,执行如下命令:

$ npx yo eslint:rule

这也是一个交互式命令,如下:

? What is your name? // guming-eslint-plugin-custom-lodash
? Where will this rule be published? ESLint Plugin
? What is the rule ID? // 规则名称 lodash-auto-import
? Type a short description of this rule: // 规则的描述
? Type a short example of the code that will fail: // 这里写你这条规则校验不通过的案例代码


填写完上述信息后, 我们可以得到如下的一个项目目录结构:

guming-eslint
├─ .eslintrc.js
├─ .git
├─ README.md
├─ docs
│ └─ rules
│ └─ lodash-auto-import.md
├─ lib // 规则
│ ├─ index.js
│ └─ rules
│ └─ lodash-auto-import.js
├─ node_modules
├─ package-lock.json
├─ package.json
└─ tests // 单测
└─ lib
└─ rules
└─ lodash-auto-import.js

eslint 规则配置

Eslint 官方制定了一套开发自定义规则的规范。我们只需要根据规范配置相应的内容就可以轻松的实现我们的自定义Eslint规则。具体配置详情可见官网

相关配置的说明如下:

module.exports = {
meta: {
// 规则的类型 problem|suggestion|layout
// problem: 这条规则识别的代码可能会导致错误或让人迷惑。应该优先解决这个问题
// suggestion: 这条规则识别的代码不会导致错误,但是建议用更好的方式
// layout: 表示这条规则主要关心像空格、分号等这种问题
type: "suggestion",
// 对于自定义规则,docs字段是非必须的
docs: {
description: "描述你的规则是干啥的",
// 规则的分类,假如你把这条规则提交到eslint核心规则里,那eslint官网规则的首页会按照这个字段进行分类展示
category: "Possible Errors",
// 假如你把规则提交到eslint核心规则里
// 且像这样extends: ['eslint:recommended']继承规则的时候,这个属性是true,就会启用这条规则
recommended: true,
// 你的规则使用文档的url
url: "https://eslint.org/docs/rules/no-extra-semi",
},
// 定义提示信息文本 error-name为提示文本的名称 定义后我们可以在规则内部使用这个名称
messages: {
"error-name": "这是一个错误的命名"
},
// 标识这条规则是否可以修复,假如没有这属性,即使你在下面那个create方法里实现了fix功能,eslint也不会帮你修复
fixable: "code",
// 这里定义了这条规则需要的参数
// 比如我们是这样使用带参数的rule的时候,rules: { myRule: ['error', param1, param2....]}
// error后面的就是参数,而参数就是在这里定义的
schema: [],
},
create: function (context) {
// 这是最重要的方法,我们对代码的校验就是在这里做的
return {
// callback functions
};
},
};

本次Eslint 校验规则是推荐使用更好的lodash引用方式,所以常见规则类型 typesuggestion

AST 结构

Eslint 的本质是通过代码生成的 AST 树做代码的静态分析,我们可以使用 astexplorer 快速方便地查看解析成 AST 的结构。

我们将如下代码输入

import { omit } from 'lodash'

得到的 AST 结构如下:

{
"type": "Program",
"start": 0,
"end": 29,
"body": [
{
"type": "ImportDeclaration",
"start": 0,
"end": 29,
"specifiers": [
{
"type": "ImportSpecifier",
"start": 9,
"end": 13,
"imported": {
"type": "Identifier",
"start": 9,
"end": 13,
"name": "omit"
},
"local": {
"type": "Identifier",
"start": 9,
"end": 13,
"name": "omit"
}
}
],
"source": {
"type": "Literal",
"start": 21,
"end": 29,
"value": "lodash",
"raw": "'lodash'"
}
}
],
"sourceType": "module"
}

分析 AST 的结构,我们可以知道:

  • type 为 包的引入方式

  • source 为 资源名(依赖包名)

  • specifiers 为导出的模块

节点访问方法

Eslint 规则中的 create 函数create (function) 返回一个对象,其中包含了 ESLint 在遍历 JavaScript 代码的抽象语法树 AST (ESTree 定义的 AST) 时,用来访问节点的方法。其中, 访问节点的方法如下:

  • VariableDeclaration,则返回声明中声明的所有变量。

  • 如果节点是一个 VariableDeclarator,则返回 declarator 中声明的所有变量。

  • 如果节点是 FunctionDeclarationFunctionExpression,除了函数参数的变量外,还返回函数名的变量。

  • 如果节点是一个 ArrowFunctionExpression,则返回参数的变量。

  • 如果节点是 ClassDeclarationClassExpression,则返回类名的变量。

  • 如果节点是一个 CatchClause 子句,则返回异常的变量。

  • 如果节点是 ImportDeclaration,则返回其所有说明符的变量。

  • 如果节点是 ImportSpecifierImportDefaultSpecifierImportNamespaceSpecifier,则返回声明的变量。

本次我们是校验资源导入规范,所以我们使用ImportDeclaration获取我们导入资源的节点结构

代码修复

report()函数返回一个特定结构的对象,它用来发布警告或错误, 我们可以通过配置对象去配置错误AST 节点,错误提示的内容(可使用 meta 配置的 meaasge 名称)以及修复方式

实例配置代码如下

context.report({
node: node,
message: "Missing semicolon",
fix: function(fixer) {
return fixer.insertTextAfter(node, ";");
}
});

编写代码

了解完上述内容,我们就可以开始愉快的编写代码了。

自定义规则代码如下:

 // lib/rules/lodash-auto-import.js

/**
* @fileoverview 这是一个lodash按需引入的eslint规则
* @author guming-eslint
*/
"use strict";

//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------

/** @type {import('eslint').Rule.RuleModule} */

const SOURCElIST = ["lodash", "lodash-es"];
module.exports = {
// eslint-disable-next-line eslint-plugin/prefer-message-ids
meta: {
type: "suggestion", // `problem`, `suggestion`, or `layout`
docs: {
description: "这是一个lodash按需引入的eslint规则",
recommended: true,
url: null, // URL to the documentation page for this rule
},
messages: {
autoImportLodash: "请使用lodash按需引用",
invalidImport: "lodash 导出依赖不为空",
},
fixable: "code",
schema: [],
},

create: function (context) {

// 获取lodash中导入的函数名称,并返回
function getImportSpecifierArray(specifiers) {
const incluedType = ["ImportSpecifier", "ImportDefaultSpecifier"];
return specifiers
.filter((item) => incluedType.includes(item.type))
.map((item) => {
return item.imported ? item.imported.name : item.local.name;
});
}

// 生成修复文本
function generateFixedImportText(importedList, dependencyName) {
let fixedText = "";
importedList.forEach((importName, index) => {
fixedText += `import ${importName} from "${dependencyName}/${importName}";`;
if (index != importedList.length - 1) fixedText += "\n";
});
return fixedText;
}

return {
ImportDeclaration(node) {
const source = node.source.value;
const hasUseLodash = SOURCElIST.inclues(source);

// 使用lodash
if (hasUseLodash) {
const importedList = getImportSpecifierArray(node.specifiers || []);

if (importedList.length <= 0) {
return context.report({
node,
messageId: "invalidImport",
});
}

const dependencyName = getImportDependencyName(node);
return context.report({
node,
messageId: "autoImportLodash",
fix(fixer) {
return fixer.replaceTextRange(
node.range,
generateFixedImportText(importedList, dependencyName)
);
},
});
}
},
};
},
};

配置规则组

// lib/rules/index.js

const requireIndex = require("requireindex");
// 在这里导入了我们上面写的自定义规则
const rules = requireIndex(__dirname + "/rules");
module.exports = {
// rules是必须的
rules,
// 增加configs配置
configs: {
// 配置了这个之后,就可以在其他项目中像下面这样使用了
// extends: ['plugin:guming-eslint/recommended']
recommended: {
plugins: ['guming-eslint'],
rules: {
'guming-eslint/lodash-auto-import': ['error'],
}
}
}
}

补充测试用例

// tests/lib/rules/lodash-auto-import.js
/**
* @fileoverview 这是一个lodash按需引入的eslint规则
* @author guming-eslint
*/
"use strict";

//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------

const rule = require("../../../lib/rules/lodash-auto-import"),
RuleTester = require("eslint").RuleTester;

//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------

const ruleTester = new RuleTester();
ruleTester.run("lodash-auto-import", rule, {
valid: ['import omit from "lodash/omit";', 'import { omit } from "lodash-es";'],

invalid: [
// eslint-disable-next-line eslint-plugin/consistent-output
{
code: 'import {} from "lodash";',
errors: [{ message: "invalidImport" }],
output: 'import xxx from lodash/xxx'
},
{
code: 'import {} from "lodash-es";',
errors: [{ message: "invalidImport" }],
output: 'import { xxx } from lodash-es'
},
{
code: 'import { omit } from "lodash";',
errors: [{ message: "directlyImportLodash" }],
output: 'import omit from "lodash/omit";',
},
{
code: 'import { omit as _omit } from "lodash";',
errors: [{ message: "directlyImportLodash" }],
output: 'import omit from "lodash/omit";',
},
{
code: 'import { omit, debounce } from "lodash";',
errors: [{ message: "directlyImportLodash" }],
output:
'import omit from "lodash/omit"; \n import debounce from "lodash/debounce";',
},
],
});

可输入如下指令,执行测试

$ yarn run test

注意事项

开发这个插件的一些注意事项如下

  • 多个模块导出

  • lodash 具名导出和默认导出

  • 模块别名(as)

使用

插件安装

  • npm 包发布安装调试

$ yarn add eslint-plugin-guming-eslint
  • npm link 本地调试(推荐使用) 插件项目目录执行如下指令

$ npm link

项目目录执行如下指令

$ npm link eslint-plugin-guming-eslint

项目配置

添加你的 plugin 包名(eslint-plugin- 前缀可忽略) 到 .eslintrc 配置文件的 extends 字段。

.eslintrc 配置文件示例:

module.exports = {
// 你的插件
extends: ["plugin:guming-eslint/recommended"],
parserOptions: {
ecmaVersion: 7,
sourceType: "module",
},
};

效果

作者:古茗前端团队
来源:juejin.cn/post/7202413628807938108

收起阅读 »

用一周时间开发了一个微信小程序,我遇到了哪些问题?

功能截图特别说明:由于本项目是用于教学案例,并没有上线的二维码供大家体验。开发版本微信开发者工具版本:1.06调试基础库:2.30代码仓库gitee:gitee.com/guigu-fe/gu…github:github.com/xiumubai/gu…建议全...
继续阅读 »

功能截图

home.pic.jpginfo.pic.jpg
address-add.pic.jpgaddress-list.pic.jpg
cart-list.pic.jpgcategory.pic.jpg
goods-detail.pic.jpgorder-list.pic.jpg
goods-list.pic.jpgorder-detail.pic.jpg
特别说明:由于本项目是用于教学案例,并没有上线的二维码供大家体验。

开发版本

  • 微信开发者工具版本:1.06

  • 调试基础库:2.30

代码仓库

建议全文参考源代码观看效果更佳,代码可直接在微信开发者工具当中打开预览,appid需要替换成自己的。

获取用户信息变化

用户头像昵称获取规则已调整,现在微信小程序已经获取不到用户昵称和头像了,只能已通过用户回填(提供给用户一个修改昵称和头像的表单页面)的方式来实现。不过还是可以获取到code跟后端换取token的方式来进行登录。

具体参考 *用户信息接口调整说明*小程序用户头像昵称获取规则调整公告

vant weapp组件库的使用

1.需要使用npm构建的能力,用 npm 构建前,请先阅读微信官方的 npm 支持。初始化package.json

npm init

2.安装@vant/weapp

# 通过 npm 安装
npm i @vant/weapp -S --production

# 通过 yarn 安装
yarn add @vant/weapp --production

# 安装 0.x 版本
npm i vant-weapp -S --production

2.修改 app.json 将 app.json 中的 "style": "v2" 去除,小程序的新版基础组件强行加上了许多样式,难以覆盖,不关闭将造成部分组件样式混乱。 3.修改 project.config.json 开发者工具创建的项目,miniprogramRoot 默认为 miniprogrampackage.json 在其外部,npm 构建无法正常工作。 需要手动在 project.config.json 内添加如下配置,使开发者工具可以正确索引到 npm 依赖的位置。

{
...
 "setting": {
  ...
   "packNpmManually": true,
   "packNpmRelationList": [
    {
       "packageJsonPath": "./package.json",
       "miniprogramNpmDistDir": "./miniprogram/"
    }
  ]
}
}

注意: 由于目前新版开发者工具创建的小程序目录文件结构问题,npm构建的文件目录为miniprogram_npm,并且开发工具会默认在当前目录下创建miniprogram_npm的文件名,所以新版本的miniprogramNpmDistDir配置为'./'即可。 4.构建 npm 包 打开微信开发者工具,点击 工具 -> 构建 npm,并勾选 使用 npm 模块 选项,构建完成后,即可引入组件。 image.png

使用组件

引入组件

// 通过 npm 安装
// app.json
"usingComponents": {
 "van-button": "@vant/weapp/button/index"
}

使用组件

<van-button type="primary">按钮</van-button>

如果预览没有效果,从新构建一次npm,然后重新打开此项目

自定义tabbar

这里我的购物车使用了徽标,所以需要自定义一个tabbar,这里自定义以后,会引发后面的一系列连锁反应(比如内容区域高度塌陷,导致tabbar遮挡内容区域),后面会讲如何计算。效果如下图: image.png

1. 配置信息

  • 在 app.json 中的 tabBar 项指定 custom 字段,同时其余 tabBar 相关配置也补充完整。

  • 所有 tab 页的 json 里需声明 usingComponents 项,也可以在 app.json 全局开启。

示例:

{
 "tabBar": {
   "custom": true,
   "color": "#000000",
   "selectedColor": "#000000",
   "backgroundColor": "#000000",
   "list": [{
     "pagePath": "page/component/index",
     "text": "组件"
  }, {
     "pagePath": "page/API/index",
     "text": "接口"
  }]
},
 "usingComponents": {}
}

2. 添加 tabBar 代码文件

需要跟pages目录同级,创建一个custom-tab-bar目录。 image.png .wxml代码如下:

<!--miniprogram/custom-tab-bar/index.wxml-->
<cover-view class="tab-bar">
 <cover-view class="tab-bar-border"></cover-view>
 <cover-view wx:for="{{list}}"
   wx:key="index"
   class="tab-bar-item"
   data-path="{{item.pagePath}}"
   data-index="{{index}}"
   bindtap="switchTab">
   <cover-view class="tab-img-wrap">
     <cover-image src="{{selected === index ? item.selectedIconPath : item.iconPath}}"></cover-image>
     <cover-view wx-if="{{item.info && cartCount > 0}}"class="tab-badge">{{cartCount}}</cover-view>
   </cover-view>
   <cover-view style="color: {{selected === index ? selectedColor : color}}">{{item.text}}</cover-view>
 </cover-view>
</cover-view>

注意这里的徽标控制我是通过info字段来控制的,然后数量cartCount单独第一个了一个字段,这个字段是通过store来管理的,后面会讲为什么通过stroe来控制的。

3. 编写 tabBar 代码

用自定义组件的方式编写即可,该自定义组件完全接管 tabBar 的渲染。另外,自定义组件新增 getTabBar 接口,可获取当前页面下的自定义 tabBar 组件实例。

import { storeBindingsBehavior } from 'mobx-miniprogram-bindings';
import { store } from '../store/index';

Component({
 behaviors: [storeBindingsBehavior],
 storeBindings: {
   store,
   fields: {
     count: 'count',
  },
   actions: [],
},
 observers: {
   count: function (val) {
     // 更新购物车的数量
     this.setData({ cartCount: val });
  },
},
 data: {
   selected: 0,
   color: '#252933',
   selectedColor: '#FF734C',
   cartCount: 0,
   list: [
    {
       pagePath: '/pages/index/index',
       text: '首页',
       iconPath: '/static/tabbar/home-icon1.png',
       selectedIconPath: '/static/tabbar/home-icon1-1.png',
    },
    {
       pagePath: '/pages/category/category',
       text: '分类',
       iconPath: '/static/tabbar/home-icon2.png',
       selectedIconPath: '/static/tabbar/home-icon2-2.png',
    },
    {
       pagePath: '/pages/cart/cart',
       text: '购物车',
       iconPath: '/static/tabbar/home-icon3.png',
       selectedIconPath: '/static/tabbar/home-icon3-3.png',
       info: true,
    },
    {
       pagePath: '/pages/info/info',
       text: '我的',
       iconPath: '/static/tabbar/home-icon4.png',
       selectedIconPath: '/static/tabbar/home-icon4-4.png',
    },
  ],
},

 lifetimes: {},
 methods: {
 // 改变tab的时候,记录index值
   switchTab(e) {
     const { path, index } = e.currentTarget.dataset;
     wx.switchTab({ url: path });
     this.setData({
       selected: index,
    });
  },
},
});

这里的store大家不用理会,只需要记住是设置徽标的值就可以了。

4.设置样式

.tab-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 48px;
background: white;
display: flex;
padding-bottom: env(safe-area-inset-bottom);
}

这里的样式单独贴出来说明一下:

padding-bottom: env(safe-area-inset-bottom);

可以让出底部安全区域,不然的话tabbar会直接沉到底部 image.png 别忘了在index.json中设置component=true

{
"component": true
}

5.tabbar页面设置index

上面的代码添加完毕以后,我们的tabbar就出来了,但是有个问题,就是在点击tab的时候,样式不会改变,必须再点击一次,这是因为当你切换页面或者刷新页面的时候,index的值会重置,为了解决这个问题,我们需要在每个tabbar的页面添加下面的代码:

/**
* 生命周期函数--监听页面显示
*/
onShow() {
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
this.getTabBar().setData({
selected: 0,
});
}
},

当页面每次show的时候,设置一下selected的值,也就是选中的index就可以了。其他的tabbar页面同样也是如此设置即可。

添加store状态管理

接下来我们来讲讲微信小程序如何用store来管理我们的数据。 上面我们说了我们需要实现一个tabbar的徽标,起初我想的是直接用个缓存来解决就完事了,后来发现我太天真了,忘记了这个字段是一个响应式的,它是需要渲染到页面上的,它变了,页面中的数据也得跟着一起变。后来我想通过globalData来实现,也不行。后来我又又想到了把这个数据响应式监听一下不就行了?于是通过proxy,跟vue3的处理方式一样,监听一下这个字段的改变就可以了。在购物车这个页面触发的时候是挺好,可当我切换到其他tabbar页面的时候它就不见了。我忽略了一个问题,它是全局响应的啊。于是最后才想到了使用store的方式来实现。 我找到了一个针对微信小程序的解决方法,就是使用mobx-miniprogram-bindingsmobx-miniprogram这两个库来解决。真是帮了我的大忙了。 下面我们直接来使用。 先安装两个插件:

npm install --save mobx-miniprogram mobx-miniprogram-bindings

方式跟安装vant weapp一样,npm安装完成以后,在微信开发者工具当中构建npm即可。 下面我们来通过如何实现一个tabbar徽标的场景来学习如何在微信小程序中使用store来管理全局数据。

tabbar徽标实现

1.定义store

import { observable, action, runInAction } from 'mobx-miniprogram';
import { getCartList } from './cart';
// 获取购物车数量

export const store = observable({
/** 数据字段 */
count: 0,

/** 异步方法 */
getCartListCount: async function () {
const num = await getCartList();
runInAction(() => {
this.count = num;
});
},

/** 更新购物车的数量 */
updateCount: action(function (num) {
this.count = num;
}),
});

看起来是不是非常简单。这里我们定义了一个count,然后定义了两个方法,这两个方法有点区别:

  • updateCount用来更新count

  • getCartListCount用来异步更新count,因为这里我们在进入小程序的时候就需要获取count的初始值,这个值的计算又的依赖接口,所以需要使用异步的方式。

好了,现在我们字段有了,设置初始值的方法有了,更新字段的方法也有了。下面我们来看一下如何使用。

2.使用store

回到我们的tabbr组件,在custom-tab-bari/ndex.js中,我们贴一下主要的代码:

import { storeBindingsBehavior } from 'mobx-miniprogram-bindings';
import { store } from '../store/index';

Component({
behaviors: [storeBindingsBehavior],
storeBindings: {
store,
fields: {
count: 'count',
},
actions: [],
},
observers: {
count: function (val) {
// 更新购物车的数量
this.setData({ cartCount: val });
},
},
data: {
cartCount: 0,
},
});

解释一下,这里我们只是获取了count的值,然后通过observers的方式监听了一下count,然后赋值给了cartCount,这里你直接使用count渲染到页面上也是没有问题的。我这里只是为了演示一下observers的使用方式才这么写的。这样设置以后,tabbar上面的徽标数字已经可以正常展示了。 现在当我们的购物车数字改变以后,就要更新count的值了。

3.使用action

找到我们的cart页面,下面是具体的逻辑:

import {
findCartList,
deleteCart,
checkCart,
addToCart,
checkAllCart,
} from '../../utils/api';

import { createStoreBindings } from 'mobx-miniprogram-bindings';
import { store } from '../../store/index';
import { getCartTotalCount } from '../../store/cart';
const app = getApp();
Page({
data: {
list: [],
totalCount: 0,
},

/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
this.storeBindings = createStoreBindings(this, {
store,
fields: ['count'],
actions: ['updateCount'],
});
},

/**
* 声明周期函数--监听页面卸载
*/
onUnload() {
this.storeBindings.destroyStoreBindings();
},

/**
* 生命周期函数--监听页面显示
*/
onShow() {
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
this.getTabBar().setData({
selected: 2,
});
}
this.getCartList();
},

/**
* 获取购物车列表
*/
async getCartList() {
const res = await findCartList();
this.setData({
list: res.data,
});
this.computedTotalCount(res.data);
},

/**
* 修改购物车数量
*/
async onChangeCount(event) {
const newCount = event.detail;
const goodsId = event.target.dataset.goodsid;
const originCount = event.target.dataset.count;
// 这里如果直接拿+以后的数量,接口的处理方式是直接在上次的基础累加的,
// 所以传给接口的购物车数量的计算方式如下:
// 购物车添加的数量=本次的数量-上次的数量
const count = newCount - originCount;
const res = await addToCart({
goodsId,
count,
});
if (res.code === 200) {
this.getCartList();
}
},

/**
* 计算购物车总数量
*/
computedTotalCount(list) {
// 获取购物车选中数量
const total = getCartTotalCount(list);
// 设置购物车徽标数量
this.updateCount(total);
},


});

上面的代码有所删减。在page和component中使用action方法有所区别,需要在onUnload的时候销毁一下我们的storeBindings。当修改购物车数量的时候,我这里会重新请求一次接口,然后计算一下totalCount的数量,通过updateCount来修改count的值。到了这里,我们的徽标就可以正常的使用了。不管是切换到哪一个tabbar页面,徽标都会保持状态。

4.使用异步action

现在还剩最后一个问题,就是如何设置count的初始值,这个值还得从接口获取过来。下面是实现思路。 首先我们在store中定义了一个一步方法:

import { observable, action, runInAction } from 'mobx-miniprogram';
import { getCartList } from './cart';
// 获取购物车数量

export const store = observable({
/** 数据字段 */
count: 0,

/** 异步方法 */
getCartListCount: async function () {
const num = await getCartList();
runInAction(() => {
this.count = num;
});
},

/** 更新购物车的数量 */
updateCount: action(function (num) {
this.count = num;
}),
});

可以看到,异步action的实现跟同步的区别很大,使用了runInAction这个方法,在它的回调函数中去修改count的值。很坑的是,这个方法在[mobx-miniprogram-bindings](https://www.npmjs.com/package/mobx-miniprogram-bindings)中的官方文档中没有做任何说明,我百度了好久才找到。 现在,我们有了这个方法,在哪里触发好合适呢?答案是app.js中的onShow生命周期函数中。也就是每次我们进入小程序,就会设置一下count的初始值了。下面是代码:

// app.js
import { createStoreBindings } from 'mobx-miniprogram-bindings';
import { store } from './store/index';
App({
onShow() {
this.storeBindings = createStoreBindings(this, {
store,
fields: [],
actions: ['getCartListCount'],
});
// 在页面初始化的时候,更新购物车徽标的数量
this.getCartListCount();
},
});

到此为止,整个完整的徽标响应式改变和store的使用完美的融合了。 参考文章:blog.csdn.net/ice_stone_k…

如何获取tabbar的高度

当我们自定义tabbar以后,由于tabbar是使用的fixed定位,我们的内容区域如果不做任何限制,底部的内容就会被tabbar遮挡,所以我们需要给内容区域整体设置一个padding-bottom,那这个值是多少呢?有的人可能会说,直接把tabbar的高度固定,然后padding-bottom设置成这个高度的值不就可以了吗?你别忘了,现在五花八门的手机下面还有一个叫做安全区域的东西,如下图:

image.png

如果你没有把这个高度加上,那内容区域还是会被tabbar遮挡。下面我们就来看看这个高度具体如何计算呢? 我们以通过wx.getSystemInfoSync()获取机型的各种信息。 image.png

其中screenHeight是屏幕高度,safeAreabottom属性会自动计算安全区域也就是去除tabBar下面的空白区域后有用区域的纵坐标。如此我们就可以就算出来tabber的高度:

const res = wx.getSystemInfoSync()
const { screenHeight, safeArea: { bottom } } = res

if (screenHeight && bottom){
let safeBottom = screenHeight - bottom
const tabbarHeight = 48 + safeBottom
}

这里48是tabbar的高度,我们固定是48px。拿到tabbarHeight以后,把它设置成一个globalData,我们就可以给其他页面设置padding-bottom了。 我这里还使用了其他的一些属性,具体参考代码如下:

// app.js

App({
onLaunch() {
// 获取高度
this.getHeight();
},
onShow() {
},
globalData: {
// tabber+安全区域高度
tabbarHeight: 0,
// 安全区域的高度
safeAreaHeight: 0,
// 内容区域高度
contentHeight: 0,
},
getHeight() {
const res = wx.getSystemInfoSync();
// 胶囊按钮位置信息
const menuButtonInfo = wx.getMenuButtonBoundingClientRect();
const {
screenHeight,
statusBarHeight,
safeArea: { bottom },
} = res;
// console.log('resHeight', res);

if (screenHeight && bottom) {
// 安全区域高度
const safeBottom = screenHeight - bottom;
// 导航栏高度 = 状态栏到胶囊的间距(胶囊距上距离-状态栏高度) * 2 + 胶囊高度 + 状态栏高度
const navBarHeight =
(menuButtonInfo.top - statusBarHeight) * 2 +
menuButtonInfo.height +
statusBarHeight;
// tabbar高度+安全区域高度
this.globalData.tabbarHeight = 48 + safeBottom;
this.globalData.safeAreaHeight = safeBottom;
// 内容区域高度,用来设置内容区域最小高度
this.globalData.contentHeight = screenHeight - navBarHeight;
}
},
});

假如我们需要给首页设置一个首页设置一个padding-bottom

// components/layout/index.js
const app = getApp();
Component({
/**
* 组件的属性列表
*/
properties: {
bottom: {
type: Number,
value: 48,
},
},

/**
* 组件的方法列表
*/
methods: {},
});
<view style="padding-bottom: {{bottom}}px">
<slot></slot>
</view>

这里我简单粗暴的直接在外层套了一个组件,统一设置了padding-bottom。 除了自定义tabbar,还可以自定义navbar,这里我没这个功能,所以不展开讲了,这里放一个参考文章: 获取状态栏的高度。这个文章把如何自定义navbar,如何获取navbar的高度,讲的很通透,感兴趣的仔细拜读。

分页版上拉加载更多

为什么我称作是分页版本的上拉加载更多呢,因为就是上拉然后多加载一页,没有做那种虚拟加载,感兴趣的可以参考这篇文章(我觉得写的非常到位了)。下面我以商品列表为例,代码在pages/goods/list下,讲讲简单版本的实现:

<!--pages/goods/list/index.wxml-->

<view style="min-height: {{contentHeight}}px; padding-bottom: {{safeAreaHeight}}px">

<view wx:if="{{list.length > 0}}">
<goods-card wx:for="{{list}}" wx:key="index" item="{{item}}"></goods-card>
<!-- 上拉加载更多 -->
<load-more
list-is-empty="{{!list.length}}"
status="{{loadStatus}}"
/>
</view>

<van-empty wx:else description="该分类下暂无商品,去看看其他的商品吧~">
<van-button
round
type="danger"

bindtap="gotoBack">
查看其他商品
</van-button>
</van-empty>

</view>
// pages/goods/list/index.js
import { findGoodsList } from '../../../utils/api';
const app = getApp();
Page({
/**
* 页面的初始数据
*/
data: {
page: 1,
limit: 10,
list: [],
options: {},
loadStatus: 0,
contentHeight: app.globalData.contentHeight,
safeAreaHeight: app.globalData.safeAreaHeight,
},

/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
this.setData({ options });
this.loadGoodsList(true);
},

/**
* 页面上拉触底事件的处理函数
*/
onReachBottom() {
// 还有数据,继续请求接口
if (this.data.loadStatus === 0) {
this.loadGoodsList();
}
},

/**
* 商品列表
*/
async loadGoodsList(fresh = false) {
// wx.stopPullDownRefresh();
this.setData({ loadStatus: 1 });
let page = fresh ? 1 : this.data.page + 1;
// 组装查询参数
const params = {
page,
limit: this.data.limit,
...this.data.options,
};
try {
// loadstatus说明: 0-加载完毕,隐藏加载状态 1-正在加载 2-全部加载 3-加载失败
const res = await findGoodsList(params);
const data = res.data.records;
if (data.length > 0) {
this.setData({
list: fresh ? data : this.data.list.concat(data),
loadStatus: data.length === this.data.limit ? 0 : 2,
page,
});
} else {
// 数据全部加载完毕
this.setData({
loadStatus: 2,
});
}
} catch {
// 错误请求
this.setData({
loadStatus: 3,
});
}
},
});

代码已经很详细了,我再展开说明一下。

  • onLoad的时候第一次请求商品列表数据loadGoodsList,这里我加了一个fresh字段,用来区分是不是第一次加载,从而且控制page是不是等于1

  • 触发onReachBottom的时候,先判断loadStatus === 0,表示接口数据还没加载完,继续请求loadGoodsList

  • loadGoodsList里面,先设置loadStatus = 1,表示状态为加载中。如果fresh为false,则表示要请求下一页的数据了,page+1。

  • 接口请求成功,给了list添加数据的时候要注意了,这里需要再上次list的基础上拼接数据,所以得用concat。同时修改loadStatus状态,如果当前请求回来的数据条数小与limit(每页数据大小),则表示没有更对的数据了,loadStatus = 2,反之为0。

  • 最后为了防止特殊情况出现,还有个loadStatus = 3,表示加载失败的情况。

这里我封装了一个load-more组件,里面就是对loadStatus各种不同状态的处理。具体详情看看源码。 思考:如果加上个下拉刷新,跟上拉加载放在一起,如何实现呢?

如何分包

为什么要做小程序分包?先来看看小程序对文件包的大小限制 image.png 在不使用分包的时候,代码总包的大小限制为2M,如果使用了分包,总包大小可以达到20M,也就是我们能分10个包。 那么如何分包?非常的简单。代码如下:

{
"pages": [
"pages/index/index",
"pages/category/category",
"pages/cart/cart",
"pages/info/info",
"pages/login/index"
],
"subpackages": [
{
"root": "pages/goods",
"pages": [
"list/index",
"detail/index"
]
},
{
"root": "pages/address",
"pages": [
"list/index",
"add/index"
]
},
{
"root": "pages/order",
"pages": [
"pay/index",
"list/index",
"result/index",
"detail/index"
]
}
],
}

目录结构如下: image.png 解释一下: 我们subpackages下面的就是分包的内容,里面的每一个root就是一个包,pages里面的内容只能是这样的字符串路径,添加别的内容会报错。分包的逻辑:业务相关的页面放在一个包下,也就是一个目录下即可。 ⚠️注意:tabbar的页面不能放在分包里面。 下面是分包以后的代码依赖分析截图: image.png

后续更新计划

  • 小程序如何自定义navbar

  • 小程序如何添加typescript

  • 在小程序中如何做表单校验的小技巧

  • 微信支付流程

  • 如何在小程序中mock数据

  • 如何优化小程序

本文章可以随意转载。转给更多需要帮助的人。看了源码觉得有帮助的可以点个star。我会持续更新更多系列教程。

作者:白哥学前端
来源:https://juejin.cn/post/7202495679397511227

收起阅读 »

手把手教你实现MVVM架构

web
引言现在的前端真可谓是百花齐放,百家争鸣,各种框架层出不穷,但是主要目前用的最多的还是要数Vue、React、以及Angular,这三种,当然不乏近期新出的一些其他框架,但是她们都有一个显著的特点,那就是使用了MVVM的架构。首先我们要搞清楚什么是MVVM?M...
继续阅读 »

引言

现在的前端真可谓是百花齐放,百家争鸣,各种框架层出不穷,但是主要目前用的最多的还是要数VueReact、以及Angular,这三种,当然不乏近期新出的一些其他框架,但是她们都有一个显著的特点,那就是使用了MVVM的架构。

首先我们要搞清楚什么是MVVM

MVVM就是Model-View-ViewModel的缩写,MVVM最早由微软提出来,它借鉴了桌面应用程序的MVC思想,在前端页面中,把Model用纯JavaScript对象表示,View负责显示,两者做到了最大限度的分离。把ModelView关联起来的就是ViewModelViewModel负责把Model的数据同步到View显示出来,还负责把View的修改同步回Model

改变JavaScript对象的状态,会导致DOM结构作出对应的变化!这让我们的关注点从如何操作DOM变成了如何更新JavaScript对象的状态,而操作JavaScript对象比DOM简单多了。

这就是MVVM的设计思想:关注Model的变化,让MVVM框架去自动更新DOM的状态,从而把开发者从操作DOM的繁琐步骤中解脱出来!

接下来我会带着你们如何去实现一个简易的MVVM架构。

一、构建MVVM构造函数

创建一个MVVM构造函数,用于接收参数,如:datamethodscomputed等:

function MVVM(options) { 
   this.$options = options;
   let data = this._data = this.$options.data;
   observe(data);
   for (let key in data) {
       Object.defineProperty(this, key, {
           enumerable: true,
           get() {
               return this._data[key];
          },
           set(newVal) {
               this._data[key] = newVal;
          }
      });
  };
   initComputed.call(this);
   new Compile(options.el, this);
   options.mounted.call(this);
}

二、构建Observe构造函数

创建一个Observe构造函数,用于监听数据变化:

function Observe(data) { 
   let dep = new Dep();
   for (let key in data) {
       let val = data[key];
       observe(val);
       Object.defineProperty(data, key, {
           enumerable: true,
           get() {
               Dep.target && dep.addSub(Dep.target);
               return val;
          },
           set(newVal) {
               if (val === newVal) {
                   return;
              }
               val = newVal;
               observe(newVal);
               dep.notify();
          }
      });
  };
};

三、构建Compile构造函数

创建一个Compile构造函数,用于解析模板指令:

function Compile(el, vm) { 
   vm.$el = document.querySelector(el);
   let fragment = document.createDocumentFragment();
   while (child = vm.$el.firstChild) {
       fragment.appendChild(child);
  }
   replace(fragment);
   function replace(frag) {
       Array.from(frag.childNodes).forEach(node => {
           let txt = node.textContent;
           let reg = /\{\{(.*?)\}\}/g;
           if (node.nodeType === 3 && reg.test(txt)) {
               let arr = RegExp.$1.split('.');
               let val = vm;
               arr.forEach(key => { val = val[key]; });
               node.textContent = txt.replace(reg, val).trim();
               new Watcher(vm, RegExp.$1, newVal => {
                   node.textContent = txt.replace(reg, newVal).trim();
              });
          }
       if (node.nodeType === 1) {
           let nodeAttr = node.attributes;
           Array.from(nodeAttr).forEach(attr => {
               let name = attr.name;
               let exp = attr.value;
               if (name.includes('')) {
                   node.value = vm[exp];
              }
               new Watcher(vm, exp, newVal => {
                   node.value = newVal;
              });
               node.addEventListener('input', e => {
                   let newVal = e.target.value;
                   vm[exp] = newVal;
              });
            });
        };
        if (node.childNodes && node.childNodes.length) {
            replace(node);
        }
      });
    }
    vm.$el.appendChild(fragment);
}

四、构建Watcher构造函数

创建一个Watcher构造函数,用于更新视图:

function Watcher(vm, exp, fn) { 
   this.fn = fn;
   this.vm = vm;
   this.exp = exp;
   Dep.target = this;
   let arr = exp.split('.');
   let val = vm;
   arr.forEach(key => {
       val = val[key];
  });
   Dep.target = null;
}

Watcher.prototype.update = function() {
       let arr = this.exp.split('.');
       let val = this.vm;
       arr.forEach(key => {
           val = val[key];
      });
       this.fn(val);
}

五、构建Dep构造函数

创建一个Dep构造函数,用于管理Watcher:

function Dep() { 
   this.subs = [];
}

Dep.prototype.addSub = function(sub) {
   this.subs.push(sub);
}

Dep.prototype.notify = function() {
   this.subs.forEach(sub => {
       sub.update();
  });
}

六、构建initComputed构造函数

创建一个initComputed构造函数,用于初始化计算属性:

function initComputed() { 
   let vm = this;
   let computed = this.$options.computed;
   Object.keys(computed).forEach(key => {
       Object.defineProperty(vm, key, {
           get: typeof computed[key] === 'function' ? computed[key] : computed[key].get,
           set() {}
      });
  });
}

总结:

至此我们就完成了一个简易的MVVM框架,虽然简易,但是基本的核心思想差不多都已经表达出来了,最后还是希望大家不要丢在收藏文件夹里吃灰,还是要多多动手练习一下,所谓眼过千遍,不如手过一遍。

作者:前端第一深情阿斌
来源:juejin.cn/post/7202431872968851517

收起阅读 »

Nginx基本介绍+跨域解决方案

web
Nginx简介 Nginx 是一款由俄罗斯的程序设计师 Igor Sysoev 所开发的高性能的 http 服务器/反向代理服务器及电子邮件(IMAP/POP3)代理服务器,它的主要功能有: 反向代理 负载均衡 HTTP 服务器 目前大部分运行的 Ngin...
继续阅读 »

Nginx简介


Nginx 是一款由俄罗斯的程序设计师 Igor Sysoev 所开发的高性能的 http 服务器/反向代理服务器及电子邮件(IMAP/POP3)代理服务器,它的主要功能有:



  • 反向代理

  • 负载均衡

  • HTTP 服务器


目前大部分运行的 Nginx 服务器都在使用其负载均衡的功能作为服务集群的系统架构。


功能说明


在上文中介绍了三种 Nginx 的主要功能,下面来讲讲具体每个功能的作用。


一、反向代理(Reverse Proxy)


介绍反向代理前,我们先理解下正向代理的概念。打个比方,你准备去看周杰伦的巡演,但是发现官方渠道的票已经卖完了,所以你只好托你神通广大的朋友A去内部购票,你如愿以偿地得到了这张门票。在这个过程中,朋友A就起到了一个正向代理的作用,即代理了客户端(你)去向服务端(售票方)发请求,但服务端(售票方)并不知道源头是谁发起的请求,只知道是代理服务(朋友A)向自己请求的。由这个例子,我们再去理解下反向代理,比如我们经常接到10086或者10000的电话,但是每次打过来的人都不一样,这是因为10086是中国移动的总机号,分机打给用户的时候,都是通过总机代理显示的号码,这个时候客户端(你)无法知道是谁发起的请求,只知道是代理服务(总机)向自己请求的。
而官方的解释说明就是,反向代理方式是指以代理服务器来接受 Internet 上 的连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给 Internet 上请求连接的客户端,此时代理服务器对外就表现为一个反向代理服务器。
下面贴一段简单实现反向代理的 Nginx 配置代码:


server {  
listen 80;
server_name localhost;
client_max_body_size 1024M;

location / {
proxy_pass http://localhost:8080;
proxy_set_header Host $host:$server_port;
}
}
复制代码

其中的 http://localhost:8080 就是反代理的目标服务端,80是 Nginx 暴露给客户端访问的端口。


二、负载均衡(Load Balance**)**


负载均衡,顾名思义,就是将服务负载均衡地分摊到多个服务器单元上执行,来提高网站、应用等服务的性能和可靠性。
下面我们来对比一下两个系统拓扑,首先是未设计负载均衡的拓扑:
未命名绘图.png


下面是设计了负载均衡的拓扑:
未命名绘图2.png
从图二可以看到,用户访问负载均衡器,再由负载均衡器将请求转发给后端服务器,在这种情况下,服务C故障后,用户访问负载会分配到服务A和服务B中,避免了系统崩溃,如果这种故障出现在图一中,该系统一定会会直接崩溃。


负载均衡算法


负载均衡算法决定了后端的哪些健康服务器会被选中。几个常用的算法:



  • **Round Robin(轮询):**为第一个请求选择列表中的第一个服务器,然后按顺序向下移动列表直到结尾,然后循环。

  • **Least Connections(最小连接):**优先选择连接数最少的服务器,在普遍会话较长的情况下推荐使用。

  • **Source:**根据请求源的 IP 的散列(hash)来选择要转发的服务器。这种方式可以一定程度上保证特定用户能连接到相同的服务器。


如果你的应用需要处理状态而要求用户能连接到和之前相同的服务器。可以通过 Source 算法基于客户端的 IP 信息创建关联,或者使用粘性会话(sticky sessions)。


负载均衡同时需要配合反向代理功能才能发挥其作用。


三、HTTP服务器


除了以上两大功能外,Nginx也可以作为静态资源服务器使用,例如没有使用 SSR(Server Side Render)的纯前端资源,就可以依托Nginx来实现资源托管。
下面看一段实现静态资源服务器的配置:


server {
listen 80;
server_name localhost;
client_max_body_size 1024M;

location / {
root e:\wwwroot;
index index.html;
}
}
复制代码

root 配置就是具体资源存放的根目录,index 配置的则是访问根目录时默认的文件。


动静分离


动静分离也是Nginx作为Http服务器使用的一个重要概念,要搞清楚动静分离,首先要弄明白什么是动态资源,什么是静态资源:



  • **动态资源:**需要从服务器中实时获取的资源内容,如 JSP, SSR 渲染页面等,不同时间访问,资源内容会发生变化。

  • **静态资源:**如 JS、CSS、Img 等,不同时间访问,资源内容不会发生变化。


由于Nginx可以作为静态资源服务器,但无法承载动态资源,因此出现需要动静分离的场景时,我们需要拆分静态、动态资源的访问策略:


upstream test{  
server localhost:8080;
server localhost:8081;
}

server {
listen 80;
server_name localhost;

location / {
root e:\wwwroot;
index index.html;
}

# 所有静态请求都由nginx处理,存放目录为html
location ~ \.(gif|jpg|jpeg|png|bmp|swf|css|js)$ {
root e:\wwwroot;
}

# 所有动态请求都转发给tomcat处理
location ~ \.(jsp|do)$ {
proxy_pass http://test;
}

error_page 500 502 503 504 /50x.html;
location = /50x.html {
root e:\wwwroot;
}
}
复制代码

从这段配置可以大概理解到,当客户端访问不同类型的资源时,Nginx 会自动按照类型分配给自己的静态资源服务或者是远程的动态资源服务上,这样就能满足一个完整的资源服务器的功能了。


配置介绍


一、基本介绍


说完 Nginx 的功能,我们来简单进一步介绍下 Nginx 的配置文件。作为前端人员来讲,使用 Nginx 基本上就是修改配置 -> 启动/热重启 Nginx,就能搞定大部分日常和 Nginx 相关的工作了。
这里我们看下一份 Nginx 的默认配置,即安装 Nginx 后,默认的 nginx.conf 文件的内容:



#user nobody;
worker_processes 1;

#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;

#pid logs/nginx.pid;


events {
worker_connections 1024;
}


http {
include mime.types;
default_type application/octet-stream;

#log_format main '$remote_addr - $remote_user [$time_local] "$request" '
# '$status $body_bytes_sent "$http_referer" '
# '"$http_user_agent" "$http_x_forwarded_for"';

#access_log logs/access.log main;

sendfile on;
#tcp_nopush on;

#keepalive_timeout 0;
keepalive_timeout 65;

#gzip on;

server {
listen 80;
server_name localhost;

#charset koi8-r;

#access_log logs/host.access.log main;

location / {
root html;
index index.html index.htm;
}

#error_page 404 /404.html;

# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}

# proxy the PHP scripts to Apache listening on 127.0.0.1:80
#
#location ~ \.php$ {
# proxy_pass http://127.0.0.1;
#}

# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
#
#location ~ \.php$ {
# root html;
# fastcgi_pass 127.0.0.1:9000;
# fastcgi_index index.php;
# fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
# include fastcgi_params;
#}

# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
#location ~ /\.ht {
# deny all;
#}
}


# another virtual host using mix of IP-, name-, and port-based configuration
#
#server {
# listen 8000;
# listen somename:8080;
# server_name somename alias another.alias;

# location / {
# root html;
# index index.html index.htm;
# }
#}


# HTTPS server
#
#server {
# listen 443 ssl;
# server_name localhost;

# ssl_certificate cert.pem;
# ssl_certificate_key cert.key;

# ssl_session_cache shared:SSL:1m;
# ssl_session_timeout 5m;

# ssl_ciphers HIGH:!aNULL:!MD5;
# ssl_prefer_server_ciphers on;

# location / {
# root html;
# index index.html index.htm;
# }
#}

}

复制代码

对应的结构大致是:


...              #全局块

events { #events块
...
}

http #http块
{
... #http全局块
server #server块
{
... #server全局块
location [PATTERN] #location块
{
...
}
location [PATTERN]
{
...
}
}
server
{
...
}
... #http全局块
}
复制代码

以上几个代码块对应功能是:



  • 全局块:配置影响 Nginx 全局的指令。一般有运行 Nginx 服务器的用户组,Nginx 进程 pid 存放路径,日志存放路径,配置文件引入,允许生成 worker process 数等。

  • events块:配置影响 Nginx 服务器或与用户的网络连接。有每个进程的最大连接数,选取哪种事件驱动模型处理连接请求,是否允许同时接受多个网路连接,开启多个网络连接序列化等。

  • http块:可以嵌套多个 server,配置代理,缓存,日志定义等绝大多数功能和第三方模块的配置。如文件引入,mime-type 定义,日志自定义,是否使用 sendfile 传输文件,连接超时时间,单连接请求数等。

  • server块:配置虚拟主机的相关参数,一个 http 中可以有多个 server。

  • location块:配置请求的路由,以及各种页面的处理情况。


各代码块详细的配置方式可以参考 Nginx 文档


二、Nginx 解决跨域问题


下面展示一段常用于处理前端跨域问题的 location代码块,方面各位读者了解及使用 Nginx 去解决跨域问题。


location /cross-server/ {
set $corsHost $http_origin;
set $allowMethods "GET,POST,OPTIONS";
set $allowHeaders "broker_key,X-Original-URI,X-Request-Method,Authorization,access_token,login_account,auth_password,user_type,tenant_id,auth_code,Origin, No-Cache, X-Requested-With, If-Modified-Since, Pragma, Last-Modified, Cache-Control, Expires, Content-Type, X-E4M-With, usertoken";

if ($request_method = 'OPTIONS'){
add_header 'Access-Control-Allow-Origin' $corsHost always;
add_header 'Access-Control-Allow-Credentials' true always;
add_header 'Access-Control-Allow-Methods' $allowMethods always;
add_header 'Access-Control-Allow-Headers' $allowHeaders;
add_header 'Access-Control-Max-Age' 90000000;
return 200;
}

proxy_hide_header Access-Control-Allow-Headers;
proxy_hide_header Access-Control-Allow-Origin;
proxy_hide_header Access-Control-Allow-Credentials;
add_header Access-Control-Allow-Origin $corsHost always;
add_header Access-Control-Allow-Methods $allowMethods always;
add_header Access-Control-Allow-Headers $allowHeaders;
add_header Access-Control-Allow-Credentials true always;
add_header Access-Control-Expose-Headers *;
add_header Access-Control-Max-Age 90000000;

proxy_pass http://10.117.20.54:8000/;
proxy_set_header Host $host:443;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_redirect http:// $scheme://;

}
复制代码

可以看到,前段使用 set 设置了 location 中的局部变量,然后分别在下方的各处指令配置中使用了这些变量,以下是各指令的作用:



  • add_header:用于给请求添加返回头字段,当且仅当状态码为以下列出的那些时有效:200, 201 (1.3.10), 204, 206, 301, 302, 303, 304, 307 (1.1.16, 1.0.13), or 308 (1.13.0)

  • **proxy_hide_heade:**可以隐藏响应头中的信息。

  • **proxy_redirect:**指定修改被代理服务器返回的响应头中的location头域跟refresh头域数值。

  • **proxy_set_header:**重定义发往后端服务器的请求头。

  • **proxy_pass:**被代理的转发服务路径。


以上这段配置可以直接复制到 nginx.conf 中,然后修改 /cross-server/ (Nginx 暴露给客户端访问的路径)和 http://10.117.20.54:8000/(被转发的服务路径)即可实避免服务跨域问题。


跨域技巧补充


开发环境下,如果不想使用 Nginx 来处理跨域调试问题,也可以采用修改 Chrome 配置的方式来实现跨域调试,本质上跨域是一种浏览器的安全策略,所以从浏览器出发去解决这个问题反而更加方便。


Windows 系统:


1、复制chrome浏览器快捷方式,对快捷方式图标点右键打开“属性” 如图:
image.png
2、在“目标”后添加 --disable-web-security --user-data-dir,例如图中修改完成后为:"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" --disable-web-security --user-data-dir


3、点击确定后重新打开浏览器,出现:
image.png


此时,屏蔽跨域设置修改完毕,点开此快捷方式访问的页面均会忽略跨域规则,避免了开发环境下,服务端配置跨域的麻烦。


Mac 系统:


以下内容转载自:Mac上解决Chrome浏览器跨域问题


首先创建一个文件夹,这个文件夹是用来保存关闭安全策略后的用户信息的,名字可以随意取,位置也可以随意放。



创建一个文件夹


然后打开控制台,输入下面这段代码
open -n /Applications/Google\ Chrome.app/ --args --disable-web-security --user-data-dir=/Users/LeoLee/Documents/MyChromeDevUserData



关闭安全策略代码


大家需要根据自己存放刚刚创建的文件夹的地址来更改上面的代码,也就是下面图中的红框区域,而网上大多数的教程中也正是缺少了这部分的代码导致很多用户在关闭安全策略时失败



用户需要根据自己的文件夹地址修改代码


输入代码,敲下回车,接下来Chrome应该会弹出一个窗口



Chrome弹窗


点击启动Google Chrome,会发现与之前的Chrome相比,此时的Chrome多了上方的一段提示,告诉你现在使用的模式并不安全



浏览器上方会多出一行提示


其原理和 Windows 版本差不多,都是通过修改配置来绕过安全策略。


作者:WhaleFE
来源:juejin.cn/post/7202252704978026551
收起阅读 »

从输入 URL 到页面显示,这中间发生了什么?

web
前言从输入 URL 到页面显示的发生过程,这是一个在面试中经常会被问到的问题,此问题能比较全面地考察应聘者知识的掌握程度。其中涉及到了网络、操作系统、Web 等一系列的知识。以 Chrome 浏览器为例,目前的 Chrome 浏览器有以下几个进程:浏览器进程。...
继续阅读 »

前言

从输入 URL 到页面显示的发生过程,这是一个在面试中经常会被问到的问题,此问题能比较全面地考察应聘者知识的掌握程度。其中涉及到了网络、操作系统、Web 等一系列的知识。

以 Chrome 浏览器为例,目前的 Chrome 浏览器有以下几个进程:

浏览器进程。主要负责界面显示、用户交互、子进程管理,同时提供存储等功能。

渲染进程。主要职责是把从网络下载的 HTML、JavaScript、CSS、图片等资源解析为可以显示和交互的页面。

GPU 进程。其实,Chrome 刚开始发布的时候是没有 GPU 进程的。而 GPU 的使用初衷是为了实现 3D CSS 的效果,只是随后网页、Chrome 的 UI 界面都选择采用 GPU 来绘制,这使得 GPU 成为浏览器普遍的需求。

网络进程。主要负责页面的网络资源加载,之前是作为一个模块运行在浏览器进程里面的,后面才独立出来,成为一个单独的进程。

插件进程。主要是负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响。

1. 用户输入

如果输入的是内容,地址栏会使用浏览器默认的搜索引擎,来合成新的带搜索关键字的 URL。

如果输入的是 URL,那么地址栏会根据规则,把这段内容加上协议,合成为完整的 URL。

2. URL 请求过程

浏览器进程会通过进程间通信(IPC)把 URL 请求发送至网络进程,网络进程接收到 URL 请求后,会在这里发起真正的 URL 请求流程。那具体流程是怎样的呢?

网络进程会查找本地缓存是否缓存了该资源。如果有缓存资源,那么直接返回资源给浏览器进程;如果在缓存中没有查找到资源,那么直接进入网络请求流程。这请求前的第一步是要进行 DNS 解析,以获取请求域名的服务器 IP 地址。如果请求协议是 HTTPS,那么还需要建立 TLS 连接。

接下来就是利用 IP 地址和服务器建立 TCP 连接 (三次握手)。连接建立之后,浏览器端会构建请求行、请求头等信息,并把和该域名相关的 cookie 等数据附加到请求头中,然后向服务器发送构建的请求信息。

服务器接收到请求信息后,会根据请求信息生成响应数据(包括响应行、响应头和响应体等信息),并发给网络进程。等网络进程接收了响应行和响应头之后,就开始解析响应头的内容了。

Content-Type 是 HTTP 头中一个非常重要的字段, 它告诉浏览器服务器返回的响应体数据是什么类型,然后浏览器会根据 Content-Type 的值来决定如何显示响应体的内容。

如果 Content-Type 字段的值被浏览器判断为下载类型,那么该请求会被提交给浏览器的下载管理器,同时该 URL 请求的导航流程就此结束。但如果是 HTML,那么浏览器则会继续进行导航流程。

3. 准备渲染进程

如果协议根域名相同,则属于同一站点。

但如果从一个页面打开了另一个新页面,而新页面和当前页面属于同一站点的话,那么新页面会复用父页面的渲染进程。

渲染进程准备好之后,还不能立即进入文档解析状态,因为此时的文档数据还在网络进程中,并没有提交给渲染进程,所以下一步就进入了提交文档阶段。

4. 提交文档

所谓提交文档,就是指浏览器进程网络进程接收到的 HTML 数据提交给渲染进程,具体流程是这样的:

首先当浏览器进程接收到网络进程的响应头数据之后,便向渲染进程发起“提交文档”的消息。

渲染进程接收到“提交文档”的消息后,会和网络进程建立传输数据的“管道”。

等文档数据传输完成之后,渲染进程会返回“确认提交”的消息给浏览器进程。

浏览器进程在收到“确认提交”的消息后,会更新浏览器界面状态,包括了安全状态、地址栏的 URL、前进后退的历史状态,并更新 Web 页面。

5. 渲染阶段

一旦文档被提交,渲染进程便开始页面解析和子资源加载。

渲染进程将 HTML 内容转换为能够读懂的 DOM 树结构。

渲染引擎将 CSS 样式表转化为浏览器可以理解的 styleSheets,计算出 DOM 节点的样式。

创建布局树,并计算元素的布局信息。

对布局树进行分层,并生成分层树

为每个图层生成绘制列表,并将其提交到合成线程。

合成线程将图层分成图块,并在光栅化线程池中将图块转换成位图

合成线程发送绘制图块命令 DrawQuad 给浏览器进程。

浏览器进程根据 DrawQuad 消息生成页面,并显示到显示器上。

最后

以上就是笔者对这一常考面试题的一些总结,对于其中的一些具体过程并没有详细地列举出来。如有不足欢迎大家在评论区指出......

作者:codinglin
来源:juejin.cn/post/7202602022355779644

收起阅读 »

被泼冷水后,谁能超越微服务?

历史总会重演。一切刚过去的,又会被重新提起。开源项目Codename One的联合创始人Shai,曾是Sun Microsystems开源LWUIT项目的共同作者,参与了无数开源项目。作为最早一批Java开发者,最近感慨道:单体,又回来了!Shai说道:我已经...
继续阅读 »

历史总会重演。一切刚过去的,又会被重新提起。开源项目Codename One的联合创始人Shai,曾是Sun Microsystems开源LWUIT项目的共同作者,参与了无数开源项目。作为最早一批Java开发者,最近感慨道:单体,又回来了!

Shai说道:我已经在这个圈子里很久时间了,看到了一次次被抛弃、被重新发现的想法,超越“时髦词汇”,并凯旋而归。

他进一步举例,“近年来,SQL也挣扎过后,死而复生。我们再次热爱关系数据库。我认为单体架构将再次迎来奇幻之旅。微服务和无服务器是云供应商推动的趋势,目的当然是在向我们兜售更多的云计算资源。然而对于大多数用例来说,微服务在财务上意义不大。是的,供应商当然也可以降低成本。但当他们扩大规模时,他们会以股息来覆盖掉成本。单是可观测性成本的增加,就让‘大型云’供应商的腰包鼓起来了!”

作为从业近30年的资深技术大神,为何做此感叹?本文通过一场“利用模块降低架构成本”的探讨,帮助大家梳理现在的架构设计难题,希望对诸君有所启发。

问题背景

我最近领导了一个会议小组,讨论了微服务与单体服务的主题。组内认为,单块的规模不如微服务。这对于亚马逊、eBay等所取代的那些庞然大物来说可能是正确的。这些确实是巨大的代码库,其中的每一次修改都是痛苦的,而且它们的扩展都是具有挑战性的。但这不是一个公平的比较。较新的方法通常优于旧的方法。但如果我们用更新的工具构建一个整体,我们会得到更好的可扩展性吗?它的局限性是什么?现代的单体(也称巨石)到底该是什么样子?

单体回归范例:Modulith

Spring Modulith是一个模块化的单体结构,可以让我们使用动态隔离件构建单体结构。通过这种方法,我们可以分离测试、开发、文档和依赖项。这有助于微服务开发的独立方面,而所涉及的开销很少。它消除了远程调用和功能复制(存储、身份验证等)的开销。

Spring Modulith不是基于Java平台模块化(Jigsaw)。他们在测试期间和运行时强制分离,这是一个常规的Spring Boot项目。它有一些额外的运行时功能,可以实现模块化的可观测性,但它主要是“最佳实践”的执行者。这种分离的价值超出了我们通常使用微服务的价值,但也有一些权衡。

举个例子,传统的Spring monolith将采用分层架构,其包如下:

com.debugagent.myapp
com.debugagent.myapp.services
com.debugagent.myapp.db
com.debugagent.myapp.rest

这很有价值,因为它可以帮助我们避免层之间的依赖关系;例如,DB层不应依赖于服务层。我们可以使用这样的模块,并有效地将依赖关系图推向一个方向:向下。但随着我们的成长,这没有多大意义。每一层都将充满业务逻辑类和数据库复杂性。

有了Modulith,我们的架构看起来更像这样:

com.debugagent.myapp.customers
com.debugagent.myapp.customers.services
com.debugagent.myapp.customers.db
com.debugagent.myapp.customers.rest

com.debugagent.myapp.invoicing
com.debugagent.myapp.invoicing.services
com.debugagent.myapp.invoicing.db
com.debugagent.myapp.invoicing.rest

com.debugagent.myapp.hr
com.debugagent.myapp.hr.services
com.debugagent.myapp.hr.db
com.debugagent.myapp.hr.rest

这看起来非常接近一个合适的微服务架构。我们根据业务逻辑分离了所有部分。在这里,可以更好地控制交叉依赖关系,团队可以专注于自己的孤立区域,而不必互相踩脚。这是微服务的价值之一,且没有开销。

我们可以使用注释进一步深入地和声明性地实现分离。我们可以定义哪个模块使用哪个并强制单向依赖关系,因此人力资源模块将与发票无关。客户模块也不会。我们可以在客户和发票之间建立单向关系,并使用事件进行反馈。Modulith中的事件是简单、快速和事务性的。它们消除了模块之间的依赖关系,无需麻烦。这可以用微服务实现,但很难实现。比如,开票需要向不同的模块公开接口。如何防止客户使用该界面?

有了模块,我们就可以做到。对用户可以更改代码并提供访问权限,但这需要经过代码审查,这会带来自己的问题。请注意,对于模块,我们仍然可以依赖常见的微服务,如功能标志、消息传递系统等。您可以在文档和Nicolas Fränkel的博客中阅读有关Spring Modulith的更多信息。

模块系统中的每个依赖项都被映射并记录在代码中。Spring实现包括使用方便的最新图表自动记录所有内容的能力。你可能会认为,依赖性是Terraform的原因。对于这样的“高级”设计来说,这是正确的地方吗?

对于Modulith部署,像Terraform这样的基础设施即代码(IaC)解决方案仍然存在,但它们会简单得多。问题是责任的划分。正如下图所展示,微服务并没有消除整体结构的复杂性。我们只把“难啃的骨头”踢给了DevOps团队。更糟糕的是,我们没有给他们正确的工具来理解这种复杂性,所以他们不得不从外部管理。


图源:Twitter

这就是为什么我们行业的基础设施成本在上升,而传统行业的基础设施价格却在下降。当DevOps团队遇到问题时,他们会投入资源。这显然不是正确的做法。

其他模块

我们可以使用标准Java平台模块(Jigsaw)来构建Spring Boot应用程序。这样做的好处是可以分解应用程序和标准Java语法,但有时可能会很尴尬。当使用外部库或将一些工作拆分为通用工具时,可能会更有效。

另一个选项是Maven中的模块系统。这个系统允许我们将构建分解为多个单独的项目。这是一个非常方便的过程,可以让我们省去大量项目的麻烦。每个项目都是独立的,易于使用。它可以使用自己的构建过程。然后,当我们构建主项目时,这些全部都变成了一个单体。在某种程度上,这才是我们真正想要的。

单体架构:扩展,有解吗

可以使用大多数微服务扩展工具来扩展我们的单体们。许多与扩展和集群相关的开发都是在单体架构的情况下进行的。这是一个更简单的过程,因为只有一个移动部分:应用程序。我们复制其他实例并观察它们。没有哪项服务是失败的。我们有细粒度的性能工具,所有的功能都可以作为一个统一的版本。

我认为扩展单体为微服务比直接构建微服务更简单——

  • 我们可以使用分析工具,并获得瓶颈的合理近似值。

  • 我们的团队可以轻松地(并且经济实惠地)设置运行测试的登台环境。

  • 我们拥有整个系统及其依赖关系的单一视图。

  • 我们可以单独测试单个模块并验证性能假设。

跟踪和可观测性工具非常棒。但它们也会影响生产,有时还会产生噪音。当我们试图解决伸缩瓶颈或性能问题时,这些工具可能会让设计者踩一些坑。

我们可以将Kubernetes与monolits一起使用,就像将其与微服务一起使用一样有效。镜像尺寸会更大(如果我们使用GraalVM这样的工具,则可能不会太大)。有了这一点,我们可以跨区域复制monolith ,并提供与微服务相同的故障转移行为。相当多的开发人员将monolics部署到Lambdas。笔者不太喜欢这种方法,因为非常昂贵。

单体的瓶颈问题:有解

但仍有一点是巨大的障碍:数据库。由于微服务固有地具有多个独立的数据库,因此它们实现了巨大的规模。单体架构通常与单个数据存储一起工作。这通常是应用程序的真正瓶颈。有多种方法可以扩展现代数据库。集群和分布式缓存是强大的工具,可以让我们达到在微服务架构中很难达到的性能水平。

在一个单体结构中,也并不需要单个数据库。例如:在使用Redis进行缓存时,选择使用SQL数据库也是很常见的事情。但我们也可以为时间序列或空间数据使用单独的数据库。我们也可以使用单独的数据库来提高性能,尽管根据笔者经验,这种情况从未发生过。将数据保存在同一数据库中的好处是巨大的。

回归单体的好处

事实上,这样做有一个惊人的好处,我们可以在不依赖“最终一致性”的情况下完成交易。当我们尝试调试和复制分布式系统时,可能会遇到一个很难在本地复制的过渡状态,甚至很难通过查看可观测性数据来完全理解。

原始性能消除了大量网络开销。通过适当调整的二级缓存,我们可以进一步删除80-90%的读IO。在微服务中,要实现这一点要困难得多,而且可能不会删除网络调用的开销。

正如我之前提到的,应用程序的复杂性在微服务架构中不会消失。我们只是把它搬到了另一个地方。所以从这个层面讲,微服务并不算真正的进步,因为在此过程中平白添加了许多移动部件,增加了整体复杂性。因此,回归更智能、更简单的统一架构更有意义。

再看微服务的卖点

编程语言的选择是微服务亲和力的首要指标之一。微服务的兴起与Python和JavaScript的兴起相关。这两种语言非常适合小型应用程序,对于较大型的应用就不太适用了。

Kubernetes使得扩展此类部署相对容易,因此为已经增长的趋势增添了动力。微服务也有一些相对快速的升降能力。这可以以更细粒度的方式控制成本。在这方面,微服务被出售给组织,作为降低成本的一种方式。

这并非完全没有优点。如果以前的服务器部署需要强大(昂贵)的服务器,那么这一论点可能有一定道理。这可能适用于极端使用的情况,比如:突然面临非常高的负载,但随后没有堵塞。在这些情况下,可以从托管的Kubernetes提供商动态(廉价)获取资源。

微服务的主要卖点之一是组织调度方面。这使得各个敏捷团队能够在不完全了解“大局”的情况下解决小问题。问题也在于此,这就会创造一种“单干”文化,让每个团队都“自己做自己的事情”。在缩减规模的过程中,尤其是在代码“腐烂”的情况下,问题更甚。系统可能仍能工作数年,但实际上无法维护。

互联网建立在单体之上为什么要离开呢

笔者组内中的一个共识是,我们应该始终从单体开始。它更容易构建,如果我们选择使用微服务,我们可以稍后将单体分解。

提及具体某个软件相关的复杂性,我们讨论单个模块而不是单个应用程序要更有意义些。二者在资源使用和财务浪费上的差异是巨大的。在这个追求“降本”的时代,为什么人们还要不知变通地默认构建微服务,而不是动态的模块化单体架构呢?

我们可以从这两大“架构阵营”学到很多东西。诚然,微服务为亚马逊创造了奇迹。但公平地说,他们的云成本已包含在这个奇迹之中。所以,一位的搞微服务教条肯定是有问题的。

另一方面,互联网是建立在单体之上的。它们中的大多数都不是模块化的。两者都有普遍适用的技术。因此,笔者看来,正确的选择是构建一个模块化的单体结构,先搭建好合适的身份验证基础设施,如果我们想在未来转向微服务,我们可以利用这些基础设施来进行解构拆分。

后记

在设计应用时,我们目前更多是面临“二选一”的架构选择:单体和微服务。它们二者通常被视为相反的方法。

在小型系统演进过程中,有这样一个不争的事实:单体应用程序往往会随着时间的推移而在架构上降级,即使在其生命周期开始阶段就定义其为架构。随着时间的推移,各种架构的禁止事项会不知不觉地进入项目,久而久之,系统变得更难改变,进化性受到影响。

另一方面,微服务提供了更强的分离手段,但同时也带来了许多复杂性,因为即使对于小型应用程序,团队也必须应对分布式系统的挑战。

单体回归,也是具体的有条件的回归。我们看到,趋势的改变,代表着某段时期具体任务或者目标正在变化。出于目标的变化,我们对于微服务和单体架构的二选一的选择问题,也不能再教条式的看待。

事物往往都在螺旋式的演进,对于架构而言,亦如是。

来源:mp.weixin.qq.com/s/2peN_MezvkR9UwtalhG6kA

原文链接:dzone.com/articles/is-it-time-to-go-back-to-the-monolith

收起阅读 »

我尽然突然焦虑,并且迷茫了

最近是怎么了 最近几个朋友,突然询问我,现在应该怎么学习,将来才会更好的找工作,怕毕业以后没有饭吃,我说我其实也不太清楚,我目前三段实习我都没有找到一份真正意义的好工作,就是那种我喜欢这门领域,并且喜欢公司的氛围,并且到老了还能保持竞争力(莫有35岁危机)。 ...
继续阅读 »

最近是怎么了


最近几个朋友,突然询问我,现在应该怎么学习,将来才会更好的找工作,怕毕业以后没有饭吃,我说我其实也不太清楚,我目前三段实习我都没有找到一份真正意义的好工作,就是那种我喜欢这门领域,并且喜欢公司的氛围,并且到老了还能保持竞争力(莫有35岁危机)。



所以说我真的没有一个准确的答案回复。但是我以为目前的眼光来看一份好工作必备的条件就是,我在这个领域学的越多,我的工资和个人发展瓶颈越高,这份工作是一个持续学习的过程,并且回报和提高是肉眼可见的!



回忆那个时候


其实说实话,这个疑惑我上大一就开始有,但是那个时候是从高考的失落中寻找升学的路径,开始无脑的刷那种考研短视频



(看过可能都知道真的一下子励志的心就有了,但是回到现实生活中,看到身边人的状态~~~没错人就是一个从众的种群,你可能会问你会不会因为大一没有那么努力学习而后悔,但是其实我不会,因为那一年我的经历也是我最开心大学生活,虽然也干了很多被室友做成梗的糗事,但是想一想那不就是青春嘛,要是从小就会很有尺度的为人处世,想一想活着也很累嘛,害,浅浅致敬一下充满快乐和遗憾的青春呀!)


个人看法


哈哈,跑题了。给大家点力量把!前面满满的焦虑。其实我感觉我们都应该感谢我们来到计算机类的专业,从事这方面的学习和研究。


因为计算机的扩展性,不得不说各行各业都开始越来越喜欢我们计算机毕业的大学生(就业方向更加广),我也因为自己会计算机,成功进入一个一本高校以上的教育类公司实习(同时也是这个时候知道了更多优秀学校的毕业年轻人,真正认识到学校的层次带给人的很多东西真正的有差距);



虽然我是二本的学生,但是在亲戚朋友眼里,虽然学校比不上他们的孩子,但是计算机专业也能获得浅浅的也是唯一一点可以骄傲的东西(活在别人嘴这种思考方式肯定不是对的,但是现实就是在父母那里,我们考上什么大学和进入了哪里工作真的是他们在外人的脸面,这种比较情况在大家族或者说农村尤为严重);



技术论打败学校论,计算机专业是在“广义”上为数不多能打破学校出身论的学科,在公司上只要你能干活,公司就愿意要你,这个时候肯定有人diss我,现在培训班出来的很多都找不到工作呀,我的回答只能是:的确,因为这个行业的红利期展示达到了瓶颈期,加上大环境的不理想,会受到一些影响,但是我还是相信会好的,一切都会好的。



做技术既然这样了


关于最近论坛上说“前段已死”“后端当牛做马”“公司磨刀霍霍向测试”......



这个东西怎么说,我想大部分人看到这个都会被这个方向劝退,我从两个角度分析一下,上面说了,真滴卷,简历真滴多,存在过饱和;第二点,希望这个领域新人就不要来了,就是直接劝退,被让人来卷,狭义上少卷一些......



现在就是导致我也不敢给朋友做建议了,因为当他看到这些的时候,和进入工作环境真的不好,我真的怕被喷死


包括现在我的实习,大家看我的朋友圈看出工作环境不错很好,但是和工作的另一面,是不能发的呀,有时候我都笑称自己是“产业工人”(这个词是一个朋友调侃我的)


不行了,在传播焦虑思想,我该被喷死了,现在我给建议都变得很含蓄,因为时代红利期真的看不透,我也不敢说能维持多少年,而且我工作也一般,我不敢耽误大家(哈哈哈,突然想起一句话,一生清贫怎敢入繁华,二袖清风怎敢误佳人,又是emo小文案,都给我开E)


个人总结


本文就是调侃一下现在的环境啊,下面才是重点,只有干活和真话放在后面(印证一个道理:看到最后的才是真朋友才敢给真建议,我也不怕被骂)



心态方面:我们这个年纪就是迷茫的年纪,迷茫是一种正常的状态,因为作为一名成年人你真正在思考你的个人发展的状态,所以请把心放大,放轻松,你迷茫了已经比身边的人强太多了,如果真正焦虑的不能去学习了,去找个朋友聊一聊,实在不行,drink个两三瓶,好好睡一觉,第二天继续干,这摸想,这些都算个啥,没事你还有我,实在不行微我聊一聊,我永远都在,我的朋友!



工作方面:俗话说:女怕入错行,男怕娶错人!(突然发现引用没什么用,哈哈)我们可以多去实践,没错就是去实习,比如你想做前端的工作,你就可以直接去所在的城市(推荐省会去找实习)但是朋友其实实习很难,作为过来人,我能理解你,一个人在陌生的城市而且薪资很可怜,面对大城市的租房和吃饭有很多大坑,你要一一面对,但是在外面我们真要学会保护自己,而且实习生活中经济方面肯定要父母支持,所以一定要和父母好好沟通,其实你会发现我们越长大,和父母相处的时光越短。(我今年小年和十五都没在家过,害,那种心理苦的滋味很不好受)



升学方面:不是每一个都适合考研,不要盲从考研。但是这句话又是矛盾的,在我的实习生涯中,学历问题是一个很重要的问题,我们的工作类型真的不同,还是那句话,学历只是一个门槛,只要你迈入以后看的是你的个人能力。说一句悄悄话,我每天工作,最想的事情就是上学,心想老子考上研,不在干这活了,比你们都强。所以你要想考研,请此刻拿出你的笔,在纸上写下你要考研主要三个理由,你会更好的认识自己,更好选择。



好吧,今天的随想录就这摸多,只是对最近看文章有了灵感写下自己的看法,仅供参考哦!


回答问题


回应个问题:很多朋友问我为什么给这摸无私的建议,这是你经历了很多才得到的,要是分享出去,不是很亏?


(你要这摸问,的确你有卷到我的可能性,快给我爬。哈哈哈)可能是博客圈给的思想把,其实我说不上开源的思想,但是我遇到的人对我都是无私分享自己的经验和自己走过的坑,就是你懂吗,他们对我帮助都很大,他们在我眼里就是伟大的人,所以我也想要跟随他们,做追光的人!(上价值了哦,哈哈)



写在最后


最后一句话,迷茫这个东西,走着走着就清晰了,迷茫的时候,搞一点学习总是没错的。


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

祖传代码重构:从25万行到5万行的血泪史

背景 一、接手 7 月份组织架构调整后,我们组接手了搜索链路中的 Query 理解基础模块,包括本次重构对象 Query Optimizer,负责 query 的分词、词权、紧密度、意图识别。 二、为什么重构 面对一份 10年+ 历史包袱较重的代码,大多数开发...
继续阅读 »

背景


一、接手


7 月份组织架构调整后,我们组接手了搜索链路中的 Query 理解基础模块,包括本次重构对象 Query Optimizer,负责 query 的分词、词权、紧密度、意图识别。


二、为什么重构


面对一份 10年+ 历史包袱较重的代码,大多数开发者认为“老项目和人有一个能跑就行”,不愿意对其做较大的改动,而我们选择重构,主要有这些原因:


1.生产工具落后,无法使用现代 C++,多项监控和 TRACE 能力缺失


2.单进程内存消耗巨大——114G


3.服务不定期出现耗时毛刺


4.进程启动需要 18 分钟


5.研效低下,一个简单的功能需要开发 3 人天


基于上述原因,也缘于我们热爱挑战、勇于折腾,我们决定进行拆迁式的重构。


图片


编码实现


一、重写与复用


我们对老 QO 的代码做分析,综合考虑三个因素:是否在使用、是否Query理解功能、是否高频迭代,将代码拆分为四种处理类型:1、删除;2、lib库引入;3、子仓库引入;4、重写引入。


图片


二、整体架构


老服务代码架构堪称灾难,整体遵守“想到哪就写到哪,需要啥就拷贝啥”的设计原则,完全不考虑单一职责、接口隔离、最少知识、模块化、封装复用等。下图介绍老服务的抽象架构:


图片


请求进来先后执行 3 次分词:


1.不带标点符号的分词结果,用于后续紧密度词权算子的计算输入;


2.带标点符号的分词结果,用于后续基于规则的意图算子的计算输入;


3.不带标点符号的分词结果,用于最终结果 XML queryTokens 字段的输出。


1 和 3 的唯一区别,就是调用内核分词的代码位置不同。


下一个环节,请求 Query 分词时,分词接口中竟然包含了 RPC 请求下游 GPU 模型服务获取意图。这是此服务迭代最频繁的功能块,当想要实验模型调整、增减意图时,需要在 QO 仓库进行实验参数解析,将参数万里长征传递到 word_segmentor 仓库的分词接口里,再根据参数修改 RPC 意图调用逻辑。一个简单参数实验,要修改 2个仓库中的多个模块。设计上不符合模块内聚的设计原理,会造成霰弹式代码修改,影响迭代效率,又因为 Query 分词是处理链路中的耗时最长步骤,不必要的串行增加了服务耗时,可谓一举三失。


除此之外,老服务还有其他各类问题:多个函数超过一千行,圈复杂度破百,接口定义 50 多个参数并且毫无注释,代码满地随意拷贝,从以下 CodeCC 扫描结果可见一斑:


图片


图片


新的服务求追架构合理性,确保:


1.类和函数实现遵守单一职责原则,功能内聚;


2.接口设计符合最少知识原则,只传入所需数据;


3. 每个类、接口都附上功能注释,可读性高。


项目架构如下:


图片


CodeCC 扫描结果:


图片


三、核心实现


老服务的请求处理流程:


图片


老服务采用的是原始的线程池模型。服务启动时初始化 20 条线程,每条线程分别持有自身的分词和意图对象,监听任务池中的任务。服务接口收到请求则投入任务池,等待任意一条线程处理。单个请求的处理基本是串行执行,只少量并行处理了几类意图计算。


新服务中,我们实现了一套基于 tRPC Fiber 的简单 DAG 控制器:


1.用算子数初始化 FiberLatch,初始化算子任务间的依赖关系


2.StartFiberDetached 启动无依赖的算子任务,FiberLatch Wait 等待全部算子完成


3.算子任务完成时,FiberLatch -1 并更新此算子的后置算子的前置依赖数


4.计算前置依赖数规 0 的任务,StartFiberDetached 启动任务


通过 DAG 调度,新服务的请求处理流程如下,最大化的提升了算子并行度,优化服务耗时:


图片


图片


DIFF 抹平


完成功能模块迁移开发后,我们进入 DIFF 测试修复期,确保新老模块产出的结果一致。原本预计一周的 DIFF 修复,实际花费三周。解决掉逻辑错误、功能缺失、字典遗漏、依赖版本不一致等问题。如何才能更快的修复 DIFF,我们总结了几个方面:DIFF 对比工具、DIFF 定位方法、常见 DIFF 原因。


一、DIFF 比对工具


工欲善其事必先利其器,通过比对工具找出存在 DIFF 的字段,再针对性地解决。由于老服务对外接口使用 XML 协议,我们开发基于 XML 比对的 DIFF 工具,并根据排查时遇到的问题,为工具增加了一些个性选项:基于XML解析的DIFF工具。


我们根据排查时遇到的问题为工具增加了一些个性选项:


1.支持线程数量与 qps 设置(一些 DIFF 问题可能在多线程下才能复现);


2.支持单个 query 多轮比对(某些模块结果存在一定波动,譬如下游超时了或者每次计算浮点数都有一定差值,初期排查对每个query可重复请求 3-5 轮,任意一轮对上则认为无 DIFF ,待大块 DIFF 收敛后再执行单轮对比测试);


3.支持忽略浮点数漂移误差;


4.在统计结果中打印出存在 DIFF 的字段名、字段值、原始 query 以便排查、手动跟踪复现。


二、DIFF 定位方法


获取 DIFF 工具输出的统计结果后,接下来就是定位每个字段的 DIFF 原因。




  • 逻辑流梳理确认




梳理计算该字段的处理流,确认是否有缺少处理步骤。对流程的梳理也有利于下面的排查。




  • 对处理流的多阶段查看输入输出




一个字段的计算在处理流中一定是由多个阶段组成,检查各阶段的输入输出是否一致,以缩小排查范围,再针对性地到不一致的阶段排查细节。


例如原始的分词结果在 QO 上是调用分词库获得的,当发现最后返回的分词结果不一致时,首先查看该接口的输入与输出是否一致,如果输入输出都有 DIFF,那说明是请求处理逻辑有误,排查请求处理阶段;如果输出无 DIFF,但是最终结果有DIFF,那说明对结果的后处理中存在问题,再去排查后处理阶段。以此类推,采用二分法思想缩小排查范围,然后再到存在 DIFF 的阶段细致排查、检查代码。


查看 DIFF 常见有两种方式:日志打印比对, GDB 断点跟踪。采用日志打印的话,需要在新老服务同时加日志,发版启动服务,而老服务启动需要 18 分钟,排查效率较低。因此我们在排查过程中主要使用 GDB 深入到 so 库中打断点,对比变量值。


三、常见 DIFF 原因




  • 外部库的请求一致,输出不一致




这是很头疼的 case,明明调用外部库接口输入的请求与老模块是完全一致的,但是从接口获取到的结果却是不一致,这种情况可能有以下原因:


1.初始化问题:遗漏关键变量初始化、遗漏字典加载、加载的字典有误,都有可能会造成该类DIFF,因为外部库不一定会因为遗漏初始化而返回错误,甚至外部库的初始化函数加载错字典都不一定会返回 false,所以对于依赖文件数据这块需要细致检查,保证需要的初始化函数及对应字典都是正确的。


有时可能知道是初始化有问题,但找不到是哪里初始化有误,此时可以用 DIFF 的 query,深入到外部库的代码中去,新老两模块一起单步调试,看看结果从哪里开始出现偏差,再根据那附近的代码推测出可能原因。


2.环境依赖:外部库往往也会有很多依赖库,如果这些依赖库版本有 DIFF,也有可能会造成计算结果 DIFF。




  • 外部库的输出一致,处理后结果不一致




这种情况即是对结果的后处理存在问题,如果确认已有逻辑无误,那可能原因是老模块本地会有一些调整逻辑 或 屏蔽逻辑,把从外部库拿出来原始结果结合其他算子结果进行本地调整。例如老 QO 中的百科词权,它的原始值是分词库出的词权,结合老 QO 本地的老紧密度算子进行了 3 次结果调整才得到最终值。




  • 将老模块代码重写后输出不一致




重构过程中对大量的过时写法做重写,如果怀疑是重写导致的 DIFF,可以将原始函数替代掉重写的函数测一下,确认是重写函数带来的 DIFF 后,再细致排查,实在看不出可以在原始函数上一小块一小块的重写。




  • 请求输入不一致




可能原因包括:


1.缺少 query 预处理逻辑:例如 QO 输入分词库的 query 是将原始 query 的各短语经过空格分隔的,且去除了引号;


2.query 编码有误:例如 QO 输入分词库的 query 的编码流程经过了:utf16le → gb13080 → gchar_t (内部自定义类型) → utf16le → char16_t;


3.缺少接口请求参数。




  • 预期内的随机 DIFF




某些库/业务逻辑自身存在预期内的不稳定,譬如排序时未使用 stable_sort,数组元素分数一致时,不能保证两次计算得出的 Top1 是同一个元素。遇到 DIFF 率较低的字段,需根据最终结果的输入值,结果计算逻辑排除业务逻辑预期内的 DIFF。


图片


coredump 问题修复


在进行 DIFF 抹平测试时,我们的测试工具支持多线程并发请求测试,等于同时也在进行小规模稳定性测试。在这段期间,我们基本每天都能发现新的 coredump 问题,其中部分问题较为罕见。下面介绍我们遇到的一些典型 CASE。


一、栈内存被破坏,变量值随机异常


如第 2 章所述,分词库属于不涉及 RPC 且未来不迭代的模块,我们将其在 GCC 8.3.1 下编译成 so 引入。在稳定性测试时,进程会在此库的多个不同代码位置崩溃。没有修改一行代码挂载的 so,为什么老 QO 能稳定运行,而我们会花式 coredump?本质上是因为此代码历史上未重视编译告警,代码存在潜藏漏洞,升级 GCC 后才暴露出来,主要是如下两种漏洞:


1.定义了返回值的函数实际没有 return,栈内存数据异常。


2.sprintf 越界,栈内存数据异常。


排查这类问题时,需要综合上下文检查。以下图老 QO 代码为例:


图片


sprintf 将数字以 16 进制形式输出到 buf_1 ,输出内容占 8 个字节,加上 '\0' 实际需 9 个字节,但 buf_1 和 buf_2 都只申请了 8 个字节的空间,此处将栈内存破坏,栈上的变量 query_words 值就异常了。


异常的表现形式为,while 循环的第一轮,query_words 的数组大小是 x,下一轮 while 循环时,还没有 push 元素,数组大小就变成了 y,因内存被写坏,导致异常新增了 y - x 个不明物体。在后续逻辑中,只要访问到这几个异常元素,就会发生崩溃。


光盯着 query_words 数组,发现不了问题,因为数组的变幻直接不符合基本法。解决此类问题,需联系上下文分析,最好是将代码单独提取出来,在单元测试/本地客户端测试复现,缩小代码范围,可以更快定位问题。而当代码量较少,编译器的 warning 提示也会更加明显,辅助我们定位问题。


上段代码的编译器提示信息如下:(开启了 -Werror 编译选项)


图片


二、请求处理中使用了线程不安全的对象


在代码接手时,我们看到了老的分词模块“怪异”的初始化姿势:一部分数据模型的初始化函数定义为 static 接口,在服务启动时全局调用一次;另一部分则定义为类的 public 接口,每个处理线程中构造一个对象去初始化,为什么不统一定义为 static,在服务启动时进行初始化?每个线程都持有一个对象,不是会浪费内存吗?没有深究这些问题,我们也就错过了问题的答案:因为老的分词模块是线程不安全的,一个分词对象只能同时处理一个请求。


新服务的请求处理实现是,定义全局管理器,管理器内挂载一个唯一分词对象;请求进来后统一调用此分词对象执行分词接口。当 QPS 稍高,两个请求同时进入到线程不安全的函数内部时,就可能把内存数据写坏,进而发生 coredump。


为解决此问题,我们引入了 tRPC 内支持任务窃取的 MQ 线程池,利用 c++11 的 thread_local 特性,为线程池中的每个线程都创建线程私有的分词对象。请求进入后,往线程池内抛入分词任务,单个线程同时只处理一个请求,解决了线程安全问题。


三、tRPC 框架使用问题




  • 函数内局部变量较大 && v0.13.3 版 tRPC 无法正确设置栈大小




稳定性测试过程中,我们发现服务会概率性的 coredump 在老朋友分词 so 里,20 个字以内的 Query 可以稳定运行,超过 20 个字则有可能会崩溃,但老服务的 Query 最大长度是 40 个字。从代码来看,函数中根据 Query 长度定义了不同长度的字节数组,Query 越长,临时变量占据内存越大,那么可能是栈空间不足,引发的 coredump。


根据这个分析,我们首先尝试使用 ulimit -s 命令调整系统栈大小限制,毫无效果。经过在码客上搜寻,了解到 tRPC Fiber 模型有独立的 stack size 参数,我们又满怀希望的给框架配置加上了 fiber stack size 属性,然而还是毫无效果。


无计可施之下,我们将崩溃处相关的函数提取到本地,分别用纯粹客户端(不使用 tRPC), tRPC Future 模型, tRPC Fiber 模型承载这段代码逻辑,循环测试。结果只有 Fiber 模型的测试程序会崩溃,而 Future / 本地客户端的都可以稳定运行。


最后通过在码客咨询,得知我们选用的框架版本 Fiber Stack Size 设置功能恰好有问题,无法正确设置为业务配置值,升级版本后,问题解决。




  • Redis 连接池模式,不能同时使用一应一答和单向调用的接口




我们尝试打开结果缓存开关后,“惊喜”的发现新的 coredump,并且是 core 在了 tRPC 框架层。与 tRPC 框架开发同事协作排查,发现原因是 Redis 采取连接池模式连接时,不可同时使用一应一答接口和单向调用接口。而我们为了极致性能,在读取缓存执行 Get 命令时使用的是一应一答接口,在缓存更新执行 Set 命令时,采用的是单向调用方式,引发了 coredump。


快速解决此问题,我们将缓存更新执行 Set 命令也改为了应答调用,后续调优再改为异步 Detach 任务方式。


图片


重构效果


最终,我们的成果如下:


【DIFF】


- 算子功能结果无 DIFF


【性能】


- 平均耗时:优化 28.4% (13.01 ms -> 9.31 ms)


- P99 耗时:优化 16.7%(30ms -> 25ms)


- 吞吐率:优化 12%(728qps—>832qps)


【稳定性】


- 上游主调成功率从 99.7% 提升至 99.99% ,消除不定期的 P99 毛刺问题


- 服务启动速度从 18 分钟 优化至 5 分钟


- 可观察可跟踪性提升:建设服务主调监控,缓存命中率监控,支持 trace


- 规范研发流程:单元测试覆盖率从 0% 提升至 60%+,建设完整的 CICD 流程


【成本】


- 内存使用下降 40 G(114 GB -> 76 GB)


- CPU 使用率:基本持平


- 代码量:减少 80%(25 万行—> 5万行)


【研发效率】


- 需求 LeadTime 由 3 天降低至 1 天内


附-性能压测:


(1)不带cache:新 QO 优化平均耗时 26%(13.199ms->9.71ms),优化内存 32%(114.47G->76.7G),提高吞吐率 10%(695qps->775qps)


图片


(2)带cache:新 QO 优化平均耗时 28%(11.15ms->8.03ms),优化内存 33%(114G->76G),提高吞吐率 12%(728qps->832qps)


图片


腾讯工程师技术干货直达:


1.超强总结!GPU 渲染管线和硬件架构


2.从鹅厂实例出发!分析Go Channel底层原理


3.快收藏!最全GO语言实现设计模式【下】


4.如何成为优秀工程师之软技能篇


阅读原文


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

外包仔的自我救赎

为什么做外包仔? 开始是没得选 毕业的第三年,通过培训班转行Java,包装了两年经验。非科班出身又是半路出家,当时也不懂外包的概念,于是就糊里糊涂进了外包公司。第一家公司只干了三个多月就跑路了,一方面是工资太低(8K),另一方面是技术比较老旧(SSH)。第...
继续阅读 »

为什么做外包仔?



开始是没得选



毕业的第三年,通过培训班转行Java,包装了两年经验。非科班出身又是半路出家,当时也不懂外包的概念,于是就糊里糊涂进了外包公司。第一家公司只干了三个多月就跑路了,一方面是工资太低(8K),另一方面是技术比较老旧(SSH)。第二家公司也是外包,但是项目还不错(spring cloud),薪资也可以接受(12K)。



后来是给的多



做开发工作的第二年,跳槽时本来想着找一家自研公司,但是没忍住外包公司开的价格,一时脑热又进了外包,也就是现在这家大厂外包。薪资比较满意(18K),项目也很不错(toC业务,各种技术都有涉及)。


下定决心跳出外包



为什么要离开



干过外包的小伙伴们多多少少会有一些低人一等的感觉,说实话笔者自己也有。就感觉你虽然在大厂,但你是这里身份最低的存在,很多东西是需要权限才能接触到的。再者就是没有归属感,没有年会、没有团建、甚至不知道自己公司领导叫什么(只跟甲方主管和外包公司交付经理有接触)。



潜心修炼技术



在最近这个项目里确实学到了很多生产经验,自己写的接口也确实发生过线上故障,不再是单单的CRUD,也会参与一些接口性能的优化。对业务有了一定的的理解,技术上也有了一定的提升,大厂的开发流程、开发规范确实比较健全。



背诵八股文



三月份开始就在为跳槽做准备,先后学习了并发编程、spring源码、Mysql优化、JVM优化、RocketMQ以及分布式相关的内容(分布式缓存、分布式事务、分布式锁、分布式ID等)。学到后面居然又把前面的忘了


大环境行情和现状



大范围裁员



今年从金三银四开始,各大互联网公司就都在裁员,直到现在还有公司在裁员,说是互联网的寒冬也不为过。笔者所在的厂也是裁员的重灾区,包括笔者自己(做外包都会被优化,说是压缩预算)也遭重了,但是外包公司给换了另外一个项目组(从北京换到了杭州)。



招聘网站行情



笔者八月份先在北京投了一波简历(自研公司,外包不考虑了),三十多家公司只有一家公司给了回应(做了一道算法笔试题,然后说笔者占用内存太多就没有后续了),九月中旬又在杭州投了一波简历(也是只投自研),六十多家公司回复也是寥寥无几,甚至没约到面试(有大把的外包私聊在下,是被打上外包仔的标签了吗)。


如何度过这个寒冬



继续努力



工作之余(摸鱼的时候),笔者仍然坚持学习,今天不学习,明天变垃圾。虽然身在外包,但是笔者仍有一颗向往自研的心,仍然想把自己学到的技术运用到实际生产中(现在项目用到的技术都是甲方说了算,当然我也会思考哪些场景适合哪些技术)。



千万不要辞职



现在的项目组做的是内部业务,并发几乎没有,但是业务相对复杂。笔者只能继续狗着(简历还是接着投,期望降低一些),希望互联网的寒冬早日结束,希望笔者和正在找工作的小伙伴们早日找到心仪的公司(respect)。


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

大梦难觉,又是一年

这篇2022年的年终总结拖到了2023年开工第二周才开始动笔,当真是老拖延症了,哈哈哈。虽然新年已至,旧岁已除,但过去这一年的经历和感悟还是需要简单总结记录一下。(注意:以下内容无关技术,纯粹的个人随笔) 一、动荡的互联网,艰难的打工人   本来想要先分享一下...
继续阅读 »

这篇2022年的年终总结拖到了2023年开工第二周才开始动笔,当真是老拖延症了,哈哈哈。虽然新年已至,旧岁已除,但过去这一年的经历和感悟还是需要简单总结记录一下。(注意:以下内容无关技术,纯粹的个人随笔)


一、动荡的互联网,艰难的打工人


  本来想要先分享一下去年发生的一些事情作为开篇,但是想了想最大的事情其实还是席卷互联网的这场风暴,相比这场风暴来说其他事情就会显得不是那么重要,所以还是来谈一谈闲宇身处这场风暴时的一些体验。


  其实真要说这次互联网动荡的开始时间应该是在21年下半年,不过在2022年这场风暴变得更加强烈了。感谢万能的互联网,让相关不相关的人员都能深切地感受到这场风暴来得是多么的强烈,”寒冬“、”毕业“这些看起来就让人哆嗦的字眼遍布在了网络上的每一个角落。作为一个暂时挺过这场风暴的幸存者,我可以较为肯定地表示作为打工人的我们无人可以幸免,只是时间长短问题(个人观点,不喜勿喷)。说是幸存者是因为我所在的公司也进行了看起来规模不小的人员优化活动,一切来得那么突然、那么匆忙,当真是让人猝不及防。当时前脚正在和我讨论问题的小朋友(我带的组员)后脚就开始整理东西上交电脑,未写完文档、未提交的代码、未完成的工作一下子全部涌到我这里。虽然整个过程很匆忙,但是那天我们还是好好地完成了一次告别,并没有像歌里写的一样:“还没来得及说再见”。之后的时间里,作为留下来的幸存者,我接手着之前没有接触的业务,维护着未曾见过的代码和逻辑。每每遇到一个问题想要找个知情人的时候,一问基本上就是“人已经走了,我也不太清楚”,这就导致了为了解决这样一个问题,我需要不停去追溯和了解上下游整体的方案逻辑以及部分开发细节,工作量呈几何倍的增加。就是在这种极度残缺和混乱的情况下,我竟然摇摇晃晃地度过了人员优化后的困顿期,没有发生一次线上事故。说实话,真的很难,如果可以相同的经历还是不要让我体验第二次了(此处应该配一张”我真的太难了“,哈哈哈)。


  以上是作为一个幸存者的体验,很艰难也很痛苦,但是作为亲历者大概会比我更加艰难和痛苦吧(当然也有不少同学拿了满意的赔偿回家也是很开心的,祝福他们)。在之后的日子里,我也找过被优化的小朋友吃过一次饭,在吃饭时的闲聊中我一定程度上也能感受到那天收到这样一个通知时他内心的崩溃和难过。他说那天一起时人多还没有太大感受,但是到晚上夜深人静一个人独处时那种难过和迷茫的情绪涌上心头的感受,着实让人崩溃。听到他这样说,我只能无奈地张了张嘴,叹了一口气。不得不说,在这场风暴面前打工人显得是如此卑微和无奈。


二、内自省,改变思维模式


  在经历了这几年的互联网打工生活之后(尤其是经历去年的动荡之后),我时不时会进行一些反思,尝试改变自身在学习、工作和生活上的一些惯性思维。这些惯性思维的形成可能是来自于以往的所学所知,也有可能是来自于社会环境或者周边人群的公共认知。这些惯性思维在最开始时可以说是对我益处非常大,毕竟这些都是来自于前人或者他人的经验总结得来的,但是随着经历的越多,心中的疑惑和对职业生涯的不安也越来越多,这些惯性思维对于我的限制也变得越来越大。我开始反思这些惯性思维是否依然有效,依然能够支撑陪伴我走到最后。


专业技能只能满足工作需要吗?


  其实在一年前我就在思考一个问题,作为一个Web服务端开发的我所学所知只能运用于当前的工作当中吗?仔细思考一下,好像确实只能适用于当前的工作。虽然我能够吹牛皮说自己精熟常用中间件、数据库、企业开发框架,但是正经让我流畅地独立开发一个能够用于日常的小软件好像都做不到,比如去年我突发奇想想要开发一款小巧的Markdown编辑器(MacOS桌面应用程序)用于自己写博客使用,但是我发现作为一个Web服务端程序员,对于这样一个简单的应用程序都没有办法开发实现。我不由惊觉:我的知识体系中为什么缺失了这么一块拼图?


  仔细想一想,也不难理解。大部分服务端程序员的所知所学其实主要围绕一个核心两个基本点,即以工作为核心,坚持技术深度(内卷)八股文熟练度(面试) 两个基本点。对于技术的追捧已经完全迷住了我们的眼睛,完全忘记了更应该追寻的是如何完整、高效地完成一个让用户喜爱、满足用户需求的产品。当然,闲宇并不是说追求技术深度不重要,相反追求技术深度很重要,因为只有明白这些工具背后使用的技术和对应的原理,我们才能更好的使用这些工具,甚至在必要时刻改造对应的工具来适配特定的业务场景。但是该说不说,有些时候我们去拓展自身的技术深度其实是为了内卷而不是为了更好地使用工具(有时我就是这样的,哈哈哈),再者说你的程序写得计算比花还好看也没有办法像李白的诗一样流传千古的。讲到这里大家应该不难发现,我的所学所知其实都是围绕着工作来开展的,这就导致出现上面所说的专业知识只能满足工作的情况(前端工程师这种情况应该会少不少)。除了专业知识的限制,我们的思维模式创造力也同样都被限制在工作场景,超脱工作场景之外普通人的奇思妙想可能都比我们这些“专业领域人员”要多得多。这也导致了我们这个群体普遍存在一种职业生涯无法长久的认知和焦虑。


  在深刻反省之后,我开始尝试接触一些其他领域(前端、产品、设计等等)的知识,尝试将目光从单纯地聚焦工作改变为平等关注工作和生活,从生活中发现和尝试实现一些简单的需求(比如写一个天气预报通知的服务,同时还能针对一些突发天气进行播报)。此时我去学习对应技能和知识时会非常地愉悦,因为我知道这种学习不单纯是为了工作需要,而是能够服务于自身生活的,相信大家应该也能感受到这种将自己的所学所知用于生活的快乐的。


我是否只能成为一名软件工程师?


  这个问题实际上是针对前一个问题的拓展和延伸。在毕业之初,绝大部分人都选择了从事软件开发工程师,基本没有人回去思考是否有其他领域适合自己,因为我们有一种惯性思考:大家都是这么选的。感谢这一公共认知,至少毕业这几年我的工作相对顺遂稳定,薪资尚可。但是这同样带来一个比较严重的问题,对于自己的职业规划我们没有自己做过相对明确的思考,只是大家都是这么干的或者大家都是这么认为的我们就去做了。未来如何走?自己应该成为什么样的人?这些问题大概都没有时间去好好思考一下。可能有同学要说,我未来要成为技术专家、架构师,但是仔细思考一下,这些目标真的是我们自己定的吗?难道不是市面这些标准的程序员晋升模版已经为我们制定好的吗?好吧,可能你是真的就在最初就已经想要成为一名技术专家,但是在成为技术专家之外,你难道不想成为一些其他角色?


  至少对于我来说,经过这几年的技术学习和博客分享之后,我渐渐发现我热爱上了这种将自己所学所得分享给别人的快乐,可能在未来的某一天我会去兼职做一名讲师也说不定。除此以外,我也开始喜欢上了编写小作文的感受,说不定未来的某天我还会兼职成为一名三流网络写手,每天写着富婆爱上我的爽文也未可知。当然以上都是一些设想,未必会成为现实,但我们需要突破一些自己思维的限制,毕竟生活中唯一不变的就是变化,有时候走出舒适圈你会看到另外一片美好的天地。


三、人生路长,未来可期


  上面的这些思考完全是基于个人观点和内心的不安分,并非适用于所有人,不喜勿喷。当然之所以出现这些不安分的思考除了有互联网不景气和“35岁失业”魔咒这两柄达摩克斯之剑外,还有就是如果这一辈子都是按照固定道路走完大致确实有点无聊,花一点时间追求一下自己喜欢的事情,闯一闯、拼一拼,说不定能够看到一个不一样的风景。(当然,这个是我此时的想法,未来说不定就会打脸了,此处先标记一下,哈哈哈)


  诸位,人生路长,未来可期,可适当焦虑,但也要满怀希望勇往向前。


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

互联网大裁员原因分析

继谷歌、微软之后,Zoom、eBay、波音、戴尔加入最新一波“裁员潮”中。 2 月 7 日,美国在线会议平台 Zoom 宣布将裁减 1300 名员工,成为最新一家进行裁员的公司,大约 15% 的员工受到影响。 同日,总部位于亚特兰大的网络安全公司 Secure...
继续阅读 »

继谷歌、微软之后,Zoom、eBay、波音、戴尔加入最新一波“裁员潮”中。


2 月 7 日,美国在线会议平台 Zoom 宣布将裁减 1300 名员工,成为最新一家进行裁员的公司,大约 15% 的员工受到影响。


同日,总部位于亚特兰大的网络安全公司 Secureworks 在一份提交给美国证券交易委员会( SEC )的文件中宣布,将裁员 9%,因为该公司希望在“一些世界经济体处于不确定时期”减少开支。据数据供应商 PitchBook估计,该公司近 2500 名员工中约有 225 人将在此轮裁员中受到影响。


此外,电商公司eBay于2月7日在一份SEC文件中宣布,计划裁员500人,约占其员工总数的4%。据悉,受影响的员工将在未来24小时内得到通知。


2月6日,飞机制造商波音公司证实,今年计划在财务和人力资源部门裁减约 2000 个职位,不过该公司表示,将增加 1 万名员工,“重点是工程和制造”。


个人电脑制造商戴尔的母公司,总部位于美国德克萨斯州的戴尔科技,2 月 6 日在一份监管文件中表示,公司计划裁减约 5% 的员工。戴尔有大约 13.3 万名员工,在这个水平上,约 6650 名员工将在新一轮裁员中受到影响。


除了 Zoom、eBay、波音、戴尔 等公司,它们的科技同行早已经采取了同样的行动。


从去年 11 月开始,许多硅谷公司员工就开始增加了关注邮箱的频率,害怕着某封解除自己公司内网访问权限的邮件来临,在仅仅在 2022 年 11 月,裁员数字就达到了近 6 万人,而如果从 2022 开始计算,各家已经陆续裁员了近十五万人。



但本次裁员并非是因为营收的直接下降:事实上,硅谷各家 2022 年营收虽然略有下跌,但总体上仍然保持了平稳,甚至部分业务略有上涨,看起来并没有到「危急存亡之秋」,需要动刀进行大规模裁员,才能在寒冬中存活的程度。


相较于备受瞩目的裁员,一组来自美国政府的就业数据就显得比较有意思了。据美国劳工统计局 2 月 3 日公布的数据,美国失业率 1 月份降至 3.4%,为 1969 年 5 月以来最低。



美国 1 月份非农就业人数新增 51.7 万人,几乎是经济学家预期的三倍,即使最近主要在科技行业裁员,但建筑、酒店和医疗保健等行业带来了就业增长。


一方面是某些企业的大规模裁员,仅 1 月份就影响超过 10 万人;而另一方面,政府报告显示就业市场强劲。这样来看,美国的就业市场情况似乎有些矛盾。


2022 年 12 月初,多名B站员工在社交媒体上表示,B站开始了新一轮裁员,B端、漫画、直播、主站、Goods等部门均有涉及,整体裁员比例在30%左右。12月19日,小米大规模裁员的消息又有曝出,裁员涉及手机部、互联网部、中国部等多部门,个别部门裁员比例高达75%。12月20日,知乎又传裁员10%。


似乎全球的科技公司都在裁员,而我们想要讨论裁员的问题,肯定绕不开两个大方向:经济下行和人员问题。


下行环境


联合国一月发布的《2023年世界经济形势与展望》中指出,2022 年,一系列相互影响的严重冲击,包括新冠疫情、乌克兰战争及其引发的粮食和能源危机、通胀飙升、债务收紧以及气候紧急状况等,导致世界经济遭受重创。美国、欧盟等发达经济体增长势头明显减弱,全球其他经济体由此受到多重不利影响。与新冠疫情相关的反复封锁以及房地产市场的长期压力,延缓了中国的经济复苏进程。


在此背景下,2023 年世界经济增速预计将从 2022 年估计的 3.0% 下降至 1.9%。2024 年,由于部分不利因素将开始减弱,预计全球经济增速将适度回升至 2.7%。不过,这在很大程度上将取决于货币持续紧缩的速度和顺序、乌克兰战争的进程和后果以及供应链进一步中断的可能性。


在通货膨胀高企、激进的货币紧缩政策以及不确定性加剧的背景下,当前全球经济低迷,导致全球经济从新冠疫情的危机中复苏的步伐减缓,对部分发达国家和发展中国家均构成威胁,使其 2023 年可能面临衰退的前景。


2022 年,美国、欧盟等发达经济体增长势头明显减弱。报告预计,2023 年美国和欧盟的经济增速分别为 0.4% 和 0.2%,日本为 1.5%,英国和俄罗斯的经济则将分别出现 0.8% 和 2.9% 的负增长。


与此同时,全球金融状况趋紧,加之美元走强,加剧了发展中国家的财政和债务脆弱性。自 2021 年末以来,为抑制通胀压力、避免经济衰退,全球超过85%的央行纷纷收紧货币政策并上调利率。


报告指出,2022 年,全球通胀率约为 9%,创数十年来的新高。2023 年,全球通胀率预计将有所缓解,但仍将维持在 6.5% 的高水平。


据美国商务部经济分析局(BEA)统计,第二、三季度,美国私人投资分别衰退 14.1% 和 8.5%。加息不仅对美国企业活动产生抑制作用,而且成为美国经济复苏的最主要阻力。尤其是,非住宅类建筑物固定投资已连续六个季度衰退。预计 2023 年美国联邦基金利率将攀升至 4.6%,远远超过 2.5% 的中性利率水平,经济衰退风险陡增,驱动对利率敏感的金融、房地产和科技等行业采取裁员等必要紧缩措施。


发展上限


美国企业的业务增长和经营利润出现问题。据美国多家媒体报道,第三季度,谷歌利润率急剧下滑,Meta 等社交媒体的广告收入迅速降温,微软等其他科技企业业务增长也大幅放缓。自7月以来,美国服务业PMI已连续5个月陷入收缩区间,制造业 PMI 也于 11 月进入收缩区间。在美国经济前景和行业增长空间出现问题的背景下,部分行业采取裁员、紧缩开支等“准备过冬”计划也就在意料之中了。


2022年,在市值方面,作为中概股的代表阿里、腾讯、快手等很多企业的市值都跌了 50%,甚至70%、80%。在收入方面,BAT 已经停止增长几个季度了,阿里和腾讯为代表的企业已经开始负增长。在经济下行的背景下,向内开刀、降本增效成为企业生存的必然之举。除了裁员,收缩员工福利、业务调整,也是企业降本增效的举动之一。


如果说 2021 年的裁员,很多是由于业务受到冲击,比如字节跳动的教育业务,以及滴滴、美团等公司的社区团购项目。但到了 2022 年,更多企业裁员的背后是降本增效、去肥增肌。


全球宏观经济表现不佳,由产业资本泡沫引发的危机感传导到科技企业的经营层,科技企业不得不面对现实。科技行业处在重要的结构转型期。iPhone 的横空出世开创了一个移动互联网的新时代,而当下的科技巨头也都是移动互联网的大赢家。但十多年过去了,随着智能手机全球高普及率的完成,移动互联网的时代红利逐渐消失,也再没有划时代的创新和新的热点。


这两年整个移动互联网时代的赢家都在焦急地寻找新的创新增长点。比如谷歌和 Meta 多年来一直尝试投资新业务,如谷歌云、Web3.0等,但实际收入仍然依赖于广告业务,未能找到真正的新增长点。这使得其中一些公司容易受到持有突破性技术的初创公司影响。


科技企业倾力“烧钱”打造新赛道,但研发投入和预期产出始终不成正比,不得不进行战略性裁员。


我们这里以这两年爆火的元宇宙举例:


各大券商亦争相拥抱元宇宙,不仅元宇宙研究团队在迅速组建,元宇宙首席分析师也纷纷诞生。 2021 年下半年,短短半年内便有数百份关于元宇宙的专题研报披露。


可以说,在扎克伯格和Meta的带领下,全世界的大厂小厂都在跟着往元宇宙砸钱。


根据麦肯锡的计算,自2021年以来,全世界已经向虚拟世界投资了令人瞠目结舌的数字——1770亿美元。


但即使作为元宇宙领军的 Meta 现实实验室(Reality Labs)2022 年三季度收入 2.85 亿美元,运营亏损 36.7 亿美元,今年以来已累计亏损 94 亿美元,去年亏损超过 100亿 美元。显然,Meta 的元宇宙战略还未成为 Meta的机遇和新增长点。


虽然各 KOL 高举“元宇宙是未来”的大旗,依旧无法改写“元宇宙未至”的局面。刨除亟待解决的关键性技术问题,如何兼顾技术、成本与可行性,实现身临其境的体验,更是为之尚远。元宇宙还在遥远的未来。


早在 2021 年12 月底,人民日报等官方媒体曾多次下场,呼吁理性看待“元宇宙”。中央纪委网站发布的《元宇宙如何改写人类社会生活》提及“元宇宙”中可能会涉及资本操纵、舆论吹捧、经济风险等多项风险。就连春晚的小品中,“元宇宙”也成为“瞎忽悠”的代名词。


2022 年 2月18日,中国银保监会发布了《关于防范以“元宇宙”名义进行非法集资的风险提示》,并指出了四种常见的犯罪手法,包括编造虚假元宇宙投资项目、打着元宇宙区块链游戏旗号诈骗、恶意炒作元宇宙房地产圈钱、变相从事元宇宙虚拟币非法谋利。


2022 年 2月7日,英国《金融时报》报道称,随着《网络安全法案》逐步落实,元宇宙将会受到严格的英国监管,部分公司可能面临数十亿英镑的潜在罚款。


2022 年 2月6日,据今日俄罗斯电视台(RT)报道,俄罗斯监管机构正在研究对虚拟现实技术实施新限制的可能性,他们担心应用该技术可能会协助非法活动。


各个国家的法律监管的到来,使得元宇宙的泡沫迅速炸裂。无数的元宇宙公司迅速破产,例如白鹭科技从 H5 游戏引擎转型到元宇宙在泡沫破裂的情况下个人举债 4000 万,公司破产清算。


本质上来说如今互联网行业已经到了一个明显的发展瓶颈,大家吃的都是移动网络和智能手机的普及带来的红利。在新的设备和交互方式诞生前,大家都没了新故事可讲,过去的圈地跑马模式在这样的大环境下行不通了。


法律监管


过去十年时间,互联网世界的马太效应越来越明显。一方面,几大巨头们在各自领域打造了占据了主导份额的互联网平台,不断推出包罗万象的全生态产品与服务,牢牢吸引着绝大多数用户与数据。他们的财务业绩与股价市值急剧增长,苹果、谷歌、亚马逊的市值先后突破万亿甚至是两万亿美元。


而另一方面,诸多规模较小的互联网公司却面临着双重竞争劣势。他们不仅财力与体量都无法与网络巨头抗衡,还要在巨头们打造的平台上,按照巨头制定偏向自己的游戏规则,与包括巨头产品在内的诸多对手激烈竞争用户。


2020 年 10 月,在长达 16 个月的调查之后,美国众议院司法委员会发布了一份长达 449 页的科技反垄断调查报告,直指谷歌、苹果、Facebook、亚马逊四大科技巨头滥用市场支配地位、打压竞争者、阻碍创新,并损害消费者利益。


2020 年 10 月 20 日,美国司法部连同美国 11 个州的检察长向 Google 发起反垄断诉讼,指控其在搜索和搜索广告市场通过反竞争和排他性行为来非法维持垄断地位。


2021 年明尼苏达州民主党参议员艾米·克洛布查尔(Amy Klobuchar)和爱荷华州共和党参议员查克·格拉斯利(Chuck Grassley)共同提出的《美国创新与选择在线法案》和 《开放应用市场法案》旨在打击谷歌母公司 Alphabet、亚马逊、Facebook 母公司 Meta 和苹果公司等科技巨头的一些垄断行为,这将是互联网向公众开放近30年来的首次重要法案。


《美国创新与选择在线法案》的内容包括禁止占主导地位的平台滥用把关权,给予营产品服务特权,使竞争对手处于不利地位;禁止施行对小企业和消费者不利,有碍于竞争的行为,例如要求企业购买平台的商品或服务以获得在平台上的优先位置、滥用数据进行竞争、以及操纵搜索结果偏向自身等。


不公平地限制大平台内其他商业用户的产品、服务或业务与涵盖平台经营者自己经营的产品、服务或业务相竞争能力,从而严重损害涵盖平台中的竞争。


除了出于大平台安全或功能的需要,严重限制或阻碍平台用户卸载预装的软件应用程序,将大平台用户使用大平台经营者提供的产品或服务设置为默认或引导为默认设置。


《开放应用市场法案》针对“守门人”执行,预计将会在应用商店、定向广告、互联操作性,以及垄断并购等方面,对相应企业做出一系列规范要求。此外欧盟方面还曾透露,如“守门人”企业不遵守上述规则,将按其上一财政年度的全球总营业额对其处以“不低于 4%、但不超过20%”的罚款。法案允许应用程序侧载(在应用商店之外下载应用程序),旨在打破应用商店对应用程序的垄断能力,将对苹果、谷歌的应用商店商业模式产生重要影响。


大型科技公司们史无前例搁置竞争,并且很有默契地联合起来。他们和他们的贸易团体在两年内耗费大约 1 亿美元进行游说,超过了制药和国防等高支出行业。他们向政界人士捐赠了 500 多万美元,科技游说人士向负责捍卫民主党多数席位的政治行动委员会(PAC)捐赠了 100 多万美元。他们还向不需要披露资金来源的黑钱组织、非营利组织和行业协会投入了数百万美元。几位国会助手表示,他们收到的有关这些法案的宣传比他们多年来处理的任何其他法案都要多。


这两项法案已通过国会相关委员会的审查,依然在等待众议院和参议院的表决。而美国即将开始中期选举。Deese 称,共和党已经明确表示,如果共和党重新控制国会两院,他们将不会支持这些法案。但如果民主党当选的话,科技巨头们估计不好过了。


很遗憾的是,2023年,新一届美国国会开幕后,众议院议长的选举经多轮投票仍然“难产”,导致新一届国会众议院无法履职。开年的这一乱象凸显美国政治制度失灵与破产,警示美国党争极化的趋势恐正愈演愈烈;


欧盟也多次盯上四大公司,仅谷歌一家,欧盟近三年来对其开出的反垄断处罚的金额已累计超过 90 亿美元。


而中国的举措也不小。


2020 年年初,实施了近 12 年的《反垄断法》(2008 年 8 月 1 日生效)首次进入“大修”——国家市场监督管理总局在其官网公布了《反垄断法修订草案(公开征求意见稿)》(以下简称“征求意见稿”)。


《法制日报》报道指出,征求意见稿中共有 8 章 64 条,较现行法要多出 7 条。可见,这次修法,已与另立新法有同等规模。


值得注意的是,征求意见稿还首次将互联网业态纳入其中,新增互联网领域的反垄断条款,针对性地列明相关标准和适用规程。


以市场支配地位认定为例,征求意见稿根据互联网企业的特点,新增了包括网络效应、规模经济、锁定效应、掌握和处理相关数据的能力等因素。


11 月 10 日,赶在双 11 前一天,国家市场监管管理总局再次出手,发布了《关于平台经济领域的反垄断指南(征求意见稿)》(以下简称《指南》)公开征求意见的公告。


《指南》不仅对“互联网平台”做了进一步界定,还结合个案更为具体详尽地对垄断协议,滥用市场支配地位行为,经营者集中,滥用行政权力排除、限制竞争四个方面作出分析和规定。


国家在平台经济领域、反垄断领域的法律规范,在《反垄断指南》出台以后,已经有了相当程度的完善。后续随着《反垄断法》修正案的通过,二者结合基本构建了我国反垄断领域的法律框架。


随着《反垄断法》的完善,在互联网领域的处罚案例逐渐浮出水面,针对阿里巴巴、美团等互联网公司都开出了大额罚单。


2021年我国在网络安全方面也加速发展。2021年6月10日颁布《中华人民共和国数据安全法》,2021年8月20日颁布《中华人民共和国个人信息保护法》。有关部门相继出台了《网络安全审查办法》《常见类型移动互联网应用程序必要个人信息范围规定》《数据出境安全评估办法(征求意见稿)》等部门规章和政策性文件。


可以预见的是,未来监管部门的监管措施更能兼顾互联网行业发展特征和社会整体福利,监管部门会不断完善规章、政策文件和标准文件,提供给企业明确和细化的指引。同时,相关部门的监管反应速度会越来越及时,监管层面对违法查处的力度也会越来越严。


人口红利


我们依然处在人口规模巨大的惯性中,人口规模巨大意味着潜在市场规模巨大,伴随经济持续发展、收入水平提高、消费能力强劲,由此带来的超大市场规模不可估量。而现在人口红利没了。


中国国家统计局 1 月 17 日公布,2022年末全国人口(包括 31 个省、自治区、直辖市和现役军人的人口,不包括居住在 31 个省、自治区、直辖市的港澳台居民和外籍人员) 141175 万人,比上年末减少 85 万人。这是近61年来中国首次人口负增长。人口负增长的早期阶段是一种温和的人口减少,所以依然会沿袭人口规模巨大的惯性;但在人口负增长的远期阶段,如果生育率仍未有所回升的话,就有可能导致一种直线性的减少。


目前所有行业都不得不面临从人口红利转向素质红利的转变。


人员过剩


微软在过去两年员工数新增 6 万,Google 则是新增了 7 万,Meta 则是直接从疫情之前的 4 万翻倍至 2022 年的 8.7 万。而依赖物流服务的亚马逊则最为激进,两年时间全球全职员工数增长了令人咂舌的 8.1 万,全职员工数近乎翻倍。



高盛的经济学家在一份报告中指出“那些正在裁员的科技公司有一些共同点,希望重新平衡业务的结构性转变,并为更好的利润开路。我们发现,许多最近宣布大规模裁员的公司都有三个共同特征。首先,许多都是在科技领域。其次,许多公司在疫情期间大肆招聘。第三,它们的股价出现了更大幅度的下跌,从峰值平均下跌了 43%。”


平均而言,那些进行裁员的公司在疫情期间的员工数量增长了 41%,此举往往是因为他们过度推断了与疫情相关的趋势,比如商品需求或在线时间的增长。


行裁员的公司并不能代表更广泛的情况,最近许多裁员公告并不一定意味着需求状况会减弱。与此一致的是,高盛预计更具代表性的实时估计的裁员率最近虽有所增加,但仅恢复到疫情前的水平,以历史标准衡量,裁员率水平较低。


结论


全球经济下行是大势,层层增加的法律监管是推动,没有人口红利和新玩法股价要大跌。


全球通胀激增,激进的货币紧缩政策以及不确定性加剧、俄乌战争等影响,全球经济低迷。新冠疫情带来的影响难以快速恢复。而中国还得面临人口红利消失、房地产饮鸩止渴的深远影响。而法律的层层监管和反垄断的推进在逐步打压科技巨头的已有市场,没有新技术的突破和新玩法让科技巨头们也没了新增和突破的空间。对于未来的经济发展的错误预估和疫情特殊时期的大量增长让科技巨头们大肆招聘,这些都成为了股价下跌和缩减利润的元凶。目前的大裁员可以算是一种虚假繁荣的泡沫爆裂后的回调,虽然不知道这个回调什么时候结束,但是随着人工智能的出圈和将来新技术的突破,也许整个行业可以浴火重生。


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

兔兔按钮——安卓悬浮按钮

前言 安卓的Material库提供了许多样式更精美的控件,其中就有悬浮控件,它表现出一种悬浮在页面的效果,也就是有立体效果的,让人产生这种控件是另一个维度而不是这个维度的感觉,下面我们就来看看兔兔按钮。 正篇 实现过程 首先我们在布局中加上我们的Floatin...
继续阅读 »

前言


安卓的Material库提供了许多样式更精美的控件,其中就有悬浮控件,它表现出一种悬浮在页面的效果,也就是有立体效果的,让人产生这种控件是另一个维度而不是这个维度的感觉,下面我们就来看看兔兔按钮。


正篇


实现过程


首先我们在布局中加上我们的FloatingActionButton控件:


<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:src="@drawable/rabbit_logo" />

这样预览页面就会出现一个悬浮的圆形按钮,上面是我们的兔兔图案,而且这个控件是出现在屏幕的右下角,接着我们为其增加点击效果:


binding.fab.setOnClickListener { 
"FAB Rabbit!".showToast(context)
}

其中Toast用到了我们之前文章中的简化方法(安卓开发基础——简化Toast调用方法 - 掘金 (juejin.cn)


运行程序后,我们就可以点击这个兔兔按钮,浮现一句"FAB Rabbit!"的提示。


写的过程很简单,因为其实它的本质还是Button,不过是对其样式进行了不同的改变,有了质感和阴影,使其呈现出浮现的效果。


image.png


当然,我们也可以去改变阴影效果的呈现程度:


app:elevation="8dp"

我们在XML布局中该控件控制elevation属性,就能为FloatingActionButton指定一个高度,其中,高度值越大,投影范围越大,但投影效果越淡,而高度越小,投影范围越小,反而投影效果越浓。


总结


Material库的确让安卓很多控件效果不一样,但在我们工作设计中还是很少去用它的,因为它的独特效果在公司自己的UI设计师与产品眼中说不定最终还不如和IOS一致好。


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

Android深思如何防止快速点击

前言 其实快速点击是个很好解决的问题,但是如何优雅的去解决确是一个难题,本文主要是记录一些本人通过解决快速点击的过程中脑海里浮现的一些对这个问题的深思。 1. AOP 可以通过AOP来解决这个问题,而且AOP解决的方法也很优雅,在开源上也应该是能找到对应的成熟...
继续阅读 »

前言


其实快速点击是个很好解决的问题,但是如何优雅的去解决确是一个难题,本文主要是记录一些本人通过解决快速点击的过程中脑海里浮现的一些对这个问题的深思。


1. AOP


可以通过AOP来解决这个问题,而且AOP解决的方法也很优雅,在开源上也应该是能找到对应的成熟框架。


AOP来解决这类问题其实是近些年一个比较好的思路,包括比如像数据打点,通过AOP去处理,也能得到一个比较优雅的效果。牛逼的人甚至可以不用别人写的框架,自己去封装就行,我因为对这个技术栈不熟,这里就不献丑了。

总之,如果你想快速又简单的处理这种问题,AOP是一个很好的方案


2. kotlin


使用kotlin的朋友有福了,kotlin中有个概念是扩展函数,使用扩展函数去封装放快速点击的操作逻辑,也能很快的实现这个效果。它的好处就是突出两个字“方便”


那是不是我用java,不用kotlin就实现不了kotlin这个扩展函数的效果?当然不是了。这让我想到一件事,我也有去看这类问题的文章,看看有没有哪个大神有比较好的思路,然后我注意到有人就说用扩展函数就行,不用这么麻烦。


OK,那扩展函数是什么?它的原理是什么?不就是静态类去套一层吗?那用java当然能实现,为什么别人用java去封装这套逻辑就是麻烦呢?代码不都是一样,只不过kotlin帮你做了而已。所以我觉得kotlin的扩展函数效果是方便,但从整体的解决思路上看,缺少点优雅。


3. 流


简单来说也有很多人用了Rxjava或者kotlin的flow去实现,像这种实现也就是能方便而已,在底层上并没有什么实质性的突破,所以就不多说了,说白了就是和上面一样。


4. 通过拦截


因为上面已经说了kt的情况,所以接下来的相关代码都会用java来实现。

通过拦截来达到防止快速点击的效果,而拦截我想到有2种方式,第一种是拦截事件,就是基于事件分发机制去实现,第二种是拦截方法。

相对而言,其实我觉得拦截方法会更加安全,举个场景,假如你有个页面,然后页面正在到计算,到计算完之后会显示一个按钮,点击后弹出一个对话框。然后过了许久,改需求了,改成到计算完之后自动弹出对话框。但是你之前的点击按钮弹出对话框的操作还需要保留。那就会有可能因为某些操作导致到计算完的一瞬间先显示按钮,这时你以迅雷不及掩耳的速度点它,那就弹出两次对话框。


(1)拦截事件


其实就是给事件加个判断,判断两次点击的时间如果在某个范围就不触发,这可能是大部分人会用的方式。


正常情况下我们是无法去入侵事件分发机制的,只能使用它提供的方法去操作,比如我们没办法在外部影响dispatchTouchEvent这些方法。当然不正常的情况下也许可以,你可以尝试往hook的方向去思考能不能实现,我这边就不思考这种情况了。


public class FastClickHelper {

private static long beforeTime = 0;
private static Map<View, View.OnClickListener> map = new HashMap<>();

public static void setOnClickListener(View view, View.OnClickListener onClickListener) {
map.put(view, onClickListener);
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
long clickTime = SystemClock.elapsedRealtime();
if (beforeTime != 0 && clickTime - beforeTime < 1000) {
return;
}
beforeTime = clickTime;

View.OnClickListener relListener = map.get(v);
if (relListener != null) {
relListener.onClick(v);
}
}
});
}

}

简单来写就是这样,其实这个就和上面说的kt的扩展函数差不多。调用的时候就


FastClickHelper.setOnClickListener(view, this);

但是能看出这个只是针对单个view去配置,如果我们想其实页面所有view都要放快速点击,只不过某个view需要快速点击,比如抢东西类型的,那肯定不能防。所以给每个view单独去配置就很麻烦,没关系,我们可以优化一下


public class FastClickHelper {

private Map<View, Integer> map;
private HandlerThread mThread;

public void init(ViewGroup viewGroup) {
map = new ConcurrentHashMap<>();
initThread();
loopAddView(viewGroup);

for (View v : map.keySet()) {
v.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
int state = map.get(v);
if (state == 1) {
return true;
} else {
map.put(v, 1);
block(v);
}
}
return false;
}
});
}
}

private void initThread() {
mThread = new HandlerThread("LAZY_CLOCK");
mThread.start();
}

private void block(View v) {
// 切条线程处理
Handler handler = new Handler(mThread.getLooper());
handler.postDelayed(new Runnable() {
@Override
public void run() {
if (map != null) {
map.put(v, 0);
}
}
}, 1000);
}

private void exclude(View... views) {
for (View view : views) {
map.remove(view);
}
}

private void loopAddView(ViewGroup viewGroup) {
for (int i = 0; i < viewGroup.getChildCount(); i++) {
if (viewGroup.getChildAt(i) instanceof ViewGroup) {
ViewGroup vg = (ViewGroup) viewGroup.getChildAt(i);
map.put(vg, 0);
loopAddView(vg);
} else {
map.put(viewGroup.getChildAt(i), 0);
}
}
}

public void onDestroy() {
try {
map.clear();
map = null;
mThread.interrupt();
} catch (Exception e) {
e.printStackTrace();
}
}

}

我把viewgroup当成入参,然后给它的所有子view都设置,因为onclicklistener比较常用,所以改成了设置setOnTouchListener,当然外部如果给view设置了setOnTouchListener去覆盖我这的set,那就只能自己做特殊处理了。


在外部直接调用


FastClickHelper fastClickHelper = new FastClickHelper();
fastClickHelper.init((ViewGroup) getWindow().getDecorView());

如果要想让某个view不要限制快速点击的话,就调用exclude方法。这里要注意使用完之后释放资源,要调用onDestroy方法释放资源。


关于这个部分的思考,其实上面的大家都会,也基本是这样去限制,但是就是即便我用第二种代码,也要每个页面都调用一次,而且看起来,多少差点优雅。


首先我想的办法是在事件分发下发的过程去做处理,就是在viewgroup的dispatchTouchEvent或者onInterceptTouchEvent这类方法里面,但是我简单看了源码是没有提供方法出来的,也没有比较好去hook的地方,所以只能暂时放弃思考在这个下发流程去做手脚。


补充一下,如果你是自定义view,那肯定不会烦恼这个问题,但是你总不能所有的view都做成自定义的吧。


其次我想怎么能通过不写逻辑代码能实现这个效果,但总觉得这个方向不就是AOP吗,或者不是通过开发层面,在开发结束后想办法去注入字节码等操作,我觉得要往这个方向思考的话,最终的实现肯定不是代码层面去实现的。


(2)拦截方法


上面也说了,相对于拦截事件,假设如果都能实现的情况下,我更倾向于去拦截方法。


因为从这层面上来说,如果实现拦截方法,或者说能实现中断方法,那就不只是能做到防快速点击,而是能给方法去定制相对应的规则,比如某个方法在1秒的间隔内只能调用一次,这个就是防快速点击的效果嘛,比如某个方法我限制只能调一次,如果能实现,我就不用再额外写判断这个方法调用一次过后我设置一个布尔类型,然后下次调用再判断这个布尔类型来决定是否调用,


那现在是没办法实现拦截方法吗?当然有办法,只不过会十分的不优雅,比如一个方法是这样的。


public void fun(){
// todo 第1步
// todo 第2步
// todo ......
// todo 第n步
}

那我可以封装一个类,里面去封装一些策略,然后根据策略再去决定方法要不要执行这些步骤,那可能就会写成


public void fun(){
new FunctionStrategy(FunctionStrategy.ONLY_ONE, new CallBack{
@Override
public void onAction() {
// todo 第1步
// todo 第2步
// todo ......
// todo 第n步
}
})
}

这样就实现了,比如只调用一次,具体的只调用一次的逻辑就写在FunctionStrategy里面,然后第2次,第n次就不会回调。当然我这是随便乱下来表达这个思路,现实肯定不能这样写。首先这样写就很不优雅,其次也会存在很多问题,扩展性也很差。


那在代码层面还有其它办法拦截或者中断方法吗,在代码层还真有办法中断方法,没错,那就是抛异常,但是话说回来,你也不可能在每个地方都try-catch吧,不切实际。


目前对拦截方法或者中断方法,我是没想到什么好的思路了,但是我觉得如果能实现,对防止快速点击来说,肯定会是一个很好的方案。


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

KMP—仅需一套代码,使用kotlin也能一站式搭建android, 桌面端,和web端app!

截止上周(本文写于2023.02.07),JetBrains推出Compose跨平台已经发布了1.3.0版本,可以说是很稳定了。很明显这也是跨平台UI的一个很好的方案。 如果你还不了解Compose Multiplatform是什么, 也可以直接参考官网的...
继续阅读 »

截止上周(本文写于2023.02.07),JetBrains推出Compose跨平台已经发布了1.3.0版本,可以说是很稳定了。很明显这也是跨平台UI的一个很好的方案。



Kotlin Multiplatform overall principle (source: [kotlinlang.org])


如果你还不了解Compose Multiplatform是什么, 也可以直接参考官网的JetBrains 网站的『长懒看』说明,一句话就是:



Fast reactive Desktop and Web UI framework for Kotlin,JetBrain公司基于Google的 先进工具套件compose,为开发者打造了一套快速响应的桌面端的web端 UI框架,可以完全使用kotlin开发。



因为和jetpack Compose绑定到一起了,相信大部分android 开发者一下子就明白:我们现在可以直接仅用kotlin就打造全平台跨平台的app了。


老哥,为啥不用flutter呢?有区别么?


其实二者还是有相当大的不同的。Kotlin跨平台技术(后文称KMP)和flutter相比,最主要的优势就是不用再学一个新的语言Dart了,直接用koltin就可以搞定,降低了学习成本。除此之外,我还发现了一个有趣的不同之处——他们处理跨平台架构的方案完全不同。


使用Flutter的时候,你需要先写好基础的 业务逻辑、UI逻辑,都只写一次,之后这些基础逻辑就能在不同的平台上直接运行了。你也可以继续写一些对不同平台的适配代码,来优化在特定平台运行的兼容性效果。但,无论你怎么写,真正运行到移动设备、桌面app或者是网页端的时候,你的程序还是由Flutter引擎(由Skia图像处理库构建的引擎)渲染出来而不是直接在操作系统层级渲染的。这就导致它的可移植性很好,但是UI效果并不好,和原生效果还是有些差距。


然而,使用KMP的话,你的业务逻辑还是只写一次,但是后面的UI界面,你需要使用kotlin对目标平台分别编写。虽然大多都可以使用kt语言,但是写法还是有区别的。比如,写android就需要用jetpack Compose框架,iOS就用swift写,桌面端就用compose Multiplatform写,等等。因此,你的app最终会有一个更接近原生的UI效果 —— 只是可移植性就差一些。


最重要的是,这两者提供了不同的方法,怎么用还是得看你的业务场景。


说实话,整体来说还是KMP听起来更好,信我!


理论到此为止!说了这么久,让你有一个初步的感受。但让我们暂时把理论放在一边,关注『怎么用』


新建一个Demo APP


还是得实操一下,要决定开发点什么东西,才能展示所有要了解的实战内容。 Flutter 也有“Hello World” 项目,从这找点儿灵感,制作一个计数器应用程序,允许用户递增和递减一个数值,并记录最新注册的操作是什么。


看起来够简单了吧


想要实现上图这个app,我们得决定好使用什么样的架构


选取架构


我们将使用 干净架构(MVVM),这是构建 GUI 应用程序(尤其是在 Android 上)的常用解决方案。 写Android的肯定是对这个架构老生常谈啦。如果你不太了解这个架构,而且感兴趣,也可以先关掉页面去研究一下~比如这个链接-Android干净架构教程


好了,到这就可以开始开工了!我们会用如下几个砖块,构建堆砌我们的app:


Domain:就是Model层,正常应该包括app的全部model,但是这个比较简单,只需要一个data class数据类。
Data:我们的抽象数据源,就是保存这个计数器app的数据的。
Use Cases:所有的用例类,就是:递增计数器、递减计数器并获取其值的方法。
Presentation:界面对应的viewModel,梳理页面操作逻辑。
Framework:数据源实现以及每个平台的用户界面。


注意:上述所有『砖块』,除了只有Framework里面的UI部分,都是可以跨平台复用的。


架构


开始写代码吧


好了,现在可以开始写代码了,我们用Intellij Idea作为示例IDE,如果你用其他惯用IDE也可以找到类似的操作方式。


先创建一个工程,从上面罗列出来的架构开始实现。一个一个类的慢慢写,直到写完全部的平台内容。 Idea这个IDE提供了一些预先构建好的KMP模型应用,我们可以直接使用。不过为了更好的学会内容,我们就先从头开始写吧。


打开IDEA,点击 File > New Project(我的是英文环境,中文类似)。


image.png


填上你自己的项目名字就可以运行了。


模块


创建好项目后,第一件事儿,就得创建一下不同的module: commonandroiddesktopweb。我们就从最基本的common开始写,写好了其他module也可以依赖它。


这时候直接在根目录右键,new module,选择compose MultiPlatform。直接就可以创建相关的模块


创建module


创建成功的效果如下:


创建完成的文件结构


修改根目录的gradle.properties文件如下:


kotlin.code.style=official
android.useAndroidX=true
kotlin.mpp.enableGranularSourceSetsMetadata=true
kotlin.native.enableDependencyPropagation=false
android.enableJetifier=true

这个时候编译应该会很漫长,可以先等待,稍后我们会开始创建数据源集合。


数据集


因为compose的模板已经创建好了相关文件夹,但是需要思考一个问题:


不是说common模块应该是跨平台的嘛?为什么还要在里面创建desktop和android?


可以假定一个虚构的使用场景:你正在开发一个跨平台的app,但是你也需要获取到一些,不同平台特有的API。比如:获取系统版本,连接到底层的日志系统,或者是生成随机的uuid等功能。


KMP还是允许一些简单的方式去获取上面这些底层架构功能的嗯。你可以像如下操作:


// Under Common
expect fun randomUUID(): String// Under Android
import java.util.*

actual fun randomUUID() = UUID.randomUUID().toString()// And so on for all other platforms

当然了,这些复杂的底层操作在我们的简单demo中并不会用到~


回到我们的项目中。


可以注意一下我们的compose跨平台module中的 settings.gradle.kts 的文件内容


pluginManagement {
repositories {
google()
gradlePluginPortal()
mavenCentral()
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
}

plugins {
kotlin("multiplatform").version(extra["kotlin.version"] as String)
kotlin("android").version(extra["kotlin.version"] as String)
id("com.android.application").version(extra["agp.version"] as String)
id("com.android.library").version(extra["agp.version"] as String)
id("org.jetbrains.compose").version(extra["compose.version"] as String)
}
}

rootProject.name = "composemultidemo"

include(":android", ":desktop", ":common")

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

vue中Axios添加拦截器刷新token的实现方法

web
vue中Axios添加拦截器刷新token的实现方法Axios是一款网络前端请求框架,本文主要介绍了vue中Axios添加拦截器刷新token的实现方法,1. Axios基本用法:        const respon...
继续阅读 »

vue中Axios添加拦截器刷新token的实现方法

Axios是一款网络前端请求框架,本文主要介绍了vue中Axios添加拦截器刷新token的实现方法,

1. Axios基本用法:

        const response = await Axios.create({
           baseURL: "https://test.api.com",
           headers: {
               'Content-Type': 'application/json',
          },
        }).post<RequestResponse>('/signin', {
           user_id: "test_user",
           password: "xxx",
      });

其中,RequestResponse是返回的数据要解析为的数据类型,如下:

export interface RequestResponse {
   data: any;
   message: string;
   resultCode: number;
}

这样,得到的response就是网络请求的结果,可以进行判断处理。

2. Axios基本封装用法:

对Axios进行简单的封装,使得多个网络请求可以使用统一的header等配置。

新建一个工具类,进行封装:

import Axios, { AxiosRequestConfig, AxiosError, AxiosInstance, AxiosResponse } from 'axios';

export const BASE_URL = "https://test.api.com";

export const axiosApi = (): AxiosInstance => {
 const instance = Axios.create({
   baseURL: BASE_URL,
   headers: {
      'Content-Type': 'application/json',
      Authorization: `${getAccessToken()}`,
  },
});
   
 return instance;
}

const getAccessToken = () => {
   // 这里获取本地保存的token
   return xxxxx
}

然后使用的地方是这样:

const response = await axiosApi().post<RequestResponse>('/signin', {
    user_id: "test_user",
    password: "xxx",
});

3. 添加拦截器的用法

现在我们想再增加个功能,就是调接口时,header里传了token,但是有时候token过期了接口就会返回失败,我们想在封装的地方添加统一处理,如果token过期就刷新token,然后再调接口。

其中token的数据格式及解析方法已知如下:

import * as crypto from 'crypto';
import * as jwt from "jsonwebtoken";

export interface TokenData {
 userid: string;
 exp: number;
 iat: number;
}

export const decodeJWT = function (token: string): TokenData {
 if (!token) {
   return null;
}
 const decoded = jwt.decode(token, { complete: true });
 return decoded?.payload;
};

如何统一刷新token呢?可以添加拦截器进行处理。把对Axios的封装再改下,添加拦截器:

export const axiosApi = (): AxiosInstance => {
 const instance = Axios.create({
   baseURL: BASE_URL,
   headers: {
      'Content-Type': 'application/json',
      Authorization: `${getAccessToken()}`,
  },
});
 
 // 添加拦截器
 instance.interceptors.request.use(
   config => {
     return refreshToken(config);
  },
   err => {
     return Promise.reject(err)
  }
)
 return instance;
}

// 刷新token的方法
const refreshToken = async (config: AxiosRequestConfig) => {
 const oldToken = getAccessToken();
 if (!oldToken) { //如果本地没有token,也就是没登录,那就不用刷新token
   return config;
}

 const tokenData = decodeJWT(oldToken);//解析token,得到token里包含的过期时间信息
 const currentTimeSeconds = new Date().getTime()/1000;

 if (tokenData && tokenData.exp > currentTimeSeconds) {
   return config; // token数据里的时间比当前时间大,也就是没到过期时间,那也不用刷新
}

 // 下面是刷新token的逻辑,这里是调API获取新的token
 const response = await signInRefreshToken(tokenData?.userid);
 if (response && response.status == 200) {
   const { token, refresh_token } = response.data?.data;
   // 保存刷新后的token
   storeAccessToken(token);
   // 给API的header设置新的token
   config.headers.Authorization = token;
}
 return config;
}

经过这样添加了拦截器,如果token没过期,就直接进行网络请求;如果token过期了,那就会调接口刷新token,然后给header设置新的token再进行网络请求。

4. 注意事项:

要注意的一点是,实际应用时,要注意:

1.刷新token时如果调接口,所使用的网络请求工具不能也使用这个封装的工具,否则就会陷入无限循环,可以使用简单未封装的方式请求。

2.本例使用的方法,是进行请求前刷新token。也可以使用先调网络请求,如果接口返回错误码表示token过期,则刷新token,再重新请求的方式。

作者:程序员小徐同学
来源:juejin.cn/post/7159727466439770119

收起阅读 »

使用 husky 实现基础代码审查

web
在日常提交 PR 的过程中,我们提交的文件不应该有例如 console、debugger、test.only 等调试语句,这会影响到线上代码。那每次提交之前都检查似乎又像是一个繁琐的工作,如果有个工作能代替我们检查我们提交的代码,让不能提交到线上的代码在 co...
继续阅读 »

在日常提交 PR 的过程中,我们提交的文件不应该有例如 console、debugger、test.only 等调试语句,这会影响到线上代码。那每次提交之前都检查似乎又像是一个繁琐的工作,如果有个工作能代替我们检查我们提交的代码,让不能提交到线上的代码在 commit 阶段停止下来,对 code reviewer 的工作会减少不少。 这里就来跟大家探讨一下我的一个实现方式。

前言

在提交代码的时候不知道大家有没有注意命令行中会打印一些日志:


像这里的

husky > pre-commit
🔍 Finding changed files since ...
🎯 Found 3 changed files.
✅ Everything is awesome!
husky > commit-msg

这个出处大家应该都知道,来自 pretty-quick ,然后通过 packge.json 中的:

  "husky": {
   "hooks": {
     "commit-msg": "commitlint -E HUSKY_GIT_PARAMS",
     "pre-commit": "pretty-quick --staged"
  }
}

这段代码在我们 commit 的时候对代码进行相关处理。

这里是 husky 调用了相关的 Git hooks ,在合适的时机处理我们提交的代码文件,从而使我们的代码达到提交的要求(如上面的是格式化相关代码)。

看到这里肯定大家就会想到,那这个是不是可以做的还有更多?

没错,下面就直接上配置,来实现我们不想让某些代码提交到线上这样的需求。

第一版

知道这个原理,那就很简单了,我们在 pre-commit 这个事件触发的时候对我们要提交代码检查一下,看其中有没有那几个关键字就可以了。

那就直接动手:

我们在项目根目录找到 .git/hooks :


可以看到,这里提供了各种触发时机,我们找到我们想要的 pre-commit (如果后缀有 .sample,需要去除掉才能让此文件生效)。打开此文件,前端的话应该看到的是(不需要阅读):

#!/bin/sh
# husky

# Hook created by Husky
#   Version: 2.7.0
#   At: 2023/2/2 13:14:26
#   See: https://github.com/typicode/husky#readme

# From
#   Directory: /Users/frank/Documents/work/worktile/wt-cronus/projects/pc-flow-sky/node_modules/husky
#   Homepage: undefined

scriptPath="node_modules/husky/run.js"
hookName=`basename "$0"`
gitParams="$*"

debug() {
 if [ "${HUSKY_DEBUG}" = "true" ] || [ "${HUSKY_DEBUG}" = "1" ]; then
   echo "husky:debug $1"
 fi
}

debug "$hookName hook started"

if [ "${HUSKY_SKIP_HOOKS}" = "true" ] || [ "${HUSKY_SKIP_HOOKS}" = "1" ]; then
debug "HUSKY_SKIP_HOOKS is set to ${HUSKY_SKIP_HOOKS}, skipping hook"
 exit 0
fi


if ! command -v node >/dev/null 2>&1; then
 echo "Info: can't find node in PATH, trying to find a node binary on your system"
fi

if [ -f "$scriptPath" ]; then
 # if [ -t 1 ]; then
 #   exec < /dev/tty
 # fi
 if [ -f ~/.huskyrc ]; then
  debug "source ~/.huskyrc"
  . ~/.huskyrc
 fi
node_modules/run-node/run-node "$scriptPath" $hookName "$gitParams"
else
 echo "Can't find Husky, skipping $hookName hook"
 echo "You can reinstall it using 'npm install husky --save-dev' or delete this hook"
fi

看了下,感觉没啥用,就是一个检测 husky 有没有安装的脚本。我们这里直接使用下面的替换掉:

#!/bin/sh

errorForOnly() {
 result=""
 for FILE in `git diff --name-only --cached`; do
     # 忽略检查的文件
     if [[ $FILE == *".html"* ]] ; then
        continue
     fi
     # 匹配不能上传的关键字
     grep 'serial.only\|console.log(\|alert(' $FILE 2>&1 >/dev/null
     if [ $? -eq 0 ]; then
         # 将错误输出
         echo '❌' $FILE '此文件中包含 [only]、[console]、[alert] 中的关键字, 删除后再次提交'
         # exit 1
         result=0
       else
         result=1
       fi
 done

 if [[ ${result} == 0 ]];then
       exit 1
 fi
 echo "✅ All files is OK!"

}

errorForOnly

然后我们在一些文件中添加我们不想要的关键字,然后 git commit :


可以看到错误日志以及文件已经在命令行中打印出来了,同时文件也没有进入本地仓库(Repository),让然在我们的暂存区(Index)。


使用这个方式,在一定程度上我们避免了提交一些不想要的代码到线上这种情况了发生。同时也是在本地提交代码之前就做了这个事情,也避免了使用服务端 hooks 造成提交历史混乱的问题。

这时候你肯定会产生这样的疑问:


【欸,欸,欸,不对啊】

问题

这种方式有个显而易见的问题,那就是不同团队协同方面。由于 hooks 本身不跟随克隆的项目副本分发,所以必须通过其他途径把这些 hooks 分发到团队其他成员的 .git/hooks 目录并设为可执行文件。

另外一个问题是我们使用了 husky,在每次 npm i 之后都会重置 hooks 文件。也就是 .git/hooks/pre-commit 文件恢复到了最初(只有 husky 检测的代码)的样子,没有了我们写的逻辑。

这种情况是不能允许的。那就寻找解决途径。查了下相关文档,发现可以使用新版的 husky 来解决这个问题。(其他相关的工具应该也可以,这里使用 husky 来进行展示)。

最新实现

husky 在 v4 版本之后进行了大的重构,一些配置方式不一样了,至于为什么重构,大家可以去

安装

安装 npm install husky --save-dev

安装最新版本为 8.0.3 ,安装完成后启用 Git hooks: npx husky install


在团队协作的情景下,得让本团队的其他人也能自动的启用相关 hooks ,所以添加下面这个命令,在每次 npm install 之后执行:

npm pkg set scripts.prepare="husky install"

我们就在 package.json 得到了这样的命令:


yarn2+ 不支持 prepare 生命周期脚本命令, 安装方式在此处

使用

先按照官方文档测试一下,执行 npx husky add .husky/pre-commit "npm test" 在相关目录我们就看到:


相应的文件以及内容已经准备就绪。这里就不运行了。

那如何将之前的流程使用新的版本配置好呢?

这里直接提供相关文件内容:

# .husky/commit-msg
# 用于 commit 信息的验证
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npx --no -- commitlint --edit $1

使用下面的语句生成;

npx husky add .husky/commit-msg 'npx --no -- commitlint --edit `echo "\$1"`'

另外还有 pre-commit:

# .husky/pre-commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

# pretty-quick 相关
npx pretty-quick --staged

# 验证是否有调试关键字的脚本:
node bin/debugger-keywords.js

脚本内容:

// bin/debugger-keywords.js
const { execSync } = require('child_process');
const fs = require('fs');
const gitLog = execSync('git diff --name-only --cached').toString();
const fileList = gitLog.split('\n').slice(1);
const keyWordsRegex = /(console\.(log|error|info|warn))|(serial\.only)|(debugger)/g;
const result = [];

for (let i = 0; i < fileList.length; i++) {
const filePath = fileList[i].trim();

if (filePath.length === 0 || filePath.includes('.husky') || filePath.includes('bin/')) {
continue;
}
const fileContent = fs.readFileSync(`${filePath}`, 'utf8');
const containerKeyWords = Array.from(new Set(Array.from(fileContent.matchAll(keyWordsRegex), m => m[0])));

if (containerKeyWords.length > 0) {
const log = `❌ ${filePath} 中包含 \x1B[31m${containerKeyWords.join('、')}\x1B[0m 关键字`;
result.push(log);
console.log(log);
}
}

if (result.length >= 1) {
console.log(`💡 修改以上问题后再次提交 💡`);
process.exit(1);
} else {
console.log('✅ All files is OK! \n');
process.exit(0);
}

为几个文件添加 console 、测试 only 等,提交 commit 后效果展示:


会提示文件中不合规的关键字是哪些。

更多

有了以上的使用示例,我们可以在随意添加脚本,比如,为 19:00 之后或周末提交代码的你来上一杯奶茶🧋和一个甜甜圈🍩:

// bin/check-time.js
const now = new Date()
const week = now.getDay()
const hour = now.getHours()
const validWeek = week >= 1 && week <= 5
const validHour = hour >= 9 && hour < 19
if (validHour && validWeek) return

console.log(`🌃 来点 🧋   🍩`);

这次为了方便也在 pre-commit hook 中执行:

# .husky/pre-commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

# pretty-quick 相关
npx pretty-quick --staged

# 验证是否有调试关键字的脚本:
node bin/debugger-keywords.js

# 来杯奶茶
node bin/check-time.js

来看下结果:


结论

目前单纯使用 pre-commit hook 针对常见的 console、debugger 还有测试 only 这种。但是我们也可以看到只要我们写不同的脚本,可以实现不同的需求。

之后如果有更多的需求也能继续添加脚本,也可以产生我们 PingCode 自己的 lint 插件。

ps. 如果使用可视化工具提交可能会报错,大家可以自行查阅解决。

作者:阿朋
来源:juejin.cn/post/7202392792726011959

收起阅读 »

复盘:前端岗位的寒冬,用这3点进行自救

前言先介绍一下个人情况,方便给正在求职的朋友一个参考。本人是19年非计算机专业某末端二本毕业,去年8月31日从一线的一家国企离职,回去家里休息了一个多月,在国庆结束之后落地到了新的城市,开始了我的前端自救之旅。10月17日开始投递简历,到10月27日决定停止面...
继续阅读 »

前言

先介绍一下个人情况,方便给正在求职的朋友一个参考。

本人是19年非计算机专业某末端二本毕业,去年8月31日从一线的一家国企离职,回去家里休息了一个多月,在国庆结束之后落地到了新的城市,开始了我的前端自救之旅。

10月17日开始投递简历,到10月27日决定停止面试。一共面试了6家公司,最终拿到了2家公司的offer。

我简历主要写到了vue、微信公众号、微信小程序、node.js(主要是egg.js和koa.js)、webpack和团队管理的经验。

我对react的使用比较少,所以react问的比较深的岗位我都没有投递。

经过了半个多月的面试经历,我主要从市场环境、作为面试官的体验、面试题目三部分聊一下我的心得。

为了对前东家和现东家表示尊重,我分别用A市代替上一个城市,C市代替现在的城市,并且把关键信息隐藏。

市场环境

还在A市的时候,只是听同事说了一下外面的情况很糟糕,如果考虑换工作的话建议三思后行,一定要想清楚了再决定是不是要行动。

我这人有点不信邪,因为这样的话我好像都听腻了。

小时候老师这样劝诫我,长大了父母这样叮嘱我,工作了老板这样劝说我。

周围的声音都劝你,要走最安全的路,不要冒险。

直到我打开了脉脉。




为了保护截图楼主的隐私,特意打了马赛克。

从脉脉上面反馈的话题都很让人绝望,仿佛每个人在脉脉上过的都很惨。

每天刷出来的话题都跟职场末日一样。

但是我自己的路还是要自己走的,别人的世界和经历只能作为参考,无法影响我的决定。

于是,我选择性的忽视了脉脉话题的讨论。

专注于自己当下的问题,直面最现实的问题,并解决这些问题。

这里有一点建议需要给到仍在空窗期的朋友:如果此时正处于职场求职的阶段,应该尽可能的忽略会给你产生自我怀疑或者负能量的东西,这些东西的一点点腐蚀你的心智,磨灭你的念想,进而影响你对当下正确的判断

面试官的体验

我在上一家公司担任的是技术负责人的身份,所以在去年8月初跟上级领导提出离职的申请之后,就开始帮公司招人了。

所以我也能从面试官的视角给求职者提供一些有利的信息,进而促成面试的成功。

主要从下面3点展开来说一下吧。

1、投递有可能没有回应

给大家分享一个简历投递的小故事。

8月12日,公司人事上午刚把前端的岗位发布出去,仅仅经过了2个小时,我就收到了人事发给我的50份简历。

然后我从这50份简历里面选出了12份作为候选人,接着人事给候选人打电话,7位候选人接通了电话,并预约了面试时间。

下午人事又筛选了60份简历给我,我从这些简历里面筛选出了8位候选人,人事打电话,6位接通了电话。

这样下周就有了14位候选人进行面试,我也跟人事说暂时先不接收简历了,当前的面试结束之后,再开放招聘岗位吧。

同时说一下我的心理状态吧。

上午我刚拿到简历的时候看的很仔细,每一份简历都很认真的去看,甚至会看一下他的项目介绍。

但是当我上午看完了50份简历,我觉得很多简历很雷同,长得都差不多,千篇一律的感觉。

下午又收到了60份简历,这个时候我基本看麻了,如果技术栈方面不匹配(比如我们项目要求的是vue,但是以前的项目都是react,这样的简历我就直接跳过了),基本就不会预约面试了。

所以说,招聘的时机是有很大的偶然性的。

如果你的投递时间正好是8月12号的上午,并且是在我看简历的前1个小时,只要能满足干活的能力,大概率都有面试邀约。

但如果投递时间是在8月12号的下午,必须要技术栈强匹配,项目相对复杂,简历写精炼简洁,能一眼抓住我的眼球,才能获得邀约。

另一方面来看,

如果投递时间不是8月12号这一天的话,我其实是看不到你投递的简历的。

因为在我说下周之内都不需要给我推简历之后,人事很大概率会把这个职位挂着。

但是面试者是不了解我这边的情况,看到岗位还在招聘,就会继续投递简历。但真实的情况是:我已经触达不到他们了,简历投进来基本石沉大海。

这样有一些朋友在投递过程中遇到的很多公司人事没有回馈,很大可能是遇到了上述我说的情况:简历都在排着队,触达不到面试官。

所以正在求职的朋友也不要灰心,把时间线拉长来看这个事情。公司跟个人之间是双向选择的,好事多磨,最终都能获得自己想要的。

2、简历是门面

简历是作为公司能够在没有见到真人情况下,对你能力最直接的预判,然后决定是否需要把你列入候选人名单的重要条件。

所以这个门面特别重要。

首先是基本信息一定要清晰。

姓名、年龄、性别、工作年限、手机号、邮箱、学历跟求职地点一定要写清楚,这些算是必填项了。

然后技术栈尽量简短概括,陈述你大概能做哪些事情。

我在招聘求职者的时候首要看这一项,技术栈不精练的可能就直接跳过了。

像jquery、bootstrap这些比较老的库,或者说类似elementui等ui库尽量都不要写上去,看上去显得重复累赘,没有突出重点。

还有学历方面,上家公司的招聘学历是硬性要求的。

我也看到有技术能力完全能胜任的的两个面试者,就因为学历问题被卡了。问了人事学历能不能放宽一点,她说是硬性规定,不能放开。

我想人事大概也是不想徒增麻烦吧,直接按照流程走总不会出错,也能理解。

但是站在专科的求职者角度,技能到了但是因为学历原因,也就缺少了一个机会,还是有点遗憾的。

但是对于卡学历的求职者,我也显得有心无力,帮不上忙。

3、自我介绍很重要

面试的第一步首先是自我介绍,我认为这个是非常重要的一步。

其实面试官在拿到求职者简历的时候,可能也没有很认真的阅读。

所以面试官让你进行自我介绍,一方面是留给自己时间让面试官有的一个看你简历重点的缓冲时间,另一方面也希望你说点跟简历上不一样的东西,然后从自我介绍中找到话题,向求职者提问。

在我的面试体验下来,我觉得会让我比较舒服的自我介绍时间是2分钟左右。

一方面过于简短的自我介绍会让我感觉到面试者对项目或者对自己的认识不够深入,或者自我介绍就是简历中已经写过问题的重述,都让我感觉准备不充分,有青涩感。

还有过短的自我介绍会让面试官在认真看简历的过程有有一种突然被打断,或者没太想好根据简历中哪一个具体的点进行发问而错失了首次接触的好感度。

当然我其实在面试时已经会建立一个准备提问的题库。

即使面试者没有项目亮点,我也能就根据我在开发中遇到的问题或者一些基本面试题进行发问。

面试本身两个人就像聊天一样,本质上还是人之间情感的交互。

只要有人的地方,就会掺杂个人的情感偏好,相互之间有情绪感情的传递。

提供正向的情绪,尽可能的体现自己的加分项、专业度能让面试官觉得你很靠谱、值得推荐。

同时也能增加面试官对你的好感度。

举个栗子:我在面试中遇到一个女生在自我介绍的时候,她先简要介绍了一下自己常用的技术栈以及对应技术栈的熟悉程度,然后介绍了她上家公司做的项目,分模块的介绍一下自己在这些模块中做的事情以及两个业务线之间的关系。

她的介绍大概在3分钟左右,整个介绍说的比较有条理、有逻辑性,在她做完自我介绍的之后,我基本已经知道从她项目中挖掘哪些问题进行提问了。

全程没有一句废话,这让我对她的专业度的好感就增加了;即使她后面vue的一些原理说的很磕磕巴巴,但最后因为第一印象,我还是推荐了她。

在我面试的所有女生中,提炼中心思想,能有条理且有主次的把自己的擅长的技术和业务说明白的,她是最好的一个。因为我所在的业务生产线需要做一些跨部门沟通,所以能一句话说清楚自己的需要,情商在线的话,能提高整个工作的效率。

面试中整理的面试题

接下来就是在这6家公司面试中遇到的问题了。

一、公司一

1、机试
1、把一个url拆解成origin、文件名、hash拆解成示例的格式。
2、两个数组合并成一个数组,并进行算法优化。
3、设置值的时候是数字,输出的时候变成百分号的格式。
4、首屏优化的方案,分别从代码、网络和缓存说一下。
5、如果一次性增加100万个用户访问项目,前端角度你会怎么优化。
6、分别用es5和es6的方式解决一个继承问题
2、现场面试

第一轮的机试过了,约了下周进行面试。

双休在知乎上搜了一下这家公司,网上风评不太好,技术栈也不是我熟悉的,就拒绝了接下来的面试。

二、公司二

1、一面
1、简单的介绍一下你自己
2、你们这项项目中有微信公众号、后台管理端、医生app端和小程序端,有没有提炼出一些公共的工具js,怎么提炼的?
3、你们的git分支管理是怎么样的?
4、你在做完前端项目之后,一般都会写哪些文档,readme里面写一些什么?
5、你做完一个项目之后写一些什么内容,让接手你的人能够看懂你的项目架构,并且迅速上手?
6、你基于你的脚手架做了哪些优化?
7、你们的项目文档一般都要记录哪些基本的东西?
8、有在项目中遇到过xss攻击吗?
9、你这个错误数据上报了哪些数据,怎么实现的?
10、成功抵御过多次攻击能具体说一说吗?
11、说一下你在项目中遇到印象深刻的项目场景,并且怎么解决的?
12、能说一下sessionjwt的优缺点吗?
13、你说用户登录之后要在session中加入user_name,为什么要增加?
14jwt的整个流程怎么实现的?
15、实现jwt的实现遇到什么困难了吗?
16、如果同一部手机用户A登录了,更换了B登录,此时使用jwt会出现什么问题?
- 我觉得这里他给挖坑了,B登录了之后就会返回B用户的token,然后请求头带着的就是Btoken,验证到的用户也就是B,我认为没有问题啊。我不太清楚他说的可能会有什么问题值啥情况,可能是我考虑不周全,如果有网友知道可以提醒一下我他想问的答案,让我学习一下。
17、当线上出了紧急bug的时候,你们是怎么处理的?
18、你们团队成员是怎么配合完成任务的?
19、你近2年的职业规划?
20、还有什么想问我的吗?

问的一些问题偏向于后端,面试官大概是java工程师吧。然后从团队管理、怎么让新来的成员快速接手项目、文档方便的管理等等;前端技术问题整个面试过程中占比不大。

看得出来至少要在团队管理方面有比较深的总结,并且对于团队管理积累上,自己有独有的见解,一面才能面试通过。

一面就败了的原因,主要偏向于管理方面的经验,这部分我总结比较少。

另一方面,我觉得有可能招聘单位也没太想清楚要招聘怎样的人吧,时间过去了半年,我今天刷了一下boss直聘,发现这家公司的招聘信息还挂着,人事活跃时间也是几分钟前。

三、公司三

1、一面
1、事件循环的机制了解吗?宏任务和微任务的执行顺序是怎样的?
2、怎么理解闭包这个定义的,在平时工作中有用到闭包的使用吗,举个例子。
3、vue组件间的哪些通信方式?
4、一个父组件潜嵌套了子组件,他的生命周期函数顺序是怎么执行的?
5、vue的权限管理应该怎么做?路由级和按钮级分别怎么处理?
6、说一下你对虚拟DOM的理解
7、了解diff算法吗?vue的diff算法是怎样的一个过程
8、能说一下v-for中key的作用吗?
9、做过vue项目哪些性能方面的优化?
10、vue组件为什么只能有一个根元素?
11、如何实现路由懒加载呢?
12、客户端渲染和服务端渲染有什么区别呢?在之前的工作中有做过服务端渲染吗?
13、Vue长列表的优化方式怎么做?
14、Vue3相比Vue2有哪些优化?
15、为什么在模板中绑定事件的时候要加.native?
16、能说一下响应式原理的过程吗?
17、数组的响应式怎么实现的?
18、Vue是数据改变后页面也会重新改变嘛;this.a = 1; this.a = 2; 他是怎么实现异步更新优化整个渲染过程的?
19、render函数封装有什么特别的,或者用到比较巧妙的东西吗?
20、浏览器缓存的方式有哪些?
21、正向代理和反向代理的区别?
22、域名解析过程是怎样的?
23、TCP协议三次握手、四次挥手的过程,为什么挥手要4次?
2、二面
1、nextTick, setTimeout 以及 setImmediate 三者有什么区别?
2、说一下你在项目的安全性做了哪些工作?
3、当一张表数据量比较多的时候,为了提高查询速度,你们一般会使用哪些方式做优化?
4、webSocket与传统的http相比有什么优势?
5、用过koa吗?简要阐述一下koa的洋葱模型。
6、用过promise吗?它的使用是为了解决一个什么问题?promise底层是怎么设计的?
7、你们现在整个登录鉴权是怎么设计的?如果要考虑单点登录呢,会如何设计?
8、如何用同一套代码部署到服务器中,怎么区分当前本地开发环境还是线上环境?是测试环境还是生产环境呢,怎么去区分?
9、待支付的订单,到期后主动取消这个功能你会怎么设计去做?
10、如果要做音视频的安全性,你能想到哪些方案?
11、多台服务器部署定时任务怎么保证一个任务只会做一遍呢?
12、你觉得程序员除了提升技术能力之外,其他什么能力你比较看重?
3、人事面

人事面遇到的问题都比较类似,我在下面会专门拿一个部分汇总人事面的问题。

该公司最后也拿到了offer。

四、公司四

1、你在项目中用到mongodb吗?
2、在项目中用到mongodb存储哪些数据?
3、mongodb的管道有了解吗?聚合管道怎么用的?
4、mongodb和的mysql优缺点?
5、你对事务性的了解是怎样的?
6、node怎么开启子进程?
7、在一台机器上开启负载均衡的时候,如果这个项目有用到定时任务,你怎么去控制这个定时任务只会执行一次?
8、你在egg中怎么开启子进程,怎么编写一个定时任务?
9、react用的多吗?
10、react组件间通信的方式有哪些?
11、vuex跟redux的区别有哪些?
12、computed和watch的区别?
13、watch和computed哪一个可以实现异步?
14、vue的通信方式有哪些?
15、vue的history模式和hash模式的区别是什么?
16、history模式下会出现404,怎么处理?
17、你能说一下闭包的优缺点吗?
18、内存泄漏和内存溢出有什么区别?
19、还有什么想问我的吗?

该岗位是node全栈工程师的岗位,对后端的知识点问题的比较深,一层一层的往下问,我后端的知识点稍微薄弱一些,很多很细的问题答不上来;他那边技术栈用的是react,我复习的知识也比较少。

扑街了理所当然。

五、公司五

1.1、自我介绍?
1.2、常用的选择器有哪些,优先级怎么样?(除了这些还有其他的嘛)
1.3、垂直居中的实现方案有哪些?
2.1、你说的网格布局grid垂直居中有哪些属性值?
2.2、width:100%和width:auto有什么区别?
3、说一下cookie的作用是什么?
4、cookie有哪些属性?
5、设置cookie的domain用来实现什么功能?
6、懒加载的实现原理是怎样的?(除了你说的那一种还有其他的嘛)
7、vue中路由懒加载怎么实现?(除了你说的这一种还有其他的嘛)
8、说一下原型链的理解?
9、原型链__proto__这个隐式属性的实现原理是怎样的?
10、说一下vue中双向数据绑定?
11、vue中computed和watch的区别是什么?
12、说一下你们的前端登录流程是怎样的?
13、jwt是什么?
14、jwt由哪些部分组成?
15、你在项目中怎么实现打包优化的?
16、你说的这些优化方式是webpack哪个版本的?
17、你说一下项目中比较困难的事情有哪些(BFF处理模式)?
18、你们部署上线是怎么做的?
19、在项目中有使用jekenis和docker这些吗?
20、有什么想问我的吗?

我记得网格布局是有justify-contentalign-items属性,并且面试之后专门写了一个文件测试,测试通过。面试官说我属性记错了。

width:100%width:auto有什么区别?这个问题我没回答出来,最后我问面试官他们的区别是什么。面试结束之后我按照他说的那种方式,也没测试出来区别,很纳闷。

原型链__proto__这个隐式属性的实现原理是怎样的?,我以为这个问题就是让我说一下实例.__proto__指向构造函数的原型,抓住这个点然后扩展一下原型链的知识就好了。他说不是要你回答这个,而是让我说一下proto的底层实现,这个问题我不知道,有知道的朋友可以在评论区帮我回复一下,我学习一下这个知识点。

整个面试从面试官的表达上可以看得出来他有一些紧张,导致有一些问题我也听的不是特别清楚。结束之后我测试的几个知识点也没达到他说的效果,遗憾。

最后应该是挂了。

六、公司六

拿到offer,出于公司隐私考虑,不方便透露具体过程。

人事面问题汇总

1、对自己的评价?
2、你有哪些兴趣爱好?
3、描述一下你自己的优缺点?或者用三个词语描述你自己?
4、你在公司主要做一些什么工作?
5、离职原因是什么?
6、在工作之外有哪些学习技术的方式?
7、公司的整个开发流程是怎样的,你跟团队成员如何配合完成任务?
8、你有女(男)朋友了吗(稳定性)?
9、你有其他offer吗(稳定性)?
10、如何提高工作效率?
11、与领导意见不统一时应该怎么办?
12、你觉得目前自己的技术在什么位置,觉得自己哪一块能力需要加强?
13、您还有什么问题想问我的嘛?
15、你的职业规划是怎样的?
16、入职之后如何开展工作?
17、是否愿意接受加班?
18、你能为公司带来什么?你希望公司给你什么?
19、在项目中遇到了什么难点问题,最后怎么解决的?
20、谈一下你在上一家公司整个技术开发流程,你负责哪些工作?
21、你希望自己以后的发展方向是什么?

知己知彼,从容不迫。

接下来的计划

在这次的面试中,也发现了自己能力的不足。比如我之前通过node写了一些提升团队的工具,用于提升自己在团队中的kpi。但是在纯前端中可能只能作为一家加分项来看,因为如果问到很具体的node的内存问题、mongodb细节的问题,可能就会被问住了,整个面试体验确实会比较糟糕。

C市很多大公司都是用的react,我之前在项目中用react比较少,所以空余时间要把react用起来,不用起来就会忘。

然后vue3的源码和优化的方面也要继续看看了。

还有这一次没有投递大厂的原因是我没有刷算法,这一点在之前的求职过程中都没有重视起来。现在年限到这了,必须要刷起来,才能更进一步提升自己。

给大家的一个建议

我在面试的过程中有一个体会,就是:当面试官问你一个很大的问题的时候,你要怎么回答?

比如面试官问题,简单的跟我说一下继承是什么吧?

很多朋友遇到这样的一个问题,马上就大脑蒙圈了,脑袋里很紊乱,知道很多,但是无法把他们串起来,不知道从何说起,导致回答的结果不是很好。

所以像这些技能题,我们在记笔记的时候也应该用这样的方式去记忆和背诵。遵循下面的几个步骤就行了,套公式的方式,一点也不会慌乱。

1、解释是什么的问题。
2、解释这个技术的应用点、应用场景在哪里。
3、整理一下这个问题的优缺点是什么。

我举一个例子来回答一下。比如面试官问:你给我讲一下闭包吧?

我就可以按照上面的归纳,分3步走的原则。

1. 闭包是:能够访问其他函数内部变量的函数。
2. 闭包一般会在:封装模块的时候,通过函数自执行函数的方式进行实现;或者在模仿块级作用域的时候实现;如:我们常用的库jQuery本身就是一个大的闭包。
3. 闭包的优点是:
   a、能够在离开函数之后继续访问该函数的变量,变量一直保存在内存中。
   b、闭包中的变量是私有的,只有闭包函数才有权限访问它。不会被外面的变量和方法给污染。
闭包的缺点是:
   a、会增加对内存的使用量,影响性能。
   b、不正确的使用闭包会造成内存泄漏。

针对上面闭包的回答,可能面试官又会继续问你:内存泄漏是什么啊,你能给我讲一下吗?垃圾回收机制说一下吧?内存泄漏和内存溢出的区别是什么?等等。

就是你回答完了这个问题之后,你可以先假设性的想一下面试官看到你的这个回答可能会问你什么。然后给自己提问深挖技术深度问题。

当然那不光是面试,自己平时学习深挖知识的时候,也可以用这种办法。

以上这3点,就是我在去年的面试,从自己的真实经历中的总结。

我觉得在求职的时候我遇到的问题,相信很多朋友有类似经历的朋友也会遇到。我就本着造轮子的这个心态,给大家输出一些流程化的经验,希望大家能在本次经济下行,行业萧条的情况下拿到好的结果。

另外,如果你想要更多关于前端方面的成长干货和学习方法与资料,欢迎你关注我这个账号「程序员摩根」,并且私信我。

里面有我最宝贵的私人学习经验,全都毫无保留分享给你,比如优质的前端编程类电子书,前端学习地图,前端入门到精通的学习路径、面试题等。

我的总结

去年10月份之后,整个面试下来,能明显感觉到整个市场的供需关系发生了变化。经过这一轮的洗刷之后,对整个前端求职者的深度、专业度一定会更上一个台阶。

所以在没找到更好的收入方式之前,刷题刷经验,技术上做精做深,这些事情还是需要重复做,认真做。

当然,即使在比较惨烈的市场环境下,我依然认为每个个体都有很大机会的。

我是个乐观主义者,积小胜为大胜。打能胜的仗,才能打胜仗。与各位共勉。

当然,这是我去年10月份的求职情况,今年的情况是否比去年年底还要更惨烈。我没有进入到实际的市场环境中体会。

希望还在求职的朋友们能在底部@我一下,跟大家分享一下目前你所在城市的前端求职现状,大家群策群力,一起商量下应对方法。

欢迎大家在评论区讨论一下。

如果这篇文档对你有帮助,欢迎点赞、关注或者在评论区留言,我会第一时间对你的认可进行回应。精彩内容在后面,防止跑丢,友友们可以先关注我,每一篇文章都能及时通知不会遗失。

作者:程序员摩根
来源:https://juejin.cn/post/7201491839815139389

收起阅读 »

最近很多人都在说 “前端已死”,讲讲我的看法

web
我记得去年脉脉的论调还都是 客户端已死,前后端还都是一片祥和,有秀工资的,有咨询客户端转前端的,怎么最近打开脉脉一看,风向变了?我有时候会想,开源是个好东西,拉低了技术的门槛,好像再难的需求,只要善用搜索引擎,都能找到前人喂到嘴边的答案,如果没有开源文化,甚至...
继续阅读 »

现状

我记得去年脉脉的论调还都是 客户端已死,前后端还都是一片祥和,有秀工资的,有咨询客户端转前端的,怎么最近打开脉脉一看,风向变了?

随便刷几下,出来的信息都是 前端已死,这种悲观信息,还有失业找不到工作的。


思考

我有时候会想,开源是个好东西,拉低了技术的门槛,好像再难的需求,只要善用搜索引擎,都能找到前人喂到嘴边的答案,如果没有开源文化,甚至自己能不能进入互联网开发行业都不好说。

记得那一年:

我第一次接触技术领域,开始在百度输入前端开发相关的问题,发现出来的结果都是 csdn、博客园、简书等网站的内容,打开了新天地,我开始吸取前辈们的经验和心得,他们分享的各种书上学不到的知识让我解决了工作中遇到的各种问题,让我感叹 开源真好

我发现程序员们都推崇谷歌搜索和谷歌浏览器,于是自己也开始使用,发现 真香

我日夜沉迷在技术研究上,为自己找到了一份兴趣爱好和职业相结合的工作而庆幸和兴奋;

大佬们都说要追求 高复用、高内聚、低耦合、易拓展,于是我忙不停蹄的学习这些概念和应用实践;

后来社区开始讨论 低代码,自动化,人工智能,大家好像都蛮兴奋的;

后来听说 客户端开发 不行了,小程序、h5 分走了大量的市场需求,我很庆幸,当初选择了前端,但我隐隐有些不安,因为我发现自己达到了 瓶颈,从业务开发中已经难有太多的技术提升了;

我开始学习 Java, springboot + mysql + mybatisPlus 做了一些简单的 crud,貌似并不是很难,我又有了些自信,以后前端干不了了,还可以搞后端;

我相信 如果一种思想不能拿出来给公众思辨,那么它和不存在没什么两样,所以我开始写博客,我认为这是最好的学习方式,但是我还不太擅长,22年一年才写了20+篇;

然后 chatGPT 出来了,我注册体验了一番,它真的可以在很多场景下协助我提升效率,这点资本也看得到,所以直接后果就是市场可能不需要那么多 开发者了, 这不局限在前端,甚至 chatGPT 更擅长写后端代码;

最后,互联网市场已进入红海之争了,存量市场下,开源节流是正常的操作,去年就经常看到外国各大大厂裁员的消息,国内就更不可能独善其身了;

总结

若没有开源文化,会不会互联网开发,也是一个越老越吃香的职业呢 [微笑]?我不知道,我是开源的受益者,我也愿意为开源做贡献,但是我不会期待它能给我带来多大的商业收益了,开源和商业付费之间,是两种文化之争;

最近core-js 作者对开源社区的“控诉”更是印证了我的看法: core-js 作者快被缺钱“拖垮”了:全职做开源维护 9 年,月均收入从 2500 美元锐减到 400 美元

前端已死 更多的是一种焦虑情绪的表达,市场确实不太好,但这并不是针对前端,整个互联网行业衰败的表现而已,对此持不同意见的怕是只剩培训机构了吧;

前端老鸟,市场还是需要和欠缺的,只是对于初中级前端太卷了,我建议应届生不要继续入门前端了,搞搞嵌入式开发,或者芯片之类的,门槛高一些。

总之,不用过于悲观,互联网风口过去了,还会有下一个风口,比如 web3, 人工智能,都可能带来新的市场机会,作为时代前沿的参与者,程序员因该更容易抓住这样的机会吧。

作者:Ethan_Zhou
来源:juejin.cn/post/7201047960826052667

收起阅读 »

24 岁技术人不太平凡的一年

一年前,我整夜混迹于魔都淮海中路的酒吧迪厅,而白天,在外滩海景餐厅吃着四位数一顿的西餐。租住的洋房里陈列着各种奢侈品…… 一年后,我结婚了,并且买了套房子。 一、魔都 2021 年,是我爸妈非常兴奋的一年。因为那年我去了上海,找了一份年薪 60 几万的工作。他...
继续阅读 »

一年前,我整夜混迹于魔都淮海中路的酒吧迪厅,而白天,在外滩海景餐厅吃着四位数一顿的西餐。租住的洋房里陈列着各种奢侈品……


一年后,我结婚了,并且买了套房子。


一、魔都


2021 年,是我爸妈非常兴奋的一年。因为那年我去了上海,找了一份年薪 60 几万的工作。他们可以和邻居亲戚们炫耀他们的儿子了。



但我没什么感觉,对我而言,60 万只是手机上的一串数字。而且我知道,60 万并不多。因为那个整日听我汇报的、大腹便便的、看上去不怎么聪明的中年人拿着远比我高的薪水,我和他都一样,只是一条资本家的狗而已。


我一向不打算结婚,也不打算要孩子,也就意味着我不需要存钱。


我不会像我的领导那样为了在上海买一套 1200 万的房子而过得那样抠搜。我可以花一万多租一个小洋房,我可以在家里塞满那些昂贵的电器,我可以每周去国金、恒隆、新天地消费。我会买最新款的苹果全家桶,甚至连手机壳都要买 Lv 的。


但即使这样,钱还是花不完。


别人都因为没钱而烦恼,而我却因为钱花不完而烦恼。


后来,在机缘巧合之下,我开始了绚丽多彩的夜生活。在夜店,只需要花 1 万块钱,就可以当一晚上的王。这种快感是难以描述的。我成了 Myst、LolaClub、Ak、wm、pops 们的常客。开始了一段纸醉金迷的日子。


二、求职


找工作是一件再容易不过的事,因为几乎所有人都有工作。人只要降低要求,就一定能够找到工作。找工作难,是因为你渴望的工作岗位所需要的能力与你当前的能力并不匹配。


刚来到上海的时候,我非常自信,整天到处闲逛。朋友问我为什么不准备面试题?我说没有这种必要,他说不是很理解,我说那算了,不用理解。


因为技术上的知识,就那么多。你有自己的理解,有自己的经验,完全不需要胆怯,也没必要胆怯。合适就去,不合适就再看看。


能否找到好工作,更多的是你和面试官是否聊得来;你所做的事情,是否和这家公司的业务所匹配;你的经历、价值观和这家公司的愿景使命价值观是否契合。


我清楚我的能力究竟如何,我知道我应该去哪种公司。


但我需要了解这些公司的不同。于是我投了很多简历,大小公司都有。像携程、美团、得物、拼多多、小红书、微盟这些公司基本上都有去面试。跑了很多地方,徐家汇、北外滩、五角场、陆家嘴、奉贤岛、静安寺……。


全职面试,一天四五场,很忙。


但不到一周就确定好了 offer。


朋友说我是一个果断的人,我只是觉得没必要消耗时间。


明白自己的能力上限在哪,懂得在合适的环境最大化释放自己的价值。就一定可以收获一份合适的工作,或者说是一份事业。


找到自己的使命,就可战无不胜。


三、爱情


人缺什么,就会去寻找什么。


在 24 岁的年纪里,我缺少的不是梦想、金钱或者使命感,而是爱情。


我遇到了 Emma,她带给我的滋味,与我所有的想象都不一样。


我在上海,她在三亚。我们在相隔千里的城市,我坐着飞机去找她。结果因为晚了几分钟,于是我买了张全价票,硬是要在当天飞过去,因为我等这一天已经等了半个月了。


我们住在三亚的希尔顿。在三亚有两个希尔顿,结果她找错了,去了另一个希尔顿,后来我们很庆幸幸亏没有人开门。


虽然 Emma 笨笨的,但我喜欢她这样。


我们去了亚龙湾、天涯海角和南海观音。


三亚虽然是中国最南方的岛最南侧的城市,但有个外号是东北第四城。前三个是黑龙江、吉林和辽宁。一个城市的名字可能是错的,但外号绝对不会错。海南到处都是东北人。


爱情刚开始都会轰轰烈烈,我会给 Emma 精心挑选手链,去国金帮她挑选面膜,亲手给她写情书,那些情书至今还在我们的衣柜里。后来去全上海只有两个店的 Graff 买了一对情侣对戒。


我不介意给心爱的女人花钱。我觉得,一个男人把钱都花在哪儿了,就知道这个男人的心在哪儿。


当然,认识久了,特别是同居之后,这种轰轰烈烈式的浪漫就会褪去,更多的是生活中的点点滴滴。


有人说过一句话:


和喜欢的人在一起的快乐有两种,一种是一起看电影旅行一起看星星月亮分享所有的浪漫。还有一种则是分吃一盒冰淇淋一起逛超市逛宜家把浪漫落地在所有和生活有关的细节里。


但无论是前者还是后者,我都想和你一起,因为重要的是和你,而不是浪漫。


往事历历在目,虽然现在很难再有当时的那种感觉,可我更享受当下的温馨。


四、旅游


认识 Emma 之后,我们开始养成了旅游的习惯,每个月都会留出两三天时间一起出去旅游。


2 月,去了丽江。爬了玉龙雪山、逛了拉市海和木府。



玉龙雪山真的很神奇,在丽江市任何一个角落,都可以抬头看见白雪皑皑的雪山。


2 月 14 号是情人节,我在丽江的街头给 Emma 买了一束玫瑰。



3 月,我在上海,本来打算去北京,结果发生航变。然后再打算做高铁去杭州西湖,结果杭州顺丰出事了。哪都去不了,只能在上海本地旅游。去了陆家嘴的上海中心大厦。其实很早之前我就和朋友去过一次了,这次去的主要目的是和 Emma 锁同心锁。


我记得我们去的时候,已经是晚上九点五十,工作人员说十点关门,因为疫情原因,明天开始将无限期关停,具体开放时间等通知。如果现在上去,我们只能玩十分钟。没有犹豫,花了四百多买了两张票上去上了把锁。



那时我还不知道,这种繁华将在不久后彻底归于死寂。


后面几天,我们在上海很多地方溜达,外滩、豫园、迪士尼乐园。


迪士尼乐园是上海人流量最大的游乐场,几乎全年都是爆满。但这次疫情期间的迪士尼乐园的游客很少,很多项目都不需要排队。


但那天真的很冷,我们为了看烟花冻得瑟瑟发抖。夜晚的迪士尼城堡真的很美。



4-7 月上海封城,哪儿也没去。


8 月去了博鳌亚洲论坛。如果不是去开会的话,真没什么好玩的。



9 月打算坐轮船去广州徐闻、再坐绿皮火车去拉萨,因为我们认为这样会很浪漫。结果因疫情原因,没办法离开海南,只去了骑驴老街和世纪大桥旁边的云洞书店。




这座书店建在海边,进入看书要提前预约。


实际上没几个看书的,全是美女在拍照。


10 月去了济南大明湖和北京的天安门广场、八大胡同等地方。


很没意思,图我都不想放。


预约了故宫门票,并且打算去八达岭长城。但是在北京土生土长四十年的同事告诉我,他从来没去过故宫,并且不建议去八达岭长城。于是我把故宫的门票退了,并且把八达岭长城的计划也取消了。


11 月一整个月都在忙着买房子的事情,天天跟着中介跑,哪儿也没去。


五、封控、方舱、辞职与离沪


上海疫情暴发,改变了很多事。


这一部分我删掉了,涉及敏感话题。


总结来说就是:封控,挨饿,感染奥密克戎,进方舱,出舱,继续封控。


6 月 1 日,上海终于开放了。


虽然可以离开小区,但离开上海还是很难。


首先我需要收拾家里的东西,然后需要寄回我爸妈家,但当时所有的物流都处于瘫痪状态。当时我也想过直接丢掉,但我的东西真的很多,有二十多个箱子,差不多是我的全部家当了。


再之后需要购买飞机票,但所有的航班都在无限期航变。我给飞机场的工作人员打了很多次电话,没事就问。后来她们有些不耐烦了,告诉我,现在没有一架飞机能从上海飞出去,让我不要再打了。


可我不死心。


最后我选择了乘坐火车。


上海没有直达海口的火车,所以需要在南昌中转。由于我是上海来的,所以当地的政策是要隔离 7 天,除非你不出火车站。上海到南昌的火车是第一天下午到,而南昌到海口的火车是第二天的中午才出发。这也就意味着我要在南昌站睡一晚。而从上海到海口的整个路程,需要花费 3 天。这对以前的我来说是不可思议的。


但爱情赋予我的力量是难以想象的。


我在火车站旁边的超市买了一套被子就上了去南昌的火车,然后躺在按摩椅上睡了一晚。


第四天一大早,我终于到了海南。


六、求职


2022 年,是非常难找工作的。


特别是我准备搬到海南。海南是没有大型互联网公司的,连个月薪上万的工作都找不到。这样估计连自己都养不活。


所以我准备找一份远程工作。相比较于国内,国外远程工作的机会明显更多,可惜我英语不好,连一场基本的面试所需要的词汇都不够。所以我只能找一些国内的远程机会。


机缘巧合之下,终于找到了一家刚在美国融到资的创业公司。


他们需要一位带有产品属性和创造力的前端架构师。前前后后面了大概一周,我成功拿到 offer。这是我第一次加入远程团队。


印象中面试过程中技术相关的点聊了以下几点:



  • WebSocket 的弊端。

  • Nodejs 的 Stream。

  • TLV 相比 JSON 的优劣势。

  • HTTP 1.1/2 和 HTTP3(QUIC)的优劣势。

  • 对云的看法。

  • 真正影响前端性能的点。

  • 团队管理方面的一些问题。


因为公司主要业务是做云,所以大多数时间都是在聊云相关的问题。老板是一个拥有 20 年技术背景的老技术人,还是腾讯云 TVP。在一些技术上的观点非常犀利,有自己的独到之处。这一点上我的感受是非常明显的。


全球化边缘云怎么做?


现代应用的性能瓶颈往往不再是渲染性能。但各大前端框架所关注的点却一直是渲染。以至于现代主流前端框架都采用 Island 架构。


但另一个很影响性能的原因是时延。这不仅仅是前端性能的瓶颈,同时也是全球化分布式系统的性能瓶颈。


设想一下,你是做全球化的业务,你的开发团队在芝加哥,所以你在芝加哥买了个机房部署了你们的系统,但你的用户在法兰克福。无论你的系统如何优化,你的用户总是感觉卡顿。因为你们相隔 4000 多英里的物理距离,就一定要遭受 4000 多英里的网络延迟。


那该怎么办呢?很简单,在法兰克福也部署一套一模一样的系统就好了,用户连接距离自己最近的服务器就好了,这样就能避免高网络时延,这就是全球分布式。但如果你的用户也有来自印度的、来自新加坡、来自芬兰的。你该怎么办呢?难道全球 200 多个地区都逐一部署?那样明显不现实,因为成本太高了,99% 的公司都支付不起这笔费用。


所以很多云服务商都提供了全球化的云服务托管,这样就可以以低成本实现上述目标。


但大多数云服务商的做法也并不是在全球那么多地区部署那么多物理服务器。而是在全球主要的十几个地区买十几台 AWS 的裸机就够了。可以以大洲维度划分。有些做得比较好的云服务商可能已经部署了几十个节点,这样性能会更好。


这类云服务商中的一些小而美的团队有 vercel、deno deploy。这也是我们竞争目标之一。


招聘者到底在招什么人?


当时由于各种裁员,大环境不好,面试者非常多,但合适的人又非常少。后来据老板透露,他面试了 48 个人,才找到我。起初我还不信,以为这是常规 PUA,后来我开始负责三面的时候,在面试登记表中发现确实有那么多面试记录。其中竟然有熟人,也有一个前端网红。


后来我总结,很多人抱怨人难招,那他们到底在招什么人?


抛开技术不谈,他们需要有创造力的人。


爱因斯坦说:想象力比知识更重要。想象力是创造力的要素之一,也是创造力的前提。


那些活得不是很实际的人,会比那些活得踏踏实实的人有更大的机会去完成创造。


为什么要抛开技术不谈?因为职业生涯到了一定高度后,已经非唯技术论了。大家的技术都已经到了 80 分以上,单纯谈论技术,已经很难比较出明显差异,这时就要看你能用已有的技术,来做点什么,也就是创造力。这也是为什么我在文中没怎么谈技术面试的原因。


如果将马斯洛需求理论转化为工程师需求理论,那么同样可以有五级:




  1. 独立完成系统设计与实现。




  2. 具有做产品的悟性,关注易用、性能与稳定。




  3. 做极致的产品。对技术、组织能力、市场、用户心理都有很全面深入的理解。代表人物张小龙。




  4. 能够给世界带来惊喜的人。代表人物沃兹尼亚克。




  5. 推动人类文明进步,开创一个全新行业的人。代表人物马斯克。




绝大多数人都停留在前三个维度上,我也不例外。但我信奉尼采的超人主义,人是可以不断完善自我、超越自我、蜕变与进化的。既然沃兹尼亚克和马斯克能够做到的事情,我为什么不能去做呢?所以我会一直向着他们的方向前进。或许最终我并不能达到那种高度,但我相信我仍然可以做出一些不错的产品。


七、创业


搬到海南之后,时间明显多了起来。不需要上下班通勤,不去夜店酒吧,甚至连下楼都懒得去。三五天出去一次,去小区打一桶纯净水,再去京东超市买一大堆菜和食物,囤在冰箱。如果不好买到的东西,我都会选择网购。我一般会从山姆、网易严选、京东上面网购。


九月份十月份,海南暴发疫情,一共举行了十几轮全员核酸检测,我一轮都没参加。主要是用不到,出不去小区没关系,外卖可以送到小区门口,小区内的京东超市也正常营业,丝毫不影响我的日常起居。


所以我盘算着要做些事情。刚好之前手里经营着几个副业,虽然都没有赚到什么钱。但是局已经布好了。其中一个是互联网 IT 教育机构。是我和我的合伙人在 20 年就开始运营的,现在已经可以在知乎、B 站等渠道花一些营销费用进行招生。我们定价是 4200 一个月(后来涨价到 4800),一期教三个月左右。通过前几期的经验,我们预估,如果一期有十几个人到二十个人报名,就有大概将近 10 万多的收入,还是蛮可观的。所以我准备多花些精力做一下这个教育机构。


培训的内容是前端,目标是零基础就业。因为我在前端这方面有很大的优势,而且当时前端招聘异常火热。初级前端工程师需要掌握的技能也不多,HTML、CSS、JavaScript、Git、Vue,就这么几块内容而已。相比较成型慢的后端,前端是可以速成的,所以我认为这条路非常可行。


我负责的这一期,前期招了将近 20 人。



而下一期还没开课,已经招到了 5 个人。一切都向着好的方向发展。


但很快,十月、十一月左右,招生突然变难了。连知乎上咨询的人都明显变少了。我们复盘了下原因,原来是网上疯传现在是前所未有的互联网寒冬,加上各种大厂裁员的消息频发,搞得人心惶惶。甚至有人扬言,22 年进 IT 行业还不如去养猪。


因为我的合伙人是全职,他在北京生活花费很高,加上前期没有节制地投入营销费用,他已经开始负债了。但是营销费用是不能停的,一旦知+ 停掉,就很难继续招生。


后来经过再三讨论,最终还是决定先停掉。虽然我们笃定 IT 培训是红海,但感觉短期的大环境不行,不适合继续硬撑下去。



目前我带着这期学员即将毕业,而我们也会在 23 年停止招生。


培训的这个过程很累,一边要产出营销内容,一边要完善和设计教材、备课、一边还要批改作业。同时每天完成两个小时的不间断直播授课,不仅仅是对精力的考验,也是对嗓子的考验。


虽然创业未成,但失败是我们成长路上的垫脚石,而且这也谈不上什么失败。


我把经验教训总结如下,希望对你有所启发。


1. 市场和运营太重要了


中国有句谚语叫“酒香不怕巷子深”。意思是如果酒真香,不怕藏于深巷,人们会闻香而至。除了酒,这句话也隐喻好产品自己会说话,真才子自有人赏识。


但实际上,这是大错特错。


随着我阅历、知识、认知的提升,我越发觉得,往往越是这种一说就明白、人人都信的大道理,越是错的。


傅军老师讲过一句话:感受不等于经验、经验不等于知识、知识不等于定律、定律不等于真理。我深受启发。在这个快速发展的信息时代,必须保持独立思考的能力。


我思考的结果是:酒香也怕巷子深。


连可口可乐、路易威登、香奈儿这种世界顶级品牌每年都需要持续不断地花费大量的费用用于营销。由此可见营销的重要性,营销是一定有回报的。


我们的酒要香,同时也不能藏在巷子里。而且,我们的酒不需要特别香,只要不至于苦得无法下咽,就能卖出去。畅销品不一定就是质量好,某些国产品牌的手机就是一个简单的例子。


所以,只需要保证产品是 60 分以上就足够了,剩下的大部分精力都应该放在营销上面。绝大多数只懂得专注于打造 100 分产品的创业公司,基本上都死了。


正确的路径应该是:Poc、Alpha、Beta、MVP。就像下图所示,造车的过程中,产品在任何一个时间点都是可用的。



另外,决定一个公司成功的,一定是运营部或者市场部,绝对不是技术部。但是技术部有可能会让一个公司走向失败。如果一个公司要裁员,技术部门很可能首当其冲。当然这也要看公司文化,但大部分的 Boss 普遍更看重运营与市场。


2. 不要等万事俱备再开始


在创业早期,我们的主要招生渠道是 B 站,但我没有短视频经验。于是我买了暴漫 B 站 UP 主的课程。



本来以为买了课程就意味着学会了做视频,但实际上不是这样的。


做好一个 UP 主需要掌握非常多的知识,选题、文案、表达、拍摄、剪辑、互动,各个环节都有大学问。我很快发现不可能把整个课程全部研究透再去做视频。我需要保持频率地更新视频才能有更多人的关注。


随着业务的发展,后面主战场转到了知乎,工作内容也变成了软文和硬广。UP 主的很多技能还没来得及实践就被搁置了。


知乎也有一个写作训练营,学费大概也要三四千的样子。



我听了试听的三节课,感觉要学习的东西一样很多,所以我没有买这个课程。因为我知道我没有那么多精力。


最重要的是,我发现即使我没有什么技巧和套路,我写的软文一样有很多浏览量和转化率。知+平台的数据可以直接告诉我这篇文章或者回答写得好不好。


同时,很多相关的知识不是一成不变的,它们不是传统知识,需要从实际事物中脚踏实地地学习,我们没办法系统学习。


我们不可能做好所有的准备,但需要先去做。人生也该如此,保持渐进式。


3. 接受糟糕,不要完美主义


初创公司很多东西都是混乱的,缺乏完善的系统和结构,一切似乎都是拍脑袋决定。


报销很混乱、营销费用很混乱、内容管理很混乱、合同很混乱;甚至连招生清单都和实际上课的人对不上……


但我发现这是一个必须接受的现状,如果一切都井井有条,那就不是初创公司了。所以必须要适应这种乱糟糟、混乱的环境。


八、结婚与买房


朋友说我是一个果断的人。


因为从认识,到结婚、买房,一共用了不到一年时间。


其实起初我是强烈不建议买房的,我不看好中国的楼市,而且我们一个月花几千块租房住得非常舒服。而且我的父母在北方的城市也已经有两套房子了。但 Emma 认为没有房子不像个家,没安全感。安全感对一个女人实在太重要了,我想以她对我的了解,她怕我哪天突然悄无声息地就离开她。想了想,确实是这样,所以我就买了。


我们看了海口很多套房子,本来计划买个小两居,但看了几次,都觉得太紧凑了。我是要在家工作的,我有一张很大的双人桌,要单独留一个房间来放它。最后挑了一个三居室的房子。这种感觉和买电脑差不多,本来一台普通 Thinkpad 的预算,最终买了个 MacBookPro 顶配。



房子很漂亮,我们也很喜欢。



不过呢,同时也背负了两百万的贷款。不能再像以前那样随便花钱了。


买房只需要挑房屋布局、装修和地段,其他不需要关心。


买房贷款的注意事项:选择分期,选择还款周期最久的,不要提前还。今年由于疫情原因,房贷大概是 4.5 个点,比前几年要低。买房前保持一年流水,收入大于月供 3 倍。


还需要注意,买完房,还要考虑装修费用和家电的费用。


非刚需不建议买房,刚需的话不会考虑买房的弊端。


至于为什么结婚?


我和 Emma 都是不婚主义者。但是买房子如果不结婚,就只能写一个人的名字。但我们都不愿意写自己的名字。最后没办法,决定去办理结婚证。


但我们没有举办传统的婚礼,我们都不喜欢熙熙攘攘、吵吵闹闹。我们只想过好我们自己的生活,不希望任何人来打扰我们。


这辈子我搬了至少十次家,只有这次是搬进自己的家。


九、学习


今年在学习上主要有四个方面:



  • Web3。

  • 英语。

  • 游戏开发。

  • 自媒体。


目前对我来说,Web3 是最重要的事情。虽然我还没有 All in web3,但也差不多了。有人说 Web3 是骗局,但我认为不是骗局。


我承认 Web3 有非常多的漏洞,比如 NFT 的图片压根没上链,链上存储的只是一堆 URL。同时几乎所有用户都没有直接与区块链交互,而是与一堆中心化的平台交互。比如我们使用 MetaMask,其实数据流过了 MetaMask 和 Infura。目前的 Web3 并没有完全去中心化。


没有一个新鲜事物在一诞生就是完美的。正是由于这些不完美的地方,所以才需要我们去完善它们。


目前我已经加入了一个 Web3 团队。虽然团队不大,但是他们做的事情非常吸引我。Web3 是一个高速发展的火箭,对现在的人来说,你先上去再说,何必在乎坐在哪个位置上?


我很期待 2030 年的 Web3。


如果要进入 Web3,需要学习技术有很多,我把一些重要的技术栈列举如下:



  • Web3.js or Ethers.js or Wgami:与区块链交互的前端 SDK。

  • Solidity:智能合约编程语言。

  • Hardhat:以太坊开发环境。

  • IPFS:Web3 文件存储系统。


除了上述的技术之外,更多的是概念上的理解,比如 NFT、GameFi 相关的诸多概念等。


学习英语是因为我打算出国,同时我现在的工作环境也有很多英语,我不想让语言成为我继续提升的障碍。


至于游戏开发和自媒体,纯粹是想赚钱。


游戏开发主要是微信小游戏,靠广告费就能赚钱。微信小游戏的制作成本很低,两个人的小型团队就可以开发数款微信小游戏。而且赚钱和游戏质量并不成正比。跳一跳、羊了个羊都是成功案例。当然我的目标不是做那么大,大概只需要做到它们几百分之一的体量就非常不错了,要知道微信有将近 10 亿用户,总有人会玩你的游戏。


另外我学习自媒体和微信小游戏的原因差不多。自媒体的盘子越来越大。在 B 站、抖音这些短视频平台上,有着上千亿甚至更多的市场。这给更多人创造了创造收入的空间。


只要去做,总会有所收获。


在学习的过程中,我会记录大量笔记以及自我思考。但很少会更文。


大概在 9 月份,我参加了掘金的两次更文活动,输出了一些内容。


这个过程很累,但也有所收获。






后来考虑到时间和精力问题,所以很少更文了。


很遗憾,掘金目前只能依靠平台自身给创作者奖励,收入非常有限。如果掘金能够将变现程度发展成像 B 站、公众号或抖音那样,估计也会有更多的创作者加入。连 CSDN、博客园这些老牌产品都做不到。究其原因,还是这种性质的产品受众没有那么广泛,盘子太小了,体量自然无法涨上去。希望未来掘金能够找到属于自己的商业模式。


最后还是很感谢掘金这个平台,给了技术人们一个分享交流的空间。


十、未来


未来三年的计划是出国读硕,并全面转入 Web3 领域。


我不看好中国的经济,并且感觉在中国做生意会越来越难做。而且我更喜欢西方文化、氛围和环境。


在经历了国内疫情反复封控之后,我更加坚定了出国的打算。我不想继续生活在大陆。


我想看看外面的世界,同时系统化地提升一下自己的 CS 知识。


目前出国的思路是申请混合制研究生,F1、OPT、H1B、Green Card。目标是一所在 Chicago QS TOP500 垫底的高校。


另一条出国的路线是直接出国找一份美国的工作,但对目前的我来说是相当难,主要还是因为语言。


之所以是未来三年的计划,也是因为我的英语实在太差,目前按照 CEFR 标准,只达到了 A2。


按照 FSI 英语母语者学习外语的难度排名,中文这类语言是最难学习的。反过来,中文母语者学习英语,也是最难的。真正掌握大概需要 2200 小时,如果每天学习 3-4 小时的话,需要 88 周,将近两年。


FSI 的图片我删掉了,因为那张图上的中国版图有争议。


我的语言天赋很一般,甚至有些差。所以学习效果并不理想。我也不打算短时间内冲刺英语,因为那不太靠谱。我选择花更久的时间去磨它。


之前也想过去 Helsinki,但那边收入不高,税收太高。纠结了很久,还是觉得现在还年轻,多少还是应该有些压力的,去那种地方实在太适合养老了。


以上并不是故事,是我的亲身经历,分享出来的初衷是为大家提供更多职业和人生的思路与思考。希望对你有所帮助!




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

做软件开发20年,我学到的20件事

本文已获得原作者Justin Etheredge的翻译授权 原文链接 写在前面  你即将读的这篇文章会给你很多建议,向前辈学习对于获得成功是很有帮助的,但我们经常会忘记很重要的一点,就是几乎所有的建议都是有其特定场景和上下文的,但当人们给出建议的时候却很少带着...
继续阅读 »

本文已获得原作者Justin Etheredge的翻译授权 原文链接


写在前面


 你即将读的这篇文章会给你很多建议,向前辈学习对于获得成功是很有帮助的,但我们经常会忘记很重要的一点,就是几乎所有的建议都是有其特定场景和上下文的,但当人们给出建议的时候却很少带着上下文。
 一家一直以来以“收费低”而获得成功,并经营了二十年的公司可能会给出的建议是“你只需要多收些钱!”。
 “你需要把所有应用都构建成微服务”这个建议可能来自于一个靠快速构建的单体应用获得成千上万客户,在开始遇到规模问题时转向微服务的团队。


 如果不理解上下文,这些建议就毫无意义,甚至是有害的。如果这些人早些时候听从了自己的建议,那结果如何也很难讲。我们很可能处在自己经历的顶峰,但却在用现在的视角看待别人的问题。


 首先我想介绍一下我的建议从哪儿来,我职业生涯的前半段是一名软件工程师,为各种小型企业和初创企业工作,然后进入咨询行业,并在一些非常大的企业工作。后来自己创建了Simple Thread,团队从2人发展到25人。10年前,我们主要与中小型企业合作,现在与大大小小不同的企业合作。


我的建议来自于这些人:



  1. 几乎总是在小而精干的团队中,必须用很少的资源做很多事情。

  2. 重视可工作软件而不是特定的工具。

  3. 在维护多个系统的同时,一直有新的项目要启动。

  4. 把工程师的生产力看得比大多数其他因素都重要。
    我过去20年的经历塑造了我对软件的看法,并引导我形成了一些信念,我试图将这些信念精简并整理成一个列表,希望你会觉得它对你有所帮助。


我的列表


1.“我依然知道的不够多”


 “你怎么会不知道BGP是什么?“ “你从来没听说过Rust吗?”。我们很多人经常听到过类似的话。很多人喜欢软件开发的一个重要的原因是我们是终身学习者,软件开发中,无论你从哪个方向看,都有广阔的知识前景在各个方向发展,并且每天都在扩大。这意味着与其他职业中花费几十年的人相比,你即使已经花费了数十年,但可能仍然有巨大的知识断层,有很多新知识需要学习,你可能因为担心不能胜任而陷入焦虑。你越早意识到这一点,你就能越早摆脱这种时常的焦虑,从而放平心态,乐于向别人学习以及教授他人。


2.软件最难的部分是构建正确的东西


 我知道这已经是陈词滥调了,但是还是有很多软件工程师不相信这一点,因为他们认为这会贬低他们所做的工作。我个人认为这是无稽之谈。相反,它强调了我们工作环境的复杂性和非理性,这更突出了我们所面临的挑战。你可能可以设计出全世界技术上最牛的东西,但却没有人愿意使用它,这种事经常发生。设计软件主要是一种倾听活动,我们经常不得不一半是软件工程师,一半是心理学家,一半是人类学家。在这个设计过程中投资自己,无论是通过专门的用户体验团队的成员还是通过简单的自学,都会带来巨大的回报。因为构建错误软件的成本可不仅仅是浪费了工程师的时间。


3.最好的软件工程师会像设计师一样思考


 优秀的软件工程师会深入考虑他们代码的用户体验。他们可能不会用这些术语来考虑它,而是考虑它是外部API、编程式API、用户界面、协议还是任何其他接口;优秀的工程师会考虑谁会使用它,为什么会使用它,如何使用它,以及对这些用户来说什么是重要的。牢记用户的需求才是好的用户体验的核心。


4.最好的代码是没有代码,或者不需要维护的代码


 任何职业的人解决问题的过程中都会在自己擅长的方面犯错误,这是人的本性。大多数软件工程师在编写代码免不了会犯错误,尤其是当还没有可行的非技术性解决方案时。工程团队总是倾向于在已经有很多轮子的时候重新发明轮子。有很多原因让你自己重新做一个轮子,但一定要警惕有毒的“Not invented here”综合症,不能闭门造车,妄自尊大,尽量复用和寻找非技术性解决方案。


5.软件是达到目的的一种手段


 任何软件工程师的主要工作都是交付价值。很少有软件开发人员能理解这一点,更少人能内化它。真正的内在化会带来一种不同的解决问题的方式,以及一种不同的看待工具的方式。如果你真的相信软件是屈从于结果的,你就会准备好真正找到“适合这项工作的工具”,而这个工具可能根本不是软件。


6.有时候你不得不停止磨刀,开始切东西


 有些人倾向于一头扎进问题中,然后开始编写代码解决问题。有些人却倾向于花大量时间研究和调查,但却让自己陷进问题中。在这种情况下,给自己设定一个最后期限,然后开始探索解决方案。当你开始解决这个问题的时候,你会很快学到更多的东西,这将引导你迭代形成一个更好的解决方案。


7.如果你不能很好地把握全局的可能性,你就无法设计出一个好的系统


 这是我在每天的工作中不断努力的事情。与开发者生态保持同步是一项巨大的工作,但了解开发者生态中的可能性却是至关重要的。如果你不了解在一个给定的生态系统中什么是可能的,什么是可用的,那么你就不可能设计出一个合理的解决方案来解决所有的问题,除非是最简单的问题。总而言之,要警惕那些很长时间没有编写任何代码的系统设计者。


8.每个系统最终都很糟糕,克服它吧


 比雅尼·斯特劳斯特鲁普(Bjarne Stroustrup)有一句话是这样说的: “世界上只有两种语言,一种是人们抱怨的语言,另一种是没人用的语言。”。这也可以扩展到大型系统。不存在“正确”的架构,你永远无法偿还所有的技术债务,你永远无法设计出完美的界面,你的测试总是太慢。这不是个能让事情变得更好的借口,而是一种让你看问题的方式。少担心优雅和完美;相反,要努力持续改进,创建一个你的团队喜欢并可持续提供价值的环境。


9.没人去问“为什么”


 抓住任何机会去质疑那些“一直以来都是这样做的”假设和方法。有新队员加入?那就注意他们在哪里感到困惑,他们问了什么问题。有一个没有意义的新功能需求?确保你理解了目标,以及是什么驱动了这种功能的需求。如果你得不到一个明确的答案,继续问为什么,直到你明白。


10.我们应该更加关注如何避免0.1x程序员,而不是寻找10x程序员


 10倍的程序员其实是一个愚蠢说法。认为一个人可以在一天内完成另一个有能力、勤奋、同样有经验的程序员可以在两周内完成的任务是愚蠢的。我见过程序员抛出10倍的代码量,然后你必须用10倍的时间来修正它。一个人成为10倍程序员的唯一方法就是将他与0.1倍程序员进行比较。有些人浪费时间,不寻求反馈,不测试他们的代码,不考虑边界情况等等。我们更应该关心的是让0.1x程序员远离我们的团队,而不是找到神秘的10x程序员。


11.高级工程师和初级工程师之间最大的区别之一就是他们对事情应该如何发展形成了自己的观点


 没有什么比高级工程师对他们的工具或如何构建软件一无所知更让我担心的了。我宁愿有人给我一些强烈的反对观点,也不愿他们没有任何观点。如果你正在使用你的工具,并且你并不喜欢或讨厌它们,那么你就需要体验更多。您需要探索其他语言、库和范式。没有什么方法比积极地寻找别人如何用不同的工具和技术来完成任务能更快地提升你的技能了。


12.人们不是真的想要创新


 人们经常谈论创新,但他们通常寻找的是廉价的胜利和新奇的东西。如果你真的在创新,改变人们做事的方式,那么大部分的反馈都是负面的。如果你相信你正在做的事情,并且知道它真的会改善事情,那么就做好长期斗争的准备。


13.数据是系统中最重要的部分


 我见过许多对数据完整性要求很高的系统。在这样的系统中,任何发生在关键路径之外的事情都会创建部分数据或脏数据。将来处理这些数据可能会成为一场噩梦。请记住,您的数据可能比代码库存在的时间更长。把精力花在保持它的有序和清洁上,从长远来看它会得到很好的回报。


14.寻找技术”鲨鱼“


 许多留下来的老技术是”鲨鱼“,而不是”恐龙“。他们能够很好地解决问题,并在技术不断快速变化的今天生存了下来。只有在有一个很好的理由的情况下,再去替换它们。这些工具不会华而不实,也不会令人兴奋,但是它们可以完成工作,避免很多不必要的不眠之夜。


15.不要把谦卑误认为无知


 有很多软件工程师在没有被提问的时候,是不怎么发表意见的。永远不要以为别人没有他们的观点摆在你面前,你就觉得他们没有什么观点。有时候最吵的人恰恰是我们最不想听的人。与你周围的人交谈,寻求他们的反馈和建议。你会有意外收获。


16.软件工程师应该定期写作


 软件工程师应该定期写博客,写日志,写文档,去做任何保持书面沟通技能的事情。写作可以帮助你思考问题,并帮助你与团队和未来的自己更有效地沟通。良好的书面沟通能力是任何软件工程师都需要掌握的最重要的技能之一。


17.保持流程尽可能精简


 如今,每个人都想变得敏捷,“敏捷”就是把事情分成小块,学习,然后迭代。如果有人试图把更多的东西塞进去,那他很可能是在卖东西。想想你有多少次听到来自你最喜欢的技术公司或大型开源项目的人在吹嘘他们的Scrum流程有多棒?在你知道你需要更多的东西之前,请依靠流程。相信你的团队,他们会完成任务。


18.软件工程师,像所有人一样,需要有归属感


 如果你把某人和他的工作成果分开,他就不会那么在乎他的工作。我认为这几乎是同义反复。归属感是跨职能团队工作得如此出色的主要原因,也是DevOps变得如此流行的原因。这并不全是关于交接和低效的问题,而是关于从开始到结束去参与和享受整个过程,并直接负责交付价值。让一群充满激情的人完全拥有设计、构建和交付一个软件(或者其他任何东西)的所有权,奇妙的事情就会发生。


19.面试对于判断一个团队成员是否优秀几乎毫无价值


 面试最好是试着了解对方是谁,以及他们对某一特定专业领域有多大兴趣。试图弄清楚一个团队成员会有多好是徒劳的努力。相信我,一个人有多聪明或多有知识也不能很好地表明他们将是一个优秀的团队成员。没有人会在面试中告诉你,他们会不可靠,会骂人,会夸夸其谈,或者从不准时出席会议。人们可能会说他们在这些事情上有“信号”……“如果他们在第一次面试时就问请假,那么他们就永远不会在那里了!” 但这些都是胡扯。如果你使用这样的信号,你只是在猜测,并将优秀的候选人拒之门外。


20.始终努力构建一个更小的系统


 有很多的力量将推动你预先建立更大的系统。预算分配,无法决定哪些功能应该被削减,希望交付系统的“最佳版本”。所有这些事情会迫使我们过度建设。你应该与之抗争。在构建系统的过程中,你会学到很多东西,最终迭代得到的系统将比你最初设计的系统要好得多。令人惊讶的是,这很难说服大多数人。


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

工作7年的程序员,明白了如何正确的"卷"

大家好,我是大鹅,一名在互联网行业,工作了7年的程序员。 今天和大家聊聊工作7年后,一些感悟吧。 背景 近两年,出台和落地的反垄断法,明确指出要防止资本无序扩张。 这也就导致现在的各大互联网公司,不能再去染指其他已有的传统行业,只能专注自己目前存量的这些业务。...
继续阅读 »

大家好,我是大鹅,一名在互联网行业,工作了7年的程序员。


今天和大家聊聊工作7年后,一些感悟吧。


背景


近两年,出台和落地的反垄断法,明确指出要防止资本无序扩张。


这也就导致现在的各大互联网公司,不能再去染指其他已有的传统行业,只能专注自己目前存量的这些业务。或者通过技术创新,开辟出新的行业。


但创新这种东西,可遇不可求,互联网进入到目前这个阶段,能做的,基本都已经有公司在做了。


互联网创造出来的工作机会,例如程序员、产品经理、运营等等,已经进入了一个存量市场的时代。



无意义的卷


我反对的“卷”,是通过疯狂加班,以工作时长换取更高产出的“卷” ,而不是个人学习成长这一方面的“卷”。


这个类型的“卷”,不论是对自己,还是对于这个行业所带动的就业机会,都是不可持续且有害的事情。


对自己:


短期内,因为卷工作时长,的确可以获得了更高的产出,顺带着获得了更多表现自己和让上级看到的机会,这一点是符合逻辑的。


但从长远来看,到了一定岁数,体力肯定不如年轻人了,这时候再被年轻人卷,那真是冤冤相报何时了。。



对就业机会:


正常是下午6点下班,卷的人晚上11-12点才走。


一个业务需求正常开发一个月,硬是压缩到半个月,把1个人当成2个人用。


这样做会进一步缩减这个存量市场的工作机会,因为老板们发现裁掉一部分人,依然可以正常运转。


对于老板来说,他更赚了,因为减少了成本


对于打工人来说,很亏,因为付出了大于1人力的工作时长和产出,得到的却还是1人力的回报。



如果不“卷”,能得到什么?


1、首先应该可以释放出一部分新的工作机会出来,让这个行业可以容纳更多的人。


因为之前通过疯狂加班,1个人干2个人的事情这种情况得到制止,意味着需要新增人员才能完成以前的工作量。


2、不用疯狂加班,身体好了,也有时间了。



我认为正确的“卷”


背景交代清楚了,回到主题,如何正确的“卷”


这里没有什么长篇大论,我认为正确的卷,应该是想清楚自己的方向,坚持不断的学习和积累,让自己拥有这个领域足够的专业度和深度,这才是自己的核心优势。卷的是毅力和积累,而不是体力。


同时,还可以对外输出自己的知识,让自己在这个行业拥有一定的知名度,这可以很大的提高自己的抗风险能力。



写在最后


我个人的力量有限,我不觉得我能影响多少人,但能影响一点,也总是好的。


也希望看到这个文章的你,能一起帮助这个行业,让它的风气,变得更好一点。


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

从基本的知识技能出发,分析Android工程师到架构师的转变

从基本的知识技能出发,分析一下Android工程师到架构师的转变是什么,该如何成为一个架构师 首先成为一名Android工程师 成为一名Android工程师需要掌握以下基本技能和知识: Java编程语言: Java是Android应用程序开发的主要编程语言...
继续阅读 »

从基本的知识技能出发,分析一下Android工程师到架构师的转变是什么,该如何成为一个架构师



首先成为一名Android工程师


成为一名Android工程师需要掌握以下基本技能和知识:



  1. Java编程语言: Java是Android应用程序开发的主要编程语言。您需要掌握Java的基本概念、语法和编程范例。

  2. Android应用程序开发:学习Android应用程序开发需要掌握Android框架、Android SDK、Gradle构建系统等基本概念。您需要了解如何创建Android应用程序、构建用户界面、处理用户输入、访问网络、存储数据等。

  3. Android开发工具:为了开发Android应用程序,您需要了解Android Studio、Eclipse等集成开发环境(IDE),以及其他Android开发工具,如布局编辑器、调试工具等。

  4. 版本控制工具:版本控制工具是一种管理代码和开发项目的工具,如Git和SVN。学习如何使用这些工具可以帮助您更好地管理和维护您的代码。

  5. 软件设计和模式:了解软件设计和模式可以帮助您编写可维护、可扩展的代码。您需要掌握MVC、MVP、MVVM等设计模式,以及OO(面向对象)和设计原则,如SOLID原则。

  6. 学习社区:加入Android开发社区可以帮助您了解最新的开发趋势和最佳实践,解决问题和获取帮助。您可以加入Android开发者论坛、GitHub、Stack Overflow等社区。


对于这些基础知识技能,我们可以规划以下步骤:



  1. 学习Java编程语言:学习Java编程语言的基础知识、语法和范例。

  2. 学习Android开发:学习Android开发,包括Android框架、Android SDK、Gradle构建系统等基础知识。

  3. 练习开发:练习开发Android应用程序,开发简单的应用程序并添加不同的功能。

  4. 学习软件设计和模式:学习软件设计和模式,以编写可维护、可扩展的代码。

  5. 学习版本控制工具:学习版本控制工具,如Git和SVN,以便更好地管理和维护您的代码。

  6. 加入Android开发社区:加入Android开发社区,学习最新的开发趋势和最佳实践,获取帮助和解决问题。

  7. 参加培训和课程:参加Android开发的培训和课程可以帮助您加速学习和提高技能。


蜕变为高级Android工程师


要成为一名高级Android工程师,需要进一步提升自己的技能和知识,并有足够的实践经验,其实大家能看出来,经验是成为Android高级工程师的必备项。


那我们可以从以下方面进行蜕变:



  1. 持续学习:Android开发技术不断更新和进化,持续学习是必须的。您可以阅读Android官方文档、博客、开发者论坛等,了解最新的技术和开发趋势。

  2. 提高编程能力:除了掌握Java编程语言,您还应该了解Kotlin、Flutter、React Native等其他移动开发技术,并且要熟练掌握多种编程语言。

  3. 深入了解Android框架:深入了解Android框架的内部机制,包括Activity生命周期、Fragment、View、Service、BroadcastReceiver、Content Provider等等,对于解决问题和优化性能非常重要。

  4. 掌握Android性能优化技巧:为了使应用程序运行更流畅和更快速,需要掌握各种性能优化技巧,例如渲染优化、内存管理、网络优化、布局优化等等。

  5. 掌握软件设计和架构:掌握设计模式和软件架构,可以帮助您编写高效、可维护和可扩展的代码。了解MVP、MVVM、Clean Architecture等架构模式可以提高代码质量。

  6. 参与开源项目:参与开源项目可以让您接触到更广泛的技术和工具,并可以获得更多的实践经验。

  7. 培养团队协作和沟通技能:高级Android工程师通常需要与其他团队成员一起工作,需要具备优秀的沟通和协作技能,以确保项目顺利进行。

  8. 获得证书:获得相关的证书可以提高您的专业水平,例如Google的Android开发者认证证书。


总之,要成为一名高级Android工程师需要持续学习和不断实践,并且具备深入了解Android框架、性能优化技巧、软件设计和架构、团队协作和沟通技能等多方面的能力。


高级Android工程师怎么成为架构师呢?


要成为专业的Android架构师,需要在Android开发方面积累更多的经验和知识,同时需要在软件架构方面有更深入的理解和能力


平时可以积累架构方面的知识,从宏观到微观,从架构到需求,做事要有一步三思考,将扩展性玩的6,集合业务,架构师的职业在一定程度上是为公司节省成本,当你的项目开发迭代变得容易,bug易于管理,扩展随插随拔,你的架构能力也就OK了,首先可以从以下几点去做准备。



  1. 深入了解软件设计和架构:作为架构师,您需要具备设计和架构的能力。可以通过阅读相关的书籍和文章,了解设计模式、软件架构模式等理论知识,还要结合实践进行深入理解。

  2. 熟悉常用的架构模式:MVP、MVVM、Clean Architecture等是常用的Android架构模式,熟练掌握这些模式可以帮助您设计高效、可维护和可扩展的应用程序。

  3. 学习如何进行系统架构设计:系统架构设计需要全局考虑,包括数据流、组件之间的关系、可扩展性等等。学习如何进行系统架构设计可以帮助您更好地组织应用程序的结构,提高代码质量。

  4. 了解最佳实践:Android开发领域有许多最佳实践,例如避免使用过于庞大的Activity、Fragment、减少不必要的内存分配等等。了解这些最佳实践可以帮助您在设计应用程序架构时更加谨慎。

  5. 了解Android开发生态系统:Android开发生态系统包括许多工具、库和框架,例如RxJava、Retrofit、Dagger等等。了解这些工具的特点和优缺点可以帮助您更好地设计应用程序架构。

  6. 参与开源项目:参与开源项目可以让您接触到更广泛的技术和工具,并可以获得更多的实践经验。

  7. 培养团队协作和沟通技能:作为架构师,您需要与其他团队成员进行沟通和协作,需要具备优秀的沟通和协作技能。


总之,成为专业的Android架构师需要掌握软件设计和架构、熟悉常用的架构模式、了解最佳实践、学习如何进行系统架构设计、了解Android开发生态系统等多方面的能力。同时,需要不断学习和实践,积累经验,提高自己的能力。


让这些书籍提升你自己


Android 开发基础:



  • 《第一行代码》:作者是郭霖,介绍了Android开发的基础知识和实践方法,适合初学者。

  • 《Android编程权威指南》:作者是Bill Phillips、Chris Stewart和Kristin Marsicano,介绍了Android开发的全面知识,包括Java编程、UI设计、数据存储和网络通信等方面。

  • 《Java编程思想》:作者是Bruce Eckel,介绍了Java编程的基础知识和编程思想,是Java编程的入门经典。

  • 《Effective Java》:作者是Joshua Bloch,介绍了Java编程的最佳实践,对于提升Java编程技能非常有帮助。


Android 高级进阶



  • 《Android源码设计模式解析与实战》:作者是杨波,介绍了Android源码中常见的设计模式及其应用场景,适合进阶学习。

  • 《Android应用架构设计指南》:作者是贺博,介绍了常见的Android架构模式及其应用方法,以及如何进行应用架构设计,适合有一定经验的开发者。

  • 《Android开发艺术探索》:作者是任玉刚,介绍了Android开发中常见的技术和实践,深入浅出地讲解了一些高级的技术原理和实现方式。

  • 《Android系统源代码情景分析》:作者是徐医生,介绍了Android系统的架构和源码分析,深入剖析了Android系统中的各种核心组件和机制。


Android 架构师



  • 《软件架构设计》:作者是Martin Fowler,介绍了软件架构设计的基本原则和方法,适合初学者。

  • 《大型网站技术架构》:作者是李智慧,介绍了大型网站的架构设计和技术选型,适合深入学习。

  • 《Android源码情景分析》:作者是余晟,介绍了Android源码的内部实现和设计原理,对理解Android系统架构和开发设计有很大帮助。

  • 《软件架构实践》:作者是贾卓辉,介绍了软件架构实践的方法和案例,包括架构模式、设计原则、应用场景和架构风格等方面的内容。

  • 《大型网站架构模式》:作者是曹政,介绍了大型网站的架构设计和技术实践,包括架构模式、性能优化、高可用性和数据分析等方面的内容。


关于架构入门,特别推荐


《架构整洁之道》,是由著名软件工程师 Robert C. Martin(Bob大叔)所著,是一本介绍软件架构设计和开发实践的书籍。这本书的思想主要是基于作者多年的实践经验和教学经验,提出了一系列关于软件架构和开发实践的最佳实践和原则,如单一职责原则、依赖倒置原则、接口隔离原则、里氏替换原则、开闭原则、迪米特法则等。通过这些原则的实践,可以使软件系统更加灵活、可扩展、易维护和易测试。


总体来说,这本书是一本非常优秀的软件开发书籍,对软件架构设计和开发实践提出了很多有用的建议和指导。它的思想可以帮助开发者避免一些常见的错误和陷阱,提高代码质量和软件设计水平。


除了原则和实践,这本书还介绍了很多实际案例和代码示例,让读者可以更加深入地理解和应用其中的思想和方法。不过需要注意的是,这本书内容比较深入和技术性较强,可能对于初学者来说会有一定的难度。如果你已经有一定的开发经验并且想要提高自己的软件架构和开发能力,那么这本书是非常值得阅读和学习的。


最后


一起讨论吧,你们认为这个转变还需要什么?当然列举的学习步骤和书籍都是片面的,还有更好的方法吗?


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

上手试试 Compose for ios

前言 前段时间看了阿黄哥的一篇介绍Compose for ios 的文章 Compose跨平台第三弹:体验Compose for iOS(黄林晴) juejin.cn/post/719577… 才知道 compose 已经可以跨ios端了,自己也打算上手试...
继续阅读 »

前言


前段时间看了阿黄哥的一篇介绍Compose for ios 的文章



Compose跨平台第三弹:体验Compose for iOS(黄林晴)


juejin.cn/post/719577…



才知道 compose 已经可以跨ios端了,自己也打算上手试试。等自己实际上手之后,才发现有很多坑,尤其是配置环境方面,所以打算写一篇文章记录一下。


开始


接下来,我会从新建一个项目,依赖配置,环境配置,一直到可以使用Compose 写一个Hello WordDemo, 这样的步骤来介绍一个我曾遇见的坑以及解决方法。


如果要尝试ios方面的东西,一定是需要mac系统的,无论是用mac 电脑 还是使用虚拟机,并且还需要Xocde 这个巨无霸。



首先来介绍一个我使用的环境:



  • mac os 12.6.3

  • Xcode 13.2.1

  • Kotlin 1.8.0

  • Compsoe 1.3.0


我之前研究过KMM,曾尝试写了一个Demo,当时的mac 系统是 10.17 版本,最多只能下载Xcode 12.3,而这个版本的Xcode 只能编译 Kotlin 1.5.31,想要用高版本的kotlin 就得需要使用 12.5版本的Xcdoe,所以我就是直接将mac 系统升级到了 12.6.3Xcode 我是下载的13.2.1,直接下载最新版的Xcode14 应该也可以。这是关于环境版本需要注意的一些点。


现在开始正式的搭建项目。


首先要安装一个Android Studio 插件,直接 在插件市场搜索 Kotlin Multiplatform Mobile 就可以。


安装完成之后,在新建项目的时候 就可以看到在最后多出来两个项目模板,



这里使用第一个模板。


创建出来目录结构大概是这个样子的:




  • androidApp就是运行在Android平台上的。

  • iosApp 就是运行在ios平台上的。

  • shared就是两者通用的部分。


shared 中又分为androidMainiosMaincommonMain 三个部分。


主要是在commonMain中定义行为,然后分别在androidMainiosMain 中分别实现,这个Demo 中主要是展示系统版本。


interface Platform {
    val name: String
}

expect fun getPlatform(): Platform

expect 关键字是将此声明标记为是平台相关的,并期待从模块中实现。


然后在对应模块中实现:


//android
class AndroidPlatform : Platform {
    override val name: String = "Android ${android.os.Build.VERSION.SDK_INT}"
}

actual fun getPlatform(): Platform = AndroidPlatform()

//ios
class IOSPlatform: Platform {
    override val name: String = UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion
}

actual fun getPlatform(): Platform = IOSPlatform()

actual : 表示多平台项目中的一个平台相关实现



Kotlin关键字 参见:


http://www.kotlincn.net/docs/refere…





引用自:http://www.kotlincn.net/docs/refere…



这个模板项目大概就了解这么多,接下我们开始引入Compose相关依赖。


首先在settings.gradle.kts 中添加仓库:


pluginManagement {
    repositories {
        google()
        gradlePluginPortal()
        mavenCentral()
        //添加这两行
        maven(uri("https://plugins.gradle.org/m2/")) // For kotlinter-gradle
        maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
    }
}

dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
//      添加这个
        maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
    }
}

然后引入插件和依赖:


根目录build.gradle.kts
plugins {
    //trick: for the same plugin versions in all sub-modules
    id("com.android.application").version("7.4.1").apply(false)
    id("com.android.library").version("7.4.1").apply(false)
    kotlin("android").version("1.8.0").apply(false)
    kotlin("multiplatform").version("1.8.0").apply(false)
    //添加此行
    id("org.jetbrains.compose") version "1.3.0" apply false
}

share Module 的 build.gradle.kts
plugins {
    kotlin("multiplatform")
    id("com.android.library")
    //添加此行
    id("org.jetbrains.compose")
}


    sourceSets {
        val commonMain by getting{
            //添加依赖
            dependencies {
                with(compose) {
                    implementation(ui)
                    implementation(foundation)
                    implementation(material)
                    implementation(runtime)
                }
            }
        }
      ....
    }

然后再进行编译的时候,这里会报错:


ERROR: Compose targets '[uikit]' are experimental and may have bugs!
But, if you still want to use them, add to gradle.properties:
org.jetbrains.compose.experimental.uikit.enabled=true

这里是需要在gradle.properties 中添加:


org.jetbrains.compose.experimental.uikit.enabled=true

然后在编译ios module的时候,可以选择直接从这列选择iosApp



有时候可能因为Xcode环境问题,这里的iosApp 会标记着一个红色的x号,提示找不到设别,或者其他关于Xcode的问题,此时可以直接点击iosApp module 下的 iosApp.xcodeproj ,可以直接用Xcode 来打开编译。还可以直接直接跑 linkDebugFrameworkIosX64 这个task 来直接编译。


此时编译我是碰见了一个异常:


e: Module "org.jetbrains.compose.runtime:runtime-saveable (org.jetbrains.compose.runtime:runtime-saveable-uikitx64)" has a reference to symbol androidx.compose.runtime/remember|-2215966373931868872[0]. Neither the module itself nor its dependencies contain such declaration.

This could happen if the required dependency is missing in the project. Or if there is a dependency of "org.jetbrains.compose.runtime:runtime-saveable (org.jetbrains.compose.runtime:runtime-saveable-uikitx64)" that has a different version in the project than the version that "org.jetbrains.compose.runtime:runtime-saveable (org.jetbrains.compose.runtime:runtime-saveable-uikitx64): 1.3.0" was initially compiled with. Please check that the project configuration is correct and has consistent versions of all required dependencies.

出现这个错误是需要在gradle.properties 中添加:


kotlin.native.cacheKind=none


参见:github.com/JetBrains/c…



然后编译出现了一个报错信息巨多的异常:


//只粘贴了最主要的一个异常信息
Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/ld invocation reported errors

这里是需要在sharebuild.gradle.kts中加上这个配置:


    listOf(
        iosX64(),
        iosArm64(),
        iosSimulatorArm64()
    ).forEach {
        it.binaries.framework {
            baseName = "shared"
            //加上此行
            isStatic = true
        }
    }

    /**
     * Specifies if the framework is linked as a static library (false by default).
     * 指定框架是否作为静态库链接(默认情况下为false)。
     */
    var isStatic = false

然后再编译的时候还遇到一个异常信息:


//org.gradle.api.UnknownDomainObjectException: KotlinTarget with name 'uikitX64' not found.

这个是在sharebuild.gradle.kts中加上如下配置:


   
kotlin{
    val args = listOf(
        "-linker-option", "-framework", "-linker-option", "Metal",
        "-linker-option", "-framework", "-linker-option", "CoreText",
        "-linker-option", "-framework", "-linker-option", "CoreGraphics"
    )

    //org.gradle.api.UnknownDomainObjectException: KotlinTarget with name 'uikitX64' not found.
    iosX64("uikitX64") {
        binaries {
            executable {
                entryPoint = "main"
                freeCompilerArgs = freeCompilerArgs + args
            }
        }
    }
    iosArm64("uikitArm64") {
        binaries {
            executable {
                entryPoint = "main"
                freeCompilerArgs = freeCompilerArgs + args
                freeCompilerArgs = freeCompilerArgs + "-Xdisable-phases=VerifyBitcode"
            }
        }
    }
}

然后再编译就可以正常编译通过了。


下面我们就可以在两端使用Compose了。


首先在commonMain中写一个Composable,用来供给两端调用:


@Composable
internal fun KMMComposeView(device:String){
    Box(contentAlignment = Alignment.Center) {
        Text("Compose跨端 $device view")
    }
}

这里一定要写上internal 关键字,internal 是将一个声明 标记为在当前模块可见。



internal 官方文档


http://www.kotlincn.net/docs/refere…



不然在ios 调用定义好的Compose 的时候产生下面的异常:


Undefined symbols for architecture x86_64:
  "_kfun:com.xl.kmmdemo#KMMComposeView(kotlin.String){}", referenced from:
      _objc2kotlin_kfun:com.xl.kmmdemo#KMMComposeView(kotlin.String){} in shared(result.o)

然后再两端添加各自的调用:


androidMain的 Platform.kt
@Composable
fun MyKMMView(){
    KMMComposeView("Android")
}

iosMain的 Platform.kt

fun MyKMMView(): UIViewController = Application("ComposeMultiplatformApp") {
    KMMComposeView(UIDevice.currentDevice.systemName())
}

最后在androidApp module中直接调用 MyKMMView() 就行了,iosApp 想要使用的话,我们还得修改一下iosApp moudle的代码:


我们呢需要将 iosApp/iosApp/iOSApp.swift 的原有代码:


import SwiftUI

@main
struct iOSApp: App {
 var body: some Scene {
  WindowGroup {
   ContentView()
  }
 }
}

替换为:


import SwiftUI
import shared

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    var myWindow: UIWindow?

    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        myWindow = UIWindow(frame: UIScreen.main.bounds)
        //主要关注这一行, 这里是调用我们自己在iosMain里面定义的 KMMViewController 
        let mainViewController = PlatformKt.KMMViewController()
        myWindow?.rootViewController = mainViewController
        myWindow?.makeKeyAndVisible()
        return true
    }

     func application(
        _ application: UIApplication,
        supportedInterfaceOrientationsFor supportedInterfaceOrientationsForWindow: UIWindow?
     ) -> UIInterfaceOrientationMask {
         return UIInterfaceOrientationMask.all
    }
}

最后来看看效果:



用来演示效果的代码非常简单,就是使用一个Text 组件来展示 设备的类型。


写在最后


自己在上手的时候本以为很简单,结果就是不断踩坑,同时ios相关知识也比较匮乏,有些问题的解决方案网上的答案也非常少,最后是四五天才能正常跑起来这个Demo。现在是将这些坑都记录下来,希望能给其他的同学能够提供一些帮助。


后面的计划会尝试使用ktor接入一些网络请求,然后写一个跨端的开源项目,如果再遇见什么坑会继续分享这个踩坑系列。


关于Compose for Desktop 之前也有过尝试,是写了一个adb GUI的工具项目,非常简单 ,没遇见什么坑,就是Compose的约束布局没有。目前工作中经常用到的一些工具 也是使用Compose写的。


今天的碎碎念就到这里了,这次写的也比较细,比较碎,大家要是有什么问题,欢迎一起交流。


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

Flutter 3.7 之快速理解 toImageSync 是什么?能做什么?

随着 Flutter 3.7 的更新, dart:ui 下多了 Picture.toImageSync 和 Scene.toImageSync 这两个方法,和Picture.toImage 以及 Scene.toImage 不同的是 ,toImageSy...
继续阅读 »

随着 Flutter 3.7 的更新, dart:ui 下多了 Picture.toImageSyncScene.toImageSync 这两个方法,和Picture.toImage 以及 Scene.toImage 不同的是 ,toImageSync 是一个同步执行方法,所以它不需要 await 等待,而调用 toImageSync 会直接返回一个 Image 的句柄,并在 Engine 后台会异步对这个 Image 进行光栅化处理。


前言


toImageSync 有什么用?不是有个 toImage 方法了,为什么要多一个 Sync 这样的同步方法?




  • 目前 toImageSync 最大的特点就是图像会在 GPU 中常驻 ,所以对比 toImage 生成的图像,它的绘制速度会更快,并且可以重复利用,提高效率。



    toImage 生成的图像也可以实现 GPU 常驻,但目前没有未实现而已。





  • toImageSync 是一个同步方法,在某些场景上弥补了 toImage 必须是异步的不足。





toImageSync 的使用场景上,官方也列举了一些用途,例如:



  • 快速捕捉一张昂贵的栅格化图片,用户支持跨多帧重复使用

  • 应用在图片的多路过滤器上

  • 应用在自定义着色器上


具体在 Flutter Framework 里,目前 toImageSync 最直观的实现,就是被使用在 Android 默认的页面切换动画 ZoomPageTransitionsBuilder 上,得意于 toImageSync 的特性,Android 上的页面切换动画的性能,几乎减少了帧光栅化一半的时间,从而减少了掉帧和提高了刷新率。



当然,这是通过牺牲了一些其他特性来实现,后面我们会讲到。



SnapshotWidget


前面说了 toImageSync 让 Android 的默认页面切换动画性能得到了大幅提升,那究竟是如何实现的呢?这就要聊到 Flutter 3.7 里新增加的 SnapshotWidget


其实一开始 SnapshotWidget 是被定义为 RasterWidget ,从初始定义上看它的 Target 更大,但是最终在落地的时候,被简化处理为了 SnapshotWidget ,而从使用上看确实 Snapshot 更符合它的设定。



概念


SnapshotWidget 的作用是可以将 Child 变成的快照(ui.Image)从而替换它们进行显示,简而言之就是把子控件都变成一个快照图片,而 SnapshotWidget 得到快照的办法就是 Scene.toImageSync



那么到这里,你应该知道为什么 toImageSync 可以提高 Android 上的页面切换动画的性能了吧?因为 SnapshotWidget 会在页面跳转时把 Child 变成的快照,而 toImageSync 栅格化的图片还可以跨多帧重复使用。



那么问题来了,SnapshotWidget 既然是通过 toImageSync 将 Child 变成的快照(ui.Image)来提高性能,那么带来的副作用是什么?


答案是动画效果,因为子控件都变成了快照,所以如果 Child 控件带有动画效果,会呈现“冻结”状态,更形象的对比如下图所示:















FadeUpwardsPageTransitionsBuilderZoomPageTransitionsBuilder

默认情况下 Flutter 在 Android 上的页面切换效果使用的是 ZoomPageTransitionsBuilder ,而 ZoomPageTransitionsBuilder 里在页面切换时会开启 SnapshotWidget 的截图能力,所以可以看到,它在页面跳转时,对比 FadeUpwardsPageTransitionsBuilder 动图, ZoomPageTransitionsBuilder 的红色方块和掘金动画会停止。



因为动画很短,所以可以在代码里设置 timeDilation = 40.0;SchedulerBinding.resetEpoch 来全局减慢动画执行的速度,另外可以配置 MaterialApp ThemeData 下对应的 pageTransitionsTheme 来切换页面跳转效果。



所以在官方的定义中,SnapshotWidget 是用来协助执行一些简短的动画效果,比如一些 scale 、 skew 或者 blurs 动画在一些复杂的 child 构建上开销会很大,而使用 toImageSync 实现的 SnapshotWidget 可以依赖光栅缓存:



对于一些简短的动画,例如 ZoomPageTransitionsBuilder 的页面跳转, SnapshotWidget 会将页面内的 children 都转化为快照(ui.Image),尽管页面切换时会导致 child 动画“冻结”,但是实际页面切换时长很短,所以看不出什么异常,而带来的切换动画流畅度是清晰可见的



再举个更直观的例子,如下代码所示,运行后我们可以看到一个旋转的 logo 在屏幕上随机滚动,这里分别使用了 AnimatedSlideAnimatedRotation 执行移动和旋转动画。


Timer.periodic(const Duration(seconds: 2), (timer) {
final random = Random();
x = random.nextInt(6) - 3;
y = random.nextInt(6) - 3;
r = random.nextDouble() * 2 * pi;
setState(() {});
});

AnimatedSlide(
offset: Offset(x.floorToDouble(), y.floorToDouble()),
duration: Duration(milliseconds: 1500),
curve: Curves.easeInOut,
child: AnimatedRotation(
turns: r,
duration: Duration(milliseconds: 1500),
child: Image.asset(
'static/test_logo.png',
width: 100,
height: 100,
),
),
)


如果这时候在 AnimatedRotation 上层加多一个 SnapshotWidget ,并且打开 allowSnapshotting ,可以看到此时 logo 不再转动,因为整个 child 已经被转化为快照(ui.Image)。











所以 SnapshotWidget 不适用于子控件还需要继续动画或有交互响应的地方,例如轮播图。



使用


如之前的代码所示,使用 SnapshotWidget 也相对简单,你只需要配置 SnapshotController ,然后通过 allowSnapshotting 控制子控件是否渲染为快照即可。


 controller.allowSnapshotting = true;

SnapshotWidget 在捕获快照时,会生成一个全新的 OffsetLayerPaintingContext,然后通过 super.paint 完成内容捕获(这也是为什么不支持 PlatformView 的原因之一),之后通过 toImageSync 得到完整的快照(ui.Image)数据,并交给 SnapshotPainter 进行绘制。










所以 SnapshotWidget 完成图片绘制会需要一个 SnapshotPainter ,默认它是通过内置的 _DefaultSnapshotPainter 实现,当然我们也可以自定义实现 SnapshotPainter 来完成自定义逻辑。



从实现上看,SnapshotPainter 用来绘制子控件快照的接口,正如上面代码所示,会根据 child 是否支持捕获(_childRaster == null),从而选择调用 paintpaintSnapshot 来实现绘制。



另外,目前受制于 toImageSync 的底层实现, SnapshotWidget 无法捕获 PlatformView 子控件,如果遇到 PlatformView,SnapshotWidget 会根据 SnapshotMode 来决定它的行为:



















normal默认行为,如果遇到无法捕获快照的子控件,直接 thrown
permissive宽松行为,遇到无法捕获快照的子控件,使用未快照的子对象渲染
forced强制行为,遇到无法捕获快照的子控件直接忽略

另外 SnapshotPainter 可以通过调用 notifyListeners 触发 SnapshotWidget 使用相同的光栅进行重绘,简单来说就是:



你可以在不需要重新生成新快照的情况下,对当然快照进行一些缩放、模糊、旋转等效果,这对性能会有很大提升



所以在 SnapshotPainter 里主要需要实现的是 paintpaintSnapshot 两个方法:




  • paintSnapshot 是绘制 child 快照时会被调用




  • paint 方法里主要是通过 painter (对应 super.paint)这个 Callback 绘制 child ,当快照被禁用或者 permissive 模式下遭遇 PlatformView 时会调用此方法





举个例子,如下代码所示,在 paintSnapshot 方法里,通过调整 Paint ..color ,可以在前面的小 Logo 快照上添加透明度效果:


class TestPainter extends SnapshotPainter {
final Animation<double> animation;

TestPainter({
required this.animation,
});

@override
void paint(PaintingContext context, ui.Offset offset, Size size,
PaintingContextCallback painter) {}

@override
void paintSnapshot(PaintingContext context, Offset offset, Size size,
ui.Image image, Size sourceSize, double pixelRatio) {
final Rect src = Rect.fromLTWH(0, 0, sourceSize.width, sourceSize.height);
final Rect dst =
Rect.fromLTWH(offset.dx, offset.dy, size.width, size.height);
final Paint paint = Paint()
..color = Color.fromRGBO(0, 0, 0, animation.value)
..filterQuality = FilterQuality.low;
context.canvas.drawImageRect(image, src, dst, paint);
}

@override
void dispose() {
super.dispose();
}

@override
bool shouldRepaint(covariant TestPainter oldDelegate) {
return oldDelegate.animation.value != animation.value;
}
}


其实还可以把移动的动画部分挪到 paintSnapshot 里,然后通过对 animation 的状态进行管理,然后通过 notifyListeners 直接更新快照绘制,这样在性能上会更有优势,Android 上的 ZoomPageTransitionsBuilder 就是类似实现。


  animation.addListener(notifyListeners);
animation.addStatusListener(_onStatusChange);

void _onStatusChange(_) {
notifyListeners();
}
@override
void paintSnapshot(PaintingContext context, Offset offset, Size size, ui.Image image, Size sourceSize, double pixelRatio) {
_drawMove(context, offset, size);
}

@override
void paint(PaintingContext context, ui.Offset offset, Size size, PaintingContextCallback painter) {
switch (animation.status) {
case AnimationStatus.completed:
case AnimationStatus.dismissed:
return painter(context, offset);
case AnimationStatus.forward:
case AnimationStatus.reverse:
}
....
}


更多详细可以参考系统 ZoomPageTransitionsBuilder 里的代码实现。



拓展探索


其实除了 SnapshotWidget 之外,RepaintBoundary 也支持了 toImageSync , 因为 toImageSync 获取到的是 GPU 中的常驻数据,所以在实现类似控件截图和高亮指引等场景绘制上,理论上应该可以得到更好的性能预期。


final RenderRepaintBoundary boundary =
globalKey.currentContext!.findRenderObject()! as RenderRepaintBoundary;
final ui.Image image = boundary.toImageSync();

除此之外,dart:ui 里的 Scene_Image 对象其实都是 NativeFieldWrapperClass1 ,以前我们解释过:NativeFieldWrapperClass1 就是它的逻辑是由不同平台的 Engine 区分实现











所以如果你直接在 flutter/bin/cache/pkg/sky_engine/lib/ui/compositing.dart 下去断点 toImageSync 是无法成功执行到断点位置的,因为它的真实实现在对应平台的 Engine 实现。




另外,前面我们一直说 toImageSync 对比 toImage 是 GPU 常驻,那它们的区别在哪里?从上图我们就可以看出:



  • toImageSync 执行了 Scene:RasterizeToImage 并返回 Dart_Null 句柄

  • toImage 执行了 Picture:RasterizeLayerTreeToImage 并直接返回


简单展开来说,就是:



  • toImageSync 最终是通过 SkImage::MakeFromTexture 通过纹理得到一个 GPU SkImage 图片

  • toImage 是通过 makeImageSnapshotmakeRasterImage 生成 SkImagemakeRasterImage 是一个复制图像到 CPU 内存的操作。












其实一开始 toImageSync 是被命令为 toGpuImage ,但是为了更形象通用,最后才修改为 toImageSync



toImageSync 等相关功能的落地可以说同样历经了漫长的讨论,关于是否提供这样一个 API 到最终落地,其执行难度丝毫不比 background isolate 简单,比如:是否定义异常场景,遇到错误是否需要在Framwork 层消化,是否真的需要这样的接口来提高性能等等。












toImageSync 等相关功能最终能落地,其中最重要的一点我认为是:



toGoulmage gives the framework the ability to take performance into their own hands, which is important given that our priorities don't always line up.



最后


toImageSync 只是一个简单的 API ,但是它的背后经历了很多故事,同时 toImageSync 和它对应的封装 SnapshotWidget ,最终的目的就是提高 Flutter 运行的性能。


也许目前对于你来说 toImageSync 并不是必须的,甚至 SnapshotWidget 看起来也很鸡肋,但是一旦你需要处理复杂的绘制场景时, toImageSync 就是你必不可少的菜刀。


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

final的那些事儿

final作为java中基础常用的关键字,相信大家都很熟悉,大家都晓得final可以修饰类,方法和变量且具有以下特性: final修饰类时,该类不可被继承 final修饰方法时,该方法不可被重写 final修饰变量时,如果该变量是基础数据类型,则只能赋值一次...
继续阅读 »

final作为java中基础常用的关键字,相信大家都很熟悉,大家都晓得final可以修饰类,方法和变量且具有以下特性:



  • final修饰类时,该类不可被继承

  • final修饰方法时,该方法不可被重写

  • final修饰变量时,如果该变量是基础数据类型,则只能赋值一次,如果该变量是对象类型,则其指向的地址只能赋值一次


那么除了这些就没有了吗?看以下问题:



  • 当匿名内部类引用函数中的局部变量时,局部变量是基础数据类型会怎样?对象类型会怎样?为什么需要final修饰?

  • 当匿名内部类引用函数中的局部变量时,这个变量在堆上还是栈上?

  • 当对象类型局部变量用final修饰后,如果被子线程持有,在子线程的属性值修改能被主线程感知吗?


如果这些问题你都晓得,那么恭喜你,不用继续往下看了。


当匿名内部类引用函数局部变量时,为什么需要final修饰?


内部类中不修改函数局部变量值


要验证该问题,我们先来简单编写一个内部类引用函数中局部变量的示例,代码如下:


 public interface Callback {
     void doWork();
 }
 
 public class FinalTest {
     public FinalTest() {
    }
 
     public void execute() {
         int variable = 10;
         setCallback(new Callback() {
             @Override
             public void doWork() {
                 System.out.println("method variable is:" + variable);
            }
        });
    }
 
     public void setCallback(Callback callback) {
 
    }
 }
 
 public class Main {
     public static void main(String[] args) {
         FinalTest finalTest  = new FinalTest();
         finalTest.execute();
    }
 }

可以看到在上述代码中,我们在FinalTest的execute方法中,通过Callback这个匿名内部类引用了execute函数中的variable变量,此时虽然variable没有被final修饰,但是代码仍然是可以运行的(基于JDK 1.8)。


有同学要说了,你又乱说,看看你的问题,问的是当内部类引用函数局部变量时,为什么需要final修饰?这没有final不照样跑的好好的?别急,我们来看下该程序的字节码,FinalTest类对应的字节码如下所示:


1-3-3-1


可以看到编译后自动为variable变量添加了final关键词修饰,那么为什么需要使用final关键词来修饰呢?主要有以下几点原因:



  • 生命周期不同: 从运行时内存分区一文中可知,函数中的局部变量作为线程的私有数据,被存储在虚拟机栈对应的栈帧中,当函数结束执行后出栈,而Callback类的doWork方法其调用时机与execute函数的执行结束时机明显不一致,故如果不使用final修饰,不能保证doWork方法执行时variable变量仍然存在。

  • 数据不同步: 从变量数据同步的角度来看,局部变量在传递到内部类时,是以备份的形式拷贝到自己的构造函数中,加以存储利用,如果不添加final修饰,则内外部修改互不同步,造成脏数据。


那么为什么使用fina就能解决以上问题呢?(仅针对基础类型讨论,对象类型见下一部分)


生命周期不同

继续查看FinalTest类字节码,可以看到当variable变量使用final修饰后,其被存储于常量池中,而常量池存储于方法区中,进而生命周期必然是大于Callback类的,variable变量相关字节码如下图:


1-3-3-2


1-3-3-3


数据不同步

查看通过new Callback创建的FinalTest匿名内部类的字节码,代码如下所示:


1-3-3-4


可以看到编译生成的 FinalTest$1这个匿名内部类继承自Callback接口,其通过构造函数持有了外部类FinalTest的引用以及int类型的var2,当variable被final修饰时,由于其值不可修改,故外部的variable变量和 FinalTest$1构造函数中传入的var2值始终保持一致,故而不存在数据不同步的问题。



从这里我们明显可以看出匿名内部类会持有外部类的引用,Handler导致的Activity内部泄漏的场景就是这样引发的


同理也可以看出,为什么匿名内部类访问外部类的成员变量不需要final修饰,主要是这种访问关系都可以转化为通过外部类引用间接访问



内部类中修改函数局部变量值


仍以上文中代码为例,我们在doWork中修改variable变量的值,来看下会怎么样?


1-3-3-5


从上图可以看出编译器提示我们将variable转化成一个单元素的数组,按照编译器提示修改,果然可以正常运行了


1-3-3-6


那么这是为什么呢?不是说final修饰的变量值不能变吗?编译器bug了?


当然不是,这里我们需要明白单元素数组它不是一个基础类型变量,它指向的是一块内存地址,当其被final修饰时,说的是该变量不能重新指向一块新的内存地址,而不是内存地址处存储的内容不可以变化,也就是说我们不能再次执行variable = {20}这种赋值操作(PS:类对象同理,变量不可以重新赋值成新的对象,但是对象的成员属性取值可以发生变化)。



为对对象的指向地址修改和成员属性修改做区分,下文中将成员属性修改简称为内容修改,将地址修改简称为值修改



同时我们也可以从这里了解到当匿名内部类持有函数的局部变量时,是通过符号引用获取的(类结构中常量池中字段说明可以参考<<深入理解Java虚拟机>>), FinalTest$1类中val$variable变量声明及在常量池中引用如下图所示:


1-3-3-8


1-3-3-8


子线程修改final修饰局部变量内容,是否可同步?


仍以上文代码为例,修改FinalTest类如下所示:


 public class FinalTest {
     public FinalTest() {
    }
 
     public void execute() {
         int variable = 10;
         setCallback(new Callback() {
             @Override
             public void doWork() {
                 System.out.println("method variable is:" + variable);
            }
        });
    }
 
     public void execute2() {
         final int[] variable = {10};
         ExecutorService executorService = Executors.newFixedThreadPool(5);
         executorService.execute(new Runnable() {
             @Override
             public void run() {
                 System.out.println("variable[0] is:" + variable[0]);
                 variable[0] = 100;
                 System.out.println("change variable[0] is:" + variable[0]);
            }
        });
         try {
             Thread.sleep(2000);
        } catch (InterruptedException e) {
             throw new RuntimeException(e);
        }
         System.out.println("after change variable[0] is:" + variable[0]);
    }
 
     public void setCallback(Callback callback) {
    }
 }

在Main中运行execute2,执行结果如下图所示:


1-3-3-9


可以看到,数组元素的值确定发生了改变,这也就意味着被final修饰的变量,其内容修改在多线程环境下具有可见性。



final在多线程环境下具有可见性



总结


final1


final,static与synchronized



























关键词修饰类型作用
final类,方法,变量修饰类,则类不可继承; 修饰方法,则方法不可被子类重写; 修饰变量,则变量只能初始化一次
static内部类,方法,变量,代码段修饰内部类,则该类只能访问外部类的静态成员变量和方法,在Handler内存泄漏的修复方案中就有静态内部类的方式; 修饰方法,则该方法可以直接通过类名访问 修饰变量,则该变量可以直接通过类名访问,在类的实例中,静态变量共享同一份内存空间,故其具有全局性质 修饰代码段,则该代码段在类加载的时候就会执行,由于类加载是多线程安全的,所以可以通过静态代码段实现一些初始化操作而不用担心多线程问题
synchronized方法,代码段修饰方法时,则该方法为同步方法,使用该方法所在的类对象做为锁对象,多线程环境下,排队执行 修饰代码段时,一般会指定所使用的锁对象,多线程环境下,该代码段排队执行

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

拥有思想,你就是高级、资深、专家、架构师

当然要想成为高级工程师或者架构师,光看书是不行的,书本上来的东西都是工具型编程的体现,何为工具型编程呢? 就是说可以依据书本、网络等渠道就能完成的编程就是工具型编程,那怎么解决呢? 为什么要提升编程思想 这个问题我想大家都有答案,编程思想就是一个程序员的灵魂...
继续阅读 »

当然要想成为高级工程师或者架构师,光看书是不行的,书本上来的东西都是工具型编程的体现,何为工具型编程呢?


就是说可以依据书本、网络等渠道就能完成的编程就是工具型编程,那怎么解决呢?


为什么要提升编程思想



这个问题我想大家都有答案,编程思想就是一个程序员的灵魂,没有灵魂的程序员,只配ctrl + C/V.



专业一点来讲,提升编程思想的重要性在于它能够帮助开发者更好地解决问题、提高效率、减少错误,并提高代码的可读性、可维护性和可扩展性,而这些点位就是成为一个高级Android工程师或者架构师必不可少的技能,也是每一个程序员应该具备的技能。在国外,很多面试更看重的是学习能力和编程思想,其实也是,一个10年的经验丰富的程序员学习一门新的语言或者技术如同探囊取物,对于一个公司、一个团队、一个业务成本来讲,有这样一个人是最经济的。更具体来讲:



  1. 解决问题能力:良好的编程思想能够帮助开发者更好地理解问题,设计出高效、可靠、可扩展的解决方案。

  2. 代码质量提升:优秀的编程思想可以帮助开发者写出易于阅读、易于维护的代码,并使其更加健壮、可靠、可测试。

  3. 工作效率提高:合理的编程思想可以使开发者更加高效地编写代码,并降低代码调试和修复的时间。

  4. 技术实力提升:良好的编程思想可以使开发者更加深入地理解编程语言和计算机科学原理,并在实践中掌握更多的技能和技巧。

  5. 职业发展:具备良好编程思想的开发者在技术水平和职业发展方面具有更好的竞争力和前景。


良好的编程思想可以帮助开发者更好地解决问题、提高效率、提高代码质量和可维护性,并在职业发展中具有更好的前景和竞争力,这也就成了了中、高、架构等分级程序员的区别之分。


如何提升自己的编程思想



  1. 练习算法和数据结构:熟悉算法和数据结构可以帮助你更好地理解和解决问题,优化你的代码并提高你的代码质量。

  2. 阅读源代码:阅读其他优秀项目的源代码可以帮助你学习其他开发人员的编程思想,理解他们是如何解决问题的,进而提高自己的编程思维。

  3. 学习设计模式:设计模式是一种常用的编程思想,它可以帮助你更好地组织你的代码,提高代码的可维护性和可扩展性。

  4. 参与开源项目:参与开源项目可以帮助你学习其他开发人员的编程思想,理解他们是如何解决问题的,同时也可以帮助你获取更多的开发经验和知识。

  5. 持续学习:跟上Android开发的最新技术和趋势可以让你更好地了解开发环境和市场需求,并提升自己的编程思想。

  6. 经常review自己的代码:经常review自己的代码可以帮助你发现自己代码中的问题并及时改进,同时也可以帮助你学习其他开发人员的编程思想。


接下来,我们对这些步骤的项目进行分析和说明,


练习算法和数据结构



熟悉算法和数据结构可以帮助你更好地理解和解决问题,优化你的代码并提高你的代码质量, Android 这种移动平台,性能要求是非常高的,但是他的机型众多,系统不一,所以我们应该从编程角度就减少不必要的麻烦。怎么练习呢?




  1. 选择适合自己的算法练习平台:例如LeetCode、HackerRank、Codeforces等。这些平台都提供了大量的算法题目,可以帮助你提高算法水平。

  2. 学习基础算法:如排序、查找、树、图等算法,这些算法是其他算法的基础,掌握好基础算法对于提高算法能力非常有帮助。

  3. 练习算法的具体类型:例如贪心算法、动态规划、分治算法等,这些算法在实际开发中非常常见,掌握好这些算法可以让你更好地解决实际问题。

  4. 尝试实现算法:通过手写实现一些经典算法,你可以更好地理解算法的思想和实现方式,并加深对算法的理解。

  5. 参与算法竞赛:参与算法竞赛可以帮助你提高算法能力,同时也可以认识到其他优秀的算法工程师。


很多人在开发过程中公司是不会要求有算法参与的,特别是在Android端,也很少有人在开发中精心设计一款算法用于Android业务,Android的数据量级都在可控范围内,但是优秀的程序员不是应公司要求而编程的,我们因该在面对数据的时候,自然而然的想到算法,想到时间复杂度、空间复杂度的问题算法-时间复杂度 这是之前我在公司分享过的一篇文章,大家可以参考一下,基本涵盖了时间复杂度及在不同场景中的计算方式以及在特殊场景下的计算概念。


举几个例子,怎么将算法运用到平时的开发中呢?



  1. 优化算法复杂度:在实际开发中,我们常常需要处理大量数据,如果算法复杂度高,就容易导致程序运行缓慢。因此,优化算法复杂度是非常重要的。比如,在ListView或RecyclerView中使用二分查找算法可以快速查找到指定位置的数据。

  2. 应用动态规划算法:动态规划算法可以用于解决一些经典问题,例如最长公共子序列、背包问题等。在实际开发中,我们也可以应用动态规划算法解决一些问题,例如路径规划、字符串匹配等。

  3. 应用贪心算法:贪心算法是一种可以获得近似最优解的算法,可以用于一些优化问题。在Android开发中,例如布局优化、图片压缩等方面,也可以应用贪心算法来达到优化的效果。

  4. 应用其他常用算法:除了上述算法外,其他常用算法也可以应用于Android开发中,例如图像处理算法、机器学习算法等。对于一些比较复杂的问题,我们也可以引入其他算法来解决。


反正就是学之前要理解对应算法的大致用途,在类似场景中,直接尝试搬套,先在伪代码中演算其结果,结果正趋向时果断使用。


阅读源码



阅读其他优秀项目的源代码可以帮助你学习其他开发人员的编程思想,理解他们是如何解决问题的,进而提高自己的编程思维



前边不是说了,工具型编程可以不用看吗,是的,但是阅读源码不是查看工具,而是提升你的编程思想,借鉴思想是人类进化的最主要体现之一,多个思想的碰撞也能造就更成功的事件。那怎么阅读源码呢?这个每个人都有自己的方法,阅读后要善于变通,运用到自己的项目中,我是这么做的。



  1. 选择合适的开源项目:选择一个合适的开源项目非常重要。你可以选择一些知名度比较高的项目,例如Retrofit、OkHttp、Glide等,这些项目通常质量比较高,也有一定的文档和教程。

  2. 确定目标和问题:在阅读源码前,你需要明确自己的目标和问题。例如,你想了解某个库的实现原理,或者你想解决一个具体的问题。

  3. 仔细阅读源码:在阅读源码时,需要仔细阅读每一个类、方法、变量的注释,了解每一个细节。同时也需要了解项目的整体结构和运行流程。

  4. 了解技术背景和思路:在阅读源码时,你需要了解作者的技术背景和思路,了解为什么选择了某种实现方式,这样可以更好地理解代码。

  5. 实践运用:通过阅读源码,你可以学到许多好的编程思想和技巧,你需要将这些思想和技巧运用到自己的开发中,并且尝试创新,将这些思想和技巧进一步发扬光大。


阅读源码需要持之以恒,需要不断地实践和思考,才能真正学习到他人的编程思想,并将其运用到自己的开发中。


学习设计模式



设计模式本就是编程思想的总结,是先辈们的经验绘制的利刃,它可以帮助你更好地组织你的代码,提高代码的可维护性和可扩展性。




  1. 学习设计模式的基本概念:学习设计模式前,需要了解面向对象编程的基本概念,例如继承、多态、接口等。同时也需要掌握一些基本的设计原则,例如单一职责原则、开闭原则等。

  2. 学习设计模式的分类和应用场景:学习设计模式时,需要了解每个设计模式的分类和应用场景,例如创建型模式、结构型模式、行为型模式等。你需要了解每个模式的特点,以及何时应该选择使用它们。

  3. 练习设计模式的实现:练习实现设计模式是学习设计模式的关键。你可以使用一些例子,例如写一个简单的计算器、写一个文件读写程序等,通过练习来加深对设计模式的理解。

  4. 将设计模式应用到实际项目中:将设计模式应用到实际项目中是学习设计模式的最终目标。你需要从项目需求出发,结合实际场景选择合适的设计模式。举例来说,下面是一些在Android开发中常用的设计模式:

    • 单例模式:用于创建全局唯一的实例对象,例如Application类和数据库操作类等。

    • 适配器模式:用于将一个类的接口转换成客户端期望的另一个接口,例如ListView的Adapter。

    • 工厂模式:用于创建对象,例如Glide中的RequestManager和RequestBuilder等。

    • 观察者模式:用于实现事件机制,例如Android中的广播机制、LiveData等。




学习设计模式需要不断练习和思考,通过不断地练习和实践,才能真正将设计模式灵活运用到自己的项目中。


参与开源或者尝试商业SDK开发



参与开源,很多同学是没有时间的,并且国内缺少很多开发团队项目,都是以公司或者团队模式开源的,个人想在直接参与比较困难,所以有条件的同学可以参与商业SDK的开发,
商业SDK比较特殊的点在于受众不同,但是他所涉及的编程思想较为复杂,会涉及到很多设计模式和架构模式。



比如,Android商业SDK开发涉及到很多方面,下面列举一些常见的考虑点以及经常使用的架构和设计模式:



  1. 安全性:SDK需要考虑用户隐私保护和数据安全,确保不会泄露敏感信息。

  2. 稳定性:SDK需要保证在不同的环境下运行稳定,不会因为异常情况而崩溃。

  3. 可扩展性:SDK需要考虑未来的扩展和升级,能够方便地添加新的功能和支持更多的设备和系统版本。

  4. 性能:SDK需要保证在各种设备和网络条件下,响应速度和性能都有足够的表现。

  5. 兼容性:SDK需要考虑在不同版本的Android系统和各种厂商的设备上,都能够正常运行。


经常用到的架构和设计模式包括:



  1. MVVM架构:MVVM是Model-View-ViewModel的简称,通过将视图、模型和视图模型分离,可以实现更好的代码组织和更容易的测试。

  2. 单例模式:单例模式是一种创建全局唯一对象的模式,在SDK中常用于创建全局的配置、管理器等。

  3. 工厂模式:工厂模式是一种创建对象的模式,SDK中常用于创建和管理复杂的对象。

  4. 观察者模式:观察者模式是一种事件机制,SDK中常用于通知应用程序有新的数据或事件到达。

  5. 适配器模式:适配器模式用于将一个接口转换成另一个接口,SDK中常用于将SDK提供的接口适配成应用程序需要的接口。

  6. 策略模式:策略模式是一种动态地改变对象的行为的模式,SDK中常用于在运行时选择不同的算法实现。


Android商业SDK开发需要综合考虑多个方面,选择适合的架构和设计模式能够提高代码质量、开发效率和维护性。


了解市场、了解业务,不要埋头敲代码



掌握最新的市场需求,比如网络框架的发展历程,从开始的HttpURLConnection的自己封装使用,到okhttp,再到retrofit, 再后来的结构协程、Flow等等,其实核心没有变化就是网络请求,但是,从高内聚到,逐层解耦,变的是其编程的思想。



CodeReview



可以参考该文章,此文章描述了CodeReview 的流程和方法,值得借鉴,CodeReview 是一个天然的提升自己业务需求的过程,
zhuanlan.zhihu.com/p/604492247



经常写开发文档



设计和编写开发文档是一个很重要的工作,它不仅能够提升自己的编程思想,也能够帮助团队提高协作效率和减少沟通成本.



如果要求你在开发一个需求前对着墙或者对着人讲一遍开发思路,你可能讲不出来,也不好意思,且没有留存,开发文档可以满足你,当你写开发文档时,你记录了你的对整个需求的开发,以及你编程的功底,日益累积后,你的思想自然会水涨船高,因为你写开发文档的过程就是在锻炼自己,比如我在前公司开发国际化适配时写的文档(当然只是我的粗鄙想法国际化ICU4J 适配及SDK设计,我会先分析问题,为什么?然后设计,并且会思考可能遇到的问题,也一并解决了。时间长了,设计模式、思想也会得到提升。


当然,也要分场景去设计,按需求去设计,可以采纳以下建议:
设计和编写开发文档是一个很重要的工作,它不仅能够提升自己的编程思想,也能够帮助团队提高协作效率和减少沟通成本。下面是一些关于如何设计一份好的开发文档的建议:



  1. 明确文档的目标和受众:在编写文档之前,需要明确文档的目标和受众,确定文档需要包含的内容和写作风格

  2. 使用清晰的语言和示例:使用简洁、清晰的语言描述问题,使用示例代码和截图帮助读者理解问题。

  3. 分层次组织文档:文档应该按照逻辑和功能分层次组织,每一层都有明确的目标和内容。

  4. 使用图表和图形化工具:图表和图形化工具能够有效地展示复杂的概念和数据,帮助读者更好地理解文档内容。

  5. 定期更新和维护文档:开发文档需要定期更新和维护,以反映最新的代码和功能。


通过设计一份好的开发文档,可以提升自己的编程思想,使得代码更加清晰和易于维护,同时也能够提高团队的协作效率和代码质量。


向上有组织的反馈



经常向领导有组织的汇报开发进度、问题、结果,不仅可以提升编程思想,还能够提高自己的工作效率和沟通能力



首先,向领导汇报开发进度、问题和结果,可以让自己更加清晰地了解项目的进展情况和任务的优先级,帮助自己更好地掌控项目进度和管理时间。


其次,通过向领导汇报问题,可以促使自己更加深入地了解问题的本质和解决方案,同时也能够得到领导的反馈和指导,帮助自己更快地解决问题。


最后,向领导汇报开发结果,可以帮助自己更好地总结经验和教训,促进自己的成长和提高编程思想。同时,也能够让领导更清晰地了解自己的工作成果,提高领导对自己的认可和评价。


向领导有组织地汇报开发进度、问题和结果,不仅能够提升编程思想,还能够提高工作效率和沟通能力,促进自己的成长和发展。


总结



  1. 编程思想的提升



  • 学习数据结构和算法,尤其是常见的算法类型和实际应用

  • 阅读优秀开源代码,理解代码架构和设计思想,学习开发最佳实践

  • 学习设计模式,尤其是常见的设计模式和应用场景



  1. 实际项目开发中的应用



  • 通过代码重构,优化代码质量和可维护性

  • 运用算法解决实际问题,例如性能优化、数据处理、机器学习等

  • 运用设计模式解决实际问题,例如代码复用、扩展性、灵活性等



  1. 沟通与协作能力的提高



  • 与团队成员保持良好的沟通,及时反馈问题和进展情况

  • 向领导有组织地汇报开发进度、问题和结果,以提高工作效率和沟通能力

  • 参加技术社区活动,交流分享经验和知识,提高团队的技术实力和协作能力


以上是这些方面的核心点,当然每个方面都有很多细节需要关注和完善,需要持续学习和实践。


附件



以下是我之前为项目解决老项目的图片框架问题而设计的文档,因名称原因只能图片展示



首先,交代了背景,存在的问题


image.png


针对问题,提出设计思想


image.png


开始设计,从物理结构到架构


image.png


image.png


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

程序员英语学习指南

动机为什么程序员要学习英语?工作:我们每天接触的代码都是英文的、包括很多技术文档也是英文的学习:最新最前沿的技术最开始都是只有English版本就业:学好英语让你的就业范围扩大到全球,而不只限于国内目标读: 流畅的阅读英文文档、英文论坛。写: 可以简单的写一些...
继续阅读 »

动机

为什么程序员要学习英语?

  • 工作:我们每天接触的代码都是英文的、包括很多技术文档也是英文的

  • 学习:最新最前沿的技术最开始都是只有English版本

  • 就业:学好英语让你的就业范围扩大到全球,而不只限于国内

目标

读: 流畅的阅读英文文档、英文论坛。

写: 可以简单的写一些英文的Issue、Readme等。

借助 APP

多邻国

多邻国是一个免费的多语言学习网站,界面清爽友好,对零基础非常友好,更有游戏般通关的体验。

不止可以学习英语更支持40+种语言的学习。

安卓、IOS都支持。

墨墨背单词

墨墨背单词是根据艾宾浩斯遗忘曲线的算法打造的高效抗遗忘,精准规划海量记忆的一款APP.

可以将不会的单词记录下到墨墨里就可以做到反复练习,强化记忆了。

安卓、IOS都支持。

借助影视剧

  • 老友记

  • 摩登家庭

  • 绝望主妇

  • ...(根据自己爱好选择)

浏览器插件

阅读提升

技术类

Vue、React、Node官方文档都可以,除此之外可以看看其他的技术网站:

新闻类

如果你比较关注实时新闻的话,不妨多看看这些。

总结

没事刷刷美剧、看看英文论坛、新闻,然后将不会的单词在墨墨中反复练习。

然后经常逛逛Dev、StackOverflow等论坛,即可增加技术能力也可以熟悉学习英文。

再比如写中文文章写出来后尝试用英语也写一遍等等。

也可以和同学、朋友、网友一起尝试用英文沟通,如果有认识外国人(最近很火的chatGPT也可以)沟通就更好了。

总结一下,大概的方法:

  1. 多看英文文档、论坛

  2. 多使用英语交流、写作

  3. 观看英语电影,电视和新闻

  4. 使用英语学习软件和网站,比如多邻国这种

  5. 加入英语学习小组或找英语学习伙伴一起学习

  6. 并将不会的单词记录下到墨墨反复练习

最后,关键在于持之以恒

作者:九旬
来源:juejin.cn/post/7199882882139373625

收起阅读 »

我终于统一了团队的技术方案设计模板

团队的技术方案设计模板不管我们是做业务开发,还是做基础建设,虽然产品诉求千差万别,但是我们必然需要做好方案设计,然后还需要进行方案设计评审。之前我们团队的一些成员,甚至有些 T9 级别的同学,竟然都写不好一个技术方案设计文档。究其根本,主要还是没有形成自己的方...
继续阅读 »

团队的技术方案设计模板

不管我们是做业务开发,还是做基础建设,虽然产品诉求千差万别,但是我们必然需要做好方案设计,然后还需要进行方案设计评审。

之前我们团队的一些成员,甚至有些 T9 级别的同学,竟然都写不好一个技术方案设计文档。究其根本,主要还是没有形成自己的方法论,从我个人工作这么多年的经验来看,技术方案设计是可以总结出一套方法论或者说框架套路来的。为此,我总结出了一套通用的技术方案设计模板(提纲),然后在我们团队内部进行了统一,后面还推广到了整个中心,大家按照这个模板来写方案设计,绝对让你的领导满意。

大家参考我这个方案设计模板(提纲)和相关介绍来做自己的方案设计的时候,可以根据自己的实际业务情况和背景做相关目录的删减,最后得出自己最终的方案设计,然后再去进行方案评审。

精简版-技术方案设计模板(提纲)

精简版的模板如下,一般的方案设计,大家都可以参考这个提纲来写:


详细版-技术方案设计模板(提纲)

相对详细和复杂的版本如下:



下面是技术方案设计模板在每一章节的简单说明,用来帮助你理清每个章节大概要写什么内容

一,现状

现状,主要是用来描述当前这个业务(项目)的一些基本情况介绍和相关的背景。你的方案设计出来之后,是需要给你的 leader 或者团队其他成员进行评审或者查看,甚至是要给更高级别的人来评审。但是别人不可能都和你一样清楚你的项目,因此首先,你要把你项目的基本情况和背景都说清楚,让大家达成一个共识,站在同一个起点上,才能进行后面的方案评审和讨论。

业务背景

业务背景就是你这个业务(项目)的基本介绍,包括但不限于:

  • 项目名称

  • 业务描述

技术背景

技术背景就是你这个业务是基于什么样的技术背景下来构建的,我们的技术方案可能是从 0 到 1 来构建,也能是基于现有的方案来优化,但是不管是什么场景,一定都会存在相关的技术背景,因此包括但不限于:

  • 现有技术积淀

  • 现有架构描述

  • 现有系统的整体容量

二,需求

需求,很重要!技术人员千万不要忽略需求,因为不管你的技术有多牛逼,都一定为需求服务的,不管这个需求是技术需求,还是业务需求,一定都是要为需求服务。而需求,就是你这个技术方案的起点,技术方案一切都是围绕需求来设计,当然,这个需求可以是当下的需求,也可以包含未来潜在的需求。

只有把需求介绍清楚之后,大家才能知道你方案设计里面的所有设计和对应的折中点是否可行,也才能比较好的去评审你的方案。

业务需求

业务需求就是你这个业务具体要做的事情,包括但不限于:

  • 要改造的内容

  • 要实现的新需求

业务痛点

  • 涉及到的业务痛点有哪些

性能需求

我们做需求的时候,对于技术人员,不能只看业务需求,业务需求可能是项目管理人员,也可能是产品人员提出来的,他们只会重点关注业务的可行性,只会关注业务的逻辑。但是技术人员,要从这个业务需求里面考虑清楚我们满足这个业务之下的性能需求点,比如我做一个秒杀活动,如果你不考虑性能,可能活动一上来,服务就挂掉了。性能需求包括但不限于:

  • 预估系统平均容量

  • 预估系统峰值容量

  • 可伸缩性

  • 其他的一些性能要求点,比如安全性等

三,方案描述

前面把现状和需求说清楚后,终于到了我们的重头戏,方案描述这里了。一般我们做方案,可能会有几个可选的方案,但是你不清楚哪个方案最合适,因此你需要把相关可能的方案都描述清楚,然后给出你认为的最合适的方案,然后让大家来评审和决策,看是否同意你的意见或者有其他更好的意见。

如果没有方案对比,那么可以省略掉这一章节

方案1

概述

一句话概括方案的亮点,比如说:高性能、可扩展、双写、主从分离、分库分表、扩容等。

详细说明

详细说明这里需要图文结合,包括但不限于架构图、流程图 等。把你整个方案的架构和模块、细节流程都描述清楚

性能目标

性能一般来说可能包含以下部分:

  • 日平均请求:一般来自产品人员的评估;

  • 平均QPS:日平均请求 除以 4w秒得出,为什么是4w秒呢,24小时化为86400秒,取用户活跃时间为白天算,除2得4w秒;

  • 峰值QPS:一般可以以QPS的2~4倍计算;

性能评估

给出方案的基准数据,并按性能需求评估需要使用的资源数量。

  • 单机并发量

  • 单机容量

  • 按照预估性能需求,预估资源数量(应用服务器、缓存、存储、队列等)

  • 伸缩方式

方案优缺点

列出方案的优缺点,优缺点要具有确定性,最好是通过量化的指标来说明

方案2

可选的另外一种方案,模板和上面一样。

方案对比

前面给出了多种可选的方案,那么这里就是进行一个简单的对比,然后给出你觉得最优的方案和原因,这就是你的决策。

有了你自己的决策(倾向)的方案后,接下来的设计就应该更多的偏向你倾向的方案去做设计和描述

四,线上方案

线上方案是对上面你更倾向的方案的更为细致的描述。

架构图

整体架构是如何,把架构图画上

关键设计点 和 设计折衷

把几个关键、重点的点的设计思想表述出来,用来确保你的方案的大体方向是 OK 的。

因为没有一个方案设计是最完美,方案设计都是逐步演进和优化的,方案设计是要最符合当前的背景的。因此,一定会有你设计的关键点和折衷点,这也就是前面为何要把项目的各种业务背景和技术背景都说清楚的原因。

业务流程

整体流程是如何,弄一个整体流程图、核心流程图出来,然后分业务场景把各个业务场景的流程图也画出来,并且做好相关介绍

模块划分

有了业务流程,那么必然要针对这个业务流程的各个环节来划分模块,模块的划分需要考虑我们架构设计的一些原则,比如:架构分层、业务分模块、微服务化、高内聚低耦合 等。然后把每个模块的功能点都说清楚

异常边界【重要】

异常边界是比较重要的,一般情况下,大部分人都能考虑到正常的处理流程,对于异常的边界考虑的比较少,但是线上出问题,大部分都是异常情况导致,因此这里非常重要!!!

我们可以通过一个 xmind 格式去整理相关的异常边界,这样有助于自己在实现的时候有足够的把控度,也便于别人去 review 你的方案和具体实现(如 coding)

异常边界需要考虑:

  • 涉及到了哪些模块

  • 涉及到了哪些流程

  • 每个模块、流程出现了各种可能情况的处理是?

  • 系统底层原因导致的异常的处理是 ?

统计、监控

线上运行的项目,一定需要有各种监控,除了公司内部的基建的监控外,我们可能还需要从业务内部实现自定义的一些业务监控和相关技术统计

灰度、回滚策略

  • 如何灰度?

  • 如何回滚?

容灾方案

容灾就是当出现 IDC 异常的情况下,怎么容灾,这个可以根据实际情况去考虑。

五,部署拓扑

线上部署拓扑如何,上下游是如何

六,风险评估

标识所选方案的风险,提出解决此风险发生时候的应对策略,比如:上线失败时的回滚策略。

潜在风险

  • 相关的改动有哪些风险点

  • 不兼容点?

  • 当前设计方案目前存在哪些问题?

  • 潜在有哪些问题

七,阶段规划【架构演进规划】

架构怎么演进

阶段如何规划

每个阶段该达成什么目标

第一阶段

第二阶段

第三阶段

八,工作量评估

工作量评估也是一个重要的环节,这里需要细化到每个模块、每个接口的设计分别需要多长时间,一定要同时包括开发时间、联调时间、测试时间。

来源: 后端系统和架构

收起阅读 »

TCP 长连接层的设计和在 IM 项目的实战应用

TCP 长连接接入层的连接管理TCP 长连接的管理思路实现思路IM 架构中的 TCP 长连接接入层的 NET 连接一般会很多,比如单台服务器至少会有几十万,有的甚至会到百万连接;这个长连接的维持,也就代表中会有这么多客户端(用户)的接入。那么我们怎么去管理这些...
继续阅读 »

TCP 长连接接入层的连接管理

TCP 长连接的管理思路

实现思路

IM 架构中的 TCP 长连接接入层的 NET 连接一般会很多,比如单台服务器至少会有几十万,有的甚至会到百万连接;这个长连接的维持,也就代表中会有这么多客户端(用户)的接入。那么我们怎么去管理这些连接?当有数据需要下发的时候,怎么能够快速根据连接信息找到用户、或者根据用户快速定位到网络连接?这就需要我们能够有一个合适的数据结构去维护,并且我们需要考虑一些其他的点比如快速定位、机器内存大小等。

最容易想到的一个思路是通过 map 数据结构来管理,比如 map<conn,user>,因为每个用户的 uid(user)是唯一,因此,这样做,初期来看,并不会有太大的影响;但是试想一下,这个只能单向定位,只能根据 Conn 网络连接查找用户,那么我想根据用户信息快速查找到对应的 Conn 然后下发数据怎么办呢?

为此,一个更为合适的做法就是将用户和网络连接进行一一对应,这样,不仅是可以相互查找,并且查找定位的时间复杂度总是 O(1)。具体实现的 Golang 代码如下,只列出关键信息:

// Conn 与 User 一一映射,用来优化 map 查询方式

type Conn struct {
  conn       net.Conn // TCP 网络 连接信息
    user       *User     // 客户端用户信息(一般包含 uid、name等)
}

type User struct {
  uid             int64
  Name             string
  conn             *Conn
}

这样的结构设计,就是 Conn 里面包含了 User、User 里面包含了 Conn,这样就是一一对应,不管多少数据量,查询定位的时间复杂度都是 O(1)。这里应用了一个思想就是空间换时间,因为我们当前的机器的内存是很大的,所以就可以利用这个空间换时间的思想,快速查询。

应用场景

IM 系统中,必然会有这么几个操作:

  • • 用来连接(accept)

  • • 用户登录(login)

TCP Socket 编程模型是:

socket -> bind -> listen -> accept -> recv -> send -> close

因此对 IM 接入层来说,首先会收到用户的 accept 请求,accept 成功之后,我们就有了 Conn 信息,然后我们开始填充 Conn 结构 和 User 结构,这里算是初步建立起了对应关系,但是 User 中的信息还不够,还需要用户登录之后才有更多的数据。

连接成功之后,用户就会发起登录请求,登录成功之后,就会有了足够的 User 信息,这样就可以根据相关信息相互定位了。登录成功之后,长连接接入层还需要给用户回应 ACK ,因此在登录包之后,长连接接入层就可以从 User 结构中取出 Conn 进行回包给用户(客户端)。

随后的操作中,我们可以根据业务场景的需要,从 User(uid)中快速定位到 Conn,然后发送消息给客户端;也可以根据 Conn 快速定位到 User,更新 User 信息,或者获取 User 信息。

TCP 长连接心跳超时的处理

再来看看另外一个场景,首先,我们要清楚,长连接接入层一定是有多个的,一台机器肯定扛不住,也无法做到高可用。因此在每个接入层节点中的处理上,还有一点非常重要的就是,维持着大量长连接后,如果客户端一直没有请求,或者客户端以为异常导致关闭了连接但是服务端并不知晓,那么这些无用的长连接,服务端肯定是需要清理的,避免占用大量资源。

怎么实现?当然需要通过心跳来保持连接,如果心跳超时则踢出连接。心跳这里多说一句,一般固定心跳设置为 4.5 分钟,还有更为合适的智能心跳策略。我们现在重点在于管理 TCP 长连接,不讨论心跳策略的实现。

上面的 TCP 长连接的管理思路是需要一一对应,方便相互查找,那么针对心跳是否超时,这个和用户没有关系,因此只需 Conn 的处理。通过一个红黑树可以搞定,通过递归地从根节点向左遍历节点,直到左节点为空,可以查找树中的所有 Conn 的超时情况。

Golang 的代码片段如下:

var timeoutTree *rbtree.Rbtree  //红黑树


type TimeoutInfo struct {
  conn   *Conn         // 连接信息
  latestTime time.Time //心跳的最新时间
}


每次收到心跳包都重新更新时间
func AddTimeoutCheckInfo(conn *Conn) {
  timeoutTree.Insert(&TimeoutInfo{conn: conn, latestTime: time.Now()})
}

独立协程来遍历扫描并清除超时的连接:

    for {
      // 遍历
      item := timeoutTree.Min()

      // 取连接、取最新时间
      latestTime := item.(*TimeoutInfo).latestTime
      conn := item.(*TimeoutInfo).conn

      // 计算连接的最新时间是否超时,超时则关闭连接和清理
      if timeout {
          timeoutTree.Delete(item)
          conn.Close()
      }
  }

TCP 长连接层的负载均衡策略

既然长连接接入层节点有多个,并且可以随时根据需要扩缩容,然而客户端并不清楚你服务端到底部署了多少台节点,那么客户端该怎么发起连接呢?怎么做才能保证合理的负载均衡呢?

一般的负载均衡策略如 RR 轮询,是否能够满足 IM 的诉求呢?试想这么一个真实的场景,当前线上有 5 台机器,每台机器负载都很高了,此时连接会很不稳定,客户端出现频繁重连。此时肯定需要扩容,OK,那么扩容了 2 台,然后 client 建连如果还是轮询,那么新扩容的机器,还是不能马上分散其他机器上的压力,压力还是会往老的机器上面去打,显然不合理。

因此,针对 IM 场景,最合理的负载均衡策略,就是根据连接数来负载均衡,客户端新发起连接需要接入的接入层节点一定是连接数最少的,因为每台节点会需要控制最大连接数的限制才能保证最优性能,并且能够及时给压力大的节点减压。

怎么实现呢?这里就需要有一个服务注册发现的组件(如 Etcd)来帮助我们达到诉求。首先,接入层启动后,往 Etcd 里面注册信息,并且再在后续的生命周期中,定期更新当前节点已有的连接数到 Etcd 中;然后我们需要有一个 Router Server,这个服务去 watch Etcd 中的接入层节点信息,Etcd 的使用可以参考etcd/clientv3;然后实时计算,得到一个列表排序,这个排序是按照节点数最少的节点排序的。

然后 Router Server 提供一个 HTTP 服务的 API 接口,用来返回所有节点中连接数最少的节点的一批 IP 列表(一般可以 3 个)给到客户端。为何不是返回一个呢?因为我们返回的节点,可能因为其他原因导致连接不上,或者连接不稳定,那么此时 客户端就可以有备选方案,选择返回的下一个节点建连。

涉及点包括:

  • • 接入层注册信息(节点 IP 和 port、节点连接数)

  • • 路由层 watch 接入层的信息

  • • 路由层计算路由算法

  • • 路由层提供 HTTP 接口返回合适的节点 IP 列表

TCP 长连接接入层服务的优雅重启和缩容

对于通用的长连接接入层而言

长连接接入层是和用户客户端直接相连的,客户端通过 TCP 长连接连接到接入层,因此接入层如果需要重启,那么必然会导致客户端连接断开,发生重连。如果此时用户正在发送消息,那么必然会导致发送异常,从而影响用户体验。

那么我们需要怎么实现接入层,才能保证重启或者缩容的时候,不影响用户、对用户无感知呢?有这么几个思路:

  1. \1. 接入层做的足够轻量,尽量只是维持 TCP 长连接和数据包的转发,所有其他业务逻辑,尽量转发到业务层去处理,接入层与业务逻辑层严格分离;因为业务层逻辑是需要频繁变动,而接入层的长连接维持可以做到尽量不变,这样就会尽可能的减少重启。

  2. \2. 接入层尽可能的做到无状态化,方便随时的扩缩容;这样就需要有一个叫用户中心的服务来保存用户的各种状态和信息,如在线状态、离线状态、用户是通过哪个接入层节点连接的;通过这个方式,用户就可以随意接入到任何接入层节点,并且接入层节点也可随时扩缩容;这样的话,业务逻辑层就可以和用户中心通过 RPC 通信获取用户的各种连接信息和是否在线的状态,然后精准下发消息到指定接入层,然后接入层将消息下发给客户端用户。

  3. \3. 主动迁移信令。增加一条信令和客户端进行交互,服务端如果要重启/缩容,那么主动告知连接在此接入层节点上的所有客户端,服务端主动发送迁移信令,比如 publish(迁移信令,100%),表示发送给所有此接入层节点上的客户端,客户端收到此迁移信令后,就主动进行重新连接其他节点。因为是客户端主动断开重连其他节点的,虽然还是会有重连,但是客户端是主动发起的,因此可以通过代码逻辑来保证从业务逻辑上不会影响用户的体验,这样的话,用户在操作上就会无感知,从而提升用户体验。同时,接入层节点要发送主动迁移信令之前,需要先从服务发现与注册中心(Etcd)中下线自己,避免重连的时候还继续连接到此节点。然后当重启之前,也需要判断一下是否当前节点上所有的用户连接都已经迁移到其他节点上了。

长连接接入层的优雅扩容方案

扩容方案是指在线用户越来越多,当前已有的接入层节点已经扛不住了,需要扩容接入层节点来分摊在线用户的连接和请求。这里分两种情况考虑:

  • • 其他节点的压力还相对较小,但是事先预知到需要扩容,也就是提前扩容。此时按照路由层的最小连接数优先接入请求的策略并无不妥,新扩容的可以均摊流量,原有的节点也不会因为压力过大而导致性能问题。

  • • 其他节点压力已经扛不住了,需要紧急扩容并且快速给老的节点减压。这个时候,如果还仅仅只是新增节点,然后根据原有的负载均衡路由策略来减压是达不到减压效果的,因为只有新的连接才会接入到新扩容的节点;原有老的节点上的连接如果没有断连那么还是继续维持在原有节点上,因此根本不能给老的节点减压。

  • • 所以,就需要服务端有更好的机制,通过服务端的机制来促使客户端重新连接到新的节点上,从而进行减压。这里,还是需要一个迁移信令,但是这个信令服务端只是需要随机发送给部分比例的用户,比如 publish(迁移信令,30%),表示发送迁移信令给 30% 比例的用户,让这 30%的用户重连到新的节点上。

TCP 长连接层节点怎么防止攻击

基本的防火墙策略

公司内常规的防火墙策略,通过 iptable 设置 iptables 的防火墙策略。比如限制只能接收指定 IP 和 Port 的包,避免攻击者通过节点上其他端口的漏洞登录机器;比如只接收某些协议(TCP)的包。

SYN 攻击

SYN 攻击是一个典型的 DDOS 攻击,具体就是攻击客户端在短时间内伪造大量不存在的 IP 地址,然后向服务端发送 TCP 握手连接的 SYN 请求包,服务端收到 SYN 包后会回复 ACK 确认包,并等待客户端的 ACK 确认。但是,由于源 IP 地址不是真实有效的,因此服务端需要不断的重发直至 63s 超时后才会断开连接。这些伪造的 SYN 包将长时间占用未连接队列,引起网络堵塞甚至系统瘫痪,让正常的 TCP 握手连接请求不能处理。通过 netstat -n -p TCP | grep SYN_RECV 可以查看是否有大量 SYN_RECV 状态,如果有则可能存在 SYN 攻击。

Linux 在系统层面上,提供了三个选项来应对相关攻击:

  • • tcp_max_syn_backlog,增大 SYN 连接数

  • • tcp_synack_retries,减少重试次数

  • • tcp_abort_on_overflow,过载直接丢弃,拒绝连接

另外,还有一个 tcp_syncookies 参数可以缓解,当 SYN 队列满了后,TCP 会通过相关信息(源 IP、源 port)制造出一个 SYN Cookie 返回,如果是攻击者则不会有响应,如果是正常连接,则会把这个 SYN Cookie 发回来,然后服务端可以通过 SYN Cookie 建连接。

TCP 长连接层面上

黑名单机制

可以静态或者动态配置黑名单列表,处于黑名单中的 IP 列表则直接拒绝 accept 建连;服务端执行 accept 之后,首先先判断 remote IP 是否存在于黑名单中,如果是则直接 close 连接;如果不是则继续下一步。

限制建连速度

IM 系统为了防止恶意攻击,需要防止单个 IP 大量频繁建连,避免异常 socket 连接数爆满;因此需要限制每个 IP 每秒建立速度,如果单个 IP 在单位时间内建连的连接数超过一定阈值(如 100)该值,则将 IP 列入黑名单并且同时关闭此连接

怎么实现呢?分如下几步。

\1. 定义一个合理的防攻击的数据结构,里面包含 connRates 字段、startTime 字段。

  • • startTime 表示此连接接入的初始时间

  • • connRates 用来对统计时间内的接入 IP 做累加

\2. 服务端每次 accept 之后,针对这个 Conn 连接,先判断当前时间和此连接的 startTime 的差值是否已经超过一个统计周期,如果超过则清零重置;如果没有超过,则对此连接的 IP 做累加。

\3. 然后判断 IP 累加的结果是否超过阈值,如果超过则加入黑名单并且 close 连接;如果没有超过则进行下一步的请求。

限制发包速度

IM 系统要能够发送消息包,必然需要先进行登录操作,登录主要是为了鉴权,从而获取得到正确的 token,才能正常登录。为了避免 token 等被窃取,为了更为安全,登录之后发送消息的频率也需要进行控制;控制的机制就是针对单个连接限制每秒处理包的上限,在单位时间内收到的包的请求数量超过一定阈值(如 100p/s)则直接丢弃。

怎么实现呢?需要几个步骤:

  • • 针对每个 Conn 的数据结构,增加一个 packetsNum 字段;

  • • 当前 Conn 每收到一个包,先计算统计时间内 packetsNum 的次数是否超过阈值,然后 packetsNum++;如果超过阈值则丢包并返回错误;

  • • 开一个定时器,每隔一个统计时间周期,清零 packetsNum。

TLS 加密传输

TLS 安全传输层协议用于在两个通信应用程序之间提供保密性和数据完整性,是我们 IM 系统中保证消息传输过程中不被截获、篡改、伪造的常用手段。

TLS 过程使用到了对称加密、非对称加密、CA 认证等,安全性非常高;但是相比于 TCP 传输会多了几个秘钥相关的环节,从而导致整个握手阶段会多出 1~2 个 RTT 的耗时;不过只是握手阶段的耗时对我们 IM 的应用场景并不影响。为此,为了安全性,尽可能的使用 TLS 来建立 TCP 连接

来源: 后端系统和架构

收起阅读 »

一文了解高性能架构和系统设计经验

高性能架构和系统设计经验高性能和高并发,听着就有点类似,并且他们还经常一起提及,比如提高我们的并发性能,显然,高性能可以提高我们的并发,但是细化来看,他们是有区别的,他们的考量点的维度不同。高性能需要我们从单机维度到整体维度去考虑,更多的是先从编码角度、架构使...
继续阅读 »

高性能架构和系统设计经验


高性能和高并发,听着就有点类似,并且他们还经常一起提及,比如提高我们的并发性能,显然,高性能可以提高我们的并发,但是细化来看,他们是有区别的,他们的考量点的维度不同。高性能需要我们从单机维度到整体维度去考虑,更多的是先从编码角度、架构使用角度去让我们的单机(单实例)有更好的性能,然后再从整个系统层面来拥有更好的性能;高并发则直接是全局角度来让我们的系统在全链路下都能够抗住更多的并发请求。

一、高性能架构和系统设计的几个层面

高性能架构设计主要集中在单机优化、服务集群优化、编码优化三方面。但架构层面的设计是高性能的基础,如果架构层面的设计没有做到高性能,仅依靠优化编码,对整体系统的提升是有限的。我们从一个全局角度来看高性能的系统设计,需要整体考虑的包括如下几个层面:

  • 前端层面。 后端优化的再好,如果前端(客户端)的性能不 ok,那么对用户而言,他们的体感还是很差的,因此前端层也是有必要考虑的,只是不在我们本文的设计范围之内,在实际工作中是需要进行探讨的。

  • 编码实现层面:代码逻辑的分层、分模块、协程、资源复用(对象池,线程池等)、异步、IO 多路复用(异步非阻塞)、并发、无锁设计、设计模式等。

  • 单机架构设计层面: IO 多路复用、Reactor 和 Proactor 架构模式

  • 系统架构设计层面:架构分层、业务分模块、集群(集中式、分布式)、缓存(多级缓存、本地缓存)、消息队列(异步、削峰)

  • 基础建设层面:机房、机器、资源分配

  • 运维部署层面:容器化部署、弹性伸缩

  • 性能测试优化层面: 性能压测、性能分析、性能优化

二、前端层面

后端优化的再好,如果前端(客户端)的性能不 ok,那么对用户而言,他们的体感还是很差的,因此前端层也是有必要考虑的,只是不在我们本文的设计范围之内,在实际工作中是需要进行探讨的。

这里简单说明下,从我个人工作的经历来看,前端(客户端)这里可以优化的点包括但不限于:数据预加载、数据本地缓存、业务逻辑前置处理、CDN 加速、请求压缩、异步处理、合并请求、长连接、静态资源等

三、编码实现层面

编码实现层面:代码逻辑的分层、分模块、协程、资源复用(对象池,线程池等)、异步、IO 多路复用(异步非阻塞)、并发、无锁设计、设计模式等。

多线程、多协程

大多数情况下,多进程、多线程、多协程都可以大大提高我们的并发性能,尤其是是多协程。

在网络框架层面,现在一般成熟的后端系统框架(服务化框架)都是支持多线程、多协程的,因此对于网络框架这点,我们只要是引用相对成熟的服务化框架来实现我们的业务,基本上可以不用过多考虑和设计。

在业务层面,如果是 Go 语言,天然支持大量并发,并且创建 Go 的协程非常容易,一个 go 关键字就搞定,因此多协程那就非常容易了,Go 里面可以创建大量协程来提高我们的并发性能。如果是其他语言,我们尽可能的使用多协程、多线程去执行我们的业务逻辑。

无锁设计(lock free)

在多线程、多协程的框架下,如果我们并发的线程(协程)之间访问共享资源,那么需要特别注意,要么通过加锁、要么通过无锁化设计,否则没有任何处理的访问共享资源会产生意想不到的结果。而加锁的设计,在并发较大的时候,如果锁的力度不合适,或者频繁的加锁解锁,又会使我们的性能严重下降。

为此,在追求高性能的时候,大家就比较推崇无锁化的设计。目前很多后台底层设计,为了避免共享资源的竞争,都采用了无锁化设计,特别是在底层框架上。无锁化主要有两种实现,无锁队列和原子操作。

  • 无锁队列。可以通过 链表或者 RingBuffer(循环数组)来实现无锁队列。

  • 原子操作。利用硬件同步原语 CAS 来实现各种无锁的数据结构。比如 Go 语言中的 atomic 包、C++11 语言中的 atomic 库。

数据序列化

为什么要说 数据序列化协议?因为我们的系统,要么就是各个后端微服务之间通过 RPC 做交互,要么就是通过 HTTP/TCP 协议和前端(终端)做交互,因此不可避免的需要我们进行网络数据传输。而数据,只有序列化后,才方便进行网络传输。

序列化就是将数据结构或对象转换成二进制串的过程,也就是编码的过程,序列化后,会把数据转换为二进制串,然后可以进行网络传输;反序列化就是在序列化过程中所生成的二进制串转换成数据结构或者对象的过程,将二进制转换为对象后业务才好进行后续的逻辑处理。

常见的序列化协议如下

  • Protocol Buffer(PB)

  • JSON

  • XML

  • 内置类型(如 java 语言就有 java.io.Serializable)

常见的序列化协议的对比在网上有各种性能的对比,这里就不在贴相关截图了,只说结论:从性能上和使用广泛度上来看,后端服务之间现在一般推荐使用 PB。如果和前端交互,由于 HTTP 协议只能支持 JSON,因此一般只能 JSON。

池化技术(资源复用)

池化技术是非常常见的一个提高性能的技术,池化的核心思想就是对资源进行复用,减少重复创建销毁所带来的开销。复用就是创建一个池子,然后再在这个池子里面对各种资源进行统一分配和调度,不是创建后就释放,而是统一放到池子里面来复用,这样可以减少重复创建和销毁,从而提高性能。而这个资源就包括我们编程中常见到如 线程资源、网络连接资源、内存资源,具体到对应的池化技术层面就是 线程池(协程池)、连接池、内存池等。

  • 线程池(协程池)。本质都是进程、线程、协程这些维度的一个池子,先创建合适数量的线程(协程)并且初始处于休眠状态,然后当需要用到的时候,从池子里面唤醒一个,然后执行业务逻辑,处理完业务逻辑后,资源并不释放,而是直接放回池子里面休眠,等待后续的请求被唤醒,这样重复利用。

    • 创建线程的开销是很大的,因此如果来一个请求就频创建一个线程、进程,那么请求的性能肯定不会太高。

  • 连接池。这个是最常用的,一般我们都要操作 MySQL、Redis 等存储资源,同样的,我们并不是每次请求 MySQL、Redis 等存储的时候就新创建一个连接去访问数据,而是初始化的时候就创建合适数量的连接放到池子里面,当需要连接去访问数据的时候,从池子里面获取一个空闲的连接去访问数据,访问完了之后不释放连接,而是放回池子里面。

    • 连接池需要保证连接的可用性,就是这个连接和 MySQL、Redis 等存储是必须要定期发送数据来保证连接的,要不然会被断开。同时我们要针对已经失效(断开)的连接进行检测和摘除。

  • 内存池。常规的情况下,我们都是直接调用 new、malloc 等 Linux 操作系统的 API 来申请分配内存,而每次申请的内存块的大小不定,所以,当我们频繁 分配内存、回收内存的时候,会造成大量的内存碎片,同时每次使用内存都要重新分配也会降低性能。内存池就是先预先分配足够大的一块内存,当做我们的内存池,然后每次用户请求分配内存的时候,就会返回内存池中的一块空闲的内存,并将这块内存的标志置为已使用,当内存使用完毕释放内存的时候,也不是真正地调用 free 或 delete 来释放内存,而是把这块内存直接放回内存池内并且同时把标志置为空闲。一般业内都有相关的套件来帮我们来做这个事情,比如在 C/C++ 语言里面,都有相关库去封装原生的 malloc,glibc 实现了一个 ptmalloc 库,Google 实现了一个 tcmalloc 库。

  • 对象池。其实前面几种类型的池化技术,其实都可以作为对象池的各种应用,因为各种资源都可以当做一个对象。对象池就是避免大量创建同一个类型的对象,从而进行池化,保证对象的可复用性。

异步IO 和 异步流程

异步有两个层面的意思:

  • IO 层面的异步调用

  • 业务逻辑层面的异步流程

异步是相对同步而言的,同步就是要等待前面一个事情执行完毕才能继续执行,异步就是可以不用等待,可想而知,异步的性能要比同步好很多。

IO 层面的异步调用

针对 IO 层面的异步调用,就是我们常说的 I/O 模型,有 阻塞、非阻塞、同步、异步这几种类型。在 Linux 操作系统内核中,内置了 5 种不同的 IO 交互模式,分别是阻塞 IO、非阻塞 IO、多路复用 IO、信号驱动 IO、异步 IO

针对网络 IO 模型而言,Linux 下,使用最多性能较好的是同步非阻塞模型,具体代表是 AIO,而 Windows 下的代表作 IOCP 则实现了真正的异步非阻塞 I/O。

业务逻辑层面的异步流程

业务逻辑层面的异步流程,就是指让我们的应用程序在业务逻辑上可以异步的执行。通常比较复杂的业务,都会有很多步骤流程,如果所有步骤都是同步的话,那么当这些步骤中有一步卡住,那么整个流程都会卡住,这样的流程显然性能不会很高。为此,在业内,我们如果想要提高性能,提高并发,那么基本上都会采用异步流程的方式。

举个实际的应用案例,针对 IM 系统的发送消息的这个场景,比如微信发送消息,那么当客户端发送的消息,服务端收到后,这个消息肯定要落地存储,这个发送的流程才能算完毕,但是,如果每条消息,服务端都真正存储到 DB 后再返回给客户端说已经正确收到,那么这个性能显然会很低,因为我们知道,写 DB 的性能是很低的,尤其是像微信这种每天有大量消息的 APP。那么这个流程就可以异步化,服务端收到消息后,先把消息写入消息队列,写入队列成功就返回给客户端,然后异步流程去从消息队列里面消费数据然后落地存储到 DB 里面,这样性能就非常高了,因为消息队列的性能会很高。而比较低性能的操作都是异步处理。

并发流程

并发流程,同样是针对我们上层的应用程序而言的,我们在处理业务逻辑的时候,尤其是相对负责的业务逻辑,一般下游都可能会有多个请求,或者说多个流程,如果依赖的下游多个请求之间没有强依赖关系,那么我们可以将这些请求的流程并发处理,这个是后端系统设计里面非常常见的优化手段。

通过并发的处理流程,可以将串行的叠加处理耗时优化为单个处理耗时,这样就大大的降低了整体耗时,举个例子,一个商品活动页面,渲染的数据包括 用户基本信息、用户活动积分、用户推荐商品列表。那么当收到这个用户的请求的时候,我们需要 查询用户的基本信息、用户的活动积分,还有用户的商品推荐,而这 3 个步骤完全是没有相互依赖关系的,因此,我们可以并发去分别查询,这样可以极大的减少耗时,从而提高我们的性能。

四、单机架构设计层面

单机优化的关键点

单机优化层面就是要尽量提升单机的性能,将单机的性能发挥到极致的其中一个关键点就是我们服务器采取的并发模型,然后在这个模型下,去设计好我们的服务器对连接的管理、对请求的处理流程。而这些就涉及到我们的多协程、多线程的进程模型和异步非阻塞、同步非阻塞的 IO 模型。

在具体实现细节上,针对连接的管理,要想提高性能,那么就要采用 IO 多路复用技术,可以参考I/O Multiplexing查看,I/O 多路复用技术的两个关键点在于:

  • 当多条连接共用一个阻塞对象后,进程只需要在一个阻塞对象上等待,而无须再轮询所有连接,常见的实现方式有 select、epoll、kqueue 等。

  • 当某条连接有新的数据可以处理时,操作系统会通知进程,进程从阻塞状态返回,开始进行业务处理。

IO 多路复用(epoll 模型)

基本上来说,异步 I/O 模型的发展技术是: select -> poll -> epoll -> aio -> libevent -> libuv。

而且现在大家比较熟悉和使用的最多的恐怕就是 epoll 和 aio ,尤其是 epoll 模型,基本是 Linux 后端系统下的大部分框架和软件都是采用 epoll 模型。

但是,需要特别强调的是,仅仅依靠 epoll 不是万能的,连接数太多的时候单进程的 epoll 也是不行的。

Reactor 和 Proactor 架构模式

epoll 只是一个 IO 多路复用的模型,在后端系统设计里面,要想实现单机的高性能,那在 IO 多路复用基础之上,我们的整个网络框架,还需要配合池化技术来提高我们的性能。因此,业界一般都是采用 I/O 多路复用 + 线程池(协程池、进程池)的方式来提高性能。与之对应的,在业界常用的两个单机高性能的架构模式就是Reactor 和 Proactor 模式。Reactor 模式属于非阻塞同步网络模型,Proactor 模式属于非阻塞异步网络模型。

在业内开源软件里面,Redis 采用的是 单 Reactor 单进程的方式,Memcache 采用的是 多 Reactor 多线程的方式,Nginx 采用的是多 Reactor 多进程的方式。关于 的详细介绍,可以查看The Design and Implementation of the Reactor

Redis 可以用单进程 Reactor 模式的是因为 Redis 的应用场景是内部访问,并发数一般不会超过 1w,而 Nginx 必须用多进程 Reactor 模式是因为 Nginx 是外网访问,并发数很容易超过 1w,因此我们的网络架构模式,必须要通过 I/O 多路复用 + 线程池(协程池、进程池)来配合。

可以看到,单机优化层面其实和编码层面上的多协程、异步 IO、 池化技术都是有强关联的。这里也是一个知识相通的典型,我们所学的一些基础层面的知识点,在架构层面、模型层面都是有用武之地的。

五、系统架构设计层面

架构设计层面:架构分层、业务分模块、集群(集中式、分布式)、缓存(多级缓存、本地缓存)、消息队列(异步、削峰)

架构和模块划分的设计

整个系统想要有一个高性能,那么首先就需要有个合理的架构设计,这里需要根据一些架构设计原则,比如高内聚低耦合,职责单一等来去构建我们的架构。最有效的方式包括架构分层设计、业务分模块设计。

这么设计之后,在整体的系统性能优化上,后面就会有比较大的优化空间,从而不至于后面想要优化就根本无从下手,只能重构系统。

服务化框架的设计

目前的互联网时代,我们基本上都是采用微服务来搭建我们的系统,而微服务化的必要条件就是要有一套服务化框架,这个服务化框架最核心的功能包括 RPC 请求和最基础的服务治理策略(服务注册和发现、负载均衡等)。

为此,这里服务化框架的性能就尤为重要,这里主要包括这个服务化框架里面实现:

  • 数据处理。

    • 数据序列化协议,一般有些采用 PB 协议,不管是从性能还是维护都是最优的。

    • 数据压缩,一般采用 gzip 压缩,压缩后可以减少网络上的数据传输。

  • 网络模型。

    • 同步还是异步流程,如果是 Go 语言,那么可以来一个请求 go 一个协程来处理。

    • 是否有相关连接池的能力。

    • 其他的一些优化。

负载均衡

负载均衡系统是水平扩展的关键技术,通过负载均衡,相当于可以把流量分散到不同的机器的不同的服务实例里面,这样每个服务实例都可以承担一部分请求,从而可以提高我们的整体系统的性能。

对于负载均衡的方式,大都是在客户端发现模式(client-side) 来实现服务路和负载均衡,一般也都会支持常见的负载均衡策略,如随机,轮训,hash,权重,连接数【连接数越少,优先级越高】。

合理采用各种队列

在后端系统设计里面,很多流程和请求并不要求实时处理,更不需要做到强一致,大部分情况下,我们只需要实现最终一致性就可以了。故而,我们通过队列,就可以使我们的系统能够实现异步处理逻辑、流程削峰、业务模块解耦、柔性事务等多种效果,从而可以完成最终一致性,并且能够极大的提高我们系统的性能。

我们常见的队列包括

  • 消息队列:使用的最为广泛的队列之一,代表作有 RabbitMQ、RocketMQ、Kafka 等。可以用来实现异步逻辑、削峰、解耦等多种效果。从而可以极大的提高我们的性能

  • 延迟队列:延时队列相比于普通队列最大的区别就体现在其延时的属性上,普通队列的元素是先进先出,按入队顺序进行处理,而延时队列中的元素在入队时会指定一个延迟时间,表示其希望能够在经过该指定时间后处理。延迟队列的目的是为了异步处理。延迟队列的应用场景其实也非常的广泛,比如说以下的场景:

    • 到期后自动执行指定操作。

    • 在指定时间之前自动执行某些动作

    • 查询某个任务是否完成,未完成等待一定时间再次查询

    • 回调通知,当回调失败时,等待后重试

  • 任务队列:将任务提交到队列中异步执行,最常见的就是线程池的任务队列。

各级缓存的设计

分布式缓存

分布式缓存的代表作有 Redis、Memcache。通过分布式缓存,我们可以不直接读数据库,而是读取缓存来获取数据,可以极大的提高我们读数据的性能。而一般的业务都是读多写少,因此,对我们的整体性能的提高是非常有效的手段,而且是必须的手段。

本地缓存

本地缓存可以从几个维度来看:

  • 客户端的本地缓存:针对一些不常改变的数据,客户端也可以缓存,这样就可以避免请求后端,从而可以改善性能

  • 后端服务的本地缓存:后端服务中,一般都会采用分布式缓存,但是,有些场景下,如果我们的数据量比较小,那么可以直接将这些数据缓存到进程里面,这样直接通过内存读取,而不用网络耗时,性能会更高。但是本地缓存一般只会缓存少量数据。数据量太大就不合适。

多级缓存

多级缓存是一个更为高级的缓存架构设计,比如最简单的模式可以是 本地缓存 + 分布式缓存这样形成一个多级缓存架构。

我们把全量要缓存的数据都放到分布式缓存里面,然后把一些热点的少量缓存放到本地缓存里面,这样大部分热点数据都能够从本地直接读取,而其他非热点的数据还是通过分布式缓存读取,这样可以极大的提高我们的性能,提高并发能力。

举个例子,电商系统里面,我们做一个活动页,活动页的前面 10 个商品是特卖商品,然后后面的其他商品就是常规商品,因为是活动页面,那么这个页面的访问肯定就会非常大。而活动页面的前 10 个商品,必然是用户首先进来页面就一定会看到的,而用户想要继续看其他商品,那么就需要在手机上手动上滑刷新一下。这个场景下,前面 10 个商品的访问量无疑是最大的,而用户手动上滑刷新后的请求就会少很多。为此,我们可以把全量商品都缓存在分布式缓存如 redis 里面,然后再在这个基础之上,把前面 10 个商品的信息缓存到本地,这样,当活动开始后,拉取的第一页 10 个商品数据,都是从本地缓存拉取的,本地读取性能会非常高,因为内存读取就行,完全不需要网络交互。

其他的模式,可以 本地缓存 + 二级分布式缓存 + 一级分布式缓存,也就是针对分布式缓存再做一层分级,这样每一级的缓存都能抗一部分的量,因此整体来看,能够对外提供的性能就足够高。

缓存预热

通过异步任务提前将接下来要大量访问的数据预热到我们缓存里面。这样当有请求的突峰的时候,可以从容应对。

其他高性能的 NoSQL

除了 Redis、本地缓存这些,其他的一些 NoSQL 中,MongoDB、Elasticserach 也是常见的性能很高的组件,我们可以根据适用场景,合理选用。

比如我们在电商系统里面,我们针对商品的搜索、推荐都是采用 Elasticserach 来实现。

存储的设计

数据分区

数据分区是把数据按一定的方式分成多个区(比如通过地理位置),不同的数据区来分担不同区的流量,这需要一个数据路由的中间件,但会导致跨库的 Join 和跨库的事务非常复杂。

将数据分布到多个分区有两种比较典型的方案:

  • 根据键做哈希,根据哈希值选择对应的数据节点。

  • 根据范围分区,某一段连续的键都保存在一个数据节点上。

分库分表

一般来说,影响数据库最大的性能问题有两个,一个是对数据库的操作,一个是数据库中数据的大小。对于前者,我们需要从业务上来优化。一方面,简化业务,不要在数据库上做太多的关联查询,而对于一些更为复杂的用于做报表或是搜索的数据库操作,应该把其移到更适合的地方。比如,用 ElasticSearch 来做查询,用 Hadoop 或别的数据分析软件来做报表分析。对于后者,一般就是拆分。分库分表技术,有些地方也称为 Sharding、分片,通过分库分表可以提高我们的读写性能

分库分表有垂直切分和水平切分两种:

  • 垂直切分(分库),一般按照业务功能模块来划分,分库后分表部署到不同的库上。分库是为了提高并发能力,比如读写请求量大就需要分库。

  • 水平切分(分表),当一个表中的数据量过大时,我们可以把该表的数据通过各种 ID 的 hash 散列来划分,比如 用户 ID、订单 ID 的 hash。分表更多的是应对性能问题,比如查询慢的问题。单表一般情况下,千万级别后各种性能就开始下降了,就要考虑开始分表了。

分表包括垂直切分和水平切分,而分区只能起到水平切分的作用。

读写分离

互联网系统大多数都是读多写少,因此读写分离可以帮助主库抗量,读写分离就是将读的请求量改为从库承担,写还是主库来承担。一般我们都是一主多从的架构,既可以抗量,又可以保证数据不丢。

冷热分离

针对业务场景而言,如果数据有冷热之分的话,可以将历史冷数据与当前热数据分开存储,这样可以减轻当前热数据的存储量,可以提高性能。

我们常见的存储系统比如 MySQL、Elasticserach 等都可以支持。

分布式数据库

分布式数据库的基本思想是将原来集中式数据库中的数据分散存储到多个通过网络连接的数据存储节点上,以获取更大的存储容量和更高的并发访问量,从而提高我们的性能。现在传统的关系型数据库已经开始从集中式模型向分布式架构发展了。一般云服务厂商,都会提供分布式数据库的解决方案,比如腾讯云的 TDSQL MySQL 版,TDSQL for MySQL 是腾讯打造的一款分布式数据库产品,具备强一致高可用、全球部署架构、分布式水平扩展、高性能、企业级安全等特性,同时提供智能 DBA、自动化运营、监控告警等配套设施,为客户提供完整的分布式数据库解决方案。

六、基础建设层面

基础建设层面,大体分为 3 大块:

  • 机房层面,主要关注机房的网络出口带宽、入口带宽。一般这个对我们业务开发来说,都接触不到,但是这里还是需要注意,如果机房带宽不够,那么我们的服务就支撑不了大的并发,从而也没法让我们的系统有一个好的性能。

  • 机器配置层面,服务器本身的性能要足够好,包括 CPU、内存、磁盘(SSD)等资源。同理,一般这个对我们业务开发来说,都接触不到,但是如果机器配置较差,那么我们的服务部署在这样的机器上面,也无法充分发挥,从而使得我们的业系统也无法拥有一个好的性能。

  • 资源使用层面,我们要合理的分配 CPU 和内存等相关资源,一般 CPU 的使用率不要超过 70%-80%,超过这个阈值后,我们服务的性能就会开始下降,因此一般我们在 70% 的时候就要开始执行扩容。如果是 K8s 容器部署的话,我们可以设置 CPU 使用率超过指定阈值后就自动扩容。当然,如果是物理机部署,或者其他方式,可以同样的进行监控和及时扩容。也就是说,要保证我们所需的各种资源(CPU、内存、磁盘、带宽)都在一个合理的范围。

七、运维部署层面

在运维部署层面做好相关建设,是有助于提高我们系统的整体性能的。比如,我们可以通过容器化部署做到弹性伸缩,通过弹性伸缩的能力,可以使得我们的服务,在资源分配使用上,一直保持合理的 CPU、内存等资源的使用率。

八、性能测试优化层面

我们从架构设计层面、编码实现层面按照高性能的解决方案和思路实现了我们系统之后,理论上,我们的系统性能不会太差,但是,具体我们的系统性能如何?是否存在可优化点?代码的实现是否有性能问题?我们的依赖服务是否存在性能问题?等等,这些对我们大部分人来说,如果没有一个合理的性能压测和分析,那么可能还是黑盒的。

因此,针对我们研发人员而言,在高性能架构设计方面的最后一个环节,就是进行性能测试优化,具体包括三个环节:

  • 性能压测。针对系统的各个环节先分别做压测,然后有条件的情况下,最好能够做全链路压测。

  • 性能分析。压测后,最优的分析方式是结合火焰图去分析,看看性能最差的是哪里,是否有可优化的点。一定是先找到性能最差的进行优化,这样事半功倍

  • 性能优化。找到可优化点后,进行优化。然后反复这三个步骤,直到你认为性能已经完全符合预期。

作者:AllenWu
来源:juejin.cn/post/7198476152633163831

收起阅读 »

关于我加了一行日志搞崩了服务这件小事(下)

接:关于我加了一行日志搞崩了服务这件小事(上)// 方案一 - 这里会根据当前属性名和clazz来判断是否被忽略了,详见@JsonType注解           boolean ignor...
继续阅读 »

接:关于我加了一行日志搞崩了服务这件小事(上)

// 方案一 - 这里会根据当前属性名和clazz来判断是否被忽略了,详见@JsonType注解
           boolean ignore = isJSONTypeIgnore(clazzpropertyName);
// 如果忽略了,就不再往下走了
           if (ignore) {
               continue;
          }
//此时根据属性和类获取对应的值对象。
           Field field = ParserConfig.getField(clazzpropertyName);
           JSONField fieldAnnotation = null;
           if (field != null) {
               //方案二 - 会获取属性对应的JSONField注解
               // 如果该注解的serialize属性是false,那么也不会继续往下去加载逻辑
               fieldAnnotation = field.getAnnotation(JSONField.class);
               if (fieldAnnotation != null) {
                   if (!fieldAnnotation.serialize()) {
                       continue;
                  }
//获取顺序
                   ordinal = fieldAnnotation.ordinal();
                   serialzeFeatures = SerializerFeature.of(fieldAnnotation.serialzeFeatures());
                   if (fieldAnnotation.name().length() != 0) {
                       //获取名字
                       propertyName = fieldAnnotation.name();
                       if (aliasMap != null) {
                           propertyName = aliasMap.get(propertyName);
                           if (propertyName == null) {
                               continue;
                          }
                      }
                  }
                   if (fieldAnnotation.label().length() != 0) {
                       label = fieldAnnotation.label();
                  }
              }
          }
           if (aliasMap != null) {
               propertyName = aliasMap.get(propertyName);
               if (propertyName == null) {
                   continue;
              }
          }
           //这里会新构建一个fieldInfo对象,并存放到fieldInfoMap中进行保存
           FieldInfo fieldInfo = new FieldInfo(propertyNamemethodfieldclazznullordinalserialzeFeatures,
                                               annotationfieldAnnotationlabel);
           fieldInfoMap.put(propertyNamefieldInfo);
      }
       //紧接着第二部分是关于isXXX的方法
       if (methodName.startsWith("is")) {
           if (methodName.length() < 3) {
               continue;
          }
           char c2 = methodName.charAt(2);
           String propertyName;
           if (Character.isUpperCase(c2)) {
               if (compatibleWithJavaBean) {
                   propertyName = decapitalize(methodName.substring(2));
              } else {
                   propertyName = Character.toLowerCase(methodName.charAt(2)) + methodName.substring(3);
              }
          } else if (...) {
          //同上面几乎一样,也是针对is_x这类特殊写法做了处理。
          }else {
               continue;
          }
           Field field = ParserConfig.getField(clazzpropertyName);
           if (field == null) {
               field = ParserConfig.getField(clazzmethodName);
          }
           JSONField fieldAnnotation = null;
           if (field != null) {
               //同样是对JSONField注解做处理。
               fieldAnnotation = field.getAnnotation(JSONField.class);
               if (fieldAnnotation != null) {
                   if (!fieldAnnotation.serialize()) {
                       continue;
                  }
                   ordinal = fieldAnnotation.ordinal();
                   serialzeFeatures = SerializerFeature.of(fieldAnnotation.serialzeFeatures());
                   if (fieldAnnotation.name().length() != 0) {
                       propertyName = fieldAnnotation.name();
                       if (aliasMap != null) {
                           propertyName = aliasMap.get(propertyName);
                           if (propertyName == null) {
                               continue;
                          }
                      }
                  }
                   if (fieldAnnotation.label().length() != 0) {
                       label = fieldAnnotation.label();
                  }
              }
          }
           if (aliasMap != null) {
               propertyName = aliasMap.get(propertyName);
               if (propertyName == null) {
                   continue;
              }
          }
           FieldInfo fieldInfo = new FieldInfo(propertyNamemethodfieldclazznullordinalserialzeFeatures,
                                               annotationfieldAnnotationlabel);
           fieldInfoMap.put(propertyNamefieldInfo);
      }
  }
//最后,又是对所有的常规属性做相应的处理,避免因为某个属性没写getX()方法而得不到序列化。整体的加载逻辑同上。
   for (Field field : clazz.getFields()) {
       if (Modifier.isStatic(field.getModifiers())) {
           continue;
      }
       JSONField fieldAnnotation = field.getAnnotation(JSONField.class);
       int ordinal = 0serialzeFeatures = 0;
       String propertyName = field.getName();
       String label = null;
       if (fieldAnnotation != null) {
           if (!fieldAnnotation.serialize()) {
               continue;
          }
           ordinal = fieldAnnotation.ordinal();
           serialzeFeatures = SerializerFeature.of(fieldAnnotation.serialzeFeatures());
           if (fieldAnnotation.name().length() != 0) {
               propertyName = fieldAnnotation.name();
          }
           if (fieldAnnotation.label().length() != 0) {
               label = fieldAnnotation.label();
          }
      }
       if (aliasMap != null) {
           propertyName = aliasMap.get(propertyName);
           if (propertyName == null) {
               continue;
          }
      }

       if (!fieldInfoMap.containsKey(propertyName)) {
           FieldInfo fieldInfo = new FieldInfo(propertyNamenullfieldclazznullordinalserialzeFeatures,
                                               nullfieldAnnotationlabel);
           fieldInfoMap.put(propertyNamefieldInfo);
      }
  }

   List<FieldInfo> fieldInfoList = new ArrayList<FieldInfo>();

   boolean containsAll = false;
   String[] orders = null;

   JSONType annotation = clazz.getAnnotation(JSONType.class);
   if (annotation != null) {
       orders = annotation.orders();

       if (orders != null && orders.length == fieldInfoMap.size()) {
           containsAll = true;
           for (String item : orders) {
               if (!fieldInfoMap.containsKey(item)) {
                   containsAll = false;
                   break;
              }
          }
      } else {
           containsAll = false;
      }
  }

   if (containsAll) {
       for (String item : orders) {
           FieldInfo fieldInfo = fieldInfoMap.get(item);
           fieldInfoList.add(fieldInfo);
      }
  } else {
       for (FieldInfo fieldInfo : fieldInfoMap.values()) {
           fieldInfoList.add(fieldInfo);
      }
       if (sorted) {
           Collections.sort(fieldInfoList);
      }
  }
   return fieldInfoList;
}

代码有点长,听我一点点地慢慢解释。整个代码其实比较容易理解,我尝试从我们常规角度来理解下。fastJson组件的发明者认为,类中常见需要序列化的类型有三种:

1、getX()方法;

2、isX()方法;

3、没有写getX()方法的固有变量。

围绕这三种类型他做的事都是类似的。这里我们先以getX()方法为例子展开说明,要获取到所有的getX()方法,并对他们解析,主要分为以下四个步骤:

1、获取到所有的类下的方法信息

这个可以通过class<?>.getMethods()方法获得,如下是我coreData类的所有方法。


2、判断符合规范的getXXX方法

在获取到了所有的method以后,我们自然需要判断哪些是符合规范的getXX方法。在组件中是这么判断的:

if (methodName.startsWith("get")) {
   //此时做相应的处理逻辑  
}

没错,就是这么粗暴简单。

3、根据JSONType判断是否需要加载

那么获取到这些方法就一定要加载了吗?当然不是!对于getter方法,fastJson会首先判断当前的属性,是否已被包含在了类的@JSONType(ignores = "xxx")下,如果包含在了其中,那么此时就不会去将该方法保存到待序列化的列表中。局限点在于该种写法只会对get方法生效,对于isXXX和普通属性是不会生效的。

// 方案一 - 这里会根据当前属性名和clazz来判断是否被忽略了,详见@JsonType注解
boolean ignore = isJSONTypeIgnore(clazzpropertyName);
// 如果忽略了,就不再往下走了
if (ignore) {
   continue;
}


4、根据JSONField判断是否需要加载

什么?你说采用JSONType写一大堆不方便?fastJson自然也是想到了,那么此时就可以采用@JSONField(serialize = false)的方式在对单独的属性或方法进行标注。也能起到忽略的作用。


到此,以getXX()方法的解析判断就完成了,当然其中还有一些更为细致的判断逻辑,如跳过getMetaClass、返回值为空的跳过等等逻辑。但大体上已经不影响我们的分析了。isXXX和固有变亮的解析几乎相似。至此,我们已经大致了解了整个解析的原理。当然为了验证我们的逻辑的正确性,我对原本coreData的代码做了一下改造并进行了试验,具体内容如下所示:

@Data
@JSONType(ignores = "funcProperties")
public class CoreData {

   //正常的属性
   public String normalProperties = "normalProperties";

   /**
    * 以get开头的方法
    * @return
    */
   public String getFuncProperties(){
       double a = 2/0;
       return "getFuncProperties";
  }

   /**
    * 以is开头的方法
    * @return
    */
   @JSONField(serialize = false)
   public Boolean isType(){
       return true;
  }

   /**
    * 用于跳过,检查方法是否判断
    * @return
    */
   public String skipFuncProperties(){
       double a = 2/0;
       return "getFuncProperties";
  }
}

简要来说,这里对getFuncProperties方法,我才用了@JSONType(ignores = "funcProperties")将其进行忽略,而对于isType方法,我则用单个的@JSONField(serialize = false)对其进行忽略,如果我们的结论成立,那么此时应该只会保存一个normalProperties属性的输出,且不存在出现报错的情况。


事实证明,我们是对的。

案件总结与反思:

在经历了这次惨痛的教训之后,有哪些是值得我们深入关注去思考和反思的呢?

1、在编写方法的时候尽量避免才用getXXX、isXXX的方法进行书写,这会导致部分框架的解析出现问题。(这个点也是我曾经在JAVA开发手册中看到的,想必也是前人被坑过了。)

2、如果非要这样写,那么此时需要评估好当前这个方法是否需要被一些框架进行解析,如果不需要,尝试对这些类型属性添加基本的忽略操作。类似@JSONField(seralize = false)、@Trasient等注解。

3、避免在对象中参杂进复杂的业务逻辑。(当然这条并不一定正常,对于DDD的充血模型,有时候是需要一定的业务逻辑的混合的。)

吃一堑长一智,如此一来才能避免在未来犯下相同的错误呀~


作者:DrLauPen
来源:
juejin.cn/post/7134513215890784293



收起阅读 »

关于我加了一行日志搞崩了服务这件小事(上)

周三的时候,组内出现了一个线上问题,影响到了若干个用户的下单、支付等操作。然而实际查询到问题的原因时,发现只是由于一行小小的日志打印导致的错误。1、对案件的发生进行回顾;3、对案件总结与反思案件回顾 找到代码行后却让值班同学感到疑惑:“这个明显是fastjso...
继续阅读 »

前言

周三的时候,组内出现了一个线上问题,影响到了若干个用户的下单、支付等操作。然而实际查询到问题的原因时,发现只是由于一行小小的日志打印导致的错误。

以下的文章内容分为主要分为三部分:

1、对案件的发生进行回顾;

2、分析案件发生的原因;

3、对案件总结与反思

以三章内容来回顾出现的问题,以及提供未来的预防策略。

案件回顾

周三的时候,服务频繁收到报警,系统频繁爆出空指针异常。值班同学根据报错的错误栈,快速定位到了错误的代码行。

at com.alibaba.fastjson.serializer.JSONSerializer.write(JSONSerializer.java:285)
at com.alibaba.fastjson.JSON.toJSONString(JSON.java:696)

找到代码行后却让值班同学感到疑惑:“这个明显是fastjson的日志打印呀,这也会有什么错误么?”。旁边的同事看完却惊呼一声:“fastJson打印日志会调用对象内的其余的get方法的呀!”。

(PS:该对象是一个DDD的核心域对象,其中包含一些业务场景方法被命名为getXXX方法的,因此执行Json序列化打印也就可能因为部分数据为空而出现空指针。)

定位到了问题原因,本着优先止损的原则,值班同事快速上线代码删除了这行日志打印。系统暂时的恢复了正常,没有再出现新增的报错信息了。然而后续还有漫长的数据修复、更正的过程。

案件分析:

案件复原:

本质上来说,这起线上事故出现的原因主要是因为fastJson序列化时,会将手工编写的一些方法认为是待输出属性对象,那么如果这些方法包含一些业务逻辑代码的时候,就会存在出现异常的风险。这里我们简单复现一下场景:

@Data
public class CoreData {
   //正常的属性
   public String normalProperties = "normalProperties";

   /**
    * 以get开头的方法 不是期望输出的属性
    * @return
    */
   public String getFuncProperties(){
       return "getFuncProperties";
  }

   /**
    * 以is开头的方法 不是期望输出的属性
    * @return
    */
   public Boolean isType(){
       return true;
  }
}

如上代码是我们编写的一个纯代码类,可以看到,我们实际期望设置的属性应该只有一个normalPropertites。

public static void main(String[] args) {
   CoreData data = new CoreData();
   String dataString = JSONObject.toJSONString(data);
   System.out.println(dataString); // 对应正常的业务逻辑
}

进而我还写了一段针对当前对象进行打印的代码,从上可以看到,就是简单的对对象进行JSON序列化后打印输出。按照我们的期望来说,只是期望输出normalProperties这一个固有的字符串属性。随后我运行了代码,得到了如下的结果:


可以看到,一个类型+两个方法,都被JSON序列化后输出了。那么如果此时我们在getFuncProperties()这样的方法中如果出现了异常,就会影响整个业务的运行。例如我们把方法改成如下的例子:

public String getFuncProperties(){
   double a = 2/0;
   return "getFuncProperties";
}


可以看到,我们原本的逻辑可能只是想输出normalProperties属性,但是因为getFuncProperties2/0是无法进行运算的,导致了系统直接报错了。那么此时,main函数中的输出方法(对应于我们正常业务逻辑),也就无法再继续执行了,而这在生产环境上无疑是致命的。

背后原理:

(PS: 以下讨论内容均基于1.2.9版本的fastJson。)

根据报错的问题点,结合debug,很快找到了问题所在:


com.alibaba.fastjson.serializer.JSONSerializer#write(java.lang.Object)这个方法中,Fastjson所创建的ObjectSerializer对象中,nature下所包含的getters对象有三个。这明显不符合我们的预期。那么我们就需要找到他是如何获取到这三个方法的。紧跟着我们进行追入,在com.alibaba.fastjson.serializer.SerializeConfig#getObjectWriter方法下找到了这行代码:

put(clazzcreateJavaBeanSerializer(clazz));

很明显,这里的createJavaBeanSerializer(clazz)创建了javaBean的序列化器。对于该方法,其主要的逻辑流程就是判断当前的对象类型是否符合使用ASM的序列化器。这里一通判断下来,是符合采用ASM序列化的要求的,因此,我们又进一步定位到了如下代码:

ObjectSerializer asmSerializer = createASMSerializer(clazz);

createASMSerializer对应的方法中,最关键的代码莫过于下面这行了:

List<FieldInfo> unsortedGetters = TypeUtils.computeGetters(clazzjsonTypealiasMapfalse);

这力的代码会生成对应的fieldInfo对象,也正好对应了前面我们涉及到的那三个方法,这里让我们仔细看一下com.alibaba.fastjson.util.TypeUtils#computeGetters所对应的代码:

public static List<FieldInfo> computeGetters(Class clazzJSONType jsonTypeMap<StringString> aliasMapboolean sorted) {
   Map<StringFieldInfo> fieldInfoMap = new LinkedHashMap<StringFieldInfo>();
   for (Method method : clazz.getMethods()) {
       String methodName = method.getName();
       int ordinal = 0serialzeFeatures = 0;
       String label = null;
//判读当前方法是否为静态的
       if (Modifier.isStatic(method.getModifiers())) {
           continue;
      }
//若返回值为void则此时不需要处理
       if (method.getReturnType().equals(Void.TYPE)) {
           continue;
      }
//若此时入参不为空则跳过
       if (method.getParameterTypes().length != 0) {
           continue;
      }
//若返回类型是类加载器也进行跳过。
       if (method.getReturnType() == ClassLoader.class) {
           continue;
      }
//若方法名是getMetaClass也跳过
       if (method.getName().equals("getMetaClass")
           && method.getReturnType().getName().equals("groovy.lang.MetaClass")) {
           continue;
      }
//获取方法的有关JSONField的注释
       JSONField annotation = method.getAnnotation(JSONField.class);
       if (annotation == null) {
           //若当前类为空,则再获取父类的。
           annotation = getSupperMethodAnnotation(clazzmethod);
      }
       if (annotation != null) {
           //若父类不为空则进行序列化的判断,我们使用的例子无继承,这部分先忽略不看。
          ......
      }
       //重点来了,判断当前是否以get开头
       if (methodName.startsWith("get")) {
           //长度小于4,即不满足getXX的格式的,直接跳过。
           if (methodName.length() < 4) {
               continue;
          }
           //getClass的进行跳过
           if (methodName.equals("getClass")) {
               continue;
          }
//获取第四个位置的字符
           char c3 = methodName.charAt(3);
           String propertyName;
           if (Character.isUpperCase(c3|| c3 > 512 ) {
               //若方法遵循驼峰的写法:则依次取出对应的名称信息
               if (compatibleWithJavaBean) {
                   propertyName = decapitalize(methodName.substring(3));
              } else {
                   propertyName = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4);
              }
          } else if (...) {
               //这里针对部分特殊的写法:如get_X、getfX做了特殊的判断处理。
          } else {
               continue;
          }

续:关于我加了一行日志搞崩了服务这件小事(下)

作者:DrLauPen
来源:juejin.cn/post/7134513215890784293

收起阅读 »

实现一个微信录音功能过程

web
功能原型图其实就是微信发送语音的功能。没有转文字的功能。拆解需求根据原型图可以很容易的得出我们需要做的内容包括下面三个部分:接入微信的语音SDK调用微信SDK的API逻辑界面和交互的实现其中第一点和第二点属于业务逻辑部分,第三点属于交互逻辑部分。对于业务逻辑和...
继续阅读 »

功能原型图


其实就是微信发送语音的功能。没有转文字的功能。

拆解需求

根据原型图可以很容易的得出我们需要做的内容包括下面三个部分:

  1. 接入微信的语音SDK

  2. 调用微信SDK的API逻辑

  3. 界面和交互的实现

其中第一点和第二点属于业务逻辑部分,第三点属于交互逻辑部分。对于业务逻辑和交互逻辑的关系在我的另外一篇文章描述过,我在vue中是这样拆分组件的 - 掘金 (juejin.cn)

从原型图可以分析出如下的流程图:

评估时间

第三事情是评估时间。在接到这个需求的时候,我们需要假设我们在此之前没有接入过微信相关的SDK,并以此为前提进行工期的评估。

可以将该用户故事拆分为如下任务:

  • 微信语音SDK的技术调研(0.5天)

  • 输出开发设计文档(0.5天)

  • 接入微信语音SDK(0.5天)

  • 编码(1天)

  • 自测(0.5天)

随后将上面的时间都乘以2! 自此才可以将估算的工期上报给产品。多年的经验告诉自己,自己一开始估算的工期从来没够过。自行估算的时候,幻想的是在工作的时候能够一直保持专注。

就我自己而言,做不到,上班不可能不摸鱼!也是必须要摸鱼的。乘以2才是刚够而已。

代码实现

都说在实现代码之前要先设计,谋定而后动。我是这样做的,先想好文件夹创建,然后到文件的创建,再到具体文件中写出大体的框架。

需求并不复杂,只是一个界面中的一个模块。所以我只需要一个Record.vue来承载界面,一个use-record-layout.js来承载业务逻辑,以及一个use-record-interact.js来承接交互逻辑。

|__im-record
  |__Record.vue
  |__use-record-layout.js
  |__use-record-interact.js

为了便于说明,将这个聊天的界面简化如下:

<script setup>
import { useNamespace } from "@/use-namespace";
const ns = useNamespace('chat')
</script>
<template>
 <header :class="ns.b('header')"></header>
 <main :class="ns.b('main')">
   <section :class="[ns.b('record'), ns.w('record', 'toast')]">
     <div :class="ns.w('record', 'speak')"></div>
     <div :class="ns.w('record', 'pause')"></div>
   </section>
 </main>
 <footer :class="ns.w('button', 'wrap')">
   <button :class="ns.b('button')">
     <span>
      按住 说话
     </span>
   </button>
 </footer>
</template>

通过上面的代码片段可知,我们的主要的界面在section标签的record部分。

use-record-layout.js的主题代码如下:

  const recordStyle = {
   default: { }, // 默认样式/确定发送录音
   recording: { }, // 录音中
   pause: { }, // 暂停录音
   cancel: { } // 取消录音
}

 const init = () => {
   initEvent()
   initStyle()
}

 const initStyle = () => {
   recordStyle.default.is = true
}

 const initEvent = () => {
   el.addEventListener('touchstart', handleTouchstart)
   el.addEventListener('touchmove', handleTouchmove)
   el.addEventListener('touchend', handleTouchend)
}

 const axis = {
   posStart: 0, // 初始化起点坐标
   posMove: 0 // 初始化滑动坐标
}
 const handleTouchstart = (event) => {
   event.preventDefault()
   axis.posStart = event.touches[0].pageY
   recordStyle.recording.is = true
}
 const handleTouchmove = (event) => {
   event.preventDefault()
   axis.posMove = event.targetTouches[0].pageY
   const diffMove = axis.posMove - axis.posStart
   if (diffMove > DEFAULT_AXIS) {
     recordStyle.recording.is = true
  }
}
 const handleTouchend = (event) => {
   event.preventDefault()
   recordStyle.default.is = true
}

 init()

其中recordStyle是交互的结果,在这个需求当中,我们的界面的四种变化都对应其中一个的样式。

use-record-interact.js也很简单,注册微信录音功能 ➡️

const wx = 'wx'
const useRecordInteract = () => {
 const isAuth = localStorage.getItem('allowWxRecord')
 // 获取录音权限
 const authRecord = () => {
   if (!isAuth) {
     wx.startRecord()
     return
  }

   return isAuth
}
 // 停止录音
 const stopRecord = () => {}
 // 上传录音
 const uploadRecord = () => {}
 
}

交互逻辑和业务逻辑的联动通过recordStyle对象的存取属性来实现,代码片段如下:

const interact = useRecordInteract()
const recordStyle = {
   default: {
     _is: false,
     get is() {
       return this._is
    },
     set is(value) {
       this._is = value
       if (value) {
         this.recording.is = false
         this.pause.is = false
         this.cancel.is = false
         
         interact.uploadRecord()
      }
    }
  },
   //...
}

实现了业务逻辑和交互逻辑的分离。

作者:砂糖橘加盐
来源:juejin.cn/post/7201491839815745597

收起阅读 »

如何自动打开你的 App?

相信大家在刷 某博 / 某书 / 某音 的时候,最能体会什么叫做 条条大路通 tao bao。经常是你打开一个 App,不小心点了下屏幕,就又打开了另一个 App 了。那么这种自动打开一个 App 到底是怎么实现的呢?URL Scheme首先是最原始的方式 U...
继续阅读 »


相信大家在刷 某博 / 某书 / 某音 的时候,最能体会什么叫做 条条大路通 tao bao。经常是你打开一个 App,不小心点了下屏幕,就又打开了另一个 App 了。

那么这种自动打开一个 App 到底是怎么实现的呢?

URL Scheme

首先是最原始的方式 URL Scheme。

URL Scheme 是一种特殊的 URL,用于定位到某个应用以及应用的某个功能。

它的格式一般是: [scheme:][//authority][path][?query]

scheme 代表要打开的应用,每个上架应用商店的 App 所注册的 scheme 都是唯一的;后面的参数代表应用下的某个功能及其参数。

在 IOS 上配置 URL Scheme

在 XCode 里可以轻松配置


在 Android 上配置 URL Scheme

Android 的配置也很简单,在 AndroidManifest.xml 文件下添加以下配置即可


通过访问链接自动打开 App

配置完成后,只要访问 URL Scheme 链接,系统便会自动打开对应 scheme 的 App。

因此,我们可以实现一个简单的 H5 页面来承载这个跳转逻辑,然后在页面中通过调用 location.href=schemeUrl 或者 <a href='schemeUrl' /> 等方式来触发访问链接,从而自动打开 App

优缺点分析

优点: 这个是最原始的方案,因此最大的优点就是兼容性好

缺点:

  1. 通过 scheme url 这种方式唤起 App,对于 H5 中间页面是无法感知的,并不知道是否已经成功打开 App

  2. 部分浏览器有安全限制,自动跳转会被拦截,必须用户手动触发跳转(即 location.href 行不通,必须 a 标签)

  3. 一些 App 会限制可访问的 scheme,你必须要在白名单内,否则也会被拦截跳转

  4. 通过 scheme url 唤起 App 时,浏览器会提示你是否确定要打开该 App,会影响用户体验

DeepLink

通过上述缺点我们可以看出,传统的 URL Scheme 在用户体验上是存在一定缺陷的。

因此,DeepLink 诞生了。

DeepLink 的宗旨就是通过传统的 HTT P链接就可以唤醒app,而如果用户没有安装APP,则会跳转到该链接对应的页面。

IOS Universal Link

在 IOS 上一般称之为 Universal Link。

【配置你的 Universal Link 域名】

首先要去 Apple 的开发者平台上配置你的 domains,假设是: mysite.com


【配置 apple-app-site-association 文件】

在该域名根目录下创建一个 .well-known 路径,并在该路径下放置 apple-app-site-association 文件。

文件内容包含 appID 以及 path,path如果配置 /app 则表示访问该域名下的 /app 路径均能唤起App

该文件内容大致如下:

{
   "applinks": {
       "apps": [],
       "details": [
          {
               "appID": "xxx", // 你的应用的 appID
               "paths": [ "/app/*"]
          }
      ]
  }
}
复制代码

【系统获取配置文件】

上面两步配置成功后,当用户 首次安装App 或者后续每次 覆盖安装App 时,系统都会主动去拉取域名下的配置文件。

即系统会主动去拉取 https://mysite.com/.well-known/apple-app-site-association 这个文件

然后根据返回的 appID 以及 path 判断访问哪些路径是需要唤起哪个App

【自动唤起 App】

当系统成功获取配置文件后,只要用户访问 mysite.com/app/xxx 链接,系统便会自动唤起你的 App。

同时,客户端还可以进行一些自定义逻辑处理:

客户端会接收到 NSUserActivity 对象,其 actionType 为 NSUserActivityTypeBrowsingWeb,因此客户端可以在接收到该对象后做一些跳转逻辑处理。


Android DeepLink

与 IOS Universal Link 原理相似,Android系统也能够直接通过网站地址打开应用程序对应的内容页面,而不需要用户选择使用哪个应用来处理网站地址

【配置 AndroidManifest.xml】 在 AndroidManifest 配置文件中添加对应域名的 intent-filter:

scheme 为 https / http;

host 则是你的域名,假设是: mysite.com


【生成 assetlinks.json 文件】

首先要去 Google developers.google.com/digital-ass… 生成你的 assetlinks json 文件。


【配置 assetlinks.json 文件】

生成文件后,同样的需要在该域名根目录下创建一个 .well-known 路径,并在该路径下放置 assetlinks.json 配置文件,文件内容包含应用的package name 和对应签名的sha哈希

【系统获取配置文件】

配置成功后,当用户 首次安装App 或者后续每次 覆盖安装App 时,系统会进行以下校验:

  1. 如果 intent-filter 的 autoVerify 设置为 true,那么系统会验证其

  • Action 是否为 android.intent.action.VIEW

  • Category 是否为android.intent.category.BROWSABLE 和 android.intent.category.DEFAULT

  • Data scheme 是否为 http 或 https

  1. 如果上述条件都满足,那么系统将会拉取该域名下的 json 配置文件,同时将 App 设置为该域名链接的默认处理App

【自动唤起 App】

当系统成功获取配置文件后,只要用户访问 mysite.com/app/xxx 链接,系统便会自动唤起你的 App。

优缺点分析

【优点】

  1. 用户体验好:可以直接打开 App,没有弹窗提示

  2. 唤起App失败则会跳转链接对应的页面

【缺点】

  1. iOS 9 以后才支持 Universal Link,

  2. Android 6.0 以后才支持 DeepLink

  3. DeepLink 需要依赖远程配置文件,无法保证每次都能成功拉取到配置文件

推荐方案: DeepLink + H5 兜底

基于前面两种方案的优缺点,我推荐的解决方案是配置 DeepLink,同时再加上一个 H5 页面作为兜底。

首先按照前面 DeepLink 的教程先配置好 DeepLink,其中访问路径配置为 https://mysite.com/app

接着,我们就可以在 https://mysite.com/app 路径下做文章了。在该路径下放置一个 H5 页面,内容可以是引导用户打开你的 App。

当用户访问 DeepLink 没有自动打开你的 App 时,此时用户会进入浏览器,并访问 https://mysite.com/app 这个 H5 页面。

在 H5 页面中,你可以通过浏览器 ua 获取当前的系统以及版本:

  1. 如果是 Android 6.0 以下,那么可以尝试用 URL Scheme 去唤起 App

  2. 如果是 IOS / Android 6.0 及以上,那么此时可以判断用户未安装 App。这种情况下可以做些额外的逻辑,比如重定向到应用商店引导用户去下载之类的

作者:龙飞_longfe
来源:juejin.cn/post/7201521440612974649

收起阅读 »

IM 即时通讯实战:环信Web IM极速集成

前置技能Node.js 环境已搭建。npm 包管理工具的基本使用。Vue2 或者 Vue3 框架基本掌握或使用。学习目标项目中集成 IM 即时通讯实战利用环信 IM Web SDK 快速实现在 Vue.js 中发送出一条 Hello World!一、了解环信 ...
继续阅读 »

前置技能

  • Node.js 环境已搭建。
  • npm 包管理工具的基本使用。
  • Vue2 或者 Vue3 框架基本掌握或使用。

学习目标

  • 项目中集成 IM 即时通讯实战
  • 利用环信 IM Web SDK 快速实现在 Vue.js 中发送出一条 Hello World!

一、了解环信 IM

  1. 什么是环信 IM?

    环信即时通讯为开发者提供高可靠、低时延、高并发、安全、全球化的通信云服务,支持单聊、群聊、聊天室。提供多平台 SDK 支持,包括:Android、iOS、Web;同时,提供 EaseIM 和 EaseIMKit 以及服务端 REST API,帮助开发者快速构建端到端通信的场景。

  2. 学习完环信 WebIM 之后可以干嘛?

    可以在任意 Web 应用中极速集成搭建即时通讯功能,无论是自己搭建 IM 应用,还是实现产品需求均可以灵活集成进入到自己的项目之中。

二、环信 WebIM 实现通讯的基本流程

前置准备

  1. 有效的开发者 AppKey。 ( 注册环信)(注册参考文档)
  2. 使用 Vue-cli 创建一个空白项目,或已经具备已有待集成项目(此篇文章以 Vue3 为示例,Vue2 同样可以参考此文章)。
  3. 在项目中使用 npm 或者 yarn 安装环信 WebSDK 包,easemob-websdk
  4. 下载环信官方 Vue3-Demo

我们开始

初期配置

在确保已进行 npm install easemob-websdk 安装了环信 SDK 包,并已经下载了 Vue3 官方 Demo,将项目中的 IM 文件拖入自己的项目中。

此文件共两个功能:

  • 引入环信 WebIM-SDK
  • 将引入的 SDK 进行实例化

在这里插入图片描述

DEFAULT_APPKEY修改为自己已注册的 Appkey。

配置监听

<script setup>
import { EaseChatClient } from '@/IM/initwebsdk'
/* SDK连接 相关监听 */
EaseChatClient.addEventHandler('connection', {
onConnected: () => {}, //与环信服务器建联成功回调。
onDisconnected: () => {}, //与环信服务器断开成功回调。
onOnline: () => {}, // 本机网络连接成功。
onOffline: () => {},// 本机网络掉线。
onError: (error) => {}, //SDK Error 回调
})
/* 好友关系相关监听 */
EaseChatClient.addEventHandler('friendListen', {
// 收到好友邀请触发此方法。
onContactInvited: (data) => {},
// 联系人被删除时触发此方法。
onContactDeleted: (data) => {},
// 新增联系人会触发此方法。
onContactAdded: (data) => {},
// 好友请求被拒绝时触发此方法。
onContactRefuse: (data) => {},
// 好友请求被同意时触发此方法。
onContactAgreed: (data) => {}
})
/* message 相关监听 */
EaseChatClient.addEventHandler('messageListen', {
onTextMessage: function (message) {}, // 收到文本消息。
onEmojiMessage: function (message) {}, // 收到表情消息。
onImageMessage: function (message) {}, // 收到图片消息。
onCmdMessage: function (message) {}, // 收到命令消息。
onAudioMessage: function (message) {}, // 收到音频消息。
onLocationMessage: function (message) {}, // 收到位置消息。
onFileMessage: function (message) {}, // 收到文件消息。
onCustomMessage: function (message) {}, // 收到自定义消息。
onVideoMessage: function (message) {}, // 收到视频消息。
onRecallMessage: function (message) {}, // 收到消息撤回回执。
})
</script>

创建测试 ID

在这里插入图片描述

登录环信

这一步是所有后续操作的第一步

<script setup>
import { EaseChatClient } from '@/IM/initwebsdk'
const loginValue = reactive({
user: '', //你的测试环信ID
password: '' //你的测试环信ID密码
})
//登录接口调用
const loginIM = async () => {
try {
await EaseChatClient.open({
user: loginValue.username.toLowerCase(),
pwd: loginValue.password.toLowerCase()
}
);
} catch (error) {
console.log('>>>>登录失败', error);
}
}
</script>

紧接着是开始聊天部分。

好友关系

完成这个功能 需要将该项目开启两个页面,一个申请,一个接收,这样才能看到效果

两种方式:手动关联一个好友,第二种再创建一个测试 ID 之后,调用 SDK 添加好友。

方式一:测试时最简单的方式,手动关联好友

  1. 在管理后台中手动再创建一个 ID
    image.png

2.并手动将新创建的 ID 关联为好友。
在这里插入图片描述

方式二:开发时调用 SDK 接口添加好友

//申请添加好友
const applyAddFriends = () => {
EaseChatClient.addContact(targetId, '我想加你为好友!');
};
//接收方登录将会触发
EaseChatClient.addEventHandler('friendListen', {
// 收到好友邀请触发此方法。
onContactInvited: (data) => {
//同意申请
EaseChatClient.acceptContactInvite(data.from);
//拒绝申请
EaseChatClient.declineContactInvite(data.from);
},
});

进入页面获取好友列表并自行渲染。

<script setup>
//获取好友列表
const friendListData = reactive({})
const { data } = await EaseChatClient.getContacts()
data.length > 0 &&
data.map(item => (friendListData[item] = { hxId: item }))
</script>

收发消息

完成这个功能 需要将该项目开启两个页面,一个发送,一个接收,这样才能看到效果

发送方发送一条文本消息:

<script setup>
const props = defineProps({
nowPickInfo: {
type: Object,
required: true,
default: () => ({})
}
})
const { nowPickInfo } = toRefs(props)
const { ALL_MESSAGE_TYPE, CHAT_TYPE } = messageType
//发送文本内容
const textContent = ref('')
const sendTextMessage = _.debounce(async () => {
//如果输入框全部为空格同样拒绝发送
if (textContent.value.match(/^\s*$/)) return
const msgOptions = {
id: nowPickInfo.value.id, //要发送的目标ID
chatType: nowPickInfo.value.chatType,
msg: textContent.value,
}
textContent.value = '' //发送后清空输入框
try {
await store.dispatch('sendShowTypeMessage', { msgType: ALL_MESSAGE_TYPE.TEXT, msgOptions })
} catch (error) {
console.log('>>>>>>>发送失败+++++++', error)
}
}, 50)
</script>

接收方接收消息

/* message 相关监听 */
EaseChatClient.addEventHandler('messageListen', {
onTextMessage: function (message) {
console.log('>>>>收到文本消息');
pushNewMessage(message); //在缓存中Push一条新消息。
}, // 收到文本消息。
});

缓存的消息结构示例

messageList:{
//以好友的ID为KEY,如果获取则直接messageList[friendId]取到对应的消息。
friendId:[
{
chatType:"singleChat", //聊天类型 单聊或者群聊
ext:{}, //消息扩展
from:friendId, //消息来源ID
id:"1111864344594875684", //消息的唯一ID
msg:"Hello World!",//消息内容
time:1676440891009,//消息发送时间
to:myId,//发送目标ID
type:"txt" //消息来源
},
{
chatType:"singleChat",
ext:{},
from:friendId,
id:"1111864344594875684",
msg:"Hello World2!",
time:1676440891009,
to:myId,
type:"txt"
}
],
friendId2:[
{
chatType:"singleChat",
ext:{},
from:friendId,
id:"1111864344594875684",
msg:"Hello World!",
time:1676440891009,
to:myId,
type:"txt"
},
]
}

渲染消息列表

<script setup>
import { reactive, ref, computed, toRefs } from 'vue'
//获取其id对应的消息内容
const messageData = computed(() => {
//如果Message.messageList中不存在的话调用拉取漫游取一下历史消息
return nowPickInfo.value.id && store.state.Message.messageList[nowPickInfo.value.id] || fechHistoryMessage('fistLoad')()
})
<template>
<div>
<div class="messageList_box" v-for="(msgBody, index) in messageData" :key="msgBody.id">
<div v-if="!msgBody.isRecall && msgBody.type !== ALL_MESSAGE_TYPE.INFORM" class="message_box_item"
:style="{ flexDirection: (isMyself(msgBody) ? 'row-reverse' : 'row') }">
<div class="message_item_time">{{ handleMsgTimeShow(msgBody.time, index) || '' }}</div>
<el-avatar class="message_item_avator"
:src="isMyself(msgBody) ? loginUserInfo.avatarurl : otherUserInfo(msgBody.from).avatarurl || defaultAvatar">
</el-avatar>
<el-dropdown class="message_box_content"
:class="[isMyself(msgBody) ? 'message_box_content_mine' : 'message_box_content_other']"
trigger="contextmenu" placement="bottom-end">
<!-- 文本类型消息 -->
<p style="padding: 10px" v-if="msgBody.type === ALL_MESSAGE_TYPE.TEXT">
{{ msgBody.msg }}
</p>
<!-- 图片类型消息 -->
<!-- <div> -->
<el-image v-if="msgBody.type === ALL_MESSAGE_TYPE.IMAGE" style="border-radius:5px;"
:src="msgBody.thumb" :preview-src-list="[msgBody.url]" :initial-index="1" fit="cover" />
<!-- </div> -->
<!-- 语音类型消息 -->
<div :class="['message_box_content_audio', isMyself(msgBody) ? 'message_box_content_audio_mine' : 'message_box_content_audio_other']"
v-if="msgBody.type === ALL_MESSAGE_TYPE.AUDIO" @click="startplayAudio(msgBody, index)"
:style="`width:${msgBody.length * 10}px`">
<span class="audio_length_text">
{{ msgBody.length }}′′
</span>
<div :class="[isMyself(msgBody) ? 'play_audio_icon_mine' : 'play_audio_icon_other', audioPlayStatus.playIndex === index && 'start_play_audio']"
style=" background-size: 100% 100%;">
</div>
</div>
<div v-if="msgBody.type === ALL_MESSAGE_TYPE.LOCAL">
<p style="padding: 10px">[暂不支持位置消息展示]</p>
</div>
<!-- 文件类型消息 -->
<div v-if="msgBody.type === ALL_MESSAGE_TYPE.FILE" class="message_box_content_file">
<div class="file_text_box">
<div class="file_name">{{ msgBody.filename }}</div>
<div class="file_size">{{ fileSizeFormat(msgBody.file_length) }}</div>
<a class="file_download" :href="msgBody.url" download>点击下载</a>
</div>
<span class="iconfont icon-wenjian"></span>
</div>
<!-- 自定义类型消息 -->
<div v-if="msgBody.type === ALL_MESSAGE_TYPE.CUSTOM" class="message_box_content_custom">
<template v-if="msgBody.customEvent && CUSTOM_TYPE[msgBody.customEvent]">
<div class="user_card">
<div class="user_card_main">
<!-- 头像 -->
<el-avatar shape="circle" :size="50"
:src="msgBody.customExts && msgBody.customExts.avatarurl || msgBody.customExts.avatar || defaultAvatar"
fit="cover" />
<!-- 昵称 -->
<span class="nickname">{{ msgBody.customExts && msgBody.customExts.nickname ||
msgBody.customExts.uid
}}</span>
</div>
<el-divider style="margin:5px 0; border-top:1px solid black;" />
<p style="font-size: 8px;">个人名片</p>
</div>
</template>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-if="msgBody.type === ALL_MESSAGE_TYPE.TEXT && isSupported"
@click="copyTextMessages(msgBody.msg)">
复制
</el-dropdown-item>
<el-dropdown-item v-if="isMyself(msgBody)" @click="recallMessage(msgBody)">
撤回
</el-dropdown-item>
<el-dropdown-item @click="deleteMessage(msgBody)">
删除
</el-dropdown-item>
<el-dropdown-item v-if="!isMyself(msgBody)" @click="informOnMessage(msgBody)">
举报
</el-dropdown-item>
</el-dropdown-menu>
</template>

</el-dropdown>
</div>
<div v-if="msgBody.isRecall" class="recall_style">{{ isMyself(msgBody) ? "你" : `${msgBody.from}`
}}撤回了一条消息<span class="reEdit" v-show="isMyself(msgBody) && msgBody.type === ALL_MESSAGE_TYPE.TEXT"
@click="reEdit(msgBody.msg)">重新编辑</span></div>
<div v-if="msgBody.type === ALL_MESSAGE_TYPE.INFORM" class="inform_style">
<p>
{{ msgBody.msg }}
</p>
</div>

</div>
<ReportMessage ref="reportMessage" />
</div>


</template>
</script>

了解即时通讯IM及应用场景请访问:环信官网
更多集成IM教程请访问:IMGeek社区

收起阅读 »

七道Android面试题,先来简单热个身

马上就要到招(tiao)聘(cao)旺季金三银四了,一批一批的社会精英在寻找自己的下一家的同时,也开始着手为面试做准备,回想起自己这些年,也大大小小经历过不少面试,有被面试过,也有当过面试官,其中也总结出了两个观点,一个就是不花一定的时间背些八股文还真的不行,...
继续阅读 »

马上就要到招(tiao)聘(cao)旺季金三银四了,一批一批的社会精英在寻找自己的下一家的同时,也开始着手为面试做准备,回想起自己这些年,也大大小小经历过不少面试,有被面试过,也有当过面试官,其中也总结出了两个观点,一个就是不花一定的时间背些八股文还真的不行,一些扯皮的话别去听,都是在害人,另一个就是面试造火箭,入职拧螺丝毕竟都是少数,真正一场合格的面试问的东西,都是实际开发过程中会遇到的,下面我就说几个我遇到过的面试题吧

为什么ArrayMap比HashMap更适合Android开发

我们一般习惯在项目当中使用HashMap去存储键值队这样的数据,所以往往在android面试当中HashMap是必问环节,但有次面试我记得被问到了有没有有过ArrayMap,我只能说有印象,毕竟用的最多的还是HashMap,然后那个面试官又问我,觉得Android里面更适合用ArrayMap还是HashMap,我就说不上来了,因为也没看过ArrayMap的源码,后来回去看了下才给弄明白了,现在就简单对比下ArrayMap与HashMap的特点

HashMap

  • HashMap的数据结构为数组加链表的结构,jdk1.8之后改为数组加链表加红黑树的结构

  • put的时候,会先计算key的hashcode,然后去数组中寻找这个hashcode的下标,如果数据为空就先resize,然后检查对应下标值(下标值=(数组长度-1)&hashcode)里面是否为空,空则生成一个entry插入,否就判断hascode与key值是否分别都相等,如果相等则覆盖,如果不等就发生哈希冲突,生成一个新的entry插入到链表后面,如果此时链表长度已经大于8且数组长度大于64,则先转成树,将entry添加到树里面

  • get的时候,也是先去查找数组对应下标值里面是否为空,如果不为空且key与hascode都相等,直接返回value,否就判断该节点是否为一个树节点,是就在树里面返回对应entry,否就去遍历整个链表,找出key值相等的entry并返回

ArrayMap

  • 内部维护两个数组,一个是int类型的数组(mHashes)保存key的hashcode,另一个是Object的数组(mArray),用来保存与mHashes对应的key-value

  • put数据的时候,首先用二分查找法找出mHashes里面的下标index来存放hashcode,在mArray对应下标index<<1与(index<<1)+1的位置存放key与value

  • get数据的时候,同样也是用二分查找法找出与key值对应的下标index,接着再从mArray的(index<<1)+1位置将value取出

对比

  • HashMap在存放数据的时候,无论存放的量是多少,首先是会生成一个Entry对象,这个就比较浪费内存空间,而ArrayMap只是把数据插入到数组中,不用生成新的对象

  • 存放大量数据的时候,ArrayMap性能上就不如HashMap,因为ArrayMap使用的是二分查找法找的下标,当数据多了下标值找起来时间就花的久,此外还需要将所有数据往后移再插入数据,而HashMap只要插入到链表或者树后面即可

所以这就是为什么,在没有那么大的数据量需求下,Android在性能角度上比较适合用ArrayMap

为什么Arrays.asList后往里add数据会报错

这个问题我当初问过不少人,不缺乏一些资历比较深的大佬,但是他们基本都表示不清楚,这说明平时我们研究Glide,OkHttp这样的三方库源码比较多,而像一些比较基础的往往会被人忽略,而有些问题如果被忽略了,往往会产生一些捉摸不透的问题,比如有的人喜欢用Arrays.asList去生成一个List

val dataList = Arrays.asList(1,2,3)
dataList.add(4)

但是当我们往这个List里面add数据的时候,我们会发现,crash了,看到的日志是


不被支持的操作,这让首次遇到这样问题的人肯定是一脸懵,List不让添加数据了吗?之前明明可以的啊,但是之前我们创建一个List是这样创建的


它所在的包是java.util.ArrayList里面,我们看下里面的代码

public boolean add(E e) {
  ensureCapacityInternal(size + 1); // Increments modCount!!
  elementData[size++] = e;
  return true;
}
public void add(int index, E element) {
  if (index > size || index < 0)
      throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

  ensureCapacityInternal(size + 1); // Increments modCount!!
  System.arraycopy(elementData, index, elementData, index + 1,
                    size - index);
  elementData[index] = element;
  size++;
}

是存在add方法的,我们再回头再去看看asList生成的List


是在java.util.Arrays包里面的,而这里面的ArrayList我们看到了,并没有去实现List接口,所以也就没有add,get等方法,另外在kotlin里面,我们会看到一个细节,当你敲完Arrays.asList的时候,编译器会提示你,可以转换成listof函数,而这个还是我们知道生成的list都是只能读取,不能往里写数据

Thread.sleep(0)到底“睡没睡”

记得在上上家公司,接手的第一个需求就是做一个动画,这个动画需要一个延迟启动的功能,我那个时候想都没想加了个Thread.sleep(3000),后来被领导批了,不可以用Thread.sleep实现延迟功能,那会还不太明白,后来知道了,Thread.sleep(3000)不一定真的暂停三秒,我们来举个例子

println("start:${System.currentTimeMillis()}")
Thread(Runnable {
   Thread.sleep(3000)
   println("end:${System.currentTimeMillis()}")
}).start()

我们在主线程先打印一条数据展示时间,然后开启一个子线程,在里面sleep三秒以后在打印一下时间,我们看下结果如何

start:1675665421590
end:1675665424591

好像对了又好像没对,为什么是过了3001毫秒才打印出来呢?有的人会说,1毫秒而已,忽略嘛,那我们把上面的代码改下再试试

println("start:${System.currentTimeMillis()}")
Thread(Runnable {
   Thread.sleep(0)
   println("end:${System.currentTimeMillis()}")
}).start()

现在sleep了0毫秒,那是不是两条打印日志应该是一样的呢,我们看看结果

start:1675666764475
end:1675666764477

这下子给整不会了,明明sleep0毫秒,那么多出来的2毫秒是怎么回事呢?其实在Android操作系统中,每个线程使用cpu资源都是有优先级的,优先级高的才有资格使用,而操作系统则是在一个线程释放cpu资源以后,重新计算所有线程的优先级来重新分配cpu资源,所以sleep真正的意义不是暂停,而是在接下去的时间内不参与cpu的竞争,等到cpu重新分配完资源以后,如果优先级没变,那么继续执行,所以sleep(0)秒的真正含义是触发cpu资源重新分配

View.post为什么可以获取控件的宽高

我们都知道在onCreate里面想要获取一个控件的宽高,如果直接获取是拿不到的

val mWith = bindingView.mainButton.width
val mHeight = bindingView.mainButton.height
println("按钮宽:$mWith,高:$mHeight")
......
按钮宽:0,高:0

而如果想要获取宽高,则必须调用View.post的方法

bindingView.mainButton.post {
   val mWith = bindingView.mainButton.width
   val mHeight = bindingView.mainButton.height
   println("按钮宽:$mWith,高:$mHeight")
}
......
按钮宽:979,高:187

很神奇,加个post就可以在同样的地方获取控件宽高了,至于为什么呢?我们来分析一下

简单的来说

Activity生命周期,onCreate方法里面视图还在绘制过程中,所以没法直接获取宽高,而在post方法中执行,就是在线程里面获取宽高,这个线程会在视图没有绘制完成的时候放在一个等待队列里面,等到视图绘制执行完毕以后再去执行队列里面的线程,所以在post里面也可以获取宽高

复杂的来说

我们首先从View.post方法里面开始看


这个代码里面的两个框子,说明了post方法做了两件事情,当mAttachInfo不为空的时候,直接让mHandler去执行线程action,当mAttachInfo为空的时候,将线程放在了一个队列里面,从注释里面的第一个单词Postpone就可以知道,这个action是要推迟进行,什么时候进行呢,我们在慢慢看,既然是判断当mAttachInfo不为空才去执行线程,那我们找找什么时候对mAttachInfo赋值,整个View的源码里面只有一处是对mAttachInfo赋值的,那就是在dispatchAttachedToWindow 这个方法里面,我们看下

void dispatchAttachedToWindow(AttachInfo info, int visibility) {
mAttachInfo = info;
...省略部分源码...

// Transfer all pending runnables.
if (mRunQueue != null) {
mRunQueue.executeActions(info.mHandler);
mRunQueue = null;
}

}

当走到dispatchAttachedToWindow这个方法的时候,mAttachInfo才不为空,也就是从这里开始,我们就可以获取控件的宽高等信息了,另外我们顺着这个方法往下看,可以发现,之前的那个队列在这里开始执行了,现在就关键在于,什么时候执行dispatchAttachedToWindow这个方法,这个时候就要去ViewRootIml类里面查看,发现只有一处调用了这个方法,那就是在performTraversals这个方法里面

private void performTraversals() {
...省略部分源码...
host.dispatchAttachedToWindow(mAttachInfo, 0);
...省略部分源码...
// Ask host how big it wants to be
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...省略部分源码...
performLayout(lp, mWidth, mHeight);
...省略部分源码...
performDraw();
}

performTraversals这个方法我们就很熟悉了,整个View的绘制流程都在里面,所以只有当mAttachInfo在这个环节赋值了,才可以得到视图的信息

IdleHandler到底有啥用

Handler是面试的时候必问的环节,除了问一下那四大组件之外,有的面试官还会问一下IdleHandler,那IdleHandler到底是什么呢,它是干什么用的呢,我们来看看

Message next() {
...省略部分代码...
synchronized (this) {
// If first time idle, then get the number of idlers to run.
// Idle handles only run if the queue is empty or if the first message
// in the queue (possibly a barrier) is due to be handled in the future.
if (pendingIdleHandlerCount < 0
&& (mMessages == null || now < mMessages.when)) {
pendingIdleHandlerCount = mIdleHandlers.size();
}
if (pendingIdleHandlerCount <= 0) {
// No idle handlers to run. Loop and wait some more.
mBlocked = true;
continue;
}

if (mPendingIdleHandlers == null) {
mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
}
mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
}

// Run the idle handlers.
// We only ever reach this code block during the first iteration.
for (int i = 0; i < pendingIdleHandlerCount; i++) {
final IdleHandler idler = mPendingIdleHandlers[i];
mPendingIdleHandlers[i] = null; // release the reference to the handler

boolean keep = false;
try {
keep = idler.queueIdle();
} catch (Throwable t) {
Log.wtf(TAG, "IdleHandler threw exception", t);
}

if (!keep) {
synchronized (this) {
mIdleHandlers.remove(idler);
}
}
}

}

只有在MessageQueue中的next方法里面出现了IdleHandler,作用也很明显,当消息队列在遍历队列中的消息的时候,当消息已经处理完了,或者只存在延迟消息的时候,就会去处理mPendingIdleHandlers里面每一个idleHandler的事件,而这些事件都是通过方法addIdleHandler注册进去的

Looper.myQueue().addIdleHandler {
false
}

addIdlehandler接受的参数是一个返回值为布尔类型的函数类型参数,至于这个返回值是true还是false,我们从next()方法中就能了解到,当为false的时候,事件处理完以后,这个IdleHandler就会从数组中删除,下次再去遍历执行这个idleHandler数组的时候,该事件就没有了,如果为true的话,该事件不会被删除,下次依然会被执行,所以我们按需设置。现在我们可以利用idlehandler去解决上面讲到的在onCreate里面获取控件宽高的问题

Looper.myQueue().addIdleHandler {
val mWith = bindingView.mainButton.width
val mHeight = bindingView.mainButton.height
println("按钮宽:$mWith,高:$mHeight")
false
}

当MessageQueue中的消息处理完的时候,我们的视图绘制也完成了,所以这个时候肯定也能获取控件的宽高,我们在IdleHandler里面执行了同样的代码之后,运行后的结果如下

按钮宽:979,高:187

除此之外,我们还可以做点别的事情,比如我们常说的不要在主线程里面做一些耗时的工作,这样会降低页面启动速度,严重的还会出现ANR,这样的场景除了开辟子线程去处理耗时操作之外,我们现在还可以用IdleHandler,这里举个例子,我们在主线程中给sp塞入一些数据,然后在把这些数据读取出来,看看耗时多久

println(System.currentTimeMillis())
val testData = "aabbbbakjsdhjkahsjkasdjasdhjakshdjkahsdjkhasjdkhjaskhdjkashdjkhasjkhas" +
"jkhdaabbbbakjsdhjkahsjkasdjasdhjakshdjkahsdjkhasjdkhjaskhdjkashdjkhasjkhasjkhd" +
"aabbbbakjsdhjkahsjkasdjasdhjakshdjkahsdjkhasjdkhjaskhdjkashdjkhasjkhasjkhd" +
"aabbbbakjsdhjkahsjkasdjasdhjakshdjkahsdjkhasjdkhjaskhdjkashdjkhasjkhasjkhd" +
"aabbbbakjsdhjkahsjkasdjasdhjakshdjkahsdjkhasjdkhjaskhdjkashdjkhasjkhasjkhd" +
"aabbbbakjsdhjkahsjkasdjasdhjakshdjkahsdjkhasjdkhjaskhdjkashdjkhasjkhasjkhd"
sharePreference = getSharedPreferences(packageName, MODE_PRIVATE)
for (i in 1..5000) {
sharePreference.edit().putString("test$i", testData).commit()
}
for (i in 1..5000){
sharePreference.getString("test$i","")
}
println(System.currentTimeMillis())

......运行结果
1676260921617
1676260942770

我们看到在塞入5000次数据,再读取5000次数据之后,一共耗时大概20秒,同时也阻塞了主线程,导致的现象是页面一片空白,只有等读写操作结束了,页面才展示出来,我们接着把读写操作的代码用IdleHandler执行一下看看

Looper.myQueue().addIdleHandler {
sharePreference = getSharedPreferences(packageName, MODE_PRIVATE)
val editor = sharePreference.edit()
for (i in 1..5000) {
editor.putString("test$i", testData).commit()
}
for (i in 1..5000){
sharePreference.getString("test$i","")
}
println(System.currentTimeMillis())
false
}
......运行结果
1676264286760
1676264308294

运行结果依然耗时二十秒左右,但区别在于这个时候页面不会受到读写操作的阻塞,很快就展示出来了,说明读写操作的确是等到页面渲染完才开始工作,上面过程没有放效果图主要是因为时间太长了,会影响gif的体验,有兴趣的可以自己试一下

如何让指定视图不被软键盘遮挡

我们通常使用android:windowSoftInputMode属性来控制软键盘弹出之后移动界面,让输入框不被遮挡,但是有些场景下,键盘永远都会挡住一些我们使用频次比较高的控件,比如现在我们有个登录页面,大概的样子长这样


它的布局文件是这样

<RelativeLayout
android:id="@+id/mainroot"
android:layout_width="match_parent"
android:layout_height="match_parent">

<ImageView
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_centerHorizontal="true"
android:layout_marginTop="100dp"
android:src="@mipmap/ic_launcher_round" />

<androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/ll_view1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_marginBottom="120dp"
android:gravity="center"
android:orientation="vertical">

<EditText
android:id="@+id/main_edit"
android:layout_width="match_parent"
android:layout_height="40dp"
android:hint="请输入用户名"
android:textColor="@color/black"
android:textSize="15sp" />

<EditText
android:id="@+id/main_edit2"
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_marginTop="30dp"
android:hint="请输入密码"
android:textColor="@color/black"
android:textSize="15sp" />

<Button
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_marginHorizontal="10dp"
android:layout_marginTop="20dp"
android:text="登录" />

</androidx.appcompat.widget.LinearLayoutCompat>

</RelativeLayout>

在这样一个页面里面,由于输入框与登录按钮都比较靠页面下方,导致当输入完内容想要点击登录按钮时候,必须再一次关闭键盘才行,这样的操作在体验上就比较大打折扣了


现在希望可以键盘弹出之后,按钮也展示在键盘上面,这样就不用收起弹框以后才能点击按钮了,这样一来,windowSoftInputMode这一个属性已经不够用了,我们要想一下其他方案

  • 首先,需要让按钮也展示在键盘上方,那只能让布局整体上移把按钮露出来,在这里我们可以改变LayoutParam的bottomMargin参数来实现

  • 其次,需要知道键盘什么时候弹出,我们都知道android里面并没有提供任何监听事件来告诉我们键盘什么时候弹出,我们只能从其他角度入手,那就是监听根布局可视区域大小的变化

ViewTreeObserver

我们先获取视图树的观察者,使用addOnGlobalLayoutListener去监听全局视图的变化

bindingView.mainroot.viewTreeObserver.addOnGlobalLayoutListener {

}

接下去就是要获取根视图的可视化区域了,如何来获取呢?View里面有这么一个方法,那就是getWindowVisibleDisplayFrame,我们看下源码注释就知道它是干什么的了


一大堆英文没必要都去看,只需要看最后一句就好了,大概意思就是获取能够展示给用户的可用区域,所以我们在监听器里面加上这个方法

bindingView.mainroot.viewTreeObserver.addOnGlobalLayoutListener {
val rect = Rect()
bindingView.mainroot.getWindowVisibleDisplayFrame(rect)
}

当键盘弹出或者收起的时候,rect的高度就会跟着变化,我们就可以用这个作为条件来改变bottomMargin的值,现在我们增加一个变量oldDelta来保存前一个rect变化的高度值,用来做比较,完整的代码如下

var oldDelta = 0
val params:RelativeLayout.LayoutParams = bindingView.llView1.layoutParams as RelativeLayout.LayoutParams
val originBottom = params.bottomMargin
bindingView.mainroot.viewTreeObserver.addOnGlobalLayoutListener {
val rect = Rect()
bindingView.mainroot.getWindowVisibleDisplayFrame(rect)
val deltaHeight = r.height()
if (oldDelta != deltaHeight) {
if (oldDelta != 0) {
if (oldDelta > deltaHeight) {
params.bottomMargin = oldDelta - deltaHeight
} else if (oldDelta < deltaHeight) {
params.bottomMargin = originBottom
}
bindingView.llView1.layoutParams = params
}
oldDelta = deltaHeight
}
}

最终效果如下


弹出后页面有个抖动是因为本身有个页面平移的效果,然后再去计算layoutparam,如果不想抖动可以在布局外层套个scrollView,用smoothScrollTo把页面滑上去就可以了,有兴趣的可以业余时间试一下

为什么LiveData的postValue会丢失数据

LiveData已经问世好多年了,大家都很喜欢用,因为它上手方便,一般知道塞数据用setValue和postValue,监听数据使用observer就可以了,然而实际开发中我遇到过好多人,一会这里用setValue一会那里用postValue,或者交替着用,这种做法也不能严格意义上说错,毕竟运行起来的确没问题,但是这种做法确实是存在风险隐患,那就是连续postValue会丢数据,我们来做个实验,连续setValue十个数据和连续postValue十个数据,收到的结果都分别是什么

var testData = MutableLiveData<Int>()
fun play(){
for (i in 1..10) {
testData.value = i
}
}

mainViewModel.testData.observe(this) {
println("收到:$it")
}

//执行结果
收到:1
收到:2
收到:3
收到:4
收到:5
收到:6
收到:7
收到:8
收到:9
收到:10

setValue十次数据都可以收到,现在把setValue改成postValue再来试试

var testData = MutableLiveData<Int>()
fun play(){
for (i in 1..10) {
testData.postValue(i)
}
}

得到的结果是

收到:10

只收到了最后一条数据10,这是为什么呢?我们进入postValue里面看看里面的源码就知道了


主要看红框里面,有一个synchronized同步锁锁住了一个代码块,我们称为代码块1,锁的对象是mDataLock,代码块1做的事情先是给postTask这个布尔值赋值,接着把传进来的值赋给mPendingData,那我们知道了,postTask除了第一个被执行的时候,值是true,结下去等mPendingData有值了以后就都为false,前提是mPendingData没有被重置为NOT_SET,然后我们顺着代码往下看,会看到代码接下来就要到一个mPostValueRunnable的线程里面去了,我们看下这个线程


发现同样的锁,锁住了另一块代码块,我们称为代码块2,这个代码块里面恰好是把mPendingData的值赋给newValue以后,重置为NOT_SET,这样一来,postValue又可以接受新的值了,所以这也是正常情况下每次postValue都可以接受到值的原因,但是我们想想连续postValue的场景,我们知道如果synchronized如果修饰一段代码块,那么当这段代码块获取到锁的时候,就具有优先级,只有当全部执行完以后才会释放锁,所以当代码块1连续被访问时候,代码块2是不会被执行的,只有等到代码块1执行完,释放了锁,代码块2才会被执行,而这个时候,mPendingData已经是最新的值了,之前的值已经全部被覆盖了,所以我们说的postValue会丢数据,其实说错了,应该是postValue只会发送最新数据

总结

这篇文章讲到的面试题还仅仅只是过去几年遇到的,现在面试估计除了一些常规问题之外,比重会更倾向于Kotlin,Compose,Flutter的知识点,所以只有不断的日积月累,让自己的知识点更加的全面,才能在目前竞争激烈的行情趋势下逆流而上,不会被拍打在沙滩上

作者:Coffeeee
来源:juejin.cn/post/7199537072302374969

收起阅读 »