注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

JSON非常慢:这里有更快的替代方案!

web
是的,你没听错!JSON,这种在网络开发中普遍用于数据交换的格式,可能正在拖慢我们的应用程序。在速度和响应性至关重要的世界里,检查 JSON 的性能影响至关重要。在这篇博客中,深入探讨 JSON 可能成为应用程序瓶颈的原因,并探索更快的替代方法和优化技术,使您...
继续阅读 »

是的,你没听错!JSON,这种在网络开发中普遍用于数据交换的格式,可能正在拖慢我们的应用程序。在速度和响应性至关重要的世界里,检查 JSON 的性能影响至关重要。在这篇博客中,深入探讨 JSON 可能成为应用程序瓶颈的原因,并探索更快的替代方法和优化技术,使您的应用程序保持最佳运行状态。


JSON 是什么,为什么要关心?


image.png


JSON 是 JavaScript Object Notation 的缩写,一种轻量级数据交换格式,已成为应用程序中传输和存储数据的首选。它的简单性和可读格式使开发者和机器都能轻松使用。但是,为什么要在项目中关注 JSON 呢?


JSON 是应用程序中数据的粘合剂。它是服务器和客户端之间进行数据通信的语言,也是数据库和配置文件中存储数据的格式。从本质上讲,JSON 在现代网络开发中起着举足轻重的作用。


JSON 的流行以及人们使用它的原因...


主要有就下几点:



  1. 人类可读格式:JSON 采用简单明了、基于文本的结构,便于开发人员和非开发人员阅读和理解。这种人类可读格式增强了协作,简化了调试。

  2. 语言无关:JSON 与任何特定编程语言无关。它是一种通用的数据格式,几乎所有现代编程语言都能对其进行解析和生成,因此具有很强的通用性。

  3. 数据结构一致性:JSON 使用键值对、数组和嵌套对象来实现数据结构的一致性。这种一致性使其具有可预测性,便于在各种编程场景中使用。

  4. 浏览器支持:浏览器原生支持 JSON,允许应用程序与服务器进行无缝通信。这种本地支持极大地促进了 JSON 在开发中的应用。

  5. JSON API:许多服务和应用程序接口默认以 JSON 格式提供数据。这进一步巩固了 JSON 在网络开发中作为数据交换首选的地位。

  6. JSON 模式:开发人员可以使用 JSON 模式定义和验证 JSON 数据的结构,从而为其应用程序增加一层额外的清晰度和可靠性。


鉴于这些优势,难怪全球的开发人员都依赖 JSON 来满足他们的数据交换需求。不过,随着我们深入探讨,会发现与 JSON 相关的潜在性能问题以及如何有效解决这些挑战。


对速度的需求


应用速度和响应速度的重要性


在当今快节奏的数字环境中,应用程序的速度和响应能力是不容忽视的。用户希望在网络和移动应用中即时获取信息、快速交互和无缝体验。对速度的这种要求是由多种因素驱动的:



  1. 用户期望:用户已习惯于从数字互动中获得闪电般快速的响应。他们不想等待网页加载或应用程序响应。哪怕是几秒钟的延迟,都会导致用户产生挫败感并放弃使用。

  2. 竞争优势:速度可以成为重要的竞争优势。与反应慢的应用程序相比,反应迅速的应用程序往往能更有效地吸引和留住用户。

  3. 搜索引擎排名:谷歌等搜索引擎将页面速度视为排名因素。加载速度更快的网站往往在搜索结果中排名靠前,从而提高知名度和流量。

  4. 转换率:电子商务网站尤其清楚速度对转换率的影响。网站速度越快,转换率越高,收入也就越高。

  5. 移动性能:随着移动设备的普及,对速度的需求变得更加重要。移动用户的带宽和处理能力往往有限,因此,快速的应用程序性能必不可少。


JSON 会拖慢我们的应用程序吗?


在某些情况下,JSON 可能是导致应用程序运行速度减慢的罪魁祸首。解析 JSON 数据的过程,尤其是在处理大型或复杂结构时,可能会耗费宝贵的毫秒时间。此外,低效的序列化和反序列化也会影响应用程序的整体性能


JSON 为什么会变慢


1.解析开销


JSON 数据到达应用程序后,必须经过解析过程才能转换成可用的数据结构。解析过程可能相对较慢,尤其是在处理大量或深度嵌套的 JSON 数据时。


2.序列化和反序列化


JSON 要求在从客户端向服务器发送数据时进行序列化(将对象编码为字符串),并在接收数据时进行反序列化(将字符串转换回可用对象)。这些步骤会带来开销并影响应用程序的整体速度。


在微服务架构的世界里,JSON 通常用于在服务之间传递消息。但是,JSON 消息需要序列化和反序列化,这两个过程会带来巨大的开销。



在众多微服务不断通信的情况下,这种开销可能会累积起来,有可能会使应用程序减慢到影响用户体验的程度。



image.png


3.字符串操作


JSON 以文本为基础,主要依靠字符串操作来进行连接和解析等操作。与处理二进制数据相比,字符串处理速度较慢。


4.缺乏数据类型


JSON 的数据类型(如字符串、数字、布尔值)有限。复杂的数据结构可能需要效率较低的表示方法,从而导致内存使用量增加和处理速度减慢。


image.png


5.冗长性


JSON 的人机可读设计可能导致冗长。冗余键和重复结构会增加有效载荷的大小,导致数据传输时间延长。


6.不支持二进制


JSON 缺乏对二进制数据的本地支持。在处理二进制数据时,开发人员通常需要将其编码和解码为文本,这可能会降低效率。


7.深嵌套


在某些情况下,JSON 数据可能嵌套很深,需要进行递归解析和遍历。这种计算复杂性会降低应用程序的运行速度,尤其是在没有优化的情况下。


JSON 的替代品


虽然 JSON 是一种通用的数据交换格式,但由于其在某些情况下的性能限制,开发者开始探索更快的替代格式。我们来看呓2其中的一些替代方案。


1.协议缓冲区(protobuf)


协议缓冲区(通常称为 protobuf)是谷歌开发的一种二进制序列化格式。其设计宗旨是高效、紧凑和快速。Protobuf 的二进制特性使其在序列化和反序列化时比 JSON 快得多。


何时使用:当你需要高性能数据交换时,尤其是在微服务架构、物联网应用或网络带宽有限的情况下,请考虑使用 protobuf


2. MessagePack 信息包


MessagePack 是另一种二进制序列化格式,以速度快、结构紧凑而著称。其设计目的是在保持与各种编程语言兼容的同时,提高比 JSON 更高的效率。


何时使用:当你需要在速度和跨语言兼容性之间取得平衡时,MessagePack 是一个不错的选择。它适用于实时应用程序和对减少数据量有重要要求的情况。


3. BSON(二进制 JSON)


BSON 或二进制 JSON 是一种从 JSON 衍生出来的二进制编码格式。它保留了 JSON 的灵活性,同时通过二进制编码提高了性能。BSON 常用于 MongoDB 等数据库。


何时使用:如果你正在使用 MongoDB,或者需要一种能在 JSON 和二进制效率之间架起桥梁的格式,那么 BSON 就是一个很有价值的选择。


4. Apache Avro(阿帕奇 Avro)


Apache Avro 是一个数据序列化框架,专注于提供一种紧凑的二进制格式。它基于模式,可实现高效的数据编码和解码。


何时使用:Avro 适用于模式演进非常重要的情况,如数据存储,以及需要在速度和数据结构灵活性之间取得平衡的情况。


与 JSON 相比,这些替代方案在性能上有不同程度的提升,具体选择取决于您的具体使用情况。通过考虑这些替代方案,您可以优化应用程序的数据交换流程,确保将速度和效率放在开发工作的首位。


image.png


每个字节的重要性:优化数据格式


JSON 数据


下面是我们的 JSON 数据示例片段:


{
"id": 1, // 14 bytes
"name": "John Doe", // 20 bytes
"email": "johndoe@example.com", // 31 bytes
"age": 30, // 9 bytes
"isSubscribed": true, // 13 bytes
"orders": [ // 11 bytes
{ // 2 bytes
"orderId": "A123", // 18 bytes
"totalAmount": 100.50 // 20 bytes
}, // 1 byte
{ // 2 bytes
"orderId": "B456", // 18 bytes
"totalAmount": 75.25 // 19 bytes
} // 1 byte
] // 1 byte
}

JSON 总大小: ~139 字节


JSON 功能多样,易于使用,但也有缺点,那就是它的文本性质。每个字符、每个空格和每个引号都很重要。在数据大小和传输速度至关重要的情况下,这些看似微不足道的字符可能会产生重大影响。


效率挑战:使用二进制格式减少数据大小


现在,我们提供其他格式的数据表示并比较它们的大小:


协议缓冲区 (protobuf)


syntax = "proto3";

message User {
int32 id = 1;
string name = 2;
string email = 3;
int32 age = 4;
bool is_subscribed = 5;
repeated Order orders = 6;

message Order {
string order_id = 1;
float total_amount = 2;
}
}

0A 0E 4A 6F 68 6E 20 44 6F 65 0C 4A 6F 68 6E 20 44 6F 65 65 78 61 6D 70 6C 65 2E 63 6F 6D 04 21 00 00 00 05 01 12 41 31 32 33 03 42 DC CC CC 3F 05 30 31 31 32 34 34 35 36 25 02 9A 99 99 3F 0D 31 02 42 34 35 36 25 02 9A 99 99 3F

协议缓冲区总大小: ~38 字节


MessagePack


二进制表示法(十六进制):


a36a6964000000000a4a6f686e20446f650c6a6f686e646f65406578616d706c652e636f6d042100000005011241313302bdcccc3f0530112434353625029a99993f

信息包总大小: ~34 字节


Binary Representation (Hexadecimal):


3e0000001069640031000a4a6f686e20446f6502656d61696c006a6f686e646f65406578616d706c652e636f6d1000000022616765001f04370e4940

BSON 总大小: ~43 字节


Avro


二进制表示法(十六进制):


0e120a4a6f686e20446f650c6a6f686e646f65406578616d706c652e636f6d049a999940040a020b4108312e3525312e323538323539

Avro 总大小: ~32 字节


image.png


现在,你可能想知道,为什么这些格式中的某些会输出二进制数据,但它们的大小却各不相同。Avro、MessagePack 和 BSON 等二进制格式具有不同的内部结构和编码机制,这可能导致二进制表示法的差异,即使它们最终表示的是相同的数据。下面简要介绍一下这些差异是如何产生的:


1. Avro



  • Avro 使用模式对数据进行编码,这种模式通常包含在二进制表示法中。

  • Avro 基于模式的编码通过提前指定数据结构,实现了高效的数据序列化和反序列化。

  • Avro 的二进制格式设计为自描述格式,这意味着模式信息包含在编码数据中。这种自描述性使 Avro 能够保持不同版本数据模式之间的兼容性。


2. MessagePack



  • MessagePack 是一种二进制序列化格式,直接对数据进行编码,不包含模式信息。

  • 它使用长度可变的整数和长度可变的字符串的紧凑二进制表示法,以尽量减少空间使用。

  • MessagePack 不包含模式信息,因此更适用于模式已提前知晓并在发送方和接收方之间共享的情况。



3. BSON



  • BSON 是 JSON 数据的二进制编码,包括每个值的类型信息。

  • BSON 的设计与 JSON 紧密相连,但它增加了二进制数据类型,如 JSON 缺乏的日期和二进制数据。

  • 与 MessagePack 一样,BSON 不包括模式信息。


这些设计和编码上的差异导致了二进制表示法的不同:



  • Avro 包含模式信息并具有自描述性,因此二进制文件稍大,但与模式兼容。

  • MessagePack 的编码长度可变,因此非常紧凑,但缺乏模式信息,因此适用于已知模式的情况。

  • BSON 与 JSON 关系密切,并包含类型信息,与 MessagePack 等纯二进制格式相比,BSON 的大小会有所增加。


总之,这些差异源于每种格式的设计目标和特点。Avro 优先考虑模式兼容性,MessagePack 侧重于紧凑性,而 BSON 在保持类似 JSON 结构的同时增加了二进制类型。格式的选择取决于您的具体使用情况和要求,如模式兼容性、数据大小和易用性。


优化 JSON 性能


下面是一些优化 JSON 性能的实用技巧以及代码示例和最佳实践:


1.最小化数据大小



  • 使用简短的描述性键名:选择简洁但有意义的键名,以减少 JSON 对象的大小


// Inefficient
{
"customer_name_with_spaces": "John Doe"
}

// Efficient
{
"customerName": "John Doe"
}


  • 尽可能缩写:在不影响清晰度的情况下,考虑对键或值使用缩写。


// 效率低
{
"transaction_type": "purchase"
}

// 效率高
{
"txnType": "purchase"
}

2.明智使用数组



  • 尽量减少嵌套:避免深度嵌套数组,因为它们会增加解析和遍历 JSON 的复杂性。


// 效率低
{
"order": {
"items": {
"item1": "Product A",
"item2": "Product B"
}
}
}

// 效率高
{
"orderItems": ["Product A", "Product B"]
}

3.优化数字表示法


尽可能使用整数:如果数值可以用整数表示,就用整数代替浮点数。


// 效率低
{
"quantity": 1.0
}

// 效率高
{
"quantity": 1
}

4.删除冗余


避免重复数据:通过引用共享值来消除冗余数据。


// 效率低
{
"product1": {
"name": "Product A",
"price": 10
},
"product2": {
"name": "Product A",
"price": 10
}
}

// 效率高
{
"products": [
{
"name": "Product A",
"price": 10
},
{
"name": "Product B",
"price": 15
}
]
}

5.使用压缩


应用压缩算法:如果适用,在传输过程中使用 Gzipor Brotlito 等压缩算法来减小 JSON 有效负载的大小。


// 使用 zlib 进行 Gzip 压缩的 Node.js 示例
const zlib = require('zlib');

const jsonData = {
// 在这里填入你的 JSON 数据
};

zlib.gzip(JSON.stringify(jsonData), (err, compressedData) => {
if (!err) {
// 通过网络发送 compressedData
}
});


6.采用服务器端缓存:


缓存 JSON 响应:实施服务器端缓存,高效地存储和提供 JSON 响应,减少重复数据处理的需要。


7.配置文件和优化


剖析性能:使用剖析工具找出 JSON 处理代码中的瓶颈,然后优化这些部分。


实际优化:在实践中加快 JSON 的处理速度


在本节中,我们将探讨实际案例,这些案例在使用 JSON 时遇到性能瓶颈并成功克服。我们会看到诸如 LinkedIn、Auth0、Uber 等知名技术公司如何解决 JSON 的限制并改善他们应用的性能。这些案例为如何提升应用处理速度和响应性提供了实用的策略。


1.LinkedIn 的协议缓冲区集成:



  • 挑战:LinkedIn 面临的挑战是 JSON 的冗长以及由此导致的网络带宽使用量增加,从而导致延迟增加。

  • 解决方案:他们采用了 Protocol Buffers,这是一种二进制序列化格式,用以替换微服务通信中的 JSON。

  • 影响:这一优化将延迟降低了 60%,提高了 LinkedIn 服务的速度和响应能力。


2.Uber 的 H3 地理索引:


挑战:Uber 使用 JSON 来表示各种地理空间数据,但解析大型数据集的 JSON 会降低其算法速度。


解决方案:他们引入了 H3 Geo-Index,这是一种用于地理空间数据的高效六边形网格系统,可减少 JSON 解析开销。


影响:这一优化大大加快了地理空间业务的发展,增强了 Uber 的叫车和地图服务。


3.Slack 的信息格式优化:


挑战:Slack 需要在实时聊天中传输和呈现大量 JSON 格式的消息,这导致了性能瓶颈。


解决方案:他们优化了 JSON 结构,减少了不必要的数据,只在每条信息中包含必要的信息。


影响:这项优化使得消息展现更快,从而提高了 Slack 用户的整体聊天性能。


4.Auth0 的协议缓冲区实现:


挑战:Auth0 是一个流行的身份和访问管理平台,在处理身份验证和授权数据时面临着 JSON 的性能挑战。


解决方案:他们采用协议缓冲区(Protocol Buffers)来取代 JSON,以编码和解码与身份验证相关的数据。


影响:这一优化大大提高了数据序列化和反序列化的速度,从而加快了身份验证流程,并增强了 Auth0 服务的整体性能。


这些现实世界中的例子展示了通过优化策略解决 JSON 的性能挑战如何对应用程序的速度、响应速度和用户体验产生实质性的积极影响。它们强调了考虑替代数据格式和高效数据结构的重要性,以克服各种情况下与 JSON 相关的速度减慢问题。


结论


在不断变化的网络开发环境中,优化 JSON 性能是一项宝贵的技能,它能让你的项目与众不同,并确保你的应用程序在即时数字体验时代茁壮成长。


作者:王大冶
来源:juejin.cn/post/7303424117243297807
收起阅读 »

解锁前端难题:亲手实现一个图片标注工具

web
本文为稀土掘金技术社区首发签约文章,30 天内禁止转载,30 天后未获授权禁止转载,侵权必究! 业务中涉及图片的制作和审核功能,审核人员需要在图片中进行标注,并说明存在的问题,标注过程中需要支持放大缩小,移动等交互,将业务剥离,这个需求,可以定义为实现一个图...
继续阅读 »

本文为稀土掘金技术社区首发签约文章,30 天内禁止转载,30 天后未获授权禁止转载,侵权必究!



业务中涉及图片的制作和审核功能,审核人员需要在图片中进行标注,并说明存在的问题,标注过程中需要支持放大缩小,移动等交互,将业务剥离,这个需求,可以定义为实现一个图片标注功能。


实现这个功能并不容易,其涉及的前端知识点众多,本文带领大家从零到一,亲手实现一个,支持缩放,移动,编辑的图片标注功能,文字描述是抽象的,眼见为实,实现效果如下所示:


Kapture 2024-03-20 at 18.43.56.gif


技术方案


这里涉及两个关键功能,一个是绘制,包括缩放和旋转,一个是编辑,包括选取和修改尺寸,涉及到的技术包括,缩放,移动,和自定义形状的绘制(本文仅实现矩形),绘制形状的选取,改变尺寸和旋转角度等。


从大的技术选型来说,有两种实现思路,一种是 canvas,一种是 dom+svg,下面简单介绍下两种思路和优缺点。


canvas 可以方便实现绘制功能,但编辑功能就比较困难,当然这可以使用库来实现,这里我们考虑自己亲手实现功能。



  • 优点

    • 性能较好,尤其是在处理大型图片和复杂图形时。

    • 支持更复杂的图形绘制和像素级操作。

    • 一旦图形绘制在 Canvas 上,就不会受到 DOM 的影响,减少重绘和回流。



  • 缺点

    • 交互相对复杂,需要手动管理图形的状态和事件。

    • 对辅助技术(如屏幕阅读器)支持较差。



  • 可能遇到的困难

    • 实现复杂的交互逻辑(如选取、移动、修改尺寸等)可能比较繁琐。

    • 在缩放和平移时,需要手动管理坐标变换和图形重绘。




dom+svg 也可以实现功能,缩放和旋转可以借助 css3 的 transform。



  • 优点

    • 交互相对简单,可以利用 DOM 事件系统和 CSS。

    • 对辅助技术支持较好,有助于提高可访问性。



  • 缺点

    • 在处理大型图片和复杂图形时,性能可能不如 Canvas。

    • SVG 元素数量过多时,可能会影响页面性能。



  • 可能遇到的困难

    • 在实现复杂的图形和效果时,可能需要较多的 SVG 知识和技巧。

    • 管理大量的 SVG 元素和事件可能会使代码变得复杂。




总的来说,如果对性能有较高要求,或需要进行复杂的图形处理和像素操作,可以选择基于 Canvas 的方案。否则可以选择基于 DOM + SVG 的方案。在具体实现时,可以根据项目需求和技术栈进行选择。


下面我们选择基于 canvas 的方案,通过例子,一步一步实现完成功能,让我们先从最简单的开始。


渲染图片


本文我们不讲解 canvas 基础,如果你不了解 canvas,可以先在网上找资料,简单学习下,图片的渲染非常简单,只用到一个 API,这里我们直接给出代码,示例如下:


这里我们提前准备一个 canvas,宽高设定为 1000*700,这里唯一的一个知识点就是要在图片加载完成后再绘制,在实战中,需要注意绘制的图片不能跨域,否则会绘制失败。


<body>
<div>
<canvas id="canvas1" width="1000" height="700"></canvas>
</div>
<script>
const canvas1 = document.querySelector('#canvas1');
const ctx1 = canvas1.getContext('2d');
let width = 1000;
let height = 700;

let img = new Image();
img.src = './bg.png';
img.onload = function () {
draw();
};

function draw() {
console.log('draw');
ctx1.drawImage(img, 0, 0, width, height);
}
</script>
</body>

现在我们已经成功在页面中绘制了一张图片,这非常简单,让我们继续往下看吧。


缩放


实现图片缩放功能,我们需要了解两个关键的知识点:如何监听缩放事件和如何实现图片缩放。


先来看第一个,我用的是 Mac,在 Mac 上可以通过监听鼠标的滚轮事件来实现缩放的监听。当用户使用鼠标滚轮时,会触发 wheel 事件,我们可以通过这个事件的 deltaY 属性来判断用户是向上滚动(放大)还是向下滚动(缩小)。


可以看到在 wheel 事件中,我们修改了 scale 变量,这个变量会在下面用到。这里添加了对最小缩放是 1,最大缩放是 3 的限制。


document.addEventListener(
'wheel',
function (event) {
if (event.ctrlKey) {
// detect pinch
event.preventDefault(); // prevent zoom
if (event.deltaY < 0) {
console.log('Pinching in');
if (scale < 3) {
scale = Math.min(scale + 0.1, 3);
draw();
}
} else {
console.log('Pinching out');
if (scale > 1) {
scale = Math.max(scale - 0.1, 1);
draw();
}
}
}
},
{ passive: false }
);

图片缩放功能,用到了 canvas 的 scale 函数,其可以修改绘制上下文的缩放比例,示例代码如下:


我们添加了clearRect函数,这用来清除上一次绘制的图形,当需要重绘时,就需要使用clearRect函数。


这里需要注意开头和结尾的 save 和 restore 函数,因为我们会修改 scale,如果不恢复的话,其会影响下一次绘制,一般在修改上下文时,都是通过 save 和 restore 来复原的。


let scale = 1;

function draw() {
console.log('draw');
ctx1.clearRect(0, 0, width, height);
ctx1.save();
ctx1.scale(scale, scale);
ctx1.drawImage(img, 0, 0, width, height);
ctx1.restore();
}

这里稍微解释一下 scale 函数,初次接触,可能会不太好理解。在 Canvas 中使用 scale 函数时,重要的是要理解它实际上是在缩放绘图坐标系统,而不是直接缩放绘制的图形。当你调用 ctx.scale(scaleX, scaleY) 时,你是在告诉 Canvas 之后的所有绘图操作都应该在一个被缩放的坐标系统中进行。


这意味着,如果你将缩放比例设置为 2,那么在这个缩放的坐标系统中,绘制一个宽度为 50 像素的矩形,实际上会在画布上产生一个宽度为 100 像素的矩形。因为在缩放的坐标系统中,每个单位长度都变成了原来的两倍。


因此,当我们谈论 scale 函数时,重点是要记住它是在缩放整个绘图坐标系统,而不是单独的图形。这就是为什么在使用 scale 函数后,所有的绘图操作(包括位置、大小等)都会受到影响。


现在我们已经实现了图片的缩放功能,效果如下所示:


Kapture 2024-03-21 at 15.20.58.gif


鼠标缩放


细心的你可能发现上面的缩放效果是基于左上角的,基于鼠标点缩放意味着图片的缩放中心是用户鼠标所在的位置,而不是图片的左上角或其他固定点。这种缩放方式更符合用户的直觉,可以提供更好的交互体验。


为了实现这种效果,可以使用 tanslate 来移动原点,canvas 中默认的缩放原点是左上角,具体方法是,可以在缩放前,将缩放原点移动到鼠标点的位置,缩放后,再将其恢复,这样就不会影响后续的绘制,实现代码如下所示:


let scaleX = 0;
let scaleY = 0;

function draw() {
ctx1.clearRect(0, 0, width, height);
ctx1.save();
// 注意这行1
ctx1.translate(scaleX, scaleY);
ctx1.scale(scale, scale);
// 注意这行2
ctx1.translate(-scaleX, -scaleY);
ctx1.drawImage(img, 0, 0, width, height);
ctx1.restore();
}

scaleX 和 scaleY 的值,可以在缩放的时候设置即可,如下所示:


// zoom
document.addEventListener(
'wheel',
function (event) {
if (event.ctrlKey) {
if (event.deltaY < 0) {
if (scale < 3) {
// 注意这里两行
scaleX = event.offsetX;
scaleY = event.offsetY;
scale = Math.min(scale + 0.1, 3);
draw();
}
}
// 省略代码
}
},
{ passive: false }
);

现在我们已经实现了图片的鼠标缩放功能,效果如下所示:


3.gif


移动视口


先解释下放大时,可见区域的概念,好像叫视口吧
当处于放大状态时,会导致图像只能显示一部分,此时需要能过需要可以移动可见的图像,
这里选择通过触摸板的移动,也就是 wheel 来实现移动视口


通过 canvas 的 translate 来实现改变视口


在图片放大后,整个图像可能无法完全显示在 Canvas 上,此时只有图像的一部分(即可见区域)会显示在画布上。这个可见区域也被称为“视口”。为了查看图像的其他部分,我们需要能够移动这个视口,即实现图片的平移功能。


在放大状态下,视口的大小相对于整个图像是固定的,但是它可以在图像上移动以显示不同的部分。你可以将视口想象为一个固定大小的窗口,你通过这个窗口来观察一个更大的图像。当你移动视口时,窗口中显示的图像部分也会相应改变。


为了实现移动视口,我们可以通过监听触摸板的移动事件(也就是 wheel 事件)来改变视口的位置。当用户通过触摸板进行上下或左右滑动时,我们可以相应地移动视口,从而实现图像的平移效果。


我们可以使用 Canvas 的 translate 方法来改变视口的位置。translate 方法接受两个参数,分别表示沿 x 轴和 y 轴移动的距离。在移动视口时,我们需要更新图片的位置,并重新绘制图像以反映新的视口位置。


代码改动如下所示:


let translateX = 0;
let translateY = 0;

function draw() {
// 此处省略代码
// 改变视口
ctx1.translate(translateX, translateY);

ctx1.drawImage(img, 0, 0, width, height);
ctx1.restore();
}

// translate canvas
document.addEventListener(
"wheel",
function (event) {
if (!event.ctrlKey) {
// console.log("translate", event.deltaX, event.deltaY);
event.preventDefault();
translateX -= event.deltaX;
translateY -= event.deltaY;
draw();
}
},
{ passive: false }
);

在这个示例中,translateXtranslateY 表示视口的位置。当用户通过触摸板进行滑动时,我们根据滑动的方向和距离更新视口的位置,并重新绘制图像。通过这种方式,我们可以实现图像的平移功能,允许用户查看图像的不同部分。


现在我们已经实现了移动视口功能,效果如下所示:


4.gif


绘制标注


为了便于大家理解,这里我们仅实现矩形标注示例,实际业务中可能存在各种图形的标记,比如圆形,椭圆,直线,曲线,自定义图形等。


我们先考虑矩形标注的绘制问题,由于 canvas 是位图,我们需要在 js 中存储矩形的数据,矩形的存储需要支持坐标,尺寸,旋转角度和是否在编辑中等。因为可能存在多个标注,所以需要一个数组来存取标注数据,我们将标注存储在reacts中,示例如下:


let rects = [
{
x: 650,
y: 350,
width: 100,
height: 100,
isEditing: false,
rotatable: true,
rotateAngle: 30,
},
];

下面将 rects 渲染到 canvas 中,示例代码如下:


代码扩机并不复杂,比较容易理解,值得一提的rotateAngle的实现,我们通过旋转上下文来实现,其旋转中心是矩形的图形的中心点,因为操作上线文,所以在每个矩形绘制开始和结束后,要通过saverestore来恢复之前的上下文。


isEditing表示当前的标注是否处于编辑状态,在这里编辑中的矩形框,我们只需设置不同的颜色即可,在后面我们会实现编辑的逻辑。


function draw() {
// 此处省略代码
ctx1.drawImage(img, 0, 0, width, height);

rects.forEach((r) => {
ctx1.strokeStyle = r.isEditing ? 'rgba(255, 0, 0, 0.5)' : 'rgba(255, 0, 0)';

ctx1.save();
if (r.rotatable) {
ctx1.translate(r.x + r.width / 2, r.y + r.height / 2);
ctx1.rotate((r.rotateAngle * Math.PI) / 180);
ctx1.translate(-(r.x + r.width / 2), -(r.y + r.height / 2));
}
ctx1.strokeRect(r.x, r.y, r.width, r.height);
ctx1.restore();
});

ctx1.restore();
}

现在我们已经实现了标注绘制功能,效果如下所示:


5.png


添加标注


为了在图片上添加标注,我们需要实现鼠标按下、移动和抬起时的事件处理,以便在用户拖动鼠标时动态地绘制一个矩形标注。同时,由于视口可以放大和移动,我们还需要进行坐标的换算,确保标注的位置正确。


首先,我们需要定义一个变量 drawingRect 来存储正在添加中的标注数据。这个变量将包含标注的起始坐标、宽度和高度等信息:


let drawingRect = null;

接下来,我们需要实现鼠标按下、移动和抬起的事件处理函数:


mousedown中我们需要记录鼠标按下时,距离视口左上角的坐标,并将其记录到全局变量startXstartY中。


mousemove时,需要更新当前在绘制矩形的数据,并调用draw完成重绘。


mouseup时,需要处理添加操作,将矩形添加到rects中,在这里我做了一个判断,如果矩形的宽高小于 1,则不添加,这是为了避免在鼠标原地点击时,误添加图形的问题。


let startX = 0;
let startY = 0;
canvas1.addEventListener('mousedown', (e) => {
startX = e.offsetX;
startY = e.offsetY;
const { x, y } = computexy(e.offsetX, e.offsetY);

console.log('mousedown', e.offsetX, e.offsetY, x, y);

drawingRect = drawingRect || {};
});
canvas1.addEventListener('mousemove', (e) => {
// 绘制中
if (drawingRect) {
drawingRect = computeRect({
x: startX,
y: startY,
width: e.offsetX - startX,
height: e.offsetY - startY,
});
draw();
return;
}
});
canvas1.addEventListener('mouseup', (e) => {
if (drawingRect) {
drawingRect = null;
// 如果绘制的矩形太小,则不添加,防止原地点击时添加矩形
// 如果反向绘制,则调整为正向
const width = Math.abs(e.offsetX - startX);
const height = Math.abs(e.offsetY - startY);
if (width > 1 || height > 1) {
const newrect = computeRect({
x: Math.min(startX, e.offsetX),
y: Math.min(startY, e.offsetY),
width,
height,
});
rects.push(newrect);
draw();
}
return;
}
});

下面我们来重点讲讲上面的computexycomputeRect函数,由于视口可以放大和移动,我们需要将鼠标点击时的视口坐标换算为 Canvas 坐标系的坐标。


宽高的计算比较简单,只需要将视口坐标除以缩放比例即可得到。但坐标的计算并不简单,这里通过视口坐标,直接去推 canvas 坐标是比较困难的,我们可以求出 canvas 坐标计算视口坐标的公式,公式推导如下:


vx: 视口坐标
x: canvas坐标
scale: 缩放比例
scaleX: 缩放原点
translateX: 视口移动位置

我们x会在如下视口操作后进行渲染成vx:
1: ctx1.translate(scaleX, scaleY);
2: ctx1.scale(scale, scale);
3: ctx1.translate(-scaleX, -scaleY);
4: ctx1.translate(translateX, translateY);

根据上面的步骤,每一步vx的推演如下:
1: vx = x + scaleX
2: vx = x * scale + scaleX
3: vx = x * scale + scaleX - scaleX * scale
4: vx = x * scale + scaleX - scaleX * scale + translateX * scale

通过上面 vx 和 x 的公式,我们可以计算出来 x 和 vx 的关系如下,我在这里走了很多弯路,导致计算的坐标一直不对,不要试图通过 vx 直接推出 x,一定要通过上面的公式来推导:


x = (vx - scaleX * (1 - scale) - translateX * scale) / scale

理解了上面坐标和宽高的计算公式,下面的代码就好理解了:


function computexy(x, y) {
const xy = {
x: (x - scaleX * (1 - scale) - translateX * scale) / scale,
y: (y - scaleY * (1 - scale) - translateY * scale) / scale,
};
return xy;
}
function computewh(width, height) {
return {
width: width / scale,
height: height / scale,
};
}
function computeRect(rect) {
const cr = {
...computexy(rect.x, rect.y),
...computewh(rect.width, rect.height),
};
return cr;
}

最后,我们需要一个函数来绘制标注矩形:


function draw() {
// 此处省略代码
if (drawingRect) {
ctx1.strokeRect(
drawingRect.x,
drawingRect.y,
drawingRect.width,
drawingRect.height
);
}
ctx1.restore();
}

现在我们已经实现了添加标注功能,效果如下所示:


6.gif


选取标注


判断选中,将视口坐标,转换为 canvas 坐标,遍历矩形,判断点在矩形内部
同时需要考虑点击空白处,清空选中状态
选中其他元素时,清空上一个选中的元素
渲染选中状态,选中状态改变边的颜色,为了明显,红色变为绿色
要是先选取元素的功能,关键要实现的判断点在矩形内部,判断点在矩形内部的逻辑比较简单,我们可以抽象为如下函数:


function poInRect({ x, y }, rect) {
return (
x >= rect.x &&
x <= rect.x + rect.width &&
y >= rect.y &&
y <= rect.y + rect.height
);
}

在点击事件中,我们拿到的是视口坐标,首先将其转换为 canvas 坐标,然后遍历矩形数组,判断是否有中选的矩形,如果有的话将其存储下来。


还需要考虑点击新元素时,和点击空白时,重置上一个元素的选中态的逻辑,代码实现如下所示:


canvas1.addEventListener('mousedown', (e) => {
startX = e.offsetX;
startY = e.offsetY;
const { x, y } = computexy(e.offsetX, e.offsetY);

const pickRect = rects.find((r) => {
return poInRect({ x, y }, r);
});

if (pickRect) {
if (editRect && pickRect !== editRect) {
// 选择了其他矩形
editRect.isEditing = false;
editRect = null;
}
pickRect.isEditing = true;
editRect = pickRect;
draw();
} else {
if (editRect) {
editRect.isEditing = false;
editRect = null;
draw();
}
drawingRect = drawingRect || {};
}
});

现在我们已经实现了选取标注功能,效果如下所示:


7.gif


移动


接下来是移动,也就是通过拖拽来改变已有图形的位置
首先需要一个变量来存取当前被拖拽元素,在 down 和 up 时更新这个元素
要实现拖拽,需要一点小技巧,在点击时,计算点击点和图形左上角的坐标差,在每次 move 时,用当前坐标减去坐标差即可
不要忘了将视口坐标,换算为 canvas 坐标哦


接下来,我们将实现通过拖拽来改变已有标注的位置的功能。这需要跟踪当前被拖拽的标注,并在鼠标移动时更新其位置。


首先,我们需要一个变量来存储当前被拖拽的标注:


let draggingRect = null;

在鼠标按下时(mousedown 事件),我们需要判断是否点击了某个标注,并将其设置为被拖拽的标注,并在鼠标抬起时(mouseup 事件),将其置空。


要实现完美的拖拽效果,需要一点小技巧,在点击时,计算点击点和图形左上角的坐标差,将其记录到全局变量shiftXshiftY,关键代码如下所示。


let shiftX = 0;
let shiftY = 0;
canvas1.addEventListener('mousedown', (e) => {
const { x, y } = computexy(e.offsetX, e.offsetY);

if (pickRect) {
// 计算坐标差
shiftX = x - pickRect.x;
shiftY = y - pickRect.y;
// 标记当前拖拽元素
draggingRect = pickRect;
draw();
}
});
canvas1.addEventListener('mouseup', (e) => {
if (draggingRect) {
// 置空当前拖拽元素
draggingRect = null;
return;
}
});

在鼠标移动时(mousemove 事件),如果有标注被拖拽,则更新其位置,关键代码如下所示。


canvas1.addEventListener('mousemove', (e) => {
const { x, y } = computexy(e.offsetX, e.offsetY);

// 当前正在拖拽矩形
if (draggingRect) {
draggingRect.x = x - shiftX;
draggingRect.y = y - shiftY;
draw();
return;
}
});

现在我们已经实现了移动功能,效果如下所示:


8.gif


修改尺寸


为了实现标注尺寸的修改功能,我们可以在标注的四个角和四条边的中点处显示小方块作为编辑器,允许用户通过拖拽这些小方块来改变标注的大小。


首先,我们需要实现编辑器的渲染逻辑。我们可以在 drawEditor 函数中添加代码来绘制这些小方块。


在这里,我们使用 computeEditRect 函数来计算标注的八个编辑点的位置,并在 drawEditor 函数中绘制这些小方块,关键代码如下所示:


在这个例子中,我们只展示了上边中间编辑点的处理逻辑,其他编辑点的处理逻辑类似。


function computeEditRect(rect) {
let width = 10;
let linelen = 16;
return {
t: {
type: "t",
x: rect.x + rect.width / 2 - width / 2,
y: rect.y - width / 2,
width,
height: width,
},
b: {// 代码省略},
l: {// 代码省略},
r: {// 代码省略},
tl: {// 代码省略},
tr: {// 代码省略},
bl: {// 代码省略},
br: {// 代码省略},
};
}
function drawEditor(rect) {
ctx1.save();
const editor = computeEditRect(rect);
ctx1.fillStyle = "rgba(255, 150, 150)";

// 绘制矩形
for (const r of Object.values(editor)) {
ctx1.fillRect(r.x, r.y, r.width, r.height);
}
ctx1.restore();
}
function draw() {
rects.forEach((r) => {
// 添加如下代码
if (r.isEditing) {
drawEditor(r);
}
});
}

接下来,我们需要实现拖动这些编辑点来改变标注大小的功能。首先,我们需要在鼠标按下时判断是否点击了某个编辑点。


在这里,我们使用 poInEditor 函数来判断鼠标点击的位置是否接近某个编辑点。如果是,则设置 startEditRect, dragingEditor, editorShiftXY 来记录正在调整大小的标注和编辑点。


let startEditRect = null;
let dragingEditor = null;
let editorShiftX = 0;
let editorShiftY = 0;
function poInEditor(point, rect) {
const editor = computeEditRect(rect);
if (!editor) return;

for (const edit of Object.values(editor)) {
if (poInRect(point, edit)) {
return edit;
}
}
}
canvas1.addEventListener('mousedown', (e) => {
startX = e.offsetX;
startY = e.offsetY;
const { x, y } = computexy(e.offsetX, e.offsetY);

if (editRect) {
const editor = poInEditor({ x, y }, editRect);
if (editor) {
// 调整大小
startEditRect = { ...editRect };
dragingEditor = editor;
editorShiftX = x - editor.x;
editorShiftY = y - editor.y;
return;
}
}
});

然后,在鼠标移动时,我们需要根据拖动的编辑点来调整标注的大小。


在这个例子中,我们只展示了上边中间编辑点的处理逻辑,其他编辑点的处理逻辑类似。通过拖动不同的编辑点,我们可以实现标注的不同方向和维度的大小调整。


canvas1.addEventListener('mousemove', (e) => {
const { x, y } = computexy(e.offsetX, e.offsetY);

// 如果存在编辑中的元素
if (editRect) {
const editor = poInEditor({ x, y }, editRect);
// 调整大小中
if (dragingEditor) {
const moveX = (e.offsetX - startX) / scale;
const moveY = (e.offsetY - startY) / scale;

switch (dragingEditor.type) {
case 't':
editRect.y = startEditRect.y + moveY;
editRect.height = startEditRect.height - moveY;
break;
}
draw();
return;
}
}
});

现在我们已经实现了修改尺寸功能,效果如下所示:


9.gif


旋转


实现旋转编辑器的渲染按钮,在顶部增加一个小方块的方式来实现,


旋转图形会影响选中图形的逻辑,即点在旋转图形里的判断,这块的逻辑需要修改


接下来实现旋转逻辑,会涉及 mousedown 和 mousemove


接下来介绍旋转,这一部分会有一定难度,涉及一些数学计算,而且旋转逻辑会修改多出代码,下面我们依次介绍。


旋转涉及两大块功能,一个是旋转编辑器,一个是旋转逻辑,我们先来看旋转编辑器,我们可以在标注的顶部增加一个用于旋转的小方块作为旋转编辑器,如下图所示:


image.png


下面修改我们的drawEditorcomputeEditRect函数,增加渲染逻辑,涉及一个方块和一条线的渲染。


其中rotr就是顶部的方块,rotl是那条竖线。


function computeEditRect(rect) {
let width = 10;
let linelen = 16;
return {
...(rect.rotatable
? {
rotr: {
type: 'rotr',
x: rect.x + rect.width / 2 - width / 2,
y: rect.y - width / 2 - linelen - width,
width,
height: width,
},
rotl: {
type: 'rotl',
x1: rect.x + rect.width / 2,
y1: rect.y - linelen - width / 2,
x2: rect.x + rect.width / 2,
y2: rect.y - width / 2,
},
}
: null),
};
}
function drawEditor(rect) {
ctx1.save();
const editor = computeEditRect(rect);
ctx1.fillStyle = 'rgba(255, 150, 150)';
const { rotl, rotr, ...rects } = editor;

// 绘制旋转按钮
if (rect.rotatable) {
ctx1.fillRect(rotr.x, rotr.y, rotr.width, rotr.height);
ctx1.beginPath();
ctx1.moveTo(rotl.x1, rotl.y1);
ctx1.lineTo(rotl.x2, rotl.y2);
ctx1.stroke();
}

// 绘制矩形
// ...
}

在实现旋转逻辑之前,先来看一个问题,如下图所示,当我们在绿色圆圈区按下鼠标时,在我们之前的逻辑中,也会触发选中状态。


image.png


这是因为我们判断点在矩形内部的逻辑,并未考虑旋转的问题,我们的矩形数据存储了矩形旋转之前的坐标和旋转角度,如下所示。


let rects = [
{
x: 650,
y: 350,
width: 100,
height: 100,
isEditing: false,
rotatable: true,
rotateAngle: 30,
},
];

解决这个问题有两个思路,一个是将旋转后矩形的四个点坐标计算出来,这种方法比较麻烦。另一个思路是逆向的,将要判断的点,以矩形的中点为中心,做逆向旋转,计算出其在 canvas 中的坐标,这个坐标,可以继续参与我们之前点在矩形内的计算。


关键代码如下所示,其中rotatePoint是计算 canvas 中的坐标,poInRotRect判断给定点是否在旋转矩形内部。


// 将点绕 rotateCenter 旋转 rotateAngle 度
function rotatePoint(point, rotateCenter, rotateAngle) {
let dx = point.x - rotateCenter.x;
let dy = point.y - rotateCenter.y;

let rotatedX =
dx * Math.cos((-rotateAngle * Math.PI) / 180) -
dy * Math.sin((-rotateAngle * Math.PI) / 180) +
rotateCenter.x;
let rotatedY =
dy * Math.cos((-rotateAngle * Math.PI) / 180) +
dx * Math.sin((-rotateAngle * Math.PI) / 180) +
rotateCenter.y;

return { x: rotatedX, y: rotatedY };
}

function poInRotRect(
point,
rect,
rotateCenter = {
x: rect.x + rect.width / 2,
y: rect.y + rect.height / 2,
},
rotateAngle = rect.rotateAngle
) {
if (rotateAngle) {
const rotatedPoint = rotatePoint(point, rotateCenter, rotateAngle);
const res = poInRect(rotatedPoint, rect);
return res;
}
return poInRect(point, rect);
}

接下来实现旋转逻辑,这需要改在 mousedown 和 mousemove 事件,实现拖动时的实时旋转。


在 mousedown 时,判断如果点击的是旋转按钮,则将当前矩形记录到全局变量rotatingRect


canvas1.addEventListener('mousedown', (e) => {
startX = e.offsetX;
startY = e.offsetY;
const { x, y } = computexy(e.offsetX, e.offsetY);

if (editRect) {
const editor = poInEditor({ x, y }, editRect);
if (editor) {
// 调整旋转
if (editor.type === 'rotr') {
rotatingRect = editRect;
prevX = e.offsetX;
prevY = e.offsetY;
return;
}
// 调整大小
}
}
});

在 mousemove 时,判断如果是位于旋转按钮上,则计算旋转角度。


canvas1.addEventListener('mousemove', (e) => {
// 绘制中
const { x, y } = computexy(e.offsetX, e.offsetY);
// 当前正在拖拽矩形

// 如果存在编辑中的元素
if (editRect) {
const editor = poInEditor({ x, y }, editRect);
console.log('mousemove', editor);

// 旋转中
if (rotatingRect) {
const relativeAngle = getRelativeRotationAngle(
computexy(e.offsetX, e.offsetY),
computexy(prevX, prevY),
{
x: editRect.x + editRect.width / 2,
y: editRect.y + editRect.height / 2,
}
);
console.log('relativeAngle', relativeAngle);
editRect.rotateAngle += (relativeAngle * 180) / Math.PI;
prevX = e.offsetX;
prevY = e.offsetY;
draw();
return;
}

// 调整大小中
}
});

将拖拽移动的距离,转换为旋转的角度,涉及一些数学知识,其原理是通过上一次鼠标位置和本次鼠标位置,计算两个点和旋转中心(矩形的中心)三个点,形成的夹角,示例代码如下:


function getRelativeRotationAngle(point, prev, center) {
// 计算上一次鼠标位置和旋转中心的角度
let prevAngle = Math.atan2(prev.y - center.y, prev.x - center.x);

// 计算当前鼠标位置和旋转中心的角度
let curAngle = Math.atan2(point.y - center.y, point.x - center.x);

// 得到相对旋转角度
let relativeAngle = curAngle - prevAngle;

return relativeAngle;
}

现在我们已经实现了旋转功能,效果如下所示:


10.gif


总结


在本文中,我们一步一步地实现了一个功能丰富的图片标注工具。从最基本的图片渲染到复杂的标注编辑功能,包括缩放、移动、添加标注、选择标注、移动标注、修改标注尺寸、以及标注旋转等,涵盖了图片标注工具的核心功能。


通过这个实例,我们可以看到,实现一个前端图片标注工具需要综合运用多种前端技术和知识,包括但不限于:



  • Canvas API 的使用,如绘制图片、绘制形状、图形变换等。

  • 鼠标事件的处理,如点击、拖拽、滚轮缩放等。

  • 几何计算,如点是否在矩形内、旋转角度的计算等。


希望这个实例能够为你提供一些启发和帮助,让你在实现自己的图片标注工具时有一个参考和借鉴。


更进一步


站在文章的角度,到此为止,下面让我们站在更高的维度思考更进一步的可能,我们还能继续做些什么呢?


在抽象层面,我们可以考虑将图片标注工具的核心功能进行进一步的抽象和封装,将其打造成一个通用的开源库。这样,其他开发者可以直接使用这个库来快速实现自己的图片标注需求,而无需从零开始。为了实现这一目标,我们需要考虑以下几点:



  • 通用性:库应该支持多种常见的标注形状和编辑功能,以满足不同场景的需求。

  • 易用性:提供简洁明了的 API 和文档,使得开发者能够轻松集成和使用。

  • 可扩展性:设计上应该留有足够的灵活性,以便开发者可以根据自己的特定需求进行定制和扩展。

  • 性能优化:注重性能优化,确保库在处理大型图片或复杂标注时仍能保持良好的性能。


在产品层面,我们可以基于这个通用库,进一步开发成一个功能完备的图片标注工具,提供开箱即用的体验。这个工具可以包括以下功能:



  • 多种标注类型:支持矩形、圆形、多边形等多种标注类型。

  • 标注管理:提供标注的增加、删除、编辑、保存等管理功能。

  • 导出和分享:支持导出标注结果为各种格式,如 JSON、XML 等,以及分享给他人协作编辑。

  • 用户界面:提供友好的用户界面,支持快捷键操作,提高标注效率。

  • 集成与扩展:支持与其他系统或工具的集成,提供 API 接口和插件机制,以便进行功能扩展。


通过不断地迭代和优化,我们可以使这个图片标注工具成为业界的标杆,为用户提供高效便捷的标注体验。


感谢您的阅读和关注!希望这篇文章能够为您在前端开发中实现图像标注功能提供一些有价值的见解和启发。如果您有任何问题、建议或想要分享自己的经验,欢迎在评论区留言交流。让我们一起探索更多前端技术的可能性,不断提升我们的技能和创造力!


本文示例源码:github.com/yanhaijing/…


本文示例预览:yanhaijing.com/imagic/demo…


作者:颜海镜
来源:juejin.cn/post/7350954669742768147
收起阅读 »

Window.print() 实现浏览器打印

web
前言 由于在公司项目中有打印的具体业务场景,在查询相关资料后,找到了 Window.print() 是用来打印的方法,写下这篇文章供自己和大家查漏补缺。 语法 window.print(); 该方法没有参数和返回值,在页面中直接调用,将直接打印整个页面,具体...
继续阅读 »

前言


由于在公司项目中有打印的具体业务场景,在查询相关资料后,找到了 Window.print() 是用来打印的方法,写下这篇文章供自己和大家查漏补缺。


语法


window.print();

该方法没有参数和返回值,在页面中直接调用,将直接打印整个页面,具体使用如下面代码所示



在点击打印该页面按钮后,会触发浏览器的打印对话框,对话框里有一些配置项,可以设置打印的相关参数。


image.png


根据上面的方法,我们就可以实现在浏览器中打印页面。


但是实际的开发中,我们的业务场景比这更加复杂。例如:只打印某个 DOM 元素,需要根据用户需求调整纸张的大小和形状,调整打印的布局,字体大小,缩放比例等等。这些都是常见的情况,那我们应该怎么做呢?


使用 @media 媒体查询


媒体查询 MDN解释:
@media CSS @ 规则可用于基于一个或多个媒体查询的结果来应用样式表的一部分。使用它,你可以指定一个媒体查询和一个 CSS 块,当且仅当该媒体查询与正在使用其内容的设备匹配时,该 CSS 块才能应用于该文档。


简单来说就是使用媒体查询根据不同的条件来决定应用不同的样式。具体到我们的需求就是,在打印时,使用专门的打印样式,隐藏其他元素,实现只打印某个元素的效果



使用样式表


在你的 标签中添加 link 元素,倒入专门供打印使用的样式表。你可以在样式表中编写打印是的具体样式。


<link href="/media/css/print.css" media="print" rel="stylesheet" />

打印页面时常用的一些样式


利用 print 设置打印页面的样式,利用 page 设置打印文档的纸张配置


 /* print.css */
@media print {
* {
-webkit-print-color-adjust: exact !important; /* 保证打印出来颜色与页面一致 */
}
.no-break {
page-break-inside: avoid; /* 避免元素被剪切 */
}
.no-print {
display: none; /* 不想打印的元素设置隐藏 */
}

@page {
size: A4 prtrait; /* 设置打印纸张尺寸及打印方向:A4,纵向打印 */
margin-top: 3cm /* 利用 margin 设置页边距 */
}

}

使用 iframe 实现更加精细的打印


我们也可以将想要打印的内容在 iframe 中渲染出来并打印,通过创建一个隐藏的 iframe 元素,将想要打印的内容放入其中,然后触发 iframe 的打印功能,实现更加灵活的打印。伪代码如下所示:


<!doctype html>
<html lang="en-US">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>Use ifame print example</title>
<script>
function printPage() {
// 新建一个 iframe 元素并隐藏
const iframe = document.createElement("iframe");
iframe.style.position = "fixed";
iframe.style.right = "0";
iframe.style.bottom = "0";
iframe.style.width = "0";
iframe.style.height = "0";
iframe.style.border = "0";
// 将它添加到 document 中
document.body.appendChild(hideFrame);

// 拿到新创建的 iframe 的文档流
const iframeDocument = iframe.contentDocument;

// 在 iframe 中新建一个 link 标签,引入专门用于打印的样式表
const printCssLink = iframeDocument.createElement('link')
printCssLink.rel = 'stylesheet';
printCssLink.type = 'text/css';
printCssLink.media = 'print';
printCssLink.href = `medis/css/print.css`;
iframeDocument.head.appendChild(printCssLink);

// 在 iframe 中新建一个容器,用来存放你想要打印的元素
let printContainer = iframeDocument.createElement('div');
iframeDocument.body.appendChild(printContainer);

iframeDocument.close(); // 关闭 iframe 文档流

// 获取 iframe 的 window 对象
const iframeWindow = iframe.contentWindow;
// 当 iframe 内容加载完成后触发打印功能。
iframeWindow.onload = function() {
iframeWindow.print();
}
// 打印完后移除 iframe 元素
document.body.removeChild(iframe);
}
</script>
</head>
<body>
<p>
<span onclick="printPage();">
Print external page!
</span>
</p>
</body>
</html>

以上就是关于我在使用 Window.print() 打印页面的一些总结,欢迎大家有问题在评论区讨论学习。


作者:It_Samba
来源:juejin.cn/post/7267091417021628475
收起阅读 »

Android应用保活全攻略:30个实用技巧助你突破后台限制

在Android系统中,保活(保持应用进程一直存活)通常是为了让应用在后台持续运行,以实现某些特定的功能,如实时消息推送、定位服务等。然而,由于Android系统为了节省资源和保护用户隐私,通常会限制后台应用的运行。因此,开发者需要采取一些策略来实现保活。以下...
继续阅读 »

在Android系统中,保活(保持应用进程一直存活)通常是为了让应用在后台持续运行,以实现某些特定的功能,如实时消息推送、定位服务等。然而,由于Android系统为了节省资源和保护用户隐私,通常会限制后台应用的运行。因此,开发者需要采取一些策略来实现保活。以下是30个常见的Android保活手段,帮助你突破后台限制:


1. 前台服务(Foreground Service)


将应用的Service设置为前台服务,这样系统会认为这个服务是用户关心的,不容易被杀死。前台服务需要显示一个通知,告知用户当前服务正在运行。通过调用startForeground(int id, Notification notification)方法将服务设置为前台服务。


2. 双进程守护


创建两个Service,分别运行在不同的进程中。当一个进程被杀死时,另一个进程可以通过监听onServiceDisconnected(ComponentName name)方法来感知,并重新启动被杀死的进程。这样可以相互守护,提高应用的存活率。


3. 使用系统广播拉活


使用系统广播拉活。监听系统广播,如开机广播、网络变化广播、应用安装卸载广播等。当收到广播时,检查应用进程是否存活,如果已经被杀死,则重新启动应用。


4. JobScheduler


使用JobScheduler定时启动应用。JobScheduler是Android 5.0引入的一种任务调度机制,可以在满足特定条件下执行任务。通过创建一个Job,设置触发条件,然后将Job提交给JobScheduler。当触发条件满足时,JobScheduler会启动应用。


5. 白名单


引导用户将应用加入系统的白名单,如省电白名单、自启动白名单等。加入白名单的应用不会受到系统的限制,可以在后台持续运行。


6. 第三方推送服务


使用第三方推送服务,如极光推送、小米推送等。这些推送服务通常使用保活技巧,可以保证消息的实时推送。


7. 静态广播监听


在AndroidManifest.xml中注册静态广播,监听系统广播,如电池状态改变、屏幕解锁等。当收到广播时,检查应用进程是否存活,如果已经被杀死,则重新启动应用。需要注意的是,从Android 8.0开始,静态广播的使用受到了限制,部分隐式广播无法通过静态注册来接收。


8. 合理利用Activity


在必要时,将应用的Activity设置为singleTask或singleInstance模式,确保应用在后台时只有一个实例。这可以减少系统对应用的限制,提高应用在后台的存活率。


9. 使用AlarmManager定时唤醒


使用AlarmManager定时唤醒应用。通过设置一个定时任务,当到达指定时间时,使用PendingIntent启动应用。需要注意的是,从Android 6.0开始,AlarmManager的行为受到了限制,当设备处于低电量模式时,定时任务可能会被延迟。


10. 合理设置进程优先级


Android系统会根据进程的优先级来决定是否回收进程。通过合理设置进程优先级,可以降低系统回收进程的概率。例如,可以将Service设置为前台服务,或者将进程与用户正在交互的Activity绑定。


11. 使用sticky广播


使用sticky广播在一定程度上可以提高广播接收器的优先级。当发送一个sticky广播时,系统会将该广播存储在内存中,这样即使应用被杀死,也可以在重新启动时收到广播。但需要注意的是,从Android 5.0开始,sticky广播的使用受到了限制,部分广播无法使用sticky模式发送。


12. 使用WorkManager


WorkManager是Android Architecture Components的一部分,它为后台任务提供了一种统一的解决方案。WorkManager可以自动选择最佳的执行方式,即使应用退出或设备重启,它仍然可以确保任务完成。WorkManager在保活方面的效果可能不如其他方法,但它是一种更符合Android系统规范的解决方案,可以避免系统限制和用户体验问题。


13. 合理使用WakeLock


在某些特定场景下,可以使用WakeLock(电源锁)来防止CPU进入休眠状态,从而确保应用能够在后台持续运行。但请注意,WakeLock可能会导致设备电量消耗增加,因此应谨慎使用,并在不需要时尽快释放锁。


14. 合理使用SyncAdapter


SyncAdapter是Android提供的一种同步框架,用于处理数据同步操作。SyncAdapter可以根据设备的网络状态、电池状态等条件来自动调度同步任务。虽然SyncAdapter并非专门用于保活,但它可以在一定程度上提高应用在后台的存活率。


15. 使用AccountManager


通过在应用中添加一个账户,并将其与SyncAdapter关联,可以在一定程度上提高应用的存活率。当系统触发同步操作时,会启动与账户关联的应用进程。但请注意,这种方法可能会对用户造成困扰,因此应谨慎使用。


16. 适配Doze模式和App Standby


从Android 6.0(API级别23)开始,系统引入了Doze模式和App Standby,以优化设备的电池使用。在这些模式下,系统会限制后台应用的网络访问和CPU使用。为了保证应用在这些模式下正常运行,您需要适配这些特性,如使用高优先级的Firebase Cloud Messaging(FCM)消息来唤醒应用。


17. 使用Firebase Cloud Messaging(FCM)


对于需要实时消息推送的应用,可以使用Firebase Cloud Messaging(FCM)服务。FCM是一种跨平台的消息推送服务,可以实现高效且可靠的消息传递。通过使用FCM,您可以确保应用在后台时接收到实时消息,而无需采取过多的保活手段。


18. 遵循Android系统的最佳实践


在开发过程中,遵循Android系统的最佳实践和推荐方案,可以提高应用的兼容性和稳定性。例如,合理使用后台任务、避免长时间运行的服务、优化内存使用等。这样可以降低系统对应用的限制,从而提高应用在后台的存活率。


19. 及时适配新系统版本


随着Android系统版本的更新,系统对后台应用的限制可能会发生变化。为了确保应用在新系统版本上能够正常运行,您需要及时适配新系统版本,并根据需要调整保活策略。


20. 与用户建立信任


在实际开发中,应尽量遵循系统的规范和限制,避免过度使用保活手段。与用户建立信任,告知用户应用在后台运行的原因和目的。在用户授权的情况下,采取适当的保活策略,以实现所需功能。


21. 使用Binder机制


Binder是Android中的一种跨进程通信(IPC)机制。通过在Service中创建一个Binder对象,并在其他进程中获取这个Binder对象,可以使得两个进程建立连接,从而提高Service的存活率。


22. 使用native进程


通过JNI技术,创建一个native进程来守护应用进程。当应用进程被杀死时,native进程可以感知到这个事件,并重新启动应用进程。这种方法需要C/C++知识,并且可能会增加应用的复杂性和维护成本。


23. 使用反射调用隐藏API


Android系统中有一些隐藏的API和系统服务,可以用于提高应用的存活率。例如,通过反射调用ActivityManager的addPersistentProcess方法,可以将应用设置为系统进程,从而提高应用的优先级。然而,这种方法存在很大的风险,可能会导致应用在某些设备或系统版本上无法正常运行。


24 监听系统UI


监听系统UI的变化,如状态栏、导航栏等。当系统UI变化时,检查应用进程是否存活,如果已经被杀死,则重新启动应用。


25. 使用多进程


在AndroidManifest.xml中为Service或Activity设置android:process属性,使其运行在单独的进程中。这样,即使主进程被杀死,其他进程仍然可以存活。


26. 使用Provider


在AndroidManifest.xml中注册一个Provider,并在其他应用中通过ContentResolver访问这个Provider。这样,即使应用在后台,只要有其他应用访问Provider,应用就可以保持存活。


27. 关注Android开发者文档和官方博客


Android开发者文档和官方博客是获取保活策略和系统更新信息的重要途径。关注这些资源,以便了解最新的系统特性、开发者指南和最佳实践。


28. 性能优化


优化应用的性能,降低内存、CPU和电池的消耗。这样,系统在资源紧张时可能会优先回收其他消耗较高的应用,从而提高您的应用在后台的存活率。


29. 用户反馈


关注用户的反馈,了解他们在使用应用过程中遇到的问题。根据用户的反馈,调整保活策略,以实现最佳的用户体验。


30. 使用NotificationListenerService


通过实现一个NotificationListenerService并在AndroidManifest.xml中注册,可以监听系统通知栏的变化。当收到新的通知时,检查应用进程是否存活,如果已经被杀死,则重新启动应用。这种方法可以利用系统通知的变化来触发应用的启动,从而提高应用在后台的存活率。需要注意的是,为了使用NotificationListenerService,用户需要在设置中授权应用访问通知权限。


请注意,保活策略可能会导致系统资源消耗增加、用户体验下降,甚至引发系统限制或用户卸载应用。因此,在实际开发中,应根据功能需求和用户体验来权衡保活策略,尽量遵循系统的规范和限制。在可能的情况下,优先考虑使用系统推荐的解决方案,如前台服务、JobScheduler等。


作者:程序员陆业聪
来源:juejin.cn/post/7352079364611276819
收起阅读 »

《青春咖啡馆》书籍推介

《青春咖啡馆》书籍推介 摘要 一些书籍的阅读往往能带来自我心灵的疗愈,《青春咖啡馆》就是这样的一本书,书中描写了一个和20岁左右的女孩子关于心中幸福的追索,反映出那个年代探索人生意义的青年群像。20世纪60年代,法国存在主义兴起,青年在这些思...
继续阅读 »

《青春咖啡馆》书籍推介





摘要


一些书籍的阅读往往能带来自我心灵的疗愈,《青春咖啡馆》就是这样的一本书,书中描写了一个和20岁左右的女孩子关于心中幸福的追索,反映出那个年代探索人生意义的青年群像。20世纪60年代,法国存在主义兴起,青年在这些思潮的引导下渴望摆脱人生的束缚,不断逃离那些既定的轨道,在“去存在”中获得自由。可是,一味地追求“存在”、“自由”,真的可能吗?故事的女主人公就是如此,她最后选择存在于另一个世界……


点评


《青春咖啡馆》是莫迪亚诺最令人心碎的作品之一。作品一如既往地充满了调查与跟踪,回忆与求证,找不到答案的疑问。“您找到您的幸福吗?”可是,露姬到哪里去寻找她的幸福呢?咖啡馆?书籍?大街上的游荡?婚姻中?逃跑?她全试过了,包括毒品和神秘学,可她像其他许许多多的人一样,最终消失在时间的长河中。


历史背景


本书的故事发生在20世纪60年代的法国巴黎,正是各种青年学生运动盛行的时期。当时有一位重要的思想家居伊·德波,他的思想曾对1968年的法国青年学生运动“五月风暴”产生过巨大影响。本书的作者莫迪亚诺也引用了他的这样一句话放在小说开头:



在真实生活之旅的中途,


我们被一缕绵长的愁绪包围,


在挥霍青春的咖啡馆里,


愁绪从那么多戏谑的和伤感的话语中流露出来。 ——居伊·德波



国际情景主义者认为,“在这个被商品和景观统治的社会中,创造精神和人生梦想失去了自己的家园,人生活在这样的环境里感到窒息”,所以他们主张用创造生活取代“被动生活”,呼吁“毫无拘束地生活、毫无节制地享受”和游戏人生,并进行人生的“漂移” (dérive)。居伊·德波在《漂移理论》一书中指出,漂移是“一种快速通过各种环境的技巧”,是指对物化的城市生活,特别是建筑空间布局的凝固性的否定。《青春咖啡馆》的故事发生在六十年代初,正是情景主义者活动最如火如荼的时期。作品中的人物似乎都在按照情景主义者的规则生活着,跟那些情景主义者一样,他们都认为工作和学习是束缚人的,“永远也别工作”,写在墙上的标语非常醒目。


小说结构


这部小说以上世纪六十年代的巴黎为背景,巴黎塞纳河左岸的拉丁区,一家名叫孔岱的咖啡馆,吸引了一批年轻人。他们四处漂泊,居无定所,放荡不羁,过着今朝有酒今朝醉的生活,他们之中有作家、艺术家、大学生,他们沉湎于酒精和毒品。故事就从他们当中一个名叫露姬的女子出现、失踪、追忆到后来她自杀的过程来展开故事的。整个故事并不完全是以线性方式展开,而是通过四个人的视角来讲述一个相互交叉的故事。读完这本书,我有一个疑问,这种叙述方式是否已经成为当代文学的一种常态,福克纳、略萨的作品都用了这种方式。


叙述者一(巴黎高等矿业学院的学生)


第一个叙述者是一名巴黎高等矿业学院的学生,他也是孔岱咖啡馆的常客,通过他的视角我们第一次见到本文的女主角,“露姬”这个名字也是他给取的。他还带领我们参与了咖啡馆的日常,认识了咖啡馆的服务生和客人,其中一个人称“船长”的客人连续三年记录了咖啡馆客人到达的时间和他们的住址,“船长”离开巴黎时将记录的笔记本留个了这位讲述者,他不断的翻阅笔记本,露姬在他的追寻下,一步一步的清晰起来。露姬是一个蓝眼睛,棕色头发,指甲修长的美女。她身体笔直,形态优雅,一言不发,谨小慎微,手里经常拿着一本《消失的地平线》,与咖啡馆的其他人格格不入。叙述者因此断定“她到孔岱这里,是来避难的,仿佛她想躲避什么东西,想从一个危险中逃脱。”这是从一个陌生人的角度对女主角的描述。


叙述者二(盖世里,一名私家侦探)


第二个叙述者名叫盖世里,是一名私家侦探。他讲述了一个名叫让-皮埃尔·舒罗的人,委托他寻找舒罗离家出走几个月的妻子雅克林娜,据舒罗说,他是在自己的房地产公司结识雅克林娜的,她是他的秘书,为了“建立关系”,他们二人结婚,婚后雅克林娜觉得丈夫无趣,狠心离家出走。盖世里很快查明雅克林娜的真实身份,她二战时期出身于索洛涅,没有父亲,由母亲单独抚养,母亲在红磨坊当服务员。她幼年时,母亲上班时经常将她一人留在家里,她因孤单和恐惧,时常在母亲上班时外出闲逛,并沾染上毒品,两次因“未成年流浪”被警察抓走。在盖世里的深入调查下,雅克林娜就是露姬这一事实得以解开。随着了解的进行,盖世里被雅克林娜的经历打动,似乎理解了她离家出走的缘由,最后,放弃侦查,不再干扰她的生活。这是从一个侦探的角度让人物从模糊走向清晰的过程。


叙述者三(女主人公,本人)


第三个叙述者是露姬(雅克琳娜)本人。她讲述了她在十八区拉谢尔大道10号度过的童年和少年时光。晚上,她母亲上班不在家,她就偷偷溜出去闲逛。一次在九区,一次在十八区,一个男人和一个女人,为她提供了战胜恐惧的“良药”,她尝试吸毒,吸过之后她不再感到恐惧。在克里希林荫大道,她结识一家专门售卖科幻和天文学书籍的书店的老板,老板送了一本《无限之旅》的书给她,让她体验了阅读的乐趣。她解释了自己为什么总想逃走“每次我和什么人断绝往来,我都感到一种沉醉。”不管是年少时的离家出走,还是从丈夫家的逃离,都是居于这样的原因。这是从当事人角度的叙述,部分回答了第一叙述人和第二叙述人的疑惑,让谜一样的露姬逐渐的立体丰满起来。


叙述者四(罗兰,露姬的情人)


第四个叙述人是露姬的情人罗兰。罗兰是一个刚入门的作家。他与露姬在一个名叫居伊·德·威尔组织的聚会上认识,威尔是一个神秘学家,也是这群年轻人的精神导师,他推荐露姬阅读《消失的地平线》和《不存在的路易斯》。罗兰和露姬相识相爱,露姬不想光顾能勾起她痛苦回忆的拉丁区,十六区又离她丈夫家很近,他们因此生活在中立地区,但也并不感到安全,于是谋划出国旅游,还未成行,露姬突然跳窗自杀,故事戛然而止。


哲理


《青春咖啡馆》作者是2014年诺贝尔文学奖得主,他生活在法国,深受法国哲学思想的熏陶,他在这本书中大段引用了法国哲学家吉尔·德勒兹的“逃逸线“概念。


“逃逸线”(ligne de fuite)是法国哲学家德勒兹(1925-1995)经常使用的概念,在后期经典之作《千座高原》中,他详细区分了三种类型的“线”:坚硬线、柔软线和逃逸线。





  • 坚硬线指质量线,透过二元对立所构建僵化的常态,比如说人在坚硬线的控制下,就会循规蹈矩地完成人生的一个个阶段,从小学到大学到拿工资生活到退休。





  • 柔软线指分子线,搅乱了线性和常态,没有目的和意向。





  • 逃逸线完全脱离质量线,由破裂到断裂,主体则在难以控制的流变多样中称为碎片,这也是我们的解放之线,只有在这条线上我们才会感觉到自由,感觉到人生,但也是最危险之线,因为它们最真实。




故事的主人公露姬就是一个摆动在坚硬线和逃逸线之间的女孩子,她说:



后来,我每次与什么人断绝往来的时候,我都能重新体会到这种沉醉。只有在逃跑的时候,我才真的是我自己。我仅有的那些美好回忆都跟逃跑或者离家出走连在一起。但是,生活总会重新占据上风。当我走到迷雾街时,我深信有人约我在此见面,这对我来说又会是一个新的起点。(p82)



露姬的心理


一方面她渴望逃脱出坚硬线的束缚,追求自己认为的幸福,自己认为的“真正的生活”,每当她逃离的时候,她都会感到一种沉醉、自由。另一方面,她不断逃跑,但是后来生活总会占据上风,这是一个从“逃逸线”回归“坚硬线”的过程。


从她的逃跑经历来看,在露姬十三四岁的时候,她母亲上班要上到凌晨两点,于是露姬喜欢一个人在半夜出门,在大街上走来走去。这导致她曾两次因未成年流浪被警察叔叔送回了家,这就是她最开始体会到,她离家出走后生活总会重新占据上风,她又会进入一个有明确意义的事件中。


之后她母亲去世,露姬与一个名叫舒罗的受过良好教育的中产结婚了,她的结婚又是一种对“坚硬线”的回归。



可她为什么要嫁给舒罗?结婚之后再次出逃,但这一次却是朝左岸逃,就好像过河之后,她就逃脱了迫在眉睫的危险,并得到了保护。可是,这桩婚姻对她来说不也是一种保护吗?假如她有足够的耐心呆在诺伊利,久而久之,人们就会忘记在让-皮埃尔·舒罗夫人的底下还藏着一个雅克林娜·德朗克。(p53)



露姬不断逃离,不断融入新的关系之中,“他们只是尝试建立关系”。露姬总是在逃离那个幸福不在的地方,但当她到了一个新的地方,她又觉得幸福不在那里。



她说,真正的生活,不是这样的。可当他问她那种“真正的生活”到底是什么样子时,她就一直耸肩膀,不置一词,就好像说了也是白说。(p39)



结合拉康精神分析


可以说,露姬是一个在不断逃离大他者和小他者的女孩子。她在逃离那些社会规范,比如家庭、婚姻、学校,即所谓的大他者。她也在逃离他人的想象,比如她尝试与她原来在酒吧的那些朋友绝交。



我的心中充满了沉醉的感觉,这种沉醉是酒精或者那雪什么的永远给不了的。我往上一直走到迷雾城堡。我已经痛下决心永远也不和康特尔酒吧里的那帮人见面了。



她希望逃离一切束缚,但是她根本没有意识到她无法逃离大他者和小他者,她从家庭逃到婚姻再逃到友情、爱情,永远都有一个大他者存在。同时,她伪造自己的真实身份,骗别人说自己是学习东方语言的大学生,说自己母亲是会计师。她认为在社会中,“学习东方语言的大学生”似乎比中学辍学更加体面,会计师比红磨坊夜总会的服务员更加体面。她始终没有逃离那个大他者带给她的评价标准,那个无处不在的大他者。因此,她的逃离最终必然以失败告终,也正是如此,最后露姬选择以跳楼自杀的方式,逃离那个无处不在的重力,逃离这个无处可逃的世界。


那么,您找到您的幸福了吗?


这句话贯穿了全书,最开始是书店老板莫名奇妙地一句话,这句话深深地印在露姬的脑海里。



有人言之凿凿地告诉我:人唯一想不起的东西就是人说话的嗓音。可是,直到今天,在那些辗转难眠的夜晚,我却经常能听见那夹带巴黎口音——住在斜坡街上的巴黎人——的声音问我:“那么,您找到您的幸福了吗?”



译后记·四


这本书的译者是金龙格先生,他在译后记中这样分析“那么,您找到您的幸福了吗?”这句话。


《青春咖啡馆》以一种既写实又神秘的笔调,交织谱出青春岁月的青涩、惶惑、焦虑、孤独寂寞与莫名愁绪,描写一个弱女子从不断探寻人生真谛到最终放弃生命追寻的悲剧命运,这个悲剧发生在一个既有着迷人的魅力又像谜一样难以捉摸的年轻美丽女子身上,更使全书充满一种挥之不去的忧伤情调。书中的一句问话像哲学命题一样尤其发人深省:“您找到了您的幸福吗?”,可是,人能找到自己的幸福吗?人终其一生到底能够得到什么?小说中的主人公什么都尝试过了,最终却一无所获。莫迪亚诺的小说似乎在告诉我们,幸福只是昙花一现的东西,人生寻寻觅觅,到头来得到的只有落寞、失去、不幸、迷茫,只有时时袭来的危机与恐慌,只有萍踪不定的漂泊,只有处在时代大潮中身不由己的无奈和顾影自怜的悲哀。


总结


《青春咖啡馆》的法语原文是Dans le café de la jeunesse perdue,意思是“在咖啡馆里青春消逝”,作者以一种伤感的笔调描述了露姬消逝的青春,但是在我们的一生中,青春虽然过去了,但并不会消逝,它只会永远的封存在我们的记忆之中。


在我们的青春年代,或许学业、社会的压力,父母、老师的规训让我们也有点想要逃离,去追寻自己所认为的幸福所在。虽然露姬生活在20世纪60年代,但其实人们的心底或多或少都有露姬的影子,就比如那个巴黎高等矿业学校的大学生、侦探盖世里、情人罗兰,包括你我。我们或许向往着那种不断逃离的生活,这就是当时情景主义者的生活,但作者并没有站在道德制高点对这种生活做出评判,只是如实进行记录,让读者跟着他的笔触,去认识去感受曾经有那样一群人、一个人这样生活过。探讨人生价值的文学作品屡见不鲜,网络发达的今天,许多人也会在网上总结、抱怨、指导、感悟人生的意义,不同的人会有不同的答案,这个答案不可能统一,也没有必要统一,千差万别的人生才是真正的人生。


作者:yueyh
来源:mdnice.com/writing/e894ad0fefa54767a64e22f5a7ac50ff
收起阅读 »

如何正确编写一个占满全部可视区域的组件(hero component)?

web
什么是 hero component hero component 或者 hero image 是一个网页设计术语,用于描述欢迎访问者访问网页的全屏视频、照片、插图或横幅。该图像始终位于网页顶部附近的显着位置,通常会在屏幕上延伸整个宽度。 我们经常见到这种...
继续阅读 »

什么是 hero component



hero component 或者 hero image 是一个网页设计术语,用于描述欢迎访问者访问网页的全屏视频、照片、插图或横幅。该图像始终位于网页顶部附近的显着位置,通常会在屏幕上延伸整个宽度。



我们经常见到这种 hero component,在视觉上占据了整个视口的宽度和高度。比如特斯拉的官网:


image.png


如何实现一个 hreo component?


很多人可能会不假思索的写出下面的 css 代码:


.hero {
width: 100vw;
height: 100vh;
}

你写完了这样的代码,满意地提测后。测试同学走过来告诉你:你的代码在苹果手机上不好使了!


image.png


如图所示,hreo component 被搜索栏挡住了。


又是 safari 的问题!我们可以先停止关于 Is Safari the new Internet Explorer? 的辩论,看看问题的成因和解决办法。


什么是视口单位


vh 单位最初存在时被定义为:等于初始包含块高度的 1%。



  • vw = 视口尺寸宽度的 1%。

  • vh = 视口大小高度的 1%。


将元素的宽度设置为 100vw,高度设置为 100vh,它就会完全覆盖视口。这就是上面 hero component 实现的基本原理:


image.png
这看起来非常完美,但在移动设备上。苹果手机的工程师觉得我们应该最大化利用手机浏览器的空间,于是在 Safari 上引入了动态工具栏,动态工具栏会随着用户的滑动而收起。


页面会表现为:高度为 100vh 的元素将从视口中溢出


image.png


当页面向下滚动时,动态工具栏会收起。在这种状态下,高度设为 100vh 的元素将覆盖整个视口。


image.png



图片来自于:大型、小型和动态视口单元



svh / lvh / dvh


为了解决上面提到的问题,2019 年,一个新的 CSS 提案诞生了。



上面的解释来自于 MDN,读起来有点拗口,其实顾名思义,再结合下面的动图就很好理解:
dvh.gif



在 tailwindcss 的文档中,对 dvh 有一个漂亮的动画演示:tailwindcss.com/blog/tailwi…



用以上的属性可以完美解决上面的 safari 中视口大小的问题,值得一提的是,有的人会建议始终使用 dvh 代替 vh。从上面的动图可以看到,dvh 在手机上其实会有个延迟和跳跃。所以是否使用dvh还是看业务的实际场景,就我而言,上面的 hero component 的例子使用 lvh 更合适。


兼容性


你可以在 can I use 中查看兼容性:


image.png


可以看到,这三个 css 属性还算是比较新的特性,如果为了兼容旧浏览器,最好是把 vh 也加上:


.hero {
width: 100vw;
height: 100vh;
height: 100dvh;
}

这样,即使在不支持新特性的浏览器中,也会降级到 vh 的效果。


感谢阅读本文~


作者:李章鱼
来源:juejin.cn/post/7352079427863592971
收起阅读 »

如何完成一个完全不依赖客户端时间的倒计时

web
前言 最近在做一个调查问卷系统,其中有一个需求就是倒计 40 分钟以后自动提交问卷。由于 UI 库使用的是 antd,所以我第一反应是使用 antd 的 CountDown 组件。 于是我就愉快的写出以下代码: import { Statistic } fro...
继续阅读 »

前言


最近在做一个调查问卷系统,其中有一个需求就是倒计 40 分钟以后自动提交问卷。由于 UI 库使用的是 antd,所以我第一反应是使用 antdCountDown 组件。
于是我就愉快的写出以下代码:


import { Statistic } from 'antd';
const { Countdown } = Statistic;

const TOTAL_TIME = 40;
const deadline = dayjs(startTime).add(TOTAL_TIME, 'minute').valueOf();


function TitleAndCountDown() {
useEffect(() => {
if (currentTime >= deadline) {
onFinish();
}
}, []);

return (
<Countdown
value={deadline}
onFinish={onFinish}
format="mm:ss"
prefix={<img src={clock} style={{ width: 25, height: 25 }} />
}
/>

);
}

其中 startTimecurrentTime 是服务端给我返回的开始答题时间以及现在的时间,onFinish 是提交问卷的函数。测试一切正常,并且看起来好像没有依赖客户端时间,于是我就愉快的提交了代码。


antd 的问题


上线后,有客户反映倒计时不正常,进入系统后直接显示 9000 多秒,导致业务直接进行不下去。这个时候我就懵了,我的代码中并没有依赖任何客户端时间,问题肯定是出现在 antdCountDown 组件上。于是我就去看了一下 antdCountDown 组件的源码,果不其然


 // 30帧
const REFRESH_INTERVAL= 1000 / 30;

const stopTimer = () => {
onFinish?.();
if (countdown.current) {
clearInterval(countdown.current);
countdown.current = null;
}
};

const syncTimer = () => {
const timestamp = getTime(value);
if (timestamp >= Date.now()) {
countdown.current = setInterval(() => {
forceUpdate();
onChange?.(timestamp - Date.now());
if (timestamp < Date.now()) {
stopTimer();
}
}, REFRESH_INTERVAL);
}
};

React.useEffect(() => {
syncTimer();
return () => {
if (countdown.current) {
clearInterval(countdown.current);
countdown.current = null;
}
};
}, [value]);

核心代码就是这段,本质 CountDown 并不是一个倒计时,而是根据客户端时间算出来的一个时间差值,这也能解释为啥这个倒计时相对比较准确。


但是依赖了客户端时间,就意味客户的本地时间会影响这个倒计时的准确性,甚至可以直接通过修改本地时间来绕过倒计时。一开始我的方案是加入 diff 值修正客户端时间,我也给 antd 官方提了一个 PR,但是被拒绝了。后来想了一下 CountDown 组件可以直接传入 diff 后的 value,确实没有必要新增 props


这个方案后来也是被否了,因为还是依赖了客户端时间。客户的机房条件比较复杂,可能一开始时间不对,但是做题途中时间会校正回来。因为我们这个调查系统短时间有几十万人参加调查,为了不给服务器过多的压力,查询服务器时间接口的频率是 1 分钟一次,所以会有很长时间的倒计时异常。


完全不依赖客户端时间的倒计时


倒计时的方案大致有 4 种, setTimeoutsetIntervalrequestAnimationFrameWeb WorkerrequestAnimationFrameWeb Worker 因为兼容性问题暂时放弃。


setInterval 实现倒计时是比较方便的,但是 setInterval 有两个缺点



  1. 使用 setInterval 时,某些间隔会被跳过;

  2. 可能多个定时器会连续执行;


每个 setTimeout 产生的任务会直接 push 到任务队列中;而 setInterval 在每次把任务push到任务队列前,都要进行一下判断(看上次的任务是否仍在队列中)。


可以看到,主线程的渲染都会对 setTimeoutsetInterval 的执行时间产生影响,但是 setTimeout 的影响小一点。所以我们可以使用 setTimeout 来实现倒计时.


const INTERVAL = 1000;

interface CountDownProps {
restTime: number;
format?: string;
onFinish: () => void;
key: number;
}
export const CountDown = ({ restTime, format = 'mm:ss', onFinish }: CountDownProps) => {
const timer = useRef<NodeJS.Timer | null>(null);
const [remainingTime, setRemainingTime] = useState(restTime);

useEffect(() => {
if (remainingTime < 0 && timer.current) {
onFinish?.();
clearTimeout(timer.current);
timer.current = null;
return;
}
timer.current = setTimeout(() => {
setRemainingTime((time) => time - INTERVAL);
}, INTERVAL);
return () => {
if (timer.current) {
clearTimeout(timer.current);
timer.current = null;
}
};
}, [remainingTime]);

return <span>{dayjs(remainingTime > 0 ? remainingTime : 0).format(format)}</span>;
};

为了修正 setTimeout 的时间误差,我们需要在 聚焦页面的时候 以及 定时一分钟请求一次服务器时间来修正误差。这里我们使用 swr 来轻松实现这个功能。


const REFRESH_INTERVAL = 60 * 1000;

export function useServerTime() {
const { data } = useSWR('/api/getCurrentTime', swrFetcher, {
// revalidateOnFocus 默认是开启的,但是我们项目中给关了,所以需要重新激活
revalidateOnFocus: true,
refreshInterval: REFRESH_INTERVAL,
});
return { currentTime: data?.currentTime };
}

最后我们把 CountDown 组件和 useServerTime 结合起来


function TitleAndCountDown() {
const { currentTime } = useServerTime();

return (
<Countdown
restTime={deadline - currentTime}
onFinish={onFinish}
key={deadline - currentTime}
/>

);
}

这样,就完成了一个完全不依赖客户端时间的倒计时组件。


总结



  • 上面方案中的 setTimeout 其实换成 requestAnimationFrame 计时会更加准确,也解决了 requestAnimationFrame未被激活的页面中 中不会执行的问题。

  • setIntervalsetTimeout 的时间误差是由于主线程的渲染时间造成的,所以如果我们的页面中有很多的动画,那么这个误差会更大。

  • 未激活的页面,setTimeout 的最小执行间隔是 1000ms


作者:xinglee
来源:juejin.cn/post/7229898205256417341
收起阅读 »

回县城躺平,感觉我的人生过得好失败

从春节前到现在,一个半月没更新了,做啥都提不起劲来。 越来越感觉我的人生过的好失败。 去年我爸因为癌症去世了,家里的门头房用不到了,就想卖掉,找好了买家,价格谈了 140 万。 当时想买我们这个房子的人很多,那个买家怕我们卖给别人,就拿了 20 万定金,和我们...
继续阅读 »

从春节前到现在,一个半月没更新了,做啥都提不起劲来。


越来越感觉我的人生过的好失败。


去年我爸因为癌症去世了,家里的门头房用不到了,就想卖掉,找好了买家,价格谈了 140 万。


当时想买我们这个房子的人很多,那个买家怕我们卖给别人,就拿了 20 万定金,和我们签了一个买房合同。


没想到因为这个房子因为在我爸名下,涉及到继承,需要我奶奶放弃继承才可以过户。


我奶奶现在生活不能自理,由我大娘二大娘养着,然后他们提出条件来,要我们的宅基地,也就是村里的老房子。


我妈开始不同意,因为她想之后从那里出殡,和我爸合葬。


后来也同意了,在哪不能殡出去呢?


然后我这边准备好了材料之后,我堂姐跳了出来,一拍桌说,不行,老房子我们也要,另外你还要给我们 25 万。


她们说咨询了律师要是打官司的话,我们青岛的房子也要分,那个门头房也要分,另外我爸的银行流水也要查,起码得分她们 40 万。


我给讲价到了 20 万,但是我妈不同意,说是她和我爸辛苦攒下的家业凭什么白给他们。


我妈这边也找了律师,给出的意见就是拖着就行,一辈子不卖了。


这时候买房子的不干了,说是合同上写了,如果违约,要双倍返回定金,也就是赔她们 20 万。


当时我们以为就是个收到条就签了,没想到在这等着呢。


其实我们早就把定金返给她了,也说了我们家的情况,但她就是不行。


年前就一直威胁我们要告,刚过完年,马上又来了:



我妈问了下和谈的话怎么谈,她说起诉你赔我 25 万,和谈赔我 18 万。



两头挤我们,家里就要我们老房子 + 21 万,卖房子的就要我们 20 万违约金。


我们夹在中间,几度濒临崩溃。


我想了下,这件事早晚得解决,反正都是一家人,给家里人点钱也没啥。


然后我前几天去找我大爷二大爷,还有堂姐、堂哥坐在一起谈了,我说我同意这 21 万。


最终转了她们 18 万(老房子折合了 2 万块),然后又拿了 1 万律师费(他们请的律师让我拿律师费),还有同意给他们老房子。


但我提的要求是和我妈只能说是 10 万 + 老房子,不然我妈不会同意的。


就这样,我们顺利公证过了户。


公证放弃继承那天,我奶奶才刚知道这个事,她说她只要老房子,不要我爸的其他遗产。


但没办法,她不要不行,我大爷二大爷要啊。


这个房子卖了,到手 120 万。


然后还有青岛的房子,这个房子我买的时候是 70万首付 + 100 万贷款,一共是 200 万下来的,最近我妈又花了 7 万装修。


因为不去青岛住,也打算卖了,中介说价格不到 80 万还是可以卖掉的。



这么一算,这边亏了 120 万,那边房子卖了剩下 120,相当于我爸就给我留下了 70 多万还不好卖的房子。


其实我爸这辈子攒了不少钱,差不多 300 万,都是省出来的,从小我跟着他就是一天吃一顿那种,从没感觉到家里是有钱的。


再就是他对我妈也不好,前几年的时候经常打骂,后来我妈离家出走了,但是他生病了还是会来照顾他。


我爸癌症住院那段时间,生活不能自理,都是我妈没日没夜的照顾他。


临走之前,我爸一只手抓着我的手,一只手抓着我妈的手,然后让我们好好相处,他一直觉得对不起我妈,口里一直喊着“从头再来、从头再来”。


我送我爸去火葬场的时候,送我骨灰盒爸入土的时候,我也一直在说,“爸,你别怕,我们从头再来”。


其实我爸葬礼上,我没有咋哭出来,可能当时没大反应过来。


但是之后好长一段时间,我在村里别人家坐着聊天,一谈起我爸,就再也忍不住了,哭的稀里哗啦的。


我有个干兄弟,在村里拜了干爹,因为疫情好多年也没走动了,但是我爸的棺材是他帮忙抬出去的。


而我大爷二大爷就在一边看着。


我今年过年给他家送了礼,我说,我妈说我爸是你们抬出去的,让我一辈子记得你们的好。


当时说到这里,没忍住,又哭的稀里哗啦的。


我想我爸这辈子,是攒了不少钱,但是不舍得吃不舍得喝的,还在房子上亏了半辈子的积蓄。


对老婆孩子不好,临走前才后悔想着从头再来。


我想我前五年是赚了不少钱,但因为工作,好多年没回家,和家人一年待在一起也就几天。


而且最后都赔在青岛的房子上了。


人这一辈子,到底图啥呢?


年后这几周我找了找工作,有几个不错的 offer,都是 base 40+ 那种。



但我又不那么想出去了。


我这一年没工作,其实和我妈在一块生活还是很踏实的。


而且家里房子卖了,青岛的房子也快了,这样我的存款差不多能到 300w。存定期的话每年银行利息 8w 左右,够我生活了。


就这样在家躺平一辈子是不是也不错。


王小波说,那一天我二十一岁,在我一生的黄金时代,我有好多奢望。我想爱,想吃,还想在一瞬间变成天上半明半暗的云,后来我才知道,生活就是个缓慢受锤的过程,人一天天老下去,奢望也一天天消逝,最后变得像挨了锤的牛一样。可是我过二十一岁生日时没有预见到这一点。我觉得自己会永远生猛下去,什么也锤不了我。


韩寒说,平凡才是唯一的答案。


小的时候,我希望长大后的自己会光芒万丈。


长大以后,我希望自己有个好的工作打工到退休。


现在的我只想躺平。


我觉得我自己的人生很失败:


打工这些年,钱都赔在房子上了。


我比较社恐,永远达不到我妈对我的会来事、会察言观色的期望。


人家都在大城市结婚生子、买房定居了,而我又回到了小县城。


当年和我同时入职高德的朋友都升 p7 了,我现在啥也不是:



我是 94 年的,今年就 30 了,人生的各种机会和可能性越来越少。


后半辈子,我应该就是在小县城躺平,度过余生了。


但文章我还是想继续写,毕竟人这一生总要坚持点什么。


作者:zxg_神说要有光
来源:juejin.cn/post/7343503718183059471
收起阅读 »

分支管理:master,release,hotfix,sit,dev等等,听着都麻烦。

背景 从一开始的svn到后来接触到git,到目前已经和git打交道比较多了,突然觉得可以把项目中所用到一些分支的管理方式分享出来,希望帮助到大家。 分支介绍 现在使用git一般很少是单个分支来使用,通常是多个分支来进行,接下来我以我最新的项目中所用到的分支,来...
继续阅读 »

背景


从一开始的svn到后来接触到git,到目前已经和git打交道比较多了,突然觉得可以把项目中所用到一些分支的管理方式分享出来,希望帮助到大家。


分支介绍


现在使用git一般很少是单个分支来使用,通常是多个分支来进行,接下来我以我最新的项目中所用到的分支,来做一些介绍。


master



  • master分支代码只能被release分支分支合并,且合并动作只能由特定管理员进行此操作。

  • master分支是保护分支,开发人员不可直接push到远程仓库的master分支


release



  • 命名规则:release/*,“*”一般是标识项目、第几期、日期等

  • 该分支是保护分支,开发人员不可直接push,一般选定某个人进行整体的把控和push

  • 该分支是生产投产分支

  • 该分支每次基于master分支拉取


dev



  • 这个是作为开发分支,大家都可以基于此分支进行开发

  • 这个分支的代码要求是本地启动没问题,不影响其他人的代码


hotfix



  • 这个分支一般是作为紧急修复分支,当前release发布后发现问题后需要该分支

  • 该分支一般从当前release分支拉取

  • 该分支开发完后需要合并到release分支以及dev分支


feat



  • 该分支一般是一个长期的功能需要持续开发或调整使用

  • 该分支基于release创建或者基于稳定的dev创建也可以

  • 一般开发完后需要合并到dev分支


分支使用


以上是简单介绍了几个分支,接下来我针对以上分支,梳理一些场景,方便大家理解。


首先从master创建一个release分支作为本次投产的分支,然后再从master拉取一个dev分支方便大家开发,dev分支我命名为:dev/soe,然后我就在这个分支上进行开发,其他人也是这样。


然后当我开发完某个任务后,又有一个任务,但是呢,这个任务需要做,只是是否要上这次的投产待定,所以为了不影响到大家的开发,我就不能在dev分支进行开发了,此时我基于目前已经稳定了的dev分支创建了一个feat分支,叫做:feat/sonar,主要是用来修复一些扫描的问题,在此期间,如果我又接到了开发的任务,仍然可以切换到dev来开发,并不影响。


当开发工作完成后,并且也基于dev分支进行了测试,感觉没问题之后,我就会把dev分支的代码合并到release上。


当release投产之后,如果业务验证过也没有问题,那么就可以由专人把release合并到master了,如果发现了问题,那么此时就需要基于release创建一个hotfix分支,开发人员在此分支进行问题的修复,修复完成并测试后,合并到release分支和sit分支。然后再使用release分支进行投产。


总结


以上就是我在项目中,对分支的使用,我觉得关于分支使用看团队以及项目的需要,不必要定死去如何如何,如果有的项目不规定必须要release投产,那么hotfix就不必使用,直接release修改完合并也未尝不可,所以大家在项目中是如何使用的呢?可以评论区一起讨论分享。


致谢


感谢你的耐心阅读,如果我的分享对你有所启发或帮助,就给个赞呗,很开心能帮到别人。


作者:bramble
来源:juejin.cn/post/7352075703859150899
收起阅读 »

年会结束,立马辞职了!

那是发生在多年前的一件事,当时我也是在那家公司做 Java 开发。公司很大,大到去了很长一段时间都感觉毫无存在感。 那年年会,作为技术部的我,依然被安排到一个比较边缘化的桌子,这么多年走来,早已经习惯了这样的安排。 可能只有我们做技术人的心里才会觉得“技术...
继续阅读 »

那是发生在多年前的一件事,当时我也是在那家公司做 Java 开发。公司很大,大到去了很长一段时间都感觉毫无存在感。


那年年会,作为技术部的我,依然被安排到一个比较边缘化的桌子,这么多年走来,早已经习惯了这样的安排。


可能只有我们做技术人的心里才会觉得“技术牛逼,技术万岁!”,但在公司领导层看来,这技术研发部就是整个公司开销最大的一个部门,又不能直接产生效益,但开除了又不合适,还要靠他们干活呢,这真是一件即讽刺、又无奈的事儿啊。


说回正题,那年公司所有人依旧是尴尬的、极不情愿的、又不得不碍于情面凑在一起,听完了所谓的又毫无意义的年终总结,然后又敷衍的敬完酒之后,才能装模作样的挥手告别亲爱的同事。


我之所以,要等待年会的第二天才告诉我的顶头上司“我要离职”的主要原因是,年会的时候才给大家集中发年终奖。


我也是领到钱之后就不装了,我摊牌了,第二天就找到了领导,告诉他,我要离职了。这个时候上司也知道你的心思,话已经收出来了,尤其是离职的事,大概率是劝不回来了,毕竟覆水难收。大家都是明白人,寒暄了几句之后,就签了离职的申请。


工作就像谈对象,合不来也没必要勉强。那时候开发的行情还很好,出去面试 4 家公司,最少也能拿 3 个 Offer,所以跳槽基本都是裸跳,一副此地不留爷,自有留爷处的傲娇姿态。


然而,年终奖是拿到手了,新工作也很快又着落了,薪资每次跳槽也能涨到自己满意的数,但干着干着发现,好像还是原来的配方,还是原来的味道,好像也不是理想中的工作嘛。


于是,在周而复始的折腾中才发现,只要是给别人上班,永远不会有理想中的工作,因为上班的本质是你替别人办事,别人给你发薪水,工作从来都是简单的雇佣关系,那来的别人要为你的理想来买单嘛,这本来就不合理,只是想明白这点时,以是上班了十年之后(此处可见自己的笨拙)。


理解了这点之后,我才发现,给任何公司上班的区别不会太大,无非是钱多钱少、活多活少、周围人好相处与否的细微差别,但碍于生计,又不得不苟延残喘的上下班,这可能是大部分打工人的真实感受和现状了。


但即使这样,你依然会发现,你的岗位正在被新人所替代,你的选择也变的越来越少,你的挣钱能力也变的越来越弱,这可能就是所谓的“中年危机”吧。所以说“中年危机”这个词,不是那个行业的专属名称,而是所有行业共性,那要怎么解决呢?


三个小小的建议:




  1. 尽量不要买房:不要和自己过不去,买房一时爽,还贷“火葬场”。我有一个朋友,一个月 2.1W 的房贷,生活中哪怕有一点点小小的变动,对于他来说都是不可承受之殇。“如履薄冰”也不过如此吧?


  2. 培养自己的第二职业:找到自己感兴趣点,并且它能帮你长久的带来经济收益最好,不求大富大贵,只要能够日常开支已经很不错了。任何时候有准备都比没准备要强很多。还有,在做之前,不要怕起步晚、进步慢,只要肯坚持,终会有收获。路虽远,行则将至;事虽难,做则必成。


  3. 提升自己主业的能力:任何时候,提升自己主业的能力,都是收益最大的投资,也是最明智的投资,当你看不清前进的道路时,当你感觉人生黯淡无光事,唯有干好自己目前本职的工作,才是最优的选择,这也能让你为以后的新计划积攒足够的能量。


最后,愿新的一年里:奔赴热爱、享受自由,找到自己热爱的事,并为之努力。加油,XDM~


作者:王磊
来源:mdnice.com/writing/728744844d414145b2efa61ec218606c
收起阅读 »

产品经理的多维度划分

产品经理的进阶是一个涉及专业技能提升、经验积累、视野拓宽以及领导力培养的过程。一.产品经理的层次划分以下是一些产品经理进阶的关键步骤和能力要求:初级产品经理阶段:需求细化与执行:学会撰写高质量的产品需求文档(PRD)、绘制产品原型,具备基本的设计理解和一定的技...
继续阅读 »

产品经理的进阶是一个涉及专业技能提升、经验积累、视野拓宽以及领导力培养的过程。

一.产品经理的层次划分

以下是一些产品经理进阶的关键步骤和能力要求:

  1. 初级产品经理阶段

    • 需求细化与执行:学会撰写高质量的产品需求文档(PRD)、绘制产品原型,具备基本的设计理解和一定的技术背景知识,能够准确传达并跟进需求的执行。
    • 沟通协作:与开发、设计、运营等部门紧密合作,确保需求被正确理解和实施。
    • 基础技能:掌握需求分析、文档编写、原型设计工具使用、基本的用户研究和竞品分析。
  2. 中级产品经理阶段

    • 主动挖掘与项目管理:具备独立进行用户研究、数据分析、竞品分析的能力,主动寻找和验证市场需求,制定并执行产品策略。
    • 产品决策:开始涉及产品方向的选择与功能优化的取舍,具有更全面的产品视角和全局观。
    • 项目管理:有效管理产品生命周期中的各个阶段,保证项目的进度和质量。
  3. 高级产品经理或产品专家阶段

    • 战略规划:负责产品体系的整体规划,形成独特的产品方法论,能够预见和引领行业趋势。
    • 领导力:带领产品团队,负责整个产品线的战略布局和生命周期管理,影响团队成员成长和决策。
    • 成功案例与影响力:拥有成功的产品案例,能通过实践提炼出可复制的产品模式和最佳实践,对外输出方法论和思想领导力。
  4. 管理线进阶

    • 团队建设:组建和管理高效的产品团队,培养人才梯队,进行有效的团队激励和绩效管理。
    • 业务拓展:推动产品商业化进程,对市场反馈敏感,根据市场变化调整产品战略。
  5. 专业线深化

    • 领域专家:在某一细分领域成为专家,深入研究行业规则和技术发展趋势,指导复杂产品解决方案的构建。

此外,产品经理在进阶过程中还需要不断学习和适应新技术、新商业模式的变化,持续提升跨部门协调、解决问题、创新思维和决策能力。同时,良好的自我管理和情绪智商同样是职业发展中不可忽视的素质。随着职位的提升,产品经理可能面临更多战略层面的问题,因此需要不断提升自身的商业洞察力和战略规划能力。

二.高级产品经理能力模型

高级产品经理的能力模型通常涵盖了以下几个关键领域:

  1. 战略思维能力

    • 深入理解行业发展趋势,能够制定长远的产品发展战略和路线图。
    • 能够结合公司整体战略目标,明确产品定位,设定并达成有挑战性的产品愿景和目标。
  2. 用户洞察与需求提炼

    • 具备深入的用户研究和洞察力,能精准把握用户痛点与需求,并转化为产品特性。
    • 利用数据分析手段,量化用户行为,驱动产品迭代优化。
  3. 数据驱动决策

    • 强大的数据解读和分析能力,利用A/B测试、用户行为数据等工具,以数据为基础做出科学决策。
    • 能够建立和完善数据指标体系,用于衡量产品性能和市场表现。
  4. 跨部门协同与领导力

    • 出色的团队管理和组织协调能力,能有效调动内外部资源,推动跨职能团队合作。
    • 具备卓越的领导魅力,激发团队潜能,带领团队完成复杂项目。
  5. 创新与解决问题能力

    • 在面对复杂问题时,能够提出创新解决方案,突破现有框架,引领产品创新。
    • 对技术和市场变化保持敏锐度,预见并应对潜在的竞争威胁和市场机会。
  6. 专业技术能力

    • 深厚的产品设计理论基础,熟练掌握产品生命周期管理、敏捷开发流程等专业技能。
    • 能够深入参与产品设计和技术实现细节,与工程师团队有效沟通。
  7. 商业敏感性与财务知识

    • 具备较强的商业头脑,能从经济和盈利角度考虑产品发展,平衡用户价值与商业价值。
    • 了解财务模型和成本效益分析,能够在产品设计中合理权衡投入产出。

综上所述,高级产品经理不仅需要具备扎实的产品设计和管理能力,还需要有很强的跨领域整合能力、领导力以及对市场、技术、用户和商业的深刻理解,从而成功引领产品的长期发展和市场竞争。

三.产品经理的多维分类

产品经理的角色可以根据多个维度进行分类,以下是几个主要分类方式及其具体类型:

1. 按照服务对象划分:

  • B端产品经理 (Business-to-Business, B2B):主要负责为企业级客户提供产品和服务,例如企业软件、SaaS解决方案、云服务、后台系统等,这类产品经理需要深入理解客户业务流程,解决企业级痛点,注重产品的易用性和效率提升。

  • C端产品经理 (Business-to-Consumer, B2C):专注于为消费者打造产品,这涵盖各种消费级应用、网站、游戏、电商产品等,要求产品经理深入了解用户需求、偏好及行为模式,提供优秀的用户体验。

  • G端产品经理(Business-to-Government, B2G):G端产品经理是指专为政-府机构服务的产品经理,他们聚焦于政务信息化领域,负责设计、优化和管理面向政-府内部或公众的政务类数字产品。核心任务包括:依据政-府业务需求定制信息化解决方案,如搭建行政审批、公-文处理等内部管理系统;开发一站式便民政-务服务产品,如政-务服务网、政-务APP等,实现政-策查询、在线办理等功能

2. 按前后端划分:

  • 前端产品经理:专注于用户界面和交互设计,确保用户能够直观地使用产品,关注用户体验的全流程,包括UI/UX设计、页面跳转逻辑、信息展示结构等。

  • 后端产品经理:主要关注产品后台系统的设计和维护,包括但不限于数据存储、服务器架构、API接口设计、性能优化等非用户直觉感知的功能部分。

  • 中台产品经理:主要负责设计和规划公司的业务中台产品,确保中台系统的稳定、高效运行,并赋能前台业务快速发展。业务中台作为一种重要的IT架构模式,旨在沉淀和复用企业的核心能力,如用户中心、订单中心、商品中心等,为多个前台业务提供通用、灵活且可配置的服务。

3. 按照职能分类:

  • 功能型产品经理:主要职责是设计和优化产品功能,确保功能满足用户需求和业务目标。

  • 战略型产品经理:负责产品整体战略规划,确定产品定位、发展方向,以及基于市场分析、竞争态势作出前瞻性决策。

  • 运营型产品经理:侧重于产品与运营相结合,将运营策略转化为产品功能,关注用户增长、活跃度、留存等运营指标的提升。

  • 数据驱动产品经理:利用数据驱动产品优化和决策,熟悉数据分析工具,能从海量数据中发现规律并指导产品迭代。

  • AI/算法产品经理:在AI、大数据等领域,负责设计和优化基于算法和模型驱动的产品功能。

4. 按照行业分类:

  1. 电子商务产品经理
    • 负责电商平台的产品规划和优化,比如淘宝、京东等综合电商平台的商品管理、订单系统、支付系统、物流跟踪等模块,以及垂直细分市场的电商解决方案。
  2. 社交网络产品经理
    • 主要负责社交产品如微信、微博、QQ等的策划与迭代,关注社交互动、社区运营、信息传播、用户关系链构建等。
  3. 金融产品经理
    • 设计和优化金融服务产品,包括银行应用、证券交易平台、金融科技产品(如P2P借贷、众筹平台、区块链应用)、支付工具等,涉及资金流转、风险管理、合规要求等内容。
  4. 教育科技产品经理
    • 开发在线教育平台、教育管理软件、智慧校园系统等,关注课程设计、学习路径规划、教学资源管理等功能。
  5. 健康医疗产品经理
    • 专注医疗健康管理、电子病历系统、远程诊疗平台、智能穿戴设备配合的健康管理APP等产品的研发,需对接医疗资源、遵循医疗规范和隐私保护法规。
  6. 游戏产品经理
    • 负责游戏产品的策划、更新、运营,关注游戏玩法设计、用户体验、付费模型、社交元素等。
  7. 企业服务(B2B)产品经理
    • 包括CRM、ERP、HRM、SCM等各种企业管理软件,以及云服务、大数据分析工具等,需了解企业运营流程并提供针对性的解决方案。
  8. 物联网(IoT)产品经理
    • 负责智能家居、智能城市、工业自动化等领域的软硬件一体化产品设计,涉及传感器、智能设备、数据分析平台等。
  9. 媒体娱乐产品经理
    • 从事视频流媒体平台、音乐播放器、新闻资讯App等产品设计,关注内容分发、版权管理、个性化推荐等功能。
  10. 人工智能(AI)产品经理
    • 专门从事AI产品如智能语音助手、图像识别系统、自动驾驶系统等的研发,需理解机器学习原理、数据训练过程并转化为用户友好的产品形态。

5. 按照层级划分:

  • 初级产品经理:通常负责较具体模块或任务,配合上级完成产品设计与迭代。
  • 中级产品经理:可以独立承担某个产品线或子产品的规划与执行。
  • 高级产品经理乃至产品总监:负责整个产品部门或多个产品线的战略规划与执行,具备较强的战略眼光和团队管理能力。

6. 按照产品领域分类:

  • 软件产品经理:负责软件产品的规划、设计和管理。
  • 硬件产品经理:专注于硬件产品的开发和推广。
  • 互联网产品经理:管理互联网产品,如网站、移动应用等。
  • 电商产品经理:负责电商平台或相关产品的策划和运营。

以上分类并不绝对孤立,实际工作中产品经理的角色可能会结合多种分类特征,且随着行业的发展和市场需求的变化,还会出现新的产品经理角色类型。


作者:西边一山
来源:mdnice.com/writing/f1f3d57fedef4800932a6afd643809f1

收起阅读 »

需求分析

产品经理在进行需求分析时,首先需要明确需求分析的目的和重要性,即从用户需求出发,挖掘用户的真正目标,并转化为产品需求的过程。接下来,可以通过以下文了解如何来做好需求分析。image-202403070006130051.什么是需求分析需求分析也称为软件需求分析...
继续阅读 »

产品经理在进行需求分析时,首先需要明确需求分析的目的和重要性,即从用户需求出发,挖掘用户的真正目标,并转化为产品需求的过程。接下来,可以通过以下文了解如何来做好需求分析。

image-20240307000613005
image-20240307000613005

1.什么是需求分析

需求分析也称为软件需求分析、系统需求分析或需求分析工程等,是开发人员经过深入细致的调研和分析,准确理解用户和项目的功能、性能、可靠性等具体要求,将用户非形式的需求表述转化为完整的需求定义,从而确定系统必须做什么的过程。

需求分析是软件计划阶段的重要活动,也是软件生存周期中的一个重要环节,该阶段是分析系统在功能上需要“实现什么”,而不是考虑如何去“实现”。需求分析的目标是把用户对待开发软件提出的“要求”或“需要”进行分析与整理,确认后形成描述完整、清晰与规范的文档,确定软件需要实现哪些功能,完成哪些工作。此外,软件的一些非功能性需求(如软件性能、可靠性、响应时间、可扩展性等),软件设计的约束条件,运行时与其他软件的关系等也是软件需求分析的目标。

2.需求分析的重要性

需求分析指的是在建立一个新的或改变一个现存的产品时,确定新产品的目的、范围、定义和功能时所要做的所有工作。这个过程通常涉及多个部门和团队成员,包括产品经理、设计师、开发者、销售团队和潜在用户。产品需求分析的目的是确保产品满足市场的需求,为用户提供价值,并与公司的战略目标和愿景保持一致。

img
img

需求分析的重要性在于:

  • 确保产品方向正确:帮助团队确定正确的产品方向,避免开发与市场和用户需求不符的产品。
  • 提高资源利用效率:需求分析能够明确需求,而明确的需求可以帮助团队更加高效地分配资源,避免浪费时间和资金在不必要或优先级较低的功能上。
  • 降低项目风险:需求分析需要我们去深入了解用户需求和市场趋势,所以它可以帮助团队识别潜在的风险,并提前采取措施来应对。

除此以外,需求分析还能够起到提高产品质量加强团队沟通提高用户满意度等等。

3.需求分析的时机

  • 产品规划阶段:在确定产品方向和目标时,进行需求分析以了解市场和用户需求。
  • 产品设计阶段:在设计产品功能和界面时,根据需求分析结果进行具体的设计。
  • 产品迭代阶段:在产品上线后,根据用户反馈和市场变化,持续进行需求分析以优化产品。

需求分析贯穿整个产品生命周期,但尤其重要的是在项目启动阶段和迭代更新时进行。在项目开始前进行全面的需求分析;在产品上线后根据用户反馈和市场变化不断调整优化需求。

4.需求分析的方法

4.1需求分析的最佳实践和方法论

主要包括以下几个方面:

  1. 业务分析而非系统实现:需求分析的任务不仅仅是分析系统如何实现用户的需要,而是更广泛的业务分析,这包括了对业务知识、问题列表等方面的定义。

  2. SERU框架:《软件需求最佳实践》一书中提倡的SERU框架是一套重要的需求分析方法论,它将目标系统分解为主题域,再分解为流程,最后得到用例以及业务实体。

  3. 需求定义和捕获:需求定义是需求分析的起点,涉及到从用户需求中提炼出产品需求的过程。需求捕获则是在此基础上进一步细化需求,确保需求的准确性和完整性。

  4. 需求分析与建模:在需求分析的第一阶段完成结构框架和行为脉络的梳理后,第二阶段的工作任务是填充需求的细节,即根据前面的框架进行需求细节的填充。

  5. 功能分解、结构化分析、信息建模、面向对象分析:需求分析的主要方法包括功能分解方法、结构化分析方法、信息建模方法以及面向对象的分析方法。这些方法有助于从不同角度深入理解和描述需求。

  6. 深入理解需求并调整认知:需求分析的本质是根据认知进行假设,然后给出判断。核心是不断深入理解需求,调整需求认知,让自己的假设尽可能贴近客观事实,以得出更加准确的判断。

需求分析的最佳实践和方法论是一个综合性的过程,涉及到业务分析、需求定义、捕获、分析与建模等多个环节。通过采用SERU框架等方法论,结合功能分解、结构化分析、信息建模、面向对象分析等方法,可以有效地进行需求分析和建模,从而确保软件开发项目能够满足用户的真实需求。

这里,以下对第5点进行简单阐述包括以下几点:1、功能分解方法;2、结构化分析方法;3、信息建模方法;4、面向对象的分析方法。功能分解方法是将新系统作为多功能模块进行组合。各功能亦可分解为若干子功能及接口,子功能再继续分解。

4.3 功能分解方法在需求分析中的具体步骤和技巧有哪些?

  1. 分解步骤:功能分解首先需要将复杂系统分解为更小、更简单的功能单元。这些步骤可以是具体的功能点,也可以是更抽象的操作流程或用户界面元素。例如,通过用户故事切分流程图来准备待切分的需求,或者通过功能分解法将业务功能和辅助功能分开。

  2. 分析技巧:在进行功能分解时,需要分析每个用例之间的约束关系、执行条件,并组织出各种业务流程图。这有助于清晰地理解每个功能单元之间的关系和相互作用。

  3. 评估与优化:在完成功能分解后,需要对分解后的功能单元进行评估,以确定哪些是核心需求,哪些是次要需求。根据评估结果,可能需要对需求进行进一步的调整和优化。

  4. 技术应用:功能分解方法不仅限于软件开发领域,它还可以应用于其他复杂系统的设计、分析和实现过程中。通过对系统功能的分解,可以简化设计和实现的复杂性,提高效率。

  5. 实践案例:在业务场景分析中,功能分解方法结合从场景到挑战再到方案的思考模型,可以有效完成分析过程,输出初步的解决方案。这种方法强调了从具体场景出发,逐步深入到问题挑战和解决方案的整个过程。

功能分解方法在需求分析中的应用涉及到分解步骤的制定、分析技巧的运用、评估与优化的过程,以及技术应用和实践案例的分享。这些步骤和技巧共同作用于需求分析的各个阶段,帮助团队更高效、准确地理解和满足用户的真实需求。

4.4结构化分析方法在识别关键需求时的有效性如何评估?

  1. 需求分析的重要性:首先,需要认识到对软件需求的深入理解是开发成功的前提和关键。这意味着在进行结构化分析时,必须确保需求分析的准确性和全面性。

  2. 结构化分析过程:结构化分析方法包括与用户沟通获取用户需求的方法、分析建模与规格说明、实体—关系图、数据流图、状态转换图等内容。这些过程有助于确保对需求的全面理解和准确描述。

  3. 图形工具的应用:结构化分析中常用的图形工具包括层次方框图、Warnier图和IPO图等。这些工具有助于清晰地展示需求之间的关系和逻辑结构。

  4. 需求结构化的目标:结构化的目标是在业务需求向代码开发转换时,建立一个数字化标准,统一表达方式。这样做可以减少信息损耗,提高开发效率。

  5. 系统分析师的角色:结构化分析方法假定系统分析师理解问题域的全部,并且有能力正确地识别和分解问题。这种方法通过一次性将系统的功能分解到位,有助于提高分析的深度和广度。

  6. 系统体系结构的有效性评估:虽然结构化分析方法主要用于软件需求分析,但其有效性也可以扩展到评估系统体系结构。这需要考虑业务需求、适应性、可靠性、安全性等多个方面。

  7. 结构化数据分析:对于数据分析而言,确立明确的目标和提出假设是进行有效分析的重要第一步。这同样适用于结构化分析,有助于明确分析的方向和目标。

  8. 功能模型、数据模型和行为模型的使用:结构化分析中通常采用软件的功能模型、数据模型和行为模型来建模用户需求。这种方法有助于更准确地捕捉用户需求。

  9. 从可行性研究阶段得到的数据流图出发:结构化分析方法从可行性研究阶段得到的数据流图为起点,有助于确保需求分析的合理性和有效性。

结构化分析方法在识别关键需求时的有效性可以通过需求分析的重要性、过程的完整性、图形工具的应用、需求结构化的目的、系统分析师的角色、系统体系结构的有效性评估、数据分析方法的应用以及功能模型和行为模型的使用等多个方面进行评估。

4.5 信息建模方法在需求分析中的应用

它从数据角度对现实世界建立模型。大型软件较复杂;很难直接对其分析和设计,常借助模型。模型是开发中常用工具,系统包括数据处理、事务管理和决策支持。实质上,也可看成由一系列有序模型构成,其有序模型通常为功能模型、信息模型、数据模型、控制模型和决策模型。有序是指这些模型是分别在系统的不同开发阶段及开发层次一同建立的。建立系统常用的基本工具是E—R图。经过改进后称为信息建模法,后来又发展为语义数据建模方法,并引入了许多面向对象的特点。 信息建模可定义为实体或对象、属性、关系、父类型/子类型和关联对象。此方法的核心概念是实体和关系,基本工具是E-R图,其基本要素由实体、属性和联系构成。该方法的基本策略是从现实中找出实体,然后再用属性进行描述。

4.6 面向对象的分析方法在需求分析中的使用

面向对象的分析方法的关键是识别问题域内的对象,分析它们之间的关系,并建立三类模型,即对象模型、动态模型和功能模型。面向对象主要考虑类或对象、结构与连接、继承和封装、消息通信,只表示面向对象的分析中几项最重要特征。类的对象是对问题域中事物的完整映射,包括事物的数据特征(即属性)和行为特征(即服务)。

5.需求管理

5.1如何有效地进行需求管理以避免项目延误和资源浪费?

  1. 建立综合性需求框架:这是精细化管理的基础,有助于统一和明确需求的范围、质量要求等关键信息。

  2. 采纳迭代式需求优化:需求不是一成不变的,通过迭代优化可以更快地响应市场变化和用户反馈,同时也能减少因需求不明确而导致的项目延误。

  3. 运用数据分析提高预测准确性:利用数据分析工具来预测项目完成所需的时间和资源,可以帮助管理者合理规划项目进度,减少不必要的等待和返工。

  4. 构建跨部门沟通机制:良好的沟通是需求管理成功的关键。跨部门之间的有效沟通可以确保信息的准确传递,避免误解和重复工作,从而提高工作效率。

  5. 维护产品需求变更的历史记录:对于不断产生的新需求,需要有一个清晰的需求变更历史记录,以便追踪每一次需求变更的原因、影响以及解决方案,确保需求管理的准确性和一致性。

  6. 使用资源管理工具:通过资源管理工具,确保资源的分配与项目的优先级和实际需求相匹配,避免资源的过度分配和浪费,同时也能及时发现资源分配中的问题并进行调整。

  7. 实施版本控制与管理策略:有效实施版本控制可以帮助团队更好地管理需求变更,确保需求文档的准确性和一致性,降低沟通成本和避免冲突。

  8. 区分需求类别并制定优先级规则:在制定优先级规则之前,需要先区分需求类别,并根据“产品管理权”和“需求确定性”来划分需求类型,以确保资源的有效分配。

通过上述步骤,可以有效地进行需求管理,避免项目延误和资源浪费,从而提高项目管理的效率和成功率。

5.2敏捷开发方法在需求分析中的应用及其效果如何?

敏捷开发方法在需求分析中的应用主要体现在其迭代、增量式的方法论上,强调团队成员的自我管理、面对变化时的快速适应能力,以及持续的沟通和协作。敏捷需求分析在需求时机与过程、文档要求、变更、参与者角色等方面与传统方法有所不同,能够更好地参与到项目的生命周期演进中。通过合理的需求收集、需求分析与细化、需求优先级排序和需求跟踪等方法,可以更好地管理和满足项目的需求,提高团队的开发效率和产品质量。

敏捷开发的效果表现在多个方面。首先,敏捷指标的引入为软件开发生命周期中的不同阶段提供了对生产力的洞察,有助于评估产品质量并跟踪、优化团队绩效。其次,敏捷实践可以量化分析多种效能指标,如工作效率、可预测性、质量和响应程度等,这些指标有助于团队和项目管理者了解敏捷实践的实际效果。此外,敏捷开发中的过程度量指标,如业务指标和敏捷指标的跟踪,对于衡量开发过程的各个方面非常重要,这不仅侧重于解决方案是否满足市场需求,也包括衡量开发过程的各个方面。

敏捷开发方法在需求分析中的应用通过其独特的迭代和增量式方法论,结合合理的需求收集和管理方法,有效提升了团队的开发效率和产品质量,同时通过敏捷指标的应用,实现了对项目效能的全面评估和优化。

6.需求分析的关键点

6.1需求分析的关键点主要包括以下几个方面:

  1. 明确用户需求与产品需求的区别:这是需求分析的基础,需要区分用户需求和产品需求,确保产品或服务真正满足目标用户的需求。

  2. 将用户需求转化为产品需求的方法:通过深入细致的调研和分析,将用户非形式的需求表述转化为完整的需求定义,确定系统必须做什么。

  3. 深入挖掘用户动机:在需求分析中,了解和分析用户的动机是非常重要的,这有助于更好地理解和满足用户的需求。

  4. 筛选和优化需求:产品经理需要筛选和优化接收的需求,确保需求的质量和优先级,以提高产品质量和用户满意度。

  5. 收集需求、整理需求、分析需求、确认需求和编写需求文档:这是需求分析的一般流程,包括收集、分类、筛选需求、分析需求等步骤[[3]]。

  6. 使用合适的需求分析方法和工具:根据不同的项目需求和使用场景,选择合适的需求分析方法和工具,如HWM分析法、功能分解方法等。

  7. 考虑需求的业务诉求、目标用户的用户诉求、分析总结需求目标:合理地归类接收的需求,明确需求的业务诉求,明确目标用户的用户诉求,分析总结需求目标,并给设计提供依据。

  8. 需求评估分析方法:包括模糊聚类分析、质量功能展开、KANO模型分析、A/B测试等,这些方法可以帮助评估需求的质量和可行性[[22]]。

综上所述,需求分析的关键点是多方面的,涉及到从明确用户需求到优化需求的整个过程,以及在此过程中采用合适的方法和技术来确保需求的准确性和有效性。

6.2需求分析中的HWM分析法具体是什么,如何应用?

HWM分析法,即"How Might We"(我们可以怎样),是一种用于需求分析和问题解决的方法。它的全称为"How Might We",即"我们可以怎样"。在这个过程中,我们假设问题是可以解决的,只是我们尚不知道如何解决。这种方法强调的是通过头脑风暴的方式,最大范围地搜集产品的各种可能性,然后抽象地整理出这些想法背后所隐藏的核心概念和产品需求,快速梳理出正确的产品设计方向。

应用HWM分析法时,可以分为五个步骤:首先是明确用户场景问题,其次是HMW分解问题,然后是扩展思路,接着是使用不同的分解思路如积极、转移、否定、拆解、脑洞等来拆解许多不同的解决方案,最后是抽象地总结这些想法并确定解决方案。这一过程中,工具的使用也很重要,例如思维导图可以帮助清晰地展示问题和可能的解决方案。

此外,HWM分析法不仅仅局限于产品需求分析,它还涉及到对人类行为和社会系统的分析,以识别存在的问题和潜在的风险,并制定出相应的应对策略和措施。这表明HWM分析法具有较强的综合性和灵活性,能够适应不同领域的需求分析。

HWM分析法是一种通过广泛探索和思考各种可能性来解决复杂问题的方法论,它要求团队成员共同参与,通过明确问题、分解问题、扩展思路、采用多种分解思路以及最终的抽象总结,来寻找最合适的解决方案。

7.需求收集技巧

7.1 需求收集技巧有哪些,特别是在多渠道反馈收集方面的策略?

需求收集技巧在多渠道反馈收集方面的策略主要包括以下几点:

  1. 多渠道反馈收集:企业可以提供多种反馈渠道来确保用户能通过多种方式进行反馈。这包括但不限于在线调查问卷、社交媒体平台、用户论坛等,以确保能够覆盖到不同的用户群体和使用场景。

  2. 观察法:通过观察目标用户的日常行为来理解他们的真正需求。这种方法可以是主动的,即观察用户的行为和工作流程,也可以是被动的,如收集用户对设计原型的反馈。这种方法有助于深入了解用户的实际操作习惯和需求,从而提供更准确的产品改进建议。

  3. 访谈和问卷调查:通过与利益相关者进行沟通和访谈,了解他们对于产品或服务的需求和预期。同时,设计有效的问卷调查也是需求收集的重要手段,可以帮助收集更广泛的需求信息。

  4. 文档分析:利用文档(如用户手册、操作指南等)进行分析,可以发现那些不易直接表达但对产品改进至关重要的需求。这种方法适用于那些需要详细说明或解释的产品功能。

  5. 用户反馈渠道:除了上述提到的多种渠道外,还应考虑建立专门的用户反馈机制,如定期发布用户调查问卷、开放日活动等,以直接从用户那里获取反馈。这些反馈可以帮助团队及时了解当前产品或服务的不足之处,以及潜在的用户痛点。

多渠道反馈收集策略要求企业综合运用多种技术和方法,既要注重面对面的沟通和观察,也要充分利用网络和数字工具,同时不忘通过文档分析等方式深入挖掘用户需求。这样的策略能够帮助企业构建更加全面和准确的需求收集系统,为产品和服务改进提供坚实的基础。

7.2 如何有效地进行用户访谈以收集需求?

有效地进行用户访谈以收集需求,首先需要确保访谈环境轻松愉快,避免给受访人带来社会压力。用户访谈是一种有计划、有目的、有意识的过程,通常有明确的时间安排和谈话主题。在准备阶段,应具备正确的预备知识,具备细致的洞察力、耐心和责任感。

在访谈过程中,应积极倾听受访者的意见,不要害怕沉默,也不要强制用户回答。可以在用户回答后以自己理解的方式重复答案,以避免对用户的回答产生误解。此外,正确并恰当地提出问题是解决困惑的第一步,用户访谈首先是一门艺术——说话和倾听艺术,也是提问的艺术。

有效的访谈需要满足提对问题、正确沟通、提炼转化三个条件。这意味着在访谈中,不仅要提出正确的问题,还要通过访谈技巧有效获取用户信息,并将调研信息转化为洞察分析。

观察法也是理解用户真正需求的有效方式之一。通过观察用户的日常行为,可以主动或被动进行访谈,以理解他们当前的工作流程。这种方法有助于深入了解用户的需求和偏好。

总之,有效地进行用户访谈收集需求的过程包括创造友好的访谈环境、准备充分的预备知识、积极倾听和正确提问。同时,结合观察法等其他方法,可以更全面地理解和满足用户需求。

8.需求分析的误区

避免需求分析常见误区的具体步骤和方法:

  1. 目标驱动,结构分解:首先,明确需求分析的目标是非常重要的。这包括了解项目旨在达成的目标,如吸引新用户、保留老用户、提高用户活跃度或产生营收等。接着,根据这些目标,结构分解出为了达成这些目标而需要的需求。

  2. 避免把用户描述当作需求:在需求分析中,不应该仅仅基于用户的描述来确定需求。这种做法很容易导致需求偏离实际业务目标,从而无法满足核心业务的要求。

  3. 避免把数据表象当需求:需求分析时,不应只关注数据表现,而忽视是否有偏离业务的情况。过分依赖数据可能会误导需求分析,导致开发出来的产品与预期目标不符。

  4. 避免把竞品功能当需求:在需求分析过程中,不应该简单地将竞品的功能照搬过来。每个产品的市场定位和目标用户群体都有所不同,直接复制竞品的需求可能会导致产品失败。

  5. 关注用户及业务目标并重:在进行需求分析时,不仅要关注用户需求,还要关注业务目标。确保需求分析能够支持业务目标的实现,而不是单纯地迎合用户需求。

  6. 使用严谨而科学的分析方法:需求分析不应该是一种随意的思考过程,而是需要遵循一定的科学方法和步骤。例如,可以使用马洛斯需求模型来分析需求,然后分析用户和场景,最后分析用户期望。

  7. 识别伪需求:通过学习和实践,学会辨别哪些是需求分析中的常见伪需求。这包括识别那些看似合理但实际上并不符合业务逻辑或目标的需求。

需求分析的误区包括没有进行有效的需求管理、对需求理解不够深入、忽视了需求的可追踪性和变更控制机制等。为了避免误区,产品经理应该采用敏捷开发方法,建立严格的需求变更管理流程,对任何需求变更进行详细讨论、评估、记录。此外,正确分辨用户提出的是需求还是解决需求的方案也是避免的一个误区。

综上所述,产品经理在进行需求分析时,需要从宏观到微观全面考虑,利用多种工具和方法进行深入分析,同时注意需求管理的各个方面,以确保产品能够满足市场和用户的真实需求,与公司的战略目标和愿景保持一致。


作者:西边一山
来源:mdnice.com/writing/b3ea0873bba04ab98767930c4d9a268b
收起阅读 »

JavaScript作用域详解

web
作用域可分为词法作用域和动态作用域,JavaScript 使用词法作用域,也称为静态作用域。 词法作用域是指变量的作用域在代码写好的时候就确定了,而不是在运行时确定。函数在定义的时候就决定了其作用域,而不是在调用的时候。 JavaScript 的作用域(S...
继续阅读 »

作用域可分为词法作用域和动态作用域,JavaScript 使用词法作用域,也称为静态作用域。


词法作用域是指变量的作用域在代码写好的时候就确定了,而不是在运行时确定。函数在定义的时候就决定了其作用域,而不是在调用的时候。


JavaScript 的作用域(Scope)是指在代码中定义变量时,这些变量在哪里以及在哪些地方可以被访问。作用域控制着变量的可见性和生命周期。在 JavaScript 中,有全局作用域和局部作用域的概念,作用域的规则由函数定义和代码块定义来决定。


1. 全局作用域(Global Scope)


全局作用域是指在整个 JavaScript 程序中都可访问的范围。在全局作用域中定义的变量和函数可以被任何地方访问,包括代码文件、函数内部、循环块等。例如:


var globalVariable = "I am global";

function globalFunction({
  console.log(globalVariable);
}

globalFunction(); // 输出: I am global

2. 局部作用域(Local Scope)


局部作用域是指在函数内部或代码块内部定义的变量,其可见性仅限于该函数或代码块内部。这种作用域遵循 "变量提升" 的规则,即变量在声明之前就可以被访问,但其值为 undefined。例如:


function localScopeExample({
  var localVariable = "I am local";
  console.log(localVariable);
}

localScopeExample(); // 输出: I am local
console.log(localVariable); // 错误,localVariable 不在此处可见

3. 块级作用域(Block Scope)


在 ES6 引入块级作用域概念,可以通过 letconst 关键字在代码块内定义变量,这使得变量在块级范围内有效。在此之前,JavaScript 只有函数作用域,使用 var 关键字定义的变量在整个函数范围内有效。


if (true) {
  let blockVariable = "I am in a block";
  console.log(blockVariable);
}

console.log(blockVariable); // 错误,blockVariable 不在此处可见

总结


作用域是 JavaScript 中重要的概念,理解作用域有助于正确使用变量、避免命名冲突,提高代码的可维护性。


作者:MasterBao
来源:mdnice.com/writing/c771e23f7b014afbbe42499a1b32b0f7
收起阅读 »

2023总结:30岁,结束8年北漂回老家,降薪2/3,我把人生过的稀烂

一转眼又快过年了,回想整个23年,简直是我人生中最黑暗的一年(之一)。 23年,我30岁,在北京干了8年程序员。30岁这年我做了一个决定:结束8年北漂生涯,回老家(一个三线城市)自己创业,去做自媒体。 一、为何做出这个决定 这个决定也不是一时拍脑袋做出的决定...
继续阅读 »

一转眼又快过年了,回想整个23年,简直是我人生中最黑暗的一年(之一)。



23年,我30岁,在北京干了8年程序员。30岁这年我做了一个决定:结束8年北漂生涯,回老家(一个三线城市)自己创业,去做自媒体。


一、为何做出这个决定


这个决定也不是一时拍脑袋做出的决定,导火索是在22年:


那时候大环境不好,大家都越来越卷,下班的时间也越来越晚。


放假回家亲戚朋友总说,你在北京996这么累,图啥啊,工资是高点,但是完全没有生活啊。而且你在北京漂到啥时候是个头?你又买不起房,又没户口,早晚得回来吧。


我仔细想想也有道理,活了这么多年了都在当牛做马,被pua,还得面临35岁危机,真的受够这种生活了!所以那时候心里埋下了一颗种子:我要去浪浪山的那边看看!


其实我本身就是一个喜欢自由的人,这么多年那句“打工是不可能打工的,这辈子都不会打工”一直激励着我,我想自己有一天也能实现不打工这个目标。


于是22年底我做了一个决定:23年去山的那边看看大海的样子!拿完年终奖就辞职!去创业,去开启我的新的人生!


在准备辞职前的几件事情,都让我更加坚定了辞职的决心:



  1. 那时候还没有放开,在家线上办公,本来在公司办公是995,晚上9-10点下班了基本就没啥事情了,但是在家就不一样了,每天各种电话、视频会议,甚至十一二点都要开会,恨不得让你24h都在线,生活和工作基本都没有边界。那个时候只要听到会议呼叫的声音,内心就一紧,心中默默祈祷不要出什么幺蛾子,都快成心理阴影了。

  2. 当时得了新冠也只敢请了一天假,第二天晕晕乎乎的就继续开始工作了。因为我知道,落下的工作最后还得你自己加班完成,否则领导最后还会赖你延期。

  3. 周末也需要随时在线,需要及时回复群里的消息,需要随时解决线上的问题,否则就会打上工作态度不好的标签,绩效肯定低。导致我周末基本不敢出去,出去也得随时看着手机,看有没有@你的消息,整天提心吊胆,玩也玩不好,还不如在家躺着。


我觉得这不是我想要的生活,每天太累了,身体累不算,心还累,生怕自己负责的业务出了什么问题,如坐针毡,如芒刺背,如履薄冰。


二、我辞职了


终于,熬到23年,拿到年终奖后,我决定提出离职。


当时身边有些人劝我不要辞职,说现在环境不好,更不应该放弃你的老本行,去做啥自媒体。


我当时内心嗤之以鼻,心想程序员这行也就干到35岁,而且现在卷的不行,加班加的身体都快废了,这行岁数大了没前途!我趁现在30岁还年轻,创业正值当年,辞职改行的选择非常有战略眼光!(当时真的是感觉杰克马附体,准备在这个三十而立的年纪,大干一场!)


2b5f9de5dbc7cd40403819a50d693574.jpeg


当然我也不是脑袋一热就想着辞职去做自媒体,辞职前我做了充足的准备,和很长时间的调研&分析:



  • 我作为一个互联网人,做实体店肯定不是我擅长的,肯定只能从互联网上选择行业,互联网项目有很多:个人工具站,知乎好物,闲鱼二手书,小红书带货,抖音带货,抖音个人ip,公众号写作,短剧cps,小说推文,知识付费等等的项目,我可以说都看了一个遍,其中抖音现在流量最大,要做风口上的猪,做抖音相关肯定要容易很多。

  • 然后我也学习了一些创业相关的知识,比如如何找对标,如何分析对方商业模式,参加了很多知识付费的圈子,然后还报了小红书和抖音的培训班,总共加起来得有1w多呢。

  • 而且我还预留了充足的资金,我做了最坏的打算,就算我一分钱不挣,也够我活3年呢,我不会3年都创业不成功吧!(此处白岩松表情包:不会吧!.jpg)


u=1021210702,2199782272&fm=253&fmt=auto&app=120&f=JPEG.webp


三、想象很美好


为了这次创业,我还制定了计划,年度计划,季度计划,月计划,周计划,天计划,真的非常详细。


我也要很自律,每天按时起床,锻炼,学习,做业务。这次我真的抱着必胜的决心来做!


当然我也提前列出可能要遇到的风险,并思考解决方案:


比如项目进展慢怎么办,拖延症怎么办,家人反对怎么办,朋友约吃饭打乱我的计划怎么办,遇到困难我该怎么应对等等


这么一套组合拳下来,我觉得已经万事俱备,只差开干了!


四、现实很残酷


4月我如期辞职,当时正值清明节,淅淅沥沥的小雨并没有浇灭我开启新生活的热情。辞职后,我就按计划开始早睡早起,锻炼,学习,搞创业的事情。


但是马上就被打脸了,这是我创业中遇到的第一个难题,也是我万万没有预料到的


就在我创业后的不久,我患上焦虑症,失眠了,而且还很严重,就是那种从晚上11点躺下,躺到早上6点才睡着的那种失眠,而且还时不时的心悸。


我万万没想到会患上失眠症。因为我觉得没有上班的压力了,想啥时候干活就啥时候干活,想干多少干多少,想啥时候下班就啥时候下班,也没人pua我了,还有时间锻炼,应该睡得更好才是。


但实际并不是这样,对于一个从小被学校管,长大了被公司管的芸芸众生来说,创业实际是这样的:



  1. 你会非常忙,比上班还要忙,因为你之前是螺丝钉,做好自己的本职工作就好了,现在事无巨细,都你一个人。比如做自媒体,从开始的账号定位-》内容选题-》写脚本-》置景&拍摄-》后期剪辑-》选品-》商务对接-》客服-》用户社群运营,所有的环节,都得你自己一个人。然后视频没流量怎么办,违规了怎么办,付费转化率低怎么办,还是只有你自己去解决。(之前公司让你干啥你干啥,你只需要规定时间完成你的任务就好了)

  2. 面对大量的自由时间,你根本不会支配时间,因为很多环节你都是小白,要学习的东西太多,但是你天天光学习了,每天看似很忙,但是看不到产出,导致你就很沮丧。(之前你只做熟悉的工作,产出是有保证的)

  3. 行动困扰,没有目标感,没有人给你一个目标&方向,你不知道你现在做的事情对挣钱有没有价值,你会迷茫,你会时常自我怀疑。(之前你只是专注领导安排的任务,至于这个任务能不能帮公司挣到钱,那是公司的事情,你关心到手的工资而已)

  4. 没有成就感,认同感。因为现在你很多事情要从0开始,比如写文案要求写作能力,拍视频要求表现力,搞流量要求你有运营&营销的能力 ,相比之前做熟悉工作,感觉上会有落差(之前工作中都是做你擅长的领域,每完成一项任务也很有成就感,做的出色还能收获同事和领导的认可)

  5. 和社会断了链接,没有存在感,归属感(这是人类的基本需求之一),你不属于任何一个群体,没有人赞扬,尊重,接纳你,甚至你想被骂两句也没人鸟你(之前在公司,做的好了领导或者同事会夸你两句,做的不好了可能会给你建议,起码有人能倾诉,能交流,能寻求帮助)

  6. 没有了收入,眼见钱包一天天变少,你肯定会焦虑。但是更让你焦虑的,是你不知道未来什么时候能挣到钱。更更让你焦虑的,是不知道最后能不能挣到钱。(之前工作压力不管有多大,多累,起码你还有工资,你还有吃饭的钱,这是底气)


所以在此奉劝有裸辞创业想法的人,千万不要裸辞!裸辞创业九死一生! 正确的做法是一边工作愿一边做副业,等副业的收入和工资差不多甚至超过工资了,再去辞职。有人会说,我工作那么忙,根本没时间搞副业啊。我之前也是这么想的,但是现在我会告诉你:没有时间就去挤出时间,每天晚睡或者早起一会,周末也抽出时间搞。这点问题都解决不了?创业的遇到问题会比这难十倍!如果这个你觉得太难了,那我劝你还是老老实实打工吧。


但是我已经裸辞了,没办法,只能去解决问题,我开始吃助眠药,喝中药,有些好转,但也没治好,只是比之前好点。


就这么拖着残血的半条命,我坚持了半年多,一半时间学习,一半时间实践,搞了两个自媒体号,第一个号违规被封了,第二个号流量也没啥起色。这条路是越走越看不到希望,每天晚上都不想睡觉,因为害怕明天的到来,因为明早一起床,眼前又是一片黑暗。


五、彻底崩溃


11月,因为种种原因和媳妇生了一场气,我觉得对于我创业,她不鼓励也就算了,在我状态这么差的情况下还不能对我包容一点,甚至有点拆后台的感觉,那几天我就像一个泄了气的皮球,内心被彻底击垮了。(所以现在有点理解每个成功男人的背后,都有一个伟大的女人这句话的含义了)


终于,在创业的压力,8个月没有收入的恐慌,焦虑失眠心悸的折磨中,我决定放弃了。


失败了,彻彻底底的失败。回想这次经历,就好像之前在一艘航行的货轮上打工,然后受不船上的种种压榨,终于鼓起勇气,自己带着一艘救生艇,跳海奔向自己想要的自由。结果高估了自己的目前的实力,经不起茫茫大海狂风骤雨,翻船了。。濒临溺亡。。。


六、重新找工作


放弃后的那几周,我开始熬夜,开始暴饮暴食,之前的运动也放弃了。整天在家里拉着窗帘,除了吃饭就是躺在床上刷手机,让我尽可能分散注意力,减少内心的痛苦。


但是这样的状态也不是事儿啊,目前肯定是不想再去面对创业的事情了,那只能去找个工作先干着了。


刚开始找工作内心又有不甘,因为一个三线城市比起北京来说,不管是工作机会,环境,薪资来说,都差太多。


但是没办法,我现在的感觉就是快溺死了,我现在急需一个救命稻草,活下来,是我目前的首要任务。


于是在网上海投了一遍,结果惨不忍睹,根本没几家公司招人,前前后后一个月,真正靠谱的面试就一家,是的,只有一家。


好在这家也顺利拿了offer,是一家刚创业的公司,一共十几个人,薪资只有原来1/3多点,但是拿到offer那一刻我依然有些激动,我感觉我活下来了,不管怎样,现在能喘口气了。


七、迷茫的未来


现在上班已经一个多月了,公司挺好,不加班,基本上7点前就都走了,离家也挺近,骑个共享单车也就10分钟。这一个月,焦虑没了,不心悸了,失眠也好了。每天就是按部就班上下班,完成老板给的任务,其他的事情也不用自己操心,终于又做起自己熟悉且擅长的事情。


但是内心还是有落差,本来北京好好的工作自己作死给辞了,要不这一年也能攒不少钱呢,现在不但钱没了,这几个月还花了好几w,最后还差点嘎了。


其实入职这家公司前,北京之前的同事问我要不要回去,说现在正忙,我说你先问问吧。


我当时也纠结,如果真的能回去,我还要不要回去,毕竟在那边挣一个月顶这边仨月。但是回都回来了,再去北京可能就一辈子留北京了吧。


不过后来同事说年前没有招人计划了,可能要年后了,如果招人到时再联系我。正好我不用纠结了,这可能就是命运的安排吧。


不过真的想问问你们,如果到时有机会,是继续北漂呢,还是选择在老家呢?


八、结语


说实话,我现在知道了,山的那边还是山,我不知道什么时候才能看到海,甚至我可能一辈子都看不到海了。不过目前想的就是,调整好状态,先走一步算一步吧。


30岁的年纪,学会和自己和解,学会接受自己的平庸,但是依然要努力,毕竟在这个阴雨连天的环境下,没有伞的孩子只能努力奔跑。


作者:骆驼箱子
来源:juejin.cn/post/7330439494666453018
收起阅读 »

7年Android仔的逆袭人生

1.引言 最近在代码人生模块,看到了很多优秀的同行分享自己的人生经历。有感情的,有创业的,有独立开发者的。看完后感慨良多。为此我也想讲讲我的成长经历。相信能给各位一定的启发。 2.毕业 我是一个来自湖北农村的少年。从小到大学费都是父母卖粮食,卖棉花,找亲朋好友...
继续阅读 »

1.引言


最近在代码人生模块,看到了很多优秀的同行分享自己的人生经历。有感情的,有创业的,有独立开发者的。看完后感慨良多。为此我也想讲讲我的成长经历。相信能给各位一定的启发。


2.毕业


我是一个来自湖北农村的少年。从小到大学费都是父母卖粮食,卖棉花,找亲朋好友,东拼西凑的。要不是勉强考上二本大学。可能现在的我就在工厂搬砖。我记得大四快出去实习的时候,家里没有钱给我路费,硬是卖掉家里十几袋麦子,给我凑的。同期的同学别人家里都是给5000.6000的。而我只有2500。就这样我带着仅有的2500,拉着一个破旧的行李箱踏上南下之旅。怀揣着兴奋,对未来美好的期待来到深圳。运气比较好,很顺利的找到一家公司。那家公司给我开了10000/月的工资。你们知道吗。我是多么多么的兴奋啊。1万啊!,我从来没有掌管过这么多钱。甚至我家里从来没有过1w。过去的二十多年,我的物质生活一直没有被满足过。吃的,用的,都是差人一等。第一台华为手机,也是硬生生用了4年。哪一年我22岁。现在想起来,依旧很兴奋。当时把工作的事情,告诉了父母。父母开心的不得了。我爸在家对我妈说:“我就知道儿子,会有出息”。


3.动荡


本来一切都在向好的方向发展,可是老天爷却给你开了一个玩笑。2017年7月份的一天,一通电话彻底打乱了我的生活节奏。就像一块砖头一下子丢进水里,“咚”的一声。砸在我的心里。电话那头,我姐姐哭着叫我回去,叫我赶紧回去。她告诉我:“爸爸去世了,你赶紧回来,赶紧回来”。面对突如其来的恶耗。经历过的人,应该都知道。那一刻大脑实际上是懵的。呼吸紧促,仿佛喘不过气。内心会质疑这个消息的准确性。觉得不可能,不可能。在家里好好的,好好的,为什么会突然间走了。时至今日,即使过去多年,回想到这些。我依旧会泪流满面。哪一年我22岁。本该幸福的家庭,却发生这样的变故。父亲是一个家庭的基石。是子女坚实的靠山。哪一年,我失去了依靠。也失去了一个完整的家庭。
往后的这些年,我时常梦见我父亲。梦见他还活着,梦到他再和我说话。


父亲是因为急性心肌梗塞走的。发生心肌梗塞的时候,实际上身体会有反应的,例如 四肢无力,例如心跳加速。在出事的前几天,他因为身体不舒服,去过诊所问诊,但是因为医疗条件的不足,加上之前整个家族没有这样的例子。所以没有检测出来。这件事让我意识到世上很多事,我们无能为力,唯有学会的接受


同时,我也会不断的追问自己,为什么会出现在我的身上。因果因果,有这个果,必然有这个因。这个因在哪里呢。我想是这几个因:1.农村人的健康意识差 2.身体不舒服 总觉得自己扛扛就过去了 3.本身存在各种疾病,例如高血压。4.农村的医疗条件差


同样世上很多事,也可以用因果来解释。例如溺水身亡,触电,交通事故。一桩桩悲惨的事件的后面,肯定是有很多因。触电之前,可能有过多次,湿手触碰电器的行为,进而养成习惯; 交通事故之前,可能经常抢红绿灯,经常超车。


也正是因为这个意识的养成。帮助了我改变人生,当然这都是后话。在我父亲去世的三个月之后,爷爷因为接受不了打击,也去世了。仿佛是潘多拉魔盒被打开了,让这个家族饱受磨难。


4.工作


处理完家里的事之后。又匆匆返回深圳,继续当螺丝钉。整个人思想压力也变重了。我未来结婚,买车,买房,给母亲养老都得靠自己。每当我望向身后,我看到的是深渊。我不敢松懈,我不敢像一个正常人那样生活,因为我没有依靠,我只有自己,我得努力,拼搏,奋斗。周六周天会抽出一天的时间去图书馆学习。现在想想,自己能坚持下来,也挺佩服曾经的自己。或许是上天的眷顾,也或许是运气好。在面试OPPO的时候。面试官的问题,正好是我前两天写的博客。于是顺理成章的进入了大厂。在OPPO呆了2年多,技术上得到很大的提升,经历过裁员,经历过背锅,经历过职场的勾心斗角。这段工作经历,拔高了我的世界观,让我从懵懂的少年,蜕变成一个合格的职场人。同时这段经历也间接的改变了我的人生轨迹。


5.感情


自从家里发生变故后,我对未来的规划更加清楚。不管是工作,还是感情上的事。都会提前做准备。例如在oppo的时候。注意到当时一个表现优异,绩效A的同事,来年却转岗了。通过他,我知道了部门发展不顺利,未来可能有裁员的风险。于是就开始私下准备面试的工作。21年一整年,我私下面试了接近40家公司。22年又面试了10多家。当时就明显感受到市场的寒冷。意识到22年假如不跳槽。可能就要一直待在OPPO,等待被裁。于是就果断的跳槽到顺丰。拿到了将近40%的涨幅。虽然后面又离开了顺丰。但是现在来看,当初的选择是没有错误的。


在我年仅26岁的时候。我就在考虑结婚的事,因为我笃信 一个好的伴侣抵得上百万资产,抵得上几十万的高薪工作,她直接决定了你下半生的生活质量。同时我也知道自己一无所有,无存款,无家庭,甚至攒下的钱都不够房子的首付。但是我审视了自己一番。发现自己还是有一些优点的,例如 五官不差,身高1.8,工作也还行,年薪30多万。了解了自己的优点和缺点之后。就开始着手改变自己。


例如跑步减肥从180减到150,每天5公里户外跑步。同时也积极参加一些穿衣打扮课程。了解自己的穿衣风格。 有时间就去相亲会相亲。
说到相亲,我可是相亲界老手了。😁😝 前前后后相亲了 20多场。最后找到了现如今的老婆。


在这段经历当中,一个意识深深的插入到了我的脑海中。那就是 任何事都是有俩面性的,看待事情,争取看到事情的俩面性。这样自己才能提前做准备。


例如:程序员以高薪著称,但是反面就是,肥胖,脱发,油腻。这在相亲市场上是非常不利的。


工程型人才,往往看不上溜须拍马,耍嘴皮子那套。但是随着自媒体的盛行,发现自媒体做的好的人,恰恰都是耍嘴皮子,能放下身段,脱下长衫的人。这个认识正对应了一句话:”当你凝视深渊的时候,深渊正在凝视着你"


6.后记


写这篇分享的时候,老婆已经怀孕8个月了,再有2个月,我就要当爸了。孩子的名字都已经想好。目前定居于深圳,因为老婆家庭条件好,老丈人给了一套豪宅居住。让我免于房贷的压力。有充足的精力为下一个10年奋斗。说到这里我很感谢老婆一家人。他们没有嫌弃我穷,反而时时刻刻照顾我的自尊心。他们一家是我的人生贵人。我问了老丈人很多问题。他都能给我带来不一样的视角,耐心讲给我听。这是多少钱都买不来的。以后有机会可以在聊聊他的一些观点。有一些深深的刻在我的脑海中了。


作者:薯条1492738192844
来源:juejin.cn/post/7351658802393546802
收起阅读 »

面试官:线程调用2次start会怎样?我支支吾吾没答上来

写在开头 刚刚吃晚饭时,突然想到了多年前自己面试时的亲身经历,决定再回来补充一个小知识点! 记得是一个周末去面试Java后端开发工程师岗位,面试官针对Java多线程进行了狂轰乱炸般的考问,什么线程创建的方式、线程的状态、各状态间的切换、如果保证线程安全、各种锁...
继续阅读 »

写在开头


刚刚吃晚饭时,突然想到了多年前自己面试时的亲身经历,决定再回来补充一个小知识点!


记得是一个周末去面试Java后端开发工程师岗位,面试官针对Java多线程进行了狂轰乱炸般的考问,什么线程创建的方式、线程的状态、各状态间的切换、如果保证线程安全、各种锁的区别,如何使用等等,因为有好好背八股文,所以七七八八的也答上来了,但最后面试官问了一个现在看来很简单,但当时根本不知道的问题,他先是问了我,看过Thread的源码没,我毫不犹豫的回答看过,紧接着他问:



线程在调用了一次start启动后,再调用一次可以不?如果线程执行完,同样再调用一次start又会怎么样?



这个问题抛给你们,请问该如何作答呢?


线程的启动


我们知道虽然很多八股文面试题中说Java创建线程的方式有3种、4种,或者更多种,但实际上真正可以创建一个线程的只有new Thread().start();


【代码示例1】


public class Test {
public static void main(String[] args) {
Thread thread = new Thread(() -> {});
System.out.println(thread.getName()+":"+thread.getState());
thread.start();
System.out.println(thread.getName()+":"+thread.getState());
}
}

输出:


Thread-0:NEW
Thread-0:RUNNABLE

创建一个Thread,这时线程处于NEW状态,这时调用start()方法,会让线程进入到RUNNABLE状态。


RUNNABLE的线程调用start


在上面测试代码的基础上,我们再次调用start()方法。


【代码示例2】


public class Test {
public static void main(String[] args) {
Thread thread = new Thread(() -> {});
System.out.println(thread.getName()+":"+thread.getState());
//第一次调用start
thread.start();
System.out.println(thread.getName()+":"+thread.getState());
//第二次调用start
thread.start();
System.out.println(thread.getName()+":"+thread.getState());
}
}

输出:


Thread-0:NEW
Thread-0:RUNNABLE
Exception in thread "main" java.lang.IllegalThreadStateException
at java.lang.Thread.start(Thread.java:708)
at com.javabuild.server.pojo.Test.main(Test.java:17)

第二次调用时,代码抛出IllegalThreadStateException异常。


这是为什么呢?我们跟进start源码中一探究竟!


【源码解析1】


// 使用synchronized关键字保证这个方法是线程安全的
public synchronized void start() {
// threadStatus != 0 表示这个线程已经被启动过或已经结束了
// 如果试图再次启动这个线程,就会抛出IllegalThreadStateException异常
if (threadStatus != 0)
throw new IllegalThreadStateException();

// 将这个线程添加到当前线程的线程组中
group.add(this);

// 声明一个变量,用于记录线程是否启动成功
boolean started = false;
try {
// 使用native方法启动这个线程
start0();
// 如果没有抛出异常,那么started被设为true,表示线程启动成功
started = true;
} finally {
// 在finally语句块中,无论try语句块中的代码是否抛出异常,都会执行
try {
// 如果线程没有启动成功,就从线程组中移除这个线程
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
// 如果在移除线程的过程中发生了异常,我们选择忽略这个异常
}
}
}

这里有个threadStatus,若它不等于0表示线程已经启动或结束,直接抛IllegalThreadStateException异常,我们在start源码中打上断点,从第一次start中跟入进去,发现此时没有报异常。


new线程.png
此时的threadStatus=0,线程状态为NEW,断点继续向下走时,走到native方法start0()时,threadStatus=5,线程状态为RUNNABLE。此时,我们从第二个start中进入断点。


runnable线程.png
这时threadStatus=5,满足不等于0条件,抛出IllegalThreadStateException异常!


TERMINATED的线程调用start


终止状态下的线程,情况和RUNNABLE类似!


【代码示例3】


public class Test {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {});
thread.start();
Thread.sleep(1000);
System.out.println(thread.getName()+":"+thread.getState());
thread.start();
System.out.println(thread.getName()+":"+thread.getState());
}
}

输出:


Thread-0:TERMINATED
Exception in thread "main" java.lang.IllegalThreadStateException
at java.lang.Thread.start(Thread.java:708)
at com.javabuild.server.pojo.Test.main(Test.java:17)

这时同样也满足不等于0条件,抛出IllegalThreadStateException异常!


我们其实可以跟入到state的源码中,看一看线程几种状态设定的逻辑。


【源码解析2】


// Thread.getState方法源码:
public State getState() {
// get current thread state
return sun.misc.VM.toThreadState(threadStatus);
}

// sun.misc.VM 源码:
// 如果线程的状态值和4做位与操作结果不为0,线程处于RUNNABLE状态。
// 如果线程的状态值和1024做位与操作结果不为0,线程处于BLOCKED状态。
// 如果线程的状态值和16做位与操作结果不为0,线程处于WAITING状态。
// 如果线程的状态值和32做位与操作结果不为0,线程处于TIMED_WAITING状态。
// 如果线程的状态值和2做位与操作结果不为0,线程处于TERMINATED状态。
// 最后,如果线程的状态值和1做位与操作结果为0,线程处于NEW状态,否则线程处于RUNNABLE状态。
public static State toThreadState(int var0) {
if ((var0 & 4) != 0) {
return State.RUNNABLE;
} else if ((var0 & 1024) != 0) {
return State.BLOCKED;
} else if ((var0 & 16) != 0) {
return State.WAITING;
} else if ((var0 & 32) != 0) {
return State.TIMED_WAITING;
} else if ((var0 & 2) != 0) {
return State.TERMINATED;
} else {
return (var0 & 1) == 0 ? State.NEW : State.RUNNABLE;
}
}

总结


OK,今天就讲这么多啦,其实现在回头看看,这仅是一个简单且微小的细节而已,但对于刚准备步入职场的我来说,却是一个难题,今天写出来,除了和大家分享一下Java线程中的小细节外,更多的是希望正在准备面试的小伙伴们,能够心细,多看源码,多问自己为什么?并去追寻答案,Java开发不可浅尝辄止。




作者:JavaBuild
来源:juejin.cn/post/7345071481375932451
收起阅读 »

【CSS定位属性】用CSS定位属性精确控制你的网页布局!

CSS定位属性是用于控制网页中元素位置的一种方式,它能够让元素在页面上精准地落在我们想要的位置。在CSS中,定位(Positioning)是控制元素在页面上如何定位和显示的一种机制。它主要包括四种属性:静态定位(static)、相对定位(relative)、绝...
继续阅读 »

CSS定位属性是用于控制网页中元素位置的一种方式,它能够让元素在页面上精准地落在我们想要的位置。

在CSS中,定位(Positioning)是控制元素在页面上如何定位和显示的一种机制。它主要包括四种属性:静态定位(static)、相对定位(relative)、绝对定位(absolute)、固定定位(fixed)。

每种定位方式都有其独特的特点和使用场景,下面将分别介绍这几种定位属性。

一、Static(静态定位)

静态定位是元素的默认定位方式,元素按照正常的文档流进行排列。在静态定位状态下,不能配合top、bottom、left、right来改变元素的位置。

  • 可以用于取消元素之前的定位设置。

代码示例:

<!DOCTYPE html>
<html>
<head>
<style>
.static {
background-color: lightblue;
padding: 100px;
}
</style>
</head>
<body>


<div>这是一个静态定位的元素。</div>


</body>
</html>

Description

二、Fixed(固定定位)

固定定位使元素相对于浏览器窗口进行定位,即使页面滚动,元素也会保持在固定的位置。

  • 固定定位的元素会脱离正常的文档流。

示例代码:

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
*{
margin: 0;
padding: 0;
}
body{
/* 给整个页面设置高度,出滚动条以便观察 */
height: 5000px;
}
div{
width: 100px;
height: 100px;
background-color: blue;
/* 固定定位 */
position: fixed;
right: 100px;
bottom: 100px;
}
</style>
</head>
<body>
<div></div>
</body>
</html>

运行结果:

移动前

Description

移动后

Description

比如我们经常看到的网页右下角显示的“返回到顶部”,就可以用固定定位来实现。

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
body {
position: relative;
}
.content {
/* 页面内容样式 */
}
#backToTop {
position: fixed;
bottom: 20px;
right: 20px;
background-color: #333;
color: #fff;
border: none;
padding: 10px;
cursor: pointer;
}
</style>
</head>
<body style="height: 5000px;">
<div>

</div>
<button id="backToTop" onclick="scrollToTop()">返回顶部</button>
<script>
function scrollToTop() {
window.scrollTo({top: 0, behavior: 'smooth'});
}
</script>
</body>
</html>

运行结果:

Description

三、Relative(相对定位)

相对定位是将元素对于它在标准流中的位置进行定位,通过设置边移属性top、bottom、left、right,使指定元素相对于其正常位置进行偏移。如果没有定位偏移量,对元素本身没有任何影响。

不使元素脱离文档流,空间会保留,不影响其他布局。

代码示例:


<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.box1{
width:200px;
height:100px;
background:skyblue;
margin:10px;
}
.box2{
width:200px;
height:100px;
background:pink;
margin:10px;
position:relative;/*相对定位*/
left:100px;/*向右偏移100px*/
top:-50px;/*向上偏移50px*/
}
.box3{
width:200px;
height:100px;
background:yellowgreen;
margin:10px;
}
</style>
</head>
<body>
<div>1</div>
<div>2</div>
<div>3</div>
</body>
</html>

运行结果:

没使用相对定位之前是这样的:

Description

使用相对定位后:相对于原来的位置向右偏移了100px,向上偏移50px。
Description

虽然它的位置发生了变化,但它在标准文档流中的原位置依然保留。

四、Absolute(绝对定位)

绝对定位使元素相对于最近的非 static 定位祖先元素进行定位。如果没有这样的元素,则相对于初始包含块(initial containing block)。绝对定位的元素会脱离正常的文档流。

  • 如果该元素为内联元素,则会变成块级元素,可直接设置其宽和高的值(让内联具备快特性);

  • 如果该元素为块级元素,使其宽度根据内容决定。(让块具备内联的特性)

<style>
.wrap{
width:500px;
height:400px;
border: 2px solid red;
}
.box1{
width:200px;
height:100px;
background:skyblue;
margin:10px;
}
.box2{
width:200px;
height:100px;
background:pink;
margin:10px;
position:absolute;/*绝对定位*/
left:100px;/*向右偏移100px*/
top:30px;/*向下偏移30px*/
}
.box3{
width:200px;
height:100px;
background:yellowgreen;
margin:10px;


}
</style>
<div>
<div>1</div>
<div>2</div>
<div>3</div>
</div>

将第二个设置为绝对定位后,它脱离了文档流可以定位到页面的任何地方,在标准文档流中的原有位置会空出来,所以第三个会排到第一个下面。

Description

第二个相对于它的父元素向右偏移100,向下偏移30。

想要快速入门前端开发吗?推荐一个前端开发基础课程,这个老师讲的特别好,零基础学习无压力,知识点结合代码,边学边练,可以免费试看试学,还有各种辅助工具和资料,非常适合新手!

点这里前往学习哦!

五、z-index(层级顺序的改变)

层叠顺序决定了元素之间的堆叠顺序。z-index 属性用于设置元素的层叠顺序。具有较高 z-index 值的元素会覆盖具有较低 z-index 值的元素。

注意:

  • 默认值是0
  • 数值越大层越靠上
  • 不带单位
  • 没有最大值和最小值
  • 可以给负数

代码示例:

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
div:nth-of-type(1){
width: 300px;
height: 300px;
background-color: skyblue;
position: absolute;
}
div:nth-of-type(2){
width: 200px;
height: 200px;
background-color: pink;
position: absolute;
}
div:nth-of-type(3){
width: 100px;
height: 100px;
background-color: yellowgreen;
position: absolute;
z-index: -1;
}
</style>
</head>
<body>

<div></div>
<div></div>
<div></div>


</body>
</html>

运行结果:

Description

可以看到,最后一个div依然存在,但是看不见了,原因就是我们改变了z-index属性值。

Description

以上就是CSS定位属性的介绍了,通过这些定位属性,可以灵活地控制网页中元素的位置和堆叠顺序。

在实际应用中,CSS定位属性的使用需要考虑到整体布局和用户体验。合理运用这些定位技巧,可以让你的网页不仅美观,而且易于使用和维护。记住,好的设计总是细节和功能的完美结合。

收起阅读 »

解锁 JSON.stringify() 5 个鲜为人知的功能

web
作为一名前端开发者,你可能熟悉JSON.stringify()方法,通常用于调试。但是很多只是简单使用一下接下来,让我们深入了解其实用性。 考虑一个对象如果想把她转成字符串打印出来: const obj = { name: 'San Shang Y...
继续阅读 »

u=142040142,590010156&fm=253&fmt=auto&app=138&f=JPEG.webp


作为一名前端开发者,你可能熟悉JSON.stringify()方法,通常用于调试。但是很多只是简单使用一下接下来,让我们深入了解其实用性。


考虑一个对象如果想把她转成字符串打印出来:


const obj = {  
name: 'San Shang You Ya',
age: 18
};
console.log(obj.toString()); // Result: [object Object]

如果你想这样打印你所看到的只能是 [object Object]


我们可以借助JSON.stringify()方法


const obj = {  
name: 'San Shang You Ya',
age: 18
};
console.log(JSON.stringify(obj));
// Result: {"name":"San Shang You Ya","age":18}

大多数开发者直接使用 JSON.stringify(),但我即将揭示一些隐藏的技巧。


1. 第二个参数(Array)


-JSON.stringify() 接受第二个参数,它是一个你想在控制台中显示的对象的键的数组。例如:


const obj = {  
name: 'San Shang You Ya',
age: 18
};
console.log(JSON.stringify(obj, ['name']));
// Result: {"name": "San Shang You Ya"}

这样而不是将整个 JSON 对象混乱地显示在控制台中,可以通过将所需的键作为数组传递给第二个参数来选择性地打印。


2. 第二个参数(Function)



  • 第二个参数也可以是一个函数,根据函数内的逻辑输出键值对。

  • 如果返回 undefined,则该键值对将不会被打印出来。


const obj = {  
name: 'San Shang You Ya',
age: 18
};

console.log(JSON.stringify(obj, (key, value) => (key === "age" ? value : undefined)));
// Result: {"age": 18}

3. 第三个参数作为数字



  • 第三个参数控制最终字符串中的间距。如果是一个数字,字符串化的每个级别将相应缩进。


const obj = {  
name: 'San Shang You Ya',
age: 18
};
console.log(JSON.stringify(obj, null, 2));

image.png


4. 第三个参数作为字符串


如果第三个参数是一个字符串,它将替换为空格字符


image.png


5. toJSON 方法


对象可以拥有一个 toJSON 方法。
JSON.stringify() 返回该方法的结果,并对其进行字符串化,而不是转换整个对象。


const superhero= {  
firstName: "San Shang",
lastName: "You Ya",
age: 21,
toJSON() {
return {
fullName: `${this.firstName} + ${this.lastName}`
};
}
};

console.log(JSON.stringify(superhero));
// Result: "{ "fullName" : "San Shang You Ya"}"

作者:StriveToY
来源:juejin.cn/post/7329164061390798883
收起阅读 »

Redis 大 key 问题一文通

1. 背景 最近对接了一个卧龙同事的接口,因为接口比较慢,所以打算对第三方接口加个缓存。但是会有大 key 的问题。设计过程中调研了一些解决方案,这里总结下。 关键字:Redis;大Key问题; 2. 大 key 会带来什么问题 我们都知道,redis 是单线...
继续阅读 »

1. 背景


最近对接了一个卧龙同事的接口,因为接口比较慢,所以打算对第三方接口加个缓存。但是会有大 key 的问题。设计过程中调研了一些解决方案,这里总结下。
关键字:Redis;大Key问题;


2. 大 key 会带来什么问题


我们都知道,redis 是单线程架构,日常的读写操作都是由一个线程完成。一旦某一个线程执行了大 key 的读写,就会影响之后所有命令的执行,进而影响 redis 实例甚至整个 redis 集群的稳定。


3. 什么才叫大 key


那么什么才叫大 key?普遍认同的规范是:



  1. value > 10kb,即认定为大 key

  2. 像list,set,hash 等容器类型的 redis key,元素数量 > 5000,即认定为大 key


现在我们知道了大 key 会来带什么问题,也知道了什么样的 key 才算大key。接下来我们看看都有哪些解决方案。


4. 解决方案一:压缩


适用于字符串类型的 redis key。采用压缩算法,将 key 压缩至可接受的范围内。压缩也是有讲究的,首先要选择无损的压缩算法,然后在压缩速率和压缩率之间也要权衡。比较常用的压缩算法/工具如下:



  • google snappy:无损压缩,追求压缩速度而不是压缩率(Compression rate)

  • message pack:无损压缩,仅适用于 json 字符串的压缩,可以得到一个更小的 JSON,官网是:msgpack.org/


5. 解决方案二:value 切片


适用于 list,set,hash 等容器类型的 redis key。规范要求容器的元素数量 < 5000,我们可以在写 redis 的时候做个逻辑,如果超过了 5000 的容器就做切片。


举个例子,现在有一个 list 类型的缓存 ,他包含 12000 个元素。是很典型的大key。
image.png
我们以 5000 为阈值,把 list 切分成三份:user_list_slice_1、user_list_slice_2、user_list_slice_3,另外还需要一个存储切片具体情况的key,所以还需要一个 user_list_ctl。
业务程序后续访问这个缓存的时候,先请求 user_list_ctl,解析出缓存的切分情况,再去请求具体的切片即可。


6. 解决方案三:抛弃大 key(discard)


大多数场景,我们是把 redis 当缓存用,缓存失效了就走数据库查出数据。我们可以设定一个阈值,如果缓存对象特别大的话,我们就抛弃这个key,不缓存,直接走数据库。这样不会影响 redis 正常的运作。


image.png


当然,这是个取巧的方案,灵感是来自线程池的拒绝策略(DiscardPolicy)。采用这个方案得确认直接抛弃不会影响业务,还需要确保不走缓存后的性能业务上能够接受。



7. 俯瞰一下,从架构的角度解决这个问题


千叮咛万嘱咐,大 key 问题造成的线上事故仍然没有断过,这个怎么解决?
我觉得有如下几个思路



  • 完善监控机制,有大 key 出现就及时告警

  • 封禁/限流能力,能够及时封禁大 key 的访问,降低业务影响(保命用)

  • 在服务和 redis 集群之间建设 proxy 层,在 proxy 做大 key 的处理(压缩或者切片处理),让业务开发无需感知大key。


8. 总结


总结一下,解决 redis 的大 key,我们常规有三种解决方案。一是压缩,而是切片,三是直接抛弃不缓存。


作者:小黑233
来源:juejin.cn/post/7261254961923768380
收起阅读 »

拯救强迫症!前端统一代码规范

web
1. 代码格式化 1.1 工具介绍 ESLint 是一款用于查找并报告代码中问题的工具 Stylelint 是一个强大的现代 CSS 检测器 Prettier 是一款强大的代码格式化工具,支持多种语言 lint-staged 是一个在 git 暂存文件上运...
继续阅读 »

1. 代码格式化


1.1 工具介绍


Untitled 1.png



  • ESLint 是一款用于查找并报告代码中问题的工具

  • Stylelint 是一个强大的现代 CSS 检测器

  • Prettier 是一款强大的代码格式化工具,支持多种语言

  • lint-staged 是一个在 git 暂存文件上运行 linters 的工具

  • husky 是 Git Hook 工具,可以设置在 git 各个阶段触发设定的命令


1.2 配置说明


1.2.1 ESLint 配置


在项目根目录下增加 .eslintrc.js 文件进行配置,配置项详见官方文档,以下为参考配置:


npm i -D eslint eslint-plugin-vue eslint-plugin-import eslint-import-resolver-typescript @typescript-eslint/parser @typescript-eslint/eslint-plugin prettier eslint-plugin-prettier eslint-config-prettier

module.exports = {
// 此项是用来告诉 eslint 找当前配置文件不能往父级查找
root: true,
// 全局环境
env: {
browser: true,
node: true,
},
// 指定如何解析语法,eslint-plugin-vue 插件依赖vue-eslint-parser解析器
parser: "vue-eslint-parser",
// 优先级低于parse的语法解析配置
parserOptions: {
// 指定ESlint的解析器
parser: "@typescript-eslint/parser",
// 允许使用ES语法
ecmaVersion: 2020,
// 允许使用import
sourceType: "module",
// 允许解析JSX
ecmaFeatures: {
jsx: true,
},
},
extends: [
"eslint:recommended", // 引入 ESLint的核心功能并且报告一些常见的共同错误
"plugin:import/recommended", // import/export语法的校验
"plugin:import/typescript", // import/export 语法的校验(支持 TS)
// 'plugin:vue/essential' // vue2 版本使用
// 'plugin:vue/recommended', // vue2 版本使用
"plugin:vue/vue3-essential", // vue3 版本使用
"plugin:vue/vue3-recommended", // vue3 版本使用
"plugin:@typescript-eslint/recommended",
"prettier", // prettier 要放在最后!
],
plugins: ["prettier"],
rules: {
"prettier/prettier": "error",
"@typescript-eslint/explicit-module-boundary-types": "off",
"no-undef": "off",
// 更多规则详见:http://eslint.cn/docs/rules/
},
settings: {
"import/parsers": {
"@typescript-eslint/parser": [".ts", ".tsx"],
},
"import/resolver": {
typescript: {
alwaysTryTypes: true,
},
},
},
};

💡当 ESLint 同时使用 prettier 的时候,prettier 和 ESLint 可能存在一些规则冲突,我们需要借助 eslint-plugin-prettiereslint-config-prettier 进行解决,在安装完依赖包后在 .eslintrc.js 配置文件中进行添加如下内容:


module.exports = {
"extends": [
// 其他扩展内容...
"prettier" // prettier 要放在最后!
],
"plugins": ["prettier"],
"rules": {
"prettier/prettier": "error"
},
}

1.2.2 StyleLint 配置


在项目根目录下增加 .stylelintrc.js 文件进行配置,配置项详见官方文档,以下为参考配置:


npm i -D stylelint stylelint-config-standard stylelint-order stylelint-config-rational-order prettier stylelint-prettier stylelint-config-prettier postcss-html postcss-less stylelint-config-recommended-vue

module.exports = {
extends: [
'stylelint-config-standard', // 官方 stylelint 规则
'stylelint-config-rational-order', // 属性排列顺序规则
/*
* 通过安装 stylelint-prettier,设置 'stylelint-prettier/recommended',其包含了三个操作
plugins: ['.'],
extends: ['stylelint-config-prettier'], // 需要安装 stylelint-config-prettier
rules: {'prettier/prettier': true},
*/

'stylelint-prettier/recommended',
],
plugins: [
'stylelint-order', // CSS 属性排序
],
rules: {
// 更多规则详见:https://stylelint.io/user-guide/rules/list
},
};

💡当 StyleLint 同时使用 prettier 的时候,prettier 和 StyleLint 可能存在一些规则冲突,我们需要借助 stylelint-prettierstylelint-config-prettier 进行解决,在安装完依赖包后在 .stylelintrc.js 配置文件中进行添加如下内容:


module.exports = {
extends: [
/*
* 通过安装 stylelint-prettier,设置 'stylelint-prettier/recommended',其包含了三个操作
plugins: ['.'],
extends: ['stylelint-config-prettier'], // 需要安装 stylelint-config-prettier
rules: {'prettier/prettier': true},
*/

'stylelint-prettier/recommended',
],
};

1.2.3 Prettier 配置


在项目根目录下增加 .prettierrc.js 文件进行配置,配置项详见官方文档,以下为参考配置:


npm i -D prettier

module.exports = {
// 更多规则详见:https://prettier.io/docs/en/options.html
printWidth: 120, // 单行长度
tabWidth: 2, // 缩进长度
useTabs: false, // 使用空格代替tab缩进
semi: true, // 句末使用分号
singleQuote: true, // 使用单引号
bracketSpacing: true, // 在对象前后添加空格-eg: { foo: bar }
quoteProps: 'consistent', // 对象的key添加引号方式
trailingComma: 'all', // 多行时尽可能打印尾随逗号
jsxBracketSameLine: true, // 多属性html标签的‘>’折行放置
arrowParens: 'always', // 单参数箭头函数参数周围使用圆括号-eg: (x) => x
jsxSingleQuote: true, // jsx中使用单引号
proseWrap: 'preserve',
htmlWhitespaceSensitivity: 'ignore', // 对HTML全局空白不敏感
};

1.2.4 husky 和 lint-staged 配置


step1. 初始化 husky


npx husky-init && npm install

step2. 在 .husky/pre-commit 文件中进行修改(注意区别 husky@7 与 husky@4 的设置方式)


#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

npx lint-staged

step3. 安装 lint-statged 并在 package.json 中进行设置


npm i -D lint-staged

{
"lint-staged": {
"*.{js,ts,vue}": [
"eslint --fix",
"prettier --write",
"git add"
],
"*.{css,less,vue}": [
"stylelint --fix",
"prettier --write",
"git add"
]
}
}

1.3 使用参考



  1. 代码提交:根据上述工具配置,代码在提交仓库时进行检查和格式化,实现代码风格统一;

  2. 本地保存:在 VSCode 中进行配置,使得代码在保存的时候即按照相应的规则进行格式化;



如何在 VSCode 中进行配置使得能够自动按照相应的规则进行格式化呢?接下来进入第二章《编辑器配置》。



2. 编辑器配置


2.1 VSCode 配置


2.1.1 配置内容


Untitled.png


所有 VSCode 配置自定义的内容(包括插件部分)都在 setting.json 文件中,以下为参考配置:


{
"editor.tabSize": 2,
"window.zoomLevel": 0,
"editor.fontSize": 14,
"editor.formatOnPaste": true,
"editor.formatOnSave": true,
"editor.multiCursorModifier": "ctrlCmd",
"editor.snippetSuggestions": "top",
"eslint.codeAction.showDocumentation": {
"enable": true
},
"eslint.run": "onSave",
"eslint.format.enable": true,
"eslint.options": {
"extensions": [
".js",
".vue",
".ts",
".tsx"
]
},
"eslint.validate": [
"javascript",
"typescript",
"vue"
],
"stylelint.validate": [
"css",
"less",
"postcss",
"scss",
"sass",
"vue",
],
// 保存时按照哪个规则进行格式化
"editor.codeActionsOnSave": {
"source.fixAll": true,
"source.fixAll.eslint": true
},
"files.autoSave": "afterDelay", // 文件自动保存
"files.autoSaveDelay": 2000, // 2s 后文件自动保存
}

参考资料: VS Code 使用指南VS Code 中 Vetur 与 prettier、ESLint 联合使用


2.1.1 插件推荐



  1. Eslint: Integrates ESLint JavaScript int0 VS Code

  2. stylelint: Official Stylelint extension for Visual Studio Code

  3. Prettier: Code formatter using prettier

  4. EditorConfig: EditorConfig Support for Visual Studio Code

  5. Npm Intellisense: VS Code plugin that autocompletes npm modules in import statements

  6. Path Intellisense: VS Code plugin that autocompletes filenames

  7. Auto Rename Tag: Auto rename paired HTML/XML tag

  8. Auto Close Tag: Automatically add HTML/XML close tag

  9. Code Spelling Checker: Spelling checker for source code

  10. Volar / Vetur: Language support for Vue 3 / Vue tooling for VS Code


2.2 EditorConfig 配置


EditorConfig 的优先级高于编辑器自身的配置,因此可用于维护不同开发人员、不同编辑器的编码风格。在项目根目录下增加 .editorconfig 文件进行配置即可,以下为参考配置:


# Editor configuration, see http://editorconfig.org

# 表示是最顶层的 EditorConfig 配置文件
root = true

[*] # 表示所有文件适用
charset = utf-8 # 设置文件字符集为 utf-8
indent_style = space # 缩进风格(tab | space)
indent_size = 2 # 缩进大小
end_of_line = lf # 控制换行类型(lf | cr | crlf)
insert_final_newline = true # 始终在文件末尾插入一个新行
trim_trailing_whitespace = true # 去除行尾的任意空白字符

3. Commit Message 格式化


3.1 工具介绍


Conventional Commits 约定式提交规范是一种用于给提交信息增加人机可读含义的规范,可以通过以下工具来进行检查、统一和格式化:



  • commitlint:检查您的提交消息是否符合 conventional commit format

  • commitizen:帮助撰写规范 commit message 的工具

  • cz-customizable:自定义配置 commitizen 工具的终端操作

  • commitlint-config-cz:合并 cz-customizable 的配置和 commitlint 的配置


3.2 配置说明


3.2.1 格式化配置


step1. 安装 commitizen 和 cz-customizable


npm install -D commitizen cz-customizable

step2. 在 package.json 添加以下内容:


{
"config": {
"commitizen": {
"path": "node_modules/cz-customizable"
}
}
}

step3. 在项目根目录下增加 .cz-config.js 文件进行配置即可,以下为参考配置:


module.exports = {
// type 类型
types: [
{ value: 'feat', name: 'feat: 新增功能' },
{ value: 'fix', name: 'fix: 修复 bug' },
{ value: 'docs', name: 'docs: 文档变更' },
{ value: 'style', name: 'style: 代码格式改变(不影响功能)' },
{ value: 'refactor', name: 'refactor: 代码重构(不包括 bug 修复、功能新增)' },
{ value: 'perf', name: 'perf: 性能优化' },
{ value: 'test', name: 'test: 添加或修改测试用例' },
{ value: 'build', name: 'build: 构建流程或外部依赖变更' },
{ value: 'ci', name: 'ci: 修改 CI 配置或脚本' },
{ value: 'chore', name: 'chore: 对构建过程或辅助工具和库的更改(不影响源文件、测试用例)' },
{ value: 'revert', name: 'revert: 回滚 commit' },
],
// scope 类型
scopes: [
['components', '组件相关'],
['hooks', 'hook 相关'],
['utils', 'utils 相关'],
['styles', '样式相关'],
['deps', '项目依赖'],
// 如果选择 custom,后面会让你再输入一个自定义的 scope。也可以不设置此项,把后面的 allowCustomScopes 设置为 true
['custom', '以上都不是,我要自定义'],
].map(([value, description]) => {
return {
value,
name: `${value.padEnd(30)} (${description})`,
};
}),
// 交互提示信息
messages: {
type: '确保本次提交遵循 Angular 规范!\n选择你要提交的类型:',
scope: '选择一个 scope(可选):\n',
customScope: '请输入自定义的 scope:\n', // 选择 scope: custom 时会出现的提示
subject: '填写简短精炼的变更描述:\n',
body: '填写更加详细的变更描述(可选)。使用 "|" 换行:\n',
breaking: '列举非兼容性重大的变更(可选):\n',
footer: '列举出所有变更的 ISSUES CLOSED(可选):\n',
confirmCommit: '是否确认提交?',
},
// 设置只有 type 选择了 feat 或 fix,才询问 breaking message
allowBreakingChanges: ['feat', 'fix'],
// subject 限制长度
subjectLimit: 100,
};

step4. 新增 husky 配置,使得提交 commit message 时触发 commitizen,快捷命令如下:


npx husky add .husky/prepare-commit-msg "exec < /dev/tty && node_modules/.bin/cz --hook || true"

注意,commitizen 如果是全局安装,则使用下面的快捷命令:


npx husky add .husky/prepare-commit-msg "exec < /dev/tty && git cz --hook || true"

3.2.2 格式检查配置


step1. 安装 commitlint 和 commitlint-config-cz ****依赖:


npm install --save-dev @commitlint/{config-conventional,cli} commitlint-config-cz

step2. 在项目根目录下增加 commitlint.config.js 文件进行配置即可,以下为配置内容:


module.exports = {
extends: ['@commitlint/config-conventional', 'cz'],
rules: {},
};

step3. 新增 husky 配置,使得提交 commit message 时触发 commitlint 检验,配置内容如下:


npx husky add .husky/commit-msg 'npx --no -- commitlint --edit "$1"'

3.3 使用参考


在命令行输入 git commit,然后根据命令行提示输入相应的内容,完成之后则会自动生成符合规范的 commit message,从而实现提交信息的统一。


4. 代码规范参考


4.1 JS/TS 规范


社区类代码风格:



工具类代码风格:



4.2 CSS 规范


社区类代码风格:



工具类代码风格



4.3 VUE 规范


推荐阅读 Vue 官方风格指南:Vue2 版本Vue3 版本,其他可参考 eslint-plugin-vue


作者:植物系青年
来源:juejin.cn/post/7278575483909799947
收起阅读 »

APP端上通用安全体系建设

背景:APP端上安全在谈什么 APP的每个业务场景都有其既定的运行模式,若被人为破坏就可认为是不安全的。举个栗子,比如秒杀场景:大量用户在特定时间点,通过点击抢购来秒杀优惠商品,从而营造一种紧迫而有噱头的营销场景,但如果能通过非法手段自动抢购、甚至提前开始刷接...
继续阅读 »

背景:APP端上安全在谈什么


APP的每个业务场景都有其既定的运行模式,若被人为破坏就可认为是不安全的。举个栗子,比如秒杀场景:大量用户在特定时间点,通过点击抢购来秒杀优惠商品,从而营造一种紧迫而有噱头的营销场景,但如果能通过非法手段自动抢购、甚至提前开始刷接口抢购,那就彻底破坏了业务的玩法,这就是一种不安全的运行模式。再比如常用的用户拉新场景:新客获取成本高达200左右,所有产品的拉新投入都蛮高,如何获得真正的新用户而不是羊毛党也是拉新必须处理的事,一般而言,新设备+新账户是新用户的基本条件,但新账户的成本其实不高,大部分是要靠新设备来识别的,但如果能通过非法手段不断模拟新设备,那拉新投入获取的可能大部分都是无效的羊毛党,这也可看做是一种不安全的运行场景,甚至还有二次篡改,构建马甲APP等各种场景。而APP端上安全要做的就是甄别并防范这种异常场景的发生,简而言之它就是:一种确保官方APP既定业务模型中运行的能力。


APP端上安全体系应该具备哪些能力


一个安全体系要具备哪些能力呢,简单说可分两块:甄别与防御。即一:甄别运行环境是否安全的能力,二:针对不同的场景作出不同的防御的能力,场景千变万化,所以防御手段也没有一剑破万法的能力,基本都要根据具体的风险场景,产出不同的应对方案,但是整体涉及的流程基本一致,如下:


image.png


要做的事情就是围绕四个阶段构建不同的能力。先看第一阶段:风险场景的假设,预判有哪些风险场景,从AppStore或者官方应用市场下载,安装、正常使用,这是理想中的运行模式,但不安分的用户千千万,异常场景也多变,不存在一剑破万法的说法,这里人为做了一些场景归类:


image.png


从实践来看,安全模型必须覆盖的场景包含上述四类,即



一: 运行的APP非官方应用



这种情况一般是非法用户为了谋求特定的权益,对原装APP进行魔改二次打包后发布,这对于一些广告类、离线付费类APP是一种毁灭性的打击,最常见的就是一些APP会在闪屏页面投放广告,从而获取收益,但一旦这个业务逻辑被篡改绕过,那么广告收益直接归零;还有些情况APP被碰瓷,魔改一些功能,跳转到非官方设定的网站,比如在比较火的APP添加劫持逻辑,跳转到一些黄赌网站,这给官方APP带来的负面影响是难易估量的,如果不能自证清白,甚至还会面临法律上的追责。



二:业务的运转模型被篡改



简单的讲就是没有按照产品的既定玩法进行,就像文章开头的秒杀场景【电商平台的茅台抢购】,如果能通过API直接组单,那APP里点击的那批用户无论如何也抢不过插件;再比如有些签到的场景是为了促活,如果通过自动签到工具自动打卡,那促活的目的也无法达到,这也是要重点防控的一个场景。



三:运行的设备非目标设备



这种场景主要影响一些拉新、优惠权益等,甚至直接影响整体运营策略,简单举个例子,拉新场景中会投放大量的新人免单、直减券,基本等于免费领取权益,比如我们都经历过的打车软件、外卖软件、新电商产品上线等。全国有无数的专业羊毛党专注于这一场景,他们掌握大量手机号,可以注册大量新账号,同时能通过虚拟机、应用多开、复刻设备等手段无限冒充新设备,如果不加反制,优惠策略一上线,各种券基本全被这批人掠夺,被这种手段折磨致死的产品数不胜数,也几乎是每个电商平台的噩梦。



四:一些核心逻辑的泄漏



这不是端上特有的风险,比如代码泄漏这种,各方都有这种风险,只不过端上风险更高,因为APP是要上架发出去的,虽然经历过各种混淆与保护,但是用户最终还是能拿到可执行的APP包的,里面各种的核心的逻辑、秘钥都有潜在泄漏的风险,一旦泄漏,也是一种毁灭性的打击,比如某些音视频软件特有滤镜、转场、特效,这些算法是一个产品的核心,一旦破解,产品优势不再,如果防止逻辑代码的泄漏与破解也是安全必须关注的点。


覆盖上述场景的意思是要提供甄别的能力,针对不同的场景,抽象特征值并搜集上报,建立特定的模型,推导C端操作环境是否安全,之后上线不同的应对策略:比如直接退出应用、标记为风险用户等。


建设方案


甄别与防御是体系的核心,建设方案主要是围绕这两个主题展开,虽说名称是“端上安全体系”,但只依靠端自己是无法解决所有问题的,也无法将价值发挥到最大,仍需多端系统配合来完成整个体系的搭建,分工的基本原则是:端上侧重特征信息的搜集,云端负责整体策略的执行,根据上述场景,搭建示意如下:


image.png


按层次跟功能大致分四块,端、网关、业务后台、数据风控中心:



  • 端是信息的来源,负责信息采集上报,是安全体系建设的基石,所以端上采集信息的真实性、为完整性至关重要,同时端上也可以执行部分风险低,但收益高的拦截略,比如对于一些已经侦测到的马甲包,在上报用户信息之后,可以选择Crash,对于一些机器点击类的签到、秒杀场景,可以主动拦截请求,降低带宽压力。

  • 网关是第二层,一般处理一些具体规则类的拦截与信息采集,比如有些简单的规则检验,Header里是否携带必备的校验字段,如多开标识、模拟器标志等,如果携带则可以在这一层直接拦截,并沉淀到数据中心,既保证了信息的采集,又能减轻业务后台的压力,尤其对于一些秒杀类的场景非常有效。

  • 业务后台跟数据平台可以一起看做第三层,负责更复杂的模型建设跟业务落地,比如在什么样的节点,才有什么样的策略,比如在组单的时候,业务后台可根据风控侧的判断决定用户的优惠力度等


针对各个具体的场景会有具体的建设方案,



如何应对非官方APP:APP包识别



非法人员总是出于自己的权益来破解官方APP,定制一些逻辑再二次打包发布,比如对一些付费类APP,含广告类工具APP等,通过破解代码,并二次打包后就可以官方造成冲击,甚至是毁灭性的打击,对于这种场景如何甄别,又如何处理呢?拿Android为例,检测手段有签名校验、文件校验、包完整性校验等,一旦检测到风险就可以做出响应处理,在处理方式上也需要根据不同产品不同场景随机变动,比如工具类APP就Crash阻断,而对于一些有用户体系类的APP则可以先回传用户信息,用作用户画像,再做响应的处理,而处理的手段也可以根据风险等级的不同再做定制,甄别的技术手段可能是死的,但获取的收益一定要灵活。



如何应对非理想设备:设备识别与设备指纹



非理想设备最经典的就是文章开头的场景,拉新拉来一堆老羊毛党,说到底其实就是对于用户设备的定位追踪能力不足,对于这种场景的如何应对?这里单独说说设备指纹,设备指纹主要解决的如何定位一台设备的问题,在理想情况下,一台设备只有一个身份信息,它不因APP卸载、升级、HOOK伪装所改变,这在现在的互联产品生态中是非常困难的,困难主要来源于两个方面:一技术上的、一是法规上的,从技术上来讲,以Android端为例,它是一个开源的系统,每一行代码都不可信,任何通过官方API拿到的信息都可以被HOOK篡改,而指纹很大程度依赖API获得的设备特征信息,如果这些信息都不可信,那指纹的可信程度也会降低。另一方面,从法规上来讲,现在注重保护个人隐私,不可以随意获取用户信息,这一点有利于用户【包括羊毛党】,但是对于运营方却是不利的,信息越少越难定位到,因此,在隐私合规的前提下,仍需要多维度的获取更多的用户信息,从更多的维度定位到该用户。


具体如何执行?以Android为例,定位一台设备的信息有MAC、IMEI、IMSI、序列号、AndroidID、IP+UA、OAID、各种设备型号等,虽然信息很多,但单独任何一条的可信度都不高,比如之前的某盾、某盟都曾用MAC地址作为指纹,甚至有些产品直接用IMEI作为指纹,但网上利用XPOSED来篡改的插件比比皆是,通过官方API获取的分分钟被破解,但是可以对多种信息进行整合生成一个唯一可信的ID,这种方式获取的ID的稳定性要比单一的稳定性要高,原理示意如下:


image.png


简单来说:只有篡改了全部的设备特征信息,才会导致设备指纹更新,这会大幅提高设备逃逸的难度。设备指纹的另一个难题是如何识别虚拟设备,这里特指模拟器,每个新开的模拟器都可以看做是新设备,如果不能识别,同样无法解决设备跟踪的问题,尤其对于国内的Android生态来讲,问题更加严重,各种游戏厂商都有对应的手游模拟器,不仅支持多开,还原生支持篡改各种设备特征信息,可以算得上助纣为虐,在模拟器甄别与防控的层面能做的有如下几种:



  • 通过特征信息甄别【容易绕过】

  • 通过CPU架构甄别【ARM与SimpleX86】

  • 限定APP的运行平台


这里简单介绍下通过CPU架构甄别方式,就目前的硬件市场,几乎99.9%以上的手机设备都是基于ARM处理,而模拟器大部分是面向x86平台设计的,采用的是simplex86架构,两者采用的不一样缓存机制,ARM采用的哈弗架构将指令存储跟数据存储分开,分为I-Cache(指令缓存)与D-Cahce(数据缓存),CPU无法直接修改I-Cache【同步延迟导致不一致】,但Simpled X86架构的模拟器只有一块缓存,这一点导致两者在运行Self-Modifying Code【自修改代码】时会有不同的表现,可以借助这个特性进行甄别,示意如下


image.png


至此,设备的定位与跟踪能力基本已经具备,在用户在领券的节点,就可以从更多维度判断他当前的设备是否有资格享受这个权益,保障业务按既定模型运转。


image.png


当然还有更多的场景,比如应用多开、应用分身等,都要具体问题具体分析,但思路一致:特征搜集、甄别、防控,因为所有的不轨行为一定有迹可循,



如何应对非设定业务场景:场景识别与校验



每种业务都有其既定的运行模式,只有照章办事,运营才能获取最大的收益,这里特指一些可以通过自己的参与获得收益的场景,比如秒杀、签到、预约摇号等。一般而言,在这类场景下,破坏者可钻的空子有两个方向,一个是便利:通过插件自动预约,免得用户自己操作,适合摇号、签到类【签到领积分】;一个是速度,通过插件直接API请求,抢跑下单,获得收益,适合限时秒杀类的场景【各大平台强茅台】。以秒杀为例,通过营造紧迫而又刺激的氛围可以让活动更有意思,但如果能直接刷接口/或者通过插件抢跑,那就会破坏其公平性,影响用户的参与感,造成资产及口碑受损,这类场景如何应对?其实要做的事情分两块:一 识别请求是从APP发出来 , 二 识别是真实用户操作的,这两快一般会整体考虑,非APP端的请求往往伴随着非用户触发,多归结于脚本,所以识别“人”与识别“场景”殊途同归,具体有哪些手段可以用呢?



  • 扩展核心API接口的能力,承载更多逻辑

  • 通过埋点、用户操作轨迹分析识别用户

  • 启用端上特有能力校验,如短信验证码、行程码分析


如何拓展API接口的能力?比如预约接口其基础能力就是预约,如不特殊处理,PC上就完全可以复制APP端发出的请求,进而通过脚本预约,如要加以限制就必须拓展端上API能力,让其携带更多端上独有特征,同时服务端可以完成校验,形成一个闭环,比较容易理解的就是让APP端与服务端协商一套加解密通信协议,并假定协议无法破解,避免接口直刷,从而确保请求是从APP发出的,即使不是从APP发出的,也能被甄别出来,进而提高APP与服务端通信阶段的安全性。当然,无法破解只是理论上,实际上只要舍得投入成本,暴力破解并不是问题,这种就需要通过更多元的手段,不断更新迭代,持续做攻防,例如,为了保证加密算法的保密性,可以将其用c实现,并通混淆、加固、防探测等手段保证这个策略的正确执行;暴力堆积加密的类型、节点,提升秘钥的更新频率也是一种应对手段,而且,惩罚手段上也可以多元,同直接拦截相比,隐秘的搜捕,诱捕也是一种灵活收益的手段。


其次,基于埋点、用户操作行为的大数据分析是另一种更高级的防御手段,对于识别用户操作场景更加科学,正常的用户轨迹与插件类的访问轨迹会有很大的差异,直刷的目标明确,主攻几个关键接口,但正常用户访问会有一系列的曝光、点击等行为,并且每次的点击也会有各种零零散散的活体特征可以采集,比如点击的点位置、数量、力度、频率等,这些维度为用户识别提供了更广的操作空间。基于以上几点的模型示意如下:


image.png


最后一点,启用端上特有的校验能力,这个已经是最终的防御手段,在实在没有办法的情况才会采用的,因为这种手段很影响用户体验,由于采用的是端上特有的能力,比如短信验证码,必须真机才能收到,这就从根本上避免了插件类的直刷,所以可靠性确实所有手段中最高的,但体验差,成本高,所以算是最后一道防线。



如何应对核心逻辑的泄漏



这一块主要关注的是APP端的一些核心逻辑的破解或泄密,可以分两个方向,对外与对内,对外主要是APP包的逆向与破解,不法人员从发布上架的APP包中获取核心业务实现或其他敏感信息;而对内主要指工程安全,核心源码或秘钥的泄漏、误改等。


相应的防范策略也是分两块,对外的线上防破解可以从以下几点入手:



  • 利用代码混淆防APP逆向,一般而言官方会提供相应的能力,也可借助三方加固来提高混淆的力度

  • 核心源码、秘钥下沉,采用更难破解的方式实现,同时增加防外部调用的防范策略,比如Android采用C+混淆来处理

  • 为线上APP添加防调试与HOOK的能力,防止动态调试探测,

  • 添加防止代理与中间人劫持的能力,例如SSLPING等技术,避免被抓包探测

  • 从二次打包入手,添加签名、完整性检测的能力,防止被探测、篡改


而对内主要从工程安全角度推进,主要是做好代码的权责管理



  • 采用组件化开发模式,不同等级的基础能力、业务、核心逻辑做好隔离

  • 仓库单独部署,同时做好权责划分,代码、文档做好权限隔离

  • 加强秘钥、KEY的管控,开发与生产环境严格隔离


上述手段基本涵盖大部可预见的风险场景,即使未覆盖,也大概有类似的手段作为参考,无非就是抽象、搜集、判断、处理。


线上执行方案


最后一步是上线执行,上述的手段多种多样,但相互之间并非孤立运行,彼此可以相互穿插,灵活配合,不存在特定的章法,全看使用方的意图,如何探测,探测之后如何处理,是全杀还是放一部分,都看操刀者自己的运作,以应用多开场景为例,除了利用多开基础的多开检测手段,还可以配合设备指纹做更多的事情,有时虽然没有检测到多开,但是基于设备指纹的补刀,也能定位到问题设备,而在最后一步惩治处理中,不同处理手段也会获得不一样的收益:


类型处理方式最终收益优缺点
被动拦截端上部署检测规则,检测到风险,100%在端上拦截处理【如Crash】效果明显,但易被发现,徒增防御成本
被动捕获检测到风险,在端上不处理,只上报,后端隐形标记或拦截不易被发现,但长期运行收益比较局限
主动诱捕人为制造有迹可循的漏洞,捕获后在端上不拦截或部分拦截,并上报,后端隐形标记不易被发现,虚虚实实,操作空间更大,收益更大

理论上讲,APP技术层面不存在100%有效的安防策略,虚虚实实才是王道,敬畏,才是最有效的防御手段


总结与展望


目前国内APP的生态环境并不健康,甚至可以说野蛮,随着隐私策略收紧,APP所能获取的信息越来越少,安防也越来越难做,反之,刷子却越活越滋润,技术所面临的的挑战也更加棘手,安防注定是一个长期攻防的领域。最后,技术不能解决所有问题,最终还是要依赖法律的健全与全民意识提升。


作者:看书的小蜗牛
来源:juejin.cn/post/7350354672861052980
收起阅读 »

反转反转再反转,揭秘人心深处的“恶意”

“……真是过分,我每天可是难过得要死。有个爱管闲事的邻居,每天都来找我,我没办法,只好去上学,都快给他烦死了。” “老师和学生的关系建立在一种错觉上。老师错以为自己可以教学生什么,而学生错以为能从老师那里学到什么。重要的是,维持这种错觉对双方而言都是件幸福的...
继续阅读 »

“……真是过分,我每天可是难过得要死。有个爱管闲事的邻居,每天都来找我,我没办法,只好去上学,都快给他烦死了。”


“老师和学生的关系建立在一种错觉上。老师错以为自己可以教学生什么,而学生错以为能从老师那里学到什么。重要的是,维持这种错觉对双方而言都是件幸福的事。因为若看清了真相,反而一点好处都没有。我们在做的事,不过是教育的扮家家酒而已。”


是什么样的经历让他说出这样的话呢?我不明白。


大家好,我是杰哥


昨天晚上凌晨三点,我终于第三次翻完了《恶意》。这本书,真的让我欲罢不能!每次阅读都像是一次心灵的冒险,让我惊叹不已。


作者东野圭吾巧妙地运用手记的方式,将故事的发展娓娓道来。那些看似简单平实的文字,却隐藏着令人震撼的真相。他对复杂人性抽丝剥茧的深刻描画,简直让我眼花缭乱,哑口无言。


故事围绕着一起谋杀案展开:畅销书作家在出国的前一晚于家中被杀,凶手很快便落网了。但别以为这只是个简单的“谁是凶手”的故事,其实更多的是”我为什么要杀他“的故事。


凶手对作案动机语焉不详,倒是引起了著名侦探”加贺“的兴趣,他凭借自己一贯对于人性觉察比较敏锐的”直觉“,以及自己曾经作为老师所亲身经历过的“校园暴力”事件,对”作案动机“展开了缜密的分析与调查。经过层层曲折的调查,终于将真实的动机呈现在我们的面前(此处故意不剧透,以免影响了大家的阅读体验)。我只能说,得知真相的你,一定会被震撼到,从而陷入深思。


我是一个悬疑推理类书籍的书迷,看过很多悬疑推理类的书籍,而这本则是一本题材与故事都很新颖且富有创造力,结局也会很让人意外的其中之一。


读这本书的过程中,我就像是坐过山车一样,情绪起伏不定。每当我以为抓到了真相的尾巴,作者就会巧妙地用一个新的情节把我甩回去。反转再反转,直到最后,我才恍然大悟:哦,原来真相是这样的!大概真正优秀的悬疑推理类小说的作家的仅有的几部作品中,才可以与读者的互动达到这样的效果吧。


这本书不仅仅是悬疑,它更深刻地探讨了人性。恶意,这个看似抽象的概念,在书中被具象化,变得触手可及。它让我不禁深思,人心的黑暗面到底能有多深,我们又该如何面对和控制自己内心的恶意。


东野圭吾的笔下,每个人物都有自己的秘密,每个线索都可能是个陷阱。他的作品,让人读起来往往感觉惊险又刺激,恨不得一口气读完,甚至连旁边的手机也被冷落了。


总之,如果你喜欢心理悬疑,喜欢深度剖析人性的作品,那《恶意》绝对不容错过。它会让你在紧张刺激的阅读中,体验到心灵的震撼!


作者:舒米勒
来源:mdnice.com/writing/924e74e14de748d3b72493d7224aba0d
收起阅读 »

跳舞的人

跳舞的人 从我们大学的老校区南院门口进入,迎面便能看到庄严的主楼。 河北大学主楼 主楼后面是一段有花草树木的路,旁边是多功能馆。在主楼和多功能馆中间,有一块空旷的场地,人们清晨、傍晚常常在那里嬉戏玩耍。 大学一年级下半年、...
继续阅读 »

跳舞的人


从我们大学的老校区南院门口进入,迎面便能看到庄严的主楼。



河北大学主楼

河北大学主楼


主楼后面是一段有花草树木的路,旁边是多功能馆。在主楼和多功能馆中间,有一块空旷的场地,人们清晨、傍晚常常在那里嬉戏玩耍。


大学一年级下半年、大学二年纪一整学年我都是在主楼内的我们学院的机房里值班的。所谓值班就是,我晚上需要在机房对面的小屋里睡觉,白天需要管理好机房的日常使用工作。


因为早晨经常需要帮在机房上课的老师学生开门,所以在主楼住的时候,我会醒的比较早。


醒来后,我会先去食堂吃饭,然后在主楼后面散散步,转一圈。


有天在散步的过程中,听到了主楼前的方向有音乐的声音,我对这音乐比较好奇,便循着声音来到了主楼和多功能馆旁的空旷场地。这里放着一个看起来又大又重的音响,音乐便是从这个音箱里传出来的。在大音箱旁边,一个男生在跟随着音乐跳着舞。


早晨的时间是比较充分的,我便在这站了一会,发现跳舞的男生,每个舞蹈都会重复很多遍。我并不懂跳舞,只是能够感觉出来每一遍舞蹈,都是那么认真、那么投入,我猜他很热爱跳舞吧。


后面每每有时间,我都会走到那块空旷的地方,也经常能够看到他在这里跳舞。我想,这块空旷的场地就是他的舞台吧。


有天早晨,雨下得很大,我很早便被雨声吵醒了,我突然想看看「那个跳舞的人,下雨天会不会来练习跳舞呢?」


我便穿好衣服、鞋子,来到主楼不会被雨淋到的台子上。不一会,我便看到一个打着大伞的男生,拉着音箱走了过来。随着男生越来越近,我看清了,就是他——那个跳舞的人


他把音箱拉离地面,一步一步地走上台阶。我心里想,这么大的音箱,拉到台子上,不重吗?


他慢慢近了,来到了我旁边,我和他互相说了声你好,他便打开了音箱,放起了音乐,跳起了舞来,依旧那么认真、那么投入。


外面很冷,看到他来了,心里的疑问算是解开了,我便向主楼里面走去,回到了机房。


在回机房的路上,我便想,因为热爱,音箱便不会重了吧。


作者:随机的未知
来源:mdnice.com/writing/6882be2b53a04567a77d7be826eef49c
收起阅读 »

工作思考|研发环境好好的,怎么上线就出问题了?

场景再现 那是一个夜黑风高的晚上,某个版本迭代经过了完备的测试,正准备上线。研发同事A开完了上线评审后,信心满满地对运维同事B说:“开冲!” 几分钟后,同事B发了条消息过来,看着抖动的头像,同事A心想:小B效率真高啊,这么快!点开消息一看【启动报错了,你看一下...
继续阅读 »

场景再现


那是一个夜黑风高的晚上,某个版本迭代经过了完备的测试,正准备上线。研发同事A开完了上线评审后,信心满满地对运维同事B说:“开冲!”


几分钟后,同事B发了条消息过来,看着抖动的头像,同事A心想:小B效率真高啊,这么快!点开消息一看【启动报错了,你看一下】。


什么?启动还能报错,不可能啊,我研测环境都好好的。


小A火急火忙地连上堡垒机,看了下日志,报错信息大致为 【表tb_xxx没有找到】。


“怎么可能,我用了伟大的flyway,怎么可能会没有表呢?”小A如是说道。



提到flyway,这里简单介绍一下。Flyway是一款开源的数据库版本管理工具,可以实现管理并跟踪数据库变更,支持数据库版本自动升级,而且不需要复杂的配置,能够帮助团队更加方便、合理的管理数据库变更。只需要引入相应依赖,添加配置,再在resource目录下创建db/migration/xxxx.sql文件,在sql文件中写入用到的建表语句,插入语句即可。



不管怎么说,代码是不会骗人的。先找下是哪里出了问题!


小A很快就定位到了代码位置,是一个用于缓存的HashMap,这操作也没什么问题,相信大家都这么用过,对于一些一次查找,到处使用,还亘古不变的表信息,可以先查一次,把它用Map缓存起来,以便后续使用。


但是研发同事C把这段代码放在了afterPropertiesSet()​方法内部,没错,就是那个InitializingBean​接口的方法。看到这里,相信各位熟练背诵Bean生命周期的Java Boy已经明白了!查询数据库的操作在Bean还没有创建完成的时候就进行了!而此时,flyway脚本还没有执行,自然就找不到对应的表信息了。


那怎么办呢?


解决方法


解决方法很简单,sql执行的时候找不到表,那就让它在表创建完之后再执行!


1.CommandLineRunner接口


一个方法就是我们常用的CommandLineRunner​接口,重写run()​方法,把缓存逻辑移到run()​方法中。原因是run()方法的执行时机是在SpringBoot应用程序启动之后,此时flyway已经执行完毕,表结构已经存在,就没问题了!


2.@DependsOn注解


通过代码分析,flyway的加载是由flywayInitializer​这个Bean负责的。所以只需要我们的Bean在它之后加载就行了,这就用上了@DependsOn​注解。



@DependsOn注解可以定义在类和方法上,意思是我这个Bean要依赖于另一个Bean,也就是说被依赖的组件会比该组件先加载注册到IOC容器中。



也就是在我们的Bean上加上这么个注解@DependsOn("flywayInitializer")


总结


此次线上问题复习了Bean的生命周期,复习了InitializingBeanCommandLineRunner​两个接口,复习了@DependsOn​注解。


作者:钱思惘
来源:juejin.cn/post/7349750846898913332
收起阅读 »

时间格式化,显示昨天、今天

web
时间格式化的需求: 今天的数据显示“时分”,HH:mm 10:00 昨天的数据显示“昨天 时分”, 昨天 10:00 今年的数据,显示 “月日 时分”, 05-01 10:00 不是今年的数据,显示“年月日 时分”, 2022-05-01 10:00 代...
继续阅读 »

时间格式化的需求:



  • 今天的数据显示“时分”,HH:mm 10:00

  • 昨天的数据显示“昨天 时分”, 昨天 10:00

  • 今年的数据,显示 “月日 时分”, 05-01 10:00

  • 不是今年的数据,显示“年月日 时分”, 2022-05-01 10:00


代码展示



在 ios中 用new Date("2022-05-01 10:00").getTime()会有兼容性问题,跟日期格式的连字符有关系,这里使用moment插件



const moment = require("moment");

// 判断日期是不是今天、昨天, 0:今天 -1:昨天 1-明天
// str: 2023-02-07 14:09:27.0
export function isWhichDay(str) {
const date = new Date();
const that = moment(moment(str).format("YYYY-MM-DD")).valueOf();
const today = moment(moment(date).format("YYYY-MM-DD")).valueOf();
const timeStampDiff = that - today;
const obj = {
"-86400000": "-1",
0: "0",
86400000: "1",
};
return obj[timeStampDiff] || null;
}

// 判断是不是当年
export function isCurYear(str) {
return moment().format("YYYY") === moment(str).format("YYYY");
}

/**
* 格式化时间 YYYY-MM-DD HH:mm:ss
* 1、当天时间显示如 10:00
* 2、昨天显示如 昨天10:00
* 3、昨天之前且当年的,显示如,05-01 10:00
* 4、昨天之前且跨年的,显示如, 2022-05-01 10:00
*
@param {string} time "2022-05-01 10:00:01.0"
*
@returns {string}
*/

export function formatTime(time) {
const t = isWhichDay(time);
if (t === "0") {
return moment(time).format("HH:mm");
} else if (t === "-1") {
return `昨天 ${moment(time).format("HH:mm")}`;
} else if (
isCurYear(time) &&
moment(time).valueOf() < moment(new Date()).valueOf()
) {
return moment(time).format("MM-DD HH:mm");
} else {
return moment(time).format("YYYY-MM-DD HH:mm");
}
}



作者:甜点cc
来源:juejin.cn/post/7226300253921558583
收起阅读 »

环信WEB端单群聊 UIKit 快速集成与消息发送指南

写在前面:千呼万唤始出来,环信web端终于出uikit了,环信单群聊 UIKit 是基于环信即时通讯云 IM SDK 开发的一款即时通讯 React UI 组件库。该组件库提供了聊天相关的组件,包括会话列表、聊天界面、联系人列表和群组设置等组件,组件内部集成了...
继续阅读 »

写在前面:

千呼万唤始出来,环信web端终于出uikit了,环信单群聊 UIKit 是基于环信即时通讯云 IM SDK 开发的一款即时通讯 React UI 组件库。该组件库提供了聊天相关的组件,包括会话列表、聊天界面、联系人列表和群组设置等组件,组件内部集成了 IM SDK,可以帮助开发者不关心内部实现和数据管理就能根据实际业务需求快速搭建包含 UI 界面的即时通讯应用。现在就让我们一起探索如何集成吧!本文介绍如何快速实现在单聊会话中发送消息


准备工作:

  1. React 环境:需要 React 16.8.0 或以上版本;React DOM 16.8.0 或以上版本。

  2. 即时通讯 IM 项目:已在环信即时通讯云控制台创建了有效的环信即时通讯 IM 开发者账号,并获取了 App Key

  3. 环信用户:在环信控制台创建 IM 用户,并获取用户 ID 和密码或 token。


  4. 好友关系:双方需要先添加好友才可以聊天







集成uikit:

准备工作完成就开始集成!在此先奉上uikit源码

第一步:创建一个uikit项目

# 安装 CLI 工具。
npm install create-react-app
# 构建一个 my-app 的项目。
npx create-react-app my-app
cd my-app

第二步:安装 easemob-chat-uikit

cd my-app
  • 使用 npm 安装 easemob-chat-uikit 包
npm install easemob-chat-uikit --save
  • 使用 yarn 安装 easemob-chat-uikit 包
yarn add easemob-chat-uikit

第三步:引入uikit组件

在你的 React 项目中,引入 UIKit 提供的组件和样式:

// 导入组件
import {
UIKitProvider,
Chat,
ConversationList,
// ...
} from "easemob-chat-uikit";

// 导入样式
import "easemob-chat-uikit/style.css";

第四步:初始化配置

easemob-chat-uikit 提供 UIKitProvider 组件管理数据。UIKitProvider 不渲染任何 UI, 只用于为其他组件提供全局的 context,自动监听 SDK 事件, 在组件树中向下传递数据来驱动组件更新。单群聊 UIKit 中其他组件必须用 UIKitProvider 包裹。

import "./App.css";
import { UIKitProvider} from "easemob-chat-uikit";
import "easemob-chat-uikit/style.css";
function App() {
return (
<div>
<UIKitProvider
initConfig={{
appKey: "your app key", // 你的 app key
userId: "userId", // 用户 ID
password: "password", // 如果使用密码登录,传入密码。
translationTargetLanguage: "zh-Hans", // 翻译功能的目标语言
useUserInfo: true, // 是否使用用户属性功能展示头像昵称(UIKit 内部会获取用户属性,需要用户自己设置)
}}
local={{
fallbackLng: "zh",
lng: "zh",
resources: {
zh: {
translation: {
hello: "欢迎使用",
conversationTitle: "会话列表",
deleteCvs: "删除会话",
//...
},
},
},
}}
>
</UIKitProvider>
</div>
);
}

export default App;


第五步:引入组件

根据自己的项目引入所需组件,组件文档,本文只介绍如何快速实现在单聊会话中发送消息,为了方便快速体验,一定要确保准备工作的第四条双方已经互为好友

import "./App.css";
import { UIKitProvider} from "easemob-chat-uikit";
import "easemob-chat-uikit/style.css";
function App() {
return (
<div>
<UIKitProvider
initConfig={{
appKey: "your app key", // 你的 app key
userId: "userId", // 用户 ID
password: "password", // 如果使用密码登录,传入密码。
translationTargetLanguage: "zh-Hans", // 翻译功能的目标语言
useUserInfo: true, // 是否使用用户属性功能展示头像昵称(UIKit 内部会获取用户属性,需要用户自己设置)
}}
local={{
fallbackLng: "zh",
lng: "zh",
resources: {
zh: {
translation: {
hello: "欢迎使用",
conversationTitle: "会话列表",
deleteCvs: "删除会话",
//...
},
},
},
}}
>
<div style={{ display: "flex" }}>
<div style={{ width: "40%", height: "100%" }}>
<ContactList
onItemClick={(data) => {
rootStore.conversationStore.addConversation({
chatType: "singleChat",
conversationId: data.id,
lastMessage: {},
unreadCount: "",
});
}}
/>
</div>//联系人组件,点击某个好友通过‘rootStore.conversationStore.addConversation’创建会话
<div style={{ width: "30%", height: "100%" }}>
<ConversationList />//会话列表组件
</div>
<div style={{ width: "30%", height: "100%" }}>
<Chat />//聊天消息组件
</div>
</div>
</UIKitProvider>
</div>
);
}

export default App;


第六步:运行并测试

1、运行项目

npm run start

2、点击好友并发送一条消息


总结:

通过以上步骤,你已经成功集成了环信单聊 UIKit 并实现了基本的即时通讯功能,接下来继续根据 UIKit 提供的组件和 API 文档进行进一步开发吧


收起阅读 »

【CSS浮动属性】别再纠结布局了!一文带你玩转CSS Float属性

在网页设计的世界里,CSS浮动属性(float)就像一把双刃剑。它能够让元素脱离文档流,实现灵活的布局,但如果处理不当,也可能引发一系列布局问题。今天,我们就来深入探讨这把“剑”的正确使用方法,让你的页面布局既美观又稳定。一、什么是CSS浮动属性浮动属性是CS...
继续阅读 »

在网页设计的世界里,CSS浮动属性(float)就像一把双刃剑。它能够让元素脱离文档流,实现灵活的布局,但如果处理不当,也可能引发一系列布局问题。

今天,我们就来深入探讨这把“剑”的正确使用方法,让你的页面布局既美观又稳定。

一、什么是CSS浮动属性

浮动属性是CSS中的一个定位属性,它允许元素脱离文档流,并向左或向右移动,直到它的外边缘碰到包含框或者另一个浮动元素的边缘。简单来说,它就像是让元素“漂浮”在页面上,不受常规排列规则的限制。

在网站开发中需要一行排列多个元素,使用浮动可以方便实现。下面是使用浮动排列多个元素。

Description

下面我们来了解一下浮动属性的基本使用语法和常用的应用场景都有哪些。

1.1 浮动属性的语法

selector {
float: 值;
}

其中,选择器是你想要应用浮动属性的元素的选择器,值可以是以下之一:

  • none:这是默认值,元素不会浮动,即保持在标准文档流中的位置。

  • left:元素将向左浮动,它会尽量向左移动,直到它的外边缘碰到包含框或另一个浮动框的边框为止。

  • right:元素将向右浮动,它会尽量向右移动,直到它的外边缘碰到包含框或另一个浮动框的边框为止。

1.2 浮动的应用场景

CSS浮动属性的应用场景主要包括以下几点:

布局定位:
浮动可以用于创建复杂的页面布局,例如将块级元素放置在一行内,或者创建多列布局。

文本环绕图片:
这是浮动最常见的应用之一,通过将图片设置为浮动,可以使文本自动环绕在图片周围,从而实现类似印刷布局中的文本环绕效果。

清除元素间缝隙:
浮动元素会紧挨着排列,没有间隙,这可以用来清除列表或图片间的空格,使得元素紧密排列。

创建下拉菜单
浮动还常用于创建下拉菜单或弹出式菜单,通过将菜单项设置为浮动,可以实现菜单的显示和隐藏。

实现侧边栏:
在网页设计中,浮动可以用来创建固定在一侧的侧边栏,而主要内容则围绕侧边栏流动。

创建瀑布流布局:
在响应式设计中,浮动可以用来实现瀑布流布局,这种布局可以根据浏览器窗口的大小自动调整列数和列宽。

1.3 盒子的排列规则

在使用浮动属性后盒子是如何排列的呢?

Description

  • 左浮动的盒子向上向左排列
  • 右浮动的盒子向上向右排列
  • 浮动盒子的顶边不得高于上一个盒子的顶边
  • 若剩余空间无法放下浮动的盒子,则该盒子向下移动,直到具备足够的空间能容纳盒子,然后再向左或向右移动

二、浮动的核心特点

下面将通过这个示例来给大家讲解浮动的特点:

<div>
<div>正常元素</div>
<div>
浮动元素
</div>
<div>我是浮动元素后面的第一个同级正常元素</div>
<div>我是浮动元素后面的第二个同级正常元素</div>
</div>
.container{
width: 200px;
height: 200px;
background-color: red;
}
.box1{
background-color: green;
}
.box2{
background-color: brown;
}
.box3{
background-color: pink;
}


.float{
float:left;


background-color: yellow;
}

Description

2.1 包裹性

具有“包裹性”的元素当其未主动设置宽度时,其宽度右内部元素决定。且其宽度最大不会超过其包含块的宽度。

设置了float属性(不为none)的元素都会具有包裹性。

在上面的例子中float元素不设置宽度,其宽度也不会超过container元素的宽度。

2.2 块状化并格式化上下文

设置了float属性(不为none)的元素,无论该元素原本是什么元素类型,其display属性的计算值都会自动变成"block"或’table(针对inline-table元素)'。并且浮动元素会生成一个BFC(块级格式化上下文)。

所以永远不需要对设置了float属性(不为none)的元素再设置"display:block"属性或者vertical-align属性!这都是多余和无效的。

2.3 脱离标准流

设置了float属性(不为none)的元素,都会脱离标准流。标准流一般是针对块级或行级元素(不包括行内块)。

通俗一点解释是,浮动元素A会“漂浮”在标准流上面,此时其原始占用的标准流空间由同级的后续第一个标准流兄弟元素B顶替(但是元素B中的文本内容会记住浮动元素A的位置,并在排布时避开它,由此形成文本环绕效果)。所以会出现B的部分内容会被飘起来的A所遮挡的现象。

有人可能会问,上面的例子中好像没发现类似"A遮挡B"的现象啊?

其实并不是,具体解释如下:
我们将box2元素(即浮动元素后续的第一个同级元素)的文本内容减少一些,使其不换行并不占满一行方便解释效果。

Description

图片这时候发现box2是和container元素宽度200是一致的,而不是自身文本的宽度。由于浮动元素的脱离文档流,.box2会忽略浮动元素的原空间(即当其不存在),由因为普通div不设置宽度默认会是父元素宽度。

所以这里box2和其父元素container宽度一致。但又因为浮动元素会使box2的文本环绕,导致box2的文本重新布局排版,“移动”到了紧跟浮动元素的右边界的地方。所以此时可以看作box2被浮动元素遮挡的那一部分实际是空背景。

2.4 高度坍塌

当你给浮动元素设置了具体宽高度,并增加box2元素的文本内容,也许这种脱离文档流现象更明显,如下示例:

Description

浮动元素脱离标准流的特性很容易带来的一大问题就是——父元素的高度塌陷

我们将html内容精简,只保留浮动元素和box2元素:

<div>
<div>
浮动元素
</div>
<div>我是浮动元素后面的第一个同级元素</div>


</div>

然后设置浮动元素宽高度,并去掉父元素设置的宽高度(核心点)

.container{
/* width: 200px;*/
/*height: 200px;*/
background-color: red;
}
.float{
float:left;
width: 40px;
height: 40px;
background-color: yellow;
}

Description

此时我们发现:没有设置高度的container元素,其实际高度只由标准文档流的box2元素撑起来了21px,而设置了30px高度的浮动元素由于脱离文档流其高度被忽略了。

这就是浮动经典的“高度塌陷”问题了。

2.4 无margin重叠问题

普通的块级元素之间的margin-top和margin-bottom有时会出现margin合并的现象,而浮动元素由于其自身会变成一个BFC(块级格式化上下文),不会影响外部元素,所以不会出现margin重叠问题。

三、清除浮动

清除浮动并不是去掉浮动,而是解决因为浮动带来的副作用的消极影响,也就是我们上面说的父元素高度塌陷问题。

3.1 clear属性

在此之前,我们需要了解另一个CSS属性,就是float的克星——clear

官方对于clear属性的解释是:元素盒子的边不能和前面的浮动元素相邻。其本质在于让当前元素不和前面的float元素在一行显示。

对此我们可以对于clear的属性值形象地理解为:

  • left:元素左边抗浮动

  • right:元素右边抗浮动

  • both:元素两侧抗浮动

注意:由于clear属性只关注当前元素前面的浮动元素,所以使用clear:left/right都是和clear:both等效的。实际上我们只需要用到clear:both即可。

想要快速入门前端开发吗?推荐一个前端开发基础课程,这个老师讲的特别好,零基础学习无压力,知识点结合代码,边学边练,可以免费试看试学,还有各种辅助工具和资料,非常适合新手!

点这里前往学习哦!


3.2 清除方法

当今流行的浮动布局已不再是最初的文字环绕了,而是通过给每个子元素添加浮动来实现元素横向排列(一行占不下就换行继续)的布局。

注意:这种横向排列布局建议最好让每个子元素的高度一致,否则可能会出现以下图这种高度不齐引起的布局问题:

Description

即使如此,依然需要解决父元素高度塌陷问题,以下分别对几种常见解决方案简单说明下:

  • 让父元素也浮动

没有从根本解决问题,反而生成了新的浮动问题。

  • 给父元素设置高度

此法太过死板,让父元素高度固定死了,无法自适应高度。

  • 给父元素设置overflow:hidden

此法原理在于让父元素成为一个BFC,唯一缺点容易导致溢出内容被隐藏掉,不过这种场景较少,还是可以用此方法的。

  • 伪元素与clear属性配合(推荐)
/*对浮动元素的父元素设置*/
.clear::after{
clear: both;
content:'';
/*clear属性只对块元素有效,而伪元素::afer默认是行级*/
display: block;
}

CSS浮动属性是网页设计师的重要工具,但也需要谨慎使用。通过今天的介绍,希望你能够更加自信地在你的设计中运用这一属性,创造出既美观又稳定的网页布局。

在CSS的世界里,每一个属性都有其独特的魅力和规则。浮动属性作为布局的强大工具,虽然有时会带来挑战,但只要我们理解它的本质,就能将它变为实现创意设计的利器。

收起阅读 »

Redis不再 “开源”

Redis 官方今日宣布修改开源协议 —— 未来所有版本都将使用 “源代码可用” 的许可证 (source-available licenses)。具体来说,Redis 将不再遵循 BSD 3-Clause 开源协议进行分发。从 Redis 7.4 版本开始,...
继续阅读 »

Redis 官方今日宣布修改开源协议 —— 未来所有版本都将使用 “源代码可用” 的许可证 (source-available licenses)。


具体来说,Redis 将不再遵循 BSD 3-Clause 开源协议进行分发。从 Redis 7.4 版本开始,Redis 采用 SSPLv1 和 RSALv2 双重许可证。Redis 源代码将通过 Redis 社区版免费提供给开发者、客户和合作伙伴。

SSPL:Server Side Public License

RSAL:Redis Source Available License Redis 产品家族的具体许可证如下:


根据新许可证的条款,托管 Redis 产品的云服务提供商将不再允许免费使用 Redis 的源代码。例如,云服务提供商只有在与 Redis(Redis 代码的维护者)达成许可条款后,才能向用户交付 Redis 7.4。

Redis 官方表示:

实际上,Redis 开发者社区不会发生任何变化,他们将继续拥有双重许可证下的宽松许可。同时,Redis 负责的所有 Redis 客户端库将保持采用开源许可证。 Redis 将继续支持其庞大的合作伙伴生态系统(包括托管服务提供商和系统集成商),并独家访问 Redis 通过其合作伙伴计划开发和提供的所有未来版本、更新和功能。 现有 Redis Enterprise 客户没有变化。 总的来说,对于使用 Redis 开源版本和新版本的 Redis 的最终用户(使用双重许可证进行内部或个人使用),没有任何变化。

对于使用 Redis 构建客户端库或其他集成的集成合作伙伴,同样没有任何变化。 Redis 对这次修改开源协议的举措十分坦诚,他们承认 Redis 不再是 OSI 定义下的“开源”项目。但他们仍是开源理念的支持者,并会继续维护开源项目。


原文:https://mp.weixin.qq.com/s/9_-w6lF7ffiu49WbEORPtQ

收起阅读 »

正则表达式太难写?试试这个可视化工具

在工作中有没有觉得写正则表达式很难,我就一直很头疼。今天我们就介绍一个开源项目,它可以用可视化的方式查看、编辑和测试正则表达式,大大的提升效率,它就是:regex-vis regex-vis是什么 regex-vis是一个辅助学习、编写和验证正则的工具,你输入...
继续阅读 »

在工作中有没有觉得写正则表达式很难,我就一直很头疼。今天我们就介绍一个开源项目,它可以用可视化的方式查看、编辑和测试正则表达式,大大的提升效率,它就是:regex-vis


regex-vis是什么


regex-vis是一个辅助学习、编写和验证正则的工具,你输入一个正则表达式后,会生成它的可视化图形。然后可以点选或框选图形中的单个或多个节点,再在右侧操作面板对其进行操作,具体操作取决于节点的类型,比如在其右侧插入空节点、为节点编组、为节点增加量词等。



安装regex-vis


首先regex-vis提供了一个在线环境,可以直接到regex-vis.com/ 去试用,这是最简单的方式。



当然,作为一个开源项目,另外一种方式就是自己运行啦。按以下步骤:



  • 首先下载代码到本地。

  • 安装依赖:pnpm install

  • 安装完成后运行服务:pnpm start



启动完成后到3000端口访问即可。


这里可能会遇到一些小问题,比如SSL的问题,稍微修改一些运行命令的配置即可解决。


使用 regex-vis


首先我准备一个例子的正则表达式,验证身-份-证的正则:


^[1-9]\d{5}(18|19|([23]\d))\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$

可视化


直接把正则表达式贴进去就能看到可视化的效果了。



编辑


在右侧是正则表达式的编辑区,可以在这里修改、编辑正则的内容。



首先是一些图例,点击图形中想要编辑的部分,然后点击中间的编辑就可以进入到编辑页面了。



测试


修改完了正则的内容,想要验证一下写的对不对,那就到测试里去试一试吧。通过的会显示绿色,失败的则显示为红色。



示例


项目还自带了几个示例,如果你 刚一进来,不知道用什么来试用,可以直接打开示例来看看。



设置


本身是个小工具,这里有2个可用的设置,一个是切换语言,可以切成中文显示,另一个就是明/暗显示模式的转换。



总结


作为一个小工具还是挺不错的,对于像我这样不熟练正则的人有所帮助,一步步的编辑可以渐进式的编辑。


当然现在写正则最好的方式是让AI帮忙写,所以我建议可以AI帮忙写,然后通过这个工具来检查一下,通过可是的方式检查和AI的沟通有没有错误。


另外测试正则的功能里,不能显示出事那部分 正则出错略有可惜,如果能增强就更好用了。


项目信息



作者:IT咖啡馆
来源:juejin.cn/post/7350683679297290294
收起阅读 »

趣解适配器模式之《买了苹果笔记本的尴尬》

〇、小故事 小王考上了理想的大学,为了更好的迎接大学生活,他决定买一台苹果的笔记本电脑犒赏自己。 电脑很快买好了,用起来也非常的流畅,但是,当他想要插U盘传资料的时候,尴尬的事情来了,这台电脑两侧的插口非常少,只有1个耳机插孔和2个雷电插孔,根本没有USB插...
继续阅读 »

〇、小故事


小王考上了理想的大学,为了更好的迎接大学生活,他决定买一台苹果的笔记本电脑犒赏自己。



电脑很快买好了,用起来也非常的流畅,但是,当他想要插U盘传资料的时候,尴尬的事情来了,这台电脑两侧的插口非常少,只有1个耳机插孔2个雷电插孔根本没有USB插口!这咋办呀?



他赶快咨询了他的哥哥,他哥哥告诉他,去买一个扩展坞就可以了,然后他上网一看,原来买一个扩展坞之后,无论是U盘还是连接显示器的HDMI都可以连接啦!!他开心极了,本来要遗憾退掉这台心爱的苹果笔记本电脑,这回也不用退啦!



以上这个小故事,相信很多使用过苹果笔记本的同学们都会遇到,大多也都会购买这种扩展坞,那么,这种扩展坞其实就是适配器模式的一个具体实现例子了。那么,言归正传,我们来正式了解一下这个设计模式——适配器模式


一、模式定义


适配器模式定义:



该模式将一个类的接口,转换成客户期望的另一个接口。适配器模式让原本接口不兼容的类可以合作无间。



为了进一步加深该模式的理解,我们再举一个研发过程中会遇到的例子:



此时我们维护了一个员工管理系统,然后接入我们系统的第三方系统,我们都要求对方遵守我们的接口规范去开发,比如:提供方法名为queryAllUser()的方法等等。但是,这次接入的系统已经有类似功能了,他们不希望因为两个系统的接入而重新开发新的接口,那么这对这种情况,我们就可以采用适配器模式,将接口做中间层的适配转换。



如图下图所示:



二、模式类图


通过上面的介绍,相信大家对适配器模式也有了一定的了解了。那么,下面我们就来看一下如果要实现适配器模式,我们的类图应该是怎么样的。


首先,我们要说明两个重要的概念:AdapterAdaptee,其含义分别是适配器待适配的类。我们就是通过实现Target接口创建Adapter类,然后在具体的方法内部来通过调用Adaptee方法来实现具体的业务逻辑。具体类图如下所示:



三、代码实现


首先创建目标类接口——Target


public interface Target {
void prepare();
void execute();
}

实现Target接口,创建具体实现类——NormalTarget


public class NormalTarget implements Target {
public void prepare() {
System.out.println("NormalTarget prepare()");
}
public void execute() {
System.out.println("NormalTarget execute()");
}
}

创建待适配的类Adaptee,用于后续适配器对其进行适配工作:


public class Adaptee {
public void prepare1() {
System.out.println("Adaptee prepare1()");
}
public void prepare2() {
System.out.println("Adaptee prepare2()");
}
public void prepare3() {
System.out.println("Adaptee prepare3()");
}
public void doingSomething() {
System.out.println("Adaptee doingSomething()");
}
}

创建适配器Adapter,由于要适配目标对象Target,所以需要实现Target接口:


public class Adapter implements Target {
// 待适配的类
private Adaptee adaptee;

public Adapter(Adaptee adaptee) {
this.adaptee = adaptee;
}

public void prepare() {
adaptee.prepare1();
adaptee.prepare2();
adaptee.prepare3();
}

public void execute() {
adaptee.doingSomething();
}
}

创建客户端Client,用于操作Target目标对象执行某些业务逻辑:


public class Client {
Target target;
public void work() {
target.prepare();
target.execute();
}
public void setTarget(Target target) {
this.target = target;
}
}

创建测试类AdapterTest,使得Client操作NormalTarget和Adaptee:


public class AdapterTest {
public static void main(String[] args) {
Client client = new Client();

System.out.println("------------NormalTarget------------");
client.setTarget(new NormalTarget());
client.work();

System.out.println("------------Adaptee------------");
client.setTarget(new Adapter(new Adaptee())); // 适配器转换
client.work();
}
}

通过输出结果我们可以看到,适配器运行正常:


------------NormalTarget------------
NormalTarget prepare()
NormalTarget execute()
------------Adaptee------------
Adaptee prepare1()
Adaptee prepare2()
Adaptee prepare3()
Adaptee doingSomething()

今天的文章内容就这些了:



写作不易,笔者几个小时甚至数天完成的一篇文章,只愿换来您几秒钟的 点赞 & 分享



更多技术干货,欢迎大家关注公众号“爪哇缪斯” ~ \(^o^)/ ~ 「干货分享,每天更新」


作者:爪哇缪斯
来源:juejin.cn/post/7273125596951298060
收起阅读 »

我不敢把水烧的太开

我不敢把水烧开,那样家里会有两个沸物。我也不敢把房间打扫的很干净,我怕太干净,垃圾只剩下我。我常坐在窗口看着窗外发呆,这悲伤来的没有油头。床上有两个枕头,一个是我的,另一个也是我的,因为我裂开了。我也经常熬夜,因为天冷了,时间就不说了。白天面对的是生活,夜晚面...
继续阅读 »

我不敢把水烧开,那样家里会有两个沸物。我也不敢把房间打扫的很干净,我怕太干净,垃圾只剩下我。我常坐在窗口看着窗外发呆,这悲伤来的没有油头。床上有两个枕头,一个是我的,另一个也是我的,因为我裂开了。我也经常熬夜,因为天冷了,时间就不说了。白天面对的是生活,夜晚面对的是灵魂,一半的时间来生存,剩下的时间拿来滋养灵魂!我熬的不是夜,是我短暂的自由。青春献给了梦想,梦想败给了现实。小时候无忧无虑,倒头就睡,长大后忙于生计,失眠怎么常态。背井离乡的颠沛流离,不过是为了碎银几两。习惯了一个人的独处,也慢慢接受了自己的平庸。我们不是故事里的主角,也没有主角光环!我们只是茫茫人海中的渺小一个。不曾坐过飞机,也没喝过星巴克,更没有用过奢侈品。小时候的自由人,长大后的笼中鸟。以前叫醒你的是父母,现在叫醒你的是生活。我们变得好像是机器人,每天起床的意义就是重复的工作。 我似乎感受到那袭来的风划过,渴望度图欣赏,但更吹醒了我的意识,要继续生存下去,夜深人静是自由醒来清晨是生活,朋友人间自由身,却非人间自由人,看似自由自在,实则身不由己。


我不敢轻易点燃热情,以免世界再添两个燃烧的灵魂。我也不敢让心扉过于透彻明亮,只怕纯净得只剩下自我孤独。我常常倚在窗前凝望星空,那份寂寥悄无声息地蔓延。床榻上有两个梦乡,一个属于清醒时的我,另一个属于深夜里破碎的我。我时常与黑夜为伴,仿佛只有此刻,才握住了片刻的宁静。白昼应对的是繁杂尘世,夜晚则是对内心世界的对话,一半的生命为了生存而奋斗,另一半则用来沉淀和丰盈心灵。我熬的不只是夜,更是那一份难能可贵的自我空间。青春曾向理想倾注所有,而理想最终却在现实中折戟沉沙。儿时懵懂无知,酣然入梦,成人后疲于奔命,反倒是失眠成了常态。远离故土、四海为家,无非是为了那些散落各处的铜板。渐渐习惯独自行走,也逐渐接纳了自己的平凡无奇。我们并非小说里的主人公,身上也没有主角光环庇佑,我们只是芸芸众生中的一员。未曾体验云端飞翔,亦未尝品鉴星巴克的香醇,更谈不上拥有奢侈品牌的光环。曾经那个随性率真的孩子,如今已变成囚禁在生活牢笼中的飞鸟。过去的黎明由父母唤醒,现在的黎明却是生活的催促。我们如同被设定程序的机器,日复一日只是为了重复的工作奔波。


我恍若感受到那阵疾风吹过,虽有心去追逐欣赏,但它更像是一记警钟,提醒我要坚韧地活下去,深夜的清醒是对自由的领悟,破晓的苏醒则是对生活的担当,身边的朋友皆似世间自由行者,却非人人能真正活得洒脱。表面看似悠然自得,实则都身陷无形的生活枷锁之中。


作者:Young_
来源:mdnice.com/writing/ac9b73a2a7f84f1eb843a6c404a82701
收起阅读 »

深入研究Kotlin运行时的泛型

深入研究Kotlin运行时的泛型 通过前面的学习,对Kotlin的泛型已经有了比较全面的了解了,泛型的目的是让通用的代码更加的类型安全。现在我们离写出类型安全的泛型代码还差最后一块拼图,那就是泛型的类型擦除,今天就来深入地学习一下运行时的泛型,彻底的弄懂类型...
继续阅读 »

深入研究Kotlin运行时的泛型


通过前面的学习,对Kotlin的泛型已经有了比较全面的了解了,泛型的目的是让通用的代码更加的类型安全。现在我们离写出类型安全的泛型代码还差最后一块拼图,那就是泛型的类型擦除,今天就来深入地学习一下运行时的泛型,彻底的弄懂类型擦除的前因后果,并学会如何在运行时做类型检查和类型转换,以期完成拼图掌握泛型,写出类型安全的通用代码。





关于泛型话题的一系列文章:



泛型类型擦除(Type erasure)


泛型的类型安全性(包括类型检查type check,和类型转换type casting)都是由编译器在编译时做的,为了保持在JVM上的兼容性,编译器在保障完类型安全性后会对泛型类型进行擦除(Type erasure)。在运行时泛型类型的实例并不包含其类型信息,也就是说它不知道具体的类型参数,比如Foo和Foo都被擦除成了Foo<*>,在虚拟机(JVM)来看,它们的类型是一样的。


因为泛型Foo的类型参数T会被擦除(erased),所以与类型参数相关的类型操作(类型检查is T和类型转换as T)都是不允许的。


可行的类型检查和转换


虽然类型参数会被擦除,但并不是说对泛型完全不能进行类型操作。


星号类型操作


因为所有泛型会被擦除成为星号无界通配Foo<*>,它相当于Foo,是所有Foo泛型的基类,类型参数Any?是根基类,所以可以进行类型检查和类型转换:


if (something is List<*>) {
 something.forEach { println(it) } // 元素被视为Any?类型
}

针对星号通配做类型操作,类型参数会被视为Any?。但其实这种类型操作没有任何意义,毕竟Any是根基类,任何类当成Any都是没有问题的。


完全已知具体的类型参数时


另外一种情况就是,整个方法的上下文中已经完全知道了具体的类型参数时,不涉及泛型类型时,也是可以进行类型操作的,说的比较绕,我们来看一个🌰:


fun handleStrings(list: MutableList<String) {
 if (list is ArrayList) {
  // list is smart-cast to ArrayList
 }
}

这个方法并不涉及泛型类型,已经知道了具体的类型参数是String,所以类型操作也是可行的,因为编译器知道具体的类型,能对类型进行检查 保证是类型安全的。并且因为具体类型参数String可以推断出来,所以是可以省略的。


未检查的转换


当编译器能推断出具体的类型时,进行类型转换就是安全的,这就是被检查的转型(checked cast),如上面的🌰。


如果无法推断出类型时,比如涉及泛型类型T时,因为类型会被擦除,编译器不知道具体的类型,这时as T或者as List都是不安全的,编译器会报错,这就是未检查转型(unchecked cast)。


但如果能确信是类型转换是安全的,可以用注解@Suppress("UNCHECKED_CAST")来忽略。


用关键reified修饰inline泛型函数


要想能够对泛型类型参数T做类型操作,只能是在用关键字reified修饰了的inline泛型函数,在这种函数体内可以对泛型类型参数T做类型操作,🌰如:


inline fun <reified A, reified B> Pair<*, *>.asPairOf(): Pair? {
    if (first !is A || second !is B) return null
    return first as A to second as B
}

val somePair: Pair = "items" to listOf(123)


val stringToSomething = somePair.asPairOf()
val stringToInt = somePair.asPairOfInt>()

需要注意的是关键字reified能够让针对类型参数T的操作得到编译器的检查,保证安全,是允许的。但是对于泛型仍是不允许的,🌰如:


inline fun <reified T> List<*>.asListOfType(): List? =
    if (all { it is T })
        @Suppress("UNCHECKED_CAST")
        this as List else
        null

这个inline泛型函数用关键字reified修饰了,因此针对类型参数T是允许类型检查类型转换,如第2行是允许的。但泛型仍是不合法,如第4行,这时可以用上一小节提到的注解@Suppress("UNCHECKED_CAST")来忽略未检查类型转换。


inline和reified的原理


对于一些泛型工厂方法,就非常适合使用inline和reified,以保证转换为类型参数(因为工厂方法最终肯定要as T)是允许的且是安全的:


inline fun <reified T> logger(): Logger = LoggerFactory.getLogger(T::class.java)

class User {
    private val log = logger()
    // ...
}

关键字reified其实也没有什么神秘的,因为这是inline函数,这种函数是会把函数体嵌入到任何调用它的地方(call site),而每个调用泛型函数的地方必然会有明确的具体类型参数,那么编译器就知道了具体的类型能保证类型安全(checked cast)。上面的工厂方法在调用时就会大概变成酱紫:


class User {
 private val log = LoggerFactory.getLogger(User.class.java)
}

这时其实在函数体内已经知道了具体的类型参数User,编译器能够进行类型检查,所以是安全的。


总结


本文深入的讨论一下运行时泛型的一些特性,泛型类型在运行时会被擦除,无法做泛型相关的类型操作,因为编译器无法保证其类型安全。例外就是在用reified修饰的inline函数中可以对类型参数T做类型操作,但泛型类型(带尖括号的)仍是会被擦除,可以用注解@Suppress("UNCHECKED_CAST")来忽略unchecked cast。


参考资料



作者:稀有猿诉
来源:toughcoder.net/blog/2024/03/16/deep-dive-int0-kotlin-generics-runtime
收起阅读 »

《健听女孩》——一部感人的电影

故事 《健听女孩》的主人公是一个听力正常爱唱歌的女孩鲁比。这里为什么强调听力正常呢?因为她的父母和哥哥都是聋哑人,家里只有她自己一个人具有听力。这个家庭以捕鱼为生,渔船通知响应,渔获交易,与人沟通等事情都是由她进行翻译来进行的。这个家庭很需要她。 她有去伯...
继续阅读 »

故事


《健听女孩》的主人公是一个听力正常爱唱歌的女孩鲁比。这里为什么强调听力正常呢?因为她的父母和哥哥都是聋哑人,家里只有她自己一个人具有听力。这个家庭以捕鱼为生,渔船通知响应,渔获交易,与人沟通等事情都是由她进行翻译来进行的。这个家庭很需要她。


她有去伯克利音乐学院面试的机会,这是她的梦想,本片就是主要讲述了她在梦想与家庭进行抉择的过程和家庭成员的看法、处理方式等。


被“嘲笑”


鲁比出生在聋哑家庭,可想而知,她的家人没有办法教她讲话,她的发声很难听,这遭到了她的同学们的嘲笑、欺凌。她每天凌晨三点便起床与家人一起捕鱼,有时会来不及换衣服,同学也会嫌弃她身上的鱼腥味,进行讥讽。


种种这些,让她幼小的心灵受到了很大打击,所以她害怕面对她们,同时也造成了她的自卑、敏感。


师生情


在报课程的时候,她选择了合唱课,因为她热爱唱歌。但是第一节课,老师让大家开唱的时候,她却逃避得跑出去了。后来和老师讲明因为自己得经历,不敢在同学面前唱歌,害怕被嘲笑。老师给了她鼓励,让她有了自信。


老师发现她的天赋,推荐她去伯克利面试,同时教她唱歌。


在家庭不同意她去伯克利学院时,对她进行惋惜。


友情和爱情


鲁比得闺蜜是她参加合唱团前唯一的好友,闺蜜帮她分担心事,支持她唱歌。


参加合唱团后,她又有了一个男性朋友,是她的二重唱的搭档,当然也发生了一些小插曲。这位朋友家庭也存在一些问题,他的爸妈关系不和,他选择唱歌也并不是自己本意。后来她俩互相了解,互谈心事。 成为了男女朋友。


亲情


这部电影主要还是讲解亲情的。


在得知鲁比想去读大学时,她的妈妈特别反对,她说,“她不能离开我,她是我的宝宝”,爸爸说,“可是她从来没有当过宝宝”。是啊,她从小就帮家里进行翻译,是这个家与外界沟通的桥梁。她这么小就承担了这么多。她的爸爸希望她去,但是这个家庭没有她是没办法运作的。她的哥哥在看到父母把鲁比当成最重要的沟通媒介时,他心事重重,他认为他是哥哥,这些事情他也是能做好的。


虽说她的妈妈开始不支持唱歌,但是在她要进行表演的时候还是送了她一条红色裙子。她俩谈心过程中,妈妈在生孩子时希望鲁比是聋哑人,因为妈妈怕如果是正常儿童的话,自己的聋哑会成为一个“坏妈妈”。 在闺蜜和鲁比哥哥讲述了鲁比唱歌方面的天赋后,他很希望她去参加面试。


她还是没有选择去面试。这是她想成全家庭,因为她知道家庭对她的需要。


一家人一起去看了她的表演,看着台下人的感动,鼓掌,落泪等行为,爸爸心事重重,回到家后爸爸让她为她又唱了一遍,在星空下,他摸着她的振动,感受歌曲。


后来一家人一起去参加了伯克利的面试。这是家庭成员对鲁比的成全。


总结


这是一部很感人的电影,故事的讲述行云流水,每一个转折恰到好处。能够让我们聆听到悦耳的声音。


作者:随机的未知
来源:mdnice.com/writing/4a81248164b4409f8be55c27575fbf3b
收起阅读 »

Android:优雅的处理首页弹框逻辑:责任链模式

背景 随着业务的发展,首页的弹窗越来越多,隐私政策弹窗,广告弹窗,好评弹窗,应用内更新弹窗等等。 并且弹框显示还有要求,比如: 用户本次使用app,只能显示一个弹框,毕竟谁都不愿意打开app就看到一堆弹框 这些弹框有优先级:如隐私政策弹窗优先级肯定比好评弹窗...
继续阅读 »

背景


随着业务的发展,首页的弹窗越来越多,隐私政策弹窗,广告弹窗,好评弹窗,应用内更新弹窗等等。
并且弹框显示还有要求,比如:



  • 用户本次使用app,只能显示一个弹框,毕竟谁都不愿意打开app就看到一堆弹框

  • 这些弹框有优先级:如隐私政策弹窗优先级肯定比好评弹窗高,所以希望优先级高的优先显示

  • 广告弹框只展示一次

  • 等等


如何优雅的处理这个逻辑呢?请出我们的主角:责任链模式。


责任链模式


举个栗子🌰


一位男性在结婚之前有事要和父母请示,结婚之后要请示妻子,老了之后就要和孩子们商量。作为决策者的父母、妻子或孩子,只有两种选择:要不承担起责任来,允许或不允许相应的请求; 要不就让他请示下一个人,下面来看如何通过程序来实现整个流程。


先看一下类图:
未命名文件.png
类图非常简单,IHandler上三个决策对象的接口。


//决策对象的接口
public interface IHandler {
//处理请求
void HandleMessage(IMan man);
}

//决策对象:父母
public class Parent implements IHandler {
@Override
public void HandleMessage(IMan man) {
System.out.println("孩子向父母的请求是:" + man.getRequest());
System.out.println("父母的回答是:同意");
}
}

//决策对象:妻子
public class Wife implements IHandler {
@Override
public void HandleMessage(IMan man) {
System.out.println("丈夫向妻子的请求是:" + man.getRequest());
System.out.println("妻子的回答是:同意");
}
}

//决策对象:孩子
public class Children implements IHandler{
@Override
public void HandleMessage(IMan man) {
System.out.println("父亲向孩子的请求是:" + man.getRequest());
System.out.println("孩子的回答是:同意");
}
}

IMan上男性的接口:


public interface IMan {
int getType(); //获取个人状况
String getRequest(); //获取个人请示(这里就简单的用String)
}

//具体男性对象
public class Man implements IMan {
/**
* 通过一个int类型去描述男性的个人状况
* 0--幼年
* 1--成年
* 2--年迈
*/

private int mType = 0;
//请求
private String mRequest = "";

public Man(int type, String request) {
this.mType = type;
this.mRequest = request;
}

@Override
public int getType() {
return mType;
}

@Override
public String getRequest() {
return mRequest;
}
}

最后我们看下一下场景类:


public class Client {
public static void main(String[] args) {
//随机生成几个man
Random random = new Random();
ArrayList<IMan> manList = new ArrayList<>();
for (int i = 0; i < 5; i++) {
manList.add(new Man(random.nextInt(3), "5块零花钱"));
}
//定义三个请示对象
IHandler parent = new Parent();
IHandler wife = new Wife();
IHandler children = new Children();
//处理请求
for (IMan man: manList) {
switch (man.getType()) {
case 0:
System.out.println("--------孩子向父母发起请求-------");
parent.HandleMessage(man);
break;
case 1:
System.out.println("--------丈夫向妻子发起请求-------");
wife.HandleMessage(man);
break;
case 2:
System.out.println("--------父亲向孩子发起请求-------");
children.HandleMessage(man);
break;
default:
break;
}
}
}
}

首先是通过随机方法产生了5个男性的对象,然后看他们是如何就要5块零花钱这件事去请示的,运行结果如下所示:


--------丈夫向妻子发起请求-------
丈夫向妻子的请求是:5块零花钱
妻子的回答是:同意
--------丈夫向妻子发起请求-------
丈夫向妻子的请求是:5块零花钱
妻子的回答是:同意
--------父亲向孩子发起请求-------
父亲向孩子的请求是:5块零花钱
孩子的回答是:同意
--------孩子向父母发起请求-------
孩子向父母的请求是:5块零花钱
父母的回答是:同意
--------丈夫向妻子发起请求-------
丈夫向妻子的请求是:5块零花钱
妻子的回答是:同意


发没发现上述的代码是不是有点不舒服,有点别扭,有点想重构它的感觉?那就对了!这段代码有以下几个问题:



  • 职责界定不清晰



对孩子提出的请示,应该在父母类中做出决定,父母有责任、有义务处理孩子的请示,



因此Parent类应该是知道孩子的请求自己处理,而不是在Client类中进行组装出来,
也就是说 原本应该是父亲这个类做的事情抛给了其他类进行处理,不应该是这样的。



  • 代码臃肿



我们在Client类中写了if...else的判断条件,而且能随着能处理该类型的请示人员越多,
if...else的判断就越多,想想看,臃肿的条件判断还怎么有可读性?!




  • 耦合过重



这是什么意思呢,我们要根据Man的type来决定使用IHandler的那个实现类来处理请



求。有一个问题是:如果IHandler的实现类继续扩展怎么办?修改Client类?
与开闭原则违背了!【开闭原则:软件实体如类,模块和函数应该对扩展开放,对修改关闭】
http://www.jianshu.com/p/05196fac1…



  • 异常情况欠考虑



丈夫只能向妻子请示吗?丈夫向自己的父母请示了,父母应该做何处理?
我们的程序上可没有体现出来,逻辑失败了!


既然有这么多的问题,那我们要想办法来解决这些问题,我们先来分析一下需求,男性提出一个请示,必然要获得一个答复,甭管是同意还是不同意,总之是要一个答复的,而且这个答复是唯一的,不能说是父母作出一个决断,而妻子也作出了一个决断,也即是请示传递出去,必然有一个唯一的处理人给出唯一的答复,OK,分析完毕,收工,重新设计,我们可以抽象成这样一个结构,男性的请求先发送到父亲,父母一看是自己要处理的,就作出回应处理,如果男性已经结婚了,那就要把这个请求转发到妻子来处理,如果男性已经年迈,那就由孩子来处理这个请求,类似于如图所示的顺序处理图。
未命名文件 (1).png
父母、妻子、孩子每个节点有两个选择:要么承担责任,做出回应;要么把请求转发到后序环节。结构分析得已经很清楚了,那我们看怎么来实现这个功能,类图重新修正,如图 :
未命名文件 (2).png
从类图上看,三个实现类Parent、Wife、Children只要实现构造函数和父类中的抽象方法 response就可以了,具体由谁处理男性提出的请求,都已经转移到了Handler抽象类中,我们 来看Handler怎么实现,


public abstract class Handler {
//处理级别
public static final int PARENT_LEVEL_REQUEST = 0; //父母级别
public static final int WIFE_LEVEL_REQUEST = 1; //妻子级别
public static final int CHILDREN_LEVEL_REQUEST = 2;//孩子级别

private Handler mNextHandler;//下一个责任人

protected abstract int getHandleLevel();//具体责任人的处理级别

protected abstract void response(IMan man);//具体责任人给出的回应

public final void HandleMessage(IMan man) {
if (man.getType() == getHandleLevel()) {
response(man);//当前责任人可以处理
} else {
//当前责任人不能处理,如果有后续处理人,将请求往后传递
if (mNextHandler != null) {
mNextHandler.HandleMessage(man);
} else {
System.out.println("-----没有人可以请示了,不同意该请求-----");
}
}
}

public void setNext(Handler next) {
this.mNextHandler = next;
}
}

再看一下具体责任人的实现:Parent、Wife、Children


public class Parent extends Handler{

@Override
protected int getHandleLevel() {
return Handler.PARENT_LEVEL_REQUEST;
}

@Override
protected void response(IMan man) {
System.out.println("----------孩子向父母提出请示----------");
System.out.println(man.getRequest());
System.out.println("父母的回答是:同意");
}
}

public class Wife extends Handler{
@Override
protected int getHandleLevel() {
return Handler.WIFE_LEVEL_REQUEST;
}

@Override
protected void response(IMan man) {
System.out.println("----------丈夫向妻子提出请示----------");
System.out.println(man.getRequest());
System.out.println("妻子的回答是:同意");
}
}

public class Children extends Handler{
@Override
protected int getHandleLevel() {
return Handler.CHILDREN_LEVEL_REQUEST;
}

@Override
protected void response(IMan man) {
System.out.println("----------父亲向孩子提出请示----------");
System.out.println(man.getRequest());
System.out.println("孩子的回答是:同意");
}
}

那么再看一下场景复现:
在Client中设置请求的传递顺序,先向父母请示,不是父母应该解决的问题,则由父母传递到妻子类解决,若不是妻子类解决的问题则传递到孩子类解决,最终的结果必然有一个返回,其运行结果如下所示。


----------孩子向父母提出请示----------
15块零花钱
父母的回答是:同意
----------丈夫向妻子提出请示----------
15块零花钱
妻子的回答是:同意
----------父亲向孩子提出请示----------
15块零花钱
孩子的回答是:同意
----------丈夫向妻子提出请示----------
15块零花钱
妻子的回答是:同意
----------父亲向孩子提出请示----------
15块零花钱
孩子的回答是:同意

结果也正确,业务调用类Client也不用去做判断到底是需要谁去处理,而且Handler抽象类的子类可以继续增加下去,只需要扩展传递链而已,调用类可以不用了解变化过程,甚至是谁在处理这个请求都不用知道。在这种模式就是责任链模式


定义


Avoid coupling the sender of a request to its receiver by giving more than one object a chance to handle the request.Chain the receiving objects and pass the request along the chain until an object handles it.
(使多个对象都有机会处理请求,从而避免了请求的发送者和接受者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有对象处理它为止。) 责任链模式的重点是在“链”上,由一条链去处理相似的请求在链中决定谁来处理这个请 求,并返回相应的结果,其通用类图如图所示
未命名文件 (3).png
最后总结一下,责任链的模版:
包含四个对象,Handler,Request,Level,Response:


public class Request {
//请求的等级
public Level getRequestLevel(){
return null;
}
}

public class Level {
//请求级别
}


public class Response {
//处理者返回的数据
}

//抽象处理者
public abstract class Handler {
private Handler mNextHandler;

//每个处理者都必须对请求做出处理
public final Response handleMessage(Request request) {
Response response = null;
if (getHandlerLevel().equals(request.getRequestLevel())) {
//是自己处理的级别,自己处理
response = echo(request);
} else {
//不是自己处理的级别,交给下一个处理者
if (mNextHandler != null) {
response = mNextHandler.echo(request);
} else {
//没有处理者能处理,业务自行处理
}
}
return response;
}

public void setNext(Handler next) {
this.mNextHandler = next;
}

@NotNull
protected abstract Level getHandlerLevel();

protected abstract Response echo(Request request);
}

实际应用


我们回到开篇的问题:如何设计弹框的责任链?


//抽象处理者
abstract class AbsDialog(private val context: Context) {
private var nextDialog: AbsDialog? = null

//优先级
abstract fun getPriority(): Int

//是否需要展示
abstract fun needShownDialog(): Boolean

fun setNextDialog(dialog: AbsDialog?) {
nextDialog = dialog
}

open fun showDialog() {
//这里的逻辑,我们就简单点,具体逻辑根据业务而定
if (needShownDialog()) {
show()
} else {
nextDialog?.showDialog()
}
}

protected abstract fun show()

// Sp存储, 记录是否已经展示过
open fun needShow(key: String): Boolean {
val sp: SharedPreferences = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE)
return sp.getBoolean(key, true)
}

open fun setShown(key: String, show: Boolean) {
val sp: SharedPreferences = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE)
sp.edit().putBoolean(key, !show).apply()
}

companion object {
const val LOG_TAG = "Dialog"
const val SP_NAME = "dialog"
const val POLICY_DIALOG_KEY = "policy_dialog"
const val AD_DIALOG_KEY = "ad_dialog"
const val PRAISE_DIALOG_KEY = "praise_dialog"
}
}

/**
* 模拟 隐私政策弹窗
* */

class PolicyDialog(context: Context) : AbsDialog(context) {
override fun getPriority(): Int = 0

override fun needShownDialog(): Boolean {
// 这里可以根据业务逻辑判断是否需要显示弹窗,如接口控制等等
// 这里通过Sp存储来模拟
return needShow(POLICY_DIALOG_KEY)
}

override fun show() {
Log.d(LOG_TAG, "显示隐私政策弹窗")
setShown(POLICY_DIALOG_KEY, true) //记录已经显示过
}
}

/**
* 模拟 广告弹窗
* */

class AdDialog(private val context: Context) : AbsDialog(context) {
private val ad = DialogData(1, "XX广告弹窗") // 模拟广告数据

override fun getPriority(): Int = 1

override fun needShownDialog(): Boolean {
// 广告数据通过接口获取,广告id应该是唯一的,所以根据id保持sp
return needShow(AD_DIALOG_KEY + ad.id)
}

override fun show() {
Log.d(LOG_TAG, "显示广告弹窗:${ad.name}")
setShown(AD_DIALOG_KEY + ad.id, true)
}
}

/**
* 模拟 好评弹窗
* */

class PraiseDialog(context: Context) : AbsDialog(context) {
override fun getPriority(): Int = 2

override fun needShownDialog(): Boolean {
// 这里可以根据业务逻辑判断是否需要显示弹窗,如用户使用7天等
// 这里通过Sp存储来模拟
return needShow(PRAISE_DIALOG_KEY)
}

override fun show() {
Log.d(LOG_TAG, "显示好评弹窗")
setShown(PRAISE_DIALOG_KEY, true)
}
}

//模拟打开app
val dialogs = mutableListOf<AbsDialog>()
dialogs.add(PolicyDialog(this))
dialogs.add(PraiseDialog(this))
dialogs.add(AdDialog(this))
//根据优先级排序
dialogs.sortBy { it.getPriority() }
//创建链条
for (i in 0 until dialogs.size - 1) {
dialogs[i].setNextDialog(dialogs[i + 1])
}
dialogs[0].showDialog()

第一次打开
image.png


第二次打开
image.png


第三次打开
image.png


总结:



  • 优点


责任链模式非常显著的优点是将请求和处理分开。请求者可以不用知道是谁处理的,处理者可以不用知道请求的全貌,两者解耦,提高系统的灵活性。



  • 缺点


责任链有两个非常显著的缺点:一是性能问题,每个请求都是从链头遍历到链尾,特别是在链比较长的时候,性能是一个非常大的问题。二是调试不很方便,特别是链条比较长, 环节比较多的时候,由于采用了类似递归的方式,调试的时候逻辑可能比较复杂。



  • 注意事项


链中节点数量需要控制,避免出现超长链的情况,一般的做法是在Handler中设置一个最大节点数量,在setNext方法中判断是否已经是超过其阈值,超过则不允许该链建立,避免无意识地破坏系统性能。


作者:蹦蹦蹦
来源:juejin.cn/post/7278239421706633252
收起阅读 »

腾讯女后端设计了一套短链系统,当场就想给她offer!

你好,我是猿java 如上图,对于这种客评短信,相信大家并不陌生,通过点击短信里“蓝色字体”,就能跳转到一个网页。其实,背后的秘密就是一套完整的短链系统,今天我们就来看看字节的后端女生是如何设计的? 上图中那串蓝色字符,有个专业的术语叫做“短链”,它可以是一...
继续阅读 »

你好,我是猿java


image.png


如上图,对于这种客评短信,相信大家并不陌生,通过点击短信里“蓝色字体”,就能跳转到一个网页。其实,背后的秘密就是一套完整的短链系统,今天我们就来看看字节的后端女生是如何设计的?


上图中那串蓝色字符,有个专业的术语叫做“短链”,它可以是一个链接地址,也可以设计成二维码。


为什么要用短链?


存在既合理,这里列举 3个主要原因。


1.相对安全


短链不容易暴露访问参数,生成方式可以完全迎合短信平台的规则,能够有效地规避关键词、域名屏蔽等风险,而原始 URL地址,很可能因为包含特殊字符被短信系统误判,导致链接无法跳转。


2.美观


对于精简的文字,似乎更符合美学观念,不太让人产生反感。


3.平台限制


短信发送平台有字数限制,在整条短信字数不变的前提下,把链接缩短,其他部分的文字描述就能增加,这样似乎更能达到该短信的实际目的(比如,营销)。


短链的组成


如下图,短链的组成通常包含两个部分:域名 + 随机码


image.png


短链的域名最好和其他业务域名分开,而且要尽量简短,可以不具备业务含义(比如:xyz.com),因为短链大部分是用于营销,可能会被三方平台屏蔽。


短链的随机码需要全局唯一,建议 10位以下。


短链跳转的原理


首先,我们先看一个短链跳转的简单例子,如下代码,定义了一个 302重定向的代码示例:


import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.servlet.view.RedirectView;

@Controller
public class RedirectController {

@GetMapping("/{shortCode}")
public RedirectView redirect(@PathVariable String shortCode) {
String destUrl = "https://yuanjava.com";
// destUrl = getDestUrlByShortCode(shortCode); //真实的业务逻辑
return new RedirectView(destUrl);
}
}

接着,在浏览器访问短链”http://127.0.0.1:8080/s2TYdWd” 后,请求会被重定向到 yuanjava.com ,下图为浏览器控制台信息:


image.png


从上图,我们看到了 302状态码并且请求被 Location到另外一个 URL,整个交互流程图如下:


image.png


是不是有一种偷梁换柱的感觉???


最后,总结下短链跳转的核心思想:


生成随机码,将随机码和目标 URL(长链)的映射关系存入数据库;


用域名+随机码生成短链,并推送给目标用户;


当用户点击短链后,请求会先到达短链系统,短链系统根据随机码查找出对应的目标 URL,接着将请求 302重定向到目标 URL(长链);


关于重定向有 301 和 302两种,如何选择?



  • 302,代表临时重定向:每次请求短链,请求都会先到达短链系统,然后重定向到目标 URL(长链),这样,方便短链系统做一些统计点击数等操作;通常采用 302

  • 301,代表永久重定向:第一次请求拿到目标长链接后,下次再次请求短链,请求不会到达短链系统,而是直接跳转到浏览器缓存的目标 URL(长链),短链系统只能统计到第一次访问的数据;一般不采用 301。


如何生成短链?


从短链组成章节可以知道短链=域名+随机码,随意如何生成短链的问题转换成了如何生成一个随机码,而且这个随机码需要全局唯一。通常会有 3种做法:


Base62


Base62 表示法是一种基数为62的数制系统,包含26个英文大写字母(A-Z),26个英文小写字母(a-z)和10个数字(0-9)。这样,共有62个字符可以用来表示数值。 如下代码:


import java.security.SecureRandom;

public class RandomCodeGenerator {
private static final String CHAR_62 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
private static final SecureRandom random = new SecureRandom();

public static String generateRandomCode(int length) {
StringBuilder sb = new StringBuilder(length);
for (int i = 0; i < length; i++) {
int rndCharAt = random.nextInt(CHAR_62.length());
char rndChar = CHAR_62.charAt(rndCharAt);
sb.append(rndChar);
}
return sb.toString();
}
}

对于 Base62算法,如果是生成 6位随机数有 62^6 - 1 = 56800235583, 568亿多,如果是生成 7位随机数有 62^7 - 1 = 3521614606208,合计3.5万亿多,足够使用。


Hash算法


Hash算法算法是我们最容易想到的办法,比如 MD5, SHA-1, SHA-256, MurmurHash, 但是这种算法生成的 Hash算法值还是比较长,常用的做法是把这个 Hash算法值进行 62/64进行压缩。


如下代码,通过 Google的 MurmurHash算法把长链 Hash成一个 32位的 10进制正数,然后再转换成62进制(压缩),这样就可以得到一个 6位随机数,


import com.google.common.hash.HashFunction;
import com.google.common.hash.Hashing;
import java.nio.charset.StandardCharsets;

public class MurmurHashToBase62 {

private static final String BASE62 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
public static String toBase62(int value) {
StringBuilder sb = new StringBuilder();
while (value > 0) {
sb.insert(0, BASE62.charAt(value % 62));
value /= 62;
}
return sb.toString();
}
public static void main(String[] args) {
// 长链
String input = "https://yuanjava.cnposts/short-link-system/design?code=xsd&page=1";
// 长链利用 MurmurHash算法生成 32位 10进制数
HashFunction hashFunction = Hashing.murmur3_32();
int hash = hashFunction.hashString(input, StandardCharsets.UTF_8).asInt();
if (hash < 0) {
hash = hash & 0x7fffffff; // Convert to positive by dropping the sign bit
}
// 将 32位 10进制数 转换成 62进制
String base62Hash = toBase62(hash);
System.out.println("base62Hash:" + base62Hash);
}
}

全局唯一 ID


比如,很多大中型公司都会有自己全局唯一 ID 的生成服务器,可以使用这些服务器生成的 ID来保证全局唯一,也可以使用雪花算法生成全局唯一的ID,再经过 62/64进制压缩。


如何解决冲突


对于上述3种方法的前 2种:base62 或者 hash,因为都是哈希函数,所以,不可避免地会产生哈希冲突(尽管概率很低),该怎么解决呢?


要解决冲突,首先要检测冲突,通常来说有 3种检测方法。


数据库索


如下,这里以 MySQL数据库为例(也可以保存在 Redis中),表结构如下:


CREATE TABLE `short_url_map` (   
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`long_url` varchar(160) DEFAULT NULL COMMENT '长链',
`short_url` varchar(10) DEFAULT NULL COMMENT '短链',
`gmt_create` int(11) DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE INDEX 'short_url' ('short_url')
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

首先创建一张长链和短链的关系映射表,然后通过给 short_url字段添加唯一锁,这样,当数据插入时,如果存在 Hash冲突(short_url值相等),数据库就会抛错,插入失败,因此,可以在业务代码里捕获对应的错误,这样就能检测出冲突。


也可以先用 short_url去查询,如果能查到数据,说明 short_url存在 Hash冲突了。


对于这种通过查询数据库或者依赖于数据库唯一锁的机制,因为都涉及DB操作,所以对数据库是一个开销,如果流量比较大的话,需要保证数据库的性能。


布隆过滤器过滤器


在 DB操作的上游增加一个布隆过滤器,在长链生成短链后, 先用短链在布隆过滤器中进行查找,如果存在就代表冲突了,如果不存在,说明 DB里不存在此短链,可以插入。 对于布隆过滤器的选择,单机可以采用 Google的布隆过滤器,分布式可以使用 RedisBloom。


整体流程可以抽象成下图:


image.png


检测出了冲突,需要如何解决冲突?


再 Hash,可以在长链后面拼接一个 UUID之类的随机字符串,然后再次进行 Hash,用得出的新值再进行上述检测,这样 Hash冲突的概率又大大大的降低了。


高并发场景


在流量不大的情况,上述方法怎么折腾似乎都没有问题,但是,为了架构的健壮性,很多时候需要考虑高并发,大流量的场景,因此架构需要支持水平扩展,比如:



  • 采用微服务

  • 功能模块分离,比如,短链生成服务和长链查询服务分离

  • 功能模块需要支持水平扩容,比如:短链生成服务和长链查询服务能支持动态扩容

  • 缓解数据库压力,比如,分区,分库分表,主从,读写分离等机制

  • 服务的限流,自保机制

  • 完善的监控和预警机制


这里给出一套比较完整的设计思路图:


image.png


总结


本文通过一个客服评价的短信开始,分析了短链的构成,短链跳转的原理,同时也给出了业内的一些实现算法,以及一些架构上的建议。


对于业务体量小的公司,可以根据成本来搭建服务(单机或者少量服务器做负载),对于业务体量比较大的公司,更多需要考虑到高并发的场景,如何保证服务的稳定性,如何支持水平扩展,当服务出现问题时如何具备一套完善的监控和预警服务器。


其实,很多系统都是在一次又一次的业务流量挑战下成长起来的,我们需要不断打磨自己宏观看架构,微观看代码的能力,这样自己也就跟着业务,系统一起成长起来了。


作者:猿java
来源:juejin.cn/post/7350585600858898484
收起阅读 »

手把手教集成环信新版UIKit组件,快速构建Android应用

前言环信新版UIKit已重磅发布!目前包含单群聊UIKit、聊天室ChatroomUIKit,本文详细讲解Android端单群聊UIKit的集成教程。环信单群聊 UIKit 是基于环信即时通讯云 IM SDK 开发的一款即时通讯 UI 组件库,提供各种组件实现...
继续阅读 »

前言

环信新版UIKit已重磅发布!目前包含单群聊UIKit、聊天室ChatroomUIKit,本文详细讲解Android端单群聊UIKit的集成教程。

环信单群聊 UIKit 是基于环信即时通讯云 IM SDK 开发的一款即时通讯 UI 组件库,提供各种组件实现会话列表、聊天界面、联系人列表及后续界面等功能,帮助开发者根据实际业务需求快速搭建包含 UI 界面的即时通讯应用。



在环信旧版UIkit 中,很多开发者比较头疼的是头像昵称问题,新版UIkit 将解决这一烦恼。新的UIkit从消息扩展中获取昵称和头像,把用户基本的昵称和头像的URL放到消息的扩展中,通过消息传递给接收方,当收到一条消息时,则通过消息的扩展得到发送者的昵称和头像URL,然后保存到本地数据库和缓存,需要显示昵称和头像时进行获取。

此外,新版UIkit 使用的是Kotlin语言进行开发,目前支持本地module 形式的导入,远程依赖可以关注官网后续更新。

官方文档链接

1、本地module 导入

集成条件以及环境配置需要参考文档 :

https://doc.easemob.com/uikit/chatuikit/android/chatuikit_quickstart.html
hatuikit/android/chatuikit_quickstart.html

本地module 导入:选择File - new - import module 进行导入。


部分项目按照上面可能会导入失败。可以选择把 ease-im-kit 放到跟app 一个级别下面,手动进行配置。
在settings.gradle.kts 下进行添加

include(":ease-im-kit")


在app module 下进行添加本地依赖

implementation(project(mapOf("path" to ":ease-im-kit")))

导入以后编译项目即可

2、设置聊天界面的头像昵称

用户调用 EaseIM.login 方法登录时需要传入一个 EaseProfile 对象,包含 idname 和 avatar 三个属性。id 为必填参数,name和 avatar 用于展示当前用户昵称和头像。发送消息时,将 name 和 avatar 属性设置到消息的 ext 中,方便其他用户展示。

EaseIM.login(
user = EaseProfile(
id = "",
name = "",
avatar = ""
),
token = "",
onSuccess = {

},
onError = {code,error ->

}
)

这里登录时进行了设置。发消息时把头像昵称携带在扩展字段里面,扩展字段key 如下

exts: [{key: ease_chat_uikit_user_info,type: 8,value: {"nickname": "小刚","avatarURL": "https:\/\/img0.baidu.com\/it\/u=4105778329,1297102594&fm=253&app=120&size=w931&n=0&f=JPEG&fmt=auto?sec=1710954000&t=d0bfcba4c95d2e7b9bc8aaaf870f0582"}}]

接收到消息以后UIKit 会进行处理,聊天页面就可以展示头像跟昵称了




这里接收到的消息获取到扩展字段解析如下

val jsonObject = JSONObject(message.ext().get("ease_chat_uikit_user_info").toString())
jsonObject.get("nickname").toString()//获取昵称
jsonObject.get("avatarURL").toString()// 获取头像

3、设置会话列表的头像和昵称

EaseIM.setConversationInfoProvider(object : EaseConversationInfoProvider {
// 同步获取会话信息
override fun getProfile(id: String?, type: ChatConversationType): EaseProfile? {
return when(type) {
ChatConversationType.Chat ->{
// 可以从本地数据库或者缓存中获取用户信息,并返回,不可进行异步操作。
loadUserInfoFromLocal(id)
}

ChatConversationType.GroupChat -> {
// 可以从本地数据库或者缓存中获取群组信息,并返回,不可进行异步操作。
loadGroupInfoFromLocal(id)
}

else -> null
}
return null
}

override fun fetchProfiles(
idsMap:
Map
>,
onValueSuccess:
OnValueSuccess
>
)
{
fetchProfilesFromServer(idsMap, onValueSuccess)
}

})

这里可以在接收到消息以后更新会话列表的头像昵称,也可以通过从服务端获取,获取到以后调用上面方法进行设置。
在上面代码中 loadUserInfoFromLocal(id) 是需要自己从本地获取,然后设置到会话列表中,这里也可以在接收消息时从消息扩展字段获取对方的信息时进行设置。

4、会话列表的使用

通过继承 EaseConversationListFragment 进行自定义设置


EaseConversationListFragment 里面实现了页面跳转逻辑,如果继承EaseConversationListFragment 的情况下需要实现对应的方法,也可以直接使用EaseConversationListFragment 。

至此,集成与头像昵称使用介绍完,要比历史版本头像昵称实现简单很多,欢迎大家体验使用环信新版UIkit,有更多丰富功能助您快速实现单群聊完整功能!

参考文档

收起阅读 »

前端如何用密文跟后端互通?原来那么简单!

web
后端:密码得走密文哇! 我:base64?md5? 后端:这次不行哇,新来的测试不好糊弄呢!必须要国密sm2加密捏 我: 好吧,看我的。 我这边使用的是sm-crypto,当然也有很多优秀的库如:forge,我在业务上搭配jszip做过上传zip文件内藏加密后...
继续阅读 »

后端:密码得走密文哇!

我:base64?md5?

后端:这次不行哇,新来的测试不好糊弄呢!必须要国密sm2加密捏

我: 好吧,看我的。


我这边使用的是sm-crypto,当然也有很多优秀的库如:forge,我在业务上搭配jszip做过上传zip文件内藏加密后的私钥进行证书登录,还是不错的支持,但是文档社区不是很友好,所以推荐sm-crypto,接下来让我们一起使用它吧。


sm-crypto是一个基于Node.js的密码学库,专为与国密算法(中国密码算法标准)兼容而设计。它提供了各种加密、解密、签名和验证功能。


sm-crypto包含多种密码算法的实现,例如:



  • SM1:对称加密算法,其加密强度与AES相当,但该算法不公开,调用时需要通过加密芯片的接口进行调用。

  • SM2:非对称加密算法,基于ECC(椭圆曲线密码学)。该算法已公开,且由于基于ECC,其签名速度与秘钥生成速度都快于RSA。此外,ECC 256位(SM2采用的就是ECC 256位的一种)的安全强度比RSA 2048位高,但运算速度快于RSA。

  • SM3:消息摘要算法,可以用MD5作为对比理解,其校验结果为256位。

  • SM4:无线局域网标准的分组数据算法,属于对称加密,密钥长度和分组长度均为128位。


sm-crypto内部方法介绍


1.SM2加密与解密


SM2是一种基于椭圆曲线密码学的非对称加密算法。sm-crypto提供了SM2的密钥生成、加密、解密等功能。通过调用相关方法,开发者可以轻松地生成SM2密钥对,并使用公钥进行加密、私钥进行解密。


const { sm2 } = require('sm-crypto');  
const keyPair = sm2.generateKeyPairHex(); // 生成密钥对
const publicKey = keyPair.publicKey; // 公钥
const privateKey = keyPair.privateKey; // 私钥

const message = 'Hello, SM2!'; // 待加密的消息
const encrypted = sm2.doEncrypt(message, publicKey, { hash: true }); // 使用公钥加密
const decrypted = sm2.doDecrypt(encrypted, privateKey, { hash: true, raw: true }); // 使用私钥解密

console.log('加密结果:', encrypted);
console.log('解密结果:', decrypted.toString()); // 输出原始消息

image.png


2.SM3摘要算法


SM3是一种密码杂凑算法,用于生成消息的摘要值。sm-crypto提供了SM3的摘要计算功能,开发者可以通过调用相关方法计算任意消息的SM3摘要值。


const { sm3 } = require('sm-crypto');  
const message = 'Hello, SM3!'; // 待计算摘要的消息
const digest = sm3(message); // 计算SM3摘要值

console.log('SM3摘要值:', digest);

image.png


3.SM4分组加密算法


SM4是一种分组密码算法,适用于无线局域网等场景。sm-crypto提供了SM4的加密与解密功能,开发者可以使用SM4密钥对数据进行加密和解密操作。


const sm4 = require('sm-crypto').sm4;                                               |
const sm4 = require('sm-crypto').sm4;
const key = '0123456789abcdeffedcba9876543210'; // 16字节的SM4密钥
const message = 'Hello, SM4!'; // 待加密的消息
const encrypted = sm4.encrypt(Buffer.from(message), Buffer.from(key, 'hex')); // 加密
const decrypted = sm4.decrypt(encrypted, Buffer.from(key, 'hex')); // 解密

console.log('加密结果:', encrypted.toString('hex'));
console.log('解密结果:', decrypted.toString()); // 输出原始消息

image.png


4、签名/验签


签名(Sign)


const { sm2 } = require('sm-crypto'); 
const keyPair = sm2.generateKeyPairHex(); // 生成密钥对
const publicKey = keyPair.publicKey; // 公钥
const privateKey = keyPair.privateKey; // 私钥

const message = '这是要签名的消息'; // 替换为实际要签名的消息
// 使用私钥对消息进行签名
let sigValueHex = sm2.doSignature(message, privateKey);
console.log('签名结果:', sigValueHex);

image.png
验签(Verify Signature)


const message = '这是要验证签名的消息'; // 应与签名时使用的消息相同  
const sigValueHex = '签名值'; // 替换为实际的签名值字符串,即签名步骤中生成的sigValueHex

// 使用公钥验证签名是否有效
let verifyResult = sm2.doVerifySignature(message, sigValueHex, publicKey);

console.log('验签结果:', verifyResult); // 如果验证成功,应输出true;否则输出false


image.png


实战例子


登录注册,对用户密码进行加密



注意:前端是不储存任何涉及安全的密钥(公钥是直接拿后端生成的)。



新建个工具文件,专门存放加密逻辑,我这用的是SM2


// smCrypto.js
import { sm2 } from 'sm-crypto' // 引入加密库

export const doEncrypt = ( // 加密
data,
pKey = publicKey,
cipherMode = 0
) =>
sm2.doEncrypt(
typeof data === 'object'
? JSON.stringify(data) : data,
pKey,
cipherMode
)
export const encryptionPwd = async data => { // 加密密码高阶
let servePublicKey = ''
await user.getSm2Pkeys()
.then(res => {
servePublicKey = res.data.content
})
return doEncrypt(
data,
servePublicKey
)
}

sm-crypto作为一款基于Node.js的国密算法库,为开发者提供了丰富的密码学功能。通过调用sm-crypto的内部方法,开发者可以轻松地实现SM2加密与解密、SM3摘要计算以及SM4分组加密等操作。这些功能在保障数据安全、构建安全应用等方面发挥着重要作用。同时,开发者在使用sm-crypto时,也需要注意遵循最佳的安全实践,确保密钥的安全存储和管理,以防止潜在的安全风险。


作者:大码猴
来源:juejin.cn/post/7350168797637558272
收起阅读 »

浏览器无痕模式就真的无痕了吗?不一定哦!

web
概述 无痕模式,有些浏览器也叫隐身模式,隐私模式。该模式下所有cookie、缓存是失效的,也就是所有原来的登录信息都会消失,那么是否你打开一个网站,网站平台就真的不确定你是谁了吗? 不一定哦。这个世界上有一种技术叫浏览器指纹技术,不需要你登录,它就可以根据你的...
继续阅读 »

概述


无痕模式,有些浏览器也叫隐身模式,隐私模式。该模式下所有cookie、缓存是失效的,也就是所有原来的登录信息都会消失,那么是否你打开一个网站,网站平台就真的不确定你是谁了吗? 不一定哦。这个世界上有一种技术叫浏览器指纹技术,不需要你登录,它就可以根据你的特定标志来区分,从而跟踪你的所有操作记录。今天我们就来看看这种技术的原理,还有用途。


一、原理


浏览器指纹可以在用户没有任何登录的情况下仍然知道你谁,比如你在登录了一下网站A,现在开启无痕模式,再次打开网站A, 那么网站A大概率还是能区分现在操作网站的人是谁。 因为在这个世界上,用户的浏览器环境极小概率才能相同,考虑的因素包括浏览器版本、浏览器型号、屏幕分辨率、系统语言、本地时间、CPU架构、电池状态、网络信息、已安装的浏览器插件等等各种各样。浏览器指纹技术就是用这些因素来综合计算一个哈希值,这个哈希值大概率是唯一的


二、展示


今天我们就展示一下单单利用Canvas画布这个功能来确定用户的唯一标识,因为Canvas API专为通过JavaScript和HTML绘制图形而设计,而图形在画布上的呈现方式可能因Web浏览器、操作系统、显卡和其他因素而不一样,从而产生可用于创建指纹的唯一图像,我们利用下方代码展示一个图像。


// 输入一个带有小写、大写、标点符号的文本
var txt = "BrowserLeaks,com <canvas> 1.0";
ctx.textBaseline = "top";
ctx.font = "14px 'Arial'";
ctx.textBaseline = "alphabetic";
ctx.fillStyle = "#f60";
ctx.fillRect(125,1,62,20);
ctx.fillStyle = "#069";
ctx.fillText(txt, 2, 15);
ctx.fillStyle = "rgba(102, 204, 0, 0.7)";
ctx.fillText(txt, 4, 17);

下面动画GIF图展示了虽然JavaScript代码相同,但是由于图像在不同系统上的呈现方式不同,因此每一个用户所显示的图像都有细微的差别。这种差别大多数时候人眼通常无法识别,但通过对生成图像的数据进行分析就能发现不一样。



我们直接看成品,利用普通模式和无痕模式打开这个测试网站,就能发现2个哈希值是完全一样的,并且是每15万个用户中有2个人可能是相同的哈希值,唯一性达到了99.99%。


三、用途


1、广告


广告联盟通过浏览器指纹技术,不需要你登录,就可以知道你看过哪些记录,然后在其他站点给你推送你感兴趣的广告。所以有时候我们经常会碰到在淘宝或者百度搜索了一个商品,然后去到其他网站,马上给你推送你搜索过的商品的广告。


2、防刷


针对一些商品的浏览量、内容的阅读量、投票等等各种类似平台,往往会针对用户做一个限制,比如只能浏览一次。这种时候往往也都是用浏览器指纹技术,虽然你可能注册了很多用户账号,但是你的浏览器指纹都是相同的,可以判断你就是同一个人。不过上有政策,下有对策,有个东西叫做指纹浏览器,这类浏览器里面可以随意的切换用户的指纹。 所以这个技术针对广大的普通用户防刷还是有效的,对专业的刷量工作室就没什么效果了。


作者:明同学
来源:juejin.cn/post/7347958786050637875
收起阅读 »

勇闯天涯-程序员小哥哥,来婚介所试一下嘛(七千字长文预警!)

前言 今天技术博主不聊技术,聊聊生活聊聊情感。先叠个甲,我纯纯乐子人,对婚介所无任何恶意,可能后面还得用上,本次纯纯试探下婚介所是个什么路数!事件源于上周五的一通红娘电话,我一听要给我介绍有钱有颜的小姐姐,直接挂断,这天底下哪有这样的好事。当天下午的时候,又给...
继续阅读 »

前言


今天技术博主不聊技术,聊聊生活聊聊情感。先叠个甲,我纯纯乐子人,对婚介所无任何恶意,可能后面还得用上,本次纯纯试探下婚介所是个什么路数!事件源于上周五的一通红娘电话,我一听要给我介绍有钱有颜的小姐姐,直接挂断,这天底下哪有这样的好事。当天下午的时候,又给我来了一通电话,仿佛不知道我给她挂断了一样,基于一些奇妙的想法,我想尝试以及找找素材,我决定让她微信跟我聊。然后因为博主工作比较忙,打了招呼后,就没理她,最关键是到晚上下班后,我也忘了这档子事。周六的下午四点左右,一通电话开启了本次的故事,即使315曝光了相关产业,但是博主依然要冲,就是头铁。


阳光下午的微醺时光


接到电话的时候,我挺疑惑的,这次上来就问我现在是不是有空,出于礼貌,并且我也听出了她是昨天那个红娘,我就没有挂断电话。上来就说,弟弟,我是XX老师,今天来电话主要是聊一下,看能不能进一步推荐小姐姐。说她那里看资料我是97年的程序员,年收入XX,对吗?然后我说是,她就跟我说这里有个小姐姐挺合适的,北京XX工作的,身高165身高,本地的,收入XX。哥们一听就懵了,本地的,心里OS是,那咋遭得住,咱也不是高富帅,纯纯普男。红娘一听就问我有没有考虑常驻北京,然后就问我老家哪里的,我说四川的,她就在资料库里找了找,说真有个四川在北京的,快速给我念了一遍女生的资料。


接着就离谱了,问我怎么称呼?我说姓王,哪里工作,海淀XX公司?红娘直接薄纱,诶,没听过耶( •̀ ω •́ )y,咱们一般服务百度、阿里和字节的比较多。好好好,北京人中龙凤就是多,哥们就是菜中菜,脸都绿了。接着问我加班如何,公司福利啥的,然后说我这个181cm的身高在四川挺高的,她知道的四川男生都不太高来着。然后聊到了哥们的毕业学校,说前两天有个大连理工的女孩,真绝了,大家都这么爱走婚介所吗?我感觉聊到啥,都有女孩能配上,后面说到工作,又聊到一个大二的姑娘还没工作就来找对象了,好好好,走上了人生快车道。


然后聊到了我父母的职业,就说哥们的条件在当地不错,怎么没找上对象?我当时就说到了我的前对象,网恋的,就在掘金找的,一说到这个,好兄弟们,掘金真的能找对象的,不骗人。我的失败不是网恋的问题,纯属我自己的问题,第一次谈吧,不知道咋谈,自己也是摸不准进度,达不到对象的期待,慢慢地就淡了。我也是无脑纯爱战士,这是红娘听了之后的评价,哈哈,不过我觉得前对象确实挺不错的,当时要是硬扛着也能继续,但是我感觉可能是热情燃尽了,她也算是太忙了太累了,无论是生活还是工作,我都挺佩服,主动退出,我感觉应该是我被甩了。不后悔,男人就该当断即断,她也不小了,继续耗着,对她不公平。说远了啊,核心意思就是,网恋也有靠谱的,掘金这质量高的离谱,大家多去相亲角看看。


说着说着,突然红娘聊到了星座,说我应该了解了解星座,也说到了一些星座的小知识。然后顺嘴提到了应该门当户对,不只是能力,还有三观,阶层之类的。接着说道,我条件还行,为啥这么晚谈恋爱,是不是打游戏了?我说姐,没毛病,游戏真好玩!又接着问我单身多久了,身边还有没有介绍的呀,同事父母啥的资源。接着就进入正题了,就说小弟弟你呀,身边没有资源,姐姐这里有呀,你又有点慢热,男人花期这么短,这不得抓紧时间。你没谈就是没有遇到合适的人,圈子太小,北京这边初婚太晚了,25岁才准备开始,你想想四川重庆那边20左右就开始准备结婚了,你回去那就是大龄青年了,不是市场上最合适的那一批了。姐姐之前认识一个大哥,一年给女生花了七八万,环球影城去哈尔滨各种礼物砸下去结果手都没牵上。我???这不是大冤种嘛,移动ATM。红娘就跟我说,弟弟你啊,现在这恋爱能力,跟女生比划比划半年结果嘴都亲不上,咱们也不要浪费时间,选对人比胡乱去试强多了。姐姐我这啊,适合你的资源还蛮多的呀,你是B站来的资源,这种比较少,一般只占15%,我们婚介所大部分都是线下和国企政府合作的,所以需要你提供一定的身份和征信资料,因为我们这的姑娘都是认证过的,要对大家负责。接着又说觉得我性格比较好,很坦诚,就问我对女生有没有什么要求,我说年龄上不超过我三岁吧,不抽烟不去酒吧夜店,然后红娘跟我说,你这就没别的要求啦,那还挺低的,然后就问我三观、家庭情况之类的要求,问的很细。然后谈笑间,红娘给我猛推三个女生,嘎嘎读资料,听得哥们心里发慌,动不动就是大厂,家里有矿,问女生条件是否符合我的要求.......我真蚌埠住了,哥们真的配嘛,这条件谷歌,父母地方小领导或者开公司,问我符不符合,好好好,我能傍富婆不?


有一说一,专业红娘就是能聊,我俩聊了得有一小时,最后说让我去线下谈,给我预约了她明早10点半的时间。她想要了解下我这个人的衣着品位、谈吐举止、再做下深度的了解,以及一些信息的认证,同时也给我一些情感上的建议,让我少走些弯路。哥们听到情感上的建议,真的心动了,教练,俺就缺这个,确认了下这次聊天不收钱,就是个单纯的信息认证,我就决定去了。


小雨霖霖的春日午前


因为约好的时间是10点半,我住在石景山,婚介所在朝外SOHO那里,差不多一小时十几分钟的路程,我就九点出发了。到了地方之后,周围有好几个SOHO,没有明显指示牌,而且感觉周日早上那里人也少,当时就觉得奇怪了。因为事前的时候,跟亲友和群里的小伙伴下过军令状,不掏一分钱,实时播报进度,没消息就赶紧救我,所以我倒也没有特别慌。我的想法就很纯粹,想通过红娘的眼睛来认识我在相亲市场中是什么样的,因为我接触的人相对少嘛,所以也算是练练胆,特别是这种情感上的,多积累积累经验总没错。没找到地方,我就拍了个附件的标识给红娘老师,她就指挥我上了楼,上楼之后还蛮吓人的,左边黑的,右边就她家灯亮的。


进门之后有个助理来接待,给了我一张表单去填,相对详细哈,但是身-份-证这敏感信息可以选择不填,其实这会儿我觉得还挺不错,放下了防备,但是打开了录音,哈哈。把我领到一个逼仄的会议室,大概两平米吧,首先给我拍了一张坐着的照片,然后在我填表的时候,说要看下我的征信,简单点的就看芝麻信用就行,看了我的八百多分吧,就说你这还蛮高的。又试探性的问了能不能看下花呗借呗之类的,我寻思着也没多少额度,那就看呗,他一看就挺奇怪的,就说按你这个分数应该有好几万才对呀,你是自己调了吗?我说是呀,没事搞那么多干啥,又问我有没有信用卡,我说就拿花呗当信用卡。又说想看我支付宝里的资产,我寻思着没几个钱,那就看呗。他一看就说,你没炒基金股票啥的嘛,我说没,他就顺着话说,昨天有个姑娘和你一样,也是这样的,她就喜欢这种稳赚不赔的。浅聊了一下就说,去给我叫红娘老师,说红娘老师特别看重我,昨天周六来了好多姑娘,很多都不错的,让我跟红娘老师好好学学。哥们一听这个就来劲了,好啊,就是要这个,我不就缺这套恋爱情商嘛。


热情似火的夏日狂欢


红娘老师进来首先点评了我的头发,有点长还有点凌乱,说怎么还没理发呀。这一点说的挺对,我快一个月没理了,因为头发烫过,又有点长,显得乱。然后又说我好白呀,南方人皮肤就是好,因为我有痘痘嘛,就聊了聊油性皮肤。然后就跟我说想看看我短发的照片,说比较难想象我短发的照片,又说我这个是不是渣男锡纸烫,认出来了我这是前刺发型,感谢我的四川托尼,摩根前刺音容犹在。然后又说我字写的还不错,我说着就没必要硬夸了吧,她说跟其他人对比算是工整了。因为看到我手机壳了,就问我是个什么图案,我给她看了是二次元,她就问我是不是老爱玩B站,是不是博主,我说我哪是啊,只是喜欢看。她就说你声音还蛮好听的有点像肖战,可以整点视频啥的,说不定还能火。我说没那颜值,哈哈,谈笑间我就给她发了几张我的生活照,还有全身照片。她就说没有很胖呀,看着还好,而且家里还蛮干净的,弟弟习惯不错。


我也比较能说嘛,我就说红娘老师怎么不开个号啥的,吐吐槽啥的,比如B站上的XX说媒啥的。她跟我说她们上班很忙的,周一周二休息,平时都是快九点才下班,哪有时间开视频吐槽,都是朋友之间聊聊。然后她就很好奇我之前电话里说的前对象嘛,就聊了聊我前对象的事。因为我是抱着想要咨询的目的来的,所以我也说的相对比较详细,红娘比较在意的点是各阶段时间点、怎么认识的、告白怎么做的、准备了什么样的礼物和GG的理由。大致说了一遍,我觉得我前女友挺好的,无奈博主高攀不上,也没有达到人家的期望,就选择了默默退出。前女友说实话很不错的,情侣头像是我先换她才换的,我说话她也会回我,是我感觉被甩了,也是她有点冷吧,就放弃了。说这些呢,也是跟前面一样,说一下兄弟们要勇敢冲,掘金相亲角也靠谱,哪有那么多渣女,大部分还是好女孩,希望大家都把握得住,找到合适的另一半!


红娘老师跟我说了几个之前来聊的哥们,就说谈恋爱不能太晚,有一些大哥因为圈子比较小,导致来婚介所就想找个合适的姑娘一步到位。但是哪有这么合适的呀,一点攻略和目标都没有,怎么可能一口吃个胖子!我也附和老师说,对啊,心急吃不了热豆腐。然后就跟我说,昨天来了两哥们,一个京东一个百度的,还有00年来找对象的,我一听都懵了,这不都是人中龙凤嘛,这还愁对象,有没有大厂哥们来现身说法的,让小弟我长长见识。聊着聊着,就跟我看了个小姐姐,大四左右吧,想找个大五岁左右工作稳定的哥们,本身条件很优秀的,北交大硕士进国企,所以想要个稳定的生活。反正红娘老师意识就是说,现在女生吧,恋商普遍比男生多个三五年,现在这么卷,考虑得都比较现实。然后就说现在女生都是要求有钱有颜,小弟弟你也没钱,正在发展期嘛,颜的话还算周正,需要打理打理。然后就说想看看我之前的照片,要看我手机,emmm,这时候我没发现这是个套路,应该之前有哥们也做过这事儿。我有开录音嘛,左上角有小标,很烦,红娘应该是特意盯了这个地方,一开始问我怎么不给她看手机,而是直接发给她。我也没在意,她就老找理由看我手机,然后我也不小心被她看见了,她就问我是不是忘了关导航,哈哈,我打了个马虎眼就过了,然后后面过了一阵又看到了,就直接问我是不是录音。我非常坦诚的告诉她就是在录音,有顾虑,然后我也当着她面关了,我俩嘻嘻哈哈也就把这事过了。


想要摸一下秋天的老虎


聊了聊我的家庭,跟我聊了聊她最近咨询过的男生,有成功也有失败案例,反正都是会员。比较隐晦地指出了我这个水平在北京不够看,在成都也不算特别优秀地类型,但是结合家庭和年龄来讲,也算是还不错,至少还有操作空间。我一开始就想知道我在婚恋市场是个什么档次,以及从红娘视角来看我是什么样的,所以我就直球问她了。或许是基于职业素养,或者我问的有点子收费味道了,她顿了一下,从两方面给了我建议,一是从我初恋这段历程总结出来的,说我就是没有技巧的纯爱战士,需要提高情商,反正套话居多。二是不要乱撒网,要精准定位,选择自己合适的人,基于星座啥的跟我聊了半天,我这金牛座适合什么样的人。然后就说我这个改是来不及了,27也不小了,没救了,就从选对人开始,然后就开始围绕她们的服务开始聊。


总共聊了得有三小时左右吧,前一个半小时唠家常,根据你的资料聊细节,时不时说说女生的资料。后面就开始猛攻,围绕中介所的两项核心服务,一是心动女生匹配筛选,二是全程跟踪指导,结合真实案例开始跟我介绍。比如女生这个,真的是拿了五六份女生的资料,就跟简历似的,问我有没有眼缘,又跟我说这几天来了什么什么姑娘,有还没毕业的,有点子真实案例掺点焦虑的感觉。比如全程指导,给我看了她手下成功的案例,放了点截图和视频啥的,让我看效果。


中间插一下哈,聊聊我对红娘的看法,真的很强。能干这行的都是社牛,还有很强的气场,我确实能感觉出来她是很快乐地在撮合,觉得是在做正确的事,收的钱理所应当。说真的,要不是我去之前就抱着分文不出的想法,她一顿吹我真给了,业务能力太强了。她聊天的话,目的性很强,总是往婚介服务,这个筛选方向去带,会弱化你的劣势,不会很明显地贬低你,但也不会特别夸耀你的长处,就把你当作一个朋友一样,所有都是围绕我们能给你找一个天造地设的对象这一核心点展开。说实在的,动不动认个亲弟弟这操作也太骚了,见面第一次,这社交强度直接拉满,很热情。


聊聊收费啥的,好像每个人都不一样,给我的标准是1年49990,半年29990,3个月19990,后面我说不用了,拉扯半小时,认我做亲弟弟,然后给我亲属价对赌协议,先交10100等成功了再补9890或者给她介绍2个就不用给尾款了。最后博主一是领导在催干活,嘎嘎电话,朋友在催吃饭,也微信电话不停,就想润了,红娘就说要不先交500定金,不行再退,我确实没这想法,就发了50红包给姐买奶茶。反正这吃相一出来就有点尬住了,我这确实是很好说话,没有一甩脸子直接走了,就这么耗着。说实在的,我确实觉得人家说了3小时,这总得有点收益,50块钱在北京不是什么大钱,但是我本来也是抱着聊聊的想法,没想到上来就这么狠,直接2W起步,真恐怖,润了润了。


又到了白色相簿的季节


最后聊聊博主的收获,有用信息不多哈,大部分是一些套话,这大概就是免费内容吧。她给我看了客户的跟踪记录,红娘会全程指导男生的行为,会推一些书或者情感内容啥的,当然,这都是收费内容。怎么说呢,这一次三小时的激烈battle,我也能感觉出来,我还是该在情商上做一些提升,红娘有提到男女之间的情商和朋友之间的情商是两码事,说我上一次挂了就是技巧不足,竟然半年连嘴都没亲,这还是人???跟我说她们那里的都是一个月速通,我这样的已经没救了,好好好,这年头纯爱战士就是鲨BEE。


我其实对这家婚介所没啥意见,我也是想着既然聊上了那就去看看呗,没想到第一次这强度就这么大,哈哈,离谱。我看了下女生资源都很不错,男生也很厉害,都是人中龙凤,不知真假。XX爱恋,她跟我说主要还是线下,我的资料是从B站过去的,就离谱,不知道我填啥了,一开始就知道我的性别年龄和手机号,其他一概不知,可能是我资料被卖了吧,根据年龄给我推送的。喔,对了,说是每周给政府或者国企办公益相亲会,我之前倒是没怎么了解,不知道她家是不是在北京属于厉害的了,还是纯诈骗,摸不清楚,这也是我没交钱的理由之一。因为我在B站上看过XX说媒嘛,我就问她,怎么不开个自媒体号吐吐槽,还能挣点外快。哈哈,这姐非常真实,说剪辑、化妆、文案太累了,线下就够赚了,没必要混线上。


写着写着,突然想起来这姐还说过我一个坏毛病,说我没有底线,有点舔狗,女生一般不喜欢姿态放低的男生,希望我有点神秘感,或者说装一点。哈哈,下一个更好,咱们有的是资源,看得出姐是相当洒脱,成年人不改变只选择这话倒是诠释的清清楚楚。还说我应该有个底,不要一上来就跟人托底,比如她一看我就是个逗比的人,结果跟我一聊还真是个嘻嘻哈哈的乐天派,这样就没有神秘感,她说女生一般不太喜欢这种。


最后的最后,她把经理叫来了,做个回访,我是很欣赏红娘老师的,因为我这次没谈成业绩我也是相当抱歉,所以当着经理面说了很多红娘的好话,没有谈成也确实是我的问题。哈哈,经理直接问我,免费给我介绍同意不,我都懵了,这太狠了,我也没答应,这种形式还是太超前了。相亲说实话还是摆条件,看见女生资料搁面前一遍遍刷,人中龙凤太多啦,哈哈,不由得想起了我初恋也就是我前对象,我也得好好努力呀!


锵锵!我闪亮登场!


今天突然想到,好像真是在B站一个UP那里填过资料的,是我错怪人家了。红娘这姐姐挺有意思的,我是真的觉得她干这行很快乐,她还问我对这行有没有兴趣,哈哈。这种撮合有情人就能收钱的事可太行了,我也蛮喜欢这种做好事的感觉。不过骗人的也不少,315曝光过了,大家也长个心眼吧,学会识人。


最后做个总结吧,博主从昨年开始琢磨男女大事,经历了人生第一次相亲会、初恋、婚介所,虽然都只有一次,但都是挺有意思的。哎,母胎SOLO真的太难了,不会谈啊,盲目冲锋的纯爱战士真的不是版本答案了,红娘老师也跟我说,女生感情上比男生早熟三四年以上,难难难,但是博主依旧对生活抱有期望,哈哈,毕竟我是个乐子人。


我的人生信条是,我要让这痛苦压抑的世界绽放幸福快乐之花,向美好的世界献上祝福!!!


作者:云雨雪
来源:juejin.cn/post/7350123500936986674
收起阅读 »

祖传屎山代码平时不优化,一重构就翻天覆地

写作背景 写背景之前先放一张网图,侵删。 有一个活跃应用包含了2个相似业务场景,所以共用了底层模型。 前期在开发过程中,强行将两波研发组正在研发的产品底层模型和能力统一。底层能力统一遇到了挺多问题,比如数据库字段适配、转换、冗余;repo 层 SQL...
继续阅读 »



写作背景



写背景之前先放一张网图,侵删。



image.png


有一个活跃应用包含了2个相似业务场景,所以共用了底层模型。




  1. 前期在开发过程中,强行将两波研发组正在研发的产品底层模型和能力统一。底层能力统一遇到了挺多问题,比如数据库字段适配、转换、冗余;repo 层 SQL 条件拼接用了大量 if else,导致建索引困难…等等。

  2. 一些历史原因,该应用经手了十多个研发,代码是垒了又垒,出现一个很有规律的现象,大家都是只增代码不减代码。

  3. 代码性能随着数据规模增加不断降低,靠着优化补丁缝缝补补支撑着,业务高峰期经常被运维同学拿着 SQL 光顾。



重构该项目想法不止10次,想逮着机会拉着各方大佬商讨重构事项,因为重构对业务是没有收益的,并且重构难度相当大,所以迟迟没有下定决心。


最近刚好产品需要打磨下一个版本,需要挺长时间,几个后端研发商讨要不重构吧。嗯,我想可以,于是我找上前端负责人沟通拉他入伙,找上前端之前测试已经同意了。


于是一场重构拉开序幕了。




  1. A 同学负责梳理和收敛模型、数据订正、向上提供能力。

  2. B 同学负责梳理前端接口,编排底层能力,提供原子接口给前端(最复杂,直接面向Web端业务,接口有很多特殊逻辑)。

  3. C 同学负责引擎层,和一些计算类逻辑,另外就是打打杂。



重构


大型重构耗时不说还费人力,搞不好重构完你拿不到业务结果,所以重构前你要明确收益是啥?无非就是下面几种




  1. 性能提升产品体验更好;

  2. 简化架构并提升架构扩展性(后面迭代基于重构后架构能快速开发上线);

  3. 历史债清理,历史代码可读性差维护费劲(大部分程序员看别人代码都是这样吧)。



我们是三种情况都中,下面简单总结重构的思路吧。


模型梳理和能力收敛


底层模型我认为最重要,要可靠、稳定且变动少,如果在迭代中你的模型变来变去,上层业务根本开发不了或者边开发边改,到项目收尾就是另一坨屎山。


上层业务是根据底层模型长出来的,所以一定要跟产品讨论确定最终模型,若有好的竞品参考更好了,你的设计可能会看的更远,以防过度设计,架构设计满足未来1-2年迭代即可。


模型设计需要预估数据规模,数据规模决定是否采用分库分表/分区表。如果不好预估采用简单原则先上线看看业务效果,但基础框架这些能力一定要预留好,上量后能快速开发上线。


底层模型和能力收敛了,上层业务编排对能力的复用性更高。ps:这次模型梳理我们干掉了 2 张千万级表。


数据订正


模型梳理和收敛一般会涉及数据订正,特指线上模型对应的数据割接在新模型。一般会有下面几种方式




  1. 写脚本从数据库捞数据订正数据,一般会先 select 查询到内存中,重新组装数据再 insert 新模型。数据量小场景完全可行,但数据量大是跑不动(已踩过坑,线上数据几天没跑完最后发布失败)。

  2. 写 SQL 直接操作数据表,简单的数据处理场景、数据量小场景可行,数据量大场景不可靠,容易超时并且会有数据库稳定性风险,另外订正逻辑复杂是搞不定的(已踩过坑,线上跑数据失败导致发布失败)。

  3. oplog、binlog.... 日志采集同步到消息队列(kafka、pulsar 等),启消费组消费订正数据(我最常用也是最可靠的),数据量大的场景特别爽,处理存量数据的同时还能保证增量数据同步处理。



数据订正是清理过期数据最佳时期。假若平台过期数据体量大,这部分数据不迁移新表,留在历史表中当备份就行,亦可快速恢复。ps:本次重构过期数据预估是千万级别。


API 接口


接口是你对外的门面,应该提前规划明确,不能新增需求就干一个接口,需求迭代到后期,大大小小接口加起来几十上百个维护成本是很高的。


我们一般会按照下面几个原则:




  1. 按操作分类比如:增、删、改、查是一类,只会定义4个接口上游业务方调用需传入 source 区分调用源。

  2. 接口保持简洁,不耦合非当前业务的复杂数据。比如业务上需要回显组织架构数据(员工名称、部门、员工上级等),这类数据需要业务方自行编排组织架构 byids 接口。

  3. 接口具备降级能力,不能因为接口内部编排的非重要接口、逻辑报错导致整个接口不可用。
    降级指将某些业务或者接口的功能降低,可以是只提供部分功能,也可以是完全停掉所有功能。



这次重构 B 同学和前端面临了巨大压力,接口多、混乱、逻辑不清晰,决定梳理业务逻辑按照上面 3 个原则重写接口。


代码重构技巧


代码重构也是本次重构重点,经过长时间迭代已经闻到了坏代码味道。怎么重构早就心中有数,很早就盘点和推演了,下面是我常用的一些重构技巧,这些技巧都是非常经典的,如果看过「重构改善既有代码设计」应该都不陌生。


内联临时变量

项目里有一些临时变量,只被简单赋值了一次。将这些临时变量的赋值语句直接嵌入到使用它们的地方,而不是创建一个新的变量来存储这个临时值。


func Publish() error {
// ... 省略一部分代码
err = Producer(context.TODO()).ProducerOne(&obj)
if err != nil {
return err
}
return nil
}

临时变量内联改造后👇👇👇


func Publish() error {
// ... 省略一部分代码
return Producer(context.TODO()).ProducerOne(&obj)
}

魔幻数字"(Magic Number)

指代码中使用未经解释或定义的常数值,这些值通常没有命名并且没有给出其含义或用途。这样的数字使代码难理解和维护,项目里面很多魔幻数字使用。


func Update(ids []string, nodeID string) {
// ... 省略一部分代码
Report(context.TODO(), nodeID, "6", ids)
}

要解决魔幻数字比较简单,只需你把业务逻辑理解定义成枚举就可以了,这个数字6表示朋友圈类型,魔幻数字改造后👇👇👇


type TargetType int

const (
QWMoment TargetType = 6
)

func Update(ids []string, nodeID string) {
// ... 省略一部分代码
Report(context.TODO(), nodeID, QWMoment, ids)
}

删除注释、未引用代码「俗称死代码」

根据我 review 代码的经验,不少研发同学会把已注释、未引用代码保留,这部分代码是非常影响后面维护者思路的,我们在重构过程中遇到不少这类代码,来来回回找测试和研发确认为什么会保留,哪些业务常用在用?带来了不小负担。(尤其是越上层的死代码引用了一堆下层代码,比如controller 引用 service,service 再引用 repo ,若重写 repo 非常上头)


所以我强烈建议,一旦代码不用了,应该立刻删除。若删除这部分代码后面迭代可能会使用,我建议重新开发。我列一些删除代码后的收益。




  1. 清晰度和简洁性;

  2. 减少维护成本;

  3. 减少冗余和混乱;

  4. 避免误导。



卫语句取代条件表达式

卫语是用来提前结束方法执行的结构。通常情况下,卫语句用来检查某些前置条件是否满足,如果条件不满足,则立即退出方法执行,以避免进入后续的代码块。有助于减少代码嵌套深度,增加代码的可读性和可维护性。


按照我的经验,卫语句应该有下面 2 种情况:




  1. 两个条件分支都属于正常行为;

  2. 有一个条件分支是正常行为,另一个分支则是异常的情况。



我们review过的代码一般是第二种情况比较严重。


func Recall(exclusion constant.ExclusionType)error  {
if exclusion == constant.OnlyOneExec {
if detail.TargetID == "" {
return nil
}
_, err := repo.Update(ctx,....)
if err != nil {
return xerrors.Wrapf(err, "Update")
}
}
return nil
}

调整后代码👇👇👇


func Recall(exclusion constant.ExclusionType)error  {
if exclusion != constant.OnlyOneExec {
return nil
}

if detail.TargetID == "" {
return nil
}
_, err := repo.Update(ctx,....)
return err
}

变量改名

好的命名能让读者一目了然,变量名可以很好的解释一段代码干了什么。我发现项目里面很多字段名、类名、包名很模糊,很难理解具体的业务(包括我自己也经常命名错)。


下段代码是我整理的坏的命名


TaskCommand 是 Kafka 消费者依赖的实体,收到消息后根据 Type 和 Status 撤回数据。但你看 struct 名 跟撤回没有任何关系。


// TaskCommand 任务相关命令
type TaskCommand struct {
Type   int8 `json:"type"`   // 执行类型
Status int8 `json:"status"` 
}

所以我选择把 TaskCommand 替换成跟业务更贴切的名称👇👇👇


type RecallDataParam struct {
Type   int8 `json:"type"` // 执行类型
Status int8 `json:"status"`
}

引入参数对象

以一个对象取代一些参数,可以改善代码的可读性和维护性,尤其是在函数参数列表较长或者参数之间存在复杂关系的情况下。将一组相关的参数封装到一个对象中,将该对象作为函数的参数传递,简化函数签名并提高代码的清晰度。


在一些历史比较久的代码里过长参数真的很常见,从 controller 透传到 service 再透传到 repo 层,代码复用性也非常低。


type AppImpl struct {
}

func (app *AppImpl) List(tp []int, status, page, pageSize int, keyword string, domain string) ([]interface{}, error) {
// .... 省略业务逻辑
return nil, nil
}

上段代码我一般会在 controller 和 service 中间抽一个 dto 实体。👇👇👇


type AppImpl struct {
}

func (app *AppImpl) List(listDTO *ListDTO) ([]interface{}, error) {
// .... 省略业务逻辑
return nil, nil
}

type ListDTO struct {
tp []int
status, page, pageSize int
keyword, domain string
}

提炼类

一个类应该是一个明确的抽象,它的职责是单一的,只处理一些明确的职责。


提炼类一般是下面两种情况




  1. 需求是在不停变化和累加,你会这儿加一个函数,那儿加一个方法。导致某些文件或者类非常臃肿。

  2. 相似的能力,散落在不同的业务板块,涉及的开发都在重复建设,有一个需求建一个烟囱。



典型案例是项目中事件上报能力,本应该是一个通用能力集中收敛上报代码,据我梳理代码散落在多处,上报触点有 10 来个,每个触点都在写同样的上报代码,假设某一天上报逻辑变化必须在这 10 多处做出许多小修改。


所以我决定把上报能力收敛在一个类,将复杂逻辑封装到该类,定义有限参数露出给使用方。


上报通用能力封装在 EventTracking。👇👇👇


// Tracker 埋点上报接口
type Tracker[T any] interface {
EventTracking(in T) error
}

type CMSReachDTO struct {
}

type CMSReachTracking[T any] struct {
ctx context.Context
}

func NewCMSReachTracking[T any](ctx context.Context) Tracker[*CMSReachDTO] {
return &CMSReachTracking[T]{ctx: ctx}
}

func (t *CMSReachTracking[T]) EventTracking(in *CMSReachDTO) error {
// ....逻辑省略

return nil
}

提炼超类

如果两个类在做相似的事,可以利用基本的继承/组合(GO 只有组合)机制把它们的相似之处提炼到超类。一般会把字段、方法都搬移过去。


我遇到的 case 在 entity 上会多一些,比如下面这两个 struct。


type Task struct {
ID string `gorm:"column:id"`
Tenant string `gorm:"column:tenant"`
SubDomain string `gorm:"column:sub_domain"`
IsDel int8   `gorm:"column:is_del;default:1" `
CreateAt int64  `gorm:"column:create_at;autoCreateTime:milli"`
UpdateAt int64  `gorm:"column:update_at;autoUpdateTime:milli"`
CreateUserID string `gorm:"column:create_uid"`
UpdateUserID string `gorm:"column:update_uid"`
// ... 省略其他字段
}

type TaskDetail struct {
ID string `gorm:"column:id"`
TargetID string `gorm:"column:target_id"`
Tenant string `gorm:"column:tenant"`
SubDomain string `gorm:"column:sub_domain"`
IsDel int8   `gorm:"column:is_del;default:1" `
CreateAt int64  `gorm:"column:create_at;autoCreateTime:milli"`
UpdateAt int64  `gorm:"column:update_at;autoUpdateTime:milli"`
CreateUserID string `gorm:"column:create_uid"`
UpdateUserID string `gorm:"column:update_uid"`
// ... 省略其他字段
}

上面这段代码他们都有共性的代码,并且我非常熟悉业务是不可能更改的,所以我会提炼一个超类。👇👇👇


type SuperParty struct {
Tenant string `gorm:"column:tenant"`
SubDomain string `gorm:"column:sub_domain"`
IsDel int8   `gorm:"column:is_del;default:1" `
CreateAt int64  `gorm:"column:create_at;autoCreateTime:milli"`
UpdateAt int64  `gorm:"column:update_at;autoUpdateTime:milli"`
CreateUserID string `gorm:"column:create_uid"`
UpdateUserID string `gorm:"column:update_uid"`
}

type Task struct {
ID string `gorm:"column:id"`
SuperParty
// ... 省略其他字段
}

type TaskDetail struct {
ID string `gorm:"column:id"`
TargetID string `gorm:"column:target_id"`
SuperParty
// ... 省略其他字段
}

当然某些场景还有一些配套的方法,也可以一并搬迁到 SuperParty 里面。


提炼方法/函数

提炼函数/方法是我常用的一种手段,我不喜欢长函数/方法。我看过一个说法,一个函数/方法应该能在一屏中显示,我一直奉为经典语录(我写的代码函数/方法基本不会超过一百行);另外只要有一段代码不止被用一次,我就会把他们单独放进一个函数。


有这样一个场景,调用外部 byids 查询员工信息获取 externalId 执行业务逻辑,封装外部接口调用。


type Client struct {
ctx context.Context
}

func (c *Client) GetByIDs(id []string) ([]*User, error) {
// ....省略业务逻辑
return []*User{}, nil
}

type User struct {
ID         string `json:"id"`
ExternalID string `json:"externalId"`
}

下面是业务方使用 GetByIDs() 方法


func TestGetUserByIDs(t *testing.T) {
ids := []string{"1""2"}

client := &Client{}
users, err := client.GetByIDs(ids)
if err != nil {
panic(err)
}

l := make([]string0len(users))
for _, v := range users {
l = append(l, v.ExternalID)
}

// ...执行业务逻辑
}

 业务方调用 GetByIDs() 方法,遍历 users 获取 ExternalID 执行业务逻辑。在业务上这种操作还真不少,所以决定优化复用一部分代码。👇👇👇




  1. 定义 Users 切片。

  2. GetByIDs() 方法返回 Users。

  3. Users 提供 GetExternalIDs 方法。



type Client struct {
ctx context.Context
}

func (c *Client) GetByIDs(id []string) (Users, error) {
// .... 省略业务逻辑
return Users{}, nil
}

type Users []*User

func (u Users) GetExternalIDs() []string {
out := make([]string0len(u))
for _, v := range u {
out = append(out, v.ExternalID)
}

return out
}

type User struct {
ID         string `json:"id"`
ExternalID string `json:"externalId"`
}

下面是业务方使用 GetByIDs() 方法


func TestGetUserByIDs(t *testing.T) {
ids := []string{"1""2"}

client := &Client{}
users, err := client.GetByIDs(ids)
if err != nil {
panic(err)
}

l := users.GetExternalIDs()
// 执行业务逻辑
   // ....
}

代码空行

我非常非常不喜欢代码从头到尾写下来没有任何空行,难以阅读让读者很难提起兴趣。空行在我看来是必不可少的,在代码中使用空行来分隔不同功能或逻辑块之间的代码,空行使得代码更易读。


下面段代码是没有任何空行的,代码比较短阅读起来可能并不费劲。
image.png


适当进行空行优化后👇👇👇
企业微信截图_25861264-b69d-48eb-bd5c-eb2e4e9f87aa.png


上段代码先不关注逻辑,优化后可读性更强了,代码分为3段逻辑,每段逻辑都有各自的职责。


空行是用来区分不同逻辑块的,过度空行也会影响代码阅读,如下:
image.png


引入设计模式

设计模式是被大佬们验证过的、开发经验的总结,可以帮助我们更好地组织和管理代码,并提高代码的可维护性、可读性、可扩展性和可重用性。下面链接是我最常用的设计模式,也在这次重构过程中全部用上了,有兴趣可以看看。


最后总结




  1. 如果你的项目不是外包项目(交付了就完事儿),一定要多回头看看自己写的代码,跟着版本迭代持续优化和改进,你才能进步。另外对代码一定要有洁癖。

  2. 重构是持续的过程,如果是重要项目,每个版本我们都会推进代码优化,保证代码可维护性、可扩展性、另外就是高性能。千万别堆积最后,那可是大工程到后面很多人是没有决心干这个事儿的,所以大家应该平时迭代中不断优化和完善,才可持续性。

  3. 大型重构时,一定要明确收益并且是可量化的,比如重构后 qps 提升了10%,应用消耗资源降低了…等等,你才有跟老板谈判的筹码。



作者:彭亚川Allen
来源:juejin.cn/post/7344290391989485578
收起阅读 »

2024年的安卓现代开发

大家好 👋🏻, 新的一年即将开始, 我不想错过这个机会, 与大家分享一篇题为《2024年的安卓现代开发》的文章. 🚀 如果你错过了我的上一篇文章, 可以看看2023年的安卓现代开发并回顾一下今年的变化. 免责声明 📝 本文反映了我的个人观点和专业见解, 并参考...
继续阅读 »


大家好 👋🏻, 新的一年即将开始, 我不想错过这个机会, 与大家分享一篇题为《2024年的安卓现代开发》的文章. 🚀


如果你错过了我的上一篇文章, 可以看看2023年的安卓现代开发并回顾一下今年的变化.


免责声明


📝 本文反映了我的个人观点和专业见解, 并参考了 Android 开发者社区中的不同观点. 此外, 我还定期查看 Google 为 Android 提供的指南.


🚨 需要强调的是, 虽然我可能没有明确提及某些引人注目的工具, 模式和体系结构, 但这并不影响它们作为开发 Android 应用的宝贵替代方案的潜力.


Kotlin 无处不在 ❤️



Kotlin是由JetBrains开发的一种编程语言. 由谷歌推荐, 谷歌于 2017 年 5 月正式发布(查看这里). 它是一种与 Java 兼容的现代编程语言, 可以在 JVM 上运行, 这使得它在 Android 应用开发中的采用非常迅速.


无论你是不是安卓新手, 都应该把 Kotlin 作为首选, 不要逆流而上 🏊🏻 😎, 谷歌在 2019 年谷歌 I/O 大会上宣布了这一做法. 有了 Kotlin, 你就能使用现代语言的所有功能, 包括协程的强大功能和使用为 Android 生态系统开发的现代库.


请查看Kotlin 官方文档


Kotlin 是一门多用途语言, 我们不仅可以用它来开发 Android 应用, 尽管它的流行很大程度上是由于 Android 应用, 我们可以从下图中看到这一点.




KotlinConf ‘23


Kotlin 2.0 要来了


另一个需要强调的重要事件是Kotlin 2.0的发布, 它近在眼前. 截至本文发稿之日, 其版本为 2.0.0-beta4



新的 K2 编译器 也是 Kotlin 2.0 的另一个新增功能, 它将带来显著的性能提升, 加速新语言特性的开发, 统一 Kotlin 支持的所有平台, 并为多平台项目提供更好的架构.


请查看 KotlinConf '23 的回顾, 你可以找到更多信息.


Compose 🚀




Jetpack Compose 是 Android 推荐的用于构建本地 UI 的现代工具包. 它简化并加速了 Android 上的 UI 开发. 通过更少的代码, 强大的工具和直观的 Kotlin API, 快速实现你的应用.




Jetpack Compose 是 Android Jetpack 库的一部分, 使用 Kotlin 编程语言轻松创建本地UI. 此外, 它还与 LiveData 和 ViewModel 等其他 Android Jetpack 库集成, 使得构建具有强反应性和可维护性的 Android 应用变得更加容易.


Jetpack Compose 的一些主要功能包括



  1. 声明式UI

  2. 可定制的小部件

  3. 与现有代码(旧视图系统)轻松集成

  4. 实时预览

  5. 改进的性能.


资源:



Android Jetpack ⚙️




Jetpack是一套帮助开发者遵循最佳实践, 减少模板代码, 编写在不同Android版本和设备上一致运行的代码的库, 这样开发者就可以专注于他们的业务代码.


_ Android Jetpack 文档



其中最常用的工具有:



Material You / Material Design 🥰



Material You 是在 Android 12 中引入并在 Material Design 3 中实现的一项新的定制功能, 它使用户能够根据个人喜好定制操作系统的视觉外观. Material Design 是一个由指南, 组件和工具组成的可调整系统, 旨在坚持UI设计的最高标准. 在开源代码的支持下, Material Design 促进了设计师和开发人员之间的无缝协作, 使团队能够高效地创造出令人惊叹的产品.



目前, Material Design 的最新版本是 3, 你可以在这里查看更多信息. 此外, 你还可以利用Material Theme Builder来帮助你定义应用的主题.


代码仓库


使用 Material 3 创建主题


SplashScreen API



Android 中的SplashScreen API对于确保应用在 Android 12 及更高版本上正确显示至关重要. 不更新会影响应用的启动体验. 为了在最新版本的操作系统上获得一致的用户体验, 快速采用该 API 至关重要.


Clean架构



Clean架构的概念是由Robert C. Martin提出的. 它的基础是通过将软件划分为不同层次来实现责任分离.


特点



  1. 独立于框架.

  2. 可测试.

  3. 独立于UI

  4. 独立于数据库

  5. 独立于任何外部机构.


依赖规则


作者在他的博文Clean代码中很好地描述了依赖规则.



依赖规则是使这一架构得以运行的首要规则. 这条规则规定, 源代码的依赖关系只能指向内部. 内圈中的任何东西都不能知道外圈中的任何东西. 特别是, 外圈中声明的东西的名称不能被内圈中的代码提及. 这包括函数, 类, 变量或任何其他命名的软件实体.




安卓中的Clean架构:


Presentation 层: Activitiy, Composable, Fragment, ViewModel和其他视图组件.
Domain层: 用例, 实体, 仓库接口和其他Domain组件.
Data层: 仓库的实现类, Mapper, DTO 等.


Presentation层的架构模式


架构模式是一种更高层次的策略, 旨在帮助设计软件架构, 其特点是在一个可重复使用的框架内为常见的架构问题提供解决方案. 架构模式与设计模式类似, 但它们的规模更大, 解决的问题也更全面, 如系统的整体结构, 组件之间的关系以及数据管理的方式等.


在Presentation层中, 我们有一些架构模式, 我想重点介绍以下几种:



  • MVVM

  • MVI


我不想逐一解释, 因为在互联网上你可以找到太多相关信息. 😅


此外, 你还可以查看应用架构指南.



依赖注入


依赖注入是一种软件设计模式, 它允许客户端从外部获取依赖关系, 而不是自己创建依赖关系. 它是一种在对象及其依赖关系之间实现控制反转(IoC)的技术.



模块化


模块化是一种软件设计技术, 可将应用划分为独立的模块, 每个模块都有自己的功能和职责.



模块化的优势


可重用性: 拥有独立的模块, 就可以在应用的不同部分甚至其他应用中重复使用.


严格的可见性控制: 模块可以让你轻松控制向代码库其他部分公开的内容.


自定义交付: Play特性交付 使用应用Bundle的高级功能, 允许你有条件或按需交付应用的某些功能.


可扩展性: 通过独立模块, 可以添加或删除功能, 而不会影响应用的其他部分.


易于维护: 将应用划分为独立的模块, 每个模块都有自己的功能和责任, 这样就更容易理解和维护代码.


易于测试: 有了独立的模块, 就可以对它们进行隔离测试, 从而便于发现和修复错误.


改进架构: 模块化有助于改进应用的架构, 从而更好地组织和结构代码.


改善协作: 通过独立的模块, 开发人员可以不受干扰地同时开发应用的不同部分.


构建时间: 一些 Gradle 功能, 如增量构建, 构建缓存或并行构建, 可以利用模块化提高构建性能.


更多信息请参阅官方文档.


网络



序列化


在本节中, 我想提及两个我认为非常重要的工具: 与 Retrofit 广泛结合使用的Moshi, 以及 JetBrains 的 Kotlin 团队押宝的Kotlin Serialization.



MoshiKotlin Serialization 是用于 Kotlin/Java 的两个序列化/反序列化库, 可将对象转换为 JSON 或其他序列化格式, 反之亦然. 这两个库都提供了友好的接口, 并针对移动和桌面应用进行了优化. Moshi 主要侧重于 JSON 序列化, 而 Kotlin Serialization 则支持包括 JSON 在内的多种序列化格式.


图像加载



要从互联网上加载图片, 有几个第三方库可以帮助你处理这一过程. 图片加载库为你做了很多繁重的工作; 它们既处理缓存(这样你就不用多次下载图片), 也处理下载图片并将其显示在屏幕上的网络逻辑.


_ 官方安卓文档




响应/线程管理




说到响应式编程和异步进程, Kotlin协程凭借其suspend函数和Flow脱颖而出. 然而, 在 Android 应用开发中, 承认 RxJava 的价值也是至关重要的. 尽管协程和 Flow 的采用率越来越高, 但 RxJava 仍然是多个项目中稳健而受欢迎的选择.


对于新项目, 请始终选择Kotlin协程❤️. 可以在这里探索一些Kotlin协程相关的概念.


本地存储


在构建移动应用时, 重要的一点是能够在本地持久化数据, 例如一些会话数据或缓存数据等. 根据应用的需求选择合适的存储选项非常重要. 我们可以存储键值等非结构化数据, 也可以存储数据库等结构化数据. 请记住, 这一点并没有提到我们可用的所有本地存储类型(如文件存储), 只是提到了允许我们保存数据的工具.



建议:



测试 🕵🏼



软件开发中的测试对于确保产品质量至关重要. 它能检测错误, 验证需求并确保客户满意. 下面是一些最常用的测试工具::



工具文档的测试部分


截屏测试 📸



Android 中的截屏测试涉及自动捕获应用中各种 UI 元素的屏幕截图, 并将其与基线图像进行比较, 以检测任何意外的视觉变化. 它有助于确保不同版本和配置的应用具有一致的UI外观, 并在开发过程的早期捕捉视觉回归.



R8 优化


R8 是默认的编译器, 可将项目的 Java 字节码转换为可在 Android 平台上运行的 DEX 格式. 通过缩短类名及其属性, 它可以帮助我们混淆和减少应用的代码, 从而消除项目中未使用的代码和资源. 要了解更多信息, 请查看有关 缩减, 混淆和优化应用 的 Android 文档. 此外, 你还可以通过ProGuard规则文件禁用某些任务或自定义 R8 的行为.




  • 代码缩减

  • 缩减资源

  • 混淆

  • 优化


第三方工具



  • DexGuard


Play 特性交付





Google Play 的应用服务模式称为动态交付, 它使用 Android 应用Bundle为每个用户的设备配置生成并提供优化的 APK, 因此用户只需下载运行应用所需的代码和资源.




自适应布局



随着具有不同外形尺寸的移动设备使用量的增长, 我们需要一些工具, 使我们的 Android 应用能够适应不同类型的屏幕. 因此, Android 为我们提供了Window Size Class, 简单地说, 就是三大类屏幕格式, 它们是我们开发设计的关键点. 这样, 我们就可以避免考虑许多屏幕设计的复杂性, 从而将可能性减少到三组, 它们是 Compat, MediumExpanded.


Window Size Class




支持不同的屏幕尺寸


我们拥有的另一个重要资源是Canonical Layout, 它是预定义的屏幕设计, 可用于 Android 应用中的大多数场景, 还为我们提供了如何将其适用于大屏幕的指南.



其他相关资源



Form-Factor培训


本地化 🌎



本地化包括调整产品以满足不同地区不同受众的需求. 这包括翻译文本, 调整格式和考虑文化因素. 其优势包括进入全球市场, 增强用户体验, 提高客户满意度, 增强在全球市场的竞争力以及遵守当地法规.


注: BCP 47 是安卓系统使用的国际化标准.


参考资料



性能 🔋⚙️



在开发 Android 应用时, 我们必须确保用户体验更好, 这不仅体现在应用的开始阶段, 还体现在整个执行过程中. 因此, 我们必须使用一些工具, 对可能影响应用性能的情况进行预防性分析和持续监控:



应用内更新



当你的用户在他们的设备上不断更新你的应用时, 他们可以尝试新功能, 并从性能改进和错误修复中获益. 虽然有些用户会在设备连接到未计量的连接时启用后台更新, 但其他用户可能需要提醒才能安装更新. 应用内更新是 Google Play 核心库的一项功能, 可提示活跃用户更新你的应用*.


运行 Android 5.0(API 等级 21)或更高版本的设备支持应用内更新功能. 此外, 应用内更新仅支持 Android 移动设备, Android 平板电脑和 Chrome OS 设备.


- 应用内更新文档




应用内评论



Google Play 应用内评论 API 可让你提示用户提交 Play Store 评级和评论, 而无需离开你的应用或游戏.


一般来说, 应用内评论流程可在用户使用应用的整个过程中随时触发. 在流程中, 用户可以使用 1-5 星系统对你的应用进行评分, 并添加可选评论. 一旦提交, 评论将被发送到 Play Store 并最终显示出来.


*为保护用户隐私并避免 API 被滥用, 你的应用应严格遵守有关何时请求应用内评论评论提示的设计的规定.


- 应用内评论文档




可观察性 👀



在竞争日益激烈的应用生态系统中, 要获得良好的用户体验, 首先要确保应用没有错误. 确保应用无错误的最佳方法之一是在问题出现时立即发现并知道如何着手解决. 使用 Android Vitals 来确定应用中崩溃和响应速度问题最多的区域. 然后, 利用 Firebase Crashlytics 中的自定义崩溃报告获取更多有关根本原因的详细信息, 以便有效地排除故障.


工具



辅助功能



辅助功能是软件设计和构建中的一项重要功能, 除了改善用户体验外, 还能让有辅助功能需求的人使用应用. 这一概念旨在改善的不能包括: 有视力问题, 色盲, 听力问题, 灵敏性问题和认知障碍等.


考虑因素:



  • 增加文字的可视性(颜色对比度, 可调整文字大小)

  • 使用大而简单的控件

  • 描述每个UI元素


更多详情请查看辅助功能 - Android 文档


安全性 🔐



在开发保护设备完整性, 数据安全性和用户信任的应用时, 安全性是我们必须考虑的一个方面, 甚至是最重要的方面.



  • 使用凭证管理器登录用户: 凭据管理器 是一个 Jetpack API, 在一个单一的 API 中支持多种登录方法, 如用户名和密码, 密码匙和联合登录解决方案(如谷歌登录), 从而简化了开发人员的集成.

  • 加密敏感数据和文件: 使用EncryptedSharedPreferencesEncryptedFile.

  • 应用基于签名的权限: 在你可控制的应用之间共享数据时, 使用基于签名的权限.


android="http://schemas.android.com/apk/res/android"
package="com.example.myapp">
name="my_custom_permission_name"
android:protectionLevel="signature" />


  • 不要将密钥, 令牌或应用配置所需的敏感数据直接放在项目库中的文件或类中. 请使用local.properties.

  • 实施 SSL Pinning: 使用 SSL Pinning 进一步确保应用与远程服务器之间的通信安全. 这有助于防止中间人攻击, 并确保只与拥有特定 SSL 证书的受信任服务器进行通信.


res/xml/network_security_config.xml


"1.0" encoding="utf-8"?>
<network-security-config>
<domain-config>
<domain includeSubdomains="true">example.comdomain>
<pin-set expiration="2018-01-01">
<pin digest="SHA-256">ReplaceWithYourPinpin>

<pin digest="SHA-256">ReplaceWithYourPinpin>
pin-set>
domain-config>
network-security-config>


  • 实施运行时应用自我保护(RASP): 这是一种在运行时保护应用免受攻击和漏洞的安全技术. RASP 的工作原理是监控应用的行为, 并检测可能预示攻击的可疑活动:



    • 代码混淆.

    • 根检测.

    • 篡改/应用钩子检测.

    • 防止逆向工程攻击.

    • 反调试技术.

    • 虚拟环境检测

    • 应用行为的运行时分析.




想了解更多信息, 请查看安卓应用中的运行时应用自我保护技术(RASP). 还有一些 Android 安全指南.


版本目录


Gradle 提供了一种集中管理项目依赖关系的标准方式, 叫做版本目录; 它在 7.0 版中被试验性地引入, 并在 7.4 版中正式发布.


优点:



  • 对于每个目录, Gradle 都会生成类型安全的访问器, 这样你就可以在 IDE 中通过自动补全轻松添加依赖关系.

  • 每个目录对构建过程中的所有项目都是可见的. 它是声明依赖项版本的中心位置, 并确保该版本的变更适用于每个子项目.

  • 目录可以声明 dependency bundles, 即通常一起使用的 "依赖关系组".

  • 目录可以将依赖项的组和名称与其实际版本分开, 而使用版本引用, 这样就可以在多个依赖项之间共享一个版本声明.


请查看更多信息


Secret Gradle 插件


Google 强烈建议不要将 API key 输入版本控制系统. 相反, 你应该将其存储在本地的 secrets.properties 文件中, 该文件位于项目的根目录下, 但不在版本控制范围内, 然后使用 Secrets Gradle Plugin for Android 来读取 API 密钥.


日志


日志是一种软件工具, 用于记录程序的执行信息, 重要事件, 错误调试信息以及其他有助于诊断问题或了解程序运行情况的信息. 日志可配置为将信息写入不同位置, 如日志文件, 控制台, 数据库, 或将信息发送到日志记录服务器.



Linter / 静态代码分析器



Linter 是一种编程工具, 用于分析程序源代码, 查找代码中的潜在问题或错误. 这些问题可能是语法问题, 代码风格不当, 缺乏文档, 安全问题等, 它们会对代码的质量和可维护性产生影响.



Google Play Instant



Google Play Instant 使本地应用和游戏无需安装即可在运行 Android 5.0(API 等级 21)或更高版本的设备上启动. 你可以使用 Android Studio 构建这类体验, 称为即时应用即时游戏. 通过允许用户运行即时应用或即时游戏(即提供即时体验), 你可以提高应用或游戏的发现率, 从而有助于吸引更多活跃用户或安装.



新设计中心



安卓团队提供了一个新的设计中心, 帮助创建美观, 现代的安卓应用, 这是一个集中的地方, 可以了解安卓在多种形态因素方面的设计.


点击查看新的设计中心


人工智能



GeminiPalM 2是谷歌开发的两个最先进的人工智能(AI)模型, 它们将改变安卓应用开发的格局. 这些模型具有一系列优势, 将推动应用的效率, 用户体验和创新.



人工智能编码助手工具


Studio Bot



Studio Bot 是你的 Android 开发编码助手. 它是 Android Studio 中的一种对话体验, 通过回答 Android 开发问题帮助你提高工作效率. 它采用人工智能技术, 能够理解自然语言, 因此你可以用简单的英语提出开发问题. Studio Bot 可以帮助 Android 开发人员生成代码, 查找相关资源, 学习最佳实践并节省时间.


Studio Bot



Github Copilot


GitHub Copilot 是一个人工智能配对程序员. 你可以使用 GitHub Copilot 直接在编辑器中获取整行或整个函数的建议.


Amazon CodeWhisperer


这是亚马逊的一项服务, 可根据你当前代码的上下文生成代码建议. 它能帮助你编写更高效, 更安全的代码, 并发现新的 API 和工具.


Kotlin Multiplatform 🖥 📱⌚️ 🥰 🚀



最后, 同样重要的是, Kotlin Multiplatform 🎉 是本年度最引人注目的新产品. 它是跨平台应用开发领域的有力竞争者. 虽然我们的主要重点可能是 Android 应用开发, 但 Kotlin Multiplatform 为我们提供了利用 KMP 框架制作完全本地 Android 应用的灵活性. 这一战略举措不仅为我们的项目提供了未来保障, 还为我们提供了必要的基础架构, 以便在我们选择多平台环境时实现无缝过渡. 🚀


如果你想深入了解 Kotlin Multiplatform, 我想与你分享这几篇文章. 在这些文章中, 探讨了这项技术的现状及其对现代软件开发的影响:



作者:bytebeats
来源:juejin.cn/post/7342861726000791603
收起阅读 »

H5唤起APP路子

web
前一段时间在做一些H5页面,需求中落地页占比较大,落地页承担的职责就是引流。引流有两种形式,同时也是我们对唤端的定义:引导已下载用户打开APP,引导未下载用户下载APP。 引导已下载用户打开APP,从数据上说用户停留在APP中的时间更多了,是在提高用户粘性;从...
继续阅读 »

前一段时间在做一些H5页面,需求中落地页占比较大,落地页承担的职责就是引流。引流有两种形式,同时也是我们对唤端的定义:引导已下载用户打开APP,引导未下载用户下载APP。


引导已下载用户打开APP,从数据上说用户停留在APP中的时间更多了,是在提高用户粘性;从体验上说,APP体验是要比H5好的。引导未下载用户下载APP,可以增加我们的用户量。


上面其实分别解释了 什么是唤端 以及 为什么要唤端,也就是 3W法则 中的 What 和 Why,那么接下来我们就要聊一聊 How 了,也就是 如何唤端


我们先来看看常见的唤端方式以及他们适用的场景:


唤端媒介


URL Scheme


来源


我们的手机上有许多私密信息,联系方式、照片、银彳亍卡信息...我们不希望这些信息可以被手机应用随意获取到,信息泄露的危害甚大。所以,如何保证个人信息在设备所有者知情并允许的情况下被使用,是智能设备的核心安全问题。


对此,苹果使用了名为 沙盒 的机制:应用只能访问它声明可能访问的资源。但沙盒也阻碍了应用间合理的信息共享,某种程度上限制了应用的能力。


因此,我们急需要一个辅助工具来帮助我们实现应用通信, URL Scheme 就是这个工具。


URL Scheme 是什么


我们来看一下 URL 的组成:

[scheme:][//authority][path][?query][#fragment]

我们拿 https://www.baidu.com 来举例,scheme 自然就是 https 了。


就像给服务器资源分配一个 URL,以便我们去访问它一样,我们同样也可以给手机APP分配一个特殊格式的 URL,用来访问这个APP或者这个APP中的某个功能(来实现通信)。APP得有一个标识,好让我们可以定位到它,它就是 URL 的 Scheme 部分。


常用APP的 URL Scheme


APP微信支付宝淘宝微博QQ知乎短信
URL Schemeweixin://alipay://taobao://sinaweibo://mqq://zhihu://sms://

URL Scheme 语法


上面表格中都是最简单的用于打开 APP 的 URL Scheme,下面才是我们常用的 URL Scheme 格式:

     行为(应用的某个功能)    
|
scheme://[path][?query]
| |
应用标识 功能需要的参数

Intent


安卓的原生谷歌浏览器自从 chrome25 版本开始对于唤端功能做了一些变化,URL Scheme 无法再启动Android应用。 例如,通过 iframe 指向 weixin://,即使用户安装了微信也无法打开。所以,APP需要实现谷歌官方提供的 intent: 语法,或者实现让用户通过自定义手势来打开APP,当然这就是题外话了。


Intent 语法

intent:
HOST/URI-path // Optional host
#Intent;
package=[string];
action=[string];
category=[string];
component=[string];
scheme=[string];
end;

如果用户未安装 APP,则会跳转到系统默认商店。当然,如果你想要指定一个唤起失败的跳转地址,添加下面的字符串在 end; 前就可以了:

S.browser_fallback_url=[encoded_full_url]

示例


下面是打开 Zxing 二维码扫描 APP 的 intent。

intent:
//scan/
#Intent;
package=com.google.zxing.client.android;
scheme=zxing;
end;

打开这个 APP ,可以通过如下的方式:

 <a href="intent://scan/#Intent;scheme=zxing;package=com.google.zxing.client.android;S.browser_fallback_url=http%3A%2F%2Fzxing.org;end"> Take a QR code a>

Universal Link


Universal Link 是什么


Universal Link 是苹果在 WWDC2015 上为 iOS9 引入的新功能,通过传统的 HTTP 链接即可打开 APP。如果用户未安装 APP,则会跳转到该链接所对应的页面。


为什么要使用 Universal Link


传统的 Scheme 链接有以下几个痛点:



  • 在 ios 上会有确认弹窗提示用户是否打开,对于用户来说唤端,多出了一步操作。若用户未安装 APP ,也会有一个提示窗,告知我们 “打不开该网页,因为网址无效”

  • 传统 Scheme 跳转无法得知唤端是否成功,Universal Link 唤端失败可以直接打开此链接对应的页面

  • Scheme 在微信、微博、QQ浏览器、手百中都已经被禁止使用,使用 Universal Link 可以避开它们的屏蔽( 截止到 18年8月21日,微信和QQ浏览器已经禁止了 Universal Link,其他主流APP未发现有禁止 )


如何让 APP 支持 Universal Link


有大量的文章会详细的告诉我们如何配置,你也可以去看官方文档,我这里简单的写一个12345。



  1. 拥有一个支持 https 的域名

  2. 开发者中心 ,Identifiers 下 AppIDs 找到自己的 App ID,编辑打开 Associated Domains 服务。

  3. 打开工程配置中的 Associated Domains ,在其中的 Domains 中填入你想支持的域名,必须以 applinks: 为前缀

  4. 配置 apple-app-site-association 文件,文件名必须为 apple-app-site-association不带任何后缀

  5. 上传该文件到你的 HTTPS 服务器的 根目录 或者 .well-known 目录下


Universal Link 配置中的坑


这里放一下我们在配置过程中遇到的坑,当然首先你在配置过程中必须得严格按照上面的要求去做,尤其是加粗的地方。



  1. 跨域问题


    IOS 9.2 以后,必须要触发跨域才能支持 Universal Link 唤端。


    IOS 那边有这样一个判断,如果你要打开的 Universal Link 和 当前页面是同一域名,ios 尊重用户最可能的意图,直接打开链接所对应的页面。如果不在同一域名下,则在你的 APP 中打开链接,也就是执行具体的唤端操作。


  2. Universal Link 是空页面


    Universal Link 本质上是个空页面,如果未安装 APP,Universal Link 被当做普通的页面链接,自然会跳到 404 页面,所以我们需要将它绑定到我们的中转页或者下载页。



如何调用三种唤端媒介


通过前面的介绍,我们可以发现,无论是 URL Scheme 还是 Intent 或者 Universal Link ,他们都算是 URL ,只是 URL Scheme 和 Intent 算是特殊的 URL。所以我们可以拿使用 URL 的方法来使用它们。


iframe

<iframe src="sinaweibo://qrcode">

在只有 URL Scheme 的日子里,iframe 是使用最多的了。因为在未安装 app 的情况下,不会去跳转错误页面。但是 iframe 在各个系统以及各个应用中的兼容问题还是挺多的,不能全部使用 URL Scheme。


a 标签

<a href="intent://scan/#Intent;scheme=zxing;package=com.google.zxing.client.android;end"">扫一扫</a>

前面我们提到 Intent 协议,官方给出的用例使用的就是使用的 a 标签,所以我们跟着一起用就可以了


使用过程中,对于动态生成的 a 标签,使用 dispatch 来模拟触发点击事件,发现很多种 event 传递过去都无效;使用 click() 来模拟触发,部分场景下存在这样的情况,第一次点击过后,回到原先页面,再次点击,点击位置和页面所识别位置有不小的偏移,所以 Intent 协议从 a 标签换成了 window.location。


window.location


URL Scheme 在 ios 9+ 上诸如 safari、UC、QQ浏览器中, iframe 均无法成功唤起 APP,只能通过 window.location 才能成功唤端。


当然,如果我们的 app 支持 Universal Link,ios 9+ 就用不到 URL Scheme 了。而 Universal Link 在使用过程中,我发现在 qq 中,无论是 iframe 导航 还是 a 标签打开 又或者 window.location 都无法成功唤端,一开始我以为是 qq 和微信一样禁止了 Universal Link 唤端的功能,其实不然,百般试验下,通过 top.location 唤端成功了。


判断唤端是否成功


如果唤端失败(APP 未安装),我们总是要做一些处理的,可以是跳转下载页,可以是 ios 下跳转 App Store... 但是Js 并不能提供给我们获取 APP 唤起状态的能力,Android Intent 以及 Universal Link 倒是不用担心,它们俩的自身机制允许它们唤端失败后直接导航至相应的页面,但是 URL Scheme 并不具备这样的能力,所以我们只能通过一些很 hack 的方式来实现 APP 唤起检测功能。

// 一般情况下是 visibilitychange 
const visibilityChangeProperty = getVisibilityChangeProperty();
const timer = setTimeout(() => {
const hidden = isPageHidden();
if (!hidden) {
cb();
}
}, timeout);

if (visibilityChangeProperty) {
document.addEventListener(visibilityChangeProperty, () => {
clearTimeout(timer);
});

return;
}

window.addEventListener('pagehide', () => {
clearTimeout(timer);
});

APP 如果被唤起的话,页面就会进入后台运行,会触发页面的 visibilitychange 事件。如果触发了,则表明页面被成功唤起,及时调用 clearTimeout ,清除页面未隐藏时的失败函数(callback)回调。


当然这个事件是有兼容性的,具体的代码实现时做了事件是否需要添加前缀(比如 -webkit- )的校验。如果都不兼容,我们将使用 pagehide 事件来做兜底处理。


没有完美的方案


透过上面的几个点,我们可以发现,无论是 唤端媒介调用唤端媒介 还是 判断唤端结果 都没有一个十全十美的方法,我们在代码层上能做的只是在确保最常用的场景(比如 微信、微博、手百 等)唤端无误的情况下,最大化的兼容剩余的场景。


好的,我们接下来扯一些代码以外的,让我们的 APP 能够在更多的平台唤起。




  • 微信、微博、手百、QQ浏览器等。


    这些应用能阻止唤端是因为它们直接屏蔽掉了 URL Scheme 。接下来可能就有看官疑惑了,微信中是可以打开大众点评的呀,微博里面可以打开优酷呀,那是如何实现的呢?


    它们都各自维护着一个白名单,如果你的域名在白名单内,那这个域名下所有的页面发起的 URL Scheme 就都会被允许。就像微信,如果你是腾讯的“家属”,你就可以加入白名单了,微信的白名单一般只包含着“家属”,除此外很难申请到白名单资质。但是微博之类的都是可以联系他们的渠道童鞋进行申请的,只是条件各不相同,比如微博的就是在你的 APP 中添加打开微博的入口,三个月内唤起超过 100w 次,就可以加入白名单了。




  • 腾讯应用宝直接打开 APP 的某个功能


    刚刚我们说到,如果你不是微信的家属,那你是很难进入白名单的,所以在安卓中我们一般都是直接打开腾讯应用宝,ios 中 直接打开 App Store。点击腾讯应用宝中的“打开”按钮,可以直接唤起我们的 APP,但是无法打开 APP 中的某个功能(就是无法打开指定页面)。


    腾讯应用宝对外开放了一个叫做 APP Link 的申请,只要你申请了 APP Link,就可以通过在打开应用宝的时候在应用宝地址后面添加上 &android_schema={your_scheme} ,来打开指定的页面了。




开箱即用的callapp-lib


信息量很大!各种问题得自己趟坑验证!内心很崩溃!


不用愁,已经为你准备好了药方,只需照方抓药即可😏 —— npm 包 callapp-lib


你也可以通过 script 直接加载 cdn 文件:

<script src="https://unpkg.com/callapp-lib"></script>

它能在大部分的环境中成功唤端,而且炒鸡简单啊,拿过去就可以用啊,还支持很多扩展功能啊,快来瞅瞅它的 文档 啊~~~


作者:阳光多一些
链接:juejin.cn/post/7348249728939130907
收起阅读 »

Mysql中Varchar(50)和varchar(500)区别是什么?

Mysql中Varchar(50)和varchar(500)区别是什么? 一. 问题描述 我们在设计表结构的时候,设计规范里面有一条如下规则: 对于可变长度的字段,在满足条件的前提下,尽可能使用较短的变长字段长度。 为什么这么规定,我在网上查了一下,主要基...
继续阅读 »

Mysql中Varchar(50)和varchar(500)区别是什么?


一. 问题描述


我们在设计表结构的时候,设计规范里面有一条如下规则:



  • 对于可变长度的字段,在满足条件的前提下,尽可能使用较短的变长字段长度。


为什么这么规定,我在网上查了一下,主要基于两个方面



  • 基于存储空间的考虑

  • 基于性能的考虑


网上说Varchar(50)和varchar(500)存储空间上是一样的,真的是这样吗?


基于性能考虑,是因为过长的字段会影响到查询性能?


本文我将带着这两个问题探讨验证一下


二.验证存储空间区别


1.准备两张表


CREATE TABLE `category_info_varchar_50` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
`name` varchar(50) NOT NULL COMMENT '分类名称',
`is_show` tinyint(4) NOT NULL DEFAULT '0' COMMENT '是否展示:0 禁用,1启用',
`sort` int(11) NOT NULL DEFAULT '0' COMMENT '序号',
`deleted` tinyint(1) DEFAULT '0' COMMENT '是否删除',
`create_time` datetime NOT NULL COMMENT '创建时间',
`update_time` datetime NOT NULL COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_name` (`name`) USING BTREE COMMENT '名称索引'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='分类';

CREATE TABLE `category_info_varchar_500` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
`name` varchar(500) NOT NULL COMMENT '分类名称',
`is_show` tinyint(4) NOT NULL DEFAULT '0' COMMENT '是否展示:0 禁用,1启用',
`sort` int(11) NOT NULL DEFAULT '0' COMMENT '序号',
`deleted` tinyint(1) DEFAULT '0' COMMENT '是否删除',
`create_time` datetime NOT NULL COMMENT '创建时间',
`update_time` datetime NOT NULL COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_name` (`name`) USING BTREE COMMENT '名称索引'
) ENGINE=InnoDB AUTO_INCREMENT=288135 DEFAULT CHARSET=utf8mb4 COMMENT='分类';

2.准备数据


给每张表插入相同的数据,为了凸显不同,插入100万条数据


DELIMITER $$
CREATE PROCEDURE batchInsertData(IN total INT)
BEGIN
DECLARE start_idx INT DEFAULT 1;
DECLARE end_idx INT;
DECLARE batch_size INT DEFAULT 500;
DECLARE insert_values TEXT;

SET end_idx = LEAST(total, start_idx + batch_size - 1);

WHILE start_idx <= total DO
SET insert_values = '';
WHILE start_idx <= end_idx DO
SET insert_values = CONCAT(insert_values, CONCAT('(\'name', start_idx, '\', 0, 0, 0, NOW(), NOW()),'));
SET start_idx = start_idx + 1;
END WHILE;
SET insert_values = LEFT(insert_values, LENGTH(insert_values) - 1); -- Remove the trailing comma
SET @sql = CONCAT('INSERT INTO category_info_varchar_50 (name, is_show, sort, deleted, create_time, update_time) VALUES ', insert_values, ';');

PREPARE stmt FROM @sql;
EXECUTE stmt;
SET @sql = CONCAT('INSERT INTO category_info_varchar_500 (name, is_show, sort, deleted, create_time, update_time) VALUES ', insert_values, ';');
PREPARE stmt FROM @sql;
EXECUTE stmt;

SET end_idx = LEAST(total, start_idx + batch_size - 1);
END WHILE;
END$$
DELIMITER ;

CALL batchInsertData(1000000);

3.验证存储空间


查询第一张表SQL


SELECT
table_schema AS "数据库",
table_name AS "表名",
table_rows AS "记录数",
TRUNCATE ( data_length / 1024 / 1024, 2 ) AS "数据容量(MB)",
TRUNCATE ( index_length / 1024 / 1024, 2 ) AS "索引容量(MB)"
FROM
informati0n—schema.TABLES
WHERE
table_schema = 'test_mysql_field'
and TABLE_NAME = 'category_info_varchar_50'
ORDER BY
data_length DESC,
index_length DESC;

查询结果


image.png


查询第二张表SQL


SELECT
table_schema AS "数据库",
table_name AS "表名",
table_rows AS "记录数",
TRUNCATE ( data_length / 1024 / 1024, 2 ) AS "数据容量(MB)",
TRUNCATE ( index_length / 1024 / 1024, 2 ) AS "索引容量(MB)"
FROM
informati0n—schema.TABLES
WHERE
table_schema = 'test_mysql_field'
and TABLE_NAME = 'category_info_varchar_500'
ORDER BY
data_length DESC,
index_length DESC;

查询结果


image.png


4.结论


两张表在占用空间上确实是一样的,并无差别


三.验证性能区别


1.验证索引覆盖查询


select name from category_info_varchar_50 where name = 'name100000'
-- 耗时0.012s
select name from category_info_varchar_500 where name = 'name100000'
-- 耗时0.012s
select name from category_info_varchar_50 order by name;
-- 耗时0.370s
select name from category_info_varchar_500 order by name;
-- 耗时0.379s

通过索引覆盖查询性能差别不大


1.验证索引查询


select * from category_info_varchar_50 where name = 'name100000'
--耗时 0.012s
select * from category_info_varchar_500 where name = 'name100000'
--耗时 0.012s
select * from category_info_varchar_50 where name in('name100','name1000','name100000','name10000','name1100000',
'name200','name2000','name200000','name20000','name2200000','name300','name3000','name300000','name30000','name3300000',
'name400','name4000','name400000','name40000','name4400000','name500','name5000','name500000','name50000','name5500000',
'name600','name6000','name600000','name60000','name6600000','name700','name7000','name700000','name70000','name7700000','name800',
'name8000','name800000','name80000','name6600000','name900','name9000','name900000','name90000','name9900000')
-- 耗时 0.011s -0.014s
-- 增加 order by name 耗时 0.012s - 0.015s

select * from category_info_varchar_50 where name in('name100','name1000','name100000','name10000','name1100000',
'name200','name2000','name200000','name20000','name2200000','name300','name3000','name300000','name30000','name3300000',
'name400','name4000','name400000','name40000','name4400000','name500','name5000','name500000','name50000','name5500000',
'name600','name6000','name600000','name60000','name6600000','name700','name7000','name700000','name70000','name7700000','name800',
'name8000','name800000','name80000','name6600000','name900','name9000','name900000','name90000','name9900000')
-- 耗时 0.012s -0.014s
-- 增加 order by name 耗时 0.014s - 0.017s

索引范围查询性能基本相同, 增加了order By后开始有一定性能差别;


3.验证全表查询和排序


全表无排序


image.png


image.png


全表有排序


select * from category_info_varchar_50 order by  name ;
--耗时 1.498s
select * from category_info_varchar_500 order by name ;
--耗时 4.875s

image.png
image.png


结论:


全表扫描无排序情况下,两者性能无差异,在全表有排序的情况下, 两种性能差异巨大;


分析原因


varchar50 全表执行sql分析

1711426760869.jpg
我发现86%的时花在数据传输上,接下来我们看状态部分,关注Created_tmp_files和sort_merge_passes
1711426760865.jpg


image.png
Created_tmp_files为3

sort_merge_passes为95


varchar500 全表执行sql分析

image.png


增加了临时表排序


image.png
image.png
Created_tmp_files 为 4

sort_merge_passes为645


关于sort_merge_passes, Mysql给出了如下描述:



Number of merge passes that the sort algorithm has had to do. If this value is large, you may want to increase the value of the sort_buffer_size.



其实sort_merge_passes对应的就是MySQL做归并排序的次数,也就是说,如果sort_merge_passes值比较大,说明sort_buffer和要排序的数据差距越大,我们可以通过增大sort_buffer_size或者让填入sort_buffer_size的键值对更小来缓解sort_merge_passes归并排序的次数。


四.最终结论


至此,我们不难发现,当我们最该字段进行排序或者其他聚合操作的时候,Mysql会根据该字段的设计的长度进行内存预估, 如果设计过大的可变长度, 会导致内存预估的值超出sort_buffer_size的大小, 导致mysql采用磁盘临时文件排序,最终影响查询性能;


作者:向显
来源:juejin.cn/post/7350228838151847976
收起阅读 »

华为自研的前端框架是什么样的?

web
大家好,我卡颂。 最近,华为开源了一款前端框架 —— openInula。根据官网提供的信息,这款框架有3大核心能力: 响应式API 兼容ReactAPI 官方提供6大核心组件 并且,在官方宣传视频里提到 —— 这是款大模型驱动的智能框架。 ...
继续阅读 »

大家好,我卡颂。


最近,华为开源了一款前端框架 —— openInula。根据官网提供的信息,这款框架有3大核心能力:



  1. 响应式API




  1. 兼容ReactAPI




  1. 官方提供6大核心组件



并且,在官方宣传视频里提到 —— 这是款大模型驱动智能框架


那么,这究竟是款什么样的前端框架呢?我在第一时间体验了Demo,阅读了框架源码,并采访了框架核心开发者。本文将包括两部分内容:



  1. 对框架核心开发者陈超涛的采访

  2. 卡颂作为一个老前端,阅读框架源码后的一些分析

采访核心开发者


开发Inula的初衷是?


回答:


华为内部对于业务中强依赖的软件,考虑到竞争力,通常会开发一个内部使用的版本。


Inula在华为内部,从立项到现在两年多,基本替换了公司内绝大部分React项目。



卡颂补充背景知识:Inula兼容React 95% API,最初开发的目的就是为了替换华为内部使用的React。为了方便理解,你可以将Inula类比于华为内部的React



为什么开源?


回答:


华为对于自研软件的公司策略,只要是公司内部做的,觉得还ok的自研都会开源。



接下来的提问涉及到官网宣传的内容



宣传片提到的大模型赋能、智能框架是什么意思?


回答:


这主要是Inula团队与其他外部团队在AI低代码方向的一些探索。比如:



  1. 团队与上海交大的一个团队在探索大模型赋能chrome调试业务代码方面有些合作,目的是为了自动定位问题

  2. 团队与华为内部的大模型编辑器团队合作,探索框架与编辑器定制可能性


以上还都属于探索阶段。


Inula未来有明确的发展方向么?


回答:


团队正在探索引入响应式API,相比于React的虚拟DOM方案,响应式API能够提高运行时性能。24年可能会从Vue composition API中寻求些借鉴。


新的发展方向会在项目仓库以RFC的形式展开。



补充:RFCRequest for Comments的缩写。这是一种协作模式,通常用于提出新的特性、规范或者改变现有的一些规则。RFC的目的是收集不同的意见和反馈,以便在最终确定一个决策前,考虑尽可能多的观点和影响。



为什么要自研核心组件而不用社区成熟方案?



卡颂补充:所谓核心组件,是指状态管理、路由、国际化、请求库、脚手架这样的框架生态相关的库。既然Inula兼容React,为什么不直接用React生态的成熟产品,而要自研呢?毕竟,这些库是没有软件风险的。




回答:


主要还是丰富Inula生态,根据社区优秀的库总结一套Inula官方推荐的最佳实践。至于开发者怎么选择,我们并不强求。


卡颂的分析


以上是我对Inula核心开发者陈超涛的采访。下面是我看了Inula源码后的一些分析。


要分析一款前端框架,最重要的是明白他是如何更新视图的?这里我选择了两种触发时机来分析:



  1. 首次渲染


触发的方式类似如下:


Inula.render(<App />, document.getElementById("root"));


  1. 执行useState的更新方法触发更新


触发的方式类似如下:


function App() {
const [num, update] = useState(0);
// 触发更新
update(xxx);
// ...
}

顺着调用栈往下看,他们都会执行两步操作:



  1. 创建名为update的数据结构

  2. 执行launchUpdateFromVNode方法


比如这是首屏渲染时:



这是useState更新方法执行时:



launchUpdateFromVNode方法会向上遍历到根结点(源码中遍历的节点叫VNode),再从根节点开始遍历树。由此可以判断,Inula的更新机制与React类似。


所有主流框架在触发更新后,都不会立刻执行更新,中间还有个调度流程。这个流程的存在是为了解决:



  1. 哪些更新应该被优先执行?

  2. 是否有些更新是冗余的,需要合并在一块执行?


Vue中,更新会在微任务中被调度并统一执行,在React中,同时存在微任务(promise)与宏任务(MessageChannel)的调度模式。


Inula中,存在宏任务的调度模式 —— 当宿主环境支持MessageChannel时会使用它,不支持则使用setTimeout调度:



同时,与这套调度机制配套的还有个简单的优先级算法 —— 存在两种优先级,其中:



  • ImmediatePriority:对应正常情况触发的更新

  • NormalPriority:对应useEffect回调


每个更新会根据更新的ID(一个自增的数字)+ 优先级对应的数字 作为优先级队列中的排序依据,按顺序执行。


假设先后触发2次更新,优先级分别是ImmediatePriorityNormalPriority,那么他们的排序依据分别是:



  1. 100(假设当前ID到100了)- 1(ImmediatePriority对应-1) = 99

  2. 101(100自增到101)+ 10000(NormalPriority对应10000)= 10101


99 < 10101,所以前者会先执行。


需要注意的是,Inula中对更新优先级的控制粒度没有React并发更新细,比如对于如下代码:


useEffect(function cb() {
update(xxx);
update(yyy);
})

React中,控制的是每个update对应优先级。在Inula中,控制的是cb回调函数与其他更新所在回调函数之间的执行顺序。


这意味着本质来说,Inula中触发的所有更新都是同步更新,不存在React并发更新中高优先级更新打断低优先级更新的情况。


这也解释了为什么Inula兼容 95% 的React API,剩下 5% 就是并发更新相关API(比如useTransitionuseDeferredvalue)。


现在我们已经知道Inula的更新方式类似React,那么官网提到的响应式API该如何实现呢?这里存在三条路径:



  1. 一套外挂的响应式系统,类似ReactMobx的关系

  2. 内部同时存在两套更新系统(当前一套,响应式一套),调用不同的API使用不同的系统

  3. 重构内部系统为响应式系统,通过编译手段,使所有API(包括当前的React API与未来的类 Vue Composition API)都走这套系统



其中第一条路径比较简单,第二条路径应该还没框架使用,第三条路径想象空间最大。不知道Inula未来会如何发展。


总结


当前,Inula是一款类React的框架,功能上可以类比为React并发更新之前的版本


下一步,Inula会引入响应式API,目的是提高渲染效率。


对于未来的发展,主要围绕在:



  • 探索类 Vue Composition API的可能性

  • 迭代官方核心生态库


对于华为出的这款前端框架,你怎么看?


现在开放原子开源基金会搞了个开源大赛,奖金有35w,有两个选题:



  1. 基于openInula实现社区生态库,比如组件库、图表库、Rust基建、SSR、跨平台、高性能响应式更新方案...

  2. 基于openInula实现的AI应用


由于 openInulaReact API基本一致,说白了只要你把自己的 React 项目改下依赖适配下就能报名,有奖金拿,还有华为背书,这波属于稳赚不赔。感兴趣的朋友可以搜openInula前端框架生态与AI创新挑战赛报名。


作者:魔术师卡颂
来源:juejin.cn/post/7307451255432249354
收起阅读 »

告别混乱布局:用CSS盒子模型为你的网页穿上完美外衣!

在网络设计的世界里,盒子模型是构建网页布局的基石,只有理解了盒子模型,我们才能更好的进行网页布局。HTML中的每一个元素都可以看成是一个盒子,拥有盒子一样的外形和平面空间,它不可见、不直观,但无处不在,所以初学者很容易在这上面出问题。今天就让我们来深入了解一下...
继续阅读 »

在网络设计的世界里,盒子模型是构建网页布局的基石,只有理解了盒子模型,我们才能更好的进行网页布局。

HTML中的每一个元素都可以看成是一个盒子,拥有盒子一样的外形和平面空间,它不可见、不直观,但无处不在,所以初学者很容易在这上面出问题。今天就让我们来深入了解一下盒子模型。

一、盒子模型是什么?

首先,我们来理解一下什么是CSS盒子模型。

简单来说,CSS盒子模型是CSS用来管理和布局页面上每一个元素的一种机制。每个HTML元素都可以被想象成一个矩形的盒子,这个盒子由内容(content)、内边距(padding)、边框(border)和外边距(margin)四个部分组成。

Description

这四个部分共同作用,决定了元素在页面上的最终显示效果。

二、盒子模型的组成部分

一个盒子由外到内可以分成四个部分:margin(外边距)、border(边框)、padding(内边距)、content(内容)。

Description

其中margin、border、padding是CSS属性,因此可以通过这三个属性来控制盒子的这三个部分。而content则是HTML元素的内容。

下面来一一介绍盒子模型的各个组成部分:

2.1 内容(Content)

内容是盒子模型的中心,它包含了实际的文本、图片等元素。内容区域是盒子模型中唯一不可或缺的部分,其他三部分都是可选的。

内容区的尺寸由元素的宽度和高度决定,但可以通过设置box-sizing属性来改变这一行为。

下面通过代码例子来了解一下内容区:

<!DOCTYPE html>
<html>
<head>
<style>
.box {
width: 200px;
height: 100px;
background-color: lightblue;
border: 2px solid black;
padding: 10px;
margin: 20px;
box-sizing: content-box; /* 默认值 */
}
</style>
</head>
<body>


<div>这是一个盒子模型的例子。</div>


</body>
</html>

Description

在这个例子中,.box类定义了一个具有特定样式的<div>元素。这个元素的宽度为200px,高度为100px,背景颜色为浅蓝色。边框为2像素宽的黑色实线,内边距为10像素,外边距为20像素。

由于我们设置了box-sizing: content-box;(这是默认值),所以元素的宽度和高度仅包括内容区的尺寸。换句话说,元素的宽度是200px,高度是100px,不包括内边距、边框和外边距。

如果我们将box-sizing属性设置为border-box,则元素的宽度和高度将包括内容区、内边距和边框,但不包括外边距。这意味着元素的总宽度将是234px(200px + 2 * 10px + 2 * 2px),高度将是124px(100px + 2 * 10px + 2 * 2px)。

总之,内容区是CSS盒子模型中的一个核心概念,它表示元素的实际内容所在的区域。通过调整box-sizing属性,您可以控制元素尺寸是否包括内容区、内边距和边框。

2.2 内边距(Padding)

内边距是内容的缓冲区,它位于内容和边框之间。通过设置内边距,我们可以在内容和边框之间创建空间,让页面看起来不会太过拥挤。

内边距是内容区和边框之间的距离,会影响到整个盒子的大小。

  • padding-top: ; 上内边距
  • padding-left:; 左内边距
  • padding-right:; 右内边距
  • padding-bottom:; 下内边距

代码示例:

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title></title>
<style type="text/css">
/*
1、 padding-top: ; 上内边距
padding-left:; 左内边距
padding-right:; 右内边距
padding-bottom:; 下内边距
2、padding简写 可以跟多个值
四个值 上 右 下 左
三个值 上 左右 下
二个值 上下 左右
一个值 上下左右


*/

.box1 {
width: 200px;
height: 200px;
background-color: #bfa;
/* padding-top:30px ;
padding-left: 30px;
padding-right: 30px;
padding-bottom: 30px; */

padding: 40px;
border: 10px transparent solid;
}
.box1:hover {
border: 10px red solid;
}

/*
* 创建一个子元素box2占满box1,box2把内容区撑满了
*/

.box2 {
width: 100%;
height: 100%;
background-color: yellow;
}
</style>
</head>
<body>
<div>
<div></div>
</div>
</body>
</html>

Description

2.3 边框(Border)

边框围绕在内边距的外围,它可以是实线、虚线或者其他样式。边框用于定义内边距和外边距之间的界限,同时也起到了美化元素的作用。

边框属于盒子边缘,边框里面属于盒子内部,出了边框都是盒子的外部,设置边框必须指定三个样式 边框大小、边框的样式、边框的颜色

  • 边框大小:border-width
  • 边框样式:border-style
  • 边框颜色:border-color

代码示例:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
<style type="text/css">


.box {
width: 0px;
height: 0px;
/* background-color: rgb(222, 255, 170); */
/* 边框的大小 如果省略,有默认值,大概1-3px ,不同的浏览器默认大小不一样
border-width 后可跟多个值
四个值 上 右 下 左
三个值 上 左右 下
二个值 上下 左右
一个值 上下左右

单独设置某一边的边框宽度
border-bottom-width
border-top-width
border-left-width
border-right-width
*/

border-width: 20px;
/* border-left-width:40px ; */
/*
边框的样式
border-style 可选值
默认值:none
实线 solid
虚线 dashed
双线 double
点状虚线 dotted
*/

border-style: solid;
/* 设置边框的颜色 默认值是黑色
border-color 也可以跟多个值
四个值 上 右 下 左
三个值 上 左右 下
二个值 上下 左右
一个值 上下左右
对应的方式跟border-width是一样
单独设置某一边的边框颜色
border-XXX-color: ;
*/

border-color: transparent transparent red transparent ;
}
.box1{
width: 200px;
height: 200px;
background-color: turquoise;
/* 简写border
1、 同时设置边框的大小,颜色,样式,没有顺序要求
2、可以单独设置一个边框
border-top:; 设置上边框
border-right 设置右边框
border-bottom 设置下边框
border-left 设置左边框
3、去除某个边框
border:none;
*/

border: blue solid 10px;
border-bottom: none;
/* border-top:10px double green ; */

}
</style>
</head>
<body>
<div></div>
<div></div>
</body>
</html>

Description

2.4 外边距(Margin)

外边距是元素与外界的间隔,它决定了元素与其他元素之间的距离。通过调整外边距,我们可以控制元素之间的相互位置关系,从而影响整体布局。

  • margin-top:; 正值 元素向下移动 负值 元素向上移动
  • margin-left:; 正值 元素向右移动 负值 元素向左移动
  • margin-bottom:; 正值 元素自己不动,其靠下的元素向下移动,负值 元素自己不动,其靠下的元素向上移动
  • margin-right: ; 正值负值都不动

代码示例:

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title></title>
<style type="text/css">
/* 外边距 不会影响到盒子的大小
可以控制盒子的位置
margin-top:; 正值 元素向下移动 负值 元素向上移动
margin-left:; 正值 元素向右移动 负值 元素向左移动
margin-bottom:; 正值 元素自己不动,其靠下的元素向下移动,负值 元素自己不动,其靠下的元素向上移动
margin-right: ; 正值负值都不动
简写 margin 可以跟多个值
规则跟padding一样
*/

.box1 {
width: 200px;
height: 200px;
background-color: #bfa;
border: 10px solid red;
/* margin-top: -100px;
margin-left: -100px;
margin-bottom: -100px;
margin-right: -100px; */

margin: 40px;
}


.box2 {
width: 200px;
height: 200px;
background-color: yellow;
}
</style>
</head>
<body>
<div></div>
<div></div>
</body>
</html>

Description

三、盒子的大小

盒子的大小指的是盒子的宽度和高度。大多数初学者容易将宽度和高度误解为width和height属性,然而默认情况下width和height属性只是设置content(内容)部分的宽和高。

盒子真正的宽和高按下面公式计算

  • 盒子的宽度 = 内容宽度 + 左填充 + 右填充 + 左边框 + 右边框 + 左边距 + 右边距
  • 盒子的高度 = 内容高度 + 上填充 + 下填充 + 上边框 + 下边框 + 上边距 + 下边距

我们还可以用带属性的公式表示:

  • 盒子的宽度 = width + padding-left + padding-right + border-left + border-right + margin-left + margin-right
  • 盒子的高度 = height + padding-top + padding-bottom + border-top + border-bottom + margin-top + margin-bottom

上面说到的是默认情况下的计算方法,另外一种情况下,width和height属性设置的就是盒子的宽度和高度。盒子的宽度和高度的计算方式由box-sizing属性控制。

Description

box-sizing属性值

content-box:默认值,width和height属性分别应用到元素的内容框。在宽度和高度之外绘制元素的内边距、边框、外边距。

想要快速入门前端开发吗?推荐一个前端开发基础课程,这个老师讲的特别好,零基础学习无压力,知识点结合代码,边学边练,可以免费试看试学,还有各种辅助工具和资料,非常适合新手!

点这里前往学习哦!

border-box:为元素设定的width和height属性决定了元素的边框盒。就是说,为元素指定的任何内边距和边框都将在已设定的宽度和高度内进行绘制。通过从已设定的宽度和高度分别减去 边框 和 内边距 才能得到内容的宽度和高度。

  • 当box-sizing:content-box时,这种盒子模型成为标准盒子模型;
  • 当box-sizing: border-box时,这种盒子模型称为IE盒子模型。

四、盒子模型应用技巧

掌握了盒子模型的基本概念后,我们就可以开始创造性地应用它来设计网页。以下是一些技巧:

  • 使用内边距来创建呼吸空间,不要让内容紧贴边框,这样可以让页面看起来更加舒适。

  • 巧妙运用边框来分隔内容区域,或者为特定的元素添加视觉焦点。

  • 利用外边距实现元素间的对齐和分组,保持页面的整洁和组织性。

  • 考虑使用负边距来实现重叠效果,创造出独特的层次感和视觉冲击力。

CSS盒子模型是前端开发的精髓之一,它不仅帮助我们理解和控制页面布局,还为我们提供了无限的创意空间。现在,你已经掌握了盒子模型的奥秘,是时候在你的项目中运用这些知识,创造出令人惊叹的网页设计了。

记住,每一个细节都可能是打造卓越用户体验的关键。开启你的CSS盒子模型之旅,让我们一起构建更加精彩、更加互动的网页世界!

收起阅读 »

爱猫程序员给自家小猪咪做了一个上门喂养小程序

web
🐱前言 每次一到节假日,都好想和好朋友们一起出去玩,但是心里总放不下家里的小猪咪,于是心想能不能找一个喂养师上门喂养呢。于是找了几个上门喂养的平台,并最终下单了服务。 真的不得不说,上门喂养的小姐姐真的非常用心和专业。她们来到我家期间,全情投入地照顾着我的毛孩...
继续阅读 »

🐱前言


每次一到节假日,都好想和好朋友们一起出去玩,但是心里总放不下家里的小猪咪,于是心想能不能找一个喂养师上门喂养呢。于是找了几个上门喂养的平台,并最终下单了服务。


真的不得不说,上门喂养的小姐姐真的非常用心和专业。她们来到我家期间,全情投入地照顾着我的毛孩子,让它吃得饱饱的,看起来胖乎乎的。更令我感动的是,她们还全程录像了照料过程,并将视频发送给我,让我能够时刻了解小猪咪的情况。


分享下小猪咪美照👇



🤔️思考


我也是程序员,为什么我不能自己也做一个上门喂养的小程序呢,于是经过调研,发现了其他的几个平台各有各的弊端和优点,然后诞生出了最初的想法如何做一个把其他平台的弊端去除掉,做一个最好的上门喂养平台。


🎨开始设计


于是开始琢磨figma~~~


因为任何c端都是通过首页再去衍生出其他页面整体样式的,所以先着手制作首页,只要首页定好了其他页面都好说。
image.png


一周后....开始着手设计🤔️...思考...参考....初版定稿


由于刚入门设计一开始丑丑的,不忍直视~~~



再经过几天的琢磨思考...改版...最终确定首页


经过不断的练习琢磨参考最后定稿,给大家推荐下我经常参考我素材网站花瓣



N天之后......其他页面陆续出炉


由于页面太多了就不一一展示啦~~~


image.png


最满意的设计页面


给各大掘友分享一下我最满意的设计页面


签到


结合了猫咪的元素统一使用了同一只猫咪素材~整体效果偏向手绘风格。


image.png


抽奖扭蛋


这个扭蛋机真是一笔一画画了一天才出来的,真的哭😭啦~,由于AE动画太过麻烦所以每一个扭蛋球球的滚动都用代码去实现~~


image.png



💻编程


技术选型


uniapp + nestjs + mysql


NestJS是一个基于Node.js的开发框架,它提供了一种构建可扩展且模块化的服务端应用程序的方式,所以对于前端而言是最好上手的一门语法。


Nestjs学习推荐


给各大掘友推荐一下本人从0到1上手nestjs的教程,就是一下小册就是以下这本,初级直接上手跟着写一遍基本就会啦


image.png


建议学习到 61章左右就可以开始写后端项目啦



小程序端基本使用逻辑



  • 用户下单-服务人员上门服务完成-用户检查完成后确认订单完成-订单款项打款到服务人员钱包

  • 用户注册成为服务人员-设置服务范围-上线开始服务-等待用户给服务人员下单


下单流程


选择服务地点-选择服务人员-点击预约-添加服务宠物-付款


image.png


服务人员认证流程


根据申请流程逐步填写,由于服务人员是平台与用户产生信任的标准,所以我们加大了通过审核的门槛,把一些只追求利益,而不是真正热爱宠物的人员拒之门外,保护双方利益。


image.png


后端Nestjs部署


后端代码写完之后我们需要把服务部署到腾讯云,以下是具体步骤


1.腾讯云创建镜像仓库


前往腾讯云创建容器镜像服务,这样我们就可以把本地docker镜像推送到腾讯云中了,这个容器镜像服务个人版是免费的


image.png


2.打包Nestjs


通过执行docker命令部署到本地的docker


image.png


👇以下是具体docker代码


FROM --platform=linux/amd64 node:18-alpine3.14 as build-stage

WORKDIR /app

COPY package.json .
COPY cert .
COPY catdogship.com_nginx .
COPY ecosystem.config.js .

RUN npm config set registry https://registry.npmmirror.com/

RUN npm install

COPY . .

# 第一个镜像执行 build
RUN npm run build

# FROM 继承 node 镜像创建一个新镜像
FROM --platform=linux/amd64 node:18-alpine3.14 as production-stage

# 通过 COPY --from-build-stage 从那个镜像内复制 /app/dist 的文件到当前镜像的 /app 下
COPY --from=build-stage /app/package.json /app/package.json
COPY --from=build-stage /app/ecosystem.config.js /app/ecosystem.config.js

COPY --from=build-stage /app/dist /app/src/
COPY --from=build-stage /app/cert /app/cert/
COPY --from=build-stage /app/public /app/public/
COPY --from=build-stage /app/static /app/static/
COPY --from=build-stage /app/catdogship.com_nginx /app/catdogship.com_nginx/

WORKDIR /app

# 切到 /app 目录执行 npm install --production 只安装 dependencies 依赖
RUN npm install --production

RUN npm install pm2 -g

EXPOSE 443

CMD ["pm2-runtime", "/app/ecosystem.config.js"]

3.推送到腾讯云


本地打包完成之后我们需要把本地的docker推送到腾讯云中,所以我们本地写一个sh脚本执行推送


#!/bin/bash

# 生成当前时间
timestamp=$(date +%Y-%m-%d-%H-%M)

# Step 1: 构建镜像
docker build -t hello:$timestamp .

# Step 2: 查找镜像的标签
image_id=$(docker images -q hello:$timestamp)

# Step 3: 为镜像添加新的标签
docker tag $image_id 你的腾讯云镜像地址:$timestamp

docker push 你的腾讯云镜像地址:$timestamp

4.部署到服务器


由于我使用的是轻量级应用服务器,所以直接使用自动化助手去进行部署(PS:可能有一些小伙伴会问为什么用轻量级应用服务器呢,因为目前用户量不是很多,轻量级应用服务器足够支撑,后面用户量起来会考虑转为k8s集群


image.png


然后我们去创建一个自动化执行命令,去执行服务器的docker部署


image.png


创建命令


image.png


执行命令


image.png


👇以下是命令代码


# 停止服务
docker stop hello

# 删除容器
docker rm hello

# 拉取镜像
docker pull 你的腾讯云镜像地:{{key}}

#读取image名称
image_id=$(docker images -q 你的腾讯云镜像地:{{key}})

# 运行容器
docker run -d -p 443:443 -e TZ=Asia/Shanghai --name hello $image_id

5.部署完成


命令返回执行结果显示执行完成,说明已经部署成功了


image.png


6.Nestjs服务器端的管理


由于node是一个单线程,所以我们使用的是pm2去进行管理node,它可以把node变成一个多线程并进行管理


由于nestjs中使用到了定时任务,而定时任务只需要开一条线程去做就好了,所以我增加了一个环境变量NODE_ENV来对定时任务进行管理


module.exports = {
apps: [
{
name: 'wx-applets',
// 指定要运行的应用程序的入口文件路径
script: '/app/src/main.js',
exec_mode: 'cluster',
// 集群模式下的实例数-启动了2个服务进程
instances: 4,
// 如果设置为 true,则避免使用进程 ID 为日志文件添加后缀
combine_logs: true,
// 如果服务占用的内存超过300M,会自动进行重启。
// max_memory_restart: '1000M',
env: {
NODE_ENV: 'production',
},
},
{
name: 'wx-applets-scheduled-tasks',
script: '/app/src/main.js',
instances: 1,
// 采用分叉模式,创建一个单独的进程
exec_mode: 'fork',
env: {
NODE_ENV: 'tasks',
},
},
],
};

后端总结


到目前为止前台的业务接口都写完了做了个统计一共有179个接口


image.png


后期版本更新


预计这个月上线毛孩子用品盲盒抽奖,有兴趣的友友们也可关注下哦


Frame 2608921.png


后期展望,帮助更多的流浪动物有一个温暖的家


image.png

小程序上线


目前小程序已经上线啦~,友友们可以前往小程序搜索 喵汪舰 前往体验,
或者扫描一下二维码前往


开业海报.jpg

我的学习路线


因为我是一个前端开发,所以对于设计感觉还是挺好的,所以上手比较快。
一条学习建议路线:前端-后端-设计-产品,最终形成了一个完整的产品产出。


以下的链接是这个项目中我经常用到的素材网站:


freepik国外素材网站-可以找到大部份的插画素材


figma自带社区-获取参考的产品素材


花瓣国内素材参考网站-涵盖了国内基本的产品素材


pinterest国外大型素材网-你想到的基本都有


总结


一个产品的产出不仅仅依靠代码,还要好的用户体验,还需要不断的优化迭代,


最后给一起并肩前行的创业者们的一段话:


在创业的道路上,我们正在追逐梦想,挑战极限,为自己和世界创造新的可能性。这个旅程充满了风险和不确定性,但也蕴藏着无限的机遇和成就,不要害怕失败,勇于面对失败,将其视为成功的必经之路。


作者:热爱vue的小菜鸟
来源:juejin.cn/post/7348363812948033575
收起阅读 »

业务开发做到零 bug 有多难?

大家好,我是树哥,好久不见啦。 作为一个工作了 10 多年的开发,写业务代码总是写了不少的。但你想过做到零 bug 吗?我可是想过的,毕竟我还是有点追求的。不然每天都是浑浑噩噩地过,多没意思啊。 大概在一年多前,我给自己立下一个目标 —— 尽量将自己经手的业务...
继续阅读 »

大家好,我是树哥,好久不见啦。


作为一个工作了 10 多年的开发,写业务代码总是写了不少的。但你想过做到零 bug 吗?我可是想过的,毕竟我还是有点追求的。不然每天都是浑浑噩噩地过,多没意思啊。


大概在一年多前,我给自己立下一个目标 —— 尽量将自己经手的业务需求做到零 bug。不试不知道,一试吓一跳,原来零 bug 还真的还不容易。今天,树哥就跟大家分享关于「业务开发零 bug」的一些思考。


要做到业务开发零 bug,其实挺难的。这涉及到非常多方面,有些方面可能还不只是你能控制的,例如:产品 PRD 详尽程度,产研组织的稳定性等等。经过一段时间的思考与摸索,我自己总结出一些影响因素,分别是:



  1. 产品需求文档的清晰程度

  2. 需求的复杂程度

  3. 开发人员的细心程度

  4. 开发人员是否详细自测过

  5. 开发人员对项目的熟悉程度

  6. 开发人员开发时间是否充足


针对上面说到的影响因素,我们一个个详细聊聊。


需求文档清晰程度


对于研发、测试人员来说,他们获取信息的源头就是产品的 PRD 文档。因此,需求文档是否写得清晰、明确,就显得非常重要。


如果产品自己对功能都不了解,那么输出的需求文档肯定「缺斤少两」,到时候就是边开发边补充需求,甚至是在测试过程中补充需求。遇到这种情况,想要做到零 bug 真的非常难。


因此,清晰明确的需求文档,是我们实现业务开发零 bug 的重要前提。如果这个前提保证不了,那要做到零 bug 真的很难。毕竟想做成啥样都不知道,程序员又不是神仙,咋能猜出你想要什么。但这块内容,更多是对于产品人员专业能力的要求,开发人员无法控制。


在一些公司,会再需求评审之前先对需求文档进行一次初审,筛除那些有明显重大问题的需求,这样可以减少一部分劣质需求。


但初审的作用还是有限的,它没办法对功能的细节做较多的判断。很多时候恰恰就是一些功能细节的缺失,导致了一些 bug 的诞生。


需求的复杂程度


需求的复杂程度,对于实现业务开发零 bug 也有很大的影响。举个简单地例子:一个改文案的需求,和一个完全重新做的功能。


这样的两个需求,其复杂程度差别很大,肯定是改文案的需求实现业务开发零 bug 的难度低很多。对于一个完全重新做的功能,要做到完全零 bug,对于开发人员的要求非常高。


对于越复杂的项目,零 bug 的可能性就越低。因此,很多项目为了追求产出功能的高质量,会采用将功能点拆得非常细的方式,来减少单个需求的复杂度。


笔者公司在去年做过这个尝试,确实是可以较大地提高产出功能的质量。


细心程度


前面说到需求文档的清晰程度很重要,这取决于产品人员对于业务的理解程度,以及对于对于功能的熟悉程度。开发人员的细心,就像是一个质检关卡一样,在开发之前就对产品的需求内容进行详尽的思考与提问。


对于粗心的开发人员来说,其可能不看需求文档就直接参加需求评审,等到开发的时候边写代码边看需求文档,其写得代码也是一边熟悉需求一边改。这样写出来的系统功能是比较差的,没有一个统一、全局的设计与思考,很容易在细节处发生问题。


一个细心的开发人员,其会在评审之前就详细阅读需求文档,甚至会前前后后翻阅好几次。他甚至会逐字逐句地阅读,弄懂每个文字、句子的意思,甚至有时候会让你觉得他是在玩文字游戏(但不得不说,确实有必要细致一些)。


最后会联系上下文思考功能的合理性。如果发现一些不合理的地方,他会积极与产品沟通反馈,以确保其对于需求的理解,与产品经理对于需求的理解是一致的。


通过对比,我们知道细心的开发人员对于产品经理来说,是一个莫大的帮助,可以帮助他查漏补缺,让其对于功能的考虑更加细致、严谨。


这里的开发人员不仅仅指的是后端开发人员,也包括前端开发、移动端开发,他们都会从不同角度提出问题。


对于后端开发人员来说,他们可能会提出性能问题。对于前端开发以及移动端开发同学,他们可能会提出交互问题、样式统一等问题。


简单地说,细心的开发人员可以弥补需求文档的缺陷,从而让大家对于需求的理解更趋于一致,从而减少 bug 的发生。因此,开发人员的细心程度也是决定业务开发能否实现零 bug 的关键因素!


是否详细自测过


即使写过 10 多年代码的开发人员,刷 Leetcode 也不敢说 bug free 一把过,对于更加复杂的业务代码更是如此。因此,要做到业务开发零 bug,其中一个很重要的操作便是 —— 自测。


自测可以帮你再次检查可能出现的问题,从而提高零 bug 的概率。对于我而言,我习惯性在自测的时候再次对照一遍需求文档,从而避免自己遗漏一些功能的细节点。


对于自测而言,业界有很多种自测方法,包括:单测、集成测试、功能测试。一般情况,建议自己选择适合自己的自测方法。


很多时候,功能测试是相对来说性价比较高的方式。除此之外,自测的详细程度也根据实际情况有所不同,例如有些人只会测试正常情况,但有些老手会测试一些边界情况、异常情况。


毫无疑问,你越能像测试人员一样测试,你的提测质量肯定就越高,bug 当然也就越少。


对项目的熟悉程度


这里说的项目熟悉程度,既指技术层面的熟悉程度,也指业务功能层面的熟悉程度。


技术层面的熟悉程度,指的是项目之间是用什么技术栈搭建的,你对这些技术是否都熟悉。举个很简单的例子,项目中采用了微服务的方式进行调用,那么你是否清楚是什么微服务调用?


如果采用了 ElasticSearch 进行搜索,那么你是否对 ElasticSearch 有一些了解,知道一些基本使用及最佳实践?等等。


这些算是技术层面的熟悉程度,你对这些越熟悉,你在技术层面发生问题的可能性就越小。


业务功能层面的熟悉程度,指的是你对项目其他模块的业务是否熟悉。例如你经常负责 A 模块的功能,你对 A 模块肯定很熟悉。


但下个迭代你就要去做 B 迭代的需求了,这时候你肯定不是很熟,相对来说出错的可能性就更大一些。


无论是技术层面,还是业务层面的熟悉程度,都会随着你做了更多的需求,变得更加熟悉。到了后面某个阶段,你基本上就不存在踩坑的问题了,也为你业务开发零 bug 奠定了基础。如果你是一个刚刚进入公司的新手,那么做到零 bug 还是很难的。


开发时间是否充足


开发时间是否充足,决定了你是否有充足的时间去熟悉需求,去和产品经理确定细节。有了充足的时间,你也才能有一定时间去进行更详细的自测。更为关键的一点,有充足的时间,你写代码才能写得更好。因此,开发时间是否充足是很重要的。


在实际的开发过程中,会因为各种各样的原因,其实并没有办法给你留出特别理想的开发时间。这时候该怎么办?有些人选择接受,去压缩自己的时间。


有些人则会选择去沟通,或者协调资源,保证自己有充足的时间。其实,正确的做法还是第二种,这样会更好一些。


这需要开发人员有更强的综合能力(沟通、协调能力),但并不是每个开发人员都具备的。关于这点,又是可以聊的一个话题 —— 当你的需求被压缩工时的时候,你应该怎么做?这里暂不展开,后续有时间可以聊聊。


简单来说,开发时间是基础,没有合理、充足的时间保障的话,要做到业务开发零 bug 是不可能的事情。


总结


要做到业务开发零 bug,其实就是要消除功能开发过程中的所有不确定性,包括:需求功能的不确定性、自己写错代码的不确定性等等。而发生这些不确定性的地方,可能就有:



  1. 产品需求文档的清晰程度

  2. 需求的复杂程度

  3. 开发人员的细心程度

  4. 开发人员是否详细自测过

  5. 开发人员对项目的熟悉程度

  6. 开发人员开发时间是否充足


除了上面说到的 6 个影响业务开发零 bug 的因素之外,肯定还有其他影响因素。


你能想到什么影响业务开发零 bug 的因素吗?欢迎在评论区留言与大家分享。


好了,今天的分享就到此为止,希望大家都能做到业务开发零 bug,业务开发代码一把过!


如果你觉得今天的文章对你有帮助,欢迎点赞转发评论,你的转发对于我很重要,感谢大家!


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

聊一聊定时任务重复执行以及解决方案

大家好,我是小趴菜,关于定时任务大家都有接触过,项目中肯定也使用过,只需要在项目中的启动类上加上 @EnableScheduling 注解就可以实现了 现在是单个节点部署,倒是没什么问题。如果是多节点部署呢? 假设现在我们系统要每天给用户新增10积分,那么更新...
继续阅读 »

大家好,我是小趴菜,关于定时任务大家都有接触过,项目中肯定也使用过,只需要在项目中的启动类上加上 @EnableScheduling 注解就可以实现了


现在是单个节点部署,倒是没什么问题。如果是多节点部署呢?


假设现在我们系统要每天给用户新增10积分,那么更新的SQL如下


  update user set point = point + 10 where id = 1;

这时候你的服务部署了两台,那么每一台都会来执行这条更新语句,那么用户的积分就不是+10了。而是+20。


当然这里我们只是举了个例子来引出 @EnableScheduling 的定时任务会存在定时任务重复执行的问题。而且有可能会因为重复执行导致数据不一致的问题


使用数据库乐观锁


在使用乐观锁的时候,首先我们会新增一个字段,也就是更新日期,但是这个更新日期不是指我们修改数据的那个更新时间,比如说今天是2024-03-25,那么到了明天,第一台机器更新成功了,这个值就更新成2024-03-26,其它机器的线程来更新判断这个值是否是2024-03-25,如果不是,说明已经有线程更新了,那么就不需要再次更新了


  update user set point = point + 10,modifyTime = 2023-03-26 where id = 1 and modifyTime = 2024-03-25

基于乐观锁的方式有什么缺点呢??


现在我们只有两台服务器,那如果有1千台,1万台呢,对于同一条数据的,那么这1万台服务器都会去执行更新操作,但是其实在这1万次更新操作中,只有一次操作是成功的,其余的操作都是不需要执行的


所以使用这种方式当机器数量很多的时候,对数据库的压力是非常大的


分布式锁


我们还可以使用分布式锁的方式来实现,比如要执行这个定时任务之前要先获取一把锁,这个锁是对每一条记录都分别有对应的一把锁


当线程来更新某一条数据的时候,首先要获取这条记录的一个分布式锁,拿到锁了就可以去更新了,没有拿到锁的也不要去等待获取锁了,就直接更新下一条数据即可,同样的步骤,只有拿到某条数据的锁,才可以更新


image.png


但是这里有一个注意的点,就是比如服务-1先获取到id=100的这条记录的锁,然后执行更新,但是此时因为某些原因,导致某台服务器过了一会才来执行id=100的这条数据,因为服务-1已经执行完了,所以会释放掉这把锁,所以这台服务器来就能获取到锁,那么也会执行更新操作


所以在更新的时候,还是做一下判断,判断这条记录是否已经被更新了,如果已经更新了,那么就不要再次更新了


分布式锁相对于乐观锁来说,减少了大量的无用的更新操作,但还是会存在极少量的重复更新操作,但是相对来说,对数据库的压力就减少了很多


但是与此同时,这就依赖于Redis,要保证Redis的服务可用


消息队列


我们可以将要更新的数据提前加载到消息队列中去,然后每台服务就是一个消费者,保证一条记录只能让一个消费者消费,这样也就可以避免重复更新的问题了


但是消费失败的记录不要重回队列,可以在数据库记录,让人工进行处理


使用消息队列会有什么问题呢?


如果你的消息数据特别多,比如有上亿条,那么消息队列就会被堆满,,而且每天都要把数据库的数据都加载到消息队列中去


或许有人说,数据量大我可以多弄几个消息队列,这样确实可以解决一个消息队列堆积消息过多的问题,但是你要如何控制某些服务器只访问某个队列呢?不能每台服务都循环获取每一个队列中的消息吧


而且如果你的消息队列是单点的,那么服务宕机那么所有数据都没法更新了,这时候你还要去弄一个集群
,这成本就有点大了


所以不推荐使用这种方式


分布式任务调度-xxl-job


最后就是使用分布式定时任务调度框架 xxl-job了,关于xxl-job的使用大家可以网上自己搜一下资料。


XXL-JOB是一个开源的分布式任务调度框架,它主要用于解决大规模分布式任务的调度和执行问题。该框架提供了任务调度中心执行器任务日志等组件,支持任务的定时调度、动态添加和删除、执行情况监控和日志记录等功能。


总结


以上就是为大家提供的定时任务重复执行的解决方案,大家可以根据自己的实际情况来选择不同的方案来实现


作者:我是小趴菜
来源:juejin.cn/post/7350167062364979226
收起阅读 »

职场上的人情世故 - 初入公司的第一个项目

说下背景 来公司的第二周,上面下发了新需求,也是我参与公司的第一个项目。 我先说下公司一个项目的大体配置 产品经理 1~2人 一般中小版本就1人 UI 1~2人 研发前端 1~3人 研发后端 1~5人(包含协同团队) ps:其中后端中1...
继续阅读 »

说下背景


来公司的第二周,上面下发了新需求,也是我参与公司的第一个项目。


我先说下公司一个项目的大体配置


产品经理 1~2人  一般中小版本就1人
UI 1~2人
研发前端 1~3人
研发后端 1~5人(包含协同团队)
ps:其中后端中1人为项目负责人,负责整体项目进度和协调资源
测试 1~2人

好的,故事由此开始。先铺垫下,该项目是我从业以来最无力最绝望最想扯呼的一个。


需求评审


周一刚来公司,就被公司给我安排的导师L 叫到身边


导师L:「一会儿有个需求评审,我文档先发你,你先抓紧时间看看」


懵懂的我:「好的,我先熟悉下任务」


说实话文档看起来真的有点头疼,主要是上面的一行字我真的无语至极【与线上保持一致】。公司的项目还是比较大的,我上哪儿去了解保持一致是个啥意思......问下导师吧,他也在排查线上问题;问下其他同事吧,也刚来没多久不了解这块业务....


好吧时间应该够,我先打开系统,看看这功能到底在哪里。


「走,A2评审会」 导师L给了我一个眼神


大哥,我才看了不到20分钟,功能我都没看完......


我的情况稍微有点特殊,一般来说,大家跟随导师的节奏,问题都不大
看不懂的需求,一定要整懂了在动手,否则返工风险很大
时间不允许?那要再考虑下该公司是不是适合你了哦!!!总不能说能跑起来就行吧

过程我全程梦游,大家都在提问题:各种数据的兼容方案、功能冲突怎么兼容、有类似功能是否能套用、数据权限是否考虑、特殊逻辑的兜底方案.......


开始还能记一下笔记,后面发现 跟不上,根本跟不上 直接 弃笔,从容 面对现实了


从会议室走出来,嗯~~ 怎么形容心情呢。我高考语文作文那一页名字忘了写出考场都没这么忧虑过(当然作文最后还是有得分)。


分配任务


直接上对话详情,后端加上我一共3人


导师L:「任务我这边大概分配了下,你们先选吧」


我内心很慌,我一个都看不明白要怎么做


不知所措的我:「要不我拿剩下的」


同事P:「那把这个、这个、这个给我吧,剩下的你们分?」


导师L看着我:「那就这样分,你做那个、那个、那个,有问题随时找我」


不知所措的我:「好的,我主要是不熟悉这块业务,有问题我先整理出来再一起问下你呗」


这里有个关键点,整理完问题再一起咨询,切记遇到一个问一个
态度摆正,表现得自信点,第一次在同事面前“刷脸”,印象要留好一些

研发阶段


我的任务主要是客户群统计模块


导师给我开了两个服务的权限,让我先看看代码


懵逼的我:「L哥,要不大概说下在那部分写的统计数据?我应该从哪些表出结果?」


导师L:「你先看代码,里面都有逻辑,你看得懂」


无语的我:「好的」 内心(这么玩儿是吧)


知道那代码有多难懂吗?全都是A队列推到B队列,判断后再推C队列,并且还有了很多观察者模式。可能有兄弟姐妹问:「看不懂注释?」。注释少得可怜,也就一些字段含义,不过看单词也能看出来。


队列和队列之间很难看懂,并且很多跨服务的队列,给我开的两个服务根本不够,前后一共加上4个服务才把统计的逻辑串起来,我还专门画了个流程图出来


不管代码设计和质量如何,作为新人别公开喷前辈的代码
前期不要加入太多个性化的代码和设计,每个公司的要求不一致,最好的方式还是模仿
逻辑复杂的话,建议画一个流程图or时序图,架构图的话还是问下老同事,自己画可能不准确
不要轻易下手编码,确保自己很明确整体逻辑了再跟熟悉的同事复述一遍先

好吧,我终于找到逻辑线了(花了1天半时间,留给我开发的时间很近很近),但我发现,结果表很多很多,有AAA_statistics、BBB_statistics、CCC_statistics...... 我拿不定主意,刚好我也对这几个服务熟悉得差不多了,于是我拿着这些成果去问导师L


诚恳的我:「L哥,帮忙看看这几张表,我感觉有一些数据是重复的,还对不上数,帮忙看看呢?」


导师L:「逻辑都捋清楚了吗?」


我直接掏出了我的流程图


导师L:「这是JW(我们组的TL)给你的吗?」


求知的我:「我自己画的,不晓得准确性怎么样,帮忙check下呢」


导师L:「主要是我这还有点事,你再看看吧,晚点我们过一下」


前期难免会有很多问题,但自己也要有点准备,否则被问成麻瓜,印象分直接拉低
不知道的不要说"不知道",可以说"我还不太熟悉,我先去看看逻辑,X哥要不跟我说下大概位置或则有啥文档?"

到了晚上7:00左右,我发现L哥不见了,后来才晓得那天他溜得挺早的。只剩我原地爆炸,明天要联调了,我关键的数据组装service还只有一行//TODO。没招了,我按照我的想法和数据指标,找了几张最合适的表,把数据产出了。


第二天


我早早地来公司,重新check了下代码并又调试了几次,除了数据对不上没啥其他问题。导师L带着早饭来到公司,我还等到吃完早饭再去问了下。


疲惫的我:「L哥,你看我用这张统计表去输出功能有问题没?」


导师L:「就那么几张表,你造几条数据不就看出来了」


疲惫的我:「我造了几条数据,但是昨天的数据我看今天才过来,昨天忘了记录过程」


导师L:「怎么可能,你怎么造的数据,算了等会儿我看一下」


......


过程就不细讲了,总之,不好受...... 最后问题也定位到了,是导师L当初做测试,把定时任务往后延了1天。好吧,这还有定时任务的事儿?我只能看代码看到一堆数据同步的接口,真的是应征了那句话:【最怕的是:我不知道我'不知道'】。


时间允许的话,着手开工前还是留一份概要设计的存档,别把坑都留给后面的人了
流程图、时序图、架构图等,前期多花时间,少走弯路
心态摆好,我们是来打工挣钱的,交不交朋友是其次而已,关系不恶化怎么着都行

而后,比较顺利地进行到提测阶段,bug真的是多得要命,主要是以下几点:


1、数据权限没加(就是prd文档上写的跟xxx模块保持一致逻辑)


2、数据同步时好时坏,排查后发现是定时任务不稳定,大家都在一个环境提测版本,都在修改功能


3、未做改造的功能点测出历史bug(功能类似,故测试同学也纳入了测试范围)


4、服务被其他版本覆盖


导致最终版本交付时,我个人产生了大概20+个bug。周五下午TL专门找我谈话,约谈此事,大概意思是版本复杂度不高,但bug数过多,对我的版本质量不满意。对此,我也阐述了我的解释,这里我就不细说解释的话,大概从以下几点去阐述


不要丢锅,先把由自己粗心产生的bug讲一下,记得提一下数据,因自身代码质量产生了X个bug,后续会加强编码质量,多自测
也不要背锅,说下由于环境问题导致了X个bug的产生,并给出自己的建议,是否能做环境隔离,一个环境只能容纳服务不冲突的1~2个版本
注意下,TL直接对话的机会并不是很多,可以结合自身入职以来的经历,提一些建议(不用管是否重要)
总之,事事多总结,否则像这种情况下TL突然找咱们约谈,说不出个123,印象分会很差

项目总结


在项目研发中,不一定能得到公司前辈们的"关照",这时候就需要先自己憋一下(注意合理安排时间),怎么着也能憋出点内容,那这这些内容再去咨询,效果会好很多。像我这种被孤立的情况,我也不好说是不是常态,我个人还是认为好同事还是居多的。总之还是与同事搞好关系,或者关系一般遇事能帮忙也行,不要跟任何同事把关系闹僵(哪怕脾气差、技术菜、背后勾心斗角)。


还是那句话,遇事不要慌,先调整好心态(道理都看得懂,但希望大家都能做得到),尽力吃透项目再开始施工,否则返工或者低质量产出不好避免。


多做总结,不甩锅不背锅,不管是项目总结还是转正答辩还是晋升答辩,都能有个有条理有依据的总结出来。这点在职场中至关重要。


作者:snowlover
来源:juejin.cn/post/7347542133610364980
收起阅读 »

低谷期可能是在救你

怎么来定义低谷期? 我觉得真正的低谷期是迷茫,挫败,痛苦,自我怀疑等等,反正一定是比肉体上的痛苦更煎熬! 可能你会觉得暂时性找不到工作,面试受挫,分手等是你的低谷期,实则不然,这大概是一个不敢面对自己的借口。 因为多数人找到工作后,只要能在温饱线上,那么90%...
继续阅读 »

怎么来定义低谷期?


我觉得真正的低谷期是迷茫,挫败,痛苦,自我怀疑等等,反正一定是比肉体上的痛苦更煎熬!


可能你会觉得暂时性找不到工作,面试受挫,分手等是你的低谷期,实则不然,这大概是一个不敢面对自己的借口。


因为多数人找到工作后,只要能在温饱线上,那么90%的人就会心安理得摆烂,和男女朋友复合后,依然是维持原样,不会被爱情去刺激自己成长。


如果用工作和分手这两件事情来描述真正的低谷期,那么我觉得应该是这样描述的:


自己付出了很多,每天都像一条疯狗干,工作和学习的时长维持在18个小时左右,坚持了很久,但是没有收获自己想要的东西,希望还是如此渺茫,于是产生严重的自我怀疑,挫败,深圳绝望!


自己一直在努力,在慢慢成长,目标越来越近,但是自己爱得死去活来的女朋友实在等不了自己成长,自己变强,所以无情的选择离开。


经过上面的描述,成为了毋庸置疑的低谷期。


因为没经过描述之前其实你是没有任何斗志的,目的就是能安心混日子,可以用摆烂来形容,和女朋友在一起,自己也不需要啥成本,她也没有啥诉求,所以算得上是互相将就。


但是经过描述后,实际上你的努力近乎疯狂,对于成功是无比的渴望,但是迟迟没降临到你的身上,而女朋友是在你身上压了筹码的,而你又非常爱她,但是迟迟不能让她压的筹码赚个翻倍,所以离开成了定局。


这下就一目了然了吧。


所以这时候你再去回想一下自己所谓的低谷期,是否真的是低谷期?还是自己矫情?


从上面的场景我们能看到深刻二字,第一个是根本谈不上深刻,而第二个是无比的深刻,如果你经历过,那么你自然能理解!


只有让自己刻骨铭心和痛苦的事情才能让自己成长,其他的基本不能。


就像你比较肥胖,三高找上你,家里人叫你不要再吃垃圾食品,不要喝饮料,但是这时候你还没真正躺在病床上,你基本上不会去听。


但是如果此刻你躺在病床上,插着呼吸管,医生给你说,如果你再不注意饮食,那么你很快就会死掉。你看你还会不会再去吃垃圾食品,喝饮料。


所以说,只有让自己脱一层皮,自己才会意识到问题的所在。


那么低谷期里,也自然会脱一层皮,甚至还会换骨头,那么你觉得这难道不是最佳的成长期吗?


所以一个人能遇上真正的低谷期,其实是一种幸运,应该要感谢这个遭遇。


就像一个朋友,因为一些原因导致自己不能再继续读书,家里为了自己的事情而负债累累,自己承受巨大的压力,想死的心都有了。


但是在这段堪称被恶魔惩罚的时期,他没有一蹶不振,反而置之死地而后生,最后无论是财富还是个人能力,阅历都有很大的提升!


后面和他聊天的时候,他说那是自己最痛苦,最无助的时候,但是自己庆幸没有放弃,才造就了一身钢筋铁骨?


所以你说这个低谷期是不幸的吗?


不是,这算的上是恩赐,因为并不是谁都有这种机会。


所以我们在低谷期的时候,如果能有把它作为翻盘的机会的思想,那么它一定是很难得的机会!


但是如果把它作为上天对自己的惩罚,从而一蹶不振,那么其实是错失一次很好的机会,并且也学让自己越陷越深!


作者:苏格拉的底牌
来源:juejin.cn/post/7349750846900125748
收起阅读 »