注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

最近遇到的奇葩进度条

web
前言 本文将介绍几个我最近遇到的奇葩进度条,需求看似简单,但是我们可以用平常巧妙的属性来解决,再也不用复杂的html结构和颜色渐变算法。 “奇葩”的环形渐变进度条 需求描述:需要环形渐变的进度条让人快速理解进度实现程度,10-20%是青绿色,20%到30%是黄...
继续阅读 »

前言


本文将介绍几个我最近遇到的奇葩进度条,需求看似简单,但是我们可以用平常巧妙的属性来解决,再也不用复杂的html结构和颜色渐变算法。


“奇葩”的环形渐变进度条


需求描述:需要环形渐变的进度条让人快速理解进度实现程度,10-20%是青绿色,20%到30%是黄色.....


乍一看是不是很容易,但是我思来想去用了echarts的svg渲染,但是只要到了90%,一定会渐变到青绿色,从红色渐变到青绿色,做实让我心一凉。


image.png


思路一:径向渐变分割


网上思路很多,稍微复杂的比如分割区域做大量的颜色的径向渐变。原理是将rgba转为16进制计算颜色插值。这样我们通过计算step步长就可以根据细分做渐变了。但是好像无法很好满足我们的指定区域10%-20%是某种颜色,虽然可以但是也太麻烦了。


  function gradientColor(startRGB, endRGB, step) {
let startR = startRGB[0]
let startG = startRGB[1]
let startB = startRGB[2]
let endR = endRGB[0]
let endG = endRGB[1]
let endB = endRGB[2]
let sR = (endR - startR) / step // 总差值
let sG = (endG - startG) / step
let sB = (endB - startB) / step
var colorArr = []
for (var i = 0; i < step; i++) {
let color = 'rgb(' + parseInt((sR * i + startR)) + ',' + parseInt((sG * i + startG)) + ',' + parseInt((sB * i + startB)) + ')'
colorArr.push(color)
}
return colorArr
}

思路二:CSS结合svg


我们可以用css的background: conic-gradient


background: conic-gradient(#179067, #62e317, #d7f10f, #ffc403, #fcc202, #ff7327, #ff7327, #FF5800, #ff5900, #f64302, #ff0000, #ff0000);

image.png


看着好像不错,那么接下来只要我们做个遮罩,然后用svg的strokeDashoffset来形成我们的环状进度条就可以了。至于百分之几到百分之几我们可以将conic-gradient内部属性做个百分比的拆分就可以了


image.png


<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<style>
.circle {
width: 300px;
height: 300px;
background: conic-gradient(#179067, #62e317, #d7f10f, #ffc403, #fcc202, #ff7327, #ff7327, #FF5800, #ff5900, #f64302, #ff0000, #ff0000);
border-radius: 50%;
position: relative;
}

#progress-circle circle {
stroke-dasharray: 880;
stroke: #f2f2f2;
}

#progress-circle {
transform: rotate(-90deg);
}

.circle-mask {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 2;
width: 260px;
height: 260px;
background: #fff;
border-radius: 50%;
}
</style>

<body>
<div class="circle">
<svg id="progress-circle" width="300" height="300">
<circle r="140" cx="150" cy="150" stroke-width="21" fill="transparent" />
</svg>
<div class="circle-mask"></div>
</div>
</body>
<script>
const circle = document.querySelector('#progress-circle circle');
const radius = circle.r.baseVal.value;
const circumference = radius * 2 * Math.PI;
function setProgress(percent) {
const progress = circumference - (percent / 100) * circumference;
circle.style.strokeDashoffset = -progress;
}
let prog = 40
let val = 100 - prog
setProgress(val); //设置初始进度

</script>

</html>

这里简单讲下逻辑,我们通过计算环的周长,总长其实就是stroke-dasharray,通过strokeDashoffset来偏移我们的虚线线段,那么开始的就是我们的实线线段。其实就是一个蚂蚁线。让这个线长度等于我们的环长度,通过api让实线在开始的位置。


最终效果


image.png


"奇葩"的横向进度条


在我们平常需求用用组件库实现进度条很容易,但是我们看看这个需求的进度条的场景,文字要能被裁剪成黑白两色。


image.png


image.png


思路一: overflow:hidden


具体就不演示了,内部通过两个副本的文案,一套白色一套黑色,通过定位层级的不同,overflow:hidden来隐藏,缺点是相对繁琐的dom结构。


思路二: background-clip 裁剪


<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<style>
:root {
--d: 20%
}

.inverted {
padding: 0 8px;
display: flex;
justify-content: space-between;
background: linear-gradient(-90deg, #000 var(--d), #fff 0) no-repeat, linear-gradient(-90deg, #0000 var(--d), rgb(192, 23, 23) 0) no-repeat;
-webkit-background-clip: text, padding-box;
background-clip: text, padding-box;
color: #0000;
font-weight: bold;
cursor: pointer;
}

.box {
background: #ebebeb;
width: 300px;
border-radius: 24px;
overflow: hidden;
}
</style>

<body>
<div class="box">
<div class="inverted">
<div class="inverted-item">888w/12</div>
<div class="inverted-item">100%/10s</div>
</div>
</div>
</body>

<script>
function modifyProg(prog) {
let val = 100 - prog
document.documentElement.style.setProperty('--d', val + '%')
}
modifyProg(6)
</script>

</html>

这里我们主要用了background-clip的text和padding-box两个裁剪,一个裁剪文本,一个裁剪背景延伸至内边距padding外沿。不会绘制到边框处。在js中我们通过setProperty修改css变量即可。


最终效果


image.png


附录



  1. 卷出新高度,几百行的Canvas游戏秘籍 - 掘金 (juejin.cn)

  2. 为什么我的WebGL开发这么丝滑 🌊

  3. Echarts无法实现这个曲线图😭,那我手写一个 - 掘金 (juejin.cn)

  4. Redux已死,那就用更爽的Zustand - 掘金 (juejin.cn)<
    作者:谦宇
    来源:juejin.cn/post/7244172094547492923
    /a>

收起阅读 »

区块链中的平行链

随着区块链技术的不断发展,人们对于区块链的应用也越来越广泛。在这个过程中,平行链技术应运而生。那么,什么是平行链呢? 1. 平行链的定义和概念 平行链是一种特定的应用程序数据结构,它是全局一致的,由 Polkadot 中继链的验证节点进行验证] 。它允许交易...
继续阅读 »

随着区块链技术的不断发展,人们对于区块链的应用也越来越广泛。在这个过程中,平行链技术应运而生。那么,什么是平行链呢?


1. 平行链的定义和概念


平行链是一种特定的应用程序数据结构,它是全局一致的,由 Polkadot 中继链的验证节点进行验证] 。它允许交易在专用的 Layer1 区块链生态系统中并行分布和处理,从而显著提高了吞吐量和可扩展性


平行链技术来自于波卡项目。Polkadot 是一个可伸缩的异构多链系统,其本身不提供任何内在的功能应用,主要是连接各个区块链协议,并维护协议通讯有效安全,并保存这些通讯信息。平行链可以作为公共或私有网络运行,可以用于企业或组织运行,可以作为平台运行,让其他人在他们的基础上构建应用程序或作为公共利益平行链,来造福整个 Polkadot 生态系统,以及各种各样的其他模型 。


简单来说,平行链是一种依赖于波卡中继链提供安全性并与其他中继链通信的自主运行的原生区块链。它是一种简单、易扩展的区块链,它的安全性由“主链”提供。


2. 平行链与主链的关系


平行链是一种独立的区块链,它与主链(mainchain)通过双向桥接相连。它允许代币或其他数字资产在主链和平行链之间进行转移。每个平行链都是一个独立的区块链网络,拥有自己的代币、协议、共识机制和安全性


双向桥(two-way bridge)是一种连接两个区块链的技术,它允许资产在两个区块链之间进行转移。有单向(unidirectional)桥和双向(bidirectional)桥两种类型。单向桥意味着用户只能将资产桥接到一个目标区块链,但不能返回其原生区块链。双向桥允许资产在两个方向上进行桥接


平行链和主链保持既独立又连结的关系,在主链之下,它可以拥有自己的超级节点、状态机和原始交易数据。主链可以给平行链做跨链操作,从而形成链条生态系统。


3. 平行链的优势


平行链不仅可以利用系统的共识保证安全,还可以共享原有的生态。它们执行的计算本质上是独立,但又连结在一起。平行链之间有明确的隔离分界线,可以立即执行所有交易,而不用担心和其他链产生冲突。


使用平行链的原因是它能够显著提高吞吐量和可扩展性,并且能够支持多种不同的应用场景。


平行链可以用来运行区块链应用程序,如去中心化应用程序(dapps),并将计算负载从主链上移除,从而帮助扩展区块链。它们还可以与其他扩展解决方案结合使用。尽管平行链看起来是一个有前途的解决方案,但它们增加了区块链设计的复杂性,并且需要大量的努力和投资进行初始设置。由于平行链是独立的区块链,它们的安全性可能会受到损害,因为它们不受主链的保护。另一方面,如果一个平行链被攻击,它不会影响主链,因此它们可以用来试验新的协议和对主链的改进


4. 平行 链的关键特征


平行 链具有许多关键特征。例如,它们可以多条并行处理交易,效率可提升十倍。此外,由于只需要下载平行 链相关的数据,因此相对效率更高,速度更快。这些平行 链开枝散叶,可以打造自己独有的生态系。


5. 平行链的应用实例


目前市场上已经出现了许多基于平行 链技术开发 的项目。例如 Crust Network 是一个激励去中心化云服务 的应用型公 链。其通过对平行 链技术 的应用 ,既完整地保存了项目 的自主发展空间 ,也可更好地拓展项目功能 ,为用户提供更好地使用体验。


6. 平行链的经济政策


针对目前公 链技术自主权较弱 的问题 ,Polkadot 平 行 链 上 的社 区根据自己 的意志治理他们 的网络 ,不受波卡网络管理 的限制 ,拥有绝对 的自主权 。通过分片协议连接 的区块 链网络 ,可较好地实现拓展定制 。而基于 Polkadot 技术应用开发 的 Crust Network 是一个激励去中心化云服务 的应用型公 链。其通过对平行 链技术 的应用 ,既完整地保存了项目 的自主发展空间 ,也可更好地拓展项目功能 ,为用户提供更好地使用体验。from刘金,转载请

作者:Pomelo_刘金
来源:juejin.cn/post/7244174365172187193
注明原文链接。感谢!

收起阅读 »

IntelOne Mono 英特尔编程字体

web
一、简介 IntelOne Mono 是一个被设计为富有表现力的字体,在设计之初就充分的考虑到 开发者的需求:清晰度/易读性,以解决视觉的疲劳, 减少编码错误等问题,并且 IntelOne Mono 已经得到 低视力 等人群的反馈,证明确实得到一些正面反馈效果...
继续阅读 »

IntelOne Mono.png


一、简介


IntelOne Mono 是一个被设计为富有表现力的字体,在设计之初就充分的考虑到 开发者的需求:清晰度/易读性,以解决视觉的疲劳, 减少编码错误等问题,并且 IntelOne Mono 已经得到 低视力 等人群的反馈,证明确实得到一些正面反馈效果。



开源协议:SIL Open Font License 1.1 商用需要注意协议内容。



二、字体粗细


Untitled.png


IntelOne Mono 按照字体粗细可以分为四种字体:细体/常规字体/中等粗细/粗体 以及斜体效果


三、字体格式支持


font-images.png


官方仓库 中,给出四种格式的样式字体,就安装体验:



  • Windows/MacOS 桌面端使用 ttf/otf,这两种字体都具有跨平台性。

  • Web 端使用 woff/woff2 字体。


四、下载并使用字体


1. Git clone 下载


cd <dir> & git clone https://github.com/intel/intel-one-mono.git

2. Github Release 下载



intel/intel-one-mono 根据需要下载即可。



3、在 VS Code 中配置


位置设置示例
1、配置Settings(设置) -> User(用户) -> Text Editor (文本编辑器) -> Font Family (字体家族) -> IntelOne Monovs-code-setting-font.png
2、集成终端Settings(设置) -> User(用户) -> Features(特性) -> Terminal (终端) -> Intergrated Font Family (字集成字体家族) -> IntelOne Monointel-one-inter.png

4、在 WebStrome 中配置


编辑器配置设置位置示例
1、配置编辑器Settings(设置) -> Editor(编辑器) -> Font -> IntelOne Monowebstrom-use-intel-one-mono-font.png

5、在 Sublime Text 中配置


覆盖 json 数据 font_face 属性值:


编辑器配置设置位置示例
font_face"font_face": "IntelOne Mono"sublime-intel-one-mono-screen-cut.png


其他环境字体,例如:终端中配置也比较简单,就不再复述。



五、字体显示效果


不同大小字体展示效果.png


在渲染字体方面,IntelOne Mono 字体推荐使用大号字体,ttf 文件格式字体,已经对大号字体进行了优化,尤其是在 Windows 平台。



注意:在 VSCode 中,当字体大小发生变化的时候,字体/建议窗口/终端的行高最好一起配置。



六、好编程字体的特点


要素说明
1、清晰易读避免过度装饰,准确还原字体,易读易懂。
2、等宽字体编程对排版的整齐度有较高的要求,排版整齐的代码更加容易阅读和理解。
3、字符与符号层次封面字符中,字母,数字等都具有应该具有不同的展示高度,凸显不同的层级的内容,使得编码更具有层次感。便于快速理解代码字符。
4、特殊字符逗号、句号、括号等等编程中常用的字符,应该突出、便于识别。

七、不同编辑器显示效果


编辑器展示
VS Codeshow-vscode.png
WebStromewebstrom-intelOne-mono-show.png
Sublimeshow-sublime-intel-one-mono-screen-cut.png

八、社区反馈


IntelOne Mono 字体自 4 月 22 发布第一个版本,到今天 Github 社区 Star 数目目前 已经达到 5.5K+ star 数目,足以证明字体的受欢迎程度。


九、与其他字体对比



对比是为了找到更加 适合自己 的字体。



字体名称效果展示
IntelOne Monocompare-intelone-mono.png
JetBrainsMono Nerd FontJetBrainsMono Nerd Font.png
Input Monocompare-Input Mono.png
InconsolataGo Nerd Font Monocompare-InconsolataGo Nerd Font Mono.png
Cascadia Monocompare-cascadiamono.png

十、从 IntelOne Mono 看字体设计


1. UFO


logo.svg

  • 全名:(The Unified Font Object) 统一字体对象

  • UFO 3 (unifiedfontobject.org)
    一种跨平台、跨应用程序、人类可读、面向未来的格式,用于存储字体数据。

  • UFO3 文件的目录结构是这样的。


2. glif


全名:(glyph interchage format) 描述字型文件格式,glif 存储了单个字形的 (TrueType、PostScript)轮廓、标记点、参考线,使用 xml 语法。


3. plist


属性列表格式包含一系列以 XML 编码的键值对。


4. fea


.fea文件名扩展名主要与 Adobe OpenType Feature File ( .fea)文件类型相关联,该文件类型与 OpenType 一起使用,OpenType 是一个开放的标准规范,用于可扩展的排版字体,最初由微软和 Adobe Systems 开发。


5. 软件


RoboFont


robotfont.png


仅限 MacOS 支持,推荐使用。


FontForge


fontforge.png


适用于 Windows/Mac。


6. PostScript 字体


是由 Adobe Systems 为专业数字排版桌面排版开发的轮廓字体计算机字体规范编码的字体文件。


7. TrueType 字体


TrueType 是由美国苹果公司和微软公司共同开发的一种电脑轮廓字体(曲线描边字)类型标准。这种类型字体文件的扩展名是.ttf,类型代码是 tfil。


十一、小结


本文主要介绍 IntelOne Mono 字体在当前主流编辑器中使用方法和展示效果,了解好的编程字体的优秀特带你。并且探索其背后实现的直奔知识点涉及字体的设计方法和工具软件,如果感兴趣,可以自己设计一套字体。IntelOne Mono 字体在 GitHub 上 Star 数量反映了其被人快速关注且喜欢的特点。但也有不足,没有提供 Nerd Font 字体,对于喜欢用终端的小伙伴

作者:进二开物
来源:juejin.cn/post/7244174500785373241
,暂时可能会受欢迎。

收起阅读 »

还有多少公司在使用H5?不怕被破解吗?

web
H5还有人在用吗 近几天,老板让我调查一下现在市面上H5的使用现状。粗略地调查了一下,发现现在使用H5的真不多了,但是还是有人在用H5的,原因无非一是成本低,相比户外广告,H5制作费用根本不值一提;二是效果好,转化率高,好一点的H5大概有10W浏览量,这也反映...
继续阅读 »

H5还有人在用吗


近几天,老板让我调查一下现在市面上H5的使用现状。粗略地调查了一下,发现现在使用H5的真不多了,但是还是有人在用H5的,原因无非一是成本低,相比户外广告,H5制作费用根本不值一提;二是效果好,转化率高,好一点的H5大概有10W浏览量,这也反映H5数据统计精准,企业知道钱花在哪个地方,心里就踏实。


但是,H5的安全性会让企业非常头疼,不知道大家还记不记得几年前某App H5 页面被植入色情内容广告在安全圈引起了轰动。后来排查才基本确定为用户当地运营商http劫持导致H5页面被插入广告,给该App 造成极大影响。


为什么H5是黑灰产高发区?


从顶象多年的防控经验来看,H5面临的风险相对较多是有其原因的。


1、JavaScript代码特性。


H5平台开发语言是JavaScript,所有的业务逻辑代码都是直接在客户端以某种“明文”方式执行。代码的安全防护主要依靠混淆,混淆效果直接决定了客户端的安全程度。不过对于技术能力较强的黑产,仍然可以通过调试还原出核心业务处理流程。




2、企业营销推广需求追求简单快捷。


首先,相比其他平台,很多公司在H5平台的开放业务往往会追求简单,快捷。比如在营销推广场景,很多企业的H5页面只需从微信点击链接直接跳转到一个H5页面,点击页面按钮即可完成活动,获取积分或者小红包。


一方面确实提升了用户体验,有助于拉新推广;但另一方面简便的前端业务逻辑,往往也会对应简单的代码,这也给黑灰产提供了便利,相比去破解App,H5或者小程序的破解难度要低一些。


数据显示,如果企业在营销时不做风险控制,黑产比例一般在20%以上,甚至有一些高达50%。这就意味着品牌主在营销中相当一部分费用被浪费了。


3、H5平台自动化工具众多。


核心流程被逆向后,攻击者则可以实现“脱机”,即不再依赖浏览器来执行前端代码。攻击者可以自行构造参数,使用脚本提交请求,即实现完全自动化,如selenium,autojs,Puppeteer等。这些工具可以在不逆向JS代码的情况下有效实现页面自动化,完成爬虫或者薅羊毛的目的。


4、防护能力相对薄弱。


从客观层面来看,H5平台无论是代码保护强度还是风险识别能力,都要弱于App。这是现阶段的框架导致,并不是技术能力问题。JavaScript数据获取能力受限于浏览器,出于隐私保护,浏览器限制了很多数据获取,这种限制从某种程度上也削弱了JavaScript在业务安全层面的能力。


以电商App为例,出于安全考虑,很多核心业务只在App上支持。如果H5和App完全是一样的参数逻辑和加密防护,对于攻击者,破解了H5也就等于破解了App。


5、用户对H5缺乏系统认识。


最后,大部分用户对H5的安全缺乏系统性的认识,线上业务追求短平快,没有在H5渠道构建完善的防护体系情况下就上线涉及资金的营销业务。


H5代码混淆


基于上面这些问题,我们可以采取H5代码混淆的方式来稍微解一下困境。


一、产品简介



  • H5代码混淆产品,通过多层加密体系,对H5文件进行加密、混淆、压缩,可以有效防止H5源代码被黑灰产复制、破解。


二、混淆原理



  • 对代码中的数字,正则表达式、对象属性访问等统一转为字符串的表示形式

  • 对字符串进行随机加密(多种加密方式,倒序/随机密钥进行轮函数加密等)

  • 对字符串进行拆分,并分散到不同作用域

  • 打乱函数顺序

  • 提取常量为数组引用的方式


举个简单的例子来说明一下流程
(1)变量和函数重命名:


// 混淆前
function calculateSum(a, b) {
var result = a + b;
return result;
}

// 混淆后
function a1xG2b(c, d) {
var e = c + d;
return e;
}


(2)代码拆分和重新组合:


// 混淆前
function foo() {
console.log('Hello');
console.log('World');
}

// 混淆后
function foo() {
console.log('Hello');
}

function bar() {
console.log('World');
}


(3)控制流转换:


// 混淆前
if (condition) {
console.log('Condition is true');
} else {
console.log('Condition is false');
}

// 混淆后
var x = condition ? 'Condition is true' : 'Condition is false';
console.log(x);

(4)添加无用代码:


// 混淆前
function foo() {
console.log('Hello');
}

// 混淆后
function foo() {
console.log('Hello');
var unusedVariable = 10;
for (var i = 0; i < 5; i++) {
unusedVariable += i;
}
}

结语


当然,实际的代码混淆技术可能更加复杂。而且,代码混淆并不能完全阻止源代码的泄露或逆向工程,但可以增加攻击者分析和理解代码的难度。


H5现在的使用场景其实更多可能偏向日常的投票场景、活动场景以及游戏营销等等,其实使用场景很少了,但是一旦被攻击,尤其是对于运营商这种大厂来说,危害性还是很大的,企业或者说公司还是需

作者:昀和
来源:juejin.cn/post/7244004118222078010
要注意这方面的安全。

收起阅读 »

技术人创业是怎么被自己短板KO的

这几天搜索我的聊天记录,无意中看到了一个两年前的聊天消息。那是和我讨论出海业务的独立开发网友,后来又去创业的业界精英。顺手一搜,当初他在我的一个开发者群也是高度活跃的人群之一,还经常私下给我提建议,人能力很强,做出海矩阵方向的项目做的很拿手。但是在后来忽然就没...
继续阅读 »

这几天搜索我的聊天记录,无意中看到了一个两年前的聊天消息。那是和我讨论出海业务的独立开发网友,后来又去创业的业界精英。顺手一搜,当初他在我的一个开发者群也是高度活跃的人群之一,还经常私下给我提建议,人能力很强,做出海矩阵方向的项目做的很拿手。但是在后来忽然就没消息了,虽然人还留在群里。


我好奇点开了他的朋友圈,才知道他已经不做独立开发了,而且也(暂时)不在 IT 圈里玩了,去帮亲戚家的服装批发业务打打下手,说是下手,应该也是二当家级别了,钱不少,也相对安稳。朋友圈的画风以前是IT行业动态,出海资讯现在是销售文案和二维码。


和他私下聊了几句,他跟我说他现在过的也还好,人生路还长着呢,谈起了自己在现在这行做事情的经历,碎碎念说了不少有趣的事情,最后还和我感慨说:“转行后感觉脑子灵活了很多”,我说那你写程序的时候脑子不灵活吗,他发了个尴尬而不失礼貌的表情,“我以前技术搞多了,有时候死脑筋。”


这种话我没少听过,但是从一个认识(虽然是网友)而且大跨度转行的朋友这里说出来,就显得特别有说服力。尤其了解了他的经历后,想写篇文章唠叨下关于程序员短板的问题,还有这种短板不去补强,会怎么一步步让路越走越窄的。


现在离职(或者被离职)的程序员越来越多了,程序员群体,尤其是客户端程序员这个群体,只要能力过得去,都有全栈化和业务全面化的潜力。尤其是客户端程序员,就算是在公司上班时,业余时间写写个人项目,发到网上,每个月赚个四到五位数的副业收入也是可以的。


再加上在公司里遇到的各种各样的窝囊事,受了无数次“煞笔领导”的窝囊气,这会让一些程序员产生一种想法,我要不是业余时间不够,不然全职做个项目不就起飞了?


知道缺陷在哪儿,才能扬长避短,所以我想复盘一下,程序员创业,在主观问题上存在哪些短板。(因为说的是总体情况,也请别对号入座)


第一,认死理。


和代码,协议,文档打交道多了,不管自己情愿不情愿,人多多少少就有很强的“契约概念”,代码的世界条理清晰,因果分明,1就是1,0就是0,在这样的世界里呆多了,你要说思维方式不被改变,那是不可能的 --- 而且总的来说,这种塑造其实是好事情。要不然也不会有那么多家长想孩子从小学编程了。(当然了,家长只是想孩子学编程,不是做程序员。)


常年埋头程序的结果,很容易让技术人对于社会上很多问题的复杂性本质认识不到位,恐惧,轻视,或者视而不见,总之,喜欢用自己常年打磨的逻辑能力做一个推理,然后下一个简单的结论。用毛爷爷的话说,是犯了形而上的毛病。


例如,在处理iOS产品上架合规性一类问题时,这种毛病暴露的就特别明显。


比如说相信一个功能别的产品也是这么做的,也能通过审核,那自己照着做也能通过。但是他忽略了这种判断背后的条件是,你的账号和别的账号在苹果眼里分量也许不同的,而苹果是不会把这件事写在文档上的。


如果只是说一说不要紧,最怕的是“倔”,要不怎么说是“认死理”呢。


第二,喜欢拿技术套市场。


​这个怎么理解呢,就是有追求的技术人喜欢研究一些很强的技术,但是研究出来后怎么用,也就是落实到具体的应用场景,就很缺点想象力了。


举个身边有意思的例子,有个技术朋友花了三年时间业余时间断断续续的写,用 OpenGL 写了一套动画效果很棒的 UI 引擎,可以套一个 View 进去后定制各种酷炫的动画效果。做出来后也不知道用来干嘛好,后来认识了一个创业老板,老板一看你这个效果真不错啊,你这引擎多少钱我买了,朋友也没什么概念,说那要不五万卖你。老板直接钱就打过去了。后来老板拿给手下的程序员维护,用这套东西做了好几个“小而美”定位的效率工具,简单配置下就有酷炫的按钮动画效果,配合高级的视觉设计逼格拉满,收入怎么样我没问,但是苹果在好几个国家都上过推荐。


可能有人要说,那这个程序员哥哥没有UI帮忙啊,对,是这个理,但是最根本的问题是,做小而美工具这条路线,他想都没想到,连意识都意识不到的赚钱机会,怎么可能把握呢?有没有UI帮忙那是实现层的门槛而已。


第三,不擅长合作。


为什么很多创业赚到小钱(马化腾,李彦宏这些赚大钱就不说了,对我们大部分人没有参考价值)而且稳定活下来的都是跑商务,做营销出身的老板。


他们会搞钱。


他们会搞钱,是​因为他们会搞定人,投资人,合伙人,还有各种七七八八的资源渠道。


大部分人,在创业路上直接卡死在这条路线上了。


投资人需要跑,合作渠道需要拉,包括当地的税务减免优惠,创业公司激励奖金,都需要和各种人打交道才能拿下来。


那我出海总行了吧,出海就不用那么麻烦了吧。不好意思,出海的合作优势也是领先的,找海外的自媒体渠道合作,给产品提曝光。坚持给苹果写推荐信,让自家产品多上推荐。你要擅长做这些,就不说比同行强一大截,起码做出好产品后创业活下来的希望要高出不少,还有很多信息差方法论,需要进圈子才知道。



--- 


我说的这些,不是贬损也不是中伤,说白了,任何职业都有自己的短板,也就是我们说的职业病,本来也不是什么大不了的事情。只是我们在大公司拧螺丝的时候,被保护的太好了。


只是创业会让一个人的短处不断放大,那是因为你必须为自己的选择负责了,没人帮你擦屁股了背锅了。所以短板才显得那么刺眼。


最后说一下,不是说有短板就会失败,谁没点短处呢。写出来只是让自己和朋友有更好的自我认知,明白自己的长处在哪,短处在哪。


最后补一个,左耳朵耗子的事情告诉我们,程序员真的要保养身子,拼到最后其实还是拼身

作者:风海铜锣
来源:juejin.cn/post/7238443713873199159
体,活下来才有输出。

收起阅读 »

何谓实事求是地工作?

提到实事求是,大家第一时间会想到什么?我想大部分是客观,事实,脚踏实地?这么一想,大家都会觉得,自己挺实事求是的呀,没毛病。但是,我会经常在工作中感受到不是那么实事求是的行为,比如张嘴就来,不带思考,做事全靠猜的行为,真太多了。 随着我这两年的学习和总结,我越...
继续阅读 »

提到实事求是,大家第一时间会想到什么?我想大部分是客观,事实,脚踏实地?这么一想,大家都会觉得,自己挺实事求是的呀,没毛病。但是,我会经常在工作中感受到不是那么实事求是的行为,比如张嘴就来,不带思考,做事全靠猜的行为,真太多了。


随着我这两年的学习和总结,我越发觉得实事求是非常重要,并把它视为我做事情和成长的基石。对于实事求是,我主要有以下 3 层理解。


首先,尊重客观事实,探寻真理。我们要承认事实,即使这个事实有多么的难以置信,但存在即是合理,我们首先要尊重它,承认它。然后我们还要积极主动地面对它,探寻事实背后的真理,获得真知,这样才能真正的成长,并有可能寻得机会。当某个事情的进展超出自己预期的时候,我们正确的态度应该是思考为什么会这样,而不是去想对错得失。


其次,数据说话,数据驱动。事实如何去量化?答案是数据。使用数据去表达事实,是我们程序员应该有的技能。工作的本质就是解决问题,之前的文章有讲解,问题就是理想状态和现实状态之间的差别,因此,我们在工作当中做的每一项决策的依据、制定的每一个目标,都应该用数据说话。我们应该使用数据表达现状,并使用数据衡量目标,驱动自己去工作。一些沟通的细节就能够体现出他是不是在实事求是地工作,比如“这个页面加载太慢了,需要优化”。那这个页面加载到底有多慢?业界标准或者竞品的加载耗时是多少?优化的目标值是多少?


最后,从客观事实中获取反馈,不断迭代。工作中想要获得成功和成长,最核心的一个环节是反馈。很多人没有意识到这点。更多的人没有意识到的是,获取反馈其实很简单,随处都是。敏捷开发、精益创业、增长黑客,这些理论的底层核心都是基于事实和数据的反馈,不断迭代改进自己的产品,从而获得成功。对于个人成长来说也是一样的,我们要从客观事实中获取反馈,思考总结,不断迭代自己的能力。


总结一下,实事求是地工作有 3 个层次,首先,要正视事实,并主动探究真理;然后我们慢慢地开始用数据驱动自己的工作;最后让数据驱动变成循环,不断迭代,并把这种循环融入到各个方面,包括工作和个人成长,让它成为自己下意识的动作。


我在努力学习和践行实事求是地工作,我也希望我的团队可以用实事求是的态度来工作,以此文共勉!



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

收起阅读 »

我裸辞了,但是没走成!

人在国企,身不由己!公司福利和薪资还可以,但有个难顶的组长就不可以,说走就走!如果把这个组长换了的话就另说了! 1.为什么突然想不干了? 1.奇葩的新组长 我的前组长辞职了,然后被安排到这个自研项目组,而这个新组长我之前得罪过,老天爷真爱开玩笑! 今年过年前,...
继续阅读 »

人在国企,身不由己!公司福利和薪资还可以,但有个难顶的组长就不可以,说走就走!如果把这个组长换了的话就另说了!


1.为什么突然想不干了?


1.奇葩的新组长


我的前组长辞职了,然后被安排到这个自研项目组,而这个新组长我之前得罪过,老天爷真爱开玩笑!


今年过年前,我主开发的平台要嵌入到他负责的项目里面,一切对接都很顺利,然而某天,有bug,我修复了,在群里面发消息让他合并分支更新一下。他可能没看到,然后我下班后一个小时半,我还在公司,在群里问有没有问题,没回应!


然后我就坐车回家,半路,产品经理组长、大组长和前组长一个个轮流call我,让我处理一下bug,我就很无语!然后我就荣获在家远程办公,发现根本没问题!然后发现是对方没更新的问题!后面我修复完直接私聊他merge分支更新,以免又这样大晚上烦人!
而类似的事情接连发生,第三次之后,我忍不住了,直接微信怼了他,他还委屈自己晚上辛苦加班,我就无语大晚有几个人用,晚上更新与第二天早上更新有什么区别?然后就这样彻底闹掰了!


我就觉得这人很奇葩,有什么问题不能直接跟我沟通,一定要找我的上级一个个间接联系我呢?而且,这更新流程就很有问题,我之前在别的组支援修bug,是大早上发布更新,一整天测试,保证不是晚上的时候出现要紧急处理的问题!


然后,我跟这人有矛盾后,我就没继续对接这个项目了,前组长安排了别人代替我!


结果兜兜转转,竟然调到他这里来!作孽啊!


2.项目组乱糟糟


新项目组可以看出新组长管理水平很糟糕!


新组长给自己的定位是什么都管!产品、前后端、测试、业务等,什么都往自己身上揽!他自己觉得很努力,但他不是那部分的专业人员,并不擅长,偏偏还没那个金刚钻揽一堆瓷器活!老爱提建议!


项目组就两个产品,其中一个是UI设计刚转,还没成长为专业的产品经理,而那个主要负责的产品经理根本忙不过来!


然后,他一个人搞不定,就开始了PUA大法,周会的时候就会说:“希望大家要把这个项目当成自己的事业来奋斗,一起想,更好地做这个产品!”


“这个项目集成了那么多的模块功能,如果大家能够做好,对自己有很大的历练和成长!”


“我们项目是团队的重点项目,好多领导都看好,开发不要仅限于开发,要锻炼产品思维……”


……


简而言之就是,除了本职工作也要搞点产品的工作!


然后建模师开始写市场调研文档,前后端开发除了要敲代码,还得疯狂想新功能。


整个组开始陷入搞新东西的奇怪旋涡!


某次需求评审的时候,因为涉及到大量的文件存储,我提出建议,使用minio,fastdfs,这样就不用每次部署的时候,整体文件还要迁移,结果对方一口拒绝,坚决使用本地存储,说什么不要用XX平台的思想来污染他这个项目,他这个项目就要不需要任何中间件都能部署。


就很无语!那个部署包又大又冗余,微服务都不用,必须要整包部署整套系统,只想要某几个功能模块都不行,还坚持说这样可以快速整包部署比较好!


一直搞新功能的问题就是版本更新频繁!一堆新功能都没测清楚就发布,导致产品质量出现严重问题,用户体验极差!终于用户积攒怨气爆发了,在使用群里面@了我们领导,产品质量问题终于被彻底揭开了遮羞布!


领导开始重视这个产品质量的问题,要求立即整改!


然后这个新组长开始新一轮搞事!


“大家保证新功能进度的同时,顺便测试旧功能,尽量不要出bug!”


意思就是你开发进度也要赶,测试也要搞!


就不能来点人间发言吗?


3.工作压力剧增


前组长是前端的,他带我入门3D可视化的,相处还算融洽!然而他辞职了,去当自由职业者了!


新组长是后端的,后端组长问题就是习惯以后端思维评估前端工作,给任务时间很紧。时间紧就算了,还要求多!


因为我之前主开发的项目是可视化平台,对方不太懂,但不妨碍他喜欢异想天开,加个这个,加个那个,他说一句话,就让你自行想象,研究竞品和评估开发时间!没人没资源,空手套白狼,我当时就很想爆他脑袋了!


我花一个星期集成了可视化平台的SDK,连接入文档都写好了,然后他验收的时候提出一堆动态配置的要求,那么大的可视化平台,他根本没考虑项目功能模块关联性和同步异步操作的问题,他只会提出问题,让你想解决方案!


然后上个月让我弄个web版的Excel表格,我看了好多开源项目,也尝试二开,增加几个功能,但效率真的好低!于是我就决定自己开发一个!


我开发了两个星期,他就问我搞定没?我说基本功能可以,复杂功能还在做!


更搞笑的是,我都开发两个星期了,对方突然中午吃饭的时候微信我,怕进度赶不上,建议我还是用开源的进行二开,因为开源有别人帮忙一起搞。


我就很无语,人家搞的功能又不是一定符合你的需求,开源不等于别人给你干活,大家都是各干各的,自己还得花精力查看别人代码,等价于没事找事,给自己增加工作量!别人开发的有隐藏问题,出现bug排查也很难搞,而自己开发的,代码熟悉,即便有问题也能及时处理!


我就说他要是觉得进度赶不上就派个人来帮忙,结果他说要我写方案文档,得到领导许可才能给人。


又要开发赶进度,又要写文档,哪有那么多时间!最终结果就是没资源,没人手,进度依旧要赶!


因为我主开发的那个可视化平台在公司里有点小名气,好多平台想要嵌入,然后,有别的平台找到他要加上这个可视化平台,但问题是我很忙,又要维护又要开发,哪搞得了那么多?还说这个很赶!赶你个头!明知道时间没有,就别答应啊!工作排期啊!


新组长不帮组员解决问题,反而把问题抛给组员,压榨组员就很让人反感!


2.思考逃离


基于以上种种!我觉得这里不是一个长久之地,于是想要逃离这里!


我联系了认识的其他团队的人,别人表示只要领导愿意放人,他们愿意接收我,然后我去咨询一些转团队的流程,那些转团队成功的同事告诉我,转团队最难的是领导放人这关,而且因为今年公司限制招聘,人手短缺,之前有人提出申请,被拒绝了!并且转团队的交接的一两个月内难免要承受一些脸色!有点麻烦!


我思虑再三,我放弃了转团队这条路,因为前组长走了之后,整个团队只剩下我一个搞3D开发的,估计走不掉!


3.提出辞职


忍了两个月,还是没忍住,工作最重要的是开开心心!赚钱是一回事,要是憋出个心理疾病就是大事了!于是我为了自己的身心健康,决定走人!拜拜了喂!老娘不奉陪了!


周一一大早,我就提交了辞职信,大组长表示很震惊,然后下午的时候,领导和大组长一起来跟我谈话,聊聊我为什么离职?问我有没有意愿当个组长之类的,我拒绝了,我只想好好搞技术!当然我不会那么笨去说别人的坏话得罪人!


我拿前组长当挡箭牌,说自己特别不习惯这个新组长的管理方式!前组长帮我扛着沟通之类的问题,我只要专心搞开发就好了!


最终,我意志坚定地挡住了领导和大组长的劝留谈话,并且开始刷面试题,投简历准备寻找新东家!


裸辞后真的很爽,很多同事得知消息都来关心我什么情况,我心里挺感动的!有人说我太冲动了,可以找好下家再走!但我其实想得很清楚,我没可能要求一个组长委屈自己来适应我,他有他的管理方式,我有我的骄傲!我不喜欢麻烦的事,更不喜欢委屈自己,一个月后走人是最快解决的方案!


4. 转机


其实我的离开带来了一点影响,然后加上新组长那个产品质量问题警醒了领导,然后新组长被调去负责别的项目了,换个人来负责现在的项目组,而这个人就是我之前支援过的项目组组长,挺熟悉的!


新新组长管理项目很有条理也很靠谱,之前支援的项目已经处于稳定运行的状态了,于是让他来接手这个项目!他特意找我谈话,劝我留下来,并且承诺以后我专心搞技术,他负责拖住领导催进度等问题!


我本来主要就是因为新组长的问题才走人的,现在换了个不错的组长!可以啊!还能苟苟!


5.反思



  1. 其实整件事情中,我也有错,因为跟对方闹掰了,就拒绝沟通,所以导致很多问题的发生,如果我主动沟通去说明开发难度的问题,并且争取时间,就不至于让自己处于一个精神内耗的不快乐状态。

  2. 发现问题后,我没有尝试去跟大组长反馈,让大组长去治治对方,或者让大组长帮忙处理这个矛盾,我真的太蠢了!

  3. 我性格其实挺暴躁的,看不顺眼就直接怼,讨厌的人就懒得搭理,这样的为人处世挺不讨喜的,得改改这坏
    作者:敲敲敲敲暴你脑袋
    来源:juejin.cn/post/7241884241616076858
    脾气!

收起阅读 »

AnyScript:前端开发的最佳良药!

web
不以繁琐为名,更以简洁为声! 作为一名Api调用工程师,深知在前端开发中的各种挑战和痛点。在我们开发过程中,代码的可维护性和可扩展性是至关重要的因素。TypeScript(简称TS)作为JavaScript的超集,为我们带来了更好的开发体验和更高的代码质量。 ...
继续阅读 »

cover.png


不以繁琐为名,更以简洁为声!


作为一名Api调用工程师,深知在前端开发中的各种挑战和痛点。在我们开发过程中,代码的可维护性和可扩展性是至关重要的因素。TypeScript(简称TS)作为JavaScript的超集,为我们带来了更好的开发体验和更高的代码质量。


1. 类型系统:保驾护航


1.1 强大的类型检查


TypeScript引入了静态类型检查,这是它最吸引人的特点之一。通过在代码中定义变量的类型,TypeScript可以在编译时发现潜在的错误,大大减少了在运行时遇到的意外错误。例如,在JavaScript中,我们可以将一个字符串类型的变量传递给一个预期接收数字类型的函数,这将导致运行时错误。而在TypeScript中,编译器将会提示我们这个潜在的类型不匹配错误,使得我们可以在开发过程中及早发现并修复问题。


举个例子,假设我们有以下的TypeScript代码:


function add(x: number, y: number): number {
return x + y;
}

const result = add(3, '5');
console.log(result);

在这个例子中,我们本应该传递两个数字给add函数,但是错误地传递了一个字符串。当我们尝试编译这段代码时,TypeScript编译器会提示错误信息:


Argument of type 'string' is not assignable to parameter of type 'number'.

通过这种类型检查,我们可以在开发过程中发现并解决类型相关的问题,避免了一些常见的错误。


1.2 类型推断的魅力


在TypeScript中,我们不仅可以显式地定义变量的类型,还可以利用类型推断的功能。当我们没有明确指定类型时,TypeScript会根据上下文和赋值语句自动推断变量的类型。这个特性不仅减少了我们编写代码时的工作量,还提供了代码的简洁性。


举个例子,考虑以下的代码:


const name = 'John';

在这个例子中,我们没有显式地指定name的类型,但TypeScript会自动推断它为字符串类型。这种类型推断让我们在编写代码时更加灵活,减少了类型注解的需求。


2. 更好的代码编辑体验


2.1 智能的代码补全和提示


使用TypeScript可以带来更好的代码编辑体验。由于TypeScript具备了静态类型信息,编辑器可以提供智能的代码补全和提示功能,减少我们编写代码时的出错几率。当我们输入一个变量名或者函数名时,编辑器会根据类型信息推断可能的属性和方法,并展示给我们。


例如,当我们有一个对象,并想获取它的属性时,编辑器会给出属性列表供我们选择。这在大型项目中尤其有用,因为我们可以快速了解某个对象的可用属性,而不必查阅文档或者浏览源代码。


2.2 重构的艺术


在大型项目中进行代码重构是一项棘手的任务。TypeScript提供了强大的重构能力,使得我们能够更轻松地重构代码而不担心破坏现有功能。在进行重构操作时,TypeScript会自动更新相关的类型注解,帮助我们在整个重构过程中保持代码的一致性。


举个例子,假设我们有一个函数:


function multiply(x: number, y: number): number {
return x * y;
}

现在我们决定将multiply函数的参数顺序调换一下。在传统的JavaScript中,我们需要手动修改所有调用multiply函数的地方。而在TypeScript中,我们只需要修改函数本身的定义,TypeScript会自动检测到这个变化,并指示我们需要更新的地方。


3. 生态系统的繁荣


3.1 类型定义文件


TypeScript支持类型定义文件(.d.ts),这些文件用于描述 JavaScript 库的类型信息。通过使用类型定义文件,我们可以在TypeScript项目中获得第三方库的类型检查和智能提示功能。这为我们在开发过程中提供了极大的便利,使得我们能够更好地利用现有的JavaScript生态系统。


例如,假设我们使用著名的React库进行开发。React有一个官方提供的类型定义文件,我们只需要将其安装到项目中,就能够获得对React的类型支持。这使得我们可以在编写React组件时,获得相关属性和方法的智能提示,大大提高了开发效率。


3.2 社区的支持


TypeScript拥有庞大而活跃的社区,开发者们不断地分享自己的经验和资源。这意味着我们可以轻松地找到许多优秀的库、工具和教程,帮助我们更好地开发和维护我们的前端项目。无论是遇到问题还是寻找最佳实践,社区都会给予我们及时的支持和建议。


4. 面向未来的技术栈


4.1 ECMAScript的最新特性支持


ECMAScript是JavaScript的标准化版本,不断更新以提供更多的语言特性和功能。TypeScript紧密跟随ECMAScript标准的发展,支持最新的语法和特性。这意味着我们可以在TypeScript项目中使用最新的JavaScript语言功能,而不必等待浏览器的支持。


例如,当ECMAScript引入了Promiseasync/await等异步编程的特性时,TypeScript已经提供了对它们的完整支持。我们可以在TypeScript项目中使用这些特性,而无需担心兼容性问题。


4.2 渐进式采用


对于已有的JavaScript项目,我们可以渐进式地引入TypeScript,而无需一次性对整个项目进行重写。TypeScript与JavaScript是高度兼容的,我们可以逐步将JavaScript文件改写为TypeScript文件,并为每个文件逐渐添加类型注解。这种渐进式的采用方式可以降低迁移的风险和成本,并让我们享受到TypeScript带来的好处。


结语


推荐使用TypeScript来提升我们的开发体验和代码质量。它强大的类型系统、智能的代码编辑体验、丰富的生态系统以及面向未来的技术栈,都使得TypeScript成为当今前端开发的首选语言之一。


但是,我们也需要明确TypeScript并非万能的解决方案。在某些特定场景下,纯粹的JavaScript可能更加合适。我们需要根据具体项目的需求和团队的情况,权衡利弊并做出适当的选择。



示例代码仅用于说明概念,可能不符合最佳实践。在实际开发中,请根据具体情况进行调整。


作者:ShihHsing
来源:juejin.cn/post/7243413799347798072

收起阅读 »

如果让你设计一个弹幕组件,你会怎么做???

web
大家好,我是前端小张同学,这次给大家更新一个开发中常见的需求,接下来我会将我的弹幕实现以及设计思路一步一步描述出来,并分享给大家,希望大家喜欢。 今天我们的主题是 ,用vue手写一个弹幕 1:关于弹幕设计思想 1.1 : 业务层 | 视图层(全局组件) 1.1...
继续阅读 »

大家好,我是前端小张同学,这次给大家更新一个开发中常见的需求,接下来我会将我的弹幕实现以及设计思路一步一步描述出来,并分享给大家,希望大家喜欢。


今天我们的主题是 ,用vue手写一个弹幕


1:关于弹幕设计思想


1.1 : 业务层 | 视图层(全局组件)


1.1.1 : 从业务角度来说,如果你设计的是全局弹幕组件,你要考虑以下几点。



  1. 容器的高度?

  2. 容器层次结构划分?

  3. 渲染弹幕的方式,使用组件的人应该传递什么数据?

  4. 是否支持全屏弹幕?

  5. 是否支持弹幕关闭和开启?

  6. 是否需要重置弹幕?

  7. 是否支持暂停弹幕?

  8. 是否需要集成发送功能?


设计方案考虑完整了以后,你将可以开始考虑 数据层的设计


1.2 数据层


1.2.1 : 从数据角度来说每一条弹幕无非是一个element,然后把弹幕内容放到这个element元素中,并且给 element 添加动画,那接下来,你应该这样考虑。




  1. 弹幕是JS对象?它的属性有哪些?




  2. 谁去管理这些弹幕?如何让他能够支持暂停和关闭?




  3. 你如何把后台的数据,与你前台的一些静态数据进行合并,创造出一个完整对象?




  4. 你怎么去渲染这些弹幕?




  5. 你想要几秒创建一次弹幕并在容器内显示和运行?




  6. 弹幕具备哪些灵活的属性?

    运行动画时间 , 用户自己发布的弹幕样式定制?
    又或者,弹幕需要几条弹道内运行等等这些你都需要考虑。




数据设计方案考虑完整了以后,你将可以开始考虑 数据管理层的设计


1.3 数据管理层


1.3.1 从管理的角度来说,外界调用某些方法,你即可快速的响应操作,例如外界调用 open 方法,你就播放弹幕,调用Stop方法,你就关闭弹幕 接下来,你应该考虑以下几点。



  1. 面向对象设计,应该提供哪些方法,具备哪些功能?

  2. 调用了指定的方法,应该怎么对数据进行操作。

  3. 如何对弹幕做性能优化?


到这里 , 我们设计方案基本完成,接下来我们可以开始编写代码。


2: 代码实现


2.1 : 数据层设计方案实现


我们需要构建一个 Barrage 类 ,我们每次去创建一个弹幕的时候都会 new Barrage,让他帮助我们生成一些弹幕属性。


export class Barrage {
constructor(obj) {
// 每次 new Barrage() 传入一个 后台返回的数据对象 obj
const { barrageId, speed, level, top, jumpUrl, barrageContent, animationPlayState, ...args } = obj
this.barrageId = barrageId; // id : 每条弹幕的唯一id
this.speed = speed; // speed : 弹幕运行的速度,由外界控制
this.level = level; // level : 弹幕的层级 --> 弹幕可分为设计可分为 上下 1 , 1 两个层级 ,可决定弹幕的显示是全屏还是半屏显示
this.top = top; // top :弹幕生成的位置相对于 level 的层级 决定 ,相对于 Level 层级 盒子距离顶部的位置
this.jumpUrl = jumpUrl; // jumpUrl :点击弹幕需要跳转的链接
this.barrageContent = barrageContent; // barrageContent : 弹幕的内容
this.animationPlayState = ''; // 设计弹幕 是否可 点击暂停功能
this.color = '#FFF' // 弹幕颜色
this.args = args // 除去Barrage类之外的一些数据属性全部丢到这里,例如后台返回的数据
}
}

2.1 : 数据管理层设计方案实现


2.1.1 :我们在这里实现了 , 弹幕的 增加删除初始化重置关闭开启功能


1. 实现弹幕开启功能.


BarrageManager.js


export class BarrageManager {

constructor(barrageVue) {
this.barrages = []; // 填弹幕的数组
this.barragesIds = [] // 批量删除弹幕的数组id
this.sourceBarrages = [] // 源弹幕数据
this.timer = null //控制弹幕的开启和关
this.barrageVue = barrageVue // 弹幕组件实例
this.deleteCount = 0, // 销毁弹幕的总数
this.lastDeleteCount = 0, // 最后可销毁的数量
this.row = 0,
this.count = 0
}
init(barrages) {
this.sourceBarrages = barrages
this.deleteCount = parseInt(this.sourceBarrages.length / deleteQuantity.FIFTY) // 计算可删除数量
this.lastDeleteCount = this.sourceBarrages.length % deleteQuantity.FIFTY // 计算 最后一次可删除数量
}
/**
*
* @param {*} barrages 接收一个弹幕数组数据
* @description 循环创建 弹幕对象 ,将后台数据与 创建弹幕的属性结合 存入弹幕数组
*/

loopCreateBarrage(barrages) {
const { rows, createTime, crearteBarrageObject } = this.barrageVue
let maxRows = rows / 2 // 最大的弹幕行数
this.timer = setInterval(() => {
for (let i = 0; i < 1; i++) {
let barrageItem = barrages[this.count]
if (this.row >= maxRows) { this.row = 0 } // 如果当前已经到了 最大的弹幕行数临界点则 回到第0 行弹道继续 创建
if (!barrageItem) return clearInterval(this.timer) // 如果取不到了则证明没数据了 , 结束弹幕展示
const item = crearteBarrageObject({ row: this.row, ...barrageItem }) // 添加对象到 弹幕数组中
this.addBarrage(item)
this.count++ // 用于取值 ,取了多少条
this.row++ // 用于弹道
}
}, createTime * 1000);
}
/**
* @param {*} barrages 传入一个弹幕数组数据
* @returns 无返回值
* @description 调用 该方法 开始播放弹幕
*/

open(barrages) {
if (barrages.length === 0) return
this.init(barrages)
this.loopCreateBarrage(this.sourceBarrages)
}
}

在这里我们初始化了一个 open 方法,并接收一个数组 ,并调用了 init 方法 去做初始化操作,并调用了 循环创建的方法,没 createTime 秒创建一条弹幕,加入到弹幕数组中。



  1. 连接视图层


2.1 : 视图层 | 业务层设计方案实现


index.vue


<template>
<div class="barrage">
<div class="barrage-container" ref="barrageContainer">
<div class="barrage-half-screen" ref="halfScreenContainer">
<template v-for="item in barrageFiltering.level1">
<barrage-item
:item="item" :class="{pausedAnimation : paused }"
:options='barrageTypeCallback(item)'
@destory="destoryBraageItem" :key="item.barrageId">

</barrage-item>
</template>
</div>
<div class="barrage-full-screen" v-if="fullScreen">
<template v-for="item in barrageFiltering.level2">
<barrage-item
:item="item" :class="{pausedAnimation : paused }"
:options='barrageTypeCallback(item)'
@destory="destoryBraageItem" :key="item.barrageId">

</barrage-item>
</template>
</div>
</div>
<user-input ref="publishBarrage" v-if="openPublishBarrage" @onBlur="handleBlur">
<template #user-operatio-right>
<!-- 处理兼容性问题 ios 和 安卓 触发点击事件 -->
<div class="send" @click="sendBarrage($event)" v-if="IOS">
<slot name="rightRegion"></slot>
</div>
<div class="send" @mousedown="sendBarrage($event)" v-else>
<slot name="rightRegion"></slot>
</div>
</template>
</user-input>
</div>

</template>
export default {
created () {
this.barrageManager = new BarrageManager(this)
},
mounted() {
// 初始化弹幕渲染数据
this.initBarrageRenderData();
},
data() {
return {
barrageManager : null,
isClickSend: false,
paused : false
};
},
methods : {
initBarrageRenderData() {
this.barrageManager.open(this.barrages);
},
},
computed : {
barrageFiltering() {
return {
level1:
this.barrageManager.barrages.filter(
item => item.level === barrageLevel.LEVEL1
) || [],
level2:
this.barrageManager.barrages.filter(
item => item.level === barrageLevel.LEVEL2
) || []
};
},
}
}

视图层知识点回顾


在这里我们在弹幕组件创建的时候去创建了一个 弹幕管理对象,并且在挂载的时候去初始化了以下 弹幕渲染的数据,于是我们调用了 弹幕管理类open方法,这样当组件挂载时,就会去渲染 barrageFiltering 数据,这里我们是在管理类中拿到了管理类中循环创建的数据。


open 方法实现


到这里我们的弹幕的开启基本上已经完成了,可以看得出,如果你是这样设计的,你只需要在组件中调用管理类的一些方法,它就能帮你完成一些功能。


3: 实现弹幕关闭功能


barrageManager.js


 class BarrageManager {
constructor(barrageVue) {
this.barrages = []; // 填弹幕的数组
this.barragesIds = [] // 批量删除弹幕的数组id
this.sourceBarrages = [] // 源弹幕数据
this.timer = null //控制弹幕的开启和关
this.barrageVue = barrageVue // 弹幕组件实例
this.deleteCount = 0, // 销毁弹幕的总数
this.lastDeleteCount = 0, // 最后可销毁的数量
this.row = 0,
this.count = 0
}

/**
* @return 无返回值
* @description 调用close 方法 关闭弹幕
*/

close() {
clearInterval(this.timer)
this.removeAllBarrage()
}
/**
* @description 删除全部的弹幕数据
*/

removeAllBarrage() {
this.barrages = []
}
}


关闭功能知识点回顾


在这里我们可以看到,关闭弹幕的功能其实很简单,你只需要把开启弹幕时的定时器关闭,并且把弹幕数组数据清空就可以了


4: 实现弹幕添加功能


index.vue



addBarrage(barrageContent) {
// 获取当前 定时器正在创建的 一行
let currentRow = this.barrageManager.getRow();
let row = currentRow === this.rows / 2 ? 0 : currentRow + 1;
if (row === this.rows / 2) {
row = 0;
}
let myBarrage = {
row,
barrageId: '1686292223004',
barrageContent,
style: this.style,
type: "mySelf", // 用户自己发布的弹幕类型
barrageCategory: this.userBarrageType
};

const item = this.crearteBarrageObject(myBarrage);

this.barrageManager.addBarrage(item); // 数据准备好了 调用添加方法

console.info("发送成功")

this.barrageManager.setRow(row + 1);
},

barrageManager.js


 class BarrageManager {
constructor(barrageVue) {
this.barrages = []; // 填弹幕的数组
this.barragesIds = [] // 批量删除弹幕的数组id
this.sourceBarrages = [] // 源弹幕数据
this.timer = null //控制弹幕的开启和关
this.barrageVue = barrageVue // 弹幕组件实例
this.deleteCount = 0, // 销毁弹幕的总数
this.lastDeleteCount = 0, // 最后可销毁的数量
this.row = 0,
this.count = 0
}
/**
*
* @param {*} obj 合并完整的的弹幕对象
* @param {...any} args 开发者以后可能需要传递的剩余参数
*/

addBarrage(obj, ...args) {
const barrage = new Barrage(obj, ...args)
this.barrages.push(barrage)
}
}

添加功能知识点回顾


在这里我们可以看到,添加的时候,我们 组件 只需要去调用 addBarrage 方法进行弹幕添加,并且在调用的过程中我们去 new Barrage 这个类 , 也就是我们之前准备好的 弹幕数据类 | 数据层设计


5: 实现弹幕删除功能


class BarrageManager {
constructor(barrageVue) {
this.barrages = []; // 填弹幕的数组
this.barragesIds = [] // 批量删除弹幕的数组id
this.sourceBarrages = [] // 源弹幕数据
this.timer = null //控制弹幕的开启和关
this.barrageVue = barrageVue // 弹幕组件实例
this.deleteCount = 0, // 销毁弹幕的总数
this.lastDeleteCount = 0, // 最后可销毁的数量
this.row = 0,
this.count = 0
}

/**
*
* @param {*} barrageId // 入参 弹幕id
* @returns 无返回值
* @description 添加需要批量删除的 id 到 批量删除的栈中 barragesIds
*/

addBatchRemoveId(barrageId) {
this.barragesIds.push(barrageId)
this.batchRemoveHandle()
}
/**
*
* @param {*} start 你需要从第几位开始删除
* @param {*} deleteCount // 删除的总数是多少个
* @returns 无返回值
*/

batchRemoveBarrage(start, deleteCount) {
if (this.barrages.length === 0) return
this.barrages.splice(start, deleteCount)
}
batchRemoveId(start, deleteCount) {
if (this.barragesIds.length === 0) return
this.barragesIds.splice(start, deleteCount)
}
/**
* @param {*} barrageId 弹幕 id 针对单个删除弹幕时 使用
*/

removeBarrage(barrageId) {
let index = this.barrages.findIndex(item => item.barrageId === barrageId)
this.barrages.splice(index, 1)
}
/**
* @description 删除全部的弹幕数据
*/

removeAllBarrage() {
this.barrages = []
}
// 批量移除逻辑处理
batchRemoveHandle() {
if (this.deleteCount === 0 || this.deleteCount === 0) {
if (this.barragesIds.length === this.lastDeleteCount) {
this.batchRemoveBarrage(0, this.lastDeleteCount)
this.batchRemoveId(0, this.lastDeleteCount)
}
} else {
if (this.barragesIds.length === deleteQuantity.FIFTY) {
this.batchRemoveBarrage(0, deleteQuantity.FIFTY)
this.batchRemoveId(0, deleteQuantity.FIFTY)
this.deleteCount--
}
}
}
}

删除功能知识点回顾


在这里我们可以看到,删除的时候我们把每一个弹幕id加入到了一个数组中 , 当 弹幕id数组长度达到我想要删除的数量的时候, 调用 splice 方法 执行批量删除操作,当数据发生更新,视图也会更新,这样我们只需要执行一次dom操作,不需要每一次删除弹幕更新dom,造成不必要的性能消耗。


5: 实现弹幕重置功能


到这里,我相信你已经明白了我的设计,如果现在让你实现一个 重置弹幕方法 你会怎么做 ? 是不是只需要,调用一下 close 方法 , 然后再去 调用 open方法就可以了,ok 接下来我会将完整版代码 放入我的github仓库,小伙伴们可以去拉取 仓库链接,具体代码还需要小伙伴们自己从头阅读一次,这里只是说明了部分内容 , 阅读完成后 , 你就会彻底理解。


关于 barrageTypeCallback 函数


这个方法主要是可以解决弹幕样式定制的问题,你可以根据每个弹幕的类型 做不同的样式对象返回,我们会自动帮你渲染。


barrageTypeCallback ( {args} ) {

const { barrageCategary } = args

if(barrageCategary === 'type1'){

retun {
className : 'classOne',
children : {
show : false
i : {
showIcon : false,
style : {
color : 'red'
}
}
}
}
}
else{

return { className : 'default' }
}
}




结束语


前面的所有代码只是想告诉大家这个设计思想,当你的思维模型出来以后,其实很轻松。


我是 前端小张同学

作者:前端小张同学
来源:juejin.cn/post/7243680440694980668
期待你的关注,谢谢。

收起阅读 »

Android 即将进入大AI时代

一. 前言 自从OpenAI流行之后,我对这一块的方向还是比较关注的。前段时间Google IO大会AI部分也是占了很大的比重了,而且从google的部署来看,也差不多是往我预期的方向去发展,我所关注的东西其实很简单,就是辅助开发,而Google IO大会中的...
继续阅读 »

一. 前言


自从OpenAI流行之后,我对这一块的方向还是比较关注的。前段时间Google IO大会AI部分也是占了很大的比重了,而且从google的部署来看,也差不多是往我预期的方向去发展,我所关注的东西其实很简单,就是辅助开发,而Google IO大会中的内容也让我意识到了,他们确实有在往这个方向去发展。虽然现在还处于一个比较鸡肋的阶段,但是这是一个进入大AI时代的信号。


二. 现状


对于现在的一个环境而言,AI是已经能进行一些基础的辅助开发了。最简单的做法我之前也有说过一些,juejin.cn/post/721986…


这时候有人就说了,我们这些做开发的,还要搞这些打开网页后复制粘贴的操作,太low了。没错,所以一般我们希望能做到的第一步就是集成,把AI集成到我们的IDE中。


1. Studio Bot


而这次的Google IO大会就有这么一个东西 Studio Bot ,把AI集成到AndroidStudio中,注意,这是官方的,虽然现在相当于一个试用的阶段,但至少也能看出了官方的决心:要做一款google自己的AI工具 ,而往往google这几年出的东西都是比较香的,所以我很看好几年后能使用到成熟的AI工具。


想要了解的可以去看看官网 developer.android.com/studio/prev… ,首先需要下载最新的版本Android Studio Hedgehog,然后按照流程去注册使用Studio Bot,文档卡里面讲得还是比较清楚的,我这里就不重复搬过来说了。


但其实你别看它这个东西提出来了,其实当前还是比较鸡肋的,而且现在使用的人很少,后续可能还会进行优化和功能的扩充。我建议大家看看演示就行了,没必要下载预览版来尝试,首先预览版会有很多问题,其次上面说了,当前的功能比较鸡肋,估计你就玩个一两个小时就失去兴趣了,现在用来直接辅助开发我觉得还尚早。


讲完官方的,我们可以来讲讲目前成熟的插件。


2. Bito


Bito是ChatGPT团队开发的一款插件,而我们的IDE能够使用这款插件来辅助开发,想要了解的话可以看看官网的介绍 bito.ai/


AndroidStudio使用Bito的方法也很简单,首先在搜索这个插件


image.png


然后安装,然后点击Help ->Find Action,输入Choose Boot Java Runtime for the IDE


image.png


select runtime中JCEF有两个一模一样的,有一个是有问题的,有一个是正常的,试试就知道了。安装之后重启,然后点AS右边的Bito进行登录就能使用了


image.png


没有号的话注册一个就行,流程挺简单的,登录之后就可以直接使用


image.png


Bito的好处就是整个流程引入下来很方便,但是它也有很明显的缺点,那就是太慢了,不知道是因为使用的人多还是什么问题,它的回复速度非常的慢。我自己是给了它一个json,然后让他生成一个kotlin的data类,结果很久才生成出来,我可能自己撸代码都撸完了。虽然慢,但也是能用,比如你有什么问题,还是可以问他的,但我是宁愿打开GPT网页直接使用


3. Github Copilot


Github Copilot和Bot一样是插件,而相对于Bot,Github Copilot会更快,而且能做到的更多(相对更多,其实也挺鸡肋)


使用的方法也是直接插件搜Github Copilot


image.png


安装,装完之后它会重启AS,然后弹出个通知让你去登录GitHub,你不小心关掉也没关系,AS的底部也有个图片能点出来


image.png


image.png


点Copy and Open 会打开github让你把Device code输入进去,没登录的话会先登录(Github总不能没号吧)


image.png


输入后你的github就会进入这个页面,你的github菜单就会多出一个Copliot


image.png


那么这是干什么的呢?这是收费的啊大哥,是可以有一个月的免费体验,但是你要有VISA,因为要填信息,你得填完信息才给用,或者你有教育邮件(就是学生或者老师),一般你上大学的话都会有个学校的邮箱。这个类似苹果那种教育优惠。


但我非常不建议学生使用这种方式去注册,因为我上面说了,这个功能其实不算成熟,你肯呢个玩个一两小时就失去兴趣了,然后你把你的信息给暴露出去,我觉得这有点得不偿失。


所以有VISA的可以体验一个月,有钱的,当我没说。那又没VISA,不用学校邮箱,要怎么弄呢?买一个号啊,这种东西在我神州大陆会缺?


Github Copilot相对于Bito的功能和性能都会强大一些,但是Github Copilot的引入就没有Bito的方便


4. 小结


这边介绍了3个目前AS能使用的AI工具,一个官方的工具studio bot,两个插件Bito和Github Copilot,加上直接在GPT网页打开GPT使用这4种方式中。


我个人肯定是最看好studio bot,官方出品,必属精品,其他IDE我不敢说,但是Android Studio未来肯定是studio bot最好用。


而就目前来说,无论使用哪种都有一定的成本,首先肯定是科学上网,GPT有些大佬会迁移出来,不科学上网也是能使用的。其次就是账号问题,像Github Copilot这种账号申请难度就比较高,我建议想用的话直接买号。


最后目前对于辅助开发而言(对于其他使用我可能不太清楚,我比较关心的是辅助开发的效果),功能上还不是很成熟,说得好听就是可能对我的开发流程而言用处不是很大,说得难听一点就是我自己敲代码都比他快。


有的人可能会觉得还是用处挺大的。我仅代表我自己的观点,像我开发的话习惯使用一些模板,快捷键和AS提供的一些工具。我不敢保证说每个人都会去使用这些工具,但就我而言,比如拿Bot来说,你可能会喜欢给他一些简单的逻辑让它生成代码,但是我使用模板使用AS的工具生成代码的速度比它更快。我反而会在一些比如说Json生成Data,下划线转驼峰,或者突然忘记一些知识点的时候去使用,但是这种情况下直接打开网页使用GPT我感觉更好。


三. 展望


就是这个google IO大会,提出AI这个方向之后,特别是今年提出这些东西之后,其实对之后的影响还是挺大的。这个可以一点一点慢慢说。


首先是Android的一个技术更新,像之前google提出的JetPack、kotlin、flutter等,其实都是很好的技术,都是要学的,现在相当于是重心放AI了,所以之后像这类技术的展现可能周期会相对长一些。


然后是大家比较关心的一个问题,会不会AI技术成熟之后,就不需要开发人员了,直接AI就行了,程序员全部失业。我觉得应该不会达到这种地步吧,如果真能达到这种地步,那AI都能自己给自己编程了,这种情况就超出想象了,完全就是科幻里面的那种。 但是我认为最终是能做到极致的辅助开发,宏观上来看就是能辅助我们把开发时间缩短到一半以下。那么就有可能会出现说公司只要一两个核心的成员,配合AI进行开发,就顶原本的五六个人开发。 这其实是要看老板吧,他觉得你的效率因为AI的配合能提高一倍,你一个人就能做两个人的事,那我干嘛养两个人,养一个人不更划算?当然老板也会觉得,那AI提高了你的效率,你就做完6点下班吧,不用加班了。其实话说到这里,懂的都懂。


所以这个发展是好还是坏,其实我也不清楚。但是单纯对开发来说,肯定是好的。那要怎样才能达到我说的辅助开发的地步,我又为什么这么看好studio bot?


这里得聊一些基础,


我们写代码,我们编译,打包APK等等这些操作,其实都是对文件操作,这个能理解吧。比如class文件用dx工具生成dex文件。然后这些操作,我可以写一个脚本去做吧,写脚本去操作文件。


比如我想用json生成kotlin的data类这件事,我可以用脚本去做,我可以用脚本接受json输入,然后按照data类的格式去让脚本生成一个data类的.kt文件,这个脚本是可以做到的,这个要先清楚。


那既然脚本能做到的事,AI你觉得做不做得到?为什么我不写脚本去完成这件事,因为这件事不麻烦,我反而写脚本更麻烦,但是使用AI去完成这件事并不麻烦啊,所以这是能很明显的提高开发的效率。


但是现阶段的AI的问题是什么呢?是拿不到上下文,简单来说它只能单纯的作为聊天工具,它拿不到我们项目的上下文。“帮我根据以下json生成一个data文件放到xxx目录下”,AI当前实现不了这个功能,但是它要做这个效果so esay。我们当前只能说让他生成个data类,然后它在聊天窗口给你生成,你自己创建一个data类然后复制它的内容过去。


所以我看好studio bot的原因之一是因为它是官方的,我觉得它未来是能拿到上下文的。举个简单的例子,它能拿到整个项目并且读写整个项目,那我们让它做的操作,和它说的话都是基于整个项目的,我就不会花费很多时间去给他描述我要做什么,也会省去很多步骤,我现在使用AI来和项目接轨都是要经过一些步骤进行转换的,而这些转换的时间还不如我直接自己撸代码。


如果它能拿到项目这个上下文,我对它说“帮我找xxx页面的布局”,它能帮我直接找的。或者说我们使用retrofit做网络请求,往往要写一些分散的代码,我直接和它说“根据链接、入参、出参新增一个名为xxx的请求”,它能帮我按照其他请求的格式去写到各个文件中。要是能做到这步,那对我们效率的提高就很大了。


“给这个XXXXActivity写个LiveData”,“检查当前类是否有内存泄露可能”,“帮我将当前的中文放到string.xml中”等等。这些操作其实都是开发中的重复操作,其实并没有很依赖业务,而我相信最终studio bot终将会做到这一步。


大AI时代已经开启,这股洪流又会将我们带到何处

作者:流浪汉kylin
来源:juejin.cn/post/7243725952789823525

收起阅读 »

为什么推荐用svg而不用icon?

web
为什么要用svg而没有用icon? 使用背景: 1.因为svg图标在任何设备下都可以高清显示,不会模糊。而icon会在显卡比较低的电脑上有显示模糊的情况 2.svg图标在页面render时 速度会比icon稍微快一点 3.实现小程序换肤功能 ;方案见:ht...
继续阅读 »

为什么要用svg而没有用icon?


使用背景:


图片.png



1.因为svg图标在任何设备下都可以高清显示,不会模糊。而icon会在显卡比较低的电脑上有显示模糊的情况


2.svg图标在页面render时 速度会比icon稍微快一点
3.实现小程序换肤功能 ;方案见:http://www.yuque.com/lufeilizhix…



// svg在html里的使用示例01
<div>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<title>home</title>
<path d="M32 18.451l-16-12.42-16 12.42v-5.064l16-12.42 16 12.42zM28 18v12h-8v-8h-8v8h-8v-12l12-9z"></path>
</svg>
</div>


SVG基础可参考:http://www.yuque.com/lufeilizhix…


Svg-inline的使用


//示例02

import iconShop from '../assets/menuIcon/shop.svg?inline'
import iconCustomer from '../assets/menuIcon/customer.svg?inline'
import iconCustomerService from '../assets/menuIcon/customerService.svg?inline'
import iconNuCoin from '../assets/menuIcon/nuCoin.svg?inline'
import iconBanner from '../assets/menuIcon/banner.svg?inline'
import iconAccount from '../assets/menuIcon/account.svg?inline'
import iconDataReport from '../assets/menuIcon/dataReport.svg?inline'
import iconVera from '../assets/menuIcon/banner_01.svg?inline'

inline svg是目前前端图标解决方案的最优解(当然不仅限于图标),而且使用方式也及其简单,只要将svg图标代码当成普通的html元素来使用即可,如:


<!-- 绘制右箭头 -->
<svg viewBox="0 0 1024 1024" height="1em" width="1em" fill="currentColor">
<path d="M665.6 512L419.84 768l-61.44-64 184.32-192L358.4 320l61.44-64 184.32 192 61.44 64z" />
</svg>

<!-- 绘制边框 -->
<svg viewBox="0 0 20 2" preserveAspectRatio="none" width="100%" height="2px">
<path d="M0 1L20 1" stroke="#000" stoke-width="2px"></path>
</svg>

注意: 新版chrome不支持 # , 需要改成%23 ;stroke="%23000"

作为图片或背景使用时


 icon: https://www.baidu.com+ '/icons/icon_01.svg' 
<image class="headIcon" src="data:image/svg+xml,{{icon}}"></image>
**特别注意 需要把img标签换成image标签**

将上面的代码插入html文档即可以很简单地绘制出一些图标。
正常情况下会将svg保存在本地,具体的页面中导入,参考示例02 作为组件使用;目的是可复用
一般来说,使用inline svg作为图标使用时,想要保留svg的纵横比,可以只指定width属性,但是一般为了清晰都同时指定height属性。但如果是像上面绘制边框这种不需要保留纵横比的情形,可将preserveAspectRatio设置为none


优势与使用方式


从示例01可以看到,将svg直接作为普通html元素插入文档中,其本质和渲染出一个div、span等元素无异,天生具有渲染快、不会造成额外的http请求等优势,除此之外还有以下优势之处:


样式控制更加方便;
inline svg顶层的元素会设置以下几个属性:


height=“1em” width=“1em” 可以方便地通过设置父元素的font-size属性控制尺寸


fill=“currentColor” 可以方便地根据父元素或自身的color属性控制颜色


但是我们也可以为其内部的子元素单独设置样式 参考


注意事项


如需对svg中各部分分别应用样式,则在设计svg时最好不要将各部分都编于一组,可以将应用相同样式的部分进行分别编组,其他不需要设置样式的部分编为一组,这样我们在应用样式时,只需为对应的标签设置class属性即可。


一般在拿到svg文件后,推荐使用svgo优化svg代码,节省体积,但是如果我们需要针对性设置样式时则需要谨慎使用,因为优化代码会进行路径合并等操作,可能我们想要设置的子元素已经不是独立的了。


inline svg的复用及组件化


同一个inline svg必须能够进行复用,将需要复用inline svg封装成组件


// 使用inline svg组件
import AnySvgIcon from './inline-svg-component'
<AnySvgIcon width="16px" height="16px" />

参考:


inline svg和字体图标的对比


字体图标

的使用与设计

收起阅读 »

这几个让代码清新的魔法,让领导追着给我涨薪

web
清晰易维护的代码对于任何软件项目的长期成功和可扩展性至关重要。它提升团队成员之间的协作效率,减少错误的可能性,并使代码更易于理解、测试和维护。在本博文中,我们将探讨一些编写清晰易维护的 JavaScript 代码的最佳实践,并提供代码示例以阐明每个实践方法。 ...
继续阅读 »

清晰易维护的代码对于任何软件项目的长期成功和可扩展性至关重要。它提升团队成员之间的协作效率,减少错误的可能性,并使代码更易于理解、测试和维护。在本博文中,我们将探讨一些编写清晰易维护的 JavaScript 代码的最佳实践,并提供代码示例以阐明每个实践方法。


1. 一致的代码格式:


一致的代码格式对于可读性非常重要。它有助于开发人员更快地理解代码,提升协作效果。使用一致且被广泛接受的代码风格指南,比如 ESLint 提供的指南,并配置你的编辑器或 IDE 以自动格式化代码。
示例:


// 错误的格式化
function calculateSum(a,b){return a+b; }

// 正确的格式化
function calculateSum(a, b) {
return a + b;
}

2. 有意义的变量和函数命名:


为变量、函数和类使用有意义且描述性的名称。避免使用单个字母或容易引起他人困惑的缩写。这种做法提高了代码的可读性,并减少了对注释的需求。
示例:


// 错误的命名
const x = 5;

// 正确的命名
const numberOfStudents = 5;

3. 模块化和单一职责原则:


遵循单一职责原则,为函数和类设定单一、明确的职责。这种做法提高了代码的可重用性,并使其更易于测试、调试和维护。
示例:


// 错误的做法
function calculateSumAndAverage(numbers) {
let sum = 0;
for (let i = 0; i < numbers.length; i++) {
sum += numbers[i];
}
const average = sum / numbers.length;
return [sum, average];
}

// 正确的做法
function calculateSum(numbers) {
let sum = 0;
for (let i = 0; i < numbers.length; i++) {
sum += numbers[i];
}
return sum;
}

function calculateAverage(numbers) {
const sum = calculateSum(numbers);
const average = sum / numbers.length;
return average;
}

4. 避免全局变量:


尽量减少使用全局变量,因为它们可能导致命名冲突,并使代码更难以理解。相反,封装你的代码到函数或模块中,并尽可能使用局部变量。
示例:


// 错误的做法
let count = 0;

function incrementCount() {
count++;
}

// 正确的做法
function createCounter() {
let count = 0;

function incrementCount() {


count++;
}

return {
incrementCount,
getCount() {
return count;
}
};
}

const counter = createCounter();
counter.incrementCount();

5. 错误处理和鲁棒性:


优雅地处理错误,并提供有意义的错误信息或适当地记录它们。验证输入,处理边界情况,并使用正确的异常处理技术,如 try-catch 块。
示例:


// 错误的做法
function divide(a, b) {
return a / b;
}

// 正确的做法
function divide(a, b) {
if (b === 0) {
throw new Error('Cannot divide by zero');
}
return a / b;
}

try {
const result = divide(10, 0);
console.log(result);
} catch (error) {
console.error(error.message);
}

6. 避免重复代码:


代码重复不仅会导致冗余代码,还会增加维护和修复错误的难度。将可重用的代码封装到函数或类中,并努力遵循 DRY(Don't Repeat Yourself)原则。如果发现自己在复制粘贴代码,请考虑将其重构为可重用的函数或模块。
示例:


// 错误的做法
function calculateAreaOfRectangle(length, width) {
return length * width;
}

function calculatePerimeterOfRectangle(length, width) {
return 2 * (length + width);
}

// 正确的做法
function calculateArea(length, width) {
return length * width;
}

function calculatePerimeter(length, width) {
return 2 * (length + width);
}

7. 明智地使用注释:


干净的代码应该自解释,但有些情况下需要注释来提供额外的上下文或澄清复杂的逻辑。谨慎使用注释,并使其简洁而有意义。注重解释“为什么”而不是“如何”。
示例:


// 错误的做法
function calculateTotalPrice(products) {
// 遍历产品
let totalPrice = 0;
for (let i = 0; i < products.length; i++) {
totalPrice += products[i].
price;
}
return totalPrice;
}

// 正确的做法
function calculateTotalPrice(products) {
let totalPrice = 0;
for (let i = 0; i < products.length; i++) {
totalPrice += products[i].
price;
}
return totalPrice;
// 总价格通过将数组中所有产品的价格相加来计算。
}

8. 优化性能:


高效的代码提升了应用程序的整体性能。注意不必要的计算、过度的内存使用和潜在的瓶颈。使用适当的数据结构和算法来优化性能。使用类似 Chrome DevTools 的工具对代码进行性能分析和测量,以识别并相应地解决性能问题。


示例:


// 错误的做法
function findItemIndex(array, target) {
for (let i = 0; i < array.length; i++) {
if (array[i] === target) {
return i;
}
}
return -1;
}

// 正确的做法
function findItemIndex(array, target) {
let left = 0;
let right = array.length - 1;

while (left <= right) {
const mid = Math.floor((left + right) / 2);

if (array[mid] === target) {
return mid;
}

if (array[mid] < target) {
left = mid +
1;
}
else {
right = mid -
1;
}
}

return -1;
}

9. 编写单元测试:


单元测试对于确保代码的正确性和可维护性非常重要。编写自动化测试以覆盖不同的场景和边界情况。这有助于尽早发现错误,便于代码重构,并对修改现有代码充满信心。使用像 Jest 或 Mocha 这样的测试框架来编写和运行测试。
示例(使用 Jest):


// 代码
function sum(a, b) {
return a + b;
}

// 测试
test('sum 函数正确地相加两个数字', () => {
expect(sum(2, 3)).toBe(5);
expect(sum(-1, 5)).toBe(4);
expect(sum(0, 0)).toBe(0);
});

10. 使用函数式编程概念:


函数式编程概念,如不可变性和纯函数,可以使代码更可预测且更易于理解。拥抱不可变数据结构,并尽量避免对对象或数组进行突变。编写无副作用且对于相同的输入产生相同输出的纯函数,这样更容易进行测试和调试。
示例:


// 错误的做法
let total = 0;

function addToTotal(value) {
total += value;
}

// 正确的做法
function addToTotal(total, value) {
return total + value;
}

11. 使用 JSDoc 文档化代码:


使用 JSDoc 来为函数、类和模块编写文档。这有助于其他开发人员理解你的代码,并使其更易于维护。


/**
* 将两个数字相加。
* @param {number} a - 第一个数字。
* @param {number} b - 第二个数字。
* @returns {number} 两个数字的和。
*/

function add(a, b) {
return a + b;
}

12. 使用代码检查工具和格式化工具:


使用 ESLint 和 Prettier 等工具来强制执行一致的代码风格,并在问题出现之前捕获潜在问题。


// .eslintrc.json
{
"extends": ["eslint:recommended", "prettier"],
"

plugins"
: ["prettier"],
"rules": {
"prettier/prettier": "error"
}
}

// .prettierrc.json
{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "all"
}

结论:


编写清晰且易于维护的代码不仅仅是个人偏好的问题,而是一项专业责任。通过遵循本博文中概述的最佳实践,您可以提高 JavaScript 代码的质量,使其更易于理解、维护和协作,并确保软件项目的长期成功。在追求清晰且易于维护的代码时,请牢记一致性、可读性、模块化和错误处理这些关键原则。祝你编码愉快!

原文:dev.to/wizdomtek/b…
翻译 / 润色:ssh

作者:ssh_晨曦时梦见兮
来源:juejin.cn/post/7243680592192782393

收起阅读 »

速度优化:重新认识速度优化

应用的速度优化是我们使用最频繁,也是应用最重要的优化之一,它包括启动速度优化,页面打开速度优化,功能或业务执行速度优化等等,能够直接提升应用的用户体验。因此,只要是 Android 开发者,肯定或多或少有过速度相关的优化经验。但是大部分人谈到速度优化,只能想到...
继续阅读 »

应用的速度优化是我们使用最频繁,也是应用最重要的优化之一,它包括启动速度优化,页面打开速度优化,功能或业务执行速度优化等等,能够直接提升应用的用户体验。因此,只要是 Android 开发者,肯定或多或少有过速度相关的优化经验。但是大部分人谈到速度优化,只能想到一些零碎的优化点,比如使用多线程、预加载等等。这对速度的提升肯定是不够的,想要做得更好,我们不妨来思考下面几个问题:




  • 我们的优化方案是全面且体系的吗?




  • 我们的方案为什么能提升速度呢?




  • 我们的方案效果怎样?




想要回答好这几个问题,我们就需要了解影响和决定应用速度的底层原理及本质。那从底层来看,CPU、缓存、任务调度才是决定应用速度最本质的因素。CPU 和缓存都属于硬件层,任务调度机制则属于操作系统层。


那这一节课,我们就一起深入硬件和操作系统层面去了解以上三个因素是如何决定应用速度的,重新认识应用的速度优化,由下而上地建立起速度优化的认知体系和方法。


如何从 CPU 层面进行速度优化?


我们知道,所有的程序最终会被编译成机器码指令,然后交给 CPU 执行,CPU 以流水线的形式一条一条执行程序的机器码指令。当我们想要提升某些场景(如启动、打开页面、滑动等)的速度时,本质上就是降低 CPU 执行完这些场景指令的时间,这个时间简称为 CPU 时间。想要降低 CPU 时间,我们需要先知道程序所消耗 CPU 时间的计算公式:CPU 时间=程序的指令数 x 时钟周期时间 x 每条指令的平均时钟周期数。下面一一解释一下这三项因子的含义。




  • 程序的指令数:这一项很好理解,就是程序编译成机器码指令后的指令数量。




  • 时钟周期时间:每一次时钟周期内,CPU 仅完成一次执行,所以时钟周期时间越短,CPU 执行得越快。或许你对时钟周期时间不熟悉,但是它的倒数也就是时钟周期频率,你肯定听说过。1 纳秒的时钟周期时间就是 1 GHZ 的时钟周期频率,厂商发布新手机或者我们购买新手机时,都或多或少会提到 CPU 的时钟频率,比如高通骁龙 888 这款 CPU 的时钟频率是 2.8 GHZ,这个指标也是衡量 CPU 性能最重要的一个指标




  • 每条指令的平均时间周期:是指令执行完毕所消耗的平均时间周期,指令不同所需的机器周期数也不同。对于一些简单的单字节指令,在取指令周期中,指令取出到指令寄存器后会立即译码执行,不再需要其它的机器周期。对于一些比较复杂的指令,例如转移指令、乘法指令,则需要两个或者两个以上的机器周期。




从 CPU 来看,当我们想要提升程序的速度时,优化这三项因子中的任何一项都可以达到目的。那基于这三项因子有哪些通用方案可以借鉴呢?


减少程序的指令数


通过减少程序的指令数来提升速度,是我们最常用也是优化方案最多的方式,比如下面这些方案都是通过减少指令数来提升速度的。




  1. 利用手机的多核:当我们将要提速的场景的程序指令交给多个 CPU 同时执行时,对于单个 CPU 来说,需要执行的指令数就变少了,那 CPU 时间自然就降低了,也就是并发的思想。但要注意的是,并发只有在多核下才能实现,如果只有一个 CPU,即使我们将场景的指令拆分成多份,对于这个 CPU 来说,程序的指令数依然没有变少。如何才能发挥机器的多核呢?使用多线程即可,如果我们的手机是 4 核的,就能同时并发的运行 4 个线程。




  2. 更简洁的代码逻辑和更优的算法:这一点很好理解,同样的功能用更简洁或更优的代码来实现,指令数也会减少,指令数少了程序的速度自然也就快了。具体落地这一类优化时,我们可以用抓 trace 或者在函数前后统计耗时的方式去分析耗时,将这些耗时久的方法用更优的方式实现。




  3. 减少 CPU 的闲置:通过在 CPU 闲置的时候,执行预创建 View,预准备数据等预加载逻辑,也是减少指令数的一种优化方案,我们需要加速场景的指令数量由于预加载执行了一部分而变少了,自然也就快了。




  4. 通过其他设备来减少当前设备程序的指令数:这一点也衍生很多优化方案,比如 Google 商店会把某些设备中程序的机器码上传,这样其他用户下载这个程序时,便不需要自己的设备再进行编译操作,因为提升了安装或者启动速度。再比如在打开一些 WebView 网页时,服务端会通过预渲染处理,将 IO 数据都处理完成,直接展示给用户一个静态页面,这样就能极大提高页面打开速度。




上面提到的这些方案都是我们最常用的方案,基于指令数这一基本原理,还能衍生出很多方案来提升速度,这里没法一一列全,大家也可以自己想一想还能扩展出哪些方案出来。


降低时钟周期时间


想要降低手机的时钟周期,一般只能通过升级 CPU 做到,每次新出一款 CPU,相比上一代,不仅在时钟周期时间上有优化,每个周期内可执行的指令也都会有优化。比如高通骁龙 888 这款 CPU 的大核时钟周期频率为 2.84GHz,而最新的 Gen 2 这款 CPU 则达到了 3.50GHz。


虽然我们没法降低设备的时钟周期,但是应该避免设备提高时钟周期时间,也就是降频现象,当手机发热发烫时,CPU 往往都会通过降频来减少设备的发热现象,具体的方式就是通过合理的线程使用或者代码逻辑优化,来减少程序长时间超负荷的使用 CPU。


降低每条指令的平均时间周期


在降低每条指令的平均时间周期上,我们能做的其实也不多,因为它和 CPU 的性能有很大的关系,但除了 CPU 的性能,以下几个方面也会影响到指令的时间周期。




  1. 编程语言:Java 翻译成机器码后有更多的简介调用,所以比 C++ 代码编译成的机器码指令的平均时间周期更长。




  2. 编译程序:一个好的编译程序可以通过优化指令来降低程序指令的平均时间周期。




  3. 降低 IO 等待:从严格意义来说,IO 等待的时间并不能算到指令执行的耗时中,因为 CPU 在等待 IO 时会休眠或者去执行其他任务。但是等待 IO 会使执行完指令的时间变长,所以这里依然把减少 IO 等待算入是降低每条指令的平均时间周期的优化方案之一。




如何从缓存层面进行速度优化?


程序的指令并不是直接就能被 CPU 执行的,而是要放在缓存中,CPU 从缓存中读取,而且一个程序也不可能全是 CPU 计算逻辑,必然也会涉及到 IO 的操作或等待,比如往磁盘或者内存中读写数据成功后才能继续执行后面的逻辑,所以缓存也是决定应用速度的关键因素之一。缓存对程序速度的影响主要体现在 2 个方面:




  1. 缓存的读写速度;




  2. 缓存的命中率。




下面就详细讲解一下这 2 方面对速度的影响。


缓存的读写速度


手机或电脑的存储设备都被组织成了一个存储器层次结构,在这个层次结构中,从上至下,设备的访问速度越来越慢,但容量也越来越大,并且每字节的造价也越来越便宜。寄存器文件在层次结构中位于最顶部,也就是第 0 级。下图展示的是三层高速缓存的存储结构。


img


高速缓存是属于 CPU 的组成部分,并且实际有几层高速缓存也是由 CPU 决定的。以下图高通骁龙 888 的芯片为例,它是 8 块核组成的 CPU,从架构图上可以看到,它的 L2 是 1M 大小(没有 L1 是因为这其实只是序号称呼上的不同而已,你也可以理解成 L1),L3 是 3M 大小,并且所有核共享。


img


不同层之间的读写速度差距是很大的,所以为了能提高场景的速度,我们需要将和核心场景相关的资源(代码、数据等)尽量存储在靠上层的存储器中。 基于这一原理,便能衍生出了非常多的优化方案,比如常用的加载图片的框架 Fresco,请求网络的框架 OkHttp 等等,都会想尽办法将数据缓存在内存中,其次是磁盘中,以此来提高速度。


缓存的命中率


将数据放在缓存中是一种非常入门的优化思想,也是非常容易办到的,即使是开发新手都能想到以此来提升速度。但是我们的缓存容量是有限的,越上层的缓存虽然访问越快,但是容量越少,价格也越贵,所以我们只能将有限的数据存放在缓存中,在这样的制约下,提升缓存的命中率往往是一件非常难的事情


一个好的编译器可以提升寄存器的命中率,好的操作系统可以提升高速缓存的命中率,对于我们应用来说,好的优化方案可以提升主存和硬盘的命中率,比如我们常用的 LruCache 等数据结构都是用来提升主存命中率的。除了提升应用的主存,应用也可以提升高速缓存的命中率,只是能做的事情不多,后面的章节中也会介绍如何通过 Dex 中 class 文件重排,来提升高速缓存读取类文件时的命中率。


想要提高缓存命中率,一般都是利用局部性原理(局部性原理指如果某数据被访问,则不久之后该数据可能再次被访问,或者程序访问了某个存储单元,则不久之后,其附近的存储单元也将被访问)或者通过行为预测,分析大概率事件等多种原理来提高缓存命中率。


如何从任务调度层面进行速度优化?


我们学过操作系统为了能同时运行多个程序,所以诞生了虚拟内存这个技术,但只有虚拟内存技术是不够的,还需要任务调度机制,所以任务调度也属于操作系统关键的组成之一。有了任务调度机制,我们的程序才能获得 CPU 的资源并正常跑起来,所以任务调度也是影响程序速度的本质因素之一


我们从两个方面来熟悉任务调度机制,一是调度机制的原理,二是任务的载体,即进程的生命周期。


在 Linux 系统中,任务调度的维度是进程,Java 线程也属于轻量级的进程,所以线程也是遵循 Linux 系统的任务调度规则的,那进程的调度规则又是怎样的呢?Linux 系统将进程分为了实时进程和普通进程这两类,实时进程需要响应技术的进程,比如 UI 交互进程,而普通进程对响应速度要求不是非常高,比如读写文件、下载等进程。两种类型的进程的调度规则也不一样,我们分别来说。


首先是实时进程的调度规则。Linux 系统对实时进程的调度策略有两种:先进先出(SCHED_FIFO)和循环(SCHED_RR)。Android 只使用了 SCHED_FIFO 这一策略,所以我们主要介绍 SCHED_FIFO 。当系统使用先进先出的策略来调度进程时,如果某个进程占有 CPU 时间片,此时没有更高优先级的实时进程抢占 CPU,或该进程主动让出,那么该进程就始终保持使用 CPU 的状态。这种策略会提高进程运行的持续时间,减少被打断或被切换的次数,所以响应更及时。Android 中的 AudIO、SurfaceFlinger、Zygote 等系统核心进程都是实时进程。


非实时进程也称为普通进程,针对普通进程,Linux 系统则采用了一种完全公平调度算法来实现对进程的切换调度,我们可以不需要知道这一算法的实现细节,但需要了解它的原理。在完全公平调度算法中,进程的优先级由 nice 值表示,nice 值越低代表优先级越大,但是调度器并不是直接根据 nice 值的大小作为优先级来进行任务调度的,当每次进程的时间片执行完后,调度器就会寻找所有进程中运行时间最少的进程来执行


既然调度器是根据进程的运行时间来进行任务调度,那进程优先级即 nice 值的作用又体现在哪呢?实际上,这里进程的运行时间并不是真实的物理运行时间,而是进行了加权计算的虚拟时间,这个权值系数就是 nice 值,所以同样的物理时间内,nice 值越低的进程所记录的运行时间实际越少,运行时间更少就更容易被调度器所选择,优先级也就这样表现出来了。在 Android 中,除了部分核心进程,其他大部分都是普通进程。


了解了进程的调度原理,我们再来了解一下进程的生命周期。


img


通过上图可以看到,进程可能有以下几种状态。并且运行、等待和睡眠这三种状态之间是可以互相转换的。




  • 运行:该进程此刻正在执行。




  • 等待:进程能够运行,但没有得到许可,因为 CPU 分配给另一个进程。调度器可以在下一次任务切换时选择该进程。




  • 睡眠:进程正在睡眠无法运行,因为它在等待一个外部事件。调度器无法在下一次任务切换时选择该进程。




  • 终止:进程终止。




知道了任务调度相关的原理后,怎样根据这些原理性知识来优化应用场景的速度呢?实际上,我们对进程的优先级做不了太大的改变,即使改变了也产生不了太大的作用,但是前面提到了线程实际是轻量级的进程,同样遵循上面的调度原理和规则,所以我们真正落地的场景在线程的优化上。基于任务调度的原理,我们可以衍生出这 2 类的优化思路:




  1. 提高线程的优先级:对于关键的线程,比如主线程,我们可以提高它的优先级,来帮助我们提升速度。除了直接提高线程的优先级,我们还可以将关键线程绑定 CPU 的大核这一种特殊的方式来提高该线程的执行效率。




  2. 减少线程创建或者状态切换的耗时:这一点可以通过在线程池中设置合理的常驻线程,线程保活时间等参数来减少线程频繁创建或者状态切换的耗时。因为线程池非常重要,我们后面会专门用一节课来详细讲解。




小结


在这一节中,我们详细介绍了影响程序速度的三个本质因素,并基于这三个因素,介绍了许多衍生而来优化思路,这其实就是一种自下而上的性能优化思路,也就是从底层原理出发去寻找方案,这样我们在进行优化时,才能更加全面和体系。


希望你通过这一节的学习,能对速度优化建立起一个体系的认知。当然,你可能会觉得我们这一节介绍的优化思路太过简洁,不必担心,在后面的章节中,我们会基于 CPU、缓存和任务调度这三个维度,挑选出一些优化效果较好的方案,进行更加深入的详细讲解。


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

程序员浪漫起来 做一个心形layout

序言 最近浪漫心饱满,于是写了个心形控件。可以将一切内容约束成心形,并且支持两种心形,使用起来很简单,就当成FrameLayout就行了。 让一切都浪漫起来 效果 下面依次展示的是: 包裹ScrollView ,形状为桃心 包裹ScrollView,形状为圆...
继续阅读 »

序言


最近浪漫心饱满,于是写了个心形控件。可以将一切内容约束成心形,并且支持两种心形,使用起来很简单,就当成FrameLayout就行了。
让一切都浪漫起来


效果


下面依次展示的是:



  1. 包裹ScrollView ,形状为桃心

  2. 包裹ScrollView,形状为圆心

  3. 包裹WebView,形状为圆心


在这里插入图片描述


代码


LoveLayout


主要代码就是下面的。

package com.example.myapplication;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Path;
import android.util.AttributeSet;
import android.widget.FrameLayout;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

/**
* <pre>
* Created by zhuguohui
* Date: 2023/6/6
* Time: 10:26
* Desc:爱心Layout
* </pre>
*/
public class LoveLayout extends FrameLayout {

private Path path1;
private int heardType;

public LoveLayout(@NonNull Context context) {
super(context);
}

public LoveLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.LoveLayout);
heardType = array.getInt(R.styleable.LoveLayout_HeardType,0);
array.recycle();

}

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if(heardType==0) {
getHeardPath();
}else{
getHeardPath2();
}
}

private void getHeardPath(){

int px = getMeasuredWidth() / 2;
int py = getMeasuredHeight() / 2;
path1=new Path();
path1.moveTo(px, py);
float rate=getMeasuredWidth()*1.0f/34;
// 根据心形函数画图
for (double i = 0; i < 2 * Math.PI; i += 0.001) {
float x = (float) (16 * Math.sin(i) * Math.sin(i) * Math.sin(i));
float y = (float) (13 * Math.cos(i) - 5 * Math.cos(2 * i) - 2 * Math.cos(3 * i) - Math.cos(4 * i));
x *= rate;
y *= rate;
x = px - x;
y = py - y;
path1.lineTo(x, y);
}
}

private void getHeardPath2(){
// f(x)=sqrt(1-(abs(x)-1)^2)
// h(x)=-2*sqrt(1-0.5*abs(x))
path1=getPathByMathFunction(x -> (float) Math.sqrt(1-Math.pow((Math.abs(x)-1),2)));
Path path2=getPathByMathFunction(x->(float) (-2* Math.sqrt(1-0.5*Math.abs(x))));
path1.moveTo(0,getMeasuredHeight()*1.0f/2);
path1.addPath(path2);
}

private interface MathFunction{
float call(float x);
}

private Path getPathByMathFunction(MathFunction function){
Path path=new Path();
path.moveTo(0,getMeasuredHeight()*1.0f/2);
int px = getMeasuredWidth() / 2;
int py = getMeasuredHeight() / 2;
float scale=getMeasuredWidth()*1.0f/4;
for(float i=-2;i<=2;i+=0.01){
float x=i;
float y= function.call(x);
x*=scale;
y*=scale;
x=px-x;
y=py-y;

path.lineTo(x,y);
}
return path;
}

@Override
protected void dispatchDraw(Canvas canvas) {
int save = canvas.save();
canvas.clipPath(path1);
super.dispatchDraw(canvas);
canvas.restoreToCount(save);
}
}


xml属性

<?xml version="1.0" encoding="utf-8"?>
<resources>


<declare-styleable name="LoveLayout">

<attr name="HeardType">
<!-- 桃心 -->
<enum name="PeachHeart" value="0" />
<!-- 圆一点的心 -->
<enum name="CircularHeart" value="1" />
</attr>
</declare-styleable>


</resources>

数学原理


圆心是按照以下公式实现的,桃心是网上找到的代码改的,没找到公司。
在这里插入图片描述


使用


很简单,当成FrameLayout包裹就行了。
在这里插入图片描述


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

Android稳定性方案浅析

定义: 这里的Android稳定性单指Crash 指标口径: 用户可感知崩溃率,以用户为纬度,即Crash设备数 / 总设备数 精细化单个bug的纬度: bug频次 爆炸半径 影响时长 从崩溃的发生解决整个链路出发: RD编码 --> 线下测试 --...
继续阅读 »

定义: 这里的Android稳定性单指Crash


指标口径: 用户可感知崩溃率,以用户为纬度,即Crash设备数 / 总设备数


精细化单个bug的纬度:



  • bug频次

  • 爆炸半径

  • 影响时长


从崩溃的发生解决整个链路出发: RD编码 --> 线下测试 --> 灰度发版 --> 崩溃发生&数据采集聚类 --> RD修复 --> 版本发布


整体思路


image.png


1、编码阶段


制定代码规范,加强代码Review,引入静态代码检查,完善测试流程等减少问题发生


a、静态代码检查

有时会出现某些低级错误如:divide by zero 导致的crash,使用Lintdetekt进行静态代码检查不失为一种好方案,可以放在代码合入的SA阶段


b、查看是否需要解决警告问题

在我们编码或者改动历史遗留的代码时,AS中存在一些警告⚠️,commit时要根据提示进行review,是否需要每次修复和改动还有待商榷


c、检测SQL质量

参考Matrix SQLite Lint: 按官方最佳实践自动化检测 SQLite 语句的使用质量;(看能否拓展到其他场景)


d、依赖库的检查 (可以跟组件化相关)

采用gradle脚本编译时检查依赖库是否相同,解决不同组件或者不同APP之间SDK版本不一致可能导致的崩溃,避免CI阶段打包失败或者上线后出现异常



  • 新增or改动SDK检测,CI diff 产出依赖树 全功能提测阶段


e、review规范


  • 提交reviewer的质量把控意识;关键代码需要两个人review,+1 +2通过才可以合入

  • 跟流水线CI自动识别核心类文件要求两人review的能力结合起来


2、测试&流水线


提高稳定性的意识,不管多微小的改动都要进行自测!!!


a.单元测试


  • 重点模块的单测能力


b.自动化测试


  • 结合QA补齐自动化测试的能力,覆盖核心场景

  • 跟devops平台能力结合,将LeakCanary等能力跟Monkey结合,自动创建卡片

  • 针对函数接口的异常数据排雷测试(参考juejin.cn/post/702812…)


c.CI/CD


  • 流水线打包效率提升


3、崩溃数据采集&分析


正如RD讲的那样,给我一个崩溃堆栈我就能定位到问题所在,能完整的还原"事故现场"成为重中之重


a.堆栈反混淆

结合各个公司APM平台上传mapping文件以及符号表,每个崩溃数据均展示混淆之前的定位


b.平台堆栈聚类


  • 接入iqiyi开源的XCrash库或者breakpad,上报java和native的崩溃堆栈到Server并聚类展示

  • 接入开源的Sentry库并搭建本地私有化服务,实现崩溃的上报和聚类


c.平台数据状态标识

已修复、Pending、下个版本修复等状态或者备注的标识,每次分析结果留下文字记录


d.分模块上传关键数据

会员模块上传会员信息、支付模块上传订单信息等


4、灰度阶段


a.测试轨道前置小版本提前暴露问题

b.三方SDK降级策略

firebase、广告库等三方SDK升级一定要观察崩溃率变化,并做好降级处理


c.crash率异常熔断机制


  • 灰度过程缺少中间对Crash率异常的评定标准,业务异常的评定标准

  • 整体&每个崩溃数据的量级(该崩溃人数/安装率)记录,设定阈值,每日将两个版本对比骤增或者骤降的数据输出并产出报告


c.上车策略

核心思路是代码改动最小化,预留todo下迭代改,避免造成新的线上crash


5、重点问题解决


I.源码分析

虽然 androidxref.comcs.android.com 都可以在线查阅源码,但这两处的Android版本并不全;android.googlesource.com 这里可以下载到几乎所有版本的源码,本地通过 Sublime 分析源码也十分方便(可以直接显示和跳转到方法的定义&引用位置)。


II.OOM问题

a.大图监控治理


  • 线下监控:通过插件在 mergeResources 任务后,遍历图片资源,搜集超过阈值的图片资源,输出列表

  • 参考NativeBitmap 把应用内存使用的大头(即 Bitmap 的像素占用的内存)转移到 Native 堆;但是可能会导致32位虚拟内存不足

  • 在接口层中将所有被创建出来的 Bitmap 加入一个 WeakHashMap,同时记录创建 Bitmap 的时间、堆栈等信息,然后在适当的时候查看这个 WeakHashMap 看看哪些 Bitmap 仍然存活来判断是否出现 Bitmap 滥用或泄漏。 微信 Android 终端内存优化实践


b.hook pthred

采用bhook,针对pthread_create创建的子线程中发生了信号(SIGSEGV)进行兜底操作;相当于catch native crash,发生crash时重新执行之前的逻辑


c.32位虚拟内存优化


  • Patrons 通过一系列技术手段实现运行期间动态调整Region Space预分配的地址空间

  • mSponge 优化了虚拟机对 LargeObjectSpace 的内存管理策略,间接增加其它内存空间使用上限 (未开源)

  • pthread hook 对 native 线程的默认栈大小进行减半


image.png


d.监控

接入KOOM,进行线下OOM发生时的采样上报


III.native crash


6、防裂化&基础建设


a.版本回顾

崩溃数据自动化采集,以月度或者季度为纬度爬取数据,形成总结


b.崩溃保护和安全模式机制


  • 通过ASM在编译时对四大组件生命周期等关键代码加上try-catch处理

  • 通过一定的策略,针对反复重启导致的崩溃问题,让用户选择继续初始化或者清除数据


c.日志回捞


  • 全埋点用户操作路径辅助分析

  • 参考Logan进行日志回捞系统的建设,方便针对某一用户发生问题后捞回日志分析


d.移动端性能中台

自建集崩溃监控、上报、分析、归因于一体(可以参考Matrix直接建立),可以轻松定位各种线上疑难杂症,更有超详细性能、卡顿、打点等全流程监控处理平台


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

我竟然突然焦虑,并且迷茫了

随想录】我尽然突然焦虑,并且迷茫了 「随想录」 这是师叔对自我现状的剖析和寻找了一些 “新的方向” “新的视角” 来重新审视自我的思想录,希望我的家银们在文章中得到思想启发或以我为鉴,不去做无谓思想内耗! 最近是怎么了 最近几个朋友,突然询问我,现在应该怎...
继续阅读 »

随想录】我尽然突然焦虑,并且迷茫了



「随想录」


这是师叔对自我现状的剖析和寻找了一些 “新的方向” “新的视角” 来重新审视自我的思想录,希望我的家银们在文章中得到思想启发以我为鉴,不去做无谓思想内耗



最近是怎么了


最近几个朋友,突然询问我,现在应该怎么学习,将来才会更好的找工作,怕毕业以后没有饭吃,我说我其实也不太清楚,我目前三段实习我都没有找到一份真正意义的好工作,就是那种我喜欢这门领域,并且喜欢公司的氛围,并且到老了还能保持竞争力(莫有35岁危机)。



所以说我真的没有一个准确的答案回复。但是我以为目前的眼光来看一份好工作必备的条件就是,我在这个领域学的越多,我的工资和个人发展瓶颈越高,这份工作是一个持续学习的过程,并且回报和提高是肉眼可见的!



回忆那个时候


其实说实话,这个疑惑我上大一就开始有,但是那个时候是从高考的失落中寻找升学的路径,开始无脑的刷那种考研短视频



(看过可能都知道真的一下子励志的心就有了,但是回到现实生活中,看到身边人的状态~~~没错人就是一个从众的种群,你可能会问你会不会因为大一没有那么努力学习而后悔,但是其实我不会,因为那一年我的经历也是我最开心大学生活,虽然也干了很多被室友做成梗的糗事,但是想一想那不就是青春嘛,要是从小就会很有尺度的为人处世,想一想活着也很累嘛,害,浅浅致敬一下充满快乐和遗憾的青春呀!)


个人看法


哈哈,跑题了。给大家点力量把!前面满满的焦虑。其实我感觉我们都应该感谢我们来到计算机类的专业,从事这方面的学习和研究。


因为计算机的扩展性,不得不说各行各业都开始越来越喜欢我们计算机毕业的大学生(就业方向更加广),我也因为自己会计算机,成功进入一个一本高校以上的教育类公司实习(同时也是这个时候知道了更多优秀学校的毕业年轻人,真正认识到学校的层次带给人的很多东西真正的有差距);



虽然我是二本的学生,但是在亲戚朋友眼里,虽然学校比不上他们的孩子,但是计算机专业也能获得浅浅的也是唯一一点可以骄傲的东西(活在别人嘴这种思考方式肯定不是对的,但是现实就是在父母那里,我们考上什么大学和进入了哪里工作真的是他们在外人的脸面,这种比较情况在大家族或者说农村尤为严重);



技术论打败学校论,计算机专业是在“广义”上为数不多能打破学校出身论的学科,在公司上只要你能干活,公司就愿意要你,这个时候肯定有人diss我,现在培训班出来的很多都找不到工作呀,我的回答只能是:的确,因为这个行业的红利期展示达到了瓶颈期,加上大环境的不理想,会受到一些影响,但是我还是相信会好的,一切都会好的。



做技术既然这样了


关于最近论坛上说“前段已死”“后端当牛做马”“公司磨刀霍霍向测试”......



这个东西怎么说,我想大部分人看到这个都会被这个方向劝退,我从两个角度分析一下,上面说了,真滴卷,简历真滴多,存在过饱和;第二点,希望这个领域新人就不要来了,就是直接劝退,被让人来卷,狭义上少卷一些......



现在就是导致我也不敢给朋友做建议了,因为当他看到这些的时候,和进入工作环境真的不好,我真的怕被喷死


包括现在我的实习,大家看我的朋友圈看出工作环境不错很好,但是和工作的另一面,是不能发的呀,有时候我都笑称自己是“产业工人”(这个词是一个朋友调侃我的)


不行了,在传播焦虑思想,我该被喷死了,现在我给建议都变得很含蓄,因为时代红利期真的看不透,我也不敢说能维持多少年,而且我工作也一般,我不敢耽误大家(哈哈哈,突然想起一句话,一生清贫怎敢入繁华,二袖清风怎敢误佳人,又是emo小文案,都给我开E)


个人总结


本文就是调侃一下现在的环境啊,下面才是重点,只有干活和真话放在后面(印证一个道理:看到最后的才是真朋友才敢给真建议,我也不怕被骂)



心态方面:我们这个年纪就是迷茫的年纪,迷茫是一种正常的状态,因为作为一名成年人你真正在思考你的个人发展的状态,所以请把心放大,放轻松,你迷茫了已经比身边的人强太多了,如果真正焦虑的不能去学习了,去找个朋友聊一聊,实在不行,drink个两三瓶,好好睡一觉,第二天继续干,这摸想,这些都算个啥,没事你还有我,实在不行微我聊一聊,我永远都在,我的朋友!



工作方面:俗话说:女怕入错行,男怕娶错人!(突然发现引用没什么用,哈哈)我们可以多去实践,没错就是去实习,比如你想做前端的工作,你就可以直接去所在的城市(推荐省会去找实习)但是朋友其实实习很难,作为过来人,我能理解你,一个人在陌生的城市而且薪资很可怜,面对大城市的租房和吃饭有很多大坑,你要一一面对,但是在外面我们真要学会保护自己,而且实习生活中经济方面肯定要父母支持,所以一定要和父母好好沟通,其实你会发现我们越长大,和父母相处的时光越短。(我今年小年和十五都没在家过,害,那种心理苦的滋味很不好受)



升学方面:不是每一个都适合考研,不要盲从考研。但是这句话又是矛盾的,在我的实习生涯中,学历问题是一个很重要的问题,我们的工作类型真的不同,还是那句话,学历只是一个门槛,只要你迈入以后看的是你的个人能力。说一句悄悄话,我每天工作,最想的事情就是上学,心想老子考上研,不在干这活了,比你们都强。所以你要想考研,请此刻拿出你的笔,在纸上写下你要考研主要三个理由,你会更好的认识自己,更好选择。



好吧,今天的随想录就这摸多,只是对最近看文章有了灵感写下自己的看法,仅供参考哦!


回答问题


回应个问题:很多朋友问我为什么给这摸无私的建议,这是你经历了很多才得到的,要是分享出去,不是很亏?


(你要这摸问,的确你有卷到我的可能性,快给我爬。哈哈哈)可能是博客圈给的思想把,其实我说不上开源的思想,但是我遇到的人对我都是无私分享自己的经验和自己走过的坑,就是你懂吗,他们对我帮助都很大,他们在我眼里就是伟大的人,所以我也想要跟随他们,做追光的人!(上价值了哦,哈哈)



写在最后


最后一句话,迷茫这个东西,走着走着就清晰了,迷茫的时候,搞一点学习总是没错的。


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

中年程序员写给36岁的自己

笔者是一名程序员老司机,局限于笔者文笔一般,想到哪写到哪__胡乱写一通,篇幅较长 _,希望通过文章的方式简单的回顾过去、总结现在和展望未来,顺便记录一下,方便以后总结。_ 回顾  忙忙碌碌又一年,看着自己的发量在逐渐的减少,深感焦虑,今天终于有时间可...
继续阅读 »

笔者是一名程序员老司机,局限于笔者文笔一般,想到哪写到哪__胡乱写一通,篇幅较长


_,希望通过文章的方式简单的回顾过去、总结现在和展望未来,顺便记录一下,方便以后总结。_


回顾 


忙忙碌碌又一年,看着自己的发量在逐渐的减少,深感焦虑,今天终于有时间可以回顾自己过去一年的得与失,去年是写给35岁的自己,今年该+1了,还是随笔的方式,想到哪写到哪。


2022年1月30日的时候 ,给自己过去的工作做一次简单的总结,主要还是写给自己,就像现在的时候可以回过头去看,也收获了许多朋友的关注,回去看一年前写的文章,以及大家的留言反馈,深有感触。


回看去年的flag,首先需要检讨一下,基本上都没有完成,但是自己也朝着这个目标在做,收获也是有的:



读书


每个月一本书,一年完成至少10本书的学习计划,学以致用,而不是读完就忘。


写文章


一周完成一篇原创文章,不限类别


早睡早起


每天不晚于11:30休息



关于读书


上半年的时候,自己也是有点焦虑和迷茫,想成长想进步,所以焦虑心情自然就会出现,所以看了一些鸡汤书籍,什么《被讨厌的勇气》、《程序员的自我修养》、《情商》等等。实话说看完之后,确实能够缓解缓解内心的焦虑情绪,但是这些书籍能给到自己的,更多是一些方式和方法,对于内心的空洞和不充实带来的焦虑是没办法缓解的。


所以还需要对症下药,我自己所感受到的空洞和不充实,很多是来自自己对技术知识技能的缺乏和退步,说白了就是作为技术人,不能把技术给弄丢了 ,同样也想不断的跟上时代的步伐。


想要快速解决这种“焦虑”,我需要快速的制定一个短期、中期、长期的目标,围绕着目标去充实自己的知识体系。这里所说的目标还是比较容易制定的,毕竟是关乎自己的成长,也就是自己接下来想要成为什么样的人,什么样的知识体系能够让自己在当前以及未来几年的工作中都有帮助。从这个方面去想,首先未来还是想从事前端,所以我给自己制定的短期目标是算法成长 、中期目标是计算机图形学方面的知识掌握、长期目标是成为一名地图领域的技术专家(ps:说到这里先立个flag,我后面想写一个小册,专门关于地图领域相关的,我也是比较期待自己能写出来什么样的小册,不为别的,就是想把自己的知识沉淀下来)。


讲讲为什么要这么去规划目标,算法算是现在任何技术面试都会涉及的,但是我不是为了面试去看,而是为了提升自己在团队内部的技术影响力,《算法图解》这本书写的简单好理解,作者的思路非常清晰 ,看完之后给团队内部的同学分享,不仅能提升自己,还能带动团队一起学习,一举多得。计算机图形学知识是目前工作中会碰到的,比如渲染、大数据可视化、自动驾驶等等都会涉及,这一部分不建议大家先去看书,没有一本书能够说明白,推荐大家去搜《闫令琪》,非常厉害的大佬,上班路上每天花半个小时-1小时足够了,一个月基本上能够学完,之后再运用到工作中,融会贯通。


单独再讲讲长远目标,我之前并不是搞地图方向的,但是近期这份工作有机会接触到了这方面的工作,让我又重新燃起了工作中的那种欲望,很久没有工作中的那种成就感,这也许是10年前才会有的那种热情,所以我比较坚信未来几年自己希望能够深入投入这个方向,不一定是地图,但一定是和这个方向相关的领域,因为知识都是想通的。


关于写文章


写文章这件事情,我非常佩服一位前同事,也或许是因为我没有做到 ,但是别人坚持每天每日的在做 ,他连续两年每天都能产出一篇原创,关于各个方面的,这是值得我学习的地方,今年争取突破自己。


关于早睡


头发卡卡掉,感觉都是因为没有按时睡觉引起的,还是在能有条件的时候,尽量早睡。


工作


今年的工作可以用“黑暗”、“光明”两个词来概括。


黑暗


2022年经历疫情最严重的一年,大部分时间都是居家办公状态,这也导致和同事们的交流变得很少,很多想要推进的工作变得没那么顺利,徒增了不少压力。


2022年也是“财源滚滚”的一年,看着同事一个个离开,也有不少同事询问工作机会,也确实给自己内心带来不小的冲击,同时危机感也很明显。


在一个地方工作一段时间之后,多少都会遇到各种各样的问题,技术上是最省心的问题,解决就好。有江湖的地方就会有各种复杂到不敢想的关系网,谁是谁的小弟,谁是谁的心腹、谁是大老板招来的等等,遇到这种问题我更多的是做好自己,但我更多还是更愿意沉浸在技术的知识中,享受解决问题带来的快感。面对频繁换老板,技术人的通病,不善于抱大腿,当然我也不想在这方便再去做过多改变或者违背内心去做一些事情,保持好内心的底线,不突破我的底线则相安无事。


光明


呵护好内心的明灯


今年工作最大的动力是来自于自身能力的成长,规划的短中长目标基本上都在按照正确的方向在行进,这也是在排除各种各样的干扰后带来的好的结果,也是抱着一种积极向上的心态在努力,工作中最让人糟心的,无非就是背锅、背指标、裁员,最坏的情况也就这样了,守好内心的方向,做自己想做的事情就对了,自己左右不了的事情不去想太多,行业不景气的时候,我基本上是以这种心态在工作,人生并不是只有工作。


人情往来


工作中


今年在和外部部门的合作当中,收获了许多的认可,也建立了许多新的人脉关系,这也是人生中比较宝贵的资源。与合作方合作共赢一直都是我做事的指导方法 ,提前思考好双方的目标和边界,剩下的就是努力合作完成目标了。相信他人,他人也会给予你同样的信任。


生活中


生活中的关系会比工作中的关系更加的牢靠,当然工作中的关系发展的好的话,也可以沉淀到生活中,而不是换个工作全没了,今年工作中积累的关系,确实是可以有这方面的转换的,这也是一种收获。


技术成长


我一直都不太赞成技术人转纯管理这个方向,管好人其实可以很简单,丑话在前,用心对待,以诚相待,能做好这三点感觉都不会有太大问题,但技术丢了就很难再捡起来了,切记切记。


今年反尝试不直接带团队,更多的是以技术顾问、专家视角,甚至是一线coding的方式在工作,看似管人但又不管人,所以在技术上成长也是非常快的,少了很多其他的琐事,能够更加投入。


渲染


第一次接触这个词的时候是在2021年,公司专门配了一个渲染团队做这个事情,用前端白话讲,就是能把各种各样的图像画到canvas上,一个好的渲染引擎可以呈现任何想要呈现的物体。


为了学习渲染是做什么的,怎么做,当时把简单的数学知识重新学习了一下,看闫令琪大佬的课,看openGL、webGPU等等相关的知识,过程是比较辛苦的,但收获也是很多的。现在再看一些框架就能够理解为什么代码会这么写了,比如Threejs、deckgl等等,我们自己也用c++实现了一套底层的跨端渲染框架,虽然不全面,但内部够用同时也能提升自身技术水平。


架构


架构能力是随着工作中不断积累起来能力,当然这也需要在工作中不断的打磨和锻炼,如果一直是以完成任务的心态在工作那是很难练出来的。我所推崇的架构能力是以解决业务问题为主,提升产研的效率为辅。所以在工作中不会刻意去做架构,而是围绕着如何解决问题去架构,如何才能控制好不至于过度设计。


举个简单例子,假如我们已经有各种完善的点餐业务,需要做一个邀请大家一起喝奶茶的这么一个功能,从业务上我们先考虑两个核心逻辑:

1、用户点餐之后回到邀请页面,点完的所以人实时能看到其他人下单状态
2、队长确认所有人点完之后,下单付款,所有人的页面切换到送餐状态

如果是快速实现这个功能的话,其实是比较简单的,起一个轮询任务实时问服务端要数据,拿到数据后,根据状态决定下一步显示什么状态的页面


但是随着业务发展,会加入很多奇怪的逻辑,比如要支持修改、删除、踢人等等,这就会导致这个页面逻辑及其的复杂起来,如果不去思考的话,很容易就写出一堆面条代码,最后自己都不愿意去改。


所以针对这个功能  ,我自己会抽象成几部分去思考:

1、store该如何拆解,拆成几个,每个store对应哪个组件
2、store该如何去更新
3、与服务端如何通信,websocket、轮询都可以,看当下实际情况,保证稳定性即可
4、可以写几个js类去做这个事情,每个类的职责是什么

我觉得思考完这几个问题 ,大家对于这个页面该怎么去写应该能有一个很清晰的架构图在脑海中了吧,这里我就不过多展开了 ,有兴趣的话私聊,核心是要说架构其实也可以很简单。


总结


今年就不立flag了,目标能把去年的flag实现好,2023年是疫情结束的一年 ,我认为这是一个好的开始,努力工作是一方面,享受生活我认为也同样重要,今年更需要做好工作和生活的平衡,工作以外能有一些其他的成就。


写给36岁的自己,简单地回顾过去、总结现在、展望未来,希望当37岁的自己回过头来看的时候,能够鄙视现在的自己,写出更好的《写给37岁的自己》。


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

程序员如何成长

做技术是打怪兽不是养宠物,为什么要打怪兽?因为难;为什么难很重要?因为难的事情才能带来成长;为什么要成长?承认吧,因为「如何成长」是当代人,包括你我他在内焦虑的源泉。 过去几个月内我在写一系列主题为「NodeJS实战」的文章,内容来源是过去两年独自开发和运维 ...
继续阅读 »

做技术是打怪兽不是养宠物,为什么要打怪兽?因为难;为什么难很重要?因为难的事情才能带来成长;为什么要成长?承认吧,因为「如何成长」是当代人,包括你我他在内焦虑的源泉。


过去几个月内我在写一系列主题为「NodeJS实战」的文章,内容来源是过去两年独自开发和运维 site2share 网站的经验,本篇文章是对这个系列的一个暂时收尾。


今天我不聊代码,聊些更重要的事情


养宠物


从两件事情开始说起


其一是在此之前,我直接或间接听到了一些来自早已离开项目同学的声音,陈旧的项目技术栈和代码是驱使他们离开的原因之一。


其二是在偶尔浏览掘金网站的内容过程中,有一类让我印象深刻的文章标题,大意诸如「教你用xx + xx + xx 打造一个开源系统」,因为我关注前端领域的关系,标题里的 xx 通常是围绕某个前端框架的时髦技术栈,从点赞和评论数来看它们颇受欢迎。


对于前者我当然理解:一方面脱离当下容易让自己丧失竞争力,不管你愿不愿意承认,简历驱动型开发是所有程序员秘而不宣的默契;另一方面陈旧代码给开发工作带来的挫败感不言而喻,我相信每个程序员面对「屎山代码」都有宁愿把它重写也不愿意修改一行的冲动。第二件事的出现也就顺理成章了:我想毫无负担地学习新技术,还能抛开白天工作中的螺丝钉角色,体验一次愉快的项目实战经历。


我把从零开始做新项目比喻为「养宠物」,因为它能给你带来无与伦比的掌控感。假设一个代码库完全是由你一手搭建的话,那么关于它的一切,例如如何启动、如何部署、它适用于什么场景又无法解决什么样的问题你都心中有数。如果你恰巧又在Thoughtworks 工作,那么Thoughtworks 工作体验更增强了这种掌控感的正当性:对于有坏味道的代码我们允许用工作时间进行重构,对于代码内不懂的知识点,只要提出问题就一定可以得到回答。


也许是我运气不够好,我的工作经验告诉我,「养宠物」般的工作机会是可遇而不可求,在大厂晋升靠造轮子而不是填坑是公理人尽皆知,但造轮子的机会屈指可数。维护遗留系统依然是我们大部分人的工作。这也就是我接下来想说的「打怪兽」,此时我们面对的系统哪怕只上线一年,源代码也可能是满目疮痍。


这里是对于下面不中听的一些话的免责声明,我不是在否定精通


React 没有价值,我也不认为简历驱动开发有什么错,只不过要小心它们让我们的眼界变得狭隘


打怪兽


真正的常态是我接下来想说的「打怪兽」。


之所以把它称为「打怪兽」,不仅仅因为你接触的代码会超出你的预期,你甚至想象不到你会遇到什么样(啼笑皆非亦或是让你无从下手)的困难:


- 这个一千行代码的文件应该从哪开始读起?
- 我如何才能让代码进入这个分支?
- 你发现项目用到的一个框架没有任何文档,在 github 上也找不到源码,原来是上一个离职的老大自己写的
- 项目的打包工具用的既不是webpack 也不是 grunt ,而是 shell 脚本
- 现在需要你优化一个超过包含上百个组件的 React 应用的性能


「怪兽」依然是一个友好的比喻,此时此刻你至少还能够把它具象化,将它和某些电影或者游戏里的角色联系在一起,这意味着它造成破坏的手段和范围是可以预知的。但工作中我们实际遇到的问题无法预测。


你一定想象不到在编写 site2share的过程中,困扰我最久的问题背后的罪魁祸首竟然是 ExpressJS 里的 trust proxy 参数,它导致 API 从来无法访问到部署在 Azure App Service 上的后端服务


为什么要打怪兽


实际的出发点正如上面所说,如果我们工作中绝大部分人、绝大部分时间面临的都是怪兽,那么逃避它就是自欺欺人。


说点不实际的,是因为打怪兽比养宠物更难——为什么「难」重要?因为难的事情才能带来成长。为什么要成长?承认吧,因为「如何成长」是当代人,包括你我他在内焦虑的源泉。


除此之外,我还想强调的是它在锻炼解决问题能力本身


随着工作的深入,越来越发现我的角色从「解决技术问题的人」变成「解决问题的人」:从 Javascript、SQL Server 到代码设计、代码规范,再到团队方向、团队培养。整个过程其实不允许你循序渐进地去适应,可能明天醒来新的问题就摆在你面前,你也永远也没有准备好的那一天。也许可以把团队管理当作一门新技术用学习编程语言的方式去学习,也许求助对的人是当务之急,也许有的问题压根可以不解决。但无论如何,思路不会有如神助般凭空出现在你脑海里,举一反三需要的是练习。问题的多样性在练习的过程中起到非常大的作用,解决新问题会带给你明确的反馈:我的经验可以移植到这个领域,亦或者我的工作模式需要调整。


或者忘掉我上面的长篇大论,通俗点说,打完怪兽以后你就是见过「地狱」的人了,还怕什么。我想起来大二时候为了制作这款软件代码被推翻了无数次,从那之后就再也不怕重构了


app.png


另一方面,养宠物的风险在于,它让我们不自觉地陷入舒适区中。


我曾经有差不多有一年的时间可以自由选择技术栈来开发各式各样前端应用。最流行的框架和搭配起来最时髦的全家桶便成了我的不二之选。在热门冷门尝试了个遍之后最终我难免会对自己产生怀疑:**我似乎永远都在被输入,我永远都在给某个工具打工,如果今天哪个框架告诉我它是业内明日之星那我就要去学它,因为 fear of missing out 是每个技术人的通病。**我似乎能做的也只有如此了,但这就真的足够了吗?


工具正在变得自动化,并且「帮助」我们专注于业务开发这件事带有迷惑性。这里的陷阱在于他能替你做很多事,会让你以为你具备同样的能力的错觉。例如虽然 Parcel 可以无须任何一行配置就把脚本打包得漂漂亮亮的,但你可能对背后的缓存策略一无所知。当每个人都在简历上强调「精通 xx 框架」的今天,我们应该问自己除了框架我还有没有更有力的竞争力?


这类陷阱还有另一种变形是,在团队内你只做业务开发。身处大型开发组中会让你以为你有独立驾驭一个相同体量项目的能力,但实际遇到的问题会非常受限,因为功能性需求和底层设计已经交给你们团队的 Tech Lead 甚至是团队前成员去做了。(公允地说这不是完全负面,而是一件需要把握平衡的事情。虽然这会给团队成员的成长带来不利,但另一方面却可以让项目风险变得可控)


「打怪兽」也是在打破你的乌托邦


打怪兽的另一层含义是经历实战


「教你用 xx 打造 xx」这类系列教程的前置条件太美好了:你有无限的业余时间投入其中,你就是你自己的产品经理。但实际工作中我们永远是戴着镣铐跳舞。例如糟糕代码不一定是个人能力的结果,考虑到当时的交付压力,团队状态和历史包袱,换做你不一定能做得更好。所以大部分技术决策其实是在恶劣环境下做出的,然而如何学习在不同环境中作出恰当的反应,我不认为这是脱离实践可以达成的。


另一个问题是它缺少对方案的闭环验证:我不确定有多少此类项目投入到真实的商业运营中,如果没有,很遗憾它的代码就不一定是有效的。例如它设计有异常捕获功能,异常捕获的目的之一是帮助我们在实际运营过程中排查问题,那当异常发生时它可以提供什么样的信息帮助我们定位到错误代码?通常在捕获异常之后紧接着要把信息作为日志输出,有相当一部分公司其实购买的第三方日志系统,那集成难度如何?如果只有零星的用户上报了此类问题,我们可否在实际生产环境下,在每秒上千条日志增速的日志海洋里甄别到他们?


退一步说,即使方案完美无缺,我们还需要关注它的成本如何。再一次强调,实际工作中人力、时间都是有限的,假使我们能做到满分条件也不会允许。当你把方案拍到老板面前,但是他告诉你预算只有三成时,选择留下哪三分之一的功能,或者说如何用三成的预算做出来一个及格的功能比纯粹的编码更棘手。老板更多关心的是风险,说实话「时髦」技术表达的并不一定都是褒义,它意味着技术的关注度仍在持续提升中,意味着它还可能没有被大规模地应用,也意味着我们其实有更成熟的方案可供选择。决策者都厌恶风险,因此在推广新方案时风险可控也是因素之一。除此之外代码的学习曲线如何?代码库毕竟在依赖团队维护,你应当考虑到团队下限对于新技术的接受程度。



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

安卓埋点策略+Retrofit上传埋点数据

安卓埋点 在企业级安卓项目中,埋点是一项重要的技术,用于收集用户行为数据以进行分析和改进产品。以下是一个常见的安卓企业级项目开发中使用的埋点方案: 定义埋点事件:首先,确定需要埋点的关键事件,如页面访问、按钮点击、数据提交等。为每个事件定义唯一的标识符或名...
继续阅读 »

安卓埋点


在企业级安卓项目中,埋点是一项重要的技术,用于收集用户行为数据以进行分析和改进产品。以下是一个常见的安卓企业级项目开发中使用的埋点方案:




  1. 定义埋点事件:首先,确定需要埋点的关键事件,如页面访问、按钮点击、数据提交等。为每个事件定义唯一的标识符或名称。




  2. 埋点代码插入:在关键事件的代码位置插入埋点代码,以便在事件发生时触发埋点记录。可以通过在代码中手动插入埋点代码或使用 AOP(面向切面编程)等技术自动插入埋点代码。




  3. 数据收集和存储:在埋点代码中,收集相关的事件数据,如事件类型、时间戳、页面名称、按钮名称等。将这些数据存储到本地数据库或发送到服务器进行存储。




  4. 数据上传和分析:定期将本地存储的埋点数据上传到服务器端进行分析。可以使用网络请求库发送数据到服务器,并在服务器端使用数据分析工具进行处理和分析。




  5. 数据展示和可视化:通过数据分析工具,将埋点数据进行可视化展示,生成报表、图表等形式的数据分析结果,以便开发团队或业务团队进行数据分析和决策。




  6. 隐私和合规性:在进行埋点时,要确保遵守隐私保护和数据合规性的相关法规和政策。确保用户数据的安全和保密,并进行必要的用户授权和通知。




  7. 埋点策略优化:根据实际业务需求和数据分析结果,优化埋点策略,增加或调整关键事件的埋点,提高数据的准确性和有用性。




需要注意的是,具体的埋点方案可能因项目需求、技术架构和团队实际情况而有所不同。因此,在实施埋点方案时,应根据项目的具体情况进行定制化开发,并考虑到性能、稳定性、安全性和用户体验等因素。


埋点数据和上传埋点数据代码示例


定义埋点事件的工具类,包含事件的标识符、名称、属性等信息

public class TrackEventUtils {
public static final String EVENT_PAGE_VIEW = "page_view";
public static final String EVENT_BUTTON_CLICK = "button_click";
// 其他事件定义...

// 获取页面访问事件
public static TrackEvent getPageViewEvent(String pageName) {
TrackEvent event = new TrackEvent(EVENT_PAGE_VIEW);
event.addProperty("page_name", pageName);
// 其他属性...
return event;
}

// 获取按钮点击事件
public static TrackEvent getButtonClickEvent(String buttonName) {
TrackEvent event = new TrackEvent(EVENT_BUTTON_CLICK);
event.addProperty("button_name", buttonName);
// 其他属性...
return event;
}

// 其他事件获取方法...
}


定义埋点事件的实体类,包含事件类型、属性等信息

public class TrackEvent {
private String eventType;
private Map<String, Object> properties;

public TrackEvent(String eventType) {
this.eventType = eventType;
this.properties = new HashMap<>();
}

public String getEventType() {
return eventType;
}

public void addProperty(String key, Object value) {
properties.put(key, value);
}

public Map<String, Object> getProperties() {
return properties;
}
}


使用Retrofit框架上传埋点数据到对应路径


1.添加 Retrofit 依赖到项目的 build.gradle 文件中:

implementation 'com.squareup.retrofit2:retrofit:2.x.x'
implementation 'com.squareup.retrofit2:converter-gson:2.x.x' // 如果要使用 Gson 解析器


2.创建 Retrofit 实例并定义 API 接口:

public interface TrackApiService {
@POST("/track")
Call<Void> sendTrackEvent(@Body TrackEvent event);
}


3.修改 TrackManager 类,使用 Retrofit 发送网络请求:

public class TrackManager {
private static final String API_ENDPOINT = "https://your-api-endpoint.com";
private static TrackManager instance;
private Context context;
private TrackApiService apiService;

private TrackManager(Context context) {
this.context = context.getApplicationContext();

// 创建 Retrofit 实例
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(API_ENDPOINT)
.addConverterFactory(GsonConverterFactory.create()) // 使用 Gson 解析器
.build();

// 创建 API Service
apiService = retrofit.create(TrackApiService.class);
}

public static synchronized TrackManager getInstance(Context context) {
if (instance == null) {
instance = new TrackManager(context);
}
return instance;
}

public void trackEvent(TrackEvent event) {
// 发送网络请求
Call<Void> call = apiService.sendTrackEvent(event);
call.enqueue(new Callback<Void>() {
@Override
public void onResponse(Call<Void> call, Response<Void> response) {
// 处理服务器响应...
}

@Override
public void onFailure(Call<Void> call, Throwable t) {
// 处理请求失败...
}
});
}
}


4.解释下上面的代码中有关Retrofit中的注解和上面我们定义的接口TrackApiService
在 Retrofit 中,TrackApiService 是一个接口,用于定义网络请求的方法。@POST("/track") 是一个注解,表示发送 POST 请求到指定的路径 "/track"。


@Body TrackEvent event 是另一个注解,用于指定请求体的内容。它告诉 Retrofit 将 TrackEvent 对象作为请求体发送给服务器。


具体解释如下:




  • @POST("/track"):表示将使用 POST 方法发送请求到路径 "/track"。这个路径是你的 API 后端定义的接收埋点事件的路径。




  • Call<Void>:表示 Retrofit 将返回一个 Call 对象,用于异步执行网络请求并处理响应。Void 表示响应的主体内容为空。




  • sendTrackEvent(@Body TrackEvent event):这是一个方法定义,用于发送埋点事件。@Body 注解表示将 TrackEvent 对象作为请求体发送。TrackEvent 是你定义的类,包含了发送给服务器的埋点事件数据。




综合起来,TrackApiService 接口中的 sendTrackEvent 方法定义了一个发送埋点事件的请求,通过 POST 方法发送到指定路径,并将 TrackEvent 对象作为请求体发送给服务器。


你可以根据实际需求修改这个接口,添加其他请求方法和参数,以适应你的埋点需求。




在 Retrofit 中,@Body 注解用于将对象作为请求体发送给服务器。这意味着你可以将任何 Java 类的实例作为请求体发送出去,不限于特定的类或数据类型。


当你使用 @Body 注解时,Retrofit 将会自动将指定的对象序列化为请求体的格式,例如 JSON 或者其他格式。然后,它将使用适当的请求头信息将请求发送到服务器。


因此,你可以创建自己的 Java 类,用于表示需要发送的数据,并将其作为请求体发送给服务器。这样,你可以根据实际需求定义和发送不同类型的数据。


请确保在使用 @Body 注解时,服务器能够正确地解析和处理请求体的格式。通常,你需要在服务器端进行相应的处理和解析,以确保能够正确地接收和处理你发送的 Java 对象。


注:,Retrofit 会动态地创建接口的实现类,你无需手动编写实现类。当你使用 Retrofit 创建接口的实例时,它会在运行时生成一个代理类来处理实际的网络请求。因此,你不需要手动实现 TrackApiService 接口中的方法。


使用异步或者同步请求


使用 enqueue 方法是一种常见的异步执行网络请求的方式,它会在后台线程执行网络请求,并在请求完成后回调相应的方法。


Retrofit 支持同步和异步的网络请求方式。如果你希望使用同步请求,可以使用 execute 方法来执行请求,但需要注意的是,在 Android 主线程上执行网络请求会导致阻塞,可能会引起 ANR(Application Not Responding)错误,因此建议在后台线程中执行同步请求。


关于接口是异步还是同步的,一般情况下是由接口的定义和服务端的实现决定的。通常,网络请求都会以异步方式执行,以避免阻塞主线程。在 Retrofit 中,默认情况下,接口的方法会被当作异步请求进行处理,需要使用 enqueue 方法来执行异步请求。


如果你想要执行同步请求,可以在 Retrofit 创建时设置合适的执行器(Executor),以控制请求的执行方式。例如,可以使用 OkHttp 客户端来创建 Retrofit 实例,并设置自定义的执行器来执行同步请求。

// 创建 OkHttpClient 实例
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.build();

// 创建 Retrofit 实例,并指定 OkHttp 客户端
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.example.com")
.addConverterFactory(GsonConverterFactory.create())
.client(okHttpClient) // 设置自定义的 OkHttpClient
.build();

// 创建 TrackApiService 实例
TrackApiService trackApiService = retrofit.create(TrackApiService.class);

// 创建同步请求执行器
ExecutorService executor = Executors.newSingleThreadExecutor();

// 执行同步请求
try {
Response<Void> response = executor.submit(() -> trackApiService.sendTrackEvent(event)).get();
// 请求成功的处理逻辑
} catch (ExecutionException | InterruptedException e) {
// 请求失败的处理逻辑
}

// 关闭执行器
executor.shutdown();


在这个示例中,我们使用 OkHttp 客户端创建了一个自定义的 OkHttpClient 实例,并将其传递给 Retrofit 的构建器。然后,我们创建了一个 ExecutorService 实例,并使用 submit 方法执行网络请求。通过调用 get 方法获取 Response 对象,我们可以同步地获取请求的结果。


需要注意的是,同步请求仍然需要在合适的线程中执行,以避免阻塞主线程。在这个示例中,我们使用了单线程的执行器来执行同步请求,并在请求完成后关闭执行器。


综上所述,Retrofit 提供了异步和同步两种方式来执行网络请求,具体使用哪种方式取决于你的需求和服务器端的实现。一般来说,推荐使用异步请求以避免阻塞主线程,除非你确切地知道需要执行同步请求,并且在合适的线程上执行它们。
如果使用的是同步请求,即使使用了execute方法,也要手动开启子线程来调用execute方法,若是异步请求,则使用Retrofit的enqueue方法即可,无需自己手动开启子线程。


服务器如何决定接口是异步请求或是同步请求


1.在服务端,决定接口是同步请求还是异步请求是由服务端的实现逻辑来决定的。
通常情况下,服务端会为每个接口定义好其执行方式,包括是同步还是异步。这通常是通过服务端框架或编程语言提供的特定机制来实现的。
例如,在某些服务器框架中,可以使用异步处理机制(如基于回调的异步编程、Future/Promise、协程等)来处理异步请求。而对于同步请求,则可能直接在请求处理方法中执行阻塞操作。
因此,具体接口是同步还是异步请求,你需要参考服务端接口文档或与服务端开发人员进行沟通,了解其设计和实现细节。根据服务端的要求,你可以相应地选择使用 Retrofit 的 enqueue 方法或 execute 方法来发送请求。


2.在服务端的代码中,决定接口是同步还是异步的方式取决于所使用的服务器框架和编程语言。以下是一些常见的示例代码,展示了如何在不同的环境中定义同步和异步接口:


a.Node.js(使用 Express 框架):

// 异步接口
app.get('/async', (req, res) => {
someAsyncOperation((data) => {
res.send(data);
});
});

// 同步接口
app.get('/sync', (req, res) => {
const result = someSyncOperation();
res.send(result);
});

b.Java(使用 Spring 框架):

// 异步接口
@GetMapping("/async")
public CompletableFuture<String> asyncEndpoint() {
return CompletableFuture.supplyAsync(() -> {
// 异步操作
return "Async response";
});
}

// 同步接口
@GetMapping("/sync")
public String syncEndpoint() {
// 同步操作
return "Sync response";
}

这些示例只是简单的展示了如何在不同环境中定义同步和异步接口。实际上,具体的实现方式取决于所使用的服务器框架和编程语言的特性和机制。因此,你需要根据你所使用的具体服务器框架和编程语言的文档,了解如何定义和处理同步和异步接口。


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

Android自定义一个车牌字母选择键盘

今天的内容大致如下: 1、最终实现效果及简单分析 2、设置属性,制定可扩展效果 3、部分源码剖析 4、开源地址及使用总结 一、最终实现效果及简单分析 以上就是本篇文章最终要实现的效果,和省份简称键盘不同的是,数据源上和边距有所差别之外,其他的实现方式均是一样...
继续阅读 »

今天的内容大致如下:


1、最终实现效果及简单分析


2、设置属性,制定可扩展效果


3、部分源码剖析


4、开源地址及使用总结


一、最终实现效果及简单分析



以上就是本篇文章最终要实现的效果,和省份简称键盘不同的是,数据源上和边距有所差别之外,其他的实现方式均是一样的,采用外部垂直LinearLayout,内部多个横向的LinearLayout的搭配方式。


需要注意的是,英文和数字键盘,默认状态下,顶部的数字是禁止的,也就是输入完地区代码之后,数字的禁止状态才会释放;由于距离左右的边距不同,其在数据源的判断上也会有不同,这个也是需要注意的。


二、设置属性,制定可扩展效果


其相关属性和上篇的省份键盘基本上没有太大的出入,主要就是动态化设置,设置一些,文字的背景,大小,颜色以及格子之间的编辑等,大概罗列了以下属性:

属性类型概述
ek_backgroundcolor整体的背景颜色
ek_rect_spacingdimension格子的边距
ek_rect_heightdimension格子的高度
ek_rect_margin_topdimension格子的距离上边
ek_margin_left_rightdimension左右距离
ek_margin_topdimension上边距离
ek_margin_bottomdimension下边距离
ek_rect_backgroundreference格子的背景
ek_rect_select_backgroundreference格子选择后的背景
ek_rect_text_sizedimension格子的文字大小
ek_rect_text_colorcolor格子的文字颜色
ek_rect_select_text_colorcolor格子的文字选中颜色
ek_is_show_completeboolean是否显示完成按钮
ek_complete_text_sizedimension完成按钮文字大小
ek_complete_text_colorcolor完成按钮文字颜色
ek_complete_textstring完成按钮文字内容
ek_complete_margin_topdimension完成按钮距离上边
ek_complete_margin_bottomdimension完成按钮距离下边
ek_complete_margin_rightdimension完成按钮距离右边
ek_other_lines_margindimension其他行边距
ek_is_num_prohibitboolean数字是否禁止
ek_text_prohibit_colorcolor数字禁止颜色
ek_text_click_effectboolean是否触发点击效果,true点击后背景消失,false不消失



设置回调函数



























方法概述
keyboardContent获取点击的省份简称简称信息
keyboardDelete删除省份简称简称信息
keyboardComplete键盘点击完成
openProhibit打开禁止(使领学港澳),使其可以点击

三、部分源码剖析


这里只贴出部分的关键性代码,整体的代码,大家滑到底部查看源码地址即可。


定义字母和数字数组

   private val mEnglishList = arrayListOf(
"1", "2", "3", "4", "5", "6", "7", "8", "9", "0",
"Q", "W", "E", "R", "T", "Y", "U", "O", "P",
"A", "S", "D", "F", "G", "H", "J", "K", "L",
"Z", "X", "C", "V", "B", "N", "M"
)

定义遍历数字和字母


由于在数据源上使用的是同一个,那么需要做截取分别进行遍历,便于控制左右的边距和本身的格子大小。

 //遍历数字
eachData(mEnglishList.subList(0, 10), mLength, true)
//遍历字母
eachData(mEnglishList.subList(10, mEnglishList.size), mLength - 1, false)
//追加最后一个删除按钮View,动态计算宽度
addEndView(mLineLayout)

遍历数据


遍历数据的逻辑和上篇保持一致,当和定义的长度取模为0时,就需要换行,换行就是重新创建一个水平的LinearLayout,添加至垂直的LinearLayout之中,需要做判断的是,左右的边距。

/**
* AUTHOR:AbnerMing
* INTRODUCE:遍历数据
*/
private fun eachData(
list: List,
len: Int,
isNumber: Boolean = false
) {
list.forEachIndexed { index, s ->
if (index % len == 0) {
//重新创建,并添加View
mLineLayout = createLinearLayout()
mLineLayout?.weightSum = len.toFloat()
addView(mLineLayout)
val params = mLineLayout?.layoutParams as LayoutParams
params.apply {
topMargin = mRectMarginTop.toInt()
height = mRectHeight.toInt()
if (isNumber) {
//是数字
leftMargin = mMarginLeftRight.toInt()
rightMargin = mMarginLeftRight.toInt() - mSpacing.toInt()
} else {
//是字母
leftMargin = mOtherLinesMargin.toInt()
rightMargin = mOtherLinesMargin.toInt() - mSpacing.toInt()
}
mLineLayout?.layoutParams = this
}
}

//创建文字视图
val textView = TextView(context).apply {
text = s
//设置文字的属性
textSize = px2sp(mRectTextSize)
//禁止
if (isNumber) {
//是数字
if (mNumProhibit) {
setTextColor(mRectTextColor)
} else {
setTextColor(mNumProhibitColor)
}
} else {
setTextColor(mRectTextColor)
}
setBackgroundResource(mRectBackGround)
gravity = Gravity.CENTER
setOnClickListener {
//每个格子的点击事件
if (isNumber && !mNumProhibit) {
//如果是数字,根据规则暂时不触发点击
return@setOnClickListener
}
changeTextViewState(this)
}
}
//是数字
if (isNumber) {
mTempTextViewList.add(textView)
}
addRectView(textView, mLineLayout, 1f)
}
}

添加视图


设置每个格子的宽高和权重。

 /**
* AUTHOR:AbnerMing
* INTRODUCE:追加视图
*/
private fun addRectView(view: View, layout: LinearLayout?, w: Float) {
layout?.addView(view)
val textParams = view.layoutParams as LayoutParams
textParams.apply {
weight = w
width = 0
height = LayoutParams.MATCH_PARENT
//每行的最后一个
rightMargin = mSpacing.toInt()
view.layoutParams = this
}

}

至于最后一个删除按钮,也需要动态的计算其本身的宽高,基本上和上篇一致,就不过多赘述了。


四、开源地址及使用总结


开源地址:github.com/AbnerMing88…


关于如何使用,有两种方式,一种是下载源码,直接把源码复制出来,二是可以使用以下的远程Maven依赖方式。


Maven具体调用


1、在你的根项目下的build.gradle文件下,引入maven。

allprojects {
repositories {
maven { url "https://gitee.com/AbnerAndroid/almighty/raw/master" }
}
}

2、在你需要使用的Module中build.gradle文件下,引入依赖。

dependencies {
implementation 'com.vip:board:1.0.0'
}


代码使用


android:layout_width="match_parent"
android:layout_height="wrap_content" />

总结


属性配置了有很多,可以实现多种自定义的相关效果,大家可以查找第二项中的属性介绍,进行自定义配置,还是那句话,本身的实现方式有很多种,本篇只是其中的一个简单的案例,仅供大家作为一个参考。


自定义英文和数字键盘,大家有没有发现了少了一个字母,为什么会没有这个字母呢?你知道原因吗?


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

安卓滴滴路由框架DRouter原理浅析

前言 最近的一个新项目使用了Clean Architecture+模块化+MVVM架构,将首页每个tab对应的功能都放到单独的模块且不相互依赖,这时就有了模块间页面跳转的问题,经过一番研究选择了滴滴的DRouter,因为其出色的性能、灵活的组件拆分,更重要的是...
继续阅读 »

前言


最近的一个新项目使用了Clean Architecture+模块化+MVVM架构,将首页每个tab对应的功能都放到单独的模块且不相互依赖,这时就有了模块间页面跳转的问题,经过一番研究选择了滴滴的DRouter,因为其出色的性能、灵活的组件拆分,更重要的是生成路由表时支持插件增量编译、多线程扫描,运行时异步加载路由表,支持回调式ActivityResult,比ARouter好太多。本着用一个新框架,只会用还不够的原则,我决定去了解一下框架的原理,并给自己制定了以下几个问题:


1、框架的设计分层是什么样的?

2、它是如何生成路由表的?

3、它是如何加载路由表的?

4、相比于ARouter如何提高了性能?


阅读官方文档


相比于直接一头扎进源码,先阅读官方的文档总是没错的,官方给了一篇介绍的文章,写得非常好,基本回答了我以上的所有问题。


滴滴开源DRouter:一款高效的Android路由框架


首先在介绍DRouter的亮点部分得到了问题2、3、4的答案。



路由表在编译期通过插件动态生成。插件会启动多线程同时异步处理所有的组件;增量扫描功能可以帮助开发者在第二次编译时,只对修改过的代码进行处理,极大地缩短路由表生成的时间。



在编译器使用gradle插件配合transform扫描所有的类,生成路由表,并且支持增量扫描,回答了问题2。



另外框架初始化的时候启动子线程去加载路由表,不阻塞主线程的执行,尽其所能提高效率。



回答了问题3。



加载路由表、实例化路由、以及跨进程命令到达服务端后的分发这些常规应该使用反射的场景,使用预占位或动态生成代码来替换成java的new创建和显式方式执行,最大限度的去避免反射执行,提高性能。



回答了问题4,通过减少使用反射提升了性能。


在原理和架构章节处给了一张架构的设计图:


架构设计



整体架构分三层,自下而上是数据流层、组件层、开放接口层。


数据流层是DRouter最重要的核心模块,这里承载着插件生成的路由表、路由元素、动态注册、以及跨进程功能相关的序列化数据流。所有的路由流转都会从这里取得对应的数据,进而流向正确的目标。



RouterPlugin和MetaLoader负责生成路由表,路由元素指的是RouterMeta,存放scheme/host/path等信息。



组件层,核心的路由分发、拦截器、生命周期、异步暂存和监控、ServiceLoader、多维过滤、Fragment路由,以及跨进程命令打包等。



开放接口层则是使用时接触到的一些类,API设计得也很简单易用,DRouter类和Request类分别只有75和121行代码。


问题1得到解答,到此处也对整个框架有了一个整体的认识。


阅读源码


1.初始化流程


调用DRouter.init(app)后的时序图如下:


截屏2023-06-02 16.54.46.png


默认是在子线程实现路由表加载,不影响主线程。

    public static void checkAndLoad(final String app, boolean async) {
if (!loadRecord.contains(app)) {
// 双重校验锁
synchronized (RouterStore.class) {
if (!loadRecord.contains(app)) {
loadRecord.add(app);
if (!async) {
Log.d(RouterLogger.CORE_TAG, "DRouter start load router table sync");
load(app);
} else {
new Thread("drouter-table-thread") {
@Override
public void run() {
Log.d(RouterLogger.CORE_TAG, "DRouter start load router table in drouter-table-thread");
load(app);
}
}.start();
}
}
}
}
}

最终走到了RouterLoader的load方法来加载路由表到一个map中,仔细看它的引入路径是com.didi.drouter.loader.host.RouterLoader,是不存在于源码中的,因为它是编译的时候生成的,位置位于app/build/intermediates/transforms/DRouter/dev/debug/../com/didi/drouter/loader/host/RouterLoader。

public class RouterLoader extends MetaLoader {
@Override
public void load(Map var1) {
var1.put("@@$$/browse/BrowseActivity", RouterMeta.build(RouterMeta.ACTIVITY).assembleRouter("", "", "/browse/BrowseActivity", "com.example.demo.browse.BrowseActivity", (IRouterProxy)null, (Class[])null, (String[])null, 0, 0, false));
}

public RouterLoader() {
}
}

public abstract class MetaLoader {

public abstract void load(Map<?, ?> data);

// for regex router
protected void put(String uri, RouterMeta meta, Map<String, Map<String, RouterMeta>> data) {
Map<String, RouterMeta> map = data.get(RouterStore.REGEX_ROUTER);
if (map == null) {
map = new ConcurrentHashMap<>();
data.put(RouterStore.REGEX_ROUTER, map);
}
map.put(uri, meta);
}

// for service
protected void put(Class<?> clz, RouterMeta meta, Map<Class<?>, Set<RouterMeta>> data) {
Set<RouterMeta> set = data.get(clz);
if (set == null) {
set = Collections.newSetFromMap(new ConcurrentHashMap<RouterMeta, Boolean>());
data.put(clz, set);
}
set.add(meta);
}
}

不难猜出其是在编译期加了一个transform,生成RouterLoader类时加入了load方法的具体实现,具体来说是javaassit API+Gradle Transform,所以去看看drouter-plugin在编译期做了什么。


2.编译期transform


直接看时序图。


截屏2023-06-02 17.56.36.png


创建了一个RouterPlugin,并且注册了一个Gradle Transform。

class RouterPlugin implements Plugin<Project> {

@Override
void apply(Project project) {
...
project.android.registerTransform(new TransformProxy(project))
}
}

class TransformProxy extends Transform {
@Override
void transform(TransformInvocation invocation) throws TransformException, InterruptedException, IOException {
String pluginVersion = ProxyUtil.getPluginVersion(invocation)
if (pluginVersion != null) {
...

if (pluginJar.exists()) {
URLClassLoader newLoader = new URLClassLoader([pluginJar.toURI().toURL()] as URL[], getClass().classLoader)
Class<?> transformClass = newLoader.loadClass("com.didi.drouter.plugin.RouterTransform")
ClassLoader threadLoader = Thread.currentThread().getContextClassLoader()
// 1.设置URLClassLoader
Thread.currentThread().setContextClassLoader(newLoader)
Constructor constructor = transformClass.getConstructor(Project.class)
// 2.反射创建一个RouterTransform
Transform transform = (Transform) constructor.newInstance(project)
transform.transform(invocation)
Thread.currentThread().setContextClassLoader(threadLoader)
return
} else {
ProxyUtil.Logger.e("Error: there is no drouter-plugin jar")
}
}
}
}

注释2处反射创建一个com.didi.drouter.plugin.RouterTransform对象,并执行其transform方法,此处真正处理transform逻辑,它的位置位于drouter-plugin模块。

class RouterTransform extends Transform {
@Override
void transform(TransformInvocation invocation) throws TransformException, InterruptedException, IOException {
...
// 1.创建一个DRouterTable目录
File dest = invocation.outputProvider.getContentLocation("DRouterTable", TransformManager.CONTENT_CLASS,
ImmutableSet.of(QualifiedContent.Scope.PROJECT), Format.DIRECTORY)
// 2.执行RouterTask
(new RouterTask(project, compilePath, cachePathSet, useCache, dest, tmpDir, setting, isWindow)).run()
FileUtils.writeLines(cacheFile, cachePathSet)
Logger.v("Link: https://github.com/didi/DRouter")
Logger.v("DRouterTask done, time used: " + (System.currentTimeMillis() - timeStart) / 1000f + "s")
}
}

注释2处new了一个RouterTask对象,并执行其run方法,之后的log输出就是平时编译能看到的信息,表示transform的耗时。

public class RouterTask {
void run() {
StoreUtil.clear();
JarUtils.printVersion(project, compileClassPath);
pool = new ClassPool();
// 1.创建ClassClassify
classClassify = new ClassClassify(pool, setting);
startExecute();
}

private void startExecute() {
try {
...
// 2.执行ClassClassify的generatorRouter
classClassify.generatorRouter(routerDir);
Logger.d("generator router table used: " + (System.currentTimeMillis() - timeStart) + "ms");
Logger.v("scan class size: " + count.get() + " | router class size: " + cachePathSet.size());
} catch (Exception e) {
JarUtils.check(e);
throw new GradleException("Could not generate d_router table\n" + e.getMessage(), e);
} finally {
executor.shutdown();
FileUtils.deleteQuietly(wTmpDir);
}
}
}

重点在于ClassClassify这个类,其generatorRouter方法便是最终处理生成路由表的逻辑。

public class ClassClassify {
private List<AbsRouterCollect> classifies = new ArrayList<>();

public ClassClassify(ClassPool pool, RouterSetting.Parse setting) {
classifies.add(new RouterCollect(pool, setting));
classifies.add(new ServiceCollect(pool, setting));
classifies.add(new InterceptorCollect(pool, setting));
}

public void generatorRouter(File routerDir) throws Exception {
for (int i = 0; i < classifies.size(); i++) {
AbsRouterCollect cf = classifies.get(i);
cf.generate(routerDir);
}
}
}

构造函数处添加了RouterCollect/ServiceCollect/InterceptorCollect,最终执行的是他们的generate方法,分别处理路由表、service、拦截器,我们只看路由表的。

class RouterCollect extends AbsRouterCollect {
@Override
public void generate(File routerDir) throws Exception {
// 1.创建RouterLoader类
CtClass ctClass = pool.makeClass(getPackageName() + ".RouterLoader");
CtClass superClass = pool.get("com.didi.drouter.store.MetaLoader");
ctClass.setSuperclass(superClass);

StringBuilder builder = new StringBuilder();
builder.append("public void load(java.util.Map data) {\n");
for (CtClass routerCc : routerClass.values()) {
try {
// 处理注解、class类型等逻辑
...
StringBuilder metaBuilder = new StringBuilder();
metaBuilder.append("com.didi.drouter.store.RouterMeta.build(");
metaBuilder.append(type);
metaBuilder.append(").assembleRouter(");
metaBuilder.append("\"").append(schemeValue).append("\"");
metaBuilder.append(",");
metaBuilder.append("\"").append(hostValue).append("\"");
metaBuilder.append(",");
metaBuilder.append("\"").append(pathValue).append("\"");
metaBuilder.append(",");
if ("com.didi.drouter.store.RouterMeta.ACTIVITY".equals(type)) {
if (!setting.isUseActivityRouterClass()) {
metaBuilder.append("\"").append(routerCc.getName()).append("\"");
} else {
metaBuilder.append(routerCc.getName()).append(".class");
}
} else {
metaBuilder.append(routerCc.getName()).append(".class");
}
metaBuilder.append(", ");
...
metaBuilder.append(proxyCc != null ? "new " + proxyCc.getName() + "()" : "null");
metaBuilder.append(", ");
metaBuilder.append(interceptorClass != null ? interceptorClass.toString() : "null");
metaBuilder.append(", ");
metaBuilder.append(interceptorName != null ? interceptorName.toString() : "null");
metaBuilder.append(", ");
metaBuilder.append(thread);
metaBuilder.append(", ");
metaBuilder.append(priority);
metaBuilder.append(", ");
metaBuilder.append(hold);
metaBuilder.append(")");
...
if (isAnyRegex) {
// 2. 插入路由表
items.add(" put(\"" + uri + "\", " + metaBuilder + ", data); \n");
//builder.append(" put(\"").append(uri).append("\", ").append(metaBuilder).append(", data); \n");
} else {
items.add(" data.put(\"" + uri + "\", " + metaBuilder + "); \n");
//builder.append(" data.put(\"").append(uri).append("\", ").append(metaBuilder).append("); \n");
}
} catch (Exception e) {
e.printStackTrace();
}
Collections.sort(items);
for (String item : items) {
builder.append(item);
}
builder.append("}");

Logger.d("\nclass RouterLoader" + "\n" + builder.toString());
// 3.生成代码
generatorClass(routerDir, ctClass, builder.toString());
}
}
}

此处逻辑比较多,但总体是清晰的,处理完注解和类型的判断,获取路由的信息,构造将要插入的代码,最后统一在父类AbsRouterCollect的generatorClass处理load方法的生成,此时编译器的工作就完成了。


ARouter也提供了arouter-register插件,同是在编译期生成路由表,不同的是在生成代码时,ARouter使用的是ASM,DRouter使用Javassist,查了一下资料,ASM性能比Javassist更好,但更难上手,需要懂字节码知识,Javassist在复杂的字节码级操作上提供了更高级别的抽象层,因此实现起来更容易、更快,只需要懂很少的字节码知识,它使用反射机制。


3.运行期加载路由表


重新贴一下加载路由表的load方法。

public class RouterLoader extends MetaLoader {
@Override
public void load(Map var1) {
var1.put("@@$$/browse/BrowseActivity", RouterMeta.build(RouterMeta.ACTIVITY).assembleRouter("", "", "/browse/BrowseActivity", "com.example.demo.browse.BrowseActivity", (IRouterProxy)null, (Class[])null, (String[])null, 0, 0, false));
}

public RouterLoader() {
}
}

看下RouteMeta的build方法。

public static RouterMeta build(int routerType) {
return new RouterMeta(routerType);
}

可见是直接new的一个路由类,这与ARouter直接通过反射创建路由类不同,性能更好。

private static void register(String className) {
if (!TextUtils.isEmpty(className)) {
try {
// 1.反射创建路由类
Class<?> clazz = Class.forName(className);
Object obj = clazz.getConstructor().newInstance();
if (obj instanceof IRouteRoot) {
registerRouteRoot((IRouteRoot) obj);
} else if (obj instanceof IProviderGroup) {
registerProvider((IProviderGroup) obj);
} else if (obj instanceof IInterceptorGroup) {
registerInterceptor((IInterceptorGroup) obj);
} else {
logger.info(TAG, "register failed, class name: " + className
+ " should implements one of IRouteRoot/IProviderGroup/IInterceptorGroup.");
}
} catch (Exception e) {
logger.error(TAG,"register class error:" + className, e);
}
}
}

4.总结


本文分析了DRouter路由部分的原理,其在编译器使用Gradle Transform和Javassist生成路由表,运行时new路由类,异步初始化加载路由表,实现了高性能。


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

你以为搞个流水线每天跑,团队就在使用CI/CD实践了?

在实践中,很多团队对于DevOps 流水线没有很透彻的理解,要不就创建一大堆流水线,要不就一个流水线通吃。实际上,流水线的设计和写代码一样,需要基于“业务场景”进行一定的设计编排,特别是很多通过“开源工具”搭建的流水线,更需要如此(商业的一体化平台大部分已经把...
继续阅读 »

在实践中,很多团队对于DevOps 流水线没有很透彻的理解,要不就创建一大堆流水线,要不就一个流水线通吃。实际上,流水线的设计和写代码一样,需要基于“业务场景”进行一定的设计编排,特别是很多通过“开源工具”搭建的流水线,更需要如此(商业的一体化平台大部分已经把设计思想融入自己产品里了)。



  • 流水线的设计与分支策略有关

  • 流水线的设计与研发活动有关


清晰的代码结构,标准的环境配置,原子化的流水线任务编排,再加上团队的协作纪律,和持续优化的动作,才是真正的践行CI/CD实践


流水线设计原则


1. 确定好变量



  • 哪些是构建/部署需要变化的,比如构建参数,代码地址,分支名称,安装版本,部署机器IP等,控制变化的,保证任务的可复制性,不要写很多hardcode进去


2. 流水线变量/命名的规范化



  • 标准化的命名,有助于快速复制;有意义的流水线命名,有助于团队新成员快速了解


3. 一次构建,多次部署



  • 一次构建,多次部署(多套环境配置+多套构建版本标签);杜绝相同代码重复打包

  • 相似技术栈/产品形态具备共性,通过以上原则可以抽取复用脚本,良好的设计有助于后续的可维护性!


4. 步骤标准化/原子化



  • 比如docker build/push, helm build/deploy, Maven构建等动作标准化,避免重复性写各种脚本逻辑

  • 根据业务场景组装,例如. 提测场景,每日构建场景,回归测试场景


image.png
5. 快速失败



  • 尽可能把不稳定的,耗时短的步骤 放在流水线的最前面,如果把一个稳定的步骤放在前面,并且耗时几十分钟,后面的某个步骤挂了,反馈周期就会变长


从零开始设计流水线


流水线分步骤实施, 从 “点” 到 “线” 结合业务需要串起来,适合自己团队协作开发节奏的流水线才是最好的。



  1. 价值流进行建模并创建简单的可工作流程

  2. 将 构建 和 部署 流程自动化

  3. 将 单元测试和 代码分析 自动化

  4. 将 验收测试 自动化

  5. 将 发布 自动化


image.png


流水线的分层


由于产品本身的形态不同,负责研发的团队人员组成不同,代码的版本管理分支策略不同,使用的部署流水线形式也会各不相同,所以基于实际业务场景设计流水线是团队工程实践成熟的重要标志



1. 提交构建流水线(个人级)


适用场景:每名研发工程师都创建了自己专属的流水线(一般对应个人的开发分支),用于个人在未推送代码到团队仓库之前的快速质量反馈。
注意:个人流水线并不会部署到 团队共同拥有的环境中,而是仅覆盖个人开发环节。如图所示,虚线步骤非必选

image.png


2. 集成验收流水线(团队级)


适用场景:每个团队都根据代码仓库(master/release/trunk)分支,创建产品专属的流水线,部署到 团队共同拥有的环境中e.g. dev)。
注意:如图所示,虚线步骤非必选,根据情况可通过 启动参数true/flase 跳过执行,自动化测试仅限于保证基本功能的用例。

image.png


3. 部署测试流水线(团队级)


适用场景:每个团队的测试工程师都需要专门针对提测版本的自动化部署/测试流水线,部署到团队共同拥有的环境中(e.g. test).
注意:如图所示,该条流水线的起点不是代码,而是提测的特定版本安装包;虚线步骤非必选,根据情况可通过 启动参数true/flase 跳过执行 或 裁剪。

image.png


4. 多组件集成流水线


适用场景:如果一个产品由多个组件构建而成,每个组件均有独自的代码仓库,并且每个组件由一个单独的团队负责开发与维护,那么,整个产品 的部署流水线的设计通常如下图所示。 集成部署流水线的集成打包阶段将自动从企业软件包库中获取每个组件最近成功的软件包,对其进行产品集成打包
image.png


5. 单功能流水线


适用场景:适用于和代码变更无关的场景,不存在上面步骤复杂的编排 (也可通过上述流水线的 启动参数进行条件控制,跳过一些步骤)



  • 针对某个环境的漏洞扫描

  • 针对某个已部署环境的自动化测试

  • 定时清理任务

  • ...


6. 全功能(持续交付)流水线


适用场景:需求、代码构建、测试、部署环境内嵌自动化能力,每次提交都触发完整流水线,中间通过人工审批层次卡点,从dev环境,test环境,stage环境一直到 prod环境。 常适用于快速发布的 PASS/SASS服务,对团队各项能力和流程制度要求较高,支持快速发布(策略)和快速回滚(策略)
image.png


流水线运转全景图


团队研发工程师每人每天都会提交一次。因此,流水线每天都会启动多次。当然并不是每次提交的变更都会走到最后的“上传发布” 。 也不是每次提交都会走到UAT 部署,因为开发人员并不是完成一个功能需求后才提交代码,而是只要做完一个开发任务,就可以提交。每个功能可能由 多个开发任务组成,研发工程师需要确保即使提交了功能尚未开发完成的代码,也不会影响已开发完成的那些功能。
制品经过一个个质量卡点,经历各种门禁验证,最终交付给客户 可以工作的软件
pipeline-status.jpg

收起阅读 »

技术管理必备技能之管理好系统性风险

我们在平常工作中经常会听到有人说系统性风险,但系统性风险到底是个啥? 1 系统性风险是什么 1.1 定义 「系统性风险」是一个经济术语,主要指的是一种可能导致整个金融系统或市场瘫痪的风险或概率。它是从系统性风险的整体性出发,而不是单一机构或者单一行业的危机。这...
继续阅读 »

我们在平常工作中经常会听到有人说系统性风险,但系统性风险到底是个啥?


1 系统性风险是什么


1.1 定义


「系统性风险」是一个经济术语,主要指的是一种可能导致整个金融系统或市场瘫痪的风险或概率。它是从系统性风险的整体性出发,而不是单一机构或者单一行业的危机。这通常是由于金融体系中一个重要组成部分的失败,例如一个大银行或一系列银行的破产,这可能引发一种连锁反应,影响整个系统。


当突发性事件导致金融机构或市场失灵时,资金无法在市场中有效输送和配置,从而引起整个市场的崩溃。系统性风险不仅仅是经济价值损失,还会对实体经济造成严重影响,并导致大部分金融体系的信心丧失。


如 2008 年的全球金融危机。在这个危机中,许多大型金融机构由于负债过重和资产质量下降而陷入困境,这引发了对全球金融系统稳定性的广泛担忧。


系统性风险是监管机构、政策制定者和经济学家关注的主要问题,因为如果这种风险实现,可能会导致重大的经济损失和社会动荡。因此,他们会尝试制定和执行各种政策和法规,以减少系统性风险的可能性。


1.2 系统性风险和非系统性风险的差别


系统性风险作为一种具有更大影响面的风险,和非系统性风险有以下几个方面的区别:


1. 影响范围:系统性风险具有广泛的影响范围,不仅仅局限于特定个体或组织,而是可能波及整个系统、市场或行业。非系统性风险则相对较局部化,通常只对特定个体、组织或项目产生影响。


2. 相互关联性:系统性风险与系统中各个组成部分相互关联,其中一个部分的风险可能会传播、扩大或影响其他部分。非系统性风险通常是单个因素或事件的结果,并不涉及系统的相互依赖关系。


3. 复杂性和不确定性:系统性风险往往更加复杂和不确定,因为它们涉及到多个变量、因素和相互作用。非系统性风险可能更加可控和可预测,因为它们通常涉及特定事件或条件。


4. 长期影响:系统性风险可能具有长期影响,并可能导致持续的连锁反应和不良后果。非系统性风险通常具有较短期的影响,并且其影响通常更容易限定和控制。


5. 解决方法:由于系统性风险的复杂性和广泛影响,解决它们通常需要跨部门、跨组织或跨行业的合作和综合性措施。非系统性风险通常可以通过特定个体或组织的行动来解决。


系统性风险与非系统性风险在影响范围、相互关联性、复杂性和不确定性、长期影响以及解决方法等方面存在明显的区别。


2 技术上的系统性风险


类比经济上的系统性风险,对于一家企业的技术负责人来说,技术上的系统性风险也是一个需要重点关注的点。


2.1 定义


在技术上,系统性风险指的是一个技术系统或者一个技术生态系统中,某个关键组件或者某些关键组件出现故障、漏洞、安全问题等,导致整个系统或者生态系统无法正常运行,进而引发连锁反应和影响。


例如,在云计算生态系统中,某个云服务提供商的故障可能会影响到众多企业和用户的业务运营;在物联网领域,某个智能设备的漏洞可能会导致整个物联网网络遭受攻击和瘫痪。因此,在技术领域中,识别和防范系统性风险也是非常重要的。


2.2 系统性风险和非系统性风险的不同


和经济上的系统性风险一样,技术上的系统性风险和非系统性风险也有 5 个不同点:


1. 影响范围和规模:系统性风险通常具有广泛的影响范围和规模,涉及整个技术系统或架构。它可能涉及多个组件、子系统或关键基础设施,甚至可能跨越多个应用程序或服务。非系统性风险更倾向于局部范围,通常仅影响特定的组件、功能或子系统。


2. 相互关联和依赖:系统性风险涉及到技术系统中各个组件和环节之间的相互关联和依赖关系。它们可能因为一个组件或环节的故障或问题而影响其他组件或环节的正常运行。非系统性风险更倾向于独立存在,其影响相对较为局限,不会对其他组件或环节造成波及效应。


3. 复杂性和不确定性:系统性风险通常更加复杂和不确定,因为它们涉及到多个技术组件、系统交互、数据流和相关的外部因素。这使得系统性风险的评估、预测和解决变得更加困难。非系统性风险通常更容易辨识、评估和控制,因为其范围和影响相对较小。


4. 长期影响和连锁反应:系统性风险可能导致长期的影响和连锁反应,其中一个问题可能触发多个级联故障或影响多个关键业务流程。非系统性风险的影响通常更为短期和局限,不会引起大规模的系统级问题。


5. 解决方法和复杂度:由于系统性风险的复杂性和广泛影响,解决它们通常需要跨部门、跨团队的协作,涉及多个技术专长和领域的知识。这可能需要综合性的技术改进、架构调整或系统重构。非系统性风险通常可以通过单个组件或功能的修复或改进来解决,其处理相对较为简单和局部化。


3 系统性风险的传播


在技术系统中,系统性风险通过多种方式传播,包括以下几种:




  • 级联传播:级联传播是指一个组件的故障导致其他相关组件的故障,从而在整个系统中形成一种连锁反应。这种传播方式可能导致整个系统的瘫痪,影响业务的正常运行。例如,在一个分布式计算环境中,如果某个关键任务执行节点发生故障,可能导致其他依赖于该节点的任务无法正常执行,从而引发其他节点的过载或故障。这种风险传播会在整个分布式系统内形成级联效应,可能导致整个系统瘫痪。




  • 传染传播:传染传播是指一个系统的风险通过某种途径传播给其他系统,从而导致多个系统受到相同类型风险的影响。例如,WannaCry 勒索病毒,它通过网络传播,利用 Windows 系统的一个漏洞进行攻击。当某个系统被感染后,病毒会自动搜索其他具有相同漏洞的系统,并尝试感染它们。这种风险传播方式导致了全球范围内大量系统受到勒索病毒的影响。




  • 共同暴露:共同暴露是指多个系统由于共享相同的风险因素,而同时受到该风险因素的影响。例如,多个在线服务都依赖于一个第三方身份验证服务。如果这个第三方身份验证服务出现故障或者安全漏洞,那么所有依赖它的在线服务都将面临安全风险或者无法正常运行,因为它们共同暴露在同一个风险因素下。




  • 放大效应:放大效应是指一个较小的初始风险经过多次传播和叠加,最终导致整个系统面临较大的风险。例如,在社交网络中,一个虚假信息可能经过多次转发和传播,形成恶性舆论,对整个社会产生较大的负面影响。




在技术系统中,了解这些传播方式和机制对于有效管理技术风险至关重要。


4 系统性风险的来源


系统性风险的由来可以追溯到技术系统的复杂性和相互依赖性。当一个技术系统由多个组件、流程和环节组成时,它们之间存在着相互依赖和相互作用。这种相互依赖性使得一个组件或环节的故障或问题可能会影响整个系统的运行和稳定性。


以下是一些常见的系统性风险的来源:




  • 复杂性和交互作用:技术系统的复杂性和各组件之间的交互作用可能导致系统性风险的出现。当系统变得越来越复杂,组件之间的相互依赖性增加,可能出现不可预见的问题和故障。例如,一个庞大的分布式系统可能由多个模块和子系统组成,彼此之间的相互作用可能导致系统范围的故障,如性能下降或数据不一致。




  • 外部环境因素:外部环境因素也是技术系统性风险的重要来源。例如,技术系统可能受到恶劣天气、自然灾害(如山洪地震等导致光纤断了)、供应链中断或恶意攻击等外部因素的影响。这些因素可能导致系统中断、数据丢失、安全漏洞暴露等问题。例如,一家电子商务平台可能受到网络攻击,导致用户信息泄露或交易中断。




  • 人为错误和疏忽:技术系统性风险也可能源自人为错误和疏忽。人员的操作失误、编码错误、配置错误或安全意识薄弱等问题都可能导致系统故障或数据泄露。例如,一个开发人员可能在代码中引入漏洞,导致系统容易受到攻击。




  • 技术演进和更新:技术的演进和系统的更新也可能引入系统性风险。当引入新的技术、框架或库时,可能存在兼容性问题或未知的缺陷。例如,将系统从一个版本升级到另一个版本时,可能出现功能不兼容、新增的安全漏洞或数据不一致的问题等。




  • 依赖供应商和第三方:技术系统通常会依赖外部供应商或第三方服务。这种依赖性可能带来风险。例如,如果一个关键供应商无法按时提供所需的硬件设备,可能导致项目延期或无法正常运作。另外,如果一个 CDN 第三方服务提供商的服务出现故障,可能会影响到技术系统的正常运行。




以上是一些常见的技术系统性风险的来源示例。在技术管理中,了解和识别这些来源是非常重要的,以便采取相应的措施来减轻和管理系统性风险的影响。


5 管理好系统性风险的意义


聊了这么多术语类的东西,看一下对于一个技术管理者来说,管理好系统性风险到底有什么用,有什么收益。这里我们从技术管理和技术团队,以及业务的角度来看。


5.1 技术管理上的意义


从技术管理和技术团队的角度来看,管理好技术上的系统性风险具有以下意义:


1. 保障系统的稳定性和可靠性:系统性风险管理可以帮助确保技术系统的稳定性和可靠性,减少系统故障和服务中断的可能性。这有助于降低业务中断的风险,提高技术系统的可用性和持续性,保障业务的正常运行。


2. 提高技术投资的回报率:有效管理系统性风险可以降低技术投资的风险并提高回报率。通过规避潜在的系统性风险,可以减少因系统故障或不稳定性而造成的额外成本和资源浪费,提高技术投资的效益和投资回报。


3. 增强技术管理者决策能力:系统性风险管理使技术管理者能够更全面地了解和评估技术系统的风险情况。这有助于他们做出明智的决策,选择合适的措施来降低风险,并确定优先级,以使资源和精力能够最大程度地应对最重要的风险。


4. 提高团队效率:通过管理系统性风险,技术管理者可以减少系统故障和问题的发生,从而减少紧急修复和事后处理的工作量。这使团队能够更加专注于战略性的工作,提高工作效率和生产力。


5. 增加业务可信度:有效管理系统性风险可以提高技术系统的可靠性和稳定性,增加业务的可信度。这有助于提高内部和外部利益相关者对技术部门的信任,加强与其他部门的合作和协调,为企业的可持续发展和成长奠定基础。


6. 促进技术创新和发展:管理好系统性风险有助于为技术管理者提供稳定的技术基础,支持技术创新和发展。他们可以更好地专注于推动新技术的应用、优化现有技术架构和流程,为业务增长提供技术支持和竞争优势。


5.2 业务价值上的意义


从业务价值的角度来看,管理好技术上的系统性风险具有以下意义:


1. 提高效率和生产力:通过管理系统性风险,技术系统可以更加稳定和可靠地运行,减少系统故障和问题的发生,从而减少因为系统问题导致的客诉、修复、沟通等成本。这有助于提高业务的效率和生产力,节省时间和资源,并降低运营成本。


2. 支持业务增长和扩展:有效的系统性风险管理可以为业务提供可靠的技术基础,支持业务的增长和扩展。通过降低系统故障和数据泄露的风险,技术管理者可以为业务提供稳定的平台,支持业务的创新、市场拓展和新产品的推出。


3. 支持业务创新和竞争优势:系统性风险管理为技术团队提供稳定的技术基础,支持业务的创新和发展。通过降低系统性风险,技术团队能够更好地专注于业务创新、新产品开发和市场敏捷性,从而获得竞争优势。


4. 提升用户体验和满意度:系统性风险管理有助于提供稳定、安全和高性能的技术系统,提升用户体验和满意度。用户倾向于选择那些能够提供稳定服务、快速响应和数据安全的产品或服务,有效的系统性风险管理可以增强用户对技术产品或服务的信任和满意度。


5. 降低损失和风险:有效的系统性风险管理有助于降低业务面临的潜在损失和风险。通过识别和管理系统中的风险,可以减少数据泄露、安全漏洞和技术故障所带来的损失,并降低法律诉讼和声誉损害的风险。


6. 提升客户信任和忠诚度:通过管理系统性风险,技术管理者可以建立客户信任和忠诚度。稳定、安全和可靠的技术系统能够增强客户对企业的信心,提高客户满意度和保持客户的长期合作关系。


可以看到如果能管理好系统性的风险,对于技术组织,对于技术管理者,对于业务和业务价值来说,都是一件非常好的事情。从生产效率的提升,到业务稳定性,到对成本的减少以及客户成功都是极好的。


那么如何管理系统性风险呢?


6 如何管理系统性风险


6.1 风险模型


风险模型是风险管理的第一步:理解系统中已有的风险,识别、标记并对已知的风险排列优先级,最终形成一张包含了系统所有已知风险的当前状态的表格。这就是我们所说的风险模型。


建立风险模型的过程是识别风险的过程,在这个过程中我们需要识别出系统中已有的风险,并对其进行分析,标记出优先级、梳理当前状态和历史情况。


风险模型构建过程中需要考虑模型的作用范围,是公司级的,团队级的,项目组的,还是服务级的。


对于一个小公司,可以是公司级的,对于大型一些的公司,可以考虑团队或项目级的。


风险模型至少包括以下一些方面:



  • 严重性/可能性:高中低,先评估严重性,再评估可能性

  • 风险缓和计划:可以使用的或者正在使用的用来降低该风险严重性或者可能性的风险缓和措施。

  • 监控:对该风险的发生是否进行了监控,如果监控了说明监控的指标,如果没有监控,说明原因,以及达成监控目标的原因,最终所有的风险应该是要监控起来的。

  • 状态:活跃 / 已缓和 / 正在修复 / 已解决

  • 历史风险情况:该风险在历史上有没有发生过,什么时候,发生频率等

  • 风险缓和计划:当我们制定风险缓和计划的时候,需要从严重性最高的项开始,缓和风险不是为了消除,而是为了降低风险的严重性和可能性。并不是每一个风险都要制订风险缓和计划。

  • 风险预案:当风险发生的时候,我们可以采取的措施


除此之外,还包括一些常规的添加时间,ID,负责人之类的


6.2 识别和评估系统性风险


识别系统性风险是一个关键的步骤,它需要深入分析和理解组织或项目所面临的技术环境和相关因素。以下是一些常见的技术上的系统性风险示例:




  • 依赖单点故障:系统中存在关键组件、设备或服务的单点故障,一旦出现故障,将导致整个系统或业务的中断。例如,网络设备的故障、云服务提供商的服务中断等。




  • 服务间的强弱依赖:如果系统中的服务之间存在强依赖关系,一旦其中一个服务发生故障或不可用,可能会导致整个系统的故障或性能下降。




  • 内部和外部/离线和在线业务的相互影响:系统中的离线和在线业务之间存在相互依赖关系,如果其中一个业务出现问题,可能会影响其他业务的正常运行。




  • 安全漏洞和数据泄露:系统存在安全漏洞或不当的安全措施,可能导致黑客攻击、数据泄露或信息安全问题。这可能对组织的声誉、客户信任和合规性产生严重影响。




  • 技术过时和不可维护:系统采用的技术或架构已过时,不再受支持或难以维护。这可能导致系统难以升级、演进和修复漏洞,增加系统故障和风险的概率。




  • 第三方供应商问题:系统依赖于第三方供应商提供的技术、服务或组件,但供应商出现问题,无法提供所需的支持、维护或升级。这可能导致系统中断、服务质量下降或业务受阻。




  • 文档或流程的问题,如没有文档,没有沉淀,只在某些人的脑袋里面:如果系统或流程存在缺乏文档、知识沉淀或依赖于个别人员的情况,可能会造成知识孤立和团队合作的问题,影响系统的可维护性和可扩展性。




  • 数据完整性和一致性问题:数据在系统内部或与其他系统之间的传输和处理过程中,可能遭受损坏、丢失或篡改,导致数据完整性和一致性问题。这可能对决策和业务流程产生负面影响。




  • 大规模系统故障:系统由多个组件、服务或子系统组成,如果其中一个组件出现故障,可能导致整个系统的大规模故障。例如,云服务提供商的故障、硬件故障等。




  • 法规和合规风险:系统必须符合特定的法规要求和合规标准,如果系统无法满足这些要求,将面临法律风险、罚款或业务停摆的风险。




  • 服务容量的不足:系统中的某些服务容量可能不足以应对高负载或峰值流量,这可能导致性能下降、响应时间延迟或系统崩溃。




  • 基建发布或扩容等发布操作会影响业务的情况:系统基础设施的发布操作,如服务器扩容、网络配置变更等,可能会对业务产生影响,例如服务中断或性能下降。




  • 线上配置/环境/网络等的变更:对线上系统的配置、环境或网络进行变更时,可能会引入风险,如配置错误、网络中断等,导致系统故障或不稳定。




  • 安全问题:系统面临的安全漏洞、攻击风险或数据泄露等问题可能对业务运行和用户数据安全产生重大影响。




要识别系统性风险,可以采取以下方法:



  • 审查历史数据和经验教训,了解以前的系统故障和问题。

  • 进行风险评估和风险工作坊,与团队一起识别潜在的系统性风险。

  • 与各个部门和团队合作,收集反馈和洞察,了解系统的弱点和关键风险点。

  • 借鉴行业标准和最佳实践,了解常见的系统性风险和应对方法。

  • 定期进行系统评估和安全审查,以发现潜在的系统性风险。

  • 通过识别系统性风险,组织可以有针对性地采取措施来降低风险,并确保系统的稳定性、安全性和可靠性。


6.3 风险治理


风险治理不是一个一蹴而就的事情,需要持续的来做,需要从组织,流程机制,系统工具和文化层面进行治理。



  • 组织层面:一个事情或方案想要比较好的落地,一定是有一个完整的组织来承接,至少需要有 PACE 的逻辑来支撑,明确分工。

  • 流程层面:流程层面至少要建立明确的沟通机制,如周报、例会等,同时还需要建议风险控制流程,明确制定风险识别、评估、控制和监测的标准流程,确保风险管理工作的有序进行。

  • 系统工具:理想中是希望有建立统一的风险管理信息系统,用于收集、整理和分析风险相关信息。甚至可以利用数据分析和人工智能,对潜在风险进行预测和预警,提高风险应对的时效性。简化版可以通过群、Jira 系统等项目管理工具来达到前期的系统工具落地的程度。

  • 文化层面:通过宣导、洞察、关注、固化、奖励等方式引导大家对于风险的关注,将风险意识融入日常工作中,提高大家对风险的认知,强化风险意识。


以上的组织、流程、系统工具和文化层面的治理都是为了更好的管理风险而存在。在这个过程中,风险模型是抓手,通过不停的识别风险,消除风险,缓和风险,不断提高系统变好的可能,以最终达到治理系统性风险的目标。


风险评估和应对规划是一个反复重复的过程,不停的迭代风险模型,识别出新的风险。


当风险模型构建完成后,我们需要定期逐个风险拉出来 review 一次,我们可以问我们自己如下的一些问题:



  • 与上次回顾相比,风险有更严重吗?可能性有更高吗?

  • 接下来会排专人来解决某些风险吗?是否应该安排?

  • 上次回顾安排的事项落实了,对应的风险情况如何,是否有更新到风险模型中?


问完问题,我们可能需要有一些实际的行动:



  • 评估是否有新的风险;

  • 删除旧的风险:如果风险已经解决了,可以归档;

  • 评估原有风险模型中的每一项风险,评估其严重性和可能性,如果有变动,对其进行更新;

  • 对于不同的优先级的风险区别对待。


以上的回顾操作我们在上面建设的某个管理系统来承载,并且这个管理系统是带有通知等功能,以更好的将风险相关的信息周知出去,如 Jira 系统。


7 小结


系统性风险是一个动态的概念,持续反复的监测和评估至关重要。定期审查系统的运行情况、漏洞和潜在风险,确保及时发现和解决问题

作者:潘锦
来源:juejin.cn/post/7242720768885309495
,以减少系统性风险。

收起阅读 »

末日终极坐标安卓辅助工具

前言 本文档只介绍工具的使用方法,有时间再写一篇介绍一下实现细节。 整体的话就是借助这个工具方便记录当前坐标,可以实现游戏资源不浪费。 阅读本文档前提是大家是《末日血战》游戏玩家。 工具下载安装 download.csdn.net/download/u0… 安...
继续阅读 »

前言


本文档只介绍工具的使用方法,有时间再写一篇介绍一下实现细节。
整体的话就是借助这个工具方便记录当前坐标,可以实现游戏资源不浪费。

阅读本文档前提是大家是《末日血战》游戏玩家。


工具下载安装


download.csdn.net/download/u0…


安装工具


工具安装后,桌面会有这个图标。

在打开工具前需进入应用设置页打开这个应用的显示悬浮窗权限图片说明


填入初始坐标


打开工具大致显示是这个样子,坐标初始都是0,那么填入相应的坐标保存就会是这个样子。
14ae0a88098b04a18b0a0c36b400e3c.jpg
可以看到左上角有个小图,这是一个直接坐标系的缩略图,拖拽可以移动位置。
小图中会显示3个红点,一个绿点。绿点表示当前坐标,红点表示终极坐标。当前点的坐标是固定显示在左上角的


此时可以按返回键退出app,但是不要杀掉应用。


建立坐标系


初次使用,建议可以打开小程序截一张生存之路的全屏图,然后我们打开这张图并横屏显示图片开始操作。当已经熟悉工具如何使用后可以在游戏中进行操作了


建立坐标系

打开图片或者游戏,进入到这个界面,点击左上角悬浮窗上的开始按钮,会看到这样一个界面(没有中间两条直线)
47b446b703476c931a27667de16d706.jpg
我们的目标就是为了建立中间两条直线。


按图片指示的顺序操作。尽可能点击在轴线的中心位置
d6bdd46daf0a94960c54bf8918af5f5.png
以上操作只需要执行一次,后续就不需要操作了。
完成以上操作,就得到有两条线的图了。这个时候就完成了建立坐标系


开始寻找终极坐标


注意观察小地图,找一个我们没有到达的离的近的终极坐标为目标。可以看到x和y的差距。

举个例子,我们当前坐标49,52,刚刚已经在48,52这里取得了一个宝箱,那么下一个目的地选237,29。因为是x坐标相差较大,我们x太小,而y坐标我们的大一点。所以主要的方向是加x,少量的减y。
05c5394993b5a8877db3d81c8ce6425.png
所以我们应该按x轴正方向和y轴负方向这里走,因为x相差较大,所以如果可以的话(有障碍物就走不了)就直接沿着x轴正方向走就好了。


我想游戏玩家应该还是知道要往哪走的,但是容易算错坐标或者根本懒得记,凭运气。那么我们指定往哪走之后,接下来怎么使用这个工具。



  • 1、点击一个位置(我们要让小车开到的位置),这个时候小车不会走,因为我们工具盖住了游戏

  • 2、app回退一下(不是杀掉应用),这时可以发现小车在抖动了,其实就是小车可以走了,再点一下刚才那个位置,小车就会走到那个位置。这样我们就完成了一次移动和坐标记录。小地图当前坐标就会变化。绿点也会移动。

  • 3、小车走完之后,我们再点开始,然后重复1,2 步骤。


补充



  • 1、本工具存在误差,一般每次执行在小车x,y <=|2| 基本100%准确。x,y <= |3| 100个汽油可能会有|2|以内坐标的误差(仅本人测试数据)

  • 1、点击位置尽可能点在地图块的中间,这样可以减少误差。遇到坐标点在路径上,可以进入其中对当前坐标进行校准,当然一般是不需要的。

  • 1、如果遇到了事件,我们就处理完事件后再点开始按钮

  • 2、回退怎么用:右下角回退用途是当我们不想走这一步,可以回退一步。重新再点一个点。确认这个点没问题我们就回退app,如果回退还是后悔不想走这一步(这个回退是指回退我们的记录,游戏中的步骤我们肯定是做不到回退的),再打开开始点击回退

  • 3、本次汽油用完后,就可以杀掉辅助工具app了。下次有汽油可以继续直接使用,记住使用过程中的退出都是回退而不是杀掉app


最后


希望大家先熟悉工具流程,可以截一张图去操作,然后再在游戏中操作避免浪费资源。坐标可以随时矫正。

希望大家游戏愉快,也希望本工具对大家有所帮助。

如有建议或问题可在文章评论

作者:流光无影
来源:juejin.cn/post/7243081126826491941
中反馈或者群里找我。

收起阅读 »

开发的功能不都是经过上线测试,为什么上线后还会那么多 Bug ?

你是否也经过这样的灵魂拷问:「开发的功能不都是经过上线测试的吗?为什么上线后还会那么多 Bug ?」。 大家明明都很努力,为什么「输出」的结果没有更进一步?今天我们就水一水这个「狗血」话题,究竟是谁个锅? 本篇只是毫无意义的「故事」,内容纯属「虚构」,如有...
继续阅读 »

你是否也经过这样的灵魂拷问:「开发的功能不都是经过上线测试的吗?为什么上线后还会那么多 Bug ?」。


大家明明都很努力,为什么「输出」的结果没有更进一步?今天我们就水一水这个「狗血」话题,究竟是谁个锅?




本篇只是毫无意义的「故事」,内容纯属「虚构」,如有雷同,自己「反思」。



对于这个话题,我想用一个「虚构」的故事来介绍下,这个故事里我有一个「朋友」,他在某电商项目组,当时恰逢经历了公司内部的「双十一需求立项」:


立项时


老板:“这次的需求很简单,主要就是参考去年 TB 的预热活动来走,核心就是提高客单量和活跃,具体细节你们和产品沟通就行”


产品:“TB 去年的活动预热大家应该都了解吧,我们这次主要就是复刻一个类似的活动,时间一个月,具体有***,总的来说,双十一活动一定要准时上线,这次运营那边投入很多经费,需求方面我会把控好,核心围绕老板的思路,细节大家可以看文档,基本就是这些内容,一个月应该够的,大家没问题吧?”


开发:“没问题,保证完成任务”


3 天后:


老板:“我刚看了 JD 好像推出了一个不错的小游戏,我觉得这个可以导入到这边活动里,这样可以提高用户的活跃,用户活跃了,消费自然就起来了”


开会


产品:“鉴于老板的意见,我觉得 JD 的这个游戏活动效果可以提升我们的复购,所以我计划在原本需求基础上增加一个支线活动。


产品:“大家不要紧张,支线会和当前主线同步开发,支线活动比较灵活,不对账号级别做限制,另外「设计」那边看下入口放哪里比较合适。”


开发:“上线时间还是没变吗?”


产品:“双十一日期会变吗?老板说了大家抓紧点,功能不多,时间还是够的”


10 天后:


老板:“我刚和x总沟通了下,他觉得我们的活动少了互动,不利于留存,你看下怎么处理”


开会


产品:“经过这几天和老板的探讨,我们发现这次活动少了一些互动性,必须增加一些交互游戏,这样才能提升日活和用户体验。


产品:“这样吧,这部分功能也是比较迫切,我知道任务比较重,但是为了这次能取到较好成果,大家加把劲,接下来周末幸苦下大家,先不休假,后面调休补回来,具体需求大家可以看文档,有一些调整,不多,时间还是够的”


开发:“。。。。”


14 天后:


老板:“我看大家工作的热情还是可以的,刚运营提了可以增加一些视频支持,我觉得这是一个很好的意见”


开会


产品:“目前看起来我们的开发进度还比较乐观,运营的同学说了他们录制了一些活动视频,我看了还不错,就在主会场增加一些视频播放的功能吧,细节你们直接和设计讨论下”


产品:“这个应该不难吧,把分享和下载加上,视频播放这么基础的东西,应该不耽误事,我看网上开源服务就挺多的。”


开发:“。。。。”


20 天后:


老板:“我刚去开发那里看了下效果,感觉分会场的效果挺好的,做支线太可惜了,给他加回主流程里”


开会


产品:“老板那边觉得分会场的效果更好,让我们把分会场的效果做到主会场里的核心交互里,分会场部分的逻辑就不要了,入口也取消。


产品:“大家不要激动,都是现成的东西,改一改很快的,不过项目进度目前看来确实起有点拉垮,接下来大家晚上多幸苦点,我们晚上11点后再下班,我申请给大家报销打车的费用”


开发:“。。。。”


23 天后:


老板:“整体效果我觉得可以,就是好像有一些糙,你们再过一过,另外大家开个会统一下目标,看看能不能有新的补充”


产品:“可以的,过去我们也一直在开会对齐,基本每两天都会对齐一次”


开会


产品:“我和设计对了下,发现有一些细节可以优化,比如一些模块的颜色可以细调一下,还有这些按键的动画效果也加上,我知道工期紧张,所以我从「隔壁」项目组借了几个开发资源,未来一周他们可以帮助大家缓解下压力”


开发:“。。。。”


28 天后:


开会


产品:“好了,项目可以提测了,相信之前测试也陆续介入跟进了需求,应该问题不大,目前看起来「燃尽图」还可以,测试完了尽快上线,老板那边也在催了”


测试:“不行啊,今天走了用例,一堆问题,而且提测版本怎么和用例还有需求文档不一致,这个提测的版本开发是不是自己没做过测试啊,一大堆问题,根本测不下去,这样我们很难做”


产品:“这个我来沟通,开发接下来这几天大家就不要回家了,马上活动上线,一起攻克难关”


产品:“我也理解大家很努力了,但是这次提测的质量确实不行,你们还是要自己多把把关,不能什么问题都等到 QA 阶段才暴露,这样不利于项目进度,需求一直都很明确,大家把 Bug 尽可能修一修,有什么问题我们及时沟通,尽快解决,敏捷开发”


开发:“。。。。”


上线前一天晚上 10 点:


开会


测试:“不行,还有 20 几个问题没有确认,这种情况我不能签字,上线了有问题谁负责。”


产品:“一些问题其实并不影响正常使用,目前主流程应该没问题,让开发把 P0 的两个问题修了之后先上线,剩下的在运营过程中逐步更新就好了,有问题让运营先收集和安抚”


开发:“上线了脏数据不好弄,会有一些账号同步的问题,而且用户等级可能还有坑”


产品:“没什么不好弄的,到时候有问题的用户让运营做个标志,接下来小步快跑修复就好了,时间不等人,明天就是上线时间, 活动上不去对老板和运营都没办法交代”


项目上线:


老板:“运营和我反馈了好多问题,你们版本上线是怎么测试的,要反思一下xxxx”


开会


产品:“我说过用户要集齐碎片和好友砍价之后才能给优惠券,等级不够的不让他们砍价成功,为什么只完成砍价的新人拿到大额优惠券?”


产品:“什么?因为账号数据绑定有 Bug ,不同渠道用户合并账号就可以满足?为什么会有这个 Bug ,测试那边没覆盖到吗?”


测试:“我不知道啊,需求文档没有说还能账号合并,而且这个功能之前没说过要限制用户等级吧?”


产品:“我出的需求肯定有,文档里肯定有,另外开发你既然知道问题,为什么不提前沟通,现在用户都消费了,这个事故你和测试 55 责,后面复盘的时候要避免这样的问题再次发生”


开发:“。。。。。”



最后


所以大家觉得是谁应该背锅?是开发的能力不够,还是测试用例的覆盖缺失?说起来,其实在此之前,我在掘金也遇到了一个 “Bug” ,比如:



文章 Markdown 里图片链接的 content-type 字段如果不符合 image/*** 规格,那么发布出来的时候链接就不会被掘金转码,所以不会有图片水印,同时直接指向了我自己的图床地址。



那么你觉得这个是 Bug 吗?明明是用户不遵循规范。但是这样不就留下漏洞了吗?



如果在文章审核通过之后,我修改图床上的图片内容,这样不就可以绕过审核在掘金展示一些「违规」内容了吗?



所以有时候一些功能的初衷是好的,但是引发的问题却又很隐蔽,那么这能怪「测试用例覆盖不到位吗」?


那么,你觉得「经过测试,为什么上线后还会那么多 Bug 」

作者:恋猫de小郭
来源:juejin.cn/post/7207715311758393405
更多可能是谁的问题?

收起阅读 »

接口耗时2000多秒!我人麻了!

接口耗时2000多秒!我人麻了! 前几天早上,有个push服务不断报警,报了很多次,每次都得运维同学重启服务来维持,因为这个是我负责的,所以我顿时紧张了起来,匆忙来到公司,早饭也不吃了,赶紧排查! 1、 现象与排查步骤: 下面是下午时候几次告警的截图: ...
继续阅读 »

接口耗时2000多秒!我人麻了!



  • 前几天早上,有个push服务不断报警,报了很多次,每次都得运维同学重启服务来维持,因为这个是我负责的,所以我顿时紧张了起来,匆忙来到公司,早饭也不吃了,赶紧排查!


1、 现象与排查步骤:


下面是下午时候几次告警的截图:



  • 来看下图。。。。接口超时 2000多秒。。。。我的心碎了!!!人也麻了!!!脑瓜子嗡嗡的。。。



  • image.png



  • 另外还总是报pod不健康、不可用 这些比较严重的警告!



  • image.png


我的第一反应是调用方有群发操作,然后看了下接口的qps 貌似也不高呀!也就 9req/s,
之后我去 grafana 监控平台 观察jvm信息发现,线程数量一直往上涨,而且线程状态是 WAITING 的也是一直涨。


如下是某一个pod的监控:


image.png
image.png


为了观察到底是哪个线程状态一直在涨,我们点进去看下详情:


image.png


上图可以看到 该pod的线程状态图 6种线程状态全列出来了, 分别用不同颜色的线代表。而最高那个同时也是14点以后不断递增那个是蓝线 代表的是 WAITING 状态下的线程数量。


通过上图现象,我大概知道了,肯定有线程是一直在wait无限wait下去,后来我找运维同学 dump了线程文件,分析一波,来看看到底是哪个地方使线程进入了wait !!!


如下 是dump下来的线程文件,可以看到搜索出427个WAITING这也基本和 grafana 监控中状态是WAITTING的线程数量一致


image.png


重点来了(这个 WAITING 状态的堆栈信息,还都是从 IOSPushStrategy#pushMsgWithIOS 这个方法的某个地方出来的(151行出来的)),于是我们找到代码来看看,是哪个小鬼在作怪?



image.png
而类 PushNotificationFuture 继承了 CompletableFuture,他自己又没有get方法,所以本质上 就是调用的 CompletableFuture的 get 方法。
image.png
ps:提一嘴,我们这里的场景是 等待ios push服务器的结果,不知道啥情况,今天(指发生故障的那天)ios push服务器(域名: api.push.apple.com )一直没返回,所以就导致一直等待下去了。。



看到这, 我 豁然开朗 && 真相大白 了,原来是在使用 CompletableFutureget时候,没设置超时时间,这样的话会导致一直在等结果。。。(但代码不是我写的,因为我知道 CompletableFuture 的get不设置参数会一直等下去 ,我只是维护,后期也没怎么修改这块的逻辑,哎 ,说多了都是泪呀!)


好一个 CompletableFuture#get();


(真是 死等啊。。。一点不含糊的等待下去,等的天荒地老海枯石烂也要等下去~~~ )


到此,问题的原因找到了。


2、 修复问题


解决办法很简单,给CompletableFuture的get操作 加上超时时间即可,如下代码即可修复:
image.png


在修复后,截止到今天(6月8号)没有这种报警情况了,而且线程数和WAITING线程都比较稳定,都在正常范围内,如下截图(一共4个pod):
image.png


至此问题解决了~~~ 终于可以睡个好觉啦!


3、 复盘总结


3.1、 代码浅析


既然此次的罪魁祸首是 CompletableFuture的get方法 那么我们就浅析一下 :



  1. 首先看下 get(); 方法
    image.png
    image.png


上边可以看到
不带参数的get方法: if(deadLine==0) 成立 ,也就是最终调用了LockSupport的park(this);方法,而这个方法最终调了unsafe的这个方法-> unsafe.park(false, 0L); 其中第二个参数就是等待多长时间后,unpark即唤醒被挂起的线程,而0 则代表无限期等待。



  1. 再来看下 get(long timeOut,TimeUnit unit);方法
    image.png
    我们可以看到
    带参数的get方法: if(deadLine==0) 不成立,走的else逻辑 也就是最终调用了LockSupportparkNanos(this,nanos);方法,而这个方法最终调了unsafe的这个方法-> unsafe.park(false, nanos); 其中第二个参数就是你调用get时,传入的tiimeOut参数(只不过底层转成纳秒了)


    我们跑了下程序,发现超过指定时间后,get(long timeOut,TimeUnit unit); 方法抛出 TimeoutException异常,而至于超时后我们开发者怎么处理,就在于具体情况了。
    Exception in thread "main" java.util.concurrent.TimeoutException
    at java.base/java.util.concurrent.CompletableFuture.timedGet(CompletableFuture.java:1886)
    at java.base/java.util.concurrent.CompletableFuture.get(CompletableFuture.java:2021)



而在 我的另一篇文章 万字长文分析synchroized 这篇文章中,我们其实深入过openjdk的源码,见识过parkunpark操作,我们截个图回忆一下:


image.png


3.2、最后总结:


1.在调用外部接口时。一定要注意设置超时时间,防止三方服务出问题后影响自身服务。


2.以后无论看到什么类型的Future,都要谨慎,因为这玩意说的是异步,但是调用get方法时,他本质上是同步等待,所以必须给他设置个超时时间,否则他啥时候能返回结果,就得看天意了!


3.凡是和第三方对接的东西,都要做最坏的打算,快速失败的方式很有必要。


4.遇到天大的问题,都尽可能保持冷静,不要乱了阵脚!
作者:蝎子莱莱爱打怪
来源:juejin.cn/post/7242237897993814075
strong>

收起阅读 »

聊聊自己进入大厂前,创业公司的经历,我学到了什么?

前言 自从毕业开始工作之后,我坚持写了 2 年多的日记,基本节奏是每半年会写一篇,目的是为了记录自己成长的痕迹。 未来某个时间自己再回首的时候,能回味到此时此刻自己在做什么。当时的选择是否正确,是否在此时此刻,看到了未来发展的趋势,跟上了正确的车。 现在是 2...
继续阅读 »

前言


自从毕业开始工作之后,我坚持写了 2 年多的日记,基本节奏是每半年会写一篇,目的是为了记录自己成长的痕迹。


未来某个时间自己再回首的时候,能回味到此时此刻自己在做什么。当时的选择是否正确,是否在此时此刻,看到了未来发展的趋势,跟上了正确的车。


现在是 23 年 6 月,我第一次羊了,上一轮虽然逃过去了,但是这一次没能幸免,现在还在发着低烧。


距离上次写日记又过去了半年多,自己也步入 25 岁,这半年发生了很多事情,体会到时间变得更快了。


工作第一年:误打误撞加入创业公司



在来到字节之前,经历了校招被毁约,来到了创业公司,在创业公司工作了一年,作息是大小周,早 9 晚 9 ,对我来说这是一段比较记忆深刻的经历,教会了我很多互联网行业的东西。



校招被毁约


19 年的时候签约了一家金融公司,也是临近 20 年毕业前几十天,才被公司以疫情经济情况不好的理由,告知单方面毁约。


那个时候的我还是很害怕的,一点是临近毕业,大多校招已停止,第二点是自己有一段时间没有复习,很多知识已经遗忘。


一个朋友得知了我的情况,内推我到一家创业公司,也是内推第二天一大早就去公司面试,还好我还有一些基础在脑子里,现场面了两轮,当场就通过了,成为了前端的实习生一枚,公司也是答应毕业之后就转正。


面试通过之后,第二天就去公司上班了。第一天入职就拿到了 Macbook,也是第一次用 Mac 办公,想象那时的自己,就跟刚进城的人一样。


公司的结构


一个公司,总共就 50 来个人,做着 DAU 百万的产品。由于公司不大,所以做各种不同事情的人(销售、CEO、内容运营、新媒体运营、算法、产品),都坐在一起。敲代码的时候还能听到销售打电话的声音,因为是教育产品,所以销售的合作方大多是猿辅导、好未来、作业帮、学霸君等教育头部公司。


虽然公司小,但是产品各个环节非常全面:



  • ABTest

  • 算法

  • Hive


公司小的好处是,决策层级少,一个想法想要落地是很快的。在公司 1 年期间,快速上线了 3 款小程序,也快速从 0 到 1 上线了金融产品。PC、H5、小程序开发都有涉及。


整个团队的氛围是非常 Open 的,可以试验各种新技术,不像大厂内部会封装一些框架,有内部标准,不好去实践一些开源的框架。


与普通公司不同的地方


个人扮演的角色


我觉得最大不同的地方是角色,我是一名前端,但不仅扮演着前端的角色。要对产品体验负责,也要自己设计页面 UI。


公司没有测试,所以在日常开发中,每个研发和产品都是 QA,要对 C 端百万 DAU 产品对应的用户体验负责。


每个人都对需求负责,一个需求上线之后,可以自己写 SQL 去 Hive 里捞埋点数据,验证 AB 的可行性。


可以深入感受互联网不同角色发挥的作用


在创业公司,因为每周都有公司周会,大家会聚在一起聊每周各部门的进展,在会上,也可以全流程的了解到一个产品的完整生命周期。




  • 内容运营视角:



    • 做公众号,发文章,在存在粉丝基础的情况下,头版收入是很可观的,可以达到 6 位数+。




  • 产品运营视角:



    • 做竞品分析,看出对方哪方面做的好,我们要抄袭哪快的功能。

    • 做电话回访,了解用户的痛点,尤其针对停留时间较长的重度 C 端用户。




  • 销售视角:



    • 通过和大型教育机构合作,由于家长是很愿意为孩子进行教育付费的,所以通过弹窗收集信息配合电销,可以达到很可观的收入。




  • 算法视角:



    • 通过 AB 试验调整算法策略,可以优化广告收入,另外也可以提前计算预测插入广告可能带来的收益。

    • 调整算法策略,也可以优化用户停留时长,增强用户粘性。




  • 数据分析:



    • 通过 Hive 离线数据计算,可以生成一些报表数据,给提供用户信息聚合查看功能。




  • 产品视角:



    • 在产品基本功能打磨完毕后,要尽可能往付费方向引导。




  • 运维视角:



    • 将服务迁移到 K8S 集群,可以降低维护成本。




  • 后端视角:



    • 和算法、数分团队配合,另外还需要负责机器运维相关。




  • 老板视角:



    • 关注一些重要事项的进展,以及查看上线后的数据,是否符合产品预期。

    • 最终产品需要自负盈亏,功能不能一步设计到位,也需要把一些功能做成付费使用的。

    • 关注公司每个方向资金支出情况,控制公司收支,避免快速花光融资资金。保持自己的股份不被过度稀释。




我的直观感受是,自己虽然初入茅庐,但通过这一年的感知,深入理解了 C 端产品的全流程。这对我来说也是一笔很大的财富。


营收方面


App 内广告占大头,开屏广告>插屏广告>贴片广告,其次公众号文章等也是赚钱的利器,销售带来的收益远不如以上两个。总共这些一个月7位数还是有的。实际上最大的开销除了人力成本,还是服务器的成本,这个成本逼近7位数。


创业公司的生命周期



  1. 公司在快速发展期,有很多功能需要开发,这时是需要人的时候,会无限招人。

  2. 在产品 2-3 年之后,如没有新的大方向,进入一段停滞期,指的是 DAU 的不增长或下降。

  3. 产品稳定期,不再需要人,核心骨干退出团队,HC 缩减,产品转向以盈利为目标。

  4. 自负盈亏,break even。不再为公司资金发愁,不再需要融资。

  5. 保持公司运作,通过手段维持 DAU 和用户付费意愿,通过一些预消费手段留住用户,扩大收益。


快速验证


快速验证是 CEO 经常提的一个点,不过在王兴和张一鸣成功的经验来看,这也是正确的。


快速验证是说快速从 0 到 1 上线一个产品,冷启动或硬广,在短期查看一个产品的数据,如果产品数据不够理想,便放弃产品。试验下一个风口上的题材。


像美团,或者字节现在也在使用这种策略,快速上线 App 并试错,留下那些抓住用户的产品。


公司的瓦解


一个产品的瓶颈


当一个产品被打磨到 3 年之后,一般来说主流程就比较完善了,换句话说是用户需要的功能,产品都有了。这个时候也就过了 PM 发力 RD 开发的时期,在这之后即便这个公司只有运营,也可以保持产品正常运行。


创业公司的问题


CEO 的话语权会很大


一个人带来的决策不一定合理,当产品的发展不再合理时。大家会出现不满情绪,久而久之大家也不再团结协作,在快速上线几个小程序无果之后,3 个月内 50% 的研发团队成员纷纷离职了,不过大伙也很厉害,离职之后大多都去了大厂。


转变方向为营收优先


通过缩减一系列支出,想方设法让公司达到赚钱的状态。


手段有:砍 HC,团建,下线产品不需要的费钱的功能。


另外我也是一步步看着,公司从半层多楼的工区面积,变成 5分之二,4分之一的大小,最后工区被卖掉,撤离北京。


我的离开


我的离开也是必然,在后期被拉到老板的新产品线帮忙做产品从 0 到 1 建设。对当时工作还不到 1 年的我来说,还是很有压力,独自 own 一个私人银行项目。


在长时间宣传下,仍是没有用户使用,我能明显的感受到,新产品前景是渺茫的,只是老板的一厢情愿。另外新产品线的研发非常少,只是一番的催活,其实过程也决定了结果,产品是做不成的。在这种情况下,我提出了离职。


不过我也很感谢这段经历,能让我对从 0 到 1 创业有新的理解,另外也锻炼了我的抗压能力,增强了技术积累。


最近的工作


工作上


工作上在建设插件市场,提供了一种能快速开发页面组件的方式,能直接嵌入组件到前端中,类似动态执行模块联邦注入的组件。是一块很有意思的功能,类似于 Chrome 应用商城,其实开发工具建设一直是我比较喜欢也擅长的方向,未来也会继续在这方面努力,学习其它语言,做更快更高效的工具,为开发提效。


详细可以看这篇我今年写的文章 带你从 0 到 1 实现前端「插件市场架构」


能力提升方面


编程技能


学习并实战了以下技能:



  • VSCode 插件开发


  • Rush.js

    • 大型项目构建管理。



  • Golang (MySQL / Redis / Kafka)

    • 主要还是 API 层面的熟悉,目的还是为了能用非 NodeJS 语言写一写后端,以及了解更多的后端知识。



  • Rust
    稍微了解了一下语法,之前也写了一篇文章:以 JS 的视角带你入门 Rust 语言,快速上手


开源库


轻量的模块联邦


非编程相关


最近这 2 年,锻炼了画图、写 PRD、拉通对齐的能力,大厂更加专精一个方面,这让我能静下心来,不再像创业公司一样,受老板的影响,不再做快速迭代的事情,而是把产品打磨好,更加以用户角度出发思考用户需要什么,补齐什么功能。


愿望


毕业之后,由于疫情一直都是在国内旅游。还没出过国,希望疫情后每年自己都能出去走走,行万里路。把最好的景色都记录下来,拓宽眼界,放松心情。我很喜欢大海,尤其是四环环海的小岛,看着大海能让自己的心平静下来。接下来还有几个非常想去的地方、意大利、冰岛、新西兰、瑞士,夏威夷,希望能在 3

作者:EricLee
来源:juejin.cn/post/7243252896392314937
0 岁之前达成目标。

收起阅读 »

大专前端,三轮面试,终与阿里无缘

web
因为一些缘故,最近一直在找工作,再加上这个互联网寒冬的大环境,从三月找到六月了,一直没有合适的机会 先说一下背景,目前三年半年经验,base 杭州,大专学历+自考本科 就在前几天,Boss 上收到了阿里某个团队的投递邀请(具体部门就不透露了),因为学历问题...
继续阅读 »

因为一些缘故,最近一直在找工作,再加上这个互联网寒冬的大环境,从三月找到六月了,一直没有合适的机会



先说一下背景,目前三年半年经验,base 杭州,大专学历+自考本科



就在前几天,Boss 上收到了阿里某个团队的投递邀请(具体部门就不透露了),因为学历问题,基本上大厂简历都不会通过初筛,但还是抱着破罐子破摔的心态投递给了对方,出乎意料的是简历评估通过了,可能是因为有两个开源项目和一个协同文档加分吧。


进入到面试环节,首先是两道笔试题,算是前置面试:


第一道题目是算法题:


提供了一个数组结构的 data,要求实现一个 query 方法,返回一个新的数组,query 方法内部有 过滤排序分组 等操作,并且支持链式调用,调用最终的 execute 方法返回结果:


const result = query(list)
.where(item => item.age > 18)
.sortBy('id')
.groupBy('name')
.execute();

console.log(result);

具体实现这里就不贴了,过滤用原生的数组 filter 方法,排序用原生的数组 sort 方法,分组需要手写一下,类似 lodash/groupBy 方法。


过滤和排序实现都比较顺利,在实现分组方法的时候不是很顺利,有点忘记思路了,不过最后还是写出来了,关于链式调用,核心是只需要在每一步的操作最后返回 this 即可。


第二道题目是场景题:


要求用 vue 或者 react 实现一个倒计时抢券组件,页面加载时从 10s 开始倒计时,倒计时结束之后点击按钮请求接口进行抢券,同时更新文案等等功能。因为我对 react 比较熟悉一点,所以这里就选择了 react。


涉及到的知识点有 hook 中对 setTimeout 的封装、异步请求处理、状态更新CSS基本功 的考察等等……


具体实现这里也不贴了,写了一堆自定义 hook,因为平时也在参与 ahooks 的维护工作,ahooks 源码背的滚瓜烂熟,所以直接搬过来了,这道题整体感觉没啥难度,算是比较顺利的。


笔试题整个过程中唯一不顺利的是在线编辑器没有类似 vscode 这样的 自动补全 功能,不管是变量还是保留字,很多单词想不起来怎么拼写,就很尴尬,英文太差是硬伤 :(


笔试过程最后中出现了一点小插曲,因为笔试有时间限制,需要在规定的时间内完成,但是倒计时还没结束,不知道为什么就自动交卷了,不过那个时候已经写的差不多了,功能全部实现了,还剩下卡片的样式没完成,css 还需要完善一下,于是就在 Boss 上跟对方解释了一下,说明了情况。


过了几分钟,对面直接回复笔试过了,然后约了面试。


一面:




  • 自我介绍


    这里大概说了两分钟,介绍了过往工作经历,做过的业务以及技术栈。




  • 七层网络模型、和 DNS 啥的


    计网这方面属于知识盲区了,听到这个问题两眼一黑,思索了一会儿,直接说回答不上来。




  • 然后问了一些 host 相关的东西



    • 很遗憾也没回答上来,尴尬。对方问我是不是计算机专业的,我坦诚的告诉对方是建筑工程。




  • React 代码层的优化可以说一下么?



    • 大概说了 class 组件和 function 组件两种情况,核心是通过减少渲染次数达到优化目的,具体的优化手段有 PureComponentshouldComponentUpdateReact.memoReact.useMemoReact.useCallbackReact.useRef 等等。




  • 说一下 useMemouseCallback 有什么区别



    • 很基础的问题,这里就不展开说了。




  • 说一下 useEffectuseLayoutEffect 有什么区别



    • 很基础的问题,这里就不展开说了。




  • 问了一下 useEffect 对应在 class 中都生命周期怎么写?



    • 很基础的问题,这里就不展开说了。




  • 如果在 if 里面写 useEffect 会有什么表现?



    • 开始没听清楚,误解对方的意思了,以为他说的是在 useEffect 里面写 if 语句,所以胡扯了一堆,后面对方纠正了一下,我才意识到对方在问什么,然后回答了在条件语句里面写 useEffect 控制台会出现报错,因为 hook 的规则就是不能在条件语句或者循环语句里面写,这点在 react 官方文档里面也有提到。




  • 说一下 React 的 Fiber 架构是什么




    • 这里说了一下 Fiber 本质上就是一个对象,是 React 16.8 出现的东西,主要有三层含义:




      1. 作为架构来说,在旧的架构中,Reconciler(协调器)采用递归的方式执行,无法中断,节点数据保存在递归的调用栈中,被称为 Stack Reconciler,stack 就是调用栈;在新的架构中,Reconciler(协调器)是基于 fiber 实现的,节点数据保存在 fiber 中,所以被称为 fiber Reconciler。




      2. 作为静态数据结构来说,每个 fiber 对应一个组件,保存了这个组件的类型对应的 dom 节点信息,这个时候,fiber 节点就是我们所说的虚拟 DOM。




      3. 作为动态工作单元来说,fiber 节点保存了该节点需要更新的状态,以及需要执行的副作用。




      (这里可以参考卡颂老师的《自顶向下学 React 源码》课程)






  • 前面提到,在 if 语句里面写 hook 会报错,你可以用 fiber 架构来解释一下吗?



    • 这里说了一下,因为 fiber 是一个对象,多个 fiber 之间是用链表连接起来的,有一个固定的顺序…… 其实后面还有一些没说完,然后对方听到这里直接打断了,告诉我 OK,这个问题直接过了。




  • 个人方面有什么规划吗?



    • 主要有两个方面,一个是计算机基础需要补补,前面也提到,我不是科班毕业的,计算机底层这方面比起其他人还是比较欠缺的,尤其是计网,另一方面就是英文水平有待提高,也会在将来持续学习。




  • 对未来的技术上有什么规划呢?



    • 主要从业务转型工程化,比如做一些工具链什么的,构建、打包、部署、监控几个大的方向,node 相关的,这些都是我感兴趣的方向,未来都可以去探索,当然了现在也慢慢的在做这些事情,这里顺便提了一嘴,antd 的 script 文件夹里面的文件是我迁移到 esm + ts 的,其中一些逻辑也有重构过,比如收集 css token、生成 contributors 列表、预发布前的一些检查等等…… 所以对 node 这块也有一些了解。




  • 能不能从技术的角度讲一下你工作中负责业务的复杂度?




    • 因为前两份工作中做的是传统的 B 端项目和 C 端项目,并没有什么可以深挖的技术难点,所以这里只说了第三份工作负责的项目,这是一个协同文档,既不算 B 端,也不算 C 端,这是一款企业级的多人协作数据平台,竞品有腾讯文档、飞书文档、语雀、WPS、维卡表格等等。


      协同文档在前端的难点主要有两个方面:




      1. 实时协同编辑的处理:当两个人同时进入一个单元格编辑内容,如果保证两个人看到的视图是同步的?那么这个时候就要提到冲突处理了,冲突处理的解决方案其实已经相对成熟,包括:




        • 编辑锁:当有人在编辑某个文档时,系统会将这个单元格锁定,避免其他人同时编辑,这种方法实现方式最简单,但也会直接影响用户体验。




        • diff-patch:基于 Git 等版本管理类似的思想,对内容进行差异对比、合并等操作,也可以像 Git 那样,在冲突出现时交给用户处理。




        • 最终一致性实现:包括 Operational Transformation(OT)、 Conflict-free replicated data type(CRDT,称为无冲突可复制数据类型)。






      2. 性能问题




        • 众所周知,互联网一线大厂的协同文档工具都是基于 canvas 实现,并且有一套自己的 canvas 渲染引擎,但是我们没有,毕竟团队规模没法跟大厂比,这个项目前端就 2 个人,所以只能用 dom 堆起来(另一个同事已经跑路,现在就剩下我一个人了)。这导致页面卡顿问题非常严重,即使做了虚拟滚动,但是也没有达到很好的优化效果。老板的要求是做到十万量级的数据,但是实际上几千行就非常卡了,根本原因是数据量太大(相当于一张很大的 Excel 表格,里面的每一个单元格都是一个富文本编辑器),渲染任务多,导致内存开销太大。目前没有很好的解决方案,如果需要彻底解决性能问题,那么就需要考虑用 canvas 重写,但是这个基本上不太现实。




        • 因为卡顿的问题,暴露出来另一个问题,状态更新时,视图同步缓慢,所以这时候不得不提到另一个优化策略:乐观更新。乐观更新的思想是,当用户进行交互的时候,先更新视图,然后再向服务端发送请求,如果请求成功,那么什么都不用管,如果请求失败,那么就回滚视图。这样做的好处是,用户体验会好很多,在一些强交互的场景,不会阻塞用户操作,比如抖音的点赞就是这样做的。但是也会带来一些问题,比如:如果用户在编辑某个单元格时,另一个用户也在编辑这个单元格,那么就会出现冲突,这个时候就需要用到前面提到的冲突处理方案了。










  • 可以讲一下你在工作中技术上的建设吗?



    • 这里讲了一下对 hooks 仓库的建设,封装了 100 多个功能 hook业务 hook,把不变的部分隐藏起来,把变化的部分暴露出去,在业务中无脑传参即可,让业务开发更加简单,同时也提高了代码的复用性。然后讲了一下数据流重构之类的 balabala……




  • 你有什么想问我的吗?



    • 问了一下面试结果大概多久能反馈给我,对方说两三天左右,然后就结束了。





结束之后不到 20 分钟,对方就在 Boss 上回复我说面试过了,然后约了二面。



二面:




  • 自我介绍



    • 跟上一轮一样,大概说了两分钟,介绍了过往工作经历,做过的业务以及技术栈。




  • 在 js 中原型链是一个很重要的概念,你能介绍一下它吗?



    • 要介绍原型链,首先要介绍一下原型,原型是什么…… 这块是纯八股,懒得打字了,直接省略吧。




  • object 的原型指向谁?



    • 回答了 null。(我也不知道对不对,瞎说的)




  • 能说一下原型链的查找过程吗?



    • 磕磕绊绊背了一段八股文,这里省略吧。




  • node 的内存管理跟垃圾回收机制有了解过吗?




    • 暗暗窃喜,这个问题问到点子上了,因为两年前被问到过,所以当时专门写了一篇文章,虽然已经过去两年了,但还是背的滚瓜烂熟:




    • 首先分两种情况:V8 将内存分成 新生代空间老生代空间




      • 新生代空间: 用于存活较短的对象




        • 又分成两个空间: from 空间 与 to 空间




        • Scavenge GC 算法: 当 from 空间被占满时,启动 GC 算法



          • 存活的对象从 from space 转移到 to space

          • 清空 from space

          • from space 与 to space 互换

          • 完成一次新生代 GC






      • 老生代空间: 用于存活时间较长的对象




        • 新生代空间 转移到 老生代空间 的条件(这个过程称为对象晋升



          • 经历过一次以上 Scavenge GC 的对象

          • 当 to space 体积超过 25%




        • 标记清除算法:标记存活的对象,未被标记的则被释放



          • 增量标记:小模块标记,在代码执行间隙执,GC 会影响性能

          • 并发标记:不阻塞 js 执行










  • js 中的基础类型和对象类型有什么不一样?



    • 基础类型存储在栈中,对象类型存储在堆中。




  • 看你简历上是用 React,你能简单的介绍一下 hooks 吗?



    • 本质上就是一个纯函数,大概介绍了一下 hooks 的优点,以及 hooks 的使用规则等等。




  • 简单说一下 useEffect 的用法:



    • useEffect 可以代替 class 中的一些生命周期,讲了一下大概用法,然后讲了一下 useEffect 的执行时机,以及 deps 的作用。




  • 说一下 useEffect 的返回值用来做什么?



    • 返回一个函数,用来做清除副作用的工作,比如:清除定时器清除事件监听等等。




  • 你知道 useEffect 第二个参数内部是怎么比较的吗?



    • 说了一下内部是浅比较,源码中用 for 循环配合 Object.is 实现。(感觉这个问题就是在考察有没有读过 React 源码)




  • 前端的话可能跟网络打交道比较多,网络你了解多少呢?



    • 这里直接坦诚的说了一下,网络是我的弱项,前面一面也问到了网络七层模型,没回答出来。




  • 那你回去了解过七层模型吗?我现在再问你一遍,你能回答出来吗?



    • 磕磕绊绊回答出来了。




  • 追问:http 是在哪一层实现的?



    • 应用层。




  • 说一下 getpost 有什么区别?



    • 两眼一黑,脑子一片空白,突然不知道说什么了,挤了半天挤出来一句:get 大多数情况下用来查询,post 大多数情况下用来提交数据。get 的入参拼在 url 上,post 请求的入参在 body 里面。面试官问我还有其它吗?我说想不起来了……




  • 说一下浏览器输入 url 到页面加载的过程:




    • 输入网址发生以下步骤:



      1. 通过 DNS 解析域名的实际 IP 地址

      2. 检查浏览器是否有缓存,命中则直接取本地磁盘的 html,如果没有命中强缓存,则会向服务器发起请求(先进行下一步的 TCP 连接)

      3. 强缓存协商缓存都没有命中,则返回请求结果

      4. 然后与 WEB 服务器通过三次握手建立 TCP 连接。期间会判断一下,若协议是 https 则会做加密,如果不是,则会跳过这一步

      5. 加密完成之后,浏览器发送请求获取页面 html,服务器响应 html,这里的服务器可能是 server、也可能是 cdn

      6. 接下来是浏览器解析 HTML,开始渲染页面




    • 顺便说了渲染页面的过程:



      1. 浏览器会将 HTML 解析成一个 DOM 树,DOM 树的构建过程是一个深度遍历过程:当前节点的所有子节点都构建好后才会去构建当前节点的下一个兄弟节点。

      2. 将 CSS 解析成 CSS Rule Tree(css 规则树)。

      3. 解析完成后,浏览器引擎会根据 DOM 树CSS 规则树来构造 Render Tree。(注意:Render Tree 渲染树并不等同于 DOM 树,因为一些像 Headerdisplay:none 的东西就没必要放在渲染树中了。)

      4. 有了 Render Tree,浏览器已经能知道网页中有哪些节点、各个节点的 CSS 定义以及他们的从属关系。下一步进行 layout,进入布局处理阶段,即计算出每个节点在屏幕中的位置。

      5. 最后一个步骤就是绘制,即遍历 RenderTree,层绘制每个节点。根据计算好的信息绘制整个页面。




    • 渲染完成之后,开始执行其它任务:



      1. dom 操作

      2. ajax 发起的 http 网络请求等等……

      3. 浏览器处理事件循环等异步逻辑等等……






  • 菜单左中右布局,两边定宽,中间自适应,说一下有几种实现方式



    • 比较经典的面试题,说了 flexfloat 两种方式。




  • 项目难点



    • 和一面一样,说了协同文档的两大难点,这里就不重复了。




  • 你有什么想问我的吗?



    • 和一面一样,问了一下面试结果大概多久能反馈给我,对方说两三天左右,然后就结束了。




  • 最后问了期望薪资什么的,然后就结束了。




二面结束之后,大概过了几个小时,在 Boss 上跟对方说了一声,如果没过的话也麻烦跟我说一下,然后这时候,对方在 Boss 上问我,第一学历是不是专科?我说是的,感觉到不太妙的样子,


然后又过了一会儿,对方说定级应该不会高,他后续看一下面试官的反馈如何……


然后又追问我,换工作的核心诉求是涨薪还是能力的提升,这里我回答的比较委婉,其实两个都想要 QAQ


今天已经是第二天了,目前没有下文,看起来二面是过了,但是因为学历不够,中止了三面的流程,基本上是失败了,我也不会报有什么希望了,所以写个面经记录一下。


最后,给自己打个广告!求职求职求职!!!


社交信息:



个人优势:



  • antd 团队成员、ahooks 团队成员,活跃于 github 开源社区,给众多知名大型开源项目提交过 PR,拥有丰富的 React + TS 实战经验

  • 熟悉前端性能优化的实现,例如代码优化、打包优化、资源优化,能结合实际业务场景进行优化

  • 熟悉 webpack / vite 等打包工具的基本配置, 能够对以上工具进行二次封装、基于以上工具搭建通用的开发环境

  • 熟悉 prettier / eslint 基本配置,有良好且严格的编码习惯,唯客户论,实用主义者

  • 熟悉代码开发到上线全流程,对协同开发分支管理项目配置等都有较深刻的最佳实践



可内推的大佬们麻烦联系我!在 github 主页有联系方式,或者直接在掘金私聊我也可,谢谢!!


作者:三年没洗澡
来源:juejin.cn/post/7239715208792342584

收起阅读 »

✅让我们制作一个漂亮的头像吧

头像 头像是指在互联网上用于代表个人或实体的图像。在社区中,头像通常用于标识和区分用户,是用户身份的象征。 社区的头像有多种意义,不限于以下几点: 身份标识:社区头像可以让用户在互联网上更好地代表自己,帮助用户与其他用户区分身份。 个性表达:社区头像可以是用...
继续阅读 »

头像


头像是指在互联网上用于代表个人或实体的图像。在社区中,头像通常用于标识和区分用户,是用户身份的象征。


社区的头像有多种意义,不限于以下几点:



  • 身份标识:社区头像可以让用户在互联网上更好地代表自己,帮助用户与其他用户区分身份。

  • 个性表达:社区头像可以是用户个人喜好的表达,例如使用特定的头像可以代表用户的风格、爱好等。

  • 社交互动:社区头像也可以促进用户之间的互动,例如用户可以通过观察其他用户的头像来了解对方的性格、兴趣爱好等。

  • 社区文化:在某些社区中,头像可能会成为社区文化的的一部分,例如某些社区可能会有特定的头像格式、颜色等。


怎么获取一个头像


如果你想要获取一个头像,你可以考虑以下几种方法:



  • 使用第三方头像库:有许多第三方头像库提供大量的头像选择,你可以通过搜索或浏览这些头像库来获取头像。常见的第三方头像库包括 imgur、imgur、av.com 等。

  • 使用网站或应用程序的内置头像选择器:许多网站和应用程序都提供了内置的头像选择器,你可以通过选择器来浏览和使用现有的头像。例如,在社交媒体网站上,你可以使用内置的头像选择器来浏览和使用其他用户分享的头像。

  • 使用自己的图片编辑器:如果你拥有一张图片,你可以考虑使用一些图片编辑器来创建或修改头像。例如,你可以使用 Photoshop、GIMP 等专业的图形编辑软件来创建或修改头像。

  • 使用在线头像制作工具:有一些在线头像制作工具可以帮助你创建自己的头像。这些工具通常提供各种图像模板、字体和颜色选择,你可以根据工具的指导来创建自己的头像。


AIGC



  • 确定您的头像风格:您可以选择自己喜欢的风格,例如卡通、现代、传统等等。

  • 选择适当的图像工具:您可以选择任何适当的图像工具,例如 Photoshop、GIMP 等等。

  • 创建您自己的头像:您可以使用工具中的绘图工具、滤镜和调整工具等来创建自己的头像。

  • 参考其他头像:您可以浏览互联网上的各种头像,以获得灵感和创意。

  • 注意细节:当您创建头像时,一定要注意细节,例如颜色、纹理、形状等等。


我们来生成一下


00841-100689130.png


00843-100689132.png


00844-100689133.png


00845-100689134.png


00846-100689135.png


00847-100689136.png


00849-4042383308.png


00850-4042383309.png


00852-4042383311.png


00853-4042383312.png


00854-4042383313.png


00856-4042383315.png


00857-4042383316.png

收起阅读 »

环信十年 -- 我的失败恋情

多久了?不知道,从何时起,我的心里开始空虚,开始寂寞?总想把自己用忙碌湮没,让内心不在空虚,却一次又一次的被失败填满?          无奈的回味着,友谊。从...
继续阅读 »
多久了?不知道,从何时起,我的心里开始空虚,开始寂寞?总想把自己用忙碌湮没,让内心不在空虚,却一次又一次的被失败填满?
          无奈的回味着,友谊。从刚认识开始,到现在,不知道过了多久了吧,我们亦都是对彼此之间熟悉的不能在熟悉了吧?可为什么我们还会因为一句话而闹的人心惶惶,友谊不在?最终成为路人?
           或许,是我的那句玩笑话错了吧,不应该说吧。可,说出来的话,泼出来的水,收是收不回来了。既然,你知道,那句话是玩笑话,为什么你还要打电话威胁我?让我们把彼此搞成陌路人?
           多久了?我们没有一起聊过天,吃过饭啦?以前,我们总是一起的打打闹闹,把彼此作为最好的朋友,可现在?我们见了面就像陌生人一样,谁也不理谁,亦当作谁也没看到谁。呵呵,这是我们想要的结果?
       也许,那刚开始时的美好,我们永远都回不去了吧,我不想因为我跟你的事破坏你与他们之间的关系,毕竟他们是无辜的,你不应该也淡忘了他们。
             或许吧,我们形同陌路的关系,让他们很难堪,但,也许,这并不破坏我与他们亦或者你们与他们之间的关系,这是我们之间的关系吧?
         何时吧?回忆那年逝去的美好与快乐,想起来,总有一种莫名的心痛,却还是那么让人心情愉快,不知道吧,何时,我们还能在一起打打闹闹,快快乐乐了?
           不知道吧,忘记了吧,陌路了吧,但我们还是会见面的,即使,我们陌路了,但是为了他们高兴,我们难道不应该在一起聚聚亦或出去玩玩?虽然吧,装作陌生人,我们还是能够在一起相处的。

         那年,美好的幸福,只可能成为回忆,而不可能再实现么?何时,我们拾起了那年美好的回忆,我们就可能同归于好了。
收起阅读 »

环信十年趴 -- 我的游戏人生

我还记得第一次接触游戏的时候,那是在我还是个小学生的时候。当时有一个同学在班上给我们展示他玩的游戏,我也跟着看了一会儿。从那一刻起,我就被游戏中那无边无际的世界吸引住了。随着时间的推移,我越来越沉迷于游戏。每当我遇到困难或者压力大的时候,我总是会找到一款喜欢的...
继续阅读 »

我还记得第一次接触游戏的时候,那是在我还是个小学生的时候。当时有一个同学在班上给我们展示他玩的游戏,我也跟着看了一会儿。从那一刻起,我就被游戏中那无边无际的世界吸引住了。

随着时间的推移,我越来越沉迷于游戏。每当我遇到困难或者压力大的时候,我总是会找到一款喜欢的游戏,投入其中,忘却烦恼和焦虑。对我而言,游戏不再仅仅是一种娱乐方式,它已经成为了我的精神寄托和解压的出口。

我所玩的游戏类型很多,有角色扮演、策略、射击等等。其中最让我难以忘怀的是《魔兽世界》这款游戏。这个游戏的世界非常巨大,里面有许多任务需要完成,也有许多其他玩家可以交流和合作。我花费了很长时间去玩这个游戏,认识了许多志同道合的朋友,一起探索这个神奇的虚拟世界。

但是,我也深知游戏对我的生活造成了一定的负面影响。因为玩游戏,我经常熬夜,导致身体状况下降;因为玩游戏,我经常缺席学校课程,导致成绩不够好。这些问题让我的父母非常担心和不满,他们要求我减少游戏时间,更加专注于学业和健康生活。我也意识到游戏与现实之间的平衡很重要,不能让游戏占据全部的时间和精力。

然而,游戏对我而言并不仅仅是负面的影响,它也影响着我的人生观和价值观。在游戏中,我学到了很多东西:如何与陌生人沟通、如何合作完成任务、如何处理复杂的情境和困难等等。这些技能不仅可以应用到游戏中,也可以应用到现实生活和职业发展中。此外,游戏还教会了我坚持不懈、勇于尝试、追求自我超越等等品质。这些品质也是我在现实生活中必须具备的。

总的来说,我的游戏人生是一段充满着欢乐、挑战和启示的旅程。游戏对我产生了深远的影响,它不仅让我找到了人生中的解压出口和娱乐方式,也让我学会了很多珍贵的技能和品质。当然,我也意识到在玩游戏的同时,要合理安排时间、注意身体健康和更加专注于现实生活。

本文参与环信十周年活动

收起阅读 »

10 秒看懂 Android 动画的实现原理

介绍 动画是 Android 应用程序中重要的交互特性。Android 提供了多种动画效果,包括平移、缩放、旋转和透明度等,它们可以通过代码或 XML 来实现。本文将介绍 Android 动画的原理和实现方法,并提供一些示例。 原理 Android 动画的实现...
继续阅读 »

介绍


动画是 Android 应用程序中重要的交互特性。Android 提供了多种动画效果,包括平移、缩放、旋转和透明度等,它们可以通过代码或 XML 来实现。本文将介绍 Android 动画的原理和实现方法,并提供一些示例。


原理


Android 动画的实现原理是通过改变视图的属性来实现的。当我们在代码中设置视图的属性值时,Android 会通过平滑过渡的方式来将视图从一个状态过渡到另一个状态。这种平滑过渡的效果就是动画效果。


属性


Android 中有许多属性可以用来实现动画效果,以下是一些常用的属性:



  • translationX:视图在 X 轴上的平移距离。

  • translationY:视图在 Y 轴上的平移距离。

  • scaleX:视图在 X 轴上的缩放比例。

  • scaleY:视图在 Y 轴上的缩放比例。

  • rotation:视图的旋转角度。

  • alpha:视图的透明度。


类型


Android 中有多种不同类型的动画,每种类型都有其自身的特点和用途:


View 动画


View 动画是一种在应用程序中实现动画效果的简单方法。它可以通过 XML 或代码来实现。View 动画可以应用于任何 View 对象,包括按钮、文本框、图像等等。常见的 View 动画包括平移、缩放、旋转和透明度等效果。以下是一个 View 动画的 XML 示例:


<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:fromXDelta="0%"
android:toXDelta="50%"
android:duration="500"
android:repeatCount="infinite"
android:repeatMode="reverse" />

</set>

帧动画


帧动画是一种将一系列图像逐帧播放来实现动画效果的方法。它可以通过 XML 或代码来实现。帧动画常用于播放一系列连续的图像,例如动态图像、电影等等。以下是一个帧动画的 XML 示例:


<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
android:oneshot="false">

<item android:drawable="@drawable/animation_frame1" android:duration="50" />
<item android:drawable="@drawable/animation_frame2" android:duration="50" />
<item android:drawable="@drawable/animation_frame3" android:duration="50" />
...
</animation-list>

属性动画


属性动画是一种可以改变视图属性值的动画效果。它可以通过 XML 或代码来实现。属性动画可以应用于任何属性,包括大小、颜色、位置、透明度等等。它可以在运行时动态地更改属性值,从而实现平滑的动画效果。以下是一个属性动画的 Java 代码的示例:


ObjectAnimator animator = ObjectAnimator.ofFloat(view, "translationX", 0f, 300f);
animator.setDuration(1000);
animator.start();

过渡动画


过渡动画是一种在应用程序中实现平滑过渡效果的方法。它可以通过 XML 或代码来实现。过渡动画常用于实现屏幕之间的切换效果,例如滑动、淡入淡出等等。以下是一个过渡动画的 XML 示例:


<transition xmlns:android="http://schemas.android.com/apk/res/android">
<fade android:duration="500" />
</transition>

Lottie 动画


Lottie 是 Airbnb 开源的一种动画库,它可以将 Adobe After Effects 中制作的动画直接导出为 JSON 格式,并在 Android 应用程序中使用。Lottie 动画可以实现非常复杂的动画效果,例如骨骼动画、粒子效果等等。


实现


要实现 Android 动画,我们需要按照以下步骤:



  1. 创建动画资源文件。

  2. 在代码中加载动画资源文件。

  3. 将动画应用到相应的视图中。


我们可以通过 XML 或代码来创建动画资源文件。以下是一个简单的平移动画的 XML 示例:


<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:fromXDelta="0%p"
android:toXDelta="50%p"
android:duration="500"
android:repeatCount="infinite"
android:repeatMode="reverse" />

</set>

在代码中加载动画资源文件的方法如下:


Animation animation = AnimationUtils.loadAnimation(this, R.anim.translate);

最后,我们需要将动画应用到相应的视图中:


imageView.startAnimation(animation);

下面是一个实现平移动画效果的 Java 代码示例:


View view = findViewById(R.id.view);
ObjectAnimator animator = ObjectAnimator.ofFloat(view, "translationX", 0f, 300f);
animator.setDuration(1000);
animator.start();

结论


无论是在应用程序设计中还是在用户体验中,动画都是一个非常重要的因素。如果你想要在你的应用程序中实现动画效果,本文提供了 Android 动画的基本原理和实现方法。你可以根据自己的需要使用不同类型的动画来实现不同的效果。


推荐


android_startup: 提供一种在应用启动时能够更加简单、高效的方式来初始化组件,优化启动速度。不仅支持Jetpack App Startup的全部功能,还提供额外的同步与异步等待、线程控制与多进程支持等功能。


AwesomeGithub: 基于Github的客户端,纯练习项目,支持组件化开发,支持账户密码与认证登陆。使用Kotlin语言进行开发,项目架构是基于JetPack&DataBinding的MVVM;项目中使用了Arouter、Retrofit、Coroutine、Glide、Dagger与Hilt等流行开源技术。


flutter_github: 基于Flutter的跨平台版本Github客户端,与AwesomeGithub相对应。


android-api-analysis: 结合详细的Demo来全面解析Android相关的知识点, 帮助读者能够更快的掌握与理解所阐述的要点。


daily_algorithm: 每日一算法,由浅入深

作者:午后一小憩
来源:juejin.cn/post/7242596746180739128
,欢迎加入一起共勉。

收起阅读 »

flutter 刷新、加载与占位图一站式服务(基于easy_refresh扩展)

前文 今天聊到的是滚动视图的 刷新与加载,对于这个老生常谈的问题解决方案就太多了,优秀的插件也屡见不鲜,譬如 pull_to_refresh、 easy_refresh、还有笔者前段时间使用过的 infinite_scroll_pagination 这些都是...
继续阅读 »

前文


今天聊到的是滚动视图刷新与加载,对于这个老生常谈的问题解决方案就太多了,优秀的插件也屡见不鲜,譬如 pull_to_refresheasy_refresh、还有笔者前段时间使用过的 infinite_scroll_pagination 这些都是极具代表性的优秀插件。
那你在使用时有没有类似情况:



  • 为了重复的样版式代码感到厌倦?

  • 为了Ctrl V+C感到无聊?

  • 看到通篇类似的代码想大刀阔斧的整改?

  • 更有甚者有没有本来只想自定义列表样式,却反而浪费更多的时间来完成基础配置?


现在我们来解决这类问题,欢迎来到走近科学探索发现 - Approaching Scientific Exploration and Discovery。(可能片场走错了:) )


注意



  • 本文以 easy_refresh 作为刷新框架举例说明(其他框架的类似)

  • 本文案例demo以 getx 作为项目状态管理框架(与刷新无关仅作为项目基础框架构成,不喜勿喷)


正文


现在请出我们重磅成员mixin,对于这个相信大家已经非常熟悉了。我们要做的就是利用easy_refresh提供的refresh controller对视图逻辑进行拆分,从而精简我们的样板式代码。


首页拆离我们刷新和加载方法:


import 'dart:async';
import 'package:easy_refresh/easy_refresh.dart';
import 'state.dart';

mixin PagingMixin<T> {
/// 刷新控制器
final EasyRefreshController _pagingController = EasyRefreshController(
controlFinishLoad: true, controlFinishRefresh: true);
EasyRefreshController get pagingController => _pagingController;

/// 初始页码 <---- 单独提出这个的原因是有时候我们请求的起始页码不固定,有可能是0,也有可能是1
int _initPage = 0;

/// 当前页码
int _page = 0;
int get page => _page;

/// 列表数据
List<T> get items => _state.items;
int get itemCount => items.length;

/// 错误信息
dynamic get error => _state.error;

/// 关联刷新状态管理的控制器 <---- 自定义状态类型,后文会有阐述主要包含列表数据、初始加载是否为空、错误信息
PagingMixinController get state => _state;
final PagingMixinController<T> _state = PagingMixinController(
PagingMixinData(items: []),
);

/// 是否加载更多 <---- 可以在控制器中初始化传入,控制是否可以进行加载
bool _isLoadMore = true;
bool get isLoadMore => _isLoadMore;

/// 控制刷新结束回调(异步处理) <---- 手动结束异步操作,并返回结果
Completer? _refreshComplater;

/// 挂载分页器
/// `controller` 关联刷新状态管理的控制器
/// `initPage` 初始页码值(分页起始页)
/// `isLoadMore` 是否加载更多
void initPaging({
int initPage = 0,
isLoadMore = true,
}) {
_isLoadMore = isLoadMore;
_initPage = initPage;
_page = initPage;
}

/// 获取数据
FutureOr fetchData(int page);

/// 刷新数据
Future onRefresh() async {
_refreshComplater = Completer();
_page = _initPage;
fetchData(_page);
return _refreshComplater!.future;
}

/// 加载更多数据
Future onLoad() async {
_refreshComplater = Completer();
_page++;
fetchData(_page);
return _refreshComplater!.future;
}

/// 获取数据后调用
/// `items` 列表数据
/// `maxCount` 数据总数,如果为0则默认通过 `items` 有无数据判断是否可以分页加载, null为非分页请求
/// `error` 错误信息
/// `limit` 单页显示数量限制,如果items.length < limit 则没有更多数据
void endLoad(
List<T>? list, {
int? maxCount,
// int limit = 5,
dynamic error,
}) {
if (_page == _initPage) {
_refreshComplater?.complete();
_refreshComplater = null;
}

final dataList = List.of(_state.value.items);
if (list != null) {
if (_page == _initPage) {
dataList.clear();
// 更新数据
_pagingController.finishRefresh();
_pagingController.resetFooter();
}
dataList.addAll(list);
// 更新列表
_state.value = _state.value.copyWith(
items: dataList,
isStartEmpty: page == _initPage && list.isEmpty,
);

// 默认没有总数量 `maxCount`,用获取当前数据列表是否有值判断
// 默认有总数量 `maxCount`, 则判断当前请求数据list+历史数据items是否小于总数
// bool hasNoMore = !((items.length + list.length) < maxCount);
bool isNoMore = true;
if (maxCount != null) {
isNoMore = page > 1; // itemCount >= maxCount;
}
var state = IndicatorResult.success;
if (isNoMore) {
state = IndicatorResult.noMore;
}
_pagingController.finishLoad(state);
} else {
_state.value = _state.value.copyWith(items: [], error: error ?? '数据请求错误');
}
}

}


创建PagingMixin<T>混入类型,泛型<T>属于列表子项的数据类型


void initPaging(...):初始化的时候可以写入基本设置(可以不调用)


Future onRefresh() Future onLoad():供外部调用的刷新加载方法


FutureOr fetchData(int page):由子类集成重写,主要是完成数据获取方法,在获取到数据后,需要调用方法void endLoad(...)来结束整个请求操作,通知视图刷新


PagingMixinController继承自ValueNotifier,是对数据相关状态的缓存,便于独立逻辑操作与数据状态:


class PagingMixinController<T> extends ValueNotifier<PagingMixinData<T>> {
PagingMixinController(super.value);

dynamic get error => value.error;
List<T> get items => value.items;
int get itemCount => items.length;
}
// flutter 关于easy_refresh更便利的打开方式
class PagingMixinData<T> {
// 列表数据
final List<T> items;

/// 错误信息
final dynamic error;

/// 首次加载是否为空
bool isStartEmpty;

PagingMixinData({
required this.items,
this.error,
this.isStartEmpty = false,
});

....

}

完成这两个类的编写,我们对于逻辑部分的拆离已经完成了。


下面是对easy_refresh的使用,封装:


class PullRefreshControl extends StatelessWidget {
const PullRefreshControl({
super.key,
required this.pagingMixin,
required this.childBuilder,
this.header,
this.footer,
this.locatorMode = false,
this.refreshOnStart = true,
this.startRefreshHeader,
});

final Header? header;
final Footer? footer;

final bool refreshOnStart;
final Header? startRefreshHeader;

/// 列表视图
final ERChildBuilder childBuilder;

/// 分页控制器
final PagingMixin pagingMixin;

/// 是否固定刷新偏移
final bool locatorMode;

@override
Widget build(BuildContext context) {
final firstRefreshHeader = startRefreshHeader ??
BuilderHeader(
triggerOffset: 70,
clamping: true,
position: IndicatorPosition.above,
processedDuration: Duration.zero,
builder: (ctx, state) {
if (state.mode == IndicatorMode.inactive ||
state.mode == IndicatorMode.done) {
return const SizedBox();
}
return Container(
padding: const EdgeInsets.only(bottom: 100),
width: double.infinity,
height: state.viewportDimension,
alignment: Alignment.center,
child: SpinKitFadingCube(
size: 25,
color: Theme.of(context).primaryColor,
),
);
},
);

return EasyRefresh.builder(
controller: pagingMixin.pagingController,
header: header ??
RefreshHeader(
clamping: locatorMode,
position: locatorMode
? IndicatorPosition.locator
: IndicatorPosition.above,
),
footer: footer ?? const ClassicFooter(),
refreshOnStart: refreshOnStart,
refreshOnStartHeader: firstRefreshHeader,
onRefresh: pagingMixin.onRefresh,
onLoad: pagingMixin.isLoadMore ? pagingMixin.onLoad : null,
childBuilder: (context, physics) {
return ValueListenableBuilder(
valueListenable: pagingMixin.state,
builder: (context, value, child) {
if (value.isStartEmpty) {
return _PagingStateView(
isEmpty: value.isStartEmpty,
onLoading: pagingMixin.onRefresh,
);
}
return childBuilder.call(context, physics);
},
);
},
);
}
}

创建PullRefreshControl类型,设置必须属性pagingMixinchildBuilder,前者是我们创建的PagingMixin对象(可以是任何类型,只要支持混入就可以了),后者是对我们滚动列表的实现。 其他的都是对 easy_refresh的属性配置,参考相关文档就行了。


到这里我们减配版的封装就完成了,使用方式如下:


截图


截图


但是我们并没有完成我们前文所说的简化操作,还是需要一遍又一遍创建重复的滚动列表,所以我们继续:


/// 快速构建 `ListView` 形式的分页列表
/// 其他详细参数查看 [ListView]
class SpeedyPagedList<T> extends StatelessWidget {
const SpeedyPagedList({
super.key,
required this.controller,
required this.itemBuilder,
this.scrollController,
this.padding,
this.header,
this.footer,
this.locatorMode = false,
this.refreshOnStart = true,
this.startRefreshHeader,
double? itemExtent,
}) : _separatorBuilder = null,
_itemExtent = itemExtent;

const SpeedyPagedList.separator({
super.key,
required this.controller,
required this.itemBuilder,
required IndexedWidgetBuilder separatorBuilder,
this.scrollController,
this.padding,
this.header,
this.footer,
this.locatorMode = false,
this.refreshOnStart = true,
this.startRefreshHeader,
}) : _separatorBuilder = separatorBuilder,
_itemExtent = null;

final PagingMixin<T> controller;

final Widget Function(BuildContext context, int index, T item) itemBuilder;

final Header? header;
final Footer? footer;

final bool refreshOnStart;
final Header? startRefreshHeader;
final bool locatorMode;

/// 参照 [ScrollView.controller].
final ScrollController? scrollController;

/// 参照 [ListView.itemExtent].
final EdgeInsetsGeometry? padding;

/// 参照 [ListView.separator].
final IndexedWidgetBuilder? _separatorBuilder;

/// 参照 [ListView.itemExtent].
final double? _itemExtent;

@override
Widget build(BuildContext context) {
return PullRefreshControl(
pagingMixin: controller,
header: header,
footer: footer,
refreshOnStart: refreshOnStart,
startRefreshHeader: startRefreshHeader,
locatorMode: locatorMode,
childBuilder: (context, physics) {
return _separatorBuilder != null
? ListView.separated(
physics: physics,
padding: padding,
controller: scrollController,
itemCount: controller.itemCount,
itemBuilder: (context, index) {
final item = controller.items[index];
return itemBuilder.call(context, index, item);
},
separatorBuilder: _separatorBuilder!,
)
: ListView.builder(
physics: physics,
padding: padding,
controller: scrollController,
itemExtent: _itemExtent,
itemCount: controller.itemCount,
itemBuilder: (context, index) {
final item = controller.items[index];
return itemBuilder.call(context, index, item);
},
);
},
);
}
}

...


归纳我们所需要的使用方式(我这里只写了ListView/GridView),构创建快速初始化加载列表的方法,将我们仅需要的Widget Function(BuildContext context, int index, T item) itemBuilder单个元素的创建(因为对于大多列表来说我们仅关心单个元素样式)暴露出来,简化PullRefreshControl的使用。


截图


对比前面的使用方式,现在更加简洁了,总计代码也就十几行吧。


到这里就结束啦,文章也仅算是对繁杂重复使用的东西进行一些归纳总结,没有特别推崇的意思,更优秀的方案也比比皆是,所以仁者见仁了各位。


附GIF展示:


GIF 2023-6-9 14-23-45.gif


附Demo地址: boomcx/templat

e_getx

收起阅读 »

mybatis拦截器实现数据权限

前端的菜单和按钮权限都可以通过配置来实现,但很多时候,后台查询数据库数据的权限需要通过手动添加SQL来实现。 比如员工打卡记录表,有id,name,dpt_id,company_id等字段,后两个表示部门ID和分公司ID。 查看员工打卡记录SQL为:selec...
继续阅读 »

前端的菜单和按钮权限都可以通过配置来实现,但很多时候,后台查询数据库数据的权限需要通过手动添加SQL来实现。

比如员工打卡记录表,有id,name,dpt_id,company_id等字段,后两个表示部门ID和分公司ID。

查看员工打卡记录SQL为:select id,name,dpt_id,company_id from t_record


当一个总部账号可以查看全部数据此时,sql无需改变。因为他可以看到全部数据。

当一个部门管理员权限员工查看全部数据时,sql需要在末属添加 where dpt_id = #{dpt_id}


如果每个功能模块都需要手动写代码去拿到当前登陆用户的所属部门,然后手动添加where条件,就显得非常的繁琐。

因此,可以通过mybatis的拦截器拿到查询sql语句,再自动改写sql。


mybatis 拦截器


MyBatis 允许你在映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:



  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)

  • ParameterHandler (getParameterObject, setParameters)

  • ResultSetHandler (handleResultSets, handleOutputParameters)

  • StatementHandler (prepare, parameterize, batch, update, query)


这些类中方法的细节可以通过查看每个方法的签名来发现,或者直接查看 MyBatis 发行包中的源代码。 如果你想做的不仅仅是监控方法的调用,那么你最好相当了解要重写的方法的行为。 因为在试图修改或重写已有方法的行为时,很可能会破坏 MyBatis 的核心模块。 这些都是更底层的类和方法,所以使用插件的时候要特别当心。


通过 MyBatis 提供的强大机制,使用插件是非常简单的,只需实现 Interceptor 接口,并指定想要拦截的方法签名即可。


分页插件pagehelper就是一个典型的通过拦截器去改写SQL的。



可以看到它通过注解 @Intercepts 和签名 @Signature 来实现,拦截Executor执行器,拦截所有的query查询类方法。

我们可以据此也实现自己的拦截器。



import com.skycomm.common.util.user.Cpip2UserDeptVo;
import com.skycomm.common.util.user.Cpip2UserDeptVoUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;

@Component
@Intercepts({
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
})
@Slf4j
public class MySqlInterceptor implements Interceptor {

@Override
public Object intercept(Invocation invocation) throws Throwable {
MappedStatement statement = (MappedStatement) invocation.getArgs()[0];
Object parameter = invocation.getArgs()[1];
BoundSql boundSql = statement.getBoundSql(parameter);
String originalSql = boundSql.getSql();
Object parameterObject = boundSql.getParameterObject();

SqlLimit sqlLimit = isLimit(statement);
if (sqlLimit == null) {
return invocation.proceed();
}

RequestAttributes req = RequestContextHolder.getRequestAttributes();
if (req == null) {
return invocation.proceed();
}

//处理request
HttpServletRequest request = ((ServletRequestAttributes) req).getRequest();
Cpip2UserDeptVo userVo = Cpip2UserDeptVoUtil.getUserDeptInfo(request);
String depId = userVo.getDeptId();

String sql = addTenantCondition(originalSql, depId, sqlLimit.alis());
log.info("原SQL:{}, 数据权限替换后的SQL:{}", originalSql, sql);
BoundSql newBoundSql = new BoundSql(statement.getConfiguration(), sql, boundSql.getParameterMappings(), parameterObject);
MappedStatement newStatement = copyFromMappedStatement(statement, new BoundSqlSqlSource(newBoundSql));
invocation.getArgs()[0] = newStatement;
return invocation.proceed();
}

/**
* 重新拼接SQL
*/
private String addTenantCondition(String originalSql, String depId, String alias) {
String field = "dpt_id";
if(StringUtils.isBlank(alias)){
field = alias + "." + field;
}

StringBuilder sb = new StringBuilder(originalSql);
int index = sb.indexOf("where");
if (index < 0) {
sb.append(" where ") .append(field).append(" = ").append(depId);
} else {
sb.insert(index + 5, "
" + field +" = " + depId + " and ");
}
return sb.toString();
}

private MappedStatement copyFromMappedStatement(MappedStatement ms, SqlSource newSqlSource) {
MappedStatement.Builder builder = new MappedStatement.Builder(ms.getConfiguration(), ms.getId(), newSqlSource, ms.getSqlCommandType());
builder.resource(ms.getResource());
builder.fetchSize(ms.getFetchSize());
builder.statementType(ms.getStatementType());
builder.keyGenerator(ms.getKeyGenerator());
builder.timeout(ms.getTimeout());
builder.parameterMap(ms.getParameterMap());
builder.resultMaps(ms.getResultMaps());
builder.cache(ms.getCache());
builder.useCache(ms.isUseCache());
return builder.build();
}


/**
* 通过注解判断是否需要限制数据
* @return
*/
private SqlLimit isLimit(MappedStatement mappedStatement) {
SqlLimit sqlLimit = null;
try {
String id = mappedStatement.getId();
String className = id.substring(0, id.lastIndexOf("
."));
String methodName = id.substring(id.lastIndexOf("
.") + 1, id.length());
final Class<?> cls = Class.forName(className);
final Method[] method = cls.getMethods();
for (Method me : method) {
if (me.getName().equals(methodName) && me.isAnnotationPresent(SqlLimit.class)) {
sqlLimit = me.getAnnotation(SqlLimit.class);
}
}
} catch (Exception e) {
e.printStackTrace();
}
return sqlLimit;
}


public static class BoundSqlSqlSource implements SqlSource {

private final BoundSql boundSql;

public BoundSqlSqlSource(BoundSql boundSql) {
this.boundSql = boundSql;
}

@Override
public BoundSql getBoundSql(Object parameterObject) {
return boundSql;
}
}
}


顺便加了个注解 @SqlLimit,在mapper方法上加了此注解才进行数据权限过滤。

同时注解有两个属性,


@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface SqlLimit {
/**
* sql表别名
* @return
*/

String alis() default "";

/**
* 通过此列名进行限制
* @return
*/

String columnName() default "";
}

columnName表示通过此列名进行限制,一般来说一个系统,各表当中的此列是统一的,可以忽略。


alis用于标注sql表别名,如 针对sql select * from tablea as a left join tableb as b on a.id = b.id 进行改写,如果不知道表别名,会直接在后面拼接 where dpt_id = #{dptId},

那此SQL就会错误的,通过别名 @SqlLimit(alis = "a") 就可以知道需要拼接的是 where a.dpt_id = #{dptId}


执行结果


原SQL:select * from person, 数据权限替换后的SQL:select * from person where dpt_id = 234

原SQL:select * from person where id > 1, 数据权限替换后的SQL:select * from person where dpt_id = 234 and id > 1


但是在使用PageHelper进行分页的时候还是有问题。



可以看到先执行了_COUNT方法也就是PageHelper,再执行了自定义的拦截器。


在我们的业务方法中注入SqlSessionFactory


@Autowired
@Lazy
private List<SqlSessionFactory> sqlSessionFactoryList;


PageInterceptor为1,自定义拦截器为0,跟order相反,PageInterceptor优先级更高,所以越先执行。




mybatis拦截器优先级




@Order




通过@Order控制PageInterceptor和MySqlInterceptor可行吗?



将MySqlInterceptor的加载优先级调到最高,但测试证明依然不行。


定义3个类


@Component
@Order(2)
public class OrderTest1 {

@PostConstruct
public void init(){
System.out.println(" 00000 init");
}
}

@Component
@Order(1)
public class OrderTest2 {

@PostConstruct
public void init(){
System.out.println(" 00001 init");
}
}

@Component
@Order(0)
public class OrderTest3 {

@PostConstruct
public void init(){
System.out.println(" 00002 init");
}
}

OrderTest1,OrderTest2,OrderTest3的优先级从低到高。

顺序预期的执行顺序应该是相反的:


00002 init
00001 init
00000 init

但事实上执行的顺序是


00000 init
00001 init
00002 init

@Order 不控制实例化顺序,只控制执行顺序。
@Order 只跟特定一些注解生效 如:@Compent @Service @Aspect … 不生效的如: @WebFilter


所以这里达不到预期效果。


@Priority 类似,同样不行。




@DependsOn




使用此注解将当前类将在依赖类实例化之后再执行实例化。


在MySqlInterceptor上标记@DependsOn("queryInterceptor")



启动报错,

这个时候queryInterceptor还没有实例化对象。




@PostConstruct




@PostConstruct修饰的方法会在服务器加载Servlet的时候运行,并且只会被服务器执行一次。

在同一个类里,执行顺序为顺序如下:Constructor > @Autowired > @PostConstruct。


但它也不能保证不同类的执行顺序。


PageHelper的springboot start也是通过这个来初始化拦截器的。





ApplicationRunner




在当前springboot容器加载完成后执行,那么这个时候pagehelper的拦截器已经加入,在这个时候加入自定义拦截器,就能达到我们想要的效果。

仿照PageHelper来写


@Component
public class InterceptRunner implements ApplicationRunner {

@Autowired
private List<SqlSessionFactory> sqlSessionFactoryList;

@Override
public void run(ApplicationArguments args) throws Exception {
MySqlInterceptor mybatisInterceptor = new MySqlInterceptor();
for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {
org.apache.ibatis.session.Configuration configuration = sqlSessionFactory.getConfiguration();
configuration.addInterceptor(mybatisInterceptor);
}
}
}

再执行,可以看到自定义拦截器在拦截器链当中下标变为了1(优先级与order刚好相反)



后台打印结果,达到了预期效果。


收起阅读 »

天马行空使用适配器模式

1. 前言 因为最近没有什么比较好的技术想要分享,所以来水一下文章,啊不对,是分享一下一些思路,和大家交流一下想法,没准能产生一些新的想法。这篇文章不具备权威,不一定正确,简单来说全都是我瞎吹。 开发这么久,多多少少也有了解过适配器模式。我还是一如既往的建议学...
继续阅读 »

1. 前言


因为最近没有什么比较好的技术想要分享,所以来水一下文章,啊不对,是分享一下一些思路,和大家交流一下想法,没准能产生一些新的想法。这篇文章不具备权威,不一定正确,简单来说全都是我瞎吹。


开发这么久,多多少少也有了解过适配器模式。我还是一如既往的建议学习设计模式,不能光靠看别人的文章,得靠自己的积累和理解源码的做法来得更切实际。


2. 浅谈适配器


我们都知道,适配器模式是一种结构型模式,结构型模式我的理解都有一个特点,简单来说就是封装了一些操作,就是隐藏细节嘛,比如说代理就是隐藏了通过代理调用实体的细节。


我还是觉得我们要重头去思考它是怎样的一个思路。如果你去查“适配器模式”,相信你看到很多关于插头的说法,而且你会觉得这东西很简单,但好像看完之后又感觉学了个寂寞。


还是得从源码中去理解。先看看源码中最经典的适配器模式的使用地方,没错,RecyclerView的Adapter。但是它又不仅仅只使用Adapter模式,如果拿它的源码来做分析反而会有些绕。但是我们可以进行抽象。想想Adapter其实主要的流程就是输入数据然后输出View给RecyclerView


然后又可以大胆的去思考一下,如果不定义一个Adapter,没有ViewHolder,要怎么实现这个效果?写一个循环,然后在循环里面做一些逻辑判断,然后创建对应的子View,添加到父View中 ,大概会这样做吧。那其实Adapter就帮我们做了这一步,并且还做了很多比较经典的优化操作,其实大概就是这样。


然后从这样的模型中,我大概是能看出了一些东西,比如使用Adapter是为了输入一些东西,也可以说是为了让它封装一些逻辑,然后达到某些目的,如果用抽象的眼光去看,我不care你封装的什么东西,我只要你达到我的目的(输出我的东西),RecyclerView的adapter就是输出View。这是其一,另外,在这个模型中我有一个使用者去使用这个Adaper,这个Apdater在这里不是一个概念,而是一个对象,我这个使用者通过Adaper这个对象给它一些输入,以此来实现我的某些目标


ok,结合Adapter模式的结构图看看(随便从网上找一张图)


image.png


可以看到这个模型中有个Client,有个Tagrget,有个Adapter。拿RecyclerView的Adapter来说,Client是RecyclerView (当然具体的addView操作是在LayoutManager中,这里是抽象去看),Adapter就是RecyclerView.Adapter,而Tagrget抽象去看就是对view的操作。


3. 天马行空的使用


首先一般使用官方的RecyclerView啊这些,都会提供自己的Adapter,但是会让人会容易觉得Adapter就是在复用View的情况下使用。而我的理解是,RecyclerView的复用是ViewHolder的思想,不是Adapter的思想,比如早期的ListView也有Adapter啊,当时出RecyclerView之前也没有说用到ViewHolder的这种做法(这是很多年前的事),我说这个是想要表达不要把Adapter和RecyclerView绑一起,这样会让思维受到局限。


对我而言,Adapter在RecyclerView中的作用是 “Data To View” ,适配数据而产出对应的View。


那我可以把Apdater的作用理解成“Object To Object” ,对于我来说,Object它可以是Data,可以是View,甚至可以是业务逻辑。


所以当我跳出RecyclerView这种传统的 “Data To View” 的思维模式之后,Adapter适配器模式可以做到的场景就很多,正如上面我理解的,Adapter简单来说就是 “Data To View” ,那我可以用适配器模式去做 “Data To Data” ,可以去做 “View To Data” ,可以去做 “Business To Business” , “Business To View” 等等,实现多种效果。


假设我这里做个Data To Data的场景 (强行举的例子可能不是很好)


我请求后台拿到个人的基本信息数据


data class PeopleInfo(
var name: String?,
var sex: Int?,
......
)

然后通过个人的ID,再请求服务端另外一个接口拿到成绩数据


data class ScoreInfo(
var language : Int,
var math : Int,
......
)

然后我有个数据竞赛的报名表对象。


data class MathCompetition(
var math : Int,
var name : String
)

然后一般我们使用到的时候就会这样赋值,假设这段代码在一个Competition类中进行


val people = getPeopleInfo()
val score = getScoreInfo()
val mathTable = MathCompetition(
score.math?,
people.name?,
......
)

就是深拷贝的一种赋值,我相信很多人的代码里面肯定会有


两个对象 AB
A.1 = B.1
A.2 = B.2
A.3 = B.3
......

这样的代码,然后对象的熟悉多的话这个代码就会写得很长。当然这不会造成什么大问题,但是你看着看着,总觉得缺少一些美感,但是好像这玩意没封装不起来这种感觉。


如果用适配器来做的话,首先明确要做的流程是从Competition这个类中,将所有数据整合成MathCompetition对象,目标就是输出MathCompetition对象。那从适配器模式的模型上去看,client就是Competition,是它的需求,Taget就是getMathCompetition,输出mathTable,然后我们可以写一个Adapter。


class McAdapter {

var mathCompetition : MathCompetition? = null

init {
// 给默认值
mathCompetition = MathCompetition(0, "name", ......)
}

fun setData(people : PeopleInfo? = null, score : ScoreInfo? = null){
people?.let {
mathCompetition?.name = it.name
......
}

score?.let {
mathCompetition?.math = it.math
......
}
}

fun getData() : MathCompetition?{
return mathCompetition
}

}

然后在Competition中就不需要直接引用MathCompetition,而是设置个setAdapter方法,然后需要拿数据时再调用adapter的getData()方法,这样就恰到好处,不会把这些深拷贝方式的赋值代码搞得到处都是。 这个Demo看着好像没什么,但是真碰到了N合1这样的数据场景的时候,使用Adapter显然会更安全。


我再简单举一个Business To View的例子吧。假设你的app中有很几套EmptyView,比如你有嵌入到页面的EmptyView,也有做弹窗类型的EmptyView,我们一般的做法就是对应的页面的xml文件中直接写EmptyView,那这些EmptyView的代码就会很分散是吧。OK,你也想整合起来,所以你会写个EmptyHelper,大概是这个意思,用单例写一个管理EmptyView的类,然后里面统一封装对EmptyView的操作,一般都会这样写。 其实如果你让我来做,我可能就会用适配器模式去实现。 ,当然也有其他办法能很好的管理,这具体的得看心情。


写一个Adapter,我这里写一段伪代码,应该比较容易能看懂


class EmptyAdapter() {
// 这样语法要伴生,懒得写了,看得懂就行
const val STATUS_NORMAL = 0
const val STATUS_LOADING = 1
const val STATUS_ERROR = 2

private var type: Int = 0
var parentView: ViewGroup? = null
private var mEmptyView: BaseEmptyView? = null
private var emptyStatus = 0 // 有个状态,0是不显示,1是显示加载中,2是加载失败......

init {
createEmptyView()
}

private fun createEmptyView() {
// 也可以判断是否有parentView决定使用哪个EmptyView等逻辑
mEmptyView = when (type) {
0 -> AEmptyView
1 -> BEmptyView
2 -> CEmptyView
else -> {
AEmptyView
}
}
}

fun setData(status: Int) {
when (status) {
0 -> parentView?.removeView(mEmptyView)
1 -> mEmptyView?.showLoading()
2 -> mEmptyView?.showError()
}
}

fun updateType(type: Int) {
setData(0)
this.type = type
createEmptyView()
}
}

然后在具体的Activity调用的时候,可以


val emptyAdapter = EmptyAdapter(getContentView())
// 然后在每次要loading的时候去设置adapter的的状态
emptyAdapter.setData(EmptyAdapter.STATUS_LOADING)
emptyAdapter.setData(EmptyAdapter.STATUS_NORMAL)
emptyAdapter.setData(EmptyAdapter.STATUS_ERROR)

可以看出这样做就有几个好处,其一就是不用每个xml都写EmptyView,然后也能做到统一的管理,和一些人写的Helper这种的效果类似,最后调用的方法也很简单,你只需要创建一个Adaper,然后调用它的setData就行,我们的RecyclerView也是这样的,在外层去创建然后调用Adapter就行。


4. 总结


写这篇文章主要是为了水,啊不对,是为了想说明几个问题:

(1)开发时要善于跳出一些限制去思考,比如RecyclerView你可能觉得适配器模式就和它绑定了,就和View绑定了,有View的地方才能使用适配器模式,至少我觉得不是这样。

(2)学习设计模式,只去看一些介绍是很难理解的,当然要知道它的一个大致的思想,然后要灵活运用到开发中,这样学它才有用。

(3)我对适配器模式的理解就是 Object To Object,我可以去写ViewAdapter,可以去写DataAdapter,也可以去写BusinessAadpter,可以用这个模式去适配不同的场景,利用这个思想来使代码更加合理。


当然最后还是要强调一下,我不敢保证我说的就是对的,我肯定不是权威的。但至少我使用这招之后的的新代码效果要比一些旧代码更容

作者:流浪汉kylin
来源:juejin.cn/post/7242623772301459517
易维护,更容易扩展。

收起阅读 »

vue打包脚本:打包前判定某个npm link依赖库是否是指定分支

web
1. 需求场景 有一个项目A,它依赖项目B 项目B是本地开发的,也在本地维护 项目A通过npm link链接到了项目B 随着项目A的不断迭代,功能升级较大创建了新的分支项目A-dev 项目A-dev分支也要求项目B也要创建出项目B-dev分支与之对应 项目A...
继续阅读 »

1. 需求场景



  • 有一个项目A,它依赖项目B

  • 项目B是本地开发的,也在本地维护

  • 项目A通过npm link链接到了项目B

  • 随着项目A的不断迭代,功能升级较大创建了新的分支项目A-dev

  • 项目A-dev分支也要求项目B也要创建出项目B-dev分支与之对应

  • 项目A项目A-dev都在产品同时运行,遇到问题都要在各自分支同步修复缺陷


在启动或者打包的时候需要特别小心项目B处在什么分支,错配分支就会导致项目启动失败或报错,于是需要有一个脚本帮我在项目启动时,检查依赖脚本是否在正确的分支上,如果不在,就自动将依赖分支切换到对应需要的分支上。


2. 脚本编写


2.1 脚本思路:



  • 先去指定项目B的文件夹中查看该项目处于哪一个分支

  • 通过动态参数获得本地启动或打包的是哪一个分支,和当前项目分支进行比对

  • 如果不是当前分支,就切换到要求的分支, 切到对应分支后,再执行install操作

  • 如果是当前分支则直接跳过分支切换操作,直接往下走


2.2 脚本创建


在vue项目根目录创建一个脚本文件check-dependency.js


下面脚本中分支名@tiamaes/t4-framework就对应项目B


const t4FrameworkFilePath = "D:/leehoo/t4-framework"; //本地@tiamaes/t4-framework目录地址

const { spawnSync } = require("child_process");

const branchName = process.argv[2];//获取参数分支名,打包时需要传递进来
if (!branchName) {
console.error("Branch name is not specified.");
process.exit(1);
}

// 获取当前分支
const result = spawnSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd: t4FrameworkFilePath });
if (result.status !== 0) {
console.error("Failed to get current branch:", result.stderr.toString());
process.exit(1);
}
const currentBranch = result.stdout.toString().trim();

// 判断分支是否需要切换
if (currentBranch !== branchName) {
console.log(`@tiamaes/t4-framework is not in ${branchName} branch. Switching to ${branchName} branch...`);
const checkoutResult = spawnSync("git", ["checkout", branchName], { cwd: t4FrameworkFilePath });
if (checkoutResult.status !== 0) {
console.error(`Failed to switch to ${branchName} branch:`, checkoutResult.stderr.toString());
process.exit(1);
}

// 安装依赖包
console.log("Installing dependencies...");
const installResult = spawnSync(process.platform === "win32" ? "npm.cmd" : "npm", ["install"], { cwd: t4FrameworkFilePath });
if (installResult.status !== 0) {
console.error("Failed to install dependencies:", installResult.stderr.toString());
process.exit(1);
}
console.log("Dependencies installed successfully.");
}

console.log(`@tiamaes/t4-framework is in ${branchName} branch. Proceeding to build...`);

process.exit(0);


3. package.json引用


在该脚本的script中增加引用方式,在项目启动或打包的时候都要执行一次node check-dependency.js,其后跟随的是项目B的分支名,我这里是erp-dev和erp-m1两个分支


"scripts": {
"serve:erp": "node check-dependency.js erp-dev && npm run link:local && vue-cli-service serve --mode development.erp",
"serve:m1": "node check-dependency.js erp-m1 && npm run link:local && vue-cli-service serve --mode development.m1",
"build:erp": "node check-dependency.js erp-dev && npm run link:local && vue-cli-service build --report --mode production.erp",
"build:m1": "node check-dependency.js erp-m1 && npm run link:local && vue-cli-service build --mode production.m1",
"link:local": "npm link @tiamaes/t4-framework",
},

下面是执行效果


Video_2023-06-09_202602.gif


image.png

收起阅读 »

单例模式

单例设计模式 单例设计模式是一种对象创建模式,用于产生一个对象的具体实例,它可以确保系统中一个类只产生一个实例。 好处: 对于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级对象而言,是非常可观的一笔系统开销。 由于new操作的次数减少,因而对...
继续阅读 »

单例设计模式


单例设计模式是一种对象创建模式,用于产生一个对象的具体实例,它可以确保系统中一个类只产生一个实例。


好处:



  • 对于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级对象而言,是非常可观的一笔系统开销。

  • 由于new操作的次数减少,因而对系统内存的使用频率也会降低,这将减轻GC压力,缩短GC停顿时间。


单例模式的六种写法:


一、饿汉单例设计模式


步骤:



  1. 私有化构造函数。

  2. 声明本类的引用类型变量,并且使用该变量指向本类对象。

  3. 提供一个公共静态的方法获取本类的对象。


//饿汉单例设计模式 ----> 保证Single在在内存中只有一个对象。
public class HungrySingleton {
//声明本类的引用类型变量,并且使用该变量指向本类对象
private static final HungrySingleton instance = new HungrySingleton();
//私有化构造函数
private HungrySingleton(){
System.out.println("instance is created");
}
//提供一个公共静态的方法获取本类的对象
public static HungrySingleton getInstance(){
return instance;
}
}


不足:无法对instance实例做延迟加载


优化:懒汉


二、懒汉单例设计模式



  1. 私有化构造函数。

  2. 声明本类的引用类型变量,但是不要创建对象。

  3. 提供公共静态的方法获取本类的对象,获取之前先判断是否已经创建了本类对象,如果已经创建了,那么直接返回对象即可,如果还没有创建,那么先创建本类的对象,然后再返回。


//懒汉单例设计模式 ----> 保证Single在在内存中只有一个对象。
public class LazySingleton {
//声明本类的引用类型变量,不创建本类的对象
private static LazySingleton instance;
//私有化构造函数
private LazySingleton(){

}
public static LazySingleton getInstance(){
//第一次调用的时候会被初始化
if(instance == null){
instance = new LazySingleton();
}
return instance;
}
}


不足:在多线程的情况下,无法保证内存中只有一个实例


public class MyThread extends Thread{

@Override
public void run() {
System.out.println(LazySingleton.getInstance().hashCode());
}

public static void main(String[] args) {
MyThread[] myThread = new MyThread[10];
for(int i=0;i<myThread.length;i++){
myThread[i] = new MyThread();
}
for(int j=0;j<myThread.length;j++){
myThread[j].start();
}
}
}


打印结果:


257688302
1983483740
1983483740
1983483740
1983483740
1983483740
1983483740
1388138972
1983483740
257688302

在多线程并发下这样的实现无法保证实例是唯一的。


优化:懒汉线程安全


三、懒汉线程安全


通过使用同步函数或者同步代码块保证


public class LazySafetySingleton {

private static LazySafetySingleton instance;
private LazySafetySingleton(){

}
//方法中声明synchronized关键字
public static synchronized LazySafetySingleton getInstance(){
if(instance == null){
instance = new LazySafetySingleton();
}
return instance;
}

//同步代码块实现
public static LazySafetySingleton getInstance1(){
synchronized (LazySafetySingleton.class) {
if(instance == null){
instance = new LazySafetySingleton();
}
}
return instance;
}
}

不足:使用synchronized导致性能缺陷


优化:DCL


四、DCL


DCL:double check lock (双重检查锁机制)


public class DclSingleton {

private static DclSingleton instance = null;

private DclSingleton(){

}
public static DclSingleton getInstance(){
//避免不必要的同步
if(instance == null){
//同步
synchronized (DclSingleton.class) {
//在第一次调用时初始化
if(instance == null){
instance = new DclSingleton();
}
}
}
return instance;
}

}


不足:在if判断中执行的instance = new DclSingleton(),该操作不是一个原子操作,JVM首先会按照逻辑,第一步给instance分配内存;第二部,调用DclSingleton()构造方法初始化变量;第三步将instance对象指向JVM分配的内存空间;JVM的缺点:在即时编译器中,存在指令重排序的优化,即以上三步不一定会按照顺序执行,就会造成线程不安全。


优化:给instance的声明加上volatile关键字,volatile能保证线程在本地不会存有instance的副本,而是每次都到内存中读取。即禁止JVM的指令重排序优化。即按照原本的步骤。把instance声明为volatile之后,对它的写操作就会有一个内存屏障,这样,在它的赋值完成之前,就不会调用读操作。



注意:volatile阻止的不是instance = new DclSingleton();这句话内部的指令排序,而是保证了在一个写操作完成之前,不会调用读操作(if(instance == null))



public class DclSingleton {

private static volatile DclSingleton instance = null;

private DclSingleton(){

}
public static DclSingleton getInstance(){
//避免不必要的同步
if(instance == null){
//同步
synchronized (DclSingleton.class) {
//在第一次调用时初始化
if(instance == null){
instance = new DclSingleton();
}
}
}
return instance;
}

}

五、静态内部类


JVM提供了同步控制功能:static final,利用JVM进行类加载的时候保证数据同步。


在内部类中创建对象实例,只要应用中不使用内部类,JVM就不会去加载该类,就不会创建我们要创建的单例对象,


public class StaticInnerSingleton {

private StaticInnerSingleton(){

}
/**
* 在第一次加载StaticInnerSingleton时不会初始化instance,
* 只在第一次调用了getInstance方法时,JVM才会加载StaticInnerSingleton并初始化instance
* @return
*/

public static StaticInnerSingleton getInstance(){
return SingletonHolder.instance;
}
//静态内部类
private static class SingletonHolder{
private static final StaticInnerSingleton instance = new StaticInnerSingleton();
}

}


优点:JVM本身机制保证了线程安全,没有性能缺陷。


六、枚举


public enum EnumSingleton {
//定义一个枚举的元素,它就是Singleton的一个实例
INSTANCE;

public void doSomething(){

}
}

优点:写法简单,线程安全



注意:如果在枚举类中有其他实例方法或实例变量,必须确保是线程安全的。


作者:我可能是个假开发
来源:juejin.cn/post/7242614671001862199

收起阅读 »

Vue和React权限控制的那些事

web
自我介绍 看官们好,我叫JetTsang,之前都是在掘金潜水来着,现在偶尔做一些内容输出吧。 前言 无论是后台管理系统,还是面向C端的产品,权限控制都是日常工作中常见的需求。在此梳理一下权限控制的那些逻辑,以及在Vue/React框架下是有什么样的解决方案。 ...
继续阅读 »

自我介绍


看官们好,我叫JetTsang,之前都是在掘金潜水来着,现在偶尔做一些内容输出吧。


前言


无论是后台管理系统,还是面向C端的产品,权限控制都是日常工作中常见的需求。在此梳理一下权限控制的那些逻辑,以及在Vue/React框架下是有什么样的解决方案。


什么是权限控制?


现在基本上都是基于RBAC权限模型来做权限控制


一般来说权限控制就是三种




  • 页面权限:说白了部分页面是具备权限的,没权限的无法访问




  • 操作权限:增删改查的操作会有权限的控制




  • 数据权限:不同用户看到的、数据是不一样的,比如一个列表,不同权限的查看这部分数据,可能有些字段是**脱敏的,有些条目无法查看详情,甚至部分条目是无法查看




那么对应到前端的维度,常见的就4种




  • 权限失效(无效)(token过期/尚未登录)




  • 页面路由控制,以路由为控制单位




  • 页面上的操作按钮、组件等的权限控制,以组件/按钮为最小控制单位




  • 动态权限控制,比如1个列表,部分数据可以编辑,部分部分不可编辑




image.png


⚠️注意: 本文一些方案 基于 React18 React-Router V6 以及 Vue3 Vue-Router V4


⚠️Umi Max 这种具备权限控制系统的框架暂时不在讨论范围内~~


前置条件


由于市面上各家实现细节不一样,这里只讨论核心逻辑思路,不考虑细节实现


无论框架如何,后端根据RABC角色权限这套逻辑下来的,会有如下类似的权限标识信息,可以通过专门的接口获取,或者跟登录接口放在一起。


image.png


然后根据这些数据,去跟路由,按钮/组件等,比对产生真正的权限


像这种权限标识一般都存在内存当中(即便存在本地存储也需要加密,不过其实真正的权限控制还是需要后端来控),一般都是全局维护的状态,配合全局状态管理库使用。


权限失效(无效)


image.png


这种场景一般是在发送某些请求,返回过期状态


或者跟后端约定一个过期时间(这种比较不靠谱)


通常是在 全局请求拦截 下做,整理一下逻辑


路由级别权限控制


通常前端配好的路由可以分为 2 种:


一种是静态路由:即无论什么权限都会有的,比如登录页、404页这些


另一种是动态路由:虽然叫动态路由,其实也是在前端当中定义好了的。说它是动态的原因是根据后端的权限列表,要去做动态控制的


vue实现


在vue体系下,可以通过路由守卫以及动态添加路由来实现


动态路由


先配置静态路由表 , 不在路由表内的路由重定向到指定页(比如404)


在异步获取到权限列表之后,对动态部分的路由进行过滤之后得到有权限的那部分路由,再通过router.addRoute()添加到路由实例当中。


流程为:


(初始化时) 添加静态路由 --> 校验登录态(比如是否有token之类的) --> 获取权限列表(存到vuex / pinia) --> 动态添加路由(在路由守卫处添加)



rightsRoutesList // 来自后端的当前用户的权限列表,可以考虑存在全局状态库
dynamicRoutes // 动态部分路由,在前端已经定义好, 直接引入

// 对动态路由进行过滤,这里仅用path来比较
// 目的是添加有权限的那部分路由,具体实现方案自定。
const generateRoute = (rightsRoutesList)=>{
//ps: 这里需要注意下(如果有)嵌套路由的处理
return dynamicRoutes.filter(i=>
rightsRoutesList.some(path=>path === i.path)
)
}

// 拿到后端返回的权限列表
const getRightsRoutesList = ()=>{
return new Promise(resolve=>{
const store = userStore()
if(store.rightsRoutesList){
resolve(store.rightsRoutesList)
}else{
// 这里用 pinia 封装的函数去获取 后端权限列表
const rightsRoutesList = await store.fetchRightsRoutesList()
resolve(rightsRoutesList)
}
}
}

let hasAddedDynamicRoute = false
router.beforeEach(async (to, from) => {
if(hasAddedDynamicRoute){
// 获取
const rightsRoutesList = await getRightsRoutesList()

// 添加到路由示例当中
const routes = generateRoute(rightsRoutesList)
routes.forEach(route=>router.addRoute(route))
// 对于部分嵌套路由的子路由才是动态路由的,可以
router.addRoute('fatherName',route)
hasAddedDynamicRoute = true
}
// 其他逻辑。。。略


next({...to})
}


踩坑

通过动态addRoute去添加的路由,如果你F5刷新进入这部分路由,会有白屏现象。


image.png


因为刷新进入的过程经历了 异步获取权限列表 --> addRoute注册 的过程,此时跳转的目标路由就和你新增的路由相匹配了,需要去手动导航。


因此你需要在路由守卫那边next放行,等下次再进去匹配到当前路由


你可以这么写


router.beforeEach( (to,from,next) => {
// ...其他逻辑

// 关键代码
next({...to})
})


路由守卫


一次性添加所有的路由,包括静态和动态。每次导航的时候,去对那些即将进入的路由,如果即将进入的路由是在动态路由里,进行权限匹配。


可以利用全局的路由守卫


router.beforeEach( (to,from,next) => {
// 没有访问权限,则重定向到404
if(!hasAuthorization(to)){
// 重定向
return '/404'
}
})

也可以使用路由独享守卫,给 权限路由 添加


    // 路由表
const routes = [
//其他路由。。。略

// 权限路由
{
path: '/users/:id',
component: UserDetails,
// 定义独享路由守卫
beforeEnter: (to, from) => {
// 如果没有许可,则
if(!hasAuthorization(to)){
// 重定向到其他位置
return '/404'
}
},
},
]


react实现


在react当中,一般先将所有路由添加好,再通过路由守卫来做权限校验


局部守卫loader


React-router 当中没有路由守卫功能,可以利用v6版本的新特性loader来做,给权限路由都加上对应的控制loader


import { redirect, createBrowserRouter, RouterProvider } from 'react-router-dom'


const router = createBrowserRouter([
{
// it renders this element
element: <Team />,

// when the URL matches this segment
path: "teams/:teamId",

// with this data loaded before rendering
loader: async ({ request, params }) => {
// 拿到权限
const permission = await getPermission("teams/:teamId")
// 没有权限则跳到404
if(!permission){
return redirect('/404')
}
return null
},

// and renders this element in case something went wrong
errorElement: <ErrorBoundary />,
},
]);

// 使用
function RouterView (){
return (
<RouterProvider router={router}/>
)
}



包装路由(相当于路由守卫)


配置路由组件的时候,先渲染包装的路由组件


image.png


在包装的组件里做权限判断


function RouteElementWrapper({children, path, ...props }: any) {
const [isLoading, setIsLoading] = useState<boolean>(false);
useEffect(()=>{
// 判断登录态之类的逻辑

// 如果要获取权限,则需要setIsLoading,保持加载状态

// 这里判断权限
if(!hasAccess(path)){
navigate('/404')
}
},[])
// 渲染routes里定义好的路由
return isLoading ? <Locading/> : children
}

按钮(组件)级别权限控制


组件级别的权限控制,核心思路就是 将判断权限的逻辑抽离出来,方便复用。


vue 实现思路


在vue当中可以利用指令系统,以及hook来实现


自定义指令


指令可以这么去使用


<template>
<button v-auth='/site/config.btn'> 编辑 </button>
</template>

指令内部可以操作该组件dom和vNode,因此可以控制显隐、样式等。


hook


同样的利用hook 配合v-if 等指令 也可以实现组件级颗粒度的权限控制


<template>
<button v-if='editAuth'> 权限编辑 </button>
<div v-else>
无权限时做些什么
</div>

<button v-if='saveAuth'> 权限保存 </button>
<button v-if='removeAuth'> 权限删除 </button>
</template>
<script setup>
import useAuth from '@/hooks/useAuth'
// 传入权限
const [editAuth,saveAuth,removeAuth] = useAuth(['edit','save','remove'])
</script>


hook里的实现思路: 从pinia获取权限列表,hook里监听这个列表,并且匹配对应的权限,同时修改响应式数据。


react 实现思路


在React当中可以用高阶组件和hook的方式来实现


hook


定义一个useAuth的hook


主要逻辑是: 取出权限,然后通过关联响应式,暴露出以及authKeys ,hasAuth函数


export function useAuth(){
// 取出权限 ps: 这里从redux当中取
const authData = useSelector((state:any)=>state.login)
// 取出权限keys
const authKeys = useMemo(()=>authData.auth.components ?? [],[authData])
// 是否拥有权限
const hasAuth = useCallback(
(auths:string[]|string)=>(
turnIntoList(auths).every(auth=>authKeys.includes(auth))
),
[authKeys]
)
const ret:[typeof authKeys,typeof hasAuth] = [authKeys,hasAuth]
return ret
}

使用


const ProductList: React.FC = () => {
// 引入
const [, hasAuth] = useAuth();
// 计算是否有权限
const authorized = useMemo(() => hasAuth("edit"), [hasAuth]);

// ...略
return (
<>
{ authorized ? <button> 编辑按钮(权限)</button> : null}
</>

)
};


权限包裹组件


可以跟进一步,依据这个权限hook,封装一层包裹组件


const AuthWrapper:React.FC<{auth:string|string[],children:JSX.Element}> = ({auth, children})=>{
const [, hasAuth] = useAuth();
// 计算是否有权限
const authorized = useMemo(() => hasAuth(auth), [hasAuth]);
// 控制显隐
return authorized ? children : null
}

使用


<AuthWrapper auth='edit'>
<button> 编辑按钮(AuthWrapper) </button>
</AuthWrapper>

还可以利用renderProps特性


const AuthWrapper:React.FC<{auth:string|string[],children:JSX.Element}> = ({auth, children})=>{
const [, hasAuth] = useAuth();
// 计算是否有权限
const authorized = useMemo(() => hasAuth(auth), [hasAuth]);
+ if(typeof children === 'function'){
+ return children(authorized)
+ }
// 控制显隐
return authorized ? children : null
}

<AuthWrapper auth='edit'>
{
(authorized:boolean)=> authorized ? <button> 编辑按钮(rederProps) </button> : null
}
</AuthWrapper>

动态权限控制


这种主要是通过动态获取到的权限标识,来控制显隐、样式等。可以根据特定场景做特定的封装优化。主要逻辑其实是在后端处理。


结尾


可以看到在两大框架下实现权限控制时,思路和细节上还是稍稍有点不一样的,React给人的感觉是手上的积木更加零碎的一点,有些功能需要自己搭起来。相反Vue给人的感觉是面面俱到,用起来下限会更高。


最后


如果大家有什么想法和思考,欢迎在评论区留言~~。


另外:本人经验有限,

作者:JetTsang
来源:juejin.cn/post/7242677017034915899
如果有错误欢迎指正。

收起阅读 »

环信十周年趴——我的程序人生

        2011年,那时候刚上大学,计算机科学与技术专业,大家都是很迷茫的。尤其是大一的时候,面对十几门专业课程(《C语言程序设计》、《C++程序设计》、《微机原理》、《单片机原理》、《算法导论》、《数据结构》、...
继续阅读 »

        2011年,那时候刚上大学,计算机科学与技术专业,大家都是很迷茫的。尤其是大一的时候,面对十几门专业课程(《C语言程序设计》、《C++程序设计》、《微机原理》、《单片机原理》、《算法导论》、《数据结构》、《计算机原理》、《逻辑与数字电路》、《高数》、《线性代数》等)的时候,实在是没有一点儿想法。也不知道该学哪个,该丢哪个。但是要想不挂科,还是得雨露均沾,学习时间平均分配。印象最深的课程就是《C语言程序设计》,刚开始学真是一脸懵,这是啥,这玩意儿有啥用?用来干啥的?然后迫于对知识的渴求,努力学了,也是大学课程里面学得最好的专业,这也是后来为啥成为了iOS开发。

        我是一名曾经从事iOS开发工作的程序员,在这个行业中度过了多年的光阴。但是,在某个时刻,我的程序人生彻底转向了一个不同的方向,那就是我遭遇了落魄的iOS开发之路。

        落魄经历

        在我的iOS开发过程中,我也遇到了很多挑战和困难。其中最重要的一点是,随着竞争日益激烈,市场需求变得更加严格和复杂,我的职业前景开始黯淡下来。

        从那之后,我开始频繁地跳槽,但是并没有找到一个令我满意的岗位。在移动应用开发的生态系统中,iOS领域的变化是惊人的。新的技术和框架不断驱动和推动着市场和用户需求的变化,而这个速度比任何其他应用领域都要快。但我并没有保持这个变化的步伐,慢慢地,我的技能逐渐落后,导致我的职业发展受到了影响。

        在这个过程中,我开始感到自己正在与市场和软件发展的步伐背道而驰。我不再能够适应市场需求和客户的期望,我甚至感觉到自己的运气都已经耗尽了。

        逆境中的人生反思

        在落魄的状态下,我开始经历了一段自我反思的旅程。我开始回顾自己的职业生涯,思考我所从事的工作和做出的决策是否真的为我带来了成就感和满足感。我也开始考虑其他领域和技能的发展可能性。

        这段旅程让我意识到,“业内良心”(本意是良心味道的事物)这个说法是存在的,它深刻地体现了我在这个行业中的经历。与此同时,我也意识到,真正的成功无法用市场或行业发展的脉搏来衡量,而是要经由自己的内心感觉。

        在自我反省的过程中,我也发现了自己职业规划和发展的不足之处。我没有及时了解新技术和框架的发展趋势,并没有花费足够的精力和时间来提高自己的职业素养和思考能力。这让我在竞争激烈的市场中不断失利。

         今天,虽然我没有从事iOS开发了,但我始终没有忘记自己所学到的知识和经验。落魄的经历让我成为一个更好的Programmer,坚持自己的初心且不断进取。不管你们做着什么,无论遇到什么困境,都请不要磨灭自己的热情和信念。这就是我从我的落魄经历中得到的宝贵经验。


本文参与环信十周年活动 ,活动链接:https://www.imgeek.net/question/474026

收起阅读 »

Vue3项目实现图片实现响应式布局和懒加载

web
Element实现响应式布局 分享一下,在Vue3项目中实现响应式布局(一行显示7列)。在这个例子中,我参考了Element官方的Layout布局,使用el-card来放置图片。 利用分栏布局,el-row行上设置每列的间隔gutter,el-col上设置响应...
继续阅读 »

Element实现响应式布局


分享一下,在Vue3项目中实现响应式布局(一行显示7列)。在这个例子中,我参考了Element官方的Layout布局,使用el-card来放置图片。
利用分栏布局,el-row行上设置每列的间隔gutter,el-col上设置响应式的栅格布局,Element官方预设了5个响应式尺寸,官方给出了详细的属性解释。这个例子中我设置了4个尺寸。
在这里插入图片描述
栅格默认的占据的列数是24,设置24就是一列,设置12就显示两列,设置8就显示3列,设置6就显示4列,设置4显示6列......可以根据自己的场景需求来进行布局。这个例子中我设置的响应式布局如下:



:xs="12" 当浏览器宽度<768px时,一行展示2列

:sm="8" 当浏览器宽度>=768px时,一行展示3列

:md="6" 当浏览器宽度>=992px时,一行展示4列

:lg="{ span: '7' }" 当浏览器宽度>=1200px时,一行展示7列 这个需要在css样式中设置一下。



这里例子中的图片都是放在el-card中的,并且图片都是一样大小的。修改图片可以利用图片处理工具,分享一个自己常用的工具:轻量级图片处理工具photopea
Element的Card组件由 header 和 body 组成。 header 是可选的,卡片可以只有内容区域。可以配置 body-style 属性来自定义body部分的style。
:body-style="{padding:10px}" ,这个其实是对el-card头部自动生成的横线下方的body进行边距设置。也就是除了el-card的header部分,其余都是body部分了。
这里例子中没有头部,就是给卡片的body部分设置内边距。
在这里插入图片描述
具体代码如下所示:
在这里插入图片描述
在这里插入图片描述


在这里插入图片描述
图片效果如下所示:
当浏览器宽度>=1200px时,一行展示7列:
图片14.png


当浏览器宽度>=992px时,一行展示4列:


图片15.png
当浏览器宽度>=768px时,一行展示3列:


图片16.png
当浏览器宽度<768px时,一行展示2列:


图片17.png
接下来,优化一下页面,对图片进行懒加载处理。


图片懒加载


看下上面没有用于懒加载方式的情况,F12---NetWork---Img可以看到页面加载就会显示这个页面用到的所有图片。


图片18.png
可以利用vue-lazyload,它是一个Vue.js 图片懒加载插件,可以在页面滚动至图片加载区域内时按需加载图片,进一步优化页面加载速度和性能。采用懒加载技术,可以仅在需要加载的情况下再进行加载,从而减少资源的消耗。也就是在页面加载时仅加载可视区域内的图片,而对于网页下方的图片,我们滑动到该图片时才会进行加载。


下载、引入vue-lazyload


npm install vue-lazyload --save


在package.json中查看:


图片19.png
在main.ts中引入:


图片20.png


使用vue-lazyload


在需要使用懒加载的图片中使用v-lazy指令替换src属性。


图片21.png
也可以是下面的写法:


图片22.png
这样,就实现图片的懒加载了。
验证一下懒加载是否生效,F12---NetWork---Img,可以看到图片的加载情况。


一开始没有滑动到图片区域,就不会加载图片,可以在Img中看到loding占位图片在显示。


图片23.png
滑动到了对应的照片才会显示对应的图片信息。


图片24.png


图片25.png


图片26.png


作者:YM13140912
来源:juejin.cn/post/7242516121769033787
>这就实现了懒加载。

收起阅读 »

Android-apk动态加载研究

前言 近期工作中遇到两个问题。 换应用皮肤 加载插件apk中的view Android 换肤技术一文中已经详细说明了如何进行应用换肤。而加载插件apk中的view,利用前文提到的换肤技术,居然无法实现!仔细重新研究Android apk动态加载机制,有了新...
继续阅读 »

前言


近期工作中遇到两个问题。



  • 换应用皮肤

  • 加载插件apk中的view


Android 换肤技术一文中已经详细说明了如何进行应用换肤。而加载插件apk中的view,利用前文提到的换肤技术,居然无法实现!仔细重新研究Android apk动态加载机制,有了新的发现,而且还可以提高插件加载效率。


布局加载


Android 换肤技术文中提到的加载插件图片方法,无法加载插件的布局文件。怎么尝试都是失败。布局文件是资源中最复杂的,需要解析xml中的其它元素,虽然布局文件的id可以获取,但xml中其它元素的id或者其它关联性的东西仍然无法获取,这应该就是加载插件布局文件失败的原因。


宿主应用无法直播加载插件中的xml布局文件,换一个思路,插件将xml解析成view,将view传递给宿主应用使用。


插件apk需要使用插件context才能正确加载view,宿主如何生成插件context呢?

  public abstract Context createPackageContext(String packageName,
@CreatePackageOptions int flags)

context.createPackageContext(packageName, Context.CONTEXT_IGNORE_SECURITY |
Context.CONTEXT_INCLUDE_CODE);

使用上述方法可正确创建插件context。


除此之外还有一种方法(本人没有验证过),activity的工作主要是由ContextImpl来完成的, 它在activity中是一个叫做mBase的成员变量。注意到Context中有如下两个抽象方法,看起来是和资源有关的,实际上context就是通过它们来获取资源的,这两个抽象方法的真正实现在ContextImpl中。也即是说,只要我们自己实现这两个方法,就可以解决资源问题了。我们在代码中可以创建activity继承类,重写对应方法即可。具体可参考 下文

/** Return an AssetManager instance for your application's package. */
public abstract AssetManager getAssets();
/** Return a Resources instance for your application's package. */
public abstract Resources getResources();

动态加载方式


目前动态加载方式均是使用DexClassLoader方式获取对应的class实例,再使用反射调用对应接口,代码如下:

  DexClassLoader loader = new DexClassLoader(mPluginDir, getActivity().getApplicationInfo().dataDir, null, getClass().getClassLoader());
String dex = getActivity().getDir("dex", 0).getAbsolutePath();
String data = getActivity().getApplicationInfo().dataDir;
Class<?> clazz = loader.loadClass("com.okunu.demoplugin.TailImpl");
Constructor<?> constructor = clazz.getConstructor(new Class[] {});

这种方式存在一个问题,较为耗时,如果宿主管理着许多插件,这种加载方式就有问题,使用下面这种方式可加快插件的加载。

public void getTail2(Context pluginContext){
try {
Class clazz = pluginContext.getClassLoader().loadClass("com.okunu.demoplugin.TailImpl");
Constructor<?> localConstructor = clazz.getConstructor(new Class[] {});
Object obj = localConstructor.newInstance(new Object[] {});
mTail = new IPluginProxy(clazz, obj);
} catch (Exception e) {
Log.i("okunu", "ee", e);
e.printStackTrace();
}
}

注意,一定要使用插件的context为参数,它和插件的其它类使用同一个classloader,既然能获取插件classloader,则可以获取插件中的其它类。如果不使用插件context为参数,则上述方法一定会报错。


总结


针对插件资源加载,其实分为两种形式。



  • 宿主直接使用插件资源,比如使用插件图片、字符串等

  • 宿主间接使用插件资源,比如在宿主中启动插件activity或者显示插件的view


第1种形式,可以在宿主应用中构造AssetManager,添加插件的资源路径。


第2种形式,宿主创建插件context并传递给插件,插件使用自己的context则可自由调用自己的资源了,如何创建插件context前文详述了两种方法。


注意一点,宿主中肯定无法直接调用插件的R文件的。


动态加载apk,也有两种方式。



  • 使用DexClassLoader加载插件路径,获取插件的classLoader。

  • 使用已经创建好的插件context,获取插件的classLoader,效果和第1种一样,但速度要更快


动态加载apk机制还有很多东西可以深入研究,比如说插件activity的生命周期等等,这些内容后续补充。


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

Android屏幕刷新机制

基础知识 CPU、GPU CPU:中央处理器,主要负责计算数据,在Android中主要用于三大绘制流程中Surface的计算过程。 GPU:图像处理器,主要负责对图形数据进行渲染,在Android中主要用于将CPU计算好的Surface数据合成后放到buff...
继续阅读 »

基础知识


CPU、GPU



  • CPU:中央处理器,主要负责计算数据,在Android中主要用于三大绘制流程中Surface的计算过程。

  • GPU:图像处理器,主要负责对图形数据进行渲染,在Android中主要用于将CPU计算好的Surface数据合成后放到buffer中,让显示器进行读取呈现到屏幕上。


逐行扫描


屏幕在刷新buffer的时候,并不是一次性扫描完成,而是从左到右,从上到下的一个读取过程,顺序显示一屏的每个像素点,按60HZ的屏幕刷新率来算,这个过程只有16.66666...ms。



  • 从初始位置(第一行左上角)开始扫描,从左到右,进行水平扫描(Horizontal Scanning)

  • 每一行扫描完成,扫描线会切换到下一行起点,这个切换过程叫做水平消隐,简称 hblank(horizontal blank interval),并发送水平同步信号(horizontal synchronization,又称行同步)

  • 依次类推,整个屏幕(一个垂直周期)扫描完成后,显示器就可以呈现一帧的画面

  • 屏幕最后一行(一个垂直周期)扫描完成后,需要重返左上角初始位置,这个过程叫垂直消隐,简称 vblank(vertical blank interval)

  • 扫描线回到初始位置之后,准备扫描下一帧,同时发出垂直同步信号(vertical synchronization,又称场同步)。


image.png


显卡帧率


表示GPU在1s中内可以渲染多少帧到buffer中,单位是fps,这里要理解的是帧率是一个动态的,比如我们平时说的60fps,只是1s内最多可以渲染60帧,假如我们屏幕是静止的,则GPU此时就没有任何操作,帧率就为0.


屏幕刷新频率


屏幕刷新频率:屏幕在1s内去buffer中取数据的次数,单位为HZ,常见屏幕刷新率为60HZ。屏幕刷新率是一个固定值和硬件参数有关。也就是以这个频率发出 垂直同步信号,告诉 GPU 可以往 buffer 里写数据了,即渲染下一帧。


屏幕刷新机制演变过程


单buffer


GPU和显示器共用一块buffer


screen tearing 屏幕撕裂、画面撕裂


当只有一个buffer时,GPU 向 buffer 中写入数据,屏幕从 buffer 中取图像数据、刷新后显示,理想的情况是显卡帧率和屏幕刷新频率相等,每绘制一帧,屏幕显示一帧。而实际情况是,二者之间没有必然的大小关系,如果没有同步机制,很容易出现问题。

当显卡帧率大于屏幕刷新频率,屏幕准备刷新第2帧的时候,GPU 已经在生成第3帧了,就会覆盖第2帧的部分数据。

当屏幕开始刷新第2帧的时候,缓冲区中的数据一部分是第3帧数据,一部分是第2帧的数据,显示出来的图像就会出现明显的偏差,称为屏幕撕裂,其本质是显卡帧率和屏幕刷新频率不一致所导致。


双buffer


安卓4.1之前

基本原理就是采用两块buffer。

GPU写入的缓存为:Back Buffer

屏幕刷新使用的缓存为:Frame Buffer

因为使用双buffer,屏幕刷新时,frame buffer不会发生变化,通过交换buffer来实现帧数据切换。
什么时候就行buffer交换呢,当设备屏幕刷新完毕后到下一帧刷新前,因为没有屏幕刷新,所以这段时间就是缓存交换的最佳时间。

此时硬件屏幕会发出一个脉冲信号,告知GPU和CPU可以交换了,这个就是Vsync信号,垂直同步信号。
不可否认,双缓冲可以在很大程度上降低screen tearing错误,但是呢,还是会出现一些其他问题。


Jank 掉帧


如果在Vsync到来时back buffer并没有准备好,就不会进行缓存的交换,屏幕显示的还是前一帧画面,即两个刷新周期显示的是同一帧数据,称为Jank掉帧。


image.png
发生jank的原因是:在第2帧CPU处理数据的时候太晚了,GPU没有及时将数据写入到buffer中,导致jank的发生。

CPU处理数据和GPU写入buffer的时机比较随意。


Project Butter 黄油工程


安卓4.1
系统在收到VSync信号之后,马上进行CPU的绘制以及GPU的buffer写入。最大限度的减少jank的发生。


image.png
如果显卡帧率大于屏幕刷新频率,也就是屏幕在刷新一帧的时间内,CPU和GPU可以充分利用刷新一帧的时间处理完数据并写入buffer中,那么这个方案是完美的,显示效果将很好。


image.png
由于主线程做了一些相对复杂耗时逻辑,导致CPU和GPU的处理时间超过屏幕刷新一帧的时间,由于此时back buffer写入的是B帧数据,在交换buffer前不能被覆盖,而frame buffer被Display用来做刷新用,所以在B帧写入back buffer完成到下一个VSync信号到来之前两个buffer都被占用了,CPU无法继续绘制,这段时间就会被空着,于是又出现了三缓存。


三buffer


image.png
最大程度避免CPU空闲的情况。


Choreographer


系统在收到VSync信号之后,会马上进行CPU的绘制以及GPU的buffer写入。在安卓系统中由Choreographer实现。



  • 在Choreographer的构造函数中会创建一个FrameDisplayEventReceiver类对象,这个对象实现了onVSync方法,用于VSync信号回调。

  • FrameDisplayEventReceiver这个对象的父类构造方法中会调用nativeInit方法将当前FrameDisplayEventReceiver对象传递给native层,native层返回一个地址mReceiverPtr给上层。

  • 主线程在scheduleVsync方法中调用nativeScheduleVsync,并传入2中返回的mReceiverPtr,这样就在native层就正式注册了一个FrameDisplayEventReceiver对象。

  • native层在GPU的驱使下会定时回调FrameDisplayEventReceiver的onVSync方法,从而实现了:在VSync信号到来时,立即执行doFrame方法。

  • doFrame方法中会执行输入事件,动画事件,layout/measure/draw流程并提交数据给GPU。

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

Android边框裁切的正确姿势

前言 今天写什么呢,没有太好的思路,就随便写一些细节的点吧。 平时我们都会接触到的一个东西就是设置view的边缘为圆角,因为默认的直角比较难看,这个是涉及比较多的场景,其它当然也有一些场景需用到非正常边框的情况,也需要裁切。 1. 设置圆角边框 一般我们怎么设...
继续阅读 »

前言


今天写什么呢,没有太好的思路,就随便写一些细节的点吧。

平时我们都会接触到的一个东西就是设置view的边缘为圆角,因为默认的直角比较难看,这个是涉及比较多的场景,其它当然也有一些场景需用到非正常边框的情况,也需要裁切。


1. 设置圆角边框


一般我们怎么设置圆角边框的

<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#ffffff" />
<stroke
android:width="0.8dp"
android:color="#ffffff" />

<corners android:radius="10dp" />
</shape>

这是我们比较常做的设置边框圆角的操作,有没有过这样去设置会不会出问题?其实这样的操作只不过是改变背景而已,它可能会出现内部内容穿透的效果。


2. 使用ClipToOutline进行裁切


这个是android 5.0之后提出的方法,具体的操作是这样

public static void setRoundRect(View view) {
try {
view.setOutlineProvider(new ViewOutlineProvider() {
@Override
public void getOutline(View view, Outline outline) {
outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), 10);
}
});
view.setClipToOutline(true);
} catch (Exception e) {
e.printStackTrace();
}
}

可以看出就是调用了view的setOutlineProvider方法和setClipToOutline方法。看这个ViewOutlineProvider,它的注释是

Interface by which a View builds its Outline, used for shadow casting and clipping.


能明显看出它就是为了处理阴影和裁切的。其中我们要设置的话,主要是设置Outline outline这个对象,我们可以看看它所提供的方法


setRect


先随便拿一张图片表示原本的显示效果来做对比


lQDPJxak951EiLzNArPNBBuwlGgcKIdKCsUD55urdoAVAA_1051_691.jpg_720x720q90g.jpg


调用setRect给原图进行边缘裁切

outline.setRect(view.getWidth()/4, view.getWidth()/4, view.getWidth()/4 *3, view.getHeight()/4 * 3);

得到这样的效果,注意,我的原效果是贴边的,这些裁切之后发现是不贴边的


lQDPJxbScR-Lx7zNAtzNBDiwSuSMvAe6JokD55uq3UAVAA_1080_732.jpg_720x720q90g.jpg


setRoundRect的效果和setRect一样,就是多了一个参数用来设置圆角。这里就不演示了


setOval
调用setOval,它的传参和setRect一样

outline.setOval(view.getWidth()/4, view.getWidth()/4, view.getWidth()/4 *3, view.getHeight()/4 * 3);

可以看到效果


lQDPJw5Lp5oLqLzNAqnNBDiwjzj9J14HHxID55ur-8AVAA_1080_681.jpg_720x720q90g.jpg


发现再裁切尺寸的同时并且把图片切成圆形,我记得很早之前,还没毕业时做圆形头像的时候还需要引用别人的第三方,现在5.0之后直接调这个就行,多方便。当然现在头像都是用Glide来做。


setAlpha和setConvexPath也一样,etAlpha是设置透明度,setConvexPath是设置路径,路径和自定义view一样用Path,我这边就不演示了


3.总结


Outline相对于shape来说,是真正的实现边缘裁切的,shape其实只是设置背景而已,它的view的范围还是那个正方形的范围。最明显的表现于,shape如果内容填满布局,会看到内容超出圆角,而Outline不会。当然如果你shape配合padding的话肯定也不会出现这种情况。


使用Outline也需要注意,一般的机子会在当范围超过圆之后,会一直显示圆。比如你设置radius为50是圆角的效果,但是甚至成100已经是整个边是半圆,这时你设200会发现还是半圆,但是在某些机子上200会变成圆锥,所以如果要做半圆的效果也需要去计算好radius


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

Android与JavaScript通信(相互回调)

简述      在移动应用开发中,Android和JavaScript是两个常用的技术栈。Android作为主流的移动操作系统,而JavaScript则是用于网页前端和跨平台开发的脚本语言。为了实现更好的用户体...
继续阅读 »

简述


     在移动应用开发中,Android和JavaScript是两个常用的技术栈。Android作为主流的移动操作系统,而JavaScript则是用于网页前端和跨平台开发的脚本语言。为了实现更好的用户体验和功能扩展,Android与JavaScript之间的通信变得至关重要。本文将介绍Android与JavaScript之间的回调通信技巧


通信基础




  1. 通过 WebView 进行通信


    Android 的 WebView 组件提供了 evaluateJavascript 方法,该方法可以执行 JavaScript 代码并获取返回结果。我们可以利用这一特性实现 Android 和 JavaScript 之间的通信。具体实现步骤如下:



    1. 在 Android 代码中,通过 evaluateJavascript 方法执行 JavaScript 代码。




  2. 使用 JavaScriptInterface 实现通信


    我们可以使用 JavascriptInterface 接口实现JavaScript 与 Android 的通信。具体实现步骤如下:



    1. 在 Android 代码中创建一个类,实现 JavascriptInterface 接口。

    2. 在该类中定义需要供 JavaScript 调用的方法,并添加 @JavascriptInterface 注解。

    3. 在 JavaScript 中通过 window.AndroidFunction 对象调用 Android 代码中的方法,实现通信,其中AndroidFunction是注册JavascriptInterface时指定的, 如下所示:
      webView.addJavascriptInterface(new Object() {
      @JavascriptInterface
      public void jsCallback(String message) {
      // ...
      }
      }, "AndroidFunction");





Android调用JavaScript函数




  1. 忽略返回值

        val webView = findViewById<WebView>(R.id.webView)

    webView.evaluateJavascript("jsFunction('message')", null)




  2. 获取返回值

        val webView = findViewById<WebView>(R.id.webView)

    webView.evaluateJavascript("jsFunction('message')") { result ->
    Log.e("TAG", result)
    }




JavaScript调用Android函数

// 在JavaScript中调用Android函数,并传递参数
function callAndroidFunctionWithParameter() {
var message = "Hello from JavaScript!";
AndroidFunction.jsCallback(message);
}

在上述示例中,JavaScript函数callAndroidFunctionWithParameter()将参数message传递给Android函数jsCallback()


双向回调通信




  1. Javascript传递回调给Android


    上述的 AndroidFunction.jsCallback(message)方式目前只能传递字符串,如果不做特殊处理,是无法执行回调函数的, 执行 AndroidFunction.jsCallback(message)时,在Android中获取到的是字符串,这时将回调函数转换成回调令牌,然后通过令牌执行相应的回调函数,步骤如下:




    1. 在js中将回调函数转换成唯一令牌,然后使用map以令牌为key存储回调函数,以便让Android端根据令牌来执行回调函数

      const recordCallMap = new Map<string, Function>();

      // Android端调用
      window.jsbridgeApiCallBack = function (callUUID: string, callbackParamsData: any) {
      // 通过ID获取对应的回调函数
      const fun = recordCallMap.get(callUUID);
      // 执行回调 `callbackParamsData`回调函数的参数 可有可无
      fun && fun(callbackParamsData);
      // 执行完毕后释放资源
      recordCallMap.delete(callUUID);

      }

      function getUuid() {
      // 生成唯一ID
      return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/\[xy]/g, function (c) {
      var r = (Math.random() \* 16) | 0,
      v = c == 'x' ? r : (r & 0x3) | 0x8;
      return v.toString(16);
      });
      }

      // 统一处理调用Android的方法
      export function androidCall(funname: string, funData: string, fun: Function) {

      if (!AndroidFunction) {
      return;
      }

      const dataObj = {
      funName: funname,
      funData: funData
      }

      if (typeof fun === "function") {
      const funId = getUuid();
      Object.assign(dataObj, { funId });
      recordCallMap.set(funId, fun);
      }

      AndroidFunction.jsCall(JSON.stringify(dataObj))

      }



    2. 在Android端注册JavascriptInterface统一让js调用

      class JsCallbackModel {
      lateinit var funData: String

      lateinit var funId: String

      lateinit var funName: String
      }

      abstract class JsFunctionCallBack(var funId: String) {
      abstract fun callback(respData: String?)
      abstract fun callback()
      abstract fun callback(respData: Boolean)
      }

      class JavaScriptCall(private val webView: WebView) {
      private fun jsCall(funName: String, funData: String, jsFunction: JsFunctionCallBack) {
      when(funName) {
      "screenCapture" -> {
      screenshot(funData.toInt(), jsFunction)
      }
      }
      }

      @JavascriptInterface
      fun jsCall(data: String) {
      // 将json字符串解析成kotlin对象
      val gson = GsonBuilder().create()
      val model = gson.fromJson(data, JsCallbackModel::class.java)

      // 如果不存在函数名称 则忽略
      if (model.funName == "") {
      return
      }

      val jsFunction: JsFunctionCallBack = object : JsFunctionCallBack(model.funId) {
      override fun callback(respData: String?) {
      if (webView == null) {
      return
      }
      if (funId.isEmpty()) {
      return
      }

      content.webView.post {
      webView.evaluateJavascript("jsBridgeApiCallBack('$funId', "$respData")", null)
      }
      }

      override fun callback() {
      if (funId.isEmpty()) {
      return
      }

      content.webView.post {
      webView.evaluateJavascript("jsBridgeApiCallBack('$funId')", null)
      }
      }

      override fun callback(respData: Boolean) {
      if (funId.isEmpty()) {
      return
      }

      content.webView.post {
      webView.evaluateJavascript("jsBridgeApiCallBack('$funId', '$respData')", null)
      }
      }
      }

      jsCall(model.funName, model.funData, jsFunction);
      }

      private fun screenshot(quality: Int, jsFunction: JsFunctionCallBack) {
      // 执行逻辑...
      // 执行js回调
      jsFunction("base64...")
      }
      }




    3. 在js中传递回调函数调用Android截图

      function screenshot(): Promise<string> {
      return new Promise(resolve => {
      androidCall("screenshot", (base64: string) => resolve(base64))
      })
      }
      screenshot().then(base64 => {
      })





  2. Android传递回调给Javascript


    原理跟Javascript传递回调给Android是一样的,具体实现如下:




    1. Android端

      class JavaScriptCall(private val webView: WebView) {
      companion object {
      val dataF: MutableMap<String, (callData: String) -> Unit> = mutableMapOf()

      fun jsCall(webView: WebView, funName: String, funData: String, funHandler: (callData: String) -> Unit) {
      val ctx: MutableMap<String, String> = mutableMapOf()

      ctx["funName"] = funName
      ctx["funData"] = funData
      val uuid = UUID.randomUUID().toString()
      ctx["funId"] = uuid
      dataF[uuid] = funHandler

      webView.post {
      val gson = GsonBuilder().create()
      val json = gson.toJson(ctx)
      webView.evaluateJavascript("jsCall('$json')", null)
      }
      }
      }

      @JavascriptInterface
      fun androidsBridgeApiCallBack(callUUID: String, callData: String) {
      val funHandler = dataF[callUUID];

      if (funHandler != null) {
      funHandler(callData)
      dataF.remove(callUUID)
      }
      }
      }



    2. Js端

      function doSome(funId, data, callback) {
      // 执行逻辑...
      // 执行Android的回调函数
      callback(funId, data)
      }

      function androidFunction(funId, respData: any?) {
      AndroidFunction.androidsBridgeApiCallBack(funId, respData)
      }

      function androidCallback(funId, funName, funData, fun) {
      switch (funName) {
      case 'doSome': {
      doSome(funId, funData, fun)
      }
      }
      }

      window.jsCall = function (json: string) {
      const obj = JSON.parse(json)
      const funName = obj['funName']
      const funData = obj['funData']
      const funId = obj['funId']

      if (!funName) {
      return
      }

      androidCallback(funId, funName, funData, androidFunction)
      }



    3. 在Android中传递回调函数调用js的doSome

      JavaScriptCall.jsCall(webView, "doSome", "data") { callParam ->
      Log.e("tAG", "回调参数: $callParam")
      }

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

🚗我毕业/转行了,怎么适应我的第一份开发工作?

🚗我毕业/转行了,怎么适应我的第一份开发工作? 嗨,大家好!这里是道长王jj~ 🎩🧙‍♂️ 最近一直在回顾自己的职业生涯,思考自己在这几年里做了什么、成为了什么,实现了什么,失去了什么。虽然一路上充满了挫折和困难,但我其实非常感恩最近几年自己的成长和突破。 在...
继续阅读 »

🚗我毕业/转行了,怎么适应我的第一份开发工作?


嗨,大家好!这里是道长王jj~ 🎩🧙‍♂️


最近一直在回顾自己的职业生涯,思考自己在这几年里做了什么、成为了什么,实现了什么,失去了什么。虽然一路上充满了挫折和困难,但我其实非常感恩最近几年自己的成长和突破。


在这几天的对职业生涯的思考中,我查阅了很多资料和观点,才有了之前那篇《🎖️怎么知道我的能力处于什么水平?我该往哪里努力?》的文章。


在那篇文章中,我从整个职业生涯的角度定义了开发人员可能会经历的各个阶段。


今天,我们来好好聊一聊,当我们因为各种原因,成为一名新晋专业开发者时,如何尽快适应这种变化。


对于那些从学生身份转变为专业开发者,或者是面临职业转行的人来说,希望我的文章可以给你们提供帮助和一些建议。


此外,如果你个人在职业生涯中已经超越了这个阶段(我相信在掘金的大部分人都是大佬了),请不要嫌弃我这篇文章初级,希望回顾一下仍然可能对你有所帮助。


当然,如果我的文章可以对你或其他处于类似情况的开发人员提供指导和帮助,那我会因为能够帮助到你和你的团队更加喜出望外!😄🚀


🤔转型成“初级开发工程师”,会遇到什么挑战?



这些观点源于:《🎖️怎么知道我的能力处于什么水平?我该往哪里努力?》




  1. 以前大部分时间可能是一个人写demo学习,但是现在意味着你需要适应团队协作,和小伙伴们一起完成任务。💪

  2. 以前一个人就能负责完一个不大的项目建设,但是现在项目的体量已经变得超级庞大,一个人根本搞不定!😱

  3. 现在你要学会进行跨领域沟通,就像是翻越一座高山,不仅要搞清楚别人的问题,还要让别人明白你的问题!💡

  4. 怎么在职场中向更高一级进发?就像是玩游戏一样,如何不断升级自己的技能,向着更高的目标冲刺!🚀


基于这些问题,接下来我们将逐一回答它们。


🤔我如何适应可能让我不开心的团队工作呢?


没错!在团队里合作肯定会有些不愉快的事情发生。想想看,世界上可不可能每个人都喜欢你呢?哈哈,当然不可能啦!你也不可能喜欢世界上各种各样的人。


所以,当你加入一个团队时,真的是进入了一个全新的领域!以前,你只需要和自己相处融洽,做错了就怪自己,做不到也只能责怪自己。但是现在,你需要接受别人的不完美,甚至要接受你自己可能把整个团队搞砸的事实。相信我,这真的是一件让人感到尴尬的事情。


我想,面对这些问题,我会鼓励你有意识地培养下面这两个方面的能力:😉💪


1. 学会有效沟通


当你参加各种开发流程中的会议时,一定要意识到会议的重点是什么。要不然这些会议就成了浪费你宝贵时间的活动,还不如在会议期间多写几行代码,多看几篇文章来得实际。


举个例子,每日晨会是每个敏捷团队都有的例会。在这个时候,如果你需要告诉你的领导你今天在做什么,请千万不要深入研究你正在做的事情的细节。(比如,这个模块有个bug一直调不通,你试了很多种方法在会上详细说明)


相反地,因为每日站会的作用是团队之间了解进度和提出问题的会议,你只需要简单说明你在做什么任务,需要什么帮助即可:



“我要修复移动端APP水印失效的问题,但是这个功能我不是很懂,找不到关键代码段,需要有人能帮我梳理一下。”



我在晨会时真的遇到过很多开发人员控制不住自己的发散思维,会讲到他们正在开发的技术细节。如果没有一个有权威的人及时中止这些无意义的展开,真的会浪费大家的时间和精力!


所以如果有可能的话,请花时间多了解对方的需求,让每次沟通都变得简单快速。这样做会让你看起来很干练且专业。😄👍


2. 克制情绪,有意识地锻炼自己的情商


我们都会有克制不住自己情绪的时候,特别是当生活不太顺心时,比如游戏五连败之后,第二天还要上班的情况下。


尤其是当你运气不好,恰好触碰到某人的敏感点,或者开个小小的玩笑,这就有可能无意中挑起双方的争端。


这时候,就是考验一个人情商的时候了。我建议你能尽力地鼓励你的同事,你的团队成员。当他们表现出不好的情绪时,有意识地用理解和支持的态度去面对。当你真的能做到这一点,你就为未来承担更大管理职责做好了最重要的情商储备。


如果你和你的同事确实遇到问题,请从问题本身开始分析,一个一个地解决事情而不是与人对立。要清楚你解决的是问题,而不是与你有冲突的那个人。



当然,如果你真的遇到了难以沟通的团队成员,请顺其自然,让时间或者等待Leader来解决这个问题。我并不倡导无休止的退让。



如果因为这些不可避免的摩擦影响了整个团队的氛围很长一段时间。你只能祈祷你的Leader可以很好的解决这个问题。😄


🤔我怎么在一个我完全看不懂的项目中显得专业?


现在咱们聊聊专业领域的事情吧,我想也是你最关心的问题!


当你加入一个团队时,最大的挑战可能不是适应团队,而是面对一堆看不懂的代码!


刚开始的时候,你可能会觉得自己还行,毕竟学过编程语言,不太可能完全看不懂。


但很快你就会发现处理项目代码跟写小小的Demo程序很不一样!有时候逻辑跳转起来像个迷宫,过几年再回头看,还是一头雾水啊!😵


在团队里,你会面对一个庞大的代码库,可不是一两年就能完全掌握的。又能能用两年时间彻底精通你公司的项目吗?有这种人吗?🤔


所以,当你接到第一个任务,投入开发的时候,别担心,你不是一个人陷入迷茫,我也是一样的。我们都曾经历过那个阶段,只需要时间去适应,一定不要觉得自己不行!


如果你想问有什么可以实践的方法论,我想我能给你的建议是:在进行需求评审的时候,写下所有你可能不理解的内容!


比如:




  • 哪些数据库需要我特别关注?🔍

  • 我需要关注哪些代码文件啊?📂

  • 项目里有没有类似的代码实践可以帮我解决这个需求?🤔

  • 需求里有没有没有说清楚的问题或者一些不够明确的要求呢?🌪️



当你问这些问题的时候,你的小伙伴一定会对你刮目相看哦!相信我,这可是难得一见的专业和细心的表现。(可不是每个人都能做到的!)


这些你整理出来的内容,在你整个开发过程中会给你巨大的支持。当别人想不起来一年都做了些什么事情的时候,这些记录可以让你在年终总结的时候,脱颖而出,变成你宝贵的项目经验! 📚


💪写一份成就清单,为未来赋能


如果可以的话,我建议你保留一份完成的需求任务日记或电子表格。而且,一定一定要记录下你取得的每一个新成就!


把成长当成一个游戏的过程!看着经验值一点点涨上去,发现世界里的新奇事物。


当我这样做的时候,每次一个新成就,都会我的幸福感简直爆棚!我会迫不及待地想要完成下一个新成就。



  • “我设计和开发了一个超棒的用户成就组件库!它提供了一致的界面风格和交互效果,减少了80%的代码冗余!” 🎉

  • “我成功实现了功能X,移动端新用户流入增长了整整120%!” 📈

  • “我掌握了localStorage,并巧妙地用它给功能X实现了用户本地数据缓存!” 💾


有时候,在过程中你可能觉得自己有些傻,但是等有一天你想回顾过去的1年或3年经历,或者当你又被互联网世界”卷“到,对自己失望的时候,你会发现原来那看似平凡无奇的职场生活中,你一直在默默成长。


当然,最重要的是,当你迎接人生中新的阶段,需要换工作或重新制作简历时,这些记录将带来巨大的帮助。你会惊讶地发现,你的项目经历比你想象的要丰富得多! 🌟📝


🥨学习如何提升自己的level



“Never Memorize Something That You Can Look Up” – Albert Einstein


”永远不要记住你可以查到的东西“ - 阿尔伯特·爱因斯坦



大部分的小伙伴们都很热衷于收藏那些像是"100个超牛JavaScript函数"或者"Vue3实用API大集合"这样的文章。


嗯,这真的很棒!我觉得这是个很好的习惯。比起死记硬背那些八股文,这样做要强太多了。因为只要你在需要的时候能找到它们,那它们就是你的宝藏了。


我是绝对反对八股文的开发者,但有时候,面对大环境,我们可能不得不做些妥协。为了面试,我们得背诵各种JavaScript高级函数和Vue生命周期都有什么用。


不过,如果你有时间想要提升自己,有空闲去思考进步的话,我建议你加强阅读。很多很多的阅读!


试试去阅读一些超出你舒适区和当前理解范围的书籍和课程吧!比如计算机组成原理、设计模式,或者现在非常热门的人工智能领域的基础书籍。


这样做可以拓宽你的思维,让你的知识领域更广阔。最终,你会逐渐掌握阅读的技巧,面对这些全新的知识领域时,能更快、更准确地找到重点并掌握它们。


这个过程会很痛苦,因为可能400个字的内容你都需要花一周的时间去消化。


但是只要你坚持下去,未来的你一定会与普通程序员拉开差距。


因为让你有价值的不是那些沉闷的八股文,而是你脑海中关于各个领域的认知和解决方案。


如果你能迅速解决别人不知道怎么办的问题,那你就是人群中那个最了不起的人,很多人会跟着指示做很多的需求,但是他们并不能形成解决方案。


解决方案,才是真正证明你实力的硬通货! 💪


🥩如果有机会,积极加入开发者社区


如果在我刚毕业的时候有人提醒我这个事情,我一定会非常感激他。


回顾我的职业生涯,我最后悔的一件事就是没有早点参与到开发者社区,无论是GitHub还是现在的掘金社区。


当你真正活跃在社区中,试图融入他们,你会结识新朋友,找到可以指导你的导师,让你能够突破当前的认知。你的未来将逐步变得清晰起来。


在你喜欢的领域中,找出谁适合成为你在特定领域发展的导师型开发者朋友。然后关注他们,开始阅读和评论他们的文章和作品,与他们展开讨论,加入他们目前的方向和事业!


最终,借助这个社区,你完全有可能进入职业生涯中一个全新的维度:




  • 你可以为一个开源项目做出属于自己的贡献(甚至是文档方面) 🚩

  • 与社区的开发者合作,发起一个全新的开源项目

  • 你将拥有自己高质量的小圈子💒



这样做会让你收获很多。你不仅能够积累宝贵的经验,还能与行业内优秀的开发者们互动,共同进步!🚀😄




🎉 你觉得怎么样?这篇文章可以给你带来帮助吗?如果你有任何疑问或者想进一步讨论相关话题,请随时发表评论分享您的想法,让其他人从中受益。🚀✨


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

谈知识付费与专家建议

我最近可能是病了,或者是心态出了问题。 我经常会听一些付费类的课程,也会在微信上阅读互联网大牛发表的文章。 以前听,还觉得挺有道理的。我甚至绘声绘色地转述给别人。可是后来,怎么听,怎么想怼他们。 可能是我的觉悟降低了,眼界不够。也可能是他们为了证明自己的观点,...
继续阅读 »

我最近可能是病了,或者是心态出了问题。


我经常会听一些付费类的课程,也会在微信上阅读互联网大牛发表的文章。


以前听,还觉得挺有道理的。我甚至绘声绘色地转述给别人。可是后来,怎么听,怎么想怼他们。


可能是我的觉悟降低了,眼界不够。也可能是他们为了证明自己的观点,刻意扭曲一些东西


所以,我打算摆出来,也让读者们帮我分析一下。


首先是我在听罗胖的跨年演讲时,他讲述疫情让大家挣不到钱,他鼓励大家改行,并讲了很多改行成功的故事。


他说南京有一位胡先生,是天文学博士。原本在大学教书,后来他转行去干装修了。还是那种带着劳保手套,搬砖动瓦的那种装修工作。


罗胖说,有人会觉得他得从头开始学吧?实际上不会。


他14年的天文学积累,形成的方法论,可以转到装修上。甚至还可以吊打普通装修工人,这叫降维打击。


首先,他作为天文学家,具有还原宇宙的能力。那么,他可以还原毛坯房的效果图。


这时候,我还能听得进去,觉得有点道理。


随后他说,作为科学家,还具有研究能力。两种建筑材料能不能混合,普通的装修工人靠得是经验。但是科学家,可以分析成分,采用科技手段进行判断。


听到这里,我有点坐不住了。他不是个天文学家吗?怎么又成了材料学家了?两个学科之间没有壁垒吗?


另外,关于乳胶漆和腻子粉混合的后果,科学实验室的数据,同装修工人传下来的经验,效果到底有多大差别?成本又有多大差别?


接下来,罗胖说的,把我的这种情绪推上顶峰。


罗胖说,天文学家转行装修工人,还解决了装修行业的一个痛点。



传统的装修流程,设计、采购、施工是分开的,出了问题会相互扯皮。



但是,天文学家与普通人相比,具有高度的统筹全局的能力、高效的沟通能力,他能把这三个流程都打通,马上就出类拔萃了。反而干得更好。


因此,他给大家的启发是:重要的不是身份,而是内核


我没有干过装修,但我找人装修过我家房子。因此经验很浅。就我个人了解,装修的采购是讲渠道的。有些行业老手拿货很便宜,比我自己去买要便宜很多。因此才有各司其职的划分,不是打不通,而是各自更专业。


他说身份不重要,但是在我的生活中,我感觉身份还是很重要的。甚至自考本科和统招本科,区别都很大。可能还是我境界太低了。


我听完上面的故事,没有再继续往下听。


我感觉可能是自己出问题了。我是无名小卒。人家的课程可是千万用户呢?其中,还不乏好多商界大老板。老板肯定是比我聪明的。


今天,我又从微信公众号看到一篇文章。


写这篇文章的是IT行业的大佬。我不认识。但是他的简介很厉害:国内知名IT管理专家、某甲创始人、某乙创始人,畅销书《xx之道》作者,曾担任大厂的各种总、各种O。


他写的体裁是关于人工智能方面的,题目大意是ChatGPT骗了全世界、你们都被ChatGPT骗了。


因为我就写人工智能代码,也写过关于AIGC原理解析的文章


我就点进去看,一看我就想怼。


他列举了很多条关于人工智能的谎言,然后自己再说出了真相。他揭穿谎言,警醒世人。


他指出现在流传的一个谎言:人工智能会代替人类的工作


他给出的真相是:抢饭碗的永远不是AI,而是会用AI的人


他也讲了一个故事。



原来有很多电话客服人员,负责推销、查询等工作。但是,现在90%的这类工作,已经被AI机器给替代了。那你说原来的客服都失业了吗?没有!你们不知道的是,他们有的转行做了AI训练师。也就是教AI怎么打电话。


你看,客服掌握了AI,重新上岗了。


而你,不愿意学习AI,只能被淘汰。



我想,这不还是人类的工作被替代了吗?只是因为AI的出现,衍生了一些周边岗位。


原来停车场收钱抬杆的大爷,因为车牌识别外加移动支付,他们下岗了。高速口的收费员,因为ETC的出现,也下岗了。


这确实是科技代替了人工。没见哪个大爷跑去教AI如何抬杆儿,就算有,能用得了那么多大爷吗?


AI客服出现的目的,就是提高效率,节省成本。花钱搞了AI客服,那些人工客服还会保留吗?要留也只能留少数一部分人。而大多数人会因为AI的出现,不得不重新选择工作。人工智代替人类的一些工作,就是时代的发展。


这位大佬,最后总结:淘汰你的永远不是这个时代,是你自己。不管时代如何,你不努力,被淘汰很正常


我的思想又活跃了,很想怼。


我感觉,我们的生活深受时代的影响。就像是蚂蚁的生活,也会受大雨、野火、巨兽的踩踏影响一样。


蚂蚁努力积累食物,突然有一天,某个小孩朝着洞穴撒了一泡尿,或者扔了一个鞭炮,蚂蚁的努力就白费了。


这种破坏和是否努力是没有关系


我估计,专家也会怼我:



你早应该有预判,提早发现熊孩子的破坏行为。


从他今天在家多喝了水,还买了鞭炮开始,就该猜到他的这类行为。


这叫见微知著,未雨绸缪。其实,归根到底,还是你不够努力!



其实,他们说的对,没有错。错在哪里?难道不应该努力吗?但是这些话,对你用处不大。


我发现,引发我想怼欲望的事情。大多都是在宣传一件事,那就是人的力量是无限大的。


关系不重要,环境不重要,行业不重要,你的努力最重要!


早年听陈安之的成功学,他说成为马拉松冠军是世上最简单的事情。只需要一句口诀,那就是当你跑不动、心肺难以支撑的时候,大声喊出:我没有心脏,我没有心脏!


我当时想,没有心脏不就倒下了吗?


这是正能量。正能量永远没有错。


你想赚大钱,请了一个大师。


大师传授你三个独家秘诀:



第一,要有强烈赚钱欲望;


第二,要对赚钱抱有持之以恒的行动;


第三,赚到钱时一定不要骄傲,继续赚。



你感觉很有道理,但是收入依然没有什么改变。可能是因为你不够努力。


你想学舞蹈,找了一个大师,大师说,想成为舞蹈高手,首先要有强烈跳舞的欲望,第二要持之以恒的行动,第三要戒骄戒躁。


还是一样的话。


请问,他们错了吗?没错。你成功了吗?没有。


听过马三立的一个相声段子。



说有个穷人,穷的揭不锅了。实在没办法,就去摆摊。他摆了一个算命的摊子,也兼职修鞋。


来算命的人少,挣钱多。来修鞋的人多,挣钱少。两者互补。


有一个人来算命,想发财。这个算命的就指点他去东北,那里能发财。


然后,又跟他说,去东北得走不少路,加固下鞋子才能成功抵达。



我想,现在很多课程可能也是这样。


他们一方面宣传你的遭遇跟环境无关,另一方面强调是你不够努力,让你焦虑。同时,他还有课程,你买了课程就算是努力了


至于最后的结果,他们是不关心的。不成功,可能是你还不够努力。


你自己的事情,需要自己去思考。每个人的境遇和你不一样。


那些行业大佬们,可能没有去小摊上吃过油条,也没有逛过装修市场去买瓷砖。甚至他们的父辈也没有过类似经历。他们更精于顶层建筑。但是有时候,他必须要写相关的文章。于是,他们举的例子,更多的是一种导向,是劝人向善。


而我们,也不要过于盲从他们的指导。他们的意见参考一下就好了。


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

程序员你为什么迷茫

今天在知乎收到一个问题:「程序员你为什么迷茫?」,看到这问题时突然间「福灵心至」想写点什么,大概是好久没写这种「焦虑文」,想想确实挺久没更新《程序员杂谈》 这个专栏了,那就写点「找骂」的东西吧。 其实也不算「贩卖焦虑」,主要是我一直不卖课也不做广告,就是纯...
继续阅读 »

今天在知乎收到一个问题:「程序员你为什么迷茫?」,看到这问题时突然间「福灵心至」想写点什么,大概是好久没写这种「焦虑文」,想想确实挺久没更新《程序员杂谈》 这个专栏了,那就写点「找骂」的东西吧。




其实也不算「贩卖焦虑」,主要是我一直不卖课也不做广告,就是纯粹想着写点东西,不过如果你看了之后觉得「焦虑」了,那也没有「售后服务」。



当我看到「程序员你为什么迷茫?」这个问题的时候,我第一想法居然是:大概是因为预期和结果不一致


现在想想确实好像是这样,在步入职场之前,你一直以为程序员是一个技术岗,是用一双手和一台电脑,就可以改变世界的科技岗位,是靠脑吃饭,至少在社会地位上应该是个白领。


但是入职后你又发现,明明是个技术岗位,干的却是体力活,别人在工地三班倒,而你是在电脑前 996,唯一庆幸的是你可以吹着空调,目前的收入还挺可观。



但是现在看来,程序员的职业生涯好像很短,农民工少说可以干到 40 多,为什么程序员 35 就需要毕业再就业?明明一心一意搬砖,说好奋斗改变人生,最后老板却说公司只养有价值的人,而有价值的人就是廉价而又精力充沛的年轻人


那时候你也开始明白,好像努力工作 ≠ 改变人生,因为你努力改变的是老板的人生,工作带来的自我提升是短暂的,就像工地搬砖,在你掌握搬砖技巧之后,剩下的都只是机械性的重复劳动,机械劳动的勤奋只会带来精神上的奋斗感,并不致富,而对比工地,通过电脑搬砖需要的起点更高,但是这个门槛随着技术成熟越来越低,因为搜索引擎上的资源越来越多,框架和社区越来约成熟,所以🧱越来越好拿,工价也就上不去了,甚至已经开始叫嚣用 AI 自动化来代替人工。



其实对于「老人」来说,这个行业一开始不是这样,刚入行的时候到处在抢人,他们说这是红利期,那时候简历一放出来隔天就可以上岗,那时候的老板每天都强调狼性,而狼需要服从头领,只有听话的狼才有肉吃,所以年轻时总是充满希望,期待未来可以成为头狼,也能吃上肉住炕头。


虽然期间你也和老板说过升职加薪,但是老板总是语重心长告诉大家:



年轻人不好太浮躁,你还需要沉淀,公司这是在培养你,所以你也要劳其筋骨,饿其体肤,才能所有成就,路要一步一走,饭要一步一吃,总会有的。



当然你可以看到了一些人吃到了肉,所以你也相信自己可以吃到肉,因为大家都是狼,而吃到肉的狼也在不停向你传递吃肉经验:



你只需要不停在电梯里做俯卧撑,就可以让电梯快一点到顶楼,从而占据更好的吃肉位置,现在电梯人太多了,你没空间做俯卧撑,那就多坐下蹲起立,这样也是一种努力。




直到有一天,公司和你突然和你说:



你已经跟不上公司的节奏了,一个月就请了三次病假,而且工作也经常出错,所以现在需要你签了这份自愿离职协议书,看在你这么多年的劳苦功高,公司就不对你做出开除证明,到时候给其他人也是说明你是有更好机会才离职的,这样也可以保全你的脸面。



而直到脱离狼群之后你才明白,原来沉淀出来的是杂质,最终是会被过滤掉,电梯空间就那么点,超重了就动不了,所以总有人需要下来


所以你回顾生涯产生了疑惑和迷茫:程序员不是技术岗位吗?作为技术开发不应该是越老越值钱吗?为什么经验在 3-5 年之后好像就开始可有可无,而 35 岁的你更是被称为依附在企业的蛀虫,需要给年轻人让路。


回想刚入行那会,你天天在想着学习技术,无时无刻都在想着如何精通和深入,你有着自己的骄傲,想着在自己的领域内不断探索,在一亩三分地里也算如鱼得水,但是如今好像深入了,为什么就开始不被需要了


回过头来,你发现以前深入研究的框架已经被替代,而当年一直让他不要朝三暮四嚼多不烂的前辈,在已经转岗到云深不知处,抱着一技之长总不致于饿死是你一直以来的想法,但是如今一技之长好像补不上年龄的短板。



如今你有了家庭,背负贷款,而立之年的时候,你原以为生涯还只是开始,没想到早已过了巅峰,曾经的你以为自己吃的技术饭,做的是脑力活,壮志踌躇对其他事务不屑一顾,回过头来,如今却是无从下手,除了在电脑对着你熟悉的代码,你还能做什么?放下曾经的骄傲,放下以往的收入去吃另一份体力活?



但是不做又能怎样?提前透支的未来时刻推着你在前行,哪怕前面是万丈深渊。



所以以前你认为技术很重要,做好技术就行了,但是现在看,也许「技术」也只是奇技淫巧之一,以前你觉得生育必须怀胎十月,但是现在互联网可以让十个孕妇怀胎一月就早产,这时候你才发现,你也没自己想象中的重要。


所以你为什么迷茫?因为到了年龄之后,好像做什么都是错的,你发现其实你并没有那么热爱你的职业,你只想养家糊口,而破局什么的早就无所谓了,只要还能挣扎就行。



所以我写这些有什么用?没什么用,只是有感而发,大部分时候我们觉得自己是一个技术人才,但是现在看来技术的门槛好像又不是那么的高,当技术的门槛没那么高之后,你我就不过是搬砖的人而已,既然是搬砖,那肯定是年轻的更有滋味


所以,也许,大概,是不是我们应该关心下技术之外的东西?是不是可以考虑不做程序员还能做什么?35 岁的人只会越来越多,而入行的年轻人也会越来越多,但是近两年互联网的发展方向看起来是在走「降本增效」,所以这时候你难道不该迷茫一下?


程序员是迷茫或者正是如此,我们都以为自己是技术人,吃的是脑力活,走的是人才路,但是经过努力才知道,也许你的技术,并没有那么技术。


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

思考 | 闲话工作

工作五年有余,似有感悟,可是每每提笔时,脑袋又一片混沌。然而胸中总有些东西不吐不快,如鲠在喉,如芒在背。尤其去年年末和一位好友聊至深夜,席间的一番话令我思绪万千,怀念起曾经那个稚嫩青涩的我。 我想是时候该记录一些东西,哪怕这些东西是混乱的,潦草的。这些内容看似...
继续阅读 »

工作五年有余,似有感悟,可是每每提笔时,脑袋又一片混沌。然而胸中总有些东西不吐不快,如鲠在喉,如芒在背。尤其去年年末和一位好友聊至深夜,席间的一番话令我思绪万千,怀念起曾经那个稚嫩青涩的我。


我想是时候该记录一些东西,哪怕这些东西是混乱的,潦草的。这些内容看似和工作相关,实则背后都是人生的态度,价值的选择。它们写给路上的伙伴,更写给未来的自己。毕竟,在这慌乱走过的人生中,如果不留些印记,回头顾盼时将只剩茫然。


人生重要的是选择,还是努力?


这个问题的答案取决于你如何理解人生。当下流行的回答是“选择”,而我的回答是“努力”。


我打小生活在小镇上,属于严格意义上的小镇青年。条件谈不上优渥,但也算不上艰辛。或许得益于良好的家庭氛围,我的内心从未有过“短缺感”。所以那种“不上进”的小富即安的状态,在我看来颇为自足。穷人和富人都有烦恼,也都有欢乐。二者体验到的事物可能有差别,但通过事物体验到的快乐未必有差别。这就好比躺在豪车和草地上都不是快乐,但是躺下之后能够心无旁骛地哼着小曲,这是快乐的。


我选择“努力”,是因为我更欣赏把一件事做到极致的态度。这可能和我的父亲有关。他是一名柴油机修理工,从小教导我的就是“要么不做,要么做到极致”的人生态度。在他30多年的修理生涯里,一直引以为豪的就是自己的修理质量。别家修好的机器可以管一到两年,他修好的可以管三到五年。要说这其中有什么秘诀,说破天也只有“用心”二字。你说赛道的选择重要么?当然重要。但是各行各业都要有人做,“择一行,爱一行,精一行”的态度,才是更普适,更让每个人都心安的选择(我讨厌当下价值体系的一个主要原因就是它不普适,它只让少数人心安)。


我选择“努力”,也是希望做到尽量务实。因为看重选择,背后的心理通常是从众。什么是好选择?是媒体口中的?长辈口中的?还是内心深处的?社会风向变来变去,时间一久就容易浮躁。浮躁到后来只关注结果而忽略过程。不过事情的发展却很玄妙,你越是盯着某样东西,就越是得不到它。你紧盯着财富,往往也得不到财富。


我选择“努力”,还因为在我看来选择是自然发生的。太过刻意的选择,会有反向的作用。譬如,当一个人修行不足,内心不够坚定时,过高的荣誉和财富都会将他推向深渊。他会面临更多的诱惑,更多的苛责,处理不好便会失去原先宝贵的家庭和健康。因此,但行好事,莫问前程。


接着谈谈工作中的一些感受。


我日常的工作是处理稳定性问题。当一个问题暴露出来后,多数人在流程中唯一的作用就是施压和传话,他们只关心问题有没有解决,何时解决。如果这个问题不再出现,那么围观的人群将立即散开。它仿佛一个从未被打开过的黑盒,被一群人用灼热的目光炙烤后,又无情地抛弃。从始至终,没有人关心过它的前世今生,它的前因后果。当我们碰到新问题时总希望能举一反三,然而举一反三的前提一定是充分理解这个“一”。否则所有的问题在我们脑中只是漂浮的孤岛。可是现实就是很多问题被我们当成了黑盒。或许是手头的任务太重,或许是兴趣寥寥,总之愿意追本溯源,探究举一反三中的“一”的人很少。


这个话题再延展一下,国内很多所谓的科技公司都偏好商业运营而轻技术。它们眼中的技术只是实现业务的手段,或许它们更应该叫做“消磨时间公司”、“线上百货公司”或者“跑腿服务公司”。我知道这话一说完,肯定有人要站起来说:好个不懂商业,不懂管理的小白!懂也好,不懂也罢。然而我知道一个浅显的道理:一个饭馆想要生意好,就应该把精力重点放在提升口味上,而不是放在店面装修和营销广告上。同理,国内的科技想要真有起色,就要把技术当成技术,而不是其他目的的附庸。也唯有把技术当成技术,我们才能尊重技术。同样位于东亚的日本人,或许正是因为身上有比我们更为专注的匠人精神,才能在高端科技占有一席之地。


此外还有一个工作责任心的问题。


最低一个等级的责任心是“太极推手”,遇到棘手任务时想的是如何脱责。他们工作的目标就是将棘手问题转移出去,只处理那些简单不费脑且容易产生汇报成绩的活。这种人每个公司都不少,君不见那些人一天邮件好多封,一看内容全空空。


稍微高一个等级的责任心是“听命行事”,老板让我干我就干,至于干的质量和结果如何,抱歉,不在本人考虑范围内。他们每天没有主见地做事,付出了时间,但未必付出努力。


再稍微高一点等级的责任心是“自扫门前雪”,他们对自己所负责的领域普遍有了主人翁意识,在意别人对自己领地的评价,因此比较尽心尽责。不过这份尽责也止于自己领地的边界,越线的部分他是压根不会伸手。


再往上一个等级的责任心是“舍我其谁”,那些无人负责却又对公司有利的事情谁来干?这帮人往往冲在前面。他们对边界以外且力所能及的事情愿意伸手,也更享受为公司带来利益后的价值认同。


最高一个等级的责任心是“社会主人”,如果说之前的责任心还仅仅局限在工作和利益的维度,那么这个等级将会扩大到社会责任。他会思考自己的工作对社会所产生的影响,以及个人能力在其中起到的正面作用。


一个人选择什么样的责任心,通常是性格和价值观的产物。公司可以通过绩效这根大棒来影响员工的责任心,但从员工个人角度来看,它不会是决定性因素。


今日且谈到此,希望这篇文章也能开个先河,让自己在输出技术内容的同时多一些个人思考。当然,文中观点尚且稚嫩,还请各位看官不吝赐教。


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

Gradle 浅入浅出

环境介绍 OpenJDK 17.0.5 Gradle 7.6 示例代码 fly-gradle Gradle 项目下文件介绍 如果你的电脑安装了 gradle,可以使用 gradle init 去初始化一个新的 gradle 工程,然后使用电脑安装的 gra...
继续阅读 »

环境介绍



Gradle 项目下文件介绍


如果你的电脑安装了 gradle,可以使用 gradle init 去初始化一个新的 gradle 工程,然后使用电脑安装的 gradle 去执行构建命令。


但是每个开发电脑上的 gradle 版本不一样,为了统一构建环境,我们可以使用 gradle wrapper 限定项目依赖 gradle 的版本。

# 会生成 gradle/wrapper/* gradlew gradlew.bat
gradle wrapper --gradle-version 7.5.1 --distribution-type bin
.
├── build.gradle
├── gradle
│   └── wrapper
│   ├── gradle-wrapper.jar
│   └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── settings.gradle

gradlew 和 gradlew.bat


运行项目 wrapper 下定义的 gradle 去构建项目。


gradlew 是 macos 和 linux 系统下。


gradlew.bat 是 windows 系统下使用的。


wrapper


wrapper-workflow.png


wrapper 定义项目依赖那个版本的 gradle,如果本地 distributionPath 没有对应版本的 gradle,会自动下载对应版本的 gradle。

# gradle-wrapper.properties
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
# 如果是国内项目,只需要修改这个url 就可以提高下载速度
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

GRADLE_USER_HOME 没有配置的话,默认是 ~/.gradle


zipStoreBasezipStorePath 定义了下载的 gradle (gradle-7.6-bin.zip) 存储的本地路径。


distributionBasedistributionPath 定义下载的 gradle 解压的本地目录。


gradlew 实际是运行 gradle-wrapper.jar 中的 main 方法,传递给 gradlew 的参数实际上也会传递给这个 main 方法。


gradle-wrapper.jar 会判断是否下载 wrapper 配置的 gradle,并且将传递参数给下载的 gradle,并运行下载的 gralde 进行构建项目。


升级 wrapper 定义的 gradle 版本

./gradlew wrapper --gradle-version 7.6

settings.gradle

pluginManagement {
repositories {
maven {
url="file://${rootDir}/maven/plugin"
}
gradlePluginPortal()
}
plugins {
// spring_boot_version 可以在 gradle.properties 配置
id 'org.springframework.boot' version "${spring_boot_version}"
}
}
rootProject.name = 'fly-gradle'
include 'user-manage-service','user-manage-sdk'
include 'lib-a'
include 'lib-b'

settings.gradle 主要用于配置项目名称,和包含哪些子项目。


也可以用于配置插件的依赖版本(不会应用到项目中去,除非项目应用这个插件)和插件下载的


build.gradle


build.gradle 是对某个项目的配置。配置 jar 依赖关系,定义或者引入 task 去完成项目构建。


gradle.properties


主要用于配置构建过程中用到的变量值。也可以配置一些 gradle 内置变量的值,用于修改默认构建行为。

org.gradle.logging.level=quiet
org.gradle.caching=true
org.gradle.parallel=true
org.gradle.jvmargs=-Xms512m -Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8

org.gradle.jvmargs 用来配置 Daemon 的 JVM 参数,默认值是 -Xmx512m "-XX:MaxMetaspaceSize=384m"


当我们的项目比较大的时候,可能会由于 JVM 堆内存不足导致构建失败,就需要修改此配置。


org.gradle.logging.level 调整 gradle 的日志级别。参考 gradle logging 选择想要的日志级别。


Gradle Daemon


为加快项目构建,gralde 会启动一个常驻 JVM 后台进程去处理构建,Daemon 进程默认三小时过期且当内存压力大的时候也会关闭。


Gradle 默认会启用 Daemon 进程去构建项目。

# 查看 daemon 运行状态
./gradlew --status
# stop daemon 进程
./gradlew --stop
# 重启 daemon 进程
./gradlew --daemon

构建生命周期


Gradle 是基于 task 依赖关系来构建项目的,我们只需要定义 task 和 task 之间的依赖关系,Gradle 会保证 task 的执行顺序。


Gradle 在执行 task 之前会建立 Task Graphs,我们引入的插件和自己构建脚本会往这个 task graph 中添加 task。


task-dag-examples.png


Gradle 的构建过程分为三部分:初始化阶段、配置阶段和执行阶段。


初始化阶段



  • 找到 settings.gradle,执行其中代码

  • 确定有哪些项目需要构建,然后对每个项目创建 Project 对象,build.gradle 主要就是配置这个 Project 对象
// settings.gradle
rootProject.name = 'basic'
println '在初始化阶段执行'

配置阶段



  • 执行 build.gradle 中的配置代码,对 Project 进行配置

  • 执行 Task 中的配置段语句

  • 根据请求执行的 task,建立 task graph
println '在配置阶段执行 Task 中的配置段语句'

tasks.register('configured') {
println '在配置阶段执行 Task 中的配置段语句'
doFirst {
println '在执行阶段执行'
}
doLast {
println '在执行阶段执行'
}
}

执行阶段


根据 task graph 执行 task 代码。


依赖管理


Maven 私服配置


我们一般都是多项目构建,因此只需要在父项目 build.gradle 配置 repositories。

allprojects {
repositories {
maven {
url "${mavenPublicUrl}"
credentials {
username "${mavenUsername}"
password "${mavenPassword}"
}
}
mavenLocal()
mavenCentral()
}
}

credentials 配置账号密码,当私服不需要权限下载的时候可以不配置。


Gradle 会按照配置的仓库顺序查询依赖下载。


配置依赖来自某个目录

dependencies {
compile files('lib/hacked-vendor-module.jar')
}
dependencies {
compile fileTree('lib')
}

有的时候第三方库没有 maven 供我们使用,可以使用这个。


依赖冲突


默认依赖冲突


::: tip
当出现依赖冲突的时候,gradle 优先选择版本较高的,因为较高版本会兼容低版本。
:::

dependencies {
implementation 'com.google.guava:guava:31.1-jre'
implementation 'com.google.code.findbugs:jsr305:3.0.0'

testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'
}

我们可以执行下面命令查看项目依赖的版本:

./gradlew dependency-management:dependencies --configuration compileClasspath

------------------------------------------------------------
Project ':dependency-management'
------------------------------------------------------------

compileClasspath - Compile classpath for source set 'main'.
+--- org.springframework.boot:spring-boot-dependencies:3.0.2
+--- com.google.guava:guava:31.1-jre
| +--- com.google.guava:failureaccess:1.0.1
| +--- com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava
| +--- com.google.code.findbugs:jsr305:3.0.2
| +--- org.checkerframework:checker-qual:3.12.0
| +--- com.google.errorprone:error_prone_annotations:2.11.0
| \--- com.google.j2objc:j2objc-annotations:1.3
\--- com.google.code.findbugs:jsr305:3.0.0 -> 3.0.2

我们可以看到,gradle 选择了 com.google.code.findbugs:jsr305:3.0.2 这个版本。


强制使用某个版本


如果我们想使用 com.google.code.findbugs:jsr305:3.0.0 版本

dependencies {
implementation 'com.google.guava:guava:31.1-jre'
implementation 'com.google.code.findbugs:jsr305:3.0.0', {
force = true
}
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'
}
./gradlew -q dependency-management:dependencyInsight --dependency jsr305 --configuration compileClasspath
com.google.code.findbugs:jsr305:3.0.0 (forced)
Variant compile:
| Attribute Name | Provided | Requested |
|--------------------------------|----------|--------------|
| org.gradle.status | release | |
| org.gradle.category | library | library |
| org.gradle.libraryelements | jar | classes |
| org.gradle.usage | java-api | java-api |
| org.gradle.dependency.bundling | | external |
| org.gradle.jvm.environment | | standard-jvm |
| org.gradle.jvm.version | | 17 |

com.google.code.findbugs:jsr305:3.0.0
\--- compileClasspath

com.google.code.findbugs:jsr305:3.0.2 -> 3.0.0
\--- com.google.guava:guava:31.1-jre
\--- compileClasspath

禁用依赖传递


guava 不会传递依赖它依赖的库到当前库,可以看到

dependencies { 
implementation 'com.google.guava:guava:31.1-jre', {
transitive = false
}
implementation 'com.google.code.findbugs:jsr305:3.0.0'
}
./gradlew dependency-management:dependencies --configuration compileClasspath

------------------------------------------------------------
Project ':dependency-management'
------------------------------------------------------------

compileClasspath - Compile classpath for source set 'main'.
+--- org.springframework.boot:spring-boot-dependencies:3.0.2
+--- com.google.guava:guava:31.1-jre
\--- com.google.code.findbugs:jsr305:3.0.0

可以看到 guava 依赖的 jar 没有传递到当前项目中来。


排除某个依赖


Guava 依赖的别的 jar 可以传递进来,而且排除了 findbugs, 项目依赖的版本为 3.0.0

dependencies { 
implementation 'com.google.guava:guava:31.1-jre', {
exclude group: 'com.google.code.findbugs', module: 'jsr305'
}
implementation 'com.google.code.findbugs:jsr305:3.0.0'
}
./gradlew dependency-management:dependencies --configuration compileClasspath

------------------------------------------------------------
Project ':dependency-management'
------------------------------------------------------------

compileClasspath - Compile classpath for source set 'main'.
+--- org.springframework.boot:spring-boot-dependencies:3.0.2
+--- com.google.guava:guava:31.1-jre
| +--- com.google.guava:failureaccess:1.0.1
| +--- com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava
| +--- org.checkerframework:checker-qual:3.12.0
| +--- com.google.errorprone:error_prone_annotations:2.11.0
| \--- com.google.j2objc:j2objc-annotations:1.3
\--- com.google.code.findbugs:jsr305:3.0.0

可以看到 guava 传递到当前项目的依赖少了 findbugs


配置依赖之间继承

configurations {
integrationTestImplementation.extendsFrom testImplementation
integrationTestRuntimeOnly.extendsFrom testRuntimeOnly
}

configurations.all {
resolutionStrategy {
force 'org.apache.tomcat.embed:tomcat-embed-core:9.0.43'
}
exclude group: 'org.slf4j', module: 'slf4j-simple'
}

api 和 implementation 区别


jar b 包含一下依赖

dependencies {
api 'org.apache.commons:commons-lang3:3.12.0'
implementation 'com.google.guava:guava:31.1-jre'
}

项目 a 引入 jar b ,commons-lang3 和 guava 都可以被工程 a 使用,只是二者 scope 不一样。


api 对应 compile,在 工程 a 可以直接使用,编译可以通过。


implementation 对应 runtime,编译找不到 guava 中的类。


Task


我们引用的插件实际是添加 task 到 task graph 中去。


我们知道 build.gradle 实际是用来配置对应项目的 org.gradle.api.Project。


因此我们可以在 build.gradle 中引用 org.gradle.api.Project 暴露的属性。


我们可以在 gradle dsl 和 Project 接口中可以知道可以访问哪些属性。


tasks 实际就是 Project 暴露的一个属性,因此我们可以使用 tasks 往当前项目中注册 task。

tasks.register('hello') {
doLast {
println 'hello'
}
}

推荐使用 tasks.register 去注册 task,而不是 tasks.create 去直接创建。


tasks.register 注册的 task 只会在用到的时候才会初始化。


Groovy 闭包


Groovy Closures

{ [closureParameters -> ] statements }

闭包的示例

{ item++ }                                          

{ -> item++ }

{ println it }

{ it -> println it }

:::tip


当方法的最后一个参数是闭包时,可以将闭包放在方法调用之后。


:::


比如注册一个 task 的接口是

register(String var1, Action<? super Task> var2)

tasks.register("task55"){
doFirst {
println "task55"
}
}

tasks.register("task66",{
doFirst {
println "task66"
}
})

Task Type


gradle 已经定义好一些 task type,我们可以使用这些 task type 帮助我们完成特定的事情。比如我们想要执行某个 shell 命令。


Exec - Gradle DSL Version 7.6

tasks.register("task3", Exec) {
workingDir "$rootDir"
commandLine "ls"
}

Plugin


插件分类


Gradle 有两种类型的插件 binary plugins and script plugins


二进制插件就是封装好的构建逻辑打成 jar 发布到线上,供别的项目使用。


脚本插件就是一个 *.gradle 文件。


buildSrc


一般我们的项目是多项目构建,各个子项目会共享一些配置,比如 java 版本,repository 还有 jar publish 到哪里等等。


我们可以将这些统一配置分组抽象为单独的插件,子项目引用这个插件即可。便于维护,不用在各个子项目都重复配置相同的东西。


buildSrc 这个目录必须在根目录下,它会被 gradle 自动识别为一个 composite build,并将其编译之后放到项目构建脚本的 classpath 下。


buildSrc 也可以写插件,我们可以直接在子项目中使用插件 id 引入。

buildSrc/
├── build.gradle
├── settings.gradle
└── src
├── main
│   ├── groovy
│   │   └── mflyyou.hello2.gradle
│   ├── java
│   │   └── com
│   │   └── mflyyou
│   │   └── plugin
│   │   ├── BinaryRepositoryExtension.java
│   │   ├── BinaryRepositoryVersionPlugin.java
│   │   └── LatestArtifactVersion.java

buildSrc/build.gradle


groovy-gradle-plugin 对应的是使用 groovy 写插件。


java-gradle-plugin 对应 java

plugins {
id 'groovy-gradle-plugin'
id 'java-gradle-plugin'
}
gradlePlugin {
plugins {
helloPlugin {
id = 'com.mflyyou.hello'
implementationClass = 'com.mflyyou.plugin.BinaryRepositoryVersionPlugin'
}
}
}

我们就可以在子项目使用插件

plugins {
id 'com.mflyyou.hello'
id 'mflyyou.hello2'
}

插件使用



  • Applying plugins with the plugins DSL
plugins {
id 'java'
}


  • Applying a plugin with the buildscript block
buildscript {
repositories {
gradlePluginPortal()
}
dependencies {
classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.5'
}
}

apply plugin: 'com.jfrog.bintray'


  • Applying a script plugin
apply from: 'other.gradle'

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

[Git废弃提交]需求做一半,项目停了,我该怎么废弃commit

Git
在实际开发中我们要拥抱变化。我们都知道需求它是很不稳定的,在实际开发过程中会经常改变。经常会遇到一个需求,已经开始进入开发阶段了,开发到一半的时候说这个功能不需要了。甚至会出现我们已经实现某一个功能,然后被告知,这个功能被砍掉了。 那么针对这种代码已经写了,现...
继续阅读 »

在实际开发中我们要拥抱变化。我们都知道需求它是很不稳定的,在实际开发过程中会经常改变。经常会遇到一个需求,已经开始进入开发阶段了,开发到一半的时候说这个功能不需要了。甚至会出现我们已经实现某一个功能,然后被告知,这个功能被砍掉了。


那么针对这种代码已经写了,现在要废弃的情况我们应该怎么操作呢?


当然,如果这个功能都在一个单独的分支上,且这个分支只有这个功能的代码,那么可以直接废弃这个分支。(这也是为什么会有多种Git工作流的原因,不同的软件需求场景,需要配合不同的工作流。)


代码还没有提交


如果代码还在本地,并没有提交到仓库上。可以用reset来重置代码。
git reset是将当前的分支重设(reset)到指定的commit或者HEAD(默认),并且根据参数确定是否更新索引和工作目录。

# 重置暂存区的指定文件,与上一次commit保持一致,但工作区不变
git reset [file]
# 重置暂存区与工作区,与上一次commit保持一致
git reset --hard
# 重置当前分支的指针为指定commit,同时重置暂存区,但工作区不变
git reset [commit]
# 重置当前分支的HEAD为指定commit,同时重置暂存区和工作区,与指定commit一致
git reset --hard [commit]
# 重置当前HEAD为指定commit,但保持暂存区和工作区不变
git reset --keep [commit]

其实reset也可以指定某个commit,这样就会重置到对应的commit,该commit之后的commit都会丢失。如果你没法确定这些commit中是否有需要保留的commit,就不要这样操作。


如果代码已经提交


如果代码已经提交了,且提交的分支上还有其他的代码。那么操作起来就比较麻烦。我们需要用revert来删除。


revert


git revert命令用来「撤销某个已经提交的快照(和reset重置到某个指定版本不一样)」。它是在提交记录最后面加上一个撤销了更改的新提交,而不是从项目历史中移除这个提交,这避免了Git丢失项目历史。

# 生成一个撤销最近的一次提交的新提交
git revert HEAD
# 生成一个撤销最近一次提交的上n次提交的新提交
git revert HEAD~num
# 生成一个撤销指定提交版本的新提交
git revert <commit_id>
# 生成一个撤销指定提交版本的新提交,执行时不打开默认编辑器,直接使用 Git 自动生成的提交信息
git revert <commit_id> --no-edit

比如我现在的dev分支,最近3次提交是"10","11","12"。



我现在要11这个提交去掉,我就直接revert "11" 这个commit



运行后,他会出现一个冲突对比,要求我修改完成后重新提交。(revert是添加一个撤销了更改的新提交)


这个提交,会出现"10"和"12"的对比。



修改完对比后,我们commit这次修改。



看下日志,我们可以看出,revert是新做了一个撤销代码的提交。



撤销(revert)被设计为撤销公共提交的安全方式,重设(reset)被设计为重设本地更改。
因为两个命令的目的不同,它们的实现也不一样:重设完全地移除了一堆更改,而撤销保留了原来的更改,用一个新的提交来实现撤销。「千万不要用 git reset 回退已经被推送到公共仓库上的提交,它只适用于回退本地修改(从未提交到公共仓库中)。如果你需要修复一个公共提交,最好使用 git revert」。


rebase


前面课程说过,rebase是对已有commit的重演,rebase命令也可以删除某个commit的。他和reset一样,删除掉的commit就彻底消失了,无法还原。
具体用法,这里不在再次介绍,可以去看前面的课程。


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