注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

环信FAQ

环信FAQ

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

单线程 Redis 如此快的 4 个原因

本文翻译自国外论坛 medium,原文地址:levelup.gitconnected.com/4-reasons-w… 作为内存数据存储,Redis 以其速度和性能而闻名,通常被用作大多数后端服务的缓存解决方案。 然而,在 Redis 内部采用的也只是单线程的...
继续阅读 »

本文翻译自国外论坛 medium,原文地址:levelup.gitconnected.com/4-reasons-w…


作为内存数据存储,Redis 以其速度和性能而闻名,通常被用作大多数后端服务的缓存解决方案。


然而,在 Redis 内部采用的也只是单线程的设计。


为什么 Redis 单线程设计会带来如此高的性能?如果利用多个线程并发处理请求不是更好吗?


在本文中,我们将探讨使 Redis 成为快速高效的数据存储的设计选择。


长话短说


Redis 的性能可归因于 4 个主要因素



  • 基于内存存储

  • 优化的数据结构

  • 单线程架构

  • 非阻塞IO


让我们一一剖析一下。



推荐博主开源的 H5 商城项目waynboot-mall,这是一套全部开源的微商城项目,包含三个项目:运营后台、H5 商城前台和服务端接口。实现了商城所需的首页展示、商品分类、商品详情、商品 sku、分词搜索、购物车、结算下单、支付宝/微信支付、收单评论以及完善的后台管理等一系列功能。 技术上基于最新得 Springboot3.0、jdk17,整合了 MySql、Redis、RabbitMQ、ElasticSearch 等常用中间件。分模块设计、简洁易维护,欢迎大家点个 star、关注博主。


github 地址:github.com/wayn111/way…



基于内存存储




Redis 是在内存中进行键值存储。


Redis 中的每次读写操作都相当于从内存的变量中进行读写。


访问内存比直接访问磁盘快几个数量级,因此Redis 比其他数据存储快得多。


优化的数据结构




作为内存数据存储,Redis 利用各种底层数据结构来高效存储数据,无需担心如何将它们持久化到持久存储中。


例如,Redis list 是使用链表实现的,它允许在列表的头部和尾部附近进行恒定时间 O(1) 插入和删除。


另一方面,Redis sorted set 是通过跳跃列表实现的,可以实现更快的查询和插入。


简而言之,无需担心数据持久化,Redis 中的数据可以更高效地存储,以便通过不同的数据结构进行快速检索。


单线程




Redis 中的写入和读取速度非常快,并且 CPU 使用率从来不是 Redis 关心的问题。


根据 Redis 官方文档,在普通 Linux 系统上运行时,Redis 每秒最多可以处理 100 万个请求。


通常瓶颈来自于网络 I/O, Redis 中的处理时间大部分浪费在等待网络 I/O 上。


虽然多线程架构允许应用程序通过上下文切换并发处理任务,但这对 Redis 的性能增益很小,因为大多数线程最终会在 I/O 中被阻塞。


所以 Redis 采用单线程架构,有如下好处



  • 最大限度地减少由于线程创建或销毁而产生的 CPU 消耗

  • 最大限度地减少上下文切换造成的 CPU 消耗

  • 减少锁开销,因为多线程应用程序需要锁来进行线程同步,而这容易出现错误

  • 能够使用各种“线程不安全”命令,例如 Lpush


非阻塞I/O




为了处理传入的请求,服务器需要在套接字上执行系统调用,以将数据从网络缓冲区读取到用户空间。


这通常是阻塞操作,线程被阻塞并且在完全接收到来自客户端的数据之前不能执行任何操作。


为什么我们不能在只有确定套接字中的数据已准备好读取时,才执行系统调用嘞?


这就是 I/O 多路复用发挥作用的地方。


I/O 多路复用模块同时监视多个套接字,并且仅返回可读的套接字。


准备读取的套接字被推送到单线程事件循环,并由相应的处理程序使用响应式模型进行处理。


总之,



  • 网络 I/O 速度很慢,因为其阻塞特性,

  • Redis 收到命令后可以快速执行,因为这在内存中执行,操作速度很快,


所以 Redis 做出了以下决定,



  • 使用 I/O 多路复用来缓解网络 I/O 缓慢问题

  • 使用单线程架构减少锁开销


结论




综上所述,单线程架构是 Redis 团队经过深思熟虑的选择,并且经受住了时间的考验。


尽管是单线程,Redis 仍然是性能最高、最常用的内存数据存储之一。

作者:waynaqua
来源:juejin.cn/post/7257783692563611685

收起阅读 »

SpringBoot可以同时处理多少请求?

SpringBoot是一款非常流行的Java后端框架,它可以帮助开发人员快速构建高效的Web应用程序。但是,许多人对于SpringBoot能够同时处理多少请求的疑问仍然存在。在本篇文章中,我们将深入探讨这个问题,并为您提供一些有用的信息。 首先,我们需要了解一...
继续阅读 »

SpringBoot是一款非常流行的Java后端框架,它可以帮助开发人员快速构建高效的Web应用程序。但是,许多人对于SpringBoot能够同时处理多少请求的疑问仍然存在。在本篇文章中,我们将深入探讨这个问题,并为您提供一些有用的信息。


首先,我们需要了解一些基本概念。在Web应用程序中,请求是指客户端向服务器发送的消息,而响应则是服务器向客户端返回的消息。在高流量情况下,服务器需要能够同时处理大量的请求,并且尽可能快地响应这些请求。这就是所谓的“并发处理”。


SpringBoot使用的是Tomcat作为默认的Web服务器。Tomcat是一种轻量级的Web服务器,它可以同时处理大量的请求。具体来说,Tomcat使用线程池来管理请求,每个线程都可以处理一个请求。当有新的请求到达时,Tomcat会从线程池中选择一个空闲的线程来处理该请求。如果没有可用的线程,则该请求将被放入队列中,直到有线程可用为止。


默认情况下,SpringBoot会为每个CPU内核创建一个线程池。例如,如果您的服务器有4个CPU内核,则SpringBoot将创建4个线程池,并在每个线程池中创建一定数量的线程。这样可以确保服务器能够同时处理多个请求,并且不会因为线程过多而导致性能下降。


当然,如果您需要处理大量的请求,您可以通过配置来增加线程池的大小。例如,您可以通过修改application.properties文件中的以下属性来增加Tomcat线程池的大小:


server.tomcat.max-threads=200

上述配置将使Tomcat线程池的最大大小增加到200个线程。请注意,增加线程池大小可能会导致服务器资源消耗过多,因此应该谨慎使用。


除了Tomcat之外,SpringBoot还支持其他一些Web服务器,例如Jetty和Undertow。这些服务器也都具有良好的并发处理能力,并且可以通过配置来调整线程池大小。


最后,需要注意的是,并发处理能力不仅取决于Web服务器本身,还取决于应用程序的设计和实现。如果您的应用程序设计得不够好,那么即使使用最好的Web服务器也无法达到理想的并发处理效果。因此,在开发应用程序时应该注重设计和优化。


总之,SpringBoot可以同时处理大量的请求,并且可以通过配置来增加并发处理能力。但是,在实际应用中需要根据具体情况进行调整,并注重应用程序的设计和优化。希望本篇文章能够帮助您更好地理解SpringBo

作者:韩淼燃
来源:juejin.cn/post/7257732392541618237
ot的并发处理能力。

收起阅读 »

随着鼠标移入,图片的切换跟着修改背景颜色(Vue3写法)

web
先看看效果图吧 下面来看实现思路 又是摸鱼的下午,无聊来实现了一下这个效果,记录一下,说不定以后有这需求,记一下放到官网上也是OK的, 我这里提供一种实现方法,当然你们想用放大加模糊也是可以的,想怎么来就怎么来 1.背景颜色不是固定的,是随着图片的切换动态...
继续阅读 »

先看看效果图吧


image.png


image.png


下面来看实现思路


又是摸鱼的下午,无聊来实现了一下这个效果,记录一下,说不定以后有这需求,记一下放到官网上也是OK的,
我这里提供一种实现方法,当然你们想用放大加模糊也是可以的,想怎么来就怎么来


1.背景颜色不是固定的,是随着图片的切换动态改变


原理:

1.当鼠标移入到某一张图片时,拿到这张图片

2.我们就可以把这张图片画到canvas里,就可以获取到每一个像素点

3.我们的背景是需要渐变的,我们是需要三种颜色的渐变,当然也可以有很多种,看你们的心情

4.我们就要计算出前三种的主要颜色,但是每个像素点的颜色非常非常多,好多颜色也非常相近,我们通过肉眼肯定看不出来的,这个时候就要用到计算机了

5.需要一种近似算法(颜色聚合算法)了,就是把好多相近的颜色聚合成一种颜色,当然我们就要用到第三方库(colorthief)了


准备好html


<template>
<div class="box">
<div class="item" v-for="item in 8" :key="item" :class="item === hoverIndex ? 'over' : ''">
<img crossorigin="anonymous" @mouseenter="onMousenter($event.target, item)" @mouseleave="onMousleave"
:src="`https://picsum.photos/438/300?id=${item}`" alt=""
:style="{ opacity: hoverIndex === -1 ? 1 : item === hoverIndex ? 1 : 0.2 }">
// 设置透明度
</div>
</div>
</template>


scss


.box {
height: 100vh;
display: flex;
justify-content: space-evenly;
align-items: center;
flex-wrap: wrap;
background-color: rgb(var(--c1), var(--c2), var(--c3));
}

.item {
border: 1px solid #fff;
margin-top: 50px;
transition: 0.8s;
padding: 5px;
box-shadow: 0 0 10px #00000058;
background-color: #fff;
}

img {
transition: .8s;
}

npm安装colorthief库


npm i colorthief

导入到文件中


import ColorThief from "colorthief";

因为这是一个构造函数,所以需要创建出一个实例对象


const colorThief = new ColorThief()
const hoverIndex = ref<number>(-1) //设置变换样式的响应式变量

重点函数:鼠标移入事件onMousenter


getPalette(img,num) img是dom元素,是第三库需要将其画入到canvas中,所以需要在img标签中添加一个允许跨域的属性 crossorigin="anonymous",不然会报错

num是需要提取几种颜色,同样也会返回多少个数组

返回的是一个promise,需要await


const onMousenter = async (img: EventTarget | null, i: number) => {
hoverIndex.value = i //将响应式变量改成自身,样式就生效了
const colors = await colorThief.getPalette(img, 3)
console.log(colors); //获取到三个数组,将其数组改造成rgb格式
const [c1, c2, c3] = colors.map((c: string[]) => `rgb(${c[0]},${c[1]},${c[2]})`)//将三个颜色解构出来
html.style.setProperty('--c1', c1) //给html设置变量,下面有步骤
html.style.setProperty('--c2', c2)
html.style.setProperty('--c3', c3)
}

鼠标移出事件


将响应式变量初始化,将背景颜色改为白色


const onMousleave = () => {
hoverIndex.value = -1
html.style.setProperty('--c1', '#fff')
html.style.setProperty('--c2', '#fff')
html.style.setProperty('--c3', '#fff')
}

获取html根元素


const html = document.documentElement

在主文件index.html给html设置渐变变量


<style>
html{
background-image: linear-gradient(to bottom, var(--c1), var(--c2),var(--c3));
}
</style>

image.png
需要注意的是colorthief使用的时候需要给img设置跨域,不然会报错,还有就是给html设置渐变变量


🔥🔥🔥好的,到这里基本上就已经实现了,看着代码也不多,也没啥技术含量,全靠三方库干事,主要是记录生活,方便未来cv


作者:井川不擦
来源:juejin.cn/post/7257733186158903356
收起阅读 »

几何算法:判断两条线段是否相交

web
‍ ‍大家好,我是前端西瓜哥。 如何判断两条线段(注意不是直线)是否有交点? 传统几何算法的局限 上过一点学的西瓜哥我,只用高中学过的知识,还是可以解这个问题的。 一条线段两个点,可以列出一个两点式(x - x1) / (x2 - x1) = (y - y1)...
继续阅读 »


‍大家好,我是前端西瓜哥。


如何判断两条线段(注意不是直线)是否有交点?


传统几何算法的局限


上过一点学的西瓜哥我,只用高中学过的知识,还是可以解这个问题的。


一条线段两个点,可以列出一个两点式(x - x1) / (x2 - x1) = (y - y1) / (y2 - y1)),两条线段是两个两点式,这样就是 二元一次方程组 了 ,就能求出两条直线的交点。


然后判断这个点是否在其中一条线段上。如果在,说明两线段相交,否则不相交。


看起来不错,但这里要考虑直线垂直或水平于坐标轴的特殊情况,还有两条直线平行导致没有唯一解的情况,除数不能为 0 的情况。


特殊情况实在是太多了,能用是能用,但不好用。


那么,有其他的更好的解法吗?


有的,叉乘。


叉乘是什么?


叉乘(cross product)是线性代数的一个概念,也叫外积、叉积、向量积,是在三维空间中两个向量的二元运算的结果,该结果为一个向量。


但那是严格意义上的。实际也可以用在二维空间的二维向量中,不过此时它们的叉乘结果变成了标量。


假设向量 A 为 (x1, y1),向量 B 为 (x2, y2),则叉乘 AxB 的结果为 x1 * y2 - x2 * y1


(注意叉乘不满足交换律)


在几何意义上,这个叉乘结果的绝对值对应两个向量组成的平行四边形的面积。


此外可通过符号判断向量 A 变成向量 B 的旋转方向。


如果叉乘为正数,说明 A 变成 B 需要逆时针旋转(旋转角度小于 180 度);


如果为负数,说明 A 到 B 需要顺时针旋转;


如果为 0,说明两个向量平行(或重合)


叉乘解法的原理


回到题目本身。


假设线段 1 的端点为 A 和 B,线段 2 的端点为 C 和 D。


图片


我们可以换另一个角度去解,即判断线段 1 的两个端点是否在线段 2 的两边,然后再反过来比线段 2 的两点是否线段 1 的两边。


这里我们可以利用上面 叉乘的正负代表旋转方向的特性


以上图为例, AB 向量到 AD 向量位置需要逆时针旋转,AB 向量到 AC 向量则需要顺时针,代表 C 和 D 在 AB 的两侧,对应就是两个叉乘相乘为负数。


function crossProduct(p1: Point, p2: Point, p3: Point)number {
  const x1 = p2[0] - p1[0];
  const y1 = p2[1] - p1[1];
  const x2 = p3[0] - p1[0];
  const y2 = p3[1] - p1[1];
  return x1 * y2 - x2 * y1;
}

const [a, b] = seg1;
const [c, d] = seg2;

// d1 的符号表示 AB 旋转到 AC 的旋转方向
const d1 = crossProduct(a, b, c);


只是判断了 C 和 D 在 AB 线段的两侧还不行,因为可能还有下面这种情况。


图片


所以我们还要再判断一下,A 和 B 是否在 CD 线的的两侧。计算过程同上,这里不赘述。


一般实现


type Point = [numbernumber];

function crossProduct(p1: Point, p2: Point, p3: Point): number {
  const x1 = p2[0] - p1[0];
  const y1 = p2[1] - p1[1];
  const x2 = p3[0] - p1[0];
  const y2 = p3[1] - p1[1];
  return x1 * y2 - x2 * y1;
}

function isSegmentIntersect(
  seg1: [Point, Point],
  seg2: [Point, Point],
): boolean {
  const [a, b] = seg1;
  const [c, d] = seg2;

  const d1 = crossProduct(a, b, c);
  const d2 = crossProduct(a, b, d);
  const d3 = crossProduct(c, d, a);
  const d4 = crossProduct(c, d, b);

  return d1 * d2 < 0 && d3 * d4 < 0;
}

// 测试
const seg1: [PointPoint] = [
  [00],
  [11],
];
const seg2: [PointPoint] = [
  [01],
  [10],
];

console.log(isSegmentIntersect(seg1, seg2)); // true


注意,这个算法认为线段的端点刚好在另一条线段上的情况,不属于相交。


考虑点在线段上或重合


如果你需要考虑线段的端点刚好在另一条线段上的情况,需要额外在叉乘为 0 的情况下,再判断一下线段 1 的端点是否在另一个线段的 x  和 y 范围内。


对应的算法实现:


type Point = [numbernumber];

function crossProduct(p1: Point, p2: Point, p3: Point): number {
  const x1 = p2[0] - p1[0];
  const y1 = p2[1] - p1[1];
  const x2 = p3[0] - p1[0];
  const y2 = p3[1] - p1[1];
  return x1 * y2 - x2 * y1;
}

function onSegment(p: Point, seg: [Point, Point]): boolean {
  const [a, b] = seg;
  const [x, y] = p;
  return (
    x >= Math.min(a[0], b[0]) &&
    x <= Math.max(a[0], b[0]) &&
    y >= Math.min(a[1], b[1]) &&
    y <= Math.max(a[1], b[1])
  );
}

function isSegmentIntersect(
  seg1: [Point, Point],
  seg2: [Point, Point],
): boolean {
  const [a, b] = seg1;
  const [c, d] = seg2;

  const d1 = crossProduct(a, b, c);
  const d2 = crossProduct(a, b, d);
  const d3 = crossProduct(c, d, a);
  const d4 = crossProduct(c, d, b);

  if (d1 * d2 < 0 && d3 * d4 < 0) {
    return true;
  }
 
  // d1 为 0 表示 C 点在 AB 所在的直线上
  // 接着会用 onSegment 再判断这个 C 是不是在 AB 的 x 和 y 的范围内
  if (d1 === 0 && onSegment(c, seg1)) return true;
  if (d2 === 0 && onSegment(d, seg1)) return true;
  if (d3 === 0 && onSegment(a, seg2)) return true;
  if (d4 === 0 && onSegment(b, seg2)) return true;

  return false;
}

// 测试
const seg1: [PointPoint] = [
  [00],
  [11],
];
const seg2: [PointPoint] = [
  [01],
  [10],
];
const seg3: [PointPoint] = [
  [00],
  [22],
];
const seg4: [PointPoint] = [
  [11],
  [10],
];
// 普通相交情况
console.log(isSegmentIntersect(seg1, seg2)); //  true
// 线段 1 的一个端点刚好在线段 2 上
console.log(isSegmentIntersect(seg3, seg4)); // true


结尾


总结一下,判断两条线段是否相交,可以判断两条线段的两端点是否分别在各自的两侧,对应地需要用到二维向量叉乘结果的正负值代表向量旋转方向的特性。


我是前端西瓜哥,关注我,学习更多几何算法。



作者:前端西瓜哥
来源:juejin.cn/post/7257547252540751909

收起阅读 »

在字节的程序员的 2023 年中总结

2023 年已经过去了,回顾这一年,作为字节的程序员,我想分享一下我的总结。 首先,2023 年是一个非常特殊的一年。全球疫情已经得到有效控制,人们的生活逐渐恢复正常。在这个背景下,科技行业得到了更多的关注和投资。作为一名程序员,我感到非常幸运能够在这个行业中...
继续阅读 »

2023 年已经过去了,回顾这一年,作为字节的程序员,我想分享一下我的总结。


首先,2023 年是一个非常特殊的一年。全球疫情已经得到有效控制,人们的生活逐渐恢复正常。在这个背景下,科技行业得到了更多的关注和投资。作为一名程序员,我感到非常幸运能够在这个行业中工作。


在这一年中,我参与了很多有趣的项目。其中最令我印象深刻的是我们团队开发的一款智能家居系统。这个系统可以通过语音控制来控制家里的各种设备,比如灯光、温度、音响等等。用户可以通过手机 App 或者智能音箱来控制家居设备。这个项目不仅让我学习了很多新技术,还让我感受到了科技带来的便利和乐趣。


除了技术方面的学习和成长,我也意识到了作为一名程序员的责任和使命。在这个信息时代,程序员们不仅仅是技术人员,更是社会的建设者和推动者。我们所开发的软件和系统,不仅仅是为了满足商业需求,更应该为社会带来更多的价值和福利。比如,在疫情期间,我们团队开发了一款在线医疗咨询系统,让患者可以在线上咨询医生,减少了人员聚集和传染的风险。这种为社会做贡献的感觉真的很棒。


当然,在这一年中也遇到了很多挑战和困难。比如,我们团队在开发一个大型项目时遇到了很多技术难题和进度压力。但是,通过团队合作和不断努力,最终我们成功地完成了项目,并得到了客户的高度评价。这也让我更加深刻地认识到团队合作和自我提升的重要性。


总之,2023 年对我来说是一个非常充实和有意义的一年。在未来的日子里,我会继续努力学习和提升自己,为社会做出更多的贡献。同时也祝愿所有的程序员们都能在自己的岗位上取

作者:韩淼燃
来源:juejin.cn/post/7257733186158805052
得更好的成绩和发展。

收起阅读 »

如何写出一手好代码(上篇 - 理论储备)?

无论是刚入行的新手还是已经工作多年的老司机,都希望自己可以写一手好代码,这样在代码 CR 的时候就可以悄悄惊艳所有人。特别是对于刚入职的新同学来说,代码写得好可以帮助自己在新环境快速建立技术影响力。因为对于从事 IT 互联网研发工作的同学来说,技术能力是研发同...
继续阅读 »

无论是刚入行的新手还是已经工作多年的老司机,都希望自己可以写一手好代码,这样在代码 CR 的时候就可以悄悄惊艳所有人。特别是对于刚入职的新同学来说,代码写得好可以帮助自己在新环境快速建立技术影响力。因为对于从事 IT 互联网研发工作的同学来说,技术能力是研发同学的立身之本,而写代码的能力又是技术能力的重要体现。但可惜的是理想很丰满,现实很骨感。结合慕枫自己的经验来看,我们在工作中其实没那么容易可以看到写得很好的代码。造成这种情况的原因也许很多,但是无论什么原因都不应该妨碍我们对于写好代码的追求。今天慕枫就和大家探讨下到底怎样做才能写出一手大家都认为好的代码?


哪些因素制约好代码的产生?


我们首先来分析下到底哪些因素造成了现实工作中好代码难以产出。因为只有搞清楚了这个问题才能对症下药,这样在我们自己写代码的时候才能尽量避免这些问题影响我们写好代码。


假如让我们说出哪些是烂代码,我们也许会罗列出来代码不易理解、没有注释、方法或者类词不达意、分层不合理、不够抽象、单个方法过长、单个类过长、代码难以维护每次改动都牵一发动全身、重复代码过多等等,这些都是我们在实际项目开发过长中经常遇到的代码问题。那么到底是什么原因造成了现实项目中有这么多的代码问题呢?慕枫认为主要存在以下三方面的原因。



1、项目倒排时间不够


项目需求倒排导致没有时间在写代码前好好进行设计,所以只能先快速满足需求等后面有时间再优化(大概率是没有时间的)。这就造成技术同学在写代码的时候怎么快怎么写,优先把功能实现了再说,很多该考虑的细节就不会考虑那么多,该处理的异常没有进行处理,所以可能写出来的代码可以说是一次性代码,只针对当前的业务场景,基本没什么扩展性可言。


2、团队技术氛围不足


团队内技术氛围不是很浓厚,本来你是想好好把代码写好的,但是发现大家都在短平快的写代码,而且没有太多人关心代码写的好不好,只关心需求有没有按时完成。在这样的团队氛围影响之下,自己写出来的代码也在慢慢地妥协。像在阿里这样的一线互联网公司,团队中的代码文化还是很强的,很多技术团队在需求上线前必须要进行代码 CR,CR 不过的代码不允许上线。因此好的团队技术氛围会促使你不得不把代码写好,否则在代码 CR 的时候就等着接受暴风雨般的吐槽吧。


3、自身技术水平有限


第三个原因就是自身的技术水平有限,设计模式不知道该在什么样的业务场景下使用,框架的高级用法没有掌握,经验不足导致异常情况经常考虑不到。自己本身没有把代码写好的追求,总想着能满足需求代码能跑就行。


以上大概是我们实际工作中导致我们不能产出好代码最主要的三大原因,第一个原因我们基本无法改变,因为在互联网行业竞争本身就非常激烈,谁能先推出新业务优化用户体验,谁就能占得市场先机。因此项目倒排必定是常有的事情,也是无法避免的事情。第二个原因,如果你自己是团队的 TL,那么尽量在团队中去营造代码 CR 的文化,提升团队中的技术氛围。因为代码是技术团队的根本,所有的业务效果落地都需要通过代码来实现,因此好的代码可以帮助团队减少 Bug 出现的概率、提升大家的代码效率从而达到降低人力物力成本的目的。如果你不是团队的 TL,同时团队中的技术氛围也没那么足,那么我们也不要放弃治疗,先把自己负责的模块的代码写好,一点点影响团队,逐渐唤起大家对于好代码的重视。


前两个因素都属于环境因素,也许我们不好改变,但是对于第三个因素,我觉得我们可以通过理论知识的学习,不断的代码实践以及思考总结是可以改变的,因此本文主要还是讨论如何通过改变自己来把代码写好。


到底什么是好代码?


要想写出好的代码,首先我们得知道什么样的代码才是好代码。但是好这个字本身就具有较强的主观性,正所谓一千个读者心中就有一千个哈姆雷特。因此我们需要先统一一下好代码的标准,有了标准之后我们再来探讨到底怎么做才能写出好代码。


我相信大家肯定听说过代码可读性、代码扩展性、可维护性等词汇来描述好代码的特点,实际上这些形容词都是从不同方面对代码进行了阐述。但是在慕枫看来,在实际的项目开发中,可维护性以及高鲁棒性是好代码的两个比较核心的衡量标准。因为无论是开发新需求还是修复 Bug,都是在原有的平台代码中进行修改,如果原来代码的扩展性比较强,那么我们编码的时候就就可以做到最小化修改,降低引入问题的风险。而鲁棒性高的代码在线上出现 Bug 的概率相对来说就第一点,对于维护线上服务的稳定性具有重要意义。


可维护性


我们都知道代码开发并不是一个人的工作,通常涉及到很多人团队合作。因此慕枫认为代码的可维护性是好代码的第一要义。而可维护性主要体现在代码可读容易理解以及修改方便容易扩展这两方面,下面分别进行阐述说明。


代码可读


我们写出来的代码不仅仅要自己能看得懂自己写的代码,别人也应该可以轻松看得懂你的代码。在一线的互联网大厂中工作内容发生变化是常有的事情,如果别人接手我们的代码或者我们接手别人的代码时,可读性强的代码无疑可以减少大家理解业务的时间成本。因为代码是最直接的业务表现,那些所谓的设计文档要么过时要么写的非常粗略,基本不太能指导我们熟悉业务。那么什么样的代码称得上可读性强呢?


命名准确


无论是包的命名、类的命名、方法的命名还是变量的命名都能很准确地表达业务含义,让人可以看其名知其义。命名应该和实际的代码逻辑相匹配,否则不合适的命名只会让人丈二和尚摸不着脑袋误导看代码的同学。以前看代码的时候我看过以 main 作为类中的方法名称,所以得看完这个方法的实现逻辑才能明白它到底干什么的,这对于后期维护的同学来说非常不友好。


代码注释


另外就是必要的注释,有些同学非常自信觉得自己写的代码很好懂,根本不需要写什么注释。结果自己过了一两个月再回头看自己的代码的时候,死活想不起来某段代码为什么要这么写。当然我们不必每一行代码都写注释,但是该注释的地方就要写注释,特别是一些逻辑比较复杂,业务性比较强的地方,既方便自己以后排查问题也方便后面维护的同学理解业务。因此不要对自己写的代码过于自信,间隔时间一长也许连你自己都未必记得代码为什么这么写。


结构清晰


无论是服务的包结构还是代码结构都体现了技术同学对于技术的理解,因此即便是不深入看代码逻辑,通过包结构的划分、模块的划分类结构的设计已经基本可以判断出来项目的代码质量了。我们在进行包结构设计的时候可以遵循依赖倒置的原则,让非核心层依赖核心层。



可扩展性


随着业务需求的不断变化,技术同学免不了在原有的代码逻辑中进行修改。因此项目代码的可扩展性直接影响着后期维护的成本。如果改一个小需求就需要对原有的代码大动干戈,修改的地方越多引入 Bug 的风险就会越大。我们都知道线上的故障有七八成都是由于变更引起的,因此可扩展性强的代码可以有效控制变更的范围。


高鲁棒性


当我们说到代码鲁棒性高的时候,实际就是说代码比较健壮,能够应对各种输入,即便出现异常也会有对应的异常处理机制进行响应而不至于直接崩溃。而项目开发不是一个人的工作,通常都是团队合作,因此我们写的代码无时无刻不在和别人的代码进行交互,所以我们负责的代码模块总是在处理可能正常可能异常的输入。如果不能对可能出现的异常输入进行妥善的防御性处理,那么可能就会造成 Bug 的产生,严重情况下甚至会影响系统正常运行。因此好的代码除了方便扩展方便维护之外,它必定也是高鲁棒性的,否则如果每天 Bug 满天飞,哪有时间和精力去琢磨代码的可扩展性,大部分精力都用来修复 Bug,长此以往自己也会感觉身心俱疲,总是感觉自己没什么成长。


如何写出好代码?


强烈内在驱动


为什么我把强烈的内在驱动摆在首要位置,主要是因为我觉得程序员只有有了想把代码写好的愿望,才能真正驱动自己写出来好代码。否则即便掌握了各种设计原则以及优化技巧,但是自己没有写好代码的内在驱动,总是觉得程序又不是不能用,或者觉得代码和自己有一个能跑就行,亦或是抱着后面有时间再优化的态度(基本是没时间)是不可能写好代码的。因此首先我们得有写好代码的内在驱动和愿望,我们才能有把代码写好的可能。不过话又说回来,内在驱动是基础,全是感情没有技巧肯定也不行。


沉淀业务模型


谈完了内在驱动这个感情,我们就要来看看要掌握哪些技巧才能帮助我们写出来好代码,首当其冲的就是业务领域模型,因为它是领域业务在工程代码中的落地也是整个服务的核心,不过遗憾的是很多同学并没有意识到它的重要性,甚至经常会把数据模型和业务模型相混淆。而我自己在在团队中落地 DDD 领域驱动设计的时候,被技术同学问过比较多的问题就是数据库表对应的数据实体满足不了业务需要吗?为什么还需要业务领域模型?那么想要回答这些问题,我们得先搞清楚到底什么是领域模型,它到底能给技术团队带来什么。


从本质上来说领域模型就是我们对于本行业业务领域的认知,体现了你对行业认知的沉淀以及外化表现。那么怎么体现你对行业领域业务认知的深度呢?领域模型就是很好的验证手段,对行业认知越深刻的同学构建的领域模型越能够刻画现实中的业务场景,我们也可以认为领域模型是现实世界业务场景到代码世界的映射,同时它也是公司重要的业务资产。那么每个行业的业务认知又是从哪里来的呢?实际上就从实际的业务场景中抽象出来的。所以领域模型的建立通常都是伴随着业务需求的出现。因此领域模型是核心,包含了业务概念以及概念之间的关系,它可以帮助团队统一认识以及指导设计。



但是领域建模具有一定的门槛,其中包含了很多难以理解的概念,这也造成了在很多技术团队中难以落地。但是在阿里等国内一线互联网公司却有着广泛的应用,因为 DDD 领域驱动设计可以指导我们应对复杂系统的设计开发,控制系统复杂度,帮助我们划分业务域,将业务模型域实现细节相分离。所以慕枫觉得让大家认识到 DDD 领域驱动设计以及领域模型的的重要性比如何玩转 DDD 本身更加重要。



另外在这里不得不提一下数据模型和领域模型的区别,在实际的工作中我发现很多同学都容易将这两者混淆。领域模型关注的是业务场景下的领域知识,是业务需求中概念以及概念之间的关系,它的存在就是显示的精确的表达业务语义。而数据模型关注的是业务数据如何存储,如何扩展以及如何操作性能更高。因此他们关注的层面不同,领域模型关注业务,数据模型关心实现。


这里可以举个例子给大家说明一下,假设有这样的业务场景,告警规则中存在一个规则范围的概念,主要可以给出不同的告警取值判断的范围,比如某个接口调用次数失败的最大值,或者设备在线数量不能低于某个最小值等等,因此有了如下简化版本的领域模型。



那么在实际实现落地的时候,就很自然想到将 AlarmRule 以及 RuleRange 分别用一个表进行进行存储。这其实就是把领域模型和数据模型混淆的典型例子,实际上我们没有必要搞两张表来存储,一张表其实就够了,主要有以下两个原因:


1、写代码的时候我们维护一张表肯定比维护两张表操作起来更加方便;


2、另外万一后面 ruleRange 有新的变化,增减了新的判断条件,我们还得要修改 rule_ranged 字段,不利于后期的扩展。



因此我们用一张表来就进行存储就好了,多一个 json 类型的字段,专门存储阈值判断范围。只不过在领域模型中我们需要把 c_rule_range 定义为一个对象,这样在代码层面操作起来比较方便。



牢记设计原则


无论设计原则还是设计模式,都是先驱们在以往大量软件设计开发实践中总结出来的宝贵经验,因此我们在项目开发中完全可以站在巨人的肩膀上利用这些设计原则指导我们进行编码。当然如果我们想熟练使用这些设计原则,就必须先要理解他们,搞清楚这些设计原则到底是为了解决什么问题而产生的。


我们不妨仔细想一想,平日时间里技术同学的开发工作基本上都是在已有的服务中进行新需求开发或者在原有的逻辑中修修改改。因此如果因为一个需求需要修改原有代码逻辑,我们总是希望修改的地方越少越好,否则如果修改的地方多了,那么引入的 Bug 风险就会越大。即便是项目需要进行重构的情况,那我们也希望重构后的服务或者组件可以满足高内聚低耦合的大要求,这样在未来进行需求开发的时候可以更加方便的进行修改。这也是我们希望我们开发的代码高内聚低耦合的原因。可以看得出来,设计原则的核心思想就是帮助技术人员开发的软件平台能够更好地应对各种各样的需求变化,从而最终达到降低维护成本,提高工作效率的目的。


当我们说到设计原则的时候,通常都会想到 SOLID 五大原则,这里所说的设计原则主要包括 SOLID 原则、迪米特法则。


单一职责原则


对于一个方法、类或者模块来说,它的职责应该是单一的,方法、类或者模块应该只负责处理一个业务。这个原则应该很好理解,当我们在写代码的时候,无论是方法、类以及模块都应该从功能或者业务的角度考虑将无关的逻辑抽离出去。为什么这么做呢?主要还是为了能够实现代码业务功能的原子化操作,这样即便未来进行修改的时候影响的范围也会变得有限。如果我们不遵守单一职责原则,那么在修改代码逻辑的时候很可能影响了其他业务的逻辑,造成修改影响范围不可控的情况。



You want to isolate your modules from the complexities of the organization as a whole, and design your systems such that each module is responsible (responds to) the needs of just that one business function.



不过需要说明的是,这里的所说的单一职责是针对当前的业务场景来说的,也许随着业务的发展和场景的扩充,原来满足单一职责的方法、类或者模块可能现在就不满足了需要进一步的拆分细化。


开闭原则


慕枫认为开闭原则与其说它是一种设计原则,不如说它是一种软件设计指导思想。无论我们编写框架代码还是业务代码都可以在开闭原则这样的核心思想指导下进行设计。



Software entities (modules, classes, functions, etc.) should be open for extension , but closed for modification。



所谓开闭原则指的就是我们开发的框架、模块以及类等软件实体应该对扩展开放,对修改关闭。这个原则看上去很容易理解,但是在进行项目实际落地的时候却不是一件容易的事情。因为对于扩展以及修改并没有明确的定义,到底什么样的代码才是扩展,什么样的代码才是修改?这些问题不搞清楚的话,我们很难把开闭原则落地到实际的项目开发中。


结合自己的开发经验可以这么理解,假设我们在项目中开发一个功能的时候,如果能做到不修改已有代码逻辑,而是在原有代码结构中扩展新的模块、类或者方法的话,那么我们认为代码是䄦开闭原则的。当然这也不是绝对的,比如假设你修改一个原有逻辑中的判断条件的阈值,那只能在原有代码逻辑中进行修改。总不能因为要满足这个原则非要搞出来。所以我觉得我们不必要教条的去追求满足开闭原则,而是从大方向上以及整体上考虑满足开闭原则。


里氏替换原则


在面向对象思想构建的程序中,子类对象可以替换程序中任何地方出现的父类对象,同时还能保证程序的逻辑不变以及正确性不变,这就是里氏替换原则的字面理解。不知道大家有没有发现,这个里氏替换原则看上去和 Java 中的多态一样一样的。实际上他们还是有区别的,多态是面向对象编程的特性,是重要的代码实现思路。而里氏替换原则是一种设计原则,约定子类不能破坏父类定义好的逻辑以及异常处理。


比如在仓储业务域中,父类中有对拣货任务进行排序的 sortPickingTaskByTime()方法,它是按照任务创建的时间对到来的拣货任务进行排序,那么我们在子类实现的时候如果在 sortPickingTaskByTime()方法内部按照拣货任务涉及的商品品类进行排序,那么明显是不符合里氏替换原则的,但是从多态的角度来说或者从语法的角度来说却没有问题。


里氏替换原则的核心思想就是按照约定办事,父类约定好了的行为,子类实现需要严格遵守。那么里氏替换原则对于实际编码有什么指导意义呢?比如上文所说的 sortPickingTaskByTime()排序方法,如果父类中的算法实现效率不高,我们可以在子类中进行优化,有了里氏替换原则就可以通过子类改进当前已有的实现。另外父类中的方法定义就是契约,可以指导我们后面的编码。


接口隔离原则


所谓接口隔离说的是接口调用方不应该被迫依赖它不需要的接口。怎么理解这句话呢?按照慕枫自己的理解,接口调用方只关心和自己业务相关的接口,其他不相关的接口应该隔离到其他接口中。



Clients should not be forced to depend upon interfaces that they do not use。



从扩展能力层面来看,我们定义接口的时候按照原子能力进行定义,避免了定义一个大而全的接口,这样在进行扩展的时候就可以按照具体的原子能力来进行,这样无论是灵活性还是通用性上面都会更加满足需求。


从实现上来说,如果实现方仅仅需要实现它以来的接口功能就好,它不需要的接口功能就不需要实现,这样也会大大降低代码实现量。当我们扩展或者修改代码的时候能够做到最小化的修改。


依赖倒置原则                                                                                      依赖倒置原则不太容易理解,但是我们在实际的项目开发中却每一天都在使用,只是我们可能没太在意罢了。                  



High-level modules shouldn't depend on low-level modules. Both modules shoud depend on abstractions.In addition,abstractions shouldn't depend on details.Details depend on abstractions.



按照字面意思理解,高层级模块不应该依赖低层级模块,同时两者都应该依赖于抽象。另外抽象不应该依赖于细节,细节应该依赖于抽象。用大白话来说主要是两个核心点,一是面向接口编程,另一个是基础层依赖核心层。


面向接口编程这个应该很好理解,因为接口定义了清晰的协议规范,研发同学可以基于接口进行开发。



                                                                     


迪米特法则                                                                                         


迪米特法则看名字是一点不知道它是干什么的,简单来说就是类和类之间能不要有关系就不要有关系,实在没办法必须要有关系的那也尽量只依赖必要的接口。这样说起来感觉还是比较抽象。看下面的图就明白了,左边的各个模块拆分比较独立,符合单一职责原则,同时模块间只依赖它所需要的模块,而下图右边的模块拆分不够独立,A 模块本来只需要依赖 F 模块,但是 FG 模块颗粒度较大,导致不得不依赖 G 模块的接口,显然这是不符合迪米特法则的。                                                                                            



当我们有了写出来的代码能够实现高内聚低耦合、易扩展以及易维护愿景之后,那就要好好学习一些代码实现的设计原则,这些设计原则在战略层面可以指导我们扩展性强的代码应该往哪些方向进行设计考虑。而有了指导思想之后,结合不同场景下的设计模式就自然催生出来我们想要的结果。



运用设计模式


设计模式是先驱们在实践的基础上总结出来可以落地的代码实现模板,针对一些业务场景提供代码级解决方案。我们根据各个设计模式的能力特点可以将 23 种设计模式分类为创建型模式、结构型模式以及行为型模式。这里不再对设计模式进行展开说明,后面有时间可以写系列文章专门进行介绍。不过我们需要清楚的是这 23 种设计模式就是程序员写代码打天下的招式,而提升代码扩展性才是最终目的。



面向失败编码


代码中的异常处理往往最能体现技术同学的编码功力。完成一个需求并不难,但是能够考虑到各种异常情况,在异常发生的时候依然可以得到预想输出的代码,却不是每个程序员都能写出来的。  因此无论是写代码还是系统设计,都要有面向失败进行设计的意识,每一个业务流程都要考虑如果失败了应该怎么办,尽可能考虑周全可能会出现的意外情况,同时针对这些意外情况设计相应的兜底措施,以实现防御性编码。


这里假设有这样的业务场景,当我们的业务中有调用外部服务接口的逻辑,那么我们在编写这部分代码的时候就需要考虑面向失败进行编码。因为调用外部接口有可能成功,有可能失败。如果接口调用成功自然没什么好说的,继续执行后续的业务逻辑就好。但是如果调用失败了怎么办,是直接将调用异常返回还是进行重试,如果重试还是失败应该怎么办,需不需要设计下重试的策略,比如连续重试三次都失败的话,后续间隔固定时间再进行重试等等。当然我们并不需要在每个这样的业务流程中这么做,在一些比较核心的业务链路中不能出错的流程中要有兜底措施。



总结


本文主要从理论层面为大家介绍写好代码的需要哪些知识储备,下一篇会从具体业务场景出发,具体实操怎么结合这些理论知识来把代码写好。不过我们必须认识到好代码是需要不断打磨的,并非一朝一夕就能练就,总是需要在不断的实践,不断的思考,不断的体会以及不断的沉淀中实现代码能力的提升。左手设计原则,右手设计模式,心中领域模型再加上强烈的内在驱动,我相信我们有信心一定可以写出一手好代码。


作者:慕枫技术笔记
来源:juejin.cn/post/7257518360099405883
收起阅读 »

技术人如何快速融入团队?

写在前面:文末「拓展阅读」的两篇文章写得很好,提纲挈领,推荐阅读。本文偏个人感悟,教学不敢说,日后若有更深刻的感悟会再重新整理。 很多人在进入新团队时会焦虑,害怕做不好,害怕才不配位,不知道如何开展工作。这是一种正常现象,因为针对「融入团队」这件事,我们没有...
继续阅读 »

写在前面:文末「拓展阅读」的两篇文章写得很好,提纲挈领,推荐阅读。本文偏个人感悟,教学不敢说,日后若有更深刻的感悟会再重新整理。



很多人在进入新团队时会焦虑,害怕做不好,害怕才不配位,不知道如何开展工作。这是一种正常现象,因为针对「融入团队」这件事,我们没有刻意练习,没有找到一套行之有效的方法。


下面,我将结合《程序员的底层思维》 这本书介绍的方法,以及个人实践经验,来聊聊如何快速融入团队。


本文适合有 3 年以上的技术工作者阅读,低年限或者非技术同学也有一定的参考意义。


工作拆解


对于一个企业而言,核心组成要素无非就是人、业务、技术、文化。因此工作的开展可以从这四个角度出发,并逐层拆解,力争从陌生变熟悉。





目标:熟悉组织结构、人员分工,并与未来可能有合作关系的人建立关系。


行动:



  1. 了解组织结构

  2. 了解人员分工

  3. 建立关系




业务


目标:熟悉业务,对产品定位、用户人群、行业现状有一定了解。


行动:



  1. 了解业务现状

  2. 梳理业务流程

  3. 理解用户




技术


目标:熟悉团队技术现状,方便后续开展工作



切勿一上来就高谈阔论、方法论,推翻重构,对过往保持敬畏。



行动:



  1. 熟悉架构,包括系统架构、领域模型、代码结构

  2. 了解研发流程,从一个小需求入手,掌握相关的流程和权限

  3. 先小后大,以点破面。从小点突破,比如性能优化,先拿到业绩,再准备大的规划。




文化


目标:熟悉企业文化


行动:



  1. 理解公司使命

  2. 理解业务愿景

  3. 理解公司价值观,并做到知行合一




心态调整



不着急,不害怕,不要脸 — 冯唐《冯唐成事心法》



上一章讲的是「术」,是方法论,但光会「术」有可能会碰壁,因为心态问题。



  • 不着急:每个人到一个新团队,总想着快速理解业务、快速出成绩,来证明自己的价值。可以理解,但是不必着急,多给自己和他人一些时间,做好规划,安排好时间尽力而为即可,切勿急功近利。

  • 不害怕:不害怕事情失败,培养成长性思维,相信明天的自己比今天更优秀。记住一句话:成功是一时的,成长是一辈子的;还有一句老话:失败是成功之母。大不了,重头再来。

  • 不要脸:不怕丢脸、不怕打脸。很多人进入新团队,不敢发言不敢提问,殊不知这是露脸的好机会,可以让更多人更快地认识自己。还有一种是怕向年龄或资历更小的人提问,觉得丢人,选择自己研究导致浪费时间。孔子有云 “不耻下问”,改变心态,对方对某块事物的理解就是比自己熟,不害怕提问,帮助自己更快地获取知识并融入团队。


以上,不着急、不害怕、不要脸,改变心态,方能更好的融入。


总结


融入团队是需要刻意练习的。


先调整心态,不着急、不害怕、不要脸。


再逐步拆解工作,按人、业务、技术、文化四个方向开展。


最后会发现,「融入团队」这件事,其实和做题一样简单,唯一的变量,也就是人而已。



作者:francecil
来源:juejin.cn/post/7257774805431877689

收起阅读 »

一名(陷入Android无法自拔的)大二狗的年中总结

前言 大家好,我是入行没多久,不会写代码的RQ。一名来自双非学校的大二狗。2023时间已经过去了一半,我的大学生活也过去了一半。借着这个2023年中总结的话题我也想给我的前半段大学生活做一个总结和记录(希望大家原谅理工男的表达能力,已经在学着写博客了🙃)。 大...
继续阅读 »

前言


大家好,我是入行没多久,不会写代码的RQ。一名来自双非学校的大二狗。2023时间已经过去了一半,我的大学生活也过去了一半。借着这个2023年中总结的话题我也想给我的前半段大学生活做一个总结和记录(希望大家原谅理工男的表达能力,已经在学着写博客了🙃)。


大学之前


大学之前,我的高中初中都是在一个小乡镇度过,每天都是过着教室、食堂、厕所三点一线的生活。可能偶尔会和几个好兄弟打打球,开开黑。那时候一心只读圣贤书,从未碰过电脑(也只有偶尔去网吧玩玩电脑游戏),也未曾了解过任何跟代码相关的东西。只有在高三快毕业了,学校进行志愿填报培训的时候,我才在想我想干什么。


qq_pic_merged_1689681281489.jpg


我想学编程,我想搞钱,我要成为编程高手!!哈哈哈,当时的确是这么想的,因为我一直都觉得会电脑,会编程的“大黑客”很酷!。然而结果是


qq_pic_merged_1689681309027.jpg


当时我还一时兴起,在京东上买了本0基础学python的书:


IMG_20230718_152056.jpg


奈何高三学业繁忙没时间看,而且也没有电脑实操,看了十几页压根不知道在讲什么,之后这本书也就放着吃灰了。现在看来当时确实挺傻×的。后来上了大学,自学了python,这本书也就送给了室友。


高三的时候大家都想着我要上某某985!我要上某某211!当然我也不例外。然而等到高考出分,才知道现实是多么残酷。我的高考成绩也只够报一个末流的211,报不了什么985。思虑最终我报了一个专业性比较强的双非计算机。因为我觉得一个专业不对口的末流211不如一个专业好的双非。


上大学前我是保持怀疑的,我没有任何相关编程经验,甚至是接触电脑的机会都少。不过幸运的是家里人都支持我,给我买了一台不错的笔记本。那个暑假我加入了我们学校的新生群,我发现原来大家都是卷王。有初中就开始接触编程的,有高中就学完的java的,有暑假已经快把c语言学完了的。为了不落后,高考完的那个后半个暑假我也在偷偷学c语言,能力有限,到开学也才学到指针多一点点(指针这个东西对于当时的我简直就是噩梦)。


大一


大一开学后,我同大多数人一样,满怀期待地踏进了向往的大学生活。在第一次年级集中会上,我收到了一份宣传单。那是一份我们学校的一个互联网组织的宣传单。分有产品、视觉、后端、移动、前端、运维几个部门。听说里面全是编程大牛,学校里顶尖技术人员的集聚地。这不就是我想成为的人吗?于是我下定决心我要加入他们。


大一的时候大部分课余时间都花在了这个叫做红岩网校工作站的课程上面。大一上半个学期学会了javase,下半个学期开始学写APP,会写几个简单的Activity页面,当时我还写了个整蛊APP(只是简单将声音放到最大然后播放整蛊音乐lost-rivers。哈哈哈这个不提倡,小心被打)。当然学校课程我也没有忘记,我记得c语言期末大作业自己写了个贪吃蛇和俄罗斯方块:


image.png


image.png


一行一行敲了八九百行,对于当时还是编程小白的我是个不小的成就了。


后来的一整个寒假都在写我们移动开发部的寒假考核,也是我人生中的第一个项目--彩云天气app(地址就不贴了,现在看来写的代码就是💩)。


大一的下学期,开学自学了Kotlin语言,从此再也不想用java了😭。之后也是按照网校的课程学了jetpack、rxjava、retrofit、MvvM等等。到了五一,写了自己的第二个项目--星球app(时间管理类app),也是网校的期中考核(当然也顺利通过啦~)。后面自学了python,简单写了一个抢课的脚本(以后再也不怕抢不到课了😭)。之后自己租了个服务器用python搭了个QQ机器人,后面搞到网校招新群里去玩了。不得不说Bot社区真的不错,文档什么的都很完善,对QQ机器人感兴趣的可以试试(概览 | Bot (baka.icu))。


大一的暑假,我留在了学校参加了网校的暑期培训。培训期间简单研究了一下Android性能优化跟LeakCanary,然后也是写了自己的第三个项目:开眼APP(RQ527/KaiYan,图片可能寄掉了。)


最终呢也是没有辜负自己的努力通过了最终的考核成为了网校的干事:


mmexport1689672953594.jpg

总的来说,大一学年算是踏入了编程的门吧,没有在荒废中度过。同时也要感谢网校给了我这个机会😁。


大二


大二的课余时间主要都花在了给移动开发部门培养新血液的事情上面。因为我的上一届也就是带我们的学长他们大三了,准备考研的考研,就业的就业,自然教学的任务就落到了我们头上。期间上了三节课,我发现给他们上课的同时也是给我自己上课。学习一个东西最有效的方式就是给别人讲懂。


这是大二刚开学的宣讲会😁:


IMG_20221003_185411.jpg


1664878518099.jpeg


大二期间我还了解了一下ktor和compose,嗯~,不算深入吧,简单写了几个demo。
同时自己也接手了一个多人项目,跟我们部门的另外一个人写一个类似于微博投票表决的项目,不过还没上线。


下半个学期自己用hexo+butterfly搭了个个人博客网站:rq527.github.io (还没钱买域名,暂时先用github吧😭),页面大概长这样:


image-20230719144318074


image-20230719144342289


个人思考


我认识到了什么



  • 接受自己的平庸,接受任何方面的平庸。

  • 永远不要斤斤计较

  • 杜绝一分钟热度,永远保持一颗热忱的心

  • 打铁还需自身硬

    从入行Android 开发以来,网上很多人都说 “Android 开发早就凉了,现在就是死路一条”,“现在学Android就是49年入国军!”等等。但是我身边同行的人还不是能找到实习,找到工作。我的意思是,什么事情都是需要自己有实力。





说实话,上了大学我最痛惜的是那些曾经交好的朋友也逐渐不联系了,一张通知书撕裂了一群人,以后再见也不知道是什么时候了。


未来的事情



  • 管理移动开发部

  • 找实习(目标是进大厂)


盘点一下要做的事情,发现太多了,主要的方向是这两个。人外有人,天外有天,比你牛逼的人还有很多,一直保持学习吧🤕!


最后


最后我想说很感谢家里人的支持,他们没有说反对我,强制要求我当老师,当警察等等,而是支持我所做的一切。同时也很感谢那个她,陪我一起成长,学习,愿意和我分享快乐,听我诉说(世上最幸运的事情莫过于此了吧😁)。也很感谢网校给我这么一个平台,让我认识了很多志

作者:RQ527
来源:juejin.cn/post/7257056512610517048
同道合的兄弟和伙伴。

收起阅读 »

前端:需要掌握哪些技能才能找到满意的工作?

如果你在找前端工作,你一定求助过不少大佬传授找工作和面试经验,而你得到的答案肯定很多时候就是简单的一句话:把 html、css、 js 基础学扎实,再掌握vue或react前端框架之一就可以了。 真的是这样吗?技术上看似乎没问题,但是找工作不只要从技术上下手,...
继续阅读 »

如果你在找前端工作,你一定求助过不少大佬传授找工作和面试经验,而你得到的答案肯定很多时候就是简单的一句话:

把 html、css、 js 基础学扎实,再掌握vue或react前端框架之一就可以了。

真的是这样吗?技术上看似乎没问题,但是找工作不只要从技术上下手,还要从个人目标和公司的招人标准综合进行考量,然后你还需要掌握一套有逻辑、有结构的面试回答技巧。接下来我们逐一分析一下,相信你看完之后就有了方向和方法,一定能找到满意的工作。

个人目标

现在我们的教育并没有太着重于个人目标和职业规划的设定,但找工作与其关系特别大。如果你想找一个大厂,那么准备方向就跟创业公司完全不一样。我们分别来看一下这两种情况。

大厂

大厂可能更看重你的 htmlcss 和 JavaScript 基础,以及数据结构、算法和计算机网络。你的准备方向就应该是这些基础方面的东西。另外还有一些原理方面的知道,比如你要做 vue 或者 react 开发,那就要知道 virtual dom 和 diff 算法的原理。

创业公司

如果你的目标是创业公司(这种公司的发展前景不可预测,可能大展宏图,也可能半途而废),你需要有大量的实战经验,因为创业公司为了抢占市场,产品的开发进度一般都会特别紧张,你需要去了就能够立刻干活;而理论方面的东西则会关注的少一些。针对面试,你需要去准备相关技术(比如 React 或 Vue) 的实战项目经验。

所以要想知道学到什么程度才能去找工作,首先得明确一下你的目标,是想去大厂,还是去创业公司,然后分别进行准备和突破。

公司要求

接下来再看一下公司的招聘要求,好多公司都写的特别专业、全面,除了基本语法、框架外,还要求有兼容性调整、性能优化、可视化经验,或者是掌握一些小众框架。这些招聘信息其实描述的是最佳人选,几乎在100个里面才能挑出1个来,而这种大牛级别的人自己也向往更好的工作机会,所以可能根本不会跟你有竞争关系。公司这么写招聘要求目的只有一个,就是找一个技能越全的人越好。

事实上,你只需满足要求的百分之80%,70%,甚至 50% 都有可能获得这份工作机会,因为面试不光看技术,还要看眼缘、人缘:如果面试官觉得你们投缘的话,你即使有不会的问题,他也会主动引导你帮你回答上来;要是不投缘(有些比较250的面试官),那就算你会的再多,他也会觉得你很菜(你不懂他懂的)。所以说那些招聘要求就只作为参考就好了,可以作为你以后的学习路线。不过这些技能还是掌握的越多越好,技多不压身,你可以一边面试一边准备,这样也不会互相影响。

技术能力

分析完外界的因素之后,来看一下咱们需要具体掌握哪些技术。

基础

作为一名前端工程师,htmlcssJavaScript 基础是一定要掌握牢固的,所有的语法点都必须要掌握,然后还要熟识面试必考的题,比如 ES6 及后面的新特性原型链Event Loop 等等。这些不是从学校学来的,而是为了面试专门突击准备的,需要反复的去看,去研究,最后把它们理解并记住。

框架

掌握这些基础之后,就需要看一下前端比较火爆的框架,react 和 vue。大厂用 React 的比较多,中小型公司用 vue 的比较多,当然这也不是绝对的。据我目前的经验来看,React 的薪水还是比较高的,不过看你自己喜好,喜欢做什么就做什么,从这两个框架中选一个深入去学,后面有时间再去研究另外一个。具体学习和准备方法可以

  • 先学基础用法,再学高级用法,最后掌握框架原理,比如:React / Vue,Redux / Vuex ,因为面试官通常喜欢问这方面的问题。针对这些一定要去看看别人的总结,然后自己研究一下,会更容易理解并记住。了解原理后,有时间再去研究一下源码,对于面试会更有帮助。
  • 理论准备完之后,实战肯定也少不了,无论是校招还是社招,无论是面大厂还是面小厂,都需要应聘者有实战经验。因为光会纸上谈兵,编码能力不够也不会有公司愿意去培养。实战就建议大家自己去网上找一些项目的灵感,然后动手去做一下。刚开始可能会觉得自己技术不够,也没有一个全局的概念,这些都是正常的过程,可以跟一些课程或者书籍,或者是网上的一些资源,学习一下,免费或收费的都可以。收费的好处就是它有一个完整的体系,让你从全局上有一条路径顺着走下去,就能完成一个目标。而免费资源需要你有充裕的时间,因为在遇到问题的时候,需要你一点一点去研究。不过在完成之后,回顾一下你的项目开发过程,也会在脑子里形成体系,再把之前看过的所有资料整理一下,也就学会了,只是时间上会比较长。
  • 有些公司的实战经验要求的比较丰富,比如兼容性调整和性能优化。这种经验就需要你在开发项目中,刻意去创造问题的场景,然后解决它。比如说兼容性调整,你就得在项目中体验一下不同浏览器对于JS和CSS 特性的支持程度,然后按需调整。而性能优化则就需要从网络请求、图片加载、动画和代码执行效率下手。

这些你搞懂了之后,基本上百分之七八十的公司都可以面过去。

软技能

上面说的是必备的硬性技术技能,还有一些必要的软技能,用以展示个人性格和工作能力。最重要的一项软技能是沟通能力。

沟通能力

沟通能力,对于面试或是汇报工作都是必须的。它跟你的自信程度是完全挂钩的,你只有自信之后才能有更好的沟通和表达能力,如果唯唯诺诺,低三下四,那么在面试或汇报工作的时候就会支支吾吾,颠三倒四。

举个例子:好多人,包括我本人,在面试的时候都会紧张,而我又属于那种特别紧张的,有些技术可能本来是熟悉的,但面试的时候人家换一个问法、或者气氛比较紧张的话,大脑就会一片空白,想说也说不出来,特别吃亏。要解决这个问题,**就要相信自己就是什么都会,面试官也不见得比自己会的多,然后面试前事先准备好常见面试题的答案,以及过往的工作经验,可以极大的增加自信。**当准备面试题的时候,可以采用框架的形式进行组织,下边介绍两个常用框架用来回答工作经验类和原理类的问题。

STAR 框架

对于工作经验相关的问题,可以使用框架组织回答,比如亚马逊北美那边面试会提前会告诉你,用一个叫STAR的框架回答问题:

  • S 是说 situation,事件/问题发生的场景。
  • T 指的是 task,在这个场景下你要解决的问题或者要完成的任务。
  • A 是 action,行动,要解决上边那些 tasks,你需要付出哪些行动?比如说第1步先去调试代码,然后第2步再去检查一下哪个变量出问题了,描述清楚每一步行动。
  • R 是 result,结果,这些行动有了什么样的结果,是成功了还是失败了,对你来说有什么帮助或者增长了什么教训。又或者往大里了说,给公司带来了什么效益。
    这样一整套就比较有逻辑。

原理回答框架

再说原理概念类的问题的回答,也是要有一套逻辑的,就比如说解释一下某某技术的工作原理,那么你要:

  • 解释一下这个技术是干什么的(What)。
  • 它有什么好处(Why)。
  • 分析一下这个技术内部用了哪些核心算法或机制,从外到里,或者由浅入深的给它剖析出来。如果是能拆解的技术,那就把每个部分或者组件的作用简单的描述一下(How)。
  • 最后再给他总结一下这个技术的核心部分。
    例如,你要回答 react 工作原理的问题:
  • 可以先说一下 React 是做什么的它是一个构建用户界面的库。
  • 然后它使用了(从浅一点的方面) virtual dom 把组件结构放在了内存中,用于提升性能。
  • 组件刷新的时候又使用了 diff 算法,根据状态的变化去寻找并更新受影响的组件(然后继续深入 diff 算法…)。
  • 再底层一些, React 分为了 React 核心和 React-dom,核心负责维护组件结构,React-dom 负责组件渲染,而 React 核心又使用了 Fiber 架构等等等等。
  • 如果你深入阅读过它的源代码也可以再结合源码给面试官详细介绍一下,最后再总结一下 react 加载组件、渲染组件和更新组件的过程,这个就是它的工作原理。

总结

这些就是前端工程师要学到什么程度才能去找工作、以及怎么找工作的一些个人看法。你需要:

设定个人目标。
辩证看待公司的招聘要求。
掌握硬技能和软技能(沟通能力)。
使用 STAR 框架和 WWH 框架组织面试回答。
按照这些方向去准备的话,一定可以会找到满意的工作。如果找到了还请记得回来炫耀一下,如果觉得文章有帮助请点个赞吧~感谢!


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

OutOfMemoryError是如何产生的

背景其实这个问题也挺有趣的,OutOfMemoryError,算是我们常见的一个错误了,大大小小的APP,永远也逃离不了这个Error,那么,OutOfMemroyError是不是只有才分配内存的时候才会发生呢?是不是只有新建对象的时候才会发生呢?要弄清楚这个...
继续阅读 »

背景

其实这个问题也挺有趣的,OutOfMemoryError,算是我们常见的一个错误了,大大小小的APP,永远也逃离不了这个Error,那么,OutOfMemroyError是不是只有才分配内存的时候才会发生呢?是不是只有新建对象的时候才会发生呢?要弄清楚这个问题,我们就要了解一下这个Error产生的过程。

OutOfMemoryError

我们常常在堆栈中看到的OOM日志,大多数是在java层,其实,真正被设置OOM的,是在ThrowOutOfMemoryError这个native方法中

void Thread::ThrowOutOfMemoryError(const char* msg) {
LOG(WARNING) << "Throwing OutOfMemoryError "
<< '"' << msg << '"'
<< " (VmSize " << GetProcessStatus("VmSize")
<< (tls32_.throwing_OutOfMemoryError ? ", recursive case)" : ")");
ScopedTrace trace("OutOfMemoryError");
jni调用设置ERROR
if (!tls32_.throwing_OutOfMemoryError) {
tls32_.throwing_OutOfMemoryError = true;
ThrowNewException("Ljava/lang/OutOfMemoryError;", msg);
tls32_.throwing_OutOfMemoryError = false;
} else {
Dump(LOG_STREAM(WARNING)); // The pre-allocated OOME has no stack, so help out and log one.
SetException(Runtime::Current()->GetPreAllocatedOutOfMemoryErrorWhenThrowingOOME());
}
}

下面,我们就来看看,常见的抛出OOM的几个路径

MakeSingleDexFile

在ART中,是支持合成单个Dex的,它在ClassPreDefine阶段,会尝试把符合条件的Class(比如非数据/私有类)进行单Dex生成,这里我们不深入细节流程,我们看下,如果此时把旧数据orig_location移动到新的final_data数组里面失败,就会触发OOM

static std::unique_ptr<const art::DexFile> MakeSingleDexFile(art::Thread* self,
const char* descriptor,
const std::string& orig_location,
jint final_len,
const unsigned char* final_dex_data)
REQUIRES_SHARED(art::Locks::mutator_lock_) {
// Make the mmap
std::string error_msg;
art::ArrayRef<const unsigned char> final_data(final_dex_data, final_len);
art::MemMap map = Redefiner::MoveDataToMemMap(orig_location, final_data, &error_msg);
if (!map.IsValid()) {
LOG(WARNING) << "Unable to allocate mmap for redefined dex file! Error was: " << error_msg;
self->ThrowOutOfMemoryError(StringPrintf(
"Unable to allocate dex file for transformation of %s", descriptor).c_str());
return nullptr;
}

unsafe创建

我们java层也有一个很神奇的类,它也能够操作指针,同时也能直接创建类对象,并操控对象的内存指针数据吗,它就是Unsafe,gson里面就大量用到了unsafe去尝试创建对象的例子,比如需要创建的对象没有空参数构造函数,这里如果malloc分配内存失败,也会产生OOM

static jlong Unsafe_allocateMemory(JNIEnv* env, jobject, jlong bytes) {
ScopedFastNativeObjectAccess soa(env);
if (bytes == 0) {
return 0;
}
// bytes is nonnegative and fits into size_t
if (!ValidJniSizeArgument(bytes)) {
DCHECK(soa.Self()->IsExceptionPending());
return 0;
}
const size_t malloc_bytes = static_cast<size_t>(bytes);
void* mem = malloc(malloc_bytes);
if (mem == nullptr) {
soa.Self()->ThrowOutOfMemoryError("native alloc");
return 0;
}
return reinterpret_cast<uintptr_t>(mem);
}

Thread 创建

其实我们java层的Thread创建的时候,都会走到native的Thread创建,通过该方法CreateNativeThread,其实里面就调用了传统的pthread_create去创建一个native Thread,如果创建失败(比如虚拟内存不足/FD不足),就会走到代码块中,从而产生OOM

void Thread::CreateNativeThread(JNIEnv* env, jobject java_peer, size_t stack_size, bool is_daemon) {
....

if (pthread_create_result == 0) {
// pthread_create started the new thread. The child is now responsible for managing the
// JNIEnvExt we created.
// Note: we can't check for tmp_jni_env == nullptr, as that would require synchronization
// between the threads.
child_jni_env_ext.release(); // NOLINT pthreads API.
return;
}
}

// Either JNIEnvExt::Create or pthread_create(3) failed, so clean up.
{
MutexLock mu(self, *Locks::runtime_shutdown_lock_);
runtime->EndThreadBirth();
}
// Manually delete the global reference since Thread::Init will not have been run. Make sure
// nothing can observe both opeer and jpeer set at the same time.
child_thread->DeleteJPeer(env);
delete child_thread;
child_thread = nullptr;
如果没有return,证明失败了,爆出OOM
SetNativePeer(env, java_peer, nullptr);
{
std::string msg(child_jni_env_ext.get() == nullptr ?
StringPrintf("Could not allocate JNI Env: %s", error_msg.c_str()) :
StringPrintf("pthread_create (%s stack) failed: %s",
PrettySize(stack_size).c_str(), strerror(pthread_create_result)));
ScopedObjectAccess soa(env);

soa.Self()->ThrowOutOfMemoryError(msg.c_str());
}
}

堆内存分配

我们平时采用new 等方法的时候,其实进入到ART虚拟机中,其实是走到Heap::AllocObjectWithAllocator 这个方法里面,当内存分配不足的时候,就会发起一次强有力的gc后再尝试进行内存分配,这个方法就是AllocateInternalWithGc

mirror::Object* Heap::AllocateInternalWithGc(Thread* self,
AllocatorType allocator,
bool instrumented,
size_t alloc_size,
size_t* bytes_allocated,
size_t* usable_size,
size_t* bytes_tl_bulk_allocated,
ObjPtr<mirror::Class>* klass)

流程如下: 

void Heap::ThrowOutOfMemoryError(Thread* self, size_t byte_count, AllocatorType allocator_type) {
// If we're in a stack overflow, do not create a new exception. It would require running the
// constructor, which will of course still be in a stack overflow.
if (self->IsHandlingStackOverflow()) {
self->SetException(
Runtime::Current()->GetPreAllocatedOutOfMemoryErrorWhenHandlingStackOverflow());
return;
}
这里官方给了一个钩子
Runtime::Current()->OutOfMemoryErrorHook();
输出OOM的原因
std::ostringstream oss;
size_t total_bytes_free = GetFreeMemory();
oss << "Failed to allocate a " << byte_count << " byte allocation with " << total_bytes_free
<< " free bytes and " << PrettySize(GetFreeMemoryUntilOOME()) << " until OOM,"
<< " target footprint " << target_footprint_.load(std::memory_order_relaxed)
<< ", growth limit "
<< growth_limit_;
// If the allocation failed due to fragmentation, print out the largest continuous allocation.
if (total_bytes_free >= byte_count) {
space::AllocSpace* space = nullptr;
if (allocator_type == kAllocatorTypeNonMoving) {
space = non_moving_space_;
} else if (allocator_type == kAllocatorTypeRosAlloc ||
allocator_type == kAllocatorTypeDlMalloc) {
space = main_space_;
} else if (allocator_type == kAllocatorTypeBumpPointer ||
allocator_type == kAllocatorTypeTLAB) {
space = bump_pointer_space_;
} else if (allocator_type == kAllocatorTypeRegion ||
allocator_type == kAllocatorTypeRegionTLAB) {
space = region_space_;
}

// There is no fragmentation info to log for large-object space.
if (allocator_type != kAllocatorTypeLOS) {
CHECK(space != nullptr) << "allocator_type:" << allocator_type
<< " byte_count:" << byte_count
<< " total_bytes_free:" << total_bytes_free;
// LogFragmentationAllocFailure returns true if byte_count is greater than
// the largest free contiguous chunk in the space. Return value false
// means that we are throwing OOME because the amount of free heap after
// GC is less than kMinFreeHeapAfterGcForAlloc in proportion of the heap-size.
// Log an appropriate message in that case.
if (!space->LogFragmentationAllocFailure(oss, byte_count)) {
oss << "; giving up on allocation because <"
<< kMinFreeHeapAfterGcForAlloc * 100
<< "% of heap free after GC.";
}
}
}
self->ThrowOutOfMemoryError(oss.str().c_str());
}

这个就是我们常见的,也是主要OOM产生的流程

JNI层

这里还有很多,比如JNI层通过Env调用NewString等分配内存的时候,会进入条件检测,比如分配的String长度超过最大时产生Error,即使说内存空间依旧可以分配,但是超过了虚拟机能处理的最大限制,也会产生OOM

    if (UNLIKELY(utf16_length > static_cast<uint32_t>(std::numeric_limits<int32_t>::max()))) {
// Converting the utf16_length to int32_t would overflow. Explicitly throw an OOME.
std::string error =
android::base::StringPrintf("NewStringUTF input has 2^31 or more characters: %zu",
utf16_length);
ScopedObjectAccess soa(env);
soa.Self()->ThrowOutOfMemoryError(error.c_str());
return nullptr;
}

OOM 路径总结

通过本文,我们看到了OOM发生时,可能存在的几个主要路径,其他引起OOM的路径,也是在这几个基础路径之上产生的,希望大家以后可以带着源码学习,能够帮助我们了解ART更深层的秘密。


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

我又听到有人说:主要原因是人不行

在工作中,我们经常把很多问题的原因都归结为三个字:人不行。曾经有UI同事指着我的鼻子说,你们没有把设计稿百分百还原,是因为你们人不行。昨天,我又听到一个研发经理朋友说,唉呀,项目干不好,主要是人不行。哦,我听到这里,有种似曾相识的感觉,于是我详细问了一下,你的...
继续阅读 »

在工作中,我们经常把很多问题的原因都归结为三个字:人不行。

曾经有UI同事指着我的鼻子说,你们没有把设计稿百分百还原,是因为你们人不行

昨天,我又听到一个研发经理朋友说,唉呀,项目干不好,主要是人不行

哦,我听到这里,有种似曾相识的感觉,于是我详细问了一下,你的人哪个地方不行。

朋友说,项目上线那天晚上,他们居然不主动留下来值班,下班就走了,自觉意识太差。代码写的很乱,不自测就发到生产环境,一点行业规范都没有。他们……还……反正就是,能不干就不干,能偷懒就偷懒,人不行!

这个朋友,代码写的很好,人品也很好,刚刚当上管理岗,我也没有劝他,因为我知道,劝他没用,反而会激怒他。

当一个人,代码写得好,人品好,他就会以为别人也和他一样。他的管理方式就会是:大家一定要像我这样自觉,不自觉我就生闷气了!

反而,当一个人代码写得差,自觉性不那么强,如果凑巧还有点自知之明,那么因为他很清楚自己是如何糊弄的,因此他才会考虑如何通过管理的方法去促成目标。

我的这些认知,满是血泪史。因为我就经历过了“好人”变“差人”的过程。

因为代码写得好,几乎在每一个公司,干上一段时间,领导都会让我做管理,这在IT行业,叫:码而优则仕

做管理以后,我就发现,并不是所有人都像我一样,也并不是各个部门都各司其职,所谓课程上学的项目流程,只存在于理想状态下。当然,其中原因非常复杂,并不一定就是人不行,也可能是流程制度有问题。比如我上面的朋友,他就没有安排上线必须留人,留什么人,留到几点,什么时候开始,什么标准算是上线完成,完成之后有什么小奖励,这些他都没有强调和干预。

但是,我们无法活在理想中。不能说产品经理的原型逻辑性差,UI的设计稿歪七扭八,我们就建议老板把公司解散吧,你这个公司不适合做软件产品,那样我们就失业了。

你只能是就目前遇到的问题,结合目前手头的仅有的仨瓜俩枣,想办法去解决。可能有些方案不符合常规的思路,但都是解决实际问题特意设置的。

比如我在项目实践中,经常遇到的一点:

产品经理没有把原型梳理明白,就拿出来给开发人员看,导致浪费大家的时间,同时也打击大家的积极性:这样就开始了,这项目能好的了吗?我们也做不完就交给测试!

这种情况,一般我都会提前和产品经理沟通,我先预审,我这关过了,再交给开发看,起码保证不会离大谱。这里面有一个点,产品没有干好自己的活,人不行?他也只有3天时间设计原型。

还有一个问题也经常出现:

即便是产品原型还算可以,评审也过了。让开发人员看原型,他们没有看的。一直到开发了,自己的模块发现了问题,然后开始吐槽产品经理设计的太烂,流程走不通。

这是开发人不行?他们不仔细看,光想着糊弄。其实是他们没有看的重点,你让我看啥,我就是一个小前端,让我看整个平台吗?让我看整个技术架构?Java该用什么技术栈?看前端,你告诉我前端我做哪一模块的功能?此时,我一般都是先分配任务,然后再进行原型评审。如果先把任务分下去,他知道要做这一块,因为涉及自己的利益,会考虑自己好不好实现,就会认真审视原型,多发现问题。这样会避免做的过程中,再返过头来,说产品经理没设计好。已经进入开发了,再回头说产品问题,其实是开发人员不负责,更确切说是开发领导的责任。

一旦听到“人不行”的时候,我就会想到一位老领导。

他在我心中的是神一般的存在,在我看来,他有着化腐朽为神奇的力量。

有一次,我们给市场人员做了一个开通业务的APP:上面是表单输入,下面是俩按钮,左边是立即开通,右边是暂时保存。后来,市场同事经常找我们:能不能把我已开通的业务,改为暂时保存,我点错了。这点小事还闹到公司大会上讨论,众人把原因归为市场推广的同事人不行:没有上过学?不认识字?开不开通自己分不清吗?

此事持续了很久,闹得不愉快。甚至市场部和研发部出现了对立的局面,市场部说研发部不支持销售,研发部说市场部销售不利乱甩锅。

我老领导知道后,他就去了解,不可能啊,成年人了,按钮老按错,肯定有问题。原来,客户即便是有合作意向,也很少有立即开通的,他们都会调查一下这个公司的背景,然后再联系市场人员开通。两个按钮虽然是左右平分,但是距离很近。于是,他把软件改了,立即开通按钮挪到上边,填完信息后,顺势点击暂时保存,想开通得滑到上面才能点击。此后,出错的人就少了。

后来,行政部又有人抱怨员工人不行。发给员工的表格填的乱七八糟,根本不认真。有一项叫:请确认是否没有错误_____。明明没有错误,但是很多人都填了“否”。尽管反复强调,一天说三遍,依然有人填错,没有基本的职场素质。

老领导,他又去了解。他把表格改了,“是否没有错误”改为“全对”,空格改为打钩。后来,填错的现象明显少了。

很多事情,我们都想以说教来控制形势。比如反复强调,多次要求,我嗓子都喊哑了。因为不管是区分按钮,还是填写表格,你不是个傻子,你的能力是可以做到的,不应该出错,出了错你就是人不行。而老领导总是以人性来控制,知道你是懒散的,肯定不愿意认真付出,因此设置一个流水线,让你随着预设的轨迹被迫走一圈。下线后,居然发现自己合格了,甚至自己都变成人才了。用他的话说就是:流程弥补能力不足。

当归因为人不行时,其实分两种情况:别人不行自己不行


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

什么是优雅的代码设计

今天我来解释一下什么样的代码才是优雅的代码设计。当然我们的代码根据实际的应用场景也分了很多维度,有偏向于底层系统的,有偏向于中间件的,也有偏向上层业务的,还有偏向于前端展示的。今天我主要来跟大家分析一下我对于业务代码的理解以及什么样才是我认为的优雅的业务代码设...
继续阅读 »

今天我来解释一下什么样的代码才是优雅的代码设计。当然我们的代码根据实际的应用场景也分了很多维度,有偏向于底层系统的,有偏向于中间件的,也有偏向上层业务的,还有偏向于前端展示的。今天我主要来跟大家分析一下我对于业务代码的理解以及什么样才是我认为的优雅的业务代码设计。

大家吐槽非常多的是,我们这边的业务代码会存在着大量的不断地持续的变化,导致我们的程序员对于业务代码设计得就比较随意。往往为了快速上线随意堆叠,不加深入思考,或者是怕影响到原来的流程,而不断在原来的代码上增加分支流程。

这种思想进一步使得代码腐化,使得大量的程序员更失去了“好代码”的标准。

那么如果代码优雅,那么要有哪些特征呢?或者说我们做哪些事情才会使得代码变得更加优雅呢?

结构化

结构化定义是指对某一概念或事物进行系统化、规范化的分析和定义,包括定义的范围、对象的属性、关系等方面,旨在准确地描述和定义所要表达的概念或事物。

我觉得首要的是代码,要一个骨架。就跟我们所说的思维结构是一样,我们对一个事物的判断,一般都是综合、立体和全面的,否则就会成为了盲人摸象,只见一斑。因此对于一个事物的判断,要综合、结构和全面。对于一段代码来说也是一样的标准,首先就是结构化。结构化是对一段代码最基本的要求,一个有良好结构的代码才可能称得上是好代码,如果只是想到哪里就写到哪里,一定成不了最优质的代码。

代码的结构化,能够让维护的人一眼就能看出主次结构、看出分层结构,能够快速掌握一段代码或者一段模块要完成的核心事情。

精简

代码跟我们抽象现实的物体一样,也要非常地精简。其实精简我觉得不仅在代码,在所有艺术品里面都是一样的,包括电影。电影虽然可能长达一个小时,两个小时,但你会发现优雅的电影它没有一帧是多余的,每出现的一个画面、一个细节,都是电影里要表达的某个情绪有关联。我们所说的文章也是一样,没有任何一个伏笔是多余的。代码也是一样,严格来说代码没有一个字符、函数、变量是多余的,每个代码都有它应该有的用处。就跟“奥卡姆剃刀”原理一样,每块代码都有它存在的价值包括注释。

但正如我们的创作一样,要完成一个功能,我们把代码写得复杂是简单的,但我们把它写得简单是非常难的。代码是思维结构的一种体现,而往往抽象能力是最为关键的,也是最难的。合适的抽象以及合理的抽象才能够让代码浓缩到最少的代码函数。

大部分情况来说,代码行数越少,则运行效率会越高。当然也不要成为极端的反面例子,不要一味追求极度少量的代码。代码的优雅一定是精要的,该有的有,不该有的一定是没有的。所以在完成一个业务逻辑的时候,一定要多问自己这个代码是不是必须有的,能不能以一种简要的方式来表达。

善用最佳实践

俗话说太阳底下没有新鲜事儿,一般来说,没有一个业务场景所需要用到的编码方式是需要你独创发明的。你所写的代码功能大概率都有人遇到过,因此对于大部分常用的编码模式,也都大家被抽象出来了一些最佳实践。那么最经典的就是23种设计模式,基本上可以涵盖90%以上的业务场景了。

以下是23种设计模式的部分简单介绍:

  1. 单例模式(Singleton Pattern):确保类只有一个实例,并提供全局访问点。
  2. 工厂模式(Factory Pattern):定义一个用于创建对象的接口,并让子类决定实例化哪个对象。
  3. 模板方法模式(Template Method Pattern):提供一种动态的创建对象的方法,通过使用不同的模板来创建对象。
  4. 装饰器模式(Decorator Pattern):将对象包装成另一个对象,从而改变原有对象的行为。
  5. 适配器模式(Adapter Pattern):将一个类的接口转换成客户希望的另一个接口,以使其能够与不同的对象交互。
  6. 外观模式(Facade Pattern):将对象的不同方面组合成一个单一的接口,从而使客户端只需访问该接口即可使用整个对象。

我们所说的设计模式就是一种对常用代码结构的一种抽象或者说套路。并不是说我们一定要用设计模式来实现功能,而是说我们要有一种最高效,最通常的方式去实现。这种方式带来了好处就是高效,而且别人理解起来也相对来说比较容易。

我们也不大推荐对于一些常见功能用一些花里胡哨的方式来实现,这样往往可能导致过度设计,但实际用处可能反而会带来其他问题。我觉得要用一些新型的代码,新型的思维方式应该是在一些比较新的场景里面去使用,去验证,而不应该在我们已有最佳实践的方式上去造额外的轮子。

这个就比如我们如果要设计一辆汽车,我们应该采用当前最新最成熟的发动机方案,而不应该从零开始自己再造一套新的发动机。但是如果这个发动机是在土星使用,要面对极端的环境,可能就需要基于当前的方案研制一套全新的发动机系统,但是大部分人是没有机会碰到土星这种业务环境的。所以通常情况下,还是不要在不需要创新的地方去创新。

除了善用最佳实践模式之快,我们还应该采用更高层的一些最佳实践框架的解决方案。比如我们在面对非常抽象,非常灵活变动的一些规则的管理上,我们可以使用大量的规则引擎工具。比如针对于流程式的业务模型上面,我们可以引入一些工作流的引擎。在需要RPC框架的时候,我们可以根据业务情况去调研使用HTTP还是DUBBO,可以集百家之所长。

持续重构

好代码往往不是一蹴而就的,而是需要我们持续打磨。有很多时候由于业务的变化以及我们思维的局限性,我们没有办法一次性就能够设计出最优的代码质量,往往需要我们后续持续的优化。所以除了初始化的设计以外,我们还应该在业务持续的发展过程中动态地去对代码进行重构。

但是往往程序员由于业务繁忙或者自身的懒惰,在业务代码上线正常运行后,就打死不愿意再动原来的代码。第一个是觉得跑得没有问题了何必去改,第二个就是改动了反而可能引起故障。这就是一种完全错误的思维,一来是给自己写不好的线上代码的一个借口,二来是没有让自己持续进步的机会。

代码重构的原则有很多,这里我就不再细讲。但是始终我觉得对线上第一个要敬畏,第二个也要花时间持续续治理。往往我们在很多时候初始化的架构是比较优雅的,是经过充分设计的,但是也是由于业务发展的迭代的原因,我们持续在存量代码上添加新功能。

有时候有一些不同的同学水平不一样,能力也不一样,所以导致后面写上的代码会非常地随意,导致整个系统就会变得越来越累赘,到了最后就不敢有新同学上去改,或者是稍微一改可能就引起未知的故障。

所以在这种情况下,如果还在追求优质的代码,就需要持续不断地重构。重构需要持续改善,并且最好每次借业务变更时,做小幅度的修改以降低风险。长此以往,整体的代码结构就得以大幅度的修改,真正达到集腋成裘的目的。下面是一些常见的重构原则:

  1. 单一职责原则:每个类或模块应该只负责一个单一的任务。这有助于降低代码的复杂度和维护成本。
  2. 开闭原则:软件实体(类、模块等)应该对扩展开放,对修改关闭。这样可以保证代码的灵活性和可维护性。
  3. 里氏替换原则:任何基类都可以被其子类替换。这可以减少代码的耦合度,提高代码的可扩展性。
  4. 接口隔离原则:不同的接口应该是相互独立的,它们只依赖于自己需要的实现,而不是其他接口。
  5. 依赖倒置原则:高层模块不应该依赖低层模块,而是依赖应用程序的功能。这可以降低代码的复杂度和耦合度。
  6. 高内聚低耦合原则:尽可能使模块内部的耦合度低,而模块之间的耦合度高。这可以提高代码的可维护性和可扩展性。
  7. 抽象工厂原则:使用抽象工厂来创建对象,这样可以减少代码的复杂度和耦合度。
  8. 单一视图原则:每个页面只应该有一个视图,这可以提高代码的可读性和可维护性。
  9. 依赖追踪原则:对代码中的所有依赖关系进行跟踪,并在必要时进行修复或重构。
  10. 测试驱动开发原则:在编写代码之前编写测试用例,并在开发过程中持续编写和运行测试用例,以确保代码的质量和稳定性。

综合

综上所述,代码要有结构化、可扩展、用最佳实践和持续重构。追求卓越的优质代码应该是每一位工程师的基本追求和基本要求,只有这样,才能不断地使得自己成为一名卓越的工程师。


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

设计模式漫谈

开门见山一句话,我认为设计模式的核心就是“封装变化点”,用古早软件工程的话语体系说就是“低耦合,高内聚”,用糙一点的话说就是既不要重复代码,又要好扩展。比如:工厂模式的核心是解除了对象创建导致的具体依赖,因为在对象的传递过程中可以使用父类型,但是在对象创建时一...
继续阅读 »

开门见山一句话,我认为设计模式的核心就是“封装变化点”,用古早软件工程的话语体系说就是“低耦合,高内聚”,用糙一点的话说就是既不要重复代码,又要好扩展。

比如:

  • 工厂模式的核心是解除了对象创建导致的具体依赖,因为在对象的传递过程中可以使用父类型,但是在对象创建时一定是要依赖到特定类型的;
  • 桥接模式的核心是避免多维度造成的子类数量指数级的膨胀;
  • 单例模式就是避免创建重复对象并使其方便分享;
  • Builder模式的核心是避免初始化是构造函数参数过多;
  • 常说组合优于继承,其实是因为继承其实也是一种耦合,子类与父类的耦合。而组合可以解除这种耦合
  • ...

这些东西吧,就是理解的人不用多说,不理解的人多说也没用。我是一个极度不擅长记忆的人,尤其年龄大了,n 年前看过的设计模式,早忘的一干二净了。所以基本当别人扯到设计模式的时候,我一般都敬而远之,因为很多时候别人说一个设计模式的时候,我都不记得这个设计模式到底是干嘛用的了。这里还刚妄言设计模式,纯粹是想表述一下我对设计的理解与实践,话不多说,直接 show code。

举例

以 Android 中的列表举例,我们可以先把元素列出,看看哪些属于模版代码

  1. url
  2. 返回的数据结构
  3. 网络请求框架
  4. 列表展示的 UI
  5. 分页逻辑
  6. 下拉刷新
  7. 网络请求失败展示错误提示
  8. 列表条目点击事件处理

暂时只列这几个比较公共的逻辑,我们可以挨个分析一下这些元素哪些是“公共”的,哪些是独有的

url

以 restful api 为例,格式为 ${domain}/${version}/${targetObj}?offset=${offsetNum}&limit=${limitNum} 我们可以看到其实其中的五个参数,只有 ${targetObj} 是与本次业务相关,其他都是公共代码

  • 推荐使用 restful api
  • 客户端与服务器端在定 api 时一定要慎之又慎,可以简单理解为客户端与服务器端交互就是通过 api 的,api 设计的合理则前后端解藕。后续不论是前端重构还是后端重构就会互不影响。如果业务耦合,那前后端动代码都要互相同步,这样的后果不用多说,就是大家深陷泥潭,动身不得。

返回的数据结构

返回的数据如下:

{
"errorCode": "",
"errorMsg": "",
"results": [
{
"id": "xxxxxx1",
...
},
{
"id": "xxxxxx2",
...
}
]
}

其中 errorCode、errorMsg、results 也都是样式代码,都可以通过 json 解析一次性解决问题,只有 results 中的数据是不同的

在 kotlin 中基本都是一行代码解决问题:

data class DataAItem(val id: String, val others: String, ...)

网络请求框架

这个不多说,Android 端现在的最佳实践就是 retrofit + okhttp

列表展示的 UI

在 Android 中,可以简单理解为单条目的 UI 对应的其实就是 holder

class DataAItemHolder(context: Context, root: ViewGroup) : BaseViewHolder<DataAItem>(context, root, R.layout.layout_data_a_item) {

override fun bindData(item: DataItem) {
binding.idView.text = item.id
binding.othersView.setContent(item.others)

binding.idView.setOnClickListener {
context.startActivity(...)
}
}
}

为了篇幅,这里就不列 R.layout.layout_data_a_item 了,相信 Androider 都明白

分页逻辑、下拉刷新、网络请求失败展示错误提示

与 url 结合,只要当时约定的 api 是格式化的,那么这里的分页逻辑与下拉刷新其实也都是公共的,因为所有类似列表页面的形式都是相同的 至于错误展示,基本逻辑也都是公共的

不同点:

  • 部分页面不需要分页和下拉刷新
  • 下拉刷新根据内容不同动画效果不同
  • 网络请求失败根据内容不同展示提示不同

我们最后说这些问题的处理

列表条目点击事件处理

这个是根据内容不同事件是不同的,但是这部分的逻辑是可以些在 DataItemAHolder 中的,见上文中定义的 DataItemAHolder

理想完整形态

所以一个单独的列表页面理论上的所有代码便如下:

data class DataAItem(val id: String, val others: String, ...)

class DataAItemHolder(context: Context, root: ViewGroup) : BaseViewHolder<DataAItem>(context, root, R.layout.layout_data_a_item) { ... }

class DataAListFragment : BaseListFragment<CommonListBinding>() {

init {
setPageUrlTarget("${targetObj}");
registerHolder(DataAItemHolder::class.java)
}
}

// 如果用注解形式,则更简洁
@Endpoint("targetObj")
@Holder(DataAItemHolder::class.java)
class DataAListFragment : BaseListFragment<CommonListBinding>() {}

还有一个 layout_data_a_item.xml

所有变化点都在代码里了,以这种形式去实现一个列表,就只有 layout_data_a_item.xml 会稍微费点时间,总共加起来也不会超过 1 小时,而且逻辑清晰、代码简洁、便于维护。

不需要 adapter,不需要 LayoutManager,不需要 ItemDecoration,甚至,这个 DataAListFragment.kt 都是模版代码,既然是模版代码,那就可以动态生成。 哪怕上述代码只能够替代 50% 的真实列表需求,其实都是极大的劳动力的解放。

至于为什么是理想完整形态,是因为我也没有完全实现上述逻辑,主要是以往写 sdk 居多,少写 UI,以上逻辑都是在我大概五六年前写过的一个框架的基础上优化而来。

问题

上边看着舒服,但是其实问题还是很多的。如果所有逻辑都往 base 或者 common 中塞,不用我多说,大家也知道是垃圾设计
我们做到了不要重复代码,那好扩展怎么办呢?
像上边 分页逻辑、下拉刷新、网络请求失败展示错误提示 中所述的不同点,还有其他的:

  • 部分页面不需要分页和下拉刷新
  • 下拉刷新根据内容不同动画效果不同
  • 网络请求失败根据内容不同展示提示不同
  • 自定义 LayoutManager
  • 自定义 ItemDecoration
  • 自定义 adapter
  • DataAItemHolder 如何创建,即 registerHolder 到底如何实现(在一个模版代码中创建具体类)
  • 支持数据缓存
  • ...

我们拿其中的几个举例:

分页开关

在 BaseBindingFragment 中:

fun enablePaging(): Boolean {
return true
}

这就是简单的模版模式。如果 DataAListFragment 是动态生成的,那可以使用 Builder 模式。

Holder 如何创建

这里其实出现了反向依赖。正常来讲,如果要创建具体的 DataAItemHolder,那么模版代码一定要依赖 DataAItemHolder,不然没法调用构造函数。 这里解决方案是固定构造函数:

  class DataAItemHolder(context: Context, root: ViewGroup) : BaseViewHolder<DataAItem>(context, root, R.layout.layout_data_a_item) {}

即所有 holder 的构造函数都是固定的 context: Context, root: ViewGroup,那可以在 Adapter 中

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CommonViewHolder<T> {
holderTypeMap[viewType]?.let {
val holder = it.getConstructor(Context::class.java, ViewGroup::class.java).newInstance(parent.context, parent)
holder.setOnClickListener(viewHolderClickListener)
return holder
}
throw IllegalArgumentException("The type :$viewType create exception")
}

通过这种形式创建,这里的 holderTypeMap 可以理解成 DataAItem 的缓存,即去看一下是否已经注册过该 Holder 了,因为 DataAItem 与 DataAItemHolder 是 1v1 绑定的关系。

当然还有其他解决方案,不过我个人目前感觉这应该算是比较好的。

其他

其他问题怎么解决大家可以自己想办法,这里不做赘述。

大总结

以上使用了哪些设计模式?我也不知道。其实只要知道了 理想完整形态,那剩下的就是想办法去解决具体细节问题了,这些细节工作量占 80%,但是从设计角度讲大概只占 20%。不管怎么搞,只要能完成既不要重复代码,又要好扩展的目标,其实就不用管啥设计模式了。重意不重形。

番外篇

我这些年大多是做 sdk 开发,呆过的公司不少,亲眼见证了不少公司从 native -> h5 -> RN -> native -> flutter(or kmm) 的技术路线修改,五味杂陈。变得只是技术路线,写代码的依然还是3年+ 初级工程师
再一个例子,前公司为了增效专门聘请了一个敏捷教练,这其实与换技术路线如出一辙。这就像是拿着设计模式往里套,套不进去再换一个。

大部分人不是尽力去解决问题,而是把时光和精力花在绕过问题上。换技术路线并不能解决产品逻辑问题,也不能解决3年+ 初级工程师的问题。这两个问题才是核心。 产品逻辑问题由人去解决,3年+ 初级工程师的问题也是人的问题。而人的问题要从解决,而不应该从外部下手。所谓的无非也是两个,一是能力,二是责任心

责任心问题

正常开发中,一定是前后端协同、rd pm 协同、产研与运营销售等部门协同,我一直认为好的业务(产品的理想完整形态)一定可以引导出代码上的合理架构。如果代码架构乱七八糟,原因无非两个,一是产品逻辑问题,二是程序员能力问题。这种通过代码设计过程中体现出的问题,绝大多数都可以追溯到产品逻辑上。这是产品优化的及其重要的一条渠道,但是遗憾的大多数时候,这条渠道名存实亡。原因无非也是两个,一是程序员的责任心问题、二是 pm 的责任心问题,大多数人本着能少一事就少一事的原则混饭吃,放任不合理的产品设计,自己也写不负责任的代码。当然万方有罪,罪在朕躬

我是亲眼见过运维的同学在月度总结会上把影响公司营收10%以上的事故当成笑话讲,我也亲眼见过只做营销活动而一点不关心产品的事业部总监。其实我想说,程式化对应员工的公司,一定也会收获程式化应对的员工,这就是你糊弄我,我糊弄你,最终双输的局面,这其实是我离职这么多家公司的最核心原因。

能力问题

程序员更应该增加的对产品和业务的感知能力,产品、迭代流程、管理,其实都是可重构的,核心从来都不是设计模式,而是找到理想完整形态并落实。只有锻炼审美能力,才能知道代码的丑、产品的丑以及管理的丑。

关于二者的解决方案其实很简单,就是人为本。公司中其实是员工占主体,但管理层是大脑。管理层建立正向循环机制,逐步剔除混日子员工。你要是还问我怎么建立正向循环机制,那你是没理解人为本

闲庭随笔,大话漫谈,鄙俚浅陋,诸君勿怪。


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

面试题:Android 中 Intent 采用了什么设计模式?

答案是采用了原型模式。原型模式的好处在于方便地拷贝某个实例的属性进行使用、又不会对原实例造成影响,其逻辑在于对 Cloneable接口的实现。话不多说看下 Intent 的关键源码: // frameworks/base...
继续阅读 »

答案是采用了原型模式

原型模式的好处在于方便地拷贝某个实例的属性进行使用、又不会对原实例造成影响,其逻辑在于对 Cloneable接口的实现。

话不多说看下 Intent 的关键源码:

 // frameworks/base/core/java/android/content/Intent.java
 public class Intent implements Parcelable, Cloneable {
    ...
     private static final int COPY_MODE_ALL = 0;
     private static final int COPY_MODE_FILTER = 1;
     private static final int COPY_MODE_HISTORY = 2;
 
     @Override
     public Object clone() {
         return new Intent(this);
    }
 
     public Intent(Intent o) {
         this(o, COPY_MODE_ALL);
    }
 
     private Intent(Intent o, @CopyMode int copyMode) {
         this.mAction = o.mAction;
         this.mData = o.mData;
         this.mType = o.mType;
         this.mIdentifier = o.mIdentifier;
         this.mPackage = o.mPackage;
         this.mComponent = o.mComponent;
         this.mOriginalIntent = o.mOriginalIntent;
        ...
 
         if (copyMode != COPY_MODE_FILTER) {
            ...
             if (copyMode != COPY_MODE_HISTORY) {
                ...
            }
        }
    }
    ...
 }

可以看到 Intent 实现的 clone() 逻辑是直接调用了 new 并传入了自身实例,而非调用 super.clone() 进行拷贝。

默认的拷贝策略是 COPY_MODE_ALL,顾名思义,将完整拷贝源实例的所有属性进行构造。其他的拷贝策略是 COPY_MODE_FILTER 指的是只拷贝跟 Intent-filter 相关的属性,即用来判断启动目标组件的 actiondatatypecomponentcategory 等必备信息。无视启动 flagbundle 等数据。

 // frameworks/base/core/java/android/content/Intent.java
 public class Intent implements Parcelable, Cloneable {
    ...
     public @NonNull Intent cloneFilter() {
         return new Intent(this, COPY_MODE_FILTER);
    }
 
     private Intent(Intent o, @CopyMode int copyMode) {
         this.mAction = o.mAction;
        ...
 
         if (copyMode != COPY_MODE_FILTER) {
             this.mFlags = o.mFlags;
             this.mContentUserHint = o.mContentUserHint;
             this.mLaunchToken = o.mLaunchToken;
            ...
        }
    }
 }

还有中拷贝策略是 COPY_MODE_HISTORY,不需要 bundle 等历史数据,保留 action 等基本信息和启动 flag 等数据。

 // frameworks/base/core/java/android/content/Intent.java
 public class Intent implements Parcelable, Cloneable {
    ...
     public Intent maybeStripForHistory() {
         if (!canStripForHistory()) {
             return this;
        }
         return new Intent(this, COPY_MODE_HISTORY);
    }
 
     private Intent(Intent o, @CopyMode int copyMode) {
         this.mAction = o.mAction;
        ...
 
         if (copyMode != COPY_MODE_FILTER) {
            ...
             if (copyMode != COPY_MODE_HISTORY) {
                 if (o.mExtras != null) {
                     this.mExtras = new Bundle(o.mExtras);
                }
                 if (o.mClipData != null) {
                     this.mClipData = new ClipData(o.mClipData);
                }
            } else {
                 if (o.mExtras != null && !o.mExtras.isDefinitelyEmpty()) {
                     this.mExtras = Bundle.STRIPPED;
                }
            }
        }
    }
 }

总结起来:

Copy Modeaction 等数据flags 等数据bundle 等历史
COPY_MODE_ALLYESYESYES
COPY_MODE_FILTERYESNONO
COPY_MODE_HISTORYYESYESNO

除了 Intent,Android 源码中还有很多地方采用了原型模式。

  • Bundle 也实现了 clone(),提供了 new Bundle(this) 的处理:

     public final class Bundle extends BaseBundle implements Cloneable, Parcelable {
        ...
         @Override
         public Object clone() {
             return new Bundle(this);
        }
     }
  • 组件信息类 ComponentName 也在 clone() 中提供了类似的实现:

     public final class ComponentName implements Parcelable, Cloneable, Comparable<ComponentName> {
        ...
         public ComponentName clone() {
             return new ComponentName(mPackage, mClass);
        }
     }
  • 工具类 IntArray 亦是如此:

     public class IntArray implements Cloneable {
        ...
         @Override
         public IntArray clone() {
             return new IntArray(mValues.clone(), mSize);
        }
     }

原型模式也不一定非得实现 Cloneable,提供了类似的实现即可。比如:

  • Bitmap 没有实现该接口但提供了 copy(),内部将传递原始 Bitmap 在 native 中的对象指针并伴随目标配置进行新实例的创建:

     public final class ComponentName implements Parcelable, Cloneable, Comparable<ComponentName> {
        ...
         public Bitmap copy(Config config, boolean isMutable) {
            ...
             noteHardwareBitmapSlowCall();
             Bitmap b = nativeCopy(mNativePtr, config.nativeInt, isMutable);
             if (b != null) {
                 b.setPremultiplied(mRequestPremultiplied);
                 b.mDensity = mDensity;
            }
             return b;
        }
     }

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

这代码,你不包装下?

不管做什么事情,我们都要有一颗上进的心,写代码也是如此。最开始要写得出,然后要写得对,然后要写得又对又好,最后再追求那个传说中的快。当然,现在好用又强大的三方库越来越多了,业务开发渐渐的变成 api boy 了。当然,api boy&nbs...
继续阅读 »

不管做什么事情,我们都要有一颗上进的心,写代码也是如此。最开始要写得出,然后要写得对,然后要写得又对又好,最后再追求那个传说中的快。

当然,现在好用又强大的三方库越来越多了,业务开发渐渐的变成 api boy 了。当然,api boy 亦有差距,就看你能不能把 api 封装得足够好用了。

今天,我来教教你们如何用协程包装下微信分享的接口。

首先看看微信分享接口,它的发送数据接口和接收接口是分开的。

发送请求为:

fun shareToWx(msg: WXMediaMessage, transaction: String){
val req = SendMessageToWX.Req()
// 唯一标示一个请求, 用于微信返回的回调使用
req.transaction = transaction
// 调用 api 接口,发送数据到微信
api.sendReq(req)
}

然后跳转到微信,微信处理完后,会调起 WXEntryActivity ,通过 onResp 去接收消息:

class WXEntryActivity() : AppCompatActivity(), IWXAPIEventHandler{
override fun onResp(baseResp: BaseResp) {
// 通过 baseResp.transaction 去 match 请求
// TODO handle the baseResp
finish()
}
}

整个流程清晰,就是请求和回应分离了,虽然有 transaction 去追踪请求连,但我也见过完全不用这个参数的开发,而是做了个假设:短时间必定只有一次分享,而且回应应该也很快,所以从微信收到的结果必定是当前界面分享的。。。(反正一般跑起来都是正常的。)

更多高明的开发可能就会用上 EventBus 了,把 resp 抛到 EventBus 里,然后业务自己去监听 EventBus 里的消息,然后处理。

这样也不是不可以,但总归一条业务链下来,代码要写在两个地方,维护起来也不是很爽,而且每次还要去关注那个 transaction 参数。

那该怎么包装呢?RxJava 有 RxJava 的包装,协程有协程的包装,但大体思路应该一样。总之我们要善于利用新的工具去让这个世界变得更美好。对于我,我当然是选择更现代的协程了。

代码如下:

private val wxMsgChannelMap = HashMap<String, Channel<BaseResp>>()

// 入口方法
suspend fun shareToWx(msg: WXMediaMessage): BaseResp {
val req = SendMessageToWX.Req()
// 构造唯一标示,仅内部使用
req.transaction = System.currentTimeMillis().toString()
// 构建一个缓存一个结果的 channel
val channel = Channel<BaseResp>(1)
return withContext(Dispatchers.Main) {
// 将 channel 存储起来
wxMsgChannelMap[transaction] = channel
try {
// 调用 api 接口,发送数据到微信
api.sendReq(req)
// 协程等待 channel 的结果
channel.receive()
} finally {
channel.close()
wxMsgChannelMap.remove(transaction)
}
}
}

class WXEntryActivity() : AppCompatActivity(), IWXAPIEventHandler{
override fun onResp(baseResp: BaseResp) {
AppScope.launch(Dispatchers.Main) {
// 从 map 中寻找到对应的 channel
val channel = wxMsgChannelMap[baseResp.transaction] ?: return@launch
if (channel.isClosedForSend || channel.isClosedForReceive) {
return@launch
}
// 向 channel 发送数据
channel.send(baseResp)
}
finish()
}
}

上面的代码,其实很简单,就是借助了 channel 而已,但只要有这一层封装,业务放就可以将逻辑写得简洁明了了:

val msg = WXMediaMessage(...) // 构造消息
val result = shareToWx(msg)
// 处理结果

是不是看上去就清爽多了?那可不可以做得更好呢? 我们必须要在工程中的特定目录下新建 WXEntryActivity, 然后各个 app 里面的代码都是非常雷同,所以,我们能不能搞个库,把它给封装并隐藏起来?就可以造福更多的开发?WXEntryActivity 要求特定目录,那是不是只能动态生成? ksp 是不是就可以上场装逼了?

问:那继续搞下去吗?

答:不搞,现在的又不是不能用。


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

学习Retrofit后,你要知道的知识点

下面我将从以下几个问题来梳理Retrofit的知识体系,方便自己理解Retrofit中create为什么使用动态代理?谈谈Retrofit运用的动态代理及反射?Retrofit注解是怎么进行解析的?Retrofit如何将注解封装成OKHttp的Call?Rre...
继续阅读 »

下面我将从以下几个问题来梳理Retrofit的知识体系,方便自己理解

  • Retrofitcreate为什么使用动态代理?
  • 谈谈Retrofit运用的动态代理及反射?
  • Retrofit注解是怎么进行解析的?
  • Retrofit如何将注解封装成OKHttpCall?
  • Rretrofit是怎么完成线程切换和数据适配的?

Retrofitcreate为什么使用动态代理

我们首先可以看Retrofit代理实例创建过程,通过一个例子来说明

    val retrofit = Retrofit.Builder()
          .baseUrl("https://www.baidu.com")
          .build()
       val myInterface = retrofit.create(MyInterface::class.java)

创建了一个MyInterface接口类对象,create函数内使用了动态代理来创建接口对象,这样的设计可以让所有的访问请求都给被代理,这里我简化了下它的create函数,简单来说它的作用就是创建了一个你传入类型的接口实例

/**
    *
    * @param loader 需要代理执行的接口类
    * @return 动态代理,运行的时候生成一个loader对象类型的类,在调用它的时候走
    */
   @SuppressWarnings("unchecked")
   public <T> T create(final Class<T> loader) {
       return (T) Proxy.newProxyInstance(loader.getClassLoader(),
               new Class<?>[]{loader}, new InvocationHandler() {
                   @Override
                   public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                       System.out.println("具体方法调用前的准备工作");
                       Object result = method.invoke(object, args);
                       System.out.println("具体方法调用后的善后事情");
                       return result;
                  }
              });
  }

那么这个函数为什么要使用动态代理呢,这样有什么好处?

我们进行Retrofit请求的时候构建了许多接口,并且都要调用接口中的对象;接下来我们再调用这个对象中的getSharedList方法

val sharedListCall:Call<ShareListBean> = myService.getSharedList(2,1)

在调用它的时候,在动态代理里面,在运行的时候会存在一个函数getSharedList,这个函数里面会调用invoke,这个invoke函数就是Retrofit里的invoke函数;并且也形成了一个功能拦截,如下图所示:

所以,相当于动态代理可以代理所有的接口,让所有的接口都走invoke函数,这样就可以拦截调用函数的值,相当于获取到所有的注解信息,也就是Request动态变化内容,至此不就可以动态构建带有具体的请求的URL了么,从而就可以将网络接口的参数配置归一化

这样也就解决了之前OKHttp存在的接口配置繁琐问题,既然都是要构建Request,为了自主动态的来完成,所以Retrofit使用了动态代理

谈谈Retrofit运用的动态代理及反射

那么我们在读Retrofit源码的时候,是否都有这样一个问题,为什么我写个接口以及一些接口Api,我们就可以完成相应的http请求呢?Retrofit到底在这其中做了什么工作?简单来说,其核心就是通过反射+动态代理来解决的,那么动态代理和反射的原理是怎么样的?

代理模式梳理

首先我们要明白代理模式到底是怎么样的,这里我简单梳理下

  • 代理类与委托类有着同样的接口
  • 代理类主要为委托类预处理消息,过滤消息,然后把消息发送给委托类,以及事后处理消息等等
  • 一个代理类的对象与一个委托类的对象关联,代理类的对象本身并不真正实现服务,而是通过调用委托类的对象的相关方法,为提供特定的服务

以上是普通代理模式(静态代理)它是有一个具体的代理类来实现的

动态代理+反射

那么Retrofit用到的动态代理呢?答案不言而喻,所谓动态就是没有一个具体的代理类,我们看到Retrofitcreate函数中,它是可以为委托类对象生成代理类, 代理类可以将所有的方法调用分派到委托对象上反射执行,大致如下

  • 接口的classLoader
  • 只包含接口的class数组
  • 自定义的InvocationHandler()对象, 该对象实现了invoke() 函数, 通常在该函数中实现对委托类函数的访问

这就是在create函数中所使用的动态代理及反射

扩展:通过这些文章,了解更多动态代理与反射

反射,动态代理在Retrofit中的运用

Retrofit的代理模式解析

Retrofit注解是怎么进行解析的?

在使用Retrofit的时候,或者定义接口的时候,在接口方法中加入相应的注解(@GET,@POST,@Query@FormUrlEncoded等),然后我们就可以调用这些方法进行网络请求了;那么就有了问题,为什么注解可以完整的覆盖网络请求?我们知道,注解大致分为三类,通过请求方法、请求体编码格式、请求参数,大致有22种注解,它们基本完整的覆盖了HTTP请求方案 ;通过它们我们确定了网络请求request的具体方案;

此时,就抛出了开始的问题,Retrofit注解是怎么被解析的呢?这里就要熟悉Retrofit中的ServiceMethod类了,总的来说,它首先选择Retrofit里提供的工具(数据转换器converter,请求适配器adapter),相当于就是具体请求Request的一个封装,封装完成之后将注解进行解析;

下面通过官方提供例子来说明下ServiceMethod组成的部分

5ce1ff984d5b49de878b45e7a88af7a.png

其中 @GET("users/{user}/repos"是由parseMethodAnnotation负责解析的;@Path参数注解就由对应ParameterHandler进行处理,剩下的Call<List<Repo>毫无疑问就是使用CallAdapter将这个Call 类型适配为用户定义的 service method 的返回类型。

那么ServiceMethod是怎么对注解进行解析的呢,来简单梳理下它的源码

  • 首先,在loadService方法中进行检测,禁止静态方法,这里Retrofit笔者使用的是2.9.0版本,不再是直接调用ServiceMethod.Builder(),而是使用缓存的方式调用ServiceMethod.parseAnnotations(this, method),将它转为RequestFactory对象,其实本地大同小异,原理是差不多的
final class RequestFactory {
 static RequestFactory parseAnnotations(Retrofit retrofit, Method method) {
   return new Builder(retrofit, method).build();
}
...
  • 同样在RequestFactory中也是使用Builder模式,其实就是封装了一层,传入retrofit-method两个参数,在这里面我们调用了Method类获取了它的注解数组methodAnnotations,型参的类型数组parameterTypes,型参的注解数组parameterAnnotationsArray

     Builder(Retrofit retrofit, Method method) {
         this.retrofit = retrofit;
         this.method = method;
         this.methodAnnotations = method.getAnnotations();
         this.parameterTypes = method.getGenericParameterTypes();
         this.parameterAnnotationsArray = method.getParameterAnnotations();
      }
  • 然后在build方法中,它会创建一个ReqeustFactory对象,最终解析它通过HttpServiceMethod又转换成ServiceMethod实例,这个方法里主要就是针对注解的解析过程,由于源码非常长,感兴趣的同学可以去详细阅读下,这里大概概括下几个重要的解析方法

    1. parseMethodAnnotation

    该方法就是确定网络的传输方式,判断加了哪些注解,下面借用一张网络上的图表达会更直观点

aHR0cDovL2ltZy5ibG9nLmNzZG4ubmV0LzIwMTYwOTIzMTMyMzU4NDA2.png

  1. parseHttpMethodAndPath,parseHeaders

    我们通过上图也可以看到,其实就是解析httpMethodheaders,它们都是在parseMethodAnnotation方法中被调用的,从而进行细化。前者确定的是请求方法(get,post,delete等),后者顾名思义确定的是headers头部;前者会检测httpMethod,它不允许有多个方法注解,会使用正则表达式进行判断,url中的参数必须用括号占位,最终提取出了真实的urlurl中携带的参数名称;后者就是解析当前Http请求的头部信息

  • 经过以上方法注解处理以及验证,在build方法中还要对参数注解进行处理

    int parameterCount = parameterAnnotationsArray.length;
         parameterHandlers = new ParameterHandler<?>[parameterCount];
         for (int p = 0, lastParameter = parameterCount - 1; p < parameterCount; p++) {
           parameterHandlers[p] =
               parseParameter(p, parameterTypes[p], parameterAnnotationsArray[p], p == lastParameter);
        }

    它循环进行参数的验证处理,通过parseParameter方法最后一个参数判断是否继续,参考下网络上的图示

aHR0cDovL2ltZy5ibG9nLmNzZG4ubmV0LzIwMTYwOTIzMTMyNDM1MDQ3.png

简单说下ParameterHandler是怎么处理这个参数注解的?它会通过进行两项校验,分别是对不确定的类型合法校验路径名称的校验,然后就是一堆参数注解的处理,分析源码后可以看到ParameterHandler最终都组装成了一个RequestBuilder,那么它是用来干神马的?答案是生成OKHttpRequest,网络请求还是交给OKHttp来完成

以上简单分析了下Retrofit注解的解析过程,需要深入了解的同学请自行探索。

如果同学对注解不太熟悉,想要了解Java注解的相关知识点可以阅读这篇文章--->(Retrofit注解

Retrofit如何将注解封装成OKHttpcall

上个问题已经知道了Retrofit中的ServiceMethod对会注解进行解析封装,这时候各种网络请求适配器,请求头,参数,转换器等等都准备好了,最终它会将ServiceMethod转为Retrofit提供的OkHttpCall,这个就是对okhttp3.Call的封装,答案已经呼之欲出了。

 @Override
 final @Nullable ReturnT invoke(Object[] args) {
   Call<ResponseT> call = new OkHttpCall<>(requestFactory, args, callFactory, responseConverter);
   return adapt(call, args);
}

换种说法,相当于在ServiceMethod中已经将注解转变为url +请求头+参数等等格式,对照OKHttp请求流程,是不是已经完成了构建Request请求了,它最终要变成Okhttp3.call才能进行网络请求,所以OkHttpCall基本上就是做了这么一件事情,下面有张图可以直观看下ServiceMethod大概做了哪些事情

接着我们看下okhttpCall中的enqueue方法,它会去创建一个真实的Call,这个其实就是OKHttp中的call,接下来的网络请求工作就交给OKHttp来完成

private okhttp3.Call createRawCall() throws IOException {
   okhttp3.Call call = callFactory.newCall(requestFactory.create(args));
   if (call == null) {
     throw new NullPointerException("Call.Factory returned null.");
  }
   return call;
}

到这里,说白了Retrofit其实没有任何与网络请求相关的东西,它最终还是通过统一解析注解Request去构建OkhttpCall执行,通过设计模式去封装执行OkHttp

Rretrofit是怎么完成线程切换和数据适配的

Retrofit在网络请求完成后所做的就只有两件事,自动线程切换和数据适配;那么它是如何完成这些操作的呢?

关于数据适配

其实这个在上文注解解析问题中已经回答了一部分了,这里我大概总结下流程,具体的数据解析适配过程细节需要大家私下去深入探讨;在封装的OkhttpCall调用OKHttp进行网络请求后会拿到接口响应结果response,这时候就要进行数据格式的解析适配了,会调用parsePerson方法,里面最终还是会调用RetrofitconverterFactories拿到数据转换器工厂的集合其中的一个,所以当我们创建Retrfoit中进行addConvetFactory的时候,它保存在了Retrofit当中,交给了responseConverter.convert(responsebody),从而完成了数据转换适配的过程

关于线程切换

首先我们要知道线程切换是发生在什么时候?毫无疑问,肯定是在最后一步,当网络请求回来后,且进行数据解析后,那这样我们向上寻根,发现最终数据解析由HttpServiceMethod之后它会调用callAdapter.adapt()进行适配

 protected Object adapt(Call<ResponseT> call, Object[] args) {
     call = callAdapter.adapt(call);
    ....
}

这意味着callAdapter会去包装OkhttpCall,那么这个callAdapter是来自哪里的,追本朔源,它其实在Retrofit中的build会去添加defaultCallAdapterFactories,这个方法里就调用了DefaultCallAdapterFactory,真正的线程切换就在这里

 Executor callbackExecutor = this.callbackExecutor;
     if (callbackExecutor == null) {
       callbackExecutor = platform.defaultCallbackExecutor();
    }    
// Make a defensive copy of the adapters and add the default Call adapter.
     List<CallAdapter.Factory> callAdapterFactories = new ArrayList<>(this.callAdapterFactories);
     callAdapterFactories.addAll(platform.defaultCallAdapterFactories(callbackExecutor));

这个默认的defaultCallAdapterFactories会传入平台的defaultCallbackExecutor(),由于我们平台是Android,所以它里面存放的就是主线程的Executor,它里面就是一个Handler

    static final class MainThreadExecutor implements Executor {
     private final Handler handler = new Handler(Looper.getMainLooper());

     @Override
     public void execute(Runnable r) {
       handler.post(r);
    }
  }

到这来看下DefaultCallAdapterFactory中的enqueue(代理模式+装饰模式), 这里面使用了一个代理类delegate,它其实就是Retrofit中的OkhttpCall,最终请求结果完成后使用callbackExecutor.execute()将线程变为主线程,最终又回到了MainThreadExecutor当中

callbackExecutor.execute(
                () -> {
                   if (delegate.isCanceled()) {
                     // Emulate OkHttp's behavior of throwing/delivering an IOException on
                     // cancellation.
                     callback.onFailure(ExecutorCallbackCall.this, new IOException("Canceled"));
                  } else {
                     callback.onResponse(ExecutorCallbackCall.this, response);
                  }
                });

所以总的来说,这其实是一个层层叠加的过程,Retrofit的线程切换原理本质上就是Handler消息机制;到这关于数据适配和线程切换的回答就告一段落了,有很多细节的东西没有提到,有时间的话,需要自己去补充,用一张草图来展示下Retrofit对它们进行的封装


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

面试官:“你知道什么情况下 HTTPS 不安全么”

在现代互联网的安全保障中,HTTPS 已经成为了一种标配,几乎所有的网站都会使用这种加密方式来保护用户的隐私和数据安全。但是,就算是 HTTPS,也并不是绝对安全的,存在一些情况下 HTTPS 可能会被攻击或者不安全。那么,这些情况具体是什么呢?下面我们就来一...
继续阅读 »

在现代互联网的安全保障中,HTTPS 已经成为了一种标配,几乎所有的网站都会使用这种加密方式来保护用户的隐私和数据安全。但是,就算是 HTTPS,也并不是绝对安全的,存在一些情况下 HTTPS 可能会被攻击或者不安全。那么,这些情况具体是什么呢?下面我们就来一一解析。

  1. 中间人攻击

中间人攻击是指攻击者通过某种手段,让用户和服务器之间的 HTTPS 连接被攻击者所掌握,从而窃取用户的信息或者篡改数据。这种攻击方式的实现原理是攻击者在用户和服务器之间插入一个自己的服务器,然后将用户的请求转发到真正的服务器上,同时将服务器返回的数据再转发给用户。在这个过程中,攻击者可以窃取用户的信息或者篡改数据,而用户和服务器之间的通信则被攻击者所掌握。

中间人攻击的防御方式主要是使用证书验证机制。在 HTTPS 连接建立之前,服务器会将自己的数字证书发送给客户端,客户端会验证证书的合法性。如果证书合法,则可以建立 HTTPS 连接;如果证书不合法,则会提示用户连接不安全。因此,在防范中间人攻击时,特别需要注意数字证书的合法性。

  1. SSL/TLS 协议漏洞

SSL/TLS 协议是 HTTPS 的核心协议,负责加密和解密数据。然而,SSL/TLS 协议本身也存在漏洞,攻击者可以通过这些漏洞来破解加密数据或者进行其他攻击。比如,2014 年发现的 Heartbleed 漏洞就是 SSL/TLS 协议中的一个严重漏洞,可以让攻击者窃取服务器内存中的数据。

为了防范 SSL/TLS 协议漏洞,需要及时更新 SSL/TLS 协议版本,并使用最新的加密算法。同时,也需要定期对 SSL/TLS 协议进行安全评估和漏洞扫描,以确保协议的安全性。

  1. SSL/TLS 证书被盗用

SSL/TLS 证书是 HTTPS 连接中保障安全性的重要组成部分,如果证书被盗用,则攻击者可以冒充合法网站进行欺骗和攻击。SSL/TLS 证书被盗用的方式有很多种,比如私钥泄露、数字证书机构被攻击等。

为了防范 SSL/TLS 证书被盗用,需要使用可靠的数字证书机构颁发证书,并定期更换证书。同时,也需要对私钥进行保护,并定期更换私钥。

  1. 安全策略不当

HTTPS 的安全性除了依赖协议和证书外,还和网站本身的安全策略有关。如果网站本身的安全策略不当,则可能会导致 HTTPS 连接不安全。比如,网站没有启用 HSTS(HTTP Strict Transport Security)机制,则可能被攻击者利用 SSLStrip 攻击进行欺骗。

为了防范安全策略不当导致 HTTPS 连接不安全,需要建立完善的安全策略体系,并对网站进行定期安全评估和漏洞扫描。

总结

虽然 HTTPS 在现代互联网中已经成为了一种标配,并且相对于 HTTP 来说具有更高的安全性,但是 HTTPS 也并不是绝对安全的。在使用 HTTPS 的过程中,需要注意中间人攻击、SSL/TLS 协议漏洞、SSL/TLS 证书被盗用以及安全策略不当等情况。只有建立完善的安全策略体系,并定期进行安全评估和漏洞扫描,才能确保 HTTPS 连接的安全性。


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

我给项目加了性能守卫插件,同事叫我晚上别睡的太死

web
点击在线阅读,体验更好链接现代JavaScript高级小册链接深入浅出Dart链接现代TypeScript高级小册链接 引言 给组内的项目都在CICD流程上更新上了性能守卫插件,效果也还不错,同事还疯狂夸奖我 接下里进入我们的此次的主题吧 由于我组主要是负...
继续阅读 »

点击在线阅读,体验更好链接
现代JavaScript高级小册链接
深入浅出Dart链接
现代TypeScript高级小册链接

引言


给组内的项目都在CICD流程上更新上了性能守卫插件,效果也还不错,同事还疯狂夸奖我


WX20230708-170807@2x.png


接下里进入我们的此次的主题吧



由于我组主要是负责的是H5移动端项目,老板比较关注性能方面的指标,比如首页打开速度,所以一般会关注FP,FCP等等指标,所以一般项目写完以后都会用lighthouse查看,或者接入性能监控系统采集指标.



WX20230708-141706@2x.png


但是会出现两个问题,如果采用第一种方式,使用lighthouse查看性能指标,这个得依赖开发自身的积极性,他要是开发完就Merge上线,你也不知道具体指标怎么样。如果采用第二种方式,那么同样是发布到线上才能查看。最好的方式就是能强制要求开发在还没发布的时候使用lighthouse查看一下,那么在什么阶段做这个策略呢。聪明的同学可能想到,能不能在CICD构建阶段加上策略。其实是可以的,谷歌也想到了这个场景,提供性能守卫这个lighthouse ci插件


性能守卫



性能守卫是一种系统或工具,用于监控和管理应用程序或系统的性能。它旨在确保应用程序在各种负载和使用情况下能够提供稳定和良好的性能。



Lighthouse是一个开源的自动化工具,提供了四种使用方式:




  • Chrome DevTools




  • Chrome插件




  • Node CLI




  • Node模块




image.png


其架构实现图是这样的,有兴趣的同学可以深入了解一下


这里我们我们借助Lighthouse Node模块继承到CICD流程中,这样我们就能在构建阶段知道我们的页面具体性能,如果指标不合格,那么就不给合并MR


剖析lighthouse-ci实现


lighthouse-ci实现机制很简单,核心实现步骤如上图,差异就是lighthouse-ci实现了自己的server端,保持导出的性能指标数据,由于公司一般对这类数据敏感,所以我们一般只需要导出对应的数据指标JSON,上传到我们自己的平台就行了。


image.png


接下里,我们就来看看lighthouse-ci实现步骤:





    1. 启动浏览器实例:CLI通过Puppeteer启动一个Chrome实例。


    const browser = await puppeteer.launch();




    1. 创建新的浏览器标签页:接着,CLI创建一个新的标签页(或称为"页面")。


    const page = await browser.newPage();




    1. 导航到目标URL:CLI命令浏览器加载指定的URL。


    await page.goto('https://example.com');




    1. 收集数据:在加载页面的同时,CLI使用各种Chrome提供的API收集数据,包括网络请求数据、JavaScript执行时间、页面渲染时间等。





    1. 运行审计:数据收集完成后,CLI将这些数据传递给Lighthouse核心,该核心运行一系列预定义的审计。





    1. 生成和返回报告:最后,审计结果被用来生成一个JSON或HTML格式的报告。


    const report = await lighthouse(url, opts, config).then(results => {
    return results.report;
    });




    1. 关闭浏览器实例:报告生成后,CLI关闭Chrome实例。


    await browser.close();



// 伪代码
const puppeteer = require('puppeteer');
const lighthouse = require('lighthouse');
const {URL} = require('url');

async function run() {
// 使用 puppeteer 连接到 Chrome 浏览器
const browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox'],
});

// 新建一个页面
const page = await browser.newPage();

// 在这里,你可以执行任何Puppeteer代码,例如:
// await page.goto('https://example.com');
// await page.click('button');

const url = 'https://example.com';

// 使用 Lighthouse 进行审查
const {lhr} = await lighthouse(url, {
port: new URL(browser.wsEndpoint()).port,
output: 'json',
logLevel: 'info',
});

console.log(`Lighthouse score: ${lhr.categories.performance.score * 100}`);

await browser.close();
}

run();

导出的HTML文件


image.png


导出的JSON数据


image.png


实现一个性能守卫插件


在实现一个性能守卫插件,我们需要考虑以下因数:





    1. 易用性和灵活性:插件应该易于配置和使用,以便它可以适应各种不同的CI/CD环境和应用场景。它也应该能够适应各种不同的性能指标和阈值。





    1. 稳定性和可靠性:插件需要可靠和稳定,因为它将影响整个构建流程。任何失败或错误都可能导致构建失败,所以需要有强大的错误处理和恢复能力。





    1. 性能:插件本身的性能也很重要,因为它将直接影响构建的速度和效率。它应该尽可能地快速和高效。





    1. 可维护性和扩展性:插件应该设计得易于维护和扩展,以便随着应用和需求的变化进行适当的修改和更新。





    1. 报告和通知:插件应该能够提供清晰和有用的报告,以便开发人员可以快速理解和处理任何性能问题。它也应该有一个通知系统,当性能指标低于预定阈值时,能够通知相关人员。





    1. 集成:插件应该能够轻松集成到现有的CI/CD流程中,同时还应该支持各种流行的CI/CD工具和平台。





    1. 安全性:如果插件需要访问或处理敏感数据,如用户凭证,那么必须考虑安全性。应使用最佳的安全实践来保护数据,如使用环境变量来存储敏感数据。




image.png


// 伪代码
//perfci插件
const puppeteer = require('puppeteer');
const lighthouse = require('lighthouse');
const { port } = new URL(browser.wsEndpoint());

async function runAudit(url) {
const browser = await puppeteer.launch();
const { lhr } = await lighthouse(url, {
port,
output: 'json',
logLevel: 'info',
});
await browser.close();

// 在这里定义你的性能预期
const performanceScore = lhr.categories.performance.score;
if (performanceScore < 0.9) { // 如果性能得分低于0.9,脚本将抛出错误
throw new Error(`Performance score of ${performanceScore} is below the threshold of 0.9`);
}
}

runAudit('https://example.com').catch(console.error);


使用


name: CI
on: [push]
jobs:
lighthouseci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16
- run: npm install && npm install -g @lhci/cli@0.11.x
- run: npm run build
- run: perfci autorun


性能审计


const lighthouse = require('lighthouse');
const puppeteer = require('puppeteer');
const nodemailer = require('nodemailer');

// 配置邮件发送器
const transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
user: 'your-email@gmail.com',
pass: 'your-password',
},
});

// 定义一个函数用于执行Lighthouse审计并处理结果
async function runAudit(url) {
// 通过Puppeteer启动Chrome
const browser = await puppeteer.launch({ headless: true });
const { port } = new URL(browser.wsEndpoint());

// 使用Lighthouse进行性能审计
const { lhr } = await lighthouse(url, { port });

// 检查性能得分是否低于阈值
if (lhr.categories.performance.score < 0.9) {
// 如果性能低于阈值,发送警告邮件
let mailOptions = {
from: 'your-email@gmail.com',
to: 'admin@example.com',
subject: '网站性能低于阈值',
text: `Lighthouse得分:${lhr.categories.performance.score}`,
};

transporter.sendMail(mailOptions, function(error, info){
if (error) {
console.log(error);
} else {
console.log('Email sent: ' + info.response);
}
});
}

await browser.close();
}

// 使用函数
runAudit('https://example.com');


接下来,我们分步骤大概介绍下几个核心实现


数据告警


// 伪代码
const lighthouse = require('lighthouse');
const puppeteer = require('puppeteer');
const nodemailer = require('nodemailer');

// 配置邮件发送器
const transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
user: 'your-email@gmail.com',
pass: 'your-password',
},
});

// 定义一个函数用于执行Lighthouse审计并处理结果
async function runAudit(url) {
// 通过Puppeteer启动Chrome
const browser = await puppeteer.launch({ headless: true });
const { port } = new URL(browser.wsEndpoint());

// 使用Lighthouse进行性能审计
const { lhr } = await lighthouse(url, { port });

// 检查性能得分是否低于阈值
if (lhr.categories.performance.score < 0.9) {
// 如果性能低于阈值,发送警告邮件
let mailOptions = {
from: 'your-email@gmail.com',
to: 'admin@example.com',
subject: '网站性能低于阈值',
text: `Lighthouse得分:${lhr.categories.performance.score}`,
};

transporter.sendMail(mailOptions, function(error, info){
if (error) {
console.log(error);
} else {
console.log('Email sent: ' + info.response);
}
});
}

await browser.close();
}

// 使用函数
runAudit('https://example.com');

处理设备、网络等不稳定情况


// 伪代码

// 网络抖动
const { lhr } = await lighthouse(url, {
port,
emulatedFormFactor: 'desktop',
throttling: {
rttMs: 150,
throughputKbps: 1638.4,
cpuSlowdownMultiplier: 4,
requestLatencyMs: 0,
downloadThroughputKbps: 0,
uploadThroughputKbps: 0,
},
});


// 设备
const { lhr } = await lighthouse(url, {
port,
emulatedFormFactor: 'desktop', // 这里可以设定为 'mobile' 或 'desktop'
});


用户登录态问题



也可以让后端同学专门提供一条内网访问的登录态接口环境,仅用于测试环境



const puppeteer = require('puppeteer');
const lighthouse = require('lighthouse');
const fs = require('fs');
const axios = require('axios');
const { promisify } = require('util');
const { port } = new URL(browser.wsEndpoint());

// promisify fs.writeFile for easier use
const writeFile = promisify(fs.writeFile);

async function runAudit(url, options = { port }) {
// 使用Puppeteer启动Chrome
const browser = await puppeteer.launch();
const page = await browser.newPage();

// 访问登录页面
await page.goto('https://example.com/login');

// 输入用户名和密码
await page.type('#username', 'example_username');
await page.type('#password', 'example_password');

// 提交登录表单
await Promise.all([
page.waitForNavigation(), // 等待页面跳转
page.click('#login-button'), // 点击登录按钮
]);

// 运行Lighthouse
const { lhr } = await lighthouse(url, options);

// 保存审计结果到JSON文件
const resultJson = JSON.stringify(lhr);
await writeFile('lighthouse.json', resultJson);

// 上传JSON文件到服务器
const formData = new FormData();
formData.append('file', fs.createReadStream('lighthouse.json'));

// 上传文件到你的服务器
const res = await axios.post('https://your-server.com/upload', formData, {
headers: formData.getHeaders()
});

console.log('File uploaded successfully');

await browser.close();
}

// 运行函数
runAudit('https://example.com');

总结


性能插件插件还有很多需要考虑的情况,所以,不懂还是来私信问我吧,我同事要请

作者:linwu
来源:juejin.cn/post/7253331974051823675
我吃饭去了,不写了。

收起阅读 »

村镇级别geojson获取方法

web
前言 公司需要开发某个村镇的网格地图,个大搜索引擎找了地图板块都只能到村镇级的,在高德地图上搜索出来只是一个标记并没有详细的网格分布,并使用BigMap等工具尝试也只能到村镇不能到具体下面的网格。下文将介绍一种思路用于获取村镇的geojson。 准备工作 ...
继续阅读 »

前言


公司需要开发某个村镇的网格地图,个大搜索引擎找了地图板块都只能到村镇级的,在高德地图上搜索出来只是一个标记并没有详细的网格分布,并使用BigMap等工具尝试也只能到村镇不能到具体下面的网格。下文将介绍一种思路用于获取村镇的geojson。
1.png


准备工作



  • 需要转换村镇的png/svg图

  • Vector Magin用于将png转换为svg工具

  • Svg2geojson工具,git地址:Svg2geojson

  • geojson添加属性工具:geojson.io(需要T子)

  • geojson压缩工具:mapshaper(需要T子)


整体思路


2.jpeg


PNG转SVG


导入png图片


3.png


配置Vector Magic参数


我这里是一直点击下一步直到出现转换界面,这里也可基于自己的图片配置参数。出现下面界面表示已经转换完成,这里选择Edit Result能够对转换完成的svg进行编辑
image.png


Vector Magic操作



  • Pan(A)移动画布

  • Zap(D)删除某块区域

  • Fill(F)对某块区域进行填充颜色

  • Pencil(X)使用画笔进行绘制

  • Color(C)吸取颜色


操作完成点击Update完成svg更新


保存为svg


image.png



如果有svg图片本步骤可以省略,另外如果是UI出的svg图片注意边与边不能重合,不能到时候只能识别为一块区域



SVG转换为GeoJson


安装工具


npm install svg2geojson


获取村镇经纬度边界


使用BigMap选择对应的村镇,获取边缘四个点的经纬度并记录
uTools_1689736099805.png


编辑svg加入边界经纬度


<MetaInfo xmlns="http://www.prognoz.ru">
<Geo>
<GeoItem
X="0" Y="0"
Latitude="最右边的lat" Longitude="最上边的lng"
/>
<GeoItem
X="1445" Y="1047"
Latitude="最左边的lat" Longitude="最下边的lng"
/>
</Geo>
</MetaInfo>

最终的svg文件如下
image.png


转换svg


svg2geojson canggou.svg


使用geojson.io添加对应的属性


image.png
右边粘贴转换出来的geojson,点击对应的区域即可添加属性


注意事项⚠️



  1. 转换出来的geojson可能复制到geojson.io不能使用,可以先放到mapshaper里面然后导出geojson再使用geojson.io使用。

  2. 部分区域粘连问题(本来是多个区域,编辑时却是同一个区域),需要使用Vector Magin重新编辑下生成出来的svg,注意边界。


最终效果


PS:具体使用geojson需要自己百度下,下面是最终呈现的效果,地图有点丑请忽略还未来得及优化
image.png

收起阅读 »

Java与Go到底差别在哪,谁要被时代抛弃?

在当今软件开发行业中,Java和Go是两个备受瞩目的编程语言。Java作为一门成熟的编程语言,已经被广泛应用于企业级应用开发、云计算、大数据处理等领域。而Go则是近年来崭露头角的新兴编程语言,以其高效、简洁的特性受到了越来越多开发者的青睐。那么,这两种编程语言...
继续阅读 »

在当今软件开发行业中,Java和Go是两个备受瞩目的编程语言。Java作为一门成熟的编程语言,已经被广泛应用于企业级应用开发、云计算、大数据处理等领域。而Go则是近年来崭露头角的新兴编程语言,以其高效、简洁的特性受到了越来越多开发者的青睐。那么,这两种编程语言到底有哪些不同之处呢?它们各自的优势和劣势是什么?又有哪些因素可能导致它们被时代抛弃呢?本文将从多个方面对Java和Go进行比较,为读者提供参考。


一、语法特性


Java是一门面向对象的编程语言,采用强类型、静态类型的语言特性。它的语法结构相对复杂,需要开发者花费较多的时间和精力去学习和掌握。而Go则是一门以并发编程为主要特点的编程语言,采用强类型、静态类型的语言特性。相较于Java,Go的语法更加简洁明了,容易上手。


二、并发编程


在当今互联网时代,应用程序的并发能力越来越受到重视。Java作为一门历史悠久的编程语言,在并发编程方面有着丰富的经验和成熟的技术栈。Java提供了多线程、线程池、锁等机制,可以很好地支持并发编程。而Go则是一门专门为并发编程而设计的编程语言,其内置了goroutine、channel等机制,可以轻松实现高效的并发编程。相比之下,Go在并发编程方面更加优秀。


三、性能表现


性能是衡量一门编程语言优劣的重要指标之一。Java作为一门成熟的编程语言,在性能方面表现出色。Java虚拟机(JVM)具有良好的优化机制,可以实现高效的垃圾回收和内存管理。而Go则是一门以高性能为目标而设计的编程语言,其在内存管理和垃圾回收方面也做出了很多优化。相比之下,Go在性能方面略胜一筹。


四、生态支持


生态支持是衡量一门编程语言是否成熟和受欢迎的重要指标之一。Java作为一门历史悠久的编程语言,在生态支持方面非常强大。Java拥有丰富的类库和框架,可以轻松实现各种功能需求。而Go则是一门相对年轻的编程语言,在生态支持方面还不如Java成熟。但随着Go的不断发展和壮大,相信它的生态支持也会越来越强大。


五、社区活跃度


社区活跃度也是衡量一门编程语言是否具有前途和生命力的重要指标之一。Java作为一门历史悠久的编程语言,在全球范围内有着庞大的开发者社区和广泛的应用场景。而Go则是一门新兴编程语言,在全球范围内的社区规模还不如Java成熟。但随着Go的不断壮大和应用场景的扩大,相信它的社区活跃度也会逐渐提升。


六、未来发展趋势


未来发展趋势也是考虑一门编程语言是否具有前途和生命力的重要因素之一。Java作为一门历史悠久的编程语言,在未来仍然会继续保持其广泛应用和强大生态支持的优势。而Go则是一门新兴编程语言,在未来也有着广阔的发展前景。随着互联网时代的不断深入和技术创新的不断涌现,相信Go在未来会越来越受到开发者们的青睐。


综上所述,Java和Go都是优秀的编程语言,各自具有其独特的优势和劣势。在选择使用哪种编程语言时,需要根据具体需求和场景来进行权衡和选择。无论是Java还是Go,只要我们能够深入学习和掌握它们,并将其运用到实际开发中,都能够取

作者:韩淼燃
来源:juejin.cn/post/7257410685118595128
得良好的效果和成果。

收起阅读 »

适合小公司的自动化部署脚本

背景(偷懒) 在小小的公司里面,挖呀挖呀挖。快挖不动了,一件事重复个5次,还在人肉手工,身体和心理就开始不舒服了,并且违背了个人的座右铭:“偷懒”是人类进步的第一推动力。 每次想要去测试环境验证个新功能,又或者被测试无情的催促着部署新版本后;都需要本地打那个2...
继续阅读 »

背景(偷懒)


在小小的公司里面,挖呀挖呀挖。快挖不动了,一件事重复个5次,还在人肉手工,身体和心理就开始不舒服了,并且违背了个人的座右铭:“偷懒”是人类进步的第一推动力


每次想要去测试环境验证个新功能,又或者被测试无情的催促着部署新版本后;都需要本地打那个200多M的jar包;以龟速般的每秒几十KB网络,通过ftp上传到服务器;用烂熟透的jps命令查找到进程,kill后,重启服务。


是的,我想偷懒,想从已陷入到手工部署的沼泽地里走出来。如何救赎?


自我救赎之路


我的诉求很简单,想要一款“一键CI/CD的工具”,然后可以继续偷懒。为了省事,我做了以下工作


找了一款停止服务的脚本,并做了小小的优化


首推 陈皮大哥的停服脚本(我在里面加了个sleep 5);脚本见下文。只需要修改 APP_MAINCLASS的变量“XXX-1.0.0.jar”替换为自己jar的名字即可,其它不用动


该脚本主要是通过jps + jar的名字获得进程号,进行kill。( 脚本很简单,注释也很详细,就不展开了,感兴趣可以阅读下,不到5分钟,写过代码的你能看懂的)


把以下脚本保存为stop.sh


#!/bin/bash
# 主类
APP_MAINCLASS="XXX-1.0.0.jar"
# 进程ID
psid=0
# 记录尝试次数
num=0
# 获取进程ID,如果进程不存在则返回0,
# 当然你也可以在启动进程的时候将进程ID写到一个文件中,
# 然后使用的使用读取这个文件即可获取到进程ID
getpid() {
javaps=`jps -l | grep $APP_MAINCLASS`
if [ -n "$javaps" ]; then
psid=`echo $javaps | awk '{print $1}'`
else
psid=0
fi
}
stop() {
getpid
num=`expr $num + 1`
if [ $psid -ne 0 ]; then
# 重试次数小于3次则继续尝试停止服务
if [ "$num" -le 3 ];then
echo "attempt to kill... num:$num"
kill $psid
sleep 5
else
# 重试次数大于3次,则强制停止
echo "force kill..."
kill -9 $psid
fi
# 检查上述命令执行是否成功
if [ $? -eq 0 ]; then
echo "Shutdown success..."
else
echo "Shutdown failed..."
fi
# 重新获取进程ID,如果还存在则重试停止
getpid
if [ $psid -ne 0 ]; then
echo "getpid... num:$psid"
stop
fi
else
echo "App is not running"
fi
}
stop

编写2行的shell 启动脚本


修改脚本中的XXX-1.0.0.jar为你自己的jar名称即可。保存脚本内容为start.sh。jvm参数可自行修改


basepath=$(cd `dirname $0`; pwd)
nohup java -server -Xmx2g -Xms2g -Xmn1024m -XX:PermSize=128m -Xss256k -XX:+DisableExplicitGC -XX:+UseParNewGC -XX:-UseAdaptiveSizePolicy -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled -XX:+UseCMSCompactAtFullCollection -XX:LargePageSizeInBytes=128m -XX:+UseFastAccessorMethods -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=70 -XX:+HeapDumpOnOutOfMemoryError -XX:+PrintGCDetails -Xloggc:logs/gc.log -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps -XX:HeapDumpPath=logs/dump.hprof -XX:ParallelGCThreads=4 -jar $basepath/XXX-1.0.0.jar &>nohup.log &

复用之前jenkins,自己写部署脚本


脚本一定要放到 Post Steps里


1689757456174.png


9行脚本,主要干了几件事:



  • 备份正在运行的jar包;(万一有啥情况,还可以快速回滚)

  • 把jenkins上打好的包,复制到目标服务上

  • 执行停服脚本

  • 执行启动服务脚本


脚本见下文:


ssh -Tq $IP << EOF 
source /etc/profile
#进入应用部署目录
cd /data/app/test
##备份时间戳
DATE=`date +%Y-%m-%d_%H-%M-%S`
#删除备份jar包
rm -rf /data/app/test/xxx-1.0.0.jar.bak*
#备份历史jar包
mv /data/app/test/xxx-1.0.0.jar /data/app/test/xxx-1.0.0.jar.bak$DATE
#从jenkins上拉取最新jar包
scp root@$jenkisIP:/data/jenkins/workspace/test/target/XXX-1.0.0.jar /data/app/test
# 执行停止应用脚本
sh /data/app/test/stop.sh
#执行重启脚本
sh /data/app/test/start.sh
exit
EOF

注:



  • $IP 是部署服务器ip,$jenkisIP 是jenkins所在的服务器ip。 在部署前请设置jenkins服务器和部署服务器之间ssh免密登录

  • /data/app/test 是部署jar包存放路径

  • stop.sh 是上文的停止脚本

  • start.sh 是上文的启动脚本


总结


如果不想把时间浪费在本地打包,忍受不了上传jar包的龟速网络,人肉停服和启动服务。请尝试下这款自动部署化脚本。小小的投入,带来大大的回报。


原创不易,请 点赞,留言,关注,转载 4暴击^^


参考资料:

xie.infoq.cn/article/52c…
Linux ----如何使用 kill 命令优雅停止 Java 服务

blog.csdn.net/m0_46897923…

作者:程序员猪佩琪
来源:juejin.cn/post/7257440759569055802
--ssh免密登录

收起阅读 »

来字节的第一年,我都做了些啥?有后悔过吗?

选择字节是一股脑的冲动。 和大多数热血沸腾的应届生一样,在众多offer中被字节高薪资、大平台和各种优厚福利所吸引,加上迫切摆脱学校桎梏到社会闯一闯的豪迈心态作祟。吭哧吭哧就背着行李箱赴京开始漫长的北漂生活。 各种不舒适 初到字节举手投足都是仓皇无措的。 时...
继续阅读 »

选择字节是一股脑的冲动。


和大多数热血沸腾的应届生一样,在众多offer中被字节高薪资、大平台和各种优厚福利所吸引,加上迫切摆脱学校桎梏到社会闯一闯的豪迈心态作祟。吭哧吭哧就背着行李箱赴京开始漫长的北漂生活。


在这里插入图片描述


各种不舒适


初到字节举手投足都是仓皇无措的。


时不时的拉群沟通让社交恐惧的我几近崩溃,短短两周后的技术分享让零基础的我一脸懵逼。启程的第一步还没有迈出去,就被大公司高速运转的节奏吓得连连败退。


字节跳动带给我的第一感受就是:各种不舒适。但我也清楚,这些所谓的不舒适,也只是长期处于安逸学生期留下的诟病,与其选择每天上班如同上刑场般痛不欲生,不妨换个心态,


把工作作为最好的练习场所


边学边干,在工作中抓住一切可以练习的机会, 哪怕是只是会议上的每一次主动发言,所解决的每一个小bug,都去用心做好,并仔细复盘。


我开始在拉群沟通前把每一次讨论的context理清,并把自己的疑问与希望了解到的点一一列出,并拟写草稿,会议后会去听自己发言录像,听听看自己的表达是否足够清晰,并整理出精简的语言框架,形成一套属于自己的高效沟通方式;我开始在日常工作结束后熬到深夜阅读工程源码,整理学习脑图,沉淀技术文档,保持技术高强度输入……


在这里插入图片描述

(左图是整理的学习笔记,右图是技术分享时写的文档,写了整整1w+字,比写论文还刻苦)


两周后,我圆满完成了组内技术分享,有条有理地回答了同事们提出的问题。当我走出会议室后,一股暖流充满了整个胸腔,眼眶也略带酸涩。


那一刻我是多么自豪。


开始去思考自己的角色…


经历初次考验,我开始被安排做组内一些项目。刚开始做的都是边边角角的事情,基本哪里有砖哪里搬。至于搬这块砖的意义是什么,为什么要去做这件事,我却从来没有想过。


“只见冰川一角,不见冰川全貌”成为我入职初期一段时间以来保持的状态。不过这种状态没有持续太久,我mentor就跟我说,


“要多去思考事情背后的本质是什么,并且要去主动成为一件事的Owner。”


Owner,这是我第一次听说这个词,就像一个衡量的标尺,硬生生地扎在心底。于是,我开始思考,怎么样才算得上一个Owner?老实说,做一个默默搬砖的码农未尝不可,按部就班完成任务,亦步亦趋跟着团队节奏前进。然而比起这样的角色,我更倾向于去推动,甚至去发起一件事。多一份全身心的投入与责任感。


在做完事情的时候,再多想一些,做完就可以了吗?我还能做些什么来给团队带来一些帮助呢?


这些问题刚开始可能很难得出答案,但是我可以去模仿和请教,因为在字节最不缺的就是人才。这种思考事情的方式也给了我一些启发,当你不知道自己应该做到什么程度或者不知道自己还有哪些提升空间的时候,就去看那些比你更厉害的人,观察别人怎么做事,倾听别人的观点,多给自己一些外部视角的比较与启发,就能够弥补自己思维上的不足并慢慢拥有自己的一套方法论。


成为一名Owner


重新定位自己角色后,我开始主动去留意团队内的声音。因为我所在的部门是视频通话业务,常常注意到研发同学在查bug时会去抱怨在一次通话结束后,通话过程中所有信息就丢失了,对于一些偶现的问题,很难去复现错误的现场。于是我开始思考,我是不是能够去开发一个小助手,能够做到将通话中产生的即时信息沉淀下来,就像打印出一张快照,使研发通过这张快照能够清晰还原现场所有数据和信息。


说干就干,结果真的捣鼓出了一个通话小助手,并主动组织了一个技术分享会介绍它的使用方式和技术方案。


分享会结束后,mentor对小助手的思想和前景颇为赞许,给我提了很多宝贵建议和持续迭代的方向;并且夸我这件事做的不错,鼓励我去cover更多的事情。


“你现在就是小助手的Owner啦” ,mentor笑着拍了拍我的肩膀。


相隔于第一次听说这个词,我现在已经慢慢理解了Owner这个词背后所包含的分量。我也开始知道,一件事情能做成什么样,并不是一个固定值,别人对你的预期也并不是一开始就设定好的,你的身份决定了预期的下限,但天花板是你自己去争取。因为工作/职场并不是做客观题,总会有标准答案,而更像是在去创作,可能性是由你去探索的,你愿意花更多的心思去雕刻去打磨,那你的作品会越来越出色;如果你甘愿写平淡乏味的文字,那这个作品也仅仅只能达到完成的程度而已。


最后回答一下,有后悔过这个决定吗?


没有。相反,来字节可能是我目前二十多年来做出过最正确的选择之一。


字节实在是一个可以快速成长的绝佳平台,扁平化管理和数不清的机会让作为实习生的我实现了“做一番大事”的梦想,身边无数优秀的同事让刚步入职场的我不断向前不断拔高。我发自内心热爱并全身心投入自己想要做的事,并在一件件事中塑造自己。


这篇文章,其实只是我入职一年来的思考和探索。我并不是什么职场达人,技术高管,能够给年少有为的大家指明正道之光;只能将刚刚步入职场从懵懂到适应到热爱的自己真实地展示给大家,文章里可能还有一些不成熟的观点,也请大家包涵与指正。


当然,如果这篇文章能够给刚入职或即将入职的你带来一些共鸣或帮助,那更是再好不过了。




最后,我想在文末和各位刚刚入职的小白们说一句,


可能初入社会会让你们感受到茫然与无助,因为小学到高校的教育是系统性地输入一些知识给我们,会有老师带着我们去吃透课本,吸收知识,即使你不想学习,也会有考试的压力倒逼你去学习。但离开校园这边土壤,并没有人规定了一个人的成长路径是如何,没有人给你方向,也没有人规定你成长的方向。读书、沟通、工作等等,都是你成长的机会;当然,你什么都不做也没关系。


因为成长并不是别人给予自己的任务,而是自发性的行为。


我们在每一天的得失中不断塑造自己,可能是最初理想中光鲜亮丽的自己,也可能是被现实打磨摧残不堪的自己。


时间和未来会告诉你,你现在的拼搏和努力值不值得。


感谢字节跳动,感谢在这里遇到的所有人,也感谢屏幕后的你认真看完这篇文章,希望我们能在下一个高处相遇。加油。


在这里插入图片描述

收起阅读 »

我终于成功登上了JS 框架榜单,并且仅落后于 React 4 名!

web
前言 如期而至,我独立开发的 JavaScript 框架 Strve.js 迎来了一个大版本5.6.2。此次版本距离上次大版本发布已经接近半年之多,为什么这么长时间没有发布新的大版本呢?主要是研究 Strve.js 如何支持单文件组件,使代码智能提示、代码格式...
继续阅读 »

前言


如期而至,我独立开发的 JavaScript 框架 Strve.js 迎来了一个大版本5.6.2。此次版本距离上次大版本发布已经接近半年之多,为什么这么长时间没有发布新的大版本呢?主要是研究 Strve.js 如何支持单文件组件,使代码智能提示、代码格式化方面更加友好。之前也发布了 Strve SFC,但是由于其语法规则的繁琐以及是在运行时编译的种种原因,我果断放弃了这个方案的继续研究。而这次的版本5.6.2成功解决了代码智能提示、代码格式化方面友好的问题,另外还增加了很多锦上添花的特性,这些都归功于我们这次版本成功支持JSX语法。熟悉React的朋友知道,JSX语法非常灵活。 而 Strve.js 一大特性也就是灵活操作代码块,这里的代码块我们可以理解成函数,而JSX语法在一定场景下也恰恰满足了我们这种需求。


那么,我们如何在 Strve 项目中使用JSX语法呢?我们在Strve项目构建工具 CreateStrveApp 预置了模版,你可以选择 strve-jsx 或者 strve-jsx-apps 模版即可。我们使用 CreateStrveApp 搭建完 Strve 项目会发现,同时安装了babelPluginStrvebabelPluginJsxToStrve,这是因为我们需要使用 babelPluginJsxToStrve 将 JSX 转换为标签模版,之后再使用babelPluginStrve 将标签模版转换为 Virtual DOM,进而实现差异化更新视图。


尝试


我既然发布出了一个大版本,并且个人还算比较满意。那么下一步我如何推广它呢?毕竟毛遂自荐有时候还是非常有意义的。所以,我打算通过js-framework-benchmark 这个项目评估下性能。


js-framework-benchmark 是什么?我们这里就简单介绍下 js-framework-benchmark,它是一个用于比较 JavaScript 框架性能的项目。它旨在通过执行一系列基准测试来评估不同框架在各种场景下的性能表现。这些基准测试包括渲染大量数据、更新数据、处理复杂的 UI 组件等。通过运行这些基准测试,可以比较不同框架在各种方面的性能优劣,并帮助开发人员选择最适合其需求的框架。js-framework-benchmark 项目提供了一个包含多个流行 JavaScript 框架的基准测试套件。这些框架包括 Angular、React、Vue.js、Ember.js 等。每个框架都会在相同的测试场景下运行,然后记录下执行时间和内存使用情况等性能指标。通过比较这些指标,可以得出不同框架的性能差异。这个项目的目标是帮助开发人员了解不同 JavaScript 框架的性能特点,以便在选择框架时能够做出更加明智的决策。同时,它也可以促进框架开发者之间的竞争,推动框架的不断改进和优化。


那么,我们就抱着试试的心态去运行下这个项目。


测试


我们进入js-framework-benchmark Github主页,然后 clone 下这个项目。


git clone https://github.com/krausest/js-framework-benchmark.git

然后,我们 clone 到本地之后,打开 README.md 文件找到如何评估框架。大体浏览之后,我们得出的结论是:通过使用自己的框架完成js-framework-benchmark规定的练习项目。


01.png


那么,我们就照着其他框架已经开发完成的示例进行开发吧!在开发之前,我们必须要了解js-framework-benchmark 中有两种模式。一种是keyed,另一种是non-keyed。在 js-framework-benchmark 中,"keyed" 模式是指通过给数据项分配一个唯一标识符作为 "key" 属性,从而实现数据项与 DOM 节点之间的一对一关系。当数据发生变化时,与之相关联的 DOM 节点也会相应更新。而 "non-keyed" 模式是指当数据项发生变化时,可能会修改之前与其他数据项关联的 DOM 节点。因为 Strve 暂时没有类似唯一标识符这种特性,所以我们选择non-keyed模式。


我们打开项目下/frameworks/non-keyed文件夹,找一个案例框架看一下它们开发的项目,我们选择 Vue 吧!
我们根据它开发的样例迁移到自己的框架中去。为了测试新版本,我们将使用JSX语法进行开发。


import { createApp, setData } from "strve-js";
import { buildData } from "./data.js";

let selected = undefined;
let rows = [];

function setRows(update = rows.slice()) {
setData(
() => {
rows = update;
},
{
name: TbodyComponent,
}
);
}

function add() {
const data = rows.concat(buildData(1000));
setData(
() => {
rows = data;
},
{
name: TbodyComponent,
}
);
}

function remove(id) {
rows.splice(
rows.findIndex((d) => d.id === id),
1
);
setRows();
}

function select(id) {
setData(
() => {
selected = +id;
},
{
name: TbodyComponent,
}
);
}

function run() {
setRows(buildData());
selected = undefined;
}

function update() {
for (let i = 0; i < rows.length; i += 10) {
rows[i].label += " !!!";
}
setRows();
}

function runLots() {
setRows(buildData(10000));
selected = undefined;
}

function clear() {
setRows([]);
selected = undefined;
}

function swapRows() {
if (rows.length > 998) {
const d1 = rows[1];
const d998 = rows[998];
rows[1] = d998;
rows[998] = d1;
setRows();
}
}

function TbodyComponent() {
return (
<tbody $key>
{rows.map((item) => (
<tr
class={item.id === selected ? "danger" : ""}
data-label={item.label}
$key
>

<td class="col-md-1" $key>
{item.id}
</td>
<td class="col-md-4">
<a onClick={() => select(item.id)} $key>
{item.label}
</a>
</td>
<td class="col-md-1">
<a onClick={() => remove(item.id)} $key>
<span
class="glyphicon glyphicon-remove"
aria-hidden="true"
>
</span>
</a>
</td>
<td class="col-md-6"></td>
</tr>
))}
</tbody>

);
}

function MainBody() {
return (
<>
<div class="jumbotron">
<div class="row">
<div class="col-md-6">
<h1>Strve-non-keyed</h1>
</div>
<div class="col-md-6">
<div class="row">
<div class="col-sm-6 smallpad">
<button
type="button"
class="btn btn-primary btn-block"
id="run"
onClick={run}
>

Create 1,000 rows
</button>
</div>
<div class="col-sm-6 smallpad">
<button
type="button"
class="btn btn-primary btn-block"
id="runlots"
onClick={runLots}
>

Create 10,000 rows
</button>
</div>
<div class="col-sm-6 smallpad">
<button
type="button"
class="btn btn-primary btn-block"
id="add"
onClick={add}
>

Append 1,000 rows
</button>
</div>
<div class="col-sm-6 smallpad">
<button
type="button"
class="btn btn-primary btn-block"
id="update"
onClick={update}
>

Update every 10th row
</button>
</div>
<div class="col-sm-6 smallpad">
<button
type="button"
class="btn btn-primary btn-block"
id="clear"
onClick={clear}
>

Clear
</button>
</div>
<div class="col-sm-6 smallpad">
<button
type="button"
class="btn btn-primary btn-block"
id="swaprows"
onClick={swapRows}
>

Swap Rows
</button>
</div>
</div>
</div>
</div>
</div>
<table class="table table-hover table-striped test-data">
<component $name={TbodyComponent.name}>{TbodyComponent()}</component>
</table>
<span
class="preloadicon glyphicon glyphicon-remove"
aria-hidden="true"
>
</span>
</>

);
}

createApp(() => MainBody()).mount("#main");


其实,我们虽然使用了JSX语法,但是你会发现有很多特性并不与JSX语法真正相同,比如我们可以直接使用 class 去表示样式类名属性,而不能使用 className 表示。


评估案例项目开发完成了,我们下一步就要测试一下项目是否符合评估标准。


npm run bench non-keyed/strve

02.gif


测试标准包括:




  • create rows:创建行,页面加载后创建 1000 行的持续时间(无预热)




  • replace all rows:替换所有行,替换表中所有 1000 行所需的时间(5 次预热循环)。该指标最大的价值就是了解当页面上的大部分内容发生变化时库的执行方式。




  • partial update:部分更新,对于具有 10000 行的表,每 10 行更新一次文本(进行 5 次预热循环)。该指标是动画性能和深层嵌套数据结构开销等方面的最佳指标。




  • select row:选择行,在单击行时高亮显示该行所需的时间(进行 5 次预热循环)。




  • swap rows:交换行,在包含 1000 行的表中交换 2 行的时间(进行 5 次预热迭代)。




  • remove row:删除行,在包含 1,000 行的表格上移除一行所需的时间(有 5 次预热迭代),该指标可能变化最少,因为它比库的任何开销更多地测试浏览器布局变化(因为所有行向上移动)。




  • create many rows:创建多行,创建 10000 行所需的时间(没有预热),该指标更容易受到内存开销的影响,并且对于效率较低的库来说,扩展性会更差。




  • append rows to large table:追加行到大型表格,在包含 10000 行的表格上添加 1000 行所需的时间(没有预热)。




  • clear rows:清空行,清空包含 10000 行的表格所需的时间(没有预热),该指标说明了库清理代码的成本,内存使用对这个指标的影响很大,因为浏览器需要更多的 GC。




最终,Strve 顶住了压力,通过了测试。


03.gif


看到了successful run之后,觉得特别开心!那种成就感是任何事物都难以代替的。


跑分


我们既然通过了测试,那么下一步我们将与前端两大框架Vue、React进行比较跑分,我们先在我自己本地环境上跑一下,看一下效果。


性能测试基准分为三类:



  • 持续时间

  • 启动指标

  • 内存分配


持续时间


04.png


启动指标


05.png


内存分配


06.png


总体而言,我感觉还不错,毕竟跟两个大哥在比较。到这里我还是觉得不够,跟其他框架比比呢!


提交


只要框架通过了测试,并且按照提交PR的规定提交,是可以被选录到 js-framework-benchmark 中去的。


好,那我们就去试试!


07.png


又一个比较有成就感的事!提交的PR被作者合并了!


成绩单


我迫不及待的去榜单上看下我的排名,会不会垫底啊!


因为浏览器版本发布的时差问题,暂时 Official results ( 官方结果 ) 还没有发布最新结果,我们可以先来 Snapshot of the results ( 快照结果 ) 中查看。


我们打开下方网址就可以看到JS框架的最新榜单了。


https://krausest.github.io/js-framework-benchmark/current.html

我们在持续时间这个类别下从后往前找,目前63个框架我居然排名 50 名,并且大名鼎鼎的 React 排名45名。


08.png


我们先不激动,我们再看下启动指标类别。Strve 平均分数是1.04,我看了看好几个框架分数是1.04。Strve 可以排到前8名。


09.png


我们再稳一下,继续看内存分配这个类别。Strve 平均分数是1.40,Strve 可以排到前12名。


10.png


意义


js-framework-benchmark 的测试结果是相对准确的,因为它是针对同样的测试样本和基准测试情境进行比较,可以提供框架之间的相对性能比较。然而,需要注意的是,这个测试结果也只是反映了测试条件下的性能表现。框架实际的性能可能还会受到很多方面的影响。
此外,js-framework-benchmark 测试结果也不应该成为选择框架的唯一指标。在选择框架时,还需要考虑框架的生态、开发效率、易用性等多方面因素,而不仅仅是性能表现。


虽然,Strve 跟 React 比较是有点招黑,但是不妨这样想,榜样的力量是巨大的!只有站在巨人的肩膀上才能望得更远!


Strve 要走的路还有很长,入选JS框架榜单使我更加明确了方向。我觉得做自己喜欢做得事情,这样才会有意义!


加油


Strve 要继续维护下去,我也会不断学习,继续精进。



Strve 源码仓库:github.com/maomincodin…




Strve 中文文档:maomincoding.gitee.io/strve-doc-z…



谢谢大家的阅读!如果大家觉得Strve不错,麻烦帮我点下Star吧!


作者:前端历劫之路
来源:juejin.cn/post/7256250499280158776
收起阅读 »

一个小公司的技术开发心酸事

背景 长话短说,就是在2022年6月的时候加入了一家很小创业公司。老板不太懂技术,也不太懂管理,靠着一腔热血加上对实体运输行业的了解,加上盲目的自信,贸然开始创业,后期经营困难,最终散伙。 自己当时也是不察,贸然加入,后边公司经营困难,连最后几个月的工资都没给...
继续阅读 »

背景


长话短说,就是在2022年6月的时候加入了一家很小创业公司。老板不太懂技术,也不太懂管理,靠着一腔热血加上对实体运输行业的了解,加上盲目的自信,贸然开始创业,后期经营困难,最终散伙。


自己当时也是不察,贸然加入,后边公司经营困难,连最后几个月的工资都没给发。


当时老板的要求就是尽力降低人力成本,尽快的开发出来App(Android+IOS),老板需要尽快的运营起来。


初期的技术选型


当时就自己加上一个刚毕业的纯前端开发以及一个前面招聘的ui,连个人事、测试都没有。


结合公司的需求与自己的技术经验(主要是前端和nodejs的经验),选择使用如下的方案:



  1. 使用uni-app进行App的开发,兼容多端,也可以为以后开发小程序什么的做方案预留,主要考虑到的点是比较快,先要解决有和无的问题;

  2. 使用egg.js + MySQL来开发后端,开发速度会快一点,行业比较小众,不太可能会遇到一些较大的性能问题,暂时看也是够用了的,后期过渡到midway.js也方便;

  3. 使用antd-vue开发运营后台,主要考虑到与uni-app技术栈的统一,节省转换成本;


也就是初期选择使用egg.js + MySQL + uni-app + antd-vue,来开发两个App和一个运营后台,快速解决0到1的问题。


关于App开发技术方案的选择


App的开发方案有很多,比如纯原生、flutter、uniapp、react-native/taro等,这里就当是的情况做一下选择。



  1. IOS与Android纯原生开发方案,需要新招人,两端同时开发,两端分别测试,这个资金及时间成本老板是不能接受的;

  2. flutter,这个要么自己从头开始学习,要么招人,相对于纯原生的方案好一点,但是也不是最好的选择;

  3. react-native/taro与uni-app是比较类似的选择,不过考虑到熟练程度、难易程度以及开发效率,最终还是选择了uni-app。


为什么选择egg.js做后端


很多时候方案的选择并不能只从技术方面考虑,当是只能选择成本最低的,当时的情况是egg.js完全能满足。



  1. 使用一些成熟的后端开发方案,如Java、、php、go之类的应该是比较好的技术方案,但对于老板来说不是好的经济方案;

  2. egg.js开发比较简单、快捷,个人也比较熟悉,对于新成员的学习成本也很低,对于JS有一定水平的也能很快掌握egg.js后端的开发


中间的各种折腾


前期开发还算顺利,在规定的时间内,完成了开发、测试、上线。但是,老板并没有如前面说的,很快运营,很快就盈利,运营的开展非常缓慢。中间还经历了各种折腾的事情。



  1. 老板运营遇到困难,就到处找一些专家(基本跟我们这事情没半毛钱关系的专家),不断的提一些业务和ui上的意见,不断的修改;

  2. 期间新来的产品还要全部推翻原有设计,重新开发;

  3. 还有个兼职的领导非要说要招聘原生开发和Java开发重新进行开发,问为什么,也说不出什么所以然,也是道听途说。


反正就是不断提出要修改产品、设计、和代码。中间经过不断的讨论,摆出自己的意见,好在最终技术方案没修改,前期的工作成果还在。后边加了一些新的需求:系统升级1.1、ui升级2.0、开发小程序版本、开发新的配套系统(小程序版本)以及开发相关的后台、添加即时通信服务、以及各种小的功能开发与升级;


中间老板要加快进度了就让招人,然后又无缘无故的要开人,就让人很无奈。最大的运营问题,始终没什么进展,明显的问题并不在产品这块,但是在这里不断的折腾这群开发,也真是难受。


明明你已经很努力的协调各种事情、站在公司的角度考虑、努力写代码,却仍然无济于事。


后期技术方案的调整



  1. 后期调整了App的打包方案;

  2. 在新的配套系统中,使用midway.js来开发新的业务,这都是基于前面的egg.js的团队掌握程度,为了后续的开发规范,做此升级;

  3. 内网管理公用npm包,开发业务组件库;

  4. 规范代码、规范开发流程;


人员招聘,团队的管理


人员招聘


如下是对于当时的人员招聘的一些感受:



  1. 小公司的人员招聘是相对比较难的,特别是还给不了多少钱的;

  2. 好在我们选择的技术方案,只要对于JS掌握的比较好就可以了,前后端都要开发一点,也方便人员工作调整,避免开发资源的浪费。


团队管理


对于小团队的管理的一些个人理解:



  1. 小公司刚起步,就应该实事求是,以业务为导向;

  2. 小公司最好采取全栈的开发方式,避免任务的不协调,造成开发资源的浪费;

  3. 设置推荐的代码规范,参照大家日常的代码习惯来制定,目标就是让大家的代码相对规范;

  4. 要求按照规范的流程设计与开发、避免一些流程的问题造成管理的混乱和公司的损失;

    1. 如按照常规的业务开发流程,产品评估 => 任务分配 => 技术评估 => 开发 => 测试 => cr => 上线 => 线上问题跟踪处理;



  5. 行之有效可量化的考核规范,如开发任务的截止日期完成、核心流程开发文档的书写、是否有线上bug、严谨手动修改数据库等;

  6. 鼓励分享,相互学习,一段工作经历总要有所提升,有所收获才是有意义的;

  7. 及时沟通反馈、团队成员的个人想法、掌握开发进度、工作难点等;


最后总结及选择创业公司避坑建议!important



  1. 选择创业公司,一定要确认老板是一个靠谱的人,别是一个总是画饼的油腻老司机,或者一个优柔寡断,没有主见的人,这样的情况下,大概率事情是干不成的;

    1. 老板靠谱,即使当前的项目搞不成,也可能未来在别的地方做出一番事情;



  2. 初了上边这个,最核心的就是,怎么样赚钱,现在这种融资环境,如果自己不能赚钱,大概率是活不下去的@自己;

  3. 抓住核心矛盾,解决主要问题,业务永远是最重要的。至于说选择的开发技术、代码规范等等这些都可以往后放;

  4. 对上要及时反馈自己的工作进度,保持好沟通,老板总是站在更高一层考虑问题,肯定会有一些不一样的想法,别总自以为什么什么的;

  5. 每段经历最好都能有所收获,人生的每一步都有意义。


以上只是个人见解,请指教

作者:qiuwww
来源:juejin.cn/post/7257085326471512119

收起阅读 »

在创业公司做前端一年,这些经验到底值不值?

之前公司调整洗牌,裁掉了一大波人,像我这样做了快一年的,也竟是研发老员工了...最近领导安排我开始面试,拿到第一份简历是一位10年经验的前端大佬,看完简历后突然蒙圈,我该问什么问题,用过什么框架? 项目遇到过什么问题?困难是怎么解决的?webpack做过哪些性...
继续阅读 »

之前公司调整洗牌,裁掉了一大波人,像我这样做了快一年的,也竟是研发老员工了...最近领导安排我开始面试,拿到第一份简历是一位10年经验的前端大佬,看完简历后突然蒙圈,我该问什么问题,用过什么框架? 项目遇到过什么问题?困难是怎么解决的?webpack做过哪些性能优化?



诶,想到这,突然觉得哪天我也去投递简历了,很有可能会同样被问到这些问题,看看自己在现在公司干了快一年了,似乎也没有做过太多总结,瞬间感觉不寒而栗。



既然这些都迟早是要总结的,今天那就来回顾一下, 创业公司工作一年, 到底收获了什么?同时也希望我的经历能给大家带来一丝启发。


后台管理web阶段


坐标一线城市北京,前面刚来两三个月一直在做公司后台系统,使用技术栈主要是vue2+elementUI,因为刚来,很多业务逻辑要熟悉,没有时间去做优化,而且入职第三天就开始写代码了。


Tailwind


为了提高写代码的效率,正好当时看到tailwind,觉得很新奇,就给引入到项目里来了。可以用类名来直接书写css样式:



tailwind可以节省很多编写类名的脑力, 同时开发的时候不再需要在style和template来回切换,直接在类名里写属性,用起来是真香!而且据官网介绍,全部使用tailwind编码之后,css的文件最后打包编译出来,基本都小于10KB?!因为引入了elementUI组件库,也没法验证,但节省心智是实实在在的。



Drone CI


CI工具,配置之后本地npm run build后会自动提交到drone,它会帮忙完成测试,代码缓存,cdn刷新等操作,并且可以设置在自动化部署完成后,出现飞书提醒,整体界面看上去也比较舒服



Sentry


线上日志监控,用户在使用过程中产生了报错,sentry会实时发送提醒,咱们就可以通过日志回放可以分析出错原因,相当于飞机的黑盒子, 甚至可以回放用户的操作过程,挺强大的。


官网服务端渲染


第三个月的时候我们部门接下了公司官网的活,但开发周期只有半个月,而且当时都没经验,领导为了保险起见,就直接让把vue2的后台项目让我们拷了一份,所以官网做的没有什么新花样。其实现在想想,更好的办法应该是使用nuxt.js服务端渲染首屏加载快,还方便做SEO,奈何实在太菜~


开放图谱协议


开放图谱协议,全称叫Open Graph Protocol,可以让分享的链接在社交媒体上以图文的形式展示出来,比如没有填写就是下面这个样子:



填过之后就是这样子:



填写过开放图谱协议的话,将网站分享在社交媒体上,链接的内容更生动,算是一个小优化点。


Vue3


做完官网之后,公司又有一个后台系统,这次需要从0到1搭建,我果断申请了使用Vue3。只要效果能实现,老板不会在意用什么方法,那咱就去尝试尝试新东西,毕竟也是提升公司的技术储备~



这个项目说是从0到1,但实际开发为了追求效率,避免踩坑,还是让调研了市面上主流的前端集成框架,这次使用的是一个基于Vue3-element-admin的框架。


本来想使用ts进行开发,但考虑到我们团队成员ts都很弱,最后还是放弃了避免踩坑,主要技术栈是Vue3 + Vite2 + Vue Router + Pinia + Element Plus,还有Volar插件代替了原来Vue2的Vetur。


说下vue3的使用感受吧~



  1. vite启动速度极快,启动项目只需要3秒,vue2的项目怎么也得十几秒吧,热更新也极快,开发体验好

  2. 另外一个是vue3的setup语法糖,可以少写很多没有用的重复代码,比如让组件自动注册,属性及方法无需return等等,好用!节省心智!

  3. Volar插件配合vscode保存代码卡顿,不知道是配置问题还是做的优化不太好,没有之前vetur舒服,查阅资料发现很多人都有这问题,可能是保存代码时和eslint有冲突。


H5阶段


做完Vue3的项目后,也差不多小半年过年了,回来之后公司做了人员调整,我被调到公司自研App部门,开始做H5。相对来说我们公司H5的内容就很核心了,而且因为toC,产品对于细节的要求更高,甚至有一个排期就是专门给技术去做优化的。



webpack打包、热更新优化


刚接手H5的项目就遇到一个头疼的问题,项目文件很大,每次编写代码保存,热更新时间能有8、90秒,热更新之后如果在手机上运行,H5还需要进行打包发版,也就是将打包的文件更新至cdn,打包发版更夸张,打包有时候能花费十几分钟,再加上上传cdn,更新一段代码要等小20分钟!



毋庸置疑,接手第一件事就是一通抄作业...不,是做webpack的优化。



  1. cache-loader, 在rules中给加载速度久的文件,如js文件加上cache-loader之后,可以让打包速度有所提升,缩减到5分钟,但还是不够快,而且这个loader本身开启也需要花费时间。

  2. 持久化缓存,继续寻找 webpack打包缓存的问题,再一通抄作业后,发现加入持久化缓存的配置,能达到比较好的效果,打包明显加快! 如果不更改webpack配置,第二次打包只有10秒, 是10min变从10秒,就是这么强大!而且热更新也会加快!配置贴出来,webpack中cache的type类型换成filesystem,并指定路径即可。



  3. 发版上传优化,也就是发布文件到cdn,这块做了两个优化,一是静态文件抽离,资源较大且更改频率低的文件,如assets下的图片,动画等,单纯拆出来写脚本上传,那么每次打包上传只需要更新变化较多的js和css文件,二是开发环境更换国内云服务器存储桶(这个因“司”而异,因为我们公司业务是海外),也可以加速上传。

  4. externals,将需要引用的比较大的第三方库抽离,不要直接打进包里,加快首屏加载速度,等到需要的时候再去请求。


算法状态机


这是使用动画时需要用到的一种解决方案,后端返回的动画数据,前端会用它处理成多个帧,每一帧包含了一段动画、语音和字幕,将这些帧按照顺序播放,就变成了一个动画。是从算法迁移过来的项目,逻辑会比较复杂,从名字也能看得出来。


Fetch流式输出


使用fetch请求返回二进制流的形式,通过TextDecoder解码,逐渐push到展示的数组中,实现逐步渲染文本内容,类似gpt实时渲染。


stripe第三方支付平台


stripe是方便海外用户的第三方支付平台,类似于国内的收钱吧这种?功能很强大,可以看看我的另一篇文章,使用Stripe做类似于gpt的支付跳转(checkout方式)。后面还会出一篇,自定义搭建stripe的完整流程(elements方式)。


微前端框架--qiankun


有一个需求是,公司里的项目框架各不相同,有的是vue2,vue3,react,还有jquery,如果要在一个项目里把这些项目的功能都实现,重写一遍代码显然效率太低,这个时候就需要一个解决方案,也就是微前端,能融合不同框架项目,通过路由的切换显示,这里我们采用了qiankun,并且恰好我负责qiankun的基座搭建。这是qiankun官网。我参阅了这两篇文章,qiankun搭建, qiankun保姆级攻略,以及一个很棒github的qiankun例子,github.com/fengxianqi/


总结


到这就是我这一年的工作中遇到所有技术经验啦!不知相比大厂的经历算是怎样,了解的小伙伴可以评论下,分享出来也是期望我的经历能给大家带来一些启发,同时能知道自己的局限,欢迎多多指导和交流~


后面会根据情况继续完善,逐渐更新技术帖,还会分享在工作中心态上的成长变化,毕竟有人的地方就有江湖~

作者:大白萝卜
来源:juejin.cn/post/7257076605570646076
码字不易,望多点赞!

收起阅读 »

Android | View.post 到底是在什么时候执行的?

前言相信绝大部分人都使用过 view.post这个方法,且使用场景基本上都是用来获取 view 的一些属性数据,并且我们也都知道,该方法会使用 handler 发送一个消息,并且该消息被回调执行的时候 ...
继续阅读 »

前言

相信绝大部分人都使用过 view.post这个方法,且使用场景基本上都是用来获取 view 的一些属性数据,并且我们也都知道,该方法会使用 handler 发送一个消息,并且该消息被回调执行的时候 view 是已经绘制完成的,今天我们来聊一聊它内部的一些细节。

View.post

public boolean post(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
return attachInfo.mHandler.post(action);
}
getRunQueue().post(action);
return true;
}

代码看起来非常清楚明了,主要可以分为两部分

  • 如果 attachInfo 不为 null ,则直接获取它的 handler 将 action 发送出去
  • 否则就调用 getRunQueue.post ,并传入 action,看名字好像是一个可运行的队列

下面我们来分别看一下这两者都干了什么

AttachInfo

/**
* 将视图附加到其父视图时提供的一组信息 窗口
*/
final static class AttachInfo {
//......
@UnsupportedAppUsage
final Handler mHandler;

AttachInfo(IWindowSession session, IWindow window, Display display,
ViewRootImpl viewRootImpl, Handler handler, Callbacks effectPlayer,
Context context) {
mSession = session;
mWindow = window;
mWindowToken = window.asBinder();
mDisplay = display;
mViewRootImpl = viewRootImpl;
mHandler = handler;
mRootCallbacks = effectPlayer;
mTreeObserver = new ViewTreeObserver(context);
}
}

根据该类的注释信息可以看出来这个类是用来保存窗口信息的,并且熟悉 View 添加流程的同学应该清楚,该类是在 WindowManager.addView 中创建 ViewRootImpl 的时候在 ViewRootImpl 的构造方法中创建的:

public ViewRootImpl(@UiContext Context context, Display display, IWindowSession session,
boolean useSfChoreographer) {
//.....
mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this,
context);

}

ViewRootImpl 是所有 View 的顶层,测量,布局绘制都是从该类中开始的。

WindowManager 创建完 ViewRootImpl 后会调用他的 setView 方法

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,
int userId) {
synchronized (this) {
if (mView == null) {
//......
mAttachInfo.mDisplayState = mDisplay.getState();
mSoftInputMode = attrs.softInputMode;
mWindowAttributesChanged = true;
mAttachInfo.mRootView = view;
mAttachInfo.mScalingRequired = mTranslator != null;
mAttachInfo.mApplicationScale =
mTranslator == null ? 1.0f : mTranslator.applicationScale;
if (panelParentView != null) {
mAttachInfo.mPanelParentWindowToken
= panelParentView.getApplicationWindowToken();
}
mAdded = true;
int res; /* = WindowManagerImpl.ADD_OKAY; */

// 请求布局
requestLayout();

try {
mOrigWindowType = mWindowAttributes.type;
//调用 WMS 添加窗口
res = mWindowSession.addToDisplayAsUser(mWindow, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(), userId,
mInsetsController.getRequestedVisibilities(), inputChannel, mTempInsets,
mTempControls);
} catch (RemoteException e) {
}
}
}

上面代码中先是对 mAttachInfo 进行各种赋值操作,接着 requestLayout ,View 的测量绘制布局都是从该方法中开始的,最后调用系统服务添加窗口,我们需要关心的就是 requestLayout

@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();//检测线程
mLayoutRequested = true;
scheduleTraversals();
}
}
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
//发送同步屏障,立即执行 mTraversalRunnable 任务
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}

最终会调用到 doTraversal 方法中:

void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
//移除同步屏障
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
//开始执行view的绘制流程
performTraversals();
}
}
private void performTraversals() {
// cache mView since it is used so much below...
final View host = mView;

if (mFirst) {
//调用 view 该方法
host.dispatchAttachedToWindow(mAttachInfo, 0);
}
//.........
}
#View
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
mAttachInfo = info; 进行赋值
//...
}

通过上面可以看出来最终 mAttachInfo 的的赋值是在 performTraversals 方法中调用完成的,该方法中也进行了测量布局绘制等操作,如果仔细看源码就会发现 dispatchAttachedToWindow 是在测量等操作之前执行的,那为什么 View.post 中还能获取到 View 的宽高等属性呢?

其实这个问题也不是特别难,因为 performTraversals 方法也是通过 handler 发送的,在执行 mTraversalRunnable 的时候才对 mAttachInfo 进行的赋值,然后再执行绘制流程,所以通过 mAttachInfo.handler 发送的消息肯定是在 mTraversalRunnable 之后执行的,这个时候绘制流程已经结束了,正因为如此,所以才可以获取到 View 的宽高等属性。

小结一下

在 mAttachInfo 不为空的情况下会直接使用 handler 发送消息,为什么 mAttacheInfo 发送后就可以获取到各种属性数据,主要流程如下所示:

  1. View 在创建出来后需要使用 WindowManager.addView 添加到屏幕上,期间会创建 View 的顶层类 ViewRootImpl
  2. 在 ViewRootImpl 构造方法中回创建 mAttachInfo
  3. 在 ViewRootImpl.setView 中对 mAttacheInfo 添加各种数据,并调用 View 的绘制流程,设置同步屏障,使用 handler 发送绘制任务,使得该消息可以再第一时间执行
  4. 在绘制流程的最开始的时候将 mAttachInfo 传递给 View,这样便是整个流程了
  5. 等到 View.post 执行的时候,使用 mattachInfo.handler 发送的消息肯定会在 View 绘制的任务之后执行

如果你对 View 的添加流程和绘制流程不太熟悉,这里推荐两篇文章对你会有一点帮助

Android | 理解 Window 和 WindowManager :里面有 View 的添加流程等

Android | 理解 ViewRootImpl : View 的绘制流程等

getRunQueue.post

通过 View.post 中的代码可以知道如果 mAttachInfo 为 null 就会执行 getRunQueue().post() 方法,下面我们来看一下这个方法:

private HandlerActionQueue getRunQueue() {
if (mRunQueue == null) {
mRunQueue = new HandlerActionQueue();
}
return mRunQueue;
}
/**
* 当没有附加处理程序时,用于从视图中排队等待处理的类
*
* @hide Exposed for test framework only.
*/
public class HandlerActionQueue {
private HandlerAction[] mActions;
private int mCount;
//......
private static class HandlerAction {
final Runnable action;
final long delay;

public HandlerAction(Runnable action, long delay) {
this.action = action;
this.delay = delay;
}

public boolean matches(Runnable otherAction) {
return otherAction == null && action == null
|| action != null && action.equals(otherAction);
}
}
}

该类也比较简单,主要就是对需要处理的任务进行排队等待,我们直接来看 post 方法

public void post(Runnable action) {
postDelayed(action, 0);
}

public void postDelayed(Runnable action, long delayMillis) {
final HandlerAction handlerAction = new HandlerAction(action, delayMillis);

synchronized (this) {
if (mActions == null) {
mActions = new HandlerAction[4];
}
mActions = GrowingArrayUtils.append(mActions, mCount, handlerAction);
mCount++;
}
}
#GrowingArrayUtils.java
public static <T> T[] append(T[] array, int currentSize, T element) {
if (currentSize + 1 > array.length) {
T[] newArray = (T[]) Array.newInstance(array.getClass().getComponentType(),
growSize(currentSize));
System.arraycopy(array, 0, newArray, 0, currentSize);
array = newArray;
}
array[currentSize] = element;
return array;
}
public static int growSize(int currentSize) {
return currentSize <= 4 ? 8 : currentSize * 2;
}

上面代码中创建了 HandlerAction 对象,并且保存到 mActions 数组中,默认数组大小等于 4,如果已经满了就会通过反射重新创建一个数组,并将数据迁移过去,每次创建数组的大小都是之前的两倍。

到这里添加到数组之后就没有别的操作了,此时我们需要推测一下这个数组中的任务会在何时被取出来然后在执行,通过上面的分析,我们大致就可以推断出来八成是在 dispatchAttachedToWindow() 方法中执行的,我们重新看一下这个方法:

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

果不其然,就是在该方法中执行的,在该方法中执行肯定就可以保证任务是在绘制流程之后执行的,我们继续跟进一下执行的方法:

//
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;
}
}

上面遍历数组,将任务取出使用 Handler 发送,最后清理资源就完事了。

总结一下

通过上面的分析,其实这个逻辑本身还是非常简单的,但是需要你提前了解 View 的添加流程以及绘制流程和Handler ,了解这些你再去看这个源码就会非常简单。

如果感觉本文对你有点帮助的话请点赞支持一下,多谢啦!


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

Moshi 真正意义上的完美解决Gson在kotlin中默认值空的问题

MoshiMoshi是一个对Kotlin更友好的Json库,square/moshi: A modern JSON library for Kotlin and Java. (github.com)依赖implementation("com.squareup....
继续阅读 »

Moshi

Moshi是一个对Kotlin更友好的Json库,square/moshi: A modern JSON library for Kotlin and Java. (github.com)

依赖

implementation("com.squareup.moshi:moshi:1.8.0")
kapt("com.squareup.moshi:moshi-kotlin-codegen:1.8.0")

使用场景

基于kotlin-reflection反射需要额外添加 com.squareup.moshi:moshi-kotlin:1.13.0 依赖

// generateAdapter = true 表示使用codegen生成这个类的JsonAdapter
@JsonClass(generateAdapter = true)
// @Json 标识json中字段名
data class Person(@Json(name = "_name")val name: String, val age: Int)
fun main() {
   val moshi: Moshi = Moshi.Builder()
       // KotlinJsonAdapterFactory基于kotlin-reflection反射创建自定义类型的JsonAdapter
      .addLast(KotlinJsonAdapterFactory())
      .build()
   val json = """{"_name": "xxx", "age": 20}"""
   val person = moshi.adapter(Person::class.java).fromJson(json)
   println(person)
}
  • KotlinJsonAdapterFactory用于反射生成数据类的JsonAdapter,如果不使用codegen,那么这个配置是必要的;如果有多个factory,一般将KotlinJsonAdapterFactory添加到最后,因为创建Adapter时是顺序遍历factory进行创建的,应该把反射创建作为最后的手段
  • @JsonClass(generateAdapter = true)标识此类,让codegen在编译期生成此类的JsonAdapter,codegen需要数据类和它的properties可见性都是internal/public
  • moshi不允许需要序列化的类不是存粹的Java/Kotlin类,比如说Java继承Kotlin或者Kotlin继承Java

存在的问题

所有的字段都有默认值的情况

@JsonClass(generateAdapter = true)
data class DefaultAll(
  val name: String = "me",
  val age: Int = 17
)

这种情况下,gson 和 moshi都可以正常解析 “{}” json字符

部分字段有默认值

@JsonClass(generateAdapter = true)
data class DefaultPart(
  val name: String = "me",
  val gender: String = "male",
  val age: Int
)

// 针对以下json gson忽略name,gender属性的默认值,而moshi可以正常解析
val json = """{"age": 17}"""

产生的原因

Gson反序列化对象时优先获取无参构造函数,由于DefaultPart age属性没有默认值,在生成字节码文件后,该类没有无参构造函数,所有Gson最后调用了Unsafe.newInstance函数,该函数不会调用构造函数,执行对象初始化代码,导致name,gender对象是null。

Moshi 通过adpter的方式匹配类的构造函数,使用函数签名最相近的构造函数构造对象,可以是的默认值不丢失,但在官方的例程中,某些情况下依然会出现我们不希望出现的问题。

Moshi的特殊Json场景

1、属性缺失

针对以下类

@JsonClass(generateAdapter = true)
data class DefaultPart(
   val name: String,
   val gender: String = "male",
   val age: Int
)

若json = """ {"name":"John","age":18}""" Moshi可以正常解析,但如果Json=""" {"name":"John"}"""Moshi会抛出Required value age missing at $ 的异常,

2、属性=null

若Json = """{"name":"John","age":null} ”“”Moshi会抛出Non-null value age was null at $ 的异常

很多时候后台返回的Json数据并不是完全的统一,会存在以上情况,我们可以通过对age属性如gender属性一般设置默认值的方式处理,但可不可以更偷懒一点,可以不用写默认值,系统也能给一个默认值出来。

完善Moshi

分析官方库KotlinJsonAdapterFactory类,发现,以上两个逻辑的判断代码在这里

internal class KotlinJsonAdapter<T>(
 val constructor: KFunction<T>,
   // 所有属性的bindingAdpter
 val allBindings: List<Binding<T, Any?>?>,
   // 忽略反序列化的属性
 val nonIgnoredBindings: List<Binding<T, Any?>>,
   // 反射类得来的属性列表
 val options: JsonReader.Options
) : JsonAdapter<T>() {

 override fun fromJson(reader: JsonReader): T {
   val constructorSize = constructor.parameters.size

   // Read each value into its slot in the array.
   val values = Array<Any?>(allBindings.size) { ABSENT_VALUE }
   reader.beginObject()
   while (reader.hasNext()) {
       //通过reader获取到Json 属性对应的类属性的索引
     val index = reader.selectName(options)
     if (index == -1) {
       reader.skipName()
       reader.skipValue()
       continue
    }
       //拿到该属性的binding
     val binding = nonIgnoredBindings[index]
// 拿到属性值的索引
     val propertyIndex = binding.propertyIndex
     if (values[propertyIndex] !== ABSENT_VALUE) {
       throw JsonDataException(
         "Multiple values for '${binding.property.name}' at ${reader.path}"
      )
    }
// 递归的方式,初始化属性值
     values[propertyIndex] = binding.adapter.fromJson(reader)

       // 关键的地方1
       // 判断 初始化的属性值是否为null ,如果是null ,代表这json字符串中的体现为 age:null
     if (values[propertyIndex] == null && !binding.property.returnType.isMarkedNullable) {
         // 抛出Non-null value age was null at $ 异常
       throw Util.unexpectedNull(
         binding.property.name,
         binding.jsonName,
         reader
      )
    }
  }
   reader.endObject()

   // 关键的地方2
    // 初始化剩下json中没有的属性
   // Confirm all parameters are present, optional, or nullable.
     // 是否调用全属性构造函数标志
   var isFullInitialized = allBindings.size == constructorSize
   for (i in 0 until constructorSize) {
     if (values[i] === ABSENT_VALUE) {
         // 如果等于ABSENT_VALUE,表示该属性没有初始化
       when {
           // 如果该属性是可缺失的,即该属性有默认值,这不需要处理,全属性构造函数标志为false
         constructor.parameters[i].isOptional -> isFullInitialized = false
           // 如果该属性是可空的,这直接赋值为null
         constructor.parameters[i].type.isMarkedNullable -> values[i] = null // Replace absent with null.
           // 剩下的则是属性没有默认值,也不允许为空,如上例,age属性
           // 抛出Required value age missing at $ 异常
         else -> throw Util.missingProperty(
           constructor.parameters[i].name,
           allBindings[i]?.jsonName,
           reader
        )
      }
    }
  }

   // Call the constructor using a Map so that absent optionals get defaults.
   val result = if (isFullInitialized) {
     constructor.call(*values)
  } else {
     constructor.callBy(IndexedParameterMap(constructor.parameters, values))
  }

   // Set remaining properties.
   for (i in constructorSize until allBindings.size) {
     val binding = allBindings[i]!!
     val value = values[i]
     binding.set(result, value)
  }

   return result
}

 override fun toJson(writer: JsonWriter, value: T?) {
   if (value == null) throw NullPointerException("value == null")

   writer.beginObject()
   for (binding in allBindings) {
     if (binding == null) continue // Skip constructor parameters that aren't properties.

     writer.name(binding.jsonName)
     binding.adapter.toJson(writer, binding.get(value))
  }
   writer.endObject()
}

通过代码的分析,是不是可以在两个关键的逻辑点做以下修改


// 关键的地方1
// 判断 初始化的属性值是否为null ,如果是null ,代表这json字符串中的体现为 age:null
if (values[propertyIndex] == null && !binding.property.returnType.isMarkedNullable) {
   // 抛出Non-null value age was null at $ 异常
   //throw Util.unexpectedNull(
   //   binding.property.name,
   //   binding.jsonName,
   //   reader
   //)
   // age:null 重置为ABSENT_VALUE值,交由最后初始化剩下json中没有的属性的时候去初始化
values[propertyIndex] = ABSENT_VALUE
}

// 关键的地方2
// 初始化剩下json中没有的属性
// Confirm all parameters are present, optional, or nullable.
// 是否调用全属性构造函数标志
var isFullInitialized = allBindings.size == constructorSize
for (i in 0 until constructorSize) {
   if (values[i] === ABSENT_VALUE) {
       // 如果等于ABSENT_VALUE,表示该属性没有初始化
       when {
           // 如果该属性是可缺失的,即该属性有默认值,这不需要处理,全属性构造函数标志为false
           constructor.parameters[i].isOptional -> isFullInitialized = false
           // 如果该属性是可空的,这直接赋值为null
           constructor.parameters[i].type.isMarkedNullable -> values[i] = null // Replace absent with null.
           // 剩下的则是属性没有默认值,也不允许为空,如上例,age属性
           // 抛出Required value age missing at $ 异常
           else ->{
               //throw Util.missingProperty(
                   //constructor.parameters[i].name,
                   //allBindings[i]?.jsonName,
                   //reader
          //)
               // 填充默认
               val index = options.strings().indexOf(constructor.parameters[i].name)
               val binding = nonIgnoredBindings[index]
               val propertyIndex = binding.propertyIndex
// 为该属性初始化默认值
               values[propertyIndex] = fullDefault(binding)

          }
      }
  }
}



private fun fullDefault(binding: Binding<T, Any?>): Any? {
       return when (binding.property.returnType.classifier) {
           Int::class -> 0
           String::class -> ""
           Boolean::class -> false
           Byte::class -> 0.toByte()
           Char::class -> Char.MIN_VALUE
           Double::class -> 0.0
           Float::class -> 0f
           Long::class -> 0L
           Short::class -> 0.toShort()
           // 过滤递归类初始化,这种会导致死循环
           constructor.returnType.classifier -> {
               val message =
                   "Unsolvable as for: ${binding.property.returnType.classifier}(value:${binding.property.returnType.classifier})"
               throw JsonDataException(message)
          }
           is Any -> {
               // 如果是集合就初始化[],否则就是{}对象
               if (Collection::class.java.isAssignableFrom(binding.property.returnType.javaType.rawType)) {
                   binding.adapter.fromJson("[]")
              } else {
                   binding.adapter.fromJson("{}")
              }
          }
           else -> {}
      }
  }

最终效果

"""{"name":"John","age":null} ”“” age会被初始化成0,

"""{"name":"John"} ”“” age依然会是0,即使我们在类中没有定义age的默认值

甚至是对象@JsonClass(generateAdapter = true)

data class DefaultPart(
   val name: String,
   val gender: String = "male",
   val age: Int,
   val action:Action
)
class Action(val ac:String)

最终Action也会产生一个Action(ac:"")的值

data class RestResponse<T>(
val code: Int,
val msg: String="",
val data: T?
) {
fun isSuccess() = code == 1

fun checkData() = data != null

fun successRestData() = isSuccess() && checkData()

fun requsetData() = data!!
}
class TestD(val a:Int,val b:String,val c:Boolean,val d:List<Test> ) {
}

class Test(val a:Int,val b:String,val c:Boolean=true)



val s = """
{
"code":200,
"msg":"ok",
"data":[{"a":0,"c":false,"d":[{"b":null}]}]}
""".trimIndent()

val a :RestResponse<List<TestD>>? = s.fromJson()


最终a为 {"code":200,"msg":"ok","data":[{"a":0,"b":"","c":false,"d":[{"a":0,"b":"","c":true}]}]}


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

Kotlin 常见面试知识点

一.Kotlin 与 Java 对比Kotlin 和 Java 都是针对 JVM 的编程语言。它们有一些相似之处,比如都支持面向对象编程、静态类型和垃圾回收等。但是 Kotlin 和 Java 也有很多不同之处。以下是一些 Kotlin 和 Java 的比较:...
继续阅读 »

一.Kotlin 与 Java 对比

Kotlin 和 Java 都是针对 JVM 的编程语言。它们有一些相似之处,比如都支持面向对象编程、静态类型和垃圾回收等。但是 Kotlin 和 Java 也有很多不同之处。以下是一些 Kotlin 和 Java 的比较:

  1. 代码量:Kotlin 比 Java 代码量少很多。Kotlin 通过使用更简洁的语法和函数式编程的概念来简化 Java 代码,以减少代码的复杂性。

  2. 空指针安全:Kotlin 通过引入空指针安全机制来避免空指针异常,而 Java 中需要手动检查 null 值。

  3. 扩展函数:Kotlin 中有一个强大的功能叫做扩展函数,它允许用户将一个已存在的类进行扩展。

  4. 函数式编程概念:Kotlin 支持更多的函数式编程概念,比如 lambda 表达式、高阶函数和尾递归等。

  5. 数据类:Kotlin 中引入了数据类,它允许程序员快速创建简单的数据类。相比之下,Java 需要编写大量的样板代码。

总的来说,Kotlin 相对于 Java 拥有更简洁的语法,更少的瑕疵,更多的功能和更高的生产效率,但是 Java 相对于 Kotlin 拥有更成熟的生态体系,更广泛的支持和更好的跨平台支持。

Kotlin 常见关键字

Kotlin 作为一种独立的编程语言,有一些 Java 中没有的关键字,以下是 Kotlin 特有的一些关键字:

  1. companion:伴生对象,可以在类内部定义一个对象,用于实现静态方法和属性。

  2. data:数据类,用于快速创建一个用于存储数据的类。

  3. by:委托,可以在一个对象中使用另一个对象的属性或方法。

  4. reified:具体化,用于解决 Java 泛型擦除问题。

  5. inline:内联,用于在编译时将函数代码插入到调用处,提高性能。

  6. non-local return:非局部返回,可以在嵌套函数中使用 return 关键字返回到外部函数。

  7. tailrec:尾递归,用于将递归函数改为尾递归函数,提高性能。

  8. suspend 和 coroutine:协程,Kotlin 支持协程编程,可以使用 suspend 关键字定义挂起函数,使用 coroutine 构建异步和并发程序。

这些关键字提供了 Kotlin 编程中一些独特的语法异构,使得程序员可以更轻松地编写高效、可读性优秀的代码。

Kotlin 常见内置函数

  1. let:作用于某个对象,让其调用一个函数,并返回 Lambda 表达式的结果。let 函数可以避免在调用 Lambda 表达式时产生多余的变量名,提高了代码可读性。

  2. apply:作用于某个对象,将对象本身作为接收器(this)返回,可以连续进行多次调用,非常适合链式调用代码块的场景。

  3. with:非扩展函数,接受一个对象和一个 Lambda 表达式,可以让您在将对象本身作为参数传递的情况下调用 Lambda 表达式。with 函数允许编写更紧凑的代码,特别是当您需要访问一个对象的属性时。

  4. run:类似于 let 函数,但是只能作用于可空对象。如果对象不为空,run 函数会让对象调用 Lambda 表达式并返回其结果;如果对象为空,run 函数返回 null。

  5. also:类似于 let 函数,但是返回的值是指定的接收器对象,而不是 Lambda 表达式的结果。可以用于在对象的生命周期内执行额外的操作。

  6. takeIf:接受一个谓词(Lambda 表达式),并返回任何满足该谓词的对象,否则返回 null。

  7. takeUnless:与 takeIf 函数相反,如果对象不满足指定的谓词,则返回对象本身,否则返回 null。

  8. when:作为表达式或语句,类似于 Java 中的 switch 语句,可以匹配多个条件或者值,并执行与条件/值对应的代码块。

这些内置函数属于 Kotlin 标准库的一部分,使得 Kotlin 代码更加简洁、易读、易于维护,特别适用于链式调用或需要多次对某个对象执行某个操作的场景。

Kotlin 与 RxJava

Kotlin是一种现代的编程语言,它对函数式编程和响应式编程提供了很好的支持。RxJava也是一种非常流行的响应式编程库。虽然Kotlin本身没有RxJava那么强大,但它提供了一些工具和语言功能来简化异步编程和响应式编程。下面是一些使用Kotlin替代RxJava的技术:

  1. 协程:Kotlin提供了一种名为协程的轻量级线程,可以简化异步编程。协程使用类似于JavaScript的async/await语法,允许您轻松地编写异步代码而无需编写回调或使用RxJava。

  2. Flow:Kotlin的流是一种响应式编程的替代方案。它提供了与RxJava的Observable类似的流式API,但它是基于协程的,并且更容易与Kotlin集成。

  3. LiveData:LiveData是一种Kotlin Android架构组件,它提供了类似于RxJava的观察者模式。LiveData可以让您轻松地观察数据变化,同时避免RxJava的一些复杂性和性能问题。

总之,Kotlin提供了许多替代RxJava的工具和功能,从而使异步编程和响应式编程更加简单和直观。

Kotlin 协程

以下是一些与Kotlin协程相关的面试题和答案:

  1. 什么是Kotlin协程?

答:Kotlin协程是一种轻量级的线程,它使用协作式调度来实现并发。与传统的线程不同,协程可以自由地挂起和恢复。它们使并发代码更加轻松和直观,并且可以避免一些常见的并发问题。

  1. Kotlin协程的优点是什么?

答:Kotlin协程的优点包括:

  • 简单易用:协程使异步代码更加轻松和直观,而无需编写复杂的回调或使用RxJava。
  • 轻量级:协程使用协作式调度,因此它们比传统线程更加轻量级。
  • 避免共享状态问题:协程通过将计算任务拆分为许多小的、非共享的组件来避免共享状态问题。
  • 更好的性能:因为协程是轻量级的,它们的创建和销毁所需的开销更小,因此具有更好的性能。
  1. Kotlin协程中的“挂起”意味着什么?

答:在Kotlin协程中,挂起是指暂停协程的执行,直到某些条件满足。在挂起期间,协程不会占用线程,并且可以由另一个协程或线程执行。协程通常在遇到I/O操作或长时间运行的计算时挂起。

  1. 如何在Kotlin中创建协程?

答:在Kotlin中,可以使用launch、async和runBlocking等函数来创建协程。例如:

// 使用launch创建协程
GlobalScope.launch {
// 协程执行的代码
}

// 使用async创建协程
val deferred = GlobalScope.async {
// 协程执行的代码并返回结果
42
}

// 使用runBlocking创建协程
runBlocking {
// 协程执行的代码
}
  1. Kotlin中的“协程作用域”是什么?

答:协程作用域是一种可以帮助协程被正确地取消和清理的机制。它是由Kotlin提供的一个结构,可以创建和管理多个相关联的协程。协程作用域可以确保在其范围内创建的所有协程都被正确地取消,并且可以管理这些协程的执行顺序。

  1. Kotlin协程中的“挂起函数”是什么?

答:挂起函数是指可以在协程中使用的特殊函数,它们可以在执行过程中暂停协程的执行,直到某些条件满足。通常,挂起函数通过使用“挂起标记”(suspend)来定义。例如:

suspend fun getUser(id: Int): User {
// 从远程服务器获取用户数据
return user
}
  1. 如何处理Kotlin协程中的异常?

答:在Kotlin协程中,可以使用try/catch语句来处理异常。如果协程中的异常未被捕获,它将传播到协程的上层。可以使用CoroutineExceptionHandler在协程中设置一个全局异常处理程序。例如:

val handler = CoroutineExceptionHandler { _, exception ->
// 处理异常
}

GlobalScope.launch(handler) {
// 协程执行的代码
}

Kotlin 泛型-逆变/协变

Kotlin中的泛型支持协变和逆变。接下来分别对它们进行介绍:

  1. 协变(Covariant)

协变意味着可以使用子类型作为父类型的替代。在Kotlin中,为了支持协变,我们可以将out修饰符添加到泛型参数上。例如,让我们看一个用于生产者的接口:

interface Producer<out T> {
fun produce(): T
}

这个接口可以使用out修饰符,表示这是一个生产者,它只会产生类型T的值,而不会对其进行任何更改。因此,我们可以将子类型作为父类型的替代:

class AnimalProducer : Producer<Animal> {
override fun produce(): Animal {
return Animal()
}
}

class DogProducer : Producer<Dog> {
override fun produce(): Dog {
return Dog()
}
}

这里DogAnimal的子类型,所以我们可以使用DogProducer作为类型为Producer<Animal>的变量的值。因为我们知道我们总是可以期望DogProducer生产类型为Animal的值。

  1. 逆变(Contravariant)

逆变意味着可以使用父类型作为子类型的替代。在Kotlin中,为了支持逆变,我们可以将in修饰符添加到泛型参数上。例如,让我们看一个用于消费者的接口:

interface Consumer<in T> {
fun consume(item: T)
}

这个接口可以使用in修饰符,表示这是一个消费者,它只接受类型T的值,而不会返回任何值。因此,我们可以将父类型作为子类型的替代:

class AnimalConsumer : Consumer<Animal> {
override fun consume(item: Animal) {
// 消费Animal类型的值
}
}

class DogConsumer : Consumer<Dog> {
override fun consume(item: Dog) {
// 消费Dog类型的值
}
}

这里AnimalDog的父类型,所以我们可以使用AnimalConsumer作为类型为Consumer<Dog>的变量的值。因为我们知道我们总是可以期望AnimalConsumer会接受类型为Dog的值。

总之,Kotlin中的协变和逆变提供了更好的类型安全性和代码灵活性。使用它们可以确保类型转换是正确的,并且可以使程序更加健壮和易于维护。


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

Kotlin:正则Regex原来这么方便

一、前言不着急讲述Regex,我们先看一个需求,统计某个字符或字符串在整个字符串中出现的次数,举例,字符串如下:今天是2023年6月29日,北京,天气晴,与昨天不同的是,今天格外的热,也不知道明天会怎么样,是晴天还是阴天呢,具体得到明天才能知道了。 请统计“天...
继续阅读 »

一、前言

不着急讲述Regex,我们先看一个需求,统计某个字符或字符串在整个字符串中出现的次数,举例,字符串如下:

今天是2023年6月29日,北京,天气晴,与昨天不同的是,今天格外的热,也不知道明天会怎么样,是晴天还是阴天呢,具体得到明天才能知道了。

请统计“天”字出现的次数。

实现上边的功能,我相信大家,有很多种方式,下面也举例几种:

1、循环遍历

这是最常见,也是首先能想到的。

 val content = "今天是2023年6月29日,北京,天气晴,与昨天不同的是,今天格外的热,也不知道明天会怎么样,是晴天还是阴天呢,具体得到明天才能知道了。"
var count=0
//遍历
content.forEach {
if ('天' == it) {
count++//次数累加
}
}
print(count)

打印结果

8

2、Kotlin中count操作符

 val content = "今天是2023年6月29日,北京,天气晴,与昨天不同的是,今天格外的热,也不知道明天会怎么样,是晴天还是阴天呢,具体得到明天才能知道了。"
val count=content.count { ch -> ch == '天' }
print(count)

打印结果

8

3、Kotlin中的filter操作符

 val content = "今天是2023年6月29日,北京,天气晴,与昨天不同的是,今天格外的热,也不知道明天会怎么样,是晴天还是阴天呢,具体得到明天才能知道了。"
val filterContent=content.filter { ch -> ch == '天' }
print(filterContent.length)

打印结果

8

4、使用Java中的正则

val content = "今天是2023年6月29日,北京,天气晴,与昨天不同的是,今天格外的热,也不知道明天会怎么样,是晴天还是阴天呢,具体得到明天才能知道了。"
val pattern = Pattern.compile("天")
val matcher = pattern.matcher(content)
var count = 0
while (matcher.find()) {
count++
}
print(count)

打印结果

8

5、使用Regex对象

val content = "今天是2023年6月29日,北京,天气晴,与昨天不同的是,今天格外的热,也不知道明天会怎么样,是晴天还是阴天呢,具体得到明天才能知道了。"
val matchResult = Regex("天").findAll(content)
print(matchResult.count())

当然了还有很多的实现方式,我们暂且举这五种,看到第五种的Regex对象实现方式,有的铁子就问了,我丝毫没有发现Regex到底有什么可取之处啊,简单吗?与Kotlin的操作符相比,简直就是小巫见大巫,毫无优点可言,别慌啊铁子,如果只是简单的文本寻找,Regex绝对没有优势,毕竟,这只是它的一个引子,冰山一角的功能,我们慢慢拉开序幕~

二、Regex方法列举

通过以上的前言,我们大致知道了,原来Regex也可以实现查找的功能,无形当中,又多了一种选择方式,除此之外,它还有那些功能呢?

构造函数

我们先看一下基本的构造函数

方法参数类型概述
Regex(pattern: String)String要匹配的正则表达式模式。
Regex(pattern: String, option: RegexOption)String,RegexOption根据指定的模式字符串和指定的单个选项创建正则表达式。
Regex(pattern: String, options: Set)String,Set根据指定的模式字符串和指定的选项集创建正则表达式

对于一个参数的构造,没什么好说的,就是一个正则表达式,这也是我们最常用的,至于后面两个,相对使用的较少,不过我们还是简单的介绍一下:

RegexOption是一个枚举类型,具体类型如下:

参数概述
IGNORE_CASE启用不区分大小写的匹配。大小写比较支持Unicode
MULTILINE启用多行模式。在多行模式中,表达式^和$分别在输入序列的行终止符或末尾之后或之前匹配。
LITERAL启用模式的文字分析。输入序列中的元字符或转义序列将不会被赋予特殊含义。
UNIX_LINES启用Unix行模式。在这种模式下,只有'\n'被识别为行终止符。
COMMENTS允许在模式中使用空格和注释。
DOT_MATCHES_ALL启用表达式时的模式。匹配任何字符,包括行终止符。
CANON_EQ通过规范分解实现等价。

大家可以根据不同的情况,选择对应的参数即可,至于Set,无非就是多个RegexOption。

常见方法

了解完基本的构造,我们再来看下常用的方法:

方法参数类型概述
find(input: CharSequence, startIndex: Int = 0)CharSequence,Int寻找字符串中第一个匹配的MatchResult对象,默认从索引0开始。
findAll(input: CharSequence, startIndex: Int = 0)CharSequence,Int字符串中所有匹配的MatchResult序列,默认从索引0开始。
containsMatchIn(input: CharSequence)CharSequence如果包含输入的字符就返回true。
replace(input: CharSequence, replacement: String)CharSequence,String和String的replace类似,第一个是输入的目标字符,第二个是替换字符。
replaceFirst(input: CharSequence, replacement: String)CharSequence,String替换第一个查找到的字符
matches(input: CharSequence)CharSequence输入字符序列是否与正则表达式匹配
matchEntire(input: CharSequence)CharSequence用于匹配模式中的完整输入字符

三、Regex常见方法使用举例

在上篇文章《Android:这个需求搞懵了,产品说要实现富文本回显展示》中,不知道大家是否还有印象,对于富文本的截取,我们就采用了Regex,简简单单的就实现了富文本的内容获取,当然了也简单的介绍了部分的方法。下面,我们针对第二项中的各个方法,简单做个使用案例。

1、find

find,用于寻找第一次出现的结果,比如我们要寻找某个字符串中第一次出现的数字,如下举例:

  val content = "有这样一串数字2345,还有6789,以及012,我们如何只获取数字2345呢"
val regex = Regex("\d+")
val matchResult = regex.find(content)
print(matchResult?.value)

打印结果

2345

2、findAll

findAll,顾名思义,就是寻找所有的结果,还是上面那个案例,我们改成findAll

 val content = "有这样一串数字2345,还有6789,以及012,我们如何只获取数字2345呢"
val regex = Regex("\d+")
val matchResult = regex.findAll(content)
matchResult.forEach {
println(it.value)
}

打印结果

2345
6789
012
2345

还有一个典型的案例,就是富文本标签的截取,这个在上篇文章举例过了,大家可以看上篇文章。

3、containsMatchIn

用于判断是否包含某个字符,和String的使用方式类似:

 val content = "二流小码农"
val regex = Regex("农")
val regex2 = Regex("中")
val isContains = regex.containsMatchIn(content)
val isContains2 = regex2.containsMatchIn(content)
println(isContains)
println(isContains2)

打印结果

true
false

4、replace

用于替换字符串中的相关内容:

 val content = "二流小码农"
val regex = Regex("二")
val replaceContent=regex.replace(content,"一")
println(replaceContent)

打印结果

一流小码农

5、replaceFirst

用于替换字符串中第一次相符合的内容:

val content = "有这样一串数字2345,还有6789,以及012,我们如何只获取数字2345呢"
val regex = Regex("\d+")
//把第一次出现的数字替换为字母abcd
val replaceContent=regex.replaceFirst(content,"abcd")
println(replaceContent)

打印结果

有这样一串数字abcd,还有6789,以及012,我们如何只获取数字2345呢

6、matches

用于输入的字符和目标内容是否匹配,比如用于邮箱的验证,手机号的验证等等情况:

//邮箱验证
val content = "11@qq.com"
val content2 = "11@qq"
val regex = Regex("[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+")
val matches=regex.matches(content)
val matches2=regex.matches(content2)
println(matches)
println(matches2)

打印结果

true
false

7、matchEntire

用于匹配模式中的完整输入字符。

 //匹配数字
val regex = Regex("\d+")
val matchResult=regex.matchEntire("二流小码农")
val matchResult2=regex.matchEntire("二流小码农666")
val matchResult3=regex.matchEntire("123456")
println(matchResult?.value)
println(matchResult2?.value)
println(matchResult3?.value)

打印结果

null
null
123456

四、总结

Regex相对于Java的Api来说,使用起来更加的简单,如果大家在非正则的功能使用时,比如寻找,替换,是否包含等等,完全可以使用字符串自带的功能即可,如果说要实现一些较为复杂的,比如邮箱的验证,手机号的验证等等,那么Regex绝对是你的首选。


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

聊聊javascript中令人头大的this

在JavaScript中,this是一个非常重要的关键字,可以用来访问对象中的属性和方法。它指向当前函数的执行上下文。由于 JavaScript 可以是面向对象的,也可以是基于原型的语言,所以 this 的含义有时会有些复杂,它的行为有时候...
继续阅读 »

在JavaScript中,this是一个非常重要的关键字,可以用来访问对象中的属性和方法。它指向当前函数的执行上下文。由于 JavaScript 可以是面向对象的,也可以是基于原型的语言,所以 this 的含义有时会有些复杂,它的行为有时候会让人感到困惑。

一、让人头痛的this

在JavaScript中,this的值取决于它在哪个上下文中被调用。上下文可以是全局对象、函数、对象等。下面我们来详细聊聊这些情况:

1. 全局上下文中的this

在全局上下文中,this指向全局对象。在浏览器环境中,这个全局对象是window对象。在Node.js环境中,这个全局对象是global对象。

console.log(this === window); // true

需要注意的是,虽然this指向全局对象,但是在严格模式下,thisundefined。此外,在函数内部,this的值取决于函数是如何被调用的。如果函数被作为对象的方法调用,那么this指向该对象。如果函数是作为普通函数调用,那么this指向全局对象。如果函数是作为构造函数调用,那么this指向新创建的对象。

2. 函数上下文中的this

在函数中,this的值取决于函数是如何被调用的。当一个函数被作为普通函数调用时,this指向全局对象。例如:

function myFunction() {
console.log(this);
}

myFunction(); // 输出全局对象(window或global)

但是,当一个函数作为对象的方法调用时,this指向调用该方法的对象。例如:

const myObject = {
myMethod: function() {
console.log(this);
}
};

myObject.myMethod(); // 输出myObject对象

3. 构造函数中的this

当一个函数被用作构造函数时,this指向新创建的对象。例如:

function Person(name) {
this.name = name;
}

const person = new Person('张三');
console.log(person.name); // 输出 '张三'

4. 箭头函数中的this

箭头函数中的this与普通函数不同,它没有自己的执行上下文,而是与其所在的执行上下文共享同一个执行上下文。在箭头函数中,this指向函数定义时所在的上下文。例如:

const myObject = {
myMethod: function() {
const myArrowFunction = () => {
console.log(this);
}
myArrowFunction();
}
};

myObject.myMethod(); // 输出myObject对象

箭头函数在定义时会捕获其所在的执行上下文中的 this值。因此,箭头函数的执行上下文中的 this 值与定义它时所在的执行上下文中的 this 值相同,且无法通过 call()apply()bind()改变箭头函数中的 this 指向。

二、改变this的值

有时候,我们需要显式地改变this的值,这时就可以使用call()apply()bind()以及new操作符方法。

1. call()和apply()方法

call()apply()方法可以用来改变函数中this的值,并立即调用该函数。两者的区别在于传参方式不同,call()方法传参以逗号分隔,apply()方法传参以数组形式。例如:

function myFunction(a, b) {
console.log(this, a, b);
}

const myObject = {
myProperty: 'Hello'
}

myFunction.call(myObject, 1, 2); // 输出myObject对象,1,2
myFunction.apply(myObject, [1, 2]); // 输出myObject对象,1,2

2. bind()方法

bind()方法可以用来改变函数中this的值,并返回一个新的函数,不会立即调用该函数。例如:

function myFunction(a, b) {
console.log(this, a, b);
}

const myObject = {
myProperty: 'Hello'
}

const boundFunction = myFunction.bind(myObject, 1, 2);
boundFunction(); // 输出myObject对象,1,2

3. new绑定

在JavaScript中,new操作符用于创建一个新的对象,并将构造函数中的this指向该新对象。new操作符执行以下操作:

  1. 创建一个新的空对象
  2. 将该空对象的原型指向构造函数的prototype属性
  3. 将构造函数中的this指向该新对象
  4. 执行构造函数中的代码,并给该新对象添加属性和方法
  5. 返回该新对象

例如:

function Person(name) {
this.name = name;
}

const person = new Person('张三');
console.log(person.name); // 输出 '张三'

在这个例子中,new操作创建了一个新的对象,并将构造函数Person中的this指向该新对象。在构造函数中,this.name = name将新对象的name属性设置为'张三'。最后,new操作返回该新对象,将其赋值给变量person。

new绑定是一种特殊的方式,它可以使this指向新创建的对象。在JavaScript中,new操作符是一种常用的创建新对象的方式,使用它能够方便地创建新的对象并初始化它们的属性和方法。

三、写在最后

JavaScript中的this关键字是非常重要的,它可以用来访问对象中的属性和方法。由于this的行为有时候会让人感到困惑,因此需要对它有一个全面而深入的理解。只有理解了this的行为,才能更好地使用它,写出更加高效和可读性强的JavaScript代码。


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

为什么很多程序员都建议使用Linux

在计算机领域,Linux 是一个极具影响力的操作系统。它是由 Linus Torvalds 在 1991 年创建的,现在已经成为了开源世界中最流行的操作系统之一。很多程序员都建议使用 Linux,那么,为什么呢?安全性高Linux 是一个开源操作系统,因此,任...
继续阅读 »

在计算机领域,Linux 是一个极具影响力的操作系统。它是由 Linus Torvalds 在 1991 年创建的,现在已经成为了开源世界中最流行的操作系统之一。很多程序员都建议使用 Linux,那么,为什么呢?

  1. 安全性高

Linux 是一个开源操作系统,因此,任何人都可以查看和修改其源代码。这意味着,如果存在安全漏洞,那么很多人都可以发现并修复它们。相比之下,闭源操作系统只有少数人可以查看和修改其源代码,这意味着安全漏洞可能会被发现但不会被修复。

  1. 免费

Linux 是免费的,这意味着你可以在没有任何费用的情况下使用它。相比之下,商业操作系统需要支付高昂的许可证费用。对于个人用户和小型企业来说,这是非常有吸引力的。

  1. 强大的命令行工具

Linux 提供了非常强大的命令行工具,这些工具可以让程序员更快地完成任务。相比之下,Windows 和 macOS 的命令行工具要弱得多。

  1. 可定制性高

Linux 可以根据你的需要进行定制。你可以选择不同的桌面环境、窗口管理器、文件管理器等等。这意味着你可以创建一个完全适合你自己工作风格的操作系统。

  1. 支持众多编程语言

Linux 支持众多编程语言,包括 C、C++、Python、Java、PHP 等等。这意味着你可以使用自己喜欢的编程语言开发应用程序。

  1. 社区支持强大

Linux 社区非常强大,你可以在社区中找到各种各样的问题解答和支持。相比之下,商业操作系统的支持通常需要支付高昂的费用。

总结

在计算机领域,Linux 是一个极具影响力的操作系统。它具有高安全性、免费、强大的命令行工具、可定制性高、支持众多编程语言和社区支持强大等优点。因此,很多程序员都建议使用 Linux。


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

一个小公司的技术开发心酸事

背景长话短说,就是在2022年6月的时候加入了一家很小创业公司。老板不太懂技术,也不太懂管理,靠着一腔热血加上对实体运输行业的了解,加上盲目的自信,贸然开始创业,后期经营困难,最终散伙。自己当时也是不察,贸然加入,后边公司经营困难,连最后几个月的工资都没给发。...
继续阅读 »

背景

长话短说,就是在2022年6月的时候加入了一家很小创业公司。老板不太懂技术,也不太懂管理,靠着一腔热血加上对实体运输行业的了解,加上盲目的自信,贸然开始创业,后期经营困难,最终散伙。

自己当时也是不察,贸然加入,后边公司经营困难,连最后几个月的工资都没给发。

当时老板的要求就是尽力降低人力成本,尽快的开发出来App(Android+IOS),老板需要尽快的运营起来。

初期的技术选型

当时就自己加上一个刚毕业的纯前端开发以及一个前面招聘的ui,连个人事、测试都没有。

结合公司的需求与自己的技术经验(主要是前端和nodejs的经验),选择使用如下的方案:

  1. 使用uni-app进行App的开发,兼容多端,也可以为以后开发小程序什么的做方案预留,主要考虑到的点是比较快,先要解决有和无的问题;
  2. 使用egg.js + MySQL来开发后端,开发速度会快一点,行业比较小众,不太可能会遇到一些较大的性能问题,暂时看也是够用了的,后期过渡到midway.js也方便;
  3. 使用antd-vue开发运营后台,主要考虑到与uni-app技术栈的统一,节省转换成本;

也就是初期选择使用egg.js + MySQL + uni-app + antd-vue,来开发两个App和一个运营后台,快速解决0到1的问题。

关于App开发技术方案的选择

App的开发方案有很多,比如纯原生、flutter、uniapp、react-native/taro等,这里就当是的情况做一下选择。

  1. IOS与Android纯原生开发方案,需要新招人,两端同时开发,两端分别测试,这个资金及时间成本老板是不能接受的;
  2. flutter,这个要么自己从头开始学习,要么招人,相对于纯原生的方案好一点,但是也不是最好的选择;
  3. react-native/taro与uni-app是比较类似的选择,不过考虑到熟练程度、难易程度以及开发效率,最终还是选择了uni-app。

为什么选择egg.js做后端

很多时候方案的选择并不能只从技术方面考虑,当是只能选择成本最低的,当时的情况是egg.js完全能满足。

  1. 使用一些成熟的后端开发方案,如Java、、php、go之类的应该是比较好的技术方案,但对于老板来说不是好的经济方案;
  2. egg.js开发比较简单、快捷,个人也比较熟悉,对于新成员的学习成本也很低,对于JS有一定水平的也能很快掌握egg.js后端的开发

中间的各种折腾

前期开发还算顺利,在规定的时间内,完成了开发、测试、上线。但是,老板并没有如前面说的,很快运营,很快就盈利,运营的开展非常缓慢。中间还经历了各种折腾的事情。

  1. 老板运营遇到困难,就到处找一些专家(基本跟我们这事情没半毛钱关系的专家),不断的提一些业务和ui上的意见,不断的修改;
  2. 期间新来的产品还要全部推翻原有设计,重新开发;
  3. 还有个兼职的领导非要说要招聘原生开发和Java开发重新进行开发,问为什么,也说不出什么所以然,也是道听途说。

反正就是不断提出要修改产品、设计、和代码。中间经过不断的讨论,摆出自己的意见,好在最终技术方案没修改,前期的工作成果还在。后边加了一些新的需求:系统升级1.1、ui升级2.0、开发小程序版本、开发新的配套系统(小程序版本)以及开发相关的后台、添加即时通信服务、以及各种小的功能开发与升级;

中间老板要加快进度了就让招人,然后又无缘无故的要开人,就让人很无奈。最大的运营问题,始终没什么进展,明显的问题并不在产品这块,但是在这里不断的折腾这群开发,也真是难受。

明明你已经很努力的协调各种事情、站在公司的角度考虑、努力写代码,却仍然无济于事。

后期技术方案的调整

  1. 后期调整了App的打包方案;
  2. 在新的配套系统中,使用midway.js来开发新的业务,这都是基于前面的egg.js的团队掌握程度,为了后续的开发规范,做此升级;
  3. 内网管理公用npm包,开发业务组件库;
  4. 规范代码、规范开发流程;

人员招聘,团队的管理

人员招聘

如下是对于当时的人员招聘的一些感受:

  1. 小公司的人员招聘是相对比较难的,特别是还给不了多少钱的;
  2. 好在我们选择的技术方案,只要对于JS掌握的比较好就可以了,前后端都要开发一点,也方便人员工作调整,避免开发资源的浪费。

团队管理

对于小团队的管理的一些个人理解:

  1. 小公司刚起步,就应该实事求是,以业务为导向;
  2. 小公司最好采取全栈的开发方式,避免任务的不协调,造成开发资源的浪费;
  3. 设置推荐的代码规范,参照大家日常的代码习惯来制定,目标就是让大家的代码相对规范;
  4. 要求按照规范的流程设计与开发、避免一些流程的问题造成管理的混乱和公司的损失;
    1. 如按照常规的业务开发流程,产品评估 => 任务分配 => 技术评估 => 开发 => 测试 => cr => 上线 => 线上问题跟踪处理;
  5. 行之有效可量化的考核规范,如开发任务的截止日期完成、核心流程开发文档的书写、是否有线上bug、严谨手动修改数据库等;
  6. 鼓励分享,相互学习,一段工作经历总要有所提升,有所收获才是有意义的;
  7. 及时沟通反馈、团队成员的个人想法、掌握开发进度、工作难点等;

最后总结及选择创业公司避坑建议!important

  1. 选择创业公司,一定要确认老板是一个靠谱的人,别是一个总是画饼的油腻老司机,或者一个优柔寡断,没有主见的人,这样的情况下,大概率事情是干不成的;
    1. 老板靠谱,即使当前的项目搞不成,也可能未来在别的地方做出一番事情;
  2. 初了上边这个,最核心的就是,怎么样赚钱,现在这种融资环境,如果自己不能赚钱,大概率是活不下去的@自己;
  3. 抓住核心矛盾,解决主要问题,业务永远是最重要的。至于说选择的开发技术、代码规范等等这些都可以往后放;
  4. 对上要及时反馈自己的工作进度,保持好沟通,老板总是站在更高一层考虑问题,肯定会有一些不一样的想法,别总自以为什么什么的;
  5. 每段经历最好都能有所收获,人生的每一步都有意义。

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

面试官直问:你晓得什么是有序广播?

当面试官直接问你该问题时,对我们考察的是看我们否了解广播相关的知识这里我们必须了解广播的基本知识和原理,接下来对该问题进行回答解析。问题正解广播是什么?广播是一种广泛运用的在应用程序之间传输信息的机制,Android 主要是将系统运行时的各种“事件”通知给其他...
继续阅读 »

当面试官直接问你该问题时,对我们考察的是看我们否了解广播相关的知识

这里我们必须了解广播的基本知识和原理,接下来对该问题进行回答解析。

问题正解

  1. 广播是什么?

  • 广播是一种广泛运用的在应用程序之间传输信息的机制,Android 主要是将系统运行时的各种“事件”通知给其他应用,因此设计了多种广播。广播机制最大的特征就是发送方并不关注接收方是否接到数据,也不关注接收方是如何处理数据的。
  • Android 中的每个应用程序都可以对自己有利的广播进行注册,这样程序就只会接收到自己所关心的广播内容,这些广播可能是来自于系统的,也可能是来自于其他应用程序的,前者是系统广播,后者是自定义广播。广播在具体的项目中应用场景并不多,但一旦使用会使得程序变得精简很多。
  • 安卓的广播原理,BroadCastReceiver是对分发出来的Broadcast 进行过滤接受并响应的一类组件,属于Android四大组件之一,主要用于接收系统或者app发送的广播事件。在我们的项目中常常使用广播接收者接收系统通知,比如开机启动、低电量、外播电话、锁屏、sd挂载等。 如果我们需要做的是播放器,那么监听到用户锁屏后我们应该将播放状态的暂停。android的四大组件核心就是为了实现移动、或者讲是嵌入式设备上的架构,它们之间有时候是一种相互依赖的关系, 有时候又是一种补充关系,引入广播机制可以极大方便几大组件的信息和数据交流。广播有利于程序间交流消息,例如在自己的应用程序内监听系统来电。
  1. 如何使用广播?

  • 自定义一个BroadcastReceiver,在onReceive()方法中完成广播要处理的事务,比如这里的提示Toast信息。
public class MyReceiver extends BroadcastReceiver{
   @Override
   public void onReceive(Context context, Intent intent) {
       Toast.makeText(context,"你的网络状态发生改变~",Toast.LENGTH_SHORT).show();
  }
}
  • MainActivity.java中动态注册广播:
public class MainActivity extends AppCompatActivity {

   MyBRReceiver myReceiver;

   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_main);
       //核心部分代码:
       myReceiver = new MyReceiver();
       IntentFilter itFilter = new IntentFilter();
       itFilter.addAction("android.net.conn.CONNECTIVITY_CHANGE");
       registerReceiver(myReceiver, itFilter);
  }

   //别忘了将广播取消掉哦~
   @Override
   protected void onDestroy() {
       super.onDestroy();
       unregisterReceiver(myReceiver);
  }
}
  • 我们也可以在AndroidManifest.xml中静态注册:
<receiver android:name=".MyReceiver">
   <intent-filter>
       <action android:name = "android.net.conn.CONNECTIVITY_CHANGE">
   </intent-filter>
</receiver>
  • 动态注册的广播接收器可以自由的控制注册、取消,有很大的灵活性。但是只能在程序启动之后才能收到广播,此外,广播接收器的注销是在onDestroy()方法中的。所以广播接收器的生命周期是和当前活动的生命周期一致。
  • 静态注册的广播不受程序是否启动的约束,当应用程序关闭之后,还是能接收到广播。
  1. 有序广播、无序广播。

  • 按照广播的属性区分,广播分两种:有序广播和无序广播。
  • 无序广播:又叫普通广播,完全异步,不会被某个广播接收者终止,逻辑上可以被任何广播接收者接收到,如果在广播发出之后,所有的广播接收器几乎都会在同时接收到这条广播消息,因此它们之间没有任何先后顺序可言。优点是效率较高。缺点是一个接收者不能将处理结果传递给下一个接收者,并无法停止广播intent的传播。Context.sendBroadcast() 发送的是普通广播,所有订阅者都有机会获得并进行处理。
  • 有序广播:按照被接收者的优先级顺序,在被接收者中依次传播。比如有三个广播接收者A,B,C,优先级顺序是A > B > C。那这个消息先传递给A,再传给B,最后传给C。因此常常需要在AndroidManifest.xml 中进行注册,优先级别声明在intent-filter 元素的android:priority 属性中,数值越大优先级别越高,取值范围:-1000 到1000,优先级别也可以调用IntentFilter 对象的setPriority()进行设置。
  • 有序广播的接收者可以停止广播的传播,广播的传播一旦停止,后面的接收者就无法接收到广播,有序广播的接收者可以将数据传递给下一个接收者,如:A 得到广播后,可以往它的结果对象中存入数据,当广播传给B 时,B 可以从A 的结果对象中得到A 存入的数据。Context.sendOrderedBroadcast() 发送的是有序广播。Bundlebundle = getResultExtras(true))可以获取上一个接收者存入在结果对象中的数据。
  1. Android中常用的系统广播。

  • 系统广播是指系统作为广播的发送方,发出来的广播,我们可以在应用程序中通过监听这些广播来得到各种系统的状态信息。
//关闭或打开飞行模式时的广播
Intent.ACTION_AIRPLANE_MODE_CHANGED;

//充电状态,或者电池的电量发生变化。电池的充电状态、电荷级别改变,不能通过组建声明接收这个广播,只有通过Context.registerReceiver()注册
Intent.ACTION_BATTERY_CHANGED;

//表示电池电量低
Intent.ACTION_BATTERY_LOW;

//表示电池电量充足,即从电池电量低变化到饱满时会发出广播
Intent.ACTION_BATTERY_OKAY;

//在系统启动完成后,这个动作被广播一次(只有一次)。
Intent.ACTION_BOOT_COMPLETED;

//按下照相时的拍照按键(硬件按键)时发出的广播
Intent.ACTION_CAMERA_BUTTON;

//当屏幕超时进行锁屏时,当用户按下电源按钮,长按或短按(不管有没跳出话框),进行锁屏时,android系统都会广播此Action消息
Intent.ACTION_CLOSE_SYSTEM_DIALOGS;

//设备当前设置被改变时发出的广播(包括的改变:界面语言,设备方向,等,请参考Configuration.java)
Intent.ACTION_CONFIGURATION_CHANGED;

//设备日期发生改变时会发出此广播
Intent.ACTION_DATE_CHANGED;
//用户重新开始一个包,包的所有进程将被杀死,所有与其联系的运行时间状态应该被移除,包括包名(重新开始包程序不能接收到这个广播)
Intent.ACTION_PACKAGE_RESTARTED;

//插上外部电源时发出的广播
Intent.ACTION_POWER_CONNECTED;

//已断开外部电源连接时发出的广播
Intent.ACTION_POWER_DISCONNECTED;

Intent.ACTION_PROVIDER_CHANGED;//

//重启设备时的广播
Intent.ACTION_REBOOT;

//屏幕被关闭之后的广播
Intent.ACTION_SCREEN_OFF;

//屏幕被打开之后的广播
Intent.ACTION_SCREEN_ON;

//关闭系统时发出的广播
Intent.ACTION_SHUTDOWN;

//时区发生改变时发出的广播
Intent.ACTION_TIMEZONE_CHANGED;

//时间被设置时发出的广播
Intent.ACTION_TIME_CHANGED;

//广播:当前时间已经变化(正常的时间流逝), 当前时间改变,每分钟都发送,不能通过组件声明来接收
,只有通过Context.registerReceiver()方法来注册
Intent.ACTION_TIME_TICK;

//一个用户ID已经从系统中移除发出的广播
Intent.ACTION_UID_REMOVED;

  1. 广播的原理。

  • 从实现原理看上,Android中的广播运用了观察者模式,基于消息的发布/订阅事件模型。因此,从实现的角度来看,Android中的广播将广播的发送者和接受者进行了极大程度上解耦,使得系统能够容易集成,更易扩展。具体实现流程要点如下:

    1.广播接收者BroadcastReceiver通过Binder机制向AMS(Activity Manager Service)进行注册;

    2.广播发送者通过binder机制向AMS发送广播;

    3.AMS查找符合相应条件(IntentFilter/Permission等)的BroadcastReceiver,将广播发送到BroadcastReceiver(一般情况下是Activity)相应的消息循环队列中;

    4.消息循环执行得到此广播,回调BroadcastReceiver中的onReceive()方法。 对于不同的广播类型,以及不同的BroadcastReceiver注册方式,具体实现上会有不一样。

  • 广播发送者和广播接收者分别属于观察者模式中的消息发布和订阅两端,AMS是处理中心。广播发送者和广播接收者的执行是异步的,发出去的广播不会关心有无接收者接收,也不确定接收者到底是何时才能接收到。
  • 对Binder和AMS知识不熟悉的话可以翻阅前面讲解Binder和AMS的原理章节,学习其中的内容。

总结

有序广播是广播中比较特殊的类型,广播接受者接收广播的时间是不一样的,它们之间是有先后顺序的。系统通过priority的大小来排列优先级别,数值越大级别越高。广播的传播可以依照优先级别逐个传递下去,也可以在某一接收者处中断,那样后面的接收者就无法收到广播。


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

最强实习生!什么?答辩刚结束,领导就通知她转正成功了?

写在前面 熟知我的人应该都知道我是实习转正上岸字节的。 那是一个平平淡淡的下午,leader突然神神秘秘凑到我身边:“一恩,快秋招了。我给你预留了一个HC,快准备准备转正答辩吧。” 于是乎,伴随着leader自以为充满关怀的安排下,我开始轰轰烈烈筹备起自己的转...
继续阅读 »

写在前面


熟知我的人应该都知道我是实习转正上岸字节的。


那是一个平平淡淡的下午,leader突然神神秘秘凑到我身边:“一恩,快秋招了。我给你预留了一个HC,快准备准备转正答辩吧。”

于是乎,伴随着leader自以为充满关怀的安排下,我开始轰轰烈烈筹备起自己的转正大业。


和很多小伙伴一样,我刚刚准备转正时非常茫然无措。因为转正并没有明确的大纲,且不同业务、不同部门考核的形式都是不确定的,在网上搜索经验资料也少得可怜。


别急,转正的内容和形式虽然具有不确定性,但其固有流程又决定了他存在着一定的“潜规则”。下面一恩姐姐就带你发出灵魂三问,深度剖析转正那些不得不说的套路。


灵魂三问


第一问,你了解转正流程吗?


转正流程对于各个公司大同小异。


以字节为例,需要当年毕业的同学,在出勤满40个工作日(技术和测试序列)且经过部门Leader和HR同意后,即有资格发起转正流程。此时HR会根据评估人和候选人的时间,约一个时间组织进行转正答辩。这个短短1个小时的转正答辩,决定了你的去留。

在这里插入图片描述


转正答辩上,一般包含你的HR,部门领导和跨部门的领导。除了跨部门的领导外,其他人都是你在实习过程中可能一起干过饭喝过酒,讨论过诗和远方的伙伴。只要在实习过程中没有发生过什么反目成仇的惨剧,他们都是偏向你的,甚至私下有过“兄弟情义”,“生死之交”还会去引导你去把控答辩的节奏。


比如我就听过自己的同事说过,当时他的导师还在答辩时争着抢着帮他解答领导的问题……

在这里插入图片描述


所以你所需要的做的基本只有一件事:


就是保证转正答辩的过程是顺利的。


整个答辩过程基本分为三块,其中属于你的有效时间仅有两块。第一块为个人展示,你需要以PPT的形式去描述一下实习期间工作,这一块大约有40min;第二块为问答环节,评估人会去根据你的工作与业务询问一些项目及基础知识,这一块大约有20min;第三块为审判环节,评估人会根据转正答辩过程中对你的了解决定你最终的去留。


因此,只有利用好有效的两块时间,才能Hold住整个答辩过程,让评估人被你的魅力闪瞎双眼!


第二问,实习期间的我为团队做了什么?


日常有随时记录工作进度的好习惯,因此我非常迅速地将自己实习阶段的工作按照优先级总结总结写了一下答辩PPT。导师在看了我的草稿后,一个劲儿吐槽:“比起你在这里学的东西,老板们更关心的是你给团队和业务带来的产出,你跟别人做这件事的区别在哪里?你在团队的定位是什么?拿出让他们去选择你的理由吧!


这与我一开始想的完全不一样。我本以为答辩就是汇报自己学了什么,做了什么。但其实不是,公司看中的是你的个人想法和价值实现,以及你身上是否有可输出的内容。


你的一言一行都要表达出:你是完全能胜任这个职位的。


想明白这点,我重新组织了自己的PPT和答辩的内容:


首先,我用了一页画了一个时间轴,分别用关键词总结每一part工作主要内容,核心,和工作亮点项目。这一部分重在简洁清晰。目的是让评审人清晰的了解我的工作内容重点和核心。


接下来,我选择2~3个核心项目详细地介绍工作内容并量化自己的产出。如果大家不清楚如何介绍的话,可以参考金字塔原理中 先总后分的表达方式——先给你的听众一个核心结论,在后面逐层展开。



比如我去介绍自己做多人视频通话这个需求时,首先需求的背景是需要支持多个人一起视频通话,我的主要工作是技术方案的设计与开发,具体工作是通过获取多路视频流,并将视频流分给对应的成员,因此我需要去维护所有成员的视图窗口以及流的稳定性与正确性。为了实现这个功能,我去了解了视频流编码,推拉流的逻辑,并且与多媒体业务同学进行了沟通,保证整体形成一条稳定的通路。


(截图取自我的PPT答辩文档,针对强化通话感知的需求,我列出了需求的目标,以及技术方案,并采用流程图方便说明,以及最后写上了需求的收益)



第三部分我会去对自己的价值角色进行提炼,即向评估人去证明自己的独特价值以及在团队中的定位。如果你不知道如何去证明,那就将这个问题回答好:凭什么别人要选择你而不选择别人?


最后一部分可以向评估人讲述一下自己的期望和未来的规划,我当时是舒情并茂地表达了自己对团队的热爱和对前景的向往,并表达了自己对未来的无限期盼。说的导师当场差点“热泪盈眶”。


以及提供给大家一个小妙招,作为一名研发,如果拥有产品思维,无疑是非常加分的。因此大家可以对自己所在的业务从产品本身进行思考,比如能做些什么才能让产品吸引更多用户,以及在产品上有什么意见和规划。


第三问,基础知识还记得吗?


在40分钟ShowTime之后,剩余20分钟评估人可能会针对你的某个具体项目询问一些实现上的细节,也有可能会询问一些技术方案设计上的问题。因此需要保证你所介绍的每一个项目都是你切身参与且明确其中实现的技术方案与细节,而且你应该提前去准备一些代码或技术上可扩展或优化的思考,来体现出你对项目的一种全局的视角。


同时评估人也会针对你目前所处团队的业务特性去询问一些基础问题,这一点和面试比较像,虽然难度比较于面试会简单很多。但也需要去多少刻意准备一些基础知识。比如我做视频通话业务,当时评估人就问我,你觉得通话传输的音视频流信息是通过udp还是tcp传输的,以及他们的区别。


这些问题是不是对于现在的你实在太简单了?


一个日常实习阶段小tip


不清楚大家在日常工作的过程中有没有对自己工作进行总结的习惯。如果没有,请从现在开始,立!刻!记!录!


“记录”这个行为听起来难度很高,其实真正实施起来你会发现它就像一种“陪伴”,非常潜移默化地融入你的生活中。


我会在日常工作过程中我会将自己的每一份思考和产出都落地文档并定时整理与复盘,每周五下班前会抽出15分钟将本周的工作以及下周需要做的事情整理成一个TODO列表。且会以月为纬度进行一次工作量和心态的反思,并与导师进行一次整体沟通,这种定期的总结和复盘能够让我永远对自己保持清醒。


当我整理自己实习工作时,这些文字更是我的宝藏,我能很清楚地看到自己日积月累的自我升级,并非常轻松地以时间线的角度看出自己各个阶段的产出。


写在最后


希望大家在实习期间一直保持一个谦卑学习的态度,正式阶段繁重的工作压力会让你没有过多心思去进行一些软硬实力的提高。


因此实习是一个非常好的机会去适应、去成长,一定要耐心地倾听、观察,向身边优秀的同事学习。


相信在以后的工作中,你一定也能如鱼得水,熠熠生辉。




FAQ时间


Q1:工作上犯了个常识性错误,感觉转正无望,该不该及时止损?

首先,要明白,作为实习生,犯错是一件正常的事。错误才能让你意识到不足,才能成长。转正评估的不是你的过去,而是你的价值和你可以塑造的可能性。如果你能对自己过去的工作上的错误进行复盘与总结,并且能够对未来进行合理的规划。相信你也能给出一份完美的转正答卷。


Q2:秋招无望走实习转正是否可行?

这个选择是完全没有问题的。实习不仅能够提高转正的几率,也是给你一定机会提前感受一下社会环境,在体验过真实互联网工作环境后,有些人会明白自己是否合适,才会有更精确的职业规划。


新增一个小栏目,收集着目前为止小伙伴们私信一恩的一些关于实习转正问题的答复。如果大家还有其他问题欢迎继续在评论区回复,一恩会一一回答的~


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

聊聊Flutter中json序列化和反序列化

Flutter中是否有类似于GSON/Jackson/Moshi的json序列化插件?没有,因为这样的插件使用了反射,反射在flutter中是被禁止使用的,这个是因为在Dart中存在Tree Shaking功能,Tree Shaking可以在release b...
继续阅读 »

Flutter中是否有类似于GSON/Jackson/Moshi的json序列化插件?

没有,因为这样的插件使用了反射,反射在flutter中是被禁止使用的,这个是因为在Dart中存在Tree Shaking功能,Tree Shaking可以在release build去掉无用的代码, 而反射会对Tree Shaking功能造成影响.

在Dart中有两种json序列化

第一种:手动序列化

第二种:使用代码生成的方式自动的序列化

对于一些小工程或者demo应用来说手动序列化是一个比较方便的方式,在大型工程中使用代码生成的方式自动的序列化可以减少工作量,而且避免拼写错误的情况.

第一种:手动序列化

Flutter 中提供一个dart:convert库包含一个json encoder和decoder用来序列化和反序列化. 如下我要序列化下面这段json内容.

{
"name": "John Smith",
"email": "john@example.com"
}

这里有2种序列化使用方式.

内联序列化Json

Map<String, dynamic> user = jsonDecode(jsonString);

print('Howdy, ${user['name']}!');
print('We sent the verification link to ${user['email']}.');

这种序列化有2个问题

1,非类型安全

2,容易出现拼写错误

序列化Json到模型类中

为了解决上面提到的2个问题,我们把json转化为模型类,需要创建一个User模型

class User {
final String name;
final String email;

User(this.name, this.email);

User.fromJson(Map<String, dynamic> json)
: name = json['name'],
email = json['email'];

Map<String, dynamic> toJson() => {
'name': name,
'email': email,
};
}

这里我们多了2个方法:

1,fromJson用于使用一个json map构造一个User对象.

2, toJson把该对象转换成一个json map

decode过程放在模型类里

Map<String, dynamic> userMap = jsonDecode(jsonString);
var user = User.fromJson(userMap);

print('Howdy, ${user.name}!');
print('We sent the verification link to ${user.email}.');

encode直接调用dart:convert库中的jsonEncode既可.

String json = jsonEncode(user);

第二种:使用代码生成的方式自动的序列化

如果要使用自动生成序列化代码需要添加依赖

flutter pub add json_annotation dev:build_runner dev:json_serializable

这里我们还是创建一个模型

import 'package:json_annotation/json_annotation.dart';

part 'user.g.dart';

@JsonSerializable()
class User {
User(this.name, this.email);

String name;
String email;

factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);

Map<String, dynamic> toJson() => _$UserToJson(this);
}

JsonSerializable 代表我需要序列化该类 JsonKey 用这个注解我可以重新对成员进行命名,因为有些情况是json中的key是蛇形的,例如

@JsonKey(name: 'registration_date_millis')
final int registrationDateMillis;

我们在命令行运行

flutter pub run build_runner build --delete-conflicting-outputs

既可以生成对应的对应的序列化代码.

嵌套类的情况 有时候我们会遇到一个类中嵌套一个类的情况, 例如

import 'package:json_annotation/json_annotation.dart';
part 'address.g.dart';

@JsonSerializable()
class Address {
String street;
String city;

Address(this.street, this.city);

factory Address.fromJson(Map<String, dynamic> json) =>
_$AddressFromJson(json);
Map<String, dynamic> toJson() => _$AddressToJson(this);
}

这个Address嵌套在User中.

import 'package:json_annotation/json_annotation.dart';

import 'address.dart';

part 'user.g.dart';

@JsonSerializable()
class User {
User(this.name, this.address);

String name;
Address address;

factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
Map<String, dynamic> toJson() => _$UserToJson(this);
}

我们运行命令后看到这里输出的序列化代码

Map<String, dynamic> _$UserToJson(User instance) => <String, dynamic>{
'name': instance.name,
'address': instance.address,
};

这里address赋值了一个引用类型,显然不是需要的,我们需要的是嵌套类的json。这里我们要在JsonSerialization中加上explicitToJson: true参数

import 'package:json_annotation/json_annotation.dart';

import 'address.dart';

part 'user.g.dart';

@JsonSerializable(explicitToJson: true)
class User {
User(this.name, this.address);

String name;
Address address;

factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
Map<String, dynamic> toJson() => _$UserToJson(this);
}

总结

Flutter的序列化和反序列方式相对于Android原生平台更为繁琐一些,我们可以根据项目的情况选择手动进行序列化,也可以通过注解进行生成.


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

Activity界面路由的一种简单实现

1. 引言平时Android开发中,启动Activity是非常常见的操作,而打开一个新Activity可以直接使用Intent,也可以每个Activity提供一个静态的启动方法。但是有些时候使用这些方法并不那么方便,比如:一个应用内的网页需要打开一个原生Act...
继续阅读 »

1. 引言

平时Android开发中,启动Activity是非常常见的操作,而打开一个新Activity可以直接使用Intent,也可以每个Activity提供一个静态的启动方法。但是有些时候使用这些方法并不那么方便,比如:一个应用内的网页需要打开一个原生Activity页面时。这种情况下,网页的调用代码可能是app.openPage("/testPage")这样,或者是用app.openPage("local://myapp.com/loginPage")这样的方式,我们需要用一种方式把路径和页面关联起来。Android可以允许我们在Manifest文件中配置<data>标签来达到类似效果,也可以使用ARouter框架来实现这样的功能。本文就用200行左右的代码实现一个类似ARouter的简易界面路由。

2. 示例

2.1 初始化

这个操作建议放在Application的onCreate方法中,在第一次调用Router来打开页面之前。

public class AppContext extends Application {
@Override
public void onCreate() {
super.onCreate();
Router.init(this);
}
}

2.2 启动无参数Activity

这是最简单的情况,只需要提供一个路径,适合“关于我们”、“隐私协议”这种简单无参数页面。

Activity配置:

@Router.Path("/testPage")
public class TestActivity extends Activity {
//......
}

启动代码:

Router.from(mActivity).toPath("/testPage").start();
//或
Router.from(mActivity).to("local://my.app/testPage").start();

2.3 启动带参数Activity

这是比较常见的情况,需要在注解中声明需要的参数名称,这些参数都是必要参数,如果启动的时候没有提供对应参数,则发出异常。

Activity配置:

@Router.Path(value = "/testPage",args = {"id", "type"})
public class TestActivity extends Activity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//加载布局...

String id = getIntent().getStringExtra("id"); //获取参数
int type = getIntent().getIntExtra("type", 0);//获取参数
}
}

启动代码:

Router.from(mActivity).toPath("/testPage").with("id", "t_123").with("type", 1).start();

2.4 启动带有静态启动方法的Activity

有一些Activity需要通过它提供的静态方法启动,就可以使用Path中的method属性和Entry注解来声明入口,可以提供参数。在提供了method属性时,需要用Entryargs来声明参数。

Activity配置:

@Router.Path(value = "/testPage", method = "open")
public class TestActivity extends Activity {

@Router.Entry(args = {"id", "type"})
public static void open(Activity activity, Bundle args) {
Intent intent = new Intent(activity, NestWebActivity.class);
intent.putExtras(args);
activity.startActivity(intent);
}

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//加载布局...

String id = getIntent().getStringExtra("id"); //获取参数
int type = getIntent().getIntExtra("type", 0);//获取参数
}
}

启动代码:

Router.from(mActivity).toPath("/testPage").with("id", "t_123").with("type", 1).start();

3. API介绍

3.1 Path注解

这个注解只能用于Activity的子类,表示这个Activity需要页面路由的功能。这个类有三个属性:

  • value:表示这个Activity的相对路径。
  • args:表示这个Activity需要的参数,都是必要参数,如果打开页面时缺少指定参数,就会发出异常。
  • method:如果这个Activity需要静态方法做为入口,就将这个属性指定为方法名,并给对应方法添加Entry注解。(注意:这个属性值不为空时,忽略这个注解中的args属性内容)

3.1 Entry注解

这个注解只能用于Activity的静态方法,表示这个方法作为打开Activity的入口。仅包含一个属性:

  • args:表示这个方法需要的参数。

3.2 Router.init方法

  • 方法签名:public static void init(Context context)
  • 方法说明:这个方法用于初始化页面路由表,必须在第一次用Router打开页面之前完成初始化。建议在Application的onCreate方法中完成初始化。

3.3 Rouater.from方法

  • 方法签名:public static Router from(Activity activity)
  • 方法说明:这个方法用于创建Router实例,传入的参数通常为当前Activity。例如,要从AActivity打开BActivity,那么传入参数为AActivity的实例。

3.4 Rouater.to和Rouater.toPath方法

  • 方法签名:
  1. public RouterBuilder to(String urlString)
  2. public RouterBuilder toPath(String path)
  • 方法说明:这个方法用于指定目标的路径,to需要执行绝对路径,而toPath需要指定相对路径。返回的RouterBuilder用于接收打开页面需要的参数。

3.4 RouterBuilder.with方法

  • 方法签名:
  1. public RouterBuilder with(String key, String value)
  2. public RouterBuilder with(String key, int value)
  • 方法说明:这个方法用于添加参数,对应Bundle的各个put方法。目前只有常用的Stringint两个类型。如有需要可自行在RouterBuilder中添加对应的方法。

3.4 RouterBuilder.start方法

  • 方法签名:public void start()
  • 方法说明:这个方法用于打开页面。如果存在路径错误、参数错误等异常情况,会发出对应运行时异常。

4. 实现

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;

import androidx.annotation.Keep;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;

@Keep
public class Router {

public static final String SCHEME = "local";
public static final String HOST = "my.app";
public static final String URL_PREFIX = SCHEME + "://" + HOST;

private static final Map<String, ActivityStarter> activityPathMap = new ConcurrentHashMap<>();

public static void init(Context context) {
try {
PackageManager packageManager = context.getPackageManager();
PackageInfo packageInfo = packageManager.getPackageInfo(
context.getPackageName(), PackageManager.GET_ACTIVITIES);

for (ActivityInfo activityInfo : packageInfo.activities) {
Class<?> aClass = Class.forName(activityInfo.name);
Path annotation = aClass.getAnnotation(Path.class);
if (annotation != null && !TextUtils.isEmpty(annotation.value())) {
activityPathMap.put(annotation.value(), (Activity activity, Bundle bundle) -> {
if (TextUtils.isEmpty(annotation.method())) {
for (String arg : annotation.args()) {
if (!bundle.containsKey(arg)) {
throw new IllegalArgumentException(String.format("Bundle does not contains argument[%s]", arg));
}
}
Intent intent = new Intent(activity, aClass);
intent.putExtras(bundle);
activity.startActivity(intent);
} else {
try {
Method method = aClass.getMethod(annotation.method(), Activity.class, Bundle.class);
Entry entry = method.getAnnotation(Entry.class);
if (entry != null) {
for (String arg : entry.args()) {
if (!bundle.containsKey(arg)) {
throw new IllegalArgumentException(String.format("Bundle does not contains argument[%s]", arg));
}
}
method.invoke(null, activity, bundle);
} else {
throw new IllegalStateException("can not find a method with [Entry] annotation!");
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
});
}
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}

public static Router from(Activity activity) {
return new Router(activity);
}

private final Activity activity;

private Router(Activity activity) {
this.activity = activity;
}

public RouterBuilder to(String urlString) {
if (TextUtils.isEmpty(urlString)) {
return new ErrorRouter(new IllegalArgumentException("argument [urlString] must not be null"));
} else {
return to(Uri.parse(urlString));
}
}

public RouterBuilder toPath(String path) {
return to(Uri.parse(URL_PREFIX + path));
}

public RouterBuilder to(Uri uri) {
try {
if (SCHEME.equals(uri.getScheme())) {
if (HOST.equals(uri.getHost())) {
String path = uri.getPath();//note: 二级路径暂不考虑
ActivityStarter starter = activityPathMap.get(path);
if (starter == null) {
throw new IllegalStateException(String.format("path [%s] is not support", path));
} else {
NormalRouter router = new NormalRouter(activity, starter);
for (String key : uri.getQueryParameterNames()) {
if (!TextUtils.isEmpty(key)) {
router.with(key, uri.getQueryParameter(key));
}
}
return router;
}
} else {
throw new IllegalArgumentException(String.format("invalid host : %s", uri.getHost()));
}
} else {
throw new IllegalArgumentException(String.format("invalid scheme : %s", uri.getScheme()));
}
} catch (RuntimeException e) {
return new ErrorRouter(e);
}
}

public static abstract class RouterBuilder {
public abstract RouterBuilder with(String key, String value);

public abstract RouterBuilder with(String key, int value);

public abstract void start();
}


private static class ErrorRouter extends RouterBuilder {
private final RuntimeException exception;

private ErrorRouter(RuntimeException exception) {
this.exception = exception;
}

@Override
public RouterBuilder with(String key, String value) {
return this;
}

@Override
public RouterBuilder with(String key, int value) {
return this;
}

@Override
public void start() {
throw exception;
}
}

private static class NormalRouter extends RouterBuilder {
final Activity activity;
final Bundle bundle = new Bundle();
final ActivityStarter starter;

private NormalRouter(Activity activity, ActivityStarter starter) {
this.activity = Objects.requireNonNull(activity);
this.starter = Objects.requireNonNull(starter);
}

@Override
public RouterBuilder with(String key, String value) {
bundle.putString(key, value);
return this;
}

@Override
public RouterBuilder with(String key, int value) {
bundle.putInt(key, value);
return this;
}

@Override
public void start() {
starter.start(activity, bundle);
}
}

@FunctionalInterface
private interface ActivityStarter {
void start(Activity activity, Bundle bundle);
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Path {
String value();

String method() default "";

String[] args() default {};
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Entry {
String[] args() default {};
}
}

5. 注意

  1. 这个工具的一些功能与ARouter类似,实际项目中建议使用ARouter。如果有特殊需求,例如,页面参数的检查或定制具体打开行为,可以考虑基于这个工具进行修改。
  2. 使用了Pathmethod属性时注意添加对应的混淆设置,避免因混淆而导致找不到对应方法。

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

Android SpannableStringBuilder 持久化探索

问题业务上需要将一些数据缓存到本地,思路是定义个类,赋值后使用 Gson 转换为 Json 数据存到本地。但是由于需要 SpannableStringBuilder 来保存Text的富文本属性,尝试序列化会 Json 后,再反序列化为 SpannableStr...
继续阅读 »

问题

业务上需要将一些数据缓存到本地,思路是定义个类,赋值后使用 Gson 转换为 Json 数据存到本地。但是由于需要 SpannableStringBuilder 来保存Text的富文本属性,尝试序列化会 Json 后,再反序列化为 SpannableStringBuilder 赋值给 TextView 会有一些意外的错误。

Stack trace:  
java.lang.IndexOutOfBoundsException: setSpan (0 ... -1) has end before start
at android.text.SpannableStringInternal.checkRange(SpannableStringInternal.java:485)
at android.text.SpannableStringInternal.setSpan(SpannableStringInternal.java:199)
at android.text.SpannableStringInternal.copySpansFromSpanned(SpannableStringInternal.java:87)
at android.text.SpannableStringInternal.<init>(SpannableStringInternal.java:48)
at android.text.SpannedString.<init>(SpannedString.java:35)
at android.text.SpannedString.<init>(SpannedString.java:44)
at android.text.TextUtils.stringOrSpannedString(TextUtils.java:532)
at android.widget.TextView.setText(TextView.java:6318)
at android.widget.TextView.setText(TextView.java:6227)
at android.widget.TextView.setText(TextView.java:6179)

探索

SpannableString

起初尝试将 SpannableStringBuilder 转为 SpannableString:

val spannableStringBuilder = SpannableStringBuilder("测试文本")
val spannableString = SpannableString.valueOf(spannableStringBuilder)

虽然恢复数据时不会报错,但 SpannableString 的属性全部消失了。

Html

于是开始检索如何持久化 SpannableStringBuilder, 在 Stackoverflow 上有这么一个方案

android: how to persistently store a Spanned?

其中提到需要可以使用 Android 的 Html 类的 Html.toHtml 方法将 SpannableStringBuilder 数据转换为 html 的标签语言,恢复时再使用 Html.fromHtml

val spannableStringBuilder = SpannableStringBuilder("测试文本")

val htmlString = Html.toHtml(spannableStringBuilder)

val spannableStringBuilder = Html.fromHtml(htmlString)

测试了一个,以上方式确实是一个顺利解决的崩溃问题。需要注意的是,Html 的两个方法都是耗时方法,最好异步调用。

自定义 Gson 序列化和反序列化适配器

项目的 Json 解析框架使用的是 Gson,支持自定义序列化和反序列化。于是,编写一个适配器实现 JsonSerializer和 JsonDeserializer

class SpannableStringBuilderTypeAdapter : JsonSerializer<SpannableStringBuilder>,
JsonDeserializer<SpannableStringBuilder> {
override fun serialize(
src: SpannableStringBuilder?,
typeOfSrc: Type?,
context: JsonSerializationContext?
): JsonElement {
return src?.let {
JsonPrimitive(Html.toHtml(src))
} ?: JsonPrimitive("")
}

override fun deserialize(
json: JsonElement?,
typeOfT: Type?,
context: JsonDeserializationContext?
): SpannableStringBuilder {
return json?.let {
val fromHtml = Html.fromHtml(json.asString).trim()
SpannableStringBuilder(fromHtml)
} ?: SpannableStringBuilder("")
}
}

//使用
Gson gson = new GsonBuilder()
.setDateFormat("yyyy-MM-dd hh:mm:ss")
.registerTypeAdapter(SpannableStringBuilder.class,
new SpannableStringBuilderTypeAdapter())
.create();

以上代码可以很好的工作,如果细心的话,可以注意到反序列化时用到 trim(),因为反序列化为 SpannableStringBuilder 后字符串末尾会多处两个换行符,这个 Stackoverflow 有提到HTML.fromHtml adds space at end of text?

总结,这次探索让我对持久化多了一些思路,对于一些无法修改源码的类可以自定义适配器来序列化。


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

你真的理解 JavaScript 中的 “this” 了

前言JavaScript 中的 this 是一个非常重要的概念,也是一个令新手开发者甚至有些不深入理解的多年经验开发者都会感到困惑的概念。如果你希望自己能够使用 this 编写更好的代码或者更好理解他人的代码,...
继续阅读 »

前言

JavaScript 中的 this 是一个非常重要的概念,也是一个令新手开发者甚至有些不深入理解的多年经验开发者都会感到困惑的概念。

如果你希望自己能够使用 this 编写更好的代码或者更好理解他人的代码,那就跟着我一起理解一下this吧。

要理解this的原因

我们先搞清楚为什么要理解 this 的,再去学习它。

  • 学习 this 可以帮助我们更好地理解代码的上下文和执行环境,从而编写更好的代码。

例1

function speakfullName(){
console.log(this.firstname + this.lastname)
}

var firstname = '南'
var lastname = '墨'

const gril = {
firstname: '黎',
lastname: '苏苏',
speakfullName,
}

const boy = {
firstname: '澹台',
lastname: '烬',
speakfullName,
}

gril.speakfullName(); // 输出: 黎苏苏
boy.speakfullName(); // 输出: 澹台烬
speakfullName(); // 输出: 南墨

在这个例子中,如果你没理解 this 的用法,那么阅读这段代码就会觉得奇怪,为什么同一个函数会输出不同的结果。之所以奇怪,是因为你不知道他的上下文到底是什么。

  • 学习 this 可以帮助我们编写更具可重用性和可维护性的代码

在例1中可以在不同的上下文中使用 this,不用针对不同版本写不同的函数。当然不使用 this,也是可以的。

例2

function speakfullName(person){
console.log(person.firstname + person.lastname)
}

const gril = {
firstname: '黎',
lastname: '苏苏',
}

const boy = {
firstname: '澹台',
lastname: '烬',
}

speakfullName(gril); // 黎苏苏
speakfullName(boy); // 澹台烬

虽然目前这段代码没有问题,如果后续使用的模式越来越复杂,那么这样的显示传递会让代码变得难以维护和重用,而this的隐式传递会显得更加优雅一些。因此,学习this可以帮助我们编写更具有可重用性和可维护性的代码。

接下来我们开始正式全面解析 this

解析 this

我相信大家多多少少都理解一些 this 的用法,但可能不够全面,所以接下来我们就全面性的理解 this

很多人可能认为this 写在哪里就是指向所在位置本身,如下代码:

var a = 2
function foo (){
var a = 1
console.log(this.a)
}
foo();

有些人认为会输出1,实际是输出2,这就是不够理解 this 所产生的的误会。

this的机制到底是什么样的呢?

其实this不是写在哪里就被绑定在哪里,而是代码运行的时候才被绑定的。也就是说如果一个函数中存在this,那么this到底被绑定成什么取决于这个函数以什么样的方式被调用。

既然已经提出了这样一个机制,那我们该如何根据这个机制,去理解和判断this被绑定成什么呢?

下面我们继续介绍这个机制的基本原理。

调用位置

上面说过,函数的调用位置会影响this被绑定成什么了,所以我们需要知道函数在哪里被调用了。

我们回头去看一下 例1,来理解什么是调用位置:

// ...
gril.speakfullName(); // 输出: 黎苏苏
boy.speakfullName(); // 输出: 澹台烬
speakfullName(); // 输出: 南墨

同一个函数 speakfullName, 在不同的调用位置,它的输出就不一样。

在 gril 对象中调用时,输出了黎苏苏,在 boy 对象中调用时,输出了澹台烬,在全局调用时输出了南墨

当然例子中的调用位置是非常容易看出来的。所以我们接下来继续讲解在套多层的情况下如何找到调用位置。

我们要找到调用位置就要分析调用栈。

看下简单例子:

function baz() {
// 当前调用栈:baz
console.log('baz')
bar(); // bar 调用的位置
}

function bar() {
// 当前调用栈:baz-bar
console.log('bar')
foo(); // foo 调用的位置
}

function foo() {
// 当前调用栈:baz-bar-foo
console.log('foo')
}
baz() // baz的调用位置

其实调用栈就是调用位置的链条,就像上面代码例子中所分析的一样。不过在一些复杂点的代码中,这样去分析很容易出错。所以我们可以用现代浏览器的开发者工具帮助我们分析。

比如上例中,我们想找到 foo 的调用位置,在 foo 中第一行输入debugger

// ...

function foo() {
debugger
// ...
}
// ...

或者打开浏览器的开发者工具到源代码一栏找到,foo的代码的第一行打一个断点也行,如下图:

1682495831231.png

接着在源代码一栏,找到调用堆栈的foo的下一个就是bar,bar就是foo的调用位置。

绑定规则

接下来看看调用位置如何决定this被绑定成什么,并且进行总结。

默认规则

第一种情况是函数最经常被调用的方式,函数被单独调用。看以下例子:

var name = '澹台烬'
function fn(){
console.log('我是' + this.name)
}
fn() // 我是澹台烬

运行fn后,最终输出了 我是澹台烬。众所周知,上例中的 name 是全局的变量,这样说明了fn中的 this.name 被绑定成了全局变量name。因此,this指向了全局对象。

因为在上例的代码片段中,foo的调用位置是在全局中调用的,没有其他任何修饰, 所以我们称之为默认规则

使用了严格模式的话,上例代码会出现什么样的情况呢?

var name = '澹台烬'
function sayName(){
"use strict"
console.log(this) // (1)
console.log('我是' + this.name) // (2)
}
fn()
// undefined
// TypeError: cannot read properties of undefined (reading 'name') as sayName

可以看出来(1)也就是this,输出了undefiend 所以(2)就直接报错了。

因此我们可以得出默认规则的结论:在非严格模式下,this默认绑定成全局对象,在严格模式下,this 被绑成 undefined

隐式绑定

这条规则需要我们去判断函数的调用是否有上下文对象,也就是说函数调用的时候前面是否跟了一个对象,举个例子看下。

function sayName() {
console.log(`我是` + this.name)
}
var person = {
name: '澹台烬',
sayName,
}
person.sayName(); // 我是澹台烬

在这个例子中, sayName 前面有一个 person,也就是说 sayName 函数有一个上下文对象person, 这样调用 sayName 的时候,函数中 this 被绑定成了person,因此 this.name 和 person.name 是一样的。

在观察隐式绑定的时候,有两种值得我们注意的情况:

  • 如果说一个函数是通过对象的方式调用时,只有最后一个对象起到上下文的作用。 例3:
function sayName() {
console.log(`我是` + this.name)
}

var child = {
name: '澹台烬',
sayName,
}

var father = {
name: '澹台无极',
child,
}

father.child.sayName(); // 我是澹台烬

这个例子中,是通过一个对象链调了sayName,没有输出我是澹台无极,而是我是澹台烬。因此 this 指向了child 对象,说明this 最终绑定为对象链的最后一个对象。

  • 隐式丢失的情况就是被隐式绑定的函数丢失绑定的上下文,转而变成了应用默认绑定。
function sayName() {
console.log(`我是` + this.name)
}
var person = {
name: '澹台烬',
sayName,
}

var personSayName = person.sayName;

var name = '南墨'

pesonSayName() // '我是南墨'

虽然 personSayName 看起来是由 person.sayName 赋值的来,拥有上下文对象person,但实际上 personSayName 被赋予的是 sayName 函数本身,因此此时的 personSayName其实是一个不带修饰的函数, 所以说会被认为是默认绑定。

显示绑定

隐式绑定是通过一个看起来不经意间的上下文的形式去绑定的。

那也当然也有通过一个明显的上下文强制绑定的,这就是显示绑定

在 javaScript 中,要是使用显示绑定,就要通过 call 和 apply方法去强制绑定上下文了

这两个方法的使用方法是什么样的呢? call 和 apply的第一个参数的是一样的,就是传入一个我们想要给函数绑定的上下文。

来看一下下面的例子

function sayName () {
console.log(this.name)
}

var person = {
name: 南墨
}

sayName.call(person) // 南墨

看到没? 我们通过call的方式,将函数的上下文绑定为了 person,因此打印出了 南墨

使用了 call 绑定也会有绑定丢失的情况,所以我们需要一种保证在我意料之内的办法, 可以改造显示绑定,思考如下代码:


function sayName() {
console.log(this.name)
}

var person = {
name: 南墨
}

function sayNanMo() {
sayName.call(person);
}

sayNanMo() // 南墨
setTimeout(sayNanMo, 10000) // 南墨
sayNanMo.call(window) // 南墨

这样一来,不管怎么操作,都会输出南墨,是我想要的结果

我们将 sayName.call(person) 放在的 sayNanMo 中,因此sayName 只能被绑定为我们想要绑定的 person

我们可以将其写成可复用的函数

function bind(fn, obj) {
return function() {
fn.apply(obj, arguments)
}
}

ES5 就提供了内置的方法 Function.prototype.bind,用法如下:

function sayName() {
console.log(this.name)
}

var person = {
name: 南墨
}

sayName.bind(person)

new绑定

new 绑定也可以影响函数调用时的 this 绑定行为,我们称之为new 绑定。

思考如下代码:

function person(name) {
this.name = name;
this.sayName = function() {
console.log(this.name)
}
}

var personOne = new person('南墨')

personOne.sayName() // 南墨

personOne.sayName 能够输出南墨,是因为使用 new 调用 person 时,会创建一个新的对象并将它绑定到 person中的 this 上,所以personOne.sayName中的 this.name 等于外面的this.name

规则之外

值得一提的是,ES6的箭头函数,它的this无法使用以上四个规则,而是而是根据外层(函数或者全局)作用域来决定this。

  function sayName () {
return () => {
console.log(this.name)
}
}

var person1 = {
name: 南墨
}

var person2 = {
name: '澹台烬'
}
sayName.call(person1)
sayName.call(person1).call(person2) // 澹台烬,如果是普通函数会输南墨
}

总结

要想判断一个运行的函数中this的绑定,首先要找到函数调用位置,因为它会影响this的绑定。然后使用四个绑定规则:new绑定、显示绑定、隐式绑定、默认规则 来判断this的绑定。


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

安卓拍照、裁切、选取图片实践

安卓拍照、裁切、选取图片实践前言最近项目里面有用到裁切功能,没弄多复杂,就是系统自带的,顺便就总结了一下系统拍照、裁切、选取的使用。网上的资料说实话真是没什么营养,但是Android官网上的说明也有点太简单了,真就要实践出真理。拍照本来拍照是没什么难度的,不就...
继续阅读 »

安卓拍照、裁切、选取图片实践

前言

最近项目里面有用到裁切功能,没弄多复杂,就是系统自带的,顺便就总结了一下系统拍照、裁切、选取的使用。网上的资料说实话真是没什么营养,但是Android官网上的说明也有点太简单了,真就要实践出真理。

拍照

本来拍照是没什么难度的,不就是调用intent去系统相机拍照么,但是由于文件权限问题,Uri这东西就能把人很头疼。下面是代码(onActivityResult见后文):

    private fun openCamera() {
val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
// 应用外部私有目录:files-Pictures
val picFile = createFile("Camera")
val photoUri = getUriForFile(picFile)
// 保存路径,不要uri,读取bitmap时麻烦
picturePath = picFile.absolutePath
// 给目标应用一个临时授权
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
//android11以后强制分区存储,外部资源无法访问,所以添加一个输出保存位置,然后取值操作
intent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri)
startActivityForResult(intent, REQUEST_CAMERA_CODE)
}

private fun createFile(type: String): File {
// 在相册创建一个临时文件
val picFile = File(requireContext().getExternalFilesDir(Environment.DIRECTORY_PICTURES),
"${type}_${System.currentTimeMillis()}.jpg")
try {
if (picFile.exists()) {
picFile.delete()
}
picFile.createNewFile()
} catch (e: IOException) {
e.printStackTrace()
}

// 临时文件,后面会加long型随机数
// return File.createTempFile(
// type,
// ".jpg",
// requireContext().getExternalFilesDir(Environment.DIRECTORY_PICTURES)
// )

return picFile
}

private fun getUriForFile(file: File): Uri {
// 转换为uri
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
//适配Android 7.0文件权限,通过FileProvider创建一个content类型的Uri
FileProvider.getUriForFile(
requireActivity(),
"com.xxx.xxx.fileProvider", file
)
} else {
Uri.fromFile(file)
}
}

简单说明

这里的file是使用getExternalFilesDir和Environment.DIRECTORY_PICTURES生成的,它的文件保存在应用外部私有目录:files-Pictures里面。这里要注意不能存放在内部的私有目录里面,不然是无法访问的,外部私有目录虽然也是私有的,但是外面是可以访问的,这里拿官网上的说明:

在搭载 Android 9(API 级别 28)或更低版本的设备上,只要您的应用具有适当的存储权限,就可以访问属于其他应用的应用专用文件。为了让用户更好地管理自己的文件并减少混乱,以 Android 10(API 级别 29)及更高版本为目标平台的应用在默认情况下被授予了对外部存储空间的分区访问权限(即分区存储)。启用分区存储后,应用将无法访问属于其他应用的应用专属目录。

Uri的获取

再一个比较麻烦的就是Uri的获取了,网上有一大堆资料,不过我这也贴一下,网上的可能有问题。

manifest.xml

        <provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.xxx.xxx.fileProvider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"
/>
</provider>

res -> xml -> file_paths.xml

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<!--1、对应内部内存卡根目录:Context.getFileDir()-->
<files-path
name="int_root"
path="/" />
<!--2、对应应用默认缓存根目录:Context.getCacheDir()-->
<cache-path
name="app_cache"
path="/" />
<!--3、对应外部内存卡根目录:Environment.getExternalStorageDirectory()-->
<external-path
name="ext_root"
path="/" />
<!--4、对应外部内存卡根目录下的APP公共目录:Context.getExternalFileDir(String)-->
<external-files-path
name="ext_pub"
path="/" />
<!--5、对应外部内存卡根目录下的APP缓存目录:Context.getExternalCacheDir()-->
<external-cache-path
name="ext_cache"
path="/" />
</paths>

ps. 注意authorities这个最好填自己的包名,不然有两个应用用了同样的authorities,后面的应用会安装不上。

打开相册

这里打开相册用的是SAF框架,使用intent去选取(onActivityResult见后文)。

    private fun openAlbum() {
val intent = Intent()
intent.type = "image/*"
intent.action = "android.intent.action.GET_CONTENT"
intent.addCategory("android.intent.category.OPENABLE")
startActivityForResult(intent, REQUEST_ALBUM_CODE)
}

裁切

裁切这里比较麻烦,参数比较多,而且Uri那里有坑,不能使用provider,再一个就是图片传递那因为安卓版本变更,不会传略缩图了,很坑。

    private fun cropImage(path: String) {
cropImage(getUriForFile(File(path)))
}

private fun cropImage(uri: Uri) {
val intent = Intent("com.android.camera.action.CROP")
// Android 7.0需要临时添加读取Url的权限
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
// intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
intent.setDataAndType(uri, "image/*")
// 使图片处于可裁剪状态
intent.putExtra("crop", "true")
// 裁剪框的比例(根据需要显示的图片比例进行设置)
// if (Build.MANUFACTURER.contains("HUAWEI")) {
// //硬件厂商为华为的,默认是圆形裁剪框,这里让它无法成圆形
// intent.putExtra("aspectX", 9999)
// intent.putExtra("aspectY", 9998)
// } else {
// //其他手机一般默认为方形
// intent.putExtra("aspectX", 1)
// intent.putExtra("aspectY", 1)
// }

// 设置裁剪区域的形状,默认为矩形,也可设置为圆形,可能无效
// intent.putExtra("circleCrop", true);
// 让裁剪框支持缩放
intent.putExtra("scale", true)
// 属性控制裁剪完毕,保存的图片的大小格式。太大会OOM(return-data)
// intent.putExtra("outputX", 400)
// intent.putExtra("outputY", 400)

// 生成临时文件
val cropFile = createFile("Crop")
// 裁切图片时不能使用provider的uri,否则无法保存
// val cropUri = getUriForFile(cropFile)
val cropUri = Uri.fromFile(cropFile)
intent.putExtra(MediaStore.EXTRA_OUTPUT, cropUri)
// 记录临时位置
cropPicPath = cropFile.absolutePath

// 设置图片的输出格式
intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString())

// return-data=true传递的为缩略图,小米手机默认传递大图, Android 11以上设置为true会闪退
intent.putExtra("return-data", false)

startActivityForResult(intent, REQUEST_CROP_CODE)
}

回调处理

下面是对上面三个操作的回调处理,一开始我觉得uri没什么用,还制造麻烦,后面发现可以通过流打开uri,再去获取bitmap,好像又不是那么麻烦了。

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == RESULT_OK) {
when(requestCode) {
REQUEST_CAMERA_CODE -> {
// 通知系统文件更新
// requireContext().sendBroadcast(Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE,
// Uri.fromFile(File(picturePath))))
if (!enableCrop) {
val bitmap = getBitmap(picturePath)
bitmap?.let {
// 显示图片
binding.image.setImageBitmap(it)
}
}else {
cropImage(picturePath)
}
}
REQUEST_ALBUM_CODE -> {
data?.data?.let { uri ->
if (!enableCrop) {
val bitmap = getBitmap("", uri)
bitmap?.let {
// 显示图片
binding.image.setImageBitmap(it)
}
}else {
cropImage(uri)
}
}
}
REQUEST_CROP_CODE -> {
val bitmap = getBitmap(cropPicPath)
bitmap?.let {
// 显示图片
binding.image.setImageBitmap(it)
}
}
}
}
}

private fun getBitmap(path: String, uri: Uri? = null): Bitmap? {
var bitmap: Bitmap?
val options = BitmapFactory.Options()
// 先不读取,仅获取信息
options.inJustDecodeBounds = true
if (uri == null) {
BitmapFactory.decodeFile(path, options)
}else {
val input = requireContext().contentResolver.openInputStream(uri)
BitmapFactory.decodeStream(input, null, options)
}

// 预获取信息,大图压缩后加载
val width = options.outWidth
val height = options.outHeight
Log.d("TAG", "before compress: width = " +
options.outWidth + ", height = " + options.outHeight)

// 尺寸压缩
var size = 1
while (width / size >= MAX_WIDTH || height / size >= MAX_HEIGHT) {
size *= 2
}
options.inSampleSize = size
options.inJustDecodeBounds = false
bitmap = if (uri == null) {
BitmapFactory.decodeFile(path, options)
}else {
val input = requireContext().contentResolver.openInputStream(uri)
BitmapFactory.decodeStream(input, null, options)
}
Log.d("TAG", "after compress: width = " +
options.outWidth + ", height = " + options.outHeight)

// 质量压缩
val baos = ByteArrayOutputStream()
bitmap!!.compress(Bitmap.CompressFormat.JPEG, 80, baos)
val bais = ByteArrayInputStream(baos.toByteArray())
options.inSampleSize = 1
bitmap = BitmapFactory.decodeStream(bais, null, options)

return bitmap
}

这里还做了一个图片的质量压缩和采样压缩,需要注意的是采样压缩的采样率只能是2的倍数,如果需要按任意比例采样,需要用到Matrix,不是很难,读者可以研究下。

权限问题

如果你发现你没有申请权限,那你的去申请一下相机权限;如果你发现你还申请了储存权限,那你可以试一下去掉储存权限,实际还是可以使用的,因为这里并没有用到外部储存,都是应用的私有储存内,具体关于储存的适配,可以看我转载的这几篇文章,我觉得写的非常好:

Android 存储基础

Android 10、11 存储完全适配(上)

Android 10、11 存储完全适配(下)

结语

以上代码都经过我这里实践了,确认了可用,可能写法不是最优,可以避免使用绝对路径,只使用Uri。至于请求码、布局什么的,读者自己改一下加一个就行,核心部分已经在这了。如果需要完整代码,可以看下篇文章末尾!

Android 不申请权限储存、删除相册图片


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

巧妙利用枚举来替代if语句

前言亲爱的友友们,我们今天来看一下如何巧妙利用枚举来替代if语句能实现功能的代码千篇一律,但优雅的代码万里挑一业务背景在工作中遇到一个需求,经过简化后就是:需要根据不同的code值,处理逻辑,然后返回对应的对象。我就简答举个栗子哈💬根据 不同的code,返回不...
继续阅读 »

前言

亲爱的友友们,我们今天来看一下如何巧妙利用枚举来替代if语句

能实现功能的代码千篇一律,但优雅的代码万里挑一

业务背景

在工作中遇到一个需求,经过简化后就是:需要根据不同的code值,处理逻辑,然后返回对应的对象。

我就简答举个栗子哈💬

根据 不同的code,返回不同的对象 传1 返回 一个对象,包含属性:name、age ; 传2,返回一个对象,包含属性name ; 传3,返回一个对象,包含属性sex .... 字段值默认为 test

思路

摇头版

public class TestEnum {
public static void main(String[] args) {
Integer code = 1;//这里为了简单,直接这么写的,实际情况一般是根据参数获取
JSONObject jsonObject = new JSONObject();
if(Objects.equals(0,code)){
jsonObject.fluentPut("name", "test").fluentPut("age", "test");
System.out.println("jsonObject = " + jsonObject);
}
if(Objects.equals(1, code)){
jsonObject.fluentPut("name", "test");
System.out.println("jsonObject = " + jsonObject);
}
if(Objects.equals(2,code)){
jsonObject.fluentPut("sex", "test");
System.out.println("jsonObject = " + jsonObject);
}
}
}

上面的代码在功能上是没有问题滴,但是要扩展的话就💘,比如 当code为4时,ba la ba la,我们只有再去写一遍if语句,随着code的增加,if语句也会随之增加,后面的人接手你的代码时 💔

优雅版

我们首先定义一个枚举类,维护对应Code需要返回的字段

@Getter
@AllArgsConstructor
public enum DataEnum {
/**
* 枚举类
*/
CODE1(1,new ArrayList<>(Arrays.asList("name","age"))),
CODE2(2,new ArrayList<>(Arrays.asList("name"))),
CODE3(3,new ArrayList<>(Arrays.asList("sex")))
;
private Integer code;
private List<String> fields;
//传入code 即可获取对应的 fields
public static List<String> getFieldsByCode(Integer code){
DataEnum[] values = DataEnum.values();
for (DataEnum value : values) {
if(Objects.equals(code, value.getCode())) {
return value.getFields();
}
}
return null;
}
}

客户端代码

public class TestEnum {
public static void main(String[] args) {
//优雅版
JSONObject jsonObject = new JSONObject();
//传入code,获取fields
List<String> fieldsByCode = DataEnum.getFieldsByCode(1);
assert fieldsByCode != null;
fieldsByCode.forEach(x->{
jsonObject.put(x,"test");
});
System.out.println(jsonObject);
}
}

实现的功能和上面的一样,但是我们发现TestEnum代码里面一条if语句都没有也,这时,即使code增加了,我们也只需要维护枚举类里面的代码,压根不用在TestEnum里面添加if语句,是不是很优雅😎

小总结

【Tips】我们在写代码时,一定要考虑代码的通用性

上面的案例中,第一个版本仅仅只是能实现功能,但是当发生变化时难以维护,代码里面有大量的if语句,看着也比较臃肿,后面的人来维护时,也只能不断的添加if语句,而第二个版本巧用枚举类的方法,用一个通用的获取fields的方法,我们的TestEnum代码就变得相当优雅了😎

结语

谢谢你的阅读,由于作者水平有限,难免有不足之处,若读者发现问题,还请批评,在留言区留言或者私信告知,我一定会尽快修改的。若各位大佬有什么好的解法,或者有意义的解法都可以在评论区展示额,万分谢谢。 写作不易,望各位老板点点赞,加个关注!😘😘😘


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

如何提升团队代码质量

目标提升代码质量统一编码规范和风格知识分享防止代码腐烂降低bug数量,提前检查出线上隐患一、接入SonarQube,SonarQube关于js部分的规则rules.sonarsource.com/javascript/sonar的主要作用CR大部分情况只会关注...
继续阅读 »

目标

  1. 提升代码质量
  2. 统一编码规范和风格
  3. 知识分享
  4. 防止代码腐烂
  5. 降低bug数量,提前检查出线上隐患

一、接入SonarQube,

  1. SonarQube关于js部分的规则
  2. sonar的主要作用
    • CR大部分情况只会关注提交部分代码,所以需要一个工具可以从全局检查潜在的代码缺陷,这其中SonarQube是一个不错的选择
    • sonar可以展示源码中重复严重的地方,人肉CR很难做到,除非对这个项目代码深入了解
    • sonar可以当做辅助CR工具,仅用于记录问题,不阻塞发布流程
    • 由于CR只针对于新增代码,所以不会照顾到老代码的质量。sonar可以辅助修复老项目代码的潜在缺陷,提高老项目的代码质量
  3. 如何使用
    • SonarQube看板
    • 定时记录sonar的问题统计信息

二、增加CR环节

CR 本身的收益

  1. 统一编码风格
  2. 增加代码规范的主动性(⼼理暗示别⼈会review,所以会注意规范)
  3. 代码复查,提升质量(尽早发现bug和设计中存在的问题)
  4. 代码分享,知识共享(例如一些好用的库或者解决方案能通过CR在组内快速广播开)
  5. 新人培养(CR流程可以作为新人培训的一部分,让新人能够迅速接入项目)

CR 规范

  1. 所有需要发布⽣产的代码提交都要得到CR,至少需要指定一个人appove
  2. 所有的comment都需要解决之后才可以合并到master
  3. 应用可以设置 Review 建议需全部解决,对于非必需修改的建议可以进行打标或说明
  4. MR的备注信息要详细说明本次MR的功能点,让reviewer能容易理解作者意图
  5. reviewer不能指定自己
  6. 优先指定熟悉项目的相关人员

CR 过程

  1. 冒烟测试通过之后,提交MR到develop分支,并把MR链接发到群里并艾特reviewer,简要说明此次提交/修改的内容(也可通过机器人
  2. 鼓励大胆comment,有不理解的地方,或者觉得不合适的地方都直接表达出来
  3. 作者对每个comment也要做出反馈,无论是展开讨论还是简单的给个 OK 都是有效的反馈
  4. 复杂的、大量的代码提交可以采用线下开会集体cr以提高效率
  5. 作者处理完所有comment,代码也进行修改之后,要在群里艾特通知一下reviewer
  6. reviewer确认没问题,点approve, 然后由作者来点merge

CR Gitlab配置

  1. webhook配置
  2. approvals设置

CR 准则

  1. 如果变更达到可以提升系统整体代码质量的程度,就可以让它们通过,即使它们可能还不完美
  2. 没有完美的代码,只有更好的代码。不要求代码实现每个细节都完美,应该做好修改时间和重要性之间的权衡
  3. 遵循基础的代码规范指南,任何与规范不一致的地方都属于个人偏好,比如变量命名规范是驼峰,代码实现是下划线命名。但如果某项代码样式在指南中未提及,那就接受作者的样式

关于Comment

  1. 一般预期挑的越多越好,但代码是人写的,很多情况下会让作者感到不适,所以在comment的时候也尽量注意一下措辞,有一些在规范之外的偏个人主观的东西,一般以建议的方式给出
  2. 对于原则性的问题,还是要坚守质量标准的
  3. 发现了一些好的代码好的设计,也请给予对方以肯定和赞美,这样有助于Review有效的进行

reviewer需要注意的点

  1. 逻辑上

    • 代码设计
    • 功能实现
    • 边界条件
    • 性能隐患
  2. 代码质量

    • 编码规范
    • 可读性:逻辑清晰、易理解,避免使用奇淫巧技、过度拆分
    • 复杂度:是否有重复可简化的复杂逻辑,代码复杂度是否过高,功能实现尽量要简洁
  3. 参考 CR常见问题

  4. 准则:代码是写给人看的,除非这份代码仅使用一次,不再需要维护。基于此准则review,只要作者提交的代码让你感觉到接手后维护困难,那都应该comment提出自己的想法

CR 心态

  1. Author
    • 自己对代码的质量负责,因此应当怀着感恩的心去看待坚持认真帮你 Review 代码的同事,因为并不是所有人都有时间和精力来帮着提高代码质量
    • 不要依赖于reviewer,不要说什么"review的时候你怎么没看出来"这种话,就好像出了线上bug不要怪测试没有测出来,reviewer只是提供了一些建议,不是做质量把关的
    • 对comment不要有抵触情绪,有自己觉得不合理的地方,可以恰当的回复拒绝,或者说明一下自己这么做的原因
    • 代码好坏这件事情上本身就带有很大的个人偏好色彩在里面,每个commont都看做是一次思想交流就好了
  2. Reviewer
    • 保持学习者的心态,review既是帮助他人提高代码质量的过程,也是学习他人,提高自己代码能力和沟通能力的过程,既要发现潜在质量问题,也要努力发现一些值得学习的亮点,这样对自己也是一个很大的帮助
    • 代码review的时候不用有什么心里负担,有什么疑惑的、不清楚的地方或者有什么自己的想法,可以直接提出来。有不少人 在写Comment的时候会犹豫,担心自己提出的问题或建议比较“蠢”

CR 疑问

  1. 组员参与度和积极性不够高,无法有效对比小A和小B和小C在CR上的贡献
    • 激励措施,鼓励全员积极CR
    • 定期统计comment数量,挑选好的comment,和一些坏的代码展示并讨论
  2. 对于一些重要且复杂的功能代码是否需要定期开会宣讲,多人review
  3. 发布前发现问题多,改动太大,影响项目计划

三、CR常见问题(包含规范/风格指南)

  1. CR常见问题 文档
  2. CR常见问题仅供reviewer做参考, 分严重/中等/建议三个等级
    • 严重: 可能会造成bug
    • 中等: 造成后续维护困难
    • 建议: 代码有优化空间或者代码风格、格式问题,但是不影响使用和迭代
  3. 根据项目紧急程度和对质量的要求,不同等级的问题可酌情处理
  4. 规范要轻量,先抛出严重问题,不过分追求细枝末节,根据后续CR的情况持续增加

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

思维模型:打破996工作制下的僵局,让打工人更具竞争力

你身边有没有这样的人:面对堆积如山的工作、随时弹出的任务,接二连三的群@也能游刃有余地处理。回看自己,旧的任务还在做,新的任务已经从天而降,日程表上满是任务却无从下手……明明忙个不停却成果甚微,这怎么跟想象的不一样!说好的,一分耕耘一分收获呢?说好的,世上无难...
继续阅读 »

你身边有没有这样的人:面对堆积如山的工作、随时弹出的任务,接二连三的群@也能游刃有余地处理。回看自己,旧的任务还在做,新的任务已经从天而降,日程表上满是任务却无从下手……

明明忙个不停却成果甚微,这怎么跟想象的不一样!

  • 说好的,一分耕耘一分收获呢?
  • 说好的,世上无难事,只怕有心人呢?
  • 说好的,只要功夫深,铁杵磨成针呢?

在一切讲究高效率的现代职场,职场人常常被要求高效率完成工作,但缺乏方法的大多数人选择了低效的加班加点。

《论语》中提到:“工欲善其事,必先利其器”。对于想要“逃离苦海”、提高效率的职场人来说,思维模型就是职场人最好的工具。掌握高效率的思维模型,不仅能快速看透工作的本质,不盲目地开展工作,还能让工作效率翻倍,逃离996、007的悲惨生活。

话说思维模型这么多,给点实际的行不行?要想成为精英打工人,我们可以从以下四方面出发,提高效率、解决问题不是梦!

问题1:摆烂现场

职场生存法则第一招:战略规划

要想为自己制定清晰的发展方向,首先要做的就是“认清自我”。SWOT模型、PEST模型、商业模式画布等思维方法能够帮助我们更客观、理性地认识自我或自己的产品。

以SWOT模型为例,它不仅能够帮助我们分析自身的优劣势以及外部的机会与威胁,还能通过几个象限的两两结合产生四个具有行为导向的问题:

  • SO:利用哪些优势,抓住外部机会;
  • ST:利用哪些优势,减少威胁;
  • OW:利用哪些机会,改善自身的哪些劣势;
  • WT:在哪些威胁中,需要改善自身的哪些劣势。

SWOT分析模型不会直接告诉你答案,但通过罗列分析后,答案早已浮出水面。同样,PEST、商业模式画布等模型都能够达到类似的效果。在了解了自己之后,我们就能依据这些因素,做出长远的个人规划。

问题2:混乱现场

职场生存法则第二招:目标管理

面对突如其来的各种需求,SMART原则、5W2H原则、MECE原则、黄金圈法则等目标管理模型,都可以实现任务、计划的稳步前进。再也不用担心面对目标无从下手了!

**如何正确制定目标?如何不让目标成为摆设?**用SMART原则举个例子:

SMART原则强调目标是具体的、可衡量的、可实现的、要与计划有相关性,还要有具体时限的。仅这些还不够,在设定目标时,还有需要注意的维度。

  • 如何能让目标具体:用具体语言清楚说明要达成的行为标准,不能模棱两可、不能笼统模糊;
  • 如何让目标可衡量:目标要数量化、行为化;
  • 目标怎么定才能实现:在付出努力的情况下可以实现目标,禁止目标过高、他人强加 ;
  • 目标如何相关:该目标与本职工作、其他目标是相关联的,不能漫无目的、胡乱跑题 ;
  • 目标如何定时限:目标要有一定的时间限制,不能毫无期限、不设约束。

但目标如何拆分?目标如何管理?还需要借助 MECE原则、 5W2H原则、OKR等模型管理。

问题3:社恐现场

职场生存法则第三招:行为管理

想做又不敢做?这时候,福格行为模型的作用就凸显出来了 !突破自己,再也不用担心自己做不了了!

福格行为模型简单来说可以总结如下,人的行为有三个触发因素:动机、能力、提示。满足这三个因素,人们才会做出行动。

福格行为模型有一个经典公式:

  • M是动机,是想做某件事的欲望;
  • A是能力,也就是能完成某件事的能力;
  • P是触发条件,也就是提示你做某件事的信号。
  • B就是行为,当三个条件同时起到作用时,才能使行为发生,从而完成一件事。

再次回看阿道的社恐现场:

这件事情的M动机(Motivation)是阿道在讲完课后专业技能会得到了巩固,不仅会克服社恐,说不定还会得到其他女同事的青睐,A能力(Ability)是阿道有很好的专业能力,而P触发条件(Prompt)则是老板让阿道给大家讲课。

由此看来,当M动机、A能力、P触发条件同时发生作用时,阿道最终会突破自己,做成自己曾经不敢的事情。

所以,在日常生活工作中,想做一件事,为什么没做呢?无非就是想而不能、能而不想、又能又想但缺乏触发条件而已。当遇到这些情况时,我们可以提高动机、提高能力、设置触发条件。这样我们就能“动起来”了。

问题4:挨批现场

职场生存法则第四招:总结复盘

复盘最大的好处就是能够帮助我们摆脱“低水平的重复”,避免365天过成一天的情况,停下来去反思,到底哪里做得好,哪里做得不好,这样才能够真正高效地提升我们某方面的水平。 因此,复盘是一个打工精英不可忽视的一项思维能力。

复盘相关的思维模型也有很多:PDCA循环模型、5W2H模型、KISS模型等。比如PDCA循环,能让我们用同样的时间做更多的事情,不断螺旋上升地循环起来。

以上这些场景是不是似曾相识?以前百思不得其解的问题好像一下子迎刃而解,就像点外卖时某团推来了大额优惠券、购物时打开了某宝。这些思维模型像存在于人们脑中的APP,遇到问题时能够帮助自己快速获取解决办法。

正如巴菲特的合伙人查理·芒格所说:“思维模型会给你提供—种视角或思维框架,从而决定你观察事物和看待世界的视角。顶级的思维模型能提高你成功的可能性,并帮你避免失败。 ”

其实在能力上,人与人并没有太大的区别,真正的区别在于思维的不同。对大多数职场人来说,只要能掌握其中的一部分的思维模型就足以解决日常职场生活中的各种问题。


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

2023年中总结,全栈工程师在深圳工作6年后的一些杂谈

今天是2023上半年的最后一天,刚好也是我在深圳一家公司的第6年,2017年6月我入职现在这家公司,到如今整好是6年。世事变迁,在这个6月回想6年前的6月,是怎样一番情愫呢。6年前不同如今,那是一个很好找工作的年份,没有什么L型增长的经济,大环境滋润着众多中小...
继续阅读 »

今天是2023上半年的最后一天,刚好也是我在深圳一家公司的第6年,2017年6月我入职现在这家公司,到如今整好是6年。世事变迁,在这个6月回想6年前的6月,是怎样一番情愫呢。

6年前不同如今,那是一个很好找工作的年份,没有什么L型增长的经济,大环境滋润着众多中小企业如雨后春笋一般冒头,行业内大多数人都充满希望。但我在那时候找起工作来依然是十分困苦,四处奔走,因为我,遇到了各种各样的小公司。不过我发现一个特别之处,我与医疗行业十分有缘分,2016年3月就在北京朝阳三间房乡入职的一家企业也为医疗单位服务,虽然是我不大感兴趣的医疗咨询,并非医疗系统,但也引发了我对从事医疗系统服务的向往,没想到一年多之后来到深圳,真的如愿以偿了,在如今这家公司做了整整6年的HIS。

当然了即便是6年前,我也不可能根据自己的偏好去寻找工作,能找一份工作就心满意足,能找到匹配自己喜爱的业务场景,实在是一种幸运。当时我想的是,无论是什么行业只要是做erp类型的软件,都适配于我当时所掌握的一些技能。简单描述就是这类系统注重模块之间复杂的关系,并不需要担心什么高并发场景,笼统的说这也就是b2b的特性(与这些年网络上流行的技术路线b2c截然相反),我一直认为这种方式才比较锻炼复杂而抽象、又要能够连续下去的思维能力,这符合我长期对复杂系统的想象。当然了,面对这些模块比较多也比较杂乱的系统,很难快速响应其中一点,有时候定位一个问题都比较困难,这需要消耗不停的消耗耐心。即便是很有耐心的人,心力也会逐渐被消磨掉,就如同深圳的夏天,即便绿化很好,路上常常有树荫,但真要走一走,仍然是热到冒汗。

对版本控制的一些简言

6年前刚进现在的公司,最让我惊讶的是完全没有版本控制?!只通过sshscp文件拷贝来合并,好在我进入公司之后不久,由于开发进度的深入,就开始使用gitgithub private了。但通过这一短暂的时光,我深刻的体会到版本控制的重要性,表面上看,版本控制只是解决团队协作文件合并的问题,在模块高度分离,负责明确的前期阶段,确实很难感受到版本控制的作用,只会感觉到多了一层麻烦。但他有很多的隐藏的好处,是很多人没有说明的,下面简单概括几点:

  • 标记与溯源,很多时候只要通过提交信息,就能知道一块老代码的前世今生,即便信息粗糙,只要有提交的时间,就能对应上很多其他的信息,例如项目管理面板的bug列表、任务列表,亦或是聊天记录、邮件、保存的图片,通过这样的方式对应上,就能回答这样几个问题,何时新增的模块? 新增模块的初衷可能是什么? 修改的目的是什么?
  • 上下文,一个提交除了一个代码段,还可能包括其他的文件,以及他前后的一些提交内容,这些信息是很丰富的,它们可以回答这样的问题,这段代码修改的前后都在做什么? 这段修改是否可能是因为类似全局替换的批量处理,或是错误的格式化等意外导致的错误?
  • 还原,或许你经常能听到这种抱怨,这个功能花费了好大精力快做完了,用户却要把整个功能设计推倒重来,这无疑是需要撤回一整个提交的,但你可能会想在项目的早期阶段很难发生这种事情,所以拖一段时间再加入版本控制也无所谓。但可能还有一种情况,没有外界的任何干预,一个功能卡在了最终阶段,在这个功能尚未结束的情况下,项目其他很多地方都没办法运行,此时急需进行一个阶段性的演示,这时你就不得不看看这个功能到底牵扯了哪些部分,没有版本控制的帮助,很难快速的只通过代码本身的上下文找到这些需要中止的部分。

这些虽然都是不多见的场景,但我都曾遇到过,如果没有版本控制几乎无从下手解决这些事项,版本控制是需要长期的使用才能感受到它的好处,它甚至不一定需要在有团队合作的场景下使用,即便是你一个人做的玩具项目,或是独立开发的小插件,都能避免这样的问题。但它们不是一时能体验到的,千万不要因为一些小麻烦而错过这么多解决小问题的预防性措施,用git并不一定要提交到一个centre,即便是全程都在本地使用,都能感受到许多益处,所以一个项目应该尽快从速的加入版本控制,不要等到有合并的需求才加入。

版本控制就像道路,不要觉得他只是为拼合一个集体而生,只有人多了才需要修路,其实只要是需要车辆通过,哪怕是一天只会通过一辆车,有条道路也比没有强一些。

多解决棘手问题了解一些极为罕见的意外情况

曾经看过这样的科普,硬盘会被环境噪声影响,速度会降低,寿命会减少,我想如果我遇到这样的问题一定不会想到是噪声的问题,因为环境噪声能严重到干扰硬盘设备,可能像因为吃香蕉而辐射超标一样不现实,但我却真实的遇到了一次极为罕见的意外情况:

Chrome语法检查插件Grammarly曾备受推崇,我在油管见过好多次他的广告,但我并没有尝试,没想到它却依然走入了我的视野,众所周知面对单词或语法错误的输入框,他会显示出一个类似于tooltip的悬浮块来提醒用户,但它却很神奇的影响了kendo前端框架的富文本输入框,会导致其高度发生变化,甚至遮盖下方的元素,用户完全无法正常使用,我百思不得其解,最终通过用户的截图发现这个插件,自己安装了一试,发现了问题,原来Chrome插件是真的会影响页面功能的。

遇到这样的情况当然是运气不好,但不能不有一些大胆的假象,如果完全不敢设想极端情况,只去寻找自身的常规的问题,那是很难解决问题的。

也不要害怕棘手的问题,如果能解决,自然可以向所有人分享自己的经验获得分享的快乐。就算最终解决不了,也可以成为一桩值得回味的悬案,就像一个开放式结局的小故事值得他人解读。

持续跟进社群的走向

这一点是毋容置疑的,最直接的影响就是技术路线的更新迭代,如果自己用于开发的语言、平台、框架本身已经没落,官方停止支持,那自然是要学习其他代替品的,但这个没落也分等级,像FlashVisual Basic&VBScriptWindows Phone.NET Framework&WinFormAngular.js 1.x、属于已经宣告死亡的产品,是绝对要抽身的。而像PHPCoffeeScriptDelphiMemcached,是很多年前就被替代品取代但仍然被一些坚持的人维护着,这还是有些区别的,前者就算真的很热爱也无力挽救了,后者还可以倔强下去。但无论是哪种,都要面对下面几个问题:

  • 即便是大多数人都不用了,也总会有维护性的需求在吧?
    • 并不可取,说不定某一天就要换新系统,维护性需求消失,不学点新的怎么能行呢,而且很多新技术变化并非翻天覆地,只要慢慢的接触,消除掉陌生感,就会自然的对新技术越来越感兴趣。
  • 所处的公司业务体系庞大,短时间内很难更新技术,即便是要更新,到时候再学就是了。
    • 任务到面前了才开始学,时间够吗,现在很多产品开发周期越来越快,仓促之间错误一定不会少,后面还能改善这些错误呢?会不会需要很大的代价,并且引出更多的错误?技术栈一定要提前布局
  • 新技术虽然用的人很多,但占用大、而且看上去只能做些简单的东西,恐怕做不了太复杂的需求,卡在替代过程中的某个位置就白费力气了。
    • 很多框架占用内存确实较大,但现在用户的设备越来越好,完全是有条件的,而且底层设计完善,占用的增加可能并非线性的,而是随着开发进度趋于平缓。如果认为做不了复杂需求,就应该看一下用例,甚至是我们所使用的工具,像掘金本身是VueElement-UI做的,如果你关注AI,你就会发现OpenAI官网也用了Vue,这就很给人信心。

即便许多年后也许离开了软件开发的行业,十余年后回顾这段历程,都找不到有人聊自己用过的技术,成为了一个完全的死话题,也无从向后来者介绍从前的开发经历与故事,是否会像一个孤寡老人一样无助呢,所以说为了自己的历程与故事,也要不断了解和学习新的技术,新技术的发展也并非需要天天了解,如今很多流行的技术其实也已经存在了七八年了,如果真的热爱,就值得持续的关注,并不占用多少时间,向后来者讲述技术演化的细节,每个人都会有自己独特的故事,这是其他媒体无法讲述的。

未完待续


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

工作 6 年,我不想再「键政」了

今天,刷推时看到一张图,感觉和我工作几年来的心路历程很像,特此分享下。 第一个人脚下空无一物,眼中均是美好。 第二个人读了一些书,看到美好背后的黑暗,开始陷入迷茫。 第三个人学识渊博,了解运行规律,明白世界不是非黑即白,故此看到曙光。 而我呢,目前可能还处在...
继续阅读 »

今天,刷推时看到一张图,感觉和我工作几年来的心路历程很像,特此分享下。



第一个人脚下空无一物,眼中均是美好。


第二个人读了一些书,看到美好背后的黑暗,开始陷入迷茫。


第三个人学识渊博,了解运行规律,明白世界不是非黑即白,故此看到曙光。


而我呢,目前可能还处在第二阶段,但也清楚应该继续向前,走向第三阶段。


第一阶段



无知小粉红心态



读书期间,小镇出身的我,比较追求应试教育和实用主义,所思所学全为了考高分、学技术,除此之外的素质教育全然不顾。


同样是去图书馆,我看的是「精通 Java」,而舍友看的是「毛选」、「中国近代史」这类的书籍。在那时,我是不屑一顾的,认为这就是 「浪费时间」,看这些又不能当饭吃。


毕业后,舍友进了体制内,而我去了一家小厂当码农。小厂也挺好,朝九晚六,不追求结婚买房,过得很快乐。


然而,我还是没有继续读书,技术之外脑袋空空,只会被动的接收主流媒体提供的资讯,从不思考内在逻辑。


有一次,社保税改(2018年)要求公司按员工真实收入去上报缴纳基数,也就是说社保缴纳金额变多、到手工资变少。看到群里都在吐槽,而那时的我却在群里发表了「高见」:



社保不也是自己的钱么,提高缴纳基数更赚么?gj 这是为我们个人谋福利!



结果招来一顿全嘲,说我「啥也不懂」。后面又工作了一段时间,我才彻底明白了他们的槽点。


第二阶段



生活压力,终使自己变成自己最讨厌的人



早期很喜欢逛知乎,也关注了一些前端大佬,希望学点技术。


但从某段时间开始(大概2020 左右),发现这些人很喜欢「键政」,大谈国事。


大多都是负面情绪,当时作为「小粉红」的我难以接受,于是拉黑了好几个人。


随着年龄上去,迫使自己需要关注技术之外的内容:房产、婚姻、生育、教育、理财、交际,往大点说,是政治、历史、和经济。


粗浅了解之后,我开始悲观:



  • 刑不上大夫

  • 十年寒窗凭什么拼得过人家三代人的努力

  • 历史就是圈,教员想改变的事情是无法改变的

  • zg人的劣根性

  • tz内的劣根性


于是,我也开始键政,变成了那个曾经最讨厌的人。


第三阶段



探索底层逻辑



工作压力加上生活压力,使我一度抑郁,甚至产生过极端想法。


好在,我有一个好伴侣,是她陪我度过了那段痛苦的岁月,鼓励我多看书、多思考。


现在,我也分享下我的一些想法,虽然还未正式踏入第三阶段,但也大概摆脱了第二阶段的影响。



  1. 接纳自己的平凡

  2. 最重要的能力,是获得能力的能力

  3. 遵从历史规律,做务实求进的人

  4. 思考底层逻辑,所有方法论都可以通过底层逻辑(相同之处)+ 环境变量 (不同之处) 来解释

  5. 提升思维认知,多学习技术之外的内容


最后


以上便是我工作六年的心路历程,从开始的无知,再到键政,最后开始寻求转变。


本文纯碎碎念

作者:francecil
来源:juejin.cn/post/7257022428194209849
,欢迎各位客官吐槽~

收起阅读 »

远程办公,重新定义新的生活方式

1、远程办公为什么出现? 别再把人生耗费在通勤路上 每天早上,我们定了无数个闹钟,就为赶上早班地铁和公交;没有时间吃早餐,路上随便买一份吃的急匆匆地往地铁站和公交站跑。好不容易挤上了车,车里拥挤的人们和混杂的气味,瞬间让我们头痛欲裂。 我们时不时就会思考这个问...
继续阅读 »

1、远程办公为什么出现?


别再把人生耗费在通勤路上


每天早上,我们定了无数个闹钟,就为赶上早班地铁和公交;没有时间吃早餐,路上随便买一份吃的急匆匆地往地铁站和公交站跑。好不容易挤上了车,车里拥挤的人们和混杂的气味,瞬间让我们头痛欲裂。


我们时不时就会思考这个问题:我一定要上班吗?


当然,为了生活,我们每一个人都要工作,但是,为什么要选择通勤时间那么久的工作呢?让我们面对这个事实吧:没人喜欢通勤。


出门上班的日子里,闹钟响得更早,你到家也更晚。时间溜走了,耐心磨没了,没准你的食欲也没了,除了塑料盒子装的快餐之外什么都懒得吃。你可能会放弃健身活动,错过哄孩子入睡的机会,累到没力气跟你的另一半深入地谈谈心。工作日,跟高速公路较了半天劲之后, 你懒得去做家务,于是杂事全都积攒在一起,留到了周末。


假设你每天要在交通拥堵的时段开30分钟车去上班,再加上走到车边和走进办公室的15分钟,那么每天来回需要1.5小时,一周就是7.5小时,考虑到假日和休假,每年你花在路上的时间大约有300~400个小时。想象一下,如果每年多出400个小时,你能做到什么。通勤不仅会危害你的健康、人际关系和环境,它还会影响你的事业。


因此,我们需要工作,但工作不一定非得通勤不可。


微信图片_20230718143525_1.png


逃离996福报


 
马云说要珍惜我们的工作,996是证明你努力工作的象征。可是,随着互联网的发展,我们还必须坐在同一个房间里,用同一种方式工作吗?至少马云的公司绝不是这样。


科技的发展改变着人们的工作方式,团队间的工作从“同步”协作变成了无须同步的协作。我们早已用不着都凑到同一个地点工作了,而且,连同时工作都用不着。一旦你调整了工作方法和期望,哪怕你的团队伙伴在国外,你也可以与他协调好时间,实行弹性工作制,来共同完成一个项目。


弹性工作制的魅力就在于它能满足每一个人的需求,无论是早起族、夜猫子,还是需要在白天接送孩子的顾家型员工,人人都很方便。逃离996福报把。想要掌握跟团队不同步工作的诀窍,或许需要一点时间和实践,但用不了多久你就会发现,真正重要的是把工作做好,而不是死守着上下班时间。


微信图片_20230718143528_1.png


何必都挤在大城市


从前,那些掌握着资本主义引擎的人心想:“我们把一大群人凑到一个小区域里来吧,让他们一个挨一个地拥挤地住在一起,这样我们就可以找到充足又能干的人手了。”


所幸,令工厂得益的高密度人口对其他很多事情也有好处。我们有了图书馆、体育馆、剧院、餐馆,以及现代文明中的一切奇迹。可我们也有了狭小的格子间,丁点大的公寓住宅,挤得像沙丁鱼罐头一样的公车载着我们在各地穿梭。


为了换取便利和兴奋感,我们交出了自由、壮观的乡村美景和新鲜的空气。
 


但幸运的是,令远程工作成为可能的先进科技也让相应的文化和生活变得更有魅力。想想看,要是对一个20世纪60年代的城市人描述这幅情景:每一个人都能看到有史以来拍摄的每一部电影、每一本书、每一个相册,几乎每一场体育赛事都有现场直播(画质和色彩比以往任何时候都更清晰、鲜明),此人绝对会大笑不止。


其实,就算你把这些讲给一个生活在80年代的人听,他也会笑出来。可我们现在就生活在这样的世界里。然而,把这一切便利视作理所当然,与从中得出符合理性的结论,这两者是有区别的。


如果现在我们能够不受限制地接触这些文化和娱乐生活,一切都跟所在的地域无关,那么,我们为何还要困在原先的教条里?


微信图片_20230718143526.png


新时代的奢侈


 
在高楼大厦的顶层一角有间时髦的办公室,公司给你配备了豪华车子,身边还跟着秘书……这种老式的奢侈福利很容易遭人嘲笑。可当今富豪的特权也没多大区别:米其林餐厅,世界各地旅游,网红酒店打卡。而我们得到了什么呢?你被困在办公室里,远离了家人、朋友和业余爱好。


在漫长的岁月中,支持你熬过去的是一个希望:等你退休了就自由了。


可是,为什么要等?如果你真心热爱滑雪,为何要等到髋骨老化到经受不起摔跟头以后,才去滑?如果你喜欢冲浪,为何还要困在水泥丛林中,不搬到海边去?如果亲爱的家人都住在小镇上,你为何还要强留在大城市?


新时代的奢侈就是摆脱“日后再享受生活”的思维桎梏,现在就去做你热爱的事,跟工作并行。何必要把时间浪费在那种“等我退休了,生活该有多美好”的白日梦上?把工作跟退休之间划一道界限,这其实是相当武断的。你的人生无须再遵循这样的规则。你可以把这两样混合在一起,既有趣,又有钱——设计一种更好的、能把工作变得有趣的生活方式,因为工作不是这辈子唯一的事儿。


微信图片_20230718143527.png


2、远程工作和你想象的一样吗


不一定非得远在天边


远程工作的“远”,指的不一定非得是另一个城市、另一个省份或另一个国家。在街道的另一头工作也可以叫作远程。


远程工作的意思只是说你用不着朝九晚五, 全天都待在办公室里工作。随着网络的发达,各种通讯硬件和软件的逐渐完善,是科技推动了我们工作方式的转变,让我们可以不用坐在一起才能沟通,哪怕我们只是在同一层楼或者同一个小区,都可以实时互联,完成这项远程工作。远不是指的距离,而是我们可以通过更便捷的手段和方式来缩短距离,让我们的团队成员不再受距离的限制。
 
微信图片_20230718143528.png


工作成果才是最根本的衡量标准 


当老板没法整天盯着员工的时候,判断你是否努力工作的唯一标准就是工作成果。除此之外的一大堆琐碎标准全都不见了。
 


“她是9点钟到的吗?”或是,“她这一天里休息的次数是不是太多了?”再或者,“跟你说吧,我每次经过他的办公桌,都看见他在逛微博,刷淘宝!”


这种评价压根儿就无从说起。
 


剩下的判断标准只有一个,“他今天到底做了些什么?”而不是,“他们几点到办公室的?”或是,“他们几点下班的?”
 


而在远程工作中,你只需看工作成果就可以了。因此,你不必问远程工作的员工,“你今天都做了些什么?”而是说一句“把你今天的成果给我看看”就行。


身为管理者,你可以直接评估工作的质量(你是根据这个给他们发工资的),其他所有不重要的东西都可以忽略不计。这样做的好处是一切都清清楚楚。只看重工作成果的时候,公司里谁尽心尽力,谁没有,你一眼就能看出来。


它可以告诉你,谁是好员工


微信图片_20230718143529.png


采用远程工作之后,在工作成果上糊弄人变得更加困难。由于在办公室东拉西扯的机会减少了,对工作本身的关注度就提高了。此外,用于跟踪工作进度、汇报进展的网上展示平台会把你做的事都记录下来,留下确凿无疑的证据,把每个人的成果和用时都展示给大家看。


因此,安静但高产的员工有了优势,尽管在传统的办公室环境中,这种人常常会败下阵来。
 


远程工作把幕布揭开,让人们看到一个一直存在却并不是总被人承认或被人看到的事实:优秀的远程员工就是优秀的员工,就是这么简单。当工作成果被展示出来的时候,谁真正聪明就更容易看得出来(同样,谁“显 得”聪明也一目了然)。大家心里都有数,连说都不用说。


远程工作能够加快人才优胜劣汰的速度,更快地把不合适的人请下车,让合适的人留下来。


微信图片_20230718143529_1.png


3、结语


远程办公:只是一种新的生活方式。


其实,远程办公模式因企业而异,因企业工作性质导致整体效率不同;同时也因人而异,一些习惯性自律的人,远程办公的场景会让他更有效率,但是一些本身需要约束的人,一旦人员管理不到位,就会放任自我,导致约定的任务拖延,近而直接影响个人产出和公司效益。
 


相比于远程办公,传统的集中办公形式有其天然的优点:比如面对面沟通的工作效率、快速调动人手的协同能力、超强的团队凝聚力和积极的工作氛围。这些天然的优点也是其一直存在的原因和目的。那么发展远程办公绝不是为了完全取代线下集中办公的工作模式,而是为更自律、更高效的人们提供一种新的生活方式,让他们可以更加从容地享受工作,享受生活。*


相信未来技术的变革,将会使远程办公的硬件设备和软件设施搭建的趋于完美,逐渐弥补其与线下集中办公的差距,营造

作者:MasutaaDAO
来源:juejin.cn/post/7256990215746682936
更加真实的工作环境。

收起阅读 »

web端实现远程桌面控制

web
阐述 应着标题所说,web端实现远程桌面控制,方案有好几种,达到的效果就是类似于向日葵一样可以远程桌面,但是操作方可以不用安装客户端,只需要一个web浏览器即可实现,桌面端需写一个程序用来socket连接和执行Windows指令。 实现方案 使用webSock...
继续阅读 »

阐述


应着标题所说,web端实现远程桌面控制,方案有好几种,达到的效果就是类似于向日葵一样可以远程桌面,但是操作方可以不用安装客户端,只需要一个web浏览器即可实现,桌面端需写一个程序用来socket连接和执行Windows指令。


实现方案


使用webSocket实现web端和桌面端的实时TCP通讯和连接,连接后桌面端获取自己的桌面流以照片流的形式截图发送blob格式给web端,web端再接收后将此格式解析再赋值在img标签上,不停的接收覆盖,类似于快照的形式来呈现画面,再通过api获取web端的鼠标事件和键盘事件通过webSocket发送给客户端让他执行Windows事件,以此来达到远程控制桌面控制效果。


Demo实现


因为为了方便同事观看,所以得起一个框架服务,我习惯用vue3了,但大部分都是js代码,可参考改写。


html只需一行搞定。


<div>
<img ref="imageRef" class="remote" src="" alt="">
</div>

接下来就是socket连接,用socket库和直接new webSocket都可以连接,我习惯用库了,因为多一个失败了可以自动连接的功能,少写一点代码🤣,库也是轻量级才几k大小。顺便把socket心跳也加上,这个和对端协商好就行了,一般都是ping/pong加个type值,发送时记得处理一下使用json格式发送,如果连接后60秒后没有互相发送消息客户端就会认为你是失联断开连接了,所以他就会强行踢掉你连接状态,所以心跳机制还是必不可少的。


import ReconnectingWebSocket from 'reconnecting-websocket'
const remoteControl = '192.168.1.175'
const scoketURL = `ws://${remoteControl}:10086/Echo`
const imageRef = ref()

onMounted(() => {
createdWebsocket()
})

const createdWebsocket = () => {
socket = new ReconnectingWebSocket(scoketURL)
socket.onopen = function () {
console.log('连接已建立')
resetHeart()
}
socket.onmessage = function (event) {
// console.log(event.data)
const objectDta = JSON.parse(event.data)
console.log(objectDta)
if (objectDta.type === 100) {
resetHeart()
}
}
socket.onclose = function () {
console.log('断开连接')
}
}

let heartTime = null // 心跳定时器实例
let socketHeart = 0 // 心跳次数
let HeartTimeOut = 20000 // 心跳超时时间
let socketError = 0 // 错误次数
// socket 重置心跳
const resetHeart = () => {
socketHeart = 0
socketError = 0
clearInterval(heartTime)
sendSocketHeart()
}
const sendSocketHeart = () => {
heartTime = setInterval(() => {
if (socketHeart <= 3) {
console.log('心跳发送:', socketHeart)
socket.send(
JSON.stringify({
type: 100,
key: 'ping'
})
)
socketHeart = socketHeart + 1
} else {
reconnect()
}
}, HeartTimeOut)
}
// socket重连
const reconnect = () => {
socket.close()
if (socketError <= 3) {
clearInterval(heartTime)
socketError = socketError + 1
console.log('socket重连', socketError)
} else {
console.log('重试次数已用完的逻辑', socketError)
clearInterval(heartTime)
}
}

成功稳定连接后那么恭喜你完成第一步了,接下来就是获取对端发来的照片流了,使用socket.onmessageapi用来接收对端消息,需要转一下json,因为发送的数据照片流很快,控制台直接刷屏了,所以简单处理一下。收到照片流把blob格式处理一下再使用window.URL.createObjectURL(blob)赋值给img即可。


socket.onmessage = function (event) {
// console.log(event.data)
if (event.data instanceof Blob) { // 处理桌面流
const data = event.data
const blob = new Blob([data], { type: "image/jpg" })
imageRef.value.src = window.URL.createObjectURL(blob)
} else {
const objectDta = JSON.parse(event.data)
console.log(objectDta)
if (objectDta.type === 100) {
resetHeart()
}
}
}

此时页面可以呈现画面了,并且是可以看得到对面操作的,但让人挠头的是,分辨率和尺寸不对,有上下和左右的滚动条显示,并不是百分百的,解决这个问题倒是不难,但如需考虑获取自身的鼠标坐标发送给对端,这个坐标必须准确无误,简单来说就是分辨率自适应,因为web端使用的电脑屏幕大小是不一样的,切桌面端发送给你的桌面流比如是全屏分辨率的,以此得做适配,这个放后面解决,先来处理鼠标和键盘事件,纪录下来并发送对应的事件给桌面端。记得去除浏览器的拖动和鼠标右键事件,以免效果紊乱。


const watchControl = () => { // 监听事件
window.ondragstart = function (e) { // 移除拖动事件
e.preventDefault()
}
window.ondragend = function (e) { // 移除拖动事件
e.preventDefault()
}
window.onkeydown = function (e) { // 键盘按下
console.log('键盘按下', e)
socket.send(JSON.stringify({ type: 0, key: e.keyCode }))
}
window.onkeyup = function (e) { // 键盘抬起
console.log('键盘抬起', e)
socket.send(JSON.stringify({ type: 1, key: e.keyCode }))
}
window.onmousedown = function (e) { // 鼠标单击按下
console.log('单击按下', e)
console.log(newPageX, 'newPageX')
console.log(newPageY, 'newPageY')
socket.send(JSON.stringify({ type: 5, x: pageX, y: pageY }))
}
window.onmouseup = function (e) { // 鼠标单击抬起
console.log('单击抬起', e)
socket.send(JSON.stringify({ type: 6, x: pageX, y: pageY }))
}
window.oncontextmenu = function (e) { // 鼠标右击
console.log('右击', e)
e.preventDefault()
socket.send(JSON.stringify({ type: 4, x: pageX, y: pageY }))
}
window.ondblclick = function (e) { // 鼠标双击
console.log('双击', e)
}
window.onmousewheel = function (e) { // 鼠标滚动
console.log('滚动', e)
const moving = e.deltaY / e.wheelDeltaY
socket.send(JSON.stringify({ type: 7, x: e.x, y: e.y, deltaY: e.deltaY, deltaFactor: moving }))
}
window.onmousemove = function (e) { // 鼠标移动
if (!timer) {
timer = setTimeout(function () {
console.log("鼠标移动:X轴位置" + e.pageX + ";Y轴位置:" + e.pageY)
socket.send(JSON.stringify({ type: 2, x: pageX, y: pageY }))
timer = null
}, 60)
}
}
}

现在就可以实现远程控制了,发送的事件类型根据桌面端服务需要什么参数协商好就成,接下来就是处理分辨率适配问题了,解决办法大致就是赋值img图片后拿到他的参数分辨率,然后获取自身浏览器的宽高,除以他的分辨率再乘上自身获取的鼠标坐标就OK了,获取img图片事件需要延迟一下,因为是后面socket连接后才赋值的图片,否则宽高就一直是0,加在watchControl事件里面,发送时坐标也要重新计算。


const watchControl = () => {
console.dir(imageRef.value)
imgWidth.value = imageRef.value.naturalWidth === 0 ? 1920 : imageRef.value.naturalWidth// 图片宽度
imgHeight.value = imageRef.value.naturalHeight === 0 ? 1080 : imageRef.value.naturalHeight // 图片高度
clientHeight = document.body.offsetHeight

......

window.onmousedown = function (e) { // 鼠标单击按下
console.log('单击按下', e)
const newPageX = parseInt(e.pageX * (imgWidth.value / clientWidth)) // 计算分辨率
const newPageY = parseInt(e.pageY * (imgHeight.value / clientHeight))
console.log(newPageX, 'newPageX')
console.log(newPageY, 'newPageY')
socket.send(JSON.stringify({ type: 5, x: newPageX, y: newPageY }))
}
}

现在就几乎大功告成了,坐标稳定发送,获取的也是正确计算出来的,下面再做一些socket加密优化,还有事件优化,集成到项目里面离开时还是要清除所有事件和socket连接,直接上完整全部代码。


<template>
<div>
<img ref="imageRef" class="remote" src="" alt="" />
</div>
</template>

<script setup>
import ReconnectingWebSocket from 'reconnecting-websocket'
import { Base64 } from 'js-base64'

onMounted(() => {
createdWebsocket()
})

const route = useRoute()
let socket = null
const secretKey = 'keyXXXXXXX'
const remoteControl = '192.168.1.xxx'
const scoketURL = `ws://${remoteControl}:10086/Echo?key=${Base64.encode(secretKey)}`
const imageRef = ref()
let timer = null
const clientWidth = document.documentElement.offsetWidth
let clientHeight = null
const widthCss = (window.innerWidth) + 'px'
const heightCss = (window.innerHeight) + 'px'
const imgWidth = ref() // 图片宽度
const imgHeight = ref() // 图片高度

const createdWebsocket = () => {
socket = new ReconnectingWebSocket(scoketURL)
socket.onopen = function () {
console.log('连接已建立')
resetHeart()
setTimeout(() => {
watchControl()
}, 500)
}
socket.onmessage = function (event) {
if (event.data instanceof Blob) { // 处理桌面流
const data = event.data
const blob = new Blob([data], { type: 'image/jpg' })
imageRef.value.src = window.URL.createObjectURL(blob)
} else {
const objectDta = JSON.parse(event.data)
console.log(objectDta)
if (objectDta.type === 100) {
resetHeart()
}
}
}
socket.onclose = function () {
console.log('断开连接')
}
}

const handleMousemove = (e) => { // 鼠标移动
if (!timer) {
timer = setTimeout(function () {
const newPageX = parseInt(e.pageX * (imgWidth.value / clientWidth)) // 计算分辨率
const newPageY = parseInt(e.pageY * (imgHeight.value / clientHeight))
// console.log(newPageX, 'newPageX')
// console.log(newPageY, 'newPageY')
// console.log('鼠标移动:X轴位置' + e.pageX + ';Y轴位置:' + e.pageY)
socket.send(JSON.stringify({ type: 2, x: newPageX, y: newPageY }))
timer = null
}, 60)
}
}
const handleKeydown = (e) => { // 键盘按下
console.log('键盘按下', e)
socket.send(JSON.stringify({ type: 0, key: e.keyCode }))
}
const handleMousedown = (e) => { // 鼠标单击按下
console.log('单击按下', e)
const newPageX = parseInt(e.pageX * (imgWidth.value / clientWidth)) // 计算分辨率
const newPageY = parseInt(e.pageY * (imgHeight.value / clientHeight))
console.log(newPageX, 'newPageX')
console.log(newPageY, 'newPageY')
socket.send(JSON.stringify({ type: 5, x: newPageX, y: newPageY }))
}
const handleKeyup = (e) => { // 键盘抬起
console.log('键盘抬起', e)
socket.send(JSON.stringify({ type: 1, key: e.keyCode }))
}

const handleMouseup = (e) => { // 鼠标单击抬起
console.log('单击抬起', e)
const newPageX = parseInt(e.pageX * (imgWidth.value / clientWidth)) // 计算分辨率
const newPageY = parseInt(e.pageY * (imgHeight.value / clientHeight))
console.log(newPageX, 'newPageX')
console.log(newPageY, 'newPageY')
socket.send(JSON.stringify({ type: 6, x: newPageX, y: newPageY }))
}

const handleContextmenu = (e) => { // 鼠标右击
console.log('右击', e)
e.preventDefault()
const newPageX = parseInt(e.pageX * (imgWidth.value / clientWidth)) // 计算分辨率
const newPageY = parseInt(e.pageY * (imgHeight.value / clientHeight))
console.log(newPageX, 'newPageX')
console.log(newPageY, 'newPageY')
socket.send(JSON.stringify({ type: 4, x: newPageX, y: newPageY }))
}

const handleDblclick = (e) => { // 鼠标双击
console.log('双击', e)
}

const handleMousewheel = (e) => { // 鼠标滚动
console.log('滚动', e)
const moving = e.deltaY / e.wheelDeltaY
socket.send(JSON.stringify({ type: 7, x: e.x, y: e.y, deltaY: e.deltaY, deltaFactor: moving }))
}

const watchControl = () => { // 监听事件
console.dir(imageRef.value)
imgWidth.value = imageRef.value.naturalWidth === 0 ? 1920 : imageRef.value.naturalWidth// 图片宽度
imgHeight.value = imageRef.value.naturalHeight === 0 ? 1080 : imageRef.value.naturalHeight // 图片高度
clientHeight = document.body.offsetHeight

window.ondragstart = function (e) { // 移除拖动事件
e.preventDefault()
}
window.ondragend = function (e) { // 移除拖动事件
e.preventDefault()
}
window.addEventListener('mousemove', handleMousemove)
window.addEventListener('keydown', handleKeydown)
window.addEventListener('mousedown', handleMousedown)
window.addEventListener('keyup', handleKeyup)
window.addEventListener('mouseup', handleMouseup)
window.addEventListener('contextmenu', handleContextmenu)
window.addEventListener('dblclick', handleDblclick)
window.addEventListener('mousewheel', handleMousewheel)
}

let heartTime = null // 心跳定时器实例
let socketHeart = 0 // 心跳次数
const HeartTimeOut = 20000 // 心跳超时时间
let socketError = 0 // 错误次数
// socket 重置心跳
const resetHeart = () => {
socketHeart = 0
socketError = 0
clearInterval(heartTime)
sendSocketHeart()
}
const sendSocketHeart = () => {
heartTime = setInterval(() => {
if (socketHeart <= 3) {
console.log('心跳发送:', socketHeart)
socket.send(
JSON.stringify({
type: 100,
key: 'ping'
})
)
socketHeart = socketHeart + 1
} else {
reconnect()
}
}, HeartTimeOut)
}
// socket重连
const reconnect = () => {
socket.close()
if (socketError <= 3) {
clearInterval(heartTime)
socketError = socketError + 1
console.log('socket重连', socketError)
} else {
console.log('重试次数已用完的逻辑', socketError)
clearInterval(heartTime)
}
}

onBeforeUnmount(() => {
socket.close()
console.log('组件销毁')
window.removeEventListener('mousemove', handleMousemove)
window.removeEventListener('keydown', handleKeydown)
window.removeEventListener('mousedown', handleMousedown)
window.removeEventListener('keyup', handleKeyup)
window.removeEventListener('mouseup', handleMouseup)
window.removeEventListener('contextmenu', handleContextmenu)
window.removeEventListener('dblclick', handleDblclick)
window.removeEventListener('mousewheel', handleMousewheel)
})
</script>

<style scoped>
.remote {
width: v-bind(widthCss);
height: v-bind(heightCss);
}
</style>

现在就算是彻底大功告成了,加密密钥或者方式还是和对端协商,流畅度和清晰度也不错的,简单办公还是没问题的,和不开会员的向日葵效果差不多,后面的优化方式大致围绕着图片压缩来做应该能达到更加流畅的效果,如果项目是https的话socket服务也要升级成wss协议,大致就这样,若有不正确的地

作者:小胡不糊涂
来源:juejin.cn/post/7256970964533297211
方还请指正一番😁😁。

收起阅读 »

前端基建原来可以做这么多事情

web
前端基建是指在前端开发过程中,为提高开发效率、代码质量和团队协作而构建的一些基础设施和工具。下面是前端基建可以做的一些事情: 脚手架工具:开发和维护一个通用的脚手架工具,可以帮助团队快速初始化项目结构、配置构建工具、集成常用的开发依赖等。 组件库:开发...
继续阅读 »

guide-cover-2.2d36b370.jpg


前端基建是指在前端开发过程中,为提高开发效率、代码质量和团队协作而构建的一些基础设施和工具。下面是前端基建可以做的一些事情:




  1. 脚手架工具:开发和维护一个通用的脚手架工具,可以帮助团队快速初始化项目结构、配置构建工具、集成常用的开发依赖等。




  2. 组件库:开发和维护一个内部的组件库,包含常用的UI组件、业务组件等,提供给团队成员复用,减少重复开发的工作量。




  3. 构建工具和打包工具:搭建和维护一套完善的构建和打包工具链,包括使用Webpack、Parcel等工具进行代码的压缩、合并、打包等工具,优化前端资源加载和性能。




  4. 自动化测试工具:引入自动化测试工具,如Jest、Mocha等,编写和维护测试用例,进行单元测试、集成测试、UI测试等,提高代码质量和可靠性。




  5. 文档工具:使用工具如JSDoc、Swagger等,生成项目的API文档、接口文档等,方便团队成员查阅和维护。




  6. Git工作流:制定和规范团队的Git工作流程,使用版本控制工具管理代码,方便团队协作和代码回退。




  7. 性能监控和优化:引入性能监控工具,如Lighthouse、Web Vitals等,对项目进行性能分析,优化网页加载速度、响应时间等。




  8. 工程化规范:制定并推广团队的代码规范、目录结构规范等,提高代码的可读性、可维护性和可扩展性。




  9. 持续集成和部署:搭建持续集成和部署系统,如Jenkins、Travis CI等,实现代码的自动构建、测试和部署,提高开发效率和代码质量。




  10. 项目文档和知识库:建立一个内部的项目文档和知识库,记录项目的技术细节、开发经验、常见问题等,方便团队成员查阅和学习。




通过建立和维护前端基建,可以提高团队的协作效率,减少重复劳动,提高代码质量和项目的可维护性。


当涉及到前端基建时,还有一些其他的事情可以考虑:




  1. 代码质量工具:引入代码质量工具,如ESLint、Prettier等,对代码进行静态分析和格式化,提高代码的一致性和可读性。




  2. 国际化支持:为项目添加国际化支持,可以通过引入国际化库,如i18next、vue-i18n等,实现多语言的切换和管理。




  3. 错误监控和日志收集:引入错误监控工具,如Sentry、Bugsnag等,实时监控前端错误,并收集错误日志,方便进行问题排查和修复。




  4. 前端性能优化工具:使用工具如WebPageTest、Chrome DevTools等,对项目进行性能分析和优化,提高页面加载速度、响应时间等。




  5. 缓存管理:考虑合理利用浏览器缓存和服务端缓存,减少网络请求,提升用户访问速度和体验。




  6. 移动端适配:针对移动端设备,采用响应式设计或使用CSS媒体查询等技术,实现移动端适配,保证页面在不同尺寸的设备上有良好的显示效果。




  7. 安全防护:对项目进行安全审计,使用安全防护工具,如CSP(Content Security Policy)、XSS过滤等,保护网站免受常见的安全攻击。




  8. 性能优化指标监控:监控和分析关键的性能指标,如页面加载时间、首次渲染时间、交互响应时间等,以便及时发现和解决性能问题。




  9. 前端日志分析:使用日志分析工具,如ELK(Elasticsearch、Logstash、Kibana)等,对前端日志进行收集和分析,了解用户行为和页面异常情况。




  10. 跨平台开发:考虑使用跨平台开发框架,如React Native、Flutter等,实现一套代码在多个平台上复用,提高开发效率。




以上是一些可以考虑的前端基建事项,根据项目需求和团队情况,可以选择适合的工具和技术进行实施。同时,持续关注前端领域的最新技术和工具,不断优化和改进前端基建,以提高开发效率和项目质量。


当涉及到前端基建时,还有一些其他的事情可以考虑:




  1. 编辑器配置和插件:为团队提供统一的编辑器配置文件,包括代码格式化、语法高亮、代码自动补全等,并推荐常用的编辑器插件,提高开发效率。




  2. 文档生成工具:使用工具如Docusaurus、VuePress等,为项目生成漂亮的文档网站,方便团队成员查阅和维护项目文档。




  3. Mock数据和接口管理:搭建一个Mock服务器,用于模拟后端接口数据,方便前端开发和测试,同时可以考虑使用接口管理工具,如Swagger等,方便接口的定义和调试。




  4. 前端监控和统计:引入前端监控工具,如Google Analytics、百度统计等,收集用户访问数据和行为信息,用于分析和优化用户体验。




  5. 移动端调试工具:使用工具如Eruda、VConsole等,帮助在移动端设备上进行调试和错误排查,提高开发效率。




  6. 自动化部署:配置自动化部署流程,将项目的代码自动部署到服务器或云平台,减少人工操作,提高发布效率和稳定性。




  7. 前端团队协作工具:使用团队协作工具,如GitLab、Bitbucket等,提供代码托管、项目管理、任务分配和团队沟通等功能,增强团队协作效率。




  8. 前端培训和知识分享:组织定期的前端培训和技术分享会,让团队成员相互学习和交流,推动技术的共享和提升。




  9. 客户端性能优化:针对移动端应用,可以使用工具如React Native Performance、Weex等,进行客户端性能优化,提高应用的响应速度和流畅度。




  10. 技术选型和评估:定期评估和研究前端技术的发展趋势,选择适用的技术栈和框架,以保持项目的竞争力和可持续发展。




以上是一些可以考虑的前端基建事项,根据项目需求和团队情况,可以选择适合的工具和技术进行实施。同时,持续关注前端领域的最新技术和工具,不断优化和改进前端基建,以提高开发效率和项目质量。


当涉及到前端基建时,还有一些其他的事情可以考虑:




  1. 统一的状态管理:引入状态管理工具,如Redux、Vuex等,帮助团队管理前端应用的状态,提高代码的可维护性和可扩展性。




  2. 前端日志记录:引入前端日志记录工具,如log4javascript、logrocket等,记录前端应用的运行日志,方便排查和解决问题。




  3. 前端代码扫描:使用静态代码扫描工具,如SonarQube、CodeClimate等,对前端代码进行扫描和分析,发现潜在的问题和漏洞。




  4. 前端数据可视化:使用数据可视化工具,如ECharts、Chart.js等,将数据以图表或图形的形式展示,增强数据的可理解性和可视化效果。




  5. 前端容灾和故障处理:制定容灾方案和故障处理流程,对前端应用进行监控和预警,及时处理和恢复故障,提高系统的可靠性和稳定性。




  6. 前端安全加固:对前端应用进行安全加固,如防止XSS攻击、CSRF攻击、数据加密等,保护用户数据的安全性和隐私。




  7. 前端版本管理:建立前端代码的版本管理机制,使用工具如Git、SVN等,管理和追踪代码的变更,方便团队成员之间的协作和版本控制。




  8. 前端数据缓存:考虑使用Local Storage、Session Storage等技术,对一些频繁使用的数据进行缓存,提高应用的性能和用户体验。




  9. 前端代码分割:使用代码分割技术,如Webpack的动态导入(Dynamic Import),将代码按需加载,减少初始加载的资源大小,提高页面加载速度。




  10. 前端性能监测工具:使用性能监测工具,如WebPageTest、GTmetrix等,监测前端应用的性能指标,如页面加载时间、资源加载时间等,进行性能优化。




以上是一些可以考虑的前端基建事项,根据项目需求和团队情况,可以选择适合的工具和技术进行实施。同时,持续关注前端领域的最新技术和工具,不断优化和改进前端基建,以提高

作者:服部
来源:juejin.cn/post/7256879435339628604
开发效率和项目质量。

收起阅读 »

通勤两万公里,聊聊我在华米的这一年~

前言 去年的今天,我正式入职了华米。现在想想,在华米的这一年经历了迷茫,也获得了成长。 迷茫 老读者朋友们都知道,在入职华米之前我在LB担任研发副经理算得上是半个管理职位,入职华米之后担任高级Android开发工程师。角色的转变加上新环境的不适应,让我迷茫了...
继续阅读 »

前言


去年的今天,我正式入职了华米。现在想想,在华米的这一年经历了迷茫,也获得了成长。



迷茫


老读者朋友们都知道,在入职华米之前我在LB担任研发副经理算得上是半个管理职位,入职华米之后担任高级Android开发工程师。角色的转变加上新环境的不适应,让我迷茫了很久。


在LB: 之前的管理职位主要就是负责技术选型架构设计等方面,相当于是一个领头羊的作用。


在华米: 多数是负责特定的业务模块


角色的转变说到底,也真实一点说,无非就是没有人以你为中心了,心里的这种落差感是存在的,加上自身适应新环境的能力差也不擅长与人交往。所以就每天处于郁闷之中,一直在思考自己是不是选择错了。毕竟在此之前也有人不理解我为什么从一个管理职位跳回了研发职位,认为这是在走回头路。而这一切我都在之后的时间中找到了答案。


寻找自己


我们部门主要负责两款APP:Mifit与Zepp,都是百万日活的APP,而我主要负责APP中的通知提醒模块,也欢迎大佬们下载使用给我提Bug。


刚开始的时候我觉得这个模块没有什么意思,因为可做的东西很少,涉及到系统兼容性的问题又太多。所以觉得自己处于一个边缘的角落。感觉做不出什么成绩,也有可能随时被T走,所以找不到自己存在的价值。


后来在一次与TL的聊天中,TL告诉我技术、思维、专项... 此处省略100个字,因为我不太能记清原话什么了。总之,每个人的存在都是有价值的,不管是工作中还是生活中。从那之后我慢慢开始接受,不断学习,也在自己的专项中做出了一些不足外人道的小成绩。


现在,我确定,我很热爱华米,很热爱现在的工作。


我眼中的华米


浓厚的技术氛围


在华米技术氛围真的是没得说,可以自由组织会议分享技术。从这里学习到了很多关于AI、性能优化、包体积优化等相关的知识。并且部门也持续探索并使用新技术不管是Flutter、还是原生Kotlin、Gradle版本的升级以及系统适配工作一直都是与时俱进的。这也是为什么近半年我在Kotlin跨平台中有所产出的原因。


来到这里之后我才发现,领导的公众号粉丝比我的多很多😂。


热爱运动


身边的许多大佬都非常热爱运动,公司也有很多运动社团,比如羽毛球、跑步、游泳、骑行等社团,每周都会组织活动,并且每个月第一周的周五下午是 FridayGO,全民运动。


体育废铁的我只是偶尔参与羽毛球活动,现在每天也在公司健身房跑步,希望自己也可以慢慢养成热爱运动的习惯。



友善


从入职到现在不管是身边同事还是TL或是其他领导,都非常的友善。关系也非常融洽,我本以为所有公司领导层都是那种高高在上的..., 感恩遇到一群志同道合的人。


说到这里,你敢信吗,我离开LB后,听前同事说之前的领导一直吐槽甚至诋毁我,真的笑不活了。


总之,如果满分为10分的话,我愿意给公司打9分,丢的那一分,不是加班,我也从来没加过班。


通勤


缺失的那一分便是丢在了我这一年两万公里,住家地址离公司单向通勤35KM,每天来回70KM,所以这一年我一共通勤了2万多公里。



不得不说,通勤距离是让幸福感降低的最大杀手。很多朋友告诉我,换房子啊,在公司附近买啊。我想说,你人还怪好哩。



最后


最后希望我在华米,有更多的N年。也告诉自己,永远积极向上,永远热泪盈眶。不破不立,未来可期~


image.png

收起阅读 »

马斯克的Twitter迎来严重危机,我国的超级App模式是否能拯救?

Meta公司近期推出的Threads 被网友戏称为“Twitter杀手”,该应用上线仅一天,用户就突破了3000 万人。外界普遍认为,这是推特上线17年来遭遇的最严峻危机。面对扎克伯格来势汹汹的挑战马斯克会如何快速组织反击? 前段时间闹得沸沸扬扬的“马扎大战”...
继续阅读 »

Meta公司近期推出的Threads 被网友戏称为“Twitter杀手”,该应用上线仅一天,用户就突破了3000 万人。外界普遍认为,这是推特上线17年来遭遇的最严峻危机。面对扎克伯格来势汹汹的挑战马斯克会如何快速组织反击?


前段时间闹得沸沸扬扬的“马扎大战”再出新剧情,继“笼斗”约架被马斯克妈妈及时叫停之后,马斯克在7月9日再次向扎克伯克打起嘴炮,这次不仅怒骂小扎是混蛋,还要公开和他比大小?!!此番马斯克的疯狂言论,让网友直呼他不是疯了就是账号被盗了。



互联网各路“吃瓜群众”对于大佬们宛如儿戏般的掐架喜闻乐见,摇旗呐喊!以至于很多人忘了这场闹剧始于一场商战:“马扎大战”开始之初,年轻的扎克伯格先发制人,率先挥出一记左钩拳——Threads,打得老马措手不及。


Threads 被网友戏称“Twitter杀手”,该应用上线仅一天,用户就突破了3000 万人。其中,不乏从推特中逃离的各界名流。舆论普遍认为,这是Twitter上线17年来遭遇的最严峻危机。



紧接着马斯克还以一记右勾拳,一封律师函向小扎发难,称Meta公司“非法盗用推特的商业秘密和其他知识产权的行为”。虽然Meta公司迅速回应,否认其团队成员中有Twitter的前雇员。但这样的回应似乎没有什么力度,Threads在功能、UI设计上均与Twitter相似,并在相关宣传中表示,Threads“具有良好的运营”,并称其为当前“一片混乱中的”Twitter的绝佳替代品。


社交平台之战的第一个回合,小扎向老马发起了猛烈的攻势。吃了一记闷拳的马斯克除了打嘴炮之外,会如何快速组织有效的反击?


会不会是老马嘴里的“非秘密武器”App X —App of Everything?


超级App或成为Twitter反击重拳


时间回溯到去年,在收购Twitter之前,马斯克就放出豪言即将创建一款他称之为“App X”的功能包罗万有的超级应用软件(Super App), 在他的愿景中,超级 “App X”就如同多功能瑞士军刀(Swiss Army Knife)般,能够包办用户日常生活大小事,包括:社交、购物、打车、支付等等。他希望这款App可以成为美国首个集食、衣、住、行功能于一身的平台。收购Twitter,似乎给了他改造实现这个超级App的起步可能。


马斯克坦言这是从微信的经营模式中汲取的灵感。微信一直被视为“超级应用程序”的代表,作为一体化平台,满足了用户的各种需求,包括即时通讯、社交、支付等等。在去年6月的推特全体员工大会上,马斯克就表示“我们还没有一个像微信那样优秀的应用,所以我的想法是为何不借鉴微信”。马斯克还在推特上写到“购买推特是创建App X的加速器,这是一个超级App(Everything App)。”


从他接手Twitter的任期开始,马斯克便加快推动超级 “App X”的发展步伐。对标于微信,除了社交功能之外,还将推出支付与电子商务。而获得监管许可是实现支付服务的重要第一步,支付也成了推特转型超级 “App X”的第一步,除了商业的必要性外,此举多少还有点宿命感。要知道,马斯克是从支付行业起家的,1999 年他投资 1200 万美元与Intuit前首席执行官 Bill Harris 共同创立了 X.com,而这家公司就是PayPal的前身。


据英国《金融时报》 1月份报道,Twitter 已经开始申请联邦和州监管许可。同时Twitter内部正在开发电子支付功能,未来更会整合其他金融服务,以实现超级App的终极目标。


但是,在亚洲“超级应用”巨头之外,几乎没有消息应用实现支付服务的先例,Whats App和Telegram 都未推出类似服务。老马领导下的Twitter,能不能成功?


添加了支付能力,也只不过是迈向“超级”的第一小步。挑战在于怎么把“everything”卷进来:衣食住行的数字服务、各行各业的商业场景。在微信世界,everything = 小程序。老马是否也要开发一套Twitter版小程序技术、缔造一个“Twitter小程序”宇宙?



“超级App”技术已实现普世化


事实上,马斯克并非“Super App ”技术理念在欧美的唯一拥趸。超级App的雄心壮志多年来早已成为美国公司管理层炫酷PPT展示中的常客了,甚至连沃尔玛都曾考虑过超级App的计划。


全球权威咨询机构Gartner发布的企业机构在2023年需要探索的十大战略技术趋势中也提到了超级应用。并预测,到2027年,全球50%以上的人口将成为多个超级应用的日活跃用户。


国外互联网巨头们开始对超级App技术趋之若鹜,但超级App的技术,是不是只有巨头才能拥有呢?


答案是否定的。互联网技术往往领先于企业应用5~7年,现在这个技术正在进入企业软件世界,任何行业的任何企业都可以拥有。


一种被称为“小程序容器”的技术,是构建超级App的核心,目前已经完全实现普及商用。背后推手是 FinClip,它作为当前市场上唯一独立小程序容器技术产品,致力于把制造超级App的技术带进各行各业,充当下一代企业数字化软件的技术底座。


超级App的技术实现,原理上是围绕一种内容载体,由三项技术共同组成:内容载体通常是某种形态的“轻巧应用”——读者最容易理解的,当然就是小程序,万事万物的数字场景,以小程序形态出现。马斯克大概率在把Twitter改造成他所谓的App X的过程中,要发展出一种类似的东西。反正在国内这就叫小程序,在W3C正在制定的标准里,这叫做Mini-App。我们就姑且依照大家容易理解的习惯,把这种“轻巧应用”称之为小程序吧。


围绕小程序,一个超级App需要在设备端实现“安全沙箱”+ “运行时”,负责把小程序从网上下载、关在一个安全隔离环境中,然后解释运行小程序内容;小程序内容的“镜像”(也就是代码包),则是发布在云端的小程序应用商店里,供超级App的用户在使用到某个商业场景或服务的时候,动态下载到设备端按需运行 – 随需随用且可以用完即弃。小程序应用商店负责了小程序的云端镜像“四态合一“(开发、测试、灰度、投产)的发布管理。


不仅仅这样,超级App本质上是一个庞大的数字生态平台,里面的小程序内容,并不是超级App的开发团队开发的,而是由第三方“进驻”和“上架”,所以,超级App还有一个非常重要的云端运营中心,负责引进和管理小程序化的数字内容生态。


超级App之所以“超级”,是因为它的生命周期(开发、测试、发版、运营),和运行在它里面的那些内容(也就是小程序)的生命周期完全独立,两者解耦,从而可运行“全世界”为其提供的内容、服务,让“全世界”为它提供“插件”而无需担心超级App本身的安全。第三方的内容无论是恶意的、有安全漏洞的或者其他什么潜在风险,并不能影响平台自身的安全稳定、以及平台上由其他人提供的内容安全保密。在建立了这样的安全与隔离机制的基础上,超级App才能实现所谓的“Economy of Scale”(规模效应),可以大开门户,放心让互联网上千行百业的企业、个人“注入插件”,产生丰富的、包罗万有的内容。


对于企业来说,拥有一个自己的超级App意味着什么呢?是超级丰富的业务场景、超级多元的合作生态、超级数量的内容开发者、以及超级敏捷的运营能力。相比传统的、封闭的、烟囱式的App,超级App实际上是帮助企业突破传统边界、建立安全开放策略、与合作伙伴实现数字化资源交换的技术手段,真正让一家企业具备平台化商业模式,加速数字化转型、增强与世界的在线连接、形成自己的网络效应。


超级App不是一个App -- Be A“world” platform


超级App+小程序,这不是互联网大平台的专利。对于传统企业来说,考虑打造自己的超级App动因至少有三:


首先,天下苦应用商店久矣。明明是纯粹企业内部一个商业决策行为,要发布某个功能或服务到自己的App上从而触达自己的客服服务自己的市场,这个发版却不得不经过不相干的第三方(App store们)批准。想象一下,你是一家银行,现在你计划在你的“数字信用卡”App里更新上架某个信用卡服务功能,你的IT完成了开发、测试,你的信用卡业主部门作了验收,你的合规、风控、法务部门通过内部的OA系统环环相扣、层层审批,现在流程到了苹果、谷歌… 排队等候审核,最后流程回到IT,服务器端一顿操作配合,正式开闸上线。你的这个信用卡服务功能,跟苹果谷歌们有一毛钱关系?但对不起,他们在你的审批流程里拥有终极话语权。


企业如果能够控制业务内容的技术实现粒度,通过自己的“服务商店”、“业务内容商店”去控制发布,让“宿主”App保持稳定,则苹果谷歌们也不用去操这个心你的App会不会每次更新都带来安全漏洞或者其他风险行为。


第二,成为一个“world platform”,企业应该有这样的“胸襟”和策略。虽然你可能不是腾讯不是推特不拥有世界级流量,这不妨碍你成为自己所在细分市场细分领域的商业世界里的平台,这里背后的思路是开放——开放平台,让全“世界”的伙伴成为我的生态,哪怕那个“世界”只存在于一个垂直领域。而这,就是数字化转型。讲那么多“数字化转型”理念,不如先落地一个技术平台作为载体,talk is cheap,show me the code。当你拥有一个在自己那个商业世界里的超级App和数以百千计的小程序的时候,你的企业已经数字化转型了。


第三,采用超级App是最有效的云化策略,把你和你的合作伙伴的内容作为小程序,挪到云端去,设备端只是加载运行和安全控制这些小程序内容的入口。在一个小小的手机上弹丸之地,“尺寸”限制了企业IT的生产力 – 无法挤进太大的团队让太多工程师同时开发生产,把一切挪到云上,那里的空间无限大,企业不再受限于“尺寸”,在云上你可以无上限的扩展技术团队,并行开发,互不认识互不打扰,为你供应无限量的内容。互联网大平台上动辄几百万个小程序是怎么来的?并行开发、快速迭代、低成本试错、无限量内容场景供应,这样的技术架构,是不是很值得企业借鉴?


做自己所在细分市场、产业宇宙里的“World Platform”吧,技术的发展已经让这一切唾手可得,也许在马斯克还在打“App of Everything”嘴炮的时候,你的超级App已经瓜熟蒂落、呱呱坠地。


作者:Finbird
来源:juejin.cn/post/7256759718811418682
收起阅读 »

无虚拟 DOM 版 Vue 进行到哪一步了?

web
前言 就在一年前的 Vue Conf 2022,尤雨溪向大家分享了一个非常令人期待的新模式:无虚拟 DOM 模式! 我看了回放之后非常兴奋,感觉这是个非常牛逼的新 feature,于是赶紧写了篇文章: 《无虚拟 DOM 版 Vue 即将到来》 鉴于可能会有...
继续阅读 »

前言


就在一年前的 Vue Conf 2022,尤雨溪向大家分享了一个非常令人期待的新模式:无虚拟 DOM 模式!


我看了回放之后非常兴奋,感觉这是个非常牛逼的新 feature,于是赶紧写了篇文章:



《无虚拟 DOM 版 Vue 即将到来》



鉴于可能会有部分人还不知道或者还没听过什么是 Vue 无虚拟 DOM 模式,我们先来简单的介绍一下:Vue 无虚拟 DOM 编译模式在官方那边叫 Vue Vapor Mode,直译过来就是:Vue 蒸汽模式。


为什么叫蒸汽模式呢?个人瞎猜的哈:第一次工业革命开创了以机器代替手工劳动的时代,并且是以蒸汽机作为动力机被广泛使用为标志的。这跟 Vue1 有点像,Vue 赶上了前端界的第一次工业革命(以声明式代替命令式的时代),此时的 Vue 还没有虚拟 DOM,也就是 Vue 的蒸汽时代。


不过万万没想到的是历史居然是个轮回,当年火的不行的虚拟 DOM 如今早已日薄西山、跌落了神坛,现在无虚拟 DOM 居然又开始重返王座。当然重返王座这个说法也不是很准确,只能说开始演变成为了一种新的流行趋势吧!这无疑让尤大想起了那个蒸汽时代的 Vue1,于是就起名为 Vapor


当然也有这么一种可能:自从为自己的框架起名为 Vue 之后,尤大就特别钟意以 V 开头的单词,不信你看:



  • Vue

  • Vite

  • Vetur

  • Volar

  • Vapor


不过以上那些都是自己瞎猜的,人家到底是不是那么想的还有待商榷。可以等下次他再开直播时发弹幕问他,看他会怎么回答。


但是吧,都过了一年多了,这个令我特别期待的新特性一点信都没有,上网搜到的内容也都是捕风捉影,这甚至让我开始怀疑 Vapor Mode 是不是要难产了?不过好在一年后的今天,Vue Conf 2023 如期而至,在那里我终于看到了自己所期待的与 Vapor Mode 有关的一系列信息。


正文



他说第三第四季度会主要开发 Vapor Mode,我听了以后直呼好家伙!合着这一年的功夫一点关于 Vapor Mode 相关的功能都没开发,鸽了一年多啊!




[译]:这是一个全新的编译策略,还是相同的模板语法一点没变,但编译后的代码性能更高。利用 Template 标签克隆元素 + 更精准的绑定,并且没有虚拟 DOM




他说 Vapor 是一个比较大的工程,所以会分阶段开发,他目前处在第一阶段。第一阶段是运行时,毕竟以前的组件编译出来的是虚拟 DOM,而 Vapor 编译出的则是真实 DOM,这需要运行时的改变。他们基本已经实现了这一点,现在正在做一些性能测试,测试效果很不错,性能有大幅度的提升。



下一阶段则是编译器,也就是说他们现在虽然能写出一些 Vapor Mode 的代码来测试性能,但写的基本上都是编译后的代码,人肉编译无疑了。



第三阶段是集成,第四阶段是兼容特殊组件,接下来进行每个阶段的详细介绍。


第一阶段



他们先实现了 v-ifv-for 等核心指令的 runtime,看来以前的 v-ifv-for 代码不能复用啊,还得重新实现。然后他们用 Benchmark 做了一些基准测试,效果非常理想,更合理的内存利用率,性能有着明显的提升。还有与服务端渲染兼容的一些东西,他们还是比较重视 SSR 的。


第二阶段



他们希望能生成一种中间语言,因为现在用 JSX 的人越来越多了,我知道肯定有人会说我身边一个用 JSX 的都没有啊(指的是 Vue JSX,不是 React JSX)咱们暂且先不讨论这种身边统计法的准确性,咱就说 Vue 的那些知名组件库,大家可以去看看他们有多少都用了 JSX 来进行开发的。只能说是 JSX 目前相对于 SFC 而言用的比较少而已,但它的用户量其实已经很庞大了:



我知道肯定还会有人说:这个统计数据不准,别的包依赖了这个包,下载别的包的时候也会顺带着下载这个包。其实这个真的没必要杠,哪怕说把这个数据减少到一半那都是每周 50 万的下载量呢!就像是国内 185 的比例很小吧?但你能说国内 185 的人很少么?哪怕比例小,但由于总数大,一相乘也照样是个非常庞大的数字。


Vue 以前是通过 render 函数来进行组件的渲染的,而如今 Vapor Mode 已经没有 render 函数了,所以不能再手写 render 了,来看一个 Vue 官网的例子:



由于 Vapor Mode 不支持 render 函数,如果想要拥有同样的灵活性那就只有 JSX,所以他们希望 SFCJSX 能编译成同一种中间语言,然后再编译为真实 DOM


第三阶段



尤大希望 Vapor Mode 是个渐进式的功能而不是破坏性功能,所以他们要做的是让 Vapor Mode 的代码可以无缝嵌入到你现有的项目中而不必重构。不仅可以选择在组件级别嵌入,甚至还可以选择在项目的性能瓶颈部分嵌入 Vapor Mode。如果你开发的是一个新项目的话,你也可以让整个项目都是 Vapor Mode,这样的话就可以完全删除掉虚拟 DOM 运行时,打包出来的尺寸体积会更小。


最牛逼的是还可以反向操作,还可以在无虚拟 DOM 组件里运行虚拟 DOM 组件。比方说你开发了款无虚拟 DOM 应用,但你需要组件库,组件库是虚拟 DOM 写的,没关系,照样可以完美运行!


第四阶段



这一阶段要让 Vapor 支持一些特殊组件,包括:



  • <transition>

  • <keep-alive>

  • <teleport>

  • <suspense>


等这一阶段忙完,整个 Vapor Mode 就可以正式推出了。


源码解析


本想着带大家看看源码,但非常不幸的是目前没在 GitHubVue 仓库里发现任何有关 Vapor Mode 的分支,可能是还没传呢吧。关注我,我会实时紧跟 Vue Vapor 的动态,并会试图带着大家理解源码。其实我是希望他能早点把源码给放出来的,因为一个新功能或者一个新项目就是在最初始的阶段最好理解,源码也不会过于的复杂,后续随着功能的疯狂迭代慢慢的就不那么易于理解了。而且跟着最初的源码也可以很好的分析出他的后续思路,想要实现该功能后面要怎么做,等放出下一阶段源码时就能很好的延续这种思路,这对于我们学习高手思路而言非常有帮助。


而且我怀疑会有些狗面试官现在就开始拿跟这玩意有关的东西做面试题了,你别看这项功能还没正式推出,但有些狗官就是喜欢问这些,希望能把你问倒以此来压你价。


我们经常调侃说学不动了,尤雨溪还纳闷这功能不影响正常使用啊?有啥学习成本呢?如果他真的了解国情的话就会知道学不动的压根就

作者:手撕红黑树
来源:juejin.cn/post/7256983702810181688
不是写法,而是源码!

收起阅读 »