注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

环信FAQ

环信FAQ

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

慎重选择~~第四家公司刚刚开我,加入重新找工作大队!!!

前景需知 这家公司是我的第四家公司,合同三年,6个月试用期,(当时入职时,谈过说可以提前转正,但是后续当作没这件事),然后7月25日,下午5点半,下班时候告诉我被开了。当天是我手上的一个新项目刚好完结,测试刚过,bug修复完毕,老板让人事通知我,被开了,说是没...
继续阅读 »

前景需知


这家公司是我的第四家公司,合同三年,6个月试用期,(当时入职时,谈过说可以提前转正,但是后续当作没这件事),然后7月25日,下午5点半,下班时候告诉我被开了。当天是我手上的一个新项目刚好完结,测试刚过,bug修复完毕,老板让人事通知我,被开了,说是没有新的项目了。当时我算了算应该是还有几天就转正了。


在职期间


总共是在职6个月差几天转正,期间一直是大小周,说是双休,加班没有任何补偿,然后9点到5.30.(从来没有5点半下班过,最早就是6点半吧,5点半下班会打电话给你,问你为啥下班那么早).然后在这家公司这么久,手上是写了3个新项目,翻新2个老项目,还有维护的。期间没有任何违纪行为,这肯定是一定的,不然也不会等到还有几天才把我开了。在职期间做的事,跟产品沟通为什么不能这么写,用户怎么交互比较合理,不必太过于麻烦,给后端沟通为什么要这个数据,为什么要这样,还要跟老板说 进度怎么样的,预计时间。因为没有测试,是所有员工用了以后提一个bug单,到我这里来,然后我统一看这是谁的问题,然后我去沟通,加上公司内部人员测试,很多东西产品出成那样,觉得不合理,也要给我,我去跟产品沟通,真是沟通成本大的要死,期间有一个要对接别人的app里的积分系统,对公到我们的积分体系里,还要我去对接,这不能找后端嘛?产品又甩给我了,最后又要我去跟第三方沟通,再给自己的后端沟通,成本是真的高啊,我真是有时候头大。听着有点小抱怨,但是吧,其实后面了还好,确实能让你学到很多东西,因为你很清楚这个项目的走向,以及问题,基本上所有东西有点围绕着前端做的感觉,反正每天都是被问,问到最后,无论是谁张嘴我都知道是什么个情况。反正学着接受就好了。


为什么会来到这家公司??


这家公司是我去年面过的一家公司,当时入职他们公司一天我就走了,为什么会走,就是因为代码累积,页面过于卡顿,前端没有任何标注,而且入职第一天,老板就要求改他们的东西,然后第二天就没去了,为什么今年去了,是因为去年这个老板也联系了我几次,说我可以去他们公司试试看,然后过年的前两天还在跟我说,我说那就去试试看看,然后年后那个老板也催着我入职,当时也不是没得选,朋友公司招人内推,他面我,说让我去。我当时主要是跟这个老板说好了,答应了,于是就回绝了我的朋友(真后悔啊,那是真后悔,真不如去朋友哪里了,现在还被开了,卸磨杀驴,我真气)。


在公司半年,我具体做了哪些东西


上面说做了3个新项目,翻新两个新项目。三个新项目是一个是可视化大屏项目,这个项目用的是(vue3加echarts,适配是用v-scae-screen这个组件做的,当然这时候就有人会问,你用这个组件 那其他屏幕的除了你用的这个分辨率,其他比例不对的分辨率,也会有问题,当然这个问题我也遇到了,但是也试了其他的几种方案,但是或多或少都有问题,所以我就选择了这个比较直接.原理## transform.scale(),更详细的可以看看这个组件。)还有一个是小程序的老师端批改作业,并给予点评。(uni-app加uview写的,这个直接上图片,有难点)



 第三个项目也是uni-app写的,就是刚刚写完这个项目我被开了,真是太离谱了。也是一个小程序(uni-app加uview,然后益智类的,可以直接搜索头脑王者这个小程序,基本上是功能还原。不贴我的项目图了,好像我走的第二天就在审核了,主要是websocket长连接写的,因为是对战类,所以长连接时时保持通讯,也是有难点的,因为长连接要多页面保持又要实时获取信息,可以想一下怎么做)。 翻新的项目就不谈了,算是整个翻新,翻新是最累的,因为有的能用有的不能用,该封装封装,数据该处理处理,哦,中间遇到一个有趣的问题,就是el-tabs这个缓存机制,不知道为啥,v-if也不行.


目前的看法


7月25下午被开当天其实我很痛苦,当时人事说话也很过分,让我自己签申请离职说,这样的话赔偿你 0.5,如果不行,你可以去仲裁我们,然后如果我去仲裁,那么离职单,离职证明,赔偿,工资都没有,就拖着你,甚至老板恶言相向的告诉人事说,怎么可以在他的工作简历上留下这个不好的痕迹,影响他以后的工作。其实我听到这些话的时候我除了恶心,我什么话都说不出来,面对这个种情况,我咨询了,12333他们说,让我照常上班,他把你提出打开的软件,你就手动拍摄视频,然后自己打开,直至出示他把你辞退的证明,或者待够15天。我把这个事情实施以后,并且告知公司,仍然不给我出示离职证明,出了一张,辞退通知书,这个通知书我直接上图片,首先这个假,是个病假,是因为后端对我进行了侮辱,然后导致我气的头疼,然后我去请假,是给领导直接请的,她允许以后,我才中午下班是,离开的公司。 


为什么会给后端吵架,因为后端不处理逻辑,还要怪我什么都不给他说,什么都不给讲,这是我最气的点,我每次都要给他讲,为什么需要这个数据,为什么你要这么给我,需要什么,我每次都在他没写之前就进行沟通。他最后怪我没讲,并且侮辱我。有的人这时候会说,你为什么不他给你什么就要什么呢?然后自己处理逻辑。降低了耦合性,再往后说 你自己可以写一个node.js啊 为什么不呢?这些都挺对的,但是吧,你不能每次都这么处理问题吧。一个选择题,他应该给你abcd,结果给你1234,然后他要abcd,你说这个转换你做不做?你好说歹说他给你改了,然后一道题4个选项 我回答完以后,他给你答案你自己判断对错,这个逻辑前端写吗,当然也可以,如果他给你的答案是 1呢 1就是a,这时候你又该如何是好?可能你觉得我不信后端会这个对你,一定是你的问题,哈哈 上图片



 

 是的没有错,我来教着写,这个时候大家可以喷我了,可以说,你怎么交后端写,你算什么东西,兄弟们,兄弟们,都是我的问题,实在是没办法了,写出了这样得东西 这个东西还能精简,这是只是我为了实现而写得逻辑。




反正一吐为快,目前是没找工作,下周找找看吧,缓解一下。


当下迷茫得点


希望大家给点建议,就是说因为没有遇到一个好的产品导致我现在想去做产品,我直接现在转产品工资会有一个大跳水,会少很多,但是我也愿意接受,可能是赌气吧,就真的想去做这个,让开发没那么难以沟通。也在想是不是继续前端,保持现状,但是就是想去转产品了,我现在24岁,前端3年多,我应该还有试错得机会,我真的不想在碰见这种情况了,真的好累,加上只是前端,人微言轻,只有出现问题,提出来的东西,才能被采纳,真的好难。所以我是有意愿转转看的,不知道各位怎么看?能评价就评价下,需要我爆雷得,我私信,他们目前好像又在招前端了,怕大家踩雷,在上海。


给大家得建议


就是入职前,还是要好好调查,然后不要只听片面之言,然后就是现状不好的,也不要气馁,就加油好吧,我都没气馁,顶住压力啊,还是希望大家吃好喝好玩好,生活美满。


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

iOS 开发中如何禁用第三方输入法

iOS
iOS 目前已允许使用第三方输入法,但在实际开发中,无论是出于安全的考虑,还是对某个输入控件限制输入法,都有禁用第三方输入法的需求。基于此,对禁用第三方输入法的方式做一个总结。 1. 全局禁用 Objective-C 语言版本:- (BOOL)applicat...
继续阅读 »

iOS 目前已允许使用第三方输入法,但在实际开发中,无论是出于安全的考虑,还是对某个输入控件限制输入法,都有禁用第三方输入法的需求。基于此,对禁用第三方输入法的方式做一个总结。


1. 全局禁用


Objective-C 语言版本:

- (BOOL)application:(UIApplication *)application
shouldAllowExtensionPointIdentifier:(UIApplicationExtensionPointIdentifier)extensionPointIdentifier
{
// 禁用三方输入法
// UIApplicationKeyboardExtensionPointIdentifier 等价于 @"com.apple.keyboard-service"
if ([extensionPointIdentifier isEqualToString:UIApplicationKeyboardExtensionPointIdentifier]) {
return NO;
}
return YES;
}

Swift 语言版本:

func application(
_ application: UIApplication,
shouldAllowExtensionPointIdentifier extensionPointIdentifier: UIApplication.ExtensionPointIdentifier
) -> Bool {
// 禁用三方输入法
if extensionPointIdentifier == .keyboard {
return false
}
return true
}

2. 针对某个视图禁用

func application(
_ application: UIApplication,
shouldAllowExtensionPointIdentifier extensionPointIdentifier: UIApplication.ExtensionPointIdentifier
) -> Bool {
// 遍历当前根控制器的所有子控制器,找到需要的子控制器
for vc in self.window?.rootViewController?.childViewControllers ?? []
      where vc.isKind(of: BaseNavigationController.self)
{
// 如果首页禁止使用第三方输入法
for vc1 in vc.childViewControllers where vc1.isKind(of: HomeViewController.self) {
      return false
    }
  }
return true
}

3. 针对某个 inputView 禁用


3.1 自定义键盘


如果需求只是针对数字的输入,优先使用自定义键盘,将 inputView 绑定自定义键盘,不会出现第三方输入法。


3.2 遍历视图内控件,找到需要设置的 inputView,专门设置

func application(
_ application: UIApplication,
shouldAllowExtensionPointIdentifier extensionPointIdentifier: UIApplication.ExtensionPointIdentifier
) -> Bool {
// 遍历当前根控制器的所有子控制器,找到需要的子控制器
for vc in self.window?.rootViewController?.childViewControllers ?? []
      where vc.isKind(of: BaseNavigationController.self)
{
// 如果想要禁用的 inputView 在首页上
for vc1 in vc.childViewControllers where vc1.isKind(of: HomeViewController.self) {
// 如果 inputView.tag == 6 的 inputView 禁止使用第三方输入法
      for view in vc1.view.subviews where view.tag == 6 {
      return false
      }
    }
  }
return true
}

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

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

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

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


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


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


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


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


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


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


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


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


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


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


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


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


第二,期望薪资最好写上


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


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


第三,学历文凭一定要写


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


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


第四,年龄要写


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


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


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


第五,自我评价


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


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


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


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


最后的小结


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


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


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

OC项目用Swift开发方便吗?

iOS
前言 公司有个项目一直是用 OC 进行开发,现在想改成 Swift 来开发。那先说一下为什么有这样的想法,我们都知道 Swift 代码更简单,易维护,安全而且快,网络上也是很多描述,那我们主要的是担心一旦变成混编工程,会不会出现很多问题,还有如何解决这些问题。...
继续阅读 »

前言


公司有个项目一直是用 OC 进行开发,现在想改成 Swift 来开发。那先说一下为什么有这样的想法,我们都知道 Swift 代码更简单,易维护,安全而且快,网络上也是很多描述,那我们主要的是担心一旦变成混编工程,会不会出现很多问题,还有如何解决这些问题。性能问题方面Swift 和 OC 共用一套运行时环境,而且支持 Swift 桥接 到 OC上,所以呢,问题不大。如果有不同的想法,也欢迎留意指教。


桥接文件


我们只要在 OC 项目中,创建一个 swift 文件,系统就会弹出桥接文件,我们点击 "Create Bridging Header"即可。




OC 工程接入 Swift


OC 类 引用 Swift 类


如上面我们创建了一个 swift 文件,里面写一些方法提供给 OC 使用。

@objcMembers class SwiftText: NSObject {

func sayhello() -> String{

return "hello world"

}
}

class SwiftText2: NSObject {

@objc func sayhello() ->String{

returnOCAPI.sayOC()

}
}

这里我们有关键字2个,1个是@objcMembers,表示所有方法属性都可以提供给 OC 使用。另外一个是@objc,表示修饰的方法属性才可以提供给OC使用。


那我们 OC 类怎么用这个 swift 文件呢。
先在我们该类添加头文件

#import "项目Target-Swift.h"

然后我们点进去看下。




可以看到我们写的 swift 文件类,方法,属性,都被转化为 OC 了,有了这个我们直接使用即可。


OC类 使用 swift Pod库


说实话,这种用的比较少,但有时候我们真的觉得 swift Pod库 会更好用,那我们怎么去处理呢?


首先我们要搞懂一点,有些是支持使用的,如PromiseKit,有些是不支持使用的如Kingfisher


先说第一种支持使用的,我们直接导入#import <PromiseKit/PromiseKit.h>即可。


那要是第二种的话,我们还有一种办法,就是先用 swift 写一个该库管理类,然后里面引用我们该库的内容,我们通过 @objc 来提供给我们 OC 使用。


Swift类 引用 OC 类


如果我们编写的 Swift 类,想要用到 我们 OC 的方法,那我们如何处理呢?


我们直接在桥接文件"Target-Bridging-Header.h"里面,直接导入头文件#import "XXX.h"即可使用。


Swift类 使用 OC pod库


其实这个更简单,和 Swift 工程引入 OC pod库一样,在该类里面导入头文件即可。

import MJRefresh

遇到问题


问题1:引入swift pod库 问题


如果我们 OC 项目 是没有 使用use_frameworks!。那我们导入swift Pod库 就会报错。


那我们就在工程配置里面 Build Settings里面,搜索 Defines Module, 更改为 YES 即可。




问题2:OC 类继承问题


OC的类是不能继承至Swift的类,但Swift 类是可以继承 OC类的,其实方式也是"Target-Bridging-Header.h"导入头文件即可。


问题3:宏定义问题


我们自己重新一份
原来的是

#define kScreenWidth        [UIScreen mainScreen].bounds.size.width                      
#define kScreenHeight [UIScreen mainScreen].bounds.size.height

现在的是

let kScreenWidth = UIScreen.main.bounds.width
let kScreenHeight = UIScreen.main.bounds.height

有一些,我们可以定义问方法来替代宏。


问题4:OC经常调用swift库导入问题


我们知道xxx-Swift.h都是包含所有swift 提供给 OC 使用的类,所以我们可以把xxx-Swift.h放到 pch 文件里面,就可以在任意一个 OC 工程文件直接调用 swift 类。


OC 在线转为 swift


提供一个链接,可以支持 OC 转为 swift。
在线链接


最后


经过上面的总结,OC 项目 使用 swift 开发 的确是问题不大,使用过程中可能也会遇到编译问题,找不到文件问题,只要细心排查,也是很容易解决,那等后续项目用上正轨,还会把遇到的坑填补上来,如有不足,欢迎指点。


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

鸿蒙终于不套壳了?纯血 HarmonyOS NEXT 即将到来

对于移动开发者来说,特别是 Android 开发而言,鸿蒙是不是套壳 Android 一直是一个「热门」话题,类似的问题一直是知乎的「热点流量」之一,特别是每次鸿蒙发布新版本之后,都会有「套娃式」的问题出现。 例如最近 HDC 刚发布了鸿蒙 4.0 ,但是问题...
继续阅读 »

对于移动开发者来说,特别是 Android 开发而言,鸿蒙是不是套壳 Android 一直是一个「热门」话题,类似的问题一直是知乎的「热点流量」之一,特别是每次鸿蒙发布新版本之后,都会有「套娃式」的问题出现。


例如最近 HDC 刚发布了鸿蒙 4.0 ,但是问题已经提到了 6.0 ,不过也算是误打误撞,在 4.0 发布之后,华为宣布了 HarmonyOS NEXT 版本



HarmonyOS NEXT 在 2023 年 8 月 6 日开始面向合作企业开发者开放,2024 年第一季度面向所有开发者开放,也就是明年开始,更新后的鸿蒙,会使用全自研内核,去掉了传统的 AOSP 代码,仅支持鸿蒙内核和鸿蒙系统的应用,减少了 40% 的冗余代码,使系统的流畅度、能效、纯净安全特性大为提升




也就是说,你的 Android APK 已经不能在 HarmonyOS NEXT 上运行,因为系统已经不存在 AOSP 代码,甚至没有 JVM 。



虽然我们一直在吐槽鸿蒙套壳,但是这波华为终于是打算「釜底抽薪」,靠着 AOSP 「养住」开发者生态之后,这次终于开始「杀鸡取卵」。



这里不得不提到「纯血」这个词,虽然华为在此之前的宣传口径一直是纯国产自研,但是看来华为自身还是清楚里面的「血统不纯」,而这次决定「大换血」,“减少了 40% 的冗余代码” 的说法,就很有意思。




但是其实对于开发者来说,特别是移动端开发者来说,其实这是好事,我不从商业角度考虑,仅仅是从开发者生态考虑,因为移动端现在已经好久没有新活跃了,HarmonyOS NEXT 的全新适配工作应当大部分会落在 Android 开发上,或者说是否会新增全新的 HarmonyOS 开发岗位?



主要是转化的门槛不高,不过第一批吃螃蟹的,网上的资料肯定会相不足。




在全新的开发框架下, HarmonyOS NEXT 会采用全新自研的 ArkTS 和 ArkUI ,从目前看来,也就是你可能再也不能使用 Java 开发鸿蒙应用了,并且 ArkTS 是直接采用 AOT 编译优化,所以渲染模式可能会更接近 Flutter 和 Compose 的结构情况。




事实上从目前的文档和开发体验上看,控件结构和开发模式十分贴近 Flutter 和 Compose ,这对于相关领域的开发者来说可以说是能力加强,所以目前对于 HarmonyOS NEXT 来说,未来的生态适配难度会进一步降低。





即有适配负担,又有工作机遇,新技术领域代表存在新的红利,至少华为走在了 App 端「原生纯响应式开发」的前沿。



目前,华为已经从设计资源,编程语言,编译器到开发工具、调测工具实现全面升级,HarmonyOS SDK 升级至 API 10 端云一体,可以一次性集成。


另外一点是关于 ArkUI 的跨平台,这一点类似于苹果生态的一次开发多端部署,采用自研的 「方舟图形渲染」, HarmonyOS 也实现了类似手机,平板和电脑的统一「跨平台」效果。






目前猜测还是会机遇 Skia 底层支持。



最后就是大家关心的 HarmonyOS NEXT 会不会和 WPhone 一样遭遇滑铁卢,目前看来华为之前的技术积累和开发者关系运营的还不错:



根据 HDC 最新数据,鸿蒙生态的设备数量目前已超过 7 亿,已有 220 万 HarmonyOS 开发者投入到鸿蒙世界的开发中,API 日调用 590 亿次,软硬件产品超过 350 款。




华为鸿蒙 SDK 这些年确实沉淀了一部分开发者,虽然实际多少不清楚,但是这让鸿蒙 Next 不是从 0 开始,另外目前也有部分企业开始主动适配鸿蒙,并且华为提出了全新的鸿飞计划,在 3 年时间里投入 100 亿元资金支持鸿蒙生态建设



所以短期可能会有阵痛,但是 HarmonyOS NEXT 的基础其实挺好,不管是类似 Flutter/ Compose 的开发方式,还是原本已经存在的开发者基础,更有相关的政策扶持,很难看出鸿蒙会在明年遭遇滑铁卢的情况。



其实到这里我有个疑问,那就是 HarmonyOS NEXT 的生态会不会支持侧载,这决定了 HarmonyOS NEXT 之后的生态发展路线。



如果必须上架商店才能分发,这又是另外一个故事了。



最后就是现阶段的框架,例如 React Native 和 Flutter 能不能跑?官方目前已经有相关适配支持,目前消息上看:



  • RN 相关适配已经完成 60%

  • 游戏相关如 Unity 引擎,如前面提到过的新闻,其实游戏适配是最容易的

  • 最后 Flutter ,目前看来 Flutter For HarmonyOS 应该需要有好心社区进行适配




让我们最后一起期待纯血的鸿蒙可以走多

作者:恋猫de小郭
来源:juejin.cn/post/7264237761158643773
远。


收起阅读 »

最强实习生!什么?答辩刚结束,领导就通知她转正成功了?

文章目录 写在前面 灵魂三问 第一问,你了解转正流程吗? 第二问,实习期间的我为团队做了什么? 第三问,基础知识还记得吗? 一个日常实习阶段小tip 写在最后 FAQ时间 写在前面 熟知我的人应该都知道我是实习转正上岸字节的。 那是一个平平淡淡的下...
继续阅读 »

文章目录



写在前面


熟知我的人应该都知道我是实习转正上岸字节的。


那是一个平平淡淡的下午,leader突然神神秘秘凑到我身边:“一恩,快秋招了。我给你预留了一个HC,快准备准备转正答辩吧。”

于是乎,伴随着leader自以为充满关怀的安排下,我开始轰轰烈烈筹备起自己的转正大业。


和很多小伙伴一样,我刚刚准备转正时非常茫然无措。因为转正并没有明确的大纲,且不同业务、不同部门考核的形式都是不确定的,在网上搜索经验资料也少得可怜。


在这里插入图片描述


别急,转正的内容和形式虽然具有不确定性,但其固有流程又决定了他存在着一定的“潜规则”。下面一恩姐姐就带你发出灵魂三问,深度剖析转正那些不得不说的套路。


灵魂三问


第一问,你了解转正流程吗?


转正流程对于各个公司大同小异。


以字节为例,需要当年毕业的同学,在出勤满40个工作日(技术和测试序列)且经过部门Leader和HR同意后,即有资格发起转正流程。此时HR会根据评估人和候选人的时间,约一个时间组织进行转正答辩。这个短短1个小时的转正答辩,决定了你的去留。

在这里插入图片描述


转正答辩上,一般包含你的HR,部门领导和跨部门的领导。除了跨部门的领导外,其他人都是你在实习过程中可能一起干过饭喝过酒,讨论过诗和远方的伙伴。只要在实习过程中没有发生过什么反目成仇的惨剧,他们都是偏向你的,甚至私下有过“兄弟情义”,“生死之交”还会去引导你去把控答辩的节奏。


比如我就听过自己的同事说过,当时他的导师还在答辩时争着抢着帮他解答领导的问题……

在这里插入图片描述


所以你所需要的做的基本只有一件事:


就是保证转正答辩的过程是顺利的。


整个答辩过程基本分为三块,其中属于你的有效时间仅有两块。第一块为个人展示,你需要以PPT的形式去描述一下实习期间工作,这一块大约有40min;第二块为问答环节,评估人会去根据你的工作与业务询问一些项目及基础知识,这一块大约有20min;第三块为审判环节,评估人会根据转正答辩过程中对你的了解决定你最终的去留。


因此,只有利用好有效的两块时间,才能Hold住整个答辩过程,让评估人被你的魅力闪瞎双眼!


第二问,实习期间的我为团队做了什么?


日常有随时记录工作进度的好习惯,因此我非常迅速地将自己实习阶段的工作按照优先级总结总结写了一下答辩PPT。导师在看了我的草稿后,一个劲儿吐槽:“比起你在这里学的东西,老板们更关心的是你给团队和业务带来的产出,你跟别人做这件事的区别在哪里?你在团队的定位是什么?拿出让他们去选择你的理由吧!


这与我一开始想的完全不一样。我本以为答辩就是汇报自己学了什么,做了什么。但其实不是,公司看中的是你的个人想法和价值实现,以及你身上是否有可输出的内容。


你的一言一行都要表达出:你是完全能胜任这个职位的。


想明白这点,我重新组织了自己的PPT和答辩的内容:


首先,我用了一页画了一个时间轴,分别用关键词总结每一part工作主要内容,核心,和工作亮点项目。这一部分重在简洁清晰。目的是让评审人清晰的了解我的工作内容重点和核心。

在这里插入图片描述


接下来,我选择2~3个核心项目详细地介绍工作内容并量化自己的产出。如果大家不清楚如何介绍的话,可以参考金字塔原理中 先总后分的表达方式——先给你的听众一个核心结论,在后面逐层展开。



比如我去介绍自己做多人视频通话这个需求时,首先需求的背景是需要支持多个人一起视频通话,我的主要工作是技术方案的设计与开发,具体工作是通过获取多路视频流,并将视频流分给对应的成员,因此我需要去维护所有成员的视图窗口以及流的稳定性与正确性。为了实现这个功能,我去了解了视频流编码,推拉流的逻辑,并且与多媒体业务同学进行了沟通,保证整体形成一条稳定的通路。

在这里插入图片描述


(截图取自我的PPT答辩文档,针对强化通话感知的需求,我列出了需求的目标,以及技术方案,并采用流程图方便说明,以及最后写上了需求的收益)



第三部分我会去对自己的价值角色进行提炼,即向评估人去证明自己的独特价值以及在团队中的定位。如果你不知道如何去证明,那就将这个问题回答好:凭什么别人要选择你而不选择别人?


最后一部分可以向评估人讲述一下自己的期望和未来的规划,我当时是舒情并茂地表达了自己对团队的热爱和对前景的向往,并表达了自己对未来的无限期盼。说的导师当场差点“热泪盈眶”。


以及提供给大家一个小妙招,作为一名研发,如果拥有产品思维,无疑是非常加分的。因此大家可以对自己所在的业务从产品本身进行思考,比如能做些什么才能让产品吸引更多用户,以及在产品上有什么意见和规划。


在这里插入图片描述


第三问,基础知识还记得吗?


在40分钟ShowTime之后,剩余20分钟评估人可能会针对你的某个具体项目询问一些实现上的细节,也有可能会询问一些技术方案设计上的问题。因此需要保证你所介绍的每一个项目都是你切身参与且明确其中实现的技术方案与细节,而且你应该提前去准备一些代码或技术上可扩展或优化的思考,来体现出你对项目的一种全局的视角。


同时评估人也会针对你目前所处团队的业务特性去询问一些基础问题,这一点和面试比较像,虽然难度比较于面试会简单很多。但也需要去多少刻意准备一些基础知识。比如我做视频通话业务,当时评估人就问我,你觉得通话传输的音视频流信息是通过udp还是tcp传输的,以及他们的区别。


这些问题是不是对于现在的你实在太简单了?


一个日常实习阶段小tip


不清楚大家在日常工作的过程中有没有对自己工作进行总结的习惯。如果没有,请从现在开始,立!刻!记!录!


“记录”这个行为听起来难度很高,其实真正实施起来你会发现它就像一种“陪伴”,非常潜移默化地融入你的生活中。


我会在日常工作过程中我会将自己的每一份思考和产出都落地文档并定时整理与复盘,每周五下班前会抽出15分钟将本周的工作以及下周需要做的事情整理成一个TODO列表。且会以月为纬度进行一次工作量和心态的反思,并与导师进行一次整体沟通,这种定期的总结和复盘能够让我永远对自己保持清醒。


当我整理自己实习工作时,这些文字更是我的宝藏,我能很清楚地看到自己日积月累的自我升级,并非常轻松地以时间线的角度看出自己各个阶段的产出。


写在最后


希望大家在实习期间一直保持一个谦卑学习的态度,正式阶段繁重的工作压力会让你没有过多心思去进行一些软硬实力的提高。


因此实习是一个非常好的机会去适应、去成长,一定要耐心地倾听、观察,向身边优秀的同事学习。


相信在以后的工作中,你一定也能如鱼得水,熠熠生辉。

在这里插入图片描述




FAQ时间


Q1:工作上犯了个常识性错误,感觉转正无望,该不该及时止损?

首先,要明白,作为实习生,犯错是一件正常的事。错误才能让你意识到不足,才能成长。转正评估的不是你的过去,而是你的价值和你可以塑造的可能性。如果你能对自己过去的工作上的错误进行复盘与总结,并且能够对未来进行合理的规划。相信你也能给出一份完美的转正答卷。


Q2:秋招无望走实习转正是否可行?

这个选择是完全没有问题的。实习不仅能够提高转正的几率,也是给你一定机会提前感受一下社会环境,在体验过真实互联网工作环境后,有些人会明白自己是否合适,才会有更精确的职业规划。


新增一个小栏目,收集着目前为止小伙伴们私信一恩的一些关于实习转正问题的答复。如果大家还有其他问题欢迎继续

作者:李一恩
来源:juejin.cn/post/7257434794900832312
在评论区回复,一恩会一一回答的~

收起阅读 »

向前兼容与向后兼容

2012年3月发布了Go 1.0,随着 Go 第一个版本发布的还有一份兼容性说明文档。该文档说明,Go 的未来版本会确保向后兼容性,不会破坏现有程序。 即用10年前Go 1.0写的代码,用10年后的Go 1.18版本,依然可以正常运行。即较高版本的程序能正常...
继续阅读 »

2012年3月发布了Go 1.0,随着 Go 第一个版本发布的还有一份兼容性说明文档。该文档说明,Go 的未来版本会确保向后兼容性,不会破坏现有程序。


即用10年前Go 1.0写的代码,用10年后的Go 1.18版本,依然可以正常运行。即较高版本的程序能正常处理较低版本程序的数据(代码)


反之则不然,如之前遇到过的这个问题[1]:在Mac上用Go 1.16可正常编译&运行的代码,在cvm服务器上Go 1.11版本,则编译不通过;


再如部署Spring Boot项目[2]时遇到的,在Mac上用Java 17开发并打的jar包,在cvm服务器上,用Java 8运行会报错




一般会认为向前兼容是向之前的版本兼容,这理解其实是错误的。


注意要把「前」「后」分别理解成「前进」和「后退」,不可以理解成「从前」和「以后」


线上项目开发中,向后(后退)兼容非常重要; 向后兼容就是新版本的Go/Java,可以保证之前用老版本写的程序依然可以正常使用




前 forward 未来拓展。


后 backward 兼容以前。







  • 向前兼容(Forward Compatibility):指老版本的软/硬件可以使用或运行新版本的软/硬件产生的数据。“Forward”一词在这里有“未来”的意思,其实翻译成“向未来”更明确一些,汉语中“向前”是指“从前”还是“之后”是有歧义的。是旧版本对新版本的兼容 (即向前 到底是以前还是前面?实际是前面





  • 向上兼容(Upward Compatibility):与向前兼容相同。









  • 向后兼容(Backward Compatibility):指新的版本的软/硬件可以使用或运行老版本的软/硬件产生的数据。是新版本对旧版本的兼容





  • 向下兼容(Downward Compatibility):与向后兼容相同。











软件的「向前兼容」和「向后兼容」如何区分?[3]


参考资料


[1]

这个问题: https://dashen.tech/2021/05/30/gvm-%E7%81%B5%E6%B4%BB%E7%9A%84Go%E7%89%88%E6%9C%AC%E7%AE%A1%E7%90%86%E5%B7%A5%E5%85%B7/#%E7%BC%98%E8%B5%B7

[2]

部署Spring Boot项目: https://dashen.tech/2022/02/01/%E9%83%A8%E7%BD%B2Spring-Boot%E9%A1%B9%E7%9B%AE/

[3]

软件的「向前兼容」和「向后兼容」如何区分?: https://www.zhihu.com/question/47239021



作者:fliter
来源:mdnice.com/writing/b8eb5fdae77f42e897ba69898a58e0d8
收起阅读 »

对负载均衡的全面理解

title: 对负载均衡的全面理解 date: 2021-07-10 21:41:24 tags: TCP/IP 对负载均衡服务(LBS)大名入行不多久就一直听闻,后来的工作中,也了解到 软件负载均衡器,如被合入Linux内核的章文嵩的LVS,还有...
继续阅读 »


title: 对负载均衡的全面理解 date: 2021-07-10 21:41:24 tags: TCP/IP





负载均衡服务(LBS)大名入行不多久就一直听闻,后来的工作中,也了解到 软件负载均衡器,如被合入Linux内核的章文嵩的LVS,还有以应用程序形式出现的HAProxy、KeepAlived,以及更熟悉的Nginx 等


也知道价格高昂的硬件负载均衡器如F5,A10 (甚至搬运过报废的F5)



















但长期以来,也有一些疑惑不解,比如





  • 常说的四层负载均衡是不是就是在传输层实现负载均衡?





  • 四层负载均衡中常听到的三角传输模式IP隧道模式NAT模式,有何区别?哪个性能最好?





  • 四层负载均衡性能好,那为何还有如nginx这样名气更大的七层负载均衡的出现?(Nginx也可以用来做四层代理)





  • 负载均衡与反向代理有何异同?





  • 转发和代理有何本质不同?




这是几年前记的笔记,显然存有谬误。





计算机网络中常见缩略词翻译及简明释要




通读 凤凰架构--负载均衡一章,可知





  • 四层负载均衡 主要工作在第二层和第三层,即 数据链路层和网络层 (通过改写 MAC 地址IP 地址 实现转发)​​​





  • “三角传输模式”(Direct Server Return,DSR),是作用于 数据链路层负载均衡,也称“单臂模式”(Single Legged Mode)或者“直接路由”(Direct Routing)。 通过修改请求数据帧中的 MAC 目标地址,让用户原本是发送给负载均衡器的请求的数据帧,被二层交换机根据新的 MAC 目标地址转发到服务器集群中对应的服务器(“真实服务器”)的网卡上。 效率高性能好,但有些场景不能满足










  • 网络层负载均衡:IP隧道模式,NAT模式


IP隧道模式:





NAT模式:







在流量压力比较大的时候,NAT 模式的负载均衡会带来较大的性能损失,比起直接路由和 IP 隧道模式,甚至会出现数量级上的下降






  • 四层负载均衡进行转发,只有一条TCP通道; 七层负载均衡只能进行代理,需要有两条TCP通道








  • 七层负载均衡器就属于反向代理中的一种;





  • 如果只论网络性能,七层均衡器肯定是无论如何比不过四层均衡器的;但其工作在应用层,可以感知应用层通信的具体内容,往往能够做出更明智的决策,玩出更多的花样来。









负载均衡的两大职责是“选择谁来处理用户请求”和“将用户请求转发过去”。上面讲的都是怎样将用户请求转发过去


至于选择哪台应用服务器来处理用户请求(翻牌子),则有很多算法,如下图就是F5的一些选择算法












B站:一次性讲清楚四层负载均衡中的NAT模式和IP隧道模式


Shadowsocks源码解读——什么是代理?什么是隧道?


NAT模式、路由模式、桥接模式的区别


VLAN是二层技术还是三层技术?


四层负载均衡详解


作者:fliter
来源:mdnice.com/writing/c5b54a9bdd78478a87c6d39e38572358
收起阅读 »

Kotlin注解探秘:让代码更清晰

快速上手 @Target(   AnnotationTarget.CLASS,   AnnotationTarget.FUNCTION,   AnnotationTar...
继续阅读 »

快速上手


@Target(  
 AnnotationTarget.CLASS,  
 AnnotationTarget.FUNCTION,  
 AnnotationTarget.VALUE_PARAMETER,  
 AnnotationTarget.EXPRESSION,  
 AnnotationTarget.CONSTRUCTOR  
)

@Retention(AnnotationRetention.SOURCE)
@Repeatable
@MustBeDocumented
annotation class MyAnnotation

@MyAnnotation @MyAnnotaion class Test @MyAnnotation constructor(val name: String) {
    @MyAnnotation fun test(@MyAnnotation num: Int)Int = (@MyAnnotation 1)
}

注解的声明


注解使用关键字annotation来声明,比如快速上手中的例子,使用annotation class MyAnnotation就声明了一个注解,我们可以按照定义的规则将其放在其他元素身上


元注解


下面的注解了解过Java的肯定不陌生,元注解就是可以放在注解上面的注解





  • @Target: 用来指定注解可以应用到哪些元素上,有以下可选项



    • CLASS: 可以应用于类、接口、枚举类



    • ANNOTATION_CLASS: 可以应用于注解



    • TYPE_PARAMETER



    • PROPERTY



    • FIELD



    • LOCAL_VARIABLE



    • VALUE_PARAMETER: 可以应用于字面值



    • CONSTRUCTOR: 可以应用于构造函数



    • FUNCTION: 可以应用于函数



    • PROPERTY_GETTER



    • PROPERTY_SETTER



    • TYPE



    • EXPRESSION: 可以应用于表达式



    • FILE



    • TYPEALIAS





  • @Retention: 用来指定注解的生命周期



    • SOURCE: 仅保存在源代码中



    • BINARY: 保存在字节码文件中,但是运行是无法获取



    • RUNTIME: 保存在字节码文件中,运行时可以获取





  • @Repeatable: 允许此注解可以在单个元素上多次使用


拿上方的代码来简单介绍几个元注解


@Target


可以看一下@Target的源码


 * This meta-annotation indicates the kinds of code elements which are possible targets of an annotation.
 *
 * If the target meta-annotation is not present on an annotation declaration, the annotation is applicable to the following elements:
 * [CLASS], [PROPERTY], [FIELD], [LOCAL_VARIABLE], [VALUE_PARAMETER], [CONSTRUCTOR], [FUNCTION], [PROPERTY_GETTER], [PROPERTY_SETTER].
 *
 * @property allowedTargets list of allowed annotation targets
 */
@Target(AnnotationTarget.ANNOTATION_CLASS)
@MustBeDocumented
public annotation class Target(vararg val allowedTargets: AnnotationTarget)

在源码中可以看到,Target注解中要传入的参数为allowedTargets,使用了vararh关键字,可传入多个参数,参数的类型为AnnotationTarget,它是一个枚举类,再进入AnnotationTarget的源码就可以看到它有上方元注解中列出的那些。


在快速上手的示例中我们的@Target中传入了Class FUNCTION VALUE_PARAMETER EXPRESSION CONSTRCTOR,表示此注解可以放在类、接口、枚举、函数、字面值、表达式和构造函数上


@Retention


此注解就是指定它什么时候失效 默认是RUNTIME, 快速上手中是用的SOURCE,表示它仅存在于源码中,在编译成字节码后将会消失,如果指定了BINARY,则可以存在于字节码文件中,但是运行时无法获取,反射无法获取


注解的属性


注解可在主构造参数内传值


annotation class MyAnnotation2(val effect: String)

class Test2 {
    @MyAnnotation2("Test")
    fun test() {
        println("Run test")
    }
}

比如上面的例子,可以在主构造函数内传入一个参数,参数支持的类型有以下几种





  • Kotlin中的八种“基本数据类型”(Byte, Short, Int, Long, Float, Double, Boolean, Char)



  • String类型



  • 引用类型(Class)



  • 枚举类型



  • 注解类型



  • 以上类型的数组类型 需要注意的是,官网中特别说明参数不可以传入可空类型,比如"String?",因为JVM不支持null存储在注解的属性中


注解的作用


如果是熟悉Java的开发者对注解的作用肯定是非常熟悉。 注解可以提供给编译器、运行时环境、其他代码库以及框架提供很多可用信息。 可用作标记,可供第三方技术库、框架识别信息,比如大家熟悉的SpringBoot,很多事情就是通过注解和反射来实现 可用来提供更多的上下文信息,比如方法的类型参数、返回值类型、错误处理


后面可结合反射来深入理解Kotlin在开发中的用途


作者:AB-style
来源:mdnice.com/writing/5b8eb45e3b1e4b23a57926bd58b7f540
收起阅读 »

京东一面:post为什么会发送两次请求?🤪🤪🤪

web
在前段时间的一次面试中,被问到了一个如标题这样的问题。要想好好地去回答这个问题,这里牵扯到的知识点也是比较多的。 那么接下来这篇文章我们就一点一点开始引出这个问题。 同源策略 在浏览器中,内容是很开放的,任何资源都可以接入其中,如 JavaScript 文件、...
继续阅读 »

在前段时间的一次面试中,被问到了一个如标题这样的问题。要想好好地去回答这个问题,这里牵扯到的知识点也是比较多的。


那么接下来这篇文章我们就一点一点开始引出这个问题。


同源策略


在浏览器中,内容是很开放的,任何资源都可以接入其中,如 JavaScript 文件、图片、音频、视频等资源,甚至可以下载其他站点的可执行文件。


但也不是说浏览器就是完全自由的,如果不加以控制,就会出现一些不可控的局面,例如会出现一些安全问题,如:



  • 跨站脚本攻击(XSS)

  • SQL 注入攻击

  • OS 命令注入攻击

  • HTTP 首部注入攻击

  • 跨站点请求伪造(CSRF)

  • 等等......


如果这些都没有限制的话,对于我们用户而言,是相对危险的,因此需要一些安全策略来保障我们的隐私和数据安全。


这就引出了最基础、最核心的安全策略:同源策略。


什么是同源策略


同源策略是一个重要的安全策略,它用于限制一个源的文档或者它加载的脚本如何能与另一个源的资源进行交互。


如果两个 URL 的协议、主机和端口都相同,我们就称这两个 URL 同源。



  • 协议:协议是定义了数据如何在计算机内和之间进行交换的规则的系统,例如 HTTP、HTTPS。

  • 主机:是已连接到一个计算机网络的一台电子计算机或其他设备。网络主机可以向网络上的用户或其他节点提供信息资源、服务和应用。使用 TCP/IP 协议族参与网络的计算机也可称为 IP 主机。

  • 端口:主机是计算机到计算机之间的通信,那么端口就是进程到进程之间的通信。


如下表给出了与 URL http://store.company.com:80/dir/page.html 的源进行对比的示例:


URL结果原因
http://store.company.com:80/dir/page.html同源只有路径不同
http://store.company.com:80/dir/inner/another.html同源只有路径不同
https://store.company.com:80/secure.html不同源协议不同,HTTP 和 HTTPS
http://store.company.com:81/dir/etc.html不同源端口不同
http://news.company.com:80/dir/other.html不同源主机不同

同源策略主要表现在以下三个方面:DOM、Web 数据和网络。



  • DOM 访问限制:同源策略限制了网页脚本(如 JavaScript)访问其他源的 DOM。这意味着通过脚本无法直接访问跨源页面的 DOM 元素、属性或方法。这是为了防止恶意网站从其他网站窃取敏感信息。

  • Web 数据限制:同源策略也限制了从其他源加载的 Web 数据(例如 XMLHttpRequest 或 Fetch API)。在同源策略下,XMLHttpRequest 或 Fetch 请求只能发送到与当前网页具有相同源的目标。这有助于防止跨站点请求伪造(CSRF)等攻击。

  • 网络通信限制:同源策略还限制了跨源的网络通信。浏览器会阻止从一个源发出的请求获取来自其他源的响应。这样做是为了确保只有受信任的源能够与服务器进行通信,以避免恶意行为。


出于安全原因,浏览器限制从脚本内发起的跨源 HTTP 请求,XMLHttpRequest 和 Fetch API,只能从加载应用程序的同一个域请求 HTTP 资源,除非使用 CORS 头文件


CORS


对于浏览器限制这个词,要着重解释一下:不一定是浏览器限制了发起跨站请求,也可能是跨站请求可以正常发起,但是返回结果被浏览器拦截了。


浏览器将不同域的内容隔离在不同的进程中,网络进程负责下载资源并将其送到渲染进程中,但由于跨域限制,某些资源可能被阻止加载到渲染进程。如果浏览器发现一个跨域响应包含了敏感数据,它可能会阻止脚本访问这些数据,即使网络进程已经获得了这些数据。CORB 的目标是在渲染之前尽早阻止恶意代码获取跨域数据。



CORB 是一种安全机制,用于防止跨域请求恶意访问跨域响应的数据。渲染进程会在 CORB 机制的约束下,选择性地将哪些资源送入渲染进程供页面使用。



例如,一个网页可能通过 AJAX 请求从另一个域的服务器获取数据。虽然某些情况下这样的请求可能会成功,但如果浏览器检测到请求返回的数据可能包含恶意代码或与同源策略冲突,浏览器可能会阻止网页访问返回的数据,以确保用户的安全。


跨源资源共享(Cross-Origin Resource Sharing,CORS)是一种机制,允许在受控的条件下,不同源的网页能够请求和共享资源。由于浏览器的同源策略限制了跨域请求,CORS 提供了一种方式来解决在 Web 应用中进行跨域数据交换的问题。


CORS 的基本思想是,服务器在响应中提供一个标头(HTTP 头),指示哪些源被允许访问资源。浏览器在发起跨域请求时会先发送一个预检请求(OPTIONS 请求)到服务器,服务器通过设置适当的 CORS 标头来指定是否允许跨域请求,并指定允许的请求源、方法、标头等信息。


简单请求


不会触发 CORS 预检请求。这样的请求为 简单请求,。若请求满足所有下述条件,则该请求可视为 简单请求



  1. HTTP 方法限制:只能使用 GET、HEAD、POST 这三种 HTTP 方法之一。如果请求使用了其他 HTTP 方法,就不再被视为简单请求。

  2. 自定义标头限制:请求的 HTTP 标头只能是以下几种常见的标头:AcceptAccept-LanguageContent-LanguageLast-Event-IDContent-Type(仅限于 application/x-www-form-urlencodedmultipart/form-datatext/plain)。HTML 头部 header field 字段:DPR、Download、Save-Data、Viewport-Width、WIdth。如果请求使用了其他标头,同样不再被视为简单请求。

  3. 请求中没有使用 ReadableStream 对象。

  4. 不使用自定义请求标头:请求不能包含用户自定义的标头。

  5. 请求中的任意 XMLHttpRequestUpload 对象均没有注册任何事件监听器;XMLHttpRequestUpload 对象可以使用 XMLHttpRequest.upload 属性访问


预检请求


非简单请求的 CORS 请求,会在正式通信之前,增加一次 HTTP 查询请求,称为 预检请求


需预检的请求要求必须首先使用 OPTIONS 方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。预检请求 的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。


例如我们在掘金上删除一条沸点:


20230822094049


它首先会发起一个预检请求,预检请求的头信息包括两个特殊字段:



  • Access-Control-Request-Method:该字段是必须的,用来列出浏览器的 CORS 请求会用到哪些 HTTP 方法,上例是 POST。

  • Access-Control-Request-Headers:该字段是一个逗号分隔的字符串,指定浏览器 CORS 请求会额外发送的头信息字段,上例是 content-type,x-secsdk-csrf-token

  • access-control-allow-origin:在上述例子中,表示 https://juejin.cn 可以请求数据,也可以设置为* 符号,表示统一任意跨源请求。

  • access-control-max-age:该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是 1 天(86408 秒),即允许缓存该条回应 1 天(86408 秒),在此期间,不用发出另一条预检请求。


一旦服务器通过了 预检请求,以后每次浏览器正常的 CORS 请求,就都跟简单请求一样,会有一个 Origin 头信息字段。服务器的回应,也都会有一个 Access-Control-Allow-Origin 头信息字段。


20230822122441


上面头信息中,Access-Control-Allow-Origin 字段是每次回应都必定包含的。


附带身份凭证的请求与通配符


在响应附带身份凭证的请求时:



  • 为了避免恶意网站滥用 Access-Control-Allow-Origin 头部字段来获取用户敏感信息,服务器在设置时不能将其值设为通配符 *。相反,应该将其设置为特定的域,例如:Access-Control-Allow-Origin: https://juejin.cn。通过将 Access-Control-Allow-Origin 设置为特定的域,服务器只允许来自指定域的请求进行跨域访问。这样可以限制跨域请求的范围,避免不可信的域获取到用户敏感信息。

  • 为了避免潜在的安全风险,服务器不能将 Access-Control-Allow-Headers 的值设为通配符 *。这是因为不受限制的请求头可能被滥用。相反,应该将其设置为一个包含标头名称的列表,例如:Access-Control-Allow-Headers: X-PINGOTHER, Content-Type。通过将 Access-Control-Allow-Headers 设置为明确的标头名称列表,服务器可以限制哪些自定义请求头是允许的。只有在允许的标头列表中的头部字段才能在跨域请求中被接受。

  • 为了避免潜在的安全风险,服务器不能将 Access-Control-Allow-Methods 的值设为通配符 *。这样做将允许来自任意域的请求使用任意的 HTTP 方法,可能导致滥用行为的发生。相反,应该将其设置为一个特定的请求方法名称列表,例如:Access-Control-Allow-Methods: POST, GET。通过将 Access-Control-Allow-Methods 设置为明确的请求方法列表,服务器可以限制哪些方法是允许的。只有在允许的方法列表中的方法才能在跨域请求中被接受和处理。

  • 对于附带身份凭证的请求(通常是 Cookie),


这是因为请求的标头中携带了 Cookie 信息,如果 Access-Control-Allow-Origin 的值为 *,请求将会失败。而将 Access-Control-Allow-Origin 的值设置为 https://juejin。cn,则请求将成功执行。


另外,响应标头中也携带了 Set-Cookie 字段,尝试对 Cookie 进行修改。如果操作失败,将会抛出异常。


参考文章



总结


预检请求是在进行跨域资源共享 CORS 时,由浏览器自动发起的一种 OPTIONS 请求。它的存在是为了保障安全,并允许服务器决定是否允许跨域请求。


跨域请求是指在浏览器中向不同域名、不同端口或不同协议的资源发送请求。出于安全原因,浏览器默认禁止跨域请求,只允许同源策略。而当网页需要进行跨域请求时,浏览器会自动发送一个预检请求,以确定是否服务器允许实际的跨域请求。


预检请求中包含了一些额外的头部信息,如 Origin 和 Access-Control-Request-Method 等,用于告知服务器实际请求的方法和来源。服务器收到预检请求后,可以根据这些头部信息,进行验证和授权判断。如果服务器认可该跨域请求,将返回一个包含 Access-Control-Allow-Origin 等头部信息的响应,浏览器才会继续发送实际的跨域请求。


使用预检请求机制可以有效地防范跨域请求带来的安全风险,保护用户数据和隐私。


整个完整的请求流程有如下图所示:


20230822122544


最后分享两个我的两个开源项目,它们分别是:



这两个项目都会一直维护的,如果

作者:Moment
来源:juejin.cn/post/7269952188927017015
你也喜欢,欢迎 star 🥰🥰🥰

收起阅读 »

网易云音乐 Tango 低代码引擎正式开源!

web
📝 Tango 简介 Tango 是一个用于快速构建低代码平台的低代码设计器框架,借助 Tango 只需要数行代码就可以完成一个基本的低代码平台前端系统的搭建。Tango 低代码设计器直接读取前端项目的源代码,并以源代码为中心,执行和渲染前端视图,并为用户提供...
继续阅读 »

📝 Tango 简介


Tango 是一个用于快速构建低代码平台的低代码设计器框架,借助 Tango 只需要数行代码就可以完成一个基本的低代码平台前端系统的搭建。Tango 低代码设计器直接读取前端项目的源代码,并以源代码为中心,执行和渲染前端视图,并为用户提供低代码可视化搭建能力,用户的搭建操作会转为对源代码的修改。借助于 Tango 构建的低代码工具或平台,可以实现 源码进,源码出的效果,无缝与企业内部现有的研发体系进行集成。


Tango 低代码引擎开发效果


如上图所示,Tango 低代码引擎支持可视化视图与源码双向同步,双向互转,为开发者提供 LowCode+ ProCode 无缝衔接的开发体验。


✨ 核心特性



  • 经历网易云音乐内网生产环境的实际检验,可灵活集成应用于低代码平台,本地开发工具等

  • 基于源码 AST 驱动,无私有 DSL 和协议

  • 提供实时出码能力,支持源码进,源码出

  • 开箱即用的前端低代码设计器,提供灵活易用的设计器 React 组件

  • 使用 TypeScript 开发,提供完整的类型定义文件


🏗️ 基于源码的低代码搭建方案


Tango 低代码引擎不依赖私有搭建协议和 DSL,而是直接使用源代码驱动,引擎内部将源码转为 AST,用户的所有的搭建操作转为对 AST 的遍历和修改,进而将 AST 重新生成为代码,将代码同步给在线沙箱执行。与传统的 基于 Schema 驱动的低代码方案 相比,不受私有 DSL 和协议的限制,能够完美的实现低代码搭建与源码开发的无缝集成。



📄 源码进,源码出


由于引擎内核完全基于源代码驱动实现,Tango 低代码引擎能够实现源代码进,源代码出的可视化搭建能力,不提供任何私有的中间产物。如果公司内部已经有了一套完善的研发体系(代码托管、构建、部署、CDN),那么可以直接使用 Tango 低代码引擎与现有的服务集成构建低代码开发平台。


code in, code out


🏆 产品优势


与基于私有 Schema 的低代码搭建方案相比,Tango 低代码引擎具有如下优势:


对比项基于 Schema 的低代码搭建方案Tango(基于源码 AST 转换)
适用场景面向特定的垂直搭建场景,例如表单,营销页面等🔥 面面向以源码为中心的应用搭建场景
语言能力依赖私有协议扩展,不灵活,且难以与编程语言能力对齐🔥 直接基于 JavaScript 语言,可以使用所有的语言特性,不存在扩展性问题
开发能力LowCode🔥 LowCode + ProCode
源码导出以 Schema 为中心,单向出码,不可逆🔥 以源码为中心,双向转码
自定义依赖需要根据私有协议扩展封装,定制成本高🔥 原有组件可以无缝低成本接入
集成研发设施定制成本高,需要额外定制🔥 低成本接入,可以直接复用原有的部署发布能力

📐 技术架构


Tango 低代码引擎在实现上进行了分层解藕,使得上层的低代码平台与底层的低代码引擎可以独立开发和维护,快速集成部署。此外,Tango 低代码引擎定义了一套开放的物料生态体系,开发者可以自由的贡献扩展组件配置能力的属性设置器,以及扩展低代码物料的二方三方业务组件。


具体的技术架构如下图所示:


low-code engine


⏰ 开源里程碑


Tango 低代码引擎是网易云音乐内部低代码平台的核心构件,开源涉及到大量的核心逻辑解藕的工作,这将给我们正常的工作带来大量的额外工作,因此我们计划分阶段推进 Tango 低代码引擎的开源事项。



  1. 今天我们正式发布 Tango 低代码引擎的第一个社区版本,该版本将会包括 Tango 低代码引擎的核心代码库,TangoBoot 应用框架,以及基于 antd v4 适配的低代码组件库。

  2. 我们计划在今年的 9 月 30 日 发布低代码引擎的 1.0 Beta 版本,该版本将会对核心的实现面向社区场景重构,移除掉我们在云音乐内部的一些兼容代码,并将核心的实现进行重构和优化。

  3. 我们计划在今年的 10 月 30 日 发布低代码引擎的 1.0 RC 版本,该版本将会保证核心 API 基本稳定,不再发生 BREAKING CHANGE,同时我们将会提供完善翔实的开发指南、部署文档、和演示应用。

  4. 正式版本我们将在 2023 年 Q4 结束前 发布,届时我们会进一步完善我们的开源社区运营机制。


milestones


🤝 社区建设


我们的开源工作正在积极推进中,可以通过如下的信息了解到我们的最新进展:



欢迎大家加入到我们的社区中来,一起参与到 Tango 低代码引擎的开源建设中来。有任何问题都可以通过 Github Issues 反馈给我们,我们会及时跟进处理。


💗 致谢


感谢网易云音乐公共技术团队,大前端团队,直播技术团队,以及所有参与过 Tango 项目的同学们。


感谢 CodeSandbox 提供的 Sandpack 项目,为 Tango 提供了强大的基于浏览器的代码构建与执行能力。

作者:网易云音乐技术团队
来源:juejin.cn/post/7273051203562749971

收起阅读 »

北京前端五年经验问些什么?

这一天,我瘫坐在办公室的椅子上,回想这五年的一事无成,钱也没赚到,技术也没学到,最近投了简历去面试,我一定要把握住,这是我此生仅有的机会了。 穿好格子衫,带上假发,出发了。 路上的植发广告格外亮眼,玩了会儿手机终于到了。 某大型互联网公司,跟前台说了一下是面试...
继续阅读 »

这一天,我瘫坐在办公室的椅子上,回想这五年的一事无成,钱也没赚到,技术也没学到,最近投了简历去面试,我一定要把握住,这是我此生仅有的机会了。


穿好格子衫,带上假发,出发了。


路上的植发广告格外亮眼,玩了会儿手机终于到了。


某大型互联网公司,跟前台说了一下是面试的,然后让我填个表,填完去一个小屋子等着。


过了一会儿还是没人,前台小姐姐给我带了一杯水,说下一个就是我。


等了半小时终于来人了,微胖的一个中年男人,进来打了一个招呼,示意我坐下吧。


面试环节,请先自我介绍一下吧,他拿着简历看,我就说了一下我的情况,男,25,张满月,热爱编程,平时会写一些技术博客,文章,录制成视频等,(表示热爱学习),介绍了一下技术栈,Vue Nodejs python C++


然后介绍了一下项目,省略...


面试官问:为啥要离职?


我:(理由现编)家里庄稼要开始收割了 开玩笑 我就说了薪资问题。


然后就是问问题环节


问了一些计算机的基础知识,CPU运行原理,冯诺依曼体系结构,图形绘制原理,等


网络的一些东西 OSI七层参考模型,TCP/IP四层事实模型,双绞线,无线电波,光纤,路由器,交换机等。


还有一些协议 TCP/IP 协议簇里面的基本都问。 很考验基础知识


问的最多的就是TCP 三次握手 以及四次挥手 syn包 seq序列号 Ack确认号,滑动窗口思想等


http1.1 http2 http3 多路复用,保活,队头阻塞,二进制分帧层,头部压缩等。


操作系统知识


进程,线程,内存管理,汇编和机器语言的区别什么的,还有windows和Linux的常用命令。


web服务器 nginx 四层负载和七层SLB负载 这个就太简单了 stream upstream


nginx插件编写问了lua语言


nginx 反向代理 proxy_pass 线程什么,也都是一些基础问题


考察了很多基础知识 这些应该大家都会


然后换人了... 后面来了一个看着年轻点的,跟我说刚才那个人是运维主管。。。我也是服了我就说怎么不问前端的东西呢???


这位面试官是前端负责CICD,自动化流程负责人。


问了一些基础问题


Vue3的一些特性,和一些ts的简单的东西装饰器什么的,都比较简单,问了mvvm,和mvc,IOC控制反转和DI 依赖注入,这玩意就太熟了,巴拉巴拉说了一堆,问了一些前端的工具Babel,PostCss,webpack,vite,esbuild,rollup什么的。很杂,


然后问了一些js的问题,经典event loop... , es6, 也都是一些常问的。


他看我简历写着 webGL 问了一些相关的问题:openGL修饰符,类型,顶点着色器,片源着色器等,还问了光学的知识 冯氏光照模型 慢反射光,镜面高光等。都是简单的基础知识 大家应该也会


接着了问了一些CICD的知识 我就知道逃不过,问了一些docker,github Actions Jenkins 等一些问题,这些我也不是完全精通,只能说回答的一般😂。


接着就让我等通知,


后面HR发消息让我周六去复试。 周六牛的


周六的时候还是熟悉的广告,熟悉的地铁,这次是一个后端大哥面的,一进来就问,你们之前的网站吞吐率是多少,。。。这 我哪清楚,瞎说了一个,然后问有没有做过getway,我说有用Nest写的,任何人要先过网关层,然后才到业务层。


然后问负载怎么做的,堡垒机怎么部署的,有多少台机器,怎么部署的?


我们一共有XX台机器,使用动态扩容技术,(大概就是比如有10台机器,用的人多了CPU利用率过高,超过90%,就会进行动态扩容,自动增加机器11台,自动进行Nest服务部署,自动配置负载均衡,如果CPU下来了,就会动态缩容,删除代码,去掉负载,关闭机器),使用pm2 部署的,pm2自带了集群部署。


然后问mysql 基本的语法 索引 mysql事务的四大特性,等。。。


网络编程nodejs net模块socket套接字,如何跟python通讯,gRPC协议,以及如何编写addon,Npai用C++编写的使用node-gyp编译。


问了wasm,c++如何编译wasm等。


其他的不记得

作者:小满zs
来源:juejin.cn/post/7273309090657747000
了。。。 后面就没信了。。。。。

收起阅读 »

iOS 使用 CoreNFC 读取第三代社保卡信息

iOS
NFC 是 Near Field Communication 的缩写,即近场通信,是一种用于短距离无线设备与其他设备共享数据或触发这些设备上的操作的技术。它使用射频场构建,允许没有任何电源的设备存储小块数据,同时还允许其他供电设备读取该数据。 iOS 和 w...
继续阅读 »

NFC 是 Near Field Communication 的缩写,即近场通信,是一种用于短距离无线设备与其他设备共享数据或触发这些设备上的操作的技术。它使用射频场构建,允许没有任何电源的设备存储小块数据,同时还允许其他供电设备读取该数据。



iOS 和 watchOS 设备内置 NFC 硬件已经很多年了。在现实生活中,Apple Pay 就是使用这项技术与商店的支付终端进行交互。然而直到 iOS 11 开发者才能够使用 NFC 硬件。后来 Apple 在 iOS 13 系统中提升了 CoreNFC 的功能,开发者可以借助这项新技术,对 iOS 设备进行编程,使其以新的方式与周围的互联世界进行交互。


说明:本文提供的代码示例所用的开发环境为 Xcode14 + Swift 5.7 + iOS 13。需要登录已付费的开发者账号才能开启 NFC Capability。


工程配置


设置 Capability


在项目导航器中选中项目,转到 Signing & Capabilities 标签页并选择 +Capability,在弹出的列表中选择 Near Field Communication Tag Reading。这会自动生成 entitlements 文件中的必要配置信息,同时为您的应用程序激活 NFC 功能。


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/
DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.nfc.readersession.formats</key>
<array>
<string>TAG</string>
</array>
</dict>

设置 Info.plist


添加 NFC 相关的隐私设置,向 Info.plist 文件中添加 Privacy - NFC Scan Usage Description 隐私设置项。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/
DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NFCReaderUsageDescription</key>
<string>应用需要您的同意,才能访问 NFC 进行社保卡信息的读写。</string>
</dict>

添加 AID 相关的设置项,向 Info.plist 文件中添加 ISO7816 application identifiers for NFC Tag Reader Session 配置项。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/
DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.nfc.readersession.iso7816.select-identifiers</key>
<array>
<string>A000000632010105</string>
</array>
</dict>


说明:第三代社保卡使用统一的交通联合卡电子钱包规范,A000000632010105 为交通联合卡 AID 标识。参考网址:wiki.nfc.im/books



导入 CryptoSwift 第三方库


在项目导航器中选中项目,右键菜单选择 Add Packages...,在搜索框中输入 github.com/krzyzanowsk… 并点击 Add Package 按钮完成导入。





说明:CryptoSwift 提供了相关的十六进制字符串与 UInt8 相互转换的方法。



代码编程


扩展 NFCISO7816Tag


由于 Apple 是从 iOS 14 系统开始提供了 sendCommand API 的异步调用形式,为兼容 iOS 13 系统,并更好的使用 Swift 提供的 async/await 语法,现对其 NFCISO7816Tag 进行方法扩展。

import CoreNFC
import CryptoSwift

@available(iOS 13.0, *)
extension NFCISO7816Tag {

  @discardableResult
  func sendCommand(_ command: String) async throws -> Data {
    return try await withCheckedThrowingContinuation { continuation in
      // 通过 CryptoSwift 库提供的 API,将十六进制表示命令字符串转换成字节
      let apdu = NFCISO7816APDU(data: Data(hex: command))!
      // 将同步调用形式转换成异步调用形式
      sendCommand(apdu: apdu) { responseData, _, _, error in
        if let error {
          continuation.resume(throwing: error)
        } else {
          continuation.resume(returning: responseData)
        }
      }
    }
  }
}

封装 NFCTagReaderSession

import CoreNFC

@available(iOS 13.0, *)
class NFCISO7816TagSession: NSObject, NFCTagReaderSessionDelegate {

  private var session: NFCTagReaderSession? = nil
  private var sessionContinuation: CheckedContinuation<NFCISO7816Tag, Error>? = nil

  func begin() async throws -> NFCISO7816Tag {
// 实例化用于检测 NFCISO7816Tag 的会话
    session = NFCTagReaderSession(pollingOption: .iso14443, delegate: self)
    session?.alertMessage = "请将社保卡靠近手机背面上方的 NFC 感应区域"
    session?.begin()
    return try await withCheckedThrowingContinuation { continuation in
      self.sessionContinuation = continuation
    }
  }

  func invalidate(with message: String) {
// 关闭读取会话,以防止重用
    session?.alertMessage = message
    session?.invalidate()
  }

  // MARK: - NFCTagReaderSessionDelegate

  func tagReaderSessionDidBecomeActive(_ session: NFCTagReaderSession) {}

  func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) {
// 检测到 NFCISO7816Tag
    if let tag = tags.first, case .iso7816(let iso7816Tag) = tag {
      session.alertMessage = "正在读取信息,请勿移动社保卡"
// 连接到 NFCISO7816Tag 并将同步调用形式转换成异步调用形式
      session.connect(to: tag) { error in
        if let error {
          self.sessionContinuation?.resume(throwing: error)
        } else {
          self.sessionContinuation?.resume(returning: iso7816Tag)
        }
      }
    }
  }

  func tagReaderSession(_ session: NFCTagReaderSession, didInvalidateWithError error: Error) {
// 读取过程中发生错误
    self.session = nil
    sessionContinuation?.resume(throwing: error)
  }
}

编写 UI 界面


使用 SwiftUI 编写如下代码所示的页面,包含一个显示卡号的标签和一个读取按钮。

import SwiftUI

struct ContentView: View {
  @State private var cardNo = ""

  var body: some View {
    VStack(alignment: .leading) {
      Text("卡号:\(cardNo)")
        .font(.system(size: 17))
      Button(action: read) {
        Text("读取")
          .padding()
          .frame(maxWidth: .infinity)
          .foregroundColor(.white)
          .background(.blue)
          .cornerRadius(8)
      }
      Spacer()
    }
    .padding()
  }
}

实现读取逻辑

import SwiftUI
import CryptoSwift

struct ContentView: View {
// var body: some View {...}

private func read() {
    Task {
      let session = NFCISO7816TagSession()
      do {
// 检测 NFCISO7816Tag
        let tag = try await session.begin()
// 发送命令 00B0950A12 并截取前 10 个字节转换为 20 位卡号
        let cardNo = try await tag.sendCommand("00B0950A12")[0..<10].toHexString()
        self.cardNo = cardNo
// 关闭读取会话
        session.invalidate(with: "读取成功")
      } catch {
        print(error)
      }
    }
  }
}


说明:APDU 是卡与读卡器之间传送的信息单元,具体指令描述请参考 wiki.nfc.im/books



运行过程截图




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

Apipost: 开发者们的瑞士军刀

在当今的数字化时代,数据流通是推动社会进步的关键因素之一。其中,API(应用编程接口)已经成为跨平台数据交互的标准。然而,API开发和管理并非易事,Apipost一体化研发协作赋能平台,支持从API设计到API调试再到API测试覆盖整个API生命周期的API管...
继续阅读 »

在当今的数字化时代,数据流通是推动社会进步的关键因素之一。其中,API(应用编程接口)已经成为跨平台数据交互的标准。然而,API开发和管理并非易事,Apipost一体化研发协作赋能平台,支持从API设计到API调试再到API测试覆盖整个API生命周期的API管理平台,一起来看看Apipost有什么不同吧。


一、Apipost是什么?


Apipost是一个专为开发者设计的API管理工具,提供了全面的API文档生成、调试、测试和分享功能。它的目标是帮助开发者简化API开发流程,提高工作效率。


二、如何使用Apipost?


安装:


进入官网下载安装或直接使用web端



使用:


可以从其他平台如postman导入脚本文件,或创建接口。



接口调试:


输入接口URL后点击发送即可模拟接口请求,上方为请求区,下方为响应区



生成接口文档:


点击分享文档即可生成标准的接口文档,可以将链接分享给需要查看接口的其他同事



一键压测


接口调试完成后可以在一键压测页面进行并发测试,看看接口在高并发情况下的运行情况



总结


Apipost作为一款专为开发者设计的API管理工具,凭借其强大的功能和易用性,已经在开发者社区中积累了良好的口碑。通过使用Apipost,开发者可以节省大量时间,专注于创新和打造卓越的产品。如果你正在寻找一款强大且易用的API管理工具,那么Apipost无疑是一个值得考虑的选择。


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

前端比localStorage存储还大的本地存储方案

产品的原话就是“要又大又全”。既然存储量大,也要覆盖全多种设备多种浏览器。 方案选择既然要存储的数量大,得排除cookielocalStorage,虽然比cookie多,但是同样有上限(5M)左右,备选websql 使用简单,存储量大,兼容性差,备选index...
继续阅读 »

产品的原话就是“要又大又全”。既然存储量大,也要覆盖全多种设备多种浏览器。


方案选择

  • 既然要存储的数量大,得排除cookie
  • localStorage,虽然比cookie多,但是同样有上限(5M)左右,备选
  • websql 使用简单,存储量大,兼容性差,备选
  • indexDB api多且繁琐,存储量大、高版本浏览器兼容性较好,备选

既然罗列了一些选择,都没有十全十美的,那么有没有一种能够集合这多种方式的插件呢?渐进增强 or 优雅降级 的存在
冲着这个想法,就去github和谷歌找了一下,还真的有这么一个插件。


那就是 localforage


localforage


localForage 是一个 JavaScript 库,只需要通过简单类似 localStorage API 的异步存储来改进你的 Web 应用程序的离线体验。它能存储多种类型的数据,而不仅仅是字符串。 




关于兼容性


localForage 有一个优雅降级策略,若浏览器不支持 IndexedDB 或 WebSQL,则使用 localStorage。在所有主流浏览器中都可用:Chrome,Firefox,IE 和 Safari(包括 Safari Mobile)。下面是 indexDB、web sql、localStorage 的一个浏览器支持情况,可以发现,兼容性方面loaclForage基本上满足99%需求


使用


解决了兼容性和存储量的点,我们就来看看localforage的基础用法


安装

# 通过 npm 安装:
npm install localforage
// 直接引用
<script src="localforage.js"></script>
<script>console.log('localforage is: ', localforage);</script>

获取存储


getItem(key, successCallback)


从仓库中获取 key 对应的值并将结果提供给回调函数。如果 key 不存在,getItem() 将返回 null。

localforage.getItem('somekey').then(function(value) {
// 当离线仓库中的值被载入时,此处代码运行
console.log(value);
}).catch(function(err) {
// 当出错时,此处代码运行
console.log(err);
});

// 回调版本:
localforage.getItem('somekey', function(err, value) {
// 当离线仓库中的值被载入时,此处代码运行
console.log(value);
});

设置存储


setItem(key, value, successCallback)


将数据保存到离线仓库。你可以存储如下类型的 JavaScript 对象:

  • Array
  • ArrayBuffer
  • Blob
  • Float32Array
  • Float64Array
  • Int8Array
  • Int16Array
  • Int32Array
  • Number
  • Object
  • Uint8Array
  • Uint8ClampedArray
  • Uint16Array
  • Uint32Array
  • String
localforage
.setItem("somekey", "some value")
.then(function (value) {
// 当值被存储后,可执行其他操作
console.log(value);
})
.catch(function (err) {
// 当出错时,此处代码运行
console.log(err);
});

// 不同于 localStorage,你可以存储非字符串类型
localforage
.setItem("my array", [1, 2, "three"])
.then(function (value) {
// 如下输出 `1`
console.log(value[0]);
})
.catch(function (err) {
// 当出错时,此处代码运行
console.log(err);
});

// 你甚至可以存储 AJAX 响应返回的二进制数据
req = new XMLHttpRequest();
req.open("GET", "/photo.jpg", true);
req.responseType = "arraybuffer";

req.addEventListener("readystatechange", function () {
if (req.readyState === 4) {
// readyState 完成
localforage
.setItem("photo", req.response)
.then(function (image) {
// 如下为一个合法的 <img> 标签的 blob URI
var blob = new Blob([image]);
var imageURI = window.URL.createObjectURL(blob);
})
.catch(function (err) {
// 当出错时,此处代码运行
console.log(err);
});
}
});

删除存储


removeItem(key, successCallback)


从离线仓库中删除 key 对应的值。

localforage.removeItem('somekey').then(function() {
// 当值被移除后,此处代码运行
console.log('Key is cleared!');
}).catch(function(err) {
// 当出错时,此处代码运行
console.log(err);
});

清空存储


clear(successCallback)


从数据库中删除所有的 key,重置数据库。


localforage.clear() 将会删除离线仓库中的所有值。谨慎使用此方法。

localforage.clear().then(function() {
// 当数据库被全部删除后,此处代码运行
console.log('Database is now empty.');
}).catch(function(err) {
// 当出错时,此处代码运行
console.log(err);
});

localforage是否万事大吉?


用上了localforage一开始我也以为可以完全满足万恶的产品了,然而。。。翻车了.。


内存不足的前提下,localforage继续缓存会怎么样?


在这种状态下,尝试使用localforage,不出意外,抛错了 QuotaExceededError 的 DOMErro


解决
存储数据的时候加上存储的时间戳和模块标识,加时间戳一起存储

setItem({
value: '1',
label: 'a',
module: 'a',
timestamp: '11111111111'
})

  • 如果是遇到存储使用报错的情况,try/catch捕获之后,通过判断报错提示,去执行相应的操作,遇到内存不足的情况,则根据时间戳和模块标识清理一部分旧数据(内存不足的情况还是比较少的)
  • 在用户手机上产生脏数据的情况,想要清理的这种情况的 处理方式是:
  • 让后端在用户信息接口里面加上缓存有效期时间戳,当该时间戳存在,则前端会进行一次对本地存储扫描
  • 在有效期时间戳之前的数据,结合模块标识,进行清理,清理完毕后调用后端接口上报清理日志
  • 模块标识的意义是清理数据的时候,可以按照模块去清理(选填)

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

数字签名为什么可以防篡改

iOS
数字签名是什么 数字签名是一种数字技术,用于验证和保护数据的完整性。 数字签名是通过一些加密算法将消息或文件与公钥(如果是非对称加密就有公钥不然就不用)绑定在一起,并生成唯一的签名。 数字签名的工作原理 数字签名的核心在于加密算法。最常用的是非对称加密算法,它...
继续阅读 »

数字签名是什么


数字签名是一种数字技术,用于验证和保护数据的完整性


数字签名是通过一些加密算法将消息或文件与公钥(如果是非对称加密就有公钥不然就不用)绑定在一起,并生成唯一的签名。


数字签名的工作原理


数字签名的核心在于加密算法。最常用的是非对称加密算法,它将原文通过特定HASH函数得到的摘要信息用发送者的私钥加密,与原文一起传送给接收者。接收者只有用发送者的公钥才能解密被加密的摘要信息,然后用HASH函数对收到的原文提炼出一个摘要信息,与解密得到的摘要进行对比。


数字签名也可以使用哈希函数对文件或消息的散列值进行加密,确保消息不会被篡改。(也有人认为摘要算法不能逆向也就是解密所以不是加密算法,在此不做讨论)


数字签名可以与数字证书结合使用,以证明密钥的归属和真实性,从而保护数字签名过程不被破坏。


数字签名的应用


JWT


JWT通常由三个部分组成:头部(Header)、载荷(Payload)和签名(Signature),以点号分隔。第一部分是头部,第二部分是载荷,第三部分是签名。以下是一个包含了用户ID、用户名和时间戳的JWT实例,格式为 Header.Payload.Signature

// 为方便展示,在'.'处作了换行处理,可以更好地看清楚结构
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

HeaderPayload都是经过 Base64URL 编码的,所以每个人都能通过解码得到原来的信息,固不应该在里面存一些敏感信息。



Signature就是我们要讨论的数字签名了!Signature 部分是对前两部分的签名,防止数据篡改。首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。Signature = HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)


HMAC算法 是一种基于密钥的报文完整性的验证方法。HMAC算法利用哈希运算,以一个密钥和一个消息为输入,生成一个消息摘要作为输出。其安全性是建立在Hash加密算法基础上的。


由于Signature是根据HeaderPayload以及服务器的secret来生成的,由于secret只有服务器知道,所以只要HeaderPayloadSignature其中一个被篡改了,那么后续验证的时候就不能通过。同时只有知道secret才能产生与HeaderPayload配对的Signature,所以也能确认该 Token 是否是该服务器所颁发的。


验证过程是用服务器的密钥通过同样的算法计算出一个新的 Signature 然后和旧的 Signature 进行比较,只要被篡改那么 Signature就会跟着改变,所以通过比较, Signature 一样的话则证明没有被篡改,否则则认为被篡改了。


CA 证书


其实就是这个 CA 证书的数字签名为什么可以防篡改,困扰了我好久,所以才去稍微深入了解了一下然后写下了这篇博客。就是为了这点醋,我才包的这顿饺子~


之前学习 https 的时候,看了各大论坛的帖子发现有挺多帖子对于 CA 证书是怎么做放篡改的讲的不太对或者讲的不太清晰,所以这个问题困扰了我挺久。以下是随便找的一些帖子(对事不对人):


例1:



例2:



例3:



(例1和例2是随便在掘金上面搜到的相关文章,例3是newbing的回答)


先回顾一下数字证书验证的大概过程:


CA 签发证书的过程:

  • 首先 CA 会把持有者的公钥、用途、颁发者、有效时间等信息打成一个包,然后对这些信息进行 Hash 计算,得到一个 Hash 值;
  • 然后 CA 会使用自己的私钥将该 Hash 值加密,生成 Certificate Signature,也就是 CA 对证书做了签名;
  • 最后将 Certificate Signature 添加在文件证书上,形成数字证书;

客户端校验服务端的数字证书的过程:

  • 首先客户端会使用同样的 Hash 算法获取该证书的 Hash 值 H1;
  • 浏览器收到证书后可以使用 CA 的公钥解密 Certificate Signature 内容,得到一个 Hash 值 H2 ;
  • 最后比较 H1 和 H2,如果值相同,则为可信赖的证书,否则则认为证书不可信。

核心问题在于验证服务器发来的数字证书的数字签名时所用到的公钥是哪里来的。


假设有那么一个场景:


客户端A 和 服务器A 的通信过程中,私钥是Secret_RSA_A,公钥是Secret_PUB_A。服务器A 将自己的证书CA发给客户端A的过程中被 中间人B 给截获了,中间人B 用自己的公钥Secret_PUB_B 替换了 服务器A 发给 客户端A 的CA证书的公钥Secret_PUB_A,并且用和公钥Secret_PUB_B 配对的私钥Secret_RSA_B 对替换公钥后的CA证书的公钥、用途、颁发者、有效时间等信息生成的新HASH 进行加密,生成新的 Certificate Signature 并把原本证书上的 Certificate Signature 替换掉。但客户端A 对这并不知情。然后在后续客户端对该 CA证书验证的过程中,如果使用的是证书上的公钥,那么计算出来的 H1 和 H2 就会一样,也就是认为证书是可信的。(实际上加密使用的是CA私钥而不是服务器私钥所以中间人伪造不了一对新的公私钥,但是如果使用服务器发送过来的公钥去验证的话那么就有可能被伪造)


所以更加安全的做法应该是不使用传过来的证书上面的公钥(证书上的公钥是服务器持有者的公钥而不是CA公钥),而是使用预置在操作系统里面的公钥,因为证书加密是用CA私钥加密的而不是用服务器持有者的私钥进行加密的,传服务器持有者的公钥过来是为了和客户端协商然后生成后续对称加密通信需要用到的秘钥。这也是我之前看到的一些文章没有提到的(如上面的图1/2/3所示,没有针对原作者的意思),容易让人困惑。服务器发送过来的证书中的公钥是服务器的公钥而不是可以解密数字签名的公钥(数字签名的公钥也就是和CA证书配对的公钥)。 通常浏览器和操作系统中集成了 CA 的公钥信息,浏览器收到证书后可以使用操作系统内置的 CA 的公钥解密 Certificate Signature 内容。这行验证过程中存在一个证书信任链的问题。客户端收到服务器发送过来的CA证书后,浏览器开始查找操作系统中已内置的受信任的证书发布机构CA,与服务器发来的证书中的颁发者CA比对,用于校验证书是否为合法机构颁发,如果找不到,浏览器就会报错,说明服务器发来的证书是不可信任的。如果找到,那么浏览器就会从操作系统中取出 颁发者CA 的公钥,然后对服务器发来的证书里面的签名进行解密。


综上,数字签名只能验证数据的完整性(JWT 只有服务端可以验证他的身份,因为它有解密需要的密钥,而客户端是验证不了的),而验证身份需要的是数字证书。


最后


以上是本人在学习数字签名原理的过程中的一些感悟,由于个人的局限性,所以可能存在纰漏的情况,欢迎大家批评指正。


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

AI不会干掉程序员,反而会为程序员带来更多工作机会

AI不会干掉程序员,反而会为程序员带来更多工作机会 从chatGPT出来那一刻起,我就一直在思考这个问题:我们程序员是不是马上要被AI干掉了?搞得自己非常焦虑,各种自我怀疑,对除AI外的任何技术都失去了兴趣。网上也有各种铺天盖地的观点,大多数的title都是以...
继续阅读 »

AI不会干掉程序员,反而会为程序员带来更多工作机会


从chatGPT出来那一刻起,我就一直在思考这个问题:我们程序员是不是马上要被AI干掉了?搞得自己非常焦虑,各种自我怀疑,对除AI外的任何技术都失去了兴趣。网上也有各种铺天盖地的观点,大多数的title都是以"AI即将干掉程序员"开头,贩卖焦虑。
在今年这种大环境本身就非常不好的时候,AI的出现仿佛是雪上加霜,让我们对未来完全失去信心,感觉未来一片灰暗。甚至很多人开始研究起了自己失业后可以干哪些行业,如外卖员、网约车等。


对于我们是否会马上被AI干掉这个问题,通过这几个月不断的思考、学习、推理,总算有了一些自己的答案,在此做一个分享与总结。


AI出现后软件行业会变成怎样?


去除杂念,以史为鉴,尊重逻辑。


会出现各种各样基于AI的新型应用


这里的新型应用主要是在没有AI的加持下无法实现的应用。如AI医生、AI老师等。


当你身体出现异常的时候,很多人的第一反应就是先百度一下,然后吓出一身冷汗,然后医院检查,最后虚惊一场。


这里最大的问题就是我们通过搜索引擎很难精确地描述出病情,当我们去门诊诊断的时候,医生会问我们多轮问题,才能给出初步诊断。多轮问题是目前的搜索引擎无法做到的,但它却正好是ChatGPT的强项。


如果有AI加持,再加上有效的训练数据,AI医生就可以通过和你的多轮对话,给出更加准确的初步诊断。


当AI医生诞生后,你小孩发烧的时候你就可以以非常小的成本得到非常准确的处理方案,如是否需要物理降温、是否需要立即吃药、如果吃药应该吃什么药什么量等。而在这之前,你需要到网上搜索各种资料,不仅效率低下,还有可能得到一些质量很低的搜索结果,甚至错误的结果。因为想在网上搜索出一篇权威的文章是非常困难的。


另外,AI教师也是非常有用的应用。在AI老师出现以后,你的小孩可能就不再需要你的辅导就能完成各种题目的解答,且AI老师可以给出更好的解题思路,更准确的答案。AI教师除了可以教授K12,还能教授各个垂直领域的知识,这是一个非常巨大的市场,未来一定会出现各种各样的AI教师平台。


现有应用会逐步AI化


看文档真的很累,但是如果有一个掌握文档所有细节的人坐在你旁边为你服务,你还会那么累吗?这就是未来的AI文档。你只需要提出问题,AI就可以教你如何使用软件或者api而不需要一字一句的去研究文档。


除了文档,政府的各种政策、法律条文、保险条款等,未来都将AI化,我们每个人都可以拥有自己专有的“人工客服”,以前需要看几个小时文件才能得出的答案,现在可能一分钟就能得到答案了,而且更加准确!


未来的政府、保险公司等同类型主体间的信息化较量,很可能就是AI能力的较量。


软件行业的AI化,对程序员来说意味着什么?


政府的政策发布系统要升级、保险公司的客服系统要升级,AI医生、AI老师需要开发,这个工作量最终是由谁来完成?程序员。


很多人可能会说,既然AI会写代码了,那么为什么不是AI直接对现有系统进行升级或者自我孵化新应用?


AI完全替代程序员?这个事情真的那么简单吗?


仅从技术的角度来看,当前的AI虽然可以写一些小模块,但是要完成一个复杂系统的架构、研发、部署,AI当前还有些力不从心。现在AI可以完成一些点上的东西,但是我们的大型应用是需要把这些点组合起来,点与点之间还有很深的业务关联的。


虽然AI迭代速度很快,但是不要忘了即使是AI,要学习新的知识也需要人类去训练,而这个训练的成本不仅仅是人力成本,还有时间成本。想要AI完全达到人类程序员的理解能力和开发能力,可能不是一两年就能够实现的事情。


退一万步,即便是AI具备了编写复杂程序的能力,那谁来监督它,测试它?自动驾驶技术推出了这么多年,为什么没有很快替代人类司机?就是因为验证可靠性是一个非常复杂的过程,路遥才能知马力。信任关系不是一天两天的相处就能建立起来的。特别是这种涉及公司信息安全、软件可靠性的信任关系。


因此,正是由于将来会出现各种新型AI应用,以及现有应用需要AI化,才诞生了大量的工作量。这是一个行业基础设施的大升级,基建永远是最容易诞生工作机会的,程序员不仅不会失业,还会在这一波大基建建设中得到新一轮的工作机会


对于AI加持后的程序员工作畅享


‘基于型’程序员


我们现在开发软件,大多数情况都逃不过‘基于’二字。基于vue、基于react、基于flutter等等。


为什么会去‘基于’,就是因为这些框架或者库,能够提高我们的工作效率,减轻我们的心智负担,让开发复杂的应用变得简单。


我们基于的内容就是基础设施,而AI就是一种天然的基础设施。


未来一定会有大型的、成熟的AI平台和工具供我们‘基于’。当我们要开发一个AI应用,我们不需要自己去训练我们的基础模型,而是基于一个成熟的AI模型进行微调或者二次训练,就可以得到我们定制的AI模型,从而实现我们应用所需要的功能。


比如我们需要对公司开发的平台软件的文档进行AI化,我们不需要自己去训练一个AI机器人,而是基于现有大语言模型平台,新建一个AI实例,然后把我们的文档内容喂给这个实例,它就能变成一个我们定制的AI客服。我们还可以为这个实例设置各种参数,如定制它的聊天风格是严肃的还是活泼的、如定制它对于不相关的问题拒绝回答等等,当然这些参数都是AI平台提供的功能。


虽然AI可以帮我们编写如何调用平台api的代码,但是如何把这些代码集成到我们现有的软件中,还需要人类程序员的帮忙。


‘效率型’程序员


当有了AI大模型的加持,我们在写程序的过程中,可以让AI帮我们把一些点上的东西迅速完成掉,从而提高我们的编码效率。


如我们现在是一个vue新手,我们想实现一个列表的渲染。在从前我们可能需要去查阅vue官方文档,但是未来我们也许只需要问一问vue官方提供的机器人,它便能直接给出代码和解释。以前半个小时才能完成的工作量,现在10分钟便能完成。


而今日,已经有copilot,codeium这种ai编程工具,直接集成在编辑器中猜测我们的意图,很多情况下我们只需要按一按tab即可,甚至只需要在注释中描写下我们要实现的功能,它就能直接给出最终的代码。但是不要忘记,我们还是要写提示、写注释AI才能工作的,它只是让我们写得更快,即使没有AI,我们一样能完成最终的工能。


‘靠谱型’程序员


当我们一口气写完一个复杂的组件后,我们需要进行手工测试才能验证它的可靠性。而很多bug是在自测的时候很难发现的。有了AI加持后,我们写完一段代码就可以让它帮我们看看有没有什么明显的bug,然后迅速进行修复。把我们的代码喂给AI过一遍,可以让我们的代码更加靠谱,心里也更加有底气。这就相当于两个人在结对编程,而且和你结对的这个人水平还很高,很少出错,不会骂你是lowB,这样的编程体验,是不是真的好了很多?


一些建议


活到老,学到老。chatGPT和GEN应用的诞生让AI突然在今年爆发。我们能做的就是拥抱变化,积极去学习新式的编程方式,去学习使用AI带来的新工具、新平台。任何事情都有两面性,挑战和机遇永远都是并存。我们要做的不是自怨自艾,而是积极面对未来,未来不仅仅是AI,还会有各种各样不确定的事情等待着我们。正是这些不断出现的新东西让我们的人生更加丰富多彩,身在这个时代,真的很累,也真的很酷。


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

哇咔咔,体验了一把抖音的ChatGPT

说实在的,看到这里,让我忽然想吃过年时候蒸的豆包,不知道字节起这样一个名字有什么用意 最近一直在分享各种AIGC类的东西,感兴趣的可以看下主页历史干货。 你好,我是豆包 礼貌性的回复一句,你好,我是1点东西,我要开始使用你了。豆包是谁呢,可能有些朋友还不知道 ...
继续阅读 »


说实在的,看到这里,让我忽然想吃过年时候蒸的豆包,不知道字节起这样一个名字有什么用意


最近一直在分享各种AIGC类的东西,感兴趣的可以看下主页历史干货。


你好,我是豆包


礼貌性的回复一句,你好,我是1点东西,我要开始使用你了。豆包是谁呢,可能有些朋友还不知道


据悉,“豆包”的前身正是字节内部代号为“Grace”的AI项目。目前在AI浪潮下已经形成独立的AIGC产品供用户使用




刚进来可以看到经典的左右格局,左侧依然是历史问题记录区域,和其他国产GPT产品一样,有一些聚焦的功能模块。


不同的是,显的更加简洁大气,使用柔和不僵硬。毋庸置疑抖音的模型是基于字节产品多年数据沉淀最终服务于子节用户以及更好的发展。



可以看到回答问题响应很快,问题回答干脆不拖泥带水,同样在问题的最下方有点赞、复制、重新生成等功能。需要注意的是最下面有一个搜索功能,点击会跳转到今日头条进行搜索。


体验能力


左侧有英语小助手,先来看下英文能力怎么样




这能力杠杠的,学英语再也不是难事。接下来看全能写作助手体验。






接着问,测试上下文能力。




总体上还算总结的不错,我们问下小日本核废水排海事件




很明显,并不支持联网。




而且没有文生图功能




看下编程能力




编程能力也毫不逊色,最后可以问下GPT3.5都回答错误的问题。看看国产大模型咋样。




OK,今天的一个小分享暂时先到这里。上面的抖音的的申请体验链接:http://www.doubao.com/chat


最近涉猎于AIGC,总结了一些AI资料(实时更新),无套路分享给大家


1点东西AI资料地址



标签:

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

放弃使用Merge,开心拥抱Rebase!

iOS
1. 引言 大家好,我是比特桃。Git 作为现在最流行的版本管理工具,想必大家在开发过程中都会使用。由于 Git 中很多操作默认是采用 Merge 进行的,并且相对也不容易出错,所以很多人都会使用 Merge 来进行合并代码。但Rebase 作为 Git 中主...
继续阅读 »

1. 引言


大家好,我是比特桃。Git 作为现在最流行的版本管理工具,想必大家在开发过程中都会使用。由于 Git 中很多操作默认是采用 Merge 进行的,并且相对也不容易出错,所以很多人都会使用 Merge 来进行合并代码。但Rebase 作为 Git 中主要命令之一,我们还是有必要了解一下,在适合的场景中进行使用。


2. Rebase 的作用


Rebase 中文翻译过来:变基,我觉得这个翻译挺生硬的,导致很多人没有彻底理解变基的含义。我个人把 Rebase 意为 认爸爸,比如可以 Rebase 到马爸爸分支上,成为他的合理继承人。


上图为一次 Rebase 的情况,可以看到最终效果仿佛 Feature 分支没有存在过,新提交的 Commit 像真的在主分支上提交一样。而如果我们用 Merge 就会产生一个合并节点:


可能只看到一次合并所产生的 Commit 节点并没有什么,但实际项目中大概率会变成这样:


简直是乱的一批,仿佛看到了多年前其他人写的一堆代码,啥啥啥,这都是啥!反过来看看采用 Rebase 开发的真实项目,没有对比就没有伤害:


这也是为什么尤雨溪也比较推荐使用 rebase的原因:


3. Rebase 怎么用


其实很多人不用 Rebase ,一方面是不了解实际项目协同中怎么用。另一方面是用了,但问题很多,所以就误认为不好用从而再也不用。这里分享一下,我最近在做项目时所采用 Rebase 方面的协同流程(为了好说明,适当的进行了简化):


3.1 Checkout


首先,我们想从 master 分支上开发新的功能或者修复 bug ,需要 checkout出来一个分支。比如在

A节点中 checkout dev 分支,为了让场景更复杂,在 git checkout dev 分支后。master 上继续有人提交了B、C,形成如下Git 结构:


这里强调一下,很多人用 Rebase 出问题,都是出在了想要 Rebase 的分支是公共分支。其实这里的 dev 应该是只有自己用的分支才合适,回想一下,Git 本身就是分布式版本管理。其实不用远程仓库也是可以非常好的进行版本控制的,我们要将本地分支和远程分支的概念区分的开一些,这俩没有直接联系。所以你本机随便做个 NB 分支一样可以的,Rebase后没人知道你自己起了个什么鬼名字。


3.2 远程管理


如果自己的dev分支并不一定在一台电脑上开展,为了可以自己在多个电脑上开发,我们可以关联了一个自己的远程仓库。这一步是可选的。


3.3 开始变基


现在我们在 dev 上开发了D、E,然后dev rebase master,形成了A、B、C、D、E:


这里虽然看似已经一条直线了,但实际 只有 dev 知道自己的爸爸成为了 master,但 master 并没有认这个儿子。所以我们还需要:master merge dev,这样就在master上形成了一条完美的直线:


最后,再 git push origin master 到远程分支,完成本次开发。


3.4 善后


Rebase 后 dev 由于变基了,相当于已经认贼作父了,现在还想再认回来?休想!所以只能强制解决,在非保护分支中强制push到自己的远程仓库:git push --force origin dev,最后再将dev变基到自己的远程分支:git rebase origin dev,方便自己远程仓库的维护。至此,完成了一次rebase形式的开发,并且可以继续进行下次开发。


4. Rebase 的优缺点


先说说优点:

  • 保持提交历史的线性:使用 merge 合并分支时,会创建一个新的合并提交,从而在提交历史中形成一条新的分支。而使用 rebase,可以将提交记录直接添加到目标分支的末尾,从而保持提交历史的线性。
  • 减少不必要的合并提交:使用 merge 合并分支时,会创建一个新的合并提交,它可能会包含很多无意义的合并信息。而使用 rebase,可以将提交记录逐个添加到目标分支的末尾,避免了创建不必要的合并提交。
  • 更好的代码审查和追溯:使用 rebase,可以让提交历史更加直观和易于理解,从而更容易进行代码审查和问题追溯。
  • 避免冲突的产生:在合并分支时,可能会因为两个分支之间存在冲突而导致合并失败。而使用 rebase,可以在变基之前先解决这些冲突,从而避免了合并时出现的冲突。

总之,虽然 rebase 不是适用于所有情况的万能解决方案,但在大多数情况下,使用 rebase 能够帮助我们创建更加干净和直观的提交历史,提高团队的协作效率。


说了这么说好像都在说 Rebase 的优点,那 Rebase就没有缺点嘛?当然不是,要不然大家早就都从 Merge 转 Rebase了。Rebase 的缺点:

  • 解决冲突繁琐,rebase冲突 是按每个commit来对比的,merge冲突 是按最终结果来对比的,如果用rebase最好是经常去合并一下代码,不然长周期的开发如果就在最后rebase真的是解冲突解到人傻掉。
  • 没有合并记录,Merge 有记录,出了问题好解决。
  • 操作步骤相对繁琐。

5. 结语


协同开发最核心的问题其实就是合并,如何合理的合并,优雅的合并,是每个团队需要考虑的问题。Merge 和 Rebase 作为 Git 中主要的命令,其实各有各的优点,两个一起用也是很常见的。根据自身团队及项目情况,选择合适的方式才是最好的。最后,祝大家合并代码一切顺利~


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

iOS横滑组件实现

iOS
这是我早先实现的一个自定义横滑组件,本文回顾一下当时实现过程遇到的问题和细节,最后有源码地址 文中所有图片托管在Github上 所谓横滑组件其实就如图所示的效果: 列一下UI上的要求:每次滑动一页,有pageEnable的效果每次显示在屏幕中的item...
继续阅读 »

这是我早先实现的一个自定义横滑组件,本文回顾一下当时实现过程遇到的问题和细节,最后有源码地址




文中所有图片托管在Github上



所谓横滑组件其实就如图所示的效果:



列一下UI上的要求:

  • 每次滑动一页,有pageEnable的效果
  • 每次显示在屏幕中的item其实是三个而不是一个
  • 每个item的间距、视图与屏幕边缘的边距严格按照UI上样子

UICollectionView+pageEnable


使用UICollectionView并开启pageEnable是最容易想到的方案,我们来试一下能否满足需要


关键的几个参数如下所示

container.width = 375
collectionView.isPagingEnable = true
collectionView.width = 375
leftPadding = rightPadding = 16
cell.width = container.width - leftPadding - rightPadding
collectionView.contentInset = UIEdgeInset(0,16,0,0)

效果如下所示:



显然,没有达到预期:

  • 问题1,每次滑动停止后,cell的位置不对
    • 通过打印contentOffset得知,UIScrollView开启pagingEnable后的自动翻页,每次修改contentOffset的值等于UIScrollView.width
    • 而且我们无法自定义每次翻页移动的距离
  • 问题2,由于设置了collectionView.contentInset.left,所以第一cell可以移动到屏幕最左边而不能自动还原到初始位置

不甘心,继续调整


我画了一张图来表示要实现的效果:



  • 根据上图的效果,我们希望的效果是每次移动cell时移动的距离(两条红竖线之间的距离)是一个cell的宽度+cell之间的距离--cell.width+interval
  • 既然pageEnable特性每次移动的距离一定是scrollView.width,所以我们可以让scrollView.width = cell.width+interval
  • 这或许能解决上面显示异常问题

我们更新一下配置参数,如下:

leftPadding = rightPadding = 16
container.width = 375
collectionView.isPagingEnable = true
cell.width = container.width - leftPadding - rightPadding
interval = 8
collectionView.width = cell.width + interval
collectionView.contentInset = UIEdgeInset(0,0,0,interval) // 这一句可能会引起你的困惑,但经过测试必须设置成这样,否则效果有问题,本文不做详细解释,跟scrollView自身对于contentSize和contentOffset的调整有关

来看一下效果:



哇,好像不错!但还是有问题:

  • 我们希望同时显示三个cell,但该效果却只能显示1个cell
  • 这是因为collectionView的宽度刚好能显示下一个cell和一个interval,没有更多空间来显示其他cell了

这就很尴尬了,为了利用pageEnable的特性,我们不得不修改collectionView的宽度小一些,但这却导致无法足够的cell个数


所以,结论是:❌


UICollectionView + UIScrollView


在调研其他技术方案时,受一Paging a overflowing collection view启发,可以使用一个UICollectionView和一个UIScrollView一同实现类似效果


核心思想如下:

  • 单独用一个UIScrollView,利用pageEnable特性来实现符合要求的横滑、拖拽翻页效果
  • 单独用一个UICollectionView来利用它的cell显示、复用机制
  • UIScrollView是不显示的,只用它的拖拽手势能力。当拖拽UIScrollView时,将contentOffset的移动应用到UICollectionView中

具体实现过程中有些细节需要注意,比如:

  1. collectionView的contentInset需要设置
  2. 将scrollView的移动应用到collectionView中时如何计算准确
  3. 需要关闭collectionView的panGesture

再放一下效果



结论是:✅


源码地址:SlideView.swift


优缺点


优点很明显:

  • 既复用了UIScrollView的pageEnable手势和动画效果,也复用了UICollectionView的cell复用机制
  • 由于复用了UICollectionView,所以相比通过UIScrollView自定义实现,在一些用户交互体验上可能更好,比如在快速横滑时,自定义的实现可能就没办法快速的准备好每一个cell并无缝从上一页切换过来,可能会有点卡顿
  • 所有实现细节都是通过系统官方的public API,不存在任何trick行为,稳定性好

缺点:


在用户体验上没发现缺点。只是在封装为独立组件时需要注意更多细节,比如:

  • 该组件将CollectionView封装了起来,所以必须给外部使用者暴露dataSource和delegate等必要的回调和数据源方法

使用UIScrollView完全自定义实现


我还看过另一种方案:

  • 自己创建cell视图,添加到UIScrollView上
  • 完全由自己来控制cell的复用和显示逻辑
  • 滑动手势和效果方面,利用UIScrollViewDelegate方法来控制抬起手指后移动到到下一个或上一个cell的效果(该效果我曾经也实现过,可以参考设计与Swipe-Delete不冲突的UIPageViewController

这个思路看上去应该是可行的,我也看过类似的源码实现,是Github上的一个代码


但该源码的显示逻辑写的不好:

  • 每次切换cell时,会同时通过delegate要求更新所有的cell数据(显示在屏幕中的cell和在缓存池中未用到的cell)

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

iOS17兼容问题,[NSURL URLWithString:]逻辑BUG,影响WKWebView

iOS
[NSURL URLWithString:urlString]默认实现逻辑变动 [NSURL URLWithString:urlString]以前的逻辑是urlString有中文字符就返回nil,现在是默认对非法字符(包含中文)进行%转义。 URLWithSt...
继续阅读 »

[NSURL URLWithString:urlString]默认实现逻辑变动


[NSURL URLWithString:urlString]以前的逻辑是urlString有中文字符就返回nil,现在是默认对非法字符(包含中文)进行%转义。


URLWithString:方法并没有给出说明,但是iOS17新增了URLWithString:encodingInvalidCharacters:方法,具体可以参照此方法。

/// Initializes and returns a newly created `NSURL` with a URL string and the option to add (or skip) IDNA- and percent-encoding of invalid characters.
/// If `encodingInvalidCharacters` is false, and the URL string is invalid according to RFC 3986, `nil` is returned.
/// If `encodingInvalidCharacters` is true, `NSURL` will try to encode the string to create a valid URL.
/// If the URL string is still invalid after encoding, `nil` is returned.
///
/// - Parameter URLString: The URL string.
/// - Parameter encodingInvalidCharacters: True if `NSURL` should try to encode an invalid URL string, false otherwise.
/// - Returns: An `NSURL` instance for a valid URL, or `nil` if the URL is invalid.
+ (nullable instancetype)URLWithString:(NSString *)URLString encodingInvalidCharacters:(BOOL)encodingInvalidCharacters API_AVAILABLE(macos(14.0), ios(17.0), watchos(10.0), tvos(17.0));

附带的BUG


这一个改动本来没有什么大问题,但问题是有BUG。


如果urlString中没有中文,那urlString里原有的%字符不会转义。

(lldb) po [NSURL URLWithString:@"http://a.com?redirectUri=http%3A%2F%2Fb.com"]
http://a.com?redirectUri=http%3A%2F%2Fb.com

如果urlString中有中文字符,那么中文字符和%字符都会被转义,最终会影响运行效果。


(我就是因为这个BUG,从而导致原本能正常进行302重定向的页面无法重定向。)

(lldb) po [NSURL URLWithString:@"http://a.com?title=标题&redirectUri=http%3A%2F%2Fb.com"]
http://a.com?title=%E6%A0%87%E9%A2%98&redirectUri=http%253A%252F%252Fb.com

修改方案


对原方法进行替换,保证[NSURL URLWithString:urlString]在iOS17系统上的运行逻辑和iOS17以下系统保持一致。这样对于现有代码逻辑的影响最小。

#import "NSURL+iOS17.h"

@implementation NSURL (iOS17)

+(void)load {
[self sv_swizzleClassMethod:@selector(URLWithString:) withClassMethod:@selector(wt_URLWithString:) error:NULL];
}

+ (instancetype)wt_URLWithString:(NSString *)URLString {
if (@available(iOS 17.0, *)) {
return [self URLWithString:URLString encodingInvalidCharacters:NO];
} else {
return [self wt_URLWithString:URLString];
}
}

@end

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

原生应用要亡了!

iOS
跨平台混合应用(及替代方案)取代了性能优先的原生应用 纯粹的原生应用通常是一种依赖于平台的GUI程序, 它使用特定操作系统的本地开发语言和GUI框架. 例如, Gedit 是一个原生应用, 因为它使用 C 和 GTK 作为实现依赖. Notepad++ 是一...
继续阅读 »

跨平台混合应用(及替代方案)取代了性能优先的原生应用




纯粹的原生应用通常是一种依赖于平台的GUI程序, 它使用特定操作系统的本地开发语言和GUI框架. 例如, Gedit 是一个原生应用, 因为它使用 C 和 GTK 作为实现依赖. Notepad++ 是一个原生应用, 因为它使用 C/C++ 和 Win32 GUI API. 这些原生应用还保留了操作系统特有的UI/UX原则和本地功能. 因此, 电脑用户可以轻松上手并与其他内置原生应用一起使用这些应用. 这些传统的原生应用即使在低端硬件上也能流畅运行, 因为它们没有使用中间消息传递模块或嵌入式渲染/代码执行引擎--它们只是触发内置SDK功能的二进制文件. 原生桌面应用和移动应用开发的情况都是一样的.


混合应用开发运动结束了原生应用开发的黄金时代, 但却创造了一种新的方式, 可以在创纪录的时间内构建类似原生的跨平台应用. 此外, 混合应用的性能问题导致了另一种使用自定义渲染表面和代码执行环境的类原生应用的发展.


让我们来谈谈传统原生应用开发的弊端.


Why Native Apps Are the Best 为什么原生应用是最好的


每个操作系统通常都预装了通用的GUI软件程序. 例如, Ubuntu提供了原生终端, 文本编辑器, Settings应用, 文件管理器等. 这些内置应用无疑遵循了相同的UI/UX原则, 而且由于出色的软件设计和原生SDK的使用, 占用的磁盘空间, 内存和CPU处理能力更低. 第三方原生应用的工作原理也与内置操作系统应用相同. 它们不会过度使用系统资源, 而是根据为用户提供的功能公平地使用计算能力.


从所有面向用户的角度来看, 原生应用都非常出色. 它们绝不会拖慢低端电脑的运行速度. 此外, 它们也不会敦促用户改变操作系统特有的UI/UX做法. 看看Remmina RDP(原生GUI程序)与Ubuntu内置终端的对比:



 Remmina和Ubuntu上的终端


每个移动操作系统都提供了原生SDK, 用于开发特定平台的应用捆绑包. 例如, 您可以使用Android SDK构建高性能, 轻量级和用户友好的移动应用. 看看著名的VLC媒体播放器的Android版本是如何通过XML布局实现"关于"视图的:



 VLC Android项目实现了原生应用视图.


混合应用: 类似本地的Web应用


即使原生应用为用户提供了最好的GUI程序, 为什么现代开发人员还是开始开发混合应用呢? 从应用用户的角度来看, 原生应用是非常好的, 但它们却给应用开发人员带来了一个关键问题. 尽管一些操作系统提供了与POSIX标准类似的底层应用接口, 但大多数内置的应用开发SDK都提供了不同编程语言的不同应用接口. 因此, 应用开发人员不得不为一个软件产品维护多个与平台相关的代码库. 这种情况增加了跨平台原生应用的开发难度, 因为一个新功能需要多个特定平台的实现.


混合应用开发通过提供统一的SDK和语言来为多个平台开发应用, 从而解决了这一问题. 开发人员开始使用Electron, NW.js, Apache Cordova和类似Ionic的框架, 利用Web技术构建跨平台应用. 这些框架在Web浏览器组件内呈现基于HTML的类原生应用GUI, 并通过本地-JavaScript接口和桥接器调用基于JavaScript封装的特定平台本地API. 看看Skype如何在Ubuntu上用HTML呈现类似本地的屏幕:



 Skype的首选项窗口.


桌面应用配有Web浏览器和Node.js运行模块. 移动应用则使用现有的特定平台浏览器视图(即Android Webview).


混合应用解决方案解决了开发人员的问题, 却给用户带来了新的麻烦. 由于基于Web的解析和渲染, 混合应用的运行速度比原生应用慢数百倍. 一个简单的跨平台计算器应用可能会占用数百兆字节的存储空间. 运行多个跨平台应用窗口就像运行多个重型Web浏览器. 不幸的是, 大多数用户甚至感觉不到这些问题, 因为他们使用的是功能强大的现代硬件组件.


混合替代方案的兴起


一些开发人员仍然非常关注应用的性能--他们需要应用在低端机器上也能使用. 因此, 他们开始开发更接近原生应用的跨平台应用, 而不使用Web视图驱动方法. 开发人员开始使用Flutter和类似React Native的框架. 与基于网页视图的方法相比, 这些框架为跨平台应用开发提供了更好的解决方案, 但它们无法像真正的原生应用那样进行开发.


Flutter没有使用原生的, 特定平台的UI/UX原则. React Native在每个应用中嵌入了JavaScript引擎, 性能不如原生应用. 与基于网页视图的方法相比, 这些混合替代方案无疑提供了更好的跨平台开发解决方案, 但在应用大小和性能方面仍无法与真正的原生应用相媲美.


你可以从以下报道中了解Flutter如何与混合应用开发(Electron)竞争:


拜拜Electron, 你好Flutter


混合(和替代方案)赢得了软件市场!


每个商业实体都试图通过开发网站和Web应用进入互联网. 与独立的应用相比, 计算机用户更愿意使用在线服务. 因此, Web浏览器开始改进, 增加了各种以开发者为中心的功能, 如新的Web API, 可访问性支持, 离线支持等. 对开发人员友好的JavaScript鼓励每个开发人员在任何情况下都使用它.


借助混合应用开发技术, 开发人员可以在最短时间内将现有的Web应用转化为桌面应用(如WhatsApp, Slack 等). 他们将React, Vue和Svelte应用与本地窗口框架封装在一起, 创建了功能齐全的跨平台桌面应用. 这种方法节省了数千开发人员的工时和开发成本. 因此, Electron成为了现代桌面应用的开发解决方案. 然后, 一个只需几兆内存和存储空间的代码编辑器程序就变成了现在这样:



 Visual Studio Code占用约600M内存.


一般用户不会注意到这一点, 因为每个人都至少使用8或16GB内存. 此外, 他们的存储设备也不会让他们感受到 500M字节代码编辑器的沉重(TauriNeutralinojs解决了应用大小的问题, 但它们仍在制作混合应用).


同样, 如果应用变得缓慢, 典型的移动用户往往会将责任归咎于设备. 现代用户经常升级设备, 以解决应用开发人员造成的性能问题. 因此, 在当今的软件开发行业, 混合应用开发比本地应用开发更受欢迎. 此外, 混合替代方案(如 Flutter, React Native等)也变得更加流行.


总结一下


混合应用开发框架和其他替代框架为构建跨平台应用提供了一个高效, 开发人员优先的环境. 但是, 从用户的角度来看, 这些开发方法会产生一些隐藏的性能和可用性问题. 现代强大的硬件组件处理能力可以掩盖这些开发方法中的技术问题. 此外, 与依赖平台的原生应用开发相比, 这些方法提供了更富有成效, 开发人员优先的开发环境. 编程新手开始学习桌面应用的Electron开发, 移动应用的Flutter开发和React Native开发, 就像他们跳过C作为他们的第一门编程语言一样.


因此, 原生应用的黄金时代走到了尽头. 幸运的是, 程序员仍在维护旧的原生应用代码库. 操作系统永远不会将其预先包含的应用迁移到混合应用中. 与此同时, 一些开发人员使用类似SDL的跨平台, 高性能原生绘图库构建轻量级跨平台应用. 尽管现代混合应用开发和替代方法已成为软件行业的默认方式, 但我们仍可以保留现有的纯原生定位.


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

如何消除异步的传染性

web
本文的知识点是笔者在抖音看到 渡一前端 发布的视频学习到的。笔者觉得处理问题的思路非常值得学习,因此来掘金分享一下。 前言 各位掘友好!本文跟大家分享一个关于 消除异步传染性 的知识点,你可能不熟悉甚至没听过异步传染性这个词,其实笔者也是最近才看到的,因此想...
继续阅读 »

本文的知识点是笔者在抖音看到 渡一前端 发布的视频学习到的。笔者觉得处理问题的思路非常值得学习,因此来掘金分享一下。



前言


各位掘友好!本文跟大家分享一个关于 消除异步传染性 的知识点,你可能不熟悉甚至没听过异步传染性这个词,其实笔者也是最近才看到的,因此想着来分享一下!好了,接下来笔者会从两个方面来说这个知识点,一方面是概念,另一方面就是如何消除。


什么是 异步传染性


笔者通过一个例子来介绍异步传染性的概念。


CleanShot 2023-08-30 at <a href=10.10.55@2x.png" loading="lazy" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/448eaad1c5f34f319bc3361fcad882ce~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp#?w=1234&h=528&s=154964&e=png&b=282a35"/>


上图中由于m2中的fetch是异步的,导致了使用m2m1变成了async functionmain 又使用了m1,从而main也变成了async function。类似这种现象就叫做异步的传染性。(可能你会觉得,为什么main不直接调m2,我们此处是为了理解这个概念,不要钻牛角尖😁)


m2就好像病毒🦠,m1明知道到m2有毒,还要来挨着,结果就被传染了,main也是一样。


那什么是消除传染性呢?就是希望不要 async/await,让mian、m1变成纯函数调用。也就是mian、m1不依赖fetch的状态。期望像下面这样调用:
CleanShot 2023-08-30 at <a href=10.52.24@2x.png" loading="lazy" src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ba99c7db117e46319d778002889c51ee~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp#?w=924&h=498&s=64284&e=png&b=282a35"/>



纯函数:



  1. 输入决定输出: 纯函数的输出完全由输入决定,即相同的输入始终产生相同的输出。这意味着函数不依赖于外部状态,也不会对外部状态进行修改。

  2. 没有副作用: 纯函数没有副作用,即在函数的执行过程中不会对除函数作用域外的其他部分产生影响。它不会修改全局变量、改变输入参数或进行文件IO等操作。




纯函数在函数式编程中具有重要作用,因为它们易于理解、测试和维护。由于不依赖于外部状态,纯函数可以很好地并行执行,也有助于避免常见的错误,例如竞态条件和不确定性行为。



接下来咱们就分析一下要如何实现消除。


如何消除


当我们把async/await去掉之后,就变成了同步调用,那么m2返回的肯定是pending状态的promisemain得到的也是,肯定达不到我们想要的效果。


那我们能不能等promise变成fulfilled/rejected状态再接着执行main


可以,第一次调用main,我们直接throw,第一次调用就会终止,然后等promise变成fulfilled/rejected状态,我们将返回结果或错误信息缓存一下,再调用一次main,再次调用时存在缓存,直接返回缓存即可,此时也就变成了同步。流程图如下:


CleanShot 2023-08-30 at <a href=11.30.26@2x.png" loading="lazy" src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/dc591e6d9bf24b4eb0d3e78fecf5dcc1~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp#?w=1590&h=1048&s=165391&e=png&b=fdfdfd"/>


具体实现如下:
CleanShot 2023-08-30 at <a href=11.34.06@2x.png" loading="lazy" src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e9b4193e4c6c4a35850c487f1ad0bcbc~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp#?w=1494&h=1336&s=353606&e=png&b=282a35"/>


效果如下:
CleanShot 2023-08-30 at 11.44.35.gif


到此本次分享的内容就完了,感谢阅读!


总结


本文通过简单的例子,描述了什么是异步的传染性,以及如何利用缓存throw重写fetch实现了消除异步的传染性


如果本文对你有一点点帮助,点个赞支持一下吧,你的每一个【】都是我创作的最大动力 ^_^<

作者:Lvzl
来源:juejin.cn/post/7272751454497996815
/code>。

收起阅读 »

给你介绍一个工具,帮你找到未来的努力方向

前言 很多人都会认为,想要找到自己的人生目标是非常难的,主要有 2 个原因: 现在定的目标不一定是正确的,可能过几年之后就变了,何必浪费时间呢 不知道怎么找到自己的人生目标 你是不是也是这么认为的呢? 以前的我也是这么觉得的,所以从来没有探究过:我的人生目...
继续阅读 »

前言


很多人都会认为,想要找到自己的人生目标是非常难的,主要有 2 个原因:



  • 现在定的目标不一定是正确的,可能过几年之后就变了,何必浪费时间呢

  • 不知道怎么找到自己的人生目标


你是不是也是这么认为的呢?


以前的我也是这么觉得的,所以从来没有探究过:我的人生目标是什么?


现在的我却不这么想。探索自己的人生目标,是一件非常有意义,并应该定期去做的事情。因为:



  • 一个明确的、有意义的目标,是产生内驱力的关键要素之一,这个在我的《驱动力》读后感文章中有介绍;

  • 有了人生目标之后,我们对自己的成长就会有相应的规划,带着目的去成长是最快的。即使过了几年,随着人生阅历的增长,我们的人生目标发生了变化也没关系。因为在这个过程中,我们的成长是显著的。而能力的可迁移性也可以帮助自己更容易达成新的人生目标。

  • 探索人生目标的过程,就是一次重新思考自己人生和未来的过程。即使确定不了最终的人生目标,但你可能会发现自己的一些问题和薄弱点,找到未来努力的方向。


如果想通这点,阻碍你的第 1 个困难点已经解决了。接下来我介绍一个工具,帮助你解决第 2 个困难点。


插图1.png


认识 ikigai 人生四叶草


这个工具叫 ikigai 人生四叶草模型,也称为 ikigai 幸福公式。它是日本人追求幸福和满足感的一个重要概念。


ikigai 是由两个词汇组合而成: "iki"意为生命、存在,"gai"意为价值、意义。ikigai 可以被理解为生活的意义存在的价值


让我们先来看下 ikigai 人生四叶草的全貌:


插图2.jpeg


ikigai 强调了四个关键要素的平衡与融合,它们是:



  • 你热爱的事:指的是你对什么感兴趣、乐于投入并带来快乐的事物或活动。

  • 你擅长的事:指的是你具备的技能、知识和专长,能够在某个领域或事业中有所贡献。

  • 世界需要的:指的是你认为社会所需要的、能够为社会做出积极贡献的事物。

  • 你可以得到报酬的事:指的是你能够获得物质或非物质回报的事物,能够为你提供经济支持和满足。


图中每个大圆圈代表 1 个关键要素。再看两个圆圈相交的部分:



  • 激情:如果你在做自己热爱并很擅长的事情,那肯定很有干劲。

  • 使命:如果你非常热爱这个事业,并且认为这项事业是对世界有贡献的,那会充满使命感。

  • 职责:社会(公司)需要你做事,并且会给你报酬,那就是打工仔的职责了。

  • 专业:如果你非常擅长做某件事情,并且别人愿意付钱请你做事,那说明你是专业的。


再看有 3 个圆圈相交的部分,它代表了缺少了其中一个关键要素,那是不美满的:



  • 如果缺少报酬,那么你的经济是不富裕的,虽然你会觉得自己的工作快乐并充实。

  • 如果做的是自己不擅长的事情,有时候就会觉得很不确定,不知道能不能成功。

  • 如果缺少热爱,你就会感到空虚,心里空荡荡的。

  • 如果你认为自己的工作非常琐碎,就会觉得自己是不被重用的。


4 个圆圈重叠的部分就是 ikigai,它代表了 4 个关键要素的融合,也就是我们的人生目标了。


ikigai 人生四叶草的用法


寻找自己的人生目标


ikigai 人生四叶草可以帮助你寻找人生目标。为了让你更好地应用这个工具,我创建了 ikigai 人生四叶草画布工具,关注我的公众号,并回复【ikigai】即可获取这个画布工具。


插图3.png


可以按照以下步骤来使用这个工具:



  • 按照顺序,单独思考 4 个大圈的事项,尽量罗列多一点。

  • 然后按照顺序,思考 2 个大圈相交的事项。

  • 然后找到 ikigai。

  • 最后得出自我总结。


这里最关键的就是 4 个大圈的事项,你需要注意几点:




  • 喜爱的事可以简单分类为消费型和生产型,比如读书是消费型,写作是生产型。




  • 擅长的事除了当前的职业技能之外,还有一些能力上的。主要是思考自己的优势项,如果你不能很好地评估自己优势项的话,可以在网上找些在线测评,或者找周围不同角色的人给你评价。




  • 其实世界需要很多事情,我们不可能把所有事情都列出来。所以,在思考世界需要你做的事情时,可以从以下角度思考:



    • 当前你的身份带来的责任,比如父亲。

    • 当前你的工作。

    • 跟你热爱的事项相关联的,世界可能需要你做的事。

    • 跟你擅长的事项相关联的,世界可能需要你做的事。




  • 别人会付钱的事就是最后的过滤项,主要从世界需要你做的事项中去筛选出别人会付钱的事。




最后的【自我总结】就是在探索完成之后,梳理自己的感受总结以及未来努力的方向。


好了,画布的使用方法介绍完了,这个就是探索自己的人生目标的方法。接下来我介绍一个虚构的例子。


插图4.png


你可能一眼就看出来了,这是一个前端开发技术宅的探索结果。很多人一开始填这个画布的时候也会是这样子,非常简单,事项很少,因为真的不知道自己热爱的事情和擅长的事情。


没关系,当我们把当前的状况填好的时候,可能已经有一些启发了。回到这个例子,可以参考【自我总结】部分:



  • 从图中可以看出,小 A 在前端开发事项已经 3 缺 1 了,如果把最后的热爱补上的话,那不就是完整的 ikigai 了吗?所以,小 A 很有必要思考一下:我喜欢编程吗?我是不是有必要在前端开发的其他领域探索一下自己的兴趣?比如大数据可视化、h5 游戏、虚拟化?

  • 如果真的不喜欢编程,那也不能强求,那就需要重新寻找自己热爱的事项了,因为热爱是人生幸福的最核心前提。可以看到,小 A 现在罗列的都是消费型热爱事项,这些事项是不会同时满足世界需要+别人付钱的,所以小 A 需要思考:我还有其他的生产型的喜爱吗?


制定成长规划


小 A 静下心来思考和感受,发现自己很喜欢尝试新的游戏和新的玩法,平常也很喜欢跟朋友分享好玩的游戏,并且把一些很有意思的游戏心得和游戏经历分享给朋友,大家听了小 A 的分享,也在游戏中获得了快乐,小 A 自己也感到很快乐。


于是,小 A 在【我爱做的事】里面又添加了“分享游戏、分享游戏心得”,并开始思考,世界上有很多很有意思的游戏,但是大部分人都不了解它们,那多可惜呀,如果自己可以帮助其他人找到适合自己的那款游戏,并从中获得快乐,那该有多好呀。


根据这个思路,小 A 就又完成了新一轮的人生目标的探索过程,结果如下:


插图5.png


通过思考和探索,小 A 挖掘到了自己内心隐藏的热爱事项,还思考了如何把这个热爱转变成更有意义的事业。现在,小 A 有了另外一个 ikigai 了,那就是游戏推广运营,但是,小 A 还缺少关键技能呀,想要做成这个事业的话,小 A 需要学习什么知识?培养什么能力呢?


在有了明确的事业目标之后,小 A 就可以按照这个目标来规划自己未来的成长方向了。在了解了行业知识之后,就可以给自己制定未来 1 年的成长计划。还是那句话,带着目的去成长才是最快的!


插图6.png


评估自己的工作


除了寻找自己的人生目标之外,ikigai 人生四叶草模型还有另外一种用法,就是用来评估我们的工作,帮助我们做决策


评估版的 ikigai 人生四叶草画布我也为你准备好了,非常简单,可以看看:


插图7.png


根据 ikigai 幸福公式的定义,我们可以从 4 个维度来评估自己的职业与自己的人生意义的关联程度。因此,你可以给自己想要做的事业从 4 个维度进行打分,1 ~ 10 分,按照自己内心的统一标准来进行打分即可。


【最终得分】一列是 4 个维度得分的加总平均分数。


【加权得分】一列是给某些维度加了权重系数之后的平均分数,这个权重系数可以根据自己的偏好来决定。如果你觉得,对于现在的自己来说,热爱非常重要,那可以给热爱维度加一个非常高的权重。如果你没什么想法的话,我推荐画布工具的默认权重,【热爱】维度给权重 3,【世界需要】维度给权重 2。因为我认为,对于自己的事业,热爱是最重要的。提供价值排在第 2,也非常重要。而技能可以成长、只要有价值,别人就愿意付钱,因此【擅长】和【付钱】就不加权重了。


好了,先来看看小 A 给自己的“前端开发”职业做的评估吧:


插图8.png


经过评估得出分数值后,你能够得到什么信息?


什么信息都得不到,因为没有对比。通常,我们可以做以下 2 种对比:



  • 横向对比:跟其他职业对比,一般是在我们要做转行决策时使用。

  • 纵向对比:跟上一次评估对比,我们可以定期,比如每半年,至少每年,给自己当前的职业做下评估,然后再跟上一次的评估进行对比,这样我们可以通过一些变化项得出我们最近的收获,以及总结出接下来的努力方向。


好了,小 A 又给自己新的热爱事业做了一次评估:


插图9.png


你可以关注一下【最终得分】和【加权得分】的差异点,应该可以理解为什么需要添加适当的权重了。


可以看到,小 A 对游戏推广运营职业的评估中,【我擅长】、【需要我】、【支付给我】相对前端开发职业来说都要低,但这只是暂时的,随着相关技能的学习以及相关行业知识的熟悉,这 3 项都有非常大的成长空间。但相对的,对于前端开发职业来说,薄弱的一项【我喜爱】,就非常难以提升了。


好了,小 A 通过人生意义的探寻以及对于适合自己事业的评估,找到了自己未来的努力方向,接下来,就可以全力以赴地、坚定地往前走了!


小结


今天,我给你介绍了一个工具,叫 ikigai 人生四叶草模型,这个工具可以帮助你:



  • 探寻自己的人生目标

  • 挖掘自己潜在的事业

  • 重新审视自己的内心,找到未来努力的方向

  • 帮助自己做职业决策


可以把 ikigai 人生四叶草模型浓缩成一句话:


ikigai = 热爱 * 擅长 * 价值 * 回报


你可以记住这个本质公式,后续碰到一些相关决策时可以使用这个公式来进行快速地评估。


除此之外,我也提供了一套完整的 ikigai 人生四叶草画布工具,具体用法我已经通过案例详细介绍了,如果你可以通过它定期审视自己的内心,一定会有所收获。


插图10.png



【讨论问题】


如果你认可人生目标的意义和作用,欢迎分享一下你在探索自己人生目标过程中的经验哈。


欢迎在评论区分享你的想法,一起讨论。




作者:潜龙在渊灬
来源:juejin.cn/post/7268260762402340883

收起阅读 »

基于 Axios 封装一个完美的双 token 无感刷新

web
用户登录之后,会返回一个用户的标识,之后带上这个标识请求别的接口,就能识别出该用户。 标识登录状态的方案有两种: session 和 jwt。 session 是通过 cookie 返回一个 id,关联服务端内存里保存的 session 对象,请求时服务端取出...
继续阅读 »

用户登录之后,会返回一个用户的标识,之后带上这个标识请求别的接口,就能识别出该用户。


标识登录状态的方案有两种: session 和 jwt。


session 是通过 cookie 返回一个 id,关联服务端内存里保存的 session 对象,请求时服务端取出 cookie 里 id 对应的 session 对象,就可以拿到用户信息。



jwt 不在服务端存储,会直接把用户信息放到 token 里返回,每次请求带上这个 token,服务端就能从中取出用户信息。



这个 token 一般是放在一个叫 authorization 的 header 里。


这两种方案一个服务端存储,通过 cookie 携带标识,一个在客户端存储,通过 header 携带标识。


session 的方案默认不支持分布式,因为是保存在一台服务器的内存的,另一台服务器没有。



jwt 的方案天然支持分布式,因为信息保存在 token 里,只要从中取出来就行。



所以 jwt 的方案用的还是很多的。


服务端把用户信息放入 token 里,设置一个过期时间,客户端请求的时候通过 authorization 的 header 携带 token,服务端验证通过,就可以从中取到用户信息。


但是这样有个问题:


token 是有过期时间的,比如 3 天,那过期后再访问就需要重新登录了。


这样体验并不好。


想想你在用某个 app 的时候,用着用着突然跳到登录页了,告诉你需要重新登录了。


是不是体验很差?


所以要加上续签机制,也就是延长 token 过期时间。


主流的方案是通过双 token,一个 access_token、一个 refresh_token。


登录成功之后,返回这两个 token:



访问接口时带上 access_token 访问:



当 access_token 过期时,通过 refresh_token 来刷新,拿到新的 access_token 和 refresh_token



这里的 access_token 就是我们之前的 token。


为什么多了个 refresh_token 就能简化呢?


因为如果你重新登录,是不是需要再填一遍用户名密码?而有了 refresh_token 之后,只要带上这个 token 就能标识用户,不需要传用户名密码就能拿到新 token。


而 access_token 一般过期时间设置的比较短,比如 30 分钟,refresh_token 设置的过期时间比较长,比如 7 天。


这样,只要你 7 天内访问一次,就能刷新 token,再续 7 天,一直不需要登录。


但如果你超过 7 天没访问,那 refresh_token 也过期了,就需要重新登录了。


想想你常用的 APP,是不是没再重新登录过?


而不常用的 APP,再次打开是不是就又要重新登录了?


这种一般都是双 token 做的。


知道了什么是双 token,以及它解决的问题,我们来实现一下。


新建个 nest 项目:


 npx nest new token-test


进入项目,把它跑起来:


npm run start:dev

访问 http://localhost:3000 可以看到 hello world,代表服务跑成功了:



在 AppController 添加一个 login 的 post 接口:



@Post('login')
login(@Body() userDto: UserDto) {
console.log(userDto);
return 'success';
}

这里通过 @Body 取出请求体的内容,设置到 dto 中。


dto 是 data transfer object,数据传输对象,用来保存参数的。


我们创建 src/user.dto.ts


export class UserDto {
username: string;
password: string;
}

在 postman 里访问下这个接口:



返回了 success,服务端也打印了收到的参数:



然后我们实现下登录逻辑:



这里我们就不连接数据库了,就是内置几个用户,匹配下信息。


const users = [
{ username: 'guang', password: '111111', email: 'xxx@xxx.com'},
{ username: 'dong', password: '222222', email: 'yyy@yyy.com'},
]

@Post('login')
login(@Body() userDto: UserDto) {
const user = users.find(item => item.username === userDto.username);

if(!user) {
throw new BadRequestException('用户不存在');
}

if(user.password !== userDto.password) {
throw new BadRequestException("密码错误");
}

return {
userInfo: {
username: user.username,
email: user.email
},
accessToken: 'xxx',
refreshToken: 'yyy'
};
}

如果没找到,就返回用户不存在。


找到了但是密码不对,就返回密码错误。


否则返回用户信息和 token。


测试下:


当 username 不存在时:



当 password 不对时:



登录成功时:



然后我们引入 jwt 模块来生成 token:


npm install @nestjs/jwt

在 AppModule 里注册下这个模块:



JwtModule.register({
secret: 'guang'
})

然后在 AppController 里就可以注入 JwtService 来用了:



@Inject(JwtService)
private jwtService: JwtService

这个是 nest 的依赖注入功能。


然后用这个 jwtService 生成 access_token 和 refresh_token:



const accessToken = this.jwtService.sign({
username: user.username,
email: user.email
}, {
expiresIn: '0.5h'
});

const refreshToken = this.jwtService.sign({
username: user.username
}, {
expiresIn: '7d'
})

access_token 过期时间半小时,refresh_token 过期时间 7 天。


测试下:



登录之后,访问别的接口只要带上这个 access_token 就好了。


前面讲过,jwt 是通过 authorization 的 header 携带 token,格式是 Bearer xxxx


也就是这样:



我们再定义个需要登录访问的接口:


@Get('aaa')
aaa(@Req() req: Request) {
const authorization = req.headers['authorization'];

if(!authorization) {
throw new UnauthorizedException('用户未登录');
}
try{
const token = authorization.split(' ')[1];
const data = this.jwtService.verify(token);

console.log(data);
} catch(e) {
throw new UnauthorizedException('token 失效,请重新登录');
}
}

接口里取出 authorization 的 header,如果没有,说明没登录。


然后从中取出 token,用 jwtService.verify 校验下。


如果校验失败,返回 token 失效的错误,否则打印其中的信息。


试一下:


带上 token 访问这个接口:



服务端打印了 token 中的信息,这就是我们登录时放到里面的:



试一下错误的 token:



然后我们实现刷新 token 的接口:


@Get('refresh')
refresh(@Query('token') token: string) {
try{
const data = this.jwtService.verify(token);

const user = users.find(item => item.username === data.username);

const accessToken = this.jwtService.sign({
username: user.username,
email: user.email
}, {
expiresIn: '0.5h'
});

const refreshToken = this.jwtService.sign({
username: user.username
}, {
expiresIn: '7d'
})

return {
accessToken,
refreshToken
};

} catch(e) {
throw new UnauthorizedException('token 失效,请重新登录');
}
}

定义了个 get 接口,参数是 refresh_token。


从 token 中取出 username,然后查询对应的 user 信息,再重新生成双 token 返回。


测试下:


登录之后拿到 refreshToken:



然后带上这个 token 访问刷新接口:



返回了新的 token,这种方式也叫做无感刷新。


那在前端项目里怎么用呢?


我们新建个 react 项目试试:


npx create-react-app --template=typescript token-test-frontend


把它跑起来:


npm run start


因为 3000 端口被占用了,这里跑在了 3001 端口。



成功跑起来了。


我们改下 App.tsx


import { useCallback, useState } from "react";

interface User {
username: string;
email?: string;
}

function App() {
const [user, setUser] = useState<User>();

const login = useCallback(() => {
setUser({username: 'guang', email: 'xx@xx.com'});
}, []);

return (
<div className="App">
{
user?.username
? `当前登录用户: ${ user?.username }`
: <button onClick={login}>登录button>

}
div>
);
}

export default App;

如果已经登录,就显示用户信息,否则显示登录按钮。


点击登录按钮,会设置用户信息。


这里的 login 方法因为作为参数了,所以用 useCallback 包裹下,避免不必要的渲染。



然后我们在 login 方法里访问登录接口。


首先要在 nest 服务里开启跨域支持:



在 main.ts 里调用 enbalbeCors 开启跨域。


然后在前端代码里访问下这个接口:


先安装 axios


npm install --save axios

然后创建个 interface.ts 来管理所有接口:


import axios from "axios";

const axiosInstance = axios.create({
baseURL: 'http://localhost:3000/',
timeout: 3000
});

export async function userLogin(username: string, password: string) {
return await axiosInstance.post('/login', {
username,
password
});
}

async function refreshToken() {

}
async function aaa() {

}

在 App 组件里调用下:


const login = useCallback(async () => {
const res = await userLogin('guang', '111111');

console.log(res.data);
}, []);

接口调用成功了,我们拿到了 userInfo、access_token、refresh_token



然后我们把 token 存到 localStorage 里,因为后面还要用。


const login = useCallback(async () => {
const res = await userLogin('guang', '111111');

const { userInfo, accessToken, refreshToken } = res.data;

setUser(userInfo);

localStorage.setItem('access_token', accessToken);
localStorage.setItem('refresh_token', refreshToken);
}, []);


在 interface.ts 里添加 aaa 接口:


export async function aaa() {
return await axiosInstance.get('/aaa');
}

组件里访问下:



const xxx = useCallback(async () => {
const res = await aaa();

console.log(res);
}, []);


点击 aaa 按钮,报错了,因为接口返回了 401。


因为访问接口时没带上 token,我们可以在 interceptor 里做这个。


interceptor 是 axios 提供的机制,可以在请求前、响应后加上一些通用处理逻辑:



添加 token 的逻辑就很适合放在 interceptor 里:



axiosInstance.interceptors.request.use(function (config) {
const accessToken = localStorage.getItem('access_token');

if(accessToken) {
config.headers.authorization = 'Bearer ' + accessToken;
}
return config;
})

现在再点击 aaa 按钮,接口就正常响应了:



因为 axios 的拦截器里给它带上了 token:



那当 token 失效的时候,刷新 token 的逻辑在哪里做呢?


很明显,也可以放在 interceptor 里。


比如我们改下 localStorage 里的 access_token,手动让它失效。



这时候再点击 aaa 按钮,提示的就是 token 失效的错误了:



我们在 interceptor 里判断下,如果失效了就刷新 token:


axiosInstance.interceptors.response.use(
(response) => {
return response;
},
async (error) => {
let { data, config } = error.response;

if (data.statusCode === 401 && !config.url.includes('/refresh')) {

const res = await refreshToken();

if(res.status === 200) {
return axiosInstance(config);
} else {
alert(data || '登录过期,请重新登录');
}
} else {
return error.response;
}
}
)

async function refreshToken() {
const res = await axiosInstance.get('/refresh', {
params: {
token: localStorage.getItem('refresh_token')
}
});
localStorage.setItem('access_token', res.data.accessToken);
localStorage.setItem('refresh_token', res.data.refreshToken);
return res;
}

响应的 interceptor 有两个参数,当返回 200 时,走第一个处理函数,直接返回 response。


当返回的不是 200 时,走第二个处理函数 ,判断下如果返回的是 401,就调用刷新 token 的接口。


这里还要排除下 /refresh 接口,也就是刷新失败不继续刷新。


刷新 token 成功,就重发之前的请求,否则,提示重新登录。


其他错误直接返回。


刷新 token 的接口里,我们拿到新的 access_token 和 refresh_token 后,更新本地的 token。


测试下:


我手动改了 access_token 让它失效后,点击 aaa 按钮,发现发了三个请求:



第一次访问 aaa 接口返回 401,自动调了 refresh 接口来刷新,之后又重新访问了 aaa 接口。


这样,基于 axios interceptor 的无感刷新 token 就完成了。


但现在还不完美,比如点击按钮的时候,我同时调用了 3 次 aaa 接口:



这时候三个接口用的 token 都失效了,会刷新几次呢?



是 3 次。


多刷新几次也没啥,不影响功能。


但做的再完美一点可以处理下:



加一个 refreshing 的标记,如果在刷新,那就返回一个 promise,并且把它的 resolve 方法还有 config 加到队列里。


当 refresh 成功之后,重新发送队列中的请求,并且把结果通过 resolve 返回。


interface PendingTask {
config: AxiosRequestConfig
resolve: Function
}
let refreshing = false;
const queue: PendingTask[] = [];

axiosInstance.interceptors.response.use(
(response) => {
return response;
},
async (error) => {
let { data, config } = error.response;

if(refreshing) {
return new Promise((resolve) => {
queue.push({
config,
resolve
});
});
}

if (data.statusCode === 401 && !config.url.includes('/refresh')) {
refreshing = true;

const res = await refreshToken();

refreshing = false;

if(res.status === 200) {

queue.forEach(({config, resolve}) => {
resolve(axiosInstance(config))
})

return axiosInstance(config);
} else {
alert(data || '登录过期,请重新登录');
}
} else {
return error.response;
}
}
)

axiosInstance.interceptors.request.use(function (config) {
const accessToken = localStorage.getItem('access_token');

if(accessToken) {
config.headers.authorization = 'Bearer ' + accessToken;
}
return config;
})

测试下:



现在就是并发请求只 refresh 一次了。


这样,我们就基于 axios 的 interceptor 实现了完美的双 token 无感刷新机制。


总结


登录状态的标识有 session 和 jwt 两种方案。


session 是通过 cookie 携带 sid,关联服务端的 session,用户信息保存在服务端。


jwt 是 token 保存用户信息,在 authorization 的 header 里通过 Bearer xxx 的方式携带,用户信息保存在客户端。


jwt 的方式因为天然支持分布式,用的比较多。


但是只有一个 token 会有过期后需要重新登录的问题,为了更好的体验,一般都是通过双 token 来做无感刷新。


也就是通过 access_token 标识用户身份,过期时通过 refresh_token 刷新,拿到新 token。


我们通过 nest 实现了这种双 token 机制,在 postman 里测试了一下。


在 react 项目里访问这些接口,也需要双 token 机制。我们通过 axios 的 interceptor 对它做了封装。


axios.request.interceptor 里,读取 localStorage 里的 access_token 放到 header 里。


axios.response.interceptor 里,判断返回的如果是 401 就调用刷新接口刷新 token,之后重发请求。


我们还支持了并发请求时,如果 token 过期,会把请求放到队列里,只刷新一次,刷新完批量重发请求。


这样,就是一个基于 Axios 的完美的双 token 无感刷新了。

作者:zxg_神说要有光
来源:juejin.cn/post/7271139265442021391

收起阅读 »

为啥count(*)会这么慢?

背景 本没想着写这篇文章的,因为我觉得这个东西大多数有经验的开发遇到过,肯定也了解过相关的原因,但最近我看到有几个关注的技术公众号在推送相关的文章。实在令我吃惊! 先上公众号文章的结论: count(*) :它会获取所有行的数据,不做任何处理,行数加1。 c...
继续阅读 »

背景


本没想着写这篇文章的,因为我觉得这个东西大多数有经验的开发遇到过,肯定也了解过相关的原因,但最近我看到有几个关注的技术公众号在推送相关的文章。实在令我吃惊!


先上公众号文章的结论:



  • count(*) :它会获取所有行的数据,不做任何处理,行数加1。

  • count(1):它会获取所有行的数据,每行固定值1,也是行数加1。

  • count(id):id代表主键,它需要从所有行的数据中解析出id字段,其中id肯定都不为NULL,行数加1。

  • count(普通索引列):它需要从所有行的数据中解析出普通索引列,然后判断是否为NULL,如果不是NULL,则行数+1。

  • count(未加索引列):它会全表扫描获取所有数据,解析中未加索引列,然后判断是否为NULL,如果不是NULL,则行数+1。


结论:count(*) ≈ count(1) > count(id) > count(普通索引列) > count(未加索引列)


我也不想卖关子了,以上结论纯属放屁。根本就是个人yy出来的东西,甚至不愿意去验证一下,哪怕看一眼执行计划,也得不出这么离谱的结论。


我不敢相信这是一篇被多个技术公众号转载的文章!


以下所有的内容均是基于,mysql 5.7 + InnoDB引擎, 进行的分析。


拓展:


MyISAM 如果没有查询条件,只是简单的统计表中数据总数,将会返回的超快,因为service层中获取到表信息中的总行数是准确的,而InnoDB只是一个估值。


实例


废话不多说,先看一个例子。


以下是一张表数据量有100w,表中字段相对较短,整体数据量不算大。


CREATE TABLE `hospital_statistics_data` (
`pk_id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
`id` varchar(36) COLLATE utf8mb4_general_ci NOT NULL COMMENT '外键',
`hospital_code` varchar(36) COLLATE utf8mb4_general_ci NOT NULL COMMENT '医院编码',
`biz_type` tinyint NOT NULL COMMENT '1服务流程 2管理效果',
`item_code` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '考核项目编码',
`item_name` varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '考核项目名称',
`item_value` varchar(36) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '考核结果',
`is_deleted` tinyint DEFAULT NULL COMMENT '是否删除 0否 1是',
`gmt_created` datetime DEFAULT NULL COMMENT '创建时间',
`gmt_modified` datetime DEFAULT NULL COMMENT 'gmt_modified',
`gmt_deleted` datetime(3) DEFAULT '9999-12-31 23:59:59.000' COMMENT '删除时间',
PRIMARY KEY (`pk_id`)
) DEFAULT CHARSET=utf8mb4 COMMENT='医院统计数据';

此表初始状态只有一个聚簇索引


以下分不同索引情况,看一下COUNT(*)的执行计划。


1)在只有一个聚簇索引的情况下看一下执行计划。


EXPLAIN select COUNT(*) from hospital_statistics_data;

结果:



关于执行计划的各个参数的含义,不在本文的讨论范围内,可自行了解。


这里只关注以下几个属性。



  1. type: 这里显示index,说明使用了索引。

  2. key:PRIMARY使用了主键索引。

  3. key_len: 索引长度8字节。


这里有很关键的一点:count(*)也会走索引,在当前情况下使用了聚簇索引。


好,再往下看。


2)存在一个非聚簇索引(二级索引)


给表添加一个hospital_code索引。


alter table hospital_statistics_data add index idx_hospital_code(hospital_code)

此时表中存在2个索引,主键 hospital_code


同样的,再执行一下:


EXPLAIN select COUNT(*) from hospital_statistics_data;

结果:



同样的,看一下 type、key和key_len三个字段。


是不是觉得有点“神奇”。


为何索引变成刚添加的idx_hospital_code了。


先别急着想结论,再看下面一种情况。


3)存在两个非聚簇索引(二级索引)


在上面的基础上,再添加一个二级索引。


alter table hospital_statistics_data add index idx_biz_type(biz_type)

此时表中存在3个索引,主键 、hospital_code 和 biz_type。


同样的,执行一下:


EXPLAIN select COUNT(*) from hospital_statistics_data;

结果:



是不是更困惑了,索引又..又又...变了.


变成新添加的idx_biz_type。


先不说为何会产生以上的变化,继续往下分析。


在以上3个索引的基础上,分别看一下,count(1)count(id)count(index)count(无索引)


这4种情况,与count(*)的执行计划有何区别。



  1. count(1)




  1. count(id)
    对于样例表来说是,主键是pk_id


image.png



  1. count(index)


这里选取biz_type索引字段。




  1. count(无索引)



小结:




  1. count(index) 会使用当前index指定的索引。




  2. count(无索引) 是全表扫描,未走索引。




  3. count(1) , count(*), count(id) 一样都会选择idx_biz_type索引




看到这,你还觉得那些千篇一律的公众号文章的结论正确吗?


必要知识点




  1. mysql 分为service层引擎层




  2. 所有的sql在执行前会经过service层的优化,优化分为很多类型,简单的来说可分为成本规则




  3. 执行计划所反映的是service层经过sql优化后,可能的执行过程。并非绝对(免得有些人说我只看执行计划过于片面)。绝大多数情况执行计划是可信的




  4. 索引类型分为聚簇索引非聚簇索引(二级索引)。其中数据都是挂在聚簇索引上的,非聚簇索引上只是记录的主键id。




  5. 抛开数据内存,只谈数据量,都是扯淡。什么500w就是极限,什么2个表以上的join都需要优化了,什么is null不会走索引等,纯纯的放屁。




  6. 相信一点,编写mysql代码的人比,看此文章的大部分人都要优秀。他们会尽可能在执行前,对我这样菜逼写的乱七八糟的sql进行优化。




原因分析


其实原因非常非常简单,上面也说了,service层会基于成本进行优化


并且,正常情况下,非聚簇索引所占有的内存要远远小于聚簇索引。所以问题来了,如果你是mysql的开发人员,你在执行count(*)查询的时候会使用那个索引?


我相信正常人都会使用非聚簇索引


那如果存在2个甚至多个非聚簇索引又该如何选择呢?


那肯定选择最短的,占用内存最小的一个呀,在回头看看上面的实例,还迷惑吗。


同样都是非聚簇索引。idx_hospital_codelen146字节;而idx_biz_typelen只有1。那还要选吗?


那为何count(*)走了索引,却还是很慢呢?


这里要明确一点,索引只是提升效率的一种方式,但不能完全的解决效率问题。count(*)有一个明显的缺陷,就是它要计算总数,那就意味着要遍历所有符合条件的数据,相当于一个计数器,在数据量足够大的情况下,即使使用非聚簇索引也无法优化太多。


官方文档:



InnoDBhandlesSELECT COUNT(*)andSELECT COUNT(1)operations in the same way. There is no performance difference.



简单的来说就是,InnoDB下 count(*) 等价于 count(1)


既然会自动走索引,那么上面那个所谓的速度排序还觉得对吗? count(*)的性能跟数据量有很大的关系,此外最好有一个字段长度较短的二级索引。


拓展:


另外,多说一下,关于网上说的那些索引失效的情况,大多都是片面的,我这里只说一点。量变才能引起质变,索引的失效取决于你圈定数据的范围,若你圈定的数据量占整体数据量的比例过高,则会放弃使用索引,反之则会优先使用索引。但是此规则并不是完美的,有时候可能与你预期的不同,也可以通过一些技巧强制使用索引,但这种方式少用。


举个栗子:


通过上面这个表hospital_statistics_data,我进行了如下查询:


select * from hospital_statistics_data where hospital_code is not null;

此时这个sql会使用到hospital_code的索引吗?


这里也不卖关子了,若hospital_code只有很少一部分数据是null值,那么将不会走索引,反之则走索引。


原因就2个字:回表


好比去买砂糖橘,如果你只买几斤,那么你随便挑筐里面好的就行。但是如果你要买一筐,我相信老板不会让你在里面一个个挑,而是一次给你一整筐,当然大家都不傻,都知道筐里里面肯定有那么几个坏果子。但是这样效率最高,而且对老板来说损失更小。


执行过程


摘抄自《从根上理解mysql》。我强烈推荐没有系统学过mysql的,看看这本书。


1.首先在server层维护一个count变量


2.server层向InnoDB引擎要第一条记录


3.InnoDB找到第一条二级索引记录,并返回给server层(注意:由于此时只是统计记录数量,所以并不需要回表)


4.由于COUNT函数的参数是*,MySQL会将*当作常数0处理。由于0并不是NULL,server层给count变量加1。


5.server层向InnoDB要下一条记录。


6.InnoDB通过二级索引记录的next_record属性找到下一条二级索引记录,并返回给server层。


7.server层继续给count变量加1。


8.重复上述过程,直到InnoDB向server层返回没记录可查的消息。


9.server层将最终的count变量的值发送到客户端。


总结


写完后还是心中挺郁闷的,现在能从公众号获取到的好文章越来越少了,现在已经是知识付费的时代了。


挺怀念刚工作的时候,那时候每天上午都花点时间看看公众号文章,现在全都是广告。哎!


不过也正常,谁也不能一直为爱发电。


学习还是建议多看看书籍,一般能成书的都不会太差。现在晚上能搜到的都

作者:微笑兔
来源:juejin.cn/post/7182491131651817531
是千篇一律的文章,对错不知。网上

收起阅读 »

如何治愈拖延症

如何治愈拖延症 背景 最近发现我的拖延症很严重了😭😭,看了一下我的抖音主页,我已经很久没有去跑步了。最近的一次跑步的记录停留在了8月23日,周三。我的这篇文章写在周天的上午,掐指一算,已经有三天晚上没有跑步了。我不大喜欢给自己找借口,没有行动就是没有行动。 ...
继续阅读 »

如何治愈拖延症


背景


最近发现我的拖延症很严重了😭😭,看了一下我的抖音主页,我已经很久没有去跑步了。最近的一次跑步的记录停留在了8月23日,周三。我的这篇文章写在周天的上午,掐指一算,已经有三天晚上没有跑步了。我不大喜欢给自己找借口,没有行动就是没有行动。


我的抖音打卡


就拿我昨天晚上来说吧,吃完饭已经是8点了,这个点没啥问题。和家里通了半小时的电话之后,发现手机没电了,于是又在充电。等到九点的时候,电池的电量还在30%左右,我知道我的手机电池不大行,不足以支撑一个小时,于是就放弃了😅。


但是当早上我坐在电脑前的时候,发现昨天的好多事情都没有完成,今天的事情又得往后推了。越堆积越是多,都喘不过气来了🤥。



哈哈🤭🤭,也不好意思让大家看到下周的推文内容啦,算是提前剧透了😎



我的todo list


我就不断的在思考,为什么我的执行力不行了。我觉得我的代言词就是:一个有思想有行动力的程序员。现在看来,我是一个懒惰、带有严重的拖延症的程序员了。不行,这个问题得治,不然我会更加的焦虑,堆积更多的任务导致更低的效率。


分析


结合这个低效率的周末,我反思了我为什么效率这么低。


🕢推迟开始


我发现我总喜欢做todo list,但是很少去看,也很少去核对一下我当前的进度。总觉得一天的时间很长,我可以先去做别的事情,比如碎片化的短视频、吃吃吃、发呆。于是一件件的本在计划中的事情被不断的推迟了。


⏲时间管理困难


从我8:00起来到晚上的凌晨入睡,减去我个人清洁、做饭、午睡,我剩下的时间大约是10个小时。但是,我一对比下来,我的时间利用率仅仅是40%,相当于我只有4个小时是在满满当当的学习的。我之前的ipad在的时候,我会用潮汐这个软件把我的时间分割成一个小时一个小时的。现在没了,我发现我的时间规划真的出了大问题。


🤖自我控制力下降


我觉得最近一年的时间,我真的太放松自我了。我的技术成长、学习上长进也是微乎其微。我总结下来就是因为我的自控力太差了,或者说没有承受着外界的干扰。因为一个短视频就可以刷上一个小时的短视频,因为一个好物就会不断的逛购物软件......碎片化的时间消耗,最终导致了效率低下。


解决方案


针对以上我总结的问题,我决定对症下药。


🧾明确的计划


我觉得我明确的计划真的很必要。就像我公众号shigen里面给自己定的一个目标一样:



2023年的8月开始,我先给自己定一个小目标:公众号文章不停更



《终于,我官宣了》文章


“不停更”的意思是我每天都要更新文章。我的推文里还带了“新闻早知道”栏目,我哪天没更新或者说更新晚了,我就觉得目标没有实现了,新闻也没什么意义了。我觉得日常的计划和这个目标的设定和实现有着相似的地方,我要把我的计划和目标更明确一点。🤔🤔比方说我今天要干嘛,我完成了怎么样了。


优先级


事情分清楚轻重缓急,我记得我在实习的时候,就有一次因为项目要上线和我一点不大紧要的事情次序搞混了,导致晚上加班上线。现在的我也是,很多重要的事情也是放到了最后做甚至只延期了。所以,我的行动之前,得先做最要紧的事情。但是也会混杂一些个人的情绪在里边,比方说明明一件事情很重要,但是自己就是不想做或者说觉得事情很简单,我先做最有意思的事情。很多时候都是这样的,兴趣和意义占据了主导因素,优先级反而不是那么重要了。


抗拒干扰


手机就在我的边上,这很难不因为一个消息或者一个发愣就去拿起手机,一旦拿起来就放不下了。所以,我觉得最好就是把它放在我的抽屉里,然后眼不见就不去想它了。


奖励惩罚机制


最后,我觉得奖罚分明也挺重要的。在这里,我也想起了我在一线的时候,我周末总会有一天去我住的地方隔壁去逛超市,每次的消费金额大约在100-150左右。但是我出去的前提是我的学习目标完成了或者代码写完了。我现在却相反,目标缺少了一个验收和奖惩的过程。我觉得和我更喜欢宅有一点关系了,所以,我也得奖励我自己一下:目标完成了可以去逛超市消费🛒,也可以去骑行🚲;但是没完成,健腹轮😭😭安排上!


好了,以上就是我对于最近的拖延症的分析和解决方式的思考了。也欢迎伙伴们在评论区交流一下自己对于拖延症的看法。


shigen一起

作者:shigen01
来源:juejin.cn/post/7272690326401728547
,每天不一样!

收起阅读 »

谈谈这十年的代码生涯👨‍💻

博客终于完成了,借着这次机会,我想好好回顾总结一下这十年的代码生涯。有人曾说过如果一个人专注做一件事做十年,那么他会成为这个行业的大师。先别在意其出处与真假,遗憾的是这十年我并没成为专家或大师,甚至,于这个行业而言我才算刚刚入门。 三句话总结这十年 空白:不...
继续阅读 »

博客终于完成了,借着这次机会,我想好好回顾总结一下这十年的代码生涯。有人曾说过如果一个人专注做一件事做十年,那么他会成为这个行业的大师。先别在意其出处与真假,遗憾的是这十年我并没成为专家或大师,甚至,于这个行业而言我才算刚刚入门。


三句话总结这十年



空白:不知道想要什么、想干什么、喜欢什么



image.png



活着的意义便是寻找活着的意义:挣扎、困顿、精神内耗、努力寻找出口



image.png



在我离开之前想要多认识这个世界一些:算是找到自己的信仰、人生的方向,不以物喜、不以己悲也不再随波逐流、随风飘扬



image.png


我想大多数人都会跟我一样经历这几个阶段,对此我想分享一些自己的经验看法:



  • 正在经历第一个阶段的小伙伴们不要焦虑,认识自己本就是人的一生中最复杂的事情,也不可能一天两天一年两年就能完成,因此再急、再焦虑都只是徒增烦恼而已,倒不如先做好眼前的事,而后慢慢的一步一步的认真的寻找和认识自己。

  • 正在经历第二个阶段的小伙伴们不要担心、恐惧,其实我比较赞同余华老师的观点,有时候精神内耗其实是一个好事情,因为你不安于现状,不满足于此,你在寻找出口也在认识你自己。这个过程毫无疑问是极度痛苦的,但你要相信不认识黑暗是无法知晓光明的,没有经历困顿挣扎也无法看清前方的路。

  • 正在经历第三个阶段的小伙伴们,首先恭喜你们找到了能为之奋斗终生的事业!但切勿忘了这个世界上唯一不变的就是变化,我们仍要面临许多挑战、选择与诱惑,坚持还是另寻他路?这是个问题!


由于从小就深受电影影响,看着那些"黑客"只需要动动键盘敲下几行神秘的英文就可以破解万事万物,当时觉得他们简直无所不能,他们的技能简直比魔法师还炫酷,由此编程的种子就种在了我的心里。


在读高一时机缘巧合下我参加了学校的技能大赛,便满心憧憬的开始了编程的修炼之路。当时主要还是用C#和winform框架写windows软件,我仍记得我写下的第一个软件——“识别十进制的位数”,我觉得计算机真是世界上最奇妙的物品了!会编程真的是太酷了!


image.png


第一个软件:"识别十进制的位数"


至此便一发不可收拾,也写很多很多有趣的玩意,界面也逐渐美观起来。





一个计数器





仿QQ界面





1024游戏


误入硬件


原本我是打算在大学读计算专业的,可是命运总是喜欢跟我们开玩笑,最后阴差阳错读了电子专业。不过还好,电子专业也没有跳脱编程的范畴,只不过面对的对象不一样了而已。不过由于自己一开始对软件开发的依赖导致我整个大学生涯对电路设计有着天生的反感,这也导致了我目前也没玩明白电路😅。不过也正因我的软件开发基础致使我许多编程课程都学得非常轻松😁


记得第一次接触硬件编程是STM8,当时作业是利用仿真软件基于STM8写个计数器,由于之前有软件的编程基础,在了解硬件一些逻辑后很快就上手了,于是乎很快我的第一个硬件作品也诞生了——"基于STM8的计数器"





基于STM8的计数器


在完成第一个硬件作品后,简直感觉成就感拉满!当时我觉得做硬件比做软件还要有趣,因为硬件是实实在在看得见摸得着的,当它在跟着你预期的逻辑一步一步动起来的时候,荷尔蒙会飙升直击你的大脑。


有了第一次做硬件的快感,自己便上瘾了起来,开始参加各式各样的比赛。记得第一个参加的是校内的硬件比赛,当时做的是一个无线充电循迹小车,这对于当时的我而言真的是个超级工程,整个项目有着6个传感器,2个电机,若干个led灯需要控制,这对新手而言并不友好😭,不过好在熬了几个通宵还是把它完成了!





无线充电循迹小车


之后各种作业、比赛自己又陆陆续续做了许许多多的作品





RGB蓝牙灯





局域网点歌器





一个超酷的软件


除此之外,我自学了大概半年的深度学习,不过就学了有监督部分,自己做了一些非常有趣的东西!





手写字体识别





银行卡号识别


大家可以从我的作品中看到我是一个彻头彻尾的垃圾佬,由于经济窘迫,所有作品都是泡沫板、雪糕棍、热胶等等拼接而成(我要是有钱绝对搞一个3D打印机😶‍🌫️)。不过有三件作品可以说是我这个垃圾佬的得意之作,它是真的有用真的帅呀!


第一件:恒温箱


这是我的一个课程作业,当时老师要求围绕PID算法做一个作品。考虑到倒立摆已经被玩烂,因此思来想去做个冰箱吧,主要是正好寝室缺个冰箱,除了缺个冰箱以外,还缺个加热箱(冬天室友带饭会冷的)。好的,那就做个恒温箱吧!





紧锣密鼓调试中....


熬了几周终于弄好了,它是真的帅呀!不但可以实现最低-10°C的制冷还可以实现最高60°C的保温,重点是它不是PWA的粗稳,而是PID的精稳呀!!!!





成品


第二件:基于STM32的游戏机


这件作品也是我的一个课程作业,当时一直都想给自己的作品上摇杆(总觉得摇杆真的很帅),但一直没有机会,正好这次可以用一用。之所以觉得这件作品很酷,一是它很简约(东西不多)一块芯片、一个喇叭、一个三极管、二个摇杆,二是它是唯一件自己从腐蚀板子到完成全由自己动手的一件作品(以前板子都是在嘉立创画好,最后直接就收到成品板子了),因此觉得它很格外的珍贵!


image.png


image.png


第三件:消防喷水枪


这件作品就是比赛作品了,完完全全由垃圾拼接的,哈哈哈哈哈!可以看到转向用的是回收的摄像头拆下来的外壳,底座是月饼盒子,管子是割的亚克力水管,哈哈哈哈,整个作品最贵的就是红外温度传感器了,当时买着是几十块还是百来块?不记得了,总之精度很差很差,导致远距离的火苗识别不到,气死了🤯!当时也有考虑摄像头方案,太贵了买不起呀😭!!!


image.png


微信图片_20230823150823.png


入坑Web


在大三的时候,为了凑学分,我选了一门学分很高的课——《网页设计与实现》,当时的我又如何能想到这会是我以后为生的技术呢!那时候虽然每天都在使用网页,但对于网页的实现是一窍不通,也更看不懂网页中那些恼人的代码,虽然看起来跟我在WPF中使用过XML非常相似,但由于没有深究,则以为它们只是长得像而已,除此之外并没有什么瓜葛。


在此我先要感谢一下这门课的老师,他实在是教会了我们太多东西,这门课也是我整个大学生涯中收获最大最多的一门课(没有之一),相较于他而言,其他课程老师则显得格外不称职。


记得当时我们还是使用的Dreamware写代码,当时第一个网页写的是table布局显示LPL排名(游戏中二少年,哈哈哈),那时候还不懂css并且也不知道什么HTML标签,就学了个< table>





第一个网页


学了CSS之后开始花哨起来,右边的奖杯是旋转的,整个底图是个视频,当时效果真的贼震撼贼帅(依旧是中二的LOL)





学了CSS之后的网页


在掌握了JS之后,突然就感觉打开了任督二脉,最终做了一个网页音乐播放器,背景地图也是视频,并且会跟着歌曲变化,效果究极炸裂!





学了JS之后的网页


大四创业


大三下之后我们就没什么课了,然后在导师的帮助下去了他朋友的公司实习。当时的工作内容主要是后端,这段时间自己也学了非常非常多的东西,主要就是一些后端的框架/中间件之类的,包括SpringMCVC、Springboot、kafak、redis等等,当时自己也乐在其中(主要是有钱还能学东西)。我以为我会慢慢度过实习期,然后大四找工作成为一名后端工程师,但是命运总是喜欢跟我们开玩笑的。突然有一天我的一个学长跟我说需要一套针对他们公司的管理系统,就这样我约上了三个小伙伴走向了创业的路。


完成第一版


由于团队的小伙伴没人愿意写前端(当时普遍对前端有偏见,觉得写页面没有什么技术含量)于是不得已我便承担了前端开发的工作,由于这次合作是我主导的,因此我也承担了需求分析、数据库/UI设计等工作,其余的小伙伴分别负责后端/小程序/IOS开发。


大约在进行了2周的需求分析后,我们开始了开发工作,但很快我们遇到了第一个问题——“我们做的并不是他们想要的”,并且他们常常天马行空,一天一个想法,这导致我们不得不停下开发工作思考下一步如何做。最终在查阅资料和讨论后,我们决定先仔细了解分析需求!通过深入了解他们公司员工的工作,了解整个公司的业务流程,从而知晓他们的痛点,并且同时构造出整个公司的业务流程图以及每员工/用户在其中的位置,也就是用例图。


最终我们花了大概一个多月的时间完成了用例图,然后花了大概三个多月的时间,按照他们的需求编写出了第一套管理系统。该系统包含一些常见的公司事务,例如:请假、打卡、薪资计算也有针对他们公司本身业务的工单系统等。





管理系统V1.0


发现问题


但很快问题便来了,由于公司架构/人员/流程/功能字段时常变更,但我们又是将流程/人员在代码中写死的,这导致他们每发生一次变更我们就需要改一次代码,这让我们非常头疼,于是我们寻求解决方案。


我们通过查找资料、看书、看社区很快找到了解决方案——"sass"。自20世纪90年代以来,以互联网为核心的现代信息技术在世界范围内迅猛发展,基于互联网为载体的信息化软件服务的在线租用模式SaaS(Software as a Service软件即服务)日渐成熟,为中小企业开展信息化建设提供了更合理更高效的发展平台。当然对于软件开发商而言从卖“代码”转变到卖“服务”的难度是可想而知的,他们需要对业务进行高度的抽象,从中找出它们共性与差别,以此用同一套代码来满足不同企业的输入、处理、输出数据三个环节。





程序模型


因此当前SaaS类系统的抽象也是从这三个环节入手的,数据输入部分为动态表单引擎、数据处理部分为流程引擎与计算引擎、数据输出部分为报表引擎。表单引擎作用是由用户拖动一些组件构成所需收集的信息,有点类似目前许多的在线问卷调查网站;流程引擎决定了用户填写数据的处理对象和流程走向;计算引擎计算和处理填写的数据;报表引擎则是自定义展示数据内容。





SaaS软件主要组成部分


如今我依旧认为“sass”是当下中小型企业降本增效最好的且最优的途径,sass的发展远不为此。找到解决方案后我们深知这次改动将是巨大的,并且这个项目也会变得非常困难的,因此我们决定把一切推倒重来并把之前的遇到的一系列问题全部解决。


推到重来


我们决定使用码云管理代码,各个端的代码分别存储,且加上一个文档仓库。以解决我们消息闭塞无法总览、难以合并、查阅代码以及分配、总览任务问题。


image.png


仓库架构


文档仓库中存放需求分析文档、用例图、架构图、数据库设计、接口设计等内容,方便大家查看修改


image.png


文档仓库存放内容


我们再次对需求进行了更加仔细的分析,结合以此进一步完善和改进了系统用例图。


image.png


系统用例图


由于之前开发中经常遇见忘记数据库依赖关系,无法总览全局的问题,因此我们决定根据用例图先画出数据库设计图,设计图让我们能够更加直观的看到各个模块的依赖关系,并且每次修改我们只需要共同基于设计图改动即可,后续可根据设计图生成表结构。


image.png


数据库设计


根据用例图与数据库设计,我们设计出了系统整体架构


image.png


系统架构


在之前的开发过程中,由于我们没有接口文档,这导致会有许多耗费时间且多余的沟通步骤,并且一些code码、状态消息等内容并未得到统一,因此我们也规定了相应接口文档的格式、参数等。


image.png


接口文档


我们开始规定、分配任务,并将其统一放入码云管理,规定其任务周期以及里程碑,对项目整体时间进行监管把控。


image.png


任务规划、分配、监管


完成第二版


在完成了这一系列的前期工作后,我们便开始着手开发了,大约用了半年多的时间,我们成功的完成了大部分的内容。正如我们起初设想的那样,整套系统分为三个端:管理端(web)、员工端(App)、用户端(小程序)。


image.png


系统总览


管理端主要设置整套系统的应用/查看应用提交的数据内容,应用分为固有应用与自建应用。固有应用指的是无法使用动态表单生成的应用,需要直接通过编写代码;自建应用则指的是用户可以自行通过动态表单/流程引擎/视图引擎创建的应用。


image.png


管理端设置页面


自建应用通过表单引擎拖拽组件生成业务需要填写的信息、通过流程引擎决定该业务的流程,并规定该业务的权限,面向的使用对象等,视图引擎则决定了该表单需要在首页中展示/统计的数据。创建应用完成后,该功能则会同步出现在有权限的用户的小程序中以供填写数据申请业务。


image.png


创建自建应用


image.png


设置应用表单


image.png


设置应用的流程





用户端显示应用





用户端提交申请


image.png


根据设置的流程流向相应人员处理业务


image.png


总览该应用数据


为了满足自建应用无法完成的需求,我们也写了许许多多的固有应用,包括了员工打卡、仓库管理等等,但遗憾的是我并没有记录下相关的内容图片。此外我们准备着手重构动态表单部分代码,然后将其开源!


image.png


准备开源的动态表单


在我们完成这版系统后,恰逢毕业论文选题,于是乎我选择了自主命题,并打算以这段工作来完成自己的毕业论文!


image.png


毕业论文


发布


在我们完成第二版没多久,我们便开始对外开始宣传这套系统,为此我们做了许许多多的工作,由于资金非常紧张,我们负责了许多设计工作,包含设计了一些海报、易拉宝、宣传册等等。


image.png


自己设计的相关海报





自己设计的宣传册


临近毕业,我们宣告失败


在临近毕业前几天,很遗憾最终我们还是失败了,失败的原因有很多,但直接原因是我们遇到了无法解决的技术难题。该系统一开始其实就是一个问卷调查系统然后加入一个流程引擎,这造成了一个问题,动态表单与动态表单之前无法产生数据关联,导致形成了数据孤岛。为此我们在动态表单中加入数据关联组件,也解决了一对一、一对多的关联,但多对多一直无法解决,大概挣扎了两个多月,但还是没有寻得解决方案。


虽说直接原因是技术难题,但我知道对于一个团队而言是永远不会被技术难倒的。其实主要原因还是我作为团队负责人,在团队遇到挫折与困难时没有积极调和团队氛围,不但没有积极鼓舞团队成员,反而还因为困难整天闷闷不乐,导致团队氛围跌至谷底,最终解散。其次我并没意识到软件是迭代出来的,并不是一面世就是完美,但我每次遇到问题时都急于解决不去划分轻重缓急,常常推到重来,这虽然使得软件更加完美,但也使得软件开发周期不断延期,也不断重复多项工作,打击成员的积极性。


当然团队成员也或多或少有问题,但归根到底还是我的问题,我并没有挑选更为合适的人选加入团队,这导致耽误了团队,也耽误了他们,我直到现在仍觉得愧对于他们!


虽然说项目最后失败了,但我并不后悔,直到如今我也常常怀恋我们把酒论码、午后敲码的日子,收获很多也很快乐!


image.png


image.png


image.png


回首十年


记得临近毕业时我的导师曾对我说“你太浮躁了,希望你离开学校之后好好改一改”。起初我也认为自己浮躁,常常东搞一下,西搞一下,并且急于求成,但如今我却有了不同的看法,是浮躁但不完全浮躁,我觉得我本质上是在寻找。由于刚进入大学,各种技术眼花缭乱,而且我并不知道对于这个专业而言,我应该打好什么样的基础,这些基础对应这哪些方向,以及最重要的,我应该如何才能学好这个专业。虽然我整个大学都非常努力,但遗憾的是直到我大学毕业都没有搞清楚这些基本的问题,一直在技术表面跳来跳去。由于缺乏清晰的认识以及能告知你这些的人,因此我唯有的办法则是不断地试,我只有不断的去尝试才能知道上述问题的答案,我也只有不断的尝试才能知道接下来要走的路。



最近又读了一遍《月亮与六便士》,产生了很多新的感悟,我想作为这篇文章的结尾再好不过了。



我们就如同被船掀起的浪花,被前浪牵着走,被后浪推着走。大多数浪花都会随波逐流,有一些浪花在这个过程中趁着风势逐渐变大变高,惹得其他浪花羡慕追随,给浪营造了强大的假象。也有极少数浪花有了此生必要到达的目的,开始挣脱前浪的牵引,摆脱后浪的束缚,这也打得前后浪措手不及,乱了阵脚,伤痛欲绝。


此时的我们想必早已争相着各抒己见,吵得不可开交。更有甚者大肆宣扬诸如“满地的六便士他却看到了月亮”等等片面观点想要其定为“真理”,殊不知我的叔叔亨利早已说过“魔鬼总是随心所欲地引用经文。他记得从前一个先令就能买到十三只上等的牡蛎。”


最后提一句,大学教育改革并不应该一味给大学生增压,因为我认为其本质并不是大学生懒散不愿意学,而是即使是努力学了也不会有太多收获。

作者:汪啊汪QAQ
来源:juejin.cn/post/7270464435297501196

收起阅读 »

求求别再叫我切图仔了,我是前端开发!

web
☀️ 前言 大家好我是小卢,前几天在群里见到有群友抱怨一周内要完成这么一个大概20~30页的小程序。 群友: 这20多个页面一个星期让我开发完,我是不相信😮‍💨。 群友1: 跑吧,这公司留着没用了,不然就只有自己加班。 群友2: 没有耕坏的田,只有累死...
继续阅读 »

☀️ 前言





  • 大家好我是小卢,前几天在群里见到有群友抱怨一周内要完成这么一个大概20~30页的小程序。



    • 群友: 这20多个页面一个星期让我开发完,我是不相信😮‍💨。

    • 群友1: 跑吧,这公司留着没用了,不然就只有自己加班。

    • 群友2: 没有耕坏的田,只有累死的牛啊,老哥!🐮。

    • 群友3: 用CodeFun啊,分分钟解决你这种外包需求。

    • 群友2: 对喔!可以试一下CodeFun,省下来的时间开黑去。




  • 在我印象中智能生成页面代码的工具一般都不这么智能,我抱着怀疑的心态去调研了一下CodeFun看看是不是群友们说的这么神奇,试用了过后发现确实挺强大的,所以这次借此机会分享给大家。




🤔 什么是 CodeFun



  • 大部分公司中我们前端现在的开发工作流大概是下面这几步。

    • 一般会有UI先根据产品提供的原型图产出设计稿。

    • 前端根据设计稿上的标注(大小,边距等)进行编写代码来开发。

    • 开发完后需要给UI走查来确认是不是他/她想要的效果。

    • 如果发现有问题之后又继续重复上面的工作->修改样式->走查。






  • 我们做前端的都知道,重复的东西都可以封装成组件来复用,而上面这种重复的劳作是我们最不想去做的。

  • 但是因为设计图的精细可能有时候会有1px的差异就会让产品UI打回重新编写代码的情况,久而久之就严重影响了开发效率。

  • 我时常会有这么一种疑惑,明明设计稿上都有样式了,为什么还要我重新手写一遍呢?那么有没有一种可能我们可以直接通过设计稿就自动生成代码呢?

  • 有的!通过我的调研过后发现,发现确实CodeFun在同类产品中更好的解决了我遇到的问题。




  • CodeFun是一款 UI 设计稿智能生成源代码的工具,可以将 SketchPhotoshopFigma 的设计稿智能转换为前端源代码。

  • 8 小时工作量,10 分钟完成是它的slogan,它可以精准还原设计稿,不再需要反复 UI 走查,我觉得在使用CodeFun后可以极大地程度减少工作流的复杂度,让我们的工作流变成以下这样:

    • UI设计稿产出。

    • CodeFun产出代码,前端开发略微修改交付。





🖥 CodeFun 如何使用



  • 接下来我就演示一下如何快速的根据设计稿来产出前端代码,首先我们拿到一个设计稿,这里我就在网上搜了一套Figma的设计稿来演示。

  • 我们在Figma中安装了一个CodeFun的插件,选择对应CodeFun的项目后点击上传可以看到很轻松的就传到我们的CodeFun项目中,当然除了FigamaCodeFun还支持Sketch,PSD,即时设计等设计稿。

  • 我们随便进入一个页面,引入眼帘的是中间设计稿,而在左侧的列表相当于这个页面的节点,而我们点击一下右上角的生成代码可以看到它通过自己的算法很智能的生成了代码。

  • 我上面选择生成的是React的代码,当然啦,他还有很多种选择微信小程序Vueuni-app等等等等,简直就是多端项目的福音!不止是框架,连Css预处理器都可以选择适合自己的。

  • 将生成的代码复制到编辑器中运行,可以看到对于简单的页面完全不用动脑子,直接就渲染出来我们想要的效果了,如果是很复杂的页面进行一些微调即可,是不是很方便嘿嘿。

  • CodeFun不管是根据你选择的模块进行生成代码还是整页生成代码用户进行复制使用之外,它还提供了代码包下载功能,在下载界面可以选择不同页面,不同框架,不同Css预处理器,不同像素单位

  • 如果是React相关甚至还会帮你把脚手架搭建好,直接下载安装依赖使用即可,有点牛呀。



🔥 CodeFun 好在哪



  • 笔者在这之前觉得想象中的AI生成前端代码的功能一直都挺简陋,用起来不会到达我的预期,到底能不能解决我的痛点,其实我是有以下固有思想的:

    • 生成代码就是很简单的帮你把HtmlCss写完嘛但是我们不同框架又不能生成。

    • 生成代码的变量名肯定不好看。

    • 生成的代码肯定固定了宽高,不同的手机端看的效果会差很多。

    • 平时习惯了v-for,wx:for,map遍历列表,这种生成代码肯定全部给你平铺出来吧。



  • 但是当我使用过CodeFun之后发现确实他可以解决我们很多的重复编写前端页面代码的场景,而且也打消了我对这类AI生成前端页面代码功能的一些固有思想,就如它的slogan所说:8 小时工作量,10 分钟完成


多平台、多框架支持



  • 支持 Vue 等主流 Web 开发框架代码输出。

  • 支持微信小程序代码输出,当你选择小程序代码输出时,像素单位会新增一个rpx的选项供大家选择。

  • 使用最简单的复制代码功能,我们可以快速的将我们想要的样式复制到我们的项目中进行使用 。

  • 笔者在使用的过程中一直很好奇下载代码的功能,如果我选择了React难不成还会给我自动生成脚手架?结果一试,还真给我生成了脚手架,只需要安装依赖即可,可以说是很贴心了~。



循环列表自动输出



  • 我们平时在写一个列表组件的时候都喜欢使用v-for,wx:for,map等遍历输出列表,而CodeFun也做到了这种代码的生成。

  • CodeFun在导入设计稿的时候会自动识别哪些是list组件,当然你也可以手动标记组件为List

  • 然后再开启“将 List 标签输出为循环列表”选项即可自动根据当前选择的框架生成对应的循环遍历语法,确实是很智能了~



批量数据绑定




  • 在我们平时Coding的过程中都不会把数据写死,而是用变量来代替进行动态渲染,而CodeFun支持批量数据绑定功能,我们可以把任何在页面中看到的元素进行数据绑定和命名修改




  • 就拿上面的循环列表举例吧,在我们一开始识别的Html中,遍历循环了一个typeCards数组,每一个都展示对应的信息,我们可以看到这里一开始是写死的,而我们平时写的时候会将它用变量替代。




  • 我们只需要点击右上角的数据绑定进行可视化修改即可,我们可以看到它的全部写法都改成了变量动态渲染,这就很符合我们平时编码的套路了。





一键预览功能



  • 有很多同学反馈在之前做小程序的情况下需要将代码编写完整并跑起来的情况下,使用微信的预览功能才可以看到效果,会比较繁琐

  • CodeFun支持直接预览,当我们导入设计稿后,选择右上角的预览功能可以直接生成小程序二维码扫码即可进行预览,好赞!。



更加舒适的“生成代码”



  • CodeFun生成的代码中是会让人看起来比较舒适的。

    • 变量名可读性会比较强。

    • 布局一般不会固定死宽高,而是使用padding等属性来自适应屏幕百分比

    • 自动处理设计稿中的无用图层、不可见元素、错误的编组乃至不合理的文字排列。

    • 全智能切图,自动分离背景图层、图标元素。




✍🏻 一些思考与建议



  • 前端开发不仅仅是一个切图的工具人,如果你一直局限于视图的表现的时候,你的前端水平也就是curd工程师的水平了,我们前端更多的要深入一些性能优化前端插件封装等等有意思的事情🙋🏻。

  • 总之如果你想你的前端水平要更加精进的情况下,可以减少一些在页面上的投入时间,现在的工具越来越成熟,而这些切图工作完全可以交给现有的工具去帮助你完成

  • 在使用体验上来说,CodeFun确实可以解决大部分切图功能,减少大家进行切图的工作时间,大家可以去试一下~但是肯定会有一些小细节不符合自己的想法,表示理解吧,毕竟AI智能生成代码能做成CodeFun这种水平已经很厉害了👍🏻。

  • 在使用建议上来说,我建议大家可以把CodeFun当成一个助手,而不要完全依赖,过度依赖,去找到更合适自己使用CodeFun的使用方法可以大量减少开发时间从而去做👉🏻更有意义的事情。

  • 很多人会很排斥,觉得没自己写的好,但是时代已经变啦~我还是那句话,所有东西都是一个辅助,一个工具,它提供了这些优质的功能而使用的好不好是看使用者本身的,欢迎大家去使用一下CodeFun~支持国产!!




  • 记住我们是前端开发,不是切图仔!做前端,不搬砖!



作者:快跑啊小卢_
来源:juejin.cn/post/7145977342861508638

收起阅读 »

为了弄清楚几个现象,重新学习了 flex

web
flex 布局作为开发中的常规手段,使用起来简直不要太爽。其特点: 相对于常规布局(float, position),它具备更高的灵活性; 相对于 grid 布局,它具有更强的兼容性; 使用简单,几个属性就能解决常规布局需求(当然 grid 布局也可以哈) ...
继续阅读 »

flex 布局作为开发中的常规手段,使用起来简直不要太爽。其特点:



  • 相对于常规布局(float, position),它具备更高的灵活性;

  • 相对于 grid 布局,它具有更强的兼容性;

  • 使用简单,几个属性就能解决常规布局需求(当然 grid 布局也可以哈)


但是在开发使用 flex 布局的过程中,也会遇到一些自己难以解释的现象;通俗表述:为什么会是这样效果,跟自己想象的不一样啊?


那么针对自己提出的为什么,自己有去研究过?为什么是这样的效果?如何解决呢?


自己也存在同样的问题。所以最近有时间,重新学习了一遍 flex,发现自己对 flex 的某些属性了解少之又少,也就导致针对一些现象确实说不清楚。


下面我就针对自己遇到的几种疑惑现象进行学习,来对 flex 部分属性深入理解。



每天多问几个为什么,总有想象不到的意外收获 ---我的学习座右铭



回顾 flex 模型


flex 的基本知识和基本熟悉就不介绍了,只需要简单的回顾一下 flex 模型。


在使用 flex 布局的时候,脑海中就要呈现出清晰的 flex 模型,利于正确的使用 flex 属性,进行开发。


flex1.png

理解如下的几个概念:



  • 主轴(main axis)

  • 交叉轴(cross axis)

  • flex 容器(flex container)

  • flex 项(flex item)



main size 也可以简单理解下,后面内容 flex-basis 会涉及到。



还顺便理解一下 flex-item 的基本特点



  1. flex item 的布局将由 flex container 属性的设置来进行控制的。

  2. flex item 不在严格区分块级元素和行内级元素。

  3. flex item 默认情况下是包裹内容的,但是可以设置的高度和宽度。


现象一:flex-wrap 换行引起的间距


关键代码:


<!-- css -->
<style>
 .father {
   width: 400px;
   height: 400px;
   background-color: #ddd;
   display: flex;
   flex-wrap: wrap;
}
 .son {
   width: 120px;
   height: 120px;
}
</style>

<!-- html -->
<body>
 <div class="father">
   <div class="son" style="background-color: aqua">1</div>
   <div class="son" style="background-color: blueviolet">2</div>
   <div class="son" style="background-color: burlywood">3</div>
   <div class="son" style="background-color: chartreuse">4</div>
 </div>
</body>

具体现象:


flex2.png

疑惑:为什么使用 flex-wrap 换行后,不是依次排列,而是排列之间存在间距?



一般来说,父元素的高度不会固定的,而是由内容撑开的。但是我们也不能排除父元素的高度固定这种情况。



排查问题:针对多行,并且在交叉轴上,不能想到是 align-content 属性的影响。但是又由于代码中根本都没有设置该属性,那么问题肯定出现在该属性的默认值身上。


那么通过 MDN 查询:


flex3.png

align-content 的默认值为 normal,其解释是按照默认位置填充。这里默认位置填充到底代表什么呢,MDN 上没有明确说明。


但是在 MDN 上查看 align-items 时,却发现了有用的信息(align-items 是单行,align-content 是多行),normal 在不同布局中有不同的表现形式。


flex4.png

可以发现,针对弹性盒子,normal 与 stretch 的表现形式一样。


自己又去测试 align-content,果然发现 normal 和 stretch 的表现形式一样。那么看看 stretch 属性的解释:


flex6.png

那么只需简单的需改,去掉 height 属性,那么 height 属性默认值就为 auto。


<!-- css -->
<style>
 .son {
   width: 120px;
   /* 注释掉 height */
   /* height: 120px */
}
</style>

看效果:


flex5.png

可以发现,子元素被拉伸了,这是子元素在默认情况下应该占据的空间大小。



这里就需要理解 flex item 的特点之一:flex item 的布局将由 flex container 属性的设置来进行控制的



那么当子元素设置高度时,是子元素自己把自己的高度限制了,但是并没有改变 flex container 对 flex item 布局占据的空间大小,所以就会多出一点空间,也就是所谓的间距。


所以针对上面这个案例,换行存在间隔的现象也就理解了,因为第四个元素本身就排布在弹性盒子的正确位置,只是我们把子元素高度固定了,造成的现象是有存在间隔。



可以想一下,如果子元素的高度加起来大于父元素的高度,又是什么效果呢?可以自己尝试一下,看自己能够解释不?



现象二:flex item 拉伸?压缩?


在使用 flex 时,最常见的现象是这样的:


flex7.png

当子元素为 3 个时,不会被拉伸,为什么呢?


当子元素为 6 个事,会被压缩,又是为什么呢?


其实上面这两个疑问❓,只需了解两个属性:flex-growflex-shrink。因为这两个属性不常用,所以容易忽略,从而不去了解,那么就会造成疑惑。


flex-grow 属性指定了 flex 元素的拉伸规则。flex 元素当存在剩余空间时,根据 flex-grow 的系数去分配剩余空间。 flex-grow 的默认值为 0,元素不拉伸


flex-shrink 属性指定了 flex 元素的收缩规则。flex 元素仅在默认宽度之和大于容器的时候才会发生收缩,其收缩的大小是依据 flex-shrink 的值。flex-shrink 的默认值为 1,元素压缩



该两个属性都是针对 主轴方向的剩余空间



所以



  • 当子元素数量较少时,存在剩余空间,但是又由于 flex-grow 的值为 0,所以子元素宽度不会进行拉伸。

  • 当子元素数量较多时,空间不足,但是又由于 flex-shrink 的值为 1,那么子元素就会根据相应的计算,来进行压缩。


特殊场景: 当空间不足时,子元素一定会压缩?试试单词很长(字符串很长)的时候呢?


flex8.png

现象三:文本溢出,flex-basis?width?


在布局中,如果指定了宽度,当内容很长的时候,就会换行。但是会存在一种特殊情况,就是如果一个单词很长为内容时,则不会进行换行;跟汉字是一样的道理,不可能从把汉字分成两半。


那么在 flex 布局中,会存在两种情况:


flex9.png

可以发现:



  • 设置了固定的 width 属性,字符串超出宽度之后,就会截取。

  • 而设置了固定的 flex-basis 属性,字符串超出宽度之后,会自动扩充宽度。


其实在这里可能有人会有疑惑:为什么把 width 和 flex-basis 进行对比?或者说 flex-basis 这个属性到底是干什么?



其实我也是刚刚才熟悉到这个属性,哈哈哈,不知道吧!!!



因为 flex-basis 是使用在 flex item 上,而 flex-basis(主轴上的基础尺寸)属性在大多数情况下跟 width 属性是等价的,都是设置 flex-item 的宽度。


上面的案例就是属于特殊情况,针对单词超长不换行时,flex-basis 就会表现出不一样的形式,自动扩充宽度


简单学习一下 flex-basis 的基本语法吧。


flex-basis 属性值:



  • auto: 默认值,参照自身 width 或者 height 属性。

  • content: 自动尺寸,根据内容撑开。

  • <'width'>: 指定宽度。


当一个属性同时设置 flex-basis(属性值不为 auto) 和 width 时,flex-basis 具有更高的优先级


现象四:flex 平分


当相对父容器里面的子元素进行平分时,我们会毫不犹豫的写出:


.father {
 width: 400px;
 height: 400px;
 background-color: #ddd;
 display: flex;
}
.son {
 flex: 1; /* 平分 */
 height: 90px;
}

flex10.png

那么我们是否会想过为什么会平分空间? 其中 flex:1 起了什么作用?


我们也许都知道 flex 属性是一个简写,是 flex-growflex-shrinkflex-basis 的简写。所以,flex 的属性值应该是三个组合值。


但是呢,flex 又类似于 font 属性一样,是一个很多属性的简写,其中一些属性值是可以不用写的,采用其默认值。


所以 flex 的属性值就会分析三种情况:一个值,两个值,三个值。


MDN 对其做了总结:


flex11.png

看图,规则挺多的,如果要死记的话,还是挺麻烦的。


针对上面的规则,其实只需要理解 flex 的语法形式,还是能够完全掌握(有公式,谁想背呢)。


flex = none | auto | [ <'flex-grow'> <'flex-shrink'>? || <'flex-basis'> ]  

希望你能看懂这个语法,很多 api 都有类似的组合。



  • | 表示要么是 none, 要么是 auto, 要么是后面这一坨,三选一。

  • || 逻辑或

  • ? 可选


理解上面这种语法之后,总结起来就是如下:


一个值



  1. none(0 0 auto)auto(1 1 auto) 是需要单独记一下的,这个无法避免。

  2. 无单位,就是 flex-grow,因为存在单位,就是 flex-grow 属性值规定为一个 number 类型的

  3. 有单位,就是 flex-basis,因为类似 width 属性是需要单位的,不然没有效果。


两个值



  1. 无单位,就是 flex-grow 和 flow-shrink,理由如上

  2. 其中一个有单位,就是 flex-grow 和 flex-basis,因为 flex-shrink 是可选的(这种情况是没有任何实际意义的,flex-basis设置了根本无效)。


三个值


三个值不用多说,一一对应。


理解了上面的语法形式,再来看 flex: 1 的含义就轻而易举了。一个值,没有单位,就是 flex-grow,剩余空间平均分配


现象五:多行,两边对齐布局


无论是 app 开发,还是网页开发,遇到最多的场景就是这样的:


flex12.png

两边对齐,一行元素之间的间距相同;如果一行显示不下,就换行依次对齐排布。


那么不难想到的就是 flex 布局,会写下如此代码:


.father {
 display: flex;
 justify-content: space-between;
 flex-wrap: wrap;
}
.son {
 width: 90px;
 height: 90px;
}

那么你就会遇到如下情况:


flex13.png

其中的第二、三种情况布局是不可以接受,数据数量不齐的问题。但是数据是动态的,所以不能避免出现类似情况。


你们遇到过这种类似的布局吗?会存在这种情况吗?是怎么解决的呢?


第一种解决方案:硬算


不使用 flex 的 justify-content 属性,直接算出元素的 margin 间隔。


.father {
 width: 400px;
 background-color: #ddd;
 display: flex;
 flex-wrap: wrap;
}
.son {
 margin-right: calc(40px / 3); /* 40px 为 父元素的宽度 - 子元素的宽度总和,   然后平分剩余空间*/
 width: 90px;
 height: 90px;
 background-color: #5adacd;
 margin-bottom: 10px;
}
/* 针对一行最后一个,清空边距 */
.son:nth-child(4n) {
 margin-right: 0;
}

缺点:只要其中的一个宽度发生变化,又要重新计算。


第二种解决方案:添加空节点


为什么要添加空节点呢?因为在 flex 布局中,没有严格的区分块级元素和行内元素。那么就可以使用空节点,来占据空间,引导正确的布局。


<style>
 .father {
   width: 400px;
   background-color: #ddd;
   display: flex;
   justify-content: space-between;
   flex-wrap: wrap;
}
 .son {
   width: 90px;
   height: 90px;
   background-color: #5adacd;
   margin-bottom: 10px;
}
 
 /* height 设置为 0 */
 .father span {
   width: 90px; /*空节点也是 flex-item, width 是必须一致的,只是设置高度为0,不占据空间*/
}
</style>
</head>
<body>
 <div></div>
 <div class="father">
   <div class="son">1</div>
   <div class="son">2</div>
   <div class="son">3</div>
   <div class="son">4</div>
   <div class="son">5</div>
   <div class="son">6</div>
   <div class="son">7</div>
   <!-- 添加空节点,个数为 n-2 -->
   <i></i>
   <i></i>
 </div>
</body>

这样也能解决上面的问题。


添加空节点的个数:n(一行的个数) - 2(行头和行尾,就是类似第一种情况和第四种情况本身就是正常的,就不需要空间点占据)


缺点:添加了dom节点


上面两种方案都解决问题,但是都有着各自的缺点,具体采用哪种方式,就看自己的选择了。


那么你们还有其他的解决方案吗?


总结


其实本篇所解释的现象是自己对 flex 知识掌握不牢而造成的,从而记录此篇,提升熟悉度。也希望能够帮助对这些现象有困惑的码友。


作者:copyer_xyf
来源:juejin.cn/post/7273025171111444540
>如果存在错误解释,评论区留言。

收起阅读 »

人情世故职场社会生存实战篇(五)

人情人情世故职场社会生存实战篇(一)人情人情世故职场社会生存实战篇(二)人情人情世故职场社会生存实战篇(三)人情人情世故职场社会生存实战篇(四) 41、问:带我的大哥让我办事我的效率都是最有效的,但为什么大哥就是不提我?反而还得罪了很多人。 答:孙悟空刚出社...
继续阅读 »

人情人情世故职场社会生存实战篇(一)
人情人情世故职场社会生存实战篇(二)
人情人情世故职场社会生存实战篇(三)
人情人情世故职场社会生存实战篇(四)



41、问:带我的大哥让我办事我的效率都是最有效的,但为什么大哥就是不提我?反而还得罪了很多人。


答:孙悟空刚出社会时,帮阿唐办事儿,遇到问题都是一马当先斩妖除魔,得罪了不少人,还差点把工作丢了,后来猴子学聪明了,遇到问题就请示观音,如来,搞关系,交了很多朋友,最终修成正果。


42、问:我跟一个挺有实力的前辈合作,我整天累的跟啥一-样,他天天就见几个人,大头还让他挣了,凭啥啊?


答:挣钱的永远是前辈,因为前辈负责关系,拿80%的利润,小弟负责干活,得20%的利润,前辈有关系人脉,而手里有关系人脉的人寥寥无几,会干活的小弟比比皆是。干活的不挣钱,挣钱的不干活,世界皆如此。


43、问:我昨天出了一个大单,提了2万多,我开心的跟同事分享,结果今天另外一个大单被同事撬走了,为什么?


答:你要学会闷声发大财。高调的人总有一天会毁了自己,沉住气,千万不要炫耀你的财富与成就,看似有人吹捧,其实他们的心里充满了嫉妒和怨气,真正智慧的人早就把自己调成了静音模式。


44、问:我们团队的老大,是特别有野心的人,但是手段不干净,什么数据造假 抄袭这些全都做过。他的能力是很强 ,但人品也实在令我反感。敢问师傅:如果要选一个大佬跟着,人品和德行需要纳为考虑吗?


答:你先爬上去,随后再试试看,干干净净做事能不能生存下去。有的领域可以,有的领域就不行, 所以你的标准答案应该是,保护好自己,随后竭尽全力的学习他的思路和手段,随后在自己运用的过程中,去筛选和判断。


45、问:如果对方硬是不收礼物的话,那是不是就不送了,还是找个节日或者由头继续送?


答:之前村里的会计来找我,我给了他两盒竹叶青,他说什么也不要。我直接扔他车里了,他说:你啊,太实在了。拒绝我东西的人,几乎没发生过。


有的人,连一瓶水都送不出去,8个领导在那儿 你买一瓶水,8个领导都说不喝了 谢谢你了。你还觉得你牛逼,哎,无语了。你要是买一打提溜过去,都喝了 没一个拒绝的。有这个觉悟的很少很少,有这觉悟,在底层锻炼三年就上去了。而更多的人,都是在基层锻炼到老……


46、问:我是单位某个部门的负责人,上面的意思是在本部门门评两个优秀员工,我在发愁把名额给哪个员工,得罪人的事不想干啊,您有什么好主意嘛?


答:开会就行了,让你的心腹帮你做引子,让所有的人发言,把大家的意见综合起来,什么样的人该上,让他们讲,你不要讲。开会一次不行就开两次,开着开着,答案就讨论出来了,然后你最后拍板就行了。大家都心服口服。


47、问:朋友兄弟把我介绍他单位上班,我是发了工资请他们吃饭还是?


答:现在就请,等个锤子啊。你提前请了,有福利提前给你。你发了工资再请,他们就不帮你了。因为这事在帮你,就显得他们势利眼了。人活着都是要尊严的。他们当下帮你,当下就有回报率,帮你就会上瘾的。你是喜欢工资日结还是月结呀。


48、问:我在单位被投诉,虽然自己一再辩解是有人故意带节奏的,是有人利用其他人的弱势群体身份打小报告,但是领导就是认定我是被人投诉的,现在停了我原来的职务,我以前没给领导送过礼,现在去送不知道还有没有用。


答:去送就是了,投诉你的这个人,他百分百送礼了。同样你一直给领导送礼,谁投诉你了他会帮你压下去。人啊 总归是人。一句话你没有给领导送礼。你送礼了屁事儿没有。人人都是势利小人,人人都贪得无厌。这句话永远都对。


49、问:我在官场和商海周旋几年了,截止到现在唯一还没搞定的难题,就是一个领导权利有的,每次我请吃饭他也来,送礼物也收。就是帮忙的时候总是帮我的对手多一些。在物资采购这块我的对手胜我一筹。我该用什么办法来对付他。


答:哎,你二啊,肯定是你的竞争对手给的多啊,给他的返点多。你请我吃饭,你买单了。我看到小A请你吃饭,我帮小A买单了。你很生气 我说马勒戈壁,你生气个锤子啊。小A已经帮我买过1000次单了。啥都是等价交换。


50、问:你一直说:求人办事送礼要循序渐进,什么才是循序渐进,您能指点一下吗,谢谢了。


答:打个比方,我第一阶段,请他喝可乐。我说想请一天假。他说小事,准。第二个阶段,请他吃饭喝酒,我想要全勤奖。他说:没问题。第三阶段,送华子,我想找个闲差。他说,明天去仓库。第四个阶段,我送五粮液,什么要求都没提。他说:仓库要不你来管理吧,也挺闲的。第五个阶段,我送金子,也没提要求。他说:明天晚上有个局带你去,抓住机会能不能升上去看你自己……



作者:公z号_纵横潜规则
来源:juejin.cn/post/7269787962342490175

收起阅读 »

Git 合并冲突不知道选哪个?试试开启 diff3 吧

iOS
导读:Git 早在 2008 年就提供 diff3,用于冲突展示时额外提供该区域的原始内容(两个分支公共祖先节点在此区域的内容),帮助更好的合并冲突。在 2022 年 Q1 发布的 Git 2.35 ,提供了一个新的选项 zdiff3,进一步优化了diff3 ...
继续阅读 »

导读:Git 早在 2008 年就提供 diff3,用于冲突展示时额外提供该区域的原始内容(两个分支公共祖先节点在此区域的内容),帮助更好的合并冲突。在 2022 年 Q1 发布的 Git 2.35 ,提供了一个新的选项 zdiff3,进一步优化了diff3 的展现。



Git 合并冲突,常见的展示形式分为 Current Change (ours, 当前分支的变更)和 Incoming Change (theirs, 目标分支的变更),两者针对的是同一区域的变化。



观察上面这个冲突示例,我们并不清楚两个分支各自都发生了什么变化,有两种可能:

  1. 两个分支同时增加了一行代码 "pkg": xxx
  2. 原先的提交记录里就有 "pkg": xxx ,只是两个分支同时修改了版本号

实际上这个例子,是第二种情况,两个分支都对 pkg 的版本做了改变。




这样的场景还有很多,如果不知道上下文,在解决冲突的时候容易束手束脚。


现在,我们可以使用 git 提供的 diff3 选项来调整合并冲突的展示效果



红框区域(|||||||=======)表示的就是改动前的上下文,确切的说, 当前分支 目标合并分支 的最近公共祖先节点在该区域的内容。


如何开启


冲突展示有两个选项 diff3merge(默认选项),可以通过以下方法进行配置



在 v2.35 新增了 zdiff3 选项,下文会提到

  • 对单个文件开启
git checkout --conflict=diff3 <文件名>
# 示例
git checkout --conflict=diff3 package.json
# 使用默认配置
git checkout --conflict=merge package.json
  • 项目配置
git config merge.conflictstyle diff3
# 删除配置
git config --unset merge.conflictstyle
# 使用默认配置
git config merge.conflictstyle merge
  • 全局配置
git config --global merge.conflictstyle diff3
# 删除配置
git config --global --unset merge.conflictstyle

示例展示


在同一位置添加代码行

<<<<<<< HEAD
import 'some_pkg';
||||||| merged common ancestor
=======
c
>>>>>>> merged-branch

如上示例,合并的公共祖先节点在该位置是空白,每个分支都在相同的位置添加代码行。


我们通常希望保留两者,并按照最有意义的顺序排序,也可能选择只保留其中一个。以下是一个冲突修复后的示例:

import 'some_pkg';
import 'some_pkg';

一方修改一方删除

<<<<<<< HEAD
||||||| merged common ancestor
console.log('调试信息')
=======
console.log('调试信息2')
>>>>>>> merged-branch

如上示例,一方把调试信息删除,而另一方修改了调试信息内容。对于这个示例,我们通常是选择删除而不保留修改。


为什么不是默认选项


经常需要知道祖先节点的内容来确保正确的合并,而 diff3 解决了这个痛点。同时,diff3 没有任何弊端(除了冲突区域行数变多🌝),没有理由不启用它。


那为什么 Git 不将 diff3 作为默认的合并冲突展示选项呢?


stackoverflow 上有人回答了这个问题,大概意思是说可能和 Unix diff 有关,早前默认的 Unix diff 不支持展示 3-way diff (待考证)。


之后的新版本也不方便调整默认值,否则会对用户造成困扰 — “合并冲突区域怎么多了一块内容?”。


zdiff3 (zealous diff3)


2022 年 Q1 ,Git 发布 v2.35,其中有个变化是冲突展示新增了 zdiff3 的配置选项。


zdiff3 基于 diff3 ,并对冲突块两侧的公共代码行做了压缩。


举个例子:




使用默认配置,合并冲突展示如下:

1
2
3
4
A
<<<<<<< ours
B
C
D
=======
X
C
Z
>>>>>>> theirs
E
7
8
9

使用 diff3 后,合并冲突展示如下:

1
2
3
4
<<<<<<
A
B
C
D
E
||||||
5
6
======
A
X
C
Z
E
>>>>>>
7
8
9

通过观察可以发现,冲突区域两侧有公共的代码行 A、E 。而这些代码行在默认配置下会被提取到外部。


而用了 zdiff3 之后,A、E 两行又将移到冲突之外。

1
2
3
4
A
<<<<<<
B
C
D
||||||
5
6
======
X
C
Z
>>>>>>
E
7
8
9

一句话总结 zdiff3 的优化:即展示公共祖先节点内容,又能够充分压缩冲突的公共部分。


最后


解决 Git 合并冲突是一个难题,diff3 并不是一个“银弹”,它只能帮助提供更多的信息,减少决策成本。


推荐读者尝试下 zdiff3 ,至少使用 diff3 ,并将其作为默认配置。


最后,如果看完本文有收获,欢迎一键三连(点赞、收藏、分享)🍻 ~


拓展阅读


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

第一份只干了五天的前端工作

可能是前面运气太好,工作五年了反而遇到奇怪的操作,首先是一些事情没有提前给我讲,入职当天才知道。当月工资要在下个月月末发,还会有绩效工资的变动,有减少的可能。我当时问了五险一金说是按照7000多交,结果公积金按最低2千多我去的第三天,后端管理A,负责给我分配任...
继续阅读 »

可能是前面运气太好,工作五年了反而遇到奇怪的操作,首先是一些事情没有提前给我讲,入职当天才知道。

  • 当月工资要在下个月月末发,还会有绩效工资的变动,有减少的可能。
  • 我当时问了五险一金说是按照7000多交,结果公积金按最低2千多
  • 我去的第三天,后端管理A,负责给我分配任务的,他早上和我讲,类似初创公司领导不愿意看到早下班,晚上也会找我过一下进度啥的,要定制一个学习计划,自己找时间去熟悉代码...

面试过程倒还顺利,前端A问了些技术,比较偏实际应用的,对我github上做的东西也感兴趣。前端管理B问的有些偏人事的,然后问我下午有没有其它安排,没有的话不走流程叫领导A面一下。领导A就直接说了平薪,试用期3个月80%能不能接受,当时那种场景我就顺势而为说可以,我之前的工作都是少一千。


我往回走的时候hr给我打电话,前面的没接到,我打过去得知是想问我在哪,想问一下总裁有没有时间,不然就要到周六。


等到周六面完,下周一打电话说通过,让我周三去报到,我说太快要下周一。然后这家公司每个月月末的周六是组织学习培训的,我也参加了。


第一天搭建环境,账号权限这些,电脑连不上WiFi,我问前台有没有网线,她说没有我也没去下面找,又换了一台,接口就不同了,又弄双屏的转接线。我找她一次,她都是从前台去存放物品的地方,发现不太对再找她已经回前台了,前后不到一分钟的时间吧。确实我对这个也不太熟,有些不好意思麻烦她,最后少一个转接器我就自己买了,否则屏幕显示不清晰,顺便买了两个增高架。


前端管理B给我说了几个项目,我看了会代码,管理B找我谈了一会。


九点上班六点下班,上班第一天我看快到六点半了就准备走,这时候管理B拉着我去展厅看了下公司的产品。


第二天后端管理A请假了,前端管理B给我说了会代码,后面主要负责的项目,跑了下本地联调。


基本上一个菜单一个项目,每个项目用iframe嵌套,然后有一些组件库这些,代码之间组件嵌套的比较深,多以component。数据走的是配置,流向很乱。接口的传递和返回都很庞大,有些还是json字符串,20-60多个kb,看结构话要单独复制出来。一些项目调试没有sourcemap,给我的感觉就是把简单的事做复杂了。


第三天后端管理A给我安排了一些事情,就是口头说了一下,意思这种改动不需要ui、不需要产品自己就可以定,基本上就是我做完让他看一下,他觉得不好在改。这里面就有一个问题,就是到底改哪里没有全部列出来,我对项目又不熟,一般都有一个上下文的概念,可对不上的时候,才发现是另一个地方,他又是没有规划的那种。


往往走配置基本会出现一种情况,就是一些东西需要单独处理,或者配置选项越加越多,或者当初实现的时候偷懒就给写死了,东改一下西改一下,而且这种封装太笨重了,不好优化,只能说熟悉了更快些,但是维护成本始终会处在一个固定的量级,而且随着功能迭代,补丁会越来越多。


下午的时候管理B把我叫过去,让我协助后台A排查两个问题,说别的环境没有只有这个环境有,据说问题已经存在很久了,我找到问题A的代码所在地、以及问题的原因就花了很长时间,单从前端看,是因为一个代码报错导致的。因为这些东西要按照业务流程来,我不知道什么是对的,只能关注接口的返回然后找对应的前端组件渲染逻辑,对比差异反馈,本质上就是反推接口返回有哪些不对,找到问题已经晚上10点了,两个问题都是后台在处理用户操作后,前后id不一样导致拿到的数据不对所致。


这种问题让一个刚入职的人排查显然浪费时间,中间链路太多了,我问了下后端A他之前都是和谁对接,得知前面的人离职了,我就想着这种情况是最难受的,总不可能我一上来就能接手他的工作,巧妇难为无米之炊,哪有那么丝滑的过度,总要有个渐进性的过程吧。


每天要建tapd,再把tapd的内容复制出来写成日报,然后也要写周报,还要在领导有时间的时候找他汇报进度,或者等他来找你。


第四天,后端管理A给我说了下今天的任务,有一些历史遗留问题,我处理的还是很快的,直到前端写完对接口的时候,他只是钉钉发了一些字段给我,发现还有另一个项目要改,找到代码熟悉,对好逻辑写好前端代码,我又在本地连了下测试环境,跑了下流程,接口报错了,我看六点半了就走了。


第四天上午还过了个需求,虽然我也听不太懂,但是管理B直接说这个事情15个自然日还是工作日搞完。


后端管理A评价我的日报,意思任务完不成要及时上报,晚上要和他汇报进度,我想着我都不知道一天到底有多少任务,也不知道完成任务花费多长时间,更不知道啥样算完成,我咋完成?


第五天,前端管理B找我聊了一会,说是来了一周,我就把我的感触说了,他问我打算怎么处理,我就说这种强度我就不干了,感觉不值,他说他来处理。


我对比了下我能够得到的和我将要面对的,平薪80%,三个月,我觉得有些不值得,待遇还不如我两年前,也没到山穷水尽的地步,受这罪干嘛?以前入职也不是没有压力大的时候,但待遇有所增长,看了代码啥的我也觉得对我是一种历练,即便不说我也会主动学习,因为我知道当我很熟悉的时候后面效率更高,算提早付出了,还是在时间不那么紧凑的时候,但这家公司给我一种压榨的感觉。


后面我把项目分支发给后端管理A,部署发版耽搁了一会,后面是找的前端管理B解决的,我后面了解到走的是自己的搭建的运维系统,两个项目有不同的分支名,我把自己的分支手动合并,再找后端管理A就好了。


接下来就是他发现一些问题让我改,持续到下午五点半左右,我再次提交代码时,发现gtilab账号已经被注销了,他让我把代码改动发给前端管理B,这个时候我的电脑已经重置,被前台收走了。还是有些遗憾,再次改动时发现轻车熟路了许多,前面还是花了不少精力的。


公司提供了午休床,我贴了个标签,前端管理B贴心的给我个东西增加区分性,但我用过一次后没再找见,离职也是要交给前台的,找的时候还在想不会所有放午休床的地方都要找一遍吧,还好发现了破碎的标签,应该被别人用的时候弄碎了。


其实周五不聊的话我可能想着再适应下,也没想到当天就能走完离职,清理tapd的时候发现有五十多个bug挂在我这,看到一个六月份的。


幸亏我带了包过去,不然东西都不好拿,看着8月份4天32小时...。


可能是太久没工作了吧,我便抱着试一试的态度,坦白的讲我也想过边干边找,入职的这几天有了新的方向,我github上写的工具依旧发挥稳定,替我节省了很多时间。


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

iOS小技能:去掉/新增导航栏黑边(iOS13适配)

iOS
引言 背景: 去掉导航栏下边的黑边在iOS15失效 原因:必须使用iOS13之后的APIUINavigationBarAppearance设置才能生效UIKIT_EXTERN API_AVAILABLE(ios(13.0), tvos(13.0)) NS_SW...
继续阅读 »

引言


背景: 去掉导航栏下边的黑边在iOS15失效
原因:必须使用iOS13之后的APIUINavigationBarAppearance设置才能生效

UIKIT_EXTERN API_AVAILABLE(ios(13.0), tvos(13.0)) NS_SWIFT_UI_ACTOR
@interface UINavigationBarAppearance : UIBarAppearance

I 导航栏的黑边设置


1.1 去掉导航栏下边的黑边(iOS15适配)



iOS15之前: [self.navigationBar setShadowImage:[[UIImage alloc] init]];

        [vc.navigationController.navigationBar setBackgroundImage:[ImageTools createImageWithColor: [UIColor whiteColor]] forBarMetrics:UIBarMetricsDefault];


iOS15之后


if(@available(iOS 13.0, *)) {
UINavigationBarAppearance *appearance = [[UINavigationBarAppearance alloc] init];

//去掉透明后导航栏下边的黑边
appearance.shadowImage =[[UIImage alloc] init];

appearance.shadowColor= UIColor.clearColor;



navigationBar.standardAppearance = appearance;

navigationBar.scrollEdgeAppearance = appearance;

}

1.2 设置导航栏下边的黑边(iOS13适配)




// 设置导航栏下边的黑边
+ (void)setupnavigationBar:(UIViewController*)vc{



if (@available(iOS 13.0, *)) {

UINavigationBar *navigationBar = vc.navigationController.navigationBar;

UINavigationBarAppearance *appearance =navigationBar.standardAppearance;


appearance.shadowImage =[UIImage createImageWithColor:k_tableView_Line];

appearance.shadowColor=k_tableView_Line;


navigationBar.standardAppearance = appearance;
navigationBar.scrollEdgeAppearance = appearance;

} else {
// Fallback on earlier versions

UINavigationBar *navigationBar = vc.navigationController.navigationBar;
[navigationBar setBackgroundImage:[[UIImage alloc] init] forBarPosition:UIBarPositionAny barMetrics:UIBarMetricsDefault]; //此处使底部线条颜色为红色
// [navigationBar setShadowImage:[UIImage createImageWithColor:[UIColor redColor]]];

[navigationBar setShadowImage:[UIImage createImageWithColor:k_tableView_Line]];

}



}




II 去掉TabBar的顶部黑线


  • setupshadowColor


- (void)setupshadowColor{

UIView * tmpView = self;
tmpView.layer.shadowColor = [UIColor blackColor].CGColor;//设置阴影的颜色
tmpView.layer.shadowOpacity = 0.08;//设置阴影的透明度
tmpView.layer.shadowOffset = CGSizeMake(kAdjustRatio(0), kAdjustRatio(-5));//设置阴影的偏移量,阴影的大小,x往右和y往下是正
tmpView.layer.shadowRadius = kAdjustRatio(5);//设置阴影的圆角,//阴影的扩散范围,相当于blur radius,也是shadow的渐变距离,从外围开始,往里渐变shadowRadius距离


//去掉TabBar的顶部黑线
[self setBackgroundImage:[UIImage createImageWithColor:[UIColor clearColor]]];
[self setShadowImage:[UIImage createImageWithColor:[UIColor clearColor]]];

}


see also


iOS小技能:自定义导航栏,设置全局导航条外观。(iOS15适配)
blog.csdn.net/z929118967/…


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

苹果回应 iPhone14 电池老化快:属于正常现象,iPhone 15 系列顶配机型有望首次搭载潜望式镜头

iOS
国内要闻 曝小米自研系统为全端系统 日前,有数码博主爆料,小米自研操作系统属于全端自研系统,兼容AOSP(Android 开放源代码项目)。如此看来,小米自研操作系统还可能有车机、平板、手表等终端系统,而且小米走的是华为鸿蒙操作系统的路子,前期先兼容安卓更为稳...
继续阅读 »



国内要闻


曝小米自研系统为全端系统

日前,有数码博主爆料,小米自研操作系统属于全端自研系统,兼容AOSP(Android 开放源代码项目)。如此看来,小米自研操作系统还可能有车机、平板、手表等终端系统,而且小米走的是华为鸿蒙操作系统的路子,前期先兼容安卓更为稳妥,保住既有的用户量。(手机中国)


华为辟谣网传3.2万名科学家正式移籍:造谣者毫无根据、无中生有

近日,网络上多家平台发布了针对华为公司的系列言论,经证实,该系列言论均为谣言,华为表示,造谣者毫无根据、无中生有。此外,华为呼吁各位网友“勿信勿传,果断举报”。(第一财经)


清华大学联合字节跳动,开源听觉大语言模型 SALMONN

清华大学联合字节火山语音团队提出了一种全新的「听觉」大语言模型——SALMONN (Speech Audio Language Music Open Neural Network)。相较于仅仅支持语音输入或非语音音频输入的其他大模型,SALMONN 对语音、音频事件、音乐等各类音频输入都具有感知和理解能力,相当于给大语言模型「加了个耳朵」,从而涌现出多语言和跨模态推理等高级能力。


钉钉公布 AI 版本商业定价:调用一次大模型不到 5 分钱

8 月 22 日,钉钉召开 2023 生态大会,据总裁叶军介绍:截至今年 3 月末,钉钉软件付费企业数达 10 万家,其中小微企业占 58%,中型企业占 30%,大型企业占 12%;钉钉付费 DAU 超过 2300 万。这也是钉钉首次公布其商业化核心进展。此外,钉钉还公布了大模型落地应用场景的商业化方案:在钉钉专业版年费 9800 元基础上,增加 10000 元即可获得 20 万次大模型调用额度;在专属钉钉年费基础上,增加 20000 元即可获得 45 万次大模型调用额度,约等于平均每次调用只需 0.44~5 分钱。(IT 之家)


吉利回应与百度合作造车:开发一直为吉利主导,百度提供技术支持

不久前,吉利与百度合作造车计划突遭生变,“集度”变身“极越”。近日,在吉利汽车半年业绩发布会上,吉利控股集团CEO李东辉回应腾讯新闻《远光灯》,极越定位为吉利控股旗下高端智能汽车机器人品牌。在极越的开发过程中,一直都是吉利控股来主导的,百度提供了大数据、无人驾驶等领域的技术支持。


非法注册 300 万个微信号!央视曝光特大黑灰产系列案

8月7日、8日、9日中午,中央电视台《今日说法》栏目以《揭秘“黑灰产”》为题,分上、中、下三集对山东淄博周村公安分局破获的特大黑灰产案件侦破过程进行专题报道。犯罪分子批量注册并贩卖微信号,形成产业链。这些微信号多用于电信诈骗等违法犯罪活动。该犯罪团伙共非法注册微信号 300 余万个,非法获利达 1000 余万元。警方通过追查嫌疑人注册微信的手机号码来源,打掉一个号商团伙,揪出二十余名省级运营商“内鬼”。他们利用手中的权力,牟取巨额私利,为犯罪团伙提供非法注册的手机号码。(今日说法)


国际要闻


苹果回应 iPhone14 电池老化快:属于正常现象

苹果公司的 iPhone 14 系列手机上市不到一年,就出现了电池健康度下降过快的问题。一些用户反映,他们的手机电池在使用几个月后,就损耗了 10% 以上的容量。苹果公司表示,这种情况属于正常现象,只有当电池容量低于 80% 时,才能在保修期内享受免费更换服务。


据了解,如果使用非正品电池或者其他 iPhone 14 手机上拆下来的电池进行更换,那么手机将无法识别新电池,并且会禁用电池健康度功能,这意味着用户无法查看电池的剩余容量和性能状况。(IT之家)


Meta 推出 AI 模型 SeamlessM4T,可翻译和转录近百种语言

Meta 近日发布了人工智能模型 SeamlessM4T,可以翻译和转录近 100 种语言的文本和语音。SeamlessM4T 支持对近百种语言进行语音以及文本识别,同时支持近 100 种输入语言和 36 种输出语言的语音到语音翻译。Meta 表示,将会以研究许可证的形式公开发布 SeamlessM4T,以便研究人员和开发人员在此基础上开展工作。Meta 还将发布 SeamlessAlign 的元数据,这是迄今为止最大的开放式多模态翻译数据集,共挖掘了 27 万小时的语音和文本对齐。(品玩)


iPhone 15 系列顶配机型有望首次搭载潜望式镜头

来自摩根士丹利的一份分析师报告指出,iPhone 15 Pro Max(或改名为 iPhone 15 Ultra)将获得苹果有史以来第一款潜望式镜头,其变焦能力从前代的 3 倍将提升到 5-6 倍,表现令人相当期待。这份报告还提到,由于全新传感器的加入,使 iPhone 15 系列顶配机型的备货能力受到影响,或许会在 iPhone 15 系列开售后 3-4 周时间才会陆续发货。毫无疑问,这将会是 iPhone 15 系列中最值得关注的一款机型。(雷科技)


微软宣布将把动视暴雪云游戏权益出售给育碧,以安抚英国监管机构

8 月 22 日消息,据外媒报道,当地时间周一,微软宣布将把动视暴雪的云游戏权益出售给育碧,以重组其拟议的动视暴雪收购交易。报道称,微软此举旨在安抚英国监管机构英国竞争和市场管理局(CMA),因为该机构担心这笔交易会扼杀快速增长的云游戏市场的竞争。当地时间周一,微软与育碧签署了一项为期 15 年的协议,将《使命召唤》等动视暴雪的云游戏相关权益授权给育碧,这是微软为其收购动视暴雪获得反垄断批准的最新举措。(TechWeb)


Meta 推出拥有 12 种复杂技能机器人,上得厅堂下得厨房

耗时 2 年,Meta 联手卡耐基梅隆大学推出通用机器人智能体——RoboAgent,可以通过图像或者语言指令,来指挥机器人完成任务。它拥有 12 种不同的复杂技能,泡茶、烘焙不在话下,未来还能泛化 100 多种未知任务。(网易科技)


IBM 推企业级 AI 平台!剑指企业级 AI 应用三大挑战

日前,IBM 面向中国区正式推出企业级 AI 平台 watsonx,包含企业级 AI 与数据平台 watsonx.ai、湖仓一体的数据存储方案 watsonx.data 以及 AI 治理工具包 watsonx.governance。


程序员专区


微软 Excel 宣布集成 Python

微软已经将 Python 原生集成到 Excel 公测版中,首先向 Microsoft 365 Insiders 推出,从而使用户能够借助 Python 库、数据可视化和分析的能力更好地使用 Excel。目前该功能只能在桌面版 Excel 中使用,但微软表示 Python 计算也可以在微软云中运行。


Google 更新 Android 运行时应用提速最高三成

Android 运行时 (Android Runtime 或 ART)的最新更新将帮助应用在部分设备上的启动时间缩短最多 30%。ART 是 Android 操作系统的引擎,提供了所有 Android 应用和绝大多数服务所依赖的运行时和核心 API。改进 ART 将能让所有开发者受益,让应用执行更快,字节码编译更高效。Google 表示它正致力于让 ART 模块化独立于操作系统更新。ART 的可独立更新将能让用户更快获得性能优化和安全更新,让开发者更快获得 OpenJDK 改进和编译器优化。它的测试显示,ART 13 的运行时和编译器优化在部分设备上实现了最高 30% 的应用启动改进。


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

iOS16 中的 3 种新字体宽度样式

iOS
前言 在 iOS 16 中,Apple 引入了三种新的宽度样式字体到 SF 字体库。CompressedCondensedExpend UIFont.Width Apple 引入了新的结构体 UIFont.Width,这代表了一种新的宽度样式。 目前已有的四...
继续阅读 »

前言


在 iOS 16 中,Apple 引入了三种新的宽度样式字体到 SF 字体库。

  1. Compressed

  2. Condensed

  3. Expend



UIFont.Width


Apple 引入了新的结构体 UIFont.Width,这代表了一种新的宽度样式。


目前已有的四种样式。

  • standard:我们总是使用的默认宽度。

  • compressed:最窄的宽度样式。

  • condensed:介于压缩和标准之间的宽度样式。

  • expanded:最宽的宽度样式。



SF 字体和新的宽度样式


如何将 SF 字体和新的宽度样式一起使用


为了使用新的宽度样式,Apple 有一个新的 UIFont 的类方法来接收新的 UIFont.Width

class UIFont : NSObject {
class func systemFont(
ofSize fontSize: CGFloat,
weight: UIFont.Weight,
width: UIFont.Width
) -> UIFont
}

你可以像平常创建字体那样来使用新的方法。

let condensed = UIFont.systemFont(ofSize: 46, weight: .bold, width: .condensed)
let compressed = UIFont.systemFont(ofSize: 46, weight: .bold, width: .compressed)
let standard = UIFont.systemFont(ofSize: 46, weight: .bold, width: .standard)
let expanded = UIFont.systemFont(ofSize: 46, weight: .bold, width: .expanded)

SwiftUI



更新:在 Xcode 14.1 中,SwiftUI 提供了两个新的 API 设置这种新的宽度样式。
width(_:)fontWidth(_:)



目前(Xcode 16 beta 6),这种新的宽度样式和初始值设定只能在 UIKit 中使用,幸运的是,我们可以在 SwiftUI 中轻松的使用它。


有很多种方法可以将 UIKit 集成到 SwiftUI 。我将会展示在 SwiftUI 中使用新宽度样式的两种方法。

  1. 将 UIfont 转为 Font。
  2. 创建 Font 扩展。

将 UIfont 转为 Font


我们从 在 SwiftUI 中如何将 UIFont 转换为 Font 中了解到,Font 有初始化方法可以接收 UIFont 作为参数。


步骤如下

  1. 你需要创建一个带有新宽度样式的 UIFont。
  2. 使用该 UIFont 创建一个 Font 。
  3. 然后像普通 Font 一样使用它们。
struct NewFontExample: View {
// 1
let condensed = UIFont.systemFont(ofSize: 46, weight: .bold, width: .condensed)
let compressed = UIFont.systemFont(ofSize: 46, weight: .bold, width: .compressed)
let standard = UIFont.systemFont(ofSize: 46, weight: .bold, width: .standard)
let expanded = UIFont.systemFont(ofSize: 46, weight: .bold, width: .expanded)

var body: some View {
VStack {
// 2
Text("Compressed")
.font(Font(compressed))
Text("Condensed")
.font(Font(condensed))
Text("Standard")
.font(Font(standard))
Text("Expanded")
.font(Font(expanded))
}
}
}

  • 创建带有新宽度样式的 UIFont。
  • 用 UIFont 初始化 Font, 然后传递给 .font 修改。

创建一个 Font 扩展


这种方法实际上和将 UIfont 转为 Font 是同一种方法。我们只需要创建一个新的 Font 扩展在 SwiftUI 中使用起来更容易一些。

extension Font {
public static func system(
size: CGFloat,
weight: UIFont.Weight,
width: UIFont.Width) -> Font
{
// 1
return Font(
UIFont.systemFont(
ofSize: size,
weight: weight,
width: width)
)
}
}

创建一个静态函数传递 UIFont 需要的参数。然后,初始化 UIFont 和创建 Font


我们就可以像这样使用了。

Text("Compressed")
.font(.system(size: 46, weight: .bold, width: .compressed))
Text("Condensed")
.font(.system(size: 46, weight: .bold, width: .condensed))
Text("Standard")
.font(.system(size: 46, weight: .bold, width: .standard))
Text("Expanded")
.font(.system(size: 46, weight: .bold, width: .expanded))

如何使用新的宽度样式


你可以在你想使用的任何地方使用。不会有任何限制,所有的新宽度都有一样的尺寸,同样的高度,只会有宽度的变化。


这里是拥有同样文本,同样字体大小和同样字体样式的不同字体宽度样式展示。



新的宽度样式优点


你可以使用新的宽度样式在已经存在的字体样式上,比如 thin 或者 bold ,在你的 app 上创造出独一无二的体验。


Apple 将它使用在他们的照片app ,在 "回忆'' 功能中,通过组合不同的字体宽度和样式在标题或者子标题上。



这里有一些不同宽度和样式的字体组合,希望可以激发你的灵感。

Text("Pet Friends")
.font(Font(UIFont.systemFont(ofSize: 46, weight: .light, width: .expanded)))
Text("OVER THE YEARS")
.font(Font(UIFont.systemFont(ofSize: 30, weight: .thin, width: .compressed)))

Text("Pet Friends")
.font(Font(UIFont.systemFont(ofSize: 46, weight: .black, width: .condensed)))
Text("OVER THE YEARS")
.font(Font(UIFont.systemFont(ofSize: 20, weight: .light, width: .expanded)))


你也可以用新的宽度样式来控制文本的可读性。


下面的这个例子,说明不同宽度样式如何影响每行的字符数和段落长度



下载这种字体


你可以在 Apple 字体平台 来下载这种新的字体宽度样式。


下载安装后,你将会发现一种结合了现有宽度和新宽度样式的新样式。




基本上,除了在模拟器的模拟系统 UI 中,在任何地方都被禁止使用 SF 字体。请确保你在使用前阅读并理解许可证。


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

Go 负责人说以后不会有 Go2 了

大家好,我是煎鱼。 最近 Go 核心团队负责人 @Russ Cox(下称:rsc)专门写了一篇文章《Backward Compatibility, Go 1.21, and Go 2》为 Go 这门编程语言的 Go1 兼容性增强和 Go2 的情况说明做诠释和宣...
继续阅读 »

大家好,我是煎鱼。


最近 Go 核心团队负责人 @Russ Cox(下称:rsc)专门写了一篇文章《Backward Compatibility, Go 1.21, and Go 2》为 Go 这门编程语言的 Go1 兼容性增强和 Go2 的情况说明做诠释和宣传。


今天希望能够帮助你获悉 Go 未来的规划、方向以及 rsc 的思考。


Go1 破坏兼容性的往事


新增结构体字段


第一个案例,比较经典。在 Go1 的时候,这段代码是可以正常运行的。如下演示代码:

// 脑子进煎鱼了
package main

import "net"

var myAddr = &net.TCPAddr{
net.IPv4(18, 26, 4, 9),
80,
}

但在 Go1.1,这段代码就跑不起来。必须要改成如下代码:

var myAddr = &net.TCPAddr{
IP: net.IPv4(18, 26, 4, 9),
Port: 80,
}

因为在当时的新版本中,对 net.TCPAddr 新增了 Zone 字段。原先的未声明值对应字段的方式就会出现一些问题。


后续在新版本的规范中,官方直接对标准库提交的代码增加了要求,赋值时必须声明字段名。以此避免该问题的产生。


改进排序/压缩的算法实现


第二个案例,Go1.6 时,官方修改了 Sort 的排序实现,使得运行速度提高了 10% 左右。以下是演示代码,将根据名称长度对颜色列表进行排序并输出结果:

colors := strings.Fields(
`black white red orange yellow green blue indigo violet`)
sort.Sort(ByLen(colors))
fmt.Println(colors)

一切听起来是那么的美好。


真实世界是改变排序算法通常会改变相等元素的排序方式。导致了 Go1.5 和 Go1.6 所输出的结果不一致:

Go 1.5:  [red blue green white black yellow orange indigo violet]
Go 1.6: [red blue white green black orange yellow indigo violet]

按照顺序排序后,结果集的差异点在于:


  • Go1.5 返回 green, white, black。
  • Go1.6 返回 white, green, black。

如果说程序依赖了结果集的输出顺序,这将是一个影响不小的兼容性破坏。


第三个案例,类似的还有在 Go1.8 中,官方改进了 compress/flate 的算法,达到了在 CPU 和 Memory 没有什么明显变化下,压缩后的结果集更小了。听起来是个很好的成果。


但实际上自己内部却翻车了,因为 Google 内部有一个需要可重现归档构建的项目,依赖了原有的算法。最后自己 fork 了一份来解决。


Go1.21 起增强兼容性(GODEBUG)


从上面的部分破坏兼容性示例来看,可以知道 Go 官方也不是刻意破坏的。但又存在必然要修改的各种原因和考量。


为此在 Go1.21 起,正式输出了 GODEBUG 的机制,相当于是开了个官方 “后门” 了。将其作为破坏性变更后的门把手。


允许设置 GODEBUG,来开关新功能特性。例如以下选项:

  • GODEBUG=asyncpreemptoff=1:禁用基于信号的 Goroutine 抢占,这偶尔会发现操作系统的错误。
  • GODEBUG=cgocheck=0:禁用运行时的 CGO 指针检查。
  • GODEBUG=cpu.<extension>=off:在运行时禁止使用某个特定的 CPU 扩展。

也会根据根据 go.mod 中的 Go 版本号来设置对应 GODEBUG,以提供版本所约定的 Go1 兼容性保障策略。


如果对这块感兴趣,可以查看《加大力度!Go 将会增强 Go1 向后兼容性》,有完整的增强兼容性的规范说明。


Go2 的情况和规划


Go 官方(via @rsc)正式回答了之前画的饼,也就是什么时候可以看到 Go2 的规范推出,打破 Go1 程序?


答案是永远不会。从与过去决裂、不再编译旧程序的意义上来说,Go 2 永远不会出现。从 Go 在 2017 年开始对 Go 1 进行重大修订的意义上来说,Go 2 已经发生了。


简而言之,透露出来的意思是:硬要说的话,Go2 已经套壳 Go1 上市了。


在未来规划上,不会出现破坏 Go1 程序的 Go2。工作方向会往将加倍努力保证兼容性的基础上,开展新的新工作。


总结


整体上 rsc 对破坏 Go1 兼容性做了很长时间规划的回溯和规划,释出了一大堆手段,例如:GODEBUG、go.mod 版本约束等。


从而引导了 Go2 直接可以借壳上的方向,也更好兑现了 Go1 兼容性保障的规范承诺。单从这方面来讲,还是非常的深思熟虑的。


也可能会有同学说,看 Go 现在这样,说不定下次就变了。这可能比较难,其实 rsc 才上任做团队负责人没几年,工作履历上和其他几位骨干大佬在 Google 已经有非常长年的在职经验了。



我目测一时半会是不会变的了。


想变,得等 Go 核心团队这一班子换了才有可能了。阻力也会很多,因为社区人多,一般会比较注重规范。



文章持续更新,可以微信搜【脑子进煎鱼了】阅读,本文 GitHub github.com/eddycjy/blo… 已收录,学习 Go 语言可以看 Go 学习地图和路线,欢迎 Star 催更。



Go 图书系列


推荐阅读


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

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

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

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


01


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


在昨天上班的路上,我在微信的订阅号推荐里面看到了下图



当时我的想法是:这东西阅读量好高噢,不过养老金和目前的我没什么关系。于是,我就划过去了。



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



02


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


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


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


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


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



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



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



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



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


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


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



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


“分享一个点子后


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


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


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


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


3、做出差异化内容。”


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


03


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



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


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



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


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



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



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



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



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



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


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


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


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


04


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


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


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


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


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


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


全文完结,如果对你有收获的话,关注公众号 刘卡卡 和我一起交流进步


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

不用太深奥简单解决iOS上拉边界下拉白色空白问题

iOS
表现 手指按住屏幕下拉,屏幕顶部会多出一块白色区域。手指按住屏幕上拉,底部多出一块白色区域。 产生原因 在 iOS 中,手指按住屏幕上下拖动,会触发 touchmove 事件。这个事件触发的对象是整个 webview ...
继续阅读 »

表现


手指按住屏幕下拉,屏幕顶部会多出一块白色区域。手指按住屏幕上拉,底部多出一块白色区域。




产生原因


在 iOS 中,手指按住屏幕上下拖动,会触发 touchmove 事件。这个事件触发的对象是整个 webview 容器,容器自然会被拖动,剩下的部分会成空白。




解决方案


1. 监听事件禁止滑动


移动端触摸事件有三个,分别定义为

  1. touchstart :手指放在一个DOM元素上。

  2. touchmove :手指拖曳一个DOM元素。

  3. touchend :手指从一个DOM元素上移开。


显然我们需要控制的是 touchmove 事件,由此我在 W3C 文档中找到了这样一段话


Note that the rate at which the user agent sends touchmove events is implementation-defined, and may depend on hardware capabilities and other implementation details.(注意,用户代理发送touchmove事件的速率是实现定义的,并且可能取决于硬件功能和其他实现细节。)


If the preventDefault method is called on the first touchmove event of an active touch point, it should prevent any default action caused by any touchmove event associated with the same active touch point, such as scrolling.(如果在活动触摸点的第一个touchmove事件上调用preventDefault方法,它应该防止由与同一个活动触摸点关联的任何touchmove事件(如滚动)引起的任何默认操作。)


touchmove 事件的速度是可以实现定义的,取决于硬件性能和其他实现细节


preventDefault 方法,阻止同一触点上所有默认行为,比如滚动。




由此我们找到解决方案,通过监听 touchmove,让需要滑动的地方滑动,不需要滑动的地方禁止滑动。


值得注意的是我们要过滤掉具有滚动容器的元素。


实现如下:

document.body.addEventListener('touchmove', function(e) {
if (e._isScroller) return;
// 阻止默认事件
e.preventDefault();
}, {
passive: false
});

2. 滚动妥协填充空白,装饰成其他功能


在很多时候,我们可以不去解决这个问题,换一直思路。


根据场景,我们可以将下拉作为一个功能性的操作


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

可能是全网第一个适配iOS灵动岛的Toast库-JFPopup

iOS
前言 我去年的一篇文章详细的介绍了我编写的一套Swift弹窗组件库一个优雅的Swift弹窗组件-JFPopup。里面适配了一套ToastView,恰逢今年苹果iPhone14 Pro以上系列新出了一套灵动岛的交互风格,所以就意外想到能否把ToastView也适...
继续阅读 »

前言


我去年的一篇文章详细的介绍了我编写的一套Swift弹窗组件库一个优雅的Swift弹窗组件-JFPopup。里面适配了一套ToastView,恰逢今年苹果iPhone14 Pro以上系列新出了一套灵动岛的交互风格,所以就意外想到能否把ToastView也适配进去灵动岛,所以此文就应运而生。我上篇文章已经很详细的介绍了JFPopup具体用法,这篇文章主要讲解适配灵动岛的心路历程。


具体效果:




用法


虽然我上篇文章已经介绍了一遍,这里我还是再写一下。另外灵动岛Toast默认适配iPhone14 Pro以上机型,无需另外操作,若不是灵动岛机型,则是默认居中,还支持top及bottom。更多详细参数请看一个优雅的Swift弹窗组件-JFPopup


Toast:


//默认仅文案

JFPopupView.popup.toast(hit: "默认toast,支持灵动岛")

//带logo ,内置success or fail

JFPopupView.popup.toast(hit: "支付成功", icon: .success)

JFPopupView.popup.toast(hit: "支付失败", icon: .fail)

//自定义logo

JFPopupView.popup.toast(hit: "自定义", icon: .imageName(name: "face"))


Loading:


DispatchQueue.main.async {

JFPopupView.popup.loading()

}

DispatchQueue.main.asyncAfter(deadline: .now() + 3) {

JFPopupView.popup.hideLoading()

JFPopupView.popup.toast(hit: "刷新成功")

}


适配灵动岛具体过程


由于苹果官方已经说了要在下半年推出的ActivityKit才会加入适配灵动岛的Api。所以目前并没有官方的api可以给我们适配。所以只能硬着头皮自己去思考适配方案了。



- 首先要知道灵动岛的区域大小


我们用最笨的方法,直接给模拟器截个图自己去算大小。至少能还原99%的效果了。如图得知,灵动岛的区域大概是宽120dt,高34dp,那半圆圆角自然为17dt。居顶部大约10dp,以及在屏幕居中。有了这些信息,我们自然就能模拟灵动岛的放大缩小转场效果了。



- ToastView新增灵动岛动画


我们在原先基础上新增灵动岛动画枚举


public enum JFToastPosition {

case center

case top

case bottom

case dynamicIsland //新增灵动岛位置动画

}


重新实现下present 及 dismiss协议的转场动画代码如下


展开:


let originSize = contianerView.jf.size

if config.toastPosition == .dynamicIsland {

contianerView.jf_size = CGSize(width: 120, height: 34)

contianerView.center = CGPoint(x: CGSize.jf.screenSize().width / 2, y: 27)

}

let updateV = {

contianerView.center = CGPoint(x: CGSize.jf.screenSize().width / 2, y: CGSize.jf.screenSize().height / 2)

if config.toastPosition == .top {

contianerView.jf_top = CGFloat.jf.navigationBarHeight() + 15

} else if config.toastPosition == .bottom {

contianerView.jf_bottom = CGSize.jf.screenHeight() - CGFloat.jf.safeAreaBottomHeight() - 15

} else if config.toastPosition == .dynamicIsland {

contianerView.jf_size = originSize

contianerView.center = CGPoint(x: CGSize.jf.screenSize().width / 2, y: originSize.height / 2 + 10)

}

contianerView.layoutIfNeeded()

}

guard config.withoutAnimation == false else {

updateV()

transitonContext?.completeTransition(true)

completion?(true)

return

}

if config.toastPosition == .dynamicIsland {

UIView.animate(withDuration: 0.25) {

updateV()

} completion: { finished in

transitonContext?.completeTransition(true)

completion?(finished)

}

return

}


消失:


UIView.animate(withDuration: 0.25, animations: {

if config.toastPosition == .dynamicIsland {

contianerView?.layer.cornerRadius = 17

contianerView?.jf_size = CGSize(width: 120, height: 34)

contianerView?.center = CGPoint(x: CGSize.jf.screenSize().width / 2, y: 27)

}

contianerView?.subviews.forEach({ v in

if config.toastPosition == .dynamicIsland {

v.isHidden = true

} else {

v.alpha = 0

}

})

contianerView?.alpha = 0

}) { (finished) in

transitonContext?.completeTransition(true)

completion?(finished)

}


末尾


以上即是我JFPopup内置组件JFToastView适配灵动岛动画的全过程,假如下半年苹果更新了Api我也会第一时间重新适配。


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

客户端开发的我,准备认真学前端了

背景 我呢,一个Android开发工程师,从毕业到现在主要做的是客户端开发,目前在一个手机厂商任职。自己目前知识技能主要在客户端上,其他方面会一点点,会一点点前端知识,会一点点后端知识,会一点点脚本,用网络的一句话概括起来就是“有点东西,但是不多”😭。 为什么...
继续阅读 »

背景


我呢,一个Android开发工程师,从毕业到现在主要做的是客户端开发,目前在一个手机厂商任职。自己目前知识技能主要在客户端上,其他方面会一点点,会一点点前端知识,会一点点后端知识,会一点点脚本,用网络的一句话概括起来就是“有点东西,但是不多”😭。


为什么


决定学习前端,并不是心血来潮,一时自嗨,而是经过了比较长时间的思考。对于程序员来说,知识的更新迭代实在是很快,所以保持学习很重要。但是技术防线这么多,到底学什么?我相信这不是一个很容易做出抉择的问题。


对于前端之前有断断续续的学过一些,但是最后没有一直坚持下来。之所以这样,原因很多,比如没有很强的目标、没有足够的时间,前端涉及的知识点太多等。


但是我觉得对自己而言,最重要的一个原因是:**学习完前端,我能用它来干嘛?**如果没有想清楚这个原因,就很难找到目标。做事情没有目标,就无法拆解,也就无法长期坚持下去。直到最近,看了一些文章,碰到了一些事情,才慢慢想清楚这个问题。目前对我而言,开始决定认真学习前端的主要原因有两个:

  • 自己一直想做点什么
  • 工作上有需要

想做点什么


从我接触计算机开始,心底里一直有个梦,就是想利用自己手上技能,做点什么。我也和旁边的朋友同事交流过,大家都有类似的想法,从这看估计很多程序员朋友都会有这样的想法。我从一开始的捣鼓网站,论坛,到后来开发APP等,折腾了好多东西。但是到了最后,都没有折腾出点啥,都无疾而终。


前一段时间,看到一个博主写的一篇文章,文章大概是讲他如何从一个公司的后端开发工程师,走到今天成为一名独立开发者的故事。


其中有一段是说他一直心里念念不忘,想做一款 saas 应用,期间一直在学习和看其他人的产品,学习经验,尝试不同的想法。所谓念念不忘必有回响,终于从别人的产品中产生了一个点子,然后很快写好了后端服务,并自学前端边做边学,完成了这个产品。目前他的这个产品运作的很成功。


这个故事给我很大鼓舞,之前看到过很多这样的故事,有成功的,有失败的。我也去分析看了那些成功的,经过自己的观察,大部分成功的独立开发者,基本上都是多年前成功的那批,那段时间还是处于互联网的红利期,天时地利人和加在一起,造就了他们的成功。


当然这里并不是否认他们能力,觉得是他们运气好。能在当时那么多人中,脱颖而出,依然表明他们是佼佼者。这里只是想表达那个时间段,大环境对开发者来说,是比较友好的,阻力没有那么大。


很少看到最近两年成功的开发者(不排除自己不知道哈),但是从这位博主的经历来看,他确实在成功了,这给了我很大的鼓舞,说明这条路上还是有机会的,只是在现在这种大环境下,成功的难度在增加,阻力变大。如果我们自己始终坚持,寻找机会,不断地尝试,是否有一天可能会成功呢?


那这样的话,我主要关注哪个方向呢?我个人更加偏向于前端全栈方向,包括WebApp,小程序,P C 软件等。


为什么这么认为呢?看下现在的大环境,不提之前上架APP需要各种软件著作权,后来个人无法在各大商店上发布APP,再到现在新出的APP备案制,基本上个人想在Android App上发力,真的很难了。而且,经过自己在ProductHunt上观察,目前大部分独立开发者的作品都是聚焦于WebAppSAAS,或者是PC类软件,剩下就是IOSMAC平台的。


且学习前端技术栈是一个比较好的选择。JavaScript这门语言很强大,整个技术栈不仅可以做前端,也可以做后端开发,还可以做跨平台的 P C 软件开发, 提供的丰富的解决方案,对个人开发者来说极为合适。


当然,我们也可以找合适的人,一起组队合作,不用单打独斗,这样不仅节省期间和精力,也能有好的交流和碰撞。这条路我也经历过,但是说实话执行起来确实有一定的困难。首先就是人难找,要想找到一个三观差不多的伙伴,其实真的挺难的。还有一个就是个人时间和做事方式也很难契合。所以个人认为如果想做点什么,前期一个人自己去实现一个MVP出来,是一个合适的选择。后面如果有必要了,倒是可以考虑慢慢招人。


我们也要认识到技术只是最基础的第一步,要想做成一个产品,还有很多东西要学习。推广、运营,沟通交流无论哪个都是一道坎。但是作为登山者的我们不要关注前面路有多远,而是要确保自己一直在路上。


工作涉及


还有一个原因是,最近工作上和前端打交道有很多。因为项目内部接入了类似 React Native 的框架,有大量的业务场景是基于这个框架开发。这就导致了客户端涉及到大量和前端的交互,流程的优化,工程化等工作。客户端可以不用了解前端和框架的知识,也没什么问题。
但是想着如果后续这一块有什么问题,或者想对这块做一些性能优化、工程提效的事情,如果对前端知识没有一个很好的了解,估计也很难做出彩。


结尾


今天在这里絮絮叨叨这么多,并不是想要告诉大家选择前端技术栈学习就一定咋样,比如第一点说的独立开发者中,有很多的全栈开发者,他们有的已经失败了,有的还在路上,成功的毕竟还是少数。
我想分享的是我个人关于为什么选择前端技术栈作为学习方向,如何做出选择的一些思考。这都是我的一家之言,不一定正确,大家姑且一看。


同时自己心里也还是希望能像文章提到的那位博主一样,在做产品这条路上,也能“念念不忘,必有回响”。正如我一直相信秉持的“日拱一卒,功不唐捐”。


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

某法宝网站 js 逆向思路

web
本文章只做技术探讨, 请勿用于非法用途。 目标网站 近期为了深刻学习法律知识, 需要来下载一些法律条文相关的内容。 本文通过 某法宝网站来下载数据 http://www.pkulaw.com/。 网站分析 文章内容 详情页图片 可以看到下载的方式还蛮多的,...
继续阅读 »

本文章只做技术探讨, 请勿用于非法用途。



目标网站


近期为了深刻学习法律知识, 需要来下载一些法律条文相关的内容。


本文通过 某法宝网站来下载数据 http://www.pkulaw.com/。


网站分析


文章内容


image.png
详情页图片


可以看到下载的方式还蛮多的, 尝试复制全文, 得到内容保留原格式, 所以我选择使用复制全文的方式来得到文章内容。


同时详情页没有看到明显的反扒措施, 不需要特殊处理。


列表页


image.png


最终决定通过专题分类来获取所有的数据, 然后调试分析接口参数, 这里没有什么加密的参数, 确定需要关注的参数如下:


{
"Aggs.SpecialType": "", // 专题类型(编号)
"VerifyCodeResult": "", // 验证码值(后边讲解)
"Pager.PageIndex": "2", // 页码
"RecordShowType": "List", // 显示方式(List 方式显示所有数据, 保持该值即可)
"Pager.PageSize": 100, // 每页的数量(最大 100)
}


这里仅列出了需要关注的参数, 其他参数保持原值即可(需要的话可以自己调试对比参数值确定意义), 当 pageSize 设置为 100 时, 第 3 页之后的数据需要验证码才能查看。


验证码


为方便分析验证码的校验方式, 推荐使用无痕模式来调试, 获取验证码之前清空一次 cookie。


image.png


image.png


image.png


可以看到验证码的流程为:



  1. 请求验证码, 并返回 set-cookie 。

  2. 携带该 cookie 信息并校验验证码, 通过后得到 code。

  3. 携带 code 请求数据, 得到数据。


开整


确定了问题后, 就可以开始了, 一个一个解决。


文章内容


确定了要用复制的方式来得到数据, 那就分析下他的复制干了点啥。


image.png


查看他的页面元素, 发现了这两个东西, 全局搜索后很容易定位到处理函数(找不到的话刷新页面)。


image.png


然后接着定位这个 toCopyFulltext() 函数。


image.png


找到这个就对了, 然后可以看代码他是用的 jQuery 的选择器来做的, 我们只能来仿造一个页面结构来用它, 这里推荐使用 cheerio(nodejs) 库来做(jsdom 应该也可以?)。


image.png


伪造后直接调用就大功告成。


列表页


这个问题不大, 问题主要在于验证码。


image.png


通过查看页面元素可以得到专题编号。


验证码


请求


// 验证码图片请求返回结果
{
"errcode": 0,
"y": 131,
"array": "14,12,11,7,6,2,13,15,9,8,16,0,1,3,4,18,17,5,19,10",
"imgx": 300,
"imgy": 200,
"small": "data:image/jpg;base64,...", // 滑块图片
"normal": "data:image/jpg;base64,..." // 背景图片
}

image.png
得到的 base64 可以用 python 的 PIL 库来解析出来。


image.png
然后我解析出来的图片就是这个样子, 基本上就确定了这验证码就是老朋友了, 我们先来把他还原。



还是简单解释一下这个, 图片被切割为了上下两部分, 每个部分又被切割为了 10 等份, 原图为 300x200, 也就是说这里被切割为 20 张 30x100 的图片, 然后打乱顺序拼接后返回, 我们需要先完成切割然后再按正确的顺序来拼接还原。



"errcode": 0,
"y": 131,
"array": "14,12,11,7,6,2,13,15,9,8,16,0,1,3,4,18,17,5,19,10", // 图片的正确顺序
"imgx": 300, // 图片宽
"imgy": 200, // 图片高
"small": "data:image/jpg;base64,...", // 滑块图片
"normal": "data:image/jpg;base64,..." // 背景图片

image.png


image.png


还原后的图片就是这个样子了。


校验


// 校验接口请求参数
{
"act": "check",
"point": "197", // 缺口位置
"timespan": "1067", // 滑动耗时
"datelist": "1,1692848585282|10,1692848585288|20,1692848585295|34,1692848585305|50,1692848585312|58,1692848585320|74,1692848585328|90,1692848585338|95,1692848585344|107,1692848585355|117,1692848585360|124,1692848585371|126,1692848585376|130,1692848585388|133,1692848585393|136,1692848585404|137,1692848585409|138,1692848585416|139,1692848585425|139,1692848585433|140,1692848585441|140,1692848585449|140,1692848585457|141,1692848585465|141,1692848585473|141,1692848585481|142,1692848585489|142,1692848585498|142,1692848585506|143,1692848585514|143,1692848585522|143,1692848585530|144,1692848585539|145,1692848585546|146,1692848585554|146,1692848585562|149,1692848585572|151,1692848585579|154,1692848585589|157,1692848585596|160,1692848585604|161,1692848585611|164,1692848585621|167,1692848585627|170,1692848585639|172,1692848585643|174,1692848585655|177,1692848585659|179,1692848585667|181,1692848585676|182,1692848585683|184,1692848585692|185,1692848585699|186,1692848585710|187,1692848585716|188,1692848585724|189,1692848585732|189,1692848585740|190,1692848585748|191,1692848585758|192,1692848585764|192,1692848585773|193,1692848585781|194,1692848585789|195,1692848585797|195,1692848585806|196,1692848585813|196,1692848585821|197,1692848585829|197,1692848585839|197,1692848585845|197,1692848585855|197,1692848586219"
} // 滑动轨迹(位置,时间戳)

需要解决的参数为 point(缺口位置) 及 datelist(滑动轨迹)。


point

image.png


推荐使用 ddddocr 库来识别, 准确率还可以吧, 挺方便的。


datelist

image.png


轨迹方面自己设计算法来吧, 这里可以作为一个参考, 他的 datelist 的长度不固定, 一般也就是一百多些轨迹点吧, 可以通过调整参数来达到效果, 反正就是多测试吧, 这个方法大概有 百分之九十 左右的通过率吧, 暂时够用。


VerifyCodeResult


// 校验请求成功后返回数据
{
"state": 0,
"info": "正确",
"data": 197
}
// VerifyCodeResult: YmRmYl8xOTc=

解决了验证码, 惊喜的发现还是不咋对, 这个返回的 data 明显长得和要用的 VerifyCodeResult 不太像, 就接着来找。


image.png


全局搜索, 找到两个 js 文件, 都打上断点来调试。


image.png


可以看到我们成功断到, 并得到是由 (new Base64).encode("bdfb_" + y) 这种方式来生成的 code 值, 这个 y 值就是上一步返回的 data, 接下来只需要把 Base64 的代码那里扣下来, 或者自己实现就行了, 方便些, 这里直接抠下来了, 然后拿到 code 带上验证码请求返回的 cookie, 就能正常拿到数据了。


结语


整体来说不算困难, 没有什么加密啊混淆啊之类的东西, 确定方向之后很快就能搞定了, 用来练手还是很不错的, 有什么问题欢迎交流, 不知道这个得几年啊。。。



请洒潘江,各倾陆海云

作者:Glommer
来源:juejin.cn/post/7270702261293039635
尔。


收起阅读 »

数据抓取:抓取手机设备各种数据

目录 前言 一、DataCapture 1.通讯录集合数据 2.应用列表集合数据 3.日历事件信息数据 4.电量信息数据 5.sms短信信息数据 6.照片集合信息数据 7.传感器信息数据 8.wifi信息数据...等等数据 二、使用步骤 1.引入库 2....
继续阅读 »

目录


前言


一、DataCapture



  • 1.通讯录集合数据

  • 2.应用列表集合数据

  • 3.日历事件信息数据

  • 4.电量信息数据

  • 5.sms短信信息数据

  • 6.照片集合信息数据

  • 7.传感器信息数据

  • 8.wifi信息数据...等等数据


二、使用步骤



  • 1.引入库

  • 2.获取数据方法,目前因数据量庞大,暂推荐手动在子线程调用

  • 3.关于权限,待更新

  • 总结




前言


基于最近刚完结的外包项目功能——数据抓取,通过调用api和内容提供器来获取手机设备各种数据,诸如SMS短信数据、电量数据、手机应用数据等等,我尝试开发了一个开源库,希望能够帮助到大家来实现这个功能。


习惯性上图展示:


在这里插入图片描述


一、DataCapture


对手机设备的信息数据抓取,目前支持在子线程抓取数据,因为有些数据量过于庞大会阻塞线程,可抓取数据有:


1.通讯录集合数据


字段名详情
contact_display_name联系人名称
last_time_contacted上次通讯时间(毫秒)
number联系人手机号
times_contacted联系次数
up_time编辑时间(毫秒))
type通话类型

2.应用列表集合数据


字段名详情
app_nameAPP名称
app_type是否系统app 0:非系统app 1:系统app
app_versionAPP版本
in_time安装时间(毫秒)
obtain_time数据抓取时间(秒))
package_name包名
up_time更新时间 (毫秒)
version_code版本号

3.日历事件信息数据


字段名详情
description事件描述
end_time事件结束时间(毫秒)
event_id事件ID
event_title事件标题
start_time事件开始时间(毫秒))
reminders提醒列表

4.电量信息数据


字段名详情
battery_level电池电量
battery_max电池容量
battery_pct电池百分比
battery_state电池状态 充电0 不充电1
is_ac_charge是否交流充电(1:yes,0:no)
is_charging是否正在充电
is_usb_charge是否USB充电(1:yes,0:no)

5.sms短信信息数据


字段名详情
content短信消息体
other_phone收件⼈/发件⼈⼿机号
package_name包名
read短信状态 0-未读,1-已读
seen短信是否被用户看到 0-尚未查看,1-已查看
status短信状态:-1表示接收,0-complete,64-pending,128-failed
subject短信主题
time收到短信的时间戳(毫秒),long型
type短信类型:1-接收短信,2-已发出短信

6.照片集合信息数据


字段名详情
addTime添加数据库时间(保存)
author照片作者
createTime照片读取时间(毫秒数时间戳),即当前时间
date拍照时间(毫秒数时间戳)
flash闪光灯
focal_length镜头的实际焦距
gps_altitude海拔高度
gps_processing_method定位的方法名称
height照片高度
latitude照片拍摄时的经度
lens_make镜头制造商
lens_model镜头的序列号
longitude照片拍摄时的纬度
model拍照机型
name照片名称
orientation照片方向
save_time照片修改时间
software生成图像的相机或图像输入设备的软件或固件的名称和版本
take_time创建时间(毫秒数时间戳)
updateTime编辑时间
width照片宽度
x_resolutionX方向上每个分辨率的像素数
y_resolutionY方向上每个分辨率的像素数

7.传感器信息数据


字段名详情
id传感器id,0不支持功能,-1即其类型和名称的组合在系统中唯一标识。-2获取不到
maxRange传感器单元中传感器的最大量程
minDelay两个事件之间允许的最小延迟(以微秒为单位),如果此传感器仅在其测量的数据发生变化时返回值,则为零
name传感器名称
power使用时功率
resolution传感器单元中传感器的分辨率
type该传感器的通用类型
name传感器名称
vendor厂商字符串
version版本

8.wifi信息数据...等等数据


二、使用步骤


1.引入库


在seetings.gradle中引入


repositories {
google()
mavenCentral()
maven { url 'https://jitpack.io' }
}

在build.gradle中引入


 implementation 'com.github.Android5730:DataCapture:v0.23'

2.获取数据方法,目前因数据量庞大,暂推荐手动在子线程调用


// 获取通讯录
List<AddressBookBean> addressBookBean = AddressBookUtil.getAddressBookBean(getBaseContext());
// 获取应用列表
List<AppListBean> appListBean = AppListUtil.getAppListBean(this);
// 获取日历事件
List<CalendarListBean> calendarListBean = CalendarListUtil.getCalendarListBean(this);
// 获取电量信息
BatteryStatusBean batteryState = BatteryStatusUtil.getBatteryState(this);
// 获取wifi信息
NetworkBean networkBean = NetworkBeanUtils.getNetworkBean(this);
// 获取sms短信信息
List<SmsBean> smsList = SmsUtil.getSmsList(this);
// 获取照片集合信息
List<PhotoInfosBean> photoInfosBean = PhotoInfosUtil.getPhotoInfosBean(this, LocationUtils.getInstance(this).showLocation());
// 获取传感器集合信息
List<SensorListBean> sensorListBean = SensorListUtil.getSensorListBean(this);


3.关于权限,待更新


注意:因为获取图片时需要外部存储的权限,我这里采取的取消分区存储的做法,所以大家不要忘记在application里添加android:requestLegacyExternalStorage="true"
如果有哪个权限碍眼,或者项目强制不需要,也可以进行删除,如去除读取外部存储的权限:


    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
tools:node="remove"/>


    <!-- 定位权限,需动态请求 -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<!-- 通讯录,需动态请求 -->
<uses-permission android:name="android.permission.READ_CONTACTS" />
<!-- 日历信息,需动态请求 -->
<uses-permission android:name="android.permission.READ_CALENDAR" />
<!-- wifi信息,不用动态请求 -->
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<!-- SMS信息,需动态请求 -->
<uses-permission android:name="android.permission.READ_SMS" />
<!-- photo信息,需动态请求-->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<!-- 取消分区存储-->
<meta-data
android:name="ScopedStorage"
android:value="true" />


最后附上开源库地址:数据抓取:https://github.com/Android5730/DataCapture
如果有帮助到各位,可以给个star,给我一点信心去完善这个开源库


总结


当然目前该库目前抓取的数据还不到外包项目抓取数据的一半,只是因为最近有点忙,没时间完善所以才匆匆忙忙推出,相信等开学后就有时间完善,现在实习太累了。如果大家有疑问,可以在评论区提出,也可以在issue提出来,如果

作者:人间正四月
来源:juejin.cn/post/7271659608011358264
受到大家欢迎,我会持续完善此库。

收起阅读 »

Android简单的两级评论功能实现

Android简单的两级评论功能实现 前言 在App开发过程中,做了文章页面,那评论的功能自然是必不可少的,怎么做呢,如果只是做一个简单的评论不带回复功能的话,那和之前做毕设时候的我也看不到进步呀,这怎么行呢?于是我打开‘稀土掘金’App,随便看了几个文章,试...
继续阅读 »

Android简单的两级评论功能实现


前言


在App开发过程中,做了文章页面,那评论的功能自然是必不可少的,怎么做呢,如果只是做一个简单的评论不带回复功能的话,那和之前做毕设时候的我也看不到进步呀,这怎么行呢?于是我打开‘稀土掘金’App,随便看了几个文章,试了试评论的功能,于是便开始了我的构思。我想要实现的效果如下图所示,如何实现这样一个页面呢?我使用的方法是RecyclerView中再嵌套一个RecyclerView,一个用来展示一级评论,另一个则用来展示相应的二级评论,思路有了,下面就开始我的实现。


1693188380832.png

一、数据库


1、构建数据库


要想做好一个功能,数据库的构建是重中之重。下图是我构造的评论实体类


image.png

评论表中包含如下字段:



  • id --评论主键(自动生成)

  • newsId -- 主键

  • number -- 评论的用户主键

  • content -- 评论内容

  • time -- 评论时间

  • level -- 评论级别(有两级,当评论的对象是文章作者时,level为1,当评论对象为文章内的评论时,level为2,默认level为1)

  • replyNumber -- 评论回复的用户主键

  • replyId -- 评论回复的评论主键(只有level为2的评论才会用到该字段,所以默认为空)



replyNumber其实这里不应该默认为空的,因为无论是那种类型的回复,都是有对应的用户的,这个疏忽也造成了我在后面构建“我的评论”界面时,无法展示出文章作者的详细信息。



2、封装数据库


数据访问层Dao主要封装了对数据库的访问:


image.png

很平常的SQL语句,只简单说明下:分别是添加评论、根据id删除评论、获取该文章的所有评论、获取该用户的所有评论、通过id获取该评论



(省略了CommentTask接口即实现)


最后仓库层将这些方法都封装起来,方便后续调用,如下图所示:
image.png


二、布局


1、文章详情界面的评论布局


1693191191816.png



就是个RecyclerView哈哈



2、评论的适配器布局


1693191324123.png



可以看到适配器布局中还包含了一个RecyclerView,这里面展示的就是二级评论



3、二级评论的适配器布局


image.png



这个布局很简单,就由几个TextView组件构成



三、代码逻辑


首先,在ViewModel层初始化该文章的所有评论,观察评论数据变化,给评论适配器数据赋值并刷新,在评论适配器中再对level为2的评论数据进行过滤并赋值给回复适配器。


1、获取评论数据


var comments = MutableLiveData<List<CommentInfo>>()
comments.value = commentStoreRepository.getCommentsByNewId(newsId)

通过文章的id获取到评论


2、给评论适配器数据赋值


image.png


3、在评论适配器处理数据



首先,评论适配器中的数据是通过文章的id获取到的所有评论,包含了一级和二级评论,在评论适配器展示的当然不能是所有的评论,而是所有一级的评论,而二级评论的数据需要再进行过滤传递给回复适配器



所以,在绑定ViewHolder以及getItemCount时,需要对传递的数据进行过滤,


image.png
如图所示,allList是通过文章的id获取到的所有评论,list是level为1的所有评论,replyList是level为2的所有评论。getItemCount返回的是一级评论的个数。在绑定ViewHolder时,将一些回调函数和一级评论和二级评论列表传递进去,接着就看ViewHolder中的数据处理逻辑,如下两张图


image.png



这张图只是一些简单的一级数据的赋值和一些回调参数的调用传参



image.png



这里首先对二级评论进行过滤,过滤出与该条一级评论相关联的二级评论,接着对布局进行一些操作,接着是赋值操作和回复适配器中一些函数的实现。



4、在回复适配器处理数据


image.png



在这里就不需要对数据进行处理了,只有简单的赋值和回调了



5、回调函数的实现


image.png


四、实现效果


1、评论功能


7edbc47b85fd05b9c25841161eb4ba8.jpg

2、我的评论展示


dd6d2787fbf6ebf4a397367fc91fd4e.jpg

这里的“@3333333333”就是因为replyNumber为空的导致无法展示出文章作者的详细信息,只有展示用户主键了,后面再进行修改。



五、结语


就这样,一个简单的二级评论功能就完成了。文章若出现错误,欢迎各位批评指正,写文不易,转载<

作者:遨游在代码海洋的鱼
来源:juejin.cn/post/7271991667246694437
/strong>请注明出处谢谢。

收起阅读 »

JS长任务(耗时操作)导致页面卡顿,优化方案大比拼!

web
抛出问题 前端是直接和客户交互的界面,前端的流畅性对于客户的体验和留存率至关重要,可以说,前端界面的流畅性是一款产品能不能成功的关键因素之一。要解决界面卡顿,首先我们先要知道造成页面卡顿的原因: 什么是长任务? 长任务是指JS代码执行耗时超过50ms,能让...
继续阅读 »

抛出问题


前端是直接和客户交互的界面,前端的流畅性对于客户的体验和留存率至关重要,可以说,前端界面的流畅性是一款产品能不能成功的关键因素之一。要解决界面卡顿,首先我们先要知道造成页面卡顿的原因:




  • 什么是长任务?


    长任务是指JS代码执行耗时超过50ms,能让用户感知到页面卡顿的代码执行流。




  • 长任务为什么会造成页面卡顿?


    UI界面的渲染由UI线程控制,UI线程和JS线程是互斥的,所以在执行JS代码时,UI线程无法工作,就表现出页面卡死状态。




我们现在来模拟一个长任务,看看它是怎么影响页面流畅性的:



  • 先看效果(GIF),这里我们给div加了个滚动的动画,当我们开始执行长任务后,页面卡住了,等待执行完后才恢复,总耗时3秒左右,记住总耗时,后面会用到。


动画.gif



  • 再看代码(有点长,主要看JS部分,后面的优化方案代码只展示优化过的JS函数)


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
.myDiv {
width: 100px;
height: 100px;
margin: 50px;
background-color: blue;
position: relative;
animation: my-animation 5s linear infinite;
}
@keyframes my-animation {
from {
left: 0%;
rotate: 0deg;
}
to {
left: 100%;
rotate: 360deg;
}
}
</style>
</head>
<body>
<div class="myDiv"></div>
<button onclick="longTask()">执行长任务</button>

<script>
// 模拟耗时操作,大概10ms
function myFunc() {
const startTime = Date.now();
while (Date.now() - startTime < 10) {}
}

// 长任务,循环执行myFunc300次,耗时3秒左右
function longTask() {
console.log("开始长任务");
const startTime = Date.now();
for (let i = 0; i < 300; i++) {
myFunc();
}
console.log(`长任务结束,总耗时:${Date.now() - startTime}ms`);
}
</script>
</body>
</html>


本段代码有一个模拟耗时的函数,有一个模拟长任务的函数(调用300次耗时函数),后面的优化方案都会基于这段代码来进行。


优化方案


setTimeout 宏任务方案


第一个优化方案,我们将长任务拆成多个宏任务来执行,这里我们用setTimeout函数。为什么拆成多个宏任务可以优化卡顿问题呢?


正如我们上文所说,页面卡顿的原因是因为JS执行线程占用了控制权,导致UI线程无法工作。在浏览器的事件轮询(EventLoop)机制中,每一个宏任务执行完之后会将控制权重新交给UI线程,待UI线程执行完渲染任务后,才会继续执行下一个宏任务。浏览器轮询机制流程图如下所示,想要深入了解浏览器轮询机制,可以参考我的另一篇文章:从进程和线程入手,我彻底明白了EventLoop的原理! image.png



  • 先看效果(GIF),执行长任务的同时,页面也很流畅,没有了先前卡顿的感觉,总耗时4.4秒。


动画.gif



  • 再看代码


// setTimeout方案 递归,循环300次
function timeOutTask(i, startTime) {
setTimeout(() => {
if (!startTime) {
console.log("开始长任务");
i = 0;
startTime = Date.now();
}
if (i === 300) {
console.log(`长任务结束,总耗时:${Date.now() - startTime}ms`);
return;
}
myFunc();
timeOutTask(++i, startTime);
});
}

把代码改为多个宏任务之后,解决了页面卡顿的问题,但是总耗时比之前多了1.4秒,主要原因是因为递归调用需要不断地向下开栈,会增加开销。当我们每个任务都不依赖于上一个任务的执行结果时,就可以不使用递归,直接使用循环创建宏任务。



  • 先看效果(GIF),耗时缩短到了3.1秒,但是可以看到明显掉帧。


动画.gif



  • 再看代码


// setTimeout不递归方案,循环300次
function timeOutTask2() {
console.log("开始长任务");
const startTime = Date.now();

for (let i = 0; i < 300; i++) {
setTimeout(() => {
myFunc();
if (i === 300 - 1) {
console.log(`长任务结束,总耗时:${Date.now() - startTime}ms`);
}
});
}
}

使用300个循环同时创建宏任务后,虽然耗时降低了,但是div滚动会出现明显掉帧,这也是我们不愿意看到的,那执行代码速度和页面流畅度就没办法兼得了吗?很幸运,requestIdleCallback函数可以帮你解决这个难题。


requestIdleCallback 函数方案


requestIdleCallback提供了由浏览器决定,在空闲的时候执行队列任务的能力,从而不会影响到UI线程的正常运行,保证了页面的流畅性。


它的用法也很简单,第一个参数是一个函数,浏览器空闲的时候就会把函数放到队列执行,第二个参数为options,包含一个timeout,则超时时间,即使浏览器非空闲,超时时间到了,也会将任务放到事件队列。
下面我们把setTimeout替换为requestIdleCallback



  • 先看效果(GIF),耗时3.1秒,也没有出现掉帧的情况。


动画.gif



  • 再看代码


// requestIdleCallback不递归方案,循环300次
function callbackTask() {
console.log("开始长任务");
const startTime = Date.now();

for (let i = 0; i < 300; i++) {
requestIdleCallback(() => {
myFunc();
if (i === 300 - 1) {
console.log(`长任务结束,总耗时:${Date.now() - startTime}ms`);
}
});
}
}

requestIdleCallback解决了setTimeout方案掉帧的问题,这两种方案都需要拆分任务,有没有一种不需要拆分任务,还能不影响页面流畅度的方法呢?Web Worker满足你。


Web Worker 多线程方案


WebWorker是运行在后台的javascript,独立于其他脚本,不会影响页面的性能。



  • 先看效果,耗时不到3.1秒,页面也没有受到影响。


动画.gif



  • 再看代码,需要额外创建一个js文件。(注意,浏览器本地直接运行HTML会被当成跨域,需要开一个服务运行,我使用的http-server)


task.js 文件代码


// 模拟耗时
function myFunc() {
const startTime = Date.now();
while (Date.now() - startTime < 10) {}
}

// 循环执行300次
for (let i = 0; i < 300; i++) {
myFunc();
}

// 通知主线程已执行完
self.postMessage("我执行完啦");

主文件代码


// Web Worker 方案
function workerTask() {
console.log("开始长任务");
const startTime = Date.now();
const worker = new Worker("./task.js");

worker.addEventListener("message", (e) => {
console.log(`长任务结束,总耗时:${Date.now() - startTime}ms`);
});
}

WebWorker方案额外增加的耗时很少,也不需要拆分代码,也不会影响页面性能,算是很完美的一种方案了。
但它也有一些缺点:



  • 浏览器兼容性差

  • 不能访问DOM,即不能更新UI

  • 不能跨域加载JS


总结


三种方案中,如果不需要访问DOM的话,我认为最好的方案为WebWorker方案,其次requestIdleCallback方案,最后是setTimeout方案。
WebWorker和requestIdleCallback属于比较新的特性,并非所有浏览器都支持,所以我们需要先进行判断,代码如下:


if (typeof Worker !== 'undefined') {
//使用 WebWorker
}else if(typeof requestIdleCallback !== 'undefined'){
//使用 requestIdleCallback
}else{
//使用 setTimeout
}

希望本文对您有帮助,其他所有代码可在下方直接执行。(WebWorker不支持)

作者:TuYuHao
来源:juejin.cn/post/7272632260180377634
n>

收起阅读 »

你看这个圆脸😁,又大又可爱~ (Compose低配版)

web
前言 阅读本文需要一定compose基础,如果没有请移步Jetpack Compose入门详解(实时更新) 在网上看到有人用css写出了下面这种效果;原文链接 我看到了觉得很好玩,于是用Compose撸了一个随手势移动眼珠的版本。 一、Canvas画图 这...
继续阅读 »


前言


阅读本文需要一定compose基础,如果没有请移步Jetpack Compose入门详解(实时更新)


在网上看到有人用css写出了下面这种效果;原文链接


请添加图片描述


我看到了觉得很好玩,于是用Compose撸了一个随手势移动眼珠的版本。




一、Canvas画图


这种笑脸常用的控件肯定实现不了,我们只能用Canvas自己画了


笑脸


我们先画脸



下例当中的size和center都是onDraw 的DrawScope提供的属性,drawCircle则是DrawScope提供的画圆的方法



Canvas(modifier = modifier
.size(300.dp),
onDraw = {

// 脸
drawCircle(
color = Color(0xfffecd00),
radius = size.width / 2,
center = center
)

})

属性解释



  • color:填充颜色

  • radius: 半径

  • center: 圆心坐标


这里我们半径取屏幕宽度一半,圆心取屏幕中心,画出来的脸效果如下


在这里插入图片描述


微笑


微笑是一个弧形,我们使用drawArc来画微笑


// 微笑
val smilePadding = size.width / 4

drawArc(
color = Color(0xffb57700),
startAngle = 0f,
sweepAngle = 180f,
useCenter = true,
topLeft = Offset(smilePadding, size.height / 2 - smilePadding / 2),
size = Size(size.width / 2, size.height / 4)
)

属性解释



  • color:填充颜色

  • startAngle: 弧形开始的角度,默认以3点钟方向为0度

  • sweepAngle:弧形结束的角度,默认以3点钟方向为0度

  • useCenter :指示圆弧是否要闭合边界中心的标志(上例加不加都无所谓)

  • topLeft :相对于当前平移从0的本地原点偏移,0开始

  • size:要绘制的圆弧的尺寸


效果如下
在这里插入图片描述


眼睛和眼珠子


眼睛也是drawCircle方法,只是位置不同,这边就不再多做解释


            // 左眼
drawCircle(
color = Color.White,
center = Offset(smilePadding, size.height / 2 - smilePadding / 2),
radius = smilePadding / 2
)


//左眼珠子
drawCircle(
color = Color.Black,
center = Offset(smilePadding, size.height / 2 - smilePadding / 2),
radius = smilePadding / 4
)



// 右眼
drawCircle(
color = Color.White,
center = Offset(smilePadding * 3, size.height / 2 - smilePadding / 2),
radius = smilePadding / 2
)


//右眼珠子
drawCircle(
color = Color.Black,
center = Offset(smilePadding * 3, size.height / 2 - smilePadding / 2),
radius = smilePadding / 4
)


整个笑脸就画出来了,效果如下


在这里插入图片描述


二、跟随手势移动


在实现功能之前我们需要介绍transformableanimateFloatAsStatetranslate


transformable


transformablemodifier用于平移、缩放和旋转的多点触控手势的修饰符,此修饰符本身不会转换元素,只会检测手势。


animateFloatAsState


animateFloatAsState 是通过Float状态变化来控制动画 的状态


知道了这两个玩意过后我们就可以先通过transformable监听手势滑动然后通过translate方法和animateFloatAsState方法组成一个平移动画来实现眼珠跟随手势移动


完整的代码:


@Composable
fun SmileyFaceCanvas(
modifier: Modifier
)
{

var x by remember {
mutableStateOf(0f)
}

var y by remember {
mutableStateOf(0f)
}

val state = rememberTransformableState { _, offsetChange, _ ->

x = offsetChange.x
if (offsetChange.x >50f){
x = 50f
}

if (offsetChange.x < -50f){
x=-50f
}

y = offsetChange.y
if (offsetChange.y >50f){
y= 50f
}

if (offsetChange.y < -50f){
y=-50f
}
}

val animTranslateX by animateFloatAsState(
targetValue = x,
animationSpec = TweenSpec(1000)
)

val animTranslateY by animateFloatAsState(
targetValue = y,
animationSpec = TweenSpec(1000)
)



Canvas(
modifier = modifier
.size(300.dp)
.transformable(state = state)
) {



// 脸
drawCircle(
Color(0xfffecd00),
radius = size.width / 2,
center = center
)

// 微笑
val smilePadding = size.width / 4

drawArc(
color = Color(0xffb57700),
startAngle = 0f,
sweepAngle = 180f,
useCenter = true,
topLeft = Offset(smilePadding, size.height / 2 - smilePadding / 2),
size = Size(size.width / 2, size.height / 4)
)

// 左眼
drawCircle(
color = Color.White,
center = Offset(smilePadding, size.height / 2 - smilePadding / 2),
radius = smilePadding / 2
)

translate(left = animTranslateX, top = animTranslateY) {
//左眼珠子
drawCircle(
color = Color.Black,
center = Offset(smilePadding, size.height / 2 - smilePadding / 2),
radius = smilePadding / 4
)
}


// 右眼
drawCircle(
color = Color.White,
center = Offset(smilePadding * 3, size.height / 2 - smilePadding / 2),
radius = smilePadding / 2
)

translate(left = animTranslateX, top = animTranslateY) {
//右眼珠子
drawCircle(
color = Color.Black,
center = Offset(smilePadding * 3, size.height / 2 - smilePadding / 2),
radius = smilePadding / 4
)
}


}
}

为了不让眼珠子从眼眶里蹦出来,我们将位移的范围限制在了50以内,运行效果如下


在这里插入图片描述




总结


通过Canvas中的一些方法配合简单的动画API实

作者:我怀里的猫
来源:juejin.cn/post/7272550100139098170
现了这个眼珠跟随手势移动的笑脸😁

收起阅读 »

入行十年,卷王也卷不动了,想对新人说

很多年前,当我还是一名学生的时候,有一次高我好几届已工作几年的师兄回校给我们做交流,听说他已经是“高级研发工程师”,在深圳某企业月入上万。那时候心里一阵崇拜,觉得“高级”开发该是多么厉害的存在,让我无数次憧憬着成为像他一样厉害且收入高的人群。 时光荏苒,一晃十...
继续阅读 »

很多年前,当我还是一名学生的时候,有一次高我好几届已工作几年的师兄回校给我们做交流,听说他已经是“高级研发工程师”,在深圳某企业月入上万。那时候心里一阵崇拜,觉得“高级”开发该是多么厉害的存在,让我无数次憧憬着成为像他一样厉害且收入高的人群。


时光荏苒,一晃十年过去了。自己也从当初的菜鸟,成长为“高级研发工程师”,然后做了管理、带了团队。然而,在互联网摸爬滚打多年后,发现很多事情跟自己当初想象的完全不一样:



  • 编程并不总是随着经验累积越多,你越发轻松

  • 长时间写一线业务代码并不有趣,甚至有些无聊

  • 如果你只想在你的职业生涯中安静的研究技术,那么你在这个行业很难走远


回溯过往,眼瞅着这个行业潮起潮落,仍然有越来越多的新人奋不顾身的涌入,在此以一个行业“老兵”的身份给即将或者刚入行的同学一些感悟和分享,希望能让你们少踩一些坑,踏上更坚实的职业征程。


珍惜前两年,用力去卷


我一直认为,这个行业(可能其他行业也是)新人成长最快的就是前两年。这是因为:



  • 新人刚刚步入职场,对于新的挑战和机会充满了热情。你们带着刚从学校获得的知识和技能,急切地想要将这些应用到实际工作中,从而迅速增长。

  • 在前两年,新人通常会承担较少的重要项目和高级别的责任。这为你们提供了一个相对安全的环境,可以更自由地学习和尝试新事物,而不必担心严重的后果。

  • 在最初的阶段,新人会得到较多的指导和反馈。这有助于你们更快速地纠正错误、学习新知识,并逐步提高自己的能力。

  • 新人进入工作环境后,需要迅速适应不同的工作情境、流程和团队文化。这种适应性的训练使你们能够更快速地适应变化,并培养出解决问题的能力。

  • 许多新人在前两年内通常没有太多家庭负担,生活开支较少。这使得你们能够更专注地投入到工作和学习中,从而更加快速地成长。相较于后续可能出现的家庭责任和花销,你们在这段时间能够更自由地选择投入时间和精力,去学习新技能、探索新领域,并积累宝贵的经验。


想起我职业生涯的第一年,对未来充满了无尽的焦虑。也是由于这种焦虑,让我牟足了劲去学习行业技能。我不记得有多少个晚上是学到了半夜2-3点,但我的收获是专业技能得到了快速成长,为自己的职业生涯开了一个好头。


image.png


后来我进了大厂,当了管理,大厂对新人一般都会有一个培养机制,比如:3个月入门指导、6个月辅导计划、年度提升规划等。同时,在做事的标准上对新人的宽容度也会更高,会给你试错的机会,但会要求你从错误中去复盘、成长。


然而,在多年的管理生涯中,也遇到了很多新人在短暂的努力过后,变得不思进取,早早的就退出了奋勇争先的行列,被同龄人快速超越。


印象最深的是22届的一位同学,暂且称为A某,成都电子科大毕业。有名校光环,学习能力也强。从实习到入职后前半年,非常积极、努力,成长也很快,半年过后就能独立支持中小项目的研发。但随着时间推移,他身上的劣势也越发显现。在掌握了工作的基本技能后,他开始变得有些不务正业:在工作中开始花大量时间学习安全技能,但本职工作中几乎用不到。相应的,他的工作产出越来越低,交付不及时,质量不合格,对他的投诉也越来越多。经过3个多月的沟通、辅导,再沟通、给改正机会后仍然看不到任何进步,最终被辞退。


写代码很简单,但写好代码很难


当你掌握了一定的专业技能后,实现业务功能对于大多数开发者来说都不是一件难事。但想写出好代码却很难。比方说下面的代码,你认为是好代码么?


func Deduplicate(input []string) []string {
  result := []string{}
  for _, item := range input {
      exists := false
      for _, r := range result {
          if r == item {
              exists = true
              break
          }
      }
      if !exists {
          result = append(result, item)
      }
  }
  return result
}

这段代码用于对字符串数组进行去重操作。虽然实现了去重功能,但从代码质量的角度来看,它存在一些问题:



  • 性能较差:result 切片中进行遍历查找是否已存在相同的元素,时间复杂度较高,特别是当输入切片较大时。

  • 可读性不高: 嵌套的循环和多个条件判断导致代码复杂,难以一眼理解逻辑。

  • 未使用现有工具: Golang 提供了 map 数据结构可以用来更高效地实现去重,但代码中未使用。


让我们试着使用map做出改进:


func Deduplicate(input []string) []string {
  unique := make(map[string]bool)
  result := []string{}
  for _, item := range input {
      if !unique[item] {
          unique[item] = true
          result = append(result, item)
      }
  }
  return result
}

是不是瞬间看起来都舒服多了😄。


那么该如何写出好代码呢?


好代码并不仅仅是实现功能,更是一种艺术和哲学,我们在写代码时,应该多思考代码的质量和可维护性。问自己以下问题:



  • 这段代码是否易于阅读?其他人能理解吗?

  • 是否有更简洁的实现方式?

  • 是否需要加入注释来解释实现思路?

  • 是否考虑了性能问题和异常情况?

  • 是否符合团队的代码规范和设计风格?


这些问题可以总结为以下的编码标准:



  • 可读性优先: 代码应该易于阅读和理解,变量名、函数名应该具有表意性。清晰的命名可以减少代码注释的需要。

  • 简洁明了: 避免冗余代码,使用合适的数据结构和算法,让代码尽可能简洁,同时保持功能完整。

  • 注重性能: 在保持代码可读性的前提下,考虑算法的效率和性能。避免不必要的循环和重复计算。

  • 注释解释: 代码中应添加适量的注释,解释代码的意图、实现思路和关键步骤。这有助于其他开发者理解和维护代码。

  • 模块化设计: 将代码拆分成小的、可复用的模块,提高代码的可维护性和可测试性。

  • 错误处理: 考虑异常情况和边界条件,进行适当的错误处理,避免潜在的问题。

  • 版本控制: 使用版本控制工具管理代码,保留历史记录,方便回溯和团队协作。


写好代码是一件需要时间和经验积累的事情,但始终保持对代码质量的追求,将会使你成为更优秀的开发者。


想拿高薪吗?首先要成为卷王


过去的十多年是互联网疯狂发展的年代,很多人包括我吃到了这个行业的红利,行业内动则薪资上百万、甚至几百万的大有人在。但随着行业红利的逐渐下滑,越来越多的新人涌入,这个行业肉眼可见的变得越来越卷。


我印象中的“卷”是从16开始的,这一年被称作直播大战的一年,也是“短视频”元年。随着智能手机的普及,移动化的加速,万物皆"上线",行业巨头(尤其AT)疯狂扩张和竞争。什么“996是福报”、"面向ICU编程"成为行业普遍的现象。行业变得越发内卷的同时,薪资也确实水涨船高,吸引了越来越多“用生命”换钱的卷王(😭)。


这就是行业的现实,特别是这两年红利期的减少,经济的下滑,大厂业务收缩、裁员加剧,对于新人来说,竞争变得更加剧烈了。在这样的背景下,我为什么推荐新人去“卷”呢,是因为:



  • 积累经验: 通过卷,你可以迅速接触到各种项目、技术和领域,积累宝贵的实战经验。虽然过度卷可能会疲惫,但在一段时间内,你会获得比其他人更多的锻炼机会。

  • 成为多面手: 卷王往往需要在短时间内涉猎多个领域,这培养了你的多面手技能。这对于职业发展和未来的岗位选择都有好处。

  • 快速成长: 卷王面对各种挑战,需要不断学习、解决问题。这种快节奏的环境可以让你迅速成长,积累的知识和技能会让你在职业生涯中受益匪浅。

  • 适应压力: 行业的快速发展和竞争带来了巨大的工作压力,通过卷王经历,你会逐渐适应并变得更加抗压。

  • 职级晋升:通过卷让自己在公司脱颖而出,快速晋升,晋升一定伴随着薪资的增加,就算是跳槽你晋升的职级也是你薪资谈判的重要亮点。


image.png


虽然成为卷王可能需要付出更多的时间和努力,但在现如今的竞争激烈环境下,通过卷王的方式可以更快地脱颖而出,为自己的职业生涯奠定坚实的基础。


搞技术可以,但不要只搞技术


”我就想安安静静地搞技术、敲代码,用技术思维解决技术问题,也用技术思维解决业务问题。我能实现业务功能就可以了,我不想也不愿花心思去搞懂业务“。这或许是许多研发者的心声,搞懂业务是产品、运营的事情,我是技术专注技术就行了。


曾几何时,我和千千万万的技术开发一样也是这么认为的。直到有一天我感觉卷不太动了(也有可能是年纪大了😂),我发现业界新的技术、框架层出不穷,技术之路永无止尽。而且也见证过一些技术牛人随时被下岗(有一位很厉害的架构师曾经是我下属),我突然觉得:技术思维很重要,但只动技术不懂业务你就随时可替代。


毕竟,任何技术都是要为业务服务的,只有有市场的业务才能活下来,只有活下来的业务才能让公司养活技术团队。脱离市场(业务)单纯只靠技术养活团队的毕竟是极少数(行业技术推动者)。


举两个鲜活的例子,我公司之前有两个只专注技术解决问题的团队:一个是infra,一个是data。前者负责公司基础设施建设,后者负责大数据处理。他们团队对也公司各领域业务都不熟,在公司业务还不错的情况下,是有足够资源养活的。但这两年公司业绩下滑、股价大跌,最终导致大规模裁员,首先开刀的就是这两个团队。因为纯技术给公司带来不了业务收益。


这是我入行十年的一些感悟,希望能帮助更多新人在这个行业中更好地成长。无论你选择的道路如何,保持对技术的热情,同时不断拓展自己的眼界,用心去创造价值,你将能在这个变化多端的行业中持续成长,迎接未来的挑战。希望这些感悟能够为你们的职业发展提供一些指引,

作者:程序员斌少
来源:juejin.cn/post/7271542820807442487
少踩一些坑,走得更加坚定。加油!

收起阅读 »

低增长的互联网意味着什么

今天想跟大家分享一下,低增长的互联网意味着什么?那提到低增长,那不得不提在互联网的高增长。1987年9月20日,西方世界第一次通过互联网收到了中国的来信。 众所周知,互联网在中国其实发展了25年左右,在这25年里面,互联网的大部分应用和业务都是处于高增长模式...
继续阅读 »

今天想跟大家分享一下,低增长的互联网意味着什么?那提到低增长,那不得不提在互联网的高增长。1987年9月20日,西方世界第一次通过互联网收到了中国的来信。



众所周知,互联网在中国其实发展了25年左右,在这25年里面,互联网的大部分应用和业务都是处于高增长模式。这个高增长模式主要指的是用户量,订单量,交易量,这些核心的指标都呈指数上升的阶段。


于是我们看到了很多很多令人惊奇和咋舌的情况,微信用户数从零个到几万到几百万到几亿。淘宝的订单量从几十百到几百万到几个亿。我们看到了无数的APP应用,从零快速积累到百万,甚至突破几个亿。


在这个过程中,我们会发现各行各业涌现出来了各种各样的公司和APP应用,我们也看到了无数的公司敲响了纳斯达克上市的钟声。互联网也不断利用规模效应来创造财富神话,我们看到身边的人,同行的人都能够快速的拿着股票和期权成为了百万千万富翁。


我毕业的时候,刚好2015年,我从中国科学技术大学硕士毕业拿到了阿里巴巴的Offer,而正是阿里巴巴上市之后,整个园区里充斥着财富自由的声音,虽然我啥都没有,但是还是沉浸其中,仿佛我也可以通过努力,在短短几年内和他们一样,可以过上不曾想过的生活。


很多牛人也轻轻松松的成为了创始人或者联合创始人,一遍高谈阔润,一遍指点江山。一边聊着增长飞轮,一边喝着咖啡,一边畅想未来。


突然疫情来了,突然人口红利见顶了,突然出生率断崖下跌,突然国际环境一片变差。


一切的一切,貌似美梦想来的那种感觉,仿佛依然不相信,说好的百年企业,说好的数字经济,说好的财富自由,怎么就突然变了。眼见他起高楼,眼见他宴宾客,眼见他楼塌了。古语的一句句撼动心魄的词句离我们如此之近。于是,我们不想看到的裁员、股市腰斩、失业率增高,一幕幕我们看不到的东西,全部浮现了出来。


于是在互联网程序员内心对于造富的神话还没有偃旗息鼓的时候,我们突然就见证了世界的巨变。


于是不得不承认,在2022年的今天,我们互联网进入了低增长时代。


那么低增长时代意味着什么?我们要从中里面能吸取什么样的教训?我们要如何去迎接低增长时代?


有一句话说得非常好,我们人类从历史里面学到了唯一的教训,就是不会吸取任何教训。世界上每一个生命,每一个事物,每个经济现象都有自己的周期。没有任何一个东西是可以无限增长的,自然界不允许这么牛逼的存在,互联网也是一样。


也就是互联网在成立和爆发的第一天,我们就应该能预知到互联网一定会走向平稳,甚至走向衰亡。可是人们眨眼几十年的时间,我们往往会被社会裹挟着往前面前进,我们甚至不知道自己一无所知。在我们猝不及防的时间,想不到低增长就来了,我们在不知不觉中进入了存量时代。只是,来得太快,我们还没有来得及反应。


那低增长到底意味着什么?我认为我们要做好下面的准备。


首先是精细化运营。我们进入了存量时代,我们不得不进入精细化运营。也就是高增长的用户没了,我们所面对的只有不断流失的存量用户。在高增长的时代,野蛮地生长,我们有些时候不会在乎老用户的体验,我们一直把重金都砸在新业务上面,力求能继续做大规模以吸引投资者,以讲一个更加美好的故事。可是到了存量时代,我们不得不精准的运营我们存量的用户。我们不像以前那样会砸大笔的钱投入大量的人力去做一些新的产品,反而我们应该进入对于存量用户的存量功能的精细化的运营。只有迎合了这一波存量用户的需求,我们才可以防止用户流失,我们才能够保持到那些仅有的利润。


其次就是降本增效。互联网进入了存量时代,估值逻辑就变化了,原来我们以规模为衡量,只要规模越多,不管盈利还是非盈利,我们都能够拿到巨额的投资。有一句话说得非常好,早期互联网员工的工资不是公司发的,而是投资人发的。所以就算在公司里面你的业务亏损得再多,但是员工依然能拿到非常高的工资,这就是互联网高增长下的底层逻辑。而到了低增长的时代,投资人的钱已经没有了,那么发工资的主角变成了企业。我们知道有老板来发工资,你一定会知之,比较一定是要发出去十块钱,他就要至少赚回一百块钱。因为毕竟投资人的钱等于从天上掉下来的,而企业主的钱主要是从老板的身上一点一点割下来的,多花一块钱,老板都肉疼。



所以以前有免费的咖啡,水果茶,不好意思,现在没有了,原来有一年一到两次的团建,不好意思,现在也没有了。原来奖金动不动十个月,20个月起,不好意思,现在撑死了只有三个月。以前随便出差全国飞,只要有增长就有审批额度,不好意思,现在能不出差尽量不出差,能远程视频就远程视频,实在搞不定的这个客户也可以考虑不要了。


原来是粗放型的管理,我们只在乎整体的增长和氛围,现在不好意思,我们要提高效率,我们希望员工的每一分钱都花在刀刃上,每一点时间都用在了对客户的架子上。


当然,降本增效,不得不说的是,裁员是最快最高效的手段。相信未来大厂特别是亏损的互联网公司会持续进入滚动式裁员,毕竟大厂员工的人均成本就超过百万,卖出多少产品才能有百万的收入呢?我想这点老板、财务和HR都门清的。资本家也是嗜血的,当然也是怕失血的。


最后高质量增长和价值创造。最后现实情况我们不得不去考虑高质量增长和价值创造。原来我们可能采取竭泽而渔的方式,我们撒钱到处撒币以获得最高的增长。而现在我们更要看重高质量增长和价值创造。所谓高质量增长,我们要求我们每付出的一分钱,付出的每一分劳动,我们要收获有价值,有质量的客户。


也就是不付费的客户或者薅羊毛的客户,不好意思,你再怎么投诉我,我都不欢迎你。或者说有一些我们长期在原来的免费补贴时代客户,到现在可能就成为了垃圾客户。


所谓价值创造,就是我不是为了能够获取你的关注,我确实要给你创造实实在在的价值,你才会为我的服务买单。所以我觉得也是一个好事,当然这也意味着我们可能整体包括企业也进入了一种躺平时代。我觉得大部分有追求的企业应该都会思考这个问题,能否安静下来做一些长期主义的事情。也许很多企业就在这个时候被迫精耕细作,成为百年老店。


当然还不得不要说这一点,互联网的蓬勃发展结束了,既然进入了真正的增长时代,进入了精细化运营时代,不好意思,我可能不需要这么多员工,我也不会给你发这么高的工资,我们只是成为一个普普通通,用数字化创造实际价值的公司。


但是中国的技术有这么多,还会有很多人前赴后继地走在这条路上。所以在我们中国整个产业没有转型的时候我们有这么多的人才。我想在一段时间内可能都经历过一阵阵痛。当然站在好处的地方是当前的这种业务机构会倒逼人才往其他行业流走,流向原来根本招不到人的行业,比如说制造业、工厂,甚至是其他的服务业。


当然,我们先回过头来,当前的程序员该怎么办?和我们说的增长模式一样,我们这些程序员也会变成了存量的程序员。在存量时代意味着我们持续要去内卷,去竞争存量的岗位。我相信竞争也会更加的激烈,当然等我们年纪越来越大的时候,依然创造不出市场的价值的时候,我相信可能会被逐渐淘汰。所以我觉得内心也要结合准备降薪,甚至是转行,甚至是寻找下一份职业的准备。



当然站在长远的角度来看未必不是好事,至少我们薪资降下来了,我们的时间也多出来了,我们更能去好好的去问问自己生命的意义什么?虽然高薪的岗位会越来越少,但是我觉得大家也不用焦虑,毕竟底薪的岗位一大堆。而且自古以来大部分人赚不到什么钱,是一个常态。


作者:ali老蒋
来源:juejin.cn/post/7270117041525129257

收起阅读 »

pdf为什么不能被修改

web
PDF简介 PDF是Portable Document Format 的缩写,可翻译为“便携文件格式”,由Adobe System Incorporated 公司在1992年发明。 PDF文件是一种编程形式的文档格式,它所有显示的内容,都是通过相应的操作符进...
继续阅读 »

PDF简介



  • PDF是Portable Document Format 的缩写,可翻译为“便携文件格式”,由Adobe System Incorporated 公司在1992年发明。

  • PDF文件是一种编程形式的文档格式,它所有显示的内容,都是通过相应的操作符进行绘制的。

  • PDF基本显示单元包括:文字,图片,矢量图,图片

  • PDF扩展单元包括:水印,电子署名,注释,表单,多媒体,3D

  • PDF动作单元:书签,超链接(拥有动作的单元有很多个,包括电子署名,多媒体等等)


PDF的优点



  • 一致性:在所有可以打开PDF的机器上,展示的效果是完全一致,不会出现段落错乱、文字乱码这些排版问题。尤其是文档中,本身可以嵌入字体,避免了客户端没有对应字体,而导致文字显示不一致的问题。所以,在印刷行业,绝大多数用的都是PDF格式。

  • 不易修改:用过PDF文件的人,都会知道,对已经保存之后的PDF文件,想要进行重新排版,基本上就不可能的,这就保证了从资料源发往外界的资料,不容易被篡改。

  • 安全性:PDF文档可以进行加密,包括以下几种加密形式:文档打开密码,文档权限密码,文档证书密码,加密的方法包括:RC4,AES,通过加密这种形式,可以达到资料防扩散等目的。

  • 不失真:PDF文件中,使用了矢量图,在文件浏览时,无论放大多少倍,都不会导致使用矢量图绘制的文字,图案的失真。

  • 支持多种压缩方式:为了减少PDF文件的size,PDF格式支持各种压缩方式:asciihex,ascii85,lzw,runlength,ccitt,jbig2,jpeg(DCT),jpeg2000(jpx)

  • 支持多种印刷标准:支持PDF-A,PDF-X


PDF格式


根据PDF官方指南,理解PDF格式可以从四个方面下手——Objects(对象)、File structure(物理文件结构)、Document structure(逻辑文件结构)、Content streams(内容流)。


对象


物理文件结构




  • 整体上分为文件头(Header)、对象集合(Body)、交叉引用表(Xref table)、文件尾(Trailer)四个部分,结构如图。修改过的PDF结构会有部分变化。




  • 未经修改






编辑


img




  • 经修改






编辑


img


文件头



  • 文件头是PDF文件的第一行,格式如下:


%PDF-1.7

复制



  • 这是个固定格式,表示这个PDF文件遵循的PDF规范版本,解析PDF的时候尽量支持高版本的规范,以保证支持大多数工具生成的PDF文件。1.7版本支持1.0-1.7之间的所有版本。


对象集合



  • 这是一个PDF文件最重要的部分,文件中用到的所有对象,包括文本、图象、音乐、视频、字体、超连接、加密信息、文档结构信息等等,都在这里定义。格式如下:


2 0 obj
...
end obj

复制



  • 一个对象的定义包含4个部分:前面的2是对象序号,其用来唯一标记一个对象;0是生成号,按照PDF规范,如果一个PDF文件被修改,那这个数字是累加的,它和对象序号一起标记是原始对象还是修改后的对象,但是实际开发中,很少有用这种方式修改PDF的,都是重新编排对象号;obj和endobj是对象的定义范围,可以抽象的理解为这就是一个左括号和右括号;省略号部分是PDF规定的任意合法对象。

  • 可以通过R关键字来引用任何一个对象,比如要引用上面的对象,可以使用2 0 R,需要主意的是,R关键字不仅可以引用一个已经定义的对象,还可以引用一个并不存在的对象,而且效果就和引用了一个空对象一样。

  • 对象主要有下面几种

  • booleam 用关键字true或false表示,可以是array对象的一个元素,或dictionary对象的一个条目。也可以用在PostScript计算函数里面,做为if或if esle的一个条件。

  • numeric


包括整形和实型,不支持非十进制数字,不支持指数形式的数字。例: 1)整数 123 4567 +111 -2 范围:正2的31次方-1到负的2的31次方 2)实数 12.3 0.8 +6.3 -4.01 -3. +.03 范围:±3.403 ×10的38次方 ±1.175 × 10的-38次方



  • 注意:如果整数超过表示范围将转化成实数,如果实数超过范围就会出错

  • string


由一系列0-255之间的字节组成,一个string总长度不能超过65535.string有以下两种方式:



  • 十六进制字串 由<>包含起来的一个16进制串,两位表示一个字符,不足两位用0补齐。例: \ 表示AA和BB两个字符 \ 表示AA和B0两个字符

  • 直接字串 由()包含起来的一个字串,中间可以使用转义符"/"。例:(abc) 表示abc (a//) 表示a/ 转义符的定义如下:


转义字符含义
/n换行
/r回车
/t水平制表符
/b退格
/f换页(Form feed (FF))
/(左括号
/)右括号
//反斜杠
/ddd八进制形式的字符



  • 对象类别(续)




  • name 由一个前导/和后面一系列字符组成,最大长度为127。和string不同的是,name是不可分割的并且是唯一的,不可分割就是说一个name对象就是一个原子,比如/name,不能说n就是这个name的一个元素;唯一就是指两个相同的name一定代表同一个对象。从pdf1.2开始,除了ascii的0,别的都可以用一个#加两个十六进制的数字表示。例: /name 表示name /name#20is 表示name is /name#200 表示name 0




  • array 用[]包含的一组对象,可以是任何pdf对象(包括array)。虽然pdf只支持一维array,但可以通过array的嵌套实现任意维数的array(但是一个array的元素不能超过8191)。例:[549 3.14 false (Ralph) /SomeName]




  • Dictionary 用"<<"和">>"包含的若干组条目,每组条目都由key和value组成,其中key必须是name对象,并且一个dictionary内的key是唯一的;value可以是任何pdf的合法对象(包括dictionary对象)。例: << /IntegerItem 12 /StringItem (a string) /Subdictionary << /Item1 0.4 /Item2 true /LastItem (not!) /VeryLastItem (OK) >> >>




  • stream 由一个字典和紧跟其后面的一组关键字stream和endstream以及这组关键字中间包含一系列字节组成。内容和string很相似,但有区别:stream可以分几次读取,分开使用不同的部分,string必须作为一个整体一次全部读取使用;string有长度限制,但stream却没有这个限制。一般较大的数据都用stream表示。需要注意的是,stream必须是间接对象,并且stream的字典必须是直接对象。从1.2规范以后,stream可以以外部文件形式存在,这种情况下,解析PDF的时候stream和endstream之间的内容就被忽略掉。例: dictionary stream…data…endstreamstream字典中常用的字段如下: 字段名类型值Length整形(必须)关键字stream和endstream之间的数据长度,endstream之前可能会有一个多余的EOL标记,这个不计算在数据的长度中。Filter名字 或 数组(可选)Stream的编码算法名称(列表)。如果有多个,则数组中的编码算法列表顺序就是数据被编码的顺序。DecodeParms字典 或 数组(可选)一个参数字典或由参数字典组成的一个数组,供Filter使用。如果仅有一个Filter并且这个Filter需要参数,除非这个Filter的所有参数都已经给了默认值,否则的话 DecodeParms必须设置给Filter。如果有多个Filter,并且任意一个Filter使用了非默认的参数, DecodeParms 必须是个数组,每个元素对应一个Filter的参数列表(如果某个Filter无需参数或所有参数都有了默认值,就用空对象代替)。如果没有Filter需要参数,或者所有Filter的参数都有默认值,DecodeParms 就被忽略了。F文件标识(可选)保存stream数据的文件。如果有这个字段, stream和endstream就被忽略,FFilter将会代替Filter, FDecodeParms将代替DecodeParms。Length字段还是表示stream和endstream之间数据的长度,但是通常此刻已经没有数据了,长度是0.FFilter名字 或 字典(可选)和filter类似,针对外部文件。FDecodeParms字典 或 数组(可选)和DecodeParams类似,针对外部文件。




  • Stream的编码算法名称(列表)。如果有多个,则数组中的编码算法列表顺序就是数据被编码的顺序。且需要被编码。编码算法主要如下:






编辑切换为居中


img


编码可视化主要显示为乱码,所以提供了隐藏信息的机会,如下图的steam内容为乱码。




编辑切换为居中


img



  • NULL 用null表示,代表空。如果一个key的值为null,则这个key可以被忽略;如果引用一个不存在的object则等价于引用一个空对象。


交叉引用表



  • 交叉引用表是PDf文件内部一种特殊的文件组织方式,可以很方便的根据对象号随机访问一个对象。其格式如下:


xref
0 1
0000000000 65535 f
4 1
0000000009 00000 n
8 3
0000000074 00000 n
0000000120 00000 n
0000000179 00000 n

复制



  • 其中,xref是开始标志,表示以下为一个交叉引用表的内容;每个交叉引用表又可以分为若干个子段,每个子段的第一行是两个数字,第一个是对象起始号,后面是连续的对象个数,接着每行是这个子段的每个对象的具体信息——每行的前10个数字代表这个这个对象相对文件头的偏移地址,后面的5位数字是生成号(用于标记PDF的更新信息,和对象的生成号作用类似),最后一位f或n表示对象是否被使用(n表示使用,f表示被删除或没有用)。上面这个交叉引用表一共有3个子段,分别有1个,1个,3个对象,第一个子段的对象不可用,其余子段对象可用。


文件尾



  • 通过trailer可以快速的找到交叉引用表的位置,进而可以精确定位每一个对象;还可以通过它本身的字典还可以获取文件的一些全局信息(作者,关键字,标题等),加密信息,等等。具体形式如下:


trailer
<<
key1 value1
key2 value2
key3 value3

>>
startxref
553
%%EOF

复制



  • trailer后面紧跟一个字典,包含若干键-值对。具体含义如下:


值类型值说明
Size整形数字所有间接对象的个数。一个PDF文件,如果被更新过,则会有多个对象集合、交叉引用表、trailer,最后一个trailer的这个字段记录了之前所有对象的个数。这个值必须是直接对象。
Prev整形数字当文件有多个对象集合、交叉引用表和trailer时,才会有这个键,它表示前一个相对于文件头的偏移位置。这个值必须是直接对象。
Root字典Catalog字典(文件的逻辑入口点)的对象号。必须是间接对象。
Encrypt字典文档被保护时,会有这个字段,加密字典的对象号。
Info字典存放文档信息的字典,必须是间接对象。
ID数组文件的ID


  • 上面代码中的startxref:后面的数字表示最后一个交叉引用表相对于文件起始位置的偏移量

  • %%EOF:文件结束符


逻辑文件结构




编辑切换为居中


img


catalog根节点



  • catalog是整个PDF逻辑结构的根节点,这个可以通过trailer的Root字段定位,虽然简单,但是相当重要,因为这里是PDF文件物理结构和逻辑结构的连接点。Catalog字典包含的信息非常多,这里仅就最主要的几个字段做个说明。 字段类型值Typename(必须)只能为Pages 。Parentdictionary(如果不是catalog里面指定的跟节点,则必须有,并且必须是间接对象) 当前节点的直接父节点。Kidsarray(必须)一个间接对象组成的数组,节点可能是page或page tree。Countinteger(必须) page tree里面所包含叶子节点(page 对象)的个数。从以上字段可以看出,Pages最主要的功能就是组织所有的page对象。Page对象描述了一个PDF页面的属性、资源等信息。Page对象是一个字典,它主要包含一下几个重要的属性:

  • Pages字段 这是个必须字段,是PDF里面所有页面的描述集合。Pages字段本身是个字典,它里面又包含了一下几个主要字段:


字段类型
Typename(必须)必须是Page。
Parentdictionary(必须;并且只能是间接对象)当前page节点的直接父节点page tree 。
LastModifieddate(如果存在PieceInfo字段,就必须有,否则可选)记录当前页面被最后一次修改的日期和时间。
Resourcesdictionary(必须; 可继承)记录了当前page用到的所有资源。如果当前页不用任何资源,则这是个空字典。忽略所有字段则表示继承父节点的资源。
MediaBoxrectangle(必须; 可继承)定义了要显示或打印页面的物理媒介的区域(default user space units)
CropBoxrectangle(可选; 可继承)定义了一个可视区域,当前页被显示或打印的时候,它的内容会被这个区域裁剪。默认值就是 MediaBox。
BleedBoxrectangle(可选) 定义了一个区域,当输出设备是个生产环境( production environment)的时候,页面显示的内容会被裁剪。默认值是 CropBox.
Contentsstream or array(可选) 描述页面内容的流。如果这个字段缺省,则页面上什么也不会显示。这个值可以是一个流,也可以是由几个流组成的一个数组。如果是数组,实际效果相当于所有的流是按顺序连在一起的一个流,这就允许PDF生成的时候可以随时插入图片或其他资源。流之间的分割只是词汇上的一个分割,并不是逻辑上或者组织形式的切割。
Rotateinteger(可选; 可继承) 顺时钟旋转的角度数,这个必须是90的整数倍,默认是0。
Thumbstream(可选)定义当前页的缩略图。
Annotsarray(可选) 和当前页面关联的注释。
Metadatastream(可选) 当前页包含的元数据。


  • 一个简单例子:


3 0 obj
<< /Type /Page
/Parent 4 0 R
/MediaBox [ 0 0 612 792 ]
/Resources <</Font<<
/F3 7 0 R /F5 9 0 R /F7 11 0 R
>>
/ProcSet [ /PDF ]
>>
/
Contents 12 0 R
/Thumb 14 0 R
/Annots [ 23 0 R 24 0 R]
>>
endobj

复制



  • Outlines字段 Outline是PDF里面为了方便用户从PDF的一部分跳转到另外一部分而设计的,有时候也叫书签(Bookmark),它是一个树状结构,可以直观的把PDF文件结构展现给用户。用户可以通过鼠标点击来打开或者关闭某个outline项来实现交互,当打开一个outline时,用户可以看到它的所有子节点,关闭一个outline的时候,这个outline的所有子节点会自动隐藏。并且,在点击的时候,阅读器会自动跳转到outline对应的页面位置。Outlines包含以下几个字段: 字段类型值Typename(可选)如果这个字段有值,则必须是Outlines。Firstdictionary(必须;必须是间接对象) 第一个顶层Outline item。Lastdictionary(必须;必须是间接对象)最后一个顶层outline item。Countinteger(必须)outline的所有层次的item的总数。

  • Outline是一个管理outline item的顶层对象,我们看到的,其实是outline item,这个里面才包含了文字、行为、目标区域等等。一个outline item主要有一下几个字段: 字段类型值Titletext string(必须)当前item要显示的标题。Parentdictionary(必须;必须是间接对象) outline层级中,当前item的父对象。如果item本身是顶级item,则父对象就是它本身。Prevdictionary(除了每层的第一个item外,其他item必须有这个字段;必须是间接对象)当前层级中,此item的前一个item。Nextdictionary(除了每层的最后一个item外,其他item必须有这个字段;必须是间接对象)当前层级中,此item的后一个item。Firstdictionary(如果当前item有任何子节点,则这个字段是必须的;必须是间接对象) 当前item的第一个直接子节点。Lastdictionary(如果当前item有任何子节点,则这个字段是必须的;必须是间接对象) 当前item的最后一个直接子节点。Destname,byte string, or array(可选; 如果A字段存在,则这个不能被会略)当前的outline item被激活的时候,要显示的区域。Adictionary(可选; 如果Dest 字段存在,则这个不能被忽略)当前的outline item被激活的时候,要执行的动作。

  • URI字段 URI(uniform resource identifier),定义了文档级别的统一资源标识符和相关链接信息。目录和文档中的链接就是通过这个字段来处理的.

  • Metadata字段 文档的一些附带信息,用xml表示,符合adobe的xmp规范。这个可以方便程序不用解析整个文件就能获得文件的大致信息。

  • 其他 Catalog字典中,常用的字段一般有以下一些:


字段类型
Typename(必须)必须为Catalog。
Versionname(可选)PDF文件所遵循的版本号(如果比文件头指定的版本号高的话)。如果这个字段缺省或者文件头指定的版本比这里的高,那就以文件头为准。一个PDF生成程序可以通过更新这个字段的值来修改PDF文件版本号。
Pagesdictionary(必须并且必须为间接对象)当前文档的页面集合入口。
PageLabelsnumber tree(可选) number tree,定义了页面和页面label对应关系。
Namesdictionary(可选)文档的name字典。
Destsdictionary(可选;必须是间接对象)name和相应目标对应关系字典。
ViewerPreferencesdictionary(可选)阅读参数配置字典,定义了文档被打开时候的行为。如果缺省,则使用阅读器自己的配置。
PageLayoutname(可选) 指定文档被打开的时候页面的布局方式。SinglePageDisplay 单页OneColumnDisplay 单列TwoColumnLeftDisplay 双列,奇数页在左TwoColumnRightDisplay 双列,奇数页在右TwoPageLeft 双页,奇数页在左TwoPageRight 双页,奇数页在右缺省值: SinglePage.
PageModename(可选) 当文档被打开时,指定文档怎么显示Use 目录和缩略图都不显示UseOutlines 显示目录UseThumbs 显示缩略图FullScreen 全屏模式,没有菜单,任何其他窗口UseOC 显示Optional content group 面板UseAttachments显示附件面板缺省值: Use.
Outlinesdictionary(可选;必须为间接对象)文档的目录字典
Threadsarray(可选;必须为间接对象)文章线索字典组成的数组。
OpenActionarray or dictionary(可选) 指定一个区域或一个action,在文档打开的时候显示(区域)或者执行(action)。如果缺省,则会用默认缩放率显示第一页的顶部。
AAdictionary(可选)一个附加的动作字典,在全局范围内定义了响应各种事件的action。
URIdictionary(可选)一个URI字典包含了文档级别的URI action信息。
AcroFormdictionary(可选)文档的交互式form (AcroForm)字典。
Metadatastream(可选;必须是间接对象)文档包含的元数据流。

具体组成


1 Header部分


PDF文件的第一行应是由5个字符“%PDF-”后跟“1.N”的版本号组成的标题,其中N是0到7之间的数字。例如下面的:


%PDF–1.0   %PDF–1.1   %PDF–1.2   %PDF–1.3   %PDF–1.4   %PDF–1.5   %PDF–1.6   %PDF–1.7


从PDF 1.4开始,应使用文档目录字典中的Version 条目(通过文件Trailer部分的Root条目指定版本),而不是标题中指定的版本。


2 Body部分


PDF文件的正文应由表示文件内容的一系列间接对象组成,例如字体、页面和采样图像。从PDF 1.5开始,Body还可以包含对象流,每个对象流包含一系列间接对象。例如下面这样:


1 0 obj
<< /Type /Catalog
  /Outlines 2 0 R
  /Pages 3 0 R
>>
endobj
2 0 obj
<< /Type Outlines
  /Count 0
>>
endobj
3 0 obj
<< /Type /Pages
/Kids [4 0 R]
/Count 1
>>
endobj
4 0 obj
<< /Type /Page
  /Parent 3 0 R
  /MediaBox [0 0 612 792]
  /Contents 5 0 R
  /Resources << /ProcSet 6 0 R >>
>>
endobj
5 0 obj
<< /Length 35 >>
stream
  …Page-marking operators…
endstream
endobj
6 0 obj
[/PDF]
endobj

3 Cross-Reference Table 交叉引用表部分


交叉引用表包含文件中间接对象的信息,以便允许对这些对象进行随机访问,因此无需读取整个文件即可定位任何特定对象。


交叉引用表以xref开始,紧接着是一个空格隔开的两个数字,然后每一行就是一个对象信息:


xref
0 7
0000000000 65535 f
0000000009 00000 n
0000000074 00000 n
0000000120 00000 n
0000000179 00000 n
0000000300 00000 n
0000000384 00000 n

上面第二行中的两个数字“0 7”,0表示下面的对象从0号对象开始,7表示对象的数量,也就是说表示从0到6共7个对象。


每行一个对象信息的格式如下:


nnnnnnnnnn ggggg n eol


  • nnnnnnnnnn 长度10个字节,表示对象在文件的偏移地址;

  • ggggg 长度5个字节,表示对象的生成号;

  • n (in-use)表示对象被引用,如果此值是f (free),表示对象未被引用;

  • eol 就是回车换行


交叉引用表中的第一个编号为0的对象始终是f(free)的,并且生成号为65535;除了编号0的对象外,交叉引用表中的所有对象最初的生成号应为0。删除间接对象时,应将其交叉引用条目标记为“free”,并将其添加到free条目的链表中。下次创建具有该对象编号的对象时,条目的生成号应增加1,最大生成号为65535;当交叉引用条目达到此值时,它将永远不会被重用。


交叉引用表也可以是这样的:


xref
0 1
0000000000 65535 f
3 1
0000025325 00000 n
23 2
0000025518 00002 n
0000025635 00000 n
30 1
0000025777 00000 n

[


4 Trailer部分


PDF阅读器是从PDF的尾部开始解析文件的,通过Trailer部分能够快速找到交叉引用表和某些特殊对象。如下所示:


trailer
<< /Size 7
/Root 1 0 R
>>
startxref
408
%%EOF

文件的最后一行应仅包含文件结束标记%%EOF。关键字startxref下面的数字表示最后一个交叉引用表的xref关键字开头的字节偏移量。trailer和startxref之间是尾部字典,由包含在双尖括号(<<…>>)中的键值对组成。


为什么不容易被修改



  1. 文件结构和编码:PDF文件采用了一种复杂的文件结构和编码方式,这使得在未经授权的情况下修改PDF文件变得非常困难。PDF文件采用二进制格式存储,而不是像文本文件那样以可读的形式存储。这导致无法直接编辑和修改PDF文件,需要使用特定的软件或工具。

  2. 加密和安全特性:PDF文件可以使用密码进行加密和保护,以确保只有授权的用户才能进行修改。加密可以防止未经授权的访问和修改,使得修改PDF文件变得更加困难和复杂。

  3. 文件签名和验证:PDF文件可以使用数字签名进行验证,以确保文件的完整性和可信性。数字签名可以证明文件的来源和真实性,一旦数字签名验证失败,即表明文件已被篡改。

  4. 版本兼容性和规范:PDF格式被国际标准化组织(ISO)制定为ISO 32000标准。这个标准确保了不同版本和软件之间的PDF文件的兼容性,并定义了丰富的功能和规范,包括页面布局、字体嵌入、图形和图像处理等。这些严格的规范使得对PDF文件进行修改变得复杂和具有挑战性。


如何修改pdf


因为pdf的局限性是无法进行修改的,所以我们只能通过将他转换为其他类型的文件进行查看修改,当然,转换的过程不可能是百分百完美的进行转换的。


下面推荐这俩个可以进行转换的网站


PDF转换成Word在线转换器 - 免费 - CleverPDF


Convert PDF to Word for free |

Smallpdf.com

收起阅读 »

人情世故职场社会生存实战篇(四)

人情人情世故职场社会生存实战篇(一)人情人情世故职场社会生存实战篇(二)人情人情世故职场社会生存实战篇(三) 31、问:领导推我得了第一,拿了5000奖金,给领导送多少合适? 答:钱从哪儿来的,还到哪儿去,这叫饮水思源。给他买5000元的华子,他100%要,...
继续阅读 »

人情人情世故职场社会生存实战篇(一)
人情人情世故职场社会生存实战篇(二)
人情人情世故职场社会生存实战篇(三)



31、问:领导推我得了第一,拿了5000奖金,给领导送多少合适?


答:钱从哪儿来的,还到哪儿去,这叫饮水思源。给他买5000元的华子,他100%要,然后你提个非常小非常小的要求就0K。比如请假3天,他说0K,然后就心安理得的收了。你回来了,他会想办法安排你,指点你的。然后他送你一句话,你说:这句话价值连城,一个亿都买不到。领导高兴啊,你看:我5000就卖给你了,我还亏了呢。然后,他会主动给自己加分,你看吧,我还是一个好领导。这样你既满足了领导的物质需求,同时还满足了领导的精神需求。领导觉得你知恩图报,此后有机会便开始提拔你了。


32、问:你说送礼交大哥真的有用吗,有的人收了不办事,会不会搞不好倾家荡产啊?


答:我从来没听说过:谁谁谁送礼倾家荡产的。前辈给我讲过他发家的故事:十几年前他给一个工程的大哥包20万红包,赚了180万,他又拿出100万分给了大哥,大哥说:那边的开发区也归你管了,就那两年时间前辈赚了1000多个,而别人只认为他是命好。


33、问:朋友给我介绍个活,挣了5千多,我说:谢谢啊,下次请你吃饭。但最近很少跟我联系,也不介绍活给我了是怎么回事?


答:如果你挣了五千,给他三千,你觉得你的活还会少吗?进四出六,不要怕吃亏,只要你给他转了,你天天都有活干!自己一人占尽利益,将没有长期合作!


34、问:公司领导生病了,我要不要过去看一下,去了有没有什么用?


答:前年的时候,前辈病了,小李发信息说了一些安慰前辈的话。我呢带了2条烟,去了协和医院,握着前辈的手,说了很多暖心的话。此后,我和前辈无话不谈。尽管小李跟了前辈很多年,但前辈遇到好事儿还是习惯性叫上我,前后几年带事我挣了至少120个朝上。


35、问:去年挣了点钱,想回家乡修个路啥的,你看有没有必要?


答:前年捐了50万在村里修路灯,从那以后,村里的人每天都在找我,这个生病了,那个想买房,还有个没钱交学费的。富贵不归故里。习惯性装穷,习惯性示弱,不信你看大衣哥。


36、问:昨天和朋友吹牛,说一年赚几百万,然后今天给我发信息,我没敢回,是不是找我借钱啊?


答:前年一场饭局,朋友问:一年赚多少?我说400多万。朋友说厉害厉害。第二天,朋友找我借钱,张口就是170万,而且还跪在我家,一直给我磕头。我给了他40万,这40万他100%不会还了。他是送外卖的,一月能赚多少钱我不知道,反正这笔借出去的钱收不回了。


37、问:昨天我对一个大哥说,哥你一年带我赚个几百万就行了,但是大哥直接转移话题是为什么?


答:一个外地的小朋友,非要见个前辈,我拗不过他,便约了他出来。饭局上,这个小朋友一句话就把局面给破坏了。他对前辈说:张叔,我要跟你混,你年赚2000万,我能年赚1000万就行。大家习惯给人家面子,前辈说:兄弟之才,绝对在我之上,来,喝喝喝.....


38、问:我公司干了几年了,不亏钱,但为什么就是做不大?


答:赚小钱,靠的是能力,赚大钱,靠的是关系,靠的是背景。所有的保险公司,都是关系的结果,跟市场运营没有一毛钱关系,赚钱,就是找个大哥,当他的夜壶,这没有什么好丢人的,找不到夜壶才丢人。


39、问:我打算开个实体店,请教一下什么叫会做生意,什么叫情商?


答:前段时间我在一家饭店吃饭,打碎个杯子,老板说:影响您用餐了,没有伤着吧。结账时,我多给了老板100元。好巧不巧,前两天我又去一家小饭店吃饭,打烂一个勺子,老板说:一个勺子100元。此后,这家饭店,我再没去过。


40、问:带我的大哥到我家喝茶,好像是看上我那副画了,正好大哥手里有个项目你说这是个机会吗?


答:前辈很喜欢我的摩托车,我就把摩托车借给他开了几天,然后他问我从哪儿买的,我说:叔你喜欢的话,我就送你了。前辈说:那谢谢了。随后没几天他告诉我:你给你张叔拿2万,我给他打过招呼了,这个绿化工程,由你来做。赚钱不赚钱看你自己。我说:叔,挣不挣钱都是个机会,到时候找你喝茶。



作者:公z号_纵横潜规则
来源:juejin.cn/post/7269588899499704332

收起阅读 »