注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

一时兴起,聊聊当今IT行业的乱象

本文写于2024年3月31号,大的背景是行业寒冬,工作岗位的数量和质量都远远不如之前,造成了打工人卷的飞起的现象,但是从企业端去看,却是面临高端人才不足,低端人才过剩以及招的人数很多但是却满足不了业务需求的问题。 本文所描述现象有作者自己的真实经历,也有道听途...
继续阅读 »

本文写于2024年3月31号,大的背景是行业寒冬,工作岗位的数量和质量都远远不如之前,造成了打工人卷的飞起的现象,但是从企业端去看,却是面临高端人才不足,低端人才过剩以及招的人数很多但是却满足不了业务需求的问题。


本文所描述现象有作者自己的真实经历,也有道听途说但是真实存在的现象~


一、词汇高大上,过后却一地鸡毛


造词现象普遍发生在大厂牛逼人物向上汇报或者是全员会的ppt中,这些牛逼的人物已经不屑于用已存的词汇来表达自己的想法,他们会把现有的词汇融会贯通,进而创造出新的牛x词,给人一种创新的感觉,让人一下子觉得这才是核心科技。


二、无效卷



  • 白天不怎么干活或者磨洋工,但是到了晚上才认真干起活来,故意加班到很晚,其实p事都没干。

  • 故意很晚的时间群里@下同事


三、产品经理只管要要要,研发只管干干干


其实这点很可怕,一般来说对于产品经理,产品就像自己的娃一样,自己再熟悉不过。但是现实是很多产品经理可能连这个娃有没有xjj都不知道😂。研发不管需求是不是解决问题,也不会考虑实际问题,只管完成crud的任务。


四、无脑跟进新技术


比如最近几年兴起的大模型,那好,我们怎么可以落后于行业呢,我们自己也来搞个,虽然不知道对于我们有什么用,但贵在自研啊。


五、文档一坨狗屎


很多大厂对外的文档,比如云厂商的,用户照着文档一步一步做都会失败。


六、PUA


下面这段也是老经典语录了


其实,我对你是有一些失望的。当初给你定级px,是高于你面试时的水平的。我是希望进来后,你能够拼一把,快速成长起来的。px这个层级,不是把事情做好就可以的。
你需要有体系化思考的能力。你做的事情,他的价值点在哪里?你是否做出了壁垒,形成了核心竞争力?你做的事情,和公司内其他团队的差异化在哪里?你的事情,是否沉淀了一套可复用的物理资料和方法论?为什么是你来做,其他人不能做吗?你需要有自己的判断力,而不是我说什么你就做什么。后续,把你的思考沉淀到日报周报月报里,我希望看到你的思考,而不仅仅是进度。
提醒一下,你的产出,和同层级比,是有些单薄的,马上要到年底了,加把劲儿。你看咱们团队的那个谁,人家去年晋升之前,可以一整年都在项目室打地铺的。成长,一定是伴随着痛苦的,当你最痛苦的时候其实才是你成长最快的时候。


作者:李少博
来源:juejin.cn/post/7352079468507594788
收起阅读 »

如果写劣质代码是犯罪,那我该判无期

导读 程序员痛恨遇到质量低劣的代码,但在高压环境下,我们常为了最快解决当下需求而忽略代码规范,在无意识中堆积大量债务。我们还观察到许多开发者被迫加班的罪魁祸首便是写低效代码、不重视代码优化。编程路上,欲速则不达。 接下来,我将为各位列举9种我个人工作中高频遇到...
继续阅读 »



导读


程序员痛恨遇到质量低劣的代码,但在高压环境下,我们常为了最快解决当下需求而忽略代码规范,在无意识中堆积大量债务。我们还观察到许多开发者被迫加班的罪魁祸首便是写低效代码、不重视代码优化。编程路上,欲速则不达。 接下来,我将为各位列举9种我个人工作中高频遇到的不整洁代码行为,并提出针对性优化建议。继续阅读~


目录


1 代码风格和可读性


2 注释


3 错误处理和异常处理


4 代码复用和模块化


5 硬编码


6 测试和调试


7 性能优化


8 代码安全性


9 版本控制和协作


10 总结


01、代码风格和可读性



  • 错误习惯


不一致的命名规则:使用多种命名规则,如 camelCase、snake_case 和 PascalCase 等。过长的函数和方法:编写过长的函数和方法,导致代码难以阅读和理解。 过长的行:编写超过50字符的代码行,导致代码难以阅读。

1.1 变量命名不规范


在编程中,变量命名是非常重要的,良好的变量命名能够提高代码的可读性和可维护性。不规范的命名会增加理解难度,以下是一个不规范命名的例子:


int a, b, c; // 不具有描述性的变量名
float f; // 不清楚变量表示的含义

这样的变量命名不仅会降低代码的可读性,还可能会导致变量混淆,增加代码维护的难度。正确的做法应该使用有意义的名称来命名变量。例如:


int num1, num2, result; // 具有描述性的变量名
float price; // 清晰明了的变量名

1.2 长函数和复杂逻辑


长函数和复杂逻辑是另一个常见的错误和坏习惯。长函数难以理解和维护,而复杂逻辑可能导致错误和难以调试。以下是一个长函数和复杂逻辑的案例:


def count_grade(score):
if score >= 90:
grade = 'A'
elif score >= 80:
grade = 'B'
elif score >= 70:
grade = 'C'
elif score >= 60:
grade = 'D'
else:
grade = 'F'

if grade == 'A' or grade == 'B':
result = 'Pass'
else:
result = 'Fail'
return result

在这个例子中,函数 count_grade 包含了较长的逻辑和多个嵌套的条件语句,使得代码难以理解和维护。正确的做法是将逻辑拆分为多个小函数,每个函数只负责一个简单的任务,例如:


def count_grade(score):
grade = get_grade(score)
result = pass_or_fail(grade)
return result
def get_grade(score):
if score >= 90:
return 'A'
elif score >= 80:
return 'B'
elif score >= 70:
return 'C'
elif score >= 60:
return 'D'
else:
return 'F'
def pass_or_fail(grade):
if grade == 'A' or grade == 'B':
return 'Pass'
else:
return 'Fail'

通过拆分函数,我们使得代码更加可读和可维护。


1.3 过长的行


代码行过长,会导致代码难以阅读和理解,增加了维护和调试的难度。例如:


def f(x):
if x>0:return 'positive' elif x<0:return 'negative'else:return 'zero'

这段代码的问题在于,它没有正确地使用空格和换行,使得代码看起来混乱,难以阅读。正确的方法是,我们应该遵循一定的代码规范和风格,使得代码清晰、易读。下面是按照 PEP 8规范改写的代码:


def check_number(x):
if x > 0:
return 'positive'
elif x < 0:
return 'negative'
else:
return 'zero'

这段代码使用了正确的空格和换行,使得代码清晰、易读。


02、注释



  • 错误习惯


缺少注释:没有为代码编写注释,导致其他人难以理解代码的功能和逻辑。 过时的注释:未及时更新注释,使注释与实际代码不一致。 错误注释:注释上并不规范,常常使用一些不合理的注释。


  • 错误的注释


注释是非常重要的,良好的注释可以提高代码的可读性和可维护性。以下是一个不规范的例子:


int num1, num2; // 定义两个变量

上述代码中,注释并没有提供有用的信息,反而增加了代码的复杂度。


03、错误处理和异常处理



  • 错误的习惯


忽略错误:未对可能出现的错误进行处理。 过度使用异常处理:滥用 try...except 结构,导致代码逻辑混乱。 捕获过于宽泛的异常:捕获过于宽泛的异常,如 except Exception,导致难以定位问题。

3.1 忽略错误


我们往往会遇到各种错误和异常。如果我们忽视了错误处理,那么当错误发生时,程序可能会崩溃,或者出现不可预知的行为。例如:


def divide(x, y):
return x / y

这段代码的问题在于,当 y 为0时,它会抛出 ZeroDivisionError 异常,但是这段代码没有处理这个异常。下面是改进的代码:


def divide(x, y):
try:
return x / y
except ZeroDivisionError:
return 'Cannot divide by zero!'

3.2 过度使用异常处理


我们可能会使用异常处理来替代条件判断,这是不合适的。异常处理应该用于处理异常情况,而不是正常的控制流程。例如:


def divide(a, b):
try:
result = a / b
except ZeroDivisionError:
result = float('inf')
return result

在这个示例中,我们使用异常处理来处理除以零的情况。正确做法:


def divide(a, b):
if b == 0:
result = float('inf')
else:
result = a / b
return result

在这个示例中,我们使用条件判断来处理除以零的情况,而不是使用异常处理。


3.3 捕获过于宽泛的异常


捕获过于宽泛的异常可能导致程序崩溃或隐藏潜在的问题。以下是一个案例:


try {
// 执行一些可能抛出异常的代码
} catch (Exception e) {
// 捕获所有异常,并忽略错误}

在这个例子中,异常被捕获后,没有进行任何处理或记录,导致程序无法正确处理异常情况。正确的做法是根据具体情况,选择合适的异常处理方式,例如:


try {
// 执行一些可能抛出异常的代码
} catch (FileNotFoundException e) {
// 处理文件未找到异常
logger.error("File not found", e);
} catch (IOException e) {
// 处理IO异常
logger.error("IO error", e);
} catch (Exception e) {
// 处理其他异常
logger.error("Unexpected error", e);}

通过合理的异常处理,我们可以更好地处理异常情况,增加程序的稳定性和可靠性。


04、错误处理和异常处理



  • 错误的习惯


缺乏复用性:代码冗余,维护困难,增加 bug 出现的可能性。 缺乏模块化:代码耦合度高,难以重构和测试。

4.1 缺乏复用性


代码重复是一种非常常见的错误。当我们需要实现某个功能时,可能会复制粘贴之前的代码来实现,这样可能会导致代码重复,增加代码维护的难度。例如:


   def calculate_area_of_rectangle(length, width):
return length * width

def calculate_volume_of_cuboid(length, width, height):
return length * width * height

def calculate_area_of_triangle(base, height):
return 0.5 * base * height

def calculate_volume_of_cone(radius, height):
return (1/3) * 3.14 * radius * radius * height

上述代码中,计算逻辑存在重复,这样的代码重复会影响代码的可维护性。为了避免代码重复,我们可以将相同的代码复用,封装成一个函数或者方法。例如:


   def calculate_area_of_rectangle(length, width):
return length * width

def calculate_volume(length, width, height):
return calculate_area_of_rectangle(length, width) * height

def calculate_area_of_triangle(base, height):
return 0.5 * base * height

def calculate_volume_of_cone(radius, height):
return (1/3) * 3.14 * radius * radius * height

这样,我们就可以避免代码重复,提高代码的可维护性。


4.2 缺乏模块化


缺乏模块化是一种常见的错误,这样容易造成冗余,降低代码的可维护性,例如:


   class User:
def __init__(self, name):
self.name = name

def save(self):
# 保存用户到数据库的逻辑

def send_email(self, content):
# 发送邮件的逻辑

class Order:
def __init__(self, user, product):
self.user = user
self.product = product

def save(self):
# 保存订单到数据库的逻辑

def send_email(self, content):
# 发送邮件的逻辑
```

此例中,User 和 Order 类都包含了保存和发送邮件的逻辑,导致代码重复,耦合度高。我们可以通过将发送邮件的逻辑提取为一个独立的类,例如:


   class User:
def __init__(self, name):
self.name = name

def save(self):
# 保存用户到数据库的逻辑

class Order:
def __init__(self, user, product):
self.user = user
self.product = product

def save(self):
# 保存订单到数据库的逻辑

class EmailSender:
def send_email(self, content):
# 发送邮件的逻辑

通过把发送邮件单独提取出来,实现了模块化。现在 User 和 Order 类只负责自己的核心功能,而发送邮件的逻辑由 EmailSender 类负责。这样一来,代码更加清晰,耦合度降低,易于重构和测试。


05、硬编码



  • 错误的习惯


常量:设置固定常量,导致维护困难。 全局变量:过度使用全局变量,导致程序的状态难以跟踪。

5.1 常量


在编程中,我们经常需要使用一些常量,如数字、字符串等。然而,直接在代码中硬编码这些常量是一个不好的习惯,因为它们可能会在未来发生变化,导致维护困难。例如:


def calculate_score(score):
if (score > 60) {
// do something}

这里的60就是一个硬编码的常量,导致后续维护困难,正确的做法应该使用常量或者枚举来表示。例如:


PASS_SCORE = 60;
def calculate_score(score):
if (score > PASS_SCORE) {
// do something }

这样,我们就可以避免硬编码,提高代码的可维护性。


5.2 全局变量


过度使用全局变量在全局范围内都可以访问和修改。因此,过度使用全局变量可能会导致程序的状态难以跟踪,增加了程序出错的可能性。例如:


counter = 0
def increment():
global counter
counter +
= 1

这段代码的问题在于,它使用了全局变量 counter,使得程序的状态难以跟踪。我们应该尽量减少全局变量的使用,而是使用函数参数和返回值来传递数据。例如:


def increment(counter):
return counter + 1

这段代码没有使用全局变量,而是使用函数参数和返回值来传递数据,使得程序的状态更易于跟踪。


06、测试和调试



  • 错误的习惯


单元测试:不进行单元测试会导致无法及时发现和修复代码中的错误,增加代码的不稳定性和可维护性。 边界测试:不进行边界测试可能导致代码在边界情况下出现错误或异常。 代码的可测试性:有些情况依赖于当前条件,使测试变得很难。

6.1 单元测试


单元测试是验证代码中最小可测试单元的方法,下面是不添加单元测试的案例:


def add_number(a, b):
return a + b

在这个示例中,我们没有进行单元测试来验证函数 add_number 的正确性。正确示例:


import unittest

def add_number(a, b):
return a + b

class TestAdd(unittest.TestCase):
def add_number(self):
self.assertEqual(add(2, 3), 5)

if __name__ == '__main__': unittest.main()

在这个示例中,我们使用了 unittest 模块进行单元测试,确保函数 add 的正确性。


6.2 边界测试


边界测试是针对输入的边界条件进行测试,以验证代码在边界情况下的行为下面是错误示例:


def is_even(n):
return n % 2 == 0

在这个示例中,我们没有进行边界测试来验证函数 is_even 在边界情况下的行为。正确示例:


import unittest

def is_even(n):
return n % 2 == 0

class TestIsEven(unittest.TestCase):
def test_even(self):
self.assertTrue(is_even(2))
self.assertFalse(is_even(3))

if __name__ == '__main__': unittest.main()

在这个示例中,我们使用了 unittest 模块进行边界测试,验证函数 is_even 在边界情况下的行为。


6.3 可测试性


代码的可测试性我们需要编写测试来验证代码的正确性。如果我们忽视了代码的可测试性,那么编写测试将会变得困难,甚至无法编写测试。例如:


def get_current_time():
return datetime.datetime.now()

这段代码的问题在于,它依赖于当前的时间,这使得我们无法编写确定性的测试。我们应该尽量减少代码的依赖,使得代码更易于测试。例如:


def get_time(now):
return now

这段代码不再依赖于当前的时间,而是通过参数传入时间,这使得我们可以编写确定性的测试。


07、性能优化



  • 错误的习惯


过度优化:过度优化可能会导致代码难以理解和维护,甚至可能会引入新的错误。 合适的数据结构:选择合适的数据结构可以提高代码的性能。

7.1 过度优化


我们往往会试图优化代码,使其运行得更快。然而,过度优化可能会导致代码难以理解和维护,甚至可能会引入新的错误。例如:


def sum(numbers):
return functools.reduce(operator.add, numbers)

这段代码的问题在于,它使用了 functools.reduce 和 operator.add 来计算列表的和,虽然这样做可以提高一点点性能,但是这使得代码难以理解。我们应该在保持代码清晰和易读的前提下,进行适度的优化。例如:


def sum(numbers):
return sum(numbers)

这段代码使用了内置的 sum 函数来计算列表的和,虽然它可能比上面的代码慢一点,但是它更清晰、易读。


7.2 没有使用合适的数据结构


选择合适的数据结构可以提高代码的性能。使用不合适的数据结构可能导致代码执行缓慢或占用过多的内存。例如:


def find_duplicate(numbers):
duplicates = []
for i in range(len(numbers)):
if numbers[i] in numbers[i+1:]:
duplicates.append(numbers[i])
return duplicates

在这个示例中,我们使用了列表来查找重复元素,但这种方法的时间复杂度较高。我们可以使用集合来查找元素。例如:


def find_duplicate(numbers):
duplicates = set()
seen = set()
for num in numbers:
if num in seen:
duplicates.add(num)
else:
seen.add(num)
return list(duplicates)

我们使用了集合来查找重复元素,这种方法的时间复杂度较低。


08、代码安全性



  • 错误的习惯


输入验证:不正确的输入验证可能导致安全漏洞,如 SQL 注入、跨站脚本攻击等。 密码存储:不正确的密码存储可能导致用户密码泄露。 权限控制:不正确的权限控制可能导致未经授权的用户访问敏感信息或执行特权操作。

8.1 输入验证


没有对用户输入进行充分验证和过滤可能导致恶意用户执行恶意代码或获取敏感信息。例如:


import sqlite3
def get_user(username):
conn = sqlite3.connect('database.db')
cursor = conn.cursor()
query = f"SELECT * FROM users WHERE username = '{username}'"
cursor.execute(query)
user = cursor.fetchone()
conn.close()
return user

在这个示例中,我们没有对用户输入的 username 参数进行验证和过滤,可能导致 SQL 注入攻击。正确示例:


import sqlite3

def get_user(username):
conn = sqlite3.connect('database.db')
cursor = conn.cursor()
query = "SELECT * FROM users WHERE username = ?"
cursor.execute(query, (username,))
user = cursor.fetchone()
conn.close()
return user

在这个示例中,我们使用参数化查询来过滤用户输入,避免了 SQL 注入攻击。


8.2 不正确的密码存储


将明文密码存储在数据库或文件中,或使用不安全的哈希算法存储密码都是不安全的做法。错误示例:


import hashlib

def store_password(password):
hashed_password = hashlib.md5(password.encode()).hexdigest()
# 存储 hashed_password 到数据库或文件中

在这个示例中,我们使用了不安全的哈希算法 MD5 来存储密码。正确示例:


import hashlib
import bcrypt

def store_password(password):
hashed_password = bcrypt.hashpw(password.encode(), bcrypt.gensalt())
# 存储 hashed_password 到数据库或文件中

在这个示例中,我们使用了更安全的哈希算法 bcrypt 来存储密码。


8.3 不正确的权限控制


没有正确验证用户的身份和权限可能导致安全漏洞。错误示例:


def delete_user(user_id):
if current_user.is_admin:
# 执行删除用户的操作
else:
raise PermissionError("You don't have permission to delete users.")

在这个示例中,我们只检查了当前用户是否为管理员,但没有进行足够的身份验证和权限验证。正确示例:


def delete_user(user_id):
if current_user.is_authenticated and current_user.is_admin:
# 执行删除用户的操作
else:
raise PermissionError("You don't have permission to delete users.")

在这个示例中,我们不仅检查了当前用户是否为管理员,还检查了当前用户是否已经通过身份验证。


09、版本控制和协作



  • 错误的习惯


版本提交信息:不合理的版本提交信息会造成开发人员难以理解和追踪代码的变化。 忽略版本控制和备份:没有备份代码和版本控制的文件可能导致丢失代码、难以追溯错误来源和无法回滚等问题。

9.1 版本提交信息


不合理的版本提交信息可能导致代码丢失、开发人员难以理解等问题。错误示例:


git commit -m "Fixed a bug"

在这个例子中,提交信息没有提供足够的上下文和详细信息,导致其他开发人员难以理解和追踪代码的变化。正确的做法是提供有意义的提交信息,例如:


$ git commit -m "Fixed a bug in calculate function, which caused grade calculation for scores below 60"

通过提供有意义的提交信息,我们可以更好地追踪代码的变化,帮助其他开发人员理解和维护代码。


9.2 忽略版本控制和备份


忽略使用版本控制工具进行代码管理和备份是一个常见的错误。错误示例:


$ mv important_code.py important_code_backup.py
$ rm important_code.py

在这个示例中,开发者没有使用版本控制工具,只是简单地对文件进行重命名和删除,没有进行适当的备份和记录。正确示例:


$ git clone project.git
$ cp important_code.py important_code_backup.py
$ git add .
$ git commit -m "Created backup of important code"
$ git push origin master
$ rm important_code.py

在这个示例中,开发者使用了版本控制工具进行代码管理,并在删除之前创建了备份,确保了代码的安全性和可追溯性。


10、总结


好的代码应该如同一首好文,让人爱不释手。优雅的代码,不仅是功能完善,更要做好每一个细节。


最后,引用韩磊老师在《代码整洁之道》写到的一句话送给大家:



细节之中自有天地,整洁成就卓越代码。


以上是本文全部内容,欢迎分享。




原创作者|孔垂航


作者:腾讯云开发者
来源:juejin.cn/post/7257894053902565433
收起阅读 »

一次操蛋的面试经历

故事发生在10年前,因为自己的不成熟,没想好就跟老板提了离职,不得不真的开始找工作(详情见之前的文章,末尾有链接)。很快,就拿到了下家的 offer,约定3月31日入职。 猎头又推荐了「小而美」的豌豆荚,我不想去,因为他们周六也上班。猎头说周六基本是打酱油,而...
继续阅读 »

故事发生在10年前,因为自己的不成熟,没想好就跟老板提了离职,不得不真的开始找工作(详情见之前的文章,末尾有链接)。很快,就拿到了下家的 offer,约定3月31日入职。


猎头又推荐了「小而美」的豌豆荚,我不想去,因为他们周六也上班。猎头说周六基本是打酱油,而且工作氛围号称 Google 范,文艺风,不妨聊聊。吼啊,那就聊聊。


为了叙事方便,先放个当年的日历:


日历


3月15日,周六,下午,连续面了3轮后, CEO 王俊煜(下称 junyu)不在,HR 让我回去等消息,路途遥远,到家已经天黑了。


周一,猎头告诉我挂了,不知道原因,建议我找 HR 争取下,看能否跟 junyu 聊聊,也许会有转机。我拒绝了,强扭的瓜不甜,而且我也不喜欢周六上班。


周四,收到一位面试官的邮件,他觉得我还不错,想约我再聊聊,全文如下:


邮件


我那时工作刚满20个月,其中2个月,因为部门快要黄了,整天无所事事的。最后6个月,搞 iOS 去了。所以,真正做 Android 的时间也就1年,他说的没错,我确实掌握的不够深入。虽然邮件里直接指出了我的不足,但我觉得更多的还是肯定吧。


说句不要脸的,我喜欢这种被人欣赏的感觉。类似的事,在我身上发生过挺多次了,只可惜因为自己的原因,没能接住那些泼天的富贵,先挖个坑,未来有时间再填。


最后约的是3月21日,周五,又去面了两轮,junyu 还是不在,继续回去等消息。周六,发邮件给之前的面试官咨询结果,得知面试通过了,需要等 HR 约 junyu 的时间再聊聊。


距离我入职下家公司只剩一周了,豌豆荚 HR 迟迟没有动静。我多次发短信催促她,得到的答复都是还在约。印象中最后约的是29号,周六,我从狼厂离职的第2天。


当天下午,我到的时候,junyu 还在面试中,等了半个多小时,他才完事。简短的自我介绍和项目介绍后,我说自己开发了一款计算器 APP,获得了不错的用户反馈。他一脸鄙夷的眼神问我:



什么?计算器?



对,我正准备展示它的功能和获得的奖项,他打断了我,改问其他问题了。具体问题不记得了,只记得他对我回答的反馈基本都是「嗯」,外加十分不屑的表情,而且大多数时间是在看电脑。5分钟后,他说今天就到这,有消息会通知我。


很快,HR 给我发了个拒信:



经过综合评估,还是觉得不合适



收到,我谢谢你。


工作快12年了,我面过很多公司,成功了多次,也失败了多次。但没有哪次像这次令人生气,如此不尊重候选人,这是唯一的一次。我不知道他的居高临下是为何,我的出现浪费了他的宝贵时间?哪怕是面对一问三不知的面试者,也不应该如此一副高傲的态度。


也许是因为年纪轻轻,创业有所成,于是飘飘然,开始藐视众生了?又或者因为狼厂以19亿美元收购「91手机助手」,觉得自己也行?


百度收购91


当初在狼厂时,周围的同事就没有一个人觉得「91无线」能值这么多钱,妥妥的冤大头。实话说,狼厂还不如把那个「91」收购了,给「狼友」们谋福利,还能赋能、反哺后来的视频业务,懂的都懂。。。


除了那场面试,HR 的表现也是让人无语,在我反复提醒她我就要入职下家了,她依然无动于衷,迟迟未能约好时间,而且每次都是我主动联系她的。哪怕是对待备胎,女神也会偶尔主动一下吧?难道是因为我太主动,把我当舔狗了?


几个月后,豌豆荚竟然断缴了很多员工的社保,据说是因为缴费的卡上余额不足,导致扣款失败,庆幸当初他们没看上我,这种奇葩的事也能出现。


豌豆荚断缴社保


对于给我发邮件的那位工程师,我还是很感激的,感谢他又给了我一次机会,也感谢他让我知道了物种的多样性。巧合的是,多年以后,我在面试另一家大厂时,又碰到了他,他已经是一名中层管理人员了,手下估计有几百号人。不过,因为跟 HR 待遇没谈拢,我最后没去。


本文纯属吐槽,以上内容,绝对真实,如有雷同,深表同情。


在我告诉猎头挂了后,他告诉我还有一家跟豌豆荚风格类似的「小而美」的公司,建议我去聊聊看。我说马上要入职新公司了,不想再面了,况且我都没听过这个公司。彼时,那个小公司名叫「今日头条」。




作者:野生的码农
来源:juejin.cn/post/7361650229739716627
收起阅读 »

和一个做直播的朋友聊了聊

昨天,昨天和滨江的一个朋友聊了聊,他是那边的一个公司产品负责人,也算是核心合伙人的角色之一,他们的公司是做直播业务的,大概有七八十人的团队,开发人员大概是30人左右,占比35%左右,其中里面还有一个CTO角色,或者说技术总监的角色,其他的全部都是干活的小兵和小...
继续阅读 »

昨天,昨天和滨江的一个朋友聊了聊,他是那边的一个公司产品负责人,也算是核心合伙人的角色之一,他们的公司是做直播业务的,大概有七八十人的团队,开发人员大概是30人左右,占比35%左右,其中里面还有一个CTO角色,或者说技术总监的角色,其他的全部都是干活的小兵和小组长之类的。


我们主要聊到了两个不同规模的公司的工作模式的问题,因为我所在的是阿里巴巴应该是非常典型的超大型互联网公司,而他们公司这个人数刚好是属于小型的互联网公司。


他的公司主要是做直播业务的,大家都很熟悉诸如抖音快手这样的直播平台,这么小的公司怎么能做好一个直播平台呢?那他们的业务模式也非常的经典,那就是做一些非常小众的网红和用户产品。



一、直播市场的长尾用户


他描述了一下他自己的一些对于直播和用户的一些观点和理解,比如说现在众所周知的类似于抖音这样极大的平台,有超级大的网红IP,也有无数的粉丝。但是国内互联网用户基数非常之大,存在非常多的长尾用户,比如一些粉丝想在平台上获得一些娱乐感和交互感,这个是抖音这种大平台所满足不了的。另外一方面有大量的尾部网红在抖音这种大平台上面往往也拿不到任何的流量,所以他们也需要一种更小的平台,有充足的流量扶持。


在这个背景下就有了针对这些长尾用户的一些小的直播平台,那在小的直播平台上,哪怕你再小的网红,你都会有一些流量上面的倾斜,对于用户来说,在抖音上给大V打赏几万可能主播都不会理你,但是你在小平台上直接给主播进行打赏交互,就会变得更加的简单和高效。毕竟我们可以想象一下,很多花不起大价钱的“屌丝”用户,可能在这种小平台上面砸个几百几千,可能就能够约网红出来吃个饭,聊个天什么的。一些尾部网红也是一样,长期在抖音中大平台上面基本上没有流量,也没人关注和在意,但是到小平台上面可能就有比较多的几十个,甚至几百个粉丝过来和你交互和聊天打赏,很容易形成一个正反馈。


所以对于刚刚起步的网红来说,在这种小平台上面去发展,获得自己的正反馈和积累初步的影响力是非常的必要的。那对于一些没有太多钱、时间又空闲的粉丝们来说,对于小平台上面也能够有一个快速的通道去接触到这些主播或者兴趣相同的朋友。


于此同时,各行各业,蚊子肉都是大平台吃不到也不想吃的,这类长尾用户是大的平台是往往无法覆盖的,也是看不上的,所以给了这些小型的平台很多的发展空间,这个就是非常典型的一种长尾生态形式。也非常符合之前我所推荐的那本书叫做《长尾理论》,这种小平台因为它的边际成本是非常的低的,所以它可以在各个地方花钱去投放广告,吸引长尾客流,主打各种形式的娱乐化的直播并从中抽佣。


我们也可以看到这种平台本身也不大可能做的非常大,一方面它可能在形式和内容上面都可能走一些擦边或者灰色的方式,另外一方面对他们自己来说,他们也不想做的做大,做大以后以后就会面临着更加复杂的问题,比如监管问题。所以很多这种小型的平台活的非常的滋润,从来没想着做大做强,而是在自己的一亩三分细分领域里深耕,现金流反而还比大平台的还更加的充足。


他们公司在前两年就寻求上市,因为经济的原因中止,但这也就说明他这种模式实际上非常赚钱,现金流是非常的稳定的。


二、快进快出的用人理念


除了这种非常好的商业模式之外,另外一个讨论点就是我们工作模式上面的最大的区别。他提到了他们公司的员工的离职率是非常高的,基本上几个月、半年可能就大量的技术人员离职汰换。这个也很简单,她说对于新招聘的员工来说,如果半个月上不了手的情况下的话,就会在试用期里面就会解聘掉,主打就是追求实用主义,员工拿来即用没有培养一说。对于一个小的技术公司来说,它的成本控制的非常的严格,如果员工短时间内不能上手的情况下的话,对他们来说是没有任何价值的,所以对于员工都采用快进快出这样的方式,完全不像我们大平台大企业,可能给到一个员工的成长时间,短则三个月,大长则半年,一年。而小公司完全吃不消这种巨大的人力培养成本。


另外就是对于他们一些比较资深的工程师来说,工龄时间也不会太长,因为他们给不了员工的一个向上的晋升通道。当个员工工作了两年到三年,技术能力各方面能力都提高了,以后也没办法往上升或者持续加薪,因为毕竟上面只有一个技术合伙人,总不能把这个技术合伙人给顶下去吧,所以他们大部分的员工工作了两年到三年之后,技术能力上面都有非常大的成长之后,往往就会跳出这个小厂去寻求其他的大厂机会。


然后他们公司本身对于技术的追求也不深,大部分完全采用的是“拿来即用”的原则,他说在早期的时候做平台还会去找一些开源源码自己来部署,到了现在大部分能力都有非常成熟的第三方厂家来支持,他们公司技术人员只要做集成和包装就可以了。现在据我所知,类似于阿里云这样的云平台,已经把整个云计算API、网络直播的API,甚至很多底层技术全部做的非常好,都打包成SDK或者封装成API,所以上层业务方只要购买服务后把API包装一下,封装就可以直接使用了,五分钟生成一个直播平台APP已经没有任何问题了。



以我的理解,一个正常的工作了半年到一年的同学,我觉得在这种SDK或者API的加成下,就应该在一个星期内能创建出来一个直播平台APP了。所以很明显在这种基础能力非常强大的情况下,他们公司就会可以把成本压的更小,他们可以随时的去调整自己的业务方向和迭代,基本上几周就会有一个小版本迭代或者出全新的APP。


我问了一下,他们有没有一个知名的应用市场APP,给我的答案是他们开发成了很多非常小的一些APP,然后在应用市场上面去打广告引流,用户量和粉丝量都不算大,明显就能看到这种模式主打一个灵活、主打分布式。


三、反脆弱的商业形式


所以相对于小厂和中厂来说,不管从业务模式上还是从技术架构上,还是从经营理念上完全不可同日而语。但不得不说,我觉得正如我们的自然界生态系统一样,有些时候很微小的生物往往能够在漫长的生态环境中存活下来,比如蟑螂老鼠,而有一些庞然大物,诸如恐龙猛犸象这样的大体积的生物,反而还容忍不了生态气候的变化而灭绝。


而对于他这样小的一些经济体,几十个人,有自己的一些核心的产品模式,并且能够快速的迭代,对成本控制严格,对经济变化敏感,反而还能够存活到各个不同的周期里面,所以这我觉得也是一种值得我们羡慕的地方。这也是知名作家塔勒布在他的《反脆弱》一书里提到的一种形式,这种公司反而具备更强的反脆弱性,当经济越差,他们不仅不受影响,反而反弹变得更强壮、盈利性更强。


最后一步来说,对于程序员来说,根据自己的兴趣、爱好、能力水平,在当前的经济周期找到一个比较合适自己的平台,能够锻炼到自己的能力,不管是从技术还是从业务经营,产品各个方面都有所成长,那对自己来说就是好事。对于创业者来说也未必要盯着非常大的市场,动不动就来个规模效应,有时候去做这种非常小微公司和长尾市场,往往活得会更加的滋润和惬意。


作者:ali老蒋
来源:juejin.cn/post/7290898686582669351
收起阅读 »

互联网大厂,开始对领导层动刀了

最近,我周围有挺多互联网大厂leader级别的同事或朋友,被“降本增效”了。 其中最有意思的是,我的前同事老Z今年刚刚晋升了一级,在这个级别上还没待热乎了,然后就下来了。 有句话是这么说的:“世界上最残忍的事,莫过于让其拥有一切,然后再剥夺其所有。” 有次我跟...
继续阅读 »

最近,我周围有挺多互联网大厂leader级别的同事或朋友,被“降本增效”了。


其中最有意思的是,我的前同事老Z今年刚刚晋升了一级,在这个级别上还没待热乎了,然后就下来了。


有句话是这么说的:“世界上最残忍的事,莫过于让其拥有一切,然后再剥夺其所有。”


有次我跟老Z吃饭,他苦笑着跟我说:“妈的,如果不晋升,没准还能待下去呢,晋升之后反而目标变大了。”


我问他:“那你最近看新机会的结果怎么样,有没有拿到比较满意的offer呢?”


他说:“面试机会倒是不少,大厂已经面了五六个,但最后都无疾而终了。”


接下来,他又把话题聊了回来,说:“你说,如果公司对我不满意,为什么还给我晋升呢,但如果公司对我满意,又为什么还要裁我呢?”


我给他举了一个这样的例子:“就算大款给小三买奢侈品,让她住豪宅,但并不代表不会甩了她啊,对吧。”


他听了哈哈大笑,似乎释怀了。


接下来,我盘点一下,具备什么特征的管理层最容易被“降本增效”,以及在未来的日子里,我们应该如何应对这种不确定性。


“降本增效”画像


跟大家聊下,哪类用户画像的领导层最容易被“降本增效”,请大家对号入座,别心存侥幸。


(1)非嫡系


不管到哪天,大厂也都是个江湖,是江湖就有人情世故。


如果你不是老板的嫡系,那公司裁员指标下来了,你不背锅谁背锅,你不下地狱谁下地狱。


你可能会说:“我的能力比老板的嫡系强啊,公司这种操作,不成了劣币驱逐良币了吗?”


其实,这个时候对于公司来说,无论是劣币还是良币,都不如人民币来得实在。


人员冗余对于公司来讲就是负担,这个时候谁还跟你讲任人唯亲还是任人唯贤啊。


(2)老员工


可能有人会这么认为,老员工不但忠诚,而且N+1赔的钱也多,为什么会优先裁掉老员工呢。


我认为,一个员工年复一年、日复一日地待在熟悉的工作环境,就犹如温水煮青蛙一样,很容易停留在舒适区,有的甚至混成了老油子。


而老板最希望看到的是,人才要像水一样流动起来,企业要像大自然一样吐故纳新,这样才会一直保持朝气和活力。


总之,老板并不认为员工和公司一起慢慢变老,是一件最浪漫的事。


(3)高职级


对于公司来讲,职级越高的员工,薪资成本也就越高,如果能够创造价值,那自不必多说,否则的话,呵呵呵。。。


现在越来越多的公司,在制定裁员目标的时候,已经不是要裁掉百分之多少的人了,而是裁员后把人均薪资降到多少。


嗯,这就是传说中的“降均薪”,目标用户是谁,不多说也知道了吧?


(4)高龄


35+,40+,嗯,你懂的。


老夫少妻难和谐,大龄下属跟小领导不和谐的几率也很大,一个觉得年轻人不要抬气盛,另外一个觉得不气盛就不是年轻人。


不确定性——在职


恭喜你,幸存者,老天确实待你不薄,在应对不确定性这件事情上,给了你一段时间来缓冲。


如果你已经35+了,那接下来你需要把在职的每一天,都当成是最后一天来度过,然后疯狂地给自己找后路,找副业。


一定要给你自己压力,给自己紧迫感。


因为说不定哪天,曾经对你笑圃如花的HR,会忽然把你叫到一个偏僻的会议室里,面无表情地递给你一式两份的离职协议书,让你签字。


在你心乱如麻地拿起签字笔之际,她没准还得最后PUA你几句:“这次公司不是裁员,而是优化。你要反思自己过去的贡献,认识到自己的不足,这样才能持续发展。


当然,你有大厂员工的光环加持,到市场上还是非常抢手的,你要以人才输出的高度来看这次优化,为社会做贡献。”


至于找后路和副业的方式,现在网上有很多类似的星球,付费和免费的都有,加一个进去,先好好看看,主要是先把思路和视野打开。


当然,如果你周围要是有一个副业做得比较好的同事,并且他愿意言传身教你,那就更好了。


然后,找一个自己适合的方向和领域,动手去做,一定动手去做,先迈出第一步,可以给自己定一个小目标,在未来几个月内,从副业中赚到第一次钱。


从0到1最难,再接下来,应该就顺了。


不确定性——不在职


如果35+的你刚刚下来,而且手头还算殷实的话,我先劝你第一件事:放弃重返职场。


原因很简单,如果一个方向,随着你经验的积累和年龄的增长,不仅不会带来复利,而是路会越走越窄,那你坚持的意义是什么?难道仅仅是凑合活着吗?


第二件事,慢下来,别立马急急忙忙地找出路,更不要一下子拿出很多本金砸在一个项目上。据说,有的项目是专门盯着大厂员工的遣散费来割韭菜的。


有人会说,在职的人你劝要有紧迫感,离职的人你又劝慢下来,这不是“劝风尘从良,逼良家为娼”吗?


其实不是的,只是无论是在职还是离职,我们都需要在某件事情的推进上,保持一个适合且持久的节奏,不要止步不前,也不要急于求成,用力过猛。


第三件事,就是舍得把面子喂狗,不要觉得做这个不体面,做那个有辱斯文,只要在合理合法的情况下,能赚到钱才是最光荣的。


接下来,盘点周围可用资源,调研有哪些领域和方向适合你,并愿意投入下半生的精力all in去做。


这个过程可能会很痛苦,尤其对于一些悲观者来说,一上来会有一种“世界那么大,竟然再也找不到一个我能谋生的手段”的感觉,咬牙挺过去就好了。


这里说一句,人只要自己不主动崩,还是远比想象中耐操很多的。


结语


好像也没什么好说的,大家各自安好,且行且珍惜吧。


作者:托尼学长
来源:juejin.cn/post/7317859658285318170
收起阅读 »

记录我的程序猿副业首笔创收

在这个充满机遇的数字时代,我,一个普通的程序猿,编程爱好者,终于在云端源想这个平台上收获了属于我的第一桶金。这是一个关于兼职、学习与成长的故事,希望能激发同在编程路上的你,勇敢迈出那一步。先晒晒我的首笔收入:一个普通的周末,我像往常一样,泡上一杯咖啡,坐在电脑...
继续阅读 »

在这个充满机遇的数字时代,我,一个普通的程序猿,编程爱好者,终于在云端源想这个平台上收获了属于我的第一桶金。这是一个关于兼职学习与成长的故事,希望能激发同在编程路上的你,勇敢迈出那一步。

先晒晒我的首笔收入:


一个普通的周末,我像往常一样,泡上一杯咖啡,坐在电脑前,漫无目的地浏览着技术论坛偶然间看见“赢取丰厚收益”的推送,好奇心驱使我点击进去,发现这是一个内容征集的平台,里面都是一些开发实战项目内容征集,让像我这样渴望更多实战经验的程序猿,有机会接取真实项目,获得报酬的同时,也能锻炼自己的技能。

去看看云端源想内容征集

起初,我心中充满了疑虑:“我能行吗?”但转念一想,不试试怎么知道呢?于是,我开始仔细浏览平台上的需求列表,寻找与自己技能相匹配的任务。里面的项目还是挺多的,有简单的,也有比较复杂的,都可以根据自己的水平进行选择。经过一番筛选,我锁定了一个视频播放网站需求。


于是,我点击需求详情中的立即咨询,通过与在线客服的沟通,了解了需求的细节,确认我可以完成,才接下了这个需求,整个过程中,所有的疑问都可以在云端源想平台上很顺畅的进行沟通。而且平台里面的交付标准也写的很详细了已经。接下来,便是紧锣密鼓的开发阶段。我利用业余时间,一点点开始搭建网站,调试等等。每当遇到难题,我都会第一时间在云端源想的社区寻求帮助,或者也可以去问他们的在线老师和客服人员,那里总有人热心解答,过程中有问题也可以快速解决

反复沟通后,大纲的确认输出


前端项目创建后,


对接阿里点播服务的几个接口的编写


经过几周的努力,项目终于完成,当我的产出成果被满意验收,那一刻的成就感难以言喻。不久后,我收到了云端源想转来的报酬,虽然金额不大,但这却是我程序员兼职的第一桶金,意义非凡。它不仅证明了我的努力没有白费,更点燃了我继续深、挑战复杂项目的决心。


从此,我成了云端源想的常客,不仅技术日益精进,还结识了一群志同道合的朋友。现在,我感激那个勇于迈出第一步的自己,以及提供机会的云端源想平台

这就是我偶然的一个副业机会。副业虽然在时间上给我带来了较大的压力,但却给我带来了更多的收入,重构了我的收入结构,帮助我走出了“晋升无望,收入见顶,而开支直线上升”这种困境,让我有了更强的自我效能感和财务自信。

因为我感受到了发展斜杠事业的好处,所以,特地总结出来,分享给大家。

  • 做副业有非常多的好处:
  • 多赚点钱,提升生活品质;
  • 改善收入结构,应对收入见顶焦虑,增加财务自信;
  • 养多元化自我价值;
  • 探索更多可能性;
  • 打造备胎,应对裁员等黑天鹅事件;
  • 掌控生活。

如果你也想利于自己的技能赚钱,正寻找实战机会,不妨来云端源想看看,我觉得对我们程序猿还是很友好的,一方面可以赚到一部分兼职的钱,还能边学习边提升,也累积了自己的工作经验,真是一举三得。墙裂建议有时间想尝试,想挑战的程序猿朋友们可以去看看有没有适合自己的兼职项目,加入渠道给大家奉上。

去云端源想看看内容征集

收起阅读 »

这个网站真的太香了!居然可以免费使用AI聊天工具和“智能AI聊天助手”项目源码!!!

宝子们,在这个AI爆火的时代,你是否还在因为无法使用ChatGpt而头疼?是否还在寻觅一款国内的好用AI工具呢?好消息!小编花费三个月终于找到了一个可以免费使用AI聊天工具的网站,由于这个网站之前一直在内测阶段,所以就没有给大家分享。刚好,近期这个网站正式上线...
继续阅读 »

宝子们,在这个AI爆火的时代,你是否还在因为无法使用ChatGpt而头疼?是否还在寻觅一款国内的好用AI工具呢?


好消息!小编花费三个月终于找到了一个可以免费使用AI聊天工具的网站,由于这个网站之前一直在内测阶段,所以就没有给大家分享。




刚好,近期这个网站正式上线了。小编今天就来好好跟大家聊聊这个网站有哪些便宜好用的功能,之所以推荐这个网站也是因为它不光好用,还有大量免费的功能,像平时写代码遇到想不起来的,直接去这个网站用AI搜索一下,简直不要太香!


对了!这个网站的名称叫“云端源想”!大家记一下,可以直接百度搜索去体验哦!


下面就正式给大家介绍这个网站,以及我推荐大家用它的原因:


首先我先说一下,它近期不是刚上线嘛,有个巨大的福利在等着大家,就是除了前面我提到的免费使用AI聊天工具之外,还可以领取搭建这个AI聊天工具的源码!!简直了!


这对于想要找项目实战练手的编程新手宝子们,简直是“饥时饭,渴时浆”的事情,所以看到了,不要犹豫,直接点进去领到手再说!反正不要钱!


AI聊天:AI聊天工具

项目源码:“智能AI聊天助手”项目源码


这个是网站的活动海报图,也给大家放在这里啦!




说完能领取的福利之后,我再来给大家说说云端源想这个网站值得逛的几个版块,帮助大家快速找到自己想要的功能。


1、微实战




这个板块在我看来是很实用的,它里面的项目感觉都是从实际应用的功能点拆分出来的项目实战,非常地有针对性。


比如我需要开发一个线上商城,就可以把这里面的网站支付的源码拿来用,不仅能快速对接,还为我省下了很多时间,然后我就可以早早下班,不用秃头啦!简直是提升效率的好帮手!


我发现目前站里这些微实战只需要两位数就可以拿到,有时候还有限时免费的:完整的项目源码项目部署教程视频教程,甚至还有配套的免费直播课,可以说是非常有性价比了,上面给大家说免费领取的AI聊天助手就是这个板块的内容。




总之,这个微实战板块是一个非常实用的资源,无论你是新手还是有经验的开发者,都可以从中受益。通过参与这些项目实战,你可以提升自己的实际开发经验,学习到更多的技术和工具,同时也可以提高工作效率,更好地应对实际开发中的挑战。


所以!好东西要和大家一起分享,我分享给大家了,大家也可以分享给身边的朋友们哦!


2、智能AI工具




这里面目前我看到了三个AI工具,图片清晰度增强、文字合成语音和智能AI问答,鉴于都是免费的所以我都体验了一下,对我来说最实用的就是这个免费的AI问答了。



平时写东西找不到灵感,或者遇到不懂的东西,我都会在这问问AI,使用频次快超过百度了,用它辅助写代码是真的很牛,我也试过好多其他的AI产品,免费的里面对比下来这个真的好用!强烈推荐!!!


3、社区动态




这就是一个可以发布动态的板块,很适合上班摸鱼,哈哈哈!


如果上班或者学习累了,可以来逛逛看看别人发的帖子,寻觅一个有趣的灵魂,喜欢分享的朋友也可以自己发帖,我是没事了就来刷刷,看看有没有什么新鲜事可以在线吃瓜!!


4、编程体系课



里面开通了四门当下比较热门的课,这个就没什么说的,大家在别的学习网站也有,都大差不差。


值得一提的是,云端源想把重难点的知识点提炼出来组成了一个知识库,这样我可以很快速找到我想要学习的点,比较有针对性。




5、在线编程




这个板块也是一个比较少见功能板块了,可以在线编辑运行代码,比较有意思的是可以邀请别人一起协作编程,这个我用的比较少,感兴趣的朋友可以自行探索探索哈!


另外还有一个论坛板块,里面有各种质量比较高技术文章,有时候我写东西也会在里面参考参考,这就没啥好说的,我就不过多去说这个板块了。


以上就是我给大家推荐云端源想这个网站的原因了,不单单是喊大家一起来薅羊毛领源码!也是真心想给开发的朋友们推荐一个好用的工具网站!那么今天的分享就到这里啦!


最后!强烈建议大家不要错过这个宝贵的实战源码!AI工具用不用咱都不说!能够免费获取的资源才是硬道理!别犹豫了,赶紧点这里领取你的福利吧!

收起阅读 »

环信rest可视化工具(macOS版)

介绍这是个rest可视化工具,虽然简陋得破洞,但是贼特么好用不过需要苹果电脑才可以运行的,如果没有苹果电脑,建议某宝装个黑苹果,并安装xcode,即可运行.能干啥?1、通过该工具可以请求环信rest接口,同时可以获取curl命令参数,帮助开发者直观的理解环信每...
继续阅读 »

介绍

这是个rest可视化工具,虽然简陋得破洞,但是贼特么好用
不过需要苹果电脑才可以运行的,如果没有苹果电脑,建议某宝装个黑苹果,并安装xcode,即可运行.


能干啥?

1、通过该工具可以请求环信rest接口,同时可以获取curl命令参数,帮助开发者直观的理解环信每一个rest接口;

2、通过工具可以快速实现一些简单功能,例如创建群聊,加入群聊,添加好友等;也可以通过工具利用rest接口发送消息,以便快速地测试部分业务逻辑。


下载地址:

https://gitee.com/easemob_1/swiftui_easemob_rest_tool


如何使用

使用起来很简单,参考下图





有任何使用问题可以加微信(备注:rest工具)咨询,欢迎一起维护这个项目。




收起阅读 »

为安卓猿准备的Google I/O 2024省流版

前两天一年一度的谷歌开发者大会Google I/O 2024在大洋彼岸如期举行,在会上谷歌发布了一系列最新的技术。本文将以Android开发为核心来汇总一下大会的内容。Android 15 Beta 2来了自从Android站稳了脚跟以后(大概是在Androi...
继续阅读 »

前两天一年一度的谷歌开发者大会Google I/O 2024在大洋彼岸如期举行,在会上谷歌发布了一系列最新的技术。本文将以Android开发为核心来汇总一下大会的内容。

Android 15 Beta 2来了

自从Android站稳了脚跟以后(大概是在Android 4.3之后)基本上就是每年一个大版本的节奏,一般是在春季有预览版本,在秋季正式发布。为了抢在水果的前面,也都会在Google I/O时进行重点的宣传,所以每年的Google I/O一大看点就是新一代的Android。当然了,从去年开始AI变成了焦点,但是回到前几年时Android是绝对的焦点。

今年也不例外,在Google I/O上面也宣传了一下Android 15,并正式发布了第2个Beta版本,从功能和Feature角度来说,这个就非常接近于正式版本了。不过就如我在前面一篇文章中提到的那样,Android 15其实没啥亮点,主要集中在安全和隐私方面的加强,其余的改进也都非常的小。

关于Android 15具体的改动,可以看一下前排大佬的总结,总结的比较详细,就不重复了。

想体验Android 15 Beta 2的话,如果是谷歌的设备如Pixel系列,应该就有推送了。另外就是现在谷歌都会与厂商联动一起发布新版Android的Beta版本,这已经是好几年的传统了。就比如像小米,在15号大半夜(准确地说是16号凌晨)发布了四款机型的Android 15 Beta OTA包,手头有设备的可以体验一下。

再说一下Android 15(targetSdk 35)的适配,如前所述这一版本较上一代没啥变化,如果本身就已经适配到了Android 14(targetSdk 34),就不用再特殊适配了。

AI霸屏

从去年开始AI就是巨头们的焦点,今年更是霸屏,整个Keynote全是关于AI的,唯一提到Android的地方,也是说在Android手机上如何使用AI。在大模型这条赛道上Google是追随者,就在Google I/O前两天还被Open AI给抢了热度给恶心了一把,劈柴大叔今年略忧伤,讲Keynote的时候有点无精打彩,完全没了前几年那种激情四射。

今年Google发布了Gemini 1.5 Pro,支持1M的上下文Token,大约可以记得1500份PDF,并且演示了很多大模型的具体应用场景,像搜索,图片处理以及文字和代码生成助手。

当然,Android开发者更应该关注的是在端侧部署的大模型。时至今日,大模型已经进入了平稳提升期,大家都是在做出更强大的模型,比如参数更多,上下文更长等等。但大模型仍有一个短板就是无法在端侧的部署,特别是移动设备,如手机,平板,车机,甚至手表等,因受制于性能。目前来说,端侧使用大模型都还是使用网络API的方式,一方面这会依赖于网络,但更重要的是,这会受制于安全和隐私。端侧大部分的数据,是不能直接,也不太可能全都上传到服务器。因此端则部署大模型还是有价值可挖的,比如说对于设备的运行数据,以及像用户一些不愿分享的数据,就可以直接用端侧的大模型来直接处理。

Google发布了端侧的大模型Gemini Nano,将会集成在Android 15之中,并且它支持多模态,还是值得期待的。不过呢,目前Gemini Nano也没有具体的API,谷歌也只给了一个空头支票,在手机上选择文字,然后端侧大模型就可以求解其中的数学题。说实话,这个举例场景的不够好,写作业的场景,作业题怎么可能出现在手机里,然后还是现成的文字?也说明美帝的学生不够卷,在我朝,早就有了作业帮,猿辅导之类的拍一下题目就能给出详细求解过程。

google_io_cts_720.gif

不过Android生态一向受制于厂商,谷歌能做的事情并不多,估计只在谷歌的官方设备(Pixel)中可以用,其他的还是要靠厂商。这点就比不上水果,相信在6月份,水果应该会拿出更为接地气(有实际场景应用和开放API)的端侧大模型集成方案。

Android开发工具

这次谷歌把其大模型Gemini应用到了很多具体的场景中,Android开发官方IDE Android Studio新版本Koala中就深度绑定了Gemini,可以用来生成代码,分析代码和帮助解决其他编程问题。

code_transforms.gif

除了代码,此外Gemini还能帮忙分析错误报告,以及生成集成有Gemini API代码的项目,可见Gemini已经深度融合进了Android Studio之中。详细的可以看一看官文档。看着都挺美好 的,但其实最想知道的问题是,是否会对我们东方大国开放使用?

其他的都是一些常规的小的提升,如可穿待设备的不同模式下的预览,Compose的实时编辑以及Compose Glance(桌面小部件)预览, 以及Android Studio Profiler的改进等等。

Android开发套件

对于Android相关的开发套件,唯一提到的都是与Jetpack Compose相关的,可见谷歌对它的重视。新东西也都中规中矩,主要是在动画上面,如分享页过渡,可复用列表(Lazy list)元素的动画;文本控件支持HTML了;一个新的布局ContextualFlowRow,用以实现复杂的可复用流式布局,这个还是挺有用的;以及性能提升。详细内容可以看官方博客

compose-animation.gif

Jetpack Compose对于常规的UI来说已经没有问题,但是对于一些专业领域的UI还是无法胜任,比如像相机,视频和图像的预览和渲染还是无法在Compose中使用。好消息是,现在Google已经着手处理了,这次就基于CameraX搞了一个camera-viewfinder-compose,能够在Compose中显示相机预览。

再有就是Kotlin Multiplatform,这个是Jetbrains在主要搞的东西,谷歌也加大了配合力度(First class support),比如已经把一些Jetpack中的库添加了对KMM的支持。

参考资料


作者:alexhilton
来源:juejin.cn/post/7369527074590343219
收起阅读 »

产品经理:实现一个微信输入框

web
近期在开发AI对话产品的时候为了提升用户体验增强了对话输入框的相关能力,产品初期阶段对话框只是一个单行输入框,导致在文本内容很多的时候体验很不好,所以进行体验升级,类似还原了微信输入框的功能(只是其中的一点点哈🤏)。 初期认为这应该改动不大,就是把input换...
继续阅读 »


近期在开发AI对话产品的时候为了提升用户体验增强了对话输入框的相关能力,产品初期阶段对话框只是一个单行输入框,导致在文本内容很多的时候体验很不好,所以进行体验升级,类似还原了微信输入框的功能(只是其中的一点点哈🤏)。


初期认为这应该改动不大,就是把input换成textarea吧。但是实际开发过程发现并没有这么简单,本文仅作为开发过程的记录,因为是基于uniapp开发,相关实现代码都是基于uniapp


简单分析我们大概需要实现以下几个功能点:



  • 默认单行输入

  • 可多行输入,但有最大行数限制

  • 超过限制行术后内容在内部滚动

  • 支持回车发送内容

  • 支持常见组合键在输入框内换行输入

  • 多行输入时高度自适应 & 页面整体自适应


单行输入


默认单行输入比较简单直接使用input输入框即可,使用textarea的时候目前的实现方式是通过设置行内样式的高度控制,如我们的行内高度是36px,那么就设置其高度为36px。为什么要通过这种方式设置呢?因为要考虑后续多行输入超出最大行数的限制,需要通过高度来控制textarea的最大高度。


<textarea style="{ height: 36px }" />


多行输入


多行输入核心要注意的就是控制元素的高度,因为不能随着用户的输入一直增加高度,我们需要设置一个最大的行数限制,超出限制后就不再增加高度,内容可以继续输入,可以在输入框内上下滚动查看内容。


这里需要借助于uniapp内置在textarea@linechange事件,输入框行数变化时调用并回传高度和行数。如果不使用uniapp则需要对输入文字的长度及当前行高计算出对应的行数,这种情况还需要考虑单行文本没有满一行且换行的情况。


代码如下,在linechange事件中获取到最新的高度设置为textarea的高度,当超出最大的行数限制后则不处理。


linechange(event) {
const { height, lineCount } = event.detail
if (lineCount < maxLine) {
this.textareaHeight = height
}
}

这是正常的输入,还有一种情况是用户直接粘贴内容输入的场景,这种时候不会触发@linechange事件,需要手动处理,根据粘贴文本后的textarea的滚动高度进行计算出对应的行数,如超出限制行数则设置为最大高度,否则就设置为实际的行数所对应的高度。代码如下:


const paddingTop = parseInt(getComputedStyle(textarea).paddingTop);
const paddingBottom = parseInt(getComputedStyle(textarea).paddingBottom);
const textHeight = textarea.scrollHeight - paddingTop - paddingBottom;
const numberOfLines = Math.floor(textHeight / lineHeight);

if (numberOfLines > 1 && this.lineCount === 1) {
const lineCount = numberOfLines < maxLine ? numberOfLines : maxLine
this.textareaHeight = lineCount * lineHeight
}

键盘发送内容


正常我们使用电脑聊天时发送内容都是使用回车键发送内容,使用ctrlshiftalt等和回车键的组合键将输入框的文本进行换行处理。所以接下来要实现的就是对键盘事件的监听,基于事件进行发送内容和内容换行输入处理。


首先是事件的监听,uniapp不支持keydown的事件监听,所以这里使用了原生JS做监听处理,为了避免重复监听,对每次开始监听前先进行移除事件的监听,代码如下:


this.$refs.textarea.$el.removeEventListener('keydown', this.textareaKeydownHandle)
this.$refs.textarea.$el.addEventListener('keydown', this.textareaKeydownHandle)

然后是对textareaKeydownHandle方法的实现,这里需要注意的是组合键对内容换行的处理,需要获取到当前光标的位置,使用textarea.selectionStart可获取,基于光标位置增加一个换行\n的输入即可实现换行,核心代码如下:


const cursorPosition = textarea.selectionStart;
if(
(e.keyCode == 13 && e.ctrlKey) ||
(e.keyCode == 13 && e.metaKey) ||
(e.keyCode == 13 && e.shiftKey) ||
(e.keyCode == 13 && e.altKey)
){
// 换行
this.content = `${this.content.substring(0, cursorPosition)}\n${this.content.substring(cursorPosition)}`
}else if(e.keyCode == 13){
// 发送
this.onSend();
e.preventDefault();
}

高度自适应


当多行输入内容时输入框所占据的高度增加,导致页面实际内容区域的高度减小,如果不进行动态处理会导致实际内容会被遮挡。如下图所示,红色区域就是需要动态处理的高度。



主要需要处理的场景就是输入内容行数变化的时候和用户粘贴文本的时候,这两种情况都会基于当前的可视行数计算输入框的高度,那么内容区域的高度就好计算了,使用整个窗口的高度减去输入框的高度和其他固定的高度如导航高度和底部安全距离高度即是真实内容的高度。


this.contentHeight = this.windowHeight - this.navBarHeight - this.fixedBottomHeight - this.textareaHeight;

最后


到此整个输入框的体验优化核心实现过程就结束了,增加了多行输入,组合键换行输入内容,键盘发送内容,整体内容高度自适应等。整体实现过程的细节功能点还是比较多,有实现过类似需求的同学欢迎留言交流~


看完本文如果觉得有用,记得点个赞支持,收藏起来说不定哪天就用上啦~




作者:南城FE
来源:juejin.cn/post/7267791228872753167
收起阅读 »

Flutter:听说你最近到处和人说我解散了?

早上收到这条消息的时候我是懵圈的,明明几天前才收到下个月 Google I/O 时的 Flutter community 邮件,难道还能这样“出师未捷身先死” ?那 I/O 还开不开? 懵逼之余我又收到了另一条私信,结合起来大概理解了,Google 的裁员...
继续阅读 »

早上收到这条消息的时候我是懵圈的,明明几天前才收到下个月 Google I/O 时的 Flutter community 邮件,难道还能这样“出师未捷身先死” ?那 I/O 还开不开?


图片


图片


懵逼之余我又收到了另一条私信,结合起来大概理解了,Google 的裁员在一定程度上影响到了 Flutter Team ,而传着传着就变成了 「Google 解散 Flutter Team」 。。。。


图片


事实上大概10来天前谷歌就开启了新一轮的裁员计划,当时就提到谷歌正在实施新一轮裁员,试图削减成本并优化整个财务部门的运营,主要是作为谷歌内部重组的一部分,算是 1 月份裁员的延续,不过当时我也没在意,Flutter Team 会受到影响是必然的,毕竟 Flutter Team 规模不算小,只是没想到会变成 「Google 解散 Flutter 」这样的说法。



http://www.business-standard.com/companies/n…



image.png


而这次裁员计划里,Flutter Team 果然又受到波及,其实去年谷歌大裁员里 Flutter Team 也是受到波及,而结果就是 PC 端的推进陷入了一定程度的迟缓,还有无障碍相关的部分,总的来说,2023 年里 Flutter Team 里确实离开了不少元老和大神,但是其实一年下来,Flutter 整体并没有受到太大拖累。



而这次 Flutter Team 的裁员规模和波及范围还暂不明朗,但是人数应该不会太少,所以也不好说影响范围,但是有一点需要提的是, Flutter 是一个开源项目,总的来说他需要 Google 的投入和 Flutter Team 来维护,但是他的推进更主要还是来自社区里的广大开发者,例如国内的 AlexV525、luckysmg 等大佬的加持。


图片


当然,你说现在 Flutter Team 是否是因为人员冗余而裁员,我倒是觉得并不会,因为目前需要解决的问题和推进的 roadmap 其实很多,特别是 Flutter 的全平台特性,甚至近期开始落地的 Wasm Native ,这些都是需要大量时间和人力投入。



图片


所以裁员肯定多多少少会影响 Flutter 的计划,但是那也和「解散」不沾边,就是在大家全力准备 I/O 的时候来 layoffs ,多多少少还是有点不大“人道”的味道。


图片


图片


图片


不管怎么说,从去年开始,不管是国内还是国外,裁员基本都是主流,大家都在为社会贡献人才,只能说大环境如此,只是我是没想道两天不到就会传播成 「Google 要解散 Flutter 团队」,那再过几天会不会还冒出来 「 Flutter 凉凉,鸿蒙接手 Flutter 」的内容?


图片


不管怎么说,这些年 Flutter 算是 Google 比较大投入的项目,开猿节流时受到影响也很正常,如果还不放心,或者你可以看看下个月马上就要开 Google I/O ,来看下 Flutter 会再给你画什么饼:io.google/2024/intl/z…


图片


作者:恋猫de小郭
来源:juejin.cn/post/7362901975421337651
收起阅读 »

前端视角下的鸿蒙开发

web
前言 鸿蒙系统,一个从诞生就一直处于舆论风口浪尖上的系统,从最开始的“套壳”安卓的说法,到去年的不再兼容安卓的NEXT版本的技术预览版发布,对于鸿蒙到底是什么,以及鸿蒙的应用开发的讨论从来没停止过。 这次我们就从一个前端开发的角度来了解一下鸿蒙,学习一下鸿蒙...
继续阅读 »

前言



鸿蒙系统,一个从诞生就一直处于舆论风口浪尖上的系统,从最开始的“套壳”安卓的说法,到去年的不再兼容安卓的NEXT版本的技术预览版发布,对于鸿蒙到底是什么,以及鸿蒙的应用开发的讨论从来没停止过。


这次我们就从一个前端开发的角度来了解一下鸿蒙,学习一下鸿蒙应用的开发。



一、 什么是鸿蒙


在开始之前,先问大家一个问题,大家听说过几种鸿蒙?


其实到目前为止,我们经常听到的鸿蒙系统,总共有三种,分别是:


OpenHarmony,HarmonyOS,以及HarmonyOS NEXT。


1. OpenHarmony


OpenHarmony


OpenHarmony(开源鸿蒙系统),由开放原子开源基金会进行管理。开放原子开源基金会由华为、阿里、腾讯、百度、浪潮、招商银行、360等十家互联网企业共同发起组建。包含了“鸿蒙操作系统”的基础能力,是“纯血”鸿蒙的底座。


这个版本的鸿蒙是开源的,代码仓库的地址在这里:gitee.com/openharmony


从我个人的一些粗浅理解来看,OpenHarmony类似于Android里的AOSP,可以装到各种设备上,比如手表、电视甚至是一些嵌入式设备上,详见可见官网的一些例子


2. HarmonyOS


HarmonyOS


基于 OpenHarmony、AOSP等开源项目,同时加入了自己的HMS(因为被美国限制后无法使用GMS)的商用版本,可以兼容安卓,也可以运行部分OpenHarmony开发的鸿蒙原生应用。


这个也是目前经常被吐槽是“套壳”安卓的系统,截止到目前(2024.04)已经更新到了HarmonyOS 4.2。


3. HarmonyOS NEXT


HarmonyOS NEXT


2023年秋季发布的技术预览版,在当前HarmonyOS的基础上去除了AOSP甚至是JVM,不再兼容安卓,只能运行鸿蒙原生应用,同时对OpenHarmony的能里进行了大量的更新,增加和修改了很多API。


这个也就是所谓的“纯血”鸿蒙系统,可惜的是这个目前我们用不到,需要以公司名义找华为合作开权限,或者个人开发者使用一台Mate60 Pro做专门的开发机。并且目前由于有保密协议,网上也没有太多关于最新API的消息。



NEXT版本文档:developer.huawei.com/consumer/cn…



无法直接访问的NEXT版本的开发文档


据说目前HarmonyOS NEXT使用的API版本已经到了API12,目前官网可以访问的最新文档还是API9,所以接下来的内容也都是基于API9的版本来的。


4. 小结


所以一个粗略的视角来看,OpenHarmony、HarmonyOS以及HarmonyOS NEXT这三者之间的关系是这样的:


三者之间的关系


二、 初识鸿蒙开发


在大概知道了什么是鸿蒙之后,我们先来简单看一下鸿蒙开发的套件。下图是官网所描述的一些开发套件,包括了设计、开发、测试、上架所涉及到的技术和产品。


鸿蒙开发套件


我们这篇文章里主要讨论右下角的三个:ArkTSArkUIArkCompiler


ArkTS&ArkUI


ArkCompiler


三、 关于ArkTS的一些疑惑


作为一个前端开发,最常用的编程语言就是JavaScript或者TypeScript,那么在看到鸿蒙应用开发用到的编程语言是ArkTS之后,我脑子里最先蹦出来的就是下面这几个问题:


1. ArkTS语言的运行时是啥?


既然编程语言是TS(TS的拓展,ArkTS),那么它的运行时是什么呢?是V8?JSC?Hermes?还是其他什么呢?


2. ArkTS还是单线程语言吗?


ArkTS还是和JS一样,是单线程语言吗?


3. 基于TS拓展了什么?


TS是JS的超集,对JS进行了拓展,增加了开发时的类型支持。而ArkTS对对TS又进行了拓展,是TS的超集,那它基于TS拓展了什么内容呢?


下面我们一个一个来看。


1. Question1 - ArkTS语言的运行时


先说结论,ArkTS的运行时不是V8,不是JSC、Hermes,不是目前任何一种JS引擎。ArkTS的运行时是一个自研的运行时,叫做方舟语言运行时(简称方舟运行时)。


方舟运行时


而这个运行时,执行的也不是JS/TS/ArkTS代码,而是执行的字节码和机器码
这是因为方舟运行时是ArkCompiler(方舟编译器)的一部分,对于JS/TS/ArkTS的编译在运行前就进行了(和Hermes有点像,下面会讲到)。


方舟开发框架示意图


我们来简单了解一下ArkCompiler,从官网的描述可以看到,ArkCompiler关注的重点主要有三个方面:



  • AOT 编译模式

  • LiteActor 轻量化并发

  • 源码安全


AOT 编译模式


首先是编译模式,我们知道,目前编程语言大多以下几方式运行:



  • 机器码AOT编译


    在程序运行之前进行AST生成和代码编译,编译为机器码,在运行的时候无需编译,直接运行,比如C语言。


  • 中间产物AOT编译


    在程序运行前进行AST生成并进行编译,但不是编译为机器码,而是编译为中间产物,之后在运行时将字节码解释为机器码再执行。比如Hermes或Java编译为字节码,之后运行时由Hermes引擎或JVM解释执行字节码。


  • 完全的解释执行


    在程序运行前不进行任何编译,在运行时动态地根据源码生成AST,再编译为字节码,最后解释执行字节码。比如没有开启JIT的V8引擎执行JS代码时的流程。


  • 混合的JIT编译


    在通过解释执行字节码时(运行时动态生成或者AOT编译生成),对多次执行的热点代码进行进一步的优化编译,生成机器码,后续执行到这部分逻辑时,直接执行优化后的机器码。比如开启JIT的V8引擎运行JS或者支持JIT的JVM运行class文件。




当然,以上仅考虑生产环境下的运行方式,不考虑部分语言在生产和开发阶段不同的运行方式。比如Dart和Swift,一般是开发阶段通过JIT实时编译快速启动,生产环境下为了性能通过AOT编译。



在V8 JIT出现之前,所有的JS虚拟机所采用的都是采用的完全解释执行的方式,在运行时把源码生成AST语法树,之后生成字节码,然后将字节码解释为机器码执行,这是JS执行速度过慢的主要原因之一。


而这么做有以下两个方面的原因:



  • JS是动态语言,变量类型在运行时可能改变

  • JS主要用于Web应用,Web应用如果提前编译为字节码将导致体积增大很多,对网络资源的消耗会更大


我们一个一个来说。


a. JS变量类型在运行时可能改变

首先我们来看一张图,这张图描述了现在V8引擎的工作流程,目前Chrome和Node里的JS引擎都是这个:


V8现有工作流程


从上面可以看到,V8在拿到JS源码后,会先解析成AST,之后经过Ignition解释器把语法树编译成字节码,然后再解释字节码执行。


于此同时还会收集热点代码,比如代码一共运行了多少次、如何运行的等信息,也就是上面的Feedback的流程。


如果发现一段代码会被重复执行,则监视器会将此段代码标记为热点代码,交给V8的Turbofan编译器对这段字节码进行编译,编译为对应平台(Intel、ARM、MIPS等)的二进制机器码,并执行机器码,也就是图里的Optimize流程。


等后面V8再次执行这段代码,则会跳过解释器,直接运行优化后的机器码,从而提升这段代码的运行效率。


但是我们发现,图里面除了Optimize外,还有一个Deoptimize,反优化,也就是说被优化成机器码的代码逻辑,可能还会被反优化回字节码,这是为什么呢?


其实原因就是上面提到的“JS变量类型在运行时可能改变”,我们来看一个例子:


JS变量类型在运行时可能改变


比如一个add函数,因为JS没有类型信息,底层编译成字节码后伪代码逻辑大概如这张图所示。会判断xy的各种类型,逻辑比较复杂。


在Ignition解释器执行add(1, 2)时,已经知道add函数的两个参数都是整数,那么TurboFan在进一步编译字节码时,就可以假定add函数的参数是整数,这样可以极大地简化生成的汇编代码,不再判断各种类型,伪代码如第三张图里所示。


接下来的add(3, 4)add(5, 6)由于入参也是整数,可以直接执行之前编译的机器码,但是add("7", "8")时,发现并不是整数,这时候就只能将这段逻辑Deoptimize为字节码,然后解释执行字节码。


这就是所谓的Deoptimize,反优化。可以看出,如果我们的JS代码中变量的类型变来变去,是会给V8引擎增加不少麻烦,为了提高性能,我们可以尽量不要去改变变量的类型。


虽然说使用TS可以部分缓解这个问题,但是TS只能约束开发时的类型,运行的时候TS的类型信息是会被丢弃的,也无法约束,V8还是要做上面的一些假定类型的优化,无法一开始就编译为机器码。


TS类型信息运行时被丢弃


可以说TS的类型信息被浪费了,没有给运行时代码特别大的好处。


b. JS编译为字节码将导致体积增大

上面说到JS主要用于Web应用,Web应用如果提前编译为字节码将导致体积增大很多,对网络资源的消耗会更大。那么对于非Web应用,其实是可以做到提前编译为字节码的,比如Hermes引擎。


Hermes作为React Native的运行时,是作为App预装到用户的设备上,除了热更新这种场景外,绝大部分情况下是不需要打开App时动态下载代码资源的,所以体积增大的问题影响不是很大,但是预编译带来的运行时效率提升的好处却比较明显。


所以相对于V8,Hermes去掉了JIT,支持了生成字节码,在构建App的时候,就把JS代码进行了预编译,预编译为了Hermes运行时可以直接处理的字节码,省去了在运行阶段解析AST语法树、编译为字节码的工作。


Hermes对JS编译和执行流程的改进



一句题外话,Hermes去除了对JIT的支持,除了因为JIT会导致JS引擎启动时间变长、内存占用增大外,还有一部分可能的原因是,在iOS上,苹果为了安全考虑,不允许除了Safari和WebView(只有WKWebView支持JIT,UIWebView不支持)之外的第三方应用里直接使用JSC的JIT能力,也不允许第三方JS运行时支持JIT(相关问题)。


甚至V8专门出了一个去掉JIT的JIT-less V8版本来在iOS上集成,Hermes似乎也不太可能完全没考虑到这一点。



c. 取长补短

在讨论了V8的JIT和Hermes的预编译之后,我们再来看看ArkCompiler,截取一段官方博客里的描述


博客描述


还记得上面说的“TS的类型信息被浪费了”吗?TS的类型信息只在开发时有用,在编译阶段就被丢弃了,而ArkCompiler就是利用了这一点,直接在App构建阶段,利用TS的类型信息直接预编译为字节码以及优化机器码。


即在ArkCompiler中,不存在TS->JS的这一步转译,而是直接从TS编译为了字节码和优化机器码(这里存疑,官网文档没有找到很明确的说法,不是特别确定是否有TS->JS的转译。详见评论区,如果有知道的大佬可以在评论区交流一下)。


同时由于鸿蒙应用也是一个App而不是Web应用,所以ArkCompiler和Hermes一样,也是在构建App时就进行了预编译,而不是在运行阶段做这个事情。


ArkCompiler对JS/TS编译和执行流程的改进


简单总结下来,ArkCompiler像Hermes一样支持生成字节码,同时又将V8引擎JIT生成机器码的工作也提前在预编译阶段做了。是比Hermes只生成字节码的AOT更进一步的AOT(同时生成字节码和部分优化后的机器码)。


LiteActor轻量化并发


到这里其实已经可以回答上面讲到的第二个问题了,ArkTS还是单线程语言吗?


答案是:是的,还是单线程语言。但是ArkTS里通过Worker和TaskTool这两种方式支持并发。


同时ArkCompiler对现有的Worker进行了一些优化,直接看官网博客


LiteActor轻量化并发


LiteActor轻量化并发博客描述


这里的Actor是什么呢?Actor是一种并发编程里的线程模型。


线程模型比较常见的就是共享内存模型,多个线程之间共享内存,比如Java里多个线程共享内存数据,需要通过synchronized同步锁之类的来防止数据一致性的问题。


Actor模型是另一种线程模型,“Actor”是处理并发计算的基本单位,每个Actor都有自己的状态,并且可以接收和发送消息。当一个Actor接收到消息时,它可以改变自己的状态,发送消息给其他Actor,或者创建新的Actor。


这种模型可以帮助开发者更好地管理复杂的状态和并发问题,因为每个Actor都是独立的,它们之间不会共享状态,这可以避免很多并发问题。同时,Actor模型也使得代码更易于理解和维护,因为每个Actor都是独立的,它们的行为可以被清晰地定义和隔离。


到这里大家应该已经比较明白了,前端里的Web Worker就是这种线程模型的一种体现,通过Worker来开启不同的线程。


源码安全


按照官网的说法,ArkCompiler会把ArkTS编译为字节码,并且ArkCompiler使用多种混淆技术提供更高强度的混淆与保护,使得HarmonyOS应用包中装载的是多重混淆后的字节码,有效提高了应用代码安全的强度。


源码安全


2. Question2 - ArkTS还是单线程语言吗


这个刚刚已经回答了,还是单线程语言,借用官网的描述:



HarmonyOS应用中每个进程都会有一个主线程,主线程有如下职责:



  1. 执行UI绘制;

  2. 管理主线程的ArkTS引擎实例,使多个UIAbility组件能够运行在其之上;

  3. 管理其他线程(例如Worker线程)的ArkTS引擎实例,例如启动和终止其他线程;

  4. 分发交互事件;

  5. 处理应用代码的回调,包括事件处理和生命周期管理;

  6. 接收Worker线程发送的消息;


除主线程外,还有一类与主线程并行的独立线程Worker,主要用于执行耗时操作,但不可以直接操作UI。Worker线程在主线程中创建,与主线程相互独立。最多可以创建8个Worker。



ArkTS线程模型


3. Question3 - 基于TS拓展了什么


当前,ArkTS在TS的基础上主要扩展了如下能力:



  • 基本语法:ArkTS定义了声明式UI描述、自定义组件和动态扩展UI元素的能力,再配合ArkUI开发框架中的系统组件及其相关的事件方法、属性方法等共同构成了UI开发的主体。

  • 状态管理:ArkTS提供了多维度的状态管理机制。在UI开发框架中,与UI相关联的数据可以在组件内使用,也可以在不同组件层级间传递,比如父子组件之间、爷孙组件之间,还可以在应用全局范围内传递或跨设备传递。另外,从数据的传递形式来看,可分为只读的单向传递和可变更的双向传递。开发者可以灵活地利用这些能力来实现数据和UI的联动。

  • 渲染控制:ArkTS提供了渲染控制的能力。条件渲染可根据应用的不同状态,渲染对应状态下的UI内容。循环渲染可从数据源中迭代获取数据,并在每次迭代过程中创建相应的组件。数据懒加载从数据源中按需迭代数据,并在每次迭代过程中创建相应的组件。


而上面这些,也就是我们接下来要介绍的ArkTS+ArkUI的语法。


四、 ArkTS & ArkUI


首先,在聊ArkUI之前,还有一个问题大家可能比较感兴趣:ArkUI是怎么渲染我们写的UI呢?


答案是自绘,类似于Flutter,使用自己的渲染引擎(应该是发展于Skia),而不是像RN那样将UI转为不同平台上的底层UI。


不管是从官网的描述[1]、[2]来看,还是社区里的讨论来看,ArkUI的渲染无疑是自绘制的,并且ArkUI和Flutter之间的联系很密切:


社区里的一些讨论


1. 基本语法


从前端的角度来看,ArkTS和ArkUI的定位其实就是类似于前端中TS+React+配套状态管理工具(如Redux),可以用TS写声明式UI(有点像写jsx),下面是基本语法:


基本语法



  • 装饰器


    用于装饰类、结构、方法以及变量,并赋予其特殊的含义。


    如上述示例中@Entry、@Component和@State都是装饰器,@Component表示自定义组件,@Entry表示该自定义组件为入口组件,@State表示组件中的状态变量,状态变量变化会触发UI刷新


  • 自定义组件


    可复用的UI单元,可组合其他组件,如上述被@Component装饰的struct Hello


  • UI描述


    以声明式的方式来描述UI的结构,例如build()方法中的代码块


  • 系统组件


    ArkUI框架中默认内置的基础和容器组件,可直接被开发者调用,比如示例中的ColumnTextDividerButton


  • 事件方法


    组件可以通过链式调用设置多个事件的响应逻辑,如跟随在Button后面的onClick()


  • 属性方法


    组件可以通过链式调用配置多项属性,如fontSize()width()height()backgroundColor()



2. 数据驱动UI


作为一个声明式的UI框架,ArkUI和其他众多UI框架(比如React、Vue)一样,都是通过数据来驱动UI变化的,即UI = f(State)。我们这里引用一下官网的描述:



在声明式UI编程框架中,UI是程序状态的运行结果,用户构建了一个UI模型,其中应用的运行时的状态是参数。当参数改变时,UI作为返回结果,也将进行对应的改变。这些运行时的状态变化所带来的UI的重新渲染,在ArkUI中统称为状态管理机制。


自定义组件拥有变量,变量必须被装饰器装饰才可以成为状态变量,状态变量的改变会引起UI的渲染刷新。如果不使用状态变量,UI只能在初始化时渲染,后续将不会再刷新。 下图展示了State和View(UI)之间的关系。



State和UI



View(UI):UI渲染,指将build方法内的UI描述和@Builder装饰的方法内的UI描述映射到界面。
State:状态,指驱动UI更新的数据。用户通过触发组件的事件方法,改变状态数据。状态数据的改变,引起UI的重新渲染。



在ArkUI中,提供了大量的状态管理相关的装饰器,比如@State@Prop@Link等。


ArkTS & ArkUI的状态管理总览


更多细节详见状态管理


3. 渲染控制


在ArkUI中,可以像React那样,通过if elsefor each等进行跳转渲染、列表渲染等,更多细节详见渲染控制



ArkUI通过自定义组件build()函数和@builder装饰器中的声明式UI描述语句构建相应的UI。在声明式描述语句中开发者除了使用系统组件外,还可以使用渲染控制语句来辅助UI的构建,这些渲染控制语句包括控制组件是否显示的条件渲染语句,基于数组数据快速生成组件的循环渲染语句以及针对大数据量场景的数据懒加载语句。



4. 更多语法


语法其实不是我们这篇文章的重点,上面是一些大概的介绍,更多语法可以详见官网,或者我的另外一篇专门讲解语法的笔记《前端视角下的ArkTS语法》(先留个占位符,有时间了补充一下)。


5. ArkTS & ArkUI小结


从前面的内容其实可以看到,ArkUI和RN相似点还挺多的:



  1. 都是使用JS/TS作为语言(ArkTS)

  2. 都有自己的JS引擎/运行时(ArkCompiler,方舟运行时)

  3. 引擎还都支持直接AOT编译成字节码


不同的是RN是将JS声明的UI,转换成iOS、Android原生的组件来渲染,而ArkUI则是采用自绘制的渲染引擎来自绘UI。


从这点来看,鸿蒙更像是Flutter,只不过把开发语言从Dart换成了JS/TS(ArkTS),和Flutter同样是自绘制的渲染引擎。


社区里其实也有类似的思考:其它方向的探索:JS Engine + Flutter RenderPipeLine。而ArkUI则是对这种思路的实现。


感觉这也可以从侧面解释为什么ArkUI的语法和Flutter比较像,应该参考了不少Flutter的实现(比如渲染引擎)。


而华为宣称鸿蒙可以反向兼容Flutter甚至是RN也就没有那么难以理解了,毕竟ArkUI里Flutter和RN的影子确实不少。


另外,除了ArkUI以外,华为还提供了一个跨平台的开发框架ArkUI-X,可以像Flutter那样,跨HarmonyOS、Android、iOS三个平台。


这么看来,ArkTS&ArkUI从开发语言、声明式UI的语法、设计思想来看,不管是前端、iOS、安卓、或者Flutter、RN,鸿蒙应用开发都是比较入门友好的。


五、 其他


1. 包管理工具


HarmonyOS开发中,使用的包管理工具是ohpm,目前看来像是一个借鉴pnpm的三方包管理工具,详见官方文档


另外,鸿蒙也提供了第三方包发布的仓库:ohpm.openharmony.cn


2. 应用程序结构


在鸿蒙系统中,一个应用包含一个或者多个Module,每一个Module都可以独立进行编译和运行。


应用程序结构


发布时,每个Module编译为一个.hap后缀的文件,即HAP。每个HarmonyOS应用可以包含多个.hap文件。


在应用上架到应用市场时,需要把应用包含的所有.hap文件打包为一个.app后缀的文件用于上架。


但是.app包不能直接安装到设备上,只是上架应用市场的单元,安装到设备上的是.hap


打包结构


开发态和打包后视图


鸿蒙应用的整体开发调试与发布部署流程大概是这样的:


开发-调试-发布-部署


HAP可分为Entry和Feature两种类型:



  • Entry类型的HAP:是应用的主模块
    在同一个应用中,同一设备类型只支持一个Entry类型的HAP,通常用于实现应用的入口界面、入口图标、主特性功能等。

  • Feature类型的HAP:是应用的动态特性模块
    一个应用程序包可以包含一个或多个Feature类型的HAP,也可以不包含;Feature类型的HAP通常用于实现应用的特性功能,可按需下载安装


而设计成多hap,主要是有3个目标:



  1. 为了解耦应用的各个模块,比如一个支付类型的App,Entry类型的hap可以是首页主界面,上面的扫一扫、消息、理财等可以的feature类型的HAP

  2. 方便开发者将多HAP合理地组合并部署到不同的设备上,比如有三个HAP,Entry、Feature1和Feature2,其中A类型的设备只能部署Entry和Feature1。B类型的设备只能部署Entry和Feature2

  3. 方便应用资源共享,减少程序包大小。多个HAP都需要用到的资源文件可以放到单独的HAP中



多说一句:从这些描述来看,给我的感觉是每个.hap有点类似于前端项目中Mono-repo仓库中的一个package,各个package之间有一定的依赖,同时每个package可以独立发布。



另外,HarmonyOS也支持类似RN热更新的功能,叫做快速修复(quick fix)。


六、 总结


现在再回到最开始那个问题:什么是鸿蒙?从前端视角来看,它是这样一个系统:



  • ArkTS作为应用开发语言

  • 类Flutter、Compose、Swift的声明式UI语法

  • 和React有些相似的数组驱动UI的设计思想

  • ArkCompiler进行字节码和机器码的AOT编译 + 方舟运行时

  • 类似Flutter Skia渲染引擎的自绘制渲染引擎

  • 通过提供一系列ohos.xxx的系统内置包来提供TS访问系统底层的能力(比如网络、媒体、文件、USB等)


所以关于HarmonyOS是不是安卓套壳,个人感觉其实已经比较明了了:以前应该是,但快要发布的HarmonyOS NEXT大概率不再是了。


其他一些讨论


其实在华为宣布了HarmonyOS NEXT不再兼容安卓后,安卓套壳的声音越来越少了,但现在网上另外一种声音越来越多了:




  1. HarmonyOS NEXT是一个大号的小程序底座,上面的应用都是网页应用,应用可以直接右键查看源码,没有安全性可言

  2. HarmonyOS NEXT上的微信小程序就是在小程序里运行小程序

  3. 因为使用的是ArkTS开发,所以的HarmonyOS NEXT上的应用性能必然很差



这种说法往往来自于只知道鸿蒙系统应用开发语言是TS,但是没有去进一步了解的人,而且这种说法还有很多人信。其实只要稍微看下文档,就知道这种说法是完全错误的


首先它的View层不是DOM,而是类似Flutter的自绘制的渲染引擎,不能因为使用了TS就说是网页,就像可以说React Web是网页应用,但不能说React Native是网页应用,同样也不是说Flutter是网页应用。


另外开发语言本身并不能决定最终运行性能,还是要看编译器和运行时的优化。同样是JS,从完全的解释执行(JS->AST->字节码->执行),到开启JIT的V8,性能都会有质的飞跃。从一些编程语言性能测试中可以看到,开启JIT的NodeJs的性能,甚至和Flutter所使用的Dart差不多。


而ArkCompiler是结合了Hermes和V8 JIT的特点,AOT编译为字节码和机器码,所以理论上讲性能应该相当不错。


(当然我也没有实机可以测试,只能根据文档来分析)。


上面这种HarmonyOS NEXT是网页应用的说法还有可能是由于,最早鸿蒙应用支持使用HTML、CSS、JS三件套进行兼容Web的开发,导致了刻板印象。这种开发方式使用的是FA模型,而目前这种方式已经不是鸿蒙主推的开发方式了。


到这里这篇文章就结束了,整体上是站在一个前端开发的视角下来认识和了解鸿蒙开发的,希望能帮助一些像我一样对鸿蒙开发感兴趣的前端开发入门。大家如果感兴趣可以到鸿蒙官网查看更多的了解。


如果感觉对你有帮助,可以点个赞哦~


作者:酥风
来源:juejin.cn/post/7366948087129309220
收起阅读 »

“WWW” 仍然属于 URL 吗?它可以消失吗?

多年来,我们的地址栏上一直在进行着一场小小的较真战。也就是Google、Instagram和Facebook 等品牌。该群组已选择重定向 example.com 至 http://www.example.com。相反:GitHub...
继续阅读 »

多年来,我们的地址栏上一直在进行着一场小小的较真战。也就是GoogleInstagramFacebook 等品牌。该群组已选择重定向 example.com 至 http://www.example.com。相反:GitHubDuckDuckGoDiscord。该组织选择执行相反的操作并重定向 http://www.example.com 到 example.com



“WWW”属于 URL 吗?一些开发人员对此主题持有强烈的意见。在了解了一些历史之后,我们将探讨支持和反对它的论据。


WWW是什么?


WWW代表"World Wide Web",是上世纪80年代晚期的一个发明,引入了浏览器和网站。使用"WWW"的习惯源于给子域名命名的传统:



如果没有WWW会发生什么问题?


1. 向子域名泄露cookies


反对"没有WWW"的域名的批评者指出,在某些情况下,subdomain.example.com可以读取example.com设置的cookies。如果你是一个允许客户在你的域名上运营子域名的Web托管提供商,这可能是不希望看到的。


然而,这种行为只存在于Internet Explorer中。


RFC 6265标准化了浏览器对cookies的处理,并明确指出这种行为是错误的。


另一个潜在的泄露源是example.com设置的cookies的Domain值。如果Domain值明确设置为example.com,那么这些cookies也将被其子域名所访问。


Cookie 值暴露于 example.com暴露于 subdomain.example.com
secret=data
secret=data; Domain=example.com

总之,只要你不明确设置Domain值,而且你的用户不使用Internet Explorer,就不会发生cookie泄露。


2. DNS的困扰


有时,"没有WWW"的域名可能会使你的域名系统(DNS)设置复杂化。


当用户在浏览器的地址栏中输入example.com时,浏览器需要知道他们想访问的Web服务器的Internet协议(IP)地址。浏览器通过你的域名的域名服务器向其DNS服务器(通常间接通过用户的互联网服务提供商(ISP)的DNS服务器)请求IP地址。如果你的域名服务器配置为响应包含IP地址的A记录,那么"没有WWW"的域名将正常工作。


在某些情况下,你可能希望使用规范名称(CNAME)记录来代替为你的网站设置。这样的记录可以声明http://www.example.comexample123.somecdnprovider.com的别名,这会告诉用户的浏览器去查找example123.somecdnprovider.com的IP地址,并将HTTP请求发送到那里。


请注意,上面的示例使用了一个WWW子域名。对于example.com,不可能定义一个CNAME记录。根据RFC 1912,CNAME记录不能与其他记录共存。如果你尝试为example.com定义CNAME记录,example.com上的MX(邮件交换)记录将无法存在。因此,就不可能在@example.com上接收邮件


一些DNS提供商可以让你绕过这个限制。Cloudflare称其解决方案为CNAME解析。通过这种技术,域名管理员配置一个CNAME记录,但他们的域名服务器将暴露一个A记录。


例如,如果管理员为example.com配置了指向example123.somecdnprovider.com的CNAME记录,并且存在一个指向1.2.3.4example123.somecdnprovider.com的A记录,那么Cloudflare就会暴露一个指向1.2.3.4的example.com的A记录。


总之,虽然这个问题对希望使用CNAME记录的域名所有者来说是有效的,但现在有一些DNS提供商提供了合适的解决办法。


没有WWW的好处


大部分反对WWW的论点是实用性或外观方面的。"无WWW"的支持者认为example.comhttp://www.example.com更容易说和输入(对于不那么精通技术的用户可能更不容易混淆)。


反对WWW子域名的人还指出,去掉它会带来一种谦虚的性能优势。网站所有者可以通过这样做每个HTTP请求节省4个字节。虽然这些节省对于像Facebook这样的高流量网站可能会累积起来,但带宽通常并不是一种紧缺的资源。


有"WWW"的好处


支持WWW的一个实际论点适用于使用较新顶级域的情况。例如,http://www.example.miamiexample.miami无法立即被识别为Web地址。对于具有诸如.com这样的可识别顶级域的网站,这不是一个太大的问题。


对搜索引擎排名的影响


目前的共识是你的选择不会影响你的搜索引擎表现。如果你希望从一个URL迁移到另一个URL,你需要配置永久重定向(HTTP 301)而不是临时重定向(HTTP 302)。永久重定向确保你旧的URL的SEO价值转移到新的URL。


同时支持两者的技巧


网站通常会选择example.comhttp://www.example.com作为官方网站,并为另一个配置HTTP 301重定向。理论上,可以支持http://www.example.com和example.com两者。但实际上,成本可能会超过效益。


从技术角度来看,你需要验证你的技术栈是否能够处理。你的内容管理系统(CMS)或静态生成的网站需要将内部链接输出为相对URL以保留访问者的首选主机名。除非你可以将主机名配置为别名,否则你的分析工具可能会将流量分别记录在两个主机名上。


最后,你需要采取额外的措施来保护你的搜索引擎表现。谷歌将把URL的"WWW""非WWW"版本视为重复内容。为了在其搜索索引中去重复内容,谷歌将显示它认为用户更喜欢的那个版本——不论是好是坏。


为了在谷歌中保持对自己的控制,建议插入规范链接标签。首先,决定哪个主机名将成为官方(规范)主机名。


例如,如果你选择了www.example.com,则必须在 https://example.com/my-article里的  上的标记 中插入以下代码段:


    <link href="https://www.example.com/my-article" rel="canonical"> 

这个代码片段告诉谷歌"无WWW"变体代表着相同的内容。通常情况下,谷歌会在搜索结果中偏好你标记为规范的版本,也就是在这个例子中的"WWW"变体。


总结


对于是否在URL中加入"WWW",人们有不同的观点。下面是支持和反对的论点:


支持"WWW"的论点:



  1. 存在子域名的安全性问题:某些情况下,子域名可以读取主域名设置的cookies。虽然这个问题只存在于Internet Explorer浏览器中,并且已经被RFC 6265标准化修复,但仍有人认为使用"WWW"可以避免潜在的安全风险。

  2. DNS配置的复杂性:如果你的域名系统(DNS)配置为响应包含IP地址的A记录,那么"没有WWW"的域名将正常工作。但如果你想使用CNAME记录来设置规范名称,那么"没有WWW"的域名可能会导致一些限制,例如无法同时定义CNAME记录和MX(邮件交换)记录。

  3. 对搜索引擎排名的影响:对于使用较新顶级域的网站,使用"WWW"可以帮助识别网址,而不是依赖可识别的顶级域名。然而,目前的共识是选择是否使用"WWW"对搜索引擎表现没有直接影响。


支持去除"WWW"的论点:



  1. 实用性和外观:去除"WWW"可以使域名更简洁和易于输入,减少了用户可能混淆的机会。

  2. 节省字节:去除"WWW"可以每个HTTP请求节省4个字节。虽然这对于高流量网站来说可能是一个可累积的优势,但对于大多数网站来说,带宽通常不是一个紧缺的资源。


最佳实践:
一般来说,网站会选择将example.com或www.example.com作为官方网址,并对另一个进行重定向。你可以通过使用HTTP 301永久重定向来确保旧URL的SEO价值转移到新URL。同时,你还可以在页面的标签中插入规范链接标签,告诉搜索引擎两个URL代表相同的内容,以避免重复内容问题。


需要注意的是,在做决策时要考虑到技术栈的支持能力、DNS配置的限制和谷歌对搜索排名的处理方式。


作者:狗头大军之江苏分军
来源:juejin.cn/post/7263274550074507321
收起阅读 »

微信之父张小龙的一次内部分享

本文分享一下凌览近期看的一本书《微信背后的产品观》,它源自2012年7月微信产品经理张小龙一次长达8小时的腾讯内部分享。 了解人性 产品经理是站在上帝身边的人,上帝根据他的期望,创造了人,并赋予人一些习性,让人类的群体在这些习性下发展演化。而产品经理实际是在理...
继续阅读 »

本文分享一下凌览近期看的一本书《微信背后的产品观》,它源自2012年7月微信产品经理张小龙一次长达8小时的腾讯内部分享。


了解人性


产品经理是站在上帝身边的人,上帝根据他的期望,创造了人,并赋予人一些习性,让人类的群体在这些习性下发展演化。而产品经理实际是在理解了人的习性后,像上帝一样,建造系统并建立规则,让群体在系统中演化。



书中提及两本书《失控》、《乌合之众》,两本书结合的逻辑:群体在特定规则下的无序演化产生很多意想不到的结果。微信的很多产品功能的设计思想都和这两本书的理论很契合,比如漂流瓶、摇一摇,拍一拍等。



优秀的产品经理需要具备的能力:



  • 了解人的习性,需求从人性中产生

  • 了解群体的心理


"人"的特性:



  • 人是懒惰的,懒惰是创新的动力,案例:语音查找联系人,解决走路或双手不方便时要给一个人发微信,输入半天还找不出的情况

  • 人是跟风的,"因为别人都在用",时尚是驱动力,在互联网产品中,"时尚"是重要的驱动力

  • 人是没有耐心的,用户没有耐心看产品说明书,不要尝试去引导用户,去教育用户,没有人愿意去接受你的引导和教育。一定是他拿过来就会用才是最直接的(产品使用操作简单)

  • 人是不爱学习的,"马桶阅读"理论:不要给用户超过马桶上看不完的内容

  • 群体是"乌合之众",理论出自《乌合之众》一书,群体智商低于个体,互联网产品的用户是群体,不是个体


如何确定一个需求



  • 对于新点子,99%的情况下否定是对的,不要随便臆想需求

  • 不要用户说什么就做什么,用户的反馈是帮助你了解他们的想法,用户的需求是零散的,应该进行归纳抽象

  • 不从同类产品里找需求,另的产品决定做这个需求,是有他们自己的理解,并深入分析思考过。如果别人说好,我们就直接照搬,其实没有深刻理解需求

  • 不听从产品经理的需求,他们不是用户却自认为代表用户,他们分析过于理性,他们会要求要显示在线、要已读、要分组、要滤镜、要涂鸦、要多端同步、要群名片、要赞头像.....如果产品经理都把这些当作用户朴素的需求做进去,这将是可怕的事情

  • 需求来自你对用户的了解

    • 需求不来自调研

    • 需求不来自分析

    • 需求不来自讨论

    • 需求不来自竞争对手



  • "爽"用过功能,爽是体验。爽比功能更易传播

  • 只抓主场景,不做全功能,做大而全很容易,做小很难,如果没有化繁为简的功力,就控制自己的欲望,每天砍掉几个需求的爽,远大于提出几个需求,案例:朋友圈只能发图片,发140字的难度远胜一张图片


如何设计一个产品



  • 好的产品价值观和认知是成为优秀产品的前提

  • 先做产品结构,之后才是功能细节。微信功能细算特别多,但看起来还是很简单,做一个新版本都不知道它有什么新功能,先把微信的骨骼梳理清楚,枝叶的东西藏得很深也没关系,这样整个产品才会乱掉

  • 功能模块之间是有机联系的关系,独立的功能堆砌很危险

  • 设计是分类

  • 抽象才能化繁为简,如果有100个需求,而我们能把这100个需求汇总成10个需求,这就是"抽象"

  • 越简单的分类越容易被接受,微信会升级,但结构和界面依然保持简单,过多变化易引来用户不适应

  • 挖掘需求背后的本质需求

  • 宁愿损失功能也不损失体验


最后


张小龙强调我所说的,都是错的,每个人都会有自己的解决问题的办法,没有永远的正确教条


书未有问答环节,这里精简摘录下我认为有启发的回答:


Q:为什么人人都是产品经理?怎么做到跟其他也是产品经理的人不一样?


A:因为人人都可以提问题,人人都可以指手画脚,用户也会,但最难的是找到本质的东西,这才是产品经理要一定具备的


Q:从普通工程师到现在做成一个伟大的产品,你是怎么一步一步走过来的?


A:经历上千个实战的锻练,越多越好,一个人要成为一个领域的专家,要付出一万个小时的努力和专业训练



回答提及一本书《另类成功学》,有兴趣可以看看。



如果我的文章对你有帮助,您的👍就是对我的最大支持^_^。




作者:程序员凌览
来源:juejin.cn/post/7274163003158822912
收起阅读 »

程序员的未来发展会是什么?跟一位同学沟通之后的思考

Hello,大家好,我是 Sunday。 说起程序员的发展方向好像是一个老生常谈的话题了。我记得在过去的十年中,我曾经无数次的看到过各种文章来说类似的话题。 不过这样的话题又好像是一个经久不衰的,特别是在目前这样行情不好的情况下,我相信有很多同学都在处于前所未...
继续阅读 »

Hello,大家好,我是 Sunday。


说起程序员的发展方向好像是一个老生常谈的话题了。我记得在过去的十年中,我曾经无数次的看到过各种文章来说类似的话题。


不过这样的话题又好像是一个经久不衰的,特别是在目前这样行情不好的情况下,我相信有很多同学都在处于前所未有的迷茫状态之中。


比如,昨天有个同学来问我说:“前端发展方向是什么?全面发展?工程化?还是架构师?”



所以,为了能更好的解决大家的困惑,今天这篇文章咱们就来说一说程序员未来的发展方向、什么人适合什么方向、以及分别需要做什么样的准备。


程序员发展方向


程序员是一个技术岗位,但是它的发展方向绝对不仅局限于“技术领域”。


所以,当我们去考虑发展方向或者是未来职业规划的时候,就不能仅从技术的角度来进行分析。


下面是我认为对于程序员而言,最有利的几个方向(一家所见,仅供参考):



  1. 某一个行业的技术专家

  2. 技术大牛(包含 Leader 岗)

  3. 自由职业者

  4. remote 远程工作


下面咱们一个一个去说...


1. 某一个行业的技术专家


1.1 什么是某一个行业的技术专家(后面简称:技术专家)


回忆下我们的工作,我们目前所做的大部分工作是不是都在为了完成某一个业务而存在的?


有的同学在做医疗业务、有 金融业务、有 政府项目电商服务 等,总之无论是那种,所有的项目总归是在为某一个业务而服务的。


而所谓的技术专家,指的就是:以技术为基本,成为非常熟悉该行业的人员


以我为例,我首先是一个程序员,其次是一个 教育方向的程序员 就是这个道理。


1.2 什么样的人适合


想要做技术专家,那么一定要明确一个大前提:技术本没有价值,只有使用技术完成了一个有价值的事情之后,这个事情才可以赋予技术价值。


如果你想要以技术专家为目标,那前提一定是 你要深刻的认同这句话。如果你不认同,那么这个方向就 不适合 你。


1.3 需要怎么做


如果想要成为技术专家,那么就 不能 频繁的更换行业。


甚至,当你选定了一个行业之后,就应该长期立足下去,只要这样你才能逐步的熟悉这个行业的运行规律。


所以,这就要求我们在跳槽的时候,尽量 选择与上家公司从事相同行业的公司去做(没有竞业协议的前提下),而不是随便跳转一个行业就入职。


2. 技术大牛(包含 Leader 岗)


2.1 什么是技术大牛


其实技术大牛是很难定义的。古语说:文人相轻(程序员总不能说自己是武人吧)。所以,对于程序员而言,所谓的技术大牛一定是一个相对的,而不是绝对的。就算是尤雨溪也有被人喷菜的时候。


所以说,如果要给技术大牛 一个定义的话,那么指的就是:在某一个范围(公司或者团体)内,具有一定权威的人员


2.2 什么样的人适合


这里其实有两个方向,不同的方向适合的人不同:



  1. 纯技术人员,不参与管理: 比较适合不善言辞,并且不愿意余人交流,完全沉迷于技术的人

  2. 以管理为主的技术人员: 具备一定的技术能力,但是同时更愿意与人沟通,懂得人情世故的人


2.3 需要怎么做


如果是单纯的技术就比较简单了,做法分为两步:



  1. 多在“团体”内发言:这个“团体”代表了很多东西,可以是:论坛、网站、公司或其他。

  2. 多学习各种新的技术,然后把这些技术 输出出去


如果你只会输入,不会输出。那么是无法成为技术大牛的。


而 Leader 就比较复杂了,相比于技术而言,更多的其实是 人情世故。所以如果想要将来做管理岗位,那么就需要练习好人情世故的处理能力。


3. 自由职业者


关于自由职业者,前几天我专门写过一篇文章 并不自由的自由职业?回顾下我的五一小长假 ,甚至还在 B站 还录制了对应的视频:



所以这里就不再细说了。


4. remote 远程工作


4.1 什么是remote


所谓的 remote 指的就是 远程工作。比如:你在家工作,不需要到公司坐班。符合这个条件的都属于 远程工作。


目前大家提起 remote 大多数指的其实是:国外的远程工作。也就是 人在国内,为国外的公司工作。


可能是因为国内太卷的原因,目前 remote 的工作被非常多的人推崇。


但是 我个人建议大家理性看待。有兴趣的同学可以看下我写的这篇文章 remote 经验分享,它真有你想象的那么好吗?,这里就不过多赘述了。


4.2 什么样的人适合


如果不喜欢坐班,享受那种工作几个月,休息几个月的状态的话,那么可以尝试 remote 的工作。


4.3 需要怎么做


其实想要做 remote 的工作并不困难,核心是两个点:



  1. 英语:至少要可以做到 雅思6.5 的水平

  2. 岗位:可以多关注 领英、电鸭社区、indeed 等


但是要 注意:防止被骗!!!


因为 remote 无法看到对方的公司信息,并且合同形同虚设,所以有同学出现过 工作 1-2 个月之后无法收到工资的情况,所以要特别注意这一点!


总结


OK,以上是我在跟那位同学沟通之后,大致总结的一些对程序员比较友好的发展方向,希望可以对大家有帮助~~


作者:程序员Sunday
来源:juejin.cn/post/7368294440547041318
收起阅读 »

大环境越不好 人就越玄学

二零零几年,大环境还没像现在这么拉垮的时候,有个面向学生的网站叫校内网,里面曾有人发起了一次大范围投票。 问广大学子毕业后最想从事什么工作。 当时超过一半的人都选择了大型外企,排名第二的是大型国企民企,然后是自主创业。 只有很少一部分选择了事业单位和公务员,这...
继续阅读 »

二零零几年,大环境还没像现在这么拉垮的时候,有个面向学生的网站叫校内网,里面曾有人发起了一次大范围投票。


问广大学子毕业后最想从事什么工作。


当时超过一半的人都选择了大型外企,排名第二的是大型国企民企,然后是自主创业。


只有很少一部分选择了事业单位和公务员,这部分同学还有相当比例来自对考公自古有执念的山东。


而在其他省份,多数同学都认为自己能拥有光明的未来,当然不会喜欢公务员这种工资稳定得低,日复一日枯坐案前,早早就能一眼望到头的工作。


在当时年轻人眼里,公务员属于“实在不行就只能回家考公“的备胎,地位约等于“实在不行就找个老实人嫁了“的级别。


但后来的故事我们都知道了,经济大船这几年驶入了深水区,风浪越来越大,鱼也越来越贵。


于是四平八稳旱涝保收的体制内,这几年摇身一变,一跃成为了那个最靓的仔。不得不说,人确实是时代的产物,环境的变化可以完全改变一个人的决策。


大环境好的时候,人们会不自觉地高估自身的努力,那时候人们是相信努力一定会有收获的。有时候过于相信了,但这在经济高速增长的年代并不会有太大问题,你还是会得到属于自己的那块蛋糕的。


但当经济增速换档时,付出与回报的比例开始失衡,努力就能收获的简单逻辑不攻自破。变成了努力也不一定有收获,进而发展成努力大概率不会有收获,最后演变成一命二运三风水,努力奋斗算个鬼


这种心态的转变也解释了为啥从去年以来,越来越多的年轻人开始扎堆去寺庙求签祈福,排的长队连起来能绕地球三圈,看得旁观的老大爷直摇头说,“真搞不懂这些小年轻是怎么想的,偶像粉丝见面会咋还跑到庙里来开了?!”


人在逆境迷茫时,是容易被玄学吸引。逆境意味着前路遇阻,意味着你迫切需要一些指引,而玄学恰好满足了这方面需求。


命运这个东西,有时候真蛮捉摸不透的。


我认识一小姐姐,为一场决定人生的重要考试做足了准备,结果在赶往考场的路上,书包就这么巧被扒手偷了,里面开卷考试所有的资料全部丢失,直接导致她逃汰出局,泪洒当场。


还有一大哥,在升职加薪岗位竞争的关键阶段,突然一场急病,好巧不巧失声了,一句话也说不出来,参加不了竞聘答辩,眼睁睁看着大好机会就此溜走。


等这事过去了,他一下子又能正常说话,跟被老天上了沉默debuff一样,你说他找谁说理去呢。


人活得时间越长,就越信“命“这个东西,越能意识到自己真正能把控的其实少得可怜,随便一点意外都能直接改变整个人生走向。


这种感悟放在以前,一般都是上了些年纪的人才会有的,但随着这两年经济增速换挡,年轻人频繁碰壁,被命运按在地上摩擦的次数多了,自然也就信了“命”,求签问道的也就跟着多起来了。


说句不好听的话,我觉得这样挺好的。不是说求签问道这个行为好,而是这种行为背后暗含着一个巨大的心理转变,我认为很好。


那就是放过自己。亚洲人尤其是我们特别不愿意放过自己,从出生开始就活在比较中,长辈们连夸个人都要这么夸,说哎呀,你学习真用功,比学习委员还用功;哎呀,你工资挺高,比隔壁小王还要高。


骂你的时候也一定要捎带上别人,说你看谁谁谁多厉害,你再看看你,一定是你还不够努力。


就是这种搞法很容易让人把责任全揽自己身上,对自我要求过高,最后的结果就是崩掉,就累嘛!


但现在不一样了,现代人在网络上看了太多含着金汤匙出生在罗马的人,和那些老天爷追着赏饭吃的人。


他们跟我们之间的差距大到几辈子都弥补不上,那努力万能论也就不攻自破了嘛。


于是越来越多的小伙伴开始承认自我的局限,承认努力也不一定有收获,承认人生不如意十之八九,慢慢也就承认了“命运”这个东西,开始顺其自然,没那么多执念了。


不过有些人过于放飞自我,摆烂走了另一个极端,那也是要出问题的。


即便是玄学,它也没有彻底否定个人奋斗,大富靠命没错,但小富靠勤,靠双手取得一些小成就,让日子过得舒服些还是没啥问题的。


其实我觉得一个比较合适的世界观应该是这个样子:首先咱得承认不可抗力,承认“命”与“运”这个东西是真实存在的,如果你不喜欢这两个玄乎的字,可以用“概率”代替,我们永远得做好小概率事件砸到头上的准备。


有时候拼尽一切就是没有好的结果,这咱得承认,但同时这也并不意味着从此放弃一切行动,落入虚无主义的陷阱。


人还是要去做一些什么的。比如精进某项专业技能,逐步提升自身能力,为的不是那点工资,而是一件更重要的事,抓住运气。


运气有多重要,大家都明白,它比努力重要得多。


运气这东西打比方的话,就像一个宝箱,会随机在你面前掉落,但这些宝箱自带隐形属性,你等级太低的话就看不见它,自然也就抓不住这些运气。


用现实举例,“运气”就像你在工作中遇到了某个本来还可以拉你一把的贵人,结果你的等级太低,工作能力稀碎,贵人一看,这货不值得我帮,转身走了。他这个宝箱对你而言就隐形了,消失了。


而且最讽刺的是你从头到尾都被蒙在鼓里,根本不知道自己错失了一次宝贵的机会,所以为了避免运气来了你抓不住,又溜走的这种尴尬情况出现,我们还是要去精进和磨练一下社会技能,尽量达到能在某些场合被人夸奖的程度。


把等级刷高一些,之后该吃吃该喝喝,耐心等待宝箱的出现。这可能也是以前人们常说的,“尽人事听天命”的另一种解释吧。


也希望今天聊的关于命和运的这些内容,能启发到一些小伙伴,大家一起认认真真,平平淡淡的生活。


作者:程序员Winn
来源:juejin.cn/post/7317704462436139058
收起阅读 »

OPPO率先适配Android 15,首批机型名单公布

北京时间5月15日,代号为「Vanilla Ice Cream(香草冰淇淋)」的 Android 15 在2024谷歌 I/O 大会正式亮相。作为全球智能手机市场领先品牌,OPPO 作为连续六年首批适配 Android 新系统的厂商,此次不仅率先发布了基于 A...
继续阅读 »

北京时间5月15日,代号为「Vanilla Ice Cream(香草冰淇淋)」的 Android 15 在2024谷歌 I/O 大会正式亮相。作为全球智能手机市场领先品牌,OPPO 作为连续六年首批适配 Android 新系统的厂商,此次不仅率先发布了基于 Android 15 的开发者预览版助力开发者抢先适配,更以全流程、全方位的适配保障服务,持续为广大开发者保驾护航。

图1.png

OPPO 公布 Android 15 适配指南,督促开发者升级64位架构

据了解,Android 15 带来了一系列令人瞩目的新功能和改进,包括全新设计的兼容性调试工具、安全与隐私相关的强化措施、系统优化和新的API支持。这些功能使开发者能够更轻松地构建和维护应用,为用户提供更好的性能和体验。

基于 Android 15 Beta 版本,OPPO 推出 ColorOS 开发者预览版,OPPO Find X7、一加12首批支持。值得注意的是, OPPO 这次特别督促开发者对64位架构的全面升级,确保所有软件和应用在新系统中都能实现最佳性能。据了解,Android 15 系统升级 Vendor 的手机均不支持32位应用,若不进行64位升级将导致应用后续无法下载使用。64位架构优势显著,更快的处理速度、更大的内存支持以及更高的效率,使开发者能够充分利用 Android 15 的潜力,为用户提供更加流畅和丰富的应用体验。开发者可前往OPPO开放平台官网,抢先下载并体验开发者预览版。

图2.png

全方位服务保障,助力开发者推进系统适配

为了助力开发者高效适配 Android 15 系统,OPPO 提供了包括适配文档、适配工具、适配资讯以及专家交流等在内的全面支持和服务。

全面清晰的指导文档帮助各类型 APP 开发者迅速找到所需的适配方案;云测服务提供实时在线的远程调试功能,支持开发者随时接入,帮助开发者快速验证适配结果;OPPO开放平台官网适配支持专区实时更新 Android 15 最新动态,开发者可随时获取第一手适配资讯。此外,OPPO 还提供了7*24小时在线答疑服务,专人协助解决适配技术难题,进一步提升适配效率。技术指引也将实时更新,集中解答开发者提出的高频问题。

海量全面的文档支持,贯穿全程的指导升级服务,让每一个开发者都不掉队。

图3.png

据悉,5月22日,OPPO 将联合知名技术社区 51CTO 举办「OTalk | Android 15 适配开发者交流专场」线上直播活动,与行业开发者深入交流对话。届时将特别邀请 OPPO 高级工程师带来 Android 15 新特性的深度解读及适配建议,分享 OPPO 适配支持服务,解答开发者常见问题,助力开发者高效适配新版本。

图4.png

作为 Android 生态系统的关键参与者,OPPO 连续6年首批适配 Android 新版本,持续为开发者提供全流程适配支持和服务,携手开发者高效完成版本迭代优化与应用兼容性测试,共同将更安全、更流畅的系统体验带给用户。

接下来,OPPO 将持续提供关于 Android 15 适配的最新进展,广大开发者可关注「OPPO开放平台」后续公告,以获取更多详细信息和支持资源。

收起阅读 »

责任链模式最强工具res-chain🚀

web
上面的logo是由ai生成 责任链模式介绍 责任链模式(Chain of Responsibility Pattern)是一种行为型设计模式,它通过把请求的发送者和接收者解耦,将多个对象连接成一个链,并沿着这条链传递请求,直到有一个对象能够处理它为止,从而避...
继续阅读 »
image.png

上面的logo是由ai生成



责任链模式介绍


责任链模式(Chain of Responsibility Pattern)是一种行为型设计模式,它通过把请求的发送者和接收者解耦,将多个对象连接成一个链,并沿着这条链传递请求,直到有一个对象能够处理它为止,从而避免了请求的发送者和接收者之间的直接耦合


在责任链模式中,每个处理者都持有对下一个处理者的引用,即构成一个链表结构。当请求从链头开始流经链上的每个处理者时,如果某个处理者能够处理该请求,就直接处理,否则将请求发送给下一个处理者,直到有一个处理者能够处理为止。


这种方式可以灵活地动态添加或修改请求的处理流程,同时也避免了由于请求类型过多而导致类的爆炸性增长的问题。


看完以上责任链的描述,有没有发现跟Node.js的某些库特别的像,没错,就是koa。什么?没用过koa?那我建议你立马学起来,因为它用起来特别的简单。


下面来一个简单使用koa的例子:


const Koa = require('koa');
const app = new Koa();

app.use(async (ctx, next) => {
if (ctx.request.url === '/') {
ctx.body = 'home';
return;
}

next(); // 执行下面的回调函数
});

app.use(async (ctx, next) => {
if (ctx.request.url === '/hello') {
ctx.body = 'hello world';
return;
}
});

app.listen(3000);

通过node运行上面的代码,在浏览器请求localhost:3000,接口就会返回home,当我们请求localhost:3000/hello,接口就会返回hello world


上面对请求的处理过程就很符合职责链模式的思想,我们可以清楚的知道每个链做的工作,并且链条的顺序流程也很清晰。


有人就会问,只在一个回调里面也能处理呀,比如下面的代码:


app.use(async (ctx, next) => {
if (ctx.request.url === '/') {
ctx.body = 'home';
return;
} else if (ctx.request.url === '/home') {
ctx.body = 'hello world';
return
}
});

是的,上面的代码确实可以实现,但是这就要回到我们使用责任链模式的初衷了:为了逻辑解耦。


责任链解决的问题


我们继续接着聊上一节的问题,使用if确实可以实现相同效果,但是在某些场景中,if并没有职责链那么好用,为什么这么说呢。


我们找一个应用案例举个例子:



假设我们负责一个售卖手机的网站,需求的定义是:经过分别缴纳500元定金和200元定金的两轮预订,现在到了正式购买阶段。公司对于交了定金的用户有一定的优惠政策,规则如下:




  • 缴纳500元定金的用户可以收到100元优惠券;

  • 缴纳200元定金的用户可以收到50元优惠券;

  • 没有缴纳定金的用户进入普通购买模式,没有优惠券。

  • 而且在库存不足的情况下,不一定能保证买得到。


下面开始设计几个字段,解释它们的含义:



  • orderType:表示订单类型,值为1表示500元定金用户,值为2表示200元定金用户,值为3表示普通用户。

  • pay:表示用户是否支付定金,值为布尔值true和false,就算用户下了500元定金的订单,但是如果没有支付定金,那也会降级为普通用户购买模式。

  • stock:表示当前用户普通购买的手机库存数量,已经支付过定金的用户不受限制。


下面我们分别用if和职责链模式来实现:


使用if:


const order = function (orderType, pay, stock) {
if (orderType === 1) {
if (pay === true) {
console.log('500元定金预购,得到100元优惠券')
} else {
if (stock > 0) {
console.log('普通用户购买,无优惠券')
} else {
console.log('手机库存不足')
}
} else if (orderType === 2) {
if (pay === true) {
console.log('200元定金预购,得到50元优惠券')
} else {
if (stock > 0) {
console.log('普通用户购买,无优惠券')
} else {
console.log('手机库存不足')
}
}
} else if (orderType === 3) {
if (stock > 0) {
console.log('普通用户购买,无优惠券')
} else {
console.log('手机库存不足')
}
}
}

order(1, true, 500) // 输出:500元定金预购,得到100元优惠券'

虽然上面的代码也可以实现需求,但是代码实在是难以阅读,维护起来更是困难,如果继续在这个代码上开发,未来肯定会成为一座很大的屎山。


下面我们使用责任链模式来实现:


function printResult(orderType, pay, stock) {
// 这里ResChain类是模拟koa的写法,后面会讲如何实现ResChain
// 请先耐心看完它是如何处理的
const resChain = new ResChain();
// 针对500元定金的情况
resChain.add('order500', (_, next) => {
if (orderType === 1 && pay === true) {
console.log('500元定金预购,拿到100元优惠券');
return;
}
next(); // 这里将会调用order200对应的回调函数
});
// 针对200元定金的情况
resChain.add('order200', (_, next) => {
if (orderType === 2 && pay === true) {
console.log('200元定金预购,拿到50元优惠券');
return;
}
next(); // 这里会调用noOrder对应回调函数
});
// 针对普通用户购买的情况
resChain.add('noOrder', (_, next) => {
if (stock > 0) {
console.log('普通用户购买,无优惠券');
} else {
console.log('手机库存不足');
}
});

resChain.run(); // 开始执行order500对应的回调函数
}

// 测试
printResult(1, true, 500); // 500元定金预购,得到100元优惠券
printResult(1, false, 500); // 普通用户购买,无优惠券
printResult(2, true, 500); // 200元定金预购,得到50元优惠券
printResult(3, false, 500); // 普通用户购买,无优惠券
printResult(3, false, 0); // 手机库存不足

以上的代码经过责任链处理之后特别的清晰,并且减少了大量的if-else嵌套,每个链的职责分,我们可以看出责任链模式存在的优点:



  1. 降低了代码之间的耦合,很好的对每个处理逻辑进行封装。在每个链条内,只需要关注自身的逻辑实现。

  2. 增强了代码的可维护性。我们可以很轻易在原有链条内的任何位置添加新的节点,或者对链条内的节点进行替换或者删除。


责任链还特别的灵活,如果说后面pm找我们加需求,需要加多一个预付定金400,返回80元优惠券,处理起来也是易如反掌,只需要怼回去,只需要在order500下面加多一个节点处理即可:


... 
resChain.add('order500', (_, next) => {
if (orderType === 1 && pay === true) {
console.log('500元定金预购,拿到100元优惠券');
return;
}
next();
})
+ // 加上这一块
+ resChain.add('order400', (_, next) => {
+ if (orderType === 3 && pay === true) {
+ console.log('400元定金预购,拿80元优惠券');
+ return;
+ }
+ next();
+ })

resChain.add('order200', (_, next) => {
if (orderType === 2 && pay === true) {
console.log('200元定金预购,拿到50元优惠券');
return;
}
next();
})
...

就是这么简单。那这个ResChain是如何实现的呢?


封装ResChain


先别急,首先我们来了解一下koa是如何实现:在链节点的回调函数内调用next就可以跳到下一个节点的呢?


话不多说,直接看源码,参考的库是koa-compose,代码也是特别的简洁:


function compose (middleware) {
// 这里传入的middleware是函数数组,例如: [fn1, fn2, fn3, fn4, ...]
if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
// 判断数组里的元素是不是函数类型
for (const fn of middleware) {
if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
}

return function (context, next) {
// last called middleware #
let index = -1
return dispatch(0);

// 这里利用了函数申明提升的特性
function dispatch (i) {
// 这里是防止重复调用next
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i

// 从middleware中取出回调函数
let fn = middleware[i]
if (i === middleware.length) fn = next

// 如果fn为空了,则结束运行
if (!fn) return Promise.resolve()

try {
// next函数其实就是middleware的下一个函数,执行next就是执行下一个函数
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
} catch (err) {
return Promise.reject(err)
}
}
}
}

看完源代码,我们接着来实现ResChain类,首先整理一下应该要有的方法:



  • add方法。可以添加回调函数,并按添加的顺序执行。

  • run方法。开始按顺序执行责任链。


add方法执行的时候,把回调函数按顺序push进一个数组中。


export class ResChain {

/**
* 按顺序存放链的key
*/

keyOrder = [];
/**
* key对应的函数
*/

key2FnMap = new Map();
/**
* 每个节点都可以拿到的对象
*/

ctx = {}
constructor(ctx) {
this.ctx = ctx;
}

// 这里用key来标识当前callback的唯一性,后面重复添加可以区分。
add(key, callback) {
if (this.key2FnMap.has(key)) {
throw new Error(`Chain ${key} already exists`);
}

this.keyOrder.push(key);
this.key2FnMap.set(key, callback);
return this;
}

async run() {
let index = -1;
const dispatch = (i) => {
if (i <= index) {
return Promise.reject(new Error('next() called multiple times'));
}

index = i;
const fn = this.key2FnMap.get(this.keyOrder[i]);
if (!fn) {
return Promise.resolve(void 0);
}

return fn(this.ctx, dispatch.bind(null, i + 1));
};

return dispatch(0);
}
}

add方法的第一个参数key可以用来判断是否已经添加过相同的回调。


有人会说,koa的中间件是异步函数的,你这个行不行?


当然可以,接下来看个异步的例子:


const resChain = new ResChain();

resChain.add('async1', async (_, next) => {
console.log('async1');
await next();
});


resChain.add('async2', async (_, next) => {
console.log('async2')
// 这里可以执行一些异步处理函数
await new Promise((resolve, reject) => {
setTimeOut(() => {
resolve();
}, 1000)
});

await next();
});


resChain.add('key3', async (_, next) => {
console.log('key3');
await next();
});


// 执行责任链
await resChain.run();

console.log('finished');

// 先输出 async1 async2 然后停顿了1秒钟之后,才输出async3 finished


🚧 需要注意:如果是异步模式,则链上的每个回调函数必须要 await next(),因为next函数代表下一个环的异步函数。



koa的中间件方式简直一毛一样。


有人可能还注意到了,ResChain实例化的时候可以传入对象,比如下面的代码:


const resChain = new ResChain({ interrupt: false });

传入对象具体有什么用法呢?可以用来获取一些在链中处理好的数据,来实现发送者和处理者的解耦。可能比较抽象,我们来举个例子。


比如需要进行数据校验的场景,如果不通过,则中断提交:


const ctx = {
// 表单项
model: {
name: '',
phone: '',
},
// 错误提示
error: '',
// 是否中断
interrupt: false,
}
const resChain = new ResChain(ctx);

resChain.add('校验name', (ctx, next) => {
const { name = '' } = ctx;
if (name === '') {
ctx.error = '请填写name';
ctx.interrupt = true;
return;
}

next();
})

resChain.add('校验phone', (ctx, next) => {
const { phone = '' } = ctx;
if (phone === '') {
ctx.error = '请填写手机号';
ctx.interrupt = true;
return;
}

next();
})

// 执行责任链
resChain.run();

// 如果需要中断,则提示
if (resChain.ctx.interrupt) {
alert(resChain.ctx.error);
return;
}

如果是使用if来实现:


const ctx = {
// 表单项
model: {
name: '',
phone: '',
},
// 错误提示
error: '',
// 是否中断
interrupt: false,
}

if(ctx.model.name === '') {
ctx.error = '请填写用户名';
ctx.interrupt = true;
}

if (!ctx.interrupt && ctx.model.phone === '') {
ctx.error = '请填写手机号';
ctx.interrupt = true;
}

// 如果需要中断,则提示
if (resChain.ctx.interrupt) {
alert(resChain.ctx.error);
return;
}

可以发现,对phone的判断逻辑,就要先判断interrupt是否为false才能继续,而且如果下面还有其他的字段校验,那必须都走一遍if。


这也是责任链的一个优势,可以在某个环节按自己的想法停止,不用继续走后面的节点。


目前我已经把这个工具上传到npm了,如果想要在自己的项目中使用,直接安装:
res-chain即可使用:


npm install res-chain

# 或者
# yarn add res-chain

引入:


import { ResChain } from 'res-chain';
// CommonJS方式的引入也是支持的
// const { ResChain } = 'res-chain';

const resChain = new ResChain();

resChain.add('key1', (_, next) => {
console.log('key1');
next();
});

resChain.add('key2', (_, next) => {
console.log('key2');
// 这里没有调用next,则不会执行key3
});

resChain.add('key3', (_, next) => {
console.log('key3');
next();
});

// 执行职责链
resChain.run(); // => 将会按顺序输出 key1 key2

芜湖起飞🚀。


有了这个工具函数,我们就可以视场景去优化项目中的一大坨if-else嵌套,或者直接使用它来实现一些业务中比较复杂的逻辑。


起源


这个工具诞生的过程还挺巧合的,某一天周六我在公司加班赶需求,发现需要在一堆旧逻辑if-else中添加新的逻辑,强迫症的我实在是无法忍受在💩山上继续堆💩。。。


我陷入沉思,用什么方式去优化呢?看了网上责任链模式的实现,感觉还是不够优雅。


无意中翻到了之前用koa写的项目,突然灵光乍现💡,koa的中间件不就是一个很棒的实践。调用next就能够往下一个节点走,不调用的话就可以终止。


于是立即动工,三下五除二就完成了。我还推荐给部门的其他前端小伙伴,他们也在一些需求的复杂逻辑中有运用。


总结



过去无意学到的某个知识,或者某个概念,在未来也许会发挥作用,你只需要做的就是等待。



没有koa这么棒的库,估计也不会有这工具了,所以还是得感谢它的作者如此聪明。😁


如果你也喜欢这个工具,欢迎去github里给个🌟,感谢。


如果有什么更好的建议,在底下留言,一起探讨。


工具链接


res-chain


参考



作者:Johnhom
来源:juejin.cn/post/7368662916151377959
收起阅读 »

你没见过的【只读模式】,被我玩出花了

web
前言 不是标题党,不是标题党,不是标题党,重要的话说三遍!大家常见的【只读模式】,下面简称 readonly,可能最常用的是在 表单场景中,除了正常的表单场景,你还会想象到它可以应用在我们中后台场景的 编辑表格、描述列表、查询表格 吗?先看看效果吧 ~ 表单场...
继续阅读 »

前言


不是标题党,不是标题党,不是标题党,重要的话说三遍!大家常见的【只读模式】,下面简称 readonly,可能最常用的是在 表单场景中,除了正常的表单场景,你还会想象到它可以应用在我们中后台场景的 编辑表格描述列表查询表格 吗?先看看效果吧 ~


表单场景


form-readonly.gif


表单列表场景


form-list-readonly.gif


描述列表场景


description-readonly.gif


查询表格场景


table-readonly.gif


编辑表格场景


edit-table-readonly.gif


上面看到的所有效果,背后都有 readonly 的存在



  1. 表单场景示例中表单列表场景示例中 使用 readonly,在实际业务中可能会应用到 编辑详情

  2. 描述列表场景示例中 使用 readonly,在实际业务中可能会应用到 单独的详情页 页面中

  3. 查询表格场景示例中 使用 readonly,在实际业务中应用很广泛,比如常见的日期,后端可能会返回字符串、空、时间戳,就不需要用户单独处理了 (挺麻烦的,不是吗)

  4. 编辑表格场景示例中 使用 readonly,在做一些类似 行编辑单元格编辑 功能中常用


下面就以 实现思路 + 伪代码 的方式和大家分享 readonly 的玩法


以 Date 组件为例


我们这里说的 Date 就是单纯的日期组件,不包含 pickermonth(月份)quarter(季度) 等,我们先思考一下,如何让 日期组件 可以在多处公用(查询表格、表单、编辑表格、描述列表)


多处公用


我们可以将 Date 组件进行封装,变成 ProDate,我们在 ProDate 中扩展一个属性为 readonly,在扩展一个插槽 readonly,方便用户自定义,以下为伪代码


<script lang="tsx">
import { DatePicker, TypographyText } from 'ant-design-vue'

export default defineComponent({
name: 'ProDate',
inheritAttrs: false,
props: {
readonly:{
type: Boolean,
default:false
}
},
slots: {
readonly: { rawValue: any }
},
setup(props, { slots, expose }) {
const getReadonlyText = computed(() => {
const value = toValue(dateValue)
return getDateText(value, {
format: toValue(valueFormat),
defaultValue: toValue(emptyText),
})
})

return {
readonly,
getReadonlyText,
}
},
render() {
const {
readonly,
getReadonlyText,
} = this

if (readonly)
return $slots.readonly?.({ rawValue: xxx }) ?? getReadonlyText

return <DatePicker {...xxx} v-slots={...xxx} />
},
})
</script>


上面的伪代码中,我们扩展了 readonly 属性和 readonly 插槽,我们 readonly 模式下会调用 getDateText 方法返回值,下面代码是 getDateText 的实现


interface GetDateTextOptions {
format: string | ((date: number | string | Dayjs) => any)
defaultValue: any
}

// 工具函数
export function getDateText(date: Dayjs | number | string | undefined | null, options: GetDateTextOptions) {
const {
format,
defaultValue,
} = options

if (isNull(date) || isUndefined(date))
return defaultValue

if (isNumber(date) || isString(date)) {
// 可能为时间戳或者字符串
return isFunction(format)
? format(date)
: dayjs(date).format(format)
}

if (isDayjs(date)) {
return isFunction(format)
? format(date)
: date.format(format)
}

return defaultValue
}

好了,伪代码我们实现完了,现在我们就假设我们的 ProDate 就是加强版的 DatePicker,这样我们就能很方便的集成到各个组件中了


集成到 表单中


因为我们是加强版的 DatePicker,还应该支持原来的 DatePicker 用法,我们上面伪代码没有写出来的,但是如果使用的话,还是如下使用


<template>
<AForm>
<AFormItem>
<ProDate v-model:value="xxxx" />
</AFormItem>
</AForm>

</template>

这样的话,我们如果是只读模式,可以在 ProDate 中增加 readonly 属性或插槽即可,当然,为了方便,我们实际上应该给 Form 组件也扩展一个 readonly 属性,然后 ProDatereadonly 属性的默认值应该是从 Form 中去取,这里实现我就不写出来了,思路的话可以通过在 Formprovide 注入默认值,然后 ProDate 中通过 inject


好了,我们集成到 表单中 就说这么多,实际上还是有很多的细节的,如果大家想看的话,后面再写吧


集成到 描述列表中


描述列表用的是 Descriptions 组件,因为大部分用来做详情页,比较简单,所以这里我将它封装成了 json 方式,用 schemas 属性来描述每一项的内容,大概是以下用法


<ProDescriptions
title="详情页"
:data-source="{time:'2023-01-30'}"
:schemas="[
{
label:'日期',
name:'time',
component:'ProDate'
}
]"

/>

解释一下:


上面的 schemas 中的项可以简单看成如下代码


<DescriptionsItem>
<ProDate
readonly
:value="get(dataSource,'time')"
/>

</DescriptionsItem>

我们在描述组件中应该始终传递 readonly: true,这样渲染出来虽然也是一个文本,但是经过了 ProDate 组件的日期处理,这样就可以很方便的直接展示了,而不用去写一个 render 函数自己去处理


集成到 查询表格中


实际上是和 集成到描述列表中 一样的思路,无非是将 ProDescriptions 组件换成 ProTable 组件,schemas 我们用同一套就可以,伪代码如下


<ProTable
title="详情页"
:data-source="{time:'2023-01-30'}"
:schemas="[
{
label:'日期',
name:'time',
component:'ProDate'
}
]"

/>

当然我们在 ProTable 内部对 schemas 的处理就要在 customRender 函数中去渲染了,内部实现的伪代码如下


<Table 
:columns="[
{
title:'日期',
dataIndex:'time',
customRender:({record}) =>{
return <ProDate
readonly
value={get(record,'time')}
/>
}
}
]"

/>

ProTableProDescriptions 的处理方式是类似的


集成到 编辑表格中


没啥好说的,实际上是和 集成到表单中 一样的思路,伪代码用法如下


<ProForm>
<ProEditTable
:data-source="{time:'2023-01-30'}"
:schemas="[
{
label:'日期',
name:'time',
component:'ProDate'
}
]"

/>

</ProForm>

我们还是复用同一套的 schemas,只不过组件换成了 ProEditTable,不同的是,我们在内部就不能写死 readonly 了,因为可能会 全局切换成编辑或者只读某一行切换成编辑或者只读某个单元格切换成编辑或者只读,所以我们这里应该对每一个单元格都需要定义一个 readonly 的响应式属性,方便切换,具体的实现就不细说了,因为偏题了


结语


好了,我们分享了 只读模式 下不同组件下的表现,而不是简单的在 表单中 为了好看而实现的,下期再见 ~


作者:一名爱小惠的前端
来源:juejin.cn/post/7329691357211361318
收起阅读 »

面试官:能不能给 Promise 增加取消功能和进度通知功能... 我:???

web
扯皮 这段时间闲着没事就去翻翻红宝书,已经看到 Promise 篇了,今天又让我翻到两个陌生的知识点。 因为 Promise 业务场景太多了自我感觉掌握的也比较透彻,之前也跟着 Promise A+ 的规范手写过完整的 Promise,所以这部分内容基本上就大...
继续阅读 »

扯皮


这段时间闲着没事就去翻翻红宝书,已经看到 Promise 篇了,今天又让我翻到两个陌生的知识点。


因为 Promise 业务场景太多了自我感觉掌握的也比较透彻,之前也跟着 Promise A+ 的规范手写过完整的 Promise,所以这部分内容基本上就大致过一遍,直到看见关于 Promise 的取消以及监听进度...🤔


只能说以后要是我当上面试官一定让候选人来谈谈这两个点,然后顺势安利我这篇文章🤣


不过好像目前为止也没见哪个面试官出过...


2024-04-03 更新:这段时间在刷牛客,无意间看到了 25 届佬: 收心檬 的个人主页 - 文章 - 掘金 (juejin.cn) 美团暑期实习的面经,绷不住了🤣


pic.png


原面经链接:美团暑期一面_牛客网 (nowcoder.com)


不知道这位面试官是不是看了我的文章出的题,例子举的都大差不差🤣


我确实标题党了想整个活,没想到大厂面试官真出啊,还是实习生,刁难人有一手的🙃...


正文


取消功能


我们都知道 Promise 的状态是不可逆的,也就是说只能从 pending -> fulfilled 或 pending -> rejected,这一点是毋庸置疑的。


但现在可能会有这样的需求,在状态转换过程当中我们可能不再想让它进行下去了,也就是说让它永远停留至 pending 状态


奇怪了,想要一直停留在 pending,那我不调用 resolve 和 reject 不就行了🤔


 const p = new Promise((resolve, reject) => {
setTimeout(() => {
// handler data, no resolve and reject
}, 1000);
});
console.log(p); // Promise {<pending>} 💡

但注意我们的需求条件,是在状态转换过程中,也就是说必须有调用 resolve 和 reject,只不过中间可能由于某种条件,阻止了这两个调用。


其实这个场景和超时中断有点类似但还是不太一样,我们先利用 Promise.race 来看看:模拟一个发送请求,如果超时则提示超时错误:


const getData = () =>
new Promise((resolve) => {
setTimeout(() => {
console.log("发送网络请求获取数据"); // ❗
resolve("success get Data");
}, 2500);
});

const timer = () =>
new Promise((_, reject) => {
setTimeout(() => {
reject("timeout");
}, 2000);
});

const p = Promise.race([getData(), timer()])
.then((res) => {
console.log("获取数据:", res);
})
.catch((err) => {
console.log("超时: ", err);
});

问题是现在确实能够确认超时了,但 race 的本质是内部会遍历传入的 promise 数组对它们的结果进行判断,那好像并没有实现网络请求的中断哎🤔,即使超时网络请求还会发出:


超时中断.png


而我们想要实现的取消功能是希望不借助 race 等其他方法并且不发送请求。


比如让用户进行控制,一个按钮用来表示发送请求,一个按钮表示取消,来中断 promise 的流程:



当然这里我们不讨论关于请求的取消操作,重点在 Promise 上



取消请求.png


其实按照我们的理解只用 Promise 是不可能实现这样的效果的,因为从一开始接触 Promise 就知道一旦调用了 resolve/reject 就代表着要进行状态转换。不过 取消 这两个字相信一定不会陌生,clearTimeoutclearInterval 嘛。


OK,如果你想到了这一点这个功能就出来了,我们直接先来看红宝书上给出的答案:


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<button id="send">Send</button>
<button id="cancel">Cancel</button>

<script>
class CancelToken {
constructor(cancelFn) {
this.promise = new Promise((resolve, reject) => {
cancelFn(() => {
console.log("delay cancelled");
resolve();
});
});
}
}
const sendButton = document.querySelector("#send");
const cancelButton = document.querySelector("#cancel");

function cancellableDelayedResolve(delay) {
console.log("prepare send request");
return new Promise((resolve, reject) => {
const id = setTimeout(() => {
console.log("ajax get data");
resolve();
}, delay);

const cancelToken = new CancelToken((cancelCallback) =>
cancelButton.addEventListener("click", cancelCallback)
);
cancelToken.promise.then(() => clearTimeout(id));
});
}
sendButton.addEventListener("click", () => cancellableDelayedResolve(1000));
</script>
</body>
</html>

这段代码说实话是有一点绕的,而且个人觉得是有多余的地方,我们一点一点来看:


首先针对于 sendButton 的事件处理函数,这里传入了一个 delay,可以把它理解为取消功能期限,超过期限就要真的发送请求了。我们看该处理函数内部返回了一个 Promise,而 Promise 的 executor 中首先开启了定时器,并且实例化了一个 CancelToken,而在 CancelToken 中才给 cancelButton 添加点击事件。


这里的 CancelToken 就是我觉得最奇怪的地方,可能没有体会到这个封装的技巧,路过的大佬如果有理解的希望能帮忙解释一下。它的内部创建了一个 Promise,绕了一圈后相当于 cancelButton 的点击处理函数是调用这个 Promise 的 resolve,最终是在其 pending -> fuilfilled,即 then 方法里才去取消定时器,那为什么不直接在事件处理函数中取消呢?难道是为了不影响主执行栈的执行所以才将其推到微任务处理🤔?


介于自己没理解,我就按照自己的思路封装个不一样的🤣:


const sendButton = document.querySelector("#send");
const cancelButton = document.querySelector("#cancel");

class CancelPromise {

// delay: 取消功能期限 request:获取数据请求(必须返回 promise)
constructor(delay, request) {
this.req = request;
this.delay = delay;
this.timer = null;
}

delayResolve() {
return new Promise((resolve, reject) => {
console.log("prepare request");
this.timer = setTimeout(() => {
console.log("send request");
this.timer = null;
this.req().then(
(res) => resolve(res),
(err) => reject(err)
);
}, this.delay);
});
}

cancelResolve() {
console.log("cancel promise");
this.timer && clearTimeout(this.timer);
}
}

// 模拟网络请求
function getData() {
return new Promise((resolve) => {
setTimeout(() => {
resolve("this is data");
}, 2000);
});
}

const cp = new CancelPromise(1000, getData);

sendButton.addEventListener("click", () =>
cp.delayResolve().then((res) => {
console.log("拿到数据:", res);
})
);
cancelButton.addEventListener("click", () => cp.cancelResolve());

正常发送请求获取数据:


发送请求.gif


中断 promise:


取消请求.gif


没啥大毛病捏~


进度通知功能


进度通知?那不就是类似发布订阅嘛?还真是,我们来看红宝书针对这块的描述:



执行中的 Promise 可能会有不少离散的“阶段”,在最终解决之前必须依次经过。某些情况下,监控 Promise 的执行进度会很有用



这个需求就比较明确了,我们直接来看红宝书的实现吧,核心思想就是扩展之前的 Promise,为其添加 notify 方法作为监听,并且在 executor 中增加额外的参数来让用户进行通知操作:


class TrackablePromise extends Promise {
constructor(executor) {
const notifyHandlers = [];
super((resolve, reject) => {
return executor(resolve, reject, (status) => {
notifyHandlers.map((handler) => handler(status));
});
});
this.notifyHandlers = notifyHandlers;
}
notify(notifyHandler) {
this.notifyHandlers.push(notifyHandler);
return this;
}
}
let p = new TrackablePromise((resolve, reject, notify) => {
function countdown(x) {
if (x > 0) {
notify(`${20 * x}% remaining`);
setTimeout(() => countdown(x - 1), 1000);
} else {
resolve();
}
}
countdown(5);
});

p.notify((x) => setTimeout(console.log, 0, "progress:", x));
p.then(() => setTimeout(console.log, 0, "completed"));


emm 就是这个例子总感觉不太好,为了演示这种效果还用了递归,大伙们觉得呢?


不好就自己再写一个🤣!不过这次的实现就没有多大问题了,基本功能都具备也没有什么阅读障碍,我们再添加一个稍微带点实际场景的例子吧:



// 模拟数据请求
function getData(timer, value) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(value);
}, timer);
});
}

let p = new TrackablePromise(async (resolve, reject, notify) => {
try {
const res1 = await getData1();
notify("已获取到一阶段数据");
const res2 = await getData2();
notify("已获取到二阶段数据");
const res3 = await getData3();
notify("已获取到三阶段数据");
resolve([res1, res2, res3]);
} catch (error) {
notify("出错!");
reject(error);
}
});

p.notify((x) => console.log(x));
p.then((res) => console.log("Get All Data:", res));


notify获取数据.gif


对味儿了~😀


End


关于取消功能在红宝书上 TC39 委员会也曾准备增加这个特性,但相关提案最终被撤回了。结果 ES6 Promise 被认为是“激进的”:只要 Promise 的逻辑开始执行,就没有办法阻止它执行到完成。


实际上我们学了这么久的 Promise 也默认了这一点,因此这个取消功能反而就不太符合常理,而且十分鸡肋。比如说我们有使用 then 回调接收数据,但因为你点击了取消按钮造成 then 回调不执行,我们知道 Promise 支持链式调用,那如果还有后续操作都将会被中断,这种中断行为 debug 时也十分痛苦,更何况最麻烦的一点是你还需要传入一个 delay 来表示取消的期限,而这个期限到底要设置多少才合适呢...


至于说进度通知功能,仁者见仁智者见智吧...


但不管怎么样两个功能实现的思路都是比较有趣的,而且不太常见,不考虑实用性确实能够成为一道考题,只能说很符合面试官的口味😏


作者:討厭吃香菜
来源:juejin.cn/post/7312349904046735400
收起阅读 »

凯文·凯利给我们的 42 个人生建议

五一回到老家,如果以能住一晚为标志,那大概也有十年没有回老家了。 把车停好,就听到熟悉的蛙鸣声,闻着带着些许水气的潮湿的空气。 恍然 仿佛回到了那个老爸老妈还很年轻,我还要骑着自行车,自己做早餐,早早起来上学的年纪。 一切仿佛还在昨天,但一切都已经不一样了。 ...
继续阅读 »

五一回到老家,如果以能住一晚为标志,那大概也有十年没有回老家了。


把车停好,就听到熟悉的蛙鸣声,闻着带着些许水气的潮湿的空气。


恍然


仿佛回到了那个老爸老妈还很年轻,我还要骑着自行车,自己做早餐,早早起来上学的年纪。


一切仿佛还在昨天,但一切都已经不一样了。


四十不惑,不是不疑惑,应该是有些事情不计较,有些东西,想想算了,想想放下了。


前段时间读了凯文·凯利 2023 年的新书《宝贵的人生建议 : 我希望早点知道的智慧》中有提到这本书的的 目标是传递经过时间检验的智慧,但是用我的话表达出来。


这是一本小书,在读完后,我对于其中认同的建议,我也用自己的话,中国传统表述或者之前一些读的书之类的提到的句子尝试理解和表达。大概做了个分类,不是说教,也就是表达一下。


学习和成长


1. 终身学习



毫不犹豫地自我投资——

花钱上课,学习新技能。

这些不起眼的投资,

能产生丰厚的回报。



保持好奇心,读万卷书,行万里路


2. 读史使人明智



大量阅读历史,

你就会明白

过去发生过多少怪事;

这样,对于未来的怪事,

你将见怪不怪。



司马迁在《史记》中说:"究天人之际,通古今之变,成一家之言。"


以铜为鉴,可以正衣冠;以史为鉴,可以知兴替。


《圣经·旧约》中说:太阳底下没有新鲜事


3. 费曼学习法



学习的

最好方法是,

试着把你

会的东西

教给别人。



输出倒逼输入


4. 敏而好学,不耻下问



不要害怕问

听上去愚蠢的问题。

因为在99%的情况下,

其他人都在想

同一个问题,

只是不好意思问出口。



5. 三省吾身



无论在什么年纪,

你都可以问自己:

“为什么我还在做这件事?”

对这个问题,

你需要进行很好的回答。



有点扎心,吾日三省吾身


我为什么还在写文章,为什么还在工作?


6. 开始写作吧



画画能画出你看到了什么。

写作能揭示出你的所思所想。



7. 多读书



要不同凡响,

就需要读书。



曾经一直在简历上写:好读书不求甚解


也是如此践行,量变最终会产生质变。


8. 直面困难



作为一个成熟的人,
衡量你成长的尺度是,
你愿意进行多少令人不舒服的谈话。



近些年越发觉得自己成熟了


不破不立,不塞不流,不止不行。


家庭生活和教育


9. 门当户对



你不是与一个人结婚,

你是与一家人结婚。



婚姻应该在门第相当、家境相似的人家之间进行。这不仅是为了维护身份地位,更是为了确保两个家庭的文化背景和生活方式能够兼容。


选择一个人,就是选择一种生活方式。


10. 善待你的孩子



善待你的孩子,

因为以后是他们为你选择养老院。



树高千丈,叶落归根


积善之家,必有余庆。


善待那个最终决定拔不拔管子的孩子


11. 最好的教育



经常给孩子读书

是他们能受的最好的教育。



12. 回家吃饭



对你的家庭来说,

最好的良药是:

经常在一起吃饭,

不开电视。



今年的一个小目标是一周到少回家吃一次晚饭。但是过了这么久,好像很少。


成功


13. 终局思维



做事要以终为始。

碗碟架堆满后,

再想调整,

就无从下手了。



谋定而后动


凡事预则立,不预则废。


14. 要有备份



制做任何东西,

都要额外多做一些准备,

比如额外的

材料、零件、空间、装饰。

这些额外的东西是

应对错误的保障,

能减轻压力,

防范未来的风险。

这是最便宜的保险。



在程序员界流传着这样一句话:「冗余不做,日子甭过;备份不做,十恶不赦」


15. 坚持



努力,

无论锻炼、陪伴还是工作,

重要的不是数量,

而是坚持。

坚持每天做一点,

比什么都强,

这比你偶尔一为重要得多。



不积跬步,无以至千里;不积小流,无以成江海


成功三要素: 坚持,不要脸,坚持不要脸


16. 长期主义



我们往往高估

一天能完成的事,

而低估十年能取得的成就。

拿出十年来,

你可以成就

不可思议的奇迹。

坚持长期主义,

积小胜为大胜,

即使犯了大错误,

也可以慢慢改正。



长期主义,做时间的朋友


17. 复利



无论财富、
人际关系还是知识,

生活中那些最大的奖赏,

都来自

神奇的复利,

即微小的、稳定的收益不断放大。

要实现富足,

你所需的不过是,

持之以恒地让投入比减损大1%。



做时间的朋友


18. 好事多磨



坏事可能飞速发生,

但几乎所有好事都是慢慢展开的。



厚积薄发


瓜熟蒂落,水到渠成。


欲速则不达,见小利则大事不成。


19. 买卖时间



每个人的时间都是有限的,

每个人的时间都在不断减少。

你能用钱获得的最高杠杆,

就是买别人的时间。

在可能的情况下,

要聘请员工,

外包工作。



《认知红利》中提到时间商人的四种经营模式,第三种:买卖时间:本质是个放大器,通过买入别人的时间,来提升自己的效率、提高时间单价、扩大生产规模。


20. 要做多



有限的游戏,

关乎输赢。

无限的游戏,

则让游戏继续下去。

去玩那些无限的游戏,

因为无限的游戏

能带来无限的回报。



人生如逆旅,我亦是行人


输赢、得失,都只是人生的过眼云烟。


真正重要的,是在这个过程中,我们有没有不断提升自己,有没有始终保持一颗向上的心。


21. 慢可能是快



多任务操作是一个迷思。

走路、跑步、骑自行车或开车时,

不要发信息。

稍停片刻没关系,

没有人会因为这一分钟忘记你。



记得小时候有篇课文是讲时间并行的,一直这样做事情,觉得效率高,这么多年过去了,发现有时候专注的慢也是一种快。保持专注 一次只做一件事,把事情做完


22. 耐得烦



培养对小事的耐心,

你才能对大事保持耐心。



作为一个洗碗十多年的非专业选手,在多年的洗碗过程中慢慢体会了这种耐心。


在《道德经》中,老子曾说:"合抱之木,生于毫末;九层之台,起于累土;千里之行,始于足下。"


23. 打破常规



成功最可靠的方法,

是你自己定义成功。

先射箭,

然后在射中的地方,

画一个靶心。



孟子说:"舜何人也?予何人也?有为者亦若是。"


有人说:成功者都是创造机会,而不是等机会


24. 但行好事,莫问前程



当你陷入困境或力不能支时,

专注在力所能及的小事上,

这能推进事情的进展。



冯唐说面对逆境时: 看脚下,不断行,莫存顺逆


25. 聚集



在博物馆里,

你需要花至少10分钟,

才能真正地欣赏一件艺术品。

哪怕看5件展品,

每件花10分钟,

也不要看100件展品,每件花30秒。



有舍有得,百鸟在林,不如一鸟在手


工作和生活


26. 迈出舒适区



最好的工作

是一个你不够格的工作,

因为它会迫使你挖掘潜力。

事实上,

要只去应聘那些

你不够格的工作。



挑战自己,迈出舒适区


人往高处走


27. 断舍离



你的时间和空间是有限的。

那些不能再给你

带来快乐的东西,

要移走、送人、扔掉,

给能给你

带来快乐的东西

腾出时间和空间。



28. 知易行难,只是没钱



能轻松用钱解决的问题

不是真正的问题,

因为解决办法显而易见。

把注意力集中在那些

没有显而易见的

解决办法的问题上。



然而现实是大多数人没钱


29. 我选择早到



准时代表着尊重。




没有“准时”这回事。

要么你迟到了,

要么你早到了。

这是你的选择。



30. 你有什么建议吗



如果你寻求别人的反馈,

你会得到批评。

但如果你寻求建议,

你会得到一个搭档。



31. 圣人不器



穿过一个可能禁止你通行的地方,

你要表现得轻松自如,

就像你本属于这里。



别问可不可以,问了就是不可以


32. 以德报怨,何如?



当你原谅别人时,

对方可能没有察觉,

但你会释怀。

宽恕不是为了别人,

宽恕是我们给自己的礼物。



《道德经》中说:"不伐善,不夸能,不矜功,夫唯不争,故天下莫能与之争。"


不要内耗,放过自己


33. 喝喝酒



请客吃饭永远是有效的方法,

而且简单易行。

这对老朋友很有效,

也是结交新朋友的好方法。



酒逢知己千杯少,话不投机半句多。


带团队过程中,喝酒后的大家都是不一样的。


34. 听其言,不如观其行



你是什么样的人,取决于你做什么。

不在于你说什么,

不在于你信什么,给谁投票,

而在于,

你把时间花在什么上面。



躬身入局,贵在实践。


注意力是人最重要的资源


35. 我本善良



每当要在正确和

善良之间做出选择时,

你都要毫无例外地选择善良。

不要把善良和软弱混为一谈。



孔子说:"志士仁人,无求生以害仁,有杀身以成仁。"


36. 分权



分东西时,

一个人分,

另一个先选。



分权的逻辑


君子和而不同,小人同而不和。


37. 坦诚



始终在一开始就提出你想要什么。

这适用于人际关系、商业和生活。



38. 对自己好一点



人生三分之一的时间

是躺在床上睡觉,

几乎另外三分之一,

是在椅子上坐着。

花钱买好床、好椅子,

是物有所值的投资。



还有好的枕头


39. 休息一下



如果你不能确定自己迫切需要什么,

你迫切需要的也许是睡觉。



小时候看聪明的一休,开头都会说,不要着急不要着急,休息休息一会儿


40. 人生得意须尽欢



不要把精美的瓷器和好酒,

非留到难得的场合才拿出来,

这一等可能就是永久;

只要有机会,就可以拿出来。



在耳熟能详的中国诗歌中就有两句非常有名的:



  • 人生得意须尽欢,莫使金樽空对月

  • 花开堪折直须折,莫待无花空折枝


人生百年,如白驹过隙


人到中年,也越发觉得如此,应该让自己开心一些。


41. 事不过三



对每个人,

都要给第二次机会,

但不要给第三次。



这里其实只有二次,与中国传统的事不过三的说法差一次。


42. 遗憾



人生中只有很少的遗憾,

是遗憾自己做了什么。

几乎所有的遗憾都是遗憾自己没有做什么。



最后以左宗棠的对联结束本篇文章,见下图:


发上等愿,结中等缘,享下等福;择高处立,寻平处住,向宽处行.png
发上等愿,结中等缘,享下等福;择高处立,寻平处住,向宽处行


作者:潘锦
来源:juejin.cn/post/7363491538288787494
收起阅读 »

幸福不搞末位淘汰制

来深圳又已两周了,每次初来深圳的时候皮肤都会很难受,不知道是空气质量差还是空气湿度高,浑身都会长一些小疹子和痒痒的包,已经连着几天没有睡好了,既然睡不着那就写点东西,顺便发发牢骚吧。 开始表达 随着年龄的增长,我想,人的表达欲确实是会不断的下降,上一次半夜睡不...
继续阅读 »

来深圳又已两周了,每次初来深圳的时候皮肤都会很难受,不知道是空气质量差还是空气湿度高,浑身都会长一些小疹子和痒痒的包,已经连着几天没有睡好了,既然睡不着那就写点东西,顺便发发牢骚吧。


开始表达


随着年龄的增长,我想,人的表达欲确实是会不断的下降,上一次半夜睡不着写长文还是大一时和几个室友一直聊天到三四点。会不会有一天我对爱的人也不再有表达的欲望,选择三缄其口了呢?我不知道,但是总觉得这是一件很可怕的事情。严格来说我的表达欲也不是单纯的线性下降,中学、尤其是初中时期因为各种各样的原因导致我很自卑,自卑无论对哪个时期的任何人来说都一定是一个很严重的debuff。很幸运的是之后碰到了很多很好很好的人,慢慢的也逐渐走出了泥潭,变得开朗了。


和大部分人一样,大概从高中的时候心智就逐渐趋于成熟了吧,虽然依旧很幼稚,但是那时开始对身边发生的一些事、一些人、社会上的一些热点事件,进行各种各样的分析,得到各种各样经验性的结论,创造出各类只有自己才知道的名词,但是后来发现这些词所代表的含义早就有先贤提出了,虽然没什么意义,但是下意识思考为什么的习惯确实是在那个时候养成的。印象很深的是当时有想到刺猬人的概念,有的人就像刺猬一样,接受不了任何负面的评价,一旦你对他们稍稍辞严令色,他们就会立马竖起全身的刺对你进行攻击,想尽所有办法来驳斥、回击、批判你,绝不会想想自己是否真的存在对应的问题。So,我从高中开始很少对任何人进行任何形式的批评,如果真的有傻逼影响到我的心情的话,那么他不会有第二次影响我心情的机会了。


但是我一直都不爱表达和记录。我也记不清是从什么时间节点开始的,逐渐用一些笔记app习惯性的记录下来每天干了些什么,自己的一些随笔想法,新接触到的一些有用的观点和方法等等,逐渐养成了表达、记录的习惯。

我一直觉得人是环境的产物,这个环境既有时代背景,有当下所处的环境,也有一路走来的经历。有时候感觉自己真的很奇怪,不知道具体是哪部分环境影响到了自己,但是可以确定的是心理上有着不小的问题。比如危机意识过重,总是认为自己的处境不算安全,于是经常处于忙碌的状态,总是想多学点东西、提升提升自己,多做一些能规避风险,拓宽安全边界的事情,常常周末也不会停歇。这种心态一定是有问题的,但是具体怎么纠正回来,我想需要以年为单位的尝试才能成功。


个体乐观、群体悲观


同时,和多数人的个体层面悲观以及国家、社会层面的乐观不同,他们对祖国的未来充满希望,但对个人的前途却一片迷茫,看不到出路。


我刚好是反过来的,对个体乐观,但对社会、制度层面悲观。只和自己相关的事情,我总是能实现或者接近目标,并以积极的角度看待问题。比如高考成绩,几乎只取决于自己,考得不好无非是再来几次;一次面试失败,不过无非是和这家公司没有缘分,多积累积累,依旧有很多机会在前面等着你;减肥失败,时间还长,只要真的想,迟早是能瘦下来的。只要命还在,又有什么困难是能真正将一个人击倒的?


但是如果涉及到人和时间这种影响因子很大的变量,事情又会变得很复杂,我又会趋于悲观。比如一段感情的维系,需要A\B双方的努力和呵护,谁能保证对方一直爱自己呢,谁又能保证以后的自己仍会爱着对方呢?从某种意义上说,几年后甚至几个月之后的你,和现在的你已经不是同一个人了。故而,我一直没有能够和一个人长相厮守、白头偕老,共同度过数十年的自信。

但是在社会制度层面,我认为很多机制、策略是不可能改变的,这些东西就像定理一样深深烙印在现实世界中。国家从某种意义上来说是一个合法的暴力机构,这就导致一旦权利运转出现哪怕一丝一毫的问题,也会产生权利对个人无情倾轧的现象,并且无论科技、时代发展到什么程度,只要有人,就一定会有阶级,就一定会有不公,这不以个人的意志为转移。当代史就是过去的历史,未来史也会是当代史,朱令案、六盘水案、承德程序员案,也只是类似窦娥冤这种封建时代悲剧话本的重演罢了。


又不知道扯到哪里去了,我是想表达什么呢?其实我想说的是每个人都有选择自己生活方式的权利,无论和对方有多么亲密,对他人的生活方式、想法、行为加以指责都是一件很傲慢的事情,子非我,安知我不知鱼之乐?顺便记录一下现在自己的所想。


记录的意义


我大概是从2023年的一月开始发朋友圈,没有细数发了多少条,大概能有个五六十条?


之前不发朋友圈,不作任何记录,也不会拍照,是因为我对自己的记忆力有信心,我觉得我可以用眼睛和脑子记住生活中各种各样的美好,记得走过的路,路上的风景,陪我走在路上的人。但是随着时间的推移,很多本应该珍藏的记忆已经慢慢模糊了,在意识到了这一点之后,记录就已经迫在眉睫了。


发朋友圈的初衷是因为微信里面有很多我觉得很重要的人,我的爱人、家人、挚友、同学,人的精力是有限的,不可能一个个分享;同时,和别人的关系也是有周期的,有很多已经很久不曾联系、但是曾经关系很好的朋友们,他们也同样重要。一条朋友圈,就能让很多我觉得重要的人知道我最近在干什么。


感觉相比于条条框框很多的上学来讲,我还是比较适合上班,虽然压力比在学校大不少,但是离自由和幸福的距离近了不止一点半点。


我可能对物质的要求没有那么高,我不想住很大的房子,三十平不到的出租屋就能让我住的很开心;我也没有想过买豪车,骑骑共享单车或者小毛驴也很舒服,还不用停车费;我也不想穷奢极欲去吃一些很豪华的大餐之类的,自己做的饭菜我吃着就很满意。甚至对于钱我也没有那么看重,我只是把钱当成一个掌握自由的工具和底气,如果真的给我很多很多钱,除了留下自己这辈子够花的那部分,其他的会给那些真的很难很难的人。


感觉自己改变最大的还是在幸福能力上的进步,在大二我就意识到了其实我打理好自己的生活、过日子的能力很差,在那时候我的一天基本是在“一天啥也不干,只在床上躺尸”和“早出晚归,一天学习时间超长,抓紧点滴时间”这两种模式中二选一,所以时常嘲笑自己骨子里是根二极管。


但现在已经完全不一样了,我能平衡好工作和生活,事业和感情,闲的时候找点提升自己的事情做,忙的时候也要抽空兜兜风做做饭。


父母身体健康、和爱人的感情稳中向好、工作顺心、三五挚友、有一些爱好。仅仅是这样,对我来说就已经足够了。


这样想想,幸福其实很简单,毕竟幸福又不搞末位淘汰制


作者:安妮的心动录
来源:juejin.cn/post/7350971151131541567
收起阅读 »

检测图片是否cmyk

web
引入 最近业务上有要求,要求如果是 Jpeg 格式文件, 前端在上传的时候要求判断一下这个文件是否 CMYK 颜色模式(color mode/ color space)。 这个颜色模式是打印行业需要的。如果不是则禁止上传,并提示用户。 一开始我以为这个应该存储...
继续阅读 »

引入


最近业务上有要求,要求如果是 Jpeg 格式文件, 前端在上传的时候要求判断一下这个文件是否 CMYK 颜色模式(color mode/ color space)。 这个颜色模式是打印行业需要的。如果不是则禁止上传,并提示用户。


一开始我以为这个应该存储在 exif 文件信息中, 去拿一下就好了, 但是简单测试发现两个问题:



  1. 文件是否携带 exif 信息是不确定的, 即便出自设计师导出文件, 有可能也是不携带颜色模式信息的。

  2. 除此之外, 依靠 exif 信息去判断,严格来说,即便携带,也是不准确的, 因为这个信息是可以被人为修改的。


经过一番研究, 我暂时发现可能有两种方式,去达成目的。 但是这篇文章实际不是以解决问题为导向,而是期望尽可能的深入一丢丢。 如果急于找到解决方案, 直接翻到文章底部查看具体 编码实现 即可。


什么是 CMYK 颜色模式?



了解 Photoshop 颜色模式 (adobe.com)



CMYK 是一种颜色模式,它表示四种颜色通道:青色(Cyan)、品红色(Magenta)、黄色(Yellow)和黑色(Key,通常表示黑色)。这种颜色模式主要用于印刷和彩色印刷工作中。


以下是 CMYK 颜色模式中各颜色通道的简要介绍:



  1. 青色 (Cyan): 表示蓝绿色。在印刷中,它用于调整蓝色和绿色的浓度。

  2. 品红色 (Magenta): 表示品红或洋红色。在印刷中,它用于调整红色和蓝色的浓度。

  3. 黄色 (Yellow): 表示黄色。在印刷中,它用于调整红色和绿色的浓度。

  4. 黑色 (Key): 通常表示黑色。在印刷中,黑色是通过使用黑色油墨单独添加的,以增加图像的深度和对比度。在 CMYK 模式中,K 代表 Key,以避免与蓝色 (B) 冲突。


这四个颜色通道可以叠加在一起以创建各种颜色。通过调整每个通道的浓度,可以实现广泛的颜色表达。CMYK 被广泛用于印刷领域,因为它能够准确地模拟很多颜色,并提供了在印刷过程中需要的色彩控制。


与 RGB(红绿蓝)颜色模式不同,CMYK 是一种适合印刷的颜色模式,因为它更好地反映了油墨混合的方式,并考虑到印刷物质上的光的特性


怎么在web判断一个 jpeg/jpg 文件 颜色模式是否 cmyk ?


简单说一下这两种方法, 实际上是同一种原理, 因为对于一张图片而言, 它除了携带有 exif 文件元信息之外, 还有文件头信息。


既然不能通过 exif 元信息去判断, 那么我们可以通过文件头信息去做判断。


首先,简单测试可以发现, 即便一个 cmyk 图片没有 exif 描述元信息标识这是一个 cmyk 颜色模式的图片, 但是 各种设计类软件都能够标记出来。 以ps为例:


image-20231128163932682.png
但是 exif 信息中是没有的:


image-20231128164033843.png


甚至一些解析库,就连最基本携带的元信息都没读出来:



stackblitz.com/edit/exif-j…



image-20231128164214625.png


为什么设计软件可以标记出这个图片是否是 cmyk 颜色模式?


这个问题, 我在网上翻了很久,确实是找不到相关文章有阐述设计软件的原理。 不过Ai 的回答是这样的, 具备一定的参考性:



有朋友找到了记得踢我一脚,这里提前感谢啦~



image-20231128174834089.png


用 ImageMagic 解析图片文件



什么是 imageMagic ?


ImageMagick 主要由大量的命令行程序组成,而不提供像 Adobe Photoshop、GIMP 这样的图形界面。它还为很多程序语言提供了 API 库。


ImageMagick 的功能包括:



  • 查看、编辑位图文件

  • 进行图像格式转换

  • 图像特效处理

  • 图像合成

  • 图像批处理


ImageMagick 广泛用于图像处理、图形设计、Web 开发等领域。它是许多开源软件项目的重要组成部分,例如 GIMP、Inkscape、Linux 系统中的图像工具等。


ImageMagick 的优势包括:



  • 功能强大,支持多种图像格式和图像处理功能

  • 开放源代码,免费使用

  • 、、可移植性强,支持多种操作系统



@jayce: imageMagic 类似于 ffmpeg, 只不过它专注图像处理




我们可以利用 ImageMagic 的 identify 工具命令 去解析图片以查看一些信息:


image-20231128180008082.png


加上 -verbose 选项可以查看更多详细信息:


$ ./magick identify -verbose ./CMYK.jpg
$ ./magick identify -verbose ./RGB.jpg

image-20231129092244504.png


这些数据是什么? 从哪里解析出来的呢? 这个需要看一下 jpeg 文件的一些标准文件结构


ISO/IEC 10918-1 和 ISO/IEC 10918-5


这两个文件都是 JPEG 的标准文档,只是不同的部分,wiki 上对二者描述大致是 5 是 对 1 的很多细节的展开和补充。是补充规范


JPEG File Interchange Format (JFIF) 和 Exif


JFIF(JPEG File Interchange Format)和 EXIF(Exchangeable image file format)是两种与 JPEG 图像相关的标准,但它们具有不同的目的和功能。


JFIF 是一个图片文件格式标准, 它被发布于 10918-5, 是对 10918-1 的细节补充。



  1. JFIF (JPEG File Interchange Format):

    • 目的: JFIF 是一种用于在不同设备和平台之间交换 JPEG 图像的简单格式。它定义了 JPEG 文件的基本结构,以确保文件在不同系统中的一致性和可互操作性。

    • 特点: JFIF 文件通常包含了基本的图像数据,但不一定包含元数据信息。它主要关注图像的编码和解码,而不太关心图像的其他详细信息。JFIF 文件通常使用 .jpg 或 .jpeg 扩展名。



  2. EXIF (Exchangeable image file format):

    • 目的: EXIF 是一种在数字摄影中广泛使用的标准,用于嵌入图像文件中的元数据信息。这些元数据可以包括拍摄日期、相机型号、曝光时间、光圈值等。EXIF 提供了更丰富的信息,有助于记录和存储与拍摄有关的详细数据。

    • 特点: EXIF 数据以二进制格式嵌入在 JPEG 图像中,提供了关于图像和拍摄条件的详细信息。这对于数字相机和其他支持 EXIF 的设备非常有用。EXIF 文件通常使用 .jpg 或 .jpeg 扩展名。




JPEG 文件标准结构语法


jpeg 作为压缩数据结构, 是一个非常复杂的数据组织, 我们的关注点只在关系到我们想要解决的问题。 标准文档 ISO/IEC 10918-1 : 1993(E).中有部分相关说明。


概要:


结构上来说, jpeg 的数据格式由以下几个部分,有序组成: parameters, markers, 以及 entropy-coded data segments, 其中 parameters 和 markers 部分通常被组织到 marker segments, 因为它们都是用字节对齐的代码表, 都是由8位字节的有序序列组成。


Parameters


这部分携带有参数编码关键信息, 是图片成功被解析的关键。


Markers


Markers 标记用于标识压缩数据格式的各种结构部分。大多数标记开始包含一组相关参数的标记段;有些标记是单独存在的。所有标记都被分配了两个字节的代码


例如 SOI : 从 0xFF,0xD8这两个字节开始,标记为图片文件的文件头开始, SOF0: 从 0xFF, 0xD8这两个字节开始,标记了 ”帧“ 的开始,它实际上会携带有图片的一些基本信息, 例如宽高,以及颜色通道等。 这个颜色通道其实也是我们主要需要关注的地方。


下表是完整的标记代码:


image-20231129095415255.png



@refer:


http://www.digicamsoft.com/itu/itu-t81…
http://www.digicamsoft.com/itu/itu-t81…



wiki 上也有相关的帧头部字段说明:


Short nameBytesPayloadNameComments
SOI0xFF, 0xD8noneStart Of Image
SOF00xFF, 0xC0variable sizeStart Of Frame (baseline DCT)Indicates that this is a baseline DCT-based JPEG, and specifies the width, height, number of components, and component subsampling (e.g., 4:2:0).
SOF20xFF, 0xC2variable sizeStart Of Frame (progressive DCT)Indicates that this is a progressive DCT-based JPEG, and specifies the width, height, number of components, and component subsampling (e.g., 4:2:0).
DHT0xFF, 0xC4variable sizeDefine Huffman Table(s)Specifies one or more Huffman tables.
DQT0xFF, 0xDBvariable sizeDefine Quantization Table(s)Specifies one or more quantization tables.
DRI0xFF, 0xDD4 bytesDefine Restart IntervalSpecifies the interval between RSTn markers, in Minimum Coded Units (MCUs). This marker is followed by two bytes indicating the fixed size so it can be treated like any other variable size segment.
SOS0xFF, 0xDAvariable sizeStart Of ScanBegins a top-to-bottom scan of the image. In baseline DCT JPEG images, there is generally a single scan. Progressive DCT JPEG images usually contain multiple scans. This marker specifies which slice of data it will contain, and is immediately followed by entropy-coded data.
RSTn0xFF, 0xDn (n=0..7)noneRestartInserted every r macroblocks, where r is the restart interval set by a DRI marker. Not used if there was no DRI marker. The low three bits of the marker code cycle in value from 0 to 7.
APPn0xFF, 0xEnvariable sizeApplication-specificFor example, an Exif JPEG file uses an APP1 marker to store metadata, laid out in a structure based closely on TIFF.
COM0xFF, 0xFEvariable sizeCommentContains a text comment.
EOI0xFF, 0xD9noneEnd Of Image


Syntax and structure



整体结构


image-20231129111546427.png



@refer: http://www.digicamsoft.com/itu/itu-t81…



Frame Header


image-20231129111645470.png


image-20231129112439294.png


image-20231129112306831.png



@refer: http://www.digicamsoft.com/itu/itu-t81…



SOFn : 帧开始标记标记帧参数的开始。下标n标识编码过程是基线顺序、扩展顺序、渐进还是无损,以及使用哪种熵编码过程。


在其标准文档中,我们有找到 SOFn 的子字段说明,不过在其他地方,倒是看到了不少描述:


特别是在这里 JPEG File Layout and Format


image-20231129141121729.png


可以看到,在 SOFn 这个标记中, 有一个字段为会指明 components 的数量,它代表的实际上颜色通道, 如果是 1,那么就是灰度图, 如果是3,那就是RGB, 如果是 4 就是 CMYK.


到这里我们就知道了, 我们可以读取到这个对应的字节段,从而判断一个图片的颜色模式了。


怎么读取呢?


这篇资料说了明了 Jpeg 文件格式中字节和上述字段的关联关系: Anatomy of a JPEG


注意这篇资料中有一段描述,会影响到我们后续的逻辑判断:


image-20231129142053598.png



就是 SOF0 是必须的,但是可以被 SOFn>=1 替换。 所以在做逻辑判断的时候,后续的也要判断。



我们可以先大概看看一个图片文件的字节流数据长什么样子:(因为所有的字段都是 FF 字节位开头,所以高亮了)


1701248911601.png



以上页面可以在这里访问: jaycethanks.github.io/demos/DemoP…



但样太不便于阅读了, 而且实在太长了。 这里有个网站 here,可以将关键的字节段截取出来:


image-20231129171534152.png


我们主要看这里:


image-20231129171621345.png
可以看到 components 为 4.


如果是 RGB:


image-20231129171722071.png


这里就是 3,


如果是灰度图,components 就会是1


image-20231129172500605.png


EXIF 在哪里?


一个额外的小问题, 我们常见的 exif 元信息存储在哪里呢?


其实上面的 Markers 部分给出的表格中也说明了 ,在 Appn 中可以找到 exif 信息, 但是wiki 上说的是 App0, 在这个解析网站中,我们可以看到:


image-20231201113938640.png


编码实现


有了上述具体的分析, 我们就能有大致思路, 这里直接给出相关代码:



代码参考 github.com/zengming00/node-jpg-is-cmyk




/**
*
@refer https://github.com/zengming00/node-jpg-is-cmyk/blob/master/src/index.ts

*
@refer https://cyber.meme.tips/jpdump/#
*
@refer https://mykb.cipindanci.com/archive/SuperKB/1294/JPEG%20File%20Layout%20and%20Format.htm
*
@refer https://www.ccoderun.ca/programming/2017-01-31_jpeg/
*
* 通过 jpg 文件头判断是否是 CMYK 颜色模式
*
@param { Uint8Array } data
*/

function checkCmyk(data: Uint8Array) {
let pos = 0;
while (pos < data.length) {
pos++;
switch (data[pos]) {
case 0xd8: {// SOI - Start of Image
pos++;
break;
}
case 0xd9: {// EOI - End of Image
pos++;
break;
}
case 0xc0: // SOF0 - Start of Frame, Baseline DCT
case 0xc1: // SOF1 - Start of Frame, Extended Sequential DCT
case 0xc2: { // SOF2 - Start of Frame, Progressive DCT
pos++;
const len = (data[pos] << 8) | data[pos + 1];
const compoNum = data[pos + 7];
if (compoNum === 4) {
// 如果components 数量为4, 那么就认为是 cmyk
return true;
}
pos += len;
break;
}
case 0xc4: { // DHT - Define Huffman Table
pos++;
const len = (data[pos] << 8) | data[pos + 1];
pos += len;
break;
}
case 0xda: { // SOS - Start of Scan
pos++;
const len = (data[pos] << 8) | data[pos + 1];
pos += len;
break;
}
case 0xdb: { // DQT - Define Quantization Table
pos++;
const len = (data[pos] << 8) | data[pos + 1];
pos += len;
break;
}
case 0xdd: { // DRI - Define Restart Interval
pos++;
const len = (data[pos] << 8) | data[pos + 1];
pos += len;
break;
}
case 0xe0: { // APP0 - Application-specific marker
pos++;
const len = (data[pos] << 8) | data[pos + 1];
pos += len;
break;
}
case 0xfe: { // COM - Comment
pos++;
const len = (data[pos] << 8) | data[pos + 1];
pos += len;
break;
}
default: {
pos++;
const len = (data[pos] << 8) | data[pos + 1];
pos += len;
}
}
}
return false;
}

有没有其他的方法?


既然 imageMagic 这么成熟且强大, 我们有办法利用它来做判断吗?


我们可以通过 wasm, 在web中去利用这些工具, 我找到了 WASM-ImageMagick 这个, 但是他的打包好像有些问题 vite 引入的时候会报错,看着好像也没有要修复的意思, issue 里面有老哥自己修改了打包配置进行了修复在这里: image-magick


我们就写的demo测试函数:


import * as Magick from '@xn-sakina/image-magick'

export default function (file: File) {
if (!file) return;
// 创建FileReader对象
var reader = new FileReader();
// 当读取完成时的回调函数
reader.onload = async function (e) {
// 获取ArrayBuffer
var arrayBuffer = e.target?.result as ArrayBuffer;
if (arrayBuffer) {
// 将 ArrayBuffer 转换为 Uint8Array
const sourceBytes = new Uint8Array(arrayBuffer);
const inputFiles = [{ name: 'srcFile.png', content: sourceBytes }]
let commands: string[] = ["identify srcFile.png"]
const { stdout } = await Magick.execute({inputFiles, commands});

// 这里打印一下结果
console.log('stdout:',stdout[0])

}
};
// 读取文件为ArrayBuffer
reader.readAsArrayBuffer(file);
}

import isCmyk from '../utils/isCmyk.ts'
const handleFileChange = (e: Event) => {
const file = (e.target as HTMLInputElement)?.files?.[0]
isCmyk(file) // 这里文件上传调用一下
......

测试几个文件


image-20231130104941592.png


可以看到, Gray, RGB, CMYK 检测都可以正常输出, 说明可以这么干。



但是这个库, 文档写的太乱了。 - -



这个库的大小有 5 m之大 - -, npm 上找了下, 目前相关的包,也没有比这个更小的好像。


作者:sun_zy
来源:jaycethanks.github.io/blog_11ty/posts/Others/%E6%A3%80%E6%B5%8B%E5%9B%BE%E7%89%87%E6%98%AF%E5%90%A6cmyk/
收起阅读 »

我是如何使用Flow+Retrofit封装网络请求的

各位好,本人是练习kt时长一年的准练习生,接下来我将用一种我认为还行的方式封装网络请求,如有雷点,请各位佬轻喷 首先,定义一个请求结果类 sealed class RequestResult<out T> { data object INI...
继续阅读 »

各位好,本人是练习kt时长一年的准练习生,接下来我将用一种我认为还行的方式封装网络请求,如有雷点,请各位佬轻喷
首先,定义一个请求结果类


sealed class RequestResult<out T> {
data object INIT : RequestResult<Nothing>()
data object LOADING : RequestResult<Nothing>()
data class Success<out T>(val data: T) : RequestResult<T>()
data class Error(val errorCode: Int = -1, val errorMsg: String? = "") : RequestResult<Nothing>()
}

接下来,定义Retrofit的service,由于我个人的极简主义,特别讨厌复制粘贴,所以我做了一个非常大胆的决定


interface SimpleService {
//目前我们只关注这两方法
@GET
suspend fun commonGet(@Url url: String, @QueryMap param: HashMap<String, Any>): ApiResponse<Any>
//目前我们只关注这两方法
@POST
suspend fun commonPost(@Url url: String, @Body param: HashMap<String, Any>): ApiResponse<Any>

@GET
suspend fun commonGetList(@Url url: String, @QueryMap param: HashMap<String, Any>): ApiListData<Any>

@POST
suspend fun commonPostList(@Url url: String, @Body param: HashMap<String, Any>): ApiListData<Any>

@GET
suspend fun commonGetPageList(@Url url: String, @QueryMap param: HashMap<String, Any>): ApiPageData<Any>

@POST
suspend fun commonPostPageList(@Url url: String, @Body param: HashMap<String, Any>): ApiPageData<Any>
}

and在apiManager中生成这个service


object BaseApiManager {
val simpleService by lazy<SimpleService> {
getService()
}

接下来我定义了一个RequestParam类来帮助收敛请求需要的参数


@Keep
data class RequestParam<T>(
val clazz: Class<T>? = null,
val url: String,
val isGet: Boolean = true,
val paramBuilder: (HashMap<String, Any>.() -> Unit)? = null
){
val param: HashMap<String, Any>
get() {
val value = hashMapOf<String, Any>()
paramBuilder?.invoke(value)
return value
}
}

再然后便是请求真正发出的地方


internal fun <T> commonRequest(
param: RequestParam<T>,
builder: ((T) -> Unit)? = null
)
= flow {
emit(RequestResult.LOADING)
Timber.d(param.param.toString())
runCatching {
if (param.isGet) {
BaseApiManager.simpleService.commonGet(param.url, param.param)
} else {
BaseApiManager.simpleService.commonPost(param.url, param.param)
}
}.onSuccess {
if (it.code != StatusCode.REQUEST_SUCCESS) {
emit(RequestResult.Error(it.code, it.message))
} else {
val gson = Gson()
val data = gson.fromJson(gson.toJson(it.data), param.clazz)
builder?.invoke(data)
emit(RequestResult.Success(data))
}
}.onFailure {
emit(RequestResult.Error(StatusCode.REQUEST_FAILED, it.message))
}
}.flowOn(Dispatchers.IO)

在经过上述封装后,此时我在vm中发出一个网络请求就变成


viewModelScope.launch {
commonRequest(
RequestParam(
XXXBean::class.java, //数据类class
"/xxx/xxx/xxx", //地址
false //是否get
) {
put("xxx", 11)
put("xxxx", "25")
}
).collect {
when(it) {
RequestResult.INIT -> {
假设这边是Init弹窗
}
RequestResult.LOADING -> {
关闭Init弹窗
假设这边是Loading弹窗
}
is RequestResult.Error -> {
关闭Loading弹窗
toast(it.errorMsg)
}

is RequestResult.Success -> {
关闭Loading弹窗
发送成功事件或者改变UI状态
}
}
}

那么这边会遇到一个有点烦人的事情,实际上


RequestResult.INIT -> {
假设这边是Init弹窗
}
RequestResult.LOADING -> {
关闭Init弹窗
假设这边是Loading弹窗
}
is RequestResult.Error -> {
关闭Loading弹窗
toast(it.errorMsg)
}

这三兄弟中,我们经常会做一些重复的操作,于是我略施小计,将这几个行为定义成CommonEffect


sealed class MVICommonEffect {
data object ShowLoading: MVICommonEffect()
data object DismissLoading: MVICommonEffect()
data class ShowToast(val msg: String?): MVICommonEffect()
}

同时将Flow<RequestResult>的订阅步骤拆开,由于kt中两个隐式this对象写起来很繁琐,所以我是把这一串代码放到baseiewModel中的


fun <T> Flow<RequestResult<T>>.onInit(initBlock: suspend () -> Unit): Flow<RequestResult<T>> {
return onEach {
if (it == RequestResult.INIT) {
initBlock.invoke()
}
}
}

fun <T> Flow<RequestResult<T>>.onLoading(
showLoading: Boolean = true,
loadingBlock: suspend () -> Unit
)
: Flow<RequestResult<T>> {
return onEach {
if (it == RequestResult.LOADING) {
if (showLoading) {
emitLoadingEffect()
}
loadingBlock.invoke()
}
}
}

fun <T> Flow<RequestResult<T>>.onSuccess(
dismissLoading: Boolean = true,
successBlock: suspend ((data: T) -> Unit)
)
: Flow<RequestResult<T>> {
return onEach {
if (it is RequestResult.Success) {
if (dismissLoading) {
emitDismissLoadingEffect()
}
successBlock.invoke(it.data)
}
}
}

fun <T> Flow<RequestResult<T>>.onError(
dismissLoading: Boolean = true,
showToast: Boolean = true,
errorBlock: suspend (code: Int, msg: String?) -> Unit
)
: Flow<RequestResult<T>> {
return onEach {
if (it is RequestResult.Error) {
if (dismissLoading) {
emitDismissLoadingEffect()
}
if (showToast) {
emitToastEffect(it.errorMsg)
}
errorBlock.invoke(it.errorCode, it.errorMsg)
}
}
}

fun <T> Flow<RequestResult<T>>.onCommonSuccess(
loadingInvoke: Boolean,
showToast: Boolean,
successBlock: suspend ((data: T) -> Unit)
)
= this.onInit().onLoading(loadingInvoke)
.onError(
dismissLoading = loadingInvoke,
showToast = showToast
).onSuccess(
dismissLoading = loadingInvoke
) {
successBlock.invoke(it)
}

private val _commonEffect = MutableSharedFlow<MVICommonEffect>()
override val commonEffect: SharedFlow<MVICommonEffect> by lazy {
_commonEffect.asSharedFlow()
}

override suspend fun emitLoadingEffect() {
_commonEffect.emit(MVICommonEffect.ShowLoading)
}

override suspend fun emitDismissLoadingEffect() {
_commonEffect.emit(MVICommonEffect.DismissLoading)
}

override suspend fun emitToastEffect(msg: String?) {
_commonEffect.emit(MVICommonEffect.ShowToast(msg))
}

那么接下来,vm中网络请求就可以用一种很赏心悦目的方式出现了


private fun requestTestData(): Flow<RequestResult<XXXBean>> {
return commonRequest(
RequestParam(
XXXBean::class.java,
"xxx"
)
)
}

private fun updateTestData() {
requestData().onInit().onLoading().onError().onSuccess {
Timber.d(it.toString)
}.launchIn(viewModelScope)
}

接下来,只需要在基类View中订阅上述的MVICommonEffect,就可以handle大部分情况下的loading,toast.


由于本人能力有限,不足之处还望大佬指正.


作者:伟大的小炮队长
来源:juejin.cn/post/7368758932154843188
收起阅读 »

GPT-4o,遥遥领先,作为前端人的一些思考

大家好,我是LV。 我早上一般起的比较早~ 大概6点左右就起来刷各种AI资讯。 但是今天,5点左右就起来了,迫不及待想看 OpenAI 发布的内容~ 也顺便写篇文章跟大家分享一下最新的资讯~ 以及作为前端人的一些思考~ 希望对你有所帮助~ 欢迎加入最懂AI的...
继续阅读 »

大家好,我是LV。


我早上一般起的比较早~ 大概6点左右就起来刷各种AI资讯。


但是今天,5点左右就起来了,迫不及待想看 OpenAI 发布的内容~



也顺便写篇文章跟大家分享一下最新的资讯~


以及作为前端人的一些思考~


希望对你有所帮助~


欢迎加入最懂AI的前端伙伴们~群,一起探讨AI赋能前端研发。


GPT-4o



  • 结合文本、图像、视频、语音的全能模型

  • 可以通过语音交互以及具备识别物体和基于视觉信息进行快速回答的功能

  • 性能上,GPT-4o达到了GPT-4 Turbo水平

  • 成本相比GPT-4-turbo砍一半,速度快一倍,响应时间最低232毫秒,平均320毫秒。遥遥领先!

  • 将为 macOS 操作系统设计桌面ChatGPT应用程序,无缝集成到 macOs 中,可以使用键盘快捷键查询问题并与 ChatGPT 进行截图讨论或直接开展声音/视频对话。


以上详见:openai.com/index/hello…


前端人的思考


成本砍半,速度加倍


做应用层的前端er,可以换新的 API Model 了,虽然价格没有 3.5 那么便宜,也算是GPT4自由了(我也赶紧给LV0给换上)。


音视频支持



  • 通过视频连线ChatGPT,实时辅助修bug


之前只能够通过将bug转换为文字或者图片再给到AI,有了音视频功能,直接可以连线 ChatGPT,让GPT实时给你debug。



  • 通过视频连线ChatGPT,辅助编码,相当于请了一个24在线的编程导师~

  • 通过视频的形式给AI一些UI交互上的信息,从截图生代码 ==> 原型交互生代码(离AGI Code又近了一步)


跟macOS的结合


在vscode、在网页、在控制台、在Codding的任何地方,有问题,就会有答案。(作为mac粉,着实期待了~)


其他思考


作为AI应用研发的创业者角色,有几点思考~


OpenAI的这一波更新带来了新的机遇:


例如在教育领域、情感陪伴服务以及同声传译服务:



  • 语音增加了情绪理解和有感情的回复,老人或者残疾人士陪伴

  • 手机能够实时解析摄像头捕获的视频画面,并提供指导,这种能力有潜力取代家庭教师的角色

  • 同时进行翻译(即同传)的工作可以由此技术执行,从而有可能替代专业的同声传译人员


不过,这波更新也破灭了多少创业者正在做的事情~ 比如:


智能眼镜,给视疾人士提供出行便捷(我前几天还看到有人在花大力气自研这项技术,现在升级一下模型或许就能很低门槛接入了~)


Sam Altman 很早在斯坦福大学举办的一个演讲中预示:GPT-5和GPT-6将极大超越GPT-4,警示创业者考虑AI未来发展,创业不要要专注于解决当前AI的局限性问题。


简单来说:别做跟官方做技术竞争,比如:花大量时间通过各种布丁来拓展AI的上下文能力,降低迷惑性。


至于要做啥,从稳健的角度来看,不要轻易涉足一个未知的领域,建议基于熟悉的业务场景聚焦来做AI赋能。把现有你熟悉的业务场景梳理出来,尝试用AI结合进去,AI赋能现有的业务流程,让现有的业务跑起来效能更高或者门槛更低。


比如:我很熟悉前端研发领域,那我会深度聚焦AI赋能前端研发,拆解研发中的各个环节步骤,不断尝试AI赋能各个步骤,提升现有的研发效能,降低研发门槛,再把这些经验抽象产品化。


聚焦细分业务,保持敏锐度,将最新的AI技术快速结合到业务中去。


作者:LV技术派
来源:juejin.cn/post/7368421137917788198
收起阅读 »

在线人数统计功能怎么实现?

一、前言 大家好!我是sum墨,一个一线的底层码农,平时喜欢研究和思考一些技术相关的问题并整理成文,限于本人水平,如果文章和代码有表述不当之处,还请不吝赐教。 在线人数统计这个功能相信大家一眼就明白是啥,这个功能不难做,实现的方式也很多,这里说一下我常使用的方...
继续阅读 »

一、前言


大家好!我是sum墨,一个一线的底层码农,平时喜欢研究和思考一些技术相关的问题并整理成文,限于本人水平,如果文章和代码有表述不当之处,还请不吝赐教。


在线人数统计这个功能相信大家一眼就明白是啥,这个功能不难做,实现的方式也很多,这里说一下我常使用的方式:使用Redis的有序集合(zset)实现。
核心方法是这四个:zaddzrangeByScorezremrangeByScorezrem


二、实现步骤


1. 如何认定用户是否在线?


认定用户在线的条件一般跟网站有关,如果网站需要登录才能进入,那么这种网站就是根据用户的token令牌有效性判断是否在线;
如果网站是公开的,是那种不需要登录就可以浏览的,那么这种网站一般就需要自定一个规则来识别用户,也有很多方式实现如IPdeviceId浏览器指纹,推荐使用浏览器指纹的方式实现。


浏览器指纹可能包括以下信息的组合:用户代理字符串 (User-Agent string)、HTTP请求头信息、屏幕分辨率和颜色深度、时区和语言设置、浏览器插件详情等。现成的JavaScript库,像 FingerprintJSClientJS,可以帮助简化这个过程,因为它们已经实现了收集上述信息并生成唯一标识的算法。


使用起来也很简单,如下:


// 安装:npm install @fingerprintjs/fingerprintjs

// 使用示例:
import FingerprintJS from '@fingerprintjs/fingerprintjs';

// 初始化指纹JS Library
FingerprintJS.load().then(fp => {
// 获取访客ID
fp.get().then(result => {
const visitorId = result.visitorId;
console.log(visitorId);
});
});


这样就可以获取一个访问公开网站的用户的唯一ID了,当用户访问网站的时候,将这个ID放到访问链接的Cookie或者header中传到后台,后端服务根据这个ID标示用户。


2. zadd命令添加在线用户


(1)zadd命令介绍
zadd命令有三个参数



key:有序集合的名称。
score1、score2 等:分数值,可以是整数值或双精度浮点数。
member1、member2 等:要添加到有序集合的成员。
例子:向名为 myzset 的有序集合中添加一个成员:ZADD myzset 1 "one"



(2)添加在线用户标识到有序集合中


// expireTime给用户令牌设置了一个过期时间
LocalDateTime expireTime = LocalDateTime.now().plusSeconds(expireTimeout);
String expireTimeStr = DateUtil.formatFullTime(expireTime);
// 添加用户token到有序集合中
redisService.zadd("user.active", Double.parseDouble(expireTimeStr), userToken);


由于一个用户可能户会重复登录,这就导致userToken也会重复,但为了不重复计算这个用户的访问次数,zadd命令的第二个参数很好的解决了这个问题。
我这里的逻辑是:每次添加一个在线用户时,利用当前时间加上过期时间计算出一个分数,可以有效保证当前用户只会存在一个最新的登录态。



3. zrangeByScore命令查询在线人数


(1)zrangeByScore命令介绍



key:指定的有序集合的名字。
min 和 max:定义了查询的分数范围,也可以是 -inf 和 +inf(分别表示“负无穷大”和“正无穷大”)。
例子:查询分数在 1 到 3之间的所有成员:ZRANGEBYSCORE myzset 1 3



(2)查询当前所有的在线用户


// 获取当前的日期
String now = DateUtil.formatFullTime(LocalDateTime.now());
// 查询当前日期到"+inf"之间所有的用户
Set userOnlineStringSet = redisService.zrangeByScore("user.active", now, "+inf");


利用zrangeByScore方法可以查询这个有序集合指定范围内的用户,这个userOnlineStringSet也就是在线用户集,它的size就是在线人数了。



4. zremrangeByScore命令定时清除在线用户


(1)zremrangeByScore命令介绍



key:指定的有序集合的名字。
min 和 max:定义了查询的分数范围,也可以是 -inf 和 +inf(分别表示“负无穷大”和“正无穷大”)。
例子:删除分数在 1 到 3之间的所有成员:ZREMRANGEBYSCORE myzset 1 3



(2)定时清除在线用户


// 获取当前的日期
String now = DateUtil.formatFullTime(LocalDateTime.now());
// 清除当前日期到"-inf"之间所有的用户
redisService.zremrangeByScore(""user.active"","-inf", now);


由于有序集合不会自动清理下线的用户,所以这里我们需要写一个定时任务去定时删除下线的用户。



5. zrem命令用户退出登录时删除成员


(1)zrem命令介绍



key:指定的有序集合的名字。
members:需要删除的成员
例子:删除名为xxx的成员:ZREM myzset "xxx"



(2)定时清除在线用户


// 删除名为xxx的成员
redisService.zrem("user.active", "xxx");


删除 zset中的记录,确保主动退出的用户下线。



三、小结一下


这种方案的核心逻辑就是,创建一个在线用户身份集合为key,利用用户身份为member,利用过期时间为score,然后对这个集合进行增删改查,实现起来还是比较巧妙和简单的,大家有兴趣可以试试看。


作者:summo
来源:juejin.cn/post/7356065093060427816
收起阅读 »

汉文帝刘恒:权谋高手,带你看中式管理

前言 这里我打算先讲两个例子,让大家感受一下中式管理以及里面的运作规律。日常生活中,我们接触的都是表象,也就是最外层的具象,而里面的结构以及组成大部分人是没有太多去深入理解的。 汉文帝刘恒 谈到这个大家都会想起汉朝的文景之治,采用无为而治,少插手百姓生活,通...
继续阅读 »

efb27f6fa8439656d5ba22473411d951.jpeg


前言




这里我打算先讲两个例子,让大家感受一下中式管理以及里面的运作规律。日常生活中,我们接触的都是表象,也就是最外层的具象,而里面的结构以及组成大部分人是没有太多去深入理解的。


汉文帝刘恒


谈到这个大家都会想起汉朝的文景之治,采用无为而治,少插手百姓生活,通过这种市场经济的方式恢复民生。然而大部分人不了解的是他还是一个权谋高手,这还得从刘邦说起。


刘邦的势力是由功臣集团,比如说英布、彭越、韩信..,以及吕氏集团,吕后、樊哙、吕产..,刘氏集团,也就是刘邦的家族体系构成,刘邦称帝之后扶持吕氏势力来清洗异姓诸侯,当他发现吕氏力量非常强大的时候,就扶持戚夫人来平衡吕氏,没想到刘邦已经比较年迈了,无法像汉武帝扶持卫青、霍去病一样来壮大自己的力量,所以当吕后掌权之后对戚夫人干掉,扶持自家的吕氏上来,这个时候功臣集团跟他们有冲突,这里面的陈平、周勃以及刘氏集团依据刘邦最后留下的白马之盟,联手干掉吕氏,这时需要另立领导者。


因为这些功臣集团目的是为了稳固权力,又不能背锅,所以需要找一个势力比较弱小的,好拿捏的上来话事,所以刘恒上场了。


那么他做的几件事:


1、通过旧部收回御林军的权限,去清理异己,功臣集团不想背锅,只能乖乖的交出军权


2、安抚平反的人,论功行赏


3、分化内部,提了陈平,压了周勃


我们可以看到刘恒是一个权谋高手,本来是作为一个被控制人的角色出场的,通过几个关键动作最后稳住了自己的脚跟。


汉代丞相陈平


陈平是刘邦那会儿跟着他的,六处谋划为刘邦出了很大力,比如说真假汉王,为刘邦脱险;离间范增,使得项羽失去一个重要的谋士。在汉文帝的时候就被任命为丞相,他就问陈平你知道丞相是干嘛的吗?


陈平:“丞相向上是调理天子的气息,向下管理百官,对外监视诸侯,对内管好百姓”,看懂了吗,这就是位置决定职责,反观周勃他是军事人才,所以在此次之后自己辞职了。


《年会不能停》领导要领


当大鹏的来历被揭穿之后,有个领导跟他支招,想要当领导也不能就记住几点:第一不要明确自己的意思,第二会用感情牌,第三懂得分化。


上面的三个例子,大家是否对中式管理有了一个初步的认识呢?下面我将讲讲我对中式管理的认识。


power


定义




权力跟资本是同一个代名词,就是资源分配权,因为资源有限,那么就需要对它进行一个合理的分配。而权力跟资本也有不同。


权力是需要大量的铺垫,比如说乡里老人组,有很高的威望,它需要前期大量的文化铺垫的,它是一个长期有效的方式。


资本是需要权衡利弊的方式去谈判,因为资本是趋利的,跟人性一样,所以前期沟通成本很大,但是一旦达成很快执行很顺利。


构成




以古代皇权为例,下面有文官、外戚、太监,一直讲的集中中心力量,不是说皇权特别强,而是它需要支持者来巩固位置,所以一般是结合刚刚三方中的某一方来强化。


比如说刘邦,一开始借助吕氏势力干掉诸侯,然后扶持自己的外戚势力戚夫人,最后还定了白马之盟,给了功臣集团和刘氏集团正当的理由来处理过于强大的吕氏。


在公司里面也一样,ceo下面肯定需要掌握自己的核心部门的权限,来支持他执行自己的目标,并不是说这个公司是他的他想怎么弄就怎么弄,因为里面还有很多带资入组的大佬。


动作




中心力量 + 支持一方势力 => 打压其他势力


具体方式:洗牌、分化,在《年会不能停》这个电影里面讲到了公司效益不行,要进行裁员广进计划,人力部的权限就很高,从而干掉其他势力的人。至于分化,在前言的刘恒那里用到了,某个体系内部也不是说大家都是一致的,一旦有利益冲突,或者分配不均就会分化。


从上面的构成很容易理解这个动作的产生意义,这就是我们常说的内斗,我们平时很难理解为什么大家不干点正事,天天在那里斗哈哈,这就要谈到归属。


归属




之前写过一篇文章介绍过,就是管理权限是有归属的,如果说这个公司非常大,但是不是你的,跟你没有一点关系对不对。


这就是上面讲的内斗存在的意义,争取资源、机会,最终实现权力的扩大。你说资源重要吧,其实并不是,而是这种资源分配权更重要。


中式管理




上层管理


我们从陈平对宰相的理解可以知道,就是管理队伍、定好方向、管理资源(收益、风险),他还漏了一个权力斗争,这个肯定不能直说哈哈哈。


我们再回头看看《年会不能停》领导要领


1、不要明确自己的意思


这就很传统了,为什么规矩都是模棱两可的呢,如果规矩说的很清楚,还需要你干什么?这是第一点,第二点他可以再次被解读,而不是明确拍板,这样有锅也是下面干活的人出错。


2、分化,转移矛盾


这个典型的手段,当大家干的天昏地暗的时候,那么你的位置就很稳固,大家不会把矛盾指向你头上。


所以这一部分的管理核心技能是管理好团队,文化建设(权力形式、资本形式),管理好目标,管理好资源。


中下层管理


我认为这些是核心的业务主力,也就是攻城略地的大头兵。这一层他是上一层的弱化,应该更加偏向业务那块,比如说跨部门资源调用,团队工作计划制定,合理利用资源。


比如说韩信的十面埋伏,在打大战的时候你对团队的了解有多少,你对整个战场了解有多少,你的计划是怎样的。


管理的认知


1、看定位


每个等级它的要求不一样的,底层的大头兵更多是做事的技能、态度,因为要攻城略地,对于中下层管理,对局部的战况要有自己的把控。


2、职责


基于上面的定位,我们可以得出这个定位下面的职责。


3、管理:权力、资源、目标、文化(情绪)


这里涉及的知识面太多了,就不再展开了。


总结




有时我们看不懂为啥公司内部一直内斗,还有业务干的一团糟,看了它的定义、归属就会有一个大致的认知,正是因为我们对内部的构造没有比较深入的了解,以底层的大头兵角度就会觉得这是内耗的情况。


这种管理很大程度跟文化有关系,也就是几千年来演变的规律形成的习惯在影响我们现代管理模式,它很简单,就是管理资源、方向,它也很复杂,单纯一个方面拎出来都是一个很大的知识面。


作者:大鸡腿同学
来源:juejin.cn/post/7329100659877494796
收起阅读 »

刘邦-中年痞子到霸道总裁的一生

前言 最近花了9块钱开通会员,就为了读下《汉高祖刘邦》这本书,一直以来我认为社会阅历是人跟人之间的差距,这种属于后天的积累,当你在社会实践的时候过于单一,或者说接触面更少的时候,应该读读别人的传记。 刘邦有几个比较有意思的点,首先他有天选之子的面相、骨相,当...
继续阅读 »

前言




最近花了9块钱开通会员,就为了读下《汉高祖刘邦》这本书,一直以来我认为社会阅历是人跟人之间的差距,这种属于后天的积累,当你在社会实践的时候过于单一,或者说接触面更少的时候,应该读读别人的传记。


刘邦有几个比较有意思的点,首先他有天选之子的面相、骨相,当然我认识这些真正目的有几个,一个是人需要借助名头、声望来发展自身实力一样道理,另一个是权力正统性,能够说服别人,这个相当重要。其次我觉得很奇怪,他的能力可以跟他的岗位匹配上,从一个庭长到一个君主,这里面要求的能力是不一样的,他为啥能具备这方面的能力的转变呢?


下面我们就一一展开刘邦的一生历程,以及我读后的感受。


各个派系对比




背景


在秦朝末期,因为律法严苛,导致民愤,各个势力崛起,非常经典的说法;其中就有刘邦、项羽、还有很出名的陈胜吴广。


ps:当你看秦朝发家的时候你就会清楚,它是如何变成一个战争机器的,工作的细化,把种田的锁死在种田上,打战的打战,定下军功授于的规则。就像《共产宣言》里面讲的,当工作细分化之后,人会更加专业化,效率更高,同时工作量更大,更加劳累。当秦统一六国之后,推行同样的机制,而且推行郡县制,势必遭到各个传统门阀的抵抗的,也不一定适用其他地方。


你觉得的律法严苛,实际上是人家的发家史,只不过无法推广,以及郡县制的影响太大。


派系发展历程


1、陈胜吴广


相比另外两股力量,显得不太起眼,因为结束的比较早,我觉得跟资源有关系,刘邦自己一开始就是亭长,吕氏家底,项羽门阀势力,反观这个陈吴资源是利益临时凑在一起,另外能力上军事、管理都不突出。


2、刘邦


他是在40岁之后才开始走大运,他之前更像一个痞子的作风,为人豪爽,喜欢喝酒结交朋友,然后做了亭长,认识樊哙、曹参、夏侯婴这些铁子,跟当地的大户吕氏有结交,期间认识萧何,后来在沛县反叛,在发展过程中认识张良,通过张良结识其他人,比如项伯,也就是鸿门宴上解围的那哥们;本身刘邦势力比较薄弱,需要借助多方势力,比如敌方将领,项羽部下策反了英布,韩信原本也是项羽帐前持戟郎中,另外借助彭越对抗项羽;最后在韩信十面埋伏,还有张良的四面楚歌下将项羽击败。


这是一统之前的历程,后面开始削韩信,平异姓王,白马之盟,完成了从一个痞子到霸道总裁的转变。


军事:


家底是吕氏 + 后面加入带资异姓王


权谋:


a、朋友多多,敌人少少


这对于只会武力的人来讲是降维打击,用了敌方的英布、韩信,助力了彭越其他势力,从而发展自己的势力。


b、驭将


舍得利益,比如封韩信齐王,后面多次封赏,当一统之后对各个出力的人给予套现机会。


c、权力纵横术


一统之后,韩信成为刘邦心中的刺,使用了狡猾的手段去除了韩信的兵权,然后软禁,平定各个异姓王,最后因为吕氏权力太大了,定下了白马之盟。


这个是陈胜吴广所无法具备的能力,以及项羽,对比起来项羽更像莽夫。


扩展一下:首先权力的正统性是很重要的,另外维持权力的力量必不可少,最后是权力纵横术。因为本身资源就是有限的,一定会内斗,其次只有在互相制衡的基础上,可以保证这种头头的稳固。


3、项羽


对比其他势力,他有很多优势,是一个门阀家族,家族里面有项伯、项梁,门下还有很多人追随,比如说英布、季布等悍将,韩信也是在下面干过一段时间,再加上项羽本身天生神力,所向披靡。打出了非常有名的巨鹿之战,破釜沉舟,大破秦军,因为跟刘邦的约定先进城先为王,听取范增意见设置了鸿门宴,后面刘邦封为汉王,同时也为压制刘邦势力让他去汉中地方,派出以前投降的秦降将去守,所以后来被韩信的暗渡陈仓偷袭了,还有后面背水一战,围攻刘邦于荥阳,差点把刘邦嘎了,但是后面的事上面也有提到被韩信、刘邦等围功,中了计谋,被反间了范增,还有十面埋伏这些,最终失败了。


军事:


项羽的家族很雄厚的,而且跟随的人才大有人在


权谋:


a、大力出奇迹


本身项羽的条件、资源确实比较叼,有点偏科感觉哈哈


b、不会管理,妇人之仁


首先他不注重谋士,比如韩信经常给他提意见,没有给下属发展空间。然后对于鸿门宴的时候,没有对刘邦下手,没有霸道总裁的雷厉风行的手段。


说白了,不是很好的管理者,从英布、韩信的出走看出,因为人家在你下面没有施展空间,其次你没有一个奖励机制来推进大家为你卖力。


idea




很重要一点,我们能从这些历史故事中学到点什么?



  • 做事


1、人脉、资源


我们看刘邦以前虽然说是个痞子,但是人家人脉真的广,一个是亭长的位置,认识一群铁子,结识了当地的大户吕氏,如果他后面没有壮大其实在沛县也是一个小霸王,做什么事都方便。


这就是打工人不具备的东西,圈子小,能力还不强。


2、德要配位


作为一个管理者,你是否具备业务、人才规划能力,以及利益合理分配,还有激励体系建设。就像韩信点兵,每个人都能来个2w人马,点个10几个副将,东南西北布阵,你的能力跟位置匹配的。


3、博弈能力


这个是非常难的,以前我们会陷入非黑即白,就是不是朋友就是敌人,我个人也很难逃脱这个认知。但是纵观优秀的战略家,可以权衡利害,这就是很难的。


比如说刘邦就很听劝,可能人家很生气,但是只要你足够说服力,他可以听你的。他可以接受敌方的英雄,可以接受跟匈奴联姻,他可以把他儿子踹下来躲避楚军追击,理智的逻辑胜于情绪。


如果你回头再看冯唐老师讲的,不要脸,不着急,不害怕,一个痞子更容易做出点成就,“前途光明,道路曲折”,每个有成就的人都会经历各种曲折,下面的乐观精神也是一种不害怕的表现。



  • 乐观的态度


他一直很乐观,从发家没有项羽叼,到多次差点被嘎,被项羽胖揍,刘邦挺乐观的。


这个非常重要,纵观现代年轻人,他们会觉得当前环境对他们比较难发展,就是没有资源、没有比较强的赚钱能力,所以采取收缩以求得自在生存,也会产生悲观的心态,这个本身是人性、本能的选择。但是当你有乐观精神,才有面对困难的勇气,也有了捕捉机会的欲望。



  • 陈胜吴广的失败


我们可以比喻成如何办理一场活动,首先需要人才对吧,布置活动现场,策划,干活,相比之下他们就是乌合之众,没有得力干将,没有杰出的管理人才,然后办活动需要资金对吧,有支持的粮食、金钱,他们并不代表某一方的势力可以稳定的输入资金来源,当你有了资金之后,队伍庞大之后,就有派系斗争,领导者有没有对应的权力纵横术、权衡利害能力。


作者:大鸡腿同学
来源:juejin.cn/post/7322723692745031690
收起阅读 »

需求小能手——拦截浏览器窗口关闭

web
前言 最近碰到一个需求,网页端页面有评价功能,要求用户点击关闭浏览器时强制弹出评价对话框让用户评价。刚听到这个需求我大意了没有闪,以为很简单,没想到很难实现,很多需求果然不能想当然啊,接下来我们来看一下该功能实现的一些思路。 窗口关闭 要想实现该功能最简单的想...
继续阅读 »

前言


最近碰到一个需求,网页端页面有评价功能,要求用户点击关闭浏览器时强制弹出评价对话框让用户评价。刚听到这个需求我大意了没有闪,以为很简单,没想到很难实现,很多需求果然不能想当然啊,接下来我们来看一下该功能实现的一些思路。


窗口关闭


要想实现该功能最简单的想法就是监听浏览器关闭事件,然后阻止默认事件,执行自定义的事件。整个思路核心就是监听事件,搜索一番果然有浏览器关闭触发的事件。


事件



  • onunload:资源被卸载时触发,浏览器窗口关闭时卸载资源就会触发,我们监听一下该事件看能不能阻止窗口关闭。


    window.addEventListener('unload', function (e) {
console.log(e);
e.preventDefault()
});

打开页面再关闭,会发现控制台打印出了e然后就关闭了,看来在onunload事件中并不能阻止窗口关闭,得另找方法,刚好在onunload事件介绍中还链接了一个事件——beforeonunlaod。



  • beforeunload :当窗口关闭或刷新时触发,该事件在onunload之前触发。并且在该事件中可以弹出对话框,询问用户是否确认离开或者重新加载,这不是正是我们想要的效果。根据mdn上的介绍,要想出现弹出对话看需要用preventDefault()事件,并且为了兼容性我们最好再加上以下方法中的一个:

    1.将e.renturenValue赋一个字符串。

    2.事件函数返回一个字符串。
    接下来让我们试一试:


    window.addEventListener('beforeunload', function (e) {
e.preventDefault()
e.returnValue = ''
});

打开关闭未生效,再检查下代码没问题呀,这是因为浏览器本身安全机制导致的,在ie浏览器中没有任何限制,但是在chrome、edge等浏览器中用户必须在短时间操作过页面才能触发。打开页面点几个文字在关闭窗口,这次就能出现弹窗了。

2(W_WV8AVWRT3(4R1HWBRR7.png

当我们点击离开页面就会关闭,点击取消继续停留,上面提到过刷线也能触发,我们再点下刷新。

T456)ZI7MJ2XK1X3M%BE7SN.png

出现的提示有所改变,我们知道浏览器的刷新有好几种方式,我们可以都尝试一下:



  • ctrl+R:本身就是浏览器刷新按钮的快捷键,能够触发。

  • f5:能否触发。

  • 前进、后退:能够触发。

    这三种方式提示内容跟点击刷新按钮一样。回到我们的需求,虽然已经能够阻止窗口关闭,但是刷新依旧能阻止,我们需求是用户关闭,所以我们要区分用户操作是刷新还是关闭。


区分


要想区分就要找到以下两者之间的区别,两者都会执行onbeforeunload与onunload两个事件,不能直接通过某个事件区分。但是两个事件之间的时间差是不同的。刷新时两者时间差在10毫秒左右,而关闭时在3毫秒左右,判断以下时间差就能区分出来。


       var time = null;
window.addEventListener('beforeunload', function (e) {
time = new Date().getTime();
});
window.addEventListener('unload', function (e) {
const nowTime = new Date().getTime();
if (nowTime - time < 5) {
console.log('窗口关闭');
}
});

用此方法就能区分出来,但是此判断是在onunload事件中的,而窗口弹出是在beforeunlaod,这方法只适用于在关闭时执行某个函数,但不能满足我们的需求。除此之外还有一个问题就是刷新默认弹出对话框的内容是不能修改的,所以如果我们想要弹出自定义的对话框是不可能的。经过分析操作能够做到的就是,在用户刷新或关闭时出现系统自带对话框,同时在下方弹出自定义对话框,然后用户点击取消再去操作自定义对话框。


总结


总的来说要想拦截浏览器窗口关闭并且弹出自定义对话框,目前我还没有完美的实现方案,只能带有众多缺陷的去实现。如果我们只是想在关闭窗口前执行函数那就使用时间差区分即可。


作者:躺平使者
来源:juejin.cn/post/7281912738862481448
收起阅读 »

工作两年以来,被磨圆滑了,心智有所成长……

刚毕业时候年轻气盛,和邻居组的老板吵了几句。后来我晋升时,发现他是评委…… 曾经的我多么嚣张,现在的我就多么低调。 一路走来,磕磕绊绊,几年来,我总结了工作上的思考…… 工作思考 有效控制情绪,在沟通时使用适当的表情包以传达善意。无论线上还是线下,都应避免争...
继续阅读 »

刚毕业时候年轻气盛,和邻居组的老板吵了几句。后来我晋升时,发现他是评委…… 曾经的我多么嚣张,现在的我就多么低调。


一路走来,磕磕绊绊,几年来,我总结了工作上的思考……


工作思考



  1. 有效控制情绪,在沟通时使用适当的表情包以传达善意。无论线上还是线下,都应避免争吵。只有和气相处,我们才能推动工作的进展。

  2. 在讨论具体问题之前,先进行一些预备性的交流。情绪应放在第一位,工作讨论放在第二位。如果对方情绪不好,最好选择另一个时间再进行讨论。

  3. 在与他人交流时要保持初学者的态度和需求,不要用技术去怼人。

  4. 进入新团队先提升自己在团队的业务能力,对整个系统有足够的了解,不要怕问问题和学习。不要新入职就想毁天灭地,指手画脚 ”这里的设计不合理,那里有性能瓶颈“。

  5. 在各个事情上,都要比别人多了解一点。对于关键的事情要精通,对于其他事情也要多花一点时间去投入。

  6. 遇到困难时,先自己思考和尝试解决,然后再请教他人。不要机械地提问,也不要埋头一直搞而不主动提问。但如果是新入职,可以例外,多提问总没有坏处,但要在思考的基础上提问。

  7. 当向他人求助时,首先要清晰地阐述自己正在面临的问题、目标、已尝试的方法以及所需要的帮助和紧迫程度。所有的方面都要有所涉及。在提问之前,最好加上一句是否可以帮忙,这样对解决问题是否有帮助更加明确。因为别

  8. 一定有时间来帮助你,即使有时间,你也不一定找对了人。

  9. 在明确软件产品要解决的业务问题之前,先了解自己负责的那部分与业务的对应关系。

  10. 主要核心问题一定要提前叙述清楚,不要等别人问

  11. 要始终坚持追踪事情的进展,与与自己有交互的队友讨论接口,并关注他们的进度,以确保协调一致。

  12. 要主动向队友述说自己的困难,在项目延期或遇到困难时,要主动求助同事或领导,是否能分配部分工作给其他人,不要全部自己承担。

  13. 如果预计任务需要延期,要提前告知领导。如果有进展,也要及时向领导汇报。

  14. 如果无法参加会议但是自己是会议的重要参与者,一定要提前告知领导自己的进度、计划和想法,最好以书面形式或电话告知。如果可以远程参加,可以选择电话参加。除非有极其重要的事情,务必参加会议。不要假设别人都知道你的进度和想法。

  15. 要少说话,多做事。在开会时,不要凭借想当然的想法,可以询问其他小组的细节,但不要妄自揣测别人的细节,以为自己是对的。否则会被批评。

  16. 程序员如果经验丰富,很容易产生自我感觉良好的情绪。要避免这种情况,我们必须使用自己没有使用过的东西,并进行充分的测试,这样才能减少问题的出现。要提前考虑好所有细节,不要认为没有问题就不加考虑。要给自己留出处理问题的时间,并及时反馈并寻求帮助。

  17. 当与他人交流时,要始终保持有始有终的态度,特别是当寻求他人帮助时,最后一定要确认OK。要胆大心细,不要害怕犯错,要有成果,要快速并提高效率,不择手段地追求快速,并对结果负责。工作一定要完成闭环,要记事情要好,记住重要的事情并使用备忘录记录待办事项。

  18. 每完成一个项目后,应该回顾一下使用了什么知识、技能和工具。要总结并记录下这些,并与之前积累的知识和技能进行关联。如果发生了错误,也要记录下来,并将经验进行总结。

  19. 每天早上先思考今天要做什么,列出1、2、3,然后每天晚上下班时回顾已完成的任务、未完成的任务以及遇到的问题。

  20. 如果有待办事项没有立即处理,一定要用工具记录下来,不要心存侥幸以为自己能记住。


代码编写和技术问题



  1. 在代码编写过程中要认真对待,对于代码审核之前,要自己好好检查,给人一种可靠的感觉。

  2. 对于代码审核,不要过于苛刻,要容忍个人的发挥。

  3. 在提交代码给测试之前,应该先自行进行测试验证通过。

  4. 如果接口没有做到幂等性,那就会给未来的人工运维增加困难。当数据存在多份副本时,例如容量信息和上下游同时存在的资源,需要评估数据不一致的可能性以及解决方法。可以考虑通过数据校准或严格的代码编写来保证最终的一致性,或者考虑只在一方保存数据或以一方的数据为准。一旦出现数据不一致,则以其中一方的数据为准,无需人为干预即可自动达到数据再次一致。

  5. 要学会横向和纵向分割隔离系统,明确系统的边界,这样可以更好地进行并发合作开发和运维,提高效率。各个子系统应该独立变化,新的设计要考虑向后兼容性和上下游兼容性问题,包括上线期间的新老版本兼容。在设计评审阶段就应该重视这些问题。

  6. 如果在代码审查中无法发现业务问题或代码风格问题,不妨重点关注日志的打印是否合理和是否存在bug。

  7. 在依赖某个服务或与其他服务共享时,要确认该服务是否要废弃、是否是系统的瓶颈,以及是否可以自己进行改造或寻找更优的提供者。

  8. 使用缓存时注意预热,以防止开始使用时大量的缓存未命中导致数据库负载过高。

  9. 在使用rpc和mq、共享数据库、轮询、进程间通信和服务间通信时,要根据情况做出选择,并注意不要产生依赖倒置。

  10. 在接口有任何变动时,务必通过书面和口头确认。在这方面,要多沟通,尽量详细,以避免出现严重问题!毕竟,软件系统非常复杂,上下游之间的理解难以保持一致。

  11. 尽可能使用批量接口,并考虑是否需要完全批量查询。当批量接口性能较差时,设置适当的最大数量,并考虑客户端支持将批量接口聚合查询。批量接口往往是tp99最高的接口。

  12. 对于系统重要设计和功能,要考虑降级预案,并加入一些开关来满足安全性和性能需求。

  13. 如果数据不一致,可以考虑对比两方的不一致数据并打印错误日志,例如es/db等。

  14. 在系统设计之前,要充分调研其他人的设计,了解背景和现状。

  15. 废弃的代码应立即删除,如果以后需要,可以从git中找回。如果实在不想删除,也要注释掉!特别是对外的rpc、http接口,不使用的要立即删除,保持代码简洁。接手项目的人不熟悉背景情况,很难判断这段废弃代码的意义,容易造成混乱和浪费时间。要努力将其和其他有效代码联系起来,但这很困难。

  16. 在代码中要有详尽的日志记录!但是必须有条理和规范,只打印关键部分。对于执行的定时任务,应该打印足够详细的统计结果。最好使用简洁明了的日志,只记录最少量但最详细的信息,反馈程序的执行路径。

  17. 如果接口调用失败或超时,应该如何处理?幂等和重试如何处理?


当你写下一行代码前



  1. 要明确这行代码可能出现的异常情况以及如何处理,是将异常隔离、忽略还是单独处理,以防遗漏某些异常。

  2. 需要确保该行代码的输入是否已进行校验,并考虑校验可能引发的异常。

  3. 需要思考由谁调用该代码,会涉及哪些上游调用,并确定向调用者提供什么样的预期结果。

  4. 需要确定是否调用了一个方法或接口,以及该调用是否会阻塞或是异步的,并考虑对性能的影响。

  5. 需要评估该行代码是否可以进行优化,是否可以复用。

  6. 如果该行代码是控制语句,考虑是否能简化控制流程是否扁平。

  7. 对于日志打印或与主要逻辑无关的输出或报警,是否需要多加关注,因为它们可能还是很重要的。

  8. 如果代码是set等方法,也要仔细检查,避免赋错属性。IDE可能会有误提示,因为属性名前缀类似,set方法容易赋值错误。


当你设计一个接口时



  1. 接口的语义应该足够明确,避免出现过于综合的上帝接口

  2. 如果语义不明确,需要明确上下游的期望和需求。有些需求可以选择不提供给上游调用。

  3. 对于接口超时的处理,可以考虑重试和幂等性。在创建和删除接口时要确定是否具有幂等性,同时,幂等后返回的数据是否和首次请求一致也需要考虑。

  4. 接口是否需要防止并发,以及是否成为性能瓶颈也需要考虑。

  5. 设计接口时要确保调用方能够完全理解,如果他对接口的理解有问题,就需要重新设计接口。这一点非常关键,可以通过邮件确认或者面对面交流来确保调用方理解得清楚。

  6. 在开发过程中,需要定期关注队友的开发进度,了解他们是否已经使用了接口以及是否遇到了问题。这个原则适用于所有的上下游和相关方,包括产品和测试人员。要想清楚如何对接口进行测试,并与测试人员明确交流。

  7. 最好自己整理好测试用例,不要盲目地指望测试人员能发现所有的bug。

  8. 需要考虑是否需要批量处理这个接口,以减少rpc请求的次数。但即使是批量处理,也要注意一次批处理最多处理多少条记录,不要一次性处理全部记录,避免由于网络阻塞或批量处理时间过长导致上游调用超时,需要适度控制批量处理的规模。


作者:五阳
来源:juejin.cn/post/7306025036656787475
收起阅读 »

前端实现文件预览img、docx、xlsx、ppt、pdf、md、txt、audio、video

web
前言 最近有接到一个需求,要求前端支持上传制定后缀文件,且支持页面预览,上传简单,那么预览该怎么实现呢,尤其是不同类型的文件预览方案,那么下面就我这个需求的实现,分不同情况来讲解一下👇 具体的预览需求: 预览需要支持的文件类型有: png、jpg、jpeg...
继续阅读 »

前言



最近有接到一个需求,要求前端支持上传制定后缀文件,且支持页面预览,上传简单,那么预览该怎么实现呢,尤其是不同类型的文件预览方案,那么下面就我这个需求的实现,分不同情况来讲解一下👇



具体的预览需求:
预览需要支持的文件类型有: png、jpg、jpeg、docx、xlsx、ppt、pdf、md、txt、audio、video,另外对于不同文档还需要有定位的功能。例如:pdf 定位到页码,txtmarkdown定位到文字并滚动到指定的位置,音视频定位到具体的时间等等。




⚠️ 补充: 我的需求是需要先将文件上传到后台,然后我拿到url地址去展示,对于markdowntxt的文件需要先用fetch获取,其他的展示则直接使用url链接就可以。


不同文件的实现方式不同,下面分类讲解,总共分为以下几类:



  1. 自有标签文件:png、jpg、jpeg、audio、video

  2. 纯文字的文件: markdown & txt

  3. office 类型的文件: docx、xlsx、ppt

  4. embed 引入文件:pdf

  5. iframe:引入外部完整的网站




自有标签文件:png、jpg、jpeg、audio、video



对于图片、音视频的预览,直接使用对应的标签即可,如下:



图片:png、jpg、jpeg


示例代码:


 <img src={url} key={docId} alt={name} width="100%" />;

预览效果如下:


截屏2024-04-30 11.18.01.png


音频:audio


示例代码:


<audio ref={audioRef} controls controlsList="nodownload" style={{ width: '100%' }}>
<track kind="captions" />
<source src={url} type="audio/mpeg" />
</audio>

预览效果如下:


截屏2024-04-30 11.18.45.png


视频:video


示例代码:


<video ref={videoRef} controls muted controlsList="nodownload" style={{ width: '100%' }}>
<track kind="captions" />
<source src={url} type="video/mp4" />
</video>

预览效果如下:


截屏2024-05-13 18.21.13.png


关于音视频的定位的完整代码:


import React, { useRef, useEffect } from 'react';

interface IProps {
type: 'audio' | 'video';
url: string;
timeInSeconds: number;
}

function AudioAndVideo(props: IProps) {
const { type, url, timeInSeconds } = props;
const videoRef = useRef<HTMLVideoElement>(null);
const audioRef = useRef<HTMLAudioElement>(null);

useEffect(() => {
// 音视频定位
const secondsTime = timeInSeconds / 1000;
if (type === 'audio' && audioRef.current) {
audioRef.current.currentTime = secondsTime;
}
if (type === 'video' && videoRef.current) {
videoRef.current.currentTime = secondsTime;
}
}, [type, timeInSeconds]);

return (
<div>
{type === 'audio' ? (
<audio ref={audioRef} controls controlsList="nodownload" style={{ width: '100%' }}>
<track kind="captions" />
<source src={url} type="audio/mpeg" />
</audio>
) : (
<video ref={videoRef} controls muted controlsList="nodownload" style={{ width: '100%' }}>
<track kind="captions" />
<source src={url} type="video/mp4" />
</video>
)}
</div>
);
}

export default AudioAndVideo;



纯文字的文件: markdown & txt



对于markdown、txt类型的文件,如果拿到的是文件的url的话,则无法直接显示,需要请求到内容,再进行展示。



markdown 文件



在展示markdown文件时,需要满足字体高亮、代码高亮、如果有字体高亮,需要滚动到字体所在位置、如果有外部链接,需要新开tab页面再打开。



需要引入两个库:


marked:它的作用是将markdown文本转换(解析)为HTML


highlight: 它允许开发者在网页上高亮显示代码。


字体高亮的代码实现:



高亮的样式,可以在行间样式定义



  const highlightAndMarkFirst = (text: string, highlightText: string) => {
let firstMatchDone = false;
const regex = new RegExp(`(${highlightText})`, 'gi');
return text.replace(regex, (match) => {
if (!firstMatchDone) {
firstMatchDone = true;
return `<span id='first-match' style="color: red;">${match}</span>`;
}
return `<span style="color: red;">${match}</span>`;
});
};

代码高亮的代码实现:



需要借助hljs这个库进行转换



marked.use({
renderer: {
code(code, infostring) {
const validLang = !!(infostring && hljs.getLanguage(infostring));
const highlighted = validLang
? hljs.highlight(code, { language: infostring, ignoreIllegals: true }).value
: code;
return `<pre><code class="hljs ${infostring}">${highlighted}</code></pre>`;
}
},
});

链接跳转新tab页的代码实现:


marked.use({
renderer: {
// 链接跳转
link(href, title, text) {
const isExternal = !href.startsWith('/') && !href.startsWith('#');
if (isExternal) {
return `<a href="${href}" title="${title}" target="_blank" rel="noopener noreferrer">${text}</a>`;
}
return `<a href="${href}" title="${title}">${text}</a>`;
},
},
});

滚动到高亮的位置的代码实现:



需要配合上面的代码高亮的方法



const firstMatchElement = document.getElementById('first-match');
if (firstMatchElement) {
firstMatchElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
}

完整的代码如下:



入参的docUrlmarkdown文件的线上url地址,searchText 是需要高亮的内容。



import React, { useEffect, useState, useRef } from 'react';
import { marked } from 'marked';
import hljs from 'highlight.js';

const preStyle = {
width: '100%',
maxHeight: '64vh',
minHeight: '64vh',
overflow: 'auto',
};

// Markdown展示组件
function MarkdownViewer({ docUrl, searchText }: { docUrl: string; searchText: string }) {
const [markdown, setMarkdown] = useState('');
const markdownRef = useRef<HTMLDivElement | null>(null);

const highlightAndMarkFirst = (text: string, highlightText: string) => {
let firstMatchDone = false;
const regex = new RegExp(`(${highlightText})`, 'gi');
return text.replace(regex, (match) => {
if (!firstMatchDone) {
firstMatchDone = true;
return `<span id='first-match' style="color: red;">${match}</span>`;
}
return `<span style="color: red;">${match}</span>`;
});
};

useEffect(() => {
// 如果没有搜索内容,直接加载原始Markdown文本
fetch(docUrl)
.then((response) => response.text())
.then((text) => {
const highlightedText = searchText ? highlightAndMarkFirst(text, searchText) : text;
setMarkdown(highlightedText);
})
.catch((error) => console.error('加载Markdown文件失败:', error));
}, [searchText, docUrl]);

useEffect(() => {
if (markdownRef.current) {
// 支持代码高亮
marked.use({
renderer: {
code(code, infostring) {
const validLang = !!(infostring && hljs.getLanguage(infostring));
const highlighted = validLang
? hljs.highlight(code, { language: infostring, ignoreIllegals: true }).value
: code;
return `<pre><code class="hljs ${infostring}">${highlighted}</code></pre>`;
},
// 链接跳转
link(href, title, text) {
const isExternal = !href.startsWith('/') && !href.startsWith('#');
if (isExternal) {
return `<a href="${href}" title="${title}" target="_blank" rel="noopener noreferrer">${text}</a>`;
}
return `<a href="${href}" title="${title}">${text}</a>`;
},
},
});
const htmlContent = marked.parse(markdown);
markdownRef.current!.innerHTML = htmlContent as string;
// 当markdown更新后,检查是否需要滚动到高亮位置
const firstMatchElement = document.getElementById('first-match');
if (firstMatchElement) {
firstMatchElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}
}, [markdown]);

return (
<div style={preStyle}>
<div ref={markdownRef} />
</div>

);
}

export default MarkdownViewer;

预览效果如下:


截屏2024-05-13 17.59.04.png


txt 文件预览展示



支持高亮和滚动到指定位置



支持高亮的代码:


  function highlightText(text: string) {
if (!searchText.trim()) return text;
const regex = new RegExp(`(${searchText})`, 'gi');
return text.replace(regex, `<span style="color: red">$1</span>`);
}

完整代码:


import React, { useEffect, useState, useRef } from 'react';
import { preStyle } from './config';

function TextFileViewer({ docurl, searchText }: { docurl: string; searchText: string }) {
const [paragraphs, setParagraphs] = useState<string[]>([]);
const targetRef = useRef<HTMLDivElement | null>(null);

function highlightText(text: string) {
if (!searchText.trim()) return text;
const regex = new RegExp(`(${searchText})`, 'gi');
return text.replace(regex, `<span style="color: red">$1</span>`);
}

useEffect(() => {
fetch(docurl)
.then((response) => response.text())
.then((text) => {
const highlightedText = highlightText(text);
const paras = highlightedText
.split('\n')
.map((para) => para.trim())
.filter((para) => para);
setParagraphs(paras);
})
.catch((error) => {
console.error('加载文本文件出错:', error);
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [docurl, searchText]);

useEffect(() => {
// 处理高亮段落的滚动逻辑
const timer = setTimeout(() => {
if (targetRef.current) {
targetRef.current.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}, 100);

return () => clearTimeout(timer);
}, [paragraphs]);

return (
<div style={preStyle}>
{paragraphs.map((para: string, index: number) => {
const paraKey = para + index;

// 确定这个段落是否包含高亮文本
const isTarget = para.includes(`>${searchText}<`);
return (
<p key={paraKey} ref={isTarget && !targetRef.current ? targetRef : null}>
<div dangerouslySetInnerHTML={{ __html: para }} />
</p>
);
})}
</div>

);
}

export default TextFileViewer;

预览效果如下:


截屏2024-05-13 18.34.27.png




office 类型的文件: docx、xlsx、ppt



docx、xlsx、ppt 文件的预览,用的是office的线上预览链接 + 我们文件的线上url即可。




关于定位:用这种方法我暂时尝试是无法定位页码的,所以定位的功能我采取的是后端将office 文件转成pdf,再进行定位,如果只是纯展示,忽略这个问题即可。



示例代码:


<iframe
src={`https://view.officeapps.live.com/op/view.aspx?src=${url}`}
width="100%"
height="500px"
frameBorder="0"
></iframe>

预览效果如下:


截屏2024-05-07 17.58.45.png




embed 引入文件:pdf



pdf文档预览时,可以采用embed的方式,这个httpsUrl就是你的pdf文档的链接地址



示例代码:


 <embed src={`${httpsUrl}`} style={preStyle} key={`${httpsUrl}`} />;

关于定位,其实是地址上拼接的页码sourcePage,如下:


 const httpsUrl = sourcePage
? `${doc.url}#page=${sourcePage}`
: doc.url;

<embed src={`${httpsUrl}`} style={preStyle} key={`${httpsUrl}`} />;


预览效果如下:


截屏2024-05-07 17.50.07.png




iframe:引入外部完整的网站



除了上面的各种文件,我们还需要预览一些外部的网址,那就要用到iframe的方式



示例代码:


 <iframe
title="网址"
width="100%"
height="100%"
src={doc.url}
allow="microphone;camera;midi;encrypted-media;"/>


预览效果如下:


截屏2024-05-07 17.51.26.png




总结: 到这里我们支持的所有文件都讲述完了,有什么问题,欢迎评论区留言!


作者:玖月晴空
来源:juejin.cn/post/7366432628440924170
收起阅读 »

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

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

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


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


明枪暗箭防不胜防


背后捅刀子往往最不设防


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


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


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


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


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


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


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


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


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


1.实力在及格线以上


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


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


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


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


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


2.比较高的自尊水平


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


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


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


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


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


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


3.嘴严,可靠


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


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


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


4.随和,有分寸


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


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


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


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


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


5.懂得如何打扮


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


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


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


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


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


6.和优秀的人站在一起


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


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


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


7.知道如何求助


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


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


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


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


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


8.技巧地送出小恩小惠


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


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


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


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


9.良好的情绪管理能力


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


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


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


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


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


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


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


作者:程序员小高
来源:juejin.cn/post/7255589558996992059
收起阅读 »

假如互联网人都很懂冒犯

大家好,我是老三,最近沉迷于听脱口秀,并且疯狂安利同事。 脱口秀演员常常说的一句话是:“脱口秀是冒犯的艺术”。最近我发现,同事们好像有点不一样了。 阳光灿烂的早上,趿拉着我的宝马拖鞋,跨上包浆的小黄车,屁股感受着阳光积累的炙热,往公司飞驰而去。 一步跨进电梯...
继续阅读 »

大家好,我是老三,最近沉迷于听脱口秀,并且疯狂安利同事。


脱口秀演员常常说的一句话是:“脱口秀是冒犯的艺术”。最近我发现,同事们好像有点不一样了。




阳光灿烂的早上,趿拉着我的宝马拖鞋,跨上包浆的小黄车,屁股感受着阳光积累的炙热,往公司飞驰而去。


一步跨进电梯间,我擦汗的动作凝固住了,挂上了矜持的微笑:“老板,早上好。”


老板:“早,你还在呢?又来带薪划水了?”


我:“嗨,我这再努力,最后不也就让你给我们多换几个嫂子嘛。”


老板:“没有哈哈,我开玩笑。”


我:“我也是,哈哈哈。”


今天的电梯似乎比往常慢了很多。


我:“老板最近在忙什么?”


老板:“昨天参加了一个峰会,马xx知道吧?他就坐我前边。”


我:“卧槽,真能装。没有,哈哈。”


老板:“哈哈哈”。


电梯到了,我俩都步履匆匆地进了公司。


小组内每天早上都有一个晨会,汇报工作进度和计划。


开了一会,转着椅子,划着朋友圈的我停了下来——到我了。


我:“昨天主要……今天计划……”


Leader:“你这不能说没有一点产出,也可以说一点产出都没有。其实,我对你是有一些失望的,原本今年绩效考评给你一个……”


我:“影响你合周报了是吗?不是哈哈。”


Leader、小组同事:“哈哈哈“。


Leader:“好了,我们这次顺便来对齐一下双月OKR,你们OKR都写的太保守了,一看就是能完成的,往大里吹啊。开玩笑哈哈。”。


我:”我以前就耕一亩田,现在把整个河北平原都给犁了。不是,哈哈。”


同事:“我要带公司打上月球,把你踢下来,我来当话事人。唉,哈哈”


Leader、同事、我:“哈哈哈“。


晨会开完,开始工作,产品经理拉我和和前端对需求。


产品经理:“你们程序员懂Java语言、Python语言、Go语言,就是不懂汉语言,真不想跟你们对需求。开个玩笑,哈哈。”


我:“没啥,你吹牛皮像狼,催进度像狗,做需求像羊,就这需求文档,还没擦屁股纸字多,没啥好对的。不是哈哈。”


产品经理、前端、我:“哈哈哈”。


产品经理:“那我们就对到这了,你们接着聊技术实现。”


前端:“没啥好聊的,后端大哥看着写吧,反正你们那破接口,套的比裹脚布还厚,没事还老出BUG。没有哈哈。”


我:“还不是为了兼容你们,一点动脑子的逻辑都不写,天天切图当然不出错。不是哈哈。”


前端、我:“哈哈哈”。


经过一番拉扯之后,我终于开始写代码了。


看到一段代码,我皱起了眉头,同事写的,我顺手写下了这样一段注释:


/**
* 写这段代码的人,建议在脑袋开个口,把水倒掉。不是哈哈,开个玩笑。
**/


代码写完了,准备上线,找同事给我Review,同事看了一会,给出了评论。



又在背着我们偷偷写烂代码了,建议改行。没有哈哈。



同事、我:“哈哈哈”。


终于下班了,路过门口,HR小姐姐还在加班。


我:“小姐姐怎么还没下班?别装了,老板都走了。开玩笑哈哈。”


HR小姐姐:“这不是看看怎么优化你们嘛,任务比较重。不是,哈哈。”


HR小姐姐、我:“哈哈哈”。


我感觉到一种不一样的氛围在公司慢慢弥散开来,我不知道怎么形容,但我想到了一句话——


“既分高下,也决生死”。




写这篇的时候,想到两年前,有个叫码农小说家的作者横空出世,写了一些生动活泼、灵气十足的段子,我也跟风写了两篇,这就是“荒腔走板”系列的来源。


后来,他结婚了。


看(抄)不到的我只能自己想,想破头也写不不来像样的段子,这个系列就不了了之,今天又偶尔来了灵感,写下一篇,也顺带缅怀一下光哥带来的快乐。


作者:三分恶
来源:juejin.cn/post/7259036373579350077
收起阅读 »

为什么网站要使用HTTPS?

现在HTTPS基本上已经是网站的标配了,很少会遇到单纯使用HTTP的网站。但是十年前这还是另一番景象,当时只有几家大型互联网公司的网站会使用HTTPS,大部分使用的都还是简单的HTTP,这一切是怎么发生的呢?为什么要把网站升级到HTTPS?若干年前,公司开发了...
继续阅读 »

现在HTTPS基本上已经是网站的标配了,很少会遇到单纯使用HTTP的网站。但是十年前这还是另一番景象,当时只有几家大型互联网公司的网站会使用HTTPS,大部分使用的都还是简单的HTTP,这一切是怎么发生的呢?

为什么要把网站升级到HTTPS?

若干年前,公司开发了一款APP,其中的某些页面是用H5实现的,有一天用户向我们反馈,页面中弹出了一个广告窗口,这让当时身为开发小白的我感觉很懵逼,后来经过经验丰富的老程序员点拨,才知道这是被电信运营商劫持了,运营商拦截了服务器对用户的HTTP响应,并在中间夹带了一些私货。

一些网龄比较大的同学可能还有这样的记忆:网站页面找不到的时候,浏览器会跳转到一个运营商或者路由器厂商的网址导航页面;家里的宽带到期的时候,浏览器网页右下角会弹出续费通知。

这都是HTTP响应被劫持的表现,HTTP本身没什么安全机制,HTTP传输的数据很容易被窃取和篡改,这也是我们将网站升级到HTTPS的根本动机。

使用HTTPS有很多好处,这里稍微展开介绍一下:

  • 数据加密:HTTPS通过SSL/TLS协议为数据传输过程提供了加密,即便数据在传输过程中被截获,没有密钥也无法解读数据内容。这就像是特工使用密文发送电报,即使电报内容被别人截获,没有密码表也无法解读其中的内容。
  • 身份验证:使用HTTPS的网站会获得权威认证机构颁发的证书,这就像是一个“身-份-证”,让访问者能够确认自己访问的是官方合法的网站,有效防止钓鱼网站的风险。
  • 数据完整性:因为数据传输的中间人接触不到密钥,不仅不能解密,而且也无法对数据进行加密,这就保证了数据在传输过程中不被篡改、伪造。
  • 增强用户信任:由于浏览器会对HTTPS网站显示锁标志,这有助于增强访问者对网站的信任。就像是看到家门口安装了高级安全锁,人们会自然而然地觉得这家人对安全非常重视,从而更加放心。
  • SEO优势:谷歌等搜索引擎已经明确表示,HTTPS是搜索排名算法的一个信号。这意味着使用HTTPS的网站在搜索结果中可能会获得更高的排名,具备更大的竞争优势。

HTTPS的发展趋势

大约从2010年开始,大型网站和安全专家开始倡导使用HTTPS,也就是在HTTP上加上SSL/TLS协议进行加密。

根据互联网安全研究机构的报告,目前超过80%的网站已经使用HTTPS。特别是那些大型电商平台和社交媒体网站,几乎100%都已经完成了从HTTP到HTTPS的升级。

不仅是企业和网站管理员在推动HTTPS的普及,各国政府和互联网安全组织也在积极推荐使用HTTPS。例如,各种浏览器都会对那些仍然使用HTTP的网站标记为“不安全”。

随着人们对网络安全意识的增强,大家也更加偏好那些使用HTTPS的网站。就像是在选择酒店的时候,你可能会更倾向于选择那些看起来保卫严密的酒店。

HTTPS的技术原理

加密技术

HTTPS 安全通信的核心在于加密技术。这里面主要涉及两种加密方式:对称加密和非对称加密。

  • 对称加密:就像是你和朋友使用同一把钥匙来锁和解一个箱子。信息的发送方和接收方使用同一个密钥进行数据的加密和解密。这种方式的优点是加解密速度快,通信成本低,但缺点在于如果密钥被中间截获或者泄漏,通信就不安全了。
  • 非对称加密:就像是用一个钥匙锁箱子(公钥),另一个钥匙来开箱子(私钥)。发送方使用接收方的公钥进行加密,而只有接收方的私钥能解开。这样即便公钥被公开,没有私钥也无法解密信息,从而保证了传输数据的安全。

在实际应用中,HTTPS 通常采用混合加密机制。在连接建立初期使用非对称加密交换对称加密的密钥,一旦密钥交换完成,之后的通信就切换到效率更高的对称加密。就像是先通过一个安全的箱子(非对称加密)把家门钥匙(对称加密的密钥)安全送到朋友手中,之后就可以放心地使用这把钥匙进行通信了。

SSL/TLS协议

HTTPS 实际上是 HTTP 协议跑在 TLS 协议之上,TLS的全称是 Transport Layer Security,从字面上理解就是传输层安全,保护数据传输的安全。有时候我们还会看到 SSL 这个词,SSL 其实是 TLS 的前身,它的全称是 Secure Sockets Layer,Socket 就是是TCP/UDP编程中经常接触的套接字概念,也是传输层的一个组件。

可以理解为,SSL/TLS就像是一个提供安全保护的信封,确保了信件(数据)在寄送过程中的安全。

让我们来详细探查下 HTTPS 的工作流程:

1、开始握手:当浏览器尝试与服务器建立HTTPS连接时,它首先会发送一个“Hello”消息给服务器,这个消息里包含了浏览器支持的加密方法(包括对称加密和非对称加密等)等信息。

2、服务器回应:服务器收到客户端的“Hello”之后,会选择一组客户端和服务器都支持的加密方法,然后用自己的私钥对信息进行签名,把这个签名连同服务器的SSL证书一起发送到客户端,SSL证书里包含了服务器的公钥。

3、验证证书:客户端收到服务器发过来的证书后,会首先验证证书的合法性,确保证书是可信任的CA颁发,且未被篡改。这个验证会使用浏览器或者操作系统内置的安全根证书,验证从服务器证书到根证书的所有认证链上的签名都是可信任的。

4、生成临时密钥:一旦证书验证通过,客户端就会生成一串随机密钥(也就是对称密钥)。然后,客户端会用服务器的公钥对这串随机密钥进行加密,再发送给服务器。

5、服务器解密获取对称密钥:服务器收到加密后的数据,会用自己的私钥对其解密,获取到其中的对称密钥。到这里,客户端和服务器双方就都拥有了这个对称密钥,后续的通信就可以使用这个对称密钥进行加密了。

这里我们介绍的密钥交换方式是RSA,其实TLS支持多种密钥交换机制,除了RSA,还包括Diffie-Hellman密钥交换(简称DH)、椭圆曲线Diffie-Hellman(简称ECDH)密钥交换等,或者RSA和DH的结合。DH密钥交换不需要在通信双方之间直接发送对称密钥,同时即使证书的私钥被泄露,之前的会话密钥也不能被推导出来,之前的通信也就无法被解密,这样更加安全。有兴趣的同学可以去搜索了解一下。

证书和认证机构(CA)

为了保证网站的身份真实性,HTTPS还涉及到了证书(SSL证书)的使用。这个证书由认证机构(CA)颁发,包含了网站公钥、网站身份信息等。浏览器或操作系统内置了这些认证机构的信任列表,能自动验证证书的真实性。

证书认证机构会在颁发证书前确认网站的身份,这有点像买火车票之前,需要先通过身份认证来确认你的身份。根据验证的深度和范围,证书可以分为以下几种类型:

  1. 域名验证(DV)证书

这种证书只验证网站拥有者对域名的控制权。CA会通过Url文件验证或DNS记录验证等方式来确认申请者是否控制该域名。DV证书的发放速度快,成本低,但它只证明域名的控制权,不会验证组织的真实身份。

  1. 组织验证(OV)证书

OV证书不仅验证域名的控制权,还要验证申请证书的组织是真实、合法且正式注册的。这就像提交某些申请时,除了要上传身-份-证,还要上传企业的营业执照,确认你是某个公司的员工。OV证书提供了更高级别的信任,适用于商业网站。

  1. 扩展验证(EV)证书

EV证书提供了最高级别的验证。在这个过程中,CA会进行更为严格和全面的审查,包括确认申请组织的法律、运营和物理存在。这就像不仅检查身-份-证和营业执照,还要确认你的实际居住地址、实际办公地点等信息。EV证书为用户提供了最高水平的信任,但它的发放流程最为复杂,成本也最高。

配置HTTPS的步骤

1. 获取SSL/TLS证书

可以从阿里云、腾讯云等这些大的云计算平台申请你需要的证书,也可以从专门的证书颁发机构获取。

证书可以只针对单个域名,比如www.juejin.cn,那只能 http://www.juejin.cn 使用这个证书,www2.juejin.cn 不能使用这个证书;也可以配置为泛域名,比如 *.juejin.cn,那么 http://www.juejin.cn 和 www2.juejin.cn 都可以使用这个证书。

申请证书时会验证你的身份,比如对于DV证书,需要你在DNS中配置一个特殊TXT解析、或者在网站中放置一个特别的验证文件,证书颁发机构能够通过网络进行验证。

验证通过后,证书颁发结构会给你发放证书,包括公钥和私钥。

证书有免费版和收费版。免费版一般只针对单个域名,仅颁发DV证书,证书的有效期一般是3-12个月。普通用户为了节约成本,可以使用免费版本,通过一些程序脚本实现证书的到期自动更新。

2. 配置Web服务器

拿到证书后,需要在你的Web服务器上配置它,具体步骤取决于你使用的服务器软件(如Apache、Nginx等)。

注意HTTPS默认的监听端口是443,使用这个端口,用户访问时可以不输入端口号。

3. 强制使用HTTPS

为了确保所有数据都是安全传输的,我们可以使用重定向让用户始终访问HTTPS地址。

在Web服务器上设置,将所有HTTP请求重定向到HTTPS,用户使用HTTP时都会自动跳转到HTTPS,比如访问 juejin.cn 会自动跳转到 juejin.cn。

4. 维护和更新

证书都是有保质期的,需要在证书到期前进行续期。有时候我们还需要根据安全威胁报告,及时更新SSL/TLS的加密设置,确保它们符合最新的安全标准。

HTTPS的安全问题

HTTPS虽然大大提高了网站的安全性,但它也不是万无一失的。

1、弱加密算法

如果使用过时或不安全的加密算法,加密的数据可能会被破解。

在Web服务器配置中禁用已知不安全的SSL/TLS版本(如TLS 1.0和1.1)和弱加密套件,选择使用强加密算法,如AES_GCM或ChaCha20。

2、钓鱼网站

即使是使用HTTPS的网站,也可能是钓鱼网站,比如DV证书只验证网站的域名归属,不确认网站具体是干什么的。这就像强盗穿上快递员的制服,你很难一眼识破。

对于关键的服务,比如在线购物、上传个人信息,用户需要提高警惕,检查网站的URL,确保是访问的正确网站。

我们也可以使用浏览器提供的安全插件或服务来识别和阻止访问已知的恶意网站。

3、中间人攻击

即使使用了HTTPS,如果攻击者能够在通信双方之间插入自己,就能够监听、修改传输的数据。如果你使用过Fiddler 这种抓包程序做过前端通信调试,就很容易理解这个问题。这就像快递途中有个假冒的收发室,所有包裹都得先经过它。

要防范这个问题比较困难,用户尽量不要在公共的WiFi网络进行敏感操作,不随便下载安装可疑的文件或程序,网站运营者要确保网站的TLS配置是安全的,使用强加密算法和协议。

4、审核不严的证书

证书颁发机构审核不严或者胡乱颁发证书,比如别有用心的人通过特殊手段就能申请到google.com的证书。而且历史上也确实发生过。

2011年,荷兰证书颁发机构(CA)DigiNotar因被黑客入侵并滥发了大量伪造的SSL/TLS证书,包括对Google域名的证书,最终导致DigiNotar破产。

2016年,中国CA机构WoSign及旗下子公司StartCom被曝出多种违规操作,导致主流浏览器厂商逐步撤销对这两家CA的信任。

解决这个问题主要依赖证书颁发机构和监管机构的安全机制,浏览器和操作系统厂商也可以在问题发生后通过紧急更新来避免风险的进一步扩大,使用证书的用户如果有能力,可以通过监控CA机构发布的证书颁发日志来探查是否有未经授权的证书颁发给你的域名。


以上就是本文的主要内容,希望此文能让你对Https有了一个系统全面的了解,更好的保护Http通信安全。


作者:萤火架构
来源:juejin.cn/post/7366053684154777626
收起阅读 »

关于我裁员在家没事接了个私单这件事...

起因 2024年3月31日,我被公司裁员了。 2024年4月1日,果断踏上了回家的路,决定先休息一个星期。晚上回到了郑州,先跟一起被裁的同事在郑州小聚一下,聊聊后面的打算。第二天下午回家。 2024年4月8日,知道现在的大环境不好,不敢错过“金三银四”,赶忙回...
继续阅读 »

起因


2024年3月31日,我被公司裁员了。


2024年4月1日,果断踏上了回家的路,决定先休息一个星期。晚上回到了郑州,先跟一起被裁的同事在郑州小聚一下,聊聊后面的打算。第二天下午回家。


2024年4月8日,知道现在的大环境不好,不敢错过“金三银四”,赶忙回上海开始找工作。结果环境比预想的还要差啊,以前简历放开就有人找,现在每天投个几十封都是石沉大海。。。


2024年4月15日,有个好朋友找我,想让我给他们公司开发一个“拨号APP”(主要原因其实是这个好哥们想让我多个赚钱门路😌),主要的功能就是在他们的系统上点击一个“拨号”按钮,然后员工的工作手机上就自动拨打这个号码。


可行性分析


涉及到的修改:



  • 系统前后端

  • 拨号功能的APP


拿到这个需求之后,我并没有直接拒绝或者同意,而是先让他把他公司那边的的源代码发我了一份,大致看了一下使用的框架,然后找一些后端的朋友看有没人有人一起接这个单子;而我自己则是要先看下能否实现APP的功能(因为我以前从来没有做过APP!!!)。


我们各自看过自己的东西,然后又沟通了一番简单的实现过程后达成了一致,搞!


因为我这边之前的技术栈一直是VUE,所以决定使用uni-app实现,主要还是因为它的上手难度会低很多。


第一版


需求分析


虽说主体的功能是拨号,但其实是隐含很多辅助性需求的,比如拨号日志、通时通次统计、通话录音、录音上传、后台运行,另外除了这些外还有额外的例如权限校验、权限引导、获取手机号、获取拨号状态等功能需要实现。


但是第一次预算给的并不高,要把这些全部实现显然不可能。因此只能简化实现功能实现。



  • 拨号APP

    • 权限校验

      • 实现部分(拨号、录音、文件读写)



    • ❌权限引导

    • 查询当前手机号

      • 直接使用input表单,由用户输入



    • 查询当前手机号的拨号任务

      • 因为后端没有socket,使用setTimeout模拟轮询实现。



    • 拨号、录音、监测拨号状态

      • 根据官网API和一些安卓原生实现



    • 更新任务状态

      • 告诉后端拨号完成



    • ❌通话录音上传

    • ❌通话日志上传

    • ❌本地通时通次统计

    • 程序运行日志

    • 其他

      • 增加开始工作、开启录音的状态切换

      • 兼容性,只兼容安卓手机即可






基础设计


一个input框来输入用户手机号,一个开始工作的switch,一个开启录音的切换。用户输入手机号,点击开始工作后开启轮询,轮询到拨号任务后就拨号同时录音,同时监听拨号状态,当挂断后结束录音、更新任务状态,并开启新一轮的轮询。


开干


虽然本人从未开发过APP,但本着撸起袖子就是干的原则,直接打开了uni-app的官网就准备开怼。


1、下载 HbuilderX。


2、新建项目,直接选择了默认模板。


3、清空 Hello页面,修改文件名,配置路由。


4、在vue文件里写主要的功能实现,并增加 Http.jsRecord.jsPhoneCall.jsPower.js来实现对应的模块功能。


⚠️关于测试和打包


运行测试


在 HbuilderX 中点击“运行-运行到手机或模拟器-运行到Android APP基座”会打开一个界面,让你选择运行到那个设备。这是你有两种选择:



  • 把你手机通过USB与电脑连接,然后刷新列表就可以直接运行了。

    • 很遗憾,可能是苹果电脑与安卓手机的原因,插上后检测不出设备😭。。。



  • 安装Android Studio,然后通过运行内置的模拟器来供代码运行测试。

    • 这种很麻烦,要下载很久,且感觉测试效果并不好,最好还是用windows电脑连接手机的方法测试。




关于自定义基座和标准基座的差别,如果你没有买插件的话,直接使用基准插座就好。如果你要使用自定义基座,就首先要点击上图中的制作自定义基座,然后再切换到自定义基座执行。


但是不知道为什么,我这里一直显示安装自定义基座失败。。。


打包测试


除了以上运行测试的方法外,你还有一种更粗暴的测试方法,那就是打包成APP直接在手机上安装测试。


点击“发行-原生APP 云打包”,会生成一个APK文件,然后就可以发送到手机上安装测试。不过每天打包的次数有限,超过次数需要购买额外的打包服务或者等第二天打包。


我最终就是这样搞得,真的我哭死,我可能就是盲调的命,好多项目都是盲调的。


另外,在打包之前我们首先要配置manifest.json,里面包含了APP的很多信息。比较重要的一个是AppId,一个是App权限配置。参考uni-app 权限配置Android官方权限常量文档。以下是拨号所需的一些权限:



// 录制音频
<uses-permission android:name="android.permission.RECORD_AUDIO" />
// 修改音频设置
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />

// 照相机
<uses-permission android:name="android.permission.CAMERA" />
// 写入外部存储
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
// 读取外部存储
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

// 读取电话号码
<uses-permission android:name="android.permission.READ_PHONE_NUMBERS" />
// 拨打电话
<uses-permission android:name="android.permission.CALL_PHONE" />
// 呼叫特权
<uses-permission android:name="android.permission.CALL_PRIVILEGED" />
// 通话状态
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
// 读取拨号日志
<uses-permission android:name="android.permission.READ_CALL_LOG" />
// 写入拨号日志
<uses-permission android:name="android.permission.WRITE_CALL_LOG" />
// 读取联系人
<uses-permission android:name="android.permission.READ_CONTACTS" />
// 写入联系人
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
// 读取SMS?
<uses-permission android:name="android.permission.READ_SMS" />

// 写入设置
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
// 唤醒锁定?
<uses-permission android:name="android.permission.WAKE_LOCK" />
// 系统告警窗口?
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
// 接受完整的引导?
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

⚠️权限配置这个搞了很长时间,即便现在有些权限还是不太清楚,也不知道是不是有哪些权限没有配置上。。。


⚠️权限校验


1、安卓 1


好像除了这样的写法还可以写"scope.record"或者permission.CALL_PHONE


permision.requestAndroidPermission("android.permission.CALL_PHONE").then(res => {
// 1 获得权限 2 本次拒绝 -1 永久拒绝
});

2、安卓 2


plus.android.requestPermissions(["android.permission.CALL_PHONE"], e => {
// e.granted 获得权限
// e.deniedPresent 本次拒绝
// e.deniedAlways 永久拒绝
});

3、uni-app


这个我没测试,AI给的,我没有用这种方法。有一说一,百度AI做的不咋地。


// 检查权限
uni.hasPermission({
permission: 'makePhoneCall',
success() {
console.log('已经获得拨号权限');
},
fail() {
// 示例:请求权限
uni.authorize({
scope: 'scope.makePhoneCall',
success() {
console.log('已经获得授权');
},
fail() {
console.log('用户拒绝授权');
// 引导用户到设置中开启权限
uni.showModal({
title: '提示',
content: '请在系统设置中打开拨号权限',
success: function(res) {
if (res.confirm) {
// 引导用户到设置页
uni.openSetting();
}
}
});
}
});
}
});

✅拨号


三种方法都可以实现拨号功能,只要有权限,之所以找了三种是为了实现APP在后台的情况下拨号的目的,做了N多测试,甚至到后面搞了一份原生插件的代码不过插件加载当时没搞懂就放弃了,不过到后面才发现原来后台拨号出现问题的原因不在这里,,具体原因看后面。


另外获取当前设备平台可以使用let platform = uni.getSystemInfoSync().platform;,我这里只需要兼容固定机型。


1、uni-app API


uni.makePhoneCall({
phoneNumber: phone,
success: () => {
log(`成功拨打电话${phone}`);
},
fail: (err) => {
log(`拨打电话失败! ${err}`);
}
});

2、Android


plus.device.dial(phone, false);

3、Android 原生


写这个的时候有个小插曲,当时已经凌晨了,再加上我没有复制,是一个个单词敲的,结果竟然敲错了一个单词,测了好几遍都没有成功。。。还在想到底哪里错了,后来核对一遍才发现😭,control cv才是王道啊。


// Android
function PhoneCallAndroid(phone) {
if (!plus || !plus.android) return;
// 导入Activity、Intent类
var Intent = plus.android.importClass("android.content.Intent");
var Uri = plus.android.importClass("android.net.Uri");
// 获取主Activity对象的实例
var main = plus.android.runtimeMainActivity();
// 创建Intent
var uri = Uri.parse("tel:" + phone); // 这里可修改电话号码
var call = new Intent("android.intent.action.CALL", uri);
// 调用startActivity方法拨打电话
main.startActivity(call);
}

✅拨号状态查询


第一版用的就是这个获取状态的代码,有三种状态。第二版的时候又换了一种,因为要增加呼入、呼出、挂断、未接等状态的判断。


export function getCallStatus(callback) {
if (!plus || !plus.android) return;
let maintest = plus.android.runtimeMainActivity();
let Contexttest = plus.android.importClass("android.content.Context");
let telephonyManager = plus.android.importClass("android.telephony.TelephonyManager");
let telManager = plus.android.runtimeMainActivity().getSystemService(Contexttest.TELEPHONY_SERVICE);
let receiver = plus.android.implements('io.dcloud.android.content.BroadcastReceiver', {
onReceive: (Contexttest, intent) => {
plus.android.importClass(intent);
let phoneStatus = telManager.getCallState();
callback && callback(phoneStatus);
//电话状态 0->空闲状态 1->振铃状态 2->通话存在
}
});
let IntentFilter = plus.android.importClass("android.content.IntentFilter");
let filter = new IntentFilter();
filter.addAction(telephonyManager.ACTION_PHONE_STATE_CHANGED);
maintest.registerReceiver(receiver, filter);
}

⚠️录音


录音功能这个其实没啥,都是官网的API,无非是简单的处理一些东西。但是这里有一个大坑!


一坑


就是像通话录音这种涉及到的隐私权限很高,正常的这种录音是在通话过程中是不被允许的。


二坑


后来一次偶然的想法,在接通之后再开启录音,发现就可以录音了。


但随之而来的是第二个坑,那就是虽然录音了,但是当播放的时候发现没有任何的声音,还是因为保护隐私的原因,我当时还脱离代码专门试了试手机自带的录音器来在通话时录音,发现也不行。由此也发现uni的录音本身也是用的手机录音器的功能。


三坑


虽然没有声音,但是我还是试了下保存,然后就发现了第三个坑,那就是虽然获取了文件权限,但是现在手机给的读写权限都是在限定内的,录音所在的文件夹是无权访问的。。。


另辟蹊径


其实除了自己手动录音外还可以通过手机自带的通话录音来实现,然后只要手机去读取录音文件并找到对应的那个就可以了。思路是没啥问题,不过因为设置通话录音指引、获取录音文件都有问题,这一版本就没实现。


// 录音

var log = console.log,
recorder = null,
// innerAudioContext = null,
isRecording = false;

export function startRecording(logFun = console.log) {
if (!uni.getRecorderManager || !uni.getRecorderManager()) return logFun('不支持录音!');
log = logFun;
recorder = uni.getRecorderManager();
// innerAudioContext = uni.createInnerAudioContext();
// innerAudioContext.autoplay = true;
recorder.onStart(() => {
isRecording = true;
log(`录音已开始 ${new Date()}`);
});
recorder.onError((err) => {
log(`录音出错:${err}`);
console.log("录音出错:", err);
});
recorder.onInterruptionBegin(() => {
log(`检测到录音被来电中断...`);
});
recorder.onPause(() => {
log(`检测到录音被来电中断后尝试启动录音..`);
recorder.start({
duration: 10 * 60 * 1000,
});
});
recorder.start({
duration: 10 * 60 * 1000,
});
}

export function stopRecording() {
if (!recorder) return
recorder.onStop((res) => {
isRecording = false;
log(`录音已停止! ${new Date()}`); // :${res.tempFilePath}
// 处理录制的音频文件(例如,保存或上传)
// powerCheckSaveRecord(res.tempFilePath);
saveRecording(res.tempFilePath);
});
recorder.stop();
}

export function saveRecording(filePath) {
// 使用uni.saveFile API保存录音文件
log('开始保存录音文件');
uni.saveFile({
tempFilePath: filePath,
success(res) {
// 保存成功后,res.savedFilePath 为保存后的文件路径
log(`录音保存成功:${res.savedFilePath}`);
// 可以将res.savedFilePath保存到你的数据中,或者执行其他保存相关的操作
},
fail(err) {
log(`录音保存失败! ${err}`);
console.error("录音保存失败:", err);
},
});
}

运行日志


为了更好的测试,也为了能实时的看到执行的过程,需要一个日志,我这里就直接渲染了一个倒序的数组,数组中的每一项就是各个函数push的字符串输出。简单处理。。。。嘛。


联调、测试、交工


搞到最后,大概就交了个这么玩意,不过也没有办法,一是自己确实不熟悉APP开发,二是满共就给了两天的工时,中间做了大量的测试代码的工作,时间确实有点紧了。所幸最起码的功能没啥问题,也算是交付了。


image.png


第二版


2024年05月7日,老哥又找上我了,想让我们把他们的这套东西再给友商部署一套,顺便把这个APP再改一改,增加上通时通次的统计功能。同时也是谈合作,如果后面有其他的友商想用这套系统,他来谈,我们来实施,达成一个长期合作关系。


我仔细想了想,觉得这是个机会,这块东西的市场需求也一直有,且自己现在失业在家也有时间,就想着把这个简单的功能打磨成一个像样的产品。也算是做一次尝试。


需求分析



  • ✅拨号APP

    • 登录

      • uni-id实现



    • 权限校验

      • 拨号权限、文件权限、自带通话录音配置



    • 权限引导

      • 文件权限引导

      • 通话录音配置引导

      • 获取手机号权限配置引导

      • 后台运行权限配置引导

      • 当前兼容机型说明



    • 拨号

      • 获取手机号

        • 是否双卡校验

        • 直接读取手机卡槽中的手机号码

        • 如果用户不会设置权限兼容直接input框输入



      • 拨号

      • 全局拨号状态监控注册、取消

        • 支持呼入、呼出、通话中、来电未接或挂断、去电未接或挂断





    • 录音

      • 读取录音文件列表

        • 支持全部或按时间查询



      • 播放录音

      • ❌上传录音文件到云端



    • 通时通次统计

      • 云端数据根据上面状态监控获取并上传

        • 云端另写一套页面



      • 本地数据读取本机的通话日志并整理统计

        • 支持按时间查询

        • 支持呼入、呼出、总计的通话次数、通话时间、接通率、有效率等





    • 其他

      • 优化日志显示形式

        • 封装了一个类似聊天框的组件,支持字符串、Html、插槽三种显示模式

        • 在上个组件的基础上实现权限校验和权限引导

        • 在上两个组件的基础上实现主页面逻辑功能



      • 增加了拨号测试、远端连接测试

      • 修改了APP名称和图标

      • 打包时增加了自有证书






中间遇到并解决的一些问题


关于框架模板


这次重构我使用了uni中uni-starter + uni-admin 项目模板。整体倒还没啥,这俩配合还挺好的,就只是刚开始不知道还要配置东西一直没有启动起来。


建立完项目之后还要进uniCloud/cloudfunctions/common/uni-config-center/uni-id配置一个JSON文件来约定用户系统的一些配置。


打包的时候也要在manifest.json将部分APP模块配置进去。


还搞了挺久的,半天才查出来。。


类聊天组件实现



  • 设计

    • 每个对话为一个无状态组件

    • 一个图标、一个名称、一个白底的展示区域、一个白色三角

    • 内容区域通过类型判断如何渲染

    • 根据前后两条数据时间差判断是否显示灰色时间



  • 参数

    • ID、名称、图标、时间、内容、内容类型等



  • 样式

    • 根据左边右边区分发送接收方,给与不同的类名

    • flex布局实现




样式实现这里,我才知道原来APP和H5的展示效果是完全不同的,个别地方需要写两套样式。


关于后台运行


这个是除了录音最让我头疼的问题了,我想了很多实现方案,也查询了很多相关的知识,但依旧没效果。总体来说有以下几种思路。



  • 通过寻找某个权限和引导(试图寻找到底是哪个权限控制的)

  • 通过不停的访问位置信息

  • 通过查找相应的插件、询问GPT、百度查询

  • 通过程序切入后台之后,在屏幕上留个悬浮框(参考游戏脚本的做法)

  • 通过切入后台后,发送消息实现(没测试)


测试了不知道多少遍,最终在一次无意中,终于发现了如何实现后台拨号,并且在之后看到后台情况下拨号状态异常,然后又查询了应用权限申请记录,也终于知道,归根到底能否后台运行还是权限的问题。


关于通话状态、通话记录中的类型


这个倒还好,就是测试的问题,知道了上面为啥异常的情况下,多做几次测试,就能知道对应的都是什么状态了。


通话状态:呼入振铃、通话中(呼入呼出)、通话挂断(呼入呼出)、来电未接或拒绝、去电未接或拒接。


通话日志:呼入、呼出、未接、语音邮件、拒接


交付


总体上来说还过得去,相比于上次简陋的东西,最起码有了一点APP的样子,基本上该有的功能也基本都已经实现了,美中不足的一点是下面的图标没有找到合适的替换,然后录音上传的功能暂未实现,不过这个也好实现了。


image.png


后面的计划



  • 把图标改好

  • 把录音文件是否已上传、录音上传功能做好

  • 把APP的关于页面加上,对接方法、使用方法和视频、问题咨询等等

  • 原本通话任务、通时通次这些是放在一个PHP后端的,对接较麻烦。要用云函数再实现一遍,然后对外暴露几个接口,这样任何一个系统都可以对接这个APP,而我也可以通过控制云空间的跨域配置来开放权限

  • 把数据留在这边之后,就可以再把uni-admin上加几个页面,并且绑定到阿里云的云函数前端网页托管上去

  • 如果有可能的话,上架应用商店,增加上一些广告或者换量联盟之类的东西

  • 后台运行时,屏幕上加个悬浮图标,来电时能显示个振铃啥的


大致的想法就这些了,如果这个产品能继续卖下去,我就会不断的完善它。


最后


现在的行情真的是不好啊,不知道有没有大哥给个内推的机会,本人大专计算专业、6.5年Vue经验(专精后台管理、监控大屏方向,其他新方向愿意尝试),多个0-1-2项目经验,跨多个领域如人员管理、项目管理、产品设计、软件测试、数据爬虫、NodeJS、流程规范等等方面均有了解,工作稳定不经常跳,求路过的大哥给个内推机会把!



作者:前端湫
来源:juejin.cn/post/7368421971384860684
收起阅读 »

最近得了一场病 差点要了我的命

最近得了一场病,前后持续了有十多天,时至今日感觉脑袋还是昏昏沉沉的不在状态,感觉像是药吃的,毕竟连着吃了十多天西药,可能人也吃傻了吧,中间还挂了五天水,算是补充了能量。 起因是和老婆去吃饭,可能是吃得太饱晚上着凉了,结果第二天下班回来就发烧,最开始是去社康看的...
继续阅读 »

最近得了一场病,前后持续了有十多天,时至今日感觉脑袋还是昏昏沉沉的不在状态,感觉像是药吃的,毕竟连着吃了十多天西药,可能人也吃傻了吧,中间还挂了五天水,算是补充了能量。


起因是和老婆去吃饭,可能是吃得太饱晚上着凉了,结果第二天下班回来就发烧,最开始是去社康看的,想着缴纳的社保也一直没用过,不知道怎么用,刚好借这个机会去试试。


图片


不知道西安的社保是什么样,深圳这边的分一二三档,一档可以去任意社康和医院看病买药,有独立账户;二档没有独立账户,且只能去绑定的社康看病,去医院的话需要先去社康开转诊,最新消息说现在不用了,而且也不能单独买药。


去了之后,先是几项检查,抽血化验,鼻腔测试,加起来一百多块吧,当时就纳闷,一个感冒发烧要搞得这么复杂吗,问医生说流程是这样的,要检查是什么原因引起的,看完报告说有点轻微病毒感染,再就是细菌感染引起的发烧,然后就开了点药。


图片


说下这里的报销比例吧,理论上最高能报到75%,个人能承担25%,二百块钱的医药费,报销完自己付了有五十多块钱吧。听说这是深圳这边今年十月份重新调整后的,之前是二档每年报销一千额度,单次报销比例高,个人承担费用少,调整后每人每年有两千多的免费额度,但是单次报销比例也降低了,意味着个人每次承担的也多了,当然如果你是长期去医院,也是比较划算的。


图片


再来看一下在社康开的药吧,一大盒口服液,里面是多支小瓶装;一盒头孢,之前总是听说,也是头一次吃,说是消炎的;还有一盒粉末状的东西,说是用温水冲服,后来发现是盐水,怎么说呢,总感觉有点开玩笑的意思吧。


图片


两天的药吃完之后呢,中间身体短暂好转了半天,再之后到晚上就又是继续发烧,最高烧到接近四十度,第二天量的时候已经达到39.5,且中间一直伴随着头疼,实在忍不了,整宿睡不着,早上六点多给领导发了消息请了假。


图片


这次是去楼下的私人诊所医治,女医生看了就说连着发烧这么多天现在吃药肯定来不及了,必须要打针,然后就开始输液了。小时候感觉输液是一件大动干戈的事,现在看来却是那么的平淡无奇,四小瓶水挂完之后浑身冒汗,温度也降了下来,头也没有那么疼了,中途老婆一直陪着我,还给我带了吃的,挂完之后医生同时又开了几天吃的药。


图片


第二天感觉轻松多了,为了巩固又去挂了一天,然后两天药也吃完了,接下来又产生了新的问题,不知道咋回事一直打嗝,连着一整天不停歇的那种打,刚开始以为是吃东西噎着了,可是无论怎么喝水憋气都无济于事,上班时坐在工位上自己嗝的都有点不好意思了,一直到晚上回家,没办法又去问了医生,说可能是胃痉挛,胃部引起的,开了两顿吃的药,很神奇的是刚吃下去没一会打嗝就停了,这一夜算是到这了,也睡了个好觉。


图片


接下来又是继续发烧,头疼,头晕,浑身无力,中午休息头疼的睡不着觉,下午手脚发麻,实在有些扛不住了,出去外面商场找了个沙发窝着睡了会,一直扛到下班,又去看医生,说是没好彻底,继续挂水,吃药,又是三天。这一路下来,真的是折磨人,让老婆也跟着前前后后来回折腾。其实在整个过程当中,发烧这些我感觉都可以忍,最难受的是头疼,偏头痛,那种神经痛,一刻不停的那种疼,真的很折磨人,让人崩溃。


图片


再往后又连着挂了三天水,吃了三天药,折腾了十多天好觉差不多了,接下来又残留着一点小问题,就是一直咳嗽停不下来,不是喉咙咳是从肺里面的那种咳,好觉应该问题不太大,过几天就好了,可是持续了几天还是一直在咳嗽,然后就又去看了医生,买了三天的药一百多块钱,问题是和前面开的药也一模一样没啥变化,感觉这边还是挺黑的,普普通通两三天药就是一百多,放在老家可能就几十块最多了,再加上前面打针输液的,总共花了一千多块吧,就是个普通的感冒发烧,稍微严重点。


图片


经过这次事件,有以下几点感触吧。无论什么时候,身体健康是第一位,所谓的工作,都是建立在你有个好的身体的前提下,身体状态良好,你才能更好的投入工作。第二,平时除了多加锻炼身体,还要注重身体按摩,长期坐在办公室不运动,颈椎难免有影响,没事多按摩活动下,也利用颈部头部血液循环,有助于偏头痛的缓解;再就是第三点,要好好吃饭,注重身体包养,好好对待自己的胃,很多东西都是吃出来的。


作者:编程迪
来源:juejin.cn/post/7306018817687765044
收起阅读 »

28个令人惊艳的JavaScript单行代码

web
JavaScript作为一种强大而灵活的脚本语言,充满了许多令人惊艳的特性。本文将带你探索28个令人惊艳的JavaScript单行代码,展示它们的神奇魅力。 1. 阶乘计算 使用递归函数计算给定数字的阶乘。 const factorial = n => ...
继续阅读 »

JavaScript作为一种强大而灵活的脚本语言,充满了许多令人惊艳的特性。本文将带你探索28个令人惊艳的JavaScript单行代码,展示它们的神奇魅力。


1. 阶乘计算


使用递归函数计算给定数字的阶乘。


const factorial = n => n === 0 ? 1 : n * factorial(n - 1);
console.log(factorial(5)); // 输出 120

2. 判断一个变量是否为对象类型


const isObject = variable === Object(variable);

3. 数组去重


利用Set数据结构的特性,去除数组中的重复元素。


const uniqueArray = [...new Set(array)];

4. 数组合并


合并多个数组,创建一个新的数组。


const mergedArray = [].concat(...arrays);

5. 快速最大值和最小值


获取数组中的最大值和最小值。


const max = Math.max(...array);
const min = Math.min(...array);

6. 数组求和


快速计算数组中所有元素的和。


const sum = array.reduce((acc, cur) => acc + cur, 0);

7. 获取随机整数


生成一个指定范围内的随机整数。


const randomInt = Math.floor(Math.random() * (max - min + 1)) + min;

8. 反转字符串


将字符串反转。


const reversedString = string.split('').reverse().join('');

9. 检查回文字符串


判断一个字符串是否为回文字符串。


const isPalindrome = string === string.split('').reverse().join('');

10. 扁平化数组


将多维数组转换为一维数组。


const flattenedArray = array.flat(Infinity);

11. 取随机数组元素


从数组中随机取出一个元素。


const randomElement = array[Math.floor(Math.random() * array.length)];

12. 判断数组元素唯一


检查数组中的元素是否唯一。


const isUnique = array.length === new Set(array).size;

13. 字符串压缩


将字符串中重复的字符进行压缩。


const compressedString = string.replace(/(.)\1+/g, match => match[0] + match.length);

14. 生成斐波那契数列


生成斐波那契数列的前n项。


const fibonacci = Array(n).fill().map((_, i, arr) => i <= 1 ? i : arr[i - 1] + arr[i - 2]);

15. 数组求交集


获取多个数组的交集。


const intersection = arrays.reduce((acc, cur) => acc.filter(value => cur.includes(value)));

16. 验证邮箱格式


检查字符串是否符合邮箱格式。


const isValidEmail = /^[\w-]+(\.[\w-]+)*@([\w-]+\.)+[a-zA-Z]{2,7}$/.test(email);

17. 数组去除假值


移除数组中的所有假值,如falsenull0""undefined


const truthyArray = array.filter(Boolean);

18. 求阶乘


计算一个数的阶乘。


const factorial = n => n <= 1 ? 1 : n * factorial(n - 1);

19. 判断质数


检查一个数是否为质数。


const isPrime = n => ![...Array(n).keys()].slice(2).some(i => n % i === 0);

20. 检查对象是空对象


判断对象是否为空对象。


const isEmptyObject = Object.keys(object).length === 0 && object.constructor === Object;

21. 判断回调函数为真


检查数组中的每个元素是否满足特定条件。


const allTrue = array.every(condition);

22. 检查回调函数为假


检查数组中是否有元素满足特定条件。


const anyFalse = array.some(condition);

23. 数组排序


对数组进行排序。


const sortedArray = array.sort((a, b) => a - b);

24. 日期格式化


将日期对象格式化为指定格式的字符串。


const formattedDate = new Date().toISOString().slice(0, 10);

25. 将字符串转为整数类型


const intValue = +str;

26. 计算数组中元素出现的次数


统计数组中各元素的出现次数。


const countOccurrences = array.reduce((acc, cur) => (acc[cur] ? acc[cur]++ : acc[cur] = 1, acc), {});

27. 交换两个变量的值


[a, b] = [b, a];

28. 利用逗号运算符分隔多个表达式


const result = (expression1, expression2, ..., expressionN);

作者:慕仲卿
来源:juejin.cn/post/7307963529872605218
收起阅读 »

困扰我 1 小时的 404 错误 别人 1 分钟解决了

上周五遇到了一个 Bug,没有任何异常,困扰了我一个小时,差点连周末都过不好了。最后没办法,请教了组内的同学,没想到竟被他 1 分钟解决了! 事情的起因,只是因为我把一个接口从 @GetMapping 改成了 @PostMapping ,然后接口就报以下的错误...
继续阅读 »

上周五遇到了一个 Bug,没有任何异常,困扰了我一个小时,差点连周末都过不好了。最后没办法,请教了组内的同学,没想到竟被他 1 分钟解决了!


事情的起因,只是因为我把一个接口从 @GetMapping 改成了 @PostMapping ,然后接口就报以下的错误:


image.png
没有任何的 WARN 或者 ERROR 日志!

网上搜了一下,也没有什么有效的信息,万能的 AI 给出了下面这样的回答:


image.png
404 的错误太常见了,有很多原因造成这一结果。


但可以确定的是,我的请求路径和控制器配置都是没有问题的,因为只要要把 @PostMapping 改回 @GetMapping ,一切都运行正常。


在这种情况下,搜索引擎和AI,除了给我造成干扰误导排查方向外,不能起到什么实质性的作用。


无奈,我只能硬着头皮打开 DEBUG 日志,尝试对照源码,去解决问题了。


不幸的是,DEBUG 日志实在太多了,里面也没有任何异常。我刚学 SpringBoot 不久,这些碎片化的日志,不能引起我的任何联想,因此,实质上也起不到辅助排查的作用。


折腾了一个多小时,还是没有什么头绪,明天就周末了,带着这个 Bug,周末恐怕都休息不好。于是,我就硬着头皮找了组内一个比较有经验的同学帮忙看一下。


他过来翻了翻日志,查看了一下配置类,淡淡地说到,你打开了 Csrf 验证,但是请求却没带 Token。说罢,指导我加上了一行代码:


.csrf().disable()

然后,再次访问,竟然就真的可以了!整个过程也就 1 分钟左右!


我这个小弱鸡的心灵着实有些触动。于是追问到,大神你是怎么看出来的呀。


“没什么,就是经验多了。日志里面有些信息,比如 token 相关的, 其实已经提示了你答案。不过,需要你对框架比较了解,才能 get 到这些信息。新手遇到这种没有明显异常的问题,确实会比较费劲。”


“那有没有什么办法,可以快速搞懂这种框架问题啊,每次遇到都挺烦躁的,不仅影响研发进度,也影响心情” , 上进的我还是想从大神这里获取更多的经验。


“额….我想想”,大神迟疑了一会儿,“你可以试试这个 XCodeMap 插件“,”它可以提供更丰富的信息,图形化的形式,可以较为容易看出可能存在的问题。实在看不出,你也可以基于这些信息再去问搜索引擎或者AI”。


“感谢大神,我去试一下”。


试用了一下,这个工具画出了下面的序列图:


image.png
我虽然不懂什么 Csrf 的原理,但是这个图已经可以清晰地表达出问题了,在 SpringBoot 的FilterChain 中,走到 CsrfFilter 就终止了,并且调用了一个 AccessDeniedHandler。


看起来,这个序列图是实时动态采集的,而且做了很多剪枝,把一些关键调用给标记了出来。对于 SpringBoot 系列,其会把各种 Filter 的调用情况展示出来,可以让人一眼看出来是哪个 Filter 出了问题。


点击 CsrfFilter 的 doFilter 方法,可以看到以下代码:


image.png
这个代码可以看出来,Csrf 的原理(以CookieCsrfToken为例)就是取两个token进行比对。其中一个从请求的 Header 或者 Parameter 中读出。另外一个,从 Cookie 中读出。


image.png
由于浏览器的同源策略,攻击网站无法获取本网站的Cookie,也即其无法完成下面这样的JS操作:


image.png
但是本网站可以通过上面的操作,把 Cookie 中的token设置到 Header 中,这样就达到了避免 CSRF 攻击的效果。


不过,这里还有一个小插曲,csrf 验证失败,本意应该是报 403 错误码,然后转发到 “/403” 页面,只是因为我没有配置 “/403” 页面,最终才报了404 错误。


image.png


这次由 Csrf 引起的 404 错误,就到此为止了。


我独自完成了后面的排查,还是很开心的。我没有大神那样丰富的经验,可以凭借只言片语的日志信息,就可以推断出问题所在。但我借助 XCodeMap 绘制的动态序列图,按图索骥,搞清楚了问题的来龙去脉。这让我想起了下面的一句话:


人类有了弓箭,拳头就不再是绝对硬实力了。好的工具,可以削平人与人的差距!


感觉自己与大神接近了不少!


参考资料:



作者:摸鱼总工
来源:juejin.cn/post/7362722064069427237
收起阅读 »

工作七年后,我不太关注是否升职加薪了,你很优秀,别因世俗的目标限制自己

前言 Hi 你好,我是东东拿铁,一个正在探索个人IP&副业的后端程序员。 工作七年后,我不太关注是否升职加薪了,你很优秀,别因世俗的目标限制自己。 这篇文章我只想用我半年的经历告诉你一件事:探索无限可能,注重个人成长。 为什么别着眼于晋升or加薪 毕业...
继续阅读 »

前言


Hi 你好,我是东东拿铁,一个正在探索个人IP&副业的后端程序员。


工作七年后,我不太关注是否升职加薪了,你很优秀,别因世俗的目标限制自己。


这篇文章我只想用我半年的经历告诉你一件事:探索无限可能,注重个人成长


为什么别着眼于晋升or加薪


毕业刚到北京的前三年,面对自己在的小公司的尴尬局面,看着Boss上琳琅满目的招聘信息,我的脑海里只有四个字,跳槽涨薪


技术,什么不会我学什么,面经写的什么我就去背什么,算法更是赶鸭子上架,哪怕先从背代码开始。


由于咱们程序员行业特殊性,在北京的时候,下班都在晚上10点左右。当然,上班时间也比较晚,十点到就可以。


刚到北京那段时间,我早上起床后,都在看一些SpringBoot的专栏,因为18、19年那会,面试要求就是这样的,只用Spring MVC落后了。


我觉着那时候和高三的那段时间特别相似,上学期间,宿舍、食堂、教室的三点一线。工作之后变成了小出租屋、办公室两点一线,连食堂这一步都给省了,订外卖嘛。


那几年的时间,我从没和非程序员的朋友们,吃过一次晚饭。


工作累的时候,我时常会去楼道里站一会放松一下,记得那时候办公室的楼道里的墙壁,是洞洞形状的,望着外面车水马龙,我第一次有一种深处困境的感觉。(这个印象让我非常深刻,直接就翻出了老照片)



就这样,我一路摸索前行,直到进入字节,因为工作范围变化,我才去接触了更多的朋友,产品、运营、策划、销售。


很多同事,虽然职责不同,但是工作产出高,既能把控方向,也能处理风险。之前我总觉着技术才是那个扶大厦于将倾的角色,慢慢发现我们才是金字塔的最底层,我看到了需求是一步步怎么从市场到运营、从运营到产品,再从产品到研发。


接触的越多,越感觉自己的见识与认知的狭窄。


后来我了解到一个词,“信息茧房”。



“信息茧房”是指人们倾向于只关注与自己兴趣相符的信息,久而久之,会限制自己的视野和认知范围。


“信息茧房”会导致个人的认知和价值观固化,失去批判性思维和多元化思考的能力。它还会加剧社会分化和对立,因为不同的信息茧房之间缺乏有效沟通和交流,容易引发群体极化和冲突。



我接触的都是程序员,我的同学也都是程序员,所以晋升、加薪、进入大厂,变成了我工作之路上唯一的目标。


慢慢的我就掉进这个陷阱了,为了升职加薪,我上班、下班都在学技术,总以为技术都学会了,自然就能升职加薪了。可最后发现,技术好像并不是最重要的那个。


目标单一是如何影响程序员的


技术


在上一篇文章里,有一个读者给我留言:最讽刺的是大部分程序员竟然觉得c端高并发高可用才叫技术。



上一篇文章里面我也讲了我对于技术的本质的四个阶段,其中三个阶段都是对于技术的追求。


这就是技术人的执念,我们想在技术上分一个高低,想去追求高并发,追求更新的技术。


但事实上,做不完的需求,写不完的CRUD才是常态,能少和产品撕一撕,保持一个良好的心情都挺难的。



回到之前文章聊到的,技术的本质是工具。当前你的产品有什么问题,技术是不是能够发挥作用,就已经产生业务价值了,技术含量绝不是由高并发、大流量来衡量的。


追求确定性


做技术久了,习惯了程序的输入与输出,习惯按照某种规律、某个流程、某个框架、某个计划去做事情,我们写的每一行代码,都是确定性的,我们不大喜欢“变化”,喜欢确定性的东西。


记得有一段时间,已经带着团队半年,却迟迟没有晋升,心里很着急。


我对着wiki上的职级能力要求表,一条条看,一条条对比,我觉着我的能力都足够了,为什么还不让我晋升。


可晋升是这样的吗,我满足了能力要求表,就能够立刻轮到我吗?



晋升是企业的一次人才选拔,选拔那些对于公司未来发展更有价值,能承担更大责任的人。



晋升需要你拿到成绩、具备能力、还要具备一定的影响力。


但还有一件最重要的,就是只有在企业不断发展,业务不断发展,团队快速扩张的时候,才会有充足的机会,提供给我们。


但你说,企业能不能高速发展,这是一件确定性的事情吗,可能老板们都不能给出一个确定性的答复。


同样的,涨薪也一样是不确定的,行业、企业发展不好的时候,拖欠工资都有可能,那如何希望能够涨薪呢?


后来我想明白了这件事之后,回想自己自己当时死磕级别要求的样子,感觉挺有意思的。


社交关系


我是山东人,人情社会从小就感受到了许多,也见识了靠社交关系真的能解决很多问题。但是我总觉着,只要靠自己的努力,哪有什么是花钱解决不了的问题,如果有,那就加钱。


后来,在宝宝出生前,很突然的去医院住院,我们先被安排到了一个三人病房。一个病床,一张小桌子,一个沙发床,就是全部空间。孩子的东西很多,我必须把行李箱打开铺在地上,才能及时拿到需要的东西,护士来的时候,我要不就需要把沙发床收起来,要不就得收起行李箱,特别狼狈。


最重要的是普通病房只能有一个陪护,我陪着老婆情况下就不能再请月嫂了。我又是个新奶爸,照顾孩子和还得照顾老婆,忙的不亦乐乎。


其实,我们早早就预约了独立病房,但是资源有限,需要的时候却住满了,无论我怎么去问,人家都说安排不了。独立病房一晚800多元,但你想花钱都花不出去。


最后家人给某个朋友打了电话,然后又联系了医院,我当晚就搬到独立病房了。


是的,医院一般预留着几间独立病房,就是为了方便一些领导临时安排。


Enmmmm,毕业几年都在北京,这几年来一人吃饱全家不饿,可是在有了孩子的第一天,我就被这个社会深深的毒打了。



无效社交确实没用,有效社交都是资源。



高薪


毕业半年,我有勇气裸辞去北京闯荡。


19年时比毕业薪资饭了两倍,给了我高位上车买房的勇气,觉着明天会更好,房贷嘛,只会越来越不值钱。


毕业五年,薪资翻5倍,但你现在让我我裸辞去闯荡,想想房贷,想想娃,反而觉着被限制住了。


环境变了,市场增速放缓,内卷又严重。在这种环境下,想跳槽,发现机会少,或者有机会也不一定能能接的住你的package。你身边有没有这样的“动弹不得”的朋友呢。


其实高薪,更多的是平台、行业红利带来的,毕竟互联网更容易形成规模。


但我们如果因为高薪,被高薪限制住了自己而畏手畏脚,舍不得放下眼前的利益,放弃更多的可能性,那我们自然会因为高薪,限制自己更长远的发展。


去探索那些不确定的东西


你可能想说,程序员不注重升职加薪,那注重什么呢?我是这么做的。


爱表达的人,先影响世界


第一点我想说的是,去找到自己喜欢或者擅长的事情,并坚持下去。


我探索的方向,是写技术博客,扩大个人影响力,做个人IP。


高中的时候买硬皮本子写,大学了买手帐写,工作了从印象笔记写,写日记、写感悟,开心了写,难过了写。


这是一件从不需要人督促我,但我缺断断续续坚持好多年的事情。


后来看了很多书,看了很多文章,有些文字真的很有力量,能让人感同身受,又能激励我去前进。


我也被一个个优秀的博主,不断的激励着,直到我自己迈出了这一步


我在低谷中,为了缓解焦虑,报名参加了技术人写作训练营,很快里面的内容就不满足我的输出,我又买来粥左罗老师的《学会写作2.0》,读了三遍


不知不觉间,写文章好像并不难了。一直困扰我的没人看怎么办,写的不好怎么办,写什么,怎么排版,怎么起标题,怎么写开头,怎么收尾,一点点的都被解决了。


半年前,我第一次认真的写了一篇文章,并发到掘金上。



半年后,我的文章,竟然上了掘金综合榜榜一,我到现在也觉着挺让人激动的。



我朋友常说,你写这些有什么用,赚到钱了吗?Enmmmm,我写下这句话就发给了他,刚刚发给他,他依然这么说。



借用明白老师文章里的一段话,来回答我为什么坚持。



当一个人能持续成长,包含了知识、思维、能力、心态、情绪、赚钱、关系、健康、感情等,并且他能把自己的成长过程,不断真实的分享出来,大家看到后,就会慢慢对他有信任感,他也就会慢慢拥有影响力。





还有一位朋友在一篇文章中提到我



保持真诚,保持利他,这个世界的规律是,当你在做一件帮助很多人成功的事情时,很多人会希望并帮助你成功,利他终利己。


见识更多人,试着了解可能


做技术的人,都有一个习惯,就是遇到技术难题,自己会苦苦钻研,查阅资料、阅读源码,对于技术的攻关、学习来讲,确实是对的。


但你我很容易就会把这个习惯,迁移到面对的人生其他问题上,小到买房买车,大到职业发展、人生选择,自己钻研很有可能会走很多弯路。


我在字节最累、最迷茫的时候,每次和我的mentor、leader聊完天,我都会有豁然开朗的感觉,因为他们走过你走过的路,对你的问题就是降维打击。


最近半年,在互联网上,了解、认识了好多大佬、朋友。


有做程序员副业社群的刘卡卡,看到了他一路做过来的经历,也在认识他之后,见识了他飞速成长、快速发展的一段时间。


还有已经作出成绩、完成转型的大佬托尼学长。


有和我一样在努力在公众号、掘金输出的朋友猿java、江天飞鸟、Goland猫、IT男的一人企业,每当想到有人在结伴前行,心中便不再孤单。


还有毕业三年,就靠小红书、闲鱼月变现1w+的读者朋友。


还有更多我认识他他不认识我的大佬,亦仁、芷蓝、靠谱、明白、雪梅。


我见识了太多种可能性,你可以做社群,可以做闲鱼电商、可以做面试辅导,可以做自媒体教练。你或许一个月可以通过互联网增加一万元甚至几万的收入,也可能通过互联网实现财务自由。


最重要的,我发现这些人可以自由选择喜欢的事情去做,而且做的很好。这不比只跟随市场需求走,逼迫自己做些不喜欢的事情,强太多了吗?


所以,认识更多人,学习他们的经验,同时这些人也是你的资源。在你苦恼、迷茫的时候去请教一下,聊聊天,你就能从不一样的角度看问题,甚至直接解决问题。就像你看到我,你可以认识我,有程序员方面的问题,也可以直接联系我。


保持头脑开放


赚钱的机会往往是开始于我们第一眼看不上、瞧不起的信息差。


比如说 大学期间,我曾经对微商嗤之以鼻。卖假货、朋友圈刷屏,太low了!
几年之后,当年做微商的,做得好的赚到第一桶金,做得不好,也积累了项目经验、私域用户。


工作后,我觉着那些整理面经的,没什么意思,不就是罗列了知识,照着书本上的内容,那可差的太远了,有这时间我看看书不好吗?


但整理面经的,在技术平台持续输出的那些人,不但积累了第一波粉丝,在那个快速发展的时间,很多人靠公众号赚到了第一桶金。


现在我开始接受一些知识付费,加入了一些社群,我让自己沉浸在一个有着各类机会的环境里,我尝试去看那些曾经我嗤之以鼻的小项目。


只有保持头脑极度开放,才能让各种信息流入。特别是对于一个想赚钱的人,开放的头脑意味着我们允许赚钱机会向我们靠近。


我之前一直不是一个头脑开放的人,所以现在我可能没有太好的经验和大家分享,但今年我一定会有所尝试,通过加入的社群去开阔眼界,也会把过程、收获分享给大家。


相信时间的力量


最后一点我想说,很多事情要慢慢来。


你不必因为别人的成绩而感到焦虑,也不要用当下进展的快慢,去定义以后是否能到达远方。


更不要忽视时间带来的力量,所有的积累都会在未来某一时刻回报给你。


说在最后


好了,文章到这里就要结束啦,很感谢你能看到最后。


当然,每个人的阶段不同,如果你工作5年内,还是把更多的精力放在晋升、加薪,因为你的空间还很大,未来一定不可限量,但不要让他成为你唯一的目标。


但工作5年以后,当职业生涯遇到瓶颈,你的人生还很长,不妨试着去探索更多的可能。




作者:东东拿铁
来源:juejin.cn/post/7367701663170592818
收起阅读 »

如何快速实现多行文本擦除效果

web
今天来实现一个多行文本擦除的效果,有种经典咏流传节目中表演开始前阅读诗句的一些既视感,在工作中其实也遇到过这样的需求当时是用的其他方法来实现的,现在发现了更简单的一种方法并且里面也涵盖了不少的知识点。 以上就是最终要实现的效果,比较敏感的同学呢应该能看到文本...
继续阅读 »

今天来实现一个多行文本擦除的效果,有种经典咏流传节目中表演开始前阅读诗句的一些既视感,在工作中其实也遇到过这样的需求当时是用的其他方法来实现的,现在发现了更简单的一种方法并且里面也涵盖了不少的知识点。


img1.gif


以上就是最终要实现的效果,比较敏感的同学呢应该能看到文本是由歌词组成的哈哈,没错今天是我偶像发新歌的一天,就用歌词来致敬一下吧!


思路


首先先来捋一下思路,乍一看效果好像只有一段文本,但其实是由两段相同文本组成的。



  1. 两段相同文本组成,这是为了让它们实现重合,第二段文本会覆盖在第一段文本上。

  2. 修改第二段文本背景色为渐变色。

  3. 最后对渐变颜色的背景色添加动画效果。


先来搭建一下结构部分:


<body>
<div class="container">
<p>
失去你以来 万物在摇摆 你指的山海 像玩具一块一块 我是你缔造又提防的AI 如果我存在 是某种伤害
不被你所爱 也不能具象出来 我想拥有你说失去过谁的 那种痛感 失去你以来 万物在摇摆 你指的山海 像玩具一块一块我是你缔造又提防的AI 如果我存在 只对你无害 想做你所爱 再造你要的时代 执行你最初设计我的大概
成为主宰 失去你以来 万物在摇摆 你指的山海 像玩具一块一块 也许我本来 就是种伤害 我终于明白 我根本就不存在 谁不在造物主设置的循环 活去死来
</p>
<p class="eraser">
<span class="text">
失去你以来 万物在摇摆 你指的山海 像玩具一块一块 我是你缔造又提防的AI 如果我存在 是某种伤害
不被你所爱 也不能具象出来 我想拥有你说失去过谁的 那种痛感 失去你以来 万物在摇摆 你指的山海 像玩具一块一块我是你缔造又提防的AI 如果我存在 只对你无害 想做你所爱 再造你要的时代
执行你最初设计我的大概
成为主宰 失去你以来 万物在摇摆 你指的山海 像玩具一块一块 也许我本来 就是种伤害 我终于明白 我根本就不存在 谁不在造物主设置的循环 活去死来
</span>
</p>
</div>
</body>

代码中两段文本都是由p标签包裹,第二段中加入了一个span标签是因为后面修改背景色的时候凸显出行的效果,这个下面加上样式后就看到了。


添加样式:


* {
margin: 0;
padding: 0;
box-sizing: border-box;
}

body {
background: #000;
color: #fff;
}

.container {
width: 60%;
text-indent: 20px;
line-height: 2;
font-size: 18px;
margin: 30px auto;
}

img2.png


现在只需要给第二段增加一个定位效果即可实现文本的覆盖:


* {
margin: 0;
padding: 0;
box-sizing: border-box;
}

body {
background: #000;
color: #fff;
}

.container {
width: 60%;
/* 直接加在父元素中即可对所有块级元素的子元素进行首行缩进 */
text-indent: 20px;
line-height: 2;
font-size: 18px;
margin: 30px auto;
position: relative;
}

.eraser {
position: absolute;
/* 这里等同于top:0 right:0 bottom:0 left:0 */
inset: 0;
/*
这里解释一下inset属性,inset属性用作定位元素的top、right、bottom 、left这些属性的简写
依照的也是上右下左的顺序。
例如:inset:1px 2px 等同于 top:1px right:2px bottom:1px left:2px
*/

}

image.png


那接下来就应该修改背景颜色了。


以上重复代码省略......

.text {
background: #fff;
}

这时候给span标签加上背景颜色后会看到:


image.png


而不是这样的效果,这就是为什么需要加一个span标签的原因了。


image.png


以上重复代码省略......

.text {
background: linear-gradient(to right, #0000 10%, #000 10%);
color:transparent;
}

image.png


下面要调整的就是将渐变里面的百分比变为动态的,我们可以声明一个变量:


以上重复代码省略......

.text {
--p:0%;
background: linear-gradient(to right, #0000 var(--p), #000 calc( var(--p) + 30px)); // 加上30px显示一个默认的渐变区域
color:transparent;
}

image.png


下面就该加上动画效果了,在设置动画时改变--p变量的值为100%


以上重复代码省略......

.text {
--p:0%;
background: linear-gradient(to right, #0000 var(--p), #000 calc( var(--p) + 30px));
color:transparent;
animation: erase 8s linear;
}

@keyframes erase{
to{
--p:100%;
}
}

但是这样写完之后发现并没有出现动画的效果,这是因为css动画中只有数值类的css属性才会生效,这里已经是一个数值了但--p还不是一个属性,所以我们要把他变成一个css属性,可以利用@property规则来帮助我们生成一个-xxx的自定义,它的结构:


@property 属性名称 {
syntax: '<类型>'; // 必须
initial-value: 默认值; // 必须
inherits: false; // 是否可继承 非必须
}

以上重复代码省略......

.text {
--p:0%;
background: linear-gradient(to right, #0000 var(--p), #000 calc( var(--p) + 30px));
color:transparent;
animation: erase 8s linear;
}

@property --p {
syntax: '<percentage>';
initial-value: 0%;
inherits: false;
}

@keyframes erase{
to{
--p:100%;
}
}

到此为止也就实现开头的效果了!!!


作者:孤独的根号_
来源:juejin.cn/post/7333761832472838144
收起阅读 »

超级离谱的前端需求:搜索图片里的文字!!难倒我了!

web
前言 大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心~ 背景 是这样的,我们公司有一个平台,这个平台上面有一个页面,是一个我们公司内部存放一些字幕图片的,图片很多,差不多每一页有100张的样子,类似于下面这样的图片 ...
继续阅读 »

前言


大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心~


背景


是这样的,我们公司有一个平台,这个平台上面有一个页面,是一个我们公司内部存放一些字幕图片的,图片很多,差不多每一页有100张的样子,类似于下面这样的图片



前几天上面大佬们说想要更加方便快捷地找到某一张图片,怎么个快捷法呢?就是通过搜索文字,能搜索到包含这些文字的图片。。。我一想,这需求简直逆天啊!!!!平时只做过搜索文字的,没做过根据文字搜索出图片的。。。。



思路


其实思路很清晰,分析出每一张图片上的文字,并存在对象的keyword中,搜搜的时候去过滤出keyword包含搜索文字的图片即可。


但是难就难在,我要怎么分析出图片上的文字并存起来呢?


tesseract.js


于是我就去网上找找有哪些库可以实现这个功能,你还真别说,还真有!!这个库就是tesseract.js



tesseract.js 是一个可以分析出图片上文字的一个库,我们通过一个小例子来看看他的使用方式


首先需要安装这个库


npm i tesseract.js

接着引入并使用它解析图片文字,它识别后会返回一个 Promise,成功的话会走 then



可以看出他直接能把图片上的结果解析出来!!!真的牛逼!!!有了这个,那我轻轻松松就可以完成上面交代的任务了!!!



实现功能


我们需要解析每一张图片的文字,并存入 keyword属性中,以供过滤筛选



可以看到每一张图片都解析得到keyword



那么搜索效果自然可以完成



搜索中文呢?


上面只能解析英文,可以看到有 eng 这个参数,那怎么才能解析中文呢?只需要改成chi_sim即可




如果你想要中文和英文一起解析,可以这么写eng+chi_sim





作者:Sunshine_Lin
来源:juejin.cn/post/7355554711167369268
收起阅读 »

大专生两年经验的年度总结

  🍉写在前面,00后程序猿(身高183!),今天是12.11,已经可以算的上是23年的末尾了。从21年8月开始入行,已经一坤年的时间了。 🍋年度目标 目标的话,基本是重复上年的操作,没有几条是达成的。 薪资目标        算是勉强到达年前的目标了吧 手...
继续阅读 »

  🍉写在前面,00后程序猿(身高183!),今天是12.11,已经可以算的上是23年的末尾了。从21年8月开始入行,已经一坤年的时间了。


🍋年度目标


目标的话,基本是重复上年的操作,没有几条是达成的。



  1. 薪资目标        算是勉强到达年前的目标了吧

  2. 手写promise全部规范   总体流程是可以的,个别规范还未实现,算成功85%

  3. react          不好估计,是学完了,但是一直没有完整的demo项目,算完成70%

  4. java          只会java基础,算完成15%左右

  5. 《vue的设计与实现》  仅把书买了,进度0%

  6. 个人博客系统      前端完成七七八八了,由于java的原因后端没写,前端完成度:80%,后端:0%

  7. 自己组装个台式电脑   完成完成,这种目标肯定是第一时间完成的

    看了一下上面的目标和完成度的对比,今年真的蛮失败的,尤其是工作上,这个等文章后面再提。


先来说一下组装台式机的过程


  为什么首先写这个呢?肯定不是因为我想炫耀。电脑是四月份开始决定组装的,依据贴吧老哥和自己的要求在JD上选了一些配置(3060ti g6x+12600kf),机箱选的罗宾3,总体体验还不错,尤其这9个棱镜风扇,风扇当初考虑了半天,因为已经买了水冷,只打算再买三个风扇,毕竟我是第一次装机,风扇太多我不一定装的好,后面一想(风扇肯定是越多越帅啊),就多入手了6个风扇。后面又多加了一个发光的显卡支架,最终成了下面的样子(手机像素不行,电脑实物还是很帅的)。


86DE025CB6BFCC2B2E116C511C5EBAD7.jpg


2023这一年我是如何实现上面的目标的?


  工资这方面,没什么好说的,肯定是正常的面试吹嘘。上面列出的几条技术要求,一般都是工作闲的时候写一写,没有刻意要求自己(可能正是这种心态,导致了那么多目标没有完成)。下面的图片便是我今年的所有收获。其实还有一个小目标完成了,上面没有提到,那就是锻炼身体,毕竟钱再多,技术再好都不如身体重要,晚上都会去小区广场那边进行跳绳,从起初的一天500已经变成一天3000个^_^


image.png


然后就是这两年就职的公司情况


  一坤年的时间,我已经入职了三家公司,离职的原因都是一些不可抗力因素。



第一家:南京,公司是做自研项目的,开发团队有10个人左右,前端最多的时候是4个前端,单休,每周三下午固定时间有下午茶,工作很辛苦,但是公司氛围很不错,每个人都很好沟通,都很照顾我。后面离职是因为老板认为南京这边人力成本有点高,把公司搬回老家了。




第二家:南京,公司算是做自研项目的,为什么说算是呢?这边的主要业务是做自己的项目,然后把这个项目的核心内容卖出去(嵌套到甲方的项目中),当时入职时就我一个前端,4个后端,老板本身也是后端,一个测试,大小周,一般是同时进展两个项目(老板和领导能力比较强,他俩负责一个项目,我和另外两个后端负责另一个项目),每周三下午是固定的羽毛球运动,小零食管饱,公司氛围同第一家一样很好,每个人都很好沟通,第一天入职时,老板会请吃饭,一般是一个项目结束后会团建聚餐一次。后面离职是因为当时公司暂时没项目,老板和我们讨论:他想降低整体的薪资(是讨论,并不是那种直接通知),我和几个同事都能理解老板,但是都表示不能接受(这是很现实的事情),老板最终给予了我们三个n+1(已经很好了)。`




第三家:依旧在南京,也就是现在在职的一家公司,仍然是自研,工作内容十分轻松,一共是三个开发,年终奖、午餐、双休这些都有,并且项目已经很成熟了,基本不会有什么大改动(至少目前是,来这边一年了都是一些小问题的修改)。并且公司基本不会存在倒闭的问题(老板的其它业务需要这个系统,并且其它业务十分赚钱,老板是身价很高的那种人)。从上面的几条内容来看,这个公司应该是大多数人心中不错的公司了,但是可能有人会发现我没有提到公司氛围这个内容:

  接下来我要提的便是氛围了,我只能说差,并且不是一般的差。为什么这么说呢?首先我们是只有三个开发,按常理来说:人越少,氛围会越好,但是我们办公的地方很独特,我们是同公司其它业务的人一同办公的,都是在同一个场地办公,问题的一小部分就出现在这,其它业务的人员都是官职很高的老领导(你可以这么理解:和你一起办公的都是清华大学、北京大学各大高校的退休校长、退休教授),如果是同样业务,那倒很好,说不定咱还能攀攀别人的关系的,可惜不是,并且我是基本不敢进行激情讨论的,我这边的领导怕激情讨论影响他人。并且在这个公司让我学会了语言的艺术,勾心斗角的人是真TiMi的多,到处都是人情世故、阿谀奉承(排除个别不是的),这个时候可能又会有人说,小团队氛围干嘛要受别人的影响呢,做好自己就可以了。

  对于上面的问题,我只能说,哥们,真不能怪我们,我本人还是蛮开朗的,在我之前的两位同事还未离职的时候,我们三个的氛围也算还不错吧,后面他俩因为种种原因离职了。后面就入职了另一个同事,可能是性格问题,我们之间的沟通很少,除了对接口的时候会说几句话。可能又有人会说了,领导主持一次聚餐,大家互相熟悉一下不就好了吗。看到这种,只能微微一笑,我可以这么给你解释我的领导,我之所以能学会人情世故、阿谀奉承都得去谢谢他。对外他是唯唯诺诺,对内是重拳出击。变脸比翻书还快,上级对他有好脸色,他对我们不一定有好脸色,上级对他没好脸色,他对我们必定是重拳出击(这个哥们让我见识到了心态可以决定年纪,因为他真会装孙子)。比活火山还离谱,事情多的时候嫌我们做的不行、做的慢,事情少的时候,嫌我太闲,怎么看怎么不顺眼,我是真提莫的无语了。并且这个人十分喜欢讲冷笑话(至少我是这么认为的),十分冷的那种。



image.png


image.png


  上面两张截图,是我周末在外面玩,然后领导叫我来加班的乌龙事情,我只能说:罕见,这种罕见的极品是怎么混上领导的(我刚入职的时候只有他一个光杆司令)。工作中的甩锅事情我就不想说了,因为根本说不过来(我不知道这个甩锅在其它公司多不多),在我之前的公司出了问题基本都是领导自己揽下来(无论是不是他的问题),并且这种人我们都很听他的,也很佩服这种人。但是这个极品就不一样了只要是对外演示的时候,无论是谁的问题,永远是甩锅给我们,绝对不能因为这个问题影响他装逼,是真极品。日常中还有更极品的事情我就不写了(只能说比三国杀还恶心)。


🍍日后的计划


  换工作,年后必须先把工作换了,哪怕是裸辞,这种极品领导很难相处。其次是java,java已经学了两年了,去年的计划就已经有java了,一直没有学,2024一定要学会。然后就是爬武功山,是真想去那看看,另一个目标的话就是要攒一部分钱出来,要把买车提上日程。


作者:外围前端吴彦祖
来源:juejin.cn/post/7310964581052121100
收起阅读 »

设计呀,你是真会给前端找事呀!!!

web
背景 设计:我想要的你听明白了吗,你做出来的和我想要的差距很大,你怎么没有一点审美(你个臭男人,你怎么不按我画的做)! 我:啊?这样自适应不是很好吗,适配了大部分机型呀,而且不会有啥显示的兼容性,避免不必要的客户咨询和客户投诉。 设计: 你上一家公司就是因为...
继续阅读 »

背景



  • 设计:我想要的你听明白了吗,你做出来的和我想要的差距很大,你怎么没有一点审美(你个臭男人,你怎么不按我画的做)!

  • :啊?这样自适应不是很好吗,适配了大部分机型呀,而且不会有啥显示的兼容性,避免不必要的客户咨询和客户投诉。

  • 设计: 你上一家公司就是因为有你这样的优秀员工才倒闭的吧?!

  • :啊?ntm和产品是一家的是吗?





我该如何应对


先看我实现的


b0nh2-9h1qy.gif


在看看设计想要的


9e2b0572-aff4-4644-9eeb-33a9ea76265c.gif
总结一下:



  • 1.一个的时候宽度固定,不管屏幕多大都占屏幕的一半。

  • 2.俩个的时候,各占屏幕的一半,当屏幕过小的时候两个并排展示换行。

  • 3.三个的时候,上面俩,下面一个,且宽度要一样。

  • 4.大于三个的时候,以此类推。



有句话叫做什么,乍一看很合理,细想一下,这不是扯淡么。



所以我又和设计进行了亲切的对话



  • :两个的时候你能考虑到小屏的问题,那一个和三个的时候你为啥不考虑,难道你脑袋有泡,在想一个和三个的时候泡刚好堵住了?

  • 设计: 你天天屌不拉几的,我就要这样,这样好看,你懂个毛的设计,你知道什么是美感和人体工学设计,视觉效果拉满吗?

  • :啊?我的姑奶奶耶,你是不是和产品一个学校毕业的,咋就一根筋呢?

  • 产品:ui说的对,我听ui的。汪汪汪(🐶)


当时那个画面就像是,就像是:





而我就像是
1b761c13b4439463a77ac8abf563677d.png


那咋办,写呗,我能咋办?



我月黑风夜,
黑衣傍我身,
潜入尔等房,
打你小屁屁?



代码实现


   class={[
'group-even-number' : this.evenNumber,
'group-odd-number' : this.oddNumber,
'themeSelectBtnBg'
]}
value={this.currentValue}
onInput={(value: any) => {
this.click(value)
}}
>
...


   .themeSelectBtnBg {
display: flex;
&:nth-child(2n - 1) {
margin-left: 0;
margin-right: 10px;
}
&:nth-child(2n) {
margin-left: 0;
margin-right: 0;
}

}
// 奇数的情况,宽度动态计算,将元素挤下去
.group-odd-number {
// 需要减去padding的宽度
width: calc(50% - 7.5px);
}

.group-even-number {
justify-content: space-between;
@media screen and (max-width:360px) {
justify-content: unset;
margin-right: unset;
flex: 1;
flex-wrap: wrap;
}
}

行吧,咱就这样吧




作者:顾昂_
来源:juejin.cn/post/7304268647101939731
收起阅读 »

完蛋,我失忆了,记一次团建的翻车之旅😵

0x0. 背景介绍 本文首发于我的同名公众号,「野生的码农」。 转眼间,距离上篇文章的发表已经1个多月了,正当我纠结于再写点什么的时候,一场突如其来的事故,让我短暂地失忆了。不久后,流感来袭,发烧咳嗽了许久,完全打断了我的计划。现已基本康复,就分享下团建而引发...
继续阅读 »

0x0. 背景介绍


本文首发于我的同名公众号,「野生的码农」。


转眼间,距离上篇文章的发表已经1个多月了,正当我纠结于再写点什么的时候,一场突如其来的事故,让我短暂地失忆了。不久后,流感来袭,发烧咳嗽了许久,完全打断了我的计划。现已基本康复,就分享下团建而引发的「血案」,顺便聊聊一些倒霉事,博君一笑😁。


2015年6月,刚到腾讯不久,在张北草原团建,有个斗鸡的热身环目,就像这样:


斗鸡


不同的是,我们是在水泥地上开斗的。组里有个身高1米85的大长腿同事,可能因为重心太高或者忌惮对手是领导,被斗输,摔倒在地。看上去摔的并不重,但他脸色不太好,走路一瘸一拐的。


次日,返回北京,积水潭医院,诊断为股骨骨折。先后做了两次大手术,需要卧床休息很久,印象中休了近1年的假。屋漏偏逢连夜雨,他受伤时老婆快生了,算是陪他老婆休了个产假。


因为是在团建中受伤的,走的工伤保险报销医疗费和申请假期。万万没想到,码农有朝一日也能用上工伤保险。


说到保险,当初我老婆生娃,我问 HR 能否用我的生育保险申请北京的什么津贴,她说刚取消了。好吧,那我的生育保险能干啥?答曰:



可以报销男性的输精管结扎术的费用



啊?这。。。此时无声胜有声。。。再见。。。


自「斗鸡」团建之后,我拒绝参加可能有风险的活动,只参加爬山、散步、扯淡之类的「老年团」了,我可不想再休个产假。


10月底,组里在筹划「鬼屋探险」主题的剧本杀团建,在看了宣传片后,我怂了,立刻放弃。


虽然我本科是学医的,多次接触过大体老师,是坚定的无神论者,但我不敢看恐怖片。初中,不小心和老表们一起看了《山村老尸》,连续多个晚上,刚闭上眼就开始放电影,很久才能睡着。某天,房间突然出现一滩水,和电影里的情节一样,吓的我以为女鬼要来杀我了。。。


在我退出之后,陆续又有几位老哥跑路,组织的同学又贴心地攒了个「泡温泉」的老年团,而且还有按摩😍。鹅妹子嘤!这个不戳,零风险,带条泳裤即可,冲!


美中不足的是,稍微有点远,接近70多公里。出发前一天,统计拼车信息,顺路的几位老哥,都跑去捉鬼了,我只能单飞了。温泉在隔壁的巢湖市,不走高速的话,沿途会经过巢湖,应该会有些不错的风景,遂决定骑摩托车去,也就1个多小时。


巢湖


同事问我是否确定骑车去,必须确定啊,小 case。去年7月,历时两天,先后经历暴晒和暴雨的「冰火两重天」,把我的125踏板摩托车从北京骑回合肥,接近1200公里,我每年骑车都超过7000公里,区区70公里,何足挂齿?


然鹅,自认为老司机的我,终究还是翻车了,温泉团建之后,我就基本没碰过摩托车了。有意思的是,至今我都不知道车祸是怎么发生的,那段记忆完全丢失了😵‍💫。。。


0x1. 案发经过


10月27日,周五,阴,20℃,上午10:50,我拿着手机,一脸懵逼地站在一个陌生城市的某大道的辅路上。旁边躺着我的小踏板,后视镜和转向灯摔烂了,手机支架掉在地上,外套、护膝、鞋子都有损坏,手臂、胳膊、膝盖等多处擦伤,正在流血,身上也有点疼,但我完全想不起来发生什么了。


扶起踏板后,发现旁边工地有个保安一直在朝我这边看,遂走上前咨询刚才发生了什么事。他说我自己摔倒了,没有人撞我,我也没撞到人,提醒我满脸是血,让我赶紧去医院。打开前置摄像头看了下,嘴巴肿了,下巴紫了,脸上有几个出血点,皮肉伤,破相而已,不碍事,大不了去挂个「丑科」。


丑科


比起伤痛来说,此刻,我更想知道的是:



我是谁?我在哪?我要去干啥?



走到路边,找块空地坐了下来,试图想起点什么。发现当天早上的部分记忆有问题,时常出现「该内存不能为 Read」,而之前的记忆都可任意读取。


打开微信看了看,有个团建群在活跃着,依稀想起来今天是去泡温泉,但记忆非常模糊,像是在回忆几十年前的事情,又非常像是在做梦。打电话给同事确认了团建的事,我在原地待命,他开车过来接我。


等待的间隙,打电话给老婆,告诉她我因为车祸失忆了,今天早上的事记不清了,问她我是否来泡温泉的。我特意强调了我没在开玩笑,这略带搞笑的提问,似乎把她给吓到了,问我是不是脑子摔坏了。我随后补充道问题不大,我还记得她跟儿子,也记得我父母,同事在过来的路上了,一会就去医院。在确认了我的安全后,她问我工资卡密码,啊,我失忆了,不记得了,再见👋🏻👋🏻


同事到了后,跟刚才的保安又简单聊了下,得知工地上没有摄像头。如果报警的话,应该能调取附近的监控,我嫌麻烦,毕竟除了失忆,也没啥大事,就不麻烦警察蜀黍了。


前往附近医院,急诊,CT,万幸戴了头盔,没有脑出血。急诊医生说他看片不专业,是放射科医生出具的报告。遗憾的是,忘了拿 CT 的报告单了,也不知道上面写的啥,现在只有病历能证明我曾经「脑子坏了」:


病历


完事后,同事帮忙买了碘伏和湿巾,往伤口喷了喷,嘶~,嘶~~,我靠,这么疼😭。消毒时,才发现腹股沟和肚子也有多处血肉模糊的,虽然有点疼,但也都是皮肉伤,并无大碍。


幸好当天气温不高,穿的比较厚,戴了手套,穿了护膝,身上只有一些表面的擦伤。最重要的还是戴了头盔,否则团建就要改吃席了。我记得最后一次看导航是10:30左右,联系同事是10:50,所以我很可能倒地后昏迷了一段时间。


去年从北京骑回合肥,头盔是唯一的装备,连手套都没有,还在高速上跑了300多公里,差点被大车撞到,现在想想都后怕。刚到家时,问我老婆我是否很牛逼,她回复道:



我觉得你是傻逼



彼时,我觉得她不懂我的爱好,话不投机半句多。现在,我认可她的说法。两天,骑行1200公里,确实很牛逼,但也是一种不负责任,毕竟上有老下有小的,没资格去冒险做这种「装逼」的事了。


继续说回团建,虽然下巴和嘴唇受伤了,应该不影响吃饭吧,而且中午有大餐,正好补补身子。出发,干饭🍚。


0x2. 参观温泉


干饭完毕,前往温泉度假酒店。我这一身伤,肯定是不能下水了,本着来都来了的原则,土鳖的我决定进去涨个见识,至少能看看温泉是啥样的。


好么,原来温泉就是公共澡堂子啊,还不区分男女。有意思的是,这澡堂还是露天的:


露天温泉


可能是工作日的缘故,人很少。实话说,我觉得所谓温泉,其实是电加热的(泉)水,不过我没有证据,如果我错了,就当我胡说吧。


澡堂的种类还挺多的,区别就是添加剂不同,有红酒澡堂、牛奶澡堂、玫瑰澡堂等等,建议老板可以再搞个酱香澡堂,说不定会门庭若市。室内有个澡堂养了些小鱼,可去除腿部和脚上的死皮,就是不知道小鱼是否会觉得恶心🤮


同事们泡完澡,去二楼按摩,问我是否一起。拒绝,我当时浑身都在疼,别给我送走了。后来,听他们描述按摩的过程,还挺有意思的,此处省略18万字,付费后可见🐶。


17:00,约了辆货拉拉,正好有同事要提前走,搭他的车去了事故地点。其他同事留下来,晚上又搓了一顿(饭)。


19:00,我和踏板到家了。


货拉拉


就这样,我参加了团建,但又没有完全参加。


啊!这是一次多么难忘的团建啊!!虽然我失忆了,但这次刻骨铭心的团建将永远在我的脑海中「阴魂不散」!!!这一刻,感觉脖子上的工牌都更加鲜艳了,唯一不好的是,工牌是被我的鲜血染红的😎。


写到这,想起了另一件悲催的事:


7月底,带孩子去海边玩,买了条泳裤。傍晚到达目的地后,去附近熟悉下环境,然后,我就摔倒在海边的礁石上了。胳膊破了一大块,碰到海水就生疼。次日,只敢在浅水区站着,泳裤几乎没碰到水。温泉团建,泳裤一直躺在踏板的坐垫下,都没能出来看一眼温泉。。。


0x3. 第二滴血


因为后视镜摔碎了,上路非常危险,手上的伤口尚未愈合,气温也在逐渐走低,今年基本是告别摩托车了,只能被迫开着「仅摇号一年就中标的京牌油车」上下班了。


团建后的第1个工作日晚上,开车前往合肥的「华强北」--「大钟楼」,把车停在城泊画的停车位上。拿到送修的 iPad 后,在开出车位时,看到左侧有个电动车飞奔而来,立刻踩下了刹车。在我静止了3秒后,他还是直接撞了上来,摔倒在地。实话说,我怕他讹我,没有立刻下车。


对方是个年轻小伙,让我帮忙扶一把,说起不来,应该不是碰瓷的,遂下车把他的电动车扶了起来。随后,他自己站起来了,膝盖破了一大块,还在流着血。小伙看着学生模样,戴着个耳环,抽烟,看着还算实诚。我说我停着在啊,你咋还撞上来了?他回答说「我知道,我知道,接电话没注意到」。


我问他伤势怎么样,是否要去医院看看。他说没事没事,不需要去医院,他去找朋友有急事,让我直接走。我不放心,我不知道这算谁的责任,万一他后面有啥事,我这算是肇事逃逸吗?我跟他说先别走,还是让专业的人来处理吧,遂报警并打了保险公司电话。


等待的间隙,看了下车损情况,前保险杠被撞变形了,松松垮垮的,有个雷达被撞脱落了:


撞车现场


很快,交警就来了,出示正件,拍照,如实反映情况,小伙说他是在打电话没注意到汽车。流程走完后,交警说情况复杂,他定不了责,需要我和小伙一起前往某指定的地点定责。


啥??还有交警定不了责的?难道是我全责?我已经停车了啊,交警说那肯定不是。我又问道,如果我自认全责,保险公司是否会认可?交警笑道,那当然可以,保险公司是以交警的结论来赔付的,但是没必要,我不是全责,会影响保费。


交警说我们自己协商也行,但是估计协商不出来,还是一起去定责吧。确实,我不知道责任如何划分,也不知道修车费用,完全没法协商。


小伙一脸诚恳地说肯定会承担修车费用,我们互留了联系方式,后面再约时间一起去定责。交警特意提醒了我们,任何一方都有义务协助对方完成定责,否则可能会被追究法律责任。


搞定,各回各家。然后,就没有然后了。


消失的他


次日上午,微信问他是否需要去医院,傍晚回复我没啥事;傍晚约定责时间,久未回复,电话之,不接,凌晨回复我待定。


考虑到定责还要请假,我那破车也不值钱,就近找了个便宜的修车店,150元就搞定了。我没指望他全付,随便给多少都行,微信之,让他看着给。整整一天,没有回复,难道他没看懂「看着给」的意思?


修车后的第2天,打了两个电话给他,都被挂断,微信回复我说要等发工资。罢了,我自认倒霉,不想为这点钱再浪费时间了。原打算说他两句的,想想还是算了,不值得。


本来,这里是放了聊天记录截图的,最后整理文章内容时删除了,没必要。


很久之后,我想到一件事,他骑的是美团共享电动车,应该自带保险?很可能根本不需要他掏钱,我懒得去操心了,就酱。


果然,不要考验人性,男人都是大猪蹄子🐷。


0x4. 第一滴血


现在回想起来,那段时间一直在倒霉,交通工具陆续因为意外而罢工,只能靠「11路公交车」了。但是,很不幸,最先遭殃的,恰恰就是我的大长腿。


在团建的前两周,周一早上,送孩子上学,电梯死活不来,马上要迟到了,只能走楼梯了。因为走的很着急,没注意到一楼大堂里的平板车,发生了下面悲剧的一幕:




  • 地上有辆平板车,平板车前面有个30多岁的女人摔倒了,面部朝下

  • 平板车后面有个30多岁的男人摔倒了,面部朝上

  • 平板车附近有一些液体的药渍

  • 平板车右侧是一扇玻璃门,玻璃门外站着一个小孩,一脸茫然地望着里面的男女。



我让 AI 帮我画出上面的场景,很快啊,它画了4张图:


AI 画的图


呃,它应该是理解错了平板车,我补充说明「平板车」是用来临时拉货的小推车,它又重新画了两幅图:


AI 画的图2


得,叫狗不如自走,这画的都是啥啊,驴头不对马嘴的,只能老夫亲自出马了,上述惨案现场的真实还原图如下:


惨案现场


惨案的复现步骤如下:




  1. 我踩到了平板车尾部,摔倒,右腿撞到了车尾

  2. 平板车动起来了,撞到走在前面的老婆,摔倒

  3. 老婆手里端着一杯液体的药,摔倒时洒落一地



我骂骂咧咧地站了起来,忍着右腿的疼痛,一脚踹向了平板车。只见平板车不急不慢地驶向了玻璃门外,不偏不倚地撞到我儿子腿上了,尼玛。。。


送完孩子后,返回家中,看了下伤口,右侧大腿,有10cm的划痕,在出血,估计是碰到了平板车的拐角了。在微信上跟物业说了此事,回复了个捂脸的表情;问我是啥车,回复道挺大的一辆铁车,继续捂脸。他这是觉得我是傻逼?我本来是想调监控看下当时发生了啥,为啥会没看见那么大的车,算了,自讨没趣,喷了点酒精和紫汞就上班去了。


当晚,划痕的「线」就演化成了「面」,大腿内侧接近1/5的皮肤都是瘀伤的红色,挺吓人的,就不放照片了。应该没啥大事,肯定是没伤到骨头,就没去医院了。


几天后,物业主动联系我,说他前几天在休息,今天刚回来,现在找到了当初放平板车的人,让我把伤势说的重一点,可以多索要一些赔偿。我回复道,不用了,我不交物业费就是了,你也别再找我了,然后我就挂断了电话。


后来我才知道,是我老婆跟物业总公司交涉后,物业才找我的。特么的,我缺那点赔偿?物业这工作态度,感觉是我为他服务似的。原来,我都是提前预存一年的物业费,看来我脑子早就坏了。


好在,老婆孩子的伤势较轻,没多久就好了。我到现在也没完全恢复,腿上的大片红色已经褪去,平时完全没感知,但是按上去还有点疼,只能等着时间来磨平伤痛了。


0x5. 总结 & 体会


半个月前,我又去了趟巢湖,喝喜酒。高速入口,取卡,发现左侧胳膊不能完全抬起来,稍微用点力就巨疼无比。其实,自从团建骑车摔倒后,左侧肩膀一直在疼,因为不影响日常活动,我就没在意。


原来那次事故,除了「失忆」外,还摔出了隐藏 bug,只是需要特定的姿势才能复现:



坐着,手臂外展,向上抬起



拍了个核磁共振,问题不大,肩袖损伤,保守治疗,开药,外敷内用。一周后,依然疼痛,复查,医生说等它自己慢慢恢复吧,也不需要再来复查了。「伤筋动骨一百天」,古人总结的经验数据,多少还是有点道理的。


前几天,骑车去修后视镜和转向灯,因为路太烂,手机从支架上掉了下来,可能是情景再现,依稀想起了一点事。当初貌似是为了躲避路上突然出现的大坑或什么东西,双手捏死了刹车,因为车速较快,又没有 abs,然后就翻车了。但是记忆非常模糊,我也不知道是真实发生还是自己的心理暗示。


简单算了下,因为骑摩托车参加团建,我多花了2000余元,也不知道踏板能否卖这么多钱:




  1. 外套:300元

  2. 货拉拉:150元

  3. 摩托车后视镜 + 手机支架 + 转向灯:200元

  4. 汽车保险杠:150元

  5. 头部 CT:260元

  6. 核磁共振 + 医药费:1000元



整个团建,我只参加了吃饭,然后看人泡澡,听人描述按摩。我这算是响应刺激消费的号召,创造了2000元的 GDP 吗?


你很机车耶


在我「失忆」后不久,我的台式机也「失忆」了,这个机器是为组内提供 Crash 堆栈在线解析服务的,服务部署在 WSL 里。某天,WSL 的文件系统突然变成只读的了,不能创建和修改任何文件,也即无法产生新的记忆了。


我怀疑是所谓的安全软件搞的鬼,联系 IT,回复说要等更高权限的人来处理。我等不及了,尝试了微软官网提供的修复方法Read-only fallback error


so easy,只需3个命令,一顿操作猛如虎后,WSL 彻底凉凉了,完全启动不了了,之前好歹还能进入系统。。。


其实,Crash 除了表示「崩溃」,还有「车祸」之意,也就是说:



Crash 后的我,让解析 Crash 堆栈的机器 Crash 了



宕机后的几天,我可能中招了甲流或支原体,39℃,周末连续烧了3天,忽冷忽热的,又体验了一把「冰火两重天」。


周一,还在发着烧,但感觉好了不少,就去公司了。发烧时精神状态不好,代码肯定是写不了了,但来都来了,索性乘着一股热劲,花了一天时间,把之前的坑又踩了一遍,重新安装了 WSL 并搭建了环境,有种失而复得的感觉。这次,我把坑记录下来了,防止哪天又 Crash 了。


看了下安全软件的日志,某天强制重启了电脑,估计是突然断电导致没有正确关闭 WSL,把 WSL 的文件系统搞坏了,变成只读了。我猜测,车祸后的失忆也是类似的,因为摔倒后大脑突然断电,导致Cache在大脑RAM里的记忆没能及时FlushDisk上,那段记忆就这么永远丢失了。


不同的是,电脑挂了可以重装,人要是挂了,再也没有机会重来了,直接少走几十年弯路。


这次「失忆」的经历,既是不幸,也是万幸。万幸在没有酿成大祸前,让我深刻地理解了随处可见的标语:



道路千万条,安全第一条



最后,也请读者朋友们时刻牢记「安全第一」,包括但不限于走路、骑车、开车,祝大家在人生的旅途中一路平安。


道路千万条,安全第一条


作者:野生的码农
来源:juejin.cn/post/7310423310167916594
收起阅读 »

一个排查了一天的BUG,你在摸鱼🐟吧!

web
站会 在一次日常站会上,组员们轮流分享昨天的工作进展。一个组员提到:“昨天我整天都在排查一个BUG,今天还得继续。” 出于好奇,我问:“是什么BUG让你排查了这么久还没解决呢?” 他解释说:“是关于一个数据选择弹窗的问题。这个弹窗用表格展示数据,并且表格具有选...
继续阅读 »

站会


在一次日常站会上,组员们轮流分享昨天的工作进展。一个组员提到:“昨天我整天都在排查一个BUG,今天还得继续。”


出于好奇,我问:“是什么BUG让你排查了这么久还没解决呢?”


他解释说:“是关于一个数据选择弹窗的问题。这个弹窗用表格展示数据,并且表格具有选择功能。问题在于,编辑这个弹窗时,表格中原本应该显示为已选状态的数据并没有正确显示已选状态。”


我猜测道:“是不是因为表格中数据的主键ID是大数值导致的?”


他回答说:“大数值?我不太确定。”


我有些质疑地问:“那你昨天都是怎么排查的?需要花一整天的时间,难道是在摸鱼吗?”


“没有摸鱼,只是这个BUG真得有点难搞,那个什么是大数值?”


“行吧,姑且信你,我待会给你看看。”


排查


表格使用的是 Ant Design 4.0 提供的 Table 组件。我检查了组件的 rowKey 属性配置,如下所示:


<Table rowKey={record => record.obj_id}></Table>

这表明表格行的 key 是通过数据中的 obj_id 字段来指定的。随后,我进一步查看了服务端返回的数据。


image.png

可以看到一条数据中的 obj_id 字段值为 "898186844400803840",这是一个18位的数值。



在ES6(ECMAScript 2015)之前,JavaScript没有专门的整数类型,所有的数字都被表示为双精度64位浮点数(遵循IEEE 754标准)。这意味着在这种情况下,JavaScript能够安全地表示的整数范围是从253+1-2^{53} + 125312^{53} - 1(即-9,007,199,254,740,991到9,007,199,254,740,991)。可以简单地认为超过16位的数值就是大数值。



JavaScript中很多操作处理大数值时会导致大数值失去精度。比如 Number("898186844400803840")


image.png


可以看到 "898186844400803840""898186844400803800" 的区别在第16位后,从 40 变成 00 这就是大数值失去精度的表现。


在看一下表格的数据展示,如下图所示:


image.png


可以确定的是,从服务端返回的数据到在表格中的渲染过程是没有问题的。那么,可能出现问题的地方还有两个:一是在选择数据后,数据被传递到父组件的过程中;二是父组件将已选数据发送回选择数据组件的过程中。


定位


我检查了他将数据传递给父组件的逻辑代码,发现了一个可疑点。


image.png

在上述代码中,JSON.parse 被用来转换数据中的每个值。在这个转换过程中,如果 item[key] 是以字符串形式出现的数值,并且这个字符串能够被 JSON.parse() 解析为 JSON 中的数值类型,那么 JSON.parse() 将会把它转换为 JavaScript 的 Number 类型。


这种转换过程中可能会出现精度丢失的问题。因为一旦字符串表示的数值的位数超过16位后,在转换为 Number 类型时就无法保证其精度完整无损。


解决


我们通过正则表达式排除了这种情况,如下所示:


newItem[key] = typeof item[key] === 'string' && /^\d{16,}$/.test(item[key]) ? 
item[key] :
JSON.parse(item[key]);

经过修改并重新验证,问题得到了解决,数据选择弹窗现在可以正确展示已选择状态。


image.png


反思


这个表面上不起眼的BUG为何花费了如此长的时间来排查?除了对大数值的概念不甚了解外,还有一个关键原因是对JavaScript中可能导致大数值失去精度的操作缺乏深入理解。


大数值通常由两种表示方式,一个是用数值类型表示,一个是字符串类型表示。


如果用数值类型表示一个大数值,而且你不能直接修改源代码或源数据,这种情况比较棘手,因为一旦 JavaScript 解析器处理这个数值,它可能已经失去了精度。


这种情况通常发生在你从某个源(比如一个API或者外部数据文件)接收到一个数值类型的大数值,如果数据源头不能修改,只能使用第三方库lossless-json、json-bigint来解决。


如果用字符串类型表示一个大数值,在JS中只要有把其转成Number类型的值就会失去精度,不管是显式转换还是隐式转换。


显式转换,比如 Number()parseInt()parseFloat()Math.floorMath.ceilMath.round等等。


隐式转换,比如除了加法外的算术运算符、JSON.parseswitch 语句、sort的回调函数等等。


作者:前端大骆
来源:juejin.cn/post/7348712837849284644
收起阅读 »

被裁员半年了,谈谈感想

后端开发,22年9月,跳槽到某新能源生态企业,23年3月中旬的某个周一下午,被HR通知到会议室做个沟通,两周前收到转正答辩PPT模板让我填写,原本以为是做转正答辩的相关沟通,结果是沟通解除劳动合同,赔偿N+1,第二天就是lastday。 进入公司后经历了几次组...
继续阅读 »

后端开发,22年9月,跳槽到某新能源生态企业,23年3月中旬的某个周一下午,被HR通知到会议室做个沟通,两周前收到转正答辩PPT模板让我填写,原本以为是做转正答辩的相关沟通,结果是沟通解除劳动合同,赔偿N+1,第二天就是lastday。

进入公司后经历了几次组织架构调整,也不断变化着业务形态,但本着拥抱变化的心态,想着会越来越好,所以对于这个突发状况毫无准备。


心路历程


首月


刚刚经历裁员,下个月会有工资、奖金和赔偿金入账,赔偿金不扣税,同时对于市场环境没有了解,比较乐观。首月的想法就是写简历,并开始投递,先投不想去的公司找面试经验;找学习资料、刷题;期望薪资是不需要涨薪,大概平薪就行。

首月面了三家公司,发现了自己的诸多漏洞,项目比较垂类,讲解过程混乱;基础知识复习不足,很多新出来的延展概念了解不够。


第二个月


上个月期盼的奖金到账了,有些庆幸,又有些失落。庆幸的是收到一笔不菲的补偿金,失落的是下月开始就没有收入了。

发现面试机会变少了,整月才面了三四家,这个月发现的问题,更多的是从架构角度来的,诸如幂等、一致性hash等场景,个人了解的相对简单了。


第三个月


广深的工作机会实在是少,开始同时投递其他城市的岗位试水。月初一家公司现场面了4轮都很顺利,第二天最后一轮CTO面,被嘲讽之前业务简单,比较受打击。月底面其他城市的岗位,一面过后第二天晚上10点又被拉上线做一面的补充面。

开始焦虑了,一想到还没找到工作,补偿金估计一两个月也会花完,可能要动用积蓄了,心跳就加速,越想越加速。努力想让自己变得不去想,只去想没有掌握的知识点,算是熬过了这个月。


第四个月


这个月,感觉蛮顺利,月初面一家大厂,技术面、主管面、HR面、提交资料都很顺利,感觉稳了,每天都看看公众号的面试状态,希望能快点沟通offer;月中也走完了一家中厂的4轮面试流程;月底又走完了另一家新能源车企的面试流程。

整个月过完,自己感觉飘了,感觉同时手握3个offer机会,晚几天随便一家给offer call就去了。个人心态一下子就变了,月内简历几乎没怎么投了,看知识点好像也没那么认真了。


第五、第六个月


好吧,上个月的3个机会,全都没有等来,继续面试。心态有点躺平,焦虑感少了,颓废感来了,BOSS直聘岗位几乎能投的都投过了,没有面试的日子,会过得略显浑浑噩噩,不知道要做什么。
陆续来了几个offer,也终于决定下来了,降薪差不多40%,但好在稳定性应该有保障。


心态的转变



  • 从渴望周末,到期盼工作日


    工作时渴望周末的休息 ,没找到工作时,每一个周末的到来,都意味着本周没有结果,而过完周末,意味着过完了1/4月。感觉日子过得好快,以前按天过,现在按周过,半年时间感觉也只是弹指一挥间。

    每一个周一的到来,意味着拥抱新的机会。每周的面试频率比较高时,会感到更充实;面试频率低下来时,焦虑感会时不时的涌上心头,具体表现是狂刷招聘软件,尝试多投递几个职位。


  • 肯定 -> 否定 -> 肯定


    找工作初期,信心满满。定制计划,每天刷多少题,每天看什么知识点,应该按照什么节奏投递简历,自己全都规划好了

    中期,备受打击,总有答不上来的问题,有些之前看过的知识点,临场也会突然忘记,感觉太糟糕了。

    后期,受的打击多了,自己不会的越来越少,信心又回来了



可能能解决你的问题


要不要和家里人说


自己这半年下来,没有和家里人说,每周还是固定时间给家里打电话,为了模拟之前在路边遛弯打电话,每次电话都会坐在阳台。

个人情况是家在北方,本人在南方,和爸妈说了只能徒增他们的焦虑,所以我先瞒着了。


被裁员,是你的问题吗?


在找工作的初期,总会这样问自己,是不是自己选错了行业,是不是自己不该跳槽,会陷入一种自责的懊恼情绪。请记住,你没有任何问题,你被裁员是公司的损失,你不需要为此担责,你需要做的是让自己更强,不管是心理、身体还是技术。


用什么招聘软件


我用了BOSS直聘和猎聘两个,建议准备好了的话,可以再多搞几个平台海投。另外需要注意几点:



  1. 招聘者很久没上线,对应岗位应该是不招的

  2. 猎聘官方会不定期打电话推荐岗位,个人感觉像是完成打电话KPI,打完电话或加完微信后就没有后续跟进消息了

  3. 你看岗位信息,招聘者能看到你的查看记录,如果对某个岗位感兴趣,怕忘记JD要求,可以截图保存,避免暴露特别感兴趣的想法被压价


在哪复习


除非你已经有在家里持续专注学习的习惯,否则不管你有没有自己的书房,建议还是去找一个自习室图书馆,在安静的氛围中,你会更加高效、更加专注。

如果只能在家里复习,那么远离你的手机,把手机放到其他房间,并确保有电话你能听到,玩手机会耗费你的专注力和执行力。

(你在深圳的话,可以试试 南山书房 ,在公众号可以预约免费自习室,一次两小时)


如何度过很丧的阶段


多多少少都会有非常沮丧的阶段,可能是心仪的offer最终没有拿到手,可能是某些知识点掌握不牢的自我批判。

沮丧需要一个发泄的出口,可以保持运动习惯,比如日常爬楼梯、跑步等,一场大汗淋漓后,又是一个打满鸡血积极向上的你。

不要总在家待着,要想办法出门,多建立与社会的联系,社会在一直进步,你也不能落下。


一些建议


1. 项目经历


讲清楚几点:



  • 项目背景


    让人明白项目解决了什么问题,大概是怎么流转的,如果做的比较垂类,还需要用通俗易懂的话表达项目中的各个模块。


  • 你在其中参与的角色


    除了开发之外,是否还承担了运维、项目管理等职责,分别做了什么


  • 取得的成果


    你的高光时刻,比如解决了线上内存泄漏问题、消息堆积问题、提升了多少QPS等,通常这些亮点会被拿出来单独问,所以成果相关的延展问题也需要提前想好



还比较重要的是,通过项目介绍,引导面试官的问题走向,面试只通过几十分钟的时间来对你做出评价,其实不够客观,你需要做的是在这几十分钟的时间内尽可能的放大你的优势



除此之外,还需要做项目的延展思考



比如我自己,刚工作时做客户端开发,负责客户端埋点模块的重构,面试时被问到,“如果让你设计一个埋点服务端系统,你会考虑哪些方面”? 对于这类问题,个人感觉需要在场景设计类题目下功夫,需要了解诸如秒杀抢购等场景的架构实现方案,以及方案解决的痛点问题,这类问题往往需要先提炼痛点问题,再针对痛点问题做优化。


2. 知识点建议


推荐两个知识点网站,基本能涵盖80%的面试知识点,通读后基本能实现知识点体系化

常用八股 -- JavaGuide

操作系统、网络、MYSQL、Redis -- 小林coding


知识成体系,做思维导图进行知识记忆

那么多知识点,你是不可能全都记全的,部分知识点即使滚瓜烂熟了,半个月后基本也就忘光了。让自己的知识点成框架、成体系,比如Redis的哨兵模式是怎么做的,就需要了解到因为要确保更高的可用性,引入了主备模式,而主备模式不能自动进行故障切换,所以引入了哨兵模式做故障切换。

不要主观认为某个知识点不会被问到

不要跳过任何一个知识点,不能一味的把认为不重要的知识点往后放,因为放着放着可能就不会去看了。建议对于此类知识点,先做一个略读,做到心中大概有数,细节不必了解很清楚,之后有空再对细节查漏补缺。

之前看到SPI章节,本能认为不太重要,于是直接略过,面试中果然被问到(打破双亲委派模型的方式之一),回过头再去看,感觉其实不难,别畏惧任何一个知识点。

理论结合实践

不能只背理论,需要结合实践,能实践的实践,不能实践的最好也看看别人的实现过程。

比如线程顺序打印,看知识点你能知道可以使用join、wait/notify、condition、单线程池等方式完成,但如果面试突然让你写,对于api不熟可能还是写不出。

又比如一些大型系统的搭建,假如是K8S,你自己的机器或者云服务器没有足够的资源支撑一整套系统的搭建,那么建议找一篇别人操作的博客细品。

不要强关联知识点

被面试官问到一些具体问题,不要强行回答知识点,可能考察的是一个线上维护经验,此时答知识点可能给面试官带来一个理论帝,实操经验弱的感觉。

举两个例子,被问过线上环境出问题了,第一时间要如何处理?,本能的想到去看告警、基于链路排查工具排查是哪个环节出了问题,但实际面试官想得到的答案是版本回滚,第一时间排查出问题前做了什么更新动作,并做相应动作的回滚;又被问过你调用第三方服务失败了,如何本地排查问题?,面试官想考察的是telnet命令,因为之前出现过网络环境切换使用不同hosts配置,自己回答的是查看DNS等问题,这个问题问的并不漂亮,但是也反映出强关联知识点的问题。

建立自己的博客,并长期更新

养成写博客的习惯,记录自己日常遇到的问题,日常的感受,对于某些知识点的深度解析等。面试的几十分钟,你的耐心,你解决问题的能力,没办法完全展示,这时候甩出一个持续更新的博客,一定是很好的加分项。同时当你回顾时,也是你留下的积累和痕迹。



半年很长,但放在一生来看却又很短

不管环境怎样,希望你始终向前,披荆斩棘

如果你也正在经历这个阶段,希望你早日上岸



作者:雪梨酒色托帕石
来源:juejin.cn/post/7274229908314308666
收起阅读 »

泰国游记:声色张扬的奇妙之旅

囧途开始 四月初的时候,阿宇问我:“老三,五一去泰国不?” 其实那一阵状态“水深火热”,没什么太多玩的心思,但是一想到小卡拉米一把年纪了,还没出过国,这次不去,下次又不知道什么时候了。 “去他妈的,整!” 订好机票,简单做个攻略。四月三十号,上海下着雨,浦东机...
继续阅读 »

囧途开始


四月初的时候,阿宇问我:“老三,五一去泰国不?”


其实那一阵状态“水深火热”,没什么太多玩的心思,但是一想到小卡拉米一把年纪了,还没出过国,这次不去,下次又不知道什么时候了。


“去他妈的,整!”


订好机票,简单做个攻略。四月三十号,上海下着雨,浦东机场,还有几分凉意,但是此时的我,已经开始畅想那头的曼谷有多么火热。


万万没想到,飞机延误!延了三次!原本五点开的飞机,到了差不多八点,还没动静,而且最后一次航司没有给出任何官方通知和说明。我心想:“今天不会走不了,要打道回府吧?”


飞机延误


正忐忑间,通知飞机起飞!哦吼,晚了三个多小时,但是终于飞了!望着上海的的灯光渐渐模糊,这下终于要曼谷见了。


上海出发


当天的气候不太友好,航程中,经历了三次颠簸。到了凌晨,终于抵达了曼谷,按照原定计划,找到接机司机,直奔芭提雅。


飞机到达曼谷


在去的路上,我们讨论了一下要不要订酒店,合计了一下,到了那先整点夜市小烧烤,再马杀鸡一下解解乏,酒店这事,不着急。


经历了两个多小时的路程,终于到达了芭提雅,这时候发现,我们去的那片地方,怎么都是灯火灰暗,说好的夜市小烧烤呢?哥俩累了,要不先把酒店顶了,携程再一看,明明曼谷看还有很多房间,这回附近的酒店都满了。要不先找个马杀鸡店?哥俩茫然地转在街上,芭提雅的热浪、下水道的恶臭,冲面而来,转了一圈,发现马杀鸡店也都打烊了,哥俩大眼瞪小眼,一时相对无言。


携程是拉了,要不试试美团吧?抱着试一试的态度,用美团搜了一下,哎,发现几百米远的一个酒店有房间,订!顺着高德,一路导航了过去。进了一个疑似前台的大厅,结果一看,没有人,什么情况?


疑似的酒店大堂


赶紧打电话问问,电话打通,用我多年来一直稳定磕碜的英语,开始磕磕巴巴地咨询:


“We we… book en… your room on meituan er website,we en are in the the the…hotel, but there is no body.”


对面也回复了一堆话,英语也不是很好,当然主要是我听力也差,只听懂了一点点:


“Do you in the hotel?”


"Ah yes."


……


又鸡同鸭讲了一阵,阿宇又接过去掰扯了一会,还是没掰扯明白。


我接过来,只能说,一时无言,“We need help.”,她也想帮我,但她回复的我都听不懂,唉,挂了吧,哥俩这个酒店看样子是住不上了。


我看了眼大堂的沙发,心想,要不哥俩就在这凑合一宿得了。结果,接电话的那个前台大姐找过来了!


Madam,你就是我们的光!


原来酒店的前台就在对面,我还埋怨高德又缺德了,导错了地方,结果领完房卡,发现住房还真差不多真就在高德导航到的位置——我也知道为什么前台大姐会知道要出来找我们,看起来不是第一次了。


此时,已经是凌晨三点了,洗漱洗漱,躺在床上,凌晨三点半。


再规划规划第二天的行程,订订酒店,凌晨四点,奔波了大半天的三某,终于睡了。


住的房间


第二天不太早,老三在一阵饿意中醒来,得出去找点吃的,不知道吃什么,找个711吧,去的途中,发现有一条小路,看着是通向海边,先去海边看看吧,结果路边的小棚子里,跳出了三只狗子,一阵狂吠,奔着我而来,没感受到芭提雅人的热情,先感受到了芭提雅狗的”热情“,我随手抄起一根棍子,今天非得让你们尝点中国的颜色。


——狗主人出来了,我扔掉棍子,算了,今天先放你们一马,以后好好做狗。


小路


随便吃点东西,回去准备换酒店,我突然反应过来,我们看了半天地图,要住海边,要离今天去的真理寺近,选来选去,阿宇重新订的,不就是现在这家吗?我俩真是热昏了头,这还不如直接续呢,瞅瞅这破酒店,哪有海景?


换房间吧,先住着,放下东西,到了前台,正在办手续,我往外面瞟一眼,大海!这个酒店还真的是海景酒店!酒店的楼下,泳池,海滩,在往侧前方一看,真理寺也赫然在目——芭提雅的热辣假期要开始了!


酒店泳池


沙滩和真理寺


芭提雅


海滩午餐


一天没怎么正经吃饭的老三和阿宇,决定中午得找个餐厅好好吃一顿,刷了会大众点评,本来想去附近的一个海鲜市场,但是阿宇不能吃海鲜,找了另外一家评分不错的餐厅Surf & Turf Beach Club & Restaurant,这家餐厅在海边,吹着海风,看着大海和沙滩,用餐心情非常美妙。


海边餐厅


服务员阿姨不太会英文,对着菜单,像两个看图学字的小学生一样,我们点了饮料和餐,整体上还是没让人失望的。


菠萝炒饭色泽金黄,味道清甜,混杂着坚果、果干、肉松,口感非常丰富,我们一致认为,这是目前为止,吃过的最好吃的菠萝饭。


点的餐


冬阴功汤,一口下去——嗯,泰国味儿!酸辣鲜三种口味汇聚,咖喱和辣椒的辣,柠檬的酸,在口腔里交相奏响,互相融合。


点之前,服务员阿姨特意提示了我“辣”,我还忐忑了一下,结果发现这种辣和我想象的辣不一样。怎么形容呢,如果说国内的菜,比如川菜的辣是一种干燥的辣,这里的辣是一种湿润的辣。


路边马杀鸡


来了泰国,怎么能不体验一下“马杀鸡”呢?吃过午饭,天气正热,下午的行程还早,穿过烫脚的街头,找到了路边的一家按摩店。


价格看起来很合算,按!我选了肩背按摩。


路边按摩店


先洗个脚——咱这也算两只脚踏进洗脚界了。换好衣服,趴在塌板上,来自中国的面团,要被泰国的师傅揉搓了。


洗脚


先刷油,精油滴在背上,有一点辛辣的感觉,让我想起了以前尝试用过的泰王油。上好油,开始反复揉搓,师傅的手、肘、膝都是这次“白案面点制作”的工具。


和中式按摩相比,泰式按摩要轻柔很多,这种轻柔是恰到好处的轻柔,既能感受到力量的渗透,又不会感觉到很痛。最后,师傅拉扯一下手指,就像是把面团给拉长,结束了这次下厨。打开店门,走进芭提雅滚烫的天气离,中国的面团”下锅“了。


真理寺


下午去了一片海滩之隔的真理寺,直线距离很近,但是真要去的话,得绕很长一段路。


真理寺外景


进入真理寺之前,工作人员给我们发了顶安全帽,去过很多景点,真理寺是第一个需要戴安全帽参观的。为什么呢?因为真理寺还在施工。


真理寺始建于1981年,历经多年建设,至今仍然没有完工。它是纯木制结构,所有的构造都采用卡榫结构,雕塑都是纯手工打造,虽然它被称之为寺,但其实不是一座寺庙,而是一件艺术品。


真理寺正在施工中的雕刻师傅


真理寺的修建耗时弥久,似乎不知道它到底会什么时候结束,但是和真正的真理相比,又不一样,毕竟真理寺总有一天修完,但是真理是无穷无尽的。


真理寺的小奶猫


环绕真理寺的时候,在一个台阶上看到两只稚嫩的小奶猫正在打闹,真理是有生命力的,这两只小猫比我们离”真理“更近。


真理寺和花与海


真理寺在一座宁静的海滩,从一汪孔洞看过去,一枝白花,开在绿的大海旁,远处的天是蓝的,光洒进来,滴在老的木头上。


真理寺内的雕塑风格各异,能看出来一些佛教和印度教的风格,我们参观的时候,刚好有一个旅游团,蹭到了一点导游的解说,导游说到:”……观世音菩萨……孔子“,这两个熟悉的名字引起了我的注意,仔细一看,还真有这样的雕塑,真理寺真是一个多国家、多宗教的文化汇聚地。


真理寺内景


在真理寺还看到了大象,不过这大象驮着人,看起来很温顺的样子,希望以后有机会能看看野生的大象吧。


真理寺驼人的大象


芭提雅落日


在真理寺结束地比较早,天气又比较热,回到了我们的海景酒店休息,下了水,在蓝色的泳池里休息扑腾了一会。


天色渐晚,太阳没那么晒人,躺在沙滩的躺椅上,喝口冰凉的啤酒,吃点水果,吹着海风,看着太阳慢慢变成橘色。


躺在海边


没忍住下海泡了一会,在温热的海水里,看着太阳慢慢落到海平面之下,这一刻,感到格外松弛。


日落


日落真理寺


蒂芬妮人妖秀


日落不是芭提雅一天生活的结束,夜晚才刚刚开始。


来了芭提雅,怎么能不看人妖秀,芭提雅知名度最高的是蒂芬妮人妖秀。蒂芬妮的场馆,白色的礼堂风格,喷泉闪着不同的颜色。


芬妮人妖秀场馆


蒂芬妮秀禁止摄影,所以只拍了开幕和谢幕。抛开猎奇的成分,单纯从表演的角度来看,我觉得蒂芬妮秀的艺术水平很高。


蒂芬妮秀开场


演员都称得上是端庄典雅,尤其是每场得领舞,我不知道这是不是就是所谓得”人妖皇后“,身材笔挺、面容姣好,超模的风范、专业的歌舞水平。


演出的节目,风格形式也非常多样,歌舞剧、独舞、现代舞、民族舞……全场大概有三十个节目,每个节目都有不一样的特点。很多节目看得出来,是迎合了游客的审美,包含了不同国家的歌舞形式,泰国、美国、中国、印度、越南、朝鲜……


我印象里特别深刻的节目有两个。


一个是“菊花台”,当《菊花台》的音乐响起,演员表演起了中国风的舞蹈,整个剧院充满了中国观众的欢呼,剧院是懂怎么撩拨观众的。


另外一个,也是全场印象最深刻的一个,是“对唱”,“男演员”戴礼帽,穿礼服,声音清澈,灯光熄灭,“女演员”迅速登场,声音妩媚。


我最开始以为这是两个演员,切换的时候,一个演员钻到幕布后,另外一个演员钻出来。瞪大眼睛,我想看看到底怎么换的,不对——这好像是一个演员!


灯光打开,谜底揭晓,果然是一个演员,左边身子穿着男礼服,右边身子穿着女礼服,左面示人,男声开唱,右边示人,女声开唱。


演员快速左右转身,男女声切换自如,全场充满了欢呼。


这位表演者唱什么我没听懂,但听出来了情绪非常激亢,甚至挺出来了愤怒。“人妖”这个群体,对自己的认同到底是男还是女呢?是向左,还认为自己是男,还是向右,认为自己是女?或者正面对人,雌雄莫辨?


来泰国的路上,我看了一些书,我以前以为人妖在泰国有悠久的历史,后来发现,”人妖“是现代的产物。


芭提雅曾经只是个小渔村,为什么后来逐渐能逐渐成为度假胜地呢?因为美国来过,确切说美国海军陆战队来过。二战后,泰国倒向美国,成为美国反共的桥头堡,芭提雅就曾经是美国海军陆战队的驻地,它也因此而兴。


人妖同样也因此而来,驻扎的美国大兵给芭提雅的市场带来了消费,当然也包括成人市场,因为汇率物价等等方面的差异,美国大兵指甲缝里漏出的一点油,都可能会改变一个泰国的贫穷家庭。有些家庭,家里只有儿子,穷得实在受不了了怎么办呢?如果儿子长的还算清秀,那就牙一咬,送去做变性手术,去挣美国大兵的钱,没想到有些美国大兵还挺喜欢,因为不用担心怀孕。


富可能使人变坏,穷可能使人变态。人妖的来源,算是穷的没办法的办法。


蒂芬妮秀落幕


蒂芬妮秀落幕后,演员会站在小广场上,招徕游客合影,一次100泰铢。歌舞秀中的艺术感,终究还是变成生活的真实感,这不是艺术,这是生意。


等待合影的演员


这是一条不能回头的路,演员得在年轻的时候尽可能挣到养老的钱——如果有老可养的话,因为注射激素过多,通常很难长寿。


到了三十岁以后,雄性激素的分泌难以抑制,TA们的男性特征会越来越明显,难以遮掩,TA们也会被老板无情地抛弃。我在蒂芬妮的前台,看到售货员,化妆、穿裙子、踩高跟,但是男性特征已经无法遮掩,检票员也是如此,剧场的一个内场管理员,留了美式的油头,乍一看是一个“小哥”,仔细一看也发现步态和声音不太对劲。


整个蒂芬妮秀才几个工作人员,其他人都去哪了呢?而且,蒂芬妮的演员,绝对都是百里挑一的,那剩下的九十九呢?曲终人落幕,台上永远会有人,谁会知道离开台子的人呢。


风俗步行街


芭提雅的夜生活,还有一个地方,是一定要去看一看的,那就是风月步行街。坐着双条车,穿行在中滩天海滩旁的长街,风月步行街到了。


坐双条车


刚到街口,五色的灯光,喧闹的人声,嘈杂的音乐铺面而来。


风月步行街的街口


街边的夜店灯红酒绿,街边的女郎穿着清凉性感,招徕客人的皮条客穿梭其间。


风月步行街


皮条客和街边女郎


路上的行人来来往往,有的走着走着,脚就不听使唤地就拐进了街边的一家店,成为喝着、唱着、跳着的人群中的一员。


夜店里的人群


——欲望在这条街上流淌。


真是万恶的资本主义啊!唉,怎么这就到头了,阿宇,你拉我回去干什么?


格兰岛


如果想在芭提雅体验一下热带小岛风情,那么格兰岛是个不错的选择。


从芭提雅的码头登船,大概过了半个小时,在海船的摇晃中,一座小岛渐渐映入眼帘,青的山下,红瓦的房子,和蓝的海界限分明。


抵达格兰岛


在燥热的风里,登上Tawaen观景点,浅蓝色的天空下,白色的沙滩渐渐变得湿润,绿色的海,渐渐变成深深的墨绿。


Tawaen观景点看海


下到Tawaen海滩,满是戏水的游客,男男女女,各个国家。


Tawaen海滩的游客


Tawaen的水上项目很多,我选择了4个项目,摩托艇+香蕉船+浮潜+滑翔伞。


摩托艇,之前在秦皇岛试过,这里再试一次,感觉是海水更清,浪更大,不到一会,全身上下都湿透了。


看到了香蕉船,那必须尝试一下,为什么呢?因为想起了NBA的”香蕉船兄弟“,对了,最后带完香蕉船的小哥,一定会推荐玩一下”翻船“,拉着香蕉船的快艇,最后会控制让香蕉船翻掉,所有人都落到水里。


NBA香蕉船兄弟


可惜坐船的时候只有我一个人,如果“支付四狗”组合能齐聚,那大可Cosplay一下香蕉船兄弟,对了,我们也要翻船的。


香蕉船和湿透的我


试了下浮潜,体验一般。作为一个旱鸭子,只能在水里乱扑腾,一不小心,就是原地打陀螺,头埋进水里,满眼都是绿色,海水往耳朵里,顺着缝隙往口鼻里流。当然,如果会游泳,应该体验会很不错,同船会游泳的一些伙计,开心地都流连忘返。在会游泳之前,我应该是不会再玩这个项目了。


浮潜


最期待的的是滑翔伞,远远地看着,人像风筝一样在天上飘着。


远看滑翔伞


上了快艇,两位师傅都很皮,一个师傅话多、活泼,我们一上船,“中国人?”肯定之后,这位师傅放起了音乐《我们不一样》,放着放着他也跟着在船头唱了起来“我们不一样,每个人有不同的境遇…”唱着唱着还跳了起来。


唱跳的皮师傅


另一位师傅看起来稳重一点,安静地掌着舵。但是快艇开起来之后,就不是那么回事了,这位师傅喜欢开“快船”,快艇在海上速度拉满,还玩起了漂移。


很快,开始滑翔,第一位勇士上了天,小船上充满了欢呼声。第二位,第三位,第四…快乐戛然而止,船抛锚了。


两个师傅尝试了半天,都没能打响快艇,抛锚的小船,就像一块木板,在海浪中飘摇晃荡,我终于体会到什么叫“海上孤舟”,我们只是在海滩上,想想如果远洋抛锚,那该多么绝望。最后另外一艘快艇过来救了我们,拖着抛锚的快艇回了码头。


船拖船


又想起了皮师傅之前唱的歌:“我们不一样,我们的船会Done”……下次有机会再玩吧。


伴随着半斜的太阳,从码头乘船离开了格兰岛。看着身后的格兰岛越来越远,又回到了暮色中的芭提雅。


离开格兰岛


暮色中的芭提雅


芭提雅码头远眺


曼谷


大皇宫


来到曼谷的第一站,直奔大皇宫,大皇宫在泰国的地位,就相当于中国的故宫,去了曼谷不去大皇宫,就像是去了北京不去故宫。


大皇宫外景


但是去的时候不赶巧,大皇宫似乎在接待什么团体,暂时不开放。第二天再去大皇宫,终于开放了,而且免费。


大皇宫的殿宇


一座大皇宫,一部泰国近代史。


泰国的历史不算悠久,它有四个王朝,素可泰王朝、阿瑜陀耶王朝、吞武里王朝、曼谷王朝,大皇宫就是曼谷王朝的皇宫,也称作拉玛王朝,在现在的泰国仍然延续,目前在位的是拉玛十世。


现任泰王-拉玛十世


拉玛王朝创立于公元1782年,此时的中国处于清朝乾隆年间,拉玛一世初建大皇宫,后来历代泰王逐渐完善。


大皇宫舍利塔


大皇宫的建筑风格很混搭,既有传统的东南亚风格,白墙红瓦,也有宗教元素,金灿灿的舍利塔,华丽的佛堂。西洋风格同样也非常浓厚,很多建筑都被珐琅点缀,有些建筑完全是欧式风格。


这是因为大皇宫后来的完善,同样伴随着拉玛王朝的动荡与变革,资本主义殖民浪潮兴起,泰国虽然没有沦为殖民地,但是却沦为了半殖民地。和宗主国清国被人用坚船利炮打开国门不一样,从拉玛四世开始,泰国主动打开了国门。


大皇宫一角


拉玛四世主动派遣留学生到欧洲学习军事政治法律,并对国内的政治经济进行了改革。拉玛四世的继任者,也就是著名的朱拉隆功大帝,是个更激进的改革者,少年即位的朱拉隆功,组织了一些王室和贵族青年,自称为“少年暹罗”,在“进步”和“改革”的主题下争辩和前进。


“少年暹罗”的意思,是把他们的对手暗喻为“老年暹罗”, 立志要把他们扫进故纸堆。朱拉隆功的改革是成功的,所以他成为泰国五大帝之一。


这跟大皇宫的建筑风格有什么关系呢?《泰国史》里写了这么一段:



在19世纪70年代,朱拉隆功建造了一座新的宫殿,采用意大利风格的设计,但是在顶部采用了暹罗式的屋顶,以取悦传统主义者。1907年,这种妥协就被抛弃了,国王修建了带有强烈古典主义风格的阿南达沙玛空御座厅(Ananta Samakhom throne hall),采用了卡雷拉大理石、米兰的花岗岩、德国的铜和维也纳的陶瓷。整座建筑成为通往旧城区北部的新的王室宫邸区的入口,那是一片欧式风格的宫殿群,以及为其他王室家族成员建造的宅邸。国王和贵族们进口了许多欧洲的小摆设来装点这些新房子。



泰国宫廷和欧洲联系越来越紧密,体现在建筑风格,就是有很多欧化的地方。对了,从《泰国通史》看到比较有意思的一个点,拉玛王朝的前四位国王都有中文名:郑华、郑佛、郑福、郑明,拉玛五世也就是朱拉隆功开始,没有中文名,朱拉隆功即位于1868年,此时的中国属于清朝的同治年间。


大皇宫一角


大皇宫也有血色,年轻的拉玛八世在大皇宫饮弹身亡,泰王是自杀还是他杀?行刺者是谁?已经成了历史谜案。拉玛八世的弟弟普密蓬·阿杜德即位,命运的改变总是这么突然,年轻的普密蓬应该是个热爱自由的人,在瑞士留学的时候,他因为飙车失去了一只眼睛。


街头供奉的普密蓬


如果没有兄长的遇刺,他也许会成为一个风流贵族,也许泰国会是另外一个样子。命运把他推到他该在的位置,他做的非常好,正如他的名字泰文的意思,“无与伦比的能力”,可以说是名副其实。


他影响了几乎整个泰国的现代史,军政府、民选政府……在每次政治危机的时候,普密蓬都会在恰当的时候出手,来保证政局的基本稳定。他亲近民众,是泰国民众非常热爱的”父亲“。


也正是由于拉玛八世的遇刺,王室认为大皇宫不详,搬到了其它地方,所以我们今天才有机会参观大皇宫。


郑王庙


郑王庙和大皇宫大概一水之隔,它纪念的是一个传奇国王——郑信大帝。


湄南河岸的郑王庙


郑信大帝的一生,是一个传奇,也如同一部跌宕起伏的戏剧。


他的前半生比起爽文不遑多让,郑信的父亲郑镛是潮州人,因为谋生,孤身来到泰国。郑镛从贩水果的小贩做起,逐渐发达,承包赌税,也籍着这个关系,郑信被过继给当时的财政大臣昭披耶却克里。从贫民到大亨,两代完成阶层晋升,郑镛已经足够励志,但是他的儿子更加传奇。


郑王庙塔


青年郑信,成为北方的一个城主。泰缅战争爆发,阿瑜陀耶王朝灭亡,属于郑信的舞台却搭建好了。郑信率兵赴京勤王,参与了出城出击,但是遭遇了失败,在撤退到城下的时候,发现已经被阻拦在外,进退无据之中,郑信毅然带兵突围,没想到阿瑜陀耶城亡,郑信生。


太阳照耀下的郑王庙


王朝乱世之中,郑信逐渐崛起,他先是在泰国南部站稳脚跟,又团结各方力量,挥师北上,收复阿瑜陀耶城。


1768年1月4日,郑信加冕为吞武里王。


郑信和吞武里王朝的落幕也很突然,阿瑜陀耶城发生骚动,郑信派遣大臣披耶讪镇压,披耶讪却被叛军说服,加入了叛军。此时吞武里空虚,叛军长驱直入,王宫卫队不支,郑信不得已退位,出家为僧。


郑王庙塔侧面


此时听闻国内政变的远征军主帅,郑信的女婿通銮,回国平乱,平乱之后的第二天,郑信身亡,死于紫檀木棍的击打之下——这是帝王的死法。


通銮,也就是拉玛一世,他让我想起了另外一个历史人物——司马懿,郑信出家为僧按照惯例可以免死,司马懿同样指洛水为誓,一定会放过曹爽——政治也许有时候不能讲道德吧。


对了,这位拉玛一世的中文名叫郑华,在对宗主国清国的国书中,他自称郑信的儿子。


郑信大帝的一生,在绝境中奋起,在最辉煌时戛然而止,让人不得不感概历史和命运的无常。


拳赛


来到泰国,我最大的愿望就是看一场泰拳赛,泰国有两大知名泰拳场,伦披尼和迦南隆,可以说是泰拳圣地,几乎所有泰拳手都梦想披上这两个拳场的金腰带。


伦披尼经历了一次搬迁,目前在曼谷郊区,迦南隆,在曼谷市区,在某些app上,被翻译成那差达慕,我一开始还吃惊,点评拳赛,竟然没有迦南隆,原来是这个翻译闹了乌龙。


这次我选择是更远的伦披尼,是因为周五,伦披尼和One冠军赛合作,有ONE周五格斗夜比赛。ONE冠军赛是是目前世界排名第二、东南亚排名第一的综合格斗赛事,当然,地处东南亚,ONE冠军赛自然也免不了泰拳比赛,甚至目前ONE的踢拳赛事同样开始迅猛发力。


能在泰拳两大圣地之一的伦披尼,看一场顶级的现代格斗赛事,实在是一件无比快乐的事情。


ONE格斗夜


我已经有六七年没到现场看比赛了,经历了两个小时的拥堵路程,我们终于赶到了伦披尼。


领票,进场,坐到座位上,音乐、灯光,现场的欢呼声,我感觉开始兴奋起来了———It's Time!


拳赛准备开场


开局的第一场比赛,日本选手VS白俄罗斯选手,日本选手有很浓重的空手道风格的意味,站立更强一些,几次打晃白俄罗斯选手,但是白俄罗斯选手的摔柔更胜一筹,在第三回后半段,裸绞终结了日本选手。第一场比赛就出现了终结,太兴奋了!


日本选手被终结


还有一场日本vs泰国的比赛,日本选手身高臂展更有优势,一度压制了泰国选手,肘法给泰国选手的脸开了一个大口子,泰国选手血流满面,苦苦支撑,我以为这场比赛就这么结束了,没想到泰国选手抓住一个漏洞,前手摆拳命中,日本选手重重倒地不起,赛事方赶紧用担架把日本选手抬走。我觉得这场是整个比赛最残酷的一场,赢的人血流满面,输的人担架抬走。


输赢皆惨烈


联合主赛同样精彩,一个年轻的黎巴嫩选手,挑战老牌的泰拳王,这个泰拳王应该是个明星选手,出场的时候全场欢呼,还现场表演了一段拜师舞,没想到场上局势风云突变,年轻选手,一开始就凶猛发力,老拳王显然有些慢热,没有进入节奏,第一回合就被击倒了两次,虽然最后依靠老道的经验,撑到了最后,但还是一致判定负。


现场还有很多精彩的比赛,现场看比赛和隔着屏幕看比赛,完全是不同的感觉,现场的欢呼声,选手的打击声,再好的摄影设备都制造不出来这样的临场感,去了现场,才能真正感受到现场的热烈!看比赛的时候,我完全兴奋起来了,呐喊,嘶吼,赛事结束后嗓子都是哑的。


只能说:过瘾,下次还看!


ONE巅峰赛


ONE格斗夜的赛事结束之后,我突然发现周六的早上还有一场比赛,ONE巅峰赛,这场比赛有一场金腰带的卫冕战,而且最重要的是,这一场有三位中国选手:胡勇、魏锐、张立鹏,两场MMA赛事,一场踢拳赛事。


尤其是魏锐VS秋元皓贵的这场比赛,让我想起了之前我非常喜欢的选手邱建良,带着前世界第一的势头,打入了ONE冠军赛,结果第一场被秋元皓贵阻击,至今还没有再次复出比赛。这场,毫无疑问,是一场恩怨局,这样的机会可遇不可求,我一定要来现场,一定要亲眼看到魏锐击败秋元皓贵。


晚上回到酒店已经差不多两点,我完全兴奋地睡不着,一早五点,我又起床到了伦披尼现场,这次现场摆放了一条金腰带,金腰带在前,谁能拒绝合影呢?


和金腰带的合影


开局两场速杀


首局终结获胜,在拳赛里是很少见的,终结一般发生在选手体能下降和伤害累积之后,但是今天的比赛,开场的两局都是首回合终结速胜。


今天比赛的第一场是泰拳比赛,在第一回合的后段,先是一个摆拳,打晃,接着一个击腹的直拳,选手痛苦倒地,这一拳应该是岔气了。


第二场比赛是无道服的巴西柔术比赛,日本选手上场之后,很快就被巴西选手压制,拿背裸绞一气呵成,几秒,裁判试了一下,日本选手胳膊已经软了,赶紧终止比赛。巴西选手在赛后抑制不住地哭泣,巴西柔术比赛,需要一点欣赏的门槛,所以纯巴西柔术选手很难接到商业比赛,很难赚到钱,平时生活很艰难,所以也能理解为什么巴西选手忍不住哭泣。


为了三位中国选手的比赛而来,加上状态不佳,中间的几场比赛,都没有上一场那么高的兴致。


胡勇被克制


终于来了,第一位出场的中国选手是胡勇,伴随着中文的出场音乐,全场响起了巨大的欢呼声,能在异国他乡,为本国的选手加油助威,这是一件多么令人激动的事!


现场的声音非常整齐,一位大哥自发担当了”领喊“的角色,他喊”胡勇“,现场的中国观众跟着一起喊”加油“,”胡勇……加油“的声音响彻整个场馆。


胡勇的脚下移动比较灵活,能看出,他以前应该是有散打的背景,他的站立比对手出色,开局几次重击对手。


对手随后调整了策略,坚决要和胡勇打地面,对手的摔柔水平在胡勇之上,每一次防守,胡勇都得拼尽全力,几次被拖入地面,被拿到了很深的把位,现场的中国观众心都揪了起来。


胡勇


最终三局战罢,胡勇点数落败。胡勇还很年轻,还有时间能继续完善和提高自己,加油!


魏锐稳健取胜


这是我最期待的比赛,魏锐是我特别佩服的一个选手,已过而立之年,家庭圆满,带的徒弟都成了冠军,他的职业生涯,武林风金腰带、勇士的荣耀金腰带、还有最具含金量的K1金腰带,功成名就,他已经不需要证明什么了。但是在职业生涯暮年,他还是选择了走出国门,挑战自己,继续冲击ONE的金腰带,这就是真正的武者意气!


魏锐出场


这场也是一场复仇局,对手秋元皓贵,曾经击败过邱建良,这次魏锐前来,也有为好兄弟复仇的意味。魏锐是是非常有名字的选手,他出场的时候,全场的欢呼像海啸一样。


魏锐迎击拳命中


伴随着同样整齐的”魏锐……加油“的声音,比赛开始。魏锐赛前说,针对秋元的技术特点,他已经找到了应对的办法,果然所言非虚。魏锐灵活的脚下移动,让秋元皓贵根本打不出他习惯的快速组合,到了第二回合,秋元皓贵甚至顶着伤害,硬往前压,给魏锐造成了一些麻烦,但是魏锐经验丰富、拳商很高,进行了调整,加强移动和迎击,整场比赛,基本上没见到秋元像往常一样的高速组合。


第三回合开始


就这样,三局战罢,在异国他乡的赛场上,魏锐一致判定击败了秋元皓贵,向着金腰带又迈进了一步。


获胜的魏锐身披国旗


裁判举起魏锐的手的时候,我满脑子和嘶吼的只有四个字:”魏锐牛逼!“


张立鹏憾负


张立鹏是老牌的MMA选手,曾经也打进过UFC,这次他遇到了不小的麻烦,他的对手赛前超重,协议之下,张立鹏选择了接下比赛。


果然,临场,对手的维度比张立鹏要大一号,在地面的缠斗中,张立鹏在力量上劣势太大,根本压不住对手,而且体能消耗地过快,到了第三回合,完全是强撑着打完,中间有一些好机会,因为体能不足都没有抓住。


张立鹏拿到把位但压制不住


最终,对手一致判定获胜。


张立鹏因为体能消耗过度,被轮椅推离现场,希望他能尽快恢复,早日拿下下一场胜利。


这场有个彩蛋,目前在UFC排名最高的中国男子选手宋亚东也来到了现场,作为张立鹏的边角,希望中国MMA的未来越来越好!


宋亚东来到现场


射击


来泰国,有第二件必做的事情,就是体验射击,因为没有提前预订,去不了最推荐的海军射击俱乐部,我选择了陆军俱乐部。


带着兴奋过后的眩晕,我来到了陆军射击俱乐部,下午两点开门,经历了一阵等待之后,领到了我的子弹,黄橙橙,亮晶晶。


领到了子弹


等待的时间,看着里面正在射击的游客,耳边传来了鞭炮一样的声音,震得耳朵嗡嗡的。


下午的射击场


终于轮到我了,我选择的套餐是三款手枪,教练帮忙上子弹上膛,我接枪瞄准,想起之前看的一点教程,身子往前压——后来看视频,嗯,怎么是勾着头的,好丑,以后有机会动作练漂亮一点。


设计中


开枪,手枪的后坐力不大,但是射击的时候,枪会往上跳,一开始,空靶了若干次,后来强打精神,努力瞄准,终于上靶了,甚至还蒙中了一发靶心。


调整射击姿势


这次射击体验呢,整体感觉是隔靴搔痒,瞄准、扣动扳机,多少感觉有点麻木,当然也可能和我此时头脑已经昏沉有关。


我买了五十发子弹,本来以为不少,打完却觉得意犹未尽。最后离开的时候,旁边的步枪靶场,一声巨响,我的耳朵瞬间轰鸣,回头一看,原来是有个大哥在打霰弹枪。


步枪靶场


没有哪个男人不爱枪,希望以后有机会能更深度地体验甚至学习射击。


感受


人生意义就是体验,我们回忆的时候,永远只会回忆自己体验过的东西,看一万个短视频,看一百本书,不如一次滚烫的沙滩来的记忆深刻。


泰国这个国家给我的感受,就是混杂,各种东西混杂,有寺庙的肃穆,有海岛的宁静、有拳赛的刺激,有红灯区的放荡…就像冬阴功汤,一下子把各种味道混杂在了一起。


在芭提雅和曼谷的街头,也能感受到泰国的贫富差距,大街时不时能看到几辆超跑,但更多的是坐在路边的乞丐,那天阿宇查了一下泰国王室的财富,他惊讶了一下,我也是——殿堂之下有杂草。


泰国很自由,但我觉得太过自由不是一件好事,大麻店、红灯区…这类声色刺激的东西,其实就像是表面能看到的纱布,底下不知道隐藏着多少溃烂的伤口,感谢我们的国家,社会主义铁拳,压得这些东西抬不了头。


最后,世界这么大,还是要去看看!




参考



  1. 《泰国常识》

  2. 《泰国通史》

  3. 《爱上泰国:你的色彩惊艳了我的时光》

  4. 《泰国史》

  5. 《泰国攻略》


作者:三分恶
来源:juejin.cn/post/7364785775345762356
收起阅读 »

怎么用一句话证明你在游戏公司里的最底层?

引言 今天在知乎看到一个有趣的帖子:如何一句话证明你在公司最底层?我们把范围缩小到游戏公司。 关于这个问题,身边80%的朋友描述了自己在公司底层的难忘回忆,还有几位朋友甚至因为这不堪的回忆破防了。 刚进入游戏公司的新人,迷茫是常态。和大家一样,笔者也曾是公司的...
继续阅读 »

如何一句话证明你在公司最底层?


引言


今天在知乎看到一个有趣的帖子:如何一句话证明你在公司最底层?我们把范围缩小到游戏公司。


关于这个问题,身边80%的朋友描述了自己在公司底层的难忘回忆,还有几位朋友甚至因为这不堪的回忆破防了。


刚进入游戏公司的新人,迷茫是常态。和大家一样,笔者也曾是公司的最底层,总觉得每天一睁眼就是各种困难的事等着我:



担心工作内容不会做,担心与同事沟通不好,担心自己考核不过关......



今天的这篇文章,大家一起来看看一位位于游戏公司底层的游戏开发者的最底层体验。


最底层体验


图片源于网络


1.介绍一下你自己


大家好,我是XXX,来自XXX。虽然我是一个新人,但我对游戏充满了热情,这种热情已经伴随我多年。小时候,我就沉迷于各种游戏,从那时起,我就梦想着有一天能够为创造令人陶醉的游戏世界做出贡献。我加入这个行业的目标是成为一个出色的游戏开发者,并参与创造令人惊叹的游戏体验。我相信,通过与这个行业的优秀人才一起工作,我可以不断成长,并为我们的团队和项目做出贡献。谢谢大家,请多多指教。


此处应有一阵热烈的掌声,那是对一位懵懂的游戏行业新人的勇敢表示敬畏。他或许不知道他的棱角将在这里被磨平。


熟悉又让人崩溃的弹窗


2.熟悉项目,体验游戏。


游戏行业新人刚进到游戏公司,可能第一件事就是登陆公司内部使用的通讯工具。你的直属上司可能早早的在网线那头等候着你的上线。


你好,XXX。你先接收一下这份文档,仔细阅读一下里面的内容。检出一下公司的游戏项目,然后根据文档把游戏跑起来。体验一下游戏,熟悉一下游戏的每个系统。有问题可以请教你旁边的那位大神,他负责带你。


好的,谢谢。由于在来公司之前做足了准备,检出项目、运行项目这种小问题肯定难不倒你。这时候你会惊讶,原来这就是大型的商业化游戏项目,看起来有那么点高大上,但是最多的是还是看不懂。不过这游戏玩着好无聊,不是我喜欢的类型。想到未来的日子里,需要不停地重复地在这个游戏里面遨游,"真的会谢"。



3.分配任务



  • 修改禅道bug序号XXX的问题。

  • 修改活动XXX文本显示异常问题。

  • 修改XXX报错问题,完成禅道单子序号1、2、3、4、5......。


游戏行业新人的入门任务往往就是这些看起来微不足道,但是却非常细节的问题。正所谓不积跬步无以至千里,通过慢慢处理这些小小的bug和显示异常的问题,无疑是熟悉项目的最好方式。虽然这些都是比较基本的内容,修改bug、调整UI、修复报错。但是能够体现一个新人的基本功:阅读问题描述、理解问题描述、定位问题所在系统、定位系统所在代码、读懂代码原有逻辑、修改错误代码、验证问题是否修复、思考会不会对其他内容造成影响。


这对于管理者来说是非常合理的,但对于新人来说,未免太过于简单了。


支线任务


4.支线任务


游戏行业新人入门有可能并不能第一时间接触到游戏项目主分支的代码,往往是参与其他的一些分支版本,例如审核服(专门为了应对平台审核员的审核搭建的游戏服)、版署服(用于申请版号专门搭建的游戏服)、海外服(主要负责多语言版本的语言提取、翻译替换、本地化处理)等等。


安排新人去处理这些支线任务,为的就是让新人从另外一个相对安全的分支去熟悉游戏项目,避免因新人的处理不当造成线上版本出问题,从而造成公司的经济损失。支线任务通常就是枯燥单一的体力劳动,不需要过多的技巧,只需要耐得住寂寞的心。


图片源于网络


5.几点下班


一位有着远大抱负的新人,往往在刚进入公司的日子里,不知道几点下班。领导分配给我的任务,实在太简单了,三两下就完成了,还不到规定的时间。为了能够更加快速地熟悉项目,参与游戏功能的开发,继续研究代码。


HR说19点下班,但是18点的时候大家都跑去吃饭,不解,跟着。等到19点的时候,果然没有人下班。继续奋笔疾书。20点的时候终于有人下班了,可是领导还是没动静,算了,再看看代码吧。21点,领导好像发现了这个新人,让他早点回去休息。(没有人告诉他,这将是常态。) "没事,我再看会代码,马上就回去了。"


手机先吃


6.福利


同事: “公司发月饼了,你没去领吗?”,“不知道啊,没人通知。我刚来几天。”


同事: ”我看大家都去领了,现在。“,”我不知道自己是否算正式员工“


同事: ”你先去看看吧,反正大家都在领。“,兴致冲冲地跑到发月饼的地方。


发月饼的: “叫什么名字?”,”XXX“


发月饼的: ”名单上没这个人,不能领!“


刚加入公司的时候,可能由于没转正或者名字还没有进入公司的名册,往往会导致有些福利不能享受。例如公司发月饼的时候,人人有份,唯独你。又或者公司发奖金,你拿200慰问金。公司发年终奖,你还是拿慰问金。 但是如果你想请假,领导秒批。甚至说你想离职,领导也是轻描淡写,“好的”。没有丝毫的牵挂留恋。这是前所未有的福利。


结语


不管怎样,虽然你是公司的最底层,但你是公司中最坚实的基石,因为你在每一颗砖石上都留下了你的汗水和努力,为了让整座大厦能够稳固地矗立在成功的巅峰。加油,请认真工作,积极向上。




作者:亿元程序员
来源:juejin.cn/post/7281589318329925689
收起阅读 »

从事程序媛工作的我都经历了什么?

第一段工作经历     大学一直就是学的Java,学校在湖南后面安排去浙江嘉兴实习做毕业设计项目的时候发现更加喜欢前端, 就想往这个方向去发展,2020年6月毕业就开始了我的前端求职之路,好在那个时候互联网对这块需求还是挺大的,以应届生的身份成功入职了一家小型...
继续阅读 »

第一段工作经历


    大学一直就是学的Java,学校在湖南后面安排去浙江嘉兴实习做毕业设计项目的时候发现更加喜欢前端,
就想往这个方向去发展,2020年6月毕业就开始了我的前端求职之路,好在那个时候互联网对这块需求还是挺大的,以应届生的身份成功入职了一家小型公司,面试也比较简单,更多的还是了解性格和学习方式以及自己对未来发展想法,也很顺利就入职了。


离职原因


    在那家公司大概做了半年,到了发展的瓶颈期,就两个前端,来来往往离职了三批人,就我还在原地,很多时候感觉学不到什么实际性的东西就果断离职了......


   我发现入职的第一家公司对自己的职业规划还是挺重要的,有人带你给予你学习的方向这点很重要,但可惜我没有这样的运气,基本都是靠自己在工作中摸索和自学试错成本也高,学校学的大部分东西在工作中基本都派不上用场,这也让我很苦恼,不过感觉大部分的人基本都是这样吧......


第二段工作经历


    第二家公司是一个大型的厂做晶导体和手机电脑电子产品,幸运的是,遇上了急需人手的时候,面试基本都能对答如流,他们对我也挺满意的,就破格让大专学历的我入职了,收到offer的时候还是很开心的,毕竟这样的机会少之又少......


    实际开发的时候整个IT开发团队都是分不同的组,我所在的组做的项目更多的是偏向公司内部的考勤、人事、薪资这类型的后台管理系统。项目用的vue2是基于vue-element-template后台模板进行二次开发,之前的老大会进行项目搭建给我们顺流程和定开发规则,让我根据规则来进行模块开发,这在某种程度上做了统一性也省了很多的麻烦更方便后期维护。


    熟悉了之后有时候也自己尝试从0开始搭建项目自己也学到了很多。其他的项目组更多的是跟流水线上的产品打交道,基本每天都在加班改需求事情太多流水线也不稳定,有bug的时候都要在公司守株待兔去解决这种突发情况,但是我几乎都是早上8.30上班到下午5.30就下班,加班的情况很少。


    上班有时候活多的时候就忙的键盘真的冒火,不忙的时候时间都属于自己,通过刷刷前端的视频,看看知乎和掘金之类的补充能量来度过这普通的一天......


嘉兴生活


    工作坐标嘉兴,是个很悠闲慢节奏适合养老的城市,不内卷,生活也没什么压力,消费也不高,过的很轻松,平时周末约上小姐妹出去玩玩逛逛街啥的,后面搬家换了个离公司近的地方感觉晚上阴森森的有点害怕,就养了一只蓝猫,现在已经快4岁了,性格超级好,每天等着我回家跟家人一样很温暖......搬家后通勤时间基本骑个共享电动车10分钟左右就能到,也没有啥特别大的变化,除了不包吃住,其他方面真的挺好的,同事也很好相处,后面还内推了一个姐妹来这边上班,刚好也是大学同学,我们就开始了合租的生活,她也有一只猫猫,就这样过上了两人两猫的生活,这样的生活持续了一年半左右...


离开浙江去深圳


    后面找男朋友了,算是大学同学,实习的时候分到一个班级做毕业设计项目,变成了同桌。那个时候我们还不熟,很少讲话。我比较高冷,我俩对话仅限于问问作业以及项目上的一些问题,直到毕业设计答辩完我们两个加起来的对话也不超过20句,加了个QQ也是为了方便传递老师布置的作业,偷懒直接抄他的作业罢了......


    2020年疫情居家期间,无聊就开始玩王者,突然看到他在线,就随便点了一下邀请,结果他同意了,这在我意料之外,然后就开始带游戏,慢慢熟悉起来了,这样的生活持续了差不多一年左右,算是暧昧期吧。2021的某一天,突然在QQ上跟我表白,我没准备好拒绝了!后面我们状态依旧持续这样,大概过了一两个月之后,我感觉我们之间状态还是没变化,后面就同意了在一起了。就这样开始了异地恋,我们除了五一和国庆放假会去到双方城市见面到处去玩,其他时候都在自己的城市做着忙碌着自己的工作,大部分也只能通过手机微信聊天去了解和关心对方。


    他跟我一样都是做前端,一开始他在中山工作,工作一段时间他感觉工资低就去深圳发展,在那边上班时间越来越久,通勤时间是一个小时左右,我们只有下班才有机会聊聊天也让我们格外珍惜,我每天都等他下班洗漱完就一块休息,久而久之习惯了,基本每天晚上都会开着语音一整晚不挂电话。后面下班越来越晚,异地了一年多,很多矛盾开始出现。就不太想继续异地恋,他就想让我去深圳发展,他觉得我们老家都在湖南,离深圳比较近,我想让他来浙江,他说离家太远了,家里有爷爷奶奶回去一趟不方便,后面拗不过,争执了半年,最后我妥协去了深圳......


深圳工作后续


    2022年7月份来的深圳,提前把猫从浙江托运到深圳,花了我800大洋。行李太多,打包了一堆包裹寄过去让他给我拿。跨越1600公里坐了8个小时的高铁,不远千里只为你而来!我当时想:“如果有个人能为我这样,我辜负全世界也绝对不能辜负他”。


    我男朋友提前一个半小时坐公交来高铁站接我,傻傻的在那等了我一个多小时。到站下车就快要见面的那段时间我心里好紧张,有点不知所措,他在出口等我,看到对方后,一时间双方都有点尴尬,不知道该说些什么......


    每次很久没见之后都会这样,但我还挺喜欢这种感觉,我称之为属于两人的“新鲜感”。他先开的口:"坐车辛苦了,累不累,饿不饿?”,我回了个还好,接过我手里的密码箱,知道我中午没吃饭,提前从车站下面的商圈打包了一份鱼粉。给我找了个能坐着的地方,递给我让我先吃,不知道怎么描述当时的场景又好笑又温馨,异地了一年多,终于奔现的感觉(~ ̄(OO) ̄)ブ。让我开心又有点陌生,之前都是短暂的相聚,所以这次跟以往的感觉都不太一样,打怪终于打到大BOSS了......


   他一直看着我吃,我有些不好意思,坐了一天车顾不上形象有些许狼狈。我叫他背对着我别老看,他就时不时偷瞄,一碗粉都能吃半个小时挺不可思议的~ 我偶尔也关注看着他,只是不敢直视他只是偷瞄,可能是害羞吧(✿◡‿◡)


   他穿着一个白衬衫牛仔裤白白净净的,那天的印象一直留存在我的脑海中,都说情人眼里出西施,不外乎这种吧,人活几个瞬间,喜欢也是,就是在某个时间里面在某种环境的衬托下刚好对上了眼刚好他的行为让你有喜欢和心动的感觉....


   吃了准备回去了,走着走着突然停下:“把手给我”,我挺惊讶的也挺开心的,感觉他的手大能直接把我两个手都包住,天气太热了,牵手热的出汗了也没放开过......后面不太熟悉高铁站,他带着我转了大半个小时,后面终于找到出口了,打了个出租车花了70多,真的颠覆了我的想象力,深圳消费确实是挺高的! 一路上看着深圳的风景对这个地方充满了好奇......


          持续更新中......


作者:小婉婉
来源:juejin.cn/post/7363209007829041167
收起阅读 »

Node拒绝当咸鱼,Node 22大进步

web
这几年,deno和bun风头正盛,大有你方唱罢我登场的态势,deno和bun的每一次更新版本,Node都会被拿来比较,比较结果总是Node落后了。 这种比较是不是非常熟悉,就像卖手机的跟iPhone比,卖汽车的跟特斯拉比,比较的时候有时候还得来个「比一分钱硬币...
继续阅读 »

这几年,deno和bun风头正盛,大有你方唱罢我登场的态势,deno和bun的每一次更新版本,Node都会被拿来比较,比较结果总是Node落后了。


这种比较是不是非常熟悉,就像卖手机的跟iPhone比,卖汽车的跟特斯拉比,比较的时候有时候还得来个「比一分钱硬币还薄」的套路。


1.png


Node虽然没有落后了,但是确实有点压力了,所以20和22版本都大跨步前进,拒绝当咸鱼了。


因为Node官网对22版本特性的介绍太过简单,所以我决定来一篇详细介绍新特性的文章,让学习Node的朋友们知道,Node现在在第几层。


首先我把新特性分为两类,分别是:开发者可能直接用到的特性、开发者相对无感知的底层更新。本文重点介绍前者,简单介绍后者。先来一个概览:


开发者可能直接用到的特性:



  1. 支持通过 require() 引入ESM

  2. 运行 package.json 中的脚本

  3. 监视模式(--watch)稳定化

  4. 内置 WebSocket 客户端

  5. 增加流的默认高水位线

  6. 文件模式匹配功能


开发者相对无感知的底层更新:



  1. V8 引擎升级至 12.4 版本

  2. Maglev 编译器默认启用

  3. 改进 AbortSignal 的创建性能


接下来开始介绍。


支持通过 require() 导入 ESM


以前,我们认为 CommonJS 与 ESM 是分离的。


例如,在 CommonJS里,我们用并使用 module.exports 导出模块,用 require() 导入模块:


// CommonJS

// math.js
function add(a, b) {
return a + b;
}
module.exports.add = add;

// useMath.js
const math = require('./math');
console.log(math.add(2, 3));

在 ECMAScript Modules (ESM) **** 里,我们使用 export 导出模块,用 import 导入模块:


// ESM

// math.mjs
export function add(a, b) {
return a + b;
}

// useMath.js
import { add } from './math.mjs';
console.log(add(2, 3));

Node 22 支持新的方式——用 require() 导入 ESM:


// Node 22

// math.mjs
export function add(a, b) {
return a + b;
}

// useMath.js
const { add } = require('./mathModule.mjs');
console.log(add(2, 3));

这么设计的原因是为了给大型项目和遗留系统提供一个平滑过渡的方案,因为这类项目难以快速全部迁移到 ESM,通过允许 require() 导入 ESM,开发者就可以逐个模块迁移,而不是一次性对整个项目进行修改。


目前这种写法还是实验性功能,所以使用是有“门槛”的:



  • 启动命令需要添加 -experimental-require-module 参数,如:node --experimental-require-module app.js

  • 模块标记:确保 ESM 模块通过 package.json 中的 "type": "module" 或文件扩展名是 .mjs

  • 完全同步:只有完全同步的ESM才能被 require() 导入,任何含有顶级 await 的ESM都不能使用这种方式加载。


运行package.json中的脚本


假设我们的 package.json 里有一个脚本:


"scripts": {
"test": "jest"
}

在此之前,我们必须依赖 npm 或者 yanr 这样的包管理器来执行命令,比如:npm run test


Node 22 添加了一个新命令行标志 --run,允许直接从命令行执行 package.json 中定义的脚本,可以直接使用 node --run test 这样的命令来运行脚本。


刚开始我还疑惑这是不是脱裤子放屁的行为,因为有 node 的地方一般都有 npm,我要这 node —run 有何用?


后来思考了一下,主要原因应该还是统一运行环境和提升性能。不同的包管理器在处理脚本时可能会有微小的差异,Node 提供一个标准化的方式执行脚本,有助于统一这些行为;而且直接使用 node 执行脚本要比通过 npm 执行脚本更快,因为绕过了 npm 这个中间层。


监视模式(--watch)稳定化


在 19 版本里,Node 引入了 —watch 指令,用于监视文件系统的变动,并自动重启。22 版本开始,这个指令成为稳定功能了。


要启用监视模式,只需要在启动 Node 应用时加上 --watch ****参数。例如:


node --watch app.js

正在用 nodemon 做自动重启的朋友们可以正式转战 --watch 了~


内置 WebSocket 客户端


以前,要用 Node 开发一个 socket 服务,必须使用 ws、socket.io 这样的第三方库来实现。第三方库虽然稳如老狗帮助开发者许多年,但是终究是有点不方便。


Node 22 正式内置了 WebSocket,并且属于稳定功能,不再需要 -experimental-websocket 来启用了。


除此之外,WebScoket 的实现还遵循了浏览器中 WebSocket API 的标准,这意味着在 Node 中使用 WebSocket 的方式将与在 JavaScript 中使用 WebSocket 的方式非常相似,有助于减少学习成本并提高代码的一致性。


用法示例:


const socket = new WebSocket("ws://localhost:8080");

socket.addEventListener("open", (event) => {
socket.send("Hello Server!");
});

增加流(streams)的默认高水位线(High Water Mark)


streams 在 Node 中有举足轻重的作用,读写数据都得要 streams 来完成。而 streams 可以设置 highWaterMark 参数,用于表示缓冲区的大小。highWaterMark 越大,缓冲区越大,占用内存越多,I/O 操作就减少,highWaterMark 越小,其他信息也对应相反。


用法如下:


const fs = require('fs');

const readStream = fs.createReadStream('example-large-file.txt', {
highWaterMark: 1024 * 1024 // 设置高水位线为1MB
});

readStream.on('data', (chunk) => {
console.log(`Received chunk of size: ${chunk.length}`);
});

readStream.on('end', () => {
console.log('End of file has been reached.');
});

虽然 highWaterMark 是可配置的,但通常情况下,我们是使用默认值。在以前的版本里,highWaterMark 的默认值是 16k,Node 22 版本开始,默认值被提升到 64k 了。


文件模式匹配——glob 和 globSync


Node 22 版本在 fs 模块中新增了 globglobSync 函数,它们用于根据指定模式匹配文件路径。


文件模式匹配允许开发者定义一个匹配模式,以找出符合特定规则的文件路径集合。模式定义通常包括通配符,如 *(匹配任何字符)和 ?(匹配单个字符),以及其他特定的模式字符。


glob 函数(异步)


glob 函数是一个异步的函数,它不会阻塞 Node.js 的事件循环。这意味着它在搜索文件时不会停止其他代码的执行。glob 函数的基本用法如下:


const { glob } = require('fs');

glob('**/*.js', (err, files) => {
if (err) {
throw err;
}
console.log(files); // 输出所有匹配的.js文件路径
});

在这个示例中,glob 函数用来查找所有子目录中以 .js 结尾的文件。它接受两个参数:



  • 第一个参数是一个字符串,表示文件匹配模式。

  • 第二个参数是一个回调函数,当文件搜索完成后,这个函数会被调用。如果搜索成功,err 将为 null,而 files 将包含一个包含所有匹配文件路径的数组。


globSync 函数(同步)


globSyncglob 的同步版本,它会阻塞事件循环,直到所有匹配的文件都被找到。这使得代码更简单,但在处理大量文件或在需要高响应性的应用中可能会导致性能问题。其基本用法如下:


const { globSync } = require('fs');

const files = globSync('**/*.js');
console.log(files); // 同样输出所有匹配的.js文件路径

这个函数直接返回匹配的文件数组,适用于脚本和简单的应用,其中执行速度不是主要关注点。


使用场景


这两个函数适用于:



  • 自动化构建过程,如自动寻找和处理项目中的 JavaScript 文件。

  • 开发工具和脚本,需要对项目目录中的文件进行批量操作。

  • 任何需要从大量文件中快速筛选出符合特定模式的文件集的应用。


V8 引擎升级至 12.4 版本


从这一节开始,我们了解一下开发者相对无感知的底层更新,第一个就是 V8 引擎升级到 12.4 版本了,有了以下特性升级:



  • WebAssembly 垃圾回收:这一特性将改善 WebAssembly 在内存管理方面的能力。

  • Array.fromAsync:这个新方法允许从异步迭代器创建数组。

  • Set 方法和迭代器帮助程序:提供了更多内建的Set操作和迭代器操作的方法,增强了数据结构的操作性和灵活性。


Maglev 编译器默认启用


Maglev 是 V8 的新编译器,现在在支持的架构上默认启用。它主要针对短生命周期的命令行程序(CLI程序)性能进行优化,通过改进JIT(即时编译)的效率来提升性能。这对开发者编写的工具和脚本将带来明显的速度提升。


改进AbortSignal的创建性能


在这次更新中,Node 提高了 AbortSignal 实例的创建效率。AbortSignal 是用于中断正在进行的操作(如网络请求或任何长时间运行的异步任务)的一种机制。通过提升这一过程的效率,可以加快任何依赖这一功能的应用,如使用 fetch 进行HTTP请求或在测试运行器中处理中断的场景。


AbortSignal 的工作方式是通过 AbortController 实例来管理。AbortController 提供一个 signal 属性和一个 abort() 方法。signal 属性返回一个 AbortSignal 对象,可以传递给任何接受 AbortSignal 的API(如fetch)来监听取消事件。当调用abort()方法时,与该控制器关联的所有操作将被取消。


const controller = new AbortController();
const signal = controller.signal;

fetch(url, { signal })
.then(response => response.json())
.catch(err => {
if (err.name === 'AbortError') {
console.log('Fetch aborted');
} else {
console.error('Fetch error:', err);
}
});

// 取消请求
controller.abort();

总结


最后,我只替 Node 说一句:Node 没有这么容易被 deno 和 bun 打败~


3.jpeg


关于我


全栈工程师,Next.js 开源手艺人,AI降临派。


今年致力于 Next.js 和 Node.js 领域的开源项目开发和知识分享。




作者:程普
来源:juejin.cn/post/7366185272768036883
收起阅读 »