注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

Swift Enum 关联值嵌套的一些实践

iOS
前言 Swift 中的枚举很强大,算是一等公民。可以定义函数,也可以遵守协议、实现 extension 等等。 关联值也是 Swift 枚举的一大特性。基本用法如下:enum RequestResult { case success case ...
继续阅读 »

前言


Swift 中的枚举很强大,算是一等公民。可以定义函数,也可以遵守协议、实现 extension 等等。


关联值也是 Swift 枚举的一大特性。基本用法如下:

enum RequestResult {
case success
case failure(Error)
}

let result = RequestResult.failure(URLError(URLError.timedOut))
switch result {
case .success:
print("请求成功")
case .failure(let error):
print(error)
}

1、在需要关联值的 case 中声明关联值的类型。


2、在 switch 的 case 中声明一个常量或者变量来接收。


遇到的问题


一般情况下,上述的代码是清晰明了的。但在实际开发的过程中,遇到了以下的情况:关联值的类型也是枚举,而且嵌套不止一层。


比如下面的代码:

enum EnumT1 {
case test1(EnumT2)
case other
}

enum EnumT2 {
case test2(EnumT3)
case other2
}

enum EnumT3 {
case test3(EnumT4)
case test4
}

根据我们的需求,需要进行多次嵌套来进行类型细化。当进行枚举的声明时,代码还是正常的,简单明了。但当进行 case 判断时,代码就变得丑陋难写了。


比如,我只想处理 EnumT3 中的 test4 的情况,在 switch 中我需要进行 switch 的嵌套来处理:

let t1: EnumT1? = .test1(.test2(.test4))
switch t1 {
case .test1(let t2):
switch t2 {
case .test2(let t3):
switch t3 {
case .test4:
print("test4")
case default:
print("default")
}
default:
print("default")
}
default:
print("default")
}

这种写法,对于一个程序员来说是无法忍受的。它存在两个问题:一是代码臃肿,我的本意是只处理某一种情况,但我需要显式的嵌套多层 switch;二是枚举本身是不推荐使用 default 的,官方推荐是显式的写出所有的 case,以防出现难以预料的问题。


废话不多说,下面开始简化之路。


实践一


首先能想到的是,因为是对某一种情况进行处理,考虑使用 if + == 的判断来进行处理,比如下面这种写法:

if t1 == .test1(.test2(.test4)) { }

这样处理有两个不足之处。首先,如果对枚举用 == 操作符的话,需要对每一个枚举都遵守 Equatable 协议,这为我们带来了工作量。其次最重要的是,这种处理方式无法应对 test3 这种带有关联值的情况。

if t1 == .test1(.test2(.test3) { } 

如果这样写的话,编译器会报错,因为 test3 是需要传进去一个 Int 值的。

if t1 == .test1(.test2(.test3(20))) { }

如果这样写的话也不行,因为我们的需求是处理 test3 的统一情况(所有的关联值),而不是某一个具体的关联值。


实践二


经过在网上的一番搜查,发现可以用 if-case 关键字来简化写法:

if case .test1(.test2(.test3)) = t1 { }

这样就能统一处理 test3 这个 case 的所有情况了。如果想获取关联值,可以用下面的写法:

if case .test1(.test2(.test3(let i))) = t1 {
print(i)
}

对比上面的 switch 写法,可以看到,下面的这种写法既易懂又好写😁。


总结来说,当我们遇到关联值多层枚举嵌套的时候,又需要对某一种情况进行处理。那么可以采用实践二的做法来进行代码简化。


参考链接


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

做点微小的工作,实现 iOS 日历和提醒事项双向同步

iOS
前言 作为一名资深谷粉和十年的 Android 用户,在 2020 年看着各家厂商在笔记本、手机、手表、耳机甚至是智能家居上不断推成出新,补齐数字生活的每一块拼图,辅以“生态化反”的概念牢牢绑住每一个入坑的用户,此时再看看自己手里孤身寡人的 Pixel 手机,...
继续阅读 »

前言


作为一名资深谷粉和十年的 Android 用户,在 2020 年看着各家厂商在笔记本、手机、手表、耳机甚至是智能家居上不断推成出新,补齐数字生活的每一块拼图,辅以“生态化反”的概念牢牢绑住每一个入坑的用户,此时再看看自己手里孤身寡人的 Pixel 手机,以及不知何时就被砍掉的 Pixelbook 系列,默默留下了悔恨的泪水。久苦于谷歌令人失望的硬件生态,我终于还是放弃了 Android 生态,转身拥抱苹果全家桶。苹果硬件生态品类齐全,多年深耕的软件生态和云服务也赋予了这些硬件无缝的使用体验。但有一点一直令我不解,那就是 iOS 的自带应用:日历和提醒事项,它们的事件竟不是相互联动的。而在谷歌套件中,只要一个任务在 Google Tasks 中被新增或是被勾选完成,就会自动同步到 Google Calendar 中,以方便用户进行日程安排或是日程回顾。虽然第三方应用如滴答清单、Sunsama 也提供了类似的功能,但为了原生(免费)体验,只能自己动手折腾了。


前提条件


为了在 iOS 上实现日历和提醒事项双向同步的效果,需要借助快捷指令,搭配 JSBox 写一个脚本,创建数据库来绑定和管理日历和提醒事项中各自的事件。

  1. iOS 14+;
  2. 愿意花 40 RMB 开通 JSBox 高级版;
  3. 不满足第2点,则需要设备已越狱,或者装有 TrollStore;

*破解 JSBox


步骤:

  1. 在 App Store 安装 JSBox;
  2. 通过越狱的包管理工具或者 TrollStore 安装 Apps Manager;
  3. 下载 JSBox 备份文件,在文件管理中长按该文件,选择分享,使用 Apps Manager 打开,在弹出的菜单中点取消;
  4. 在 Apps Manager 中的 Applications 选项卡中,选择 JSBox,点击 Restore 进行还原,即可使用 JSBox 高级版功能(在 JSBox 中的设置选项卡中不要点击“JSBox 高级版”选项,否则需要再次还原);



加载脚本


步骤:

  1. 下载 Reminders ↔️ Calendar 项目文件,在文件管理中长按该文件,选择分享,使用 JSBox 打开;
  2. 在日历和提醒事项中各自新建一个“test”列表,在提醒事项的“test”列表中新建一个定时事件;
  3. 返回 JSBox 中的 Reminders ↔️ Calendar 项目,点击界面下的“Sync now”按钮;
  4. 回到日历中查看事件是否同步成功;



设置项说明:

  1. 同步周期 —— 周期内的事件才会被同步;
  2. 同步备注 —— 是否同步日历和提醒事项的备注;
  3. 同步删除 —— 删除一方事件时,是否自动删除另一方对应的事件;
  4. 单边提醒 —— 日历和提醒事项的事件,谁创建谁通知,关闭则日历和提醒事项都会通知;
  5. 历史待办默认超期完成 —— 补录历史待办,是否默认为已完成;
  6. 提醒事项:默认优先级 —— 在日历创建的事件,同步到提醒事项时候默认的优先级;
  7. 日历:默认用时 —— 在提醒事项创建的事件,同步到日历时默认的时间间隔;
  8. 日历:快速跳转 —— 日历的事件是否在链接项中添加跳转到对应提醒事项的快速链接;
  9. 日历:显示剩余时间 —— 日历的事件是否在地点项中添加时间信息;
  10. 日历:完成变全天 —— 日历的事件是否在完成时,自动变成全天事件(这样日历视图就会将该项目置顶,方便查看未完成项目);



设置快捷指令


步骤:

  1. 打开快捷指令应用,选择自动化选项卡,点击右上角 + 号新增一个任务;
  2. 选择新建个人自动化,设置触发条件为打开应用,指定应用为日历和提醒事项,点击下一步;
  3. 点击按钮新增一个行动,选择执行 JSBox 脚本,在脚本名上填入“Reminders ↔️ Calendar”,点击右下角的 ▶️ 测试,如果输出成功则点击下一步;(注意区分执行 JSBox 脚本和执行 JSBox 界面);
  4. 关闭执行前询问的选项,点击右上角的完成保存任务;



总结


JSBox 是一款运行在 iOS 设备上的轻量级脚本编辑器和开发环境。它内置了大量的 API,允许用户使用 JavaScript 访问原生的 iOS API。另一款相似的应用 Scriptable 在语法的书写上更亲和,但其暴露的事件对象中缺少 last modified 字段,当信息不对称时,没有办法判断日历和提醒事项中事件的新旧。期待 Scriptable 的后续更新,毕竟它是免费的🤡。


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

在国企做程序员怎么样?

本文已经收录到Github仓库,该仓库包含计算机基础、Java核心知识点、多线程、JVM、常见框架、分布式、微服务、设计模式、架构等核心知识点,欢迎star~ Github地址:github.com/Tyson0314/J… Gitee地址:gitee.com...
继续阅读 »

本文已经收录到Github仓库,该仓库包含计算机基础、Java核心知识点、多线程、JVM、常见框架、分布式、微服务、设计模式、架构等核心知识点,欢迎star~


Github地址:github.com/Tyson0314/J…


Gitee地址:gitee.com/tysondai/Ja…



有读者咨询我,在国企做开发怎么样?


当然是有利有弊,国企相对稳定,加班总体没有互联网多,不过相对而言,工资收入没有互联网高,而且国企追求稳定,往往技术栈比较保守,很难接触新的技术,导致技术水平进步缓慢。


下面分享一位国企程序员的经历,希望能给大家一些参考价值。



下文中的“我”代表故事主人公



我校招加入了某垄断央企,在里面从事研发工程师的工作。下面我将分享一些入职后的一些心得体会。


在国企中,开发是最底层最苦B的存在,在互联网可能程序员还能够和产品经理argue,但是在国企中,基本都是领导拍脑袋的决定,即便这个需求不合理,或者会造成很多问题等等,你所需要的就是去执行,然后完成领导的任务。下面我会分享一些国企开发日常。


1、大量内部项目


在入职前几个月,我们都要基于一种国产编辑器培训,说白了集团的领导看市场上有eclipse,idea这样编译器,然后就说咱们内部也要搞一个国产的编译器,所有的项目都要强制基于这样一个编译器。


在国企里搞开发,通常会在项目中塞入一大堆其他项目插件,本来一个可能基于eclipse轻松搞定的事情,在国企需要经过2、3个项目跳转。但国企的项目本来就是领导导向,只需给领导演示即可,并不具备实用性。所以在一个项目集成多个项目后,可以被称为X山。你集成的其他项目会突然出一些非常奇怪的错误,从而导致自己项目报错。但是这也没有办法,在国企中搞开发,有些项目或者插件是被要求必须使用的。


2、外包


说到开发,在国企必然是离不开外包的。在我这个公司,可以分为直聘+劳务派遣两种用工形式,劳务派遣就是我们通常所说的外包,直聘就是通过校招进来的校招生。


直聘的优势在于会有公司的统一编制,可以在系统内部调动。当然这个调动是只存在于规定中,99.9%的普通员工是不会调动。劳务派遣通常是社招进来的或者外包。在我们公司中,项目干活的主力都是外包。我可能因为自身本来就比较喜欢技术,并且觉得总要干几年技术才能对项目会有比较深入的理解,所以主动要求干活,也就是和外包一起干活。一开始我认为外包可能学历都比较低或者都不行,但是在实际干活中,某些外包的技术执行力是很强的,大多数项目的实际控制权在外包上,我们负责管理给钱,也许对项目的了解的深度和颗粒度上不如外包。


上次我空闲时间与一个快40岁的外包聊天,才发现他之前在腾讯、京东等互联网公司都有工作过,架构设计方面都特别有经验。然后我问他为什么离开互联网公司,他就说身体受不了。所以身体如果不是特别好的话,国企也是一个不错的选择。


3、技术栈


在日常开发中,国企的技术一般不会特别新。我目前接触的技术,前端是JSP,后端是Springboot那一套。开发的过程一般不会涉及到多线程,高并发等技术。基本上都是些表的设计和增删改查。如果个人对技术没啥追求,可能一天的活2,3小时就干完了。如果你对技术有追求,可以在剩余时间去折腾新技术,自由度比较高。


所以在国企,作为普通基层员工,一般会有许多属于自己的时间,你可以用这些时间去刷手机,当然也可以去用这些时间去复盘,去学习新技术。在社会中,总有一种声音说在国企呆久了就待废了,很多时候并不是在国企待废了,而是自己让自己待废了。


4、升职空间


每个研发类央企都有自己的职级序列,一般分为技术和管理两种序列。


首先,管理序列你就不用想了,那是留给有关系+有能力的人的。其实,个人觉得在国企有关系也是一种有能力的表现,你的关系能够给公司解决问题那也行。


其次,技术序列大多数情况也是根据你的工龄长短和PPT能力。毕竟,国企研发大多数干的活不是研发与这个系统的接口,就是给某个成熟互联网产品套个壳。技术深度基本上就是一个大专生去培训机构培训3个月的结果。你想要往上走,那就要学会去PPT,学会锻炼自己的表达能力,学会如何讲到领导想听到的那个点。既然来了国企,就不要再想钻研技术了,除非你想跳槽互联网。


最后,在国企底层随着工龄增长工资增长(不当领导)还是比较容易的。但是,如果你想当领导,那还是天时地利人和缺一不可。


5、钱


在前面说到,我们公司属于成本单位,到工资这一块就体现为钱是总部发的。工资构成分由工资+年终奖+福利组成。


1.工资构成中没有绩效,没有绩效,没有绩效,重要的事情说三遍。工资是按照你的级别+职称来决定的,公司会有严格的等级晋升制度。但是基本可以概括为混年限。年限到了,你的级别就上去了,年限没到,你天天加班,与工资没有一毛钱关系。


2.年终奖,是总部给公司一个大的总包,然后大领导根据实际情况对不同部门分配,部门领导再根据每个人的工作情况将奖金分配到个人。所以,你干不干活,活干得好不好只和你的年终奖相关。据我了解一个部门内部员工的年终奖并不会相差太多。


3.最后就是福利了,以我们公司为例,大致可以分为通信补助+房补+饭补+一些七七八八的东西,大多数国企都是这样模式。


总结


1、老生常谈了。在国企,工资待遇可以保证你在一线城市吃吃喝喝和基本的生活需要没问题,当然房子是不用想的了。


2、国企搞开发,技术不会特别新,很多时候是项目管理的角色。工作内容基本体现为领导的决定。


3、国企研究技术没有意义,想当领导,就多学习做PPT和领导搞好关系。或者当一个平庸的人,混吃等死,把时间留给家人,也不乏是一种好选择。


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

像支付宝那样“致敬”第三方开源代码

前言 通常我们在App中会使用第三方的开源代码,按照许可协议,我们应该在App中公开使用的开源代码并且附上对应的开源协议。当然,实际上只有少部分注重合规性的大厂才会这么干,比如下图是支付宝的关于界面的第三方信息。当然,对于小企业,基本上都不会放使用的第三方开源...
继续阅读 »

前言


通常我们在App中会使用第三方的开源代码,按照许可协议,我们应该在App中公开使用的开源代码并且附上对应的开源协议。当然,实际上只有少部分注重合规性的大厂才会这么干,比如下图是支付宝的关于界面的第三方信息。当然,对于小企业,基本上都不会放使用的第三方开源代码的任何信息。 



不过,作为一个有“追求”的码农,我们还是想对开源软件致敬一下的,毕竟,没有他们我都不知道怎么写代码。然而,我们的 App 里用了那么多第三方开源插件,总不能一个个找出来一一致敬吧?怎么办?其实,Flutter 早就为我们准备好了一个组件,那就是本篇要介绍的 AboutDialog


AboutDialog 简介


AboutDialog 是一个对话框,它可以提供 App 的基本信息,如 Icon、版本、App 名称、版权信息等。 



同时,AboutDialog还提供了一个查看授权信息(View Licenses)的按钮,点击就可以查看 App 里所有用到的第三方开源插件,并且会自动收集他们的 License 信息展示。所以,使用 AboutDialog 可以让我们轻松表达敬意。怎么使用呢?非常简单,我们点击一个按钮的时候,调用 showAboutDialog 就搞定了,比如下面的代码:

IconButton(
onPressed: () {
showAboutDialog(
context: context,
applicationName: '岛上码农',
applicationVersion: '1.0.0',
applicationIcon: Image.asset('images/logo.png'),
applicationLegalese: '2023 岛上码农版权所有'
);
},
icon: const Icon(
Icons.info_outline,
color: Colors.white,
),
),

参数其实一目了然,具体如下:

  • context:当前的 context
  • applicationName:应用名称;
  • applicationVersion:应用版本,如果要自动获取版本号也可以使用 package_info_plus 插件。
  • applicationIcon:应用图标,可以是任意的 Widget,通常会是一个App 图标图片。
  • applicationLegalese:其他信息,通常会放置应用的版权信息。

点击按钮,就可以看到相应的授权信息了,点击一项就可以查看具体的 License。我看了一下使用的开源插件非常多,要是自己处理还真的很麻烦。 



可以说非常简单,当然,如果你直接运行还有两个小问题。


按钮本地化


AboutDialog 默认提供了两个按钮,一个是查看授权信息,一个是关闭,可是两个按钮 的标题默认是英文的(分别是VIEW LICENSES和 CLOSE)。 



如果要改成本地话的,还需要做一个自定义配置。我们扒一下 AboutDialog 的源码,会发现两个按钮在DefaultMaterialLocalizations中定义,分别是viewLicensesButtonLabelcloseButtonLabel。这个时候我们自定义一个类集成DefaultMaterialLocalizations就可以了。

class MyMaterialLocalizationsDelegate
extends LocalizationsDelegate<MaterialLocalizations> {
const MyMaterialLocalizationsDelegate();

@override
bool isSupported(Locale locale) => true;

@override
Future<MaterialLocalizations> load(Locale locale) async {
final myTranslations = MyMaterialLocalizations(); // 自定义的本地化资源类
return Future.value(myTranslations);
}

@override
bool shouldReload(
covariant LocalizationsDelegate<MaterialLocalizations> old) =>
false;
}

class MyMaterialLocalizations extends DefaultMaterialLocalizations {
@override
String get viewLicensesButtonLabel => '查看版权信息';

@override
String get closeButtonLabel => '关闭';

}

然后在 MaterialApp 里指定本地化localizationsDelegates参数使用自定义的委托类对象就能完成AboutDialog两个按钮文字的替换。

return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const AboutDialogDemo(),
localizationsDelegates: const [MyMaterialLocalizationsDelegate()],
);

添加自定义的授权信息


虽然 Flutter 会自动收集第三方插件,但是如果我们自己使用了其他第三方的插件的话,比如没有在 pub.yaml 里引入,而是直接使用了源码。那么还是需要手动添加一些授权信息的,这个时候我们需要自己手动添加了。添加的方式也不麻烦,Flutter 提供了一个LicenseRegistry的工具类,可以调用其 addLicense 方法来帮我们添加授权信息。具体使用如下:

LicenseRegistry.addLicense(() async* {
yield const LicenseEntryWithLineBreaks(
['关于岛上码农'],
'我是岛上码农,微信公众号同名。\f如有问题可以加本人微信交流,微信号:island-coder。',
);
});

这个方法可以在main方法里调用。其中第一个参数是一个数组,是因为可以允许多个开源代码共用一份授权信息。同时,如果一份开源插件有多个授权信息,可以多次添加,只要名称一致,Flutter就会自动合并,并且会显示该插件的授权信息条数,点击查看时,会将多条授权信息使用分割线分开,代码如下所示:

void main() {
runApp(const MyApp());
LicenseRegistry.addLicense(() async* {
yield const LicenseEntryWithLineBreaks(
['关于岛上码农'],
'我是岛上码农,微信公众号同名。如有问题可以加本人微信交流,微信号:island-coder。',
);
});

LicenseRegistry.addLicense(() async* {
yield const LicenseEntryWithLineBreaks(
['关于岛上码农'],
'使用时请注明来自岛上码农、。',
);
});
}



总结


本篇介绍了在 Flutter 中快速展示授权信息的方法,通过 AboutDialog 就可以轻松搞定,各位“抄代码”的码农们,赶紧用起来向大牛们致敬吧!


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

微信(群)接入ChatGPT,MJ聊天机器人Bot

前言 微信接入ChatGPT机器人还是挺有必要的,不用被墙,可以直接问它问题,还可以接入微信群等一些实用的功能。 注意:微信接入ChatGPT机器人Bot,微信账号极其容易被封,请谨慎接入 注意:微信接入ChatGPT机器人Bot,微信账号极其容易被封,...
继续阅读 »

前言


微信接入ChatGPT机器人还是挺有必要的,不用被墙,可以直接问它问题,还可以接入微信群等一些实用的功能。



注意:微信接入ChatGPT机器人Bot,微信账号极其容易被封,请谨慎接入




注意:微信接入ChatGPT机器人Bot,微信账号极其容易被封,请谨慎接入




注意:微信接入ChatGPT机器人Bot,微信账号极其容易被封,请谨慎接入



首先你需要一个 OpenAI 的账号并且创建一个可用的 api key,这里不做过多介绍,有任何问题可以加博客首页公告处微信群进行沟通。


相关的聊天机器人Bot GitHub上有非常多的项目,不仅支持接入ChatGPT,还支持接入MJ画图等一些其他功能。


本篇介绍两个项目(我用的第一个 chatgpt-on-wechat 项目):


chatgpt-on-wechat 项目最新版支持如下功能:

  • 多端部署: 有多种部署方式可选择且功能完备,目前已支持个人微信,微信公众号和企业微信应用等部署方式
  • 基础对话: 私聊及群聊的消息智能回复,支持多轮会话上下文记忆,支持 GPT-3,GPT-3.5,GPT-4模型
  • 语音识别: 可识别语音消息,通过文字或语音回复,支持 azure, baidu, google, openai等多种语音模型
  • 图片生成: 支持图片生成 和 图生图(如照片修复),可选择 Dell-E, stable diffusion, replicate模型
  • 丰富插件: 支持个性化插件扩展,已实现多角色切换、文字冒险、敏感词过滤、聊天记录总结等插件
  • Tool工具: 与操作系统和互联网交互,支持最新信息搜索、数学计算、天气和资讯查询、网页总结,基于 chatgpt-tool-hub 实现

支持 Linux、MacOS、Windows 系统(可在Linux服务器上长期运行),同时需安装 Python。



建议Python版本在 3.7.1~3.9.X 之间,推荐3.8版本,3.10及以上版本在 MacOS 可用,其他系统上不确定能否正常运行。


注意:Docker 或 Railway 部署无需安装python环境和下载源码



Windows、Linux、Mac本地部署


本地部署请参考官方文档,按照文档一步一步操作即可。


注意要安装相对应的环境,例如 Node、Python等,这里不做过多介绍,建议大家用 Docker 方式安装,无需关心环境问题,一个命令直接部署。


环境变量

# config.json文件内容示例
{
"open_ai_api_key": "YOUR API KEY", # 填入上面创建的 OpenAI API KEY
"model": "gpt-3.5-turbo", # 模型名称。当use_azure_chatgpt为true时,其名称为Azure上model deployment名称
"proxy": "127.0.0.1:7890", # 代理客户端的ip和端口
"single_chat_prefix": ["bot", "@bot"], # 私聊时文本需要包含该前缀才能触发机器人回复
"single_chat_reply_prefix": "[bot] ", # 私聊时自动回复的前缀,用于区分真人
"group_chat_prefix": ["@bot"], # 群聊时包含该前缀则会触发机器人回复
"group_name_white_list": ["ChatGPT测试群", "ChatGPT测试群2"], # 开启自动回复的群名称列表
"group_chat_in_one_session": ["ChatGPT测试群"], # 支持会话上下文共享的群名称
"image_create_prefix": ["画", "看", "找"], # 开启图片回复的前缀
"conversation_max_tokens": 1000, # 支持上下文记忆的最多字符数
"speech_recognition": false, # 是否开启语音识别
"group_speech_recognition": false, # 是否开启群组语音识别
"use_azure_chatgpt": false, # 是否使用Azure ChatGPT service代替openai ChatGPT service. 当设置为true时需要设置 open_ai_api_base,如 https://xxx.openai.azure.com/
"azure_deployment_id": "", # 采用Azure ChatGPT时,模型部署名称
"character_desc": "你是ChatGPT, 一个由OpenAI训练的大型语言模型, 你旨在回答并解决人们的任何问题,并且可以使用多种语言与人交流。", # 人格描述
# 订阅消息,公众号和企业微信channel中请填写,当被订阅时会自动回复,可使用特殊占位符。目前支持的占位符有{trigger_prefix},在程序中它会自动替换成bot的触发词。
"subscribe_msg": "感谢您的关注!\n这里是ChatGPT,可以自由对话。\n支持语音对话。\n支持图片输出,画字开头的消息将按要求创作图片。\n支持角色扮演和文字冒险等丰富插件。\n输入{trigger_prefix}#help 查看详细指令。"
}

配置说明:


1.个人聊天

  • 个人聊天中,需要以 "bot"或"@bot" 为开头的内容触发机器人,对应配置项 single_chat_prefix (如果不需要以前缀触发可以填写 "single_chat_prefix": [""])
    • 机器人回复的内容会以 "[bot] " 作为前缀, 以区分真人,对应的配置项为 single_chat_reply_prefix (如果不需要前缀可以填写 "single_chat_reply_prefix": "")

    2.群组聊天

  • 群组聊天中,群名称需配置在 group_name_white_list 中才能开启群聊自动回复。如果想对所有群聊生效,可以直接填写 "group_name_white_list": ["ALL_GROUP"]
    • 默认只要被人 @ 就会触发机器人自动回复;另外群聊天中只要检测到以 "@bot" 开头的内容,同样会自动回复(方便自己触发),这对应配置项 group_chat_prefix
    • 可选配置: group_name_keyword_white_list配置项支持模糊匹配群名称,group_chat_keyword配置项则支持模糊匹配群消息内容,用法与上述两个配置项相同。(Contributed by evolay)
    • group_chat_in_one_session:使群聊共享一个会话上下文,配置 ["ALL_GROUP"] 则作用于所有群聊

    3.语音识别

  • 添加 "speech_recognition": true 将开启语音识别,默认使用openai的whisper模型识别为文字,同时以文字回复,该参数仅支持私聊 (注意由于语音消息无法匹配前缀,一旦开启将对所有语音自动回复,支持语音触发画图);
    • 添加 "group_speech_recognition": true 将开启群组语音识别,默认使用openai的whisper模型识别为文字,同时以文字回复,参数仅支持群聊 (会匹配group_chat_prefix和group_chat_keyword, 支持语音触发画图);
    • 添加 "voice_reply_voice": true 将开启语音回复语音(同时作用于私聊和群聊),但是需要配置对应语音合成平台的key,由于itchat协议的限制,只能发送语音mp3文件,若使用wechaty则回复的是微信语音。

    4.其他配置

  • model: 模型名称,目前支持 gpt-3.5-turbotext-davinci-003gpt-4gpt-4-32k (其中gpt-4 api暂未完全开放,申请通过后可使用)
    • temperature,frequency_penalty,presence_penalty: Chat API接口参数,详情参考OpenAI官方文档。
    • proxy:由于目前 openai 接口国内无法访问,需配置代理客户端的地址,详情参考 #351
    • 对于图像生成,在满足个人或群组触发条件外,还需要额外的关键词前缀来触发,对应配置 image_create_prefix
    • 关于OpenAI对话及图片接口的参数配置(内容自由度、回复字数限制、图片大小等),可以参考 对话接口 和 图像接口 文档,在config.py中检查哪些参数在本项目中是可配置的。
    • conversation_max_tokens:表示能够记忆的上下文最大字数(一问一答为一组对话,如果累积的对话字数超出限制,就会优先移除最早的一组对话)
    • rate_limit_chatgptrate_limit_dalle:每分钟最高问答速率、画图速率,超速后排队按序处理。
    • clear_memory_commands: 对话内指令,主动清空前文记忆,字符串数组可自定义指令别名。
    • hot_reload: 程序退出后,暂存微信扫码状态,默认关闭。
    • character_desc 配置中保存着你对机器人说的一段话,他会记住这段话并作为他的设定,你可以为他定制任何人格 (关于会话上下文的更多内容参考该 issue)
    • subscribe_msg:订阅消息,公众号和企业微信channel中请填写,当被订阅时会自动回复, 可使用特殊占位符。目前支持的占位符有{trigger_prefix},在程序中它会自动替换成bot的触发词。

    本说明文档可能会未及时更新,当前所有可选的配置项均在该config.py中列出。


    Railway部署



    Railway 每月提供5刀和最多500小时的免费额度,目前大部分账号已无法免费部署


    1. 进入 Railway
    2. 点击 Deploy Now 按钮。
    3. 设置环境变量来重载程序运行的参数,例如open_ai_api_keycharacter_desc

    Docker方式搭建


    如果想一直跑起来这个项目,建议在自己服务器上搭建,如果在自己本地电脑上搭建,电脑关机后就用不了啦,下面演示的是在我服务器上搭建,和在本地搭建步骤是一样的。


    环境准备

    1. 域名、服务器购买
    2. 服务器环境搭建,需要系统安装docker、docker-compose
    3. docker、docker-compose安装:blog.fanjunyang.zone/archives/de…

    创建相关目录


    我自己放在服务器中 /root/docker_data/wechat_bot 文件夹下面

    mkdir -p /root/docker_data/wechat_bot
    cd /root/docker_data/wechat_bot

    创建yml文件


    /root/docker_data/wechat_bot文件夹下面新建docker-compose.yml文件如下:

    version: '2.0'
    services:
    chatgpt-on-wechat:
    image: zhayujie/chatgpt-on-wechat
    container_name: chatgpt-on-wechat
    security_opt:
    - seccomp:unconfined
    environment:
    OPEN_AI_API_KEY: 'YOUR API KEY'
    MODEL: 'gpt-3.5-turbo'
    PROXY: ''
    SINGLE_CHAT_PREFIX: '["bot", "@bot"]'
    SINGLE_CHAT_REPLY_PREFIX: '"[bot] "'
    GROUP_CHAT_PREFIX: '["@bot"]'
    GROUP_NAME_WHITE_LIST: '["ChatGPT测试群", "ChatGPT测试群2"]'
    IMAGE_CREATE_PREFIX: '["画", "看", "找"]'
    CONVERSATION_MAX_TOKENS: 1000
    SPEECH_RECOGNITION: 'False'
    CHARACTER_DESC: '你是ChatGPT, 一个由OpenAI训练的大型语言模型, 你旨在回答并解决人们的任何问题,并且可以使用多种语言与人交流。'
    EXPIRES_IN_SECONDS: 3600
    USE_LINKAI: 'False'
    LINKAI_API_KEY: ''
    LINKAI_APP_CODE: ''

    运行yml文件


    进入/root/docker_data/wechat_bot文件夹下面,运行命令:docker-compose up -d


    或者在任意文件夹下面,运行命令:docker-compose -f /root/docker_data/wechat_bot/docker-compose.yml up -d


    然后服务就跑起来了,运行 sudo docker ps 能查看到 NAMES 为 chatgpt-on-wechat 的容器即表示运行成功。


    使用


    运行以下命令可查看容器运行日志,微信扫描日志中的二维码登录后即可使用:

    sudo docker logs -f chatgpt-on-wechat

    插件使用:

    如果需要在docker容器中修改插件配置,可通过挂载的方式完成,将 插件配置文件 重命名为 config.json,放置于 docker-compose.yml 相同目录下,并在 docker-compose.yml 中的 chatgpt-on-wechat 部分下添加 volumes 映射:

    volumes:
    - ./config.json:/app/plugins/config.json

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

    如何不花钱也能拥有一个属于自己的在线网站、博客🤩🤩🤩

    作为一个资深的切图仔,我们难免会享有一个自己的博客,去分享一些文章啊或者一些学习笔记,那么这时候可能就需要一台服务器去把我们的项目部署到网上了,但是考虑到服务器价格昂贵,也并没有必要去花这么多钱搞这么一个东西。那么 Github pages 就为我们解决了这么...
    继续阅读 »

    作为一个资深的切图仔,我们难免会享有一个自己的博客,去分享一些文章啊或者一些学习笔记,那么这时候可能就需要一台服务器去把我们的项目部署到网上了,但是考虑到服务器价格昂贵,也并没有必要去花这么多钱搞这么一个东西。那么 Github pages 就为我们解决了这么一个烦恼,他能让我们不花钱也能拥有自己的在线网站。


    什么是 GitHub Pages?


    GitHub Pages 是 GitHub 提供的一个托管静态网站的服务。它允许用户将自己的代码仓库转化为一个在线可访问的网站,无需复杂的服务器设置或额外的托管费用。通过 GitHub Pages,我们可以轻松地创建个人网站、项目文档、博客或演示页面,并与其他开发者和用户分享自己的作品。


    使用


    要想使用这个叼功能,我们首先要再 Gayhub 上面建立一个仓库,如下图所示:




    紧接着我们使用 create-neat 来创建一个项目,执行以下命令:

    npx create-neat mmm

    跟着提示选择相对应的项目即可,选择 vue 或者 react 都可以。




    当项目创建成功之后我们进入到该目录并安装相关依赖包:

    pnpm add gh-pages --save-dev

    并在 package.json 文件中添加 homepage 字段,如下所示:

    "homepage": "http://xun082.github.io/mmm"

    其中 xun082 要替换为你自己 github 上面的用户名,如下图所示: 



    而 mmm 替换为我们刚才创建的仓库名称。


    接下来在 package.json 文件中 script 字段中添加如下属性:

      "scripts": {
    "start": "candy-script start",
    "build": "candy-script build",

    "deploy": "gh-pages -d dist"
    },

    完整配置如下所示:




    完成配置后我们将代码先提交到仓库中,如下命令所示:

    git add .

    git commit -m "first commit"

    git branch -M main

    git remote add origin https://github.com/xun082/mmm.git

    git push -u origin main

    这个时候我们的本地项目已经和远程 GayHub 仓库关联起来了,那么我们这个时候可以执行如下命令:

    pnpm run build

    首先执行该命令对我们的项目进行打包构建。打包完成之后会生成如下文件,请看下图:




    接下来我们可以使用 gh-pages 将项目发布到网上面了

    pnpm run deploy

    使用该命令进行打包并且部署到网上,这个过程可能需要一点时间,可以在工位上打开手机开把王者了。
    当在终端里出现 published 字段就说明我们部署成功了:




    这个时候,访问我们刚才在 package.json 文件中定义的 homepage 字段中的链接去访问就可以正常显示啦!




    总结


    通过该方法我们可以不用花钱,也能部署一个属于自己的网站,如果觉得不错那就赶紧用起来吧!


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

    实现一个简易的热🥵🥵更新

    简单模拟一个热更新 什么是热更新 热更新是指在应用程序运行时,对程序的部分内容进行更新,而无需重启整个应用程序。通常,热更新是在不停止整个应用程序的情况下,将新的代码、资源或配置应用于正在运行的应用程序,以实现功能修复、性能优化或新增功能等目的。 热更新的...
    继续阅读 »

    简单模拟一个热更新


    什么是热更新



    热更新是指在应用程序运行时,对程序的部分内容进行更新,而无需重启整个应用程序。通常,热更新是在不停止整个应用程序的情况下,将新的代码、资源或配置应用于正在运行的应用程序,以实现功能修复、性能优化或新增功能等目的。



    热更新的优点


    实时反馈,提高开发效率:热更新允许开发人员在进行代码更改后立即看到结果,无需重新编译和重启应用程序。这大大减少了开发、测试和调试代码所需的时间,提高了开发效率。


    保持应用程序状态:通过热更新,应用程序的状态可以在代码更改时得以保留。这意味着开发人员不需要在每次更改后重新导航到特定页面或重现特定的应用程序状态,从而节省了时间和精力。


    webpack 中的热更新



    在Webpack中,热更新(Hot Module Replacement,HMR)是一种通过在应用程序运行时替换代码的技术,而无需重新加载整个页面或应用程序。它提供了一种高效的开发方式,可以在开发过程中快速看到代码更改的效果,而不中断应用程序的状态。



    原理如下:


    客户端连接到开发服务器:当启动Webpack开发服务器时,它会在浏览器中创建一个WebSocket连接。


    打包和构建模块:在开发模式下,Webpack会监视源代码文件的变化,并在代码更改时重新打包和构建模块。这些模块构成了应用程序的各个部分。


    将更新的模块发送到浏览器:一旦Webpack重新构建了模块,它会将更新的模块代码通过WebSocket发送到浏览器端。


    浏览器接收并处理更新的模块:浏览器接收到来自Webpack的更新模块代码后,会根据模块的标识进行处理。它使用模块热替换运行时(Hot Module Replacement Runtime)来执行以下操作:(个人理解就像是 AJAX 局部刷新的过程,不对请指出)


    (1)找到被替换的模块并卸载它。


    (2)下载新的模块代码,并对其进行注入和执行。


    (3)重新渲染或更新应用程序的相关部分。


    保持应用程序状态:热更新通过在模块替换时保持应用程序状态来确保用户不会失去当前的状态。这意味着在代码更改后,开发人员可以继续与应用程序进行交互,而无需手动重新导航到特定页面或重现特定状态。


    代码模拟



    在同一个目录下创建 server.js 和 watcher.js



    server.js

    const http = require("http");
    const server = http.createServer((req, res) => {
    res.statusCode = 200;
    // 设置字符编码为 UTF-8,若有中文也不乱码
    res.setHeader("Content-Type", "text/plain; charset=utf-8");
    res.end("offer get!!!");
    });

    server.listen(7777, () => {
    console.log("服务已启动在 7777 端口");
    process.send("started");
    });

    // 监听来自 watcher.js 的消息
    process.on("message", (message) => {
    if (message === "refresh") {
    // 重新加载资源或执行其他刷新操作
    console.log("重新加载资源");
    }
    });


    (1)使用 http.createServer 方法创建一个 HTTP 服务器实例,该服务器将监听来自客户端的请求。


    (2)启动服务,当服务器成功启动并开始监听指定端口时,回调函数将被调用。


    (3)通过调用 process.send 方法,我们向父进程发送一条消息,消息内容为字符串 "started",表示服务器已经启动。


    (4)使用 process.on 方法监听来自父进程的消息。当收到名为 "refresh" 的消息时,回调函数将被触发。


    (5)在回调函数中,我们可以执行资源重新加载或其他刷新操作。一般是做页面的刷新操作。


    server.js 创建了一个简单的 HTTP 服务器。当有请求到达时,服务器会返回 "offer get!!!" 的文本响应。它还与父进程进行通信,当收到名为 "refresh" 的消息时,会执行资源重新加载操作。


    watcher.js

    const fs = require("fs");
    const { fork } = require("child_process");

    let childProcess = null;

    const watchFile = (filePath, callback) => {
    fs.watch(filePath, (event) => {
    if (event === "change") {
    console.log("文件已经被修改,重新加载");

    // 如果之前的子进程存在,终止该子进程
    childProcess && childProcess.kill();

    // 创建新的子进程
    childProcess = fork(filePath);
    childProcess.on("message", callback);
    }
    });
    };

    const startServer = (filePath) => {
    // 创建一个子进程,启动服务器
    childProcess = fork(filePath);
    childProcess.on("message", () => {
    console.log("服务已启动!");
    // 监听文件变化
    watchFile(filePath, () => {
    console.log("文件已被修改");
    });
    });
    };

    // 注意文件的相对位置
    startServer("./server.js");


    watcher.js 是一个用于监视文件变化并自动重新启动服务器的脚本。


    (1)首先,引入 fs 模块和 child_processfork 方法,fork 方法是 Node.js 中 child_process 模块提供的一个函数,用于创建一个新的子进程,通过调用 fork 方法,可以在主进程中创建一个全新的子进程,该子进程可以独立地执行某个脚本文件。


    (2)watchFile 函数用于监视指定文件的变化。当文件发生变化时,会触发回调函数。


    (3)startServer 函数负责启动服务器。它会创建一个子进程,并以指定的文件作为脚本运行。子进程将监听来自 server.js 的消息。


    (4)在 startServer 函数中,首先创建了一个子进程来启动服务器并发送一个消息表示服务器已启动。


    (5)然后,通过调用 watchFile 函数,开始监听指定的文件变化。当文件发生变化时,会触发回调函数。


    (6)回调函数中,会终止之前的子进程(如果存在),然后创建一个新的子进程,重新运行 server.js


    watcher.js 负责监视特定文件的变化,并在文件发生变化时自动重新启动服务器。它使用 child_process 模块创建子进程来运行 server.js,并在需要重新启动服务器时终止旧的子进程并创建新的子进程。这样就可以实现服务器的热更新,当代码发生变化时,无需手动重启服务器,watcher.js 将自动处理这个过程。


    效果图


    打开本地的 7777 端口,看到了 'offet get!!!' 的初始字样




    当修改了响应文字时,刷新页面,无需重启服务,也能看到更新的字样!


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

    28岁小公司程序员,无车无房不敢结婚,要不要转行?

    大家好,这里是程序员晚枫,又来分享程序员的职场故事了~ 今天分享的这位朋友叫小青,我认识他2年多了。以前从事的是土木行业,2年前找我咨询转行程序员的学习路线和职业规划后,通过自学加入了一家创业公司,成为了一名Java开发。 **最近他遇到了新的职业上的困惑,又...
    继续阅读 »

    大家好,这里是程序员晚枫,又来分享程序员的职场故事了~


    今天分享的这位朋友叫小青,我认识他2年多了。以前从事的是土木行业,2年前找我咨询转行程序员的学习路线和职业规划后,通过自学加入了一家创业公司,成为了一名Java开发。


    **最近他遇到了新的职业上的困惑,又找我聊了一下,我也没想到好的解决方法,**大家可以一起看一下~下面是沟通的核心内容。


    1、他的问题


    小青是中原省份省会城市的大专毕业,毕业季就去了帝都实习和工作。后来发现同学中有转行程序员的,薪资很诱惑,所以就找到我咨询如何学习和转行,现在一家帝都创业公司负责后端开发。工资1w出头。


    今年已经28岁了,有一个女朋友,最近女方家里催他结婚,他自己也有结婚的意愿。但是考虑到自己人在大城市,无车无房,创业公司的工作也不稳定,以后吃住花销,结婚后养孩子的花销,再看看自己1w多的工资,女朋友做财务,一个月到手不到1w。


    双方家里也都是普通家庭,给不了什么实质的资助,靠自己目前的收入根本不敢想象成家后压力有多大。


    所以目前非常迷茫, 不知道自己在28岁这个年龄应该怎么办,应不应该成家?应该怎样提高收入?


    虽然自己很喜欢程序员这份工作,但是感觉自己学历不好,天花板有限,程序员还能继续干下去吗?


    2、几个建议


    平时收到后台读者的技术问题或者转行的困惑,我都会尽力给一些详尽的回复。


    但是这次听到小青的问题,说实话,我也不知道该说什么。


    在28岁这种黄金年龄,想去大城市奋斗一番也是人之常情,但因为现实的生活压力,却不得不面临着选择离开大城市或者转行到自己不喜欢但是更务实的职业里去。


    如果想继续留在帝都,我能想到的有以下几个办法:



    • 首先,如果想继续从事程序员工作,努力提高收入。最快的办法就是跳槽,已经工作2年多了,背一背八股文,总结一下项目经验,应该是可以跳槽到一家更好的公司了。

    • 其次,探索另一个副业收入,例如自媒体。因为我自己就是通过在各个平台开通了自媒体账号:程序员晚枫,分享自己的程序员学习经验获得粉丝,进而得到自媒体收入的。小青也可以实事求是的分享一下自己大专毕业从建筑工作转行到程序员的经验,应该也能帮助到别人。

    • 最后,努力提高学历,想继续在程序员这行卷出高收入,趁着年轻,获得一个本科或者本科以上的学历还是很有帮助的。


    受限于自己的经验,我只能给出以上几个建议了。


    大家还有什么更有效的建议,欢迎在评论区交流~


    3、写在最后


    说句题外话,很多人都觉得程序员工资高想来转行,但其实程序员和其它行业一样,高收入的只是一小部分,而且那部分人既聪明又努力。


    最重要的是,高收入的那部分人里,大部分都不是转行的,而是在一个专业深耕了多年,最终获得了应有的报酬。


    无意冒犯,但听完小青的经历,我依然要给大专以下,想转行程序员拿高薪的朋友提个醒

    作者:程序员晚枫
    来源:juejin.cn/post/7209447968218841144
    :如果不是十分热爱,请务必三思~

    收起阅读 »

    JavaScript中return await究竟有无用武之地?

    web
    我先回答:有的,参考文章末尾。 有没有区别?  先上一个Demo,看看async函数中return时加和不加await有没有区别: function bar() { return Promise.resolve('this from bar().'); ...
    继续阅读 »

    我先回答:有的,参考文章末尾。



    有没有区别?


     先上一个Demo,看看async函数中return时加和不加await有没有区别:


    function bar() {
    return Promise.resolve('this from bar().');
    }

    async function foo1() {
    return await bar(); // CASE#1 with await
    }

    async function foo2() {
    return bar(); // CASE#2 without await
    }

    // main
    (() => {
    foo1().then((res) => {
    console.log('foo1:', res); // res is string: 'this from bar().'
    })
    foo2().then((res) => {
    console.log('foo2:', res); // res is string: 'this from bar().'
    })
    })();

     可能在一些社区或团队的编程规范中,有明确要求:不允许使用非必要的 return await。给出的原因是这样做对于foo函数而言,会增加等待bar函数返回的Promise出结果的时间(但其实它可以不用等,因为马上就要return了嘛,这个时间应留给foo函数的调用者去等)。


     如果你觉得上面的文字不大通顺,直接看代码,问:以上例子中,foo1()函数和foo2()函数的写法对程序的执行过程有何影响?


     先说结论:async 函数中 return await promise;return promise; 从宏观结果来看是一样的,但微观上有区别。


    有什么区别?


     基于上面的Demo改造一下,做个试验:


    const TAG = Symbol();
    const RESULT = Promise.resolve('return from bar().');
    RESULT[TAG] = 'TAG#RESULT';

    function bar() {
    return RESULT;
    }

    async function foo1() {
    return await bar();
    }

    async function foo2() {
    const foo2Ret = bar();
    console.log('foo2Ret(i):', foo2Ret[TAG], foo2Ret === RESULT); // 'TAG#RESULT', true (1)
    return foo2Ret; // without await
    }

    // main
    (() => {
    const foo1Ret = foo1();
    console.log('foo1Ret:', foo1Ret[TAG], foo1Ret === RESULT); // undefined, false (2)
    console.log('--------------------------------------------');
    const foo2Ret = foo2();
    console.log('foo2Ret(o):', foo2Ret[TAG], foo2Ret === RESULT); // undefined, false (3)
    })();

     从注释标注的执行结果可以看到:



    • (1)处没有疑问,foo2Ret 本来就是 RESULT

    • (2)处应该也没有疑问,foo1Ret 是基于 RESULT 这个Promise的结果重新包装的一个新的Promise(只是这个Promise的结果和Result是一致的);

    • (3)处应该和常识相悖,竟然和(2)不一样?是的,对于 async 函数不管return啥都会包成Promise,而且不是简单的通过 Pomise.resolve() 包装。


     那么结论就很清晰了,async 函数中 return await promise;return promise; 至少有两个区别:



    1. 对象上的区别:

      • return await promise; 会先把promise的结果解出来,再构造成新的Promise

      • return await promise; 直接在promise的基础上构造Promise,也就是套了两个Promise(两层Promise的状态和结果是一致的)



    2. 时间上的区别:假设 bar() 函数耗时 10s

      • foo1() 中的写法会导致这10s消耗在 foo1() 函数的执行上

      • foo2() 的写法则会让10s的消耗在 foo2() 函数的调用者侧,也就是注释为main的匿名立即函数




     从对象上的区别看,不论怎样async函数都会构造新的Promise对象,有无await都节约不了内存;从时间上来看,总体的等待时长理论上是一样的,怎么写对结果都没啥影响嘛。


     举个不大恰当的例子:你的上司交给你一个重要任务让你完成后发邮件给他,你分析了下后发现任务需要同事A做一部分,遂找他。同事A完成他的部分需要2天。这个时候你有两个做法选择:一、做完自己的部分后等着A出结果,有结果后再发邮件回复上司;二、将自己的部分完成后汇报给上司,并跟和上司说已经告知A:让A等完成他的部分后直接回邮件给上司。


     如果,我是说假如果哈,如果,这个重要任务本来要求必须在12h内完成,但实际耗时了两天严重超标......请问上述例子中哪种做法更容易获取N+1大礼包?


    到底怎么写?


     回到代码层,通过上述分析可以知道,一个主要是耗时归属问题,一个是async函数“总是”会返回的那个Promise对象不是由Promise.resolve()简单包装的(因为Promise.resolve(promise) === promise),可以得到两个编码指南:



    强调下,async函数不是通过Pomise.resolve()简单包装的,其实进一步思考下也不难理解,因为它要考虑执行有异常的场景,甚至还可能根据不同的Promise状态做一些其他的操作(比如日志输出、埋点统计?我瞎猜的)



    // 避免非必要的 return await 影响模块耗时统计的准确性
    async function foo() {
    return bar();
    }

    // 除非你要处理执行过程中的异常
    async function foo() {
    try {
    return await bar();
    } catch (_) {
    return null;
    }
    }
    // 或:
    async function foo() {
    return bar().catch(() => null);
    }

    // async 函数中避免对返回值再使用多余的 Pomise 包装
    async function bar() {
    return 'this is from bar().'; // YES
    }
    async function bar() {
    return Promise.resolve('this is from bar().'); // !!! NO !!!
    }

    回到标题:JavaScript中return await有无用武之地?


    答:有的,当需要消化掉依赖 Promise

    作者:Chavin
    来源:juejin.cn/post/7268593569781350455
    执行中的异常时。

    收起阅读 »

    吐槽大会,来瞧瞧资深老前端写的垃圾代码

    web
    阅读提示:网页版带有主题和代码高亮,阅读体验更佳 🍉 忍无可忍,不吐不快。 本期不写技术文章,单纯来吐槽下公司项目里的奇葩代码,还都是一些资深老前端写的,希望各位对号入座。 知道了什么是烂代码,才能写出好代码。 别说什么代码和人有一个能跑就行的话,玩笑归玩笑...
    继续阅读 »

    阅读提示:网页版带有主题和代码高亮,阅读体验更佳 🍉



    忍无可忍,不吐不快。


    本期不写技术文章,单纯来吐槽下公司项目里的奇葩代码,还都是一些资深老前端写的,希望各位对号入座。


    知道了什么是烂代码,才能写出好代码。


    别说什么代码和人有一个能跑就行的话,玩笑归玩笑。


    人都有菜的时候,写出垃圾代码无可厚非,但是工作几年了还是写垃圾代码,有点说不过去。


    我认为的垃圾代码,不是你写不出深度优先、广度优先的算法,也不是你写不出发布订阅、策略模式的 if else


    我认为垃圾代码都有一个共性:看了反胃。就是你看了天然的不舒服,看了就头疼的代码,比如排版极丑,没有空格、没有换行、没有缩进。


    优秀的代码就是简洁清晰,不能用策略模式优化 if else 没关系,多几行代码影响不大,你只要清晰,就是好代码。


    有了差代码、好代码的基本标准,那我们也废话不多说,来盘盘这些资深前端的代码,到底差在哪里。


    ---------------------------------------------更新------------------------------------------------


    集中回答一下评论区的问题:


    1、项目是原来某大厂的项目,是当时的外包团队写的,整体的代码是还行的。eslintcommitlint 之类的也是有的,只不过这些都是可以禁用的,作用全靠个人自觉。


    2、后来项目被我司收购,原来的人也转为我司员工,去支撑其他项目了。这个项目代码也就换成我们这批新来的维护了。很多代码是三几年前留下的确实没错,谁年轻的时候没写过烂代码,这是可以理解的。只不过同为三年前的代码,为啥有人写的简单清晰,拆分清楚,有的就这么脏乱差呢?


    3、文中提到的很多代码其实是最近一年写的,写这些功能的前端都是六七年经验的,当时因为项目团队刚组建,没有前端,抽调这些前端过来支援,写的东西真的非常非常不规范,这是让人难以理解的,他们在原来的项目组(核心产品,前端基建和规范都很厉害)也是这么写代码的吗?


    4、关于团队规范,现在的团队是刚成立没多久的,都是些年轻人,领导也不是专业前端,基本属于无前端 leader 的状态,我倒是很想推规范,但是我不在其位,不好去推行这些东西,提了建议,领导是希望你简单写写业务就行(说白了我不是 leader 说啥都没用)。而且我们这些年轻前端大家都默认打开 eslint,自觉性都很高,所以暂时都安于现状,代码 review 做过几次,都没太大的毛病,也就没继续做了。


    5、评论区还有人说想看看我写的代码,我前面文章有,我有几个开源项目,感兴趣可以自己去看。项目上的代码因为涉及公司机密,没法展示。


    6、本文就是篇吐槽,也不针对任何人,要是有人看了不舒服,没错,说的就是你。要是没事,大家看了乐呵乐呵就行。轻喷~


    文件命名千奇百怪


    同一个功能,都是他写的,三个文件夹三种奇葩命名法,最重要的是第三个,有人能知道它这个文件夹是什么功能吗?这完全不知道写的什么玩意儿,每次我来看代码都得重新理一遍逻辑。


    image.png


    组件职责不清


    还是刚才那个组件,这个 components 文件夹应该是放各个组件的,但是他又在底下放一个 RecordDialog.jsx 作为 components 的根组件,那 components 作为文件名有什么意义呢?


    image.png


    条件渲染逻辑置于底层


    这里其实他写了几个渲染函数,根据客户和需求的不同条件性地渲染不同内容,但是判断条件应该放在函数外,和函数调用放在一起,根据不同条件调用不同渲染函数,而不是函数内就写条件,这里就导致逻辑内置,过于分散,不利于维护。违反了我们常说的高内聚、低耦合的编程理念。


    image.png


    滥用、乱用 TS


    项目是三四年前的项目了,主要是类组件,资深前端们是属于内部借调来支援维护的,发现项目连个 TS 都没有,不像话,赶紧把 TS 配上。配上了,写了个 tsx 后缀,然后全是无类型或者 anyscript。甚至于完全忽视 TSESlint 的代码检查,代码里留下一堆红色报错。忍无可忍,我自己加了类型。


    image.png


    留下大量无用注释代码和报错代码


    感受下这个注释代码量,我每次做需求都删,但是几十个工程,真的删不过来。


    image.png


    image.png


    丑陋的、隐患的、无效的、混乱的 css


    丑陋的:没有空格,没有换行,没有缩进


    隐患的:大量覆盖组件库原有样式的 css 代码,不写类名实现 namespace


    无效的:大量注释的 css 代码,各种写了类名不写样式的垃圾类名


    混乱的:从全局、局部、其他组件各种引入 css 文件,导致样式代码过于耦合


    image.png


    一个文件 6 个槽点


    槽点1:代码的空行格式混乱,影响代码阅读


    槽点2:空函数,写了函数名不写函数体,但是还调用了!


    槽点3:函数参数过多不优化


    槽点4:链式调用过长,属性可能存在 undefined 或者 null 的情况,代码容易崩,导致线上白屏


    槽点5:双循环嵌套,影响性能,可以替换为 reduce 处理


    槽点6:参数、变量、函数的命名随意而不语义化,完全不知道有何作用,且不使用小驼峰命名


    image.png


    变态的链式取值和赋值


    都懒得说了,各位观众自己看吧。


    image.png


    代码拆分不合理或者不拆分导致代码行数超标


    能写出这么多行数的代码的绝对是人才。


    尽管说后来维护的人加了一些代码,但是其初始代码最起码也有近 2000 行。


    image.png


    这是某个功能的数据流文件,使用 Dva 维护本身代码量就比 Redux 少很多了,还是能一个文件写出这么多。导致到现在根本不敢拆出去,没人敢动。


    image.png


    杂七杂八的无用 js、md、txt 文件


    在我治理之前,充斥着非常多的无用的、散乱的 md 文档,只有一个函数却没有调用的 js 文件,还有一堆测试用的 html 文件。


    实在受不了干脆建个文件夹放一块,看起来也要舒服多了。


    image.png


    less、scss 混用


    这是最奇葩的。


    image.png


    特殊变量重命名


    这是真大佬,整个项目基建都是他搭的。写的东西也非常牛,bug 很少。但是大佬有一点个人癖好,例如喜欢给 window 重命名为 G。这虽然算不上大缺点,但真心不建议大家这么做,window 是特殊意义变量,还请别重命名。


    const G = window;
    const doc = G.document;

    混乱的 import


    规范的 import 顺序,应该是框架、组件等第三方库优先,其次是内部的组件库、包等,然后是一些工具函数,辅助函数等文件,其次再是样式文件。乱七八糟的引入顺序看着都烦,还有这个奇葩的引入方式,直接去 lib 文件夹下引入组件,也是够奇葩了。


    总而言之,css 文件一般是最后引入,不能阻塞 js 的优先引入。


    image.png


    写在最后


    就先吐槽这么多吧,这些都是平时开发过程中容易犯的错误。希望大家引以为戒,不然小心被刀。


    要想保持一个好的编码习惯,写代码的时候就得时刻告诉自己,你的代码后面是会有人来看的,不想被骂就写干净点。


    我觉得什么算法、设计模式都是次要的,代码清晰,数据流向清晰,变量名起好,基本 80% 不会太差。


    不过说这么多,成事在人。


    不过我写了一篇讲解如何写出简洁清晰代码的文章,我看不仅是一年前端需要学习,六七年的老前端也需要看看。

    作者:北岛贰
    来源:juejin.cn/post/7265505732158472249

    收起阅读 »

    实现一个简易的热🥵🥵更新

    web
    简单模拟一个热更新 什么是热更新 热更新是指在应用程序运行时,对程序的部分内容进行更新,而无需重启整个应用程序。通常,热更新是在不停止整个应用程序的情况下,将新的代码、资源或配置应用于正在运行的应用程序,以实现功能修复、性能优化或新增功能等目的。 热更新的...
    继续阅读 »

    简单模拟一个热更新


    什么是热更新



    热更新是指在应用程序运行时,对程序的部分内容进行更新,而无需重启整个应用程序。通常,热更新是在不停止整个应用程序的情况下,将新的代码、资源或配置应用于正在运行的应用程序,以实现功能修复、性能优化或新增功能等目的。



    热更新的优点


    实时反馈,提高开发效率:热更新允许开发人员在进行代码更改后立即看到结果,无需重新编译和重启应用程序。这大大减少了开发、测试和调试代码所需的时间,提高了开发效率。


    保持应用程序状态:通过热更新,应用程序的状态可以在代码更改时得以保留。这意味着开发人员不需要在每次更改后重新导航到特定页面或重现特定的应用程序状态,从而节省了时间和精力。


    webpack 中的热更新



    在Webpack中,热更新(Hot Module Replacement,HMR)是一种通过在应用程序运行时替换代码的技术,而无需重新加载整个页面或应用程序。它提供了一种高效的开发方式,可以在开发过程中快速看到代码更改的效果,而不中断应用程序的状态。



    原理如下:


    客户端连接到开发服务器:当启动Webpack开发服务器时,它会在浏览器中创建一个WebSocket连接。


    打包和构建模块:在开发模式下,Webpack会监视源代码文件的变化,并在代码更改时重新打包和构建模块。这些模块构成了应用程序的各个部分。


    将更新的模块发送到浏览器:一旦Webpack重新构建了模块,它会将更新的模块代码通过WebSocket发送到浏览器端。


    浏览器接收并处理更新的模块:浏览器接收到来自Webpack的更新模块代码后,会根据模块的标识进行处理。它使用模块热替换运行时(Hot Module Replacement Runtime)来执行以下操作:(个人理解就像是 AJAX 局部刷新的过程,不对请指出)


    (1)找到被替换的模块并卸载它。


    (2)下载新的模块代码,并对其进行注入和执行。


    (3)重新渲染或更新应用程序的相关部分。


    保持应用程序状态:热更新通过在模块替换时保持应用程序状态来确保用户不会失去当前的状态。这意味着在代码更改后,开发人员可以继续与应用程序进行交互,而无需手动重新导航到特定页面或重现特定状态。


    代码模拟



    在同一个目录下创建 server.js 和 watcher.js



    server.js


    const http = require("http");
    const server = http.createServer((req, res) => {
    res.statusCode = 200;
    // 设置字符编码为 UTF-8,若有中文也不乱码
    res.setHeader("Content-Type", "text/plain; charset=utf-8");
    res.end("offer get!!!");
    });

    server.listen(7777, () => {
    console.log("服务已启动在 7777 端口");
    process.send("started");
    });

    // 监听来自 watcher.js 的消息
    process.on("message", (message) => {
    if (message === "refresh") {
    // 重新加载资源或执行其他刷新操作
    console.log("重新加载资源");
    }
    });


    (1)使用 http.createServer 方法创建一个 HTTP 服务器实例,该服务器将监听来自客户端的请求。


    (2)启动服务,当服务器成功启动并开始监听指定端口时,回调函数将被调用。


    (3)通过调用 process.send 方法,我们向父进程发送一条消息,消息内容为字符串 "started",表示服务器已经启动。


    (4)使用 process.on 方法监听来自父进程的消息。当收到名为 "refresh" 的消息时,回调函数将被触发。


    (5)在回调函数中,我们可以执行资源重新加载或其他刷新操作。一般是做页面的刷新操作。


    server.js 创建了一个简单的 HTTP 服务器。当有请求到达时,服务器会返回 "offer get!!!" 的文本响应。它还与父进程进行通信,当收到名为 "refresh" 的消息时,会执行资源重新加载操作。


    watcher.js


    const fs = require("fs");
    const { fork } = require("child_process");

    let childProcess = null;

    const watchFile = (filePath, callback) => {
    fs.watch(filePath, (event) => {
    if (event === "change") {
    console.log("文件已经被修改,重新加载");

    // 如果之前的子进程存在,终止该子进程
    childProcess && childProcess.kill();

    // 创建新的子进程
    childProcess = fork(filePath);
    childProcess.on("message", callback);
    }
    });
    };

    const startServer = (filePath) => {
    // 创建一个子进程,启动服务器
    childProcess = fork(filePath);
    childProcess.on("message", () => {
    console.log("服务已启动!");
    // 监听文件变化
    watchFile(filePath, () => {
    console.log("文件已被修改");
    });
    });
    };

    // 注意文件的相对位置
    startServer("./server.js");


    watcher.js 是一个用于监视文件变化并自动重新启动服务器的脚本。


    (1)首先,引入 fs 模块和 child_processfork 方法,fork 方法是 Node.js 中 child_process 模块提供的一个函数,用于创建一个新的子进程,通过调用 fork 方法,可以在主进程中创建一个全新的子进程,该子进程可以独立地执行某个脚本文件。


    (2)watchFile 函数用于监视指定文件的变化。当文件发生变化时,会触发回调函数。


    (3)startServer 函数负责启动服务器。它会创建一个子进程,并以指定的文件作为脚本运行。子进程将监听来自 server.js 的消息。


    (4)在 startServer 函数中,首先创建了一个子进程来启动服务器并发送一个消息表示服务器已启动。


    (5)然后,通过调用 watchFile 函数,开始监听指定的文件变化。当文件发生变化时,会触发回调函数。


    (6)回调函数中,会终止之前的子进程(如果存在),然后创建一个新的子进程,重新运行 server.js


    watcher.js 负责监视特定文件的变化,并在文件发生变化时自动重新启动服务器。它使用 child_process 模块创建子进程来运行 server.js,并在需要重新启动服务器时终止旧的子进程并创建新的子进程。这样就可以实现服务器的热更新,当代码发生变化时,无需手动重启服务器,watcher.js 将自动处理这个过程。


    效果图


    打开本地的 7777 端口,看到了 'offet get!!!' 的初始字样


    image.png


    当修改了响应文字时,刷新页面,无需重启服务,也能看到更新的字样!


    image.png

    收起阅读 »

    工作6年了日期时间格式化还在写YYYY疯狂给队友埋雷

    前言 哈喽小伙伴们好久不见,今天来个有意思的雷,看你有没有埋过。 正文 不多说废话,公司最近来了个外地回来的小伙伴,在广州工作过6年,也是一名挺有经验的开发。 他提交的代码被小组长发现有问题,给打回了,原因是里面日期格式化的用法有问题,用的Simpl...
    继续阅读 »

    前言



    哈喽小伙伴们好久不见,今天来个有意思的雷,看你有没有埋过。



    正文



    不多说废话,公司最近来了个外地回来的小伙伴,在广州工作过6年,也是一名挺有经验的开发。




    他提交的代码被小组长发现有问题,给打回了,原因是里面日期格式化的用法有问题,用的SimpleDateFormat,但不知道是手误还是什么原因,格式用了YYYY-MM-dd。




    这种写法埋了一个不大不小的雷。




    用一段测试代码就可以展示出来问题



    1.jpg



    打印结果如下:



    2.jpg



    很明显,使用YYYY时,2023年变成了2024年,在正常情况下可能没问题,但是在跨年的时候大概率就会有问题了。




    原因比较简单,与小写的yyyy不同,大写的YYYY表示一个基于周的年份。它是根据周计算的年份,而不是基于日历的年份。通常情况下,两者的结果是相同的,但在跨年的第一周或最后一周可能会有差异。




    比如我如果换成2023-12-30又不会有问题了



    3.jpg



    另外,Hutool工具类本身是对Java一些工具的封装,DateUtil里面也有用到SimpleDateFormat,因此也会存在类似的问题。



    4.jpg



    避免这个问题的方法也十分简单,要有公用的格式类,所有使用日期格式的地方都引用这个类,这个类中就定义好yyyy-MM-dd想给的格式即可,这样就不会出现有人手误给大家埋雷了。



    总结




    1. 日期时间格式统一使用yyyy小写;

    2. 日期格式要规定大家都引用定义好的工具类,避免有人手误打错。




    最后再回头想一想,这种小问题并不会马上暴露出来,倘若没有被发现,到了明年元旦,刚好跨年的时候,是不是就要坑死一堆人

    作者:程序员济癫
    来源:juejin.cn/post/7269013062677823528
    了。


    收起阅读 »

    简历中不写年龄、毕业院校、预期薪资会怎样?

    无意中看到一条视频,点赞、转发量都非常高,标题是“不管你有多自信,简历中的个人信息都不要这样写”。看完之后简直有些无语,不仅哗众取宠,甚至会误导很多人。 之所以想写这篇文章,主要是分享给大家一种思维方式:如果别人说的事实或观点,只有情绪、结论,没有事实依据和推...
    继续阅读 »

    无意中看到一条视频,点赞、转发量都非常高,标题是“不管你有多自信,简历中的个人信息都不要这样写”。看完之后简直有些无语,不仅哗众取宠,甚至会误导很多人。


    之所以想写这篇文章,主要是分享给大家一种思维方式:如果别人说的事实或观点,只有情绪、结论,没有事实依据和推导,那么这些事实和观点是不足信的,需要慎重对待。


    视频的内容是这样的:“不管你有多自信,简历中的个人信息都不要这样写。1、写了期望薪资,错!2、写了户籍地址,错!3、写了学历文凭,错!4、写了离职原因,错!5、写了生日年龄,错!6、写了自我评价,错!


    正确写法,只需要写姓名和手机号、邮箱及求职意向即可,简历个人信息模块的作用是让HR顺利联系到你,所有任何其他内容都不要写在这里……”


    针对这条视频的内容,有两个不同的表现:第一就是分享和点赞数量还可以,都破千了;第二就是评论区很多HR和求职着提出了反对意见。


    第一类反对意见是:无论求职者或HR都认为这样的简历是不合格的,如果不提供这些信息,根本没有预约面试的机会,甚至国内的招聘平台的简历模板都无法通过。第二类,反对者认为,如果不写这些信息,特别是预期薪资,会导致浪费双方的时间。


    针对上述质疑,作者的回复是:”看了大家的评论,我真的震惊,大家对简历的误解是如此至深……“


    仔细看完视频和评论,在视频的博主和评论者之间产生了一个信息差。博主说的”个人信息“不要写,给人了极大的误导。是个人信息栏不要写,还是完全不写呢?看评论,大多数人都理解成了完全不写。博主没有说清楚是不写,还是写在别处,这肯定是作者的锅。


    本人也筛选过近千份简历,下面分享一下对这则视频中提到的内容的看法:


    第一,户籍、离职原因可以不写


    视频中提到的第2项和第4项的确可以不写。


    户籍这一项,大多数情况下是可以不写的,只用写求职城市即可,方便筛选和推送。比如,你想求职北京或上海的工作,这个是必须有的,而你的户籍一般工作没有强制要求。但也有例外,比如财务、出纳或其他特殊岗位,出于某些原因,某些公司会要求是本地的。写不写影响没那么大。


    离职原因的确如他所说的,不建议写,是整个简历中都不建议写。这个问到了再说,或者填写登记表时都会提到,很重要,要心中有准备,但没必要提前体现。


    第二,期望薪资最好写上


    关于期望薪资这个有两种观点,有的说可以不写,有的说最好写上。其实都有道理,但就像评论中所说:如果不写,可能面试之后,薪资相差太多,导致浪费了双方的时间。


    其实,如果可以,尽量将期望薪资写上,不仅节省时间,这里还稍微有一个心理锚定效应,可以把薪资写成范围,而范围的下限是你预期的理想工资。就像讨价还价时先要一个高价,在简历中进行这么一个薪资的锚定,有助于提高最终的薪资水平。


    第三,学历文凭一定要写


    简历中一定要写学历文凭,如果没有,基本上是会默认为没有学历文凭的,是不会拿到面试邀约的。仔细想了一下,那则视频的像传达的意思可能是不要将学历文凭写作个人信息栏,而是单独写在教育经历栏中。但视频中没有明说,会产生极大的误导。


    即便是个人信息栏,如果你的学历非常漂亮,也一定要写到个人信息栏里面,最有价值,最吸引眼球的信息,一定要提前展现。而不是放在简历的最后。


    第四,年龄要写


    视频中提到了年龄,这个是招聘衡量面试的重要指标,能写尽量写上。筛选简历中有一项非常重要,就是年龄、工作经历和职位是否匹配。在供大于求的市场中,如果不写年龄,为了规避风险,用人方会直接放弃掉。


    前两个月在面试中,也有遇到因为年龄在30+,而在简历中不写年龄的。作为面试官,感觉是非常不好的,即便不写,在面试中也需要问,最终也需要衡量年龄与能力是否匹配的问题。


    很多情况下,不写年龄,要么认为简历是不合格的,拿不到面试机会,要么拿到了面试机会,但最终只是浪费了双方的时间。


    第五,自我评价


    这一项与文凭一样,作者可能传达的意思是不要写在个人信息栏中,但很容易让人误解为不要写。


    这块真的需要看情况,如果你的自我评价非常好,那一定要提前曝光,展现。


    比如我的自我评价中会写到”全网博客访问量过千万,CSDN排名前100,出版过《xxx》《xxx》书籍……“。而这些信息一定要提前让筛选简历的人感知到,而不是写在简历的最后。


    当然,如果没有特别的自我评价,只是吃苦耐劳、抗压、积极自主学习等也有一定的积极作用,此时可以考虑放在简历的后面板块中,而不是放在个人信息板块中。这些主观的信息,更多是一个自我声明和积极心态的表现。


    最后的小结


    经过上面的分析,你会看到,并不是所有的结论都有统一的标准的。甚至这篇文章的建议也只是一种经验的总结,一个看问题的视角而已,并不能涵盖和适用所有的场景。而像原始视频中那样,没有分析,没有推导,没有数据支撑,没有对照,只有干巴巴的结论,外加的煽动情绪的配音,就更需要慎重对待了。


    在写这篇文章的过程中,自己也在想一件事:任何一个结论,都需要在特定场景下才能生效,即便是牛顿的力学定律也是如此,这才是科学和理性的思维方式。如果没有特定场景,很多结

    作者:程序新视界
    来源:juejin.cn/post/7268593569782054967
    论往往是不成立的,甚至是有害的。

    收起阅读 »

    良言难劝该死鬼,居然有人觉得独立开发做三件套是件好事

    没想到某个大V居然写了一篇公众号文章回应独立开发者三件套。认为无脑做大路货也好! 我其实挺烦这种清新圣母派,就是无论你做什么,反正我都不拦着,我就鼓励你 follow your heart。人生没有白走的路。口头上一百个支持,谁都不得罪,等你真吃亏了这些人就不...
    继续阅读 »

    Pasted Graphic.png


    没想到某个大V居然写了一篇公众号文章回应独立开发者三件套。认为无脑做大路货也好!


    我其实挺烦这种清新圣母派,就是无论你做什么,反正我都不拦着,我就鼓励你 follow your heart。人生没有白走的路。口头上一百个支持,谁都不得罪,等你真吃亏了这些人就不知道哪去了。


    我提两个点:首先这个作者就不是干独立开发的,你就听不会游泳的人跟你讲如何游泳这靠谱吗,IT王语嫣是吧。要是你真独立开发有成绩,你这么说我也觉得多少有点信用背书。现在说这话立场就跟某些博主推荐东西一样,你买我推荐,我买我不买。哪天他要是自己下场做笔记我也服。


    其次他的观点就是反正做出来就是成功。你还好意思问别人怎么定义成功,你这成功定义的,既然我做什么都是成功,我为什么要做大路货,我做其他也算是成功。既然第一款做笔记失败概率大,不还是要面临到底我要做什么问题吗。既然都是做,为什么不做点有趣的东西



    但是我要补充说明一个观点,独立开发起步雷区里是没有番茄钟的。番茄钟还是挺新手友好的。



    这就是我讽刺做这些是独立开发死亡加速三件套的原因,因为独立开发的真正内核是独创性。独创性可以是不赚钱的,可以是小众的,可以是无用但有趣的,但是不应该是我脑子一热没想法我跟风做一个。既然要做独立开发,就不能用战术上的勤奋掩盖战略上的懒惰。最关键的产品定位你跳过不考虑就是捡芝麻丢西瓜。问题的核心不是做不做三件套,是你有没有想好产品的差异化是什么,能不能通过差异化获得产品的定价权,而不是做另外一个xx。


    良言难劝该死鬼,以上的言论是我糊涂了。我觉得做笔记、做记账光明是前途的,做出来就很厉害了。世界上虽然有很多记账了,但是说不定就缺你这个记账。做你想做的,just do it!


    作者:独立开花卓富贵
    来源:juejin.cn/post/7268896098827403301

    收起阅读 »

    一个上午,我明白了,为什么总说人挣不到认知以外的钱

    你好,我是刘卡卡,94年的大厂奶爸程序员,探索副业中 01 接下来,以我昨天上午的一段经历,分析下为什么我挣不到认知以外的钱 在上班的路上,我在微信的订阅号推荐里面看到了这张图,当时我的想法是:这东西阅读量好高噢,不过养老金和目前的我没什么关系。于是,我就划过...
    继续阅读 »

    你好,我是刘卡卡,94年的大厂奶爸程序员,探索副业中


    01


    接下来,以我昨天上午的一段经历,分析下为什么我挣不到认知以外的钱


    在上班的路上,我在微信的订阅号推荐里面看到了这张图,当时我的想法是:这东西阅读量好高噢,不过养老金和目前的我没什么关系。于是,我就划过去了。




    (读者可以先停5s 思考下,假设是你看到这张图,会有什么想法)



    02


    当我坐上工位后,我看到我参加的社群里也有有发了上图,并附上了一段文字:


    “养老金类型的公众号容易出爆文。


    小白玩转职场这个号,篇篇10w+,而且这并不是一个做了很久的老号,而是今年5月才注册不久的号。 之前这个号刚做的时候是往职场方向发展,所以取名叫小白玩转职场,但是发了一阵后数据不是很好于是就换风格做了养老金的内容。


    换到养老金赛道后就几乎篇篇10w+。 这些内容一般从官方网站找就好,选一些内容再加上自己想法稍微改下,或者直接通过Chatgpt辅助,写好标题就行”。


    同时,文字下面有社群圈友留下评论说:“这是个好方向啊,虽然公众号文章已经同质化很严重了,但可以往视频号、带货等方向发展”。



    读者可以先停5s 思考下,假设是你看到这段文字,会有什么想法。如果你不知道公众号赚钱的模式,那你大概率看不出这段话中的赚钱信息的



    我想了想,对噢,确实可以挣到钱,于是将这则信息发到了程序员副业交流的微信群里。



    然后,就有群友在交流:“他这是转载还是什么,不可能自己天天写吧”,“这种怎么冷启动呢,不会全靠搜索吧,“做他这种类型的公众号挺多吧,怎么做到每篇10w的”



    有没有发现,这3个问题都是关注的怎么做的问题?怎么写的,怎么启动的,怎么每篇10w。


    这就是我们大部分人的认知习惯,看到一个信息或别人赚钱的点子后,我们大部分人习惯去思考别人是如何做到的,是不是真的,自己可不可以做到。


    可一般来说,我们当下的认知是有限的,大概率是想不出完整的答案的的,想不出来以后,然后就会觉得这个事情有点假,很难或者不适合。从而就错过这条信息了。



    我当时觉得就觉得可能大部分群友会错过这则信息了,于是,在群里发了下面这段话


    “分享一个点子后


    首先去看下点子背后的商业变现机会,如带货/流量主/涨粉/等一系列


    而后,才去考虑执行的问题,执行的话


    1、首先肯定要对公众号流量主的项目流程进行熟悉


    2、对标模仿,可以去把这个公众号的内容全看一看,看看别人为什么起号


    3、做出差异化内容。”


    发完后还有点小激动(嗯,我又秀了波自己的认知)。可到中午饭点,我发现我还是的认知太低了。


    03


    在中午吃饭时,我看到亦仁(生财有术的老大)发了这段内容



    我被这段话震撼到了,我发现我现在的思考习惯,还是只停留在最表面的看山是山的地步。


    我仅仅看到了这张图流量大,没有去思考它为什么这么大流量?



    因为他通过精心制作的文章,为老年用户介绍了养老金的方方面面,所以,才会有流量


    简单说,因为他满足了用户的需求



    同样,我也没有思考还有没有其他产品可以满足用户的这个需求,我仅仅是停留在了视频号和公众号这两个产品砂锅。



    只要满足用户需求,就不只有一个产品形态,对于养老金这个信息,我们可以做直播,做课程,做工具,做咨询,做1对1私聊的。这么看,就能有无数的可能



    同时,我想到了要做差异化,但没有想到要通过关键字挖掘,去挖掘长尾词。



    而亦仁,则直接就挖掘了百万关键字,并无偿分享了。



    这才知道,什么叫做看山不是山了。


    之前知道了要从“需求 流量 营销 变现”的角度去看待信息,也知道“需求为王”的概念。


    可我看到这则信息时,还是没有考虑到需求这一层,更没有形成完整的闭环思路。


    因此,以后要在看到这些信息时,去刻意练习“需求 流量 营销 变现”这个武器库,去关注他用什么产品,解决了用户什么需求,从哪里获取到的流量的,怎么做营销的,怎么做变现的。


    04


    于是,我就把这些思考过程也同样分享到了群里。


    接着,下午我就看到有群友在自己的公众号发了篇和养老金相关的文章,虽然文章看上去很粗糙,但至少是起步了。


    同时,我也建了个项目交流群,方便感兴趣的小伙伴交流进步(一群人走的更远)


    不过我觉得在起步之前,也至少得花一两天时间去调研下,去评估这个需求有哪些地方自己可以切入进去,值不值得切入,能切入的话,怎么切入。


    对了,可能你会关心我写了这么多,自己有没有做养老金相关的?


    我暂时还没有,因为我目前关心的领域还在出海工具和个人IP上。

    作者:刘卡卡
    来源:juejin.cn/post/7268590610189418533

    收起阅读 »

    228欢乐马事件,希望大家都能平安健

    iOS
    我这个人体质很奇怪,总能遇到一些奇怪的事。比如六年前半夜打车回家,差点被出租车司机拉到深山老林。比如两年前去福州出差,差点永远回不了家。比如十点从实验室下班,被人半路拦住。比如这次,被人冒充 (在我心里这事和前几件同样恶劣) 不过幸好每次都是化险为夷...
    继续阅读 »

    我这个人体质很奇怪,总能遇到一些奇怪的事。

    • 比如六年前半夜打车回家,差点被出租车司机拉到深山老林。
    • 比如两年前去福州出差,差点永远回不了家。
    • 比如十点从实验室下班,被人半路拦住。
    • 比如这次,被人冒充 (在我心里这事和前几件同样恶劣)

    不过幸好每次都是化险为夷,所以我顺顺利利活到现在。




    事情起因是这样的:


    去年朋友B突然告诉我:发现了你的闲鱼账号。


    :我没有闲鱼啊?


    他给我截图,那个人卖掘金的周边,名字叫LolitaAnn


    因为我遇到太多离谱的事,再加上看的一堆被冒充的新闻,所以我第一反应是:这人也在冒充我


    当时朋友F 说我太敏感了,他觉得只是巧合。


    但我觉得不是巧合,因为LolitaAnn是我自己造的词。




    把我的沸点搬到小红书


    又是某一天, 朋友H告诉我:你的小红书上热门了。


    :?我没有小红书啊?


    然后他们给我看,有个人的小红书完全照搬我的沸点。


    为此我又下载小红书和他对线。起初正常交涉,但是让他删掉,他直接不回复我了,最后还是投诉他,被小红书官方删掉的。




    现在想了想,ip一样,极有可能是一个人干的。




    闲鱼再次被挖出来


    今年,有人在掘金群里说我卖周边。


    我跑到那个群解释,说我被人冒充了。


    群友很热心,都去举报那个人的昵称。昵称被举报下去了。


    但是几天之后:




    看到有人提醒我,它名字又改回来了。


    当时以为是热心群友,后来知道就是它冒充我,现在回想起来一阵恶寒。


    把名字改回去之后还在群里跟炫耀一样,心里想着:我改回来了,你们不知道是我吧。




    冒充我的人被揪出来了


    2.28的时候, 朋友C突然给我发了一段聊天记录。


    是它在群里说 它的咸鱼什么掘金周边都有。结果打开一看,闲鱼是我的名字和头像。




    事情到今天终于知道是谁冒充我了


    虽然Smile只是它的微信小号之一,都没实名认证。但是我还是知道了一些强哥的信息。


    发现是谁冒充我,我第一反应必然是喷一顿。


    刚在群里被我骂完,它脑子也不太好使,马上跑去改了自己掘金和闲鱼的名字。这不就是自爆了? 证明咸鱼和掘金都是他的号。


    奔跑姐妹兄弟(原名一只小小黄鸭) ←点击链接即可鞭尸。






    牵扯出一堆小号


    本来我以为事情已经结束了,我就去群里吐槽他。结果一堆认识他的掘友们给我说它还有别的掘金号。因为它和不同掘友用不同掘金号认识的。所以掘友们给我提供了一堆。我就挨个搜。


    直到我看到了这两条:




    因为我搜欢乐马出来上万个同名账号, 再加上他说自己有脚本,当时我以为都是他的小号。


    后来掘友们提醒我,欢乐马是微信默认生成的,所以有一堆欢乐马,不一定是他的小号。


    但是我确信他有很多小号,比如:




    比如咸鱼卖了六七个掘金鼠标垫,卖了掘金的国行switch……

    • 你们有没有想过为什么fixbug不许助攻了

    • 你们有没有想过为什么矿石贬值了,兑换商店越来越贵了?

    • 你们有没有想过为什么掘金活动必得奖励越来越少了?


    有这种操作在,普通用户根本没办法玩吧。


    所以最后,我就把这个事交给官方了。




    处理结果


    所幸官方很给力,都处理了,感谢各位官方大大。



    本次事件,共涉及主要近期活跃UserID 4个,相关小号570个。 





    我再叨叨几句

    • 卖周边可以, 你别冒充别人卖吧,又不是见不得光,做你自己不丢人。

    • 开小号可以,你别一开开一堆,逼得普通玩家拿不到福利吧。

    • 先成人后成才,不指望能为国家做多大贡献,起码别做蛀虫吧。

    • 又不是没生活,专注点自己的东西,别老偷别人沸点。

    • 我以后改名了嗷。叫Ann的八百万个,别碰瓷你爹了。


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

    孤独的游戏少年

    本人是95前端菜鸟一枚,目前在广州打工混口饭吃。刚好换了工作,感觉生活节奏变得慢了下来,打了这么多年工总觉得想纪录些什么,怕以后自己老了忘记自己还有这么一些风流往事。书接上回。 楔子 又是一个闲暇的周末,正读一年级的我坐在床上,把象棋的旗子全部倒了出来,根据...
    继续阅读 »

    本人是95前端菜鸟一枚,目前在广州打工混口饭吃。刚好换了工作,感觉生活节奏变得慢了下来,打了这么多年工总觉得想纪录些什么,怕以后自己老了忘记自己还有这么一些风流往事。书接上回。



    楔子


    又是一个闲暇的周末,正读一年级的我坐在床上,把象棋的旗子全部倒了出来,根据颜色分为红色和绿色阵营,旗子翻盖为建筑,正面为单位,被子和枕头作为地图障碍,双手不断的移动着双方阵营的象棋,脑海中演练着星级争霸的游戏画面,将两枚不同阵营的旗子进行碰撞后,通过我对两枚旗子主观的判断,一方阵营的旗子直接被销毁,进入回收,一方的单位对建筑物全部破坏取得游戏胜利。因为我的父亲酷爱玩这款游戏,年少的我也被其玩法、画面所深深吸引,不过最主要的还是父亲获胜或者玩累后,能幸运的奖励我玩上几把。星际争霸成了我第一个启蒙游戏,那时候怎么样都获胜不了,直到发现了show me the money。因为Blizzard这个英文单词一直在游戏的启动界面一闪一闪,那时候还以为这款游戏的名字叫Blizzard,最后才发现,其实Blizzard是魔鬼的意思。


    纸笔乐趣


    小学一二年级的时候家里管得严,电视也不给我看,电脑直接上锁,作为家里的独生子女,没有同龄人的陪伴,闲暇时间要不就看《格林童话》、《安徒生童话》、《伊索寓言》,要不就打开这副象棋在那里自娱自乐。



    起源



    在某一天的音乐课上,老师喉咙不舒服,在教室播放猫和老鼠给我们观看,正当我看的津津有味,前面的同学小张突然转过头来,问我“要不要跟我玩个游戏”。小孩子一般都比较贪玩,我直接拒绝了他,“我想看猫和老鼠”。小张满脸失落转了回去,因为我是个特别怕伤害别人情绪的人,所以不忍心又用铅笔的橡皮端戳了戳他,问他“什么游戏,好玩不”。他顿时也来了精神,滔滔不绝的介绍起了这款游戏。


    “游戏的规则非常的简单~~在一张纸上中间画一条分割线,双方有各自的基地,每人基地都有5点血量,双方通过猜拳,获胜的一方可以在自己阵营画一个火柴人,火柴人可以攻击对面的火柴人和基地,基地被破坏则胜利。管你听没听懂,试一把就完了!”



    我呆呆的看着他,“这么无聊,赢了又怎样?”。他沉默了一会,好像我说的的确有点道理,突然想到了一个好办法,“谁获得胜利扇对方一巴掌”。我顿时来了兴趣。随着游戏逐渐深入,我们的猜拳速度和行动力也越来越快,最后发现扇巴掌还是太狠,改为扇对方的手背,这节课以双方手背通红结束了。


    游戏改良


    这个《火柴人对战》小游戏在班里火了一会儿,但很快就又不火了,我玩着玩着也发现没啥意思,总是觉得缺少点什么,但毕竟我也只是个没有吃APTX4869的小学生,想不出什么好点子。时间一晃,受到九年义务教育的政策,我也成功成为了一名初中生。在一节音乐课上,老师想让我们放松放松,给我们班看猫和老鼠,隔壁同桌小王撕了一张笔记本的纸,问我有没有玩过《火柴人对战》游戏,只能说,熟悉的配方,熟悉的味道。


    当天晚上回到家,闲来无事,我在想这个《火柴人游戏》是不是可以更有优化,这种形式的游戏是不是可以让玩家更有乐趣。有同学可能会问,你那个年代没东西玩的吗?既然你诚心诚意发问,那我就大发慈悲的告诉你。玩的东西的确很多,但是能光明正大摆在课桌上玩的基本没有,一般有一个比较新鲜的好玩的东西,都会有一群人围了过来,这时候老师会默默站在窗户旁的阴暗角落,见一个收一个。


    坐在家里的椅子上,我整理了一下思绪,突然产生了灵感,将《魔兽争霸》《游戏王》这两个游戏产生化学反应,游戏拥有着资源,单位,建筑,单位还有攻击力,生命值,效果,攻击次数。每个玩家每回合通过摇骰子的方式获得随机能源点,能源能够解锁建筑,建筑关联着高级建筑和单位,通过单位进行攻击,直至对方玩家生命值为0,那么如何在白纸上面显示呢?我想到比较好的解决方案,单位的画像虽然名字叫骷髅,但是在纸上面用代号A表示,建筑骷髅之地用代号1表示。我花了几天时间,弄了两个阵营,不死族和冰结界。立刻就拿去跟同桌试玩了一下,虽然游戏很丰富,但是有一个严重的弊端就是玩起来还挺耗费时间的,而且要人工计算单位之间的扣血量,玩家的剩余生命,在纸片上去完成这些操作,拿个橡皮擦来擦去,突然觉得有点蠢,有点尴尬,突然明白,一张白纸的承受能力是有限的。之后,我再也没有把游戏拿出来玩过,但我没有将他遗忘,而是深深埋藏在我的心里。


    筑梦


    直到大学期间《炉石传说》横空出世,直到《游戏王》上架网易,直到我的项目组完成1.0后迎来空窗期一个月,我再也蚌埠住了,之前一直都对微信小游戏很有兴趣,每天闲着也是闲着,所以我有了做一个微信小游戏的想法。而且,就做一款在十几年前,就已经被我设计好的游戏。


    但是我不从来不是一个好学的人,领悟能力也很低,之前一直在看cocos和白鹭引擎学习文档,也很难学习下去,当然也因为工作期间没有这么多精力去学习,所以我什么框架也不会,不会框架,那就用原生。我初步的想法是,抛弃所有花里胡哨的动效,把基础的东西做出来,再作延伸。第一次做游戏,我也十分迷茫,最好的做法肯定是打飞机————研究这个微信项目如何用js原生,做出一个小游戏。



    虽然微信小游戏刚出来的时候看过代码,但是也只是一扫而过,而这次带着目标进行细细品味,果然感觉不一样。微信小游戏打飞机这个项目是js原生使用纯gL的模式编写的,主要就是在canvas这个画布上面作展示和用户行为。

      // 触摸事件处理逻辑
    touchEventHandler(e) {
    e.preventDefault()

    const x = e.touches[0].clientX
    const y = e.touches[0].clientY

    const area = this.gameinfo.btnArea

    if (x >= area.startX
    && x <= area.endX
    && y >= area.startY
    && y <= area.endY) this.restart()
    }

    点击事件我的理解就是用户点击到屏幕的坐标为(x, y),如果想要一个按钮上面做处理逻辑,那么点击的范围就要落在这个按钮的范围内。当我知道如何在canvas上面做点击行为时,我感觉我已经成功了一半,接下来就是编写基础js代码。


    首先这个游戏确定的元素分别为,场景,用户,单位,建筑,资源(后面改用能源替代),我先将每个元素封装好一个类,一边慢慢的回忆着之前游戏是如何设计的,一边编程,身心完全沉浸进去,已经很久很久没有试过如此专注的去编写代码。用了大概三天的时间,我把基本类该有的逻辑写完了,大概长这个样子



    上面为敌方的单位区域,下方为我方的单位区域,单位用ABCDEFG表示,右侧1/1/1 则是 攻击力/生命值/攻击次数,通过点击最下方的icon弹出创建建筑,然后创建单位,每次的用户操作,都是一个点击。


    一开始我设想的游戏名为想象博弈,因为每个单位每个建筑都只有名称,单位长什么样子的就需要玩家自己去脑补了,我只给你一个英文字母,你可以想象成奥特曼,也可以想象成哥斯拉,只要不是妈妈生的就行。



    湿了


    游戏虽然基本逻辑都写好了,放到整个微信小游戏界别人一定会认为是依托答辩,但我还是觉得这是我的掌上明珠,虽然游戏没有自己的界面,但是它有自己的玩法。好像上天也认可我的努力,但是觉得这个游戏还能更上一层楼,在某个摸鱼的moment,我打开了微信准备和各位朋友畅谈人生理想,发现有位同学发了一幅图,上面有四个格子,是赛博朋克风格的一位篮球运动员。他说这个AI软件生成的图片很逼真,只要把你想要的图片告诉给这个AI软件,就能发你一幅你所描绘的图片。我打开了图片看了看,说实话,质感相当不错,在一系列追问下,我得知这个绘图AI软件名称叫做midjourney



    midjourney



    我迫不及待的登录上去,询问朋友如何使用后,我用我蹩脚的英格力士迫不及待的试了试,让midjourney帮我画一个能源的icon,不试不要紧,一试便湿了,眼睛留下了感动地泪水,就像一个阴暗的房间打开了一扇窗,一束光猛地照射了进来。




    对比我之前在iconfont下载的免费图标,midjourney提供这些图片简直就是我的救世主,我很快便将一开始的免费次数用完,然后氪了一个30美刀的会员,虽然有点肉痛,但是为了儿时的梦想,这点痛算什么


    虽然我查找了一下攻略,别人说可以使用gpt和midjourney配合起来,我也试了一下,效果一般,可能姿势没有对,继续用我的有道翻译将重点词汇翻译后丢给midjourney。midjourney不仅可以四选一,还可以对图片不断优化,还有比例选择,各种参数,但是我也不需要用到那么多额外的功能,总之一个字,就是棒。


    但当时的我突然意识到,这个AI如此厉害,那么会不会对现在行业某些打工人造成一定影响呢,结果最近已经出了篇报道,某公司因为AI绘图工具辞退了众多插画师,事实不一定真实,但是也不是空穴来风,结合众多外界名人齐心协力抵制gpt5.0的研发,在担心数据安全之余,是否也在担心着AI对人类未来生活的各种冲击。焦虑时时刻刻都有,但解决焦虑的办法,我有一个妙招,仍然还是奖励自己


    门槛


    当我把整个小游戏焕然一新后,便兴冲冲的跑去微信开放平台上传我的伟大的杰作。但微信突然泼了我一盆冷水,上传微信小游戏前的流程有点出乎意外,要写游戏背景、介绍角色、NPC、介绍模块等等,还要上传不同的图片。我的小游戏一共就三个界面,有六个大板块要填写,每个板块还要两张不同的图片,我当时人就麻了。我只能创建一个单位截一次图,确保每张图片不一样。做完这道工序,还要写一份自审自查报告。


    就算做完了这些前戏,我感觉我的小游戏还是难登大雅之堂,突然,我又想到了这个东西其实是不是也能运行在web端呢,随后我便立刻付诸行动,创建一个带有canvas的html,之前微信小游戏是通过weapp-adapter这个文件把canvas暴露到全局,所以在web端使用canvas的时候,只需要使用document.getElementById('canvas')暴露到全局即可。然后通过http-server对应用进行启动,这个小游戏便以web端的形式运行到浏览器上了,终于也能理解之前为啥微信小游戏火起来的时候,很多企业都用h5游戏稍微改下代码进行搬运,原来两者之间是有异曲同工之妙之处的。


    关于游戏





    上面两张便是两个种族对应的生产链,龙族是我第一个创建的,因为我自幼对龙产生好感和兴趣,何况我是龙的传人/doge。魔法学院则是稍微致敬一下《游戏王》中黑魔导卡组吧。


    其实开发难度最难的莫过于是AI,也就是人机,如何让人机在有限的资源做出合理的选择,是一大难题,也是我后续要慢慢优化的,一开始我是让人机按照创建一个建筑,然后创建一个单位这种形式去做运营展开,但后来我想到一个好的点子,我应该可以根据每个种族的特点,走一条该特点的独有运营,于是人机龙族便有了龙蛋破坏龙两种流派,强度提升了一个档次。


    其实是否能上架到微信小游戏已经不重要了,重要的是这个过程带给我的乐趣,一步一步看着这个游戏被创建出来的成就感,就算这个行业受到什么冲击,我需要被迫转行,我也不曾后悔,毕竟是web前端让我跨越了十几年的时光,找到了儿时埋下的种子,浇水,给予阳光,让它在我的心中成长为一棵充实的参天大树


    h5地址:hslastudio.com/game/


    github地址: github.com/FEA-Dven/wa…


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

    关于晋升的一点思考

    晋升是一个极好的自我review的机会,不,应该是最好,而且没有之一。 晋升是最好的自我review的一次机会,不管有没有晋升成功,只要参加了晋升,认真准备过PPT,就已经包赚不赔了。 总的来说,晋升的准备工作充分体现出了——功夫在平时。平时要是没有两把刷子,...
    继续阅读 »

    晋升是一个极好的自我review的机会,不,应该是最好,而且没有之一。

    晋升是最好的自我review的一次机会,不管有没有晋升成功,只要参加了晋升,认真准备过PPT,就已经包赚不赔了。


    总的来说,晋升的准备工作充分体现出了——功夫在平时。平时要是没有两把刷子,光靠答辩准备的一两个月,是绝无可能把自己“包装”成一个合格的候选人的。

    下面整体剖析一下自己在整个准备过程中的观察、思考、判断、以及做的事情和拿到的结果。


    准备工作


    我做的第一件事情并不是动手写PPT,而是搜集信息,花了几天在家把网上能找到的所有关于晋升答辩的文章和资料全撸了一遍,做了梳理和总结。


    明确了以下几点:

    • 晋升是在做什么
    • 评委在看什么
    • 候选人要准备什么
    • 评判的标准是什么
    • 常见的坑有哪些

    首先要建立起来的,是自己对整个晋升的理解,形成自己的判断。后面才好开展正式的工作。



    写PPT


    然后开始进入漫长而又煎熬的PPT准备期,PPT的准备又分为四个子过程,并且会不断迭代进行。写成伪代码就是下面这个样子。

    do {
    确认思路框架;
    填充内容细节;
    模拟答辩;
    获取意见并判断是否还需要修改;
    } while(你觉得还没定稿);

    我的PPT迭代了n版,来来回回折腾了很多次,思路骨架改了4次,其中后面三次均是在准备的后半段完成的,而最后一次结构大改是在最后一周完成的。这让我深深的觉得前面准备的1个月很多都是无用功。


    迭代,迭代,还是迭代


    在筹备的过程中,有一个理念是我坚持和期望达到的,这个原则就是OODA loop ( Boyd cycle)


    OODA循环是美军在空战中发展出来的对敌理论,以美军空军上校 John Boyd 为首的飞行员在空战中驾驶速度慢火力差的F-86军刀,以1:10的击落比完胜性能火力俱佳的苏联米格-15。而Boyd上校总结的结论就是,不是要绝对速度快,而是要比对手更快的完成OODA循环


    而所谓的OODA循环,就是指 observe(观察)–orient(定位)–decide(决策)–act(执行) 的循环,是不是很熟悉,这不就是互联网的快速迭代的思想雏形嘛。


    相关阅读 what is OODA loop

    wiki.mbalib.com/wiki/包以德循环 (from 智库百科)

    en.wikipedia.org/wiki/OODA_l… (from Wikipedia)


    看看下图,PPT应该像第二排那样迭代,先把框架确定下来,然后找老板或其他有经验的人对焦,框架确定了以后再填充细节。如果一开始填充细节(像第一排那样),那么很有可能越改越乱,最后一刻还在改PPT。


    btw,这套理论对日常工作生活中的大部分事情都适用。


    一个信息论的最新研究成果


    我发现,程序员(也有可能是大部分人)有一个倾向,就是show肌肉。明明很简单明了的事情,非要搞得搞深莫测,明明清晰简洁的架构,非要画成“豆腐宴”。


    晋升述职核心就在做一件事,把我牛逼的经历告诉评委,并让他们相信我牛逼。


    所以,我应该把各种牛逼的东西都堆到PPT里,甚至把那些其实一般的东西包装的很牛逼,没错吧?


    错。


    这里面起到关键作用的是 “让他们相信我牛逼” ,而不是“把我牛逼的故事告诉评委”。简单的增大的输出功率是不够的,我要确保评委能听进去并且听懂我说的东西,先保证听众能有效接收,再在此基础上,让听众听的爽。


    How?


    公式:喜欢 = 熟悉 + 意外


    从信息论的角度来看,上面的公式说的就是旧信息和新信息之间要搭配起来。那么这个搭配的配比应该是多少呢?


    这个配比是15.87% ——《科学美国人》


    也就是说,你的内容要有85%是别人熟悉的,另外15%是能让别人意外的,这样就能达到最佳的学习/理解效果。这同样适用于心流、游戏设计、神经网络训练。所以,拿捏好这个度,别把你的PPT弄的太高端(不知所云),也别搞的太土味(不过尔尔)。


    能够否定自己,是一种能力


    我审视自己的时候发现,很多时候,我还保留一张PPT或是还持续的花心思做一件事情,仅仅是因为——舍不得。你有没有发现,我们的大脑很容易陷入“逻辑自洽”中,然后越想越对,越想越兴奋。


    千万记得,沉没成本不是成本,经济学里成本的定义是放弃了的最大价值,它是一个面向未来的概念,不面向过去。


    能够否定和推翻自己,不留恋于过去的“成就” ,可以帮助你做出更明智的决策。


    我一开始对好几页PPT依依不舍,觉得自己做的特牛逼。但是后来,这些PPT全被我删了,因为它们只顾着自己牛逼,但是对整体的价值却不大,甚至拖沓。


    Punchline


    Punchline很重要,这点我觉得做的好的人都自觉或不自觉的做到了。想想,当你吧啦吧啦讲的时候,评委很容易掉线的,如果你没有一些点睛之笔来高亮你的成果和亮点的话,别人可能就糊里糊涂的听完了。然后呢,他只能通过不断的问问题来挖掘你的亮点了。


    练习演讲


    经过几番迭代以后,PPT可以基本定稿,这个时候就进入下一个步骤,试讲。


    可以说,演讲几乎是所有一线程序员的短板,很多码农兄弟们陪电脑睡的多了,连“人话”有时候都讲不利索了。我想这都要怪Linus Torvalds的那句


    Talk is cheap. Show me the code.


    我个人的经验看来,虽然成为演讲大师长路漫漫不可及,但初级的演讲技巧其实是一个可以快速习得的技能,找到几个关键点,花几天时间好好练几遍就可以了,演讲要注意的点主要就是三方面:

    • 形象(肢体语言、着装等)
    • 声音(语速、语调、音量等)
    • 文字(逻辑、关键点等)


    演讲这块,我其实也不算擅长,我把仅有的库存拿出来分享。


    牢记表达的初衷


    我们演讲表达,本质上是一个一对多的通信过程,核心的目标是让评委或听众能尽可能多的接受到我们传达的信息


    很多程序员同学不善于表达,最明显的表现就是,我们只管吧啦吧啦的把自己想说的话说完,而完全不关心听众是否听进去了。


    讲内容太多


    述职汇报是一个提炼的过程,你可能做了很多事情,但是最终只会挑选一两件最有代表性的事情来展现你的能力。有些同学,生怕不能体现自己的又快又猛又持久,在PPT里塞了太多东西,然后又讲不完,所以只能提高语速,或者囫囵吞枣、草草了事。


    如果能牢记表达的初衷,就不应该讲太多东西,因为听众接收信息的带宽是有限的,超出接收能力的部分,只会转化成噪声,反而为你的表达减分。


    过度粉饰或浮夸


    为了彰显自己的过人之处,有时候会自觉或不自觉的把不是你的工作也表达出来,并没有表明哪些是自己做的,哪些是别人做的。一旦被评委识破(他本身了解,或问问题给问出来了),那将会让你陈述的可信度大打折扣。


    此外,也表达的时候也不要过分的浮夸或张扬,一定的抑扬顿挫是加分的,但过度浮夸会让人反感。


    注意衔接


    作为一个演讲者,演讲的逻辑一定要非常非常清晰,让别人能很清晰明了的get到你的核心思路。所以在演讲的时候要注意上下文之间的衔接,给听众建设心理预期:我大概会讲什么,围绕着什么展开,分为几个部分等等。为什么我要强调这个点呢,因为我们在演讲的时候,很容易忽略听众的感受,我自己心里有清楚的逻辑,但是表达的时候却很混乱,让人一脸懵逼。


    热情


    在讲述功能或亮点的时候,需要拿出自己的热情和兴奋,只有激动人心的演讲,才能抓住听众。还记得上面那个分布图吗?形象和声音的占比达到93%,也就是说,你自信满满、热情洋溢的说“吃葡萄不吐葡萄皮”,也能打动听众。


    第一印象


    这个大家都知道,就是人在最初形成的印象会对以后的评价产生影响 。

    这是人脑在百万年进化后的机制,可以帮助大脑快速判断风险和节省能耗——《思考,快与慢》

    评委会刻意避免,但是人是拗不过基因的,前五分钟至关重要,有经验的评委听5分钟就能判断候选人的水平,一定要想办法show出你的与众不同。可以靠你精心排版的PPT,也可以靠你清晰的演讲,甚至可以靠一些小 trick(切勿生搬硬套)。


    准备问题


    当PPT准备完,演讲也练好了以后,不出意外的话,应该没几天了。这个时候要进入最核心关键的环节,准备问题。


    关于Q&A环节,我的判断是,PPT和演讲大家都会精心准备,发挥正常的话都不会太差。这就好像高考里的语文,拉不开差距,顶多也就十几分吧。而Q&A环节,则是理综,优秀的和糟糕的能拉开50分的差距,直接决定总分。千万千万不可掉以轻心。


    问题准备我包含了这几个模块:

    • 业务:业务方向,业务规划,核心业务的理解,你做的事情和业务的关系,B类C类的差异等
    • 技术:技术难点,技术亮点,技术选型,技术方案的细节,技术规划,代码等
    • 数据:核心的业务数据,核心的技术指标,数据反映了什么等等
    • 团队:项目管理经验,团队管理经验
    • 个人:个人特色,个人规划,自己的反思等等

    其中业务、技术和数据这三块是最重要的,需要花80%的精力去准备。我问题准备大概花了3天时间,整体还是比较紧张的。准备问题的时候,明显的感觉到自己平时的知识储备还不太够,对大业务方向的思考还不透彻,对某些技术细节的把控也还不够到位。老话怎么说的来着,书到用时方恨少,事非经过不知难。


    准备问题需要全面,不能有系统性的遗漏。比如缺少了业务理解或竞品分析等。


    在回答问题上,也有一些要点需要注意:


    听清楚再回答


    问题回答的环节,很多人会紧张,特别是一两道问题回答的不够好,或气氛比较尴尬的时候,容易大脑短路。这个时候,评委反复问你一个问题或不断追问,而自己却觉得“我说的很清楚了呀,他还没明白吗”。我见过或听说过很多这样的案例,所以这应该是时有发生的。


    为了避免自己也踩坑,我给自己定下了要求,一定要听清楚问题,特别是问题背后的问题。如果觉得不清楚,就反问评委进行doubel check。并且在回答的过程中,要关注评委的反映,去确认自己是否答到点子上了。


    问题背后的问题


    评委的问题不是天马行空瞎问的,问题的背后是在考察候选人的某项素质,通过问题来验证或挖掘候选人的亮点。这些考察的点都是公开的,在Job Model上都有。


    我认为一个优秀的候选人,应当能识别出评委想考察你的点。找到问题背后的问题,再展开回答,效果会比单纯的挤牙膏来的好。


    逻辑自洽、简洁明了


    一个好的回答应该是逻辑自洽的。这里我用逻辑自洽,其实想说的是你的答案不一定要完全“正确”(其实往往也没有标准答案),但是一定不能自相矛盾,不能有明显的逻辑漏洞。大部分时候,评委不是在追求正确答案,而是在考察你有没有自己的思考和见解。当然,这种思考和见解几乎都是靠平时积累出来的,很难临时抱佛脚。


    此外,当你把逻辑捋顺了以后,简洁明了的讲出来就好了,我个人是非常喜欢能把复杂问题变简单的人的。一个问题的本质是什么,核心在那里,关键的几点是什么,前置条件和依赖是什么,需要用什么手段和资源去解决。当你把这些东西条分缕析的讲明白以后,不用再多啰嗦一句,任何人都能看出你的牛逼了。


    其他


    心态调整


    我的心态经历过过山车般的起伏,可以看到



    在最痛苦最难受的时候,如果身边有个人能理解你陪伴你,即使他们帮不上什么忙,也是莫大的宽慰。如果没有这样的人,那只能学会自己拥抱自己,自己激励自己了。


    所以,平时对自己的亲人好一点,对朋友们好一点,他们绝对是你人生里最大的财富。


    关于评委


    我从一开始就一直觉得评委是对手,是来挑战你的,对你的汇报进行证伪。我一直把晋升答辩当作一场battle来看待,直到进入考场的那一刻,我还在心理暗示,go and fight with ths giants。


    但真实的经历以后,感觉评委更多的时候并不是站在你的对立面。评委试图通过面试找到你的一些闪光点,从而论证你有能力晋升到下一个level。从这个角度来讲,评委不但不是“敌人”,更像是友军一般,给你输送弹药(话题)。


    一些教训

    • 一定要给自己设置deadline,并严格执行它。如果自我push的能力不强,就把你的deadline公开出来,让老板帮你监督。

    • 自己先有思考和判断,再广开言路,不要让自己的头脑成为别人思想的跑马场。

    • 坚持OODA,前期千万不要扣细节。这个时候老板和同事是你的资源,尽管去打扰他们吧,后面也就是一两顿饭的事情。


    附件


    前期调研



    参考文章


    知乎


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

    Widget开发流程

    iOS
    本文所用到的 Demo 可以在这里下载: github.com/zhenyunzhan… 一、创建Widget Extension 1、创建Widget Target 点击 Project —> 添加新的Target —> 搜索Widget Ext...
    继续阅读 »

    本文所用到的 Demo 可以在这里下载: github.com/zhenyunzhan…


    一、创建Widget Extension


    1、创建Widget Target


    点击 Project —> 添加新的Target —> 搜索Widget Extension —> 点击Widget Extension —> 点击 Next




    2、添加配置信息


    Include Configuration Intent 是创建 intentdefinition 文件用的,可以让用户动态配置组件,可以先不勾选,后期可以手动创建




    3、添加Widget


    创建好之后就可以运行程序,程序运行完成之后,长按主屏幕进入编辑状态,点击主屏幕右上方添加按钮,找到程序,就可以添加Widget,简单体验下了


    二、大致了解 Widget 文件


    查看创建完 Widget Extension 后默认生成的 xxxx Widget.swift 文件,Xcode 为我们生成了 Widget 所需的模版代码,如下这个文件




    widget的入口 @main标识的部分




    view EntryView 是我们实际展示的UI




    数据 Entry 是用来携带widget展示需要的数据




    Provider Timeline的提供对象,包含TimelineEntry & ReloadPolicy,用来后续刷新 Widget 内容




    三、开发


    以Demo为例,做一个展示古诗内容的Widget,间隔一段时间后自动更新widget内容,并且可以自动选择古诗内容来跟新Widget,例子如下:


    展示古诗内容 -> 长按后可编辑小组件 -> 进入选择界面 -> 选择并更新




    四、静态配置 StaticConfiguration


    创建完 Widget Extension Target之后,系统会给我们创建好一个Widget的开发模板


    1、TimelineEntry


    自己创建的模型作为参数,模型 (itemModel) 用 swift 或者 OC创建均可




    2、界面样式


    界面有三种尺寸的类型,每种尺寸还可以准备不同的布局,另外界面填充的数据就来源于 TimelineEntry




    3、Timeline时间线


    实现 TimelineProvider 协议 getTimeline 方法,主要是构建 Entry 和 reloadPolicy,用这两个参数初始化 Timeline ,之后再调用completion回调,回调会走到 @main ,去更新 Widget 内容。


    demo中是每次刷新 Timeline ,创建一个 Entry, 则更新一次主屏幕的 Widget 内容, 刷新间隔为 60 分钟,注意:

    • atEnd 所有的entry执行完之后,再次调用 getTimeline 构造数据

    • after(date) 不管这次构造的entry 是否执行完,等系统时间到达date之后,就会在调用getTimeline

    • never 最后一个 entry 展示完毕之后 Widget 就会一直保持那个 entry 的显示内容




    开发完成后,可以运行代码,试一下效果,此时的更新时间有点长,可以改为 5 秒后再试。


    五、动态配置 IntentConfiguration


    程序运行到这里,有的会想,怎么实现编辑小组件功能,动态配置 widget 的显示内容呢?




    1、创建 intentdefinition 文件


    command + N 组合键创建新 File —> 搜索 intent




    选择xxx.intentdefinition文件 —>点击下方 + ,选择intent创建 —> 对intent命名






    这个 intent 文件包含了你所有的(intents),通过这个文件编译到你的app中,系统将能够读取你的 intents ,一旦你定义了一个intent文件,Xcode也会为你生成一个intent类别


    2、可以添加到 intent 中的参数类型


    参数类型有多种,下方为一些示例
    参数类型分别为:String、Boolean、Integer时的展示




    你也可以用自己定义的类型去设置,参数也支持多个值




    3、如何为小组件添加丰富的配置


    a、确定配置对象


    以这个demo为例,小组件只能显示一首古诗,但是app中有很多首古诗,这就可以创建多个 古诗 组件,然后通过动态配置,每个小组件想要显示不同的古诗。这样的例子还有很多,比如某个人有多张银行卡,每个组件显示不同银行卡余额




    b、配置intent文件


    category选项设置为View,然后勾选下图中的选项,现在我们可以只关注小组件选项,将快捷指令的勾选也取消,如下图




    c、intent添加参数


    使用参数列表中的 + 按钮,添加一个参数




    Type类型可以选择自定义的type




    参数添加完后,系统会在ClickBtnIntent类中生成相应的属性




    随后ClickBtnIntent 的实例将在运行时传递到 小组件扩展中,让你的小组件知道用户配置了什么,以及要显示什么




    d、代码中的更改


    StaticConfiguration 切换为 IntentConfiguration,相应的provider也改为IntentTimelineProvider,provider就不上截图了,可以去demo中的ClickBtn.swift文件查看




    现在运行APP,然后长按古诗小组件,选择编辑小组件,会弹出带有Btn Type的参数,点击Btn Type一栏弹出带有搜索的列表页面。 效果如下:




    显示的Btn Type就是下图中框选Display Name,自己可以随便起名字,中英文均可




    目前,带有搜索的列表页面是一个空白页面,如果想要使其有数据,则要都选Dynamic Options复选框,为其添加动态数据




    e、如何为列表添加动态数据?


    勾选了Dynamic Options复选框,系统会自动生成一个ClickBtnIntentHandling协议,可以点开ClickIntent类去查看,现在有了intent文件,有了新的可遵守协议,就需要有一个Extension去遵守协议,实现协议里边的方法,为搜索列表提供数据



    • 点击Project —> 新建target —> 搜索intent —> 选择 Intents Extentsion







    • 贴上类的方法,以及方法对应的效果图




    f、注意点


    实现IntentHandler时,Xcode会报找不到ClickBtnIntentHandling这个协议的错误,

    • 引入头文件 Intents
    • 需要将下图所标的地方做下修改



    六、APP创建多个Widget


    这个比较简单,按照demo中的例子处理一下就可以,如下图:




    目前测试,最多可以同时创建五个不同的Widget


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

    LangChain 是 LLM 交响乐的指挥家

    本文分享 LangChain 框架中一个引人入胜的概念 Prompt Templates。如果热衷于语言模型并且热爱 Python,那么将会大饱口福。 深入了解 LangChain 的世界,这个框架在我作为开发者的旅程中改变了游戏规则。 LangChain 是...
    继续阅读 »

    本文分享 LangChain 框架中一个引人入胜的概念 Prompt Templates。如果热衷于语言模型并且热爱 Python,那么将会大饱口福。


    深入了解 LangChain 的世界,这个框架在我作为开发者的旅程中改变了游戏规则。


    LangChain 是一个框架,它一直是我作为开发者旅途中的规则改变者。 LangChain 是一个独特的工具,它利用大语言模型(LLMs)的力量为各种使用案例构建应用程序。Harrison Chase 的这个创意于 2022 年 10 月作为开源项目首次亮相。从那时起,它就成为 GitHub 宇宙中一颗闪亮的明星,拥有高达 42,000 颗星,并有超过 800 名开发者的贡献。


    LangChain 就像一位大师,指挥着 OpenAI 和 HuggingFace Hub 等 LLM 模型以及 Google、Wikipedia、Notion 和 Wolfram 等外部资源的管弦乐队。它提供了一组抽象(链和代理)和工具(提示模板、内存、文档加载器、输出解析器),充当文本输入和输出之间的桥梁。这些模型和组件链接到管道中,这让开发人员能够轻而易举地快速构建健壮的应用程序原型。本质上,LangChain 是 LLM 交响乐的指挥家。


    LangChain 的真正优势在于它的七个关键模块:

    1. 模型:这些是构成应用程序主干的封闭或开源 LLM
    2. 提示:这些是接受用户输入和输出解析器的模板,这些解析器格式化 LLM 模型的输出。
    3. 索引:该模块准备和构建数据,以便 LLM 模型可以有效地与它们交互。
    4. 记忆:这为链或代理提供了短期和长期记忆的能力,使它们能够记住以前与用户的交互。
    5. :这是一种在单个管道(或“链”)中组合多个组件或其他链的方法。
    6. 代理人:根据输入决定使用可用工具/数据采取的行动方案。
    7. 回调:这些是在 LLM 运行期间的特定点触发以执行的函数。

    GitHub:python.langchain.com/


    什么是提示模板?


    在语言模型的世界中,提示是一段文本,指示模型生成特定类型的响应。顾名思义,提示模板是生成此类提示的可重复方法。它本质上是一个文本字符串,可以接收来自最终用户的一组参数并相应地生成提示。


    提示模板可以包含语言模型的说明、一组用于指导模型响应的少量示例以及模型的问题。下面是一个简单的例子:

    from langchain import PromptTemplate

    template = """
    I want you to act as a naming consultant for new companies.
    What is a good name for a company that makes {product}?
    """

    prompt = PromptTemplate(
    input_variables=["product"],
    template=template,
    )

    prompt.format(product="colorful socks")

    在此示例中,提示模板要求语言模型为生产特定产品的公司建议名称。product 是一个变量,可以替换为任何产品名称。


    创建提示模板


    在 LangChain 中创建提示模板非常简单。可以使用该类创建简单的硬编码提示 PromptTemplate。这些模板可以采用任意数量的输入变量,并且可以格式化以生成提示。以下是如何创建一个没有输入变量、一个输入变量和多个输入变量的提示模板:

    from langchain import PromptTemplate

    # No Input Variable 无输入变量
    no_input_prompt = PromptTemplate(input_variables=[], template="Tell me a joke.")
    print(no_input_prompt.format())

    # One Input Variable 一个输入变量
    one_input_prompt = PromptTemplate(input_variables=["adjective"], template="Tell me a {adjective} joke.")
    print(one_input_prompt.format(adjective="funny"))

    # Multiple Input Variables 多个输入变量
    multiple_input_prompt = PromptTemplate(
    input_variables=["adjective", "content"],
    template="Tell me a {adjective} joke about {content}."
    )
    print(multiple_input_prompt.format(adjective="funny", content="chickens"))

    总结


    总之,LangChain 中的提示模板是为语言模型生成动态提示的强大工具。它们提供了对提示的灵活性和控制,能够有效地指导模型的响应。无论是为特定任务创建语言模型还是探索语言模型的功能,提示模板都可以改变游戏规则。


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

    AI孙燕姿翻唱爆火,多亏这个开源项目!广西老表带头打造,上手指南已出

    明敏 发自 凹非寺 量子位 | 公众号 QbitAI AI孙燕姿这么快翻唱了这么多首歌,到底是咋实现的? 关键在于一个开源项目。 最近,这波AI翻唱趋势大火,不仅是AI孙燕姿唱的歌越来越多,AI歌手的范围也在扩大,就连制作教程都层出不穷了。 而如果在各大教程...
    继续阅读 »
    明敏 发自 凹非寺 量子位 | 公众号 QbitAI

    AI孙燕姿这么快翻唱了这么多首歌,到底是咋实现的?


    关键在于一个开源项目




    最近,这波AI翻唱趋势大火,不仅是AI孙燕姿唱的歌越来越多,AI歌手的范围也在扩大,就连制作教程都层出不穷了。


    而如果在各大教程中溜达一圈后就会发现,其中的关键秘诀,还是要靠一个名为so-vits-svc的开源项目。



    它提供了一种音色替换的办法,项目在今年3月发布。


    贡献成员应该大部分都来自国内,其中贡献量最高的还是一位玩明日方舟的广西老表。




    如今,项目已经停止更新了,但是星标数量还在蹭蹭上涨,目前已经到了8.4k。


    所以它到底实现了哪些技术能引爆这波趋势?


    一起来看。


    多亏了一个开源项目


    这个项目名叫SoftVC VITS Singing Voice Conversion(歌声转换)。


    它提供了一种音色转换算法,采用SoftVC内容编码器提取源音频语音特征,然后将矢量直接输入VITS,中间不转换成文本,从而保留了音高和语调。


    此外,还将声码器改为NSF HiFiGAN,可以解决声音中断的问题。


    具体分为以下几步:

    • 预训练模型
    • 准备数据集
    • 预处理
    • 训练
    • 推理

    其中,预训练模型这步是关键之一,因为项目本身不提供任何音色的音频训练模型,所以如果你想要做一个新的AI歌手出来,需要自己训练模型。


    而预训练模型的第一步,是准备干声,也就是无音乐的纯人声。


    很多博主使用的工具都是UVR_v5.5.0


    推特博主@歸藏介绍说,在处理前最好把声音格式转成WAV格式,因为So-VITS-SVC 4.0只认这个格式,方便后面处理。


    想要效果好一些,需要处理两次背景音,每次的设置不同,能最大限度提高干声质量。


    得到处理好的音频后,需要进行一些预处理操作。


    比如音频太长容易爆显存,需要对音频切片,推荐5-15秒或者再长一点也OK。


    然后要重新采样到44100Hz和单声道,并自动将数据集划分为训练集和验证集,生成配置文件。再生成Hubert和f0。


    接下来就能开始训练和推理了。


    具体的步骤可以移步GitHub项目页查看(指路文末)。


    值得一提的是,这个项目在今年3月上线,目前贡献者有25位。从贡献用户的简介来看,很多应该都来自国内。


    据说项目刚上线时也有不少漏洞并且需要编程,但是后面几乎每一天都有人在更新和修补,现在的使用门槛已经降低了不少。


    目前项目已经停止更新了,但还是有一些开发者创建了新的分支,比如有人做出了支持实时转换的客户端。




    项目贡献量最多的一位开发者是Miuzarte,从简介地址判断应该来自广西。




    随着想要上手使用的人越来越多,也有不少博主推出了上手难度更低、更详细的食用指南。


    歸藏推荐的方法是使用整合包来推理(使用模型)和训练,还有B站的Jack-Cui展示了Windows下的步骤指南(http://www.bilibili.com/read/cv2237…


    需要注意的是,模型训练对显卡要求还是比较高的,显存小于6G容易出现各类问题。


    Jack-Cui建议使用N卡,他用RTX 2060 S,训练自己的模型大概用了14个小时


    训练数据也同样关键,越多高质量音频,就意味着最后效果可以越好。


    还是会担心版权问题


    值得一提的是,在so-vits-svc的项目主页上,着重强调了版权问题。



    警告:请自行解决数据集的授权问题。因使用未经授权的数据集进行培训而产生的任何问题及其一切后果,由您自行承担责任。存储库及其维护者、svc开发团队,与生成结果无关!





    这和AI画画爆火时有点相似。


    因为AI生成内容的最初数据取材于人类作品,在版权方面的争论不绝于耳。


    而且随着AI作品盛行,已经有版权方出手下架平台上的视频了。


    据了解,一首AI合成的《Heart on My Sleeve》在油管和Tik Tok上爆火,它合成了Drake和Weekend演唱的版本。


    但随后,Drake和Weekend的唱片公司环球音乐将这个视频从平台上下架了,并在声明里向潜在的仿冒者发问,“是要站在艺术家、粉丝和人类创造性表达的一边,还是站在Deepfake、欺诈和拒付艺术家赔偿的一边?”


    此外,歌手Drake也在ins上对AI合成翻唱歌曲表达了不满。


    而另一边,也有人选择拥抱这项技术。


    加拿大歌手Grimes表示,她愿意让别人使用自己的声音合成歌曲,但是要给她一半版权费。


    GitHub地址:

    github.com/svc-develop…


    参考链接:

    [1]mp.weixin.qq.com/s/bXD1u6ysY…

    [2]http://www.vulture.com/article/ai-…


    —  —


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

    PAG动效框架源码笔记 (四)渲染框架

    iOS
    前言 PAG采用自研TGFX特效渲染引擎,抽象分离了接口及平台实现类,可以扩展支持多种图形渲染库,比如OpenGL、Metal等 TGFX引擎是如何实现纹理绘制?本文基于OpenGL图形库分析讲解TGFX渲染框架分层及详细架构设计。开始之前,先提一个问题: 绘...
    继续阅读 »

    前言


    PAG采用自研TGFX特效渲染引擎,抽象分离了接口及平台实现类,可以扩展支持多种图形渲染库,比如OpenGL、Metal等


    TGFX引擎是如何实现纹理绘制?本文基于OpenGL图形库分析讲解TGFX渲染框架分层及详细架构设计。开始之前,先提一个问题:


    绘制一个Texture纹理对象,一般需要经历哪些过程?


    渲染流程


    通常情况下,绘制一个Texture纹理对象到目标Layer上,可以抽象为以下几个阶段:


    1. 获取上下文: 通过EGL获取Context绘制上下文,提供与渲染设备交互的能力,比如缓冲区交换、Canvas及Paint交互等


    2. 定义着色器: 基于OpenGL的着色器语言(GLSL)编写着色器代码,编写自定义顶点着色器和片段着色器代码,编译、链接加载和使用它们


    3. 绑定数据源: 基于渲染坐标系几何计算绑定顶点数据,加载并绑定纹理对象给GPU,设置渲染目标、混合模式等


    4. 渲染执行: 提交渲染命令给渲染线程,转化为底层图形API调用、并执行实际的渲染操作




    关于OpenGL完整的渲染流程,网上有比较多的资料介绍,在此不再赘述,有兴趣的同学可以参考 OpenGL ES Pipeline


    框架层级


    TGFX框架大致可分为三大块:


    1. Drawable上下文: 基于EGL创建OpenGL上下文,提供与渲染设备交互的能力


    2. Canvas接口: 定义画布Canvas及画笔Paint,对外提供渲染接口、记录渲染状态以及创建绘制任务等


    3. DrawOp执行: 定义并装载着色器函数,绑定数据源,执行实际渲染操作


    为了支持多平台,TGFX定义了一套完整的框架基类,实现框架与平台的物理隔离,比如矩阵对象Matrix、坐标Rect等,应用上层负责平台对象与TFGX对象的映射转化

    - (void)setMatrix:(CGAffineTransform)value {
    pag::Matrix matrix = {};
    matrix.setAffine(value.a, value.b, value.c, value.d, value.tx, value.ty);
    _pagLayer->setMatrix(matrix);
    }

    Drawable上下文


    PAG通过抽象Drawable对象,封装了绘制所需的上下文,其主要包括以下几个对象


    1. Device(设备): 作为硬件设备层,负责与渲染设备交互,比如创建维护EAGLContext等


    2. Window(窗口): 拥有一个Surface,负责图形库与绘制目标的绑定,比如将的opengl的renderBuffer绑定到CAEAGLLayer上;


    3. Surface(表面): 创建canvas画布提供可绘制区域,对外提供flush绘制接口;当窗口尺寸发生变化时,surface会创建新的canvas


    4. Canvas(画布): 作为实际可绘制区域,提供绘制api,进行实际的绘图操作,比如绘制一个image或者shape等



    详细代码如下:


    1、Device创建Context
    std::shared_ptr<GLDevice> GLDevice::Make(void* sharedContext) {
    if (eaglShareContext != nil) {
    eaglContext = [[EAGLContext alloc] initWithAPI:[eaglShareContext API]
    sharegroup:[eaglShareContext sharegroup]];
    } else {
    // 创建Context
    eaglContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3];
    if (eaglContext == nil) {
    eaglContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
    }
    }
    auto device = EAGLDevice::Wrap(eaglContext, false);
    return device;
    }

    std::shared_ptr<EAGLDevice> EAGLDevice::Wrap(EAGLContext* eaglContext, bool isAdopted) {
    auto oldEAGLContext = [[EAGLContext currentContext] retain];
    if (oldEAGLContext != eaglContext) {
    auto result = [EAGLContext setCurrentContext:eaglContext];
    if (!result) {
    return nullptr;
    }
    }
    auto device = std::shared_ptr<EAGLDevice>(new EAGLDevice(eaglContext),
    EAGLDevice::NotifyReferenceReachedZero);
    if (oldEAGLContext != eaglContext) {
    [EAGLContext setCurrentContext:oldEAGLContext];
    }
    return device;
    }

    // 获取Context
    bool EAGLDevice::makeCurrent(bool force) {
    oldContext = [[EAGLContext currentContext] retain];
    if (oldContext == _eaglContext) {
    return true;
    }
    if (![EAGLContext setCurrentContext:_eaglContext]) {
    oldContext = nil;
    return false;
    }
    return true;
    }

    2、Window创建Surface,绑定RenderBuffer
    std::shared_ptr<Surface> EAGLWindow::onCreateSurface(Context* context) {
    auto gl = GLFunctions::Get(context);
    ...
    gl->genFramebuffers(1, &frameBufferID);
    gl->bindFramebuffer(GL_FRAMEBUFFER, frameBufferID);
    gl->genRenderbuffers(1, &colorBuffer);
    gl->bindRenderbuffer(GL_RENDERBUFFER, colorBuffer);
    gl->framebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, colorBuffer);
    auto eaglContext = static_cast<EAGLDevice*>(context->device())->eaglContext();
    // 绑定到CAEAGLLayer上
    [eaglContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:layer];
    ...
    GLFrameBufferInfo glInfo = {};
    glInfo.id = frameBufferID;
    glInfo.format = GL_RGBA8;
    BackendRenderTarget renderTarget = {glInfo, static_cast<int>(width), static_cast<int>(height)};
    // 创建Surface
    return Surface::MakeFrom(context, renderTarget, ImageOrigin::BottomLeft);
    }

    // 通过renderTarget持有context、frameBufferID及Size
    std::shared_ptr<Surface> Surface::MakeFrom(Context* context,
    const BackendRenderTarget& renderTarget,
    ImageOrigin origin, const SurfaceOptions* options) {
    auto rt = RenderTarget::MakeFrom(context, renderTarget, origin);
    return MakeFrom(std::move(rt), options);
    }

    3、Surface创建Canvas及flush绘制
    Canvas* Surface::getCanvas() {
    // 尺寸变化时会清空并重新创建canvas
    if (canvas == nullptr) {
    canvas = new Canvas(this);
    }
    return canvas;
    }

    bool Surface::flush(BackendSemaphore* signalSemaphore) {
    auto semaphore = Semaphore::Wrap(signalSemaphore);
    // drawingManager创建tasks,装载绘制pipiline
    renderTarget->getContext()->drawingManager()->newTextureResolveRenderTask(this);
    auto result = renderTarget->getContext()->drawingManager()->flush(semaphore.get());
    return result;
    }

    4、渲染流程
    bool PAGSurface::draw(RenderCache* cache, std::shared_ptr<Graphic> graphic,
    BackendSemaphore* signalSemaphore, bool autoClear) {
    // 获取context上下文
    auto context = lockContext(true);
    // 获取surface
    auto surface = drawable->getSurface(context);
    // 通过canvas画布
    auto canvas = surface->getCanvas();
    // 执行实际绘制
    onDraw(graphic, surface, cache);
    // 调用flush
    surface->flush();
    // glfinish
    context->submit();
    // 绑定GL_RENDERBUFFER
    drawable->present(context);
    // 释放context上下文
    unlockContext();
    return true;
    }

    Canvas接口


    Canvas API主要包括画布操作及对象绘制两大类:


    画布操作包括Matrix矩阵变化、Blend融合模式、画布裁切等设置,通过对canvasState画布状态的操作实现绘制上下文的切换


    对象绘制包括Path、Shape、Image以及Glyph等对象的绘制,结合Paint画笔实现纹理、文本、图形、蒙版等多种形式的绘制及渲染

    class Canvas {
    // 画布操作
    void setMatrix(const Matrix& matrix);
    void setAlpha(float newAlpha);
    void setBlendMode(BlendMode blendMode);

    // 绘制API
    void drawRect(const Rect& rect, const Paint& paint);
    void drawPath(const Path& path, const Paint& paint);
    void drawShape(std::shared_ptr<Shape> shape, const Paint& paint);
    void drawImage(std::shared_ptr<Image> image, const Matrix& matrix, const Paint* paint = nullptr);
    void drawGlyphs(const GlyphID glyphIDs[], const Point positions[], size_t glyphCount,
    const Font& font, const Paint& paint);
    };
    // CanvasState记录当前画布的状态,包括Alph、blend模式、变化矩阵等
    struct CanvasState {
    float alpha = 1.0f;
    BlendMode blendMode = BlendMode::SrcOver;
    Matrix matrix = Matrix::I();
    Path clip = {};
    uint32_t clipID = kDefaultClipID;
    };

    // 通过save及restore实现绘制状态的切换
    void Canvas::save() {
    auto canvasState = std::make_shared<CanvasState>();
    *canvasState = *state;
    savedStateList.push_back(canvasState);
    }

    void Canvas::restore() {
    if (savedStateList.empty()) {
    return;
    }
    state = savedStateList.back();
    savedStateList.pop_back();
    }

    DrawOp执行


    DrawOp负责实际的绘制逻辑,比如OpenGL着色器函数的创建装配、顶点及纹理数据的创建及绑定等


    TGFX抽象了FillRectOp矩形绘制Op,可以覆盖绝大多数场景的绘制需求


    当然,其还支持其它类型的绘制Op,比如ClearOp清屏、TriangulatingPathOp三角图形绘制Op等

    class DrawOp : public Op {
    // DrawOp通过Pipiline实现多个_colors纹理对象及_masks蒙版的绘制
    std::vector<std::unique_ptr<FragmentProcessor>> _colors;
    std::vector<std::unique_ptr<FragmentProcessor>> _masks;
    };

    // 矩形实际绘制执行者
    class FillRectOp : public DrawOp {
    FillRectOp(std::optional<Color> color, const Rect& rect, const Matrix& viewMatrix,
    const Matrix& localMatrix);
    void onPrepare(Gpu* gpu) override;
    void onExecute(OpsRenderPass* opsRenderPass) override;
    };

    总结


    本文结合OpenGL讲解了TGFX渲染引擎的大概框架结构,让各位有了一个初步认知


    接下来将结合image纹理绘制介绍TGFX渲染引擎详细的绘制渲染流程,欢迎大家关注点赞!


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

    SwiftUI关于菜单 iOS 的长按 & macOS 右键的实现

    iOS
    长按 按钮或者图片出现菜单是个很平常的操作。 从app的icon 到app 内部的按钮 可以将内部的一些操作整合到这个特点内 SwiftUI 自带的菜单选择 ContextMenu 代码 iOS 效果 macOS 在mac上不是长按了,是右键的菜单操作 文案...
    继续阅读 »

    长按 按钮或者图片出现菜单是个很平常的操作。


    从app的icon 到app 内部的按钮 可以将内部的一些操作整合到这个特点内


    SwiftUI 自带的菜单选择 ContextMenu


    代码




    iOS 效果



    macOS


    在mac上不是长按了,是右键的菜单操作



    文案可能要修改一下,应该叫 右键


    这里有一个有趣的点,mac 版本的样式是没有图标。必须加一句

    Button(action: { fileData.selectedFilesToOperate = [item] //单个  
    fileWindow.isShowMoveFileView = true })
    { Label("移动", systemImage: "folder")
    .labelStyle(.titleAndIcon)
    }

    但是现实的情况往往没有如此的简单,至少产品和老板的需求,都不是那么简单。下面几个我自己遇到的情况
    可能不太全面,但是按图索骥应该可以给看遇到相似问题的人一点启发的感觉


    问题1 菜单 不能太单调,分别来显示

    Section {
    Button1
    Button2 ....
    }

    用section 包裹 可以让菜单有明显的分区



    问题2 菜单里面放点别的


    那再放开一点,,contextMenu 内部 放点别的

          contextMenu {
    // picker
    // list
    // toggle
    // image...
    }



    放入单选记得选什么的 Picker



    放入子菜单


    这里用到了 Menu 这个标签


    这个表情 也是个菜单,点击就有,不用长按。


    菜单里面放菜单的效果


    Menu {

                                Picker(selection: $sort, label: Text("Sorting options")) {

                                    Text("Size").tag(0)

                                    Text("Date").tag(1)

                                    Text("Location").tag(2)

                                }

                            } label: {

                                Label("Sort", systemImage: "arrow.up.arrow.down")

                            }

    这个效果挺有意思,和mac 的右键的子菜单一个效果。



    这个放一切UI的效果,确实比较有趣。有兴趣可以尝试放入更丰富的控件。


    SwiftUI 的控件我个人感觉的套路

    1. 一切view 都是声明的方式,靠@State 或者@Publish 一些的Modify来控制控件的显示数据
    2. 因为没有了生命周期,对于onAppair 和DisAppair的控制放在了每一个控件上的@ViewBuilder上,这个可以自定义,开始的时候都用自带的 @ViewBuilder
    3. View 都是Struct,class用的不多。
    4. View 里面包View,尽量做到了控件复用。而且是挑明了就是,比如之前的Text里面label,Button里面的Label,NavigationLink里面的View(也可以一切不同类型的View)

    个人感觉这些都是在表面SwiftUI 打破以前Swift UIKit或者是OC中的UIKit的思维逻辑。


    既: UI廉价 刷新廉价


    让程序员 特别是iOS 开发过程中,不同状态的刷新UI ,回调刷新UI的开发复杂度


    总结


    对于一个控件的开始编写,到不停叠加复杂的情况,还有许多场景还没遇到和想到。目前SwiftUI的源码和网上的资料,还不如OC 如此内核的解析资料丰富。但是未来的iOS开发 一定是SwiftUI的时代,特别是对于个人开发者相比OC 友好程度明显。


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

    禁止别人调试自己的前端页面代码

    web
    🎈 为啥要禁止? 由于前端页面会调用很多接口,有些接口会被别人爬虫分析,破解后获取数据 为了 杜绝 这种情况,最简单的方法就是禁止人家调试自己的前端代码 🎈 无限 debugger 前端页面防止调试的方法主要是通过不断 debugger 来疯狂输出断点...
    继续阅读 »

    🎈 为啥要禁止?



    • 由于前端页面会调用很多接口,有些接口会被别人爬虫分析,破解后获取数据

    • 为了 杜绝 这种情况,最简单的方法就是禁止人家调试自己的前端代码


    禁止调试


    🎈 无限 debugger



    • 前端页面防止调试的方法主要是通过不断 debugger 来疯狂输出断点,因为 debugger 在控制台被打开的时候就会执行

    • 由于程序被 debugger 阻止,所以无法进行断点调试,所以网页的请求也是看不到的

    • 基础代码如下:


    /**
    * 基础禁止调试代码
    */

    (() => {
    function ban() {
    setInterval(() => {
    debugger;
    }, 50);
    }
    try {
    ban();
    } catch (err) { }
    })();

    基础禁止调试


    🎈 无限 debugger 的对策



    • 如果仅仅是加上面那么简单的代码,对于一些技术人员而言作用不大

    • 可以通过控制台中的 Deactivate breakpoints 按钮或者使用快捷键 Ctrl + F8 关闭无限 debugger

    • 这种方式虽然能去掉碍眼的 debugger,但是无法通过左侧的行号添加 breakpoint


    取消禁止对策


    🎈 禁止断点的对策



    • 如果将 setInterval 中的代码写在一行,就能禁止用户断点,即使添加 logpointfalse 也无用

    • 当然即使有些人想到用左下角的格式化代码,将其变成多行也是没用的


    (() => {
    function ban() {
    setInterval(() => { debugger; }, 50);
    }
    try {
    ban();
    } catch (err) { }
    })();

    禁止断点


    🎈 忽略执行的代码



    • 通过添加 add script ignore list 需要忽略执行代码行或文件

    • 也可以达到禁止无限 debugger


    忽略执行的代码


    🎈 忽略执行代码的对策



    • 那如何针对上面操作的恶意用户呢

    • 可以通过将 debugger 改写成 Function("debugger")(); 的形式来应对

    • Function 构造器生成的 debugger 会在每一次执行时开启一个临时 js 文件

    • 当然使用的时候,为了更加的安全,最好使用加密后的脚本


    // 加密前
    (() => {
    function ban() {
    setInterval(() => {
    Function('debugger')();
    }, 50);
    }
    try {
    ban();
    } catch (err) { }
    })();

    // 加密后
    eval(function(c,g,a,b,d,e){d=String;if(!"".replace(/^/,String)){for(;a--;)e[a]=b[a]||a;b=[function(f){return e[f]}];d=function(){return"\w+"};a=1}for(;a--;)b[a]&&(c=c.replace(new RegExp("\b"+d(a)+"\b","g"),b[a]));return c}('(()=>{1 0(){2(()=>{3("4")()},5)}6{0()}7(8){}})();',9,9,"block function setInterval Function debugger 50 try catch err".split(" "),0,{}));

    解决对策


    🎈 终极增强防调试代码



    • 为了让自己写出来的代码更加的晦涩难懂,需要对上面的代码再优化一下

    • Function('debugger').call() 改成 (function(){return false;})['constructor']('debugger')['call']();

    • 并且添加条件,当窗口外部宽高和内部宽高的差值大于一定的值 ,我把 body 里的内容换成指定内容

    • 当然使用的时候,为了更加的安全,最好加密后再使用


    (() => {
    function block() {
    if (window.outerHeight - window.innerHeight > 200 || window.outerWidth - window.innerWidth > 200) {
    document.body.innerHTML = "检测到非法调试,请关闭后刷新重试!";
    }
    setInterval(() => {
    (function () {
    return false;
    }
    ['constructor']('debugger')
    ['call']());
    }, 50);
    }
    try {
    block();
    } catch (err) { }
    })();

    终极增强防调试

    收起阅读 »

    记录用前端代替后端生成zip的过程,速度快了 57 倍!!!

    业务场景: 产品有个功能是设置主题。类似手机自动切换壁纸,以及其他功能颜色,icon,字体等。 管理员需要在后端管理系统多次下载不同主题,(至于要干啥就不说了...),主题中可能有 30 ~ 100个高清壁纸, icon 等。现在每次下载主题(31张高清图片)...
    继续阅读 »

    业务场景:


    产品有个功能是设置主题。类似手机自动切换壁纸,以及其他功能颜色,icon,字体等。


    管理员需要在后端管理系统多次下载不同主题,(至于要干啥就不说了...),主题中可能有 30 ~ 100个高清壁纸, icon 等。现在每次下载主题(31张高清图片)至少需要 10s。有什么方法能够优化下。



    因为代码不具备可复用性,因此部分代码直接省略,思路为主


    原始逻辑


      public async getZip(themeId: string, res: any) {
    const theme = await this.model.findById(themeId); // 从数据库

    // 这里需要借用一个服务器上的主题模板文件夹 template/,

    /*
    theme = {
    wallpapers: [
    { url: 'https://亚马逊云.com/1.jpg', ... },
    ...
    ]
    }
    */


    // for 循环遍历 theme.wallpapers , 并通过 fetch 请求 url,将其写进 template/static/wallpapers 文件夹中
    theme.wallpapers.map((item) => {
    const response = await fetch(item.url);
    const buffer = new Uint8Array(await response.arrayBuffer());
    await fs.writeFile(`template/wallpapers/${fileName}`, buffer);
    })

    // ... 还有其他一些处理

    // 将 template 压缩成 zip 文件,发送给前端
    }

    思考 ing ...


    1 利用图片可以被浏览器缓存


    当一次下载主题从请求亚马逊云的图片数据,这步没有问题。 但是当重复下载的时候,之前下载过的图片又会再次下载,操作人员每次都需要等个十几秒,这就不太友好了。这部分时间花费还是挺多的。


    可以利用下浏览器能够将图片缓存到 disk cache 中的特点,将这部分的代码逻辑放到前端完成,因为还需要对压缩包中的文件做一些处理,因此需要借助下 jszip 这个库。


    看下改后的代码



    onDownload () {
    // 请求拿到 theme 数据
    const theme = api.getTheme()
    const template = api.getTemplate() // Blob

    const zip = new JSZip()
    await zip.loadAsync(getTmplResp) // 读取 template.zip 文件数据

    console.time('handle images')
    const wallpaperList = theme.wallpapers
    for (const wallpaper of wallpaperList) {
    const response = await fetch(wallpaper.url) // 请求图片数据
    const buffer = new Uint8Array(await response.arrayBuffer())
    const fileName = wallpaper.url.split('/').pop()
    zip.file(`static/wallpapers/${fileName}`, buffer, { binary: true }) // 写进压缩包
    }
    console.timeEnd('handle images') // 统计用时

    // 还需要读取 template.zip 中的 config.json, 然后修改,重新保存到 template.zip 中
    ...

    // 导出 template.zip
    zip.generateAsync({ type: 'base64' }).then(
    (base64) => {
    const link = document.createElement('a')
    link.href = 'data:application/zip;base64,' + base64
    link.download = 'template.zip'
    link.target = '_blank'
    link.style.display = 'none'

    document.body.appendChild(link)
    link.click()
    document.body.removeChild(link)
    },
    (err) => {
    console.log('打包失败', err)
    }
    )
    }

    优化完成


    当第一次下载时,handle images 步骤耗时 20 - 21 s,流程和后端差不多。


    当第二次下载时,handle images 步骤耗时 0.35s - 0.45 s。会直接读取 disk cache 中的图片数据,50 ms 内就完成了。


    速度快了 57 倍有余!!!, 你还能想到其他优化方式吗?继续往后看 🍒


    第一次请求各个图片耗时
    image.png


    第二次请求各个图片耗时
    image.png


    2 并发请求


    我们都知道,浏览器会为每个域名维持 6 个 TCP 链接(再拓展还有域名分片知识),我们是否可以利用这个特点做些什么?


    答案是:并发上传


    通过上面的代码,可以看到,每个图片请求都是串行的,一个图片请求完了再进行下一个图片请求。我们一次请求 4 个图片,这样就更快了。


    首先写一个能够管理并发任务的类


    export class TaskQueue {
    public queue: {
    task: <T>() => Promise<T>
    resolve: (value: unknown) => void
    reject: (reason?: any) => void
    }[]
    public runningCount: number // 正在执行的任务数量
    public tasksResloved?: (value: unknown) => void
    public tasksRejected?: (reason?: any) => void

    public constructor(public maxConcurrency: number = 4) { // 最多同时执行 4 个任务
    this.queue = [] // 任务队列
    this.runningCount = 0
    }

    // 添加任务
    public addTask(task) {
    return new Promise((resolve, reject) => {
    this.queue.push({ task, resolve, reject })
    })
    }

    // 执行
    public run() {
    return new Promise((resoved, rejected) => {
    this.tasksResloved = resoved
    this.tasksRejected = rejected
    this.nextTask()
    })
    }

    private nextTask() {
    if (this.queue.length === 0 && this.runningCount === 0) {
    this.tasksResloved?.('done')
    return
    }

    // 如果任务队列中还有任务, 并且没有到最大执行任务数,就继续取出任务执行
    while (this.queue.length > 0 && this.runningCount < this.maxConcurrency) {
    const { task, resolve, reject } = this.queue.shift()
    this.runningCount++
    task()
    .then((res) => {
    this.runningCount--
    resolve(res)
    this.nextTask()
    })
    .catch((e) => {
    this.runningCount--
    reject(e)
    this.nextTask()
    })
    }
    }
    }


    改造代码


    onDownload () {
    // 请求拿到 theme 数据
    const theme = api.getTheme()
    const template = api.getTemplate() // Blob

    const zip = new JSZip()
    await zip.loadAsync(getTmplResp) // 读取 template.zip 文件数据

    console.time('handle images')
    const wallpaperList = theme.wallpapers

    // 注释之前的逻辑
    // for (const wallpaper of wallpaperList) {
    // const response = await fetch(wallpaper.url)
    // const buffer = new Uint8Array(await response.arrayBuffer())
    // const fileName = wallpaper.url.split('/').pop()
    // zip.file(`static/wallpapers/${fileName}`, buffer, { binary: true })
    // }

    const taskQueue = new TaskQueue() // 新建任务队列,默认同时执行 4 个
    for (const wallpaper of wallpaperList) {
    taskQueue
    .addTask(() => fetch(wallpaper.url)) // 添加任务
    .then(async (res) => { // 任务执行完后的回调
    const buffer = new Uint8Array(await (res as Response).arrayBuffer())
    const fileName = wallpaper.url.split('/').pop()
    zip.file(`static/wallpapers/${fileName}`, buffer, { binary: true })
    })
    .catch((e) => console.log('壁纸获取失败', e))
    }
    await taskQueue.run() // 等待所有图片都拿到
    console.timeEnd('handle images') // 统计用时

    // 还需要读取 template.zip 中的 config.json, 然后修改,重新保存到 template.zip 中
    ...

    // 导出 template.zip
    zip.generateAsync({ type: 'base64' }).then(
    (base64) => {
    const link = document.createElement('a')
    link.href = 'data:application/zip;base64,' + base64
    link.download = 'template.zip'
    link.target = '_blank'
    link.style.display = 'none'

    document.body.appendChild(link)
    link.click()
    document.body.removeChild(link)
    },
    (err) => {
    console.log('打包失败', err)
    }
    )
    }

    大功告成!


    当第一次下载时,handle images 步骤耗时 7 s,速度是之前的 3 倍。


    当第二次下载时,handle images 步骤耗时 0.25s,速度是之前的 1.4 - 1.8


    3 更多的可能


    越来越感觉到计算机网络的重要性, 还有未实现的优化方式:



    1. 域名分片,更多的并发(也有劣势 ,比如 每个域都需要额外的 DNS 查找成本以及建立每个 TCP 连接的开销, TCP 慢启动带宽利用不足)

    2. 升级 HTTP2 这不是靠前端一人能够完成的


    如果学到了新知识,麻烦点个
    作者:marh
    来源:juejin.cn/post/7267418197746270271
    👍 和 ⭐

    收起阅读 »

    公司没钱了,工资发不出来,作为员工怎么办?

    公司没钱了,工资发不出来,作为员工怎么办? 现在大环境不好,很多公司都会遇到一些困难。如果公司真的有现金流还好,如果没有,还等着客户回款。那么就有点难办了。很多公司会采取延期发工资,先发80%、50%工资这样的操作。 员工遇到这种情况,无非以下几种选择。 认...
    继续阅读 »

    公司没钱了,工资发不出来,作为员工怎么办?


    现在大环境不好,很多公司都会遇到一些困难。如果公司真的有现金流还好,如果没有,还等着客户回款。那么就有点难办了。很多公司会采取延期发工资,先发80%、50%工资这样的操作。


    员工遇到这种情况,无非以下几种选择。



    1. 认同公司的决策,愿意跟公司共同进退。

    2. 不认同公司的决策,我要离职。

    3. 不认同公司的决策,但感觉自己反对也没用。所以嘴上先答应,事后会准备去找新的工作机会。

    4. 不认同公司的决策,我也不主动离职。准备跟公司battle,”你们这么做是不合法滴“


    你可以代入这个场景看看自己是哪一类。首先由于每个人遇到的真实情况不一样,所以所有的选择只有适合自己的,并没有对错之分。


    我自己的应对思路是,抛开存量,看增量。存量就是我在公司多少多少年了,公司开除我要给我N+1的补偿。公司之前对我特别特别好,老板对我有知遇之恩等等。你就当做自己已经不在公司了,现在公司给你发了offer,现在的你是否愿意接受公司开给你的条件?如果愿意,那么你可以选择方案一。如果不愿意,那么你就可以选择方案三。现在这环境,骑驴找马,否则离职后还要自己交社保。还不如先苟着。


    为什么不选择方案四?因为不值得,如果公司属于违法操作,你先苟着,后面离职后还是可以找劳动局仲裁的。这样既不耽误你换工作,也不耽误你要赔偿。如果公司是正规操作,那么闹腾也没用,白浪费自己的时间。


    离职赔偿还是比较清晰明确的,如果是散伙那可能会牵扯到更多利益。我自己的经验是,不能什么都想着要。当最优解挺难获得的时候,拿个次优解也可以。当然,不管你选择的哪个,我都有一个建议。那就是当一天和尚,敲一天钟。在职期间,还是要把事情干好的,用心并不全是为了公司,更多是为了自己。人生最大的投资是投资自己的工作和事业,浪费时间就是浪费生命。哪怕公司没有事情安排给你做,也要学会自己找事情做。


    如果公司后面没钱了,欠的工资还拿得到吗?


    我们作为员工是很难知道公司财务状况的,所以出了这样的事就直接去仲裁,最好是跟同事凑齐十个人去,据说会优先处理。公司如果还要做生意,一般会在仲裁前选择和解,大概是分几个月归还欠款。如果公司不管,那么仲裁后,会冻结公司公账。但有没有钱就看情况了。


    如果公司账上没钱且股东已经实缴了股本金,那么公司是可以直接破产清算的。公司破产得话,基本上欠员工的钱就没有了。如果没有实缴,那么

    作者:石云升
    来源:juejin.cn/post/7156242740034928671
    股东还需要按照股份比例偿还债务。

    收起阅读 »

    差点让我崩溃的“全选”功能

    web
    今天在实现电商结算功能的时候遇到了个全选功能,做之前不以为然,做之后抓狂。为了让自己全身心投入到对这个问题的攻克,我特意将其拿出来先创建一个demo单独实现下。不搞不知道,一搞吓一跳,下面是我demo在浏览器运行的截图: 开始,我是这样写代码的: f...
    继续阅读 »

    今天在实现电商结算功能的时候遇到了个全选功能,做之前不以为然,做之后抓狂。为了让自己全身心投入到对这个问题的攻克,我特意将其拿出来先创建一个demo单独实现下。不搞不知道,一搞吓一跳,下面是我demo在浏览器运行的截图:


    1679229898519.png
    开始,我是这样写代码的:


        for (let i = 0; i < aCheckbox.length; i++) {
    aCheckbox[i].checked = this.checked;
    }
    });

    for (let i = 0; i < aCheckbox.length; i++) {
    aCheckbox[i].addEventListener("click", function () {
    for (let index = 0; index < aCheckbox.length; index++) {
    if (aCheckbox[index].checked) {
    oAllchecked.checked = aCheckbox[index].checked;
    } else {
    oAllchecked.checked = !aCheckbox[index].checked;
    }
    }
    });
    }

    点击全选这个功能不难,主要问题出现在如何保证另外两个复选框在其中一个没有选中的情况下,全选的这个复选框没有选中。苦思良久,最后通过查找资料看到了如今的代码:


        aCheckbox[i].addEventListener("click", function () {
    let flag = true;
    for (let index = 0; index < aCheckbox.length; index++) {
    console.log(aCheckbox[index].checked);
    if (!aCheckbox[index].checked) {
    flag = false;
    break;
    }
    }
    oAllchecked.checked = flag;
    });
    }

    功能完美就解决,第一个代码问题的原因是‘aCheckbox[index].checked’这个判断不能解决两个复选框什么时候一个选中一个没选中的问题。这个问题不解决也就不能让全选复选框及时更新正确的选中状态了。


    而下面这个代码通过设置一个中间值flag,及时记录每个复选框按钮的选中状态,能准确的赋值给全选功能的复

    作者:一个对前端不离不弃的中年菜鸟
    来源:juejin.cn/post/7212942861518864421
    选框按钮。于是这个需求就解决了~

    收起阅读 »

    前端使用a链接下载内容增加loading效果

    web
    问题描述:最近工作中出现一个需求,纯前端下载 Excel 数据,并且有的下载内容很多,这时需要给下载增加一个 loading 效果。 代码如下: // utils.js const XLSX = require('xlsx') // 将一个sheet转成最终...
    继续阅读 »

    1. 问题描述:最近工作中出现一个需求,纯前端下载 Excel 数据,并且有的下载内容很多,这时需要给下载增加一个 loading 效果。

    2. 代码如下:


    // utils.js
    const XLSX = require('xlsx')
    // 将一个sheet转成最终的excel文件的blob对象,然后利用URL.createObjectURL下载
    export const sheet2blob = (sheet, sheetName) => {
    sheetName = sheetName || 'sheet1'
    var workbook = {
    SheetNames: [sheetName],
    Sheets: {}
    }
    workbook.Sheets[sheetName] = sheet
    // 生成excel的配置项
    var wopts = {
    bookType: 'xlsx', // 要生成的文件类型
    bookSST: false, // 是否生成Shared String Table,官方解释是,如果开启生成速度会下降,但在低版本IOS设备上有更好的兼容性
    type: 'binary'
    }
    var wbout = XLSX.write(workbook, wopts)
    var blob = new Blob([s2ab(wbout)], { type: 'application/octet-stream' })
    // 字符串转ArrayBuffer
    function s2ab(s) {
    var buf = new ArrayBuffer(s.length)
    var view = new Uint8Array(buf)
    for (var i = 0; i !== s.length; ++i) view[i] = s.charCodeAt(i) & 0xff
    return buf
    }
    return blob
    }

    /**
    * 通用的打开下载对话框方法,没有测试过具体兼容性
    * @param url 下载地址,也可以是一个blob对象,必选
    * @param saveName 保存文件名,可选
    */

    export const openDownloadDialog = (url, saveName) => {
    if (typeof url === 'object' && url instanceof Blob) {
    url = URL.createObjectURL(url) // 创建blob地址
    }
    var aLink = document.createElement('a')
    aLink.href = url
    aLink.download = saveName + '.xlsx' || '1.xlsx' // HTML5新增的属性,指定保存文件名,可以不要后缀,注意,file:///模式下不会生效
    var event
    if (window.MouseEvent) event = new MouseEvent('click')
    else {
    event = document.createEvent('MouseEvents')
    event.initMouseEvent(
    'click',
    true,
    false,
    window,
    0,
    0,
    0,
    0,
    0,
    false,
    false,
    false,
    false,
    0,
    null
    )
    }
    aLink.dispatchEvent(event)
    }

    <el-button
    @click="clickExportBtn"
    >
    <i class="el-icon-download"></i>下载数据
    </el-button>
    <div class="mongolia" v-if="loadingSummaryData">
    <el-icon class="el-icon-loading loading-icon">
    <Loading />
    </el-icon>
    <p>loading...</p>
    </div>


    clickExportBtn: _.throttle(async function() {
    const downloadDatas = []
    const summaryDataForDownloads = this.optimizeHPPCDownload(this.summaryDataForDownloads)
    summaryDataForDownloads.map(summaryItem =>
    downloadDatas.push(this.parseSummaryDataToBlobData(summaryItem))
    )
    // donwloadDatas 数组是一个三维数组,而 json2sheet 需要的数据是一个二维数组
    this.loadingSummaryData = true
    const downloadBlob = aoa2sheet(downloadDatas.flat(1))
    openDownloadDialog(downloadBlob, `${this.testItem}报告数据`)
    this.loadingSummaryData = false
    }, 2000),

    // css
    .mongolia {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background-color: rgba(0, 0, 0, 0.9);
    display: flex;
    justify-content: center;
    align-items: center;
    font-size: 1.5rem;
    color: #409eff;
    z-index: 9999;
    }
    .loading-icon {
    color: #409eff;
    font-size: 32px;
    }


    1. 解决方案探究:




    • 在尝试了使用 $nextTick、将 openDownloadDialog 改写成 Promise 异步函数,或者使用 async/await、在 openDownloadDialog 中添加 loadingSummaryData 逻辑,发现依旧无法解决问题,因此怀疑是 document 添加新元素与 vue 的 v-if 渲染产生冲突,即 document 添加新元素会阻塞 v-if 的执性。查阅资料发现,问题可能有以下几种:



      • openDownloadDialog 在执行过程中执行了较为耗时的同步操作,阻塞了主线程,导致了页面渲染的停滞。

      • openDownloadDialog 的 click 事件出发逻辑存在问题,阻塞了事件循环(Event Loop)。

      • 浏览器在执行 openDownloadDialog 时,将其脚本任务的优先级设置得较高,导致占用主线程时间片,推迟了其他渲染任务。

      • Vue 的批量更新策略导致了 v-if 内容的显示被延迟。




    • 查阅资料后找到了如下几种方案:





        1. 使用 setTimeout 使 openDownloadDialog 异步执行


        clickExport() {
        this.loadingSummaryData = true;

        setTimeout(() => {
        openDownloadDialog(downloadBlob, `${this.testItem}报告数据`);

        this.loadingSummaryData = false;
        });
        }




        1. 对 openDownloadDialog 内部进行优化



        • 避免大循环或递归逻辑

        • 将计算工作分批进行

        • 使用 Web Worker 隔离耗时任务


          • 在编写 downloadWorker.js 中的代码时,要明确这部分代码是运行在一个独立的 Worker 线程内部,而不是主线程中。





              1. 不要直接依赖或者访问主线程的全局对象,比如 window、document 等。这些在 Worker 内都无法直接使用。





              1. 不要依赖 DOM 操作,比如获取某个 DOM 元素。Worker 线程无法访问页面的 DOM。





              1. 代码执行的入口是 onmessage 回调函数,在其中编写业务逻辑。





              1. 和主线程的通信只能通过 postMessage 和 onmessage 发送消息事件。





              1. 代码应该是自包含的,不依赖外部变量或状态。





              1. 可以导入其他脚本依赖,比如用 import 引入工具函数等。





              1. 避免修改或依赖全局作用域,比如定义全局变量等。






          • 总之,编写 downloadWorker.js 时要注意它执行在一个独立环境中,只能通过消息事件与主线程通信。


            // 创建 Worker
            const worker = new Worker('downloadWorker.js');

            // 点击下载时向 Worker 发送消息
            function clickDownload() {

            showLoading();

            worker.postMessage({
            url: fileURL,
            filename: 'report.xlsx'
            });

            worker.onmessage = function(e) {
            // 收到下载完成的消息
            hideLoading();
            }

            }

            // 显示 loading
            function showLoading() {
            loading.style.display = 'block';
            }

            // 隐藏 loading
            function hideLoading() {
            loading.style.display = 'none';
            }

            // downloadWorker.js

            onmessage = function(e) {
            const url = e.data.url;
            const filename = e.data.filename;

            // 创建并点击链接触发下载
            const a = document.createElement('a');
            a.href = url;
            a.download = filename;
            a.click();

            postMessage('下载完成');
            }

            <div id="loading" style="display:none;">
            Downloading...
            </div>

            <button onclick="clickDownload()">
            Download
            </button>

            <script src="downloadWorker.js"></script>








        1. 使用 requestIdleCallback 进行调度


        clickExport() {
        this.loadingSummaryData = true;

        requestIdleCallback(() => {
        openDownloadDialog(downloadBlob, `${this.testItem}报告数据`);

        this.loadingSummaryData = false;
        });
        }



      • 当使用上述方案时发现里面的内容变得异常的慢,于是去问GPT后,得到以下答案:


        使用 requestIdleCallback 来执行 openDownloadDialog 确实可能会需要更多时间。


        因为 requestIdleCallback 会在主线程空闲时才执行回调函数,所以不像 setTimeout 可以直接按指定时间异步执行。


        requestIdleCallback 的优先级较低,只在主线程空闲时才会执行,所以 download 的速度会比 setTimeout 慢一些。


        但使用 requestIdleCallback 的优点是它不会占用主线程时间,可以避免阻塞页面渲染。


        所以需要权衡执行速度和避免阻塞之间的关系:




        • 如果下载速度是优先考虑的因素,可以继续使用 setTimeout 异步执行。




        • 如果避免阻塞渲染更重要,则 requestIdleCallback 是更好的选择。




        偶尔下载任务被延迟不是大问题,但频繁的下载任务阻塞会严重影响页面体验。


        此外,可以结合两者试试:


        clickExport() {

        this.loadingSummaryData = true;

        setTimeout(() => {

        requestIdleCallback(() => {
        openDownloadDialog(downloadBlob);
        });

        this.loadingSummaryData = false;

        }, 200);

        }

        setTimeout 先异步调度,然后在回调中使用 requestIdleCallback 避免阻塞。这样就可以兼顾下载速度和避免渲染阻塞了。





        1. 分析线程模型,找到具体的阻塞点



        • 使用 Performance 工具分析线程

        • debugger 及 console.log 打印关键函数的执行时间

        • 检查是否有非主线程的任务阻塞了主线程





        1. 调整 vue 的批量更新策略


        new Vue({
        config: {
        // 修改批量更新的队列长度
        batchUpdateDuration: 100
        }
        })

        作者:李卫泽
        来源:juejin.cn/post/7268050036474609683
        i>


    收起阅读 »

    const声明的变量还能修改?原理都在这了

    web
    前言 const关键字用于声明只读的常量,一旦被赋值,其值就不能再被改变。但是,这并不意味着该变量持有的值是不可变的。 本文将结合案例逐步探讨JavaScript中const声明的工作原理,解释为何这些看似常量的变量却可以被修改,并探讨其实际应用。 耐心看完,...
    继续阅读 »

    前言


    const关键字用于声明只读的常量,一旦被赋值,其值就不能再被改变。但是,这并不意味着该变量持有的值是不可变的。


    本文将结合案例逐步探讨JavaScript中const声明的工作原理,解释为何这些看似常量的变量却可以被修改,并探讨其实际应用。


    耐心看完,你一定有所收获。


    giphy.gif


    正文


    const关键字用于声明一个变量,该变量的值在其生命周期中不会被重新赋值。


    现象


    1. 基本数据类型


    对于基本数据类型(如数字、字符串、布尔值),const确保变量的值不会改变。


    const num = 42;
    // num = 43; // 这会抛出错误

    2. 对象


    对于对象,仍然可以修改对象的属性,但不能重新赋值整个对象。


    const girlfriend = {
    name: "小宝贝"
    };

    girlfriend.name = "亲爱的"; // 这是允许的,因为你只是修改了对象的一个属性

    // girlfriend = { name: "亲爱的" }; // 这会抛出错误,因为你试图改变obj的引用

    假如你有个女朋友,也许并没有,但我们可以假设,她的名字或者你平时叫她的昵称是"小宝贝"。


    有一天,你心血来潮,想换个方式叫她,于是叫她"亲爱的"。这完全没问题,因为你只是给她换了个昵称,她本人并没有变。


    但是,如果有一天你看到另一个女生,你却说:“哎,这不是亲爱的吗?”这就出大问题了!因为你把一个完全不同的人当成了你的女朋友。


    这就像你试图改变girlfriend的引用,把它指向了一个新的对象。


    JavaScript不允许这样做,因为你之前已经明确地告诉它,girlfriend就是那个你叫"小宝贝"的女朋友,你不能突然把另一个人说成她。


    154eb98c10eaf8356b5da0e44b9e9fe6.gif


    简单来说,你可以随时给你的女朋友起个新昵称,但你不能随便把别的女生当成你的女朋友。


    3. 数组


    对于数组,你可以修改、添加或删除元素,但不能重新赋值整个数组。


    const arr = [1, 2, 3];

    arr[0] = 4; // 这是允许的,因为你只是修改了数组的一个元素
    arr.push(5); // 这也是允许的,因为你只是向数组添加了一个元素

    // arr = [6, 7, 8]; // 这会抛出一个错误,因为你试图改变arr的引用

    假设arr是你的超市购物袋,里面有三个苹果,分别标记为1、2和3。


    你检查了第一个苹果,觉得它不够新鲜,所以你把它替换成了一个新的苹果,标记为4。这就像你修改数组的一个元素。这完全可以,因为你只是替换了袋子里的一个苹果。


    后来,你决定再放一个苹果进去,标记为5。这也没问题,因为你只是向袋子里添加了一个苹果。


    苹果再变,袋子仍然是原来的袋子。


    但是,当你试图拿个新的装着6、7、8的购物袋来代替你原来的袋子时,就不对了。你不能拿了一袋子苹果,又扔在那不管,反而又去拿了一袋新的苹果。


    你礼貌吗?


    f2e0de05371993107839d315b5639a30.jpg


    你可以随时替换袋里的苹果或者放更多的苹果进去,但你不能拿了一袋不要了又拿一袋。


    原理


    在JavaScript中,const并不是让变量的值变得不可变,而是让变量指向的内存地址不可变。换句话说,使用const声明的变量不能被重新赋值,但是其所指向的内存中的数据是可以被修改的。


    使用const后,实际上是确保该变量的引用地址不变,而不是其内容。


    结合上面两个案例,女朋友和购物袋就好比是内存地址,女朋友的外号可以改,但女朋友是换不了的,同理袋里装的东西可以换,但袋子仍然是那个袋子。


    当使用const声明一个变量并赋值为一个对象或数组,这个变量实际上存储的是这个对象或数组在内存中的地址,形如0x00ABCDEF(这只是一个示例地址,实际地址会有所不同),而不是它的内容。这就是为什么我们说变量“引用”了这个对象或数组。


    实际应用


    这种看似矛盾的特性实际上在开发中经常用到。


    例如,在开发过程中,可能希望保持一个对象的引用不变,同时允许修改对象的属性。这可以通过使用const来实现。


    考虑以下示例:


    假设你正在开发一个应用,该应用允许用户自定义一些配置设置。当用户首次登录时,你可能会为他们提供一组默认的配置。但随着时间的推移,用户可能会更改某些配置。


    // 默认配置
    const userSettings = {
    theme: "light", // 主题颜色
    notifications: true, // 是否开启通知
    language: "en" // 默认语言
    };

    // 在某个时间点,用户决定更改主题颜色和语言
    function updateUserSettings(newTheme, newLanguage) {
    userSettings.theme = newTheme;
    userSettings.language = newLanguage;
    }

    // 用户调用函数,将主题更改为"dark",语言更改为"zh"
    updateUserSettings("dark", "zh");

    console.log(userSettings); // 输出:{ theme: "dark", notifications: true, language: "zh" }

    在这个例子中,我们首先定义了一个userSettings对象,它包含了用户的默认配置。尽管我们使用const来声明这个对象,但我们仍然可以随后更改其属性来反映用户的新配置。


    这种模式在实际开发中很有用,因为它允许我们确保userSettings始终指向同一个对象(即我们不会意外地将其指向另一个对象),同时还能够灵活地更新该对象的内容以反映用户的选择。


    为什么不用let


    以上所以案例中,使用let都是可行,但它的语义和用途相对不同,主要从这几个方面进行考虑:



    1. 不变性:使用const声明的变量意味着你不打算重新为该变量赋值。这为其他开发人员提供了一个明确的信号,即该变量的引用不会改变。在上述例子中,我们不打算将userSettings重新赋值为另一个对象,我们只是修改其属性。因此,使用const可以更好地传达这一意图。

    2. 错误预防:使用const可以防止意外地重新赋值给变量。如果你试图为const变量重新赋值,JavaScript会抛出错误。这可以帮助捕获潜在的错误,特别是在大型项目或团队合作中。

    3. 代码清晰度:对于那些只读取和修改对象属性而不重新赋值的场景,使用const可以提高代码的清晰度,可以提醒看到这段代码的人:“这个变量的引用是不变的,但其内容可能会变。”


    一般我们默认使用const,除非确定需要重新赋值,这时再考虑使用let。这种方法旨在鼓励不变性,并使代码更加可预测和易于维护。


    避免修改


    如果我们想要避免修改const声明的变量,当然也是可以的。


    例如,我们可以使用浅拷贝来创建一个具有相同内容的新对象或数组,从而避免直接修改原始对象或数组。这可以通过以下方式实现:


    const originalArray = [1, 2, 3];
    const newArray = [...originalArray]; // 创建一个原始数组的浅拷贝
    newArray.push(4); // 不会影响原始数组
    console.log(originalArray); // 输出: [1, 2, 3]
    console.log(newArray); // 输出: [1, 2, 3, 4]

    总结


    const声明的变量之所以看似可以被修改,是因为const限制的是变量指向的内存地址的改变,而不是内存中数据的改变。这种特性在实际开发中有其应用场景,允许我们保持引用不变,同时修改数据内容。


    然而,如果我们确实需要避免修改数据内容,可以采取适当的措施,如浅拷贝。


    9a9f1473841eca9a3e5d7e1408145a4b.gif

    收起阅读 »

    好烦啊,为什么点个链接还让我确认一下?

    web
    万丈苍穹水更深,无限乾坤尽眼中 背景 最近经常看到各大SNS平台有这样一项功能,点击跳转链接,如果不是本站的链接,那么就会跳转到一个中转页,告诉你跳转了一个非本站链接,提醒你注意账号财产安全,如图: 很明显,这个是为了一定程度的避免用户被钓鱼,预防XSS或...
    继续阅读 »

    万丈苍穹水更深,无限乾坤尽眼中



    背景


    最近经常看到各大SNS平台有这样一项功能,点击跳转链接,如果不是本站的链接,那么就会跳转到一个中转页,告诉你跳转了一个非本站链接,提醒你注意账号财产安全,如图:


    A6C73047-4041-4584-9F97-BA04C896D73E.png


    很明显,这个是为了一定程度的避免用户被钓鱼,预防XSS或CRSF攻击,所以请不要像标题一样抱怨,多点一下也花不了2S时间。


    原理


    那么这个是如何实现的呢,原理其实很简单。


    a标签的onclick事件可以被拦截,当返回false时不会默认跳转。


    那么具体如何实现呢,拿掘金来举例:


            function SetSafeA(whiteDomList: string[], safeLink = 'https://link.juejin.cn/?target=') {
              const aArr = document.getElementsByTagName('a')
              Array.from(aArr).forEach(item=>{
                item.onclick = ()  => {
                  let target = item.getAttribute('href')!
                  if(/^\//.test(target)) {
                    // 相对本站链接
                    return true
                  }
                 const isSafe = undefined !==  whiteDomList.find(item=>{
                     return target.indexOf(item) !== -1
                  })
                  if(!isSafe) {
                    window.open(`${safeLink}${target}`, '_blank')
                  } else {
    return true
    }
                  return false
                }
              })
            }

    可以随便找一个网页在控制台执行一下,都能跳到掘金的中转页,中转页的代码就不写了^_^


    实践


    刚好最近遇到一个使用场景,公司APP产品里面都有各自用户协议,其中SDK协议我们都是直接跳转链接的,结果在部分渠道如小天才,步步高等对用户信息非常敏感的平台上,要求所有的链接必须要跳转到平台默认的安全浏览器上,不能在APP内打开。那么协议有很多如何快速处理呢。由于项目用到了vue,这里就想到使用指令,通过批量添加指令来达到快速替换,比如'<a' =>'<a v-link="x"',代码如下:


    Vue.directive('outlink', {
      bind: (el, binding) => {
        el.outlink = () => {
          if (GetEnv() === 'app') {
            const from = isNaN(+binding.value) ? 1 : +binding.value
            const url = el.getAttribute('href')
            if (url && url !== '' && url != 'javascript:;') {
              window.location.href = `${GetSchemeByFrom(from)}://outside_webview?url=${url}`
            }
            return false
          }
        }
        el.onclick = el.outlink
      },
      unbind: (el) => {
        el.onclick = null
        delete el.outlink
      }
    })

    这里我们传入了from值来区分APP平台,然后调用APP提供的相应scheme跳转到客户端的默认浏览器,如下:


    DE2DFA5F-ED19-4e74-97B6-2D19246D5D84.png


    结语


    链接拦截可以做好事,也可以做一些hack,希望使用的人保持一颗爱好和平的心;当然遇到让你确认安全的

    作者:CodePlayer
    来源:juejin.cn/post/7161712791089315877
    链接时,也请你保持一颗感谢的心。

    收起阅读 »

    兄弟,不要试图在业务代码中炫技。

    你好呀,我是歪歪。 最近项目迭代非常密集,导致组里面的同事都在同一个微服务里面进行不同需求的迭代开发。 由于我们的代码提交规则规定,提交代码必须有一个 review 环节,所以有时候我会去仔细看同事提交的代码,但是有一说一,绝大部分情况下我没有仔细的去看,只是...
    继续阅读 »

    你好呀,我是歪歪。


    最近项目迭代非常密集,导致组里面的同事都在同一个微服务里面进行不同需求的迭代开发。


    由于我们的代码提交规则规定,提交代码必须有一个 review 环节,所以有时候我会去仔细看同事提交的代码,但是有一说一,绝大部分情况下我没有仔细的去看,只是草草的瞟上几眼,就点击了通过。


    其实我之前也会非常仔细的去看的,但是不得不说这个 review 的过程真的会占据比较多的时间,在需求不密集的时候做起来没有问题。


    但是一旦任务在手上堆起来了,就很难去仔细 review 别人的代码了,分身乏术。


    去年有一段时间就是忙的飞起,多线程并发需求迭代,别人提交代码之后,我就是无脑点通过。


    我并没有核对最终落地的代码和我最初的设计方案是否匹配,而且由于代码不是我开发的,我甚至没有看过,等出了问题,排查问题的时候我再去找代码,就发现根本不知道写在哪里的。


    方案设计和代码落地之间的断层,这样带来的一个后果就是我后期完全失去了对服务的掌握。


    每天都担心,生怕出线上问题。但是每天也不知道哪个地方会出现问题,就很恼火。


    对每一次我点进 review 通过的代码负责,这是我写进年度计划的一句话。


    所以,今年为了避免这个现象的再次出现,在同事对一个完整的功能点提交之后,即使再忙,我自己会花时间仔细去 review 一次对应的代码,然后拿着我看完之后记录的问题再去找对应的同事给我答疑,确保我们至少在业务逻辑的理解上是一致的。


    通过这个方式,我又重新掌握了主动权。


    在这个过程中还暴露出一个问题,各个开发同事的编码风格各异,经常可以闻到一些代码的“坏味道”。


    比如,我见过一个新增操作,所有的逻辑都在一个 controller 里面,没有所谓的 biz 层、service 层、dao 层,一把梭直接把 mapper 注入到了 controller 里面,在一个方法里面从数据校验到数据库交互全部包圆了。


    功能能用吗?


    能用。


    但是我们常常提到的一个词是“技术含量”。


    这样代码是有“技术含量”的代码吗?


    我觉得可以说是毫无技术含量了,就是偷懒了,觉得怎么方便就怎么来了。


    那如果我要基于对于这一段代码继续开发新功能,我能做什么呢?


    我无能为力,原来的代码实在不想去动。


    我只能保证在这堆“屎山”上,我新写出来的代码是干净的、清晰的,不继续往里面扔垃圾。


    我读过一本书叫做《代码整洁之道》,里面有一个规则叫做“童子军军规”。


    军规中有一句话是这样的:让营地比你来时更干净。


    类比到代码上其实就是一件很小的事情,比如只是改好一个变量名、拆分一个有点过长的函数、消除一点点重复代码,清理一个嵌套 if 语句...


    这是让项目代码随着时间流逝而越变越好的最简单的做法,持续改进也是专业性的内在组成部分。


    我觉得我对于这一点“规则”落实的还是挺好的,看到一些不是我写的,但是我觉得可以有更好的写法时,而且改动起来非常简单,不影响核心功能的时候,我会主动去改一下。


    我能保证的是,这段代码在经过我之后,我没有让它更加混乱。


    把一段混乱的代码,拆分的清晰起来,再后来的人愿意按照你的结构继续往下写,或者继续改进。


    你说这是在写“有技术含量”的代码吗?


    我觉得不是。


    但是,我觉得这应该是在追求写“有技术含量”的代码之前,必须要具备的一个能力。而且是比写出“有技术含量”的代码更加重要的一个基础能力。


    先不说代码优雅的事儿了,至少得让代码整体看起来不混乱。


    一个人维护一个项目,想要把代码搞优雅是一件很简单的事情,但是如果是好几个人一起维护就有点不好做了。


    只有大家相互磨合,最后慢慢的形成好的、较为统一风格。


    所以我最近也是持续在找一些关于代码风格、代码规范、代码重构这方面的好的资料在组分享,总是能慢慢有所改变的。


    比如这周,我就找到了“京东云开发者”的一篇文章:



    《让代码优雅起来:记一次代码微重构实践 | 京东云技术团队》

    juejin.cn/post/725714…



    在这篇文章里面,作者给到了一个完整的关于代码重构的示例。


    把一个功能代码,从这样冗长臃肿的代码:



    最终拆分为了三个类,每个类各司其职。


    这个类只是负责组装对象:



    金额计算拆分到了枚举类里面去:




    这才是符合面向对象编程的思想。


    这部分代码具体是干啥的,以及重构前后的代码是怎么样的,如果你感兴趣可以自己打开链接看一下。


    我这边主要还是赞同作者的一个观点:不要觉得重构前的代码每次修改也就肉眼可见的几个地方,没必要在这上面花费时间。


    其实我觉得还是很有必要的,大家写代码的时候都想要追求技术含量,追求优雅性,这就是一个体现的地方,为什么不改呢?


    但是我还得补充一句,结合个人截至目前有限的职业生涯和工作经验来说,我有一点小小的体会:



    写业务代码,代码可读性的优先级甚至比代码写的优雅、写的有技术含量更高,且高的多。不要试图在业务代码中炫技。



    我前面分享的“记一次代码微重构实践”文章的最后也列举了两个引用的地方,我也放在这里,共勉之。


    软件工程中的“破窗效应”:



    破窗效应指的是在软件开发过程中,如果存在低质量的代码或设计,如果不及时修复,就会导致其他开发人员也采用同样的低质量方案。这会逐渐升级到更严重的问题,导致软件系统变得难以维护、扩展和改进。因此,在软件开发中,及时解决问题和保持代码质量非常重要,以避免破窗效应对于整个项目造成的负面影响。



    同时看看 Martin Fowler 在《重构:改善既有代码的设计》一书中对重构的部分解释:



    重构的每个步骤都很简单,甚至显得有些过于简单:你只需要把某个字段从一个类移到另一个类,把某些代码从一个函数拉出来构成另一个函数,或是在继承体系中把某些代码推上推下就行了。但是,聚沙成塔,这些小小的修改累积起来就可以根本改善设计质

    作者:why技术
    来源:juejin.cn/post/7268183236740317225
    量。


    收起阅读 »

    都什么年代了,还在用传统方式写代码?

    前言 还在把 AI 当作搜索引擎的替代品,有问题才问 AI,没问题就在那边吭哧吭哧地撸代码?如果是这样,那你真的 OUT了!现在正经人谁还自己一行行地写代码啊,都是 AI 生成的代码——没有 AI 我不写(手动滑稽)。 本文将分享 AI 时代的编程新实践,教你...
    继续阅读 »

    前言


    还在把 AI 当作搜索引擎的替代品,有问题才问 AI,没问题就在那边吭哧吭哧地撸代码?如果是这样,那你真的 OUT了!现在正经人谁还自己一行行地写代码啊,都是 AI 生成的代码——没有 AI 我不写(手动滑稽)。


    本文将分享 AI 时代的编程新实践,教你如何从一个 "Ctrl + C"、 "Ctrl + V" 工程师,变成一个 "Tab + Enter" 工程师🤣。


    开发流程


    软件的一般研发流程为:

    1. 需求分析
    2. 程序设计
    3. 代码编写
    4. 软件测试
    5. 部署上线

    我们在这里主要关心步骤2~4,因为与 AI 结合得比较紧密。虽然需求分析也可以借助 AI,但不是本文的重点,故不做讨论。


    程序设计


    经过需求分析、逻辑梳理后,在编写实际代码前,需要进行程序设计。


    此环节的产物是设计文档,是什么类型的设计文档不重要,重要的是伪代码的输出。


    虽然《Code Complete》早就推荐过伪代码的实践,但对此人们容易有一个误区:认为写伪代码花的时间,已经够把实际代码写好了。但 AIGC 时代,此问题可以轻松破解:AI 写代码的速度肯定比人快,因此,只要能找到方法能让 AI 生成符合需求的代码,就值得花时间去研究。而伪代码,就是让 AI 快速生成符合期望的实际代码的最好方式。


    为什么这么说呢?因为想要让 AIGC 符合期望,恰当的 Prompt 必不可少。但如何写好这个 Prompt,需要提供多少上下文,才能让 AI 更好地理解我们的意图,这是需要技巧、需要调试的。而经过精心设计的伪代码,本身已经提供了足够的上下文,且意图足够聚焦,减轻了对 Prompt 的要求,又提高了 AIGC 的成功率。


    我们来看一下伪代码示例:

    plainText = JSON.stringify(data)
    digest = hash(plainText) // 防篡改
    secret = Symmetric.getKey()
    cipherText = encryptText(data, secret) // 防内容泄密


    pair = Asymmetric.getPair()
    cipherSecret = encryptKey(secret, pair.getPrivateKey()) // 防密钥泄露

    return {
    digest,
    cipherText,
    cipherSecret,
    pair.getPublicKey(),
    }

    上述代码主要表达的内容是:

    1. 对初始数据进行加密
    2. 返回加密后的相关内容
    3. 涉及到了一系列的算法

    对于伪代码的编写,有如以建议:

    1. 不要纠结类型,不要局限于某一种编程语言的语法,不用试图写能编译通过的代码——这只会限制了你的表达
    2. 命名一定要准确,符合领域术语,这一点很关键。这一点可以通过查找资料、看书、问 AI 来实现。千万不要只是生硬地汉译英,不专业的表达会妨碍 AI 的理解

    代码编写


    代码编写分为以下几步:

    1. 把伪代码转换成目标语言可执行的代码
    2. 根据项目情况,对生成的代码进行改造
    3. 利用 AI 编码辅助工具编写剩余的代码

    生成真实代码


    让 AI 生成代码的 prompt 很简单,示例如下(根据目标语言进行修改):

    please transform the following pseudo code into practical java code ```
    your code```please transform the following pseudo code into practical java code ```
    your code```

    使用 chatGPT 结果截图:



    完整代码如下:

    import com.fasterxml.jackson.databind.ObjectMapper;

    import javax.crypto.Cipher;
    import javax.crypto.KeyGenerator;
    import javax.crypto.SecretKey;
    import java.security.*;

    public class EncryptionExample {

    public static void main(String[] args) throws Exception {
    // Prepare the license data
    LicenseData licenseData = new LicenseData();
    licenseData.setLicenseId("123");
    licenseData.setLicenseType("Standard");

    // Convert the license data to JSON string
    ObjectMapper objectMapper = new ObjectMapper();
    String plainText = objectMapper.writeValueAsString(licenseData);

    // Hash the plain text
    String digest = hash(plainText);

    // Generate a symmetric key
    SecretKey secretKey = generateSymmetricKey();

    // Encrypt the license data with the symmetric key
    byte[] cipherText = encryptText(plainText, secretKey);

    // Generate an asymmetric key pair
    KeyPair keyPair = generateAsymmetricKeyPair();

    // Encrypt the symmetric key with the private key
    byte[] cipherSecret = encryptKey(secretKey.getEncoded(), keyPair.getPrivate());

    // Print the results
    System.out.println("Plain Text: " + plainText);
    System.out.println("Digest: " + digest);
    System.out.println("Cipher Text: " + new String(cipherText));
    System.out.println("Cipher Secret: " + new String(cipherSecret));
    }

    public static String hash(String input) throws NoSuchAlgorithmException {
    MessageDigest digest = MessageDigest.getInstance("SHA-256");
    byte[] hash = digest.digest(input.getBytes());
    return bytesToHex(hash);
    }

    public static SecretKey generateSymmetricKey() throws NoSuchAlgorithmException {
    KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
    keyGenerator.init(128);
    return keyGenerator.generateKey();
    }

    public static byte[] encryptText(String data, SecretKey key) throws Exception {
    Cipher cipher = Cipher.getInstance("AES");
    cipher.init(Cipher.ENCRYPT_MODE, key);
    return cipher.doFinal(data.getBytes());
    }

    public static byte[] encryptKey(byte[] secret, PrivateKey privateKey) throws Exception {
    Cipher cipher = Cipher.getInstance("RSA");
    cipher.init(Cipher.ENCRYPT_MODE, privateKey);
    return cipher.doFinal(secret);
    }

    public static KeyPair generateAsymmetricKeyPair() throws NoSuchAlgorithmException {
    KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
    keyPairGenerator.initialize(2048);
    return keyPairGenerator.generateKeyPair();
    }

    public static String bytesToHex(byte[] bytes) {
    StringBuilder result = new StringBuilder();
    for (byte b : bytes) {
    result.append(String.format("x", b));
    }
    return result.toString();
    }

    public static class LicenseData {
    private String licenseId;
    private String licenseType;

    public String getLicenseId() {
    return licenseId;
    }

    public void setLicenseId(String licenseId) {
    this.licenseId = licenseId;
    }

    public String getLicenseType() {
    return licenseType;
    }

    public void setLicenseType(String licenseType) {
    this.licenseType = licenseType;
    }
    }
    }

    把上述代码 copy 下来,放到工程中,根据需要改造即可。


    这里特别要说下,强烈推荐使用原版 AI,而不是寻找平替,同样的 prompt,下图是某一平替输出的结果:


    只生成了函数声明,没有生成函数实现。二者对比,未免相形见绌。



    辅助编程工具


    改造的过程中,少不了 AI pair programming tools。对此,我推荐使用 Amazon 的 CodeWhisperer,原因很简单,跟 GitHub Copilot 相比,它是免费的😃。


    CodeWhisperer 的安装可以看文末的安装教程,我们先来看一下它是怎么辅助我们编码的。


    第一种方式是最简单的,那就是什么都不管,等待智能提示即可,就好像 IDEA 原来的提示一样,只不过更智能。


    下图示例中,要把原来的中文异常提示,修改成英文,而我只输入了两个字符 IM, 就得到了智能提示,补全了完整的英文字符串!



    可以注意到,上图的智能建议一共有 5 条,相应的快捷键为:

    1. 方向键 ->,查看下一条提示
    2. 方向键 <-,查看上一条提示
    3. Tab,采用该提示
    4. Esc,拒绝提示

    我们再来看第二种 CodeWhisperer 的使用方式,编写注释,获得编码建议。



    最后一种就是编写一个空函数,让 CodeWhisperer 根据函数名去猜测函数的实现,这种情况需要足够的上下文,才能得到令人满意的结果。


    软件测试


    AI 生成的内容,并不是完全可信任的,因此,单元测试的重要性变得尤为突出。


    对上述代码编写测试代码后,实际上并不能一次通过,因为前面 AI 生成的代码参数有误。


    此时需要一边执行单测,一边根据结果与 AI 进行交互:



    经过修改,最终测试用例通过👏!



    总结


    本文通过案例,展示了 AI 如何结合软件研发的流程,提升我们的编程效率的。


    其中,个人认为最重要的是编写伪代码与进行单元测试。有趣的是,这两样实践在 AIGC 时代之前,就已经被认为是最佳实践。这给我们启示:某些方法论、实践经得起时间的考验,技术更新迭代,它们历久弥新。


    另外,AI 是否能进一步渗透我们的工作流,还有待探索。此文作引抛砖引玉之用,期待大家的后续分享。


    附:CodeWhisperer 安装


    下载 2023 年的 IDEA,打开 Plugins Marketplace,找到 AWS Toolkit



    安装完成、重启 IDEA 后,点击左下角,按下图所示操作:




    如果第一次使用,就点击 1 处进行注册,如果已经有账号了,就点击 2 处使用自己的账号登录。



    注册、登录、授权成功后,出现如图所示页面,即可使用 CodeWhisperer。



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

    前端重新部署如何通知用户刷新网页?

    1.目标场景 有时候上完线,用户还停留在老的页面,用户不知道网页重新部署了,跳转页面的时候有时候js连接hash变了导致报错跳不过去,并且用户体验不到新功能。 2.思考解决方案 如何去解决这个问题 思考中... 如果后端可以配合我们的话我们可以使用webSoc...
    继续阅读 »



    1.目标场景


    有时候上完线,用户还停留在老的页面,用户不知道网页重新部署了,跳转页面的时候有时候js连接hash变了导致报错跳不过去,并且用户体验不到新功能。


    2.思考解决方案


    如何去解决这个问题
    思考中...


    如果后端可以配合我们的话我们可以使用webSocket 跟后端进行实时通讯,前端部署完之后,后端给个通知,前端检测到Message进行提示,还可以在优化一下使用EvnentSource 这个跟socket很像只不过他只能后端往前端推送消息,前端无法给后端发送,我们也不需要给后端发送。


    以上方案需要后端配合,奈何公司后端都在忙,需要纯前端实现。


    重新进行思考...


    根据和小伙伴的讨论得出了一个方案,在项目根目录给个json 文件,写入一个固定的key值然后打包的时候变一下,然后代码中轮询去判断看有没有变化,有就提示。




    果然是康老师经典不知道。




    但是写完之后发现太麻烦了,需要手动配置json文件,还需要打包的时候修改,有没有更简单的方案,
    进行第二轮讨论。


    第二轮讨论的方案是根据打完包之后生成的script src 的hash值去判断,每次打包都会生成唯一的hash值,只要轮询去判断不一样了,那一定是重新部署了.




    3.代码实现

    interface Options {
    timer?: number
    }

    export class Updater {
    oldScript: string[] //存储第一次值也就是script 的hash 信息
    newScript: string[] //获取新的值 也就是新的script 的hash信息
    dispatch: Record<string, Function[]> //小型发布订阅通知用户更新了
    constructor(options: Options) {
    this.oldScript = [];
    this.newScript = []
    this.dispatch = {}
    this.init() //初始化
    this.timing(options?.timer)//轮询
    }


    async init() {
    const html: string = await this.getHtml()
    this.oldScript = this.parserScript(html)
    }

    async getHtml() {
    const html = await fetch('/').then(res => res.text());//读取index html
    return html
    }

    parserScript(html: string) {
    const reg = new RegExp(/<script(?:\s+[^>]*)?>(.*?)<\/script\s*>/ig) //script正则
    return html.match(reg) as string[] //匹配script标签
    }

    //发布订阅通知
    on(key: 'no-update' | 'update', fn: Function) {
    (this.dispatch[key] || (this.dispatch[key] = [])).push(fn)
    return this;
    }

    compare(oldArr: string[], newArr: string[]) {
    const base = oldArr.length
    const arr = Array.from(new Set(oldArr.concat(newArr)))
    //如果新旧length 一样无更新
    if (arr.length === base) {
    this.dispatch['no-update'].forEach(fn => {
    fn()
    })

    } else {
    //否则通知更新
    this.dispatch['update'].forEach(fn => {
    fn()
    })
    }
    }

    timing(time = 10000) {
    //轮询
    setInterval(async () => {
    const newHtml = await this.getHtml()
    this.newScript = this.parserScript(newHtml)
    this.compare(this.oldScript, this.newScript)
    }, time)
    }

    }

    代码用法

    //实例化该类
    const up = new Updater({
    timer:2000
    })
    //未更新通知
    up.on('no-update',()=>{
    console.log('未更新')
    })
    //更新通知
    up.on('update',()=>{
    console.log('更新了')
    })

    4.测试


    执行 npm run build 打个包


    安装http-server


    使用http-server 开个服务




    重新打个包npm run build




    这样子就可以检测出来有没有重新发布就可以通知用户更新了。


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

    超出范围自动滚动、支持彩色流动特效的UILabel封装

    iOS
    JKRShimmeringLabel 特征 支持炫彩字支持炫彩流动字支持超出显示范围自动滚动文本支持RTL下的对称显示和滚动支持Frame布局支持Xib和StoryBoard内使用支持AutoLayout布局 使用 源码连接 和原生UILabel一样用,只需...
    继续阅读 »

    JKRShimmeringLabel


    特征





    1. 支持炫彩字

    2. 支持炫彩流动字

    3. 支持超出显示范围自动滚动文本

    4. 支持RTL下的对称显示和滚动

    5. 支持Frame布局

    6. 支持Xib和StoryBoard内使用

    7. 支持AutoLayout布局


    使用


    源码连接


    和原生UILabel一样用,只需要设置mask属性(一张彩色的图片遮罩)即可。


    原有项目的UILabel替换


    因为JKRAutoScrollLabel和JKRShimmeringLabel本身就是继承UILabel,可以直接把原有项目的UILabel类,替换成JKRAutoScrollLabel或JKRShimmeringLabel即可。


    JKRAutoScrollLabel


    超出范围自动滚动的Lable,需要设置attributedText,不能设置text。要同时支持流动彩字,设置mask即可。不需要彩色可以不设置mask,只有自动滚动的特性。


    // Frame布局,字体支持炫彩闪动,同时超出显示范围自动滚动

    NSMutableAttributedString *textForFrameAttr = [[NSMutableAttributedString alloc] initWithString:@"我是滚动测试文本Frame布局,看看我的效果" attributes:@{NSForegroundColorAttributeName: UIColorHex(FFFFFF), NSFontAttributeName: [UIFont systemFontOfSize:19 weight:UIFontWeightBold]}];

    self.autoScrollLabelForFrame = [[JKRAutoScrollLabel alloc] initWithFrame:CGRectMake(isRTL ? kScreenWidth - 10 - 300 : 10, CGRectGetMaxY(title0.frame) + 10, 300, 24)];

    // 滚动文本需要设置 attributedText 才能生效

    self.autoScrollLabelForFrame.attributedText = textForFrameAttr;

    // 设置文字颜色的mask图片遮罩,如果不需要字体炫彩,不设置即可

    self.autoScrollLabelForFrame.mask = [self maskImage];

    [self.view addSubview:self.autoScrollLabelForFrame];


    JKRShimmeringLabel


    支持流动彩字,设置mask即可,如果还需要超出范围自动滚动,需要使用JKRAutoScrollLabel。


    // Frame布局,字体支持炫彩闪动

    self.shimmerLabelForFrame = [[JKRShimmeringLabel alloc] initWithFrame:CGRectMake(isRTL ? kScreenWidth - 10 - 300 : 10, CGRectGetMaxY(title1.frame) + 10, 300, 24)];

    self.shimmerLabelForFrame.text = @"我是彩色不滚动文本Frame布局,看看我的效果";

    self.shimmerLabelForFrame.font = [UIFont systemFontOfSize:19];

    // 设置文字颜色的mask图片遮罩,如果不需要字体炫彩,不设置即可

    self.shimmerLabelForFrame.mask = [self maskImage];

    [self.view addSubview:self.shimmerLabelForFrame];


    Xib使用


    控件支持xib和autolayout的场景,和UILabel一样设置约束即可,自动滚动和彩色动画,会自动支持。只需要正常配置约束,然后设置mask彩色遮罩即可。


    同时,因为JKRShimmeringLabel和JKRAutoScrollLabel本身就是继承UILabel的,所以UILabel在Xib中的文本自动填充宽度、约束优先级等等特性,也都可以正常使用。


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

    iOS 线程安全和锁机制

    iOS
    一、线程安全场景 多个线程中同时访问同一块资源,也就是资源共享。多牵扯到对同一块数据的读写操作,可能引发数据错乱问题。 比较经典的线程安全问题有购票和存钱取钱问题,为了说明读写操作引发的数据混乱问题,以存钱取钱问题来做个说明。 1. 购票案例 用代码示例如下...
    继续阅读 »

    一、线程安全场景


    多个线程中同时访问同一块资源,也就是资源共享。多牵扯到对同一块数据的读写操作,可能引发数据错乱问题。


    比较经典的线程安全问题有购票和存钱取钱问题,为了说明读写操作引发的数据混乱问题,以存钱取钱问题来做个说明。


    1. 购票案例




    用代码示例如下:

    @IBAction func ticketSale() {

            tickets = 30

            let queue = DispatchQueue.global()

            queue.async {

                for _ in 0..<10 {

                    self.sellTicket()

                }

            }

            queue.async {

                for _ in 0..<10 {

                    self.sellTicket()

                }

            }

            queue.async {

                for _ in 0..<10 {

                    self.sellTicket()

                }

            }

        }

        //卖票

        func sellTicket() {

            var oldTicket = tickets

            sleep(UInt32(0.2))

             oldTicket -= 1

             tickets = oldTicket

            print("还剩\(tickets)张票 ---- \(Thread.current)")

        }

    同时有三条线程进行卖票,对余票进行减操作,三条线程每条卖10次,原定票数30,应该剩余票数为0,下面看下打印结果:




    可以看到打印票数不为0


    2. 存钱取钱案例


    先用个图说明




    上图可以看出,存钱和取钱之后的余额理论上应该是500,但由于存钱取钱同时访问并修改了余额,导致数据错乱,最终余额可能变成了400,下面用代码做一下验证说明:

    //存钱取钱

        @IBAction func remainTest() {

            remain = 500

            let queue = DispatchQueue.global()

            queue.async {

                for _ in 0..<5 {

                    self.saveMoney()

                }

            }

            queue.async {

                for _ in 0..<5 {

                    self.drawMoney()

                }

            }

        }

        //存钱

        func saveMoney() {

           var oldRemain = remain

            sleep(2)

            oldRemain += 100

            remain = oldRemain

            print("存款100元,账户余额还剩\(remain)元 ----\(Thread.current)")

        }

        

        //取钱

        func drawMoney() {

            var oldRemain = remain

             sleep(2)

             oldRemain -= 50

             remain = oldRemain

            print("取款50元,账户余额还剩\(remain)元 ---- \(Thread.current)")

        }

    上述代码存款5次100,取款5次50,最终的余额应该是 500 + 5 * 100 - 5 * 50 = 750




    如图所示,可以看到在存款取款之间已经出现错乱了



    上述两个案例之所以出现数据错乱问题,就是因为有多个线程同时操作了同一资源,导致数据不安全而出现的。



    那么遇到这个问题该怎么解决呢?自然而然的,我们想到了对资源进行加锁处理,以此来保证线程安全,在同一时间,只允许一条线程访问资源。


    加锁的方式大概有以下几种:

    • OSSpinLock
    • os_unfair_lock
    • pthread_mutex
    • dispatch_semaphore
    • dispatch_queue(DISPATCH_QUEUE_SERIAL)
    • NSLock
    • NSRecursiveLock
    • NSCondition
    • NSConditionLock

    1. OSSpinLock 自旋锁




    OSSpinLock 是自旋锁,在系统框架 libkern/OSAtomic




    如图,系统提供了以下几个API

    • 定义lock let osspinlock = OSSpinLock()
    • OSSpinLockTry

    官方给定的解释如下

    Locks a spinlock if it would not block
    return false, if the lock was already held by another thread,
    return true, if it took the lock successfully.


    尝试加锁,加锁成功则继续,加锁失败则直接返回,不会阻塞线程

    • OSSpinLockLock
    Although the lock operation spins, it employs various strategies to back

    off if the lock is held.

    加锁成功则继续,加锁失败,则会阻塞线程,处于忙等状态

    • OSSpinLockUnlock: 解锁

    使用
    @IBAction func ticketSale() {

            osspinlock = OSSpinLock()

            tickets = 30

            let queue = DispatchQueue.global()

            queue.async {

                for _ in 0..<10 {

                    self.sellTicket()

                }

            }

            queue.async {

                for _ in 0..<10 {

                    self.sellTicket()

                }

            }

            queue.async {

                for _ in 0..<10 {

                    self.sellTicket()

                }

            }

        }

        //卖票

        func sellTicket() {

            OSSpinLockLock(&osspinlock)

            var oldTicket = tickets

            sleep(UInt32(0.2))

             oldTicket -= 1

             tickets = oldTicket

            print("还剩\(tickets)张票 ---- \(Thread.current)")

            OSSpinLockUnlock(&osspinlock)

        }



    可以看到,最终的余票数量已经是正确的了,这里要注意的是osspinlock需要做成全局变量或者属性,多个线程要用这同一把锁去加锁和解锁,如果每个线程各自生成锁,则达不到要加锁的目的了


    那么自旋锁是怎么样做到加锁保证线程安全的呢?
    先来介绍下让线程阻塞的两种方法:

    • 忙等:也就是自旋锁的原理,它本质上就是个while循环,不停地去判断加锁条件,自旋锁没有让线程真正的阻塞,只是将线程处在while循环中,系统CPU还是会不停地分配资源来处理while循环指令。
    • 真正阻塞线程: 这是让线程休眠,类似于Runloop里的match_msg() 实现的效果,它借助系统内核指令,让线程真正停下来处于休眠状态,系统的CPU不再分配资源给线程,也不会再执行任何指令。系统内核用的是symcall指令来让线程进入休眠

    它的原理就是,自旋锁在加锁失败时,让线程处于忙等状态,让线程停留在临界区之外,一旦加锁成功,就可以进入临界区对资源进行操作。




    通过这个可以看到,苹果在iOS10之后就弃用了OSSpinLock,官方建议用 os_unfair_lock来代替,那么为什么要弃用呢?因为在iOS10之后线程可以设置优先级,在优先级配置下,可以产生优先级反转,使自旋锁卡住,自旋锁本身已经不再安全。


    2. os_unfair_lock


    os_unfair_lock 是苹果官方推荐的,自iOS10之后用来替代 OSSpinLock 的一种锁

    • os_unfair_lock_trylock: 尝试加锁,加锁成功返回true,继续执行。加锁失败,则返回false,不会阻塞线程。
    • os_unfair_lock_lock: 加锁,加锁失败,阻塞线程继续等待。加锁成功,继续执行。
    • os_unfair_lock_unlock : 解锁

    使用:

    //卖票

        func sellTicket() {

            os_unfair_lock_lock(&unfairlock)

            var oldTicket = tickets

            sleep(UInt32(0.2))

             oldTicket -= 1

             tickets = oldTicket

            print("还剩\(tickets)张票 ---- \(Thread.current)")

            os_unfair_lock_unlock(&unfairlock)

        }

    打印结果和OSSpinLock一样,os_unfair_lock摒弃了OSSpinLock的while循环实现的忙等状态,而是采用了真正让线程休眠,从而避免了优先级反转问题。


    3. pthread_mutex


    pthread_mutexpthread跨平台的一种解决方案,mutex 为互斥锁,等待锁的线程会处于休眠状态。
    互斥锁的初始化比较麻烦,主要为以下方式:

    1. var ticketMutexLock = pthread_mutex_t()
    2. 初始化属性:
    var attr = pthread_mutexattr_t()
    pthread_mutexattr_init(&attr)
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL)

    3. 初始化锁:pthread_mutex_init(&ticketMutexLock, &attr)

    关于互斥锁的使用,主要提供了以下方法:

    1. 尝试加锁:pthread_mutex_trylock(&ticketMutexLock)
    2. 加锁:pthread_mutex_lock(&ticketMutexLock)
    3. 解锁:pthread_mutex_unlock(&ticketMutexLock)
    4. 销毁相关资源:pthread_mutexattr_destory(&attr)pthread_mutex_destory(&ticketMutexLock)

    使用方式如下:




    要注意,在析构函数中要将锁进行销毁释放掉
    在初始化属性中,第二个参数有以下几种方式:




    PTHREAD_MUTEX_DEFAULT = PTHREAD_MUTEX_NORMAL,代表普通的互斥锁
    PTHREAD_MUTEX_ERRORCHECK 代表检查错误锁
    PTHREAD_MUTEX_RECURSIVE 代表递归互斥锁


    互斥锁的底层原理实现也是通过阻塞线程,等待锁的线程处于休眠状态,CPU不再给等待的线程分配资源,和上面讲到的os_unfair_lock原理类似,都是通过内核调用symcall方法来休眠线程,通过这个对比也能推测出,os_unfair_lock实际上也可以归属于互斥锁


    3.1 递归互斥锁



    如图所示,如果是上述场景,方法1里面嵌套方法2,正常调用时,输出应该为:




    若要对上述场景保证线程安全,先用普通互斥锁添加锁试下




    结果打印如下:




    和预想中的不一样,如果懂得锁机制便会明白,图中所示的rsmText2中加锁失败,需要等待rsmText1中的锁释放后才可加锁,所以rsmText2方法开始等待并阻塞线程,程序无法再执行下去,那么rsmText1中锁释放的逻辑就无法执行,就这样造成了死锁,所以只能打印rsmText1中的输出内容。
    解决这个问题,只需要给两个方法用两个不同的锁对象进行加锁就可以了,但是如果是针对于同一个方法递归调用,那么就无法通过不同的对象去加锁,这时候应该怎么办呢?递归互斥锁就该用上了。








    如上,已经可以正常调用并加锁
    那么递归锁是如何避免死锁的呢?简而言之就是允许对同一个对象进行重复加锁,重复解锁,加锁和解锁的次数相等,调用结束时所有的锁都会被解开


    3.2 互斥锁条件 pthread_cond_t

    互斥锁条件所用到的常见方法如下:

    1. 定义一个锁: var condMutexLock = pthread_mutex_t()
    2. 初始化锁对象:pthread_mutex_init(&condMutexLock)
    3. 定义条件对象:var condMutex = pthread_cond_t()
    4. 初始化条件对象:pthread_cond_init(&condMutex, nil)
    5. 等待条件:pthread_cond_wait(&condMutex, &condMutexLock) 等待过程中会阻塞线程,知道有激活条件的信号发出,才会继续执行
    6. 激活一个等待该条件的线程:pthread_cond_signal(&condMutex)
    7. 激活所有等待该条件的线程pthread_cond_broadcast(&condMutex)
    8. 解锁:pthread_mutex_unlock(&condMutexLock)
    9. 销毁锁对象和销毁条件对象:pthread_mutex_destroy(&condMutexLock) pthread_cond_destroy(&condMutex)

    下面设计一个场景:

    • 在一个线程里对dataArr做remove操作,另一个线程里做add操作
    • dataArr为0时,不能进行删除操作
    @IBAction func mutexCondTest(_ sender: Any) {

            initMutextCond()

        }

        func initMutextCond() {

            //初始化属性

            var attr = pthread_mutexattr_t()

            pthread_mutexattr_init(&attr)

            pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL)

            //初始化锁

            pthread_mutex_init(&condMutexLock, &attr)

            //释放属性

            pthread_mutexattr_destroy(&attr)

            //初始化cond

            pthread_cond_init(&condMutex, nil)

            _testDataArr()

            

        }

        func _testDataArr() {

            let threadRemove = Thread(target: self, selector: #selector(_remove), object: nil)

            threadRemove.name = "remove 线程"

            threadRemove.start()

            

            sleep(UInt32(1))

            let threadAdd = Thread(target: self, selector: #selector(_add), object: nil)

            threadAdd.name = "add 线程"

            threadAdd.start()

            

        }

        @objc func _add() {

            //加锁

            pthread_mutex_lock(&condMutexLock)

            print("add 加锁成功---->\(Thread.current.name!)开始")

            sleep(UInt32(2))

            dataArr.append("test")

            print("add成功,发送条件信号 ------ 数组元素个数为\(dataArr.count)")

            pthread_cond_signal(&condMutex)

            //解锁

            pthread_mutex_unlock(&condMutexLock)

            print("解锁成功,\(Thread.current.name!)线程结束")

        }

        @objc func _remove() {

            //加锁

            pthread_mutex_lock(&condMutexLock)

            print("remove 加锁成功,\(Thread.current.name!)线程开启")

            if(dataArr.count == 0) {

                print("数组内没有元素,开始等待,数组元素为\(dataArr.count)")

                pthread_cond_wait(&condMutex, &condMutexLock)

                print("接收到条件更新信号,dataArr元素个数为\(dataArr.count),继续向下执行")

            }

            dataArr.removeLast()

            print("remove成功,dataArr数组元素个数为\(dataArr.count)")

            

            //解锁

            pthread_mutex_unlock(&condMutexLock)

            print("remove解锁成功,\(Thread.current.name!)线程结束")

        }

        

        deinit {

    //        pthread_mutex_destroy(&ticketMutexLock)

            pthread_mutex_destroy(&condMutexLock)

            pthread_cond_destroy(&condMutex)

        }

    输出结果为:




    从打印结果来看,如果不满足条件时进行条件等待 pthread_cond_wati,remove 线程是解锁,此时线程是休眠状态,然后等待的add 线程进行加锁成功,处理add的逻辑。


    当add 操作完毕时,通过 pthread_cond_signal发出信号,remove线程收到信号后被唤醒,然后remove线程会等待add线程解锁后,再进行加锁处理后续的逻辑.


    整个过程中一共用到了三次加锁,三次解锁,这种锁可以处理线程依赖的场景.


    4. NSLock, NSRecursiveLock, NSCondition


    上文中提到了mutex普通互斥锁 mutex递归互斥锁mutext条件互斥锁,这几种锁都是基于C语言的API,苹果在此基础上做了面向对象的封装,分别对应如下:

    • NSLock 封装了 pthread_mutex_t的 attr类型为 PTHREAD_MUTEX_DEFAULT 或者 PTHREAD_MUTEX_NORMAL 普通锁
    • NSRecursiveLock 封装了 pthread_mutex 的 attr类型为PTHREAD_MUTEX_RECURSIVE递归锁
    • NSCondition 封装了 pthread_mutex_t 和 pthread_cond_t

    底层实现和 pthread_mutex_t一样,这里只看下使用方式即可:


    4.1 NSLock
    //普通锁 
    let lock = NSLock()
    lock.lock()
    lock.unlock()

    4.2 NSRecursiveLock
    let lock = NSRecursiveLock()
    lock.lock()
    lock.unlock()

    4.3 NSCondition
    let condition = NSCondition()
    condition.lock()
    condition.wait()
    condition.signal()//condition.broadcast()
    condition.unlock()

    4.4 NSConditionLock

    这个是NSCondition 的进一步封装,该锁允许我们在锁中设定条件具体条件值,有了这个功能,我们可以更加方便的多条线程的依赖关系和前后执行顺序


    下面用一个场景来模拟下顺序控制的功能,有三条线程执行A,B,C三个方法,要求按A,C,B的顺序执行

    @IBAction func conditionLockTest(_ sender: Any) {

           let threadA = Thread(target: self, selector: #selector(A), object: nil)

            threadA.name = "ThreadA"

            threadA.start()

           let threadB = Thread(target: self, selector: #selector(B), object: nil)

            threadB.name = "ThreadB"

            threadB.start()

           let threadC = Thread(target: self, selector: #selector(C), object: nil)

            threadC.name = "ThreadC"

            threadC.start()

        }

        @objc func A() {

            conditionLock.lock()

            print("A")

            sleep(UInt32(1))

            conditionLock.unlock(withCondition: 3)

        }

        @objc func B() {

            conditionLock.lock(whenCondition: 2)

            print("B")

            sleep(UInt32(1))

            conditionLock.unlock()

        }

        @objc func C() {

            conditionLock.lock(whenCondition: 3)

            print("C")

            conditionLock.unlock(withCondition: 2)

        }

    输出结果为:

    A

    C

    B

    5. dispatch_semaphore


    信号量 的初始值可以用来控制线程并发访问的最大数量,初始值为1,表示同时允许一条线程访问资源,这样可以达到线程同步的目的

    • 创建信号量:dispatch_semaphore_create(value)

    • 等待:dispatch_semaphore_wait(semaphore, 等待时间) 信号量的值 <= 0,线程就休眠等待,直到信号量 > 0,如果信号量的值 > 0,则就将信号量的值递减1,继续执行下面的程序

    • 信号量值+1: dispatch_semaphore_signal(semaphore)


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

    【iOS】高效调试 iOS APP 的 UI

    iOS
    调试是程序是开发过程中必不可少的环节,每当我们完成一段代码或者发现一些问题都需要对程序进行调试。高效的调试能帮我们节省大量的开发时间。这篇文章我将分享一些提高调试效率的工具和它们的使用方法。 在开发iOS APP的时候我们最频繁进行调试的莫过于UI了。 一、U...
    继续阅读 »

    调试是程序是开发过程中必不可少的环节,每当我们完成一段代码或者发现一些问题都需要对程序进行调试。高效的调试能帮我们节省大量的开发时间。这篇文章我将分享一些提高调试效率的工具和它们的使用方法。


    在开发iOS APP的时候我们最频繁进行调试的莫过于UI了。


    一、UI的调试


    开发中我们经常需要多次修改UI元素的样式进行微调,查看效果并确定正确的数值。

    Xcode

    如下图所示,Xcode 提供了完备的UI调试工具。




    在左边,我们可以看到完整对视图树,中间有各个视图对3D拆分展示,右边,可以看到当前选中的视图的一些信息。


    Xcode在进行UI调试的时候,会暂停APP,视图的信息也只能查看不能方便的修改。在UI调试的时候需要修改代码然后重新编译运行才能看到最终的效果。


    在频繁调试UI样式的时候是很耗费时间的(如果电脑性能非常好可能会耗费的时间可能会短一些)所以这不是最佳的选择。

    LookIn

    在这里向大家介绍一款视图调试工具Lookin,它是由腾讯的QMUI团队开发并开源的一款免费的UI调试工具。


    有了它,我们就能进行高效的UI调试。


    使用方法也非常简单,具体可以查看官方的集成指导


    接下来我将分几点简单的介绍一下这个工具的强大功能。

    查看与修改UI

    Lookin 可以查看与修改 iOS App 里的 UI 对象,类似于 Xcode 自带的 UI Inspector 工具,不需要重新编译运行。而且借助于“控制台”和“方法监听”功能,Lookin 还可以进行 UI 之外的调试。



    独立运行
    此外,虽然 Lookin 主体是一款 macOS 程序,它亦可嵌入你的 iOS App 而单独运行在 iPhone 或 iPad 上。



    显示变量名
    Lookin 会显示变量名,以及 indexPath 等各种提示。



    显示手势
    添加了手势的 UIView,或添加了 Target-Action 的 UIControl,左侧都会被加上一个小蓝条,点击即可查看信息或调试手势



    测距
    按住 Option 键,即可测量任意两个 view 之间的距离



    导出文件

    通过手机或电脑将当前 iOS App 的 UI 结构导出为 Lookin 文件以备之后查看,或直接转发给别人。
    当测试发现BUG时可以完美对固定现场,并可以将文件发送给开发者查看当时的视图结构。


    二、热重载


    💉Injection III


    Lookin已经帮我们解决了很多问题,但当我们修改了代码的业务逻辑,或者修改了UI的加载逻辑,或者对代码进行了比较大的改动,此时还是需要重新编译运行才能使新的代码生效。同样会耗费许多时间编译、重新运行、点击屏幕到达刚才修改的页面的时间。


    这个时候就是我们的第二款高效开发的得力助手登场的时候了。


    它就是 💉 Injection III,一款开源免费的热重载工具。


    Injection III 是一款能在iOS开发时实现类似Web前端那样热重载的工具。他会监听代码文件的变化,当代码发生改变,他会将改变的部分自动编译成一个动态链接库,然后动态的加载到程序中,达到不重启APP直接热重载的目的。


    下面我简单介绍一下如何使用它。


    我们可以在 Mac App Store 上下载InjectionIII。打开后会在状态栏有一个蓝色的注射器图标,选择Open Project 打开工程所在目录开始监听我们的文件更改。




    接下来在工程中进行一些配置,


    Xcodebuild settingOther Linker Flags 中添加-Xlinker -interposable


    AppDelegateapplicationDidFinishLaunching方法中加入如下代码:

    #if DEBUG
    //for iOS:
    Bundle(path: "/Applications/InjectionIII.app/Contents/Resources/iOSInjection.bundle")?.load()
    //for tvOS:
    Bundle(path: "/Applications/InjectionIII.app/Contents/Resources/tvOSInjection.bundle")?.load()
    //Or for macOS:
    Bundle(path: "/Applications/InjectionIII.app/Contents/Resources/macOSInjection.bundle")?.load()
    #endif

    接下来,编译运行你的APP,此时控制台会打印Injection的相关信息




    同时状态栏的图标也会变为红色。此时说明Injection启动成功。


    接下来你就可以修改一下代码,并保存,Injection会自动编译并自动将其注入到模拟器中运行的APP。控制台也会打印相关的信息。




    同时,它会为被注入的类提供一个回调@objc func injected() ,当某个类被注入时,会调用该方法。


    我们可以在这里刷新UI,就能做到实时更新UI了。


    注意事项


    虽然Injection很强大,但它也有很多限制:

    • 你可以修改class、enum、struct 的成员方法的实现,但如果是inline函数则不行,如果有这种情况需要重新编译运行。

    • 它只支持模拟器,不支持真机。

    • 你不能修改class、enum、struct的布局(即成员变量和方法的顺序),如果有这种情况需要重新编译运行。

    • 你不能增加或删除存储类型的属性和方法,如果有这种情况需要重新编译运行。


    更多详情可以参见官方的说明:InjectionIII/README.md at main · johnno1962/InjectionIII (github.com)


    虽然 Injection III 有很多限制,但它依然能为我们带来非常大的效率提升。


    另一个热重载神器: krzysztofzablocki/Inject


    krzysztofzablocki/Inject: Hot Reloading for Swift applications! (github.com)


    它配合 Injection III 可以更方便的实现热重载和界面自动刷新,实现类似Swift UI的自动刷新效果,但是,它只支持Swift,并且通过Swift Package Manager进行安装。


    三、写在最后


    实用的工具很多,找到一款既强大又好用的工具,并且把它用好能够很大的提升我们开发的效率。


    希望大家能喜欢我分享的这两款工具,希望它们能为大家带来效率的提升。


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

    iOS UITableView 图片刷新闪烁问题记录

    iOS
    一. 问题背景 项目中遇到一个问题,就是当App不在首页的时候,切换到其他App比如微信,然后返回App当前页面,然后从当前页面返回首页,会在首页viewWillAppear这里去拉取是否有未完成订单的接口,刷新UITableView,这时会出现广告位闪烁问题...
    继续阅读 »

    一. 问题背景


    项目中遇到一个问题,就是当App不在首页的时候,切换到其他App比如微信,然后返回App当前页面,然后从当前页面返回首页,会在首页viewWillAppear这里去拉取是否有未完成订单的接口,刷新UITableView,这时会出现广告位闪烁问题。




    二. 问题排查


    1.原因分析


    这个问题经过断点调试和排除法,发现只要当App进入后台后,回来刷新首页的UITableView都有可能出现闪烁现象。


    因此首先我们对图片的加载做延迟操作,并在Cell生成方法调用里面添加相关打印:






    可以看到如下打印日志:




    从打印日志我们可以看出来,调用reloadData方法后,原来UITableViewcell位置会调整。


    但是如果我们App没有进入后台,而是直接调用UITableViewreloadData方法,并不会出现闪烁现象。


    因此可以这里可以推测应该是进入后台做了什么操作导致,回到App刷新才会导致闪烁。


    因为使用的是SDWebImage加载框架加载,我们合理的怀疑是加载图片的SDWebImage框架,进入后台的处理逻辑导致的,因此我们先使用imageCacheDict字典写下图片加载和缓存逻辑:




    经测试,进入后台,再返回App刷新不会出现闪烁现象。


    因此可以肯定UITableView调用reloadData方法闪烁原因是SDWebImage,在进入后台的时候对内存缓存做了相关操作导致。


    我们都知道SDWebImage,默认是使用NSCache来做内存缓存,而NSCache在进入后台的时候,默认会清空缓存操作,导致返回App调用UITableView调用reloadData方法时候,SDWebImage需要根据图片地址重新去磁盘获取图像数据,然后解压解码渲染,因为是从缓存磁盘直接获取图像数据,没有渲染流程,因此会造成闪烁。


    为了验证这个猜想,我们使用YYWebImage加载框架来做对比实验:

    首先注释掉YYWebImage进入后台清空内存缓存的逻辑: 


    然后进入后台,返回App调用UITableView调用reloadData刷新,发现一切正常。

    原因总结:

    • 第一个原因是UITableView调用reloadData方法,由于UITableViewCell的复用,会出现Cell位置调整现象

    • 由于SDWebImage使用了NSCache做内存缓存,当App进入后台,NSCache会清空内存缓存,导致返回App后调用UITableView调用reloadData,刷新去加载图片的时候,需要从SDWebImage的磁盘中重新获取图片数据,然后重新解压解码渲染,因为从磁盘中读取速度快,两者原因导致了闪烁。


    三. 解决方案


    因为该现象是由如上两个原因导致,因此针对这两个原因,有如下两种解决方案:

    1. 解决UITableViewCell复用问题


    可以通过设置ReusableCellWithIdentifier不同,保证广告cell不进行复用。

     NSString *cellId = [NSString stringWithFormat:@"%ld-%ld-FJFAdTableViewCell", indexPath.section, indexPath.row];

    2. 从后台返回后,提早进行刷新操作

    当从后台返回App前台的时候或者视图添加到父视图的时候,先执行下UITableView调用reloadData方法,提前通过SDWebImage去从磁盘中加载图片。


    从后台返回前台:

    [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(willEnterForeground) name:UIApplicationWillEnterForegroundNotification object:nil];
    - (void)willEnterForeground {
    [self.tableView reloadData];
    NSLog(@"--------------------------willEnterForeground");
    }

    视图添加到父视图:

    - (void)willMoveToParentViewController:(UIViewController *)parent {
    [self.tableView reloadData];
    }

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

    RunLoop:iOS开发中的神器,你真的了解它吗?

    iOS
    在iOS开发中,RunLoop是一个非常重要的概念,它提供了一个事件循环机制,用于处理各种事件,例如用户交互、网络请求、定时器等等。RunLoop不仅是iOS开发中的核心之一,而且在其他平台的开发中也有广泛的应用。本文将为您介绍Swift中RunLoop的基本...
    继续阅读 »

    iOS开发中,RunLoop是一个非常重要的概念,它提供了一个事件循环机制,用于处理各种事件,例如用户交互、网络请求、定时器等等。RunLoop不仅是iOS开发中的核心之一,而且在其他平台的开发中也有广泛的应用。本文将为您介绍SwiftRunLoop的基本概念和使用方法。


    什么是RunLoop?


    RunLoop是一个事件循环机制,它用于在iOS应用程序中处理各种事件。RunLoop在应用程序的主线程中运行,它负责管理该线程中的事件,并确保UI更新等重要任务能够顺利执行。RunLoop还负责处理其他线程发送的事件,例如网络请求等等。


    RunLoop的基本思想是循环地处理事件。当RunLoop启动时,它会进入一个无限循环,等待事件的发生。当有事件发生时,RunLoop会调用相应的处理方法来处理该事件,并继续等待下一个事件的发生。RunLoop会一直运行,直到被手动停止或应用程序退出。


    RunLoop与线程


    iOS中,每个线程都有一个RunLoop,但默认情况下,RunLoop是被禁用的。要使用RunLoop,必须手动启动它,并将其添加到线程的运行循环中。


    例如,要在主线程中使用RunLoop,可以使用如下代码:

    RunLoop.current.run()

    这将启动主线程的RunLoop,并进入一个无限循环,等待事件的发生。


    RunLoop模式


    RunLoop模式是RunLoop的一个重要概念,它定义了RunLoop在运行过程中需要处理的事件类型。一个RunLoop可以有多个模式,但在任何时刻只能处理一个模式。每个模式都可以包含多个输入源(input source)和定时器(timer)RunLoop会根据当前模式中的输入源和定时器来决定下一个事件的处理方式。


    RunLoop提供了几个内置模式,例如:

    1. NSDefaultRunLoopMode:默认模式,处理所有UI事件、定时器和PerformSelector方法。
    2. UITrackingRunLoopMode:跟踪模式,只处理与界面跟踪相关的事件,例如UIScrollView的滚动事件。
    3. NSRunLoopCommonModes:公共模式,同时包含NSDefaultRunLoopModeUITrackingRunLoopMode。 RunLoop还允许开发者自定义模式,以满足特定需求。

    定时器


    iOS开发中,定时器是一种常见的事件,例如每隔一段时间刷新UI、执行后台任务等等。RunLoop提供了定时器(timer)机制,用于在指定时间间隔内执行某个操作。


    例如,要在主线程中创建一个定时器并启动它,可以使用如下代码:

    let timer = Timer(timeInterval: 1.0, repeats: true) { timer in // 定时器触发时执行的操作 } RunLoop.current.add(timer, forMode: .common)

    这将创建一个每隔1秒钟触发一次的定时器,并在公共模式下添加到主线程的RunLoop中。


    在添加定时器时,需要指定它所属的RunLoop模式。如果不指定模式,则默认为NSDefaultRunLoopMode。如果需要在多个模式下都能响应定时器事件,可以使用NSRunLoopCommonModes


    输入源


    输入源(input source)是一种与RunLoop一起使用的机制,用于处理异步事件,例如网络请求、文件读写等等。RunLoop在运行过程中,会检查当前模式下是否有输入源需要处理,如果有则会立即处理。


    输入源可以是一个Port、Socket、CFFileDescriptor等等。要使用输入源,必须将其添加到RunLoop中,并设置回调函数来处理输入事件。


    例如,要在主线程中使用输入源,可以使用如下代码:

    let inputSource = InputSource()
    inputSource.setEventHandler {
    // 输入源触发时执行的操作
    }
    RunLoop.current.add(inputSource, forMode: .common)

    这将创建一个输入源,并在公共模式下添加到主线程的RunLoop中。


    Perform Selector


    Perform Selector是一种调用方法的方式,可以在RunLoop中异步执行某个方法。在调用方法时,可以设置延迟执行时间和RunLoop模式。该方法会在指定的时间间隔内执行,直到被取消。


    例如,要在主线程中使用Perform Selector,可以使用如下代码:

    RunLoop.current.perform(#selector(doSomething), target: self, argument: nil, order: 0, modes: [.default])

    这将在默认模式下异步执行doSomething方法。


    RunLoop的常用操作


    除了上述基本操作之外,RunLoop还提供了其他常用操作,例如:

    1. stop:停止RunLoop的运行。
    2. runUntilDate:运行RunLoop直到指定日期。
    3. runMode:运行RunLoop指定模式下的事件处理循环。
    4. currentMode:获取当前RunLoop的运行模式。

    RunLoop与线程安全


    iOS开发中,多线程是一个常见的问题。RunLoop在处理异步事件时,可能会导致线程不安全的问题。为了保证RunLoop的线程安全,可以使用以下方法:

    1. 使用RunLoopQueue,在队列中使用RunLoop来执行异步操作。
    2. 在主线程中使用RunLoop来处理异步事件,避免跨线程操作。

    结论


    RunLoopiOS开发中非常重要的一个概念,它提供了一种事件循环机制,用于处理各种事件。RunLoop的基本思想是循环地处理事件,当有事件发生时,RunLoop会调用相应的处理函数来处理事件。RunLoop还提供了定时器、输入源、Perform Selector等机制来处理异步事件。了解RunLoop的工作原理,可以帮助我们更好地理解iOS应用的运行机制,避免出现一些奇怪的问题。


    最后,我们再来看一下RunLoop的一些注意事项:

    1. 不要在主线程中阻塞RunLoop,否则会导致UI卡顿。
    2. 避免使用RunLoopNSDefaultRunLoopMode模式,因为这个模式下会处理大量UI事件,可能会导致其他事件无法及时处理。
    3. 在使用RunLoop的过程中,需要注意线程安全问题。

    RunLoop是一种事件循环机制,通过它,我们可以很方便地处理各种事件,避免出现一些奇怪的问题。在日常开发中,我们需要经常使用RunLoop,所以建议大家多多练习,掌握RunLoop的各种用法。


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

    作为一个前端为什么要学习 Rust ?

    这里抛出一个问题 作为一个前端为什么要去学习 Rust ? 这是个好问题,有人可能觉得前端学习 Rust 意义不大,学习成本高,投入产出比低啥的,JavaScript、TypeScript 这些前端语言还没搞明白呢,为什么还要去学一门这么难的新语言? 那么今天...
    继续阅读 »

    这里抛出一个问题


    作为一个前端为什么要去学习 Rust ?


    这是个好问题,有人可能觉得前端学习 Rust 意义不大,学习成本高,投入产出比低啥的,JavaScript、TypeScript 这些前端语言还没搞明白呢,为什么还要去学一门这么难的新语言?


    那么今天我就谈谈我自己对于这个问题的看法~,主要是分为 5 点:

    • 性能
    • 跨平台特性
    • 安全性
    • 职业视野
    • 职业竞争力

    性能


    Rust 可以给 Node.js 提供一个性能逃生通道,当我们使用 Node.js 遇到性能瓶颈或 CPU 密集计算场景的时候,便可以使用 Rust 编写 Native Addon 解决这个问题了,Native Addon 就是一个二进制文件,也就是 xxx.node 文件,比如 swc(对应 babel)、Rspack(对应Webpack)、Rome(对应 eslint、prettier、babel、webpack 等,目标是代替我们所熟悉的所有前端工具链...),上面提到的工具链就都是使用 Rust 编写的,性能比 Node.js 对应功能的包都有了极大的提高,同时 Rust 也是支持多线程的,你编写的多线程代码在 Node.js 中一样可以跑,这就可以解决了 Node.js 不擅长 CPU 密集型的问题。在前端架构领域目前 Rust 已经差不多是标配了,阿里、字节内部的前端基建目前都开始基于 Rust 去重构了。


    跨平台


    可以编写高性能且支持跨平台的 WebAssembly 扩展,可以在浏览器、IOT 嵌入式设备、服务端环境等地方使用,并且也拥有很不错的性能;和上面提到的 Native Addon 不一样, Native Addon 在不同的平台上都需要单独的进行编译,不支持跨平台;但是 WebAssembly 不一样,虽然它的性能没 Native Addon 好,但是跨平台成本很低,我编写的一份代码在 Node.js 中执行没问题,在 Deno 中跑也没问题,在 Java 或者 Go 中跑也都没问题,甚至在单片机也可以运行,只要引入对应的 Wasm 运行时即可。现在 Docker 也已经有 WebAssembly 版本了;同时 Rust 也是目前编写 WebAssembly 最热门的语言,因为它没有垃圾回收,性能高,并且有一个超好用的包管理器 cargo。


    安全


    Rust 编译器真的是事无巨细,它保证你编写的代码不会出低级错误,比如一些类型上的错误和内存分配上的错误,基本上只要 Rust 代码能够编译通过,就可以安心上线,在服务端、操作系统等领域来说这也是个很好的特性,Linux 系统和安卓系统内核都已经开始使用 Rust ,这还信不过嘛?


    视野


    Rust 可以提升自己在服务端领域的视野,Rust 不同于 Node.js 这个使用动态 JS 语言的运行时,它是一门正儿八经的静态编译型编程语言,并且没有垃圾回收,可以让我们掌握和理解计算机的一些底层工作机制,比如内存是如何分配和释放的,Rust 中使用所有权、生命周期等概念来保证内存安全,这对我们对于编程的理解也可以进一步提升,很多人说学习了 Rust 之后对自己编写其它语言的代码也有了更深的理解,毕竟计算机底层的概念都是相通的,开阔自己的编程思维。


    职业竞争力


    这个问题简单,你比别人多一门技能,比如 WebAssembly 和 Native Addon 都可以作为 Node.js 性能优化的一种手段,面试的时候说你会使用 Rust 解决 Node.js 性能问题,这不是比别人多一些竞争力吗?面试官那肯定也会觉得你顶呱呱~ 另外虽然目前 Rust 的工作机会比较少,但是也不代表没有,阿里和字节目前都有关于前端基建的岗位,会 Rust 是加分项,另外 Rust 在 TIOBE 编程语言榜排名中已经冲进了前 20,今年 6 月份是第 20 名,7 月份是第 17 名,流行度开始慢慢上来了,我相信以后工作机会也会越来越多的。


    总结


    不过,总的来说,这还是得看自己个人的学习能力,学有余力的时候可以学习一下 Rust,我自己不是 Rust 吹啊,我学习 Rust 的过程中真的觉得很有趣,因为里面的很多概念在前端领域中都是接触不到的,学了之后真的像是打开了新世界的大门,包括可以去看 Deno 的源码了,可以了解到一个 Js 运行时是怎么进行工作的,这些都是与我们前端息息相关的东西,即使哪天不做前端了,可以去转服务端或嵌入式方向,起码编程语言这一关不需要费多大力气了,Rust 是目前唯一一门从计算机底层到应用层都有落地应用的语言。不多说了,学就完事了,技多不压身嘛


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

    只改了五行代码接口吞吐量提升了10多倍

    背景 公司的一个ToB系统,因为客户使用的也不多,没啥并发要求,就一直没有经过压测。这两天来了一个“大客户”,对并发量提出了要求:核心接口与几个重点使用场景单节点吞吐量要满足最低500/s的要求。 当时一想,500/s吞吐量还不简单。Tomcat按照100个线...
    继续阅读 »

    背景


    公司的一个ToB系统,因为客户使用的也不多,没啥并发要求,就一直没有经过压测。这两天来了一个“大客户”,对并发量提出了要求:核心接口与几个重点使用场景单节点吞吐量要满足最低500/s的要求。


    当时一想,500/s吞吐量还不简单。Tomcat按照100个线程,那就是单线程1S内处理5个请求,200ms处理一个请求即可。这个没有问题,平时接口响应时间大部分都100ms左右,还不是分分钟满足的事情。


    然而压测一开,100 的并发,吞吐量居然只有 50 ...


    image.png


    而且再一查,100的并发,CPU使用率居然接近 80% ...




    从上图可以看到几个重要的信息。


    最小值: 表示我们非并发场景单次接口响应时长。还不足100ms。挺好!


    最大值: 并发场景下,由于各种锁或者其他串行操作,导致部分请求等待时长增加,接口整体响应时间变长。5秒钟。有点过分了!!!


    再一看百分位,大部分的请求响应时间都在4s。无语了!!!


    所以 1s钟的 吞吐量 单节点只有 50 。距离 500 差了10倍。 难受!!!!


    分析过程


    定位“慢”原因



    这里暂时先忽略 CPU 占用率高的问题



    首先平均响应时间这么慢,肯定是有阻塞。先确定阻塞位置。重点检查几处:



    • 锁 (同步锁、分布式锁、数据库锁)

    • 耗时操作 (链接耗时、SQL耗时)


    结合这些先配置耗时埋点。



    1. 接口响应时长统计。超过500ms打印告警日志。

    2. 接口内部远程调用耗时统计。200ms打印告警日志。

    3. Redis访问耗时。超过10ms打印告警日志。

    4. SQL执行耗时。超过100ms打印告警日志。


    上述配置生效后,通过日志排查到接口存在慢SQL。具体SQL类似与这种:


    <!-- 主要类似与库存扣减 每次-1 type 只有有限的几种且该表一共就几条数据(一种一条记录)-->
    <!-- 压测时可以认为 type = 1 是写死的 -->
    update table set field = field - 1 where type = 1 and filed > 1;

    上述SQL相当于并发操作同一条数据,肯定存在锁等待。日志显示此处的等待耗时占接口总耗时 80% 以上。


    二话不说先改为敬。因为是压测环境,直接先改为异步执行,确认一下效果。实际解决方案,感兴趣的可以参考另外一篇文章:大量请求同时修改数据库表一条记录时应该如何设计


    PS:当时心里是这么想的: 妥了,大功告成。就是这里的问题!绝壁是这个原因!优化一下就解决了。当然,如果这么简单就没有必要写这篇文章了...


    优化后的效果:


    image.png


    嗯...


    emm...


    好! 这个优化还是很明显的,提升提升了近2倍。




    此时已经感觉到有些不对了,慢SQL已经解决了(异步了~ 随便吧~ 你执行 10s我也不管了),虽然对吞吐量的提升没有预期的效果。但是数据是不会骗人的。


    最大值: 已经从 5s -> 2s


    百分位值: 4s -> 1s


    这已经是很大的提升了。


    继续定位“慢”的原因


    通过第一阶段的“优化”,我们距离目标近了很多。废话不多说,继续下一步的排查。


    我们继续看日志,此时日志出现类似下边这种情况:


    2023-01-04 15:17:05:347 INFO **.**.**.***.50 [TID: 1s22s72s8ws9w00] **********************
    2023-01-04 15:17:05:348 INFO **.**.**.***.21 [TID: 1s22s72s8ws9w00] **********************
    2023-01-04 15:17:05:350 INFO **.**.**.***.47 [TID: 1s22s72s8ws9w00] **********************

    2023-01-04 15:17:05:465 INFO **.**.**.***.234 [TID: 1s22s72s8ws9w00] **********************
    2023-01-04 15:17:05:467 INFO **.**.**.***.123 [TID: 1s22s72s8ws9w00] **********************

    2023-01-04 15:17:05:581 INFO **.**.**.***.451 [TID: 1s22s72s8ws9w00] **********************

    2023-01-04 15:17:05:702 INFO **.**.**.***.72 [TID: 1s22s72s8ws9w00] **********************

    前三行info日志没有问题,间隔很小。第4 ~ 第5,第6 ~ 第7,第7 ~ 第8 很明显有百毫秒的耗时。检查代码发现,这部分没有任何耗时操作。那么这段时间干什么了呢?



    1. 发生了线程切换,换其他线程执行其他任务了。(线程太多了)

    2. 日志打印太多了,压测5分钟日志量500M。(记得日志打印太多是有很大影响的)

    3. STW。(但是日志还在输出,所以前两种可能性很高,而且一般不会停顿百毫秒)


    按照这三个思路做了以下操作:


    首先,提升日志打印级别到DEBUG。emm... 提升不大,好像增加了10左右。


    然后,拆线程 @Async 注解使用线程池,控制代码线程池数量(之前存在3个线程池,统一配置的核心线程数为100)结合业务,服务总核心线程数控制在50以内,同步增加阻塞最大大小。结果还可以,提升了50,接近200了。


    最后,观察JVM的GC日志,发现YGC频次4/s,没有FGC。1分钟内GC时间不到1s,很明显不是GC问题,不过发现JVM内存太小只有512M,直接给了4G。吞吐量没啥提升,YGC频次降低为2秒1次。


    唉,一顿操作猛如虎。


    PS:其实中间还对数据库参数一通瞎搞,这里不多说了。




    其实也不是没有收获,至少在减少服务线程数量后还是有一定收获的。另外,已经关注到了另外一个点:CPU使用率,减少了线程数量后,CPU的使用率并没有明显的下降,这里是很有问题的,当时认为CPU的使用率主要与开启的线程数量有关,之前线程多,CPU使用率较高可以理解。但是,在砍掉了一大半的线程后,依然居高不下这就很奇怪了。


    此时关注的重点开始从代码“慢”方向转移到“CPU高”方向。


    定位CPU使用率高的原因


    CPU的使用率高,通常与线程数相关肯定是没有问题的。当时对居高不下的原因考虑可能有以下两点:



    1. 有额外的线程存在。

    2. 代码有部分CPU密集操作。


    然后继续一顿操作:



    1. 观察服务活跃线程数。

    2. 观察有无CPU占用率较高线程。


    在观察过程中发现,没有明显CPU占用较高线程。所有线程基本都在10%以内。类似于下图,不过有很多线程。


    image.png


    没有很高就证明大家都很正常,只是多而已...


    此时没有下一步的排查思路了。当时想着,算了打印一下堆栈看看吧,看看到底干了啥~


    在看的过程中发现这段日志:


    "http-nio-6071-exec-9" #82 daemon prio=5 os_prio=0 tid=0x00007fea9aed1000 nid=0x62 runnable [0x00007fe934cf4000]
    java.lang.Thread.State: RUNNABLE
    at org.springframework.core.annotation.AnnotationUtils.getValue(AnnotationUtils.java:1058)
    at org.springframework.aop.aspectj.annotation.AbstractAspectJAdvisorFactory$AspectJAnnotation.resolveExpression(AbstractAspectJAdvisorFactory.java:216)
    at org.springframework.aop.aspectj.annotation.AbstractAspectJAdvisorFactory$AspectJAnnotation.<init>(AbstractAspectJAdvisorFactory.java:197)
    at org.springframework.aop.aspectj.annotation.AbstractAspectJAdvisorFactory.findAnnotation(AbstractAspectJAdvisorFactory.java:147)
    at org.springframework.aop.aspectj.annotation.AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(AbstractAspectJAdvisorFactory.java:135)
    at org.springframework.aop.aspectj.annotation.ReflectiveAspectJAdvisorFactory.getAdvice(ReflectiveAspectJAdvisorFactory.java:244)
    at org.springframework.aop.aspectj.annotation.InstantiationModelAwarePointcutAdvisorImpl.instantiateAdvice(InstantiationModelAwarePointcutAdvisorImpl.java:149)
    at org.springframework.aop.aspectj.annotation.InstantiationModelAwarePointcutAdvisorImpl.<init>(InstantiationModelAwarePointcutAdvisorImpl.java:113)
    at org.springframework.aop.aspectj.annotation.ReflectiveAspectJAdvisorFactory.getAdvisor(ReflectiveAspectJAdvisorFactory.java:213)
    at org.springframework.aop.aspectj.annotation.ReflectiveAspectJAdvisorFactory.getAdvisors(ReflectiveAspectJAdvisorFactory.java:144)
    at org.springframework.aop.aspectj.annotation.BeanFactoryAspectJAdvisorsBuilder.buildAspectJAdvisors(BeanFactoryAspectJAdvisorsBuilder.java:149)
    at org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator.findCandidateAdvisors(AnnotationAwareAspectJAutoProxyCreator.java:95)
    at org.springframework.aop.aspectj.autoproxy.AspectJAwareAdvisorAutoProxyCreator.shouldSkip(AspectJAwareAdvisorAutoProxyCreator.java:101)
    at org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator.wrapIfNecessary(AbstractAutoProxyCreator.java:333)
    at org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator.postProcessAfterInitialization(AbstractAutoProxyCreator.java:291)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsAfterInitialization(AbstractAutowireCapableBeanFactory.java:455)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1808)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:620)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:353)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:233)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveNamedBean(DefaultListableBeanFactory.java:1282)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveNamedBean(DefaultListableBeanFactory.java:1243)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveBean(DefaultListableBeanFactory.java:494)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:349)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:342)
    at cn.hutool.extra.spring.SpringUtil.getBean(SpringUtil.java:117)
    ......
    ......

    上边的堆栈发现了一个点: 在执行getBean的时候,执行了createBean方法。我们都知道Spring托管的Bean都是提前实例化好放在IOC容器中的。createBean要做的事情有很多,比如Bean的初始化,依赖注入其他类,而且中间还有一些前后置处理器执行、代理检查等等,总之是一个耗时方法,所以都是在程序启动时去扫描,加载,完成Bean的初始化。


    而我们在运行程序线程堆栈中发现了这个操作。而且通过检索发现竟然有近200处。


    通过堆栈信息很快定位到执行位置:


    <!--BeanUtils 是 hutool 工具类。也是从IOC容器获取Bean 等价于 @Autowired 注解 -->
    RedisTool redisTool = BeanUtils.getBean(RedisMaster.class);

    而RedisMaster类


    @Component
    @Scope("prototype")
    public class RedisMaster implements IRedisTool {
    // ......
    }

    没错就是用了多例。而且使用的地方是Redis(系统使用Jedis客户端,Jedis并非线程安全,每次使用都需要新的实例),接口对Redis的使用还是比较频繁的,一个接口得有10次左右获取Redis数据。也就是说执行10次左右的createBean逻辑 ...


    叹气!!!


    赶紧改代码,直接使用万能的 new 。


    在看结果之前还有一点需要提一下,由于系统有大量统计耗时的操作。实现方式是通过:


    long start = System.currentTimeMillis();
    // ......
    long end = System.currentTimeMillis();
    long runTime = start - end;


    或者Hutool提供的StopWatch:


    这里感谢一下huoger 同学的评论,当时还误以为该方式能够降低性能的影响,但是实际上也只是一层封装。底层使用的是 System.nanoTime()。


    StopWatch watch = new StopWatch();
    watch.start();
    // ......
    watch.stop();
    System.out.println(watch.getTotalTimeMillis());

    而这种在并发量高的情况下,对性能影响还是比较大的,特别在服务器使用了一些特定时钟的情况下。这里就不多说,感兴趣的可以自行搜索一下。





    最终结果:



    image.png





    排查涉及的命令如下:



    查询服务进程CPU情况: top –Hp pid


    查询JVM GC相关参数:jstat -gc pid 2000 (对 pid [进程号] 每隔 2s 输出一次日志)


    打印当前堆栈信息: jstack -l pid >> stack.log


    总结


    结果是好的,过程是曲折的。总的来说还是知识的欠缺,文章看起来还算顺畅,但都是事后诸葛亮,不对,应该是事后臭皮匠。基本都是边查资料边分析边操作,前后花费了4天时间,尝试了很多。



    • Mysql : Buffer Pool 、Change Buffer 、Redo Log 大小、双一配置...

    • 代码 : 异步执行,线程池参数调整,tomcat 配置,Druid连接池配置...

    • JVM : 内存大小,分配,垃圾收集器都想换...


    总归一通瞎搞,能想到的都试试。


    后续还需要多了解一些性能优化知识,至少要做到排查思路清晰,不瞎搞。




    最后5行代码有哪些:



    1. new Redis实例:1

    2. 耗时统计:3

    3. SQL异步执行 @Async: 1(上图最终的结果是包含该部分的,时间原因未对SQL进行处理,后续会考虑Redis原子操作+定时同步数据库方式来进行,避免同时操数据库)


    TODO


    问题虽然解决了。但是原理还不清楚,需要继续深挖。



    为什么createBean对性能影响这么大?



    如果影响这么大,Spring为什么还要有多例?


    首先非并发场景速度还是很快的。这个毋庸置疑。毕竟接口响应时间不足50ms。


    所以问题一定出在,并发createBean同一对象的锁等待场景。根据堆栈日志,翻了一下Spring源码,果然发现这里出现了同步锁。相信锁肯定不止一处。


    image.png


    org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean


    image.png



    System.currentTimeMillis并发度多少才会对性能产生影响,影响有多大?



    很多公司(包括大厂)在业务代码中,还是会频繁的使用System.currentTimeMillis获取时间戳。比如:时间字段赋值场景。所以,性能影响肯定会有,但是影响的门槛是不是很高。



    继续学习性能优化知识




    • 吞吐量与什么有关?


    首先,接口响应时长。直接影响因素还是接口响应时长,响应时间越短,吞吐量越高。一个接口响应时间100ms,那么1s就能处理10次。


    其次,线程数。现在都是多线程环境,如果同时10个线程处理请求,那么吞吐量又能增加10倍。当然由于CPU资源有限,所以线程数也会受限。理论上,在 CPU 资源利用率较低的场景,调大tomcat线程数,以及并发数,能够有效的提升吞吐量。


    最后,高性能代码。无论接口响应时长,还是 CPU 资源利用率,都依赖于我们的代码,要做高性能的方案设计,以及高性能的代码实现,任重而道远。



    • CPU使用率的高低与哪些因素有关?


    CPU使用率的高低,本质还是由线程数,以及CPU使用时间决定的。


    假如一台10核的机器,运行一个单线程的应用程序。正常这个单线程的应用程序会交给一个CPU核心去运行,此时占用率就是10%。而现在应用程序都是多线程的,因此一个应用程序可能需要全部的CPU核心来执行,此时就会达到100%。


    此外,以单线程应用程序为例,大部分情况下,我们还涉及到访问Redis/Mysql、RPC请求等一些阻塞等待操作,那么CPU就不是时刻在工作的。所以阻塞等待的时间越长,CPU利用率也会越低。也正是因为如此,为了充分的利用CPU资源,多线程也就应运而生(一个线程虽然阻塞了,但是CPU别闲着,赶紧去运行其他的线程)。



    • 一个服务线程数在多少比较合适(算上Tomcat,最终的线程数量是226),执行过程中发现即使tomcat线程数量是100,活跃线程数也很少超过50,整个压测过程基本维持在20
      作者:FishBones
      来源:juejin.cn/post/7185479136599769125
      左右。

    收起阅读 »

    前段时间面试了一些人,有这些槽点跟大家说说

    大家好,我是拭心。 前段时间组里有岗位招人,花了些时间面试,趁着周末把过程中的感悟和槽点总结成文和大家讲讲。 简历书写和自我介绍 今年的竞争很激烈:找工作的人数量比去年多、平均质量比去年高。裸辞的慎重,要做好和好学校、有大厂经历人竞争的准备 去年工...
    继续阅读 »

    大家好,我是拭心。


    前段时间组里有岗位招人,花了些时间面试,趁着周末把过程中的感悟和槽点总结成文和大家讲讲。


    image.png


    简历书写和自我介绍



    1. 今年的竞争很激烈:找工作的人数量比去年多、平均质量比去年高。裸辞的慎重,要做好和好学校、有大厂经历人竞争的准备


    image.png




    1. 去年工作经历都是小公司的还有几个进了面试,今年基本没有,在 HR 第一关就被刷掉了




    2. 这种情况的,一定要走内推,让内推的人跟 HR 打个招呼:这人技术不错,让用人部门看看符不符合要求




    3. 用人部门筛简历也看学历经历,但更关注这几点:过去做了什么项目、项目经验和岗位对不对口、项目的复杂度怎么样、用到的技术栈如何、他在里面是什么角色




    4. 如果项目经历不太出彩,简历上可以补充些学习博客、GitHub,有这两点的简历我都会点开仔细查看,印象分会好很多




    5. 现在基本都视频面试,面试的时候一定要找个安静的环境、体态认真的回答。最好别用手机,否则会让人觉得不尊重!




    6. 我面过两个神人,一个在马路上边走边视频;另一个聊着聊着进了卫生间,坐在马桶上和我讲话(别问我怎么知道在卫生间的,他努力的声音太大了。。。)




    7. 自我介绍要自然一点,别像背课文一样好吗亲。面试官不是考你背诵,是想多了解你一点,就当普通聊天一样自然点




    8. 介绍的时候不要过于细节,讲重点、结果、数据,细节等问了再说




    9. 准备介绍语的时候问问自己,别人可以得到什么有用的信息、亮点能不能让对方快速 get 到




    10. 实在不知道怎么介绍,翻上去看第 4 点和第 5 点




    11. 出于各种原因,很多面试官在面试前没看过你的简历,在你做自我介绍时,他们也在一心二用 快速地浏览你的简历。所以你的自我介绍最好有吸引人的点,否则很容易被忽略




    12. 你可以这样审视自己的简历和自我介绍:


      a. 整体:是否能清晰的介绍你的学历、工作经历和技能擅长点


      b. 工作经历:是否有可以证明你有能力、有结果的案例,能否从中看出你的能力和思考


      c. 技能擅长点:是否有岗位需要的大部分技能,是否有匹配工作年限的复杂能力,是否有区别于其他人的突出点




    面试问题


    image.png




    1. 根据公司规模、岗位级别、面试轮数和面试官风格,面试的问题各有不同,我们可以把它们简单归类为:项目经历、技能知识点和软素质




    2. 一般公司至少有两轮技术面试 + HR 面试,第一轮面试官由比岗位略高一级的人担任,第二轮面试官由用人部门领导担任




    3. 不同轮数考察侧重点不同。第一轮面试主要确认简历真实性和基础技术能力,所以主要会围绕项目经历和技能知识点;第二轮面试则要确认这个人是否适合岗位、团队,所以更偏重过往经历和软素质




    项目经历


    项目经历就是我们过往做过的项目。


    项目经历是最能体现一个程序员能力的部分,因此面试里大部分时间都在聊这个。


    有朋友可能会说:胡说,为什么我的面试大部分时候都是八股文呢?


    大部分都是八股文有两种可能:要么是初级岗位、要么是你的经历没什么好问的。哦还有第三种可能,面试官不知道问什么,从网上搜的题。


    image.png


    在项目经历上,面试者常见的问题有这些:



    1. 不重要的经历占比过多(比如刚毕业的时候做的简单项目花了半页纸)

    2. 经历普通,没有什么亮点(比如都是不知名项目,项目周期短、复杂度低)

    3. 都是同质化的经历,看不出有成长和沉淀(比如都是 CRUD、if visible else gone)


    出现这种情况,是因为我们没有从面试官的角度思考,不知道面试的时候对方都关注什么。


    在看面试者的项目经历时,面试官主要关注这三点:


    1. 之前做的项目有没有难度


    2. 项目经验和当前岗位需要的是否匹配


    3. 经过这些项目,这个人的能力有哪些成长


    因此,我们在日常工作和准备面试时,可以这样做:



    1. 工作时有意识地选择更有复杂度的,虽然可能花的时间更多,但对自己的简历和以后发展都有好处

    2. 主动去解决项目里的问题,解决问题是能力提升的快车道,解决的问题越多、能力会越强

    3. 解决典型的问题后,及时思考问题的本质是什么、如何解决同一类问题、沉淀为文章、记录到简历,这些都是你的亮点

    4. 经常复盘,除了公司要求的复盘,更要做自己的复盘,复盘这段时间里有没有成长

    5. 简历上,要凸显自己在项目面试的挑战、解决的问题,写出自己如何解决的、用到什么技术方案

    6. 投简历时,根据对方业务类型和岗位要求,适当的调整项目经历里的重点,突出匹配的部分

    7. 面试时,要强调自己在项目里的取得的成果、在其中的角色、得到什么可复制的经验


    技能知识点


    技能知识点就是我们掌握的编程语言、技术框架和工具。


    相较于项目经历,技能知识点更关键,因为它决定了面试者是否能够胜任岗位。


    image.png


    在技能知识点方面,面试者常见的问题有这些:



    1. 不胜任岗位:基础不扎实,不熟悉常用库的原理

    2. 技术不对口:没有岗位需要的领域技术

    3. 技术过剩:能力远远超出岗位要求


    第一种情况就是我们常说的“技术不行”。很多人仅仅在工作里遇到不会的才学习,工作多年也没有自己的知识体系,在面试的时候很容易被基础知识点问倒,还给自己找理由说“我是高级开发还问这么细节的,面试官只会八股文”。框架也是浅尝辄止,会用就不再深入学了,这在面试的时候也很容易被问住。


    第二种情况,是岗位工作内容属于细分领域,但面试者不具备这方面的经验,比如音视频、跨端等。为了避免这种情况,我们需要打造自己的细分领域技能,最好有一个擅长的方向,越早越好。


    第三种情况简单的来说就是“太贵了”。有时候一些资深点的开发面试被挂掉,并不是因为你的能力有问题,而是因为岗位的预算有限。大部分业务需求都是增删改查和界面展示,并不需要多复杂的经验。这种情况下,要么再去看看更高级的岗位,要么降低预期。


    在我面试的人里,通过面试的都有这些特点:



    1. 技术扎实:不仅仅基础好,还有深度

    2. 解决过复杂的问题:项目经验里除了完成业务需求,也有做一些有挑战的事


    有些人的简历上只写项目经历不写技能知识点,对此我是反对的,这样做增加了面试官了解你的成本。问项目经历的目的还是想确认你有什么能力,为什么不直接明了的写清楚呢?


    软素质


    这里的「软素质」指面试时考察的、技术以外的点。


    程序员的日常工作里,除了写代码还需要做这些事:



    1. 理解业务的重点和不同需求的核心点,和其他同事协作完成

    2. 从技术角度,对需求提出自己的思考和建议,反馈给其他人

    3. 负责某个具体的业务/方向,成为这个方面所有问题的处理者


    image.png


    因此,面试官或者 HR 还会考察这些点,以确保面试者具备完成以上事情的能力:



    1. 理解能力和沟通表达能力

    2. 业务能力

    3. 稳定性


    第一点是指面试者理解问题和讲清楚答案的能力。遇到过一些面试者,面试的时候过于紧张,讲话都讲不清楚,这种就让人担心“会不会是个社恐”、“工作里该不会也这样说不清楚吧”;还有的人爱抢答,问题都没听明白就开始抢答,让人怀疑是不是性格太急躁太自大;还有的人过于能讲,但讲不到重点,东扯西扯,让人对他的经历和理解能力产生了怀疑。


    第二点是指在实现业务目标的过程中可以提供的能力。 业务发展是需要团队共同努力的,但有的人从来没这么想过,觉得自己上班的任务就是写代码,来什么活干什么活,和外包一样。


    业务发展中可能有各种问题。定方向的领导有时候会过于乐观、跨部门协作项目可能会迟迟推进不动、产品经理有时候也会脑子进水提无用需求、质量保障的测试同学可能会大意漏掉某个细节测试。这个时候,程序员是否能够主动站出来出把力,帮助事情向好的方向发展,就很重要了。


    遇到过一些面试者,在一家公司干了好几年,问起来业务发展情况语焉不详,让人感觉平时只知道写代码;还有的面试者,说起业务问题抱怨指责一大堆,“领导太傻逼”、“产品经理尽提蠢需求”,负能量满满😂。


    第三点是指面试者能不能在一家公司长久干下去。 对于级别越高的人,这点要求就越高,因为他的离开对业务的发展会有直接影响。即使级别不高,频繁换工作也会让人对你有担心:会不会抗压能力很差、会不会一不涨工资就要跑路。一般来说,五年三跳就算是临界线,比这个频繁就算是真的“跳的有点多”。


    针对以上这三点,我们可以这样做:



    1. 面试时调整心态,当作普通交流,就算不会也坦然说出,不必过于紧张

    2. 回答问题时有逻辑条理,可以采用类似总分总的策略

    3. 工作时多关注开发以外的事,多体验公司产品和竞品,在需求评审时不摸鱼、多听听为什么做、思考是否合理、提出自己的想法

    4. 定好自己的职业规划(三年小进步、五年大进步),在每次换工作时都认真问问自己:下一份工作能否帮助自己达到目标


    总结


    好了,这就是我前段时间面试的感悟和吐槽。


    总的来说,今年找工作的人不少,市面上的岗位没有往年那么多。如果你最近要换工作,最好做足准备。做好后面的规划再换、做好准备再投简历、经历整理清楚再面试。

    作者:张拭心
    来源:juejin.cn/post/7261604248319918136

    收起阅读 »

    揭秘:Android屏幕中你不知道的刷新机制

    前言 之前在整理知识的时候,看到android屏幕刷新机制这一块,以前一直只是知道,Android每16.6ms会去刷新一次屏幕,也就是我们常说的60fpx,那么问题也来了: 16.6ms刷新一次是什么一次,是以这个固定的频率去重新绘制吗?但是请求绘制的代码时...
    继续阅读 »

    前言


    之前在整理知识的时候,看到android屏幕刷新机制这一块,以前一直只是知道,Android每16.6ms会去刷新一次屏幕,也就是我们常说的60fpx,那么问题也来了:


    16.6ms刷新一次是什么一次,是以这个固定的频率去重新绘制吗?但是请求绘制的代码时机调用是不同的,如果操作是在16.6ms快结束的时候去绘制的,那么岂不是就是时间少于16.6ms,也会产生丢帧的问题?再者熟悉绘制的朋友都知道请求绘制是一个Message对象,那这个Message是会放进主线程Looper的队列中吗,那怎么能保证在16.6ms之内会执行到这个Message呢?


    View ## invalidate()


    既然是绘制,那么就从这个方法看起吧


    public void invalidate() {
    invalidate(true);
    }
    public void invalidate(boolean invalidateCache) {
    invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
    }
    void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
    boolean fullInvalidate
    ) {
    ......
    final AttachInfo ai = mAttachInfo;
    final ViewParent p = mParent;
    if (p != null && ai != null && l < r && t < b) {
    final Rect damage = ai.mTmpInvalRect;
    damage.set(l, t, r, b);
    p.invalidateChild(this, damage);
    }
    .....
    }
    }

    主要关注这个p,最终调用的是它的invalidateChild()方法,那么这个p到底是个啥,ViewParent是一个接口,那很明显p是一个实现类,答案是ViewRootImpl,我们知道View树的根节点是DecorView,那DecorView的Parent是不是ViewRootImpl呢


    熟悉Activity启动流程的朋友都知道,Activity 的启动是在 ActivityThread 里完成的,handleLaunchActivity() 会依次间接的执行到 Activity 的 onCreate(), onStart(), onResume()。在执行完这些后 ActivityThread 会调用 WindowManager#addView(),而这个 addView()最终其实是调用了 WindowManagerGlobal 的 addView() 方法,我们就从这里开始看,因为是隐藏类,所以这里借助Source Insight查看WindowManagerGlobal


    public void addView(View view, ViewGroup.LayoutParams params,
    Display display, Window parentWindow
    ) {
    synchronized (mLock) {
    .....
    root = new ViewRootImpl(view.getContext(), display);
    view.setLayoutParams(wparams);
    mViews.add(view);
    mRoots.add(root);
    mParams.add(wparams);
    }
    try {
    root.setView(view, wparams, panelParentView);
    } catch (RuntimeException e) {
    // BadTokenException or InvalidDisplayException, clean up.
    synchronized (mLock) {
    final int index = findViewLocked(view, false);
    if (index >= 0) {
    removeViewLocked(index, true);
    }
    }
    throw e;
    }
    }
    public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    synchronized (this) {
    ....
    view.assignParent(this);
    ...
    }
    }
    void assignParent(ViewParent parent) {
    if (mParent == null) {
    mParent = parent;
    } else if (parent == null) {
    mParent = null;
    }
    }

    参数是ViewParent,所以在这里就直接将DecorView和ViewRootImpl给绑定起来了,所以也验证了上述的结论,子 View 里执行 invalidate() 之类的操作,最后都会走到 ViewRootImpl 里来


    ViewRootImpl##scheduleTraversals


    根据上面的链路最终是会执行到scheduleTraversals方法


    void scheduleTraversals() {
    if (!mTraversalScheduled) {
    mTraversalScheduled = true;
    mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
    mChoreographer.postCallback(
    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
    if (!mUnbufferedInputDispatch) {
    scheduleConsumeBatchedInput();
    }
    notifyRendererOfFramePending();
    pokeDrawLockIfNeeded();
    }
    }
    复制代码方法不长,首先如果mTraversalScheduled为false,进入判断,同时将此标志位置位true,第二句暂时不管,后续会讲到,主要看postCallback方法,传递进去了一个mTraversalRunnable对象,可以看到这里是一个请求绘制的Runnable对象
    final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
    doTraversal();
    }
    }
    void doTraversal() {
    if (mTraversalScheduled) {
    mTraversalScheduled = false;
    mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
    if (mProfile) {
    Debug.startMethodTracing("ViewAncestor");
    }
    performTraversals();
    if (mProfile) {
    Debug.stopMethodTracing();
    mProfile = false;
    }
    }
    }

    doTraversal方法里面,又将mTraversalScheduled置位了false,对应上面的scheduleTraversals方法,可以看到一个是postSyncBarrier(),而在这里又是removeSyncBarrier(),这里其实涉及到一个很有意思的东西,叫同步屏障,等会会拉出来单独讲解,然后调用了performTraversals(),这个方法应该都知道了,View 的测量、布局、绘制都是在这个方法里面发起的,代码逻辑太多了,就不贴出来了,暂时只需要知道这个方法是发起测量的开始。


    这里我们暂时总结一下,当子View调用invalidate的时候,最终是调用到ViewRootImpl的performTraversals()方法的,performTraversals()方法又是在doTraversal里面调用的,doTraversal又是封装在mTraversalRunnable之中的,那么这个Runnable的执行时机又在哪呢


    Choreographer##postCallback


    回到上面的scheduleTraversals方法中,mTraversalRunnable是传递进了Choreographer的postCallback方法之中


    private void postCallbackDelayedInternal(int callbackType,
    Object action, Object token, long delayMillis
    ) {
    if (DEBUG_FRAMES) {
    synchronized (mLock) {
    final long now = SystemClock.uptimeMillis();
    final long dueTime = now + delayMillis;
    mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);

    if (dueTime <= now) {
    scheduleFrameLocked(now);
    } else {
    Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
    msg.arg1 = callbackType;
    msg.setAsynchronous(true);
    mHandler.sendMessageAtTime(msg, dueTime);
    }
    }
    }

    可以看到内部好像有一个类似MessageQueue的东西,将Runnable通过delay时间给存储起来的,因为我们这里传递进来的delay是0,所以执行scheduleFrameLocked(now)方法


    private void scheduleFrameLocked(long now) {
    if (!mFrameScheduled) {
    mFrameScheduled = true;
    if (isRunningOnLooperThreadLocked()) {
    scheduleVsyncLocked();
    } else {
    Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
    msg.setAsynchronous(true);
    mHandler.sendMessageAtFrontOfQueue(msg);
    }
    }
    }
    private boolean isRunningOnLooperThreadLocked() {
    return Looper.myLooper() == mLooper;
    }

    这里有一个判断isRunningOnLooperThreadLocked,看着像是判断当前线程是否是主线程,如果是的话,调用scheduleVsyncLocked()方法,不是的话会发送一个MSG_DO_SCHEDULE_VSYNC消息,但是最终都会调用这个方法


    public void scheduleVsync() {
    if (mReceiverPtr == 0) {
    Log.w(TAG, "Attempted to schedule a vertical sync pulse but the display event "
    + "receiver has already been disposed.");
    } else {
    nativeScheduleVsync(mReceiverPtr);
    }
    }

    如果mReceiverPtr不等于0的话,会去调用nativeScheduleVsync(mReceiverPtr),这是个native方法,暂不跟踪到C++里面去了,看着英文方法像是一个安排信号的意思 之前是把CallBack存储在一个Queue之中了,那么必然有执行的方法


    void doCallbacks(int callbackType, long frameTimeNanos) {
    CallbackRecord callbacks;
    synchronized (mLock) {
    try {
    Trace.traceBegin(Trace.TRACE_TAG_VIEW, CALLBACK_TRACE_TITLES[callbackType]);
    for (CallbackRecord c = callbacks; c != null; c = c.next) {
    if (DEBUG_FRAMES) {
    Log.d(TAG, "RunCallback: type=" + callbackType
    + ", action=" + c.action + ", token=" + c.token
    + ", latencyMillis=" + (SystemClock.uptimeMillis() - c.dueTime));
    }
    c.run(frameTimeNanos);
    }
    } finally {
    synchronized (mLock) {
    mCallbacksRunning = false;
    do {
    final CallbackRecord next = callbacks.next;
    recycleCallbackLocked(callbacks);
    callbacks = next;
    } while (callbacks != null);
    }
    Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
    }

    看一下这个方法在哪里调用的,走到了doFrame方法里面


    void doFrame(long frameTimeNanos, int frame) {
    final long startNanos;
    synchronized (mLock) {
    try {
    .....
    mFrameInfo.markInputHandlingStart();
    doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);

    mFrameInfo.markAnimationsStart();
    doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);

    mFrameInfo.markPerformTraversalsStart();
    doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);

    doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
    } finally {
    AnimationUtils.unlockAnimationClock();
    Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
    .....
    }

    那么这样一来,就找到关键的方法了:doFrame(),这个方法里会根据一个时间戳去队列里取任务出来执行,而这个任务就是 ViewRootImpl 封装起来的 doTraversal() 操作,而 doTraversal() 会去调用 performTraversals() 开始根据需要测量、布局、绘制整颗 View 树。所以剩下的问题就是 doFrame() 这个方法在哪里被调用了。


    private final class FrameDisplayEventReceiver extends DisplayEventReceiver
    implements
    Runnable {
    private boolean mHavePendingVsync;
    private long mTimestampNanos;
    private int mFrame;

    public FrameDisplayEventReceiver(Looper looper, int vsyncSource) {
    super(looper, vsyncSource);
    }

    @Override
    public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {
    scheduleVsync();
    return;
    }
    mTimestampNanos = timestampNanos;
    mFrame = frame;
    Message msg = Message.obtain(mHandler, this);
    msg.setAsynchronous(true);
    mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
    }
    @Override
    public void run() {
    mHavePendingVsync = false;
    doFrame(mTimestampNanos, mFrame);
    }
    }

    可以看到,是在onVsync回调中,Message msg = Message.obtain(mHandler, this),传入了this,然后会执行到run方法里面,自然也就执行了doFrame方法,所以最终问题也就来了,这个onVsync()这个是什么时候回调来的,这里查询了网上的一些资料,是这么解释的



    FrameDisplayEventReceiver继承自DisplayEventReceiver接收底层的VSync信号开始处理UI过程。VSync信号由SurfaceFlinger实现并定时发送。FrameDisplayEventReceiver收到信号后,调用onVsync方法组织消息发送到主线程处理。这个消息主要内容就是run方法里面的doFrame了,这里mTimestampNanos是信号到来的时间参数。



    那也就是说,onVsync是底层回调回来的,那也就是每16.6ms,底层会发出一个屏幕刷新的信号,然后会回调到onVsync方法之中,但是有一点很奇怪,底层怎么知道上层是哪个app需要这个信号来刷新呢,结合日常开发,要实现这种一般使用观察者模式,将app本身注册下去,但是好像也没有看到哪里有往底层注册的方法,对了,再次回到上面的那个native方法,nativeScheduleVsync(mReceiverPtr),那么这个方法大体的作用也就是注册监听了,


    同步屏障


    总结下上面的知识,我们调用了 invalidate(),requestLayout(),等之类刷新界面的操作时,并不是马上就会执行这些刷新的操作,而是通过 ViewRootImpl 的 scheduleTraversals() 先向底层注册监听下一个屏幕刷新信号事件,然后等下一个屏幕刷新信号来的时候,才会去通过 performTraversals() 遍历绘制 View 树来执行这些刷新操作。


    那么这样是不是产生一个问题,因为我们知道,平常Handler发送的消息都是同步消息的,也就是Looper会从MessageQueue中不断去取Message对象,一个Message处理完了之后,再去取下一个Message,那么绘制的这个Message如何尽量保证能够在16.6ms之内执行到呢,


    这里就使用到了一个同步屏障的东西,再次回到scheduleTraversals代码


    void scheduleTraversals() {
    if (!mTraversalScheduled) {
    mTraversalScheduled = true;
    mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
    mChoreographer.postCallback(
    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
    if (!mUnbufferedInputDispatch) {
    scheduleConsumeBatchedInput();
    }
    notifyRendererOfFramePending();
    pokeDrawLockIfNeeded();
    }
    }

    mHandler.getLooper().getQueue().postSyncBarrier(),这一句上面没有分析,进入到方法里


    private int postSyncBarrier(long when) {
    synchronized (this) {
    final int token = mNextBarrierToken++;
    final Message msg = Message.obtain();
    msg.markInUse();
    msg.when = when;
    msg.arg1 = token;
    Message prev = null;
    Message p = mMessages;
    if (when != 0) {
    while (p != null && p.when <= when) {
    prev = p;
    p = p.next;
    }
    }
    if (prev != null) { // invariant: p == prev.next
    msg.next = p;
    prev.next = msg;
    } else {
    msg.next = p;
    mMessages = msg;
    }
    return token;
    }
    }

    可以看到,就是生成Message对象,然后进一些赋值,但是细心的朋友可能会发现,这个msg为什么没有设置target,熟悉Handler流程的朋友应该清楚,最终消息是通过msg.target发送出去的,一般是指Handler对象


    那我们再次回到MessageQueue的next方法中看看


    Message next() {
    for (;;) {
    ....
    synchronized (this) {
    ...
    //对,就是这里了,target==null
    if (msg != null && msg.target == null) {
    do {
    prevMsg = msg;
    msg = msg.next;
    } while (msg != null && !msg.isAsynchronous());
    }
    if (msg != null) {
    if (now < msg.when) {
    nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
    } else {
    // Got a message.
    mBlocked = false;
    if (prevMsg != null) {
    prevMsg.next = msg.next;
    } else {
    mMessages = msg.next;
    }
    msg.next = null;
    if (DEBUG) Log.v(TAG, "Returning message: " + msg);
    msg.markInUse();
    return msg;
    }
    } else {
    nextPollTimeoutMillis = -1;
    }
    }
    }

    可以看到有一个Message.target==null的判断, do while循环遍历消息链表,跳出循环时,msg指向离表头最近的一个消息,此时返回msg对象


    可以看到,当设置了同步屏障之后,next函数将会忽略所有的同步消息,返回异步消息。换句话说就是,设置了同步屏障之后,Handler只会处理异步消息。再换句话说,同步屏障为Handler消息机制增加了一种简单的优先级机制,异步消息的优先级要高于同步消息


    这样的话,能够尽快的将绘制的Message给取出来执行,嗯,这里为什么说是尽快呢,因为,同步屏障是在 scheduleTraversals() 被调用时才发送到消息队列里的,也就是说,只有当某个 View 发起了刷新请求时,在这个时刻后面的同步消息才会被拦截掉。如果在 scheduleTraversals() 之前就发送到消息队列里的工作仍然会按顺序依次被取出来执行

    作者:花海blog
    来源:juejin.cn/post/7267528065809907727

    收起阅读 »

    毕业两年,我的生活发生了翻天覆地的变化

    去年从大学毕业的我大概没想到自己仅仅毕业两年,生活、心态会发生这么多变化吧。 我学会接受了自己的平凡 大概大家都做过这样的梦吧,毕业我要月入几万,我要做职场最靓的仔,我要去大城市闯荡出属于自己的天地。结果这几年经济状况不太好,很多企业都在裁员,校招名额明显减...
    继续阅读 »

    去年从大学毕业的我大概没想到自己仅仅毕业两年,生活、心态会发生这么多变化吧。


    c129b212a0f596a998f904adaf8772c.jpg
    我学会接受了自己的平凡


    大概大家都做过这样的梦吧,毕业我要月入几万,我要做职场最靓的仔,我要去大城市闯荡出属于自己的天地。结果这几年经济状况不太好,很多企业都在裁员,校招名额明显减少,于是校招、找工作的时候就认清现实,好像是个offer就去。


    毕业后,我从中选了最满意的一个offer,前往深圳。


    在公司的一年,我浑浑噩噩,每天感觉时间像不够用似的。每天升级打怪,学到了很多,后面因为公司业务原因,跳槽了。但是很开心的是在这里认识到了很多小伙伴,大家现在也有联系,时不时出来喝个酒。也和公司技术很牛的大佬成了朋友。也学会接受了自己的平凡,原来真的有那种写代码很轻松,把写代码当游戏,把工作当乐趣的人呀,真的是降维打击我这个小菜鸡。


    78840c794394308c40b286f3321073b.jpg


    人生就是不断的坍塌,然后自我重建。


    一年过去了,我好像没了刚出社会的冲劲,偶尔下班也会学习,但是没有像刚毕业一样有很多的学习热情,闲暇时间就会多刷技术贴,技术文章。


    跳槽跑路了,结果我发现我从刀山跑到了火海。入职后我才知道我所在部门的前端再过几天就要跑路了,相当于就我一个啥也不熟悉的来接锅了。组长带人和我交接时用了两小时,然后留下一脸蒙蔽的我。总之,后面度过了艰难的两个月,好歹算是背着锅缓慢前行。后面公司又出了不少幺蛾子,挺了7个月,忍不了又跑路了。但是这几个月吃的苦也让我的工作能力上涨,技能增多,抗压能力增强。于是我发现“人生就是不断的坍塌,然后自我重建,最后堆成了现在的我”。


    相亲也不是不行


    话说我年纪也不大,但是不知道为什么毕业两年,时间飞逝,居然就开始有点年龄焦虑。工作后也没什么渠道去认识女孩子。办公室一屋子男生,问他们对象哪儿来的,都说自己的对象是new来的。


    也不是没被家里人拉去相亲过,第一年我觉得自己还小,也考虑到在家乡相亲的岂不是要异地啊,无比抗拒。第二年我成熟了,(不是,被毒打了)发现工作后是真难找对象啊。


    转折点在某个风和日丽的下午,大数据都知道我单身了。我刷脉脉看到了相亲贴,然后知道了大厂相亲角公众号这个平台,这个公众号标榜用户都是阿里、字节、百度、腾讯、华为等大厂的单身同学。因为注册需要企业认证,最开始不太信任平台,就没注册。先进了他们的单身群观望,后面群里面每天都发一些嘉宾推荐,然后想到这种有门槛的,用户都是经过审核的,岂不是更可靠。感觉确实还蛮靠谱的,于是就注册了。被拉到上墙群后发现上墙群里的群友们都好优秀,小厂的我夹缝求生。


    我算是发现了,人的观念是会一直变的,想当初我怎么也想不到自己会去相亲吧。


    66b61a33c9903edc332893e26b27945.jpg


    总结


    毕业两年,我的生活好像变了很多,又好像没啥变化,曾经我不能接受的,现在又行了。曾经觉得自己可以了,现在也认清

    作者:苍苍尔
    来源:juejin.cn/post/7158708534471819278
    现实了。哈哈哈哈哈。

    收起阅读 »

    卸下if-else 侠的皮衣!- 状态模式

    web
    🤭当我是if-else侠的时候 😶怕出错 给我一个功能,我总是要写很多if-else,虽然能跑,但是维护起来确实很难受,每次都要在一个方法里面增加逻辑,生怕搞错,要是涉及到支付功能,分分钟炸锅 😑难调试 我总是不知道之前写的逻辑在哪里,一个方法几百行逻辑,而且...
    继续阅读 »

    🤭当我是if-else侠的时候


    😶怕出错


    给我一个功能,我总是要写很多if-else,虽然能跑,但是维护起来确实很难受,每次都要在一个方法里面增加逻辑,生怕搞错,要是涉及到支付功能,分分钟炸锅


    😑难调试


    我总是不知道之前写的逻辑在哪里,一个方法几百行逻辑,而且是不同功能点冗余在一起!这可能让我牺牲大量时间在这查找调试中


    🤨交接容易挨打


    当你交接给新同事的时候,这个要做好新同事的白眼和嘲讽,这代码简直是坨翔!这代码简直是个易碎的玻璃,一碰就碎!这代码简直是个世界十大奇迹!


    🤔脱下if-else侠的皮衣


    先学习下开发的设计原则


    单一职责原则(SRP)



    就一个类而言,应该仅有一个引起他变化的原因



    开放封闭原则(ASD)



    类、模块、函数等等应该是可以扩展的,但是不可以修改的



    里氏替换原则(LSP)



    所有引用基类的地方必须透明地使用其子类的对象



    依赖倒置原则(DIP)



    高层模块不应该依赖底层模块



    迪米特原则(LOD)



    一个软件实体应当尽可能的少与其他实体发生相互作用



    接口隔离原则(ISP)



    一个类对另一个类的依赖应该建立在最小的接口上



    在学习下设计模式


    大致可以分三大类:创建型结构型行为型

    创建型:工厂模式 ,单例模式,原型模式

    结构型:装饰器模式,适配器模式,代理模式

    行为型:策略模式,状态模式,观察者模式


    之前文章学习了**策略模式适配器模式,有兴趣可以过去看看,下面我们来学习适配器模式**


    场景:做四种咖啡的咖啡机


    - 美式咖啡(american):只吐黑咖啡
    - 普通拿铁(latte):黑咖啡加点奶
    - 香草拿铁(vanillaLatte):黑咖啡加点奶再加香草糖浆
    - 摩卡咖啡(mocha):黑咖啡加点奶再加点巧克力


    用if-else来写,如下


    class CoffeeMaker {
    constructor() {
    /**
    这里略去咖啡机中与咖啡状态切换无关的一些初始化逻辑
    **/

    // 初始化状态,没有切换任何咖啡模式
    this.state = 'init';
    }

    // 关注咖啡机状态切换函数
    changeState(state) {
    // 记录当前状态
    this.state = state;
    if(state === 'american') {
    // 这里用 console 代指咖啡制作流程的业务逻辑
    console.log('我只吐黑咖啡');
    } else if(state === 'latte') {
    console.log(`给黑咖啡加点奶`);
    } else if(state === 'vanillaLatte') {
    console.log('黑咖啡加点奶再加香草糖浆');
    } else if(state === 'mocha') {
    console.log('黑咖啡加点奶再加点巧克力');
    }
    }
    }


    分下下问题,假如我们多了几种格式,我们又要在这个方法体里面扩展,违反了开放封闭原则(ASD),所以我们现在来采用适配器模式来写下:


    class CoffeeMaker {
    constructor() {
    // 初始化状态,没有切换任何咖啡模式
    this.state = 'init';
    // 初始化牛奶的存储量
    this.leftMilk = '500';
    }
    stateToProcessor = {
    that: this,
    american() {
    this.that.leftMilk = this.that.leftMilk - 100
    console.log('咖啡机现在的牛奶存储量是:', this.that.leftMilk)
    console.log('吐黑咖啡');
    },
    latte() {
    this.american()
    console.log('加点奶');
    },
    vanillaLatte() {
    this.latte();
    console.log('再加香草糖浆');
    },
    mocha() {
    this.latte();
    console.log('再加巧克力');
    }
    }

    changeState(state) {
    this.state = state;
    if (!this.stateToProcessor[state]) {
    return;
    }
    this.stateToProcessor[state]();
    }
    }

    const mk = new CoffeeMaker();
    mk.changeState('latte');
    mk.changeState('mocha');



    这个状态模式实际上跟策略模式很像,但是状态模式会关注里面的状态变化,就像上诉代码能检测咖啡牛奶量,去除了if-else,能很好的扩展维护



    结尾


    遵守设计规则,脱掉if-else的皮衣,善用设计模式,加油,骚年们!给我点点赞,关注下!

    作者:向乾看
    来源:juejin.cn/post/7267207014382829579

    收起阅读 »

    老板搞这些事情降低我写码生产力,我把他开除了

    web
    Hi 大家好我是 ssh,在工作的时候,我经常会感觉到很头疼,觉得自己的时间被各种各样的事情浪费了,可能是没用的会议,写得很垃圾的文档,各种为了达到某些目的而做的 Code Review 和工程化建设等等,正巧看到这篇 Top 7 Things That Ki...
    继续阅读 »

    Hi 大家好我是 ssh,在工作的时候,我经常会感觉到很头疼,觉得自己的时间被各种各样的事情浪费了,可能是没用的会议,写得很垃圾的文档,各种为了达到某些目的而做的 Code Review 和工程化建设等等,正巧看到这篇 Top 7 Things That Kill Developer Productivity,不敢私藏干货,赶紧来分享给大家,希望能共同避免



    简介


    几周前,我突然发现:在工作 4 小时内,我的工作时间和有效的编码时间差了整整 2 小时。为了重回正轨,我决定主动减少阻碍,来缩小这个差距,争取能写更多代码,把无关的事情抛开。这个时间差越大,我的效率就越低。


    和其他行业的人相比,程序员在这方面遇到的困境更甚。这些障碍往往会导致码农信心下降、写代码和优化的时间变少,职业倦怠率更高。影响创造力和热情。


    根据本周 CodeTime 的全球平均编码时间,约 45%的总编码时间都是消极编码。时间和资金都在被浪费。


    低效的开发流程是罪魁祸首。


    1. 会议


    会议


    低效的会议是导致开发人员生产力下降的最不必要的因素之一。编程需要心流。平均而言,进入心流状态大约需要 30 分钟。但是由于乱七八糟会议,专注力就被打断了,码农必须重复这个过程。


    有时 10 分钟的会议硬拖到一个小时,这也很浪费时间。减少了用于实际编程和解决问题的时间。有些会议还需要码农无用的出席。如果这次会议和码农的专业知识无关,根本没必要让他们参会。


    2. 技术债(Fix it Later)


    技术债


    技术债,简单来说就是“以后再修”的心态。先采用捷径实现,妄图后面有空再修改成更优的方式。


    最开始,先让功能可用就行,而优化留到以后。短期来看这可能行得通,因为它可以加快项目进度,你可能在 deadline 前完成。但是反复这么做就会留下大量待完成的工作。会使维护、扩展和优化软件变得更加困难。


    技术债会以多种方式阻碍码农的生产力。列举一些:




    • Code Review 的瓶颈:当技术债增加时,会增加 Code Review 所花费的时间。




    • 更多 Bug:由于关注点都在速度而不是优化上,会导致引入隐藏的 Bug。




    • 代码质量降低:只为了让它可以跑,会导致糟糕的代码质量、随意的 Code Review,甚至没有代码注释,随意乱写复杂的代码等。




    上述所有点都需要额外的时间来处理。因此,这会拖长项目的时间线。


    3. Code Review


    Code Review


    Code Review 需要时间,如果 Review 时间过长,会延迟新代码的集成并放缓整个开发过程。有时候码农提出 PR 但 Code Reviewer 没有时间进行审查。会码农处理下一任务的时间。在进行下一个任务的同时,再回头 Code Review 时会有上下文切换。会影响码农的专注力和生产力。


    对于 Code Review,码农可能不得不参加多个会议,减少了码农的生产力。代码反馈往往不明确或过于复杂,需要进一步讨论来加深理解,解决问题通常需要更长时间。Code Review 对一个组织来说必不可少且至关重要,但是需要注意方式和效率。


    4. 微观管理 (Micromanagement)(缺乏自治)


    微观管理


    微观管理是一种管理方式,主管密切观察和管理下属的工作。在码农的语境下,当管理者想要控制码农的所有编码细节时就发生了。这可能导致码农对他们的代码、流程、决策和创造力的降低。


    举例来说:




    • 缺乏动力:微观管理可能表明组织对码农能力的信任不足。这样,码农很容易感到失去动力。




    • 缺乏创造力:开发软件是一项需要专注以探索创造性解决方案的创作性任务。但是微观管理会导致码农对代码的控制较少,阻碍码农的创造力。




    • 决策缓慢:码农必须就简单的决定向管理层寻求确认,在这个过程中大量时间被浪费。




    在所有这些情况下,码农的生产力都会下降。


    5. 职业倦怠


    职业倦怠


    职业倦怠是码农面临的主要问题之一。面对复杂具有挑战性的项目和紧迫的 deadline,以及不断提高代码质量的压力都可能导致职业倦怠。这最终会导致码农的生产力下降,会显著减弱码农的注意力和写代码的能力。


    这也会导致码农的创造力和解决问题的能力下降。这最终会导致开发周期变慢。


    6. 垃圾文档


    垃圾文档


    文档对码农至关重要,因为它传达有关代码、项目和流程的关键信息。垃圾文档可能会导致开发周期被延迟,因为码农需要花更多时间试图理解代码库、项目和流程。这会导致码农生产力降低。


    在码农入职期间,提供垃圾文档会导致码农在设置项目、管理环境、理解代码等任务上花费更多时间。在缺乏清晰文档的情况下,维护和修改现有代码变得困难。由于担心破坏功能,码农可能会犹豫重构或进行更改。因此,码农的生产力将浪费在非核心任务上。


    7. 痴心妄想的 Deadline


    痴心妄想的Deadline


    Deadline 是使码农发疯的原因之一。你必须在较短的时间窗口内完成大量工作时,你会很容易感到沮丧。这可能导致职业倦怠、代码质量差、疏忽 Code Review 等。这将导致技术债的积累。因此,码农的生产力会下降。


    Deadline 对计划开发周期是必要的,但是通过设置不切实际的 Deadline 来向码农施加压力,会让他们承受压力。在压力下,整体生产力和代码质量都会下降。


    总结


    上文提到的会议、技术债积累、拖沓的 Code Review、微观管理、导致职业倦怠的压力、垃圾代码文档以及为项目设置不切实际的 Deadline 等因素会阻碍码农的生产力。我试图阐明软件开发人员在追求效率和创造性解决方案的过程中面临的挑战。


    其中的重点是,这些挑战是可以通过在码农的健康和高生产力之间建立平衡来克服的。你可以使用一些码农工具来帮助管理你的生产力、深度专注和工作效率。


    下面是一些可以帮助提高生产力的工具:




    • FocusGuard:这是一个 Chrome 扩展,可以通过屏蔽网站帮助你保持专注。




    • Code Time:这是一个 VSCode 扩展,用于跟踪你的编码时间和活动编码时间。




    • JavaScript Booster:这个 VSCode 扩展可以为代码重构提供建议。你也给其他编程语言找找这种扩展。




    • Hatica:虽然上述工具局限于一个任务:专注于编码,但 Hatica 可以处理更多工作。它通过改进工作流程、识别瓶颈和跟踪进度来帮助工程团队提高码农生产力。通过这种方式,它可以给码农节约大量的时间。在这里了解更多关于这个工程管理平台的信息。





    作者:ssh_晨曦时梦见兮

    来源:juejin.cn/post/7267578376050114614


    收起阅读 »

    拿到优秀员工了

    大家好呀,我是楼仔。 上周拿了个优秀员工,又收获了一枚证书,刚好抽屉里也塞了几个,发现装不下了,准备打包一并带回家。 收拾的时候,旁边的同事看到了,“楼哥,你咋这么多证书,是准备集齐后,召唤神龙么?” 尼玛,他这句话,差点让我笑出声。 后来发现,来小米已经 ...
    继续阅读 »

    大家好呀,我是楼仔。


    上周拿了个优秀员工,又收获了一枚证书,刚好抽屉里也塞了几个,发现装不下了,准备打包一并带回家。


    收拾的时候,旁边的同事看到了,“楼哥,你咋这么多证书,是准备集齐后,召唤神龙么?”


    尼玛,他这句话,差点让我笑出声。



    后来发现,来小米已经 4 年了,刚好一年一个,中间也换了几个部门,但是发现每个部门都能做得还不错,这个其实挺难的,下面就给大家简单分享一下个人经验。


    可以再好一点


    我们有个小群,除了我是菜鸡以外,其余基本都是大佬,有一次有个大佬在群里问 “大家觉得,在公司工作,什么是你觉得最重要的呢?”


    有同学回答是技术,也有回答情绪管理,但是有一位同学的回答,让我印象深刻,就三个字“超预期”。


    是的,很多同学都能把事情做到 70 分,甚至 80 分,但是如何才能做到让同事、老板对你印象深刻呢,答案就是超预期。


    比如你是一个核心研发,写的代码质量高,甚至可以提前完成,完全不用老板操心,那么就能到 80 分,但是如果你还能带领大家一起把事情做好,慢慢成为项目的领头羊,那么这就是“超预期”。


    当然,我们不可能所有事情都做到超预期,甚至我们也不知道有些事情是否能超预期,但是只要我们对自己高要求,反复琢磨,如何才能做得更好一点点,首先让自己满意,然后重要事情能超预期,就非常棒了。


    打破边界,持续学习


    偶尔让别人觉得你做得好,会比较容易,如果一直让别人觉得你做得好,就很难了。


    我举个例子,比如今年你是核心研发,但是别人发现,你居然能把项目管理也做得很好,别人对你投来赞许的目光。


    但是如果到了明年,你还是能把项目管理做得很好,可能也还好,但是如果过了 2-3 年呢,别人感觉你项目管理还不错,但是还会投来赞许的目光么?


    这个就叫边际递减效应,就好比吃苹果,吃第一个苹果,感觉很好吃,但是当你一次性吃到第 6 个苹果时,你可能就觉得没那么好吃的。


    所以工作也一样,你需要突破自己的边界,还是拿项目管理举例,比如我今年把项目管理做好,但是明年,我沉淀了一些项目管理的方法论,然后对部门进行培训,并指导他人如何进行项目管理,是不是又更进一步了?


    那如何才能突破自己的边界呢?答案就是持续学习,不断突破自己。


    当然,这个学习,肯定不是漫无目的地学习,你需要知道自己的短板,以及你岗位上限需要具备的技能,试探自己的边界,然后有针对地学习。


    年龄焦虑


    其实做计算机这一行,无论你是研发,还是产品、测试,很多同学都有年龄焦虑,这个很正常。


    当你在公司,如果一直原地踏步,哪怕你现在表现得很好,但是当你年纪更大时,你具备的技能和你的年龄不匹配,当公司进行人员优化时,你可能就很危险了。


    比如小王在公司里面,每天老老实实敲代码,兢兢业业,平时连假都不会请,这工作劲,是不是连他自己都感动?


    当小王到了 40 岁,如果还是只会写代码,哪怕他代码水平一流,每天还是兢兢业业,公司如果要进行人员优化,你觉得他安全么?


    所以当你的能力,能匹配上你的年龄,我想就不会那么焦虑了。


    可能有同学会和我杠,那我到 40 多岁,然后被裁了呢?如果你到 40 多岁,都还没完成自己的财富积累,还没有提前准备好自己的退路,那真不能怪别人了。


    ........................................................


    二哥上周六来武汉了,我和小灰给二哥接风,带着二哥的老婆和孩子,溜达了一整天。


    小朋友没看过企鹅,就去了海洋世界,二哥不喜欢露脸,那就狗头伺候(二哥看到会不会打我),年龄虽然大了,但是精气神还是要有的。


    大家猜猜,最右边的小伙伴是谁呢?


    作者:楼仔
    来源:juejin.cn/post/7266265543412793398

    收起阅读 »

    24岁 我裸辞啦😀

    21年-22年间快速发展 23年-至今停滞不前 1.引言👺     交了辞呈估计有半个月了,自从我把钉钉状态改成last date 08-24之后清静了许多,现在坐在工位上突然回想起刚入职那会,罢了罢了,索性记录一下2年开发的心路历程,以纪念我咋从腼腆小白到相...
    继续阅读 »

    21年-22年间快速发展

    23年-至今停滞不前


    1.引言👺


        交了辞呈估计有半个月了,自从我把钉钉状态改成last date 08-24之后清静了许多,现在坐在工位上突然回想起刚入职那会,罢了罢了,索性记录一下2年开发的心路历程,以纪念我咋从腼腆小白相由薪生 怨气深重的打工🐶


    image.png


    2.工作背景💻︎


        事情还得从今年年初说起,那时候刚接手负责省厅的项目的研发,以前都是做自研或者市级的项目,作为一个小菜鸡内心还是有些(很大的)雀跃。参与需求调研=>了解用户以前的核心业务=>技术选型分析=>项目从零搭建到一期部署上线,都给我带来了很大的成就感。

        成就感的buff加持坚持不了多久,日渐疲惫,😇加上一直带我的领导跳槽之后暴露的问题越来越多由于需求改动很大且频繁,加上非gis专业,边自学边开发,从三月份开始了每日加班生活。

         简单来说就是:



    • 一个星期重做1-2版设计(没办法预测,省厅主任一句话不喜欢就得全部推翻)

    • 由于需要用到数广提供的平台部署生产环境(更新一次最少一个小时),实际开发时间为1-1.5天(包含推送、部署上线、测试)

    • 一周演示2次,通常是:演示(一天)=>修改/开发(1-2天)=>演示(1天)=>反复修改(1-2天)=>演示

    • 原型跟UI跟开发几乎是同步修改的

    • 开发替实施背锅


    01b6054e850b3282dc1d12e3fea39f1.jpg


    衍生出一系列问题可想而知,实施人员与产品没办法把握甲方要的效果,加上能拒绝不合理需求的领导走了,实施人员没有开发经验,实施运维的活推给我们开发做,我与她沟通也较为困难(当前业务如果这样做的话一定会被要求重做的、时间赶不及等等),我能做的只有反馈2个人给我这边一起开发,捋清楚需求、接口再分模块派任务等等。

        本来是我一个人骂骂咧咧😶‍,后来参与进来的全部人都在骂骂咧咧。。。

        第一次跟同事(实施项目经理)吵架:(因为她是女孩子,另一个经理让我们多理解包容一下。。。后面我实在忍不住了。。。🧐)


    63641c20d9110e8b78474d6520a35c4.jpg


    周五园区发通知说周六停电一天,我本以为可以好好休息,周末刷刷剧之类的,因为周四刚演示(前几天都加班到很晚)真的很累,下午的时候实施人员开完会说出新需求,周一前上线,领导就一句话让我们几个今晚扛电脑去另一个园区把项目赶出来(我们加班是没有加班费的,只有周末加班能调休,调休假期很难批)


    昨天加班到一两点的同事们受不了了。。。开始抗议🤯


    661f841e113699854488b618d8e8614.jpg

    然后公司大领导出面开导我们了。。。


    image.png

    省流版:



    • 大家都在加班 研发突然不想加班 有点不负责任

    • 无加班费,无餐补无打车费,但是加班超过晚上八点半 早上可以晚半个钟打卡

    • 不涨工资,收益好的话(我们公司规模蛮大的,1000人+,绩效都是按项目统一定好的)绩效应该能多个几十块


    3.摆脱内耗👋


    看着领导发的话,有点内疚,我知道我能力不足,很多时候没能统筹好,理解业务起来也很慢,效率不高,还拉着同组同事一起加班,我下班回到家躺在床上望着天花板发呆,房间到是很安静,脑子很吵


    是不是我不够认真 想的不够周全


    是不是我太矫情了 加点班就不舒服


    是不是我太敏感了 被人说两句就不得劲


    想了很多下次我要安排好时间,动态设计参数等等等


    后来躲在被窝偷偷哭了很久 给我妈打电话 我妈说:人家多少工资 你多少工资啊一起担责


    嗯???啊???对啊 。。。我突然脑子短路。。。害得我哭饿了吃了一份烤鸡 两个冰淇淋 一份红豆糖水才缓过来,啊。。。就这样结束了我的内耗


    image.png


    想通了,然后我就在公司里开启了“一分钱一分货”的上班原则


    PS:我是来赚钱的,两年没涨薪了,我的能力值得最少工资有百分之20%的涨幅,加班有加班费也是合理的(这句话听起来怪怪的),加上gis行业就业面窄,好领导还走了,加上还要出差,那也是时候换个更好的平台了


    总之很心累


    至于我。。。那晚没有在群里说话,而是第二天直接交了辞呈,表示周六要去复诊,以后也不加班了

    然后周一被各个领导约去谈谈心,以前部门的同事也问我是不是受啥委屈了


    和领导谈的时候,他们都只是表示会争取涨薪跟绩效。我没有聊太多,提出需要涨基本工资而不是绩效后没有明确的答复,还是决定裸辞。
    613827a4b1e752a43404146dd4c914c.jpg


    4.初入职场回忆


         21年入职这里,对于刚毕业的想蹲在广州的小菜鸡来说还是一份很体面的工作,面试的时候有些题没答上来,面试官(也是一直带我的前辈/领导)鼓励我按照自己的思路解答(我觉得当时的回答不沾边),感觉这个领导温柔又靠谱就选择了这里。


         该说不说,新哥就像老父亲一样💙


    30755cdfe988e8ec4a4506175fdc7e8.jpg


    团队里的各位靓仔美女前辈一直以来也对我很包容


    3bb53172ffb7eecca3022c9c8b7b3a9.png
    2a19a340e763803ba1ca303d084ca37.png
    76ddce2a0bfba9a92356979338cd35c.png


    那时候我的猪猪好像跟我一样 也在闪闪发光


    4a0b8489318c5021c6f7ef127d305a0.jpg


    055b0561e9ccbb74b9130d7df66360c.jpg


    5.入行契机


        大学念的软件工程专业,虽然读书的时候java学不会C#看不懂但还是莫名觉得会留在互联网行业,
    写写代码或者做剪辑。

        由于我的人设是个社恐话痨,虽是个吹水佬但不想做售前或营销之类(每天需要接触很多客户的)岗位,加上做前端有种“QQ秀”的吸引力,OK~决定往前端发展。

        其实真正开始学习前端是从毕业前的六个月(培训班太贵了,就在B站自学,主要还是我这个人比较抠),一直到现在仍然觉得女生做技术也很酷,环境也相对包容,与同事间的病情交流也十分和睦,头发还十分茂盛😎。


    687b65bbb8e136be27fc8cd82044220.jpg


    6.最后(关于未来)


    目前即将成为无业游民,有合适的岗位积极争取,也做好了几个月待业的准备,嘎嘎刷题不焦虑。




    • 先出去旅游散散心,或者逛逛广州的博物馆 尝尝美食,吃好喝好睡好 给自己充充电




    • 做更详细的职业规划:



      • 继续做前端

      • 转岗产品 or 数据分析岗

      • 前端=>逐渐转产品 / 管理岗




    • 坐标广州,或许各位大佬可以给我一点建议(听劝!!🤟)或者内推~感激不尽😁




    • 最后的最后,躺平计划尚未成功,还需继

      作者:慌张的葡萄
      来源:juejin.cn/post/7267496335845163068
      续努力~



    收起阅读 »

    大佬都在用的 IDE 主题,21k Star!

    大家好,我是 Zhero! 作为程序员,在我们的日常工作中,会花费大量时间在编码和阅读代码上。为了减轻眼睛的疲劳并提高工作效率,选择一个优秀的代码编辑器主题非常重要。今天我向大家推荐一款备受欢迎的 JetBrains IDE 主题插件 - Dracula。它采...
    继续阅读 »

    大家好,我是 Zhero!


    作为程序员,在我们的日常工作中,会花费大量时间在编码和阅读代码上。为了减轻眼睛的疲劳并提高工作效率,选择一个优秀的代码编辑器主题非常重要。今天我向大家推荐一款备受欢迎的 JetBrains IDE 主题插件 - Dracula。它采用深色调和高对比度的设计风格,成为黑暗系编程主题的杰出代表。Dracula 主题的界面清晰简洁,代码高亮显示明确鲜明,使得代码结构更加清晰易读。使用 Dracula 主题不仅能减少眼睛的疲劳,还能让我们更专注于代码的编写和理解。如果你正在寻找一个优秀的代码编辑器主题,不妨给 Dracula 一试,相信它会给你带来全新的编程体验。


    来源



    Dracula 主题源自于一种热门的色彩风格,也被称为“Dracula”。它最初由 Zeno Rocha 在 TextMate 编辑器上设计和实现。随着其受欢迎程度的不断增加,Dracula Color Scheme 成为一个跨平台的开源项目,并得到了许多编辑器和 IDE 的支持。


    JetBrains 公司注意到了 Dracula 这种深色调和高对比度的设计,并将其引入了他们的 IDE 产品线。现在,IntelliJ IDEA、PyCharm、WebStorm 等 JetBrains 的 IDE 都提供了官方支持的 Dracula 主题插件。这款黑暗炫彩的主题受到了广大程序员的喜爱,成为了他们工作中常用的选择之一。无论是在日常编码还是阅读代码时,Dracula 主题都能为程序员带来舒适的使用体验。


    设计风格



    Dracula 主题的设计具有以下魅力:

  • 深邃紫罗兰色基调: Dracula 的主题采用深邃的紫罗兰色作为基调,给人一种神秘而吸引人的感觉。
    1. 高对比度的前景和背景: Dracula 主题使用高对比度的前景和背景色,使得代码内容的层次分明,易于阅读和理解。
    2. 强调重要内容的语法高亮: Dracula 主题使用明亮的绿色进行语法高亮,能够清晰地强调代码中的重要部分,帮助程序员更好地理解代码逻辑。
    3. FLAT 扁平化设计风格: Dracula 主题采用简洁大方的 FLAT 扁平化设计风格,界面整洁清晰,让代码更加突出。
    4. 黑客文化与美学融合: Dracula 主题融合了黑客文化中的深色基调和对于对比度和视觉冲击的美学追求。它既展现了黑客式的科技感,又兼具艺术家般的美学气质。

    通过这些设计特征,Dracula 主题确保了代码的可读性,提供了令人愉悦的编程体验,并为开发者们带来了独特的视觉享受。


    优点


    Dracula 主题在技术上具有以下优势:

  • 中性深色背景的精心调配: Dracula 主题采用中性深色背景,软化了强光对眼睛的刺激,减轻了长时间工作导致的眼睛疲劳问题。
    • 明暗分明的前景和背景: Dracula 主题使用明暗分明的前景和背景色,使得代码的视觉层次感强,识别度高,提高了代码的可读性和理解效率。
    • 温暖色菜单栏和标识色边框: Dracula 主题在菜单栏和标识色边框上采用温暖色,增加了页面元素的识别度,帮助用户更好地找到所需功能。
    • 强调重要内容的明亮色彩: Dracula 主题使用明亮的色彩来突出重要的内容,提高了可关注点的辨识度,使开发者能够更快速地定位和理解关键代码部分。
    • 条件颜色支持: Dracula 主题提供了适应不同环境光照条件的条件颜色支持,确保在不同的工作环境中都能有良好的显示效果。

    Dracula 主题带来的用户体验提升包括:

  • 减轻眼睛疲劳问题: 通过精心调配的色彩和对比度,Dracula 主题可以减轻长时间工作导致的眼睛疲劳问题。
    • 提高代码可读性和理解效率: 明暗分明的视觉层次感和明亮色彩的使用使得代码更易于阅读和理解,提高了开发者的工作效率。
    • 丰富的语法色彩增强趣味性: Dracula 主题提供丰富的语法色彩,使得编程过程更具趣味性和乐趣,激发开发者的工作热情。
    • 酷炫的外观满足个性化追求: Dracula 主题具有独特的外观设计,满足技术宅对个性化的追求,让开发环境更具魅力和个性。
    • 对色弱用户友好: Dracula 主题经过精心设计,在保证美观的同时也考虑到了色弱用户的需求,不会造成视觉障碍。

    正因为这些优势,Dracula 主题备受码农的青睐。它极大地提升了 JetBrains IDE 的美观性和可用性,无论是初学者还是老手,都能够享受到 Dracula 主题带来的舒适的用户体验。


    支持产品


    到目前为止,Dracula 主题已经广泛支持341+款应用程序,涵盖了各个平台和工具。除了 JetBrains IDE,Dracula 还适用于许多其他知名的应用程序,其中包括但不限于以下几个:

    • Notepad++: Dracula 主题为 Notepad++ 提供了独特的外观,使得文本编辑器更加美观和舒适。
    • iTerm: Dracula 主题为 iTerm 终端应用程序带来了独特的配色方案,提升了终端界面的可视性和使用体验。
    • Visual Studio Code: Dracula 主题为 Visual Studio Code 提供了一套酷炫且易于辨识的代码颜色方案,让开发者能够更好地编写和调试代码。
    • Vim: Dracula 主题为 Vim 编辑器提供了一种简洁而又优雅的配色方案,使得代码在终端中的显示更加清晰明了。
    • Terminal.app: Dracula 主题为 macOS 上的 Terminal.app 终端应用程序提供了一种时尚和易于识别的配色方案,提升了终端的可用性和美观性。
    • Zsh: Dracula 主题兼容 Zsh 终端的配色方案,使得命令行界面更加美观和个性化。

    这些应用程序只是 Dracula 主题所支持的众多应用程序中的一部分,它们的加入使得 Dracula 主题在各个平台和工具上都能够提供一致的视觉体验,满足开发者对于美观和可用性的追求。



    查看更多支持产品:



    draculatheme.com



    使用


    下面我用 IDEA 实例给大家展示一下如何使用吧!



    1. 前往插件市场,搜索Dracula,点击安装




    1. 前往 Preferences > Appearance & Behavior > Appearance,从下拉菜单中选择Dracula



    1. 前往 Preferences > Editor > Color Scheme,从下拉菜单中选择Dracula



    通过上述步骤,您可以启用Dracula主题啦!


    总结


    Dracula 主题为 JetBrains IDE 带来了卓越的高对比度黑暗风格,本文我为大家介绍一下它的优点。如果你还没有尝试过这款插件,快去试试吧!


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

    🔥搞钱必看🔥 盘点那些靠谱的程序员副业,狠狠提升财富值💯

    这是一个职业生涯三叶草模型,它分为兴趣、价值、能力三个维度,完美的主职业最好同时满足这三项。但事情往往未必那么如意,如果主职业没能同时满足,那么剩下的部分,完全可以用副业填充。 或者,通俗点说,做副业的第一目标一般是赚钱,想大幅度增加“价值”尤其是物质价值这个...
    继续阅读 »


    这是一个职业生涯三叶草模型,它分为兴趣、价值、能力三个维度,完美的主职业最好同时满足这三项。但事情往往未必那么如意,如果主职业没能同时满足,那么剩下的部分,完全可以用副业填充。


    或者,通俗点说,做副业的第一目标一般是赚钱,想大幅度增加“价值”尤其是物质价值这个维度,那就让我们进入正题,看看程序员可以做的副业都有哪些。


    内容变现


    特点:高投入、门槛中等、长期主义


    公众号


    现在短视频更为吸引流量的时代,做公众号可能已经不是最优解,但做公众号的思路可以推广到一切内容副业上。认真写的内容也终将被看到,现在获取的粉丝反而更为核心、粘性更高。

    公众号的赚钱途径主要有三个:流量主、接受打赏、接广告。

    到达一定流量后,官方会邀请开通“流量主”功能,每篇推文末尾会有广告卡片,被点击即可获得收益。也就是说,开通该功能后,不需要做额外动作,发送推文就有收入。



    第二种是接受打赏,是很直观的读者对作者的物质激励。不过引导比较少的情况下,很少有读者自行去做打赏这个动作的,这部分收入就当做运营公众号的额外小激励吧。



    公众号积累粉丝,长期下来阅读量和粉丝比较好会有广告商找来做广告,俗称“恰饭”,这种收入相比前面两者来说就是“大单子”了,一单一般来说根据公众号质量和流量,会上百、上千、甚至上万、几十万不等。那这种收入就比较可观了,而且有广告主找到,也往往意味着,这个公众号质量还不错。


    为什么说公众号的思路可以推广到一切内容副业呢?因为内容可复用,就可以搭建自己的技术博客网站、分发到其他技术社区,而获利思路也是一致的,无非是流量和知名度的获益。具体的平台运营方式可能不会完全相同,还需要自己提前学习,不断在使用中进行探索。


    知识专栏


    如果你的写作水平还可以,也有可以成体系、系统化的知识输出,那可以尝试整理一个系列课程,以付费知识专栏的形式推出。



    极客时间、掘金小册、慕课网……非常多技术社区都推出了付费专栏,除了内容创作者自发上传的零散内容,一般也有这种成体系的、可以系统性掌握一部分技术的课程,大多数价格不高,一般在百元以内,正因为如此付费门槛也很低,用户乐意为此支付。这种付费专栏如果是线上分成形式,相当于是一个一劳永逸持续入账的项目。


    视频博主


    同样地,如果内容足够好的话,也可以考虑做视频博主,这分两个方向,个人博主和视频教程。不过相比于图文内容的专栏,视频的门槛会更高,对表达能力、授课能力、互动能力都有要求,还可能需要学习相应的录制、剪辑、运营知识。


    做个人视频博主,有平台本身给到内容创作者的流量收益,也有接广告恰饭的收益,一个细水长流,一个来得快但未必持久。长期下来还是需要持续更新、长久维护,小流量不断的同时,打广告有规律。出视频教程,则是与对应平台签约,看自己的版权要求、价格要求、付费机制,自行选择即可。


    技术变现


    特点:技术门槛低、项目导向、投入一般


    接外包


    说起程序员赚外快、副业来,一般最容易想到的就是接私活接外包,比如有很多外包服务平台如猪八戒、一品威客、解放号、码客帮、码市、程序员客栈等等,不过看起来价格并不很美好,大家可以自行筛选。比较靠谱的是熟人介绍,这样需求确认、尾款交付时都会比单纯陌生人更顺利。所以重要的还是积累技术、积累资源、积累人脉,逐渐打造自己的渠道和核心竞争力。


    自主开发项目


    如果技术水平不错,有想法有新意,但不想写文章、不愿录视频,依然只想跟代码打交道,那就可以自主开发项目来变现。根据自己的技术栈还有创意想法,可以是不同的产品,如桌面应用程序、手机APP、小程序、各种插件等。当然,产品要做到通过付费来盈利,门槛确实比较高了,而且除了技术,对运营能力也有一定的要求。做得好的完全可以做独立开发者来养活自己。


    比如这样在禅道插件市场上传插件的用户,已经获得了很高的持续性收益。


    其他方向


    特点:门槛高低不一、收入不一、形式灵活


     除了前面的内容导向和技术导向,其实还有很多跟技术、跟主业完全没有任何关系的方向。比如培训师,往往是按天收费,费用很高,不过当然也需要极高的知识壁垒;比如经常调侃的跑滴滴、送外卖,也能见证一些人生百态;比如有的人做手绘涂鸦、游戏陪玩这种兴趣驱动的副业,满足兴趣爱好的同时还能获得收入;比如很多脑力劳动者跑去做的轻体力活,如便利店收银、酒店前台、快餐店兼职,在机械操作的体力活中解放大脑。


    总之一句话:副业形式千千万,还是得靠实力干。短期的在线刷单、杀猪盘投资等等都被翻来覆去地反诈骗讲烂了,但任何宣称短期的、高回报率,都可能是带着引号的“副业”,需要擦亮眼睛加以鉴别。



    大多数副业还是有门槛的,需要投入精力和时间,找好方向,坚持长期主义,去做吧!


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

    为什么开发者应该多关注海外市场

    在上一篇《单枪匹马程序员,月营收从 0 到 1 万刀,近 90% 的毛利》里有掘友评论,这样的产品只有国外有,国内缺乏生态。 这篇文章主要是分享一下为什么我认为作为国内的开发者,应该多关注海外市场、做面向海外用户的产品。 早在 2000 年(点 com 泡沫...
    继续阅读 »

    在上一篇《单枪匹马程序员,月营收从 0 到 1 万刀,近 90% 的毛利》里有掘友评论,这样的产品只有国外有,国内缺乏生态。


    这篇文章主要是分享一下为什么我认为作为国内的开发者,应该多关注海外市场、做面向海外用户的产品。




    早在 2000 年(点 com 泡沫那阵),国外就有了 indie hacker / indie maker 这个名词来称呼做“独立”产品的人。这里的 indie 是 independent 的意思,意在独立(解脱)于各种束缚,比如:朝九晚五的工作时间、固定的办公室、领导、或者是投资人。


    而国内在最近几年也涌现了一拨独立开发者,多数以工程师为主,当然做的产品也是面向国内的市场。有做地不错的,像 Baye 的熊猫吃短信、vulgur 的极简时钟、Kenshin 的简阅等;但综合我这两年来对海外一些独立产品的研究,海外市场或许是更好的选择。


    当然凡是都有个前提,就是你没有一个豪华的创始团队或者是顶级投资人的背书,就是个人或者两三人的小团队。这个条件我觉得可以覆盖 90% 的中国的开发者;对于另外 10% 的拥有资源或者金主爸爸靠山的个人或者团队,不仅“可以”还“应该”去磕中国市场。但这不是今天要讨论的主题。


    在 BAT TMD 等巨头和背靠资源的精英创业者们的夹缝里,我觉得只有做面向海外市场的小产品是更有胜率一点;做国内市场面临的四个问题:


    第一、不存在足够的空间给个人/小团队做独立产品存活。


    Slack 大家应该都知道,在美国已经上市了,市值 200 亿刀。Slack 一直是被 qiang 的,但是为什么国内没有出现 Slack 这样的产品作为一个信息中心来连接各个办公工具?


    其实有,还不少,但都没活太久。一部分原因是腾讯阿里都非常重视这个“商业流量入口”,不想有可能被对方占有了。另外是国内互联网生态,从 BAT TMD 巨头到小软件公司,都太封闭;不仅不开放,还相互制约,都想把自己流量的守住,所以就同时出现了三个 Slack:

    • 微信出个企业微信(还封杀了 wetools)
    • 阿里出个钉钉
    • 字节出个飞书

    在这种巨头虎视眈眈且相互对抗的格局里,作为三缺(缺钱、缺资源、缺核心门槛)的个人或者团队是无法存活的。或许在 2010 年至 2016 年间还有草根产品团队依靠“热钱”注入有爆发的可能性,时至今日,特别是这个蜜汁 2020 的局势,是不太可能的了。


    即使,你找到了一个空白的利基市场(niche),你接下来面对三个问题:需求验证(试错)、推广、和商业化。


    第二点、需求验证或者叫“试错”成本高。


    由于国情不同,咱们需要经过一些不可避免的审核流程来保证互联网的干净。这个没话说,在哪做事就守哪的规矩。但这“需求验证”的门槛可就提高了不少。


    比如要做个网站吧,备案最快也两周。做游戏?有没有版号?做 app ?有没有软著?小程序(从用户端来讲)是个不错的创新,但是你最烦看到的是不是“审核不通过,类目不符合”?稍微做点有用户互动的功能都需要公司主体。公司注册、银行开户、做帐、以及各种实名制等;这些虽然都不是不可达到的门槛,但是去完成这些要耗费大量的精力,对于本身就单打独斗的开发者来说 - 太累了。


    再看看海外,简直不要太爽。做 app 还是需要经过苹果和谷歌的审核,但几乎不会对程序本身以外的东西设置门槛。网站注册个域名,30 秒改个 DNS 指到你的 IP,Netlify 或 Vercel 代码一推,就自动构建、部署、上线了。哪怕你不会写代码或者会写代码但是想先验证一下需求,看看潜在用户的响应如何,国外有不少非常值得一样的 no code 或 low code 平台。这个以后可以单独写一篇。


    OK,国内你也通过重重难关项目终于上线了,你面临剩下的两个问题:推广和商业化。


    第三点、推广渠道少 && 门槛高。


    海外市场的推广渠道更多元,比如 ProductHunt, IndieHackers, BetaList 等。这些平台不仅国内没有,我想表达的更重要一点是,这些平台用户都比较真诚和热心会实在地给你提建议,给你写反馈。国内也有几个论坛/平台,但是用户氛围和友好度就和上述几个没法比了。


    在付费推广方面,国内门槛(资质、资金)都挺高,感觉只有大品牌才能投得起广告,这对于缺资金的团队来讲,又是闭门羹。而国外,facebook 和 google 拿个 50、100 刀去做个推广计划都没问题。


    可以以非常低的成本来获取种子用户或者验证需求。


    行吧,推广也做得不错,得到了批种子用户并且涨势还不错;就最后一步了,商业化。


    第四点、商业化选择少。


    说商业化选择少可能说过了。国内是由于巨头间的竞争太激烈,出现各种补贴手段,导致互联网用户习惯于免费的互联网产品,甚至觉得应该倒贴给他来使用;伸手党、白 piao 党挺多;付费以及版权意识都还有改善空间。


    想想你都给哪些浏览器插件付费过?“插件还需要付钱?!”


    而海外用户的付费意愿足够强烈,在以后的案例分享中就能体会到。一个小小的浏览器插件,做得精美,触碰到了用户的购买欲望,解决了他一个痛点,他就愿意购买。


    下一篇就分享国外浏览器插件的产品案例。


    顺便再说一个「免费 vs 付费」的问题,这个不针对哪个国家,全世界都一样。免费(不愿意付费)的用户是最难伺候的,因为他们不 value 你的产品,觉得免费的就是创造者轻易做出来的、廉价的。如果依着这部分不愿意付费的客户来做需求,产品只会越做越难盈利。




    掘友们,下一篇文章见了


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