什么是系统的鲁棒性?
嗨,你好啊,我是猿java
现实中,系统面临的异常情况和不确定性因素是不可避免的。例如,网络系统可能会遭受网络攻击、服务器宕机等问题;金融系统可能会受到市场波动、黑天鹅事件等因素的影响;自动驾驶系统可能会遇到天气恶劣、道路状况复杂等情况。
在这些情况下,系统的鲁棒性就显得尤为重要,它能够确保系统能够正确地处理各种异常情况,保持正常运行。因此,这篇文章我们将分析什么是系统的鲁棒性?如何保证系统的鲁棒性?
什么是系统的鲁棒性?
鲁棒性,英文为 Robustness,它是一个多学科的概念,涉及控制理论、计算机科学、工程学等领域。
在计算机领域,系统的鲁棒性是指系统在面对各种异常情况和不确定性因素时,仍能保持稳定运行和正常功能的能力。
鲁棒性是系统稳定性和可靠性的重要指标,一个具有良好鲁棒性的系统能够在遇到各种异常情况时做出正确的响应,不会因为某些异常情况而导致系统崩溃或失效。
鲁棒性要求系统在在遇到各种异常情况都能正常工作,各种异常很难具像化,这看起来是一种比较理想的情况,那么系统的鲁棒性该如何评估呢?
系统鲁棒性的评估
系统的鲁棒性可以从多个方面来考虑和评估,这里主要从三个方面进行评估:
首先,系统的设计和实现应该考虑到各种可能的异常情况,并采取相应的措施来应对
例如,在网络系统中,可以采用防火墙、入侵检测系统等技术来保护系统免受网络攻击;在金融系统中,可以采用风险管理技术来降低市场波动对系统的影响;在自动驾驶系统中,可以采用传感器融合、路径规划等技术来应对复杂的道路状况。
其次,系统在面临异常情况时应该具有自我修复和自我调整的能力
例如,当网络系统遭受攻击时,系统应该能够及时发现并隔离攻击源,同时自动恢复受影响的服务;当金融系统受到市场波动影响时,系统应该能够自动调整投资组合,降低风险;当自动驾驶系统面临复杂道路状况时,系统应该能够根据实时的道路情况调整行驶策略。
此外,系统的鲁棒性还包括对数据异常和不确定性的处理能力
在现实生活中,数据往往会存在各种异常情况,例如数据缺失、噪声数据等。系统应该能够对这些异常数据进行有效处理,保证系统的正常运行。同时,系统也应该能够对数据的不确定性进行有效处理,例如通过概率模型、蒙特卡洛方法等技术来处理数据不确定性,提高系统的鲁棒性。
鲁棒性的架构策略
对于系统的鲁棒性,有没有一些可以落地的策略?
如下图,展示了一些鲁棒性的常用策略,核心思想是:事前-事中-事后!

预防故障(事前)
对于技术人员来说,要有防范未然的意识,因此,对于系统故障要有预防措施,主要的策略包括:
- 代码质量:绝大部分软件系统是脱离不了代码,因此代码质量是预防故障很核心的一个前提。
- 脱离服务:脱离服务(Removal from service)这种策略指的是将系统元素临时置于脱机状态,以减轻潜在的系统故障。
- 替代:替代(Substitution)这种策略使用更安全的保护机制-通常是基于硬件的-用于被视为关键的软件设计特性。
- 事务:事务(Transactions)针对高可用性服务的系统利用事务语义来确保分布式元素之间交换的异步消息是原子的、一致的、隔离的和持久的。这四个属性被称为“ACID属性”。
- 预测模型:预测模型(Predictive model.)结合监控使用,用于监视系统进程的健康状态,以确保系统在其标称操作参数内运行,并在检测到预测未来故障的条件时采取纠正措施。
- 异常预防:异常预防(Exception prevention)这种策略指的是用于防止系统异常发生的技术。
- 中止:如果确定某个操作是不安全的,它将在造成损害之前被中止(Abort)。这种策略是确保系统安全失败的常见策略。
- 屏蔽:系统可以通过比较几个冗余的上游组件的结果,并在这些上游组件输出的一个或多个值不同时采用投票程序,来屏蔽(Masking)故障。
- 复盘:复盘是对事故的整体分析,发现问题的根本原因,查缺补漏,找到完善的方案。
检测故障(事中)
当故障发生时,在采取任何关于故障的行动之前,必须检测或预测故障的存在,故障检测策略主要包括:
- 监控:监控(Monitor)是用于监视系统的各个其他部分的健康状态的组件:处理器、进程、输入/输出、内存等等。
- **Ping/echo:**Ping/echo是指在节点之间交换的异步请求/响应消息对,用于确定通过相关网络路径的可达性和往返延迟。
- 心跳:心跳(Heartbeat)是一种故障检测机制,它在系统监视器和被监视进程之间进行周期性的消息交换。
- 时间戳:时间戳(Timestamp)这种策略用于检测事件序列的不正确性,主要用于分布式消息传递系统。
- 条件监测:条件检测(Condition monitoring.)这种策略涉及检查进程或设备中的条件或验证设计过程中所做的假设。
- 合理性检查:合理性检查(Sanity checking)这种策略检查特定操作或计算结果的有效性或合理性。
- 投票:投票(Voting)这种策略的最常见实现被称为三模块冗余(或TMR),它使用三个执行相同操作的组件,每个组件接收相同的输入并将其输出转发给投票逻辑,用于检测三个输出状态之间的任何不一致。
- 异常检测:异常检测(Exception detection)这种策略用于检测改变执行正常流程的系统状态。
- 自检测:自检测(Self-test)要求元素(通常是整个子系统)可以运行程序来测试自身的正确运行。自检测程序可以由元素自身启动,或者由系统监视器不时调用。
故障恢复(事后)
故障恢复是指系统出现故障之后如何恢复工作。这是对团队应急能力的一个极大考验,如何在系统故障之后,将故障时间缩小到最短,将事故损失缩减到最小?这往往决定了一个平台,一个公司的声誉,决定了很多技术人员的去留。故障恢复的策略主要包括:
- 冗余备用:冗余备用(Redundant spare)有三种主要表现形式:主动冗余(热备用)、被动冗余(温备用)和备用(冷备用)。
- 回滚:回滚(Rollback)允许系统在检测到故障时回到先前已知良好状态,称为“回滚线”——回滚时间。
- 异常处理:异常处理(Exception handling)要求在检测到异常之后,系统必须以某种方式处理它。
- 软件升级:软件升级(Software upgrade)的目标是在不影响服务的情况下实现可执行代码映像的在线升级。
- 重试:重试(Retry)策略假定导致故障的故障是暂时的,重试操作可能会取得成功。
- 忽略故障行为:当系统确定那些消息是虚假的时,忽略故障行为(Ignore faulty behavior)要求忽略来自特定来源的消息。
- 优雅降级:优雅降级(Graceful degradation)这种策略在元素故障的情况下保持最关键的系统功能,放弃较不重要的功能。
- 重新配置:使用重新配置(Reconfiguration),系统尝试通过将责任重新分配给仍在运行的资源来从系统元素的故障中恢复,同时尽可能保持关键功能。
上述这些策略看起来很高大上,好像离你很远,但是其实很多公司都有对应的措施,比如:系统监控,系统告警,数据备份,分布式,服务器集群,多活,降级策略,熔断机制,复盘等等,这些术语应该就和我们的日常开发息息相关了。
总结
系统的鲁棒性是指系统在面对各种异常情况和不确定性因素时,仍能保持稳定运行和正常功能的能力。系统鲁棒性看似一个理想的状态,却是业界一直追求的终极目标,比如,系统稳定性如何做到 5个9(99.999%),甚至是 6个9(99.9999%),这就要求技术人员时刻保持工匠精神、在自己的本职工作上多走一步,只有在各个相关岗位的共同协作下,才能确保系统的鲁棒性。
学习交流
如果你觉得文章有帮助,请帮忙点个赞呗,关注公众号:猿java,持续输出硬核文章。
来源:juejin.cn/post/7393312386571370536
uniapp适配android、ios的引导页、首页布局
uniapp适配Android、Ios的引导页和首页布局
真是很久没来掘金写文章了,最近一直在学习Nest和Next这些后端知识,忙只是一方面,更多的还是懒吧。其实今年来北京工作之后,完全独立挑大梁来写app收获还是蛮多的,但是我一般忙完就完事,着急学习自己的东西去,没有把工作中遇到的一些问题及时总结。这点感觉很不好,以后尽量把工作中遇到的有价值的问题总结下来,也算是给自己这段时间工作的复习,也能锻炼自己的表达能力。
引导页
原型图和需求

需求大致是这样:一共有三页,每页有2-3组图片,产品想要炫酷的视觉效果
我接收到需求后,首先想的是gif图,于是让UI帮我做了一张12帧的gif,大家来感受一下效果

不知道大家感受怎么样,放到手机来模拟的时候有些模糊、有些卡顿,且占用空间很大,一张12帧的图片已经20M+,
整个应用不过才30M的情况下,绝对接受不了这种情况,于是我就放弃的gif,想要用代码来实现。
思路
留给我的开发时间并不多,只有半天,自己本身css能力一般,按照gif这样估计最多做出来一页,所以我和产品决定阉割掉一部分动效,做三页。
- UI负责把每条图片列表切图给我
- 引导页用
swiper实现,这样页面切换动画可以省时间 - 第一页水平做动画两两一组,交替实现动画
- 第二页垂直做动画,交替实现
- 第三页原图和AI图在一个父盒子下,原图动态改变宽度来实现交替播放
- 每页文字和按钮通过
position:fixed置底 - 最后一页手动加上滑动事件,可以不点击按钮进入首页
代码实现
- template布局
<view class="swiperLayout">
<swiper
:current="current"
class="swiper"
duration="350"
@change="change"
:indicator-active-color=" '#FFF272' "
:indicator-color="'#ccc'"
indicator-dots="true"
>
<swiper-item class="swiperItem">
<view class="itemLayout">
<image
class="img an1"
src="@/static/guide/guide1_1.png"
mode="scaleToFill"
/>
<image
class="img an2"
src="@/static/guide/guide1_2.png"
mode="scaleToFill"
/>
<image
class="img an1"
src="@/static/guide/guide1_3.png"
mode="scaleToFill"
/>
<image
class="img an2"
src="@/static/guide/guide1_4.png"
mode="scaleToFill"
/>
<view class="buttonBox">
<view class="title">海量模板</view>
<view class="button" @click="next(1)">下一步</view>
</view>
</view>
</swiper-item>
<swiper-item class="swiperItem">
<view class="itemLayout">
<view class="guide2Box">
<image
class="img2 an3"
src="@/static/guide/guide2_1.png"
mode="scaleToFill"
/>
<image
class="img2 an4"
src="@/static/guide/guide2_2.png"
mode="scaleToFill"
/>
<image
class="img2 an3"
src="@/static/guide/guide2_3.png"
mode="scaleToFill"
/>
</view>
<view class="buttonBox">
<view class="title">5000+云端照片存储</view>
<view class="button" @click="next(2)">下一步</view>
</view>
</view>
</swiper-item>
<swiper-item
class="swiperItem"
@touchstart="handlerStart($event)"
@touchmove="handerMove($event)"
>
<view class="itemLayout">
<view class="guide3">
<-- img3动态改变自己的宽度,来实现动画效果 -->
<image
class="img3 an5 z"
src="@/static/guide/guide3_1.png"
mode="aspectFill"
/>
<image
class="img4 "
src="@/static/guide/guide3_2.png"
mode="heightFix"
/>
</view>
<view class="buttonBox">
<view class="title">高清照片,无水印无广告</view>
<view class="button" @click="toIndex">继续</view>
</view>
</view>
</swiper-item>
</swiper>
</view>
- css部分
.swiper {
width: 100vw;
height: 100vh;
background: #000;
.swiperItem {
.itemLayout {
display: flex;
flex-direction: column;
align-items: center;
padding-top: 60rpx;
.img {
width: 220vw;
height: 35vw;
margin: 20rpx 0 0rpx 0;
}
.img2 {
width: 30vw;
height: 256vw;
}
.title {
color: $themeColor;
margin-top: 40rpx;
text-align: center;
font-size: 36rpx;
font-weight: 600;
margin-bottom: 40rpx;
}
.button {
background: $themeColor;
color: #000;
height: 88rpx;
line-height: 88rpx;
width: 88%;
text-align: center;
border-radius: 48rpx;
font-size: 32rpx;
font-weight: 600;
}
}
.guide2Box{
display: flex;
justify-content: space-evenly;
width: 100%;
overflow: hidden;
height: 70vh;
}
}
}
// 动画1 执行三秒 匀速 无限次 镜像执行
.an1 {
animation: guide1 3s linear infinite alternate-reverse ;
}
// 水平X轴正向
@keyframes guide1 {
from {
transform: translateX(0);
}
50% {
transform: translateX(200rpx);
}
to {
transform: translateX(400rpx);
}
}
.an2 {
animation: guide2 3s linear infinite alternate-reverse ;
}
// 水平X轴负向
@keyframes guide2 {
from {
transform: translateX(0);
}
50% {
transform: translateX(-200rpx);
}
to {
transform: translateX(-400rpx);
}
}
.an3 {
animation: guide3 3s linear infinite alternate-reverse ;
}
// 水平正向 但是起始点要给负数 不然会有空缺的部分
@keyframes guide3 {
from {
transform: translateY(-500rpx);
}
50% {
transform: translateY(-250rpx);
}
to {
transform: translateY(0rpx);
}
}
.an4 {
animation: guide4 3s linear infinite alternate-reverse ;
}
// 水平负向
@keyframes guide4 {
from {
transform: translateY(0);
}
50% {
transform: translateY(-250rpx);
}
to {
transform: translateY(-500rpx);
}
}
.buttonBox{
position: fixed;
bottom: 120rpx;
width: 80vw;
display: flex;
flex-direction: column;
align-items: center;
z-index: 999;
}
// 最后一页动画 父盒子开启相对定位
.guide3{
position: relative;
width: 100%;
height: 100%;
// 两张图片都开始绝对定位 一左一右分布
.img3{
position: absolute;
top: 0;
left: 0;
height: 147vw;
border-right: 12rpx solid #fff;
}
.img4{
position: absolute;
top: 0;
right: 0;
height: 147vw;
}
}
// img3 缩小自己的宽度来实现动画
.an5 {
animation: changeImg 2s linear infinite alternate-reverse;
}
@keyframes changeImg {
from {
width: 0%;
}
to {
width: 100%;
}
}
.z{
z-index: 99;
}
- js部分
data() {
return {
current: 0,
// 触摸事件用到的数据
touchInfo: {
touchX: "",
touchY: "",
},
};
},
methods: {
next(num) {
this.current = num;
},
change(e) {
this.current = e.detail.current;
},
toIndex() {
uni.switchTab({ url: "/pages/index/index" });
},
handlerStart(e) {
let { clientX, clientY } = e.changedTouches[0];
this.touchInfo.touchX = clientX;
this.touchInfo.touchY = clientY;
},
handerMove(e) {
let { clientX, clientY } = e.changedTouches[0];
let diffX = clientX - this.touchInfo.touchX,
diffY = clientY - this.touchInfo.touchY,
absDiffX = Math.abs(diffX),
absDiffY = Math.abs(diffY),
type = "";
if (absDiffX > 50 && absDiffX > absDiffY) {
type = diffX >= 0 ? "right" : "left";
}
if (absDiffY > 50 && absDiffX < absDiffY) {
type = diffY < 0 ? "up" : "down";
}
if(type === 'left'){
this.toIndex()
}
},
},
最终效果

首页布局
原型图和需求
- 画风

- 贴纸

- 换脸

上面三图均为UI设计。首页的模板接口截止到目前(7.22)一共三种类型:styler(画风)、sticker(贴纸)、face_swap(换脸),本来按照UI的设计来看,每个分类的样式应该是固定写死的,我只需要v-for去不同的组件就可以,正当我写了一半时,很快老板的需求又下来:每个分类可能会杂糅在一起。说白了就是某个分类里可能既有画风、又有换脸、又有贴纸
思路
- 分析需求
在一个父组件中渲染所有的数据,根据不同的type 进入不同的子组件,三个子组件分别对应画风、贴纸、换脸,其中贴纸数据中有一个
mode字段,根据mode展示轮播、九宫格、一大八小的布局,这其中一大八小最不好实现。
一大八小的布局
- 将数据中的九张模板图片进行分组(剔除第一张,因为第一张要做“一大”),分为两组布局是上下分布(
display:flex)实现,同时将第一张和分组的view盒子的父元素也要开启display:flex - 编译到chrome调试 看html结构

- 代码
<scroll-view class="scroll_view" scroll-x="true">
<image
class="img"
:src="sceneItem.json_content.cover_image_list[0].path"
mode="scaleToFill"
/>
<view>
<view
class="Item_2"
v-for="(Item, index) in columnData"
:key="index"
>
<view v-for="item in Item" :key="item.id">
<image
class="ss"
:src="item.path"
mode="scaleToFill"
/>
</view>
</view>
</view>
</scroll-view>
...
computed:{
columnData() {
if (this.sceneItem.json_content.display_mode === "2") {
const setData = this.sceneItem.json_content.cover_image_list.filter(
(item, index) => index > 0
);
const resultArray = setData.reduce(
(acc, cur, index) => {
const targetIndex = index % 2;
acc[targetIndex].push(cur);
return acc;
},
Array.from(Array(2), () => [])
);
return resultArray;
}
},
}
...
::v-deep .uni-scroll-view-content {
display: flex;
}
.scroll_view {
white-space: nowrap;
.img {
min-width: 324rpx;
height: 324rpx;
border-radius: 24rpx;
margin-right: 24rpx;
}
.Item {
display: inline-block;
.img {
width: 324rpx;
height: 324rpx;
border-radius: 24rpx;
margin-right: 24rpx;
}
}
.Item_2 {
display: flex;
.ss {
width: 158rpx;
height: 158rpx;
margin-right: 12rpx;
border-radius: 16rpx;
}
}
}
实现效果

来源:juejin.cn/post/7394005582774960182
身份认证的尽头竟然是无密码 ?
概述
几乎所有的系统都会面临安全认证相关的问题,但是安全相关的问题是一个很麻烦的事情。因为它不产生直接的业务价值,而且处理起来复杂繁琐,所以很多时都容易被忽视。很多后期造成重大的安全隐患,往往都是前期的不重视造成的。但庆幸的是安全问题是普遍存在的,而且大家面临的问题几乎相同,所以可以制定行业标准来规范处理,甚至是可以抽出专门的基础设施(例如:AD、LDAP 等)来专门解决这类共性的问题。总之,关于安全问题非常复杂而且麻烦,对于大多数 99% 的系统来说,不要想着在安全问题领域上搞发明和创新,容易踩坑。而且行业的标准解决方案已经非常成熟了。经过长时间的检验。所以在安全领域,踏踏实实的遵循规范和标准就是最好的安全设计。
HTTP 认证
HTTP 认证协议的最初是在 HTTP/1.1标准中定义的,后续由 IETF 在 RFC 7235 中进行完善。HTTP 协议的主要涉及两种的认证机制。

基本认证
常见的叫法是 HTTP Basic,是一种对于安全性不高,以演示为目的的简单的认证机制(例如你家路由器的登录界面),客户端用户名和密码进行 Base64 编码(注意是编码,不是加密)后,放入 HTTP 请求的头中。服务器在接收到请求后,解码这个字段来验证用户的身份。示例:
GET /some-protected-resource HTTP/1.1
Host: example.com
Authorization: Basic dXNlcjpwYXNzd29yZA==
虽然这种方式简单,但并不安全,因为 base64 编码很容易被解码。建议仅在 HTTPS 协议下使用,以确保安全性。
摘要认证
主要是为了解决 HTTP Basic 的安全问题,但是相对也更复杂一些,摘要认证使用 MD5 哈希函数对用户的密码进行加密,并结合一些盐值(可选)生成一个摘要值,然后将这个值放入请求头中。即使在传输过程中被截获,攻击者也无法直接从摘要中还原出用户的密码。示例:
GET /dir/index.html HTTP/1.1
Host: example.com
Authorization: Digest username="user", realm="example.com", nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", uri="/dir/index.html", qop=auth, nc=00000001, cnonce="0a4f113b", response="6629fae49393a05397450978507c4ef1", opaque="5ccc069c403ebaf9f0171e9517f40e41"
**补充:**另在 RFC 7235 规范中还定义当用户没有认证访问服务资源时应返回 401 Unauthorized 状态码,示例:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Basic realm="Restricted Area"
这一规范目前应用在所有的身份认证流程中,并且沿用至今。
Web 认证
表单认证
虽然 HTTP 有标准的认证协议,但目前实际场景中大多应用都还是基于表单认证实现,具体步骤是:
- 前端通过表单收集用户的账号和密码
- 通过协商的方式发送服务端进行验证的方式。
常见的表单认证页面通常如下:
html>
<html>
<head>
<title>Login Pagetitle>
head>
<body>
<h2>Login Formh2>
<form action="/perform_login" method="post">
<div class="container">
<label for="username"><b>Usernameb>label>
<input type="text" placeholder="Enter Username" name="username" required>
<label for="password"><b>Passwordb>label>
<input type="password" placeholder="Enter Password" name="password" required>
<button type="submit">Loginbutton>
div>
form>
body>
html>
为什么表单认证会成为主流 ?主要有以下几点原因:
- 界面美化:开发者可以创建定制化的登录界面,可以与应用的整体设计风格保持一致。而 HTTP 认证通常会弹出一个很丑的模态对话框让用户输入凭证。
- 灵活性:可以在表单里面自定义更多的逻辑和流程,比如多因素认证、密码重置、记住我功能等。这些功能对于提高应用的安全性和便利性非常重要。
- 安全性:表单认证可以更容易地结合现代的安全实践,背后也有 OAuth 2 、Spring Security 等框架的主持。
表单认证传输内容和格式基本都是自定义本没啥规范可言。但是在 2019 年之后 web 认证开始发布标准的认证协议。
WebAuthn
WebAuthn 是一种彻底抛弃传统密码的认证,完全基于生物识别技术和实体密钥作为身份识别的凭证(有兴趣的小伙伴可以在 github 开启 Webauhtn 的 2FA 认证体验一下)。在 2019 年 3 月,W3C 正式发布了 WebAuthn 的第一版规范。

相比于传统的密码,WebAuthn 具有以下优势:
- 减少密码泄露:传统的用户名和密码登录容易受到钓鱼攻击和数据泄露的影响。WebAuthn,不依赖于密码,不存在密码丢失风险。
- 提高用户体验:用户不需要记住复杂的密码,通过使用生物识别等方式可以更快捷、更方便地登录。
- 多因素认证:WebAuthn 可以作为多因素认证过程中的一部分,进一步增强安全性。使用生物识别加上硬件密钥的方式进行认证,比短信验证码更安全。
总的来说,WebAuthn 是未来的身份认证方式,通过提供一个更安全、更方便的认证方式,目的是替代传统的基于密码的登录方法,从而解决了网络安全中的一些长期问题。WebAuthn 目前已经得到流程的浏览器厂商(Chrome、Firefox、Edge、Safari)、操作系统(WIndows、macOS、Linux)的广泛支持。
实现效果
当你的应用接入 WebAuthn 后,用户便可以通过生物识别设备进行认证,效果如下:

实现原理
WebAuthn 实现较为复杂,这里不做详细描述,具体可参看权威的官方文档,大概交互过程可以参考以下时序图:

登录流程大致可以分为以下步骤:
- 用户访问登录页面,填入用户名后即可点击登录按钮。
- 服务器返回随机字符串 Challenge、用户 UserID。
- 浏览器将 Challenge 和 UserID 转发给验证器。
- 验证器提示用户进行认证操作。
- 服务端接收到浏览器转发来的被私钥加密的 Challenge,以此前注册时存储的公钥进行解密,如果解密成功则宣告登录成功。
WebAuthn 采用非对称加密的公钥、私钥替代传统的密码,这是非常理想的认证方案,私钥是保密的,只有验证器需要知道它,连用户本人都不需要知道,也就没有人为泄漏的可能;
备注:你可以通过访问 webauthn.me 了解到更多消息的信息
文章不适合加入过多的演示代码,想要手上体验的可以参考 okta 官方给出基于 Java 17 和 Maven 构建的 webauthn 示例程序,如下:
来源:juejin.cn/post/7354632375446061083
如何优雅的给SpringBoot部署的jar包瘦身?
一、需求背景
我们知道Spring Boot项目,是可以通过java -jar 包名 启动的。
那为什么Spring Boot项目可以通过上述命令启动,而其它普通的项目却不可以呢?
原因在于我们在通过以下命令打包时
mvn clean package
一般的maven项目的打包命令,不会把依赖的jar包也打包进去的,所以这样打出的包一般都很小

但Spring Boot项目的pom.xml文件中一般都会带有spring-boot-maven-plugin插件。
该插件的作用就是会将依赖的jar包全部打包进去。该文件包含了所有的依赖和资源文件。
也就会导致打出来的包比较大。

打完包就可以通过java -jar 包名 启动,确实是方便了。
但当一个系统上线运行后,肯定会有需求迭代和Bug修复,那也就免不了进行重新打包部署。
我们可以想象一种场景,线上有一个紧急致命Bug,你也很快定位到了问题,就改一行代码的事情,当提交代码并完成构建打包并交付给运维。
因为打包的jar很大,一直处于上传中.......
如果你是老板肯定会发火,就改了一行代码却上传几百MB的文件,难道没有办法优化一下吗?
如今迭代发布是常有的事情,每次都上传一个如此庞大的文件,会浪费很多时间。
下面就以一个小项目为例,来演示如何瘦身。
二、瘦身原理
这里有一个最基础 SpringBoot 项目,整个项目代码就一个SpringBoot启动类,单是打包出来的jar就有20多M;
我们通过解压命令,看下jar的组成部分。
tar -zxvf spring-boot-maven-slim-1.0.0.jar

我们可以看出,解压出来的包有三个模块
分为 BOOT-INF,META-INF,org 三个部分
打开 BOOT-INF

classes: 当前项目编译好的代码是放在 classes 里面的,classes 部分是非常小的。
lib: 我们所依赖的 jar 包都是放在 lib 文件夹下,lib部分会很大。
看了这个结构我们该如何去瘦身呢?
项目虽然依赖会很多,但是当版本迭代稳定之后,依赖基本就不会再变动了。
如果可以把这些不变的依赖提前都放到服务器上,打包的时候忽略这些依赖,那么打出来的Jar包就会小很多,直接提升发版效率。
当然这样做你肯定有疑问?
既然打包的时候忽略这些依赖,那通过java -jar 包名 还可以启动吗?
这种方式打的包,在项目启动时,需要通过-Dloader.path指定lib的路径,就可以正常启动
java -Dloader.path=./lib -jar xxx.jar
三、瘦身实例演示
1、依赖拆分配置
只需要在项目pom.xml文件中添加下面的配置:
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<executable>true</executable>
<layout>ZIP</layout>
<!--这里是填写需要包含进去的jar,
必须项目中的某些模块,会经常变动,那么就应该将其坐标写进来
如果没有则nothing ,表示不打包依赖 -->
<includes>
<include>
<groupId>nothing</groupId>
<artifactId>nothing</artifactId>
</include>
</includes>
</configuration>
</plugin>
<!--拷贝依赖到jar外面的lib目录-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>copy</id>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<!--指定的依赖路径-->
<outputDirectory>
${project.build.directory}/lib
</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
再次打包
mvn clean package

发现target目录中多了个lib文件夹,里面保存了所有的依赖jar。

自己业务相关的jar也只有小小的168kb,相比之前20.2M,足足小了100多倍;
这种方式打的包,在项目启动时,需要通过-Dloader.path指定lib的路径:
java -Dloader.path=./lib -jar spring-boot-maven-slim-1.0.0.jar

虽然这样打包,三方依赖的大小并没有任何的改变,但有个很大的不同就是我们自己的业务包和依赖包分开了;
在不改变依赖的情况下,也就只需要第一次上传lib目录到服务器,后续业务的调整、bug修复,在没调整依赖的情况下,就只需要上传更新小小的业务包即可;
2、自己其它项目的依赖如何处理?
我们在做项目开发时,除了会引用第三方依赖,也会依赖自己公司的其它模块。
比如

这种依赖自己其它项目的工程,也是会经常变动的,所以不宜打到外部的lib,不然就会需要经常上传更新。
那怎么做了?
其实也很简单 只需在上面的插件把你需要打进jar的填写进去就可以了
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<executable>true</executable>
<layout>ZIP</layout>
<!--这里是填写需要包含进去的jar,如果没有则nothing -->
<includes>
<include>
<groupId>com.jincou</groupId>
<artifactId>xiaoxiao-util</artifactId>
</include>
</includes>
</configuration>
</plugin>
这样只有include中所有添加依赖依然会打进当前业务包中。
四、总结
使用瘦身部署,你的业务包确实小了 方便每次的迭代更新,不用每次都上传一个很大的 jar 包,从而节省部署时间。
但这种方式也有一个弊端就是增加了Jar包的管理成本,多人协调开发,构建的时候,还需要专门去关注是否有人更新依赖。
来源:juejin.cn/post/7260772691501301817
UNIAPP开发电视app教程
目前开发安卓TV的方法相对开说是十分的少的,使用uniapp开发相对来说几乎是没有的,因此,写下这篇文章供大家参考。
开发难点
- 如何方便的开发调试
- 如何使需要被聚焦的元素获取聚焦状态
- 如何使被聚焦的元素滚动到视图中心位置
- 如何在切换路由时,缓存聚焦的状态
- 如何启用wgt和apk两种方式的升级
一、如何方便的开发调试
之前我在论坛看到人家说,没办法呀,电脑搬到电视,然后调试。
其实大可不必,安装android studio里边创建一个模拟器就可以了。
注意:最好安装和电视系统相同的版本号,我这里是长虹电视,安卓9所以使用安卓9的sdk
二、如何使需要被聚焦的元素获取聚焦状态
uniapp的本质上是webview, 因此我们可以在它的元素上添加tabIndex, 就可以获取焦点了。
<view class="card" tabindex="0">
<image :src="`${VITE_URL}${props.image}`" fade-show lazy-load mode="aspectFill"></image>
<view class="bottom">
<text class="name">{{ props.name }}</text> <text class="remark">{{ props.remark }}</text>
<div class="footer">
<view class="tags">
<text class="tag" v-for="tag in tags" :key="tag">{{ tag }}</text>
</view>
<text class="price">¥ {{ props.price }}</text>
</div>
</view>
</view>
.card {
border-radius: 1.25vw;
overflow: hidden;
}
.card:focus {
box-shadow: 0 0 0 0.3vw #fff, 0 0 1vw 0.3vw #333;
outline: none;
transform: scale(1.03);
transition: box-shadow 0.3s ease, transform 0.3s ease;
}
三、如何使被聚焦的元素滚动到视图中心位置
使用renderjs进行实现如下
<script module="homePage" lang="renderjs">
export default {
mounted() {
let isScrolling = false; // 添加一个标志位,表示是否正在滚动
document.body.addEventListener('focusin', e => {
if (!isScrolling) {
// 检查是否正在滚动
isScrolling = true; // 设置滚动标志为true
requestAnimationFrame(() => {
// @ts-ignore
e.target.scrollIntoView({
behavior: 'smooth', // @ts-ignore
block: e.target.dataset.index ? 'end' : 'center'
});
isScrolling = false; // 在滚动完成后设置滚动标志为false
});
}
});
}
};
</script>
就可以使被聚焦元素滚动到视图中心,requestAnimationFrame的作用是缓存
四、如何在切换路由时,缓存聚焦的状态
通过设置tabindex属性为0和1,会有不同的效果:
- tabindex="0":将元素设为可聚焦,并按照其在文档中的位置来确定焦点顺序。当使用Tab键进行键盘导航时,tabindex="0"的元素会按照它们在源代码中的顺序获取焦点。这可以用于将某些非交互性元素(如、等)设为可聚焦元素,使其能够被键盘导航。
- tabindex="1":将元素设为可聚焦,并将其置于默认的焦点顺序之前。当使用Tab键进行键盘导航时,tabindex="1"的元素会在默认的焦点顺序之前获取焦点。这通常用于重置焦点顺序,或者将某些特定的元素(如重要的输入字段或操作按钮)置于首位。
需要注意的是,如果给多个元素都设置了tabindex属性,那么它们的焦点顺序将取决于它们的tabindex值,数值越小的元素将优先获取焦点。如果多个元素具有相同的tabindex值,则它们将按照它们在文档中的位置来确定焦点顺序。同时,负数的tabindex值也是有效的,它们将优先于零和正数值获取焦点。
我们要安装缓存插件,如pinia或vuex,需要缓存的页面单独配置
import { defineStore } from 'pinia';
export const useGlobalStore = defineStore('global', {
state: () => ({ home_active_tag: 'active0', hot_active_tag: 'hot0', dish_active_tag: 'dish0' })
});
更新一下业务代码
组件区域
<view class="card" :tabindex="home_active_tag === 'packagecard' + props.id ? 1 : 0">
<image :src="`${VITE_URL}${props.image}`" fade-show lazy-load mode="aspectFill"></image>
<view class="bottom">
<text class="name">{{ props.name }}</text> <text class="remark">{{ props.remark }}</text>
<div class="footer">
<view class="tags">
<text class="tag" v-for="tag in tags" :key="tag">{{ tag }}</text>
</view>
<text class="price">¥ {{ props.price }}</text>
</div>
</view>
</view>
const { home_active_tag } = storeToRefs(useGlobalStore());
页面区域
<view class="content">
<FoodCard
v-for="_package in list.dishes"
@click="goShopByFood(_package)"
:id="_package.id"
:name="_package.name"
:image="_package.image"
:tags="_package.tags"
:price="_package.price"
:shop_name="_package.shop_name"
:shop_id="_package.shop_id"
:key="_package.id"
></FoodCard>
<image
class="card"
@click="goMore"
:tabindex="home_active_tag === 'more' ? 1 : 0"
style="width: 29.375vw; height: 25.9375vw"
src="/static/home/more.png"
mode="aspectFill"
/>
</view>
const goShopByFood = async (row: Record<string, any>) => {
useGlobalStore().home_active_tag = 'foodcard' + row.id;
uni.navigateTo({
url: `/pages/shop/index?shop_id=${row.shop_id}`,
animationDuration: 500,
animationType: 'zoom-fade-out'
});
};
如果,要设置启动默认焦点 id和index可默认设置,推荐启动第一个焦点组用index,它可以确定
<view class="active">
<image
v-for="(active, i) in list.active"
:key="active.id"
@click="goActive(active, i)"
:tabindex="home_active_tag === 'active' + i ? 1 : 0"
:src="`${VITE_URL}${active.image}`"
data-index="0"
fade-show
lazy-load
mode="aspectFill"
class="card"
></image>
</view>
import { defineStore } from 'pinia';
export const useGlobalStore = defineStore('global', {
state: () => ({
home_active_tag: 'active0', //默认选择
hot_active_tag: 'hot0',
dish_active_tag: 'dish0'
})
});
对于多层级的,要注意销毁,在前往之前设置默认焦点
const goHot = (index: number) => {
useGlobalStore().home_active_tag = 'hotcard' + index;
useGlobalStore().hot_active_tag = 'hot0';
uni.navigateTo({ url: `/pages/hot/index?index=${index}`, animationDuration: 500, animationType: 'zoom-fade-out' });
};
五、如何启用wgt和apk两种方式的升级
pages.json
{
"path": "components/update/index",
"style": {
"disableScroll": true,
"backgroundColor": "#0068d0",
"app-plus": {
"backgroundColorTop": "transparent",
"background": "transparent",
"titleNView": false,
"scrollIndicator": false,
"popGesture": "none",
"animationType": "fade-in",
"animationDuration": 200
}
}
}
组件
<template>
<view class="update">
<view class="content">
<view class="content-top">
<text class="content-top-text">发现版本</text>
<image class="content-top" style="top: 0" width="100%" height="100%" src="@/static/bg_top.png"> </image>
</view>
<text class="message"> {{ message }} </text>
<view class="progress-box">
<progress
class="progress"
border-radius="35"
:percent="progress.progress"
activeColor="#3DA7FF"
show-info
stroke-width="10"
/>
<view class="progress-text">
<text>安装包正在下载,请稍后,系统会自动重启</text>
<text>{{ progress.totalBytesWritten }}MB/{{ progress.totalBytesExpectedToWrite }}MB</text>
</view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { onLoad } from '@dcloudio/uni-app';
import { reactive, ref } from 'vue';
const message = ref('');
const progress = reactive({ progress: 0, totalBytesExpectedToWrite: '0', totalBytesWritten: '0' });
onLoad((query: any) => {
message.value = query.content;
const downloadTask = uni.downloadFile({
url: `${import.meta.env.VITE_URL}/${query.url}`,
success(downloadResult) {
plus.runtime.install(
downloadResult.tempFilePath,
{ force: false },
() => {
plus.runtime.restart();
},
e => {}
);
}
});
downloadTask.onProgressUpdate(res => {
progress.progress = res.progress;
progress.totalBytesExpectedToWrite = (res.totalBytesExpectedToWrite / Math.pow(1024, 2)).toFixed(2);
progress.totalBytesWritten = (res.totalBytesWritten / Math.pow(1024, 2)).toFixed(2);
});
});
</script>
<style lang="less">
page {
background: transparent;
.update {
/* #ifndef APP-NVUE */
display: flex; /* #endif */
justify-content: center;
align-items: center;
position: fixed;
left: 0;
top: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.65);
.content {
position: relative;
top: 0;
width: 50vw;
height: 50vh;
background-color: #fff;
box-sizing: border-box;
padding: 0 50rpx;
font-family: Source Han Sans CN;
border-radius: 2vw;
.content-top {
position: absolute;
top: -5vw;
left: 0;
image {
width: 50vw;
height: 30vh;
}
.content-top-text {
width: 50vw;
top: 6.6vw;
left: 3vw;
font-size: 3.8vw;
font-weight: bold;
color: #f8f8fa;
position: absolute;
z-index: 1;
}
}
}
.message {
position: absolute;
top: 15vw;
font-size: 2.5vw;
}
.progress-box {
position: absolute;
width: 45vw;
top: 20vw;
.progress {
width: 90%;
border-radius: 35px;
}
.progress-text {
margin-top: 1vw;
font-size: 1.5vw;
}
}
}
}
</style>
App.vue
import { onLaunch } from '@dcloudio/uni-app';
import { useRequest } from './hooks/useRequest';
import dayjs from 'dayjs'; onLaunch(() => {
// #ifdef APP-PLUS
plus.runtime.getProperty('', async app => { const res: any = await useRequest('GET', '/api/tv/app'); if (res.code === 2000 && res.row.version > (app.version as string)) { uni.navigateTo({ url: `/components/update/index?url=${res.row.url}&type=${res.row.type}&content=${res.row.content}`, fail: err => { console.error('更新弹框跳转失败', err); } }); } });
// #endif
});
如果要获取启动参数
plus.android.importClass('android.content.Intent');
const MainActivity = plus.android.runtimeMainActivity();
const Intent = MainActivity.getIntent();
const roomCode = Intent.getStringExtra('roomCode');
if (roomCode) {
uni.setStorageSync('roomCode', roomCode);
} else if (!uni.getStorageSync('roomCode') && !roomCode) {
uni.setStorageSync('roomCode', '8888');
}
来源:juejin.cn/post/7272348543625445437
Fuse.js一个轻量高效的模糊搜索库
最近逛github的时候发现了一个非常好用的轻量工具库,Fuse.js,支持模糊搜索。感觉还是非常好用的,所以有了此篇博客,这篇文章主要是介绍Fuse的使用,同样,我对这个开源项目的实现也非常感兴趣。后续会出一篇Fuse源码解析的文章来分析其实现原理。
Fuse.js是什么?
强大、轻量级的模糊搜索库,没有任何依赖关系。
什么是模糊搜索?
一般来说,模糊搜索(更正式的名称是近似字符串匹配)是查找与给定模式近似相等(而不是完全相等)的字符串的技术。
通常我们项目中的的模糊搜索大多数情况下有几种方案可用:
- 前端工程通过正则表达式或者字符串匹配来实现
- 调用后端接口去匹配搜索
- 使用搜索引擎如:ElasticSearch或Algolia等
但是这些方案都有各自的缺陷,比如正则表达式和字符串匹配的效率较低,且无法处理复杂的搜索需求,而调用后端接口和搜索引擎虽然效率高,但是需要额外的服务器资源,且需要维护一套搜索引擎。
所以,Fuse.js的出现就是为了解决这些问题,它是一个轻量级的模糊搜索库,没有依赖关系,支持复杂的搜索需求,且效率高,当然Fuse并不适用于所有场景。
Fuse.js的使用场景
它可能不适用于所有情况,但根据您的搜索要求,它可能是最理想的。例如:
- 当您想要对小型到中等大型数据集进行客户端模糊搜索时
- 当您无法证明设置专用后端只是为了处理搜索时
- ElasticSearch 或 Algolia 虽然都是很棒的服务,但对于您的特定用例来说可能有些过度
Fuse.js的使用
安装
Fuse支持多种安装方式
NPM
npm install fuse.js
Yarn
yarn add fuse.js
CDN 引入
<script src="https://cdn.jsdelivr.net/npm/fuse.js@7.0.0">script>
引入
ES6 模块语法
import Fuse from 'fuse.js'
CommonJS 语法
const Fuse = require('fuse.js')
Tips: 使用npm或者yarn引入,支持两种模块语法引入,如果是使用cdn引入,那么Fuse将被注册为全局变量。直接使用即可
使用
以下是官网一个最简单的例子,只要简单的构造new Fuse对象,就能模糊搜索匹配到你想要的结果
// 1. List of items to search in
const books = [
{
title: "Old Man's War",
author: {
firstName: 'John',
lastName: 'Scalzi'
}
},
{
title: 'The Lock Artist',
author: {
firstName: 'Steve',
lastName: 'Hamilton'
}
}
]
// 2. Set up the Fuse instance
const fuse = new Fuse(books, {
keys: ['title', 'author.firstName']
})
// 3. Now search!
fuse.search('jon')
// Output:
// [
// {
// item: {
// title: "Old Man's War",
// author: {
// firstName: 'John',
// lastName: 'Scalzi'
// }
// },
// refIndex: 0
// }
// ]
从上述代码中可以看到我们要通过Fuse 对books的这个数组进行模糊搜索,构建的Fuse对象中,模糊搜索的key定义为['title', 'author.firstName'],支持对title及author.firstName这两个字段进行搜索。然后执行fuse的search API就能过滤出我们的期望结果。整体代码还是非常简单的。
高级配置
Demo示例只是提供了一个基础版本的模糊搜索。如果用户想获得更灵活的搜索能力,比如搜索结果排序、权重控制、搜索结果高亮等,那么就需要对Fuse进行一些高级配置。
Fuse的所有配置都是通过new Fuse时传入的参数来配置的,下面列举一些常用的配置项:
const options = {
keys: ['title', 'author'], // 指定搜索key值,可多选
isCaseSensitive: false, //是否区分大小写 默认为false
includeScore: false, //结果集中是否展示匹配项的分数字段, 分数越大代表匹配程度越低,区间值为0-1,注意:当此项为true时,会返回完整的结果集,只不过每一项中携带了score分数字段
includeMatches: false, //匹配项是否应包含在结果中。当时true,结果的每条记录都包含匹配项的索引。这个通常我们用来对搜索内容做高亮处理
threshold: 0.6, // 阈值控制匹配的敏感度,默认值为0.6,如果要完全匹配这里要设置为0
shouldSort: true, // 是否对结果进行排序
location: 0, // 匹配的位置,0 表示开头匹配
distance: 100, // 搜索的最大距离
minMatchCharLength: 2, // 最小匹配字符长度
};
出了上述常用的一些配置项之外,Fuse还支持更高阶模糊搜索,如权重搜索,嵌套搜索,运算符拓展搜索,具体高阶用法可以参考官方文档。
Fuse的主要实现原理是通过改写Bitap 算法(近似字符串匹配)算法的内部实现来支撑其模糊搜索的算法依据,后续会出一篇文章看一下作者源码的算法实现。
总结
Fuse的文章到此就结束了,你没看错就这么一点介绍就基本能支撑我们在项目中的应用,谢谢阅读,如果哪里有不对的地方请评论博主,会及时进行改正。
来源:juejin.cn/post/7393172686115569705
不使用代理,我是怎么访问Github的
背景
最近更换了 windows系统的电脑, git clone 项目的时候会连接超时的错误,不管我怎么把环境变量放到终端里尝试走代理都无果,于是开始了排查
以下命令是基于 git bash 终端使用的
检测问题
通过 ssh -T git@github.com 命令查看,会报如下错误:
ssh: connect to host github.com port 22: : Connection timed out
思索了一下,难道是端口的问题吗, 于是从 overflow 上找到回答:
修改 ~/.ssh/config 路径下的内容,增加如下
Host github.com
Hostname ssh.github.com
Port 443
这段配置实际上是让 github.com 走 443 端口去执行,评论上有些说 22端口被占用,某些路由器或者其他程序会占用它,想了一下有道理,于是使用 vim ~/.ssh/config 编辑加上,结果...
ssh: connect to host github.com port 443: : Connection timed out
正当我苦苦思索,为什么 ping github.com 超时的时候,脑子里突然回忆起那道久违的八股文面试题: “url输入网址到浏览器上会发生什么",突然顿悟:是不是DNS解析出了问题,找不到服务器地址?
网上学到一行命令,可以在终端里看DNS服务器的域名解析
nslookup baidu.com
先执行一下 baidu.com 的,得到如下:
Server: 119.6.6.6
Address: 119.6.6.6#53
Non-authoritative answer:
Name: baidu.com
Address: 110.242.68.66
Name: baidu.com
Address: 39.156.66.10
再执行一下 nslookup github.com ,果然发现不对劲了:
Name: github.com
Address: 127.0.0.1
返回了 127.0.0.1,这不对啊,笔者可是读过书的,这是本地的 IP 地址啊,原来是这一步出了问题..
解决问题
大部分同学应该都改过本地的 DNS 域名映射文件,这也是上面那道八股文题中回答的知识点之一,我们打开资源管理器输入一下路径改一下:
C:\Windows\System32\drivers\etc\hosts
MacOs的同学可以在终端使用 sudo vi /etc/hosts 命令修改
在下面加上下面这一行, 其中 140.82.113.4 是 github 的服务器地址,添加后就可以走本地的域名映射了
140.82.113.4 github.com
保存之后,就可以不使用代理,快乐访问 github.com 了,笔者顺利的完成了梦想第一步: git clone
结语
我是饮东,欢迎点赞关注,我们江湖再会
来源:juejin.cn/post/7328112739335372810
简单聊聊使用lombok 的争议
大家好,我是G探险者。
项目里,因为我使用了Lombok插件,然后代码走查的时候被领导点名了。

我心想,这么好用的插件,为啥不推广呢,整天写那些烦人的setter,getter方法就不嫌烦么?

领导既然不让用,自然有他的道理。

于是我查了一番关于lombok的一些绯闻。就有了这篇文章。
首先呢,Lombok 是一个在 Java 项目中广泛使用的库,旨在通过注解自动生成代码,如 getter 和 setter 方法,以减少重复代码并提高开发效率。然而,Lombok 的使用也带来了一些挑战和争议,特别是关于代码的可读性和与 Java Bean 规范的兼容性。
Lombok 基本使用
示例代码
不使用 Lombok:
public class User {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
// 其他 getter 和 setter
}
使用 Lombok:
import lombok.Data;
@Data
public class User {
private String name;
private int age;
// 无需显式编写 getter 和 setter
}
Lombok 的争议
- 代码可读性和透明度:Lombok 自动生成的代码在源代码中不直接可见,可能对新开发者造成困扰。
- 工具和 IDE 支持:需要特定的插件或配置,可能引起兼容性问题。
- 与 Java Bean 规范的兼容性:Lombok 在处理属性命名时可能与 Java Bean 规范产生冲突,特别是在属性名以大写字母开头的情况。
下面我就列举一个例子进行说明。
属性命名的例子
假设有这么一个属性,aName;
标准 Java Bean 规范下:
- 属性
aName的setter getter 方法应为setaName() getaName()。 - 但是 Lombok 可能生成
getAName()。
这是因为Lombok 在生成getter和setter方法时,将属性名的首字母也大写,即使它是小写的。所以对于aName属性,Lombok生成的方法可能是getAName()和setAName()。
在处理JSON到Java对象的映射时,JSON解析库(如Jackson或Gson)会尝试根据Java Bean规范匹配JSON键和Java对象的属性。它通常期望属性名的首字母在getter和setter方法中是小写的。因此,如果JSON键为"aName",解析库会寻找setaName()方法。
所以,当你使用Lombok的@Data注解,且Lombok生成的setter方法为setAName()时,JSON解析库可能找不到匹配的方法来设置aName属性,因为它寻找的是setaName()。
这种差异可能在 JSON 到 Java 对象的映射中引起问题。
Java Bean 命名规范
根据 Java Bean 规范,属性名应遵循驼峰式命名法:
- 单个单词的属性名应全部小写。
- 多个单词组成的属性名每个单词的首字母通常大写。
结论
Lombok 是一个有用的工具,可以提高编码效率并减少冗余代码。但是,在使用它时,团队需要考虑其对代码可读性、维护性和与 Java Bean 规范的兼容性。在决定是否使用 Lombok 时,项目的具体需求和团队的偏好应该是主要的考虑因素。
来源:juejin.cn/post/7310786611805863963
苦撑多年,老爷子70多!这个软件快要没人维护了

0x01、
在粒子物理学的发展过程中,有这样一个计算软件,它一度被视为粒子物理学研究的基础工具之一。
它就是:FORM。

众所周知,高能物理学领域中涉及很多超长且复杂的方程和公式,这时候就需要有一个能满足特定需求的计算软件(或者程序)来完成对应的工作。
而FORM则是一个可以进行大规模符号运算的计算程序,可以计算伽马矩阵、并行计算、包括模式匹配等。

多年来FORM一直扮演着粒子物理学领域关键工具的角色,并支撑着领域的研究和发展,行业内甚至有很多软件包都依赖于它。
但是就是这样一个领域必备的软件工具,其维护人现在都已经70多岁了,而如今却快要落得没人维护的田地了。。
0x02、
FORM自1984年就开始开发,距今已经有好几十年的历史了。
FORM的开发者是来自于荷兰的粒子物理学家乔斯·维马塞伦(Jos Vermaseren),也是现在该程序的维护者,现如今也已经70多岁高龄了。

而作为一个源自上世纪80年代的程序,彼时计算机方开始普及,软件工具也才逐渐开始兴起。
FORM的前身是由荷兰物理学家马蒂努斯·维尔特曼(Martinus Veltman)所创建的一个名为Schoonschip的程序,但是受限于当时的存储和外设条件等一系列原因,使用起来并不方便。
于是Jos Vermaseren开始着手研究该如何做出一个更易于获取和使用的工具程序。
起初Jos Vermaseren使用的是FORTRAN语言来写的这个程序,但是后来在FORM 1.0版本正式发布以前,Jos Vermaseren又重新使用C语言把该工具给重写了一遍。
就这样,从最早的Apollo工作站到后来的奔腾PC,这个程序慢慢开始被推广使用并流行起来。

经过多年的发展,目前FORM支持的版本如下:
- FORM:顺序版,可以在单个处理器上运行;
- ParFORM:多处理器版本,它可以使用集群和系统,处理器有自己的内存;
- TFORM:支持处理器共享内存系统的多线程版本,主要用于处理器数量有限的系统。
0x03、
聊回到FORM项目70多岁的维护人Jos Vermaseren老爷子,说实话还是非常佩服的。
进入Jos Vermaseren的GitHub主页(github.com/vermaseren)…

并在同期创建了他个人的首个GitHub仓库,也就是form。

截止到目前,这也是Jos Vermaseren在GitHub上的唯一一个维护的项目仓库。

不过比较遗憾的是,这个开源项目不管是访问量还是star、fork数,都十分惨淡。
0x04、
既然这个软件如此重要且无法完全被替代,那为什么现如今想找一些后继的维护人都不那么容易呢?
关于这个问题,Jos Vermaseren本人也曾说过:
“这么多年我一直都有看到,在计算工具上花费大量时间的科学家却无法得到一个物理学领域的终身职位。”
Jos Vermaseren表示自己还算是幸运的,拥有一个在荷兰国家亚原子物理研究所的终身职位,并且还有一个欣赏这个项目的老板,然而很多相关的研究者却不一定都能这样了。
所以这么看来,这也算是被一些现实的问题,所困扰到了。
投入大量精力却得不到对应的回报,而且还要求维护人员有跨学科的知识技能,不少相关领域的研究者也望而却步了。
而且在物理学术界,大家对于物理学本领域的成果产出和论文发表普遍比较看重,而程序开发的努力和关注度则往往被低估了。
可能这也某一程度上导致了像FORM这种软件工具想要找到持续的维护者都变得不那么容易了。
所以说到底,这也算是一个“坐冷板凳”的现实问题了。
文章的最后也附上和FORM相关的开源项目地址,分享给大家。
- FORM主页:http://www.nikhef.nl/~form
- GitHub主页:github.com/vermaseren/…
感兴趣的同学可以上去看一看,除此之外,大家有兴趣也可以研究一下对应的项目代码。
注:本文在GitHub开源仓库「编程之路」 github.com/rd2coding/R… 中已经收录,里面有我整理的6大编程方向(岗位)的自学路线+知识点大梳理、面试考点、我的简历、几本硬核pdf笔记,以及程序员生活和感悟,欢迎star。
来源:juejin.cn/post/7394788843207376947
2024年中总结-月亮你不懂六便士到底有多重
时光磨灭了许多东西,如今掘金也不再搞年中总结活动了。
别说年中活动了,整体互联网已经“毕业”了一大批员工,互联网缩水,程序员是最大的边际递减成本,但这里也不想再继续说关于焦虑的了。
自己还是照常写写总结吧。
24年已经过去了一半,不止,7.22是大暑,转眼下一个节气就又得是【立秋】了。
想到哪就写到哪吧,有时候很痴迷于这种随机性。
因为随机性,上半年先是出版了训练时长2年半的书《程序员成长手记》,没别的,只是有人找写书,按照流程坚持完成下来了。
因为随机性,后来4月的时候,又出了一本小册《AI BOTS 通关指南》,没别的,产品需要运营,运营需要声音,但大家早就知道了,AI Agent、或者说智能体,都在谈应用、何谈应用?估摸 GPT5 出来之前,所谓的这些 AIGC 都只玩具,无法深度参与生产。赚钱的都是教人赚钱的。
因为随机性,断断续续的更文,一方面工作、草、卷起来了,一方面生活耗时占比提升,一方面自己也没动力、懈怠了。
其实,无所谓生活吧,即使每天下午6点离开工位下班,到家也约近于7点,说是早上8.30上班,7点就要开始起床、做准备,有时候还要回想、梳理、做思想建设等等。一天12个小时围绕工作这件事(摸鱼时间也减少)、8小时围绕睡觉这件事,何谈生活呢?
但是生活确实又在持续发生,比如:2024上半年最大的变化,自己身份再转变,马上要为人父了。。
初为人父、这是一个过程。
从备孕、到验孕、到验血、到查胎心胎芽、到B超-查到积液、到多轮产检、到NT、到无创、到二维等等等等,每一个点都会分散出许多新的点,需要不断打破、建设认识。
然后,似乎又回到感觉有些焦虑了?在《何以为父》这本书上看到、这种心态或许是正常的。好的吧,总之无法作甩手掌柜、也不能。
想想,上半年,还有什么?
项目的工作更加熟练了,对其本质(解决方案、商务、PPT等)似乎有了更清晰的认识。期间也发生过一些插曲,也拿不准后面事态会去向何方,总之,好像也不是自己能定的,反复看《大明1566》桥段,“打工、晋升”是不是应该蛰伏?或者是不是自己这辈子连“田县丞”都见不到,那还想那么多干嘛?
还有什么?年初定的目标完成的并不好。
还有什么?离家多年、人在广东已经漂泊十年。理想=离乡?平民就必须拼命。
想起,最开始最开始写年中总结,引用《老人与海》,竟然现在更适用吧。
还有什么?
没有了,马上10.30了,洗洗睡了。
来源:juejin.cn/post/7394279685969199139
token是用来鉴权的,session是用来干什么的?
使用JWT进行用户认证和授权,而Session在一定程度上起到了辅助作用。
让我们详细讨论JWT和Session在这种结合模式中的各自作用以及为什么需要Session。
JWT的作用
- 用户认证:JWT包含了用户的身份信息和权限信息,客户端每次请求时将JWT发送给服务器,服务器通过验证JWT来确认用户身份。
- 无状态性:JWT不需要在服务器端存储用户会话信息,因此服务器可以是无状态的,便于扩展和负载均衡。
Session的作用
- 附加的安全层:即使JWT是无状态的,但在某些应用场景中,仅依赖JWT可能存在一些安全问题,例如Token的泄露或滥用。Session可以作为一个额外的安全层,确保Token即使有效,也必须在服务器的Session管理器中存在对应的会话。
- 管理Token的生命周期:通过Session,可以更方便地管理Token的生命周期,例如强制用户重新登录、手动注销Token等操作。
- 控制“记住我”功能:如果用户选择了“记住我”选项,Session可以记录这个状态,并在JWT过期后,通过Session来决定是否允许继续使用旧的Token。
为什么需要创建Session
尽管JWT可以在无状态环境中使用,但Session的引入带来了以下好处:
- 防止Token滥用:通过在服务器端验证Session,可以确保即使Token有效,也必须是经过服务器端认证的,从而防止Token被恶意使用。
- 支持用户主动注销:当用户选择注销时,可以直接删除服务器端的Session记录,确保Token即使没有过期,也无法再被使用。
- 提供更精细的控制:通过Session,可以实现更精细的权限控制和用户状态管理,例如强制下线、会话过期时间控制等。
- 状态追踪:在某些场景下,追踪用户状态是必要的,例如监控用户的活跃度、登录历史等,这些信息可以通过Session进行管理。
结合JWT和Session的优势
结合使用JWT和Session,可以同时利用两者的优点,实现安全性和扩展性的平衡:
- 无状态认证:JWT可以实现无状态认证,便于系统的水平扩展和负载均衡。
- 状态管理和安全性:Session可以提供额外的状态管理和安全性,确保Token的使用更加安全可靠。
代码示例
以下是一个简化的代码示例,展示了如何在用户登录时创建JWT和Session:
java
Copy code
public LoginResponse login(String username, String password) throws AuthException {
// 验证用户名和密码
User user = userService.authenticate(username, password);
if (user == null) {
throw new AuthException("Invalid username or password");
}
// 生成JWT Token
String token = createJwt(user.getId(), user.getRoles());
// 创建会话
sessionManagerApi.createSession(token, user);
// 返回Token
return new LoginResponse(token);
}
public void createSession(String token, User user) {
LoginUser loginUser = new LoginUser();
loginUser.setToken(token);
loginUser.setUserId(user.getId());
loginUser.setRoles(user.getRoles());
sessionManagerApi.saveSession(token, loginUser);
}
在请求验证时,首先验证JWT的有效性,然后检查Session中是否存在对应的会话:
java
Copy code
@Override
public DefaultJwtPayload validateToken(String token) throws AuthException {
try {
// 1. 先校验jwt token本身是否有问题
JwtContext.me().validateTokenWithException(token);
// 2. 获取jwt的payload
DefaultJwtPayload defaultPayload = JwtContext.me().getDefaultPayload(token);
// 3. 如果是7天免登陆,则不校验session过期
if (defaultPayload.getRememberMe()) {
return defaultPayload;
}
// 4. 判断session里是否有这个token
LoginUser session = sessionManagerApi.getSession(token);
if (session == null) {
throw new AuthException(AUTH_EXPIRED_ERROR);
}
return defaultPayload;
} catch (JwtException jwtException) {
if (JwtExceptionEnum.JWT_EXPIRED_ERROR.getErrorCode().equals(jwtException.getErrorCode())) {
throw new AuthException(AUTH_EXPIRED_ERROR);
} else {
throw new AuthException(TOKEN_PARSE_ERROR);
}
} catch (io.jsonwebtoken.JwtException jwtSelfException) {
throw new AuthException(TOKEN_PARSE_ERROR);
}
}
总结
在这个场景中,JWT用于无状态的用户认证,提供便捷和扩展性;Session作为辅助,提供额外的安全性和状态管理。通过这种结合,可以充分利用两者的优点,确保系统既具备高扩展性,又能提供细致的安全控制。
来源:juejin.cn/post/7383017171180568630
微信小程序 折叠屏适配
最近维护了将近的一年的微信小程序(某知名企业),突然提出要兼容折叠屏,这款小程序主要功能一些图表汇总展示,也就是专门给一些领导用的,也不知道为啥领导们为啥突然喜欢用折叠屏手机了,一句话需求,苦的还是咱们程序员,但没办法,谁让甲方是爸爸呢,硬着头皮改吧,好在最后解决了,因为是甲方内部使用的小程序,这里不便贴图,但有官方案例图片,以供参考
启用大屏模式
从小程序基础库版本 2.21.3 开始,在 Windows、Mac、车机、安卓 WMPF 等大屏设备上运行的小程序可以支持大屏模式。可参考小程序大屏适配指南。方法是:在 app.json 中添加 "resizable": true
看到这里我心里窃喜,就加个配置完事了?这也太简单了,但后面证明我想简单了,
主要有两大问题:
- 1 尺寸不同的情况下内容展示效果兼容问题
- 2 预览版和体验版 大屏模式冷启动会生效,但热启动 和 菜单中点击重新进入小程、授权操作,会失效变成窄屏
解决尺寸问题
因为css的长度单位大部分用的 rpx,窄屏和宽屏展示差异出入较大,别说客户不认,自己这关就过不了,简直都不忍直视,整个乱成一片,尤其登录页,用了定位,更是乱上加乱。
随后参考了官方的文档 小程序大屏适配指南和自适应布局,方案对于微信小程序原生开发是可行的,但这个项目用的 uni-app开发的,虽然uni-app 也有对应的响应式布局组件,再加上我是个比较爱偷懒的人(甲方给的工期事件也有限制),不可能花大量时间把所有也页面重新写一遍布局,这是不现实的。
于是又转战到uni-app官网寻找解决方案 uni-app宽屏适配指南
内容缩放拉伸的处理 这一段中提出了两个策略
- 1.局部拉伸:页面内容划分为固定区域和长宽动态适配区域,固定区域使用固定的px单位约定宽高,长宽适配区域则使用flex自动适配。当屏幕大小变化时,固定区域不变,而长宽适配区域跟着变化
- 2.等比缩放:根据页面屏幕宽度缩放。rpx其实属于这种类型。在宽屏上,rpx变大,窄屏上rpx变小。
随后看到这句话特别符合我的需求,哈哈 省事 省事 省事
策略2省事,设计师按750px屏宽出图,程序员直接按rpx写代码即可。但策略2的实际效果不如策略1好。程序员使用策略1,分析下界面,设定好局部拉伸区域,这样可以有更好的用户体验
具体实现
1.配置 pages.json 的 globeStyle
{
"globalStyle": {
"rpxCalcMaxDeviceWidth": 1200, // rpx 计算所支持的最大设备宽度,单位 px,默认值为 960
"rpxCalcBaseDeviceWidth": 375, // rpx 计算使用的基准设备宽度,设备实际宽度超出 rpx 计算所支持的最大设备宽度时将按基准宽度计算,单位 px,默认值为 375
"rpxCalcIncludeWidth": 750 // rpx 计算特殊处理的值,始终按实际的设备宽度计算,单位 rpx,默认值为 750
},
}
2.单位兼容
还有一点官方也提出来了很重要,那就很多时候 会把宽度750rpx 当成100% 使用,这在宽屏的设备上就会有问题, uniapp给了两种解决方案
- 750rpx 改为100%
- 另一种是配置rpxCalcIncludeWidth,设置某个特定数值不受rpxCalcMaxDeviceWidth约束
想要用局部拉伸:页面内容划分为固定区域和长宽动态适配区域”的策略,单位必须用px
添加脚本
项目根目录新增文件 postcss.config.js 内容如下。则在编译时,编译器会自动转换rpx单位为px。
// postcss.config.js
const path = require('path')
module.exports = {
parser: 'postcss-comment',
plugins: {
'postcss-import': {
resolve(id, basedir, importOptions) {
if (id.startsWith('~@/')) {
return path.resolve(process.env.UNI_INPUT_DIR, id.substr(3))
} else if (id.startsWith('@/')) {
return path.resolve(process.env.UNI_INPUT_DIR, id.substr(2))
} else if (id.startsWith('/') && !id.startsWith('//')) {
return path.resolve(process.env.UNI_INPUT_DIR, id.substr(1))
}
return id
}
},
'autoprefixer': {
overrideBrowserslist: ["Android >= 4", "ios >= 8"],
remove: process.env.UNI_PLATFORM !== 'h5'
},
// 借助postcss-px-to-viewport插件,实现rpx转px,文档:https://github.com/evrone/postcss-px-to-viewport/blob/master/README_CN.md
// 以下配置,可以将rpx转换为1/2的px,如20rpx=10px,如果要调整比例,可以调整 viewportWidth 来实现
'postcss-px-to-viewport': {
unitToConvert: 'rpx',
viewportWidth: 200,
unitPrecision: 5,
propList: ['*'],
viewportUnit: 'px',
fontViewportUnit: 'px',
selectorBlackList: [],
minPixelValue: 1,
mediaQuery: false,
replace: true,
exclude: undefined,
include: undefined,
landscape: false
},
'@dcloudio/vue-cli-plugin-uni/packages/postcss': {}
}
}
大屏模式失效问题
下面重头戏来了,这期间经历 蜿蜒曲折 ,到头来发现都是无用功,我自己都有被wx蠢到发笑,唉,
样式问题解决后 开始着重钻研 大屏失效的问题,但看了官方的多端适配示例demo,人家的就是好的,那就应该有解决办法,于是转战github地址 下项目,谁知这项目暗藏机关,各种报错,让你跑不起来。。。。,让我一度怀疑腾讯也这么拉跨
还好issues 区一位大神有解决办法 感兴趣的老铁可以去瞅瞅


另外 微信小程序开发工具需要取消这两项,最后当项目跑起来后我还挺开心,模拟器上没有问题,但用真机预览的时候我啥眼了,还是窄屏,偶尔可以大屏,后面发现 冷启动是大屏,热启动和点击右上角菜单中的重新进入小程序按钮都会自己变成窄屏幕
![]() | ![]() |
这是官方的项目啊,为啥人家的可以,我本地跑起来却不可以,让我一度怀疑这里有内幕,经过几轮测试还是不行,于是乎,我开始了各种询问查资料,社区、私聊、评论、github issues,最后甚至 统计出来了 多端适配示例demo 开发者的邮箱 挨个发了邮件,但都结果无一例外,全部石沉大海
![]() | ![]() |
![]() | ![]() |
![]() | ![]() |
结果就是,没有办法了,想看看是不是只有预览和体验版有问题,后面发布到正式版后,再看居然没问题了,就是这么神奇,也是无语!!!! 原来做了这么多无用功。。。。
来源:juejin.cn/post/7273764921456492581
前端项目公共组件封装思想(Vue)
1. 通用组件(表单搜索+表格展示+分页器)
在项目当中我们总会遇到这样的页面:页面顶部是一个表单筛选项,下面是一个表格展示数据。表格下方是一个分页器,这样的页面在我们的后台管理系统中经常所遇到,有时候可能不止一个页面,好几个页面的结构都是这种。如图:

本人记得,在react中的高级组件库中有这么一个组件,就实现了这么一个效果。就拿这个页面来说我们实现一下组件封装的思想:1.首先把每个页面的公共部分抽出来,比如标题等,用props或者插槽的形式传入到组件中进行展示 2. 可以里面数据的双向绑定实现跟新的效果 3. 设置自定义函数传递给父组件要做上面事情
1.将公共的部分抽离出来
TableContainer组件
<template>
<div class="container">
<slot name="navbar"></slot>
<div class="box-detail">
<div class="detail-box">
<div class="box-left">
<div class="left-bottom">
<div class="title-bottom">{{ title }}</div>
<div class="note">
<div class="note-detail">
<slot name="table"></slot>
</div>
</div>
</div>
</div>
</div>
</div>
<el-backtop style="width: 3.75rem; height: 3.75rem" :bottom="10" :right="5">
<div
style="
{
width: 5.75rem;
flex-shrink: 0;
border-radius: 2.38rem;
background: #fff;
box-shadow: 0 0.19rem 1rem 0 #2b4aff14;
}
"
>
<i class="el-icon-arrow-up" style="color: #6e6f74"></i>
</div>
</el-backtop>
</div>
</template>
这里的话利用了具名插槽插入了navbar、table组件,title通过props的属性传入到子组件当中。进行展示,
父组件
<TableContainer title="资源审核">
<template v-slot:navbar>
<my-affix :offset="0">
<Navbar/>
</my-affix>
</template>
<template v-slot:table>
<SourceAuditTable/>
</template>
</TableContainer>
当然这是一个非常非常简单的组件封装案例
接下来我们看一个高级一点的组件封装
父组件
<template>
<div>
<hr>
<HelloWorld :page.sync="page" :limit.sync="limit" />
</div>
</template>
<script>
import HelloWorld from './components/HelloWorld.vue';
export default {
data() {
return {
page: 1,
limit: 5
}
},
components: {
HelloWorld
},
}
</script>
父组件传递给子组件各种必要的属性:total(总共多少条数据)、page(当前多少页)、limit(每页多少条数据)、pageSizes(选择每页大小数组)
子组件
<template>
<el-pagination :current-page.sync="currentPage" :page-size.sync="pageSize" :total="20" />
</template>
<script>
export default {
name: 'HelloWorld',
props: {
page: {
default: 1
},
limit: {
default: 5
},
},
computed: {
currentPage: {
get() {
return this.page
},
set(val) {
//currentPage 这里对currentPage做出来改变就会走这里
//这边更新数据走这里
console.log('currentPage', this.currentPage)
this.$emit('update:page', val)
}
},
pageSize: {
get() {
return this.limit
},
set(val) {
this.$emit('update:limit', val)
}
}
},
methods: {
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
这里的page.sync、limit.sync目的就是为了实现数据的双向绑定,computed中监听page和limit的变化,子组件接收的数据通过computed生成的currentPage通过sync绑定到了 el-pagination中, 点击分页器的时候会改变currentPage 此时会调用set函数设置新的值,通过代码 this.$emit(update:page,value) 更新父组件中的值,实现双向的数据绑定
本文是作者在闲暇的时间随便记录一下, 若有错误请指正,多多包涵。感谢支持!
来源:juejin.cn/post/7312353213347708940
这一年我优化了一个46万行的超级系统
背景
我曾带领团队治理了一个超级工程,是我毕业以来治理的最庞大、最复杂的工程系统,涉及到开发的方方面面。下面我给大家列几个数字,大家感受一下:
指标 数据 菜单数量 250+ 代码行数 46 万 路由数量 300+ 业务组件、util 600+ 构建时间 6min 关联业务 报表、CRM、订单、车辆、配置、财务...

这是上一任留给我的烫手山芋,而且从需求迭代频次来看,这个系统占据了这个业务部门50%的需求,也就是说,之前的伙计把几乎所有的业务功能都做进了一个系统中,像屎山一般堆积,构建一次的时间长达6-9min,作为一个合格的前端,简直怒火冲天。
我曾带领团队治理了一个超级工程,是我毕业以来治理的最庞大、最复杂的工程系统,涉及到开发的方方面面。下面我给大家列几个数字,大家感受一下:
| 指标 | 数据 |
|---|---|
| 菜单数量 | 250+ |
| 代码行数 | 46 万 |
| 路由数量 | 300+ |
| 业务组件、util | 600+ |
| 构建时间 | 6min |
| 关联业务 | 报表、CRM、订单、车辆、配置、财务... |

这是上一任留给我的烫手山芋,而且从需求迭代频次来看,这个系统占据了这个业务部门50%的需求,也就是说,之前的伙计把几乎所有的业务功能都做进了一个系统中,像屎山一般堆积,构建一次的时间长达6-9min,作为一个合格的前端,简直怒火冲天。
问题
面对这样的超级应用,想要解决,必须先把问题整理出来,才好对症下药。
- 构建时间过长,影响开发体验。
- 系统单一,代码量庞大,未做拆分,对于后期维护难度很大,且存在增量上线风险(改一个字,也要整个系统打包上线)。
- 代码业务组件太少,复用率低,需要整理业务代码,封装高效业务组件库。
- 工具函数、请求Axios、目录规范、菜单权限、公共能力等未做统一封装和梳理。
- 存在大量重复的代码、大量重复的组件(有很多都是拷贝了一份,改个名字)。
- 页面加载极其缓慢,无论是首屏还是菜单加载,很明显存在严重的性能问题。
- 工程中随意引入各种插件,比如:big.js、xlsx.js、echarts、velocity-animate、lodash、file-saver等。
- 代码中存在很多mixin写法,导致调试难度很大。
以上是针对46万行的应用做出的问题分析报告,找出这些问题以后,我们就能开启优化之路。
面对这样的超级应用,想要解决,必须先把问题整理出来,才好对症下药。
- 构建时间过长,影响开发体验。
- 系统单一,代码量庞大,未做拆分,对于后期维护难度很大,且存在增量上线风险(改一个字,也要整个系统打包上线)。
- 代码业务组件太少,复用率低,需要整理业务代码,封装高效业务组件库。
- 工具函数、请求Axios、目录规范、菜单权限、公共能力等未做统一封装和梳理。
- 存在大量重复的代码、大量重复的组件(有很多都是拷贝了一份,改个名字)。
- 页面加载极其缓慢,无论是首屏还是菜单加载,很明显存在严重的性能问题。
- 工程中随意引入各种插件,比如:big.js、xlsx.js、echarts、velocity-animate、lodash、file-saver等。
- 代码中存在很多mixin写法,导致调试难度很大。
以上是针对46万行的应用做出的问题分析报告,找出这些问题以后,我们就能开启优化之路。
目标

我觉得不管哪一行,进入这个社会最重要的能力是生存能力,面对生存最重要的技能是解决问题的能力。想要把问题解决好,就要有章法可循,比如:找到问题要害、制定目标、提供解决方案、复盘总结。

我觉得不管哪一行,进入这个社会最重要的能力是生存能力,面对生存最重要的技能是解决问题的能力。想要把问题解决好,就要有章法可循,比如:找到问题要害、制定目标、提供解决方案、复盘总结。
方案
- 250+菜单归类整理、废弃菜单下线
- 搭建业务组件库
- 搭建工具函数库
- 基础框架优化
- 基于microApp做微服务拆分、引入webpack5的module-federation机制
- 引入rocket-render插件,对局部页面、基础功能做重构,后续逐步替换。
- 性能优化
- 250+菜单归类整理、废弃菜单下线
- 搭建业务组件库
- 搭建工具函数库
- 基础框架优化
- 基于microApp做微服务拆分、引入webpack5的module-federation机制
- 引入rocket-render插件,对局部页面、基础功能做重构,后续逐步替换。
- 性能优化
菜单整理
300 多个菜单,业务和产研人员可能已经更换过好几次,通过跟产品的沟通,可以得知,受业务调整影响,很多功能已经废弃,系统未及时下线导致日积月累。然而由于对业务不熟,产研很难判断哪些菜单需要下线,我们也不能随意凭感觉下线某一个菜单或功能,必须采取谨慎的态度,有法可依,因此我们采用如下措施:

菜单汇总
产品牵头,汇总系统所有菜单,按环境进行逐个确认。前端会根据确认结果依次删除路由配置、菜单、关联组件、调用 API 等相关代码。
- 同业务方确认后,直接下线。
- 线上访问异常菜单,进行标注。
- 数据异常菜单,进行标注。
- 对于无法确认的,通过监控查看页面访问量,通过1-3个月的观察,最终决定是否下线。
通过1个月的菜单整理,下线了大概50+的菜单、删除了近100+页面,以及不计其数的业务组件代码,整体代码量减少近8万,同时,我们将250多个菜单进行归类,拆分了6个大类,供后续微服务做准备。
300 多个菜单,业务和产研人员可能已经更换过好几次,通过跟产品的沟通,可以得知,受业务调整影响,很多功能已经废弃,系统未及时下线导致日积月累。然而由于对业务不熟,产研很难判断哪些菜单需要下线,我们也不能随意凭感觉下线某一个菜单或功能,必须采取谨慎的态度,有法可依,因此我们采用如下措施:

菜单汇总
产品牵头,汇总系统所有菜单,按环境进行逐个确认。前端会根据确认结果依次删除路由配置、菜单、关联组件、调用 API 等相关代码。
- 同业务方确认后,直接下线。
- 线上访问异常菜单,进行标注。
- 数据异常菜单,进行标注。
- 对于无法确认的,通过监控查看页面访问量,通过1-3个月的观察,最终决定是否下线。
通过1个月的菜单整理,下线了大概50+的菜单、删除了近100+页面,以及不计其数的业务组件代码,整体代码量减少近8万,同时,我们将250多个菜单进行归类,拆分了6个大类,供后续微服务做准备。
框架优化
一个稳定的系统,必然有一个稳定的框架做支撑。我们在梳理的过程中发现系统极其脆弱,无 ESLint 语法检查规范、没有 Prettier 格式化、Hard Code、随处可见的自定义组件、到处报错的控制台...
- 引入
ESLint 做语法检查. - 引入
Pettier 做代码格式化。 - 引入
Husky lint-staged 规范代码提交。 - 对于硬编码部分跟产品沟通做成可视化配置或者json配置形式。
- Axios的二次封装:常规请求、全局Loading、文件下载、登录失效拦截、报错拦截提示、状态码统一适配。
- 插件删减:
big.js 剔除改为手动计算、lodash 替换为 lodash-es、删除动画插件、文件导出统一由后端返回二进制,前端通过Axios封装下载函数进行文件下载等等。 - 配置文件封装:多环境配置、常量定义。
- 菜单权限封装。
- 按钮权限指令封装:
v-has="'create'" - router路由提取优化,针对路由守卫处理一些特殊跳转问题。
- 接入前端监控平台,对于资源请求失败、接口请求超时、接口请求异常等做异常监控。
- 对于
eslint和prettier大家自行参考其他文章,此处不再赘述。 - 对于Axios二次封装,大家可能会比较奇怪,为什么要封装文件下载?因为文件导出本身也是一个请求,只是跟常规get/post有差异,所以我们为了方便调用,把它放在一个文件里面定义,比如:
export default {
get(url, params = {}, options = { showLoading: true, showError: true }) {
return instance.get(url, { params, ...options })
},
post(url, params = {}, options = { showLoading: true, showError: true }) {
return instance.post(url, params, options)
},
download(url, data, fileName = 'fileName.xlsx') {
instance({
url,
data,
method: 'post',
responseType: 'blob'
}).then(response => {
const blob = new Blob([response.data], {
type: response.data.type
})
const name = (response.headers['file-name'] as string) || fileName
const link = document.createElement('a')
link.download = decodeURIComponent(name)
link.href = URL.createObjectURL(blob)
document.body.append(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(link.href)
})
}
}
我们在api.js中调用的时候,就会变的很简单,如:api.get('/user/list')、api.download('/export',{ id: 123 })。当然可能每个人有自己的开发习惯,这些都是我个人的经验,也不一定适合大家。对于上面的参数有一个options变量,用来做参数扩展的,比如展示loading和error,为了做全局Loading和全局错误提示用的。
- 如果你担心页面并发请求,导致重复
loading问题,可以通过计数的方式来控制,避免多次重复Loading。 - 插件删除部分大家可能有争议,这个纯属于个人行为,因为我们不是金融行业,不需要高精度,如果出现计算我们直接四舍五入即可,而
lodash-es主要为了做tree-shaking,还有很多插件根据自身情况考虑要不要引入。 - 指令封装,对于按钮权限非常管用,举个例子:
一个稳定的系统,必然有一个稳定的框架做支撑。我们在梳理的过程中发现系统极其脆弱,无 ESLint 语法检查规范、没有 Prettier 格式化、Hard Code、随处可见的自定义组件、到处报错的控制台...
- 引入
ESLint做语法检查. - 引入
Pettier做代码格式化。 - 引入
Huskylint-staged规范代码提交。 - 对于硬编码部分跟产品沟通做成可视化配置或者json配置形式。
- Axios的二次封装:常规请求、全局Loading、文件下载、登录失效拦截、报错拦截提示、状态码统一适配。
- 插件删减:
big.js剔除改为手动计算、lodash替换为lodash-es、删除动画插件、文件导出统一由后端返回二进制,前端通过Axios封装下载函数进行文件下载等等。 - 配置文件封装:多环境配置、常量定义。
- 菜单权限封装。
- 按钮权限指令封装:
v-has="'create'" - router路由提取优化,针对路由守卫处理一些特殊跳转问题。
- 接入前端监控平台,对于资源请求失败、接口请求超时、接口请求异常等做异常监控。
- 对于
eslint和prettier大家自行参考其他文章,此处不再赘述。 - 对于Axios二次封装,大家可能会比较奇怪,为什么要封装文件下载?因为文件导出本身也是一个请求,只是跟常规get/post有差异,所以我们为了方便调用,把它放在一个文件里面定义,比如:
export default {
get(url, params = {}, options = { showLoading: true, showError: true }) {
return instance.get(url, { params, ...options })
},
post(url, params = {}, options = { showLoading: true, showError: true }) {
return instance.post(url, params, options)
},
download(url, data, fileName = 'fileName.xlsx') {
instance({
url,
data,
method: 'post',
responseType: 'blob'
}).then(response => {
const blob = new Blob([response.data], {
type: response.data.type
})
const name = (response.headers['file-name'] as string) || fileName
const link = document.createElement('a')
link.download = decodeURIComponent(name)
link.href = URL.createObjectURL(blob)
document.body.append(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(link.href)
})
}
}
我们在api.js中调用的时候,就会变的很简单,如:api.get('/user/list')、api.download('/export',{ id: 123 })。当然可能每个人有自己的开发习惯,这些都是我个人的经验,也不一定适合大家。对于上面的参数有一个options变量,用来做参数扩展的,比如展示loading和error,为了做全局Loading和全局错误提示用的。
- 如果你担心页面并发请求,导致重复
loading问题,可以通过计数的方式来控制,避免多次重复Loading。 - 插件删除部分大家可能有争议,这个纯属于个人行为,因为我们不是金融行业,不需要高精度,如果出现计算我们直接四舍五入即可,而
lodash-es主要为了做tree-shaking,还有很多插件根据自身情况考虑要不要引入。 - 指令封装,对于按钮权限非常管用,举个例子:
封装指令
import { getPageButtons } from '@hll/css-oms-utils'
// 权限指令
Vue.directive('has', {
inserted: (el, binding) => {
let pageButtons = getPageButtons().map;
if (!pageButtons[binding.value]) {
el.parentNode && el.parentNode.removeChild(el)
}
}
})
权限判断
// 1. 通过v-if判断
<el-button type="primary" v-if="pageButtons.create">创建el-button>
// 2. 通过v-has指令判断
<el-button type="primary" v-has="'create'">创建el-button>
getPageButtons 其实是为了兼容历史代码而封装的函数。
整个系统权限极其混乱,代码替换,耗时近 2 周,覆盖近 500 个文件才完成。
- 状态码适配
这个我觉得有必要提一下,我相信很多前端可能都遇到这个问题,一个系统对应n个后台服务,不同的后台服务有不同的格式,比如:A系统返回code=0,B系统返回result=0,C系统返回res=0,那前端就要做不同的适配,其实也有不同的方法可以做:
- 让后端接入网关,统一在网关做适配。
- 前端在拦截器中开发
adapter函数,针对响应码做适配。 - 针对分页、返回码、错误信息等都可以在适配函数里面做几种适配,或者提供单一函数在不同的页面单独调用,类似于
request的to模块。
业务组件库建设
这一部分工作非常重要,不管你在哪个公司,做任何行业,我都建议封装业务组件库,当然封装有三种形式:
- 基于公司自建的
npm平台开发业务组件库,通过npm方式引入。 - 对于小体量项目,直接把业务组件库放在
components中进行维护,但是无法跨项目使用。 - 基于
webpack5的module federation能力开发公共组件,跨项目提供服务。
MF文档参考:http://www.webpackjs.com/concepts/mo… 我觉得在某些场景下,这个能力非常好用,它不需要像npm一样,发布以后还需要给项目做升级,只要你改了组件源码,发布以后,其他引用的项目就会立即生效。想要快速了解这个能力,我建议大家先看一下插件文档,然后了解两个概念exposes和remotes,如果还是看不懂,就去找个掘进入门文章再结合本地实践一次基本就算入门了。
我是基于上面三种方式来搭建系统的业务组件库,对于通用部分全部提取封装,基于rollup和vite搭建一套npm包,最终发布到公司私有npm平台。对于一些频繁改动,链路较长部分通过module federation进行封装和暴露。
梳理业务后,其实封装了很多业务组件,上面只是给大家提供了思路,没有一一列举具体组件,下面给大家简单列一个大纲图:

业务组件库建设对于这种大体量的超级系统收益非常大,而且是长期可持续的。
微服务搭建
前面第一部分梳理完菜单以后,其实已经将250+的菜单进行了归类,归类的目的其实就是为了服务拆分。拆分的好处非常明显:
- 服务解耦,便于维护。
- 局部需求可单独上线,不需要整包上传,减小线上风险。
- 缩小每个服务模块的构建时间,提升开发体验。
本次基于pnpm + microApp + module federation来实现的微服务拆分,为什么?
- 微服务拆分以后,创建了 7 个仓库,非常不利于代码维护。
pnpm天然具备monorepo能力,可以把多个仓库合并为一个仓库,而且可以继续按7个项目的开发构建方式去工作。 - 微服务使用的是京东的
microApp框架,集成非常简单,上手成本很低,文档友好,我觉得比阿里的乾坤简单太多了(个人主观看法)。 - 对于难于抽取的组件,直接通过
module federation对外暴露组件服务。
上面在搭建业务组件库的时候,其实遇到了一个非常棘手的问题,有些组件跨了很多项目,链路很长,在抽取的过程中难度非常大,大家看一个图:

服务之间耦合太严重,从而导致组件抽取难度很大,那如何解决? 答案就是module federation,抽取不了,就不抽取了,直接通过exposes对外暴露组件服务,在其它子服务中调用即可。
下面给大家举一个接入microApp的例子:
基座服务(主应用)
import microApp from '@micro-zoe/micro-app';
microApp.start()
添加组件容器(主应用)
<template>
<micro-app name="vms" :url="url" baseroute="/" @datachange='handleDataChange'>micro-app>
template>
<script>
export default {
name: 'ChildApp',
data() {
return {
activeMenu: '',
url: 'http://xxxx',
};
},
methods:{
handleDataChange({ detail: { data } }) {
// todo
},
}
};
script>
分配菜单(主应用)
{
path: '/child',
name: 'child',
component: () => import('./../layout/microLayout.vue'),
meta: {
appName: 'vms',
title: '子服务A'
}
}
就这样,一个主服务就搭建好了,等子服务上线以后,点击/child菜单,就能打开对应的子服务了。 当然因为我们是一个超级应用,所以我们在做的过程中其实遇到了很多问题,比如:跨域问题、变量问题、包共享问题、本地开发调试问题、远程加载问题等等,不过大家可以参考官方文档,都是可以解决的。
Rocket-render接入
这是我个人开源的一套基于Vue2的渲染引擎,通过json定义就可以快速搭建各种简单或复杂的表单、表格场景,跟formly这一类非常相似。
- 插件文档:rocket-render
- 开发文档:rocket-doc
给大家举一个简单的例子:
- 安装插件
yarn add rocket-render -S
- 组件注册
// 导入包
import RocketRender from 'rocket-render';
// 导入样式
import 'rocket-render/lib/rocket-render.css';
// 默认安装
Vue.use(RocketRender);
// 或者使用自定义属性,以下的值都是系统默认值,如果你的业务不同,可以配置下面的全局变量进行修改。
Vue.use(RocketRender, {
size: 'small',
empty: '-',
inline: 'flex',
toolbar: true,
align: 'center',
stripe: true,
border: true,
pager: true,
pageSize: 20,
emptyText: '暂无数据',
});
插件支持自定义属性,建议默认即可,我们已经给大家调好,如果确实想改,就通过自定义属性的方式进行修改。
- 页面应用
search-form 自带背景色和内填充,如果你有特殊需要,可以添加 class 进行覆盖,另外建议给 model 添加 sync 修饰符。
<template>
<search-form
:json="form"
:model.sync="queryForm"
@handleQuery="getTableList"
/>
template>
<script>
export default {
data() {
return {
queryForm: {},
form: [
{
type: 'text',
model: 'user_name',
label: '用户',
placeholder: '请输入用户名称',
},
],
};
},
};
script>
我们针对需要自定义部分提供了slot插槽,所以不用担心用了以后,对于复杂需求支持不了,导致返工的现象,完全不存在,除了json以外,我们还内置了非常多的开发技巧,保证你爱不释手,继续看几个例子:
- 日期范围组件,通过
export直接暴露两个字段。
{
type: 'daterange',
model: 'login_time',
label: '日期范围',
// 对于日期范围控件来说,一般接口需要拆分为两个字段,通过export可以很方便的实现字段拆分
export: ['startTime', 'endTime'],
// 日期转换为时间戳单位
valueFormat: 'timestamp', // 支持:yyyy-MM-dd HH:mm:ss
defaultTime: ['00:00:00', '23:59:59'], //可以设置默认时间,有时候非常有用。后端查询的时候,必须从0点开始才能查到数据。
}
前端是一个组件对应一个字段,但是接口往往需要让你传开始和结束日期,通过export可以直接拆分,太好用了。
- 下拉组件支持一步请求
{
type: 'select',
model: 'userStatus',
label: '用户状态',
multiple: true, // 支持多选
filterable: true, //支持输入过滤
clearable: true,
// 如果下拉框的值是动态的,可以使用fetchOptions获取,必须返回promise
fetchOptions: async () => {
return [
{ name: '全部', id: 0 },
{ name: '已注销', id: 1 },
{ name: '老用户', id: 2 },
{ name: '新用户', id: 3 },
];
},
// 字段映射,用来处理接口返回字段,避免前端去循环处理一次。
field: {
label: 'name',
value: 'id',
},
options: [],
// 如果想要修改其它表单值,可以通过change事件来处理
change: this.getSelectList,
}
通过fetchOptions可以直接在里面调用接口来返回下拉的值。如果是静态的,直接定义options即可。 如果接口返回的类别字段不是label和value结构,可以通过field来做映射。我觉得用的太爽了。
还有很多,我就不举例子了,大家去看文档就好了,文档写的非常清楚。
性能优化
前面几部分做完以后,我们的代码总量基本已经降到了 30 万行左右了,而且整个构建时长从9min降到了 30s ,有了业务组件库的加持,简直不要太爽,不过你看到的这些,看起来容易,其实我们花费了近1年的时间才完成。 剩下就是提升系统性能了:
- 资源全部上
cdn,不仅上cdn,还要再阿里云针对图片开启webp(需要做兼容处理),cdn记得添加Cache-Control缓存。 - 服务器全部支持
gzip压缩。 - 添加
external配置,我在npm开发了一个vite-plugin-external-new插件,可以帮你解决。
- 这个大家一定要做,因为可以有效降低vender体积,你通过LightHouse做性能分析,就能知道原因,对于体积较大的js会给你提示的。
- 通过
external,我们可以直接让vue、vue-router、vuex、element-ui等等全部通过defer加载。
- 建议在根html
中加一个Loading标签
<div id="app">
<div class="loading">加载中...div>
div>
这样做的好处是,如果vue.js还没有加载完成之前,可以让页面先loading,等new Vue({ el: '#app' })执行以后,才会覆盖#app里面的 内容,这样可以提升FCP指标。 5. 对于比较大的插件,建议按需
export const loadScript = (src: string) => {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.type = 'text/javascript';
script.defer = true;
script.onload = resolve;
script.onerror = reject;
script.src = src;
document.head.append(script);
});
};
某一个页面如果使用较大的插件,比如:xlsx、html2Canvas等,可以单独在某一个页面内做按需加载。
- 有些页面也可以针对
vue组件或者大图片做按需加载。 - 其它性能优化,我觉得可做可不做,针对LightHouse分析报告针对性下手即可,可能很多文章将性能优化会非常细致,比如chunk分包等等,我倒觉得不是很重要。 只要把上面的做完,理论上前端的性能已经会有巨大的提升了。
结果指标
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 构建时长 | 6-9min | 30-45s |
| 代码行数 | 46万 | 30 万 |
| 服务 | 1个 | 7个 |
| 业务组件库 | 乱七八糟 | 基于rollup开发构建 |
| 基础框架 | 乱七八糟 | 高逼格 |
| 性能评分 | 30分 | 92分 |
| 团队成员 | 9个 | 4个 |
以上就是我面对一个 46 万行代码的超级系统,耗时大概一年的时间所做的一些成果,很多技术细节也只是泛泛而谈,但是我希望给大家提供的是工作方法、解决思路等偏软实力方面,技术功底和技术视野还是要靠自己一步一个脚印。
这一年我过的很充实,面对突如其来的问题,我心情很复杂,这一年,我感觉也老了很多。最后,感谢你耐心的倾听,我是河畔一角,一个普通的前端工程师。
来源:juejin.cn/post/7394095950383710247
在自己没有价值前,就不要谈什么人情世故了!
Hello,大家好,我是 Sunday。
昨天有位同学跟我吐槽,大致的意思是:“感觉自己不会搞 人情世故,导致在职场中很吃亏。领导也不罩着自己!”
在我的印象中,好像很多程序员都是 “不懂人情世故的典型”,至少很多程序员都认为自己是 “不懂人情世故的”。
但是,人情世故是什么?它真的有用吗?你跟领导关系好,他就会罩着你,帮你背锅吗?
恐怕是:想多了!!
一个真实的故事
给大家讲一个之前我经历过的真实故事,里面涉及到两个人物,我们用:领导 和 员工A 代替。
员工A是一个很懂 “人情世故” 的人,主要体现在两个方面:
- 酒桌文化:不像我这种压根就不能喝酒的人。员工A的酒量很好,并且各种喝酒的说法了熟于心(可以把领导说的很舒服的那种)
- 开会文化:各种反应都在领导的 “兴奋点” 上。我不知道怎么进行形容,类似于罗老师的这张图片,大家自己体会

其他方面的事情(私下的吃饭、逢年过节送礼),这些我就不清楚了,所以这里就不乱说了。
在我的眼里看来,这应该就是 熟通人情世故 的了。不知道,大家认为是不是。
不过,结果呢?
当公司决定裁员时,员工A 是 最早一批 出现在裁员名单中的。
领导会帮他争取留下来的机会吗?并不会
当你只能为对方带来 “情绪价值” 时,对方并不会把你当成心腹,更多的只是类似“马屁精”的存在。而这样的情绪价值,并没有太大的意义。更不要指望 领导会为了你做一些影响他自己利益,或者为他自己带来风险的事情了。
在自己没有价值前,就不要谈什么人情世故了!
国人在很多时候都会探讨 “人情世故” 的重要性。因为我生在 山东,对此更是感触颇深。(山东是受 儒家思想 熏陶最为严重的地方)。甚至,在之前,我也一度认为 “人情世故” 是非常重要的一件事情。
但是,当我工作之后进入企业以来。我越来越发现,在企业之中,所谓的 “人情世故” 并没有太大的意义。
人都是非常现实的,别人对你的看法,完全取决于你能为对方带来什么价值。
而这个价值通常需要体现在 金钱上 或者 事业上!
当你无法在这两个方面为对方提供价值时,那么你做的所谓的 “人情世故” 也会被对方认为是“马屁精”的嫌疑。
所以,与其把精力放到所谓的“人情世故”中,甚至为此而感到苦恼(就像开头所提到的同学一样),是 大可不必 的!
在你无法为对方带来价值之前,先努力提升自己的的能力,当你可以和对方处于一个平等的位置进行交流时,再去谈所谓的人情世故,也不迟!
来源:juejin.cn/post/7393713240995676175
去寺庙做义工,有益身心健康
《乔布斯传》中写到:乔布斯把对事物专注的能力和对简洁的热爱归功于他的禅修。他说:“禅修磨炼了他对直觉的欣赏能力,教他如何过滤掉任何能分散时间和精力的其它不必要的事情,在他的身上培养出了专注基于至简主义的审美观。”
如何在当今物欲横流的浮躁社会里不沦陷其中?如何在每天奔波忙碌之后却不内心疲惫、焦虑?如何在巨大的工作与生活压力下保持一颗平和的心?如何在经历感情、友情和亲情的起起落落后看破放下?如何改变透支健康和生命的人生模式?
程序员无疑是一个高压的职业,尤其是在头部公司工作的程序员们,工作压力更是大。并且在互联网行业,禅修并不是一件新鲜事。我们不一定要正儿八经地参加禅修活动,只是去寺庙走一走,呼吸一下新鲜空气,给寺庙干点活,对身心健康的帮助也会很大。
我与寺庙
我最早接触寺庙是在2011年上军校的时候,我的一个老师,作为大校,经常在课上分享他周末在南京附近寺庙的奇闻轶事,也会分享他自己的一些人生体验和感悟,勾起了我对寺庙生活的向往。
2013年,作为现役军人,我跑到了江西庐山的东林寺做了一个礼拜的义工,在那里,每天早上四点起床早上寺庙早课,负责三餐的行堂,也作为机动义工,干一些杂活,比如卸菜、组装床等,晚上有时也可以听寺庙的传统文化课。
2013年底,我申请退出现役,于是14年春就可以休假了,根据流程不定期去各部门办理手续即可,期间一个周末,我弟带我去凤凰岭玩,偶遇一个皈依法会,为了能看到传说中的北大数学天才,我填了一个义工表,参加了皈依仪式。
因为没有考虑政府安排的工作,所以打算考个研,期间不时会去凤凰岭的寺庙参加活动。考完研后,到18年春季,周末节假日,基本都会去这个寺庙做义工,累计得有200天以上。
期间,作为骨干义工,参与了该寺庙组织的第二至第四届的IT禅修营,负责行堂、住宿和辅导员等相关的工作。
很多人都听过这样一件往事:2010年,张小龙(微信之父)偶然入住一个寺院,当时正是微信研发的关键时刻,因为几个技术难题,张小龙连续几天彻夜难眠,终于一气之下把资料撕得粉碎。
没想到负责打扫卫生的僧人看到后,竟然帮他把资料重新粘贴了起来,还顺手写下了几条建议。张小龙非常惊讶,打听过后才知道这位扫地僧出家前曾混迹IT界,是个著名的极客。
经扫地僧点化,张小龙回到广州苦攻一年后,微信终于大成。这件事传的很广也很玄乎,可信度不会太高,不过故事中张小龙入住的寺院,就是我常去的寺庙。
至于在故事中懂得IT的扫地僧,在这里遇到其实不是什么奇怪的事,你还有可能遇到第47届国际数学奥赛金牌得主贤宇法师,他来自北大数学系;或者是禅兴法师,他是清华大学流体力学博士;又或者贤启法师,他是清华大学核能和热能物理博士。
“扫地只不过是我的表面工作,我真正的职业是一位研究僧。” 《少林足球》这句台词的背后,隐藏着关于这个寺庙“高知僧团”的一个段子。
因为各种不可描述的原因,18年9月之后,我就很少去这个寺庙了,但我知道我依然很向往寺庙的生活。于是22年春,我下定决心离开北京去深圳,其中就有考虑到深圳后,可以去弘法寺或弘源寺做义工。
去了一次弘法寺,感觉那边人太多,后面去了一次弘源寺后,感觉这里比较适合我,人少很安静,不堵车的话,开车只需20分钟到家。
目前,只要我有时间,我都会去弘源寺干一天临时义工,或者住上几天。
何为禅?
禅,是心智的高度成熟状态。直至印度词汇传入,汉语音译为“禅那”,后世简称为“禅”,汉译意思有“静虑”、“思维修”等。
禅修的方法就是禅法,禅法是心法,并不固着于某种具体形式,也不限于宗教派别。从泛义来说,任何一种方法,如果能够让你的心灵成熟,了解生命的本质,让心获得更高层次的证悟,并从而获得生命究竟意义的了悟。这样的方法就是禅修!
从狭义来说,在绵延传承数千年的漫长时空里,形成各种系统的修行方法,存在于各种教派中。现存主要有传承并盛行于南传佛教国家的原始佛教禅法与传承并盛行于中国汉传佛教的祖师禅。
如来禅是佛陀的原始教法,注重基础练习,强调修行止观。祖师禅是中国禅宗祖师的教法,强调悟性、觉性,推崇顿悟,以参话头为代表,以开悟、明心见性为目的。
我们普遍缺乏自我觉察,甚至误解了包括自由在内的生命状态真义。禅修中,会进入深刻自我觉察中,有机会与自己整合,从而开启真我。
近年来,禅修在西方非常流行,像美国的学校、医疗机构和高科技公司都广泛地在进行打坐、禅修。美国有些科学家曾做过一个实验,实验对象是长期禅修的修行人。在实验室中,实验者一边用脑电波图测量脑波的变化,一边用功能性核磁共振测量脑部活动的位置。
最后得出结论:通过禅修,不但能够短期改变脑部的活动,而且非常有可能促成脑部永久的变化。这就是说:通过禅定,可以有效断除人的焦虑、哀伤等很多负面情绪,创造出心灵的幸福感,甚至可以重塑大脑结构。
禅修能够修复心智、疗愈抑郁、提升智慧,让我们重获身心的全面健康!禅修让人的内心变得安静。在禅修时,人能放松下来,专注于呼吸,使内心归于平静,身体和心灵才有了真正的对话与接触。
“禅修是未来科技世界中的生存必需品”时代杂志曾在封面报道中这样写道。在硅谷,禅修被认为是新的咖啡因,一种能释放能量与创造力的全新“燃料”。
禅修也帮助过谷歌、Facebook、Twitter高管们走出困惑,国内比较知名则有搜狐的张朝阳和阿里的马云,还有微信之父张小龙的传说。
对于他们来说,商海的起伏伴随着心海的沉浮,庞大的财富、名声与地位带来的更多的不是快乐,但是禅修,却在一定程度上给他们指点迷津,带领他们脱离现代社会的痛苦、让内心更加平静!
乔布斯的禅修故事
乔布斯和禅修,一直有着很深的渊源。乔布斯是当世最伟大的企业家之一,同时也是一名虔诚的禅宗教徒。他少有慧根,17岁那年,他远赴印度寻找圣人寻求精神启蒙,18岁那年,他开始追随日本禅师乙川弘文学习曹洞宗的禅法。
年轻的时候,乔布斯去印度,在印度体验,呆了七个月。乔布斯在印度干了些什么,我们不得而知。不过据我推测,也就是四处逛一逛,看一看,可能会去一些寺庙,拜访一些僧人。
我从来不认为,他遇到了什么高人,或者在印度的小村庄一待,精神就受到了莫大的洗礼。变化永远都是从内在发生的,外在的不过是缘分,是过客,负责提供一个合适的环境,或者提供一些必要的刺激。
但我们知道,从此以后,乔布斯的人生,就开始变得不一样了。乔布斯的人生追求是“改变世界”,当年他劝说百事可乐总裁,来担任苹果CEO的时候所说的话:“你是愿意一辈子卖糖水,还是跟我一起改变这个世界?”激励了无数心怀梦想的朋友。
早在1973年乔布斯已经对禅有较深的领悟了。他这样说:“我对那些超越物质的形而上的学说极感兴趣,也开始注意到比知觉及意识更高的层次——直觉和顿悟。”
他还说:“因为时间有限,不要带着面具为别人而活,不要让别人的意见左右自己内心的想法,最重要的是要勇敢地忠于自己内心的直觉。”
乔布斯说:“你不能预先把点点滴滴串在一起;唯有未来回顾时,你才会明白那些点点滴滴是如何串在一起的。所以你得相信,你现在所体会的东西,将来多少会连接在一块。你得信任某个东西,直觉也好,命运也好,生命也好,或者业力。这种作法从来没让我失望,也让我的人生整个不同起来。”
他大学时学的书法,被他用来设计能够印刷出漂亮字体的计算机,尽管他在大学选修书法课时,完全不知道学这玩意能有什么用。他被自己创立的苹果公司开除,于是转行去做动画,结果在做动画的时候,遇到了自己未来的妻子。
人呐实在不知道,自己可不可以预料。你说我一个被自己创立的公司开除的失业狗,怎么就在第二份工作里遇到了一生的挚爱呢?若干年后,他回顾起自己的人生,他把这些点点滴滴串了起来,他发现,他所经历过的每一件事,都有着特殊的意义。
所以,无论面对怎样的困境,我们都不必悲观绝望,因为在剧本结束之前,你永远不知道,自己现在面对的这件事,到底是坏事还是好事。
所谓创新就是无中生有,包括思想、产品、艺术等,重大的创新我们称为颠覆。通过那则著名的广告《think different》他告诉世人:“因为只有那些疯狂到以為自己能够改变世界的人,才能真正地改变世界。”乔布斯确实改变了世界,而且不是一次,至少五次颠覆了这个世界:
- 通过苹果电脑Apple-I,开启了个人电脑时代;
- 通过皮克斯电脑动画公司,颠覆了整个动漫产业;
- 通过iPod,颠覆了整个音乐产业;
- 通过iPhone,颠覆了整个通讯产业;
- 通过iPad,重新定义并颠覆了平板PC行业。
程序员与禅修
编程是一门需要高度专注和创造力的艺术,它要求程序员们在面对复杂问题和压力时,能够保持内心的安宁和平静。在这个快节奏、竞争激烈的行业中,如何修炼内心的禅意境界,成为程序员们更好地发挥潜力的关键。
在编程的世界里,专注是至关重要的品质。通过培养内在专注力,程序员能够集中精力去解决问题,避免被外界的干扰所困扰。以下是几种培养内在专注的方法:
- 冥想和呼吸练习: 通过冥想和深呼吸来调整身心状态,让自己平静下来。坚持每天进行一段时间的冥想练习,可以提高专注力和注意力的稳定性。
- 时间管理: 制定合理的工作计划和时间表,将任务分解为小的可管理的部分,避免心理上的压力。通过专注于每个小任务,逐步完成整个项目。
- 限制干扰: 将手机静音、关闭社交媒体和聊天工具等干扰源,创造一个安静的工作环境。使用专注工作法(如番茄钟),集中精力在一项任务上,直到完成。
编程过程中会遇到各种问题和挑战,有时甚至会感到沮丧和失望。然而,保持平和的心态是非常重要的,它可以帮助程序员更好地应对压力和困难。以下是一些培养平和心态的技巧:
- 接受不完美性: 程序永远不会是完美的,因为它们总是在不断发展和改进中。接受这一事实,并学会从错误中汲取教训。不要过于苛求自己,给自己一些宽容和理解。
- 积极思考: 关注积极的方面,让自己的思维更加积极向上。遇到问题时,寻找解决方案而非抱怨。积极的心态能够帮助你更好地应对挑战和困难。
- 放松和休息: 给自己合理的休息时间,让大脑得到充分的放松和恢复。休息和娱乐能够帮助你调整心态,保持平和的状态。
编程往往是一个团队合作的过程,与他人合作的能力对于一个程序员来说至关重要。以下是一些建立团队合作意识和促进内心安宁的方法:
- 沟通与分享: 与团队成员保持良好的沟通,分享想法和问题。倾听他人的观点和建议,尊重不同的意见。积极参与和贡献团队,建立合作关系。
- 友善和尊重: 培养友好、尊重和包容的态度。尊重他人的工作和努力,给予鼓励和支持。与团队成员建立良好的关系,创造和谐的工作环境。
- 共享成功: 当团队取得成功时,与他人一起分享喜悦和成就感。相信团队的力量,相信集体的智慧和努力。
修炼内心安宁需要时间和长期的自我管理。通过培养专注力、平和心态、创造力和团队合作意识,程序员们可以在面对复杂的编程任务和挑战时保持内心的安宁和平静。
禅修有许多不同的境界,其中最典型的可能包括:
- 懵懂:刚开始禅修时,可能会觉得茫然和困惑,不知该如何开始。
- 困扰:在进行深度内省和冥想时,可能会遇到很多烦恼和难题,需耐心思考和解决。
- 安和:通过不断地练习和开放自己的心灵,可能会进入一种更加平和和沉静的状态。
- 祥和:当一些心理障碍得到解决,你会感受到一种更深层的平静和和谐。
- 转化:通过不断的冥想与内省,你可以向内看到自己的内心,获得对自己和世界的新的认识和多样的观察角度。
- 整体意识:通过冥想,您将能够超越个人的视野和言语本身,深入探究宇宙的内心,领悟更加深入和广泛的境界和意识。
程序员写代码的境界:
- 懵懂:刚熟悉编程语言,不知做什么。
- 困扰:可以实现需求,但仍然会被需求所困,需要耐心思考和解决。
- 安和:通过不断练习已经可以轻易实现需求,更加平和沉静。
- 祥和:全栈。
- 转化:做自己的产品。
- 整体意识:有自己的公司。
一个创业设想
打开小红书,与“疗愈”相关的笔记高达236万篇,禅修、瑜伽、颂钵等新兴疗愈方法层出不穷,无论是性价比还是高消费,总有一种疗愈方法适合你。
比起去网红景点打卡拍照卷构图卷妆造,越来越多的年轻人正在借助上香、拜神、颂钵、冥想等更为“佛系”的方式去追寻内心的宁静。放空大脑,呼吸之间天地的能量被尽数吸收体内,一切紧张、焦虑都被稀释,现实的残酷和精神的困顿,都在此间找到了出口。
在过去,简单的瑜伽和冥想就能达到这种目的,但伴随着疗愈文化的兴起与壮大,不断在传统方式之上叠加buff,才是新兴疗愈的终极奥义。
从目标人群来看,不同的禅修对应不同的人群。比如临平青龙寺即将在8月开启的禅修,就分为了企业禅修、教育禅修、功能禅修、共修禅、突破禅、网络共修等多种形式。但从禅修内容来看,各个寺庙的安排不尽相同,但基本上跳脱不出早晚功课、上殿过堂、出坡劳作、诵经礼忏、佛学讲座等环节。
艺术疗愈,是截然不同于起参禅悟道这种更亲近自然,还原本真的另一疗愈流派。具体可以细分为戏剧疗愈、绘画疗愈、音乐疗愈等多种形式。当理论逐渐趋向现代化,投入在此间的花费,也成正比增长。
绘画疗愈 ,顾名思义就是通过绘画的方式来表达自己内心的情绪。画幅的大小、用笔的轻重、空间的配置、色彩的使用,都在某种程度上反映着创作者潜意识的情感与冲突。
在绘画过程中,绘画者也同样会获得纾解和满足。也有一些课程会在绘画创作之外,添加绘画作品鉴赏的内容,通过一幅画去窥视作者的内心,寻求心灵上的共鸣,也是舒缓压力的一种渠道。
疗愈市场之所以能够发展,还是因为有越来越多人的负面情绪需要治愈。不论是工作压力还是亲密关系所带来的情绪内耗,总要有一个释放的出口。
当前,我正在尝试依托自营绘馆老师提供优质课件,打造艺培课件分享的平台或社区,做平台前期研发投入比较大,当前融资也比较困难,同时自己也需要疗愈。
所以,最近也在调研市场,评估是否可以依托自营的门店,组织绘画手工+寺庙行禅+技术专题分享的IT艺术禅修营活动,两天含住宿1999元,包括半天寺庙义工体验、半天禅修、半天绘画手工课和半天的技术专题分享。
不知道,这样的活动,大家会考虑参加吗?
总结
出家人抛弃尘世各种欲望出家修行值得尊重,但却不是修行的唯一方法,佛经里著名的维摩洁居士就是在家修行,也取得了非凡成就,六祖惠能就非常鼓励大家在世间修行,他说:“佛法在世间,不离世间觉,离世觅菩提,恰如求兔角”。
普通人的修行是在红尘欲望中的修行,和出家人截然不同,但无分高下,同样可以证悟,工作就是他们最好的修练道场。禅学的理论学习并不困难,但这只是万里长征的第一步,最重要的是,我们要在日常实践中证悟。
简单可能比复杂更难做到:你必须努力理清思路,从而使其变得简单。但最终这是值得的,因为一旦你做到了,便可以创造奇迹。”乔布斯所说的这种专注和简单是直接相关的,如果太复杂,心即散乱,就很难保持专注,只有简单,才能做到专注,只有专注,才能极致。
来源:juejin.cn/post/7292781589477687350
程序员的副业发展
前言
之前总有小伙伴问我,现在没有工作,或者想在空闲时间做一些程序员兼职,怎么做,做什么,能赚点外快
因为我之前发别的文章的时候有捎带着说过一嘴我做一些副业,这里就说一下我是怎么做的,都做了什么
希望能对你有些帮助~
正文
学生单
学生单是我接过最多的,已经写了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,然后会要求几张表,以及主从表有几对,这就看客户具体要求了
需要注意的点:大部分你得给客户配环境,跑程序,还是就是毕设一般是要求论文的,有论文的会比单纯程序赚的多,但是一定要注意对方是否要求查重,如果要求查重,一般是不建议接的,一般都是要求维普和知网查重,会要了你的老命。还有需要注意的是,学生单子一般是需要答辩的,你可以选择是否包答辩,当然可以调整价格,但是你一旦包答辩,你的微信在答辩期间就不会停了。你永远不知道他们会有怎样的问题
商业单
商业单有大有小,小的跟毕设差不多,大的需要签合同
我接的单子大致就一种,小程序+后台管理+后端,也就是一个大型的全栈项目,要比学生单复杂,而且你还要打包、部署、上线,售后,有一个周期性,时间也比较长




为了防止大家不信,稍微放几个聊天记录,是这两个月来找的,也没有给自己打广告,大家都是开发者,开发个小程序有什么打广告,可吹的(真的是被杠怕了)
技术栈有两种情况:自己定,客户定
UI也有两种情况:有设计图的、无设计图的(也就是自己设计)
基本上也就是两种客户:懂技术的客户,不懂技术的客户
指定技术栈的我就不说了,对于不指定技术栈的我大致分为两种
小程序端:uni/小程序原生、后台:vue、后端:云开发小程序端:uni/小程序原生、后台:vue、后端:springboot
这取决于预算,预算高的就用springboot、预算不高的就云开发一把嗦,需要说的是并不是说云开发差,其实现在云开发已经满足绝大部分的需求,很完善了,而springboot则是应用广泛,客户后期找别人接手更方便
对于没有UI设计图的,我会选择去各种设计网站去找一些灵感
当项目达到一定金额,会签署合同,预付定金,这是对双方的一种保障
其实在整个项目中比较费劲的是沟通,不是单独说与客户的沟通,更多的是三方沟通,作为上线的程序,需要一些资料手续,这样就需要三方沟通,同时还有一定的周期,可能会被催
讲解单
当然,有的时候人家是有程序的,可能是别人代写的,可能是从开源扒下来的,这个时候客户有程序,但是看不懂,他们可能需要答辩,所以会花钱找人给他们梳理一下,讲一讲, 这种情况比较简单,因为不需要你去写代码,但是需要你能看懂别人的代码
这种情况不在少数,尤其是在小红书这种单子特别多,来钱快,我一般是按照小时收费



知识付费这东西很有意思,有时候你回答别人的一些问题,对方也会象征性地给你个几十的红包
接单渠道
我觉得相对于什么单,大家更在意的是怎么接单,很多人都接不到单,这才是最难受的
其实对此我个人并没有太好的建议的方法,我认为最重要的,还是你的交际能力,你在现实中不善于交际,网络上也不善于交际,那就很难了
因为我之前是在学校,在校期间干过一些兼职,所以认识的同学比较多,同时自身能力还可以,所以会有很多人来找,然后做完之后,熟人之间会慢慢介绍,人就越来越多,所以我不太担心能否接单这件事,反而是单太多,自己甚至成立一个小型工作室去接单
如果你是学生的话,一定要在学校积累客户,这样会越来越多,哪怕是现在我还看到学校的各种群天天有毕业很多年以及社会人士来打广告呢,你为什么就不可以呢
当然但是很多人现在已经不是学生了,也不知道怎么接触学生,那么我给大家推荐另外的道路
闲鱼接单小红书接单
大部分学生找的写手都会比较贵,这种情况下,很多学生都会选择去上面的两个平台去货比三家,那么你的机会就来了
有人说不行啊,这种平台发接单帖子就被删了,那么你就想,为什么那么多人没被删,我也没被删,为什么你被删除了
其次是我最不推荐的一种接单方式:tb写手
为什么不推荐呢,其实就是tb去接单,然后会在tb写手群外包给写手,也就是tb在赚你的差价
这种感觉很难受,而且赚的不多,但是如果你找不到别的渠道,也可以尝试一下
最后
我只是分享一下自己接单的方式,但是说实在的,接一个毕设单或者是商业单其实挺累的,不是说技术层面的,更多的是心累,大家自行体会吧,而且现在商场内卷严重,甚至有人200、300就一个小程序。。。
所以大家要想,走什么渠道,拿什么竞争
另外,像什么猪八戒这种的外包项目的网站,我只是见过,但是没实际用过,接过,所以不好评价
希望大家赚钱顺利,私单是一种赚钱的方式,但是是不稳定的,一定还是要以自己本身的工作为主,自行判断~
来源:juejin.cn/post/7297124052174848036
小程序和h5有什么差别
差别
微信小程序和H5应用在实现原理上的差异主要体现在架构、渲染方式、数据通信、运行环境和API接口等方面。以下是详细的对比:
1. 架构和运行环境
微信小程序:
架构:微信小程序主要分为逻辑层(JavaScript)和视图层(WXML、WXSS)。逻辑层运行在小程序的JSCore中,而视图层运行在WebView(它是基于浏览器内核重构的内置解析器,它并不是一个完整的浏览器,官方文档中重点强调了脚本内无法使用浏览器中常用的
window对象和document对象,就是没有DOM和BOM的相关的API,这一条就干掉了JQ和一些依赖于BOM和DOM的NPM包)中,两者通过平台提供的桥接机制进行通信。运行环境:逻辑层在微信提供的JS引擎中运行,视图层在微信内置的WebView中渲染。
H5 应用:
架构:H5应用是一个整体。HTML、CSS和JavaScript共同构成了一个Web页面。
运行环境:H5应用在浏览器中运行,所有代码都在浏览器的环境中解析和执行。
2. 渲染方式
微信小程序:
微信小程序采用双线程模型,将逻辑层和视图层分离,分别运行在不同的线程中(两者通过平台提供的桥接机制进行通信):
逻辑层:运行在小程序的JSCore环境中,负责处理业务逻辑、数据计算和API调用。
视图层:运行在WebView中,负责渲染用户界面和处理用户交互。( 性能提升:由于小程序的渲染过程并不依赖于JS,因此即使JS线程发生阻塞,页面的渲染也不会受到影响。这种机制有利于提高渲染效率,减少卡顿,提升用户体验。)
通信桥接机制
逻辑层和视图层之间不能直接访问和操作对方的数据和界面,因此需要通过微信小程序框架提供的桥接机制来进行通信。这种通信机制通常包括以下几个方面:
1. 数据绑定和响应式更新(逻辑层--->视图层)
逻辑层通过数据绑定的方式将数据传递给视图层,视图层根据数据变化自动更新界面。数据绑定的过程如下:
设置数据:逻辑层通过
Page或Component实例的setData方法,将数据传递给视图层。更新视图:视图层接收到数据变化的消息后,根据新的数据重新渲染界面。
2. 事件处理(视图层--->逻辑层)
视图层中的用户交互(如点击、输入等)会触发事件,这些事件通过桥接机制传递给逻辑层进行处理。事件处理的过程如下:
事件绑定:在视图层(WXML)中定义事件处理函数。
事件触发:用户在界面上进行交互时,触发相应的事件。
事件传递:视图层将事件信息通过桥接机制传递给逻辑层。
事件处理:逻辑层的事件处理函数接收到事件信息,执行相应的业务逻辑。
3. 消息传递
逻辑层和视图层之间的通信实际是通过消息传递的方式实现的。微信小程序框架负责在两个层之间传递消息,包括:
逻辑层到视图层的消息:如数据更新、视图更新等。
视图层到逻辑层的消息:如用户交互事件、视图状态变化等
通信桥接机制具体实现
依赖于微信小程序框架内部的设计和优化,开发者无需直接接触底层的通信细节。以下是桥接机制的一些关键点:
消息队列:逻辑层和视图层之间维护一个消息队列,用于存储待传递的消息。
消息格式:消息以JSON格式进行编码,包含消息类型、数据内容等信息。
消息处理:逻辑层和视图层各自维护一个消息处理器,负责接收、解析和处理消息。
异步通信:消息传递通常是异步进行的,以确保界面和逻辑的流畅性和响应性
H5 应用:
H5应用的逻辑层和视图层通常是在同一线程(主线程)中运行,直接通过JavaScript代码操作DOM来更新界面。主要的通信方式包括:
直接DOM操作:通过JavaScript直接操作DOM元素,更新界面。
事件监听和处理:通过JavaScript监听DOM事件(如点击、输入等)并处理。
数据绑定:使用现代前端框架(如Vue.js、React.js)的数据绑定和响应式机制,实现视图的自动更新。
3. 数据通信
微信小程序:
通信机制:逻辑层和视图层之间的通信通过小程序框架提供的机制来实现,通常是通过事件和数据绑定。
后台通信:可以通过小程序提供的API与服务器通信,例如wx.request等。
H5 应用:
通信机制:页面内的通信可以通过DOM事件、JavaScript函数调用等方式实现。
后台通信:可以使用标准的AJAX请求、Fetch API、WebSocket等方式与服务器通信。
4. 运行机制
微信小程序
启动
如果用户已经打开过某小程序,在一定时间内再次打开该小程序,此时无需重新启动,只需将后台态的小程序切换到前台,整个过程就是所谓的
热启动如果用户首次打开或小程序被微信主动销毁后再次打开的情况,此时小程序需要重新加载启动,就是
冷启动
销毁
当小程序进入后台一定时间,或系统资源占用过高,或者是你手动销毁,才算真正的销毁
h5:解析HTML CSS形成DOM树和CSSOM树,两者结合形成renderTree,js运行,当然中间存在一系列的阻塞问题,还有同源策略等等
5. 系统权限方面(特定功能)
微信小程序依托于微信平台,能够利用微信提供的特有功能和API,实现许多H5应用无法直接实现或不易实现的功能,如微信支付、微信登录、硬件接口(如摄像头、麦克风、蓝牙、NFC等)、微信特有功能等。
6.更新机制
h5更新后访问地址即可
微信小程序需要审核
开发者在发布新版本之后,无法立刻影响到所有现网用户,要在发布之后 24 小时之内才下发新版本信息到用户
小程序每次
冷启动时,都会检查有无更新版本,如果发现有新版本,会异步下载新版本代码包,并同时用客户端本地包进行启动,所以新版本的小程序需要等下一次冷启动才会应用上,当然微信也有wx.getUpdateManager可以做检查更新
7. 开发工具和调试
微信小程序:
开发工具:微信提供了专门的开发者工具,集成了调试、预览、上传等功能,方便开发者进行开发和测试。
调试:可以使用微信开发者工具进行实时调试,并提供丰富的日志和调试信息。
H5 应用:
开发工具:可以使用任何Web开发工具和IDE(如VS Code、WebStorm等),以及浏览器的开发者工具进行调试。
调试:依赖浏览器的开发者工具(如Chrome DevTools),可以进行断点调试、查看网络请求、分析性能等。
总结来说,微信小程序和H5应用在实现原理上的差异主要是由于它们的架构设计、运行环境和生态系统的不同。小程序依托于微信平台,提供了许多平台专属的优化和功能,而H5应用则更加开放和灵活,依赖于浏览器的标准和特性。
小程序为什么使用双层架构
微信小程序采用双线程架构的原因主要是为了优化性能和用户体验。双线程架构将逻辑层和视图层分离,使得业务逻辑处理和视图渲染在不同的线程中进行,从而提高了小程序的运行效率和响应速度。以下是采用双线程架构的具体原因和优势:
提高性能:
将逻辑处理和页面渲染分离到不同的线程中,可以避免互相干扰,提高整体性能。例如,在复杂的业务逻辑计算过程中,视图层仍然可以保持流畅的界面更新和响应。
逻辑层和视图层通过消息机制进行异步通信,可以避免阻塞和卡顿。这样即使逻辑层的操作较为耗时,也不会影响界面的即时响应。
安全性: 视图层无法直接操作逻辑层的数据和代码,这样可以避免一些潜在的安全风险和漏洞。
XSS
由于逻辑层和视图层分离,视图层不能直接执行逻辑层的JavaScript代码。这种隔离使得即使视图层(WXML)中存在注入的恶意代码,也不能直接影响逻辑层的数据和操作。
逻辑层和视图层之间的通信通过统一的API进行,传递的数据会经过平台的安全检查和过滤,进一步减少了XSS攻击的风险。
CSRF
小程序通过平台的统一API进行请求,这些请求包含了平台自动添加的安全令牌(如
session_key等),确保请求的合法性。由于逻辑层和视图层的分离,用户在视图层进行操作时,逻辑层的业务逻辑和数据处理经过平台的校验,减少了CSRF攻击的风险。
DOM篡改:视图层的DOM结构由WXML和WXSS定义,不能直接通过逻辑层的JavaScript代码进行操作,这种隔离减少了DOM篡改的可能性。
安全权限管理:小程序的API权限由平台统一管理和控制,开发者需要申请和用户授权后才能使用特定的API。
用户体验: 微信小程序在启动时可以并行加载逻辑层和视图层资源,减少初始加载时间,提升启动速度。同时,微信平台会对小程序进行预加载和缓存优化,进一步提升加载性能。
rpx
微信的自适应单位,可以根据屏幕宽度进行自适应。
在微信小程序中,1 rpx 表示屏幕宽度的 1/750,因此 rpx 和 px 的换算关系是动态的,基于设备的实际屏幕宽度。
作者:let_code
来源:juejin.cn/post/7389168680747614245
展开收起的箭头动画应该怎么做?
背景
我们在日常开发中,一定经常遇到折叠面板的开发,为了美观,我们经常会添加展开收起按钮,并且带有箭头旋转动画。
比如下面的几种情况
- 文字点击变化,且有箭头旋转动画

- 只有箭头动画


这几种情况的核心其实就是:点击箭头开始旋转,再点击箭头恢复初始位置。
如何实现
思路分析
要实现展开和收起箭头的旋转动画,我们可以使用 CSS 和 JavaScript。我们在点击按钮时,通过添加和移除 CSS 类,实现箭头的旋转动画。并且添加transition属性实现过渡效果。
代码实现
我们以第一种动画效果为例,先写基础代码
<template>
<div @click="open = !open">
<span>{{ open ? '收起' : '展开' }}</span>
<span>▼</span>
</div>
</template>
<script>
const open = ref(false)
</script>
现在我们点击按钮,只有文字会变化,箭头不会旋转

我们给按钮加一个动态类
<template>
<div @click="open = !open">
<span>{{ open ? '收起' : '展开' }}</span>
<span :class="{ rotate: open }">▼</span>
</div>
</template>
<script>
const open = ref(false)
</script>
<style scoped>
.rotate {
transform: rotate(180deg);
transition: transform 0.3s linear;
}
</style>
可以看到,展开的时候有动画,但是收起的时候是没有过渡效果的。

我们只需要加一个transition属性即可
<template>
<div @click="open = !open">
<span>{{ open ? '收起' : '展开' }}</span>
<span :class="{ rotate: open }" class="arrow">▼</span>
</div>
</template>
<script>
const open = ref(false)
</script>
<style scoped>
.arrow {
transition: transform 0.3s linear;
}
.rotate {
transform: rotate(180deg);
transition: transform 0.3s linear;
}
</style>
现在样式就ok了

html版本
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Arrow Rotation</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="container">
<button id="toggleButton">
<span id="arrow" class="arrow">▼</span>
</button>
</div>
<script src="script.js"></script>
</body>
</html>
css
/* styles.css */
.container {
text-align: center;
margin-top: 50px;
}
#toggleButton {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
outline: none;
}
.arrow {
display: inline-block;
transition: transform 0.3s ease;
}
.arrow.rotate {
transform: rotate(180deg);
}
js
// script.js
document.getElementById('toggleButton').addEventListener('click', function() {
const arrow = document.getElementById('arrow');
arrow.classList.toggle('rotate');
});
这种方式可以实现箭头在点击时的旋转动画效果。在实际项目中使用,我们也可以根据具体需求调整样式和逻辑。
来源:juejin.cn/post/7385132403025149989
如果iconfont停止服务了,我们怎么办

前言
个人一直都比较喜欢阿里大佬提供的一些组件服务什么的,这里就只说图标管理吧,最开始的时候我是使用的icomoon.io 后面发现http://www.iconfont.cn/ 在以前开发是时候我们为了图片的请求少点,提升网站的性能会把这些小图标做到一张图上面专业叫法是雪碧图,后来随着前端发展有了字体图标,不啰嗦了吧,回到正题,由于上次icofont官网挂了之后,导致我的项目无法进行正常迭代,我就决定要自己弄个简单的图标管理。
需求
一般使用需要的是把设计做好的图标或者其他地方下载的svg图标上传到服务器然后可以查看,并且打包成一个js文件加载到项目中。
准备
都说天下文章一大抄,我也是去抄iconfont,我把iconfont的使用demo打开研究了一下。


我这里只说Symbol,通过分析看到就是我们需要在项目中引入./iconfont.js

iconfont把我们上传的图标进行了删减优化,最外层就一个svg标签里面使用了一个symbol标签来包裹之前的图标内容,每一个symbol的id都对应之前的svg图标名称。
iconfont也没有开源,具体怎么做的咱也不知道,只能知道目前这些信息了,然后就按照自己理解开始开发实现了
前端开发
通过上面我们可以知道,现在我们前端需要的是把设计稿的svg图变成symbol里面的内容,打开svg图 可以简单测试下,直接把内容复制然后粘贴带了iconfont.js中,然后运行demo图标正常显示就说明是ok的,不然就是这样做是不行的。 
通过图片我们可以看到,现在需要的就是把svg里面的path改成用symblo来包裹,在开发中svg图我们是通过文档流上传上来的,这个时候我们就需要对文档流进行操作,先把文档流变成字符串,然后js操作字符串拼接达到目的。
- 使用到FileReader和readAsText获取到字符串
const fileParse = (file) => {
return new Promise((resolve) => {
const fileReader = new FileReader();
fileReader.readAsText(file, "UTF-8");
fileReader.onload = (e) => {
resolve({ content: e.target?.result, name: file.name })
}
})
}
- 字符操作拼接我使用的是cheerio
const handleUploadSvg = ($, result) => {
let index = result.indexOf('通过上面的操作我们可以得到一个新的svg源码,图标显示还是flow

然后我把这个字符串传送给后端就行了
后端开发
后端要做的就是把我上传上来的svg字符串拼接到一起,然后以iconfont.js一样通过一个get接口返回给我就可以了, 通过浏览器可以访问到就说明ok了

其他两种需要用到些第三方库当时也有顺便研究看到了,如果有需要我再写一篇。
文章中会有些不怎么清晰或者有问题的地方还望各位大佬见谅,刚刚开始决定写一些技术相关文章,还很生疏
来源:juejin.cn/post/7340197367515578378
iOS 开发们,是时候干掉 Charles 了
这里每天分享一个 iOS 的新知识,快来关注我吧
前言
一说到 mac 上的抓包工具,大家自然而然的会想到 Charles,作为老牌抓包工具,它功能很全面,也很强大。但是随着系统的不断更新迭代,Charles 的一些缺点也慢慢表露出来,比如:
- 卡顿,特别在一些低端 Mac 机型上比较卡,体验就很差
- 吃内存,时间久了总是得重启一下,不然内存吃的太多
- 页面老旧,感觉像是旧时代的产品
今天来介绍一个我觉得比较好用的抓包工具,Proxyman
Proxyman 配置
安装就不说了,大家可以自行去官网下载安装。
Proxyman 提供了一个免费版本,其中包含所有基本功能,平时使用应该是够了,如果重度使用,也可以考虑购买高级版本。
这是他的主页面,看起来是不是挺干净的:

安装好了之后都需要配置代理和 https 证书,这点 Proxyman 做的非常好,首先点击顶部导航上的证书,可以看到所有安装证书的选项:

教程是全中文的,而且设置步骤非常详细,比如 iOS 设置指南:

Proxyman 针对 iOS 开发还提供了一种无配置的方案,可以直接通过 Pod 或者 SPM 添加 atlantis-proxyman框架,这样可以在不进行任何配置的情况下进行代理监听:

除了监控手机的流量,也可以很方便地添加 iOS 模拟器的监控,只需要选择顶部菜单 -> 证书 -> 在 iOS 上安装证书 -> 模拟器:

按照以上步骤操作即可。
使用
配置完成之后就可以在 Proxyman 主页面上看到接口请求了,接下来介绍一些常用的功能。
本地 Mock 数据
本地 Mock 数据是很常见的需求,你只需要选中某个接口后,鼠标右键,选择工具 -> 本地映射:

然后在弹出的新页面中编辑相应即可,非常方便:

断点
断点工具可以让我们动态编辑请求或响应的内容。
它本地映射在同一个菜单栏里,鼠标右键,选择工具 -> 断点,然后进行对应的设置即可。
创建断点后,Proxyman 将在收到我们想要拦截的请求或响应后立即打开一个新的编辑窗口。然后我们根据需要修改数据,最后再继续即可。
导出请求和响应数据
有时候我们需要把有问题的接口保存下载给其他服务端的同学查看。选中具体的请求,点击鼠标右键,选择导出,然后再选择你要导出的格式:

不过这里导出的 Proxyman 日志需要使用 Proxyman 才能打开,也就是说,需要想查看这条请求的人的电脑上也安装 Proxyman,如果他没有安装,也可以选择拷贝 cURL。
模拟弱网
好的产品一定能够在弱网下正常使用,所以弱网测试也成为了日常开发必要的步骤,点击顶部菜单栏,选择工具 -> 网络状况,可以打开一个新页面,然后点击左下角为一个新的域名添加网络状况,这里可以根据你的需求选择不同的网络状况:

总结
从流畅度、功能引导等方面,我感觉 Proxyman 是比 Charles 好用的,除了以上介绍到的功能,还有很多更强大更全面的功能。例如远程映射、保存会话、GraphQL 调试、黑名单白名单、Protobuf、自定义脚本等等,大家可以自己试试看。
这里每天分享一个 iOS 的新知识,快来关注我吧
本文同步自微信公众号 “iOS新知”,每天准时分享一个新知识,这里只是同步,想要及时学到就来关注我吧!
来源:juejin.cn/post/7355845238906175551
Flutter 为什么没有一款好用的UI框架?
哈喽,我是老刘
前两天,系统给我推送了一个问题。

我理解提问者真正想问的是:有没有一个不用学习那么多UI组件和渲染知识,可以简单快速搭建UI的东西。
Flutter 包括原生开发,为什么需要考虑那么多细节,不能做的简单一些?
首先,我们需要明白Flutter的定位。
Flutter不是一个简单的甜品,而是一个能支撑大型系统开发的工程级框架。
这种定位和原生框架的定位是相当的。
因此,它要求整个框架有足够的灵活性,能适用于尽可能多的场景。

那么,如何提供足够的灵活性呢?
答案是让整个框架尽可能多的细节是可控的。
这就需要把整个框架的功能拆分的更细,提供的配置项足够多。
然而,这样的缺点就是开发起来会比较麻烦,需要控制很多细节。
因此,我们可以看到Flutter的组件拆分的很细,甚至有类似Padding这样专门负责缩进的组件,而且每个组件都有很多的配置参数。
Flutter配合Material组件库本身本就非常优秀的UI框架
虽然Flutter的灵活性带来了开发上的复杂性,但Flutter配合Material组件库本身就是一个非常优秀的UI框架。

Material组件库提供了丰富的预设组件,这些组件遵循Material Design指南,可以帮助开发者快速搭建出既美观又符合设计规范的UI界面。
使用Material组件库,开发者可以不必从头开始设计每一个UI元素,而是可以直接使用现成的组件,如按钮、对话框、卡片等,这些组件都有良好的交互和动画效果。
此外,Material组件库还提供了主题支持,开发者可以通过简单的配置,快速应用统一的风格到整个应用中。
因此,虽然Flutter的灵活性可能让初学者感到有些复杂,但配合Material组件库,Flutter实际上提供了一个非常高效和优秀的UI开发体验。
大型项目的正确打开方式
即便是Material组件库,它的设计是需要考虑应对各种不同类型app开发的,但是针对一个具体的项目,我们大多数时候不需要这样高的灵活性。
所以,这种情况下直接用Flutter提供的组件效率会比较低。
解放方法就是针对特定的项目做组件封装。
以我目前维护的项目为例,我们项目中所有的对话框都是相同的偏绿色调,圆角半径20,按钮大小固定,标题、详情的字体、字号也固定。
简单来说,就是所有的UI细节都是固定的,只是不同的dialog需要填充的文字不同。
这时候,我们就会定义一个自己的Dialog组件,只需要使用者传入标题和内容,以及设置按钮的回调即可。
UI的其他地方也是如此,比如页面框架、在多个页面都能用到的用户卡片、商品卡片等等。
当你的整个App大部分都是基于这些自定义组件进行搭积木式的开发,那开发效率是不是比找一些通用的UI框架更高呢?
总结
总而言之,Flutter因为它的工程级框架定位需要提供高度的灵活性,而这往往会导致开发细节的复杂性。
但是,通过针对具体项目的组件封装,我们可以大大提高开发效率,同时保持UI的一致性和项目的特定需求。
所以,与其寻找一个通用的UI框架,不如根据项目的具体需求进行自定义组件的开发。
如果看到这里的同学有学习Flutter的兴趣,欢迎联系老刘,我们互相学习。
点击免费领老刘整理的《Flutter开发手册》,覆盖90%应用开发场景。
可以作为Flutter学习的知识地图。
覆盖90%开发场景的《Flutter开发手册》
来源:juejin.cn/post/7387001928209170447
劝互联网的牛马请善待自己
掘友们,大家好,我是一名95后全栈程序媛,一直以来在努力追求WLB,28岁前完成了畅游中国,既努力生活也认真工作。很多人可能还不知道WLB这个词,WLB就是work life balance,一开始我看到这个词都是从猎头那里传过来的,岗位招聘一般都是:xxx神仙外企WLB,一周只上3天班,每周两天可以居家办公,每年年假几十天,要求英语口语流利,有x年工作经验,base范围在xx个w....
众所周知,外企的年包肯定不如那些一线爆肝厂,当然工作时间跟收入都是成正比的,965的外企跟11116的互联网的年包肯定是不一样的,付出的工作时间都不一样。假如拼夕夕给你年薪百万,神仙外企给你年薪50+w,你会怎么选?
最近出来几条消息刺痛了牛马的心情!这个世界变幻莫测~
四十多岁的程序员在公司工作11年被裁员
徐峥出新电影了《逆行人生》讲述的就是一个四十多岁的程序员被裁员后找不到工作只能去送外卖的心酸故事,当现实照进电影,卑微的打工人在时代的潮流下只是一个渺小颗粒的缩影。


得物“35岁被暴力裁员”、“80余万元期权直接打水漂”。
一年前,面临裁员的得物员工徐凯多次与公司沟通取得期权再离职未果后,他到上海市仲裁委员会处申请恢复与得物的劳动关系,后被予以支持。7月,因不服上海市仲裁委员会裁定的结果,得物继续上诉,再度将前员工诉于法庭之上。
去哪儿宣布每周可居家办公两天
这则消息意味着互联网公司开启新的里程碑,向神仙外企的福利看齐了,对于老弱病残的打工人简直不要太友好了。

这种待遇,以前在互联网几乎不存在的。一周连休四天的日子,体验过就不再想去卷996的牛马岗了。
不管我们有多努力,我们都只是老板眼里赚钱的工具人
在互联网,35岁已经是一道坎,人在互联网漂,哪有不挨刀,不管有多努力,到了大龄的年纪,工资比年轻人高产出比年轻人低的时候,面临着公司随时都可能会说:分手吧,没有分手费,你自己知难而退吧,大家好聚好散!像极了一个渣男遇到了更年轻漂亮的白富美抛弃糟糠之妻,完了还pua你说都是你的错我才选择了别人。同样,公司会pua说,都是你的没能力,我才选择了别的员工,渣男有道德的谴责?公司有吗?公司跟你只有劳动关系,只要合法,随时跟你说你被毕业了,给个n+1的分手费都要被牛马说这渣渣企真良心!
对于老板来说,赚钱的时候大家都是兄弟,不赚钱了不认兄弟,说好聚好散!你把公司当家,公司把你当牛马。这点,我们真的要向00后学习,提前认清职场,打工就是为了赚钱,为了更好的生活,并不是为了努力加班毁了我们的生活,那我们辛辛苦苦打工有什么意义呢?
是否真的对自己的选择满意

知乎上有一个热度很高的话题:阿里p7和副处级哪个更厉害?
总说纷云,有人选择p7:

有人选择为人民服务;

也有人两者都想要:

在稳定和高薪面前,大家都想要稳定高薪的工作,最后变成稳定焦虑的牛马,这就好像围城,体制内的羡慕体制外的高薪,高薪的牛马羡慕体制内的稳定。即使义无反顾选择了卷互联网,几年挣够了人家一辈子的钱,但是买了二居想换三居,买了三居想换别墅,收入的增长带来消费的提升,物欲的无限放大,依然很多年入百万的人并不觉得真正的快乐而满足现状!即使选择了稳定的体制内,工作体面生活稳定,但是在权力面前,一直追名逐利,在很多诱惑下,最后的选择身不由己!
所以,欲望面前,你有好好认真的生活吗?认真对待自己的身体健康吗?是为了碎银几两熬夜加班把身体搞垮还是为了三餐有汤就行选择WLB呢?希望每一个焦虑的互联网牛马都好好善待自己,平衡好自己身体的健康和对金钱物欲的追逐。
我最羡慕内心富裕,内核稳定的人,这种人一般要比同龄人状态更年轻。不容易被外界所干扰,明确知道自己该要什么,不该要什么,选择适合自己的生活,幸福满意度极高。
来源:juejin.cn/post/7390457313163067431
买房后,害怕失业,更不敢裸辞,心情不好就提前还房贷,缓解焦虑
自从买房后,心态有很大变化。虽然住自己的房子,心情和体验都很好,但是一把掏空钱包,很焦虑。买房后现金流一直吃紧,再加上每年16万的房贷,我很焦虑会失业。之前我喜欢裸辞,现在不敢想裸辞这个话题。尤其是在行业下行期,找工作很艰难,背着房贷裸辞,简直是头孢就酒,嫌命太久。
焦虑的根源是背负房贷,金额巨大,而且担心40岁以后失业,还不上房贷。
一次偶然的沟通
"你的带款利率调整了吗",同事问我。
同事比我早两年在北京买房,他在顺义买的,我在昌平买的,我俩一直有沟通房贷的问题。但我没听说利率有调整,银行好像也没通知我,于是我问道:”我不知道啊,你调整了?调到多少了?“。
”调的挺多的,已经降到了 4.3%“。同事兴高采烈的回复我。
”这么牛逼,之前我记得一直是4.85%,我去看看我的利率“,我听说房贷利率下降那么多,很是兴奋。
然而我的房贷利率没有调整,我尝试给银行打电话,沟通的过程很坎坷。工商银行客服说了很多,大概意思是:利率会自动调整,无需申请,但是要等到利率调整日才会调整”。我开始很不理解,很生气,利率都调整了,别人也都调整了,凭什么不给我调整呢?
我想到同事有尝试提前还贷,生气的时候,我就萌发了提前还贷的想法。
开始尝试提前还贷,真香
我在22年初带款买房,其中商业带款 174 万,带款25年,等额本息,每个月要还 1 万的房贷。公积金带款每个月大概需要还 2500。每个月一万二的房贷还是很有压力的,尤其是刚买房的这一两年,兜里比脸都干净,没存款,不敢失业,更不敢裸辞。
即便兜里存款不多,也要提前还贷,因为实在太香了。
我在工行App上,申请 提前还贷,选择缩短 18个月的房贷,只需要 6万2,而我每个月房贷才1万,相当于是用 6 万 顶 18 万的房贷。还有比这更划算的事情吗?

预约提前还款后,银行会安排一个时间,在这个时间前,把钱存进去。到时候银行就会扣除,如果扣除金额不足,那么提前还款计划则自动终止,需要重新预约!
工行的预约还款时间大概是1个月以后,我是10-15号申请提前还款,银行给的预约日期是 11-14号,大概是1个月。
提前还款,比理财强多了
这次还贷以后,我又申请了提前还款, 提前还 24 期,只需要 9 万,也就是 9 万顶 24 万;提前还 60 期,只需要 24 万,相当于 24 万顶 60 万。

还有比提前还贷收益更高,风险更低的理财方式吗?没有! 除了存款外,任何理财都是有风险的。债券和基金收益和风险挂钩,想找到收益5%的债券基金,要承担亏本风险。你惦记人家的利息,人家惦记你的本金!
股票的风险更不必说,我买白酒股票已经被套的死死,只能躺平装死。(劝大家不要入 A 股)
提前还贷划算吗?
我目前的带款利息是 4.85%,而存到银行的利息不会超过 3% ,很多货币基金只有 2%了。两者利息差高达 3%,肯定是提前还带款更加合适。
要明白,一年两年短期的利息差还好,但是房贷可是高达 25 年。25年 170 万带款 3% 的利息差,这个金额太大了。提前还了,省下来的钱还是很多的。例如刚才截图里展示的 提前还 24 万顶了 60 万的房贷。
网上很多砖家说,“要考虑通货膨胀因素,4.85% 的带款利率和实际通货膨胀比起来不高,提前还款不划算。”
砖家说话都是昧良心的。提前还带款是否划算,只需要和存款利率比就行了,不需要和通货膨胀比。因为把钱存在银行也会因为通货膨胀贬值。只有把钱 全都消费,全部花光才不会受通货膨胀的困扰,建议砖家,多消费,把家底败光,这样最划算!
砖家们一定是害怕太多人提前还贷,影响了银行的放贷生意。今年上半年,提前还贷已经成潮流,有些银行坐不住,甚至关闭了提前还贷的入口…… 所以要抓紧,没准哪天就提高了还贷门槛,或者直接禁止。
程序员群体收入高,手里闲钱多,可以考虑提前还带款,比存银行划算多了,别再给银行打工了!
来源:juejin.cn/post/7301530293378727971
刚入职因为粗心大意,把事情办砸了,十分后悔
刚入职,就踩大坑,相信有很多朋友有我类似的经历。
5年前,我入职一家在线教育公司,新的公司福利非常好,各种零食随便吃,据说还能正点下班,一切都超出我的期望,“可算让我找着神仙公司了”,我的心里一阵窃喜。
在熟悉环境之后,我趁着上厕所的时候,顺便去旁边的零食摊挑了点零食。接下来的一天里,我专注地配置开发环境、阅读新人文档,当然我也不忘兼顾手边的零食。
初出茅庐,功败垂成
"好景不长",第三天上午,刚到公司,屁股还没坐热。新组长立刻给我安排了任务。他决定让我将配置端的课程搜索,从使用现有的Lucene搜索切换到ElasticSearch搜索。这个任务并不算复杂,然而我却办砸了。
先说为什么不复杂?
- ElasticSearch的搜索功能 基于Lucene工具库实现的,两者在搜索请求构造方式上几乎一致,在客户端使用上差异很小。

- 切换方案无需顾虑太多稳定性问题。由于是配置端课程搜索,并非是用户端搜索,所以平稳切换的压力较小、性能压力也比较小。
总的来说,领导认为这个事情并不紧急,重要性也不算高,而且业务逻辑相对简单,难度能够把握,因此安排我去探索一下。可是,我却犯了两个错误,把入职的第一件事办砸了。现在回过头来看,十分遗憾!

难以解决的bug让我陷入困境
将搜索方式从Lucene切换为ElasticSearch后,如何评估切换后搜索结果的准确度呢?
除了通过不断地回归测试,还有一个更好的方案。
我的方案是,在调用搜索时同时并发调用Lucene搜索和ElasticSearch搜索。在汇总搜索结果时,比对两者的搜索结果是否完全一致。如果在切换搜索引擎的过程中,两个方案的搜索结果不一致,就打印异常搜索条件和搜索结果,并进行人工排查原因。

在实际切换过程中,我经常遇到搜索数据不一致的情况,这让我感到十分苦恼。我花了一周的时间编写代码,然后又用了两周多的时间来排查问题,这超出了预估的时间。在这个过程中,我感到非常焦虑和沮丧。作为一个新来的员工,我希望能够表现出色,给领导留下好印象。然而事与愿违,难以解决的bug让我陷入困境。
经过无数次的怀疑和尝试,我终于找到了问题的根源。原来,我忘记了添加排序方式。
因为存在很多课程数据,所以配置端搜索需要分页搜索。在之前的Lucene搜索方式中,我们使用课程Id来进行排序。然而在切换到新的ElasticSearch方案中时,我忘记了添加排序方式。这个错误的后果是,虽然整体上结果是一致的,但由于新方案没有排序方式,每一页的搜索结果是随机的,无法预测,所以与原方案的结果不一致。
新方案加上课程Id排序方式以后,搜索结果和原方案一致。
为此,我总结了分页查询的设计要点!希望大家不要重复踩坑!# 四选一,如何选择适合你的分页方案?
千万不要粗心大意
实际上,在解决以上分页搜索没有添加排序方式的问题之后,还存在着许多小问题。而这些小问题都反映了我的另一个不足:粗心大意。
正是这些小问题,导致线上环境总会出现个别搜索结果不一致的情况,导致这项工作被拖延很久。
课程模型是在线教育公司非常核心的数据模型,业务逻辑非常复杂,当然字段也非常多。在我入职时,该模型已经有120 个字段,并且有近 50 个字段可以进行检索。
在切换搜索方式时,我需要重新定义各种DO、DTO、Request等类型,还需新增多个类,并重新定义这些字段。在这个过程中,我必须确保不遗漏任何字段,也不能多加字段。当字段数量在20个以内时,这项工作出错的可能性非常低。然而,班课模型却有多达120个字段,因此出错的风险极大。当时我需要大量搬运这些字段,然而我只把这项工作看作是枯燥乏味的任务,未能深刻意识到出错的可能性极大,所以工作起来散漫随意,没有特别仔细校验重构前后代码的准确性。
墨菲定律:一件事可能出错时就一定会出错
墨菲定律是一种普遍被接受的观念,指出如果某件事情可能出错,那么它将以最不利的方式出错。这个定律起源于美国航天局的项目工程师爱德华·墨菲,在1950年代发现了这一规律。
墨菲定律还强调了人类的倾向,即将事情弄糟或让事情朝着最坏的方向发展。它提醒人们在计划和决策时要考虑可能出错的因素,并准备应对不利的情况。
墨菲定律实在是太准了,当你感觉某个事情可能会出错的时候,那它真的就会出错。而且我犯错不止一次,因为有120个字段,很多字段的命名非常相似,最终我遗漏了2个字段,拼写错误了一个字段,总共有三个字段出了问题。
不巧的是,这三个字段也参与检索。当用户在课程搜索页面选择这三个字段来进行检索时,因为字段的拼写错误和遗漏,这三个字段没有被包含在检索条件中,导致搜索结果出错……
导致这个问题的原因有很多,其中包括字段数量太多,我的工作不够细致,做事粗心大意,而且没有进行充分的测试……
为什么没有测试
小公司的测试人员相对较少,尤其是在面对课程管理后台的技术重构需求时,更加无法获取所需的测试资源!
组长对我说:“ 要人没有,要测试更没有!”

事情办砸了,十分遗憾
首先,从各个方面来看,切换搜索引擎这件事的复杂度和难度是可控的,而且目标也非常明确。作为入职后第一项任务,我应该准确快速地完成它,以留下一个良好印象。当然,领导也期望我能够做到这一点,然而事实与期望相去甚远。
虽然在线上环境没有出现问题,但在上线后,问题排查的时间却远远超出了预期,让领导对结果不太满意。
总的来说,从这件事中,我获得的最重要教训就是:对于可能出错的事情,要保持警惕。时刻用墨菲定律提醒自己,要仔细关注那些可能发生小概率错误的细节问题。
对于一些具有挑战性的工作,我们通常都非常重视,且在工作中也非常认真谨慎,往往不会出错。
然而,像大量搬运代码、搬运大量字段等这类乏味又枯燥的工作确实容易使人麻痹大意,因此我们必须提高警惕。要么我们远离这些乏味的工作,要么就要认真仔细地对待它们。
否则,如果对这些乏味工作粗心大意,墨菲定律一定会找上你,让你在线上翻车!
来源:juejin.cn/post/7295576148364787751
记一种不错的缓存设计思路
之前与同事讨论接口性能问题时听他介绍了一种缓存设计思路,觉得不错,做个记录供以后参考。
场景
假设有个以下格式的接口:
GET /api?keys={key1,key2,key3,...}&types={1,2,3,...}
其中 keys 是业务主键列表,types 是想要取到的信息的类型。
请求该接口需要返回业务主键列表对应的业务对象列表,对象里需要包含指定类型的信息。
业务主键可能的取值较多,千万量级,type 取值范围为 1-10,可以任意组合,每种 type 对应到数据库是 1-N 张表,示意:

现在设想这个接口遇到了性能瓶颈,打算添加 Redis 缓存来改善响应速度,应该如何设计?
设计思路
方案一:
最简单粗暴的方法是直接使用请求的所有参数作为缓存 key,请求的返回内容为 value。
方案二:
如果稍做一下思考,可能就会想到文首我提到的觉得不错的思路了:
- 使用
业务主键:表名作为缓存 key,表名里对应的该业务主键的记录作为 value; - 查询时,先根据查询参数 keys,以及 types 对应的表,得到所有
key1:tb_1_1、key1:tb_1_2这样的组合,使用 Redis 的 mget 命令,批量取到所有缓存中存在的信息,剩下没有命中的,批量到数据库里查询到结果,并放入缓存; - 在某个表的数据有更新时,只需刷新
涉及业务主键:该表名的缓存,或令其失效即可。
小结
在以上两种方案之间做评估和选择,考虑几个方面:
- 缓存命中率;
- 缓存数量、占用空间大小;
- 刷新缓存是否方便;
稍作思考和计算,就会发现此场景下方案二的优势。
另外,就是需要根据实际业务场景,如业务对象复杂度、读写次数比等,来评估合适的缓存数据的粒度和层次,是对应到某一级组合后的业务对象(缓存值对应存储 + 部分逻辑),还是最基本的数据库表/字段(存储的归存储,逻辑的归逻辑)。
来源:juejin.cn/post/7271597656118394899
有哪些事情,是当了程序员之后才知道的?
1、平庸的程序员占比很大。 还没参加工作时,觉得程序员是个改变世界的高科技职业,后来才发现,其实这个群体里有很多CRUD Boy和SQL Boy,Ctrl C+Ctrl V是我们使用最多的电脑操作,没有之一。
而且,大多数同事上班摸鱼偷懒,遇到问题躲着走,下班也从来不主动学习充电。每当我问他们,他们都说这样混着也挺好,不想太累。
2、数量堆死质量。 如果你觉得没有写代码的天赋,那么请你先写10万行代码再说。
如果你在刷leetcode的时候非常痛苦,甚至有时候看答案都看不懂。那你就先把代码背下来,然后一遍一遍默写。每当你默写五遍以上,就开始慢慢理解了,刷十遍以上,再遇到类似的题,就有条件发射,能举一反三了。
这种方法运用到看底层源码,看一些晦涩难懂的技术类书籍上,也同样适用。
后来,我在网上看了硅谷王川的一段话:所有的我们以为的质量问题,大多本质是数量问题。数量是最重要的质量。
而欧成效则说得更加直接:数量堆死质量!
3、尽量选择研发出身的老板的公司。 他们会知道程序员不是故意写bug的,也没有任何系统能做到100%的可用性。
而销售出身的老板,却永远把自家公司的程序员看做产出并不令人满意的高成本项。而且还时不时地要求程序员跟销售一样喊几句令人其鸡皮疙瘩的鸡血口号。
4、大厂和小厂的程序员,技术上差距并不大。 他们的差距也许是在学历上,也许是在人脉上,也许是在沟通和向上管理上。
5、对测试同学客气一点, 他们是你写的代码的最后一道防线。再有就是,如果线上出了故障或者严重bug,很多产研以外的人都关注是哪个程序员造成了事故,而不是哪个测试同学没测出来。
6、产品经理是SB,甲方是SB的N次方。 最令人蛋疼的是,任何一家公司都是这样,所以你根本避无可避,只能长期共存。
7、程序员涨薪,最好的方式是跳槽, 而不是兢兢业业地加班工作。如果就靠公司每年涨的那些钱,估计得用7,8年才能实现薪资翻番。但如果靠跳槽,估计3年就能实现薪资翻番。
8、能不去外包公司就尽量不去,那种寄人篱下的无归属感才最让人心累。你会发现,公司的正式员工吃饭和娱乐都是不愿意带你玩儿的,平时跟你说话的表情也是鼻孔朝天。
9、面试造火箭,工作拧螺丝是正常的。 你要做的就是提升造火箭吹牛逼的能力,毕竟这才是你定级谈薪的资本。不要抱怨,要适应。
10、35岁的危机真的存在。 那些认为技术牛逼就可以平稳度过中年危机的人,很多都SB了。人老不以筋骨和技术为能,顺势而为,尽早找后路才是王道。
11、尽量去工程师占比超过30%的公司,因为它的期权可能在未来十年内变得很有价值。因为工程师占比越高,边际成本就越低。
12、离开公司这个平台,也许你什么都不是。 很多大厂的高P前辈,甚至是总监、 VP,也可能在某一个时间点,突然被淘汰!我身边就有一个BAT的总监,真的就突然被优化了,真的就找不到哪怕一半的薪资了。突然之间!
拔剑四顾心茫然.... 所以,永远要分清楚哪些是平台资源,哪些是你的能力。时刻对自身能力保持清醒且准确的认知,千万不要陷入盲目自负的境地。实在太过乐观的大厂朋友,可以周期性出来面试,哪怕不跳槽,认知自己的真实价值。
13、技术面试官问期望薪资,记得往低了说。 因为他们往往并不负责最终的定薪,但如果你的期望薪资高于他,会让他产生强烈的不平衡,从而把你Pass掉。
14、身体才是一切的本钱。 前些天左耳朵耗子前辈的忽然离世,再次验证了这一点,如果身体健康是0,那么其他的所有一切都是0。
15、脱发和格子衫的说法,并不普遍。 我认识的程序员里,80%是不穿格子衫的,而且35岁+的程序员,80%也是不脱发的。
但是有一种东西是很普遍的,那就是装着电脑的双肩包。
16、PPT架构师、周报合并师、无损复读师真的存在,而且越是在大厂,这种人就会越多。
PPT架构师在PPT中讲的架构各种高端大气上档次,其实就是大家很常用的部署流程;周报合并师每周的任务就是每周将团队中每个人的周报进行汇总,再报告给上级;无损复读师要求可能会高一些,对老板提出的问题或者质疑,要原原本本的向下传达给项目组对应的同学,不能有一丝偏差。
或许他们最开始不是这样的,但是慢慢地,他们活成了最舒服的,也是曾经最讨厌的样子。
17、大多数程序员是不会修电脑的。 很多行业以外的人,他们会觉得很多事情程序员都可以做,从盗QQ,Photoshop,硬盘文件恢复,到装系统,处理系统故障和软件问题,安装各种盗版软件,各种手机的越狱Root装盗版应用。
并且,另外这些事情往往不涉及实物,给人的感觉是只是在键盘上打打字,又不需要买新硬件之类的,所以往往会被认为是举手之劳,理应帮忙。
18、杀死程序员不用枪,改三次需求就可以了。 很多程序员并不反感别人说他无趣,也不反感别人说他们的穿着土鳖,也不反感别人说他们长相平庸。
也就是说,除了反复改需求,别的他们都能忍受。
先说这么多吧,总结得也算是比较全了,后续有新的,我再补充。
来源:juejin.cn/post/7292960995437166601
uniapp下各端调用三方地图导航
技术栈
- 开发框架: uniapp
- vue 版本: 2.x
- 开发框架: uniapp
- vue 版本: 2.x
需求
使用uniapp在app端(Android,IOS)中显示宿主机已有的三方导航应用,由用户自主选择使用哪家地图软件进行导航,选择后,自动将目标地址设为终点在导航页面。 使用uniapp在微信小程序中调用微信内置地图导航。
使用uniapp在app端(Android,IOS)中显示宿主机已有的三方导航应用,由用户自主选择使用哪家地图软件进行导航,选择后,自动将目标地址设为终点在导航页面。 使用uniapp在微信小程序中调用微信内置地图导航。
实现
微信小程序调用微信内置地图导航
使用uni.openLocation()方法可直接调用,微信比较简单
传值字段
名称 说明 是否必传 latitude 纬度,范围为-90~90,负数表示南纬,使用 gcj02 国测局坐标系 是 longitude 经度,范围为-180~180,负数表示西经,使用 gcj02 国测局坐标系 是 name 位置名称 非必传,但不传不显示目标地址名称 address 地址的详细说明 非必传,但不传不显示目标地址名称详情
具体代码
经纬度需转为float数据类型
uni.openLocation({
latitude: parseFloat('地址纬度'),
longitude: parseFloat('地址经度'),
name: ‘地址名称,
address: '地址详情',
success: function (res) {
console.log('打开系统位置地图成功')
},
fail: function (error) {
console.log(error)
}
})
使用uni.openLocation()方法可直接调用,微信比较简单
传值字段
| 名称 | 说明 | 是否必传 |
|---|---|---|
| latitude | 纬度,范围为-90~90,负数表示南纬,使用 gcj02 国测局坐标系 | 是 |
| longitude | 经度,范围为-180~180,负数表示西经,使用 gcj02 国测局坐标系 | 是 |
| name | 位置名称 | 非必传,但不传不显示目标地址名称 |
| address | 地址的详细说明 | 非必传,但不传不显示目标地址名称详情 |
具体代码
经纬度需转为float数据类型
uni.openLocation({
latitude: parseFloat('地址纬度'),
longitude: parseFloat('地址经度'),
name: ‘地址名称,
address: '地址详情',
success: function (res) {
console.log('打开系统位置地图成功')
},
fail: function (error) {
console.log(error)
}
})
app端调用宿主机三方地图导航
步骤:
- 获取宿主机已安装的三方地图应用并显示,没有安装提示宿主机。
- 根据宿主机选择的三方地图,打开对应的三方地图进行导航。
使用plus调用原生API知识点:
- 获取宿主机系统环境
uniapp文档:uniapp.dcloud.net.cn/api/system/…
使用uniapp的uni.getSystemInfoSync().platform方法获取宿主机系统环境,结果为android、ios。
- 获取宿主机是否安装某个应用
步骤:
- 获取宿主机已安装的三方地图应用并显示,没有安装提示宿主机。
- 根据宿主机选择的三方地图,打开对应的三方地图进行导航。
使用plus调用原生API知识点:
- 获取宿主机系统环境
uniapp文档:uniapp.dcloud.net.cn/api/system/…
使用uniapp的uni.getSystemInfoSync().platform方法获取宿主机系统环境,结果为android、ios。
- 获取宿主机是否安装某个应用
使用H5产业联盟中的 plus.runtime.isApplicationExist来判断宿主机是否安装指定应用,已安装返回True,
Android平台需要通过设置appInf的pname属性(包名)进行查询。 iOS平台需要通过设置appInf的action属性(Scheme)进行查询,在iOS9以后需要添加白名单才可查询,在manifest.json文件plus->distribute->apple->urlschemewhitelist节点下添加(如urlschemewhitelist:["weixin"])。
调用示例
// Android
plus.runtime.isApplicationExist({pname: 'com.autonavi.minimap'})
// iOS
plus.runtime.isApplicationExist({action: 'iosamap://'})
- 调用系统级选择菜单显示已安装地图列表
调用示例
plus.nativeUI.actionSheet({ //选择菜单
title: "选择地图应用",
cancel: "取消",
buttons: [
{title: '1'},
{title: '2'}
]
}, function (e) {
console.log("您点击的是第几个:"+e.index)
})
- 打开三方某个应用
调用示例
// Android
plus.runtime.openURL('三方应用地址', function(res){
// todo...
}, 'com.xxx.xxxapp');
// ios
plus.runtime.openURL('三方应用地址', function(res){
// todo...
});
具体代码:
<template>
<view @click.stop="handleNavigation">导航view>
template>
<script>
...
data() {
return {
// 目标纬度
latitude: '',
// 目标经度
longitude: '',
// 目标地址名称
name: '',
// 目标地址详细信息
address: '',
// 我自己的位置经纬度(百度地图需要传入自己的经纬度进行导航)
selfLocation: {
latitude: '',
longitude: ''
}
}
},
methods: {
handleNavigation() {
const _this = this
if (!this.latitude || !this.longitude || !this.name) return
// 微信
// #ifdef MP-WEIXIN
let _obj = {
latitude: parseFloat(this.latitude),
longitude: parseFloat(this.longitude),
name: this.name,
}
if (this.address) {
_obj['address'] = this.address
}
uni.openLocation({
..._obj,
success: function (res) {
console.log('打开系统位置地图成功')
},
fail: function (error) {
console.log(error)
}
})
// #endif
// #ifdef APP-PLUS
// 判断系统安装的地图应用有哪些, 并生成菜单按钮
let _mapName = [
{title: '高德地图', name: 'amap', androidName: 'com.autonavi.minimap', iosName: 'iosamap://'},
{title: '百度地图', name: 'baidumap', androidName: 'com.baidu.BaiduMap', iosName: 'baidumap://'},
{title: '腾讯地图', name: 'qqmap', androidName: 'com.tencent.map', iosName: 'qqmap://'},
]
// 根据真机有的地图软件 生成的 操作菜单
let buttons = []
let platform = uni.getSystemInfoSync().platform
platform === 'android' && _mapName.forEach(item => {
if (plus.runtime.isApplicationExist({pname: item.androidName})) {
buttons.push(item)
}
})
platform === 'ios' && _mapName.forEach(item => {
console.log(item.iosName)
if (plus.runtime.isApplicationExist({action: item.iosName})) {
buttons.push(item)
}
})
if (buttons.length) {
plus.nativeUI.actionSheet({ //选择菜单
title: "选择地图应用",
cancel: "取消",
buttons: buttons
}, function (e) {
let _map = buttons[e.index - 1]
_this.openURL(_map, platform)
})
} else {
uni.showToast({
title: '请安装地图软件',
icon: 'none'
})
return
}
// #endif
},
// 打开第三方程序实际应用
openURL(map, platform) {
let _defaultUrl = {
android: {
"amap": `amapuri://route/plan/?sid=&did=&dlat=${this.latitude}&dlon=${this.longitude}&dname=${this.name}&dev=0&t=0`,
'qqmap': `qqmap://map/routeplan?type=drive&to=${this.name}&tocoord=${this.latitude},${this.longitude}&referer=fuxishan_uni_client`,
'baidumap': `baidumap://map/direction?origin=${this.selfLocation.latitude},${this.selfLocation.longitude}&destination=name:${this.name}|latlng:${this.latitude},${this.longitude}&coord_type=wgs84&mode=driving&src=andr.baidu.openAPIdemo"`
},
ios: {
"amap": `iosamap://path?sourceApplication=fuxishan_uni_client&dlat=${this.latitude}&dlon=${this.longitude}&dname=${this.name}&dev=0&t=0`,
'qqmap': `qqmap://map/routeplan?type=drive&to=${this.name}&tocoord=${this.latitude},${this.longitude}&referer=fuxishan_uni_client`,
'baidumap': `baidumap://map/direction?origin=${this.selfLocation.latitude},${this.selfLocation.longitude}&destination=name:${this.name}|latlng:${this.latitude},${this.longitude}&mode=driving&src=ios.baidu.openAPIdemo`
}
}
let newurl = encodeURI(_defaultUrl[platform][map.name]);
console.log(newurl)
plus.runtime.openURL( newurl, function(res){
console.log(res)
uni.showModal({
content: res.message
})
}, map.androidName ? map.androidName : '');
}
}
script>
<template>
<view @click.stop="handleNavigation">导航view>
template>
<script>
...
data() {
return {
// 目标纬度
latitude: '',
// 目标经度
longitude: '',
// 目标地址名称
name: '',
// 目标地址详细信息
address: '',
// 我自己的位置经纬度(百度地图需要传入自己的经纬度进行导航)
selfLocation: {
latitude: '',
longitude: ''
}
}
},
methods: {
handleNavigation() {
const _this = this
if (!this.latitude || !this.longitude || !this.name) return
// 微信
// #ifdef MP-WEIXIN
let _obj = {
latitude: parseFloat(this.latitude),
longitude: parseFloat(this.longitude),
name: this.name,
}
if (this.address) {
_obj['address'] = this.address
}
uni.openLocation({
..._obj,
success: function (res) {
console.log('打开系统位置地图成功')
},
fail: function (error) {
console.log(error)
}
})
// #endif
// #ifdef APP-PLUS
// 判断系统安装的地图应用有哪些, 并生成菜单按钮
let _mapName = [
{title: '高德地图', name: 'amap', androidName: 'com.autonavi.minimap', iosName: 'iosamap://'},
{title: '百度地图', name: 'baidumap', androidName: 'com.baidu.BaiduMap', iosName: 'baidumap://'},
{title: '腾讯地图', name: 'qqmap', androidName: 'com.tencent.map', iosName: 'qqmap://'},
]
// 根据真机有的地图软件 生成的 操作菜单
let buttons = []
let platform = uni.getSystemInfoSync().platform
platform === 'android' && _mapName.forEach(item => {
if (plus.runtime.isApplicationExist({pname: item.androidName})) {
buttons.push(item)
}
})
platform === 'ios' && _mapName.forEach(item => {
console.log(item.iosName)
if (plus.runtime.isApplicationExist({action: item.iosName})) {
buttons.push(item)
}
})
if (buttons.length) {
plus.nativeUI.actionSheet({ //选择菜单
title: "选择地图应用",
cancel: "取消",
buttons: buttons
}, function (e) {
let _map = buttons[e.index - 1]
_this.openURL(_map, platform)
})
} else {
uni.showToast({
title: '请安装地图软件',
icon: 'none'
})
return
}
// #endif
},
// 打开第三方程序实际应用
openURL(map, platform) {
let _defaultUrl = {
android: {
"amap": `amapuri://route/plan/?sid=&did=&dlat=${this.latitude}&dlon=${this.longitude}&dname=${this.name}&dev=0&t=0`,
'qqmap': `qqmap://map/routeplan?type=drive&to=${this.name}&tocoord=${this.latitude},${this.longitude}&referer=fuxishan_uni_client`,
'baidumap': `baidumap://map/direction?origin=${this.selfLocation.latitude},${this.selfLocation.longitude}&destination=name:${this.name}|latlng:${this.latitude},${this.longitude}&coord_type=wgs84&mode=driving&src=andr.baidu.openAPIdemo"`
},
ios: {
"amap": `iosamap://path?sourceApplication=fuxishan_uni_client&dlat=${this.latitude}&dlon=${this.longitude}&dname=${this.name}&dev=0&t=0`,
'qqmap': `qqmap://map/routeplan?type=drive&to=${this.name}&tocoord=${this.latitude},${this.longitude}&referer=fuxishan_uni_client`,
'baidumap': `baidumap://map/direction?origin=${this.selfLocation.latitude},${this.selfLocation.longitude}&destination=name:${this.name}|latlng:${this.latitude},${this.longitude}&mode=driving&src=ios.baidu.openAPIdemo`
}
}
let newurl = encodeURI(_defaultUrl[platform][map.name]);
console.log(newurl)
plus.runtime.openURL( newurl, function(res){
console.log(res)
uni.showModal({
content: res.message
})
}, map.androidName ? map.androidName : '');
}
}
script>
最终效果图
- 微信
- 微信

- app端


最后
参考链接: H5产业联盟:http://www.html5plus.org/doc/h5p.htm… uniapp: uniapp.dcloud.net.cn/api/ 百度、高德、腾讯地图,三方APP调用其的文档。
本文初发于:blog.zhanghaoran.ren/article/htm…
来源:juejin.cn/post/7262941534528700453
这可能是开源界最好用的行为验证码工具
- 💂 个人网站: IT知识小屋
- 🤟 版权: 本文由【IT学习日记】原创、需要转载请联系博主
- 💬 如果文章对你有帮助、欢迎关注、点赞、收藏(一键三连)和订阅专栏哦
写在前面
大家好,这里是IT学习日记。今日推荐项目:tianai-captcha行为验证码工具。
1000+优质开源项目推荐进度:6/1000。如需更多类型优质项目推荐,请在文章后留言。
工具简介
tianai-captcha行为验证码工具:分为 Go 和 Java 两个版本。支持多种验证方式,包括随机验证、曲线匹配、滑块验证、增强版滑块验证、旋转验证、滑动还原、角度验证、刮刮乐、文字点选、图标点选及语序点选等。

该系统能够快速集成到个人项目或系统中,显著提高开发效率。
功能展示
- 随机型验证码

- 曲线匹配验证码

- 滑动验证增强版验证码

- 滑块验证码

- 旋转验证码

- 滑动还原验证码

- 角度验验证码

- 刮刮乐验验证码

- 文字点选验证码

- 图标验证码

架构设计
tianai-captcha 验证码整体分为 生成器(ImageCaptchaGenerator)、校验器(ImageCaptchaValidator)、资源管理器(ImageCaptchaResourceManager) 其中生成器、校验器、资源管理器等都是基于接口模式实现可插拔的,可以替换为自定义实现,灵活度高
- 生成器 (ImageCaptchaGenerator)
主要负责生成行为验证码所需的图片。 - 校验器 (ImageCaptchaValidator)
主要负责校验用户滑动的行为轨迹是否合规。 - 资源管理器 (ImageCaptchaResourceManager)
主要负责读取验证码背景图片和模板图片等。
- 资源存储 (ResourceStore)
负责存储背景图和模板图。 - 资源提供者 (ResourceProvider)
负责将资源存储器中对应的资源转换为文件流。一般资源存储器中存储的是图片的 URL 地址或 ID,资源提供者则负责将 URL 或其他 ID 转换为真正的图片文件。
- 资源存储 (ResourceStore)
- 图片转换器 (ImageTransform)
主要负责将图片文件流转换成字符串类型,可以是 Base64 格式、URL 或其他加密格式,默认实现为 Base64 格式。
工具集成
引入依赖
<!-- maven 导入 -->
<dependency>
<groupId>cloud.tianai.captcha</groupId>
<artifactId>tianai-captcha</artifactId>
<version>1.4.1</version>
</dependency>
- 使用 ImageCaptchaGenerator生成器生成验证码
public class Test {
public static void main(String[] args) throws InterruptedException {
ImageCaptchaResourceManager imageCaptchaResourceManager = new DefaultImageCaptchaResourceManager();
ImageTransform imageTransform = new Base64ImageTransform();
ImageCaptchaGenerator imageCaptchaGenerator = new MultiImageCaptchaGenerator(imageCaptchaResourceManager,imageTransform).init(true);
/*
生成滑块验证码图片, 可选项
SLIDER (滑块验证码)
ROTATE (旋转验证码)
CONCAT (滑动还原验证码)
WORD_IMAGE_CLICK (文字点选验证码)
更多验证码支持 详见 cloud.tianai.captcha.common.constant.CaptchaTypeConstant
*/
ImageCaptchaInfo imageCaptchaInfo = imageCaptchaGenerator.generateCaptchaImage(CaptchaTypeConstant.SLIDER);
System.out.println(imageCaptchaInfo);
// 负责计算一些数据存到缓存中,用于校验使用
// ImageCaptchaValidator负责校验用户滑动滑块是否正确和生成滑块的一些校验数据; 比如滑块到凹槽的百分比值
ImageCaptchaValidator imageCaptchaValidator = new BasicCaptchaTrackValidator();
// 这个map数据应该存到缓存中,校验的时候需要用到该数据
Map<String, Object> map = imageCaptchaValidator.generateImageCaptchaValidData(imageCaptchaInfo);
}
}
- 使用ImageCaptchaValidator校验器 验证
public class Test2 {
public static void main(String[] args) {
BasicCaptchaTrackValidator sliderCaptchaValidator = new BasicCaptchaTrackValidator();
ImageCaptchaTrack imageCaptchaTrack = null;
Map<String, Object> map = null;
Float percentage = null;
// 用户传来的行为轨迹和进行校验
// - imageCaptchaTrack为前端传来的滑动轨迹数据
// - map 为生成验证码时缓存的map数据
boolean check = sliderCaptchaValidator.valid(imageCaptchaTrack, map).isSuccess();
// // 如果只想校验用户是否滑到指定凹槽即可,也可以使用
// // - 参数1 用户传来的百分比数据
// // - 参数2 生成滑块是真实的百分比数据
check = sliderCaptchaValidator.checkPercentage(0.2f, percentage);
}
}
工具获取
如果这篇文章对您有帮助,请**“彦祖们”**一定帮我点个 “关注” 和 “点赞”,这对我非常重要。我将会继续推荐更多优质项目和新闻。
来源:juejin.cn/post/7391351326153965568
这样做产品,死是早晚的事!

昨天和在北京的朋友聊天,他了解到之前我做过餐饮的SAAS系统,于是问我这一块是否还能分到一杯羹!
说实话,我觉得没机会,特别是对于一家小公司来说,基本上没机会,甚至连入场券都拿不到!
这不禁让我想起几年前认识的一个小公司,给他们兼职开发的两款SAAS产品,一款是连锁酒店系统,一款则是餐饮系统。
他们的酒店系统,现在在我看来依然是很牛逼的,我也去看过一些市面上的解决方案,但是依然没有他们的牛逼。
不过残酷的是,最近半年来,他们好像一套也没有卖出去,如果我没猜错的话,这几年下来,他们应该没有卖出多少套。
其实几年前我和他们协同开发,听了他们的一些想法,我就预见他们很难打出去。
因为我发现他去做了一些看似很完美,但是不是必须的功能,而且还花了大量时间去做,当时我觉得这完全就是鸡肋,现在看来是鸡骨头。
说白了,就是定位不明确,想做一个大而全方案,但是这对于一个小公司初创团队来说,这是很致命的,特别是资金不充足的情况下去干这事!
下面从几个方面去看问题。
1.定位不明确
理想一定是会被现实啪啪打脸的,当想去做一个产品的时候,不要觉得自己做得很全很大就能赢得市场,这简直是痴人说梦。
特别是在行业竞争如此之大的情况下,大公司早都入局了,人家的解决方案比你强大,价格比你便宜,售后比你全,你拿什么去拼?
当时我问他,为啥要做餐饮解决方案,你觉得你从技术,价格,服务这些方面,你有哪里比得上客如云,微盟,美团这些巨头,他说别管那么多,东西做出来自然有办法!
现在里面过去了,基本上没有任何推进。
这肯定是定位出问题了啊,不要觉得你手上有产品就能赚钱,如果是这样,那还需要销售干嘛。
对于小公司来说,大家都是技术出身,没有营销经验,就算做出产品来,也只能摆着看,如果要请销售团队,公司又支撑不起,显然矛盾了!
所以就尽量别去做这类似的产品,应该去做一些能解决别人痛点的小而美的解决方案。
就像微信公众号刚兴起的那几年,因为公众号自带的编辑器很难用,有一个人就做了一个小编辑器出来,赚得盆满钵满。
看似冷门,但是垂直!
2.陷入大而全的误区
接着上面的说。
后面有人看到看到了这个红利,就进军去做,他们希望做出更强大,功能更全的编辑器,结果花了大量时间去做,最后产品出来了,但是市场已经被别人抢了先机,最终不得不死。
这就是迷恋大而全的后果!
其实开源就是一个很好避免大而全的方案。
在开源领域,先做出一个小而美的产品,把影响力传播开,然后根据用户的需求不断迭代,这时候不是人去驱动产品了,而是需求去驱动产品。
这样做出来的产品不仅能避免出现很多无用的功能,还能节约很多的成本!
一定要让用户的需求来驱动产品的发展,而不是靠自己的臆想去决定做什么产品!
老罗当年在做锤子科技的时候,我觉得他就陷入了想去做一个大而全的产品,还陷入自己以为的漩涡,所以耗费了很多资金去研发TNT,所以导致失败。
如果那时候致力于去做好坚果系列,那么结局可能大不一样!
3.没有尝到甜头,你怎敢去做!
在我们贵州本土,有一个技术大佬,他一开始做了一个门户系统的解决方案,后续就有人来找他,说要购买他的系统,他从里面尝到了甜头!
于是就在这个领域持续深耕,最终形成了一套强大的解决方案。现在他的解决方案已经遍布全国。
他们公司基本上就是靠门户系统的解决方案来维持的。
所以,做一个产品,只有自己尝到甜头了,再去深耕,形成一套解决方案,那么成功率就会变得越高。
特别对于小公司来说,这是很重要的!
4.总结
做产品一定要忌讳大而全,也不要陷入只要我做出来了,无论如何都能分一杯羹,这是不现实的。
市场上到处是饿狼潜伏,你不过是一只小羊羔,怎么生存?
用最少的成本开发出一个小而美的解决方案,然后拿出去碰一碰,闻到味道了,再不断进击,这样成功率就高一点,即使失败了代价也不高。
今天的分享就到这里!
来源:juejin.cn/post/7313887095415324672
小小扫码枪bug引发的思考
最近新公司发生了一件bug引发思考的事
产品需求

大致如上图,一个输入框,我们制作了自定义数字键盘,input框可以回显键盘的输入,并且,可以支持扫码枪输入回显
bug描述
在win 系统没有问题,但在安卓系统:
- 每次自定义键盘输入时,还会吊起系统软键盘,且通过系统软键盘输入,input是无法回显的!

- 不支持 扫码枪输入了
最讨厌研究 系统兼容性问题了,但问题出了,就得研究
我们先看一下,自定义数字键盘是怎么实现的?
在了解自定义键盘之前,我先问问大家,键盘输入会触发哪些事件?
对,就是这三个 keydown,keypress, keyup
如何控制Input框只回显数字呢?答案就是在keyDown事件里,通过捕获 event.key来获取用户按下的物理按键的值,非数字的值直接return就能做到了
那么言归正传,自定义键盘怎么实现呢?
其实到这边我们不难想到一个解决方案的思路就是,当按下自定义键盘时,我们模拟一个 keydown事件,并向获得焦点的input 派发这个keydown事件,那么就能模拟键盘输入了
上代码:
const input = document.activeElement
const event = document.createEvent('UIEvents')
event.initUIEvent('keydown', true, true, window, 0)
event.key = key
event.keyCode = -1
input.dispatchEvent(event)
扫码枪又是个啥?
就是这个东东:

去过超市的都看过吧
用扫码枪或者其他设备扫描图形码(条形码或其他码)后将其为文本输入,
input需要识别到扫码枪输入结束,并回显input区,
其实扫码枪输入和用户键盘输入一样都可以触发keydown事件,派发给聚焦的input
那么问题来了?
怎样识别 扫码枪输入结束呢?
答案是onEnter事件
我们再来看看 安卓端出现的bug
1,为啥每次我们在自定义键盘上输入,会同时弹出系统软键盘呢??
问了下安卓侧RD,原来只要input获得焦点,系统键盘就会弹出
但是不聚焦,自定义键盘/扫码枪也没办法回显了呀?
难道真的无解了吗?这时候第n个知识点来了!用readOnly!
readonly,对,就是它,
什么?readonly不是只读吗?有了它,相当于 用户无法输入,因此无法触发系统键盘,这个可以理解,但是,加上它之后,还有焦点吗?
这里有个问题要问大家,你知道readonly和disabled的区别吗?
答案就是在交互上,readonly 仍是可以聚焦的!disabled 就不能了
并且readOnly 是禁止用户输入,所以在允许聚焦的同时,又阻止了软键盘的弹出,这时我不禁感叹: 完美!
2,安卓为啥不支持扫码枪扫码了?
我们通过调试发现,在安卓上,keyDown事件 捕获到的event.key 是 Unidentified, 被我们判定为非数字,直接return了
那解法呢?我们神奇的发现,当我们解了bug1,加上readonly后,bug2也好了!
至于为啥它也好了,具体原因我还不清楚,以下是我的猜测:
前文我们提到,只要input聚焦,软键盘就会弹出,而扫码枪其实也可以看成一个特殊的键盘,可能两个键盘冲突导致 event.key 无法识别,加上readonly禁掉 软键盘后,冲突解除,自然event.key 也可以正常识别了
清楚原因的同学可以留言给我哈!我好想知道!!
反思来了
这件问题的最终解决方案只有一行代码,一个单词: readOnly
简单到令人发指,而且这个问题是一个刚来两天的新同学搞定的
我在想这一连串的故事,太神奇了
为啥这个困扰前辈同学包括我很久的问题,一个萌新一下子就解决了呢?虽然我也是萌新

readOnly可以 解决禁止软键盘弹出,网上的答案是有的,但是我pass了这��方案,
为什么呢?
- input相关基础差,我错误的认为readOnly是只读嘛,肯定会不带焦点啊,虽然禁用了软键盘,但是 扫码枪输入也不能回显了啊
- 当我看到 event.key 是 Unidentified 时,研究重点跑偏了
- 我觉得这可能某种程度上是一种 beginer’s luck, 因为当时新同学的任务是研究如何禁用软键盘,并没有提到其他扫码枪问题,可能这种心无旁骛反而成了事
- 工作中,尤其遇到一些诡异的兼容性问题,真的需要多尝试,不要被自己的想当然绑手绑脚
- 对于兼容性问题,因为要不断尝试,最好找到一种简单方便的调试方法,会大大加快调研进度
最后还是感谢一切的发生,收获了知识,也让我有冲动分享给大家我的一点小思考,感恩感恩!
来源:juejin.cn/post/7388459061758017571
软件工程师,为什么不喜欢关电脑
💡 如果想阅读最新的文章,或者有技术问题需要交流和沟通,可搜索并关注微信公众号“希望睿智”。
概述
你是否注意到,软件工程师们似乎从不关电脑,也不喜欢关电脑?别以为他们是电脑“上瘾”,或是沉迷于电脑,这一现象背后蕴含着多种实际原因。

1、代码保存与恢复。
在编写代码过程中,遇到问题时可能会暂时离开去查阅资料或者休息,而不想打断当前的思路和工作进度。如果电脑不关机,他们可以迅速回到上次中断的地方,继续解决问题,避免了重新加载项目和找回思考线索的过程。
2、远程访问与协作。
很多软件工程师采用分布式团队协作模式,需要通过SSH等远程访问手段进行代码部署、调试或监控线上服务。下班后保持电脑开机,有利于他们在家或其他地点远程处理紧急任务。
3、持续集成/持续部署。
对于实施CI/CD流程的项目,电脑上的开发环境可能作为构建服务器的一部分,用于自动编译、测试和部署代码。在这种情况下,电脑全天候运行是必需的。
4、虚拟机与容器运行。
软件工程师使用的电脑上可能运行着虚拟机或容器,用于支持多套开发环境或者运行测试实例。这些虚拟资源,通常要求宿主机保持运行状态。
5、挂起与休眠模式。
虽然没有完全关机,但许多软件工程师会选择将电脑设置为休眠或挂起模式,这样既能节省能源,又能在短时间内快速恢复到工作状态。
实际上,以上5点归根到底,都是为了保持一个持续开发环境。那么,何为持续开发环境?
持续开发环境
持续开发环境是指软件工程师为了进行软件开发而搭建的、包含所有必要工具和服务的一套完整生态系统。它涵盖了集成开发环境(IDE)、版本控制系统(比如:Git)、本地服务器、数据库服务、构建工具以及各种编程框架和库等元素。这个环境是软件工程师日常工作的核心载体,也是他们实现高效编程、调试和测试的基础。
首先,持续开发环境通过自动化流程,极大地减少了开发过程中的人工干预。每当软件工程师提交代码到版本控制系统时,持续开发环境会自动触发构建、测试和部署流程。这意味着:软件工程师无需手动编译代码、运行测试用例或手动部署应用程序。这些繁琐的任务由持续开发环境自动完成,从而释放了软件工程师的时间和精力,让他们更专注于编写高质量的代码。
其次,持续开发环境有助于及时发现和修复问题。在持续集成的过程中,每次代码提交都会触发一次完整的构建和测试流程。这意味着:任何潜在的错误或问题都会在早期阶段被及时发现。此外,持续开发环境通常与持续监控和警报系统相结合,当出现问题时,系统会立即向团队成员发送警报,从而确保问题能够得到及时解决。
此外,持续开发环境还促进了团队协作和沟通。通过版本控制系统和自动化测试工具,团队成员可以轻松地查看彼此的代码、理解彼此的工作进度,并在出现问题时及时沟通。这种透明的工作方式有助于建立信任、减少误解,从而提高团队的整体效能。
最后,持续开发环境为创新提供了有力的支持。在快速迭代和不断试错的过程中,软件工程师可以迅速验证他们的想法和假设。如果某个功能或改进在实际应用中效果不佳,他们可以迅速调整方向,尝试新的方法。这种灵活性和敏捷性使得软件工程师能够不断尝试新的技术和方法,从而推动软件行业的创新和发展。
在这个日益复杂和快速变化的数字世界中,持续开发环境已经成为软件工程师们不可或缺的利器。但持续开发环境的搭建和启动可能耗时较长,因此为了保持工作连续性,软件工程师往往倾向于让电脑保持开机状态,以便随时可以继续编程或调试。
案例一
假设小张是一位正在开发一款大型Web应用的后端软件工程师,他的工作台的配置如下。
操作系统:Windows 10。
集成开发环境:IntelliJ IDEA,用于编写Java代码。
版本控制系统:Git,用于代码版本管理及团队协作。
本地服务器:Apache Tomcat,用于运行和测试Java Web应用。
数据库服务:MySQL,存储应用程序的数据。
构建工具:Maven,负责项目的自动化构建与依赖管理。
虚拟机环境:Docker容器,模拟生产环境以进行更真实的测试。
在每天的工作中,小张需要不断地编译代码、调试程序、提交更新到Git仓库,并在本地Tomcat服务器上验证功能是否正常。同时,他还可能需要在Docker容器内模拟不同的操作系统环境,以对软件进行兼容性测试。
如果小张下班时关闭了电脑,第二天重新启动所有服务和工具将会耗费至少半小时以上的时间。而在这段时间里,他无法立即开始编程或解决问题,影响了工作效率。
此外,小张所在的团队采用了CI/CD流程,利用Jenkins等工具自动执行代码编译、单元测试以及部署至测试服务器的任务。这就要求他的电脑作为Jenkins客户端始终在线,以便触发并完成这些自动化任务。
因此,为了确保高效流畅的开发流程,减少不必要的环境配置时间,及时响应线上问题以及支持远程协同,小张和其他许多软件工程师都会选择让自己的电脑始终保持开机状态,维持一个稳定的持续开发环境。
案例二
假设小李是一名全栈开发者,他正在参与一个大型的微服务项目,他的开发环境配置如下。
操作系统:Ubuntu 20.04 LTS。
集成开发环境:Visual Studio Code,用于编写前后端代码。
版本控制系统:Git,协同团队进行代码管理。
本地开发工具链:Node.js、NPM/Yarn用于前端开发,Python及pip用于后端开发,同时使用Kubernetes集群模拟生产环境部署。
数据库与缓存服务:MySQL作为主数据库,Redis作为缓存服务。
消息队列服务:RabbitMQ用于微服务间的异步通信。
CI/CD工具:GitHub Actions和Docker Compose结合,实现自动化构建、测试和部署。
在项目开发过程中,小李需要频繁地编译、打包、运行并测试各个微服务。一旦他关闭电脑,第二天重新启动所有服务将耗费大量时间。比如:搭建完整的Kubernetes集群可能需要数分钟到数十分钟不等,而每次重启服务都可能导致微服务间的依赖关系错乱,影响开发进度。
此外,由于团队采用了敏捷开发模式,每天都有多次代码提交和合并。为了能及时响应代码变动,小李设置了自己的电脑作为GitHub Actions的一部分,当有新的Pull Request时,可以立即触发自动化构建和测试流程,确保新代码的质量。
更进一步,在下班后或周末期间,如果线上服务出现紧急问题,小李可以通过SSH远程登录自己始终保持在线的电脑,快速定位问题所在,并在本地环境中复现和修复,然后推送到测试或生产环境,大大提高了响应速度和解决问题的效率。
综上所述,对于像小李这样的全栈开发者而言,维持一个持续稳定的开发环境是其高效工作的重要保障,也是应对复杂软件工程挑战的关键策略之一。
案例三
假设小王是一名独立游戏开发者,他正在使用Unity引擎制作一款3D角色扮演游戏,他的开发环境配置如下。
操作系统:macOS Big Sur。
集成开发环境:Unity Editor,集成了脚本编写、场景设计、动画编辑等多种功能。
版本控制系统:Perforce,用于大型项目文件的版本管理和团队协作。
资产构建工具:TexturePacker用于图片资源打包,FMOD Studio用于音频处理和混音。
本地测试环境:在电脑上运行Unity的内置播放器进行实时预览和调试。
云服务与部署平台:阿里云服务器作为远程测试和分发平台。
在游戏开发过程中,小王需要频繁地编辑代码、调整场景布局、优化美术资源并即时查看效果。由于Unity项目的加载和编译过程可能较长,尤其在处理大量纹理和模型时,如果每次关闭电脑后都要重新启动项目,无疑会大大降低工作效率。
此外,小王经常需要利用晚上或周末时间对游戏进行迭代更新,并将新版本上传到云端服务器进行远程测试。为了能在任何时刻快速响应工作需求,他的电脑始终保持开机状态,并且已连接至Perforce服务器,确保能及时获取最新的代码变更,同时也能立即上传自己的工作成果以供团队其他成员审阅和测试。
因此,对于小王这样的游戏开发者来说,保持持续开发环境不仅能有效提高日常工作效率,还能确保在非工作时段可以灵活应对突发任务,从而更好地满足项目进度要求。
总结
持续开发环境为程序员提供了一个高效、稳定且富有创新的工作环境。它通过自动化流程、及时发现问题、促进团队协作和支持创新,为软件开发带来了巨大的变革。
保持持续开发环境对于软件开发者而言至关重要,它能够显著提高工作效率,并确保项目开发的连贯性。通过维持开发环境始终在线,我们可以在任何时间方便地进行代码编辑、资源优化、实时预览和调试,并能灵活应对团队协作需求,实现快速迭代更新,从而满足项目进度要求。
来源:juejin.cn/post/7376837003520245772
种种迹象表明:前端岗位即将消失
最近,腾讯混元大模型的HR约我面试,为了确定是否真招人,我打开了腾讯内推的小程序,确实有这个岗位,但整个深圳也只有这一个。
于是,我突然意识到:在大模型时代,前端工程师这个岗位应该会是最先消失的岗位。
AI程序员的诞生
24年年初,英伟达CEO黄仁勋表示,自己相信就在不久的将来,人类再也不需要学习如何编码了,孩子们应该停止编程课。
然后24年3月,一家叫Cognition美国初创公司,发布了首个AI软件工程师Devin。它掌握全栈技能,云端部署、底层代码、改bug、训练和微调AI模型都不在话下。
只需一句指令,Devin就可端到端处理整个开发项目,这再度引发“码农是否将被淘汰”的大讨论。在SWE-bench上,它的表现远远超过Claude 2、Llama、GPT-4等选手,取得了13.86%的惊人成绩!
也就是说,它已经能通过AI公司的面试了。
接着4月,阿里发布消息称,其迎来了首位 AI 程序员——通义灵码。并在阿里云上海AI峰会上,阿里云宣布推出首个AI程序员,具备架构师、开发工程师、测试工程师等多种岗位的技能,能一站式自主完成任务分解、代码编写、测试、问题修复、代码提交整个过程,最快分钟级即可完成应用开发,大幅提升研发效率。
此次发布的AI程序员,是基于通义大模型构建的多智能体,每个智能体分别负责具体的软件开发任务并互相协作,可端到端实现一个产品功能的研发,这极大地简化了软件开发的流程。
由此带来的影响
一方面, AI技术的迅速发展和普及势必给程序员的工作带来冲击:传统的编码方式将显著改变,水平一般的程序员被取代的趋势或不可避免。
另一方面,尽管AI可以辅助程序员快速生成代码、提高开发效率,但并不能完全取代程序员的角色,尤其是技术理解深厚、能力强大的高水平程序员。
对于未来的程序员而言,掌握AI技术并应用于自己的工作流程中,与AI协同工作从而提高自己的工作效率和编码质量,是与时俱进、适应市场的必然需求。
由此,未来一名好的程序员不应仅仅是一名技术人员,还需要具备广泛的知识和技能。他们是整个人、机、环境系统框架中的创造者,要持续创新、创造价值。
具体而言,为了编写高质量代码,他们可能要精通多种编程语言;为了能按需选用合适的技术方案,他们要能迅速适应新的技术和工具。
为了面对复杂问题时能抓住原因并及时分析解决,他们必须保持与团队及客户的高效沟通协作,并不断积累知识、经验,同步跟进行业技术前沿,针对具体问题设计出创新的解决方案,保障程序的稳定性和可靠性。
所以,去年我在 从美团的开发通道合并谈谈开发的职业规划 就提出:LLM在软件工程的采用,将在众多工程领域产生突破,甚至于颠覆,由此也敦促我们必须认真审视专业能力的变迁和专业角色的定义。
为何最先消失的是前端岗
在我去年写前端学哪些技能饭碗越铁收入还高时,我还没有前端岗位可能即将消失的观点,但过去半年和很多猎头聊了一下前端岗的机会,以及看了很多后端培训课程中都包含前端的知识技能。
再结合22年我在美团内部,给几百个后端同学培训如何快速上手前端开发,我觉得前端这个岗位很有可能以后在招聘中就看不到这一细分岗位了。
其实15年前,全球应该都没有前端工程师这个岗位,当时的多数前端工作都比较简单,一部分是后端自己做,一个部分则是设计出生的切图仔完成~
后来随着移动互联网的兴起,前端开发语言发布了全新的规范ES6,整个前端开发生态逐步繁荣了起来,因为发展很快,网页的多端兼容和多版本工作比较繁杂,所以前端工作才由一个全新的岗位为负责。
原本很多前端同学在整个系统开发中就处于辅助角色,经常是多个团队的后端争抢一个专业的前端工程师,但如今,随着前端技术已经非常成熟和完善和大模型技术的加持,后端完成前端工作越来越容易。
所以,各公司自然就会减少很多前端岗位的招聘,只有少量技术比较新或业务比较复杂的项目才需要少量专职的前端工程师。
从各公司合并开发通道来看,消失的不仅是前端,还有后端和系统开发,对外招聘岗位都是软件工程师,工作内容根据需要动态调整。
总结
知识本身并不是力量,能有效将知识应用于实践才是真正的力量。同样,大量的编程知识可能是有价值的,但若不会运用、不知变通,无法解决实际问题,它就很难产生任何实质性影响。
能够有效使用程序,意味着智能体正具备将知识与学习应用转化的能力。这就需要程序员具备一些编程规则之外的能力,如分析、判断、解决问题的能力等。
程序员之所以能够不被取代,底气正在于其能将所学与实际情况相结合,并作出正确决策,而不是像AI程序员那样的编程工具,为了编程而编程。
未来,AI负责基础重复性劳动、人类程序员负责顶层设计的模式已经初露端倪,而认为人类程序员将被AI取代、沦为提要求的“边缘人”,为时尚早。
来源:juejin.cn/post/7392852233999892495
谁说forEach不支持异步代码,只是你拿不到异步结果而已
在前面探讨 forEach 中异步请求后端接口时,很多人都知道 forEach 中 async/await 实际是无效的,很多文章也说:forEach 不支持异步,forEach 只能同步运行代码,forEach 会忽略 await 直接进行下一次循环...
当时我的理解也是这样的,后面一细想好像不对,直接上我前面一篇文章用到的示例代码:
async function getData() {
const list = await $getListData()
// 遍历请求
list.forEach(async (item) => {
const res = await $getExtraInfo({
id: item.id
})
item.extraInfo = res.extraInfo
})
// 打印下最终处理过的额外数据
console.log(list)
}
上面 $getListData、$getExtraInfo 都是 promise 异步方法,按照上面说的 forEach 会直接忽略掉 await,那么循环体内部拿到的 res 就应该是 undefined,后面的 res.extraInfo 应该报错才对,但是实际上代码并没有报错,说明 await 是有效的,内部的异步代码也是可以正常运行的,所以 forEach 肯定是支持异步代码的。
手写版 forEach
先从自己实现的简版 forEach 看起:
Array.prototype.customForEach = function (callback) {
for (let i = 0; i < this.length; i++) {
callback(this[i], i, this)
}
}
里面会为数组的每个元素执行一下回调函数,实际拿几组数组测试和正宗的 forEach 方法效果也一样。可能很多人还是会有疑问你自己实现这到底靠不靠谱,不瞒你说我也有这样的疑问。
MDN 上关于 forEach 的说明
先去 MDN 上搜一下 forEach,里面的大部分内容只是使用层面的文档,不过里面有提到:“forEach() 期望的是一个同步函数,它不会等待 Promise 兑现。在使用 Promise(或异步函数)作为 forEach 回调时,请确保你意识到这一点可能带来的影响”。
ECMAScript 中 forEach 规范
继续去往 javascript 底层探究,我们都知道执行 js 代码是需要依靠 js 引擎,去将我们写的代码解释翻译成计算机能理解的机器码才能执行的,所有 js 引擎都需要参照 ECMAScript 规范来具体实现,所以这里我们先去看下 ECMAScript 上关于 forEach 的标准规范:

添加图片注释,不超过 140 字(可选)
谷歌 V8 的 forEach 实现
常见的 js 引擎有:谷歌的 V8、火狐 FireFox 的 SpiderMonkey、苹果 Safari 的 JavaScriptCore、微软 Edge 的 ChakraCore...后台都很硬,这里我们就选其中最厉害的谷歌浏览器和 nodejs 依赖的 V8 引擎,V8 中对于 forEach 实现的主要源码:
transitioning macro FastArrayForEach(implicit context: Context)(
o: JSReceiver, len: Number, callbackfn: Callable, thisArg: JSAny): JSAny
labels Bailout(Smi) {
let k: Smi = 0;
const smiLen = Cast<Smi>(len) otherwise goto Bailout(k);
const fastO = Cast<FastJSArray>(o) otherwise goto Bailout(k);
let fastOW = NewFastJSArrayWitness(fastO);
// Build a fast loop over the smi array.
for (; k < smiLen; k++) {
fastOW.Recheck() otherwise goto Bailout(k);
// Ensure that we haven't walked beyond a possibly updated length.
if (k >= fastOW.Get().length) goto Bailout(k);
const value: JSAny = fastOW.LoadElementNoHole(k)
otherwise continue;
Call(context, callbackfn, thisArg, value, k, fastOW.Get());
}
return Undefined;
}
源码是 .tq 文件,这是 V8 团队开发的一个叫 Torque 的语言,语法类似 TypeScript,所以对于前端程序员上面的代码大概也能看懂,想要了解详细的 Torque 语法,可以直接去 V8 的官网上查看。
从上面的源码可以看到 forEach 实际还是依赖的 for 循环,没有返回值所以最后 return 的一个 Undefined。看完源码是不是发现咱上面的手写版也大差不差,只不过 V8 里实现了更多细节的处理。
结论:forEach 支持异步代码
最后的结论就是:forEach 其实是支持异步的,循环时并不是会直接忽略掉 await,但是因为 forEach 没有返回值,所以我们在外部没有办法拿到每次回调执行过后的异步 promise,也就没有办法在后续的代码中去处理或者获取异步结果了,改造一下最初的示例代码:
async function getData() {
const list = await $getListData()
// 遍历请求
list.forEach(async (item) => {
const res = await $getExtraInfo({
id: item.id
})
item.extraInfo = res.extraInfo
})
// 打印下最终处理过的额外数据
console.log(list)
setTimeout(() => {
console.log(list)
}, 1000 * 10)
}
你会发现 10 秒后定时器中是可以按照预期打印出我们想要的结果的,所以异步代码是生效了的,只不过在同步代码中我们没有办法获取到循环体内部的异步状态。
如果还是不能理解,我们对比下 map 方法,map 和 forEach 很类似,但是 map 是有返回值的,每次遍历结束之后我们是可以直接 return 一个值,后续我们就可以接收到这个返回值。这也是为什么很多文章中改写 forEach 异步操作时,使用 map 然后借助 Promise.all 来等待所有异步操作完成后,再进行下面的逻辑来实现同步的效果。
参考文档
- MDN forEach 文档:developer.mozilla.org/zh-CN/docs/…
- ECMAScript 中 forEach 规范:tc39.es/ecma262/#se…
- 谷歌 V8 中 forEach 源码:chromium.googlesource.com/v8/v8.git/+…
- 谷歌 V8 中 map 源码:chromium.googlesource.com/v8/v8.git/+…
- 谷歌 V8 官网:v8.dev
- 谷歌 V8 源码:github.com/v8/v8
来源:juejin.cn/post/7389912354749087755
js如何实现当文本内容过长时,中间显示省略号...,两端正常展示
前一阵做需求时,有个小功能实现起来废了点脑细胞,觉得可以记录一下。
产品的具体诉求是:用户点击按钮进入详情页面,详情页内的卡片标题内容过长时,标题的前后两端正常展示,中间用省略号...表示,并且鼠标悬浮后,展示全部内容。
关于鼠标悬浮展示全部内容的代码就不放在这里了,本文主要写关于实现中间省略号...的代码。
实现思路
- 获取标题盒子的真实宽度, 我这里用的是clientWidth;
- 获取文本内容所占的实际宽度;
- 根据文字的大小计算出每个文字所占的宽度;
- 判断文本内容的实际宽度是否超出了标题盒子的宽度;
- 通过文字所占的宽度累加之和与标题盒子的宽度做对比,计算出要截取位置的索引;
- 同理,文本尾部的内容需要翻转一下,然后计算索引,截取完之后再翻转回来;
代码
html代码
<div class="title" id="test">近日,银行纷纷下调大额存单利率,但银行定期存款仍被疯抢。银行理财经理表示:有意向购买定期存款要尽快,不确定利率是否会再降。</div>
css代码: 设置文本不换行,同时设置overflow:hidden让文本溢出盒子隐藏
.title {
width: 640px;
height: 40px;
line-height: 40px;
font-size: 14px;
color: #00b388;
border: 1px solid #ddd;
overflow: hidden;
/* text-overflow: ellipsis; */
white-space: nowrap;
/* box-sizing: border-box; */
padding: 0 10px;
}
javascript代码:
获取标题盒子的宽度时要注意,如果在css样式代码中设置了padding, 就需要获取标题盒子的左右padding值。 通过getComputedStyle属性获取到所有的css样式属性对应的值, 由于获取的padding值都是带具体像素单位的,比如: px,可以用parseInt特殊处理一下。
获取盒子的宽度的代码,我当时开发时是用canvas计算的,但计算的效果不太理想,后来逛社区,发现了嘉琪coder大佬分享的文章,我这里就直接把代码搬过来用吧, 想了解的掘友可以直接滑到文章末尾查看。
判断文本内容是否超出标题盒子
// 标题盒子dom
const dom = document.getElementById('test');
// 获取dom元素的padding值
function getPadding(el) {
const domCss = window.getComputedStyle(el, null);
const pl = Number.parseInt(domCss.paddingLeft, 10) || 0;
const pr = Number.parseInt(domCss.paddingRight, 10) || 0;
console.log('padding-left:', pl, 'padding-right:', pr);
return {
left: pl,
right: pr
}
}
// 检测dom元素的宽度,
function checkLength(dom) {
// 创建一个 Range 对象
const range = document.createRange();
// 设置选中文本的起始和结束位置
range.setStart(dom, 0),
range.setEnd(dom, dom.childNodes.length);
// 获取元素在文档中的位置和大小信息,这里直接获取的元素的宽度
let rangeWidth = range.getBoundingClientRect().width;
// 获取的宽度一般都会有多位小数点,判断如果小于0.001的就直接舍掉
const offsetWidth = rangeWidth - Math.floor(rangeWidth);
if (offsetWidth < 0.001) {
rangeWidth = Math.floor(rangeWidth);
}
// 获取元素padding值
const { left, right } = getPadding(dom);
const paddingWidth = left + right;
// status:文本内容是否超出标题盒子;
// width: 标题盒子真实能够容纳文本内容的宽度
return {
status: paddingWidth + rangeWidth > dom.clientWidth,
width: dom.clientWidth - paddingWidth
};
}
通过charCodeAt返回指定位置的字符的Unicode编码, 返回的值对应ASCII码表对应的值,0-127包含了常用的英文、数字、符号等,这些都是占一个字节长度的字符,而大于127的为占两个字节长度的字符。
截取和计算文本长度
// 计算文本长度,当长度之和大于等于dom元素的宽度后,返回当前文字所在的索引,截取时会用到。
function calcTextLength(text, width) {
let realLength = 0;
let index = 0;
for (let i = 0; i < text.length; i++) {
charCode = text.charCodeAt(i);
if (charCode >= 0 && charCode <= 128) {
realLength += 1;
} else {
realLength += 2 * 14; // 14是字体大小
}
// 判断长度,为true时终止循环,记录索引并返回
if (realLength >= width) {
index = i;
break;
}
}
return index;
}
// 设置文本内容
function setTextContent(text) {
const { status, width } = checkLength(dom);
let str = '';
if (status) {
// 翻转文本
let reverseStr = text.split('').reverse().join('');
// 计算左右两边文本要截取的字符索引
const leftTextIndex = calcTextLength(text, width);
const rightTextIndex = calcTextLength(reverseStr, width);
// 将右侧字符先截取,后翻转
reverseStr = reverseStr.substring(0, rightTextIndex);
reverseStr = reverseStr.split('').reverse().join('');
// 字符拼接
str = `${text.substring(0, leftTextIndex)}...${reverseStr}`;
} else {
str = text;
}
dom.innerHTML = str;
}
最终实现的效果如下:

上面就是此功能的所有代码了,如果想要在本地试验的话,可以在本地新建一个html文件,复制上面代码就可以了。
下面记录下从社区内学到的相关知识:
- js判断文字被溢出隐藏的几种方法;
- JS获取字符串长度的几种常用方法,汉字算两个字节;
1、 js判断文字被溢出隐藏的几种方法
1. Element-plus这个UI框架中的表格组件实现的方案。
通过document.createRange和document.getBoundingClientRect()这两个方法实现的。也就是我上面代码中实现的checkLength方法。
2. 创建一个隐藏的div模拟实际宽度
通过创建一个不会在页面显示出来的dom元素,然后把文本内容设置进去,真实的文本长度与标题盒子比较宽度,判断是否被溢出隐藏了。
function getDomDivWidth(dom) {
const elementWidth = dom.clientWidth;
const tempElement = document.createElement('div');
const style = window.getComputedStyle(dom, null)
const { left, right } = getPadding(dom); // 这里我写的有点重复了,可以优化
tempElement.style.cssText = `
position: absolute;
top: -9999px;
left: -9999px;
white-space: nowrap;
padding-left:${style.paddingLeft};
padding-right:${style.paddingRight};
font-size: ${style.fontSize};
font-family: ${style.fontFamily};
font-weight: ${style.fontWeight};
letter-spacing: ${style.letterSpacing};
`;
tempElement.textContent = dom.textContent;
document.body.appendChild(tempElement);
const obj = {
status: tempElement.clientWidth + right + left > elementWidth,
width: elementWidth - left - right
}
document.body.removeChild(tempElement);
return obj;
}
3. 创建一个block元素来包裹inline元素
这种方法是在UI框架acro design vue中实现的。外层套一个块级(block)元素,内部是一个行内(inline)元素。给外层元素设置溢出隐藏的样式属性,不对内层元素做处理,这样内层元素的宽度是不变的。因此,通过获取内层元素的宽度和外层元素的宽度作比较,就可以判断出文本是否被溢出隐藏了。
// html代码
<div class="title" id="test">
<span class="content">近日,银行纷纷下调大额存单利率,但银行定期存款仍被疯抢。银行理财经理表示:有意向购买定期存款要尽快,不确定利率是否会再降。</span>
</div>
// 创建一个block元素来包裹inline元素
const content = document.querySelector('.content');
function getBlockDomWidth(dom) {
const { left, right } = getPadding(dom);
console.log(dom.clientWidth, content.clientWidth)
const obj = {
status: dom.clientWidth < content.clientWidth + left + right,
width: dom.clientWidth - left - right
}
return obj;
}
4. 使用canvas中的measureText方法和TextMetrics对象来获取元素的宽度
通过Canvas 2D渲染上下文(context)可以调用measureText方法,此方法会返回TextMetrics对象,该对象的width属性值就是字符占据的宽度,由此也能获取到文本的真实宽度,此方法有弊端,比如说兼容性,精确度等等。
// 获取文本长度
function getTextWidth(text, font = 14) {
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d")
context.font = font
const metrics = context.measureText(text);
return metrics.width
}
5. 使用css实现
这种方式来自评论区的掘友@S_mosar提供的思路。
先来看下效果:

代码如下:
css部分
.con {
font-size: 14px;
color: #666;
width: 600px;
margin: 50px auto;
border-radius: 8px;
padding: 15px;
overflow: hidden;
resize: horizontal;
box-shadow: 20px 20px 60px #bebebe, -20px -20px 60px #ffffff;
}
.wrap {
position: relative;
line-height: 2;
height: 2em;
padding: 0 10px;
overflow: hidden;
background: #fff;
margin: 5px 0;
}
.wrap:nth-child(odd) {
background: #f5f5f5;
}
.title {
display: block;
position: relative;
background: inherit;
text-align: justify;
height: 2em;
overflow: hidden;
top: -4em;
}
.txt {
display: block;
max-height: 4em;
}
.title::before{
content: attr(title);
width: 50%;
float: right;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
direction: rtl;
}
html部分
<ul class="con">
<li class="wrap">
<span class="txt">CSS 实现优惠券的技巧 - 2021-03-26</span>
<span class="title" title="CSS 实现优惠券的技巧 - 2021-03-26">CSS 实现优惠券的技巧 - 2021-03-26</span>
</li>
<li class="wrap">
<span class="txt">CSS 测试标题,这是一个稍微有点长的标题,超出一行以后才会有title提示,标题是 实现优惠券的技巧 - 2021-03-26</span>
<span class="title" title="CSS 测试标题,这是一个稍微有点长的标题,超出一行以后才会有title提示,标题是 实现优惠券的技巧 - 2021-03-26">CSS
测试标题,这是一个稍微有点长的标题,超出一行以后才会有title提示,标题是 实现优惠券的技巧 - 2021-03-26</span>
</li>
<li class="wrap">
<span class="txt">CSS 拖拽?</span>
<span class="title" title="CSS 拖拽?">CSS 拖拽?</span>
</li>
<li class="wrap">
<span class="txt">CSS 文本超出自动显示title</span>
<span class="title" title="CSS 文本超出自动显示title">CSS 文本超出自动显示title</span>
</li>
</ul>
思路解析:
- 文字内容的父级标签li设置
line-height: 2;、overflow: hidden;、height: 2em;,因此 li 标签的高度是当前元素字体大小的2倍,行高也是当前字体大小的2倍,同时内容若溢出则隐藏。 - li 标签内部有两个 span 标签,二者的作用分别是:类名为
.txt的标签用来展示不需要省略号时的文本,类名为.title用来展示需要省略号时的文本,具体是如何实现的请看第五步。 - 给
.title设置伪类before,将伪类宽度设置为50%,搭配浮动float: right;,使得伪类文本内容靠右,这样设置后,.title和伪类就会各占父级宽度的一半了。 .title标签设置text-align: justify;,用来将文本内容和伪类的内容两端对齐。- 给伪类
before设置文字对齐方式direction: rtl;,将伪类内的文本从右向左流动,即right to left,再设置溢出省略的css样式就可以了。 .title标签设置了top: -4em,.txt标签设置max-height: 4em;这样保证.title永远都在.txt上面,当内容足够长,.txt文本内容会换行,导致高度从默认2em变为4em,而.title位置是-4em,此时正好将.txt覆盖掉,此时显示的就是.title标签的内容了。
知识点:text-align: justify;
- 文本的两端(左边和右边)都会与容器的边缘对齐。
- 为了实现这种对齐,浏览器会在单词之间添加额外的空间。这通常意味着某些单词之间的间距会比其他单词之间的间距稍大一些。
- 如果最后一行只有一个单词或少数几个单词,那么这些单词通常不会展开以填充整行,而是保持左对齐。
需要注意的是,
text-align: justify;主要用于多行文本。对于单行文本,这个值的效果与text-align: left;相同,因为单行文本无法两端对齐。
2、JS获取字符串长度的几种常用方法
1. 通过charCodeAt判断字符编码
通过charCodeAt获取指定位置字符的Unicode编码,返回的值对应ASCII码表对应的值,0-127包含了常用的英文、数字、符号等,这些都是占一个字节长度的字符,而大于127的为占两个字节长度的字符。
function calcTextLength(text) {
let realLength = 0;
for (let i = 0; i < text.length; i++) {
charCode = text.charCodeAt(i);
if (charCode >= 0 && charCode <= 128) {
realLength += 1;
} else {
realLength += 2;
}
}
return realLength;
}
2. 采取将双字节字符替换成"aa"的做法,取长度
function getTextWidth(text) {
return text.replace(/[^\x00-\xff]/g,"aa").length;
};
参考文章
4. canvas绘制字体偏上不居中问题、文字垂直居中后偏上问题、measureText方法和TextMetrics对象
来源:juejin.cn/post/7329967013923962895
一文让你彻底悟透柯里化
什么是柯里化?
在数学和计算机科学中,柯里化是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术
前端为什么需要使用柯里化?
前端使用柯里化的用途主要就是简化代码结构,提高系统的维护性,一个方法只有一个参数,强制了功能的单一性,很自然就能做到功能内聚,降低耦合
一句话就是:降低代码重复率,提高代码适应性
普通函数
实现一个普通的累加函数,调用时需要传入三个参数,如果少 传则输出NaN,多传则后面的参数都无效
function add(a,b,c){
return a + b + c
}
add(1,2,3) //6
普通柯里化函数
实现一个普通的柯里化函数(含有柯里化性质),通过调用传参的方式将函数传入,并传入参数进行运算,返回一个新的函数的参数进行累计(取决于传入函数时传入参数的个数以及执行函数的传入参数进行累计)
function add(a, b, c) {
return a + b + c;
}
function fixedCurryAdd(fn) {
const arg = [].slice.call(arguments, 1);
return function () {
const newArg = arg.concat([].slice.call(arguments, 0));
return fn.apply(this, newArg);
};
}
const curryAdd = new fixedCurryAdd(add, 1);
console.log(curryAdd(2,11)); //14
柯里化函数
通过上面的含有柯里化性质的函数可以看出 ,要实现柯里化函数可以有多种传参方式,例如:
newAdd(1,2,3,4)
newAdd(1)(2,3,4)
newAdd(1)(2,3)(4)
newAdd(1)(2)(3)(4)
含有多种传参方式 ,无论哪种方式,最后都会把所需参数传入,但是柯里化函数只是期望你执行这次函数传入所需参数个数,并不强求你传入所需参数(4个,可传1个后续补上即可,最后一次凑齐4个即可)
function add(a, b, c) {
return a + b + c;
}
function CurryAdd(fn){
let arg = [].slice.call(arguments,1);
return function(){
let newArg = arg.concat([].slice.call(arguments,0));
return fn.apply(this,newArg);
}
}
function Curry(fn,length){
let len = length|| fn.length; //获取传入函数所需参数的个数
return function(){
if(arguments.length <len){
const callback = [fn].concat([].slice.call(arguments,0));
return Curry(CurryAdd.apply(this,callback),len-arguments.length);
}else{
return fn.apply(this,arguments);
}
}
}
let adds=new Curry(add)
let a = adds(1)(2)
console.log(a(1)); //4
以上完善柯里化函数的整个书写。下面来捋一下这个书写过程的思路
- 首先柯里化函数期待传入一个函数,并且返回一个函数(add)
- 通过fn.length获取当前以传入的参数个数
- 在返回函数中判断当前参数是否已传入完毕
- 如果已传入fn.length个 则直接调用传入函数
- 如果未传入fn.length个 则通过callback将fn放到第一位在进行合并arguments作为下一次进入此函数的参数,通过CurryAdd 函数对参数再进行一遍"过滤",通过递归调用自己来判断参数是否已经达到fn.length个从而实现柯里化
柯里化应用
说了这么多,那么柯里化到底能做哪些应用呢?
在前端页面中,向后端进行数据请求时,大部分都用到ajax进行请求
function ajax(method,url,data){
...ajax请求体(不作书写)
}
ajax("post","/api/getNameList",params)
ajax("post","/api/getAgeList",params)
ajax("post","/api/getSexList",params)
如果有这么多请求且每次都需要写请求方式("post"),页面多了请求多了自然成为冗余代码,那么优化一下
const newAjax = Curry(ajax);
const postAjax = newAjax("post")
...
如果url还有类似的那么就可以重复以上的代码,这样能减少相同代码重复出现
来源:juejin.cn/post/7389049604632166427
背调,程序员入职的紧箍咒
首先说下,目前,我的表哥自己开一家小的背调公司,所以我在跟他的平时交流中,了解到了背调这个行业的一些信息。
今天跟大家分享出来,给大家在求职路上避避坑。
上周的某天,以前的阿里同事小李跟我说,历经两个月的面试,终于拿到了开水团的offer。我心里由衷地替他高兴,赶紧恭喜了他,现在这年头,大厂的offer没这么好拿的。
又过了两周,小张沮丧地跟我说,这家公司是先发offer后背调,结果背调之后,offer GG了,公司HR没有告知他具体原因,只是委婉地说有缘自会再相见。(手动狗头)
我听了,惋惜之余有些惊讶,问了他具体情况。
原来,小李并没有在学历上作假,也没有做合并或隐藏工作经历的事。
他犯错的点是,由于在上家公司,他跟他老板的关系不好,所以他在背调让填写上级领导信息的时候,只写了上级领导的名字,电话留的是他一个同事的。
我听后惋惜地一拍脑门儿,说:“你这么做之前,怎么也不问我一下啊?第三方背调公司进行手机号和姓名核实,都是走系统的,秒出结果。而且,这种手机号的机主姓名造假,背调结果是亮红灯的,必挂。”
小李听后,也是悔得肠子都青了,没办法,只能重新来过了。
我以前招人的时候,遇到过一次这样的情况,当时有个候选人面试通过,发起背调流程。一周后,公司HR给了我一份该候选人背调结果的pdf,上面写着:
“候选人背调信息上提供,原公司上级为郭xx,但经查手机号主为王xx,且候选人原公司并无此人。”
背调结果,红灯,不通过。
基本面
学历信息肯定不能造假,这个大家应该都清楚,学信网不是吃素的,秒出结果。
最近两份工作的入离职时间不要出问题,这个但凡是第三方背调,近两份工作是必查项,而且无论是明察还是暗访,都会查得非常仔细,很难钻空子的。
再有就是刚才说的,手机号和人名要对上,而且这个人确实是在这家公司任职的。
大家耳熟能详的大厂最好查,背调公司都有人才数据库的,而且圈子里的人也好找。再有就是,随便找个内部员工,大厂的组织结构在内部通讯软件里都能看到的。
小厂难度大一些,如果人才数据库没有的话,背调员会从网上找公司电话,然后打给前台,让前台帮忙找人。但有的前台听了会直接挂断电话。
薪资方面,不要瞒报,一般背调公司会让你打印最近半年或一年的流水,以及纳税信息。
直接上级
这应该也是大家最关心的问题之一。
马云曾经说过:离职无非两种原因,钱没给够,心委屈了。而心委屈了,绝大多数都跟自己的直接上级有关。
如果在背调的时候,担心由于自己跟直接上级关系不好,从而导致背调结果不利的话,可以尝试以下三种方式。
第一,如果你在公司里历经了好几任领导的话,可以留关系最好的那任领导的联系方式,这个是在规则允许范围内的。
第二,如果你的直接上级只是一个小组长,而你跟大领导(类似于部门负责人)关系还可以的话,可以跟大领导沟通一下,然后背调留他的信息。像这个,一般背调公司不会深究的。
就像我的那个表哥,背调公司的老板所说的:“如果一个腾讯员工,马化腾都出来给他做背调了,那我们还能说什么呢?”
第三,如果前两点走不通的话,还可以坦诚地跟HR沟通一次,说明跟上级之间确实存在一些问题,原因是什么什么。
比如:我朋友遇到了这种情况,公司由于经营不善而裁员,老板竟然无耻地威胁我朋友,如果要N+1赔偿的话,背调就不会配合。
如果你确实不是责任方的话,一般HR也能理解。毕竟都是打工人,何苦相互为难呢。
你还可以这么加上一句:“我之前工作过的公司,您背调哪家都可以,我的口碑都很好的,唯独这家有些问题。”
btw:还有一些朋友,背调的时候留平级同事的真实电话和姓名,用来冒充领导,这个是有风险的。但是遇到背调不仔细的公司,也能通过。通过概率的话,一半一半吧。
就像我那个朋友所说:“现在人力成本不便宜,如果公司想盈利的话,我的背调员一天得完成5个背调,平均不到两个小时一个。你总不能希望他们个个都是名侦探柯南吧。”
信用与诉讼
一般来讲,背调的标准套餐还包括如下内容:金融违规、商业利益冲突、个人信用风险和有限民事诉讼。其中后两个大家尽量规避。
个人信用风险包括:网贷/逾期风险、反欺诈名单和欠税报告。
网贷这块,当时我有一个同事,2021年的时候,拿了4个offer,结果不明不白地都挂在了背调上,弄得他很懵逼。
当他问这三家公司HR原因的时候,HR都告诉他不便透露。
最后,他动用身边人脉,才联系上一家公司的HR出来吃饭,HR跟他说:“以后网贷不要逾期,尤其是不同来源的网贷多次逾期。”
同事听了,这才恍然大悟。
欠税这个,就更别说了,呵呵,大家都懂,千万别心存侥幸。
再说说劳动仲裁和民事诉讼。
现在有些朋友确实法律意识比较强,受到不公正待遇了,第一想法就是“我要去仲裁”,仲裁不满意了,就去打官司。
首先我要说的是,劳动仲裁是查不到的,所以尽量在这一步谈拢解决。
但民事诉讼在网上都是公开的,而且第三方背调公司也是走系统的,一查一个准儿。如果非必要的话,尽量不要跟公司闹到这一步。
如果真遇到垃圾公司或公司里的垃圾人,第一个想法应该是远离,不要让他们往你身上倒垃圾。
尤其是你主动跟公司打官司这种,索要个加班费、年终奖什么的,难免会让新公司产生顾虑,会不会我offer的这名候选人,以后也会有对簿公堂的一天。
结语
现在这大市场行情,求职不易,遇到入职前背调更是如履薄冰,希望大家都能妥善处理好,一定要避免节外生枝的情况发生,不要在距离成功一米的距离倒下。
最后,祝大家工作顺利,纵情向前,人人都能拿到自己满意的offer,开开心心地入职。
来源:juejin.cn/post/7295160228879204378
Python: 深入了解调试利器 Pdb
Python是一种广泛使用的编程语言,以其简洁和可读性著称。在开发和调试过程中,遇到错误和问题是不可避免的。Python为此提供了一个强大的调试工具——Pdb(Python Debugger)。Pdb是Python标准库中自带的调试器,可以帮助开发者跟踪代码执行、查看变量值、设置断点等功能。本文将详细介绍Pdb的使用方法,并结合实例展示其强大的调试能力。

1. Pdb简介
Pdb是Python内置的调试器,支持命令行操作,可以在Python解释器中直接调用。Pdb提供了一系列命令来控制程序的执行,查看和修改变量值,甚至可以在运行时修改代码逻辑。
2. 如何启动Pdb
在Python代码中启动Pdb有多种方式,以下是几种常见的方法:
2.1 在代码中插入断点
在代码中插入import pdb; pdb.set_trace()可以在运行到该行时启动Pdb:
def factorial(n):
if n == 0:
return 1
else:
return n * factorial(n-1)
import pdb; pdb.set_trace()
print(factorial(5))
2.2 通过命令行启动
可以通过命令行启动Python脚本,并在需要调试的地方使用pdb模块:
python -m pdb myscript.py
3. Pdb的基本命令
Pdb提供了许多命令来控制调试过程,以下是一些常用命令:
b(break): 设置断点c(continue): 继续执行程序直到下一个断点s(step): 进入函数内部逐行执行n(next): 执行下一行,不进入函数内部p(print): 打印变量的值q(quit): 退出调试器
4. 实战示例
让我们通过一个具体的例子来演示Pdb的使用。假设我们有一个简单的Python脚本,用于计算列表中元素的平均值:
def average(numbers):
total = sum(numbers)
count = len(numbers)
return total / count
numbers = [1, 2, 3, 4, 5]
print(average(numbers))
4.1 设置断点并启动调试
我们希望在计算平均值之前检查total和count的值:
import pdb; pdb.set_trace()
def average(numbers):
total = sum(numbers)
count = len(numbers)
return total / count
numbers = [1, 2, 3, 4, 5]
print(average(numbers))
运行上述代码,当程序执行到pdb.set_trace()时,将进入调试模式:
PS C:\src\uml\2024\07> python -m pdb myscript.py
> c:\src\uml\2024\07\myscript.py(1)<module>()
-> import pdb; pdb.set_trace()
(Pdb) n
> c:\src\uml\2024\07\myscript.py(3)<module>()
-> def average(numbers):
(Pdb) m
*** NameError: name 'm' is not defined
(Pdb) n
> c:\src\uml\2024\07\myscript.py(8)<module>()
-> numbers = [1, 2, 3, 4, 5]
(Pdb) n
> c:\src\uml\2024\07\myscript.py(9)<module>()
-> print(average(numbers))
(Pdb) n
3.0
--Return--
> c:\src\uml\2024\07\myscript.py(9)<module>()->
-> print(average(numbers))
4.2 查看变量值
在调试模式下,可以使用p命令查看变量值:
(Pdb) p numbers
[1, 2, 3, 4, 5]
(Pdb)
通过这种方式,可以一步步检查变量的值和程序的执行流程。
5. 高级功能
除了基本命令,Pdb还提供了许多高级功能,如条件断点、调用栈查看等。
5.1 查看调用栈
使用where命令可以查看当前的调用栈:
(Pdb) where
<frozen runpy>(198)_run_module_as_main()
<frozen runpy>(88)_run_code()
c:\users\heish\miniconda3\lib\pdb.py(1952)<module>()->
-> pdb.main()
c:\users\heish\miniconda3\lib\pdb.py(1925)main()
-> pdb._run(target)
c:\users\heish\miniconda3\lib\pdb.py(1719)_run()
-> self.run(target.code)
c:\users\heish\miniconda3\lib\bdb.py(600)run()
-> exec(cmd, globals, locals)
<string>(1)<module>()->
> c:\src\uml\2024\07\myscript.py(9)<module>()->
-> print(average(numbers))
6. 总结
Pdb是Python提供的一个功能强大的调试工具,掌握它可以大大提高代码调试的效率。在开发过程中,遇到问题时不妨多利用Pdb进行调试,找出问题的根源。通过本文的介绍,希望大家能够更好地理解和使用Pdb,为Python编程之路增添一份助力。
来源:juejin.cn/post/7392439754678321192
安卓开发转鸿蒙开发到底有多简单?
前言
相信各位搞安卓的同学多多少少都了解过鸿蒙了,有些一知半解而有些已经开始学习起来。那这个鸿蒙到底好不好搞?要不要搞?
安卓反正目前工作感觉不好找,即便是上海这样的大城市也难搞,人员挺饱和的。最近临近年关裁员的也很多。想想还是搞鸿蒙吧现在刚刚要起步说不定有机会!
首先可以肯定的一点,对于做安卓的来说鸿蒙很好搞,究竟有多好搞我来给大家说说。最近开始学鸿蒙,对其开发过程有了一定了解。刚好可以进行一些对比。
好不好搞?
开发环境
要我说,好搞的很。首先开发环境一样,不是说长得像,而是就一模一样。

你看这个DevEco-Studio和Android Studio什么关系,就是双胞胎。同样基于Intellj IDEA开发, 刚装上的时候我都惊呆了,熟悉的感觉油然而生。
再来仔细看看:
- 项目文件管理栏,同样可以切换Project和Packages视图

- 底部工具栏,文件管理,日志输出,终端,Profiler等

- SDK Manager, 和安卓一样也内建了SDK管理器,可以下载管理不同版本的SDK

- 模拟器管理器

可以看出鸿蒙开发的IDE是功能完备并且安卓开发人员可以无学习成本进行转换。
开发工具
安卓开发中需要安装Java语言支持,由于开发过程需要进行调试,adb也是必不可少的。
在鸿蒙中,安装EcoDev-Studio后,可以在IDE中选择安装Node.js即可。由于鸿蒙开发使用的语言是基于TS改进增强而来,也就是熟悉JS语言就可以上手。而会JAVA的话很容易可以上手JS
- 语言支持

- 鸿蒙上的类似adb的工具名叫hdc
hdc(HarmonyOS Device Connector)是HarmonyOS为开发人员提供的用于调试的命令行工具,通过该工具可以在windows/linux/mac系统上与真实设备或者模拟器进行交互。
- hdc list targets
- hdc file send local remote
- hdc install package File
这里列举的几个命令是不是很熟悉?一看名字就知道和安卓中的adb是对应关系。不需要去记忆,在需要使用到的时候去官网查一下就行: hdc使用指导
配置文件
安卓中最主要的配置文件是AndroidManifest.xml。 其中定义了版本号,申明了页面路径,注册了广播和服务。并且申明了App使用的权限。
而鸿蒙中也对应有配置文件,但与安卓稍有不同的是鸿蒙分为多个文件。
- build-profile.json5
Sdk Version配置在这里, 代码的模块区分也在这里
{
"app": {
"signingConfigs": [],
"compileSdkVersion": 9,
"compatibleSdkVersion": 9,
"products": [
{
"name": "default",
"signingConfig": "default",
}
],
"buildModeSet": [
{
"name": "debug",
},
{
"name": "release"
}
]
},
"modules": [
{
"name": "entry",
"srcPath": "./entry",
"targets": [
{
"name": "default",
"applyToProducts": [
"default"
]
}
]
}
]
}
- app.json5
包名,VersionCode,VersionName等信息
{
"app": {
"bundleName": "com.example.firstDemo",
"vendor": "example",
"versionCode": 1000000,
"versionName": "1.0.0",
"icon": "$media:app_icon",
"label": "$string:app_name"
}
}
- module.json5
模块的详细配置,页面名和模块使用到的权限在这里申明
{
"module": {
"name": "entry",
"type": "entry",
"description": "$string:module_desc",
"mainElement": "EntryAbility",
"deviceTypes": [
"phone",
"tablet"
],
"deliveryWithInstall": true,
"installationFree": false,
"pages": "$profile:main_pages",
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ts",
"description": "$string:EntryAbility_desc",
"icon": "$media:icon",
"label": "$string:EntryAbility_label",
"startWindowIcon": "$media:startIcon",
"startWindowBackground": "$color:start_window_background",
"exported": true,
"skills": [
{
"entities": [
"entity.system.home"
],
"actions": [
"action.system.home"
]
}
]
}
],
"requestPermissions":[
{
"name" : "ohos.permission.APPROXIMATELY_LOCATION",
"reason": "$string:reason",
"usedScene": {
"abilities": [
"FormAbility"
],
"when":"inuse"
}
}
]
}
}
官方指导
安卓开发的各种技术文档在网上可以很方便的搜索到,各种demo也有基数庞大的安卓开发者在技术网站上分享。虽然鸿蒙目前处于刚起步的阶段,但是官方的技术文档目前也已经非常完善,并且可以感受到鸿蒙的官方维护团队肯定在高强度加班中,他们的文档更新的太快了。经常能看到文档的编辑日期在迅速迭代。


从日期可以看到非常新。而且文档都是中文的,学习和查找起来都特别方便。
并且不仅仅是api文档,鸿蒙官方还提供了各种用以学习的demo, 甚至还有官方的视频教程和开发论坛。




遇到问题有各种方法可以解决,查文档,看视频课程,抄官方demo, 论坛发帖提问,简直是保姆级的官方支持!
其他
- 鸿蒙的UI开发模式是一种响应式开发,与安卓的compose UI很像。组件的名字可能不同,但是概念上是一致的,并且鸿蒙的原生组件种类丰富也比较全。熟悉以后使用起来很方便。
build() {
Column() {
Text(this.accessText)
.fontSize(20)
.fontWeight(FontWeight.Bold)
if (!this.hasAccess) {
Button('点击申请').margin({top: 12})
.onClick(() => {
this.reqPermissionsFromUser(this.permissions);
})
} else {
Text('设备模糊位置信息:' + '\n' + this.locationText)
.fontSize(20)
.margin({top: 12})
.width('100%')
}
}
.height('100%')
.width('100%')
.padding(12)
}
- 对应安卓的权限管理
鸿蒙有ATM,ATM (AccessTokenManager) 是HarmonyOS上基于AccessToken构建的统一的应用权限管理能力。
- 对应安卓的SharedPreferences能力,鸿蒙有首选项能力。

这里就不一一列举了
我们只需要知道在安卓上有的概念,就可以在鸿蒙官方文档中去找一下对应的文档。
原理都是相通的。所以有过安卓开发经验的同学相对于前端FE来说有对客户端开发理解的优势。
要不要搞?
先看看目前的情况, 各家大厂正在积极布局鸿蒙客户端开发。



虽说移动端操作系统领域对安卓和iOS进行挑战的先例也有且还没有成功的先例。但是当前从国内互联网厂商的支持态度,从国际形势的情况,从华为对鸿蒙生态的投入来看。 我觉得很有搞头!
明年鸿蒙即将剔除对安卓的支持,届时头部互联网公司的大流量App也将完成鸿蒙原生纯血版的开发。
更有消息称鸿蒙PC版本也在路上了,了解信创的朋友应该能感受到这将意味着国产移动端和PC端操作系统会占有更大比例的市场。不仅仅是企业的市场行为,也是国产操作系统快速提升市占率的大好时机。
话说回来,作为安卓开发者,学习鸿蒙的成本并不高!
而对我们来说这是个机遇,毕竟技多不压身,企业在选取人才的时候往往也会偏好掌握更多技术的候选人。
如果鸿蒙起飞,你要不要考虑乘上这股东风呢?
我是张保罗,一个老安卓。最近在学鸿蒙
来源:juejin.cn/post/7308001278420320275
只会Vue的我,一入职就让用React,用了这个工具库,我依然高效
由于公司最近项目周期紧张,还有一个项目因为人手不够排不开,时间非常紧张,所以决定招一个人来。这不,经过一段时间紧张的招聘,终于招到了一个前端妹子。妹子也坦白过,自己干了3年,都是使用的Vue开发,自己挺高效的。但如果入职想用React的话,会稍微费点劲儿。我说,没事,来就是了,我们都可以教你的。
但入职后发现,这个妹子人家一点也不拖拉,干活很高效。单独分给她的项目,她比我们几个干的还快,每天下班准时就走了,任务按时完成。终于到了分享会了,组长让妹子准备准备,分享一下高效开发的秘诀。
1 初始化React项目
没想到妹子做事还挺认真,分享并没有准备个PPT什么的,而是直接拿着电脑,要给我们手动演示她的高效秘诀。而且是从初始化React项目开发的,这让我们很欣慰。
首先是初始化React项目的命令,这个相信大家都很熟悉了:
第一步:启动终端
第二步:npm install -g create-react-app
第三步:create-react-app js-tool-big-box-website
(注意:js-tool-big-box-website是我们要创建的那个项目名称)
第四步:cd js-tool-big-box-website
(注意:将目录切换到js-tool-big-box-website项目下)
第五步:npm start
然后启动成功后,可以看到这样的界面:

2 开始分享秘诀
妹子说,自己不管使用Vue,还是React,高效开发的秘诀就是 js-tool-big-box 这个前端JS库
首先需要安装一下: npm install js-tool-big-box
2.1 注册 - 邮箱和手机号验证
注册的时候,需要验证邮箱或者手机号,妹子问我们,大家平时怎么验证?我们说:不是有公共的正则验证呢,就是验证一下手机号和邮箱的格式呗,你应该在utils里加了公共方法了吧?或者是加到了表单验证里?
妹子摇摇头,说,用了js-tool-big-box工具库后,会省事很多,可以这样:
import logo from './logo.svg';
import './App.css';
import { matchBox } from 'js-tool-big-box';
function App() {
const email1 = '232322@qq.com';
const email2 = '232322qq.ff';
const emailResult1 = matchBox.email(email1);
const emailResult2 = matchBox.email(email2);
console.log('emailResult1验证结果:', emailResult1); // true
console.log('emailResult2验证结果:', emailResult2); // false
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
js-tool-big-box,使React开发更加高效
</header>
</div>
);
}
export default App;
2.2 验证密码强度值
验证密码强度值的时候呢,妹子问我们,大家平时怎么验证?我们说:不就是写个公共方法,判断必须大于几位,里面是否包含数字,字母,大写字母,特殊符号这样子吗?
妹子摇摇头,说,不是,我们可以这样来验证:
const pwd1 = '12345';
const pwd1Strength = matchBox.checkPasswordStrength(pwd1);
console.log('12345的密码强度值为:', pwd1Strength); // 0
const pwd2 = '123456';
const pwd2Strength = matchBox.checkPasswordStrength(pwd2);
console.log('123456的密码强度值为:', pwd2Strength); // 1
const pwd3 = '123456qwe';
const pwd3Strength = matchBox.checkPasswordStrength(pwd3);
console.log('123456qwe的密码强度值为:', pwd3Strength); // 2
const pwd4 = '123456qweABC';
const pwd4Strength = matchBox.checkPasswordStrength(pwd4);
console.log('123456qweABC的密码强度值为:', pwd4Strength); // 3
const pwd5 = '123@456qwe=ABC';
const pwd5Strength = matchBox.checkPasswordStrength(pwd5);
console.log('123@456qwe=ABC的密码强度值为:', pwd5Strength); // 4
2.3 登录后存localStorage
登录后,需要将一些用户名存到localStorage里,妹子问,我们平时怎么存?我们说:就是直接拿到服务端数据后,存呗。妹子问:你们加过期时间不?我们说:有时候需要加。写个公共方法,传入key值,传入value值,传个过期时间,大家不都是这样?
妹子摇摇头,说,不是,我们可以这样来存:
import { storeBox } from 'js-tool-big-box';
storeBox.setLocalstorage('today', '星期一', 1000*6);
2.4 需要判断是否手机端浏览器
我们市场需要判断浏览器是否是手机端H5浏览器的时候,大家都怎么做?我们说:就是用一些内核判断一下呗,写好方法,然后在展示之处判断一下,展示哪些组件?不是这样子吗?
妹子又问:我这个需求,老板比较重视微信内置的浏览器,这样大家写的方法是不是就比较多了?我们说,那再写方法,针对微信内置浏览器的内核做一下判断呗。
妹子摇摇头,说,那样得写多少方法啊,可以用这个方法,很全面的:
如果你单纯的只是想判断一下是否是手机端浏览器,可以这样:
import { browserBox } from 'js-tool-big-box';
const checkBrowser = browserBox.isMobileBrowser();
console.log('当前是手机端浏览器吗?', checkBrowser);
如果你需要更详细的,根据内核做一些判断,可以这样:
const info = browserBox.getBrowserInfo();
console.log('=-=-=', info);
这个getBrowserInfo方法,可以获取更详细的ua,浏览器名字,以及浏览器版本号
2.5 日期转换
妹子问,大家日常日期转换怎么做?如果服务端给的是一个时间戳的话?我们说:不就是引入一个js库,然后就开始使用呗?
妹子问:这次产品的要求是,年月日中间不是横岗,也不是冒号,竟然要求我显示这个符号 “~” ,也不是咋想的?然后我们问:你是不是获取了年月日,然后把年月日中间拼接上了这个符号呢?
妹子摇摇头,说,你可以这样:
import { timeBox } from 'js-tool-big-box';
const dateTime2 = timeBox.getFullDateTime(1719220131000, 'YYYY-MM-DD', '~');
console.log(dateTime2); // 2024~06~24
2.6 获取数据的详细类型
妹子问,大家日常获取数据的类型怎么获取?我们说,typeof呀,instanceof呀,或者是
Object.prototype.toString.call 一下呗,
妹子摇摇头,说,你可以这样:
import { dataBox } from 'js-tool-big-box';
const numValue = 42;
console.log('42的具体数据类型:', dataBox.getDataType(numValue)); // [object Number]
const strValue = 'hello';
console.log('hello的具体数据类型:', dataBox.getDataType(strValue)); // [object String]
const booleanValue = true;
console.log('true的具体数据类型:', dataBox.getDataType(booleanValue)); // [object Boolean]
const undefinedValue = undefined;
console.log('undefined的具体数据类型:', dataBox.getDataType(undefinedValue)); // [object Undefined]
const nullValue = null;
console.log('null的具体数据类型:', dataBox.getDataType(nullValue)); // [object Null]
const objValue = {};
console.log('{}的具体数据类型:', dataBox.getDataType(objValue)); // [object Object]
const arrayValue = [];
console.log('[]的具体数据类型:', dataBox.getDataType(arrayValue)); // [object Array]
const functionValue = function(){};
console.log('function的具体数据类型:', dataBox.getDataType(functionValue)); // [object Function]
const dateValue = new Date();
console.log('date的具体数据类型:', dataBox.getDataType(dateValue)); // [object Date]
const regExpValue = /regex/;
console.log('regex的具体数据类型:', dataBox.getDataType(regExpValue)); // [object RegExp]
2.8 更多
估计妹子也是摇头摇的有点累了,后来演示的就快起来了,我后来也没听得太仔细,大概有,
比如我们做懒加载的时候,判断某个元素是否在可视范围内;
比如判断浏览器向上滚动还是向下滚动,距离底部和顶部的距离;
比如某个页面,需要根据列表下载一个excel文件啦;
比如生成一个UUID啦;
比如后面还有将小写数字转为大写中文啦,等等等等
3 最后
分享完了第二天,妹子就没来,我们还准备请教她具体js-tool-big-box的使用心得呢。据说是第一天分享的时候,摇头摇得把脖子扭到了,希望妹子能早日康复,早点来上班。
最后告诉你个消息:
js-tool-big-box的npm地址是(js-tool-big-box 的npm地址)
js-tool-big-box的git仓库地址(js-tool-big-box的代码仓库地址)
来源:juejin.cn/post/7383650248265465867
绑定大量的的v-model,导致页面卡顿的解决方案
绑定大量的的v-model,导致页面卡顿的解决方案
设计图如下:

页面布局看着很简单使用element组件,那就完蛋了,因为是大量的数据双向绑定,所以使用组件,延迟非常高,高到什么程度,请求100条数据到渲染到页面上,要10-12s,特别是下拉选择的时候,延迟都在2-3s,人麻了老铁!!!

卡顿的原因很长一段时间都是在绑定v-model,为什么绑定v-model会很卡呢,请求到的每一条数据有14个数据需要绑定v-model,每次一请求就是100个打底,那就是1400个数据需要绑定v-model;而且组件本身也有延迟,所以这个方案不能采用,那怎么做呢?
我尝试采用原生去写,写着写着,哎解决了!!!惊呆了

做完后100条数据页面渲染不超过2s,毕竟还是需要绑定v-model,能在2s内,我还是能接受的吧;选择和输入延迟基本没有

下面就来展示一下我的代码,写的不好看着玩儿就好了:

请求到的数据:





methods这两个事件做的什么事儿呢,就是手动将数据绑定到数据上去也就是row上如图:

当然还有很多解决方案
来源:juejin.cn/post/7392248233222881316
那些年,我在职场中做过的蠢事
大家好,我是程序员马晓博,目前从事前端行业已有5年有余,而近期由于被裁员有一段时间,也开始回顾自己的过往,发现自己以前在职场中,做过不少傻事,这里就写篇文章来记录下。曾经的犯傻已不可避免,索性公之于众,坦然面对。
ps: 像一直戴工牌,故意露个工牌带子在外面就不说了,谁还没年轻过呢?还真谁也别笑话谁哈哈。
自诩正义强出头
这是我在腾讯实习的时候遇到的,故事很简单,有个同事加了一个全局错误捕获的逻辑,导致原本有报错但是能够正常运行的程序,出现了线上 bug。
此时团队间要追究责任,认为是加了全局捕获错误的同事的责任。从现在的视角看,加了全局错误捕获同事自然是有问题的,但是当时的我,非常正义的认为,写 bug 的人才应该承担责任,而为了让代码更健壮写了全局错误捕获的同事是没有错的。
现在回想起当时自己义正严辞的发言,真是太年轻啦。
更何况这事我连当事人都算不上,只能安慰自己:谁还没年轻过呢?
平易近领导
这个事,也是发生在腾讯实习期间。
在来实习之前的我,深受互联网扁平化管理,工位不做区别,这些非常先进的思想影响,到了腾讯之后,听领导说,自己平时喜欢游泳,我一想我也喜欢啊,就直接跑领导工位上问,你平时在哪里游泳?
吓的我的直属上级,直接跑出来拉住我,我给你推荐,我带你游泳。
还有一次是,午休一起打王者荣耀,人多了把我空出来了,我就指导领导玩亚瑟,上来就说你这出装太肉了,没一点伤害,要怎么怎么玩。
我现在还能想起来他当时的眼神,你,是在教我做事?
对于领导,毫无距离感。这应该是很多年轻人都会有的心态,还会认为这就是年轻人的本色,是互联网的特色,而且会对奉承领导的人嗤之以鼻。
当然,互联网公司文化本身也都提倡这种,没有上下级,大家都能上。也就是倡导所谓扁平化管理,工位也是领导普通员工没区别。
曾经的我认为这一切都是问题不大的,就是扁平化,互联网就是不一样,但是透过一些其他行业的人,我虽不介意工位情况,但是难免对其底层所宣扬的扁平化产生一定的怀疑。
ps: 这里放一个其他行业的人的对互联网工位的一些看法,我妹妹(中国移动打过一段时间工)来参观了我的工位之后说了一句话:这是工位?牛马间吧这是,我不以为然。直到我看到了她的工位,差别还真不是一般的大,好歹有隔间。这里应该放一张图,但是我没有,大家自行脑补吧哈哈。
自诩性格真诚直率
可谓是初生牛犊不怕虎吧,看到认为拉垮的代码,就会找同事当面聊,应该怎么怎么样,不应该怎么怎么写。
但是实际上,代码写成你认为的不合理的样子,往往是很多因素导致的,或工期,或对方当时也是初学者,或团队风格,或当时环境,或仅仅是对方对新方案的尝试。
上下文不了解,就开始吐槽。但是实际上人家的代码线上运行毕竟没有问题,没有故障的代码,本身就是一份合格的代码以及对方能力的认证。
而我的当面不友好交流,真是一点礼貌没有。还美名其曰,性格比较直率真诚。
好在当时的同事比较友好,并未计较,还在我后来选房子的时候还提供了很大的帮助,可叹没有仔细聆听对方的教诲终究还是没有十全十美。
夜郎自大而不自知
这是在我工作两年左右时候产生的一种感觉,觉得自己完成业务没有任何压力,而且还承担了一些比较重要的工作,从而有一种觉得自己很行的错觉。
但是当时面试很快就泼了一盆冷水,一般来讲,这个阶段做业务的同学,应对业务开发其实基本都没有什么问题。
但是国内对程序员的面试根本不限于业务,深挖一些知识点,理解其原理才是及格线。
而当时的我就是一直停留在使用阶段,用好本身没有问题,但是奈何不足以应对面试。
当然,心态还是最重要的。半瓶水晃荡而不自知才是最可怕的。
开弓来个回头箭
这个事,说实话有点羞于启齿。
是我工作大概第四个年头发生的,那时我在网易工作有一年多一些。
由于自己做了一个还算比较有技术难度的项目,想要寻求晋升,结果当时的晋升期答辩都结束了自己还不知晓。
心里有闷气,就开始面试找工作,也顺利拿到了几个涨幅非常不错的 offer。
开始跟上级提离职,哈哈,对方聊了下,也答应了。
结果我自己晚上就是睡不着,始终觉得自己这个时候走,是逃避,是逃兵。而且这个时候走,之前的积累就全部白费,新公司还得从零做起。
网上都说开弓没有回头箭。但是我就还是厚着脸皮来个回头箭。
不得不说,这个决定并不算蠢事,我在整件事里最蠢的是没有想好就和上级提了离职,虽然拿了 offer,但是没有想清楚就离职,是非常不成熟的表现。
好在我的上级,也主打一个真诚,也明确说明,想清楚了就行。
接下来一年的合作非常愉快,既有可视化埋点平台这样的业务技术都有挑战的项目,也有团队状态管理方案的产出,顺利在第二年迎来了自己的晋升。
这一次,愚蠢更多是在于自己没有想清楚就开弓,而真诚待人在我看来是双向必杀技,但真诚也为我后来吃亏埋下了种子。
整体而言,在网易的几年,领导,同事,大家都比较真诚,不屑于暗地里去做一些掉份的事情,也让我在职场上,形成了真诚而缺少防范的一个问题,这在我的下一步职业生涯中,给我带来了比较大的打击和跟头。
和同事交往讲真诚
这是我在离开网易后,选择的一家规模比较小的公司。
这时候,我工作已经整整 5年了,但是我过往的经历终究让我缺少了一些对同事的防范,大公司还好,大家相互之间,利益冲突不大,更多的是合作关系,同时由于大家或多或少都有自己的一点点的"骄傲",所以其实并没有遇到一些因为利益冲突而导致的暗箭。
而过往的经历也在告诉我,真诚,并不会带来什么问题。
真诚无错,但是说者无意听者有心。
到新公司之后,也到了该带人的职级,此时,我还是主打真诚,很快就和团队融为一片。
几个关系近的同事和下属,知道我家里买了几套房,知道我平时看的书,知道我平时都在干啥,知道我对生活和工作的态度,知道我在工作上的安排。
这些事情,平时没有什么问题,但是当和有心的同事出现利益冲突的时候,这些事情就成为一把利剑,间接导致我失去了这份工作。
而这些利剑,是我亲手递给了对方。
对职场恶意的容忍
如果说真诚是给别人递了一把利剑,那么自己的容忍和锋芒的隐藏,是我自己收起了盾牌。
我在周围的同事身上,总能看到自己的影子,所以对于他们的恶意,往往有一定程度的容忍,我觉得,年轻人嘛,有点锋芒,很正常。
比如,当他们吹嘘自己写了一篇文章,获得了几个赞的时候,我往往是进行倾听并表示赞赏,虽然几个赞的文章其实真的很简单。又或者公开场合提出质疑,虽然我会讲道理,理可以辩明,但是对于这其中的恶意,我一般会选择包容。
但是就是这一步,自身锋芒的隐藏,在对方眼里却是得寸进尺的机会。
个人觉得,作为级别比对方高的,还是需要适时的漏出自身的锋芒,而不是仅仅倾听加赞赏,同时由于私下交往的密集,更导致对方的肆无忌惮。
从而亲手递给对方利剑,又自己收起盾牌。
只能说,在这条路上,我还是太稚嫩。
最后
以上,就是我个人认为在职场中,做过的一些蠢事。虽然已经工作了五年之久,但是这条路上,还是觉得太过稚嫩,谨以此文,纪念哪些蠢事!
ps: 不知道看完这篇的你,有没有回忆起一些类似的事情呢?欢迎交流哈。
// 还是那句话,都年轻过,谁也别笑话谁~
来源:juejin.cn/post/7357994849386102836
从20k到50k再到2k,聊聊我在互联网干前端的这几年
大家好,我是程序员晓博,目前从事前端行业已经有将近 6年。这六年,从最初的互联网鼎盛时期,到今年是未来十年内最好的一年,再到疫情时期的回暖,再到如今年年都喊寒冬的寒冬。从最初的 20k,到最近的一份 50k 的工作,再到如今的 "政府补贴" 2k,可谓是感悟颇多。
刚好最近 gap 一段时间,有所空闲,就整理下这几年的经历以及我所看到的行业的兴衰。
学生见闻
我是 2011 年读的大学,当时是电子科学与技术这个专业,并非计算机科班出身,更偏向于硬件编程,单片机,嵌入式,FPGA 这些会更多一些。
所以当时对于互联网行业的前端后端,并没有特别明确的概念,也对于 c++, java 这些语言的地位和适用性其实也没有明确的认知。
记得是 2012, 2013 年的时候,学校里经常有 java 培训班的宣传,说实话,那会还看不上 java,虽然自己也不会,但是学校里教的都是 c, c++, java 那会在我看来,更多的是一个 c++ 简化后的语言,所以对于我这个非科班的并没有提起兴趣。
现在回想起来,那时可真是入行的好时期,当然也是风云变换的几年。那会学校流传着一个段子: 你只要会安装 eclipse,就能找到一份美团的工作。而之后的一年,你得开发过自己的 app,才能找到安卓开发工作。
不过当时更多的观念告诉我至少得读个研究生出来,所以我选择了读研而非直接工作。可以说是错过了互联网飞速发展的黄金时期,直接毕业就来到了今年是未来十年里最好的一年。
也就是在研究生阶段,我才慢慢了解到,外界的互联网大厂,其实已经分化出了移动端,前端,后端这样的岗位,当时的前端圈最为活跃,而移动端,后端,似乎都已经定型。而前端圈的新框架此起彼伏,从 react, vue, webpack,还有很多已经消失在历史中的框架。
在当时的就业情况下,前端的工资似乎是最高的,在加上当时的前端圈确实很活跃,而学习起来也比较简单。作为非科班的学生,自学前端上手最快,所以我选择了前端作为自己的就业方向。专业对口的硬件开发就不说了,工资实在是大相径庭。
但是话说回来,硬件开发如今的热门程度,并不亚于当时的软件开发。硬件开发培训,挑战 30w 年薪这样的培训班,在 2022 年左右也出现了,一如当时 2015 年左右 java 开发包就业那样的火热。
不过这个专业,其实还给我带来了一份现在看起来可以称为副业的东西:代写课程设计和毕业设计,因为是比互联网前后端更细分的赛道,所以竞争并不激烈,我还是接到了不少的单子,但是由于自己本身也是学生,所以定价很低,基本按照 100/h 的费用在收,也有做代码复用的整合,但是在硬件这一块,它的售后并不像软件这样,往往需要花费时间帮用户在板子上走通,这一部分是比较花费时间的。也在研究生阶段尝试过转项目的方式来获取收益,但是由于定价过低以及单子并不多的问题,而没有继续。不过如今想来,借助 chatgpt 等 AI 工具,定价确实还能更低(尤其是包含论文的单子)。
第一份实习
出于提升自身竞争力的考虑,我在研究生阶段就开始了边自学,边找实习。好在自学的时间比较早,准备的也比较充分,顺利拿到了腾讯等几家公司的实习。
当时虽说还处于互联网发展的时期,但是竞争其实就已经比较激烈了,没有实习进大厂基本就是 hard 模式,我也是面试了 n 家公司,才拿到的 offer。
不过腾讯这个部门虽然在面试的时候,会问一些比较现代技术的问题,但是实际进去后,是写的 php 和 jquery。我的收获其实并不多,但是简历上好看一点,后来也顺利拿到了转正的 offer。
当时给的薪资应该是 16k 左右,还会加上城市的补贴大概 2k。不过最终因为房价的原因,并没有考虑留在深圳。
ps:我在实习的时候专门考察了深圳腾讯总部附近的房价,好像是 6w,确认过眼神,是掏光 6 个钱包也买不起的房子。不过据说一度涨到了 10w,现在没有再关注了,可能有所下跌吧。
说起来当时还有一个事让我印象比较深刻,也因此对阿里有了一些抵触。就是 2017:众多应届生被阿里毁了 offer。而对于这种事情,阿里给的解释是:拥抱变化。
可能马爸爸从那时就嗅到了危机,但这却是我第一次听说毁应届生 offer,非常败好感。ps: 现在这种毁应届生 offer 的事是非常常见啦。
似乎那时警钟已经敲响,但是我并没有未雨绸缪。
第一份工作
我是 2018 年毕业的,那会北京,上海,都有落户的限制,甚至还有一些积分制等似乎不欢迎应届生去上班的感觉。
那会刚毕业,可谓是心比天高,落个户都这么麻烦,我还不想去呢!还不如人深圳的口号,来了就是深圳人。而当时阿里的总部,就在杭州,而且杭州只要是大学生,立马就能落户,立马能摇号买房。而当时房价也比较亲民 (确认过眼神,是掏光钱包可以买得起的价格)。
所以基本上只找杭州的工作。最终入职了当时比较热门的 p2p 领域的独角兽,51 信用卡。
当时的 51信用卡,可以说是 p2p 领域的一只牛逼独角兽,甚至这家公司的缩写就是 51NB。
不过以我当时的认知,入职 51信用卡,纯粹是因为 20k 的薪资,以及全额报销来回路费。要知道当时 BAT 虽然有报销,但是实际上都有各种限制和上限。
ps: 以我当时的认知,几乎没有任何犹豫,我就关闭了我的副业通道,因为我觉得,精进前端技术,带来的收益更大,毕竟一个月 20k 的收入,更别提还有 4个月加的年终收入了。而这份副业,一方面对主业没有提升,同时还要消耗比较大的精力(主要集中在给学生讲解代码以及售后上),收入也就几千块而且时间比较集中,很难兼顾。
现在回过头来看,真的是误打误撞赶上了 p2p 行业的末班车。起始薪资确实不错,但是很快就来到了国家严控 p2p 行业的开端。
最终,入职当年,就遇到了一波一波的裁员,从开始的 n + 3,到 n + 2 再到 n + 1,可谓是一波一波的裁员。也包括了应届生。一如现在,应届生也还是裁员重灾区。
也因为 51 当时是杭州互联网第一波开启的裁员,还裁了应届生。口碑急转直下,但是很快就迎来了反转,隔壁滴滴,微店等也迅速开启裁员模式,仅仅只有 n + 1。
ps: 在 51 的第一年,是有年会的。第二年,年会倒是有,但是主题就是一句话,今年将是接下来十年内,最好的一年。也是这一年 2019,p2p 彻底宣告结束,51 也出现了警车上门的事件。最终借贷业务转型为依赖于银行的借贷业务。也结束了接近 10% 的储蓄利率时代。
短暂的阿里之旅
2020 年年初,p2p 行业宣告结束叠加疫情之初,悲观情绪四处蔓延。我在 51 的旅程也渐渐走到了尾声。
当时面试了字节和阿里。彼时的字节跳动,在杭州名气和规模还没有如今这么大。权衡之下选择了名气更盛,当时口碑更好的阿里。但是我对于字节的判断,实在是偏差的离谱,看着如今蒸蒸日上的字节,真是后悔莫及。
但是进了阿里,说实话是真有些不适应。
一方面生活上,不提供纸巾,让我颇为诧异,而时不时的团队聚餐竟然是 AA 也让我非常不适应。
当然,对方看我也很奇怪,说了一句话,感觉你是外企来的 (ps: 如今的 51信用卡还是有点小而美的感觉,各项业务依托也还在继续,也有露营等新业务的开拓,老板自由后也还在继续折腾着)。
因为疫情的原因,我并没有经历百阿培训。但是有一本小册子,写着价值观。让我印象深刻的是,"此时此刻,非我莫属" 和 "不难,要你做什么"。
这两句话听起来都没有什么问题,鼓励人奋进并没有任何问题,但是以前的奋斗,伴随着可能的巨大的回报,而我当时的付出与回报,显然已经是大打折扣了。
那时还没有 pua 的说法,但是确实有一些话让我觉得不舒服,比如目标要跳起来才是 3.5,蹦起来才是 3.75,以及业务好和你一点关系都没有,你把业务做好了,也只能给你 3.25。必须得出一些技术项目,才能拿到好绩效。
而那种没有人会点明但是大家都在执行的道理:和我 kpi 有关的就是天,无关的就是已读不回,而已读不回,就是拒绝。更是让当时还非常稚嫩的我想要逃之夭夭。
这些也不能说错,但是这确实和我在 5信用卡当时围着业务转的风格大相径庭。
说回技术,阿里整个集团的基建可以说非常好,反而我所在的这个小前端团队的基建,赶不上 51前端团队的基建。
不论是脚手架,发布系统(比较让我震惊的是我当时团队的发布是自己丢文件到服务器上,测试正式环境的区分还是靠手动维护的一份文件),开发流程,完全赶不上 51当时的丝滑程度。
可以说是对压力的逃避,也可以说是对这种环境的不适应,也可以说是对涨幅的不满,我很快就开启了下一段旅程。
现在回过头来看,当时离职,还是冲动占了大部分。一方面,随着后来业务接触的多了,能够理解当时那个小团队的基建差的原因:主要是业务形态,当时的小团队是 toB 的,要维护的仅仅是一个项目,自然在发布流程上的投入不会太多,而且收益也是远远不及 51 这种移动端几十上百个工程的发布工作来的实在。
而另一方面,所谓的深夜开会,不明说但是心里都清楚的加班氛围,以及唯 kpi 导向的风气,其实也不过是一种生存规则而已。强行说服自己接受也很容易。毕竟人生如戏,适应规则,利用规则,掌握规则,但凡能够想通这一点,当时坚持下来也非常容易。
长达三年的网易之旅
当处于阿里的水深火热之中时,一个在周末就完成了全部流程的网易团队,向我抛出了橄榄枝。
经过短暂的调整,我也就入职了这个团队,不曾想一待就是三年。此时的薪资来到了 30k 左右,但是由于当时这个团队独特的奖金制,月收入会比 base 高出不少。
团队的业务主要是直播,所以 toB 和 toC 的业务都有。基建上也比较完善,发布系统,组件库,脚手架,微前端,等等,相对更为繁荣。
这个团队并没有明确的技术项目的考核,还是以业务为主,大多数人,完成业务开发目标,就能够顺利拿到 3.5 的绩效,同时由于当时直播行业的繁荣,基本都会有一笔不菲的奖金。而技术项目属于锦上添花,确确实实能在最终的绩效上有所体现但是并不多。
但是恰恰是在这样的环境下,组内的同学在相对宽松的氛围下,更热衷于鼓捣技术项目,反而平时对技术的研究及讨论会更多一些。
这三年,也算是见证了业务的兴衰,从开始的营收暴涨开始出海,到最终营收暴跌收缩,到裁员。也不过短短三年。
现在回头来看,在这三年里,对于业务的了解更多的还是停留在表层,虽然当时觉得自己理解业务方的需求了,但是其实内部的很多玩法还是远非仅仅理解需求就能接触到的,什么大 R 运营,"军火商" 等等秀场直播的黑话,我是没有学到一点。
由于组内业务还算比较综合,c端页面的开发,b端后台都有所接触。同时业务之余还会有很多时间去做一些技术项目,比如我负责的 CloudIDE, WebIDE, 可视化埋点项目, 基于 zustand 的状态管理库, 均是这一时期的产物。
整体来讲,这三年不论是工作节奏,还是技术产出,都还算可以。
但是如今回过头来看,似乎这三年,对外界的关注,基本上有了一定的钝感,不像之前,对互联网的各个信息都会去了解看一下。反而这几年,说内敛沉稳也好,说闭门造车也好,说停留在自己的舒适圈内也好,除了技术层面的精进,对于整个行业的发展,都太过闭塞,仿佛只是重复一种舒适的生活过了三年:每天和老婆一起轮流开车上下班,顺便再健个身,住着自己的房子,还着公积金就能覆盖还有结余的带款。
如今回想起来,也正是这三年的经历,让我在技术上有所精进,但是对互联网行业的关注,反而有所下降。同时由于同事间的关系比较简单,也让我在人际交往上变得更加朴素真诚。
半年的小公司之旅
怎么说呢,好像人总是在不稳定的时候追求稳定,在稳定的时候追求不稳定。
所以在结束了网易的三年相对稳定的工作之后,我内心反而变得很躁动,想要去小公司,谋一番事业。
出来看机会之后,才发现外界的环境其实并没有平时了解的那么糟糕,确实不像之前机会那么多,但是确实也还有一些岗位。
在这之中,我选择了在发展业务第二曲线同时又有第一业务支持的说稳定又不稳定的公司 ---- 爱普拉维。
这家公司业务主要集中在海外,所以整体业务情况也还是非常客观。给出的薪资也比较客观,我的薪资也在这一时期,达到了 50k 左右。
不过入职之初,就经历了一些人事变动,如今想来,可以说是警醒,但是我应该是选择性的进行了忽视。心思沉浸在技术和一点点的管理上。
这个团队前端同学并不多,但是业务上除了常规的 h5 和少量的后台项目之外,还会存在一些 chrome 扩展逆向,爬虫项目的存在,而我被招进来的主要任务,也就是 chrome 扩展的逆向和爬虫项目。
在这一期间,我一度沉浸在了技术上的钻研中,从 webpack 的解码逆向,到 puppeteer 爬虫的实现,从 项目秒开的优化,到 svelte 的重构,都是对我之前技术经验的一个补充。也顺利在技术角度上在公司站稳了脚跟。度过了这个公司网上传言的不好过的试用期。
不过终归还是在人际交往上有所欠缺,叠加上公司的业务方向调整,导致了最终今年 1月份的离职。而这,也为我的职场画下了短暂的暂停键。
离职快小半年了
不知不觉离离职已经快小半年了,也顺利领到了失业金,也就是题目中提到的 2k。
这段时间从刚开始的玩乐,到中途的读书写文章,再到一些副业(对于无业人员来讲应该是主业)的探索。焦虑在所难免,未来也还比较迷茫,而其他主业的探索,说实话也没探索出来什么结果。
反倒是这段读书的时间给了我一些收获,一方面是 《穷爸爸富爸爸》中对于资产负债表的解释,我自己也做了一份,还参加了财富流沙盘游戏,对自己的财务状况有了更好的认知。另一方面便是 《认知觉醒》中关于焦虑的说法,一定程度上命中了当下的自己很多。
最后,就用《认知觉醒》中关于焦虑的根源来结束这篇文章吧:想同时做很多事,又想立即看到效果。自己的欲望大于能力,又极度缺乏耐心。人的天性就是避难驱易和急于求成。
ps: 避难驱易,这几个字实在太戳我了,也正是因为避难驱易,所以其实很多之前就想写的文章都拖拖拖,直到认识到是内心的避难驱易之后才开始控制自己开始输出,而也正是输出才让我注意到了自己之前没有注意到的点,才有了这篇文章以及 那些年,我在职场中做过的蠢事。
最后的最后,愿我们都有美好的未来!
来源:juejin.cn/post/7366567675315126281
uni-app 集成推送
研究了几天,终于是打通了uni-app的推送,本文主要针对的是App端的推送开发过程,分为在线推送和离线推送。我们使用uni-app官方推荐的uni-push2.0。官方文档
准备工作:开通uni-push功能

- 勾选uniPush2.0
- 点击"配置"
- 填写表单
关联服务空间说明:
uni-push2.0需要开发者开通uniCloud。不管您的业务服务器是否使用uniCloud,但实现推送,就要使用uniCloud服务器。
- 如果您的后台业务使用uniCloud开发,那理解比较简单。
- 如果您的后台业务没有使用uniCloud,那么也需要在uni-app项目中创建uniCloud环境。在uniCloud中写推送逻辑,暴露一个接口,再由业务后端调用这个推送接口。
在线推送
以上操作配置好了以后,回到HBuilderX。
因为上面修改了manifest.json配置,一定要重新进行一次云打包(打自定义调试基座和打正式包都可以)后才会生效。
客户端代码
我这边后端使用的是传统服务器,未使用云开发。要实现推送,首先需要拿到一个客户端的唯一标识,使用uni.getPushClientId API链接地址
onLaunch() {
uni.getPushClientId({
success: (res) => {
let push_clientid = res.cid
console.log('客户端推送标识:', push_clientid)
// 保存在全局,可以在进入app登录账号后调用一次接口将设备id传给后端
this.$options.globalData.pushClientId = push_clientid
// 一进来就掉一次接口把push_clientid传给后端
this.$setPushClientId(push_clientid).then(res => {
console.log('[ set pushClientId res ] >', res)
})
},
fail(err) {
console.log(err)
}
})
}
客户端监听推送消息
监听推送消息的代码,需要在收到推送消息之前被执行。所以应当写在应用一启动就会触发的应用生命周期
onLaunch中。
//文件路径:项目根目录/App.vue
export default {
onLaunch: function() {
console.log('App Launch')
uni.onPushMessage((res) => {
console.log("收到推送消息:",res) //监听推送消息
})
},
onShow: function() {
console.log('App Show')
},
onHide: function() {
console.log('App Hide')
}
}
服务端代码
- 鼠标右击项目根目录,依次执行

- 然后右击uniCloud目录,选择刚开始创建的云服务空间

- 在cloudfunctions目录右击,新建云函数/云对象,命名为uni-push,会创建一个uni-push目录

- 右击uni-push目录,点击 管理公共模块或扩展库依赖,选择uni-cloud-push

- 右击database目录,新建DB Schema,创建这三张表:
opendb-tempdata,opendb-device,uni-id-device,也就是json文件,直接输入并选择相应的模板。
- 修改index.js
'use strict';
const uniPush = uniCloud.getPushManager({appId:"__UNI__XXXX"}) //注意这里需要传入你的应用appId
exports.main = async (event, context) => {
console.log('event ===> ', event)
console.log('context ===> ', context)
// 所有要传的参数,都在业务服务器调用此接口时传入
const data = JSON.parse(event.body || '{}')
console.log('params ===> ', data)
return await uniPush.sendMessage(data)
};
- package.json
{
"name": "uni-push",
"dependencies": {},
"main": "index.js",
"extensions": {
"uni-cloud-push": {}
}
}
- 右击uni-push目录,点击上传部署
- 云函数url化
登录云函数控制台,进入云函数详情
8. postman测试一下接口

没问题的话,客户端将会打印“console.log("收到推送消息:", xxx)”,这一步最好是使用真机,运行到App基座,使用自定义调试基座运行,会在HBuilderX控制台打印。
离线推送
APP离线时,客户端收到通知会自动在通知栏创建消息,实现离线推送需要配置厂商参数。
苹果需要专用的推送证书,创建证书参考链接
安卓需要在各厂商开发者后台获取参数,参考链接
参数配置好了以后,再次在postman测试
注意 安卓需要退出app后,在任务管理器彻底清除进程,才会走离线推送
解决离线推送没有声音
这个是因为各安卓厂商为了避免开发者滥用推送进行的限制,因此需要设置离线推送渠道,查看文档
调接口时需要传一个channel参数

实现离线推送自定义铃声
这个功能只有华为和小米支持
也需要设置channel参数,并使用原生插件,插件地址
注意 使用了原生插件,一定要重新进行一次云打包
- 华为,申请了自分类权益即可
- 小米,在申请渠道时,选择系统铃声,url为
android.resource://安卓包名/raw/铃声文件名(不要带后缀)
来源:juejin.cn/post/7267417057451573304
无框架,跨框架!时隔两年,哈啰Quark Design迎来重大特性升级!

引言
历经1年多迭代,Quarkd 2.0 版本正式发布,这是自 Quarkd 开源以来第二个重大版本。本次升级主要实现了组件外部可以穿透影子Dom,修改组件内部元素的任何样式。
- (迁移后)最新官网:quark-ecosystem.github.io/quarkd-docs
- Github 地址:github.com/hellof2e/qu…
Quark Design 介绍
Quark(夸克) Design 是由哈啰平台 UED 和增长&电商前端团队联合打造的一套面向移动端的跨框架 UI 组件库。与业界第三方组件库不一样,Quark Design 底层基于 Web Components 实现,它能做到一套代码,同时运行在各类前端框架/无框架中。
前端各类框架技术发展多年,很多公司存量前端项目中必定存在各类技术栈。为了解决各类不同技术栈下UI交互统一,我们开发了这套UI组件库。
之前技术瓶颈
熟悉 quarkd 的开发者都知道其底层基因是 Web Components,从而实现了跨技术栈使用。但Web Components 中的 shadow dom 特性决定了其“孤岛”的特性,组件内部是个独立于外部的小世界,外部无法修改组件内部样式,若要修改内部样式,我们在 quarkd 1.x 版本中采用了 CSS 变量的方式来支援这种做法。
但这种做法依旧局限性非常大,你只能修改预设css变量的指定样式,比如你要修改 Dialog 内容中的字体大小/颜色:

// 使用组件
<quark-dialog class=“dialog” content="生命远不止连轴转和忙到极限,人类的体验远比这辽阔、丰富得多。"></quark-dialog>
// 内部css源码
:host .quark-dialog-content {
font-size: var(--dialog-content-font-size, 14px);
color: var(--dialog-content-color, "#5A6066");
// ... 其它样式
}
这时候,你需要在组件外部书写:
.dialog {
--dialog-content-font-size: 36px;
--dialog-content-color: red;
}
这种做法会带来一些问题,比如当源码中没有指定的css变量,就意味着你无法通过css变量从外面渗透进入组件内部去修改,比如 dialog conent 内的 font-style。
升级后
得益于 ::part CSS 伪元素的特性, 我们将 Quarkd 主要 dom 节点进行改造,升级后,你可以通过如下方式来自定义任何组件样式。
custom-element::part(foo) {
/* 样式作用于 `foo` 部分 */
}
::part 可以用来表示在阴影树中任何匹配 part 属性的元素。
该特性已兼容主流浏览器,详情见:mozilla.org # ::part()
用法示例:
// 使用组件
<quark-dialog class=“dialog” content="生命远不止连轴转和忙到极限,人类的体验远比这辽阔、丰富得多。"></quark-dialog>
.dialog::part(body) {
font-size: 24px;
color: #666;
}
.dialog::part(footer) {
font-size: 14px;
color: #333;
}
其它DEMO地址:stackblitz.com/edit/quarkd…
关于升级
Quarkd 2.x 向下兼容所有 1.x 功能及特性,之前的css变量也被保留,所以使用者可以从1.x直接升级到2.x!
One more thing
假如你也想利用 quarkd 底层能力构建属于自己的跨技术栈组件,欢迎使用:
github.com/hellof2e/qu…


最后
感谢在Quarkd迭代期间作出贡献的朋友们,感谢所有使用quarkd的开发者!

来源:juejin.cn/post/7391753478123864091
zero-privacy——uniapp小程序隐私协议弹窗组件
一. 引言
为规范开发者的用户个人信息处理行为,保障用户的合法权益,自2023年9月15日起,对于涉及处理用户个人信息的小程序开发者,微信要求,仅当开发者主动向平台同步用户已阅读并同意了小程序的隐私保护指引等信息处理规则后,方可调用微信提供的隐私接口。
公告地址:关于小程序隐私保护指引设置的公告
developers.weixin.qq.com/miniprogram…
接下来我们将打造一个保姆级的隐私协议弹窗组件
二. 开发调试基础
划重点,看文档,别说为什么没有效果,没有弹窗
1. 更新用户隐私保护指引
小程序管理员或开发者可以根据具体小程序涉及到的隐私相关接口来更新微信小程序后台的用户隐私保护指引,更新并审核通过后就可以进行相关的开发调试工作。仅有在指引中声明所处理的用户信息,才可以调用平台提供的对应接口或组件。若未声明,对应接口或组件将直接禁用。
- ���知道怎么填写隐私协议,看看文档:用户隐私保护指引设置developers.weixin.qq.com/miniprogram…
- 哪些api需要用户点击同意隐私协议才可以使用的看这里:小程序用户隐私保护指引内容介绍developers.weixin.qq.com/miniprogram…


审核时间有人说十几分钟,我自己的给大家参考一下。

审核通过!审核通过!审核通过后才可以开发调试。
2.配置调试字段 "__usePrivacyCheck__": true
- 在 2023 年 9 月 15 号之前,在 app.json 中配置
"__usePrivacyCheck__": true后,会启用隐私相关功能,如果不配置或者配置为 false 则不会启用。 - 在 2023 年 9 月 15 号之后,不论 app.json 中是否有配置 usePrivacyCheck,隐私相关功能都会启用。
- 所以在基于uni-app开发时,我们在 2023 年 9 月 15 号之前进行相关开发调试则需要在manifest.json文件mp-weixin中添加
"__usePrivacyCheck__": true - manifest.json文件源码视图
"mp-weixin" : {
"__usePrivacyCheck__": true
},
3. 配置微信开发工具基础库
将调试基础库改为3.0.0以上。具体路径为:
微信开发者工具->详情->本地设置->调试基础库
以上配置完成后,即可看看效果,我在小程序后台设置了剪切板的隐私接口,果然,已经提示没有隐私授权不能使用了。

三. zero-privacy组件介绍
组件下载地址:ext.dcloud.net.cn/plugin?name…
组件的功能和特点
- 支持 居中弹出,底部弹出
- 不依赖第三方弹窗组件,内置轻量动画效果
- 支持自定义触发条件
- 支持自定义主题色
- 组件中最重要的4个api(只需用到前3个):
- wx.getPrivacySetting 查询隐私授权情况 官方链接
- wx.onNeedPrivacyAuthorization 监听隐私接口需要用户授权事件。 官方链接
- wx.openPrivacyContract 跳转至隐私协议页面 官方链接
- wx.requirePrivacyAuthorize 模拟隐私接口调用,并触发隐私弹窗逻辑 官方链接
四. zero-privacy组件使用方法
在uniapp插件市场直接下载导入 uni_modules 后使用即可
- 最直接看到弹窗效果的测试方法
<template>
<view class="container">
<zero-privacy :onNeed='false'></zero-privacy>
</view>
</template>
注意以上是测试方案,不建议实际开发中按上面的方法使用,推荐以下两种方法
- 在小程序首页等tabbar页面直接处理隐私弹窗逻辑
<template>
<view class="container">
<zero-privacy :onNeed='false' :hideTabBar='true'></zero-privacy>
</view>
</template>
- 在页面点击某些需要用到隐私协议后处理隐私弹窗逻辑
<template>
<view class="container">
<view class="btn" @click="handleCopy">
复制
</view>
<zero-privacy></zero-privacy>
</view>
</template>
- 自定义内容使用
<template>
<view class="container">
<zero-privacy title="测试自定义标题" predesc="协议前内容" privacy-contract-name-custom="<自定义名称及括号>" subdesc="协议后内容协议后内容协议后内容. 主动换行"></zero-privacy>
</view>
</template>
五. zero-privacy组件参数说明
| 参数 | 类型 | 默认值 | 描述 |
|---|---|---|---|
| position | String | center | 可选 bottom,从底部弹出 |
| color | String | #0396FF | 主颜色: 协议名和同意按钮的背景色 |
| bgcolor | String | #ffffff | 弹窗背景色 |
| onNeed | Boolean | true | 使用到隐私相关api时触发弹窗,设置为false时初始化弹窗将判断是否需要隐私授权,需要则直接弹出 |
| hideTabBar | Boolean | false | 是否需要隐藏tabbar,在首页等tabbar页面使用改弹窗时建议改为true |
| title | String | #ffffff | 用户隐私保护提示 |
| predesc | String | 使用前请仔细阅读 Ï | 协议名称前的内容 |
| subdesc | String | 当您点击同意后,即表示您已理解并同意该条款内容,该条款将对您产生法律约束力。如您拒绝,将无法使用该服务。 | 协议名称后的内容 |
| privacyContractNameCustom | String | '' | 自定义协议名称,不传则由小程序自动获取 |
predesc 和 subdesc 的自定义内容,需要主动换行时在内容中添加实体字符 即可
六. zero-privacy组件运行效果




来源:juejin.cn/post/7273803674790150183
java就能写爬虫还要python干嘛?
爬虫学得好,牢饭吃得饱!!!切记!!!
相信大家多少都会接触过爬虫相关的需求吧,爬虫在绝大多数场景下,能够帮助客户自动的完成部分工作,极大的减少人工操作。目前更多的实现方案可能都是以python为实现基础,但是作为java程序员,咱们需要知道的是,以java 的方式,仍然可以很方便、快捷的实现爬虫。下面将会给大家介绍两种以java为基础的爬虫方案,同时提供案例供大家参考。
一、两种方案
传统的java实现爬虫方案,都是通过jsoup的方式,本文将采用一款封装好的框架【webmagic】进行实现。同时针对一些特殊的爬虫需求,将会采用【selenium-java】的进行实现,下面针对两种实现方案进行简单介绍和演示配置方式。
1.1 webmagic
官方文档:webmagic.io/
1.1.1 简介
使用webmagic开发爬虫,能够非常快速的实现简单且逻辑清晰的爬虫程序。
四大组件
- Downloader:下载页面
- PageProcessor:解析页面
- Scheduler:负责管理待抓取的URL,以及一些去重的工作。通常不需要自己定制。
- Pipeline:获取页面解析结果,数持久化。
Spider
- 启动爬虫,整合四大组件
1.1.2 整合springboot
webmagic分为核心包和扩展包两个部分,所以我们需要引入如下两个依赖:
<properties>
<webmagic.version>0.7.5</webmagic.version>
</properties>
<!--WebMagic-->
<dependency>
<groupId>us.codecraft</groupId>
<artifactId>webmagic-core</artifactId>
<version>${webmagic.version}</version>
</dependency>
<dependency>
<groupId>us.codecraft</groupId>
<artifactId>webmagic-extension</artifactId>
<version>${webmagic.version}</version>
</dependency>
到此为止,我们就成功的将webmagic引入进来了,具体使用,将在后面的案例中详细介绍。
1.2 selenium-java
1.2.1 简介
selenium是一款浏览器自动化工具,它能够模拟用户操作浏览器的交互。但前提是,我们需要在使用他的机器(windows/linux等)安装上它需要的配置。相比于webmigc的安装,它要繁琐的多了,但使用它的原因,就是为了解决一些webmagic做不到的事情。
支持多种语言:java、python、ruby、javascript等。其使用代码非常简单,以java为例如下:
package dev.selenium.hello;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
public class HelloSelenium {
public static void main(String[] args) {
WebDriver driver = new ChromeDriver();
driver.get("https://selenium.dev");
driver.quit();
}
}
1.2.2 安装
无论是在windows还是linux上使用selenium,都需要两个必要的组件:
- 浏览器(chrome)
- 浏览器驱动 (chromeDriver)
需要注意的是,要确保上述两者的版本保持一致。
下载地址
chromeDriver:chromedriver.storage.googleapis.com/index.html
windows
windows的安装相对简单一些,将chromeDriver.exe下载至电脑,chrome浏览器直接官网下载相应安装包即可。严格保证两者版本一致,否则会报错。
在后面的演示程序当中,只需要通过代码指定chromeDriver的路径即可。
linux
linux安装才是我们真正的使用场景,java程序通常是要部署在linux环境的。所以我们需要linux的环境下安装chrome和chromeDriver才能实现想要的功能。
首先要做的是判断我们的linux环境属于哪种系统,是ubuntu、centos还是其他的种类,相应的shell脚本都是不同的。
我们采用云原生的环境,所有的服务均以容器的方式部署,所以要在每一个服务端容器内部安装chrome和chromeDiver。我们使用的是Alpine Linux,一个轻量级linux发行版,非常适合用来做Docker镜像。
我们可以通过apk --help去查看相应的命令,我直接给出安装命令:
# Install Chrome for Selenium
RUN apk add gconf
RUN apk add chromium
RUN apk add chromium-chromedriver
上面的内容,可以放在DockerFile文件中,在部署的时候,会直接将相应组件安装在容器当中。
需要注意的是,在Alpine Linux中自带的浏览器是chromium和chromium-chromedriver,且版本相应较低,但是足够我们的需求所使用了。
/ # apk search chromium
chromium-68.0.3440.75-r0
chromium-chromedriver-68.0.3440.75-r0
1.2.3 整合springboot
我们只需要在爬虫模块引入依赖就好了:
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
</dependency>
二、三个案例
下面通过三个简单的案例,给大家实际展示使用效果。
2.1 爬取省份街道
使用webmagic进行省份到街道的数据爬取。注意,本文只提供思路,不提供具体爬取网站信息,请同学们自己根据使用选择。
接下来搭建webmagic的架子,其中有几个关键点:
- 创建页面解析类,实现PageProcessor。
import us.codecraft.webmagic.Page;
import us.codecraft.webmagic.Site;
import us.codecraft.webmagic.processor.PageProcessor;
/**
* 页面解析
*
* @author wjbgn
* @date 2023/8/15 17:25
**/
public class TestPageProcessor implements PageProcessor {
@Override
public void process(Page page) {
}
@Override
public Site getSite() {
return site;
}
/**
* 初始化Site配置
*/
private Site site = Site.me()
// 重试次数
.setRetryTimes(3)
//编码
.setCharset(StandardCharsets.UTF_8.name())
// 超时时间
.setTimeOut(10000)
// 休眠时间
.setSleepTime(1000);
}
- 实现PageProcessor后,要重写其方法process(Page page),此方法是我们实现爬取的核心(页面解析)。通常省市区代码分为6级,所以常见的网站均是按照层级区分,我们是从省份开始爬取,即从第三层开始爬取。
- 初始化变量
@Override
public void process(Page page) {
// 市级别
Integer type = 3;
// 初始化结果明细
RegionCodeDTO regionCodeDTO = new RegionCodeDTO();
// 带有父子关系的结果集合
List<Map<String, Object>> list = new ArrayList();
// 页面所有元素集合
List<String> all = new ArrayList<>();
// 页面中子页面的链接地址
List<String> urlList = new ArrayList<>();
}
- 根据不同级别,获取相应页面不同的元素
if (CollectionUtil.isEmpty(all)) {
// 爬取所有的市,编号,名称
all = page.getHtml().css("table.citytable").css("tr").css("a", "text").all();
// 爬取所有的城市下级地址
urlList = page.getHtml().css("table.citytable").css("tr").css("a", "href").all()
.stream().distinct().collect(Collectors.toList());
if (CollectionUtil.isEmpty(all)) {
// 区县级别
type = 4;
all = page.getHtml().css("table.countytable").css("tr.countytr").css("td", "text").all();
// 获取区
all.addAll(page.getHtml().css("table.countytable").css("tr.countytr").css("a", "text").all());
urlList = page.getHtml().css("table.countytable").css("tr").css("a", "href").all()
.stream().distinct().collect(Collectors.toList());
if (CollectionUtil.isEmpty(all)) {
// 街道级别
type = 5;
all = page.getHtml().css("table.towntable").css("tr").css("a", "text").all();
urlList = page.getHtml().css("table.towntable").css("tr").css("a", "href").all()
.stream().distinct().collect(Collectors.toList());
if (CollectionUtil.isEmpty(all)) {
// 村,委员会
type = 6;
List<String> village = new ArrayList<>();
all = page.getHtml().css("table").css("tr.villagetr").css("td", "text").all();
for (int i = 0; i < all.size(); i++) {
if (i % 3 != 1) {
village.add(all.get(i));
}
}
all = village;
}
}
}
}
- 定义一个实体类RegionCodeDTO,用来存放临时获取的code,url以及父子关系等内容:
public class RegionCodeDTO {
private String code;
private String parentCode;
private String name;
private Integer type;
private String url;
private List<RegionCodeDTO> regionCodeDTOS;
}
- 接下来对页面获取的内容(code、name、type)进行组装和临时存储,添加到children中:
// 初始化子集
List<RegionCodeDTO> children = new ArrayList<>();
// 初始化临时节点数据
RegionCodeDTO region = new RegionCodeDTO();
// 解析页面结果集all当中的数据,组装到region 和 children当中
for (int i = 0; i < all.size(); i++) {
if (i % 2 == 0) {
region.setCode(all.get(i));
} else {
region.setName(all.get(i));
}
if (StringUtils.isNotEmpty(region.getCode()) && StringUtils.isNotEmpty(region.getName())) {
region.setType(type);
// 添加子集到集合当中
children.add(region);
// 重新初始化
region = new RegionCodeDTO();
}
}
- 组装页面链接,并将页面链接组装到children当中。
// 循环遍历页面元素获取的子页面链接
for (int i = 0; i < urlList.size(); i++) {
String url = null;
if (StringUtils.isEmpty(urlList.get(0))) {
continue;
}
// 拼接链接,页面的子链接是相对路径,需要手动拼接
if (urlList.get(i).contains(provinceEnum.getCode() + "/")) {
url = provinceEnum.getUrlPrefixNoCode();
} else {
url = provinceEnum.getUrlPrefix();
}
// 将链接放到临时数据子集对象中
if (urlList.get(i).substring(urlList.get(i).lastIndexOf("/") + 1, urlList.get(i).indexOf(".html")).length() == 9) {
children.get(i).setUrl(url + page.getUrl().toString().substring(page.getUrl().toString().indexOf(provinceEnum.getCode() + "/") + 3
, page.getUrl().toString().lastIndexOf("/")) + "/" + urlList.get(i));
} else {
children.get(i).setUrl(url + urlList.get(i));
}
}
- 将children添加到结果对象当中
// 将子集放到集合当中
regionCodeDTO.setRegionCodeDTOS(children);
- 在下面的代码当中将进行两件事儿:
- 处理下一页,通过page的addTargetRequests方法,可以进行下一页的跳转,此方法参数可以是listString和String,即支持多个页面跳转和单个页面的跳转。
- 将数据传递到Pipeline,用于数据的存储,Pipeline的实现将在后面具体说明。
// 定义下一页集合
List<String> nextPage = new ArrayList<>();
// 遍历上面的结果子集内容
regionCodeDTO.getRegionCodeDTOS().forEach(regionCodeDTO1 -> {
// 组装下一页集合
nextPage.add(regionCodeDTO1.getUrl());
// 定义并组装结果数据
Map<String, Object> map = new HashMap<>();
map.put("regionCode", regionCodeDTO1.getCode());
map.put("regionName", regionCodeDTO1.getName());
map.put("regionType", regionCodeDTO1.getType());
map.put("regionFullName", regionCodeDTO1.getName());
map.put("regionLevel", regionCodeDTO1.getType());
list.add(map);
// 推送数据到pipeline
page.putField("list", list);
});
// 添加下一页集合到page
page.addTargetRequests(nextPage);
- 当本次process方法执行完后,将会根据传递过来的链接地址,再次执行process方法,根据前面定义的读取页面元素流程的代码,将不符合type=3的内容,所以将会进入到下一级4的爬取过程,5、6级别原理相同。

- 创建Pipeline,用于编写数据持久化过程。经过上面的逻辑,已经将所需内容全部获取到,接下来将通过pipline进行数据存储。首先定义pipeline,并实现其process方法,获取结果内容,具体存储数据的代码就不展示了,需要注意的是,此处pipeline没有通过spring容器托管,需要调用业务service需要使用SpringUtils进行获取:
public class RegionDataPipeline implements Pipeline{
@Override
public void process(ResultItems resultItems, Task task) {
// 获取service
IXXXXXXXXXService service = SpringUtils.getBean(IXXXXXXXXXService.class);
// 获取内容
List<Map<String, String>> list = (List<Map<String, String>>) resultItems.getAll().get("list");
// 解析数据,转换为对应实体类
// service.saveBatch
}
- 启动爬虫
//启动爬虫
Spider.create(new RegionCodePageProcessor(provinceEnum))
.addUrl(provinceEnum.getUrl())
.addPipeline(new RegionDataPipeline())
//此处不能小于2
.thread(2).start()
2.2 爬取网站静态图片
爬取图片是最常见的需求,我们通常爬取的网站都是静态的网站,即爬取的内容都在网页上面渲染完成的,我们可以直接通过获取页面元素进行抓取。
可以参考下面的文章,直接拉取网站上的图片:juejin.cn/post/705138…
针对获取到的图片网络地址,直接使用如下方式进行下载即可:
url = new URL(imageUrl);
//打开连接
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
//设置请求方式为"GET"
conn.setRequestMethod("GET");
//超时响应时间为10秒
conn.setConnectTimeout(10 * 1000);
//通过输入流获取图片数据
InputStream is = conn.getInputStream();
2.3 爬取网站动态图片
在2.2中我们可以很快地爬取到对应的图片,但是在另外两种场景下,我们获取图片将会不适用上面的方式:
- 需要拼图,且多层的gis相关图片,此种图片将会在后期进行复杂的图片处理(按位置拼接瓦片,多层png图层叠加),才能获取到我们想要的效果。
- 动态js加载的图片,直接无法通过css、xpath获取。
所以在这种情况下我们可以使用开篇介绍的selenium-java来解决,本文使用的仅仅是截图的功能,来达到我们需要的效果。具体街区全屏代码如下所示:
public File getItems() {
// 获取当前操作系统
String os = System.getProperty("os.name");
String path;
if (os.toLowerCase().startsWith("win")) {
//windows系统
path = "driver/chromedriver.exe";
} else {
//linux系统
path = "/usr/bin/chromedriver";
}
WebDriver driver = null;
// 通过判断 title 内容等待搜索页面加载完毕,间隔秒
try {
System.setProperty("webdriver.chrome.driver", path);
ChromeOptions chromeOptions = new ChromeOptions();
chromeOptions.addArguments("--headless");
chromeOptions.addArguments("--no-sandbox");
chromeOptions.addArguments("--disable-gpu");
chromeOptions.addArguments("--window-size=940,820");
driver = new ChromeDriver(chromeOptions);
// 截图网站地址
driver.get(UsaRiverConstant.OBSERVATION_POINT_URL);
// 休眠用于网站加载
Thread.sleep(15000);
// 截取全屏
File screenshotAs = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE);
return screenshotAs;
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
driver.quit();
}
}
如上所示,我们获取的是整个页面的图片,还需要对截取的图片进行相应的剪裁,保留我们需要的区域,如下所示:
public static void cutImg(InputStream inputStream, int x, int y, int width, int height, OutputStream outputStream) {//图片路径,截取位置坐标,输出新突破路径
InputStream fis = inputStream;
try {
BufferedImage image = ImageIO.read(fis);
//切割图片
BufferedImage subImage = image.getSubimage(x, y, width, height);
Graphics2D graphics2D = subImage.createGraphics();
graphics2D.drawImage(subImage, 0, 0, null);
graphics2D.dispose();
//输出图片
ImageIO.write(subImage, "png", outputStream);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
fis.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
三、小结
通过如上两个组件的简单介绍,足够应付在java领域的大多数爬取场景。从页面数据、到静态网站图片,在到动态网站的图片截取。本文以提供思路为主,原理请参考相应的官方文档。
爬虫学得好,牢饭吃得饱!!!切记!!!
来源:juejin.cn/post/7267532912617177129
领导让前端实习生在网页上添加一个长时间不操作锁定电脑的功能
前情提要
大约一个月前,公司的医疗管理系统终于完工上线。后面一个周一,领导叫大家开会,说后面没有项目进来了,用不了这么多开发人员,原地宣布裁员。再后一周后,花 2000 招了个实习生,工作内容为系统维护。
工作内容

领导:由于我们工作内容很简单,事情轻松,基本就在页面上加加按钮就行,所以工资相对较少一些,是否接受?
实习生小李:能开实习证明吗?
领导:能的。
实习生小李:好的,谢谢老板。
领导:什么时候能入职?
实习生小李:现在。
工作来源
医疗系统是一个比较数据敏感的系统,现在医院那边需要添加一个十分钟时间没有在系统进行操作,则锁定电脑的功能,使用者再次使用时,必须输入密码。客户那边在系统对接群里发出需求时,并没有人回复(PS:人都裁完了),然后老板回复到:好的。
工作安排
领导:小李,我们有个医疗系统,需要添加锁屏功能,你处理一下,两天时间应该没问题吧?
实习生小李:(思索片刻)好的,有代码吗
(4小时之后)
领导:有的,我找下
(第二天10点)
实习生小李:王总,代码找到了没有
(第二天12点)
领导:没代码改不了吗?
实习生小李:(瑟瑟发抖)我试试
(第二天14点)
实习生小李:王总,是那种长时间不操作就锁定系统的功能吗
领导:是的
实习生小李:多久不操作才锁
领导:十分钟,锁了需要输入密码才能使用
实习生小李:但是我们医疗系统没有密码功能
领导:客户电脑有密码啊
实习生小李:是锁电脑系统吗
领导:对
实习生小李:(若有所思)我试试
实现过程
实习生小李:魔镜魔镜,我们有个医疗系统,需要做一个十分钟不操作电脑,就锁定用户电脑系统的功能,在没有源代码的情况下如何实现?
魔镜:好的,在没有源代码的情况下为医疗系统添加十分钟不操作电脑就锁定用户电脑系统的功能,可以使用 sys-shim 实现。
第一步,创建一个目录例如 medical-system ,目录里有以下两个文件:
package.json文件用来配置 sys-shimpreload.js用来向医疗系统添加功能
第二步
在 package.json 中编写内容如下
{
"browserArguments": "--disable-web-security --allow-running-insecure-content ",
"form": {
"right": "1300",
"bottom": "800"
},
"page": "https://www.baidu.com/"
}
- browserArguments 用来指定浏览器参数,这里配置为允许跨域以方便注入代码
- form 用来控制窗口如何显示,这里表示窗口大小
- page 表示医疗系统的页面
在 preload.js 中编写内容如下
new Promise(async function () {
window.main = await new window.Sys({ log: true })
// 设置倒计时时间,为了测试方便,这里改为 30 秒
const TIMEOUT = 0.5 * 60 * 1000;
// 声明一个变量来存储 setTimeout 的引用
let timeoutId = null;
// 定义一个函数来重置倒计时并在2分钟后打印日志
function startInactivityCheck() {
// 清除之前的倒计时(如果有的话)
if (timeoutId !== null) {
clearTimeout(timeoutId);
}
// 设置一个新的倒计时
timeoutId = setTimeout(function() {
// 锁定系统
window.main.native.sys.lock()
}, TIMEOUT);
}
// 为 body 元素添加点击事件监听器
document.body.addEventListener('click', function() {
console.log("检测到点击事件,重新开始计时。");
// 重置倒计时
startInactivityCheck();
});
// 初始化倒计时
startInactivityCheck();
})
sys.lock()方法用于锁定操作系统。
第三步,生成应用程序
npx sys-shim pack --input medical-system
运行该命令后,会在当前目录生成一个名为 medical-system.exe 的可执行文件。它封装了医疗系统这个 web 程序,并在里面添加了锁屏功能。
pack指定表示打包应用--input参数表示要打包的目录

--input 参数也可以是线上的网页,比如:
npx sys-shim pack --input https://www.baidu.com/
即可获取一个可以调用操作系统 api 的 web 应用。
交付反馈
用户:以前我们还需要进入浏览器输入网址才能进入系统,现在直接在桌面上就能进入,并且还有安全锁屏功能,非常好!
领导:小李干得不错,但没有在规定的时间内完成,但由于客户反馈不错,就不扣你的考核分了。
实习生小李:(不得其解)谢谢老板。
后记
不知不觉,又到了周五,这是公司技术分享会的时候。当前公司技术人员只有实习生小李,由小李负责技术分享。
宽旷的会议室里,秘书、领导、小李三人面面相觑,小李强忍住尴尬,开始了自己的第一次技术分享:
实习生小李:感谢领导给我的工作机会,在这份工作里,我发现了 sys-shim 这个工具,它可以方便的在已有的 web 页面中添加系统 api,获取调用操作系统层面功能的能力,比如关机、锁屏。
领导:(好奇)那他可以读取电脑上的文件吗?
实习生小李:可以的,它可以直接读取电脑上的文件,例如电脑里面的文档、照片、视频等。
突然领导脸色一黑,看了一眼秘书,并关闭了正在访问的医疗系统,然后在技术分享考核表上写下潦潦草草的几个字:考核分-5。
续集:托领导大福!前端实习生用 vue 随手写了个系统修复工具,日赚 300
提示
大家可以直接运行这个命令生成 app 体验:
npx sys-shim pack --input https://www.baidu.com/
生成后的 app 可以右键解压,看到内部结构。如果遇到问题,可以在这里提交,方便追溯,我会及时解答的。
参考
来源:juejin.cn/post/7373831659470880806
领导被我的花式console.log吸引了!直接写入公司公共库!
文章的效果,大家可以直接只用云vscode实验一下:juejin.cn/post/738875…
背景简介
这几天代码评审,领导无意中看到了我本地代码的控制台,被我花里胡哨的console打印内容吸引了!

老板看见后,说我这东西有意思,花里胡哨的,他喜欢!
但是随即又问我,这么花里胡哨的东西,上生产会影响性能吧?我自信的说:不会,代码内有判断的,只有开发环境会打印!
老板很满意,于是让我给其他前端同事分享一下,讲解下实现思路!最终,这个方法还被写入公司的公用utils库里,供大家使用!
console简介
console 是一个用于调试和记录信息的内置对象, 提供了多种方法,可以帮助开发者输出各种信息,进行调试和分析。
console.log()
用于输出一般信息,大家应该在熟悉不过了。

console.info() :
输出信息,与 console.log 类似,但在某些浏览器中可能有不同的样式。

console.warn() :
输出警告信息,通常会以黄色背景或带有警告图标的样式显示。

console.error() :
输出错误信息,通常会以红色背景或带有错误图标的样式显示。

console.table() :
以表格形式输出数据,适用于数组和对象。
例如:
const users = [
{ name: '石小石', age: 18 },
{ name: '刘亦菲', age: 18 }
];
console.table(users);

通过上述介绍,我们可以看出,原生的文本信息、警告信息、错误信息、数组信息打印出来的效果都很普通,辨识度不高!现在我们通过console.log来实现一些花里花哨的样式!
技术方案
console.log()
console.log() 可以接受任何类型的参数,包括字符串、数字、布尔值、对象、数组、函数等。最厉害的是,它支持占位符!
常用的占位符:
- %s - 字符串
- %d or %i - 整数
- %f - 浮点数
- %o - 对象
- %c - CSS 样式
格式化字符串
console.log() 支持类似于 C 语言 printf 函数的格式化字符串。我们可以使用占位符来插入变量值。
const name = 'Alice';
const age = 30;
console.log('Name: %s, Age: %d', name, age); // Name: Alice, Age: 30
添加样式
可以使用 %c 占位符添加 CSS 样式,使输出内容更加美观。
console.log('%c This is a styled message', 'color: red; font-size: 20px;');

自定义样式的实现,其实主要是靠%c 占位符添加 CSS 样式实现的!
实现美化的信息打印
基础信息打印
我们创建一个prettyLog方法,用于逻辑编写
// 美化打印实现方法
const prettyLog = () => {
const isEmpty = (value: any) => {
return value == null || value === undefined || value === '';
};
const prettyPrint = (title: string, text: string, color: string) => {
console.log(
`%c ${title} %c ${text} %c`,
`background:${color};border:1px solid ${color}; padding: 1px; border-radius: 2px 0 0 2px; color: #fff;`,
`border:1px solid ${color}; padding: 1px; border-radius: 0 2px 2px 0; color: ${color};`,
'background:transparent'
);
};
// 基础信息打印
const info = (textOrTitle: string, content = '') => {
const title = isEmpty(content) ? 'Info' : textOrTitle;
const text = isEmpty(content) ? textOrTitle : content;
prettyPrint(title, text, '#909399');
};
return {
info
};
};
上述代码定义了一个 prettyLog 函数,用于美化打印信息到控制台。通过自定义样式,输出信息以更易读和美观的格式呈现。
我们使用一下看看效果
// 创建打印对象
const log = prettyLog();
// 不带标题
log.info('这是基础信息!');
//带标题
log.info('注意看', '这是个男人叫小帅!');

info 方法用于输出信息级别的日志。它接受两个参数:textOrTitle 和 content。如果只提供一个参数,则视为内容并设置默认标题为 Info;如果提供两个参数,则第一个参数为标题,第二个参数为内容。最后调用 prettyPrint 方法进行输出。
错误信息打印
const prettyLog = () => {
const isEmpty = (value: any) => {
return value == null || value === undefined || value === '';
};
const prettyPrint = (title: string, text: string, color: string) => {
// ...
};
const info = (textOrTitle: string, content = '') => {
// ...
};
const error = (textOrTitle: string, content = '') => {
const title = isEmpty(content) ? 'Error' : textOrTitle;
const text = isEmpty(content) ? textOrTitle : content;
prettyPrint(title, text, '#F56C6C');
};
// retu;
return {
info,
error,
};
};
// 创建打印对象
const log = prettyLog();
log.error('奥德彪', '出来的时候穷 生活总是让我穷 所以现在还是穷。');
log.error('前方的路看似很危险,实际一点也不安全。');

成功信息与警告信息打印
// 美化打印实现方法
const prettyLog = () => {
const isEmpty = (value: any) => {
return value == null || value === undefined || value === '';
};
const prettyPrint = (title: string, text: string, color: string) => {
console.log(
`%c ${title} %c ${text} %c`,
`background:${color};border:1px solid ${color}; padding: 1px; border-radius: 2px 0 0 2px; color: #fff;`,
`border:1px solid ${color}; padding: 1px; border-radius: 0 2px 2px 0; color: ${color};`,
'background:transparent'
);
};
const info = (textOrTitle: string, content = '') => {
const title = isEmpty(content) ? 'Info' : textOrTitle;
const text = isEmpty(content) ? textOrTitle : content;
prettyPrint(title, text, '#909399');
};
const error = (textOrTitle: string, content = '') => {
const title = isEmpty(content) ? 'Error' : textOrTitle;
const text = isEmpty(content) ? textOrTitle : content;
prettyPrint(title, text, '#F56C6C');
};
const warning = (textOrTitle: string, content = '') => {
const title = isEmpty(content) ? 'Warning' : textOrTitle;
const text = isEmpty(content) ? textOrTitle : content;
prettyPrint(title, text, '#E6A23C');
};
const success = (textOrTitle: string, content = '') => {
const title = isEmpty(content) ? 'Success ' : textOrTitle;
const text = isEmpty(content) ? textOrTitle : content;
prettyPrint(title, text, '#67C23A');
};
// retu;
return {
info,
error,
warning,
success
};
};
// 创建打印对象
const log = prettyLog();
log.warning('奥德彪', '我并非无路可走 我还有死路一条! ');
log.success('奥德彪', '钱没了可以再赚,良心没了便可以赚的更多。 ');

实现图片打印
// 美化打印实现方法
const prettyLog = () => {
// ....
const picture = (url: string, scale = 1) => {
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = () => {
const c = document.createElement('canvas');
const ctx = c.getContext('2d');
if (ctx) {
c.width = img.width;
c.height = img.height;
ctx.fillStyle = 'red';
ctx.fillRect(0, 0, c.width, c.height);
ctx.drawImage(img, 0, 0);
const dataUri = c.toDataURL('image/png');
console.log(
`%c sup?`,
`font-size: 1px;
padding: ${Math.floor((img.height * scale) / 2)}px ${Math.floor((img.width * scale) / 2)}px;
background-image: url(${dataUri});
background-repeat: no-repeat;
background-size: ${img.width * scale}px ${img.height * scale}px;
color: transparent;
`
);
}
};
img.src = url;
};
return {
info,
error,
warning,
success,
picture
};
}
// 创建打印对象
const log = prettyLog();
log.picture('https://nimg.ws.126.net/?url=http%3A%2F%2Fdingyue.ws.126.net%2F2024%2F0514%2Fd0ea93ebj00sdgx56001xd200u000gtg00hz00a2.jpg&thumbnail=660x2147483647&quality=80&type=jpg');

上述代码参考了其他文章:Just a moment...
url可以传支持 base64,如果是url链接,图片链接则必须开启了跨域访问才能打印
实现美化的数组打印
打印对象或者数组,其实用原生的console.table比较好
const data = [
{ id: 1, name: 'Alice', age: 25 },
{ id: 2, name: 'Bob', age: 30 },
{ id: 3, name: 'Charlie', age: 35 }
];
console.table(data);

当然,我们也可以伪实现
const table = () => {
const data = [
{ id: 1, name: 'Alice', age: 25 },
{ id: 2, name: 'Bob', age: 30 },
{ id: 3, name: 'Charlie', age: 35 }
];
console.log(
'%c id%c name%c age',
'color: white; background-color: black; padding: 2px 10px;',
'color: white; background-color: black; padding: 2px 10px;',
'color: white; background-color: black; padding: 2px 10px;'
);
data.forEach((row: any) => {
console.log(
`%c ${row.id} %c ${row.name} %c ${row.age} `,
'color: black; background-color: lightgray; padding: 2px 10px;',
'color: black; background-color: lightgray; padding: 2px 10px;',
'color: black; background-color: lightgray; padding: 2px 10px;'
);
});
};

但是,我们无法控制表格的宽度,因此,这个方法不太好用,不如原生。
仅在开发环境使用
// 美化打印实现方法
const prettyLog = () => {
//判断是否生产环境
const isProduction = import.meta.env.MODE === 'production';
const isEmpty = (value: any) => {
return value == null || value === undefined || value === '';
};
const prettyPrint = (title: string, text: string, color: string) => {
if (isProduction) return;
// ...
};
// ...
const picture = (url: string, scale = 1) => {
if (isProduction) return;
// ...
};
// retu;
return {
info,
error,
warning,
success,
picture,
table
};
};
我们可以通过import.meta.env.MODE 判断当前环境是否为生产环境,在生产环境,我们可以禁用信息打印!
完整代码
// 美化打印实现方法
const prettyLog = () => {
const isProduction = import.meta.env.MODE === 'production';
const isEmpty = (value: any) => {
return value == null || value === undefined || value === '';
};
const prettyPrint = (title: string, text: string, color: string) => {
if (isProduction) return;
console.log(
`%c ${title} %c ${text} %c`,
`background:${color};border:1px solid ${color}; padding: 1px; border-radius: 2px 0 0 2px; color: #fff;`,
`border:1px solid ${color}; padding: 1px; border-radius: 0 2px 2px 0; color: ${color};`,
'background:transparent'
);
};
const info = (textOrTitle: string, content = '') => {
const title = isEmpty(content) ? 'Info' : textOrTitle;
const text = isEmpty(content) ? textOrTitle : content;
prettyPrint(title, text, '#909399');
};
const error = (textOrTitle: string, content = '') => {
const title = isEmpty(content) ? 'Error' : textOrTitle;
const text = isEmpty(content) ? textOrTitle : content;
prettyPrint(title, text, '#F56C6C');
};
const warning = (textOrTitle: string, content = '') => {
const title = isEmpty(content) ? 'Warning' : textOrTitle;
const text = isEmpty(content) ? textOrTitle : content;
prettyPrint(title, text, '#E6A23C');
};
const success = (textOrTitle: string, content = '') => {
const title = isEmpty(content) ? 'Success ' : textOrTitle;
const text = isEmpty(content) ? textOrTitle : content;
prettyPrint(title, text, '#67C23A');
};
const table = () => {
const data = [
{ id: 1, name: 'Alice', age: 25 },
{ id: 2, name: 'Bob', age: 30 },
{ id: 3, name: 'Charlie', age: 35 }
];
console.log(
'%c id%c name%c age',
'color: white; background-color: black; padding: 2px 10px;',
'color: white; background-color: black; padding: 2px 10px;',
'color: white; background-color: black; padding: 2px 10px;'
);
data.forEach((row: any) => {
console.log(
`%c ${row.id} %c ${row.name} %c ${row.age} `,
'color: black; background-color: lightgray; padding: 2px 10px;',
'color: black; background-color: lightgray; padding: 2px 10px;',
'color: black; background-color: lightgray; padding: 2px 10px;'
);
});
};
const picture = (url: string, scale = 1) => {
if (isProduction) return;
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = () => {
const c = document.createElement('canvas');
const ctx = c.getContext('2d');
if (ctx) {
c.width = img.width;
c.height = img.height;
ctx.fillStyle = 'red';
ctx.fillRect(0, 0, c.width, c.height);
ctx.drawImage(img, 0, 0);
const dataUri = c.toDataURL('image/png');
console.log(
`%c sup?`,
`font-size: 1px;
padding: ${Math.floor((img.height * scale) / 2)}px ${Math.floor((img.width * scale) / 2)}px;
background-image: url(${dataUri});
background-repeat: no-repeat;
background-size: ${img.width * scale}px ${img.height * scale}px;
color: transparent;
`
);
}
};
img.src = url;
};
// retu;
return {
info,
error,
warning,
success,
picture,
table
};
};
// 创建打印对象
const log = prettyLog();
来源:juejin.cn/post/7371716384847364147
听说去哪儿混合办公了? 聊聊程序员如何找到远程工作和好处
哈喽大家好,这两天看到去哪儿开始混合办公了,作为远程工作的支持者我表示很开心,终于有大厂全面开始支持远程员工,去哪儿的邮件截图是这么说的
去年开始现在一些团队做小范围的尝试,好评如潮,有的同学利用通勤的时间减肥,有的同学可以回家陪家人,家人对工作的支持度变高,而且工作效率一点都没下降,所以去哪儿在7月开始,每周三周五可以居家办公,无需申请

远程的好处非常明显,尤其是在一线城市,通勤的一个半小时就省下了,生活幸福度直线提高,你出去旅游周四就可以出发,如果五天都可以居家就是完全远程,你可以回老家省会还有北京一线的收入,老板节省下了组办公室的费用
因为远程无论是对员工满意度,还是老板的成本控制都很友好,混合或者远程办公在海外已经比较流行了,基本海外所有的招聘网站都有是否远程的选项,你可以过滤只看支持远程的,你搜工作就有一个选项是坐班,混合还是远程的,不像国内boss直聘,默认都是要通勤的,哪怕跟我说支持远程,也是只是面试可以远程
比如英国这边混合办公室基本操作,好一些的就会全员远程,比如这边大厂,Meta是每周去两天还是三天,我给忘了,我觉得国内以后支持远程的会越来越多,越来越多的小老板会抛弃自己奴隶主的思想,必须得盯着你干活,反而会考虑更真实的办公室成本
对远程最大的批评就是会降低工作效率,所谓的见面沟通效率才是最高的,确实有一些场景面对面效率最高,但是扪心自问,你现在开会的效率真的高吗,大公司动辄就一小时的会议,而且你首先就在通勤上浪费了一小时,你一天的效率可能很高吗 ,腾讯会议的AI总结功能比以前人工写的会议纪要不知道好多少倍
而一个人能有更多的时间照顾家庭后,闲暇时间才会产生创意,尝试新的工具和沟通方式等等,可能会让效率有非常大的提高
远程我就可以工作的同事带家人出游,远程久了你换工作就会只考虑远程的,再也不想挤地铁了,虽然现在通勤还是主流,尤其是还有马斯克这种非常反对远程的人,但是我最近聊的一些创业公司和小公司,基本都支持混合办公的


说了这么多远程办公的好处,喜欢工作和生活平衡的你可能已经蠢蠢欲动了,那远 程工作需要哪些能力呢,以及如何找到一个远程工作呢
其实远程工作有很多最佳实践,比较典型的有37signals这家公司,这家公司坚持小而美,写出来ruby on rails这种框架,有一套书叫重来,有三本,第二本就叫remote,比较系统的介绍远程工作文化的方方面面

包括远程工作的好处,可以逃离大城市的房价,更好的work life balance,更自在的生活,还反驳了一些对远程文化的批评,比如觉得坐在一起才能效率高,家里干扰大等等
更重要的介绍了如何更好的远程协作,这是需要学习的技能,比如如何可视化你的工作进度,高效的沟通,怎么管理远程员工的效率,还有远程人如何更好的生活,非常推荐
还有就是有一个公司叫gitlab,这个更厉害,这是一家美国上市公司,应该是第一个招股书里没有办公地址的,从老板到实习生,全员都散落在世界各地办公,关于大型公司如何实践远程文化,他们有一个专门的文档,质量非常高,主要是关于如何管理远程团队,还有很重要的远程开会技巧
地址和上面重来的电子书,评论区好像没法发链接,要不加我吧,我研究下怎么发给大家


那最后如何找到一个远程工作呢,其实之前我也分享过,这里简单总结下
首先程序员是非常适合远程的,所有的代码任务都可以在线完成,通过git管理代码,腾讯会议或者zoom开会,飞书钉钉slack等工作聊天等等
就像前面所述国内远程机会比较少,而且很多国内的老板哪怕远程也依然是监控心态,比如要求你开摄像头或者响应速度也挺难受,所以我觉得比如你想明年找个远程,那现在就优先学英语,程序员怎么学英语大家感兴趣以后可以专门聊,大概方法就是不要学英语,而是用英语学习编程就可以了
希望大家都能尝试和探索混合或者远程工作的新体验,能够拥有一个更加自在的职场,在努力工作的同时,可以有时间陪家人探索世界
来源:juejin.cn/post/7392116075674927131


















