注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

一个28岁程序员入行自述和感受

我是一个容易焦虑的人,工作时候想着跳槽,辞职休息时候想着工作,休息久了又觉得自己每天在虚度光阴毫无意义,似乎陷入了一个自我怀疑自我焦虑的死循环了。我想我该做的点什么去跳出这个循环。。。 自我叙述 我相信,每个人都有一个自命不凡的梦,总觉得自己应该和别人不一样,...
继续阅读 »

我是一个容易焦虑的人,工作时候想着跳槽,辞职休息时候想着工作,休息久了又觉得自己每天在虚度光阴毫无意义,似乎陷入了一个自我怀疑自我焦虑的死循环了。我想我该做的点什么去跳出这个循环。。。


自我叙述


我相信,每个人都有一个自命不凡的梦,总觉得自己应该和别人不一样,我不可能如此普通,自己的一生不应该泯然众生,平凡平庸的度过。尤其是干我们it这一行业的,都有一个自己的程序员梦,梦想着,真的能够用 “代码改变世界”


入行回顾



你们还记得自己是什么时候,入行it行业的吗



我今年已经28岁了,想起来入行,还挺久远的,应该是2016入行的,我也算是半路出家的,中间有过武术梦 歌唱梦 但是电脑什么上学那会就喜欢玩,当然是指游戏,




武术梦




来讲讲我得第一个·梦,武术梦,可能是从小受到武打演员动作电视剧的影响,尤其那个时候,成龙大哥的电影,一直再放,我觉得学武术是很酷的一件事情,尤其那会上小学,还是初中我的体育还是非常好的,


然后我们家那个时候电视还是黑白的,电视机。哈哈哈😀电视台就那么几个,放来放去,有一个台一直重复放成龙电影,还有广告, 都是 学武术就到 xxxx学校, 我被洗脑了吧


于是真的让我爸,打电话质询了一下,可是好像他们这种武术学校都是托管式的,封闭式学习,听说很苦,,,,当然这不是重点,重点每年学费非常的贵,en~,于是乎我的这个梦想终止了,。。




歌唱梦




为啥会有唱歌想法,你猜对了,是被那个时候的好声音给影响了,那个时候好声音是真的很火,看的时候我一度以为我也可以上好声音,去当歌手然后出道,当明星,什么的。


不过不经历打击,怎么会知道自己的下线在哪里呢


我小学换了两到三个学校,到初中,再到高中,你们还记得自己读高中那会吗,高中是有专业选择的,入学军训完以后。


我们代班主任,和我们说有三个专业方向可以选择,艺术类,分美术,和唱歌,然后是文化类,然后艺术类就业考大学分数会低很多,然后一系列原因,哈哈哈,我就选择了歌唱班。


我最好伙伴他选择了,美术类就是素描。这里我挺后悔没有选择 美术类。


到了歌唱班,第一课就是到专业课有钢琴的教室,老是要测试每个同学的,音色和音高,音域
然后各自上台表演自己的拿手的一首歌,。我当时测试时候就是跟着老师的弹的钢琴键瞎唱,


表演的歌曲是张雨生《大海》 也就唱了高潮那么几句。。 😀现在想起来还很羞耻,那是我第一次在那么多人面前唱歌,


后面开始上课老师说我当时分班时候音色什么还不错,但学到后面,我是音准不太行,我发现。再加上我自己的从小感觉好像有点自卑敏感人格,到现在把,我唱歌,就越来越差,


当然我们也有乐理。和钢琴课,我就想主助攻乐理和钢琴,


但是我很天真


乐理很难学习,都是文科知识需要背诵,但是他也要有视唱,也就是唱谱子,duo,re,mi,fa,suo,la,xi,duo。。等,我发现我也学不进去


后面我又开始去学钢琴,但是钢琴好像需要一定童子功,不然可能很难学出来,于是我每天早上6点钟起来,晚上吃完饭就去钢琴教师抢占位置, 还得把门堵着怕人笑话,打扰我,


结果你们也猜到了,音乐方面天赋很重要,然后就是性格上面表演上面,要放得开,可是我第一年勉强撑过去了,后面第二年,专业课越来越多了,我感觉我越来越自卑~,然后成绩就越来越差,老师也就没太重视,嗯~好不容撑到了第二年下半年,放暑假,


但是老师布置任务暑假要自己去外面练钢琴,来了之后要考试,我还花钱去外面上了声乐课钢琴课,哎,我感觉就是浪费钱,,,,,因为没什么效果,性格缺陷加上天赋不行,基本没效果,那段时间我也很痛苦的,因为越来越感觉根本容入不进去班级体,尤其是后面高二,了专业课很多大部分是前面老师带着发生开嗓,后面自由练习,我也不好意思,不想练习,所以
到后面,高二下学习我就转学了,,,,


当然我们班转学的,不止我一个,还有一个转学的 和我一个寝室的,他是因为音高上不去,转到了文科班, 还有一个是挺有天赋,我挺羡慕的,但是人家挺喜欢学习,不喜欢唱歌什么,就申请转到了,文科班。 不过她转到文科班,没多久也不太好,后面好像退学了,,我一直想打听他的消息,都在也没打听到了




玩电脑




我对电脑的组装非常感兴趣,喜欢研究电脑系统怎么装,笔记本拆装,台式机拆装,我会拿我自己的的笔记本来做实验,自己给自己配台式机,自己给自己笔记本增加配置,哈哈哈哈。对这些都爱不释手。



这还是我很早时候,自己一点一点比价,然后去那种太平洋电脑城,电脑一条街,那种地去找人配置的。想想那时候配置这个电脑还挺激动,这是人生的第一台自己全部从零开始组装配的电脑,


本来打算,后面去电脑城上班,开一个笔记本维修,电脑装配的门面的,(因为自己研究了很多笔记本系统,电脑组装),可是好像听电脑城的人说,电脑组装什么的已经不赚钱了,没什么价格利润,都是透明的而且更新迭代非常的快,电脑城这种店铺也越来越少了,都不干了,没有新人再去干这个了,于是乎我的第一份工作失业 半道崩殂了,哈哈哈哈还没有开始就结束了。




学it




后面我又报名自学了,it编程,《xxx鸟》 但是学it我学起来,好像挺快的,挺有感觉的,入学前一个星期,要等班人数到齐才能开班,我们先来的就自己学习打字了,我每天都和寝室人,一起去打字,我感觉那段时间我过得挺开心和充实的,


后面我们觉得自带寝室不好,环境差,于是就几个人一起,搬出去住了,一起学习时候有一个年级26了,我和他关系还蛮好的,不过现在也没什么联系了,,,


学习时候,每次做项目时候我都是组长,那个时候原来是有成就感的,嗯,学习it好像改变了,我学唱歌那个时候,一些自卑性格,可能是遇到了一个好的老师吧


当然后面就顺利毕业,然后找到了工作了,,,


直到现在我还在it行业里


嗯~还想往下面写一点什么,,,下一篇分享一下我入门感受和经历吧


关注公众号,程序员三时 希望给你带来一点启发和帮助


作者:程序员三时
来源:juejin.cn/post/7230351646798643255
收起阅读 »

被问了无数次的函数防抖与函数节流,这次你应该学会了吧

web
前言 本篇文章内容,或许早已是烂大街的解读文章。不过参加几场面试下来发现,不少伙伴们还是似懂非懂地栽倒在(~面试官~)深意的笑容之下,权当温故知新吧。 文章从防抖、节流的原理说起再结合实际开发的场景,分别逐步实现完整的防抖和节流函数。 函数防抖 原理:当持续...
继续阅读 »

前言


本篇文章内容,或许早已是烂大街的解读文章。不过参加几场面试下来发现,不少伙伴们还是似懂非懂地栽倒在(~面试官~)深意的笑容之下,权当温故知新吧。


文章从防抖、节流的原理说起再结合实际开发的场景,分别逐步实现完整的防抖和节流函数。


函数防抖



  • 原理:当持续触发一个事件时,在n秒内,事件没有再次触发,此时才会执行回调;如果n秒内,又触发了事件,就重新计时

  • 适用场景:



    • search远程搜索框:防止用户不断输入过程中,不断请求资源,n秒内只发送1次,用防抖来节约资源

    • 按钮提交场景,比如点赞,表单提交等,防止多次提交

    • 监听resize触发时, 不断的调整浏览器窗口大小会不断触发resize,使用防抖可以让其只执行一次



  • 辅助理解:在你坐电梯时,当一直有人进电梯(连续触发),电梯门不会关闭,在一定时间间隔内没有人进入(停止连续触发)才会关闭。


下面我们先实现一个简单的防抖函数,请看栗子:


// 简单防抖函数
const debounce = (fn, delay) => {
  let timer;
  return function () {
    const context = this;
    const args = arguments;
    clearTimeout(timer);

    timer = setTimeout(function () {
      fn.call(context, ...args);
      //等同于上一句 fn.apply(context, args)
    }, delay);
  };
};

// 请求接口方法
const ajax = (e) => {
  console.log(`send ajax ${new Date()} ${e.target.value}`);
};

// 绑定监听事件
const noneDebounce = document.getElementsByClassName("none_debounce")[0];
const debounceInput = document.getElementsByClassName("debounce")[0];

noneDebounce.addEventListener("keyup", ajax);
debounceInput.addEventListener("keyup"debounce(ajax, 500));

运行效果如下:图片


点击这里,试试效果点击demo


可以很清晰的看到,当我们频繁输入时, 不使用节流就会不断的发送数据请求,但是使用节流后,只有当你在指定间隔时间内没有输入,才会执行发送数据请求的函数。


上面有个注意点:



  • this指向问题,在定时器中如果使用箭头函数()=>{fn.call(this, ...args)} 与上面代码效果一样, 原因时箭头函数的this是**「继承父执行上下文里面的this」**


关于防抖函数的疑问:



  1. 为什么要使用 fn.apply(context, args), 而不是直接调用 fn(args)


如果我们不使用防抖函数debounce时, 在ajax函数中打印this的值为dom节点:


<input class="debounce" type="text">

在使用debounce函数后,如果我们不使用fn.apply(context, args)修改this的指向, this就会指向window(ES6下为undefined)



  1. 为什么要传入arguments参数


我们同样与未使用防抖函数的场景进行对比


const ajax = (e) =>{
    console.log(e)
}


  1. 怎么给ajax函数传参


有的小伙伴就说了, 你的ajax只能接受绑定事件的参数,不是我想要的,我还要传入其他参数,so easy!


const sendAjax = debounce(ajax, 1000)
sendAjax(参数1, 参数2,...)

因为sendAjax 其实就是debounce中return的函数, 所以你传入什么参数,最后都给了fn


在未使用时,调用ajax函数对打印一个KeyboardEvent对象


image.png


使用debounce函数时,如果不传入arguments, ajax中的参数打印为undefined,所以我们需要将接收到的参数,传递给fn


函数防抖的理解:




我个人的理解其实和平时上电梯的原理一样:当一直有人进电梯时(连续触发),电梯门不会关闭,在一定时间间隔内没有人进入(停止连续触发)才会关闭。




从上面的例子,对防抖有了初步的认识,但是在实际开发中,需求往往要更加的复杂,比如我们要提交一个表单按钮,为了防止用户多次提交表单,可以使用节流, 但如果使用上面的节流,就会导致用户停止连续点击才会提交,而我们希望让用户点击时,立即提交, 等到n秒后,才可以重新提交。


对上面的代码进行改造,得到立即提交版:


const debounce = (fn, delay, immediate) => {
  let timer;
  return function () {
    const context = this;
    const args = arguments;
    clearTimeout(timer);

    if (immediate) {
      let startNow = !timer;

      timer = setTimeout(function () {
        timer = null;
      }, delay);

      if (startNow) {
        fn.apply(context, args);
      }
    } else {
      timer = setTimeout(function () {
        fn.call(context, ...args);
        //等同于上一句 fn.apply(context, args)
      }, delay);
    }
  };
};

从上面的代码可以看到,通过immediate 参数判断是否是立刻执行。


timer = setTimeout(function () {
    timer = null
}, delay)

立即执行的逻辑中,如果去掉上面这小段代码, 也是立即执行,但是之后就不会再执行提交了,当我们提交失败了怎么办(哭),所以加上上面这段代码,在设定的时间间隔内,将timer设置为null, 过了设定的时间间隔,可以再次触发提交按钮的立即执行,这才是完整的。


这是一个使用立即提交版本的防抖实现的了一个提交按钮demo


目前我们已经实现了包含非立即执行立即执行功能的防抖函数,感兴趣的小伙伴可以和我一起继续探究一下去,完善防抖函数~




做直播功能时,产品的小伙伴给提出这样一个需求:


直播的小窗口可以拖动, 点击小窗口以及拖动时, 显示关闭小窗口按钮,当拖动结束2s后, 隐藏关闭按钮;当点击关闭按钮时, 关闭小窗口




分析需求, 我们可以使用防抖来实现, 用户连续拖动小窗口过程中, 不执行隐藏关闭按钮,拖动结束后2s才执行隐藏关闭按钮;但是点击关闭按钮后,我们希望可以取消防抖, 所以需要继续完善防抖函数, 使其可以被取消。


「可取消版本」


const debounce = (fn, delay, immediate) => {
  let timer, debounced;
  // 修改--
  debounced = function () {
    const context = this;
    const args = arguments;
    clearTimeout(timer);

    if (immediate) {
      let startNow = !timer;

      timer = setTimeout(function () {
        timer = null;
      }, delay);

      if (startNow) {
        fn.apply(context, args);
      }
    } else {
      timer = setTimeout(function () {
        fn.call(context, ...args);
      }, delay);
    }
  };

  // 新增--
  debounced.cancel = function () {
    clearTimeout(timer);
    timer = null;
  };
  return debounced;
};

从上面代码可以看到,修改的地方是将return的函数赋值给debounced对象, 并且给debounced扩展了一个cancel方法, 内部执行了清除定时器timer, 并且将其设置为null; 为什么要将timer设置为null呢? 由于debounce内部形成了闭包, 避免造成内存泄露


上面的需求我写了个小demo, 需要的小伙伴可以看看可取消版本demo, 效果如下所示:图片


这个demo中,在拖拽过程中还可以使用节流,减少页面重新计算位置的次数,在下边学完节流,大家不妨试试


介绍节流原理、区别以及应用


前面学习了防抖,也知道了我们为什么要使用防抖来限制事件触发频率,那我们接下来学习另一种限制的方式节流(throttle)


函数节流



  • 原理:当频繁的触发一个事件,每隔一段时间, 只会执行一次事件。

  • 适用场景:



    • 拖拽场景:固定时间内只执行一次, 防止高频率的的触发位置变动

    • 监听滚动事件:实现触底加载更多功能

    • 屏幕尺寸变化时, 页面内容随之变动,执行相应的逻辑

    • 射击游戏中的mousedown、keydown时间



  • 辅助理解:


下面我们就来实现一个简单的节流函数,由于每隔一段时间执行一次,那么就需要计算时间差,我们有两种方式来计算时间差:一种是使用时间戳,另一种是设置定时器


使用时间戳实现


function throttle(func, delay) {
  let args;
  let lastTime = 0;

  return function () {
    // 获取当前时间
    const currentTime = new Date().getTime();
    args = arguments;
    if (currentTime - lastTime > delay) {
      func.apply(this, args);
      lastTime = currentTime;
    }
  };
}

使用时间搓的方式来实现的思路比较简单,当触发事件时,获取当前时间戳,然后减去之前的时间戳(第一次设置为0), 如果差值大于设置的等待时间, 就执行函数,然后更新上一次执行时间为为当前的时间戳,如果小于设置的等待时间,就不执行。


使用定时器实现


下面我们来看使用定时器实现的方式:与时间戳实现的思路是有差别的, 我们在事件触发时设置一个定时器, 当再次触发事件时, 如果定时器存在,就不执行;等过了设置的等待时间,定时器执行,我们需要在定时器执行时,清空定时器,这样就可以设置下一个定时器了


function throttle1(fn, delay) {
  let timer;
  return function () {
    const context = this;
    let args = arguments;

    if (timer) return;
    timer = setTimeout(function () {
      console.log("hahahhah");
      fn.apply(context, args);

      clearTimeout(timer);
      timer = null;
    }, delay);
  };
}

虽然两种方式都实现了节流, 但是他们达到的效果还是有一点点差别的,第一种实现方式,事件触发时,会立即执行函数,之后每隔指定时间执行,最后一次触发事件,事件函数不一定会执行;假设你将等待时间设置为1s, 当3.2s时停止事件的触发,那么函数只会被执行3次,以后不会再执行。


第二种实现方式,事件触发时,函数不会立即执行, 需要等待指定时间后执行,最后一次事件触发会被执行;同样假设等待时间设置为1s, 在3.2秒是停止事件的触发,但是依然会在第4秒时执行事件函数


总结


对两种实现方式比较得出:



  1. 第一种方式, 事件会立即执行, 第二种方式事件会在n秒后第一次执行

  2. 第一种方式,事件停止触发后,就不会在执行事件函数, 第二种方式停止触发后仍然会再执行一次


接下来我们写一个下拉加载更多的小demo来验证上面两个节流函数:点击查看代码


let state = 0 // 0: 加载已完成  1:加载中  2:没有更多
let page = 1
let list =[{...},{...},{...}]

window.addEventListener('scroll'throttle(scrollEvent, 1000))

function scrollEvent() {
    // 当前窗口高度
    let winHeight =
        document.documentElement.clientHeight || document.body.clientHeight

    // 滚动条滚动的距离
    let scrollTop = Math.max(
        document.body.scrollTop,
        document.documentElement.scrollTop
    )

    // 当前文档高度
    let docHeight = Math.max(
        document.body.scrollHeight,
        document.documentElement.scrollHeight
    )
    console.log('执行滚动')

    if (scrollTop + winHeight >= docHeight - 50) {
        console.log('滚动到底部了!')
        if (state == 1 || state == 2) {
            return
        }
        getMoreList()
    }
}

function getMoreList() {
    state = 1
    tipText.innerHTML = '加载数据中'
    setTimeout(() => {
        renderList()
        page++

        if (page > 5) {
            state = 2
            tipText.innerHTML = '没有更多数据了'
            return
        }
        state = 0
        tipText.innerHTML = ''
    }, 2000)
}

function renderList() {
    // 渲染元素
    ...
}

使用第一种方式效果如下:图片


一开始滚动便会触发滚动事件, 但是在滚动到底部时停止, 不会打印"滚动到底部了"; 这就是由于事件停止触发后,就不会在执行事件函数


使用第二种方式, 为了看到效果,将事件设置为3s, 这样更能直观感受到事件函数是否立即执行:


// window.addEventListener('scroll', throttle(scrollEvent, 1000))
window.addEventListener('scroll'throttle1(scrollEvent, 3000))

图片


一开始滚动事件函数并不会被触发,而是等到3s后才触发;而当我们快速的滚动到底部后停止滚动事件, 最后还是会执行一次


上面的这个例子是为了辅助理解这两种实现不方式的不同。


时间戳 + 定时器实现版本


在实际开发中, 上面两种实现方案都不满足我们的需求,我们希望一开始滚动就立即执行,停止触发的时候也还能执行一次。结合时间搓方式和定时器方式实现如下:


function throttle(fn, delay) {
  let timer, context, args;
  let lastTime = 0;

  return function () {
    context = this;
    args = arguments;

    let currentTime = new Date().getTime();

    if (currentTime - lastTime > delay) {
      // 防止时间戳和定时器重复
      // -----------
      if (timer) {
        clearTimeout(timer);
        timer = null;
      }
      // -----------
      fn.apply(context, args);
      lastTime = currentTime;
    }
    if (!timer) {
      timer = setTimeout(() => {
        // 更新执行时间, 防止重复执行
        // -----------
        lastTime = new Date().getTime();
        // -----------
        fn.apply(context, args);
        clearTimeout(timer);
        timer = null;
      }, delay);
    }
  };
}

使用演示效果如下:


图片


实现思路是结合两种实现方式,同时避免两种方式重复执行, 所以当调用时间戳执行函数时,需要将定时器清空;当使用到定时器执行函数时,需要增加修改执行记录的时间lastTime


我们可以看到,开始滚动立即会打印页面滚动了,停止滚动后,时间会再执行一次,滚动到底部时停止,也会执行到滚动到底部了


最终完善版


上面的节流函数满足了我们的基本需求, 但是我们可以进一步对节流函数进行优化,使得节流函数可以满足下面三种情况:



  • 事件函数立即执行,并且事件停止后再执行一次(以满足)

  • 事件函数立即执行,但是事件停止后不再执行(待探究)

  • 事件函数不立即执行,但是事件停止后再执行一次(待探究)




注意点:事件函数不立即执行,事件停止不再执行一次 这种情况不能满足,在后面从代码角度会做分析。




我们设置两个参数startlast分别控制是否立即执行与最后是否执行;修改上一版代码, 实现如下:


function throttle(fn, delay, option = {}) {
  let timer, context, args;
  let lastTime = 0;

  return function () {
    context = this;
    args = arguments;

    let currentTime = new Date().getTime();

    // 增加是否立即执行判断
    if (option.start == false && !lastTime) lastTime = currentTime;

    if (currentTime - lastTime > delay) {
      if (timer) {
        clearTimeout(timer);
        timer = null;
      }
      fn.apply(context, args);
      lastTime = currentTime;
    }
    // 增加最后是否再执行一次判断
    if (!timer && option.last == true) {
      timer = setTimeout(() => {
        // 确保再次触发事件时, 仍然不立即执行
        lastTime = option.start == false ? 0 : new Date().getTime();
        fn.apply(context, args);
        clearTimeout(timer);
        timer = null;
      }, delay);
    }
  };
}

上面代码就修改了三个地方,一个是立即执行之前增加一个判断:


if (option.start == false && !lastTime) lastTime = currentTime

如果传入参数是非立即执行, 并且lastTime为0, 将当前时间戳赋值给lastTime, 这样就不会进入 if (currentTime - lastTime > delay)


第二个修改地方, 增加最后一次是否执行的判断:


// 原来
// if (!timer) {...}

// 修改后
if (!timer && option.last == true) {
   ...
}

当传入last为true时,才使用定时器计时方式, 反之通过时间戳实现逻辑即可满足


第三个修改的地方, 也是容易被忽视的点, 如果start传入false,last传入true(即不立即执行,但最后还会执行一次), 需要在执行定时器逻辑调用事件函数时, 将lastTime设置为0:


 // 确保再次触发事件时, 仍然不立即执行
lastTime = option.start ==false0 : new Date().getTime()

这里解决的是再次触发事件时, 也能保证不立即执行。


疑问点


相信有的小伙伴会存在疑问,为什么没有讨论不立即执行, 最后一次也不执行的情况呢(即 start为true, last为true), 因为这种情况满足不了。


当最后一次不执行, 也就不会进入到 定时器执行逻辑,也就无法对 lastTime重置为0,所以,当再一次触发事件时,就会立即执行,与我们的需求矛盾了。关于这一点,大家了解即可了。


到这里,我们的节流函数功能就差不多了, 如果有兴趣的小伙伴可以自己实现一下可取消功能, 与防抖函数实现方式一致, 这里就不赘述了。


作者:白哥学前端
来源:juejin.cn/post/7230419964300951613
收起阅读 »

Flutter list 数组排序

以使用Dart的 sort() 方法对Flutter中的List进行升序或降序排序。 sort()方法需要传递一个比较函数来指定如何对对象进行比较,并按照您指定的顺序进行排序。 以下是一个示例,假设有一个包含整数的列表,可以按照整数值进行排序: List<...
继续阅读 »

以使用Dart的 sort() 方法对Flutter中的List进行升序或降序排序。
sort()方法需要传递一个比较函数来指定如何对对象进行比较,并按照您指定的顺序进行排序。
以下是一个示例,假设有一个包含整数的列表,可以按照整数值进行排序:


List<int> numbers = [1, 3, 2, 5, 4];
// 升序排序
numbers.sort((a, b) => a.compareTo(b));
print(numbers); // 输出:[1, 2, 3, 4, 5]

// 降序排序
numbers.sort((a, b) => b.compareTo(a));
print(numbers); // 输出:[5, 4, 3, 2, 1]

在上述代码中,我们使用了sort()方法将数字列表按照升序和降序进行了排序。


在比较函数中,我们使用了 compareTo() 方法来比较两个数字对象。


如果想按照其他字段进行排序,只需将比较函数中的a和b替换为您想要排序的字段即可。




以下是示例代码,假设您有一个包含Person对象的列表,可以按照Person的年龄字段进行排序:


class Person {
String name;
int age;

Person({this.name, this.age});
}

List<Person> persons = [
Person(name: "John", age: 30),
Person(name: "Jane", age: 20),
Person(name: "Bob", age: 25),
];

// 按照年龄字段进行排序
persons.sort((a, b) => a.age.compareTo(b.age));

// 输出排序后的列表
print(persons);

在上述代码中,我们使用了sort()函数将Person对象列表按照年龄字段进行排序。
在该示例中,我们使用了compareTo()函数来比较Person对象的年龄字段,并按照升序排序。



如果您有小程序、APP、公众号、网站相关的需求,您可以通过私信来联系我


如果你有兴趣,可以关注一下我的综合公众号:biglead


作者:早起的年轻人
来源:juejin.cn/post/7230420475494137913

收起阅读 »

跳舞的小人,鼠标跟随事件

web
鼠标跟随事件 在这里,我本来想弄一个灰太狼抓羊的动画效果,就是将我们的鼠标logo替换成一只羊的照片,然后后面跟随着一只狼,设置了cursor: url('./01.gif'), auto这个属性,但是好像没有成功,好像是兼容问题。于是找了一张给会动的gif。...
继续阅读 »

鼠标跟随事件


在这里,我本来想弄一个灰太狼抓羊的动画效果,就是将我们的鼠标logo替换成一只羊的照片,然后后面跟随着一只狼,设置了cursor: url('./01.gif'), auto这个属性,但是好像没有成功,好像是兼容问题。于是找了一张给会动的gif。


实现效果


整体十分简单,主要就是js代码。


01.gif
html里我们就只是放了一张图片


<div class="img"></div>

然后简简单单的给他们设置了一下大小和样式。


  * {
margin: 0;
padding: 0;
}

![image.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e17598cd683f41fe89fddda68981de97~tplv-k3u1fbpfcp-watermark.image?)
body {
background: rgb(240, 230, 240);
}

.img {
width: 10%;
height: 20%;
position: absolute;
background-image: url('./01.gif');
background-size: cover;
}

看一看主要的js代码


这里我们主要是进行一些基本属性的定义和设置,index可以看成是时间桢或者循环次数。


let img = document.querySelector('.img')
// 定义小图片的旋转角度
let deg = 0
// 定义小图片位于网页左侧的位置
let imgx = 0
// 定义小图片位于网页顶部的位置
let imgy = 0
// 定义小图片x轴的位置
let imgl = 0
// 定义小图片y轴的位置
let imgt = 0
// 定义小图片翻转的角度
let y = 0
// 定义一个计数器
let index = 0

这段代码的作用是根据鼠标在图片上的位置计算图片的旋转角度和翻转。


首先,通过 e.x 获取鼠标在页面上的横坐标,img.offsetLeft 获取图片左边边缘距页面左边边缘的距离,img.clientWidth / 2 获取图片宽度的一半,即图片中心点距图片左边边缘的距离。将这三个值相减,得到鼠标相对于图片中心点的横向偏移量,即 imgx


 imgx = e.x - img.offsetLeft - img.clientWidth / 2

同样地,通过 e.y 获取鼠标在页面上的纵坐标,img.offsetTop 获取图片上边边缘距页面上边边缘的距离,img.clientHeight / 2 获取图片高度的一半,即图片中心点距图片上边边缘的距离。将这三个值相减,得到鼠标相对于图片中心点的纵向偏移量,即 imgy


imgy = e.y - img.offsetTop - img.clientHeight / 2

接下来,根据 imgximgy 的值,使用 Math.atan2 计算出以图片中心为原点的弧度值,并将弧度值转换为角度值,即 deg,这就是图片需要旋转的角度。
最后,将 deg 赋值给 rotate 属性,就可以实现对图片的旋转了。


 deg = 360 * Math.atan(imgy / imgx) / (2 * Math.PI)

然后,通过 index = 0 定义了一个初始值为 0 的变量 index,用于后续的操作。接下来,通过 let x = event.clientX 获取到当前鼠标的水平坐标位置。


    // 每当鼠标移动的时候重置index
index = 0
// 定义当前鼠标的位置
let x = event.clientX

然后,使用 if 判断图片的左边界是否小于当前鼠标的位置,也就是判断鼠标是否在图片的右侧。如果鼠标在图片的右侧,说明图片需要向左翻转,这时候将 y 赋值为 -180,用于后续的样式设置。如果鼠标在图片的左侧,说明图片无需翻转,此时将 y 赋值为 0。最终,将 y 值赋给图片的 rotateY 样式属性,就可以实现对图片的翻转效果了。当 y-180 时,图片将被翻转过来;当 y0 时,图片不会被翻转。


 // 当鼠标的x轴大于图片的时候,就要对着鼠标,所以需要将图片翻转过来
// 否则就不用翻转
if (img.offsetLeft < x) {
y = -180
} else {
y = 0
}


 window.addEventListener('mousemove', function (e) {
// 获取网页左侧距离的图片位置
imgx = e.x - img.offsetLeft - img.clientWidth / 2
// 多去网页顶部距离图片的位置
imgy = e.y - img.offsetTop - img.clientHeight / 2
// 套入公式,定义图片的旋转角度
deg = 360 * Math.atan(imgy / imgx) / (2 * Math.PI)
// 每当鼠标移动的时候重置index
index = 0
// 定义当前鼠标的位置
let x = event.clientX
// 当鼠标的x轴大于图片的时候,就要对着鼠标,所以需要将图片翻转过来
// 否则就不用翻转
if (img.offsetLeft < x) {
y = -180
} else {
y = 0
}
})

接下来就要实现图片跟随了


这段代码的作用是通过 JavaScript 实现对图片的旋转和移动。然后利用了setInterval一直重复


首先,使用 img.style.transform 将旋转角度 deg 和翻转角度 y 应用于元素的 transform 样式。rotateZ 用于实现元素绕 z 轴旋转,而 rotateY 则用于实现元素的翻转效果。注意,这里使用字符串拼接的方式将旋转角度和翻转角度拼接起来,以达到应用两个属性值的效果。


img.style.transform = "rotateZ(" + deg + "deg) rotateY(" + y + "deg)"

接下来,将 index 的值加一,即 index++,表明下一帧需要进行的操作。


index++

然后,使用条件语句 if (index < 50) 对小图片的位置进行调整。在这里,通过不停地调整小图片的位置,实现了小图片沿着鼠标运动的效果。其中,imgl += imgx / 50 用于计算小图片应移动的水平距离,而 imgt += imgy / 50 则用于计算小图片应移动的垂直距离。50 是移动的帧数,可以根据需求进行调整。


// 在这里设置小图片的位置和速度,并判断小图片到达鼠标位置时停止移动 
if (index < 100)
{
imgl += imgx / 100
imgt += imgy / 100
}
img.style.left = imgl + "px"
img.style.top = imgt + "px"

setInterval(() => {
// 设置小图片的旋转和翻转
img.style.transform = "rotateZ(" + deg + "deg) rotateY(" + y + "deg)"
index++
// 在这里设置小图片的位置和速度,并判断小图片到达鼠标位置时停止移动
if (index < 100) {
imgl += imgx / 100
imgt += imgy / 100
}
img.style.left = imgl + "px"
img.style.top = imgt + "px"
}, 10)

源码


<!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>鼠标跟随</title>
<style>
* {
margin: 0;
padding: 0;
}

body {
background: rgb(240, 230, 240);
}

.img {
width: 10%;
height: 20%;
position: absolute;
background-image: url('./01.gif');
background-size: cover;
}
</style>
</head>

<body>
<div class="img"></div>
</body>
<script>
let img = document.querySelector('.img')
// 定义小图片的旋转角度
let deg = 0
// 定义小图片位于网页左侧的位置
let imgx = 0
// 定义小图片位于网页顶部的位置
let imgy = 0
// 定义小图片x轴的位置
let imgl = 0
// 定义小图片y轴的位置
let imgt = 0
// 定义小图片翻转的角度
let y = 0
// 定义一个计数器
let index = 0

window.addEventListener('mousemove', function (e) {
// 获取网页左侧距离的图片位置
imgx = e.x - img.offsetLeft - img.clientWidth / 2
// 多去网页顶部距离图片的位置
imgy = e.y - img.offsetTop - img.clientHeight / 2
// 套入公式,定义图片的旋转角度
deg = 360 * Math.atan(imgy / imgx) / (2 * Math.PI)
// 每当鼠标移动的时候重置index
index = 0
// 定义当前鼠标的位置
let x = event.clientX
// 当鼠标的x轴大于图片的时候,就要对着鼠标,所以需要将图片翻转过来
// 否则就不用翻转
if (img.offsetLeft < x) {
y = -180
} else {
y = 0
}
})
setInterval(() => {
// 设置小图片的旋转和翻转
img.style.transform = "rotateZ(" + deg + "deg) rotateY(" + y + "deg)"
index++
// 在这里设置小图片的位置和速度,并判断小图片到达鼠标位置时停止移动
if (index < 100) {
imgl += imgx / 100
imgt += imgy / 100
}
img.style.left = imgl + "px"
img.style.top = imgt + "px"
}, 10)
</script>

</html>

作者:Mr-Wang-Y-P
来源:juejin.cn/post/7230457833212280893
收起阅读 »

聊聊我在阿里第一年375&晋升的心得

前言 思来想去,觉得这个事情除了领导赏识大佬抬爱之外,还是挺不容易的,主观认为有一定的参考价值,然后也是复盘一下,继续完善自己。 绩效 首先晋升的条件就是要有个好绩效,那么我们就先基于绩效这个维度简单聊一下。 很多同学都知道阿里的绩效是361制度,也就是30%...
继续阅读 »

前言


思来想去,觉得这个事情除了领导赏识大佬抬爱之外,还是挺不容易的,主观认为有一定的参考价值,然后也是复盘一下,继续完善自己。


绩效


首先晋升的条件就是要有个好绩效,那么我们就先基于绩效这个维度简单聊一下。


很多同学都知道阿里的绩效是361制度,也就是30%的人拿A,60%的人拿B,10%的人拿C,不过在阿里,我们一般不用ABC来表示,除去价值观考核,我们常用的表达是3.75、3.5、3.25,初略的对应关系如下:


361通用阿里
30%A3.75
60%B3.5 ±
10%C3.25

那么,了解了阿里的绩效制度,再来看看绩效里面的门道。


首先,讲一个职场潜规则,「团队的新人是用来背绩效的」,也就是会把差绩效(325)指标分配给新人,因为如果把325给老人,容易产生团队不稳定的因素,而且不得不承认的是,少一个老人比少一个新人的成本更大,另一方面,你作为新人,业务不熟,根基不稳,也不见得能产出多大的价值,所以对于多数新人来说,也只能接受。据我所知,只有极少的公司里面会有「绩效保护」这种政策,而且一般还是针对的应届生,社招来说,还是要看能力的。


其次,基于潜规则,大部分新人都在为保住3.5做努力,只有少数优秀的人可以拿到更好的绩效。而新人拿375在阿里是很少的存在,即使是老人,拿375都是非常不容易的,何况是一个新人。


最后,就是晋升,晋升的前提条件就是满一年且是好绩效,加上现在降本增效的大环境,有的要求连续两年375才行,甚至都不一定有名额,当然,晋升也和团队绩效和级别有关系,但总之,男上加男,凤毛麟角。


个人背景


我是21年8月份入职阿里的,2022年是在阿里的第一个整财年。


之前一直是在小厂摸爬滚打,没有大厂经历,这对于我来说,是劣势,写代码不注重安全性防护,没有code review,没有ab test,没有自动化测试,没有文档沉淀,没有技术分享,纯纯小作坊。更重要的是,小厂和大厂的做事风格流程什么的,真的是千差万别,所以当时的landing对我来说,还是很难受的。但是,我有没有自带的优势呢,也有,写作能力,但是光有写作能力还是不够的,你没东西可写也不行啊。


其实试用期结束之后就差不多进入新的财年了,对于刚刚进入状态的我,也迎来了更大的挑战。过去的一整年有较多的精力都投入在研发效能和安全生产方面,这对于以前纯业务开发的我来说,完全是一个新的领域,好在不负重托,也略有成果。


其实回想过去一年的经历来看,今天的成绩是多维度的结合,比如硬实力和软实力、个人和团队、内部和外部等多个维度,下面将分别介绍一些我个人的心得经验,仅供参考。


沟通能力


这也用说?不就是说话吗,谁不会说?


我看过很多的简历,如果有「自我评价」,几乎都会提到「具备良好的沟通能力」。
可是沟通能力看起来真的有那么简单吗?我认为不是的,实际上我甚至认为它有点难。


在职场经常会有这些问题:



  1. 这个点我没太理解,你能在解释一下吗?

  2. 为什么要这么做?为什么不这么做?

  3. 现在有什么问题?

  4. 你的诉求是什么?

  5. 讲话的时候经常被打断等等...


这些问题你是不是被问到过,或者这么问别人呢。


而这些问题的背后,则反映了沟通的不完整和不够清晰。面对他人的挑战,或向跨部门的同学讲述目标价值时,会沟通的同学会显的游刃有余,而不会沟通的同学则显得捉襟见肘。


沟通方面,其实也包含很多场景。


首先是逻辑要清晰。


面对用户的一线同事,比如销售和客服,他们都是有话术的,话术就是沟通的技巧。


为什么脸皮薄/不懂拒绝的人容易被销售忽悠?


因为销售在跟客户沟通的时候,就是有一套完整的话术,他先讲一,看你反应再决定讲二还是三;当你拒绝了A方案,他还有B方案C方案。一套完整的话术逻辑把你都囊括在里面,最后只能跟着他的思维走,这就是话术的意义。


同样的,在职场,你跟人沟通的时候,不能直说怎么做,解决方案是什么,而背景和问题同样重要,同时讲述问题的时候要尽可能的拆解清楚,避免遗漏,这样不只是让别人更理解你的意思,其实有时候换个视角,解决方案可能有更优的。


逻辑清晰,方案完善,对方就会处于一个比较舒服的状态,有时候能起到事半功倍的效果。你可能会觉得有些麻烦,但如果因为没有表达清楚而导致最后的结果不符合预期,孰轻孰重,应该拎得清的吧?


其次是要分人。


我在之前的面经中提到,自我介绍要分人,技术面试官和HR的关注点一定是不一样的,面对这种不同的出发点,你讲的东西肯定不能是同一套,要有侧重点,你跟HR讲你用了什么牛逼的技术、原理是什么,那不是瞎扯嘛。


这个逻辑在职场也是一样的,你和同事讨论技术、向领导汇报、回答领导的领导问题、跟产品、运营、跨部门沟通,甚至出现故障的时候给客满提供的话术,面对不同的角色、不同的场合,表达出来的意思一定是要经过「翻译」的,多换位思考。


即要把自己的意思传达到,也要让对方get到,这才是有效沟通。


所谓沟通能力,不只是有表达,还要有倾听。


倾听方面,比如当别人给你讲一个事情的时候,你能不能快速理解,能不能get到对方的点,这也很重要。
快速且高效,这是一个双向的过程。这里面会涉及到理解能力,而能理解的基础是基于现有的认知,也就是过往的工作经验、项目经历和知识面,这是一个积累的过程。


当然,也有可能是对方表达的不够清楚,这时候就要不耻下问,把事情搞清楚,搞不清楚就会有不确定性,就是有风险,如果最终的结果不符合预期,那么复盘的时候,委屈你一下,不过分吧😅。


最后是沟通媒介。


我们工作沟通的场景一般都是基于钉钉、微信、飞书之类的即时通讯办公平台,文字表达的好处是,它可以留档记录,方便后期翻阅查看,但是也一定程度在沟通表达的传递上,存在不高效的情况。


那这时候,语音、电话就上场了,如果你发现文字沟通比较费劲的时候,那一定不如直接讲来的更高效。


但是语音、电话就一定能聊的清楚吗,也不见得。


“聊不清楚就当面聊”,为什么当面聊就能比语音、电话聊的清楚?因为当面聊,不只是可以听到语气看到表情肢体动作,更重要的是当面聊的时候,双方一定是专注在这个事情上的,不像语音的时候还可以干其他的事,文字甚至会长时间已读不回,所以讲不清楚的时候,当面聊的效果是最好的。为了弥补留档的缺陷,完事再来个文字版的纪要同步出来,就ok了。


其他。


上面提到逻辑要清晰,要分人,还有倾听能力,和善用沟通媒介。


其实沟通里还包括反应能力,当你被突然问到一个问题的时候,能不能快速流畅的表达清楚,这个也很关键,如果你支支吾吾,反反复复的都没有说清楚,设想一下,其他人会怎么看你,“这人是不是不行啊?”,长此以往,这个信任度就会降低,而一旦打上标签,你就需要很多努力才能证明回来。


还包括争辩能力,比如在故障复盘的时候,能不能有效脱身不被拉下水,能不能大事化小小事化了,也都是沟通的技巧,限于篇幅,不展开了。


学会复盘


复盘是什么?


复盘是棋类术语,指对局完毕后,复演该盘棋的记录,以检查对局中招法的优劣与得失关键。在工作中,复盘是通过外在的表象、客观的结果找出本质,形成成功经验或失败教训,并应用到其他类似事件中,提升面向未来的能力。


所以,复盘不是流水账的记录经过和简单总结,也不是为了表彰罗列成绩,更不是为了甩锅而相互扯皮。找出本质的同时一定要有思考,这个思考可以体现在后续的一些执行事项和未来规划上,总之,就是要让「复盘」变的有意义。


什么是有意义的复盘?


就是你通过这次复盘,能知道哪些错误是不能再犯的,哪些正确的事是可以坚持去做的。会有一种「再来一次结果肯定不一样」的想法,通过有意义的复盘让「不完美」趋向「完美」。


我个人复盘的三个阶段:



  • 回顾:回顾目标、经过、结果的整个流程;

  • 分析:通过主观、客观的视角分析,失败的原因是什么,成功的原因是什么;

  • 转化:把成功经验和失败教训形成方法论,应用到类似事件中;


Owner意识


什么是owner意识?


简单讲就是主人翁精神。认真负责是工作的底线,积极主动是「owner意识」更高一级的要求。


如果你就是怀着只干活的心态,或者躺平的心态,换我是领导,也不认为你能把活做好。因为这种心态就是「做了就行,能用就行」,但有owner意识不一样,这种人做事的时候就会多思考一点、多做一点,这里面最主要的就是主动性,举个栗子,好比写代码,当你想让程序运行的更好的时候,你就会多关注代码编写的鲁棒性,而不是随便写写完成功能就行。


但人性自私,责任感也不是人人都有,更别提主动性了,所以这两种心态的人其实差别很大,有owner意识的人除了本职工作能做好之外,在涉及到跨团队的情况,也能主动打破边界去push,有责任心有担当,往往能受到团队内外的一致好评。


在其位谋其职,我其实并没有特意去固化自己的owner意识,就是怀着一个要把事情做好的心态,跟我个人性格也有关系,现在回想起来,不过是水到渠成而已。



卷不一定就有owner意识,不卷也不代表没有。



向上管理


这个其实我一开始做的并不好,甚至可以说是很差,小公司出身哪需要什么向上管理,活干好就行。


但是现在不一样了,刚入职比较大的一个感受就是,我老板(领导)其实并不太过问我的工作内容,只是偶尔问一下进度。


然而这个「问」,其实也能反应出一些过往不太在意的问题:



  1. 没有及时汇报,等到老板来问的时候其实处于一个被动的局面了,虽然也不会有什么太大的影响,可能多数人也都是这样,但是这不正说明我不够出众吗?

  2. 不确定性,什么进度?有没有遇到问题?这些都是不确定性,老板不喜欢“惊喜”,有困难要说,有风险要提前暴,该有结果的时候没有,老板也很被动,你会留下一个什么印象?


当然,向上管理也不只是向上汇报,也是一个体现个人能力和学习的渠道。不要只提问题找老板要解决方案,我会简述问题,评估影响面,还会给出解决方案的Plan A和Plan B,这样老板就是做选择题了,即使方案不够完美,老板指点一下不正是学习的好机会吗。


学会写TODO


为什么写todo?


写todo的习惯其实是在上家公司养成的,因为要写周报,如果不记录每天做了什么,写周报的时候就会时不时的出现遗漏的情况。除了记录当天所做的事情之外,我还列了第二天要做的事情。虽然一直有给自己定规划的习惯,但是颗粒度都没有这么细过。彼时的todo习惯,不仅知道当天做了什么,还规划了第二天做什么,时刻有目标,也不觉得迷茫。


进入阿里之后,虽然没有周报月报这种东西,但是这个习惯也一直保持着,在此之上,我还做了一些改良。



  1. 优先级,公司体量一旦大起来之后,业务线就很多,随之而来的事情就很多,我个人对处理工作是非常反感多线程并发的,特别是需要思考的工作,虽然能并行完成,但完成度不如专注在一件事情上的好,但是有些事情又确实得优先处理,所以就只能根据事情的紧急程度排一下优先级,基本很少有一件事需要从早到晚都投入在里面的,所以抽出身来就可以处理下一件事,所以也不会出现耽误其他事情的情况,当然线上故障除外。

  2. 消失的时间,因为真的是事情又多又杂,时常在下班的时候会有一种瞎忙的感觉,就是「忙了一天,好像什么都没干」,但又确实很忙很累,仿佛陷入一个怪圈。所以后来我就把颗粒度拆的更细,精确到小时,也不是几点钟该做什么,就是把做的事情记录下来,并备注一下投入的时间,比如上午排障答疑投入了两小时,这样到中午吃饭的时候也不至于上午就这样虚度了。让“消失的时间”有迹可循,治愈精神内耗。

  3. 归纳总结,我现在是在语雀以月度为单位记录每天的todo,这样就可以进行月度的总结和复盘,在半年度的OKR总结的时候,还有了归纳总结的来源,有经过、有结果、还有时间线,一目了然,再也不用为写总结发愁了。


总之,写todo的好处除了记录做了什么、要做什么,它还能辅助你把工作安排的更好。


有规划有目标,也不会陷入一个迷茫虚度的状态,属于一个成本非常低而收益却很高的好习惯,不止工作,学习也是如此,生活亦然。


其他方面


适应能力


于我个人来说,工作强度比以前要大很多,慢慢习惯了就行,在大厂里面阿里还不是最卷的,但钱也不是最多的;工作流程方面只是有些不清楚而已,并没有什么门槛,熟悉了就行;还有阿里味儿,确实要学很多新词儿、缩写、甚至潜台词,这没啥说的,还没见到有能独善其身的😅。


适应能力也不是随便说说,有太多的人入职新公司干的怀疑人生、浑身难受而跑路的,抛开公司的问题不说,难道就没有自己的问题吗?🐶


我把适应分为两个阶段,第一个阶段就是适应工作环境,熟悉公司、同事、产品、项目;第二个阶段就是适应之后,要想想如何迎接没有新手光环的挑战,如何去展示自己、提升自己等。


技术能力


夯实自己的技术能力尤为重要,毕竟是吃饭的家伙,是做事拿结果的重要工具和手段。


在大家技术能力的基本面都不会太差的情况下,如何在技术上建立团队影响力,就是需要思考的问题。


要找准自己在团队中的定位,并在这一领域深耕,做到一提这个技术就能想到你的效果,建立技术壁垒。


其实也不只是技术,要学会举一反三,找到自己在团队的差异性,虽然不可替代性在公司离了谁都可以转的说法下有些牵强,但是可以提高自己在团队的重要性和信任度。


信息渠道


要学会拓宽自己的信息渠道,有句话叫,掌握的信息越多,决策就越正确



  • 多看,看别人在做什么,输出什么,规划什么;

  • 多聊,找合作方,相同目标的同事,甚至其他公司的朋友,互通有无;


看完聊完要把对自己有用的信息提炼出来哦。


影响力


内部的话,主要是建立同事间的信任,技术的占比相对要多一些;


外部的话,主要是在合作方那里建立一个靠谱的口碑,如果配合超预期那就更好了,我就是被几个大佬抬了一手,虽然不起决定性作用,但是也很重要。


摆脱固化


跳脱出程序员的固化思维


程序的世界非0即1,程序员的思维都是非常严谨的,这个严谨有时候可能会显得有些“死板”,在商业化的公司里面,很多事情不是能不能的问题,而是要不要的问题。而在这里面,技术大多数都不是第一要素,出发点得是业务视角、用户视角,很多技术难点、卡点,有时候甚至不用技术也能解决。


小结



  • 沟通能力:逻辑要清晰,对象要分人,还有倾听能力,和善用沟通媒介;

  • owner意识:认真负责是工作的底线,积极主动是「owner意识」更高一级的要求;

  • 向上管理:向上管理也不只是向上汇报,也是一个体现个人能力和学习的渠道;

  • 写TODO:辅助工作,治愈内耗,一个成本低而收益高的好习惯;

  • 其他方面:拓宽信息渠道,找到技术方向,简历内部外部的影响力等;



实际上不止这些,今天就到这吧。



最后


当下的市场环境无论是求职还是晋升,都挺难的,都在降本增效,寒气逼人,我能拿到晋升的门票,诚然是实力的体现,但也有运气的成分。没晋升也不一定是你的问题,放平心态,当下保住工作才是最重要的。


哔哔了这么多,可能很多同学道理也都懂,估计就难在知行合一吧...


最后送给大家一句罗翔老师的经典名言:



人生最大的痛苦,就是你知道什么是对的,但却永远做出错误的选择,知道和做到这个巨大的鸿沟,你永远无法跨越。


作者:yechaoa
来源:juejin.cn/post/7230457573719392315

收起阅读 »

React 你是真的骚啊,一个组件就有这么多个设计模式🙄🙄🙄

web
React 真的是太灵活了,写它就感觉像是在写原生 JavaScript 一样,一个功能你可以有多种实现方式,例如你要实现动态样式,只要你愿意去做,你会有很多种解决方案,这可能也就是 React 会比 Vue 相对来说比较难一点的原因,这或许也就是这么喜欢 R...
继续阅读 »

React 真的是太灵活了,写它就感觉像是在写原生 JavaScript 一样,一个功能你可以有多种实现方式,例如你要实现动态样式,只要你愿意去做,你会有很多种解决方案,这可能也就是 React 会比 Vue 相对来说比较难一点的原因,这或许也就是这么喜欢 React 的原因了吧,毕竟它可是我见一个爱一个的技术之一🤣🤣🤣


也正是因为这个原因,在 React 中编写一个组件就给我们编写一个组件提供了多种方式,那么在接下来的文章中我们就来讲解一下这几种组件的设计模式。


Mixin设计模式


在上一篇文章中有讲解到了 JavaScript 中的 Mixin,如果对这个设计模式不太理解的可以通过这篇文章进行学习 来学习一下 JavaScript 中的 Mixin


如何在多个组件之间共享代码,是开发者们在学习 React 是最先问的问题之一,你可以使用组件组合来实现代码重构,你也可以定义一个组件并在其他几个组件中使用它。


如何用组合来解决某个模式并不是显而易见的,React 受函数式编程的影响,但是它进入了由面向对象库主导的领域(hooks 出现以前),为了解决这个问题,React 团队在这加上了 Mixin,它的目标就是当你不确定如何使用组合解决想用的问题时,为你提供一种在组件之间重用代码。


React 最主流构建 Component 的方法是利用 createClass 创建,顾名思义,就是创造一个包含 React 方法 Class 类。


Mixin危害


React 官方文档 Mixins Considered Harmful 中提到了 Mixin 带来的危害,主要有以下几个方面:



  • Mixin 可能会相互依赖,相互耦合,不利于代码维护;

  • 不同的 Mixin 中的方法可能会相互冲突;

  • Mixin非常多时,组件是可以感知到的,甚至还要为其做相关处理,这样会给代码造成滚雪球式的复杂性;


装饰器模式


装饰器是一种特殊的声明,可以附加到类声明、方法、访问器、属性或参数上,装饰者使用 @+函数名 形式来修改类的行为。如果你对装饰器不太了解,你可以通过这一篇文章 TS的装饰器你再学不会我可就要报警了哈 进行学习。


现在我们来看看在 React 中怎么使用装饰器,我们现在有这样的一个需求,就是为被装饰的页面或组件设置统一的背景颜色和自定义颜色,完整代码具体如下:


import React, { Component } from "react";

interface Params {
background: string;
size?: number;
}

function Controller(params: Params) {
return function (
WrappedComponent: React.ComponentClass,
): React.ComponentClass {
WrappedComponent.prototype.render = function (): React.ReactNode {
return <div>但使龙城飞将在,不教胡马度阴山</div>;
};

return class Page extends Component {
render(): React.ReactNode {
const { background, size = 16 } = params;
return (
<div style={{ backgroundColor: background, fontSize: size }}>
<WrappedComponent {...this.props}></WrappedComponent>
</div>

);
}
};
};
}

@Controller({ background: "pink", size: 100 })
class App extends Component {
render(): React.ReactNode {
return <div>牛逼</div>;
}
}

export default App;

这段代码的具体输出如下所示:


image.png


在上面的代码中,Controller 装饰器会接收 App 组件,其中 WrappedComponent 就是我们的 App 组件,在这里我们通过修改原型方法 render 将其的返回值修改了,并对其进行了一层包裹。


所以 App 组件在使用了类装饰器,不仅可以修改了原来的 DOM,还对外层多加了一层包裹,理解起来就是接收需要装饰的类为参数,返回一个新的内部类。恰与 HOC 的定义完全一致。所以,可以认为作用在类上的 decorator 语法糖简化了高阶组件的调用。


高阶组件


HOC 高阶组件模式是 React 比较常用的一种包装强化模式之一,你也可以看作 React 对装饰模式的一种实现,高阶组件就是一个函数,并且该函数接收一个组件作为参数,并返回一个新的组件,它是一种设计模式,这种设计模式是由 React 自身的特性产生的结果。


高阶组件主要解决了以下问题,具体如下:



  • 复用逻辑: 高阶组件就像是一个加工 React 组件的工厂,你需要向该工厂提供一个坯子,它可以批量地对你送进来的组件进行加工,包装处理,还可以根据你的需求定制不同的产品;

  • 强化props: 高阶组件返回的组件,可以劫持上一层传过来的 props,染回混入新的 props,来增强组件的功能;

  • 控制渲染: 劫持渲染是 hoc 中的一个特性,在高阶组件中,你可以对原来的组件进行条件渲染,节流渲染,懒加载等功能;


HOC的实现方式


常用的高阶组件有两种方式,它们分别是 正向属性代理反向继承,接下来我们来看看这两者的区别。


正向属性代理


所谓正向属性代理,就是用组件包裹一层代理组件,在代理组件上,我们可以代理所有传入的 props,并且觉得如何渲染。实际上这种方式生成的高阶组件就是原组件的父组件,父组件对子组件进行一系列强化操作,上面那个装饰器的例子就是一个 HOC 正向属性代理的实现方式。


对比原生组件增强的项主要有以下几个方面:



  • 可操作所有传入的props: 可以对其传入的 props 进行条件渲染,例如权限控制等;

  • 可以操作组件的生命周期;

  • 可操作组件的 static 方法,但是需要手动处理,或者引入第三方库;

  • 获取 refs;

  • 抽象 state;


反向继承


反向继承其实是一个函数接收一个组件作为参数传入,并返回了一个继承自该传入的组件的类,并且在该类的 render() 方法中返回 super.render() 方法,能通过 this 访问到源组件的生命周期propsstaterender等,相比属性代理它能操作更多的属性。


两者区别



  • 属性代理是从组合的角度出发,这样有利于从外部操作被包裹的组件,可以操作的对象是 props,或者加一层拦截器或者控制器等;

  • 方向继承则是从继承的角度出发,是从内部去操作被包裹的组件,也就是可以操作组件内部的 state,生命周期,render 函数等;


具体实例代码如下所示:


function Controller(WrapComponent: React.ComponentClass) {
return class extends WrapComponent {
public state: State;
constructor(props: any) {
super(props);
this.state = {
nickname: "moment",
};
}

render(): React.ReactNode {
return super.render();
}
};
}

interface State {
nickname: string;
}

@Controller
class App extends Component {
public state: State = {
nickname: "你小子",
};
render(): React.ReactNode {
return <div>{this.state.nickname}</div>;
}
}

反向继承主要有以下优点:



  • 可以获取组件内部状态,比如 state,props,生命周期事件函数;

  • 操作由 render() 输出的 React 组件;

  • 可以继承静态属性,无需对静态属性和方法进行额外的处理;


反向继承也存在缺点,它和被包装的组件强耦合,需要知道被包装的组件内部的状态,具体是做什么,如果多个反向继承包裹在一起,状态会被覆盖。


HOC的实现


HOC 的实现方式按照上面讲到的两个分类一样,来分别讲解这两者有什么写法。


操作 props


该功能由属性代理实现,它可以对传入组件的 props 进行增加、修改、删除或者根据特定的 props 进行特殊的操作,具体实现代码如下所示:


import React, { Component } from "react";

interface Params {
background: string;
size?: number;
}

function Controller(params: Params) {
return function (
WrappedComponent: React.ComponentClass,
): React.ComponentClass {
WrappedComponent.prototype.render = function (): React.ReactNode {
return <div>但使龙城飞将在,不教胡马度阴山</div>;
};

return class Page extends Component {
render(): React.ReactNode {
const { background, size = 16 } = params;
return (
<div style={{ backgroundColor: background, fontSize: size }}>
<WrappedComponent {...this.props}></WrappedComponent>
</div>

);
}
};
};
}

@Controller({ background: "pink", size: 100 })
class App extends Component {
render(): React.ReactNode {
return <div>牛逼</div>;
}
}

export default App;

抽离state控制组件更新


高阶组件可以将 HOCstate 配合起来,控制业务组件的更新,在下面的代码中,我们将 inputvalue 提取到 HOC 中进行管理,使其变成受控组件,同时不影响它使用 onChange 方法进行一些其他操作,具体代码如下所示:


function Controller(WrappedComponent) {
return class extends React.Component {
constructor(props) {
super(props);
this.state = {
name: "",
};
this.onChange = this.onChange.bind(this);
}

onChange = (event) => {
this.setState({
name: event.target.value,
});
};

render() {
const newProps = {
value: this.state.name,
};
return (
<WrappedComponent
onChange={() =>
this.onChange}
{...this.props}
{...newProps}
/>

);
}
};
}

class App extends React.Component {
render() {
return (
<div>
<h1>{this.props.value}</h1>
<input name="name" {...this.props} />
</div>

);
}
}

export default Controller(App);

获取 Refs 实例


使用高阶组件后,获取到的 ref 实例实际上是最外层的容器组件,而非原组件,但是很多情况下我们需要用到原组件的 ref,我们先来看下面的代码,具体代码如下所示:


function Controller(WrappedComponent) {
return class Page extends React.Component {
render() {
const { ref, ...rest } = this.props;
return <WrappedComponent {...rest} ref={ref} />;
}
};
}

class Input extends React.Component {
render() {
return <input />;
}
}

class App extends React.Component {
constructor(props) {
super(props);
this.ref = React.createRef();
}
componentDidMount() {
console.log(this.ref);
}
render() {
return <Input ref={this.ref} />;
}
}

export default Controller(App);

image.png


通过查看控制台输出,你会发现获取到的是整个 Input 组件,那么有什么办法可以获取到 input 这个真实的 DOM 呢?


在之前的例子中我们可以通过 props 传递,一层一层传递给 input 原生组件来获取,具体代码如下:


class Input extends React.Component {
render() {
return <input ref={this.props.inputRef} />;
}
}

注意,因为传参不能传 ref,所以这里要修改一下


image.png


当然你也可以利用父组件的回调,具体代码如下:


class App extends React.Component {
constructor(props) {
super(props);
this.ref = React.createRef();
}
componentDidMount() {
console.log(this.ref);
}
render() {
return <Input inputRef={(e) => (this.ref = e)} />;
}
}

最终的代码如下图所示,这里展示了以上两个方法具体代码,如下图所示:


image.png


通过查看浏览器输出,两者都能成功输出原生的 ref 实例


image.png


React 给我们提供了一个 forwardRef 来帮助我们进行 refs 传递,这样我们在高阶组件上获取的 ref 实例就是原组件的 ref 了,而不需要手动传递,我们只需要修改一下 Input 组件代码即可,具体如下:


const Input = React.forwardRef((props, ref) => {
return <input type="text" ref={ref} />;
});

image.png


这样我们就获取到了原始组件的 ref 实例啦!


获取原组件的 static 方法


当待处理的组件为 class 组件时,通过属性代理实现的高阶组件可以获取到原组件的 static 方法,具体实现代码如下所示:


function Controller(WrappedComponent) {
return class Page extends React.Component {
componentDidMount() {
WrappedComponent.moment();
}
render() {
const { ref, ...rest } = this.props;
return <WrappedComponent {...rest} ref={ref} />;
}
};
}

class App extends React.Component {
static moment() {
console.log("你好骚啊");
}
render() {
return <div>你小子</div>;
}
}

export default Controller(App);

你好骚啊 正常输出


image.png


反向继承操作 state


因为我们高阶组件继承了传入组件,那么就是能访问到this了,有了 this 我们就能操作和读取 state,也就不用像属性代理那么复杂还要通过 props 回调来操作 state


反向继承的基本实现方法就是原组件继承 Component,再在高阶组件中通过把原组件传参,再生成一个继承自原组件的组件。


image.png


具体实例代码如下所示:


function Controller(WrappedComponent) {
return class Page extends WrappedComponent {
componentDidMount() {
console.log(`组件挂载时 this.state 的状态为`, this.state);
setTimeout(() => {
this.setState({ nickname: "你个叼毛" });
}, 1000);
// this.setState({ nickname: 1 });
}
render() {
return super.render();
}
};
}

class App extends React.Component {
constructor() {
super();
this.state = {
nickname: "你小子",
};
}
render() {
return <h1>{this.state.nickname}</h1>;
}
}

export default Controller(App);

代码具体输出如下图所示,当组件挂载完成之后经过一秒,state 状态发生改变:


image.png


劫持原组件生命周期


因为反向继承方法实现的是高阶组件继承原组件,而返回的新组件属于原组件的子类,子类的实例方法会覆盖父类的,具体实例代码如下所示:


function Controller(WrappedComponent) {
return class Page extends WrappedComponent {
componentDidMount() {
console.log("生命周期方法被劫持啦");
}
render() {
return super.render();
}
};
}

class App extends React.Component {
componentDidMount() {
console.log("原组件");
}
render() {
return <h1>你小子</h1>;
}
}

export default Controller(App);

代码的具体输出如下图所示:


image.png


render props 模式


render props 的核心思想是通过一个函数将组件作为 props 的形式传递给另外一个函数组件。函数的参数由容器组件提供,这样的好处就是将组件的状态提升到外层组件中,具体实例代码如下所示:


const Home = (props) => {
console.log(props);
const { children } = props;
return <div>{children}</div>;
};

const App = () => {
return (
<div>
<Home admin={true}>
<h1>你小子</h1>
<h1>小黑子</h1>
</Home>
</div>

);
};

export default App;

具体的代码运行结果如下图所示:


image.png


虽然这样能实现效果,但是官方说这是一个傻逼行为,因此官方更推荐使用 React 官方提供的 Children 方法,具体实例代码如下所示:


const Home = (props) => {
console.log(props);
const { children } = props;
return <div>{React.Children.map(children, (node) => node)}</div>;
};

const App = () => {
return (
<div>
<Home admin={true}>
<h1>你小子</h1>
<h1>小黑子</h1>
</Home>
</div>

);
};

export default App;

具体更多信息请参考 官方文档


实际上,我们经常使用的 context 就是使用的 render props 模式。


反向状态回传


这个组件的设计模式很叼很骚,就是你可以通过 render props 中的状态,提升到当前组件中也就是把容器组件内的状态,传递给父组件,具体示例代码如下所示:


import React, { useRef, useEffect } from "react";

const Home = (props) => {
console.log(props);
const dom = useRef();
const getDomRef = () => dom.current;
const handleClick = () => {
console.log("小黑子");
};
const { children } = props;
return (
<div ref={dom}>
<div>{children({ getDomRef, handleClick })}</div>
<div>{React.Children.map(children, (node) => node)}</div>
</div>

);
};

const App = () => {
const childRef = useRef(null);
useEffect(() => {
const dom = childRef.current();
dom.style.background = "red";
dom.style.fontSize = "100px";
}, [childRef]);

return (
<div>
<Home admin={true}>
{({ getDomRef, handleClick }) => {
childRef.current = getDomRef;

return <div onClick={handleClick}>你小子</div>;
}}
</Home>
</div>

);
};

export default App;

在运行代码之后,我们首先点击一下 div 元素,具体有如下输出,请看下图:


image.png


你会看到成功的在父组件操作到了子组件的 ref 实例了,还获取到了子组件的 handleClick 函数并成功调用了。


提供者模式


考虑一下这个场景,就好像爷爷要给孙子送吃的,按照之前的例子中,要通过 props 的方式把吃的送到孙子手中,你首先要经过儿子手中,再由儿子传给孙子,那万一儿子偷吃了呢?孙子岂不是饿死了.....


为了解决这个问题,React 提供了 Context 提供者模式,它可以直接跳过儿子直接把吃的送到孙子手上,具体实例代码如下所示:


import React, { createContext, useContext } from "react";

const ThemeContext = createContext({ nickname: "moment" });

const Foo = () => {
const theme = useContext(ThemeContext);
return <h1>{theme.nickname}</h1>;
};

const Home = () => {
const theme = useContext(ThemeContext);
return <h1>{theme.nickname}</h1>;
};

const App = () => {
const theme = useContext(ThemeContext);

return (
<div>
{
<ThemeContext.Provider
value={{
nickname: "你小子",
}}
>

<Foo />
</ThemeContext.Provider>
}
{
<ThemeContext.Provider
value={{
nickname: "首页",
}}
>

<Home />
</ThemeContext.Provider>
}
<div>{theme.nickname}</div>
</div>

);
};

export default App;

代码输出如下图所示:


image.png


到这里本篇文章也就结束了,Hooks 的就不讲啦,在这篇文章中有讲到一点,喜欢的可以看看 如何优雅设地计出不可维护的 React 组件


参考资料



总结


不管是使用高阶组件、render propscontext亦或是 Hooks,它们都有不同的使用场景,不能说哪个好用,哪个不好用,这就要根据到你的业务场景了,最后不得不说,React,你是真的骚啊......


最后希望这篇文章对你有帮助,如果错漏,欢迎留言指出,最后祝大嘎假期快来!


作者:Moment
来源:juejin.cn/post/7230461901356154940
收起阅读 »

别再删到手抽筋!JS中删除数组元素指南

web
作为一名前端开发工程师,我们经常需要在 JavaScript 中操作数组,其中比较常见的操作便是对数组进行元素的添加、删除和修改。在这篇文章中,我会详细介绍JS中所有删除数组元素的方法。 删除数组元素之splice() splice()方法可以向数组任意位置插...
继续阅读 »

cover.png


作为一名前端开发工程师,我们经常需要在 JavaScript 中操作数组,其中比较常见的操作便是对数组进行元素的添加、删除和修改。在这篇文章中,我会详细介绍JS中所有删除数组元素的方法。


删除数组元素之splice()


splice()方法可以向数组任意位置插入或者删除任意数量的元素,同时返回被删除元素组成的一个数组。


const arr = ['a', 'b', 'c', 'd', 'e'];
arr.splice(1, 2);//删除数组下标为1、2的元素
console.log(arr); // ["a", "d", "e"]

通过上述代码,可以看到元素'b'和'c'已被删除,被删除的元素以数组形式返回。需要注意的是,该方法会改变原数组,因此使用时应该谨慎。


删除数组元素之filter()


filter() 方法创建一个新数组,其包含通过所提供函数实现的测试的所有元素。它不会改变原始数组。


const arr = [10, 2, 33, 5];
const newArr = arr.filter(item => item !== 2);//过滤掉值为2的元素
console.log(newArr); //[10, 33, 5]

以上代码展示了如何使用 filter() 方法删除数组内某些元素。其中箭头函数 (item) => item !== 2 表示过滤掉数组元素中值为2的元素。


删除数组元素之pop()


pop() 方法用于删除并返回数组的最后一个元素。


const arr = [1, 2, 3];
const lastItem = arr.pop(); //删除元素3,lastItem为3
console.log(lastItem); //3
console.log(arr); //[1, 2]

通过上述代码可以看到,使用 pop() 方法可以非常容易地删除数组的最后一个元素。


删除数组元素之shift()


shift() 方法用于删除并返回数组的第一个元素。


const arr = [1, 2, 3];
const firstItem = arr.shift(); //删除元素1,firstItem为1
console.log(firstItem); //1
console.log(arr); //[2, 3]

与pop()类似, shift() 方法也是从数组中删除元素。但与 pop() 不同的是,它从数组头部开始删除。


删除数组元素之splice()、slice()和concat()组合操作


刚才已经讲到了 splice()方法的删除功能,现在我们还可以将slice()concat() 结合起来使用进行删除。


let arr = ['a', 'b', 'c', 'd', 'e'];
arr = arr.slice(0, 1).concat(arr.slice(2));//删除数组下标为1的元素
console.log(arr);//["a", "c", "d", "e"]

通过以上代码可以看出,使用 slice() 方法获取要删除的元素前面和后面的元素,最后使用 concat() 将两个数组合并成为一个新的数组。


删除数组元素之使用ES6中的扩展运算符


在ES6中,spread operator扩展运算符是用来展开一个可迭代对象,比如用于函数调用时的展开数组等。


let arr = ['a', 'b', 'c', 'd', 'e'];
arr = [...arr.slice(0, 1), ...arr.slice(2)];//删除数组下标为1的元素
console.log(arr);//["a", "c", "d", "e"]

通过以上代码可以看出,使用ES6中的扩展运算符(...)也可以方便地删除数组内某些元素。


总结


不同方法适用于不同情境,具体的使用应该根据情况而定。总体而言, splice()filter() 是两个最常用的方法,pop()shift() 则适合删除特定位置的元素。而在多种情况下,不同的操作组合也能实现有效删除。至于如何更好地使用这些方法,还需要根据实际情况进行深入应用和理解。


希望本文对你有所帮助,同时也欢迎拓展其他新颖的删除数组元素的方法。


作者:𝑺𝒉𝒊𝒉𝑯𝒔𝒊𝒏𝒈
来源:juejin.cn/post/7230460443189690405
收起阅读 »

快速跑通环信IM Android Demo

1、以Android 4.0.0 Demo为例https://www.easemob.com/download/demo(下载其他版本的Demo 可以修改版本号直接下载就可以)https://downloadsdk.easemob.com/downloads/...
继续阅读 »

1、以Android 4.0.0 Demo为例
https://www.easemob.com/download/demo

(下载其他版本的Demo 可以修改版本号直接下载就可以)
https://downloadsdk.easemob.com/downloads/easemob-sdk-4.0.0.zip

运行时遇到以下报错在项目build.gradle中添加运行时遇到以下报错在项目build.gradle中添加

allowInsecureProtocol = true




注意:两个地方都要修改


2、清单文件中换成自己的appkey



3、运行项目(如果不出意外的话是可以正常运行的,如遇到其他报错,把gradle 版本切换成自己可以正常运行项目的版本)



demo为了安全默认的是手机号验证码登录,切换账号密码登录,双击版本号切换



4、从环信console 注册账号(https://console.easemob.com/user/register


5、输入id,密码登录即可大功告成!


附加:环信Demo 中登录逻辑




这里的isTokenFlag参数为boolean类型(true 为id 和token 登录,false 为id 和密码登录)


收起阅读 »

低头做事,抬头看路

前言 博主出来搬砖快4年了,在职场摸爬滚打,经历挺多比较难熬的经历,在这些磨练下不断的成长,平时有空的时候我习惯去思考,当然这个跟我的习惯有关系,以前喜欢幻想各种变身机甲战斗,每个男孩子小时候的梦想。 低头做事,抬头看路 这个应该是我感受比较深的一句话,...
继续阅读 »

前言




博主出来搬砖快4年了,在职场摸爬滚打,经历挺多比较难熬的经历,在这些磨练下不断的成长,平时有空的时候我习惯去思考,当然这个跟我的习惯有关系,以前喜欢幻想各种变身机甲战斗,每个男孩子小时候的梦想。



低头做事,抬头看路



这个应该是我感受比较深的一句话,当然也是属于我自己的东西,比如说某个名人说了一句名言,但是你没有去经历过,没有去深入思考,它对你来说就是一句话,只是被名人光环笼罩着,感觉高大无比,其实对你没有什么帮助的。


接下来,找个凳子坐下,听我慢慢讲述~


image.png


低头做事





  • 干好活


我认为这是普通人最实在的一件事,也是最应该去做的,比如说在社会中有很多收入比较低的人群,他们起早贪黑,很辛苦,但是如果他们不这么做的话,可能连生活都成问题,所以努力做事是一个基础。



  • 运气差的时候


如果你读过《易经》里面会谈到人的运气类似周期变化,潜龙勿用,这里我们不讲玄学的东西,当运气差的时候,没有背景,没有靠山的时候,你再怎么吹多厉害多能干,其实是没有多大用处的,这时适合把嘴闭上,把事干好。



  • 建立信任


当你刚刚进入新的团队的时候,leader怎么会把重任给一个新人接手对吧,其实多做事,做好事,这个是建立信任的基础。如果你上来就喊要造飞机搞火箭,可能别人会觉得你是传销毕业的,不太靠谱。


image.png


我认为它是一种比较实在的做法,也是普通人最基本的生存法则,它叫低头做事。


抬头看路




低头做事作为一个基础,那么抬头看路是一个进阶版,快跑者未必先到,力战者未必能克,如果方向错了,努力事倍功半。这部分讲更多的是方法论,一种思想。



  • 方向


作为程序猿,在业界对他是有标准要求的,之前写过一篇文章是阿里工程师修炼素养里面讲到的,就是技术思维、工程思维、产品思维,但是这不是唯一标准,你可以就某一项特别突出,那一定是个人才。但很多人对自己的职业规划还是模糊的,有几方面:


一个是接触的人、事情比较少,如果跟你打交道的都是小喽喽,那你没有机会从大佬身上去学习东西。即使你没有机会接触公司这些中高层,你可以网上认识各大厂大佬,我之前在一个技术群认识很多技术大牛,至少他们可以为你的问题、你的职业抉择提出他们的想法。



毕竟你走的路,他们已经走过了。



一方面是自己没有意识去归纳,你当前这个阶段要求的能力是什么,下一阶段需要的能力、素质是什么,那下一段的要求从哪里去找呢?从你身边的大佬、业界优秀的人身上去找,甚至招聘的jd里面的要求去找。



有时迷茫是正常的,保持前进的心态,积极向上的精神



image.png



  • 参考、借鉴


在实际工作中,有很多需求其实在业界早已有解决方案了,互联网发展了几十年,你碰到的问题其实很多人都帮你踩了好几遍了,在处理这些需求的时候,需要前期的调研,比如业界有哪些优秀的设计或者思想,为什么要采用这种方式解决,跟你心里想的方案有什么差异。


就拿ddd领域来讲吧,其实这个就是换个皮又出来,高内聚、低耦合这个思想一直都是存在的,比如说各种设计模式,还有各种优化,对那些重复的代码进行抽象、聚拢,这个一直在我们身边只是没有给它定义一套方法论出来,ddd就将它讲清楚,并给了对应demo。


甚至是跨界的思维也可以帮你解决问题,这个需要你的抽象能力,就像今天有个同学在我博客下面评论他对ddd那篇文章看法,因为我项目里面应用的比较少,违背各种它的设计理念,我觉得这是大部分人的思维被技术控死了。


我举了个栗子,一棵苹果树砍成树冠,那它还是不是苹果树?大部人会觉得是的,我也是这个观点。就是你目前有结苹果的能力,或者未来有这个能力,也算苹果树。苹果树苗,人家小时候也是一点一点长起来的,又不是一下子就结果对吧。


DDD领域改造一个道理,它是一个过程,它具备往这个方向发展的能力,但是目前没有必要去做这层优化,你说我项目是不是ddd呢?我觉得算是,只是应用程度没有覆盖那么广,但是理论我们需要去了解的。



  • 机会


前面讲过,低头做事为了建立信任,那么抬头看路可以更多为你创造机会,有段时间我的飞书签名:抓住机会,创造机会。



抓住机会,创造机会



我在上家公司的时候,感受比较深,就是即使你做了很多事,其实成就不高的,原因是你干的活不被上面的人重视,或者不是很重要的活。这时你要去发现机会,抓住它,甚至主动请缨,劳资就能把事情干好,干漂亮了。即使是难度很大,要把首战必胜的信心,认真做事态度表现出来。


还有个栗子,很多高收益的项目,就是你做完之后收获很多,可能是荣誉、奖金、信任、赞赏,其实换个人去做很大几率可以干成,所以并不是你埋头苦干就能出成绩,往往属于你的机会就是那么关键几个,把它做好,做漂亮了就可以了。就像我们以前高考,把会答的题写了就已经超过60%的人了。



现在太多的人想着拿90分,但连5分的题都不屑于去做



查理芒格他们有个投资理念:棒球理论,就是有个研究,棒球在某几个格子的时候是最容易击中的,只要把那几个打好就ok了,现在的社会鼓吹一股浮躁的风气,大家都想赚大钱,拿很多荣誉,这是人性的贪在肆虐。



寻找属于自己的机会



感知能力




这个话题偏玄学一点的,当然里面的内容尽可能往实际的场景去靠,不过于虚,对读者也没有太大的帮助。上面我们讲了低头做事,抬头看路这还不够的,因为这局限于个人,人在一个场里面,或者不同场的嵌套,是会被影响的,大趋势一定比你个人的努力还重要的。(这个我觉得是前面两者做好的基础上去提高,而不是还没学会走,开始学飞)



  • 势能


有句话我听过很多次,“一命二运三风水...”,它对个人来讲有木有用呢?这个需要个人去理解、思考的,前面好几个讲的是机会问题,这个是我们决定不了的,闽南语里面“七分靠打拼,三分天注定”是这个道理。另外有个东西是我们可以去感知的,就是势能。


比如说一家技术公司,它是产品强势还是研发,还是测试,他们之间的势能不一样,会影响职场工作、项目推进,因为这是一个比较大的场。


市场也有势能,这东西就像做菜一样,买菜备料,炖的话可能要炖很久,食物才能入味,最后才能发挥它的威力。市场我们也能找到对应的事件,忽然某个社交app一直讲某件事造势,然后其他人跟着喊,然后就扩大了,最终朝着它想要去的地方发展。



个人能力、努力太单薄了,多关注势




  • 推演


这个跟上面还是有些耦合的内容,推演是依据现有的东西,然后根据自己的经验或者过往的经验进行预判。这个在五子棋的时候,ai机器人进行机器学习训练,打败了很多高手,这就是推演的魅力。


这个能力也会被决策者应用,比如说决策树,主要是列出会出现的场景,以及对应的应对措施,预防一些风险。


推演对于普通人来讲也是一项能力,比如说你能否预测下这个月的成长,今年所达到的水平,或者更长时间5年后你成长的模样,你的能力圈层能到哪里。


总结




低头做事,抬头看路,可能几年后荣誉满满、成就满满,也可能平淡无奇,人生也就那样。人生的意义不是得来衡量的,就像很多有钱人不一定就很幸福,因为他们消费水平也上去了,欲望更大了。


人生是一场体验,你在中间经历了什么,成长了什么,收获了什么,酸甜苦辣咸何尝不就是人生的味道。


image.png


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

工作这么久了,不想打工的想法越来越强烈?

每次和我的同学,朋友们电话的时候,他们总是能分享给你他所在行业以及领域的知识,并且对于自己目前的职业也是非常稳定的。 怎么说呢,就是一毕业实习或者找到自己的第一份工作,一干就是四五年,甚至更长。 他们虽然也抗拒过公司作息制度,也担忧过自己的薪资涨幅能不能跟上同...
继续阅读 »

每次和我的同学,朋友们电话的时候,他们总是能分享给你他所在行业以及领域的知识,并且对于自己目前的职业也是非常稳定的。


怎么说呢,就是一毕业实习或者找到自己的第一份工作,一干就是四五年,甚至更长。


他们虽然也抗拒过公司作息制度,也担忧过自己的薪资涨幅能不能跟上同批次的同学,但想归想,但身体总是安分的,在一家公司本本分分的工作下去,到现在不是主管经理就是别的重要职务了。



▲图/ 喝了香槟,咱就是深圳人


毕竟,一个员工在一家企业奋斗,始终最重要的品质就是忠诚,对自己的领导,对自己的公司忠诚,其次是你那出色的个人能力。


这是一个职业人的路线。


虽然和他们本质上都是职业人,但是走的路线已经出现了异同。


很有意思的是,我那同学从毕业就开始在一家公司做到现在,对岗位很敬业,对公司非常忠诚,尽管他知道老板喜欢画饼,相关福利一般般,新一线中规中矩。但还是对老板的愿景表示期待。


可能文字描述敬业已经不顶用了,那就实际情况来旁证吧。由于他所在的是乙方公司,凭借自己的敬业,负责的项目加班加点也要完成,获得了不少甲方客户的赞赏,甚至出现过甲方单独打赏几千,有的高达5k到他的私人账户上。


当然,他的老板也看在眼里,公司需要这种人,在公司已经是很难得的品质了。所以一路到现在,当上了前端组的组长,薪资按照当地中位数来算的话,已经是两倍多了。


这样的品质连我也对他表示致敬。但是在国内,这种能力像是被稀释了一般,任劳任怨的人太多太多。


或许出于站在不同的角度,我总是会敲打他工作的诟病,并引导他对于自身的规划和目的清晰点。


身边人出现了这种品质,使得我每一次的离职也好,换城市也好,都会在那么几分钟怀疑自己,怀疑自己的此举做的是不是正确的。每次出现断舍离总是充满内心活动的。



▲图/ 一个地方,最吸引你的地方是什么?


如果不这么做,内心总是备受煎熬,同时伴随着遗憾。


我尝试着计划安分守己在一家企业干到30岁,40岁,甚至退休。


表示内心仍然很难做到,我的内心总是浮现出一股强烈的自主意识,想去做一些事情,去做一些更加有意义的事情。


有这般想法,我归因于混在一群想法各异,活法各异的群体里面。在我认为的世界里面原来还有另一个世界。这种感觉就好像童年玩魂斗罗,听说有水下8关,总是充满着好奇与探索的心是一样的。


就如此次,本以为今年做足了准备和勇气选择裸辞,处于职业空档期的我会做一些具备个人发展力的路线选择。


事实上,我只是换了一个城市而已,还是一个职业码农。


说起去深圳,我甚至都没有做好充足的准备,仅仅是打算去海边游玩,见一见想见的人。


来了之后,一路上接地气的拖鞋短裤短袖装,一排排的大排档,生活充满了烟火气,那就玩几天吧?


显然,一到工作日,在深圳的伙伴也都基本上班,偶尔走走当散散心了,记录记录写几篇散文也好。


玩着玩着脑海就闪过职业人的想法。


投递着简历试试吧。


嗯?待遇还不不错,工作环境也还不错,还包吃还包住?


那就去吧。



▲图/ 工作餐,吃饭就要积极


十天左右,就没有了空档期,直接动身去上班。


在新入职一个月左右,前前后后忙碌压根没有创作的想法,一股脑地栽在工作上。


渐渐一股无形的力压迫在身上。因为所在的一家是车企,每天都能看到豪车劳斯莱斯,保时捷,宾利等等。一般的车都是见不到,基本上都是各式名车。


能够想象随便一台车都能顶一个人几年的薪资。而经常出入的客户有时只需要一句话就能将车拿下。


不清楚别的同事会有作何感想,至少我偶尔会在下班的路上emo一阵子。这人与人的财富差距,得隔几代人才能追上。


入职时,作为新员工学习了企业的文化,整整三天的洗礼。还是能够学习到一家企业的发展路线,每时每刻做出的决策和不同时期做出的战略,不能不让人佩服。


一个普通人进入金碧辉煌的宫殿,小时候能够在电视剧里看到,现在自己也成了当事人,不说表情,可能连内心活动都如出一辙。


但是作为普通人,也该有普通人的活法,稳扎稳打,逐步向上。


总归要扩大自己的眼界,开放自己的内心。


尽管我的现东家富得流油,但心态并不会规避自己,而是不断融入到集体里。学习他们的方式,学习他们对于不同时期的战略和决策。


也要懂得精于计算得失。 很多人其实不是很懂计算。绝大多数人都是在算计自己会失去多少,而不会算会得到多少。


而一般的人也总是在算短期内会失去什么,优秀则总是会算我投入后未来会有什么样的回报,前者在算计今天,目光短浅,而后者则是舍在今天,得在明天,计算的是未来。


精于计算得失的,就懂得什么是投资,不懂的只会投机。对于赚钱,你可以投机,但是对于自己最好还是投资。



▲图/ 天很蓝,是没想到的


我也一样,待稳定了自己的工作,终归还是需要花费时间和经历来投资自己,无论是在技术上,还是在行业圈子建设,又或者是领域摸索,最终还是以自身为主做出一定的成果。


这并不是承诺,而是一种必要做的过程,这也是出现和我的同学朋友走的路线出现不同的原因。


相比于职业,我更加倾向于借助职业,耳濡目染,以身作则,完备自身的空缺知识,投身于自身成果建设以及财富积累的历程。


那,我们拭目以待吧。


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

记一次蛋疼的debug经历(cycle inside)

甲方公司的大部分业务都是做成私有pod的,本人这次接手的也不例外。由于是二次开发,一开始图方便并没有做成pod而是把业务代码全部放测试工程中。昨天做完之后复制一下前面的.podspec,修改了部分信息后pod install,pod完成以为没问题了,谁知一运行...
继续阅读 »

甲方公司的大部分业务都是做成私有pod的,本人这次接手的也不例外。由于是二次开发,一开始图方便并没有做成pod而是把业务代码全部放测试工程中。昨天做完之后复制一下前面的.podspec,修改了部分信息后pod install,pod完成以为没问题了,谁知一运行直接报错:
cycle inside 工程名: building could producce xxx(后面省略)


这个报错内容还是挺详细的,但一开始没有多想,直接谷歌搜一下,stackoverflow确实有类似问题。发现很多回答也是只会叫你清缓存清derive data,还有叫你用老的编译系统,移动build phases的顺序,甚至还有命令行开启swift编译环境的。我移动了build phases的顺序发现没效果。然后在苹果开发者论坛看到一个教程教你解决库的依赖循环


此时就想着:导致依赖循环的原因难道是头文件的引用出了问题?可是OC的#import也不会重复导入,前面也一直没有报错提示。但也没办法,只能死马当作活马医,先排查了公共头文件,发现确实很多.h都直接引用此文件,于是先挨个解耦。运行,报错依旧。于是继续检查清除一些不必要的引用,还是没效果。


不得已只能重新查看报错信息,其实一开始就已经丢到翻译网页上,只是内容太长,一直没有细看。这时发现报错提到pod在生成图片资源的时候打出来一个'\\\\.bundle',和其他组件一对比明显有问题,于是检查.podspec,发现在设置s.resource_bundles的时候,居然是换行的:


s.resource_bundles = {
'xxx
' => ['xxx/Assets/*']
}

并且图片资源里有子文件夹的,也没有加上**/,修改完之后如下


s.resource_bundles = {
'xxx' => ['xxx/Assets/**/*']
}

修改完重新pod install之后,终于运行成功。真不知道前面第一版是怎么集成进去的。。。


从昨天下午发现问题到现在解决,总耗时估计有6-8小时,果然魔鬼都在细节里。以后还是要认真查看报错信息,不要单纯依赖搜索,更加不要指望清缓存


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

Kotlin ?: 语法糖的 一个坑

问题还原 先定义一个函数 /** * @param s 字符串 * @return 参数的 长度如果不为1 返回null */ fun testStr(s: String): String? { return if (s.length == 1...
继续阅读 »

问题还原


先定义一个函数


/**
* @param s 字符串
* @return 参数的 长度如果不为1 返回null
*/
fun testStr(s: String): String? {
return if (s.length == 1) {
"length:1"
} else {
null
}
}

然后定义一个类 里面有个函数调用了上面的方法


class Test {
fun testLet() {
testStr("1")?.let {
println("inner let-1")
testStr("11")?.let {
println("inner let-2")
} ?: println("inner else")

} ?: println("outer else")
}
}

在main函数里面跑一下


fun main() {

val t = Test()
t.testLet()
}

看下运行结果:


image.png


到这里还是好理解的, 但是我如果稍微改一下我们的代码


class Test {
fun testLet() {
testStr("1")?.let {
println("inner let-1")
testStr("11")?.let {
println("inner let-2")
}
} ?: println("outer else")
}
}

这个时候 你认为程序应该打印什么?


我觉得多数人可能会认为 程序只会打印一行
inner let-1


然而你运行一下你就会发现:


image.png


他竟然还多打印了 outer else


这个行为我估计很多kotlin开发者就理解不了了, 因为按照javaer 的习惯, if 和 else 总会配对出现
我们上面的代码, 内部的判断逻辑 没有写else 那就不应该有任何行为啊, 为什么 内部的else 逻辑没写的时候
自动给我走了外部的else逻辑?


我们反编译看一下


image.png


这个反编译的代码是符合 程序运行结果的,虽然 他肯定不符合 多数kotlin开发者的意愿,


我们再把代码修改回去,让我们内部的inner case 也有一个else的逻辑


class Test {
fun testLet() {
testStr("1")?.let {
println("inner let-1")
testStr("11")?.let {
println("inner let-2")
} ?: println("inner else")
} ?: println("outer else")
}
}

再反编译看一下代码:


image.png


这个代码就很符合我们写代码时候的意愿了


到这里我们就可以下一个结论


对于 ?: 这样的语法糖来说 ,如果内部 使用了?. 的写法 而没有写?: 的逻辑,则当内部的代码真的走到else的逻辑时,外部的?: 会默认执行


另外一个有趣的问题


下面的let 为什么会报错?


image.png


这里就有点奇怪了,这个let的写法为什么会报错? 但是run的写法 却是ok的?
而且这个let的写法 在我们第一个小节的代码中也没报错啊? 这到底是咋回事


看下这2个方法的定义


image.png


image.png


最关键就在于 这个let 需要有一个receiver, 而如果这个let 刚才在main函数中的写法就不对了


因为main函数 显然不属于任何一个对象,也不属于任何一个类,let在这个执行环境下 找不到自己的receiver 自然就会编译报错了


但是对于run 来说,这个函数的定义没有receiver的概念,所以可以运行


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

轻松便捷,使用Compose的Scaffold搭建一个首页布局

节后第一篇文章,稍微轻松一点,简单介绍下Scaffold的使用。在如今各式各样的app里面,如果我们想要在里面寻找一个共同点,那么就是基本每个app首页都是以底部tab切换页面加上顶部导航栏的形式出现,所以Compose也专门为了这一点设计出自己的便捷式布局-...
继续阅读 »

节后第一篇文章,稍微轻松一点,简单介绍下Scaffold的使用。在如今各式各样的app里面,如果我们想要在里面寻找一个共同点,那么就是基本每个app首页都是以底部tab切换页面加上顶部导航栏的形式出现,所以Compose也专门为了这一点设计出自己的便捷式布局--Scaffold,我们称为脚手架布局,如同字面意思,脚手架布局就是将各种Meterial Design风格的组件组合在一起形成一个具有Meterial Design风格的布局


Scaffold的参数


学习Compose的组件第一步我们肯定是去看看这个组件都支持哪些参数,哪些必传哪些可选,当我们点到Scaffold里面去查看它的参数的时候,会发现它的参数基本囊括了我们熟知的首页所具备的一切元素


image.png

其中有几个参数是专门用来设置Composable组件的,它们分别是



  • topBar:顶部导航栏布局,推荐使用TopAppBar组件

  • bottomBar:底部导航栏,推荐使用BottomAppBar

  • snackBarHost:Snackbars组件

  • floatingActionButton:悬浮按钮

  • drawerContent:侧边栏布局

  • content: Scaffold需要呈现出来的布局


一般一个普通的app首页,使用topBar,bottomBar和content这三个参数就可以搭建出来,我们来尝试下


搭建首页


导航栏


首先创建个Composable函数,在最外层使用Scaffold布局,然后topBar就使用官方推荐的TopAppBar组件


image.png

而这个TopAppBar组件在AppBar.kt文件里面,有两个重载函数,一个是内部布局完全自定义的


image.png

一个是如同脚手架布局一样帮你已经排版好了布局,你只需要往里面添加相应组件就好


image.png

我们这边就选择第二种方式来做我们的导航栏,代码如下


image.png

navigationIcon对应着导航栏上最左边的图标,一般可以放返回键,菜单键,这边使用系统自带的菜单icon,title对应着导航栏上的标题,可以直接放一个文案,也可以放一个布局,actions是一个RowScope,放置在导航栏的右侧,在里面可以添加一些小按钮,使用的也都是系统自带的icon,效果图如下


image.png

很方便是不,想想看这样的一个布局我们用传统xml形式来写需要多少代码量,用我们的脚手架布局几行代码就搞定了,接下来做底部导航栏


底部导航栏


底部导航栏也使用官方推荐的BottomAppBar,它也是个RowScope,所以往里面添加组件就能满足水平布局的效果,而我们每一个tab都可以使用另外一个组件,BottomNavigationItem来表示,它的参数如下所示


image.png

  • selected:是否被选中

  • onClick:点击事件

  • icon:图标组件,通常是个矢量图

  • modifier:操作符

  • enabled:是否可点击

  • label:文案

  • alwaysShowLabel:默认为true,表示文案常驻在图标底下,如果是false,那么只有等item被选中时候文案才会展示出来

  • interactionSource:监听导航栏点击事件

  • selectedContentColor:选中的颜色

  • unselectedContentColor:未被选中的颜色


知道参数以后,我们就可以把底部导航栏添加到布局里面


image.png

selectedIndex记录着我们点击过的下标值,我们可以通过下标值来判断应该展示哪个页面,代码如下


image.png

一个简单的首页切换页面的效果就出来了,效果如下


0503aa1.gif

侧边栏


刚刚我们看到Scaffold的参数里面还有大量drawerxxxx相关的参数,不用猜肯定知道是用来设置侧边栏的,其中drawerContent就是拿来写侧边栏里面的视图,那么我们也简单写个侧边栏布局看看


image.png

代码很简单就是一个头像加一个用户名,运行下代码看看


0504aa1.gif

加了drawerContent以后我们在界面上轻松向右一滑就出来个侧边栏了,能够通过手势直接滑出侧边栏主要是因为Scaffold里面的drawerGesturesEnabled这个参数默认为true,我们如果说哪一天不要侧边栏了,又不想把代码删掉,可以直接把drawerGesturesEnabled设置为false就好了,但是这里有个问题,我们看到drawerxxx参数里面没有用来设置去打开侧边栏的参数,要知道现在应用当中但凡有侧边栏功能的,都会提供一个按钮提示用户去点击展示出侧边栏,还是说Compose里面的侧边栏里面没有这功能?当然不是,我们看下Scaffold里面第二个参数,也就是ScaffoldState,我们看下这个状态类里面都有啥


image.png

总共俩参数,第一个就是DrawerState,一看就知道是拿来设置侧边栏状态的,我们再去DrawerState里面看看


image.png

第一个参数DrawerValue就是拿来设置侧边栏打开还是关闭的,他有两个值,分别是ClosedOpen,第二个参数是用来监听侧边栏开启或者关闭状态的,暂时用不到先忽略,这样我们先在代码当中创建一个ScaffoldState,并且传入一个DrawerState,代码如下


image.png

到了这一步我们知道了如果要通过某个按钮来展示侧边栏,只需要改变drawerStateDrawerValue属性就好,从Closed改变到Open,那怎么变呢?我们使用DrawerState里面提供的animateTo函数


image.png

我们看到animateTo函数里面第一个参数就是目标值,也就是我们需要设置成DrawerValue.Open的地方,第二个参数是个动画参数,因为侧边栏展示出来有个滑动的动画过程,当然如果不需要动画可以使用另一个函数snapTo就可以了,这个函数只需要设置一个targetValue值,另外无论是animateTo还是snapTo,都是挂起函数,所以我们还需要为它提供一个协程作用域,这里直接使用
rememberCoroutineScope函数来创建协程作用域


image.png

现在我们就可以在界面顶部的菜单按钮上设置一个打开侧边栏的动作,代码如下


image.png

代码到这里就结束了,我们看下效果


0504aa2.gif

总结


Scaffold使用起来就相当于我们平时用过的自定义View一样,已经把一些常用的视图和逻辑已经封装在里面,我们开发时候可以使用已经被封装过的组件,也可以自己去写视图,开发起来也是很方便的.


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

不是吧不是吧,fdsan 都不知道?

背景 fd 是什么 In Unix and Unix-like computer operating systems, a file descriptor (FD, less frequently fildes) is a process-unique id...
继续阅读 »

背景


fd 是什么



In Unix and Unix-like computer operating systems, a file descriptor (FD, less frequently fildes) is a process-unique identifier (handle) for a file or other input/output resource, such as a pipe or network socket.



fd 通常用作进程内或进程间通信,是进程独有的文件描述符表的索引,简单来说,就是系统内核为每个进程维护了一个 fd table,来记录进程中的fd,通常在android 系统上,每个进程所能最大读写的fd数量是有限的,如果超限,会出现fd 无法创建/读取的问题。


fdsan 是什么


fdsan 全称其实是 file descriptor sanitizer,是一种用于检测和清除进程中未关闭的文件描述符(fd)的工具。它通常用于检测程序中的内存泄漏和文件句柄泄漏等问题。文件描述符是操作系统中用于访问文件、网络套接字和其他I/O设备的机制。在程序中,打开文件或套接字会生成一个文件描述符,如果此文件描述符在使用后未关闭,就会造成文件句柄泄漏,导致程序内存的不断增加。fd sanitizer会扫描进程的文件描述符表,检测未关闭的文件描述符,并将它们关闭,以避免进程内存泄漏。


fdsan in Android


在 Android 上,fdsan(File Descriptor Sanitizer)是自 Android 11 开始引入的一项新功能。fdsan 旨在帮助开发人员诊断和修复 Android 应用程序中的文件描述符泄漏和使用错误。


fdsan 使用 Android Runtime (ART) 虚拟机中的功能来捕获应用程序的文件描述符使用情况。它会跟踪文件描述符的分配和释放,并在文件描述符泄漏或错误使用时发出警告。fdsan 还支持在应用程序崩溃时生成详细的调试信息,以帮助开发人员诊断问题的根本原因。


常见场景



void thread_one() {
int fd = open("/dev/null", O_RDONLY);
close(fd);
close(fd);
}

void thread_two() {
while (true) {
int fd = open("log", O_WRONLY | O_APPEND);
if (write(fd, "foo", 3) != 3) {
err(1, "write failed!");
}
}
}

同时运行上述两个线程,你会发现


thread one                                thread two
open("/dev/null", O_RDONLY) = 123
close(123) = 0
open("log", O_WRONLY | APPEND) = 123
close(123) = 0
write(123, "foo", 3) = -1 (EBADF)
err(1, "write failed!")

断言失败可能是这些错误中最无害的结果:也可能发生静默数据损坏或安全漏洞(例如,当第二个线程正在将用户数据保存到磁盘时,第三个线程进来并打开了一个连接到互联网的套接字)。


检测原理


fdsan 试图通过文件描述符所有权来强制检测或者预防文件描述符管理错误。与大多数内存分配可以通过std::unique_ptr等类型来处理其所有权类似,几乎所有文件描述符都可以与负责关闭它们的唯一所有者相关联。fdsan提供了将文件描述符与所有者相关联的函数;如果有人试图关闭他们不拥有的文件描述符,根据配置,会发出警告或终止进程。


实现这个的方法是提供函数在文件描述符上设置一个64位的关闭标记。标记包括一个8位的类型字节,用于标识所有者的类型(在<android/fdsan.h>中的枚举变量 android_fdsan_owner_type),以及一个56位的值。这个值理想情况下应该是能够唯一标识对象的东西(原生对象的对象地址和Java对象的System.identityHashCode),但是在难以为“所有者”推导出标识符的情况下,即使对于模块中的所有文件描述符都使用相同的值也很有用,因为它会捕捉关闭您的文件描述符的其他代码。


如果已标记标记的文件描述符使用错误的标记或没有标记关闭,我们就知道出了问题,就可以生成诊断信息或终止进程。


在Android Q(11)中,fdsan的全局默认设置为单次警告。可以通过<android/fdsan.h>中的android_fdsan_set_error_level函数在运行时使 fdsan 更加严格或宽松。


fdsan捕捉文件描述符错误的可能性与在您的进程中标记所有者的文件描述符百分比成正比。


常见问题


E fdsan : failed to exchange ownership of file descriptor: fd xxx is owned by ParcelFileDescriptor 0xfffddddd was expected to be unowned

通常情况下,fd 所有权的误用并不会造成闪退,但是由于国内外厂商对 framework 的魔改,目前线上高频出现对应的闪退,为了规避这类情况,我们首先要规范 fd 的使用,特别是所有权的迁移,另外,在操作涉及到 localsocketsharedmemory 时,要慎之又慎,系统会为每个进程记录一份 fd table,会记录每个fd 对应的所有权。如果长时间不释放并且又在不断分配,会出现fd 超限问题,报错提示 cannot open fd


image.png


来看看 java 侧对文件描述符操作的注释



/**
* Create a new ParcelFileDescriptor that is a dup of the existing
* FileDescriptor. This obeys standard POSIX semantics, where the
* new file descriptor shared state such as file position with the
* original file descriptor.
*/
public ParcelFileDescriptor dup() throws IOException {
if (mWrapped != null) {
return mWrapped.dup();
} else {
return dup(getFileDescriptor());
}
}


/**
* Create a new ParcelFileDescriptor from a raw native fd. The new
* ParcelFileDescriptor holds a dup of the original fd passed in here,
* so you must still close that fd as well as the new ParcelFileDescriptor.
*
* @param fd The native fd that the ParcelFileDescriptor should dup.
*
* @return Returns a new ParcelFileDescriptor holding a FileDescriptor
* for a dup of the given fd.
*/
public static ParcelFileDescriptor fromFd(int fd) throws IOException {
final FileDescriptor original = new FileDescriptor();
original.setInt$(fd);

try {
final FileDescriptor dup = new FileDescriptor();
int intfd = Os.fcntlInt(original, (isAtLeastQ() ? F_DUPFD_CLOEXEC : F_DUPFD), 0);
dup.setInt$(intfd);
return new ParcelFileDescriptor(dup);
} catch (ErrnoException e) {
throw e.rethrowAsIOException();
}
}


/**
* Return the native fd int for this ParcelFileDescriptor and detach it from
* the object here. You are now responsible for closing the fd in native
* code.
* <p>
* You should not detach when the original creator of the descriptor is
* expecting a reliable signal through {@link #close()} or
* {@link #closeWithError(String)}.
*
* @see #canDetectErrors()
*/
public int detachFd() {
if (mWrapped != null) {
return mWrapped.detachFd();
} else {
if (mClosed) {
throw new IllegalStateException("Already closed");
}
int fd = IoUtils.acquireRawFd(mFd);
writeCommStatusAndClose(Status.DETACHED, null);
mClosed = true;
mGuard.close();
releaseResources();
return fd;
}

Share两个闪退案例:



  1. fd 超限问题


W zygote64: ashmem_create_region failed for 'indirect ref table': Too many open files

这个时候我们去查看 系统侧对应 fd 情况,可以发现,fd table 中出现了非常多的 socket 且所有者均显示为unix domain socket,很明显是跨进程通信的 socket 未被释放的原因



  1. fd 所有权转移问题


[DEBUG] Read self maps instead! map: 0x0

[]()****#00 pc 00000000000c6144 /apex/com.android.runtime/bin/linker64 (__dl_abort+168)

[]()****#01 pc 00000000000c6114 /apex/com.android.runtime/bin/linker64 (__dl_abort+120)

这个堆栈看得人一头雾水,因为蹦在 linker 里,我们完全不知道发生了什么,但是通过观察我们发现问题日志中都存在如下报错


E fdsan : failed to exchange ownership of file descriptor: fd xxx is owned by ParcelFileDescriptor 0xsssssss was expected to be unowned

根据上述知识,我们有理由怀疑是代码中fd 的操作合法性存在问题,通过细致梳理,我们得出了对应这两类问题的一些action:


所以有以下对应的action:




  • local socket 要及时关闭 connection,避免 fd 超限问题。




  • sharememory 从 进程A 转移到 进程B 时,一定要 detachFd 进行 fd 所有权转移,如果需要在进程 A 内进行缓存,那么 share 给进程B 时需要对 fd 进行 dup 操作后再 detachFd




版本差异


fdsan 在 Android 10 上开始引入,在Android 10 上会持续输出检测结果,在Android 11 及以上,fdsan 检测到错误后会输出错误日志并中止检测,在 Android 9 以下,没有对应的实现。所以,如果你需要在代码中引入fdsan 来进行 fd 校验检测。请参照以下实现:


extern "C" {
void android_fdsan_exchange_owner_tag(int fd,
uint64_t expected_tag,
uint64_t new_tag)
__attribute__((__weak__));
}

void CheckOwnership(uint64_t owner, int fd) {
if (android_fdsan_exchange_owner_tag) {
android_fdsan_exchange_owner_tag(fd, 0, owner);
}
}

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

【直播开发】Android 端实现 WebSocket 通信

前言 在之前的文章中,我们知道了 WebSocket 是一种全双工通信协议。本文将介绍如何在 Android 端使用 WebSocket 进行双向通信。其中包括创建 WebSocket 对象、发送和接收数据等操作。 创建 WebSocket 对象 要使用 We...
继续阅读 »

前言


在之前的文章中,我们知道了 WebSocket 是一种全双工通信协议。本文将介绍如何在 Android 端使用 WebSocket 进行双向通信。其中包括创建 WebSocket 对象、发送和接收数据等操作。


创建 WebSocket 对象


要使用 WebSocket 对象,我们需要先创建一个 WebSocket 客户端对象。在 Android 中,我们可以使用 OkHttp 库来创建 WebSocket 客户端对象。在开始之前,我们先在 Gradle 文件中添加 OkHttp 库的依赖:


implementation 'com.squareup.okhttp3:okhttp:4.9.3'

在代码中创建 WebSocket 客户端对象的示例如下:


import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.WebSocket

//创建一个 OkHttpClient 对象
val client = OkHttpClient()

//请求体
val request = Request.Builder()
.url("wss://example.com/websocket")
.build()

//通过上面的 client 创建 webSocket
val webSocket = client.newWebSocket(request, object : WebSocketListener() {
override fun onOpen(webSocket: WebSocket, response: Response) {
// WebSocket 连接建立成功
}

override fun onMessage(webSocket: WebSocket, text: String) {
// 收到 WebSocket 服务器发送的文本消息
}

override fun onMessage(webSocket: WebSocket, bytes: ByteString) {
// 收到 WebSocket 服务器发送的二进制消息
}

override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
// WebSocket 连接失败
}

override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
// WebSocket 连接关闭
}
})

在上面的示例中,我们首先创建了一个 OkHttpClient 对象,然后使用 Request.Builder 类构建了一个 WebSocket 请求对象,并指定了 WebSocket 服务器的地址。接着,我们调用 OkHttpClient 对象的 newWebSocket() 方法,传入 WebSocket 请求对象和一个 WebSocketListener 对象,来创建一个 WebSocket 客户端对象。在 WebSocketListener 对象中,我们可以实现 WebSocket 连接建立、收到消息、连接失败、连接关闭等事件的回调函数。


发送和接收数据


WebSocket 客户端对象创建成功后,我们可以通过 send() 方法来向 WebSocket 服务器发送消息。在 WebSocketListener 对象中的 onMessage() 方法中,我们可以接收到 WebSocket 服务器发送的消息。下面是一个发送和接收文本消息的示例:


val message = "Hello, WebSocket!"

webSocket.send(message)

// 在 onMessage() 方法中接收消息
override fun onMessage(webSocket: WebSocket, text: String) {
Log.d(TAG, "Received message: $text")
}

如果要发送二进制消息,可以使用 send() 同名的另一个重载方法:


val message = ByteString.encodeUtf8("Hello, WebSocket!")

webSocket.send(message)

// 在 onMessage() 方法中接收消息
override fun onMessage(webSocket: WebSocket, bytes: ByteString) {
Log.d(TAG, "Received message: ${bytes.utf8()}")
}

关闭 WebSocket 连接


当 WebSocket 连接不再需要时,我们可以调用 WebSocket 对象的 close() 方法来关闭连接,及时释放资源,避免引起内存泄漏。在 WebSocketListener 对象中的 onclose() 方法中,我们可以接收到 WebSocket 关闭事件,可以在该事件中执行一些清理操作。下面是一个关闭 WebSocket 连接的示例:


webSocket.close(NORMAL_CLOSURE_STATUS, "WebSocket connection closed")

// 在 onClosed() 方法中接收关闭事件
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
Log.d(TAG, "WebSocket connection closed: $reason")
}

在上面的示例中,我们调用 WebSocket 对象的 close() 方法来关闭连接,传入一个关闭代码和关闭原因。在 WebSocketListener 对象中的 onClosed() 方法中,我们可以接收到 WebSocket 关闭事件,并处理关闭原因。


完整示例


下面是一个完整的 WebSocket 通信示例,包括创建 WebSocket 对象、发送和接收消息、关闭连接等操作:


import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*
import okhttp3.*
import okio.ByteString

class MainActivity : AppCompatActivity() {

private lateinit var webSocket: WebSocket

companion object {
private const val TAG = "WebSocketDemo"
private const val NORMAL_CLOSURE_STATUS = 1000
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

val client = OkHttpClient()

val request = Request.Builder()
.url("wss://echo.websocket.org")
.build()

webSocket = client.newWebSocket(request, object : WebSocketListener() {
override fun onOpen(webSocket: WebSocket, response: Response) {
webSocket.send("Hello, WebSocket!")
}

override fun onMessage(webSocket: WebSocket, text: String) {
Log.d(TAG, "Received message: $text")
}

override fun onMessage(webSocket: WebSocket, bytes: ByteString) {
Log.d(TAG, "Received message: ${bytes.utf8()}")
}

override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
Log.e(TAG, "WebSocket connection failed", t)
}

override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
Log.d(TAG, "WebSocket connection closed: $reason")
}
})

btn_send.setOnClickListener {
val message = et_message.text.toString()
webSocket.send(message)
}

btn_close.setOnClickListener {
webSocket.close(NORMAL_CLOSURE_STATUS, "WebSocket connection closed")
}
}
}

在上面的示例中,我们在 Activity 的 onCreate() 方法中创建了 WebSocket 客户端对象,并通过按钮的点击事件来发送消息和关闭连接。我们使用了 Echo WebSocket 服务器来测试 WebSocket 通信。在实际开发中,我们可以使用自己的 WebSocket 服务器来进行通信。


总结


WebSocket 是一种全双工通信协议,可以在 Android 应用程序中使用 WebSocket 对象实现双向通信。通过 OkHttp 库,我们可以创建 WebSocket 客户端对象,使用 send() 方法发送消息,使用 WebSocketListener 回调接口处理事件。在实际应用中,我们可以使用 WebSocket 协议来实现实时交互、即时通信等功能,提升 Android 应用程序的用户体验和竞争力。


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

Java、Kotlin不香吗?为什么Flutter要选用Dart作为开发语言?

对于任何想要了解一门新兴技术的开发者来说,语言常常是横亘在学习之路上的第一道障碍,如C/C++之于音视频,Python之于人工智能等,当然也包括Dart之于Flutter。 尤其当你原先从事的是Android开发时,你肯定也曾产生过这样的疑惑: 既然同可以归...
继续阅读 »

对于任何想要了解一门新兴技术的开发者来说,语言常常是横亘在学习之路上的第一道障碍,如C/C++之于音视频,Python之于人工智能等,当然也包括Dart之于Flutter。


尤其当你原先从事的是Android开发时,你肯定也曾产生过这样的疑惑:



既然同可以归到移动开发的范畴,也同属于Google旗下的团队,为什么Flutter不能沿用既有的Java或Kotlin语言来进行开发呢?



通过阅读本文,你的疑惑将得到充分的解答,你不仅能够了解到Flutter团队在选用Dart作为开发语言时的考量,还能充分感受到使用Dart语言进行开发的魅力所在。


照例,先奉上思维导图一张,方便复习:





热重载 (Hot Reload)一直以来都是Flutter对外推广的一大卖点,这是因为,相对于现有的基于原生平台的移动开发流程来讲,热重载在开发效率上确实是一个质的飞跃。



简单讲,热重载允许你在无需重启App的情况下,快速地构建页面、添加功能或修复错误。这个功能很大程度上依赖于Dart语言的一个很突出的特性:


同时支持AOT编译与JIT编译


AOT编译与JIT编译


AOT Compilation(Ahead-of-Time Compilation, 提前编译)是指在程序执行之前,将源代码或中间代码(如Java字节码)转换为可执行的机器码的过程。这么做可以提高程序的执行效率,但也需要更长的编译时间。


JIT Compilation(Just-in-Time Compilation, 即时编译)是指在程序执行期间,将源代码或中间代码转换为可执行的机器码的过程。这么做可以提高程序的灵活性和开发效率,但也会带来一些额外的开销,例如会对程序的初始执行造成一定的延迟。


用比较贴近生活的例子来解释二者之间的区别,就是:



AOT编译就像你在上台演讲之前,把原本全是英文的演讲稿提前翻译成中文,并写在纸上,这样当你上台之后,就可以直接照着译文念出来,而不需要再在现场翻译,演讲过程能更为流畅,但就是要在前期花费更多的时间和精力来准备。




JIT编译就像你在上台演讲之前,不需要做过多的准备,等到上台之后,再在现场将演讲稿上的英文逐句翻译成中文,也可以根据实际情况灵活地调整演讲内容,但就是会增加演讲的难度,遇到语法复杂的句子可能也会有更多的停顿。



可以看到,两种编译方式的应用场景不同,各有优劣,而Dart是为数不多的同时支持这两种编译方式的主流编程语言之一。根据当前所处项目阶段的不同,Dart提供了两种不同的构建模式:开发模式与生产模式。


开发模式与发布模式


在开发模式下,会利用 Dart VM 的 JIT 编译器,在运行时将内核文件转换为机器码,以实现热重载等功能,缩短开发周期。


热重载的流程,可以简单概括为以下几步:




  1. 扫描改动:当我们保存编辑内容或点击热重载按钮时,主机会扫描自上次编译以来的任何有代码改动的文件。




  2. 增量编译:将有代码改动的文件增量编译为内核文件。




  3. 推送更新:将内核文件注入到正在运行的 Dart VM。




  4. 代码合并:使用新的字段和函数更新类。




  5. Widget重建:应用的状态会被保留,并重建 widget 树,以便快速查看更改效果。




而在发布模式下,则会利用 Dart VM 的 AOT 编译器,在运行前将源代码直接转换为机器码,以实现程序的快速启动和更流畅地运行。


这里的“更流畅地运行”指的是在运行时能够更快地响应用户的操作,提供更流畅的用户体验,而不是单指让程序运行得更“快”。


这是因为Dart代码在被转换为机器码后,是可以直接在硬件上运行的,而不需要在运行时进行解释或编译,因此可以减少运行时的开销,提高程序的执行效率。


此外,经 AOT 编译后的代码,会强制执行健全的 Dart 类型系统,并使用快速对象分配和分代垃圾收集器来更好地管理内存。


因此,根据当前所处项目阶段的不同,采用不同的构建模式,Dart语言可以实现两全其美的效果


单线程模型


现如今,几乎所有的智能终端设备都支持多核CPU,为使应用在设备上能有更好的表现,我们常常会启动多个共享内存的线程,来并发执行多个任务。


大多数支持并发运行线程的计算机语言,如我们熟知的Java、Objective-C等,都采用了“抢占”的方式在线程之间进行切换,每个线程都被分配了一个时间片以执行任务,一旦超过了分配的时间,操作系统就会中断当前正在执行的线程,将CPU分配给正在等待队列的下一个线程。


但是,如果是在更新线程共享资源(如内存)期间发生的抢占行为,则可能会引致竞态条件的产生。竞态条件会导致严重的错误,轻则数据丢失,重则应用崩溃,且难以被定位和修复。


修复竞争条件的典型做法就是加锁,但锁本身会导致卡顿,甚至引发死锁等更严重的问题。


那Dart语言又是怎么解决这个问题的呢?


Dart语言采用了名为Isolate的单线程模型,Isolate模型是以操作系统提供的进程和线程等更为底层的原语进行设计的,所以你会发现它既有进程的特征(如:不共享内存),又有线程的特征(如:可处理异步任务)。


正如Isolate这个单词的原意“隔离”一样,在一个Dart应用中,所有的Dart代码都在Isolate内运行,每个Isolate都会有自己的堆内存,从而确保Isolate之间相互隔离,无法互相访问状态。在需要进行通信的场景里,Isolate会使用消息机制。


因为不共享内存,意味着它根本不允许抢占,因此也就无须担心线程的管理以及后台线程的创建等问题。


在一般场景下,我们甚至完全无需关心Isolate,通常一个Dart应用会在主Isolate下执行完所有代码。


虽然是单线程模型,但这并不意味着我们需要以阻塞UI的方式来运行代码,相反,Dart语言提供了包括 async/await 在内的一系列异步工具,可以帮助我们处理大部分的异步任务。关于 async/await 我们后面会有一篇单独的文章讲到,这里先不展开,只需要知道它跟Kotlin的协程有点像就可以了。



如图所示,Dart代码会在readAsString()方法执行非Dart代码时暂停,并在 readAsString()方法返回值后继续执行。


Isolate内部会运行一个消息循环,按照先进先出的模式处理重绘、点击等事件,可以与Android主线程的Looper相对照。



如图所示,在main()方法执行完毕后,事件队列会依次处理每一个事件。


而如果某个同步执行的操作花费了过长的处理时间,可能会导致应用看起来像是失去了响应。



如图所示,由于某个点击事件的同步处理耗时过长,导致其超过了处理两次重绘事件的期望时间间隔,直观的呈现就是界面卡顿。


因此,当我们需要执行消耗CPU的计算密集型工作时,可以将其转移到另外一个Isolate上以避免阻塞事件循环,这样的Isolate我们称之为后台运行对象



如图所示,生成的这个Isolate会执行耗时的计算任务,在结束后退出,并把结果返回。


由于这个Isolate持有自己的内存空间,与主Isolate互相隔离,因此即使阻塞也不会对其他Isolate造成影响。


快速对象分配与分代垃圾回收


在Android中,视图 (View)是构成用户界面的基础块,表示用户可以看到并与之交互的内容。在Flutter中,与之大致对应的概念则是Widget。Widget也是通过多个对象的嵌套组合,来形成一个层次结构关系,共同构建成一棵完整的Widget树。


但两者也不能完全等同。首先,Widget并非视图本身,最终的UI树是由一个个称之为Element的节点构成的;其次,Widget也不会直接绘制任何内容,最终的绘制工作是交由RenderObject完成的。Widget只是一个不可变的临时对象,用于描述在当前状态下视图应该呈现的样子


而所谓的Widget树只是我们描述组件嵌套关系的一种说法,是一种虚拟的结构。但 Element和RenderObject是在运行时实际存在的,如图:



这就好比手机与其规格参数的关系。Widget就像是一台手机的规格参数,是对当前组装成这个手机的真正的硬件配置的描述,当手机的硬件有更新或升级时,重新生成的规格参数也会有所变化。


由于Widget是不可变的,因此,我们无法直接对其更新,而是要通过操作状态来实现。但实际上,当Widget所依赖的状态发生改变时,Flutter框架就会重新创建一棵基于当前最新状态绘制的新的Widget树,对于原先的Widget来说它的生命周期其实已经结束了。


有人可能会对这种抛弃了整棵Widget树并完全重建一棵的做法存有疑问,担心这种行为会导致Flutter频繁创建和销毁大量短暂的Widget对象,给垃圾回收带来了巨大压力,特别对于一些可能由数千个Widget组合而成的复杂页面而言。


实际上这种担心完全没有必要,Dart的快速对象分配与分代垃圾回收足以让它应对这种情况。


快速对象分配


Dart以指针碰撞(Bump Pointer)的形式来完成对象的内存分配。


指针碰撞是指在堆内存中,Dart VM使用一个指针来跟踪下一个可用的内存位置。当需要分配新的内存时,Dart VM会将指针向前移动所需内存大小的距离,从而分配出新的内存空间



这种方式可以快速地分配内存,而不需要查找可用的内存段,并且使内存增长始终保持线性


另外,前面我们提到,由于每个Isolate都有自己的堆内存,彼此隔离,无法互相访问状态,因此可以实现无锁的快速分配。


分代垃圾回收


Dart的垃圾回收器是分代的,主要分为新生代(New Generation)与老年代(Old Generation)。


新生代用于分配生命周期较短的临时对象。其所在的内存空间会被分为两半,一个处于活跃状态,另一个处于非活跃状态,并且任何时候都只使用其中的一半。



新的对象会被分配到活跃的那一半,一旦被填满,垃圾回收器就会从根对象开始,查找所有对象的引用状态。




被引用到的对象会被标记为存活状态,并从活跃的一半复制到非活跃的一半。而没有被引用到的对象会被标记为死亡状态,并在随后的垃圾回收事件中被清除。



最后,这两半内存空间会交换活跃状态,非活跃的一半会再次变成活跃的一半,并且继续重复以上过程。



当对象达到一定的生命周期后,它们会被提升为老年代。此时的垃圾回收策略会分为两个阶段:标记与清除。


首先,在标记阶段,会遍历整个对象图,标记仍在使用的对象。


随后,在清除阶段,会扫描整个内存,回收任何没有被标记的对象,然后清除所有标记。


这种形式的垃圾回收发生频率不高,但有时需要暂停Dart Runtime以支持其运行。


为了最小化地降低垃圾回收事件对于应用程序的影响,垃圾回收器为Flutter引擎提供了钩子,当引擎检测到应用程序处于空闲状态并且没有用户交互时会发出通知,使得垃圾回收器可以在不影响性能的情况下执行回收工作。


另外,同样由于每个Isolate都在自己都独立线程内运行,因此每个Isolate的垃圾回收事件不会影响到其他Isolate的性能。


综上可知,Flutter框架所采用的工作流程,很大程度上依赖于其下层的内存分配器和垃圾回收器对于小型的、短生命周期的对象高效的内存分配和回收,缺少这个机制的语言是无法有效运作的


学习成本低


对于想要转岗Flutter的Android或iOS开发者,Dart语言是很友好的,其语法与Kotlin、Swift等语言都存在一些相似之处。


例如,它们都是面向对象的语言,都支持类、接口、继承、抽象类等概念。绝大多数开发者都拥有面向对象开发的经验,因此可以以极低的学习成本学习Dart语言。


此外,Dart语言也拥有着许多与其他语言相似的优秀的语法特性,可以提高开发人员的生产力,例如:




  • 字符串插值:可以直接在字符串中嵌入变量或表达式,而不需要使用+号相连:
    var name = 'Bob'; print('Hello, $name!');




  • 初始化形式参数:可以在构造函数中直接初始化类的属性,而不需要在函数体中赋值:
    class Point { num x, y; Point(this.x, this.y); }




  • 函数式编程风格:可以利用高阶函数、匿名函数、箭头函数等特性简化代码的结构和逻辑:
    var numbers = [1, 2, 3]; var doubled = numbers.map((n) => n * 2);




Dart团队配合度高


拥有一定工作年限的Android开发者,对于早些年Oracle与Google两家科技公司的Java API版权之争可能还有些许印象。


简单讲就是,Oracle认为Google在Android系统中对Java API的复制使用侵犯了其版权和专利权,这场持续了11年的专利纠纷最终以Google的胜利结束。


相比之下,Dart语言与Flutter之间则没有那么多狗血撕逼的剧情,相反,Flutter与Dart社区展开了密切合作,Dart社区积极投入资源改进Dart语言,以便在Flutter中更易使用。


例如,Flutter在最开始采用Dart语言时,还没有用于生成原生二进制文件的AOT工具链,但在Dart团队为Flutter构建了这些工具后,这个缺失已经不复存在了。


结语


以上,就是我汇总Flutter官网资料及Flutter社区推荐博文的说法之后,总结出的Flutter选用Dart作为开发语言的几大主要原因,希望对于刚入门或想要初步了解Flutter开发的小伙伴们有所帮助。


引用



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

一位普通前端程序媛转行创业阶段小结

背景介绍 我女朋友的身份从一个「前端开发」转变成「创业者」,所以本篇文章重点突出创业后她的一些思考。 采访者:Gopal。受访者:QPQ。 以下标题均为问题,内容均为回答。 采访开始分割线 创业方向 抖音电商(抖店)。 具体的工作就是:前期确定类目,选货,...
继续阅读 »

背景介绍


我女朋友的身份从一个「前端开发」转变成「创业者」,所以本篇文章重点突出创业后她的一些思考。


采访者:Gopal。受访者:QPQ。


以下标题均为问题,内容均为回答。




采访开始分割线




创业方向


抖音电商(抖店)。


具体的工作就是:前期确定类目,选货,进货(商品+打包袋),做商品链接,上链接卖货,谈物流,打包发货,售后,客服,日常运营等。


创业跟「打工」最大的区别是?


最大的区别就是创业需要自己缴社保,打工公司给缴哈哈哈。


然后目前由于是一个人在创业,所以一切的事物都需要自己做包括选货,进货(商品+打包袋),做商品链接,上链接卖货,打包发货,售后,客服,日常运营等, 也就是自己是自己的老板也是自己的员工。


之前工作呢是前端开发,分工明确做前端开发就好。现在是整条链路都需要自己经手,也是一种不一样的体验。


不过这个得话看个人,我自己是乐在其中,虽然所有纷繁杂乱的事情都得自己做,但很有成就感。另外就是身份上的转变,从之前的技术开发关注功能与交互转变为了现在直接接触用户,跟用户产生利益关系的这么一个身份,一种不太一样的感觉。


创业后的生活节奏?


生活节奏呢有时候忙起来真的可以做到废寝忘食,自从做抖店以来直接从50多公斤降到最低时47,减肥很轻松哈哈哈哈。


不过充实的一天忙完之后是最开心轻松的时刻,一般这个时间段我会用来逛公园,吃自己喜欢的。


但是也时长有流量冷淡,生意冷淡的时候,会比较忧桑和焦虑,尤其是突然从很高的流量和单量降到很低的流量和单量的时候心情会很忧桑,也会很焦虑。


另外处理售后也是门考验人的技术活,有时候碰到一些难搞的售后,难受一整天都不在话下,此处心疼所有的客服小姐姐一秒钟。不过通过售后也能发现一些问题,从开店到现在通过不断调整优化目前已经减少了很大一部分售后问题可喜可贺。


创业有遇到什么困难么?


人货场是硬伤。



  1. 人手问题。有时候生意特别好但是人货场跟不上真的很可惜,浪费了流量。这个真是硬伤目前无法解决,只能后面如果越做越好了有能力雇个助理看看。

  2. 场地和囤货问题。



  • 第一是场地受限放货和找货都很艰难,这是硬伤,只能后面有能力了整个仓库试下。

  • 第二有时候有一些产品卖爆了会再搞一大批,回来后发现并没有那么畅销了,就会存在囤货压力。爆款补货真的不能搞多,饥饿营销也是不错的策略,多了就不香了。

  • 第三目前所在地区域距离货品工厂挺远的,拿货也是一个硬伤,相比在货品工厂附近的店家就很劣势。



  1. 经常性会遇到瓶颈期,如何破局有点难搞。目前也正在经历,也做了很多尝试但暂时所有的尝试都还没有突破瓶颈,还需要不断的学习同行和不断的尝试不同的方式来处理这个问题。


未来计划


期望明年的这个时候能有仓库和员工哈哈哈哈,美丽的愿望。




采访结束分割线




最后,我自己总结一下。


首先,我们是深思熟虑之后才做出来的决定,并不是冲动。



  • 我女朋友一直有创业的想法。

  • 我们还年轻,现阶段负担还没那么大。设想,假如到 30+ 岁,那时候,做出一个选择,可能需要考虑的事情就更多了。也就是试错的代价可能没那么大。

  • 我们两个人,起码保证一人还是有稳定收入的前提下。

  • ...


另外,创业真的很艰难,作为一个「陪跑者」,还是有所体会。


最后一点,我觉得只要我们做的事情不违法,不违反道德,不对他人造成伤害,那你想做就去做吧

作者:Gopal
来源:juejin.cn/post/7123914177168703518
收起阅读 »

程序员如何给变量起名字

程序员如何给变量起名字 在编写代码时,为变量命名是非常重要的。良好的命名习惯可以提高代码的可读性和可维护性,使得其他开发者能够更容易地理解你的代码。在这篇文章中,我们将讨论程序员如何为变量选择合适的名称。 规范 首先,需要了解所用编程语言和项目的命名规范。不同...
继续阅读 »

程序员如何给变量起名字


在编写代码时,为变量命名是非常重要的。良好的命名习惯可以提高代码的可读性和可维护性,使得其他开发者能够更容易地理解你的代码。在这篇文章中,我们将讨论程序员如何为变量选择合适的名称。


规范


首先,需要了解所用编程语言和项目的命名规范。不同的编程语言和团队可能有不同的命名约定。例如,Python 中通常使用下划线分隔单词(snake_case),而 Java 和 JavaScript 则倾向于驼峰式大小写(camelCase)。遵循一致的命名规则会使得整个代码库更具统一性,降低学习成本。


见名知意


一个好的变量名应该尽可能描述它代表的实际含义。换句话说,当其他开发者看到变量名时,他们应该能够猜测出它表示什么以及如何使用。


好例子



  • user_name 代表用户名;

  • password_hash 表示经过哈希处理的密码;

  • email_list 是一个邮件列表。


不好的例子



  • x, y, z 这样的简单字母命名无法反映变量的实际含义(除非在特定场景下,如表示坐标或数学公式中);

  • tempdata 等过于泛化,无法直接理解其用途;

  • string1array2 只提供了数据类型信息,但未说明其用途。


避免冗长


虽然应该让变量名具有描述性,但同时需要避免使用冗长的名称。太长的名称可能会导致代码难以阅读和维护。通常情况下,选择简洁明确的单词组合更为可取。


好例子



  • index

  • user_count


不好的例子



  • the_index_of_the_current_element_in_the_list

  • the_total_number_of_users_in_the_database


使用专业术语


如果你正在编写涉及某个领域知识的代码,可以使用该领域的专业术语作为变量名。这将使得对该领域能较好理解的开发者更容易理解你的代码意图。


好例子



  • 在计算几何领域,变量名 centroid 表示多边形的质心;

  • 在密码学领域,变量名 salt 代表加密时混入的额外值。


处理复数


当变量包含一系列对象时,最好使用复数名称。这样可以让读者知道它是一个集合类型(如列表、数组、集等),而不仅仅包含一个对象。


好例子



  • users

  • files


避免重名和相似命名


为了提高代码的可读性,应尽量避免在同一作用域内使用相似或容易混淆的变量名。


不好的例子



  • user_listusers_list

  • convert_to_stringtransform_to_string


结论


良好的命名习惯对于编写高质量的代码至关重要。请确保你所选择的变量名既简洁明了,又具有描述性,并且遵循项目规范。这将使得其他开发者能够更容易地理解和维护你的代码。


作者:牙叔教程
来源:juejin.cn/post/7225490584058724410
>公众号 牙叔教程

收起阅读 »

移动端旅行网站页面

web
一、布局 1、首页 (1)头部 iconfont的使用和代码优化 iconfont.css中修改路径 引入iconfont.css import text-align: center(文字水平居中) 优化: 变量复用:src/assets/styles/...
继续阅读 »

一、布局


1、首页


(1)头部


iconfont的使用和代码优化



  • iconfont.css中修改路径

  • 引入iconfont.css import


text-align: center(文字水平居中)


优化:


  1. 变量复用:src/assets/styles/varibles.styl中定义变量 $变量名=值。在style中引入样式,@import(样式中引入样式需加@符号)

  2. 路径名过长:在css中引入其他的css想使用@符号,必须在@符号前加上~(@表示src目录)

  3. 路径别名:


build/webpack.base.conf.js->resolve->alias->创建别名


resolve: {
extensions: ['.js', '.vue', '.json'],
alias: {
'vue$': 'vue/dist/vue.esm.js',
'@': resolve('src'),
//创建别名
'styles': resolve('src/assets/styles'),
'common':resolve('src/common')
}
}

报错原因:修改webpack中的内容后需要重启服务器


(2)轮播图


Swiper插件原理


Swiper是一款基于JavaScript的开源滑动插件,可以用于制作各种类型的轮摇图、滑动菜单、图片预览等。Swiper 的原理主要是通过监听用户的手势操作来实现滑动效果,同时利用CSS3动画和过渡效果来实现平滑的过渡和动画效果。



  1. 监听手势操作


Swiper通过监听用户的手势操作来实现滑动效果,具体包括touchstart、touchmove.touchend符事件。在touchstart 事件中,Swiper记录下用户的触摸起始位置及时间,touchmove事件中,Swiper根据用户移动的距离和时间计算出滑动速度和方向,从而控制滑动的行为;touchend事件中,Swiper根据滑动的距离和速度来判断是否进行下一张图片的切换。



  1. 切换图片


Swiper通过获取当前显示的图片索引及方向来计算出下一张图片的索引,并通过CSS3过渡效果来实现平滑的图片切换。同时,Swiper可以支持多种不同的切换效果,包括淡入淡出、渐变、滑动、翻转等,



  1. 响应式设计


Swiper支持响应式设计,可以根据不同的设备尺寸和屏幕方向来自动调整轮播图的大小和样式,从而提供更好的用户体验。同时,Swiper还支持自定义参数配置,可以灵活地控制轮播图的各种属性和行为。


问题


网速慢时图片没有加载出来造成的抖动(使用padding-bottom占位):


overflow: hidden
height:0
padding-bottom: 31.25% //高宽比

显示分页:


<!-- template -->
<swiper :options="swiperOption" v-if="showSwiper">
<!-- slides -->
<!-- 循环实现轮播图 -->
<swiper-slide v-for="item of list" :key="item.id">
<img class="swiper-img" :src="item.imgUrl" alt="">
</swiper-slide>
<!-- 插槽 -->
<div class="swiper-pagination" slot="pagination"></div>
</swiper>

// script
export default {
name: 'HomeSwiper',
props: {
list:Array
},
data() {
return {
swiperOption: {
// 设置显示轮播图分页
pagination: ".swiper-pagination",
//设置轮播图循环展示
loop: true,
// 设置自动轮播及间隔时间
autoplay: 3000
},
}
},
computed: {
// 列表为空时不显示轮播图
showSwiper() {
return this.list.length;
}
}
}

样式穿透



  • >>>

  • /deep/

  • ::v-deep



  1. 引入第三方组件库(如element-ui、element-plus),修改第三方组件库的样式。

  2. 样式文件中使用了 scoped 属性,但是为了确保每个组件之间不存在相互影响所以不能去除


/* style */
.wrapper >>> .swiper-pagination-bullet
background-color: #fff

插槽slot


通过slot插槽将页面中具体的数据传递给swiper组件,希望组件的内容可以被父组件定制的时候,使用slot向组件传递内容。


(3)图标区域


图标的轮播实现


使用computed计算功能实现图标的多页显示(容量为8,多出来的的图标换页显示):


computed: {
pages () {
// 创建二维数组pages
const pages = [];
// 对图标列表使用forEach循环
this.list.forEach((item, index) => {
// 计算页码
const page = Math.floor(index/8);
if (!pages[page]){
pages[page] = []
}
pages[page].push(item)
});
// pages[0]中存储在第一页显示的图标,以此类推
return pages
},
showIcon() {
return this.list.length
}
}

优化:


(1)希望文字过多时有…提示:


css中添加:


overflow: hidden
white-space:nowrap
text-overflow:ellipsis

(2)重复代码封装:


借助stylus提供的mixin对代码进行封装:


src/assets/varibles.styl中定义ellipse方法,在css中@import 文件,直接使用ellipse()


mixin(混入)

它提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。


使用场景: 不同组件中经常会用到一些相同或相似的代码,这些代码的功能相对独立。可以通过mixin 将相同或相似的代码提出来。


缺点:



  1. 变量来源不明确

  2. 多 mixin 可能会造成命名冲突(解决方式:Vue 3的组合API)

  3. mixin 和组件出现多对多的关系,使项目复杂度变高。


(4)推荐


text-indent:文字缩进


使用ellipsis()不起作用:父元素添加min-width: 0


(5)周末游


(6)Ajax获取首页数据


安装axios,引入axios


import axios from 'axios'
methods: {
getHomeInfo() {
// 使用axios.get()请求一个URL,返回的对象是一个promise对象,使用.then()获取
axios.get('/api/index.json?city='+this.city)
.then(this.getHomeInfoSucc);
},

getHomeInfoSucc(res) {
res = res.data;
if (res.ret && res.data) {
const data = res.data
// this.city = data.city
this.swiperList = data.swiperList
this.iconList = data.iconList
this.recommendList = data.recommendList
this.weekendList = data.weekendList
}
}
}

转发机制(开发环境的转发)



  • 只有static文件夹下的内容才可以被外部访问到

  • 现在用的是本地模拟的接口地址,假如代码要上线,肯定不能填成这样的一个地址,那么就需要在上线之前把这块儿的东西都重新替换成API这种格式,上线之前去改动代码儿是有风险的

  • 转发机制:webpack-dev-server提供 proxyTable 配置项,config/index.js 中proxyTable


module.exports = {
dev: {
// Paths
assetsSubDirectory: 'static',
assetsPublicPath: '/',
// 请求api目录时将请求转发到当前服务器的8080端口上,但是路径替换为/static/mock
proxyTable: {
'/api':{
target: 'http://localhost:8080',
pathRewrite: {
'^/api': '/static/mock'
}
}
}
}


  • 整个首页发送一个ajax请求而不是每个组件发送一个


(7)首页父子组件数据传递


父->子:属性传值,子组件props:{}接收


轮播图默认显示最后一张图


原因:


还没有接收ajax数据时,swiper接收的数据是一个空数组,当接受ajax数据后, swiperList变成真正的数据项。再传给home-swiper这个组件的时候,它才获取到新的数据,然后重新渲染了新数据对应的很多的幻灯片。因为swiper的初始化创建是根据空数组创建的,所以会导致默认显示的是所有的这个页面中的最后一个页面。


解决:


让swiper的初次创建由完整的数据来创建,而不是由那个空数组来创建。只需要写一个v-if,再写一个list.length。当传递过来的list是个空数组的时候,v-if的值是false,所以swiper不会被创建。只有等真正的数据过来了之后才会被创建。由于模板中尽量少出现逻辑性代码,所以创建一个计算属性computed,计算 list.length。


取消轮播图自动播放:autoplay: false


2、城市选择页


(1)路由配置


路由跳转:


import Vue from 'vue'
import Router from 'vue-router'
import Home from '../pages/home/Home'
import City from '../pages/city/City'
import Detail from '../pages/detail/Detail'

Vue.use(Router)

export default new Router({
routes: [{
path: '/',
name: 'Home',
component: Home
},
{
path: '/city',
name: 'City',
component: City
},
{
// 动态路由
path: '/detail/:id',
name: 'Detail',
component: Detail
}
],
scrollBehavior (to, from, savedPosition) {
return { x: 0, y: 0 }
}
})

使用:


<router-link to="/city">
<div class="header-right">
{{this.city}}
<span class="iconfont arrow-icon">&#xe600;</span>
</div>

</router-link>

(2)搜索框


定义一个keyword数据,与搜索框使用v-model做双向数据绑定。


<div class="search">
<input v-model="keyword" class="search-input" type="text" placeholder="输入城市名或拼音" />
</div>

City.vue父组件向Search.vue组件传值(cities),Search.vue接收cities


使用watch监听keyword的改变(使用节流)


解决匹配城市过多无法滚动问题


import Bscroll,在mounted中创建一个BScroll, this.scroll = new BScroll(this.$refs.search),通过$ref获取需要滚动的元素。


<!--搜索结果显示框-->
<div class="search-content" ref="search" v-show="keyword">
<ul>
<!--解决删除输入列表依然存在的问题-->
<li class="search-item border-bottom" v-for="item in list" :key="item.id" @click="handleCityClick(item.name)">{{item.name}}</li>
<li class="search-item border-bottom" v-show="hasNoData">没有找到匹配数据</li>
</ul>
</div>

优化


1.解决删除输入列表依然存在的问题:v-show = "keyword"

2.没有找到匹配项时,显示“没有找到匹配数据”:v-show = "!this.list.length"

双向数据绑定原理(搜索时使用)




  • 概念:

    Vue 中双向绑定是一个指令v-model,可以绑定一个响应式数据到视图,同时视图的变化能改变该值。v-model是语法糖,默认情况下相当于:value@inputv-bind:valuev-on:input),使用v-model可以减少大量繁琐的事件处理代码,提高开发效率。




  • 使用:

    通常在表单项上使用v-model,还可以在自定义组件上使用,表示某个值的输入和输出控制。




  • 原理:

    v-model是一个指令,双向绑定实际上是Vue 的编译器完成的,通过输出包含v-model模版的组件渲染函数,实际上还是value属性的绑定及input事件监听,事件回调函数中会做相应变量的更新操作。




(3)列表


引入区块滚动


子元素使用float,父元素开启BFC(overflow:hidden)去除高度塌陷。


使列表区域无法滚动(position:absolute + overflow:hidden),然后使用better-scroll插件


better-scroll



  • 安装better-scroll包。

  • 需要 better-scroll 包裹的所有元素最外层需要使用一个div包裹,并设置一个ref属性方便创建scroll。

  • 创建scroll


<div class="list" ref="wrapper">list中的元素<div>

import Bscroll from 'better-scroll'
// 写在mounted钩子函数中,此时页面挂载完成,可以操作DOM元素。
mounted() {
this.scroll = new Bscroll(this.$refs.wrapper)
}

$ref


ref属性:获取DOM。


在vue中ref可以以属性的形式添加给标签或者组件:



  • ref 写在标签上时:this.$refs.ipt 获取的是添加了ref="ipt"标签对应的dom元素;

  • ref 写在组件上时:this.$refs['component'] 获取到的是添加了ref="component"属性的这个组件。


$refs 是所有注册过 ref 的集合(对象);若是遍历的ref,则对应$refs是个数组集合


注意:$refs不是响应式的,只在组件渲染完成之后才填充。所以想要获取DOM数据的更新要使用 this.$nextTick()


字母表和城市列表字母的联动


兄弟组件传值:


Alphabet组件将值传递给父组件City.vue,父组件将值传递给子组件List.vue实现字母表和城市列表字母的联动。


为每个字母绑定一个onclick事件,在方法中使用this.$emit向外传递change事件。


<!--Alphabet.vue-->
<template>
<ul class="list" >
<li class="item" v-for="key in letters"
:key="key"
:ref="key"
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@touchend="handleTouchEnd"
@click="handleLetterClick"
>

{{key}}
</li>
</ul>
</template>


// Alphabet.vue
methods: {
handleLetterClick (e) {
// 向外(City.vue)传递事件
this.$emit("change", e.target.innerText);
}
}

父组件


<!--City.vue-->
<city-alphabet :cities="cities" @change="handleLetterChange"></city-alphabet>

// City.vue
data () {
return {
letter: '' // 被点击的字母
}
},
methods: {
handleLetterChange (letter) {
this.letter = letter;
// console.log(letter);
}
}

<!-- 向List组件传值letter -->
<city-list :cities="cities" :hot="hotCities" :letter="letter"></city-list>

// List.vue接收letter
props: {
hot: Array,
cities: Object,
letter: String
},

使用watch监听letter变化,当letter发生变化时,调用this.scroll.scrollToElement()方法将区域自动滚动到指定区域,在template中给每一个area区域加一个:ref='key',通过this.$refs[this.letter][0]获取值为this.letter的DOM元素。


<div class="area" v-for="(item, key) in cities" :key="key" :ref="key" >
<div class="title border-topbottom">{{ key }}</div>
<div class="item-list">
<div class="item border-bottom" v-for="innerItem in item" :key="innerItem.id" @click="handleCityClick(innerItem.name)">{{innerItem.name}}</div>
</div>
</div>

watch: {
letter () {
if (this.letter) {
// 获取值为`this.letter`的DOM元素
const element = this.$refs[this.letter][0]
// 将区域自动滚动到指定区域
this.scroll.scrollToElement(element)
}
}
}

上下拖拽字母表touch

为字母表绑定三个事件:


@touchstart="handleTouchStart"  //手指开始触摸时设置this.touchStatus = true
@touchmove="handleTouchMove" //在true时对触摸事件做处理
@touchend="handleTouchEnd" //手指结束触摸时设置this.touchStatus = false

// 构建字母数组
computed: {
// 构建字母数组["A", "B", ……, "Z"]
letters () {
const letters = [];
for (let i in this.cities){
letters.push(i);
}
return letters;
}
},
methods: {
handleLetterClick (e) {
// 向外(City.vue)传递事件
this.$emit("change", e.target.innerText);

},
handleTouchStart () {
this.torchStatus = true;
},
handleTouchMove (e) {
if (this.torchStatus) {
// 如果timer已经存在,去除timmer(即上一次的事件还未执行完毕又出发了下一次事件,就用下一次事件覆盖上一次的事件)
if (this.timer){
clearTimeout(this.timer)
}
// 否则就创建一个timer
//节流,将操作延迟16ms执行,如果上一个操作间隔小于16ms,则清除上一个操作,直接执行这次操作,减少handleTouchMove的使用频率
this.timer = setTimeout(() => {
const touchY = e.touches[0].clientY - 79; // 当前手指触摸位置与头部下沿的距离,79px为头部的高度
const index = Math.floor((touchY - this.startY) / 20); //当前手指触摸位置的元素
// 合法时向父元素emit change事件
if (index >= 0 && index < this.letters.length){
this.$emit('change', this.letters[index]);
}
}, 16)

}
},
handleTouchEnd () {
this.torchStatus = false
}
}

优化

1、将字母A到头部下沿的距离的计算放在updated生命周期钩子函数中


初始时cities值为0,当Ajax获取数据后,cities的值才发生变化,AlphaBet才被渲染出来, 当往alphabet里面传的数据发生变化的时候,alphabet这个组件儿就会重新渲染。之后,updated这个生命周期钩子就会被执行。这个时候,页面上已经展示出了城市字母列表里所有的内容,去获取A这个字母所在的dom对应的offsettop的值就没有任何的问题了。


2、节流


如果timer已经存在,去除timmer(即上一次的事件还未执行完毕又出发了下一次事件,就用下一次事件覆盖上一次的事件)。


否则就创建一个timer,将操作延迟16ms执行,如果上一个操作间隔小于16ms,则清除上一个操作,直接执行这次操作,减少handleTouchMove的使用频率。


3、使用Vuex实现首页和城市选择页面的数据共享



  1. 安装vuex

  2. 创建src/store/index.js文件


import Vue from 'vue'
import Vuex from 'vuex'
import state from './state'
import mutations from './mutations'

Vue.use(Vuex)

export default new Vuex.Store({
state,
// actions: {
// changeCity (ctx, city) {
// ctx.commit('changeCity', city)
// }
// },
mutations
})


  1. main.js中引入store


import store from './store'


  1. 在mainjs中创建根实例时将store传入,store会被派发到每个子组件中,每个子组件中都可以使用this.$store获取到 store。


new Vue({
el: '#app',
router,
store,
components: { App },
template: '<App/>'
})

(1)Vuex


概念


Vuex 是 Vue 专用的状态管理库,它以全局方式集中管理应用的状态,并以相应的规则保证状态以一种可预测的方式发生变化。主要解决的问题是多组件之间状态共享。


image.png


核心概念



  • State:存放核心数据

  • Action:异步方法

  • Mutation:同步的对数据的改变

  • Getter:类似于计算属性,提供新的数据,避免数据的冗余

  • Module:模块化,拆分


image.png


项目中Vuex的使用



  • state存放当前城市CurCity

  • 为每个城市元素都绑定一个onclick事件(获取改变的city),点击城市

  • 在List组件中调用dispatch方法->触发Action

  • 在store/index.js中增加一个actions对象(接收city),使用commit调用mutation

  • mutations中,令state.city = city,完成


// List.vue
this.$store.dispatch('changeCity', city)

// store/index.js
actions: {
changeCity (ctx, city) {
// 使用Commit调用Mutation
ctx.commit('changeCity', city)
}

},
mutations: {
changeCity (state, city) {
state.city = city;
try {
localStorage.city = city
} catch (e) {}
}
}

image.png


(2)单页面与多页面


实现单页面跳转的方式



  1. 标签实现



  • a标签

  • router-link标签


<a href="#/xxx" />
<router-link to="/xxx" />


  1. 函数实现



  • 传统的window.location.href

  • 使用vue-router中的router对象 (点击城市后自动跳转到首页)
    step1 定义一个实现onclik事件的组件


<a onClick={this.goTo} />

step2 在goTo函数中实现跳转


goTo = () => {
// 方案1 传统的window.location.href
window.location.href = "#/xxx"

// 方案2 使用vue-router中的router对象
this.$router.push('/');
}

项目中,点击城市后跳转到首页:this.$router.push


methods: {
handleCityClick (city) {
this.$router.push('/');
}
},

(3)localStorage(state和mutations中使用)


//state.js
let defaultCity = "上海"
try {
if (localStorage.city){
defaultCity = localStorage.city
}
}catch (e) {}

export default{
city: defaultCity
}

// mutation.js
export default{
changeCity (state, city) {
state.city = city;
try {
localStorage.city = city
} catch (e) {}

}
}

(4)keep-alive优化网页性能


每次切换回组件时,组件就会被重新渲染,mounted生命周期钩子就会被重新执行,ajax数据就会被重新获取,导致性能低。


<!-- App.vue -->
<keep-alive>
<router-view/>
</keep-alive>

使用keep-alive后会多出两个生命周期钩子函数activated和deactivated
activated在每次切换回组件时调用,因此可以在activated中定义方法,在城市切换时重新发送ajax请求。


4、景点详情页


(1)动态路由及banner分支


为recommend组件的li标签外部包裹一个router-link标签,li标签样式改变,解决方式:


将li标签直接替换为router-link标签, 加入一个tag=“li”的属性,动态绑定::to = "'/detail/' + item.id",在router/index.js设置Detail路由


<!-- recommend.vue -->
<router-link tag="li"
class="item border-bottom"
v-for="item in list"
:key="item.id"
:to="'/detail/' + item.id">

</router-link>

// router/index.js
{
path: '/detail/:id',
name: 'Detail',
component: Detail
}

图片底部渐变效果:


background-image: linear-gradient(top, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.8))

(2)公用图片画廊组件拆分


将画廊组件变成一个公用组件src/common/gallary/Gallary.vue


画廊组件:图片轮播+页码显示


使用swiper插件实现轮播功能,使用swiper插件的 paginationType: 'fraction',将分页器样式设置为页码。

‘bullets’  圆点(默认)
‘fraction’  分式 
‘progress’  进度条
custom’ 自定义

创建路径别名,重启服务器。


一开始将gallary显示为隐藏状态,再次显示时计算宽度出现问题,swiper无法正确显示,解决:


data () {
return {
showGallary: true,
swiperOptions: {
pagination: '.swiper-pagination',

// 将分页器样式设置为页码
paginationType: 'fraction',

// swiper监听到此元素或父级元素发生DOM变化时,就会自动刷新一次,解决宽度计算问题。
observeParents:true,
observer:true,

loop:true,
autoplay: false
}
}
}

(3)实现header渐隐渐现效果


methods: {
handleScroll () {
// 获取当前页面的滚动条纵坐标位置
const top = document.documentElement.scrollTop;
// top > 60 时开始逐渐显示header,top > 40 时一直显示header
if (top > 60){
const opacity = top / 140;
opacity > 1 ? 1 : opacity;
this.opacityStyle = {
opacity
};
this.showAbs = false;
}
else{
this.showAbs = true
}
}
},
activated () {
window.addEventListener('scroll', this.handleScroll)
},


(4)对全局事件的解绑(window对象)


// 页面即将被隐藏时执行
deactivated () {
window.removeEventListener('scroll', this.handleScroll)
}

(5)使用递归组件实现详情页列表


(6)使用Ajax获取动态数据


获得动态路由参数(id)并将其传递给后端、


getDetailInfo () {
axios.get('/api/detail.json',{
params: {
id: this.lastRouteId
}
}).then(this.handleGetDetailInfoSucc)
}

(7)组件跳转时页面定位


scrollBehavior (to, from, savedPosition) {
return { x: 0, y: 0 }
}

(8)在项目中加入基础动画


在点击画廊组件时增加渐隐渐现的动画效果


//FadeAnimation.vue

<template>
<transition>
<slot></slot>
</transition>
</template>
<script>
export default{
name: "FadeAnimation"
}
</script>
<style lang="stylus" scoped>
@import '~styles/varibles.styl'
.v-enter, .v-leave-to
opacity: 0
.v-enter-active, .v-leave-active
transition: opacity .5s
</style>

common-gallary 作为插槽插入到fade-animation中:


<!--Banner.vue-->

<fade-animation>
<common-gallary
:imgs="bannerImgs"
v-show="showGallary"
@close="handleGallaryClose"
>

</common-gallary>
</fade-animation>

二、优化



  • 网速慢时图片没有加载出来造成的抖动(使用padding-bottom占位)

  • 重复代码封装:借助stylus提供的mixin对代码进行封装

  • 节流:触摸滑动字母表&搜索框中输入

  • keep-alive

  • 对全局事件解绑


作者:树上结了个小橙子
来源:juejin.cn/post/7222627262399365180
收起阅读 »

快看一看,你是不是这样使用的vscode

web
俗话说:“工欲善其事,必先利其器”。想要高效的编写代码,自然要先熟练的使用一款工具。 vscode设置简体中文 使用国外的工具,头等大事自然是必不可少的汉化。 按快捷键 ‘ctrl+shift+p’,在顶部出现的输入框输入‘configure language...
继续阅读 »

俗话说:“工欲善其事,必先利其器”。想要高效的编写代码,自然要先熟练的使用一款工具。


vscode设置简体中文


使用国外的工具,头等大事自然是必不可少的汉化。


按快捷键 ‘ctrl+shift+p’,在顶部出现的输入框输入‘configure language’,按回车,选择‘zh-cn’。此时,会自动安装中文插件,然后重新打开vscode就可以看到中文界面了。

image.png


vscode实用插件


选择vscode的原因,除了它的轻量之外,自然少不了它丰富的插件库。


1. Auto Rename Tag


自动修改匹配的html标签。在修改标签的时候,是不是需要修改完开始标签之后还需要修改结束标签。安装Auto Rename Tag,以后只需要修改一个标签就可以了,四舍五入就等于减少一半工作量啊。


2. Prettier


代码格式化插件,一键格式化代码,也可以设置保存自动格式化。我会将我的配置放在文章末尾。


3. code runner


可以直接js文件,在控制台输出结果。在写一些小算法的时候再也不用频繁刷新页面打印了。


image.png


4. Turbo Console Log


快捷添加 console.log,一键 注释、启用、删除 所有 console.log。调试js时候大概都会用console.log,每次手敲都很麻烦。


ctrl + alt + l 选中变量之后,生成 console.log
alt + shift + c 注释所有 console.log
alt + shift + u 启用所有 console.log
alt + shift + d 删除所有 console.log

注意,只能注释、启用、删除ctrl + alt + l生成的console.log。如果有小伙伴安装了印象笔记,ctrl + alt + l和印象笔记是冲突的。


5. css-auto-prefix


自动添加 CSS 私有前缀。
比如写完transform样式,会自动添加-webkit-、-moz-等样式。


配置


接下来便是无处不在的配置了,将我的配置贴出来,供大家参考。


文件->首选项->设置->工作台->设置编辑器,将editor的ui改为json,将配置直接粘贴进去


{
"eslint.enable": true,
"eslint.run": "onType",
"eslint.options": {
"extensions": [
".js",
".vue",
".jsx",
".tsx"
]
},
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"workbench.iconTheme": "material-icon-theme",
"workbench.colorTheme": "Monokai",
"[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"workbench.settings.editor": "json",
"editor.tabSize": 2,
//失去焦点后自动保存
"files.autoSave": "onFocusChange",
// #值设置为true时,每次保存的时候自动格式化;
"editor.formatOnSave": true,
// 在使用搜索功能时,将这些文件夹/文件排除在外
"search.exclude": {
"**/node_modules": true,
"**/bower_components": true,
"**/target": true,
"**/logs": true,
},
// #让vue中的js按"prettier"格式进行格式化
"vetur.format.defaultFormatter.html": "js-beautify-html",
"vetur.format.defaultFormatter.js": "prettier",
"vetur.format.defaultFormatterOptions": {
"js-beautify-html": {
// #vue组件中html代码格式化样式
"wrap_attributes": "force-aligned", //也可以设置为“auto”,效果会不一样
"wrap_line_length": 200,
"end_with_newline": false,
"semi": false,
"singleQuote": true
},
"prettier": {
"semi": false,
"singleQuote": true
}
},
"[jsonc]": {
"editor.defaultFormatter": "vscode.json-language-features"
}
}

快捷键


熟练的时候快捷键,可以更高效的提升敲代码的效率。我将我常用的快捷键列出,供大家参考。


ctrl + d 选中一个词 鼠标双击也可以
ctrl + f 搜索
ctrl + p 快速打开文件
ctrl + shift + [ 折叠区域代码
ctrl + shift + ] 展开区域代码
ctrl + shift + k 删除一行代码,不过我更喜欢用ctrl+x,因为一只手就可以操作

作者:前端手记
来源:juejin.cn/post/7226248402799345719
收起阅读 »

手写一个类似博客的个人主页 css动画效果多

web
手写一个好看的个人主页 效果图 文字,图片加上各种动画显示,使页面更加美观。然后给头像也加上了一个动画,使头像实现一个一直在上下浮动的效果。媒体组件logo,添加事件hover,置顶会变颜色。按钮添加active激活样式。 最好打开码上掘金看效果图,更明显,...
继续阅读 »

手写一个好看的个人主页


效果图


文字,图片加上各种动画显示,使页面更加美观。然后给头像也加上了一个动画,使头像实现一个一直在上下浮动的效果。媒体组件logo,添加事件hover,置顶会变颜色。按钮添加active激活样式。
image.png
最好打开码上掘金看效果图,更明显,因为显示框问题,这样看布局有点问题。


整体布局


头部


包含一个logo,和一个 导航栏。
logo我这里用了一段文字替代,大家可以自己替换。
header都处于同一行,于是我采用了弹性布局。因为一个左一个右,就可以使用justify-content: space-between;然后logo我给它设置了一个从左往右的动画,时间为1s。导航栏添加了一个从上往下的动画,不过注意的是,我们可以看到每一个导航栏元素是递进往上升的。


实现导航栏元素递进往上升的关键。<a href="#" class="item" style="--i:1">Home</a>我们给每个导航元素都添加了css属性,然后通过这个属性, calc(.2S * var(--i)) 计算每个不同元素的延迟时间,这样我们就可以看到这种延迟效果。
image.png


.header {
position: fixed;
/* 将导航栏固定在页面顶部 */
top: 0;
left: 0;
width: 100%;
padding: 20px 10%;
/* 设置导航栏内边距 */
background: transparent;
/* 设置导航栏背景为透明 */
display: flex;
/* 将导航栏的子元素设置为flex布局 */
justify-content: space-between;
/* 将导航栏子元素分散对齐 */
align-items: center;
/* 将导航栏子元素垂直居中对齐 */
z-index: 100;
/* 将导航栏设置为最上层 */
}

/* 设置导航栏Logo的样式 */
.logo {
font-size: 25px;
/* 设置字体大小 */
color: #fff;
/* 设置字体颜色 */
text-decoration: none;
/* 取消下划线 */
font-weight: 600;
/* 设置字体粗细 */
cursor: default;
/* 设置鼠标样式为默认 */
opacity: 0;
/* 设置初始透明度为0 */
animation: slideRight 1s ease forwards;
/* 设置动画效果 */
}

/* 设置导航栏链接的样式 */
.navbar a {
display: inline-block;
/* 将链接设置为块级元素 */
font-size: 18px;
/* 设置字体大小 */
color: #fff;
/* 设置字体颜色 */
text-decoration: none;
/* 取消下划线 */
font-weight: 500;
/* 设置字体粗细 */
margin-left: 35px;
/* 设置左侧间距 */
opacity: 0;
/* 设置初始透明度为0 */
transition: 0.3s;
/* 设置过渡效果 */
animation: slideTop 1s ease forwards;
/* 设置动画效果 */
animation-delay: calc(.2S * var(--i));
/* 设置动画延迟时间 */
}

/* 设置导航栏链接的鼠标悬停和点击样式 */
.navbar a:hover,
.navbar a:active {
color: #b7b2a9;
/* 设置字体颜色 */
}


<header class="header">
<!-- 网站Logo -->
<a href="#" class="logo">
This is a Logo!
</a>

<!-- 导航栏 -->
<nav class="navbar">
<!-- 导航栏选项1 -->
<a href="#" class="item" style="--i:1">Home</a>
<!-- 导航栏选项2 -->
<a href="#" class="item" style="--i:2">About</a>
<!-- 导航栏选项3 -->
<a href="#" class="item" style="--i:3">Skills</a>
<!-- 导航栏选项4 -->
<a href="#" class="item" style="--i:4">Me</a>
</nav>

</header>

主页部分


主页部分包含文字区域和头像区域。


在文字区域里有一个打印机效果输出文字,可以看我上一篇文章。html手写一个打印机效果-从最基础到学会。然后给每个文字设置不同的动画,比如第一个h1我们让它从上往下,然后第二个h1我们让它从下往上,在他们中间的h1我们让它从左向右出现。在文字区域还有一块是一些media的组件logo,这个我是通过一个js引入的库。然后这些logo跟导航栏元素大致相同,我们也给他们定义了一个css属性,可以让他们相继出现。然后按钮也是添加向上的动画,给定一个延迟时间。


头像区域,我们给头像设置了两个动画,其实动画非常简单,一个其实就是为了显示头像,另一个实现头像上下浮动的效果。


 /* 定义放大的动画 */
@keyframes zoomIn {
0% {
transform: scale(0);
}

100% {
transform: scale(1);
opacity: 1;
}
}

/* 定义图片浮动的动画 */
@keyframes floatImg {
0% {
transform: translateY(0);
}

50% {
transform: translateY(-36px);
}

100% {
transform: translateY(0);
}
}

<link rel="stylesheet" href="https://unpkg.com/boxicons@2.1.4/css/boxicons.min.css">

 <!-- 主页部分 -->

<section class="home">
<!-- 主页内容 -->
<div class="home-content">
<!-- 主页标题 -->
<h3>Hello,It's Me</h3>
<h1>Welcome To Know Me!</h1>
<!-- 主页小标题 -->
<h3>个人介绍
<!-- 小标题下的文本 -->
<span class="text">
</span>
</h3>
<!-- 主页正文 -->
<p>越努力,越幸运!!!Lucky!</p>
<!-- 社交媒体链接 -->
<div class="social-media">
<!-- 社交媒体链接1 -->
<a href="#" style="--i:7"><i class="bx bxl-tiktok"></i></a>
<!-- 社交媒体链接2 -->
<a href="#" style="--i:8"><i class="bx bxl-facebook-circle"></i></a>
<!-- 社交媒体链接3 -->
<a href="#" style="--i:9"><i class="bx bxl-google"></i></a>
<!-- 社交媒体链接4 -->
<a href="#" style="--i:10"><i class="bx bxl-linkedin-square"></i></a>
</div>
<!-- 主页按钮 -->
<a href="#" class="btn">Learn More</a>
</div>
<!-- 主页图片 -->
<div class="home-img">
<img src="https://img.wxcha.com/m00/54/ed/69d26be4a4ac700e27c2d9cf85472b8c.jpg" alt="">
</div>
</section>

整体代码


动画的代码


  /*动画*/
@keyframes blink {

from,
to {
border-color: transparent;
/* 透明边框颜色 */
}

50% {
border-color: white;
/* 白色边框颜色 */
}
}

/* 定义向右滑动的动画 */
@keyframes slideRight {
0% {
transform: translateX(-100px);
}

100% {
transform: translateX(0px);
opacity: 1;
}
}

/* 定义向左滑动的动画 */
@keyframes slideLeft {
0% {
transform: translateX(100px);
}

100% {
transform: translateX(0px);
opacity: 1;
}
}

/* 定义向上滑动的动画 */
@keyframes slideTop {
0% {
transform: translateY(100px);
}

100% {
transform: translateY(0px);
opacity: 1;
}
}

/* 定义向下滑动的动画 */
@keyframes slideBottom {
0% {
transform: translateY(-100px);
}

100% {
transform: translateY(0px);
opacity: 1;
}
}

/* 定义放大的动画 */
@keyframes zoomIn {
0% {
transform: scale(0);
}

100% {
transform: scale(1);
opacity: 1;
}
}

/* 定义图片浮动的动画 */
@keyframes floatImg {
0% {
transform: translateY(0);
}

50% {
transform: translateY(-36px);
}

100% {
transform: translateY(0);
}
}

源码


链接自取
掘金/个人页面 · Mr-W-Y-P/Html-css-js-demo - 码云 - 开源中国 (gitee.com)


作者:Mr-Wang-Y-P
来源:juejin.cn/post/7225444782331592764
收起阅读 »

Android - 统一依赖管理(config.gradle)

介绍 Android 依赖统一管理距目前为止,博主一共知道有三种方法,分别是: 传统apply from的方式(也是本文想讲的一种方式):新建一个 「config.gradle」 文件,然后将项目中所有依赖写在里面,更新只需修改 「config.gradl...
继续阅读 »
646286.webp

介绍


Android 依赖统一管理距目前为止,博主一共知道有三种方法,分别是:




  1. 传统apply from的方式(也是本文想讲的一种方式):新建一个 「config.gradle」 文件,然后将项目中所有依赖写在里面,更新只需修改 「config.gradle」 文件内容,作用于所有module。

  2. buildSrc 方式:当运行 Gradle 时会检查项目中是否存在一个名为 buildSrc 的目录。然后 Gradle 会自动编译并测试这段代码,并将其放入构建脚本的类路径中, 对于多项目构建,只能有一个 buildSrc 目录,该目录必须位于根项目目录中, buildSrc 是 Gradle 项目根目录下的一个目录。

  3. Composing builds 方式:复合构建只是包含其他构建的构建. 在许多方面,复合构建类似于 Gradle 多项目构建,不同之处在于,它包括完整的 builds ,而不是包含单个 projects,总的来说,他有 buildSrc 方式的优点,同时更新不需要重新构建整个项目。



三种方式各有各的好,目前最完美的应该是第三种实现。但是这种方式不利于框架使用,因为它属于的是新建一个module,如果项目远程依赖了框架,默认也包含了这个 module。所以博主选择了第一种方式。以下文章也是围绕第一种方式进行讲解。


实现方式


实现这个统一依赖管理,拢共分三步,分别是:




  • 第一步:创建「config.gradle」 文件

  • 第二步:项目当中引入「config.gradle」

  • 第三步:在所有module的「build.gradle」当中添加依赖





  • 第一步:创建 「config.gradle」 文件


    首先将 Aandroid Studio 目录的Android格式修改为Project,然后再创建一个「config.gradle」的文件


    1681962514751.jpg


    然后我们编辑文章里面的内容,这里直接给出框架的代码出来(篇幅太长,省略部分代码):


    ext {
    /**
    * 基础配置 对应 build.gradle 当中 android 括号里面的值
    */

    android = [
    compileSdk : 32,
    minSdk : 21,
    targetSdk : 32,
    versionCode : 1,
    versionName : "1.0.0",
    testInstrumentationRunner: "androidx.test.runner.AndroidJUnitRunner",
    consumerProguardFiles : "consumer-rules.pro"

    ......
    ]

    /**
    * 版本号 包含每一个依赖的版本号,仅仅作用于下面的 dependencies
    */

    version = [
    coreKtx : "1.7.0",
    appcompat : "1.6.1",
    material : "1.8.0",
    constraintLayout : "2.1.3",
    navigationFragmentKtx: "2.3.5",
    navigationUiKtx : "2.3.5",
    junit : "4.13.2",
    testJunit : "1.1.5",
    espresso : "3.4.0",

    ......
    ]

    /**
    * 项目依赖 可根据项目增加删除,但是可不删除本文件里的,在 build.gradle 不写依赖即可
    * 因为MVP框架默认依赖的也在次文件中,建议只添加,不要删除
    */

    dependencies = [

    coreKtx : "androidx.core:core-ktx:$version.coreKtx",
    appcompat : "androidx.appcompat:appcompat:$version.appcompat",
    material : "com.google.android.material:material:$version.material",
    constraintLayout : "androidx.constraintlayout:constraintlayout:$version.constraintLayout",
    navigationFragmentKtx : "androidx.navigation:navigation-fragment-ktx:$version.navigationFragmentKtx",
    navigationUiKtx : "androidx.navigation:navigation-ui-ktx:$version.navigationUiKtx",
    junit : "junit:junit:$version.junit",
    testJunit : "androidx.test.ext:junit:$version.testJunit",
    espresso : "androidx.test.espresso:espresso-core:$version.espresso",

    ......
    ]
    }

    简单理解就是将所有的依赖,分成版本号以及依赖名两个数组的方式保存,所有都在这个文件统一管管理。用 ext 包裹三个数组:第一个是「build.gradle」Android 里面的,第二个是版本号,第三个是依赖的名字。依赖名字数组里面的依赖版本号通过 $ 关键字指代 version 数组里面的版本号




  • 第二步:项目当中引入 「config.gradle」


    将「config.gradle」文件引入项目当中,在项目的根目录的「build.gradle」文件(也就是刚刚新建的 「config.gradle」同目录下的),添加如下代码:


    apply from:"config.gradle"

    需要注意的的是,如果你是 AndroidStudio 4.0+ 那么你将看到这样的「build.gradle」文件


    // Top-level build file where you can add configuration options common to all sub-projects/modules.
    plugins {
    id 'com.android.application' version '7.2.2' apply false
    id 'com.android.library' version '7.2.2' apply false
    id 'org.jetbrains.kotlin.android' version '1.7.10' apply false
    }

    apply from:"config.gradle"

    相反,如果你是 AndroidStudio 4.0- 那么你将会看到这样的「build.gradle」文件



    apply from: "config.gradle"

    buildscript {
    ext.kotlin_version="1.7.10"
    repositories {
    maven { url "https://jitpack.io" }
    mavenCentral()
    google()
    }
    dependencies {
    classpath "com.android.tools.build:gradle:4.2.1"
    classpath 'com.jakewharton:butterknife-gradle-plugin:10.2.3'
    classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"

    // NOTE: Do not place your application dependencies here; they belong
    // in the individual module build.gradle files
    }
    }

    allprojects {
    repositories {
    maven { url "https://jitpack.io" }
    mavenCentral()
    google()
    }
    }

    task clean(type: Delete) {
    delete rootProject.buildDir
    }


    不过仅仅是两个文件里面的内容不一致,这个文件的位置是一样的,而且我们添加的引入代码也是一样的。可以说,这只是顺带提一嘴,实际上不影响我们实现统一依赖管理这个方式。




  • 第三步:在所有module的「build.gradle」当中添加依赖


    这一步是最重要的,我们完成了上面两步之后,只是做好了准备,现在我们需要将我们每一个module里面「build.gradle」文件里面的依赖指向「config.gradle」文件。也就是下图圈起来的 那两个「build.gradle」文件。


    Snipaste_2023-04-20_14-15-58.png


    因为我们第二步的时候已经在根目录引入了「config.gradle」,所以我们在「build.gradle」就可以指向「config.gradle」例如:



    implementation rootProject.ext.dependencies.coreKtx



    这一行,就指代了我们「config.gradle」文件里面的 dependencies 数组里面的 coreKtx 的内容。完整示例如下:


    plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
    }
    android {
    namespace 'leo.dev.mvp.kt'
    // compileSdk 32
    compileSdk rootProject.ext.android.compileSdk

    defaultConfig {
    applicationId "leo.dev.mvp.kt"
    // minSdk 21
    // targetSdk 32
    // versionCode 1
    // versionName "1.0"
    //
    // testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

    minSdk rootProject.ext.android.minSdk
    targetSdk rootProject.ext.android.targetSdk
    versionCode rootProject.ext.android.versionCode
    versionName rootProject.ext.android.versionName

    testInstrumentationRunner rootProject.ext.android.testInstrumentationRunner

    }

    ......
    }

    dependencies {

    implementation fileTree(include: ['*.jar'], dir: 'libs')

    // implementation 'androidx.core:core-ktx:1.7.0'
    // implementation 'androidx.appcompat:appcompat:1.6.1'
    // implementation
    //
    // testImplementation 'junit:junit:4.13.2'
    // androidTestImplementation 'androidx.test.ext:junit:1.1.5'
    // androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'

    implementation rootProject.ext.dependencies.coreKtx
    implementation rootProject.ext.dependencies.appcompat
    implementation rootProject.ext.dependencies.material

    testImplementation rootProject.ext.dependencies.junit
    androidTestImplementation rootProject.ext.dependencies.testJunit
    androidTestImplementation rootProject.ext.dependencies.espresso

    }


    需要注意的是,我们在编写代码的时候,是没有代码自动补全的。所以得小心翼翼,必须要和「config.gradle」文件里面的名字向一致。




注意事项



  • 首先就是这种方式在coding的时候,是没有代码补全的(只有输入过的,才会有提示),我们需要确保我们的名字一致

  • 我们在增加依赖的时候,在「config.gradle」里面添加完之后,记得在对应的module里面的「build.gradle」里面添加对应的指向代码。


总结


以上就是本篇文章的全部内容,总结起来其实步骤不多,也就三步。但是需要注意的是细节。需要保持写入的依赖与「config.gradle」文件一致,并且未写过的词,是不会有代码自动补全的。


抬头图片


作者:肥仔仁
来源:juejin.cn/post/7224007334513770551
收起阅读 »

Android 自定义开源库 EasyView

  这是一个简单方便的Android自定义View库,我一直有一个想法弄一个开源库,现在这个想法付诸实现了,如果有什么需要自定义的View可以提出来,不一定都会采纳,合理的会采纳,时间周期不保证,咱要量力而行呀,踏实一点。 配置EasyView 1. 工程b...
继续阅读 »

  这是一个简单方便的Android自定义View库,我一直有一个想法弄一个开源库,现在这个想法付诸实现了,如果有什么需要自定义的View可以提出来,不一定都会采纳,合理的会采纳,时间周期不保证,咱要量力而行呀,踏实一点。


1682474222191_095705.gif.gif


配置EasyView


1. 工程build.gradle 或 settings.gradle配置


   代码已经推送到MavenCentral(),在Android Studio 4.2以后的版本中默认在创建工程的时候使用MavenCentral(),而不是jcenter()


   如果是之前的版本则需要在repositories{}闭包中添加mavenCentral(),不同的是,老版本的Android Studio是在工程的build.gradle中添加,而新版本是工程的settings.gradle中添加,如果已经添加,则不要重复添加。


repositories {
...
mavenCentral()
}

2. 使用模块的build.gradle配置


   例如在app模块中使用,则打开app模块下的build.gradle,在dependencies{}闭包下添加即可,之后记得要Sync Now


dependencies {
implementation 'io.github.lilongweidev:easyview:1.0.3'
}

使用EasyView


   这是一个自定义View的库,会慢慢丰富里面的自定义View,我先画个饼再说。


一、MacAddressEditText


MacAddressEditText是一个蓝牙Mac地址输入控件,点击之后出现一个定制的Hex键盘,用于输入值。


在这里插入图片描述


1. xml中使用


首先是在xml中添加如下代码,具体参考app模块中的activity_mac_address.xml


    <com.easy.view.MacAddressEditText
android:id="@+id/mac_et"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:boxBackgroundColor="@color/white"
app:boxStrokeColor="@color/black"
app:boxStrokeWidth="2dp"
app:boxWidth="48dp"
app:separator=":"
app:textColor="@color/black"
app:textSize="14sp" />


2. 属性介绍


这里使用了MacAddressEditText的所有属性,可以自行进行设置,使用说明参考下表。


属性说明
app:boxBackgroundColor设置输入框的背景颜色
app:boxStrokeColor设置输入框的边框颜色
app:boxStrokeWidth设置输入框的边框大小
app:boxWidth设置输入框大小
app:separatorMac地址的分隔符,例如分号:
app:textColor设置输入框文字颜色
app:textSize设置输入框文字大小

3. 代码中使用


    MacAddressEditText macEt = findViewById(R.id.mac_et);
String macAddress = macEt.getMacAddress();

macAddress可能会是空字符串,使用之前请判断一下,参考app模块中的MacAddressActivity中的使用方式。


二、CircularProgressBar


CircularProgressBar是圆环进度条控件。


在这里插入图片描述


1. xml中使用


首先是在xml中添加如下代码,具体参考app模块中的activity_progress_bar.xml


    <com.easy.view.CircularProgressBar
android:id="@+id/cpb_test"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
app:maxProgress="100"
app:progress="10"
app:progressbarBackgroundColor="@color/purple_500"
app:progressbarColor="@color/purple_200"
app:radius="80dp"
app:strokeWidth="16dp"
app:text="10%"
app:textColor="@color/teal_200"
app:textSize="28sp" />


2. 属性介绍


这里使用了MacAddressEditText的所有属性,可以自行进行设置,使用说明参考下表。


属性说明
app:maxProgress最大进度
app:progress当前进度
app:progressbarBackgroundColor进度条背景颜色
app:progressbarColor进度颜色
app:radius半径,用于设置圆环的大小
app:strokeWidth进度条大小
app:text进度条中心文字
app:textColor进度条中心文字颜色
app:textSize进度条中心文字大小

3. 代码中使用


    CircularProgressBar cpbTest = findViewById(R.id.cpb_test);
int progress = 10;
cpbTest.setText(progress + "%");
cpbTest.setProgress(progress);

参考app模块中的ProgressBarActivity中的使用方式。


三、TimingTextView


TimingTextView是计时文字控件。


在这里插入图片描述


1. xml中使用


首先是在xml中添加如下代码,具体参考app模块中的activity_timing_text.xml


    <com.easy.view.TimingTextView
android:id="@+id/tv_timing"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:text="计时文字"
android:textColor="@color/black"
android:textSize="32sp"
app:countdown="false"
app:max="60"
app:unit="s" />


2. 属性介绍


这里使用了TimingTextView的自定义属性不多,只有3个,TextView的属性就不列举说明,使用说明参考下表。


属性说明
app:countdown是否倒计时
app:max最大时间长度
app:unit时间单位:s(秒)、m(分)、h(时)

3. 代码中使用


    TimingTextView tvTiming = findViewById(R.id.tv_timing);
tvTiming.setMax(6);//最大时间
tvTiming.setCountDown(false);//是否倒计时
tvTiming.setUnit(3);//单位 秒
tvTiming.setListener(new TimingListener() {
@Override
public void onEnd() {
//定时结束
}
});
//开始计时
tvTiming.start();
//停止计时
//tvTiming.end();

参考app模块中的TimingActivity中的使用方式。


四、EasyEditText


EasyEditText是一个简易输入控件,可用于密码框、验证码输入框进行使用。


在这里插入图片描述


1. xml中使用


首先是在xml中添加如下代码,具体参考app模块中的activity_easy_edittext.xml


    <com.easy.view.EasyEditText
android:id="@+id/et_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:boxBackgroundColor="@color/white"
app:boxFocusStrokeColor="@color/green"
app:boxNum="6"
app:boxStrokeColor="@color/black"
app:boxStrokeWidth="2dp"
app:boxWidth="48dp"
app:ciphertext="false"
app:textColor="@color/black"
app:textSize="16sp" />


2. 属性介绍


这里使用了EasyEditText的所有属性,可以自行进行设置,使用说明参考下表。


属性说明
app:boxBackgroundColor设置输入框的背景颜色
app:boxFocusStrokeColor设置输入框获取焦点时的颜色
app:boxNum设置输入框的个数,4~6个
app:boxStrokeColor设置输入框的边框颜色
app:boxStrokeWidth设置输入框的边框大小
app:boxWidth设置输入框大小
app:ciphertext是否密文,用于密码框
app:textColor设置输入框文字颜色
app:textSize设置输入框文字大小

3. 代码中使用


        binding.cbFlag.setOnCheckedChangeListener((buttonView, isChecked) -> {
binding.etContent.setCiphertext(isChecked);
binding.cbFlag.setText(isChecked ? "密文" : "明文");
});
//输入框
binding.btnGetContent.setOnClickListener(v -> {
String content = binding.etContent.getText();
if (content.isEmpty()) {
showMsg("请输入内容");
return;
}
if (content.length() < binding.etContent.getBoxNum()) {
showMsg("请输入完整内容");
return;
}
showMsg("输入内容为:" + content);
});

参考app模块中的EasyEditTextActivity中的使用方式。


作者:初学者_Study
来源:juejin.cn/post/7225407341633175613
收起阅读 »

css水滴登录界面

web
前言 今天我们来分享一款非常有趣的登录界面,它使用HTML和CSS制作,具有动态的水波纹效果,让用户在登录时感受到了一股清凉之感。 基本html框架 <!DOCTYPE html> <html lang="en"> <head&...
继续阅读 »

前言


今天我们来分享一款非常有趣的登录界面,它使用HTML和CSS制作,具有动态的水波纹效果,让用户在登录时感受到了一股清凉之感。


基本html框架


<!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>
<link rel="stylesheet" href="water.css">
<link rel="stylesheet" href="form.css">
</head>

<body>
<div class="main">
<form>
<p>用户名<br />
<input type="text" class="textinput" placeholder="请输入用户名" />
</p>
<p>密码<br />
<input type="password" class="textinput" placeholder="请输入密码" />
</p>
<p>
<input id="remember" type="checkbox" /><label for="smtxt">记住密码</label>
</p>
<p>
<input type="submit" value="登录" />
</p>
<p class="txt">还没有账户?<a href="#">注册</a></p>
</form>
</div>
</body>
</html>

首先,我们来看HTML代码。这个登录界面包含一个表单,用户需要在表单中输入用户名和密码。我们使用p标签创建输入框,并设置class属性以便后续的CSS样式设置。此外,我们还在表单中添加了一个“记住密码”的复选框和一个登录按钮,同时还有一个注册链接。


表单样式


form{            
opacity: 0.8;
text-align: center;
padding: 0px 100px;
border-radius: 10px;
margin: 120px auto;
}

p {
-webkit-text-stroke: 1px #8e87c3;
}

对表单整体进行样式定义,使其位于水滴内部,p标签内文镂空。


.textinput{
height: 40px;
font-size: 15px;
width: 100px;
padding: 0 35px;
border: none;
background: rgba(250, 249, 249, 0.532);
box-shadow: inset 4px 4px 10px rgba(160, 162, 158, 0.814), 4px 4px 10px rgba(117, 117, 117, 0.3), 15px 15px 30px rgba(72, 70, 70, 0.193), inset -2px -2px 10px rgba(255, 254, 254, 0.873);
border-radius: 50px;
-webkit-text-stroke: 0px;
color: saddlebrown;
outline-style: none;
}

对输入框进行样式定义,取消镂空字体样式,取消轮廓线,设置阴影实现水滴一般效果。


input[type="submit"]{
width: 110px;
height: 40px;
text-align: center;
outline-style: none;
border-style: none;
border-radius: 50px;
background: rgb(31, 209, 218);
-webkit-text-stroke: 0px;
box-shadow: inset 4px 4px 10px rgba(160, 162, 158, 0.814), 4px 4px 10px rgba(117, 117, 117, 0.3), 15px 15px 30px rgba(72, 70, 70, 0.193), inset -2px -2px 10px rgba(255, 254, 254, 0.873);
}

我们使用了input[type="submit"] 选择器来选中提交按钮,并设置了按钮的大小、文本对齐方式、圆角和背景等样式,去除了轮廓线。同样采用了阴影来设置按钮,使其具有气泡一般的感觉,并设置背景色。


input[type="submit"]:hover {
background-color: rgb(31, 218, 78);
}

这段代码是用来为按钮添加鼠标悬停效果的。我们使用了input[type="submit"]:hover选择器来选中鼠标悬停在按钮上时的状态,并设置了背景颜色。当用户悬停在按钮上时,按钮的背景颜色会改变,非常引人注目。


a {
text-decoration: none;
color: rgba(236, 20, 20, 0.433);
-webkit-text-stroke: 1px;
}

a:hover {
text-decoration: underline;
}

提交按钮底部注册文字样式,采用镂空字体样式,鼠标移至该元素上方时,添加下划线。


* {
margin: 0;
padding: 0;
}
body {
background: skyblue;
}

这段代码是对所有元素的外边距和内边距进行清零,以便更好地控制元素的位置和大小,设置了整个页面的背景颜色为天蓝色。


.main {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 400px;
height: 400px;
box-sizing: border-box;
border-radius: 50%;
background: transparent;
box-shadow: inset 15px 10px 40px rgba(158, 158, 158, 0.303), 10px 10px 20px rgba(117, 117, 117, 0.3), 15px 15px 30px rgba(72, 70, 70, 0.193), inset -10px -10px 20px rgba(233, 229, 229, 0.873);
animation: move 6s linear infinite;
}

这段代码采用绝对定位,以便更好地控制它的位置。left: 50%; top: 50%; 将元素的左上角定位在页面的中心位置。通过transform属性将元素向左上角移动50%,以便让元素的中心位置与页面中心位置重合。设置元素的宽度和高度为400像素。background: transparent; 将元素的背景设置为透明色。box-shadow: inset 15px 10px 40px rgba(158, 158, 158, 0.303), 10px 10px 20px rgba(117, 117, 117, 0.3), 15px 15px 30px rgba(72, 70, 70, 0.193), inset -10px -10px 20px rgba(233, 229, 229, 0.873); 设置元素的阴影效果,包括内阴影和外阴影。animation: move 6s linear infinite; 为元素添加动画效果,其中move 是动画名称,6s是动画时长,linear是动画速度曲线,infinite是动画循环次数。


.main::after {
position: absolute;
content: "";
width: 40px;
height: 40px;
background: rgba(254, 254, 254, 0.667);
left: 80px;
top: 80px;
border-radius: 50%;
animation: move2 6s linear infinite;
filter:blur(1px);
}

.main::before {
position: absolute;
content: "";
width: 20px;
height: 20px;
background: rgba(255, 255, 255, 0.5);
left: 130px;
top: 70px;
border-radius: 50%;
animation: move3 6s linear infinite;
filter:blur(1px);
}

这段代码是对两个小球进行样式定义,将伪元素的定位方式设置为绝对定位,以便更好地控制它的位置,设置伪元素的宽度和高度一个为20px,一个为40px。设置伪元素的背景颜色为半透明白色。left,top 设置伪元素的左上角定位在主体元素的中心位置,设置伪元素的边框半径为50%,以便将其设置为圆形。animation: move2 6slinear infinite; 为伪元素添加动画效果,其中 move2 是动画名称,6s 是动画时长,linear 是动画速度曲线,infinite 是动画循环次数,另一个伪元素同理。
接下来是动画定义:


@keyframes move {
50% {
border-radius: 42% 58% 49% 51% / 52% 36% 64% 48% ;
}
75% {
border-radius: 52% 48% 49% 51% / 43% 49% 51% 57% ;
}
25% {
border-radius: 52% 48% 59% 41% / 43% 49% 51% 57% ;
}
}

@keyframes move2 {
25% {
left: 80px;
top: 110px;
}
50% {
left: 50px;
top: 80px;
}
75% {
left: 80px;
top: 120px;
}
}

@keyframes move3 {
25% {
left: 100px;
top: 90px;
}
50% {
left: 110px;
top: 75px;
}
75% {
left: 130px;
top: 100px;
}
}

这段代码定义了三个不同的动画,分别是move、move2和move3。move动画,它控制了元素的边框半径在不同时间点的变化。在这个动画中,元素的边框半径分别在25%、50%和75%的时间点进行了变化。move2move3动画,控制了一个伪元素的位置在不同时间点的变化。在这个动画中,伪元素的位置分别在25%、50%和75%的时间点进行了变化。


结语


以上便是全部代码,喜欢的可以自取,样式不

作者:codePanda
来源:juejin.cn/post/7225623397144199228
好看可以自行更改😜。

收起阅读 »

人生是一场长期的均值回归

我一直觉得记录并收藏每个阶段的状态是一件很有意义且奇妙的事,尤其是多少年后还能清晰地回忆其当初的心境,联想到曾经所设立的一些目标以及为之做出的努力,这些人生经历的脉纹清晰而完整,形成的回荡激扬的动力循环让人长久受益。 所以在即将进入下一个更长远的阶段之前,纪念...
继续阅读 »

我一直觉得记录并收藏每个阶段的状态是一件很有意义且奇妙的事,尤其是多少年后还能清晰地回忆其当初的心境,联想到曾经所设立的一些目标以及为之做出的努力,这些人生经历的脉纹清晰而完整,形成的回荡激扬的动力循环让人长久受益。


所以在即将进入下一个更长远的阶段之前,纪念珍贵的经历,留存美好的记忆,时间是个单向的旅程,在未来的某一刻回忆曾经,挺不错的。


真正的大师,永远怀着一颗学徒的心


尽管计算机科班出身的自己学习了这么多年,对于本专业的某些领域知识却仍然处于浅尝辄止的状态。不过庆幸自己的做事态度,由于存在一些偏执,对于一些方向的学习还做到了令自己满意的成绩。只要认定一个方向,只要开始着手做一件事,那就先做完,不然已经做的努力就白白浪费了。


究天人之际,通古今之变,成一家之言



工程应该包括业务和Infra。业务包括了UI、设计、编码、测试、部署等等,而Infra则包括了相对底层的基础设施平台的开发,其为上层提供服务。



jinengshu.png


还记得最早接触的是Android开发,曾经,2010年后的5年掀起了移动互联网的热潮,跟随着校科研团队的步伐,开始触碰这个方向。15年到17年初,从eclipse到andorid studio,从控件使用到自定义view,从Holo到曾盛极一时的Material Design,从原生开发到跨平台weex、flutter,又了解学习了性能调优、热修复、组件化、插件化、mvp、mvvm等技术,还记得在这个过程中,小程序小游戏也曾盛行一时,不过这个倒是比android简单多了。对于面向用户的终端应用,与服务器进行数据交互自然不可避免,我也因此了解了诸如xutils,httpclient,okhttp,retrofit和glide等库,在学习优雅android设计模式以及极致代码优化的同时,我永对设计这些工具库的Jake Wharton等大神致以最崇高的敬意!


记得16年暑假的时候,我用了两个多月的时间来调试一个andorid与服务端通信的bug,期间辛酸一言难尽,因为没有经验,服务器一直是一个黑盒的存在,这因此促使我去了解网络通信原理和后端开发流程。我也就开始了服务端研发的学习。一个是框架和工具的应用,诸如Netty、SSH、SSM、SpringSecurity、SpringCloud、Zookeeper、MySQL、Redis、Jetty、Tomcat、Nginx、OpenResty、Varnish、Keepalived、Kafka、RocketMQ、Xxl-Job、Seata、ShardingSphere、Docker、Kubernetes、Jenkins等等,一个是设计模式的理解和应用以及代码架构MVC到DDD的理解、迁移与应用,还有架构方面的学习,包括从单体架构到分层架构到微服务架构再到云原生架构以及DevOps的实践等,最后是为了系统稳定而进行的监控系统设计包括用于日志监控的ELK、用于链路监控的Skywalking以及用于指标监控的Prometheus和Grafana等。19年的时候,还和技术大佬川哥一起讨论当年最新的FAAS,这个过程也将我的技术理解和视野带到了一个新的高度。


当然,分布式也是后端不可逃脱的重点内容,包括分布式ID、分布式Session、分布式锁、分布式缓存、分布式文件存储、分布式事务、分布式限流、分布式任务调度、分库分表等等,这里要感谢MIT6.824课程,受益良多!


最早的服务端开发是没有前后端分离这一概念的,所有的前后端资源都集中由一个项目部署,那个时代还是JSP和Thymeleaf的天下。但随着移动互联网的普及以及当今应用复杂性和对实际生产的高性能要求,逐渐产生了诸如SOAP等协议和Restful架构风格,原来的单体项目才开始拆分。但,不论是早期还是架构设计理念有所进步以后,搞后端的人都避免不了对于web基础内容的学习,毕竟最常见的业务还有开发一个常见的管理系统。为此,我又乐此不疲的学习了JS、Html5、JQuery、Vue等技术。不过前端自己了解的都很浅很浅了,在这个过程中遇到的问题还是要请教各位前端大佬,此时,我要感谢我的老同学——房哥,一个人很好的前端开发者,曾一度帮我解决很多问题。


从设计到编码到部署到运维,基本打通了整体流程。在这里,特别感谢在阿里云EBS部门工作的汤兄,曾分享了他在做基础设施时的经验。对Infra的有一定认识后,我对工程的核心点又有了新的理解。


最后,感谢帮我改过简历的每位前辈!


jianli.jpg


冰冻三尺,非一日之寒



你知道的越多,你不知道的也越多



工程领域技术庞大繁杂,所以我对所进行的工作一直在不断的思考和总结,为了更好的于他人分享我的经验以及对诸如高可用、高性能、稳定系统的认识,我对服务端的核心内容以自己的理解进行了总结归纳。跳出技术细节的桎梏,全面考虑问题并有针对性的进行解决。


zongjie.jpg


算法是每个计算机人不可忽视的重要一课,除了可以更好的锻炼思维能力和编码能力,通过算法来学习一些代码的优化设计思想常常也可以用于实际生产之中。比如,我已经和不止一个师弟或同学讨论到某一个具体业务或者问题的解决方案,发现这些本身就是一个个经典的算法问题,如果有所总结和沉淀自然得心应手。所以硕士期间,我除了重新补充了对于专业课操作系统和计算机网络的学习,我还坚持刷了一些算法问题,主要是在LeetCode上。我要感谢我曾经看到的一个博主,奔跑的蜗牛,是他在坚持刷题的过程中不断总结并以录制Vlog的方式来分享才促使我坚持刷题并不断输出,而这一过程让我受益匪浅。


leetcode.png


bzhan.png


博学而笃志,且问而近思


一开始自己还是很浮躁的,总是想把很多内容都搞懂。虽然作为一名技术人,我们要构建一个广泛的知识体系,但技术领域的学习本质应该是一个专注钻研的过程,我们终归是要克制住诱惑,将某个领域做到精深。正如杨老师曾经说过的,“水桶装水量取决于最短板,但是大多数情况下,我们在工作中获取的回报,更多来源于自身的长处”。我在后来也逐渐认识到这一点并及时做出了调整,不过我还是非常庆幸有一段学习过不同领域各种技术的经历,这对我技术眼界的开阔有很大的提升,一个是服务端本身就是要为前端和移动端等平台来提供服务的,如果能对其他平台有一定的认识自然会对自己的编码设计有一定的科学指引,另一个是其他领域的设计思想也可以借鉴到当前领域的许多工作中。博观而约取,厚积而薄发!


感谢一路走来所遇到和知道的前辈,包括做安卓时的郭霖、张鸿洋、赖帆、任玉刚等,做前端时的Vben维护者金毛、在爱奇艺和快手工作过的老同学房哥等,做服务端时的沈剑、许令波、林晓斌、LinkedBear、松哥、Mrbird、小林、汤兄、传奇大佬Robert Tappan Morris等,学算法时的宫水三叶、左程云、闫学灿等。


大胆与礼貌



记录毛姆所著小说《刀锋》里的一段文字:


我并不怕犯错,搞不好会在其中一条冤枉路上,找到人生的目标,人生从来就没有白走的路,每一步都算数。


我们这一生,都怕走冤枉路,都想找到一条捷径,好用最快的速度接近自己的目标。


但事实上,当你并不清楚自己内心最真实的声音的时候,你只有不断尝试,才可能知道什么是适合自己的,就算尝试过后你还是不清楚自己想要什么,但你最起码知道一点,这不是自己喜欢的。



谦逊是青年人进入社会的一种必须常备的状态,但谦卑不是。


这些年的经历,我逐渐感受到因为自己缺乏勇气与自信导致自己失去了很多的机会。自己出身平凡,在见识了很多优秀的大佬后更觉自己渺小,时间长了开始优柔寡断,总觉得自己目前学的做的不充分,不敢去尝试更怕去竞争,患得患失。


鹏哥是我的一个舍友,我们也是七八年的同学了,他是一个性格很好的人,也是一个相当聪明的人,他的逻辑思维非常强,学习能力也很强,经常帮助我们周围很多同学解决很多我们查了很多资料都解决不了的硬件和软件方面的疑难杂症。如果把我们计算机相关专业的同学都放在一起,把我们的生活学习环境比作一个武林,那鹏哥绝对是一个深藏不露的世外高人,他很爱折腾技术,会主动学习一些技能来解决现实生活中的很多问题,比如只要我们去看他正在做的事,看看他的手机或电脑,总会发现很多实用的小工具,有些是他自己写的,有的也不知道是从哪些渠道获取的,但这些工具就是那么的神秘且好用!


鹏哥是我认为周围人里面绝对很出众的一个同学了,但很多时候他得到的往往与他的实力不够匹配,究其原因,也是不够自信,很多事不敢去尝试,总觉得自己的实力有限而不敢有过多的想法,但实际上呢?据我的经历以及我一些朋友跟我分享的一些事看来,总会有很多出身不如你、头脑也不会比你更聪明、技术能力不如你的人做出了非常好的成绩。他们当然很优秀,但如果仅从硬性技能出发而言,他们可能很多还比不上我们,比如我们学的时间更长,积累的编程经验或者理论基础都要超过很多入门才一两年的学生,之所以我们还停留在原地,是很多时候我们忽视了软性技能的培养,我们缺乏那一点初生牛犊不怕虎的干劲和勇气。我们都是普通人,又会有多少人有“秀美的容颜”、“聪颖的天资”、“出众的才华”、“骄人的学业”?如果不能改变自己的心态,即便我们的实力达到了某项任务需要的标准,我们的软实力还是会限制我们的发展。我们出身不如名校学生,如果还一直畏手畏脚,那就更不能在社会上与他们相竞争了。


生活、学习和工作中的很多事,其实本没有那么难,往往是自己不自信的心态为之蒙上了一层恐惧的阴影,之后面对机遇就会胆怯。这些年给我最深的感触就是,我们已经因为缺乏自信和勇气失去了好多机会,而如果有新的机遇让我们来尝试,那我们一定要敢闯,敢干,不怕失败,不畏周围可能存在的偏颇眼光,如果不去尝试那一定不会成功,即使尝试失败了也会成为下一次成功的经验砝码,人生从来就没有白走的路。


自信很重要,大胆一些,礼貌的去问就好了。


愿为江水,与君重逢


江水.jpg


这些年的读书过程中,我结识了与之相处时间最久的谭同学,我们认识了八年,在一个寝室生活就有六年多,自然对于双方的认识是无比的熟悉。谭兄一个非常优秀的人,他头脑聪明、思维敏捷,对于新知识和未知领域永远充满兴趣且会一直不断地探索和求知。做事具有极强的目标性,凡事绝不囿于当前的状态,以最大的自信来追求最接近完美的极致,我经常对他说,“你是我们周围同学中我认为最富有极客精神的那一位”,事实上,谭兄作为全院最优秀的学生同时也是最努力的那一位。


作为良师益友,我们在这八年的过程中,最多的讨论就是关于专业知识学习、技术提高、发展规划等方面的内容。在过去的五六年里,我们进行着不同方向的工作,在自己擅长的领域深耕厚植,当遇到涉及对方领域的问题时,我们都十分乐意交流双方的观点与经验。更为重要的,我十分崇尚开源精神,而谭兄也是非常重视学术交流和论点探讨的人,我们都会毫无保留的分享自己的知识及经验,即使对一方在不擅长某个领域时仍会给予尊重并提供最大的帮助,实现真正的成长。十分荣幸遇到了一个优秀的同学,不仅是在学业上得到了很多帮助,因为熟悉互相脾气秉性,我们也在日常的生活中得到了性格的磨合,在做人做事上也有了很大的进步。


我们终将分别,但想想,我的一生能和几个人共同生活达八年之久呢?一切都是命运,愿为江水,与君重逢。他将到昆士兰去继续自己新的学术研究,我也将准备进入我的下一个阶段。正如谭兄经常说的,我们还要经常保持联系,多交流学术问题,以科研会友,期待能够在KDD等顶会上能够见到对方来参会。而我则希望我们在追求理想的道路上还能相互引为知音,就像一条清澈的小溪在流向大海的途中遇到了另一条清澈的小溪,碰巧这两条小溪还是朝着同一个方向流向大海。祝我们都能实现自己的理想,成为自己想成为的那个人!


这是个拥挤的地方,而我却很平凡


在我读研的这几年里,开始逐渐将自己的精力和工作重心从工程转向科研,尽管如此,我仍然会时常关注工程界大致的一些动向,关注互联网职场人以及北漂青年的一些生活和境况,我喜欢他们忙碌的状态、积极的生活态度以及日常努力生活而表现出的韧性。


鼓楼.png
 


北漂人都是可爱的,不管生活有多少苟且都能够坚持自己的理想,在压力中求生存,平凡而生活更显真实。每个人都有自己选择的生活方式,不管遇到什么挫折与不顺,总要挺过去,毕竟生活总要继续,方向总会有的!


飞鹏各自远,且尽手中杯



君子敬而无失,与人恭而有礼,四海之内皆兄弟也!



友情是散落四处的点点温馨,平时想起一座城市,先会想起一些风景,到最后必然只想起这座城市里的朋友,是朋友,决定了我们与各个城市的亲疏。


很荣幸这些年遇到了一些很好的朋友,曾经的本科同学、如今的研究生同学、工作中认识的朋友以及各种社区交流技术和求职过程中遇到的朋友,不管是校内还是校外,不论是在济南还是在北京、南京、深圳......,他们都在我的人生中的不同阶段提供了极大的帮助。更为重要的是,他们分别从事不同领域的工作,分别在不同地域以不同身份生活,所得到的社会经验和生活感悟对我而言更是弥足珍贵的。


还记得东哥曾经给我分享他从周围人学到的做人之道。如何做一个高情商的人,其实就是与人的交往中要擅于交流,而擅于交流的前提在于你知道的很多,眼界开阔,你能够从容温和的和很多人聊,和不同阶层的人聊相应的话题,打破周围人对你的刻板印象,让自己成为一个有内涵又有趣的人。你的知识储备,你的形象以及谈吐都作为以上的评判标准。


我们终将生活在不同的城市,即使不能时常相见,我们也会随时在对方身处困境或者黑暗时及时的带去援助和光亮。所谓朋友,也只不过是互相使对方活得更加温暖、更加自在的那些人。


时空是个圆圈,直行或是转弯,我们总会相见!


最后


仅以上文来记录我当前阶段的状态与生活。《圣经.启示录》中写道:“我又看见一个新天新地,因为先前的天地已经过去了,海也不再有了”。是的,过去已经过去,未来终将来到,这是一个新的开始。


王小灏

开始于2023年4月1日

完成于2023年4月18日

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

【Git】代码提交到了错误的分支怎么办

Git
最近在git的代码提交中遇到了一个这样的情况,A分支要提交功能a1、a2、a3,B分支要提交功能b1、b2、b3,但是误把a2功能提交到了B分支上,一开始以为得在在A分支上重新把a2功能重写一遍,后来查阅了一番才知道有git cherry-pick 和 git...
继续阅读 »

最近在git的代码提交中遇到了一个这样的情况,A分支要提交功能a1、a2、a3B分支要提交功能b1、b2、b3,但是误把a2功能提交到了B分支上,一开始以为得在在A分支上重新把a2功能重写一遍,后来查阅了一番才知道有git cherry-pickgit rebase这种好东西。


image.png


首先要解决上面的失误,分为两步:




  • 把a2功能添加回A分支




  • 把a2功能从B分支中删除




1. 把a2功能添加回A分支(cherry-pick)


直接切到A分支,然后运行


git cherry-pick a2功能的SHA值

然后就可以了,就是这么简单就把a2挪到A分支上



2. 把a2功能从B分支中删掉(rebase)


切到B分支,然后运行


git rebase -i a2前一个功能的SHA值

运行后我们就能进入SHA值后面的提交文本记录



我们把a2对应的pick改为drop,就能删掉对应的提交,并且不影响后续b3的提交。


编辑方式:按i 进入编辑状态,按Esc:wq就能保存退出。


改成这样的就可以了



就能成功达到我们想要的效果了


image.png


平时Git Rebase 很少用到,直到这一次,不得不用,最后也顺利解决了问题,同时get到了新技能。


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

弯道超车的机会

前言 我一直很想把自己思考的东西表达出来,苦于语文成绩差,文字功力不够,想来想去也不知道用什么话来开场。我不喜欢站在高处对别人指指点点,毕竟每个人都不一样,即使你有成功的经验又如何,照样教育不到我,而且成功对于我来说还为时过早。更何况即使人们懂得了道理,在现实...
继续阅读 »

前言


我一直很想把自己思考的东西表达出来,苦于语文成绩差,文字功力不够,想来想去也不知道用什么话来开场。我不喜欢站在高处对别人指指点点,毕竟每个人都不一样,即使你有成功的经验又如何,照样教育不到我,而且成功对于我来说还为时过早。更何况即使人们懂得了道理,在现实中,知行合一的人一直是特立独行的猪,知而不行的人才是沉默的大多数。


但是基于一种可笑的理想,我仍然尽力把自己经历想法分享出来,希望能给你带来一点点的思考。


大学四年


大一


有一天,我在教室里用《生死时速》练习打字。


同学凑过来问这是什么游戏


“抓小偷,练习打字的”


“看起来挺有意思,好玩吗,我也试试”


“你来” 我把电脑递给他


他开始试玩这个游戏,我注意到他第一局就破掉了我的最高记录。


“挺快的” 我称赞道


接下来第二个同学也来试试,我注意到他第二局破掉我的最高记录。


他们不知道的是,我三个月前的暑假开始练习打字,这个游戏我已经练习三个月了。


打字游戏


我报名加入实验室,面试的学长问我会什么


“我学过一些 Python”


“你 Python 学到哪里了”


“函数”


“学到了函数,几乎等于什么都没学,跟他们零基础的同学差不多”


我当时对此嗤之以鼻,不以为然。


三个月前我在网上买了一本《零基础学习Python》的书,封面上的 “零基础”,“全彩”,“快速入门”,“视频教程” 和 “实战项目” 等关键词让我觉得这本书是我想要的。


现在看来这本教材质量十分堪忧。


更加令人毛骨悚然的是《零基础学习Python》还有《零基础学XXX》系列:《零基础学C语言》,《零基础学Java》,《零基础学Android》等。


“得益于” 我当时并没有意识到教材问题的严重性,我大一的对于编程学习效果可以说是微乎其微。


有幸的是学校课程成绩还可以。高数满绩,GPA全班第一。


零基础学习Python


国内除了极少专家外,大部分所谓的 “专家” 撰写的教材的质量堪忧。这种《零基础学XXX》系列的教材,更是质量堪忧中的质量堪忧,感觉是一群商人用来来哄骗刚入门的小白。如果用来当作 “字典” 查询尚可容忍,但是作为启蒙读物无法容忍,让人误入歧途。


对于启蒙书籍的选择容不得半点马虎。对于小白想在短时间评估书籍质量,首先应该咨询自己查询(比如知乎,豆瓣),并且咨询有经验的学长学姐,让他从他确实读过的书里给你一个合理的建议。


大二


发现了《零基础学XXX》系列的教材的质量问题,我在网上找了新的学习资料将之前学过的内容重新学了一遍。


我有幸加入了是食铁兽科技做研发,是一家创业公司,里面的核心人员都拥有我校第一梯队技术或者能力,我学到了一些东西。


stskj


我有幸拿到了元象科技的实习 offer,国内做全真互联网目前最领先的互联网公司。跟我一起进来的实习生有清华大学,复旦大学,香港大学,深圳大学等知名高校的本科甚至研究生,我学历最低。


一次我主持 “云原生监控 Prometheus” 的技术分享会,如果公司使用新方案替代之前的旧方案,能减少 80% 的日志储存成本。


提问环节台下隔壁项目组的 Leader 问我是那个学校的。


“重邮”


“重邮是哪个学校?”


“就是重庆邮电大学”


“重庆邮电大学?”


“邮电学校吧,听说过” 旁边的同事说。


“研究生吗?”


“不是,本科”


“讲的不错”


我向下看着桌脚下的地面,想起了曾今大一的时候去面试重邮新生典礼,我手里拿着稿子,支支吾吾,双腿发抖的场景。心里感触颇深。


大三


我看到学校里充满激情,热血,求知欲,和梦想的同学们通过层层考核进入实验室后,只能自己去学习,去B站,极客等各个网站找项目做。实验室只是提供了一个更好的自习室,实验室的项目是极少的,而且项目的质量甚至不如网上找的项目。


我无意去辩论某些项目,或者网校的项目是否会好些,这种辩论是没有意义的。


我只是看到因为没有项目,同学们独自埋头苦干,主动来实验室的同学寥寥无几,同学之间的交流和互动少,新一届的同学不能很好的借鉴前一届的经验。况且网上的项目并没有服务于真正的用户,你并不需要为你的代码负责,你只需要完成项目,这种项目对于学习的帮助是有限的。


我创立了 “关二爷科技有限公司”,团队核心人员都拥有我校第一梯队技术水平。并且有幸得到了许多在头部互联网就职的学长,创业的学长,和前公司的大佬们的指导和支持。


我们开始开发一款能最大程度降低用户记笔记压力软件 “咕咕笔记”,帮助用户能跟多的记录自己的思考,并且希望未来能不断迭代,帮助更多的人的同时,让实验室的同学能接触到生产级的项目。


遗憾的是后期项目开发进度不及预期,团队核心成员都在忙着找实习,项目暂时搁置,后转交给实验室的同学继续完成。


g2ykj


秋招有幸拿到了阿里,华为,B站,oppo等公司的 offer。


大四


在互联网上我们经常能听到每年能读几百本书的大神。这个速度听起来非常厉害,但是他们究竟用了什么方法来完成这一目标却很少有人关注。


我了解到了主题阅读,如果你想要深入了解某个领域,主题阅读是最好的方式。主题阅读可以同时读3~4本书的阅读法,在熟练后甚至可以同时读30~40本。


说得再简单一些,主题阅读是围绕着某个主题,在一定时间内快速阅读多本书。


比如我做 “如何读书” 的主题阅读。


《如何阅读一本书》:70%的书翻翻目录就行,20%的书需理解框架,7%需要将书读薄再读厚,3%的书需要一辈子反复读。读书应该分多种类型,有对应的阅读规则。


《快速阅读》:传统的读书速度太慢了!大脑得不到充分的使用所以会才会去想其他事情分神。加快速度非但不会影响阅读效果,反而能促进对文章的理解。


罗翔《至法学新人的一封信:关于读书》:读书的目的有功利性的目的和非功利性的目的,前者是为了成功,后者是对前者的纠偏。读书也要非功利性的读书。


《读懂一本书:樊登读书法》:讲书是最高效的沉浸式学习方法,死读书是没用的,需要讲出来给别人听,并且多次安利他个人的樊登读书会。


《这样读书就够了》:不少所谓的读书会主要内容就是把一本书摘抄为5页的PPT或者半个小时的精华干货。殊不知删掉的恰恰可能是最可能帮助学习者内化和应用的东西,留下了最不值钱的核心知识。我们必须区分 “以知识为中心的学习” 和 “以自己为中心的学习”。


因为主题是相似的,所以每一本书都可能有大量的重复内容,这些重复内容理的阅读是非常迅速的。当然,每一本书也有自己的特色,《这样读书就够了》甚至对《读懂一本书:樊登读书法》提到的读书会进行了批判。


因为再好的书也有局限性,甚至可能是错误的。但是大量的书中重复的内容大概率是正确的,而且不同的内容中各种思想会相互碰撞,我们择其善者而从之,就很有可能成为大师。


如何读书书架


毕业论文我准备跳出舒适圈尝试一下区块链,但是它对于我来说是全新的领域,我无法掌控它的难度。


导师催提交毕业论文开题报告,一周后发给她。


我问导师区块链方向有人做过么,效果如何。


“我带的学生没有做过,效果说不清楚”


在提交开题报告的一周时间,我在公司上班的时候抽空对区块链领域进行了主题阅读。


5 天的时间读完了 5 本书,完成了基本的区块链知识的学习。


2 天完成了毕业设计的整个代码部分,并且完成了测试。


建议


上了大学就轻松了是最大的谎言


高中的时候我的老师经常说:“现在你们辛苦点,上了大学,就轻松了”。


我每次听到这种话都想不通很多事:大一新生和高三学生,在智力,能力,经验上,差距有多大?如果大学生能力没有得到提升,和直接高中生的优势在哪里?对于上了大学就跃龙门这种事我是不愿意相信的。


“大学一定不是学习生涯的结束,是开始”


本科教育早已崩溃


我不得不遗憾地告诉大家一个消息:


“国内绝大部分大学的本科教育,已然彻底崩溃。”


我无意争论是否有更好的学校(复旦、中科大、或者清华、北大)比我们崩溃的更少一些。


我也无意列举学校偶然出现的闪光般的措施(如引入MIT麻省理工课程体系,中外合作办学)来说服你容忍余下99%的无意义。


因为任何这些都是欲盖弥彰。


没有老师会对你负责


我看到领导不会为你负责。对于一个争做世界一流大学的研究型学校,管好科研,管好实验室才是当务之急。相比之下,本科生教学显得无利可图。教授也不会为你负责。拉到足够的经费发表足够的论文,满足学院要求才是生存大计。要说管学生,也肯定先要管好自己实验室的硕士博士,而非那一百多人大课堂里的某个本科生。


学不懂?


要么因为你智力低,要么就是自己底下不用功。


为什么跟你一个班上的某某某同学,人家就能懂?


诚然,就算是老师上课说孟加拉语,一个班上也非常有可能冒出一两个翻翻书看看图就能考到八九十分的同学。


真正在课堂上口传心授的教学,其质量是不会有人过问的。教学评估会考察实验报告格式是否合格,出勤率是否够,但是绝对不会考察上百人的班上到底有几个听懂了的。


对于学校和老师来说,搞科研、发期刊才是主要任务,至于上课教学,不过是个不需要怎么负责的副业罢了,基本看具体教师的良心。


而本科生真正值得学校关心的,只有就业率。


因为这是本科生唯一与学校利益挂钩的指标。


于是,到了大四,学校就会开始关注你的就业情况,甚至进行一些神奇操作。


因为从制度上来讲


“无论是升学还是就业,培养都不是必须的。”


学生自己不负责


我看到个别国内的老师或者中外合作的外国老师尝试做出改变,却收效甚微。老师上课各种提问和互动,台下鸦雀无声,学生依旧在下面睡觉玩手机,当老师感觉自己几次都像是跳梁小丑,于是又回到了大多数中。


因为其他老师们都念 PPT 抓考勤,学生们已经习惯了。


别把期待全盘托付他人


“把期待全盘托付他人,必定会迎来悲惨的结果”


我看到无数充满求知欲、激情、与年轻梦想的同学们,不断地把自己的四年青春,充满希望与信任地托付给大学来塑造。然后在大学的所谓磨练中逐渐变得随意、平淡、失去梦想(除非你把保研/考研当作最终的梦想)。


我看到越来越多的同学们在逃避思考,逃避未来,彷佛一个成年了的大孩子(尽管现在的社会就是把大学生当不懂事的孩子一样,多么可笑)。


这使我心中非常不安。


机会


前段时间一个同学问我大学怎样学习,我说了现在的大学状况。


他问我说,“我现在好迷茫,那我该怎么办?”


“如果你意识到了而其他同学没有,这是将是一次弯道超车的机会。”


结语


本文章首次撰写于2023年3月,一定是有局限性甚至错误。内容是否正确,需要自己去评判,就像人生的路,需要自己去走。


我只希望能提供给你一个思考的方向。如果你能通过这篇文章开始思考你自身以及周围环境,那它的价值就是至高无上的了。


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

Android 项目的 Code Reviewer 清单

查看 Android 代码库时要记住的几点: 代码审查有时是一个乏味的过程,但我相信我们需要花更多的时间在这上面。 也许这是你学习或分享一些知识的机会。 这里列出了一些在 Android 项目的 Code Review 过程中检查所必需的要点。 Memory ...
继续阅读 »

查看 Android 代码库时要记住的几点:
代码审查有时是一个乏味的过程,但我相信我们需要花更多的时间在这上面。 也许这是你学习或分享一些知识的机会。
这里列出了一些在 Android 项目的 Code Review 过程中检查所必需的要点。


Memory Leaks(内存泄漏):


想象一下以下情况:一个漂亮的应用程序,但同时速度很慢,屏幕之间的导航每次都变慢。
在代码审查期间要检查的一些要点:



  • 这段新代码是否有任何 Context 保留?

  • 有没有相关的 RxAndroid 代码? 如果是,请检查 RxCall 是否在生命周期范围(ViewModel/Fragment/Activity)结束时被释放。

  • 如果代码有 coroutines(协程),检查 Job 是否从 ViewModel 范围启动以正确释放。

  • 代码中有使用 CountDownTimerAsyncTask 或任何视频/音频播放器的实现吗? 如果是,则代码应该释放与避免泄漏相关的内存资源。

  • 是否有新的 Fragment with ViewBinding 方法,代码应该在 onDestroyView 方法处释放绑定。


private ResultProfileBinding binding;

@Override
public View onCreateView (LayoutInflater inflater,
ViewGroup container,
Bundle savedInstanceState) {
binding = ResultProfileBinding.inflate(inflater, container, false);
View view = binding.getRoot();
return view;
}

@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}

深度布局:


有时我们有一个 ViewGroup 有另一个 ViewGroup 作为子布局。
一个好的建议是始终使用 ViewGroup ConstraintLayouts 来保持布局更平坦。 它可以避免导致性能问题的过度绘制。
Android 文档中提到了一些减少过度绘制的技巧。


资源注释:


Android 世界中的每个资源都有一个标识符,它是一个 Integer 类型。 如何确保我们的 Integer 值代表一个有效的资源 ID? 注释可以帮助我们解决这个问题。
例如:


fun showMessage(context: Context, val idRes: Int) {
Toast.makeText(context, idRes, Toast.SHORT_LENGHT).show()
}

在上面的代码中,Code Reviewer 会建议你,在这里使用注释。
例如:


fun showMessage(context: Context, @StringRes val idRes: Int) {
Toast.makeText(context, idRes, Toast.SHORT_LENGHT).show()
}

其他注释示例在这里


类似组件:


Code Review 期间投入时间非常重要。 尝试按照作者的想法思考,反映他的选择,提出问题以理解代码。
一个新的需求,需要写很多新代码。 应该和项目组成员及时沟通,比如:“此代码与我们在此组件中已有的相同”。 像这样的沟通说明是必不可少的,因为最重要的是重用我们代码库中已经存在的代码。


危险代码区:


如果你有遇到敏感的代码或危险代码区。 危险代码是 Code Reviewer 需要重点查看的,当你发现一些代码没有任何意义的时候。在这种情况下,不要评判作者。与他们交谈以了解该代码背后的原因。 这次谈话可能是一个学习新东西的机会。也有可能是你自己对这块业务考虑不周,没了解到这段代码本质的原因。如果交流后,发现还是确实有问题,就可以把这块代码区的问题给更正了。


架构违规📐:


软件架构是定义在软件各部分之间的通信协议。
Code Review 是识别架构违规等问题的非常有效的手段。 在这种情况下可以做如下沟通是比较合适的:
“你的 ViewModel 正在访问存储库。 我们在它们之间有一个用例。 请看一下 MainViewModel 文件,这是一个可以参考的例子。”
“为什么要在 ViewModel 中使用 Adapter 引用? 适配器是一个 RecyclerView 组件。 最好把它放在一个 Fragment 中,让它远离我们的 ViewModel。”


小细节决定一切:



  • 未使用 import

  • 未使用的资源,例如 drawables, strings, colors……

  • 注释代码;

  • 未格式化的代码;

  • 变量名、方法名、文件名……

  • 代码不遵循样式指南。 例如,Kotlin 定义了一个定义明确的样式指南。 它对任何开发人员在现有代码库中快速找到任何软件组件都有很大帮助。


结论


静态分析工具在代码审查过程中很有帮助,但它们并不是 100% 有效的。 如果你的团队正在寻找代码质量,关键的 Code Review 是必不可少的。


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

Android串口开发与SerialPort工具封装

Android串口开发之SerialPort工具封装 一. 什么是串口 串口通讯(Serial Communication),是指外设和计算机间,通过数据信号线、地线等,按位进行传输数据的一种通讯方式。 串口是一种接口标准,它规定了接口的电气标准,没有规定接口...
继续阅读 »

Android串口开发之SerialPort工具封装


一. 什么是串口


串口通讯(Serial Communication),是指外设和计算机间,通过数据信号线、地线等,按位进行传输数据的一种通讯方式。


串口是一种接口标准,它规定了接口的电气标准,没有规定接口插件电缆以及使用的协议。


在Android开发这一块来说,串口通信应用越来越广泛,得益于物联网的发展,很多定制设置在Android系统上外挂设备,如何与外挂设备进行双方通信,就需要串口来进行。


其实串口的通信和蓝牙的通信有很多相似的地方,数据帧的处理相似,只是连接通信的方式不同了而已,蓝牙的通信可以使用Android Api的方式,而串口需要自己编译Ndk,这里把封装的工具类开源出来,希望对大家有帮助。


二. Android如何进行串口通信


在Android的Framwork中是没有相关的Api的,我们想进行串口的相关操作还是得借助于谷歌2012年的老库android-serialport-api
项目结构如下:



编译NDK为对应的so库,如何使用Java调用想要的方法,想想真令人头秃







网上有很多编译的教程了,如果觉得麻烦的同学可以往下看我的封装库

三. Android串口通信的封装


allprojects {
repositories {
maven { url 'https://jitpack.io' }
}
}

implementation 'com.gitee.newki123456:serial_port_tools:1.0.0'

一句话解决串口通信:


    private lateinit var mSerialPortManager: SerialPortManager
var cmd = byteArrayOf(-91, 85, 1, -5) //开启通信的指令


//具体调用就是2个回调,设置串口,开启指令
mSerialPortManager = SerialPortManager()
mSerialPortManager
.setOnOpenSerialPortListener(object : OnOpenSerialPortListener {
override fun onFail(paramFile: File?, paramStatus: OnOpenSerialPortListener.Status) {
Toast.makeText(mContext,paramStatus.toString(),Toast.LENGTH_SHORT).show()
}

override fun onSuccess(paramFile: File) {

}
})
//设置串口的数据通信回调
.setOnSerialPortDataListener(object : OnSerialPortDataListener {
override fun onDataReceived(paramAnonymousArrayOfByte: ByteArray) {
//解析返回的数据转换为摄氏度
val i = paramAnonymousArrayOfByte[3]
val f = (paramAnonymousArrayOfByte[2] + i * 255 + 20) / 100.0f
val message = Message.obtain()
message.obj = java.lang.Float.valueOf(f)
message.what = 1
mHandler.sendMessage(message)
}

override fun onDataSent(paramArrayOfByte: ByteArray?) {

}
})
.openSerialPort(File("dev/ttyS3"), 115000) //打开指定串口

mSerialPortManager.beginBytes(cmd) //开启读取

重点概念:

串口路径 波特率 输入指定的指令

只要能通信了之后,处理数据的逻辑其实就和蓝牙的数据帧处理很像啦。


项目介绍:

自定义Android设备,支持人脸识别打卡并外挂的红外温度传感器,我们再识别比对到人脸成功之后需要通过串口拿到温度计的温度展示出来。

源码在此




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

为什么智能硬件首选MQTT

前言 前面讲了Android如何通过串口通信操作硬件,但实际业务场景大多是既可以屏幕操控硬件也可以远程下发操控,这时就需要MQTT协议来完成这一工作。本文将介绍MQTT协议及其在物联网设备通信中的应用。 一、初识MQTT 1. 什么是MQTT协议 MQTT是一...
继续阅读 »


前言


前面讲了Android如何通过串口通信操作硬件,但实际业务场景大多是既可以屏幕操控硬件也可以远程下发操控,这时就需要MQTT协议来完成这一工作。本文将介绍MQTT协议及其在物联网设备通信中的应用。


一、初识MQTT


1. 什么是MQTT协议


MQTT是一种轻量级的消息传递协议,全称为Message Queuing Telemetry Transport。它是基于发布-订阅模式的协议,可以在分布式系统中实现设备之间的消息传递和通信。


2. 为什么要用MQTT


它是一种非常轻量级的二进制协议,并且由于其最小的数据包开销,与 HTTP 等协议相比,MQTT在通过网络传输数据时表现出色。该协议的另一个重要方面是 MQTT 在客户端非常容易实现。易用性是 MQTT 开发的一个关键问题,使其非常适合当今资源有限的受限设备。


3. MQTT诞生背景



  • 1999 年:Andy Stanfork-Clark (IBM) 和 Arlen Nipper 发布 MQTT 协议,用于通过卫星连接石油管道遥测系统,MQTT 中的 TT (Telemetry Transport) 就是源于这样一个遥测系统;

  • 2010 年:MQTT 协议免费发布;

  • 2014 年:MQTT 协议正式成为 OASIS 标准,经过多年的发展,MQTT 协议已经成为互联网 (IoT) 的主要协议之一。


目前,MQTT 主要分为两个大版本:



  • MQTT v3 其中 v3.1.1 是目前接入设备最多的版本;

  • MQTT v5 2018 年发布,部分设备接入。


4. MQTT核心要求



  • 简单实现

  • 服务质量数据传输

  • 轻巧且带宽高效

  • 数据不可知

  • 连续会话意识


5. MQTT 发布/订阅模式


MQTT 发布/订阅模式(也称为发布/订阅)提供了传统客户端-服务器架构的替代方案。在客户端-服务器模型中,客户端直接与端点通信。发布/订阅模型将发送消息的客户端(发布者)与接收消息的客户端(订阅者)分离。发布者和订阅者从不直接联系对方。事实上,他们甚至不知道对方的存在。它们之间的连接由第三个组件(代理)处理。代理的工作是过滤所有传入的消息并将它们正确分发给订阅者。
image.png
pub/sub 最重要的方面是消息的发布者与接收者(订阅者)的解耦。这种解耦有几个方面:



  • 空间解耦:发布者和订阅者不需要相互了解(例如,不需要交换 IP 地址和端口)。

  • 时间解耦:发布者和订阅者不需要同时运行。

  • 同步解耦:两个组件的操作在发布或接收过程中不需要中断。


总之,MQTT 发布/订阅模型消除了消息发布者与接收者/订阅者之间的直接通信。代理的过滤活动可以控制哪个客户端/订阅者接收哪个消息。解耦具有三个维度:空间、时间和同步。


6. 可拓展性


pub/sub 比传统的客户端/服务器模式有了更好的拓展,这是由于 broker 的高度并行化,并且是基于事件驱动的模式。可扩展性还体现在消息的缓存和消息的智能路由,还可以通过集群代理来实现数百万的连接,使用负载均衡器将负载分配到更多的单个服务器上,这就是 MQTT 的深度应用了。


7. 消息过滤



  • 基于主题的过滤此过滤基于属于每条消息的主题。接收客户端向代理订阅感兴趣的主题,订阅后,broker 就会确保客户端收到发布到 topic 中的消息。

  • 基于内容的过滤在基于内容的过滤中,broker 会根据特定的内容过滤消息,接受客户端会经过过滤他们感兴趣的内容。这种方法的一个显著的缺点就是必须事先知道消息的内容,不能加密或者轻易修改。

  • 基于类型的过滤当使用面向对象的语言时,基于消息(事件)的类型/类进行过滤是一种常见做法。例如,订阅者可以收听所有类型为 Exception 或任何子类型的消息。


**发布/订阅并不是每个用例的答案。在使用此模型之前,您需要考虑一些事项。**发布者和订阅者的解耦是发布/订阅的关键,它本身也带来了一些挑战。例如,您需要事先了解已发布数据的结构。对于基于主题的过滤,发布者和订阅者都需要知道要使用哪些主题。要记住的另一件事是消息传递。发布者不能假定有人正在收听所发送的消息。在某些情况下,可能没有订阅者阅读特定消息。


8. 主要特性



  • MQTT 在空间上解耦了发布者和订阅者。要发布或接收消息,发布者和订阅者只需要知道代理的主机名/IP 和端口。

  • MQTT 按时间解耦。尽管大多数 MQTT 用例近乎实时地传递消息,但如果需要,代理可以为不在线的客户端存储消息。(必须满足两个条件才能存储消息:客户端已连接到持久会话并订阅了服务质量大于 0 的主题)。

  • MQTT 异步工作。因为大多数客户端库异步工作并且基于回调或类似模型,所以任务在等待消息或发布消息时不会被阻塞。在某些用例中,同步是可取且可能的。为了等待某个消息,一些库有同步 API。但流程通常是异步的。


二、MQTT通信过程


1. 连接到MQTT服务器


使用MQTT协议建立通信的第一步是连接到MQTT服务器。MQTT客户端需要提供MQTT服务器的IP地址和端口号,以建立到服务器的TCP连接。一旦连接建立成功,客户端将发送连接请求(Connect Packet)到MQTT服务器,服务器将响应确认请求。


2. 发布主题


发布者(Publisher)将消息发布到特定的主题上。在MQTT中,主题是消息的标识符。主题可以是任何有效的UTF-8字符串,并且可以包含多个层次结构(例如,/sensors/temperature)。当发布者发送消息到主题时,服务器将转发该消息给所有订阅该主题的订阅者。


3. 订阅主题


订阅者(Subscriber)可以订阅感兴趣的主题。在MQTT中,订阅者可以订阅整个主题树,或仅订阅特定的主题。当订阅者订阅主题时,服务器将记录其订阅信息,并将其添加到订阅列表中。


4. 接收消息


一旦订阅者订阅了主题,服务器将转发所有发布到该主题的消息给订阅者。当订阅者接收到消息时,它将执行相应的操作,例如更新设备状态或向其他设备发送命令。


5. 断开连接


使用完MQTT协议后,客户端应该断开与MQTT服务器的连接。MQTT客户端将发送断开请求(Disconnect Packet)到MQTT服务器,服务器将响应确认请求并断开连接。


三、MQTT常用术语



  1. Broker:MQTT的中间件,负责接收和转发消息。

  2. Client:MQTT的客户端,可以是发布者或订阅者。

  3. Topic:MQTT的消息主题,用于识别消息的内容和目的地。格式: “myhome/livingroom/temperature”或“Germany/Munich/Octoberfest/people”

  4. QoS(Quality of Service):MQTT消息的服务质量,分为三个级别:QoS 0,QoS 1和QoS 2。

  5. Retain:MQTT消息的保留标志,表示该主题上最新的消息是否需要保留。

  6. Last Will and Testament(LWT):MQTT客户端的遗嘱消息,用于在客户端离线时通知其他客户端。

  7. Clean Session:MQTT客户端连接时的清理会话标志,用于指示中间件是否保留客户端的订阅信息。


四、总结


MQTT协议的优点



  1. 简单和轻量级:MQTT协议非常简单和轻量级,适合于小型设备和带宽受限的环境。

  2. 可靠:MQTT协议具有多种消息传递保证机制,例如QoS级别,可确保消息可靠地传递到目标设备。

  3. 灵活性:MQTT协议具有灵活的主题订阅机制,允许客户端订阅不同级别的主题。

  4. 可拓展性:MQTT协议支持多个客户端连接到单个服务器,并且可以扩展到支持数百万个设备。


MQTT协议的缺点:



  • 安全性:MQTT协议本身没有提供加密和身份验证机制。如果需要安全通信,则需要使用TLS/SSL等其他协议。

  • 可靠性受限:MQTT提供了不同的服务质量(QoS)级别,但最高的QoS级别也不能保证消息100%可靠传递。在不稳定的网络环境下,消息可能会丢失或重复传递,需要应用程序自行处理。

  • 适用场景受限:MQTT适用于传递简单的消息,但不适用于传递大量的数据或复杂的消息。在需要进行复杂消息处理和转换的场景中,可能需要使用更为复杂的消息传递技术,例如消息队列。

  • 性能受限:MQTT是一种轻量级的协议,但在高并发和大规模消息传递的环境中,可能会面临性能瓶颈。此外,在使用较高的QoS级别时,可能会导致更多的网络流量和延迟,从而影响系统的性能。

  • 可扩展性受限:MQTT本身不提供扩展性的支持,需要使用其他技术来实现分布式消息传递和扩展性。


MQTT其他协议对比:


与其他通信协议相比,MQTT具有一定的优势:



  1. 与HTTP对比:HTTP是一种请求/响应模型的通信协议,适用于网页浏览等场景。相较于HTTP,MQTT协议更轻量级,适用于低带宽、不稳定网络环境。同时,MQTT基于发布/订阅模式,支持实时数据推送,而HTTP需要轮询获取数据,实时性较差。

  2. 与WebSocket对比:WebSocket是一种双向通信协议,适用于实时Web应用。与WebSocket相比,MQTT协议在低带宽和低功耗场景下具有更高的性能优势。此外,MQTT协议内置消息队列和QoS(Quality of Service,服务质量)机制,可确保消息可靠传输。

  3. 与CoAP对比:CoAP(Constrained Application Protocol,受限应用协议)是另一种物联网通信协议,主要针对资源受限的设备。相较于CoAP,MQTT协议在低带宽环境下具有更好的性能。然而,CoAP协议基于UDP,具有较低的传输延迟。二者之间的选择取决于具体应用场景和需求。

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

Android 带你重新认知属性动画

前言 之前写过一篇关于属性动画简单使用的文章juejin.cn/post/714417… 虽然官方直接提供的属性动画只有4个效果:透明度、位移、旋转、缩放,然后用Set实现组合,用插值器加一些效果。但其实属性动画能做的超越你的想象,他能做到anything。你...
继续阅读 »

前言


之前写过一篇关于属性动画简单使用的文章juejin.cn/post/714417…


虽然官方直接提供的属性动画只有4个效果:透明度、位移、旋转、缩放,然后用Set实现组合,用插值器加一些效果。但其实属性动画能做的超越你的想象,他能做到anything。你可以实现各种你所想象的效果,改图片形状、路径的动画、颜色的变化等(当然这得是矢量图)。而插值器,除了系统提供的那些插值器之外,你还能进行自定义实现你想要的运动效果。


实现的效果


我这里拿个形变的效果来举例。可以先看看实现的效果:


sp.gif


实现要点


要点主要有两点:(1)要去想象,到了这种程度包括更复杂的效果,没有人能教你的,只能靠自己凭借经验和想象力去规划怎么实现。 (2)要计算,一般做这种自定义的往往会涉及计算的成分,所以你要实现的效果越高端,需要计算的操作就越复杂。


思路


我做这个播放矢量图和暂停矢量图之间的形变,这个思路是这样的: 其实那个三角形是由两部分组成,左边是一个矩形(转90度的梯形),右边是一个三角形。然后把两个图形再分别变成长方形。具体计算方式是我把width分成4份,然后配合一个偏移量offset去进行调整(计算的部分没必要太纠结,都是要调整的)


步骤:



  1. 绘制圆底和两个图形

  2. 属性动画

  3. 页面退出后移除动画


1. 绘制圆底和两个图形


一共三个Paint


init {
paint = Paint()
paint2 = Paint()
paint3 = Paint()

paint?.color = context.resources.getColor(R.color.kylin_main_color)
paint?.isAntiAlias = true
paint2?.color = context.resources.getColor(R.color.kylin_white)
paint2?.style = Paint.Style.FILL
paint2?.isAntiAlias = true
paint3?.color = context.resources.getColor(R.color.kylin_white)
paint3?.isAntiAlias = true
}

绘制圆底就比较简单


paint?.let {
canvas?.drawCircle((width/2).toFloat(), (height/2).toFloat(), (width/2).toFloat(),
it
)
}

然后先看看我的一个参考距离的计算(有这个参考距离,才能让图形大小跟着宽高而定,而不是写死)


override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
if (baseDim == 0f){
baseDim = (0.25 * width).toFloat()
}
}

另外两个图用路径实现


if (path1 == null || path2 == null){
path1 = Path()
path2 = Path()
// 设置初始状态
startToStopAnim(0f)
}
paint2?.let { canvas?.drawPath(path1!!, it) }
paint3?.let { canvas?.drawPath(path2!!, it) }

看具体的绘制实现


private fun startToStopAnim(currentValue : Float){
val offset : Int = (baseDim * 0.25 * (1-currentValue)).toInt()

path1?.reset()
path1?.fillType = Path.FillType.WINDING
path1?.moveTo(baseDim + offset, baseDim) // 点1不变
path1?.lineTo(2 * baseDim+ offset - baseDim/3*currentValue,
baseDim + (0.5 * baseDim).toInt() * (1-currentValue))
path1?.lineTo(2 * baseDim+ offset - baseDim/3*currentValue,
2 * baseDim +(0.5 * baseDim).toInt() + (0.5 * baseDim).toInt() * currentValue)
path1?.lineTo(baseDim+ offset, 3 * baseDim) // 点4不变
path1?.close()


path2?.reset()
path2?.fillType = Path.FillType.WINDING
if (currentValue <= 0f) {
path2?.moveTo(2 * baseDim + offset, baseDim + (0.5 * baseDim).toInt())
path2?.lineTo(3 * baseDim + offset, 2 * baseDim)
path2?.lineTo(2 * baseDim + offset, 2 * baseDim + (0.5 * baseDim).toInt())
}else {
path2?.moveTo(2 * baseDim+ offset + baseDim/3*currentValue,
baseDim + (0.5 * baseDim).toInt() * (1-currentValue))
path2?.lineTo(3 * baseDim + offset, baseDim + baseDim * (1-currentValue))
path2?.lineTo(3 * baseDim + offset, 2 * baseDim + baseDim * currentValue)
path2?.lineTo(2 * baseDim+ offset + baseDim/3*currentValue,
2 * baseDim +(0.5 * baseDim).toInt() + (0.5 * baseDim).toInt() * currentValue)
}
path2?.close()
}

这个计算的过程不好解释,加偏移量就是一个调整的过程,可以去掉偏移量offset看看效果就知道为什么要加了。path1代表左边的路径,左边的路径是4个点,path2是右边的路径,右边的路径会根据情况去决定是3个点还是4个点,默认情况是3个。


2、属性动画


fun startToStopChange(){
isRecordingStart = true
if (mValueAnimator1 == null) {
mValueAnimator1 = ValueAnimator.ofFloat(0f, 1f)
mValueAnimator1?.addUpdateListener {
val currentValue: Float = it.animatedValue as Float
startToStopAnim(currentValue)
postInvalidate()
}
mValueAnimator1?.interpolator = AccelerateInterpolator()
}
mValueAnimator1?.setDuration(500)?.start()
}

float类型0到1其实就是实现一个百分比的效果。变过去能实现后,变回来就也就很方便


fun stopToStartChange(){
isRecordingStart = false
if (mValueAnimator2 == null) {
mValueAnimator2 = ValueAnimator.ofFloat(1f, 0f)
mValueAnimator2?.addUpdateListener {
val currentValue: Float = it.animatedValue as Float
startToStopAnim(currentValue)
postInvalidate()
}
mValueAnimator2?.interpolator = AccelerateInterpolator()
}
mValueAnimator2?.setDuration(500)?.start()
}

3.移除动画


view移除后要移除动画


fun close(){
try {
if (mValueAnimator1?.isStarted == true){
mValueAnimator1?.cancel()
}
if (mValueAnimator2?.isStarted == true){
mValueAnimator2?.cancel()
}
}catch (e : Exception){
e.printStackTrace()
}finally {
mValueAnimator1 = null
mValueAnimator2 = null
}
}

然后还要注意,这个动画是耗时操作,所以要做防快速点击。


总结


从代码可以看出,其实并实现起来并不难,难的在于自己要有想象力,要能想出这样的一个过程,比较花费时间的可能就是一个调整的过程,其它也基本没什么技术难度。


我这个也只是简单做了个Demo来演示,你要问能不能实现其它效果,of course,你甚至可以先把三角形变成一个正方形,再变成两个长方形等等,你甚至可以用上贝塞尔来实现带曲线的效果。属性动画就是那么的强大,对于矢量图,它能实现几乎所有的你想要的效果,只有你想不到,没有它做不到。


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

🔥面试官想听的离职原因清单

交锋 面试官:方便说下离职原因吗? 掘友1:不方便 掘友2:在前公司长期工作量有些太大了,我自己身体上也出现了一些信号,有段时间都完全睡不着觉,所以需要切换一个相对来讲工作量符合我个人要求的,比如说周末可以双休这样一个情况,这个对我现在的选择来讲还蛮重要的 掘...
继续阅读 »

交锋


面试官:方便说下离职原因吗?


掘友1:不方便


掘友2:在前公司长期工作量有些太大了,我自己身体上也出现了一些信号,有段时间都完全睡不着觉,所以需要切换一个相对来讲工作量符合我个人要求的,比如说周末可以双休这样一个情况,这个对我现在的选择来讲还蛮重要的


掘友3:本来已经定好的前端负责人(组长),被关系户顶掉了,我需要一个相对公平的竞争环境,所以打算换个公司


掘友4:实不相瞒,一年前我投过咱们公司(或者面试过但没过),一年了,你知道我这一年是怎么过的吗,因为当时几轮面试都很顺利的,结果却回复我说有更合适的人选了,我难受了很久,因为真的很想进入咱们公司,于是我奋发图强,每天熬夜学习到凌晨两点半,如今学有所成,所以又来了


掘友5:团队差不多解散了吧,领导层变动,没多久时间原团队基本都走了,相当于解散了吧,现在剩几个关系户,干的不开心


掘友6:公司要开发一些灰产(买马/赌球/时时彩之类的),老员工都不愿意搞,就都要我来做,我堂堂掘友6可是与赌毒不共戴天的人,怎么会干这种事(就是害怕坐牢),就辞职了(这是位入职时间不长的掘友)


掘友7:公司业务调整,然后突然让我去外地分公司驻场/让我去搞 flutter(原本是前后端),虽然是个好机会可还是很难受,而且与我的职业发展规划不符,所以不想浪费时间,就第一时间辞职了


掘友8:前东家对我挺好的,工作也得心应手(进入舒适圈了),只是我不想一直呆在舒适圈,我不是那种混日子的人,所以希望跳出来,找一份更有挑战性,更有成就感的工作,贵公司的岗位很符合我的预期


掘友9:公司最近经营不理想:1.不给交社保/公积金了,2.拖欠几个月工资了,好不容易攒的奶粉钱都花完啦(虽然还单身,可也是有想法的),为了生活,这不办法呀,3.公司倒闭了,现在十几个同事都在找工作,咱们这还需要前后端、产品、设计、测试吗,我可以内推


掘友10:您可能也知道现在各行各业行情都不太好,很多公司都裁撤了部分业务,前公司前几年疫情时就已是踏雪而行了,现在在新业务的选择上就决定裁撤掉原来的业务线,我也是其中一员,虽然很遗憾但也能接受吧,在前公司这两年也是学到了很多


掘友11:我其实挺感谢上家公司的,各方面都挺好的,也给了我很好的成长空间,但是也三年多时间了,我的薪资也没涨过,相信你也知道,其实我现在的薪资能够值得更好的,嗯被认可


掘友12:克扣工资,领导说以后生产环境上出现一次 bug 就要扣所有参与的人工资,说真的,每天加班加点的干,我们都没问题,可结果就被这样对待,被连带扣了几次之后心里真的很难受


掘友13:回老家发展咯/对象在这边咯,因为准备结婚了,之后一直在这边发展定居了(这种换城市的回答要给出准备结婚或定居发展这样的原因,不然谈个对象就换城市会显得不靠谱);如果是小城市换大城市,可以直接说是为了高薪来的,因为家里买房了生孩子了啥的经济压力大,顾家其实是能体现稳定的,也给砍价打个预防针


掘友14:(有断层,面试时间和上次离职时间相隔时间有点长,有两三个月左右的,如果真实情况是家里或者生病啥的直说就好,如果只是找了几个月工作没找到,就要组织下语言了),由于长时间加班的原因,身体受到了影响每天睡不好觉,那段时间一直不在状态,没法好好投入工作,就想休息一段时间,为避免给公司造成不好的影响,所以辞职了。当时领导坚持批我几天假,我自己也不知道具体多久能恢复过来,毕竟那种种状态也不是一天两天了,还是坚持让领导批我的辞职了,然后这段时间我去了哪哪哪,身体已经调整过来了,可以全身心投入工作了,不过现在找工作希望是周末可以双休这样一个情况,这个对我现在的选择来讲还蛮重要的


(如果断层有一年的左右的,我有一段经历可以给大家参考下)我当时没工作了,家里投了点钱让我和一个亲戚合伙搞了点生意,结果赚了点钱,但那个亲戚喜欢赌钱,被他拿去赌了,输光了,于是我撤出来了


沐华:就是觉得翅膀硬了,想出去看看(其实这是我入职现公司面试时说的离职原因,当时面试官听着就笑了)


第一轮回答结束!





心法


离职原因真实情况绝大多数情况无非就几种:钱少了,不开心了,被裁了。


大家都差不多的,面试官心里也知道,可这能直说吗?


直说也不是不行,但是要注意表达方式,回答时有些场面话/润色一下还是需要的,去掉负面的字句,目的是让人家听的人舒服一点而已,毕竟谁也不喜欢一个陌生人来向自己诉苦抱怨,发牢骚吧,谁都希望认识正能量/积极向上的人吧


所以回答的关键在于:



  1. 不能是自己的能力、懒惰、不稳定等原因,或可能影响团队的性格缺陷

  2. 不要和现任说前任的不好,除非客观原因没办法,但也要知道即便是前公司真实存在的问题,hr 并不了解真实情况,还是会对是前公司有问题,还是你有问题持怀疑态度的


就像分手原因,对别人说出来时不能显得自己很绝情,又不能让自己很跌份,而且很忌讳疯狂抹黑前任


公司想降低用人风险,看我们离职的原因在这里会不会再发生,所以我们回答中应该体现:稳定性、有想法、积极向上、找工作看重什么、想得到什么、有规划不是盲目找的....


忌讳:升职机会渺茫、个人发展遇到瓶颈、人际关系复杂受人排挤、勾心斗角氛围差...这样的回答会让人质疑是前公司的问题,还是你的能力/情商有问题?


那么,你觉得最好的答案是什么呢,如果你是面试官,会选谁进入下一轮?


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

不想写代码的程序员可以尝试的几种职位

标题不够严谨,应该是不想写业务代码的程序员可以做什么。 这里主要覆盖大家可能平时没关注,或者是国内少见的工作;所以像 technical product manager, project manager 这种就不再赘述了。 这里也主要分享 IT 行业内的岗位,...
继续阅读 »

标题不够严谨,应该是不想写业务代码的程序员可以做什么。


这里主要覆盖大家可能平时没关注,或者是国内少见的工作;所以像 technical product manager, project manager 这种就不再赘述了。


这里也主要分享 IT 行业内的岗位,要是除开行业限制,范围就太大了。


Developer Relation/Advocate


国外有很多面向开发者的技术创新公司,比如 Vercel ,PlanetScale ,Prisma ,Algolia 等。


这类公司的用户就是开发者,所以他们的市场活动也都是围绕着开发者;他们需要让更多的开发者可以更容易地把他们的技术用到他们的技术栈里,所以就有了这种岗位。用中文来表达,可能有点类似是布道师的意思?


国内更多是将技术应用起来,而不是创造一些新的技术,所以这种岗位在国内就非常少见了。当然近几年也还是有一些技术驱动型公司的,像 PingCAP ,Agora 等。


希望国内有更多像这样的公司出来。


Technical Recruiter


这个工作从 title 上就大概知道是做什么的了。


这个岗位有深有浅,深的可能是比较完整的招聘职能,浅的可能就是 HR 部门里面试和筛选技术候选人的。


Technical Writer


这个听着像是产品经理的工作,确实会和产品的职责有小部分重叠。


这是个面向内部的岗位,不喜欢对外对用户 /客户的朋友会非常喜欢。通常是一些比较大型的企业要做软件迁移或者什么系统、流程升级之类的时候,因为会牵扯到非常多的 moving parts ,所以通常都需要一个独立岗位来负责 documentation 的工作。


工作内容包括采访以及记录各部门的现有流程和业务需求,然后是新流程 /系统 /软件的手册、图表等等。


这里的“technical”不是我们研发中的技术,更多是“业务”层面的意思。同样这个岗位对技术要求不高,但是有研发背景是非常加分的。


Technical Support


通常这个岗位归属客服部门,高于普通 customer service rep 。普通的 customer support 是客户遇到问题时的第一层支持 - 基本会讲话、了解产品就能干的工作;如果第一层解决不了客户的问题,就会升级到后面 technical support 了。


这个岗位范围会更广一点,几乎任何 IT 公司都会有这种支持岗;对技术要求根据不同公司而不同,比如 Vercel 对这个岗位的技术要求肯定比 HelpScout (一个客服软件)要高。


但整体来说都不如研发要求高,但对应的薪酬待遇也没有研发那么好。


结语


其实说了这么多总结下来就是国外技术生态、开源氛围好很多,并且对技术足够的重视,促使很多技术公司的出现,然后催生了这些工作。


如果觉得本帖有启发,欢迎留言支持鼓励后续的创作。




作者:强生
来源:juejin.cn/post/7229223235680895031

收起阅读 »

朝花夕拾 - 卷王的自白(光头祭天,法力无边)

一、震撼开场 做一个卷王 ta 有什么错,无非就是 —— 「秃」了那么一点点!!! 咳咳咳,一一回复: 自愿的 没有想不开 没有考到寺庙 心态正常 …… 如果非要给这次的行为贯穿一个理由,那就是「下周四就 28 岁了」「头发长长太快想凉快点」! 二、“正...
继续阅读 »

一、震撼开场


做一个卷王 ta 有什么错,无非就是 ——


04-10-01.jpg


「秃」了那么一点点!!!


咳咳咳,一一回复:



  • 自愿的

  • 没有想不开

  • 没有考到寺庙

  • 心态正常

  • ……


如果非要给这次的行为贯穿一个理由,那就是「下周四就 28 岁了」「头发长长太快想凉快点」!


二、“正文”


Hello 小伙伴们早上、中午、下午、晚上和深夜好,这里是 jsliang~


鸽了近一个月,终于能恢复正常作息,和小伙伴们一起折腾、聊天吹水、学习啦!


04-10-02.png


当然,这次我想在 jsliang 的「朝花夕拾」频道,和小伙伴们聊的,不仅仅是鸽了的这个月的内容。


更多的,咱们要从年初,乃至年前聊起。


虽然可能会聊到很多内容,但是咱们尽可能长话短说!



  1. 这几个月怎么过来的?

  2. 你觉得有意思的事情是什么?

  3. 你觉得很失败/颓的事情是什么?

  4. 这几个月发生的事情有哪些让你关注的?

  5. 后面想做啥事情?


三、话痨小剧场


本篇图文尽可能不涉及技术等硬通货


3.1 这几个月怎么过来的?


从 2 月过完年回来后,996 甚至偶尔 997 呀,然后喜提「卷王」称号。


大概故事线就是:



  1. jsliang 想挑战下自己

  2. 然后转开发组,挑战新项目

  3. 不巧作为几年老开发,评估需求失败,3 周的需求,愣是做了 10 周

  4. 于是「自愿」加班,顶着压力 996 乃至 997


Q:你干嘛哎哟!这么一说这几个月可歌可泣的故事,你一点都没体现啊!


04-10-03.png


A:这你就不懂了吧,这里不说的简单点,小伙伴们怎么往下看~


3.2 你觉得有意思的事情是什么?


这几个月感觉蛮有意思的事情,大概有那么几个。


首先,能学到一些没玩过的技术啦!


对于一些「浅薄」的技术,我总是那么沉迷,比如这一次学习了:



  1. Formily。阿里面向中后台复杂场景的表单解决方案

  2. Slate / tinymce。这 2 款不同的富文本编辑器,一种是 React,一种是 Vue,在 2 个不同项目都接触了


04-10-04.png


然后,就是对旧知识的巩固啦!


没错,就是你 React Hook~


有一说一每次写 Hook 我都很沉迷


—— 为什么这个组件多次渲染,为什么这个组件又不渲染了!


回头写一篇小作文吧,在这里吐槽占篇幅太大了。


接着,就是对升华身心的一种挑战。


这几个月被大佬吐槽过能力不行、需求管理没做好;


这几个月碰到过技术问题,经常辗转反侧想不通;


这几个月熬夜有点多,脸色蜡黄黑眼圈……


04-10-05.png


不过还好,每天坚持跑步和跟小伙伴吐槽,让我挺过来了,很有意思,下次别喊我体验了,哈哈~


最后,就是通过直播、发视频、写文章认识了更多的小伙伴。


现在 VX 有 2000+ 好友,距离 30 岁的 5000 好友又近了一步!


不同小伙伴让我认识了世界不一样的精彩~


3.3 你觉得很失败/颓的事情是什么?


这个算糗事集合了吧哈哈~


Round 1


小丑鸭碰到白天鹅的尴尬~


04-10-06.png


Round 2


如 3.2 所说,因为瞎排期,明明做了 10 周的需求,被我在需求稿上写成了 3 周。


然后,我就被骂了哈哈哈,没人撑我,甚是失败。


当时有想过跑路……


Round 3


赚的钱还是那么卑微而少,但是肉眼可见爸妈年轻不再。


有时候会迷茫啥时候能回家买地皮自己起楼,2023 年的机遇我到底抓住了没有。


04-10-07.png


以上,简单说说 3 件印象比较深刻的,后面有更多失败和令人颓废的事咱们再唠嗑唠嗑~


3.4 这几个月发生的事情有哪些让你关注的?


有 2 个:



  • 人工智能

  • 前端已死


当时本来想蹭热度去写写,后面自己把自己说服了,按表不发,等后面逐个打通(如果有小伙伴感兴趣听我吐槽的话)


目前只能说保持对这 2 个话题的关注,提升自己对这 2 块相关技能的学习。


其他的,慢慢来啦,咱走个长期攻略。


3.5 后面想做啥事情?


先补全一些遗憾:



  • CSS 系列更新到第 8 篇(当前第 5 篇,尽量补充几个实用性的)

  • 出门旅游 1 次

  • 补充文章+视频:人工智能

  • 补充文章+视频:前端已死

  • 恢复每日晚上 9:00-11:00 的直播


长期上,可能会关注一下「自由职业」和「独立能力」,毕竟真不能想象自己还能在一线做开发多久~


那么,就酱啦!


周六下午要加班,晚上我会把剃光头的视频和本期内容整合成一个小视频,晚上回去直播,欢迎关注光头前端!


See you tonight~




不折腾的前端,和咸鱼有什么区别!


觉得文章不错的小伙伴欢迎点赞/点 Star。


如果小伙伴需要联系 jsliang



个人联系方式存放在 Github 首页,欢迎一起折腾~


争取打造自己成为一个充满探索欲,喜欢折腾,乐于扩展自己知识面的终身学习斜杠程序员。


作者:jsliang
来源:juejin.cn/post/7223417872596697148
收起阅读 »

Android 中创建子线程的方式有哪几种

在 Android 中,创建子线程的方式通常有以下几种: 使用 Thread 类进行创建 Thread 是 Java 中的一个类,可以通过继承 Thread 类或者创建 Thread 对象并传入 Runnable 对象来创建子线程。例如: // 继承 Th...
继续阅读 »

在 Android 中,创建子线程的方式通常有以下几种:



  1. 使用 Thread 类进行创建 Thread 是 Java 中的一个类,可以通过继承 Thread 类或者创建 Thread 对象并传入 Runnable 对象来创建子线程。例如:


// 继承 Thread 类
public class MyThread extends Thread {
@Override
public void run() {
// 子线程要执行的代码
}
}

// 创建 Thread 对象并传入 Runnable 对象
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
// 子线程要执行的代码
}
});
thread.start();


  1. 使用 Runnable 接口进行创建 Runnable 是 Java 中的一个接口,可以通过实现 Runnable 接口并将其传入 Thread 对象来创建子线程。例如:


// 实现 Runnable 接口
public class MyRunnable implements Runnable {
@Override
public void run() {
// 子线程要执行的代码
}
}

// 创建 Thread 对象并传入 Runnable 对象
Thread thread = new Thread(new MyRunnable());
thread.start();


  1. 使用 AsyncTask 类进行创建 AsyncTask 是 Android 中的一个类,可以通过继承 AsyncTask 类并重写其方法来创建子线程。AsyncTask 可以方便地进行 UI 操作,并且不需要手动处理线程间通信问题。例如:


public class MyTask extends AsyncTask<Void, Void, Void> {
@Override
protected Void doInBackground(Void... voids) {
// 子线程要执行的代码
return null;
}

@Override
protected void onPostExecute(Void aVoid) {
// UI 操作
}
}

// 创建 AsyncTask 对象并调用 execute 方法
MyTask task = new MyTask();
task.execute();


  1. 使用线程池进行创建 线程池是一种可以重复利用线程的机制,可以减少创建和销毁线程所带来的开销。Android 中常用的线程池包括 ThreadPoolExecutor 和 ScheduledThreadPoolExecutor。例如:


// 创建 ThreadPoolExecutor 对象
ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());

// 创建 ScheduledThreadPoolExecutor 对象
ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1);

综上所述,Android 中常用的创建子线程的方式有使用 Thread 类、使用 Runnable 接口、使用 AsyncTask 类和使用线程池。每种方式都有其优缺点,需要根据实际

作者:早起的年轻人
来源:juejin.cn/post/7229401405344415781
需求选择合适的方式。

收起阅读 »

Flutter App开发实现循环语句的方式

如果您有小程序、APP、公众号、网站相关的需求,您可以通过私信来联系我 Flutter 中循环语句的使用方式与其他编程语言比较类似,常见的包括 for 循环和 while 循环。 1 for 循环 Flutter 中的 for 循环语法如下: for (va...
继续阅读 »

如果您有小程序、APP、公众号、网站相关的需求,您可以通过私信来联系我



Flutter 中循环语句的使用方式与其他编程语言比较类似,常见的包括 for 循环和 while 循环。


1 for 循环


Flutter 中的 for 循环语法如下:


for (var i = 0; i < count; i++) {
// 循环体
}

其中的 count 为循环次数, i 初始值默认为 0,每次循环自增 1。在循环体内部可以编写需要重复执行的代码。 例如,以下代码循环输出 1 到 10 的数字:


for (var i = 1; i <= 10; i++) {
print(i);
}

下面是一个使用 for 循环实现的案例,用于遍历一个列表并输出其中的元素。假设有一个列表 fruits ,其中包含了一些水果,现在需要遍历列表并输出其中的每个元素:


List<String> fruits = ['apple', 'banana', 'orange', 'grape'];
for (String fruit in fruits) {
print(fruit);
}

上述代码中,使用 for 循环遍历了列表 fruits 中的每个元素,变量 fruit 用于存储当前循环到的元素,并输出了该元素。在每次循环中,变量 fruit 都会被更新为列表中的下一个元素,直到遍历完整个列表为止。


2 for in


在 Flutter 中, for...in 主要是用于遍历集合类型的数据,例如 List、Set 和 Map。


下面是一个使用 for...in 遍历 List 的案例:


List<int> numbers = [1, 2, 3, 4, 5];
for (int number in numbers) {
print(number);
}

上述代码中, numbers 是一个包含整数的 List, for...in 循环遍历该 List 中的每个元素,将每个元素赋值给变量 number ,并输出 number 的值。在每次遍历中, number 都会被更新为 List 中的下一个元素,直到遍历完整个 List 为止。


下面是一个使用 for...in 遍历 Map 的案例:


Map<String, String> fruits = {
'apple': 'red',
'banana': 'yellow',
'orange': 'orange',
'grape': 'purple'
};
for (String key in fruits.keys) {
print('$key is ${fruits[key]}');
}

上述代码中, fruits 是一个包含水果名称和颜色的 Map, for...in 循环遍历该 Map 中的每个键,将每个键赋值给变量 key ,并输出该键及其对应的值。在每次遍历中, key 都会被更新为 Map 中的下一个键,直到遍历完整个 Map 为止。


在遍历集合类型的数据时,使用 for...in 语句可以简化代码,避免了使用下标、索引等方式进行访问和处理,使代码更加易读、优雅。


3 while 循环


Flutter 中的 while 循环语法如下:


while (expression) {
// 循环体
}

其中, expression 是布尔表达式,循环体内部的代码会一直循环执行,直到 expression 不再为真时跳出循环。 例如,以下代码使用 while 循环实现输出 1 到 10 的数字:


var i = 1;
while (i <= 5) {
print(i);
i++;
}

上述代码中,我们定义了一个变量 i ,并使用 while 循环判断 i 是否小于 5,如果为真,则输出变量 i 的值并将 i 的值加 1,然后继续循环;如果为假,则跳出 while 循环。


在每次循环中,变量 i 都会被更新为上一次的值加 1,直到变量 i 的值达到 5 时, while 循环结束。


while 循环还可以和条件表达式一起使用,例如,下面是一个使用 while 循环判断列表是否为空的示例:


List<int> numbers = [1, 2, 3, 4, 5];
while (numbers.isNotEmpty) {
print(numbers.removeLast());
}

上述代码中,我们定义了一个包含整数的列表 numbers ,并使用 while 循环判断 numbers 是否为空,如果不为空,则输出列表中的最后一个元素并将其从列表中删除,然后继续循环;如果为空,则跳出 while 循环。
在每次循环中, numbers 列表都会被更新,直到列表为空时 while 循环结束。 使用 while 循环可以在满足一定条件的情况下,重复执行一组语句,从而实现某些特定的功能需求。


在使用 while 循环时,需要注意控制循环条件,避免出现死循环的情况。


以上就是 Flutter 中实现循环语句的方式。




如果你有兴趣,可以关注一下我的综合公

作者:早起的年轻人
来源:juejin.cn/post/7229388804611932217
众号:biglead

收起阅读 »

前端访问系统文件夹

随着前端技术和浏览器的升级,越来越多的功能可以在前端实现而不用依赖于后端。其中,访问系统文件夹是一个非常有用的功能,例如上传文件时,用户可以直接从自己的电脑中选择文件。 使用方法 在早期的浏览器中,要访问系统中的文件夹需要使用 ActiveX 控件或 Java...
继续阅读 »

随着前端技术和浏览器的升级,越来越多的功能可以在前端实现而不用依赖于后端。其中,访问系统文件夹是一个非常有用的功能,例如上传文件时,用户可以直接从自己的电脑中选择文件。


使用方法


在早期的浏览器中,要访问系统中的文件夹需要使用 ActiveX 控件或 Java Applet,这些方法已经逐渐淘汰。现在,访问系统文件夹需要使用HTML5的 API。


最常使用的 API 是FileAPI,配合 input[type="file"] 通过用户的交互行为来获取文件。但是,这种方法需要用户选择具体的文件而不是像在系统中打开文件夹来进行选择。


HTML5 还提供了更加高级的 API,如 showDirectoryPicker。它支持在浏览器中打开一个目录选择器,从而简化了选择文件夹的流程。这个 API 的使用也很简单,只需要调用 showDirectoryPicker() 方法即可。


async function pickDirectory() {
const directoryHandle = await window.showDirectoryPicker();
console.log(directoryHandle);
}

但是需要注意的是该api的兼容性较低,目前所支持的浏览器如下图所示:


image.png


点击一个按钮后,调用 pickDirectory() 方法即可打开选择文件夹的对话框,选择完文件夹后,该方法会返回一个 FileSystemFileHandle 对象,开发者可以使用这个对象来访问所选择的目录的内容。


应用场景


访问系统文件夹可以用于很多场景,下面列举几个常用的场景。


上传文件


在前端上传文件时,用户需要选择所需要上传的文件。这时,打开一个文件夹选择器,在用户选择了一个文件夹后,就可以读取文件夹中的文件并进行上传操作。


本地文件管理


将文件夹中的文件读取到前端后,可以在前端进行一些操作,如修改文件名、查看文件信息等。这个在 纯前端文件管理器 中就被广泛使用。


编辑器功能


访问系统文件夹可以将前端编辑器与本地的文件夹绑定,使得用户直接在本地进行编写代码,而不是将代码保存到云端,这对于某些敏感数据的处理尤为重要。


结尾的话


通过HTML5的 API,前端可以访问到系统中的文件夹,这项功能可以应用于上传文件、本地文件管理和编辑器功能等场景,为用户带来了极大的便利。


作者:白椰子
来源:juejin.cn/post/7222636308740014135
收起阅读 »

可视化大屏:vue-autofit 一行搞定自适应

web
可视化大屏适配/自适应现状 可视化大屏的适配是一个老生常谈的话题了,现在其实不乏一些大佬开源的自适应插件、工具但是我为什么还要重复造轮子呢?因为目前市面上适配工具每一个都无法做到完美的效果,做出来的东西都差不多,最终实现效果都逃不出白边的手掌心,可以解决白边问...
继续阅读 »

可视化大屏适配/自适应现状


可视化大屏的适配是一个老生常谈的话题了,现在其实不乏一些大佬开源的自适应插件、工具但是我为什么还要重复造轮子呢?因为目前市面上适配工具每一个都无法做到完美的效果,做出来的东西都差不多,最终实现效果都逃不出白边的手掌心,可以解决白边问题的,要么太过于复杂,要么会影响dom结构。


三大常用方式




  1. vw/vh方案



    1. 概述:按照设计稿的尺寸,将px按比例计算转为vwvh

    2. 优点:可以动态计算图表的宽高,字体等,灵活性较高,当屏幕比例跟 ui 稿不一致时,不会出现两边留白情况

    3. 缺点:每个图表都需要单独做字体、间距、位移的适配,比较麻烦




  2. scale方案



    1. 概述:也是目前效果最好的一个方案

    2. 优点:代码量少,适配简单 、一次处理后不需要在各个图表中再去单独适配.

    3. 缺点:留白,据说有事件热区偏移,但是我目前没有发现有这个问题,即使是地图也没有




  3. rem + vw vh方案



    1. 概述:这名字一听就麻烦,具体方法为获得 rem 的基准值 ,动态的计算html根元素的font-size ,图表中通过 vw vh 动态计算字体、间距、位移等

    2. 优点:布局的自适应代码量少,适配简单

    3. 缺点:留白,有时图表需要单独适配字体




基于此背景,我决定要造一个简单又好用的轮子。


解决留白问题


留白问题是在使用scale时才会出现,而其他方式实现起来又复杂,效果也不算太理想,总会破坏掉原有的结构,可能使元素挤在一起,所以我们还是选择使用scale方案,不过这次要做出一点小小的改变。


常用分辨率


首先来看一下我的拯救者的分辨率:


image-20230420141240837 它可以代表从1920往下的分辨率


我们可以发现,比例分别是:1.77、1.6、1.77、1.6、1.33... 总之,没有特别夸张的宽高比。


计算补齐白边所需的px


只要没有特别夸张的宽高比,就不会出现特别宽或者特别高的白边,那么我们能不能直接将元素宽高补过去?也就是说,当屏幕右侧有白边时,我们就让宽度多出一个白边的px,当屏幕下方有白边时,我们就让高度多出一个白边的px。


很喜欢CSGO玩家的一句话:"啊?"


先想一下,如果此时按宽度比例缩放,会在下方留下白边,所以设置一下它的高度,设置多少呢?比如 scale==0.8 ,也就是说整个#app缩小了0.8倍,我们需要将高扩大多少倍才可以回到原来的大小呢?


QQ录屏20230420144111


emmm.....


算数我最不在行了,启动高材生


image-20230420143742913


原来是八分之十,我vue烧了。


当浏览器窗口比设计稿大或者小的时候,就应该触发缩放,但是比例不一定,如果按照scale等比缩放时,宽度从1920缩小0.8倍也就是1536,而高度缩小0.8也就是743,如果此时浏览器高度过高,那么就会出现下方的白边,根据高材生所说的,缩小0.8后只需要放大八分之十就可以变回原大小,所以以现在的高度743*1.25=928,使宽度=928px就可以完全充满白边!


真的是这样吗?感觉哪里不对劲...


是浏览器高度!我忽略了浏览器高度,我可以直接使用浏览器高度乘以1.25然后再缩放达0.8!就是 1 !


也就是说 clientHeight / scale 就等于我们需要的高度!


我们用代码试一试


function keepFit(designWidth, designHeight, renderDom) {
 let clientHeight = document.documentElement.clientHeight;
 let clientWidth = document.documentElement.clientWidth;
 let scale = 1;
 if (clientWidth / clientHeight < designWidth / designHeight) {
   scale = (clientWidth / designWidth)
   document.querySelector(renderDom).style.height = `${clientHeight / scale}px`;
} else {
   scale = (clientHeight / designHeight)
   document.querySelector(renderDom).style.width = `${clientWidth / scale}px`;
}
 document.querySelector(renderDom).style.transform = `scale(${scale})`;
}

上面的代码可能看起来乱糟糟的,我来解释一下:


参数分别是:设计稿的宽高和你要适配的元素,在vue中可以直接传#app。


下面的if判断的是宽度固定还是高度固定,当屏幕宽高比小于设计宽高比时,


我们把高度写成 clientHeight / scale ,宽度也是同理。


最终效果


将这段代码放到App.vue的mounted运行一下


autofit


如上图所示:我们成功了,我们仅用了1 2 3 4....这么几行代码,就做到了足以媲美复杂写法的自适应!


我把这些东西封装了一个npm包:vue-autofit ,开箱即用,欢迎下载!


亲手打造集成工具:vue-autofit


这是一款可以使你的项目一键自适应的工具 github源码👉go



  • 从npm下载


npm i vue-autofit


  • 引入


import autofit from 'vue-autofit'


  • 快速开始


autofit.init()


默认参数为1920*929(即去掉浏览器头的1080), 直接在大屏启动时调用即可




  • 使用


export default {  
 mounted() {
 autofit.init({
       designHeight: 1080,
       designWidth: 1920,
       renderDom:"#app",
       resize: true
  })
},
}


以上使用的是默认参数,可根据实际情况调整,参数分别为


   * - renderDom(可选):渲染的dom,默认是 "#app",必须使用id选择器 
  * - designWidth(可选):设计稿的宽度,默认是 1920
  * - designHeight(可选):设计稿的高度,默认是 929 ,如果项目以全屏展示,则可以设置为1080
  * - resize(可选):是否监听resize事件,默认是 true


结语


诺克萨斯即将崛起


作者:德莱厄斯
来源:juejin.cn/post/7224015103481118757
收起阅读 »

上手 Vue 新的状态管理 Pinia,一篇文章就够了

web
Vuex 作为一个老牌 Vue 状态管理库,大家都很熟悉了 Pinia 是 Vue.js 团队成员专门为 Vue 开发的一个全新的状态管理库,并且已经被纳入官方 github 为什么有 Vuex 了还要再开发一个 Pinia ? 先来一张图,看下当时对于 Vu...
继续阅读 »

Vuex 作为一个老牌 Vue 状态管理库,大家都很熟悉了


Pinia 是 Vue.js 团队成员专门为 Vue 开发的一个全新的状态管理库,并且已经被纳入官方 github


为什么有 Vuex 了还要再开发一个 Pinia ?


先来一张图,看下当时对于 Vuex5 的提案,就是下一代 Vuex5 应该是什么样子的


微信图片_20220314212501.png


Pinia 就是完整的符合了他当时 Vuex5 提案所提到的功能点,所以可以说 Pinia 就是 Vuex5 也不为过,因为它的作者就是官方的开发人员,并且已经被官方接管了,只是目前 Vuex 和 Pinia 还是两个独立的仓库,以后可能会合并,也可能独立发展,只是官方肯定推荐的是 Pinia


因为在 Vue3 中使用 Vuex 的话需要使用 Vuex4,还只能作为一个过渡的选择,存在很大缺陷,所以在 Componsition API 诞生之后,也就设计了全新的状态管理 Pinia


Pinia 和 Vuex


VuexStateGettesMutations(同步)、Actions(异步)


PiniaStateGettesActions(同步异步都支持)


Vuex 当前最新版是 4.x



  • Vuex4 用于 Vue3

  • Vuex3 用于 Vue2


Pinia 当前最新版是 2.x



  • 即支持 Vue2 也支持 Vue3


就目前而言 Pinia 比 Vuex 好太多了,解决了 Vuex 的很多问题,所以笔者也非常建议直接使用 Pinia,尤其是 TypeScript 的项目


Pinia 核心特性



  • Pinia 没有 Mutations

  • Actions 支持同步和异步

  • 没有模块的嵌套结构

    • Pinia 通过设计提供扁平结构,就是说每个 store 都是互相独立的,谁也不属于谁,也就是扁平化了,更好的代码分割且没有命名空间。当然你也可以通过在一个模块中导入另一个模块来隐式嵌套 store,甚至可以拥有 store 的循环依赖关系



  • 更好的 TypeScript 支持

    • 不需要再创建自定义的复杂包装器来支持 TypeScript 所有内容都类型化,并且 API 的设计方式也尽可能的使用 TS 类型推断



  • 不需要注入、导入函数、调用它们,享受自动补全,让我们开发更加方便

  • 无需手动添加 store,它的模块默认情况下创建就自动注册的

  • Vue2 和 Vue3 都支持

    • 除了初始化安装和SSR配置之外,两者使用上的API都是相同的



  • 支持 Vue DevTools

    • 跟踪 actions, mutations 的时间线

    • 在使用了模块的组件中就可以观察到模块本身

    • 支持 time-travel 更容易调试

    • 在 Vue2 中 Pinia 会使用 Vuex 的所有接口,所以它俩不能一起使用

    • 但是针对 Vue3 的调试工具支持还不够完美,比如还没有 time-travel 功能



  • 模块热更新

    • 无需重新加载页面就可以修改模块

    • 热更新的时候会保持任何现有状态



  • 支持使用插件扩展 Pinia 功能

  • 支持服务端渲染


Pinia 使用


Vue3 + TypeScript 为例


安装


npm install pinia

main.ts 初始化配置


import { createPinia } from 'pinia'
createApp(App).use(createPinia()).mount('#app')

在 store 目录下创建一个 user.ts 为例,我们先定义并导出一个名为 user 的模块


import { defineStore } from 'pinia'
export const userStore = defineStore('user', {
state: () => {
return {
count: 1,
arr: []
}
},
getters: { ... },
actions: { ... }
})

defineStore 接收两个参数


第一个参数就是模块的名称,必须是唯一的,多个模块不能重名,Pinia 会把所有的模块都挂载到根容器上

第二个参数是一个对象,里面的选项和 Vuex 差不多



  • 其中 state 用来存储全局状态,它必须是箭头函数,为了在服务端渲染的时候避免交叉请求导致的数据状态污染所以只能是函数,而必须用箭头函数则为了更好的 TS 类型推导

  • getters 就是用来封装计算属性,它有缓存的功能

  • actions 就是用来封装业务逻辑,修改 state


访问 state


比如我们要在页面中访问 state 里的属性 count


由于 defineStore 会返回一个函数,所以要先调用拿到数据对象,然后就可以在模板中直接使用了


如下这样通过 store.xxx 使用,是具备响应式的


<template>
<div>{{ store.count }}</div>
</template>
<script lang="ts" setup>
import { userStore } from '../store'
const store = userStore()
// 解构
// const { count } = userStore()
</script>


比如像注释中的解构出来使用,也可以用,只是这样拿到的数据不是响应式的,如果要解构还保持响应式就要用到一个方法 storeToRefs(),示例如下


<template>
<div>{{ count }}</div>
</template>
<script lang="ts" setup>
import { storeToRefs } from 'pinia'
import { userStore } from '../store'
const { count } = storeToRefs(userStore())
</script>


原因就是 Pinia 其实是把 state 数据都做了 reactive 处理,和 Vue3 的 reactive 同理,解构出来的也不是响应式,所以需要再做 ref 响应式代理


getters


这个和 Vuex 的 getters 一样,也有缓存功能。如下在页面中多次使用,第一次会调用 getters,数据没有改变的情况下之后会读取缓存


<template>
<div>{{ myCount }}</div>
<div>{{ myCount }}</div>
<div>{{ myCount }}</div>
</template>

注意两种方法的区别,写在注释里了


getters: {
// 方法一,接收一个可选参数 state
myCount(state){
console.log('调用了') // 页面中使用了三次,这里只会执行一次,然后缓存起来了
return state.count + 1
},
// 方法二,不传参数,使用 this
// 但是必须指定函数返回值的类型,否则类型推导不出来
myCount(): number{
return this.count + 1
}
}

更新和 actions


更新 state 里的数据有四种方法,我们先看三种简单的更新,说明都写在注释里了


<template>
<div>{{ user_store.count }}</div>
<button @click="handleClick">按钮</button>
</template>
<script lang="ts" setup>
import { userStore } from '../store'
const user_store = userStore()
const handleClick = () => {
// 方法一
user_store.count++

// 方法二,需要修改多个数据,建议用 $patch 批量更新,传入一个对象
user_store.$patch({
count: user_store.count1++,
// arr: user_store.arr.push(1) // 错误
arr: [ ...user_store.arr, 1 ] // 可以,但是还得把整个数组都拿出来解构,就没必要
})

// 使用 $patch 性能更优,因为多个数据更新只会更新一次视图

// 方法三,还是$patch,传入函数,第一个参数就是 state
user_store.$patch( state => {
state.count++
state.arr.push(1)
})
}
</script>


第四种方法就是当逻辑比较多或者请求的时候,我们就可以封装到示例中 store/user.ts 里的 actions 里


可以传参数,也可以通过 this.xx 可以直接获取到 state 里的数据,需要注意的是不能用箭头函数定义 actions,不然就会绑定外部的 this 了


actions: {
changeState(num: number){ // 不能用箭头函数
this.count += num
}
}

调用


const handleClick = () => {
user_store.changeState(1)
}

支持 VueDevtools


打开开发者工具的 Vue Devtools 就会发现 Pinia,而且可以手动修改数据调试,非常方便


image.png


模拟调用接口


示例:


我们先定义示例接口 api/user.ts


// 接口数据类型
export interface userListType{
id: number
name: string
age: number
}
// 模拟请求接口返回的数据
const userList = [
{ id: 1, name: '张三', age: 18 },
{ id: 2, name: '李四', age: 19 },
]
// 封装模拟异步效果的定时器
async function wait(delay: number){
return new Promise((resolve) => setTimeout(resolve, delay))
}
// 接口
export const getUserList = async () => {
await wait(100) // 延迟100毫秒返回
return userList
}

然后在 store/user.ts 里的 actions 封装调用接口


import { defineStore } from 'pinia'
import { getUserList, userListType } from '../api/user'
export const userStore = defineStore('user', {
state: () => {
return {
// 用户列表
list: [] as userListType // 类型转换成 userListType
}
},
actions: {
async loadUserList(){
const list = await getUserList()
this.list = list
}
}
})

页面中调用 actions 发起请求


<template>
<ul>
<li v-for="item in user_store.list"> ... </li>
</ul>

</template>
<script lang="ts" setup>
import { userStore } from '../store'
const user_store = userStore()
user_store.loadUserList() // 加载所有数据
</script>


跨模块修改数据


在一个模块的 actions 里需要修改另一个模块的 state 数据


示例:比如在 chat 模块里修改 user 模块里某个用户的名称


// chat.ts
import { defineStore } from 'pinia'
import { userStore } from './user'
export const chatStore = defineStore('chat', {
actions: {
someMethod(userItem){
userItem.name = '新的名字'
const user_store = userStore()
user_store.updateUserName(userItem)
}
}
})

user 模块里


// user.ts
import { defineStore } from 'pinia'
export const userStore = defineStore('user', {
state: () => {
return {
list: []
}
},
actions: {
updateUserName(userItem){
const user = this.list.find(item => item.id === userItem.id)
if(user){
user.name = userItem.name
}
}
}
})

结语


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


更多前端文章,或者加入前端交流群,欢迎关注公众号【沐华说技术】,大家一起共同交流和进步呀





往期精彩


【保姆级】Vue3 开发文档


Vue3的8种和Vue2的12种组件通信,值得收藏


作者:沐华
来源:juejin.cn/post/7075491793642455077
收起阅读 »

🔥面试官想听的离职原因清单

大家好,我是沐华。今天聊一个面试的问题 由于面试官还要摸鱼刷沸点,不想花那么多时间一个个面,所以采用群面的方式,就出现了上图这样的场景 交锋 面试官:方便说下离职原因吗? 掘友1:不方便 掘友2:在前公司长期工作量有些太大了,我自己身体上也出现了一些信号,有段...
继续阅读 »

大家好,我是沐华。今天聊一个面试的问题


由于面试官还要摸鱼刷沸点,不想花那么多时间一个个面,所以采用群面的方式,就出现了上图这样的场景


交锋


面试官:方便说下离职原因吗?


掘友1:不方便


掘友2:在前公司长期工作量有些太大了,我自己身体上也出现了一些信号,有段时间都完全睡不着觉,所以需要切换一个相对来讲工作量符合我个人要求的,比如说周末可以双休这样一个情况,这个对我现在的选择来讲还蛮重要的


掘友3:本来已经定好的前端负责人(组长),被关系户顶掉了,我需要一个相对公平的竞争环境,所以打算换个公司


掘友4:实不相瞒,一年前我投过咱们公司(或者面试过但没过),一年了,你知道我这一年是怎么过的吗,因为当时几轮面试都很顺利的,结果却回复我说有更合适的人选了,我难受了很久,因为真的很想进入咱们公司,于是我奋发图强,每天熬夜学习到凌晨两点半,如今学有所成,所以又来了


掘友5:团队差不多解散了吧,领导层变动,没多久时间原团队基本都走了,相当于解散了吧,现在剩几个关系户,干的不开心


掘友6:公司要开发一些灰产(买马/赌球/时时彩之类的),老员工都不愿意搞,就都要我来做,我堂堂掘友6可是与赌毒不共戴天的人,怎么会干这种事(就是害怕坐牢),就辞职了(这是位入职时间不长的掘友)


掘友7:公司业务调整,然后突然让我去外地分公司驻场/让我去搞 flutter(原本是前后端),虽然是个好机会可还是很难受,而且与我的职业发展规划不符,所以不想浪费时间,就第一时间辞职了


掘友8:前东家对我挺好的,工作也得心应手(进入舒适圈了),只是我不想一直呆在舒适圈,我不是那种混日子的人,所以希望跳出来,找一份更有挑战性,更有成就感的工作,贵公司的岗位很符合我的预期


掘友9:公司最近经营不理想:1.不给交社保/公积金了,2.拖欠几个月工资了,好不容易攒的奶粉钱都花完啦(虽然还单身,可也是有想法的),为了生活,这不办法呀,3.公司倒闭了,现在十几个同事都在找工作,咱们这还需要前后端、产品、设计、测试吗,我可以内推


掘友10:您可能也知道现在各行各业行情都不太好,很多公司都裁撤了部分业务,前公司前几年疫情时就已是踏雪而行了,现在在新业务的选择上就决定裁撤掉原来的业务线,我也是其中一员,虽然很遗憾但也能接受吧,在前公司这两年也是学到了很多


掘友11:我其实挺感谢上家公司的,各方面都挺好的,也给了我很好的成长空间,但是也三年多时间了,我的薪资也没涨过,相信你也知道,其实我现在的薪资能够值得更好的,嗯被认可


掘友12:克扣工资,领导说以后生产环境上出现一次 bug 就要扣所有参与的人工资,说真的,每天加班加点的干,我们都没问题,可结果就被这样对待,被连带扣了几次之后心里真的很难受


掘友13:回老家发展咯/对象在这边咯,因为准备结婚了,之后一直在这边发展定居了(这种换城市的回答要给出准备结婚或定居发展这样的原因,不然谈个对象就换城市会显得不靠谱);如果是小城市换大城市,可以直接说是为了高薪来的,因为家里买房了生孩子了啥的经济压力大,顾家其实是能体现稳定的,也给砍价打个预防针


掘友14:(有断层,面试时间和上次离职时间相隔时间有点长,有两三个月左右的,如果真实情况是家里或者生病啥的直说就好,如果只是找了几个月工作没找到,就要组织下语言了),由于长时间加班的原因,身体受到了影响每天睡不好觉,那段时间一直不在状态,没法好好投入工作,就想休息一段时间,为避免给公司造成不好的影响,所以辞职了。当时领导坚持批我几天假,我自己也不知道具体多久能恢复过来,毕竟那种种状态也不是一天两天了,还是坚持让领导批我的辞职了,然后这段时间我去了哪哪哪,身体已经调整过来了,可以全身心投入工作了,不过现在找工作希望是周末可以双休这样一个情况,这个对我现在的选择来讲还蛮重要的


(如果断层有一年的左右的,我有一段经历可以给大家参考下)我当时没工作了,家里投了点钱让我和一个亲戚合伙搞了点生意,结果赚了点钱,但那个亲戚喜欢赌钱,被他拿去赌了,输光了,于是我撤出来了


沐华:就是觉得翅膀硬了,想出去看看(其实这是我入职现公司面试时说的离职原因,当时面试官听着就笑了)


第一轮回答结束!





心法


离职原因真实情况绝大多数情况无非就几种:钱少了,不开心了,被裁了。


大家都差不多的,面试官心里也知道,可这能直说吗?


直说也不是不行,但是要注意表达方式,回答时有些场面话/润色一下还是需要的,去掉负面的字句,目的是让人家听的人舒服一点而已,毕竟谁也不喜欢一个陌生人来向自己诉苦抱怨,发牢骚吧,谁都希望认识正能量/积极向上的人吧


所以回答的关键在于:



  1. 不能是自己的能力、懒惰、不稳定等原因,或可能影响团队的性格缺陷

  2. 不要和现任说前任的不好,除非客观原因没办法,但也要知道即便是前公司真实存在的问题,hr 并不了解真实情况,还是会对是前公司有问题,还是你有问题持怀疑态度的


就像分手原因,对别人说出来时不能显得自己很绝情,又不能让自己很跌份,而且很忌讳疯狂抹黑前任


公司想降低用人风险,看我们离职的原因在这里会不会再发生,所以我们回答中应该体现:稳定性、有想法、积极向上、找工作看重什么、想得到什么、有规划不是盲目找的....


忌讳:升职机会渺茫、个人发展遇到瓶颈、人际关系复杂受人排挤、勾心斗角氛围差...这样的回答会让人质疑是前公司的问题,还是你的能力/情商有问题?


那么,你觉得最好的答案是什么呢,如果你是面试官,会选谁进入下一轮?


同时期待掘友们在评论区补充哦


往期精彩


【保姆级】Vue3 开发文档


TS 泛型进阶


深入浅出虚拟 DOM 和 Diff 算法源码,及 Vue2 与 Vue3 中的区别


上手 Vue 新的状态管理 Pinia,一篇文章就够了


作者:沐华
来源:juejin.cn/post/7225432788044267575
收起阅读 »

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

刚毕业时,我为了赢得面试官的好感,说了很多违心话,如:“工资不要紧,主要是想学习”,又或者是“我对贵司的这块技术非常感兴趣”。 现在想想,呸!恶心,哪怕是花钱嫖培训呢,也不要再傻乎乎的说出“为块术”这种违心的话了。 那时候年轻,不知道起薪高的好处,现在被各种...
继续阅读 »

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


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


图1:为块术.png


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


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


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



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

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

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


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


Tips



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

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


应届生薪资指南


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


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


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


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


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


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


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


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



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

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


Tips



互联网的天花板


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


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


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


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


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


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


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


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


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


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


上海地区研发岗位薪酬


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


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


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


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


结语


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


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




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


附录


各行业应届生薪资数据


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


电子商务年收入成长曲线


年固定收入成长曲线:


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


年总收入成长曲线:


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


企业软件年收入成长曲线


年固定收入成长曲线:


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


年总收入成长曲线:


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


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

微信发送语音自定义view中的事件分发

这里通过一个自定义view的例子来看事件分发在自定义view中的使用,其实大部分的Android框架下的事件分发的也都差不多的样子,抛砖引玉,我自己做一个记录,如果能帮到有需要的人那就更上一层楼。 先来看一个微信发送语音的效果图: 关于事件分发我们其实耳熟能...
继续阅读 »

这里通过一个自定义view的例子来看事件分发在自定义view中的使用,其实大部分的Android框架下的事件分发的也都差不多的样子,抛砖引玉,我自己做一个记录,如果能帮到有需要的人那就更上一层楼。

先来看一个微信发送语音的效果图:


关于事件分发我们其实耳熟能详,可以通过一段非常有名的伪代码来大致了解:


  @Override
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false;
if(onInterceptTouchEvent(ev)){
consume = onTouchEvent(ev);
}else {
consume = child.dispatchTouchEvent(ev);
}
return consume;

}

事件都是从一个DOWN开始,中间经过一堆MOVE,到一个UP结束(先抛开CANCEL的情况)。

事件流向画了半天,感觉也没有人家画的好,可以参考 这里 ,很清晰,忘记了的或者细节不清楚模糊了的可以移步去复习一下。


我们的需求是点击发送按钮后显示浮层view,相当于事件先由发送按钮处理,等浮层view显示后再交由浮层view处理,这个事件的流向很清晰,那应该怎么做呢。


那最简单的view的层级结构就是发送按钮浮层view处在同一层级,那一个问题,事件能否在parent什么都不做的情况下实现事件在同级别view之间的转移呢?

肯定是不可以或者说没有必要的,最好的方式还是通过parent来做分发,由parent的决定此时到底是需要把事件交给发送按钮还是浮层view


所以层级结构上:


<?xml version="1.0" encoding="utf-8"?>
<!-- parent, 来控制事件的分发 -->
<com.yocn.af.view.widget.WeChatParentViewGroup
android:id="@+id/wechat_root"
android:layout_width="match_parent"
android:layout_height="match_parent">

<LinearLayout
android:id="@+id/ll_option"
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_alignParentBottom="true">

<!-- 发送按钮view -->
<com.yocn.af.view.widget.WeChatVoiceTextView
android:id="@+id/tv_voice"
android:layout_width="0dp"
android:layout_height="match_parent"
android:gravity="center"
android:text="按住 说话"/>
</LinearLayout>
<!-- 点击后需要显示的浮层view -->
<com.yocn.af.view.widget.WeChatVoiceView
android:id="@+id/voice_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/half"
android:visibility="gone" />

</com.yocn.af.view.widget.WeChatParentViewGroup>

我们梳理一下思路,需要做的就是:



  1. 什么都没做时ParentViewGrouponInterceptTouchEvent要返回false,使得事件能顺利的从ParentViewGroup传递到VoiceTextView(发送按钮)

  2. 点击到VoiceTextView(发送按钮)时,发送按钮的dispatchTouchEvent返回true,处理DOWN事件并告诉parent需要显示WeChatVoiceView(浮层view)

  3. parent接收到需要显示浮层view的命令,显示浮层view并且onInterceptTouchEvent返回true,表示事件我parent来处理,这时VoiceTextView(发送按钮)会收到一个CANCEL事件并且不会继续接受MOVE事件。

  4. parent来分发事件,在WeChatVoiceView(浮层view)显示出来之后直接将后续的MOVE事件交给WeChatVoiceView(浮层view)处理,当然浮层view的onInterceptTouchEvent需要返回true,会回调到浮层view的onTouchEvent,直接做对应的动画或者手势操作。

  5. 当然不要忘记在parent收到ACTION_UP的时候将浮层view置为不可见,因为事件是由parent分发给浮层view的,当然parent可以一直拿到事件。


至此,整个事件分发的流程就结束了。
附上代码地址WeChatSendVoice


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

Android项目cicd流程总结(使用jenkins)

没有cicd之前我们都是怎么做的 相信做安卓开发的都做过这些事 手动运行单元测试,根据报错改代码 检查代码风格,根据报错改代码 构建apk包,发给测试,有时候还得打很多个 接收测试的反馈,改bug, 重复之前的步骤 把apk放到ftp或者其他地方去发布 是...
继续阅读 »

没有cicd之前我们都是怎么做的


相信做安卓开发的都做过这些事



  1. 手动运行单元测试,根据报错改代码

  2. 检查代码风格,根据报错改代码

  3. 构建apk包,发给测试,有时候还得打很多个

  4. 接收测试的反馈,改bug,

  5. 重复之前的步骤

  6. 把apk放到ftp或者其他地方去发布


是不是想到这一套流程,头都大了,虽然每一步都不难,但是连起来都手工操作就很繁琐


像这些手动流程固定的事,我们完全就可以交给机器来做,让我们有更多时间做点别的事,没错,这就是今天要说的cicd


什么是cicd,以及在cicd过程中包含了哪些步骤


一般来说,安卓开发的CI/CD流程包括以下几个阶段:代码提交、代码检查、编译构建、单元测试集成测试、部署发布、用户反馈。



  1. 在代码提交阶段,开发者将自己的代码推送到远程仓库,例如Git或SVN,并触发CI/CD工具或平台例如Jenkins或Travis CI等

  2. 在代码检查阶段,CI/CD工具或平台会对代码进行静态分析和风格检查,例如使用SonarQube或Checkstyle等。

  3. 在编译构建阶段,CI/CD工具或平台会使用Gradle或Maven等工具对代码进行编译和打包,生成APK文件

  4. 在单元测试阶段,CI/CD工具或平台会使用JUnit或Espresso等框架对代码进行单元测试,并生成测试报告。

  5. 在集成测试阶段,CI/CD工具或平台会使用Appium或Selenium等框架对应用进行集成测试,并生成测试报告

  6. 在部署发布阶段,CI/CD工具或平台会将APK文件上传到内部服务器或外部平台,例如蒲公英或Google Play等,并通知相关人员。

  7. 在用户反馈阶段,开发者可以通过Bugly或Firebase等工具收集用户的反馈和错误信息,并根据需要进行修复和更新


通过以上几个步骤,我们可以把以前的app构建流程从手动变为自动,而且可以通过不断以非常低的成本的重复这个过程,提高我们的项目质量,这就是cicd带给我们的自信


今天我们来通过jenkins来实现上面的几个步骤


安装配置jenkins


本文讨论的主要是在windows环境下安装jenkins



  1. 从jenkins官网下载对应的安装包即可

  2. 安装过程很简单但是需要提供一个账号,就像下图显示的界面,这个账号需要有权限
    图片.png
    打开开始菜单,搜索本地安全策略,选择本地策略用户权限分配,在右侧的策略中找到作为服务登录,双击打开。点击添加用户或组,在输入框中填入你的账户的名字,单击检查名称,如果加上了下划线,则说明没有问题,如果输入的用户不存在,则会跳出来一个找不到名称的对话框。
    图片.png


这里需要注意一点,windows家庭版默认是没有本地安全策略的,需要用一些技巧把它开启,如下:


1.  在桌面上单击右键,选择“新建”->“文本文档”。

2. 将文本文档重命名为“OpenLocalSecurityPolicy.bat”。

3. 右键单击“OpenLocalSecurityPolicy.bat”,选择“编辑”。

4. 将以下命令复制并粘贴到文本编辑器中:


@echo off
pushd "%SystemRoot%\system32"
findstr /c:"[SR] Cannot repair member file" %windir%\logs\cbs\cbs.log >%userprofile%\Desktop\sfcdetails.txt
start ms-settings:windowsdefender
start ms-settings:windowsupdate
start ms-settings:windowsupdate-history
start ms-settings:windowsupdate-options
start ms-settings:storagesense
start ms-settings:storagesense-diagnostics
start ms-settings:storagesense-configurecleanup
start ms-settings:storagesense-changehowwesave
start ms-settings:storagesense-runstoragecleanupnow
start ms-settings:storagesense-storageusage
start ms-settings:storagesense-changestoragesavelocations
start ms-settings:backup
start ms-settings:backup-advancedsettings
start ms-settings:backup-addalocaldriveornetworklocation
start ms-settings:backup-managebackups
start ms-settings:backup-moreoptions
start ms-settings:dateandtime
start ms-settings:regionlanguage
start ms-settings:regionlanguage-languagepacks
start ms-settings:regionlanguage-speech
start ms-settings:regionlanguage-keyboards
start ms-settings:regionlanguage-morespeechservicesonline
start ms-settings:speech
start ms-settings:speech-microphoneprivacysettings


5. 保存并关闭文本编辑器。
6. 双击“OpenLocalSecurityPolicy.bat”文件,以打开本地安全策略。
复制代码

3. 修改默认根地址到其他盘符


默认情况下,jenkins的主目录都是在c盘,如果这样,我们使用中产生的数据都是在c盘,用过windows的都知道,数据放在c盘是很危险也是很让人不爽的一件事,我们可以通过修改jenkins的主目录方法来把数据放到其他盘


在成功安装了jenkins并解锁之后,我们可以配置环境变量JENKINS_HOME,地址就是我们想改的目录,


图片.png
然后修改jenkins.xml


    <env name="JENKINS_HOME" value="%LocalAppData%\Jenkins.jenkins"/>
复制代码

改为


    <env name="JENKINS_HOME" value="E:\jenkins"/>
复制代码


  1. 配置常用的插件
    在第一次启动jenkins的时候,会让你选择安装哪些插件,这时候直接选择推荐的插件就好,包含了一些常用插件,比如git等等,如下图


图片.png


配置针对于android的环境



  1. android sdk 见下图
    图片.png

  2. gradle -- Dashboard -> Manage Jenkins -> Global Tool Configuration中的Gradle配置gradle路径即可

  3. jdk -- Dashboard -> Manage Jenkins -> Global Tool Configuration中的JDK配置jdk路径即可

  4. git -- Dashboard -> Manage Jenkins -> Global Tool Configuration中的Git installations配置git路径即可


配置Android的具体job信息


新建一个freestyle的item,在里面做以下几步:




  1. 配置git仓库地址以及构建分支




  2. 设置构建触发器(定时构建) -- 找到构建触发器,勾选build periodically,在编辑框里按照规则设置构建时间,在某天的某个时段自动构建,比如45 9-15/2 * * 1-5,虽然可以提交一次就构建一次,但是不建议这么做。构建表达式的规则见下图,可以根据自己的需要写表达式。
    图片.png




  3. 添加构建步骤,打包出apk,如下图在build step中触发
    图片.png




  4. 配置构建后步骤,自动把apk包上传到ftp或者其他地方


    在Jenkins的项目中,选择“构建”->“增加构建步骤”->“执行shell”或“执行Windows批处理命令”
    一个上传ftp的例子




ftp -n <<EOF
open http://ftp.example.com
user username password
cd /remote/directory
put /local/file
bye
EOF
复制代码

配置邮件通知


在构建完成之后,特别是失败的时候,我们希望收到一封邮件告诉我们构建失败了,快去处理,我们可以通过以下步骤来实现



  1. 在Jenkins中安装Email Extension Plugin插件,可以在插件管理中搜索并安装。

  2. 在Jenkins的系统管理中,配置邮件服务器的地址,用户名,密码和端口。如果使用的是QQ邮箱或者163邮箱,还需要获取邮箱授权码并使用授权码作为密码。

  3. 在Jenkins的项目中,选择“构建后操作”->“增加构建后操作”->“Editable Email Notification”。

  4. 在邮件通知的配置中,填写收件人,抄送人,邮件主题,邮件内容等信息。可以使用一些变量来自定义邮件内容,例如BUILDSTATUS表示构建状态,BUILDSTATUS表示构建状态, {BUILD_URL}表示构建链接等。


这里特别要注意的是,上面的配置地址和授权码需要在job的设置里面进行,在全局配置有可能发不出邮件


配置单元测试和代码检查


我们还需要在运行前执行代码lint检查和单元测试,也需要配插件,插件名字是JUnit和Warnings Next Generation



  1. 参考上面 配置Android的具体job信息 中的配置,添加lint和单元测试的任务

  2. 配置单元测试插件和lint插件,主要指定报告文件的位置,见下图


图片.png


图片.png
3. 把单元测试的结果放到邮件的附件中去,配置见下图,也可以放些别的东西


图片.png


一劳永逸,使用docker把上面的配置做到随时随地使用


上面的步骤完成之后,我们就能自动构建,上传apk什么的了,但是每次换台机器我们都得再配一次,想下就很累,这时候我们就可以用docker,创建一个容器,把上面这些操作放在容器里面,在新环境里面拉个镜像,创建容器跑起来,就ok啦,关于怎么用docker,就需要大家自己去搜索学习了


最后放张图吧,jenkins真好用啊


封面.png


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

模拟点击与群控——autojs使用

写在前面 autojs是利用accessibility功能实现的一套免root自动化模拟点击框架,让开发者可以直接通过使用js脚本实现一系列的自动化操作,包括:触摸屏幕、滑动、输入文字等。autojs具有很高的灵活性和可扩展性,可以被用于各种场景,例如自动化游...
继续阅读 »

写在前面


autojs是利用accessibility功能实现的一套免root自动化模拟点击框架,让开发者可以直接通过使用js脚本实现一系列的自动化操作,包括:触摸屏幕、滑动、输入文字等。autojs具有很高的灵活性和可扩展性,可以被用于各种场景,例如自动化游戏操作、自动登录、自动化测试等。


autojs免费版本已经停止维护,官网只有Autojs Pro付费版本。然而最近(20230214)又因为一些合规问题,被强制下架已经不允许用户注册。市面上可使用代替产品autox,该项目基于原autojs4.1版本基础上进行维护开发。本文主要围绕该项目展开。


开发环境搭建


使用vscode,添加插件Auto.js-Autox.js-VSCodeExt,如下图,注意不要重复安装多个类似插件,会存在冲突问题。




按住cmd+shift+p,输入> Auto.js,选择"开启服务(Start Server)",此时右下角会提示服务正在运行提示,并显示ip地址信息,如下图,ip为:192.168.1.102




手机与电脑连接同个wifi,并打开autox app,打开"无障碍服务",并打开"连接电脑"按钮,输入ip地址,如下图,点击确认,即可与电脑同步。



连接成功后出现如下提示,此时开发环境搭建完成。



测试hello world程序,创建Autox.js文件,并输入内容toast("hello world!"),选择js文件,右键-重新运行,即可将脚本同步到手机运行,此时手机会出现hello world!的一个toast提示。



js脚本开发指导


关于autojs的API可参考官方文档,这里主要是讲解一下使用的思路。我们在开发自动化工具时,最常见的问题就是如何找到我们所需要点击的控件节点,每一个节点包含的信息包括:



  • className 类名。类名表示一个控件的类型,例如文本控件为"android.widget.TextView",图片控件为"android.widget.ImageView"等。

  • id控件节点的唯一id。

  • text节点名字,不一定有,可能为空。

  • desc节点的描述信息,不一定有,可能为空。

  • packageName 包名。包名表示控件所在的应用包名,例如 QQ 界面的控件的包名为"com.tencent.mobileqq"。

  • bounds 控件在屏幕上的范围。

  • drawingOrder 控件在父控件的绘制顺序。

  • indexInParent 控件在父控件的位置。

  • clickable 控件是否可点击。

  • longClickable 控件是否可长按。

  • checkable 控件是否可勾选。

  • checked 控件是否可以勾选。

  • scrollable 控件是否可滑动。

  • selected 控件是否已选择。

  • editable 控件是否可编辑。

  • visibleToUser 控件是否可见。

  • enabled 控件是否已启用。

  • depth 控件的布局深度。


控件id是最为常用的一个唯一性标记,我们写自动化认为时,经常使用id来对特定控件做点击操作。但是我们如何得知具体控件id信息呢?我们可以利用以下js脚本,将整个界面的控件信息进行打印输出。


toastLog("start.");

function printNode(node){
if(node){
var text = node.text();
var desc = node.desc();
let bounds = node.bounds();
let left = bounds.left;
let top = bounds.top;
let right = bounds.right;
let bottom = bounds.bottom;
var click = node.clickable();
var id = node.id();
log(id, text, desc, click, left, right, top, bottom);
}
}

function traverse(node) {
printNode(node);
var cnt = node.childCount();
for (var i = 0; i < cnt; i++) {
traverse(node.child(i));
}
}

let windowRoot = auto.rootInActiveWindow;
if(windowRoot){
log("tracerse node.");
traverse(windowRoot);
}else{
log("window root is null.");
}

我们可以结合node.bounds()中控件的大小以及所在位置,来猜测我们所要点击的目标控件。在获得某个具体控件id后,即可使用如下js脚本进行点击操作。


target_id=""
id(target_id).findOne().click()

查看viewid脚本开发


这一节我们将利用canvas绘图将每个控件绘制出来,让我们方便地看出来我们所要操作的控件viewid。首先我们需要利用递归方式遍历当前页面上的所有控件,并存放在list变量中,如下。


function traverse(node) {
if(node != null){
viewNodes.push(node);
}
var cnt = node.childCount();
for (var i = 0; i < cnt; i++) {
traverse(node.child(i));
}
}

//x:946, y:80
let windowRoot = auto.rootInActiveWindow;

if(windowRoot){
log("tracerse node.");
traverse(windowRoot); // 开始遍历控件树并打印控件的text属性
}else{
log("window root is null.");
}

function printNode(i, node){
if(node){
var text = node.text();
var desc = node.desc();
let bounds = node.bounds();
let left = bounds.left;
let top = bounds.top;
let right = bounds.right;
let bottom = bounds.bottom;
var click = node.clickable();
var id = node.id();
log(i, id, text, desc, click, left, right, top, bottom);
}
}
var len = viewNodes.length;
for (var i = 0; i < len; i++) {
let childViewNode = viewNodes[i];
printNode(i, childViewNode);
}

使用浮窗功能,在顶层绘制一张透明的画布,如下:


//ui布局为一块画布
var w = floaty.rawWindow(
<frame gravity="center" bg="#ffffff">
<canvas id="canvas" layout_weight="1"/>
</frame>
);

w.setSize(device.width, device.height); // 设置窗口大小
w.setTouchable(false); // 设置触摸透传

使用canvas绘图库,用绿色边框将各个控件圈出,并在每个控件上显示在list中对应的序号。


let paint = new Paint();
paint.setColor(colors.parseColor("#00ff00"));
paint.setStrokeWidth(5);
paint.setStyle(Paint.Style.STROKE);

let paintText = new Paint();
paintText.setColor(colors.parseColor("#FF0000"));
paintText.setTextSize(80);
paintText.setStrokeWidth(20);

var isDraw = 1;
w.canvas.on("draw", function (canvas) {
if(isDraw < 20){
isDraw = isDraw + 1;
var len = viewNodes.length;
for (var i = 0; i < len; i++) {
let childViewNode = viewNodes[i];
let bounds = childViewNode.bounds();
let left = bounds.left;
let top = bounds.top;
let right = bounds.right;
let bottom = bounds.bottom;
canvas.drawRect(left, top, right, bottom, paint);
// log(left, bottom, right, top)
canvas.drawText("" + i, left, bottom, paintText);
}
}
});

为了不让脚本退出,我们需要使用设置等待时间,让脚本持续运行,如下,若没有等待执行,脚本执行后立马退出,我们将无法看到绘图内容。


setTimeout(()=>{
w.close();
}, 50000);

效果图如下:



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

Android通过BLE传输文件

1、遇到的问题 公司要通过Android设备给外围设备的固件进行OTA升级,最开始想到的有两种方案。 1、将当前Android设备所连接 Wifi名称,WiFi密码通过BLE发送给外围设备。 外围设备拿到当前环境的WiFi名称和密码连接热点, 然后自己去服务器...
继续阅读 »

1、遇到的问题


公司要通过Android设备给外围设备的固件进行OTA升级,最开始想到的有两种方案。


1、将当前Android设备所连接 Wifi名称,WiFi密码通过BLE发送给外围设备。 外围设备拿到当前环境的WiFi名称和密码连接热点, 然后自己去服务器下载OTA文件,进行升级

2、当前Android设备和外围设备通过经典蓝牙进行传输OTA文件, 外围设备拿到OTA文件进行升级

但是很遗憾,外围设备既没有WiFi芯片, 也没有经典蓝牙芯片, 只有一颗BLE(低功耗蓝牙)芯片。 这意味着上面的两种方案都行不通。 那我们能不能通过BLE芯片来做文章, 来传输OTA文件?


BLE设计之初就是为了传输简单的指令的, 传输一些小数据的, 每次发送的数据大小不能超过20个字节。到底靠不靠谱啊?


2、 能不能通过BLE传输文件


让我们来问问 GPT 吧


p9uZOaR.png


GPT 的回答, 是可以通过BLE传输文件的, 由于BLE 每次传输的内容最大为20个字节, 传输大文件时就需要分包传输,
同时需要确保分包传输的可靠性和稳定性。


3、 如何传输文件


让 GPT 给我们一些示例代码


p9uekdA.png


可以看出, 发送端分包批量发送数据,接收端


4、如何保证可靠性和稳定性


p9K6UHO.png


1、超时重传


蓝牙在传输过程中, 可能会存在丢包的情况。分两种情况,
1、Android设备发送的数据,外设设备没有收到。
2、Android设备发送的数据,外设设备收到了,并且发送了回复确认。 回复确认包Android设备却没有收到。


出现了这两种情况的任意一种, 则认为发生了丢包的情况。 Android 对这个包进行重发。


2、序列号


针对超时重传的第二种情况, 外设设备会收到两个相同的包。 但是外设设备不清楚是不是重装包。 这时就要给每个数据包添加序列号。 等外设设备收到两个相同序列号的数据包时, 丢弃这个数据包, 回复Android设备收到此包, 开始发送下一个数据包。


3、数据校验


BLE在传输的过程中, 如果周围环境有强蓝牙干扰,或者其他传输通道, 可能会导致数据包变更, 所以需要在数据包添加一个校验位, 这个校验位根据特定的算法,由数据包的数据计算得来。 外设设备收到数据后, 重新计算校验位, 判断数据传输过程是否出现差错, 如果计算的校验位和包传输的校验位不一致, 则要求Android设备重新发送这个包。


5、 传输速度提升 RequestMtu


为了保证传输过程中的可靠性和稳定性,我们需要在传输包中,添加序列号,数据校验等信息。 Android默认每个BLE数据包不超过20个字节,当我们加了一些其他信息时, 每次传输的有效数据可能只有15个字节左右。 导致在传输的过程中分包更多, 传输时间更长。


为了提升传输的速度, 我们来提升BLE每个数据包的传输大小限制, 使每个分包可以传输更多的数据。 系统为我们提供了 RequestMtu这个接口。 需要在gatt连接成功时调用


    private val bluetoothGattCallback = object : BluetoothGattCallback() {

override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) {
super.onConnectionStateChange(gatt, status, newState)

if (newState == BluetoothGatt.STATE_CONNECTED) {
Log.d(TAG, "gatt 连接成功")
gatt?.requestMtu(40)
} else {
Log.d(TAG, "gatt 连接失败 status $status newstate $newState")
}

}


override fun onMtuChanged(gatt: BluetoothGatt?, mtu: Int, status: Int) {
super.onMtuChanged(gatt, mtu, status)

if (BluetoothGatt.GATT_SUCCESS == status) {
Log.d(TAG, "onMtuChanged suc : $mtu")
gatt?.discoverServices()
} else {
Log.d(TAG, "onMtuChanged fail : $status")
}
}
}

MTU改变成功后, 再去gatt.discoverServices()发现服务


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

【Android】Kotlin 中的 apply、let、also、run 到底有啥区别?

前言 Kotlin 作为一门现代的编程语言,为了提高代码的可读性和简洁性,引入了许多扩展函数。本文将详细介绍 Kotlin 中的四个常用扩展函数:apply、let、also、run,并比较它们的联系与区别。 apply apply 函数接收一个对象并返回该对...
继续阅读 »

前言


Kotlin 作为一门现代的编程语言,为了提高代码的可读性和简洁性,引入了许多扩展函数。本文将详细介绍 Kotlin 中的四个常用扩展函数:applyletalsorun,并比较它们的联系与区别。


apply


apply 函数接收一个对象并返回该对象本身。它允许您在对象上执行一些操作,同时仍然返回原始对象。


这个函数的语法为:


fun <T> T.apply(block: T.() -> Unit): T

其中,T 是对象的类型,block 是一个 lambda 表达式,可以在该对象上执行一些操作。在这个 lambda 中,您可以使用 this 关键字来引用当前对象。


例如:


val person = Person().apply {
name = "John"
age = 30
address = "New York"
}

在这个例子中,我们创建了一个 Person 对象并在 apply 函数中设置了该对象的属性。最终返回的是这个 Person 对象本身。


let


let 同样是一个扩展函数,它也接收一个 Lambda 表达式作为参数。不同于 apply,在 Lambda 表达式中,let 函数将接收者对象作为 Lambda 的参数,通常用 it 作为隐式名称。let 函数的返回值是 Lambda 表达式的结果。


这个函数的语法为:


kotlinCopy code
fun <T, R> T.let(block: (T) -> R): R

其中,T 是对象的类型,R 是返回值的类型,block 是一个 lambda 表达式,它对该对象进行一些操作并返回一个结果。


val person = Person("Alice", 25)
val ageAfterFiveYears = person.let {
it.age + 5
}

在这个示例中,let 函数用于计算 Person 对象五年后的年龄。


also


also 是一个扩展函数,与 apply 类似,但有一个关键区别:also 函数的返回值是接收者对象本身,而 Lambda 表达式的参数是接收者对象,通常用 it 作为隐式名称。
这个函数的语法为:


fun <T> T.also(block: (T) -> Unit): T

其中,T 是对象的类型,block 是一个 lambda 表达式,可以在该对象上执行一些操作。在这个 lambda 中,您可以使用 it 关键字来引用当前对象。


val person = Person("Alice", 25).also {
it.name = "Bob"
it.age = 30
}

在上述示例中,also 函数用于修改 Person 类的属性,最后返回修改后的对象。


run


run 是一个扩展函数,它结合了 applylet 的特点。run 函数在 Lambda 表达式中直接访问接收者对象的属性和方法,同时返回 Lambda 表达式的结果。


这个函数的语法为:


fun <T, R> T.run(block: T.() -> R): R

其中,T 是对象的类型,R 是返回值的类型,block 是一个 lambda 表达式,它对该对象进行一些操作并返回一个结果。在这个 lambda 中,您可以使用 this 关键字来引用当前对象。


val person = Person("Alice", 25)
val greeting = person.run {
"Hello, $name! You are $age years old."
}

在这个示例中,run 函数用于生成一个包含 Person 对象信息的字符串。


总结


四个函数的相同点是,它们都可以操作对象,并可以在 lambda 中引用当前对象。但是,它们的返回值和返回时机有所不同。


apply 和 also 函数的返回值是该对象本身,而 let 和 run 函数的返回值是 lambda 表达式的结果。


apply 函数在对象上执行一些操作,并返回该对象本身。它通常用于在对象创建后立即对其进行初始化。


also 函数类似于 apply 函数,但它返回原始对象的引用。它通常用于对对象进行一些副作用,例如打印日志或修改对象状态。


let 函数在 lambda 中对对象进行一些操作,并返回 lambda 表达式的结果。它通常用于在某些条件下对对象进行转换或计算。


run 函数类似于 let 函数,但它返回 lambda 表达式的结果。它通常用于对对象进行计算,并返回计算结果。


总之,这四个函数都是非常有用的函数式编程工具,可以帮助您以简洁、可读性强的方式操作对象和代码块。对于每个情况,您应该选择最合适的函数,以便以最有效的方式编写代码。


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

我的30岁,难且正确的事情是什么?

3月意料之中的最后裁员到来了,在充分了解个人意愿后留下两个不想看新工作的小伙伴,IOS Android各一个。我把自己也写进了名单,跟其他能力相对强一点的一起出来了。回顾过去2年我们做的事情,我对大家还是蛮有信心的。同时基于对《钱》这本书的学习,我从前两年开始...
继续阅读 »

3月意料之中的最后裁员到来了,在充分了解个人意愿后留下两个不想看新工作的小伙伴,IOS Android各一个。我把自己也写进了名单,跟其他能力相对强一点的一起出来了。回顾过去2年我们做的事情,我对大家还是蛮有信心的。同时基于对《钱》这本书的学习,我从前两年开始就一直留有1年以上的备用金,所以暂时也没太大经济压力,不至于因为囊中羞涩着急找一份谋生的工作。


刚离开公司的前两周,先花了1000多找了两个职业咨询师,了解目前的招聘环境,招聘平台,招聘数据,以及针对性的帮助我修改简历。都准备好以后,开始选公司试投简历,认真看完大部分JD后大概清楚自己的能力所匹配的公司,薪资范围。机会确实不多,移动端管理岗位,架构岗位就更少,尤其是像我这样工作不到10年,架构跟管理经验都还未满5年的人,选择更是寥寥无几。


先后参加了两个2面试,一个是小团队的移动 TL,在了解后双边意向都不大。另一个是 Android 架构方向。虽然拿了offer,薪资包平移,但我最终没去。一是发生了一点小误会,发offer前电话没告诉我职级,我以为架构岗过了其实没有,差一点点到P7。回看面试记录,提升并不困难,有能力冲一冲的,这一次并不会影响我的信心。


另一个则是我真的冷静下来了,也就有了这篇文章。


在这两周里,陆续写了一些文章,做了一些开源项目。完全是出于助人为乐回馈社区,没想到也因此结识了几个做阅读业务的同学,纷纷向我抛来橄榄枝。其中包含一个已经在行业内做到Top3的产品。这让我有些受宠若惊,毕竟我觉得我给的技术方案并非有很大门槛,只是运气好站在巨人的肩膀上想到了不同的方案而已。


正是这些非常正面的反馈,帮助我消化了很大一部分所谓的焦虑(难说我本身一点不受环境影响)。在Zhuang的帮助下,我大概做了两次自我梳理,最后在前几天我从地铁回家大概3km的步行中想明白了很多事情。


每次出去旅游时,比如我躺在草原上,看着日落,说实话我欣赏不了10分钟。因为我的思绪停不下来,我就会去想一些产品或者是管理方面的问题。我很爱工作,或者说喜欢工作时可以反复获取创造性的快乐,比如做出一个新的技术方案或者优化工作流程解决一个团队问题,都让人很兴奋。但现在,我想强迫自己来思考一些更长期的事情。


我的30岁,难而且正确的事情是什么?


是找一份工作吗?这显然不难,作为技术人,找一份薪资合理的工作不难,这恰恰是最容易的


是找一份自己喜欢的工作吗?这有一点难,更多的是需要运气。职业生涯就十几年,有几次选择的机会呢?这更多的是在合理化自己对稳定性,舒适性的追求,掩盖自己对风险的逃避。


是找一个自己喜欢的事情,并以此谋生吗?这很难,比如先找到自己长期喜欢长期坚持投入的事情就很难,再以此谋生就需要更多的运气与常年积累去等待这个运气出现,比如一些up主。这可以是顺其自然的理想,但不适合作为目标。


上面似乎都是一个个的问题,或者说看到这些问题的时候我就看到了自己的天花板了。因为我可以预见我在这些方向上的学习能力,积累速度,成长空间,资源储备。


这半年涌出了太多的新事物,像极了02年前后的互联网,14前后的移动互联网。我从去年12月5日开始使用GPT,帮助我提高工作,学习效率,帮助我做UI设计,帮助我改善代码,甚至帮助我学习开网店时做选品,做策略,可以说他已经完全融入我的工作学习了。


开发自己的GPT应用要仔细阅读OPEN AI 的API,我再次因为英语的理解速度过慢严重影响学习效率,即使是有众多实时翻译软件帮助下丝毫不会有所改善。


翻译必然会对原文做二次加工,翻译的质量也许很高,甚至超过原文,但这样意味着阅读者离原文越远。


比如我在Tandem上教老外“冰雪聪明”这个词的意思,我很难解释给她,更多的是告诉她这个词在什么场景用比较恰当,比“聪明”更高级。但是如果用翻译软件,这个词会变着花样被翻译成“很聪明”,美感全无。


在Tandem跟人瞎聊时以涉及复杂事件就词穷,直到认识了一个 西班牙的 PHD 与另一个 印尼的大学生,她们帮我找到了关键点,基础语法知识不扎实,英语的思维不足。有些时候他们会说我表达的很棒,口语也行,有些时候他们会说我瞎搞。其实很好理解,就像他们学中文一样,入门也不难,难的是随意调动有限的词汇自由组织句子进行表达,而不是脑子里先想一个母语再试着翻译成外语,难的是在陌生场景下做出正确的表达,能用已经学的知识学习新知识,也就是进入用英语学习英语的阶段。


另外一个例子就是做日常技术学习的时候,尤其是阅读源码的时候,往往是不翻译看懂一部分注释,翻译后看懂一部分,两个一结合就半懂不懂,基于这个半懂不懂的理解写大量测试去验证自己的理解,反推注释是否理解正确,这个过程非常慢,效率极低。


这就是为什么很多东西需要依赖大佬写个介绍文档,或是翻译+延伸解释之后才能高效率学习,为什么自己找不到深入学习的路径,总是觉得前方有些混沌。


记得在刚入行的前几年写过一篇学习笔记,把自定义view 在view层测量相关的代码中的注释,变量名称整个都翻译了,备注2进制标记位变化结果,再去理解逻辑就非常简单了。跟读小说没啥区别(读Java代码就像读小说,我一直这么觉得),很快就理解了。但这个过程要花太多时间了,多到比别人慢10倍不止。


所以这第一个难而正确的事情是学习英语


达到能顺畅阅读技术资料与代码的地步,才能提高我在学习效率上的天花板。


第二个是有关生活的事情,增加不同的收入手段,主业以外至少赚到1块钱


裁员给我最大的感触就是,我很脆弱,我的职业生涯很脆弱,我的生存能力很脆弱,不具备一点反脆弱性。如果没有工作我就没有任何收入,只要稍微发生一点意外,就会面临巨大的经济压力,对于我和家庭都会陷入严重的经济困难中。


市场寒冬与我有关但却不受我影响,我无法改变。同时平庸的职业经历在行业内的影响微乎其微,大佬们是不管寒风往哪吹的,他们只管找自己想做的方向,或者别人找到他们。


我就认识这样的大佬,去年让我去新公司负责组新团队,连续一两周持续对我进行电话轰炸,因为当时正负责的团队处于关键期,我有很深的“良知”情节,我婉拒了,这是优点也是缺点。


而我只有不断提高自己的能力,让人觉得有价值才能持续在这个行业跟关系网里谋生。


但是我知道,大风之下我依然是树叶,我不是树枝,成为树枝需要天时地利人和。就像在公司背小锅的永远都是一线,因为如果是管理层背锅那公司就出了决策性的大问题了,对公司而言已然就是灾难。


这几周陆续跟很多人聊了各种事情,了解他们在做什么。有双双辞职1年多就在家做私活忙得不亦乐乎,有开网店有做跨境电商的,也了解了很多用Chat GPT,Midjourney 等AI工具做实物产品在网上卖的。包括去年了解的生财有术知识星球等等,真的花心思去了解,打开知识茧房确实了解到非常多不同的方向,有一种刘姥姥进大观园的感觉。


自己做了一些实际尝试,跑了下基本流程,确实有一些门槛但各不相同。同时在这个过程中,又因为英语阅读效率低而受阻,文档我也是硬看,不懂的词翻译一下,理解不透再整句翻译,再倒回来看原文。


比如网上midjourney的教程一大把,其实大多数都不如看midjourney官方文档来的快,我从看到用到商品上架,不过几个小时,这中间还包括开通支付跟调整模型。


至于赚到1块钱,有多难呢,当我试了我才有所体会。


种一棵树最好的时间是在10年前,其次是现在。


继续保持在社区的输出,保持技术学习。休假我都不会完全休息,Gap 中当然也不会。


后记


去年公司陆续开始裁撤业务线,有的部门直接清零,公司规模从几千人下降到千人以内不过是几个月的事情,有被裁的,也有为了降低自身风险而主动走裁员名单,这也是双赢的解决方案,公司能精简人员个人可以拿到赔偿。管理层的主要工作是尽力留下核心成员,温和的送走要离开的成员,最大程度降低团队的负面情绪,做人才盘点,申请HC做些人力补充,减少团队震动保障项目支撑。没错,一边裁员一边还会招人。


彼时我个人也才刚刚在管理岗上站稳脚跟不久,团队里没有人申请主动离职算是对我挺大一个宽慰。有的团队人员流失率接近70%,相比之下我压力小得多,但我依然要交出一个名字给到部门负责人。我当然很不舍同时也为他们担忧,过去一年多大家一起相互成长,很多人也才刚刚步入新职级。


我努力寻找第三选择,功夫不负有心人,之前做过的一个项目独立出去了,成立了独立的子公司运营,新团还没搭建完。当时跟那个项目团队的产品,后端负责人配合得相当不错,我便以个人的背书将一个曾重点负责过这个项目的成员推荐过去,加上直属上级的帮助,最终在所有HC都要HRD审批的环境下平滑的将裁员变成了团队调配。现在即使我离开了母公司,他们小团队依然还不错,没有受到后续裁员影响。这位小伙伴人特别实在,他是我见过执行里最强的人,他值得这样的好运气。


作为管理者,我有些单纯的善意,不满足于工作层面的帮助。因为我觉得个人能量实在是太小了,而未来无人知晓。


作为核心部门虽然裁员的影响波及较为滞后,但明显的感觉还是研发压力骤减,加上公司为了早点达到账面盈亏平衡,对部分薪资采取缓发,在这样的背景下整个部门的氛围变了,需求评审得过且过,项目质量得过且过,此类情况比比皆是,工作的宽容度也一再升高。


作为个人来讲这真是躺平型工作,工作任务骤减但薪资还照样发,绩效照发,每天到公司跟去上学一样。我心里就出现了一个声音「你喜欢在这里继续混吗?这里如此安逸」。


今年3月意料之中的新一轮裁员到来,我几乎没有犹豫就答复了部门负责人。团里谁想留下谁不想留我很清楚,过去我们一直也保持比较健康的氛围,始终鼓励能力强的人出去看看,也明确告知留下来与出去将面临的不同风险。大家都有心理准备,但大家都没有懈怠自己的学习,技术目标按部就班,丝毫没有陷入负面漩涡,偶尔还会因为讨论技术问题忘记下班时间。


这一次,我把自己放在了名单上,当然这并不突然。我与部门负责人一直保持着较高的工作沟通频率,就向上管理这一点上,我自认做得非常不错。


离职后大家都积极找工作,我对他们非常有信心,抛开头部大厂,中厂依然是他们的主阵地,他们在各自专精的领域里技术都很扎实,尤其是去年大家一起补足了深层次的网络层知识。不出意料部分人都很快拿了offer,有的更是觉得不想面试了匆匆就入职了,这我就不行使自己好为人师的毛病了。


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

Android 自定义开源库 EasyView

  这是一个简单方便的Android自定义View库,我一直有一个想法弄一个开源库,现在这个想法付诸实现了,如果有什么需要自定义的View可以提出来,不一定都会采纳,合理的会采纳,时间周期不保证,咱要量力而行呀,踏实一点。 配置EasyView 1. 工程b...
继续阅读 »

  这是一个简单方便的Android自定义View库,我一直有一个想法弄一个开源库,现在这个想法付诸实现了,如果有什么需要自定义的View可以提出来,不一定都会采纳,合理的会采纳,时间周期不保证,咱要量力而行呀,踏实一点。


1682474222191_095705.gif.gif


配置EasyView


1. 工程build.gradle 或 settings.gradle配置


   代码已经推送到MavenCentral(),在Android Studio 4.2以后的版本中默认在创建工程的时候使用MavenCentral(),而不是jcenter()


   如果是之前的版本则需要在repositories{}闭包中添加mavenCentral(),不同的是,老版本的Android Studio是在工程的build.gradle中添加,而新版本是工程的settings.gradle中添加,如果已经添加,则不要重复添加。


repositories {
...
mavenCentral()
}

2. 使用模块的build.gradle配置


   例如在app模块中使用,则打开app模块下的build.gradle,在dependencies{}闭包下添加即可,之后记得要Sync Now


dependencies {
implementation 'io.github.lilongweidev:easyview:1.0.2'
}

使用EasyView


   这是一个自定义View的库,会慢慢丰富里面的自定义View,我先画个饼再说。


一、MacAddressEditText


   MacAddressEditText是一个蓝牙Mac地址输入控件,点击之后出现一个定制的Hex键盘,用于输入值。


1. xml中使用


   首先是在xml中添加如下代码,具体参考app模块中的activity_main.xml。


    <com.easy.view.MacAddressEditText
android:id="@+id/mac_et"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:boxBackgroundColor="@color/white"
app:boxStrokeColor="@color/black"
app:boxStrokeWidth="2dp"
app:boxWidth="48dp"
app:separator=":"
app:textColor="@color/black"
app:textSize="14sp" />


2. 属性介绍


   这里使用了MacAddressEditText的所有属性,可以自行进行设置,使用说明参考下表。







































属性说明
app:boxBackgroundColor设置输入框的背景颜色
app:boxStrokeColor设置输入框的边框颜色
app:boxStrokeWidth设置输入框的边框大小
app:boxWidth设置输入框大小
app:separatorMac地址的分隔符,例如分号:
app:textColor设置输入框文字颜色
app:textSize设置输入框文字大小

3. 代码中使用


    MacAddressEditText macEt = findViewById(R.id.mac_et);
String macAddress = macEt.getMacAddress();

   macAddress可能会是空字符串,使用之前请判断一下,参考app模块中的MainActivity中的使用方式。


二、CircularProgressBar


   CircularProgressBar是圆环进度条控件。


1. xml中使用


   首先是在xml中添加如下代码,具体参考app模块中的activity_main.xml。


    <com.easy.view.CircularProgressBar
android:id="@+id/cpb_test"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
app:maxProgress="100"
app:progress="10"
app:progressbarBackgroundColor="@color/purple_500"
app:progressbarColor="@color/purple_200"
app:radius="80dp"
app:strokeWidth="16dp"
app:text="10%"
app:textColor="@color/teal_200"
app:textSize="28sp" />

2. 属性介绍


   这里使用了MacAddressEditText的所有属性,可以自行进行设置,使用说明参考下表。















































属性说明
app:maxProgress最大进度
app:progress当前进度
app:progressbarBackgroundColor进度条背景颜色
app:progressbarColor进度颜色
app:radius半径,用于设置圆环的大小
app:strokeWidth进度条大小
app:text进度条中心文字
app:textColor进度条中心文字颜色
app:textSize进度条中心文字大小

3. 代码中使用


    CircularProgressBar cpbTest = findViewById(R.id.cpb_test);
int progress = 10;
cpbTest.setText(progress + "%");
cpbTest.setProgress(progress);

   参考app模块中的MainActivity中的使用方式。


三、TimingTextView


   TimingTextView是计时文字控件


1. xml中使用


   首先是在xml中添加如下代码,具体参考app模块中的activity_main.xml。


    <com.easy.view.TimingTextView
android:id="@+id/tv_timing"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:text="计时文字"
android:textColor="@color/black"
android:textSize="32sp"
app:countdown="false"
app:max="60"
app:unit="s" />

2. 属性介绍


   这里使用了TimingTextView的自定义属性不多,只有3个,TextView的属性就不列举说明,使用说明参考下表。























属性说明
app:countdown是否倒计时
app:max最大时间长度
app:unit时间单位:s(秒)、m(分)、h(时)

3. 代码中使用


    TimingTextView tvTiming = findViewById(R.id.tv_timing);
tvTiming.setMax(6);//最大时间
tvTiming.setCountDown(false);//是否倒计时
tvTiming.setUnit(3);//单位 秒
tvTiming.setListener(new TimingListener() {
@Override
public void onEnd() {
//定时结束
}
});
//开始计时
tvTiming.start();
//停止计时
//tvTiming.end();

   参考app模块中的MainActivity中的使用方式。


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

前端实习近半年的工作总结

前言:     来北京工作实习已经一年,总结一下自己这最近半年的实习经历的感受与收获吧。     首先感谢一下组内的同事,以及我的mentor对我入职实习期间的帮助,以及对我日常问题的解答,特别是我的mentor,从他身...
继续阅读 »

前言:


    来北京工作实习已经一年,总结一下自己这最近半年的实习经历的感受与收获吧。


    首先感谢一下组内的同事,以及我的mentor对我入职实习期间的帮助,以及对我日常问题的解答,特别是我的mentor,从他身上学到了很多优秀的开发习惯和技巧。


    其实在写这篇总结之前,我也去阅读了很多作者写的工作总结,看完大家工作总结之后,我发现自己这半年的工作还是太安逸了,没有逃离自己的舒适圈,这半年自己并没有主动去学习新的一些技术,更多的只是在完成业务需求,但是在工作中想有所提升仅仅做业务开发是远远不够的,要想成长就必须学习新的技术,做一些技术产出才可以,所以这次的总结更多的是对自己的反思以及对之后正式工作的计划与展望。


工作内容:


     部门的业务主要是toC方向做小程序的开发,刚来到公司前期主要跟我的mentor一起负责砍价业务,我用了一周时间来了解砍价相关业务以及代码,并且做了一个宣讲(由于自己当时对于部门业务还不是非常了解,当时的宣讲更多的仅仅是针对前端的业务以及代码逻辑,并没有去了解每一个接口内部的实现,所以感觉第一次宣讲并没有表现很好),后来由于内部调整我就开始主要负责列表的相关需求。我所在的部门属于全栈开发,前端主要采用是微信小程序以及Vue,并且我们有自己的node中间层来封装接口,所以平时会经常写node.js+TS,这在我上一家公司是没有体会过的,(我觉得这样非常合理,再也不用像在上家公司一样,因为一些接口返回字段类型不统一去排查和后端交流好久)这也让我页接触学习到了更广泛的技术栈。


    平时除了开发任务还包括一些日常的巡检等等,虽然现在我还没有加入到日常的巡检当中,但是在最近几个迭代,我也开始自己对每天的指标进行观察巡检,总结巡检报告。


成长与收获:


工具的使用:


1、charles



一款常见的抓包工具,通过代理连接,把客户端的请求数据,以及服务端返回的数据完完整整的抓取下来,方便开发调试,一般搭配switchhost来使用,switchhos是用来改变本地host的工具,实现原理就是通过修改我们本地dns域名解析与ip之间的映射关系。


2、apiPost


在此之前我只知道用postman来请求接口、mock数据,但是经过我mentor的推荐,我发现apiPost是真的好用,用起来很方便推荐给大家!


(我mentor电脑里有非常多奇奇怪怪但是又实用的工具,每一样拿出来都让我直呼🐂🍺,包括他vscode中的各种插件👍,附上一个巨好用的截图插件Snipaste,大家自行感受)


3、Echarts



echarts.js是百度团队推出的一款用于图表可视化的插件,用于以图表的形式展现数据,功能强大,上手简单,在我认知里他和element-UI都属于辅助开发的工具,当时用的多了自然就会精通熟练,不用刻意的去学习,所以把它归到了工具使用这一类。这个是刚进入公司和我mentor一起做一个砍价报表的需求里面大量的图表就用到了Echarts,里面包含折线图,柱状图等等,非常丰富。


技术的拓展:


1、node.js


在来到这个公司之前,我自己也使用node的express框架写过一些小的demo,多少对node有一些了解,express是基于content中间件框架,框架自身封装了大量的功能比如路由router和视图处理等等;我现在开发使用的是基于koa搭建的一个自己的框架,koa相较于express的使用更加灵活,并且我们框架的层次划分非常清晰,把业务代码按照controller、busoness、agent三层层来细分处理,减少了代码的冗余并且更加整洁,方便理解。


2、TypeScript


TS又称为javascript的超集,而且本质上向这个语言添加了可选的静态类型和基于类的面向对象编程。自我感觉Ts在帮我们写出更加强壮的代码,在开发过程中就能将我们的一些错误暴漏出来,编写代码的提示也极大的提升了我们的开发效率,不过TypeScript的学习并不是那么简单的,需要经过大量的联系和阅读,对我来说泛型对我来说掌握的还不够,也是后边学习的重点。


3、vuex


Vuex属于vue的一种状态管理模式,将页面组件中可以共享的变量方法进行统一的管理,告别了一层一层的变量传递,在之前只是说去了解学习过Vuex的一些用法知识,最近做公司的H5项目时多数页面都是用vuex来管理数据的,也是在实践中系统的使用学习了一下。


4、socket


socket通信是新接触的知识,Socket是应用层与TCP/IP协议族通信的中间软件抽象层,是一组接口(也是刚刚查的),部门前段时间刚刚做过分享,建立微信与h5之间的socket通信,还是比较感兴趣的,计划在下半年进行了解和学习。


5、webpack/vite


最近在做公司h5项目的时候感到很烦恼,由于项目很庞大,加上我的电脑真是非常的卡,所以每次启动h5项目的时候都要超级长的时间,n分钟,公司的项目是由webpack构建的,所以就在考虑学习一种更加快速的大包构建工具去尝试优化一下这个项目,vite就是学习的对象,从底层原理上来说,Vite是基于esbuild预构建依赖。而esbuild是采用go语言编写,因为go语言的操作是纳秒级别,而js是以毫秒计数,所以vite比用js编写的打包器快10-100倍。所以接下来的目标就是去学习这个webpack以及vite,学习优化的过程也是对自己的考验。


习惯的养成:


      其实这半年里我觉得最好的习惯就是跟我mentor学习的记笔记,刚来公司就发现我mentor每次做完需求都会记笔记复盘,包括学习一些技术等等都会记笔记,渐渐的我也养成了这个习惯,但是现在记得一些东西都还不是很成熟,都是一些做需求过程中当作思路分析来写的,之后在笔记的书写上应该对自己增加些要求不能太过随意(因为前几天看之前写的已经看不懂当时写的是啥了!)。


      还有一个就是写文章!这个总结是我在掘金写的第一篇文章,之后每周或者每学一一个内容都会在掘金记录一下总结和收获,看了这么多大佬的文章,真的觉得自己非常渺小,作为一个小白,要从头开始把所有知识都从新学习一下。


下半年学习计划:


1、vue3.x:自己之前一直都在使用学习vue2,并没有扩展到vue3,在不学习就要被淘汰了,所以vue3是下半年学习的重点!


2、TS:ts自己现在也只是了解皮毛,可以进行开发,但是掌握的还是太少,要继续学习。


3、webpack/vite:还有就是刚才说的webpack/vite,自己要学习并且优化公司的h5项目!


4、socket:这个作为下半年的拓展,了解的同时也可以复习一下网络的所有知识。


5、markdown语法:第一次写文章就没那么讲究了,写的很丑,下次学一下markdown语法,把文章写的漂亮一点。


阅读书籍:


下面这些是被我列在清单里的书籍,也是同事推荐给我的







结束语


总之这半年过的还是浑浑噩噩太过安逸,嘴上说着是因为弄毕业设计但是自己知道并没有去努力,下半年逃出自己的舒适圈!完成自己的计划,向着目标前进!


作者:fc550
来源:juejin.cn/post/7121378029678362638
收起阅读 »

一个前端实习生在蔚来的成长小结

此文章2332字,预计花费时间7-11分钟。 一、聊聊工作氛围 & 个人成长 1. 这是我的期许 “所谓前途,不过是以后生活的归途。” 这是我人生中的第一段技术实习,之前并不是一心做技术的,所以为了探索自己喜欢的事情在某小公司做过翻译实习,并且在交行...
继续阅读 »

此文章2332字,预计花费时间7-11分钟。


image.png


一、聊聊工作氛围 & 个人成长


1. 这是我的期许


“所谓前途,不过是以后生活的归途。”


这是我人生中的第一段技术实习,之前并不是一心做技术的,所以为了探索自己喜欢的事情在某小公司做过翻译实习,并且在交行做过金融实习(其实就是纯打杂)。


image.png


我很喜欢这样一段话: “我曾以为我的23岁会手提皮包西装革履,但我还是穿着休闲裤,带着十几岁的意气行事,幼稚又成熟;我曾以为我的23岁会性格外向,做事圆滑,但我连最简单的亲情都处理不好;我曾以为我的23岁会和喜欢的人看山河大海落日余晖,但没想道周围的人谈婚论嫁都近在眼前,我还在路上找自己。”


我一直在探索着自己的边界,在能闯能疯的年纪反复横跳,寻找着自己的热爱与期许。在真正从事这个行业之后,我发现了我对于这个岗位的喜爱,当你看着一个个实际的视图出现于自己的手中,你会有一种莫名其妙的成就感,这种感觉难以描述的程度就好像你要向一个完全不看vtuber的人描述你对嘉然的喜爱。


2. 工作氛围:这里是一个乌托邦(适合摸鱼学习的好地方!)


说实话,我最开始预期是每天九点来上班,九点下班的(因为看学长们实习都好辛苦的样子)。


来了之后发现完全不是,每天十点上班,六点下班(我当然是准点跑路)



实习两个月左右的时候接的一个需求,第一天是另一个前端实习生来搞,后来他要跑路,leader就把活给我了。


周四,后端六点把接口给另一个前端实习生。


另一个前端实习生:“明天再说”


周五我来接这个活,我边画页面边让他加字段。


然后提完了,六点他给我改好的接口,让我看看有没问题


我:“下周再说”。


后端:“前端是不是,都很快乐啊[流泪]”



image.png


最开始因为我对 react 不是特别熟悉,leader 让我看着组内文档学了半个月,才开始了第一个需求。


leader 没有给我指定 mentor,所以当我有问题的时候,我看组内谁没开会(或者有时间)就会去问,都能得到很耐心的解答,这点来说还是很有安全感的。


然后每天都会跟着老板和大老板一起去吃饭,有时听他们说说自己的事情,有时听听他们对某个语言的看法,也算有不少收获。


值得一提的是刚入职三天部门就开始团建了,从周五下午五点玩到了第二天凌晨两点,炫了一只烤全羊,然后就开始电玩篮球各种 happy,后面玩狼人杀我次次狼人,大老板也总觉得我是狼人,我次次和他对着刚(乐)



马上就要第二次团建了,可惜参加不了呜呜呜



在团建上 leader 说我是从五个面试感觉都 ok 的人里面选出来的(当时我超惊喜的)


还有几件有趣的事情值得一提



第一件事情是中午和 leader 散步,他说:“你干了两个月这里的情况也看到,很难接触到同龄的小姐姐的,找对象的优先级应该要提高了。”


我:“说的对说的对。”


当时我心里就暗暗想着,这是我不想找吗?这tm是我找不到啊(悲)


第二件事情是我有事开了自己的热点,热点的名字叫:“要失业了咋办呐。


被同事发到了前端大群里。


同事:“这是谁的啊?”


我:“是实习生的(悲)”



3. 个人成长:“不卑不亢,低调务实”


最开始入职当然会担心一些七的八的,诸如这样说会不会不太客气,这样搞会不会让老板不爽,后来和老板还有大老板一起吃饭之后发现他们人都挺随和的,没什么架子,他们更多的关心的是这件事情做的怎么样。


大老板曾经在周会上说:“这个事情可以做的慢一些,这是能力上的问题,这个可以商量,但是如果到了约定的日期没有交付,这就有问题了。 ”这个是说的务实。


然后就是为人处事方面了,自己有时候挺跳脱的,没有什么边界感,在实习和他们一起吃饭的时候我就回默默的听着,有些问题大家都不会问,算是看着看着就成长了。


回校远程的时候我写了这样一段话:



去打工吧,去打上海冬夜准时下班,踩雪走回家的工。


去打工吧,去打一边聊天一边发现,这个产品也是清华✌️的工。


去打工吧,去打测试前一天,人都走光了,mentor陪我赶工到半夜的工。


去打工吧,去打部门团建,大leader带我们玩狼人杀到凌晨两点,超级尽兴的工。


冴羽曾在一次读书会上分享:“开眼界就像开荤一样,只有见过了才会产生饥饿感。”


打工虽然让我变成了稍不注意就会摆烂的成年人,但大平台汇聚了很多丰富有趣的同事,让我看到了截然不同的经历与一波三折的人生。


不知道是不是部门的原因,我这边总是十六五准点上下班。


我现在依然处于打工真香的阶段,不用早起,不用日复一日的和同龄人卷同一件事,身边的人年岁不同,人生阶段也不相同,卷不到一起去。


我还在路上~



image.png


4. 代码方面 learning


说实话看到组内项目的时候体会到了不少的震撼,看着组内的项目之后真的就感觉自己写的东西和玩具一样,每次写完项目,都会兴冲冲的找组内的哥哥姐姐帮忙 CR,然后 CR 出一堆问题,自己在一个一个的修改,把这些规范点记周报上,总之就是学到了很多很多。


timeLine 大概是这样的



  • 前两周熟悉 react 写小 demo

  • 然后以两周一个需求的速度给咱活干~


记得第二次写完一个稍微有点复杂的需求,带着我做这个需求的 mentor 还夸了我一波(骄傲)


5. 对于技术和业务的想法


大leader组织组内 vau 对齐的时候我仔细的听了听,我们的很多东西都需要落地,相比来说技术只是一个实现的手段,并不是做这个的目的。


但怎么说呢,我个人还是对技术本身抱有很大的期许的,希望自己能够变得很厉害,参与到很多的开源项目中,我坚信代码可以改变世界。


二、展望未来



实习不去字节,就像读四大名著不看红楼梦,基督徒不看圣经,学相对论不知道爱因斯坦,看vtuber不看嘉然今天吃什么,这个人的素养与精神追求不足,成了无源之水,无本之木。他的格局就卡在这里了,只能度过一个相对失败的人生!




  • 话是这么说啦,但最后还是没有成功去到字节,但是我是字节不折不扣的舔狗,后面再看吧。

  • 字节给我发面试一定是喜欢我(普信)


下面这段是之前写的



离开的契机也很简单,我在小红书实习的同学跑路了,然后要找继任,顺手把我的简历投过去了,然后我顺手面了一下小红书,小红书顺手给我发了个Offer(bushi,然后就去小红书了。



image.png


小红书确实Offer了,但是老板和我约谈了很久,我决定继续远程实习,在这篇文章发布的当天,我已经实习了 一百四十天,我相信,我的旅途还在继续。


image.png


三、写在最后


不知不觉就实习快半年了啊


我真的非常感谢遇到的leader和同事,感恩遇到的每一位愿意拉我一把的人。


在这段时间里学到了好多一个人学习学不到的东西啊。


那么这就是我在蔚来的实习小结啦!


感谢阅读~


作者:阳树阳树
来源:juejin.cn/post/7228245665334198333
收起阅读 »