注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

思考:如何做一名合格的面试官?

背景 关于招聘,在最近一段时间有幸又参与到了面试工作。这次面试也包含一些相对高级一点的岗位,如何招到合适的人成为了最近一段时间一直在思考的一个点。 整体上感觉自己还是没有章法,没有清晰的思路(做不到因人而异),这种情况对自己对他人都是不负责任的。 因此,简单做...
继续阅读 »

背景


关于招聘,在最近一段时间有幸又参与到了面试工作。这次面试也包含一些相对高级一点的岗位,如何招到合适的人成为了最近一段时间一直在思考的一个点。


整体上感觉自己还是没有章法,没有清晰的思路(做不到因人而异),这种情况对自己对他人都是不负责任的。


因此,简单做一些总结思考,边面边想边改进吧。


image.png


招聘者的目标


首先,作为招聘者,都希望能找到一些厉害的人,成本不应该是他要考虑的问题。但现实总是相反。


面试官:这是我这次招聘的需求,要这个...... 那个...... 总之,能力越强越好。


公司:这次招聘成本范围已发给你了,注意查收。


面试官:......


所以,在成本有限的情况下,面试官要做的就是找到那些会发光的人。对面试官来说,招到一个即战力(不亏),招到一个高潜力(赚翻了)。


因此,招聘者的目标都是希望能够招到 能力 > 成本 的人。


image.png


梳理招聘需求


招聘的需求是需要面试官提前梳理好的,面试官作为团队的组建者,一定要提前规划好招聘需求。比如:



  1. 我要找技术强的。(我不懂的他来)

  2. 我要找态度好的。(我说啥都听)

  3. 我要找有责任心的。(不用我说就把活干得很漂亮)

  4. 我要找学习能力强的。(自我提升,啥需求都能接的住)

  5. 最好还能带团队。(这样我就轻松了)

  6. 最后一定要稳定。(这样我就一直轻松了)


哈哈,先开个玩笑。虽然我真的想......


image.png


现实就像上边提到的,招聘者希望的求职者模样。虽然知道不可能,但还是忍不住想要(我控制不住我几己呀!),所以在有限的面试时间内,问了很多方方面面的问题......


面试结束后:我问了那么多才答上来那么几个问题,不行,下一个......


面了几天后:人怎么这么难招呢?


image.png


所以真正的招聘需求应该是下边这样的:



  1. 我要招一个领导者还是执行者:这个一定要想清楚,两者考察维度完全不一样。

  2. 我要技术强的:想好哪方面技术强,不要妄图面面俱到。

  3. 我要找有责任心的,学习能力强的,稳定的:想好怎么提问,如何判断。


如果能够做到上边的三点,相信招进来的人应该都是OK得。PS:先做到不亏。


领导者Or执行者


为什么把这个作为第一点,上边也提到过,两者考察维度完全不一样。一场面试,时间就那么点,所以要有针对性。


先说领导者


如果招聘领导者。试想一下领导者有哪些特点,什么样的人你愿意让他成为领导者。



  1. 业务理解程度深,不仅限于产品规划的业务需求,还要有自己的理解和看法。

  2. 技术能力强,通常一个技术方案便能提现出来,方案好不好,考虑全面不全面。

  3. 抗压能力强,能够承担工作压力,这里不是指加班(当然加班也算),更多的是来自于技术,业务的困难和挑战。


以上三点并不代表全部,仅做参考。那么如何在面试中确认呢?




  1. 业务理解程度主要通过追问细节的方式来确认。在你不了解的情况下,依然能够给你讲明白,这个业务是做什么的,关键核心点是什么,核心点有什么难度和挑战,最后是怎么解决的,解决的是否完美,不完美的原因。如果能够回答的不错那就基本合格了。最后可以再多问一下: 有没有哪些产品提出的需求,你认为不合理或者不适合当前产品现状的?这个问题只要回答的有一定高度,那就完美了。


    ps: 如果面试者认为没有什么难度和挑战,只能证明他自己没有深度参与或主导该业务。再简单的系统,也不可能一点问题都没有,如果真的没有,那么完全没有必要安排团队去专门负责。没有简单的系统,只有简单的思考。


    举个栗子,用户管理(用户CRUD)系统我们一听可能都觉得很简单,早期,用户注册要填一堆的东西,现在都是各种登录渠道,非常的方便。站在现在的角度,对于早期的用户管理来说,如何提升用户注册效率,增加用户量就是一个有难度有挑战的事情。




  2. 技术能力,我简单分为有效技术能力和无效技术能力。无效技术能力代指无用且无聊的八股,当然也不是所有的八股都无用。有效技术能力我理解就是解决问题的能力,而解决问题不在于使用的技术手段与否高明,是否先进,只要贴合业务场景,我都会认为有技术能力。反而那些八股回答的头头是道,解决实际项目问题无一用到的会严重减分。




  3. 抗压能力,项目经验是能反应出来一些信息的:有难度,有挑战的事情你不会交给一个不合适的人来做的,所以如果简历的项目经验中有类似的经验那么就证明别人已经帮你筛选过了。PS:别忘了鉴别一下。




再说执行者


还是试想一下,好的执行者有哪些特质:



  1. 注重细节,考虑问题全面。

  2. 责任心强,不会随便应付了事。

  3. 技术OK,至少基础没有问题。


同样,上述三点仅做参考。




  1. 注重细节,直接体现其实跟方案的完善程度有关,所以问问技术方案的异常情况是如何考虑的。另外,直接体现其实就是BUG比较少,当然这个一般人肯定不会说自己BUG多,所以可以问问,对于如何减少BUG量,有没有心得。(这个问题目前来看基本没啥用,哈哈)




  2. 责任心强。责任心如何体现的我也说不太清楚,思考以后认为加班算一方面,有加薪、晋升算一方面,面试中很难直接体现,只能凭感觉。和第一条注重细节一样,我面试全凭聊完之后的直觉,这种大家都会有,而且一般来说准确率也不错。


    PS: 其实在面试沟通过程中,一问一答,很多事情靠直觉、面向也能猜个七七八八,玄学的东西这里不多说。说一点有科学依据的,人的性格简单分为外向、内向,两者都有其各自的特质,通常来说,内向者的特质更多时候适合于执行者。感兴趣的可以去了解一下两种性格特质,有益于团队管理。




  3. 技术OK。这个不做多说了,一定要多问实际使用的,不用的就不要问了,可能用的适当问一下,实际使用的也可以拔高一下往深了问问。比如:mysql都在用,mysql的八股可以多问几个。JVM这种开发基本不用的,简单问一下得了(我一般是不问的)。






这一篇先到这里把,关于技术强、责任心其实也简单提了一下。关于这两点,后续结合实际情况再更新吧。


作者:FishBones
来源:juejin.cn/post/7219943233799323704
收起阅读 »

在前端领域摸爬滚打7年,我终于掌握了这些沉淀技巧

我做开发多年,常常有人问我「软件开发难学吗?」「前端和后端哪个比较简单?」「培训后是否好找工作呢?」这些问题单拎出来比较棘手,三言两语说不清楚,需要你对开发有一个系统了解,问题才能迎刃而解。 所以,我想和你分享我的学习和工作经历,希望这对于正在准备成为一名程序...
继续阅读 »

我做开发多年,常常有人问我「软件开发难学吗?」「前端和后端哪个比较简单?」「培训后是否好找工作呢?」这些问题单拎出来比较棘手,三言两语说不清楚,需要你对开发有一个系统了解,问题才能迎刃而解。


所以,我想和你分享我的学习和工作经历,希望这对于正在准备成为一名程序员的你有所帮助。


我的经历可能会为新手提供一些有用的建议和思路。


01 萌芽之初,点燃编程学习的梦想


对于一些90后的朋友来说,网游填满了他们的高中时期,甚至是初中。


他们经常因为不走寻常路去打游戏,在回来时被门卫大爷逮个正着。尽管我没有沉迷于游戏,但我仍然被游戏所吸引。


在游戏中,我一直认为只有玩家和 NPC 的存在,但是,玩得越多,你会发现还有一些不寻常的角色,那就是“工作室”。部分“工作室”利用一些技术手段批量、自动地在游戏中完成任务以赚取游戏产出。


虽然这种行为不可取,但是他们使用的技术确实让我感兴趣。


这时候,代码的种子已经悄悄埋藏在我的内心深处,等待发芽。


高中毕业后,卸下学业负担,我开始利用暑期学习了一些脚本精灵、Tc 简单编程和易语言编程,这也是我第一次接触编程基础语法,如条件判断、循环、遍历和条件选择,再加上社区提供的一些识图插件,我就像一个蹩脚的裁缝,东拼西凑,左缝右补,费劲巴拉缝制成一件衣服,却不合身。


虽然实现了自动登录游戏的功能,但很不幸运的是,这样的小功能也还是过不去游戏的自检程序,万物皆有裨益,万事皆可为师,正是这一次编程体验促使了我后来的专业选择。


02 踏上编程学习之路,从安卓到前端,每一步都算数


英语是我成长路上的一块绊脚石,在选择专业时,我想躲开英语,于是选择了同为计算机系下的软件外包服务专业,结果发现,只要是技术,英语的要求都是一样的。


当然,我选择这个专业还有另外一个动机 -- 它开设了Android课程。毕竟,那时我刚拿到一款安卓手机,能在手机上开发自己的App是何等酷炫的体验啊!


那时,有一本厚重的《疯狂 Android 讲义》成了我的启蒙之书,我翻过无数遍,上课、参加编程比赛、实习工作、这本书我一直在用,为我第一份工作立下了汗马功劳。


临近毕业,是先就业还是先培训,许多软件相关专业的毕业生都面临着这样的选择。


所以,你要想明白,你到底需要的是什么?


我选择参加培训是出于两个原因:第一是为了将平时自学的知识整合起来,第二是希望能够认识更多的小伙伴,以便进行技术交流。编程最忌讳的就是闭门造车,不进行沟通交流。


然而,选择参加培训并不是每个人的选择。


如果你有能力自己阅读技术书籍,并且知道如何获取最新的技术信息,那么参加培训完全没有必要。


只有当你需要别人的指点和帮助来梳理技能,或者需要更好的机会来进行技术交流时,参加培训才是一个好的选择。


但是,如果你仅仅因为听说培训完就能很赚钱而选择花钱加入,那么你就要好好思考一下了,周围打水漂的人确实不在少数。


培训结束后,2015 年 12 月 7 号,我入职了第一家公司,担任 Android 开发工程师。


人生有时候做一个决策,一个行动,当时只道是寻常,当它的价值在未来某一刻兑现时,你会感谢当时努力的自己。


如果没有大学时翻过无数遍的《疯狂 Android 讲义》,我不可能找到这份工作。


03 学前端到底在学什么


工作后,我第一次真正进入团队开发模式(我是不会告诉你我当初使用百度云盘定时同步代码的,炸过一次硬盘),由于业务需要一定的前端支持(合同模板),所以在一次小组会议上,组长建议我们要着手学习前端技术(Angular1.x)。


到了17年左右,公司的业务开始由原 Pad 端转移到手机端。我和其他几个新入职的小伙伴经过一上午的 Vuejs2.x 培训后,就开始上手开发了。


也是在这次前端项目开发中,我第一次接触到了闭包导致循环失灵的问题,第一次把一个页面写到 3 千多行(烂,不懂拆分)。


由于这次前端项目开发的经验不足,导致迭代两年后,项目能编译出 200MB 的内容。我只能通过各种查找和大量的 webpack 参数调试,将产物压缩回了20MB 左右。对于我来说,这也是一次很大的成长。


我非常推荐各位小伙伴在工作中多承担,因为开发经验绝非是你熟背八股题得到的,开发经验只能是来自大量的项目实战。


多做练习,多遇困难,多做总结,得到的才是自己的。开发经验决定了你的下一个项目能否走得更顺利。


选择成为前端程序员是一件比较苦的事情,因为这个领域的技术更新非常频繁,如果你不持续学习,那么你就会落后,这也是“前端很累”的一个根本原因。


实际上,现在还有一些人对前端存在偏见,因为他们认为不就一个 JavaScript,能有多难?


但是事实上,很多前端构建技术的底层实现并不是用 JavaScript 语言编写的,而是基于了其它编程语言如 Golang(代:ESBuild)和Rust(代表:SWC)“包装”起来的,利用这些语言的特点来弥补 JavaScript 的不足。


前端学习的基础是 JavaScript,但不仅仅是 JavaScript,如果你认为学习 JavaScript 就是学习前端,那么你可能会走进死胡同。


04 正确的学习编程方式一定是这样的


在学校里,老师一定告诉过你两个正确的学习方式,其中一个是要做笔记,另一个是要能够向同学清晰地讲解。


繁多的技术是不可能靠记忆实现的,因此做笔记和写博客是记录学习过程和分享学习成果的捷径。


现在,我也发现很多在校的同学积极在各大技术社区分享自己的学习经验,这也印证了这条成长途径的正确,同时也激励我们这些已经做了多年程序员的伙伴要更加努力。


不论你是学习新的编程语言还是新的框架,都需要为其配置对应环境,但有很多框架的环境配置其实对于第一次接触的小伙伴来说并不友好,就比如我最初在从Android转前端的时候就因为安装NodeJsNpm这些东西而烦恼,因为当时莫名其妙就提示你Python2的模块找不到了,要不就是安装依赖超时了,在环境搭建问题上花费太长时间真的不划算。


为了避免环境搭建影响学习进度,我们可以使用一些在线的 IDE 环境,例如 CodePen、CodeSandBox、Stackblitz、JSRun 等。


但是,它们在依赖安装、操作习惯和响应速度上仍然有一些上手难度。


我最近一段时间一直在使用 1024Code  社区提供的在线 IDE,它提供了很多热门语言和框架的代码空间模板,免配置环境,即开即用随时学习新技术。


它支持多人开发和在线分享,无论是和朋友一起开发项目还是找大佬请教问题,都非常轻松。


05 学习编程,高效沉淀需要技巧


我发现之前写博客时做的案例很难沉淀下来。往往只是写完一遍,很少再打开运行。


但是在 1024Code 中,可以以卡片的形式记录每一个案例,也可以将一系列案例放到一个集合中归类。


此外,1024Code 还支持在个人主页中渲染 Markdown,为小伙伴打造炫酷的个人主页提供了便利。


最令人赞叹的是,1024Code 紧跟最近比较火的 ChatGPT,将其接入到了 IDE 中,让你在编码的同时可以更快速地查找解决方案。下面我给大家简单地展示一下:


在社区主页中,案例以卡片的形式展示。你可以点击你感兴趣的案例,一键运行。边浏览源码,边跟着作者提供的 README 进行学习。


如果你想在此基础上练习或二次开发,还可以 fork 一份到自己的工作空间。如果你发现作者的代码有不合理的地方,还可以在评论区大胆地给他留言,大家可以共同成长。



1024Code 提供了众多空间模板,涵盖了多种编程语言和框架,例如针对数据统计和 AI 模型训练的 Python,以及让许多程序员感到头疼的 C++。


此外,它还支持其它主流的热门编程语言和框架。



Markdown 是编程小伙伴们最常用的笔记格式之一,因此无需专门学习其语法。只需要多看几遍,就可以自然而然地掌握。


此外,你还可以参考社区中其他小伙伴的主页,来打造自己独特的个人主页。



接下来,我要展示一段时间以来我制作的合集。


最初,这个合集是为了帮助那些不熟悉滴滴 LF 框架如何使用 Vue3+TS 编写的小伙伴们而制作的。


我还将合集地址提交到了 LF 仓库,希望能够帮助那些正在转向 Vue3+TS 的小伙伴们。



最重磅的就是 ChatGPT 了。


在使用 1024Code 的 IDE 进行开发过程中,如果遇到问题,你可以快速打开 ChatGPT 来协助你查找答案,而不需要离开当前页面。


ChatGPT 支持上下文连续问答模式,虽然它不能解决你所有的问题,甚至会给出错误的答案,但对于一些常规类编程问题或正在做毕业设计的小伙伴们,它还是能够显著提升效率的。



总结


最后,我再为你做一些总结、建议和对未来的期待:




  1. 我建议你要有很强的动力来学习编程,因为坚持并不是易事;




  2. 我建议你坚守自己慎重选择的专业,因为不忘初心方得始终;




  3. 我建议你在面对技术培训时要清醒认知,因为明确目标的选择才适合自己;




  4. 我建议你在工作中抓住一切学习的机会,因为努力的人很多,只有不断学习才能跟上技术的发展;




  5. 我建议你在编程学习时要善用工具、做好笔记、写博客,不断沉淀自己的知识和经验;




最后的最后,愿我们所有付出都将是沉淀,所有美好终会如期而至。


作者:小鑫同学
来源:juejin.cn/post/7209648356530929721
收起阅读 »

python | 写一个记仇本

最近背着老婆买了一个switch卡带,这货居然给丈母娘讲,害得我被丈母娘说还小了,不买奶粉买游戏,太气人了,我连夜用python写了个《记仇本》,画个圈圈把她记下来。 本片文章,主要关注于python代码,而html和css将暂时被忽略。 记仇本展示 如题所...
继续阅读 »

最近背着老婆买了一个switch卡带,这货居然给丈母娘讲,害得我被丈母娘说还小了,不买奶粉买游戏,太气人了,我连夜用python写了个《记仇本》,画个圈圈把她记下来。


本片文章,主要关注于python代码,而htmlcss将暂时被忽略。



记仇本展示


如题所述,项目已经写好了,是基于local_storage存储在本地的项目,地址如下:



该项目运行时是基于brython, 你可能想问,为什么不使用原生python来编写网页呢,这个有个误区是,网页是由html代码编写的,而原生python想要操作DOM非常难,所以brython正是为这个来操作的。


初始打开页面,因为没有数据展示,所以只有一个增加按钮。



当我们点击【画个圈圈记下来】按钮后,会刷新为新增页面,例如:



此时,我们只需要输入信息,比如 记老婆的仇,缘由为 买switch游戏透露给丈母娘,还得被骂。



此时点击记仇,就可以存储到页面上了。



此时若点击已原谅,则可以删除该记录。


brython 之 local_storage


你可能细心发现了,哎,关掉了浏览器,下次打开,怎么还会有记录在上面呢,这是因为用了local_storage,那么,什么是local_storage呢?


哎,我们使用的是brython中的local_storage但是,它可不是python定义的哦,而是HTML 5提供的存储数据的API之一,可以在浏览器中保持键值对数据块。


现在来展示使用一下brython存储和删除的操作。


导入库:


from browser.local_storage import storage

存储数据,例如键值信息juejinName存储为pdudo


storage[juejinName] = "pdudo"

查询的话,直接使用storage[变量]就好,若为空,则返回


v = storage[juejinName]

循环所有的key,需要引入window库,再使用for...in来完成


from browser import window
for key in window.localStorage:
print(key)

也可以直接使用for...in来遍历storage


而删除数据呢?只需要像删除字典一下


del storage[juejinName]

storage是不是操作起来和字典非常类似呢?减少了开发者的学习成本。


上述案例,已经放到下面的链接中了:



制作项目


有了上述前置条件后,我们再看该项目,便可以总结为 针对localStorage的增删查,首先当页面加载出来的时候,我们需要先遍历一下localstorage数据,从而映射为一个table,例如:


  for key in window.localStorage:
tr = html.TR()
datas = json.loads(storage[key])

delBtn = html.BUTTON("已原谅")
delBtn.dataset["id"] = datas["id"]
delBtn.className = "confirm-btn"
delBtn.bind("click",delGrudges)

td = html.TD(delBtn+" "+time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(int(datas["id"]))))
tr <= td

for tdVal in datas["whos"],datas["Text"]:
td = html.TD(tdVal)
tr <= td

tb <= tr

userWindows <= tb

上述代码是遍历localStorage,而后在定义删除按钮,等将其他值组合完毕后,全部加载进table中,而后再页面上显示。


而添加数据呢?


def saveGrudges(ev):
getWhoVal = document["whos"].value
getTextVal = document["textArea"].value

if getWhoVal == "" or getTextVal == "":
return

document["saveBtn"].unbind("click")


ids = int(time.time())
datas = {
"id": ids,
"whos": getWhoVal,
"Text": getTextVal
}

storage[str(ids)] = json.dumps(datas)

上述代码,显示获取inputtextarea框中的值,再判断是否用户没有输入,我们将数据组装为一个字典,而后转换为字符串,再存入localstage中。


还有其他操作,这个可以直接看代码说明,brython很简单的。


总结


这篇文章,是善用localStorage来作为键值对存储,以此来保证打开和关闭浏览器,不会对数据产生影响,整个项目就是围绕这个localStorage增删改查来操作的。


作者:真的不能告诉你我的名字
来源:juejin.cn/post/7222229682027462693
收起阅读 »

为什么面试聊得很好,转头却挂了?

本文首发自公粽hao「林行学长」,欢迎来撩,免费领取20个求职工具资源包。 了解校招、分享校招知识的学长来了! 四月中旬了,大家面试了几场? 大家有没有这样的感受:面试的时候和面试官聊得火热朝天,气味相投。 面完觉得自己稳了,已经在畅想入职事宜了,结果一封感谢...
继续阅读 »

本文首发自公粽hao「林行学长」,欢迎来撩,免费领取20个求职工具资源包。


了解校招、分享校招知识的学长来了!


四月中旬了,大家面试了几场?


大家有没有这样的感受:面试的时候和面试官聊得火热朝天,气味相投。


面完觉得自己稳了,已经在畅想入职事宜了,结果一封感谢信让人瞬间清醒。


image.png


不少同学应该有这样的经历。


学长也曾经有过:面试两小时,自觉面试问题回答得不错,但是面试官只说:你回去等消息吧。


经历过面试的同学应该懂”回去等消息“这句话的杀伤力有多大。


在此也想先和那些面试多次但还是不通过的朋友说:千万别气馁!


找工作看能力,有时候也看运气,面试没有通过,这并不说明你不优秀。


所有,面试未通过,这其中的问题到底出在哪呢?


01 缺乏相关经验或技能


如果应聘者没有足够的经验或技能来完成职位要求,或者面试的时候没有展现自己的优势,那么失败很常见。


而面试官看重也许就是那些未展现的经验或技能,考察的是与岗位的匹配程度。


02 没有准备充分


每年学长遇到一些同学因为时间安排不当,没有任何了解就开投简历。


而被春招和毕业论文一起砸晕的同学更是昏头转向。


如果没有花足够的时间和精力来了解公司和职位,并准备回答常见的面试问题,那么可能表现不佳。


03 与招聘人员沟通不畅


在面试过程中,面试官真的非常看重沟通效果!


如果应聘者无法清晰地表达自己的想法,或者不能理解面试官的问题,那么可能会被认为不适合该职位。


04 缺乏信心或过度紧张


学长也很理解应届生的局促感,以及面对面试官的紧张。


image.png


但是如果面试场上感到非常紧张或缺乏自信,那么可能表现得不自然或不真诚。


好像,面试的时候需要表现得自信、大方,才能入面试官的眼。


05 不符合公司文化或价值观


企业文化,也成为考察面试者的一个利器。


如果应聘者的个人品格、行为或态度与公司文化或价值观不符,那么可能无法通过面试。


比如一个一心躺平的候选人,面对高压氛围,只会 Say goodbye。


image.png


06 其他候选人更加匹配


如果公司有其他候选人比应聘者更加匹配该职位,那么应聘者可能无法通过面试。


一个岗位,你会面对强劲的对手。


同样学历背景,但有工作经验比你丰富的;


工作经验都 OK,但有其他学历背景比你合适,或稳定性比你高的面试者。


经常有同学发帖吐槽面试经历:一场群面,只有 Ta 是普通本科生,其余人均 Top 学校研究生学历。


面试不容易,祝大家都能斩获心仪的 Offer!


最后,欢迎来关注公粽hao「林行学长」,聊聊职场,聊聊工作,聊聊生活,林行学长和你一起成长~


作者:林行学长
来源:juejin.cn/post/7221131892426096701
收起阅读 »

使用fabric从零开始打造互动白板(一)

web
最近由于公司业务需要,原本使用第三方的互动白板功能,准备自行实现。一方面为了节省开支,另一方面自行实现定制化也更强一些,能够和现有业务更好的结合,用以满足第三方互动白板无法实现的一些功能。 一、功能整理 既然需求明确了,于是就开始着手整理白板所需的功能。由于...
继续阅读 »

最近由于公司业务需要,原本使用第三方的互动白板功能,准备自行实现。一方面为了节省开支,另一方面自行实现定制化也更强一些,能够和现有业务更好的结合,用以满足第三方互动白板无法实现的一些功能。



一、功能整理


既然需求明确了,于是就开始着手整理白板所需的功能。由于我们的直播课都是大班课,只需要讲师在白板上操作,用户端只用来展示,也就不需要太多复杂的功能,结合几个第三方互动白板,归纳整理出了如下几个需要实现的功能点:



  • 自由画笔

  • 文字书写

  • 橡皮擦

  • 画三角、圆形、矩形

  • 画直线和箭头

  • 清空画布

  • 撤销重做

  • 画布缩放

  • 插入PPT图片及切换控制


二、技术选择


观察了现有的互动白板,都是在Canvas进行操作,为了节约开发时间于是找到了fabric这个库,这个库本身就实现了不少功能,可以大大的减少开发时间。


结合我熟悉的技术栈,最终选定了使用Vite+Vue3+TypeScript进行demo版本的构建。


相关代码放在github上,链接地址:使用vite+typescript+fabric创建的互动白板项目


三、页面结构


参考其他白板的布局进行了页面结构的搭建。白板使用一个容器进行包裹,左侧是工具区域,包括画笔、橡皮擦、画线、清空画布等各种绘制工具;左下角是撤销、重做和画布缩放控制区域;右上角是插入PPT文件控制区域;右下角是PPT控制区域。最后提供了一个容器进行的白板预览。


效果图如下:


demo.png


页面结构代码如下:


<template>
<div>
<div class="canvas-wrap">
<div class="tool-box-out">
<ToolBox></ToolBox>
</div>
<div class="redo-undo-box">
<RedoUndo></RedoUndo>
</div>
<div class="zoom-controller-box">
<ZoomController></ZoomController>
</div>
<div class="room-controller-box" v-show="!isPreviewShow">
<div class="page-controller-mid-box">
<div className="page-preview-cell" @click="insertPPT">
<img style="width: 28px" :src="folder" alt="文件"/>
</div>
</div>
</div>
<div class="page-controller-box" v-show="isShowPPTControl">
<div className="page-controller-mid-box">
<PageController></PageController>
<div className="page-preview-cell" @click="handlePreviewState(true)">
<img :src="pages" alt="PPT预览"/>
</div>
</div>
</div>
<div class="preview-controller-box" v-show="isShowPPTControl&&isPreviewShow">
<PreviewController @handlePreviewState="handlePreviewState"></PreviewController>
</div>
<canvas id="canvas" width="800" height="450"></canvas>
</div>
<div class="canvas-wrap">
<canvas id="canvas2" width="800" height="450"></canvas>
</div>
</div>
</template>

四、初始化白板


为了方便后续使用,这里对fabric进行封装,后续拓展也能更加灵活。相关代码如下:


import { fabric } from "fabric";

class FabricCanvas {
constructor(canvasId: string) {

// 初始化画布,默认可绘制
this.canvas = new fabric.Canvas(canvasId, {
isDrawingMode: true,
selection: false,
includeDefaultValues: false, // 转换成json对象,不包含默认值
});
}
}

使用示例:


const canvas = new FabricCanvas('canvas');

五、工具栏相关功能实现


页面框架搭建完成之后,就开始各种功能的开发。这里将fabric封装成一个类,所有需要实现的方法在类中实现,方便后续灵活调用。


选择


选择功能实现比较简单,只需要关闭绘制模式,然后将画布设置可选中。相关代码如下:


this.canvas.isDrawingMode = false;
this.canvas.selection = true;
// 设置鼠标光标
this.canvas.defaultCursor = 'auto';

自由画笔


fabric提供了各种丰富的笔刷,实现自由绘制这里调用了PencilBrush类,并将isDrawingMode设置为true即可。相关代码如下:


  public drawFreeDraw() {
this.canvas.freeDrawingBrush = new fabric.PencilBrush(this.canvas);
this.canvas.freeDrawingBrush.color = '#ff0000'
this.canvas.freeDrawingBrush.width = 5
this.canvas.freeDrawingCursor = 'default'
this.canvas.isDrawingMode = true;
}

使用示例:


const canvas = new FabricCanvas('canvas');
// 自由画笔
canvas.drawFreeDraw();

文字书写


文字输入使用fabric提供的IText方法,实现文字编辑和修改,并且让输入框自动获取焦点,方便输入。相关代码如下:


  public drawText(text: string, options?: ITextOptions): void {
const textObj = new fabric.IText(text, {
editingBorderColor: '#ff0000',
padding: 5,
...options
});
this.canvas.add(textObj);
this.canvas.defaultCursor = 'text'
this.currentShape = textObj;
// 文本打开编辑模式
textObj.enterEditing();
// 文本编辑框获取焦点
textObj.hiddenTextarea.focus()
this.setActiveObject(textObj);
}

使用示例:


const canvas = new FabricCanvas('canvas');
// 绘制文本
canvas.drawText('Hello World!', { left: 50, top: 250, fontSize: 24, fill: 'red' })

橡皮擦


fabric内置了EraserBrush用来实现橡皮擦功能,不过默认构建排除了该功能,避免库文件过大。如需使用,需要进入node_modules/fabric目录执行下面的命令重新构建:


node build.js modules=ALL exclude=gestures,accessors requirejs minifier=uglifyjs

构建完成之后就可以使用EraserBrush来实现橡皮擦功能了,相关代码如下:


public eraser(options?: any): void {
this.canvas.freeDrawingBrush = new fabric.EraserBrush(this.canvas, options);
this.canvas.freeDrawingBrush.width = 10
this.canvas.freeDrawingCursor = 'default'
this.canvas.isDrawingMode = true;
}

使用示例:


const canvas = new FabricCanvas('canvas');
// 橡皮擦
canvas.erase({ width: 10 });

画三角、圆形、矩形


画三角形、圆形、矩形方法相似,直接调用fabric封装的对应方法即可。


这里以绘制矩形为例,相关代码实现如下:


public drawRect(options: IRectOptions): void {
const rect = new fabric.Rect({ ...this.options, ...options });
this.canvas.add(rect);
this.currentShape = rect;
this.canvas.defaultCursor = 'crosshair'
}

使用示例:


const canvas = new FabricCanvas('canvas');
// 绘制矩形
canvas.drawRect({ left: 50, top: 150, width: 100, height: 50, fill: 'green', stroke: 'black' });

画直线和箭头


画直线功能fabric直接内置了,调用对应的方法即可,这里重点讲画箭头。画箭头的本质是在直线的一端加上一个三角形,根据起始点使用三角函数计算好三角形的方向,这样就组合成一个箭头了。这里搬运网友的画箭头方法,直接对画直线功能进行拓展,并封装成fabric中的功能模块,方便后续调用。相关代码如下:


import { fabric } from 'fabric';

fabric.Arrow = fabric.util.createClass(fabric.Line, {
type: 'arrow',
superType: 'drawing',
initialize(points: number[], options: any) {
if (!points) {
const { x1, x2, y1, y2 } = options;
points = [x1, y1, x2, y2];
}
options = options || {};
this.callSuper('initialize', points, options);
},
_render(ctx: any) {
this.callSuper('_render', ctx);
ctx.save();
const xDiff = this.x2 - this.x1;
const yDiff = this.y2 - this.y1;
const angle = Math.atan2(yDiff, xDiff);
ctx.translate((this.x2 - this.x1) / 2, (this.y2 - this.y1) / 2);
ctx.rotate(angle);
ctx.beginPath();
// Move 5px in front of line to start the arrow so it does not have the square line end showing in front (0,0)
ctx.moveTo(5, 0);
ctx.lineTo(-5, 5);
ctx.lineTo(-5, -5);
ctx.closePath();
ctx.fillStyle = this.stroke;
ctx.fill();
ctx.restore();
},
});

fabric.Arrow.fromObject = (options: any, callback: any) => {
const { x1, x2, y1, y2 } = options;
return callback(new fabric.Arrow([x1, y1, x2, y2], options));
};

export default fabric.Arrow;

封装好的代码,直接导入调用即可。相关代码如下:


import Arrow from "./objects/Arrow";
// 绘制箭头
public drawArrow(x1: number, y1: number, x2: number, y2: number, options?: ILineOptions) {
const arrow = new Arrow([x1, y1, x2, y2], { ...this.options, ...options });
this.canvas.add(arrow);
this.currentShape = arrow;
this.canvas.defaultCursor = 'crosshair'
}

使用示例:


const canvas = new FabricCanvas('canvas');
// 绘制矩形
canvas.drawArrow(10, 50, 100, 50, { stroke: 'blue', strokeWidth: 2 })

通过鼠标绘制图形


实际使用白板过程中,上面这些图形、线条的绘制都是通过鼠标拖动进行,这样更加灵活一些。


通过鼠标绘制图形,需要对鼠标的mouse:downmouse:movemouse:up事件进行监听,相关代码如下:


// 监听鼠标事件
this.canvas.on("mouse:down", this.onMouseDown.bind(this));
this.canvas.on("mouse:move", this.onMouseMove.bind(this));
this.canvas.on("mouse:up", this.onMouseUp.bind(this));

这里以绘制矩形为例,实现通过通过鼠标绘制一个矩形框。



  1. 当鼠标按下时,在鼠标按下的地方绘制一个宽高为0的矩形。相关代码如下:


// 是否处于绘制状态
private isDrawing = false;
// 鼠标起点坐标x
private startX = 0;
// 鼠标起点多表y
private startY = 0;

// 鼠标按下事件处理函数
private onMouseDown(event: IEvent) {
// 如果当前有活动的元素则不进行后续绘制
const activeObject = this.canvas.getActiveObject();
if (!event.pointer || activeObject) return;

// 切换成绘制状态
this.isDrawing = true;
// 记录当前坐标点
const { x, y } = event.pointer;
this.startX = x;
this.startY = y;

// 在当前坐标绘制一个矩形
this.drawRect({
left: x,
top: y,
width: 0,
height: 0,
});
}


  1. 在鼠标移动的过程中,动态的修改矩形的宽高,并实时渲染。相关代码如下:


// 鼠标移动事件处理函数
private onMouseMove(event: IEvent) {
if (!this.isDrawing || !event.pointer || !this.currentShape) return;

// 计算宽高
const { x, y } = event.pointer;
const width = x - this.startX;
const height = y - this.startY;

// 设置宽高
this.currentShape.set({
width,
height,
});

// 更新画布
this.canvas.renderAll();
}


  1. 当鼠标抬起后,改变绘制状态。相关代码如下:


// 鼠标抬起事件处理函数
private onMouseUp() {
this.isDrawing = false;
this.currentShape = null;
}

如果想要更加灵活的在各种图形和线条中自由的进行切换,并通过鼠标绘制,在提供的demo中也进行了对应的封装。
相关代码请在github中进行查看,对fabric的各种功能封装


清空画布


清空画布直接调用画布的清除方法即可,相关代码如下:


// 清空画布
public clearCanvas() {
this.canvas.clear();
}

不过该方法会清除画布上包含背景的所有内容,如果不想画布背景也被清除,可以遍历画布上的所有对象进行移除。相关代码如下:


// 移除所有对象
public removeAllObject() {
this.canvas.getObjects().forEach((obj) => {
this.canvas.remove(obj);
});
}

六、工具栏布局
将工具栏封装成ToolBox组件,并在组件中实现各种工具的切换。


工具栏显示效果
组件布局代码如下:


<template>
<div class="tool-mid-box-left">
<div class="tool-box-cell-box-left" v-for="item in tools" :key="item.shapeType">
<div class="tool-box-cell"
@click="clickAppliance(item.shapeType)">
<img :src="item.shapeType === currentShapType ? item.iconActive : item.icon" :alt="item.name"/>
</div>
</div>
<div class="tool-box-cell-box-left">
<div class="tool-box-cell"
@click="clickClear">
<img :src="clear" alt="清屏"/>
</div>
</div>
</div>
</template>

相关功能事件实现的代码如下:


const currentShapType = ref<string>("pencil")

// 设置当前工具
function clickAppliance(type: DrawingTool) {
currentShapType.value = type;
canvas?.value.setDrawingTool(type)
}

// 清屏事件处理
function clickClear() {
canvas?.value.clearCanvas()
}

设置当前绘制工具


// 设置绘图工具
public setDrawingTool(tool: DrawingTool) {
if(this.drawingTool === tool) return;
this.canvas.isDrawingMode = false;
this.canvas.selection = false;

this.drawingTool = tool;
if (tool === "pencil") {
this.drawFreeDraw();
} else if (tool === "eraser") {
this.eraser();
} else if (tool === "select") {
this.canvas.selection = true;
this.canvas.defaultCursor = 'auto'
}
}

其他功能说明


为避免文章太长,撤销重做、画布缩放、插入PPT图片及切换控制等功能的实现在后续文章中介绍。


如果等不及,可以直接在github上查看相关代码实现:使用vite+typescript+fabric创建的互动白板项目


六、参考资料



作者:江阳小道
来源:juejin.cn/post/7221348552513077305
收起阅读 »

你到底值多少钱?2023打工人薪酬指南

大家好,我是王有志,欢迎和我聊技术,聊漂泊在外的生活。 作为打工人,你最关心什么?技能,成长,发展还是薪酬? 刚毕业时,我为了赢得面试官的好感,说了很多违心话,如:“工资不要紧,主要是想学习”,又或者是“我对贵司的这块技术非常感兴趣”。 现在想想,呸!恶心,...
继续阅读 »

大家好,我是王有志,欢迎和我聊技术,聊漂泊在外的生活。



作为打工人,你最关心什么?技能,成长,发展还是薪酬


刚毕业时,我为了赢得面试官的好感,说了很多违心话,如:“工资不要紧,主要是想学习”,又或者是“我对贵司的这块技术非常感兴趣”。


现在想想,呸!恶心,哪怕是花钱培训呢,也不要再傻乎乎的说出“为块术”这种违心的话了。


图1:为块术.png


那时候年轻,不知道起薪高的好处,现在被各种压涨幅,各种倒挂,干最累的活,拿最少的钱,吃最硬的大饼。


2023年,后疫情时代的“元年”,我想明白了,我背上行囊,背井离乡来北漂,就为了3件事:挣钱,挣钱,还是TM的挣钱


既然要挣钱,首先要明确自己的价值。想必大家也对自己值多少钱感兴趣吧?可苦于薪酬保密协议,很难和身边人对比,难以了解自己的价值。没关系,我最近读了几份有趣的报告:



  • 《看看你该赚多少?2023薪资指南(亚太版)》连智领域

  • 《2023年市场展望与薪酬报告》任仕达

  • 《2023⾏业薪酬⽩⽪书》嘉驰国际


今天我就通过这几份报告和大家聊聊,在职场中“我”到底价值几何,“我”拿到怎样的薪资才没有辜负我的才华。


Tips



  • 本文重点分享信息技术岗位,互联网行业,金融行业,软件行业的数据,其余数据可自行阅报告,文末附下载方式;

  • 个人价值不单单由工作年限决定,更多的是与工作年限所匹配的能力。


应届生薪资指南


如果你经常逛各种论坛,可能会看到“今年春/秋招的白菜价是25K”,“XXXX给我开了20K的侮辱性Offer”这类言论。那么20K真的是侮辱性Offer吗?低于白菜价的Offer到底要不要接?


来看嘉驰国际统计到的信息技术行业应届生薪资数据:


图2:2023信息技术行业应届生平均薪资.png


数据似乎与看到的言论相反,一线城市中,本科毕业生薪资中位数是8.8K,只有25%的毕业生拿到了超过10K的薪资。热门城市(北京,上海,广州,深圳和杭州)中也只有北京,上海和深圳的应届生薪资中位数超过了8K


那么网上流传的“白菜价”是怎么回事?其实不难理解,“白菜价”是少数顶尖院校(115所211院校,含39所985院校)的学生拿到顶尖互联网大厂的平均薪资水平,而大部分应届毕业生是很难拿到这个薪资的。


我国拥有2759所普通高等院校,本科1270所,高职(专科)1489所,顶尖院校(115所211院校,含39所985院校)仅占本科院校的9%,普通高等院校的4.1%。


所以对于大部分的普通人院校的毕业生来说,没有所谓的“白菜价”。根据自身的硬性条件合理决定自身的价值范围,不要被HR忽悠,也不要有太过离谱的期望


插句题外话,我16年毕业于某双非院校,第一份工作8.5K,但我们年级的“神”,第一份工作18K。讲这个事情有两层意思:



  • 某些大佬真的可以挣脱本科院校的枷锁

  • 身边的个例并不能反应真实的平均情况


Tips



互联网的天花板


了解完应届生的薪资后,你一定会很想了解未来自己的天花板在哪。注意,标题是互联网的天花板,并非某个职业,也并非每个人都能达到天花板。


先来看互联网行业的年固定收入成长曲线:


图3:互联网年固定收入天花板.png


接着是互联网行业年总收入的成长曲线:


图4:互联网年总收入天花板.png


以我个人观察到的情况,互联网行业中,主管/高级通常对应阿里巴巴的技术职级序列的P6和P6+,经理/资深则对应的是P7,而总监/专家则是P8及以上的职级。


一个很惨淡的事实,对于大部分人来说,P7是通过勤奋可以达到的天花板


如果不是太差,当你达到P7时你的年固定收入会来到50W上下,总收入(奖金和少量股票)会在60W到70W徘徊;而其中的佼佼者,年固定收入会来到70W,总收入触摸到7位数的边界;佼佼者中的一部分会跨过P7这道坎来到P8,普通的P8年薪会在60W上下,总收入(奖金和股票)接近100W,而顶尖的P8薪资会超过100W,总收入更是超过150W。


如果说P7是普通人勤奋的天花板,那普通人想要晋升为P8就需要额外的借助机遇和人脉才有可能达成


Tips:正文部分只展示互联网行业的年固定收入成长曲线和年总收入成长曲线,附录部分提供其他行业的收入曲线。


上海地区研发岗位薪酬


任仕达在《2023年市场展望与薪酬报告》中,给出了信息技术行业中各个技术岗位的薪酬数据,但只有上海地区的数据较为全面,我们重点关注几个“奋战”在一线的技术岗位的薪酬数据:


图5:上海职位薪酬.png
可以看到,对于研发工程师来说,薪资的天花板都非常接近,最突出的是移动开发工程师,稍微落后的是Python开发工程师


当然,技术岗位的天花板远不是职业的终点,技术岗位之后是偏向管理的岗位,例如项目管理和技术管理等。


在大部分互联网公司中,产品经理也是一线岗位,但无论平均薪酬还是薪酬上限,都高于研发岗位(AI类除外)。


结语


了解市场上的薪酬行情,有助于你在求职市场上擦亮自己的双眼,一来可以防止HR恶意压薪资,二来可以清楚自身的定位。


因文章篇幅限制,仅展示部分数据,点击王有志,回复【薪酬报告】即可下载报告。




好了,今天就到这里了,Bye~~


附录


各行业应届生薪资数据


附1:2023各行业应届生平均薪资.png


电子商务年收入成长曲线


年固定收入成长曲线:


附2:电子商务年固定收入天花板.png


年总收入成长曲线:


附3:电子商务年总收入天花板.png


企业软件年收入成长曲线


年固定收入成长曲线:


图4:互联网年总收入天花板.png


年总收入成长曲线:


附5:企业软件年总收入天花板.png


作者:王有志
来源:juejin.cn/post/7217601930917838885
收起阅读 »

new 一个对象时,js 做了什么?

web
前言 在 JavaScript 中, 通过 new 操作符可以创建一个实例对象,而这个实例对象继承了原对象的属性和方法。因此,new 存在的意义在于它实现了 JavaScript 中的继承,而不仅仅是实例化了一个对象。 new 的作用 我们先通过例子来了解 n...
继续阅读 »

前言


在 JavaScript 中, 通过 new 操作符可以创建一个实例对象,而这个实例对象继承了原对象的属性和方法。因此,new 存在的意义在于它实现了 JavaScript 中的继承,而不仅仅是实例化了一个对象。


new 的作用


我们先通过例子来了解 new 的作用,示例如下:


function Person(name) {
this.name = name
}
Person.prototype.sayName = function () {
console.log(this.name)
}
const t = new Person('小明')
console.log(t.name) // 小明
t.sayName() // 小明

从上面的例子中我们可以得出以下结论:





  • new 通过构造函数 Person 创建出来的实例对象可以访问到构造函数中的属性。




  • new 通过构造函数 Person 创建出来的实例可以访问到构造函数原型链中的属性,也就是说通过 new 操作符,实例与构造函数通过原型链连接了起来。





构造函数 Person 并没有显式 return 任何值(默认返回 undefined),如果我们让它返回值会发生什么事情呢?


function Person(name) {
this.name = name
return 1
}
const t = new Person('小明')
console.log(t.name) // 小明

在上述例子中的构造函数中返回了 1,但是这个返回值并没有任何的用处,得到的结果还是和之前的例子完全一样。我们又可以得出一个结论:



构造函数如果返回原始值,那么这个返回值毫无意义。



我们再来试试返回对象会发生什么:


function Person(name) {
this.name = name
return {age: 23}
}
const t = new Person('小明')
console.log(t) // { age: 23 }
console.log(t.name) // undefined

通过上面这个例子我们可以发现,当返回值为对象时,这个返回值就会被正常的返回出去。我们再次得出了一个结论:



构造函数如果返回值为对象,那么这个返回值会被正常使用。



总结:这两个例子告诉我们,构造函数尽量不要返回值。因为返回原始值不会生效,返回对象会导致 new 操作符没有作用。


实现 new


首先我们要清楚,在使用 new 操作符时,js 做了哪些事情:



  1. js 在内部创建了一个对象

  2. 这个对象可以访问到构造函数原型上的属性,所以需要将对象与构造函数连接起来

  3. 构造函数内部的this被赋值为这个新对象(即this指向新对象)

  4. 返回原始值需要忽略,返回对象需要正常处理


知道了步骤后,我们就可以着手来实现 new 的功能了:


function _new(fn, ...args) {
const newObj = Object.create(fn.prototype);
const value = fn.apply(newObj, args);
return value instanceof Object ? value : newObj;
}

测试示例如下:


function Person(name) {
this.name = name;
}
Person.prototype.sayName = function () {
console.log(this.name);
};

const t = _new(Person, "小明");
console.log(t.name); // 小明
t.sayName(); // 小明

以上就是关于 JavaScript 中 new 操作符的作用,以及如何来实现一

作者:codinglin
来源:juejin.cn/post/7222274630395379771
个 new 操作符。

收起阅读 »

CSS链接悬停效果的的小创意

web
前言 每次写a标签的时候我都烦躁,为什么默认是蓝色的,就跟奇怪,关键他那个颜色也用不上,今天想让平凡的a标签也能做出令人眼前一亮的效果,大家以后直接cv多好 悬停滑动高亮链接效果 鼠标悬停后,链接有一个过渡动画,填充背景色,我们从链接周围的填充开始,然后添加相...
继续阅读 »

前言


每次写a标签的时候我都烦躁,为什么默认是蓝色的,就跟奇怪,关键他那个颜色也用不上,今天想让平凡的a标签也能做出令人眼前一亮的效果,大家以后直接cv多好


悬停滑动高亮链接效果


鼠标悬停后,链接有一个过渡动画,填充背景色,我们从链接周围的填充开始,然后添加相同值的负边距以防止填充破坏文本流。我们将使用box-shadow而不是 background 属性,因为它允许我们转换。


a { 
box-shadow: inset 0 0 0 0 #54b3d6;
color: #54b3d6;
margin: 0 -.25rem;
padding: 0 .25rem;
transition: color .3s ease-in-out, box-shadow .3s ease-in-out;
}
a:hover {
box-shadow: inset 100px 0 0 0 #54b3d6;
color: white;
}


悬停链接文本交换效果


我们在悬停时将链接的文本与其他一些文本交换。将鼠标悬停在文本上,链接的文本会随着新文本的滑入而滑出。


 <p><a href="#" data-replace="给个三连,好不好嘛"><span>鼠标放到这里试一试</span></a></p>

让我们给链接一些基本样式。我们需要给它相对定位来固定伪元素,确保它的显示是inline-block为了获得盒子元素样式的可供性,并隐藏伪元素可能导致的任何溢出。


  a {
overflow: hidden;
position: relative;
display: inline-block;
}

::before,::after设置为链接的全宽,左侧位置为零,并且绝对定位。


a::before,
a::after {
content: '';
position: absolute;
width: 100%;
left: 0;
}

::after伪元素从 HTML 标记中的链接数据属性获取内容:


a::after {
content: attr(data-replace);
}

transform: translate3d()::after伪元素元素向右移动 200%,悬停再回到以前的位置。


a::after {
content: attr(data-replace);
top: 0;
transform-origin: 100% 50%;
transform: translate3d(200%, 0, 0);
}

a:hover::after,
a:focus::after {
transform: translate3d(0, 0, 0);
}

我们使用transform: scaleX(0)::before伪元素,因此默认情况下它是隐藏的。悬停后我们将使它显示出来,就像2px高度一样,并将其固定到 上bottom,使其看起来像文本上的下划线那种感觉,看一下代码就理解我说的意思了


a::before {
background-color: #54b3d6;
height: 2px;
bottom: 0;
transform-origin: 100% 50%;
transform: scaleX(0);
}

a:hover::before,
a:focus::before {
transform-origin: 0% 50%;
transform: scaleX(1);
}

随后加入了transform效果、一些颜色等等以获得完整的效果。

作者:前端高级工程师宋
来源:juejin.cn/post/7143596588579946503
an>

收起阅读 »

vue3 实现 chatgpt 的打字机效果

在做 chatgpt 镜像站的时候,发现有些镜像站是没做打字机的光标效果的,就只是文字输出,是他们不想做吗?反正我想做。于是我仔细研究了一下,实现了打字机效果加光标的效果,现在分享一下我的解决方案以及效果图 共识 首先要明确一点,chatgpt 返回的文本格...
继续阅读 »

在做 chatgpt 镜像站的时候,发现有些镜像站是没做打字机的光标效果的,就只是文字输出,是他们不想做吗?反正我想做。于是我仔细研究了一下,实现了打字机效果加光标的效果,现在分享一下我的解决方案以及效果图


Kapture 2023-04-14 at 14.02.32.gif


共识


首先要明确一点,chatgpt 返回的文本格式是 markdown 的,最基本的渲染方式就是把 markdown 文本转换为 HTML 文本,然后 v-html 渲染即可。这里的转换和代码高亮以及防 XSS 攻击用到了下面三个依赖库:



  • marked 将markdwon 转为 html

  • highlight 处理代码高亮

  • dompurify 防止 XSS 攻击


同时我们是可以在 markdown 中写 html 元素的,这意味着我们可以直接把光标元素放到最后!


将 markdown 转为 html 并处理代码高亮


先贴代码


MarkdownRender.vue


<script setup>
import {computed} from 'vue';
import DOMPurify from 'dompurify';
import {marked} from 'marked';
import hljs from '//cdn.staticfile.org/highlight.js/11.7.0/es/highlight.min.js';
import mdInCode from "@/utils/mdInCode"; // 用于判断是否显示光标

const props = defineProps({
// 输入的 markdown 文本
text: {
type: String,
default: ""
},
// 是否需要显示光标?比如在消息流结束后是不需要显示光标的
showCursor: {
type: Boolean,
default: false
}
})

// 配置高亮
marked.setOptions({
highlight: function (code, lang) {
try {
if (lang) {
return hljs.highlight(code, {language: lang}).value
} else {
return hljs.highlightAuto(code).value
}
} catch (error) {
return code
}
},
gfmtrue: true,
breaks: true
})

// 计算最终要显示的 html 文本
const html = computed(() => {
// 将 markdown 转为 html
function trans(text) {
return DOMPurify.sanitize(marked.parse(text));
}

// 光标元素,可以用 css 美化成你想要的样子
const cursor = '<span class="cursor"></span>';
if (props.showCursor) {
// 判断 AI 正在回的消息是否有未闭合的代码块。
const inCode = mdInCode(props.text)
if (inCode) {
// 有未闭合的代码块,不显示光标
return trans(props.text);
} else {
// 没有未闭合的代码块,将光标元素追加到最后。
return trans(props.text + cursor);
}
} else {
// 父组件明确不显示光标
return trans(props.text);
}
})

</script>

<template>
<!-- tailwindcss:leading-7 控制行高为1.75rem -->
<div v-html="html" class="markdown leading-7">
</div>
</template>

<style lang="postcss">
/** 设置代码块样式 **/
.markdown pre {
@apply bg-[#282c34] p-4 mt-4 rounded-md text-white w-full overflow-x-auto;
}
.markdown code {
width: 100%;
}

/** 控制段落间的上下边距 **/
.markdown p {
margin: 1.25rem 0;
}
.markdown p:first-child {
margin-top: 0;
}

/** 小代码块样式,对应 markdown 的 `code` **/
.markdown :not(pre) > code {
@apply bg-[#282c34] px-1 py-[2px] text-[#e06c75] rounded-md;
}

/** 列表样式 **/
.markdown ol {
list-style-type: decimal;
padding-left: 40px;
}
.markdown ul {
list-style-type: disc;
padding-left: 40px;
}

/** 光标样式 **/
.markdown .cursor {
display: inline-block;
width: 2px;
height: 20px;
@apply bg-gray-800 dark:bg-gray-100;
animation: blink 1.2s step-end infinite;
margin-left: 2px;
vertical-align: sub;
}
@keyframes blink {
0% {
opacity: 1;
}
50% {
opacity: 0;
}
100% {
opacity: 1;
}
}
</style>

可以发现最基本的 markdown 显示还是挺简单的,话就不多说了,都在注释里。


我想你也许对判断消息中的代码块是否未闭合更感兴趣,那么就继续看下去吧!


代码块是否未闭合


markdown 有两种代码块,一种是 `code` ,另一种是 ``` code ```,我叫他小代码块和大代码块。


一开始我是想用正则去判断的,但是奈何有点复杂,我实在想不出应该如何去编写正则,让 chatgpt 写的正则也会判断失败,而且还要考虑到转义符,就算写出了正则,估计也会很复杂和难以维护。


经过短暂的苦思冥想后,我想到了之前在 《Vue.js设计与实现》 中看到的用有限元状态机解析 html 文本的方案。


这里有请 chatgpt 简单介绍一下有限元状态机:



有限元状态机(finite element state machine)是一种基于状态机的建模技术,它可以将系统的行为描述为一组状态和状态之间的转换。每个状态可以对应系统内的一种状态,而状态之间的转换则表示系统内部的转换过程。有限元状态机还可以用于系统的可靠性分析和测试,同时也可以作为软件工程中的一种建模工具,帮助代码实现、测试和调试等方面。


在实际应用中,有限元状态机通常是通过自动化工具生成,并且可以用于控制系统、网络通信协议、软件接口等方面的设计和测试。由于有限元状态机具有自动化生成和完备性检查等优点,因此它已经成为了一种广泛应用的建模与测试工具。



请站在web前端的视角下进行介绍



从前端开发的角度来看,有限元状态机可以用于构建复杂的交互式Web应用程序。在Web应用程序中,有限元状态机可以描述用户界面的不同状态(例如,登录、注册、未登录等),以及状态之间的转换(例如,从登录状态转换到注册状态)。


通常情况下,前端工程师使用JavaScript实现有限元状态机,包括定义状态、定义状态之间的转换规则、处理状态转换时的行为等。使用有限元状态机可以帮助前端工程师更好地掌控应用程序的状态,从而减少代码中的复杂条件分支和无法预测的行为。同时,有限元状态机也可以帮助前端团队共同理解应用程序的状态和转换规则,从而更好地协作开发和维护Web应用程序。


总之,有限元状态机是一种非常有用的前端开发技术,可以帮助前端工程师更好地构建和管理Web应用程序的状态和行为,提高应用程序的可靠性和用户体验。



回到正题,我可以一点一点的从头开始去解析 markdown 文本。想象这么一个简单的状态转换流程:



  • 初始状态为文本状态。

  • 遇到代码块标记,文本状态转换到代码块开始状态。

  • 再次遇到代码块标记,从代码块开始状态转换到文本状态。


不过现实要更复杂一点,我们有小代码块和大代码块。有限元状态机的妙处就在这里,当处在小代码块状态的时候,我们不需要操心大代码块和正常文本的事,他的下一个状态只能是遇到小代码块的闭合标签,进入文本状态。


理解了这些,再来看我的源码,才会发现他的精妙。


const States = {
text: 0, // 文本状态
codeStartSm: 1, // 小代码块状态
codeStartBig: 2, // 大代码块状态
}

/**
* 判断 markdown 文本中是否有未闭合的代码块
* @param text
* @returns {boolean}
*/

function isInCode(text) {
let state = States.text
let source = text
let inStart = true // 是否处于文本开始状态,即还没有消费过文本
while (source) { // 当文本被解析消费完后,就是个空字符串了,就能跳出循环
let char = source.charAt(0) // 取第 0 个字
switch (state) {
case States.text:
if (/^\n?```/.test(source)) {
// 以 ``` 或者 \n``` 开头。表示大代码块开始。
// 一般情况下,代码块前面都需要换行。但是如果是在文本的开头,就不需要换行。
if (inStart || source.startsWith('\n')) {
state = States.codeStartBig
}
source = source.replace(/^\n?```/, '')
} else if (char === '\\') {
// 遇到转义符,跳过下一个字符
source = source.slice(2)
} else if (char === '`') {
// 以 ` 开头。表示小代码块开始。
state = States.codeStartSm
source = source.slice(1)
} else {
// 其他情况,直接消费当前字符
source = source.slice(1)
}
inStart = false
break
case States.codeStartSm:
if (char === '`') {
// 遇到第二个 `,表示代码块结束
state = States.text
source = source.slice(1)
} else if (char === '\\') {
// 遇到转义符,跳过下一个字符
source = source.slice(2)
} else {
// 其他情况,直接消费当前字符
source = source.slice(1)
}
break
case States.codeStartBig:
if (/^\n```/.test(source)) {
// 遇到第二个 ```,表示代码块结束
state = States.text
source = source.replace(/^\n```/, '')
} else {
// 其他情况,直接消费当前字符
source = source.slice(1)
}
break
}
}
return state !== States.text
}

export default isInCode

到这里,就已经实现了一个 chatgpt 消息渲染了。喜欢的话点个赞吧!谢谢!


作者:七分小熊猫
来源:juejin.cn/post/7221792648541356093
收起阅读 »

裸辞一个月备考事业编所历所思

3.7日正式离职离现在也一个月了,这一个月里人类该有的情绪或大都尝了一遍。浅聊一下这一个月里干的事及一些感受。 原计划与现实 原本计划三月中旬提离职,四月 深圳-〉重庆-〉云南 半月游,五月开始刷题、下旬回到武汉。 计划永远赶不上变化。 2.23像往常一样坐在...
继续阅读 »

3.7日正式离职离现在也一个月了,这一个月里人类该有的情绪或大都尝了一遍。浅聊一下这一个月里干的事及一些感受。


原计划与现实


原本计划三月中旬提离职,四月 深圳-〉重庆-〉云南 半月游,五月开始刷题、下旬回到武汉。


计划永远赶不上变化。


2.23像往常一样坐在工位吃早餐。打开手机微信,看到一条订阅关于湖北事业单位招考的。于是,点进去看了看报考单位,那时就开始在想要不要试试。好嘛,我和一些朋友说了这个打算,好几个也一起加入备考大军。


第二天周五我调休了。算上周末三天,我开始了解事业单位考试及确定报考的岗位(实则,啥也不知道,随便选了一个我们小县城的岗位试试看)。


周一去到公司就提了离职。最后走的那天晚上请了几个同事吃了散伙饭,道了告别。


离职.png



我:拜拜,下辈子再见~ (人生中很多人再见就是再也不见了)



IMG_1089.png


备考


报名.PNG



  • 前期


离职后,当然是要给自己放松一下。于是,给自己放纵了两天。


接着开始备考。朋友分享了一套粉笔课程,每天也就看1-3个职测基础视频。


开始还是感觉蛮新鲜的。特别是一些逻辑、图形推理。哇,原来有这些套路。还记得以前面试一家公司让我做的题和这些差不多,当时觉得这都是些啥啊,和我做前端有关系吗。


其实期间除了看视频学习,其他时间基本都是在刷手机。那段时间B站推给我的全是大龄找不到工作、工作不好找等等让人致郁的视频。每天除了不专心学习就是无止尽的焦虑感。



  • 中期


后面把理论攻坚里的职测看完之后,更加放飞自我,每天也就打开粉笔app做几道题,然后就去手机里吸收消极情绪。很好笑,每天都在继续做前端、转行、摆摊、回家种地无限循环,当然包括现在偶尔也会是这样。


第一天:


等我考完这个破试回去武汉还是先找前端工作看看。


第二天:


刷了下B站,刷到说程序员找不到工作了,大龄了更加没人要了。那就考完试回来把后期视频好好学习下,回去找个剪辑师工作试试。


第三天:


打开Boss,搜索剪辑师;很多都是招流媒体,然后要求:会剪辑、会策划、会写脚本、会拍摄、会运营,还需要有工作经验。


又是那个无解的问题。


公司:我们需要有经验的。


我:我就是没有经验才找工作积累经验啊。


总结:转行不易。


第四天:


发现现在摆摊很火,去B站刷别人摆摊分享。了解了很多,常见的烤肠,还有之前没听过的热奶宝。这里可以推荐一个up主蜻园,还蛮不错的。


第五天:


算了,先考完试再说吧,实在不行就回家种地。刷B站,哎,这个剧还不错搜索全集cut,花几天看完。


这段时间差不多一个多星期,情绪就在这些上面循环往复,每天凌晨1、2点开始睡,但是得翻来覆去,差不多4、5点才能睡着,第二天起来已经中午了,再做个饭,差不多下午了,一天也差不多了。每天都是致郁的一天。



  • 后期


要考试了,得突击一下,把考前冲刺视频看了看,一天做几题。裸考综应和公共基础,其他随缘吧。


备考.png


参加考试



  • 去到酒店
    IMG_1258.png
    酒店备考.png


IMG_1271.png



  • 考试当天


考试的人真多,看起来很多都不是很大。


考试当天.png


考试当天听到一个女生对另外一个说:考试前几天整晚整晚都睡不着。(现在的人都不容易啊,很多大学生毕业即失业,都在卷考研、考编;白天准备考研,晚上准备考编。)


去到考场,安检然后需要手机关机,真是尴尬,第一次用苹果手机不会关机,当时迟迟关不了机,监考老师一度怀疑我有问题。


说:哎,你怎么回事,关机关这么久。


最后我问了旁边考生怎么关机,然后老师教了我,接着说:不会你早点说啊,一点都不谦虚。


我尴尬而不失礼貌的微笑。


快要考试了,我去上了个厕所,毕竟连续考三个半小时,时间也紧张,压根做不完,上厕所时间都得把握的好(喂,能不能在学习上多花点功夫哈)。


离谱,去厕所路上经过一个教室,外面三个女生都还在看书复习,我上完厕所出来,还在看,然后监考老师对她们说,快进来安检了,不要看了。


进考场看过来,可以看出很多女生都是那种很爱学习的人,就感觉我一个是来碰运气的。



  • 考试中


职测真做不完,之前和朋友说语文我完全不行,只能靠数学,结果数学计算相关压根没时间做。做题顺序不是按考卷顺序来的,开始也直接跳过常识题,直接言语开始啥的。


记得等我计算完资料分析的第一题后,我一看时间,我的妈,只剩下半小时了,数学运算直接放弃,当然不止数学运算,还有几十道都没有做,都是选择题,最后都是闭眼涂。


最无语的是我的综应都没有做完。作文十年没有写过了,我TM全抄的给的素材,离大谱;字写的也丑,唯一记得是以前老师对我说:一笔一画写清楚就行,不要连笔。感觉不连笔我字都不会写了,反正做的特慢,作文还大篇幅抄,最后来个总结,离大谱。



  • 考完试


考完试我就感觉自己是废物,感觉可以另谋生路了,这辈子估计是指望不上了。
考试结束.png


然后回酒店收东西,准备回家。退完房出来,下着瓢泼大雨,就像我的心情一样。


打了个的,准备去南站坐车,司机说好像只有北站有车去我那个地方;好嘛,又绕了老远了。在车上和司机聊了好多。


司机说也是回来考试的啊,然后说了我是从深圳回去考。


司机:这么远回来要是考不...没说完,算了,不说了,要是考的不好,还花了这么多钱。


我:笑了下,考不上就算了呗,还能怎么办。


然后说了很多,说她女儿也是搞计算机这行的,当时要她找个稳定的工作,不带编也行,她不干,现在在广州...叭叭叭,一起说了一路。


回家,晚上老妈回来一起聊了好多。很多时候我们焦虑,需要一个情绪缺口去宣泄。


返程


考完这次试,再加和老妈聊的许多,完全没有玩玩休息下的心情,第二天就准备返回深圳。


到深圳后就把自己后期AE中级课程开启了,开始学习(AE好难啊)。


后面准备一边上课学习一边背前端八股文,偶尔出去拍拍照,积累些剪辑素材,之后回湖北后剪视频纪念用。


最后


最后想说:想的多了都是问题,做的多了都是答案。当你一直在消极情绪里时,可以找一些喜欢的事情去做,从不好的情绪里脱离出来,不必想的过多、过远,毕竟65岁退休都“太早了”。


作者:前端Y酱
来源:juejin.cn/post/7221131892427014205
收起阅读 »

五分钟实现一个chatGPT打字效果

web
由于chatGPT最近大火,甲方爸爸觉得这样的打字效果很酷,必须要在项目中安排一下,所以动手实现了这个效果 打字状态分析 loading - 在等待打字内容的时候光标会一直显示且闪烁 tyeing - 在打字中光标会显示但不闪烁 end - 在打字结束后光标...
继续阅读 »

由于chatGPT最近大火,甲方爸爸觉得这样的打字效果很酷,必须要在项目中安排一下,所以动手实现了这个效果


打字状态分析



  1. loading - 在等待打字内容的时候光标会一直显示且闪烁

  2. tyeing - 在打字中光标会显示但不闪烁

  3. end - 在打字结束后光标隐藏


样式


// 光标字符显示
.typing::after {
content: '▌';
}
// 光标闪烁动画
.blinker::after {
animation: blinker 1s step-end infinite;
}
@keyframes blinker {
0% {
visibility: visible;
}
50% {
visibility: hidden;
}
100% {
visibility: visible;
}
}

内容打印功能实现


结合定时器和光标样式设置


**
* @description:
* @param {HTMLElement} dom - 打印内容的dom
* @param {string} content - 打印文本内容
* @param {number} speed - 打印速度
* @return {void}
*/
function printText(dom, content, speed = 50) {
let index = 0
setCursorStatus(dom, 'typing')
let printInterval = setInterval(() => {
dom.innerText += content[index]
index++
if (index >= content.length) {
setCursorStatus(dom, 'end')
clearInterval(printInterval)
}
}, speed)
}

/**
* @description: 设置dom的光标状态
* @param {HTMLElement} dom - 打印内容的dom
* @param {"loading"|"typing"|"end"} status - 打印状态
* @return {void}
*/

function setCursorStatus(dom, status) {
const classList = {
loading: 'typing blinker',
typing: 'typing',
end: '',
}
dom.className = classList[status]
}

效果预览


作者:chansee97
来源:juejin.cn/post/7221368910139113531
an>

收起阅读 »

就在昨天,我也关闭了朋友圈 ... ...

关于“关闭朋友圈”这个话题的文章,若干年前无意中就看到过,当我给我自己的第一个感觉是——不痛不痒。 为什么要关闭呢?每天在班车上闲来无事,看看周围的朋友们都有什么有趣的事情发生,多好啊。不懂为什么要去可以的关闭它,真的不懂。 随着年纪慢慢变大(很逃避“变老”这...
继续阅读 »

关于“关闭朋友圈”这个话题的文章,若干年前无意中就看到过,当我给我自己的第一个感觉是——不痛不痒


为什么要关闭呢?每天在班车上闲来无事,看看周围的朋友们都有什么有趣的事情发生,多好啊。不懂为什么要去可以的关闭它,真的不懂。


随着年纪慢慢变大(很逃避“变老”这个词),自己也越来越逃避社交,每到闲暇的时候,总是喜欢自己一个人听一听音乐,然后做几道LeetCode算法题,写一写算法题图解(虽然没多少人看)。但是很舒服,很自在。


再也不像十多年前,一到周末,三五好友,推杯换盏,牛皮吹得连马云都会觉得自己啥也不是。足迹也遍布了王府井、天安门、故宫、鸟巢、水立方、恭王府……


我有时在想,为什么自己越来越脱离了社会的群体了呢?反而更沉浸于自己的精神世界中,甚至一度怀疑自己患上了深度抑郁症


直到我近期读完了李笑来写的**《财富自由之路》**,被无数次的触动和震惊,人和人在认知的差距真的可以相差了一个南极到北极的距离。回首过往,自己干过的太多傻憨憨的事情,也不由自主的汗颜得低下了头


当时还是在2016年的时候吧,自己买了一辆很喜欢的车子,兴奋、激动、恨不得一时间给所有亲戚朋友打电话,告诉他们这车有多么的好,配置有多么的丰富。好像再迟了一秒钟,车子就会融化消失一样。从订车、到提车、再到洗车、再到开车去公司的路上,无数的朋友圈都在拼命的告诉周围的人,“我买车了!”嗨,现在一想。蛮尴尬的。


就像知乎有一篇帖子写到,“**朋友圈总是陷入到“羡慕别人”和“处心积虑让别人羡慕”的荒谬境地,发票圈和看票圈变得越来越无趣了”,**这句话说的多真实。


再看抖音也是一样,人均劳斯莱斯,人均2,3000万的豪宅,满地的“成功学小丑”——“我职高毕业,但是!毕业第一年,我开了麻辣烫店,净利润500万,第二年我就开劳斯莱斯库,手下团队100多人……


现在的网络充斥着太多的一夜暴复,沉浸在这种氛围下的我们也越来越浮躁了。就像我很喜欢的一个主播叫“在石250”,他在直播的时候就说“我是80后,当时网络也没那么发达,当我毕业工作的时候,看到路上开过一辆宝马3系,我都觉得牛逼得很。而现在呢,闹市街头好多小年轻用手机拍车子,你开过去一辆宝马5系,他都会摇一摇头,说上一句“这车一般,凑活事儿吧,比奔驰E300差远了!”,然后每个月领着2000块钱的工资,去网吧啃泡面。


我们被充斥了这么多网络垃圾之后,自己会受影响吗?绝对会的,而且会被毒害很深。我们发现,自己做的事情也来越沉不住气了,自己越来越觉得不月入10万都赶不上国民平均收入了,自己不买辆保时捷都没法出去跟朋友聚会了,自己家房子小于150平米那基本“狗都摇头了”。


我们该停一停了。我们需要信息,但并非这种信息。让我们自己安静下来,沉下心,静静的冲一杯绿茶,坐在窗边吹着微风,读一本我们很早就想读,但是被刷抖音和看朋友圈替代的书。坚持下去,你会发现,原来世界如此美好。自己的精神世界那么的安宁,外面世界的浮躁气息突然的这么让你嗤之以鼻。


说了很多,是的。就在昨天,我也关闭了朋友圈,去开始迎接一个全新的世界。在那个世界里,只有安宁、祥和、暖风和青云~


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

AutoGPT太火了,无需人类插手自主完成任务,GitHub2.7万星

OpenAI 的 Andrej Karpathy 都大力宣传,认为 AutoGPT 是 prompt 工程的下一个前沿。 近日,AI 界貌似出现了一种新的趋势:自主人工智能。 这不是空穴来风,最近一个名为 AutoGPT 的研究开始走进大众视野。特斯拉前 AI...
继续阅读 »

OpenAI 的 Andrej Karpathy 都大力宣传,认为 AutoGPT 是 prompt 工程的下一个前沿。


近日,AI 界貌似出现了一种新的趋势:自主人工智能

这不是空穴来风,最近一个名为 AutoGPT 的研究开始走进大众视野。特斯拉前 AI 总监、刚刚回归 OpenAI 的 Andrej Karpathy 也为其大力宣传,并在推特赞扬:「AutoGPT 是 prompt 工程的下一个前沿。」



不仅如此,还有人声称 ChatGPT 已经过时了,AutoGPT 才是这个领域的新成员。



项目一经上线,短短几天狂揽 27K + 星,这也侧面验证了项目的火爆。



GitHub 地址:github.com/torantulino…

问题来了,AutoGPT 到底是什么?它是一个实验性的开源应用程序,展示了 GPT-4 语言模型的功能。该程序由 GPT-4 驱动,可以自主实现用户设定的任何目标。



具体来说,AutoGPT 相当于给基于 GPT 的模型一个内存和一个身体。有了它,你可以把一项任务交给 AI 智能体,让它自主地提出一个计划,然后执行计划。此外其还具有互联网访问、长期和短期内存管理、用于文本生成的 GPT-4 实例以及使用 GPT-3.5 进行文件存储和生成摘要等功能。AutoGPT 用处很多,可用来分析市场并提出交易策略、提供客户服务、进行营销等其他需要持续更新的任务。

正如网友所说 AutoGPT 正在互联网上掀起一场风暴,它无处不在。很快,已经有网友上手实验了,该用户让 AutoGPT 建立一个网站,不到 3 分钟 AutoGPT 就成功了。 期间 AutoGPT 使用了 React 和 Tailwind CSS,全凭自己,人类没有插手。看来程序员之后真就不再需要编码了。



之后该用户补充说,自己的目标很简单,就是用 React 创建一个网站。提出的要求是:创建一个表单,添加标题「Made with autogpt」,然后将背景更改为蓝色。AutoGPT 成功的构建了网站。该用户还表示,如果给 AutoGPT 的 prompt 更多,表现会更好。

图源:twitter.com/SullyOmarr/…

接下里我们再看一个例子。假装自己经营一家鞋公司,给 AutoGPT 下达的命令是对防水鞋进行市场调查,然后让其给出 top5 公司,并报告竞争对手的优缺点 :



首先,AutoGPT 直接去谷歌搜索,然后找防水鞋综合评估 top 5 的公司。一旦找到相关链接,AutoGPT 就会为自己提出一些问题,例如「每双鞋的优缺点是什么、每款排名前 5 的防水鞋的优缺点是什么、男士排名前 5 的防水鞋」等。

之后,AutoGPT 继续分析其他各类网站,并结合谷歌搜索,更新查询,直到对结果满意为止。期间,AutoGPT 能够判断哪些评论可能偏向于伪造,因此它必须验证评论者。



执行过程中,AutoGPT 甚至衍生出自己的子智能体来执行分析网站的任务,找出解决问题的方法,所有工作完全靠自己。

结果是,AutoGPT 给出了 top 5 防水鞋公司的一份非常详细的报告,报告包含各个公司的优缺点,此外还给出了一个简明扼要的结论。全程只用了 8 分钟,费用为 10 美分。期间也完全没有优化。



这个能够独立自主完成任务的 AutoGPT 是如何运行的呢?我们接着来看。

AutoGPT:30 分钟内构建你自己的 AI 助手

作为风靡互联网的 AI 智能体,AutoGPT 可以在 30 分钟内完成设置。 你就可以拥有自己的 AI,协助完成任务,提升工作效率。

这一强大的 AI 工具能够自主执行各种任务,设置和启动的简便性是一大特征。在开始之前,你需要设置 Git、安装 Python、下载 Docker 桌面、获得一个 OpenAI API 密钥。

克隆存储库

首先从 GitHub 中克隆 AutoGPT 存储库。



使用以下命令导航到新建文件夹 Auto-GPT。



配置环境

在 Auto-GPT 文件夹中,找到.env.template 文件并插入 OpenAI API 密钥。接着复制该文件并重命名为.env。



安装 Python 包

运行以下命令,安装需要的 Python 包。



运行 Docker

运行 Docker 桌面,不需要下载任何容器,只需保证程序处于激活状态。



运行 AutoGPT



执行以下命令,运行 AutoGPT。



设置目标**

AutoGPT 虽是一个强大的工具,但并不完美。为避免出现问题,最好从简单的目标开始,对输出进行测试,并根据自身需要调整目标,如上文中的 ResearchGPT。

不过,你如果想要释放 AutoGPT 的全部潜力,需要 GPT-4 API 访问权限。GPT-3.5 可能无法为智能体或响应提供所需的深度。

AgentGPT:浏览器中直接部署自主 AI 智能体

近日,又有开发者对 AutoGPT 展开了新的探索尝试,创建了一个
可以在浏览器中组装、配置和部署自主 AI 智能体的项目 ——AgentGPT。** 项目主要贡献者之一为亚马逊软件工程师 Asim Shrestha,已在 GitHub 上获得了 2.2k 的 Stars。



AgentGPT 允许你为自定义 AI 命名,让它执行任何想要达成的目标。自定义 AI 会思考要完成的任务、执行任务并从结果中学习,试图达成目标。如下为 demo 示例:HustleGPT,设置目标为创立一个只有 100 美元资金的初创公司。



再比如 PaperclipGPT,设置目标为制造尽可能多的回形针。



不过,用户在使用该工具时,同样需要输入自己的 OpenAI API 密钥。AgentGPT 目前处于 beta 阶段,并正致力于长期记忆、网页浏览、网站与用户之间的交互。

GPT 的想象力空间还有多大,我们继续拭目以待。

参考链接: medium.com/@tsaveratto…


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

GPT-4自我修复!国外小哥开发神级「金刚狼」,无限自我Debug

【新智元导读】继 GPT-4 超强进化后,现在还能自我修复。国外网友开发一个「金刚狼」项目,能够自动修复 Python 中的 bug,并运行代码。 要问程序员,一天中最烦的时候是什么? 那一定是给写好的程序 Debug 了。而现在,这种局面要得到改善了! 国...
继续阅读 »

【新智元导读】继 GPT-4 超强进化后,现在还能自我修复。国外网友开发一个「金刚狼」项目,能够自动修复 Python 中的 bug,并运行代码。


要问程序员,一天中最烦的时候是什么?


那一定是给写好的程序 Debug 了。而现在,这种局面要得到改善了!



国外一名叫 BioBootloader 的开发者基于 GPT-4 搞了一个叫「金刚狼」的项目,能够自我修复 Python 脚本。


从名字就能看出来,这项目主打一个「自我愈合」。通过 GPT 识别代码中的错误,并提供修改,直至程序顺利运行。


不过,「金刚狼」目前只能用在 Python 上。


这项目已经在 GitHub 上收揽了 1.2k 星,108 个 Fork。



金刚狼?金刚狼!


BioBootloader 表示,用「金刚狼」运行你的程序,只要一崩溃,GPT-4 就会自动编辑,然后给出出错的原因。


哪怕码农写了一大堆 Bug,也没事。「金刚狼」会反复运行,直到一切 Bug 都被 De 掉。



GPT-4 想必大家都不陌生。这是由 OpenAI 开发的多模态人工智能语言模型。


BioBootloader 在推特上的演示视频中,展示了「金刚狼」的具体使用方式。



视频中,开发者先写了个简单的四则运算代码,然后故意把其中一些部分写错。



(正确的)


比方说,把结尾的 return result 随便改成 return res,而 res 没有定义,于是就出错了。


小哥还把减法部分的代码删掉了,就是上方的 substract_numbers。这样一来,下面 calculate 那里就一定会报错。因为 subtract 没有定义了。



(错误的)


之后直接运行「金刚狼」即可,GPT 生成的部分会出现在右侧。



可以看到,「金刚狼」快速识别出了错误,并且附上了解释。


「脚本中没有定义 subtract_numbers.


res 这个变量没有定义,应该用 result 代替。」



不光给了建议,「金刚狼」还直接把改好的代码附上了。红色是应该删掉的部分,绿色是添加的部分。


实际上,「金刚狼」是一个封装器,它负责运行程序,捕捉错误信息,再把这些错误发送给 GPT-4,询问 GPT 代码出了什么问题。


像 GPT-4 这种 LLM(即大型语言模型),是用自然语言「编程」的,而这些指令被视为 prompts。


「金刚狼」所实现的功能很大一部分要归功于精心编写的提示,阅读这些提示就可以更好的理解整个过程。


目前该项目已经发布在了 GitHub 上。小哥也是贴心的给出了设置上的要求。



不止是 Python


在 GitHub 上,BioBootloader 发布了自己未来的计划,「金刚狼」的功能会越来越全面、强大。



「目前的版本只是我花了几个小时搞得一个原型产品。未来还会有很多可能的延展,同时欢迎大家一起来开发。」




  • 添加标志来定制使用方法,例如在运行改变的代码前要求用户确认。




  • 对 GPT 的编辑格式进行进一步的迭代。目前,GPT 在缩进方面有点困难,但我确信这一点可以得到改善。




  • 一套有问题的文件的例子,我们可以在上方的测试进行提示,以确保其可靠性,并衡量改进的如何。




  • 多个文件 / 代码库——向 GPT 发送堆栈跟踪中出现的所有文件




  • 对大文件更好地处理,即我们是否应该只向 GPT 发送相关的类 / 函数?




  • 扩展到 Python 以外的编程语言




从上面那个简单的例子可以看出来,这个脚本还是未来可期的。


毕竟,总不能让用别的语言工作的码农们看着智能 Debug Python 的「金刚狼」眼红呀。


参考资料:


twitter.com/bio_bootloa…


hackaday.com/2023/04/09/…


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

推荐几个可以免费使用的ChatGPT工具

在ChatGPT相关API推出之后,各种工具如雨后春笋一般层出不穷,这篇文章就列举一些日常使用到的工具。 工具列表 myreader.io myReader主页 这款工具的作者是@madawei2699,github主页地址为t.co/adJBYWbjkF,...
继续阅读 »

在ChatGPT相关API推出之后,各种工具如雨后春笋一般层出不穷,这篇文章就列举一些日常使用到的工具。


工具列表


myreader.io


myReader主页


myReader主页


这款工具的作者是@madawei2699,github主页地址为t.co/adJBYWbjkF,…



  • 在线读取任意网页内容包括视频(YouTube),并根据这些内容回答你提出的相关问题或总结相关内容

  • 支持读取电子书与文档(支持PDF、EPUB、DOCX、Markdown、TXT),并根据这些内容回答你提出的相关问题或总结相关内容

  • 定时发送每日热榜新闻,无论新闻是中文还是其他语言,它都能使用chatGPT用中文自动总结新闻的内容,方便快速获取热点新闻信息

  • 支持 prompt 模版,能根据消息历史记录的上下文回答你的问题,甚至能和你玩游戏

  • 支持多国语音交互(英文、中文、德语与日语),它会根据你的语言使用相关语言的声音来回答你的问题,从而帮助你训练外语能力,可以理解为它是你的私人外教


具体功能演示可以参考我的AI阅读助手


chatpdf


ChatPDF主页


ChatPDF主页


这个可以看作是一个PDF辅助阅读的工具,用户上传自己的PDF之后,可以以对话的方式与工具进行交互,快速获取PDF文件的内容。


ChatPaper


专注于“科研狗”的工具,通过ChatGPT实现对论文进行总结,帮助“科研人”进行论文初筛(目前不支持针对论文内容进行对话)。


ChatPaper主页


ChatPaper主页


另外相关的工具还有润色工具、审稿工具、审稿回复工具


最后,这篇文章——ChatGPT应用开发小记中提到的基于chatGPT的应用类型的分类也有借鉴意义。


原理


之前准备写一篇专门介绍上述工具类的原理介绍(其实ChatGPT的 插件——chatgpt-retrieval-plugin),但是后来查看了几个项目的源码之后发现,这类工具的主要原理其实比较直观:



  • 解析相关输入为文本

  • 将文本分句后获取句子的embedding(这一步目前处理的处理方式大都是根据长度截断)并存储至数据库

  • 用户输入转换为embedding,并在数据库中召回相关性最高的句子集合

  • 将召回的句子与用户输入句子组装为ChaptGPT的输入,获取输出


上述思路虽然直观,但要获取更好的结果,其实除了第三步,其余每一步都有优化的空间:



  • 文本解析可以针对不同类型的数据针对性解析

  • 文本分句方式可以采取特殊标点进行分句,同时句子embedding也有很多可选生成方法

  • 召回的句子与用户输入句子组装为ChaptGPT的输入,结合任务特定的prompt,获取更适合任务的输出


具体流程图可以参考gpt-langchain-pdf:


gpt-langchain-pdf


gpt-langchain-pdf


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

面试整理-kotlin与jetpack

面试可能会问到的问题 内联函数与高阶函数 对委托的理解 扩展方法以及其原理 协变与逆变 协程相关知识(创建方式、原理) jetpack使用过哪些库 LiveData和LifeCycle的原理 Viewmodel的原理 WorkManager的使用场景 Nav...
继续阅读 »

面试可能会问到的问题



  1. 内联函数与高阶函数

  2. 对委托的理解

  3. 扩展方法以及其原理

  4. 协变与逆变

  5. 协程相关知识(创建方式、原理)

  6. jetpack使用过哪些库

  7. LiveData和LifeCycle的原理

  8. Viewmodel的原理

  9. WorkManager的使用场景

  10. Navigation使用过程中有哪些坑


内联函数和高阶函数



关键不是问你什么概念,而是看你在实际使用中有没有注意这些细节



概念



  • 内联函数:编译时把调用代码插入到函数中,避免方法调用的开销。

  • 高阶函数:接受一个或多个函数类型的参数,并/或返回一个函数类型的值


概念就这两句话,实际使用的时候却有很大的用途。比如我们常用的apply、run、let这些其实就是一个内联高阶函数。


// apply 
public inline fun <T> T.apply(block: T.() -> Unit): T { block() return this }
// run
public inline fun <T, R> T.run(block: T.() -> R): R { return block() }
// let
public inline fun <T, R> T.let(block: (T) -> R): R { return block(this) }

使用心得



  1. 有时候为了代码整洁,我们不会让一个方法超过一屏幕,会把里面的方法抽成几个小的方法,但是方法会涉及到入栈出栈,而内联函数就可以保证代码的整洁又避免了方法进栈出栈的开销。这个是我们稍微注意一下很方便做的优化。

  2. 为了简化函数的调用我们可以使用高阶函数,除了系统提供的apply、run、let这些外,自己其实平时也会写一些高阶函数,比如下面的例子





    • 使用高阶函数增加代码可读性




// 使用高阶函数简化网络请求处理
fun <T> Call<T>.enqueue(
onSuccess: (response: Response<T>) -> Unit,
onError: (error: Throwable) -> Unit,
onCancel: () -> Unit
) {
enqueue(object : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
if (response.isSuccessful) {
onSuccess(response)
} else {
onError(Exception("Request failed with code ${response.code()}"))
}
}

override fun onFailure(call: Call<T>, t: Throwable) {
if (!call.isCanceled) {
onError(t)
} else {
onCancel()
}
}
})
}

---
// 使用的时候
call.enqueue(
onSuccess = { response ->
// 在这里处理网络请求成功的逻辑

},
onError = { error ->
// 在这里处理网络请求失败的逻辑
},
onCancel = {
// 在这里处理网络请求取消的逻辑
}
)





    • 使用高阶函数减少无用回调,方便使用




// 使用高阶函数简化回调函数
fun EditText.doOnTextChanged(action: (text: CharSequence?, start: Int, before: Int, count: Int) -> Unit) {
addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
}

override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
action(s, start, before, count)
}

override fun afterTextChanged(s: Editable?) {
}
})
}

// 使用的时候只需要关系一个回调就可以
editText.doOnTextChanged { text, _, _, _ ->
// 在这里处理输入框文本变化的逻辑
}


这两个例子很好的说明了高阶函数的作用,可以简化一些操作,也可以增强可读性。其实还有一些其他的作用比如用高阶函数实现RecycleView初始化时的函数式编程、对一些方法添加缓存等等。
只要涉及到对原有方法的增强或者简化或者添加多一层封装实现链式调用都可以考虑使用高阶函数。


对委托的理解



因为委托在开发中真的非常好用,问这个问题就想看看你有没有真的理解委托



首先委托的概念就是把一个对象的职责委托给另外一个对象,在kotlin中有属性的委托和类的委托。属性的委托比如by lazy,他的作用是使用到的时候才加载简化了判空代码也节省了性能。类的委托通常是一个接口委托一个对象interface by Class。目的是对一个类的解耦方便以后相同功能的代码复用。例子就不举例了,就是但凡开发中想到有些代码是可以复用的时候可以考虑能不能写成一个接口去交给委托类去实现。


问到by lazy可能还会问你与lateinit的区别。



  • lateinit:延时加载,只是告诉编译器不用检查这个变量的初始化,不能使用val修饰

  • by lazy:懒加载,lazy是一个内联高阶函数,通过传入自身来做一些初始化的判断。


扩展方法以及其原理



扩展函数也是使用kotlin时非常好用的一个特性,多多少少可能也会提一嘴。



实际开发中我们的点击事件、资源获取等都可以使用。好处就不多说了,比如加入防抖,或者获取资源时的捕获异常,都可以减少日后添加需求时的开发量


private var lastClickTime = 0L
fun View.setSingleClickListener(delay: Long = 500, onClick: () -> Unit) {
setOnClickListener {
val currentTime = System.currentTimeMillis()
if (currentTime - lastClickTime > delay) {
onClick()
lastClickTime = currentTime
}
}
}


  • 原理
    Kotlin 中的扩展方法其实是一种静态的语法糖,本质上是一个静态函数,不是实例函数。编译器会将扩展方法转化为静态函数的调用。
    比如



fun String.lastChar(): Char = this.get(this.length - 1)
---
val s = "hello"
val c = s.lastChar() // 转化为 StringKt.lastChar(s)

协变与逆变(out 和 in)



这个问的可能比较少,这个问题其实主要还是看你有没有写过一些大型架构,尤其是像rxjava这种设计到入参出参的。




  • 协变与逆变是数学中的概率,协变就是x跟y正相关图形是往上的,逆变就是x跟y负相关图形是往下的。

  • 协变往上的肯定有个最大的上限,java中的上限就是obj,所以你会看到很多这样的代码out Any或者?extentd Object

  • 逆变往下的肯定有个最小值,所以你会看到很多这样的代码out T或者? super T


这里面还会涉及到一个set和get的问题,协变只能get不能set。比如逆变只能set不能get。这个结论你可以记起来,也可以理解一下,这个是面向对象的基础。举个例子说明


爷爷辈(会玩手机)、爸爸辈(会玩手机会上网)、孙子辈(会玩手机会上网会打游戏)。 比如指定的上限(out、extends)是爷爷辈,如果只是作为返回值,直接返回T就可以,因为不管你返回什么类型,最后都可以用爷爷辈来接。而如果用于set,你可以传个爸爸辈或者孙子辈的进来,里面并不知道你确切的类型就出问题了。


反过来,如果逆变(in、super)指定的下限是孙子辈,用于set就可以,因为孙子已经包含了爷爷、爸爸辈的内容了。而返回就不行,因为你外面返回如果用t接,你不知道是孙子辈还是老一辈。如果返回的是老一辈你外面调用用的是孙子辈打游戏就崩了。


协程相关知识



  • 协程的基本概念:协程是一个轻量级线程。可以用同步的方式编写异步代码,避免了异步代码传参时所引发的回调地狱。核心概念是挂起跟恢复。即协程可以在执行过程中主动挂起,等待某些事件发生后再恢复执行。挂起可以开发者控制比如调用await或者直接用suspend修饰。恢复是编译器的活。我们只管用就好了。

  • 其他的概念其实跟线程差不多



      • 和协程构建器:launchasync创建一个协程





      • 调度器是切换线程的:Dispatchers.IO、Dispatchers.Main





      • 协程作用域:通常由 coroutineScope 或 supervisorScope 函数创建,协程作用域可以用于确保协程在退出时所有资源都被正确释放。





      • 异常处理和取消:异常处理可以使用try-cache也可以使用CoroutineExceptionHandler指定一个协程异常处理的函数。





  • Flow



使用协程肯定会使用的一个机制,可以代替rxJava做一些简单的操作。



使用例子


import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

fun main() = runBlocking {
val flow = flow {
for (i in 1..10) {
delay(100)
emit(i)
}
}

flow
.buffer() // 缓冲区大小
.onEach {
println("Emitting $it")
delay(200)
}
.collectLatest {
println("Collecting $it")
delay(300)
}
}




    • 原理




Flow 是一种基于懒加载的异步数据流,它可以异步产生多个元素,同时也可以异步消费这些元素。Flow 的每个元素都是通过 emit 函数产生的,而这些元素会被包装成一个包含了多个元素的数据流。Flow 还支持各种各样的操作符,如 map、filter、reduce 等等,可以方便地对数据流进行处理和转换。


jetpack使用过哪些库



下面的不一定都用过,说几个自己用过的就好,但是既然用了就要对原理很熟悉,不然别人一问就倒




  1. ViewModel:用于在屏幕旋转或其他配置更改时管理UI数据的生命周期。

  2. LiveData:用于将数据从ViewModel传递到UI组件的观察者模式库。

  3. Room:用于在SQLite数据库上进行类型安全的ORM操作的库。

  4. Navigation:用于管理应用程序导航的库。

  5. WorkManager:用于管理后台任务和作业的库。

  6. Paging:用于处理分页数据的库。

  7. Data Binding:用于将布局文件中的视图绑定到应用程序数据源的库。

  8. Hilt:用于实现依赖注入的库。

  9. Security:提供加密和数据存储的安全功能。

  10. Benchmark:用于测试应用程序性能的库。


LiveData和LifeCycle的原理


LiveData


使用上非常简单,就是上下游的通知,就是一个简化版的rxjava




  1. LiveData持有一个观察者列表,可以添加和删除观察者。

  2. 当LiveData数据发生变化时,会通知观察者列表中的所有观察者。

  3. LiveData可以感知Activity和Fragment的生命周期,当它们处于激活状态时才会通知观察者,避免了内存泄漏和空指针异常。

  4. LiveData还支持线程切换,可以在后台线程更新数据,然后在主线程中通知观察者更新UI。


LiveData提供了setValuepostValue两个方法来设置数据通知



  • setValue:方法只能在主线程调用,不依赖Handler机制来回调,

  • postValue:可以在任何线程调,同步到主线程依赖于Handler,需要等待主线程空闲时才会执行更新操作。


LifeCycle


用于监听生命周期,包含三个角色。LifecycleOwner、LifecycleObserver和Lifecycle




  • LifecycleObserver是Lifecycle的观察者。viewmodel默认就实现了这个接口

  • LifecycleOwner是具有生命周期的组件,如Activity、Fragment等,它持有一个Lifecycle对象

  • Lifecycle是LifecycleOwner的生命周期管理器,它定义了生命周期状态和转换关系,并负责通知LifecycleObserver状态变化的事件


了解这三个角色其实就很容易理解了,本质上LifeCycle也是一个观察者模式,管理数据的是LifeCycle,生命周期的状态都是通过它来完成的。而我们写代码的时候要写的一句是getLifecycle().addObserver(xxLifeCycleObserver());是添加一个观察者,这个观察者就能收到相应的通知了。


Viewmodel的原理



这个问题有可能会问你Viewmodel跟Activity哪个先销毁、Viewmodel跟Activity是怎么进行生命周期的绑定的。



Viewmodel的两个重要类:ViewModelProviderViewmodelStore。其实就是我们使用时用到的


// 这里this接收的其实是一个`ViewModelStoreOwner`是一个接口,我们的AppCompatActivity已经实现了
aViewModel = ViewModelProvider(this).get(AViewModel::class.java)


  • ViewModelStore 是一个存储 ViewModel 的容器,用于存储与某个特定的生命周期相关联的 ViewModel



是一个全局的容器,实际上就是一个HashMap。




  • ViewModelProvider用于管理ViewModel实例的创建和获取


其实这里设计的理念也比较好理解,比如旋转屏幕这个场景,我们会使用Viewmodel来保存数据,因为他数据不会被销毁,之所以不被销毁不用想也只是肯定是脱离Activity或者Fragment保存的。



知道了Viewmodel会全局保存这一点,应该会有一些疑问,就是这个Viewmodel是什么时候回收的。



在Activity或者Fragment销毁其实只是移除了他的引用,当内存不足时gc会回收或者手动调用clear方法回收。所以回答Activity和Viewmodel谁的生命周期比较长时就知道了,只要不是手动清除肯定是ViewModel的生命周期比Activity长。


因为ViewModel一直存在,所以如果太多需要做一些优化,原则很简单,就是把ViewModel细分,有些没必要保存的手动清除,有些需要全局的就使用单例。


WorkManager的使用场景



其实就是一个定时任务,人家问你使用场景是看你有没有真正用过。




  1. 需要在特定时间间隔内执行后台任务,例如每天的定时任务或周期性的数据同步。

  2. 执行大型操作,例如上传或下载文件,这些操作需要时间较长,需要在后台执行。

  3. 应用退出时需要保存数据,以便在下一次启动时可以使用。

  4. 执行重复性的任务,例如日志记录或数据清理。


Navigation使用过程中有哪些坑



这个问题首先要明确Navigation是干嘛的才知道有什么坑





  • Navigation翻译过来是导航,其实就是一个管理Fragment的栈类似与我们使用Activity一样,样提供的方法也是一样的比如动画、跳转模式,并且它还可以让我们不用担心Fragment是否被回收直接调用它的跳转,没有的话会帮我们做视图的恢复数据它已经内部处理好了,还支持一些跳转的动画传参等都有相应的api。简而言之,Navigation能做的FragmentManager都能做,只是相对麻烦而已。




  • Navigation优势就不多说了,合适的场景就是线性的跳转,比如A跳B跳C跳D这种,直接一行代码就可以跳转。返回到指定的页面也有方法,比如从D返回到navController.popBackStack(R.id.fragmentA, false)。这里的ture和false要注意,具体的细节就去看官网了。




  • 不太适合的场景就是相互的调用,比如A跳B跳A跳B这种反复的,需要你设置好跳转模式,如果模式不对会出现反复的创建和销毁,这里使用SingleTop跳转模式可以解决。但是要处理的可能是你是什么地方跳过来是,返回方法要处理一下。


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

Android 开发中需要了解的 Gradle 知识

Gradle 是一个基于 Groovy 的构建工具,用于构建 Android 应用程序。在 Android 开发中,了解 Gradle 是非常重要的,因为它是 Android Studio 默认的构建工具,可以帮助我们管理依赖项、构建应用程序、运行测试等。 本...
继续阅读 »

Gradle 是一个基于 Groovy 的构建工具,用于构建 Android 应用程序。在 Android 开发中,了解 Gradle 是非常重要的,因为它是 Android Studio 默认的构建工具,可以帮助我们管理依赖项、构建应用程序、运行测试等。


本文将介绍 Android 开发中需要了解的一些 Gradle 知识,包括 Gradle 的基本概念、Gradle 的构建脚本、Gradle 的任务和插件等。


Gradle 的基本概念


Gradle 是一个基于项目的构建工具,它允许我们通过编写构建脚本来定义构建过程。Gradle 的基本概念包括:



  • 项目(Project):Gradle 中的项目是指构建的基本单元,一个项目包含多个模块。

  • 模块(Module):Gradle 中的模块是指项目中的一个组件,可以是一个库模块或应用程序模块。

  • 任务(Task):Gradle 中的任务是指执行构建过程的基本单元,每个任务都有一个名称和一个动作(Action)。

  • 依赖项(Dependency):Gradle 中的依赖项是指项目中的一个模块或库,用于在构建过程中引用其他代码或资源。


Gradle 的构建脚本


Gradle 的构建脚本是基于 Groovy 语言的脚本文件,文件名为 build.gradle,位于项目的根目录和每个模块的目录中。构建脚本可以定义项目的依赖项、构建任务和发布应用程序等。


Gradle 的构建脚本由以下两个部分组成:




  1. buildscript 块:用于定义 Gradle 自身的依赖项和配置。




  2. 模块配置块:用于定义模块的依赖项和任务。




下面是一个示例构建脚本:


// 定义构建脚本使用的 Gradle 版本
buildscript {
repositories {
// 定义依赖项所在的仓库
google()
mavenCentral()
}
dependencies {
// 定义 Gradle 自身的依赖项
classpath 'com.android.tools.build:gradle:7.1.3'
}
}

// 定义模块的依赖项和任务
apply plugin: 'com.android.application'

android {
compileSdkVersion 31

defaultConfig {
applicationId "com.example.myapp"
minSdkVersion 21
targetSdkVersion 31
versionCode 1
versionName "1.0"
}

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}

dependencies {
// 定义模块的依赖项
implementation 'androidx.appcompat:appcompat:1.4.2'
implementation 'com.google.android.material:material:1.6.1'
}

Gradle 的任务


Gradle 的任务是构建过程的基本单元,每个任务都有一个名称和一个动作。Gradle 内置了很多任务,例如编译代码、运行测试、打包应用程序等。我们也可以根据需要自定义任务。


Gradle 的任务由以下三个部分组成:




  1. 任务名称:任务的唯一标识符,通常由一个或多个单词组成,例如 build、assembleDebug 等。




  2. 任务依赖项:任务依赖于其他任务,可以使用 dependsOn() 方法指定任务依赖项,例如:




task myTask {
dependsOn otherTask
doLast {
println 'myTask executed'
}
}

上面的示例中,myTask 任务依赖于 otherTask 任务,即在执行 myTask 之前需要先执行 otherTask。




  1. 任务动作:任务要执行的操作,可以使用 doFirst() 和 doLast() 方法指定任务动作,例如:




task myTask {
doFirst {
println 'myTask starting'
}
doLast {
println 'myTask executed'
}
}

上面的示例中,myTask 任务在执行前会先打印一条消息,然后执行任务动作,执行完毕后再打印一条消息。


Gradle 的插件


Gradle 的插件是用于扩展 Gradle 功能的组件,每个插件都提供一组任务和依赖项,用于构建应用程序或库模块。Gradle 中有很多插件,例如 Android 应用程序插件、Java 库插件等。我们也可以根据需要自定义插件。


Gradle 的插件由以下两个部分组成:



  1. 插件声明:用于声明插件及其依赖项,例如:


plugins {
id 'com.android.application' version '7.1.3'
}

上面的示例中,声明了 Android 应用程序插件及其依赖项。



  1. 插件配置:用于配置插件的行为和属性,例如:


android {
compileSdkVersion 31
defaultConfig {
applicationId "com.example.myapp"
minSdkVersion 21
targetSdkVersion 31
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}

上面的示例中,配置了 Android 应用程序插件的属性,例如编译版本、应用程序 ID、最小 SDK 版本等。


总结


本文介绍了 Android 开发中需要了解的一些 Gradle 知识,包括 Gradle 的基本概念、构建脚本、任务和插件等。


Gradle 是一个功能强大的构建工具,通过掌握 Gradle 的基本概念、构建脚本、任务和插件等知识,可以更好地理解和使用 Gradle,从而提高 Android 应用程序的开发效率和质量。


需要注意的是,Gradle 是一项非常庞大和复杂的技术,本文仅对其中一些基本概念和知识进行了介绍,对于更深入和复杂的问题,需要通过进一步的学习和实践来掌握和解决。


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

Kotlin | 使用vararg可变参数

背景 一般在项目开发中,我们经常会在关键节点上埋点,而且埋点中会增加一些额外参数,这些参数通常是成对出现且参数个数是不固定的。如下: //定义事件EVENT_ID const val EVENT_ID = "event_xmkp" //注意:这里传入的是va...
继续阅读 »

背景


一般在项目开发中,我们经常会在关键节点上埋点,而且埋点中会增加一些额外参数,这些参数通常是成对出现参数个数是不固定的。如下:


//定义事件EVENT_ID
const val EVENT_ID = "event_xmkp"

//注意:这里传入的是vararg可变参数
fun String.log(vararg args: String) {
if (args.size % 2 > 0) {
throw RuntimeException("传入的参数必须是偶数")
}
if (args.isEmpty()) {
buryPoint(this)
} else {
//注意这里:可变参数在作为数组传递时需要使用伸展(spread)操作符(在数组前面加 *)
buryPoint(this, *args)
}
}

private fun buryPoint(eventId: String, vararg args: String) {
if (args.isNotEmpty()) {
Log.e(TAG, "buryPoint: $eventId, args: ${args.toList()}")
} else {
Log.e(TAG, "buryPoint: $eventId")
}
}

调用方式如下:


EVENT_ID.log()
EVENT_ID.log("name", "小马快跑")
EVENT_ID.log("name", "小马快跑", "city", "北京")

示例中可变参数可以是0个、2个、4个,执行结果:


2022-11-22 19:00:54 E/TTT: eventID: event_xmkp
2022-11-22 19:00:54 E/TTT: eventID: event_xmkp, args: [name, 小马快跑]
2022-11-22 19:00:54 E/TTT: eventID: event_xmkp, args: [name, 小马快跑, city, 北京]

可以看到通过定义可变参数,在调用方可以灵活地传入0个多个参数,下面就分析下Kotlin方法中的可变参数。


注意:可变参数在作为数组传递时需要使用伸展操作符(在数组前面加 *),如果去掉 *号,编译器会报如下错:


请添加图片描述


Kotlin中使用可变参数


Java中可变参数规则:



  • 使用...表示可变参数

  • 可变参数只能在参数列表的最后

  • 可变参数在方法体中最终是以数组的形式访问


Kotlin中可变参数规则:



  • 不同于Java,在Kotlin中如果 vararg 可变参数不是列表中的最后一个参数, 可以使用具名参数语法传递其后的参数的值。

  • Java一样,在函数内,可以以数组的形式使用这个可变参数的形参变量,而如果需要传递可变参数,需要在前面加上伸展(spread)操作符(在数组前面加 *),第一节已给出示例。


对Kotlin可变参数反编译


对上一节中的String.log()代码反编译成Java代码:


//kt代码
fun String.log(vararg args: String) {
if (args.size % 2 > 0) {
throw RuntimeException("传入的参数必须是偶数")
}
if (args.isEmpty()) {
buryPoint(this)
} else {
//注意这里:可变参数在作为数组传递时需要使用伸展(spread)操作符(在数组前面加 *)
buryPoint(this, *args)
}
}

转换之后:


 // Java代码
public final void log(@NotNull String $this$log, @NotNull String... args) {
...
if (args.length % 2 > 0) {
throw (Throwable)(new RuntimeException("传入的参数必须是偶数"));
} else {
if (args.length == 0) {
this.buryPoint($this$log);
} else {
this.buryPoint($this$log, (String[])Arrays.copyOf(args, args.length));
}
}
}


  • Kotlinvararg args: String参数转换成Java的 @NotNull String... args

  • Kotlinspread伸展操作符*args转换成Java(String[])Arrays.copyOf(args, args.length),可见最终还是通过系统拷贝生成了数组。

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

5分钟带你学会MotionLayout

1、前言 最近在开发中,同事居然对MontionLayout一知半解,那怎么行!百里偷闲写出此文章,一起学习、一起进步。如果写的不好,或者有错误之处,恳请在评论、私信、邮箱指出,万分感谢🙏 希望你在阅读这篇文章的时候,已经对下面的内容熟练掌握了 Animat...
继续阅读 »

1、前言


最近在开发中,同事居然对MontionLayout一知半解,那怎么行!百里偷闲写出此文章,一起学习、一起进步。如果写的不好,或者有错误之处,恳请在评论、私信、邮箱指出,万分感谢🙏


希望你在阅读这篇文章的时候,已经对下面的内容熟练掌握了



对了还有ConstraintLayout务必熟练掌握


对了,如果可以,请跟随敲代码,毕竟你脑补的代码,没有编译器。


2、简介


1)根据功能将MontionLayout视为属性动画框架、TransitionManagerCoordinatorLayout 的混合体。允许描述两个布局之间的转换(如 TransitionManager),但也可以为任何属性设置动画(不仅仅是布局属性)。


2)支持可搜索的过渡,如 CoordinatorLayout(过渡可以完全由触摸驱动并立即过渡到的任何点)。支持触摸处理和关键帧,允许开发人员根据自己的需要轻松自定义过渡。


3)在这个范围之外,另一个关键区别是 MotionLayout 是完全声明式的——你可以用 XML 完整地描述一个复杂的转换——不需要代码(如果你需要通过代码来表达运动,现有的属性动画框架已经提供了一种很好的方式正在做)。


4)MotionLayout 只会为其直接子级提供其功能——与 TransitionManager 相反,TransitionManager 可以使用嵌套布局层次结构以及 Activity 转换。


3、何时使用


MotionLayout 设想的场景是当需要移动、调整实际 UI 元素(按钮、标题栏等)或为其设置动画时——用户需要与之交互的元素。


重要的是要认识到运动是有目的的——不应该只是你应用程序中一个无偿的特殊效果;应该用来帮助用户了解应用程序在做什么。Material Design 原则网站很好地介绍了这些概念。


有一类动画只需要处理播放预定义的内容,用户不会——或不需要——直接与内容交互。视频、GIF,或者以有限的方式,动画矢量可绘制对象或lottie文件通常属于此类。MotionLayout 并不专门尝试处理此类动画(但当然可以将们包含在 MotionLayout 中)。


4、依赖


确保constraintlayout版本>=2.0.0即可



build.gradle



dependencies {
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
}
//or
dependencies {
implementation("androidx.constraintlayout:constraintlayout:2.1.3")
}

5、ConstraintSet


如果使用ConstraintLayout并不够多,那对ConstraintSets的认识可能不够完善,我们也展开说说


ConstraintSets包含了一个或多个约束关系,每个约束关系定义了一个视图与其父布局或其他视图之间的位置关系。通过使用ConstraintSets,开发者可以在运行时更改布局的约束关系,从而实现动画或动态变化的布局效果。


比如ConstraintSets包含了以下方法:



  1. clone():克隆一个ConstraintSet实例。

  2. clear():清除所有的约束关系。

  3. connect():连接一个视图与其父布局或其他视图之间的约束关系。

  4. center():将一个视图水平或垂直居中于其父布局或其他视图。

  5. create():创建一个新的ConstraintSet实例。

  6. constrain*():约束一个视图的位置、大小、宽高比、可见性等属性。

  7. applyTo():将约束关系应用到一个ConstraintLayout实例。


还有更多方法就不一一列举了


只使用 ConstraintSet 和 TransitionManager 来实现一个平移动画



fragment_motion_01_basic.xml




<androidx.constraintlayout.widget.ConstraintLayout   
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/cl_container"
android:layout_width="match_parent"
android:layout_height="match_parent">

<View
android:id="@+id/button"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_marginStart="8dp"
android:background="@color/orange"
android:text="Button"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>


// 定义起始状态的 ConstraintSet (clContainer顶层容器)
val startConstraintSet = ConstraintSet()
startConstraintSet.clone(clContainer)
// 定义结束状态的 ConstraintSet
val endConstraintSet = ConstraintSet()
endConstraintSet.clone(clContainer)
endConstraintSet.connect(
R.id.button,
ConstraintSet.END,
ConstraintSet.PARENT_ID,
ConstraintSet.END,
8.dp
)
endConstraintSet.setHorizontalBias(R.id.button,1f)
clContainer.postDelayed({
// 在需要执行动画的地方
TransitionManager.beginDelayedTransition(clContainer)
// 设置结束状态的 ConstraintSet
endConstraintSet.applyTo(clContainer)
}, 1000)

我们首先使用 ConstraintSet.clone() 方法来创建起始状态的 ConstraintSet。然后,我们通过 ConstraintSet.clone() 和 ConstraintSet.connect() 方法来创建结束状态的 ConstraintSet,其中 connect() 方法用于连接视图到另一个视图或父容器的指定位置。在这里,我们将按钮连接到父容器的右端(左端在布局中已经声明了),从而使其水平居中。接着我们使用setHorizontalBias使其水平居右。


在需要执行动画的地方,我们调用 TransitionManager.beginDelayedTransition() 方法告诉系统要开始执行动画。然后,我们将结束状态的 ConstraintSet 应用到 MotionLayout 中,从而实现平滑的过渡。


图片转存失败,建议将图片保存下来直接上传


ConstraintSet 的一般思想是它们封装了布局的所有定位规则;由于您可以使用多个 ConstraintSet,因此您可以即时决定将哪组规则应用于您的布局,而无需重新创建您的视图——只有它们的位置/尺寸会改变。


MotionLayout 基本上建立在这个想法之上,并进一步扩展了这个概念


6、引用现有布局


在第5点中,我们新建了一个xml,我们继续使用,不过需要将androidx.constraintlayout.widget.ConstraintLayout修改为androidx.constraintlayout.motion.widget.MotionLayout



fragment_motion_01_basic.xml



<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.motion.widget.MotionLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">

<View
android:id="@+id/button"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_marginStart="8dp"
android:background="@color/orange"
android:text="Button"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.motion.widget.MotionLayout>

你会得到一个错误


image-20230407090719919

 靠着强大的编辑器,生成一个


image-20230407090719919

你就会得到下面这个和一个新的xml文件


<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.motion.widget.MotionLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutDescription="@xml/motion_layout_01_scene">

</androidx.constraintlayout.motion.widget.MotionLayout>

也就是一个MotionScene文件



motion_layout_01_scene.xml



<?xml version="1.0" encoding="utf-8"?>
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
</MotionScene>

这里我们先用再新建两个xml,代表开始位置和结束位置



motion_01_cl_start.xml



<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<View
android:id="@+id/button"
android:background="@color/orange"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_marginStart="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>


motion_01_cl_end.xml



<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<View
android:id="@+id/button"
android:background="@color/orange"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_marginEnd="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

修改一下



motion_layout_01_scene.xml



<?xml version="1.0" encoding="utf-8"?>
<MotionScene xmlns:motion="http://schemas.android.com/apk/res-auto">
<!-- Transition 定义动画过程中的开始状态和结束状态 -->
<!-- constraintSetStart 动画开始状态的布局文件引用 -->
<!-- constraintSetEnd 动画结束状态的布局文件引用 -->
<Transition
motion:constraintSetEnd="@layout/motion_01_cl_end"
motion:constraintSetStart="@layout/motion_01_cl_start"
motion:duration="1000">
<!--OnClick 用于处理用户点击事件 -->
<!--targetId 设置触发点击事件的组件 -->
<!--clickAction 设置点击操作的响应行为,这里是使动画过渡到结束状态 -->
<OnClick
motion:clickAction="toggle"
motion:targetId="@+id/button" />
</Transition>
</MotionScene>

部分解释都在注释中啦。好了 ,运行吧。


图片转存失败,建议将图片保存下来直接上传


这里的TransitionOnClick我们先按下不表。


7、独立的 MotionScene


上面的例子中,我们使用了两个XML+一个原有的布局为基础,进行的修改。最终重用您可能已经拥有的布局。MotionLayout 还支持直接在目录中的 MotionScene 文件中描述 ConstraintSet res/xml


我们在res/xml目录中新建一个xml文件



motion_layout_02_scene.xml



<?xml version="1.0" encoding="utf-8"?>
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:motion="http://schemas.android.com/apk/res-auto">

<Transition
motion:constraintSetStart="@+id/start"
motion:constraintSetEnd="@+id/end"
motion:duration="1000">
<!--OnClick 用于处理用户点击事件 -->
<!--targetId 设置触发点击事件的组件 -->
<!--clickAction 设置点击操作的响应行为,这里是使动画过渡到结束状态 -->
<OnClick
motion:clickAction="toggle"
motion:targetId="@+id/button" />
</Transition>

<ConstraintSet android:id="@+id/start">
<Constraint
android:id="@+id/button"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_marginStart="8dp"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintStart_toStartOf="parent"
motion:layout_constraintTop_toTopOf="parent" />
</ConstraintSet>

<ConstraintSet android:id="@+id/end">
<Constraint
android:id="@+id/button"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_marginEnd="8dp"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintEnd_toEndOf="parent"
motion:layout_constraintTop_toTopOf="parent" />
</ConstraintSet>

</MotionScene>

首先,在 <MotionScene> 标签内定义了两个 <ConstraintSet>,分别代表动画的开始状态(start)和结束状态(end)。每个 <ConstraintSet> 内包含一个 <Constraint>,用于描述一个界面组件(如按钮或文本框)的属性。


<Transition> 标签中,我们通过 app:constraintSetStartapp:constraintSetEnd 属性指定了动画的起始和终止状态。在这个简单的示例中,我们没有插值器等属性,但可以通过添加相应的属性(如 android:durationapp:interpolator 等)来自定义动画效果。


运行一下,一样


图片转存失败,建议将图片保存下来直接上传


7、注意



  1. ConstraintSet 用于替换受影响View的所有现有约束。

  2. 每个 Constraint 元素应包含要应用于View的所有约束。

  3. ConstraintSet 不是应用增量约束,而是清除并仅应用指定的约束。

  4. 对于只有一个View需要动画的场景,MotionScene 中的 ConstraintSet 只需包含该View的 Constraint。

  5. 可以看出 MotionScene 定义和之前是相同的,但是我们将开始和结束 ConstraintSet 的定义直接放在文件中。与普通布局文件的主要区别在于我们不指定此处使用的View的类型,而是将约束作为元素的属性。


8、AndroidStudio预览工具


Android Studio 支持预览 MotionLayout,可以使用设计模式查看并编辑 MotionLayout


Snipaste_2023-04-11_14-25-19


标号含义如下



  1. 点击第一个你可以看到,当前页面的具有IDimage-20230411160718057

  2. 点击第二个,可以看到起始动画的位置 image-20230411160815353

  3. 点击第三个,可以看到终止动画的位置 image-20230411160808841

  4. 第四个,可以操作动画的预览,暂停,播放,加速,拖动,等等。

  5. 而你可以看到途中有一条线,可以使用tools:showPaths="true"开启


9、补充


今天回过来一看,示例还是少了,我稍微加几个


<?xml version="1.0" encoding="utf-8"?>
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:motion="http://schemas.android.com/apk/res-auto">

   <Transition
       motion:constraintSetEnd="@+id/end"
       motion:constraintSetStart="@+id/start"
       motion:duration="1000">
       <!--OnClick 用于处理用户点击事件 -->
       <!--targetId 设置触发点击事件的组件 -->
       <!--clickAction 设置点击操作的响应行为,这里是使动画过渡到结束状态 -->
       <OnSwipe
           motion:dragDirection="dragEnd"
           motion:touchAnchorId="@+id/button1"
           motion:touchAnchorSide="end" />

   </Transition>

   <ConstraintSet android:id="@+id/start">

       <Constraint
           android:id="@+id/button1"
           android:layout_width="64dp"
           android:layout_height="64dp"
           android:layout_marginStart="8dp"
           motion:layout_constraintBottom_toTopOf="@id/button2"
           motion:layout_constraintStart_toStartOf="parent"
           motion:layout_constraintTop_toTopOf="parent" />

       <Constraint
           android:id="@+id/button2"
           android:layout_width="64dp"
           android:layout_height="64dp"
           android:layout_marginStart="8dp"
           android:alpha="1"
           motion:layout_constraintBottom_toTopOf="@id/button3"
           motion:layout_constraintStart_toStartOf="parent"
           motion:layout_constraintTop_toBottomOf="@id/button1" />

       <Constraint
           android:id="@+id/button3"
           android:layout_width="64dp"
           android:layout_height="64dp"
           android:layout_marginStart="8dp"
           android:rotation="0"
           motion:layout_constraintBottom_toTopOf="@id/button4"
           motion:layout_constraintStart_toStartOf="parent"
           motion:layout_constraintTop_toBottomOf="@id/button2" />

       <Constraint
           android:id="@+id/button4"
           android:layout_width="64dp"
           android:layout_height="64dp"
           android:layout_marginStart="8dp"
           android:elevation="0dp"
           motion:layout_constraintBottom_toTopOf="@id/button5"
           motion:layout_constraintStart_toStartOf="parent"
           motion:layout_constraintTop_toBottomOf="@id/button3" />

       <Constraint
           android:id="@+id/button5"
           android:layout_width="64dp"
           android:layout_height="64dp"
           android:layout_marginStart="8dp"
           android:scaleX="1"
           android:scaleY="1"
           motion:layout_constraintBottom_toBottomOf="parent"
           motion:layout_constraintStart_toStartOf="parent"
           motion:layout_constraintTop_toBottomOf="@id/button4" />
   </ConstraintSet>

   <ConstraintSet android:id="@+id/end">
       <Constraint
           android:id="@+id/button1"
           android:layout_width="64dp"
           android:layout_height="64dp"
           android:layout_marginEnd="8dp"
           motion:layout_constraintEnd_toEndOf="parent"
           motion:layout_constraintHorizontal_bias="1"
           motion:layout_constraintBottom_toTopOf="@id/button2"
           motion:layout_constraintStart_toStartOf="parent"
           motion:layout_constraintTop_toTopOf="parent" />

       <Constraint
           android:id="@+id/button2"
           android:layout_width="64dp"
           android:layout_height="64dp"
           android:layout_marginStart="8dp"
           android:alpha="0.2"
           motion:layout_constraintBottom_toTopOf="@id/button3"
           motion:layout_constraintHorizontal_bias="1"
           motion:layout_constraintEnd_toEndOf="parent"
           motion:layout_constraintStart_toStartOf="parent"
           motion:layout_constraintTop_toBottomOf="@id/button1" />


       <Constraint
           android:id="@+id/button3"
           android:layout_width="64dp"
           android:layout_height="64dp"
           motion:layout_constraintHorizontal_bias="1"
           android:layout_marginStart="8dp"
           android:rotation="360"
           motion:layout_constraintBottom_toTopOf="@id/button4"
           motion:layout_constraintEnd_toEndOf="parent"
           motion:layout_constraintStart_toStartOf="parent"
           motion:layout_constraintTop_toBottomOf="@id/button2" />

       <Constraint
           android:id="@+id/button4"
           android:layout_width="64dp"
           android:layout_height="64dp"
           android:layout_marginStart="8dp"
           android:elevation="10dp"
           motion:layout_constraintBottom_toTopOf="@id/button5"
           motion:layout_constraintEnd_toEndOf="parent"
           motion:layout_constraintHorizontal_bias="1"
           motion:layout_constraintStart_toStartOf="parent"
           motion:layout_constraintTop_toBottomOf="@id/button3" />

       <Constraint
           android:id="@+id/button5"
           android:layout_width="64dp"
           android:layout_height="64dp"
           android:layout_marginStart="8dp"
           android:scaleX="2"
           motion:layout_constraintHorizontal_bias="1"
           android:scaleY="2"
           motion:layout_constraintBottom_toBottomOf="parent"
           motion:layout_constraintEnd_toEndOf="parent"
           motion:layout_constraintStart_toStartOf="parent"
           motion:layout_constraintTop_toBottomOf="@id/button4" />
   </ConstraintSet>

</MotionScene>

其余部分就不一一展示了,因为你们肯定都知道啦。效果如下


2023-04-12_11-49-35 (1)


10、下个篇章


因为篇幅原因,我们先到这,这篇文章,只是了解一下,下一篇我们将会深入了解各种没有详细讲解的情况。


如果您有任何疑问、对文章写的不满意、发现错误或者有更好的方法,欢迎在评论、私信或邮件中提出,非常感谢您的支持。🙏


11、感谢



  1. 校稿:ChatGpt

  2. 文笔优化:ChatGpt

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

同一页面多次调用图形验证码

缘由一个页面需要两个验证码,使用同一个验证码调用两次会导致有前一个失效。那么我们需要创建不同的两个验证码,分别做验证。截图展示具体实现同时引入多个KgCaptcha的js。引入多个JS时,请定义 plural 参数;通过该参数区分定义对象名...
继续阅读 »

缘由

一个页面需要两个验证码,使用同一个验证码调用两次会导致有前一个失效。那么我们需要创建不同的两个验证码,分别做验证。


截图展示



具体实现

  • 同时引入多个KgCaptcha的js。
  • 引入多个JS时,请定义 plural 参数;通过该参数区分定义对象名,如plural=1,则对象名为kg1,以此类推。
<script src="captcha.js?appid=XXX&plural=1" id="KgCaptcha1"></script>
<script src="captcha.js?appid=XXX&plural=2" id="KgCaptcha2"></script>
  • 初始化验证码
<script type="text/javascript">

// 第一个验证码
kg1.captcha({
// 绑定元素,验证框显示区域
bind: "#captchaBox1",
// 验证成功事务处理
success: function(e) {
console.log(e);
},
// 验证失败事务处理
failure: function(e) {
console.log(e);
},
// 点击刷新按钮时触发
refresh: function(e) {
console.log(e);
}
});

// 第二个验证码
kg2.captcha({
// 绑定元素,验证框显示区域
bind: "#captchaBox2",
// 验证成功事务处理
success: function(e) {
console.log(e);
},
// 验证失败事务处理
failure: function(e) {
console.log(e);
},
// 点击刷新按钮时触发
refresh: function(e) {
console.log(e);
}
});

</script>

  • 创建验证码框区域
<!-- 第一个验证码 -->
<div id="captchaBox1"></div>
<!-- 第二个验证码 -->
<div id="captchaBox2"></div>


总结

SDK开源地址:https://github.com/KgCaptcha,顺便做了一个演示:https://www.kgcaptcha.com/demo/

收起阅读 »

一个Node.js图形验证码的生成

效果图准备访问KgCaptcha网站,注册账号后登录控制台,访问“无感验证”模块,申请开通后系统会分配给应用一个唯一的AppId、AppSecret。提供后端SDK来校验token(即安全凭据)是否合法 ,目前支持PHP版、Python版、Java/JSP版、...
继续阅读 »

效果图


准备

  • 访问KgCaptcha网站,注册账号后登录控制台,访问“无感验证”模块,申请开通后系统会分配给应用一个唯一的AppId、AppSecret。
  • 提供后端SDK来校验token(即安全凭据)是否合法 ,目前支持PHP版、Python版、Java/JSP版、.Net C#版。
  • 访问Node.js官网,下载Node.js运行环境,访问Vue.js中文官网,安装下载Vue.js,创建一个Vue项目,具体操作请查看Vue.js中文官网。

项目目录


index.html

项目根目录index.html文件,头部引用KgCaptcha的js。

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<!--引入凯格行为验证码js-->
<script id="KgCaptcha" src="captcha.js?appid=XXX"></script>
<!--引入凯格行为验证码js-->
</head>
<body>
<!--Vue主体-->
<div id="app"></div>
<!--Vue主体-->
</body>
</html>

main.js

src/main.js文件中,配置路由。

import Vue from 'vue'
import App from './App'
import router from './router'
Vue.config.productionTip = false
// 配置全局路由、组件
new Vue({
el: '#app',
router,
components: { App },
template: ''
})

App.vue

src/App.vue文件中,定义html。

<template>
<div id="app">
<!--自定义组件、内容-->
<form id="form">
token: <input name="token" _cke_saved_name="token" _cke_saved_name="token" _cke_saved_name="token" id="token">
<!--凯格行为验证码组件-->
<div id="captchaBox"></div>
<!--凯格行为验证码组件-->
<button type="submit">提交</button>
</form>
<!--自定义组件、内容-->
</div>
</template>

<script>
export default {
name: 'App',
}
//初始化凯格行为验证码
kg.captcha({
// 绑定元素,验证框显示区域
bind: "#captchaBox",
// 验证成功事务处理
success: function(e) {
console.log(e);
kg.$('#token').value = e['token']
},
// 验证失败事务处理
failure: function(e) {
console.log(e);
},
// 点击刷新按钮时触发
refresh: function(e) {
console.log(e);
}
});
</script>


总结

SDK开源地址:https://github.com/KgCaptcha,顺便做了一个演示:https://www.kgcaptcha.com/demo/


收起阅读 »

Vue.js 滑动拼图验证码实现笔记

背景关于验证码的使用场景还是非常多的,很多网站上的验证码可谓是五花八门,下面是我使用Vue.js实现滑动拼图验证码做的一个笔记。效果展示准备工作访问KgCaptcha网站,注册账号后登录控制台,访问“无感验证”模块,申请开通后系统会分配给应用一个唯一的AppI...
继续阅读 »

背景

关于验证码的使用场景还是非常多的,很多网站上的验证码可谓是五花八门,下面是我使用Vue.js实现滑动拼图验证码做的一个笔记。

效果展示



准备工作

  • 访问KgCaptcha网站,注册账号后登录控制台,访问“无感验证”模块,申请开通后系统会分配给应用一个唯一的AppId、AppSecret。
  • 提供后端SDK来校验token(即安全凭据)是否合法 ,目前支持PHP版、Python版、Java/JSP版、.Net C#版。
  • 访问Vue.js中文官网,复制Vue.js插件链接。
  • 注意:先HTML头部初始化行为验证码,然后HTML底部初始化Vue.js,否则KgCaptcha的js部分函数与被Vue.js发生冲突,导致失效。

实现代码

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<!--头部引入Vue.js插件-->
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<!--头部引入Vue.js插件-->
<!--头部引入行为验证码js插件-->
<script id="KgCaptcha" src="captcha.js?appid=XXX"></script>
<script>
kg.captcha({
// 绑定元素,验证框显示区域
bind: "#captchaBox",
// 验证成功事务处理
success: function(e) {
console.log(e);
kg.$('#token').value = e['token'];
},
// 验证失败事务处理
failure: function(e) {
console.log(e);
},
// 点击刷新按钮时触发
refresh: function(e) {
console.log(e);
}
});
</script>
<!--头部引入行为验证码js插件-->
</head>

<body>
<div id="app">
<!--自定义内容、Vue组件-->
token: <input name="token" id="token" />
<!--行为验证码组件-->
<div id="captchaBox"></div>
<!--行为验证码组件-->
<button type="button">提交</button>
<!--自定义内容、Vue组件-->
</div>
</body>

<!--底部运行Vue.js代码-->
<script>
var app = new Vue({
el: '#app',
})
</script>
<!--底部运行Vue.js代码-->

</html>


最后

SDK开源地址:https://github.com/KgCaptcha,顺便做了一个演示:https://www.kgcaptcha.com/demo/

收起阅读 »

就在昨天,我也关闭了朋友圈 ... ...

关于“关闭朋友圈”这个话题的文章,若干年前无意中就看到过,当我给我自己的第一个感觉是——不痛不痒。 为什么要关闭呢?每天在班车上闲来无事,看看周围的朋友们都有什么有趣的事情发生,多好啊。不懂为什么要去可以的关闭它,真的不懂。 随着年纪慢慢变大(很逃避“变老”这...
继续阅读 »


关于“关闭朋友圈”这个话题的文章,若干年前无意中就看到过,当我给我自己的第一个感觉是——不痛不痒


为什么要关闭呢?每天在班车上闲来无事,看看周围的朋友们都有什么有趣的事情发生,多好啊。不懂为什么要去可以的关闭它,真的不懂。


随着年纪慢慢变大(很逃避“变老”这个词),自己也越来越逃避社交,每到闲暇的时候,总是喜欢自己一个人听一听音乐,然后做几道LeetCode算法题,写一写算法题图解(虽然没多少人看)。但是很舒服,很自在。


再也不像十多年前,一到周末,三五好友,推杯换盏,牛皮吹得连马云都会觉得自己啥也不是。足迹也遍布了王府井、天安门、故宫、鸟巢、水立方、恭王府……


我有时在想,为什么自己越来越脱离了社会的群体了呢?反而更沉浸于自己的精神世界中,甚至一度怀疑自己患上了深度抑郁症


直到我近期读完了李笑来写的**《财富自由之路》**,被无数次的触动和震惊,人和人在认知的差距真的可以相差了一个南极到北极的距离。回首过往,自己干过的太多傻憨憨的事情,也不由自主的汗颜得低下了头


当时还是在2016年的时候吧,自己买了一辆很喜欢的车子,兴奋、激动、恨不得一时间给所有亲戚朋友打电话,告诉他们这车有多么的好,配置有多么的丰富。好像再迟了一秒钟,车子就会融化消失一样。从订车、到提车、再到洗车、再到开车去公司的路上,无数的朋友圈都在拼命的告诉周围的人,“我买车了!”嗨,现在一想。蛮尴尬的。


就像知乎有一篇帖子写到,“**朋友圈总是陷入到“羡慕别人”和“处心积虑让别人羡慕”的荒谬境地,发票圈和看票圈变得越来越无趣了”,**这句话说的多真实。


再看抖音也是一样,人均劳斯莱斯,人均2,3000万的豪宅,满地的“成功学小丑”——“我职高毕业,但是!毕业第一年,我开了麻辣烫店,净利润500万,第二年我就开劳斯莱斯库,手下团队100多人……


现在的网络充斥着太多的一夜暴复,沉浸在这种氛围下的我们也越来越浮躁了。就像我很喜欢的一个主播叫“在石250”,他在直播的时候就说“我是80后,当时网络也没那么发达,当我毕业工作的时候,看到路上开过一辆宝马3系,我都觉得牛逼得很。而现在呢,闹市街头好多小年轻用手机拍车子,你开过去一辆宝马5系,他都会摇一摇头,说上一句“这车一般,凑活事儿吧,比奔驰E300差远了!”,然后每个月领着2000块钱的工资,去网吧啃泡面。


我们被充斥了这么多网络垃圾之后,自己会受影响吗?绝对会的,而且会被毒害很深。我们发现,自己做的事情也来越沉不住气了,自己越来越觉得不月入10万都赶不上国民平均收入了,自己不买辆保时捷都没法出去跟朋友聚会了,自己家房子小于150平米那基本“狗都摇头了”。


我们该停一停了。我们需要信息,但并非这种信息。让我们自己安静下来,沉下心,静静的冲一杯绿茶,坐在窗边吹着微风,读一本我们很早就想读,但是被刷抖音和看朋友圈替代的书。坚持下去,你会发现,原来世界如此美好。自己的精神世界那么的安宁,外面世界的浮躁气息突然的这么让你嗤之以鼻。


说了很多,是的。就在昨天,我也关闭了朋友圈,去开始迎接一个全新的世界。在那个世界里,只有安宁、祥和、暖风和青云~



作者:爪哇缪斯
来源:juejin.cn/post/7221418382108622906

收起阅读 »

uniapp 手机号码一键登录保姆级教程

背景 通过uniapp来开发App,目前内部上架的App产品现有的登录方式有「账号/密码」 和 「手机号/验证码」两种登录方式;但这两种方式还是不够便捷,目前「手机号一键登录」是替代短信验证登录的下一代登录验证方式,能消除现有短信验证模式等待时间长、操作繁琐和...
继续阅读 »

背景


通过uniapp来开发App,目前内部上架的App产品现有的登录方式有「账号/密码」 和 「手机号/验证码」两种登录方式;但这两种方式还是不够便捷,目前「手机号一键登录」是替代短信验证登录的下一代登录验证方式,能消除现有短信验证模式等待时间长、操作繁琐和容易泄露的痛点。


因此,结合市面上的主流App应用,以及业务方的需求,我们的App产品也需要增加「手机号一键登录」功能。 DCloud联合个推公司整合了三大运营商网关认证的服务,通过运营商的底层SDK,实现App端无需短信验证码直接获取手机号。


uni官方提供了对接的方案文档,可自行查阅,也可继续阅读本文


准备工作


1 目前支持的版本及运营商



  • 支持版本:HBuilderX 3.0+

  • 支持项目类型:uni-app的App端,5+ App,Wap2App

  • 支持系统平台: Android,iOS

  • 支持运营商: 中国移动,中国联通,中国电信


2 费用


2.1 运营商费用

目前一键登录收费规则为每次登录成功请求0.02元,登录失败则不计费。


2.2 云空间费用

开通uniCloud是免费的,其中阿里云是全免费,腾讯云是提供一个免费服务空间。


阿里云

选择阿里云作为服务商时,服务空间资源完全免费,每个账号最多允许创建50个服务空间。阿里云目前处于公测阶段,如有正式业务对稳定性有较高要求建议使用腾讯云。


image.png


阿里云的服务空间是纯免费的。但为避免资源滥用,有一些限制,见下:


image.png



除上面的描述外,阿里云没有其他限制。
因为阿里云免费向DCloud提供了硬件资源,所以DCloud也没有向开发者收费。如果阿里云后续明确了收费计划,DCloud也会第一时间公布。



腾讯云

选择腾讯云作为服务商时,可以创建一个免费的服务空间,资源详情参考腾讯云免费额度;如想提升免费空间资源配额,或创建更多服务空间,则需付费购买。


image.png


2.3 云函数费用

如果你的一键登录业务平均每天获取手机号次数为10000次,使用阿里云正式版云服务空间后,对应云函数每天大概消耗0.139元


接入


1 重要前置条件



  • 手机安装有sim卡

  • 手机开启数据流量(与wifi无关,不要求关闭wifi,但数据流量不能禁用。)

  • 开通uniCloud服务(但不要求所有后台代码都使用uniCloud)

  • 开发者需要登录 DCloud开发者中心,申请开通一键登录服务。


2 开发者中心-开通一键登录服务


此官方文档详细步骤开通一键登录服务,开通后将当前项目加入一键登录内,审核2-3天;


3 开通uniCloud


一键登录在客户端获取 access_token 后,必须通过调用uniCloud中云函数换取手机号码,
所以需要开通uniCould;


登录uniCloud中web控制台里,新建服务空间,开通uniCloud


在uniCloud的云函数中拿到手机号后,可以直接使用,也可以再转给传统服务器处理,也可以通过云函数url化方式生成普通的http接口给5+ App使用。


4 客户端-一键登录


当前项目关联云空间

项目名称点击右键,创建云环境,创建的云环境应与之前开通的云空间类型保持一致,我这里选择腾讯云;


image.png


创建好后当前项目下会多个文件夹「uniCloud」,点击右键关联创建好的云空间


image.png


image.png


关联成功


image.png


获取可用的服务提供商(暂时作用不大)

一键登录对应的 provider ID为 'univerify',当获取provider列表时发现包含 'univerify' ,则说明当前环境打包了一键登录的sdk;


uni.getProvider({
service: 'oauth',
success: function (res) {
console.log(res.provider)// ['qq', 'univerify']
}
});

参考文档


预登录(可选)

预登录操作可以判断当前设备环境是否支持一键登录,如果能支持一键登录,此时可以显示一键登录选项;


uni.preLogin({
provider: 'univerify',
success(){ //预登录成功
// 显示一键登录选项
},
fail(res){ // 预登录失败
// 不显示一键登录选项(或置灰)
// 根据错误信息判断失败原因,如有需要可将错误提交给统计服务器
console.log(res.errCode)
console.log(res.errMsg)
}
})

参考文档


请求登录授权

弹出用户授权界面。根据用户操作及授权结果返回对应的回调,拿到 access_token,此时客户端登录认证完成;设置自定义按钮等;后续「需要将此数据提交到服务器获取手机号码」


uni.login({
provider: 'univerify',
univerifyStyle: { // 自定义登录框样式
//参考`univerifyStyle 数据结构`
},
success(res){ // 登录成功 在该回调中请求后端接口,将access_token传给后端
console.log(res.authResult); // {openid:'登录授权唯一标识',access_token:'接口返回的 token'}
},
fail(res){ // 登录失败
console.log(res.errCode)
console.log(res.errMsg)
}
})

参考文档


获取用户是否选中了勾选框

新增判断是否勾选一键登录相关协议函数;


uni.getCheckBoxState({
success(res){
console.log(res.state) // Boolean 用户是否勾选了选框
console.log(res.errMsg)
},
fail(res){
console.log(res.errCode)
console.log(res.errMsg)
}
})

参考文档


用access_token换手机号

客户端获取到 access_token 后,传递给uniCloud云函数,云函数中通过uniCloud.getPhoneNumber方法获取真正的手机号。


换取手机号有三种方式:




  1. 在前端直接写 uniCloud.callFunction ,将 access_token 传给指定的云函数。但需要在「云函数内部」请求服务端接口并将电话号码传到服务器;




  2. 使用普通ajax请求提交 access_token 给uniCloud的云函数(不考虑);




  3. 使用普通ajax请求提交 access_token 给自己的传统服务器,通过自己的传统服务器再转发给 uniCloud 云函数。但uniCloud上的「云函数需要做URL化」;




我们目前使用的是第三种,防止电话号码暴露到前端,通过java小伙伴去请求uniCloud云函数,返回电话号码给后端;


// 云函数验证签名,此示例中以接受GET请求为例作演示
const crypto = require('crypto')
exports.main = async(event) => {

const secret = 'your-secret-string' // 自己的密钥不要直接使用示例值,且注意不要泄露
const hmac = crypto.createHmac('sha256', secret);

let params = event.queryStringParameters
const sign = params.sign
delete params.sign
const signStr = Object.keys(params).sort().map(key => {
return `${key}=${params[key]}`
}).join('&')

hmac.update(signStr);

if(sign!==hmac.digest('hex')){
throw new Error('非法访问')
}

const {
access_token,
openid
} = params
const res = await uniCloud.getPhoneNumber({
provider: 'univerify',
appid: 'xxx', // DCloud appid,不同于callFunction方式调用,使用云函数Url化需要传递DCloud appid参数
apiKey: 'xxx', // 在开发者中心开通服务并获取apiKey
apiSecret: 'xxx', // 在开发者中心开通服务并获取apiSecret
access_token: access_token,
openid: openid
})
// 返回手机号给自己服务器
return res
}

res结果


{
"data": {
"code": 0,
"success": true,
"phoneNumber": "166xxxx6666"
},
"statusCode": 200,
"header": {
"Content-Type": "application/json; charset=utf-8",
"Connection": "keep-alive",
"Content-Length": "53",
"Date": "Fri, 06 Nov 2020 08:57:21 GMT",
"X-CloudBase-Request-Id": "xxxxxxxxxxx",
"ETag": "xxxxxx"
},
"errMsg": "request:ok"
}

参考文档


客户端关闭一键登录授权界面

请求登录认证操作完成后,不管成功或失败都不会关闭一键登录界面,需要主动调用closeAuthView方法关闭。完成业务服务登录逻辑后通知客户端关闭登录界面。


uni.closeAuthView()

参考文档


错误码

一键登录相关的错误码


但其中状态码30006,官方未给出相关的说明,但与相关技术沟通得知,该状态码是运营商返回的,大概率是网络信号不好,或者其它等原因造成的,没办法修复,只能是想办法兼容改错误;


目前我们的兼容处理方案是:程序检测判断如果出现该状态码,则关闭一键登录授权页面,并跳转到原有的「手机号验证码」登录页面


参考文档


5 云函数-一键登录


自HBuilderX 3.4.0起云函数需启用uni-cloud-verify之后才可以调用getPhoneNumber接口,扩展库uni-cloud-verify


需要在云函数的package.json内添加uni-cloud-verify的引用即可为云函数启用此扩展,无需做其他调整,因为HbuilderX内部已经集成了该扩展库,只需引入即可,不用安装,代码如下:


{
"name": "univerify",
"extensions": {
"uni-cloud-verify": {} // 启用一键登录扩展,值为空对象即可
}
}

参考文档


6 运行基座和打包


使用uni一键登录,不需要制作自定义基座,使用HBuilder标准真机运行基座即可。在云函数中配置好apiKey、apiSecret后,只要一键登录成功,就会从你的账户充值中扣费。


在菜单中配置模块权限


image.png


参考文档


需要注意的问题


1. 开通手机号一键登录是否同时需要开通苹果登录?


目前只开通手机号一键登录,未开通苹果登录,在我们项目里是可以的,但是App云打包时是会弹框提示的,但是并不影响项目在App Store中发布;


2. 如果同一个token多次反复获取手机号会重复扣费么?


不会,这种场景应该仅限于联调测试使用,正式上线每次都应该获取最新token,避免过期报错;


3. access_token过期时间



  • token过期时间是10分钟

  • 每次请求获取手机号接口时,都应该从客户端获取最新的token

  • 在取号成功时进行扣费,获取token不计费


4. 预登录有效期


预登录有效期为10分钟,超过10分钟后预登录失效,此时调用login授权登录相当于之前没有调用过预登录,大概需要等待1-2秒才能弹出授权界面。 预登录只能使用一次,调用login弹出授权界面后,如果用户操作取消登录授权,再次使用一键登录时需要重新调用预登录。


作者:Wendy的小帕克
来源:juejin.cn/post/7221422131857506359
收起阅读 »

整个活儿~永远加载不满的进度条

web
前言各位开发大佬,平时肯定见到过这种进度条吧,一直在加载,但等了好久都是在99% 如下所示: 有没有好奇这个玩意儿咋做的呢? 细听分说 (需要看使用:直接看实践即可)fake-progress如果需要实现上面的这个需求,其实会涉及到fake-progre...
继续阅读 »

前言

各位开发大佬,平时肯定见到过这种进度条吧,一直在加载,但等了好久都是在99%

如下所示:

有没有好奇这个玩意儿咋做的呢?
细听分说 (需要看使用:直接看实践即可)

fake-progress

如果需要实现上面的这个需求,其实会涉及到fake-progress这个库,具体是干嘛的呢?
这个库会提供一个构造函数,创建一个实例对象后,里面的属性会给我们进度条需要的数据等信息。
如图所示:


fake-progress库的源码如下:

/**
* Represents a fakeProgress
* @constructor
* @param {object} options - options of the contructor
* @param {object} [options.timeConstant=1000] - the timeConstant in milliseconds (see https://en.wikipedia.org/wiki/Time_constant)
* @param {object} [options.autoStart=false] - if true then the progress auto start
*/

const FakeProgress = function (opts) {
 if (!opts) {
   opts = {};
}
 // 时间快慢
 this.timeConstant = opts.timeConstant || 1000;
 // 自动开始
 this.autoStart = opts.autoStart || false;
 this.parent = opts.parent;
 this.parentStart = opts.parentStart;
 this.parentEnd = opts.parentEnd;
 this.progress = 0;
 this._intervalFrequency = 100;
 this._running = false;
 if (this.autoStart) {
   this.start();
}
};

/**
* Start fakeProgress instance
* @method
*/

FakeProgress.prototype.start = function () {
 this._time = 0;
 this._intervalId = setInterval(
   this._onInterval.bind(this),
   this._intervalFrequency
);
};

FakeProgress.prototype._onInterval = function () {
 this._time += this._intervalFrequency;
 this.setProgress(1 - Math.exp((-1 * this._time) / this.timeConstant));
};

/**
* Stop fakeProgress instance and set progress to 1
* @method
*/

FakeProgress.prototype.end = function () {
 this.stop();
 this.setProgress(1);
};

/**
* Stop fakeProgress instance
* @method
*/

FakeProgress.prototype.stop = function () {
 clearInterval(this._intervalId);
 this._intervalId = null;
};

/**
* Create a sub progress bar under the first progres
* @method
* @param {object} options - options of the FakeProgress contructor
* @param {object} [options.end=1] - the progress in the parent that correspond of 100% of the child
* @param {object} [options.start=fakeprogress.progress] - the progress in the parent that correspond of 0% of the child
*/

FakeProgress.prototype.createSubProgress = function (opts) {
 const parentStart = opts.start || this.progress;
 const parentEnd = opts.end || 1;
 const options = Object.assign({}, opts, {
   parent: this,
   parentStart: parentStart,
   parentEnd: parentEnd,
   start: null,
   end: null,
});

 const subProgress = new FakeProgress(options);
 return subProgress;
};

/**
* SetProgress of the fakeProgress instance and updtae the parent
* @method
* @param {number} progress - the progress
*/

FakeProgress.prototype.setProgress = function (progress) {
 this.progress = progress;
 if (this.parent) {
   this.parent.setProgress(
    (this.parentEnd - this.parentStart) * this.progress + this.parentStart
  );
}
};

我们需要核心关注的参数只有timeConstant,autoStart这两个参数,通过阅读源码可以知道timeConstant相当于分母,分母越大则加的越少,而autoStart则是一个开关,如果开启了直接执行start方法,开启累计的定时器。
通过这个库,我们实现一个虚拟的进度条,永远到达不了100%的进度条。
但是如果这时候像接口数据或其他什么资源加载完了,要到100%了怎么办呢?可以看到代码中有end()方法,因此显示的调用下实例的end()方法即可。

实践

上面讲了这么多下面结合圆形进度条(后面再出个手写圆形进度条)来实操一下,效果如下:


代码如下所示:

<template>
 <div ref="main" class="home">
   </br>
   <div>{{ fake.progress }}</div>
   </br>
   <Progress type="circle" :percentage="parseInt(fake.progress*100)"/>
   </br></br>
   <el-button @click="stop">停止</el-button>
   </br></br>
   <el-button @click="close">关闭</el-button>
 </div>
</template>

<script>
import FakeProgress from "fake-progress";

export default {
 data() {
   return {
     fake: new FakeProgress({
       timeConstant : 6000,
       autoStart : true
    })
  };
},
 methods:{
   close() {
     this.fake.end()
  },
   stop() {
     this.fake.stop()
  }
},
};
</script>

总结

如果需要实现一个永远不满的进度条,那么你可以借助fake-progress
核心是1 - Math.exp((-1 * this._time) / this.timeConstant) 这个公式
涉及到一个数据公式: e的负无穷次方 趋近于0。所以1-e^-x永远到不了1,但趋近于1

核心原理就是:用时间做分子,传入的timeConstant做分母,通过Math.exp((-1 * this._time) / this.timeConstant) 可知,如果时间不断累积且为负值,那么Math.exp((-1 * this._time) / this.timeConstant) 就无限趋近于0。所以1 - Math.exp((-1 * this._time) / this.timeConstant) 就可以得到无限趋近于1 的值

总结,如果需要使用的话,在使用的地方创建一个实例即可(配置autoStart之后就会自动累加):

new FakeProgress({
   timeConstant : 6000,
   autoStart : true
})

如果需要操作停止或介绍使用其实例下的对应方法即可

this.fake.end()
this.fake.stop()

作者:前端xs
来源:juejin.cn/post/7219195850539057212

收起阅读 »

低代码开发,是稳扎稳打还是饮鸩止渴?

web
2023年,从业者对低代码的发展充满了想象,人们认为,未来低代码它的商业价值不可估量。据Gartner的最新报告显示,到2023年,全球低代码开发技术市场规模预计将达到269亿美元,比2022年增长19.6%。 随着数字化进入深水区,企业碎片化、个性化、临时...
继续阅读 »

2023年,从业者对低代码的发展充满了想象,人们认为,未来低代码它的商业价值不可估量。据Gartner的最新报告显示,到2023年,全球低代码开发技术市场规模预计将达到269亿美元,比2022年增长19.6%。



随着数字化进入深水区,企业碎片化、个性化、临时化的需求不断涌现,而无论传统应用还是SaaS服务,都无法满足企业的全部需求,企业组织越来越多地转向低代码开发技术,以满足对快速应用交付和高度定制的自动化工作流程不断增长的需求。


image.png


中小企业的IT基础薄弱,人才有限,自研难度很大;中大型企业虽然有专门的IT部门,但审核流程长,业务部门的需求也无法立马满足。而低代码开发,只需编写少量代码或无需代码,就可以快速生成应用程序,在理论上刚好是解决这类问题的钥匙。


全民开发


低代码确实可以满足企业大部分IT需求,普通的业务人员也能进行应用搭建,成为平台的最终用户,写更少的代码,花更少的钱,干更多的事。就算是拥有独立IT部门的中大型企业,也会存在大量临时性边缘的业务需求,低代码可以很好的应对。


image.png


目前市场上有三种类型的低代码厂家:原生厂商、应用软件厂商、云厂商。随着低代码玩家越来越多,整个赛道的竞争将越来越激烈,有从业者发出呐喊:低代码产品未来到底是继续加功能,让更多开发者进来,以此满足客户普遍需求?还是通过一些其他模块或者应用市场的方式来解决客户专业需求?


一些厂商认为应该细分领域,比如深耕CRM、进销存、OKR、人事管理等热门应用模板;还有一部分厂商认为低代码的发展应该要走一条农村包围城市的路,从小处着眼,走普遍路线,主协作,帮助产研内部进行更高效的协同和项目管理,帮助IT部门更好地与业务部门建立起协作关系即可。


image.png


所以,在低代码赛道上,未来的“分流”趋势或将越来越明显。以JNPF为代表的“轻应用”派,由表单所驱动,重视数据处理能力、快速开发能力、低门槛等。


JNPF,立足于低代码开发技术,采用主流的两大技术Java/.Net开发,专注低代码开发,有拖拽式的代码生成器,灵活的权限配置、SaaS服务,强大的接口对接,随心可变的工作流引擎。支持多端协同操作,100%提供源码,支持多种云环境部署、本地部署。


image.png


基于代码生成器,可一站式开发多端使用Web、Android、IOS、微信小程序。代码自动生成后可以下载本地,进行二次开发,有效提高整体开发效率。


开源入口:http://www.yinmaisoft.com/?from=jeuji…


已经覆盖零售、医疗、制造、银行、建筑、教育、社会治理等主流行业,一站式搭建:生产管理系统、项目管理系统、进销存管理系统、OA办公系统、人事财务等等。可以节省开发人员80%时间成本,并且有以构建业务流程、逻辑和数据模型等所需的功能。



这是看得见的价值,但也有看不见的顾虑


有人认为,低代码应用是一种“饮鸩止渴”的行为,会让部分企业觉得,数字化转型就那样,哪些业务需要,就采用低代码应用“缝缝补补”即可,最终浅尝辄止,公司的整个数字化转型停在半道,欠缺完备性、统一性以及系统性。类似的问题,或许在未来会出现,也可能会在低代码应用的迭代过程中被解决。



2023,行至水深处,低代码的路会越来越难走,但这也是黎明前必经的黑暗。稻盛和夫曾说,人生如粥,熬出至味,相信在穿过重重迷雾后,2023年低代

作者:jnpfsoft
来源:juejin.cn/post/7220696541308436541
码也将迎来新的发展。

收起阅读 »

AutoGPT太火了,无需人类插手自主完成任务,GitHub2.7万星

OpenAI 的 Andrej Karpathy 都大力宣传,认为 AutoGPT 是 prompt 工程的下一个前沿。 近日,AI 界貌似出现了一种新的趋势:自主人工智能。 这不是空穴来风,最近一个名为 AutoGPT 的研究开始走进大众视野。特斯拉前 AI...
继续阅读 »

OpenAI 的 Andrej Karpathy 都大力宣传,认为 AutoGPT 是 prompt 工程的下一个前沿。


近日,AI 界貌似出现了一种新的趋势:自主人工智能

这不是空穴来风,最近一个名为 AutoGPT 的研究开始走进大众视野。特斯拉前 AI 总监、刚刚回归 OpenAI 的 Andrej Karpathy 也为其大力宣传,并在推特赞扬:「AutoGPT 是 prompt 工程的下一个前沿。」



不仅如此,还有人声称 ChatGPT 已经过时了,AutoGPT 才是这个领域的新成员。



项目一经上线,短短几天狂揽 27K + 星,这也侧面验证了项目的火爆。



GitHub 地址:github.com/torantulino…

问题来了,AutoGPT 到底是什么?它是一个实验性的开源应用程序,展示了 GPT-4 语言模型的功能。该程序由 GPT-4 驱动,可以自主实现用户设定的任何目标。



具体来说,AutoGPT 相当于给基于 GPT 的模型一个内存和一个身体。有了它,你可以把一项任务交给 AI 智能体,让它自主地提出一个计划,然后执行计划。此外其还具有互联网访问、长期和短期内存管理、用于文本生成的 GPT-4 实例以及使用 GPT-3.5 进行文件存储和生成摘要等功能。AutoGPT 用处很多,可用来分析市场并提出交易策略、提供客户服务、进行营销等其他需要持续更新的任务。

正如网友所说 AutoGPT 正在互联网上掀起一场风暴,它无处不在。很快,已经有网友上手实验了,该用户让 AutoGPT 建立一个网站,不到 3 分钟 AutoGPT 就成功了。 期间 AutoGPT 使用了 React 和 Tailwind CSS,全凭自己,人类没有插手。看来程序员之后真就不再需要编码了。



之后该用户补充说,自己的目标很简单,就是用 React 创建一个网站。提出的要求是:创建一个表单,添加标题「Made with autogpt」,然后将背景更改为蓝色。AutoGPT 成功的构建了网站。该用户还表示,如果给 AutoGPT 的 prompt 更多,表现会更好。

图源:twitter.com/SullyOmarr/…

接下里我们再看一个例子。假装自己经营一家鞋公司,给 AutoGPT 下达的命令是对防水鞋进行市场调查,然后让其给出 top5 公司,并报告竞争对手的优缺点 :



首先,AutoGPT 直接去谷歌搜索,然后找防水鞋综合评估 top 5 的公司。一旦找到相关链接,AutoGPT 就会为自己提出一些问题,例如「每双鞋的优缺点是什么、每款排名前 5 的防水鞋的优缺点是什么、男士排名前 5 的防水鞋」等。

之后,AutoGPT 继续分析其他各类网站,并结合谷歌搜索,更新查询,直到对结果满意为止。期间,AutoGPT 能够判断哪些评论可能偏向于伪造,因此它必须验证评论者。



执行过程中,AutoGPT 甚至衍生出自己的子智能体来执行分析网站的任务,找出解决问题的方法,所有工作完全靠自己。

结果是,AutoGPT 给出了 top 5 防水鞋公司的一份非常详细的报告,报告包含各个公司的优缺点,此外还给出了一个简明扼要的结论。全程只用了 8 分钟,费用为 10 美分。期间也完全没有优化。



这个能够独立自主完成任务的 AutoGPT 是如何运行的呢?我们接着来看。

AutoGPT:30 分钟内构建你自己的 AI 助手

作为风靡互联网的 AI 智能体,AutoGPT 可以在 30 分钟内完成设置。 你就可以拥有自己的 AI,协助完成任务,提升工作效率。

这一强大的 AI 工具能够自主执行各种任务,设置和启动的简便性是一大特征。在开始之前,你需要设置 Git、安装 Python、下载 Docker 桌面、获得一个 OpenAI API 密钥。

克隆存储库

首先从 GitHub 中克隆 AutoGPT 存储库。



使用以下命令导航到新建文件夹 Auto-GPT。



配置环境

在 Auto-GPT 文件夹中,找到.env.template 文件并插入 OpenAI API 密钥。接着复制该文件并重命名为.env。



安装 Python 包

运行以下命令,安装需要的 Python 包。



运行 Docker

运行 Docker 桌面,不需要下载任何容器,只需保证程序处于激活状态。



运行 AutoGPT



执行以下命令,运行 AutoGPT。



设置目标**

AutoGPT 虽是一个强大的工具,但并不完美。为避免出现问题,最好从简单的目标开始,对输出进行测试,并根据自身需要调整目标,如上文中的 ResearchGPT。

不过,你如果想要释放 AutoGPT 的全部潜力,需要 GPT-4 API 访问权限。GPT-3.5 可能无法为智能体或响应提供所需的深度。

AgentGPT:浏览器中直接部署自主 AI 智能体

近日,又有开发者对 AutoGPT 展开了新的探索尝试,创建了一个
可以在浏览器中组装、配置和部署自主 AI 智能体的项目 ——AgentGPT。** 项目主要贡献者之一为亚马逊软件工程师 Asim Shrestha,已在 GitHub 上获得了 2.2k 的 Stars。



AgentGPT 允许你为自定义 AI 命名,让它执行任何想要达成的目标。自定义 AI 会思考要完成的任务、执行任务并从结果中学习,试图达成目标。如下为 demo 示例:HustleGPT,设置目标为创立一个只有 100 美元资金的初创公司。



再比如 PaperclipGPT,设置目标为制造尽可能多的回形针。



不过,用户在使用该工具时,同样需要输入自己的 OpenAI API 密钥。AgentGPT 目前处于 beta 阶段,并正致力于长期记忆、网页浏览、网站与用户之间的交互。

GPT 的想象力空间还有多大,我们继续拭目以待。

参考链接: medium.com/@tsaveratto…


作者:机器之心
来源:juejin.cn/post/7221089899281580091
收起阅读 »

产品说要让excel在线编辑,我是这样做的。

web
背景 最近公司项目有需求, 某导入功能, 想让客户选完excel文件, 直接将加载到web的excel编辑器中, 修改、确认, 之后上传导入。 效果查看 选择 Luckysheet(dream-num.github.io/LuckysheetD…) ,一款...
继续阅读 »

背景


最近公司项目有需求, 某导入功能, 想让客户选完excel文件, 直接将加载到web的excel编辑器中, 修改、确认, 之后上传导入。


效果查看


Kapture 2023-04-13 at 13.37.05.gif


选择



就看到了这两个, 最后选择了Luckysheet, 看他的star比较多, 哈哈。


需求实现分析


分析一下整个流程。


其实大体就两步, 搞进去,抽离出来。


一、加载本地excel到web编辑器中


1、拿到本地excel文件流


2、转换为 Luckysheet 要的格式


3、new 一个 Luckysheet 实例, 挂在到对应标签上


完成以上就把excel加载进去了, 显示出来了。


在线编辑的事就是这个库帮咱们搞定了.


二、 从web编辑器导出文件流 上传


等客户在线编辑完成, 就需要点击一个按钮, 导出文件流, 确认并调接口上传


1、获取 Luckysheet里工作表的数据


image.png


luckysheet.getAllSheets()

2、将数据加工并使用xlsx或者exceljs导出文件流


导出为为arrayBuffer, 再将arrayBuffer转为Blob


3、调后端接口上传


开发实践


一、引入 lucky-sheet


有两种方式


1、官方文档里的cdn


这种加载有点慢


<link rel='stylesheet' href='https://cdn.jsdelivr.net/npm/luckysheet/dist/plugins/css/pluginsCss.css' />
<link rel='stylesheet' href='https://cdn.jsdelivr.net/npm/luckysheet/dist/plugins/plugins.css' />
<link rel='stylesheet' href='https://cdn.jsdelivr.net/npm/luckysheet/dist/css/luckysheet.css' />
<link rel='stylesheet' href='https://cdn.jsdelivr.net/npm/luckysheet/dist/assets/iconfont/iconfont.css' />
<script src="https://cdn.jsdelivr.net/npm/luckysheet/dist/plugins/js/plugin.js"></script>
<script src="https://cdn.jsdelivr.net/npm/luckysheet/dist/luckysheet.umd.js"></script>

2、自己打包, 传到oss, 引入(推荐)



第一种第三方的cdn不稳定, 有时候很慢,还是建议,拉他的仓库,然后打个包,传到自己静态资源库, 来使用



npm run builddist 传上去使用


二、指定容器


<div id="luckysheet"></div>

三、导入本地文件


1、 用elment的上传文件组件 选择文件


但是这里不上传,仅仅是用它选择文件拿到文件对象File


<div class="import-okr">
<!-- ,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet -->
<el-upload
v-model:file-list="fileList"
accept="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
class="upload-demo"
:before-upload="beforeUpload"
action=""
:show-file-list="false"
>
<button @click="uploadFile">上传数据</button>
</el-upload>
</div>

2、beforeUpload 方法拿到文件


const beforeUpload = (file) => {
console.log(file)
}

image.png


3、将文件流转换为lucky要的格式


github.com/dream-num/L…


安装转换工具


npm install luckyexcel

使用


// After getting the xlsx file
LuckyExcel.transformExcelToLucky(file,
function(exportJson, luckysheetfile){
// exportJson就是转换后的数据
},
function(error){
// handle error if any thrown
}

4、将转换后的数据创建表格


// 将拿到的数据创建表格
luckysheet.create({
container: 'luckysheet', // luckysheet is the container id
data:exportJson.sheets,
title:exportJson.info.name,
userInfo:exportJson.info.creator,
lang: 'zh', // 设定表格语言
myFolderUrl: window.location.href,
showtoolbarConfig: {
pivotTable: false, //'数据透视表'
// protection: false, // '工作表保护'
print:false, // '打印'
image: false, // 插入图片
},
showinfobar: false,
options: {
// 其他配置
userImage:'http://qzz-static.forwe.store/public-assets/pgy_kj_pic_logo.png?x-oss-process=image/resize,m_fill,w_72,h_72', // 头像url
userName:'Lucky', // 用户名
}
});

完整代码


const beforeUpload = (file) => {
console.log(file)
// 转换工具, 将文件流转换为lucky要的格式
LuckyExcel2.transformExcelToLucky(
file,
function(exportJson, luckysheetfile){
isShowExcel.value = true
console.log(exportJson)
nextTick(() => {
window.luckysheet.destroy();
// 将拿到的数据创建表格
luckysheet.create({
container: 'luckysheet', // luckysheet is the container id
data:exportJson.sheets,
title:exportJson.info.name,
userInfo:exportJson.info.creator,
lang: 'zh', // 设定表格语言
myFolderUrl: window.location.href,
showtoolbarConfig: {
pivotTable: false, //'数据透视表'
// protection: false, // '工作表保护'
print:false, // '打印'
image: false, // 插入图片
},
showinfobar: false,
options: {
// 其他配置
userImage:'http://qzz-static.forwe.store/public-assets/pgy_kj_pic_logo.png?x-oss-process=image/resize,m_fill,w_72,h_72', // 头像url
userName:'Lucky', // 用户名
}
});
})
},
function(err){
logger.error('Import failed. Is your fail a valid xlsx?');
});
}

四、导出


1、利用 luckysheet.getAllSheets() 获取表数据


console.log(luckysheet.getAllSheets())

image.png


2、exceljs将上述对象转换为excel文件流


import Excel  from 'exceljs'
// 导出excel
const exportExcel = async function (luckysheet) { // 参数为luckysheet.getluckysheetfile()获取的对象
// 1.创建工作簿,可以为工作簿添加属性
const workbook = new Excel.Workbook()
// 2.创建表格,第二个参数可以配置创建什么样的工作表
luckysheet.every(function (table) {
if (table.data.length === 0) return true
const worksheet = workbook.addWorksheet(table.name)
// 3.设置单元格合并,设置单元格边框,设置单元格样式,设置值
setStyleAndValue(table.data, worksheet)
setMerge(table.config.merge, worksheet)
setBorder(table.config.borderInfo, worksheet)
return true
})
// 4.写入 buffer
const buffer = await workbook.xlsx.writeBuffer()
return buffer
}

3、 写个方法,执行上述两步


// 保存文件
const onClickSaveFile = async ( ) => {
console.log(luckysheet.getAllSheets())
const buf = await exportExcel(luckysheet.getAllSheets())
const blob = new Blob([buf]);
// $emit('file', blob)
handleUpload(blob)
}

4、上传方法


利用formData, 将生成的文件二进制流发给后端


const handleUpload = async(file) => {
// isShowExcel.value = false
const loading = ElLoading.service({
fullscreen: true,
text: '上传中,请稍等',
background: 'rgba(0,0,0,0.1)'
});
try {
const formData = new FormData()
formData.append('file', file)
const {code, data, message } = await IMPORT_OKR(formData)
if(code === 1) {
//...
}
loading.close()
} catch (error) {
console.log(error)
loading.close()
}
}

遇到问题


1、iconfont冲突


lucky-sheet这个项目里的iconfont类名和我项目里一样,导致有些被覆盖了.
image.png


解决: 将他项目里 iconfont 换成 lucky-sheet, 相关类名也全部替换, 然后重新打包,再引入,即可解决


2、lucky-sheet层级不够高,无法编辑


image.png


elmentui和antd的一些组件层级比较高,所以, 让kucky的层级更高即可


解决: 增加下述css即可


.luckysheet-input-box { z-index: 2000; } .luckysheet-cols-menu { z-index: 2001; }

最后


妥妥的都是站在巨人的肩膀上


求赞


作者:浏览器API调用工程师
来源:juejin.cn/post/7221368910139342907
收起阅读 »

从前端变化看终身就业

接触前端已10年有余,从菜鸟到现在的老鸟;肚皮围度增长了,发量却减少了;头开始往下低了,发际线却往上走了!感叹时光易逝之际,不免有些伤感;风云突变的年代,总是少不了焦虑和无奈; 而立之年开始背负了各种债务,操很多心,最担心的莫过于父母一年年老去,小孩成长路上各...
继续阅读 »

接触前端已10年有余,从菜鸟到现在的老鸟;肚皮围度增长了,发量却减少了;头开始往下低了,发际线却往上走了!感叹时光易逝之际,不免有些伤感;风云突变的年代,总是少不了焦虑和无奈; 而立之年开始背负了各种债务,操很多心,最担心的莫过于父母一年年老去,小孩成长路上各种担心、顾虑;当然还有工作,一份安稳的、相对不错的收入是一家人生活的保障,也让希望增加了不少,今天跟大家一起聊聊工作吧!



终身就业还是终身职业



作为社会人、搬砖人,已经错过了最好的机会;也许是从来就不曾有过机会,已经失去了终生职业的可能(公务员、事业单位....);在国内很少有人能在一家企业供职到领社保,听说国外(比如德国、日本)有,我想说的是: 而今我们(在能力不是超强、又没有Backgroud的情况下)应该努力追求终生就业; 



企业想要你终生职业,但实际呢? 不管在任何一家单位工作,作为企业总是希望员工是忠诚度极高的,可以胜任公司安排的各种任务,还一心一意的工作,把公司当自己家,有奉献精神;错吗? ---当然是对的; 如果我是老板我也会这么想;


很不幸,现实和理想总是惊为天人,面对糟糕的经济形式,活下去成为企业第一要务,而精简人员总是成为企业断臂求生的一种惯用方式,现实就是残酷如此;于个人而言,在不乐观的环境中能有一份相对稳定的工作,有个可观的收入就显得极为重要,这就需要追求终生就业的能力(不论什么行业);


互联网的发展与前端



说说自己这10多年的心路历程,可能是反面教材,如果能为你带来一些参考或借鉴或一些帮助我是很高兴的;



进入编程世界,与PHP的初恋

进入编程世界,源于羡慕!(2010年)看到同学用HTML写了一个表单,当时觉得觉得很高级,很厉害; 当时他学的是PHP(号称最好的语言),所以也就不自然的被影响、认可了PHP;


转行

从高中到大学,想从事的一直是健身相关的行业和工作,但是真正接触了发现似乎跟自己设想的那么好;当然这不是真正导致转行的原因;真正原因是朋友找我(大学专业:软件工程,但是从来没有学过)做一个网站,我却屁都不会,跟亲戚、朋友说自己还是软件相关专业毕业的;所以,为了装X,也为了对得起软件工程专业那个本本(成人自考),放弃了自己研究多年的健身,毅然报了培训班学起了PHP; 诸位有没有跟我一样的呢?


痛苦的学习

大学几年浑浑噩噩的过了,去报PHP培训时候,老师说:“学过C很容易学的,PHP先从HTML开始,很容易上手的”;交钱的时候自以为学过(上课虽然在睡觉)也或多或少听了一些,没吃过猪肉还没见过猪跑啊(实际上啥都不会),应该问题不大;想起同学说(10年刚毕业)在深圳(拿6-8K),就开始幻想上了;


最难的Table + CSS布局

学习PHP的路上,最让我难堪的竟然是HTML和CSS;保守起见,老师选择了(相比DIV + CSS)更为简单的Table(用Dreamweaver拖拽) + CSS;然而,半个月过去了,竟然连写个百度首页都写不出来;呜呼哀哉,布局难难于上青天!恰逢十一国庆节,老师留的任务就是写个百度首页,如果连百度首页都写不出来,那说明不适合走这一行;结果呢? 一周过后还是写不出来,唉,每天上课时候会想到一万个放弃,回家后每一分钟都会想到N多个放弃;后来想着,钱也交了,就多坚持一下吧,就稀里糊涂的把课程学完了(HTML CSS Javascript PHP)


找到适合自己的方向

课程结束的时候,老师给个建议: PHP感觉有难度,就好好把div + css + jQuery学好,做前端、做前端、做前端!然而,入门的我还是选择了做PHP,一年多的时间学会了从切图、写页面、写PHP、写SQL语句、搭建服务器,天呐,完全飘了(实际上还是个小白),直到偶然机会(2013年)做了前端,突然找到了码页面的灵感,这种所见即所得的搬砖工作很有感觉,哈哈哈;


其实,这里想说的是:1是坚持;2是老师的层次比我高很多,他在很早就给我指明了道路,而执着于自己的愚见(当然也不全是错,也有收获),最后还是走上了老师指导的方向!


诸位,如果你们有个好的老师、高人指导,那是极为幸福的事情,一定要珍惜!


PS: (2011年)《编写高质量代码--web前端修炼之道》这本书对我前端方面的能力提升帮助非常大; 同时也感谢作者: 阿当,在我成长道路上的一些指导和帮助;


PS: 现在互联网平台很发达,在学习视频课程、阅读技术类书籍、技术资料的时候,建议可以尝试类型一下作者(译者);很多技术大牛还是很乐意给一些建议和指导的<致敬>;


学会听取建议、做出自己的判断

3年后,厚着脸皮请教老师接下来该学点啥能让薪资再增长一些,对未来有帮助; 老师给了一个方向: “Web GIS”,这一次照做了,掌握了一些Gis相关的基础,了解了Arcgis for javascript的常用方法等,结合近期的招聘,我觉得这算是很好的扩展了自己的选择;


PS: 建议菜鸟多向行业内的大牛请教,向身边段位高的朋友、同事多请教;


拥抱变化



互联网变化之快,技术更新之快,已经让很多人发出"学不动"的呼喊,但是我想说的是,只要你还想吃这碗饭,学不动还是要学;



yu6.png


学习&&提升

记得入行时前端面试:

- 会不会处理IE6、IE8兼容,有没有写过hack

- DIV + CSS 怎么实现div的垂直居中和水平居中,有几种实现方式

- 块级标签和行内标签的区别

- jQuery的prop方法和attr方法的区别

- Ajax有没有用过

- 会不会PS切图?gif和png的区别

- 什么是闭包?举个栗子


再后来学习了: 


- Bootstrap (不用了)

- AngularJs \ BackboneJS (不用了)

- requireJs \ seajs (不用了)

- grunt \ gulp \ bower(不用了)

- 响应式布局 (几乎不用了)


现在用的Vue \ React 也写了有好多年了,我想很快也会被新的所替代吧;

18年花了接近一万大洋购买了珠峰架构的课程,系统的学习了几个月,算是第二次技术比较大的提升吧,当然收入也相应的提高了一些;


PS: 想分享的是,很多技能可能生命周期很短,但是,身处当下我们还是要去积极学习,哪怕后来不用了,但是里面的一些思想会给我们未来某个时候带来很多帮助(懂得Bootstrap的设计思想就容易理解less\sass的使用,看到ElementUI、AntD等就一看就懂);


PS: 决定工作岗位、薪资的技术只是一部分,切勿过于迷恋于某个技术,跟随时代、拥抱变化,市场才是决定二者的最重要的因素!


运动&&养生

说点轻松又严肃的,各位看官,身体才是革命的本钱! 10年的老菜鸟目前除了颈椎不舒服(怪手机不怪写代码)外,其他的还好,论加班还能跟年轻人一战,哈哈哈! 这当然得益于过去多年的习惯:


- 经常跑步、爬山、健身

- 很少胡吃海喝,水果吃的多,烧烤啤酒几乎不碰;

- 每天吃饭不吃饱,原则上是不饿就行;


PS: 建议大家适当的增加运动; 如果歇了很久,要启动你的小马达,要慢慢来,勿操之过急; 最重要的是坚持;


"舍"&&"得"


舍得之间品味人生,舍得之间自有方寸;然而,舍 && 得又何其的难;



- 菜鸟期间的我是舍不得花钱买课程学习的,心疼钱啊; 后来受朋友影响开始花钱去买课程,花钱找老师学习(有的技能人家凭什么告诉你呢?),发现自己的进步突然就快了很多、收获也很大(为什么工作后就不舍得花钱学习了?);


- 知识就是金钱,如今我们知道听歌、追剧都要买VIP,为什么找工作的时候不知道购买VIP呢(我好多朋友、同事上BOSS刷招聘说每天都是那几个,殊不知买了VIP后消息就多了很多,你都没购买服务,招聘APP凭什么给你最新的资讯呢?)


- 工作、学习之余一定要花点时间去陪陪家人、运动、多走一走(哪怕是带小孩玩、哪怕去公园晒晒太阳、去商场逛逛看看美女),工作、技术很重要,人生的全部还有很多;工作是个弹力球,掉下去还有机会弹起来,而身体、家庭是玻璃球,要是碎了那就。。。


踏平坎坷成大道,路就在脚下

- 说了那么多,此刻会想什么呢? 代码要一行一行的写,日子还得一天一天的过,我曾因为负债累累(每个月却只发一次工资)而着急,然急又能如何,倒不如平静以对,正如《论语》中有云: "吾终尝日不食 终夜不寝,以思,无益,不如学也"!


- 环境不友好,是不是就没有机会了? ----当然有机会,当然有路可走! 

- 路在哪? ---- 路在脚下


前端的路该怎么走


各位,我们看到招聘APP上前端岗的需求量比往年同期少很多,这个是事实;与此同时企业还是有各种各样的需求的; 2023年了,还是以过去的思维去看(劳资会Vue 、 React),无异于缘木求鱼,那一定会让你感动悲观;何不换个思路、换个角度呢?



- 大前端方向还有很大空间: Vue\React + Flutter(或类似) + 小程序,正所谓:“山重水复疑无路 柳暗花明又一村”


- 前端 + GIS(或3D),观察BOSS上关于Webgis的招聘就知道了,如果能先于大多数人掌握了GIS、3D方面的知识,那选择是不是广阔了很多,正所谓: "有心栽花花不开 无心插柳柳成荫",何必要拘泥于某一种形态呢


- 前端架构师也是一些技术深度追求者的方向


(个人在二线城市,结合自己的经历和对Boss上岗位、薪资变化的观察,提出的拙见,欢迎批评、指导)


结语


- 强哥说了:“风浪越大鱼越贵”,挑战与机遇共存,我们应当在大变化的浪潮中调整自己的帆,拥抱惊涛骇浪和变化,磨砺出终身就业的能力!



  • 不要给自己贴标签(强哥:“我就是个卖鱼的”),现在的处境不代表未来没有机会、希望(到强盛集团);


- 编码之路上是: 路漫漫其修远兮 吾将上下而求索


- 人生道路上需要另一种气度《定风波·莫听穿林打叶声》---苏轼 : "莫听穿林打叶声 何妨吟啸且徐行; 竹杖芒鞋轻胜马,谁怕? 一蓑烟雨任平生; 料峭春风吹酒醒,微冷,山头斜照却相迎; 回首向来萧瑟处,归去, 也无风雨也无晴" 。


作者:风雪中的兔子
来源:juejin.cn/post/7220800667589197885
收起阅读 »

何不食肉糜?

21年的时候,微博上有过一番口诛笔伐,就是就是管清友建议刚开始工作的年轻人就近租房不要把时间浪费在上班的路上,要把时间利用起来投资自己,远比省下的房租划算。 视频见这里:http://www.bilibili.com/video/BV1Bb… 当时我印象非常...
继续阅读 »

21年的时候,微博上有过一番口诛笔伐,就是就是管清友建议刚开始工作的年轻人就近租房不要把时间浪费在上班的路上,要把时间利用起来投资自己,远比省下的房租划算。


视频见这里:http://www.bilibili.com/video/BV1Bb…



当时我印象非常深刻,微博评论是清一色的 “何不食肉糜”,或者说“房租你付?”


可能是因为这件事情的刺激,管清友后来才就有了“我特别知道年轻人建议专家不要建议”的言论。


对还是错?


在我看来,管清友的这个建议可以说是掏心掏肺,非常真诚,他在视频里也说了,他是基于很多实际案例才说的这些话,不是说教。


为什么我这么肯定呢?


很简单,我就是代表案例。


我第一家公司在浦东陆家嘴,四号线浦东大道地铁站旁边,我当时来上海的时候身无分文,借的家里的钱过来的,我是贫困家庭。


但,为了节约时间,我就在公司附近居住,步行五分钟,洗头洗澡都是中午回住的地方完成,晚上几乎都是11:00之后回去,倒头就睡,因为时间可以充分利用。


节约的时间,我就来研究前端技术,写代码,写文章,做项目,做业务,之前的小册(免费章节,可直接访问)我也提过,有兴趣的可以去看看。


现在回过头来看那段岁月,那是充满了感激和庆幸,自己绝对做了一个非常正确的决定,让自己的职业发展后劲十足。


所以,当看到管清友建议就近租房的建议,我是非常有共鸣的,可惜世界是参差的,管清友忽略了一个事实,那就是优秀的人毕竟是少数,知道如何主动投资自己的人也是凤毛麟角,他们根本就无法理解。


又或者,有些人知道应该要投资自己,但是就是做不到,毕竟辛苦劳累,何苦呢,做人,不就是应该开心快乐吗?


说句不好听的,有些人的时间注定就是不值钱的。


工作积极,时间长是种优势?


一周前,我写了篇文章,谈对“前端已死”的看法,其中提到了“团队下班最晚,工作最积极”可以作为亮点写在简历里。


结果有人笑出了声。



好巧的是,管清友的租房建议也有人笑了,出没出声并不知道。



也有人回复“何不食肉糜”。


这有些出乎我的意料,我只是陈述一个简单的事实,却触动了很多人的敏感神经。


我突然意识到,有些人可能有一个巨大的认知误区,就是认为工作时长和工作效率是负相关的,也就是那些按时下班的是效率高,下班晚的反而是能力不足,因为代码不熟,bug太多。



雷军说你说的很有道理,我称为“劳模”是因为我工作能力不行。


你的leader也认为你说的对,之前就是因为我每天准时下班,证明了自己的能力,所以自己才晋升的。


另外一个认知误区在于,把事实陈述当作目标指引。


如果你工作积极,是那种为自己而工作的人,你就在简历中体现,多么正常的建议,就好比,如果你是北大毕业的,那你就在简历中体现,没任何问题吧。


我可没有说让你去拼工作时长,装作工作积极,就好比我没有让你考北大一样。


你就不是这种类型的人,对吧,你连感同身受都做不到,激动个什么呢,还一大波人跟着喊666。


当然,我也理解大家的情绪,我还没毕业的时候,也在黑心企业待过,钱少事多尽煞笔,区别在于,我相对自驱力和自学能力强一些,通过自己的努力跳出了这个循环。


但大多数人还是被工作和生活推着走,所以对加班和内卷深恶痛绝,让本就辛苦的人生愈发艰难,而这种加班和内卷并没有带来收入的提升。


那问题来了,有人通过努力奋斗蒸蒸日上,有人的辛苦努力原地踏步,同样的,有的人看到建议觉得非常有用,有的人看到建议觉得何不食肉糜,区别在哪里呢?


究竟是资本作恶呢?还是自己能力不足呢?


那还要建议吗?


管清友不再打算给年轻人建议了,我觉得没必要。


虽然,大多数时候,那些听得进去建议的人大多不需要建议,而真正需要建议的又听不进,但是,那么多年轻人,总有一部分潜力股,有一些真正需要帮助的人。


他们可能因为环境等原因,有短暂的迷茫与不安,但是,来自前人发自真心的建议或许可以让他们坚定自己前进方向,从而走出不一样的人生。


就像当年我被乔布斯的那些话语激励过那般。


所以,嘲笑之人任其笑之,只要能帮助到部分人,那就有了价值。


因此,我不会停止给出那些我认为对于成长非常有帮助的建议。


(完)


作者:张鑫旭
来源:juejin.cn/post/7221487809789182008
收起阅读 »

KgCaptcha滑动拼图验证码在搜索中的作用

开头验证码应用于我们生活、工作的方方面面,比如注册登录账号、支付订单、修改密码等。下面我是在一次项目中利用滑动拼图验证码和搜索功能“合作共赢”的记录。验证码展示具体实现前端代码// 引入js<script src="captcha.js?appid=XX...
继续阅读 »

开头

验证码应用于我们生活、工作的方方面面,比如注册登录账号、支付订单、修改密码等。下面我是在一次项目中利用滑动拼图验证码和搜索功能“合作共赢”的记录。

验证码展示



具体实现

前端代码
// 引入js
<script src="captcha.js?appid=XXX"></script>
<script>
kg.captcha({
// 绑定弹窗按钮
button: "#captchaButton",

// 验证成功事务处理
success: function (e) {
// 验证成功,直接提交表单
// form1.submit();
console.log(e);
},

// 验证失败事务处理
failure: function (e) {
console.log(e);
},

// 点击刷新按钮时触发
refresh: function (e) {
console.log(e);
}
});
</script>

<a id="captchaButton"></a>



验证结果说明

 

字段名
数据类型描述
 

code
 

number
 

返回code信息
 

msg
 

string
 

验证结果信息
 

rid
 

number
 

用户的验证码应用id
 

sense
 

number
 

是否开启无感验证,0-关闭,1-开启
 

token
 

string
 

验证成功才有:token
 

weight
 

number
 

错误严重性,0正常错误,可以继续操作,1一般错误,刷新/重新加载拼图,2严重错误,错误次数过多拒绝访问


Python代码

from wsgiref.simple_server import make_server
from KgCaptchaSDK import KgCaptcha
def start(environ, response):
# 填写你的 AppId,在应用管理中获取
AppID = "AppId"
# 填写你的 AppSecret,在应用管理中获取
AppSecret = "AppSecret"
request = KgCaptcha(AppID, AppSecret)
# 填写应用服务域名,在应用管理中获取
request.appCdn = "https://cdn.kgcaptcha.com"
# 请求超时时间,秒
request.connectTimeout = 10
# 用户id/登录名/手机号等信息,当安全策略中的防控等级为3时必须填写
request.userId = "kgCaptchaDemo"
# 使用其它 WEB 框架时请删除 request.parse,使用框架提供的方法获取以下相关参数
parseEnviron = request.parse(environ)
# 前端验证成功后颁发的 token,有效期为两分钟
request.token = parseEnviron["post"].get("kgCaptchaToken", "") # 前端 _POST["kgCaptchaToken"]
# 客户端IP地址
request.clientIp = parseEnviron["ip"]
# 客户端浏览器信息
request.clientBrowser = parseEnviron["browser"]
# 来路域名
request.domain = parseEnviron["domain"]
# 发送请求
requestResult = request.sendRequest()
if requestResult.code == 0:
# 验证通过逻辑处理
html = "验证通过"
else:
# 验证失败逻辑处理
html = f"{requestResult.msg} - {requestResult.code}"
response("200 OK", [("Content-type", "text/html; charset=utf-8")])
return [bytes(str(html), encoding="utf-8")]
httpd = make_server("0.0.0.0", 8088, start) # 设置调试端口 http://localhost:8088/
httpd.serve_forever()


最后

SDK开源地址:KgCaptcha (KgCaptcha) · GitHub,顺便做了一个演示:凯格行为验证码在线体验

收起阅读 »

Java实现KgCaptcha短信验证码

背景Java是一种流行的编程语言,验证码是一种常用的网络安全技术。Java发展至今,网上也出现了各种各样的验证码,本人初学Java,下面是我用Java实现短信验证码的总结。截图展示实现代码后台接收前台的kgCaptchaToken进行验证,验证成功执行成功处理...
继续阅读 »

背景

Java是一种流行的编程语言,验证码是一种常用的网络安全技术。Java发展至今,网上也出现了各种各样的验证码,本人初学Java,下面是我用Java实现短信验证码的总结。

截图展示



实现代码

后台接收前台的kgCaptchaToken进行验证,验证成功执行成功处理,验证失败返回错误代码及信息。

package com.kyger;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;

public class demo extends HttpServlet {
private static final long serialVersionUID = 1L;

public demo() {
super();
}

protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

// 编码
request.setCharacterEncoding("utf-8");
response.setCharacterEncoding("utf-8");;
response.setContentType("text/html; charset=utf-8");

// 后台处理
if (request.getMethod().equals("POST")){
String html, appId, appSecret, Token;

// 设置 AppId 及 AppSecret,在应用管理中获取
appId = "appId";
appSecret = "appSecret";

// 填写你的 AppId 和 AppSecret,在应用管理中获取
KgCaptchaSDK KgRequest = new KgCaptchaSDK(appId, appSecret);


// 前端验证成功后颁发的 token,有效期为两分钟
KgRequest.token = request.getParameter("kgCaptchaToken");
// System.out.print(KgRequest.token);

// 填写应用服务域名,在应用管理中获取
KgRequest.appCdn = "https://cdn.kgcaptcha.com";

// 请求超时时间,秒
KgRequest.connectTimeout = 5;

// 用户登录或尝试帐号,当安全策略中的防控等级为3时必须填写,一般情况下可以忽略
// 可以填写用户输入的登录帐号(如:request.getParameter("username"),可拦截同一帐号多次尝试等行为
KgRequest.userId = "kgCaptchaDemo";

// request 对象,当安全策略中的防控等级为3时必须填写,一般情况下可以忽略
KgRequest.request = request;
// java 环境中无法提供 request 对象,请分别定义:clientIp|clientBrowser|domain 参数,即:
// KgRequest.clientIp = "127.0.0.1"; // 填写客户端IP
// KgRequest.clientBrowser = ""; // 客户端浏览器信息
// KgRequest.domain = "http://localhost"; // 你的授权域名或服务IP

// 发送验证请求
Map requestResult = KgRequest.sendRequest();
if("0".toString().equals(requestResult.get("code"))) {
// 验签成功逻辑处理 ***

// 这里做验证通过后的数据处理
// 如登录/注册场景,这里通常查询数据库、校验密码、进行登录或注册等动作处理
// 如短信场景,这里可以开始向用户发送短信等动作处理
// ...

html = "";
} else {
// 验签失败逻辑处理
html = "";
}

response.getWriter().append(html);
} else {
response.sendRedirect("index.html");
}
}

protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}

}


后端检测

后台接收数据,同时对来源及应用进行检测。

# 服务器黑名单检测
if self.auth.client_blacklist():
return self.r_code(20017) # 服务器黑名单

# 验签次数限制检测
excess = self.auth.excess(2)
if excess:
return self.r_code(code=[20020, 20021, 20022][excess - 1])

# 来路域名检测
if not self.kg["HTTP_REFERER"]: return self.r_code(20004) # 域名不合法,无法获取来路域名
if not self.auth.domain_auth(): return self.r_code(20005) # 来源域名未授权

# 应用有效时间检测
validity = self.auth.app_validity()
if validity[0] == 1: return self.r_code(20006) # 授权未开始
if validity[0] == 2: return self.r_code(20007) # 授权已结束

if self.auth.app_state(): return self.r_code(20008) # 当前应用/域名被禁用


结尾

SDK开源地址:KgCaptcha (KgCaptcha) · GitHub,顺便做了一个演示:凯格行为验证码在线体验

收起阅读 »

两个故事:送给面试失败的你

全文一共3084字,读完预计需要5分钟 故事1:从世界五百强高管手中拿下项目 那年我大二。 在北五环外的宏福校区上满一年课后,我们正式搬回了位于市中心的海淀区本部,终于算是进城了。 进城的第一个好处,就是做项目的机会变多了! 当年为了尽早的实现经济独立,课...
继续阅读 »



全文一共3084字,读完预计需要5分钟



故事1:从世界五百强高管手中拿下项目


图片


那年我大二。


在北五环外的宏福校区上满一年课后,我们正式搬回了位于市中心的海淀区本部,终于算是进城了。


进城的第一个好处,就是做项目的机会变多了!


当年为了尽早的实现经济独立,课没怎么好好上,兼职项目和实习倒是干了不少。


北邮有一个著名的BBS,上面啥都有,极其热闹。其中最热门的板块有俩,一个是“缘来如此”,听名字就知道是用来谈情说爱的。另一个则是就业机会交流板块,各种招聘内推资讯满天飞。


我当时天天刷帖子找实习和兼职机会,自然也收到不少面试邀约,而我要聊的这一次,格外的与众不同。


面试约在了晚上8点,北师大东门外的一个安静的咖啡店里,二楼。


我面试从来不迟到,一般都会掐着点到场,可那次我到的时候,面试官已经落座了。是一个四十出头的中年男人,高高瘦瘦,戴银边方框眼镜。桌上摆了一壶明亮的金丝皇菊花茶,两个小巧精致的玻璃茶杯。


我入座后,他客气的给我倒了一杯茶,面试便开始了。说是面试,其实更像是一场闲聊,整个谈话氛围非常轻松自在,咖啡店是文艺范儿的,灯光幽暗,空气里飘荡着舒缓的爵士乐和淡淡的松香,木质的楼板被过往的客人踩出咯咯吱吱的响声。


我们大约是聊了半个多小时的样子,从他的表情可以看得出他挺满意。结束谈话的时候他说,我稳重的样子不太像90后。我羞涩的笑了笑,带着他给我布置的第一个项目走出咖啡店。


没错,这个项目就这么轻而易举的被我拿到手了,也许是出于对我母校的信任,又或许是当晚的聊天氛围实在舒服,总之我获得了他的信任。


可好戏才刚刚开始。


我收到的第一个任务,要求我在一周内根据需求文档写出一份静态网站Demo出来。


这个任务现在看来很easy,但是当时的我,连最基本的网页怎么写都不会,我只用过Dreamweaver拖拖拽拽搞过一个蹩脚且丑陋的页面。而这一次,我面对的是一个商业任务,并且限定了交付日期,一周!这可把我愁坏了。甚至都不是时间紧不紧的问题,是我根本不知道从哪里下手啊。


事情的突破口出现在我大学舍友身上。


这小子是个富二代,家境优渥,从小就接触计算机,自然是见多识广,啥都会一点。我撺掇着让他给我演示“如何使用现成框架搭建网页”,当时用的是Bootstrap 1.0。我俩在通信原理的课堂上,坐在教室的后排,偷偷摸摸的教学着。我目不转睛的盯着他一点点用现成的类名,逐渐拼出了一个漂亮的页面,那种感觉真是兴奋极了!


其实现在回头看,我们当时的编码方法非常拙劣,完全上不了台面的那种,还真是一个敢教,一个敢学哈哈哈。


一节课偷偷摸摸下来,我基本上算是知道了一个静态页面如何从0到1的写出来了,剩下的事情就只是时间问题了。一下课我就飞快地跑回宿舍,埋头学习Bootstrap的API文档,一点点一点点,小心翼翼的拼凑着页面。


一周后,我如期把Demo交给了老板,保住了这次兼职机会。这份工作给我带来了每个月2000块的收入,整整持续了一年(当时我一个月生活费也就不到800)。


我是在后来的一年的交往中才知道,这个老板是某世界五百强的集团高管,空闲时间和朋友一起做了个创业公司,出于竞业关系,不方便公开招募团队,于是私下找学生兼职做点项目。


而我,就成了那个幸运的学生。


故事2:屡败屡战拿下百度Offer


图片


转眼到了大三。


杂七杂八的项目做了不少,钱也倒是有赚一些,但基本上都是小打小闹,尤其是技术层面,苦于一直没有接触过企业级项目,我并不知道一个成熟的商业项目的代码长什么样。


抱着这样一个期望,我开始争取大公司的实习机会。


还是从北邮人论坛,我拿到了一个百度的面试机会,具体哪个部门我已经记不太清了。


面试官安排我在茶水间坐下,丢给我一份试题和一支笔。


那份题目我做得稀烂,面试官看了看卷子,当场跟我说,你可能不太行。不过没等他赶我走,我迅速拿出电脑,打开作品集,指着我画的一些设计图对他说:那么……你们招UI设计师么?这是我的作品。


面试官先是一愣(心想这小子不是码农么,怎么还会设计),然后让我等一下,便拿着我的作品,跑去找他们团队的UI设计师帮忙过目。


是不是觉得我的行为很奇怪?没错,我自己也没想明白其实。当时我还没想好毕业后从事什么工作,由于从小就喜欢画点东西,平时也会看一些用户体验设计相关的书,大学期间在学生社团里做了不少有意思的图,所以我天真的以为自己毕业后也是可以从事UI设计工作的。


面试官很快就回来了,跟我摇摇头,结束了这场面试。


这次经历给我打击是比较大的,我原本自豪的以为我既能写代码,又能做设计,一定是个抢手货,但事实上,我的两种技能在学校里勉强能秀一下,到了职场专业人士眼里,就是些玩具级别的东西,根本不够看的。


回去后我继续寻找实习机会,中间拿到了亚信联创的Offer,在亚联实习了三个月,学了些CSS基础知识。但我一直不甘心,还是一心想去大公司。


不久后,我等来了百度的另一个面试机会。这次是核心部门:网页搜索部,百度的立足之本。


经过亚联的短暂实习,我对自己的前端技能颇有信心,感觉这次应该是稳了。


然而,是我天真了。


网页搜索部的前端是重逻辑的,需要写大量的JS代码。而彼时的我,才刚把CSS入门,JS的部分只会使用jQuery写一些简单的页面交互效果。


面试官跟我反馈说没过的时候,我赶紧叫住他,说,能否指点一下我,如果想要在百度做前端的话,我应该学习些什么知识?


他也愣了一下(心想这小子不按套路出牌啊),然后拿过笔,在我笔试卷子的背面空白处,一边说一边写下一些零零散散的知识点。


拿过这张画得乱七八糟的A4纸,我如获至宝,连声感谢之后便离开了百度。


参考着这份凌乱的前端知识图谱,我窝在宿舍学习了一个月。一个月后,我给这个面试官发了条短信,请求说能不能再给我一次面试机会,我感觉我准备好了。


他同意了。于是我第3次到百度面试。


面试很快就结束了。面试官跟我说,看得出来这一个月你确实学到不少东西,进步很大,但是距离录用标准还是有点距离的,抱歉。


我又又又一次叫住面试官,和上次一样,我又跟他要了一份更加进阶的前端知识图谱。我说我回去再学习下,还会再来的。


他尴尬而不失礼貌的跟我笑了笑,便起身离开了。


这次的白纸更加密密麻麻,而且看得出来知识点比上一次又更加专业和精深了许多,学起来自然也吃力了不少。


两个月后,我再次联系到他,请求再一次面试。


他又同意了。于是我第4次到百度面试。


这次面试更快的结束了,可能我们彼此已经相对比较熟悉了吧,前几次考察过的知识点这次也就直接跳过了。


看得出来他有些犹豫,可能是他已经不太好意思当面拒绝我了,于是让我等等,找来了他的同事,给我加了一次交叉技术面。


这位新面试官说话也很直肠子,聊了二十分钟后,他直接跟我说:“技术的话勉勉强强吧,但是你这小子身上有一股抑制不住的热情和活力,学习能力也不错,感觉可以培养一下试试,我这里就给你过了吧,我再跟同事商量下,你稍等”。


过了几分钟,先前那个被我屡次骚扰的面试官来了,通知我,面试过了!


这可把我高兴坏了,经过半年多,4次5轮面试,我终于凭自己本事(厚脸皮)拿到了大公司的实习机会!(当年的百度在技术领域可是BAT之首)


这份实习工作给我带来了4000块每月的收入,让我实现了彻底的经济独立。不过这都不是最重要的,最最最重要的是,我终于知道大公司的前端开发是什么样的了!


后来我入职百度后,当我面对着屏幕上漂亮的,模块化的面向对象风格的JS代码的时候,我终于知道了业余和专业的差距。


Anyway,幸运再一次光顾了我这个小子。




在我十多年的工作经历中,诸如此类的幸运不胜枚举。旁人看到的,是我的一路顺风顺水,但只有我自己明白,生活没有奇迹,所有的幸运,都是一次次暗中努力换来的福报




全文完。


作者:沐洒
来源:juejin.cn/post/7220997060136140858
收起阅读 »

🚀 我用一小时实现的娃娃机,你敢信?

web
生活不止眼前的苟且,还有诗和远方 掘友们,大家好我是前端奶爸,入行5年的前端小学生🥜~ 工作八小时摸鱼四小时,喜欢跑步但不是为了让自己更瘦,而是为了让自己活得更久~ 活到九十九,卷到九十九~ 前言 前段时间去商场吃饭的时候看到一个有趣的娃娃机,一个密封的机...
继续阅读 »

生活不止眼前的苟且,还有诗和远方



掘友们,大家好我是前端奶爸,入行5年的前端小学生🥜~

工作八小时摸鱼四小时,喜欢跑步但不是为了让自己更瘦,而是为了让自己活得更久~

活到九十九,卷到九十九~



前言


前段时间去商场吃饭的时候看到一个有趣的娃娃机,一个密封的机器里底部放着一些被捆绑好的龙虾,可以买币去抓龙虾,抓到以后可以初加工费找附近的商家给做成龙虾大餐,感觉很有意思,把抓抓玩出了一个新的高度~


主要是抓到以后还可以出手工费进行烹饪,很吸引人,周边围观的人也很多,观察了一会发现。爪子的抓力不够,龙虾在水里还能移动,而且感觉每一个个头都不小,那小爪感觉根本抓不起来~~


到家后孩子就说爸爸你可不可以做一个娃娃机呢?


身为一个程序员,这点要求我感觉还是难不倒我,然后就突发奇想,给孩子在手机上做一个简易娃娃机。起初的想法是哄她开心,看到掘金最近有小游戏的活动,顺便分享给大家~~


效果


简易娃娃机.gif


如上图,一个移动的抓手,以及几个礼物样品,还有左右移动,抓起按钮,素材很简单,但是做出来的效果还是有娃娃机的感觉的~


地址


代码托管地址在:github在线预览地址资源路径不对无法访问,如果有需要源码的同学可以自行去git仓库获取~


布局


布局部分比较简单,直接贴代码了。可以根据自己的需求不同自定义即可~


<div class="page-portrait" id="page-portrait">
<div id="pageContainer" class="page-container game-box">
<div class="poster-main">
<ul class="poster-list">
<li class="item lw1"><img src="images/dx-lw1.png" alt=""></li>
<li class="item lw2"><img src="images/dx-lw2.png" alt=""></li>
<li class="item lw3"><img src="images/dx-lw3.png" alt=""></li>
<li class="item lw4"><img src="images/dx-lw4.png" alt=""></li>
<li class="item lw5"><img src="images/dx-lw5.png" alt=""></li>
<li class="item lw6"><img src="images/dx-lw6.png" alt=""></li>
</ul>
</div>
<div id="stop" class="button"></div>
<div id="left" class="left-btn"></div>
<div id="right" class="right-btn"></div>
<div class="zhua-top">
<span class="zhua-zuo"></span>
<span class="zhua-zhu"></span>
<div class="zhua zhuamove"></div>
</div>
</div>
</div>

css用到了几个运动处理了爪子的动效,如下方代码所示


@keyframes run {
0% {
background-image: url(../images/dx-zhua3.png);
}
25% {
background-image: url(../images/dx-zhua2.png);
}
50% {
background-image: url(../images/dx-zhua1.png);
}
75% {
background-image: url(../images/dx-zhua2.png);
}
100% {
background-image: url(../images/dx-zhua3.png);
}
}
@keyframes zhuashou {
0% {
top: 360px;
background-image: url(../images/dx-zhua1.png);
}
100% {
top: 360px;
background-image: url(../images/dx-zhua2.png);
}
}
@keyframes zhuadown {
0% {
top: 138px;
background-image: url(../images/dx-zhua1.png);
}
100% {
top: 360px;
background-image: url(../images/dx-zhua1.png);
}
}
@keyframes zhua-slideUp {
0% {
top: 360px;
background-image: url(../images/dx-zhua2.png);
}
100% {
top: 138px;
background-image: url(../images/dx-zhua2.png);
}
}
@keyframes img-slideUp {
0% {
top: 23px;
}
100% {
top: -200px;
}
}

js代码创建了一个控制器类,处理事件以及动画效果的交替等。


var Carousel = {
data: {
result: 1
},
init: function () {
Carousel.control();
},
stop: function () {
$(".zhua").removeClass("zhuamove").addClass("zhuadown");
$(".zhua-zhu").addClass("zhudown");
var timer01 = setTimeout(function () {
$(".zhua").removeClass("zhuadown").addClass("zhuashou");
var timer03 = setTimeout(function () {
$(".zhua").removeClass("zhuashou").addClass("zhuaup");
$(".zhua-zhu").removeClass("zhudown").addClass("zhuup");
$(".poster-list .lw" + (Carousel.data.result + 1)).addClass("img-slideUp");
clearTimeout(timer03);
timer03 = null;
}, 800);
var timer02 = setTimeout(function () {
$(".zhua").removeClass("zhuaup").removeClass("zhuaup1");
$(".zhua-zhu").removeClass("zhuup");
clearTimeout(timer02);
timer02 = null;
alert("恭喜您抽中一等奖~");
Carousel.start();
}, 2500);
clearTimeout(timer01);
timer01 = null;
}, 1000);
},
start: function () {
$(".zhua").addClass("zhuamove");
$(".zhua").removeClass("zhuadown").removeClass("zhuaup1").removeClass("zhuaup");
$(".poster-list .item").removeClass("img-slideUp").removeClass("img-slideOutUp");
},
zhuaMove: function (num) {
switch (num) {
case 0:
$(".zhua-top").animate({
left: -145,
},300);
break;
case 1:
$(".zhua-top").animate({
left: 0,
},300);
break;
case 2:
$(".zhua-top").animate({
left: 145,
},300);
break;
}
},
control: function () {
$("#left").on("click", function () {
Carousel.data.result--;
if (Carousel.data.result <= 0) {
Carousel.data.result = 0;
}
Carousel.zhuaMove(Carousel.data.result);
});
$("#stop").click(Carousel.stop);
$("#right").on("click", function () {
Carousel.data.result++;
if (Carousel.data.result >= 2) {
Carousel.data.result = 2;
}
Carousel.zhuaMove(Carousel.data.result);
});
},
};

总结


css现在有很多的新的特性可以解决我们工作中遇到的动效以及兼容问题,有心的同学可以多多查阅文档,写一写自己感兴趣的小demo,或者给孩子做一个小游戏来玩,何尝不是一件有成就的事呢~


我是奶爸,喜欢我的可以关注我,有什么新的想法或者意见也可以在评论区留言,我们共同学习,共同进步~



最后希望疫情早早结束,微风袭来,春暖花开~~~



作者:前端奶爸
来源:juejin.cn/post/7089371535588196366
收起阅读 »

【404】你访问的页面需要关灯后查看!

web
前言 今天在掘金首页刷到一篇文章,就是那种文字根据不同的色块显示不同的颜色,我想着能不能做一个探照灯似的 404 页面呢。毕竟也可以根据不同的白色光照来改变文字颜色的。 为了酷炫一点,先来个背景 👉 背景相对来说比较简单了,就是一些纯粹的漂浮点 <div...
继续阅读 »

前言


今天在掘金首页刷到一篇文章,就是那种文字根据不同的色块显示不同的颜色,我想着能不能做一个探照灯似的 404 页面呢。毕竟也可以根据不同的白色光照来改变文字颜色的。


为了酷炫一点,先来个背景


👉 背景相对来说比较简单了,就是一些纯粹的漂浮点


<div>
<div class="starsec"></div>
<div class="starthird"></div>
<div class="starfourth"></div>
<div class="starfifth"></div>
</div>

👉 为了显得与众不同,我们就用四个不同的 div 元素来写样式


.starsec {
content: " ";
position: absolute;
width: 3px;
height: 3px;
background: transparent;
box-shadow: 571px 173px #00BCD4, 1732px 143px #00BCD4, 1745px 454px #FF5722, 234px 784px #00BCD4, 1793px 1123px #FF9800, 1076px 504px #03A9F4, 633px 601px #FF5722, 350px 630px #FFEB3B, 1164px 782px #00BCD4, 76px 690px #3F51B5, 1825px 701px #CDDC39, 1646px 578px #FFEB3B, 544px 293px #2196F3, 445px 1061px #673AB7, 928px 47px #00BCD4, 168px 1410px #8BC34A, 777px 782px #9C27B0, 1235px 1941px #9C27B0, 104px 1690px #8BC34A, 1167px 1338px #E91E63, 345px 1652px #009688, 1682px 1196px #F44336, 1995px 494px #8BC34A, 428px 798px #FF5722, 340px 1623px #F44336, 605px 349px #9C27B0, 1339px 1344px #673AB7, 1102px 1745px #3F51B5, 1592px 1676px #2196F3, 419px 1024px #FF9800, 630px 1033px #4CAF50, 1995px 1644px #00BCD4, 1092px 712px #9C27B0, 1355px 606px #F44336, 622px 1881px #CDDC39, 1481px 621px #9E9E9E, 19px 1348px #8BC34A, 864px 1780px #E91E63, 442px 1136px #2196F3, 67px 712px #FF5722, 89px 1406px #F44336, 275px 321px #009688, 592px 630px #E91E63, 1012px 1690px #9C27B0, 1749px 23px #673AB7, 94px 1542px #FFEB3B, 1201px 1657px #3F51B5, 1505px 692px #2196F3, 1799px 601px #03A9F4, 656px 811px #00BCD4, 701px 597px #00BCD4, 1202px 46px #FF5722, 890px 569px #FF5722, 1613px 813px #2196F3, 223px 252px #FF9800, 983px 1093px #F44336, 726px 1029px #FFC107, 1764px 778px #CDDC39, 622px 1643px #F44336, 174px 1559px #673AB7, 212px 517px #00BCD4, 340px 505px #FFF, 1700px 39px #FFF, 1768px 516px #F44336, 849px 391px #FF9800, 228px 1824px #FFF, 1119px 1680px #FFC107, 812px 1480px #3F51B5, 1438px 1585px #CDDC39, 137px 1397px #FFF, 1080px 456px #673AB7, 1208px 1437px #03A9F4, 857px 281px #F44336, 1254px 1306px #CDDC39, 987px 990px #4CAF50, 1655px 911px #00BCD4, 1102px 1216px #FF5722, 1807px 1044px #FFF, 660px 435px #03A9F4, 299px 678px #4CAF50, 1193px 115px #FF9800, 918px 290px #CDDC39, 1447px 1422px #FFEB3B, 91px 1273px #9C27B0, 108px 223px #FFEB3B, 146px 754px #00BCD4, 461px 1446px #FF5722, 1004px 391px #673AB7, 1529px 516px #F44336, 1206px 845px #CDDC39, 347px 583px #009688, 1102px 1332px #F44336, 709px 1756px #00BCD4, 1972px 248px #FFF, 1669px 1344px #FF5722, 1132px 406px #F44336, 320px 1076px #CDDC39, 126px 943px #FFEB3B, 263px 604px #FF5722, 1546px 692px #F44336;
animation: animStar 150s linear infinite;
}

👉 颜色阴影部分都是一样的,不一样的地方就在于宽高和动画时长。


👉 大家可以根据自己的想法去修改不同的宽高和时长哦


👉 动画效果需要额外写一下的哦


@keyframes animStar {
0% {
transform: translateY(0px);
}

100% {
transform: translateY(-2000px);
}
}

screenshots.gif


画灯杆(电线)


👉 一般探照灯都是在顶上的,所以就需要用一根电线连接在顶部


<div class="lamp__wrap">
<div class="lamp">
<div class="cable"></div>
</div>
</div>


  • 后面的灯元素相关内容都会在 lamp 样式标签下面哦!


.lamp__wrap {
max-height: 100vh;
overflow: hidden;
max-width: 100vw;
}
.lamp {
position: absolute;
left: 0px;
right: 0px;
top: 0px;
margin: 0px auto;
width: 300px;
display: flex;
flex-direction: column;
align-items: center;
transform-origin: center top;
animation-timing-function: cubic-bezier(0.6, 0, 0.38, 1);
animation: move 5.1s infinite;
}


  • 在处理动画的时候,使用了一个 cubic-bezier 方法,它是用来定义贝塞尔曲线的


@keyframes move {
0% {
transform: rotate(40deg);
}

50% {
transform: rotate(-40deg);
}

100% {
transform: rotate(40deg);
}
}


  • 动画效果就是将灯杆旋转不同的角度



注意一下,动画效果是在整个灯的样式中完成的,所以后面的都只需要写各自的样式就行了,不需要补充动画效果。



.cable {
width: 8px;
height: 248px;
background-image: linear-gradient(rgb(32 148 218 / 70%), rgb(193 65 25)), linear-gradient(rgba(0, 0, 0, 0.7), rgba(0, 0, 0, 0.7)), linear-gradient(rgba(0, 0, 0, 0.7), rgba(0, 0, 0, 0.7));
}


  • 灯杆给了一个渐变色的样式效果


screenshots.gif


画灯罩


👉 灯杆已经有了,那就加一个灯罩就行了


<div class="cover"></div>

.cover {
width: 200px;
height: 80px;
background: #0bd5e8;
border-top-left-radius: 50%;
border-top-right-radius: 50%;
position: relative;
z-index: 200;
}


  • 灯罩是通过不同的 border-radius 的效果画出来的


screenshots.gif


画灯泡


👉 灯泡也是比较简单的样式,一个半圆加一部分阴影即可


<div class="in-cover">
<div class="bulb"></div>
</div>

.in-cover {
width: 100%;
max-width: 200px;
height: 20px;
border-radius: 100%;
background: #08ffff;
position: absolute;
left: 0px;
right: 0px;
margin: 0px auto;
bottom: -9px;
z-index: 100;
}

.in-cover .bulb {
width: 50px;
height: 50px;
background-color: #08fffa;
border-radius: 50%;
position: absolute;
left: 0px;
right: 0px;
bottom: -20px;
margin: 0px auto;
-webkit-box-shadow: 0 0 15px 7px rgba(0, 255, 255, 0.8), 0 0 40px 25px rgba(0, 255, 255, 0.5), -75px 0 30px 15px rgba(0, 255, 255, 0.2);
box-shadow: 0 0 25px 7px rgb(127 255 255 / 80%), 0 0 64px 47px rgba(0, 255, 255, 0.5), 0px 0 30px 15px rgba(0, 255, 255, 0.2);
}

screenshots.gif


来一束追光效果吧


👉 追光就是通过一个边框线画出来的


<div class="light"></div>

.light {
width: 200px;
height: 0px;
border-bottom: 900px solid rgb(44 255 255 / 24%);
border-left: 50px solid transparent;
border-right: 50px solid transparent;
position: absolute;
left: 0px;
right: 0px;
top: 270px;
margin: 0px auto;
z-index: 1;
border-radius: 90px 90px 0px 0px;
}


  • 给边框的宽度和背景透明色就可以看出追光的效果了。


screenshots.gif


文字


👉 文字通过定位居中之后,刚好显示在灯光动画效果范围之内


<section class="error">
<div class="error__content">
<div class="error__message message">
<h1 class="message__title">掘金错误页面</h1>
<p class="message__text">不好意思,你访问的页面不存在,请关灯后重新尝试</p>
</div>
</div>
</section>

👉 文字颜色和背景色一致之后,通过灯光的透明度效果就可以实现文字显隐了。


.error {
min-height: 100vh;
position: relative;
padding: 240px 0;
box-sizing: border-box;
width: 100%;
height: 100%;
text-align: center;
margin-top: 70px;
}

.error__overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: hidden;
}

.error__content {
position: absolute;
top: 50%;
left: 50%;
width: 100%;
-webkit-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
}

.error__message {
text-align: center;
color: #181828;
}

.message__title {
font-family: 'Montserrat', sans-serif;
font-weight: 900;
text-transform: uppercase;
letter-spacing: 5px;
font-size: 5.6rem;
padding-bottom: 40px;
max-width: 960px;
margin: 0 auto;
}

.message__text {
font-family: 'Montserrat', sans-serif;
line-height: 42px;
font-size: 18px;
padding: 0 60px;
max-width: 680px;
margin: auto;
}

screenshots.gif


码上掘金查看效果



作者:蜡笔小心_
来源:juejin.cn/post/7150950812489875469
收起阅读 »

记录一次机器学习模型部署

web
简介:做了一个语音识别心脏病的机器学习模型,想要实现前后端简单的部署 用到的技术栈:Python、Flask、uni-app 前端 使用uni-app做一个小程序。需要具有语音采集、录音回放、录音上传等功能。使用uni.getRecorderManager()...
继续阅读 »

简介:做了一个语音识别心脏病的机器学习模型,想要实现前后端简单的部署


用到的技术栈:Python、Flask、uni-app


前端


使用uni-app做一个小程序。需要具有语音采集、录音回放、录音上传等功能。使用uni.getRecorderManager()实现对录音全局的控制。


屏幕截图 2023-04-04 155350.png


这里给出实现上传录音并接收请求结果的主要代码


upload() {
console.log(this.voicePath)
uni.uploadFile({
url: 'http://202.115.52.33:9500/process_data',
filePath: this.voicePath,
name: 'file',
fileType: "audio", //文件类型
success: (res) => {
console.log('success',res.data)
uni.showToast({
title: '上传成功',
icon: 'success',
});
if(res.data*1 < 0.35){
uni.showModal({
title: '检测结果',
content: '您患心脏病概率为:'+ Number(res.data*100).toFixed(2) + '%'+',心脏健康请继续保持',
success: function (res) {
if (res.confirm) {
console.log('用户点击确定');
} else if (res.cancel) {
console.log('用户点击取消');
}
}
});
}else{
uni.showModal({
title: '检测结果',
content: '您患心脏病概率为:'+ Number(res.data*100).toFixed(2) + '%'+',请及时到医院检查',
success: function (res) {
if (res.confirm) {
console.log('用户点击确定');
} else if (res.cancel) {
console.log('用户点击取消');
}
}
});
}

},
fail: (err) => {
console.log((err))
uni.showToast({
title: '上传失败',
icon: 'none',
});
},
});
}

这里有个坑需要注意,微信开发者工具模拟器录音上传到服务器,服务器无法正常使用录音(一直以为是前端上传语音的问题)。开发者工具录音文件为silk格式,说是silk其实是base64加密后的webm格式,不是普通的wav格式(貌似只能用chrome浏览器打开)。可以参考这篇文章微信小程序-录音文件无法播放问题 - 知乎 (zhihu.com),真机调试则不会出现这个问题。


后端


采用Flask来进行机器学习或者深度学习模型的部署。


# app.py
from flask import Flask, request
from predict import predict

app = Flask(__name__)

@app.route('/process_data', methods=['POST'])
def process_data():
    # 从前端接收音频文件
    fileStorage = request.files['file']  # 视频文件
    buffer_data = fileStorage.read()
    filename = request.files['file'].filename
    temp_path = 'upload/'+filename
    with open(temp_path, 'wb+') as f:
        f.write(buffer_data)  # 二进制转为音频文件
    # 模型推理
    predict_outcome = round(predict(temp_path), 4)
    # 返回结果
    return str(predict_outcome)


if __name__ == "__main__":
    app.run()

部署


使用宝塔面板实现Flask项目快速部署。



  1. 在宝塔面板中安装Python项目管理器软件


屏幕截图 2023-04-04 162932.png



  1. 上传Flask项目到服务器相应目录

  2. 在Python项目管理选择Flask框架,安装Flask项目中需要的第三方包
    这里有个需要注意的问题,我修改了第三方包的源码,下载的第三方包存放目录:上传项目文件夹/一串数字_venv/lib/python3.7/site-packages,在这里修改源码重启Python服务才能生效。

  3. Python项目管理器配置参考


bind = '0.0.0.0:5000'
user = 'scu611'
workers = 1
threads = 2
backlog = 512
daemon = True
chdir = '/www/server/phpmyadmin/heartbroken'
access_log_format = '%(t)s %(p)s %(h)s "%(r)s" %(s)s %(L)s %(b)s %(f)s" "%(a)s"'
loglevel = 'info'
worker_class = 'geventwebsocket.gunicorn.workers.GeventWebSocketWorker'
errorlog = chdir + '/logs/error.log'
accesslog = chdir + '/logs/access.log'
pidfile = chdir + '/logs/heartbroken.pid'

作者:用户7850680667062
来源:juejin.cn/post/7218098727608549432
收起阅读 »

Android 应用架构指南

一 简介 遵循摩尔定律,手机终端随着每年的更新换代,其性能也飞速增长。依附于此的 Android 应用规模也愈发复杂。截止 2023 年 4 月,最新版本 8.0.32 微信 apk 大小为 238MB,而对比 2011 年微信 1.0 版本 apk 包大小仅...
继续阅读 »

一 简介


遵循摩尔定律,手机终端随着每年的更新换代,其性能也飞速增长。依附于此的 Android 应用规模也愈发复杂。截止 2023 年 4 月,最新版本 8.0.32 微信 apk 大小为 238MB,而对比 2011 年微信 1.0 版本 apk 包大小仅为 457KB,短短 12 年增长了 533 倍。


image.png


随着应用规模增大,功能扩展困难、测试规模大及并行开发难等问题愈发突出。为了从根本上解决这些问题,就需要对应用进行重构,此时应用架构设计就显得尤为重要。


Android 应用架构设计三步走:



  • 现象: 程序代码和资源越来越多,代码耦合度高,扩展、维护及测试困难

  • 手段: 分离代码,提炼模式

  • 结果: 确保应用的稳健性、可测试性和可维护性


下文主要介绍三种常见的架构设计模式 MVC、MVP、MVVM


二 MVC


MVC 全称 Model View Controller,是模型(Model)-视图(View)-控制器(Controller)的缩写。


image (1).png



  • View: 负责界面数据的展示,与用户进行交互;对应于 xml布局文件和 java 代码动态 view 部分;

  • Controller: 负责逻辑业务的处理;

  • Model: 负责管理业务数据逻辑,如网络请求、数据库处理和 I/O 的操作等。


MVC 初步解决了 Activity 代码太多的问题,但 Activity 天然不可避免要处理 UI,也要处理用户交互,导致 Activity 里糅合了视图和业务的代码,分离程度不够。


优点:



  • 耦合性较低,生命周期成本低,部署快,适用于快速开发的小型项目


缺点:



  • 不适合中大型项目,View 层和 Controller 层连接过于紧密

  • View 层对 Model 层的访问效率低

  • 一般的高级 UI 页面工具和构造器不支持 MVC 模式


三 MVP


为了将 Activity 中的表现逻辑彻底分离出来,业界提出了 MVP 的设计。


MVP 全称 Model View Controller,是模型(Model)-视图(View)-呈现者(Presenter)的缩写。


image (2).png



  • View: 只负责显示 UI,只与 Presenter 层交互,与 Model 层没有耦合。对应于 ActivityXML

  • Presenter: 负责处理业务逻辑,通过接口回调 View 层;

  • Model: 负责管理业务数据逻辑,如网络请求、数据库处理和 I/O 的操作等。


在 MVP 模式中,Model 与 View 无法直接进行交互,所以 Presenter 层会从 Model 层获得数据,适当处理后交给 View 层进行显示。在 MVP 模式中,Presenter 层将 View 层和 Model 层进行隔离,使 View 和 Model 之间不存在耦合,同时将业务逻辑从 View 层剥离。


优点:



  • 逻辑结构清晰,View 层代码不再臃肿,所有的交互都发生在 Presenter 内部


缺点:



  • View 层和 Presenter 层的交互需要定义接口方法,当交互非常复杂时,需要定义很多接口方法和回调方法,增加维护复杂度

  • Presenter 层 持有 View 层的引用,当用户关闭了 View 层,但 Model 层仍然在进行耗时操作,会有内存泄漏风险


四 MVVM


MVVM 全称 Model View ViewModel,模式改动在于中间的 Presenter 改为 ViewModel,MVVM 同样将代码划分为三个部分:


image (3).png



  • View: 与 MVP 中 View 的概念相同;

  • ViewModel: 连接 View 与 Model 的中间桥梁,ViewModel 与 Model 直接交互,通过 DataBinding 将数据变化反应给 View;

  • Model: 负责管理业务数据逻辑,如网络请求、数据库处理和 I/O 的操作等。


在实现细节上,View 和 Presenter 从双向依赖变成 View 可以向 ViewModel 发指令,但 ViewModel 不会直接向 View 回调,而是让 View 通过观察者的模式去监听数据的变化,有效规避了 MVP 双向依赖的缺点。


优点:



  • 模块间充分解耦,结构清晰,职责划分清晰

  • 在 MVP 的基础上,MVVM 把 View 和 ViewModel 也进行了解耦


缺点:



  • View 与 ViewModel 的交互分散,缺少唯一修改源,不易于追踪

  • 复杂的页面需要定义多个 MutableLiveData,并且都需要暴露为不可变的 LiveData


五 参考文献


Jetpack 系列(5)—— Android UI 架构演进:从 MVC 到 MVP、MVVM、MVI


MVC、MVP、MVVM,我到底该怎么选?


作者:话唠扇贝
来源:juejin.cn/post/7220985690795524156
收起阅读 »

我的前端之路,我的经验,相信对你会有帮助

前言   离我上一次写文章已经四年有余,期间虽不断有各种想法与思考记录,但总是被各种事情和借口打断,始终没有能真正动笔,颇为内疚。此次重新提笔,在展望未来之际,先回顾与总结下过去。期望这过程中能够有所自省,有所收获。七年时光,经历了一些起伏,面临或思考过一些问...
继续阅读 »

前言


  离我上一次写文章已经四年有余,期间虽不断有各种想法与思考记录,但总是被各种事情和借口打断,始终没有能真正动笔,颇为内疚。此次重新提笔,在展望未来之际,先回顾与总结下过去。期望这过程中能够有所自省,有所收获。七年时光,经历了一些起伏,面临或思考过一些问题,大概率大家也都曾忧虑过,期望能为大家带来一点启发与共鸣。



本文鸡汤浓度略高,对鸡汤过敏症患者抱歉。但相信看完应该有一点点收获。🦥



技术之路


  2016年时,我还是个生物学硕士,蹲在实验室里摆弄大肠杆菌,对互联网并无概念。一次偶然机会,帮朋友搭个官网,接触到了wordpress。几经周折虽成功,但过程略为艰辛(各位老铁别吐槽,当时我还只是个孩子啊~🐒)。但我发现我能沉浸其中,享受那种1+1=2的确定感 (可能是因为生物学实验其实是个概率学,结果充满着不确定性),并从中收获了远比实验成功更大的成就感。就一个瞬间,我就毅然决然决定了,这就是我应该去做的事。于是放弃了我学了7年的生物学,即使我周围所有人都表示:are u kidding me?


  当我迷茫与自我怀疑时,我就会回顾这段时光,找到那个最纯粹的初心。别人在实验室看论文,我在实验室上网易云课堂(大家有没有上过的,现在我们还经常在群中唠嗑,这里致敬下猪厂)。别人在做实验,我边做实验边看红宝书。其实真不是为了找工作(我当时还很天真,没有想太多找工作的事),也不是为了给谁一个交代,就像一个玩具一样,就是一种简单的热爱。



⭐️ 初心 ⭐️


  我相信 热爱是最纯粹的初心,只有你热爱一件事并从中找到乐趣,才能真正做好它,而它也能帮你度过那些精疲力尽的时刻。


  当你发现工作让你内心煎熬时,可以适当停下来,给自己点空间,回想下最初那些开心,也许它们能治愈你的内心,并让你放下那些无谓地执着。



  其实我这种野路子,职业生涯第一步就已经落后了。相比于各位大佬,我背景没优势,没有经验,没有技术,也没有任何人脉。但人生总是充满着神奇,记得是一天我在公交车上,看qq群里有人说有实习工作。我就回了一句,没想到这就成了我的第一份实习工作。公司虽小,但大家都很nice,我也终于敢称自己为前端工程师了,感谢我的启蒙老大,respect!


  接下来,在临近毕业的一次偶然机会,我无意间看到了美图公司在隔壁学校的校招宣讲会,我就抱着学习和了解的心态去参加了。没想到直接是全场笔试,又没想到我竟然有幸成为录取的唯一前端校招生。在美图的三年,是一段美好的旅程,快速增长的业务,轻松和谐的工作氛围以及美好的同事。我如鱼得水,在技术之路上快速吸收着养分,快速学习和成长。支撑着过亿用户的产品,追求着最极致的动画交互效果,沉迷于写公司的各种工具库、开源库,天天泡在公司,也写了一些文章,代表公司作为讲师在技术沙龙上演讲,带了个小团队。


  感恩美图公司,感恩相处的每一位同事,这段期间我学到了很多。



⭐️ 关于自信与机会 ⭐️


  我相信人生处处充满着惊喜和机遇,我们不需要妄自菲薄,也不需要感叹不公。每个机会就像是你命运的分叉点,具备蝴蝶效应,引领你去往不一样的人生。而我们需要做的,就是相信自己,然后时刻准备着,迎接即将到来的机会。


  但同样,人生不是单方向的,你不可能永远成功。一定也会有错过。抓住了机会,是你的成长。错过了机会,发现你的不足,亦是你成长的机会。


  所以,保持努力,保持平常心,尽人事,听天命。




⭐️ 关于起步 ⭐️


  每段时期,都会有不同的重点。在最开始的三年,请好好珍惜。在这段时期,不需要太多负担,也不需要太多顾虑,就是保持着激情与好奇心,让自己快速成长,去追求你所感兴趣的一切。


  从前端的角度来讲,就是写代码、写代码、写代码!这就是你最好的作品和证明,去思考最优雅的实现,最高效的实现。这将会是美妙且收获满满的一段经历。



  美好生活的某一天,我突然觉得我应该毕业了。我做完了我在这里想做的事,我得到了我想要的,是时候前往下一个目的地,开启一段新的征程了。当时我的目标是想要刷一轮offer,验证下自己的成长。我拿到了字节、美团、阿里、瑞幸的offer,最终选择来到了字节。


  离开美图的那天,大家流泪送我到公司门口,那一幕仍然历历在目。但我仍然义无反顾地出发,我知道这是我该做的。



⭐️ 关于目标 ⭐️


  我们需要目标与方向,想想五年后你期望的画面,你想成为一个什么样的前端?谁是你的灯塔?可以很天真,也可以很简单。时刻谨记着目的地,这会让你不迷失方向,然后朝着这个方向坚定前进。


  把目的地拆解成一个个具象化的车站,可能是一份理想中的简历,可能是一段让人信服的项目经历或者作品,每天都离它近一点。总有一天,你一定会到达目的地。


  每一段旅程都会有终点。当完成这趟旅程的使命后,到达目的地,不要留恋,勇敢地踏上下一段旅程,相信未来将会更美好。


  这就是成长。



  在这段三年的旅程中,每段旅程对我都有着其重要的意义,是下一段旅程的铺垫与基础。


  在实习阶段,我主要研究了PC开发以及一些数据可视化的尝试,这段旅程虽短,但这是我的前端之路中的第一个重要里程碑。它让我完成了一次重要转变,从学生踏入了社会职场,从生物转成了互联网,也成为了一名正式的前端工程师,奠定了后面一切的基础。


  接下来三年的美图阶段,我的重心主要在面向C端的移动端业务。这个阶段我的自驱力很强,对一切充满着好奇与动力。在H5活动开发、Hybrid混合应用架构、Web图片处理与合成等方向进行了深入的理解和研究,奠定了我的技术体系与擅长方向。



⭐️ 关于技术 ⭐️


  其实大家一直在讨论技术广度与深度的问题。我觉得这是分阶段的,职业生涯的不同阶段应该有不同的侧重点,但最重要的就是闪光点和标签,这能让你从竞争中脱颖而出。


  前五年是奠定自身技术体系基础的时候,业务经验的限制注定了一定无法在广度上有竞争力,所以去找到自己的喜欢和擅长,并深入研究,找到自己的标签。


  只有这样,在看过你的简历,听过你的自我介绍后,别人才能记住你的名字。



业务之路


  在字节不到半年时,一次偶然的机会,我决定跟朋友一起出去玩一把。虽然我还没完成在字节的旅程,还有很多事想做,但每个人也许都会有个“总有一天我会创业我会成功”的梦想吧?也许这就是个实现梦想的机会?


  但梦想与现实的距离往往比想象中的远,其实这很正常,往往我们在梦想的时候,我们并没有足够的认知和理解,当你真正朝梦想前进时,开始动手去深入时,你就会发现事情并非你想的那样。


  我们尝试了游戏与教育结合方向的一些产品研发,其实是很好的方向与产品,受到了很多用户的一致好评。但真正的创业,并非想法与产品,更重要的是商业与价值。这个阶段,我开始跳出技术的思维模式,思考产品体系,写商业计划书,参加路演、创业比赛等,在高压下快速提升自己的综合能力,也带领团队取得了一定的进展,看到了希望。但最终我决定停下脚步,这个决定很难,在团队仍然勇气满满的时候,我却率先主动刹车,甚至我的家人都不解和质疑,为什么在希望的前夕放弃?你是不是怕吃苦?我也纠结过,也犹豫过,但经过理智的自我剖析,我认识到我离成功的所需的能力还差得太远了。即使有了阶段性的进展,但我的能力是无法带领团队获得真正成功的。当中途发现前进的方向大概率并不是理想中的目标时,那勇往无前就不一定是明智的选择了,与其博那极小概率事件,不如重新思考与沉淀。


  这段经历其实不好过,时刻紧绷着,高压逼着自己前进。现在很多人都会问我,你会后悔吗?其实并不会,我确实也失去了一些,但失去的东西是在我的舒适圈中的,我可以再努力回来。而得到的是一段我从来没经历过的生活,我收获了很多认知,也很大程度改变了我的思维模型,也加深了很多我对互联网行业、产品、业务的理解。感谢这段旅程我遇到的每个人,也许我内心带着愧疚,但我真心由衷感谢 everyone!



⭐️ 关于经历 ⭐️


  每段旅程都是一种经历,不同的经历会带来不同的视角与收获。抛出一枚硬币,得到了正面,但也会失去反面,这就是平行世界,坦然去面对,很多东西我们无法左右,纠结和情绪很多只是徒劳。


  一段旅程,会有黑夜,也会有白天。一段经历,也会有得到与失去。放下情绪,好好去欣赏沿途的风景,至少以后能回忆那些美好的事物。




⭐️ 关于创业 ⭐️


  创业其实就是一场激情亢奋的战斗,它需要你真正发自内心的热爱与激情。当你想创业之前,给自己几天的时间,弄清楚这趟行程你想要得到什么,这是不是你内心中最纯粹的目标。


  在你还没想清楚,或者你觉得你创业的目标是融资或者赚钱,那这场战斗将会无比艰难。如果你已经开始,那就需要找到那个支撑你坚持下去的信念。


另外,如果你想创业,我有一些建议:



  • 创业第一重要的是人。如果各位小伙伴有创业的想法,那第一件事,就是去找到那些最正确的战友。

  • 第二重要的是机会,去深入研究这个行业这个环境,机会在哪里,成功就在那里。

  • 想法与产品并不是最重要的,相信我,这个世界比你天才,比你有远见的人多得是,不存在这个idea惊天地泣鬼神。(深夜会让人变得情绪和极端,如果你在深夜觉得你的想法能拯救地球,那要么起床洗把脸,看下镜子里的自己,要么答应我,早点睡身体好。😂)

  • 让专业的人做专业的事,不要觉得可以学习。创业就像一个罗马竞技场,没有人会等你成长,不要拿自己的劣势,去对抗敌人的优势。



  2021年,我决定结束创业再次回到字节。我相信这里有最优秀的团队与人才,这里有我需要的东西。二进字节,我早已不是三年前的愣头青了。创业的经历给我带来了认知与思维的提升,给了我两个最重要的改变:



  1. 具备更好的结构化思维,有了一些方法论的积累。



  • 我从一条业务方向进行切入,其实类似于创业,第一步最重要的事就是找到你的战友。我把过往的小伙伴拉起来,快速组建了一个初具战斗力的小组,并且与各方建立了紧密的合作关系。

  • 第二步,就是收集信息,梳理现状,进行行业调研,充分了解业务现状,与各方对齐目标,找到当前业务最重要的事情。

  • 第三步,那就是目标拆解,推进落地。基于前面的合作互信,以及明确的目标,过去这一年多其实进展得挺顺利的,业务核心数据得到了大幅增长,业务也成为部门重要方向,我也逐步站稳了脚跟。



⭐️ 关于方法论 ⭐️


  当遇到问题或者事时,可以先不急着动手,给自己点时间思考,可以找一些资料,借鉴一些经验。而所谓的经验,就是沉淀出的方法论。例如做业务的大致思路,产生想法 -> 找到资源 -> 梳理现状 -> 明确方向 -> 制定计划 -> 拆解目标 -> 逐一完成。


  但方法论只能避免踩坑,作为指导,并不能直接解决全部问题,真正的问题仍需要 case by case 去解决,再通过复盘,进一步沉淀出方法论。其实我们的能力就像开发生态一样,我们用 react 开发业务(生态中的方法论、经验),但仍然会遇到各种问题,于是进一步沉淀出各种工具库,推进 react 生态持续完善。


  所以,复盘是形成螺旋上升闭环的关键步骤,复盘的重点是沉淀方法论。即:



  1. 业务中,我们通过复盘沉淀方法论,下次便可做得更好;

  2. 技术中,我们通过抽象封装沉淀工具库,效率便可越来越高;

  3. 成长中,我们通过复盘积累经验,个人能力便可越来越好;


  因此,不要让自己疲于奔命,适时给自己一些空间,回顾下今天,这个月,这一年,你就知道该怎么做了。




  1. 我开始更关注技术所带来的价值。



  • 创业的经历,让我需要更进一步思考,考虑每件事的目的。其实你所做的每一件事,都有其动机与目标。带着明确的目标做事,就至少不会迷失方向。即使你玩游戏,你刷抖音,如果你从一开始就明确是为了消磨时间,休闲放松,那就不会有愧疚感。

  • 因此以前是技术->完成需求。而现在则会更进一步,技术->完成需求->带来价值。价值其实就是你所做的事的目标,它可以是多样的,可以是业务收益,也可以是技术收益、团队收益,甚至个人成长等。



⭐️ 关于价值 ⭐️


我们从技术出发影响业务主要分成三个递进式的阶段:




  1. 支撑好产品的需求,推进业务发展,这是最开始的阶段,也是每个工程师的基线。这个阶段,我们是协作者,可以多去关注一些业务的数据、进展和结论。




  2. 在完成好需求的前提下,基于数据与分析,思考如何做得更好,如何带来更多的技术附加值。从前端工程师的角度出发,为业务创造价值主要有三个方向:



    1. 保障线上的稳定性,例如更高效的错误排查与治理,线上错误率显著下降,对业务指标带来正向影响。

    2. 为用户带来更好的体验,例如提升性能、优化交互等。一个体验越好的产品,用户认可度越高,留存、活跃、转化等核心指标也有可能会越好。

    3. 产品迭代效率的提升,例如通过技术工具提升研发效率、平台化提升运营效率等,人效亦是价值。




  3. 当积累了对业务足够的认知后,跳出技术视角,真正从业务、数据、产品的视角出发,带来产品功能、策略上的优化与落地。这是一个顺其自然的阶段,只有当对业务的认知达到比较高后,以专业的角度去做。(我其实不太推崇,靠着用户视角的认知,就经常提出一大堆质疑,其实只会显得自己无知。🤷🏻♂️)




  这三个阶段是递进的,只有做好前者,做后者才会是加分项。否则便可能是不务正业。我们可以通过技术外能力来增强综合实力,而且往往更全面的领域融合,能带来一些惊喜,比如业务、数据分析、设计,甚至写文档、团队合作、做PPT等。但我们的本质仍是技术,如果我们失去技术的底层,而过于重视这些上层延伸,其实是本末倒置。(当然,除非你是规划转岗转行业。)



管理之路


  随着团队的成长,协同收益会逐步增大,通常当团队人数超过某个阈值,协同收益就会开始大于你的单人收益。这时候,开始将精力更多放在团队管理上,去提升团队的整体协同效应,解决效率卡点,是件更高性价比的事。但相信很多小伙伴也都会遇到:刚开始做管理工作时,每天好像都很忙的样子,工作、时间、精力开始碎片化,一会儿有个会议,一会儿有个讨论,一会儿有个流程。但睡觉前回顾,又好像什么事情都没完成。不知不觉,人就会陷入一种自我怀疑的状态中,怀疑自己做的事是不是有意义,担心自己的技术会不会就荒废了。


  我刚开始时也一样陷入其中。人在迷茫的时候,可以多去看看书。相比于在网上看资料,看书有个好处,就是会让人冷静下来,让你也有了更多的个人思考空间。很庆幸,我很快就得到了一些启发,也想明白了一些事,让我能继续往前走。


  从技术实现到技术管理,其实是一种思维方式的进阶。迷茫、怀疑的本质,在于认知没有产生进阶去匹配上当前的阶段需要。因此,我觉得从技术认知到管理认知,这可能是每个小伙伴走上管理之路的第一堂必修课。那为什么我会怀疑我做的事没有意义,而担心自己的技术会荒废?其实思维的本质是:我默认觉得技术是硬通货,而管理是外包装。这其实就是技术型认知,认为只有代码才是真正的技术,而编码以外的东西都是虚的。这也是为什么我们经常能听到抱怨文档工程师,PPT工程师等。


  我的第一个管理认知:管理同样也是一门技术,它也是一种硬通货。甚至文档书写、沟通技巧、总结演讲等能力,也同样是一门技术,也同样有非常多书籍在讲如何提升。当我把管理也同样看成一门技术的时候,我发现从本质上来看,这与 HTML、JavaScript、React 并没有差别,它们都是一种能力,一种去完成目标所需要的工具。当我这么想后,这不就进入了我们前端最擅长的领域,不就是学嘛(前端还怕学?🐒)。


  而学习的过程,就是建立一套属于自己的方法论(其实就是理解方式)。学习管理,就是沉淀出一套自己的方法论,形成自己的一套管理风格,去拆解重点,并逐一思考与实践:



  • 我理想中的团队是怎么样的?

  • 怎么做团队规划?

  • 怎么做团队沟通?

  • 如何做任务分配?

  • 怎么做团队技术建设?

  • 怎么做团队梯度建设?

  • 如何提升团队凝聚力?

  • ...


  另外还有很重要的一点,我觉得是管理自信。在技术认知中,我会觉得我就是喜欢并擅长做技术,但对于技术外的事情,刚开始就很容易出现缺乏自信的状态。比如我有一段就因为没有得到一些正向反馈,导致就一直在怀疑自己是不是做得不够好。也有一段一些小事情没处理好,就会觉得自己能力不足。我在反思后,觉得核心的原因主要有两个:



  • 经验不足,方法论欠缺,导致在管理工作中,缺乏一些思维主轴,做不好的概率变高;

  • 心理包袱,总觉得我不能辜负领导的期望,如果过程中刚好缺乏一些正向反馈,就容易多想是不是我哪里做得不好,让别人失望了;


  其实,第一点也源于上面提到的管理认知,因此只要将管理当成一门技术,给自己一点时间,循序渐进去拆解和沉淀,那自然就会越来越好的,我想没有前端会因为不会 React 就感到自卑吧。而第二点,一方面领导是很了解你的情况的,你的经验,你的优劣势,都会有一定的把握,通常并不会有不合理的期待,也同样会给你足够的空间来成长的。另一方面,我们也不需要完全围绕着别人的评价转,你可以相信自己的判断。不过这里延伸出来的一点,在人的相处中,合适的时机给与正向的反馈是十分重要的,它可能能让对方从自我怀疑中解脱出来。


  不过,相比于很多成熟的管理者,我还只是刚走出新手村,就不在这里高谈阔论了。待我再修炼修炼,再找个机会来跟大家分享更多管理上的思考与实践,也欢迎大家跟我一起探讨。



⭐️ 关于困惑 ⭐️


  我们无时无刻在困惑,而困惑就会带来烦恼与纠结。因此我们开始变得害怕面对困惑,畏难的情绪会引诱我们躲在自己的舒适区中。但这其实不就是人生的一部分吗?困惑,其实还有另外一个名称,叫做好奇。从出生那一刻起,你就在困惑这个世界,而你就是在一个一个的困惑中逐步成长的,有困惑是好事。


  所以这么想,是不是就没有必要去害怕面对了,自然而然去面对,出发去寻找那个属于自己的答案。给自己一些思考的时间,又或者与一名智者聊聊,去找到困惑的本质根源,然后再一一的解开那个心结,你便又成长了。



人生之路


  七年的时间,我从一个天真无知、怀揣梦想的少年一步步成长。我们中的大部分人的人生都是类似的,走着类似的路,做着类似的事。工作上,我们毕业求职升职加薪,生活上,我们恋爱吵架结婚生子。但过着过着,每个人的人生却都完全不一样。一个个小小的决定与机会,最终会让我们都走上自己的命运。


  回望过去的轨迹,首先我先看到的是规划。我的人生就是一场提前做好规划的旅行。每一步都是在规划中进行的,找什么工作、什么时机跳槽、怎么理财存钱、什么时候结婚、什么时候做什么事,都是在计划之中。其实有时我在想这是好的吗?好的一面,规划总是往好的方向的,它能让你时刻保持着前进的方向。提前的规划,会让你能够提前对一些意外情况进行规避,也能够尽量避免一些不好的事情。但另一面,完全的规划可能会让你变得过于理智,缺乏激情,而且规划可能会扼杀那些疯狂的想法,生活也许也会少了那一丝惊喜和精彩,有时候,没有计划,说走就走的旅行,反而会有意外的美丽。例如创业,也是因为我的规划和理智,我需要停下脚步重回字节。但我其实再也无法知道,如果我坚持下去的结果会是什么,也许有惊喜呢?



⭐️ 关于规划 ⭐️


  所以现在我的感悟是规划是必要的,它能让你保持正确的方向,减少迷茫。我经常跟组员讨论的就是想想三年、五年后你应该在做什么,这样你就知道你想要什么了。


  但我经常看到很多人非常不喜欢打破自己的计划,一旦一些事情或者别人打破了自己的计划,就会变得很焦虑,很有情绪,其实这也不好。我希望规划是开放的,它可以被打破,它可以随机应变,它也可以接受更多意外的发生。也许这样,你才不会错过那意外的美丽。



  另外一个点我想提的是我的乐观与积极。人生不可能永远一帆风顺,也必然伴随着痛苦和失落,我也有很多睡不着的夜晚。但我始终相信着事情会好起来的,我可以的,我努力下一定能够搞定的。比如工作上,我很少有什么畏难情绪,从我个人而言,我反而喜欢那种很有挑战的工作,比如我曾花很大精力在WebGL上手撸一个类IOS滚动条,各种手势、惯性回弹参数、边界情况等,我想大部分人会觉得吃力不讨好,可能会偏向于找个现成的库,然后跟产品说这部分没法实现。而我就会偏向于自己写一个来努力满足所有需求,并且我大概率会乐在其中,即使因此可能变得压力很大,排期很紧张之类的。


  最后,还有一个最最重要的点:身体健康永远排在第一位,没有什么事情比身体健康更重要,不仅是你自己,还包含着你的家人。而保持身体最重要的核心,就是重视它关注它。当你觉得疲惫了,就去休息。当你觉得哪里不舒服了,就去医院做下检查。当你觉得最近生活不太健康,那就动起来,让自己过得更好点。


  所以,生活总结起来就是:健健康康,保持着乐观与积极,做好规划,然后大步地向前走!



🐒 最后,关于我


  非常感谢看到这里的各位小伙伴,感恩!文章的鸡汤浓度有点高,有些地方可能带着一些我的主观,也不一定是正确的,仅供大家参考,如果能对大家有所启发和帮助,那我就很开心了。如果不能理解的,又或者一些我没提到的,比如晋升、绩效啥的,也许可以来跟我讨论讨论,可能也能碰撞出一些有意思的东西。


  另外,我也同样有很多的烦恼和缺点:说要健身,一直动不起来。说要学英语,一直没进展。书也看得不够多。说一定要开始写文章,也一直进展缓慢。有没有大佬能拯救下我。所以期待跟大家一起交流,一起成长,一起变得更好。共勉~





作者:郭东东
来源:juejin.cn/post/7220244993788854309

收起阅读 »

接受外包Offer前一定要清楚的4件事

Hello 我是光影少年。 最近有一些刚毕业的小朋友私信我,说工作贼难找,能不能先去一个软件外包公司先苟着,之后的事情等行情好些了再说。 去外包公司当然没什么不可以,成年人能基于实际做出判断和选择,并承受相应的结果就行。 只是我想补充一些细节,让可能有这个想法...
继续阅读 »

Hello 我是光影少年。


最近有一些刚毕业的小朋友私信我,说工作贼难找,能不能先去一个软件外包公司先苟着,之后的事情等行情好些了再说。


去外包公司当然没什么不可以,成年人能基于实际做出判断和选择,并承受相应的结果就行。


只是我想补充一些细节,让可能有这个想法的小朋友们对这种公司和岗位有一些大概的了解。


在这里先给部分不太清楚的观众扫个盲,软件开发行业有两种基本的用工方式:



  • 作为所服务公司的正式员工,和该公司主体签订劳动合同。

  • 作为所服务公司的劳务派遣员工,和所服务公司委托的第三方公司签订劳动合同。简单来说,你和所服务的公司实际上没有什么关系,是那家签合同的第三方公司的员工,只是替这家公司干活而已。这也是俗称的「外包员工」或者「顾问」


劳务派遣这种用工方式已经广泛地存在于许多的行业。事实上,有些公司中,围绕着派遣员工和正式员工之间的差异,存在着一整套完整的「潜规则」。


围绕着这两种不同类型的员工,在日常工作中往往会衍生出的所谓「戴蓝色工牌」和「戴绿色工牌」的区别对待的概念。


「戴绿色工牌」的他们的工资通常较低,没有其他福利待遇,在某些公司甚至被告知他们不能使用专门的员工通道,也不能参与公司组织的一些诸如团建,抽奖等活动。


「戴蓝色工牌」的是正式员工,有权享受所有的福利待遇。同时要想戴上蓝色工牌是很困难的。


在劳务派遣公司招募「戴绿色工牌」的员工时,往往会拿「表现良好者可以戴上蓝色工牌」这种承诺来作为吸引人的筹码,但是绝少有人能够如愿以偿。


表面上看上去,似乎完全不用选,正式员工就是比派遣员工要好。在大多数情况下这没错。不过我还是想介绍一下派遣员工的现状,和你在考虑是否接受这个岗位时可能会忽略的一些实际情况。


在这里,我只介绍狭义上「劳务派遣」所指的群体,


这种类型的岗位员工和一家代理机构(比如最常见的某软,某辉)签订劳动合同,然后这家机构会把他们派遣到客户公司的现场,和客户公司的雇员一起工作。


相对于客户公司,他们就是「劳务派遣」员工。代理机构会定期向客户收钱,然后从中抽成之后,再把「工资」发给员工。比如客户公司为一个岗位支付的成本是2w/月,很可能发到员工手里只有1w/月(随便举例,不代表实际情况)。


作为这种类型的员工,通常会要求定期填写一份工时记录表,来证明为客户工作的时间,然后根据这张表从所在的机构获得报酬。


代理机构的最根本的盈利模式就是赚差价,所以通常客户向机构支付的费用要比机构实际支付给员工的工资高出 50%~100%的情况也并不算少见。


可能有些小伙伴看到这里已经开始生气了。「存在即合理」,在国内,代理机构起码还得负责员工的五险一金等一些其他的基本保障。往往通过代理机构给大型客户工作,比直接去一家朝不保夕的初创公司还是稳定轻松许多,甚至收入也会高上不少——哪怕已经被拿走了一大部分。


当然,如果咱们已经选择接受一个派遣的岗位,你可以通过一些方式了解到客户给你付出的成本,之后不论是和机构谈判,还是更新对自己的定位都有好处。


同工不同酬


首先就要聊最重要的:

关于薪资。


在大多数情况下,在同等工作岗位上,作为一个派遣员工,只看固定薪资的话,单价是更高的。


在我之前的工作岗位上,我的固定时薪是比不上和我同样职级的顾问同学的。


但是,但是,通常能大量使用派遣员工的大型企业,他们的薪资构成不光是固定薪资。往往是:


薪资总包 = 薪资+绩效+其他(各种福利、股票期权等等)


派遣员工往往只有固定薪资,或者一些超时工作的加班补偿。


稳定性


为什么公司宁可支付更高的单价,也要大量使用派遣员工呢?其中一个很重要的因素就是控制固定成本支出


雇佣一个正式员工,除了薪资总包以外,还需要支付和固定工资匹配的五险一金,其他的各种福利,雇佣一个人付出的成本大概是纸面薪资的1.5倍甚至更高。而且如果公司遇到业务收缩,需要裁撤员工,正式员工也需要支付大量的赔偿金。


如果是派遣员工,那么大可以在项目建设期需要大量人力投入时短期购入大量的人手,在项目上线稳定进入维护期后终止合作,而遣散派遣员工所付出的成本低的可以忽略不计。


所以,如果你对于频繁地更换工作场所有所抵触的话,派遣员工可能对你来说是个很大的挑战。尤其是在有些时段没有什么客户需要,很可能要在代理机构坐冷板凳,甚至被裁撤掉。


工作环境


就像本文开头提到的,有人的地方就有江湖。在有些公司,派遣员工和正式员工之间存在巨大的鸿沟;有些公司中又不是那么明显,但就像一道空气墙,不知道在什么时间就会拦住你的去路。


为了控制成本,在扩张员工规模时的大型企业是十分谨慎的,对待正式员工的招募流程会比派遣员工的进场流程严格的多。高标准,严要求下,如果正式员工的数量还比派遣员工的数量少,那么难免的一些「精英思维」就会开始弥漫。


这是难以避免的,这也是许多在派遣员工岗位的小伙伴反复和我提及的:


一定要端正心态。


在很多你不经意的时候,甚至会有「被抛弃」的感觉。并不是每个客户公司的氛围都那么让人舒服,但这就是正常的,不要太过于纠结这个。


而且派遣员工有一个小小的优势,正式员工通常会因为卷而主动加班,而这种加班在不少地方是无偿的,派遣员工由于是代理机构提供的劳动力,所以通常加班都是有补偿的——毕竟你们的工作就是机构的收益,机构也会争取他们的利益。


长期发展


作为派遣员工,短期收益甚至可以说还不错。进入大型公司的劳务派遣基本不会比去初创企业工作难多少。而且也没有传统大企业对于正式员工所提出的高要求,压死人的KPI之类的影响。


但如果你想从工作中获得的不仅仅只有报酬,更想有一些为日后的履历增光添彩的项目经历的话,派遣员工可能就不是很合适。通常情况下,客户公司总倾向于让派遣员工去做一些相对简单的外围部分工作,核心的部分很难接触到,这没得说。


除非你愿意非常频繁地跳槽来换工作,否则涨薪在派遣员工中也是很困难的,而作为规模公司的正式员工,往往有很成熟的薪资上升机制,也有晋升的机会。


而且业内有一些传言,把进代理机构的经历称之为「弄花简历」,这段经历有可能会对后期想要进入大厂时的筛选过程有一定的影响。


至于说HR招人时所提到的「有转为正式员工的机会」,就像真爱,听过的人多,见过的人少,

作者:幸福少年
来源:juejin.cn/post/7217360688264134713
我也不过多发表意见。

收起阅读 »

四月·十字路口

"睡醒了就去打工,打完工就回去睡觉,想学坏都没时间",打工人的真实写照。即便放假我也是无所适从,乏善可陈。 本月下旬离职ing,这份工作是被迫离职,可以理解为"被开"。从入职之初就定义了这份工作:阶段性过渡。可是踏入社会的几次工作都告诉自己,不停的过渡过渡...
继续阅读 »

"睡醒了就去打工,打完工就回去睡觉,想学坏都没时间",打工人的真实写照。即便放假我也是无所适从,乏善可陈。



40ccab4ceae8ee8656db8ff5145f046.jpg
本月下旬离职ing,这份工作是被迫离职,可以理解为"被开"。从入职之初就定义了这份工作:阶段性过渡。可是踏入社会的几次工作都告诉自己,不停的过渡过渡过渡ing,似乎我完全不明白自己想要的是什么,胡乱抓瞎。


今又四月,最近作息颠倒,精神状态每况愈下,对现状及对未来的过多思考导致夜不能寐,复盘下这一年的种种。为了进行一次有效跳槽,此处稍作思考,梳理下:


1. 工作的能力问题




  • 此前作为初创公司的星麦,我是当时入职应届生中坚持的最久的员工了,很遗憾坚持了半年Pua主动出局。我的前端天花板老大,没有领取年终奖就跑了,对面一排的后端都提出了离职,又或者年后离职,恶劣的人文环境可见一斑。天花板曾打趣说,我应该去外包磨砺一番,这个公司不适合我,也让我回想起入职之初,部门的同事说我并不适合这种初创公司。




  • 现在这个甲方说我能力不足,调过一次项目组,碰到个刚入职的经理跟她发生过嘴角争执,觉得她不懂技术,可能当时对自己太过自信了,没处理好国企领导人之间微妙的关系,后面我又去接手另外一个项目,期间我觉得没有任何不适,这个项目让我明白了,背锅侠一定不要当,且负责人居然要出国深造,他说他扛不住了,人家都有预知风险的能力,我何尝不可?何况他是甲方都要跑,值得深思。不要沦为工位旁边的同事,不然后期颓丧成常态,一发不可收拾。





如果要换工作就好好换工作,不要带着上一份的疲惫和情绪去找工作,更好的割舍就是更好的出发。



2. 下一份工作的规划问题



  • 人文环境:"无政府"状态的工作,温水青蛙,搞不好会把自己干废,影响我的创造力和学习的劲头

  • 项目情况:技术老旧,Jsp写不上简历的项目,天花板很低,一年十年也差别不大

  • 个人成长:原项目无成长,也没有可以深度学习的前辈。好在缓冲了一年,也算看进去了点技术文档

  • 家人爱人:生活和工作状态失衡,心灵比较空虚

  • 职业发展:不能为技术和简历添彩,稳定程度很低,甲方说开就开

  • 离职成本:较低,住房成本到武汉缓冲一点时间,不至于那么焦虑


此次跳槽,属于发展性跳槽,更是为了给自己负责。单独生活了两年,心智逐渐稳定,但人不应该局限在"无朋友"的圈子,要构建新的生活圈,出门丢个垃圾和买瓶饮料已经是极限了。本以为我不会被"形单影只"所影响,终究是高估了自己。我的第一诉求,稳定+技术符合大众潮流(如Ts、React,而不是人人都会的Vue),薪资符合大众市场,首推React技术栈,公司规模中大型,向下扎根,向上开花(长长久久发展)


房子到期还有两个月,前提,找到工作这是一次规划,允许自己一个月找不到工作。不要自乱阵脚,城市选择:Base武汉or杭州。


4. 下一份工作的入职注意点



  • 是否属于外包(问清楚)

  • 面试谈薪

  • 薪资构成

  • 绩效考核

  • 年薪是几薪

  • 五险一金比例

  • 晋升机会

  • 试用期时间


5. 俯瞰未来


青年人,要追逐自己的梦想,立业、成家,有条件去深度探讨自己的出路,商人还是公务员需要后面去论证,需要有人指点迷津,而不是摸瞎过日子。


最后的最后,接下来的每一天都要有计划的准备面试,周末不应该这样度过,玩没玩好,计划也没计划。


作者:鼓浪屿
来源:juejin.cn/post/7219961144584290360
收起阅读 »

Flutter和Android原生通信的三种方式

简介 Flutter虽然有强大的跨平台能力,但是其跨平台主要体现在UI界框架上,对于一些Android原生的功能如:获取电池电量,访问手机蓝牙,定位等硬件信息显得有些不足心。还是需要调用Android原生方法获取。所以使用Flutter和Android原生通信...
继续阅读 »

简介


Flutter虽然有强大的跨平台能力,但是其跨平台主要体现在UI界框架上,对于一些Android原生的功能如:获取电池电量,访问手机蓝牙,定位等硬件信息显得有些不足心。还是需要调用Android原生方法获取。所以使用Flutter和Android原生通信的方式是必不可少的。 本文正在参加「金石计划」


本文主要介绍Flutter与Android原生三种通信方式的用法。


1.BasicMessageChannel


定义


双向通信,,有返回值,主要用于传递字符串和半结构化的信息。


基本使用


1)Flutter端



  1. 创建BasicMessageChannel对象并定义channel的name,必须和Android一端保持一致。


 late BasicMessageChannel<String> _messageChannel;
_messageChannel = const BasicMessageChannel<String>("baseMessageChannel",
StringCodec());


  1. 设置setMessageHandler监听Android原生发送的消息。


_messageChannel.setMessageHandler((message) async {
print("flutter :Message form Android reply:$message");
return "flutter already received reply ";
});


  1. 发送消息给Android原生。


 _messageChannel.send("Hello Android,I come form Flutter");

2)Android端



  1. 初始化BasicMessageChannel,定义Channel名称。


val messageChannel = BasicMessageChannel<String>(
flutterEngine.dartExecutor.binaryMessenger,
"baseMessageChannel",StringCodec.INSTANCE)


  1. 设置setMessageHandler监听来自Flutter的消息。


 messageChannel.setMessageHandler { message, reply ->
Log.i("flutter", "android receive message form flutter :$message")

}


  1. 主动发送消息给Flutter。


  messageChannel.send("flutter")

打印结果如下:



从用法上来看,Flutter和Android原生基本上是一样的,只不过是不同语言的不同写法。Flutter端主动调用send方法将消息传递给Android原生。然后打印log日志。


2.EventChannel


定义


基本使用


单向通信,是一种Android native向Flutter发送数据的单向通信方式,Flutter无法返回任何数据给Android native。主要用于Android native向Flutter发送手机电量变化、网络连接变化、陀螺仪、传感器等信息,主要用于数据流的通信


1)Flutter端



  1. 创建EventChannel对象,并给定channel名称。


late EventChannel _eventChannel;
_eventChannel = const EventChannel("eventChannel");


  1. 使用receiveBroadcastStream接收Android端的消息。


 _eventChannel.receiveBroadcastStream().listen( (event){
print("Flutter:Flutter receive from Android:$event");
},onDone: (){
print("完成");
},onError: (error){
print("失败:$error");
});

2)Android端



  1. 创建EventChannel对象,并定义channel的name。


 val eventChannel  = EventChannel(flutterEngine.dartExecutor.binaryMessenger,"eventChannel")


  1. 设置setStreamHandler监听。


  eventChannel.setStreamHandler(object :StreamHandler{
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
eventSink = events
}

override fun onCancel(arguments: Any?) {
}

})


  1. 发送消息给Flutter端。


 Handler(mainLooper).postDelayed({
Log.i("flutter", "android send message")
eventSink?.success("Hello Flutter")
},500)

打印结果如下:



EventChannel是单向通信,并且只能从Android原生端发送消息给Flutter,Flutter不可以主动发消息,使用有局限性。


3.MethodChannel


定义


双向异步通信,Flutter和原生端都可以主动发起,同时可以互相传递数据,用于传递方法调用。


基本使用


1)Flutter



  1. 创建MethodChannel对象,并定义通道名称。


 late MethodChannel _methodChannel;
_methodChannel = const MethodChannel('methodChannel');


  1. 设置setMethodCallHandler异步监听消息。


 _methodChannel.setMethodCallHandler((call) async {

});


  1. 调用invokeMethod方法发送消息给Android,并接收返回值。


  var map = {'name':'小明','age':18};
String result = await _methodChannel.invokeMethod('openMethodChannel',map);
print("Flutter接收Android返回的结果:$result");

2)Android端



  1. 创建MethodChannel对象,并定义通道名称。


 val methodChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger,"methodChannel")


  1. 设置setMethodCallHandler监听Flutter消息,并通过MethodCall.method区分方法名。


methodChannel.setMethodCallHandler { call, result ->
if (call.method == "openMethodChannel"){
val name = call.argument<String>("name")
val age = call.argument<Int>("age")
Log.i("flutter", "android receive form:$name ,$age ")
result.success("success")
}

}


  1. 发送消息给Flutter。


  messageChannel.send("Hello Flutter")

打印结果如下:



MethodChannel是Flutter和Android原生常用的通信方式,不仅可以异步通信,还能传递数据集合,Map等。通过定义不同的方法名,调用Android不同的功能。


总结


本文主要介绍Flutter与Android原生的三种通信方式,可以根据实际开发中的业务,选择合适的通信方式。熟练掌握三种通信方式可以在Flutter开发中使用Android原生提供的功能。


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

【转载】程序员不爱加班但为啥还总要加班?

连续加班一个多月后,反思一下为啥国内程序员加班这么多 关注程序员耳东,编程转码真轻松 防杠指南:本文不适用于资深大佬,若喷必回 今年过完年之后一直在加班,关注我的粉丝应该也能看出来,2 月份和 3 月份写的笔记确实比较少,最近才开始恢复 加班完毕是得好好思...
继续阅读 »

连续加班一个多月后,反思一下为啥国内程序员加班这么多


关注程序员耳东,编程转码真轻松



防杠指南:本文不适用于资深大佬,若喷必回



今年过完年之后一直在加班,关注我的粉丝应该也能看出来,2 月份和 3 月份写的笔记确实比较少,最近才开始恢复


加班完毕是得好好思考一下,毕竟咱这班也不能白加了对吧,我得好好想一想到底是为什么会导致我加班,我细数了一下平时导致我加班几个主要原因,大家看看有没有共鸣


业务需求倒排期,改的随意


互联网公司的业务迭代是非常快的,尤其是电商、营销相关的业务,基本上随时都在出需求,需求顺排倒还好,无非就是给了排期之后顺着做就行了


但是有一个非常蛋疼的点,如果这个需求业务方要的非常急,比如说 15 号出的需求 PRD ,月底就得上线,必须得倒排,那么就是说上线的时间定了,测试的时间占用一段,联调的时间再占用一段,留给开发的时间真的不多了


时间不够怎么办?要么加人要么加班,加人还有个问题,有的功能并不是很好拆分,而且人多了管理成本也在增加,1+1 并不是一定能等于 2 ,所以到最后就只能全员加班来肝需求


关于业务需求,还有一个可能导致加班的点是改的随意。


之前我在字节跳动打工的时候,每次需求评审会一堆年轻的 PM ,跟唱戏似的,你方唱罢我方上,哭爹喊娘的说自己的需求是多么多么的重要,常用的话术是:我这个需求是 xx 级别的老板看重的、我这个需求可以为公司创造 xx 的收入等等


一个个的 PRD 写的怎么样不重要,最重要的是抢占研发资源,最好可以把程序员固定在自己手里


等到需求开始做了,发现其实 PRD 里面有很多东西没想明白,这个时候就开始改 PRD ,改了 PRD 但是研发排期却不变,那这咋办呢?程序员加班呗


所以国内经常流行一个调侃的对联:


上联是:这个需求很简单


下联是:怎么实现我不管


横批是:明天上线


虽然这个对联是调侃的,但也暗示了很多公司在研发流程的不规范、管理混乱,这也是大部分程序员加班的重要原因


会议太多,占用时间


会议太多这个事情可能是大公司的通病,有时候屁大点事情就拉个会议,我细数了一下我一个月参加的会议:



  1. 需求评审会

  2. 技术方案评审会

  3. 需求复盘会

  4. 细节对齐会

  5. xx 项目启动会议

  6. xx 横向项目

  7. 技术分享会

  8. 周会

  9. 测试用例评审

  10. OKR 会议

  11. CodeReview 会议

  12. 等等......


其实这里面的会议真的太多了,有的团队还有早晨的站会等等,进一步压缩了写代码的时间


那能不能提升效率呢?我觉得可以


就说这个需求评审会吧,如果说每个人会前都能仔细的过一遍 PRD ,记录好疑点,那评审会完全可以开成答疑会,解答完疑问就差不多了,这样子可以节约很多时间,不至于一个需求评审会就开一两个小时


还有技术分享会,很多 leader 为了提升团队的技术氛围会要求组员进行技术分享,但是有的时候,分享的东西别人不一定感兴趣,深度把握的不好的话组员也会只把它当做任务去完成,这就是纯粹的浪费时间了


总之会议这部分,我觉得是一个存在很大提效空间的地方,一个事情是否需要拉会、是否要拉那么多人,是值得思考的


技术需求,各种丐版轮子


关于技术需求这个问题,我不知道是不是国内程序员的特色哈,就是纯做 PM 提的业务需求是很难得到好绩效和晋升的,因为这些事情是你工作职责范围内的事情,你很难说清楚这些事情带来的收益是 PM 的功劳还是研发的功劳


要想得到好绩效、超出预期,那就必须得做一些纯技术的事情,也就是所谓的“技术需求”,而且必须自己挤时间做,不会为这部分工作量专门划时间


常见的技术需求,比如说这两年特别流行的 LowCode 平台,据我所知很多大公司都在搞这种,并且是投入了很多研发的精力在里面的,美其名曰 LowCode 平台可以提高效率,所以在很多需求开发中强行推,要求研发必须使用 LowCode 平台来完成研发,但是在使用的过程中并没有提升效率,反而让研发增加了很多兼容成本和额外的工作量,不管能不能提供效率,先卷了再说


甚至有时候,多个团队之间在卷同样的技术轮子,一个大公司内部至少有 3 个 LowCode 平台、5 个组件库、3 个部署平台、4 个项目管理平台等等,大家都在加班卷技术项目,卷自己团队的存在感和好绩效


到最后,这个技术项目会出现在晋升答辩的 PPT 和汇报材料上,包装后的数字和成果都很亮眼,技术项目的发起者拿到了好绩效、晋升成功,等到晋升成功之后,这个技术项目的使命也就完成了,从此刻开始它就走上了烂尾的道路,历史项目也就留下了一堆烂摊子代码


老老实实做业务需求的人得不到晋升,做各种丐版技术轮子并且强推的人最后得到了晋升,这个问题在国内大公司非常普遍,这也是造成很多研发被卷着加班的重要原因


杂七杂八的事情,耗费精力


程序员还有一些杂事儿,也是相当的耗费精力了,我举几个例子


首先说线上 oncall ,这个事情其实也算是研发的正常工作范围内的事情了,但是如果一天出一个比较麻烦的线上 bug ,那今天肯定其他的事情就没空做了,或者只能加班去做


更不用说,如果所在的部门是基础架构部门的话,要处理技术之外的一些使用答疑事项,这部分事情毫无技术含量,和客服无异


还有就是非常强调技术要去深入业务,好嘛没问题,但是深入业务也是需要耗费时间的,这就意味着你除了读 PRD 以外还得去看 MRD ,可能你需要去和业务部门、市场部门的同事开会旁听 ta 门关心的事情,除过技术相关的东西以外还需要去关注业务指标


这又给自己增加了工作量,leader 不会说专门给这部分工作量去给你增加时间,只能自己挤时间了,这无形中又增加了加班


总结


我总结的这几个原因是我结合自身加班情况分析而来,可能国外的程序员也存在同样的问题,也可能有的人看法不一样,欢迎交流。


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

上海00后小伙AI「复活」奶奶,100%还原音容笑貌,引发巨大争议

**【新智元导读】**最近,00 后小伙用 AI 技术和奶奶实现「对话」,数字生命要成为现实了吗? 你有没有特别想念的人?可能 ta 是你的亲人,也可能是你的伴侣。 无论 ta 是谁,在我们的回忆中永远有他们的一席之地,他们一辈子都会在我们心底最柔软的角落驻留...
继续阅读 »

**【新智元导读】**最近,00 后小伙用 AI 技术和奶奶实现「对话」,数字生命要成为现实了吗?


你有没有特别想念的人?可能 ta 是你的亲人,也可能是你的伴侣。


无论 ta 是谁,在我们的回忆中永远有他们的一席之地,他们一辈子都会在我们心底最柔软的角落驻留。


在哈利波特与死亡圣器中,哈利手握魔法石,他已故的爸爸、妈妈、教父和老师化作幽灵围绕在他的身边。



哈利和他们聊天,攀谈,诉说衷肠,好像他们活过来了一样,紧紧地围绕着他。


而如今,随着 AI 技术和数字虚拟人技术的不断发展,让已故的亲人变成虚拟的数字人继续和我们交谈不再是停留在科幻小说中的情节。


最近,B 站 UP 主吴伍六就用 Midjounrney、ChatGPT 和 D-ID 等最新的 AI 技术「复活」了他刚刚去世的奶奶。


网友看后纷纷表示,恐怖谷。



用 AI「复活」奶奶


在最新的一期视频中,吴伍六为我们还原了「复活」奶奶的整个过程。


首先是头像。要想生成逼真的对话,那么一个栩栩如生的 AI 头像是必不可少的。


吴伍六选择了当下主流的 AI 绘画软件 Midjourney,并上传了一张自己的照片。



最终,他选择了下面这张作为头像,并继续优化。



奶奶的头像也是一样,AI 根据上传的图片与文字描述生成了一些选择,而 UP 主再根据脑海中的回忆挑选一张最逼真的。



之后再如法炮制,用文字细化某些面部特征。比如皱纹、眼眸、发型和神态。我们可以看到,生成的头像细节非常完美。



下一步是音频。这一步的素材基本来自于过去的电话录音、录像视频或者微信语音。而 UP 主将过去奶奶的电话音频上传给了 AI。



并用音频编辑软件 AU 进行调整。调整的方向主要集中在降噪、人声增强等等。


然后将更加清晰的音频样本切割成若干秒的短句,方便进行标注。


最后将处理好的音频打包放入语音合成系统中去。



利用语音合成系统,UP 主就可以尝试输入文本转语音了。


现在,基本的素材都齐全了。接下来就是最关键的一步——生成虚拟数字人。


通过 D-ID,用户可以生成数字虚拟人,并和他们展开交流。



在左侧选择头像,右侧上传文本或音频即可。



网站上奶奶的形象栩栩如生,音频也完美还原了奶奶的「乡音」。搭配上右侧拉家常一般的聊天内容,宛如和奶奶面对面视频通话一样。



有意思的是,奶奶的这些「回复」是 UP 主借助时下大热的 ChatGPT 生成的。


他告诉 ChatGPT,「我很想我的奶奶,你能不能模仿她的口吻来和我交谈?」


透着人情味的人工智能,很难不让人感动落泪。而借助对奶奶的回忆,UP 主也竭尽全力地让 ChatGPT 更像她的奶奶。



科技的最终目标是给人提供力量。这种力量不仅是物质上的,更重要的还是精神上的。虽然这些聊天内容并不完全拟人,也会有一些不贴切的表达。


但总的来说,生活中的小细节还是能给人最温柔的体验的。



数字生命,可行吗?


吴伍六的这段视频引爆了 B 站,网友们众说纷纭。


有关于把逝去的亲人变成虚拟数字人的做法还是个新鲜事物。观念上的转变并不那么容易。


UP 主亲自评论表示,这么做的目的是为了填补自己的小遗憾。在自述中他也提到,奶奶在大年初六去世,最后的几天里奶奶意识模糊,而 UP 主也没有好好的和奶奶告别。


他表示,不会过度缅怀,希望大家能通过 AI 的发展看到未来世界的多样性。



网友「水澹澹而生烟」也表示,这段视频激起了对自己爷爷的回忆。他在 AI 应用和伦理方面并没有过多想法,也不认可把数字虚拟人当作亲人生命的延续。


但他表示,如果能再看爷爷一眼,听一听爷爷的声音,自己就会倍感慰藉。



也有网友表示了不赞同的态度。铁缸哟就认为,数字虚拟人终归还是当不了缅怀之人的精神寄托的。



不过该项技术的未来发展如何,谁也不能预料。就像流浪地球 2 里提到的数字生命的概念一样,想象和现实之间总会存在鸿沟。


没有走到那一步,或者走到那一步之前,一切都是未知的。



当然,小编比较认可网友键垩家の执事的评论。无论虚拟数字人发展到什么地步,多珍藏一些自己所爱之人的照片、音频以及影像这件事,永远不会错。


让记忆停留在心里,让回忆变得更清晰。


参考资料:http://www.bilibili.com/video/BV1QM…


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

阿里正式加入ChatGPT战局,“通义千问”突然上线邀测!

阿里版ChatGPT实录 通义千问作为一个大语言模型,它的能力主要集中在文本生成上,即也能像ChatGPT一样“问啥答啥”,除此之外,它还具备一个“百宝袋”功能,功能也非常酷炫,大家普遍感兴趣的就是菜谱生成、彩虹屁生成器和免费代写情书。里面相当于一个工具箱,能...
继续阅读 »

阿里版ChatGPT实录


通义千问作为一个大语言模型,它的能力主要集中在文本生成上,即也能像ChatGPT一样“问啥答啥”,除此之外,它还具备一个“百宝袋”功能,功能也非常酷炫,大家普遍感兴趣的就是菜谱生成、彩虹屁生成器和免费代写情书。里面相当于一个工具箱,能快速生成各种指定类型的文案。


image.png


通义千问能完成哪些任务,解放哪些生产力,能达到 ChatGPT 几分功力?


从实际表现来看,通义千问与领先的GPT 4.0还有很大的差距,但是对比此前的一些AI大模型,通义千问在问答逻辑、信息检索、语句理解等方面都已经有了明显的进步,我现在对中国AI大模型的未来充满了信心。


image.png


通义千问从何而来?


通义千问的诞生并非偶然,是多年技术积累的结果。


2019年,阿里便开始了大模型的研发,当时阿里发布的语言大模型StructBERT超越谷歌、微软、Facebook,登顶了CLUE榜单。


2021年,阿里达摩院发布了国内首个超百亿参数多模态大模型M6,号称神经元达人类的10倍。


在2022年的世界人工智能大会上,阿里还发布了「通义」大模型系列,并宣布相关核心模型向全球开发者开源开放。


image.png


关于通义千问的技术细节,阿里达摩院官方没有透露详细信息。尽管官方口径低调,但懂的都懂,国内科技圈直接炸裂。实力玩家入场,大模型的竞争真正开始了。


正如ChatGPT脱胎于OpenAI的GPT系列,百度文心一言是自Ernie大模型发展而来,阿里也是国内最早开始研发大模型的技术大厂之一。


其中,M6在多次迭代之后,实现了十万亿级别的参数规模,并且M6和支付宝、淘宝的业务需求相结合,首个在国内实现了商业化落地。在去年的WAIC(世界人工智能大会)上,阿里还发布了通义大模型系列。其中核心模型均已开源开放。


image.png


人人都是低代码开发者


生成式AI产品潜力无穷,但能做出类似应用且可将其商业化的公司却凤毛麟角。不少企业认为AI能够助力企业建设,但企业内部多年积累下来的问题非常复杂,比如企业的一个人工智能平台所需要的数据需要从数十个系统获得,而这些系统的对接工作需要花费大量时间和精力,分属同一领域的不同企业的基础设施并不完全相同,简单复制粘贴是无法达到效果的。


在低代码的发展中,流行着一句话:人人都是低代码开发者。


在云计算与软件业,低代码开发,甚至无代码开发是近两年流行的热门概念。核心逻辑是利用代码库,将已有开发样例进行快速复写,整个开发过程中的人工成本趋近于0。


image.png


一套完善的底座意味着在大部分领域通用的解决方案能力。


JNPF,依托代码开发技术原理因此区别于传统开发交付周期长、二次开发难、技术门槛高的痛点。大部分的应用搭建都是通过拖拽控件实现,简单易上手,通过为开发者提供可视化的应用开发环境,降低或去除应用开发对原生代码编写的需求量,进而实现便捷构建应用程序的一种开发平台,快速助力研发人员快速搭建出一套适合企业发展的方案。


开源链接:http://www.yinmaisoft.com/?from=jueji…


image.png


大模型时代,中国力量加速竞逐


相比于现在的业界标杆ChatGPT,通义千问的进步空间还很大,阿里方面也透露,根据内测反馈,这一大模型正在飞速迭代中。


同时,阿里是全球少数在算法和算力上都有领先布局的公司之一,在算力方面也具备天然的优势。ChatGPT这把火烧到如今,国内对具备足够竞争力的国产生成式大模型的需求,正在与日俱增。


image.png


世界正在改变,一个全新的AI时代正在到来,种种不确定因素,再一次凸显了技术自研的价值。所幸这次,我们的起跑线,并没有相差那么远。


游戏不会在一夜间结束,而现在,竞逐真正开始。最后,大家有什么问题想问通义千问?请在大胆留言。


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

Android TextView中那些冷门好用的用法

介绍 TextView 是 Android 开发中最常用的小部件之一。它用于在屏幕上显示文本。但是,TextView 有几个较少为人知的功能,对开发人员非常有用。在本博客文章中,我们将探讨其中的一些功能。 自定义字体 默认情况下,TextView 使用系统字体...
继续阅读 »

介绍


TextView 是 Android 开发中最常用的小部件之一。它用于在屏幕上显示文本。但是,TextView 有几个较少为人知的功能,对开发人员非常有用。在本博客文章中,我们将探讨其中的一些功能。


自定义字体


默认情况下,TextView 使用系统字体显示文本。但其实我们也可以导入我们自己的字体文件在 TextView 中使用自定义字体。这可以通过将字体文件添加到资源文件夹(res/font 或者 assets)并在 TextView 上以编程方式设置来实现。


要使用自定义字体,我们需要下载字体文件(或者自己生成)并将其添加到资源文件夹中。然后,我们可以使用setTypeface()方法在TextView上以编程方式设置字体。我们还可以在XML中使用android:fontFamily属性设置字体。需要注意的是,fontFamily方式只能使用系统预设的字体并且仅对英文字符有效,如果TextView的文本内容是中文的话这个属性设置后将不会有任何效果。


以下是 Android TextView 自定义字体的代码示例:



  1. 将字体文件添加到 assets 或 res/font 文件夹中。

  2. 通过以下代码设置字体:


// 字体文件放到 assets 文件夹的情况
Typeface tf = Typeface.createFromAsset(getAssets(), "fonts/myfont.ttf");
TextView tv = findViewById(R.id.tv);
tv.setTypeface(tf);

// 字体文件放到 res/font 文件夹的情况, 需注意的是此方式在部分低于 Android 8.0 的设备上可能会存在兼容性问题
val tv = findViewById<TextView>(R.id.tv)
val typeface = ResourcesCompat.getFont(this, R.font.myfont)
tv.typeface = typeface

在上面的示例中,我们首先从 assets 文件夹中创建了一个新的 Typeface 对象。然后,我们使用 setTypeface() 方法将该对象设置为 TextView 的字体。


在上面的示例中,我们将字体文件命名为 “myfont.ttf”。我们可以将其替换为要使用的任何字体文件的名称。


自定义字体是 TextView 的强大功能之一,它可以帮助我们创建具有独特外观和感觉的应用程序。另外,我们也可以通过这种方法实现自定义图标的绘制。


AutoLink


AutoLink 可以自动检测文本中的模式并将其转换为可点击的链接。例如,如果 TextView 包含电子邮件地址或 URL,则 AutoLink 将识别它并使其可点击。此功能使开发人员无需手动创建文本中的可点击链接。


要在 TextView 上启用 AutoLink,您需要将autoLink属性设置为emailphoneweball。您还可以使用Linkify类设置自定义链接模式。


以下是一个Android TextView AutoLink代码使用示例:


<TextView
android:id="@+id/tv3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:autoLink="web"
android:textColorLink="@android:color/holo_red_dark"
android:text="这是我的个人博客地址: http://www.geektang.cn" />

在上面的示例中,我们将 autoLink 属性设置为 web ,这意味着 TextView 将自动检测文本中的 URL 并将其转换为可点击的链接。我们还将 text 属性将文本设置为 这是我的个人博客地址: http://www.geektang.cn 。当用户单击链接时,它们将被带到 http://www.geektang.cn 网站。另外,我们也可以通过 textColorLink 属性将 Link 颜色为我们喜欢的颜色。


AutoLink是一个非常有用的功能,它可以帮助您更轻松地创建可交互的文本。


对齐模式


对齐模式允许您通过在单词之间添加空格将文本对齐到左右边距,这使得文本更易读且视觉上更具吸引力。您可以将对齐模式属性设置为 inter_wordinter_character


要使用对齐模式功能,您需要在 TextView 上设置 justificationMode 属性。但是,此功能仅适用于运行 Android 8.0(API 级别 26)或更高版本的设备。


以下是对齐模式功能的代码示例:


<TextView
android:id="@+id/text_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="This is some sample text that will be justified."
android:justificationMode="inter_word"/>

在上面的示例中,我们将 justificationMode 属性设置为 inter_word 。这意味着 TextView 将在单词之间添加空格,以便将文本对齐到左右边距。


以下是对齐模式功能的显示效果示例:


image.png
同样一段文本,上面的设置 justificationMode 为 inter_word ,是不是看起来会比下面的好看一些呢?这个属性一般用于多行英文文本,如果只有一行文本或者文本内容是纯中文字符的话,不会有任何效果。


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

速度与安全可兼得!改造异步布局大幅提升客户端布局性能

1. 背景介绍 随着小红书用户规模的不断增长,App 性能对用户体验的影响显得越来越重要,例如页面的打开速度、App 的启动速度等,几十毫秒的提升都能带来业务数据上比较显著的收益。今天要介绍的是对一个官方框架的实践以及优化,期间踩了不少坑,...
继续阅读 »

1. 背景介绍


随着小红书用户规模的不断增长,App 性能对用户体验的影响显得越来越重要,例如页面的打开速度、App 的启动速度等,几十毫秒的提升都能带来业务数据上比较显著的收益。今天要介绍的是对一个官方框架的实践以及优化,期间踩了不少坑,但收益也很可观。


AsyncLayoutInflater 最早于 2015 年出现在 support.v4 包中,用来异步 inflate 布局。通常来讲 inflate 需要在主线程执行,所以是一个页面初始化过程中的耗时主要部分,这个工具提供了可以在异步 inflate 的能力,进而减少主线程堵塞。本文主要介绍工具的使用以及如何改进,以及改进中遇到的一些问题。


2. 使用


AsyncLayoutInflater 的使用非常简单,只需要加入一个依赖即可。



同时在代码中的使用如下:



在异步 inflate 好之后会有回调,这时候就可以使用 view 了。


3. 源码分析


这个工具最厉害的地方就在于异步 inflate view 居然没有出现线程安全相关的一些问题,下面我们就来看看它是怎么处理线程安全的问题的。



首先,里面有一个 Thread 的单例,单例里有一个线程安全的阻塞队列和一个线程安全的对象池。



这个单例里有个方法是 enqueue 方法,会调用阻塞队列的 put,将 request 插入队列中。因为是一个线程安全的队列+线程安全的对象池,所以这一系列操作就保证了线程安全。


下面是inflate的流程,inflate的时候会通过 mInflateThread.obtainRequest 从对象池里拿到一个 request,然后再将这个 request 插入队列中。



下面是一个简化过的代码,run 中有一个死循环,通过阻塞队列的 take 元素进行 inflate 的操作。



以上这个简单的工具就分析完了。这部分基本就回答了线程间如何同步数据的一个问题,在一个典型的生产者消费者模型中加入线程安全的容器即可保证。


4. 问题与改进


在使用中还是遇到很多线程相关的问题,以下列举几点相对重要的问题进行阐述。


4.1 单线程与多线程


InflateThread 在这里的设计是一个单例单线程,当需要对线程有一些定制或者收拢的话,改动就有些麻烦了,这里可以通过开放一个设置线程池的方法来提供一些线程管理和定制的能力,默认可以内置一个单线程的线程池。


通过比较长时间的实验我们发现,在主线程比较空闲的时候,单线程的效果会好一些,因为都在大核上执行了,效率更高。主线程繁忙的时候,例如冷启阶段,多线程会有更好的效率。


4.2 ArrayMap 与线程安全


我们在实际使用中发现,在一些自定义 View 的构造函数中和 darkmode 的实现中使用了 SimpleArrayMap 或 ArrayMap,ArrayMap 是 SimpleArrayMap 的子类,本身 SimpleArrayMap 是用过两个 static 的数组来实现对象的缓存,从而起到复用的作用,在多线程的情况下会有线程安全问题,这里会出现复用对象不匹配导致的 crash。一个简单的方式就是当出现 crash 的时候讲对应的 cache 数组清空,即可避免。



4.3 inflate、锁与线程安全


LayoutInflater 的 inflate 方法中有一个锁,这个导致了如果你想多线程去调用 inflate 的时候,起不到多线程的效果,如果是单线程的情况下,还可能遇到和主线程在 inflate 时同样等待锁的问题。这里 mConstructorArgs 是一个成员变量,通过重写 LayoutInflater 中的 cloneInContext 方法,配合对象池就可以避开这里锁的问题。



同时 inflate 过程中用到的这些数组和容器类型,都不是线程安全的,如果想要去掉 inflate 方法开头的 synchronize 的限制,这些线程不安全的容器类也是需要特别注意的。



4.4 BasicInflater 改造


AsyncLayoutInflater 本身有一个 BasicInflater,根据以上的一些改进点,我们在实践中对其做了一些改造,扩展出了可以设置线程池的接口,使用了基础架构提供的线程池,做到了对线程的统一管理。实践下来,在CPU比较繁忙的时候,多线程的线程池效果要好于单线程,当 CPU 比较空闲的时候,单线程的效果会更好一些,因为可以更好的利用释放出来的CPU 大核的性能。



同时重写了 ArrayMap 中线程不安全的一些处理方式,使得在多线程使用 ArrayMap 或者使用依赖 ArrayMap 的功能时不会出现 crash,这里涉及到了我们的一些自定义 View 和我们的 darkmode 的实现。


在对于 inflate 的锁和一些线程不安全的容器处理上,重写了LayoutInflater 的 cloneInContext 方法去掉了 synchronized 的限制,同时在 onCreateView 的流程中加入了线程安全的容器来保障 inflate 过程的线程安全。



综合来说就是重写了 AsyncLayoutInflater,ArrayMap 和 LayoutInflater,以达到线程安全的目的,同时将这些融入到我们的业务框架中,使得使用成本更低。


4.5  ViewCache


另一个实践是在业务侧做了进一步的封装,通过一个 ViewCache  的单例,提前将一些模块化的 View 提前 inflate 好,存在 ViewCache 中,在后续需要使用的时候从 ViewCache 中在获取,这样就避免了用的时候再 inflate 导致的耗时问题了。这块整体的代码比较简单,就不单独展开讲了,需要注意的点是有些 View 没有被使用需要及时释放,避免内存泄漏。


5. 总结


AsyncLayoutInflater 的实践与优化,前后持续了半年左右,我们在 App 冷启动和笔记详情页的性能优化中获得了超过的 20% 的性能收益以及显著的业务收益。同时,我们也将这个能力沉淀了到了业务框架中,方便了后续的接入和使用成本,通过 ViewCache 和业务框架,基本做到了可以覆盖大部分业务需求的能力。未来,我们将会在框架的易用性以及一些场景的使用上做进一步的优化,结合其他的优化手段给业务方提供更多的选择,使其能在写业务的同时无需关注这部分的耗时与复杂度,从而提升开发效率。


六、作者信息


殇不患


小红书商业技术 Android 工程师,曾负责业务架构设计与性能优化,目前专注于交易链路的迭代与优化。


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

AndroidStudio 常用插件来提高开发效率的必备

Statistic 代码统计工具 款代码统计工具,可以用来统计当前项目中代码的行数和大小。 通过顶部菜单中的View->Tool Windows->Statistic按钮开启该功能。 此时就可以看到我们项目代码的统计情况了 Translati...
继续阅读 »

Statistic 代码统计工具


款代码统计工具,可以用来统计当前项目中代码的行数和大小。


image.png


通过顶部菜单中的View->Tool Windows->Statistic按钮开启该功能。


image.png
此时就可以看到我们项目代码的统计情况了


image.png


Translation 一款翻译插件


image.png


直接选中需要翻译的内容,点击右键即可找到翻译按钮;


image.png


Markdown


IDEA官方出品的一款Markdown插件,支持编辑Markdown文件并进行预览


image.png


Key Promoter X 快捷键提示


Key Promoter X 是一款帮助你快速学习快捷键的插件,当你在AndroidStudio中用鼠标点击某些功能时,它会自动提示你使用该功能的快捷键


image.png
如下图鼠标点击某些功能时,会有对应的快捷键提示


androidstudio快捷键提醒.gif


Restful Fast Request


是IDEA版本的Postman,它是一个功能强大的Restful API工具包插件,在AndroidStudio中也可以根据已有的方法快速生成接口调试用例


image.png
然后使用方法如下:


howToUse_en.gif


PlantUML Integration


PlantUML是一款开源的UML图绘制工具,支持通过文本来生成图形,安装如下:


image.png
时序图(Sequence Diagram),是一种UML交互图。它通过描述对象之间发送消息的时间顺序显示多个对象之间的动态协作。


image.png
如这里生成的是一个简单的时序图


image.png


当然还有更多详细的用法,大家可以查看官网https://plantuml.com/zh/


image.png


Sequence Diagram 根据代码生时时序图


根据代码生成时序图的插件,还支持在时序图上直接导航到对应代码以及导出为图片或PlantUML文件。


image.png


String Manipulation 用来处理字符串


业处理字符串的插件,支持各种格式代码命名方式的切换、支持各种语言的转义和反转义、支持字符加密、支持多个字符的排序、对齐、过滤等等。


image.png


然后在使用的时候只需要选中字符串,点击右键


image.png


Rainbow Brackets 彩虹括号


image.png
安装好后,重新启动 AndroiStudio 打开项目


image.png


Android Wifi 连接手机进行调试




使用Android Studio Dolphin | 2021.3.1 Patch 1 及以上版本选择点击 Pair Devices Using Wi-Fi 弹出扫码框。提示使用android11+ 的设备扫码连接。
这时需要手机和电脑连同一个无线网。然后在手机开发者选项里面找到 无线调试,一般在USB调试开关下面。点击 无线调试 开启 无线调试 功能。点击无线调试页面的 使用二维码配对设备 扫描AS的二维码链接调试。等待一会链接好后就可以看到设备信息了。


image.png


CodeGlance Pro


代码视图页面生成浏览目录


image.png
安装成功后重启AndroidStudio


image.png


SonarLint 代码 review 插件


Sonar是一个用于代码质量管理的开源平台,用于管理源代码的质量 通过插件形式,可以支持包括java,C#,C/C++,PL/SQL,Cobol,JavaScrip,Groovy等等二十几种编程语言的代码质量管理与检测


image.png


安装成功后重启 AndroidStudio ,打开安卓项目如下:


image.png


目前支持的代码语言如下 官网在这里


image.png


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

2023年 android 裸辞跳槽经历

前言 上家公司我呆了整整三年,趁着合同到期,不想续签了,于是想出来试试新的机会,从毕业到现在每次换工作都是裸辞的,哪怕今年行情不怎么好也是裸辞的,自我认为,一来经济上也没有太大压力,二来裸辞之后能全心全意准备面试,成功率大大提升,但也不排除运气太差,一直找不到...
继续阅读 »

前言


上家公司我呆了整整三年,趁着合同到期,不想续签了,于是想出来试试新的机会,从毕业到现在每次换工作都是裸辞的,哪怕今年行情不怎么好也是裸辞的,自我认为,一来经济上也没有太大压力,二来裸辞之后能全心全意准备面试,成功率大大提升,但也不排除运气太差,一直找不到工作的情况,所以每个人需要根据自己的实际情况做选择。


我是 2 月 1 号先找我的上级聊离职的事情,然后又找公司人事聊合同不续签的事情,然后约定在 3 月 15 号离职。提完离职之后跟想象的不一样,以为提完离职就可以放松下来了,没想到一直到 3 月 14 号还在写需求🤣


复习准备


提完离职之后就开始准备简历和投简历了,因为离职这件事从去年就开始考虑了,所以在去年 11 月份就开始有意识的准备了,刷算法,刷八股文。大概用了两个月时间,leetcode 刷了 100 道题,当然一开始刷算法肯定很受挫,第一题就不会,这很正常,我一般是先看题目,不会就看题解,然后背,每隔一周就把上周刷的算法再复习一遍。


640.jpeg


关于八股文的准备就是复习 Java,kotlin,Android 基础,计算机网络,数据结构,还有设计模式等等部分,这些东西比较耗时同时知识点比较多又杂,所以只能慢慢复习,就跟大学考研一样,第一遍就是全面复习,然后把不会的知识点记录下来,然后每天上下班地铁上快速的过一遍,就是无意识的记录,看到一个知识点就在脑子里回想跟它有关联的其他知识点,这样一直持续到 2 月份,然后还有简历,简历很重要,因为面试官基本都是根据你简历上面写的提问,当然也不排除不按套路来的面试官,我就碰到过一次,一上来就一直问我计算机网络底层相关的东西,如 socket,websocket,xmpp 以及如何处理高并发的东西,然后其他东西一点都没问,这就让我很郁闷了,总之简历一定要写好,同时简历里面的提到的知识点一定要滚瓜烂熟。


找工作途径


关于在哪里投递简历的话,我是用了拉钩,BOSS,51job,猎聘网,然后拉钩充值了一个月会员 169 元(屁用都没有,感觉拉钩已经死掉了,职位不更新,投简历没反应),BOSS 先买了一个月会员,然后又续费了一个月,每个月 69 元,最后也是通过 BOSS 面试找到工作的。大伙有需要可以了解一下,这里不是给 BOSS 打广告哈。然后 51job 这个 App,用了一圈感觉都是外企或者国企事业单位发布的职位比较多,不过整体感觉用处不大,没起什么作用。猎聘网 App,广告打得到处都是,然而没卵用。这就是我找工作的途径。


offer情况


从 2 月份开始投递简历,大概投了几十份简历,总共收到的面试机会大概有 15 家,参加的面试有 10 家左右,还有其他的就是不合适啥的就拒绝掉了,最后统计了一下,只有成功了 3 家,收到了 3 家录用通知,成功率只有 30%,因为我的目标很明确,就是想去外企,国企事业单位,初创企业,互联网的公司不大想去,所以一些大厂我都没有投简历。可以想象今年的行情很差,我三年前也就是在 20 年的时候换工作,那时候都是工作追着你跑的,今年行情的确不行,你去找工作,别人都不愿意搭理你。


关于具体的面试经验总结可以看我公众号里面具体的文章,以上便是我今年换工作的整体情况。


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

给计算机专业学生的十大建议 后悔毕业才知道

经常有人问起计算机专业怎么学习,根据多年的实践经验,总结出这十条,欢迎大家收藏学习。 1. 注重计算机基础的学习 俗话说“勿在浮沙筑高台”,计算机基础是你作为一个科班生与非科班生的主要差别,也是一个计算机专业的核心竞争力,这玩意就像一个人的内功一样,学好了,...
继续阅读 »

经常有人问起计算机专业怎么学习,根据多年的实践经验,总结出这十条,欢迎大家收藏学习。



1. 注重计算机基础的学习


俗话说“勿在浮沙筑高台”,计算机基础是你作为一个科班生与非科班生的主要差别,也是一个计算机专业的核心竞争力,这玩意就像一个人的内功一样,学好了,受益无穷。像经典的《深入理解计算机系统》、《算法》、《数据结构与算法》等,学好基础再学习其他框架绝对是事半功倍。


2. 培养独立思考的能力。


遇到问题了,第一时间不是想着去问别人,而是先通过自己的努力,利用搜索引擎去查一查,慢慢提高自己的自我问题解决能力。这点上,我们可以好好利用Google搜索,这点不必多讲,在技术方面,国内百度还有其他搜索的结果简直惨不忍睹。另外有一个小的tips,在国内有很多google镜像网站,可以解决无法直接访问的问题。


3. 参加有价值的竞赛。


多参加一些有价值的计算机竞赛,比如:ACM(含金量最大)、GPLT、蓝桥杯、Kaggle、阿里天池、百度之星、中国大学生计算机设计大赛这里面含金量最大的就是ACM。一方面能提升自己的实践能力,另一方面做的好还可以得到丰厚的奖金和直通大厂的实习和工作机会,千万不要错过!特别是对于那些只上了普通学校的计算机专业的学生来说,这个,更是可以提高你将来就业竞争力的不二法宝。


4. 刷题刷题刷题。


重要的事情说三遍,主要就是刷算法题,需要你先学习《数据结构与算法》,之后你就可以多刷题,多刷题,多刷题,至于刷题网站可以推荐Leetcode和牛客网。之所以刷题这么重要,是因为后面找工作无论是私企还是外企,基本上每轮面试都会来几道算法题,而且像字节这种公司,可能算法题的难度是Hard级别,所以早点准备绝对没有错。


5. 去找个实习。


提前实习,好处多多,尤其不准备考研,毕业就准备工作的,主要的实习其实也就是在暑假时间了。根据我过往经验来看,有实习经历绝对是找工作面试时的一大优势,另外实习也可以提前让我们熟悉工作环境和社会的实际需求,帮助我们发现自己的短板,毕竟课堂和社会还是有很大差别的。所以有机会一定要尽早实习,并且越多实习越好!


6. 尝试系统化学习。


现在是一个知识爆炸的时代,各种自媒体满天飞,大家时间也越来越碎片化。如果对学习也没有一个规划,今天看到Python很吃香就学Python,明天看到AI前景很好,就开始搞AI,最后可能时间也花费了不少,但是啥也没学到。因此系统化学习非常重要,比如我要学数据库,那我就从基本的《SQL必知必会》,接着学《高性能MySQL》,最后学习《MySQL技术内幕》,只有这样才能系统掌握好一门技术!


7. 多做项目多实战。


多折腾项目,千万不要只做一个理论派,理论固然重要,但是离开了实战,理论的作用将打折扣,所以身为一个计算机专业的学生,除了要学好学透必备的理论知识,一定要花时间多去折腾一些项目!比如可以自己尝试从前端到后端搭建一个网站,从中你就可以积累项目开发经验,提升编程能力,为将来找工作打下坚实基础。


8. 学会使用GitHub。


这个真的是一个巨大的资源,这个也号称是程序员最大的同性交友网站,我觉得,每一个计算机专业的人都应该知道它,会基本的使用它,在这上面,你可以找到很多有趣实用的项目供你学习。上面还有很多开源顶级项目,像Go、Spark等,你也可以试着给他们提交代码,如果最后能够被接收,仅凭这点,你就可以在面试时秒杀90%的人了!


9. 坚持写博客分享技术。


很多程序员都在保持着写博客的习惯,这是对程序员非常有利的一件事情。把自己的日常所学,形成文字分享出来,不仅仅能帮助自己对技术的深入理解,还可以积累影响力,拓展自己的圈子等等,好处多多!


10. 锻炼身体。


最重要而最容易被忽视,千里之行始于足下,身体才是革命的本钱,如果没有身体,上面的都是空谈!!!平时有空就打打球、跑跑步,养成良好的生活习惯,学会给自己的身体投资!!!



关注公众号【码老思】,一时间获取最通俗易懂的原创技术干货。


作者:码老思
来源:juejin.cn/post/7216319218504155191

收起阅读 »

GeoJSON:地理信息的JSON表示法

web
简介 GeoJSON 是一种使用 JSON 来编码各种地理数据结构的格式,是一种轻量级的数据交换格式,可以用来表示几何对象、属性数据、空间参考系统等信息 由两种对象组成:Geometry(几何对象)和 Feature(空间行状) 几何对象用来描述地理空间中的...
继续阅读 »

简介


GeoJSON 是一种使用 JSON 来编码各种地理数据结构的格式,是一种轻量级的数据交换格式,可以用来表示几何对象、属性数据、空间参考系统等信息


由两种对象组成:Geometry(几何对象)和 Feature(空间行状)



  • 几何对象用来描述地理空间中的点、线、面等几何形状

  • 空间行状用来描述一个有界的实体,包括几何对象和其他属性信息


几何对象类型有:



  • 点:Point

  • 多点:MultiPoint

  • 线:LineString

  • 多线:MultiLineString

  • 面:Polygon

  • 多面:MultiPolygon

  • 几何集合:GeometryCollection


空间行状类型有:



  • 空间行状:Feature

  • 空间形状集合:FeatureCollection


举例


几何对象和空间行状可以相互嵌套


const GeoJSON = {
type: "FeatureCollection",
features: [
{
type: "Feature",
geometry: { type: "Point", coordinates: [121.4737, 31.2304] },
properties: { id: 1 },
},
{
type: "Feature",
geometry: { type: "Point", coordinates: [121.4837, 31.2504] },
properties: { id: 2 },
},
],
};

空间行状


FeatureCollection


FeatureCollectionFeature 对象的集合,用来表示一组 Feature 对象


typefeatures 两个属性组成:



  • type 属性的值为 FeatureCollection

  • features 属性的值为 Feature 对象的数组


const FeatureCollectionJSON = {
type: "FeatureCollection",
features: [feature],
};

Feature


Feature 对象用来表示几何对象的属性信息


typegeometryproperties 三个属性组成:



  • type 属性的值为 Feature

  • geometry 属性的值为 Geometry 几何对象

  • properties 属性的值为属性对象(可选)


const FeatureJSON = {
type: "Feature",
geometry: { type: "Point", coordinates: [121.4737, 31.2304] },
properties: { id: 1 },
};

几何对象


Point


Point 用来表示一个点


typecoordinates 两个属性组成:



  • type 属性的值为 Point

  • coordinates 属性的值为一个数组,数组的第一个元素为经度,第二个元素为纬度


const PointJSON = {
type: "Point",
coordinates: [121.4737, 31.2304],
};

MultiPoint


MultiPoint 用来表示多个点


typecoordinates 两个属性组成:



  • type 属性的值为 MultiPoint

  • coordinates 属性的值为一个数组,数组的每个元素都是一个点的坐标


const MultiPointJSON = {
type: "MultiPoint",
coordinates: [
[121.4737, 31.2304],
[121.4837, 31.2504],
],
};

LineString


LineString 用来表示一条线


typecoordinates 两个属性组成:



  • type 属性的值为 LineString

  • coordinates 属性的值为一个数组,数组的每个元素都是一个点的坐标


const LineStringJSON = {
type: "LineString",
coordinates: [
[121.4737, 31.2304],
[121.4837, 31.2504],
],
};

MultiLineString


MultiLineString 用来表示多条线


typecoordinates 两个属性组成:



  • type 属性的值为 MultiLineString

  • coordinates 属性的值为一个数组,数组的每个元素都是一个线的坐标数组


const MultiLineStringJSON = {
type: "MultiLineString",
coordinates: [
[
[121.4737, 31.2304],
[121.4837, 31.2504],
],
[
[121.4727, 31.2314],
[121.4827, 31.2514],
],
],
};

Polygon


Polygon 用来表示一个面


typecoordinates 两个属性组成:



  • type 属性的值为 Polygon

  • coordinates 属性的值为一个数组,数组的第一个元素为外环的坐标数组,后面的元素为内环的坐标数组


polygon 的坐标数组的第一个元素和最后一个元素是相同的,表示闭合


const PolygonJSON = {
type: "Polygon",
coordinates: [
[
[121.4737, 31.2304],
[121.4837, 31.2504],
[121.4937, 31.2304],
[121.4737, 31.2304],
],
[
[121.4717, 31.2314],
[121.4827, 31.2524],
[121.4937, 31.2334],
[121.4757, 31.2344],
],
],
};

MultiPolygon


MultiPolygon 用来表示多个面


typecoordinates 两个属性组成:



  • type 属性的值为 MultiPolygon

  • coordinates 属性的值为一个数组,数组的每个元素都是一个面的坐标数组


const MultiPolygonJSON = {
type: "MultiPolygon",
coordinates: [
[
[
[121.4737, 31.2304],
[121.4837, 31.2504],
[121.4937, 31.2304],
[121.4737, 31.2304],
],
[
[121.4737, 31.2304],
[121.4837, 31.2504],
[121.4937, 31.2304],
[121.4737, 31.2304],
],
],
[
[
[121.4737, 31.2304],
[121.4837, 31.2504],
[121.4937, 31.2304],
[121.4737, 31.2304],
],
[
[121.4737, 31.2304],
[121.4837, 31.2504],
[121.4937, 31.2304],
[121.4737, 31.2304],
],
],
],
};

GeometryCollection


GeometryCollection 用来表示几何对象的集合


typegeometries 两个属性组成:



  • type 属性的值为 GeometryCollection

  • geometries 属性的值为几何对象的数组


const GeometryCollectionJSON = {
type: "GeometryCollection",
geometries: [
{ type: "Point", coordinates: [121.4737, 31.2304] },
{
type: "LineString",
coordinates: [
[121.4737, 31.2304],
[121.4837, 31.2504],
],
},
],
};

可选属性


这些属性都是 GeoJSON 的扩展属性,不是 GeoJSON 规范的一部分



  • id 属性,用来描述 FeatureCollection 的唯一标识

  • bbox 属性,用来描述 FeatureCollection 的边界框

    • 四至坐标,一般用来做数据裁剪

    • 这是一组左上角和右下角的坐标,示例:[minLon, minLat, maxLon, maxLat]



  • properties 属性,用来描述 FeatureCollection 的属性

  • crs 属性,用来描述坐标参考系


其他


coordinate


coordinate 是一个数组,表示一个点的坐标,数组的长度表示坐标的维度,一般是 2 维或 3



  • 2 维:[lon, lat]

  • 3 维:[lon, lat, height]


coordinate 的第一个元素表示经度,第二个元素表示纬度,第三个元素表示高度


坐标顺序是 [lon, lat],这个是推荐顺序,可由 crs 属性指定


coordinates 是多维数组:



  • 点:[lon, lat]

  • 线:[[lon, lat], [lon, lat]]

  • 面:[[[lon, lat], [lon, lat]]]

  • 多面:[[[[lon, lat], [lon, lat]]]]


坐标参考系


最常使用的坐标系是 EPSG:4326EPSG:3857



  • EPSG:4326WGS84(CGCS2000,大地) 坐标系,是 GeoJSON 规范的默认坐标系

  • EPSG:3857Web Mercator(墨卡托) 坐标系,是 OpenLayers 的默认坐标系


它们的区别:



  • EPSG:4326 是经纬度坐标系,EPSG:3857 是投影坐标系

  • EPSG:4326 的坐标范围是 [-180, -90, 180, 90]EPSG:3857 的坐标范围是 [-20037508.342789244, -20037508.342789244, 20037508.342789244, 20037508.342789244]

  • EPSG:4326 的坐标单位是度,EPSG:3857 的坐标单位是米

  • EPSG:4326 的坐标原点是 [0, 0]EPSG:3857 的坐标原点是 [-20037508.342789244, -20037508.342789244]

  • EPSG:4326 的坐标轴方向是 [x, y]EPSG:3857 的坐标轴方向是 [x, -y]


在 ts 中使用


为了在 ts 使用 GeoJSON 能够有类型约束,我整理整理了一些 GeoJSONts 类型定义和创建 GeoJSON 的方法:



举例:




  1. 表示一个点和多个点的 GeoJSON 集合:


    使用geojson.d.ts


    type PointType = FeatureCollection<Point | MultiPoint, GeoJsonProperties<T>>;

    const point2Geojson: PointType<{ id: string; name?: string }> = {
    type: "FeatureCollection",
    features: [
    {
    type: "Feature",
    geometry: {
    type: "Point",
    coordinates: [120.4737, 31.2304],
    },
    properties: { id: "12", name: "uccs" },
    },
    {
    type: "Feature",
    geometry: {
    type: "MultiPoint",
    coordinates: [
    [121.4737, 31.2304],
    [111.4737, 31.2204],
    ],
    },
    properties: { id: "1" },
    },
    ],
    };



  2. 创建一个几何对象


    使用geojson.helper.ts


    const pointGeometry = point<{ id: string }>([120.4737, 31.2304], {
    id: "1",
    });
    const featureGeoJSON = feature<Point>(pointGeometry);



参考


收起阅读 »