注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

如果失业了,我们又将何去何从?

经历 先说说自己的经历吧,小编在21年之前在南京一家国企外包工作过;主要做的是国网的项目,那时候工资不高但是福利待遇不错,什么季度奖、项目奖、年终奖没断过,可能日子太过安逸了吧,自己又想挑战一下高薪,于是就跳槽去了一家做法院业务的公司。跳槽的时候是20年,当时...
继续阅读 »

经历


先说说自己的经历吧,小编在21年之前在南京一家国企外包工作过;主要做的是国网的项目,那时候工资不高但是福利待遇不错,什么季度奖、项目奖、年终奖没断过,可能日子太过安逸了吧,自己又想挑战一下高薪,于是就跳槽去了一家做法院业务的公司。跳槽的时候是20年,当时疫情才刚开始,对经济的影响也还好,所以感觉当时找工作也没那么难。又过了1年,由于小编家人在18年的时候就给我在无锡贷款买了房,所以想早点回去发展,也过够了那种挤在出租屋里的感觉,所以我又辞了来了无锡去了一家做云计算的公司,也还算稳定,待了大概有2年多,从去年开始就开始走下坡路了,23年中旬开始大规模裁员,小编不幸中招。

从那家做云计算的公司走了之后我抑郁了半个月,因为半个 月内都找不到工作,当时真的很着急,因为还有房贷要、信用卡、车贷等等这些要还;从第三周开始我就下定决心要好好好背复习,不只是八股文,还有数据结构、算法 、设计模式等等,功夫不负有心人在第三周终于收获2个offer,此时悬着的心终于落了下来,阴差阳错去了一家做医疗的公司做运维监控平台。

后来不知怎么的裁员风波不断,从不少朋友那边收到消息很多公司都在裁员,很快这波风也刮到了我们公司;有一天我邮件收到了三个月试用期转正的邮件,当时还高兴了一下,但是第二天就通知我转正临时取消,没有任何理由 的取消,随后就有人事来通知我去签合同;我纳闷还要签啥合同啊,不过我大概也猜到点了,因为前些天就有小道消息说要延长试用期,我当时还以为是假的。到了人事那边一看是新的劳动合同,一看试用期变成了6个月,人事和我说应公司领导要求试用期需要延长,剩下3个月的公司还是按打八折发,但是转正后一并给我;我当时惊呆了,心想还能有这种操作,不过没办法看着大家都签了也只能签。这个事情开了头之后,后面又小道消息不断,听人说要降薪20%,很多同事听了都不愿意,于是都被一起约了谈了一次话,有些脾气比较爆的直接怼了领导,最后也就不欢而散。年前董事长又召集了我们研发部聊这个降薪的事情,开头先一堆铺垫,说什么今年怎么怎么难,外面都在裁员什么的,最后又说不降20%了,但是还是会扣5%,这个5%看公司运营情况来发放,大家听完虽然还是不情愿,但也没有再多说什么。


现状


这段时间公司一直没有活,领导也没安排,每天来了就往那一坐就没有方向,再也找不到以前工作的那种感觉了;有些人可能觉得没活干还给你发工资不是挺好嘛,但其实不然你仔细想想,没活干代表公司接不到单子,没有单子就代表没有钱,没钱怎么发工资,所以这种状态持续不了多久。不过这种状态迄今为止已经持续1个多月了,年前我也找过几家单位,面试我觉得面得挺好的,但都没有后续了,有的你问人家,人家却说再等等给你答复还有好多人没面呢,我心想这下完了,今年物联网行情太差了,以前随随便便手里拿好几个offer。越是没活干我就越是焦虑,最近这个班上的真的感觉像坐牢一样,不知道有没有和我一样经历的小伙伴,有兴趣的可以私信和我聊一下。


裁员


23年各大互联网公司裁员情况如下:



  • 知乎:裁员约300人

  • 去哪儿:裁员约400人

  • 搜狐:裁员约800人

  • 美团:裁员约900人

  • 途家:裁员约1400人

  • 京东:裁员约1500人

  • 网易:裁员约2000人

  • 58同城:裁员约3000人

  • 阿里:裁员约4000人

  • 百度:裁员约5000人

  • 滴滴:裁员约5000人


寻找方向


小编是一位Java程序员,现在只会Java写写增删改查根本找不到工作(除非学历很优秀),可以说绝大部分的程序员都能满足传统企业的需求;下面是我整理的可以试试去发展的方向,个人理解不喜勿喷,有兴趣的朋友可以一起去探讨。


研发方向



  • 大数据:也比较卷

  • 云计算:容器、容器编排(K8s)、云原生

  • AI大模型:今年的热门,但是不知道怎么去做


其他方向


image.png


如果不做程序员了我们还能做什么


当前的大环境下确实很艰难,说实话如果不做程序员了,我很难立马想到一个能挣到与之相当工资的工作。虽说360行,行行出状元,可哪一行不都是需要经过千锤百炼才能成功的,隔行如隔山,转行又岂是一朝一夕的事情。

现在的市场Java开发已经趋于饱和,和Boss打招呼基本都是已读不回的状态,甚至面试机会都没有,有的就算面得再好也没用,因为还有一堆人没面跟你竞争,有的期望薪资低,有的学历比你好,有的到岗时间比你早,对此我真的很无奈,快卷不动了。


跑滴滴


由于我经常跑扬州,走高速成本太高了,于是我就注册了个顺风车,想着能回一点成本,基本上去一趟拉一单有100多,来回基本上和成本抵消了。后来我就想着通过顺风车偶尔做做兼职能赚点钱,于是下了班就跑了几回,发现在市区跑订单金额很低,后来跑了几回也就放弃了;后来想注册专车司机,于是就找朋友了解了一下 ,发现注册了滴滴只会让你跑一段时间,然后就要办那个营运证,而且也不适合兼职做,目前还有工作,也许真的哪天失业了我会考虑全职做这个吧。从我朋友那边了解到,滴滴也很卷,每天起早贪黑的,给你派了单子只能被动接受,不像顺风车可以主动选择,不过真到没工作那一步也只能选择尝试一下。


开店


小编比较幸运,在老家小镇还有一个小店面,不过大家也可以租一个,只要做起来了都是一样的,关键是要能把生意搞起来。我其实早就萌生了这个想法,但是我现在才临近30岁,还想在外面再闯一下。


食品小超市


为什么想开这个呢,主要我发现了一个路子,哈哈!我老家有个很有名的食品超市,也是从小店一点一点做起来的,而且我还知道他是去无锡金桥食品批发市场批发的,国产的、进口的食品全都有。今年过年的时候去他们家买东西人都爆满,老板在收银台堆满了车厘子,2J、3J、4J的车厘子卖的好的不行,我在那边没一会就卖了一幢(车厘子堆起来的,堆了一幢),后来听别人说这个老板过个年净利润有四五十万。在我老家那边过个年能赚个四五十万万真的可以吓死人,于是乎开食品超市的想法在我心中萌芽。不过还好我们家那个店铺和他们家靠的不是很近,再开一家也不是不行,再不济也可以考虑和他加盟。

大家可以观察一下老家有没有那种食品超市的店,店里卖的都是比较中高档的吃的,周围人群比较密集的地方可以考虑开一个,只要找到货源能持续供货,这个店应该很容易就能开起来。


电脑店


我们家那个店面我也想过开个电脑店,卖卖电脑、装装系统、装装摄像头什么的,我感觉也不错,要说修电脑硬件我还是没那个本事的,不过可以带到城里面去修。


摆美食小摊


我想着等我年纪再大一点,可以搞个小美食摊,可以做做煎饼、鸡蛋饼、臭豆腐、烤香肠这种,不过不能在城里搞,只能去乡下镇上,城里面城管管得严,不是很好搞。


做短视频


这个想法我也考虑了很久,至今还没开始实践,因为我老是怕我做不好,作为程序员,视频剪辑这种我应该是一学就会。其实我主要就是觉得没有赶上风口,现在做短视频的搞直播的一大堆,没有什么吸引流量的创意真的很难博人眼球。后面我打算尝试尝试记录生活,生活琐事都拍一下,比如出去露营、钓鱼、旅行,在家做饭、健身什么的都可以拍一下,不过我听朋友说要专注拍一类才能拍好,也不知道是不是真的。


总结


给大家总结一下就是今年能不跳槽就尽量不要跳槽,绝对不能裸辞,在今年这个风口浪尖上各大企业都在人员优化、降本增效。欢迎大家多多留言,提一些建议,最后也祝大家龙年大运,在新的一年里找到自己满意的工作。


作者:MrDong先生
来源:juejin.cn/post/7338307026245845044
收起阅读 »

请立即停止编写 Dockerfiles 并使用 docker init

本文翻译自 medium 论坛,原文链接:medium.com/@akhilesh-m… , 原文作者: Akhilesh Mishra 您是那种觉得编写 Dockerfile 和 docker-compose.yml 文件很痛苦的人之一吗? 我承认,我就是...
继续阅读 »

本文翻译自 medium 论坛,原文链接:medium.com/@akhilesh-m… , 原文作者:

Akhilesh Mishra


您是那种觉得编写 Dockerfile 和 docker-compose.yml 文件很痛苦的人之一吗?



我承认,我就是其中之一。



我总是想知道我是否遵循了 Dockerfile、 docker-compose 文件的最佳编写实践,我害怕在不知不觉中引入了安全漏洞。


但是现在,我不必再担心这个问题了,感谢 Docker 的优秀开发人员,他们结合了生成式人工智能,创建了一个 CLI 实用工具 — docker init。


介绍 docker init


微信截图_20240224145630.png


几天前,Docker 推出了 docker init 的通用版本。我已经尝试过,发现它非常有用,迫不及待地想在日常生活中使用它。


什么是 docker init?


docker init 是一个命令行应用程序,可帮助初始化项目中的 Docker 资源。它根据项目的要求创建 Dockerfiles、docker-compose 文件和 .dockerignore 文件。


这简化了为项目配置 Docker 的过程,节省时间并降低复杂性。



最新版本的 docker init 支持 Go、Python、Node.js、Rust、ASP.NET、PHP 和 Java。目前它只能于 Docker Desktop 一起使用,也就是说大家目前在 Linux 系统中是无法使用 docker init 的。



如何使用 docker init?


使用 docker init 很简单,只需几个简单的步骤。首先,转到您要在其中设置 Docker 资源的项目目录。


举个例子,我来创建一个基本的 Flask 应用程序。


一、创建 app.py 以及 requirements.txt


touch app.py requirements.txt

将以下代码复制到相应文件中


# app.py
from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello_docker():
return '<h1> hello world </h1'

if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0')

# requirements.txt
Flask

二、使用 docker init 初始化


docker init 将扫描您的项目并要求您确认并选择最适合您的应用程序的模板。选择模板后,docker init 会要求您提供一些特定于项目的信息,自动为您的项目生成必要的 Docker 资源。


现在让我们来执行 docker init。


docker init

出现如下结果,



接下来要做的就是选择应用程序平台,在我们的示例中,我们使用 python。它将建议您的项目的推荐值,例如 Python 版本、端口、入口点命令。



您可以选择默认值或提供所需的值,它将创建您的 docker 配置文件以及动态运行应用程序的说明。



让我们来看看这个自动生成的配置是什么样子。


三、生成 Dockerfile 文件


# syntax=docker/dockerfile:1

# Comments are provided throughout this file to help you get started.
# If you need more help, visit the Dockerfile reference guide at
# https://docs.docker.com/engine/reference/builder/

ARG PYTHON_VERSION=3.11.7
FROM python:${PYTHON_VERSION}-slim as base

# Prevents Python from writing pyc files.
ENV PYTHONDONTWRITEBYTECODE=1

# Keeps Python from buffering stdout and stderr to avoid situations where
# the application crashes without emitting any logs due to buffering.
ENV PYTHONUNBUFFERED=1

WORKDIR /app

# Create a non-privileged user that the app will run under.
# See https://docs.docker.com/go/dockerfile-user-best-practices/
ARG UID=10001
RUN adduser \
--disabled-password \
--gecos "" \
--home "/nonexistent" \
--shell "/sbin/nologin" \
--no-create-home \
--uid "${UID}" \
appuser

# Download dependencies as a separate step to take advantage of Docker's caching.
# Leverage a cache mount to /root/.cache/pip to speed up subsequent builds.
# Leverage a bind mount to requirements.txt to avoid having to copy them int0
# int0 this layer.
RUN --mount=type=cache,target=/root/.cache/pip \
--mount=type=bind,source=requirements.txt,target=requirements.txt \
python -m pip install -r requirements.txt

# Switch to the non-privileged user to run the application.
USER appuser

# Copy the source code int0 the container.
COPY . .

# Expose the port that the application listens on.
EXPOSE 5000

# Run the application.
CMD gunicorn 'app:app' --bind=0.0.0.0:5000

看看它,它写了一个比我更好的 Dockerfile。



它遵循人们在所有 Linkedin 和 Medium 帖子中不断告诉我们的所有性能和安全最佳实践。



docker-compose.yml



它编写了 docker-compose 配置来运行应用程序。由于我们的应用程序不包含与数据库的任何连接,因此它注释掉了数据库容器可能需要的代码。


如果您想在 Flask 应用程序中使用数据库,请从 docker-compose 文件中取消注释 db 服务配置,创建一个包含机密的本地文件,然后运行该应用程序。它还为我们生成了 .dockerignore 文件。


为什么使用 docker init?


docker init 使 Docker 化变得轻而易举,特别是对于 Docker 新手来说。它消除了编写 Dockerfile 和其他配置文件的手动任务,从而节省时间并最大限度地减少错误。


它使用模板根据您的应用程序类型自定义 Docker 设置,同时遵循行业最佳实践。


总结一下


总而言之,docker init 完成了上面这一切。



  • 它可以编写比这里 90% 的孩子更好的 Docker 配置。

  • 像书呆子一样遵循最佳实践。

  • 当安全人员的工具生成包含数百个您从未想过存在的漏洞的报告时,可以节省时间、精力和来自安全人员的讽刺评论。


最后需要说明的是,就像任何其他基于人工智能的工具一样,这个工具也不完美。不要盲目相信它生成的配置。我建议您在使用配置之前再次检查下配置。



如果觉得这篇文章翻译不错的话,不妨点赞加关注,我会更新更多技术干货、项目教学、经验分享的文章。



作者:程序员wayn
来源:juejin.cn/post/7338717224435531826
收起阅读 »

面试官:实现一个吸附在键盘上的输入框

web
实现效果 话不多说,先上效果和 demo 地址: demo 地址:codesandbox.io/p/devbox/ke… 体验地址:7fsqr8-5173.csb.app 实现原理 要实现一个吸附在键盘上的 input,可以分为以下步骤: 监听键盘高度...
继续阅读 »

实现效果


话不多说,先上效果和 demo 地址:



demo 地址:codesandbox.io/p/devbox/ke…

体验地址:7fsqr8-5173.csb.app



666.gif


实现原理


要实现一个吸附在键盘上的 input,可以分为以下步骤:



  1. 监听键盘高度的变化

  2. 获取「键盘顶部距离视口顶部的高度」

  3. 设置 input 的位置


第一步:监听监听键盘键盘高度的变化


要监听键盘高度的变化,我们得先看看在键盘展开或收起的时候,分别会触发哪些浏览器事件:



  • iOS 和部分 Android 浏览器


    展开:键盘展示时会依次触发 visualViewport resize -> focusin -> visualViewport scroll,部分情况下手动调用 input.focus 不触发 focusin


    收起:键盘收起时会依次触发 visualViewport resize -> focusout -> visualViewport scroll


  • 其他 Android 浏览器


    展开:键盘展示的时候会触发一段连续的 window resize,约过 200 毫秒稳定


    收起:键盘收起的时候会触发一段连续的 window resize,约过 200 毫秒稳定,但是部分手机上有些异常的 case:键盘收起时 viewport 会先变小,然后变大,最后再变小



总结两者来看,我们要监听键盘高度的变化,可以添加以下监听事件:


if (window.visualViewport) {
 window.visualViewport?.addEventListener("resize", listener);
 window.visualViewport?.addEventListener("scroll", listener);
} else {
 window.addEventListener("resize", listener);
}

window.addEventListener("focusin", listener);
window.addEventListener("focusout", listener);

===========================


📚 题外话: 获取键盘展开和收起状态


===========================


在实际业务中,获取键盘展开和收起的状态,同样很常见,要完成状态的判断,我们可以设定以下规则:


判断键盘展开:当 visualViewport resize/window.reszie、visualViewport scroll、focusin 任意一个事件触发时,如果高度减少,并且屏幕减少的高度(键盘高度)大于 200px 时,判断键盘为展开状态(由于 focusin 部分情况下不触发,所以还需要监听其他事件辅助判断键盘是否为展开状态)


判断键盘收起:当 visualViewport resize/window.reszie、visualViewport scroll、focusout 任意一个事件触发时,如果高度增加,并且屏幕减少的高度(键盘高度)小于 200px,判断键盘为收起状态


// 获取当前视口高度
const height = window.visualViewport
? window.visualViewport.height
: window.innerHeight;

// 获取视口增量:视口高度 - 上次获取的视口高度
const diffHeight = height - lastWinHeight;

// 获取键盘高度:默认屏幕高度 - 当前视口高度
const keyboardHeight = DEFAULT_HEIGHT - height;

// 如果高度减少,且键盘高度大于 200,则视为键盘弹起
if (diffHeight < 0 && keyboardHeight > 200) {
   onKeyboardShow();
} else if (diff > 0) {
   onKeyboardHide();
}

同时,为了避免 “收起时 viewport 会先变小,然后变大,最后再变小” 这种情况,我们需要在展开收起状态发生变化的时候加一个200毫秒的防抖,避免键盘状态频繁改变执行“收起 -> 展开 -> 收起”的逻辑


let canChangeStatus = true;

function onKeyboardShow({ height, top }) {
   if (canChangeStatus) {
     canChangeStatus = false;
     setTimeout(() => {
callback();
        canChangeStatus = true;
    }, 200);
  }
}

第二步:获取键盘顶部距离视口顶部的高度


在 safari 浏览器或者部分安卓手机的浏览器中,在点击输入框的时候,可以看到页面会滚动到输入框所在位置(这是想让被软键盘遮挡的部分展示出来),这个时候,其实是触发了虚拟视口 visualViewport 的 scroll 事件,让页面整体往上顶,即使是 fixed 定位也不例外,因此要获取「键盘顶部距离视口顶部的高度」,我们需要进行如下计算:


键盘顶部距离视口顶部的高度 = 视口当前的高度 + 视口滚动上去高度


// 获取当前视口高度
const height = window.visualViewport ? window.visualViewport.height : window.innerHeight;
// 获取视口滚动高度
const viewportScrollTop = window.visualViewport?.pageTop || 0;
// 获取键盘顶部距离视口顶部的距离,这里是关键
const keyboardTop = height + viewportScrollTop;

第三步:设置 input 的位置


我们先设置 input 的 css 样式


input {
position: absolute;
top: 0;
left: 0;
width: 100vw;
height: 50px;
transition: all .3s;
}

然后再动态调整 input 的 translateY,让 input 可以配合键盘移动,为了保证 input 能够露出,还需要用上一步计算好的「键盘距离页面顶部高度」再减去「元素高度」,从而获得「当前元素的位移」:


当前元素的位移 = 键盘距离页面顶部高度 - 元素高度


// input 的 position 为 absolute、top 为 0
keyboardObserver.on(KeyboardEvent.PositionChange, ({ top }) => {
 input.style.tranform = `translateY(${top - input.clientHeight}px)`;
});

实现原理是不是很简单?不如来看看完整代码吧~


完整代码


import EventEmitter from "eventemitter3";

// 默认屏幕高度
const DEFAULT_HEIGHT = window.innerHeight;
const MIN_KEYBOARD_HEIGHT = 200;

// 键盘事件
export enum KeyboardEvent {
 Show = "Show",
 Hide = "Hide",
 PositionChange = "PositionChange",
}

interface KeyboardInfo {
 height: number;
 top: number;
}

class KeyboardObserver extends EventEmitter {
 inited = false;
 lastWinHeight = DEFAULT_HEIGHT;
 canChangeStatus = true;

 _unbind = () => {};

 // 键盘初始化
 init() {
   if (this.inited) {
     return;
  }
   
   const listener = () => this.adjustPos();

   if (window.visualViewport) {
     window.visualViewport?.addEventListener("resize", listener);
     window.visualViewport?.addEventListener("scroll", listener);
  } else {
     window.addEventListener("resize", listener);
  }

   window.addEventListener("focusin", listener);
   window.addEventListener("focusout", listener);

   this._unbind = () => {
     if (window.visualViewport) {
       window.visualViewport?.removeEventListener("resize", listener);
       window.visualViewport?.removeEventListener("scroll", listener);
    } else {
       window.removeEventListener("resize", listener);
    }

     window.removeEventListener("focusin", listener);
     window.removeEventListener("focusout", listener);
  };
   
   this.inited = true;
}

// 解绑事件
 unbind() {
   this._unbind();
this.inited = false;
}

 // 调整键盘位置
 adjustPos() {
   // 获取当前视口高度
   const height = window.visualViewport
     ? window.visualViewport.height
    : window.innerHeight;

   // 获取键盘高度
   const keyboardHeight = DEFAULT_HEIGHT - height;
   
   // 获取键盘顶部距离视口顶部的距离
   const top = height + (window.visualViewport?.pageTop || 0);

   this.emit(KeyboardEvent.PositionChange, { top });

   // 与上一次计算的屏幕高度的差值
   const diffHeight = height - this.lastWinHeight;

   this.lastWinHeight = height;

   // 如果高度减少,且减少高度大于 200,则视为键盘弹起
   if (diffHeight < 0 && keyboardHeight > MIN_KEYBOARD_HEIGHT) {
     this.onKeyboardShow({ height: keyboardHeight, top });
  } else if (diffHeight > 0) {
     this.onKeyboardHide({ height: keyboardHeight, top });
  }
}

 onKeyboardShow({ height, top }: KeyboardInfo) {
   if (this.canChangeStatus) {
     this.emit(KeyboardEvent.Show, { height, top });
     this.canChangeStatus = false;
     this.setStatus();
  }
}

 onKeyboardHide({ height, top }: KeyboardInfo) {
   if (this.canChangeStatus) {
     this.emit(KeyboardEvent.Hide, { height, top });
     this.canChangeStatus = false;
     this.setStatus();
  }
}

 setStatus() {
   const timer = setTimeout(() => {
     clearTimeout(timer);
     this.canChangeStatus = true;
  }, 300);
}
}

const keyboardObserver = new KeyboardObserver();

export default keyboardObserver;


使用:


keyboardObserver.on(KeyboardEvent.PositionChange, ({ top }) => {
 input.style.tranform = `translateY(${top - input.clientHeight}px)`;
});

作者:DAHUIAAAAAA
来源:juejin.cn/post/7338335869709385780
收起阅读 »

最新 GitHub 骗局!千万别中招!

今天一早,焚香沐浴更衣,打开全球最大同性交友网站,准备好好摸鱼;突然方向通知多了一条: 正疑惑是触发是 GitHub 的什么隐藏关卡呢,点进去一看: 一个 21 年的创建 issue?但是有个新的评论: 这个评论的大意是: 喂!x毛! GitHub 瞎...
继续阅读 »

今天一早,焚香沐浴更衣,打开全球最大同性交友网站,准备好好摸鱼;突然方向通知多了一条:


image.png


正疑惑是触发是 GitHub 的什么隐藏关卡呢,点进去一看:


image.png


一个 21 年的创建 issue?但是有个新的评论:


image.png


这个评论的大意是:



喂!x毛!
GitHub 瞎了眼相中你了,有个很适合你的职位,年薪高达 18w 刀乐!
赶紧来申请啊,各种福利各种巴适!
但是有记得在 24 小时内点击这个链接来申请哦!过时不候!
后面芭啦芭啦@了一大堆人,其中我的用户名赫然在列!



他真的!我哭死!原来天上真的会掉馅阱 😢...


但是仔细一看,评论的这个人,是默认头像。不对劲!非常不对劲!遂点进其主页一看:


image.png


啥也没有...


这时候事情就很明显了,然后我就去 GitHub 社区找了一下相关的反馈,果不其然,两天前开始有人在反馈相关问题:


image.png



原讨论传送门:github.com/orgs/commun…



于是笔者在隐私模式下打开@我的那个评论附上的链接:


image.png


这个页面会请求你使用 GitHub 授权登录,并且要求你授权各种高级权限;而一旦你授权了,大概率会发生的第一件事,就是你的帐号会在各种 issue 中发布上面那条“GitHub 求职骗局”的评论,以导致更多的人受骗...



截至笔者写下这篇水文时,该钓鱼网站链接已经无法打开。



而在早上笔者在 github.com/orgs/commun… 中留下评论后,陆陆续续又有上百个全球各地的开发者进行了反馈。甚至有领先一步的好哥们已经直接出手向域名注册商、域名托管服务商进行了举报,并收到了反馈:


image.png


而这,仅仅是在笔者写下这篇水文前的 23 分钟(一切发生得太快...


不仅如此,在笔者截完本文第三张图后,提及我的那条评论已经删除了 🤪 (一切发生得实在太快...


不不仅如此,在笔者敲完上一句话后打算再次确认一下发出评论那个用户(第四张截图),发现他已经被封禁了...


image.png


好家伙,这发生得也太快了吧!赶上直播了???


估计这一波,有不少帐号也受到波及,最好确认一下自己的帐号是否有被影响(吓得笔者又刷新了一下页面确认自己有没有被封禁)。


这也让笔者想起最近 GitHub、NPM 等各种平台都在极力地推动用户启用双因素身份验证(2FA),以提高用户帐号的安全;这样看来,确实是一个明智之举。


最后还是提醒一下各位:


不清楚来源的链接不要点!不清楚来源的链接不要点!不清楚来源的链接不要点!


就这样。


作者:Nauxscript
来源:juejin.cn/post/7337666469903122472
收起阅读 »

看完zustand源码后,我的TypeScript水平突飞猛进。

web
前言 过年期间在家里没事,把zustand的源码看了一遍,看完后我最大的收获就是ts水平突飞猛进,再去刷那些类型体操题目就变得简单了,下面和大家分享一下zustand库是怎么定义ts类型的。 ts类型推断 个人认为ts最大的作用有两个,一个是类型约束,另外一个...
继续阅读 »

前言


过年期间在家里没事,把zustand的源码看了一遍,看完后我最大的收获就是ts水平突飞猛进,再去刷那些类型体操题目就变得简单了,下面和大家分享一下zustand库是怎么定义ts类型的。


ts类型推断


个人认为ts最大的作用有两个,一个是类型约束,另外一个是类型推断。



  • 类型约束也叫类型安全,在编译阶段就能发现语法错误,可以有效减少低级错误。

  • 类型推断,当你没有标明变量的类型时,编译器会根据一些简单的规则来推断你定义的变量的类型


这一篇主要和大家分享类型推断,类型推断主要有以下几种情况。


根据变量的值自动推导类型


image.png


image.png


函数返回值自动推断


image.png


函数中如果有条件分支,推导出来的返回值类型是所有分支的返回值类型的联合类型


image.png


ts的类型推导方式是懒推导,也就是说不会实际执行代码。


image.png


上图中如果实际执行了,c的类型是能确认为null的。


使用范型推导


image.png


可以看到按照上面写法,对象合并推导不出来,如果能推导出来u3应该等于 {name: string, age: number}


这时候我们可以借助范型来推导


image.png


可以给上面代码简写为这样,编辑器也能推导出来


image.png


实战


实现pick方法


从一个对象中,返回指定的属性名称。


image.png


上面代码中定义了两个范型T和U,T表示对象,U被限定为T的属性名(U extends keyof T),返回值的类型为{[K in U]: T[K]},in的作用就是遍历U这个数组。


image.png


可以看到数组元素被限制了只能是user对象里的key


image.png


image.png


也正确的推导出来了


实现useRequest


先看一个例子


import { useEffect, useState } from 'react';

// 模拟请求接口,返回用户列表
function getUsers(): Promise<{ name: string }[]> {
return new Promise(resolve => {
setTimeout(() => {
resolve([
{
name: 'tom',
},
{
name: 'jack',
},
]);
}, 1000);
})
}

const App = () => {

const [loading, setLoading] = useState(true);
const [users, setUsers] = useState<Awaited<ReturnType<typeof getUsers>>>([]);
const [error, setError] = useState(false);

useEffect(() => {
setLoading(true);
getUsers().then((res) => {
setUsers(res);
}).catch(() => {
setError(true);
}).finally(() => {
setLoading(false);
})
}, []);

if (loading) {
return (
<div>loading...</div>
)
}

if (error) {
return (
<div>error</div>
)
}

return (
<div
>

{users.map(u => (
<div key={u.name}>{u.name}</div>
))}
</div>

);
};

export default App;

上面这个例子实现了从后端请求用户列表,然后渲染出来。为了提高用户体验,在加载数据时,加了一个loading,当请求出错时,告诉用户请求失败。


代码比较简单我就不一一讲解了,有行代码需要注意一下。


 const [users, setUsers] = useState<Awaited<ReturnType<typeof getUsers>>>([]);


  • typeof getUsers 获取getUsers函数类型

  • ReturnType 获取某个函数的返回值

  • Awaited 如果函数返回值为Promise,这个可以获取到最终的值类型。


image.png


image.png


可以看到,正确的获取到了getUsers函数的返回值类型。


然而一个很简单的功能需要写那么多代码,肯定是不合理的,那么我们给简化一下。目前市面上已经有不少库来解决这个问题了,比如react-query或ahooks库里的useRequest,都可以解决这个问题,我这里分享的不是具体代码实现,而是怎么写ts。


封装useRequest


import { useEffect, useState } from 'react';

export function useRequest<T extends () => Promise<unknown>>(
fn: T,
): {
loading: boolean;
error: boolean;
data: Awaited<ReturnType<T>>;
} {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
const [data, setData] = useState<any>();

useEffect(() => {
setLoading(true);
fn().then(res => {
setData(res);
}).catch(() => {
setError(true);
}).finally(() => {
setLoading(false);
});
}, [fn])

return {
loading,
error,
data,
};
}

改造app.tsx文件,使用useRequest


import { useRequest } from './useRequest';

// 模拟请求接口,返回用户列表
function getUsers(): Promise<{ name: string }[]> {
return new Promise(resolve => {
setTimeout(() => {
resolve([
{
name: 'tom',
},
{
name: 'jack',
},
]);
}, 1000);
})
}

const App = () => {

const { loading, data: users, error } = useRequest(getUsers);

if (loading) {
return (
<div>loading...</div>
)
}

if (error) {
return (
<div>error</div>
)
}

return (
<div
>

{users.map(u => (
<div
key={u.name}
>

{u.name}
</div>
))}
</div>

);
};

export default App;

对比最开始的代码,是不是简单了很多。


useRequest.tsx代码也很简单,首先使用了范型限制fn只能是一个函数,返回值还必须是Promise。这个hooks返回值loading和error就不说了,主要是data,这个data要求和传进来的方法返回值一致,前面说过,可以使用Awaited<ReturnType<T>>获取函数的返回类型。


image.png


但是上面代码可能会导致bug,看下面代码,如果请求失败,users应该是空的,直接这样使用就会报错了。改造一下,当error为false的时候data为正常类型,error为true的时候data为null,这里可以使用联合类型。


image.png


image.png


image.png


image.png


加了一个判断后,下面就不会报错了。ts在某些时候,真的可以避免一些低级错误,我相信如果没有这个限制,肯定有人在写代码的时候不加判断直接用users。


如果请求接口的函数需要参数怎么办,下面来实现一下。


image.png


使用Parameters获取传入函数的参数类型


image.png


image.png


多个参数也是支持的


image.png


zustand


zustand是一个react状态管理库,使用起来比较简单没啥心智负担,所以我一直在用。


上面带着大家入门了ts的类型推断,下面给大家分享一下zustand的ts定义。我看完zustand源码后,发现这个库的ts定义比功能实现还复杂,这里我只给大家分享ts,具体实现掘金已经有很多大佬写过了,我就不分享了。


先从一个最简单的例子开始


import { create } from 'zustand';

interface State {
count: number;
}

interface Action {
inc: () => void;
}

export const useStore = create<State & Action>((set) => ({
count: 1,
inc: () => set((state) => ({count: state.count + 1})),
}));

image.png


create方法的定义


type Create = {
<T, Mos extends [StoreMutatorIdentifier, unknown][] = []>(
initializer: StateCreator<T, [], Mos>,
): UseBoundStore<Mutate<StoreApi<T>, Mos>>
<T>(): <Mos extends [StoreMutatorIdentifier, unknown][] = []>(
initializer: StateCreator<T, [], Mos>,
) =>
UseBoundStore<Mutate<StoreApi<T>, Mos>>
/**
* @deprecated Use `useStore` hook to bind store
*/

<S extends StoreApi<unknown>>(store: S): UseBoundStore<S>
}

可以看到create有三个重载方法,最后一个废弃不用了,上面例子使用的是第一个方法,第二个重载方法可以这样使用。


image.png


这样做的意义和中间件有关系,这个后面再说。


 <T, Mos extends [StoreMutatorIdentifier, unknown][] = []>(
initializer: StateCreator<T, [], Mos>,
): UseBoundStore<Mutate<StoreApi<T>, Mos>>

我们先看第一个方法,定义了两个范型,T表示返回值类型,对应上面例子中create<State & Action>,Mos是给中间件用的,这个等会再说。


create方法的参数initializer定义


initializer: StateCreator<T, [], Mos>

参数initializer对应的类型是StateCreator


export type StateCreator<
T,
Mis extends [StoreMutatorIdentifier, unknown][] = [],
Mos extends [StoreMutatorIdentifier, unknown][] = [],
U = T,
> = ((
setState: Get<Mutate<StoreApi<T>, Mis>, 'setState', never>,
getState: Get<Mutate<StoreApi<T>, Mis>, 'getState', never>,
store: Mutate<StoreApi<T>, Mis>,
) =>
U) & { $$storeMutators?: Mos }

StateCreator定义了4个范型,T还是表示返回值类型,其余三个暂时用不到。


((
setState: Get<Mutate<StoreApi<T>, Mis>, 'setState', never>,
getState: Get<Mutate<StoreApi<T>, Mis>, 'getState', never>,
store: Mutate<StoreApi<T>, Mis>,
) =>
U) & { $$storeMutators?: Mos }

这段ts表明,initializer是一个函数,并且有三个参数,& { $$storeMutators?: Mos }表示交叉类型,也就是说这个函数可能会有$$storeMutators属性。


举个例子:


image.png


因为函数上没有$$name属性,所以报错了,下面给函数加上属性就可以了


image.png


setState: Get<Mutate<StoreApi<T>, Mis>, 'setState', never>

type Get<T, K, F> = K extends keyof T ? T[K] : F

定义了一个Get类型,表示K如果在T对象的可以中,则返回K属性对应的值类型,如果不在返回F。


看个例子


image.png


因为T对象中没有count属性,所以返回never,never表示不存在的类型。


image.png


因为T对象中有name属性,所以返回name字段对应的类型string。


Mutate<StoreApi<T>, Mis>

Mutate这个类型很复杂,是为了解决中间件类型提示出现的,后面再说,没有使用中间件的情况下可以把这段代码简化为StoreApi<T>


export interface StoreApi<T> {
setState: SetStateInternal<T>
getState: () => T
getInitialState: () => T
subscribe: (listener: (state: T, prevState: T) => void) => () => void
/**
* @deprecated Use `unsubscribe` returned by `subscribe`
*/

destroy: () => void
}

type SetStateInternal<T> = {
_(
partial: T | Partial<T> | { _(state: T): T | Partial<T> }['_'],
replace?: boolean | undefined,
): void
}['_']

到这里我们就看到前面例子中set的定义了,set方法有两个参数,第一个参数可以是前面范型定义的一个对象,可以是对象中的一些属性,也可以是一个函数。第二个属性表示是否覆盖整个对象。


这里的["_"]让我有点迷惑,不知道有啥作用,也可以写成下面这样。


type SetStateInternal<T> = (
partial: T | Partial<T> | { _(state: T): T | Partial<T> }['_'],
replace?: boolean | undefined,
) =>
void

set竟然可以直接设置值,看完源码后,我才知道可以这样用,一般我都是用函数,然后使用函数返回值更新值。


image.png


create方法的返回值类型定义


UseBoundStore<Mutate<StoreApi<T>, Mos>>

上面说了没有中间件的情况下,可以简化为:UseBoundStore<StoreApi<T>>


export type UseBoundStore<S extends WithReact<ReadonlyStoreApi<unknown>>> = {
(): ExtractState<S>
<U>(selector: (state: ExtractState<S>) => U): U
/**
* @deprecated Use `createWithEqualityFn` from 'zustand/traditional'
*/

<U>(
selector: (state: ExtractState<S>) => U,
equalityFn: (a: U, b: U) => boolean,
): U
} & S

type ExtractState<S> = S extends { getState: () => infer T } ? T : never

create返回值是一个函数,这个函数有三个重载方法,并且方法上还有一些属性,(& S)表示这些属性。


第一个重载方法表示没有参数时直接返回ExtractState<S>,ExtractState其实就是获取S对象中getState的返回值类型。


image.png


第二个重载方法有一个参数,可以返回自定义属性。


image.png


第三个重载方法废弃了,就不说了。


image.png


上图中useStore之所以有setState和getState等属性,就是上面& S的作用。


create第二个重载方法的作用


zustand支持使用中间件和编写中间件,看完官方持久化persist中间件的ts定义后,直接把我CPU干烧了,太复杂。


先看一下前面说过的,为啥create方法加了一个重载方法。


<T>(): <Mos extends [StoreMutatorIdentifier, unknown][] = []>(
initializer: StateCreator<T, [], Mos>,
) =>
Mutate<StoreApi<T>, Mos>

这个重载方法主要是给使用了中间件的情况下使用的,看一个例子。


image.png


image.png


上面例子中使用了官方提供的持久化中间件,如果使用第一种重载方法会报错,使用第二种就会报错,下面我们来分析一下为啥会这样。


先给上面代码简化一下


function a() {
console.log('hello');
}
type Fn = {
<T, U extends any[] = []>(name: U): T;
<T>(): <U extends any[] = []>(name: U) => T;
};

const b = a as Fn;

b(['hello'])

image.png


这时候我们调用第一个重载方法没有报错,加了范型后就报错了。


image.png


这是因为不使用范型的时候,编辑器会自动推导类型,如果传了一个范型,那么 U extends any[] = []会强制使用默认值[],所以传['hello']会报错。传[]就不会报错了。第二个重载方法的意义就是给两个范型拆开,这样设置了T不会应用U。


image.png


回到上面问题再看一下create方法的参数类型


image.png


因为传了一个范型约束,所以第二个参数使用默认值[]了


image.png


然而persist中间件返回值类型Mos不为[],所以报错了


image.png


针对这个问题,有两个解决方案


第一个方案是把范型去掉,把范型写在persist上。


image.png


第二个方案是用第二个重载方法


image.png


中间件返回值的类型定义


前面有个东西没说,create返回值里的Mutate<StoreApi<T>, Mos>是干嘛用的,先看下代码


export type Mutate<S, Ms> = number extends Ms['length' & keyof Ms]
? S
: Ms extends []
? S
: Ms extends [[infer Mi, infer Ma], ...infer Mrs]
? Mutate<StoreMutators<S, Ma>[Mi & StoreMutatorIdentifier], Mrs>
: never

第一次看这个的时候,直接给我看懵了,这是啥,怎么还有递归,然后恶补了一下ts类型体操知识,顺便把github上类型体操题目刷了一下,然后再回来看这个类型体操就很简单了。


先写一个简单的例子让大家入门一下类型体操,合并数组中的对象类型。



// 写一个类型给a转换为{name: string, age: number}

type a = [{ name: string }, { age: number }];

// infer 可以理解为定义一个变量,
// infer F 表示取出数组中第一个元素,
// ...infer R表示把数组中剩余的元素放到R中,
// S & F 表示把S和F合并,
// C<R, S & F>递归剩余元素也合并S中
// 最后返回S

type C<T extends any[] = [], S = {}> = T extends [infer F, ...infer R] ? C<R, S & F> : S


image.png


理解了这个,那上面Mutate也就好理解了。


number extends Ms['length' & keyof Ms] ? S : : Ms extends [] ? S : ...这段表示如果Ms的类型为any[]则返回S,如果Ms为[]也返回S。


正常我们没有使用中间件的时候,Ms是[],所以直接返回S也就是StoreApi<State & Action>


当使用中间件的时候,我们先看下persist返回值类型。


image.png


persist中间件源码中的类型定义


image.png


根据create方法initializer参数定义Mos被自动推导成了[["zustand/persist", State & Action]],Mos对应Mutate里的Ms。


image.png


Ms extends [[infer Mi, infer Ma], ...infer Mrs]

对比上面的Ms类型,Mi为"zustand/persist",Ma为State & Action,Mrs为[]。


Mutate<StoreMutators<S, Ma>[Mi & StoreMutatorIdentifier], Mrs>

接下来开始递归了,StoreMutators<S, Ma>[Mi & StoreMutatorIdentifier],把Mi替换成"zustand/persist",变成StoreMutators<S, Ma>["zustand/persist" & StoreMutatorIdentifier]


image.png


最开始这段代码让很迷惑,因为StoreMutators在项目里定义的是空对象,上面这种写法取不到任何东西。然后我去persist中间件源码里看了一下,原来在persist里给StoreMutators扩展了。


image.png


这几个类型定义可以简单理解为是给Mutate里S添加了persist属性。而persist属性有下面这些方法。


image.png


type Write<T, U> = Omit<T, keyof U> & U

Write表示合并两个类型,如果有重复的key,用后面的覆盖前面。


image.png


可以看到两个对象合并了key,并且name被覆盖成了number类型。


所以当使用persist中间件时,Mutate<StoreApi<T>, Mos>最终类型为StoreApi<T> & { persist: { ... } },所以我们能create返回的值里调用persist里的方法。


image.png


自定义中间件


模拟per中间件,自己也写一个,没有写具体实现,只写了类型定义。


import { StateCreator, StoreMutatorIdentifier } from 'zustand';

type Test = <
T,
Mps extends [StoreMutatorIdentifier, unknown][] = [],
Mcs extends [StoreMutatorIdentifier, unknown][] = [],
U = T
>(
initializer: StateCreator<T, [...Mps, ['test', unknown]], Mcs>
) =>
StateCreator<T, Mps, [['test', U], ...Mcs]>;

type Write<T, U> = Omit<T, keyof U> & U;

declare module 'zustand' {
interface StoreMutators<S, A> {
test: Write<S, {test: {log: () => void}}>;
}
}

function a() {
console.log(444);
}

export const test = a as unknown as Test;

image.png


在中间件中也可以重写setState方法


image.png


image.png


总结


到此终于结束了,最复杂的create方法讲完了,其他都是简单的,就不分享了。说实话ts类型定义比代码实现难理解多了,也有可能是我开始的水平不够,所以看起来比较费劲。为了看懂这些ts,我把ts体操类型刷了一遍,现在我感觉自己ts提升了很多。找个时间看一下zod的源码,学习一下它的ts定义。


我看一些ts教程的文章下面,很多人吐槽说TypeScript没有用,个人觉得公司里的业务代码或者个人小项目确实可以不用,但是如果你要开发一个开源框架或组件库,我觉得ts或jsdoc还是有必要的,类型推断和准确的代码提示可以方便用户使用。


作者:前端小付
来源:juejin.cn/post/7339364757386264612
收起阅读 »

很多人 30 岁了,对人情世故的认知水平还停留在上学

工作和事业 当领导夸你“工作完成的不错”时。 一般的回答:谢谢领导夸奖 高情商回答: 在今后的工作中,我会继续加油,认真负责的跟着您干,如果没做好的地方,希望您指正。 工作认真负责是我的本分,主要是您指挥的好,不然我也不会成长得这么快 都是因为您给了我充分的...
继续阅读 »

工作和事业


当领导夸你“工作完成的不错”时。


一般的回答:谢谢领导夸奖


高情商回答:



  1. 在今后的工作中,我会继续加油,认真负责的跟着您干,如果没做好的地方,希望您指正。

  2. 工作认真负责是我的本分,主要是您指挥的好,不然我也不会成长得这么快

  3. 都是因为您给了我充分的信任和支持,所以我才能这么顺利地推进。

  4. 最开始我信心不足,您对我的信任给了我做事的信心和动力。能做成这件事,全靠您的帮助,非常感谢您。以后我会加倍努力。


当领导给你安排 完成不了的工作时


一般的回答:领导我干不了。


高情商回答:



  1. 好的,领导,不过我缺乏经验,有些事情还需要您的指导的帮助

  2. 好的领导,您先给我一个小时,我先整体评估一下难度,初步预估一下事情的周期。

  3. 领导,我现在手头有三个项目,你看如果我做现在这个项目的话,怕是会影响其他项目的进度和质量,同事 XXX 在某些方面比较专业,您看是否可以让他负责。


炫耀


对利益相关的人


要展示你的实力和智力


对你利益不相关的人


就展示你的礼貌就好


工资一万,对家里说 7000,对外人说 4000。程序员不要炫富,你那点工资差远了。


规则和事实


当事实对你有利,就强调事实


当规则对你有利,就强调规则


当事实和规则都对你不利


就敲桌子把事情搅混


求人帮忙



  • 求人办事,关系再好,也要让对方得到利益。 铁哥们也不例外,至少请一顿饭,当面感谢。

  • 找人办事,别一上来就说等办完事,再给什么好处,办事之前就要给到。

  • 不要轻易得罪别人,虽然他不能帮你成事,但是可能坏你的事。尤其是领导身边重要的心腹。


想要强大,必须出丑。出丑越多,脸皮越厚,成长越快


生活


管住嘴



  • 少跟妈妈说难过的事,她帮不上忙,也会睡不着觉

  • 别人一对你好,你就推心置腹的毛病一定要改

  • 交浅言深,是人际交往的大忌讳

  • 亲朋好友的孩子再不对,也不要去教育,因为教育别人家的孩子就是在打别人的脸


帮忙


别人不开口请你帮忙,尽量不要主动帮忙!


别人求你帮忙,你先探探对方的口风,看他的态度和想法,看看对方是在寻求你的意见,也许对方只是想寻求你认可他的想法。


见人说人话


遇到女人一律说对方瘦了


遇到穷人,一律说钱不重要,快乐就好


遇到美女夸有内涵,身材好、气质好


遇到带孩子,夸小孩聪明伶俐,夸小孩带的好,


遇到帅哥,夸有才华,有风度。


遇到病人夸气色好,很快就会痊愈


遇到企业家,夸有情怀


遇到小职员,夸有格局


遇到富人,夸他有眼光,有品位


饭局



  • 饭桌上一直玩手机的聚会,就这一次没有下次,没有意义的社交无需留念

  • 去别人家吃饭,饭后不要帮忙刷碗

  • 除非是铁哥们,否则不要临时通知别人去聚餐,别人会认为自己是凑数的!

  • 聚餐时,一定不要夹盘子里的最后一口菜

  • 坐同事的车,只坐副驾驶,不坐在后排。

  • 车子收拾的很干净的人,大多数不好客


作者:五阳
来源:juejin.cn/post/7338723726838218771
收起阅读 »

停止使用 localStorage !

web
medium 优秀文章翻译,也增加自己的一些使用体验。 非标题党!本文标题很明确的想表达对 localStorage 的不推荐。 localStorage 的弊病 2009年 localStorage 诞生,简单来说就是 5MB 的字符串格式的存储。让我拆...
继续阅读 »

medium 优秀文章翻译,也增加自己的一些使用体验。




非标题党!本文标题很明确的想表达对 localStorage 的不推荐。


localStorage 的弊病


2009年 localStorage 诞生,简单来说就是 5MB 的字符串格式的存储。让我拆解下定义中的关键点。



  • 字符串集合:它只能存储字符串。如果你想要存储或者检索其他格式数据,你必须进行序列化和反序列化。假如你忘记了这一点,你将会遇到各种各样的网络Bug。例如当你存储 true 和 false 时,你还要注意处理 null、undefined、空字符串等潜在返回值。

  • 非结构化数据:JavaScript 结构化克隆算法用于复制复杂的 JavaScript 对象的算法。它通过递归输入对象来构建克隆,同时保持先前访问过的引用的映射,以避免无限遍历循环。postMessage, WebWorkers, IndexedDB, Caches API, BroadcastChannel, Channel Messaging API, MessagePortHistory API 都是采用的结构化数据! 采用该结构化数据就是为了解决序列化和反序列化 JSON 带来的问题。很遗憾的是 localStorage 并没有更新该特性,并且未来也没有推进的计划。

  • 安全妥协:你永远不会在任何持久化存储中保存敏感数据,但是开发者依旧会在 localStorage 中保存 Session IDs,JWTs、API keys 等敏感信息。这是个常见的安全隐患,你可以在 window.localStorage 中随意查阅。




  • 性能:localStorage 的性能相对于之前已经有了很好的优化,但是对于超量事务的并发应用程序来说,其性能瓶颈一样要重点考虑。

  • 大小限制:localStorage 有 5MB 的限制,而且可能被浏览器删除。对于现代应用来说,5MB 是很小的容量了,几乎很难存储任何媒体数据。它并不是一成不变的,在一些场景下,浏览器也会主动删除部分持久化存储中的数据,这是个通病,这也是何为常规日志上报会有数据丢失的原因之一。因此有甚至需要主动去管理这部分的数据的生命周期,尽管没人告诉你要做这个。还有个点就是存储剩余容量是无法查询的,因此你无法确定操作是否会以为容量达到上限而无法完整写入。

  • WebWorker 无法访问:localStorage 并不是面向未来的API,也不是适用于并放进程中。

  • 非原子化:localStorage 不保证并行操作中的原子性,也没有任何锁能够保证正在写入的数据不会被覆盖。

  • 无数据隔离:localStorage 仅仅是个字符串的对象,应用下所有数据都被混淆在一起,无法进行数据隔离。

  • 无事务:常规数据库都会支持事务操作,也没办法进行分组。所有操作都是同步的、非独立的、无锁定的。

  • 同步阻塞操作:localStorage 不是异步的,它会阻塞主进程。频繁的读取甚至会影响动画的流畅性,在移动端设备最为明显


WebSQL 何去何从?



WebSQL 目标是为 Web 提供一个简单的 SQL 数据库接口,但是浏览器支持程度确实不好。


你可能好奇它为啥会被抛弃?



  • 单一浏览器厂商实现:WebSQL 主要是 Chrome 和 Safari 实现的,由于 Mozilla 和 Microsoft 不支持,业内开发者几乎不采用它。

  • 非 W3C 标准:这个是至关重要的,W3C 在 2010 年将它从标准中移除了。

  • 与 IndexedDB 的竞争:IndexedDB 主要获得更多的关注,且被设计成标准的跨浏览器解决方案了。

  • 安全问题:一些开发人员和安全专家对WebSQL的安全性表示担忧。他们在很多方面都持怀疑态度,包括缺乏权限控制和SOL风格的漏洞。


最终 IndexedDB 成为浏览器存储的标准,被评价为强壮的、跨浏览器友好。但是大多数经验丰富的开发者都视其为瘟疫,那这种推荐又有什么意义呢?


Cookies 又如何呢?


cookie是1994年由网景公司的网络浏览器程序员卢·蒙图利(Lou Montulli)创建的。


本篇文章的标题实际应该是“停止使用 localStorage 和 Cookie”,但是又不全对,我们应该使用安全的 cookies。



  • 4KB体积限制

  • 默认会被请求传输:非跨域 HTTP 请求会携带 cookie 数据,假如数据不需要被每个请求传输,就会带来带宽开销,导致网络加载速度变慢。

  • 安全隐患:cookie更容易受到XSS的攻击。由于cookie会自动包含在对域的每个请求中,因此它们可能成为恶意脚本的目标。

  • 过期:cookie被设计为在给定日期过期。


IndexedDB 呢?



  • 更好的性能:IndexedDB 操作是异步的,不和阻塞主进程。API 被设计为了事件驱动的。

  • 充足的存储配额:与localStorage的5MB上限相比,IndexedDB提供了更大的存储配额(取决于浏览器、操作系统和可用存储。

  • 可靠且结构化数据:Indexed 减少了强制类型转换,并且采用结构化克隆算法,保证数据的完整。



但是你大概并不想直接使用 IndexedDB。


IndexedDB 大概是避免过多依赖的例外。将 IndexedDB 视为后台数据库,你需要的是 ORM 或者 数据库处理程序来进行查询的管理。由于 IndexedDB 糟糕的 API 设计,你更想要一个 IndexedDB 库。



  • 基于 Promise

  • 更好使用

  • 减少样板代码

  • 关注于更关键的部分


本文比较推荐 dexie.js 和 idb 两个针对 indexedDB 的封装库,其中 idb 的体积是最小的,仅仅 1.19 KB,并不会给程序带来负担。


总结


本文的口号虽然是“停止使用 localStorage”,但是在这个时代实际是难以实现的,但是我们确实应该朝着这个目标出发。


未来开发者应该从 Promise()、async/await 和结构化数据中或者更加清晰且有意义的知识,而不应该关注为何数字“0”在条件语句中会成为“true”,而不应该愤怒与客户获得 null 的返回值。


由于 IndexedDB 的性能优势,你存储各种类型的数据,甚至可以使用游标来遍历所有对象。基于这种技术,你甚至可以构建客户端的搜索引擎,而不会像 localStorage 那样影响动画渲染。



IndexedDB is commonly described as “low-level” . There’s absolutely nothing low-level about IndexedDB, it’s just an API with an old-style and unfriendly syntax. But that doesn't negate it’s underlying capabilities, hence common library usage.



你并不需要直接使用 API,一个体积很小的封装库可以帮助你规避这些。


作者:三省法师
来源:juejin.cn/post/7338422591518457871
收起阅读 »

不可不知的Redis秘籍:事务命令全攻略!

在数据处理的世界里,事务(Transaction)是一个不可或缺的概念。它们确保了在一系列操作中,要么所有的操作都成功执行,要么都不执行。这就像是一个“全有或全无”的规则,保证了数据的一致性和完整性。今天,我们就来聊聊Redis事务的使用,看看如何通过它来提升...
继续阅读 »

在数据处理的世界里,事务(Transaction)是一个不可或缺的概念。它们确保了在一系列操作中,要么所有的操作都成功执行,要么都不执行。这就像是一个“全有或全无”的规则,保证了数据的一致性和完整性。

今天,我们就来聊聊Redis事务的使用,看看如何通过它来提升我们的数据操作效率和安全性。

一、Redis事务的概念

Redis 事务的本质是一组命令的集合。事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。

Description

总结来说: redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令。

Redis事务没有隔离级别的概念

批量操作在发送 EXEC 命令前被放入队列缓存,并不会被实际执行,也就不存在事务内的查询要看到事务里的更新,事务外查询不能看到。

Redis不保证原子性

Redis中,单条命令是原子性执行的,但事务不保证原子性,且没有回滚。事务中任意命令执行失败,其余的命令仍会被执行。

redis事务的执行阶段

  • 开始事务(multi)。
  • 命令入队。
  • 执行事务(exec)

Description

二、Redis事务优缺点

对于Redis事务的概念我们已经有了基本的了解,下面我们再来看看它都有哪些优缺点。

优点:

  • 一次性按顺序执行多个Redis命令,不受其他客户端命令请求影响;

  • 事务中的命令要么都执行(命令间执行失败互相不影响),要么都不执行(比如中间有命令语法错误);

缺点:

  • 事务执行时,不能保证原子性;

  • 命令入队每次都需要和服务器进行交互,增加带宽;

注意:

  • 当事务中命令语法使用错误时,最终会导致事务执行不成功,即事务内所有命令都不执行;

  • 当事务中命令知识逻辑错误,就比如给字符串做加减乘除操作时,只能在执行过程中发现错误,这种事务执行中失败的命令不影响其他命令的执行。

三、Redis事务相关命令

Redis事务可以通过一系列命令来执行多个操作,并确保这些操作可以原子性地执行。以下是Redis事务的相关命令及其作用:

MULTI: 开启一个事务。在调用此命令后,Redis 会将后续的命令逐个放入队列中,直到接收到 EXEC 命令为止。

EXEC: 执行事务中的所有操作命令。一旦调用 EXEC 命令,Redis 会原子性地执行队列中的所有命令。

DISCARD: 取消事务,放弃执行事务块中的所有命令。如果不想继续执行事务中的操作,可以使用 DISCARD 命令来清除当前事务队列。

WATCH: 监视一个或多个键,如果在事务执行之前这些键被其他命令所改动,那么事务将会被打断。

UNWATCH: 取消所有由 WATCH 命令监视的键。如果不想继续监视某些键,可以使用 UNWATCH 命令来取消监视。

需要注意的是,在事务执行过程中,其他客户端提交的命令请求不会插入到事务执行命令序列中,这保证了事务的隔离性。同时,Redis 事务提供了批量操作缓存的功能,即在发送 EXEC 命令前,所有操作都会被放入队列缓存。

在这里给大家分享一下【云端源想】学习平台,无论你是初学者还是有经验的开发者,这里都有你需要的一切。包含课程视频、知识库、微实战、云实验室、一对一咨询等等,现在功能全部是免费的,点击这里,立即开始你的学习之旅!

四、Redis事务的使用

使用Redis事务的步骤如下:

  • 使用MULTI命令开启一个事务。

  • 在事务中执行需要的命令,如SET、GET等。

  • 使用EXEC命令提交事务,将事务中的命令一次性发送给Redis服务器执行。

  • 如果需要取消事务,可以使用DISCARD命令。

Description

下面通过一些示例来讲解一下这些命令的使用方法:

1、正常执行

192.168.xxx.21:6379> multi
OK
192.168.xxx.21:6379> set aa AA
QUEUED
192.168.xxx.21:6379> set bb BB
QUEUED
192.168.xxx.21:6379> set cc CC
QUEUED
192.168.xxx.21:6379> set dd DD
QUEUED
192.168.xxx.21:6379> exec
1) OK
2) OK
3) OK
4) OK
192.168.xxx.21:6379> get aa
"AA"

首先,通过执行multi命令开始一个事务块。然后,依次执行了四个set命令,将键"aa"、“bb”、“cc"和"dd"分别设置为对应的值"AA”、“BB”、“CC"和"DD”。

每个set命令执行后返回的结果为"QUEUED",表示该命令已被加入到事务队列中等待执行。

接下来,通过执行exec命令来提交事务,一次性执行事务队列中的所有命令。执行结果为每个命令的返回值,即"OK"。最后,通过执行get aa命令获取键"aa"的值,返回结果为"AA"。

2、取消事务

192.168.xxx.21:6379> multi
OK
192.168.xxx.21:6379> set aa 11
QUEUED
192.168.xxx.21:6379> set ee EE
QUEUED
192.168.xxx.21:6379> discard
OK
192.168.xxx.21:6379> get aa
"AA"
192.168.xxx.21:6379> get ee
(nil)
192.168.xxx.21:6379>

示例代码中,首先,通过执行multi命令开始一个事务块。然后,依次执行了两个set命令,将键"aa"设置为值"11",将键"ee"设置为值"EE"。每个set命令执行后返回的结果为"QUEUED",表示该命令已被加入到事务队列中等待执行。

接下来,通过执行discard命令来取消事务,放弃执行事务块内的所有命令。执行结果为"OK"。

最后,通过执行get aa命令获取键"aa"的值,返回结果为"AA"。而执行get ee命令获取键"ee"的值时,由于之前已经取消了事务,所以返回结果为"(nil)",表示该键不存在。

3、事务队列中存在命令错误

如果在事务队列中存在命令性错误(类似于java编译性错误),则执行EXEC命令时,所有命令都不会执行

192.168.xxx.21:6379> multi
OK
192.168.xxx.21:6379> set aa 22
QUEUED
192.168.xxx.21:6379> set bb 33
QUEUED
192.168.xxx.21:6379> setq cc 44
(error) ERR unknown command 'setq'
192.168.xxx.21:6379> set ff FF
QUEUED
192.168.xxx.21:6379> exec
(error) EXECABORT Transaction discarded because of previous errors.
192.168.xxx.21:6379> get ff
(nil)
192.168.xxx.21:6379> get bb
"BB"
192.168.xxx.21:6379>

首先,通过执行multi命令开始一个事务块。然后,依次执行了三个set命令,将键"aa"设置为值"22",将键"bb"设置为值"33",将键"cc"设置为值"44"。每个set命令执行后返回的结果为"QUEUED",表示该命令已被加入到事务队列中等待执行。

然而,在执行第三个set命令时,出现了错误。因为Redis中并没有名为"setq"的命令,所以返回结果为"(error) ERR unknown command ‘setq’"。

接下来,通过执行exec命令来提交事务,一次性执行事务队列中的所有命令。由于之前已经出现了错误,导致事务被中断,所以执行结果为"(error) EXECABORT Transaction discarded because of previous errors."。

最后,通过执行get ff命令获取键"ff"的值时,由于事务被中断,所以返回结果为"(nil)“,表示该键不存在。而执行get bb命令获取键"bb"的值时,由于事务被中断,所以返回结果为"BB”。

4、事务队列中存在语法错误

如果在事务队列中存在语法性错误(类似于java的1/0的运行时异常),则执行EXEC命令时,其他正确命令会被执行,错误命令抛出异常。

192.168.xxx.21:6379> multi
OK
192.168.xxx.21:6379> incr aa
QUEUED
192.168.xxx.21:6379> set ff FF
QUEUED
192.168.xxx.21:6379> set bb 22
QUEUED
192.168.xxx.21:6379> exec
1) (error) ERR value is not an integer or out of range
2) OK
3) OK
192.168.xxx.21:6379> get bb
"22"
192.168.xxx.21:6379> get ff
"FF"
192.168.xxx.21:6379>

错误原因:字符串不能累加1

5、watch监控

watch 命令可以监控一个或多个键,一旦有其中一个键被修改(被删除),后面的事务就不会执行了。监控一直持续到 EXEC 命令(事务中的命令是在exec之后才执行的,所以在multi命令后可以修改watch监控的键值)

假设我们通过watch命令在事务执行之前监控了多个Keys,倘若在watch之后有任何Key的值发生了变化,exec命令执行的事务都将被放弃,同时返回Null multi-bulk应答以通知调用者事务执行失败。

(1)、执行watch,不执行multi、exec

192.168.xxx.21:6379> get aa
"AA"
192.168.xxx.21:6379> watch aa
OK
192.168.xxx.21:6379> set aa 11
OK
192.168.xxx.21:6379> get aa
"11"
192.168.xxx.21:6379>

(2)、执行 watch 命令,通知执行 MULTI、exec

192.168.xxx.21:6379> set aa Aa
OK
192.168.xxx.21:6379> get aa
"Aa"
192.168.xxx.21:6379> multi
OK
192.168.xxx.21:6379> set aa 11
QUEUED
192.168.xxx.21:6379> exec
(nil)
192.168.xxx.21:6379> get aa
"Aa"
192.168.xxx.21:6379>

(3)、exec 执行之后,会自动执行 UNWatch 命令,撤销监听操作

192.168.xxx.21:6379> set aa Aa
OK
192.168.xxx.21:6379> get aa
"Aa"
192.168.xxx.21:6379> multi
OK
192.168.xxx.21:6379> set aa 11
QUEUED
192.168.xxx.21:6379> exec
(nil)
192.168.xxx.21:6379> get aa
"Aa"
192.168.xxx.21:6379> set aa 11
OK
192.168.xxx.21:6379> get aa
"11"
192.168.xxx.21:6379>

(4) 、unwatch撤销监听

192.168.xxx.21:6379> get bb
"BBB"
192.168.xxx.21:6379> watch bb
OK
192.168.xxx.21:6379> multi
OK
192.168.xxx.21:6379> unwatch
QUEUED
192.168.xxx.21:6379> set bb 222
QUEUED
192.168.xxx.21:6379> exec
1) OK
2) OK
192.168.xxx.21:6379> get bb
"222"
192.168.xxx.21:6379>

以上就是Redis事务的概念及相关命令的使用,Redis事务是一个非常强大的工具,它可以帮助我们在处理数据的时候保持数据的一致性和完整性。通过使用Redis事务,可以让我们的数据操作更高效、更安全。

希望这篇文章能够帮助你更好地理解和使用Redis事务!

收起阅读 »

在高德地图上使用threejs,tweenjs,引入外部模型,实现动画效果

web
init展示地图展示地图-入门教程-地图 JS API 2.0|高德地图API (amap.com) 在vue3项目中使用新版高德地图_vue3使用高德地图-CSDN博客踩坑:KEY异常,错误信息:USERKEY_PLAT_NOMATCH ...
继续阅读 »

init

展示地图

展示地图-入门教程-地图 JS API 2.0|高德地图API (amap.com) 在vue3项目中使用新版高德地图_vue3使用高德地图-CSDN博客

踩坑:KEY异常,错误信息:USERKEY_PLAT_NOMATCH 原因:申请的key和使用的服务不匹配,展示地图使用JS-API的key,地理信息解析是web服务

  1. npm包安装
npm i @amap/amap-jsapi-loader --save
  1. 引入
import AMapLoader from '@amap/amap-jsapi-loader';
  1. 初始化
var AMap, map
window._AMapSecurityConfig = {
securityJsCode: "d0543d6e1c9f40e8272aa30af54e8ded",
};
AMapLoader.load({
key: "d0543d6e1c9f40e8272aa30af54e8ded", //申请好的Web端开发者key,调用 load 时必填
version: "2.0", //指定要加载的 JS API 的版本,缺省时默认为 1.4.15
})
.then((res) => {
//JS API 加载完成后获取AMap对象
AMap = res
initMap()
})
.catch((e) => {
console.error(e); //加载错误提示
});
function initMap () {
map = new AMap.Map("map", {
viewMode: '2D', //默认使用 2D 模式
zoom: 11, //地图级别
center: [116.397428, 39.90923], //地图中心点,背景天安门为例
});
}

此时,一个平平无奇的高德地图跃然纸上

Pasted image 20240221095238.png

结合THREE

自定义图层-GLCustomLayer 结合 THREE-自有数据图层-示例中心-JS API 2.0 示例 | 高德地图API (amap.com)

环境搭建:

  1. 高德地图环境搭建看上一章
  2. three环境搭建 :一定要下载对应版本的three,在官网示例中可以查看其引入的three版本
npm i three@0.142 # 24/2/1日数据

入门小案例-引入外部模型

照搬官网案例就行

我这里做了些许改动

  1. 引入外部模型猴头
  2. 创建mesh,参考官网案例
  3. 添加移动功能,将猴头移动入mesh中

效果如图

Pasted image 20240222161502.png 代码如下:

Pasted image 20240222161509.png

  1. 引入
import AMapLoader from '@amap/amap-jsapi-loader'
import * as THREE from 'three';
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
import {reactive } from 'vue'
  1. 地图准备
// step map 
var AMap, map

window._AMapSecurityConfig = {
securityJsCode: "d0543d6e1c9f40e8272aa30af54e8ded",
};
AMapLoader.load({
key: "d0543d6e1c9f40e8272aa30af54e8ded", //申请好的Web端开发者key,调用 load 时必填
version: "2.0", //指定要加载的 JS API 的版本,缺省时默认为 1.4.15
})
.then((res) => {
//JS API 加载完成后获取AMap对象
AMap = res
createMap() // 创建地图
createThree() // 创建three
})
.catch((e) => {
console.error(e); //加载错误提示
});
function createMap () {
map = new AMap.Map("map", {
center: [116.54, 39.79],
zooms: [2, 20],
zoom: 14,
viewMode: '3D',
pitch: 50,
});
}

  1. three 准备 核心内容是创建GL图层,里面的 render 基本没什么变化
// step three init
var camera, renderer, scene
var model, monkey, mesh
// 数据转换工具
var customCoords
// 测试用数据
var data
function createThree () {
customCoords = map.customCoords;
data = customCoords.lngLatsToCoords([
[116.52, 39.79],
[116.54, 39.79],
[116.56, 39.79],
])
// 创建 GL 图层
var gllayer = new AMap.GLCustomLayer({
// 图层的层级
zIndex: 10,
// 初始化的操作,创建图层过程中执行一次。
init: (gl) => {
initThree(gl)
},
render: () => {
// 这里必须执行!!重新设置 three 的 gl 上下文状态。
renderer.resetState();
// 重新设置图层的渲染中心点,将模型等物体的渲染中心点重置
// 否则和 LOCA 可视化等多个图层能力使用的时候会出现物体位置偏移的问题
customCoords.setCenter([116.52, 39.79]);
var { near, far, fov, up, lookAt, position } =
customCoords.getCameraParams();

// 这里的顺序不能颠倒,否则可能会出现绘制卡顿的效果。
camera.near = near;
camera.far = far;
camera.fov = fov;
camera.position.set(...position);
camera.up.set(...up);
camera.lookAt(...lookAt);
camera.updateProjectionMatrix();

renderer.render(scene, camera);

// 这里必须执行!!重新设置 three 的 gl 上下文状态。
renderer.resetState();
},
});
map.add(gllayer)
window.addEventListener('resize', onWindowResize);
}
function onWindowResize () {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}

然后我们来看initThree

function initThree (gl) {
// 这里我们的地图模式是 3D,所以创建一个透视相机,相机的参数初始化可以随意设置,因为在 render 函数中,每一帧都需要同步相机参数,因此这里变得不那么重要。
// 如果你需要 2D 地图(viewMode: '2D'),那么你需要创建一个正交相机
camera = new THREE.PerspectiveCamera(
60,
window.innerWidth / window.innerHeight,
100,
1 << 30
);

renderer = new THREE.WebGLRenderer({
context: gl, // 地图的 gl 上下文
// alpha: true,
// antialias: true,
// canvas: gl.canvas,
});

// 自动清空画布这里必须设置为 false,否则地图底图将无法显示
renderer.autoClear = false;
scene = new THREE.Scene();

// 环境光照和平行光
var aLight = new THREE.AmbientLight(0xffffff, 3);
var dLight = new THREE.DirectionalLight(0xffffff, 10);
dLight.position.set(1000, -100, 900);
scene.add(dLight);
scene.add(aLight);
// 加载模型、mesh
addModel()
addMesh()
}

以上内容也基本不变,但最后加载模型、mesh按照你需要加载的物体变化。

加载外部模型

function addModel () {
const glftLoader = new GLTFLoader()
glftLoader.load("/public/models/monkeyAndCube.glb", function (gltf) {
model = gltf.scene
model.traverse((child) => {
child.scale.set(500, 500, 500); // 放大模型
child.rotation.x = 0.5 * Math.PI;
child.position.z = 0.8;
console.log(child.name)
if (child.name === "monkey") { monkey = child }
})
monkey.position.set(data[0][0], data[0][1], 500); // 设置位置
scene.add(monkey)
})
}

加载mesh

function addMesh () {
// 这里可以使用 three 的各种材质
var mat = new THREE.MeshLambertMaterial({
side: THREE.DoubleSide,
color: 0x1e2f97,
transparent: true,
opacity: .4,
depthWrite: false
})
var geo = new THREE.BoxBufferGeometry(1200, 1200, 1200);
const d = data[2];
mesh = new THREE.Mesh(geo, mat);
mesh.position.set(d[0], d[1], 500);
scene.add(mesh);
animate()
}
// 动画
function animate () {
mesh.rotateZ((1 / 180) * Math.PI);
map.render();
requestAnimationFrame(animate);
}

移动猴头! 记得自己给这个函数加个按钮

function moveMonkey (checked) {
console.log(checked, 'moveMonkey')
if (checked) {
monkey.position.set(data[2][0], data[2][1], 500);
} else {
monkey.position.set(data[0][0], data[0][1], 500);
}
}

[!TIP] 这里面地图和three好像是一起渲染的。如果你只加载了猴头,没加载mesh,此时是没有动画效果的,所以移动猴头的话,这个效果有延迟,缩放平移一下地图就好了。但是如果添加了动画,由于一直调用 map.render() 函数,因此不会出现此问题

大功告成!

结合tween.js

是不是觉得猴头的移动还不够顺滑,加个tween的动画试试

  1. 引入 npm install @tweenjs/tween.js
import * as TWEEN from '@tweenjs/tween.js'
  1. 更改 moveMonkey函数
function moveMonkey (checked) {
console.log(checked, 'moveMonkey')
if (checked) {
// monkey.position.set(data[2][0], data[2][1], 500);
const tween = new TWEEN.Tween(monkey.position)
.to({ x: data[2][0], y: data[2][1], z: 500 }, 2000)
.start()
} else {
// monkey.position.set(data[0][0], data[0][1], 500);
const tween = new TWEEN.Tween(monkey.position)
.to({ x: data[0][0], y: data[0][1], z: 500 }, 2000)
.start()
}
}
  1. 在 render 函数中加入 ( 这里指Three 函数中的 创建GL图层的 render 函数)
 TWEEN.update()

大功告成!Tween的其他功能也可以使用


作者:写bug的小杜
来源:juejin.cn/post/7338240698703314985

收起阅读 »

应用容器化后为什么性能下降这么多?

1. 背景 随着越来越多的公司拥抱云原生,从原先的单体应用演变为微服务,应用的部署方式也从虚机变为容器化,容器编排组件k8s也成为大多数公司的标配。然而在容器化以后,我们发现应用的性能比原先在虚拟机上表现更差,这是为什么呢?。 2. 压测结果 2.1 容器化之...
继续阅读 »

1. 背景


随着越来越多的公司拥抱云原生,从原先的单体应用演变为微服务,应用的部署方式也从虚机变为容器化,容器编排组件k8s也成为大多数公司的标配。然而在容器化以后,我们发现应用的性能比原先在虚拟机上表现更差,这是为什么呢?。


2. 压测结果


2.1 容器化之前的表现


应用部署在虚拟机下,我们使用wrk工具进行压测,压测结果如下:


image.png


从压测结果看,平均RT1.68msqps716/s\color{red}{平均RT为1.68ms,qps为716/s},我们再来看下机器的资源使用情况,cpu基本已经被打满。
image.png


2.2 容器化后的表现


使用wrk工具进行压测,结果如下:
image.png


从压测结果看,平均RT2.11msqps554/s\color{red}{平均RT为2.11ms,qps为554/s},我们再来看下机器的资源使用情况,cpu基本已经被打满。
image.png


2.3 性能对比结果


性能对比虚拟机容器
RT1.68ms2.11ms
QPS716/s554/s


总体性能下降:RT(25%)、QPS(29%)



3. 原因分析


3.1 架构差异


由于应用在容器化后整体架构的不同、访问路径的不同,将可能导致应用容器化后性能的下降,于是我们先来分析下两者架构的区别。我们使用k8s作为容器编排基础设施,网络插件使用calico的ipip模式,整体架构如下所示。


x3.png


这里需要说明,虽然使用calico的ipip模式,由于pod的访问为service的nodePort模式,所以不会走tunl0网卡,而是从eth0经过iptables后,通过路由到calico的calixxx接口,最后到pod。


3.2性能分析


在上面压测结果的图中,我们容器化后,cpu的软中断si使用率明显高于原先虚拟机的si使用率,所以我们使用perf继续分析下热点函数。


image.png
为了进一步验证是否是软中断的影响,我们使用perf进一步统计软中断的次数。


image.png



我们发现容器化后比原先软中断多了14%,到这里,我们能基本得出结论,应用容器化以后,需要更多的软中断的网络通信导致了性能的下降。



3.3 软中断原因


由于容器化后,容器和宿主机在不同的网络namespace,数据需要在容器的namespace和host namespace之间相互通信,使得不同namespace的两个虚拟设备相互通信的一对设备为veth pair,可以使用ip link命令创建,对应上面架构图中红色框内的两个设备,也就是calico创建的calixxx和容器内的eth0。我们再来看下veth设备发送数据的过程


static netdev_tx_t veth_xmit(struct sk_buff *skb, struct net_device *dev)
{
...
if (likely(veth_forward_skb(rcv, skb, rq, rcv_xdp)
...
}

static int veth_forward_skb(struct net_device *dev, struct sk_buff *skb,
struct veth_rq *rq, bool xdp)
{
return __dev_forward_skb(dev, skb) ?: xdp ?
veth_xdp_rx(rq, skb) :
netif_rx(skb);//中断处理
}


/* Called with irq disabled */
static inline void ____napi_schedule(struct softnet_data *sd,
struct napi_struct *napi)
{
list_add_tail(&napi->poll_list, &sd->poll_list);
//发起软中断
__raise_softirq_irqoff(NET_RX_SOFTIRQ);
}

通过虚拟的veth发送数据和真实的物理接口没有区别,都需要完整的走一遍内核协议栈,从代码分析调用链路为veth_xmit -> veth_forward_skb -> netif_rx -> __raise_softirq_irqoff,veth的数据发送接收最后会使用软中断的方式,这也刚好解释了容器化以后为什么会有更多的软中断,也找到了性能下降的原因。


4. 优化策略


原来我们使用calico的ipip模式,它是一种overlay的网络方案,容器和宿主机之间通过veth pair进行通信存在性能损耗,虽然calico可以通过BGP,在三层通过路由的方式实现underlay的网络通信,但还是不能避免veth pari带来的性能损耗,针对性能敏感的应用,那么有没有其他underly的网络方案来保障网络性能呢?那就是macvlan/ipvlan模式,我们以ipvlan为例稍微展开讲讲。


4.1 ipvlan L2 模式


IPvlan和传统Linux网桥隔离的技术方案有些区别,它直接使用linux以太网的接口或子接口相关联,这样使得整个发送路径变短,并且没有软中断的影响,从而性能更优。如下图所示:


ipvlan l2 mode


上图是ipvlan L2模式的通信模型,可以看出container直接使用host eth0发送数据,可以有效减小发送路径,提升发送性能。


4.2 ipvlan L3 模式


ipvlan L3模式,宿主机充当路由器的角色,实现容器跨网段的访问,如下图所示:


ipvlan L3 mode


4.3 Cilium


除了使用macvlan/ipvlan提升网络性能外,我们还可以使用Cilium来提升性能,Cilium为云原生提供了网络、可观测性、网络安全等解决方案,同时它是一个高性能的网络CNI插件,高性能的原因是优化了数据发送的路径,减少了iptables开销,如下图所示:


cilium netwok


虽然calico也支持ebpf,但是通过benchmark的对比,Cilium性能更好,高性能名副其实,接下来我们来看看官网公布的一些benchmark的数据,我们只取其中一部分来分析,如下图:


xxxx2
xxxx3


无论从QPS和CPU使用率上Cilium都拥有更强的性能。


5. 总结


容器化带来了敏捷、效率、资源利用率的提升、环境的一致性等等优点的同时,也使得整体的系统复杂度提升一个等级,特别是网络问题,容器化使得整个数据发送路径变长,排查难度增大。不过现在很多网络插件也提供了很多可观测性的能力,帮助我们定位问题。


我们还是需要从实际业务场景出发,针对容器化后性能、安全、问题排查难度增大等问题,通过优化架构,增强基础设施建设才能让我们在云原生的路上越走越远。


最后,感谢大家观看,也希望和我讨论云原生过程中遇到的问题。


5. 参考资料


docs.docker.com/network/dri…


cilium.io/blog/2021/0…


作者:云之舞者
来源:juejin.cn/post/7268663683881828413
收起阅读 »

记一种不错的缓存设计思路

之前与同事讨论接口性能问题时听他介绍了一种缓存设计思路,觉得不错,做个记录供以后参考。 场景 假设有个以下格式的接口: GET /api?keys={key1,key2,key3,...}&types={1,2,3,...} 其中 keys 是业务...
继续阅读 »

之前与同事讨论接口性能问题时听他介绍了一种缓存设计思路,觉得不错,做个记录供以后参考。


场景


假设有个以下格式的接口:


GET /api?keys={key1,key2,key3,...}&types={1,2,3,...}


其中 keys 是业务主键列表,types 是想要取到的信息的类型。


请求该接口需要返回业务主键列表对应的业务对象列表,对象里需要包含指定类型的信息。


业务主键可能的取值较多,千万量级,type 取值范围为 1-10,可以任意组合,每种 type 对应到数据库是 1-N 张表,示意:


redis-cache-design.drawio.png


现在设想这个接口遇到了性能瓶颈,打算添加 Redis 缓存来改善响应速度,应该如何设计?


设计思路


方案一:


最简单粗暴的方法是直接使用请求的所有参数作为缓存 key,请求的返回内容为 value。


方案二:


如果稍做一下思考,可能就会想到文首我提到的觉得不错的思路了:



  1. 使用 业务主键:表名 作为缓存 key,表名里对应的该业务主键的记录作为 value;

  2. 查询时,先根据查询参数 keys,以及 types 对应的表,得到所有 key1:tb_1_1key1:tb_1_2 这样的组合,使用 Redis 的 mget 命令,批量取到所有缓存中存在的信息,剩下没有命中的,批量到数据库里查询到结果,并放入缓存;

  3. 在某个表的数据有更新时,只需刷新 涉及业务主键:该表名 的缓存,或令其失效即可。


小结


在以上两种方案之间做评估和选择,考虑几个方面:



  • 缓存命中率;

  • 缓存数量、占用空间大小;

  • 刷新缓存是否方便;


稍作思考和计算,就会发现此场景下方案二的优势。


另外,就是需要根据实际业务场景,如业务对象复杂度、读写次数比等,来评估合适的缓存数据的粒度和层次,是对应到某一级组合后的业务对象(缓存值对应存储 + 部分逻辑),还是最基本的数据库表/字段(存储的归存储,逻辑的归逻辑)。


作者:mzlogin
来源:juejin.cn/post/7271597656118394899
收起阅读 »

HTTP请求头中的Authorization

当使用HTTP请求中的Authorization头时,表示传入的是认证信息。具体的认证类型由凭证前缀指明。以下是Authorization头中常见的几种认证机制: 基本认证(Basic Authentication): Authorization: Basi...
继续阅读 »

当使用HTTP请求中的Authorization头时,表示传入的是认证信息。具体的认证类型由凭证前缀指明。以下是Authorization头中常见的几种认证机制:



  1. 基本认证(Basic Authentication):


    Authorization: Basic base64(用户名:密码)

    这是最常见的一种,涉及将用户名和密码以base64格式编码并与请求一起发送。需要注意,Basic 后面有空格, 未使用HTTPS时基本认证不够安全
    实际使用例子,比如:


    curl -u "admin:P@88w0rd" -H "Accept: application/json" http://localhost:8090/api/v1alpha1/users

    curl -u “username:password” 就相当于在请求的请求头中添加keyAuthorizationvalueadmin:P@88w0rd,这是一种认证方式。
    对账号密码进行base64编码之后


    echo -n "admin:P@88w0rd" | base64

    得到:YWRtaW46UEA4OHcwcmQ=
    ,上方的curl也可以写成:


    curl -H "Authorization: Basic YWRtaW46UEA4OHcwcmQ=" -H "Accept:  application/json" http://localhost:8090/api/v1alpha1/users


  2. Bearer令牌(Bearer Token):


    Authorization: Bearer <令牌>

    这通常与OAuth 2.0一起使用。<令牌>通常是通过单独的认证过程获取的长寿命访问令牌。


  3. 摘要认证(Digest Authentication):


    Authorization: Digest username="用户名", realm="领域", nonce="随机数", uri="URI", response="响应", opaque="opaque", qop=auth, nc=00000001, cnonce="cnonce"

    摘要认证比基本认证更安全,涉及挑战-响应机制来验证客户端。挑战-响应机制(Challenge-Response Mechanism,在这种机制中,服务器通过向客户端发送一个随机的挑战(challenge),并期望客户端使用其凭据(通常是密码)生成一个对应的响应(response)来证明其身份,服务端收到响应后验证身份)


  4. API密钥(API Key):


    Authorization: ApiKey <API密钥>

    API密钥通常用于API请求中的身份验证。密钥包含在Authorization头中。


  5. Bearer令牌(JWT):


    Authorization: Bearer eyJhbGciOiJIUzI1NiIsIn...

    JSON Web Tokens(JWT)通常在现代身份验证系统中使用。令牌包含在Bearer方案中。


  6. 自定义方案(Custom Schemes):
    一些应用程序或服务可能定义了自己的自定义认证方案。例如:


    Authorization: CustomScheme 自定义数据



以上的使用的scheme,如BasicBearer,Digest,ApiKey是约定俗成的,大家都这样使用,具体认证类型取决于服务器的要求和实现的协议,针对自己的业务也可以自定义scheme。也可以参考正在与之交互的服务或API的文档,以确定Authorization头的正确格式。


作者:星夜晚晚
来源:juejin.cn/post/7329573746464718857
收起阅读 »

简单的 Web 端实时日志实现

web
背景 cron service 在执行定时任务时,需要能够实时查看该任务的执行日志以确保程序正确的工作。为了能够在尽可能短的时间内实现该功能,我们需要一个足够简单的方案。 方案如何选择? 我相信大多数开发者第一个想到的就是 WebSocket ,然后是 HTT...
继续阅读 »

Feb-20-2024 21-40-36.gif


背景


cron service 在执行定时任务时,需要能够实时查看该任务的执行日志以确保程序正确的工作。为了能够在尽可能短的时间内实现该功能,我们需要一个足够简单的方案。


方案如何选择?


我相信大多数开发者第一个想到的就是 WebSocket ,然后是 HTTP/SSE 。WebSocket 在很多情况下是实至名归的万金油选择。但这这里他太重了,对服务端和客户端具有侵入性需要花费额外的时间来集成到项目中。


那么 SSE 呢,为什么不是他?
虽然 SSE 即轻量又实时,但 SSE 无法设置请求头。这导致无法使用缓存和通过请求头设置的 Token。而且作为长链接他还一直占用并发额度。



  • WebSocket:❌

    • 优势:

      • 实时性高

      • 不会占用 HTTP 并发额度



    • 劣势:

      • 复杂度较高,需要在客户端和服务器端都进行特殊的处理

      • 消耗更多的服务器资源。





  • SSE(Server-Sent Events):❌

    • 优势:

      • 基于HTTP协议,不需要在服务端和客户端做额外的处理

      • 实时性高



    • 劣势:

      • 无法设置请求头

      • 占用 HTTP 并发额度





  • HTTP:✅

    • 优势:

      • 简单易用,不需要在服务端和客户端做额外的处理。

      • 支持的功能丰富,如缓存,压缩,认证等功能。



    • 劣势:

      • 实时性差,取决于轮询时间间隔。

      • 每次HTTP请求都需要建立新的连接(仅针对 HTTP/0.x 而言)并可能阻塞同源的其他请求而导致性能问题。12

        • HTTP/1.x 支持持久连接以避免每次请求都重新建立 TCP 连接,但数据通信是串行的。

        • HTTP/2.x 支持持久连接且支持并行的数据通信。









以上列出的优缺点是仅对于本文所讨论的场景(即Web 端的实时日志)而言,这三种数据交互技术在其他场景中的优缺点不在本文讨论范围内。



实现


HTTP 轮询已经是老熟人了,不再做介绍。本文的着重于实时日志的实现及优化。


sequenceDiagram

participant 浏览器

participant 服务器

Note right of 浏览器: 首先,获取日志文件最后 X bytes 的内容

浏览器->>服务器: 最新的日志文件有多大?

服务器->>浏览器: 日志文件大小: Y bytes

浏览器->>服务器: 从 Y - X bytes 处返回内容

服务器->>浏览器: 日志文件大小: Y1 bytes, 日志内容: XXXX



loop 持续轮询获取最新的日志

浏览器->>服务器: 从 Y1 bytes 处返回内容

服务器->>浏览器: 日志文件大小: Y2 bytes, 日志内容: XXXX

end

上方是基本工作原理的流程图。
实现的关键点在于



  • 前端如何知道日志文件当前的大小

  • 服务端如何从指定位置获取日志文件内容


这两点都可以通过 HTTP Header 来解决。Content-Range HTTP 响应头会包含完整文件的大小,而 Range HTTP 请求头可以指示服务器返回指定位置的文件内容。


因此在服务端不需要额外的逻辑,仅通过 Web 端代码就可以实现实时日志功能。


代码实现



篇幅限制,代码不会处理异常情况



首先,根据上述流程图。我们需要获取日志文件的大小。


const res = await fetch(URL, {  
method: "GET",
headers: { Range: "bytes=0-0" }
});

const contentRange = res.headers.get("Content-Range") || "";
const match = /bytes (\d+)-(\d+)\/(\d+)/.exec(contentRange);

// 日志文件大小
const total = Number.parseInt(match[3], 10);


Range: "bytes=0-0" 指定仅获取第一个字节的内容,以免较大的日志文件导致响应时间过长。



我们发起了一个 GET 请求并将 Range 请求头设置为 bytes=0-0 如果服务器能够正确处理 Range 请求头,响应中将包含一个 Content-Range 列其中将包含日志文件的完整大小,可以通过正则解析拿到。


现在我们已经拿到了日志文件的大小并存储在名为 total 的变量中。然后根据 total 获取到最后 10 KB 的日志内容。


const res = await fetch(url, {  
method: "GET",
headers: {
Range: `bytes=${total - 1000 * 10}-`
}
});

const contentRange = res.headers.get("Content-Range") || "";
const match = /bytes (\d+)-(\d+)\/(\d+)/.exec(contentRange);

// 下一次请求的起始位置
const start = Number.parseInt(match[2], 10) + 1;

// 日志内容
const content = await res.text();

现在我们发起了一个 GET 请求并将 Range 请求头设置为 bytes=${total - 1000 * 10}- 以获取最后 10 KB 的日志内容。并且通过正则解析拿到了下一次请求的起始位置。


现在我们已经拿到了日志文件的大小和最后 10 KB 的日志内容。接下来就是持续轮询去获取最新的日志。轮询代码区别仅在于将 Range 标头设置为 bytes=${start}- 以便获取最新的日志。


const res = await fetch(url, {  
method: "GET",
headers: {
Range: `bytes=${start}-`
}
});

const contentRange = res.headers.get("Content-Range") || "";
const match = /bytes (\d+)-(\d+)\/(\d+)/.exec(contentRange);

// 下一次请求的起始位置
start = Number.parseInt(match[2], 10) + 1;

// 日志内容
const content = await res.text();

以上,基本的功能已经实现了。日志内容保存在名为 content 的变量中。


优化


HTTP 轮询常因其高延迟为人诟病,我们可以通过指数退避的方式来尽可能的降低延时且不会显著的增加服务器负担。


指数退避是一种常见的网络重试策略,它会在每次重试时将等待时间乘以一个固定的倍数。这样做的好处是,当网络出现问题时,重试的时间间隔会逐渐增加,直到达到最大值。


一个简单的实现如下:


/**
* 使用指数退避策略获取日志.
*/

export class ExponentialBackoff {
private readonly base: number;
private readonly max: number;
private readonly factor: number;
private retries: number;

/**
* @param base 基础延迟时间 默认 1000ms (1秒)
* @param max 最大延迟时间 默认 60000ms (1分钟)
* @param factor 延迟时间增长因子 默认 2
*/

constructor(base: number = 1000, max: number = 60000, factor: number = 2) {
this.base = base;
this.max = max;
this.factor = factor;
this.retries = 0;
}

/**
* 获取下一次重试的延迟时间.
*/

next() {
const delay = Math.min(this.base * Math.pow(this.factor, this.retries), this.max);
this.retries++;
return delay;
}

/**
* 重置重试次数.
*/

reset() {
this.retries = 0;
}
}

值得一提的是带有 Range 标头的请求成功时会返回 206 Partial Content 状态码。而在请求的范围超出文件大小时会返回 416 Range Not Satisfiable 状态码。我们可以通过这两个状态码来判断请求是否成功。


成功时调用 reset 方法重置重试次数,失败时调用 next 方法获取下一次重试的延迟时间。


总结


即使再优秀的方案也不是银弹,真正合适的方案需要考虑的不仅仅是技术本身,还有业务场景,团队技术栈,团队技术水平等等。在选择技术方案时,我们需要权衡各种因素,而不是盲目的选择最流行的技术。


当我们责备现有的技术方案为何如此糟糕时,或许在那个时间点上,这个方案是最合适的。


Pasted image 20240220213326.png


感谢以下网友的帮助和建议:齐洛格德


Footnotes




作者:siaikin
来源:juejin.cn/post/7337519776796295177
收起阅读 »

马斯克接手Twitter一年后的成果-工作量化的重要性

大家好,我是云舒编程。 马斯克接手Twitter的一年后,在10.27其官方团队发布了一条推文展示这一年的工程成果。 有点国内那味了,论工作量化的重要性。 这一年里,我们在工程技术上取得了许多出色的成就,除了大家在应用中看到的明显变化之外,在幕后我们还做了...
继续阅读 »

大家好,我是云舒编程。


马斯克接手Twitter的一年后,在10.27其官方团队发布了一条推文展示这一年的工程成果。



有点国内那味了,论工作量化的重要性。



这一年里,我们在工程技术上取得了许多出色的成就,除了大家在应用中看到的明显变化之外,在幕后我们还做了一系列重要的优化和改进。



  • 将「为你推荐」、「关注」、「搜索」、「个人主页」、「列表」、「社区」和「探索」等功能的技术栈整合到了一个统一的产品框架中。

  • 彻底重建了「为你推荐」的服务和排名系统,代码行数从700K减少到70k,减少了90%;同时计算资源减少了50%,处理相同请求的能力提升了80%。

  • 统一了「为你推荐」和视频的个性化、排名模型,显著提高了视频推荐质量。

  • 重构了API中间层的架构,删除了超过10万行代码和数千个未使用的内部废弃接口,同时删除了一些没人用的客户端服务。

  • 将获取帖子元数据的时间减半,全局API超时错误减少90%。

  • 对外部机器人、爬虫的屏蔽,相比2022年,增长了37%。平均每天,阻止了超过100万次机器人注册,并将私信中的无用信息减少了95%。

  • 关闭了位于萨克拉门托的数据中心,重新调配了5200台机架和148000台服务器,每年为公司节约了超1亿美元,总的来说,节约了48兆瓦的电量,60000磅的网络机架。

  • 优化了对云服务厂商的使用,开始在本地进行更多的工作,这一转变使得每月的云服务成本降低60%,同时我们还将所有的媒体和大文件从云端迁出,减少了60%的云端存储空间,除此之外,还成功将云数据处理成本减少了75%。

  • 构建本地GPU超级计算集群,并设计、交付了43.2Tbps 的高性能网络架构。

  • 提升网络主干的容量和冗余性,每年节省1390万美元。

  • 开展了自动化峰值流量故障转移测试,以持续验证平台的可扩展性和可用性。


作者:云舒编程
来源:juejin.cn/post/7295397683397066762
收起阅读 »

【日常总结】iframe内嵌网站初始路由404解决

web
背景 Vue 项目被嵌入到 <iframe> 中,通过src加载页面,结果打开后直接访问404,但是如果不是通过iframe打开的话是不会出现这个问题的。<iframe src="/dist/index.html"></ifram...
继续阅读 »

背景


Vue 项目被嵌入到 <iframe> 中,通过src加载页面,结果打开后直接访问404,但是如果不是通过iframe打开的话是不会出现这个问题的。

<iframe src="/dist/index.html"></iframe>



问题现象


在首次进入时,Vue Router 没有正确地导航到首页路由,而是到了404页面。




原因


vuerouter内部match当前路径的逻辑在这种情况下有问题,所以取到的是错误的信息,找不到这个path,就跳转到了404。前端路由是要依赖当前路径来做处理的,而路径是依赖location字段,但这个时候location跟我们预期的不一样,就跳转错误。




为什么会这样?


是因为iframe 的加载是异步的,router路径跳转之前iframe还没初始化好,导致router内部获取的当前路径不对。




所以思路有2个:



  1. 路径redirect有问题,那就不依赖它,在url中增加参数设置正确的首次跳转路径

  2. 在 iframe 加载完成时,设置正确的路由信息从而导航过去。所以就从iframe加载完后重新设置导航这个关键点下手。


梳理到这里,我意识到上面两种方案是iframe的通信方式:



  • url传递信息

  • postMessage传递消息


解决方案


方案一:iframe的src的URL 参数中增加初始路由信息



  1. 在 iframe 的 src 属性中通过url参数添加首页的路由信息,
<iframe src="/dist/index.html?route=/dashboard"></iframe>


  1. 再在 Vue 应用程序初始化时,如果读取到了router则跳转过去。
import Vue from 'vue'
import App from './App.vue'
import router from './router'

// 方案1:根据添加的url参数判断
const routeParam = new URLSearchParams(window.location.search).get('route');
if (routeParam) {
router.push(routeParam);
}

new Vue({
router,
render: h => h(App),
}).$mount('#app')

我使用的这种方案。


方案二:使用 postMessage 实现父页面和iframe的通信



  1. 父页面监听iframe加载完后通过postMessage发送消息:
const iframe = document.getElementById('myIframe');
if (iframe) {
iframe.onload = function() {
iframe.contentWindow.postMessage({ route: '/dashboard' }, '*');
};
}


  1. iframe内页面main.ts中监听消息,设置路由
// 方案2:postMessage来实现iframe的通信
window.addEventListener('message', handleMessage, false);

function handleMessage(event: any) {
const { data } = event;
if (data.route) {
console.log('message', event, data.route);
router.push(data.route); // 导航到接收到的路由
}
}

这个方案,也可以fix这个bug,但是会先闪到404,再到首页,原因应该是iframe通信是要放到onmounted钩子里的,因为依赖dom加载完,所以会有个时间差。等到iframe onload时候之前已经redirect到404了,然后又重新route了下,效果不是很好。


方案三:利用路由钩子


利用 Vue Router 的导航守卫,在路由导航前检查 URL 参数中是否存在初始路由信息,如果存在则进行相应的导航处

// 方案3: 路由拦截
router.beforeEach((to, from, next) => {
if (to.matched.length === 0) {
// 如果未匹配到任何路由,说明当前页面可能是初始加载
next('/dashboard/base'); // 或者重定向到默认页面
} else {
next(); // 继续正常导航
}
});

结果


先把今天遇到的问题简单总结下:




  • 最后采用了方案1。




  • 方案2 调试出来了,但是效果会有先闪过404页面,后续又到首页的情况,效果不是很好。




  • 3没调出来,暂时不调了。

































标题优点缺点问题
url加参数简单,直接,只需要在内嵌的项目写代码url结构不好看最终使用
postmenssage不需要修改url需要内嵌iframe的页面和iframe的项目中添加额外代码会有一闪而过的效果,体验不好
beforeEach不需要修改url路由钩子每个都处理了逻辑有问题,没调试出来

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

你不知道的JSON.stringify神操

web
一、前言 在我们面试过程中,面试官经常会提及到深浅拷贝的问题。想必大多数小伙伴会说到JSON.parse(JSON.stringify(obj))。正好今天我就和大家好好唠一唠这个JSON.stringify。 二、概念 JSON.stringify对于我...
继续阅读 »

一、前言



在我们面试过程中,面试官经常会提及到深浅拷贝的问题。想必大多数小伙伴会说到JSON.parse(JSON.stringify(obj))。正好今天我就和大家好好唠一唠这个JSON.stringify



二、概念


JSON.stringify对于我们不陌生,一般用来处理序列化(深拷贝)。就是把我们的对象转换JSON字符串,此方法确实很方便在我们的工作中,但是,这个方法也会有一些弊端,只是我们不怎么遇到。


let obj = {
name: 'iyongbao'
}

console.log(JSON.stringify(obj)); // {"name":"iyongbao"}

三、弊端


1. 对函数不友好


如果我们的对象属性是一个函数,那么在序列化的时候该属性丢失


let obj = {
name: 'iyongbao',
foo: function () {
console.log(`${ this.name }是一个小菜鸟!`)
}
}

console.log(JSON.stringify(obj)); // {"name":"iyongbao"}

2. 对undefined不友好


如果对象的属性值是undefined,转换后会丢失


let obj = {
name: undefined
}

console.log(JSON.stringify(obj)); // {}

3. 对正则表达式不友好


如果对象的属性是一个正则表达式,转换后就会变成一个空的Object


let obj = {
name: 'iyongbao',
zoo: /^i/ig,
foo: function () {
console.log(`${ this.name }是一个小菜鸟!`)
}
}

console.log(JSON.stringify(obj)); // {"name":"iyongbao","zoo":{}}

4. 数组对象


如果是一个数组对象,以上的情况也会发生。


let arr = [
{
name: undefined
}
]

console.log(JSON.stringify(arr)); // [{}]

四、JSON.stringify拓展



说完了JSON.stringify不足,下面我们来说一下你可能没有接触过的其他特性,希望看完会对你有所帮助。



1. 接收一个数组(过滤)


其实JSON.stringify第二参数,可能我们不经常用到。我们可以传入一个数组,值就是对应我们对象key,我称之为过滤。


let obj = {
name: 'iyongbao',
age: 25,
hobby: ['JavaScript', 'Vue']
}

let res = JSON.stringify(obj, ['name']);

console.log(res); // {"name":"iyongbao"}

2. 接收一个函数


第二个参数也可以是一个函数,也是类似过滤效果


let obj = {
name: 'iyongbao',
age: 25,
hobby: ['JavaScript', 'Vue']
}

let res = JSON.stringify(obj, (key, value) => {
if (key === 'age') return undefined;
return value;
});

console.log(res); // {"name":"iyongbao","hobby":["JavaScript","Vue"]}

3. 缩进


第三个参数可以接收一个数字,表示缩进多少字符


let obj = {
name: 'iyongbao',
age: 25,
hobby: ['JavaScript', 'Vue']
}

let res = JSON.stringify(obj, null, 2);

console.log(res);

擷取.PNG


4. 自身toJSON方法


对象可以有一个自身的toJSON属性,是一个返回值方法,用来输出我们自定义的数据样式。


let obj = {
name: 'iyongbao',
age: 25,
toJSON: function () {
return {
message: `${ this.name }的年龄为${ this.age }`
}
}
}

let res = JSON.stringify(obj);

console.log(res); // {"message":"iyongbao的年龄为25"}

五、总结


好了,今天就和大家分享到这吧。一般如果真涉及到深拷贝,我还是首选自己封装一个方法或者是使用第三方插件库来做深拷贝,这样最保险,避免不必要的麻烦。


作者:勇宝趣学前端
来源:juejin.cn/post/7337989768636973083
收起阅读 »

双token和无感刷新token(简单写法,一文说明白,不墨迹)

为什么有这篇小作文?最近要给自己的项目加上token自动续期,但是在网上搜的写法五花八门,有的光前端部分就写了几百行代码,我看着费劲,摸了半天也没有实现,所以决定自己造轮子项目构成后端部分:使用golang的gin框架起的服务前端部分:vue+elementu...
继续阅读 »

为什么有这篇小作文?

最近要给自己的项目加上token自动续期,但是在网上搜的写法五花八门,有的光前端部分就写了几百行代码,我看着费劲,摸了半天也没有实现,所以决定自己造轮子

项目构成

  • 后端部分:使用golang的gin框架起的服务
  • 前端部分:vue+elementui

先说后端部分,后端逻辑相对前端简单点,关键两步

  1. 登陆接口生成双token
"github.com/dgrijalva/jwt-go"
func (this UserController) DoLogin(ctx *gin.Context) {
username := ctx.Request.FormValue("username")
passWord := ctx.Request.FormValue("password")
passMd5 := middlewares.CreateMD5(passWord)
expireTime := time.Now().Add(10 * time.Second).Unix() //token过期时间10秒,主要是测试方便
refreshTime := time.Now().Add(20 * time.Second).Unix() //刷新的时间限制,超过20秒重新登录
user := modules.User{}
err := modules.DB.Model(&modules.User{}).Where("username = ? AND password = ?", username, passMd5).Find(&user).Error
if err != nil {
ctx.JSON(400, gin.H{
"success": false,
"message": "用户名或密码错误",
})
} else {
println("expireTime", string(rune(expireTime)))
myClaims := MyClaims{
user.Id,
jwt.StandardClaims{
ExpiresAt: expireTime,
},
}
myClaimsRefrrsh := MyClaims{
user.Id,
jwt.StandardClaims{
ExpiresAt: refreshTime,
},
}
jwtKey := []byte("lyf123456")
tokenObj := jwt.NewWithClaims(jwt.SigningMethodHS256, myClaims)
tokenStr, err := tokenObj.SignedString(jwtKey)
tokenFresh := jwt.NewWithClaims(jwt.SigningMethodHS256, myClaimsRefrrsh)
tokenStrRefresh, err2 := tokenFresh.SignedString(jwtKey)
if err != nil && err2 != nil {
ctx.JSON(200, gin.H{
"message": "生成token失败",
"success": false,
})
} else {
ctx.JSON(200, gin.H{
"message": "登录成功",
"success": true,
"token": tokenStr,//数据请求的token
"refreshToken": tokenStrRefresh,//刷新token用的
})
}
}
}
  1. 刷新token的方法
func (this UserController) RefrshToken(ctx *gin.Context) {
tokenData := ctx.Request.Header.Get("Authorization") //这里是个关键点,刷新token时也要带上token,不过这里是前端传的refreshToken
if tokenData == "" {
ctx.JSON(401, gin.H{
"message": "token为空",
"success": false,
})
ctx.Abort()
return
}
tokenStr := strings.Split(tokenData, " ")[1]
_, claims, err := middlewares.ParseToken(tokenStr)
expireTime := time.Now().Add(10 * time.Second).Unix()
refreshTime := time.Now().Add(20 * time.Second).Unix()
if err != nil {
ctx.JSON(400, gin.H{
"success": false,
"message": "token传入错误",
})
} else {
myClaims := MyClaims{
claims.Uid,
jwt.StandardClaims{
ExpiresAt: expireTime,
},
}
myClaimsRefrrsh := MyClaims{
claims.Uid,
jwt.StandardClaims{
ExpiresAt: refreshTime,
},
}
jwtKey := []byte("lyf123456")
tokenObj := jwt.NewWithClaims(jwt.SigningMethodHS256, myClaims)
tokenStr, err := tokenObj.SignedString(jwtKey)
tokenFresh := jwt.NewWithClaims(jwt.SigningMethodHS256, myClaimsRefrrsh)
tokenStrRefresh, err2 := tokenFresh.SignedString(jwtKey)
if err != nil && err2 != nil {
ctx.JSON(400, gin.H{
"message": "生成token失败",
"success": false,
})
} else {
ctx.JSON(200, gin.H{
"message": "刷新token成功",
"success": true,
"token": tokenStr,
"refreshToken": tokenStrRefresh,
})
}
}
}
  1. 路由中间件里验证token
package middlewares

import (
"strings"

"github.com/dgrijalva/jwt-go"
"github.com/gin-gonic/gin"
)

type MyClaims struct {
Uid int
jwt.StandardClaims
}

func AuthMiddleWare(c *gin.Context) {
tokenData := c.Request.Header.Get("Authorization")
if tokenData == "" {
c.JSON(401, gin.H{
"message": "token为空",
"success": false,
})
c.Abort()
return
}
tokenStr := strings.Split(tokenData, " ")[1]
token, _, err := ParseToken(tokenStr)
if err != nil || !token.Valid {
// 这里我感觉觉是个关键点,我看别人写的,过期了返回401,但是前端的axios的响应拦截器里捕获不到,所以我用201状态码,
c.JSON(201, gin.H{
"message": "token已过期",
"success": false,
})
c.Abort()
return
} else {
c.Next()
}
}

func ParseToken(tokenStr string) (*jwt.Token, *MyClaims, error) {
jwtKey := []byte("lyf123456")
// 解析token
myClaims := &MyClaims{}
token, err := jwt.ParseWithClaims(tokenStr, myClaims, func(token *jwt.Token) (interface{}, error) {
return jwtKey, nil
})
return token, myClaims, err
}

总结一下:后端部分三步,1.登陆时生成双token,2,路由中间件里验证token,过期时返回201状态码(201是我私人定的,并不是行业标准)。3,刷新token的方法里也和登陆接口一样返回双token

前端部分

前端部分在axios封装时候加拦截器判断token是否过期,我这里跟别人写的最大的不同点是:我创建了两个axios对象,一个正常数据请求用(server),另一个专门刷新token用(serverRefreshToken),这样写的好处是省去了易错的判断逻辑

import axios from 'axios'
import { ElMessage } from 'element-plus'
import router from '../router'
//数据请求用
const server=axios.create({
baseURL:'/shopApi',
timeout:5000
})
// 刷新token专用
const serverRefreshToken=axios.create({
baseURL:'/shopApi',
timeout:5000
})
//获取新token的方法
async function getNewToken(){
let res=await serverRefreshToken.request({
url:`/admin/refresh`,
method:"post",
})
if(res.status==200){
sessionStorage.setItem("token",res.data.token)
sessionStorage.setItem("refreshToken",res.data.refreshToken)
return true
}else{
ElMessage.error(res.data.message)
router.push('/login')
return false
}
}
//这里是正常获取数据用的请求拦截器,主要作用是给所有请求的请求头里加上token
server.interceptors.request.use(config=>{
let token=""
token=sessionStorage.getItem("token")
if(token){
config.headers.Authorization="Bearer "+token
}
return config
},error=>{
Promise.reject(error)
})
//这里是正常获取数据用的响应拦截器,正常数据请求都是200状态码,当拦截到201状态码时,代表token过期了,
server.interceptors.response.use(async(res)=>{
if(res.status==201){
//获取新token
let bl=await getNewToken()
if(bl){
//获取成功新token之后,把刚才token过期拦截到的请求重新发一遍,获取到数据之后把res覆盖掉
//这里是个关键点,下边这行代码里的第二个res是token过期后被拦截的那个请求,config里是该请求的详细信息,重新请求后返回的是第一个res,把失败的res覆盖掉,这里有点绕,文字不好表达,
res=await server.request(res.config)
}
}
return res
},error=>{
if(error.response.status==500||error.response.status==401||error.response.status==400){
router.push('/login')
ElMessage.error(error.response.data.message)
Promise.reject(error)
}

})
//这里是刷新token专用的axios对象,他的作用是给请求加上刷新token专用的refreshToken
serverRefreshToken.interceptors.request.use(config=>{
let token=""
token=sessionStorage.getItem("refreshToken")
if(token){
config.headers.Authorization="Bearer "+token
}
return config
},error=>{
Promise.reject(error)
})
export default server

总结一下,前端部分:1,正常数据请求和刷新token用的请求分开了,各司其职。省去复杂的判断。2,获取新的token和refreshToken后更新原来旧的token和refreshToken。(完结)


作者:锋行天下
来源:juejin.cn/post/7337876697427148811

为什么有这篇小作文?

最近要给自己的项目加上token自动续期,但是在网上搜的写法五花八门,有的光前端部分就写了几百行代码,我看着费劲,摸了半天也没有实现,所以决定自己造轮子

项目构成

  • 后端部分:使用golang的gin框架起的服务
  • 前端部分:vue+elementui

先说后端部分,后端逻辑相对前端简单点,关键两步

  1. 登陆接口生成双token
"github.com/dgrijalva/jwt-go"
func (this UserController) DoLogin(ctx *gin.Context) {
username := ctx.Request.FormValue("username")
passWord := ctx.Request.FormValue("password")
passMd5 := middlewares.CreateMD5(passWord)
expireTime := time.Now().Add(10 * time.Second).Unix() //token过期时间10秒,主要是测试方便
refreshTime := time.Now().Add(20 * time.Second).Unix() //刷新的时间限制,超过20秒重新登录
user := modules.User{}
err := modules.DB.Model(&modules.User{}).Where("username = ? AND password = ?", username, passMd5).Find(&user).Error
if err != nil {
ctx.JSON(400, gin.H{
"success": false,
"message": "用户名或密码错误",
})
} else {
println("expireTime", string(rune(expireTime)))
myClaims := MyClaims{
user.Id,
jwt.StandardClaims{
ExpiresAt: expireTime,
},
}
myClaimsRefrrsh := MyClaims{
user.Id,
jwt.StandardClaims{
ExpiresAt: refreshTime,
},
}
jwtKey := []byte("lyf123456")
tokenObj := jwt.NewWithClaims(jwt.SigningMethodHS256, myClaims)
tokenStr, err := tokenObj.SignedString(jwtKey)
tokenFresh := jwt.NewWithClaims(jwt.SigningMethodHS256, myClaimsRefrrsh)
tokenStrRefresh, err2 := tokenFresh.SignedString(jwtKey)
if err != nil && err2 != nil {
ctx.JSON(200, gin.H{
"message": "生成token失败",
"success": false,
})
} else {
ctx.JSON(200, gin.H{
"message": "登录成功",
"success": true,
"token": tokenStr,//数据请求的token
"refreshToken": tokenStrRefresh,//刷新token用的
})
}
}
}
  1. 刷新token的方法
func (this UserController) RefrshToken(ctx *gin.Context) {
tokenData := ctx.Request.Header.Get("Authorization") //这里是个关键点,刷新token时也要带上token,不过这里是前端传的refreshToken
if tokenData == "" {
ctx.JSON(401, gin.H{
"message": "token为空",
"success": false,
})
ctx.Abort()
return
}
tokenStr := strings.Split(tokenData, " ")[1]
_, claims, err := middlewares.ParseToken(tokenStr)
expireTime := time.Now().Add(10 * time.Second).Unix()
refreshTime := time.Now().Add(20 * time.Second).Unix()
if err != nil {
ctx.JSON(400, gin.H{
"success": false,
"message": "token传入错误",
})
} else {
myClaims := MyClaims{
claims.Uid,
jwt.StandardClaims{
ExpiresAt: expireTime,
},
}
myClaimsRefrrsh := MyClaims{
claims.Uid,
jwt.StandardClaims{
ExpiresAt: refreshTime,
},
}
jwtKey := []byte("lyf123456")
tokenObj := jwt.NewWithClaims(jwt.SigningMethodHS256, myClaims)
tokenStr, err := tokenObj.SignedString(jwtKey)
tokenFresh := jwt.NewWithClaims(jwt.SigningMethodHS256, myClaimsRefrrsh)
tokenStrRefresh, err2 := tokenFresh.SignedString(jwtKey)
if err != nil && err2 != nil {
ctx.JSON(400, gin.H{
"message": "生成token失败",
"success": false,
})
} else {
ctx.JSON(200, gin.H{
"message": "刷新token成功",
"success": true,
"token": tokenStr,
"refreshToken": tokenStrRefresh,
})
}
}
}
  1. 路由中间件里验证token
package middlewares

import (
"strings"

"github.com/dgrijalva/jwt-go"
"github.com/gin-gonic/gin"
)

type MyClaims struct {
Uid int
jwt.StandardClaims
}

func AuthMiddleWare(c *gin.Context) {
tokenData := c.Request.Header.Get("Authorization")
if tokenData == "" {
c.JSON(401, gin.H{
"message": "token为空",
"success": false,
})
c.Abort()
return
}
tokenStr := strings.Split(tokenData, " ")[1]
token, _, err := ParseToken(tokenStr)
if err != nil || !token.Valid {
// 这里我感觉觉是个关键点,我看别人写的,过期了返回401,但是前端的axios的响应拦截器里捕获不到,所以我用201状态码,
c.JSON(201, gin.H{
"message": "token已过期",
"success": false,
})
c.Abort()
return
} else {
c.Next()
}
}

func ParseToken(tokenStr string) (*jwt.Token, *MyClaims, error) {
jwtKey := []byte("lyf123456")
// 解析token
myClaims := &MyClaims{}
token, err := jwt.ParseWithClaims(tokenStr, myClaims, func(token *jwt.Token) (interface{}, error) {
return jwtKey, nil
})
return token, myClaims, err
}

总结一下:后端部分三步,1.登陆时生成双token,2,路由中间件里验证token,过期时返回201状态码(201是我私人定的,并不是行业标准)。3,刷新token的方法里也和登陆接口一样返回双token

前端部分

前端部分在axios封装时候加拦截器判断token是否过期,我这里跟别人写的最大的不同点是:我创建了两个axios对象,一个正常数据请求用(server),另一个专门刷新token用(serverRefreshToken),这样写的好处是省去了易错的判断逻辑

import axios from 'axios'
import { ElMessage } from 'element-plus'
import router from '../router'
//数据请求用
const server=axios.create({
baseURL:'/shopApi',
timeout:5000
})
// 刷新token专用
const serverRefreshToken=axios.create({
baseURL:'/shopApi',
timeout:5000
})
//获取新token的方法
async function getNewToken(){
let res=await serverRefreshToken.request({
url:`/admin/refresh`,
method:"post",
})
if(res.status==200){
sessionStorage.setItem("token",res.data.token)
sessionStorage.setItem("refreshToken",res.data.refreshToken)
return true
}else{
ElMessage.error(res.data.message)
router.push('/login')
return false
}
}
//这里是正常获取数据用的请求拦截器,主要作用是给所有请求的请求头里加上token
server.interceptors.request.use(config=>{
let token=""
token=sessionStorage.getItem("token")
if(token){
config.headers.Authorization="Bearer "+token
}
return config
},error=>{
Promise.reject(error)
})
//这里是正常获取数据用的响应拦截器,正常数据请求都是200状态码,当拦截到201状态码时,代表token过期了,
server.interceptors.response.use(async(res)=>{
if(res.status==201){
//获取新token
let bl=await getNewToken()
if(bl){
//获取成功新token之后,把刚才token过期拦截到的请求重新发一遍,获取到数据之后把res覆盖掉
//这里是个关键点,下边这行代码里的第二个res是token过期后被拦截的那个请求,config里是该请求的详细信息,重新请求后返回的是第一个res,把失败的res覆盖掉,这里有点绕,文字不好表达,
res=await server.request(res.config)
}
}
return res
},error=>{
if(error.response.status==500||error.response.status==401||error.response.status==400){
router.push('/login')
ElMessage.error(error.response.data.message)
Promise.reject(error)
}

})
//这里是刷新token专用的axios对象,他的作用是给请求加上刷新token专用的refreshToken
serverRefreshToken.interceptors.request.use(config=>{
let token=""
token=sessionStorage.getItem("refreshToken")
if(token){
config.headers.Authorization="Bearer "+token
}
return config
},error=>{
Promise.reject(error)
})
export default server

总结一下,前端部分:1,正常数据请求和刷新token用的请求分开了,各司其职。省去复杂的判断。2,获取新的token和refreshToken后更新原来旧的token和refreshToken。(完结)


作者:锋行天下
来源:juejin.cn/post/7337876697427148811
收起阅读 »

掌握Redis核心:常用数据类型的高效运用秘籍!

在数据驱动的时代,高效地存储和处理数据成为了开发者们的重要任务。Redis,作为一个开源的高性能键值对(key-value)数据库,以其独特的数据结构和丰富的功能,成为了众多项目的首选。今天,我们就来揭开Redis的神秘面纱,看看它是如何通过不同的数据类型,为...
继续阅读 »

在数据驱动的时代,高效地存储和处理数据成为了开发者们的重要任务。Redis,作为一个开源的高性能键值对(key-value)数据库,以其独特的数据结构和丰富的功能,成为了众多项目的首选。

今天,我们就来揭开Redis的神秘面纱,看看它是如何通过不同的数据类型,为我们提供高效、灵活的数据存储和处理能力的。

一、字符串(String):数据的基石

String类型简介

字符串是Redis最基本的数据类型,它可以存储文本、数字或者二进制数据。

使用字符串类型,你可以执行原子性的操作,如追加(APPEND)、设置(SET)和获取(GET)。例如,你可以将用户信息作为字符串存储,并通过键快速检索。

  • 一个key对应一个value。

  • String类型是二进制安全的。只要内容可以使用字符串表示就可以存储到string中。比如jpg图片或者序列化的对象。

  • 一个Redis中字符串value最多可以是512M。

常用命令

set key value: 添加键值对。

Description

get key: 查询key对应的键值。

Description

注意:如果设置了两次相同的key,后设置的就会把之前的key覆盖掉。

append key value: 将给定的value追加到原值的末尾。

Description

strlen key: 获得值的长度。

Description

setnx key value: 只有在key 不存在时 ,才能设置 key 的值。

Description

incr key: 将 key 中储存的数字值增1(只能对数字值操作,如果为空,新增值为1)。

Description

decr key: 将 key 中储存的数字值减1(只能对数字值操作,如果为空,新增值为-1)。

Description

incrby / decrby key 步长: 通过自定义步长方式增减 key 中储存的数字值。

Description

mset key1 value1 key2 value2 …: 同时设置一个或多个键值对。

Description

mget key1 key2 key3 …: 同时获取一个或多个value。

Description

msetnx key1 value1 key2 value2 …: 所有给定 key 都不存在时,同时设置一个或多个 key-value 对。

注意:此操作有原子性,只要有一个不符合条件的key。其他的也都不能设置成功。如下图:
Description

getrange key 起始位置、结束位置: 获得值的范围,类似java中的substring。

Description

setrange key 起始位置 value: 用 value覆写key所储存的字符串值,从起始位置开始(索引从0开始)。

Description

setex key 过期时间 value: 可以在设置键值的同时,设置过期时间,单位秒(前面的expire是给已有的键值设置过期时间,注意区别)。

Description

getset key value: 以新换旧,设置了新值同时获得旧值。

Description

应用场景

存储用户信息: 将用户的姓名、年龄等信息作为字符串存储在Redis中,通过键值对的方式快速检索和更新。

计数器: 使用INCR命令实现访问量、点赞数等计数功能。

二、哈希(Hash):组织数据的框架

Hash类型简介

哈希类型允许你存储字段-值对的集合。这种结构非常适合于存储对象,如用户的个人信息。通过HSET和HGET命令,你可以设置和获取哈希中的字段和值。

哈希类型的优势在于它可以对字段进行原子性操作,而不需要读取整个对象。

常用命令

hset key field value: 给key集合中的 field 键赋值value。
Description

hget key1 field: 从key1集合field取出 value。

Description

hmset key1 field1 value1 field2 value2… : 批量设置hash的值(一次性设置多个数据值)。

Description

hexists key1 field: 查看哈希表 key 中,给定域 field 是否存在。

Description

hkeys key: 列出该hash集合的所有field。

Description

hvals key: 列出该hash集合的所有value。

Description

hincrby key field increment: 为哈希表 key 中的域 field 的值加上增量。

Description

hsetnx key field value: 将哈希表 key 中的域 field 的值设置为 value ,当且仅当域 field 不存在。

Description

应用场景

存储用户对象: 将用户对象的多个属性(如姓名、年龄、性别等)存储在一个哈希结构中,通过HGETALL命令获取整个对象,或使用HGET/HSET针对某个属性进行操作。

存储配置信息: 将应用程序的配置信息以字段-值对的形式存储在哈希中,方便集中管理和修改。

三、列表(List):有序数据的队列

List类型简介

列表类型提供了一种顺序存储数据的方式,它类似于Python中的列表或Java中的LinkedList。

你可以使用LPUSH和RPUSH命令在列表的头部或尾部添加元素。列表还支持范围查询和列表内元素的移除操作,非常适合于实现消息队列等场景。

  • List中单键多值,即一个key对应多个value,其中的多个value值使用List进行存储。

  • Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)。

  • 它的底层实际是个双向链表,对两端的操作性能很高,通过索引下标的操作中间的节点性能会较差。

常用命令

**lpush / rpush key value1 value2 value3 … :**从左边/右边插入一个或多个值(l代表left,r代表right)。

**lrange key start stop:**按照索引下标获得元素(从左到右)。

其中lrange k1 0 -1表示取出k1中全部value值(0表示左边第一个,-1代表右边第一个)。

Description

lpop / rpop key: 从左边/右边吐出一个值。值在键在,值光键亡。
pop表示把值拿出来。

Description
图片从左边取出k1的一个value值。如下图:

Description

从右边取出k2的一个value值。如下图:

Description

value值全部取完的时候,key就没有了。如下图:

Description

rpoplpush key1 key2: 从key1列表右边吐出一个值,插到key2列表左边。

Description

lindex key index: 按照索引下标获得元素(从左到右)。

Description

llen key: 获得列表长度。

Description

linsert key before value newvalue : 在value的前面插入newvalue插入值。

Description

lrem key n value: 从开始删除n个value(从左到右)图片。

Description

lset key index value: 将列表key下标为index的值替换成value。

Description

应用场景

消息队列: 使用LPUSH/RPUSH命令将待处理的消息添加到列表头部/尾部,使用LPOP/RPOP从列表中取出并处理消息。

关注列表: 存储用户关注的其他用户列表,使用LINSERT命令在列表中插入新关注的对象。

你还在苦恼找不到真正免费的编程学习平台吗?可以试试【云端源想】!课程视频、知识库、微实战、云实验室、一对一咨询……你想要的全部学习资源这里都有,重点是现在还是免费的!

点这里即可查看!

四、集合(Set):去重数据的集合

Set类型简介

集合类型用于存储无序且唯一的数据集合。当你需要存储不允许重复的元素时,集合是一个很好的选择。

SADD命令用于向集合中添加元素,而SMEMBERS可以获取集合中的所有元素。集合还支持交集、并集和差集等高级操作,非常适合于处理标签、好友关系等场景。

Redis的Set是string类型的无序,不可重复集合。Set底层其实是一个value为null的hash表,所以添加,删除,查找的复杂度都是0(1)。

常用命令

sadd key value1 value2 …: 将一个或多个 member 元素加入到key对应的集合中,已经存在的 member 元素将被忽略

smembers key: 取出key对应的集合中的所有值。

Description

sismember key value: 判断集合是否为含有该value值。1表示有,0表示没有。

Description

scard key: 返回key对应集合中的元素个数。

Description

srem key value1 value2…: 删除key对应的集合中的某些元素。

Description

spop key: 随机从key对应的集合中吐出一个值。

Description

srandmember key n: 随机从key对应的集合中取出n个值。不会从集合中删除 (rand即为random)。

Description

smove sourceKey destinationKey value: 把集合中一个值从一个集合移动到另一个集合。

Description

sinter key1 key2: 返回两个集合的交集元素

sunion key1 key2: 返回两个集合的并集元素。

sdiff key1 key2: 返回两个集合的差集元素(key1中的,不包含key2中的)。

Description

应用场景

好友关系: 将用户的好友列表存储在一个集合中,使用SADD命令添加好友,使用SISMEMBER判断某个用户是否为好友。

标签系统: 将文章或商品的标签存储在集合中,使用SUNION/SINTER等命令进行标签的交集、并集操作。

五、有序集合(Sorted Set)

Sorted Set类型简介

有序集合是Redis中的一个高级数据类型,它结合了集合的唯一性和列表的排序功能。

每个元素都关联一个分数(score),根据分数对元素进行排序。ZADD命令用于添加元素和分数,ZRANGE则可以获取排序后的元素列表。

注:zset是sorted set的缩写

常用命令

zadd key score1 value1 score2 value2…: 将一个或多个 member 元素及其 score 值加入到有序集 key 当中。

Description

zrange key start stop (withscores): 返回有序集 key 中,下标在start和stop之间的元素。(带withscores,可以让分数一起和值返回到结果集)。

zrange rank 0 -1 (withscores): 表示取出全部元素,从小到大排列。如下图:

Description

zrangebyscore key min max (withscores): 返回有序集 key 中所有 score 值介于 min 和 max 之间(包括等于 min 或 max )的成员。有序集成员按 score 值递增(从小到大)次序排列。

Description

zrevrangebyscore key max min (withscores): 同上,改为从大到小排列(其中rev表示reverse)。

Description

zincrby key increment value: 为元素的score加上增量。

Description

zrem key value: 删除该集合下,指定值的元素。

Description

zcount key min max: 统计该集合,分数区间内的元素个数。

Description

zrank key value: 返回该值在集合中的排名。(排在第一位的是0)

Description

应用场景

排行榜系统: 可以使用有序集合来存储用户的得分,并根据得分进行排序。

带权重的集合: 有序集合可以用来实现带权重的集合,即每个元素都有一个对应的权重值。

时间线排序: 有序集合可以用于实现时间线排序,即将事件或消息按照时间顺序进行排序。

六、总结

本篇文章我们探索了Redis的五种常用数据类型及常用命令和使用场景。每一种数据类型都有其独特的优势和适用情况,掌握它们将使你在数据处理的道路上更加得心应手。

无论是构建缓存系统,还是实现复杂的数据结构,Redis都能提供强有力的支持。希望这篇文章能帮助你更好地理解和运用Redis,让你的项目在数据的世界中脱颖而出。

收起阅读 »

听说你会架构设计?来,弄一个公交&地铁乘车系统

1. 引言 1.1 上班通勤的日常 “叮铃铃”,“叮铃铃”,早上七八点,你还在温暖的被窝里和闹钟“斗智斗勇”。 突然,你意识到已经快迟到了,于是像个闪电侠一样冲进卫生间,速洗漱,急穿衣,左手抄起手机,右手拿起面包,边穿衣边啃早餐。 这个时候,通勤的老难题又摆...
继续阅读 »

1. 引言


1.1 上班通勤的日常


“叮铃铃”,“叮铃铃”,早上七八点,你还在温暖的被窝里和闹钟“斗智斗勇”。



突然,你意识到已经快迟到了,于是像个闪电侠一样冲进卫生间,速洗漱,急穿衣,左手抄起手机,右手拿起面包,边穿衣边啃早餐。


这个时候,通勤的老难题又摆在了你面前:要不要吃完这口面包、刷牙和洗脸,还是先冲出门赶车?


好不容易做出了一个艰难的决定——放下面包,快步冲出门。你拿出手机,点开了熟悉的地铁乘车 App 或公交地铁乘车码小程序。


然后,一张二维码在屏幕上亮了起来,这可是你每天通勤的“敲门砖”。





你快步走到地铁站,将手机二维码扫描在闸机上,"嗖"的一声,闸机打开,你轻松通过,不再需要排队买票,不再被早高峰的拥挤闹心。


你走进地铁车厢,挤到了一个角落,拿出手机,开始计划一天的工作。


1.2 公交&地铁乘车系统


正如上文所说,人们只需要一台手机,一个二维码就可以完成上班通勤的所有事项。


那这个便捷的公交或地铁乘车系统是如何设计的呢?它背后的技术和架构是怎样支撑着你我每天的通勤生活呢?


今天让我们一起揭开这个现代都市打工人通勤小能手的面纱,深入探讨乘车系统的设计与实现


在这个文章中,小❤将带你走进乘车系统的世界,一探究竟,看看它是如何在短短几年内从科幻电影中走出来,成为我们日常生活不可或缺的一部分。


2. 需求设计


2.1 功能需求




  • 用户注册和登录: 用户可以通过手机应用或小程序注册账号,并使用账号登录系统。

  • 路线查询: 用户可以查询地铁的线路和站点信息,包括发车时间、车票价格等。

  • 获取乘车二维码: 系统根据用户的信息生成乘车二维码。

  • 获取地铁实时位置: 用户可以查询地铁的实时位置,并查看地铁离当前站台还有多久到达。

  • 乘车扫描和自动支付: 用户在入站和出站时通过扫描二维码来完成乘车,系统根据乘车里程自动计算费用并进行支付。

  • 交易记录查询: 用户可以查询自己的交易历史记录,包括乘车时间、金额、线路等信息。


2.2 乘车系统的非功能需求


乘车系统的用户量非常大,据《中国主要城市通勤检测报告-2023》数据显示,一线城市每天乘公交&地铁上班的的人数普遍超过千万,平均通勤时间在 45-60 分钟,并集中在早高峰和晚高峰时段。


所以,设计一个热点数据分布非均匀、人群分布非均匀的乘车系统时,需要考虑如下几点:



  • 用户分布不均匀,一线城市的乘车系统用户,超出普通城市几个数量级。

  • 时间分布不均匀,乘车系统的设计初衷是方便上下班通勤,所以早晚高峰的用户数会高出其它时间段几个数量级。

  • 高并发: 考虑到公交车/地铁系统可能同时有大量的用户在高峰时段使用,系统需要具备高并发处理能力。

  • 高性能: 为了提供快速的查询和支付服务,系统需要具备高性能,响应时间应尽可能短。

  • 可扩展性: 随着用户数量的增加,系统应该容易扩展,以满足未来的需求。

  • 可用性: 系统需要保证24/7的可用性,随时提供服务。

  • 安全和隐私保护: 系统需要确保用户数据的安全和隐私,包括支付信息和个人信息的保护。


3. 概要设计


3.1 核心组件




  • 前端应用: 开发手机 App 和小程序,提供用户注册、登录、查询等功能。

  • 后端服务: 设计后端服务,包括用户管理、路线查询、二维码管理、订单处理、支付系统等。

  • 数据库: 使用关系型数据库 MySQL 集群存储用户信息、路线信息、交易记录等数据。

  • 推送系统: 将乘车后的支付结果,通过在线和离线两种方式推送给用户手机上。

  • 负载均衡和消息队列: 考虑使用负载均衡和消息队列技术来提高系统性能。


3.2 乘车流程


1)用户手机与后台系统的交互


交互时序图如下:



1. 用户注册和登录: 用户首先需要在手机应用上注册并登录系统,提供个人信息,包括用户名、手机号码、支付方式等。


2. 查询乘车信息: 用户可以使用手机应用查询公交车/地铁的路线和票价信息,用户可以根据自己的出行需求选择合适的线路。


3. 生成乘车二维码: 用户登录后,系统会生成一个用于乘车的二维码,这个二维码可以在用户手机上随时查看。这个二维码是城市公交系统的通用乘车二维码,同时该码关联到用户的账户和付款方式,用户可以随时使用它乘坐任何一辆公交车或地铁。


2)用户手机与公交车的交互


交互 UML 状态图如下:




  1. 用户进站扫码: 当用户进入地铁站时,他们将手机上的乘车码扫描在进站设备上。这个设备将扫描到的乘车码发送给后台系统。

  2. 进站数据处理: 后台系统接收到进站信息后,会验证乘车码的有效性,检查用户是否有进站记录,并记录下进站的时间和地点。

  3. 用户出站扫码: 用户在乘车结束后,将手机上的乘车码扫描在出站设备上。

  4. 出站数据处理: 后台系统接收到出站信息后,会验证乘车码的有效性,检查用户是否有对应的进站记录,并记录下出站的时间和地点。


3)后台系统的处理



  1. 乘车费用计算: 基于用户的进站和出站地点以及乘车规则,后台系统计算乘车费用。这个费用可以根据不同的城市和运营商有所不同。

  2. 费用记录和扣款: 系统记录下乘车费用,并从用户的付款方式(例如,支付宝或微信钱包)中扣除费用。

  3. 乘车记录存储: 所有的乘车记录,包括进站、出站、费用等信息,被存储在乘车记录表中,以便用户查看和服务提供商进行结算。

  4. 通知用户: 如果有需要,系统可以向用户发送通知,告知他们的乘车费用已被扣除。

  5. 数据库交互: 在整个过程中,系统需要与数据库交互来存储和检索用户信息、乘车记录、费用信息等数据。


3. 详细设计


3.1 数据库设计



  • 用户信息表(User) ,包括用户ID、手机号、密码、支付方式、创建时间等。

  • 二维码表 (QRCode) ,包括二维码ID、用户ID、城市ID、生成时间、有效期及二维码数据等。

  • 车辆&地铁车次表 (Vehicle) ,包括车辆ID、车牌或地铁列车号、车型(公交、地铁)、扫描设备序列号等。

  • 乘车记录表 (TripRecord) ,包括记录ID、用户ID、车辆ID、上下车时间、起止站点等。

  • 支付记录表 (PaymentRecord) ,包括支付ID、乘车记录ID、交易时间、交易金额、支付方式、支付状态等。


以上是一些在公交车&地铁乘车系统中需要设计的数据库表及其字段的基本信息,后续可根据具体需求和系统规模,还可以进一步优化表结构和字段设计,以满足性能和扩展性要求。


详细设计除了要设计出表结构以外,我们还针对两个核心问题进行讨论:



  • 最短路线查询



  • 乘车二维码管理


3.2 最短路线查询


根据交通部门给的公交&地铁路线,我们可以绘制如下站点图:



假设图中的站点有 A-F,涉及到的交通工具有地铁 1 号线和 2 路公交,用户的起点和终点分别为 A、F 点。我们可以使用 Dijkstra 算法来求两点之间的最短路径,具体步骤为:


步骤已遍历集合未遍历集合
1选入A,此时最短路径 A->A = 0,再以 A 为中间点,开始寻找下一个邻近节点{B、C、D、E、F},其中与 A 相邻的节点有 B 和 C,AB=6,AC=3。接下来,选取较短的路径节点 C 开始遍历
2选取C,A->C=3,此时已遍历集合为{A、C},以 A 和 C 为中间点,开始寻找下一个邻近节点{B、D、E、F},其中与 A、C 相邻的节点有 B 和 D,AB=6,ACD=3+4=7。接下来,选取较短的路径节点 B 开始遍历
3选取B,A->B=6,此时已遍历集合为{A、C、B},A 相邻的节点已经遍历结束,开始寻找和 B、C 相近的节点{D、E、F},其中与 B、C 相邻的节点有 D,节点 D 在之前已经有了一个距离记录(7),现在新的可选路径是 ABD=6+5=11。显然第一个路径更短,于是将 D 的最近距离 7 加入到集合中
4选取D,A->D=7,此时已遍历集合为{A、C、B、D},寻找 D 相邻的节点{E、F},其中 DE=2,DF=3,选取最近路径的节点 E 加入集合
5选取 E,A->E=7+2=9,此时已遍历集合为{A、C、B、D、E},继续寻找 D 和 E 相近的节点{F},其中 DF=3,DEF=2+5=7,于是F的最近距离为7+3=10.
6选取F,A->F=10,此时遍历集合为{A、C、B、D、E、F}所有节点已遍历结束,从 A 点出发,它们的最近距离分别为{A=0,C=3,B=6,D=7,E=9,F=10}

在用户查询路线之前,交通部门会把公交 & 地铁的站点经纬度信息输入到路线管理系统,并根据二维的空间经纬度编码存储对应的站点信息。


我们设定西经为负,南纬为负,所以地球上的经度范围就是[-180, 180],纬度范围就是[-90,90]。如果以本初子午线、赤道为界,地球可以分成 4 个部分。



根据这个原理,我们可以先将二维的空间经纬度编码成一个字符串,来唯一标识用户或站点的位置信息。再通过 Redis 的 GeoHash 算法,来获取用户出发点附近的所有站点信息。


GeoHash 算法的原理是将一个位置的经纬度换算成地址编码字符串,表示在某个矩形区域,通过这个算法可以快速找到同一个区域的所有站点


一旦获得了起始地点的经纬度,系统就可以根据附近的站点信息,调用路线管理系统来查找最佳的公交或地铁路线。


一旦用户选择了一条路线,导航引擎启动并提供实时导航指引。导航引擎可能会使用地图数据和 GPS 定位来指导用户前往起止站点。


3.3 乘车二维码管理


乘车码是通过 QR 码(Quick Response Code)技术生成的,它比传统的 Bar Code 条形码能存更多的信息,也能表示更多的数据类型,如图所示:



二维码的生成非常简单,拿 Go 语言来举例,只需引入一个三方库:


import "github.com/skip2/go-qrcode"

func main() {
    qr,err:=qrcode.New("https://mp.weixin.qq.com",qrcode.Medium)
if err != nil {
    log.Fatal(err)
else {
    qr.BackgroundColor = color.RGBA{50,205,50,255//定义背景色
    qr.ForegroundColor = color.White //定义前景色
    qr.WriteFile(256,"./wechatgzh_qrcode.png"//转成图片保存
    }
}

以下是该功能用户和系统之间的交互、二维码信息存储、以及高并发请求处理的详细说明:



  1. 用户与系统交互: 用户首先在手机 App 上登录,系统会验证用户的身份和付款方式。一旦验证成功,系统根据用户的身份信息和付款方式,动态生成一个 QR 码,这个 QR 码包含了用户的标识信息和相关的乘车参数。

  2. 二维码信息存储: 生成的二维码信息需要在后台进行存储和关联。通常,这些信息会存储在一个专门的数据库表中,该表包含以下字段:



    • 二维码ID:主键ID,唯一标识一个二维码。

    • 用户ID:与乘车码关联的用户唯一标识。

    • 二维码数据:QR码的内容,包括用户信息和乘车参数。

    • 生成时间:二维码生成的时间戳,用于后续的验证和管理。

    • 有效期限:二维码的有效期,通常会设置一个时间限制,以保证安全性。



  3. 高并发请求处理: 在高并发情况下,大量的用户会同时生成和扫描二维码,因此需要一些策略来处理这些请求:



    • 负载均衡: 后台系统可以采用负载均衡技术,将请求分散到多个服务器上,以分担服务器的负载。

    • 缓存优化: 二维码的生成是相对耗时的操作,可以采用 Redis 来缓存已生成的二维码,避免重复生成。

    • 限制频率: 为了防止滥用,可以限制每个用户生成二维码的频率,例如,每分钟只允许生成 5  次,这可以通过限流的方式来实现。




总之,通过 QR 码技术生成乘车码,后台系统需要具备高并发处理的能力,包括负载均衡、缓存和频率限制等策略,以确保用户能够快速获得有效的乘车二维码。


同时,二维码信息需要被安全地存储和管理,比如:加密存储以保护用户的隐私和付款信息。



不清楚如何限流的,可以看我之前的这篇文章:若我问到高可用,阁下又该如何应对呢?



4. 乘车系统的发展


4.1 其它设计


除此之外,公交车或地铁的定位和到站时间计算可能还涉及定位设备、GPS 系统、NoSQL 数据库、用户 TCP 连接管理系统等核心组件,并通过实时数据采集、位置处理、到站时间计算和信息推送等流程来为用户提供准确的乘车信息。


同时,自动支付也是为了方便用户的重要功能,可以通过与第三方支付平台的集成来实现。


4.2 未来发展


公交车/地铁乘车系统的未来发展可以包括以下方向:



  • 智能化乘车: 引入智能设备,如人脸自动识别乘客、人脸扣款等。

  • 大数据分析: 利用大数据技术分析乘车数据,提供更好的服务。


在设计和发展过程中,也要不断考虑用户体验、性能和安全,确保系统能够满足不断增长的需求。


由于篇幅有限,文章就到此结束了。


希望读者们能对公交&地铁乘车系统的设计有更深入的了解,并和小❤一起期待未来更多的交通创新解决方案叭~


作者:xin猿意码
来源:juejin.cn/post/7287495466514055202
收起阅读 »

年,是团圆

大概是我刚上小学时候,村子里流行起来出门打工,每个家庭里面的壮年男人都会去湖北挖桩,组团去。他们过完年出发,先走路下山,坐车再坐船。 出门一段时间后,他们会寄钱回家,那时寄钱的方式我不清楚也还从没有问过,只记得家里女人们听到对门肉嗓子大喊“xxx,来拿汇票”时...
继续阅读 »

大概是我刚上小学时候,村子里流行起来出门打工,每个家庭里面的壮年男人都会去湖北挖桩,组团去。他们过完年出发,先走路下山,坐车再坐船。


出门一段时间后,他们会寄钱回家,那时寄钱的方式我不清楚也还从没有问过,只记得家里女人们听到对门肉嗓子大喊“xxx,来拿汇票”时,她们是很开心的。


快过年,男人们从外面回家,背着或挑着大件的行李,行李中是破旧的棉穗,小孩的新衣与不常见的水果糖。


挖桩需要两个人配合,一人在井里往下挖,一人在上面接桶转移土石,隔一段时间换人。有想法的男人认为他可以一直待在井里,女人提桶倒土是完全没有问题的,如果与自己的女人组团,工钱不用与别人分,挣钱的速度便会快上一倍。


于是,到我上初中时候,出门打工的规模更加扩大,家中的男人女人一起出门,小孩交给父母带着。


男人女人外出打工的时间是一样的,过完年出门,快过年时回家。


男人女人年龄渐长,他们的小孩长成大人,成人都外出打工,过完年出门,快过年时回家。


男人女人与小孩的目的地并不相同。于是过年,成为一家团圆的日子。


家家张灯结彩,人人喜笑颜开。


初一的早餐,是可以很简单的,汤圆或是包面;也可以是复杂的,蒸米饭,炸鱼、牛肉干、猪耳朵、凉拌鸡脚等许多凉菜,昨天的猪脚炖鸡汤,再配几个新鲜炒菜,满满一桌。复杂与简单的差异,来自于当天行程的安排,马上要走亲戚就简单些,家中要来客人便复杂点。


午饭,必然是丰盛的,一锅必不可少的鲜炖的猪脚炖鸡汤(是的,猪脚与鸡年三十只炖四分之一,留着客人来后能炖新的猪脚汤),一盆小孩儿最是欢迎的洋芋片,一锅人人喜爱的猪头汤清炖萝卜,两盘肥腻多汁男人们热衷的辣椒炒三线儿,一盘香肠、两碗扣肉,再来一盘炸豆腐……凉菜有猪肝、猪耳朵、鸡脚、炸鱼、牛肉干、萝卜干儿、折耳根……炒一个青菜,或是再加一碗青菜猪血汤。


男人们大都是会喝点酒的,他们互相劝着,你敬我一口酒,我回你两句话:“这酒好甜啊!”女人们偶尔也喝些酒,但更多是和小孩一起喝饮料。主妇是最后上桌吃饭的,大家吃饭时,她为桌上换热汤,为客人盛热饭。


吃过午饭,晒晒太阳烤烤火,嗑嗑瓜子吹吹牛。客人准备回家,临出发告诫主人:“你们明天一定要来啊,都要来。”


初二初三初四初五初六,是初一的重复。


隔得近,午饭的重复便是晚饭。吃过晚饭,今日不用再做饭,男人女人们围着炉子说过去说现在,说去年说当下,那“哈哈”大笑声,是时常能传到地坝的。


地坝里,孩子们的游戏一个接一个的切换。老鹰抓小鸡、三个字(一个人追众人跑,快被抓到时喊出随便的三个字,被抓者定住等待救援,抓人者切换目标;待所有人都被定住,便问“桃花开了没?”)、跳绳、打沙包……四岁五岁的,大多数游戏还玩不太明白,但他们的参与感该是最强的,因为哥哥姐姐叔叔阿姨都护着他们。


地坝里的叽叽喳喳,盖过屋子里哈哈哈哈。


年,是团圆!


作者:我要改名叫嘟嘟
来源:juejin.cn/post/7337957655190847522
收起阅读 »

年收入 100 万,不敢生孩子

今日热帖,有网友发帖称:互联网大头兵夫妇,两个人都 30+ 了,老公 xhs 后端年包 80+,我私企年薪 20 左右,老家三线城市有房。本来今年要孩子了,但我这边最近又在搞优化感觉很不稳定,老公这边还挺好,他们组的 Id 和业务线比我稳一些,不知道能不能算是...
继续阅读 »

今日热帖,有网友发帖称:互联网大头兵夫妇,两个人都 30+ 了,老公 xhs 后端年包 80+,我私企年薪 20 左右,老家三线城市有房。

本来今年要孩子了,但我这边最近又在搞优化感觉很不稳定,老公这边还挺好,他们组的 Id 和业务线比我稳一些,不知道能不能算是风险对冲。

主要焦虑的点还是怕有了孩子花销太大,不知道要不要回老家发展,还是继续在大城市坚守。

2023 年中国的出生人口是 902 万,比上一年少了 208 万,已经跌破一千万了。看到这个网友的担忧,有这个出生率也就不奇怪了。

这可是年收入超过 100 万的家庭啊,这个收入都不敢生,那还有人敢生吗?

不过话说回来,现在养娃也确实费钱。回想我小时候,哪穿过什么尿不湿,一个开裆裤就养大了。

现在呢,孩子一下生就得要个月嫂、尿不湿、吸奶棒、磨牙棒、奶粉、果泥、肉泥;长大了就开始各种兴趣班,钢琴、游泳、轮滑、英语、乐高,哪一个都得学。

怪不得都说孩子就是专业碎钞机,分分钟都在烧钱。

花钱是一方面,更重要的是会牵扯家长的精力。我听好多朋友说过,有了孩子之后,就很少睡过一个整觉。

再有一个就是教育,咱们都属于穷什么都不能穷教育,在教育上,无论多少钱都能花出去。

去年年底,好多小朋友生病,结果就是一边在医院打点滴,一边做作业,太卷了。

大人在公司卷,小孩在学校卷,卷在起跑线上。

再有一个原因就是缺乏安全感。

现在经济形势不好,公司随时都有可能裁员,一旦被裁员,就断了收入,就算有一定的积蓄,也会很焦虑。

大环境我们无法改变,只能从自身出发,调整好心态,积极乐观,从容面对。

1、明确自我定位,了解优劣势,规划未来职业道路

这也是了解自己的一个过程,了解自己的优势和劣势,目标是什么。只有目标明确了,才能走的更加坚定。

2、持续学习

这一点无需多言,活到老学到老,不管是哪一个行业,做什么类型的工作,持续学习都是必须的。

3、保持身体健康

身体是革命的本钱,特别是程序员,本来就加班很多,而且每天久坐,很多人身体都不是很好。一定要保持一定时间的运动,积极锻炼,有一个好的身体才能更好的工作。

熬过这个经济周期,就又是一条好汉。

之前看过一个说法,大概意思是说如果你现在不吃苦,不在大城市扎根,那将来就得你孩子来吃苦。

我之前感觉这句话太对了,所以一定得努力,留在大城市。但现在不这么想了,凭啥就得我吃苦?宁愿孩子吃苦,也不要自己吃苦。

过年回老家,和老家的同学和朋友聊天。我说我每天基本都是十点多下班,甚至十一点或者十二点,他们都觉得不可思议。

毕竟他们都是五点多就下班了,六点多吃完饭,就是老婆孩子热炕头了。

这可能就是世界的参差,我们总在一个环境下生活,就认为生活就是这样的,工作就是应该加班的,但实际上还有很多其他的生活方式。

就看我们有没有发现,有没有勇气去尝试。

我不想把自己陷入到大城市好,还是小城市好的争论中去,毕竟我原来是铁了心的想要在大城市,但现在已经动摇了。

还是那句话,没有哪一个选择是标准的,也没有哪一个选择是一层不变的。

但有一个原则是可以坚持的,那就是选择让自己舒服的。


作者:程序员贝塔
来源:mp.weixin.qq.com/s/aQF-FEwdLkvIPFMi3ZtKWQ

收起阅读 »

年后面试,最好不要有这几种心态

大家好,我是老三,大家新年好,我在朋友圈看到有朋友已经在大张旗鼓地“内卷”,为年后的面试做准备。 成功的面试常常是源于实力+运气,失败的面试可能会有各种各样的原因,知识点的盲区、和面试官不对眼、经验不匹配……很多东西我们是没法控制的,只能尽量做好自己能做的——...
继续阅读 »

大家好,我是老三,大家新年好,我在朋友圈看到有朋友已经在大张旗鼓地“内卷”,为年后的面试做准备。


成功的面试常常是源于实力+运气,失败的面试可能会有各种各样的原因,知识点的盲区、和面试官不对眼、经验不匹配……很多东西我们是没法控制的,只能尽量做好自己能做的——八股更熟一点、算法多刷一刷,硬技能相关的暂且不多聊,我们今天聊一聊,心态相关的,哪些心态,是求职面试中最好不要有的。


我得找个特别完美的工作


“钱多事少干的爽”,几乎是所有打工人的终极梦想,但是回归现实,真有这样的工作,凭什么流转到普通人身上呢?


有人可能会说,某某在XX公司,你看干的轻松,年终还发的贼多,真爽啊。这其实可以算幸存者偏差,我们有时候只能看到经过某种筛选而产生的结果,比如今年微信视频发了十几个月的年终,一度在各个公众号刷屏——我们可能觉得微信给的钱真多啊!但其实想想,哪怕同属WXG,除了微信视频,其它的部门也有这么“钱多”吗?


“钱多”、“事少”、“干的爽”,三者里面能满足一个就很不错了,钱多一般事也多,事少一般钱也少,”干的爽“更是一个非常主观的因素,自己不去干一干真不知道,干了也可以调整。


老三经历了几次跳槽,找工作的心理预期没那么高,为什么呢?我过去的工作经历基本上从低位到高位跳的一个过程,但是有的同学比较优秀,可能一出道就是在高位,刚工作就是大厂SSP。


这些有实力的同学,也许也会面临一个幸福的烦恼,就像谈恋爱,如果一个女生的初恋特别优秀,她后面可能会单身很久,因为她发现很难找到比初恋更优秀的男朋友。“曾经沧海难为水,除却巫山不是云”。


经济学上有一个“锚定效应”:



人们需要对某个事件做定量估测时,会将某些特定数值作为起始值,起始值像锚一样制约着估测值。在做决策的时候,会不自觉地给予最初获得的信息过多的重视。



有这么好的“初恋”,一般他们都不会轻易“分手”,但是假如遇到了一些波动,主观或者客观的原因,导致“分手”,那么在找下一份工作的时候,可能需要正视“锚定”的因素。


我们找工作的时候,也会去“锚定”,和现在的工作去比,和其它人的工作去比,但其实,相比评估价值,可能找准自己的需求更加重要,钱多发展职级大厂背书业务匹配稳定工作生活平衡 ……可以像排工作任务一样,排一下需求的优先级,哪些是必须的,哪些是可以妥协的。


找工作也和找对象类似,我有一个哥们,特别喜欢古力娜扎,可能很多人的女神也是迪丽热巴、古力娜扎、马尔扎哈……但是应该没有普通人会把自己找对象的目标定在这些女神头上。这就涉及到“吸引力”和“可得性”的平衡,很多时候,一份好的工作,对我们的“吸引力”是巨大的,但是我们也不得不承认它的“可得性”是极低的。


外包,是互联网软件行业一个比较普遍,而且越来越普遍的用工方式,有的朋友在外包——“你为什么要做外包呢?”,我觉得一般人也不会问这个问题,这就像之前有个主持人:“为什么不吃肉呢?因为肉不好吃吗?”生活所迫嘛,我们很多时候,只是在可选的选择里选一个相对最优的。


尤其是在现在整体行情不景气,如果真的没有办法,比如空窗了很久,妥协也是可以接受的,华为进不去OD也能凑合。


在经济学上,资源的配置常常达不到最优,找工作也是一样,能在一定可达性的基础上,通过努力获取次优的选择,已经很不错了。


“幻想不是理想”,这个世界上少有完美的工作,也少有完美的人生,我们很多时候,都要接受这些不完美,做一些适当的取舍。


留下还是跑路,好纠结


45度的人生最难受。


我去年经历了很纠结的时刻,当时的工作满足不了我的需求,但是我又不敢跑路,因为市场的寒气实在让人望而却步,就在这种纠结中,我错过了最好的面试备战和简历投递的时机,当时的第一目标快手,等我十月份觉得可以面一面的时候,HC已经锁了一个月了。


最怕的就是这种纠结的心态,工作投入不进去,面试也准备不好。还是那句话,找准现在的问题和需求。


第一,我现在的工作是不是已经非常让我难以接受了?有哪些点让我难以接受?是客观的原因还是主观的原因?我有没有尝试最大的努力去解决?我有没有寻求别人的帮助?


第二,现在这份工作的问题解决不了,我能通过跳槽解决吗?是不是换一个公司,换一个环境,就能解决或者缓解我的问题?什么样的公司能解决或者缓解我的问题?这样的公司对我的可得性大概有多少?


想清楚这两点,就能确认自己离开的意向,留下就好好干,努力去解决问题;要走就做好规划,挤出时间准备。


第三,工作和面试,不是绝对冲突的。只要有想离开的想法,再忙每天也能抽点时间出来准备的,把刷抖音,打游戏的时间拿一部分出来准备面试,做好规划,每天时间充足就拉短周期,每天时间少就拉长周期。


其实很多的时候的纠结,只是给自己找放纵的借口,“其实,我不是特别想走”,“外面现在行情很差,没什么机会”,这样一来,就可以心安理得地玩游戏、刷抖音,至于面试准备,后面再说吧。


我也经历了这样当鸵鸟的时期,直到一些关系比较好的同事因为各种原因离开,再加上一些裁员的传言,才彻底打醒了我。


打破纠结的最好办法就是行动,最怕的就是找个借口,让自己停在了原地。


这个面试没啥,随便面面


面试有自信是一件好事,比如老三,可能会觉得自己的八股比较熟;有的朋友,可能会觉得,自己的算法比较溜;有的觉得自己的项目比较亮眼。


自信,不是轻敌。我有个朋友吃了这样的亏,面飞猪的时候,随便约了个时间,当时那个时间是在旅行中,感觉的时候觉得很简单,结果挂了,事后一问,面试官说根本听不清;后面他面快手,也吃了同样的亏,因为没怎么准备,一面挂在了自己最擅长的算法上。


同样,面试,难和简单,也是一个很主观的感觉,别人感觉简单的,自己未必简单,还是那个朋友,他面飞猪,相对简单,我面飞猪,写了两道题,几乎问到了所有知识点的八股,足足面了九十多分钟。


认真对待每一场面试,现在这个环境,每一个机会都是非常难得的,很多面试机会,错过了就不再有。每一场面试,都尽量拿比较好的状态去面。


有的人说,我不咋想去,只是练手,练手也要认真一点,就像模拟考,也得好好考。而且现在不想去,可能只是了解不多,后面如果了解多了,未必不是完全不想去。


完全准备好了再面


面试,永远没有完全准备好的时候。很多时候,觉得准备个七八成的时候,就可以去面了。


第一,很多岗位的窗口期是很短的,在现在这种僧多粥少的局面下,一个岗位可能很短的时间就关闭了,如果不及时地去面,可能就会失去这个机会。


第二,面试永远会有答不上来的问题,面试本身就是个非对称的场景,以面试官之长,攻己之短,面试,也有一些运气的成分在里面。


第三,准备六个月,未必效果比三个月好,人的投入,是有边际递减的效应的,短时间高效率的准备,也许比拉长战线更有效果。


机会来了,估计着差不多就冲,我有个同事,很短的时间准备,但是因为年底的机会很好,也认真地准备了,一年多的经验,拿下了美团和京东的Offer。


我和面试官是对立的


面试是一场交流,一头是面试官,一头是候选人,有这么一个段子,面试是”三分天注定,七分靠打拼,九十分看面试官心情“,我和这样一个能一票否决我的人,我应该怎么打交道呢?


对立情绪是不可取的,在考试这个逻辑的下面,其实我们和面试官还有合作的逻辑。我以前作为面试官,面了几十个人,面到后面,我的感受是什么呢?赶紧招一个合适的人选吧,别让我面试了。


在这种关系里,面试官也是有需求的,面试官希望尽快招到合适的人,让自己早点摆脱面试对自己的干扰。面试官也是有投入的,毕竟面试的时间,基本都不会算进工作排期。


既然双方都有需求和投入,当然候选人的需求和投入会更多,那么这段关系就不是完全从上到下俯视的,我觉得最好的面对面试官的心态,就是萍水相逢的路人,尊重但不谄媚、存异但是和谐、热情但有边界,如果有缘份的话,我们可能会成为真正的朋友,如果没有缘分那就相忘江湖。


很多人说,程序员都不太擅长交流,但至少请保持礼貌,“您好”,“谢谢”,“不好意思”,“感谢你的时间”,我觉得这些礼貌用语,应该小孩子都会吧。


面试,不仅有硬实力的碰撞,也有人情世故的交融。


面试官让我不爽


我在之前,某个博主的文章里,看到他在某个公司的三面挂了,因为感觉和面试官不太对味。根据我的经验,其实三面很少挂人,这种,就是和面试官的”碰撞“激烈了一些。


我们应该明白一个道理:在面试的流程中,我们是没有挑选的权利的,只有拿到Offer后,我们才是真正有挑选的权利。


这个世界我们不认同的东西多了,要么改变它,要么适应它,这句话不太好听,既然选择了打工,那大概率是只能适应的。


面试里,面试官说的东西,我们可能不认同,面试官的态度甚至可能不友好,我觉得我们能做的,就是保持礼貌:


“对对,你是对的。”


我去年面西二旗某厂,三面面试官给我的感觉是无知且蛮横,她的很多问题都让我无言以对,但是我还是全程保持了微笑,努力保持了风度,没想到最后面试过了。


如果你真的看面试官不爽,在Offer后拒绝他们,不比在面试阶段,闹得不欢而散,最后只能心里默默骂着傻叉要痛快?


面的不好,放弃吧


面试永远有我们不会的问题,这是正常的。可能在某个问题卡壳之后,心态就慢慢崩掉,破罐子破摔。这时候,一定要给自己一些积极的心理暗示,面试直接发生在面试官和候选人之间,间接发生在候选人和候选人之间。


“我这种八股文职业选手都答不好,其他人更不行了。”


而且,答的不好,未必就是没戏,很多时候,面试官可能通过一些开放性的问题或者压力面的方式,考察候选人的广度深度,思维方式。


所以,哪怕偶有一个点答不好,也一定要绷住心态,我之前作为面试官,面试实习生,一个候选人,坦白说,她面的不好,同事的实习生,他觉得可以,那就通过吧,结果告知她面试通过后,那个候选人心态崩了,放弃了后面的面试。


最差不过挂了,最坏的结果都能接受,那就朝最好的方向尝试努力。


失败了好几次,没信心了


我们经常在网上,刷到一些“Offer收割机”“吊打面试官”,但是经过我本人的实践,反正我是做不到,比如美团,我面了四个岗位才拿到Offer。


面试失败,是一件很正常的事情,“面了不一定能过,过了不一定能Offer”,我们可能因为各种原因挂掉。但是,千万不能因为失败几次,就灰心丧气,面一次总结一次,查缺补漏,面的多了,自然会面了,就能面过了。


而且面试也有运气的成分,此路不通,未必别路也不通。


很多人恐惧面试,其实我也怕面试,每次面试前,都很忐忑,但是想一想,最差不过挂了,越淡定的面试发挥地越好,越紧张的面试发挥地越差。


在挫折里不断进步,保持心态,才有机会拿到好的结果。






工作是为了更好地生活,生活是为了快乐和幸福,每个人的快乐和幸福都是自己定义的,祝大家既能找到好的工作,也能享受生活的快乐和幸福。






参考:


[1].《长的好看,能当饭吃吗:提升认知的33个经济学常识》


[2].《经济学原理》






作者:三分恶
来源:juejin.cn/post/7334623638116057099
收起阅读 »

前端又又出新框架,这次没有打包了

web
最近,前端开发领域又迎来了一个新框架——ofa.js。它的独特之处在于,不依赖于现有的 nodes/npm/webpack 前端开发工作流程。与jQuery类似,只需引用一个脚本,您就能像使用React/Vue/Angular一样轻松地开发大型应用。 极易上...
继续阅读 »

最近,前端开发领域又迎来了一个新框架——ofa.js。它的独特之处在于,不依赖于现有的 nodes/npm/webpack 前端开发工作流程。与jQuery类似,只需引用一个脚本,您就能像使用React/Vue/Angular一样轻松地开发大型应用。


punch-logo.png


极易上手


如果您要开发简单的项目,想要用一个漂亮的按钮组件,例如 Ant Design 中的 Button组件,你需要学习Node.js、NPM和React等知识,才能开始使用该按钮组件。对于非前端开发者或初学者来说,这将是一个漫长的过程。


如果使用基于ofa.js 开发的组件,就不需要这么复杂了;你只需要了解HTML的基础知识(即使不看ofa.js的文档),也可以轻松使用基于ofa.js开发的组件。以下是使用官方的 punch-logo 代码示例:


<!-- 引入ofa.js到您的项目 -->
<script src="https://cdn.jsdelivr.net/gh/kirakiray/ofa.js/dist/ofa.min.js"></script>

<!-- 加载预先开发的punch-logo组件 -->
<l-m src="https://ofajs.github.io/ofa-v4-docs/docs/publics/comps/punch-logo.html"></l-m>

<!-- 使用punch-logo组件 -->
<punch-logo style="margin: 50px 0 0 100px">
<img
src="https://ofajs.github.io/ofa-v4-docs/docs/publics/logo.svg"
logo
height="90"
/>

<h2>不加班了</h2>
<p slot="fly">下班给我</p>
<p slot="fly">迟点下班</p>
<p slot="fly">周末加班</p>
</punch-logo>

punch-demo.gif


你可以最直接拷贝上面的代码,放到一个空白的html文件内运行试试;这使得ofa.js非常容易与传统的Web开发技术栈相融合。


一步封装组件


封装组件同样非常简单,只需一个HTML文件即可实现。以下是一个官方封装的开关(switch)组件示例:


<!-- my-switch.html -->
<template component>
<style>
.switch {
position: relative;
display: inline-block;
width: 60px;
height: 34px;
background-color: #ccc;
transition: background-color 0.4s;
border-radius: 34px;
cursor: pointer;
}

.slider {
position: absolute;
height: 26px;
width: 26px;
left: 4px;
bottom: 4px;
background-color: white;
transition: transform 0.4s;
border-radius: 50%;
}

.switch.checked {
background-color: #2196f3;
}

.switch.checked .slider {
transform: translateX(26px);
}
</style>
<div class="switch" class:checked="checked" on:click="checked = !checked">
<span class="slider"></span>
</div>
<script>
export default {
tag: "my-switch",
data: {
checked: true,
},
};
</script>
</template>

在使用时,只需使用 l-m 组件引用它:


<script src="https://cdn.jsdelivr.net/gh/kirakiray/ofa.js/dist/ofa.min.js"></script>
<l-m src="./my-switch.html"></l-m>
<my-switch></my-switch>

switch.gif


示例可以在官方网站下方查看。由于无需打包流程,只需将文件上传到静态服务器即可发布,还可以进行跨域引用,这极大降低了组件共享的成本。


多种模板语法糖


ofa.js与Vue和Angular一样提供了许多模板语法糖,主要包括:



  • 文本渲染

  • 属性绑定/双向绑定

  • 事件绑定

  • 条件渲染

  • 列表渲染

  • ...


具体案例可在官网向下滚动至“提供多样便捷的模板语法”处查看。


天生的状态同步高手


与其他框架不同,ofa.js 使用无感状态同步。这意味着数据不需要通过函数操作,只需设置数据对象即可实现状态同步。以下是一个共享黑夜模式的按钮示例:


// is-dark.js
const isDark = $.stanz({
value: false,
});

export default isDark;

<!-- my-button.html -->
<template component>
<style>
:host {
display: block;
}

.container {
display: inline-block;
padding: 0.5em 1em;
color: white;
border-radius: 6px;
background-color: blue;
cursor: pointer;
user-select: none;
}
.container.dark {
background-color: red;
}
</style>
<div class="container" class:dark="isDark.value">
<slot></slot>
</div>
<script>
import isDark from "./is-dark.js";
export default {
data: {
isDark: {},
},
attached() {
// 共享dark对象数据
this.isDark = isDark;
},
detached() {
// 清除内存记录
this.isDark = {};
},
};
</script>
</template>

sync-state.gif


您可以跳转到 状态同步案例 以查看效果。


最简单的表单操作


表单只需调用formData方法,就能生成自动同步数据的对象:


<form id="myForm">
<input type="text" name="username" value="John Doe" />
<div>
sex:
<label>
man
<input type="radio" name="sex" value="man" />
</label>
<label>
woman
<input type radio="radio" name="sex" value="woman" />
</label>
</div>
<textarea name="message">Hello World!</textarea>
</form>
<br />
<div id="logger"></div>
<script>
const data = $("#myForm").formData();

$("#logger").text = data;
data.watch(() => {
$("#logger").text = data;
});
</script>

form1.gif


您还可以轻松地反向设置表单数据:


<form id="myForm">
<input type="text" name="username" value="John Doe" />
<div>
sex:
<label>
man
<input type="radio" name="sex" value="man" />
</label>
<label>
woman
<input type="radio" name="sex" value="woman" />
</label>
</div>
<textarea name="message">Hello World!</textarea>
</form>
<br />
<div id="logger"></div>
<script>
const data = $("#myForm").formData();

setTimeout(() => {
// 反向设置数据
data.username = "Yao";
data.sex = "man";
data.message = "ofa.js is good!";
}, 1000);
</script>

form2.gif


制作自定义表单组件也没有其他框架那么复杂,只需为组件定义valuename属性即可。


具体效果可跳转至formData API查看。


开发应用


您还可以使用ofa.js开发Web应用,然后直接引用已开发的应用到您的网页上:


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>应用演示</title>
<script src="https://cdn.jsdelivr.net/gh/kirakiray/ofa.js/dist/ofa.min.js"></script>
</head>
<body>
<o-app src="https://xxxxx.com/app-config.mjs"> </o-app>
</body>
</html>

具体效果可跳转至使用o-app组件查看。


SCSR


官方提供了一种类似服务端渲染的解决方案,它不仅保证了用户体验,还使页面在静态状态下可被搜索引擎爬取。官网采用了SCSR的方案。



SCSR的全称是Static Client-Side Rendering,又称为静态客户端渲染。它是CSR(Client-Side Rendering)的一种变种,在保留了CSR用户体验的基础上,还能够让页面在静态状态下被搜索引擎爬取。



您可以点击SCSR方案以查看详细信息。


代码简洁


当前版本4.3.29的 ofa.min.js 文件仅有52KB,经过gzip压缩后仅有18KB。


其他


最近升级到了v4版本,目前可用的第三方库还比较有限,但以后将逐渐增加。作者正在准备开发基于ofa.js的UI库。


作者是一位中国开发者,快去给 ofa.js 加星吧!


作者:皮卡怪兽
来源:juejin.cn/post/7295576148364460071
收起阅读 »

全世界都想让程序员专心写业务代码

最近两年我不知道你是否和我拥有同样的感受,编程容易了许多,同时乐趣也少了许多 一 最近有两个契机让我写这篇文章,一是过去一年我陆续把我所有 side project 后端从 Azure App Service 迁移到 Digital Oce...
继续阅读 »

最近两年我不知道你是否和我拥有同样的感受,编程容易了许多,同时乐趣也少了许多



最近有两个契机让我写这篇文章,一是过去一年我陆续把我所有 side project 后端从 Azure App Service 迁移到 Digital Ocean(以下简称 DC) 的 Droplet(Virtual Machine),再迁移到 DC 的 K8S,目的是剥离应用内托管型(managed)资源;前端顺便也从 Azure Static Web Apps 迁移到 Vercel;二是最近读完《Competing in the Age of AI》,其中关于于 operation model 的论述与近期迁移应用的感受不谋而合


书中把公司价值划分为两部分,首先是商业模型(business model),即为消费者带来了何种价值;其次是运营模型(operation model),即如何将价值传递到消费者手中。运营的难点不在交付本身,而在于如何大规模(scale)、大范围(scope)交付,并且持续对交付方法改进和创新(learning)


同样,如果代码的价值在于上线(通俗说派上用场),无论是对自己还是客户还是用户,那么交付对我们来说同样重要,以上所说的大规模、大范围、以及保证持续改进的交付难点也同样成立。


先上线,剩下的以后再说



你们有没有想过 Netlify 和 Vercel 究竟是做什么生意的?如果你的答案依然是 JAM stack,只能说明你已经 out 了。不妨去看看 Netlify 官网的 slogan,甚至不用是 slogan,看看网站首页的 title 写的是什么:



Scale & Ship Faster with a Composable Web Architecture



Netlify 所做的正如上述所标榜的,帮助代码更快的交付上线。换而言之,它们售卖的的是一站式 web 应用的部署解决方案,除 host 和流水线外,还提供免费的用户行为分析、页面性能监控、日志等工具,以及 Redis/SQL/Blob 数据库。


Azure 不是也提供类似的服务吗?问题的关键在于不同平台间的开发者体验,Vercel 能够真正做到上述服务一键集成,你会明显感觉到 Vercel 在加速你的交付,让你更快得到对于产品的反馈。而在 Azure 下开发更像是赤裸裸的买卖,Azure 团队按照他们的理解提供一个最低限度可以使用产品,至于多好用,多大程度能和其它 Azure 服务契合则不在他们的考虑范围内。如下图所示在 Vercel 的 dashboard 上,你可以纵览所有从部署状态到线上监测一系列信息。试想一下如果在 Azure 上集成所有对应的功能并呈现类似的 dashboard 需要付出多少外的精力


vercel-screenshot.png



再简单聊聊 DevSpace。这里的 DevSpace,也可以是 tilt,可以是 skaffoldtelepresence


GitOps 无疑是伟大的,但这种伟大里依然充满了“一板一眼”——我的一次上线需要经过 push 代码、创建 release、推送镜像到GHCR、更新 yaml、通过 ArgoCD 部署。我理解它们是 necessay evil。


相比之下 DevSpace 看上去是反流程的,它可以帮助你在本地直接创建镜像并执行部署,还允许热更新容器内文件。即便是创建镜像,我发现它利用也非是既有的 pipeline,而是借助 buildkit 在我的集群上创建一个 local registry 并将镜像储存在此。


别搞糊涂了,生产环境和开发环境具有完全相反的气质,我们希望生产环境稳定、可追溯,这是 GitOps 的优势所在,但开发过程具有破坏性、需要不停试错,DevSpace 着重解决的则是开发体验问题,对于代码修改它可以带来高效的反馈。


compiling-joke.png


上图已经流传了很多年,图中的 compiling 可以替换成 uploading, deploying,running test 等等。过去十年它是事实,现在应该只是一个笑话



在 Vercel 和 DevSapce上我们看到了一类区别于传统的开发方式,这里没有 pipeline,没有 IaC,没有手动的统计埋点,无需申请第三方资源,从 GitHub 上导入 repo 的那一刻起,它自动为你匹配部署脚本,寻找项目入口。开发者所需要做的,就是专心编写他所需要的业务代码


模板化的项目是带来这些便利的主要原因,继续往上追溯,主流前端框架自带 CLI 工具的流行功不可没,从大到创建一个新项目小到执行编译,命令行一键即可完成。无论是 React,Vue 还是 Angular,你都不会在官方的教程里找到专门的章节教你为项目编写 webpack 或者 vite 的配置。也许是因为习惯了早年间凡事都需要自己动手的工作方式,我花了很长时间才想通并接受这件事:传统从0到1搭建项目的能力已不再重要。


越来越多的环节像一个个黑盒出现在开发流程中,在此之前如果我对 pipeline 感兴趣,我可以阅读 .github 文件夹中的代码看 action 是如何定义的,而现在我连机会都没有了。连同代码被一同干掉的还有程序员的编程乐趣。站在交付的角度上来说这种乐趣并没有什么道理:为什么我要花上几天的时间只为了研究如何写一段 yaml 把代码送上线运行,更何况这段代码带来不了任何产品收益。


最近另一个类似的体验是,Next.js 编写起来太无趣了,不是说它不够好,而是问题在于它太好到你只能这么写。说起来有些贱,我很怀念那个一种功能有十几个写法,然后这十几个写法还有鄙视链的“坏”时光


当我们再度审视代码的生命周期,你会发现所有人努力的目标,是想让程序员尽可能少的关心核心业务代码以外的事情。一切一切都在想方设法优化运营模型。很明显个性化配置和重复劳动在规模化前是不受欢迎的



也许换个思路会好受一些:当你加入一家公司之后,发现同等体量的应用利用市面上的工具可以做到随时上线;而你们的产品却只能选择在每个周六凌晨部署,并需要若干人值班守候,这听上去让人有些失望是不是。


cover.jpg


作者:李熠
来源:juejin.cn/post/7336538738750013440
收起阅读 »

我收到的最好的职业建议

Nicholas C. Zakas 是全世界最著名的 JavaScript 程序员之一。 今天突然看到大神在13年写的一篇长文,回顾自己的职业生涯,提到七个对他来说最重要的建议 个人觉得每一条都很受益;所以决定翻写出来;做一个学习记录。 一、Don’t be...
继续阅读 »

image.png
Nicholas C. Zakas 是全世界最著名的 JavaScript 程序员之一。


今天突然看到大神在13年写的一篇长文,回顾自己的职业生涯,提到七个对他来说最重要的建议


个人觉得每一条都很受益;所以决定翻写出来;做一个学习记录。




一、Don’t be a short-order cook 不要做一个点菜厨师



你要别人点什么;你就做什么。积极表达自己的见解



我的第一份工作持续了8个月,因为公司倒闭了。当我和我的经理谈论我下一步该怎么做时,他给了我这样的建议:


尼古拉斯,你的价值不仅仅在于你的代码。无论你的下一个工作是什么,一定要确保你不是一个按需制作的厨师。不要接受那种你被告知要构建什么以及如何构建的工作。你需要在一个既欣赏你对产品的见解,又欣赏你构建能力的地方工作。


这是我在整个职业生涯中一直牢记的事情。仅仅做一个实现者是不够的--你需要参与到实现的过程中。优秀的工程师不只是服从命令,他们会给产品负责人给予反馈,并与他们合作,使产品更好。幸运的是,我明智地选择了我的工作,从来没有在人们不尊重或不重视我的见解的情况下结束。


个人反思:在选择一份工作的时候,一定要有自主权;这个自主权可以是自己争取的;通过表达意见以及个人反馈来进行争取。侧面也说明需要自己对相关系统有更深的理解。


二、Self-promote 自我推销



推销自己



进入雅虎后;有一天,我在雅虎的团队经理把我拉到一边,给予我一些建议。他一直在看我的代码和产出内容,告诉我,觉得我有点缺失:


你做得很好。我是说真的很棒。我喜欢你的代码,它很少有bug。但是; 问题是别人看不到这一点。为了让你的工作得到认可,你必须让别人知道。你需要做一些自我推销来引起注意。


我花了一点时间来消化他说的话,但我终于弄明白了。如果你做了很好的工作,但没有人知道你做了很好的工作,那么它并没有真正帮助你。你的经理可以支持你,但不能为你辩护。组织内部的人需要了解你的价值,最好的方法就是告诉人们你做了什么。


个人反思:做一个角落里静静编码的工程师,并不可取。你的主管会支持你,但是他没法替你宣传。公司的其他人需要明白你的价值,最好的办法就是告诉别人你做了什么。一封简单的Email:"嗨,我完成了XXX,欢迎将你的想法告诉我",就很管用。


三、It’s about people 关注人员,学会团队相处



学会带领团队



在我职业生涯的早期,我非常注重头衔。我一直想知道我必须做些什么才能得到晋升。在雅虎主页上与我的新经理进行第一次一对一的交谈时,我问我需要什么才能获得晋升。他的话至今仍在我耳边回响:


当你的技术能力过关以后,就要考验你与他人相处的能力了。


从那时起,我不确定我是否对软件工程专业有了更好的了解。他是完全正确的。那时,没有人质疑我的技术能力。众所周知,我是一个写出优秀、高质量代码的人,很少有错误。我缺乏的是领导能力


从那时起,我看到无数工程师在他们的职业生涯中被困在一个层面上。聪明的人,优秀的代码,但无法与他人有效合作,使他们留在原地。每当有人觉得自己被困在软件工程生涯中时,我都会讲述这个建议,而且它总是在金钱上是正确的。


个人反思:在团队工作中,除了完成自己的工作后;更多需要学会与团队成员的相处,这决定了你是否能够走向管理;当然;领导能力也是自己需要认真培养和学习的。


四、 of this matters 这些都不重要



生活才是最重要的



我在雅虎度过了一段令人沮丧的时期。也许沮丧不是正确的词,更像是生气。我愤怒地爆发,经常与人争吵。事情出了问题,我不喜欢这样。在一个特别艰难的一天,我问我的一位导师,当很多事情出错时,他是如何保持冷静的。他的回答是:


这很简单。你看,这些都不重要。所以一些蹩脚的代码被签入了,所以网站宕机了。那又怎样?工作不可能是你的全部生活。这些不是真正的问题,而是工作问题。真正重要的是工作之外发生的事情。我回家,我的妻子在等我。这真是太好了


我从马萨诸塞州搬到加利福尼亚,很难交到朋友。工作是我的生命,它让我保持理智,所以当它不顺利时,这意味着我的生活不顺利。这次谈话让我意识到我的生活中必须有其他事情发生,我可以回去忘记我在工作中遇到的麻烦。


他是对的,一旦我改变了思维方式,将工作中烦人的事情重新归类为“工作事情”,我就能够更清楚地思考。我能够在工作中冷静下来,与人进行更愉快的互动。


个人反思:把工作和生活分开,工作不是你的全部;学会分清楚这个是工作问题;不要把工作问题带出来的情绪带入到生活中。


五、Authority, your way 自主,自己决策



找到自己道路



当我第一次被提升为雅虎的首席工程师时,我和我的主管坐下来,以更好地了解这个角色的含义。我知道我必须更像一个领导者,但我很难自主决策。我寻求帮助。他是这样说的:


以前都是我们告诉你做什么,从现在开始,你必须自己回答这个问题了,我期待你来告诉我,什么事情需要做。


很多工程师都没有完成这个转变,如果能够做到,可能就说明你成熟了,学会了取舍。你不可能把时间花在所有事情上面,必须找到一个重点。


个人反思:你需要慢慢的学会自主决策,成为专家。找到自己的风格;并显现出来。


六、Act like you’re in charge 把自己当成主人


我每天要开很多会,有些会议我根本无话可说。我对一个朋友说,我不知道自己为什么要参加这个会,也没有什么可以贡献,他说:


"不要再去开这样的会了。你参加一个会,那是因为你参与了某件事。如果不确定自己为什么要在场,就停下来问。如果这件事不需要你,就离开。不要从头到尾都静静地参加一个会,要把自己当成负责人,大家会相信你的。"


从那时起,我从没有一声不发地参加会议。我确保只参加那些需要我参加的会议。


七、找到水平更高的人


最后,让我从自己的经历出发,给我的读者一个建议。


"找到那些比你水平更高、更聪明的人,尽量和他们在一起,吃饭或者喝咖啡,向他们讨教,了解他们拥有的知识。你的职业,甚至你的生活,都会因此变得更好。"


作者:GleanWu
来源:juejin.cn/post/7337227407365423131
收起阅读 »

在上海做程序员这么多年,退休后我的工资是多少?

大家好,我是拭心。 最近看到一个很可惜的事:有个阿姨在深圳缴纳了 12 年社保,第 13 年家里突然有事不得不回老家,回去后没再缴纳社保,结果退休后无法领退休工资,还得出来打工赚钱。 之所以这样,是因为阿姨及其家人对退休的相关知识了解不多,痛失一笔收入。 吸取...
继续阅读 »

image.png


大家好,我是拭心。


最近看到一个很可惜的事:有个阿姨在深圳缴纳了 12 年社保,第 13 年家里突然有事不得不回老家,回去后没再缴纳社保,结果退休后无法领退休工资,还得出来打工赚钱。


之所以这样,是因为阿姨及其家人对退休的相关知识了解不多,痛失一笔收入。


吸取教训,作为在上海工作多年的打工人,为了老有所依,我花了些时间学习了养老金相基本知识,并且估算了一下退休后每月能拿到的钱,这篇文章来聊聊。


文章主要内容:



  1. 如何能在上海领退休工资

  2. 我退休后大概能领多少钱

  3. 退休工资的组成


如何能在上海领退休工资


image.png


上海作为全国 GDP 第一的城市,居民的收入也是很可观的。平均工资在 2023 年达到了 12183,平均退休工资在全国也位列前茅:


image.png


从上图可以看到,上海的平均退休工资居然有五千多!不用早起不用挤地铁,躺在家里每月就能领五千多,花不完,根本花不完啊!


那么问题来了,我们如何能领到上海的退休工资?


主要有 2 个条件:



  1. 达到退休年龄:男性满 60 周岁,女性满 50 周岁(灵活就业人员需要满 55 周岁)

  2. 退休前累计缴费社保 >= 180 个月(也就是 15 年),其中在上海至少缴满 120 个月(10 年)


第二点很关键:在上海需要缴满社保 10 年,上海 + 其他地方累计需要缴满 15 年。 比如小张在上海工作并缴纳社保满 10 年,然后去青岛缴纳 5 年,最后可以在上海领退休工资;但如果在青岛缴纳了 10 年,在上海缴纳了 5 年,就无法在上海领退休工资了。


需要强调的是,这里说的是「累计缴满」,即使中间有断开也没关系。


还有一个细节是,发工资不等于缴纳社保,个别不正规公司会漏缴社保,这需要我们打工人自己多关注。 身边有朋友遇到过:刚毕业加入的公司规模很小,人力资源不靠谱,干了半年多只缴纳了两个月社保。


怎么看公司有没有给自己缴社保呢?我们可以从随申办上查询:


image.png


OK,这就是在上海领退休工资的条件。


退休后大概能领多少钱


掐指一算,社保缴满 15 年的任务我已经完成了一半,但还有二十九年才能领钱,心里苦啊😭。


虽然拿不到,但我对能领多少钱还是非常好奇的,究竟比平均退休工资高还是被平均?🤔


经过一番搜索,我终于发现了退休工资的计算方法。


国家社会保险公共服务平台网站中,有一个「企业职工养老保险待遇测算」的功能:


image.png



国家社会保险公共服务平台 -> 养老保险 -> 企业职工养老保险待遇测算


链接:si.12333.gov.cn/157569.jhtm…



我们只需要输入年龄、预计退休年龄、当前缴纳年限、目前及之前平均工资、养老保险个人账户余额及未来大概工资即可测算退休工资。


如果不知道你的「养老保险个人账户余额」有多少,可以从随申办 app 搜索「养老金」查询余额:


image.png


在填完所有需要的信息后,我的预算结果是这样的:


image.png


我的天,个十百千万,居然有两万五?这还花的完??


几秒后冷静下来,我才发现算错了。。。


两万五应该是这样算的:按照当前收入,再连续缴纳 29 年。 😂


image.png


对于我们程序员,保持收入 29 年基本是不可能的,我还是重新调整参数再看看吧。


我现在社保缴纳了七年半,如果缴够 15 年社保,退休后我能领多少钱呢?


image.png


答案是一万四!看着还不错哈,每天能有 480 元左右,就是不知道 30 年后的物价怎么样了😂。


如果再悲观一点,社保缴纳到 35 岁(然后最低标准缴够 15 年),退休后大概能领多少呢?


image.png


答案是一万元!


看了下人民币的贬值率,30 年后的一万元不知道有没有今天三千块钱的购买力😷。


OK,这就是我退休后大概能领到的工资范围。


退休工资的组成


从上面的预算结果中我们可以看到,养老金由三部分组成:基础养老金、个人账户养老金和过渡性养老金,它们都是什么意思呢?


1.基础养老金 = 退休时平均工资 ×(1+平均缴费指数)÷ 2 × 累计缴费年限 × 1%。


退休时平均工资指的是退休时所在地区上年度的社会平均工资。也就是说经济发达地区的基础养老保险金,要高于欠发达地区。


平均缴费指数指的参保人选择的缴纳比例(一般在0.6-3之间)。每个月社保的缴费比例越高,相应的基础养老金越高。


例如,小张退休时,上年度的社会平均工资是 12000。虎虎的缴费指数平均值是 1,累计缴存了15年,他的基础养老金约为:12000*(1+1)/2 * 15 * 1% = 1800。


2.个人账户养老金 = 养老保险个人账户累计金额 ÷ 养老金计发月数。


我们缴纳社保时,一部分会进入个人社保账户,一部分会进入国家统筹账户。个人账户的部分,直接影响退休养老金的计算。


计发月数和我们退休的年龄有直接的关系,退休的越晚,计发月数越少;退休的时间越早,计发月数越多。一般来说,按照 60岁 退休,计发月数是 139 个月。



退休金的计发月数只是用来计算退休金,而不是说只能领这么久的退休金。



例如,小张社保的个人缴纳比例为 8%,社保的计算基数是 9339,他选择在 60 岁退休,那他的个人账户养老金约为:9339 * 8% * 12 * 15 / 139 = 967.49。


3.过渡性养老金 = 退休时平均工资 × 建立个人账户前的缴费年限 × 1.3% × 平均缴费指数


过渡性养老金,是指在养老保险制度发生变化(比如缴费标准提高、计算方法改变、退休年龄调整)的时候,给予受影响群体的资金补充。


这个奖金的计算规则说法不一,一种比较广泛的计算方法是:退休时所在地区的平均工资 x 缴费指数 x 缴费年限 x 过渡系数,其中过渡系数大概在 1% 到 1.4% 之间。


OK,这就是养老金三部分组成的含义。


总结


好了,这篇文章到这里就结束了,主要讲了:



  1. 如何能上海领退休工资:缴纳 10~15 年社保,到达退休年龄

  2. 我退休后大概能领多少钱:30 年后的一万左右

  3. 退休工资的三部分组成:基础养老金、个人账户养老金和过渡性养老金


通过写这篇文章,我对养老金的认识更多了一些,希望国家繁荣昌盛,让我退休的时候能多领点钱!


作者:张拭心
来源:juejin.cn/post/7327480122407141388
收起阅读 »

22点下班的地铁上闲看发现一个现象...求解

打工人写代码写到晚上十点😭 匆匆忙忙赶上地铁,累的手机也不想玩,因为眼睛累,也不想坐因为坐了一天了,闲的蛋疼,也不知道怎么的,脑子里就出了这么个想法😓 我发现了一个奇怪的现象,无论是大学生,还是普通的上班族,或者城市写字楼里的白领,现在很多人,包括年轻人,脚上...
继续阅读 »

打工人写代码写到晚上十点😭
匆匆忙忙赶上地铁,累的手机也不想玩,因为眼睛累,也不想坐因为坐了一天了,闲的蛋疼,也不知道怎么的,脑子里就出了这么个想法😓


我发现了一个奇怪的现象,无论是大学生,还是普通的上班族,或者城市写字楼里的白领,现在很多人,包括年轻人,脚上穿的大部分都是运动鞋,很少有人穿皮鞋了,年轻人好像只有在结婚的当天,才会去穿皮鞋。


我记得,以前穿皮鞋才叫时尚,出门的时候,一个阳光的年轻人,为了给人一个好的形象,都会把皮鞋擦的增明透亮,上面再配上西装、西裤,走起路来,咔咔地响,那才叫一个酷,女孩子穿皮鞋的更多,上身穿什么的都有,脚上穿的基本上都是高跟皮鞋,走起路来,左扭一下,右扭一下,在大街上会很吸人眼球的。


不知从何时起,皮鞋突然失宠了,人们都喜欢穿运动鞋了,商场里售卖的鞋子,大部分也都是运动鞋,基本上看不到皮鞋的影子了。


前几天,我就去了一趟卖鞋的商场,里面销售的鞋子,品牌很多,但几乎全是运动鞋,包括特步、鸿星尔克、李宁、安踏等等,还有一些叫不出名字洋品牌。


我自己也已经有十多年没有穿皮鞋的习惯了,穿的也都是运动鞋,这是为什么呢


作者:threerocks
来源:juejin.cn/post/7337867671096885286
收起阅读 »

前端要懂的Docker部分

前言 最近学习部署的时候,发现前端部署可以通过多种方式来进行部署,web服务器,Docker,静态部署:Github page,网站托管平台:Vercel,还有一些自动化部署的东西目前还没有学到... Docker开始 简介 Docker是一个应用打包,分发,...
继续阅读 »

前言


最近学习部署的时候,发现前端部署可以通过多种方式来进行部署,web服务器,Docker,静态部署:Github page,网站托管平台:Vercel,还有一些自动化部署的东西目前还没有学到...


Docker开始


简介


Docker是一个应用打包,分发,部署的工具,它就相当于一个容器,将你想要的一些依赖、第三方库、环境啥的和代码进行一块打包为镜像,并上传到镜像仓库里面,这样别人可以直接从镜像仓库里面直接拉去这个代码来进行运行,可以适应不同的电脑环境,打造了一个完全封闭的环境。



  • 打包:将软件运行所需要的依赖、第三方库、软件打包到一起,变成一个安装包

  • 分发:将你打包好的安装包上传到一个镜像 仓库,其他人可以很方便的获取安装

  • 部署:拿着安装包就可以一个命令运行起来你的应用,自动模拟出一摸一样的运行环境


为什么使用Docker



Docker 的出现主要是为了解决以下问题:“在我的机器上运行正常,但为什么到你的机器上就运行不正常了?”。



平常开发的时候,项目在本地跑的好好的,但是其他人想要在他的电脑上去跑你的应用程序,但是却跑不起来,他得配置数据库,Web 服务器,插件啥的,非常不方便。Docker的出现解决了这些问题。


基本术语


镜像:理解为软件安装包,可以方便的进行传播和安装,它包含了运行应用程序所需的所有元素,包括代码、运行时环境、库、环境变量和配置文件。



  • 镜像可以通过Dockerfile来进行创建

  • Dockerfile就相当于一个脚本,编写一些命令,他会识别这些命令并且执行


容器:软件安装后的状态,每个软件运行的环境都是独立的、隔离的,称之为容器。


仓库:仓库是存放 Docker 镜像的地方。仓库允许你分享你的镜像,你可以将你的镜像推送(push)到仓库,也可以从仓库拉取(pull)其他人分享的镜像。



  • Docker 提供了一个公共的仓库 Docker Hub,你可以在上面上传你的镜像,或者寻找你需要的镜像。


常见命令



  • docker run: 用于从 Docker 镜像启动一个容器。例如,docker run -p 8080:80 -d my-app 将从名为 "my-app" 的 Docker 镜像启动一个新的容器,并将容器的 8080 端口映射到主机的 80 端口。

  • docker build: 用于从 Dockerfile 构建 Docker 镜像。例如,docker build -t my-app . 将使用当前目录中的 Dockerfile 构建一个名为 "my-app" 的 Docker 镜像。

  • docker pull: 用于从 Docker Hub 或其他 Docker 注册服务器下载 Docker 镜像。例如,docker pull nginx 将从 Docker Hub 下载官方的 Nginx 镜像。

  • docker push: 用于将 Docker 镜像推送到 Docker Hub 或其他 Docker 注册服务器。例如,docker push my-app 将名为 "my-app" 的 Docker 镜像推送到你的 Docker Hub 账户。

  • docker ps: 用于列出正在运行的 Docker 容器。添加 -a 选项(docker ps -a)可以列出所有容器,包括已停止的。

  • docker stop: 用于停止正在运行的 Docker 容器。例如,docker stop my-container 将停止名为 "my-container" 的 Docker 容器。

  • docker rm: 用于删除 Docker 容器。例如,docker rm my-container 将删除名为 "my-container" 的 Docker 容器。

  • docker rmi: 用于删除 Docker 镜像。例如,docker rmi my-app 将删除名为 "my-app" 的 Docker 镜像。

  • docker logs: 用于查看 Docker 容器的日志。例如,docker logs my-container 将显示名为 "my-container" 的 Docker 容器的日志。

  • docker exec: 用于在正在运行的 Docker 容器中执行命令。例如,docker exec -it my-container bash 将在名为 "my-container" 的 Docker 容器中启动一个 bash shell。
    docker常见命令



ps:不会使用就:docker load --help



编写Dockerfile


FROM nginx:latest
# 定义作者
MAINTAINER Merikle

#删除目录下的default.conf文件
#RUN rm /etc/nginx/conf.d/default.conf
#设置时区
RUN cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo 'Asia/Shanghai' >/etc/timezone

#将本地nginx.conf配置覆盖nginx配置
COPY nginx.conf /etc/nginx/nginx.conf
# 将dist文件中的内容复制到 /usr/share/nginx/html/ 这个目录下面
COPY dist/ /usr/share/nginx/html/
#声名端口
EXPOSE 8748

RUN echo 'web project build success!!'

FROM:指定基础镜像


RUN:一般指安装的过程


COPY:拷贝本地文件到镜像的指定目录


ENV:环境变量


EXPOSE:指定容器运行时监听到的端口,是给镜像的使用者看的


ENTRYPOINT:镜像中应用的启动命令,容器运行时调用


编写完Dockerfile生成镜像:


docker build -t test:v1 .


运行镜像:


docker run -p 8080:8080 --name test-hello test:v1


意思:跑在8080端口将test:v1命名为text-hello


之后你可以发布到上面说的仓库里面




  • 命令行登录账号: docker login -u username

  • 新建一个tag,名字必须跟你注册账号一样 docker tag test:v1 username/test:v1

  • 推上去 docker push username/test:v1

  • 部署试下 docker run -dp 8080:8080 username/test:v1


实践一下Docker部署前端项目


核心思想:


将前端打包的dist放到nginx里面,然后添加一个nginx.conf文件。


docker的话就是多了一个Dockerfile文件来构建镜像而已。


nginx.conf文件的编写


#nginx.conf文件编写
#user nobody;
worker_processes 1;

#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;

#pid logs/nginx.pid;


events {
worker_connections 1024;
}


http {
include mime.types;
default_type application/octet-stream;

#log_format main '$remote_addr - $remote_user [$time_local] "$request" '
# '$status $body_bytes_sent "$http_referer" '
# '"$http_user_agent" "$http_x_forwarded_for"';

#access_log logs/access.log main;

sendfile on;
#tcp_nopush on;

#keepalive_timeout 0;
keepalive_timeout 65;

#gzip on;

server {
#监听的端口
listen 8748;
#请填写绑定证书的域名或者IP
server_name 121.199.29.3;

gzip on;
gzip_min_length 1k;
gzip_comp_level 9;
gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png;
gzip_vary on;
gzip_disable "MSIE [1-6]\.";

#charset koi8-r;

#access_log logs/host.access.log main;

location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}

#error_page 404 /404.html;

# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}

# proxy the PHP scripts to Apache listening on 127.0.0.1:80
#
#location ~ \.php$ {
# proxy_pass http://127.0.0.1;
#}

# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
#
#location ~ \.php$ {
# root html;
# fastcgi_pass 127.0.0.1:9000;
# fastcgi_index index.php;
# fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
# include fastcgi_params;
#}

# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
#location ~ /\.ht {
# deny all;
#}
}


# another virtual host using mix of IP-, name-, and port-based configuration
#
#server {
# listen 8000;
# listen somename:8080;
# server_name somename alias another.alias;

# location / {
# root html;
# index index.html index.htm;
# }
#}


# HTTPS server
#
#server {
# listen 443 ssl;
# server_name localhost;

# ssl_certificate cert.pem;
# ssl_certificate_key cert.key;

# ssl_session_cache shared:SSL:1m;
# ssl_session_timeout 5m;

# ssl_ciphers HIGH:!aNULL:!MD5;
# ssl_prefer_server_ciphers on;

# location / {
# root html;
# index index.html index.htm;
# }
#}

}


Dockerfile文件的编写


# Dockerfile文件

FROM nginx:latest
# 定义作者
MAINTAINER Merikle

#删除目录下的default.conf文件
#RUN rm /etc/nginx/conf.d/default.conf
#设置时区
RUN cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo 'Asia/Shanghai' >/etc/timezone

#将本地nginx.conf配置覆盖nginx配置
COPY nginx.conf /etc/nginx/nginx.conf
# 将dist文件中的内容复制到 /usr/share/nginx/html/ 这个目录下面
COPY dist/ /usr/share/nginx/html/
#声名端口
EXPOSE 8748

RUN echo 'web project build success!!'


然后将这些压缩成zip(或者本地打包为镜像,然后上传到镜像仓库里面,在服务器里面拉取镜像就可以了),然后传到服务器中,进行解压,构建镜像并且运行镜像来进行前端部署。


部署node项目


Dockerfile的编写


FROM node:11
MAINTAINER Merikle

#
复制代码
ADD . /mongo-server

#
设置容器启动后的默认运行目录
WORKDIR /mongo-server

#
运行命令,安装依赖
# RUN 命令可以有多个,但是可以用 && 连接多个命令来减少层级。
# 例如 RUN npm install && cd /app && mkdir logs
RUN npm install --registry=https://registry.npm.taobao.org

#
CMD 指令只能一个,是容器启动后执行的命令,算是程序的入口。
# 如果还需要运行其他命令可以用 && 连接,也可以写成一个shell脚本去执行。
# 例如 CMD cd /app && ./start.sh
CMD node app.js

然后构建镜像


发布镜像到hub上面


在服务器上面进行拉取镜像并且运行镜像。


部署mongodb


这个还没有部署,之后再说啦。


作者:Charlotten
来源:juejin.cn/post/7332290436345905187
收起阅读 »

千分位分隔?一个vue指令搞定

web
说在前面 🎈对数字进行千分位分隔应该是大部分同学都做过的功能了吧,常规的做法通常是编写一个工具函数来对数据进行转换,那么我们可不可以通过vue指令来实现这一功能呢? 效果展示 实现原理 非输入框 非输入框我们只需要对其展示进行处理,我们可以判断绑定元素...
继续阅读 »

说在前面



🎈对数字进行千分位分隔应该是大部分同学都做过的功能了吧,常规的做法通常是编写一个工具函数来对数据进行转换,那么我们可不可以通过vue指令来实现这一功能呢?



效果展示




实现原理


非输入框


非输入框我们只需要对其展示进行处理,我们可以判断绑定元素的innerHTML是否不为空,不为空的话则直接对其innerHTML内容进行格式化。


export default {
bind: function (el, binding) {
const separator = binding.value || ",";
if (el.innerHTML) {
el.innerHTML = addThousandSeparator(el.innerText, separator);
}
},
};

输入框



对于输入框,我们希望其有以下功能:



1、输入的时候去掉分隔符


这里我们只需要监听元素的聚焦(focus)事件即可,取到元素的值,将其分隔符去掉后重新赋值。


el.addEventListener("focus", (event) => {
const value = event.target.value;
event.target.value = deleteThousandSeparator(value, separator);
});

2、输入完成后添加分隔符


这里我们只需要监听元素的失焦(blur)事件即可,取到元素的值,对其进行添加分隔符处理后重新赋值。


el.addEventListener("blur", (event) => {
const value = event.target.value;
event.target.value = addThousandSeparator(value, separator);
});

千分位分隔函数


function addThousandSeparator(num, separator = ",") {
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, separator);
}


  • num.toString(): 将输入的数字 num 转换为字符串,以便后续处理。

  • .replace(/\B(?=(\d{3})+(?!\d))/g, separator): 这里使用了正则表达式进行替换操作。具体解释如下:



    • \B: 表示非单词边界,用于匹配不在单词边界处的位置。

    • (?=(\d{3})+(?!\d)): 使用正向预查来匹配每三位数字的位置,但不匹配末尾不足三位的数字。

    • (\d{3})+: 匹配连续的三位数字。

    • separator: 作为参数传入的分隔符,默认为 ,。

    • g: 表示全局匹配,即匹配所有满足条件的位置。




这样,通过正则表达式的替换功能,在数字字符串中的每三位数字之间插入指定的千位分隔符,从而实现千位分隔符的添加。


去掉千分位分隔


function deleteThousandSeparator(numberString, separator = ",") {
return numberString.replace(new RegExp(separator, "g"), "");
}

直接将字符串中的分隔符全部替换为空即可。


完整代码


function addThousandSeparator(num, separator = ",") {
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, separator);
}
function deleteThousandSeparator(numberString, separator = ",") {
return numberString.replace(new RegExp(separator, "g"), "");
}
export default {
bind: function (el, binding) {
const separator = binding.value || ",";
if (el.innerHTML) {
el.innerHTML = addThousandSeparator(el.innerText, separator);
}
el.addEventListener("focus", (event) => {
const value = event.target.value;
event.target.value = deleteThousandSeparator(value, separator);
});
el.addEventListener("blur", (event) => {
const value = event.target.value;
event.target.value = addThousandSeparator(value, separator);
});
},
};


组件库


组件文档


目前该组件也已经收录到我的组件库,组件文档地址如下:
jyeontu.xyz/jvuewheel/#…


组件内容


组件库中还有许多好玩有趣的组件,如:



  • 悬浮按钮

  • 评论组件

  • 词云

  • 瀑布流照片容器

  • 视频动态封面

  • 3D轮播图

  • web桌宠

  • 贡献度面板

  • 拖拽上传

  • 自动补全输入框

  • 图片滑块验证


等等……


组件库源码


组件库已开源到gitee,有兴趣的也可以到这里看看:gitee.com/zheng_yongt…


作者:JYeontu
来源:juejin.cn/post/7337125978802683956
收起阅读 »

SQL中为什么不要使用1=1

最近看几个老项目的SQL条件中使用了1=1,想想自己也曾经这样写过,略有感触,特别拿出来说道说道。 编写SQL语句就像炒菜,每一种调料的使用都会影响菜品的最终味道,每一个SQL条件的加入也会影响查询的执行效率。那么 1=1 存在什么样的问题呢?为什么又会使用呢...
继续阅读 »

最近看几个老项目的SQL条件中使用了1=1,想想自己也曾经这样写过,略有感触,特别拿出来说道说道。


编写SQL语句就像炒菜,每一种调料的使用都会影响菜品的最终味道,每一个SQL条件的加入也会影响查询的执行效率。那么 1=1 存在什么样的问题呢?为什么又会使用呢?


为什么会使用 1=1?


在动态构建SQL查询时,开发者可能会不确定最终需要哪些条件。这时候,他们就会使用“1=1”作为一个始终为真的条件,让接下来的所有条件都可以方便地用“AND”连接起来,就像是搭积木的时候先放一个基座,其他的积木块都可以在这个基座上叠加。


就像下边这样:


SELECT * FROM table WHERE 1=1
<if test="username != null">
AND username = #{username}
</if>
<if test="age > 0">
AND age = #{age}
</if>

这样就不用在增加每个条件之前先判断是否需要添加“AND”。


1=1 带来的问题


性能问题


我们先来了解一下数据库查询优化器的工作原理。查询优化器就像是一个聪明的图书管理员,它知道如何最快地找到你需要的书籍。当你告诉它所需书籍的特征时,它会根据这些信息选择最快的检索路径。比如你要查询作者是“谭浩强”的书籍,它就选择先通过作者索引找到书籍索引,再通过书籍索引找到对应的书籍,而不是费力的把所有的书籍遍历一遍。


但是,如果我们告诉它一些无关紧要的信息,比如“我要一本书,它是一本书”,这并不会帮助管理员更快地找到书,反而可能会让他觉得困惑。一个带有“1=1”的查询可能会让数据库去检查每一条记录是否满足这个始终为真的条件,这就像是图书管理员不得不检查每一本书来确认它们都是书一样,显然是一种浪费。


不过这实际上可能也不会产生问题,因为现代数据库的查询优化器已经非常智能,它们通常能够识别出像 1=1 这样的恒真条件,并在执行查询计划时优化掉它们。在许多情况下,即使查询中包含了1=1,数据库的性能也不会受到太大影响,优化器会在实际执行查询时将其忽略。


代码质量


不过,我们仍然需要避免在查询中包含 1=1,有以下几点考虑:



  1. 代码清晰性:即使数据库可以优化掉这样的条件,但对于阅读SQL代码的人来说,1=1可能会造成困惑。代码的可读性和清晰性非常重要,特别是在团队协作的环境中。

  2. 习惯养成:即使在当前的数据库系统中1=1不会带来性能问题,习惯了写不必要的代码可能会在其他情况下引入实际的性能问题。比如,更复杂的无用条件可能不会那么容易被优化掉。

  3. 优化器的限制:虽然现代优化器很强大,但它们并不是万能的。在某些复杂的查询场景中,即使是简单的 1=1 也可能对优化器的决策造成不必要的影响,比如索引的使用。

  4. 跨数据库兼容性:不同的数据库管理系统(DBMS)可能有不同的优化器能力。一个系统可能轻松优化掉1=1,而另一个系统则可能不那么高效。编写不依赖于特定优化器行为的SQL语句是一个好习惯。


编写尽可能高效、清晰和准确的SQL语句,不仅有助于保持代码的质量,也让代码具有更好的可维护性和可扩展性。


替代 1=1 的更佳做法


现在开发者普遍使用ORM框架来操作数据库了,还在完全手写拼SQL的同学可能需要反思下了,这里给两个不同ORM框架下替代1=1的方法。


假设我们有一个用户信息表 user,并希望根据传入的参数动态地过滤用户。


首先是Mybatis


<!-- MyBatis映射文件片段 -->
<select id="selectUsersByConditions" parameterType="map" resultType="com.example.User">
SELECT * FROM user
<where>
<!-- 使用if标签动态添加条件 -->
<if test="username != null and username != ''">
AND username = #{username}
</if>
<if test="age > 0">
AND age = #{age}
</if>
<!-- 更多条件... -->
</where>
</select>

在 MyBatis 中,避免使用 WHERE 1=1 的典型方法是利用动态SQL标签(如 )来构建条件查询。 标签会自动处理首条条件前的 AND 或 OR。当没有满足条件的 或其他条件标签时, 标签内部的所有内容将被忽略,从而不会生成多余的 AND 或 WHERE 子句。


再看看 Entity Framework 的方法:


var query = context.User.AsQueryable();
if (!string.IsNullOrEmpty(username))
{
query = query.Where(b => b.UserName.Contains(username));
}
if (age>0)
{
query = query.Where(b => b.Age = age);
}
var users = query.ToList();

这是一种函数式编程的写法,最终生成SQL时,框架会决定是否在条件前增加AND,而不需要人为的增加 1=1。


总结


“1=1”在SQL语句中可能看起来无害,但实际上它是一种不良的编程习惯,可能会导致性能下降。就像在做饭时不会无缘无故地多加调料一样,我们在编写SQL语句时也应该避免添加无意义的条件。


每一行代码都应该有它存在的理由,不要让你的数据库像一个困惑的图书管理员,浪费时间在不必要的事情上。


作者:萤火架构
来源:juejin.cn/post/7337513754970095667
收起阅读 »

uniapp根据不同的环境配置不同的运行基础路径

web
前言当我们使用uniapp开发同一个项目发布不同的环境二级路径不同时,这时候我们就要根据环境来添加运行的基础路径product:xxx-product-api.xxx.com:9002/productConf…text:xxx-text-api.xxx.com...
继续阅读 »

前言

当我们使用uniapp开发同一个项目发布不同的环境二级路径不同时,这时候我们就要根据环境来添加运行的基础路径

product:xxx-product-api.xxx.com:9002/productConf…

text:xxx-text-api.xxx.com:9002/textConfig/

但是当我们使用HBuilderX开发时你会发现manifest.json手动配置Web配置时只能配置一个像这种情况

image.png

碰到这种情况你会怎么处理?

你是不是会在每次打包发布之前变更该环境对应基础路径?

这样也是一种方法,不过其过程太繁琐,废话不多说,上正文!!!

正文

当我们使用HX创建项目时项目中是没有package.json文件和vue.config.js文件的

  1. 在根目录下创建package.json文件,用于配置多个环境也可用于Hx自定义发行
{
"id": "sin-signature",
"name": "签名组件-兼容H5、小程序、APP",
"version": "1.0.0",
"description": "用于uni-app的签名组件,支持H5、小程序、APP,可导出svg矢量图片。",
"keywords": ["签名,签字,svg,canvas"],

"uni-app": {
"scripts": {
"h5-dev": {
"title": "H5-DEV",
"env": {
"NODE_ENV": "development",
"UNI_PLATFORM": "h5",
"VUE_APP_BASE_API": "http://192.168.3.3:8081"
},
"define": {
"H5": true
}
},
"h5-xx": {
"title": "H5-XX",
"env": {
"NODE_ENV": "production",
"UNI_PLATFORM": "h5",
"VUE_APP_BASE_API": "http://xxx.xx.xx.xxx:8092"
},
"define": {
"H5": true
}
},
"h5-test": {
"title": "H5-TEST",
"env": {
"NODE_ENV": "production",
"UNI_PLATFORM": "h5",
"VUE_APP_BASE_API": "https://beta-text-api.nextopen.cn"
},
"define": {
"H5": true
}
},
"h5-prod": {
"title": "H5-PROD",
"env": {
"NODE_ENV": "production",
"UNI_PLATFORM": "h5",
"VUE_APP_BASE_API": "https://product-api.nextopen.cn"
},
"define": {
"H5": true
}
},

}
}
  1. 在根目录下创建vue.config.js文件,用于处理不同环境配置不同的基础路径
const fs = require('fs')
//此处如果是用HBuilderX创建的项目manifest.json文件在项目跟目录,如果是 cli 创建的则在 src 下,这里要注意
//process.env.UNI_INPUT_DIR为项目所在的绝对路径,经测试,相对路径会找不到文件
const manifestPath = process.env.UNI_INPUT_DIR + '/manifest.json'
let Manifest = fs.readFileSync(manifestPath, { encoding: 'utf-8' })
function replaceManifest(path, value) {
const arr = path.split('.')
const len = arr.length
const lastItem = arr[len - 1]

let i = 0
let ManifestArr = Manifest.split(/\n/)

for (let index = 0; index < ManifestArr.length; index++) {
const item = ManifestArr[index]
if (new RegExp(`"${arr[i]}"`).test(item)) ++i;
if (i === len) {
const hasComma = /,/.test(item)
ManifestArr[index] = item.replace(new RegExp(`"${lastItem}"[\\s\\S]*:[\\s\\S]*`), `"${lastItem}": ${value}${hasComma ? ',' : ''}`)
break;
}
}

Manifest = ManifestArr.join('\n')
}

// 动态修改 h5 路由 base
if (process.UNI_SCRIPT_ENV?.NODE_ENV === 'text'){
//测试的 base
replaceManifest('h5.router.base', '"/textConfig/"')
}else if (process.UNI_SCRIPT_ENV?.NODE_ENV === 'product'){
//生产的 base
replaceManifest('h5.router.base', '"/productConfig/"')
}else {
/其他的 base
replaceManifest('h5.router.base', '""')
}

fs.writeFileSync(manifestPath, Manifest, {
"flag": "w"
})

参考uniapp官方文档:uniapp.dcloud.net.cn/collocation…


作者:快乐是Happy
来源:juejin.cn/post/7337208702201086002
收起阅读 »

程序员的副业发展

前言 之前总有小伙伴问我,现在没有工作,或者想在空闲时间做一些程序员兼职,怎么做,做什么,能赚点外快 因为我之前发别的文章的时候有捎带着说过一嘴我做一些副业,这里就说一下我是怎么做的,都做了什么 希望能对你有些帮助~ 正文 学生单 学生单是我接过最多的,已经写...
继续阅读 »

前言


之前总有小伙伴问我,现在没有工作,或者想在空闲时间做一些程序员兼职,怎么做,做什么,能赚点外快


因为我之前发别的文章的时候有捎带着说过一嘴我做一些副业,这里就说一下我是怎么做的,都做了什么


希望能对你有些帮助~


正文


学生单


学生单是我接过最多的,已经写了100多份毕设,上百份大作业了,这里给大家介绍一下


python这种的数据处理的大作业也很多,但是我个人不太会,所以没结过,我只说我做过的


我大致做过几种单子,最多的是学生的单子,分为大作业单子毕设单子


大作业单一般指一个小作业,比如:



  • 几个web界面(大多是html、css、js)

  • 一个全栈的小demo,大多是jsp+SSM或者vue+springboot,之所以不算是毕设是因为,页面的数目不多,数据库表少,而且后端也很简单


我不知道掘金这里能不能说价格,以防万一我就不说大致价格了,大家想参考价格可以去tb或者咸鱼之类的打听就行


然后最多的就是毕设单子,一般就是一个全栈的项目



  • 最多的是vue+springboot的项目,需求量特别大,这里说一下,之前基本都是vue2的项目,现在很多学校要求vue3了,但是大部分商家vue3的模板很少,所以tb上接vue3的项目要么少,要么很贵,所以我觉得能接vue3和springboot项目的可以打一定的价格战,vue2的市面上价格差不多,模板差不多,不好竞争的

  • 少数vue+node的全栈项目,一般是express或者koa,价格和springboot差不多,但是需求量特别少

  • uni+vue+springboot的项目,其实和vue+springboot项目差不多,因为单纯的vue+springboot项目太多了,所以现在很多人要求做个uni手机端,需求量适中

  • .net项目,信管专业的学生用.net的很多,需求量不少,有会的可以考虑一下


这是我接过的比较多的项目,数据库我没有单说,基本上都是MySQL,然后会要求几张表,以及主从表有几对,这就看客户具体要求了



需要注意的点:大部分你得给客户配环境,跑程序,还是就是毕设一般是要求论文的,有论文的会比单纯程序赚的多,但是一定要注意对方是否要求查重,如果要求查重,一般是不建议接的,一般都是要求维普和知网查重,会要了你的老命。还有需要注意的是,学生单子一般是需要答辩的,你可以选择是否包答辩,当然可以调整价格,但是你一旦包答辩,你的微信在答辩期间就不会停了。你永远不知道他们会有怎样的问题



商业单


商业单有大有小,小的跟毕设差不多,大的需要签合同


我接的单子大致就一种,小程序+后台管理+后端,也就是一个大型的全栈项目,要比学生单复杂,而且你还要打包、部署、上线,售后,有一个周期性,时间也比较长


72761aa2847097aa719f2c9728dc560.jpg


image.png


ff5d9aaae6207ab8cbbe847c73cbd36.jpg


9e157d5ddab294d3214fa1d8ece07dc.jpg


为了防止大家不信,稍微放几个聊天记录,是这两个月来找的,也没有给自己打广告,大家都是开发者,开发个小程序有什么打广告,可吹的(真的是被杠怕了)


技术栈有两种情况:自己定客户定


UI也有两种情况:有设计图的无设计图的(也就是自己设计)


基本上也就是两种客户:懂技术的客户,不懂技术的客户


指定技术栈的我就不说了,对于不指定技术栈的我大致分为两种



  • 小程序端:uni/小程序原生、后台:vue、后端:云开发

  • 小程序端:uni/小程序原生、后台:vue、后端:springboot


这取决于预算,预算高的就用springboot、预算不高的就云开发一把嗦,需要说的是并不是说云开发差,其实现在云开发已经满足绝大部分的需求,很完善了,而springboot则是应用广泛,客户后期找别人接手更方便


对于没有UI设计图的,我会选择去各种设计网站去找一些灵感


当项目达到一定金额,会签署合同,预付定金,这是对双方的一种保障


其实在整个项目中比较费劲的是沟通,不是单独说与客户的沟通,更多的是三方沟通,作为上线的程序,需要一些资料手续,这样就需要三方沟通,同时还有一定的周期,可能会被催


讲解单


当然,有的时候人家是有程序的,可能是别人代写的,可能是从开源扒下来的,这个时候客户有程序,但是看不懂,他们可能需要答辩,所以会花钱找人给他们梳理一下,讲一讲, 这种情况比较简单,因为不需要你去写代码,但是需要你能看懂别人的代码


这种情况不在少数,尤其是在小红书这种单子特别多,来钱快,我一般是按照小时收费


cb519bce3fedc451116b659f6cb7388.jpg


e4531c4d8d6527208a03e1dcc6ede32.jpg


aef2baeabe8859caac59fd7ae0b456c.jpg


知识付费这东西很有意思,有时候你回答别人的一些问题,对方也会象征性地给你个几十的红包


接单渠道


我觉得相对于什么单,大家更在意的是怎么接单,很多人都接不到单,这才是最难受的


其实对此我个人并没有太好的建议的方法,我认为最重要的,还是你的交际能力,你在现实中不善于交际,网络上也不善于交际,那就很难了


因为我之前是在学校,在校期间干过一些兼职,所以认识的同学比较多,同时自身能力还可以,所以会有很多人来找,然后做完之后,熟人之间会慢慢介绍,人就越来越多,所以我不太担心能否接单这件事,反而是单太多,自己甚至成立一个小型工作室去接单


如果你是学生的话,一定要在学校积累客户,这样会越来越多,哪怕是现在我还看到学校的各种群天天有毕业很多年以及社会人士来打广告呢,你为什么就不可以呢


当然但是很多人现在已经不是学生了,也不知道怎么接触学生,那么我给大家推荐另外的道路



  • 闲鱼接单

  • 小红书接单


大部分学生找的写手都会比较贵,这种情况下,很多学生都会选择去上面的两个平台去货比三家,那么你的机会就来了


有人说不行啊,这种平台发接单帖子就被删了,那么你就想,为什么那么多人没被删,我也没被删,为什么你被删除了


其次是我最不推荐的一种接单方式:tb写手


为什么不推荐呢,其实就是tb去接单,然后会在tb写手群外包给写手,也就是tb在赚你的差价


这种感觉很难受,而且赚的不多,但是如果你找不到别的渠道,也可以尝试一下


最后


我只是分享一下自己接单的方式,但是说实在的,接一个毕设单或者是商业单其实挺累的,不是说技术层面的,更多的是心累,大家自行体会吧,而且现在商场内卷严重,甚至有人200、300就一个小程序。。。


所以大家要想,走什么渠道,拿什么竞争


另外,像什么猪八戒这种的外包项目的网站,我只是见过,但是没实际用过,接过,所以不好评价


希望大家赚钱顺利,私单是一种赚钱的方式,但是是不稳定的,一定还是要以自己本身的工作为主,自行判断~


作者:Shaka
来源:juejin.cn/post/7297124052174848036
收起阅读 »

新年 10 个面试题,我曾 10 次拷问我的灵魂

web
大家好,这里是大家的林语冰。坚持阅读,自律打卡,每天一次,进步一点。 免责声明 本文属于是语冰的直男翻译了属于是,略有删改,仅供粉丝参考。英文原味版请传送 10 Interview Questions Every JavaScript Developer S...
继续阅读 »

大家好,这里是大家的林语冰。坚持阅读,自律打卡,每天一次,进步一点



免责声明


本文属于是语冰的直男翻译了属于是,略有删改,仅供粉丝参考。英文原味版请传送 10 Interview Questions Every JavaScript Developer Should Know in 2024



本期共享的是 —— 新年里前端面试需要掌握的十大面试题,知识面虽小,但思路清晰。


JS 的世界日新月异,多年来面试趋势也与时俱进。本文科普了新年每个 JS 开发者必知必会十大基本问题,涵盖了从闭包到 TDD(测试驱动开发)的一系列主题,为大家提供应对现代 JS 挑战的知识和信心。


1. 闭包到底是什么鬼物?


闭包让我们有权从内部函数访问外部函数的作用域。当函数嵌套时,内部函数可以访问外部函数作用域中声明的变量,即使外部函数返回后也是如此:


const createCat = cat => {
return {
getCat: () => cat,
setCat: newCat => {
cat = newCat
}
}
}

const myCat = createCat('薛定谔')
console.log(myCat.getCat()) // 薛定谔

myCat.setCat('龙猫')
console.log(myCat.getCat()) // 龙猫

闭包变量是对外部作用域变量的实时引用,而不是拷贝。这意味着,如果变更外部作用域变量,那么变更会反映在闭包变量中,反之亦然,这意味着,在同一外部函数中声明的其他函数将可以访问这些变更。


闭包的常见用例包括但不限于:



  • 数据隐藏

  • 柯里化和偏函数(经常用于改进函数组合,比如形参化 Express 中间件或 React 高阶组件)

  • 与事件处理程序和回调共享数据


数据隐藏


封装是面向对象编程的一个重要特征。封装允许我们向外界隐藏类的实现细节。JS 中的闭包允许我们声明对象的私有变量:


// 数据隐藏
const createGirlFans = () => {
let fans = 0
return {
increment: () => ++fans,
decrement: () => --fans,
getFans: () => fans
}
}

柯里化函数和偏函数:


// 一个柯里化函数一次接受多个参数。
const add = a => b => a + b

// 偏函数是已经应用了某些参数的函数,
// 但没有完全应用所有参数。
const increment = add(1) // 偏函数

increment(2) // 3

2. 纯函数是什么鬼物?


纯函数在函数式编程中兹事体大。纯函数是可预测的,这使得它们比非纯函数更易理解、调试和测试。纯函数遵循两个规则:



  1. 确定性 —— 给定相同的输入,纯函数会始终返回相同的输出。

  2. 无副作用 —— 副作用是在被调用函数外部可观察到的、不是其返回值的任何 App 状态更改。


非确定性函数依赖于以下各项的函数,包括但不限于:



  • 随机数生成器

  • 可以改变状态的全局变量

  • 可以改变状态的参数

  • 当前系统时间


副作用包括但不限于:



  • 修改任何外部变量或对象属性(比如全局变量或父函数作用域链中的变量)

  • 打印到控制台

  • 写入屏幕、文件或网络

  • 报错。相反,该函数应该返回表明错误的结果

  • 触发任何外部进程


在 Redux 中,所有 reducer 都必须是纯函数。如果不是,App 的状态不可预测,且时间旅行调试等功能无法奏效。reducer 函数中的杂质还可能导致难以追踪的错误,包括过时的 React 组件状态。


3. 函数组合是什么鬼物?


函数组合是组合两个或多个函数,产生新函数或执行某些计算的过程:(f ∘ g)(x) = f(g(x))


const compose = (f, g) => x => f(g(x))

const g = num => num + 1
const f = num => num * 2

const h = compose(f, g)

h(20) // 42

React 开发者可通过函数组合来清理大型组件树。我们可以将它们组合,创建一个新的高阶组件,而不是嵌套组件,该组件可以通过附加功能强化传递给它的任何组件。


4. 函数式编程是什么鬼物?


函数式编程是一种使用纯函数作为主要组合单元的编程范式。组合在软件开发中兹事体大,几乎所有编程范式都是根据它们使用的组合单元来命名的:



  • 面向对象编程使用对象作为组合单元

  • 过程式编程使用过程作为组合单元

  • 函数式编程使用函数作为组合单元


函数式编程是一种声明式编程范式,这意味着,程序是根据它们做什么,而不是如何做来编写的。这使得函数式程序比命令式程序更容理解、调试和测试。它们往往更加简洁,这降低了代码复杂性,并使其更易维护。


函数式编程的其他关键方面包括但不限于:



  • 不变性 —— 不可变数据结构比可变数据结构更易推理

  • 高阶函数 —— 将其他函数作为参数或返回函数作为结果的函数

  • 避免共享可变状态 —— 共享可变状态使程序难以理解、调试和测试。这也使得推断程序的正确性更加头大


5. Promise 是什么鬼物?


JS 中的 Promise 是一个表示异步操作最终完成或失败的对象,它充当最初未知值的占位符,通常是因为该值的计算尚未完成。


Promise 的主要特征包括但不限于:



  • 有状态Promise 处于以下三种状态之一:

    • 待定:初始状态,既未成功也未失败

    • 已完成:操作成功完成

    • 拒绝:操作失败



  • 不可变:一旦 Promise 被完成或拒绝,其状态就无法改变。它变得不可变,永久保留其结果。这使得 Promise 在异步流控制中变得可靠。

  • 链接Promise 可以链接起来,这意味着,一个 Promise 的输出可以用作另一个 Promise 的输入。这通过使用 .then() 表示成功或使用 .catch() 处理失败来链接,从而允许优雅且可读的顺序异步操作。链接是函数组合的异步等价物。


const promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('成功!')
// 我们也可以在失败时 reject 新错误。
}, 1000)
})

promise
.then(value => {
console.log(value) // 成功
})
.catch(error => {
console.log(error)
})

6. TS 是什么鬼物?


TS 是 JS 的超集,由微软开发和维护。近年来,TS 的人气与日俱增,如果您是一名 JS 工程师,您最终很可能需要使用 TS。它为 JS 添加了静态类型,JS 是一种动态类型语言。静态类型可以辅助开发者在开发过程的早期发现错误,提高代码质量和可维护性。


TS 的主要特点包括但不限于:



  • 静态类型:定义变量和函数参数的类型,确保整个代码一致性。

  • 给力的 IDE 支持:IDE(集成开发环境)可以提供更好的自动补全、导航和重构,使开发过程更加高效。

  • 编译:TS 代码被转译为 JS,使其与任何浏览器或 JS 环境兼容。在此过程中,类型错误会被捕获,使代码更鲁棒。

  • 接口:接口允许我们指定对象和函数必须满足的抽象契约。

  • 与 JS 的兼容性:Ts 与现有 JS 代码高度兼容。JS 代码可以逐步迁移到 JS,使现有项目能够顺利过渡。


interface User {
id: number
name: string
}

type GetUser = (userId: number) => User

const getUser: GetUser = userId => {
// 从数据库或 API 请求用户数据
return {
id: userId,
name: '人猫神话'
}
}

防范 bug 的最佳方案是代码审查、TDD 和 lint 工具(比如 ESLint)。TS 并不能替代这些做法,因为类型正确性并无法保证程序的正确性。即使应用了所有其他质量措施后,TS 偶尔也会发现错误。但它的主要好处是通过 IDE 支持,提供改进的开发体验。


7. Web Components 是什么鬼物?


WC(Web 组件)是一组 Web 平台 API,允许我们创建新的自定义、可重用、封装的 HTML 标签,在网页和 Web App 中使用。WC 是使用 HTML、CSS 和 JS 等开放 Web 技术构建的。它们是浏览器的一部分,不需要外部库或框架。


WC 对于拥有一大坨可能使用不同框架的工程师的大型团队特别有用。WC 允许我们创建可在任何框架或根本没有框架中使用的可重用组件。举个栗子,Adobe(PS 那个公司)的某个设计系统是使用 WC 构建的,并与 React 等流行框架顺利集成。


WC 由来已久,但最近人气爆棚,尤其是在大型组织中。它们被所有主要浏览器支持,并且是 W3C 标准。


8. React Hook 是什么鬼物?


Hook 是让我们无需编写类即可使用状态和其他 React 功能的函数。Hook 允许我们通过调用函数而不是编写类方法,来使用状态、上下文、引用和组件生命周期事件。函数的额外灵活性使我们更好地组织代码,将相关功能分组到单个钩子调用中,并通过在单独的函数调用中实现不相关功能,分离不相关的功能。Hook 提供了一种给力且富有表现力的方式来在组件内编写逻辑。


重要的 React Hook 包括但不限于:



  • useState —— 允许我们向函数式组件添加状态。状态变量在重新渲染之间保留。

  • useEffect —— 允许我们在函数式组件中执行副作用。它将 componentDidMount/componentDidUpdate/componentWillUnmount 的功能组合到单个函数调用中,减少了代码,并创建了比类组件更好的代码组织。

  • useContext —— 允许我们使用函数式组件中的上下文。

  • useRef —— 允许我们创建在组件的生命周期内持续存在的可变引用。

  • 自定义 Hook —— 封装可重用逻辑。这使得在不同组件之间共享逻辑变得容易。


Hook 的规则:Hook 必须在 React 函数的顶层使用(不能在循环、条件或嵌套函数内),且能且只能在 React 函数式组件或自定义 Hook 中使用。


Hook 解决了类组件的若干常见痛点,比如需要在构造函数中绑定方法,以及需要将功能拆分为多个生命周期方法。它们还使得在组件之间共享逻辑以及重用有状态逻辑,而无需更改组件层次结构更容易。


9. 如何在 React 中创建点击计数器?


我们可以使用 useState 钩子在 React 中创建点击计数器,如下所示:


import React, { useState } from 'react'

const ClickCounter = () => {
const [count, setCount] = useState(0) // 初始化为 0

return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count => count + 1)}>Click me</button>
</div>

)
}

export default ClickCounter

粉丝请注意,当我们从现有状态派生新值时,将函数传递给 setCount 是最佳实践,确保我们始终使用最新状态。


10. TDD 是什么鬼物?


TDD(测试驱动开发)是一种软件开发方法,其中测试是在实际代码之前编写的。它围绕一个简短的重复开发周期,旨在确保代码满足指定的要求且没有错误。TDD 在提高代码质量、减少错误和提高开发者生产力方面,可以发挥至关重要的作用。


开发团队生产力最重要的衡量标准之一是部署频率。持续交付的主要障碍之一是对变化的恐惧。TDD 通过确保代码始终处于可部署状态,辅助减少这种恐惧。这使得部署新功能和错误修复更容易,提高了部署频率。


测试先行多了一大坨福利,包括但不限于:



  • 更好的代码覆盖率:测试先行更有可能覆盖所有极端情况。

  • 改进的 API 设计:测试迫使我们在编写代码之前考虑 API 设计,这有助于避免将实现细节泄漏到 API 中。

  • 更少的 bug:测试先行可以辅助在开发过程中尽早发现错误,这样更容易修复。

  • 更好的代码质量:测试先行迫使我们编写模块化、松耦合的代码,这样更容易维护和重用。


TDD 的关键步骤包括但不限于:



  1. 编写测试:此测试最初会失败,因为相应的功能尚不存在。

  2. 编写实现:足以通过测试。

  3. 自信重构:一旦测试通过,就可以自信重构代码。重构是在不改变其外部行为的情况下,重构现有代码的过程。其目的是清理代码、提高可读性并降低复杂性。测试到位后,如果我们犯错了,我们会立即因测试失败而收到警报。


重复:针对每个功能需求重复该循环,逐步构建软件,同时确保所有测试继续通过。


学习曲线:TDD 是一项需要相当长的时间才能培养的技能和纪律。经过大半年的 TDD 体验后,我们可能仍觉得 TDD 难如脱单,且妨碍了生产力。虽然但是,使用 TDD 两年后,我们可能会发现它已经成为第二天性,并且比以前更有效率。


耗时:为每个小功能编写测试一开始可能会感觉很耗时,但长远来看,这通常会带来回报,减少错误并简化维护。我常常告诫大家,“如果你认为自己没有时间进行 TDD,那么你真的没有时间跳过 TDD。”


本期话题是 —— 你遭遇灵魂拷问的回头率最高的面试题是哪一道?


欢迎在本文下方群聊自由言论,文明共享。谢谢大家的点赞,掰掰~


《前端 9 点半》每日更新,坚持阅读,自律打卡,每天一次,进步一点


26-cat.gif


作者:人猫神话
来源:juejin.cn/post/7334653735359348777
收起阅读 »

程序员必看的几大定律,你中招了吗?

一 晕轮效应 我们通常会从局部信息形成一个完整的印象,根据最少量的情况对别人或其他事物做出全面的结论。 举个简单的例子,当你看到一个陌生美女的时候,你通常会认为对方长得这么好看,笑起来这么甜,肯定哪哪都好! 当你看到一位发量稀疏的开发老哥,你通常会觉得这位...
继续阅读 »

一 晕轮效应



我们通常会从局部信息形成一个完整的印象,根据最少量的情况对别人或其他事物做出全面的结论。



举个简单的例子,当你看到一个陌生美女的时候,你通常会认为对方长得这么好看,笑起来这么甜,肯定哪哪都好!


当你看到一位发量稀疏的开发老哥,你通常会觉得这位老哥技术能力肯定非常强,做人肯定也很靠谱!



在晕轮效应影响下,一个人或事物的优点或缺点一旦变为光圈被扩大,其缺点或优点也就隐退到光圈的背后,被别人视而不见了。



对于程序员来说,有两点可以考虑:



  1. 打造自己的光晕:让自己成为专家或者像个专家,可以提升自己的话语权。


    比如在掘金写文章,当你拥有几千粉丝,几十万阅读量的时候,即使你什么都不说,别人看到这个账号都会觉得你是个厉害人物,与你交流的时候都会注意几分。


  2. 突破别人的光晕:在我使用npm上各种组件或者工具的时候,常常会感慨,好厉害!这么多star的项目,肯定没有Bug吧,如果有,那一定是我用的方式不对!打开源码看的时候更是惊呼,好厉害,完全看不懂,也不可能看的懂吧?


    但其实,褪去这些光环,你会发现,即使是成熟的项目,也会有bug,高深的代码,也都只是一个个基础的语句组合起来的,当你理解了作者的思想,也就能理解代码。



二 眼不见为净定律



看不见的,就是干净的。



看到这个定律,我的第一反应就是,屎山代码为什么会存在?


还不是因为管理人员看不到这坨屎山,他看到的是一个功能正常运行的系统,所以人家并不觉得这是屎山,而是美丽的风景线!


只有我们这些天天在这座屎山上添砖加瓦的程序员才能会感受到这种绝望!


所以面对屎山代码,不要抱怨,最好的方法就是找个机会把这座屎山丢给其他人,毕竟眼不见为净嘛!


当它不在你手上的时候,你会发现其实它也挺好的,毕竟眼不见为净嘛!


三 虚假同感偏差


你们是否会遇到这种情况:明明一件很重要的事情,催了某个人很久了,他却迟迟未做!


这里就涉及到虚假同感偏差,因为这件事对你来说很重要,所以通常会自我推断,觉得别人也会认为这件事情很重要,然而事实上,对你很重要的事,对他人来说可能回过头就给忘记了!


所以啊,要让别人重视一件你觉得很重要的事情,就是也让他感觉到重要,这样别人就不敢忘记了,比如可以补充一句:某某领导正在关注这件事,麻烦尽快,谢谢!


另外就是当我们非常确信自己观点或意见的时候,也很容易产生虚假同感偏差,这时候如果有人提出不同的观点,我们会下意识的反驳,并且觉得问题来自于他人。


比如我们自信满满地写完一段代码并且自测之后,提交给测试人员进行测试,当测试人员跟你反馈存在某BUG,我相信第一时间反应大多都是:我不信!!!


然后就有以下对话:



你:可能前端有缓存,你刷新一下再试试?


测试:行,我试一下。


过了十分钟......


测试:还是一样的问题啊,你看一下。


你:是不是测试数据有问题啊,我自己都测试过了,不应该有问题!


测试:行吧,我再看看。


过了十分钟......


测试:数据都排查过了,是正常的,你检查一下吧!


你:(还想再挣扎一下)你怎么操作的?


测试:就点击一下这个按钮,我还能玩出什么花吗?


排查了一会,哦~居然是空值的情况没有判断,我还能再白痴一点吗!


你:问题已经修复了,是某某复杂的场景我没考虑清楚,你再测试一下!



四 自我宽恕定律



人性有个根深蒂固的特点,就是容易发现别人的缺点和错误,却不容易看到自己的不足。


正所谓,见人之过易,见己之过难。




  • 当看到别人的代码存在一个空指针异常,心里想:这都能忘记判断,其他代码会不会也有问题!

  • 当发现自己的代码存在一个空指针异常,心里想:只是不小心忘记判断了嘛,加一下就好了!

  • 当接手别人项目的时候:卧槽,这代码写的啥啊,注释没几句,变量命名这么奇葩,逻辑这么混乱,我要咋接啊!!

  • 当项目给别人接手时候:我这代码虽然注释不多,但是很规范的,你看这变量命名不就能知道是什么含义了嘛,逻辑也非常顺,这个方法几百行你按顺序看下来就行了,我都给你写在一起,不需要跳来跳去地看,多方便!


五 补偿作用:



弱点也是一种力量源!



大家应该都听说过这个现象:瞎子的眼睛虽然看不见了,听力通常会变得非常灵敏!


这种生理上的现象吸引了很多有兴趣的心理学家,所谓补偿,就是发挥一个人的最大优势,激发其自信心,抵消其弱点。


看到补偿机制,我第一想到的就是在掘金看到的各种专科大佬。


虽然学历起点比其他人低一些,但有时候正是因为学历劣势,更加激发他们深耕技术的决心,反而达到其他更高学历人员都无法达到的高度。


这又让我想起了一句话:打不倒我的,会让我更强大!!


六 皮尔斯定理



意识到无知,才是知道的开始。



还有一句话,我觉得很适合接在这句话后面:知道的越多,才发现自己不懂的越多!


于是就形成了一个闭环: 意识到无知->开始知道->知道的越多->意识到无知


这句话我相信大部分人都听过很多遍了,不知道你们是从什么时候开始意识到自己的无知呢?


曾经,我还是小白的时候,在福州某公司上班,每天做的事情就是SpringBoot接口的开发,或者修改某些业务逻辑,我以为这差不多就是开发的全部了。


那时候对接的前端是使用Vue写的,我甚至不知道什么是Vue,只知道是某个挺流行的前端技术。


每次部署,我看前端文件里就只有一个index.html文件,我真的非常奇怪,为什么这么大的项目,只有一个html文件?


那时候我对前端认知还停留在html+js+css+jquery的时代,所以完全想不通。


本来还觉得自己前端也是有点基础的,直到接触Vue,我才惊呼,卧槽,前端怎么变成这样子了?什么nodejs,什么npm完全没听说过。


用过一段时间之后,我更是惊呼,卧槽,前端还能这样子?明明我HTML+CSS+JS只懂一点,都能做出这么好看的页面了。


有了各种开源前端组件,即使对原生HTML标签和CSS不太懂,也能算是个还不错的前端开发了。


还有这ES6语法用起来也太爽了吧,比JAVA可自由太多了。


所以很感慨,当我没进入前端圈子的时候,还以为自己懂一些,进入之后,才发现自己真的是一窍不通,啥都要学。


更感慨的是,当我第一次接触掘金,我惊呼,卧槽,这个社区分享的东西都好干啊,好多听都没听过的标题,原来我有这么多东西都不懂!原来前端是个这么卷的领域!


结语


感谢阅读,希望本篇文章对你有所帮助!


作者:林劭敏
来源:juejin.cn/post/7295623585363771443
收起阅读 »

对代码封装的一点思考

之前看过一篇文章讲代码封装的过程,下面从简述文章的内容开始梳理下自己对代码封装的一点思考. --- 以下是之前文章内容简述 --- 在业务迭代的初期,代码中有一段业务逻辑A,由于A逻辑在项目中使用比较广泛,于是对A逻辑进行了代码逻辑的封装,包括参数的预设等 ...
继续阅读 »

之前看过一篇文章讲代码封装的过程,下面从简述文章的内容开始梳理下自己对代码封装的一点思考.


--- 以下是之前文章内容简述 ---


在业务迭代的初期,代码中有一段业务逻辑A,由于A逻辑在项目中使用比较广泛,于是对A逻辑进行了代码逻辑的封装,包括参数的预设等

初始化封装

随着业务的发展,又出现了B模块,B模块中几乎可以直接使用A逻辑,但是需要对A逻辑进行一些小小的改动,比如入参的调整、逻辑分支的增加

再次封装

这种通过修改较小范围代码就能实现功能的方式在实际开发中是很常规的操作,它并没有不好,只是当这种修改变的次数多的时候,最开始封装的逻辑A就变成了‘四不像’,它似乎可以服务于A模块,但是确有些冗余,出现了‘我好像看不懂这段代码’,‘这段代码还是别改了’,‘你改吧 我是改不了’的场景。在多次封装之后,逻辑A容易出现以下的问题



  • 入参不可控

  • 逻辑分支复杂,不清晰,修改范围不确定,容易对其他模块产生影响

  • 复用度逐渐下降,解决方法



    1. 将就不再继续新增逻辑,变成'老大难'问题,总有爆发的一天

    2. 重写相关逻辑,功能回测范围大,影响范围不可控,容易出现漏测等




--- 以下是正文 ---


提前封装或者过度封装对实际业务的编写都有很大影响,最实际的感受就是这段代码不好写,这个逻辑看不懂。封装的代码逻辑是结果,封装的出发点是因,从出发点去看而不是面向结果的封装似乎更能编写出可维护性的代码。以下是自己对封装的一点思考。


降低复杂度


复杂度

通过对业务中分散的相似逻辑提取,就能一定程度上降低项目的复杂度和重复度的问题。在修改相应的业务逻辑时候就相对集中和可控。新增逻辑的修改也能做到应用范围的同步。


解耦逻辑


在设计模式中需要设计的模块尽量遵循单一职责,但是在实际编写代码的过程中很容易出现模块功能的耦合,比如常见的:



  • 视图层逻辑与数据层逻辑耦合

  • 非相关逻辑的入侵 比如视图的逻辑需要感知请求状态的改变做相应的逻辑处理


通过将相关的逻辑都聚合到模块封装的内部,就能较低其他模块对不相关逻辑的感知,也就做到了解耦。在下面的例子中,通过将A逻辑的关联逻辑都封装进A模块中,降低了功能模块对封装逻辑的感知。内部封装

相关例子:



  • ahooks中的useRequest中,就通过对请求状态和数据的操作就行封装,将数据相关操作做到了内聚,减轻了视图层的负担

  • Antd pro components的table


分层


分层封装

在例子中,通过将逻辑A中的相关逻辑提取到上层,分层的方式实现了模块功能的内聚与解耦。在前端的MVC中就通过将相关操作封装成Controller,来完成逻辑的解耦


复用(一致性,可控性)


从降低复杂度、逻辑结构、分层设计最终的目标是达到封装代码的复用。通过复用能控制代码的一致性,实现修改代码行为的可控。



-- 写在最后 --


代码的优化或者设计是应该在理解业务的基础上,从相对大的视角出发来看的。在实际做的时候容易出现缺少全局视角的设计缺失和不完善,也容易出现过度设计,需要仔细去把握这个度量。


作者:前端小板凳
来源:juejin.cn/post/7337354931479085096
收起阅读 »

集帅(美)们,别再写 :key = "index" 啦!

web
浅聊一下 灵魂拷问:你有没有在v-for里使用过:key = "index",如果有,我希望你马上改正过来并且给我点个赞,如果没有,来都来了,顺手给我点个赞... 开始 在向掘友们解释为什么不能使用 :key = "index" 之前,我想我还得向你们铺垫一点...
继续阅读 »

浅聊一下


灵魂拷问:你有没有在v-for里使用过:key = "index",如果有,我希望你马上改正过来并且给我点个赞,如果没有,来都来了,顺手给我点个赞...


开始


在向掘友们解释为什么不能使用 :key = "index" 之前,我想我还得向你们铺垫一点东西


虚拟DOM


什么是虚拟DOM呢?虚拟DOM是一个对象,没想到吧...我们来看看Vue是如何将template模板里面的东西交给浏览器来渲染的


image.png


首先通过 compiler 将 template模板变成一个虚拟DOM,再将虚拟DOM转换成HTML,最后再交给浏览器V8引擎渲染,那么虚拟DOM是什么样的呢?


html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Documenttitle>
<script src="https://unpkg.com/vue@3/dist/vue.global.js">script>
head>

<body>
<div id="app">
<ul id="item">
<li v-for="item in list" class="item">{{item}}li>
ul>
div>
<script>
const { createApp, ref } = Vue
createApp({
setup() {
const list = ref(['vue','js','html'])
return {
list
}
}
}).
mount('#app')
script>
body>

html>

在这里,template模板实际上是


 <ul>
<li v-for="item in list">{{item}}li>
ul>

通过v-for循环,渲染出来了3个li


<ul>
<li>vue<li>
<li>js<li>
<li>html<li>
ul>

我们的compiler会将这个模板转化成虚拟DOM


let oldDom = {
tagName = 'ul',
props:{
//存放id 和 class 等
id:'item'
},
children[
{
tagName = 'li',
props:{
class:'item'
},
children:['vue']
},
{
tagName = 'li',
props:{
class:'item'
},
children:['js']
},
{
tagName = 'li',
props:{
class:'item'
},
children:['html']
},
]
}

diff算法


给前面的例子来点刺激的,加上一个按钮和反转函数,点击按钮,list反转


html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Documenttitle>
<script src="https://unpkg.com/vue@3/dist/vue.global.js">script>
head>

<body>
<div id="app">
<ul>
<li v-for="item in list">{{item}}li>
ul>
<button @click="change">changebutton>
div>
<script>
const { createApp, ref } = Vue
createApp({
setup() {
const list = ref(['唱','跳','rap','篮球'])
const change = ()=>{
list.
value.reverse()
}
const add = ()=>{
list.
value.unshift('6')
}
return {
list,
change,
}
}
}).
mount('#app')
script>
body>

html>

点击change按钮,此时我们的DOM更改vue又是如何来更新DOM的呢?


image.png


众所周知,回流和重绘会消耗极大的性能,而当DOM发生变更的时候会触发回流重绘(可以去看我的文章(从输入4399.com到页面渲染之间的回流和重绘),那么vue3就有一个diff算法,用来优化性能


image.png


当DOM更改,compiler会生成一个新的虚拟DOM,然后通过diff算法来生成一个补丁包,用来记录旧DOM和新DOM的差异,然后再拿到html里面进行修改,最后再交给浏览器V8进行渲染


简单介绍一下diff算法的比较规则



  1. 同层比较,是不是相同的结点,不相同直接废弃老DOM

  2. 是相同结点,比较结点上的属性,产生一个补丁包

  3. 继续比较下一层的子节点,采用双端对列的方式,尽量复用,产生一个补丁包

  4. 同上


image.png


别再写 :key = "index"


要说别写 :key = "index" ,我们得先明白key是用来干什么的...如果没有key,那么在diff算法对新旧虚拟DOM进行比较的时候就没法比较了,你看这里有两个一样的vue,当反转顺序以后diff算法不知道哪个vue该对应哪个vue了


image.png


如果我们用index来充当key的话来看,当我们在头部再插入一个结点的时候,后面的index其实是改变了的,导致diff算法在比较的时候认为他们与原虚拟DM都不相同,那么diff算法就等于没有用...


image.png


可以用随机数吗?


  • for="item in list" :key="Math.random()">

  • 想出这种办法的,也是一个狠人...当然是不行的,因为在template模板更新时,会产生一个新的虚拟DOM,而这个虚拟DOM里面的key也是随机值,和原虚拟DOM里的key99.99999%是不一样的...


    结尾


    希望你以后再也不会写 :key = "index" 了


    作者:滚去睡觉
    来源:juejin.cn/post/7337513012394115111
    收起阅读 »

    分类树,我从2s优化到0.1s

    前言 分类树查询功能,在各个业务系统中可以说随处可见,特别是在电商系统中。 但就是这样一个简单的分类树查询功能,我们却优化了5次。 到底是怎么回事呢? 背景 我们的网站使用了SpringBoot推荐的模板引擎:Thymeleaf,进行动态渲染。 它是一个XM...
    继续阅读 »

    前言


    分类树查询功能,在各个业务系统中可以说随处可见,特别是在电商系统中。



    但就是这样一个简单的分类树查询功能,我们却优化了5次。


    到底是怎么回事呢?


    背景


    我们的网站使用了SpringBoot推荐的模板引擎:Thymeleaf,进行动态渲染。


    它是一个XML/XHTML/HTML5模板引擎,可用于Web与非Web环境中的应用开发。


    它提供了一个用于整合SpringMVC的可选模块,在应用开发中,我们可以使用Thymeleaf来完全代替JSP或其他模板引擎,如Velocity\FreeMarker等。


    前端开发写好Thymeleaf的模板文件,调用后端接口获取数据,进行动态绑定,就能把想要的内容展示给用户。


    由于当时这个是从0-1的新项目,为了开快速开发功能,我们第一版接口,直接从数据库中查询分类数据,组装成分类树,然后返回给前端。


    通过这种方式,简化了数据流程,快速把整个页面功能调通了。


    第1次优化


    我们将该接口部署到dev环境,刚开始没啥问题。


    随着开发人员添加的分类越来越多,很快就暴露出性能瓶颈。


    我们不得不做优化了。


    我们第一个想到的是:加Redis缓存


    流程图如下:

    于是暂时这样优化了一下:



    1. 用户访问接口获取分类树时,先从Redis中查询数据。

    2. 如果Redis中有数据,则直接数据。

    3. 如果Redis中没有数据,则再从数据库中查询数据,拼接成分类树返回。

    4. 将从数据库中查到的分类树的数据,保存到Redis中,设置过期时间5分钟。

    5. 将分类树返回给用户。


    我们在Redis中定义一个了key,value是一个分类树的json格式转换成了字符串,使用简单的key/value形式保存数据。


    经过这样优化之后,dev环境的联调和自测顺利完成了。


    第2次优化


    我们将这个功能部署到st环境了。


    刚开始测试同学没有发现什么问题,但随着后面不断地深入测试,隔一段时间就出现一次首页访问很慢的情况。


    于是,我们马上进行了第2次优化。


    我们决定使用Job定期异步更新分类树到Redis中,在系统上线之前,会先生成一份数据。


    当然为了保险起见,防止Redis在哪条突然挂了,之前分类树同步写入Redis的逻辑还是保留。


    于是,流程图改成了这样:

    增加了一个job每隔5分钟执行一次,从数据库中查询分类数据,封装成分类树,更新到Redis缓存中。


    其他的流程保持不变。


    此外,Redis的过期时间之前设置的5分钟,现在要改成永久。


    通过这次优化之后,st环境就没有再出现过分类树查询的性能问题了。


    第3次优化


    测试了一段时间之后,整个网站的功能快要上线了。


    为了保险起见,我们需要对网站首页做一次压力测试。


    果然测出问题了,网站首页最大的qps是100多,最后发现是每次都从Redis获取分类树导致的网站首页的性能瓶颈。


    我们需要做第3次优化。


    该怎么优化呢?


    答:加内存缓存。


    如果加了内存缓存,就需要考虑数据一致性问题。


    内存缓存是保存在服务器节点上的,不同的服务器节点更新的频率可能有点差异,这样可能会导致数据的不一致性。


    但分类本身是更新频率比较低的数据,对于用户来说不太敏感,即使在短时间内,用户看到的分类树有些差异,也不会对用户造成太大的影响。


    因此,分类树这种业务场景,是可以使用内存缓存的。


    于是,我们使用了Spring推荐的caffine作为内存缓存。


    改造后的流程图如下:



    1. 用户访问接口时改成先从本地缓存分类数查询数据。

    2. 如果本地缓存有,则直接返回。

    3. 如果本地缓存没有,则从Redis中查询数据。

    4. 如果Redis中有数据,则将数据更新到本地缓存中,然后返回数据。

    5. 如果Redis中也没有数据(说明Redis挂了),则从数据库中查询数据,更新到Redis中(万一Redis恢复了呢),然后更新到本地缓存中,返回返回数据。



    需要注意的是,需要改本地缓存设置一个过期时间,这里设置的5分钟,不然的话,没办法获取新的数据。



    这样优化之后,再次做网站首页的压力测试,qps提升到了500多,满足上线要求。
    最近我建了新的技术交流群,打算将它打造成高质量的活跃群,欢迎小伙伴们加入。


    我以往的技术群里技术氛围非常不错,大佬很多。


    image.png


    加微信:su_san_java,备注:加群,即可加入该群。


    第4次优化


    之后,这个功能顺利上线了。


    使用了很长一段时间没有出现问题。


    两年后的某一天,有用户反馈说,网站首页有点慢。


    我们排查了一下原因发现,分类树的数据太多了,一次性返回了上万个分类。


    原来在系统上线的这两年多的时间内,运营同学在系统后台增加了很多分类。


    我们需要做第4次优化。


    这时要如何优化呢?


    限制分类树的数量?


    答:也不太现实,目前这个业务场景就是有这么多分类,不能让用户选择不到他想要的分类吧?


    这时我们想到最快的办法是开启nginxGZip功能。


    让数据在传输之前,先压缩一下,然后进行传输,在用户浏览器中,自动解压,将真实的分类树数据展示给用户。


    之前调用接口返回的分类树有1MB的大小,优化之后,接口返回的分类树的大小是100Kb,一下子缩小了10倍。


    这样简单的优化之后,性能提升了一些。


    第5次优化


    经过上面优化之后,用户很长一段时间都没有反馈性能问题。


    但有一天公司同事在排查Redis中大key的时候,揪出了分类树。之前的分类树使用key/value的结构保存数据的。


    我们不得不做第5次优化。


    为了优化在Redis中存储数据的大小,我们首先需要对数据进行瘦身。


    只保存需要用到的字段。


    例如:


    @AllArgsConstructor
    @Data
    public class Category {

    private Long id;
    private String name;
    private Long parentId;
    private Date inDate;
    private Long inUserId;
    private String inUserName;
    private List children;
    }

    像这个分类对象中inDate、inUserId和inUserName字段是可以不用保存的。


    修改自动名称。


    例如:


    @AllArgsConstructor
    @Data
    public class Category {
    /**
    * 分类编号
    */

    @JsonProperty("i")
    private Long id;

    /**
    * 分类层级
    */

    @JsonProperty("l")
    private Integer level;

    /**
    * 分类名称
    */

    @JsonProperty("n")
    private String name;

    /**
    * 父分类编号
    */

    @JsonProperty("p")
    private Long parentId;

    /**
    * 子分类列表
    */

    @JsonProperty("c")
    private List children;
    }

    由于在一万多条数据中,每条数据的字段名称是固定的,他们的重复率太高了。


    由此,可以在json序列化时,改成一个简短的名称,以便于返回更少的数据大小。


    这还不够,需要对存储的数据做压缩。


    之前在Redis中保存的key/value,其中的value是json格式的字符串。


    其实RedisTemplate支持,value保存byte数组


    先将json字符串数据用GZip工具类压缩成byte数组,然后保存到Redis中。


    再获取数据时,将byte数组转换成json字符串,然后再转换成分类树。


    这样优化之后,保存到Redis中的分类树的数据大小,一下子减少了10倍,Redis的大key问题被解决了。




    作者:苏三说技术
    来源:juejin.cn/post/7233012756315963452
    收起阅读 »

    宏观经济的一些判断

    估计谁都没想到2023年,疫情恢复的第一年竟然如此:大厂裁员、楼市继续下跌、年底股市暴跌等。 这篇文章说下笔者对宏观经济的一些理解和判断,以试图解释过去一年发生的一些事情。 宏观经济周期 笔者看来,由于美元具备全球货币属性,美元加息是全球的麻烦。所以总体而言,...
    继续阅读 »

    估计谁都没想到2023年,疫情恢复的第一年竟然如此:大厂裁员、楼市继续下跌、年底股市暴跌等。


    这篇文章说下笔者对宏观经济的一些理解和判断,以试图解释过去一年发生的一些事情。


    宏观经济周期


    笔者看来,由于美元具备全球货币属性,美元加息是全球的麻烦。所以总体而言,全球经济处于美元加息影响下的结果中,也就是处于美元回流,全球货币紧缩,全球经济处于下行周期中


    但每个主要经济体又有其自身的宏观经济周期,比如说我们国家,疫情三年为了人民的生命健康,我们国家没有像西方打开方便之门,大肆放水,而是选择严守死防。


    三年疫情,我们国家保护了人民的生命健康,但我们也封闭了三年,紧缩了三年。我们国家处于疫情恢复周期中,处于上行周期中。但始作俑者的美国因为超发货币,自身经济过热,所以需要降低过热经济,所以宏观经济也处于下行周期中。


    这里说一个题外话,宏观经济是通缩好还是通货膨胀好呢?实际上,过分的通缩和过分的通货膨胀都不好,适度的通胀最好,一般来说维持在2%左右是比较好的,这也就是美国一直想通过加息实现的通胀水平。


    不管是通缩还是通胀代表的都是一种趋势,通缩经济逐渐下行,规模逐渐减小,反之通胀经济处于上行,规模逐渐扩大,这样才能实现GDP的增长,实现经济的发展。


    回来继续说宏观经济周期,当下国家要做的就是让经济处于扩张区间,或者说实现经济的适度通胀。


    2023年CPI和PPI一直不温不火,其实有一点通缩的迹象,所以国家在出台各种手段改善这种迹象:



    1. 对楼市逐渐降低首付比例,逐渐放开楼市,逐渐降息,逐渐降低存款准备金等

    2. 对股市降低印花税,转融通暂停,加大逆回购力度,证监会新管理层上任等


    但改变趋势谈何容易,同时还要考虑美国的加息节奏。所以出台的手段并不是那么尽如人意,也因为受到美国加息节奏的影响,为了维持外汇的稳定性,出台的时机也并未尽如人意。


    笔者觉得正因为上述原因:美元加息周期、国内降息周期导致2023大厂裁员、楼市继续下跌、年底股市暴跌。


    2024年宏观经济


    目前美国已经暂停加息半年,加息周期已经到顶,预期5、6月份后降息。也正因为暂停加息,加息到顶我国央行今天进一步降低5年期贷款利率。


    2024年的国内的宏观经济依然是将经济恢复为适度通胀的水平。笔者的判断是在汇率稳定的前提下,稳定楼市,因为楼市涉及的面太大。稳定了楼市才能稳定经济稳定预期,才能稳定股市。


    完全有可能的是央行在美国降息后选择再一次降息贷款利息的方式,加强人们的信心,加强人们对国家进一步搞活经济的决心,给人们信心。


    稳定楼市而不是大力发展楼市。笔者认为未来国家的发展会逐渐向高科技产业倾斜,从依靠楼市带动GDP增长改为依靠科技带动。


    综上,一些配套的改革一定也会有:比如真正支持优秀公司发展的证券市场,比如政府职能转变为服务好企业,比如更加开放的区域市场(现在的浦东新区)等等。


    而笔者比较关心的股票市场,隶属于证券市场,笔者觉得未来中国的股市一定会越来越好,尤其优秀的科技公司。


    (本文完)


    作者:通往自由之路
    来源:juejin.cn/post/7337227407365963803
    收起阅读 »

    图片转base64,实现图片上传?你学会了吗?

    web
    前言 前段时间在写我的VUE全栈项目的时候,遇到要把前端的照片上传到后端,再由后端存到数据库的问题,通过网上查找资料,看了一些其他写者的解决方法,最终采用转BASE64的方法。本人觉得把上传的图片转为BASE64格式相比其他是比较简单的。 什么是BASE64 ...
    继续阅读 »

    前言


    前段时间在写我的VUE全栈项目的时候,遇到要把前端的照片上传到后端,再由后端存到数据库的问题,通过网上查找资料,看了一些其他写者的解决方法,最终采用转BASE64的方法。本人觉得把上传的图片转为BASE64格式相比其他是比较简单的。


    什么是BASE64


    Base64是一种用64个字符来表示任意二进制数据的方法。它是一种编码方式,而非加密方式,即可以将一张图片数据编码成一串字符串,使用该字符串代替图像地址


    BASE64的优缺点


    优点: 减少一张图片的http请求


    缺点: 导致转换后的css文件体积增大,而CSS 文件的体积直接影响渲染,导致用户会长时间注视空白屏幕,而且转换后的数据是一大串字符串。



    注意:图片转BASE64格式的适合小图片或者极简单图片,大图片不划算。它的格式为:data:image/type;base64,xxxx...



    虽然说这种方式不适用于体积大的图片,但不得不说有时候还挺方便的。由于在我的vue项目中上传的图片都比较小,单一,为了方便我采用了这种方式来实现将前端上传的图片存到数据库中。


    话不多说,进入正题!下面以Vue+Koa框架、数据库为MYSQL为例。


    案例


    前端: 首先在把图片传给后端之前,前端对图片进行格式转换,转换成功后就可以照常调用后端给的接口,传进去就行。



    我这里就是点了提交按钮后,触发编写的点击事件函数,在函数里先对图片转base64,转成功后再调用后端给的接口,把此数据以及其他数据传进去就行。



    在前端编写转base64的函数(很重要)


    export function uploadImgToBase64 (file) {
    return new Promise((resolve, reject) => {
    const reader = new FileReader()//html5提供的一种异步文件读取机制
    reader.readAsDataURL(file)//将文件读取为Base64编码的数据URL
    reader.onload = function () { // 图片转base64完成后返回reader对象
    resolve(reader)
    }
    reader.onerror = reject
    })
    }

    前端要为转为base64的图片的相应操作


    const state = reactive({
    picture: [] //这个是用来装表单里选中的照片
    })


    const imgBroadcastListBase64 = [] //用来存放转base64后的照片,用数组是因为上传的图片可能不止一张
    console.log('图片转base64开始...')
    // 遍历每张图片picture 异步
    const filePromises = state.picture.map(async file => {
    //console.log(file);
    const response = await uploadImgToBase64(file.file) //调用函数 将图片转为base64
    //console.log(response,111);
    return response.result.replace(/.*;base64,/, '') // 去掉data:image/jpeg;base64,
    })
    // 按次序输出 base64图片
    for (const textPromise of filePromises) {
    imgBroadcastListBase64.push(await textPromise)
    }
    console.log('图片转base64结束..., ', imgBroadcastListBase64)


    //判断imgBroadcastListBase64是否<=1,是的话就是上传一张图片,否则上传的是多张图片
    if(imgBroadcastListBase64.length<=1){
    state.imgsStr = imgBroadcastListBase64.join()//转字符串
    }else{
    state.imgsStr = imgBroadcastListBase64.join(',')//转字符串并且每个值用','拼接,这样是为了方便后面从数据库拿到数据,将图片又转为之前的base64格式
    }
    //调用后端提供的接口,传数据到数据库里(这个只是自己编写的后端接口,主要是为了展示传数据)
    const res = await secondGoodsAdd({
    create_time: ti,
    content_goods: state.content,
    color: state.title2,
    price: state.title3,
    tel: state.title4,
    img: state.imgsStr,//转base64后的图片
    concat_num: 0,
    like_num: 0,
    name_goods: state.title1
    })
    if (res.code === '80000') {
    showSuccessToast('发布成功!')
    }
    router.push('/cicle')


    存到数据库中的图片路径是转为base64后的且删除前面data:image/jpeg;base64的字符串。



    这样数据就存到数据库中啦!存进去的是字符串。


    那么问题来了,数据是存进去了,但是我又想拿到这个数据到前端显示出来,好,那我就先直接拿到前端用,结果发现报错了!说是请求头太长?想办法解决下!后面我的解决办法是拿到这个转为了base64且去掉前面data...字段的的图片数据再转为正常的base64的格式。好,来转换吧!


    //我这里是在后端编写的接口,用于展示被添加到数据库中的所有数据
    router.get('/cirleLifeLook',async(ctx,next)=>{
    try {
    const result=await userService.cirleLifeLook()
    for(let i=0;i<result.length;i++){
    var imgData=result[i].img //获取每条数据中的照片字段
    if(imgData){
    if(imgData.indexOf(',')>-1){//存在','的话代表是多张图片的混合的字符串
    let ans=imgData.split(',') //切割 获得多张之前切掉data...后的base64字符串
    let s=[]
    for(let j=0;j<ans.length;j++){
    s.push("data:image/png;base64,"+ans[j])//还原每张图片初始的base64数据
    }
    result[i].img=s
    }else{
    result[i].img="data:image/png;base64,"+imgData //就一张图片直接在前面拼接"data:image/png;base64,"
    }
    }
    }//到此为止,在给前端传数据前就修改了其中每条数据里的照片地址,这样就可以正常显示啦
    if(result.length){
    ctx.body={
    code:'80000',
    data:result,
    msg:'获取成功'
    }
    }else{
    ctx.body={
    code:'80005',
    data:'null',
    msg:'还没有信息'
    }
    }
    } catch (error) {
    ctx.body={
    code:'80002',
    data:error,
    msg:'服务器异常'
    }
    }
    })

    OK,到此就结束啦~现在是不是觉得把图片转base64还是挺简单的?还挺有用的?快去实践下吧。记住此方法只适合小图片类型的,大点的文件可能会崩掉哈!


    结束语


    本篇文章就到此为止啦,由于本人经验水平有限,难免会有纰漏,对此欢迎指正。如觉得本文对你有帮助的话,欢迎点赞收藏❤❤❤,您的点赞是持续写作的动力,感谢支持。要是您觉得有更好的方法,欢迎评论,提出建议!


    作者:嗯嗯呢
    来源:juejin.cn/post/7255785481119727672
    收起阅读 »

    听说你会架构设计?来,弄一个红包系统

    大家好,我是小❤,一个漂泊江湖多年的 985 非科班程序员,曾混迹于国企、互联网大厂和创业公司的后台开发攻城狮。 1. 引言 当我那天拿着手机,正在和朋友们的微信群里畅聊着八卦新闻和即将到来的周末计划时,忽然一条带着喜意的消息扑面而来,消息正中间赫然写着八个大...
    继续阅读 »

    大家好,我是小❤,一个漂泊江湖多年的 985 非科班程序员,曾混迹于国企、互联网大厂和创业公司的后台开发攻城狮。


    1. 引言


    当我那天拿着手机,正在和朋友们的微信群里畅聊着八卦新闻和即将到来的周末计划时,忽然一条带着喜意的消息扑面而来,消息正中间赫然写着八个大字:恭喜发财,大吉大利



    抢红包!!相信大部分人对此都不陌生,自 2015 年春节以来,微信就新增了各类型抢红包功能,吸引了数以亿万级的用户参与体验,今天,我们就来聊一聊这个奇妙有趣的红包系统。


    2. 概要设计


    2.1 系统特点



    抢红包系统从功能拆分,可以分为包红包、发红包、抢红包和拆红包 4 个功能。


    对于系统特性来说,抢红包系统和秒杀系统类似。



    每次发红包都是一次商品秒杀流程,包括商品准备,商品上架,查库存、减库存,以及秒杀开始,最终的用户转账就是红包到账的过程。


    2.2 难点


    相比秒杀活动,微信发红包系统的用户量更大,设计更加复杂,需要重视的点更多,主要包括以下几点。


    1、高并发


    海量并发请求,秒杀只有一次活动,但红包可能同一时刻有几十万个秒杀活动。


    比如 2017 鸡年除夕,微信红包抢红包用户数高达 3.42 亿,收发峰值 76 万/秒,发红包 37.77 亿 个。


    2、安全性要求


    红包业务涉及资金交易,所以一定不能出现超卖、少卖的情况。



    • 超卖:发了 10 块钱,结果抢到了 11 块钱,多的钱只能系统补上,如此为爱发电应用估计早就下架了;

    • 少卖:发了 10 块钱,只抢了 9 块,多的钱得原封不动地退还用户,否则第二天就接到法院传单了。


    3、严格事务


    参与用户越多,并发 DB 请求越大,数据越容易出现事务问题,所以系统得做好事务一致性


    这也是一般秒杀活动的难点所在,而且抢红包系统涉及金钱交易,所以事务级别要求更高,不能出现脏数据


    3. 概要设计


    3.1 功能说明


    抢红包功能允许用户在群聊中发送任意个数和金额的红包,群成员可以抢到随机金额的红包,但要保证每个用户的红包金额不小于 0.01 元



    抢红包的详细交互流程如下:



    1. 用户接收到抢红包通知,点击通知打开群聊页面;

    2. 用户点击抢红包,后台服务验证用户资格,确保用户尚未领取过此红包;

    3. 若用户资格验证通过,后台服务分配红包金额并存储领取记录;

    4. 用户在微信群中看到领取金额,红包状态更新为“已领取”;

    5. 异步调用支付接口,将红包金额更新到钱包里。


    3.2 数据库设计


    红包表 redpack 的字段如下:



    • id: 主键,红包ID

    • userId: 发红包的用户ID

    • totalAmount: 总金额

    • surplusAmount: 剩余金额

    • total: 红包总数

    • surplusTotal: 剩余红包总数


    该表用来记录用户发了多少红包,以及需要维护的剩余金额。


    红包记录表 redpack_record 如下:



    • id: 主键,记录ID

    • redpackId: 红包ID,外键

    • userId: 用户ID

    • amount: 抢到的金额


    记录表用来存放用户具体抢到的红包信息,也是红包表的副表。


    3.3 发红包



    1. 用户设置红包的总金额和个数后,在红包表中增加一条数据,开始发红包;

    2. 为了保证实时性和抢红包的效率,在 Redis 中增加一条记录,存储红包 ID 和总人数 n``;

    3. 抢红包消息推送给所有群成员。


    3.4 抢红包


    从 2015 年起,微信红包的抢红包和拆红包就分离了,用户点击抢红包后需要进行两次操作。


    这也是为什么明明有时候抢到了红包,点开后却发现该红包已经被领取完了



    抢红包的交互步骤如下:



    1. 抢红包:抢操作在 Redis 缓存层完成,通过原子递减的操作来更新红包个数,个数递减为 0 后就说明抢光了。

    2. 拆红包:拆红包时,首先会实时计算金额,一般是通过二倍均值法实现(即 0.01 到剩余平均值的 2 倍之间)。

    3. 红包记录:用户获取红包金额后,通过数据库的事务操作累加已经领取的个数和金额,并更新红包表和记录表。

    4. 转账:为了提升效率,最终的转账为异步操作,这也是为什么在春节期间,红包领取后不能立即在余额中看到的原因。


    上述流程,在一般的秒杀活动中随处可见,但是,红包系统真的有这么简单吗?


    当用户量过大时,高并发下的事务一致性怎么保证,数据分流如何处理,红包的数额分配又是怎么做的,接下来我们一一探讨。


    4. 详细设计


    由于是秒杀类设计,以及 money 分发,所以我们重点关注抢红包时的高并发解决方案和红包分配算法。


    4.1 高并发解决方案


    首先,抢红包系统的用户量很大,如果几千万甚至亿万用户同时在线发抢红包,请求直接打到数据库,必然会导致后端服务过载甚至崩溃。


    而在这种业务量下,简单地对数据库进行扩容不仅会让成本消耗剧增,另一方面由于存在磁盘的性能瓶颈,所以大概率解决不了问题。


    所以,我们将解决方案集中在 减轻系统压力、提升响应速度 上,接下来会从缓存、加锁、异步分治等方案来探讨可行性。


    1、缓存


    和大多数秒杀系统设计相似,由于抢红包时并发很高,如果直接操作 DB 里的数据表,可能触发 DB 锁的逻辑,导致响应不及时。



    所以,我们可以在 DB 落盘之前加一层缓存,先限制住流量,再处理红包订单的数据更新。


    这样做的优点是用缓存操作替代了磁盘操作,提升了并发性能,这在一般的小型秒杀活动中非常有效!


    但是,随着微信使用发&抢红包的用户量增多,系统压力增大,各种连锁反应产生后,数据一致性的问题逐渐暴露出来:



    • 假设库存减少的内存操作成功,但是 DB 持久化失败了,会出现红包少发的问题;

    • 如果库存操作失败,DB 持久化成功,又可能会出现红包超发的问题。


    而且在几十万的并发下,直接对业务加锁也是不现实的,即便是乐观锁。


    2、加锁


    在关系型 DB 里,有两种并发控制方法:分为乐观锁(又叫乐观并发控制,Optimistic Concurrency Control,缩写 “OCC”)和悲观锁(又叫悲观并发,Pessimistic Concurrency Control,缩写“PCC”)。



    悲观锁在操作数据时比较悲观,认为别的事务可能会同时修改数据,所以每次操作数据时会先把数据锁住,直到操作完成


    乐观锁正好相反,这种策略主打一个“信任”的思想,认为事务之间的数据竞争很小,所以在操作数据时不会加锁,直到所有操作都完成到提交时才去检查是否有事务更新(通常是通过版本号来判断),如果没有则提交,否则进行回滚。


    在高并发场景下,由于数据操作的请求很多,所以乐观锁的吞吐量更大一些。但是从业务来看,可能会带来一些额外的问题:



    1. 抢红包时大量用户涌入,但只有一个可以成功,其它的都会失败并给用户报错,导致用户体验极差;

    2. 抢红包时,如果第一时间有很多用户涌入,都失败回滚了。过一段时间并发减小后,反而让手慢的用户抢到了红包

    3. 大量无效的更新请求和事务回滚,可能给 DB 造成额外的压力,拖慢处理性能。


    总的来说,乐观锁适用于数据竞争小,冲突较少的业务场景,而悲观锁也不适用于高并发场景的数据更新。


    因此对于抢红包系统来说,加锁是非常不适合的。


    3、异步分治


    综上所述,抢红包时不仅要解决高并发问题、还得保障并发的顺序性,所以我们考虑从队列的角度来设计。


    我们知道,每次包红包、发红包、抢红包时,也有先后依赖关系,因此我们可以将红包 ID 作为一个唯一 Key,将发一次红包看作一个单独的 set,各个 set 相互独立处理



    这样,我们就把海量的抢红包系统分成一个个的小型秒杀系统,在调度处理中,通过对红包 ID 哈希取模,将一个个请求打到多台服务器上解耦处理。


    然后,为了保证每个用户抢红包的先后顺序,我们把一个红包相关的操作串行起来,放到一个队列里面,依次消费。


    从上述 set 分流我们可以看出,一台服务器可能会同时处理多个红包的操作,所以,为了保证消费者处理 DB 不被高并发打崩,我们还需要在消费队列时用缓存来限制并发消费数量


    抢红包业务消费时由于不存储数据,只是用缓存来控制并发。所以我们可以选用大数据量下性能更好的 Memcached。


    除此之外,在数据存储上,我们可以用红包 ID 进行哈希分表,用时间维度对 DB 进行冷热分离,以此来提升单 set 的处理性能。


    综上所述,抢红包系统在解决高并发问题上采用了 set 分治、串行化队列、双维度分库分表 等方案,使得单组 DB 的并发性能得到了有效提升,在应对数亿级用户请求时取得了良好的效果。


    4.2 红包分配算法


    抢红包后,我们需要进行拆红包,接下来我们讨论一下红包系统的红包分配算法。


    红包金额分配时,由于是随机分配,所以有两种实现方案:实时拆分和预先生成。


    1、实时拆分


    实时拆分,指的是在抢红包时实时计算每个红包的金额,以实现红包的拆分过程。


    这个对系统性能和拆分算法要求较高,例如拆分过程要一直保证后续待拆分红包的金额不能为空,不容易做到拆分的红包金额服从正态分布规律。


    2、预先生成


    预先生成,指的是在红包开抢之前已经完成了红包的金额拆分,抢红包时只是依次取出拆分好的红包金额。


    这种方式对拆分算法要求较低,可以拆分出随机性很好的红包金额,但通常需要结合队列使用。


    3、二倍均值法


    综合上述优缺点考虑,以及微信群聊中的人数不多(目前最高 500 人),所以我们采用实时拆分的方式,用二倍均值法来生成随机红包,只满足随机即可,不需要正态分布。



    故可能出现很大的红包差额,但这更刺激不是吗🐶



    使用二倍均值法生成的随机数,每次随机金额会在 0.01 ~ 剩余平均值*2 之间。


    假设当前红包剩余金额为 10 元,剩余个数为 5,10/5 = 2,则当前用户可以抢到的红包金额为:0.01 ~ 4 元之间。


    4、算法优化


    用二倍均值法生成的随机红包虽然接近平均值,但是在实际场景下:微信红包金额的随机性和领取的顺序有关系,尤其是金额不高的情况下


    于是,小❤耗费 “巨资” 在微信群发了多个红包,得出了这样一个结论:如果发出的 红包总额 = 红包数*0.01 + 0.01,比如:发了 4 个红包,总额为 0.05,则最后一个人领取的红包金额一定是 0.02



    无一例外:



    所以,红包金额算法大概率不是随机分配,而是在派发红包之前已经做了处理。比如在红包金额生成前,先生成一个不存在的红包,这个红包的总额为 0.01 * 红包总数


    而在红包金额分配的时候,会对每个红包的随机值基础上加上 0.01,以此来保证每个红包的最小值不为 0。


    所以,假设用户发了总额为 0.04 的个数为 3 的红包时,需要先提取 3*0.01 到 "第四个" 不存在的红包里面,于是第一个人抢到的红包随机值是 0 ~ (0.04-3*0.01)/3


    由于担心红包超额,所以除数的商是向下取二位小数,0 ~ (0.04-3*0.01)/3 ==> (0 ~ 0) = 0,再加上之前提取的保底值 0.01,于是前两个抢到的红包金额都是 0.01。最后一个红包的金额为红包余额,即 0.02


    算法逻辑用 Go 语言实现如下:


        import (
       "fmt"
       "math"
       "math/rand"
       "strconv"
    )

    type RedPack struct {
        SurplusAmount float64 // 剩余金额
         SurplusTotal int // 红包剩余个数
    }

    // 取两位小数
    func remainTwoDecimal(num float64) float64 {
        numStr := strconv.FormatFloat(num, 'f'264)
        num, _ = strconv.ParseFloat(numStr, 64)
        return num
    }

    // 获取随机金额的红包
    func getRandomRedPack(rp *RedPack) float64 {
        if rp.SurplusTotal <= 0 {
            // 该红包已经被抢完了
            return 0
        }

        if rp.SurplusTotal == 1 {
            return remainTwoDecimal(rp.SurplusAmount + 0.01)
        }

           // 向下取整
        avgAmount := math.Floor(100*(rp.SurplusAmount/float64(rp.SurplusTotal))) / float64(100)
        avgAmount = remainTwoDecimal(avgAmount)

           // 生成随机数种子
        rand.NewSource(time.Now().UnixNano())

        var max float64
        if avgAmount > 0 {
            max = 2*avgAmount - 0.01
        } else {
            max = 0
        }
        money := remainTwoDecimal(rand.Float64()*(max) + 0.01)

        rp.SurplusTotal -= 1
        rp.SurplusAmount = remainTwoDecimal(rp.SurplusAmount + 0.01 - money)

        return money
    }

    // 实现主函数
    func main() {
        rp := &RedPack{
            SurplusAmount: 0.06,
            SurplusTotal:  5,
        }
        rp.SurplusAmount -= 0.01 * float64(rp.SurplusTotal)
        total := rp.SurplusTotal
        for i := 0; i < total; i++ {
            fmt.Println(getRandomRedPack(rp))
        }
    }

    打印结果:



    0.01、0.01、0.01、0.01、0.02



    喜大普奔,符合预期!


    5. 总结


    设计一个红包系统不仅要考虑海量用户的并发体验和数据一致性,还得保障用户资金的安全


    这种技术难点,对于传统的 “秒杀系统” 有过之而无不及。


    本文主要探讨了高并发场景下的设计方案和红包分配的算法,覆盖了目前红包系统常见的几大难题。




    作者:xin猿意码
    来源:juejin.cn/post/7312352501406908452
    收起阅读 »

    代码要同时推送到 gitee 和 github 该怎么办?教你两招!

    前言 我们作为特色社会主义社会中的一份子,跟国外程序员不一样的地方在于,别人能轻松访问 github, 我们却要借梯子,或者用国产的 gitee。 作为有追求的程序员,作为成年人,当然是两个我都要! 那么如何优雅地把本地代码同时维护到 gitee 和 gith...
    继续阅读 »

    前言


    我们作为特色社会主义社会中的一份子,跟国外程序员不一样的地方在于,别人能轻松访问 github, 我们却要借梯子,或者用国产的 gitee


    作为有追求的程序员,作为成年人,当然是两个我都要!


    那么如何优雅地把本地代码同时维护到 giteegithub上呢?这里带给大家2个方法。


    push 2个 remote original


    假如我们在 github 创建了一个仓库,那么本地 clone 下来后,主分支(main)是默认跟github 仓库的主分支(main) 关联的。这样直接在 VSCode 里面点 同步的圈圈 就会自动同步,如下图。


    image.png


    现在我又要推到 gitee,怎么办,很简单,新增一个 remote 源,并命名为 gitee,默认的 origin 已经跟 github 关联了。


    git remote add gitee 

    这样我们就有2个源,在 VSCodeGit Graph 中可以直接 push 到2个源中,2个都勾选上。


    image.png


    点击 Yes, push 就可以推上去了。爽歪歪


    gitee 仓库镜像管理:gitee -> github


    本来上面的方法用得好好的,但是我的梯子质量不好,时不时推不上 github ( gitee 倒是轻轻松松,没有失败过),然后我又得重新 push,有的时候要重试好多次才行,非常浪费时间!!


    然后我就发现 gitee 原来是可以同步推到 github 的,根本就不用我们手动操作,点赞!


    image.png


    官方链接在这里 仓库镜像管理(Gitee<->Github 双向同步),我就不做搬运工了,给几个图:


    image.png


    image.png


    其中还需要到 github 获取 token,方法如下:


    image.png


    官方链接在这里 如何申请 GitHub 私人令牌?


    我设置成功如下图:


    image.png



    token生成的时候要设置过期时间,尽量设置长一点比如一年。



    image.png


    推到 gitee 的代码会自动同步到 github, 我们也可以手动点右侧的 更新 按钮,手动同步。


    经我检测,是成功的,刚刚推不上去 github 的代码,通过 gitee 同步过去了,本地也能看到代码是同步的。


    image.png



    有一个需要注意的是,你只能推到你自己的github仓库,不能推到你的github组织的仓库。



    以我的仓库为例:



    因为选推送的目标仓库时压根就不能选组织,只能选个人!但是源仓库可以是个人的或者组织的。


    总结


    本文介绍了2种同步 gitee 和 github 仓库的方法,视情况选择:



    • 当目标仓库是个人时,第二种会比较方便,推上到 gitee 后,会自动同步到 github

    • 当目标仓库是组织时,不能用第二种,只能用第一种,自己手动 push 到2个仓库,需要你的梯子质量好,不然可能推不上 github


    最后把我每天都看的美女分享给大家~养眼啊


    pretty-girl.png


    作者:菲鸽
    来源:juejin.cn/post/7327353620232339506
    收起阅读 »

    总结一下程序员的一些经历

    前言 大学毕业已经6年了,不知不觉已经到了30岁,不免会对中年危机产生焦虑心理。平常上掘金浏览文章,除了一些技术相关的,还有倔友工作经历相关的,以求得一丝共鸣。最近有感而发,仔细想想自己也没有写过总结的文章,所以想总结一下自己的经历,同时有什么想法大家也可以交...
    继续阅读 »

    前言


    大学毕业已经6年了,不知不觉已经到了30岁,不免会对中年危机产生焦虑心理。平常上掘金浏览文章,除了一些技术相关的,还有倔友工作经历相关的,以求得一丝共鸣。最近有感而发,仔细想想自己也没有写过总结的文章,所以想总结一下自己的经历,同时有什么想法大家也可以交流一下。


    大学


    我大学学的计算机专业,不知道大家的专业是自己深思熟虑后报的还是随便报的,反正我是随便报的。当时填志愿的时候家里人希望我去学医,其实我是有点不太想,但是我又不知道该报什么专业。由于我个人的性格是比较内向的,不太喜欢和人打交道,所以希望以后的工作也如此。在排除了一些专业后,发现计算机专业是和电脑打交道,然后就报了,后来家里人也同意了,最后在杭州上的。


    大学期间大一学的课程学的是基础课和java,c#等基础语言,大二学的是计算机组成和网络相关。期间我也没怎么规划未来方向,目标是拿了学分就行。


    到了大三,专业细分,我选择了软件工程,其他的数字媒体和计算机科学感觉不太适合我。大一大二的课程安排还算紧凑,大三课程少了很多,平均下来,一天两门课,也就是4课时。学校管的比较宽,那会儿有空闲的时间就在寝室里玩游戏。到了期末的时候,就临时抱佛脚,正所谓“一花一世界,一叶一菩提,一天一本书,一周一学期”,所以现在感觉那个时候还是挺没有目标的。


    不知道大家有没有这样的感受:大三的课程要求大多是软件设计,老师教的都是理论知识,但是实际需要完成一个项目。比方说,上课的时候教的是“类”,“设计模式”,到了期末就要做一个课程管理系统。那个时候感觉知识体系都挺零散的,也没有UI框架(或者是组件)的概念,甚至java里的swing也要学,基本使用的是原生的,也没有什么思路,一个页面也不知道怎么去做。但是最终为了完成期末作业,我不得不上网一步一步跟着做,没有加入自己的想法
    ,就这样搬运完成了。


    实习


    到了大四,基本没有课程,要求就是实习。我的技术总结起来就仅仅是学过,想着能有公司收留就行,投了几家,面了两家,都失败了。那会儿是G20,留在杭州还挺麻烦的,需要学校报备,然后还要和学校签免责协议。回老家又没有什么软件公司,正在为实习这个事情发愁的时候,好在有一个亲戚在老家有开公司,所以我就趁着这个机会被介绍进去了。


    介绍的时候亲戚说建议我去学习他的本行业,也就是制造行业;意思是他在软件这块投了挺多钱,不对软件抱太大期望,希望我转其他的。我考虑到我的实习是需要和开发相关的,所以想要毕业,必须做开发,最终我被安排在一个软件子公司下面。第二天报到之后,我的上级领导就了解我的背景,有没有学过数据库之类的,我就说有。还有刚来上几天班的两个小伙伴,他们是社招的。然后我们几个就一起学习公司的相关框架。


    公司实际上是二次开发,之前是花钱买的开发平台。页面也比较简单,就是一些单据。所以我们的工作内容也比较简单,就是拖拖控件,写写增删改查事件。简单的表单熟练了之后一下午可以搞三四个。很长时间都是比较清闲。当然一方面也是因为我是实习,优先安排学校的事,然而我也是写写实习总结。中间也碰到了一些困难,确实业务上复杂的情况也有,需要写存储过程;那时我以为做不出来是能力不行,想一个人硬着抗下所有。最后在一个小时没有解决用户反馈的问题之后,然而我的上级领导态度挺好的,说这个业务挺复杂,他来亲自上手。


    就这样过了一个月,等到了发工资的时候,人家都发了,我才发现我没有谈论这个问题,后来上级领导跟总经理反馈了,总经理说实习是没有工资的。这里说一下,我那个亲戚是总裁,总经理是他的侄子,执行权在总经理,况且我跟他也不认识。我想着实习就算了吧,毕业后再说。


    过了三个月,不知道是不是我上级领导提的,竟然和我签合同,说是总经理破例给我发工资。等到发工资那天,发了2K的实习工资,我还是挺开心的,毕竟是我自己的劳动所得。由于这几个月吃住都是家里的,我选择给了家里,然而家里给我留了一半当生活费。这几个月实习期间,有学到一些知识和经验,到了后期基本就是重复的工作,没有了更多的技术提升。


    毕业


    然后到了毕业前的上半年,由于要做毕业设计,需要和导师经常沟通,所以呆在老家不太方便,所以我得换公司实习。其实这个时候我留在学校做毕业设计就可以了,但是看到周围的童鞋都在公司上班,我也不想掉队。然后我又在手机招聘上投了几家公司,但又是石沉大海。后来突然收到杭州的两家面试,我很奇怪竟然不是我投的那几家,估计招聘软件可以看到,后面面了之后过了。然后我就跟领导说我要回学校了,明天就要走,当时我也不太清楚交接的概念,感觉做的内容也挺简单的。没想到领导竟然也同意了,然后我一天内交接完,之后就回学校了。


    毕业来了杭州的这家公司(简称公司A),一待就是4年半,你们可能会问为什么第一家公司可以待这么久,包括我和同事都这么说,其实也是无奈。公司规模还挺大,用的是.NET,之前写的是C#客户端,所以很多东西需要重新学。在这家公司实习,也没有安排太多活,主要是学习一些技术,写几个简单的页面。同时也搞一下学校的毕业设计。等到2017年6月份答辩完,我就正式签了合同。同时我也离开了学校,得知大学同学公司在同一个区,就和他们一起租了一个160平的大套房,几个人平均下来人均1300一月。那时公司A开的工资不高,只有几k,然后每个月都在精打细算,看看能存下多少钱。


    公司A不是外包公司,但是我所在的部门是做项目的,主要为甲方做定制,有时候采集数据和开发方便需要驻现场。没过多久,公司便派我出去出差。出差所做的内容基本上是数据综合展示页面,没有涉及核心业务,只需要从底层获取数据展示就好了。出差还有个好处就是有补贴,每日固定,由于甲方不提供住宿,我们就2人一组一起住酒店,有时还会跟老板说常住进行讨价还价。算下来,一个月的出差补贴能和工资差不多。所以就算每个月经常加班,也都是可以接受。这个项目上干完2个月可能又到其他项目上去干,然后根据项目时长又考虑是租房还是住酒店,如此往复。那时感觉和同学比起来,赚得还算可以。


    不知不觉这样子已经过了两年,公司A涨的工资并不多,同学跳槽获得了更高的工资,换了公司去了其他区,我们一起租的房子也退了,各自重新去租了单间。一方面那时我一个月的工资算上出差补贴,也不算高了,也就是正常水平,另一方面长期到处出差没有归宿感,长期下去找不到对象也不是办法,此时我已经萌生了跳槽的念头。但是回过头来发现,这两年一直是在用.NET做一些展示的界面,说技术没有用到很深的技术,说业务也不是懂业务,这才发现出去面试的时候也不是很有竞争力。看看市场行情java比C#好点,我把这两年的项目包装成java,但面了几家都杳无音信。最后有一家外包发了offer,我这次的目的有两个,一是不出差,2是可以学到技术,显然外包不满足这两个条件,所以我放弃了。考虑到一个月存下的钱还能接受,我就妥协了,继续干着,这次在平常工作之余,学习java相关的技术。


    迷茫


    然而到了第三年,公司组织架构变动,部门缩减预算,我负责维护产品,有什么问题远程支持。也就是意味着出差的频次减少了,必要的情况下有时出去一个礼拜,赚得出差补贴也少了。这样算下来,相比两年工作经验的人,我赚得是少的,而且进来的新人都比我们老人多,同事问起来,说真的,我都不好意思跟他讲。这个时候我想跳槽的心思再一次涌起,但是快到了年底,也没有什么机会,打算过年后再看看。不过令人惊喜的是,这次年终奖给了不少,估计是考虑到我们工资比较低,工资不涨,年终奖来凑,也算是补了一点内心的不平衡。


    原先部门的人员还一直挺稳定,到了20年中,老人也走了几个,新人进来了几个,这时我已经算是比较老了,平常同事问起来,意思说第一家公司待了3年有点久,平常一年或两年就跳槽了,我也是表面上呵呵一下,内心也想着逃离。然后我开始看起了行情,发现行情又不好了,加上我java没有项目实践,只是学了点基础的技术,业务也不精通。是的,我放弃了,我再一次陷入迷茫,难道我就只能一直这样子待下去了吗。


    没过多久,公司组织架构再一次变动,部门业务也有调整,我们的技术从.NET过渡到java,这一次对于我来说是一件好事,意味着我可以用java做项目了。而且这次公司的业务是做新的产品,不再维护老的项目,做开发的都知道,一直维护老的项目学不到太多的技术,所以这对于我来说是一种解脱。在大公司的好处是,可以及时跟进技术,然后我就接触了微服务和docker相关的技术,k8s是其他同事在研究。


    不知不觉又干了两年,这两年算是学到了一些技术,工资目前还偏低,这个时候我也思考过,我以后的方向,是在大厂当凤尾,还是去小公司当鸡头。加上之前家里催着相亲,前前后后见了不少个都没有结果。有些是因为在老家,异地不太方便,也就算了。加上家里的条件不太好,这几年也没存到什么钱,在杭州买房有很大的压力。然后我就考虑回老家,老家的软件行情也不太好,毕竟是二线城市(现在已经是三线了),机会不是很多。刚好碰到校友群里有老乡在银行工作,发了招聘,然后我联系他,获得了面试机会。在这次面试中,我自认为技术上还是比较自信的,然而银行的流程比较复杂,有5轮面试,由总行的和分行的。然而又失败了,这次失败是因为用词表达不够准确,被分行的领导们否决了。但我不知道是不是有其他因素考虑在内,比如外貌、言行举止,需要体现银行人的面貌,不过都不重要了。


    回家


    到了2022年初,在某聘发现老家有公司(公司B)在招架构师,我就去试试看。业务刚起步,我心想如果能够能有发展的话,以后在技术部门还是有话语权的。公司B工资给的一般,但考虑到可以住在家里,生活成本会少很多,有什么事情家里沟通也很方便。于是我就进了这家公司。说真的公司A待了4年半,还有些不舍。公司还是原来的公司,但是部门调整,人员流动、调整、拆分,部门是否还是原来那个部门。


    在公司B的经历只能用两个字概括:闲着。情况是公司原先有老系统的经验,现在BOSS他们在进行业务架构,我们就做一些技术架构的学习,在我看来前期还会是项目级别的开发,不会一下子上升到K8S集群管理。一来是真正成熟的产品还需从项目开始落地,二来是管理成本会很大,人员有限。


    公司实习生来了几个,又走了几个。到现在持续了快两年,如果不是工资能够正常发并且人员还在招,我都怀疑公司是不是在正常运作。好在也是在回家的这两年,经过家里人介绍,我完成了另一件终身大事。


    总结


    不知不觉写了很多,有些细节还没写出来。这几年铺天盖地的技术,我们是学了又学,卷了又卷。当软件技术不能够有效提高生产力的时候,意味着软件开发的红利已经过去。想想以前,如果我毕业时考研读了人工智能的专业,现在会不会好很多;如果大学的时候努努力,进互联网的大厂,现在又会不会不一样。但是我不后悔,我把原因归结于没有规划,大学如此,工作的时候也是如此,等行情不好的时候或者是中年危机出现的时候,再去准备就已经迟了。一切的焦虑来源于没有充足的准备。


    要为最坏的打算做好准备,如果公司不行了,难道还回杭州去背井离乡吗。所以未来我打算发展一下副业,不需要依赖于公司也可以自己生计。


    作者:哥伦比亚拆迁队
    来源:juejin.cn/post/7337112282575323146
    收起阅读 »

    研发误删的库,凭什么要 DBA 承担责任

    镇楼图 三个角色 删库以及更宽泛的数据库变更场景中有三个角色,业务研发,DBA 以及使用的数据库变更工具: 业务研发通常指的是后端研发。国内最主流的技术栈还是 Java,此外 Go 也有一部分,另有全栈的则使用 Node。这些语言通常会配备对应的 ORM ...
    继续阅读 »

    镇楼图


    file


    三个角色


    删库以及更宽泛的数据库变更场景中有三个角色,业务研发,DBA 以及使用的数据库变更工具:



    • 业务研发通常指的是后端研发。国内最主流的技术栈还是 Java,此外 Go 也有一部分,另有全栈的则使用 Node。这些语言通常会配备对应的 ORM 和数据库打交道,Java 的 MyBatis,Go 的 GORM,Node 的 TypeORM 等。

    • DBA 就是数据库管理员。有些公司即使没有全职 DBA,也会有看着数据库的那个人。

    • 数据库变更工具。公司业务稍微上了规模,一般会选择在专门的数据库变更工具上执行操作,开源的产品里比较主流的有 Archery, Yearning, Bytebase。


    生命周期


    交代完出场角色,我们再来说一下,数据库变更的整个生命周期:



    • 研发在数据库变更工具上提交了一个变更工单。

    • 工具可能进行一些自动化检测,修改字段会提示锁表,删库,删表会警告破坏应用代码的兼容性。

    • DBA 进行审核。

    • 审核通过后,进行发布。

    • 告警铺天盖地/客诉蜂拥而来,业务一排查,怀疑可能是之前的数据库变更引起的,拉上 DBA,实锤。于是再一起制定补救方案。

    • 经过几天的奋战,最终修复了问题。

    • 开始进行复盘。


    上一期回顾的 Linear 删库故障就是这样一套流程。到了复盘阶段,国外普遍采用 blameless postmortem,对事不对人,但国内通常会给故障定主责和次责。安全生产的角度来说,定责任人的威慑力肯定更强。但对于数据库变更,往往一出就是大故障,承担主责往往意味着全年绩效最低档。所以每当业务研发和 DBA 还有他们的主管走进会议室,都会很认真地想着怎样一起复盘。


    场景模拟


    我们先来模拟一个场景:


    业务研发想在 MySQL 8.0 上把一个 VARCHAR 列的长度从 20 改成 100:


    ALTER TABLE person MODIFY name VARCHAR (100); 

    研发在数据库变更工具上提交了这个语句,因为 DBA 也读了 MySQL 8.0 官方文档,知道 MySQL 8.0 修改列不会锁表导致不可用,所以审批通过,然后 DBA 执行这条语句。业务居然挂了!原来 MySQL 8.0 里当把 VARCHAR 长度修改为超过 64 时,还是会锁表的!


    切回复盘室里认真讨论的气氛。业务团队主张 DBA 主责,业务研发次责,因为



    • 工具没有自动检测出这个风险,进行提示。

    • DBA 作为专业人员在审核阶段没有发现锁表问题。

    • 最后是由 DBA 去执行了变更操作。

    • 业务研发提交了导致问题的 SQL,但要求业务研发了解这个 MySQL 的执行细节有点强人所难。


    DBA 团队主张业务团队主责,DBA 次责,因为:



    • 业务研发应该为自己的业务负责,包括数据库在内。

    • 问题 SQL 是由业务发起的。业务研发并没有提示业务风险,比如业务的重要性,业务的高峰期等。

    • DBA 没有识别到这个问题,督查失职。


    好了,到了这里两边其实都有各自的道理,小伙伴们可以停下来思考一下,更倾向于如何定责。下面说一下我的观点。


    我认为这个情况应该是**「业务团队主责,DBA 次责」**。


    针对业务团队的几个主张:



    工具没有自动检测出这个风险,进行提示。



    那好,既然 DBA 引入带自动检测的工具反而留下把柄,那大家还是提交在文档上吧,完全没有自动化检测。



    DBA 作为专业人员在审核阶段没有发现锁表问题。



    双方这点都有共识,但光这条属于失查,次责。



    最后是由 DBA 去执行了变更操作。



    是 DBA 执行还是研发执行,这是流程的设计。谁去点执行按钮都改变不了这次故障的发生。



    业务研发提交了导致问题的 SQL,但要求业务研发了解这个 MySQL 的执行细节有点强人所难。



    没有错,但我们来换一个角度。研发写代码调用了一个 API,让 API 的维护者审核一下。结果 API 维护者也没有审核出来一个 bug,上线后导致故障。研发确实可以说让他了解 API 的实现细节有点难,但是研发为了完成自己的任务,使用了一个自己不熟悉的 API,那是不是应该由自己承担风险?


    但在实际 PK 中由 DBA 承担主责的情况时有发生,往往是因为第 3 点,最后是由 DBA 点了发布按钮。但就像上面说的,我认为这点是站不住脚的。流程设计让 DBA 点那下,是因为由 DBA 来点效率是更高的,但如果因为 DBA 点了而要背锅,那 DBA 就会拒绝这个流程,甩给研发自己去发布,这样流程只会更加低效。这里也要澄清一下,也不是 DBA 点就一定是更高效的流程,但只想表达定责应该和谁最后点发布那一下无关。


    场景泛化


    我们再进一步把这个场景泛化一下。在垂直业务团队和横向平台团队的协作中,因为业务开发造成的故障,通常应该都由业务团队承担主责,否则的话,就会造成权责不对等,把平台团队推向消极合作的情况。


    这个就像老师带着小朋友和家长们集体活动,照看小朋友的第一责任人始终是家长,如果要让老师承担主责,大概率就不会组织活动了。当然这也有例外,比如假设老师罔顾家长的嘱托,携带小朋友参加不适合的活动,造成意外,那就是老师的责任了。


    回到模拟的场景,业务研发应该做的,是给 DBA 更多的业务风险提示,比如说哪些高峰时段不能做变更。如果研发说了这些,但是 DBA 依然置若罔闻,发布造成事故,那 DBA 也自然难辞其咎。


    故障定责是一个激烈的话题,关乎到大家的奖金,加薪,升职。本文尝试给出一个判定的原则,也欢迎大家留言探讨。回忆起那些年参加过的复盘会议,就像抄起刚一同肝完的啤酒瓶,朝对方劈头盖脸扔了过去。我心中不禁泛起丝丝寒意,想到了鲁迅的呐喊:


    「 从来如此,便对么?」


    作者:Bytebase
    来源:juejin.cn/post/7332346676165574707
    收起阅读 »

    对于软件开发者来说什么证书含金量最高

    1. 引言 在快速变化的软件行业,持续学习和证书认证成为了许多开发者职业发展的关键。不仅因为这些证书能够提升个人技能,而且在求职过程中,这些证书也是向雇主展示专业技能和承诺的重要方式。 2. 市场上主流的软件开发相关证书 2.1 Oracle Certifie...
    继续阅读 »

    1. 引言


    在快速变化的软件行业,持续学习和证书认证成为了许多开发者职业发展的关键。不仅因为这些证书能够提升个人技能,而且在求职过程中,这些证书也是向雇主展示专业技能和承诺的重要方式。


    2. 市场上主流的软件开发相关证书


    2.1 Oracle Certified Professional, Java SE Programmer (OCPJP)



    • 相关技能:Java 编程、面向对象的设计原理、Java SE API。

    • 为何重要:Java 是世界上最受欢迎的编程语言之一,Oracle 的 Java 认证能够显著提升一个开发者在Java领域的专业性。


    ocpjp-8-certificate.jpg


    2.2 Microsoft Certified: Azure Solutions Architect Expert



    • 相关技能:云计算、Azure服务设计和管理、安全和合规性。

    • 为何重要:随着云计算的普及,熟练掌握微软Azure平台的专家需求量大增。


    2.3 AWS Certified Solutions Architect – Associate



    • 相关技能:云计算、AWS服务设计和管理、网络技术。

    • 为何重要:AWS 是市场上最大的云服务提供商,此认证证明了在AWS平台上设计高可用、成本效益高的系统的能力。


    2.4 Certified ScrumMaster (CSM)



    • 相关技能:敏捷和Scrum实践、团队合作、项目管理。

    • 为何重要:Scrum 是最流行的敏捷开发方法之一,CSM证书证明了在使用Scrum方法管理项目方面的专业知识。


    1625704928523.png


    2.5 PMP (Project Management Professional)



    • 相关技能:项目管理、领导和团队管理、风险管理。

    • 为何重要:对于希望在项目管理方面发展的软件开发人员,PMP是一个国际认可的高级证书。


    3. 选择正确的证书



    • 自我评估:首先,理解自己的职业目标和兴趣所在是非常重要的。例如,如果我们对云计算充满热情,那么AWS或Azure相关的证书可能更适合我们,在国内阿里云和腾讯云的认证也可以。

    • 行业需求:研究市场需求和趋势,选择对应的证书。例如,随着云计算和数据科学的兴起,相关的证书需求量也在增长。

    • 长期价值:考虑证书对我们长期职业发展的帮助。一些证书可能需要持续的教育和更新。


    4. Certified ScrumMaster(CSM)推荐


    在当今快速变化的软件开发领域,敏捷方法论已成为推动项目成功的关键因素。作为最受欢迎的敏捷实践之一,Scrum 方法论在全球范围内得到了广泛应用。Certified ScrumMaster(CSM)认证对软件开发者和项目管理者来说是一项宝贵的资格认证。


    CSM-Banner.png


    4.1. CSM 认证概述


    Certified ScrumMaster(CSM)是由 Scrum Alliance 提供的一项认证,旨在确认个人在理解和实践 Scrum 方法论方面的能力。CSM 认证不仅涉及 Scrum 的基本原则和实践,还包括团队合作、项目管理和敏捷思维。


    4.2. CSM 认证的重要性



    • 提升职业素养:CSM 认证可以显著提高个人在敏捷项目管理领域的专业知识和技能。

    • 增强市场竞争力:随着敏捷方法的普及,CSM 认证成为了求职者在简历中的一个亮点,增加了他们在市场上的竞争力。

    • 促进团队合作:CSM 认证的持有者能更有效地领导和促进敏捷团队的合作与沟通。

    • 优化项目管理:CSM 认证有助于改进项目的交付周期,提高项目成功率。


    4.3. 获取 CSM 认证的流程



    • 参加培训:首先需要参加为期两天的认证课程,由 Scrum Alliance 认证的培训师(CST)授课。

    • 通过考试:完成课程后,参加在线考试,考试内容涵盖 Scrum 的基本原则、框架和实践。

    • 维持认证:CSM 认证需要每两年续期一次,通过积累 Scrum 教育单位(SEUs)和缴纳续费来实现。


    4.4. 为何选择 CSM 认证



    • 适用范围广:无论是软件开发者、项目经理还是希望提高项目管理能力的专业人士,CSM 认证都是一个极佳的选择。

    • 实践导向:CSM 认证不仅仅是理论知识的学习,更重要的是它强调实践和实际应用。

    • 全球认可:CSM 是一项国际认可的认证,对于追求国际职业发展的专业人士来说具有重要意义。


    Certified ScrumMaster 认证是一项对软件开发者和项目管理者极具价值的资格认证。它不仅提供了敏捷方法论的深入理解,还有助于在职业生涯中获得更多的机会和挑战。无论我们是刚刚开始职业生涯,还是希望在当前领域有更深入的发展,CSM 认证都是一个值得考虑的优秀选择。


    5. 总结


    选择正确的证书对于软件开发者的职业发展至关重要。在选择时,应当考虑个人的职业规划、市场需求以及证书的长期价值。同时,持续学习和保持技能的最新性是每个软件开发者成功的关键。


    作者:王义杰
    来源:juejin.cn/post/7329573732597776410
    收起阅读 »

    将一个图片地址转成文件流(File)再上传

    web
    写在开头 最近,小编在业务中遇到一个图片转存的场景。 领导🤠:大概过程就是,接口会给我返回一个图片列表数据,图片路径是全路径,但是路径中的域名是其他系统的,必须要在用户选择图片的时候将图片重新转存到自个的系统上,防止其他系统删除图片对此有影响。 我😃:Em....
    继续阅读 »

    写在开头


    最近,小编在业务中遇到一个图片转存的场景。


    领导🤠:大概过程就是,接口会给我返回一个图片列表数据,图片路径是全路径,但是路径中的域名是其他系统的,必须要在用户选择图片的时候将图片重新转存到自个的系统上,防止其他系统删除图片对此有影响。


    14DBA96E.gif


    我😃:Em...很合理的需求。


    (但是,和有什么关系?我只是一个前端小菜鸡呀,不祥的预感.......)


    我😃:(卑微提问)这个过程不是放后端做比较合理一点?


    后端大哥😡:前端不能做?


    我😣:可以可以,只是...这个好像会跨域?


    后端大哥😠:已经配置了请求头('Access-Control-Allow-Origin': '*')。


    我😖:哦,好的,我去弄一下。(*******此处省略几万字心理活动内容)


    14F03F73.jpg

    第一种(推荐)


    那么,迫于......不,我自愿的,我们来看看前端要如何完成这个转成过程,代码比较简单,直接贴上来瞧瞧:


    async function imageToStorage(path) {
    // 获取文件名
    const startIndex = path.lastIndexOf('/');
    const endIndex = path.indexOf('?');
    const imgName = path.substring(startIndex + 1, endIndex);
    // 获取图片的文件流对象
    const file = await getImgToFile(path, imgName);
    // TODO: 将File对象上传到其他接口中
    }

    /**
    * @name 通过fetch请求文件,将文件转成文件流对象
    * @param { string } path 文件路径全路径
    * @param { string } fileName 文件名
    * @returns { File | undefined }
    */

    function getImgToFile(path, fileName) {
    const response = await fetch(path);
    if (response) {
    const blob = await response.blob();
    const file = new File([blob], fileName, { type: blob.type });
    return file;
    }
    }

    上述方式,在后端配置了允许跨域后,正常是没有什么问题的,也是比较好的一种方式了。😃


    但是,在小编实际第一次编码测试后,却还是遇上了跨域。😓


    image.png


    一猜应该就是后端实际还没配置好,问了一下。


    后端大哥😑:还没部署,一会再自己试试。


    我😤:嗯嗯。


    第二种


    等待的过程,小编又在网上找了找了,找到了第二种方式,各位看官可以瞧瞧:


    /** @name 将图片的网络链接转成base64 **/
    function imageUrlToBase64(imageUrl: string, fileName: string): Promise<File> {
    return new Promise(resolve => {
    const image = new Image();
    // 让Image元素启用cors来处理跨源请求
    image.setAttribute('crossOrigin', 'anonymous');
    image.src = imageUrl + '&v=' + Math.random();
    image.onload = () => {
    const canvas = document.createElement('canvas');
    canvas.width = image.width;
    canvas.height = image.height;
    const context = canvas.getContext('2d')!;
    context.drawImage(image, 0, 0, image.width, image.height);
    // canvas.toDataURL
    const imageBase64 = canvas.toDataURL('image/jpeg', 1); // 第二个参数是压缩质量
    // 将图片的base64转成文件流
    const file = base64ToFile(imageBase64, fileName);
    resolve(file);
    };
    });
    }
    /** @name 将图片的base64转成文件流 **/
    function base64ToFile(base64: string, fileName: string) {
    const baseArray = base64.split(',');
    // 获取类型与后缀名
    const mime = baseArray[0].match(/:(.*?);/)![1];
    const suffix = mime.split('/')[1];
    // 转换数据
    const bstr = atob(baseArray[1]);
    let n = bstr.length;
    const u8arr = new Uint8Array(n);
    while (n--) {
    u8arr[n] = bstr.charCodeAt(n);
    }
    // 生成文件流
    const file = new File([u8arr], `${fileName}.${suffix}`, {
    type: mime,
    });
    return file;
    }

    这第二种方式由于要先把图片绘制到 canvas 再去转成 base64 再去转成文件流,小编用 console.time 稍微测了一下,每次转化过程都要几百毫秒,图片越大时间越长,挺影响性能的。


    所以,小编还是推荐使用第一种方式,当然,最稳妥的方案是后端去搞最好了。😉



    网上很多都说第二种方式可以直接绕过跨域,各种谈论。😪


    主要就是这个 crossOrigin 属性。MDN解释


    它原理是通过了 CORS


    image.png


    或者可以再看看这个解释:传送门










    至此,本篇文章就写完啦,撒花撒花。


    image.png


    希望本文对你有所帮助,如有任何疑问,期待你的留言哦。

    老样子,点赞+评论=你会了,收藏=你精通了。


    作者:橙某人
    来源:juejin.cn/post/7336756027385872424
    收起阅读 »

    年终总结:工作三年,满腔热血已然消散

    毕业到现在也已经有三年多的工作经验了,最开始坚信努力就会有结果,到被社会毒打后开始迷茫,一直在思考生活和工作之间的关系和意义。目前是彻底的技术躺平,如今供需关系变化了,单纯的提升技术也不见得能找到一份好的工作,所以有点学不动技术了,感觉学习技术的价值不如以前大...
    继续阅读 »

    毕业到现在也已经有三年多的工作经验了,最开始坚信努力就会有结果,到被社会毒打后开始迷茫,一直在思考生活和工作之间的关系和意义。目前是彻底的技术躺平,如今供需关系变化了,单纯的提升技术也不见得能找到一份好的工作,所以有点学不动技术了,感觉学习技术的价值不如以前大了,还不如学点软技能,或者看看书拓宽视野也好。好在人没有躺平,一直在尝试寻找一个破局的契机。既然改变不了环境,那就先改变自己吧。



    说说以前


    第一份工作


    刚毕业的第一份工作工资很低而且996,花了很多钱去报名学习编程技术的课程,当时觉得只有技术上去了薪资才会随之上涨,果不其然,最开始先投资自己才能更快提高身价的方式很对,来年跳槽工资是第一份工作的2.X倍吧,一方面当时环境还没这么糟糕,另一方面HR问期望薪资的时候自己也比较敢开价,主要是当时学了不少技术课程,面试的时候也是一顿Vue源码输出,所以当时底气比较足敢报价,现在回想起来确实有点狮子大开口的感觉[笑哭]。


    第二份工作


    第二份工作虽然比第一份工作的薪资高了很多,不过好景不长,待遇上去了,伴随而来的是更多的挑战和更大的压力,我倒是不抵触压力和挑战,适当的压力会让人更快的成长,我抵触的是那家公司的“坐牢”文化,明面上写着5点半下班,暗地里大搞工时考核,工作日至少需要待够11.5个小时,工时不够的就会被谈话了,就算没啥事也得待在公司刷工时,后面11.5都没法满足了。


    休养


    人到底能有多坏?一直以为大家同是打工人,矛盾是一致对外的,直到碰到了上家公司领导无底线的压榨、转正临近人事威胁劝退的恶心事之后,才体验到真正的险恶和残酷。没了工作后,开始怀疑自我,变得迷茫,每天都要出去走走,不让自己闷在屋子里,或许是阅历不足、亦或是承受能力不够吧,能熬过去的都会有成长。


    休息了近半年才重新开始找工作,这半年时间基本上是边做个低代码开源,边外出散心的状态,见见老友,吃饭叙旧,也尝试了很多新奇的体验。


    新工作入职前去了趟南通,在大学室友那住几天,算起来我俩都是无业游民,他住的附近有一个很大的批发市场,海鲜还算便宜种类也多,每天都让他骑着小电驴载着我去,我俩连续吃了很多顿海鲜,吃饱后骑着小电驴去看山山水水、逛逛公园,晚上回来一起开黑。对比一线城市的物价,感觉那的东西还挺实惠,可能我们是工作日去逛的,车少人也少,没有大城市的拥挤和喧嚣,也没有匆忙的旅人,突然间感觉生活节奏变得很慢,原来很多事情都可以慢慢来。


    不过小城市的工作收入确实很低很多,没有收入来源的话,这种生活不会持续太久,活在当下尽情享受当前的每分每秒。


    回到现在


    现在这份工作


    当时休息完开始找工作的时候,知道环境不好,心里已经有一个预期了,不过这难找的程度还是超过了我的预期,找的时间越长人越焦虑,虽然焦虑并没有什么正向作用,反而让人睡不着吃不好,但很难控制得住不去焦虑,当时想着随便有个外包都去了,好在最终结果是好的,拿到了唯一一份offer,也就是现在这份工作,没有太多的考虑的余地就买机票去了。


    这份工作虽然降薪了,但是双休不加班,工作上也没什么太大的压力,下班后晚上就去跑步,很喜欢跑完大汗淋漓的感觉。


    不过最近公司好像是收益不太行了,少了很多人,收益不行的时候就开始抓考勤、算考核绩效,几个人的活压到一个人身上,见怪不怪了,一般公司开始走下坡路的时候就出现这种操作。总感觉码农这份工作抗风险能力还是很差的,要资源没资源,要人脉没人脉,多数劳动合同上还限制了不能干副业,风险来临前要早做打算才是。


    谈谈未来


    2023年完成的目标


    1、看完5本书(趁着地铁通勤时看的,对扩宽视野很有帮助)


    2、坚持跑步(每个工作周至少去跑一次)


    3、打卡背单词(一直想提升英语能力,不过感觉每天只是花点时间背单词的话,还是提升太慢了)


    4、附近城市旅游(珠海跨年、清远漂流、桂林竹筏、江门海岛沙滩等,基本都是周末去的,玩得也很开心,节假日的时候人多不想去挤)


    5、交朋友(总感觉这一年交到的朋友太少了,想玩个桌游都凑不够人)


    2024年我想要做什么


    1、雅思,目标定在5.5以上吧(我的英语一直很差很差,以前没怎么学,考试基本靠蒙,想着年后报个班提升一下效率,在英语上欠下的债总归要还的)


    2、看书(闲的时候多看几本书)


    3、旅游(旅游是一个享受世界的过程,我喜欢慢慢悠悠的,总想着没工作的时候去一趟新疆或者内蒙,估摸着能住一个月吧,就想静静的在一望无际的草原上躺这么几天)


    4、出去外面看看(护-照最近也办下来了,想体验更多的风土人情,见识更宽阔的世界)


    5、运动(养生和跑步不能停,目标累计到一千公里以上吧,23年已累计到八百公里了,剩下两百公里应该洒洒水)


    6、交朋友(交到更多的朋友,偶尔想去玩一些东西,总是没有合适的朋友一起)


    寻破局之路


    什么局


    总觉得程序员这个职业稳定性差,没什么安全感,要么是三十五岁槛,要么是身体扛不住,不乏有些大咖支招转管理岗,可现实就是管理坑位就这么些,绝多大数人是坐不上的,再者拼命去卷竞争管理岗,到时身体垮了,ICU住一住,抵不住可能会人财两空。到时候年龄大了失业了,找不到工作,即没人脉和资源,也不会别的专业技能,这种可预见性的危机可以提前做些准备,避免走到这一步。


    如何破


    感觉自己总是走不出眼前的一亩三分地,不停的在这小圈子里兜兜转转,世界规则的条条例例已经把人框在一个范围里了,人生应该是旷野而不是轨道,如果自己再给自己设限,那么这个框将更小。很多时候出于对未知的恐惧,没勇气踏出第一步。我给自己找了两个破土而生的方向:1、换个赛道;2、换个环境,若当下环境找不到破局的希望,那就朝外面看看,试着换个环境,让变数更大。


    两个方向的结果未定,好在变数够大,不至于一眼就看到头。


    作者:Daw
    来源:juejin.cn/post/7330521390510866495
    收起阅读 »

    热爱前端,也没能逃过七年之痒

    大家好,我是杨成功。 从参加工作到今年十月底,我做前端已经整整七年了。都说婚姻有七年之痒,我觉得工作也同样如此。所谓工作的“七年之痒”,即职业倦怠期。我觉得我倒没有倦怠,但感受不一样了。 以前我一直想前端可以干到退休,这是我的理想职业。现在我虽然还是一名前端工...
    继续阅读 »

    大家好,我是杨成功。


    从参加工作到今年十月底,我做前端已经整整七年了。都说婚姻有七年之痒,我觉得工作也同样如此。所谓工作的“七年之痒”,即职业倦怠期。我觉得我倒没有倦怠,但感受不一样了。


    以前我一直想前端可以干到退休,这是我的理想职业。现在我虽然还是一名前端工程师,但是工作内容已经离前端越来越远了。


    以前我觉得做一个骨灰级程序员、掌握各种牛逼的技术是毕生目标;现在我会想人生精彩多样,多尝试一些不一样的事情不也同样有趣?


    1-3 年:热爱、探索


    我参加工作很早,二十出头。那时候啥也不懂,但是精力旺盛啥也想学,经常写代码到凌晨 2 点也不觉得累。有一部分人选择前端是因为简单,我就是纯粹的喜欢前端。


    前端中有很多好玩的东西,比如各种动画、特效,我都非常感兴趣。在工作中常常因为研究出一种“高级”的写法、实现了某个“牛逼”的功能而沾沾自喜。虽然现在看起来很小儿科,但想起来真让人怀念。


    我的第一份工作工资很低(<3k),应该比 95% 的前端都低。当时没有经验,心里想着只要能学到东西就成。在那家公司干了一年多,公司用到的技术基本都学了一遍,进步飞快。“又穷又爱”的状态估计以后再也不会有了。


    3-5 年:积累、挑战


    工作三年多的时候,我换了家公司,带一个前端小团队,每天都扎在项目里。以前总是追求新技术,怎么花哨怎么来。可负责项目后才发现,解决问题和快速产出才是第一位。


    当时的前端非常火热,全社会都是跳槽的机会,跳槽等于涨薪。于是面试变得千奇百怪,大家在卷各种原理、源码、八股文,不管面不面试刷题成了必修课。很多开发者们非常讨厌这些东西,但是又不得不去做。


    当然也有好处,就是各种新技术层出不穷。虽然很多都是轮子,但确实有不少突破性的技术,帮助传统前端接触到更广的技术面,能做更多的事情。


    我没有花大量时间刷面试题,新技术倒是跟了不少,而且很多都用在了项目中。像 JS 原理题、算法题、某些框架的源码之类,我基本没怎么看过;但是像 Node.js、Android、Linux、跨端开发这些,我花了很多的时间研究,因为确实可以解决项目中的问题。


    我一直认为我属于“外卷”类型的:Title 一直是前端,但从不认为自己只是一个前端。什么技术都想试试。所以后来我承担过很多攻坚的角色,像服务器、原生 App、音视频等。我发现能让我上头的可能并不是前端,而是搞定一个难题的快感。


    得益于这种心态吧,五年内我积累了很多,但我认为收获最大的是习惯了面对挑战。


    5-7 年:瓶颈、迷茫


    工作五年以上,年龄直逼 30 岁,好像一瞬间就老了,可我总觉得自己还是个孩子。这个时候总会问自己:我的工作有什么意义?我要一直这样下去吗?我想要什么样的生活?


    我是在第 6 年的时候感受到了瓶颈。技术方面一直在进步,但对项目的帮助越来越小———项目进入了稳定期。稳定期意味着没有了涨薪的机会,工作重点逐渐从“怎么实现”变成了“怎么汇报”。以前写日报是“汇总成果”,现在变成了“显得有事可做”。


    可能任何一家产品成熟的公司都是这样吧,我不习惯,我还在适应阶段。


    从今年开始,我最大的迷茫是工作与生活如何平衡。我在北京这几年,大部分精力都扑在了工作上,家人离的很远,每年见个一两次,也没把谈女朋友当回事。想和家人朋友在一块,可工作又不能放弃。成年人说自己不做选择全都要,而我好像只能二选一。


    以前一门心思地想靠技术跳槽、进大厂,今年突然觉得没意思。看到很多人被裁员、加班、互卷,我突然想也许现在挺好的呢?双休不加班、领导也 Nice、没有绩效考核、办公室关系也简单。是不是以前自己太浮躁了,没有好好享受当下呢?


    所以,要不要继续写代码?还是回老家做别的事?工作上要不要再卷一点?努力攒钱还是趁年轻消费?要不要参加相亲考虑结婚?一连串的问题汹涌而来。


    有些问题能想明白,有些问题还是不明白,但更多的是想明白了也做不到。人的成长流失最快的是勇气,可能某天一件意料之外的事情,会让你一下子做出决定。


    写了一本书


    工作五年之后,我常常会思考一个问题:如果有一天不做程序员了,我还能干什么?


    程序员大概都不喜欢社交吧,或者不擅长社交。我特别羡慕大圣老师,他可以把自己的知识通过视频很生动的表达出来。但我就不行,我好像对镜头恐惧,尝试过好多次全身的不自在。


    录视频有难度,不过写文章还行。正好积累了很多知识经验,一边总结一边练笔,于是开始写掘金。后来又碰到个机会写书,我就觉得这个更好,可以把这么多年的经验总结浓缩到一本书里。或许可以帮助一些前端朋友快速进阶,或许还能赚点稿费。


    这本书名叫 《前端开发实战派》


    之后怎么走


    七年之前觉得我会写代码到 70 岁,直到写不动了为止。七年之后,我最喜欢的工作依然是程序员,但我不再执着于能不能干到 35 岁了。世界还有很多不一样的精彩,我不能把自己困在程序里。


    与那些大厂大佬们相比,我赚的不多,心气也不高。没有想过一定要留在大城市,也不觉得以后有了小孩,就一定要奔着“好的教育”和“名校”去卷,太累了。其实只要没有大城市和名校的执念,生活压力也不会那么大。


    这样来看,如果有一天我被裁了,其实也没什么可担心的。选择一个离家近的地方,没有大都市的物欲和诱惑,过一些简单轻松的生活,或许并不糟糕。只是身在大城市,面对万千繁华仿佛难以自拔,但你心里好像知道这不是你追求的,却又停不下来。


    我有一个预感,可能 30 岁后不再做程序员了,至少不会只埋头钻研技术。做前端这几年让我在各方面成长迅速,不过做久了也有弊端,比如表达能力、社交能力退化,不擅长处理人际关系,不直接接触商业,而这些往往是人生下半场,决定幸福和事业的关键。


    但我依然喜欢技术。无论做什么,技术都会是我自己的优势。


    我们大老板是技术出身,孩子都上小学了,还经常熬夜帮我们处理技术难题。有次聚会我问他,公司那么多事情要忙,怎么还有精力写代码呢?他说写代码就是我最放松的时候。我不由得一阵佩服,或许这就是技术人的魅力吧。


    但在 30 岁之前,我会继续站在技术一线,做一个什么都搞的前端人。


    作者:杨成功
    来源:juejin.cn/post/7295551745580793919
    收起阅读 »

    360周鸿祎为什么说大模型已成茶叶蛋?

    大模型炒了一年,为什么没有特别火的应用? 最近几天360创始人周鸿祎称,去年感觉大模型是原子弹,今年感觉是茶叶蛋。 什么意思?我想大概就是说大模型谁都能玩了,现在国内的大模型没有一千,也有几百个了,大模型没什么稀奇的了。但是另一方面也反映了大家都是为了大模型而...
    继续阅读 »

    大模型炒了一年,为什么没有特别火的应用?


    最近几天360创始人周鸿祎称,去年感觉大模型是原子弹,今年感觉是茶叶蛋。


    什么意思?我想大概就是说大模型谁都能玩了,现在国内的大模型没有一千,也有几百个了,大模型没什么稀奇的了。但是另一方面也反映了大家都是为了大模型而大模型,但是大模型没能解决什么实际问题,或者说解决的问题太小,有点让人失望了。



    邓宁-克鲁格效应


    我认为这种感觉是很正常的,也符合事物的一般发展规律,一个新事物出现的时候,大家都抱着很大的期望,期待它去解决各种各样的问题,但是毕竟是新东西,和整个世界的磨合、整合还不够,还需要各种去适配,所以新鲜劲儿过去之后,很多问题还是没解决,大家就感觉失望了。然后这个新事物还要默默的发展一段时间,才有机会重回梦想之巅。


    这种情况有一个名词:邓宁-克鲁格效应(Dunning-Kruger Effect),也简称达克效应(D-K Effect),可以用下边这条曲线来理解它。达克效应本来说的是人的认知过程,但也经常被用来表示事物的发展过程。



    AI大模型的下一步


    AI大模型下一步会怎么发展?我认为首先还是要紧盯OpenAI,作为大模型的引爆者和引领者,OpenAI的发展方向至关重要。


    去年底OpenAI推出了GPTs,也就是大模型的应用商店,为什么干这件事?我认为是因为AGI发展遇阻,技术和资金都有点跟不上,这一点可以从最近OpenAI投资AI芯片、大规模融资,以及OpenAI CEO奥特曼让大家耐心等待AGI等等事件中略窥一二。为了提振信心,探寻更多机会,OpenAI不得不搞出这个应用商店,借助外部的更多力量来促进AI的发展。


    另外预计OpenAI今年就会发布GPT-5,大模型的能力进一步增强。据预测,GPT-5将是一个原生的多模态大模型,不仅能处理文本和图像,还能处理音视频内容,GPT-5甚至将会具备自主的AI模型开发能力,这将使其能够生成各种多模态的AI模型,从而学习和完成新的任务,这将大大扩展GPT-5的应用能力,有力推动通用机器人的发展,给人很多的想象空间。


    GPT-5是更好吃的茶叶蛋,还是更厉害的氢弹?让我们拭目以待!



    大模型和世界的磨合


    另外上边我提到大模型需要和世界进行磨合,怎么磨合?


    我认为第一步就是将AI能力融入到企业的产品或者服务中去。我们现在可以看到很多工具都集成了AI大模型,比如钉钉魔法棒、WPS AI助手、Photoshop AI绘画功能等等,现在也有了一些AI商用产品,比如AI客服、AI培训、AI教育等等方面,还有很多看起来不起眼的AI写作、AI绘画、AI编程等等,他们都在慢慢的渗透到各行各业,这些已经在潜移默化的发生,慢慢的改变工作方式,提升效率。


    虽然还没看到可以持续爆火的应用,也许只是磨合的不够,是黎明前的黑暗。


    对于大家特别期待的AI原生应用,或许可以小小的期待下GPT-5。


    不过我认为不管是AI+应用还是AI原生应用,最重要的是要解决确定性的问题,解决可能产生的错误或不准确的预测结果,否则大家只能把它当做一个玩具,或者只用在某些比较小的场景,无法做到各行各业遍地开花,也就无法推动整个世界的变革与发展。




    以上就是本文的主要内容,欢迎留言一起讨论。


    作者:萤火架构
    来源:juejin.cn/post/7329782406540853286
    收起阅读 »

    NestJS 依赖注入DI与控制反转IOC

    web
    1. 前言 在《NestJS 快速入门》和《NestJS HTTP 请求与响应 》两篇文章中,主要使用了 NestJS 控制器处理简单的逻辑,对于复杂的业务逻辑,通常使用单独的 Service 服务进行处理,然后可以把 Service 作为依赖注入到 Cont...
    继续阅读 »

    1. 前言


    在《NestJS 快速入门》和《NestJS HTTP 请求与响应 》两篇文章中,主要使用了 NestJS 控制器处理简单的逻辑,对于复杂的业务逻辑,通常使用单独的 Service 服务进行处理,然后可以把 Service 作为依赖注入到 Controller 中。


    2. 概念


    2.1 依赖注入、控制反转、容器


    何为容器?容器是可以装一些资源的。举个例子,普通的容器,生活中的容器比如保温杯,只能用来存储东西,没有更多的功能。而程序中的容器则包括数组、集合Set、Map 等。


    而复杂的容器,生活中比如政府,政府管理我们的一生,生老病死和政府息息相关。


    AB7410FF-F52F-4C58-9171-DFB6303157DD.png


    程序中复杂的容器比如NestJS 容器,它能够管理 Controller、Service 等组件,负责创建组件的对象、存储 组件的对象,还要负责 调用 组件的方法让其工作,并在一定的情况下 销毁 组件。


    依赖注入(Dependency Injection)是实现控制反转的一种方式。控制反转又是什么呢?控制反转(Inversion of Control)是指从容器中获取资源的方式跟以往有所不同。


    2.2 为什么需要控制反转


    2.2.1 依赖关系复杂、依赖顺序约束


    后端系统中有多个对象:



    • Controller 对象: 处理 HTTP 请求,调用 Service,返回响应。

    • Service 对象: 实现业务逻辑。

    • Repository 对象: 实现对数据库的增删改查。


    此外,还包括数据库链接对象 DataSource、配置对象 Config 等等。这些对象之间存在复杂的关系:



    • Controller 依赖 Service 实现业务逻辑。

    • Service 依赖 Repository 进行数据库操作。

    • Repository 依赖 DataSource 建立连接,而 DataSource 则需要从 Config 对象获取用户名密码等信息。


    这导致对象的创建变得复杂,需要理清它们之间的依赖关系,确保正确的创建顺序。例如:


    const config = new Config({ username: 'xxx', password: 'xxx'});
    const dataSource = new DataSource(config);
    const repository = new Repository(dataSource);
    const service = new Service(repository);
    const controller = new Controller(service);

    这些对象需要一系列初始化步骤后才能使用。此外,像 config、dataSource、repository、service、controller 这些对象不需要每次都新建一个,可以保持单例。在应用初始化时,需要明确依赖关系,创建对象组合,并确保单例模式,这是后端系统常见的挑战。


    2.3.2 高层逻辑直接依赖低层逻辑,违反依赖倒置规范



    依赖倒置: 什么是依赖倒置原则(Dependency Inversion Principle)高层模块不应该依赖底层模块,二者都应该依赖抽象(例如接口)。  抽象不应该依赖细节,细节(具体实现)应该依赖抽象。 



    1.举一个工厂例子,初始化时有工人、车间、工厂。


    2FE7E55F-42A4-48FF-9A6B-8E987E386A7F.png


    1.工厂是容器,车间是消费者,依赖工人和工人的服务,工人是依赖,是生产者。


    // 工人
    class Worker {
      manualProduceScrew(){
        console.log('A screw is built')
      }
    }

    // 螺丝生产车间
    class ScrewWorkshop {
      private worker: Worker = new Worker()
     
      produce(){
        this.worker.manualProduceScrew() // 调用工人的方法
      }
    }

    // 工厂
    class Factory {
      start(){
        const screwWorkshop = new ScrewWorkshop()
        screwWorkshop.produce()
      }
    }

    const factory = new Factory()
    // 工厂开工啦!!!
    factory.start()



    2.现在要把工人制造改为机器制造,需要直接在车间把工人制造改为机器制造,麻烦。


    // 机器
    class Machine {
      autoProduceScrew(){
        console.log('A screw is built')
      }
    }

    class ScrewWorkshop {
      // 改为一个机器实例
      private machine: Machine = new Machine()
     
      produce(){
        this.machine.autoProduceScrew() // 调用机器的方法
      }
    }

    class Factory {
      start(){
        const screwWorkshop = new ScrewWorkshop()
        screwWorkshop.produce()
      }
    }

    const factory = new Factory()
    // 工厂开工啦!!!
    factory.start()


    3.此时考虑依赖倒置原则,通过实现生产者接口来处理(PS:这也是为什么像 Java 语言中,实现业务服务时需要定义接口类和实现类,遵循依赖倒置,方便切换不同的业务逻辑)


    // 定义一个生产者接口
    interface Producer {
      produceScrew: () => void
    }

    // 实现了接口的机器
    class Machine implements Producer {
      autoProduceScrew(){
        console.log('A screw is built')
      }
     
      produceScrew(){
        this.autoProduceScrew()
      }
    }

    // 实现了接口的工人
    class Worker implements Producer {
      manualProduceScrew(){
        console.log('A screw is built')
      }
     
      produceScrew(){
        this.manualProduceScrew()
      }
    }

    class ScrewWorkshop {
      // 依赖生产者接口,可以随意切换啦!!!
      // private producer: Producer = new Machine()
      private producer: Producer = new Worker()
     
      produce(){
        this.producer.produceScrew() // 工人和机器都提供了相同的接口
      }
    }

    class Factory {
      start(){
        const screwWorkshop = new ScrewWorkshop()
        screwWorkshop.produce()
      }
    }

    const factory = new Factory()
    // 工厂开工啦!!!
    factory.start()



    4.工厂改造后,螺丝生产车间的改造变得更容易了,只需要改变其属性中所新建的遵循Producer接口的实例即可。然而,这并没有完全改善我们与车间主任之间的关系,每次厂里在改造生产机器时我们还是需要麻烦车间主任。这是因为我们还是没有完全遵守依赖倒置原则,ScrewWorkshop 仍然依赖了 Worker/Machine的实例,只不过这种依赖相较之前少了一点罢了。


    要完全遵守依赖倒置原则,需要使用控制反转依赖注入


    2.3 控制反转思想


    2.3.1 获取资源的传统方式



    • 自己做饭:买菜、洗菜、择菜、切菜、炒菜,全过程参与,费时费力,必须了解资源创建整个过程中的全部细节并熟练掌握。

    • 在应用程序中,组件需要获取资源,传统的方式是主动从容器中获取所需资源,这样开发人员需要知道具体容器中特定资源的获取方式,增加了学习成本,也降低了开发效率。


    2.3.2 获取资源的控制反转方式



    • 点外卖:下单、等待、吃外卖,省时省力,不必关心资源创建过程的全部细节。

    • 控制反转的思想改变了应用程序组件获取资源的方式,容器会主动将资源推送给需要的组件,开发人员只需要提供接收资源的方式即可,这样可以降低学习成本,提高开发效率。这种方式被称为查找的被动方式。


    2.4 如何实现控制反转


    起源:许多应用程序的业务逻辑实现需要两个或多个类之间的协作,这种协作使得每个对象都需要获取与其合作的对象(即其所依赖的对象的引用)。如果这种获取过程由对象自身实现,那么将导致代码高度耦合,难以维护和调试。


    技术描述


    在 Class A 中,我们使用了 Class B 的对象 b。通常情况下,我们需要在 A 的代码中显式地使用 new 来创建 B 的对象。但是,如果采用依赖注入技术,A 的代码只需要定义一个 private 的 B 对象,而不需要直接 new 来获取这个对象。相反,我们可以通过相关的容器控制程序来在外部创建 B 对象,并将其注入到 A 类中的引用中。具体获取的方法以及对象被获取时的状态由配置文件(如 XML)来指定。这种方法可以使代码更加清晰和正式。


    loc 也可以理解为把流程的控制从应用程序转移到框架之中。以前,应用程序掌握整个处理流程;现在,控制权转移到了框架,框架利用一个引擎驱动整个流程的执行,框架会以相应的形式提供一系列的扩展点,应用程序则通过定义扩展的方式实现对流程某个环节的定制。“框架Call 应用”。基于 MVC 的 web 应用程序就是如此。


    实现方法


    实现控制反转主要有两种方式:依赖注入和依赖查找。两者的区别在于,前者是被动的接收对象,在类 A 的实例创建过程中即创建了依赖的 B对象,通过类型或名称来判断将不同的对象注入到不同的属性中,而后者是主动索取相应类型的对象,获得依赖对象的事件也可以在代码中自由控制。


    细说


    1.依赖注入:



    • 基于接口。实现特定接口以供外部容器注入所依赖类型的对象

    • 基于set方法。实现特定属性的publicSet方法,来让外部容器调用传入所依赖类型的对象

    • 基于构造函数。实现特定参数的构造函数,在新建对象时传入所依赖类型的对象

    • 基于注解。基于Java的注解功能,在私有变量前加“@Autowired”等注解,不需要显式的定义以上三种代码,便可以让外部容器传入对应的对象。该方案相当于定义了public的set方法,但是因为没有真正的set方法,从而不会为了实现依赖注入导致暴露了不该暴露的接口(因为set方法只想让容器访问来注入而并不希望其他依赖此类的对象访问)。


    2.依赖查找


    依赖查找更加主动,在需要的时候通过调用框架提供的方法来获取对象,获取时需要提供相关的配置文件路径、key等信息来确定获取对象的状态。


    2.4.1 工厂例子依赖注入改造


    通过以上学习,现在把工厂例子代码进一步改造,将底层类的依赖,由从中间类直接引用,变为高层类在构造时的依赖注入


    // ......Worker/Machine及其所遵循的接口Producer的实现与此前一致,此处省略

    class ScrewWorkshop
      private producer: Producer
     
      // 通过构造函数注入
      constructor(producer: Producer){
        this.producer = producer
      }
     
      produce(){
        this.producer.produceScrew()
      }
    }

    class Factory {
      start(){
        // 在Factory类中控制producer的实现,控制反转啦!!!
        // const producer: Producer = new Worker()
        const producer: Producer = new Machine()
        // 通过构造函数注入
        const screwWorkshop = new ScrewWorkshop(producer)
        screwWorkshop.produce()
      }
    }

    const factory = new Factory()
    // 工厂开工啦!!!
    factory.start()



    至此,回顾对这个车间的改造三步



    1. 依赖倒置: 解除ScrewWorkshop与Worker/Machine具体类之间的依赖关系,转为全部依赖Producer接口;

    2. 控制反转: 在Factory类中实例化ScrewWorkshop中需要使用的producer,ScrewWorkshop的对依赖项Worker/Machine的控制被反转了;

    3. 依赖注入: ScrewWorkshop中不关注具体producer实例的创建,而是通过构造函数constructor注入;


    3. NestJS 依赖注入


    在Nest的设计中遵守了控制反转的思想,使用依赖注入(包括构造函数注入、参数注入、Setter方法注入)解藕了Controller 与 Provider之间的依赖。


    我们将Nest中的元素与我们自己编写的工厂进行一个类比:



    1. Provider & Worker/Machine:真正提供具体功能实现的低层类。

    2. Controller & ScrewWorkshop:调用低层类来为用户提供服务的高层类。

    3. Nest框架本身 & Factory:控制反转容器,对高层类和低层类统一管理,控制相关类的新建与注入,解藕了类之间的依赖。


    IOC 机制是在 class 上标识哪些是可以被注入的,它的依赖是什么,然后从入口开始扫描这些对象和依赖,自动创建和组装对象。


    Nest 实现了 IOC 容器,会从入口模块开始扫描,分析 Module 之间的引用关系,对象之间的依赖关系,自动把 provider 注入到目标对象。


    Nest 里通过 @Controller 声明可以被注入的 controller,通过 @Injectable 声明可以被注入也可以注入别的对象的 provider,然后在 @Module 声明的模块里引入。并且Nest 还提供了 Module 和 Module 之间的 import,可以引入别的模块的 provider 来注入。


    provider 一般都是用 @Injectable 修饰的 class:


    2864AB0B-6A5F-4C29-AE1C-87AD610BA635.png


    在 Module 的 providers 里声明:


    27933B1A-306E-43E2-8203-598E1998B6B0.png


    上面是一种简写,完整的写法是这样的


    4CF33A34-F899-4AA0-9DA0-B93D177C9760.png


    构造函数或者属性注入


    E9561C4C-D02B-47DD-8B66-D0076666E2A8.png


    异步的注入对象


    24974844-A158-4FC1-8A14-33A1AF8033FB.png


    通常情况下,提供者通过使用 @Injectable 声明,然后在 @Moduleproviders 数组中注册类来实现。默认的 token 是类本身,因此不需要使用 @Inject 来指定注入的 token。


    但是,也可以使用字符串类型的 token,但在注入时需要单独指定 @Inject。除了可以使用 useClass 指定注入的类,还可以使用 useValue 直接指定注入的对象。如果想要动态生成对象,则可以使用 useFactory,它的参数也注入到 IOC 容器中的对象中,然后动态返回提供者的对象。如果想要为已有的 token 指定一个新的 token,可以使用 useExisting 来创建别名。通过灵活运用这些提供者类型,可以在 Nest 的 IOC 容器中注入任何对象。


    4.实践


    之前部门逻辑都是放在 controller 中的,现在可以把逻辑放 dept.service.ts  上来,添加 @Injectable 装饰器。


    9102DFFB-B32C-4ECE-8A5E-4D34135A6391.png


    在 DeptModule  模块中的 propviders 中引入 DeptService


    1397E621-39A4-4EF7-829A-8500C7B7B0B6.png


    最后在 dep.controller  使用部门服务,通过 @Inject() 装饰器注入。


    C63CF5D6-F08C-440C-9255-5688F35ADA41.png


    小结


    本文我们学习了什么是容器、依赖注入与控制反转,掌握了 Nest 依赖注入使用,并通过部门服务例子进行演示。


    参考资料



    作者:jecyu
    来源:juejin.cn/post/7336055070508843048
    收起阅读 »

    前端最全的5种换肤方案总结

    web
    最近一年很多客户要求更换主题色,而团队的项目基础框架不一,因此几乎使用了所有的更换主题的方案。以下总结了各方案的实现以及优缺点,希望能帮助有需要更换主题色的伙伴少踩坑。 方案一:硬编码 对一些老的前端基础库和业务库样式没有提取公共样式,当有更换主题的需求,实现...
    继续阅读 »

    最近一年很多客户要求更换主题色,而团队的项目基础框架不一,因此几乎使用了所有的更换主题的方案。以下总结了各方案的实现以及优缺点,希望能帮助有需要更换主题色的伙伴少踩坑。


    方案一:硬编码


    对一些老的前端基础库和业务库样式没有提取公共样式,当有更换主题的需求,实现的方法只能是全局样式替换,工作量比较大,需要更改form表单、按钮、表格、tab、容器等所有组件的各种状态,此外还需更换icon图标。


    以下是我们的一个老项目实现主题色更换,全局样式替换接近500行,如下图所示:


    image.png


    image.png


    image.png


    总结: 对于这种老项目只能通过硬编码的方式去更改,工作量较大,好在老项目依赖同一个基础库和业务库,所以在一个项目上实现了也可以快速推广到其它项目。


    方案二:sass变量配置


    团队的基础组件库Link-ui是基于Eelement-ui二次开发,因此可以采取类似于Element-ui的方式进行主题更改,只需要设计师提供6个主题色即可完成主题色的更改,如下所示。


    image.png



    • 配置基础色
      基础色一般需要设计师提供,也可以通过配置化的方式实现,


    $--color-primary-bold: #1846D1 !default;
    $--color-primary: #2664FD !default;
    $--color-primary-light: #4D85FD !default;
    $--color-primary-light-1: #9AC0FE !default;
    $--color-primary-light-2: #C1DBFF !default;
    $--color-primary-lighter: #E8F2FF !default;



    • 从基础库安装包引入基础色和库的样式源文件
      image.png


    @import "./common/base_var.scss";

    /* 改变 icon 字体路径变量,必需 */
    $--font-path: '~link-ui-web/lib/theme-chalk/fonts';

    @import "~link-ui-web/packages/theme-chalk/src/index";


    • 全局引入


    import '@/styles/link-variables.scss';


    • 更换主题色
      只需要更改上面的6个变量即可实现主题色的更改,比如想改成红色,代码如下


    $--color-primary-bold: #D11824 !default;
    $--color-primary: #FD268E !default;
    $--color-primary-light: #D44DFD !default;
    // $--color-primary-light-1: #9AC0FE !default;
    $--color-primary-light-2: #DCC1FF !default;
    $--color-primary-lighter: #F1E8FF !default;

    image.png


    总结: 对于基础库和样式架构设计合理的项目更改主题色非常的简单,只要在配置文件更换变量的值即可。它的缺点是sass变量的更改每次都需要编译,很难实现配置化。


    方案三、css变量+sass变量+data-theme


    代码结构如下:


    image.png



    • 设计三套主题分别定义不同的变量(包含颜色、图标和图片)


      // theme-default.scss
    /* 默认主题色-合作蓝色 */
    [data-theme=default] {
    --color-primary: #516BD9;
    --color-primary-bold: #3347B6;

    --color-primary-light: #6C85E1;
    --color-primary-light-1: #C7D6F7;
    --color-primary-light-2: #c2d6ff;
    --color-primary-lighter: #EFF4FE;

    --main-background: linear-gradient(90deg,#4e68d7, #768ff3);

    --user-info-content-background-image: url('../../assets/main/top-user-info-bg.png');
    --msg-tip-content-background-image: url('../../assets/main/top-user-info-bg.png');
    ...
    }


      // theme-orange.scss
    // 阳光黄
    [data-theme=orange] {
    --color-primary: #FF7335;
    --color-primary-bold: #fe9d2e;

    --color-primary-light: #FECB5D;
    --color-primary-light-1: #FFDE8B;
    --color-primary-light-2: #fcdaba;
    --color-primary-lighter: #FFF3E8;

    --main-background: linear-gradient(90deg,#ff7335 2%, #ffa148 100%);


    --user-info-content-background-image: url('../../assets/main/top-user-info-bg-1.png');
    --msg-tip-content-background-image: url('../../assets/main/top-user-info-bg-1.png');
    ...
    }


      // theme-red.scss
    /* 财富红 */
    [data-theme=red] {
    --color-primary: #DF291E;
    --color-primary-bold: #F84323;

    --color-primary-light: #FB8E71;
    --color-primary-light-1: #FCB198;
    --color-primary-light-2: #ffd1d1;
    --color-primary-lighter: #FFEEEE;


    --main-background: linear-gradient(90deg,#df291e 2%, #ff614c 100%);

    --user-info-content-background-image: url('../../assets/main/top-user-info-bg-2.png');
    --msg-tip-content-background-image: url('../../assets/main/top-user-info-bg-2.png');
    ...
    }



    • 把主题色的变量作为基础库的变量


    $--color-primary-bold: var(--color-primary-bold) !default;
    $--color-primary: var(--color-primary) !default;
    $--color-primary-light: var(--color-primary-light) !default;
    $--color-primary-light-1: var(--color-primary-light-1) !default;
    $--color-primary-light-2: var(--color-primary-light-2) !default;
    $--color-primary-lighter: var(--color-primary-lighter) !default;


    • App.vue指定默认主题色


    window.document.documentElement.setAttribute('data-theme', 'default')

    data-theme会注入到全局的变量上,所以我们可以在任何地方获取定义的css变量


    image.png


    实现效果如下:


    image.png


    image.png


    image.png


    总结: 该方案是最完美的方案,但是需对颜色、背景图、icon等做配置,需设计师设计多套方案,工作量相对较大,适合要求较高的项目或者标准产品上面,目前我们的标准产品选择的是该方案。


    方案四:滤镜filter


    filter CSS属性将模糊或颜色偏移等图形效果应用于元素。滤镜通常用于调整图像,背景和边框的渲染。


    它有个属性hue-rotate() 用于改变图整体色调,设定图像会被调整的色环角度值。值为0deg展示原图,大于360deg相当于又绕一圈。
    用法如下:


    body {
    filter: hue-rotate(45deg);
    }


    产品新建UI单元测试运行录制.gif


    总结: 成本几乎为0,实现简单。缺点是对于某些图片或者不想改的颜色需要特殊处理。


    方案五:特殊时期变灰



    • filter还有个属性 grayscale() 改变图像灰度,值在 0% 到 100% 之间,值为0%展示原图,值为100% 则完全转为灰度图像。


    body {
    filter: grayscale(1);
    }

    image.png


    总结: 成本小,可以将该功能做成配置项,比如配置它的生效开始时间和生效结束时间,便于运营维护也不用频繁发布代码。


    总结


    以上就是实现换肤的全部方案,我们团队在实际项目都有使用,比较好推荐的方案是方案一、方案三、方案五,对于要求不高的切换主题推荐方案四,它的技术零成本,对于标准产品推荐方案三。如有更好的方案欢迎评论区交流。


    作者:_无名_
    来源:juejin.cn/post/7329573754987462693
    收起阅读 »