注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

如何写一个redis蜜罐

写在前面 蜜罐就是一种通过模拟真实环境来诱导入侵者的一种技术。通过它可以拖延黑客入侵时间,递给黑客用于取证假数据,溯源黑客等。通过控制平台去操作部署的仿真环境实现高效的诱捕。 之前写过一个简单的仿真redis蜜罐,简单介绍一下。 RESP 搭建这种组件的仿真环...
继续阅读 »

写在前面


蜜罐就是一种通过模拟真实环境来诱导入侵者的一种技术。通过它可以拖延黑客入侵时间,递给黑客用于取证假数据,溯源黑客等。通过控制平台去操作部署的仿真环境实现高效的诱捕。


之前写过一个简单的仿真redis蜜罐,简单介绍一下。


RESP


搭建这种组件的仿真环境,要么用真实的程序,要么就自己实现一套虚假的程序。假如自己实现的话,最关键的就是协议,对于redis来说,它的通信协议相对简单,就是resp,协议格式如下:



  • 单行字符串(Simple Strings): 响应的首字节是 "+"。例如:"+OK\r\n"。

  • 错误(Errors): 响应的首字节是 "-"。例如:"-ERROR message\r\n"。

  • 整型(Integers): 响应的首字节是 ":"。例如:":0\r\n"。

  • 多行字符串(Bulk Strings): 响应的首字节是"",后面跟字符长度,然后跟字符。例如:"",后面跟字符长度,然后跟字符。例如:"6\r\nfoobar\r\n"

  • 数组(Arrays): 响应的首字节是 "*"。例如:"*2\r\n3\nfoo˚\n˚3\r\nfoo\r\n3\r\nbar\r\n"


这就是它的通信协议,相当简单,比如我们想实现一个简单的get key操作,那么协议对应的字符格式为


*2\r\n$3\r\nget\r\n$3\r\nkey\r\n

然后传送给服务端就行。


Redis蜜罐


对于蜜罐,我们只需要实现服务端即可,客户端就用redis-cli这个工具就行。废话不多说,直接贴一下项目github.com/SSRemex/sil…



很简单,server.py实现了一个socket服务,同时加了日志收集功能。


而resp.py则实现了命令解析、命令执行、结果格式处理等操作


命令解析和结果格式主要是协议的解析和封装,这里主要想说一下两点,就是实现了一部分redis的命令


...
class RespHandler:
def __init__(self):
# 用来临时存储数据的字典
self.k_v_dict = {
"admin": "12345"
}

self.executable_command = {
"ping": (self.ping, True),
"get": (self.get, True),
"set": (self.set, True),
"keys": (self.keys, True),
"auth": (self.auth, True),
"del": (self.delete, True),
"exists": (self.exists, True),
"dbsize": (self.dbsize, True),
"config": (self.config, True)

}
self.unexecutable_command = [
"hget", "hset", "hdel", "hlen", "hexists", "hkeys", "hvals", "hgetall", "hincrby", "hincrbyfloat",
"hstrlen", "shutdown", "expire", "expireat", "pexpire", "pexpireat", "ttl", "type", "rename", "renamenx",
"randomkey", "move", "dump", "restore", "migrate", "scan", "select", "flushdb", "flushall", "mset", "mget",
"incr", "decr", "append", "strlen", "getset", "setrange", "getrange", "rpush", "lpush", "linsert", "lrange",
"lindex", "llen", "rpop", "lpop", "lrem", "lset", "blpop",

]
...

这里我内定了一些命令,并实现了他们的功能,让它像真的redis一样,你甚至可以进行kv操作,同时为了真实性,设定了一堆不可执行的命令,调用时会返回redis的报错,就像在配置文件里面禁用了这些命令一样。


演示


服务端执行,默认运行在3998端口



redis-cli连接



可以发现成功连接



此时服务端这边也接收到了,并且生成了日志



接下来,我们在redis-cli执行一些命令



很完美,甚至可以进行set get操作。


最后再次附上项目地址:github.com/SSRemex/sil…


作者:银空飞羽
来源:juejin.cn/post/7316783747491086371
收起阅读 »

90%的Java开发人员都会犯的5个错误

前言 作为一名java开发程序员,不知道大家有没有遇到过一些匪夷所思的bug。这些错误通常需要您几个小时才能解决。当你找到它们的时候,你可能会默默地骂自己是个傻瓜。是的,这些可笑的bug基本上都是你忽略了一些基础知识造成的。其实都是很低级的错误。今天,我总结一...
继续阅读 »



前言


作为一名java开发程序员,不知道大家有没有遇到过一些匪夷所思的bug。这些错误通常需要您几个小时才能解决。当你找到它们的时候,你可能会默默地骂自己是个傻瓜。是的,这些可笑的bug基本上都是你忽略了一些基础知识造成的。其实都是很低级的错误。今天,我总结一些常见的编码错误,然后给出解决方案。希望大家在日常编码中能够避免这样的问题。


1. 使用Objects.equals比较对象


这种方法相信大家并不陌生,甚至很多人都经常使用。是JDK7提供的一种方法,可以快速实现对象的比较,有效避免烦人的空指针检查。但是这种方法很容易用错,例如:


Long longValue = 123L;
System.out.println(longValue==123); //true
System.out.println(Objects.equals(longValue,123)); //false

为什么替换==Objects.equals()会导致不同的结果?这是因为使用==编译器会得到封装类型对应的基本数据类型longValue,然后与这个基本数据类型进行比较,相当于编译器会自动将常量转换为比较基本数据类型, 而不是包装类型。


使用该Objects.equals()方法后,编译器默认常量的基本数据类型为int。下面是源码Objects.equals(),其中a.equals(b)使用的是Long.equals()会判断对象类型,因为编译器已经认为常量是int类型,所以比较结果一定是false


public static boolean equals(Object a, Object b) {
return (a == b) || (a != null && a.equals(b));
}

public boolean equals(Object obj) {
if (obj instanceof Long) {
return value == ((Long)obj).longValue();
}
return false;
}

知道了原因,解决方法就很简单了。直接声明常量的数据类型,如Objects.equals(longValue,123L)。其实如果逻辑严密,就不会出现上面的问题。我们需要做的是保持良好的编码习惯。


2. 日期格式错误


在我们日常的开发中,经常需要对日期进行格式化,但是很多人使用的格式不对,导致出现意想不到的情况。请看下面的例子。


Instant instant = Instant.parse("2021-12-31T00:00:00.00Z");
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("YYYY-MM-dd HH:mm:ss")
.withZone(ZoneId.systemDefault());
System.out.println(formatter.format(instant));//2022-12-31 08:00:00

以上用于YYYY-MM-dd格式化, 年从2021 变成了 2022。为什么?这是因为 javaDateTimeFormatter 模式YYYYyyyy之间存在细微的差异。它们都代表一年,但是yyyy代表日历年,而YYYY代表星期。这是一个细微的差异,仅会导致一年左右的变更问题,因此您的代码本可以一直正常运行,而仅在新的一年中引发问题。12月31日按周计算的年份是2022年,正确的方式应该是使用yyyy-MM-dd格式化日期。


这个bug特别隐蔽。这在平时不会有问题。它只会在新的一年到来时触发。我公司就因为这个bug造成了生产事故。


3. 在 ThreadPool 中使用 ThreadLocal


如果创建一个ThreadLocal 变量,访问该变量的线程将创建一个线程局部变量。合理使用ThreadLocal可以避免线程安全问题。


但是,如果在线程池中使用ThreadLocal ,就要小心了。您的代码可能会产生意想不到的结果。举个很简单的例子,假设我们有一个电商平台,用户购买商品后需要发邮件确认。


private ThreadLocal currentUser = ThreadLocal.withInitial(() -> null);

private ExecutorService executorService = Executors.newFixedThreadPool(4);

public void executor() {
executorService.submit(()->{
User user = currentUser.get();
Integer userId = user.getId();
sendEmail(userId);
});
}

如果我们使用ThreadLocal来保存用户信息,这里就会有一个隐藏的bug。因为使用了线程池,线程是可以复用的,所以在使用ThreadLocal获取用户信息的时候,很可能会误获取到别人的信息。您可以使用会话来解决这个问题。


4. 使用HashSet去除重复数据


在编码的时候,我们经常会有去重的需求。一想到去重,很多人首先想到的就是用HashSet去重。但是,不小心使用 HashSet 可能会导致去重失败。


User user1 = new User();
user1.setUsername("test");

User user2 = new User();
user2.setUsername("test");

List users = Arrays.asList(user1, user2);
HashSet sets = new HashSet<>(users);
System.out.println(sets.size());// the size is 2

细心的读者应该已经猜到失败的原因了。HashSet使用hashcode对哈希表进行寻址,使用equals方法判断对象是否相等。如果自定义对象没有重写hashcode方法和equals方法,则默认使用父对象的hashcode方法和equals方法。所以HashSet会认为这是两个不同的对象,所以导致去重失败。


5. 线程池中的异常被吃掉


ExecutorService executorService = Executors.newFixedThreadPool(1);
executorService.submit(()->{
//do something
double result = 10/0;
});

上面的代码模拟了一个线程池抛出异常的场景。我们真正的业务代码要处理各种可能出现的情况,所以很有可能因为某些特定的原因而触发RuntimeException


但是如果没有特殊处理,这个异常就会被线程池吃掉。这样就会导出出现问题你都不知道,这是很严重的后果。因此,最好在线程池中try catch捕获异常。


总结


本文总结了在开发过程中很容易犯的5个错误,希望大家养成良好的编码习惯。


作者:JAVA旭阳
来源:juejin.cn/post/7182184496517611576
收起阅读 »

一个左侧导航栏的选中状态把我迷得颠三倒四

web
事情是这样的 👇 前段时间我用arco.design这个组件库默认的模板项目开放,刚开始需求是这样的:总共八个页面,其中四个显示在导航栏,四个不在导航栏显示,可以导航的页面里又有俩个需要登录才能访问,俩个不登录就能访问。听着是有些绕不过开发起来不是SO EAS...
继续阅读 »

事情是这样的 👇


前段时间我用arco.design这个组件库默认的模板项目开放,刚开始需求是这样的:总共八个页面,其中四个显示在导航栏,四个不在导航栏显示,可以导航的页面里又有俩个需要登录才能访问,俩个不登录就能访问。听着是有些绕不过开发起来不是SO EASY吗😎


我用的是vue版本的,说白了就一个知识点——路由和菜单。凭借我的聪明才智,肯定一看就会。


路由


首先,需要先了解一下路由表的配置。基本的路由配置请参阅 Vue-Router 官方文档


// 在本例子中,页面最终路径为 /dashboard/workplace
export default {
path: 'dashboard',
name: 'dashboard', // 路由名称
component: () => import('@/views/dashboard/index.vue'),
meta: {
locale: 'menu.dashboard',
requiresAuth: true,
icon: 'icon-dashboard',
},
children: [
{
path: 'workplace',
name: 'workplace',
component: () => import('@/views/dashboard/workplace/index.vue'),
meta: {
locale: 'menu.dashboard.workplace',
requiresAuth: true,
roles: ['admin'],
hideInMenu: false,
},
},
],
};

路由 Meta 元信息


参数名说明类型默认值
roles配置能访问该页面的角色,如果不匹配,则会被禁止访问该路由页面string[]-
requiresAuth是否需要登录鉴权booleanfalse
icon菜单配置iconstring-
locale一级菜单名(语言包键名)string-
hideInMenu是否在左侧菜单中隐藏该项boolean-
hideChildrenInMenu强制在左侧菜单中显示单项boolean-
activeMenu高亮设置的菜单项string-
order排序路由菜单项。如果设置该值,值越高,越靠前number-
noAffix如果设置为true,标签将不会添加到tab-bar中boolean-
ignoreCache如果设置为true页面将不会被缓存boolean-

hideInMenu 控制菜单显示, requiresAuth 控制是否需要登录,没错,以上知识点完全够我用了🤏🤏🤏🤏


三下五除二就开发完成了,正当我沉浸在成功的喜悦时,测试给我提了个bug。


说导航栏目切换,选中状态有俩个正常,俩个不正常,切换页面导航没选中,再点一次才会选中。我擦👏👏👏👏👏,我傻眼了😧😧😧


到底哪里出了问题,我哪里写错了呢,人家官方肯定没问题,于是我开始寻找。是路由名称重复了,还是那个组件写的有问题,还好有俩个正常的,我仔细比对一下不就好了吗,我可真是个小机灵鬼。


就这样,我又一次为我的自大付出了汗水,对比了一天,我感觉好像真不是我写的有问题,不过还是有收获的,我发现requiresAuth 设置为true的导航正常,requiresAuth设置为false的不正常。抱着怀疑的态度我开始找原来模板项目中处理requiresAuth的代码。最后在components》menu》index.vue文件下发现一个方法:


listenerRouteChange((newRoute) => {
const { requiresAuth, activeMenu, hideInMenu } = newRoute.meta;
if (requiresAuth && (!hideInMenu || activeMenu)) {
const menuOpenKeys = findMenuOpenKeys(
(activeMenu || newRoute.name) as string
);

const keySet = new Set([...menuOpenKeys, ...openKeys.value]);
openKeys.value = [...keySet];

selectedKey.value = [
activeMenu || menuOpenKeys[menuOpenKeys.length - 1],
];
}
}, true);

这... ... 我人麻了,这不是只有requiresAuth 为true的时候才会有效吗?他为啥这么写呢?还是我复制项目的时候复制错了?然后我改成了


listenerRouteChange((newRoute) => {
const { requiresAuth, activeMenu, hideInMenu } = newRoute.meta;
if ((requiresAuth === true || requiresAuth === false) && (!hideInMenu || activeMenu)) {
const menuOpenKeys = findMenuOpenKeys(
(activeMenu || newRoute.name) as string
);

const keySet = new Set([...menuOpenKeys, ...openKeys.value]);
openKeys.value = [...keySet];

selectedKey.value = [
activeMenu || menuOpenKeys[menuOpenKeys.length - 1],
];
}
}, true);

🙈🙈🙈🙈🙈看出来我改哪里了吗,考考你们的眼力,我改的方法是不是很nice。
反正好使了,下班!!!!


作者:一路向北京
来源:juejin.cn/post/7317277887567151145
收起阅读 »

【日常总结】解决el-select数据量过大的3种方法

web
背景 最近做完一个小的后台管理系统,快上线了,发现一个问题,有2个select的选项框线上的数据量是1w+。。而测试环境都是几百的,所以导致页面直接卡住了,over了。 想了一下,如果接口支持搜索和分页,那直接通过上拉加载就可以了。但后端不太愿意改😄。行吧,...
继续阅读 »

背景


最近做完一个小的后台管理系统,快上线了,发现一个问题,有2个select的选项框线上的数据量是1w+。。而测试环境都是几百的,所以导致页面直接卡住了,over了。


image.png


想了一下,如果接口支持搜索和分页,那直接通过上拉加载就可以了。但后端不太愿意改😄。行吧,前端搞也是可以的。


这个故事还有个后续


image.png


过了一周上线后,发现有一个下拉框的数据有30w+!!!加载都加载不出来,哈哈哈哈,接口直接超时报错了,所以又cuocuocuo改了一遍,最后改成了:



  1. 接口翻页请求

  2. 前端使用自定义指令实现上拉加载更多,搜索直接走的后端接口


方案


通过一顿搜索加联想总结了3种方法,以下方法都需要支持开启filterable支持搜索。


标题具体问题
方案1只展示前100条数据,这个的话配合filter-method每次只返回前100条数据。限制展示的条数可能不全,搜索需要多搜索点内容
方案2分页方式,通过指令实现上拉加载,不断上拉数据展示数据。仅过滤加载出来的数据,需要配合filterMethod过滤数据
方案3options列表采用虚拟列表实现。成本高,需要引入虚拟列表组件或者自己手写。经掘友指点,发现element-plus提供了对应的实现,如果是plus,则可以直接使用select-v2

方案一(青铜段位) filterMethod直接过滤数据量


<template>
<el-select
v-model="value"
clearable filterable
:filter-method="filterMethod">

<el-option
v-for="(item, index) in options.slice(0, 100)"
:key="index"
:label="item.label"
:value="item.value">

</el-option>
</el-select>

</template>
export default {
name: 'Demo',
data() {
return {
options: [],
value: ''
}
},
beforeMount() {
this.getList();
},
methods: {
// 模拟获取大量数据
getList() {
for (let i = 0; i < 25000; i++) {
this.options.push({label: "选择"+i,value:"选择"+i});
}
},
filterMethod(val) {
console.log('filterMethod', val);
this.options = this.options.filter(item => item.value.indexOf(val) > -1).slice(0, 100);
},
visibleChange() {
console.log('visibleChange');
}
}
}

方案二(白银段位) 自定义滚动指令,实现翻页加载


写自定义滚动指令,options列表滚动到底部后,再加载下一页。但这时候筛选出来的是已经滚动出来的值。


这里如果直接使用filterable来搜索,搜索出来的内容是已经滑动出来的内容。如果想筛选全部的,就需要重写filterMethod方法来自定义过滤功能。可以根据情况选择是否要重写filterMethod。


image.png


<template>
<div class="dashboard-editor-container">
<el-select
v-model="value"
clearable
filterable
v-el-select-loadmore="loadMore"
:filter-method="filterMethod">

<el-option
v-for="(item, index) in options"
:key="index"
:label="item.label"
:value="item.value">

</el-option>
</el-select>
</div>

</template>
<script>
export default {
name: 'Demo',
data() {
return {
options: [],
value: '',
pageNo: 0
}
},
beforeMount() {
this.getList();
},
methods: {
// 模拟获取大量数据
getList() {
const data = [];
for (let i = 0; i < 25000; i++) {
data.push({label: "选择"+i,value:"选择"+i});
}
this.allData = data;
this.data = data;
this.getPageList()
},
getPageList(pageSize = 10) {
this.pageNo++;
const list = this.data.slice(0, pageSize * (this.pageNo));
this.options = list;
},
loadMore() {
this.getPageList();
},
filterMethod(val) {
this.data = val ? this.allData.filter(item => item.label.indexOf(val) > -1) : this.allData;
this.getPageList();
}
},
directives:{
'el-select-loadmore':(el, binding) => {
// 获取element-ui定义好的scroll父元素
const wrapEl = el.querySelector(".el-select-dropdown .el-select-dropdown__wrap");
if(wrapEl){
wrapEl.addEventListener("scroll", function () {
/**
* scrollHeight 获取元素内容高度(只读)
* scrollTop 获取或者设置元素的偏移值,
* 常用于:计算滚动条的位置, 当一个元素的容器没有产生垂直方向的滚动条, 那它的scrollTop的值默认为0.
* clientHeight 读取元素的可见高度(只读)
* 如果元素滚动到底, 下面等式返回true, 没有则返回false:
* ele.scrollHeight - ele.scrollTop === ele.clientHeight;
*/

if (this.scrollTop + this.clientHeight >= this.scrollHeight) {
// binding的value就是绑定的loadmore函数
binding.value();
}
});
}
},
},
}
</script>

</script>

方案三(黄金段位) 虚拟列表


引入社区的vue-virtual-scroll-list 支持虚拟列表。但这里想的自己再实现一遍虚拟列表,后续再写吧。


另外,element-plus提供了对应的实现,如果是使用的是plus,则可以直接使用 select-v2组件


<template>
<div class="dashboard-editor-container">
<el-select
v-model="value"
clearable
filterable >

<virtual-list
class="list"
style="height: 360px; overflow-y: auto;"
:data-key="'value'"
:data-sources="data"
:data-component="item"
:estimate-size="50"
/>

</el-select>
</div>

</template>
<script>
import VirtualList from 'vue-virtual-scroll-list';
import Item from './item';
export default {
name: 'Demo',
components: {VirtualList, Item},
data() {
return {
options: [],
data: [],
value: '',
pageNo: 0,
item: Item,
}
},
beforeMount() {
this.getList();
},
methods: {
// 模拟获取大量数据
getList() {
const data = [];
for (let i = 0; i < 25000; i++) {
data.push({label: "选择"+i,value:"选择"+i});
}
this.allData = data;
this.data = data;
this.getPageList()
},
getPageList(pageSize = 10) {
this.pageNo++;
const list = this.data.slice(0, pageSize * (this.pageNo));
this.options = list;
},
loadMore() {
this.getPageList();
}
}
}
</script>


// item组件
<template>
<el-option :label="source.label" :value="source.value"></el-option>
</template>


<script>
export default {
name: 'item',
props: {
source: {
type: Object,
default() {
return {}
}
}
}
}
</script>


<style scoped>
</style>



总结


最后我们项目中使用的虚拟列表,为啥,因为忽然发现组件库支持select是虚拟列表,那就直接使用这个啦。


最后的最后


没有用虚拟列表,因为接口数据量过大(你见过返回30w+的接口吗🙄。。),后端接口改成分页,前端支持自定义指令上拉加载,引用的参数增加了remote、remote-method设置为远端的方法。


<template>
<div class="dashboard-editor-container">
<el-select
v-model="value"
clearable
filterable
v-el-select-loadmore="loadMore"
remote
:remote-method="remoteMethod">

<el-option
v-for="(item, index) in options"
:key="index"
:label="item.label"
:value="item.value">

</el-option>
</el-select>
</div>

</template>

参考文章:



作者:searchop
来源:juejin.cn/post/7278238985448341544
收起阅读 »

为啥TextureView比SurfaceView表现还差呢?

从原理上面讲,我们大众的认知就是TextureView比SurfaceView的性能要好。硬的比软的好。但是其实这种是片面的。最近就遇到一个奇怪的现象:在3399上面通过ffmpeg拉rtsp流,然后通过mediacodec解码后渲染。渲染到TextureVi...
继续阅读 »

从原理上面讲,我们大众的认知就是TextureView比SurfaceView的性能要好。硬的比软的好。但是其实这种是片面的。最近就遇到一个奇怪的现象:在3399上面通过ffmpeg拉rtsp流,然后通过mediacodec解码后渲染。渲染到TextureView上会比较频繁的出现马赛克的现象。但是换用SurfaceView立马就变好了。


TextureView 和 SurfaceView 都有各自的优势和局限性,所以它们的性能表现也会因应用的具体需求和使用场景而异。
在某些情况下,TextureView 的性能可能会比 SurfaceView 差,原因可能有以下几点:



  1. 渲染管道的差异:TextureView 是基于 OpenGL ES 的,它使用图形渲染管道来渲染内容。而 SurfaceView 则使用传统的 Android 渲染管道,这与 Android 的视图系统更加紧密集成。在某些情况下,这可能会导致 SurfaceView 的性能更好。

  2. 线程管理:SurfaceView 使用一个独立的线程来渲染内容,这可以提供更平滑的渲染性能,尤其是在处理复杂动画或游戏时。而 TextureView 则在主线程上渲染内容,这可能会导致性能下降,尤其是在处理大量数据或复杂渲染时。

  3. 硬件加速:虽然 TextureView 支持硬件加速,但在某些情况下,硬件加速可能会导致性能问题,尤其是在低端设备上。SurfaceView 则更多地依赖于软件渲染,这可能在某些情况下会提供更稳定的性能。


需要注意的是,性能差异可能会因设备和应用而异,因此在实际开发中应该根据具体需求和性能测试结果来选择合适的视图。无论选择哪种视图,都应该优化代码以提高性能,并确保在不同设备上进行充分的测试。


于是,我针对上面的3点的结论做了一个实验,在3399上面ffmpeg硬解码居然比软解码帧率要低。看来3399的CPU性能比其他硬件确实要抢。这就证明了标题中的疑惑了。


下面贴出一段出马赛克的代码,换上SurfaceView就好了。


public class IPCameraPreviewFragment extends Fragment implements TextureView.SurfaceTextureListener{

public static final String TAG = "IPCameraPreviewFragment";
public static final boolean DEBUG = true;

private TextureView mPreview;
private SurfaceTexture mSurfaceTexture;
private Handler mUiHandler = new Handler();
private Runnable mRunnable = new Runnable() {

@Override
public void run() {
if(mPreview == null || mSurfaceTexture == null) return;
Play.getInstances().startPreivew(new Surface(mSurfaceTexture));
}
};
private IErrorCallback mErrorCallback = new IErrorCallback() {

@Override
public void onError(int error) {
Log.d(TAG, "onError = " + error);
if(null == mUiHandler || null == mRunnable) return;
mUiHandler.removeCallbacks(mRunnable);
mUiHandler.postDelayed(mRunnable, 5000);
}
};

public void setDataSource(String source){
Play.getInstances().setErrorCallback(mErrorCallback);
Play.getInstances().setDataSource(source);
}

@Override
public View onCreateView(LayoutInflater inflater, ViewGr0up container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.preview_fragment, container,false);
mPreview = (TextureView)view.findViewById(R.id.preview);
mPreview.setSurfaceTextureListener(this);
return view;
}

@Override
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width,
int height)
{
mSurfaceTexture = surface;
Play.getInstances().startPreivew(new Surface(surface));
}

@Override
public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width,
int height)
{

}

@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
mSurfaceTexture = null;
Play.getInstances().releaseMediaPlay();
if(null == mUiHandler || null == mRunnable) return false;
mUiHandler.removeCallbacks(mRunnable);
mUiHandler = null;
mRunnable = null;
return false;
}

@Override
public void onSurfaceTextureUpdated(SurfaceTexture surface) {

}

}

作者:逗比先生
来源:juejin.cn/post/7316592817341218866
收起阅读 »

App防止恶意截屏功能的方法:iOS、Android和鸿蒙系统的实现方案

防止应用被截图是一个比较常见的需求,主要是出于安全考虑。下面将分别为iOS(苹果系统)、Android(安卓系统)及HarmonyOS(鸿蒙系统)提供防止截屏的方法和示例代码。在企业内部使用的应用中,防止员工恶意截屏是一个重要的安全需求。本文将详细介绍iOS、...
继续阅读 »

防止应用被截图是一个比较常见的需求,主要是出于安全考虑。下面将分别为iOS(苹果系统)、Android(安卓系统)及HarmonyOS(鸿蒙系统)提供防止截屏的方法和示例代码。

123456 (161).png

在企业内部使用的应用中,防止员工恶意截屏是一个重要的安全需求。本文将详细介绍iOS、Android和鸿蒙系统的防止截屏的方法,并提供相应的代码示例,以帮助代码初学者理解和实现该功能。

iOS系统防止截屏方法:

在iOS系统中,可以通过设置UIWindow的windowLevel为UIWindowLevelNormal + 1,使应用窗口覆盖在截屏窗口之上,从而阻止截屏。以下是Objective-C和Swift两种语言的代码示例:

  1. iOS系统防止截屏

在iOS中,可以使用UIScreen的isCaptured属性来检测屏幕是否被录制或截图。为了防止截屏,你可以监听UIScreenCapturedDidChange通知,当屏幕开始被捕获时,你可以做一些操作,比如模糊视图或显示一个全屏的安全警告。

swift

// 注册屏幕捕获变化通知
NotificationCenter.default.addObserver(
    self,
    selector: #selector(screenCaptureChanged),
    name: UIScreen.capturedDidChangeNotification,
    object: nil
)
@objc func screenCaptureChanged(notificationNSNotification) {
    if UIScreen.main.isCaptured {
        // 屏幕正在被捕获,可以在这里做一些隐藏内容的操作,比如
        // 显示一个覆盖所有内容的视图
    } else {
        // 屏幕没有被捕获,可以移除那个覆盖的视图
    }
}

但需要注意的是,iOS不允许应用程序完全禁止截屏。因为截图功能是系统级别的,而不是应用级别的,上述代码只能做到在截图时采取一定的响应措施,不能完全防止。

  1. Android系统防止截屏

在Android中,可以通过设置Window的属性来防止用户截图或录屏。这通过禁用FLAG_SECURE来实现。

java

// 在Activity中设置禁止截屏
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    // 在setContentView之前调用
    getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE,
                         WindowManager.LayoutParams.FLAG_SECURE);
    setContentView(R.layout.activity_main);
}

这样设置后,当前的Activity将无法被截屏或录屏。

  1. HarmonyOS(鸿蒙系统)防止截屏

HarmonyOS是华为开发的一个分布式操作系统,目前它在应用开发中有着与Android类似的API。因此可以使用与Android相同的方法进行禁止截屏。

java

// 在Ability(Activity)中设置禁止截屏
@Override
protected void onStart(Intent intent) {
    super.onStart(intent);
    // 在setUIContent之前调用
    getWindow().addFlags(WindowManager.LayoutConfig.FLAG_SECURE);
    setUIContent(ResourceTable.Layout_ability_main);
}

在HarmonyOS中,Ability相当于Android中的Activity。

请注意尽管上述方法能够有效地防止绝大多数截屏和录屏行为,但技术上并不是100%无法绕过的(例如某些root设备或具有特殊权限的应用可能可以绕过这些限制)。因此,在处理非常敏感的信息时,请综合其他安全措施一起使用,比如数据加密、用户行为分析等。


作者:咕噜分发企业签名梦奇
来源:juejin.cn/post/7317095140040376346
收起阅读 »

如何优雅的将MultipartFile和File互转

我们在开发过程中经常需要接收前端传来的文件,通常需要处理MultipartFile格式的文件。今天来介绍一下MultipartFile和File怎么进行优雅的互转。 前言 首先来区别一下MultipartFile和File: MultipartFile是 S...
继续阅读 »

我们在开发过程中经常需要接收前端传来的文件,通常需要处理MultipartFile格式的文件。今天来介绍一下MultipartFile和File怎么进行优雅的互转。


前言


首先来区别一下MultipartFile和File:



  • MultipartFile是 Spring 框架的一部分,File是 Java 标准库的一部分。

  • MultipartFile主要用于接收上传的文件,File主要用于操作系统文件。


MultipartFile转换为File


使用 transferTo


这是一种最简单的方法,使用MultipartFile自带的transferTo 方法将MultipartFile转换为File,这里通过上传表单文件,将MultipartFile转换为File格式,然后输出到特定的路径,具体写法如下。


transferto.png


使用 FileOutputStream


这是最常用的一种方法,使用 FileOutputStream 可以将字节写入文件。具体写法如下。


FileOutputStream.png


使用 Java NIO


Java NIO 提供了文件复制的方法。具体写法如下。


copy.png


File装换为MultipartFile


从File转换为MultipartFile 通常在测试或模拟场景中使用,生产环境一般不这么用,这里只介绍一种最常用的方法。


使用 MockMultipartFile


在转换之前先确保引入了spring-test 依赖(以Maven举例)


<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>version</version>
<scope>test</scope>
</dependency>

通过获得File文件的名称、mime类型以及内容将其转换为MultipartFile格式。具体写法如下。


multi.png



更多文章干货,推荐公众号【程序员老J】



作者:程序员老J
来源:juejin.cn/post/7295559402475667492
收起阅读 »

从 Vue 2 迁移到 Svelte

web
大家好,这里是大家的林语冰。 本周之后,Vue 2 将停止开源维护。所以本期《前端翻译计划》共享的是某企业去年从 Vue 2 迁移到 Svelte 的现实测评,以及 Vue 3 和 Svelte 的“零和博弈”。 在使用 Vue 2 作为我们的前端框架近 2 ...
继续阅读 »

大家好,这里是大家的林语冰。


本周之后,Vue 2 将停止开源维护。所以本期《前端翻译计划》共享的是某企业去年从 Vue 2 迁移到 Svelte 的现实测评,以及 Vue 3 和 Svelte 的“零和博弈”。


在使用 Vue 2 作为我们的前端框架近 2 年后,我们宣布此支持将不再维护,因此我们决定迁移到新框架。但该选谁呢:Vue 3 还是 Svelte 呢


粉丝请注意,我们迁移后的目标也是改善 DX(开发体验),尤其是类型检查、性能和构建时间。我们没有考虑 React,因为它需要投资一大坨时间成本来学习,而且与 Vue 和 Svelte 不同,它没有提供开箱即用的解决方案。此外,后者共享相同的 SFC(单文件组件)概念:同一文件中的逻辑(JS)、结构(HTML)和样式(CSS)。


Svelte vs Vue 3


Svelte 的留存率更高。对于我们的新前端,我们必须从市场上可用的 2 个框架权衡,即 Svelte 和 Vue 3。下面是过去 5 年不同框架留存率的图示(留存率 = 会再次使用/(会再次使用 + 不会再次使用))。JS 现状调查汇编了该领域开发者的数据,如你所见,Svelte 排名第二,而 Vue 3 排名第四。


01-state.jpg


这启示我们,过去使用过 Svelte 的开发者愿意再次使用它的数量比不愿使用它的要多。


Svelte 的类型体验更棒


Vue 2Vue 3Svelte
组件级类型YesYesNo
跨组件类型NoYesYes
类型事件NoNoYes

Svelte 通过更简单的组件设计流程和内置类型事件提供了更好的类型体验,对我们而言十分用户友好。


全局变量访问限制。使用 Svelte,可以从其他文件导入枚举,并在模板中使用它们,而 Vue 3 则达咩。


02-benchmark.png


语法。就我个人而言,私以为 Svelte 语法比 Vue 更优雅和用户友好。您可以瞄一下下面的代码块,并亲自查看它们。


Svelte:


03-svelte.png


Vue:


04-vue.png


没有额外的 HTML div <template>。在 Svelte 中您可以直接编写自己的 HTML。


样式在 Svelte 中会自动确定作用域,这对于可维护性而言是一个优点,且有助于避免 CSS 副作用。每个组件的样式独立,能且仅能影响该组件,而不影响其父/子组件。


更新数据无需计算属性。在 Svelte 中,感觉更像在用纯 JS 撸码。您只需要专注于编写一个箭头函数:


const reset = () => {
firstName = ''
lastName = ''
}

Svelte 中只需单个括号:


//Svelte
{fullName}

//Vue
{{fullName}}

添加纯 JS 插件更简单。此乃使用 Svelte 和 Prism.js 的语法高亮集成用例,如下所示:


05-prism.png


无需虚拟 DOM 即可编译代码。Svelte 和 Vue 之间的主要区别是,减少了浏览器和 App 之间的层数,实现更优化、更快的任务成果。


自动更新。诉诸声明变量的辅助,Svelte 可以自动更新您的数据。这样,您就不必等待变更反映在虚拟结构中,获得更棒的 UX(用户体验)。


Svelte 也有短板


理所当然,Svelte 也有短板,比如社区相对较小,因为它是 2019 年才诞生的。但随着越来越多的开发者可能会认识到其质量和用户友好的内容,支持以及社区未来可能会不断发展壮大。


因此,在审查了此分析的结果后,尽管 SvelteKit 在迁移时仍处于积极开发阶段,我们决定使用 Svelte 和 Svelte Kit 砥砺前行。


06cons.png


如何处理迁移呢?


时间:我们选择在 8 月份处理迁移,当时该 App 用户较少。


时间长度:我们花了 2 周时间将所有文件从 Vue 迁移到 Svelte。


开发者数量:2 名前端开发者全职打工 2 周,另一名开发者全职打工 1 周,因此涉及 3 名开发人员。


工作流:首先,我们使用 Notion 工具将我们的凭据归属于团队的开发者。然后,我们开始在 Storybook 中创建新组件,最后,每个开发者都会奖励若干需要在 Svelte 中重写的页面。


作为一家初创公司,迁移更简单,因为我们没有 1_000 个文件需要重写,因此我们可以快速执行。虽然但是,当 SvelteKit 仍处于积极开发阶段时,我们就冒着风险开始迁移到 SvelteKit,这导致我们在迁移仅 1 个月后就不得不做出破坏性更新。但 SvelteKit 专业且博大精深的团队为我们提供了一个命令(npx svelte-migrate routes),以及一个解释清晰的迁移指南,真正帮助我们快速适应新的更新。


此外,9 月份,SvelteKit 团队宣布该框架终于进入候选版本阶段,这意味着,它的稳定性现在得到了保证!


文件和组件组织


SvelteKit 的“文件夹筑基路由”给我们带来了很多。我们可以将页面拆分为子页面,复用标准变量名,比如 loading/submit 等等。此外,布局直接集成到相关路由中,由于树内组织的增加,访问起来更简单。


那么我们得到了什么?


除了上述好处之外,还值得探讨其他某些关键因素:


性能提高且更流畅。编译完成后,我们可以体会到该 App 的轻量级。与其他框架相比,这提高了加载速度,其他框架在 App 的逻辑代码旁嵌入了“运行时”。


DX 更棒。SvelteKit 使用 Vite 打包器,此乃新一代 JS 构建工具,它利用浏览器中 ES 模块的可用性和编译为原生(compile-to-native)的打包器,为您带来最新 JS 技术的最佳 DX。


代码执行更快。它没有虚拟 DOM,因此在页面上变更时,需要执行的层数少了一层。


启动并运行 SSR(服务器端渲染)。如果最终用户没有良好的互联网连接或启用 JS,平台仍将在 SSR 的帮助下高效运行,因为用户仍能加载网页,同时失去交互性。


代码简洁易懂。Svelte 通过将逻辑(JS)、结构(HTML)和样式(CSS)分组到同一文件中,可以使用更具可读性和可维护性的面向组件的代码。黑科技在于所有这些元素都编译在 .svelte 文件中。


固定类型检查。自从我们迁移到 Svelte 以来,我们已经成功解决了类型检查的最初问题。事实上,我们以前必须处理周期性的通知,而如今时过境迁。不再出现头大的哨兵错误。(见下文)


07-error.jpg


粉丝请注意,此博客乃之前的迁移测评,其中某些基准测试见仁见智,尤大还亲自码字撰写博客布道分享,我们之后会继续翻译 Vue 官方博客详细说明。



免责声明


本文属于是语冰的直男翻译了属于是,略有删改,仅供粉丝参考,英文原味版请传送 Migrating from Vue 2 to Svelte



您现在收看的是《前端翻译计划》,学废了的小伙伴可以订阅此专栏合集,我们每天佛系投稿,欢迎关注地球猫猫教。谢谢大家的点赞,掰掰~


26-cat.gif


作者:人猫神话
来源:juejin.cn/post/7317222425384714294
收起阅读 »

让SQL起飞(优化)

最近博主看完了《SQL进阶教程》这本书,看完后给博主打开了SQL世界的新大门,对于 SQL 的理解不在局限于以前的常规用法。借用其他读者的评论, 读完醍醐灌顶,对SQL做到了知其然更能知其所以然。全书从头到尾强调了 SQL的内在逻辑是基于集合论和谓词逻辑,而...
继续阅读 »

最近博主看完了《SQL进阶教程》这本书,看完后给博主打开了SQL世界的新大门,对于 SQL 的理解不在局限于以前的常规用法。借用其他读者的评论,



读完醍醐灌顶,对SQL做到了知其然更能知其所以然。全书从头到尾强调了 SQL的内在逻辑是基于集合论和谓词逻辑,而这两条主线恰恰对使用SQL起到了至关重要的指导作用。



本文给大家总结如何让SQL起飞(优化)


一、SQL写法优化


在SQL中,很多时候不同的SQL代码能够得出相同结果。从理论上来说,我们认为得到相同结果的不同SQL之间应该有相同的性能,但遗憾的是,查询优化器生成的执行计划很大程度上受到SQL代码影响,有快有慢。因此如果想优化查询性能,我们必须知道如何写出更快的SQL,才能使优化器的执行效率更高。


1.1 子查询用EXISTS代替IN


当IN的参数是子查询时,数据库首先会执行子查询,然后将结果存储在一张临时的工作表里(内联视图),然后扫描整个视图。很多情况下这种做法都非常耗费资源。使用EXISTS的话,数据库不会生成临时的工作表。但是从代码的可读性上来看,IN要比EXISTS好。使用IN时的代码看起来更加一目了然,易于理解。因此,如果确信使用IN也能快速获取结果,就没有必要非得改成EXISTS了。


这里用Class_A表和Class_B举例,

我们试着从Class_A表中查出同时存在于Class_B表中的员工。下面两条SQL语句返回的结果是一样的,但是使用EXISTS的SQL语句更快一些。


--慢
SELECT *
FROM Class_A
WHERE id IN (SELECT id
FROM Class_B);

--快
SELECT *
FROM Class_A A
WHERE EXISTS
(SELECT *
FROM Class_B B
WHERE A.id = B.id);

使用EXISTS时更快的原因有以下两个。



  1. 如果连接列(id)上建立了索引,那么查询 tb_b 时不用查实际的表,只需查索引就可以了。(同样的IN也可以使用索引,这不是重要原因)

  2. 如果使用EXISTS,那么只要查到一行数据满足条件就会终止查询,不用像使用IN时一样扫描全表。在这一点上NOT EXISTS也一样。


实际上,大部分情况在子查询数量较小的场景下EXISTS和IN的查询性能不相上下,由EXISTS查询更快第二点可知,子查询数量较大时使用EXISTS才会有明显优势。


1.2 避免排序并添加索引


在SQL语言中,除了ORDER BY子句会进行显示排序外,还有很多操作默认也会在暗中进行排序,如果排序字段没有添加索引,会导致查询性能很慢。SQL中会进行排序的代表性的运算有下面这些。



  • GR0UP BY子句

  • ORDER BY子句

  • 聚合函数(SUM、COUNT、AVG、MAX、MIN)

  • DISTINCT

  • 集合运算符(UNION、INTERSECT、EXCEPT)

  • 窗口函数(RANK、ROW_NUMBER等)


如上列出的六种运算(除了集合运算符),它们后面跟随或者指定的字段都可以添加索引,这样可以加快排序。



实际上在DISTINCT关键字、GR0UP BY子句、ORDER BY子句、聚合函数跟随的字段都添加索引,不仅能加速查询,还能加速排序。



1.3 用EXISTS代替DISTINCT


为了排除重复数据,我们可能会使用DISTINCT关键字。如1.2中所说,默认情况下,它也会进行暗中排序。如果需要对两张表的连接结果进行去重,可以考虑使用EXISTS代替DISTINCT,以避免排序。这里用Items表和SalesHistory表举例:

我们思考一下如何从上面的商品表Items中找出同时存在于销售记录表SalesHistory中的商品。简而言之,就是找出有销售记录的商品。


在一(Items)对多(SalesHistory)的场景下,我们需要对item_no去重,使用DISTINCT去重,因此SQL如下:


SELECT DISTINCT I.item_no
FROM Items I INNER JOIN SalesHistory SH
ON I. item_no = SH. item_no;

item_no
-------
10
20
30

使用EXISTS代替DISTINCT去重,SQL如下:


SELECT item_no
FROM Items I
WHERE EXISTS
(SELECT
FROM SalesHistory SH
WHERE I.item_no = SH.item_no);
item_no
-------
10
20
30

这条语句在执行过程中不会进行排序。而且使用EXISTS和使用连接一样高效。


1.4 集合运算ALL可选项


SQL中有UNION、INTERSECT、EXCEPT三个集合运算符。在默认的使用方式下,这些运算符会为了排除掉重复数据而进行排序。



MySQL还没有实现INTERSECT和EXCEPT运算



如果不在乎结果中是否有重复数据,或者事先知道不会有重复数据,请使用UNION ALL代替UNION。这样就不会进行排序了。


1.5 WHERE条件不要写在HAVING字句


例如,这里继续用SalesHistory表举例,下面两条SQL语句返回的结果是一样的:


--聚合后使用HAVING子句过滤
SELECT sale_date, SUM(quantity)
FROM SalesHistory
GR0UP BY sale_date
HAVING sale_date = '2007-10-01';

--聚合前使用WHERE子句过滤
SELECT sale_date, SUM(quantity)
FROM SalesHistory
WHERE sale_date = '2007-10-01'
GR0UP BY sale_date;

但是从性能上来看,第二条语句写法效率更高。原因有两个:



  1. 使用GR0UP BY子句聚合时会进行排序,如果事先通过WHERE子句筛选出一部分行,就能够减轻排序的负担。

  2. 在WHERE子句的条件里可以使用索引。HAVING子句是针对聚合后生成的视图进行筛选的,但是很多时候聚合后的视图都没有继承原表的索引结构。


二、真的用到索引了吗


2.1 隐式的类型转换


如下,col_1字段是char类型:


-- 没走索引
SELECT * FROM SomeTable WHERE col_1 = 10;
-- 走了索引
SELECT * FROM SomeTable WHERE col_1 ='10';
-- 走了索引
SELECT * FROM SomeTable WHERE col_1 = CAST(10, AS CHAR(2));

当查询条件左边和右边类型不一致时会导致索引失效。


2.2 在索引字段上进行运算


如下:


SELECT *
FROM SomeTable
WHERE col_1 * 1.1 > 100;

在索引字段col_1上进行运算会导致索引不生效,把运算的表达式放到查询条件的右侧,就能用到索引了,像下面这样写就OK了。


WHERE col_1 > 100 / 1.1

如果无法避免在左侧进行运算,那么使用函数索引也是一种办法,但是不太推荐随意这么做。使用索引时,条件表达式的左侧应该是原始字段请牢记,这一点是在优化索引时首要关注的地方。


2.3 使用否定形式


下面这几种否定形式不能用到索引。



  • <>

  • !=

  • NOT


这个是跟具体数据库的优化器有关,如果优化器觉得即使走了索引,还是需要扫描很多很多行的话,他可以选择直接不走索引。平时我们用!=、<>、not in的时候,要注意一下。


2.4 使用OR查询前后没有同时使用索引


例如下表:


CREATE TABLE test_tb ( 
id int(11) NOT NULL AUTO_INCREMENT,
name varchar(55) NOT NULL
PRIMARY KEY (id)
)
ENGINE=InnoDB DEFAULT CHARSET=utf8;

使用OR条件进行查询


SELECT * 
FROM test_tb
WHERE id = 1 OR name = 'tom'

这个SQL的执行条件下,很明显id字段查询会走索引,但是对于OR后面name字段的查询是需要进行全表扫描的。在这个场景下,优化器会选择直接进行一遍全表扫描。


2.5 使用联合索引时,列的顺序错误


使用联合索引需要满足最左匹配原则,即最左优先。如果你建立一个(col_1, col_2, col_3)的联合索引,相当于建立了 (col_1)、(col_1,col_2)、(col_1,col_2,col_3) 三个索引。如下例子:


-- 走了索引
SELECT * FROM SomeTable WHERE col_1 = 10 AND col_2 = 100 AND col_3 = 500;
-- 走了索引
SELECT * FROM SomeTable WHERE col_1 = 10 AND col_2 = 100 ;
-- 没走索引
SELECT * FROM SomeTable WHERE col_1 = 10 AND col_3 = 500 ;
-- 没走索引
SELECT * FROM SomeTable WHERE col_2 = 100 AND col_3 = 500 ;
-- 走了索引
SELECT * FROM SomeTable WHERE col_2 = 100 AND col_1 = 10 ;

联合索引中的第一列(col_1)必须写在查询条件的开头,而且索引中列的顺序不能颠倒。



可能需要说明的是最后一条SQL为什么会走索引,简单转化一下,col_2 = 100 AND col_1 = 10,
这个条件就相当于col_1 = 10 AND col_2 = 100,自然就可以走联合索引。



2.6 使用LIKE查询


并不是用了like通配符,索引一定会失效,而是like查询是以%开头,才会导致索引失效。


-- 没走索引
SELECT * FROM SomeTable WHERE col_1 LIKE'%a';
-- 没走索引
SELECT * FROM SomeTable WHERE col_1 LIKE'%a%';
-- 走了索引
SELECT * FROM SomeTable WHERE col_1 LIKE'a%';

2.7 连接字段字符集编码不一致


如果两张表进行连接,关联字段编码不一致会导致关联字段上的索引失效,这是博主在线上经历一次SQL慢查询后的得到的结果,举例如下,有如下两表,它们的name字段都建有索引,但是编码不一致,user表的name字段编码是utf8mb4,user_job表的name字段编码是utf8,


CREATE TABLE `user` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(255) CHARACTER
SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
`age` int NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

CREATE TABLE `user_job` (
`id` int NOT NULL,
`userId` int NOT NULL,
`job` varchar(255) DEFAULT NULL,
`name` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

进行SQL查询如下:


EXPLAIN
SELECT *
from `user` u
join user_job j on u.name = j.name


由结果可知,user表查询走了索引,user_job表的查询没有走索引。想要user_job表也走索引,可以把user表的name字段编码改成utf8即可。


三、减少中间表


在SQL中,子查询的结果会被看成一张新表,这张新表与原始表一样,可以通过代码进行操作。这种高度的相似性使得SQL编程具有非常强的灵活性,但是如果不加限制地大量使用中间表,会导致查询性能下降。


频繁使用中间表会带来两个问题,一是展开数据需要耗费内存资源,二是原始表中的索引不容易使用到(特别是聚合时)。因此,尽量减少中间表的使用也是提升性能的一个重要方法。


3.1 使用HAVING子句


对聚合结果指定筛选条件时,使用HAVING子句是基本原则。不习惯使用HAVING子句的人可能会倾向于像下面这样先生成一张中间表,然后在WHERE子句中指定筛选条件。例如下面:


SELECT * 
FROM (
SELECT sale_date, MAX(quantity) max_qty
FROM SalesHistory
GR0UP BY sale_date
) tmp
WHERE max_qty >= 10

然而,对聚合结果指定筛选条件时不需要专门生成中间表,像下面这样使用HAVING子句就可以。


SELECT sale_date, MAX(quantity)
FROM SalesHistory
GR0UP BY sale_date
HAVING MAX(quantity) >= 10;

HAVING子句和聚合操作是同时执行的,所以比起生成中间表后再执行的WHERE子句,效率会更高一些,而且代码看起来也更简洁。


3.2 对多个字段使用IN


当我们需要对多个字段使用IN条件查询时,可以通过 || 操作将字段连接在一起变成一个字符串处理。


SELECT *
FROM Addresses1 A1
WHERE id || state || city
IN (SELECT id || state|| city
FROM Addresses2 A2);

这样一来,子查询不用考虑关联性,而且只执行一次就可以。


需要说明的MySql中,|| 操作符是代表或者也就是OR的意思。在Mysql中可以使用下面多种写法,如下:


-- 使用CONCAT(str1,str2,...)函数,将多列合并为一个字符串
SELECT *
FROM Addresses1 A1
WHERE CONCAT(id, state, city)
IN ('1湖北武汉', '2湖北黄冈');

-- 使用多列in查询
SELECT *
FROM Addresses1 A1
WHERE (id, state, city)
IN ((1, '湖北', '武汉'), (2, '湖北', '黄冈'));

使用多列in查询这个语法在实际执行中可以走索引,CONCAT(str1,str2,...) 函数不能。


3.3 先进行连接再进行聚合


连接和聚合同时使用时,先进行连接操作可以避免产生中间表。原因是,从集合运算的角度来看,连接做的是“乘法运算”。连接表双方是一对一、一对多的关系时,连接运算后数据的行数不会增加。而且,因为在很多设计中多对多的关系都可以分解成两个一对多的关系,因此这个技巧在大部分情况下都可以使用。


到此本文讲解完毕,感谢大家阅读,感兴趣的朋友可以点赞加关注,你的支持将是我更新动力😘。



作者:waynaqua
来源:juejin.cn/post/7221735480576245819
收起阅读 »

你的@Autowired被警告了吗

一个警告 近期组里来了新同学,依赖注入的时候习惯使用@Autowired,发现被idea黄色警告,跑过来问大家。由于我平时习惯使用@Resource,没太注意过这个问题,刚好趁着这个机会学习了一波。 首先看下问题的现象,使用@Autowired被idea警告,...
继续阅读 »

一个警告


近期组里来了新同学,依赖注入的时候习惯使用@Autowired,发现被idea黄色警告,跑过来问大家。由于我平时习惯使用@Resource,没太注意过这个问题,刚好趁着这个机会学习了一波。

首先看下问题的现象,使用@Autowired被idea警告,而使用@Resource则不会:


image.png


image.png


@Autowired和@Resource的差异


来源


  • @Resource是JSR 250中的内容,发布时间是2006年

  • @Autowired是Spring2.5中的内容,发布时间是2007年


@Resource是JSR的标准,Spring框架也提供了实现。既然@Autowired是在@Resource之后发布的,应该就有@Resource不能表达的含义或者不能实现的功能。


用法

注入方式

虽然我们开发中使用最多的方式是属性注入,但其实存在构造函数注意、set方法注入等方式。相比于@Resource支持的属性注入和set方法注入,@Autowired还能支持构造方法注入的形式,@Resource是不行的。


image.png


image.png


可指定属性

@Autowired支持required属性


image.png


@Resource支持7个其它属性


image.png


bean查找策略


  • @Autowired是类型优先,如果这个类型的bean有多个,再根据名称定位

  • @Resource是名称优先,如果不存在这个名称的bean,再去根据类型查找


查找过程


@Autowired

auto.png


@Resource

resource.png


思考


对于大多数开发同学来说,@Autowired 和 @Resource 差异是很小的,虽然 @Autowired 多支持构造器注入的形式,但是直接属性注入真的太灵活太香了。而@Autowired晚生于@Resource,既然已经有JSR标准的@Resource,还要增加1个特有Autowired,必然有Spring的考虑。

个人认为,构造器注入和支持属性不同这个理由是很弱的,这些特性完全可以在@Resource上实现,也不违反JSR的约束。比较可能的原因是,含义的不同,而最大的不同体现在bean查找策略上,@Autowired默认byType,@Resource默认byName,这个不同其实隐含了,@Resource注入的bean,更加有确定性,你都已经确定了这个bean的名称了,而类型在Java中编译过程本身是个强依赖,其实这里相当于指定了类型和名称,注入的是一个非常确定的资源。而@Autowired是类型优先,根据类型去查找,相比于@Resource,确定性更弱,我知道这里要注入bean的类型,但是我不确定这个bean的名称,也隐含体现了java多态的思想。


总结


回到开篇的idea的警告,网上有很多人都赞同的一种说法是,@Resource是JSR规范,@Autowired是Spring提供,不推荐使用绑定了Spring的@Autowired,因为@Resource在更换了框架后,依然可以使用。我不太赞同这种说法,因为idea的错误提示很明确,Field injection is not recommended,不推荐使用属性注入的方式,那换成@Resource,我理解并没有解决这个问题,虽然idea确实不警告了,可能有点掩耳盗铃的意思。

比较推荐的做法是,使用构造方法注入,虽然很多时候会有点麻烦,特别是增加依赖的时候,但是正是这种麻烦,会让你不再那么随意做属性注入,更能保持类的职责单一。然后配合lombok,也可以让你省去这种麻烦,不过希望还是能通过这个警告时刻提醒自己保持类的职责单一。


image.png


作者:podongfeng
来源:juejin.cn/post/7265926762729717819
收起阅读 »

什么?你现在还想用微信小程序做副业?

前言 相信各位程序猿/程序媛小朋友都有想创业赚外快的梦想,梦想哪一天自己能做出来个爆款产品,用户量蹭蹭的涨,真金白银哗哗的涌进来,挡都挡不住,从此当上总经理、出任CEO、赢取白富美、走上人生巅峰。或者退而求其次,产品虽然不那么火。但也可以让自己每天躺着也能挣钱...
继续阅读 »

前言


相信各位程序猿/程序媛小朋友都有想创业赚外快的梦想,梦想哪一天自己能做出来个爆款产品,用户量蹭蹭的涨,真金白银哗哗的涌进来,挡都挡不住,从此当上总经理、出任CEO、赢取白富美、走上人生巅峰。或者退而求其次,产品虽然不那么火。但也可以让自己每天躺着也能挣钱,副业的收入就足够日常开销,那个班随便上上就行了,还卷什么卷。


而做副业的各种途径中,做微信小程序无疑是门槛比较低的途径之一,微信国内几乎每个人都在用,那数量庞大的用户和流量无人能及,小程序天然跨平台,运行体验也比原生h5似乎流畅那么一点点。


最关键的是开发成本低啊,只要有一点点JS基础,分分钟就能搞出来个能跑的小程序,不会后端也没关系,有云开发,照着文档,基本的增删改查也是分分钟就能搞定。


注册一个账户很简单,做完的小程序上架也很简单。一切都很简单有木有。看到这里你是不是想立马手搓个小程序试试?


然而,时代已经变了,看似简单的小程序做起来已经不那么简单了。现在我已经不建议大家在做副业的时候选择微信小程序了。


回顾


在说明为什么不建议再做小程序之前,先来回顾一下个人做小程序之前为什么是可行的。


创业也好,做副业也好,首先得看大趋势,雷总不是有句名言嘛,“站在风口上,猪都能飞起来”,换句话说,赚钱要趁早,智能手机刚起步的时候去做app,微信刚推出小程序的时候你去做小程序,那赚钱的可能性都比较大,毕竟是蓝海,而现在,app,小程序啥的早就是一片红海了。有多么红海,你随便搜一下“小程序已死”就知道了,基本上有需求的地方都已经有小程序占好坑了。对个人做小程序来讲那时也是最好的时候,做成的概率也比较大。


当然,错过了风口也不是完全没机会,虽然小程序变红海已经好长时间了,但是架不住万一你突然发现了哪个小众需求,或者蹭到了哪个热点,比如前一段火的不行的短视频去水印,虽然靠这个没法财富自由,但赚个零花钱还是可以的。


做任何事情都是开始最困难,不是有句老话叫“万事开头难”么。而小程序曾经就可以把这个开头的难度降低很多,除了上面讲的开发难度低,另一个就是试错的成本也低。如果一个小程序做不起来可以迅速再搞一个其他小程序试试,甚至可以弄出来一堆小程序,哪个能活下来就重点运营,其他不行的可以放弃后再搞新的,基本上除了开发没有其他的成本。


最重要的一个点就是,虽然平台对个人主体仅开放了有限的类目,理论上讲,个人能做的小程序基本上被限制在工具类里面,比如都被做烂了的记账,备忘录啥的。我们都知道小程序的分享能力是非常重要的,可以分享才能裂变出新用户,新流量。


虽然原则上你的小程序是不能把用户创作的内容分享出去的,一旦涉及到分享,就被归为社交类目,而这个类目是不对个人开放的,举个例子,你搞了个制作头像的小程序,那用户制作好的头像是不能发给别人的,只能保存在本地。


但是呢,之前微信小程序平台对这些个规则执行的并不是十分严格。这就存在一些可操作的空间,让你上线一些只有公司才可以做的小程序。


所以,这里就有了一个可以低成本试错的路径,你可以先利用这个模糊空间,拍脑门也好,蹭热点也罢,先以个人名义上线小程序,如果不理想,就直接弃用或者换名字改成其他类型的小程序再试,扔掉的成本基本上就只是开发成本。如果不小心成了爆款,那恭喜你,可以去成立个公司啥的,再把小程序迁移过来,然后成为CEO,赢取白富美,走上人生巅峰。。。


dead.jpg
不幸的是,现在,这条路已经被堵死了


变化


为啥说路被堵死了,因为最近发生了一些变化,让个人开发者的试错成本直线上升,甚至可以说根本无路可走了。


创业或者搞副业无非就是要赚钱,收入减去成本就是赚到的钱。那就从成本,收入两个角度来分析一下发生了什么导致现在搞小程序很难赚到钱。


成本


先看成本,成本飙升到能把个人开发者的路都堵死,还得从微信这一年的一系列操作说起。这波操作总结起来就是一个字:“得加钱”


ac608998885aa3fee54ec48980361533.jpg


云开发,个人开发者用来快速整个后端,一年多前起步免费,现在起步价:优惠19.9/月,原价:39.9/月,起步价里面包含的资源也就够你开发调试用吧,上线以后超出部分另算。


个人小程序的自然流量基本上都依靠搜索,在We分析里可以看到用户通过搜索哪些关键词访问到小程序,开发者可以相应的做搜索词优化来引流。自12月18日起,这项功能也开始收费了,起步价388/年,这个起步价啥概念?你的小程序过去一段时间日打开次数得小于100次。。。


以上都是平a,下面这个才是大招,“必杀技:备案+认证无敌真伤斩杀组合拳”


先说认证,认证这事之前仅对企业等非个人主体开放的,300一个小程序,不管认证能不能通过,如果通过了就是永久有效。个人小程序无需认证。


然而,从下半年开始,个人小程序也需要认证了,目前优惠价30每个小程序,而且认证有效期只有一年了。不认证也可以,那你的小程序就别想被搜索到,也别想往外分享了。


你说你去认证吧,就会遇到另外一个问题,认证的时候要审核你的小程序的名字。做过小程序的都知道,名字可太重要了。直接决定了你的小程序能不能被搜索到。所以之前大家起名几乎都是在直接对标搜索热门词,什么“群签到助手”,什么“去水印神器”之类的。


然而现在在认证的时候这些名字统统不合格,名字不能太宽泛,必须含有个人特征。例如,张三做了个“记事本”小程序去认证,那名字几乎得叫“张三的记事本”之类才好通过。想蹭流量?门都没有。


再说下一个,备案。从9月4号开始,所有新小程序必须备案后才能上线,已上线的小程序在明年4月之前必须完成备案,否则就下架。


平台会对要备案的小程序做初审,这个初审就相当严格了。把个人小程序直接按死在那有限的几个类目里。基本上现在个人只能做记事本三件套了。前面说的模糊空间已经不存在了,路也就堵死了。


你要说能不能先通过备案,然后在新版本里添上新功能?不好意思,现在对版本的审核也很严格。以前几个小时就能审好,现在几天都是正常的,有超过资质范围的功能直接打回。


备案也会对小程序名字做审核,你要问啥样的名字能过审?别问我,我也不知道。


由于认证和备案不是同一拨人在搞,所以如果你的小程序不太幸运的话会遇到以下问题:


30认证 --> 认证不通过 --> 改名+30认证 --> 认证通过 --> 备案不通过 --> 改名+30认证。。。


就说为了个小程序,你能不能经得住这样的折腾吧。


至于看不懂的文档,日常的违规警告,无法找到的人工客服,随便废弃接口,隐私协议闹剧(具体可看《各位微信小程序的开发者们,你们还好吗?》)等等大家都早已经麻了。


成本讲完了,下面说说收入。


收入


什么?你还想着有收入?收入就是没有收入!(全文完)


------------------------- 分 ----- 割 ----- 线 ------------------------


说没有收入,是玩笑话,也不是玩笑话。小程序除非你另有门路,大部分个人开发者就只能靠当流量主接入平台的广告赚钱。然而,现在广告的ecpm虽然不能说聊胜于无,也可以说是惨不忍睹。并且这下降的趋势似乎还看不到头,简直和股市一个尿性。


就那点广告费收入,再减去上面说的成本,能不亏钱就不错了,至于收益,这么说吧,
去各大平台薅羊毛,每天在某东签个到,在某宝果园里种个树,农场里养个鸡,或者在某音某手放一个你好基友啪叽在冰面上摔个狗啃泥的短视频,获得的收益都可能比苦哈哈做小程序的收益多。


某多多除外,你永远都别想薅到某多多的羊毛。


总结


个人做小程序的成本和收益都写在上面了,干货满满。各位想着做副业的小朋友看完之后不妨自己先算算账,然后再决定要不要做小程序。另外大家对此还有什么想吐槽的,欢迎在讨论区一起聊聊。


作者:ad6623
来源:juejin.cn/post/7317104726768697398
收起阅读 »

降本增笑,领导要求程序员半年做出一个金蝶

关于降本增效的事,今天在网上看到一个帖子,虽然有点搞笑,但是对于打工人(特别是技术同学)来说,可挖掘的东西也不少,特别分享给大家。 真的是好多年没听说这样的事了,记得以前总有老板让做个淘宝、做个京东,然后预算只有几千块,最多几万块。 这些故事程序员谈起来往往...
继续阅读 »

关于降本增效的事,今天在网上看到一个帖子,虽然有点搞笑,但是对于打工人(特别是技术同学)来说,可挖掘的东西也不少,特别分享给大家。



真的是好多年没听说这样的事了,记得以前总有老板让做个淘宝、做个京东,然后预算只有几千块,最多几万块。


这些故事程序员谈起来往往都是哈哈一笑,并疯狂吐槽一番。



不过笑过之后,大家是否想过如何去解决问题?或者真的去评估下可行性,探索一下可能的实现路径。


找到问题


首先我们看下老板的问题。老板的根本问题并不是想要做金蝶,为什么这么说呢?


我们看看网友的描述就知道了:经济下行,领导不想出金蝶系统的维护费,不想为新功能花大价钱。这才是根本问题,用四个字来说就是:降低成本。


然后才是老板想到能不能用更少的钱达到金蝶系统的使用效果,再之后才是自己能不能做一个类似金蝶的系统,并思考了自己可以承担的成本:一个前端、一个后端,半年时间。


最后问题被抛到了这位网友的手里。可以看得出来这位网友也不太懂,还去咨询了朋友。不知道它有没有向朋友说清楚问题,还是只说了老板想自己做一个金蝶系统,结果是朋友们都说不可行。


遇到问题时,我们得把这个问题完完整整的捋一遍,找到最根本的问题,然后再说怎么解决问题,否则只是停留在表面,就可能事倍功半。在这个上下文中,根本的问题就是:降低成本。



解决问题


明确了老板的根本问题,我们就可以琢磨方案了。既然是降低财务系统的成本,可行的方案应该还是有几个的。


使用替代产品


假如公司只使用产品的部分功能,是不是可以选择金蝶的低版本?是不是可以降低一些人头费?


金蝶的服务贵,是不是可以选择一些小厂的产品?国内做财务系统的应该挺多的,小厂也更容易妥协。


或者选择SaaS服务,虽然SaaS用久了成本也不低,但是可以先撑过这几年,降低当前的支出。


当然替换财务系统也是有成本的,需要仔细评估。不过既然都想自己做了,这个成本应该能hold住。


找第三方维护


金蝶的服务贵,是不是可以找其它三方或者个人来维护修改?根据我之前的了解,金蝶这种公司有很多的实施工作是外包出去的,或者通过代理商来为客户服务,能不能找这些服务商来代替金蝶呢?或者去某些副业平台上应该也能找到熟悉金蝶系统的人。


当然这个还要看系统能不能顺利交接,金蝶有没有什么软硬件限制,第三方能不能接过来。


另外最重要的必须考虑系统和数据的安全性,不能因小失大。


自己开发


虽然自己开发的困难和成本都很高,但我仍旧认为这可能也是一个合适的解决方案。理由有下面两点。



  • 功能简单:如果公司的业务比较简单,使用的流程也简单,比如不使用涉及复杂的财务处理,那么捋一捋,能给开发人员讲清楚,也是有可能在短时间内完成的。

  • 迭代渐进:长城不是一天建成的,系统也都是逐渐迭代完善的。自己开发可以先从部分模块或者功能开始,然后逐步替换,比如前边的流程先在新系统中做,最后再导入金蝶。即使不能做到逐步替换,也可以控制系统的风险,发现搞不定时,及时止损。相信老板也能明白这个道理,如果不明白或者不接受,那确实搞不了。



当然我们也肯定不能忽视这其中的困难。我之前做过和金蝶系统的对接,订单的收付款在业务系统完成,然后业务系统生成凭证导入到金蝶K3。依稀记得业务也不算复杂,但是需求分析做了好几遍,我的代码也是改了又改,上线之后遇到各种问题,继续改,最终花了几个月才稳定下来。


事后分析原因,大概有这么几点:



  • 产品或者需求分析人员没接触过类似的业务,即使他对财务系统有一些经验,也不能准确的将客户的业务处理方式转换到产品设计中;

  • 财务人员说不明白,虽然他会使用金蝶系统,但是他不能一次性的把所有规则都讲出来,讲出来也很难让程序员在短时间内理解;

  • 程序员没做过财务系统,没接触过类似的业务,系统的设计可能要反复调整,比如业务模块的划分逻辑,金额用Long还是用BigDecimal,数据保留几位小数,这都会大幅延长开发周期,如果不及时调整就可能写成一锅粥,后期维护更困难。


这还只是和金蝶系统做一个简单的对接,如果要替代它,还要实现更多的功能,总结下,企业可能会面对下面这些困难:


业务复杂:财务规则一般都比较复杂,涉及到各种运算,各种数字、报表能把人搞晕。如果公司的业务也很复杂,比如有很多分支或者特殊情况,软件开发的难度也会很大,这种难度的变化不是线性增加的,很可能是指数级增长的,一个工作流的设计可能就把人搞死了。


懂业务的人:系统过于复杂时,可能没有一个人能把系统前前后后、左左右右的整明白。而要完成这样一个复杂的系统,必须有人能从高层次的抽象,到具体数字运算的细枝末节,完完全全的整理出来,逻辑自洽,不重不漏,并形成文档,还要能把程序员讲明白。


懂架构的人:这里说的是要有一个经验丰富的程序员,不能是普通的码农,最好是有财务系统开发经验的架构师。没走过的路,总是要踩坑的。有经验的开发人员可以少走很多弯路,极大降低系统的风险。这样的人才如果公司没有,外招的难度比较大,即使能找到,成本也不低。


灵活性问题:开发固定业务流程的系统一般不会太考虑灵活性的问题,如果业务需要调整,可能需要对系统进行大幅修改,甚至推倒重来。如果要让系统灵活些,必然对业务和技术人员都提出了更高的要求,也代表着更强的工作能力和更多的工作量。


和其它系统的对接:要不要和税务系统对接?要不要和客户管理系统对接?要不要和公司的OA对接?每一次对接都要反复调试,工作量肯定下不来。


总之,稍微涉及到财务处理的系统,都不是一个前端和一个后端能在短时间内完全搞出来的。


对程序开发的启示


搞清楚需求


日常开发过程中,大家应该都遇到过不少此类问题。领导说这里要加个功能,然后产品和开发就去吭哧吭哧做了,做完了给领导一看,不是想要的,然后返工反复修改。或者说用户提了一个需求,产品感觉自己懂了,然后就让开发这么做那样改,最后给用户一看,什么破玩意。这都是没有搞清楚真正的需求,没有触达那个根本问题。


虽然开发人员可以把这些问题全部甩给产品,自己只管实现,但这毕竟实实在在的消耗了程序员的时间,大量的时间成本和机会成本,去干点有意义的事情不好吗?所以为了不浪费时间,开发也要完整的了解用户需求。在一个团队中,至少影响产品落地的关键开发人员要搞懂用户的需求。


那么遇到这种问题,程序员是不是可以直接跑路呢?


也是一个选择, 不过对于一个有追求的程序员,肯定也是想把程序设计好、架构好的,能解决实际问题的,这也需要对用户需求的良好把控能力,比如我们要识别出哪些是系统的核心模块,哪些是可扩展能力,就像设计冯诺依曼计算机,你设计的时候会怎么处理CPU和输入输出设备之间的关系呢?


对于用户需求,产品想的是怎么从流程设计上去解决,开发需要考虑的是怎么从技术实现上去满足,两者相向而行,才能把系统做好。


当然准确把握用户的需求,很多时候并不是我说的这么容易,因为用户可能也说不清楚,我们可能需要不断的追问才能得到一些关键信息。比如这位网友去咨询朋友时,可能需求就变成了:我们要做一个财务系统,朋友如果不多问,也只能拿到这个需求,说不定这位朋友也有二次开发的能力,错失了一次挣钱的好机会。还有这位老板上边可能还有更大的老板,这位老板降低成本的需求也可能是想在大老板面前表现一下,那是不是还有其它降本增效的方法呢?比如简化流程、裁掉几个不关键的岗位(这个要得罪人了)。


我们要让程序始终保持在良好的状态,就要准确的把握用户需求,要搞懂用户需求,就需要保持谦逊求知的心态,充分理解用户的问题,这种能力不是朝夕之间就可以掌握的,是需要修炼的。


动起来


任何没有被满足的需求都是一次机会。


我经常会在技术社区看到一些同学分享自己业余时间做的独立产品,有做进销存的、客户管理的、在线客服的,还有解决问题的各种小工具,而且有的同学还挣到了钱。


我并不是想说让大家都去搞钱,而是说要善于发现问题、找到机会,然后动起来、去实践,实践的过程中我们可以发现更多的问题,然后持续解决问题,必然能让自己变得越来越强。在经济不太好的情况下,我们才有更强的生存能力。




啰里八嗦一大堆,希望能对你有所启发。


关注萤火架构,加速技术提升。


作者:萤火架构
来源:juejin.cn/post/7317704464999235593
收起阅读 »

2023市场需求最大的8种编程语言出炉!

众所周知,编程语言的种类实在是太多了。直到现在,经常还会看到关于编程语言选择和学习的讨论。 虽说编程语言有好几百种,但实际项目使用和就业要求的主流编程语言却没有那么多。 大家可能也会好奇:现如今就业市场上到底什么编程语言最受欢迎?或者说最需要的编程语言是什么?...
继续阅读 »

众所周知,编程语言的种类实在是太多了。直到现在,经常还会看到关于编程语言选择和学习的讨论。


虽说编程语言有好几百种,但实际项目使用和就业要求的主流编程语言却没有那么多。


大家可能也会好奇:现如今就业市场上到底什么编程语言最受欢迎?或者说最需要的编程语言是什么?


所以今天我们就结合Devjobsscanner之前发布的「Top 8 Most Demanded Programming Languages in 2023」编程语言清单来聊一聊这个问题。



虽说这个清单并不是完全针对我们本土开发者的调查,但还是能反映一些趋势的,大家顺带也可以参看一下各种编程语言的发展趋势和前景。


Devjobsscanner是一个综合性开发者求职/岗位信息聚合网站。



上面聚合展示了很多开发者求职岗位信息,并按多种维度进行分类,以便用户进行搜索。



该网站每年都会发布一些相关方面的调查总结报告,以反映开发者求职方面的趋势。


从2022年1月到2023年5月,这17个月的时间里,Devjobsscanner分析了超过1400万个开发岗位,并从中进行筛选和汇编,并总结出了一份「2023年需求量最大的8种编程语言」榜单。


所以下面我们就来一起看一看。


No.1 JavaScript/TypeScript


基本和大家所预想到的一样,Javascript今年继续蝉联,成为目前需求最大的编程语言。



当然这也不难理解,因为基本上有Web、有前端、有浏览器、有客户端的地方都有JavaScript的身影。


而且近几年TypeScript的流行程度和需求量都在大增,很多新的前端框架或者Web框架都是基于TypeScript编写的。


所以学习JavaScript/TypeScript作为自己的主语言是完全没有问题的。



  • 职位数量/占比变化趋势图



No.2 Python


榜单上排名第二的是Python编程语言。



众所周知,Python的应用范围非常广泛。


从后端开发到网络爬虫,从自动化运维到数据分析,另外最近这些年人工智能领域也持续爆火,而这恰恰也正是Python活跃和擅长的领域。


尤其最近几年,Python强势上扬,这主要和这几年的数据分析和挖掘、人工智能、机器学习等领域的繁荣有着不小的关系。



  • 职位数量/占比变化趋势图



No.3 Java


榜单中位于第三需求量的编程语言则是Java。



自1995年5月Java编程语言诞生以来,Java语言的流行程度和使用频率就一直居高不下,并且在就业市场上的“出镜率”很高。


所以每次调查结果出来,Java基本都榜上有名,而且基本长年都维持在前三。


Java可以说是构成当下互联网繁荣生态的重要功臣,无数的Web后端、互联网服务、移动端开发都是Java语言的领地。



  • 职位数量/占比变化趋势图



No.4 C#



看到C#在榜单上位列前四的那会,说实话还是挺意外的,毕竟自己周围的同学和同事做C#这块相对来说还是较少的。


但是C#作为一种通用、多范式、面向对象的编程语言,在很多领域其实应用得还是非常广泛的。


我们都知道,其实像.NET和Unity等框架在不少公司里都很流行的,而C#则会被大量用于像Unity等框架的项目编写。



  • 职位数量/占比变化趋势图



No.5 PHP



看到PHP在榜单上位列第五的时候,不禁令人又想起了那句梗:


不愧是最好的编程语言(手动doge)。


所以以后可不能再黑PHP了,看到没,这职位数量和占比还是非常高的。



  • 职位数量/占比变化趋势图



No.6 C/C++


C语言和C++可以说都是久经考验的编程语言了。



C语言于1972年诞生于贝尔实验室,距今已经有50多年了。


自诞生之日起,C语言就凭借其灵活性、细粒度和高性能等特性获得了无可替代的位置,而且随着如今的万物互联的物联网(IoT)时代的兴起,C语言地位依然很稳。


C语言和C++的应用领域都非常广泛,在一些涉及嵌入式、物联网、操作系统、以及各种和底层打交道的场景下都有着不可或缺的存在意义。



  • 职位数量/占比变化趋势图



No.7 Ruby


Ruby这门编程语言平时的出镜率虽然不像Java、Python那样高,但其实Ruby的应用领域还是挺广的,在包括Web开发、移动和桌面应用开发、自动化脚本、游戏开发等领域都有着广泛的应用。



Ruby在20世纪90年代初首发,并在2000年代初开始变得流行。


Ruby是一种动态且面向对象的编程语言,语法简单易学,使用也比较灵活,因此也吸引了一大批爱好者。



  • 职位数量/占比变化趋势图



No.8 GO


虽说Go语言是一个非常年轻的编程语言(由谷歌于2009年对外发布),不过Go语言最近这几年来的流行程度还是在肉眼可见地增加,国内外不少大厂都在投入使用。



众所周知,Go语言在编译、并发、性能、效率、易用性等方面都有着不错的表现,也因此吸引了一大批学习者和使用者。



  • 职位数量/占比变化趋势图



完整表单


最后我们再来全局看一看Devjobsscanner给出的编程语言完整表单和职位数量/占比的趋势图。




不难看出,JavaScript、Python和Java这三门语言在就业市场上的需求量和受欢迎程度都很大,另外像C语言、C#、Go语言的市场岗位需求也非常稳定。


总体来说,选择清单里的这些编程语言来作为自己的就业主语言进行学习和精进都是没有问题的。


说到底,编程语言没有所谓的好坏优劣,而最终选择什么,还是得看自己的学习兴趣以及使用的场景和需求。


作者:CodeSheep
来源:juejin.cn/post/7316968265057828874
收起阅读 »

防御性编程?这不就来了

最近程序员界又重新流行起来了防御性编程这个概念,早期嘞,这个概念代表是一种细致、谨慎的编程方法。 防御性编程的目的是为了开发可靠的软件,我们在设计系统中每个组件的时候,都需要使其尽可能的 "保护" 自己。 但是 2023 年以来,国内的互联网市场是什么行情,相...
继续阅读 »

最近程序员界又重新流行起来了防御性编程这个概念,早期嘞,这个概念代表是一种细致、谨慎的编程方法。


防御性编程的目的是为了开发可靠的软件,我们在设计系统中每个组件的时候,都需要使其尽可能的 "保护" 自己。


但是 2023 年以来,国内的互联网市场是什么行情,相信大家都清楚,整个市场环境都在强调降本增效、开猿节流。


因此为了体现程序员们在公司代码中的不可替代性?防止被裁。"防御性编程" 概念又重新流行了起来。


不过这次它可不再是保护程序了,而是保护广大程序员群体 😎。



所以我就给大家介绍一下,新时代背景下的 "防御性" 编程理念,如何实践 😜。


本文大纲如下,



代码书写


变量名称使用单一字符


Java 语言里变量名只能由 Unicode 字母、数字、下划线或美元符号组成,并且第一个字符不能是数字


那么对于单一字符的变量名称来说,26 个字母大写加 26 个字母小写加下划线以及美元符一共有 54 种变量名称,想一想难道这些还不够你在单个 Java 文件里给变量命名用吗?


兄弟这一般够用了。


使用中文命名


兄弟,大家都是中国人,肯定看得懂中文咯。



就问你,Idea 支不支持吧,有没有提示说你变量名不规范嘛!没提示就是规范。



还有一点,兄弟们,还记得上面 Java 语言里变量名组成规范吗?中文也在 Unicode 编码里面,所以其实我们还可以用中文作为变量名称。


我已经帮你查好了,Java 里常用的 utf-8 编码下,支持的中文字符有 20902 个,所以上面单一字符的变量名称还需要新增 20902 种 😃,简直完美。



使用多国语言命名



不多说,我就问你看不看得懂吧,看得懂算你厉害,看不懂算你技术不行。



你问我看不看得懂,我当然看的懂,我写的,我请百度翻译的 😝。





这些变量名称命名法则,不仅适用与 Java,也适用于 JavaScript,广大前端程序员也有福了。


CV 大法


不要抽象、不要封装、不要继承、不要组合,我只会 CV。


抽象



抽象:我可以让调用者只需要关心方法提供了哪些功能,而不需要知道这些功能是如何实现的。我的好处是可以减少信息的复杂度,提高代码的可读性和易用性,也方便了代码的修改和扩展,我厉害吧。


我:我只会 CV。


抽象:...



封装



封装:我可以把数据和基于数据的操作封装在一起,使其构成一个独立的实体,对外只暴露有限的访问接口,保护内部的数据不被外部随意访问和修改。我的好处是可以增强数据的安全性和一致性,减少代码的耦合性,也提高了类的易用性。看见没,我比抽象好懂吧。


我:我只会 CV。


封装:...



继承



继承:我可以让一个类继承另一个类的属性和方法,从而实现代码的复用和扩展。我可以表示类之间的 is-a 关系,体现了类的层次结构和分类。我的好处是可以避免代码的重复,简化类的定义,也增加了代码的维护性。我可是面向对象三大特征之一。


我:我只会 CV。


继承:...



组合



组合:我可以让一个类包含另一个类的对象作为自己的属性,从而实现代码的复用和扩展。我可以表示类之间的 has-a 关系,体现了类的关联和聚合。我的好处是可以增加类的灵活性和可变性,也降低了类之间的耦合性。不要用继承,我可是比继承更优秀的。


我:我只会 CV。


组合:...



不要问为什么我只会 CV,因为我的键盘只有 CV。



刚出道时我们嘲讽 CV,后来逐渐理解 CV,最后我们成为 CV。


CV 的越多,代码就越复杂,代码越复杂,同事就越难看懂,同事越难看懂,就越难接手你的代码,你的不可替代性就越来越强。


那么我们防御性编程的目的不久达到了嘛。


兄弟,听我说,给你的代码上防御,是为了你好!



产品开发


运营配置、开发配置、系统配置直接写死,用魔法值,没毛病。


产品每次提需求,代码实现一定要做到最小细粒度实现,做到需求里少一个字,我的代码里绝不会多一个词,注释也是不可能有的,我写的代码只有我看得懂不是防御性编程的基操吗?


我的代码我做主。


产品原型不提,我绝对不会问。要做到这系统有你才能每一次发版上线都是相安无事,一旦缺少了你,鬼知道会发生什么。


我们能做的就是牢牢把握项目中核心成员的位置。这个项目组少了你,绝对不行!



最后聊两句


2023 全年都在降本增效,节能开猿的浪潮下度过。


虽然本文是给大家讲防御性编程如何实践,但终究只是博君一笑,请勿当真。


这里我还是希望每一个互联网打工人都能平稳度过这波寒冬。


积蓄力量,多思考,多元发展。


在来年,春暖花开,金三银四之月,都能找到自己满意的工作,得到属于自己的果实。



关注公众号【waynblog】,每周分享技术干货、开源项目、实战经验、高效开发工具等,您的关注将是我的更新动力😘。



作者:waynaqua
来源:juejin.cn/post/7312376672665075722
收起阅读 »

2023年度总结 | 迷茫

一年一年又一年 一岁一岁又一岁 岁岁愁   都说35是程序猿的一道坎,30的我也开始慌,开始愁了,都说应该要有个副业,可又不知道做什么,要是以后失业了,也不知道能转行做啥,好愁~有没大佬带飞  (இωஇ)没钱 失业寻找工作   22年底公司项目砍了然后就是被...
继续阅读 »

一年一年又一年

一岁一岁又一岁

岁岁愁



  都说35是程序猿的一道坎,30的我也开始慌,开始愁了,都说应该要有个副业,可又不知道做什么,要是以后失业了,也不知道能转行做啥,好愁~有没大佬带飞  (இωஇ)没钱


失业寻找工作


  22年底公司项目砍了然后就是被离职了,当时疫情还没彻底解封,刚刚找了2个星期的工作,所在的小区就被封了。我是住在广州海珠区康乐村隔壁小区的,当时康乐村可谓闹得人人皆知,疫情的严重带,导致小区被封了1个月。之后在刚解封的时候,我又立刻中招了,发烧几天,咳嗽了也差不多一个月,期间也没怎么去找过工作,也接近过年了。

  虽然没了工作,但还是要开开心心的过个年的啦,工作什么的,年后再考虑了。

  过完年之后,疫情基本也全面放开了,不影响找工作了,就开始了每天疯狂的投投投,拉勾上看到一个符合的就投一个,保持在每天至少面一次的频率。奈何技术相对较差,又是工作了7年的老猿,面的都是小厂,但面试上还是有点不堪。面了两个星期,最终拿到两个offer,开出的条件几乎一样,都是单双休。虽然条件还是不太满意,但迫于已经几个月没工作了,每个月还有房贷在,还是有点压力的,就选择了各方面相对好一丢丢的一家,也就是目前在职的这一家。


入职搬砖


  二月底进行入职,入职之后先是熟悉环境,自己配置下电脑的开发环境,一天就过去了。第二天,就开始分配任务了,了解到我所在的项目是刚开的新项目,Android端一直没招到人开发,但服务端和iOS端已经是22年底就开始开发了的,已经领先了3个月。刚开始听到这个,感觉还是有压力的,相当于我需要赶进度去追这3个月的落差。所幸在我开始开发的时候,产品经理也跟我说了,按正常速度开发就行,不需要去加班加点赶。嗯,那还好,不然这3个月的差距,至少也得加班1-2个月才能赶上。


开始搬砖


  刚开始,还是有点懵的,产品经理说项目文档是别人出的,他也是上年底入职接手的,所以很多文档没看到,也可能是没出,就让我直接对着设计稿和iOS的做就可以了。之后我就拿了iOS的过来看,和设计稿进行了一次对比,又发现iOS和设计稿的有很多差别,iOS就跟我说他是最新的,有些细节和UI沟通过后,他没有进行修改设计稿,而是直接让iOS修改,所以让我直接照着iOS弄就可以了。emmmm....这给后面埋了个巨坑。

  由于Android这边有java和kotlin两种语言,于是问了下主管,主管让我用java去开发,也许考虑kotlin还不完善,怕项目出什么bug吧。拿着设计稿和iOS的app,就开始了疯狂敲代码模式。kotlin我是刚学了几个月,还不是太熟,但java可是用了6年多的了,那是嘎嘎香,拿着之前封装好的框架就是一顿套,什么网络请求框架、MVVM、图片加载Glide、BaseActivity等,套完之后,项目的雏形就完成了。不得不说,选择java也让我能直接使用已经封装的框架,要是kotlin,那就只能先慢慢搭建一套了,毕竟之前没怎么用过kotlin实际开发过项目,所以没什么存货。

  项目开始时,也了解到目前iOS端只是搭建了界面,服务端接口还未提供,数据模型等还没出,大概时服务端也是刚加入项目不久吧。所以iOS这期间基本都是对界面的修修补补,直到服务端出接口文档才能继续下一步,这刚好也能给我点追赶的时间。

  经过一顿嘎嘎猛敲,时间来到了4月中,期间服务端接口也出了,iOS也对接了,接口基本是没问题了,1个半月左右,我的界面也基本搭完了,就开始对接接口。刚开始还是有点难点,得按照服务端制定的规则进行加解密请求数据,编写的时候经常因为顺序和服务端的对不上导致加解密后数据是错的,调了整整2天才把加解密的调通。调通之后,其它也没啥了,正常的对接数据模型、对接接口、测试数据是否正确啥的。


搬砖完毕,进行检验


  5月初自测没问题之后进行提测了,iOS期间因为数据模型和他原本使用的模型对不上,说是改动很大,所以花了很多时间,最终和我同时提测的。

  重点来了,提测之后,测试部反馈了200+的bug数量...我当时就震惊了,我工作7年了,就没遇到能有200+bug的,这是什么神测试?结果...一看提的测试问题,一半以上是界面问题。emmmm...巨坑出来了,测试说和设计稿没对上,我说我是照着iOS做的,iOS说他是最新的,和UI对过没问题的。嗯,我以为是真的没问题,结果,UI设计师说他没说iOS那个是最新的,那些iOS虽然和他讨论过,但是他没确认可以,是iOS自己修改的。emmmmm...什么神操作,只是讨论过,没有设计师的确认,就自己修改了,还跟我说是最新的,和UI设计师确认过了的。emmmmm...能怎么办,改呗!!

  改了半个月,终于全部没问题可以上线了,真是艰难啊!!7年来,第一次遇到这种操作,真是糟心了!!期间iOS的其它问题,也不拿出来说了,评论别人不太合适。


收楼装修走起


  转眼来到6月份啦,房子终于建好收楼了,期间还是有点担心会烂尾的,毕竟恒大那么大的盘,说烂尾就烂尾,说破产就破产,要是变成烂尾楼,那简直就是天塌了,白花花的银子就全没了。

  收楼后就要开始搞装修的事啦~原本没打算今年装修的,还想再存点钱,毕竟装修要花一大笔,但父母说什么今年双春,明年没春,所以要今年入伙,就只有火急火燎的去看看装修了。考虑单双休,没时间去看,所以就找了装修全包的公司。找了1个多月,对比了5、6家公司,最终确定了一家,也差不多到10月初了,才进行签合同。期间又要物业审核什么的,拖了一个多月,11月中才开始装修。


第二个版本


  中间基本都是优化迭代,后面第二个大版本,从8月中开始,这次终于没有坑了,可以对着设计稿、交互文档来正常开发了。一开始主管要我们给个版本开发时间,因为内容有点多,加上有几个界面需要进行自定义的,我就预计了接近2个月。iOS看了就吐槽我说这么快,哪里弄的完,界面那么复杂...我也没法说啥,毕竟我预估的,已经很充足的了,再长就真的有点说不过去了。后面开发下来,也确实是差不多的,10月初就提测了,还不到两个月...然而...iOS到12月中才进行提测,这让我有点震惊。期间我都迭代了1个版本了,只能说主管貌似对进度不太关心。


学习鸿蒙


  由于iOS进度相对慢的有点多,所以我这边也自然多出了很多时间,刚好今年华为搞事情了,鸿蒙后续版本将不会再适配Android的apk了,需要进行单独开发,我刚好可以利用这段空闲的时间学学HarmonyOS,虽然感觉可以再缓缓,不用那么快学习,毕竟刚出,问题肯定相对较多,还需要一段时间让HarmonyOS的api完善完善,bug再修改修改。因为22年中下旬离职前,花了2、3个月的时间学习过JetpactCompose,所以学习HarmonyOS的ArkUI来说,相对轻松点,基本和JC有很多一样的用法。目前ArkUI也能做个简单的APP了,但还没开始看自定义视图,后面再学学,视图方面就基本可以了,主要还是各种优化和ArkTS的Api熟悉。


总结


  没想到一晃眼就一年过去了呢,今年除了一开始找工作、收楼找装修外,也没啥大事发生了,埋头敲敲代码就年底了,希望明年会越来越好!!


作者:ShrimpF
来源:juejin.cn/post/7317325043541131274
收起阅读 »

使用双异步后,从 191s 优化到 2s

大家好,我是哪吒。 在开发中,我们经常会遇到这样的需求,将Excel的数据导入数据库中。 一、一般我会这样做: 通过POI读取需要导入的Excel; 以文件名为表名、列头为列名、并将数据拼接成sql; 通过JDBC或mybatis插入数据库; 操作起来,...
继续阅读 »

大家好,我是哪吒。


在开发中,我们经常会遇到这样的需求,将Excel的数据导入数据库中。


一、一般我会这样做:



  1. 通过POI读取需要导入的Excel;

  2. 以文件名为表名、列头为列名、并将数据拼接成sql;

  3. 通过JDBC或mybatis插入数据库;



操作起来,如果文件比较多,数据量都很大的时候,会非常慢。


访问之后,感觉没什么反应,实际上已经在读取 + 入库了,只是比较慢而已。


读取一个10万行的Excel,居然用了191s,我还以为它卡死了呢!


private void readXls(String filePath, String filename) throws Exception {
@SuppressWarnings("resource")
XSSFWorkbook xssfWorkbook = new XSSFWorkbook(new FileInputStream(filePath));
// 读取第一个工作表
XSSFSheet sheet = xssfWorkbook.getSheetAt(0);
// 总行数
int maxRow = sheet.getLastRowNum();

StringBuilder insertBuilder = new StringBuilder();

insertBuilder.append("insert int0 ").append(filename).append(" ( UUID,");

XSSFRow row = sheet.getRow(0);
for (int i = 0; i < row.getPhysicalNumberOfCells(); i++) {
insertBuilder.append(row.getCell(i)).append(",");
}

insertBuilder.deleteCharAt(insertBuilder.length() - 1);
insertBuilder.append(" ) values ( ");

StringBuilder stringBuilder = new StringBuilder();
for (int i = 1; i <= maxRow; i++) {
XSSFRow xssfRow = sheet.getRow(i);
String id = "";
String name = "";
for (int j = 0; j < row.getPhysicalNumberOfCells(); j++) {
if (j == 0) {
id = xssfRow.getCell(j) + "";
} else if (j == 1) {
name = xssfRow.getCell(j) + "";
}
}

boolean flag = isExisted(id, name);
if (!flag) {
stringBuilder.append(insertBuilder);
stringBuilder.append('\'').append(uuid()).append('\'').append(",");
for (int j = 0; j < row.getPhysicalNumberOfCells(); j++) {
stringBuilder.append('\'').append(value).append('\'').append(",");
}
stringBuilder.deleteCharAt(stringBuilder.length() - 1);
stringBuilder.append(" )").append("\n");
}
}

List<String> collect = Arrays.stream(stringBuilder.toString().split("\n")).collect(Collectors.toList());
int sum = JdbcUtil.executeDML(collect);
}

private static boolean isExisted(String id, String name) {
String sql = "select count(1) as num from " + static_TABLE + " where ID = '" + id + "' and NAME = '" + name + "'";
String num = JdbcUtil.executeSelect(sql, "num");
return Integer.valueOf(num) > 0;
}

private static String uuid() {
return UUID.randomUUID().toString().replace("-", "");
}

二、谁写的?拖出去,斩了!


优化1:先查询全部数据,缓存到map中,插入前再进行判断,速度快了很多。


优化2:如果单个Excel文件过大,可以采用 异步 + 多线程 读取若干行,分批入库。



优化3:如果文件数量过多,可以采一个Excel一个异步,形成完美的双异步读取插入。



使用双异步后,从 191s 优化到 2s,你敢信?


下面贴出异步读取Excel文件、并分批读取大Excel文件的关键代码。


1、readExcelCacheAsync控制类


@RequestMapping(value = "/readExcelCacheAsync", method = RequestMethod.POST)
@ResponseBody
public String readExcelCacheAsync() {
String path = "G:\\测试\\data\\";
try {
// 在读取Excel之前,缓存所有数据
USER_INFO_SET = getUserInfo();

File file = new File(path);
String[] xlsxArr = file.list();
for (int i = 0; i < xlsxArr.length; i++) {
File fileTemp = new File(path + "\\" + xlsxArr[i]);
String filename = fileTemp.getName().replace(".xlsx", "");
readExcelCacheAsyncService.readXls(path + filename + ".xlsx", filename);
}
} catch (Exception e) {
logger.error("|#ReadDBCsv|#异常: ", e);
return "error";
}
return "success";
}

2、分批读取超大Excel文件


@Async("async-executor")
public void readXls(String filePath, String filename) throws Exception {
@SuppressWarnings("resource")
XSSFWorkbook xssfWorkbook = new XSSFWorkbook(new FileInputStream(filePath));
// 读取第一个工作表
XSSFSheet sheet = xssfWorkbook.getSheetAt(0);
// 总行数
int maxRow = sheet.getLastRowNum();
logger.info(filename + ".xlsx,一共" + maxRow + "行数据!");
StringBuilder insertBuilder = new StringBuilder();

insertBuilder.append("insert int0 ").append(filename).append(" ( UUID,");

XSSFRow row = sheet.getRow(0);
for (int i = 0; i < row.getPhysicalNumberOfCells(); i++) {
insertBuilder.append(row.getCell(i)).append(",");
}

insertBuilder.deleteCharAt(insertBuilder.length() - 1);
insertBuilder.append(" ) values ( ");

int times = maxRow / STEP + 1;
//logger.info("将" + maxRow + "行数据分" + times + "次插入数据库!");
for (int time = 0; time < times; time++) {
int start = STEP * time + 1;
int end = STEP * time + STEP;

if (time == times - 1) {
end = maxRow;
}

if(end + 1 - start > 0){
//logger.info("第" + (time + 1) + "次插入数据库!" + "准备插入" + (end + 1 - start) + "条数据!");
//readExcelDataAsyncService.readXlsCacheAsync(sheet, row, start, end, insertBuilder);
readExcelDataAsyncService.readXlsCacheAsyncMybatis(sheet, row, start, end, insertBuilder);
}
}
}

3、异步批量入库


@Async("async-executor")
public void readXlsCacheAsync(XSSFSheet sheet, XSSFRow row, int start, int end, StringBuilder insertBuilder) {
StringBuilder stringBuilder = new StringBuilder();
for (int i = start; i <= end; i++) {
XSSFRow xssfRow = sheet.getRow(i);
String id = "";
String name = "";
for (int j = 0; j < row.getPhysicalNumberOfCells(); j++) {
if (j == 0) {
id = xssfRow.getCell(j) + "";
} else if (j == 1) {
name = xssfRow.getCell(j) + "";
}
}

// 先在读取Excel之前,缓存所有数据,再做判断
boolean flag = isExisted(id, name);
if (!flag) {
stringBuilder.append(insertBuilder);
stringBuilder.append('\'').append(uuid()).append('\'').append(",");
for (int j = 0; j < row.getPhysicalNumberOfCells(); j++) {
stringBuilder.append('\'').append(value).append('\'').append(",");
}
stringBuilder.deleteCharAt(stringBuilder.length() - 1);
stringBuilder.append(" )").append("\n");
}
}

List<String> collect = Arrays.stream(stringBuilder.toString().split("\n")).collect(Collectors.toList());
if (collect != null && collect.size() > 0) {
int sum = JdbcUtil.executeDML(collect);
}
}

private boolean isExisted(String id, String name) {
return ReadExcelCacheAsyncController.USER_INFO_SET.contains(id + "," + name);
}

4、异步线程池工具类


@Async的作用就是异步处理任务。



  1. 在方法上添加@Async,表示此方法是异步方法;

  2. 在类上添加@Async,表示类中的所有方法都是异步方法;

  3. 使用此注解的类,必须是Spring管理的类;

  4. 需要在启动类或配置类中加入@EnableAsync注解,@Async才会生效;


在使用@Async时,如果不指定线程池的名称,也就是不自定义线程池,@Async是有默认线程池的,使用的是Spring默认的线程池SimpleAsyncTaskExecutor。


默认线程池的默认配置如下:



  1. 默认核心线程数:8;

  2. 最大线程数:Integet.MAX_VALUE;

  3. 队列使用LinkedBlockingQueue;

  4. 容量是:Integet.MAX_VALUE;

  5. 空闲线程保留时间:60s;

  6. 线程池拒绝策略:AbortPolicy;


从最大线程数可以看出,在并发情况下,会无限制的创建线程,我勒个吗啊。


也可以通过yml重新配置:


spring:
task:
execution:
pool:
max-size: 10
core-size: 5
keep-alive: 3s
queue-capacity: 1000
thread-name-prefix: my-executor

也可以自定义线程池,下面通过简单的代码来实现以下@Async自定义线程池。


@EnableAsync// 支持异步操作
@Configuration
public class AsyncTaskConfig {

/**
* com.google.guava中的线程池
* @return
*/

@Bean("my-executor")
public Executor firstExecutor() {
ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("my-executor").build();
// 获取CPU的处理器数量
int curSystemThreads = Runtime.getRuntime().availableProcessors() * 2;
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(curSystemThreads, 100,
200, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(), threadFactory);
threadPool.allowsCoreThreadTimeOut();
return threadPool;
}

/**
* Spring线程池
* @return
*/

@Bean("async-executor")
public Executor asyncExecutor() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
// 核心线程数
taskExecutor.setCorePoolSize(24);
// 线程池维护线程的最大数量,只有在缓冲队列满了之后才会申请超过核心线程数的线程
taskExecutor.setMaxPoolSize(200);
// 缓存队列
taskExecutor.setQueueCapacity(50);
// 空闲时间,当超过了核心线程数之外的线程在空闲时间到达之后会被销毁
taskExecutor.setKeepAliveSeconds(200);
// 异步方法内部线程名称
taskExecutor.setThreadNamePrefix("async-");

/**
* 当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize,如果还有任务到来就会采取任务拒绝策略
* 通常有以下四种策略:
* ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
* ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
* ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
* ThreadPoolExecutor.CallerRunsPolicy:重试添加当前的任务,自动重复调用 execute() 方法,直到成功
*/

taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
taskExecutor.initialize();
return taskExecutor;
}
}


5、异步失效的原因



  1. 注解@Async的方法不是public方法;

  2. 注解@Async的返回值只能为void或Future;

  3. 注解@Async方法使用static修饰也会失效;

  4. 没加@EnableAsync注解;

  5. 调用方和@Async不能在一个类中;

  6. 在Async方法上标注@Transactional是没用的,但在Async方法调用的方法上标注@Transcational是有效的;


三、线程池中的核心线程数设置问题


有一个问题,一直没时间摸索,线程池中的核心线程数CorePoolSize、最大线程数MaxPoolSize,设置成多少,最合适,效率最高。


借着这个机会,测试一下。


1、我记得有这样一个说法,CPU的处理器数量


将核心线程数CorePoolSize设置成CPU的处理器数量,是不是效率最高的?


// 获取CPU的处理器数量
int curSystemThreads = Runtime.getRuntime().availableProcessors() * 2;

Runtime.getRuntime().availableProcessors()获取的是CPU核心线程数,也就是计算资源。



  • CPU密集型,线程池大小设置为N,也就是和cpu的线程数相同,可以尽可能地避免线程间上下文切换,但在实际开发中,一般会设置为N+1,为了防止意外情况出现线程阻塞,如果出现阻塞,多出来的线程会继续执行任务,保证CPU的利用效率。

  • IO密集型,线程池大小设置为2N,这个数是根据业务压测出来的,如果不涉及业务就使用推荐。


在实际中,需要对具体的线程池大小进行调整,可以通过压测及机器设备现状,进行调整大小。


如果线程池太大,则会造成CPU不断的切换,对整个系统性能也不会有太大的提升,反而会导致系统缓慢。


我的电脑的CPU的处理器数量是24。


那么一次读取多少行最合适呢?


测试的Excel中含有10万条数据,10万/24 = 4166,那么我设置成4200,是不是效率最佳呢?


测试的过程中发现,好像真的是这样的。


2、我记得大家都习惯性的将核心线程数CorePoolSize和最大线程数MaxPoolSize设置成一样的,都爱设置成200。


是随便写的,还是经验而为之?


测试发现,当你将核心线程数CorePoolSize和最大线程数MaxPoolSize都设置为200的时候,第一次它会同时开启150个线程,来进行工作。


这个是为什么?


3、经过数十次的测试



  1. 发现核心线程数好像差别不大

  2. 每次读取和入库的数量是关键,不能太多,因为每次入库会变慢;

  3. 也不能太少,如果太少,超过了150个线程,就会造成线程阻塞,也会变慢;


四、通过EasyExcel读取并插入数据库


EasyExcel的方式,我就不写双异步优化了,大家切记陷入低水平勤奋的怪圈。


1、ReadEasyExcelController


@RequestMapping(value = "/readEasyExcel", method = RequestMethod.POST)
@ResponseBody
public String readEasyExcel() {
try {
String path = "G:\\测试\\data\\";
String[] xlsxArr = new File(path).list();
for (int i = 0; i < xlsxArr.length; i++) {
String filePath = path + xlsxArr[i];
File fileTemp = new File(path + xlsxArr[i]);
String fileName = fileTemp.getName().replace(".xlsx", "");
List<UserInfo> list = new ArrayList<>();
EasyExcel.read(filePath, UserInfo.class, new ReadEasyExeclAsyncListener(readEasyExeclService, fileName, batchCount, list)).sheet().doRead();
}
}catch (Exception e){
logger.error("readEasyExcel 异常:",e);
return "error";
}
return "suceess";
}

2、ReadEasyExeclAsyncListener


public ReadEasyExeclService readEasyExeclService;
// 表名
public String TABLE_NAME;
// 批量插入阈值
private int BATCH_COUNT;
// 数据集合
private List<UserInfo> LIST;

public ReadEasyExeclAsyncListener(ReadEasyExeclService readEasyExeclService, String tableName, int batchCount, List<UserInfo> list) {
this.readEasyExeclService = readEasyExeclService;
this.TABLE_NAME = tableName;
this.BATCH_COUNT = batchCount;
this.LIST = list;
}

@Override
public void invoke(UserInfo data, AnalysisContext analysisContext) {
data.setUuid(uuid());
data.setTableName(TABLE_NAME);
LIST.add(data);
if(LIST.size() >= BATCH_COUNT){
// 批量入库
readEasyExeclService.saveDataBatch(LIST);
}
}

@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
if(LIST.size() > 0){
// 最后一批入库
readEasyExeclService.saveDataBatch(LIST);
}
}

public static String uuid() {
return UUID.randomUUID().toString().replace("-", "");
}
}

3、ReadEasyExeclServiceImpl


@Service
public class ReadEasyExeclServiceImpl implements ReadEasyExeclService {

@Resource
private ReadEasyExeclMapper readEasyExeclMapper;

@Override
public void saveDataBatch(List<UserInfo> list) {
// 通过mybatis入库
readEasyExeclMapper.saveDataBatch(list);
// 通过JDBC入库
// insertByJdbc(list);
list.clear();
}

private void insertByJdbc(List<UserInfo> list){
List<String> sqlList = new ArrayList<>();
for (UserInfo u : list){
StringBuilder sqlBuilder = new StringBuilder();
sqlBuilder.append("insert int0 ").append(u.getTableName()).append(" ( UUID,ID,NAME,AGE,ADDRESS,PHONE,OP_TIME ) values ( ");
sqlBuilder.append("'").append(ReadEasyExeclAsyncListener.uuid()).append("',")
.append("'").append(u.getId()).append("',")
.append("'").append(u.getName()).append("',")
.append("'").append(u.getAge()).append("',")
.append("'").append(u.getAddress()).append("',")
.append("'").append(u.getPhone()).append("',")
.append("sysdate )");
sqlList.add(sqlBuilder.toString());
}

JdbcUtil.executeDML(sqlList);
}
}

4、UserInfo


@Data
public class UserInfo {

private String tableName;

private String uuid;

@ExcelProperty(value = "ID")
private String id;

@ExcelProperty(value = "NAME")
private String name;

@ExcelProperty(value = "AGE")
private String age;

@ExcelProperty(value = "ADDRESS")
private String address;

@ExcelProperty(value = "PHONE")
private String phone;
}

作者:哪吒编程
来源:juejin.cn/post/7315730050577694720
收起阅读 »

看我如何用JDBC数据库连接池,轻松解决大量并发请求问题!

我们已经知道JDBC是Java语言中用来规范客户端程序如何访问数据库的应用程序接口,也是大多数Java开发者与数据库打交道的必备工具。但是,你是否知道,JDBC在处理大量并发请求时,可能会遇到一些问题?这就是我们今天要讨论的主题——JDBC数据库连接池。首先,...
继续阅读 »

我们已经知道JDBC是Java语言中用来规范客户端程序如何访问数据库的应用程序接口,也是大多数Java开发者与数据库打交道的必备工具。

但是,你是否知道,JDBC在处理大量并发请求时,可能会遇到一些问题?这就是我们今天要讨论的主题——JDBC数据库连接池。

首先,让我们来了解一下什么是数据库连接池。

一、数据库连接池简介

JDBC连接池,全称为Java多线程数据库连接池,是一种用于管理数据库连接的技术。其主要作用是减少每次请求时创建和释放数据库连接的开销,以此提高系统性能。

在应用程序和数据库之间,JDBC连接池会建立一个连接池,当需要访问数据库时,无需每次都重新创建连接,而是直接从池中获取已有的连接。

Description

总结一下就是:

  • 数据库连接池是个容器,负责分配、管理数据库连接(Connection)

  • 它允许应用程序重复使用一个现有的数据库连接,而不是再重新建立一个。

  • 释放空闲时间超过最大空闲时间的数据库连接来避免因为没有释放数据库连接而引起的数据库连接遗漏。

那么,为什么我们需要JDBC数据库连接池呢?

这主要有以下几个原因:

1.提高性能: 频繁地创建和销毁数据库连接会消耗大量的系统资源,而通过使用连接池,可以大大减少这部分开销,提高系统的性能。

2.提高稳定性: 在高并发的情况下,如果直接使用JDBC创建数据库连接,可能会出现系统无法创建更多的数据库连接的情况,导致系统崩溃。而通过使用连接池,可以有效地控制并发请求的数量,保证系统的稳定性。

3.提高数据库的响应速度: 通过使用连接池,可以减少等待数据库连接的时间,从而提高系统的响应速度。

之前我们代码中使用连接是没有使用都创建一个Connection对象,使用完毕就会将其销毁。这样重复创建销毁的过程是特别耗费计算机的性能的及消耗时间的。

而数据库使用了数据库连接池后,就能达到Connection对象的复用,如下图:

Description

  • 连接池是在一开始就创建好了一些连接(Connection)对象存储起来。用户需要连接数据库时,不需要自己创建连接;

  • 而只需要从连接池中获取一个连接进行使用,使用完毕后再将连接对象归还给连接池。

这样就可以起到资源重用,也节省了频繁创建连接销毁连接所花费的时间,从而提升了系统响应的速度。

你还在苦恼找不到真正免费的编程学习平台吗?可以试试云端源想!课程视频、在线书籍、在线编程、实验场景模拟、一对一咨询……你想要的全部学习资源这里都有,重点是统统免费!点这里即可查看


二、数据库连接池实现

1、标准接口:

javax.sql.DataSource。

官方(SUN公司)为数据库连接池提供了一套标准接口,由第三方组织实现此接口。

  • 核心方法:Connection getConnection(),获取连接。

Description

2、常见的数据库连接池:

JDBC的数据库连接池使用javax.sql.DataSource来表示,DataSource只是一个接口,该接口通常由第三方来实现。

市面上有很多开源的JDBC数据库连接池,如C3P0、DBCP、Druid等,它们都有各自的特点和优势。

C3P0数据库连接池: 速度相对较慢(只是慢一丢丢),但是稳定性很好,Hibernate,Spring底层用的就是C3P0。

DBCP数据库连接池: 速度比C3P0快,但是稳定性差。

Proxool数据库连接池: 有监控连接池状态的功能,但稳定性仍然比C3P0差一些。

BoneCP数据库连接池: 速度较快。

Druid数据库连接池(德鲁伊连接池): 由阿里提供,集DBCP,Proxool,C3P0连接池的优点于一身,是日常项目开发中使用频率最高的数据库连接池。

三、Durid(德鲁伊连接池)的使用

Druid使用步骤:

  • 导入jar包 druid-1.1.12.jar。

  • 定义配置文件。

  • 加载配置文件。

  • 获取数据库连接池对象。

  • 获取连接。

druid.properties配置文件:

driverClassName=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/jdbc_test?useSSL=false&useServerPrepStmts=true
username=root
password=123456
# 初始化连接数量
initialSize=5
# 最大连接数
maxActive=10
# 最大等待时间
maxWait=3000

代码示例:


package com.green.druid;


import com.alibaba.druid.pool.DruidDataSourceFactory;

import javax.sql.DataSource;
import java.io.FileInputStream;
import java.sql.Connection;
import java.util.Properties;

public class DruidDemo {

public static void main(String[] args) throws Exception {
//1、导入jar包


//2、定义配置文件


//3、加载配置文件
Properties prop = new Properties();
prop.load(new FileInputStream("jdbc-demo/src/druid.properties"));
//System.out.println(System.getProperty("user.dir")); //当前文件目录 D:\code\JDBC


//4、获取连接池对象
DataSource dataSource = DruidDataSourceFactory.createDataSource(prop);


//5、获取数据库连接 Connection
Connection conn = dataSource.getConnection();


System.out.println(conn);


}
}

以上就是JDBC数据连接池的简介与常见连接池的基本使用,希望对你有所帮助。在未来的开发过程中,不妨尝试使用JDBC数据库连接池,让你的应用性能更上一层楼!

收起阅读 »

慎用,Mybatis-Plus这个方法可能导致死锁

1 场景还原 1.1 版本信息 MySQL版本:5.6.36-82.1-log  Mybatis-Plus的starter版本:3.3.2 存储引擎:InnoDB 1.2 死锁现象 A同学在生产环境使用了Mybatis-Plus提供的 com.b...
继续阅读 »

1 场景还原


1.1 版本信息


MySQL版本:5.6.36-82.1-log 
Mybatis-Plusstarter版本:3.3.2
存储引擎:InnoDB

1.2 死锁现象



A同学在生产环境使用了Mybatis-Plus提供的 com.baomidou.mybatisplus.extension.service.IService#saveOrUpdate(T, com.baomidou.mybatisplus.core.conditions.Wrapper) 方法(以下简称B方法),并发场景下,数据库报了如下错误





2 为什么是间隙锁死锁?



如上图示,数据库报了死锁,那死锁场景千万种,为什么确定B方法是由于间隙锁导致的死锁?



2.1 什么是死锁?


两个事务互相等待对方持有的锁,导致互相阻塞,从而导致死锁。


2.2 什么是间隙锁?



  • 间隙锁是MySQL行锁的一种,与Record lock不同的是间隙锁锁定的是一个间隙。

  • 锁定规则如下:


MySQL会向左找第一个比当前索引值小的值,向右找第一个比当前索引值大 的值(没有则为正无穷),将此区间锁住,从而阻止其他事务在此区间插入数据。


2.3 MySQL为什么要引入间隙锁?


与Record lock组合成Next-key lock,在可重复读这种隔离级别下一起工作避免幻读。


2.4 间隙锁死锁分析


理论上一款开源的框架,经过了多年打磨,提供的方法不应该造成如此严重的错误,但理论仅仅是理论上,事实就是发生了死锁,于是我们开始了一轮深度排查。首先我们从这个方法的源码入手,源码如下:


    default boolean saveOrUpdate(T entity, Wrapper updateWrapper) {
        return this.update(entity, updateWrapper) || this.saveOrUpdate(entity);
    }

从源码上看此方法就没有按套路出牌,正常逻辑应该是首先执行查询,存在则修改,不存在则新增,但此方法上来就执行了修改。我们就猜想是不是MySQL在修改时增加了什么锁导致了死锁,于是我们找到了DBA获取了最新的死锁日志,即执行show engine innodb status,我们发现了两项关键信息如下:


*** (1) TRANSACTION:
...省略日志
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 0 page no 347 n bits 80 index `PRIMARY` of table `database_name`.`table_name` trx id 71C lock_mode X locks gap before rec insert intention waiting
  
*** (2) TRANSACTION:
...省略日志
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 0 page no 347 n bits 80 index `PRIMARY` of table `database_name`.`table_name` trx id 71D lock_mode X locks gap before rec insert intention waiting

简单翻译一下,就是事务一在获取插入意向锁时,需要等待间隙锁(事务二添加)释放,同时事务二在获取插入意向锁时,也在等待间隙锁释放(事务一添加), (本文不讨论MySQL在修改与插入时添加的锁,我们把修改时添加间隙锁,插入时获取插入意向锁为已知条件) 那我们回到B方法,并发场景下,是不是就很大几率会满足事务一和事务二相互等待对方持有的间隙锁,从而导致死锁。




现在我们理论有了,我们现在用真实数据来验证此场景。


2.5 验证间隙锁死锁



  • 准备如下表结构(以下简称验证一)


create table t_gap_lock(
id int auto_increment primary key comment '主键ID',
name varchar(64not null comment '名称',
age int not null comment '年龄'
comment '间隙锁测试表';


  • 准备如下表数据


mysql> select * from t_gap_lock;
+----+------+-----+
| id | name | age |
+----+------+-----+
|  1 | 张三 |  18 |
|  5 | 李四 |  19 |
|  6 | 王五 |  20 |
|  9 | 赵六 |  21 |
| 12 | 孙七 |  22 |
+----+------+-----+


  • 我们开启事务一,并执行如下语句,注意这个时候我们还没有提交事务


mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> update t_gap_lock t set t.age = 25 where t.id = 4;
Query OK, 0 rows affected (0.00 sec)
Rows matched: 0  Changed: 0  Warnings: 0


  • 同时我们开启事务二,并执行如下语句,事务二我们同样不提交事务


mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> update t_gap_lock t set t.age = 25 where t.id = 7;
Query OK, 0 rows affected (0.00 sec)
Rows matched: 0  Changed: 0  Warnings: 0


  • 接下来我们在事务一中执行如下语句


mysqlinsert int0 t_gap_lock(id, name, agevalue (7,'间隙锁7',27);  


  • 我们会发现事务一被阻塞了,然后我们执行以下语句看下当前正在锁的事务。


mysql> SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS \G;
*************************** 1. row ***************************
    lock_id: 749:0:360:3
lock_
trx_id: 749
  lock_
mode: X,GAP
  lock_type: RECORD
 lock_
table: `test`.`t_gap_lock`
 lock_index: `PRIMARY`
 lock_
space: 0
  lock_page: 360
   lock_
rec: 3
  lock_data: 5
*************************** 2. row ***************************
    lock_
id: 74A:0:360:3
lock_trx_id: 74A
  lock_mode: X,GAP
  lock_
type: RECORD
 lock_table: `test`.`t_gap_lock`
 lock_
index: `PRIMARY`
 lock_space: 0
  lock_
page: 360
   lock_rec: 3
  lock_
data: 5
2 rows in set (0.00 sec)

根据lock_type和lock_mode我们可以很清晰的看到锁类型是行锁,锁模式是间隙锁。



  • 与此同时我们在事务二中执行如下语句


insert int0 t_gap_lock(idname, agevalue (4,'间隙锁4',24);


  • 一执行以上语句,数据库就立马报了死锁,并且回滚了事务二(可以在死锁日志中看到*** WE ROLL BACK TRANSACTION (2))


ERROR 1213 (40001): Deadlock found when trying to get locktry restarting transaction 



到这里,细心的同学就会发现,诶,你这上面故意造了一个间隙,并且让两个事务分别在对方的间隙中插入数据,太刻意了,生产环境基本上不会有这种场景,是的,生产环境怎么会有这种场景呢,上面的数据只是为了让大家直观的看到间隙锁的死锁过程,接下来那我们再来一组数据,我们简称验证二。



  • 我们还是以验证一的表结构与数据,我们来执行这样一个操作。首先我们开始开启事务一并且执行如下操作,依然不提交事务


mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> update t_gap_lock t set t.age = 25 where t.id = 4;
Query OK, 0 rows affected (0.00 sec)
Rows matched: 0  Changed: 0  Warnings: 0 


  • 同时我们开启事务二,执行与事务一一样的操作,我们会惊奇的发现,竟然也成功了。


mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> update t_gap_lock t set t.age = 25 where t.id = 4;
Query OK, 0 rows affected (0.00 sec)
Rows matched: 0  Changed: 0  Warnings: 0 


  • 于是乎我们在事务一执行如下操作,我们又惊奇的发现事务一被阻塞了。


insert int0 t_gap_lock(idname, agevalue (4,'间隙锁4',24);  


  • 在事务一被阻塞的同时,我们在事务二执行同样的语句,我们发现数据库立马就报了死锁。


insert int0 t_gap_lock(idname, agevalue (4,'间隙锁4',24);    
ERROR 1213 (40001): Deadlock found when trying to get locktry restarting transaction

验证二完整的复现了线上死锁的过程,也就是事务一先执行了更新语句,事务二在同一时刻也执行了更新语句,然后事务一发现没有更新到就去执行主键查询语句,发现确实没有,所以执行了插入语句,但是插入要先获取插入意向锁,在获取插入意向锁的时候发现这个间隙已经被事务二加锁了,所以事务一开始等待事务二释放间隙锁,同理,事务二也执行上述操作,最终导致事务一与事务二互相等待对方释放间隙锁,最终导致死锁。


验证二还说明了一个问题,就是间隙锁加锁是非互斥的,也就是事务一对间隙A加锁后,事务二依然可以给间隙A加锁。


3 如何解决?


3.1 关闭间隙锁(不推荐)



  • 降低隔离级别,例如降为提交读。

  • 直接修改my.cnf,将开关,innodb_locks_unsafe_for_binlog改为1,默认为0即开启


PS:以上方法仅适用于当前业务场景确实不关心幻读的问题。


3.2 自定义saveOrUpdate方法(推荐)


建议自己编写一个saveOrUpdate方法,当然也可以直接采用Mybatis-Plus提供的saveOrUpdate方法,但是根据源码发现,会有很多额外的反射操作,并且还添加了事务,大家都知道,MySQL单表操作完全不需要开事务,会增加额外的开销。


  @Transactional(
        rollbackFor = {Exception.class}
    )
    public boolean saveOrUpdate(T entity) {
        if (null == entity) {
            return false;
        } else {
            Class cls = entity.getClass();
            TableInfo tableInfo = TableInfoHelper.getTableInfo(cls);
            Assert.notNull(tableInfo, "error: can not execute. because can not find cache of TableInfo for entity!"new Object[0]);
            String keyProperty = tableInfo.getKeyProperty();
            Assert.notEmpty(keyProperty, "error: can not execute. because can not find column for id from entity!"new Object[0]);
            Object idVal = ReflectionKit.getFieldValue(entity, tableInfo.getKeyProperty());
            return !StringUtils.checkValNull(idVal) && !Objects.isNull(this.getById((Serializable)idVal)) ? this.updateById(entity) : this.save(entity);
        }
    }

4 拓展


4.1 如果两个事务修改是存在的行会发生什么?


在验证二中两个事务修改的都是不存在的行,都能加间隙锁成功,那如果两个事务修改的是存在的行,MySQL还会加间隙锁吗?或者说把间隙锁从锁间隙降为锁一行?带着疑问,我们执行以下数据验证,我们还是使用验证一的表和数据。



  • 首先我们开启事务一执行以下语句


mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> update t_gap_lock t set t.age = 25 where t.id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0


  • 我们再开启事务二,执行同样的语句,发现事务二已经被阻塞


mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql
> update t_gap_lock t set t.age = 25 where t.id = 1;


  • 这个时候我们执行以下语句看下当前正在锁的事务。


mysql> SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS \G;
*************************** 1. row ***************************
    lock_id: 75C:0:360:2
lock_
trx_id: 75C
  lock_
mode: X
  lock_type: RECORD
 lock_
table: `test`.`t_gap_lock`
 lock_index: `PRIMARY`
 lock_
space: 0
  lock_page: 360
   lock_
rec: 2
  lock_data: 1
*************************** 2. row ***************************
    lock_
id: 75B:0:360:2
lock_trx_id: 75B
  lock_mode: X
  lock_
type: RECORD
 lock_table: `test`.`t_gap_lock`
 lock_
index: `PRIMARY`
 lock_space: 0
  lock_
page: 360
   lock_rec: 2
  lock_
data: 1
2 rows in set (0.00 sec)

根据lock_type和lock_mode我们看到事务一和二加的锁变成了Record Lock,并没有再添加间隙锁,根据以上数据验证MySQL在修改存在的数据时会给行加上Record Lock,与间隙锁不同的是该锁是互斥的,即不同的事务不能同时对同一行记录添加Record Lock。


5 结语


虽然Mybatis-Plus提供的这个方法可能会造成死锁,但是依然不可否认它是一款非常优秀的增强框架,其提供的lambda写法在日常工作中极大的提高了我们的开发效率,所以凡事都用两面性,我们应该秉承辩证的态度,熟悉的方法尝试用,陌生的方法谨慎用。


以上就是我们在生产环境间隙锁死锁分析的全过程,如果大家觉得本文让你对间隙锁,以及间隙锁死锁有一点的了解,别忘记一键三连,多多支持转转技术,转转技术在未来将会给大家带来更多的生产实践与探索。


作者:转转技术团队
来源:juejin.cn/post/7311880893841719330
收起阅读 »

js跨标签页通信

web
一、为什么要跨标签页通信 在web开发中,有时会有这样的情况,A页面中打开了B页面,B页面中操作了一些内容,再回到A页面时,需要看到更新后的内容。这种场景在电商、支付、社交等领域经常出现。 二、实现跨标签页通信的几种方式 2.1 localStorage 打...
继续阅读 »

一、为什么要跨标签页通信


在web开发中,有时会有这样的情况,A页面中打开了B页面,B页面中操作了一些内容,再回到A页面时,需要看到更新后的内容。这种场景在电商、支付、社交等领域经常出现。


二、实现跨标签页通信的几种方式


2.1 localStorage


image.png


打开A页面,可以看到localStorage和sessionStorage中都存储了testA:


image.png
image.png

B页面中,可以获取到localStorage,但是无法获取到sessionStorage:


image.png

2.2 BroadcastChannel


BroadcastChannel允许同源下浏览器不同窗口订阅它,postMessage方法用于发送消息,message事件用于接收消息。


A页面:


      const bc = new BroadcastChannel('test')

bc.postMessage('不去上班行吗?')

B页面:


      const bc = new BroadcastChannel('test')

bc.onmessage = (e) => {
console.log(e)
}

动画.gif


2.3 postMessage(跨源通信)


image.png


2.3.1 iframe跨域数据传递


parent.html


<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
</head>
<body>
<h1>主页面</h1>
<iframe id="child" src="http://10.7.9.69:8080"></iframe>
<div>
<h2>主页面跨域接收消息区域</h2>
<div id="message"></div>
</div>
</body>
<script>
// 传递数据到子页面
window.onload = function () {
// 第二个参数表示哪个窗口可以接收到消息
document.getElementById('child').contentWindow.postMessage('不上班行不行', 'http://10.7.9.69:8080')
}
// 接受子页面传递过来的数据
window.addEventListener('message', function (event) {
document.getElementById('message').innerHTML = '收到' + event.origin + '消息:' + event.data
})
</script>
</html>


App.vue


<template>
<div class="app">
<div id="message"></div>
</div>

</template>
<script>
export default {
created() {
// 接收父页面传过来的数据
window.addEventListener('message', function (event) {
// 处理addEventListener执行两次的情况,避免获取不到data
// 因此判断接收的域是否是父页面
console.log('event', event)
if (event.origin.includes('http://127.0.0.1:5501')) {
document.getElementById('message').innerHTML = '收到' + event.origin + '消息:' + event.data
// 把数据传递给父页面 window.parent === top
window.parent.postMessage('不上班你养我啊', 'http://127.0.0.1:5501')
}
})
},
}
</script>


image.png


注:



  1. http://127.0.0.1:5501/ 是使用 Open with Live Server打开后的地址

  2. http://10.7.9.69:8080/ 是启动vue后的地址


2.3.2 postMessage在window.open()中的使用


作者:蓝色海岛
来源:juejin.cn/post/7315354087829536803
收起阅读 »

【前端考古】没有await,如何处理“回调地狱”

web
太长不看 不要嵌套使用函数。给每个函数命名并把他们放在你代码的顶层 利用函数提升。先使用后声明。 处理每一个异常 编写可以复用的函数,并把他们封装成一个模块 什么是“回调地狱”? 异步Javascript代码,或者说使用callback的Javascrip...
继续阅读 »

太长不看



  • 不要嵌套使用函数。给每个函数命名并把他们放在你代码的顶层

  • 利用函数提升。先使用后声明。

  • 处理每一个异常

  • 编写可以复用的函数,并把他们封装成一个模块


什么是“回调地狱”?


异步Javascript代码,或者说使用callback的Javascript代码,很难符合我们的直观理解。很多代码最终会写成这样:


fs.readdir(source, function (err, files) {
if (err) {
console.log('Error finding files: ' + err)
} else {
files.forEach(function (filename, fileIndex) {
console.log(filename)
gm(source + filename).size(function (err, values) {
if (err) {
console.log('Error identifying file size: ' + err)
} else {
console.log(filename + ' : ' + values)
aspect = (values.width / values.height)
widths.forEach(function (width, widthIndex) {
height = Math.round(width / aspect)
console.log('resizing ' + filename + 'to ' + height + 'x' + height)
this.resize(width, height).write(dest + 'w' + width + '_' + filename, function(err) {
if (err) console.log('Error writing file: ' + err)
})
}.bind(this))
}
})
})
}
})

看到上面金字塔形状的代码和那些末尾参差不齐的 }) 了吗?这就是广为人知的回调地狱了。

人们在编写JavaScript代码时,误认为代码是按照我们看到的代码顺序从上到下执行的,这就是造成回调地狱的原因。在其他语言中,例如C,Ruby或者Python,第一行代码执行结束后,才会开始执行第二行代码,按照这种模式一直到执行到当前文件中最后一行代码。随着你学习深入,你会发现JavaScript跟他们是不一样的。


什么是回调(callback)?


某种使用JavaScript函数的惯例用法的名字叫做回调。JavaScript语言中没有一个叫“回调”的东西,它仅仅是一个惯例用法的名字。大多数函数会立刻返回执行结果,使用回调的函数通常会经过一段时间后才输出结果。名词“异步”,简称“async”,只是意味着“这将花费一点时间”或者说“在将来某个时间发生而不是现在”。通常回调只使用在I/O操作中,例如下载文件,读取文件,连接数据库等等。


当你调用一个正常的函数时,你可以向下面的代码那样使用它的返回值:


var result = multiplyTwoNumbers(5, 10)
console.log(result)
// 50 gets printed out

然而使用回调的异步函数不会立刻返回任何结果。


var photo = downloadPhoto('http://coolcats.com/cat.gif')
// photo is 'undefined'!

在这种情况下,上面那张gif图片可能需要很长的时间才能下载完成,但你不想你的程序在等待下载完成的过程中中止(也叫阻塞)。


于是你把需要下载完成后运行的代码存放到一个函数中(等待下载完成后再运行它)。这就是回调!你把回调传递给downloadPhoto函数,当下载结束,回调会被调用。如果下载成功,传入photo给回调;下载失败,传入error给回调。


downloadPhoto('http://coolcats.com/cat.gif', handlePhoto)

function handlePhoto (error, photo) {
if (error) console.error('Download error!', error)
else console.log('Download finished', photo)
}

console.log('Download started')

人们理解回调的最大障碍在于理解一个程序的执行顺序。在上面的例子中,发生了三件事情。



  1. 声明handlePhoto函数

  2. downloadPhoto函数被调用并且传入了handlePhoto最为它的回调

  3. 打印出Download started


请大家注意,起初handlePhoto函数仅仅是被创建并被作为回调传递给了downloadPhoto,它还没有被调用。它会等待downloadPhoto函数完成了它的任务才会执行。这可能需要很长一段时间(取决于网速的快慢)。


这个例子意在阐明两个重要的概念:



  1. handlePhoto回调只是一个存放将来进行的操作的方式

  2. 事情发生的顺序并不是直观上看到的从上到下,它会当某些事情完成后再跳回来执行。


怎样解决“回调地狱”问题?


糟糕的编码习惯造成了回调地狱。幸运的是,编写优雅的代码不是那么难!


你只需要遵循三大原则


1. 减少嵌套层数(Keep your code shallow)


下面是一堆乱糟糟的代码,使用browser-request做AJAX请求。


**


var form = document.querySelector('form')
form.onsubmit = function (submitEvent) {
var name = document.querySelector('input').value
request({
uri: "http://example.com/upload",
body: name,
method: "POST"
}, function (err, response, body) {
var statusMessage = document.querySelector('.status')
if (err) return statusMessage.value = err
statusMessage.value = body
})
}

这段代码包含两个匿名函数,我们来给他们命名。


var form = document.querySelector('form')
form.onsubmit = function formSubmit (submitEvent) {
var name = document.querySelector('input').value
request({
uri: "http://example.com/upload",
body: name,
method: "POST"
}, function postResponse (err, response, body) {
var statusMessage = document.querySelector('.status')
if (err) return statusMessage.value = err
statusMessage.value = body
})
}

如你所见,给匿名函数一个名字是多么简单,而且好处立竿见影:



  • 起一个一望便知其函数功能的名字让代码更易读

  • 当抛出异常时,你可以在stacktrace里看到实际出异常的函数名字,而不是"anonymous"

  • 允许你合理安排函数的位置,并通过函数名字调用它


现在我们可以把这些函数放在我们程序的顶层。


document.querySelector('form').onsubmit = formSubmit

function formSubmit (submitEvent) {
var name = document.querySelector('input').value
request({
uri: "http://example.com/upload",
body: name,
method: "POST"
}, postResponse)
}

function postResponse (err, response, body) {
var statusMessage = document.querySelector('.status')
if (err) return statusMessage.value = err
statusMessage.value = body
}

请大家注意,函数声明在程序的底部,但是我们在函数声明之前就可以调用它。这是函数提升的作用。


2.模块化(Modularize)


任何人都有有能力创建模块,这点非常重要。写出一些小模块,每个模块只做一件事情,然后把他们组合起来放入其他的模块做一个复杂的事情。只要你不想陷入回调地狱,你就不会。让我们把上面的例子修改一下,改为一个模块。


下面是一个名为formuploader.js的新文件,包含了我们之前使用过的两个函数。


module.exports.submit = formSubmit

function formSubmit (submitEvent) {
var name = document.querySelector('input').value
request({
uri: "http://example.com/upload",
body: name,
method: "POST"
}, postResponse)
}

function postResponse (err, response, body) {
var statusMessage = document.querySelector('.status')
if (err) return statusMessage.value = err
statusMessage.value = body
}

module.exports是node.js模块化的用法。现在已经有了 formuploader.js 文件,我们只需要引入它并使用它。请看下面的代码:


var formUploader = require('formuploader')
document.querySelector('form').onsubmit = formUploader.submit

我们的应用只有两行代码并且还有以下好处:



  1. 方便新开发人员理解你的代码 -- 他们不需要费尽力气读完formuploader函数的全部代码

  2. formuploader可以在其他地方复用


3.处理每一个异常(Handle every single error)


有三种不同类型的异常:语法异常,运行时异常和平台异常。语法异常通常由开发人员在第一次解释代码时捕获,运行时异常通常在代码运行过程中因为bug触发,平台异常通常由于没有文件的权限,硬盘错误,无网络链接等问题造成。这一部分主要来处理最后一种异常:平台异常。


前两个大原则意在提高代码可读性,但是第三个原则意在提高代码的稳定性。在你与回调打交道的时候,你通常要处理发送请求,等待返回或者放弃请求等任务。任何有经验的开发人员都会告诉你,你从来不知道哪里回出现问题。所以你有必要提前准备好,异常总是会发生。


把回调函数的第一个参数设置为error对象,是Node.js中处理异常最流行的方式。


var fs = require('fs')

fs.readFile('/Does/not/exist', handleFile)

function handleFile (error, file) {
if (error) return console.error('Uhoh, there was an error', error)
// otherwise, continue on and use `file` in your code
}

把第一个参数设为error对象是一个约定俗成的惯例,提醒你记得去处理异常。如果它是第二个参数,你更容易把它忽略掉。


作者:Max力出奇迹
来源:juejin.cn/post/7294166986195533843
收起阅读 »

解决hutool图形验证码bug

从网上下载了一个开源的项目,发现登录界面的验证码无论怎么刷新,显示的都是同一个,即使换一个浏览器也是相同的图形验证码,说一下解决bug的过程 😶修改前的源代码如下(部分代码) import cn.hutool.captcha.*; import lombo...
继续阅读 »

从网上下载了一个开源的项目,发现登录界面的验证码无论怎么刷新,显示的都是同一个,即使换一个浏览器也是相同的图形验证码,说一下解决bug的过程



😶修改前的源代码如下(部分代码)



import cn.hutool.captcha.*;
import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class AuthServiceImpl implements AuthService {

private final AbstractCaptcha abstractCaptcha;

@Override
public CaptchaResult getCaptcha() {
//获取验证码文本,例如 1+4=
String captchaCode = abstractCaptcha.getCode();
String imageBase64Data = abstractCaptcha.getImageBase64Data();

String captchaKey = IdUtil.fastSimpleUUID();
// 验证码文本缓存至Redis,用于登录校验
redisTemplate.opsForValue().set(CacheConstants.CAPTCHA_CODE_PREFIX + captchaKey, captchaCode,captchaProperties.getExpireSeconds(), TimeUnit.SECONDS);
//......
}
}

😐分析上面的代码,首先作者使用了Lombok提供的@RequiredArgsConstructor注解,它的作用是为下面被 final 修饰的变量生成构造方法,即利用构造方法将AbstractCaptcha对象注入到Spring容器中,但是通过这种方式注入,默认情况下Bean是单例的,即多次请求会复用同一个Bean 。


😐其次,阅读hutool有关abstractCaptcha.getCode()部分的代码,如下,可以看到,在第一次生成code时,就将生成的code赋值给了成员变量 code,再结合前面的单例Bean,真相大白。


//验证码
protected String code;

@Override
public String getCode() {
if (null == this.code) {
createCode();
}
return this.code;
}

@Override
public void createCode() {
generateCode();

final ByteArrayOutputStream out = new ByteArrayOutputStream();
ImgUtil.writePng(createImage(this.code), out);
this.imageBytes = out.toByteArray();
}

//生成验证码字符串
protected void generateCode() {
this.code = generator.generate();
}


😎最终修改业务代码如下



import cn.hutool.captcha.*;
import cn.hutool.captcha.generator.MathGenerator;
import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class AuthServiceImpl implements AuthService {

//private final AbstractCaptcha abstractCaptcha;

@Override
public CaptchaResult getCaptcha() {
ShearCaptcha captcha = CaptchaUtil.createShearCaptcha(200, 45, 4, 4);
// 自定义验证码内容为四则运算方式
captcha.setGenerator(new MathGenerator(1));
String captchaCode = captcha.getCode();
String captchaBase64 = captcha.getImageBase64Data();

String captchaKey = IdUtil.fastSimpleUUID();
// 验证码文本缓存至Redis,用于登录校验
redisTemplate.opsForValue().set(CacheConstants.CAPTCHA_CODE_PREFIX + captchaKey, captchaCode,captchaProperties.getExpireSeconds(), TimeUnit.SECONDS);
//......
}
}

😴阅读hutool关于createShearCaptcha()方法的源码,每次调用都会new一个新对象


/**
* 创建扭曲干扰的验证码,默认5位验证码
*
* @param width 图片宽
* @param height 图片高
* @param codeCount 字符个数
* @param thickness 干扰线宽度
* @return {@link ShearCaptcha}
* @since 3.3.0
*/

public static ShearCaptcha createShearCaptcha(int width, int height, int codeCount, int thickness) {
return new ShearCaptcha(width, height, codeCount, thickness);
}

作者:tomla
来源:juejin.cn/post/7316592830638800947
收起阅读 »

从零开始写一个web服务到底有多难?

背景 ​ 服务想必大家都有很多开发经验,但是从零开始搭建一个项目的经验,就少的多了。更不要说不使用任何框架的情况下从零开始搭建一个服务。那么这次就看看从零开始搭建一个好用好写web服务到底有多难? HelloWorld 官网给出的helloworld例子。ht...
继续阅读 »

背景



服务想必大家都有很多开发经验,但是从零开始搭建一个项目的经验,就少的多了。更不要说不使用任何框架的情况下从零开始搭建一个服务。那么这次就看看从零开始搭建一个好用好写web服务到底有多难?


HelloWorld


官网给出的helloworld例子。http标准库提供了两个方法,HandleFunc注册处理方法和ListenAndServe启动侦听接口。


请在此添加图片描述


请在此添加图片描述


假如业务更多


下面我们模拟一下接口增多的情况。可以看出有大量重复的部分。这样自然而然就产生了抽象服务的需求。


package main

import (
"fmt"
"net/http"
)

func greet(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Greet!: %s\n", r.URL.Path)
}

func hello(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello!: %s\n", r.URL.Path)
}

func notfound(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, you've requested: %s\n", r.URL.Path)
}

func main() {
http.HandleFunc("/", notfound)
http.HandleFunc("/hello", hello)
http.HandleFunc("/greet", greet)
http.ListenAndServe(":80", nil)
}

我们想要一个服务,它代表的是对某个端口监听的实例,它可以根据访问的路径,调用对应的方法。在需要的时候,我可以生成多个服务实例,监听多个端口。那么我们的Server需要实现下面两个方法。


type Server interface {
Route(pattern string, handlerFunc http.HandlerFunc)

Start(address string) error
}

简单实现一下。


package server

import "net/http"

type Server interface {
Route(pattern string, handlerFunc http.HandlerFunc)
Start(address string) error
}

type httpServer struct {
Name string
}

func (s *httpServer) Route(pattern string, handlerFunc http.HandlerFunc) {
http.HandleFunc(pattern, handlerFunc)
}

func (s *httpServer) Start(address string) error {
return http.ListenAndServe(address, nil)
}

func NewHttpServer(name string) Server {
return &httpServer{
Name: name,
}
}

修改业务代码


func main() {
server := server.NewHttpServer("demo")
server.Route("/", notfound)
server.Route("/hello", hello)
server.Route("/greet", greet)
server.Start(":80")
}

格式化输入输出


在我们实际使用过程中,输入输出一般都是以json的格式。自然也需要通用的处理过程。


type Context struct {
W http.ResponseWriter
R *http.Request
}

func (c *Context) ReadJson(data interface{}) error {
body, err := io.ReadAll(c.R.Body)
if err != nil {
return err
}
err = json.Unmarshal(body, data)
if err != nil {
return err
}
return nil
}

func (c *Context) WriteJson(code int, resp interface{}) error {
c.W.WriteHeader(code)
respJson, err := json.Marshal(resp)
if err != nil {
return err
}
_, err = c.W.Write(respJson)
return err
}

模拟了一个常见的业务代码。定义了入参和出参。


type helloReq struct {
Name string
Age string
}

type helloResp struct {
Data string
}

func hello(w http.ResponseWriter, r *http.Request) {
req := &helloReq{}
ctx := &server.Context{
W: w,
R: r,
}

err := ctx.ReadJson(req)

if err != nil {
fmt.Fprintf(w, "err:%v", err)
return
}

resp := &helloResp{
Data: req.Name + "_" + req.Age,
}

err = ctx.WriteJson(http.StatusOK, resp)
if err != nil {
fmt.Fprintf(w, "err:%v", err)
return
}

}

用postman试一下,是不是和我们平常开发的接口有一点像了。


请在此添加图片描述


由于200,404,500的返回结果实在是太普遍了,我们当然也可以进一步封装输出方法。但是我觉得没必要。


在我们设计的过程中,是否要提供辅助性的方法,还是只聚焦核心功能,是非常值得考虑的问题。


func (c *Context) SuccessJson(resp interface{}) error {
return c.WriteJson(http.StatusOK, resp)
}

func (c *Context) NotFoundJson(resp interface{}) error {
return c.WriteJson(http.StatusNotFound, resp)
}

func (c *Context) ServerErrorJson(resp interface{}) error {
return c.WriteJson(http.StatusInternalServerError, resp)
}


让框架来创建Context


观察下业务代码,还有个非常让人不舒服的地方。Context是框架内部使用的数据结构,居然要业务来创建!真的是太不合理了。


那么下面我们把Context移入框架内部创建,同时业务侧提供的handlefunction入参应该直接是由框架创建的Context。


首先修改我们的路由注册接口的定义。在实现中,我们注册了一个匿名函数,在其中构建了ctx的实例,并调用入参中业务的handlerFunc。


type Server interface {
Route(pattern string, handlerFunc func(ctx *Context))
Start(address string) error
}

func (s *httpServer) Route(pattern string, handlerFunc func(ctx *Context)) {
http.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) {
ctx := NewContext(w, r)
handlerFunc(ctx)
})
}

func NewContext(w http.ResponseWriter, r *http.Request) *Context {
return &Context{
W: w,
R: r,
}
}

这样修改之后我们的业务代码也显得更干净了。


func hello(ctx *server.Context) {
req := &helloReq{}
err := ctx.ReadJson(req)

if err != nil {
ctx.ServerErrorJson(err)
return
}

resp := &helloResp{
Data: req.Name + "_" + req.Age,
}

err = ctx.WriteJson(http.StatusOK, resp)
if err != nil {
ctx.ServerErrorJson(err)
return
}
}

RestFul API 实现


当然我们现在发现,不管用什么方法调用我们的接口,都可以正常返回。但是我们平常都习惯写restful风格的接口。


那么在注册路由时,自然需要加上一个method的参数。注册时候也加上一个GET的声明。


type Server interface {
Route(method string, pattern string, handlerFunc func(ctx *Context))
Start(address string) error
}

server.Route(http.MethodGet, "/hello", hello)

那么我们自然可以这样写,当请求方法不等于我们注册方法时,返回error。


func (s *httpServer) Route(method string, pattern string, handlerFunc func(ctx *Context)) {
http.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) {
if r.Method != method {
w.Write([]byte("error"))
return
}
ctx := NewContext(w, r)
handlerFunc(ctx)
})
}

那么我们现在就有了一个非常简单的可以实现restful api的服务了。


但是距离一个好用好写的web服务还有很大的进步空间。


作者:4cos90
来源:juejin.cn/post/7314902560405684251
收起阅读 »

做了几年前端,别跟我说没配置过webpack

web
引言 webpack中文官网:webpack.docschina.org/concepts/ webpack是一个javascript常用的静态模块打包工具,通过配置webpack,可以实现包括压缩文件,解析不同文件格式等强大的功能,作者发现很多同学对webp...
继续阅读 »

引言


webpack中文官网:webpack.docschina.org/concepts/


webpack是一个javascript常用的静态模块打包工具,通过配置webpack,可以实现包括压缩文件,解析不同文件格式等强大的功能,作者发现很多同学对webpack的认知都停留在入口出口以及简单的loader和plugin配置上,对webpack的核心原理都一知半解。本文期望通过更深层的解读,让读者能更彻底地理解这个打包工具的来龙去脉。


为什么要用webpack


在webpack等打包工具出世之前,我们普通的H5项目是怎么处理错综复杂的脚本呢?
第一种方式:引用不同的脚本去使用不同的功能,但脚本太多的时候会导致网络瓶颈
第二种方式:使用一个大型js文件去引入所有代码,但这样会严重影响可读性,可维护性,作用域。


举个栗子:
由于浏览器不能直接解析less文件,我们可通过引入转换的插件(file watcher)把less实时转换为css并引入,但项目里面会多出一个map跟css文件,造成项目文件的臃肿。


官方文档的说法:


node.js诞生可以让Javasrcipt在浏览器环境之外使用,而webpack运行在node.js中。CommonJS的require机制允许在文件中引用某个模块,如此一来就可以解决作用域的问题。


    const HtmlWebpackPlugin = require('html-webpack-plugin')

webpack 关心性能和加载时间;它始终在改进或添加新功能,例如:异步地加载 chunk 和预取,以便为你的项目和用户提供最佳体验


核心概念


webpack有7个核心概念:



  1. 入口(entry)

  2. 输出(output)

  3. loader

  4. 插件(plugin)

  5. 模式(mode)

  6. 浏览器兼容性(brower compatibility)

  7. 环境(environment)


新建一个build文件夹,里面新建一个webpack.config.js


入口entry


这是打包的入口文件,所有的脚本将从这个入口文件开始


单入口


    const path = require('path')
module.exports = {
entry: path.resolve(__dirname, '../src/main.js')
}

多入口


使用对象语法配置,更好扩展,例如一个项目有前台网页跟后台管理两个项目可用多入口管理。


    entry: {
app: path.resolve(__dirname, '../src/main.js'),
admin: path.resolve(__dirname, '../src/admin.js'),
},

输出output


打包后输出的文件,[name]跟[hash:8]表示文件名跟入口的保持一致但后面加上了hash的后缀让每次生成的文件名是唯一的。


单入口


module.exports = {
output: {
filename: '[name].[hash:8].js',
path: path.resolve(__dirname, '../dist'),
},
}

多入口


module.exports = {
entry: {
app: './src/app.js',
admin: './src/admin.js',
},
output: {
filename: '\[name].js',
path: \_\_dirname + '/dist',
},
};
// 写入到硬盘:./dist/app.js, ./dist/admin.js

loader转化器


用于模块的源码的转换,也是同学们在配置webpack的时候频繁接触的一个配置项。


举个例子,加载typescript跟css文件需要用到ts-loader跟css-loader、style-loader,
如果没有对应的loader,打包会直接error掉。


image.png


我们可以这么配置:先 npm i css-loader style-loader


module: {
rules: [
{
test: /\.css\$/,
use: ['style-loader','css-loader']
},
{
test: /\.ts\$/,
use: 'ts-loader'
}

必须留意的是,loader的执行是从右到左,就是css-loader执行完,再交给style-loader执行,


plugin插件


这是webpack配置的核心,有一些loader无法实现的功能,就通过plugin去扩展,建立一个规范的插件系统,能让你每次搭建项目的时候省去很多成本。


举个例子,我们会使用HtmlWebpackPlugin这个插件去生成一个html,其中会引入入口文件main.js。
假设不用这个插件,会发生什么?


当然是不会生成这个html,因此HtmlWebpackPlugin插件也是webpack的必备配置之一


const HtmlWebpackPlugin = require('html-webpack-plugin')

plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, '../public/index.html') // 同时引入main.js的hash名字
}),
],

模式mode


mode一共有production,development,node三种,如果没有设置,会默认为production


不同的mode对于默认优化的选项有所不同,环境变量也不同,具体需要了解每个插件的具体使用


选项描述
development会将 DefinePlugin 中 process.env.NODE_ENV 的值设置为 development
production会将 DefinePlugin 中 process.env.NODE_ENV 的值设置为 production。为模块和 chunk 启用确定性的混淆名称,FlagDependencyUsagePluginFlagIncludedChunksPluginModuleConcatenationPluginNoEmitOnErrorsPlugin 和 TerserPlugin
none没优化选项

module.exports = {
mode: 'production'
}

source-map 的解读


Sourcemap 本质上是一个信息文件,里面储存着代码转换前后的对应位置信息。它记录了转换压缩后的代码所对应的转换前的源代码位置,是源代码和生产代码的映射。 Sourcemap 解决了在打包过程中,代码经过压缩,去空格以及 babel 编译转化后,由于代码之间差异性过大,造成无法debug的问题


当mode为development时,devtool默认为‘eval’,当mode为production时,devtool默认为false。


sourceMap的分类



  • source-map:外部。可以查看错误代码准确信息和源代码的错误位置。

  • inline-source-map:内联。只生成一个内联 Source Map,可以查看错误代码准确信息和源代码的错误位置

  • hidden-source-map:外部用于生产环境。可以查看错误代码准确信息,但不能追踪源代码错误,只能提示到构建后代码的错误位置。

  • eval-source-map:内联。每一个文件都生成对应的 Source Map,都在 eval 中,可以查看错误代码准确信息 和 源代码的错误位置。

  • nosources-source-map:外部。可以查看错误代码错误原因,但不能查看错误代码准确信息,并且没有任何源代码信息。

  • cheap-source-map:外部。可以查看错误代码准确信息和源代码的错误位置,只能把错误精确到整行,忽略列。

  • cheap-module-source-map:外部用于生产环境。可以错误代码准确信息和源代码的错误位置,module 会加入 loader 的 Source Map。

  • eval-cheap-module-source-map: 内联,用于开发环境,构建跟热更新比较快


内联和外部的区别: 外部生成了文件(.map),内联没有。内联构建速度更快。


笔者用的两种配置分为是


// webpack.dev.js
devtool: 'eval-cheap-module-source-map',

// webpack.prod.js
devtool: 'cheap-module-source-map'

浏览器兼容性 brower compatibility


Webpack 支持所有符合 ES5 标准 的浏览器(不支持 IE8 及以下版本)。webpack 的 import() 和 require.ensure() 需要 Promise。如果你想要支持旧版本浏览器,在使用这些表达式之前,还需要提前加载 polyfill


环境 enviroment


本文使用的是webpack5 ,要求Node.js V在10.13.0+


Loader的汇总


笔者汇总了一部分常用的Loader以及其配置事项



  1. 浏览器兼容性:babel-loader

  2. css相关: css/style/less/postcss-loader

  3. vue: vue-loader



在配置loader前,先了解一下基本的配置



  • test: 匹配的文件,多用正则匹配

  • use: 使用loader,多用数组

  • exclude: 调整Loader解析的范围,包括某个路径下的文件,不如node_modules

  • include: 调整Loader解析的范围,包括某个路径下的文件


解决浏览器兼容性:babel


转义语法的babel-loader


譬如把const转为浏览器认识的var,虽然现在大部分主流浏览器都认识ES5之后的语法。


npm i babel-loader @babel/preset-env @babel/core

在rules配置:


{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
},
exclude: /node_modules/
}

转义ES API的babel/polyfill


如果只有babel-loader,浏览器并不能识别出新的API(promise,proxy,includes),如图:


image.png


因此还需要配置一个babel/polyfill,在入口里面:


// npm i @babel/polyfill

entry: ["@babel/polyfill",path.resolve(__dirname, '../src/main.js')],

解析vue的vue-loader


vue-loader: 解析vue
vue-template-compiler: 编译vue模板


npm i vue-loader vue-template-compiler vue-style-loader
npm i vue

在rules跟plugins配置:


const { VueLoaderPlugin } = require('vue-loader') // vue3的引入跟vue2路径不同

rules:{
{
test: /\.vue$/,
use: ['vue-loader']
}
},
plugins:[
...
new VueLoaderPlugin()
...
]

配置完成后,vue文件就可以正常解析了


image.png


解析CSS文件


需要引入的Loader不止一个



  • 引入的基本Loader: style-loader,css-loader,如有less还需要less-loader

  • postcss-loader 添加不同浏览器的css前缀: 解决部分css语法在不同浏览器的写法不同的弊端


modules.exports = {
modules: {
rules: [{
test: /\.css$/,
use: [
'style-loader',
'css-loader',
'postcss-loader'
] // 解析css文件必须的style-loader,css-loader
},
{
test: /\.less$/,
use: [
'style-loader',
'css-loader',
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [
[
'postcss-preset-env' // 解决css不同浏览器兼容性
],
],
},
}
}, 'less-loader'
]
},
}
}

拆分css


mini-css-extract-plugin: 把css拆分出来用外链的形式引入css文件,然后会在dist生成css文件,为每一个包含了 CSS 的 JS 文件创建一个 CSS 文件,
ps: 使用该插件不能重复使用style-loader


··· 
plugins: [
...
new MiniCssExtractPlugin({
filename: devMode ? "[name].css" : "[name].[hash].css",
chunkFilename: devMode ? "[id].css" : "[id].[hash].css",
}),
...
],
module:{
rules: [{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
// 'style-loader',
'css-loader',
'postcss-loader'
] // 解析css文件必须的style-loader,css-loader
}]
}

打包图片,字体,媒体等文件


file-loader: 就是将文件在进行一些处理后(主要是处理文件名和路径、解析文件url),并将文件移动到输出的目录中


url-loader 一般与file-loader搭配使用,功能与 file-loader 类似,如果文件小于限制的大小。则会返回 base64 编码,否则使用 file-loader 将文件移动到输出的目录中



{
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, //媒体文件
use: [{
loader: 'url-loader',
options: {
limit: 10240,
fallback: {
loader: 'file-loader',
options: {
name: 'img/[name].[hash:8].[ext]'
}
}
}
}],
include: [resolve('src/assets/image')],
exclude: /node_modules/
},
{
test: /\.(jpg|png|gif)$/i,
use: [{
loader: 'url-loader',
options: {
limit: 10240, // KB
fallback: {
loader: 'file-loader',
options: {
name: 'img/[name].[hash:8].[ext]'
}
}
}
}],
include: [resolve('src/assets/image')],
exclude: /node_modules/
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/i, // 字体
use: [{
loader: 'url-loader',
options: {
limit: 10240,
fallback: {
loader: 'file-loader',
options: {
name: 'fonts/[name].[hash:8].[ext]'
}
}
}
}],
include: [resolve('src/assets/icon')],
exclude: /node_modules/
},

使用多线程提升构建速度


js是单线程的工程,在构建工程的过程中,要消耗大量的时间在Loader的转换过程中,为了提升构建速度,这里使用了thread-loader将任务拆分为多个线程去处理。 其原理是把任务分配到各个worker线程中,之前多数人会使用happyPack,但webpack官方使用了thread-loader取代happypack。


...
{
test: /\.js$/,
use: [{
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env', ],
cacheDirectory: true,
}
},
{
loader: 'thread-loader', // 多线程解析模块
options: {
workers: 3, // 开启几个 worker 进程来处理打包,默认是 os.cpus().length - 1
}
}
],
exclude: /node_modules/
}
...

必须使用的插件Plugins


配置plugins必须注意的是,由于我们的模式(mode)区分为development跟production,因此plugins也需要按照实际需要,在config(公用),dev,prod三个配置文件分开加入。


首先先配置公用部分的plugins


公用plugins


清除打包残留文件


每次执行npm run build 会发现dist文件夹里会残留上次打包的文件,在打包输出前清空文件夹


const {
CleanWebpackPlugin
} = require('clean-webpack-plugin')



用外链的形式引入css


当一个html文件里面的css太多,全部把css添加到html中会显得很臃肿,那我们可以用mini-css-extract-plugin 把css拆分成外链引入,为每一个包含了 CSS 的 JS 文件创建一个 CSS 文件。


需要留意的是不能跟style-loader同时使用,下面用了hash


const MiniCssExtractPlugin = require("mini-css-extract-plugin");
....
plugins: [
new MiniCssExtractPlugin({
filename: devMode ? "[name].css" : "[name].[hash].css",
chunkFilename: devMode ? "[id].css" : "[id].[hash].css",
}, {
filename: devMode ? "[name].css" : "[name].[hash].less",
chunkFilename: devMode ? "[id].css" : "[id].[hash].less",
}),
]
....

module:{
rules:[{
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
// 'style-loader',
'css-loader',
'postcss-loader'
]
},
}]
}
....

生产打包后的html


wepback必备的插件之一,上述举例也有提到。
主要是生产打包后的html, 同时由于main.js文件会随机生成新的hash名字,html在引入main.js文件时频繁改名字会很浪费时间,此插件会自动同步改文件名


const HtmlWebpackPlugin = require('html-webpack-plugin')

plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, '../public/index.html') // 同时引入main.js的hash名字
}),
],

开发环境Dev


热更新: webpack-dev-server


当我们修改文件的内容时,要重新build一次才能看到变化,这样对开发的效率不友好。


需要留意的是,webpack-dev-server只是在开发环境搭建一个服务帮助开发人员提高开发效率,实现了实时更新的功能,在生产环境并不会用到这一个插件.


同时注意在plugins中加入webpack自带的HotModuleReplacementPlugin。


webpack-dev-server这个插件功能十分强大,官方文档有详细的记录
(webpack.docschina.org/configurati…)


npm i webpack-dev-server --save-dev

module.exports = {
devServer: {
// 基本目录
static: {
directory: path.join(__dirname, 'dist'),
},
// 自动压缩代码
compress: true,
port: 9000,
// 自动打开浏览器
open: true,
// 热加载,默认是true
hot: true,
},
plugins: [
new Webpack.HotModuleReplacementPlugin()
]
}

生产环境Prod


由于生产环境对性能的要求跟开发不同,需要引入的插件比较丰富,也更需要对项目构建有更高的熟悉程度


压缩Js文件


webpack mode设置production的时候会自动压缩js代码。原则上不需要引入terser-webpack-plugin进行重复工作。但是optimize-css-assets-webpack-plugin压缩css的同时会破坏原有的js压缩,所以这里我们引入terser-webpack-plugin进行压缩


option很多,使用了dropconsole去除打印的内容


const TerserPlugin = require("terser-webpack-plugin");

...

optimization: {
minimizer: [
new TerserPlugin({
terserOptions: {
compress:{
drop_console: true
}
}
}),
],
},
...


压缩CSS


前面有使用mini-css-extract-plugin的插件去拆分css,但这个插件并不能压缩CSS体积,
使用css-minimizer-webpack-plugin 可以压缩css的体积,但不同的是它是被加入到optimization的minimizer中,跟上述的js压缩插件共同作用


const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
...
optimization: {
minimizer: [
new CssMinimizerPlugin(),
new TerserPlugin({
terserOptions: {
compress:{
drop_console: true
}
}
}),
],
},
...


抽离第三方模块


使用DllReferencePlugin把不需要经常变更的静态文件抽离出来,譬如element-ui组件,这样每次打包的时候就不会再去重新打包选中的静态文件了。


如此一来,当我们修改代码后,webpack只需要打包项目的代码而不需要重复去编译没有发生改变的第三方库。这样当我们没有升级第三方库时,webpack就不会再对这些库进行打包,从而提升项目构建的速度。


首先我们在同级目录下新建文件webpack.dll.config.js,在entry的vendor里面配置了vue跟element-ui。


// webpack.dll.config.js
const path = require("path");
const webpack = require("webpack");
// 每次执行npm run build 会发现dist文件夹里会残留上次打包的文件,在打包输出前清空文件夹
const {
CleanWebpackPlugin
} = require('clean-webpack-plugin')

module.exports = {
mode: 'production',
// 你想要打包的模块的数组
entry: {
vendor: ['vue','element-plus']
},
output: {
path: path.resolve(__dirname, '../public/vendor'), // 打包后文件输出的位置,要在静态资源里面避免被打包转义
filename: '[name].dll.js',
library: 'vendor_library'
// 这里需要和webpack.DllPlugin中的`name: '[name]_library',`保持一致。
},
plugins: [
new CleanWebpackPlugin(),
new webpack.DllPlugin({
path: path.resolve(__dirname, '[name]-manifest.json'),
name: 'vendor_library',
context: __dirname
})
]
};



同时在packake.json里面配置dll的命令


"scripts":{
"dll": "webpack --config build/webpack.dll.config.js",
}

最后在webpack.prod.js 加入配置项


···
plugins: [
new webpack.DllReferencePlugin({
context: __dirname,
manifest: require('./vendor-manifest.json')
}),
···

随后执行命令npm run dll
在public/vendor会出现一个vendor.dll.js文件,我们需要在html文件引入这个文件.


<body>
<!-- dll插件的配置路径,注意是打包后的 -->
<script src="./vendor/vendor.dll.js"></script>
<!-- <img src="../assets/image/logo192.png" alt=""> -->
<!-- <img src="../assets/image/loginbg.png" alt=""> -->

<div id="app"></div>
</body>

配置完毕,这样我们在不需要更新第三方包的时候可以不用执行npm run dll,然后直接执行npm run build/dev的时候就会发现构建速度有所提高。


分析打包后的文件


使用webpack-bundle-analyzer,启动项目后会打开一个展示各个包的大小。从图中可以看出来,es6.promise.js这个包



  • stat size: webpack 从入口文件打包递归到的所有模块体积

  • parsed size: 解析与代码压缩后输出到dist目录的体积

  • gzipped size: 开启Gzip之后的体积


image.png


总结


webpack身为前端必备的一项技能,各位在学会基础的配置之后,千万别忘了因地制宜,看看哪些插件更适合自己的项目哦


作者:广州交租公
来源:juejin.cn/post/7277490138518159379
收起阅读 »

wordcloud,一个超酷的python库

微信公众号:愤怒的it男,超多Python技术干货文章。 一、简单介绍一下 词云图是文本挖掘中用来表征词频的数据可视化图像,通过它可以很直观地展现文本数据中地高频词,让读者能够从大量文本数据中快速抓住重点。如下图: wordcloud则是一个非常优秀的词云...
继续阅读 »

微信公众号:愤怒的it男,超多Python技术干货文章。



一、简单介绍一下


词云图是文本挖掘中用来表征词频的数据可视化图像,通过它可以很直观地展现文本数据中地高频词,让读者能够从大量文本数据中快速抓住重点。如下图:


图1.png


wordcloud则是一个非常优秀的词云展示python库,它支持自定义词云图的大小、颜色、字体等,甚至可以通过蒙版图片设置词云图的形状。因此,我们可以借助wordcloud轻松生成精美的词云图。


二、安装只需一行命令


pip install wordcloud

三、从一个简单例子开始


from wordcloud import WordCloud

text = "微信公众号:愤怒的it男"

wc = WordCloud(font_path='FZYTK.TTF', repeat=True)
wc.generate(text)
wc.to_file('wordcloud.png')

这里通过WordCloud类设置字体为方正姚体,背景颜色为白色,文本可以重复显示。生成WordCloud对象后,使用generate()方法将“微信公众号:愤怒的it男”生成词云图。最后,使用to_file()方法生成图片文件。


图2.png


四、细说wordcloud


WordCloud作为wordcloud库最核心的类,其主要参数及说明如下:


图3.PNG


这里以wordcloud库官方文档的constitution.txt文件作为数据,覆盖WordCloud类的各种参数设置用法,绘制出一张精美的词云图。


图4.PNG


首先,读入constitution.txt数据,并将数据清洗成空格分隔的长字符串。


import re

with open('constitution.txt') as c:
text = ' '.join([word.group().lower() for word in re.finditer('[a-zA-Z]+', c.read())])

print(text[:500])

图5.PNG


然后,在默认参数设置下,使用WordCloud对象的generate()和to_file()方法生成一张简单的词云图。


from wordcloud import WordCloud
import re

with open('constitution.txt') as c:
text = ' '.join([word.group().lower() for word in re.finditer('[a-zA-Z]+', c.read())])

wc = WordCloud()
wc.generate(text)

wc.to_file('wordcloud.png')

图6.png


以上词云图是在默认参数下生成的,简单粗糙不好看。接下来我们将对WordCloud的各种参数调整设置,不断地对以上词云图进行升级改造。


1、设置图片属性


设置图片宽为600,高为300,放大1.5倍,色彩空间为RGBA,背景颜色为。


from wordcloud import WordCloud
import re

with open('constitution.txt') as c:
text = ' '.join([word.group().lower() for word in re.finditer('[a-zA-Z]+', c.read())])

wc = WordCloud(
width=600,
height=300,
scale=1.5,
mode='RGBA',
background_color=,
)
wc.generate(text)

wc.to_file('wordcloud.png')

图7.png


2、设置文字布局


设置水平比例为1(即全部为水平文字),最多只显示100个词,停用词使用自带的词典(中文需要传入自定义的),相关一致性为0.3,文字布局为非随机,不允许重复词。


from wordcloud import WordCloud
import re

with open('constitution.txt') as c:
text = ' '.join([word.group().lower() for word in re.finditer('[a-zA-Z]+', c.read())])

wc = WordCloud(
width=600,
height=300,
scale=1.5,
mode='RGBA',
background_color=,
prefer_horizontal=1,
max_words=400,
stopwords=,
relative_scaling=0.3,
random_state=4,
repeat=False,
)
wc.generate(text)

wc.to_file('wordcloud.png')

图8.png


3、设置字体属性


设置字体为‘JOKERMAN.TTF’,最小字号为2,最大字号为150。


from wordcloud import WordCloud
import re

with open('constitution.txt') as c:
text = ' '.join([word.group().lower() for word in re.finditer('[a-zA-Z]+', c.read())])

wc = WordCloud(
width=600,
height=300,
scale=1.5,
mode='RGBA',
background_color=,
prefer_horizontal=1,
max_words=400,
stopwords=,
relative_scaling=0.3,
random_state=4,
repeat=False,
font_path='JOKERMAN.TTF',
min_font_size=2,
max_font_size=150,
)
wc.generate(text)

wc.to_file('wordcloud.png')

图9.png


4、设置蒙版


图10.PNG


设置微信公众号【愤怒的it男】头像的黑白图片为蒙版图片。


from PIL import Image
from wordcloud import WordCloud
import numpy as np
import re

mask_picture = np.array(Image.open('angry_it_man_mask.png'))

with open('constitution.txt') as c:
text = ' '.join([word.group().lower() for word in re.finditer('[a-zA-Z]+', c.read())])

wc = WordCloud(
width=600,
height=300,
scale=1.5,
mode='RGBA',
background_color=,
prefer_horizontal=1,
max_words=400,
stopwords=,
relative_scaling=0.3,
random_state=4,
repeat=False,
font_path='JOKERMAN.TTF',
min_font_size=2,
max_font_size=150,
mask=mask_picture,
)
wc.generate(text)

wc.to_file('wordcloud.png')

图11.png



微信公众号:愤怒的it男,超多Python技术干货文章。



作者:愤怒的it男
来源:juejin.cn/post/7317214007572807731
收起阅读 »

亲测实战大屏项目的适配方法

web
背景 想必在平常开发中,遇到要适配各种不同尺寸的屏幕、分辨率,甚至一套代码要兼容PC端到手机端对前端来说是比较头疼和麻烦的事情。 如果要在时间很紧迫的情况下去一套代码,要在大屏、PC和移动端都可运行,且界面不乱,也不超出范围;确实挺头疼。 我这里倒是有一个亲测...
继续阅读 »

背景


想必在平常开发中,遇到要适配各种不同尺寸的屏幕、分辨率,甚至一套代码要兼容PC端到手机端对前端来说是比较头疼和麻烦的事情。

如果要在时间很紧迫的情况下去一套代码,要在大屏、PC和移动端都可运行,且界面不乱,也不超出范围;确实挺头疼。

我这里倒是有一个亲测且实践的一个特别简单的方法,不用换算,设计给多少px前端就写几px,照着UI设计无头脑的搬就可,而且在大屏、电脑pc端、手机移动端,一套代码,就可运行,保证界面不乱,不超出屏幕范围。

这个方法是在我第一次写大屏项目时,用上的,当时时间也是很紧迫,但是项目上线后,对甲方来说很完美无缺了,一次过。

最后发现它在手机端也能看。

如果好奇想知道究竟是什么方法可以这么的丝滑去兼容大屏乃至任何尺寸任何分辨率的设备的小伙伴们,不妨接着往下看。


常见兼容各尺寸的方法


css媒体查询


@media screen

eg:


@media screen and (max-width: 1700px){
//屏幕尺寸小于1700的样式
}

这种方式,太麻烦,如果要求高,就要分的越细,几乎每个尺寸都要兼顾,写进去,实在实在是太太麻烦了。


viewport


<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,minimum-scale=1.0,user-scalable=no">

网页头部加入这段代码,网页宽度自动适应屏幕的宽度。

这种方式大屏中不太适合,这种更加适合移动端。

亲测,大屏不太适合。


rem


要换算,而且每次要根据屏幕尺寸的变化而去重新给一个新的根节点字体大小值。

还是麻烦,嗯,也是不能完美适合大屏。


vw和vh


把整个屏幕宽高分成一百分,一个vw就是1%的宽,一个vh就是10的高。

也是麻烦,而且也不能完美适配大屏。


百分百%


同vw,vh一样也不能完美适配大屏。


重点来了


最终我用了缩放,完美解决了大屏适配问题。

它不仅可以在任何尺寸任何分辨率下,去相对完美的展示,在PC甚至移动端也是可以看的。


如何?


jsx页面里:


 useEffect(() => {
//全屏模式
fullScreens();
//首次加载应用,设置一次
setScale();
//调试比例
window.addEventListener('resize', Listen(setScale, 100));
}, []);
//调试比例
// 监听手机窗口变化,重新设置
function Listen(fn, t) {
const delay = t || 100;
let timer;
return function () {
const args = arguments;
if (timer) {
clearTimeout(timer);
}
const context = this;
timer = setTimeout(() => {
timer = null;
fn.apply(context, args);
}, delay);
};
}

// 获取放大缩小比例
function getScale() {
const w = window.innerWidth / 1920;
const h = window.innerHeight / 1080;
return w < h ? w : h;
}

// 设置比例
function setScale() {
const bigMainDom = document.getElementById('bigMain');
bigMainDom && (bigMainDom.style.transform = 'scale(' + getScale() + ') translate(-50%, -50%)');
}

css:


 width:1920px;
height:1080px;
transform-origin: 0 0;
position: absolute;
left: 50%;
top: 50%;

想要哪个页面去做这种兼容,就可在哪个页面去这样设置;

利用查找id的方式找到对应的页面元素(这里只要能查找到元素就行,怎么写随自己),然后给对应的元素添加样式 transform ,去设置比例


getScale方法里面:

先求一下宽高的比例,因为设计稿一般使用的分辨率是1920和1080,所以这里使用这个取比例;

最后返回比例的时候,取比例比较小的,这样可以保证都显示出来;

只不过相对小的尺寸会出现空白,大的尺寸那个占满全屏;

js这里的translate(-50%, -50%) 和css里面的position: absolute; left:50%;top: 50%;transform-origin:0 0可保证画面在最中间


注意点:transform-origin:0 0这个很重要哦,用来设置动画的基点,没有这个,怎么都不会居中正常显示的哦。


否则就会出现以下这几个情况:


image.png


image.png


而:


// 设置比例
function setScale() {
const bigMainDom = document.getElementById('bigMain');
bigMainDom && (bigMainDom.style.transform = 'scale(' + getScale() + ') translate(0, 0)');
}

则会:


image.png
Listen方式,是专门设备窗口大小变化时,就会自动调用setScale方法,我设置的是每100秒监听一次,相当于做了一个自适应的处理;

setScale在页面加载时调用了一次,然后也加了一个resize方法;

宽度、长度也设置一样,我写的是1920 * 1080


出现问题,两边有空白?


屏幕没有铺满,左右有空白,这是因为设备上面有地址栏,而导致的;

因为地址栏占了一定的高度,但是设计稿把这块高度没有算进去,因此,实际的整体高度会比设计稿的大,而宽度是一样的,为了能够全部展示,就以大的为主,因此,宽度相对比就会出现空白啦;

image.png


解决


设置成全屏就好啦;


image.png

完美;

因为我的笔记本电脑分辨率高,所以,电脑默认缩放设置的是125%,而不是100%,默认设置为100%整体字体什么都都会变小,观看都不高,所以电脑默认设置为125%啦, 无论电脑默认是多少百分比,只要设置成全屏,都是全部铺满的。


image.png


设置全屏的代码


进入全屏、退出全屏、当前是否全屏模式的代码:

commonMethod.js:


//进入全屏
static fullScreen = (element) => {
if (window.ActiveXObject) {
var WsShell = new ActiveXObject('WScript.Shell');
WsShell.SendKeys('{F11}');
}
//HTML W3C 提议
else if (element.requestFullScreen) {
element.requestFullScreen();
}
//IE11
else if (element.msRequestFullscreen) {
element.msRequestFullscreen();
}
// Webkit (works in Safari5.1 and Chrome 15)
else if (element.webkitRequestFullScreen) {
element.webkitRequestFullScreen();
// setTimeout(()=>{
// console.log(isFullScreen());
// },100)
}
// Firefox (works in nightly)
else if (element.mozRequestFullScreen) {
element.mozRequestFullScreen();
}
};
//退出全屏
static fullExit = () => {
//IE ActiveXObject
if (window.ActiveXObject) {
var WsShell = new ActiveXObject('WScript.Shell');
WsShell.SendKeys('{F11}');
}
//HTML5 W3C 提议
else if (element.requestFullScreen) {
document.exitFullscreen();
}
//IE 11
else if (element.msRequestFullscreen) {
document.msExitFullscreen();
}
// Webkit (works in Safari5.1 and Chrome 15)
else if (element.webkitRequestFullScreen) {
document.webkitCancelFullScreen();
}
// Firefox (works in nightly)
else if (element.mozRequestFullScreen) {
document.mozCancelFullScreen();
}
};
//当前是否全屏模式
static isFullScreen = () =>{
return document.fullScreen ||
document.mozFullScreen ||
document.webkitIsFullScreen ||
document.msFullscreenElement;
}

这是react项目页面里引用的封装好的方法:


 //全屏
const fullScreens = () => {
if (!CommonMethod.isFullScreen()) {
Modal.info({
content: '开启全屏模式,体验更佳',
okText: '确定',
maskClosable: true,
mask: false,
// centered:true,
width: '200px',
height: '100px',
className: 'fullScreenModal',
onOk() {
CommonMethod.fullScreen(document.getElementsByTagName('html')[0]);
},
onCancel() {
CommonMethod.fullScreen(document.getElementsByTagName('html')[0]);
},
});
}
};

然后大屏页面第一次加载时,调用一次全屏模式方法即可,fullScreens


useEffect(() => {
//全屏模式
fullScreens();
}


注意点


如果在使用transform,缩放的地方,就不能使用其他定位,你会发现使用其他定位(绝对定位、相对定位、固定定位等)会失效;

使用antd里的模态框,就会失效哦。


完结


设置缩放的方法可以试试,并且在不同设备尺寸和分辨率下,大屏、pc、手机端都可以试一波。

回头我把自己上面那个项目在不同设备下运行的截图整理一下,补发出来。

对移动端有点要求的其实还会需要重新写一套代码的,我上面这种方式移动端只是样式没有乱也没有超出屏幕而已,毕竟这个只是专门给大屏做的代码,那么大的尺寸怎么的在手机端看也不会很符合的


作者:浅唱_那一缕阳光
来源:juejin.cn/post/7232229178278903865
收起阅读 »

Python中级知识梳理

1. 文件操作 Python中的文件操作通常使用内置的open()函数来打开文件。以下是一个简单的示例: with open("file.txt", "r") as f: content = f.read() print(content) 在...
继续阅读 »

image.png


1. 文件操作


Python中的文件操作通常使用内置的open()函数来打开文件。以下是一个简单的示例:


with open("file.txt", "r") as f:
content = f.read()
print(content)

在这个示例中,我们打开了名为"file.txt"的文件,并将其读入变量content中,最后将其打印出来。


open()函数的第一个参数是文件名,第二个参数是打开文件的模式。以下是一些常用的模式:



  • "r":只读模式

  • "w":写入模式(会覆盖已有文件)

  • "a":追加模式(不会覆盖已有文件)


2. 正则表达式


正则表达式是一种强大的工具,可以帮助我们从文本中提取信息或进行文本替换。Python中可以使用内置的re模块来进行正则表达式操作。以下是一个示例:


import re

text = "The quick brown fox jumps over the lazy dog."
pattern = r"fox"
matches = re.findall(pattern, text)
print(matches)

在这个示例中,我们定义了一个正则表达式模式r"fox",然后使用re.findall()函数来查找匹配该模式的所有字符串。最后,我们将匹配的结果打印出来。


3. 异常处理


在编写程序时,经常需要处理可能出现的错误或异常情况。Python中可以使用tryexcept语句来实现异常处理。以下是一个简单的示例:


try:
x = 1 / 0
except ZeroDivisionError:
print("Error: division by zero")

在这个示例中,我们尝试计算1除以0,这将引发一个ZeroDivisionError异常。我们使用tryexcept语句来捕获该异常并打印出一条错误消息。


4. 面向对象编程(Object-Oriented Programming)


面向对象编程是一种重要的编程范式,Python是一种面向对象的语言。以下是一个简单的示例:


class Person:
def __init__(self, name, age):
self.name = name
self.age = age

def say_hello(self):
print("Hello, my name is " + self.name)

person = Person("Alice", 25)
person.say_hello()

在这个示例中,我们定义了一个名为Person的类,并定义了一个构造函数__init__()来初始化对象的属性。我们还定义了一个名为say_hello()的方法,用于打印出问候语。最后,我们创建一个Person对象,并调用say_hello()方法。
Python是一种面向对象编程语言,它支持面向对象编程的三个基本概念:封装、继承和多态。面向对象编程的核心思想是将数据和操作数据的方法封装在一起,形成一个对象。


示例代码


class Animal:
def __init__(self, name, age):
self.name = name
self.age = age

def say_hello(self):
print(f'{self.name} is saying hello')

class Cat(Animal):
def __init__(self, name, age, color):
super().__init__(name, age)
self.color = color

def catch_mouse(self):
print(f'{self.name} is catching mouse')

cat = Cat('Tom', 2, 'White')
cat.say_hello()
cat.catch_mouse()

输出结果


Tom is saying hello
Tom is catching mouse

5. Python高级特性


5.1 生成器(Generator)


生成器是一种特殊的函数,可以在函数执行期间多次返回值,并且可以保留当前执行状态,等待下一次调用。它们是高效的迭代器,可以用于处理大量数据或无限流数据,同时也能节省内存。


示例代码


pythonCopy code
def fib():
a, b = 0, 1
while True:
yield a
a, b = b, a + b

f = fib()
for i in range(10):
print(next(f))

输出结果


Copy code
0
1
1
2
3
5
8
13
21
34

5.2 装饰器(Decorator)


装饰器是一种函数,用于修改其他函数的行为。它们提供了一种简单的方式来修改函数,而无需修改函数的原始定义。装饰器的常见用途包括添加日志、计时、缓存等功能。


示例代码


pythonCopy code
import time

def timer(func):
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f'{func.__name__} executed in {end - start} seconds')
return result
return wrapper

@timer
def calculate_sum(n):
return sum(range(n+1))

print(calculate_sum(100000000))

输出结果


Copy code
calculate_sum executed in 4.150076866149902 seconds
5000000050000000

作者:SandySY
来源:juejin.cn/post/7224335234010234935
收起阅读 »

分页合理化是什么?

一、前言 大家好!我是sum墨,一个一线的底层码农,平时喜欢研究和思考一些技术相关的问题并整理成文,限于本人水平,如果文章和代码有表述不当之处,还请不吝赐教。 只要是干过后台系统的同学应该都做过分页查询吧,前端发送带有页码(pageNum)和每页显示数量(pa...
继续阅读 »

一、前言


大家好!我是sum墨,一个一线的底层码农,平时喜欢研究和思考一些技术相关的问题并整理成文,限于本人水平,如果文章和代码有表述不当之处,还请不吝赐教。


只要是干过后台系统的同学应该都做过分页查询吧,前端发送带有页码(pageNum)和每页显示数量(pageSize)的请求,后端根据这些参数来提取并返回相应的数据集。在SpringBoot框架中,经常会使用Mybatis+PageHelper的方式实现这个功能。


但大家可能对分页合理化这个词有点儿陌生,不过应该都遇到过因为它产生的问题。这些问题不会触发明显的错误,所以大家一般都忽视了这个问题。那么啥是分页合理化,我来举几个例子:



它的定义:分页合理化通常是指后端在处理分页请求时会自动校正不合理的分页参数,以确保用户始终收到有效的数据响应。



1. 请求页码超出范围:



假设数据库中有100条记录,每页展示10条,那么就应该只有10页数据。如果用户请求第11页,不合理化处理可能会返回一个空的数据集,告诉用户没有更多数据。开启分页合理化后,系统可能会返回第10页的数据(即最后一页的数据),而不是一个空集。



2. 请求页码小于1:



用户请求的页码如果是0或负数,这在分页上下文中是没有意义的。开启分页合理化后,系统会将这种请求的页码调整为1,返回第一页的数据。



3. 请求的数据大小小于1:



如果用户请求的数据大小为0或负数,这也是无效的,因为它意味着用户不希望获取任何数据。开启分页合理化后,系统可能会设置一个默认的页面大小,比如每页显示10条数据。



4. 请求的数据大小不合理:



如果用户请求的数据大小非常大,比如一次请求1000条数据,这可能会给服务器带来不必要的压力。开启分页合理化后,系统可能会限制页面大小的上限,比如最多只允许每页显示100条数据。



二、为啥要设置分页合理化?


其实上面那些问题对于后端来讲很合理,页码和页大小设置不正确查询不出来值难道不合理吗?唯一的问题就是如果一次性查询太多条数据服务器压力确实大,但如果是产品要求的那也没办法呀!
真正让我不得不解决这个问题的原因是前端的一个BUG,这个BUG是啥样的呢?我来给大家描述一下。


1. BUG复现


我们先看看前端的分页组件



前端的这个分页组件大家应该很常见,它需要两个参数:总行数、每页行数。比如说现在总条数是6条,每页展示5条,那么会有2页,没啥问题对吧。



那么,现在我问一个问题:我们切换到第二页,把第二页仅剩的一条数据给删除掉,会出现什么情况?


理想情况:页码自动切换到第1页,并查询第一页的数据;
真实情况:页码切换到了第1页,但是查询不到数据,这明显就是一个BUG!


2. BUG分析


1. 用户切换到第二页,前端发起了请求,如:http://localhost:8080/user/pageQuery?pageNum=2&pageSize=5 ,此时第2页有一条数据;


2. 用户删除第2页的唯一数据后,前端发起查询请求,但还是第2页的查询,因为总数据的变化前端只能通过下一次的查询才能知道,但此时数据查询为空;


3. 虽然第二次查询的数据集为空,但是总条数已经变化了,只剩下5条,前端分页组件根据计算得出只剩下一页,所以自动切换到第1页;



可以看出这个BUG是分页查询的一个临界状态产生的,必现、中低频,属于必须修复的那一类。不过这个BUG想甩给前端,估计不行,因为总条数的变化只有后端知道,必须得后端修了。



三、设置分页合理化


咋一听这个BUG有点儿复杂,但如果你使用的是PageHelper框架,那么修复它非常简单,只需要两行配置
application.ymlapplication.properties中添加


pagehelper.helper-dialect=mysql
pagehelper.reasonable=true

只要加了这两行配置,这个BUG就能解决。因为配置是全局的,如果你只想对单个查询场景生效,那就在设置分页参数的时候,加一个参数,如下:


PageHelper.startPage(pageNumber, pageSize, true);

四、分页合理化配置的原理说明


这个BUG如果要自己解决的话,是不是感觉有点头痛了,但是人家PageHelper早就想到这个问题了,就像游戏开挂一样,一个配置就解决了这个麻烦的问题。
用的时候确实很爽,但是我却有点担心,这个配置现在解决了这个BUG,会不会导致新的BUG呢?如果真的出现了新BUG,我应该怎么做呢?所以我决定研究一下它的基础原理。


在com.github.pagehelper.Page类下,找到了这段核心源码,这段应该就是分页合理化的实现逻辑


// 省略其他代码
public Page<E> setReasonable(Boolean reasonable) {
if (reasonable == null) {
return this;
}
this.reasonable = reasonable;
//分页合理化,针对不合理的页码自动处理
if (this.reasonable && this.pageNum <= 0) {
this.pageNum = 1;
calculateStartAndEndRow();
}
return this;
}
// 省略其他代码

// 省略其他代码
/**
* 计算起止行号
*/

private void calculateStartAndEndRow() {
this.startRow = this.pageNum > 0 ? (this.pageNum - 1) * this.pageSize : 0;
this.endRow = this.startRow + this.pageSize * (this.pageNum > 0 ? 1 : 0);
}
// 省略其他代码

还有一些代码我没贴,比如PageInterceptor#intercept方法,这里我整理了一下它的执行流程图,如下:




看了图解,这套配置还挺清晰的,懂了怎么回事儿,用起来也就放心了。记得刚开始写代码时,啥都希望有人给弄好了,最好是拿来即用。但时间一长,自己修过一堆BUG,才发现只有自己弄明白的代码才靠谱,什么都想亲手来。等真正搞懂了一些底层的东西,才意识到要想造出好东西,得先学会站在巨人的肩膀上。学习嘛,没个头儿!



作者:summo
来源:juejin.cn/post/7316357622847995923
收起阅读 »

这下对阿里java这几条规范有更深理解了

背景 阿里java开发规范是阿里巴巴总结多年来的最佳编程实践,其中每一条规范都经过仔细打磨或踩坑而来,目的是为社区提供一份最佳编程规范,提升代码质量,减少bug。 这基本也是java业界都认可的开发规范,我们团队也是以此规范为基础,在结合实际情况,补充完善。最...
继续阅读 »

背景


阿里java开发规范是阿里巴巴总结多年来的最佳编程实践,其中每一条规范都经过仔细打磨或踩坑而来,目的是为社区提供一份最佳编程规范,提升代码质量,减少bug。

这基本也是java业界都认可的开发规范,我们团队也是以此规范为基础,在结合实际情况,补充完善。最近在团队遇到的几个问题,加深了我对这份开发规范中几个点的理解,下面就一一道来。


日志规约



这条规范说明了,在异常发送记录日志时,要记录案发现场信息和异常堆栈信息,不处理要往上throws,切勿吃掉异常。

堆栈信息比较好理解,就是把整个方法调用链打印出来,方便定位具体是哪个方法出错。而案发现场信息我认为至少要能说明:“谁发生了什么错误”。

例如,哪个uid下单报错了,哪个订单支付失败了,原因是什么。否则满屏打印:“user error”,看到你都无从下手。


在我们这次出现的问题就是有一个feign,调用外部接口报错了,降级打印了不规范日志,导致排查问题花了很多时间。伪代码如下:


	@Slf4j
@Component
class MyClientFallbackFactory implements FallbackFactory<MyClient> {

@Override
public MyClient create(Throwable cause) {
return new MyClient() {
@Override
public Result<DataInfoVo> findDataInfo(Long id) {
log.error("findDataInfo error");
return Result.error(SYS_ERROR);
}
};
}
}

发版后错误日志开始告警,打开kibana看到了满屏了:“findDataInfo error”,然后开始一顿盲查。

因为这个接口本次并没有修改,所以猜测是目标服务出问题,上服务器curl接口,发现调用是正常的。

接着猜测是不是熔断器有问题,熔断后没有恢复,但重启服务后,还是继续报错。开始各种排查,arthas跟踪,最后实在没办法了,还是老老实实把异常打印出来,走发版流程。


log.error("{} findDataInfo error", id, cause);

有了异常堆栈信息就很清晰了,原来是返回参数反序列失败了,接口提供方新增一个不兼容的参数导致反序列失败。(这点在下一个规范还会提到)

可见日志打印不清晰给排查问题带来多大的麻烦,记住:日志一定要打印关键信息,异常要打印堆栈。


二方库依赖



上面提到的返回参数反序列化失败就是枚举造成的,原因是这个接口返回新增一个枚举值,这个枚举值原本返回给前端使用的,没想到还有其它服务也调用了它,最终在反序列化时就报错了,找不到“xxx”枚举值。

比如如下接口,你提交一个不认得的黑色BLACK,就会报反序列错误:


	enum Color {
GREEN, RED
}

@Data
class Test {
private Color color;
}

@PostMapping(value = "/post/info")
public void info(@NotNull Test test) {

}

curl --location 'localhost/post/info' \
--header 'Content-Type: application/json' \
--data '{
"testEnum": "BLACK"
}'


关于这一点我们看下作者孤尽对它的阐述:


这就是我们出问题的场景,提供方新增了一个枚举值,而使用方没有升级,导致错误。可能有的同学说那通知使用方升级不就可以了?是的,但这出现了依赖问题,如果使用方有成百上千个,你会非常头痛。


那又为什么说不要使用枚举作为返回值,而可以作为输入参数呢?

我的理解是:作为枚举的提供者,不得随意新增/修改内容,或者说修改前要同步到所有枚举使用者,让大家知道,否则使用者就可能因为不认识这个枚举而报错,这是不可接受的。

但反过来,枚举提供者是可以将它作为输入参数的,如果调用者传了一个不存在的值就会报错,这是合理的,因为提供者并没有说支持这个值,调用者正常就不应该传递这个值,所以这种报错是合理的。


ORM映射



以下是规范里的说明:

1)增加查询分析器解析成本。

2)增减字段容易与 resultMap 配置不一致。

3)无用字段增加网络消耗,尤其是 text 类型的字段。


这都很好理解,就不过多说明。

在我们开发中,有的同学为了方便,还是使用了select *,一直以来也风平浪静,运行得好好的,直到有一天对该表加了个字段,代码没更新,报错了~,你没看错,代码没动,加个字段程序就报错了。

报错信息如下:



数组越界!问题可以在本地稳定复现,先把程序跑起来,执行 select * 的sql,再add column给表新增一个字段,再次执行相同的sql,报错。



具体原因是我们程序使用了sharding-jdbc做分表(5.1.2版本),它会在服务启动时,加载字段信息缓存,在查询后做字段匹配,出错就在匹配时。

具体代码位置在:com.mysql.cj.protocol.a.MergingColumnDefinitionFactory#createFromFields



这个缓存是跟数据库链接相关的,只有链接失效时,才会重新加载。主要有两个参数和它相关:

spring.shardingsphere.datasource.master.idle-timeout 默认10min

spring.shardingsphere.datasource.master.max-lifetime 默认30min


默认缓存时间都比较长,你只能赶紧重启服务解决,而如果服务数量非常多,又是一个生产事故。

我在sharding sphere github搜了一圈,没有好的处理方案,相关链接如:

github.com/apache/shar…

github.com/apache/shar…


大体意思是如果真想这么做,数据库ddl需要通过sharding proxy,它会负责刷新客户端的缓存,但我们使用的是sharding jdbc模式,那只能老老实实遵循规范,不要select * 了。如果select具体字段,那新增的字段也不会被select出来,和缓存的就能对应上。

那么以后面试除了上面规范说到的,把这一点亲身经历也摆出来,应该可以加分吧。


总结


每条开发规范都有其背后的含义,都是经验总结和踩坑教训,对于团队的开发规范我们都要仔细阅读,严格遵守。可以看到上面每个小问题都可能导致不小的生产事故,保持敬畏之心,大概就是这个意思了吧。


更多分享,欢迎关注我的github:github.com/jmilktea/jt…


作者:jtea
来源:juejin.cn/post/7308277343242944564
收起阅读 »

99年师弟,揭露华为工作的残酷真相

前几天,一位师弟约我吃饭,师弟跟我同校同一个专业,2022年本科毕业,比我低很多届,目前在华为工作。我们聊了很多母校的共同回忆,最近几年应届生求职的情况,也聊到了他在华为的工作经历。 师弟性格开朗,为人谦和,属于在学校各大社团特别混得开的那种,成绩也不错,保研...
继续阅读 »

前几天,一位师弟约我吃饭,师弟跟我同校同一个专业,2022年本科毕业,比我低很多届,目前在华为工作。我们聊了很多母校的共同回忆,最近几年应届生求职的情况,也聊到了他在华为的工作经历。


师弟性格开朗,为人谦和,属于在学校各大社团特别混得开的那种,成绩也不错,保研到本校,但拿到华为offer后,放弃了继续深造,选择直接就业。


聊到当今应届生求职现状时,感慨万千。


我的母校是一所普通211,部分专业比较有特色,在行业属于领先地位,但学校整体名气一般。


记得我们大四毕业时,就业行情还比较好,每个班都有好几个同学进入了华为。读研后,实验室的师兄师姐基本都拿到了华为offer,等我们开始找工作时,大部分同学也都拿了华为offer,华为基本属于大家的保底选择。


师弟去年毕业时,他是专业唯一拿到华为offer的学生。这几年的就业形势,本科生基本找不到什么好工作,除了继续读书深造,没有更好的选择。


短短的几年,社会瞬息万变,就像坐上了一辆过山车,身处其中的人,也只能仅仅抓住车身的杠子,在急速下坡时,不让自己摔下来。


图片


师弟进入华为,原本是一件很开心的事,短短一年,经历职场拷打,再也没有大学时代那份锐气。


华为属于员工人数最多的科技公司之一,每年吸收了大批校招生,但员工离职率也非常高。


师弟反馈,跟他同一批进去的人,很多干了几个月就离开了,他是那一批少数还留下来的人。


刚入职时,每个人都要参加新员工入职培训,那时候大家都很骄傲和自豪,也非常认同企业的价值观和文化。


但当真正的分到项目组开始工作时,发现大家实际工作情况,和价值观上说的,并不不太一样。在基层落地时,价值观更多是挂在墙上的标语。


领导也基本不太关心员工的成长和未来发展,更多是把每一个员工当作生产工具,哪一件工具用的顺手、更能出活儿,就会用谁;还没证明过自己的工具,或者经验匮乏的工具,很有可能会被无情抛弃。


华为的假期还可以,但大家有什么事,都不太敢请假。即便是生病,如果不是非住院不可,也不太会请假;公司支持请假,但大家还是轻伤不下火线,请假会影响出勤系数,各种与之相关的奖金,也会大打折扣。


……


师弟还分享了工作以来其他的一些心得体会,一句话总结:职场很残酷,相比起来,大学轻松太多了。


图片


在我看来,华为还是中国一流的科技公司。大部分人,还是要挤破脑袋才能进得去。


华为有很好的制度,比如员工转岗,门槛还不太高。华为支持不同工种互相转岗,比如,干技术的,有一天不想写代码了,可以转人力,或者转销售。开发太累,也可以转做测试。公司会提供基本的业务培训,还有三个月的考核期,只要达到一定的用人标准,就能转岗成功。


华为还有很好的奖励制度,只要能吃苦,且绩效不太差,能坚持下来,很多人最后都获得了丰厚的回报。比我高一两届的师兄师姐,还有跟我同一届的很多朋友,现在都还在华为,过得还不错。


华为还有比较完善的福利体系。在各地的办公区,远离市区,地价也很便宜,平时在公司会有夜宵、员工餐厅、下午茶和健身房各种福利,也有些地方,会有单身公寓。进入华为后,除了工作,也没什么社交,公司能满足日常的衣食住行。平时挣的钱,基本都能存下来,成家后,在华为附近买房,也基本衣食无忧了。


华为是一家跨国公司,如果是干技术支持或销售,还能享受去各国打卡的福利。在年轻的时候,能去更远的地方走一走,也是一件很宝贵的人生经历。


图片


华为是中国众多私企大厂的一个缩影,也是最早践行狼性文化的一家公司。


如今的字节、阿里等互联网企业,也在走华为的老路。


这些大厂,给了年轻人一个令人向往的机会,但并不适合每个人。如果我们能早些看清现实,自己只是大厂的一颗螺丝钉,需要做的是,尽快掌握螺丝钉需要具备的技能,与此同时,如果借助大厂的势能,多开眼界,多认识一些各个领域的前辈和牛人,把企业能提供的资源用到极致,就更了不起了。


至于在公司里寻找朋友、寻找归属感或者安全感,那并不是一个好选择。同事间的八卦,公司股价的涨跌,CEO的致辞,也并不是我们需要关心的事。


最重要的是,无论在大厂还是小厂,在国企还是私企,都要拥有良好的心态。最怕的是,在国企羡慕华为的工资;在大厂,又羡慕国企的朝九晚五。


人生在世,各有各的不易。每一种选择,都没有对错, 选择了其中一个,在另外一部分,就得有所取舍。


在任何时候,学会“自洽”,与生活和解,与自己和解很重要。


作者:工程师酷里
来源:juejin.cn/post/7316349124600168484
收起阅读 »

一个简单截图API,月收入2千美金

大家好,我是程序员凌览。又看到一个独立开发变现的故事,来分享下。 以下是变现分享内容: 大家好,我是Dmytro Krasun, screenshotone的创始人,这是一个简单的API,可以将任何网站的URL转换为网页截图。 人们经常惊讶于你可以通过自动截...
继续阅读 »

大家好,我是程序员凌览。又看到一个独立开发变现的故事,来分享下。


以下是变现分享内容:


大家好,我是Dmytro Krasun, screenshotone的创始人,这是一个简单的API,可以将任何网站的URL转换为网页截图。



人们经常惊讶于你可以通过自动截图网站来建立一个业务。如果你想在你的应用程序中显示任何网站的截图,那么就需要我的产品。


我才刚刚开始,但已经成功地将我的“小”产品发展到月收入2200美元。



你是如何想到这个主意的?


在开始我现在的产品之前,我是一个服务器端开发人员,工作了大约10年,薪水还不错。那时我写了15年的代码。

我有一些业余项目,我想尝试成为一名企业家。但在我工作的时候很难发布任何东西。我总是完全专注于我的日常工作,这是优先考虑的事情。这可能就是为什么我成长得很快,赚了不少钱。


一个机会几乎是“偶然”出现的。我家有了一个新宝宝,我休陪产假,我想这可能是一个好时机,我可以把时间花在抚养孩子、建立和启动一个项目上。这是一个艰难的决定。


最终,我决定毫无计划地跳进未知的世界。可能,因为我只是疯了,这“感觉”像一个有趣的冒险。


我是一名服务器端开发人员,在构建api方面有着丰富的经验。我坐下来写下我遇到的所有问题。我选择了一个随机截图API。我决定在运行中验证它,看看是否有需求。


然后我开始建造它。我谷歌了一下,发现有很多竞争对手,而且都是不错的。一开始我很失望。但后来我意识到,这意味着市场已经得到了验证,我只需要在细分市场打造出最好的产品。


请向我们介绍构建第一个版本的过程


在2022年1月5日买了一个域名,建立了一个简单的网站,开始写与我未来产品相关的内容。看看我的第一个丑陋版本的登陆页:



我写下了我在构建产品时遇到的任何问题,以及如何通过使用我的产品来更快更便宜地解决这些问题。


例如,我的截图API允许你在渲染截图时隐藏广告和cookie横幅,所以我写了如何自己免费做到这一点,并推荐我的产品作为替代方案。事实证明,这是一种推动转化率的有效方法。


seo优先的方法帮助了我,5个月后,当我发布产品时,我已经有了流量,我可以利用它来盈利。


我开始构建一个仪表盘应用程序,让潜在客户可以试用产品、查看使用历史、升级计划和配置通知。它是由Go渲染的纯HTML、CSS和JavaScript。我是一名服务器端开发人员,不知道如何使用现代JavaScript框架。


我将产品的第一个版本托管在Digital Ocean droplet上,然后当我开始增长时,我开始使用Render,然后需要更多的弹性服务器,并转移到谷歌云平台上。一周前,我启动了一个新的Kubernetes集群,以减少我在Google cloud Platform上的成本使用,它已经运行良好。


描述创业的过程


当我的产品的第一个版本准备好时,那是在2022年5月底,我已经从谷歌获得了一些相关流量,因为在发布之前创建了内容。但这还不足以促成销售。在Reddit, Indie Hackers,论坛,目录和Twitter上写文章。

最后,我总结了我在Indie Hackers的第一个营销月所做的一切。这是当月最热门的帖子,并迅速传播开来。


我几乎要放弃了,我累了。最终,在2022年7月4日,我迎来了第一位付费客户。我永远不会忘记这一点。我在Twitter上的朋友Jannis正在为创作者创建一个工具目录,并希望自动截图工具。


如果我今天重新开始,我不会从SEO开始。我会快速构建一个带有付费链接的原型,并尝试着将其展示给潜在的感兴趣的人。SEO是一个长期的游戏,它需要一个长期投入的工作。


是什么吸引并留住了客户?


根据我目前对市场营销的理解,没有什么灵丹妙药。你需要找到2-3个可以持续获得客户的渠道。除了实验,没有其他方法可以做到这一点,看看什么有效。


对我来说,搜索引擎优化、谷歌广告和推特都很管用。但我测试了Reddit、Indie Hackers、Twitter Ads、LinkedIn和其他平台。它们都是有效的,但问题是你可以反复从哪一个中获取客户?


我在Indie hacker上分享了我简单的SEO策略——我基本上是快速创建内容,获得流量,分析并更新内容。


对于X (Twitter),在早期,我积极推广我的产品,到处提到它。但感觉很尴尬,所以我就不再那样做了。开始专注于帮助人们,解决他们的问题,回答他们的问题。人们开始知道我在做什么,如何找到我。现在看到别人会主动去推荐我得产品。



我的主要流量来源是Twitter (X)和Google。我在Twitter上有1万名粉丝,这有助于推广我的产品。在搜索引擎优化上付出了很大的努力,现在我得到了回报。我的产品在细分市场中最具竞争力的关键词排名前5位,比如“screenshot API”之类的。


我认为没有捷径可走。无论如何,尝试每一种方法,找到最适合你的方法。


对其他创业者有什么建议吗?



先把你的野心放一边,试着一个SaaS软件或一个简单的应用程序来赚第1美元。一旦赚到钱,试着赚10美元,然后是100美元。不断重复,直到你确定创业真的是你想要的,并且你喜欢所发生的一切,不管结果有多难。


一旦你意识到这是你喜欢做的事情,不要放弃。如果你赚了1美元,10美元,然后100美元,你不放弃,那么一切皆有可能,只有天空才是你的极限。


别听任何人的建议,包括我的。只有你自己决定,如果你最终决定回到一份正常的工作,这是你的生活,你只能活一次,请保持自己的快乐。


"先把你的野心放一边,从赚第1美元开始"


作者:程序员凌览
来源:juejin.cn/post/7315586629308121140
收起阅读 »

Vue 2 最后之舞“天鹅挽歌”

web
大家好,这里是大家的林语冰。老粉都知道,我们之前已经在《Vue 2 将死》中已经提及 Vue 2 今年年底将全面停止维护,且最终版即将发布,只打补丁,不再增量更新任何新功能。圣诞节前夕,平安夜之际,Vue 团队正式官宣 —— Vue 2 最后一个补丁版本&nb...
继续阅读 »

大家好,这里是大家的林语冰。老粉都知道,我们之前已经在《Vue 2 将死》中已经提及 Vue 2 今年年底将全面停止维护,且最终版即将发布,只打补丁,不再增量更新任何新功能。

圣诞节前夕,平安夜之际,Vue 团队正式官宣 —— Vue 2 最后一个补丁版本 Vue@2.7.16 正式发布,版本代号“Swan Song”(天鹅挽歌)。

01-swan.png

地球人都知道,去年 Vue 2 官宣了最后一个次版本 Vue@2.7.x,如今 Vue 2 官宣最后一个补丁版本 Vue@2.7.16,也算是为 Vue 2 的最后之舞画上惊叹号!此去经年,再无 Vue 2。

虽然但是,前端踏足之地,Vue 亦生生不息,此乃“Vue 之意志”。故本期《前端翻译计划》一起来重温 Vue@2.7 的官方博客,为 Vue 生态的未来规划未雨绸缪。

00-wall.png

今天我们十分鸡冻地官宣,Vue 2.7(版本代号“火影忍者”)正式发布!

尽管 Vue 3 现在已经是默认版本,但我们特别理解,由于依赖兼容性、浏览器支持的要求或带宽不足无法升级,仍然有一大坨用户被迫滞留在 Vue 2。在 Vue 2.7 中,我们向后移植了 Vue 3 中的某些特色功能,Vue 2 爱好者也可以有福同享。

免责声明

本文属于是语冰的直男翻译了属于是,略有删改,仅供粉丝参考,英文原味版请传送 Vue 2.7 "Naruto" Released

向后移植的功能

  • 组合式 API
  • SFC 
收起阅读 »

2023年终,找寻和回顾我在世界存活过的痕迹

有幸拜读了诸多掘友的年终总结,那些压力、纠结、卷、焦虑,我仅透过文字就感受到了,所以我又写回了上一年的主题了~ 我没再去写什么总结,反省啊,我只想轻松再带着些许俏皮的回顾一下2023年我在世界上存活过的痕迹,欢快些吧,生活总是要活下去的。 有时候,我们健康的活...
继续阅读 »

有幸拜读了诸多掘友的年终总结,那些压力、纠结、卷、焦虑,我仅透过文字就感受到了,所以我又写回了上一年的主题了~


我没再去写什么总结,反省啊,我只想轻松再带着些许俏皮的回顾一下2023年我在世界上存活过的痕迹,欢快些吧,生活总是要活下去的。


有时候,我们健康的活着,我们已经很棒啦


这篇文章我更多的抱着分享生活的想法去写的,不谈及那些重的话题,更像是活在当下的人吧


所以如果你是想要获取一些总结上的经验或者一些思考,可能我这篇年终文并不那么适合你。



博客较长,文字较多,希望朋友你能慢慢的读,犹如生活要慢慢的过


今年有幸在湘江边、珠江边和黄浦江边三地留下足迹,三处不同的场景,三种不同的生活状态,不一样人生阶段的结束和开始。



起伏跌宕的开始😀😟


和朋友一起在长沙度过了2022年最后一天,不知疲倦的在那个五一广场,一群人看着手机,数着秒表,大声呼喊着度过了2022年的最后一刻。


可时至今日,去年一起跨年的朋友,现在已经相隔千里了,而今年多半是在房间里面一个人度过这2023年的最后一刻啦


想到了去年写的那句“去年陪在你身边的人,如今还在你身旁吗?一年过完了,你的生活有变化吗?今年你过得开心吗?
人山人海,矮一丢丢,走进去就出不来啦。


人从众2023的新年时刻
image.pngimage.pngimage.png

回忆起那时的场景,调皮的人们在附近的高楼放起了鞭炮,气球在人们手上释放,一同奔向星空


(可惜这一幕我已经没有原视频啦,现在突然回忆起那个时刻,才发觉时间真的好快好快,一晃而过啊)


说起来,那还是我第一次在长沙看到那么那么多的人,也从没想过一个地方的退场,需要走半小时,打车更别说啦,都直接交通管制啦。


看起来这一切都像是美好的开始啊


但在那一晚的九点,我接到了来自家里的电话,让我抽空回家一趟,看一看最疼爱我的爷爷。


可殊不知,那是我和他的最后一次交谈了。


只愿世间永无病痛”。


长大后,那深夜里的电话🥺


长大后,才知道深夜里的电话,无一例外都是大事。


那是在某晚凌晨4点时接到我父亲打来的电话。


其实看清是我父亲打来时,我就清楚知道,小时候最疼爱我的爷爷可能去向了另外一个世界了。


当时的我真的算是非常平静,开始思考工作相关的事情,准备和领导请假,订高铁票,我以为我真的会非常平静的


stk


图一:等地铁


但当离家越来越近时,我才明白有些情绪是没有办法隐藏住的,有些崩溃就在某一瞬间


image.png

图二:回家的路


我看着我的奶奶流泪,我觉得她好无助啊,那一瞬间,我崩溃了。奶奶陪伴我爷爷一生,却剩她独自去过这余下的时光,(我小时候是和爷爷奶奶一起长大的,一直到我上小学之后,但那之后我每年的寒暑假也都会回到乡下去陪爷爷奶奶,只是不知道从什么时候开始,变得不再那么想要回去了)


奶奶很小就嫁过来了,可以说我爷爷就是她拥有的一切啦,儿女子孙大多在外拼搏,她自己也不愿意去麻烦儿女子孙了,所以说,这余下的日子大都是她一人度过了


有很多时刻,我都不愿去深想,那将是怎样的一个生活。在写这一段的时候,我非常想给她打一个电话,但是我的奶奶她已经非常听不清啦,只有偶尔父亲回去时,通过我父亲的传递才能更好的交流。


所幸在去年爷爷奶奶还算健康时,我回到家里陪他们。那一次我非常认真去和他们促膝交谈,安安静静的听他们说他们当年的故事给我听,而我也有幸做了一份录音,只是日后不知道有没有去倾听的勇气


今年回去我有一些想法了, 比如去给奶奶买一个助听器,再去装一个可以对话的摄像头,这些我终于都可以去付出实践啦,我现在有些期待过年啦


那时的我其实也写下了许多文字,关于病痛、选择、亲情、离世、我的父亲、我的奶奶、亲人、还有村里各位长辈,也有很多很多思考,但可能不那么适合写出来吧


只能说在有那么一刻,我深刻且贴切的感受到了亲情的冷暖和选择的无奈吧


也有那么一瞬间的快乐吧,看着五岁的侄女,开开心心的,不免也有些被感染,彷佛从她的身上,才能看到那些无忧无虑的快乐啊 image.png


小时候日日期盼的长大,长大后才知道长大是这一生最大的苦痛啊


短暂的相聚时间啊 😒


自从进入社会,开始成为一名社畜后,寒暑假的消失,让能待在家里的时间开始变得愈来愈短了。


好像每一年的开始,我们的时间就已经被定好啦


以前的过年,其实最好玩的是过年前的那段时间,家里开始筹备年货,准备各种各样的吃的,熬夜打游戏、看电视,一起嗑瓜子啊,和朋友们一起聚会啊,但现在已经变得越来越少啦。


回归中心,继续聊这个短暂的相聚时间吧,文字可不是拿来抱怨的呀~


今年回去,我也开始从我父亲那接过啦一些活,比如准备年货,包蛋饺等等,还有一些杂食,都交给我来做,在那么一刻感觉自己长大啦


另外每年大家来我家干饭的时候,总是要喝酒,我老爸又喝不了酒,就是我来陪酒,今年我几个弟弟,直接把我喝趴啦,麻啦麻啦


不过也只有在家里可以那么那么放松啦,出门在外,又能有几回这样的时刻呢


返回社畜岗位前,把自己的房间收拾了一番,晒给大伙看看吧


部分小玩偶房间里的书桌
image.pngimage.png

当时租房里面的小书桌,这个小桌子,也陪伴了我将近小一年,2022年的博客大部分是在这个小书桌上敲出来的,也是我看电影、刷剧的小书桌啦~


image.png

轻松愉快的时光 😊


生日、吃饭、按摩、记录生活、离开前的放松时光


清楚的记得,这是我今年第一篇日记朋友圈的开始,写的很长很长,还放了自己的自拍照


顺颂时宜,百事从欢


打游戏&按摩篇


身边有个社牛朋友,快乐有时候会变得很简单啊,笑点也会随之一降再降啊,哈哈。


我俩在游戏城的时候,他拿全部彩-票换了把M24,他说:“这是他绝地求生,丢了所有装备都要捡的枪”,他拿到枪后的第一时间就是看看哪里有小孩子,说这必须得让他们看看,男人至死是少年,哈哈哈。


image.png

观影篇


夜晚一起去按摩的时候,看了电影《人生大事》,借用一句豆瓣的影评:“人生除死无大事,天上的每一颗星星,都是爱过我们的人”。 在今年的年初,我也亲眼看到了一颗星星缓缓上升啊,请各位要早点爱自己所爱的人,人生总是在不经意间就会留下遗憾但是我们就是不要……不要留下太多遗憾啊。 在一切都还来的及的时候,去爱你身边的一切,去尝试生活,去绽放自己的光芒,那就是属于你的人生大事啦


后面还去看啦流浪地球,我能说的是“十分震撼人心,这是一部值得我推荐的电影,如果有可能的话,一定要选择 IMAX 影院,观影体验会更佳,也更能展现这部影片的大制作”。这部真是为数不多让我在朋友圈中写上这么主观的电影观后感的片子啦。


image.png

脱口秀篇


第一次去观看了线下的脱口秀表演,那时候我才知道,原来脱口秀演员,大都数是兼职的。


观看体验的话,具体得看演员玩的是什么样的梗,有优雅的,也有不太优雅的,还有一些内容是16岁以上的朋友们才适合明白的,遇上一个能活跃气氛并且能和观众们互动的表演者的话,体验就会非常好,如果平平淡淡的话,就会显得有点枯燥啦。如果要想深度体验,就要坐第一排,那样乐趣会多很多,不过前提是需要能够接受调侃


(补充:一定要遇到开合适段子的脱口秀表演的演员,有些重口味的,是有点难受的)


哦对啦,还遇到了一位杭州的朋友赠送了一份属于ta的运气王的票和夜宵套餐


image.png

干饭篇 🍻


干饭篇(一),长沙的《一盏灯》,主打一个辣,不过味道还是蛮不错的,哈哈哈


image.png

干饭篇(二)名称忘记啦


这是第二天我生日时,拉着朋友一起去了另外的苍蝇馆子


那天聚会的餐桌旁,有一位同年同月同日生日的伙伴,我的社牛朋友就拉着隔壁桌的朋友一起给我唱生日快乐歌,让我也给人家唱生日歌,我只能说当时我的脚趾在扣地啦


不过也算是有缘认识到新的朋友


image.png

即使许久之后再看到那天的视频和图片,也仍然觉得是有趣和有缘


干饭篇(三)北二楼大排档


接脱口秀篇的夜宵套餐,这家店味道是真不错,值得我推荐。


哈哈哈,来长沙可以去试一下,味道和价格都可以,唯一的缺点就是排队排的太久啦,当时我们两个好像从夜晚八点排到了夜晚十一点,中间去了那个HIB HUB公社酒吧溜达,我们两个去看男孩子跳舞,哈哈哈哈


我是真佩服那会的我们,吃的是真多啊,这我们两个人的吃的量...


image.png

干饭篇(四)烤肉店,店名忘记啦


我们一伙人在一起,主打一个笑点低,啥事都能笑半天,我这种高冷boy直接被同化成搞笑boy


image.png

干饭篇(五)喝酒&夜宵


这个忘记是哪一天啦,只隐约记得,凌晨三点多,一起喝完酒,和朋友一起在街边买烤串和麻辣烫,那时候是真放纵自己啊,现在是吃了晚饭,就坚决不吃东西啦,我要减肥啊啊啊啊


清吧喝酒街边小摊的夜宵
image.pngimage.png

美术馆&看展 🖼


在长沙的时候,一个人去了长沙美术馆—齐白石先生的画展,在馆内看到了曾经语文教科书上那画得惟妙惟肖的“


我本没有艺术天赋,去看展大都是静心养气,瞎逛闲逛,但有些美,它就是美得非常直接,直击你的心灵。


有些画本身就真的非常美,你一眼看上去,就会立马喜欢上,换我也想收藏这些画😃


云海夕阳蚂蚱独身一人
image.pngimage.pngimage.pngimage.png

总的说来,这可能是我这一年来最放松的一个月啦吧,这么吃喝玩乐的原因也是朋友准备离开长沙啦,就聚一聚,在长沙逛一逛,吃一吃。我也非常开心,看到自己有留下这么多的瞬间和记录。




在此时此刻写下这些文字时,我才感觉自己有真实的在这个世界上活过一样。


裸辞 | 年少轻狂或是年少无知吧 🤡


在去年我就有考虑离职,但一直没有行动,主要一方面是能力不足,第二个是没有足够的存款,无法承担裸辞后的风险。


但直到我正式提出离职,其实我也还没有准备好,只是觉得拖得太久啦,不能再内耗下去啦


离开


我最终还是决定离开了我职业生涯中的第一家公司。


image.png

总的来说,其实这家公司各方面还蛮不错的吧,位于长沙、双休、不加班、偶尔也有个下午茶。当然这都是不谈薪资的情况下,哈哈哈。




只言片语聊一聊离开的原因吧。


关于离开,这个想法其实在进入这家公司数周后就有啦。熟悉一家公司所要花费的时间,真的没有想象中那么长,几周到一月,大致就可以把所属团队的技术栈,技术水平、团队氛围、领导性格、团队主要工作方向都了解的差不多。而通过这几点,大致就可以决定或影响你在这个团队中所待的时间啦


对于我这种年少无知的年轻人,那个时候我最看重的是技术水平,如果让我觉得没有技术可以继续学下去啦,就会下意识认为继续待着是没有意义的啦。


但是换到现在来说的话,有几人又能够一直做技术基建工作呢?在小公司又能做什么样的基建呢?另外IT人这么多,大家都卷技术,又能卷赢谁呢?无疑都是为了生存下来罢啦


另外很大程度上是业务推动技术发展,你如果没有十万级的数据,坦白说你日常开发中,真的不会老是去想怎么优化SQL,改数据库配置之类的,没有百万千万级,很少有机会去思考去实践分库分表这些操作,不得不说,平台真的非常重要的。


我现在还记得去年我写到一篇文章,聊到过“团队>领导>个人努力”,优秀的团队,放手让你干的领导和努力的自己




说回这小节的中心吧,我跳出属于自己的那个舒适圈,有以下几点原因吧:


1、年轻气盛。想的是年轻再不出来看看,以后混不下去了,退路都没有啦


2、学习技术。想要学习更多技术相关的知识,薪资可以少,但吃饭的技术不能停滞不前。(不过现在看来,还是蛮天真的,现在是有个班上就行,哈哈哈哈)


3、完整的软件开发流程。想要经历从零到一的项目,项目需求、文档、画图等等,都想要去了解一遍,而不仅仅是开发代码(事实证明,浪浪山外还是浪浪山,只是山大了一点,爬山的人多了一些。)


4、薪资问题。(这个对于当时的我算是问题也不算是关键问题吧,当时真觉得是只要能获得成长,我就觉得非常OK,现在让我选的话,哪家给的多就去哪家啦😂)


心路历程


团队内交流。最开始对职场是陌生的,这个要离职的想法并没有太藏着,团队的老哥都知道我这个想法,当时都挺照顾我,也没谁介意这个问题,后面在真的到离开的时候,老哥们提醒我,下次如果再离职,千万不要声张,悄无声息的就好。后面仔细想想确实是这样,你大大咧咧的说,比较影响公司团队氛围。


与人事交谈。最开始正式知道的我想要离职的第一个人是人事,因为人事和我谈岗位调整的问题,需要增加一个运维的工作给我,附带调整薪资。但那个时候我,我已经想要提离职了,就不想在交接工作时,再附加其他问题。我就提出了近期可能会离职的说法,提了之后,我的身份就变得十分敏感啦。那会人事有事没事就问我,打算什么时候离职啊之类的。现在回想起来是真不应该啊,说了之后导致该调的薪资也没了,也被大领导关注到我要离职的事情啦。


与大领导谈话。人事知道我的想法后,就向上汇报啦(与大领导认识,他是想让我之后接实施的活,后续与他和项目经理一起去到项目上)。他了解完我离职的想法,先是肯定,后谈薪资,谈未来工作,定军心,提出挽留;不过最后我还是婉拒啦,抱着那一丝对大城市的向往跑出来啦吧


与直属领导交谈。最后和直属领导了提出了正式离职的想法,与大领导类似的,也是支持和提出挽留,我都一一拒绝啦,最后只有支持和祝福啦。


正式离职。交接工作、清理账号权限、格式化文件、领导签字、人事签字,拿到离职证明,和团队中的各位伙伴说再见,也是下次再见。


坦白说,当走出公司门,还是有那么一些不舍吧,就感觉以后不会再踏及这个地方,只能是一段回忆的感觉啦。


短暂的轻松快乐刚离职的时候还能的听《蓝莲花》,还能打一打王者。后面真的是...


出远门前,还回了一趟家,和老爸老妈待了几天。


无意中聊起年龄、结婚和生孩子的问题,才发现我家老爸老妈转眼间也老了,白发也开始显眼起来了。上次聊到这个话题还是在朋友家听她谈到相亲的话题,说她的父母五十来岁,年龄再大些,站到台上就不那么美丽啦。


心情也莫名沉重了几分~


那时候想到今年的Flag之一是带着父母出去外面按按摩,洗个脚,在那一刻想的却是今年要多挣点钱,过年回去的时候,陪他们一起去医院好好做个完整的体检,好让他们在之后的岁月中可以平安健康的享享清福。


不过现在看来是都容易啊。


最后的最后也只是回家简单吃了两顿饭,然后就踏上了去远方的旅程啊。


也许我们回家和离家的路总是开心的,可父母都是站着门前望着,一个是望着回来,一个是目送着你的离开。


羊城飘荡 🛫


到啦这边之后,投了差不多两三周之后,情绪心态已经完全不一样啦


那份来时的轻松自在已不再了,虽不至于坐立难安,但也是焦虑丛生啦。


不过认真说起来,在羊城的那段找工作的时间,虽然没找到工作,但说真的其实开心还占了多数,可能还是和那群一起爱搞事的兄弟们。


image.png

坦白说,写出来,我感觉有些小伙伴都不太会相信,在一室一厅的房间里面,最多的时候一起住了五个大兄弟,哈哈哈哈哈哈。


当时都是碰巧辞职或者是想在羊城或者鹏城找个工作,就从最开始的两个人,一步一步的凑到了五个人。


那会可以说整个房间可以说是能利用的都利用上啦,房间里的地板、木板床、沙发都睡着人。


突然想到啦那句“三块是面子,四块是生活。不是小瓶买不起,而是大瓶更有性价比。”


我们则是”不是房租出不起,而是一起住更有性价比“,也感受了一番城中村的生活


image.png

在那间房里我还有另外一个身份,”大厨“,那时候也靠这件事情转移我的注意力,同时也算是造福几个哥们,给他们做了一段时间的饭菜。也只有在做饭的时候,我不用去想找不到工作的焦虑


落地羊城后的第一顿饭我第一次做九转大肠
image.pngimage.png

生活也蛮有意思的,凌晨夜话;失眠拉着大家一起发疯;翻来覆去睡不着时,就要问一嘴谁还没睡,起来聊会天;一起出去玩,逛动物园、博物馆、广州塔、大佛寺、相亲角等;还有夜晚出去散步一起喝蜜雪冰城,哈哈哈哈


逛相亲角还蛮有意思的,以前都是在网上看,真来到线下,看到那一张张如同商品挂在上面的个人介绍,压力顿感庞大,优秀的人真滴是太多啦。


夜晚的小蛮腰大厂
image.pngimage.png

广东省博物馆动物园
image.pngimage.png

明年有机会应该去看一看上海市博物馆。


另外还凑了一波雪王的不倒翁,来到魔都也想继续凑的,但是这边没有卖,遗憾。


image.png

非常感谢有他们,我才能够较为开心的度过了那段十分焦虑的求职时期吧


后来的后来,我因为身边其他兄弟引荐,机缘巧合下来到了魔都;一个则找工作去了新疆;还有一个也留在羊城了,不过已经不再从事编程这一行业啦


真正意义的相隔千里了,只有在年前年尾才能短暂的再相聚了呀




在羊城的时候,机缘巧合下,博客有帮助到一名网友,碰巧也在广州,就线下一起面基啦。


当时他还在中国建设银行实习,一起吃了个便饭,聊了很多,溜达了一圈,看了广州塔,我也有不少收获。


关于这一点我多补充一下,多和行业内的小伙伴们交流,我个人觉得真的是有意义的,大家可能都是做开发,但业务可能不同,技术栈可能不同,背景不同,很多想法也是不一样的,每次交流可以收获一些以往不知道的知识吧。


离开后,已许久未曾联系啦,希望一切安好和顺利!祝福


人这一生就是在不断的遇见和离开,珍惜每一次的相遇,或许就是我们能做到的最棒的事情啦


辗转流离 🚄


为啥说是辗转流离勒,还要听我细细道来啊。


长沙裸辞


从长沙裸辞,我就回了趟家,看啦看我的老爸老妈,在家真的只能待三天。再久,那个经就会念起来啦,哈哈哈。所以三天一到,我就踏上了去往羊城的道路啦。


当时是还没被社会毒打,心情还不错,后面真的是心态一崩再崩😐


现在想一想,我都觉得当时的自己是真勇敢,换现在的我,真的不敢裸辞啦,手上没余钱和找不到工作的焦虑真的太折磨人啦。


羊城飘荡


还是多亏有兄弟照顾,来到这边,有个住的地方,没有太多的经济压力,只要认真投简历,找工作就好。


即使后面投简历投到心态爆炸,也因为有兄弟们一起陪着,让那些焦虑生了又灭了。


如果心里有事情,是可以找一找身边愿意倾听的朋友说一说的,当然不是长期输出负面情绪,而是适当的说一说,憋在心里容易出问题


鹏城溜达


要离开前,去鹏城溜达了一遍,和我的表哥表姐吃了一顿饭。因为离开广深地区,下次再相聚也就只能是过年啦啊。


我姐的小女儿,超级可爱的小侄女~


写到这里的时候,我还想起了当时答应她,下次去要给她带玩具,我还写了备忘录,过年要准备准备啦


image.png

image.png


和我老哥晚上吃夜宵,但没想到的是,这顿生蚝将是我今年吃的最爽的一次🤡


(xxxx,上海夜宵店上的生蚝,真不知道是个什么超级刺客价格,真的该死,我这还是在深圳商场里的夜宵店里面吃的,又大,而且比起上海便宜好多,我真的气死啦)


烧烤鲜甜的生蚝
image.pngimage.png

转战魔都


转战魔都前,我又跑回家躺了两天,你没看错,我又回家躺了两天,其实也是感觉如果来了魔都,可能要等到过年才有机会回去啦【事后确实如此】


然后从湖南出发去魔都~


昂贵的高铁费上海虹桥站来接我的科科ikun之家
image.pngimage.pngimage.pngimage.png

我这个小黑子成功加入ikun之家,开启啦在ikun之家的生活~


来到上海后,重新整备后的书桌:


image.png

工作 | 新的启程 🐱‍💻


转变


来到魔都后,工作方向略有改变,不再是单纯的Java开发吧,没法单纯的说是什么样的工作吧,定位也不够准确。


另外也是目前没什么大的成就,就不太想谈论工作这个话题,明年慢慢去更新更多的文章,会一步一步谈论到现在的工作吧


不过我还是活跃在一线开发岗位上的,最让我苦恼的是垂直领域深度不够,非常烦啊。


但这边的整体团队氛围、管理风格都让人比较放松,工作也相比以前开心许多,有一段时间让我一度找回了开发的乐趣。


来到这边后,有几个方向的提升吧。


1、公司提供了更大的平台,接触到了更多的新事物;


2、负责的事情要比之前多;


3、技术的横向扩展拉得比较广。就是有点大网捞鱼,鱼全部漏掉了的感觉,头大。


4、扁平化的管理,让我更大胆的去做事


总的来说,是非常棒的,我也在努力的将手上的事情去做好,也在寻找工作上的乐趣,而不是麻木的工作


image.png

下班等电梯时的随手拍,剪刀手boy~


因为目前正在岗位上,有些内容是没有办法写出来的~


补充


浅浅的谈论一下求职这个问题


我在去年和今年年初都有求职的经历,只差一年,但感受相差甚远。


求职


去年的情况还只是有些糟糕,但还能约到面试,到处也还在招人。


今年离职出来的时候,可以说是寒气逼人,先说我自己,我是陆陆续续投了一个月,投了1.5k左右,能约到的面试少之又少,贴几个朋友的求职经历:


朋友1,求职前端开发,base:深圳、广州、厦门,boss投递3k+,4个月还没找到份前端开发的工作,最后转行啦;


朋友2,求职Java开发岗位,base:深圳、广州,boss投递1k+,最后找了个外包去了新疆;


朋友3,求职前端开发,base:深圳,找了近3个月,最后转回安卓,上个月才收到一个安卓岗位的offer;


朋友4,求职Java开发,base:成都,有美团短期实习经历、银行6个月的实习经历,在成都找了3个月,上个月才入职新的公司,转向 kotlin 开发啦


上述都是求职初中级开发,年限1-3年




收简历


这边公司有开放过岗位,因此有过收简历的经历,浅谈一下,当时两周左右,看了将近300份的简历,本科生研究生的简历都不少,就我个人看简历的心态变化大致是以下几个阶段:


最开始:每一份简历都看的非常认真。看技术栈、项目经历,比较需求匹配度,后面再看学校如何,判断是否进一步交流;


中间阶段:每天都收,真的太多啦,有时候看都看不完(有工作,可能是每天晚上回到家才看),开始转变成,没有get到需求可能就忽略了


后期:只要基本满足条件,学历优秀的优先谈。不行就下一个。主要会去关注下面几个问题,基础条件是不是满足;相关的技术栈的熟练程度;进行前期沟通,了解性格,初步判断能否融入当前团队;入职后,能不能发挥他应有能力;稳定性等方面的问题;


小小的思考
今年我求过职,同时又收过不少简历,后面就补了这么一个小节。


找工作难,招人也难


说实话,如果没有实际工作经历,看十份简历,有六份简历中的内容基本上大差不差的。


你说怎么选。


以前是想着跳槽涨工资,或者是想找个好工作,今年的想法是能有个b班上就不错啦。


魔都生活 | 出游 | 干饭


在魔都的日子,出门时间其实不算多,工作日的三点一线,休息日的步数100,总的来说,不是在干饭的路上,就是在干饭的桌上


外出篇


大魔都的标志性建筑物--东方明珠塔。


美是真的美啊,也是来到了外滩才深刻感受到金融到底多么挣钱,外滩这边一条街,全部都是xxx银行,感觉没有一栋楼,都不好意思说自己是个大银行啦。


白昼时的东方明珠傍晚时的东方明珠
image.pngimage.png

夜景更美一些,要不是上塔顶的票太贵,我感觉我已经冲啦


豫园


国庆假期时过去溜达的,当时好好的感受了一下上海的City Walk


挹秀楼阁楼
image.pngimage.png

有没有觉得第一张照片中阁楼里的两个闪光灯非常有趣呀~


广富林遗址和醉白池公园


天高云淡时,和室友一起外出骑行,去看看外面的风景~


广富林遗址醉白池含苞待放
image.pngimage.pngimage.png

游泳


和两个小伙伴一起去游泳。不对,他们两才是游泳的,我是去喝盐水,是的,我这个小菜鸡还是不会游泳,哭死。


image.png

不过我对于魔都这座城市知道的还是太少,那么多馆,那么多展,那么多景点,但我是动都不想动啊。


希望明年可以去做更多探索吧,如果有小伙伴一起的话,那就更好啦。


干饭篇 😲


在写这一小节的时候,我想我终于找到我自己反向减肥十斤的原因啦🤡


十天的照片里,八天的照片都是关于吃的… 我自己都不敢相信


同事聚餐、在家做饭、周末小聚、生日聚会、夜宵、外出游玩干饭、疯狂星期四,各种各样的干饭,上演各种碳水炸弹


可是今年原来的的目标要减肥的啊啊啊啊啊啊啊🥺


偶尔周末给自己和室友加餐的日子


干饭加餐篇
image.pngimage.png
image.pngimage.png

与团队中的小伙伴一起在住房里面小聚一下,再加上一点小酒,不免也是一个快乐时光


热时多数是炒菜,转凉后多数是吃火锅,夏天小龙虾和啤酒更配哦,冬天则是牛羊肉更暖身啦


聚会篇繁忙工作里的畅谈和轻松
image.pngimage.png
image.pngimage.png

大声呼喊着减肥,每天和室友一起吃荞麦面,还一起跳绳,但。。。


实际情况是工作日每天吃荞麦面,一到周末就是夜宵、KFC、聚餐,妥妥的反向减肥


image.png

夏天时,自制青柠和百香果的饮料果汁喝,非常爽哦


青柠百香果
image.pngimage.png

(别看我说的这么好,实际上我把所有装备买齐,就做了三次,哈哈哈哈)


外出干饭,聚会~


补充:黄酒初喝不上头,喝多啦该醉还是醉,下次我要坐小孩子那桌去


温暖你的猪小餐馆淞沪名灶和记小菜
image.pngimage.pngimage.pngimage.png

安徽


今年有幸到过两次安徽,一次是去了黄山,一次是去了合肥,尝试了安徽的菜,我觉得也蛮好吃的,可能也是偏向于辣和咸,挺合适我这个湖南人滴


总的来说,感觉内陆省份的菜品都偏向于重油、重盐和重辣,而沿海省份,感觉就是鲜、淡、甜,吃食材的本味~


忘记名称的店脑海中只记得吃啦地锅鸡
image.pngimage.pngimage.png

我们这群人属于是,遇上开心的事情,烧烤配啤酒;碰上不开心的事情,也是烧烤配啤酒!!


开心烧烤配啤酒不开心也是烧烤配啤酒
image.pngimage.png

疯狂星期四


以往都是说段子,后面真是每周都过疯狂星期四,碳水炸弹


image.png

细细想来,我的长胖好像也是有迹可循的,没写出来前,我日常还觉得我自己吃的不多😂


总的来说,在上海的周末做的最多的事情就是干饭啦。


室友篇


从个人生活状态转变到合租生活状态,其实有不少改变的。


不过非常幸运的是,在这里遇到的每一位室友都非常棒,也分别给我带来的不一样的影响


首先第一点,室友们都是大厨,这才有了上述那种每天吃吃喝喝的生活。


如果要是我一个人独居的话,吃饭在我看来更可能一种任务,随便应付一下就算是完事啦,根本不会去想着一荤一素,更不会想着好好做个菜啦,哈哈哈




keke哥哥最开始来到魔都时,我没有另外再出去租房啦,就是和keke哥哥一起睡。因为他有考虑后面要搬出去和女朋友一起住,所以最开始我们就是睡一起的;坦白说,如果他女朋友没来,他最后没搬出去住,我感觉我们两个男生才像谈恋爱的感觉【捂脸】


生活非常同频。今天我买菜做饭,他就洗碗;隔天他买菜做饭,我就洗碗;作息时间同频;有问题会直接说;一起电动车上下班;周末一起去游泳;一起吃KFC;半夜睡不着一起骑电动去吃夜宵;一起养猫;一起打游戏,cf、王者五排;安静互相不打扰,多数时候是一起同行的。有那么片刻,恍惚间有感觉像是在谈恋爱的。


这种生活一直持续到他女朋友到来,他搬出去之后才结束,说起来还是有点怀念啊。


要是谈恋爱的时候也能是这种感觉,可能我现在也不会一直单身着啦吧




wancheng哥哥。这个叫法是因为我某一天打王者,听到一个男生在用那种夹子音叫哥哥,我一下就学会啦,然后就整天在他们的名后面加个哥哥,哈哈哈,最终的结果就是整个房间里,所有人都被我带偏啦~


wancheng哥哥是这里的大厨,要是每次有个什么聚会,肯定就是我们两个搭配掌勺,不过我们两单独某一个人都懒,但要是一起合作去做某件事情,就会非常认真的去做,想要去做好,而不是偷懒。


还有他今年经常带我一起消费,笑麻了。他买机械臂,就会非常真诚的向我安利它,陈述它的一些优点,把我说的心动,然后就是买买买;冬天来啦,他买暖风机,也会非常真诚的说这款产品的优缺点等等,最后我又忍不住去剁手;还有其他的,我自己都被自己笑麻啦。另外大家都偏向于及时享乐,我也有被影响吧,多了很多消费,也比以前果断许多啦。


对啦,最最最重要的一点,他现在不减肥啦。然后他现在是直接往住房里疯狂买零食,放在客厅的桌子上,他知道我一定会去吃,我真的哭死,他真是生怕我瘦下来啦。周末吃完饭还要再点个烤串,减肥减肥,越减越肥~


不过也有带动我共同提高。因为他老是来查我房,来查我在干嘛,是不是在打游戏;如果我是在卷的话,他也会立马回到房间去卷。当然我也会去反向查他的房,他如果不在下棋,我也会立马回到房间卷,坦白说,是有点小孩子行为的。


补充:要非常感谢wancheng哥哥,还有未曾蒙面的阿姨,才让我们在魔都有吃不完的湖南特产,剁辣椒、腊肉、腊牛肉、腊肠、萝卜条、豆鼓,对啦还有扣肉等等,真的太棒啦




另外还有一个我此前并非那么了解的室友-sian哥哥,在今年给过我很多帮助。在和他的诸多交谈中,我是真的受益匪浅,也有重塑他在我心里的印象


在这里暂且称呼他是个游戏佬好啦。在我以前十分局限且带有偏见的认知中,对游戏佬的印象都比较一般。人的专注是有限的,沉迷于某一个事物当中,那么自然对其他事物的关注肯定不会那么高啦。


很长一段时间里,我认为阅读是最容易带给人深度思考的,但是在和他交流的过程中,我发现,其实我们专注做的每件事情,都是值得思考的。


比如他热爱建城类的游戏(请原谅我已经忘记游戏名称了),可能部分玩家是去追求那张掌控城市的爽感的,他在我看来,则是那种享受自己从零建造一切的成就感的。在那么一刻我突然意识到,在游戏中完成一个从零到一的任务,换成我们编程的说法,也就是从零到一完成了一个项目啊。仔细停下来想一想,是不是这样


他做的更好,他将这种从零到一的思维方式,从游戏带到了现实生活中,在和他共事的过程中,我发现他的思考和规划大多是从全局到局部的。


这点我是非常想要学习的,因为我个人的观念,老是非常局限,不管做什么,我第一个想到的,总是站在自己的位置上去思考问题,跳不出去,无法抛开自己的固有思维去思考问题犹如井底之蛙。


另外他是我身边朋友圈里,和我一样,不刷抖音的人,都是b站的深度用户,喜爱那种能够引导人进行深度思考的长视频。


不过话说回来,我感觉我今年比起往年心浮气躁了许多,以前还能心平气和的看完长视频,今年已经要加速啦。


朋友篇


今年有幸在魔都认识到一位非常真诚的朋友,见证过ta的勇敢,也见识到的ta的真诚,也有被ta写的一段话所惊艳到。


“我一生都在探索人性,我知道现实残酷人性凉薄,所以我任愿抱以真诚去对待所有人、事。


我所付出的那些金钱和精力,又比如你说我以后会吃的亏,在我看来远不及一次灵魂碰撞来的重要。


同样真诚的人难能可贵。


这条路艰难,我必定会历经诸多碰壁。


我可以被拒绝可以被推开,我都不在意,但唯独不能不真诚。”


生活状态的转变 🏄‍♂️🤼‍♀️


写完上面那部分内容,我在思考一个问题,今年的生活状态和去年还是有很多不一样的。


我在湖南的时候,多数周末还是会出去走一走;但是在上海,外出的欲望是真的低啊,直接进化成宅男啦


思考了几个原因:


1、交通不便。目前在魔都比较偏的地方吧,附近没有地铁口,只能坐金山铁路


2、外出通勤时间太长。因为没有地铁,出去一次,动辄一个小时、一个半小时,出行欲望极低。来上海近半年,准确来说我只坐过一天地铁,那时候好像还是是火山引擎的一个活动,在市中心那边的一个酒店,那天我一个人过去的。其他时候好像完全没坐过地铁。捂脸,甚至于有时候同事聊到上海几号线几号线,我完全不知道在哪。


其他时候,和室友们一起出去玩的话,要么打车,要么踩单车,要么找公司财务借车。公共交通除了当时坐轨道交通去游泳,好像其他真没坐过啥啦。回想起来,我自己也觉得有点离谱啦


3、从个人居住状态转变到合租状态。开始习惯和与人同行,而非个人独行,以往自己要干嘛,我可能自己想啦就去坐了,当习惯有人同行后,我会下意识问ta愿意一起吗,如果他不愿意,我的意愿会降低。


4、脑子暂停转动😵。一个人住的时候,要做什么,想去干嘛,都是自己在思考,与室友一起住后,思想开始懒惰啦,比如吃饭,总想问一句他想要吃什么,不想自己去动脑子思考啦。


类比到我自己打王者也是一样,一个人单排,我会去想整体局势如何,该怎么打,人在哪,和朋友一起打,我就直接开摆,成为机器人,哈哈哈哈。


我仔细想了想,我并没有因为没有减少外出溜达,而变得不快乐,也没有讨厌宅在家里的自己,我觉得这种生活状态是适应当下的我。




但是我觉得我还是要做出一些适当的改变,如果长期不动自己的脑子去做决定的话,我感觉我会丧失掉自己的主动意愿,之后就更不愿意去思考啦,有些事情,想做就做啦~


《杂乱无章》 🖋


在今年有能力的情况下,这次终于可以好好的支持喜欢了很久的《杂乱无章》啦


购买了一些周边,其实不太贵,就正常的衣物、香水、护手霜、鞋等等。


不过我老是给身边朋友安利,感觉自己已经很久很久没有做过这样的事情啦,其实就是非常单纯的想要分享自己所喜欢的宝藏吧。


一直在等正常装的香水,可惜一直没有货一个人也要穿情侣装
image.pngimage.png

新品护手霜,味道非常好闻的 image.png


我关注《杂乱》,没记错应该是在两年前啦,现在回想起来我已经不记得我为什么会关注到它,但是我记得当时看完第一篇文章就点了关注,然后一口气把当时的历史文章全部看了,就是非常喜欢他们输出来的文字。


喜欢文字的朋友,可能也会喜欢上他们吧。


另外今年,他们在广州开展啦,可惜的是,不在江浙沪地区,不可惜的是,我让一个朋友去逛了这个展。


下面是展馆里的个别片段,希望来年能够来到魔都,不行的话,江浙沪地区就好,哈哈哈


文字
我们都需要适度抽离,谈恋爱要一直要有这个状态才行,偶尔的距离感,会让人舒适许多。image.png
希望你开心,无论和谁,不管在哪image.png
成年人除了容易胖,还容易加班,容易熬夜,还容易在杂乱无章里找到同感,真的太对啦image.png
给自己的生活留一些属于自己的片段image.png
《心情》“过年了!” “又过年了。“image.png

不知道,你有没有在在上述的几幅图中呢?你又是哪种情绪呢?


猫猫相伴的时光 🐱🐱‍👓


养猫,真的是一个极费照片的事情,哈哈哈


我在这之前,很长一段时间都没拍过啥照片啦,后面和室友一起养之后,可以说是,照片以一个非正常量级的速度往上递增,哈哈哈哈


image.png

它的存在,我相信是真的能治愈人的,上班一整天,回到家,看到它,那些愁绪会消散一大半


七月刚回来时,那时候还是娇小可爱勒 image.png


image.png

现在在室友饲养下,已经是胖嘟嘟啦,变得更可爱。 image.png
虽然最后,是让它和室友一起和室友女朋友出去住啦


但最后的效果是好的,它已经成功的胖嘟嘟啦,很开心遇见和陪伴,现在偶尔也能去看一看,也算是非常快乐啦


掘金瞬间


八月Docker技术征文一等奖


不知不觉中在掘金也待了两年多啦,时间真的很快啊,同时也代表着我写网络博客也近两三年啦


用心对待的博客,被掘友们用心认可,真的非常快乐,非常感谢每位阅读过的朋友


让我记录一下这个高光时刻吧~


有在多个平台写过博客,但最让我喜欢的,还是掘金


很多原因吧,有掘金的大方,也有因为掘金而相识的朋友,大家不仅是在掘金上有交流,也是彼此的微信好友,接触的越多,就越习惯这里吧,如果有一天不再玩掘金,那么可能就是这些朋友也在逐步退圈啦吧


image.png

图一:Docker技术专题的实体奖牌


也非常感谢斗金小姐姐第一个来告诉我这个好消息,不知道斗金小姐姐能注意到这句话吗,哈哈


要是其他运营小伙伴看到啦,劳请转达一下下啦,还有记得给来个推荐,哈哈哈


image.png

图二:在奖金的基础上加了些给自己淘来的4k屏幕


后续也有多个活动想参加的,有可能是懒吧,也有可能是心浮气躁,总之今年的博客数量对比起2022、2021年,差之甚远


2023年统计2022年统计2021年统计
image.pngimage.pngimage.png

真的是每年比折半还折半,明年我一定要努力克服🤡🥺


与友人见面 | 人间有味是清欢


因博文相识的朋友,时隔许久,终于有幸在线下见面啦


开始,可能还是要提起掘金,哈哈哈,那会加上好友,还是因为同时在掘金的“神转折滴评审团”中~


image.png

在这之后,其实成为了互相的读者和粉丝,ta的文章内容简短,偏向于日常思考、感悟和读书笔记。


话说ta的日更是我当时早上的摸鱼读物,哈哈哈哈


我个人也偏好去阅读其他人的想法,想了解其他人对于某件事情是如何看待的。


ta的文章也让我在寻找自信以及与自己和解等方面,荣获不少的成长。



去年写到”我们是彼此的正向反馈”,那么今年就是”幸会“。



“杭城,幸会”


人间之幸事,遇良师益友,品人间有味,游山河大地


在这月准备写年终博文时,在回忆在思考,今年有哪件事是想做而未做的,最后才有啦这次的见面和交流~ 很感谢那晚的思考和感性的自己,才有今天关于杭城的人和事的记忆。 或许这趟出行,是略有唐突的,(因为我的到来,让朋友有不小的心理负担,特意规划了去哪里,从那里走到那里,非常细心),辛苦啦,庆幸的是,最后都收获满满~


幸会”,有预想过双方的见面,虽然都感受对方都是极易相处的人,但是也不免有一丝不安吧


但真实情况远比起预想中要好,可能是我因为成功的把我那份社牛属性释放出来啦吧。


当文中那个积极向上、热爱生活的形象真实立体出现在眼前时,有那么片刻的不真实吧,同时也非常激动;另外所期待的聊天,也贯穿于整个同行的过程,十分放松和愉快的一天,不用去这想那的。


期待下一次吧~


回忆起整个交流过程中有些问题我还存惑,不过我已经想好要再次拾起阅读啦


人间有味。一方面是尝了那份老杭州的美食味道,另一方面是品鉴了那份属于杭城的老味道。日常总是忙于工作,已经很少有过沉下心来去感受那些明明离我们很近,但又久久未曾感受的地方啦。


山河大地自然是杭州西湖啦。不过那时未曾记起苏轼先生的诗句,一开口便只能是,wc,好美,哈哈哈(已然决定要去b站学习一下关于东坡居士的历史啦),下一次再踏足时,希望自己能欣赏到另一种更深刻的美。


补充:中间也有过许多搞笑瞬间和尴尬的瞬间 比如下面👇的照片,已然是我摆的比较好的姿势了;不熟悉杭州路况,我老是走神开错道;还有最后找停车的地方,下到车库,都没想起自己车停在哪里,还信誓旦旦走错啦,回忆起来,我真的要被自己的信誓旦旦给整麻啦,现在想想都蠢死啦,还不相信你,麻啦;表情管理更不用说了,直接全放开,现在回头看照片,我真的😁😂🙄😵🤡


标题
image.pngimage.png

西湖边上拿着旁边的小朋友的饼干喂鸳鸯,哈哈


image.png

在我很喜欢的一个公众号《杂乱无章》的一篇推文中,看到了下面的这段话,我觉得是非常契合这次的见面


image.png


如果说见完面之后的想法是什么的话,一方面可能是想要继续去拾起阅读吧,觉得自己言语谈吐仍然有需要进步的地方;另外一方面就是想要改变下自己的仪态吧(在相机中的自己,实在是称的上难看啦,哈哈哈哈)


不管是和哪位好友的见面,我想我们都会永远期待下一次的见面吧


记录生活 | 观影 | 阅读 | 消费


今年总的来说,比起以往,是多了很多记录的,也是想用数字去量化,看看自己的变化。


比如关于看电影、剧,我今年线上线下观影和追剧总的来说是45部吧。有2/3我是写下了观后感的,还有三分之一要么是看完太晚,懒得写,要么纯粹就是懒得写啦。我看的大部分都集中于评分高的经典电影和突然刷到感兴趣的。




关于阅读的话,我今年阅读量降低了很多很多,只有寥寥几本😂


image.png

补充:不过这几本书,我个人觉得还是非常值得阅读的。《认知觉醒》有谈及到很多日常行为,阅读这本书的同时,会下意识的反省自己的行为;《娱乐至死》是新闻专业的考研读物,里面谈到的诸多观点,都非常有意思,而且十分深刻,但是有些话不适合写出来,只能自己知道;我还记得我当时读完这本书,就感觉是读书越多的人,并不是多么厉害,而是对这个世界背后的真实规则了解的越多,这本书,让我对宣传、新闻、娱乐了解的更深刻些了,也明白某些新闻背后的意图吧。新闻真真假假,但如今信三分都算多




关于消费。今年的消费直接上了一个量级,把自己也给吓到了。工作真就是给老板和房东挣钱,钱包该是多空还是多空。不过今年还是有几个大额消费的,主要是下面几个:


1、房租费用。一年下来真的心痛的要死;


2、双十一购物。有不少花销,反正花钱如流水的感觉...


3、一台二手台式电脑+一块4K屏幕


4、给家里打钱,给了妹妹一些生活费


5、个人消费也是真的高;


6、软件费用也贵。总的一算,都1k+啦,麻啦


白吃白喝魔都过一年


补充:详细的消费记录没有写到这里,不然大伙都该知道春春我还差一周,就真是一年穷到头啦。


立下来又总挂掉的Flag


照例回忆下去年年终写下的Flag,可以说是完成的非常糟糕啦


image.png
image.png

很坦白的说,完成度非常低,低到我自己都不愿意去翻看这个事情。


最主要很多不是无法完成,而是我没有去努力完成,这是我最无法接受的事情。


回头一看2023年,真的十分模糊,不知道自己到底做了些什么,恍惚间就又要到2024年啦


今年是这种感觉最强烈的一年,因为真的有过迷茫,找不到努力的方向,也浪费了许许多多时间。


我现在还记得我大学室友在今年12月问我的一句话,“二哥,今年我看你文章也没写多少,朋友圈也没咋更新,你今年到底在忙啥啊?”


我也不知道我忙了啥啊


关于明年


下面是我2024年的一些目标和想法,能不能完成,能完成多少,我心里也没底,反正都想冲一冲,试一试吗,先想啦,才能更好的去做,难道不是吗?


image.png


总的来说,主要是围绕生活和工作方面,其他的我没有再给自己立明确的需求啦,还有一些则是去年就立过啦,我到目前还没有完成,那些想法今年也会接着去做。


对于明年,我对自己只有一个要求,如果一件事情如果我能做好,那就希望自己能尽力去完成它,而不是因为懒惰而一拖再拖。一定一定一定要有执行力。


那么正在读这篇文章的你呢?今年的你想要做什么呢?你有什么样的想法呢?你会去尽力实现它吗?


在写完之后,我就已经开始着手去开始,有些事情想做就去做吧,没有必要一定要等到某个特殊的时间点之后,时间是在自己手上,而不是在日历上


长路修远,吾与子之所共适也


每每想到自己已将要岁至二十余四,不免有些恍惚,有些时候,在我自己看来,感觉自己还非常小,完全没长大,很多事情甚至都还没有去考虑过。


但看到身边的朋友,谈恋爱的谈恋爱,买车的买车,结婚的结婚,另外偶尔再加上家里边的催促,焦虑一下就放大了,莫名坐立难安


但其实大家都一样的,别慌,我先来自黑一下。


我呢,一年工资那么点,给房东一大把,自己吃喝玩乐一大把,购物一大把;约等于白吃白喝在魔都过一年呢~


技术呢,说啥都会点也是,但要说会多少,也就是啥都会一点,问多一句,就直接是啥都不会啦。


该努力就努力,该生活就生活,做好自己该做的事情;那么多的信息流,分辨不了,就少看点;你就是你,想那么多干嘛,想那么多也得烦恼今天晚上吃什么


无论如何,人生这条道路,仍然漫长而修远,我希望我们能一同享受这短短数十载。


那么这篇文章就写到这里啦。


如果你读到这里,如果你也喜欢的话,请说说你的感受吧,我真的非常想要收到来自于你的反馈,无论如何我都会认真的一一回复的,有问题也可以提出来,我也会一一回答~



2023年,我是宁在春,我在上海


那2024年,平安喜乐,万事胜意,祝你,祝我,祝我们



作者:宁在春
来源:juejin.cn/post/7317091895171153920
收起阅读 »

前端部署真的不简单

web
现在大部分的中小型公司部署前端代码都是比较简单的,主要步骤如下: 首先,通过脚手架提供的命令npm run build打包前端代码,生成dist文件夹; 最后,将dist文件夹丢给后台开发人员放在他们的工程里面,随后台一起部署;现在普遍是前后端分开部署,因此,...
继续阅读 »

现在大部分的中小型公司部署前端代码都是比较简单的,主要步骤如下:


首先,通过脚手架提供的命令npm run build打包前端代码,生成dist文件夹;


最后,将dist文件夹丢给后台开发人员放在他们的工程里面,随后台一起部署;现在普遍是前后端分开部署,因此,利用nginx起一个web服务器,将dist文件夹放到指定的路径下,配置下nginx访问路径,对于请求接口使用proxy_pass进行转发,解决跨域的问题。


更加高端一点的操作,是利用CI/CD + Docker进行自动化部署。


但是,你是否真的想过前端部署真的就这么简单吗?


这其实是一个非常严肃且复杂的问题,因为这关系到线上生产环境的稳定


有一天,从自知乎上看到一篇张云龙大佬在2014年写的文章,非常有启发,即使这篇文章距离现在有快10年了,但是其中的思想仍然熠熠生辉。


因为写的真的是太好了,为了让更多的人看到,所以大部分内容直接就照搬过来,为了让自己加深印象。如果想看原文,原文网址在这里。


那让我们从原始的前端开发讲起。


下图是一个 index.html 页面和它的样式文件 a.css,无需编译,本地预览,丢到服务器,等待用户访问。


image.png


哇,前端这么简单,门槛好低啊。这也是前端有太多人涌入进来的原因。


接着,我们访问页面,看到效果,再查看一下网络请求,200!不错,太完美了!


image.png


那么,研发完成。。。。了么?


等等,这还没完呢!


对于像 BAT 这种公司来说,那些变态的访问量和性能指标,将会让前端一点也不好玩。


看看那个 a.css 的请求,如果每次用户访问页面都要加载,是不是很影响性能,很浪费带宽啊,我们希望最好这样:


image.png


利用304,让浏览器使用本地缓存。


但,这样也就够了吗?


不够!


304叫协商缓存,这玩意还是要和服务器通信一次,我们的优化级别是变态级,所以必须彻底灭掉这个请求,要变成这样:


image.png


强制浏览器使用本地缓存(cache-control/expires),不要和服务器通信。


好了,请求方面的优化已经达到变态级别,那问题来了:你都不让浏览器发资源请求了,这缓存咋更新


很好,相信有人想到了办法:通过更新页面中引用的资源路径,让浏览器主动放弃缓存,加载新资源


像这样:


image.png


下次上线,把链接地址改成新的版本,这就更新资源了。


问题解决了么?当然没有,思考这种情况:


image.png


页面引用了3个 css 文件,而某次上线只改了其中的a.css,如果所有链接都更新版本,就会导致b.cssc.css的缓存也失效,那岂不是又有浪费了?


不难发现,要解决这种问题,必须让url的修改与文件内容关联,也就是说,只有文件内容变化,才会导致相应url的变更,从而实现文件级别的精确缓存控制


什么东西与文件内容相关呢?


我们会很自然的联想到利用数据摘要要算法对文件求摘要信息,摘要信息与文件内容一一对应,就有了一种可以精确到单个文件粒度的缓存控制依据了。


OK,那我们把 url 改成带摘要信息的:


image.png


这回再有文件修改,就只更新那个文件对应的 url 了,想到这里貌似很完美了。你觉得这就够了么?


图样图森破!


现代互联网企业,为了进一步提升网站性能,会把静态资源和动态网页分集群部署,静态资源会被部署到CDN节点上,网页中引用的资源也会变成对应的部署路径:


image.png


好了,当我要更新静态资源的时候,同时也会更新 html 中的引用吧,就好像这样:


image.png


这次发布,同时改了页面结构和样式,也更新了静态资源对应的url地址。现在重点来了,现在要发布代码上线,亲爱的前端研发同学,你来告诉我,咱们是先上线页面,还是先上线静态资源



这里的静态资源不仅仅包括css文件,也包括图片,以及不怎么经常变的资源。




  1. 先部署动态页面,再部署静态资源:在二者部署的时间间隔内,如果有用户访问页面,就会在新的页面结构中加载旧的资源,并且把这个旧版本的资源当做新版本缓存起来,其结果就是:用户访问到了一个样式错乱的页面,除非手动刷新,否则在资源缓存过期之前,页面会一直执行错误。

  2. 先部署静态资源,再部署动态页面:在部署时间间隔之内,有旧版本资源本地缓存的用户访问网站,由于请求的页面是旧版本的,资源引用没有改变,浏览器将直接使用本地缓存,这种情况下页面展现正常;但没有本地缓存或者缓存过期的用户访问网站,就会出现旧版本页面加载新版本资源的情况,导致页面执行错误,但当页面完成部署,这部分用户再次访问页面又会恢复正常了。


好的,上面一坨分析想说的就是:先部署谁都不成!都会导致部署过程中发生页面错乱的问题。


所以,访问量不大的项目,可以让研发同学苦逼一把,等到半夜偷偷上线,先上静态资源,再部署页面,看起来问题少一些。这也是很多公司的部署方案。


但是,大公司超变态,没有这样的绝对低峰期,只有相对低峰期。


所以,为了稳定的服务,还得继续追求极致啊!


这个奇葩问题,起源于资源的 覆盖式发布,用待发布资源覆盖已发布资源,就有这种问题。


解决它也好办,就是实现 非覆盖式发布


image.png


看上图,用文件的摘要信息来对资源文件进行重命名,把摘要信息放到资源文件发布路径中,这样,内容有修改的资源就变成了一个新的文件发布到线上,不会覆盖已有的资源文件。上线过程中,先全量部署静态资源,再灰度部署页面,整个问题就比较完美的解决了。


因为很多前端开发同学不怎么接触部署,对灰度部署不太熟悉,下面将介绍下什么是灰度部署。


软件开发一般都是一个版本一个版本的迭代。新版本上线前都会经过测试,但就算这样,也不能保证上线了不出问题。


所以,在公司里上线新版本代码一般都是通过灰度系统。灰度系统可以把流量划分成多份,一份走新版本代码,一份走老版本代码。


image.png


而且灰度系统支持设置流量的比例,比如可以把走新版本代码的流程设置为 5%,没啥问题了再放到 10%,50%,最后放到 100% 全量。这样可以把出现问题的影响降到最低。


不然一上来就全量,万一出了线上问题,那就是大事故。


另外,灰度系统不止这一个用途,比如,产品不确定某些改动是不是有效的,就要做 AB 实验,也就是要把流量分成两份,一份走 A 版本代码,一份走 B 版本代码。


那这样的灰度系统是怎么实现的呢?其实很多都是用 nginx 实现的。


nginx 是一个反向代理的服务,用户请求发给它,由它转发给具体的应用服务器。


image.png


它的过程如下图所示:


image.png


首先,需要对流量进行染色,即对这个用户进行标注,让这个用户访问服务1,另外的用户访问服务2。染色的方式有很多,可以通过cookie来完成。不同的用户携带的cookie是不同的。第一染色的时候,所有的用户都访问服务1。


然后,第二次访问的时候,nginx根据用户携带的cookie进行转发到不同的服务,这样就完成了灰度访问。


好了,灰度部署就介绍到这里,回到原文讲的先全量部署静态资源,再灰度部署页面,这是什么意思呢?


首先,部署静态资源的时候,不要删除原来的静态资源,而是把新的静态资源发复制过去,因为文件名用摘要算法重命名的,所以不会发生重名的问题。


其次,灰度部署动态页面,也就是一部分用户访问老的页面,一部分用户访问新的页面。访问老页面的用户请求的还是老资源,直接使用缓存。访问新页面的用户访问新资源,此时新资源已经部署完成,所以不会访问老的资源,导致页面出现错误。


最后,根据访问情况,利用灰度系统,逐渐把访问老页面的用户过渡到访问新页面上。


所以,大公司的静态资源优化方案,基本上要实现这么几个东西:



  1. 配置超长时间的本地缓存:节省带宽,提高性能

  2. 采用内容摘要作为缓存更新依据:精确的缓存控制

  3. 静态资源CDN部署:优化网络请求

  4. 更资源发布路径实现非覆盖式发布:平滑升级


全套做下来,就是相对比较完整的静态资源缓存控制方案了,而且,还要注意的是,静态资源的缓存控制要求在前端所有静态资源加载的位置都要做这样的处理


是的,所有!


什么js、css自不必说,还要包括js、css文件中引用的资源路径,由于涉及到摘要信息,引用资源的摘要信息也会引起引用文件本身的内容改变,从而形成级联的摘要变化,大概就是:


image.png


到这里本文结束了,我们已经了解了前端部署中关于静态资源缓存要面临的优化和部署问题,新的问题又来了:这™让工程师怎么写码啊!!!


这又会扯出一堆有关模块化开发、资源加载、请求合并、前端框架等等的工程问题。


妈妈,我再也不玩前端了。。。。


作者:小p
来源:juejin.cn/post/7316202725330796571
收起阅读 »

App跨平台框架VS原生开发深度评测之2023版

App跨平台框架历史悠久,从cordova、react native、flutter,直到最近的uni-app x。江山代有才人出,每个都试图颠覆原生,但过去却一直未成功。 过去的问题到底在哪里? 我们先捋一捋各种技术路线,分析这些跨平台开发框架和原生应用的差...
继续阅读 »

App跨平台框架历史悠久,从cordovareact nativeflutter,直到最近的uni-app x。江山代有才人出,每个都试图颠覆原生,但过去却一直未成功。


过去的问题到底在哪里?


我们先捋一捋各种技术路线,分析这些跨平台开发框架和原生应用的差别具体在哪里。


逻辑层渲染层类型代表作
webviewwebview弱类型5+App、cordova
js引擎webview弱类型uni-app之app-vue 、小程序(dount)
js引擎原生渲染弱类型react native、uni-app之app-nvue、weex
dart引擎flutter渲染引擎强类型flutter
js引擎flutter渲染引擎弱类型微信skyline、webF、ArkUI-x
kotlin原生渲染强类型uni-app x
kotlin原生渲染强类型原生应用

上面的表格,除了行尾的原生应用外,各个跨平台框架按出现时间排序,可以看到跨平台框架是如何演进的。


上表中,uni-app x和原生应用是一样的,逻辑层和渲染层都是原生,都是强类型;而其他跨平台框架或者在逻辑层、或者在渲染层与原生不一致。


webview不行已经是业内常识了,启动慢、渲染慢、内存占用高。这块本文不再详述。


但那些非web-view的框架到底哪里不如原生?


1. js逻辑+ 原生渲染


react nativeweex等抛弃webview,改由原生渲染的跨平台方案,2014年就推出了。
如今手机硬件也越来越好了,为什么性能还达不到原生?


js+原生渲染的方案主要有2点缺陷:



  • JS引擎自身的性能问题

  • JS和原生之间的通信延迟


1.1 js引擎慢,启动速度和运行速度都弱于原生


所以很多开发者即便使用这类方案,首页也还是原生来写。


React Native的Hermes引擎和华为的arkUI,提供了js编译为字节码的方案,这是一种空间换时间的方案,启动速度有了一定优化,但仍然比不过原生。


弱类型在编译期可优化的幅度有限,还是需要一个运行时来跑,无法像强类型那样直接深入底层。


以数字运算为例,js的number运算确实比强类型的int慢,内存开销也更大。


1.2 js语言与原生之间通信卡顿


每个语言有自己的内存空间,跨语言通信都有折损,每次通信几十到几百毫秒不等,视手机当时的状态。一旦频繁通信,就会明显卡顿。


逻辑层的js,即要和原生渲染层通信,还要和原生API通信:


1.2.1 js与原生ui通信


举个简单的场景例子,在js里监听滚动,根据滚动变化实时调整界面上某些元素的高度变化。这个问题能难倒一大批跨平台开发框架。


如果全部在webview里,js操作ui还好一些,所以uni-app的app-vue里的renderjs操作UI性能高,就是这个道理。同理还有微信小程序的wsx


虽然小程序和uni-app都是js,但实际上逻辑层在独立js引擎里,通过原生桥来控制web-view,通信成本很高。


weex提供了bindingx技术,这是一种弱编程,渲染层预先定义了一些操作UI的方式,调用时全部在渲染层运行,不会来回与逻辑层通信。但这种预定义方式的适应面有限,无法做到在js里高性能、自由的操作所有UI。


1.2.2 js操作原生api


操作系统和三方SDK的API都是原生的,js调用这些能力也需要跨语言通信。比如js调用原生的Storage或IO,数据较多时遍历的性能非常差。


当然在js API的封装上可以做些优化,比如微信的storage提供了wx.batchGetStorageSync这种批量读取的API,既然遍历性能差,那干脆一次性从原生读出来再传给js。


这也只能是无奈的方案,如果在遍历时想用js做什么判断就实现不了了,而且一次性读出很大的数据后传给js这一下,也需要通信时间。


2. flutter方案


flutter在2018年发布,第一次统一了逻辑层和渲染层,而且使用了强类型。


它没有使用原生渲染,而是使用由dart驱动的渲染引擎,这样逻辑层的dart代码操作UI时,再也没有延时了!bindingx、wxs这种补丁方案再也不需要了。


并且dart作为强类型,编译优化很好做,启动速度和运行速度都胜过js。


在这个开源项目下gitcode.net/dcloud/test…,提供了一个flutter编写的100个slider同时滑动的示例, 项目下有源码也有打包好apk,可以直接安装体验。


100个slider同时滑动,非常考验逻辑和UI的通信。如果在webview内部,html和js写100个这样的slider,在新的手机上表现也还ok。但在小程序和react native这种逻辑和UI分离的模式下,100个slider是灾难。


下载安装apk后可以看到dart操作flutter的UI真的没有通信折损,100个slider的拖动非常流畅。


flutter看起来很完美。但为什么也没有成为主流呢?很多大厂兴奋的引入后为何又不再扩大使用范围呢?


2.1 dart与原生API的通信


别忘了上面1.2.2提到的原生API通信。flutter虽然在逻辑层和渲染层都是dart,但要调用原生API时,还是要通信。


操作系统和三方SDK的API是原生的,让dart调用需要做一层封装,又落到了跨语言通信的坑里。


gitcode.net/dcloud/test…这是一个开源测试项目,来测试原生的claas数据与dart的通信耗时。


项目里面有源码,大家可自行编译;根目录有打包好的apk,也可以直接安装体验。


这个项目首先在kotlin中构建了包含不同数据量的class,传递到dart然后渲染在界面上,并且再写回到原生层。


有0.1k和1k两种数据量(点击界面上的1k数字可切换),有读和读并写2个按钮,各自循环1000次。


以下截图的测试环境是华为mate 30 5G,麒麟990。手机上所有进程杀掉。如下图:



  • 1k数据从原生读到dart并渲染


flutter_1k_read.jpeg



  • 1k数据从原生读到dart并渲染再写回


flutter_1k_readwrite.jpeg



  • 0.1k数据从原生读到dart并渲染


flutter_0.1k_read.jpeg



  • 0.1k数据从原生读到dart并渲染再写回


flutter_0.1k_readwrite.jpeg


通信损耗非常明显。并且数据量从1k降低到0.1k时,通信时间并没有减少10倍,这是因为通信耗时有一个基础线,数据再小也降不下去。


为什么会这样?因为dartkotlin不是一种编程语言,不能直接调用kotlinclass,只能先序列化成字符串,把字符串数据从原生传到dart,然后在dart层再重新构造。


当然也可以在原生层为dart封装API时提供wx.batchGetStorageSync这类批处理API,把数据一次读好再给dart,但这种又会遇到灵活性问题。


而在uni-app x中,这种跨语言通信是不存在的,不需要序列化,因为uni-app x使用的编程语言uts,在android上就编译为了kotlin,它可以直接调用kotlin的class而无需通信和封装。示例如下,具体uni-app x的原理后续章节会专题介绍。


<template>
template>
<script lang="uts">
import Build from 'android.os.Build';
export default {
onLoad() {
console.log(Build.MODEL); //uts可以直接导入并使用原生对象,不需要封装,没有跨语言通信折损
}
}
script>

再分享一个知识:


很多人都知道iPhone上跨平台框架的应用,表现比android好。但大多数人只知道是因为iPhone的硬件好。


其实还有一个重要原因,iOS的jscore是c写的,OS的API及渲染层也都是ObjectC,js调用原生时,某些类型可以做共享内存的优化。但复杂对象也还是无法直接丢一个指针过去共享使用内存。


而android,不管java还是kotlin,他们和v8、dart通信仍然需要跨语言通信。


2.2 flutter渲染和原生渲染的并存问题


flutter的自渲染引擎,在技术上是不错的。但在生态兼容上有问题。


很多三方软件和SDK是原生的,原生渲染和flutter自渲染并存时,问题很多。


flutter开发者都知道的一个常见坑是输入法,因为输入法是典型的原生UI,它和flutter自绘UI并存时各种兼容问题,输入框被遮挡、窗体resize适应,输入法有很多种,很难适配。


混合渲染,还有信息流广告、map、图表、动画等很多三方sdk涉及。这个时候内存占用高、渲染帧率下降、不同渲染方式字体不一致、暗黑主题不一致、国际化、无障碍、UI自动化测试,各种不一致。。。


这里没有提供开源示例,因为flutter官方是承认这个问题的,它提供了2种方式:混合集成模式和虚拟显示模式模式。


但在渲染速度、内存占用、版本兼容、键盘交互上都各自有各自的问题。详见flutter官网:docs.flutter.dev/platform-in…。这个是中文翻译:flutter.cn/docs/platfo…


在各大App中,微信的小程序首页是为数不多的使用flutter UI的界面,已经上线1年以上。


下面是微信8.0.44(此刻最新版),从微信的发现页面进入小程序首页。


视频中手机切换暗黑主题后,这个UI却还是白的,而且flutter的父容器原生view已经变黑了,它又在黑底上绘制了一个白色界面,体验非常差。


这个小程序首页界面很简单,没有输入框,规避了混合渲染,点击搜索图标后又跳转到了黑色的原生渲染的界面里。


假使这个界面再内嵌一个原生的信息流SDK,那会看到白色UI中的信息流广告是黑底的,更无法接受。


当然这不是说flutter没法做暗黑主题,重启微信后这个界面会变黑。这里只是说明渲染引擎不一致带来的各种问题。



注:如何识别一个界面是不是用flutter开发的?在手机设置的开发者选项里,有一个GPU呈现模式分析,flutter的UI不触发这个分析。且无法审查布局边界。



flutter的混合渲染的问题,在所有使用原生渲染的跨平台开发框架中都不存在,比如react native、weex、uni-app x。


总结下flutter:逻辑层和UI层交互没有通信折损,但逻辑层dart和原生api有通信成本,自绘UI和原生ui的混合渲染问题很多。


3. js+flutter渲染


flutter除了上述提到的原生通信和混合渲染,还有3个问题:dart生态、热更新、以及比较难用的嵌套写法。


一些厂商把flutter的dart引擎换成了js引擎,来解决上述3个问题。比如微信skyline、webF、ArkUI-x。


其实这是让人困惑的行为。因为这又回到了react native和weex的老路了,只是把原生渲染换成了flutter渲染。


flutter最大的优势是dart操作UI不需要通信,以及强类型,而改成js,操作UI再次需要通信,又需要js运行时引擎。


为了解决js和flutter渲染层的通信问题,微信的skyline又推出了补丁技术worklet动画,让这部分代码运行在UI层。(当然微信的通信,除了跨语言,还有跨进程通信,会更明显)


这个项目gitcode.net/dcloud/test…, 使用ArkUI-x做了100个slider,大家可以看源码,下载apk体验,明显能看到由于逻辑层和UI层通信导致的卡顿。



上述视频中,注意看手指按下的那1个slider,和其他99个通过数据通讯指挥跟随一起行动的slider,无法同步,并且界面掉帧。


不过自渲染由于无法通过Android的开发者工具查看GPU呈现模式,所以无法从条状图直观反映出掉帧。



注意ArkUI-x不支持Android8.0以下的手机,不要找太老的手机测试。



很多人以为自渲染是王道,但其实自渲染是坑。因为flutter的UI还会带来混合渲染问题。


也就是说,js+flutter渲染,和js+原生渲染,这2个方案相比,都是js弱类型、都有逻辑层和渲染层的通信问题、都有原生API通信问题,而js+flutter还多了一个混合渲染问题。


可能有的同学会说,原生渲染很难在iOS、Android双端一致,自渲染没有这个问题。


但其实完全可以双端一致,如果你使用某个原生渲染框架遇到不一致问题,那只是这个框架厂商做的不好而已。


是的,很遗憾react native在跨端组件方面投入不足,官方连slider组件都没有,导致本次评测中未提供react native下slider-100的示例和视频。


4. uni-app x


2022年,uts语言发布。2023年,uni-app x发布。


uts语言是基于typescript修改而来的强类型语言,编译到不同平台时有不同的输出:



  • 编译到web,输出js

  • 编译到Android,输出kotlin

  • 编译到iOS,输出swift


而uni-app x,是基于uts语言重新开发了一遍uni-app的组件、API以及vue框架。


如下这段示例,前端的同学都很熟悉,但它在编译为Android App时,变成了一个纯的kotlin app,里面没有js引擎、没有flutter、没有webview,从逻辑层到UI层都是原生的。


<template>
<view class="content">
<button @click="buttonClick">{{title}}button>
view>
template>

<script> //这里只能写uts
export default {
data() {
return {
title: "Hello world"
}
},
onLoad() {
console.log('onLoad')
},
methods: {
buttonClick: function () {
uni.
showModal({
"showCancel": false,
"content": "点了按钮"
})
}
}
}
script>

<style>
.content {
width: 750rpx;
background-color: white;
}
style>

这听起来有点天方夜谭,很多人不信。DCloud不得不反复告诉大家,可以使用如下方式验证:



  • 在编译uni-app x项目时,在项目的unpackage目录下看看编译后生成的kt文件

  • 解压打包后的apk,看看里面有没有js引擎或flutter引擎

  • 手机端审查布局边界,看看渲染是不是原生的(flutter和webview都无法审查布局边界)


但是开发者也不要误解之前的uni-app代码可以无缝迁移。



  • 之前的js要改成uts。uts是强类型语言,上面的示例恰好类型都可以自动推导,不能推导的时候,需要用:as声明和转换类型。

  • uni-app x支持css,但是css的子集,不影响开发者排版出所需的界面,但并非web的css全都兼容。


了解了uni-app x的基本原理,我们来看下uni-app x下的100个slider效果怎么样。


项目gitcode.net/dcloud/test…下有源码工程和编译好的apk。


如下视频,打开了GPU呈现模式,可以看到没有一条竖线突破那条红色的掉帧安全横线,也就是没有一帧掉帧。



uni-app x在app端,不管逻辑层、渲染层,都是kotlin,没有通信问题、没有混合渲染问题。不是达到了原生的性能,而是它本身就是原生应用,它和原生应用的性能没差别。


这也是其他跨平台开发框架做不到的。


uni-app x是一次大胆的技术突破,分享下DCloud选择这条技术路线的思路:


DCloud做了很多年跨平台开发,uni-app在web和小程序平台取得了很大的成功,不管规模大小的开发者都在使用;但在app平台,大开发者只使用uni小程序sdk,中小开发者的app会整体使用。


究其原因,uni-app在web和小程序上,没有性能问题,直接编译为了js或wxml,uni-app只是换了一种跨平台的写法,不存在用uni-app开发比原生js或原生wxml性能差的说法。


但过去基于小程序架构的app端,性能确实不及原生开发。


那么App平台,为什么不能像web和小程序那样,直接编译为App平台的原生语言呢?


uni-app x,目标不是改进跨平台框架的性能,而是给原生应用提供一个跨平台的写法。


这个思路的转换使得uni-app x超越了其他跨平台开发框架。


在web端编译为js,在小程序端编译为wxml等,在app端编译为kotlin。每个平台都只是帮开发者换种一致的写法而已,运行的代码都是该平台原生的代码。


然而在2年前,这条路线有2个巨大的风险:



  1. 从来没有人走通过

  2. 即便能走通,工作量巨大


没有人确定这个产品可以做出来,DCloud内部争议也很多。


还好,经历了无数的困难和挑战,这个产品终于面世了。


换个写法写原生应用,还带来另一个好处。


同样业务功能的app,使用vue的写法,比手写纯原生快多了。也就是uni-app x对开发效率的提升不只是因为跨平台,单平台它的开发效率也更高。


其实google自己也知道原生开发写法太复杂,关于换种更高效的写法来写原生应用,他们的做法是推出了compose UI。


不过遗憾的是这个方案引入了性能问题。我们专门测试使用compose UI做100个slider滑动的例子,流畅度也掉帧。


源码见:gitcode.net/dcloud/test…, 项目下有打包后的apk可以直接安装体验。


打开GPU呈现模式,可以看到compose ui的100个slider拖动时,大多数竖线都突破那条红色的掉帧安全横线,也就是掉帧严重。


既然已经把不同开发框架的slider-100应用打包出来了,我们顺便也比较了不同框架下的包体积大小、内存占用:


包体积(单位:M)内存占用(单位:Kb)
flutter18141324.8
ArtUI-x45.7133091.2
uni-app x8.5105451.2
compose ui4.498575.2

包体积数据说明:



  • 包含3个CPU架构:arm64、arm32、x86_64。

  • flutter的代码都是编译为so文件,支持的cpu类型和包体积是等比关系,1个cpu最小需要6M体积,业务代码越多,cpu翻倍起来越多。

  • ArtUI-x的业务代码虽然写在js里,但除了引用了flutter外还引用了js引擎,这些so库体积都不小且按cpu分类型翻倍。

  • uni-app x里主业务都在kotlin里,kotlin和Android x的兼容库占据了不少体积。局部如图片引用了so库,1个cpu最小需要7M体积。但由于so库小,增加了2个cpu类型只增加了不到1M。

  • compose ui没有使用so库,体积裁剪也更彻底。

  • uni-app x的常用模块并没有裁剪出去,比如slider100的例子其实没有用到图片,但图片使用的fesco的so库还是被打进去了。实际业务中不可能不用图片,所以实际业务中uni-app x并不会比compose ui体积大多少。


内存占用数据说明:



  • 在页面中操作slider数次后停止,获取应用内存使用信息VmRSS: 进程当前占用物理内存的大小

  • 表格中的内存数据是运行5次获取的值取平均值

  • 自渲染会占据更多内存,如果还涉及混合渲染那内存占用更高


5. 后记


跨语言通信、弱类型、混合渲染、包体积、内存占用,这些都是过去跨平台框架不如原生的地方。


这些问题在uni-app x都不存在,它只是换了一种写法的原生应用。


各种框架类型逻辑层与UI通信折损逻辑层与OS API通信折损混合渲染
react native、nvue、weex
flutter
微信skyline、webF、ArkUI-x
uni-app x
原生应用

当然,作为一个客观的分析,这里需要强调uni-app x刚刚面世,还有很多不成熟的地方。比如前文diss微信的暗黑模式,其实截止到目前uni-app x还不支持暗黑模式。甚至iOS版现在只能开发uts插件,还不能做完整iOS应用。


需求墙里都是uni-app x该做还未做的。也欢迎大家投票。


另外,原生Android中一个界面不能有太多元素,否则性能会拉胯。flutter的自渲染和compose ui解决了这个问题。而原生中解决这个问题需要引入自绘机制来降低元素数量,这个在uni-app x里对应的是draw自绘API。


uni-app x这个技术路线是产业真正需要的东西,随着产品的迭代完善,它能真正帮助开发者即提升开发效率又不牺牲性能。


让跨平台开发不如原生,成为历史。


欢迎体验uni-app x的示例应用,感受它的启动速度,渲染流畅度。


源码在:gitcode.net/dcloud/hell…; 


这个示例里有几个例子非常考验通信性能,除了也内置了slider-100外,另一个是“模版-scroll-view自定义滚动吸顶”,在滚动时实时修改元素top值始终为一个固定值,一点都不抖动。


我们不游说您使用任何开发技术,但您应该知道它们的原理和差别。


欢迎指正和讨论。


作者:CHB
来源:juejin.cn/post/7317091780826497075
收起阅读 »

一名小白程序员的思维风暴,大家看看当一乐了

长目标实施失败分析 1. 单词 事件:准备系统学习英语,主要是想拿到英语考级证书,后面继续扩展,能无字幕看懂英文字幕等,能更好的获取英文咨询 计划:根据自己的理解,首先找到了最基础的音标学习,同步进行单词记忆 中断情况: 每天安排的三十个单词记忆早上...
继续阅读 »

长目标实施失败分析


1. 单词



  • 事件:准备系统学习英语,主要是想拿到英语考级证书,后面继续扩展,能无字幕看懂英文字幕等,能更好的获取英文咨询

  • 计划:根据自己的理解,首先找到了最基础的音标学习,同步进行单词记忆


image-20231220152153200.png



  • 中断情况:



    1. 每天安排的三十个单词记忆早上一个小时都不够用(理论上是足够的,自己分析是自己对背单词的本能抵触,导致效率很低),有时为了完成手上工作经常放弃早上单词任务,一天内也没有补上,时间一长就逐渐放弃了;

    2. 还有一点是之前记忆的单词一般第二天都会重复检查,检查的结果是之前记忆的单词遗忘的太快,导致第二天又要花费额外的时间重新背,这样算下来觉得花费太多时间在这上面

    3. 晚上的音标听读,由于在室外,经常会遇到行人,有时候觉得不好意思经常偷工减料,冬天室外又很冷,经常断更



  • 克服情况:



    • 当时对这件事的重视程度不高,并且英语学习计划自己规划了不下五六回了,最后没能完成这件事,也很快抛掷脑后了

    • 现在看来:上述几个问题自己现在都能找到调整方案,主要在于自己是否真心决定做这件事,自我认为当时并没有觉得这件事有多么重要,就如同一个爱好一样,中断就中断了。



  • 习得性无助:之前了解过一些,关于这点,我主观认为是没有的,因为我觉得只要我觉得自己下定决心,按照记得学习计划来,是肯定能学会的,但是现在自己都已经不想开这个头了,又觉得是已经陷入了这种习得性无助中。

  • 自己的计划表单中,其实一直有英语学习的愿望,但是再也没有开始过,空着也觉得稀疏平常了。


    image-20231220154741758.png


  • 补充一点:自己在此期间因为减肥,养成了运动习惯,现在跑步这件事对我没有一点障碍,只要有跑步的念头,就能马上出发的那种,冬天也不例外,所以后面就停止运动这一块的记录,也是自己觉得很骄傲的一点。应该是得到了一种正反馈


image-20231220155323950.png


2. 找工作



  • 事件:今年六月毕业,在三月忙完毕业答辩之后,计划了为期两个月的找工作面试之旅,当时计划的是一直投简历面试到春招结束。

  • 计划:秋招没有参加,那会对自己的能力很自卑,加上那会实习,学校封校,没有参加学校组织的校园招聘(现在想来很后悔,错过了很多好的机会),于是今年三月份春招做好了最坏的打算,打算持续到招聘结束(这里的一点心得是:当我们决定在解决一件事情的时候,它大概率就是处于一种很坏的情况,任何事情都需要早做打算,能节省很多成本


    image.png


  • 中断情况:



    1. 2023年春招真的是很艰难,三月份一直处于简历投递,参加校园招聘,各种企业的宣讲会,网申评测等,最后接到面试通知的却只有一两家,并且面试的经验也少,导致面试结果也不好,逐渐也失去了信心。

    2. 事情在四月份有了转机,学校与一些学校跟企业来我们学校做宣讲,在某个学校的教师聘用岗拿到了第一份offer,后面又拿到了武汉的一份c++开发岗offer(觉得加班严重没去),期间参加了一次武汉云智的面试,面试官是腾讯员工,面试过程被暴捶,突然意识到自己的能力可能就是值上面开发岗的价位,所以后面就放缓了面试招聘的行程,觉得再怎么找也找不到更好的了,加上面试也很疲劳了,这种摆烂心态也间接导致后面武汉电信的终面都没去,现在还觉得可惜。

    3. 五月左右,毕业活动变得很多,毕业照,毕业聚餐,毕业旅行,各种活动接踵而至,所以找工作这件事也变得没有之前积极,想着有保底了,就去保底算了,自己制定的投递计划,有很多大厂,好公司都没有投递,也变成了现在的遗憾,身为一个计算机硕士,连心中梦想的大厂都没有投递过。



  • 克服情况:



    1. 在当时情况下,自己找不到克服的动力与方法,当时想着能有一份工作保底就很不错了

    2. 目前看来,其实还是有继续面试的必要的,虽然能力上有所欠缺,但是自己是有很强的学习能力的,现在入职之后发现,很多东西都是需要自己去找资料学的,而自己需要的只是一个平台罢了。



  • 总结:



    • 这段经历的给我的感觉是,到现在工作了还是觉很绝望,因为在大的趋势下,一名应届生是都找到工作显得一文不值,但是目前的我具备了能坚持下去的勇气。




3. 独立开发



  • 事件:想独自开发一款属于自己构思的小程序,自己在学习完基础课程后,当准备着手时,却不知道如何开始,自己也只是一个前端小白,每次做这件事的时候,根本没有方向。

  • 计划:目前就制作了该小程序核心功能的思维导图,事件计划没有写,因为自己也没有能力预估各功能模块完成的时间,也不清楚如何将自己列举的具体功能细分成功能模块,变成我可以明确了解的实现步骤。

    • 已完成

      • 购买云服务,并配置环境

      • 找个一个完整的前后端分离项目案例,准备在此基础上进行修改

      • 程序核心功能思维导图

      • 构建前端后仓库





  • 中断情况:

    1. 由于在心中对程序整体功能只有一个大概的想法,所以在对程序功能实现时总觉得很模糊,没有办法量化

    2. 没有实际独立的开发经验,对完成整件事具有莫名的不自信,或者是恐惧,在推进工作时,一直没有明确的实现路线

    3. 找不到发力的着力点,准备完成前端界面时,没有原型图自己都不知道怎么开始,用别人的模板有总觉得不合适,在纠结之前内耗停滞。



  • 克服情况:

    • 暂没有解决方案

    • 下一步计划:在选好的项目上就研究,在已有的项目上进行修改。




作者:用户8430186848879
来源:juejin.cn/post/7314559238720749608
收起阅读 »

写一个简易的前端灰度系统

写在前面的话灰度这个概念,来自数字图像领域,最初是描述黑白数字图像的灰度值,范围从 0 到 255,0 表示黑色,255 表示白色,中间的数值表示不同程度的灰色。灰度系统的诞生源于交叉学科的建设,在互联网上也不...
继续阅读 »

写在前面的话

灰度这个概念,来自数字图像领域,最初是描述黑白数字图像的灰度值,范围从 0  2550 表示黑色,255 表示白色,中间的数值表示不同程度的灰色。

灰度系统的诞生源于交叉学科的建设,在互联网上也不例外。对于一个软件产品,在开发和发布的时候肯定希望用户能够顺利的看到想让其看到的内容。但是,发布没有一帆风顺的,如果在发布的某个环节出了问题,比如打错了镜像或者由于部署环境不同触发了隐藏的bug,导致用户看到了错误的页面或者旧的页面,这就出现了生产事故。为了避免这种情况出现,借鉴数字图像处理的理念,设计师们设计出了一种介于 0  1 之间的过渡系统的概念:让系统可以预先发布,并设置可见范围,就像朋友圈一样,等到风险可控后,再对公众可见。这就是灰度系统。

灰度系统版本的发布动作称作 灰度发布,又名金丝雀发布,或者灰度测试,他是指在黑与白之间能够平滑过渡的一种发布方式。在其上可以进行A/B testing,即让一部分用户继续用产品特性A,一部分用户开始用产品特性B,如果用户对B没有什么反对意见,那么逐步扩大范围,把所有用户都迁移到B上面来。(概念来自知乎)

对于前端领域,演进到现在,灰度系统主要有如下几点功能:

  1. 增量灰度:小的patch可以增量的添加在发布版本上,也可以通过开关一键关闭
  2. 用户灰度:增量和全量版本都可对不同群体或者某几个特定的用户进行灰度可见
  3. 版本回退:每一个版本都在灰度系统里可见,可以一键回退

前端灰度系统工作流程图如下:

sequenceDiagram
前端项目-->灰度系统: 部署阶段
前端项目->>灰度系统: 1.CI 打包后写入打包资源,状态初始化
前端项目-->灰度系统: 访问阶段
前端项目->>灰度系统: 1.页面访问,请求当前登录用户对应的资源版本
灰度系统-->>前端项目: 2.从对应版本的资源目录返回前端资源

灰度规则

关于灰度资源优先级的说明如下:

灰度策略优先级
未生效
生效
全量一般

如此就起到了灰度的作用:全量表示所有人都可以看;生效表示只有在规则中的用户才可以看到这部分增量更新,优先级最高;未生效表示不灰度,优先级最低。

灰度系统数据库设计

为什么灰度系统有后端:前端项目 CI 部署后,会产生一个 commit 号和一个镜像记录,并且打包后的文件存放在服务器中某一个深层的文件夹目录中,灰度系统需要存入该部署的目录地址,便于在切换灰度时查找不同版本的文件。

先介绍一个要部署的前端项目(你可以根据自己的前端项目动态调整)。

本项目针对的前端项目是一个基于微服务架构的项目,

下面是设计ER图:

image.png

我们依此来分析:

子项目表

该表用于存放所有子项目的信息,新建一个微服务子项目时,会在这个表里新建一个条目,数据示意如下:

image.png

灰度用户表

用于灰度系统登录的用户,拥有灰度权限的人才可以加入。

资源表

资源表存放项目在 CI 中写入的 commit 信息和 build 完以后在服务器的存放位置,数据示意如下:

image.png

其中 branch 是跑CI的分支,data 存放打包资源目录信息,一般结构如下:

image.png

gitProjectId 存放该产品在 gitlab 中的项目号, status 表示构建状态:0:构建完成 1:部署完成 2:构建失败,3:部署失败。

这里简单提一下 CI 是如何写入灰度系统数据库的,过多详情不做解释,写入数据库方式很多,这只是其中一种实现方式。

  1. 首先在 CI build 环节往服务器写入打包信息的 JSON:

image.png

其中 build.sh 负责把传入的参数写到一个 json 中。上图中是往根目录copy,方便下一个 CI job读取json文件的示意图。

  1. 在 CI 部署环节,通过调用脚本创建资源:

image.png

其中 run_gray.js:

const { ENV, file, branch, projectId, gitProjectId, user, commitMsg } = require('yargs').argv;

axios({
url: URL,
method: "POST",
headers: {
remoteUser: user
},
data: {
Action: "CreateResource",
projectId,
branch,
commitMsg,
gitProjectId,
channel: Channel,
data: fs.readFileSync(file, 'utf8'),
status: "0"
}
}).then(...)

其中 status 的变化,在 CI 部署服务器完成后,追加一个 UpdateResource 动作即可:

if [[ $RetCode != 0 ]]; then curl "$STARK_URL" -X 'POST' -H 'remoteUser: '"$GITLAB_USER_NAME"'' -H 'Content-Type: application/json' -d '{"Action": "UpdateResource", "id": "'"$ResourceId"'", "status": "2"}' > test.log && echo `cat test.log`; fi

灰度策略表

灰度策略是对灰度资源的调动配置。其设计如下:

image.png

其中,prijectId 表示灰度的项目,resourceId 表示使用的资源,rules 配置了对应的用户或用户组(看你怎么配置了,我这里只配置了单独的 userId),status 是灰度的状态,我设置了三种:

  • default: 未生效
  • failure: 生效
  • success: 全量

状态生效表示是增量发布的意思。

到这里,数据库设计就完毕了。


灰度系统接口API开发

有了数据库,还需要提供能够操作数据库的服务,上边创建资源的接口就是调用的灰度自己的API实现的。主要的API列表如下:

名称描述
getResourcesByProjectId获取单个产品下所有资源
getResourcesById通过主键获取资源
createResource创建一个资源
updateResource更新一个资源
getIngressesByProjectId获取单个产品下灰度策略任务列表
getIngressById通过主键获取单个灰度策略任务详情
createIngress创建一个策略
updateIngress更新一个策略

剩余的接口有用户处理的,有子项目管理的,这里不做详述。除了上边的必须的接口外,还有一个最重要的接口,那就是获取当前登录用户需要的资源版本的接口。在用户访问时,需要首先调用灰度系统的这个接口来获取资源地址,然后才能重定向到给该用户看的页面中去:

名称描述接收参数输出
getConsoleVersion获取当前用的产品版本userId,productsresource键值对列表

getConsoleVersion 接受两个参数,一个是当前登录的用户 ID, 一个是当前用户访问的微服务系统中所包含的产品列表。该接口做了如下几步操作:

  1. 遍历 products,获取每一个产品的 projectId
  2. 对于每一个 projectId,联查资源表,分别获取对应的 resourceId
  3. 对于每一个resourceId,结合 userId,并联查灰度策略表,筛选出起作用的灰度策略中可用的资源
  4. 返回每一个资源的 data 信息。

其中第三步处理相对繁琐一些,比如说,一个资源有两个起作用的灰度资源,一个是增量的,一个是全量的,这里应该拿增量的版本,因为他优先级更高。

获取用户版本的流程图如下:

graph TD
用户登录页面 --> 获取所有产品下的资源列表
获取所有产品下的资源列表 --> 根据灰度策略筛选资源中该用户可用的部分 --> 返回产品维度的资源对象

最后返回的资源大概长这个样子:

interface VersionResponse {
[productId: number]: ResourceVersion;
}

interface ResourceVersion {
files: string[];
config: ResourceConfig;
dependencies: string[];
}

其中 files 就是 JSON 解析后的上述 data 信息的文件列表,因为打包后的文件往往有 css和多个js。

至于这个后端使用什么语言,什么框架来写,并不重要,重要的是一定要稳定,他要挂掉了,用户就进不去系统了,容灾和容错要做好;如果是个客户比较多的网站,并发分流也要考虑进去。

前端页面展示

前端页面就随便使用了一个前端框架搭了一下,选型不是重点,组件库能够满足要求就行:

  • 登录

image.png

  • 查看资源

image.png

  • 配置策略

image.png

image.png


部署以后,实际运行项目看看效果:

image.png

可以看到,在调用业务接口之前,优先调用了 getConsoleVersion来获取版本,其返回值是以产品为 key 的键值对:

image.png

访问转发

这里拿到部署信息后,服务器要进行下一步处理的。我这里是把它封装到一个对象中,带着参数传给了微服务的 hook 去了(微服务系统需要);如果你是单页应用,可能需要把工作重心放在 Nginx 的转发上,Nginx内部服务读取灰度系统数据库来拿到版本目录,然后切换路由转发(可能只是改变一个路由变量)。 (你也可以参照我 nginx 相关文章),下面我简单的给个示意图:

graph TD
灰度系统配置灰度策略 --> Nginx+Lua+Mysql获取灰度策略并写入Nginx变量
Nginx+Lua+Mysql获取灰度策略并写入Nginx变量 --> Nginx服务器配置资源转发

总结

前端灰度系统,其实就是一个后台管理系统。他配置和管理了不同版本的前端部署资源和对应的用户策略,在需要的时候进行配置。

接下来的文章我会配套性的讲一下 Nginx 和 Docker 的前端入门使用,敬请期待!

完!大家对灰度系统有什么好的建议,可以在评论区讨论哦!



作者:小肚肚肚肚肚哦
来源:juejin.cn/post/7212054600162132029

收起阅读 »

为什么程序员一定要写单元测试?

大家好,我是鱼皮,很多初学编程的同学都会认为 “程序员的工作只有开发新功能,功能做完了就完事儿”。但其实不然,保证程序的正常运行、提高程序的稳定性和质量也是程序员的核心工作。 之前给大家分享过企业项目的完整开发流程,其中有一个关键步骤叫 “单元测试”,这篇文章...
继续阅读 »

大家好,我是鱼皮,很多初学编程的同学都会认为 “程序员的工作只有开发新功能,功能做完了就完事儿”。但其实不然,保证程序的正常运行、提高程序的稳定性和质量也是程序员的核心工作。


之前给大家分享过企业项目的完整开发流程,其中有一个关键步骤叫 “单元测试”,这篇文章就来聊聊程序员如何编写单元测试吧。


什么是单元测试?


单元测试(Unit Testing,简称 UT)是软件测试的一种,通常由开发者编写测试代码并运行。相比于其他的测试类型(比如系统测试、验收测试),它关注的是软件的 最小 可测试单元。


什么意思呢?


假如我们要实现用户注册功能,可能包含很多个子步骤,比如:



  1. 校验用户输入是否合法

  2. 校验用户是否已注册

  3. 向数据库中添加新用户


其中,每个子步骤可能都是一个小方法。如果我们要保证用户注册功能的正确可用,那么就不能只测试注册成功的情况,而是要尽量将每个子步骤都覆盖到,分别针对每个小方法做测试。比如输入各种不同的账号密码组合来验证 “校验用户输入是否合法” 这一步骤在成功和失败时的表现是否符合预期。


同理,如果我们要开发一个很复杂的系统,可能包含很多小功能,每个小功能都是一个单独的类,我们也需要针对每个类编写单元测试。因为只有保证每个小功能都是正确的,整个复杂的系统才能正确运行。


单元测试的几个核心要点是:



  1. 最小化测试范围:单元测试通常只测试代码的一个非常小的部分,以确保测试的简单和准确。

  2. 自动化:单元测试应该是自动化的,开发人员可以随时运行它们来验证代码的正确性,特别是在修改代码后。而不是每次都需要人工去检查。

  3. 快速执行:每个单元测试的执行时间不能过长,应该尽量做到轻量、有利于频繁执行。

  4. 独立性:每个单元测试应该独立于其他测试,不依赖于外部系统或状态,以确保测试的可靠性和可重复性。


为什么需要单元测试?


通过编写和运行单元测试,开发者能够快速验证代码的各个部分是否按照预期工作,有利于保证系统功能的正确可用,这是单元测试的核心作用。


此外,单元测试还有很多好处,比如:


1)改进代码:编写单元测试的过程中,开发者能够再次审视业务流程和功能的实现,更容易发现一些代码上的问题。比如将复杂的模块进一步拆解为可测试的单元。


2)利于重构:如果已经编写了一套可自动执行的单元测试代码,那么每次修改代码或重构后,只需要再自动执行一遍单元测试,就知道修改是否正确了,能够大幅提高效率和项目稳定性。


3)文档沉淀:编写详细的单元测试本身也可以作为一种文档,说明代码的预期行为。


鱼皮以自己的一个实际开发工作来举例单元测试的重要性。我曾经编写过一个 SQL 语法解析模块,需要将 10000 多条链式调用的语法转换成标准的 SQL 语句。但由于细节很多,每次改进算法后,我都不能保证转换 100% 正确,总会人工发现那么几个错误。所以我编写了一个单元测试来自动验证解析是否正确,每次改完代码后执行一次,就知道解析是否完全成功了。大幅提高效率。


所以无论是后端还是前端程序员,都建议把编写单元测试当做一种习惯,真的能够有效提升自己的编码质量。


如何编写单元测试?


以 Java 开发为例,我们来学习如何编写单元测试。


Java 开发中,最流行的单元测试框架当属 JUnit 了,它提供了一系列的类和方法,可以帮助我们快速检验代码的行为。


1、引入 JUnit


首先我们要在项目中引入 JUnit,演示 2 种方式:


Maven 项目引入


在 pom.xml 文件中引入 JUnit 4 的依赖:


<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.13.2</version>
    <scope>test</scope>
</dependency>

Spring Boot 项目引入


如果在 Spring Boot 中使用 JUnit 单元测试,直接引入 spring-boot-starter-test 包即可:


<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

然后会自动引入 JUnit Jupiter,它是 JUnit 5(新版本)的一部分,提供了全新的编写和执行单元测试的方式,更灵活易用。不过学习成本极低,会用 JUnit 4,基本就会用 JUnit Jupiter。


2、编写单元测试


编写一个单元测试通常包括三个步骤:准备测试数据、执行要测试的代码、验证结果。


一般来说,每个类对应一个单元测试类,每个方法对应一个单元测试方法。


编写 JUnit 单元测试


比如我们要测试一个计算器的求和功能,示例代码如下:


import org.junit.Test;
import org.junit.Assert;

public class CalculatorTest {

    // 通过 Test 注解标识测试方法
    @Test
    public void testAdd() {
        // 准备测试数据
        long a = 2;
        long b = 3;
        
        // 执行要测试的代码
        Calculator calculator = new Calculator();
        int result = calculator.add(23);
        
        // 验证结果
        Assert.assertEquals(5, result);
    }
}

上述代码中的 Assert 类是关键,提供了很多断言方法,比如 assertEquals(是否相等)、assertNull(是否为空)等,用来对比程序实际输出的值和我们预期的值是否一致。


如果结果正确,会看到如下输出:



如果结果错误,输出如下,能够清晰地看到执行结果的差异:



Spring Boot 项目单测


如果是 Spring Boot 项目,我们经常需要对 Mapper 和 Service Bean 进行测试,则需要使用 @SpringBootTest 注解来标识单元测试类,以开启对依赖注入的支持。


以测试用户注册功能为例,示例代码如下:


import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import javax.annotation.Resource;

@SpringBootTest
public class UserServiceTest {

    @Resource
    private UserService userService;

    @Test
    void userRegister() {
        // 准备数据
        String userAccount = "yupi";
        String userPassword = "";
        String checkPassword = "123456";
        // 执行测试
        long result = userService.userRegister(userAccount, userPassword, checkPassword);
        // 验证结果
        Assertions.assertEquals(-1, result);
        // 再准备一组数据,重复测试流程
        userAccount = "yu";
        result = userService.userRegister(userAccount, userPassword, checkPassword);
        Assertions.assertEquals(-1, result);
    }
}

3、生成测试报告


如果系统的单元测试数量非常多(比如 1000 个),那么只验证某个单元测试用例是否正确、查看单个结果是不够的,我们需要一份全面完整的单元测试报告,便于查看单元测试覆盖度、评估测试效果和定位问题。


测试覆盖度 是衡量测试过程中被测试到的代码量的一个指标,一般情况下越高越好。测试覆盖度 100% 表示整个系统中所有的方法和关键语句都被测试到了。


下面推荐 2 种生成单元测试报告的方法。


使用 IDEA 生成单测报告


直接在 IDEA 开发工具中选择 Run xxx with Coverage 执行单元测试类:



然后就能看到测试覆盖度报告了,如下图:



显然 Main 方法没有被测试到,所以显示 0%。


除了在开发工具中查看测试报告外,还可以导出报告为 HTML 文档:



导出后,会得到一个 HTML 静态文件目录,打开 index.html 就能在浏览器中查看更详细的单元测试报告了:



这种方式简单灵活,不用安装任何插件,比较推荐大家日常学习使用。


使用 jacoco 生成单测报告


JaCoCo 是一个常用的 Java 代码覆盖度工具,能够自动根据单元测试执行结果生成详细的单测报告。


它的用法也很简单,推荐按照官方文档中的步骤使用。


官方文档指路:http://www.eclemma.org/jacoco/trun…


首先在 Maven 的 pom.xml 文件中引入:


<plugin>
  <groupId>org.jacoco</groupId>
  <artifactId>jacoco-maven-plugin</artifactId>
  <version>0.8.11</version>
</plugin>

当然,只引入 JaCoCo 插件还是不够的,我们通常希望在执行单元测试后生成报告,所以还要增加 executions 执行配置,示例代码如下:


<plugin>
    <groupId>org.jacoco</groupId>
    <artifactId>jacoco-maven-plugin</artifactId>
    <version>0.8.11</version>
    <configuration>
        <includes>
            <include>com/**/*</include>
        </includes>
    </configuration>
    <executions>
        <execution>
            <id>pre-test</id>
            <goals>
                <goal>prepare-agent</goal>
            </goals>
        </execution>
        <execution>
            <id>post-test</id>
            <phase>test</phase>
            <goals>
                <goal>report</goal>
            </goals>
        </execution>
    </executions>
</plugin>

然后执行 Maven 的 test 命令进行单元测试:



测试结束后,就能够在 target 目录中,看到生成的 JaCoCo 单元测试报告网站了:



打开网站的 index.html 文件,就能看到具体的测试报告结果,非常清晰:



通常这种方式会更适用于企业中配置流水线来自动化生成测试报告的场景。


作者:程序员鱼皮
来源:juejin.cn/post/7301311492095885349
收起阅读 »

封装一个丝滑的评分组件

web
效果 实现 找到喜欢的图标 http://www.iconfont.cn/ 打开 iconfont,随便搜个点赞,仅需一个点赞即可,节省内存,另一个用CSS完成 下载解压 取出如下文件,在项目入口导入 iconfont.css,或者统一放入一个文件导入也...
继续阅读 »

效果


score.gif


实现


找到喜欢的图标


http://www.iconfont.cn/


打开 iconfont,随便搜个点赞,仅需一个点赞即可,节省内存,另一个用CSS完成


image.png


下载解压


image.png


取出如下文件,在项目入口导入 iconfont.css,或者统一放入一个文件导入也行


image.png


编写组件


一个点赞,就叫Like


一个点踩,就叫Hate


Hate点踩组件


先写属性,就几个,不用注释也明白


分别是



  • 绑定值 modelValue

  • 默认颜色 defaultColor

  • 激活颜色 activeColor

  • 大小 size


image.png


HTML给个容器,再加个固定格式(xxx-container)的容器类名,防止样式穿透


再把iconfont样式加上,图标就出来了


这些用VSCode自定义代码片段都能自动生成


image.png


现在是这样的图标,也就是点赞


image.png


那么怎样让它变成点踩手势呢?答案是rotate旋转


加上下图类名有用吗? 答案是没用,他并不会旋转


image.png


为什么呢?因为i标签是行盒模型,必须变成块盒才行


方式有很多,不过我就爱flex,优点很多,这里就不赘述了


image.png


这不就转起来了吗


image.png


动效


加个鼠标移入变色效果,考虑到一会还有另一个点赞组件,那就写个通用的sass


接收一个激活颜色和默认颜色参数


image.png


导入并使用,下面用了个v-bind绑定当前被点击的颜色,下面来实现逻辑


image.png


上面接收了一个modelValue,类型是布尔值,当他为真时就把颜色改为激活颜色


于是就理所当然的使用计算属性


image.png


事件


当被点击或hover时,就激活图标颜色,现在还差点击


点击事件要做 3 件事



  1. 改变父组件的值

  2. 改变颜色

  3. 实现开头的丝滑动画


image.png


这里用一个showAnimation变量控制动画展示


再改变父组件的值,父组件的值一变,自己的颜色也会跟着变


还差个动画,只要让动画在其中一段不停反复横跳,即可实现国际友好手势


image.png


但这样有问题,只有第一次触发动画才有动效,后续需要改变showAnimation的值


那么怎么知道动画什么时候结束呢?


答案是事件onanimationend,只要在这个事件把动画关掉,点击时开启即可


image.png


ok,实现最主要的组件了,另一个点赞就是复制改东改西,大家都会


score.gif


组合组件


接下来需要把点赞和点踩组合一起,那就叫Gr0up


需要实现逻辑



  1. 根据父组件的值,动态展示

  2. 点击时传递一个事件,告诉父组件,究竟是点赞还是点踩

  3. 提供一个值,锁定按钮,因为点完之后一般都不能反悔

  4. 提供样式设置,传递到子组件


类型


传递一个固定类型的值,才能分辨是什么操作


image.png


null就是初始状态,无操作,另外俩见名知义


那么现在需要编写自身的状态


image.png



  • wasChoose是否有按钮被选中

  • disabled当被选中,同时父组件完成(done),就禁用


样式


image.png


初始化


建议写代码都提供一个入口,后面有空出一期如何像写诗一般写代码


image.png


事件


当被点击时,改变两个状态,没什么好说的,完成


image.png


测试


test.gif



源码: gitee.com/cjl2385/dig…



作者:寅时码
来源:juejin.cn/post/7316321509857034280
收起阅读 »

微观层面看为什么读书

我媳妇是一个不爱读书的人。她有时候会反问我为什么读书? 最初我回答读书可以增长见识,陶冶情操。再后来我回答读书可以提升认知,获得更好的解决方案,而且腹有诗书气自华。 然而随着时间推移,我总觉得上面的回答差点意思。具体差什么呢?差的是和自己的真实链接,简单来说就...
继续阅读 »

我媳妇是一个不爱读书的人。她有时候会反问我为什么读书?


最初我回答读书可以增长见识,陶冶情操。再后来我回答读书可以提升认知,获得更好的解决方案,而且腹有诗书气自华。


然而随着时间推移,我总觉得上面的回答差点意思。具体差什么呢?差的是和自己的真实链接,简单来说就是对自己的具体的实践层的意义。


回顾一下我的读书历程。


我是7岁上的小学一年级,后来按部就班的读初中高中,高中复读了,之后读大学读研。现在的感受高中以前的书都已经回忆不起来了。高中的书也只剩下零星点点,而且中学阶段主要都是教科书。


我读书的主要阶段还是大学之后。从这点来说,和现在的孩子相比,我读书读课外书的年纪真是太晚了。大学主要读的是文学性的书籍和所谓当时的显学经济学方面的。


我能想起来的文学性的书有法国司汤达的《红与黑》,罗曼罗兰的《约翰克利斯朵夫》、《简爱》。经济学方面的有曼昆《微观经济学》和《经济学原理》宏观经济分册,犹记得宏观经济分册一页一页的读完的。后来陆续也读了张五常的《经济解释》……其它肯定还有但是一时半会想不起来了。


网络小说也读了不少,反倒是网络小说记得很清晰。萧鼎的《诛仙》,土豆的《斗破苍穹》,纷舞妖姬的《弹痕》和《诡刺》,缘分0的《无尽武装》、《全能炼金师》、《天风》、《天纵商才》,还有三戒大师的历史类权谋小说《官居一品》《大官人》等;还有其他一些的历史类和玄幻类小说,想起来的《凡人修仙传》、《鬼吹灯》和《盗墓笔记》。


如果要推荐的话,喜欢军文的话,强烈推荐《弹痕》,还有《天风》;前者是现代军文,后者是古代军文。喜欢历史权谋的推荐《官居一品》。喜欢玄幻的推《凡人修仙传》、《鬼吹灯》和《盗墓笔记》。


为啥网络小说记得比正统文学清楚?一方面网络小说是兴趣;另一方面,最近这些年上面的一些小说拍成了动画片,让自己有机会回顾颓废的那些年,那些年熬夜看的小说。


言归正传。正统的文学名著,当时我特意将文中给我启迪的句子摘抄下来,但如今那些句子绝大部分已经是不知踪迹。能想起来的:这是最好的时代,这是最坏的时代……这出自名著《双城记》,但这本书我并没有读,还有那句:世界上有一种英雄主义,就是看清生活的真相以后依然热爱生活,出自罗曼罗兰的《名人传》,但这本书我也没读。


对于经济学,记住的:产权的界定的是市场经济的基本前提,这就是大名鼎鼎的科斯定理。


网络小说类脑袋里面只有小说情节,没有名句。


从上面来看,为什么读书?那些年读的书没有获得解决方案,也没有能大面积地提高认知。


我现在反思读书要有更加具体的目的性,要与个人的认知体系链接,与实践链接。提升特定认知,改善特定实践。


读书是一种主动的迭代认知体系的方式,但相比写文章,相比阐述,读书是一种相对被动的方式,因为读书往往不知道会读到什么具体而微的内容,也不知道这些具体而微的内容会与认知体系发生怎样的具体链接。


也因为这种被动性,相比较写文章,相比阐述,读书拥有更多可能性,这也正是一直说的开卷有益。


综上,读书是为了主动迭代认知体系,以一种相对开放的方式


(本文完)


作者:通往自由之路
来源:juejin.cn/post/7316592822823010342
收起阅读 »

Android自定义锁屏实践总结

1. 背景 在我们的业务场景中,用户在完成下单后大部分的概率不再需要进入App做其他的操作,只需要知道当前的订单状态,为了方便用户在不解锁的情况下也能实时查看当前订单的状态,货拉拉用户 iOS端上线了灵动岛功能,用户的接受度较高,由于Android暂不支持灵动...
继续阅读 »

1. 背景


在我们的业务场景中,用户在完成下单后大部分的概率不再需要进入App做其他的操作,只需要知道当前的订单状态,为了方便用户在不解锁的情况下也能实时查看当前订单的状态,货拉拉用户 iOS端上线了灵动岛功能,用户的接受度较高,由于Android暂不支持灵动岛,所以我们自定义了一个锁屏页面。


2. 实践


  2.1 方案选择


  实现锁屏的方式有多种(锁屏应用、悬浮窗、普通Activity伪造锁屏等等),由于我们的业务场景简单只展示我们的订单状态,且不需要很强的保活干扰用户的操作,采用了普通的Activity伪造锁屏。


  2.2 方案原理


  锁屏的大概实现原理都很简单,监听系统的亮屏广播,在亮屏的时候展示自己的锁屏界面,自定义的锁屏界面会覆盖在系统的锁屏界面上,用户在自定义锁屏界面上进行一系列的动作后进入系统的解锁界面。



  2.3 代码实现


    2.3.1 锁屏页面


    锁屏页Activity在普通的Activity需要加上一些配置


      1. 在onCreate中设置添加Flags,让当前Activity可以在锁屏时显示



  • FLAG_SHOW_WHEN_LOCKED:使Activity在锁屏时仍然能够显示

  • FLAG_DISMISS_KEYGUARD:去掉系统锁屏页,设置了系统锁屏密码是没有办法去掉的,现在手机一般都会设置锁屏密码,该配置可基本忽略。


    this.window.addFlags(
WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD or
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
)

      2. 在AndroidManifest.xml中进行对锁屏页Activity进行配置



  • 主题配置,


      主要是配置锁屏Activity的背景为透明和去除过度动画,让锁屏Activity过渡到系统锁屏更自然






  • 启动模式配置



    • BroadcastReceiver中启动锁屏页Activity,需要添加Intent.FLAG_ACTIVITY_NEW_TASKflag,造成锁屏Activity单独创建一个history stack,会在最近任务中显示出来,通过配置excludeFromRecentsnoHistorytaskAffinity来规避这个问题。


      name=".lockscreen.LockScreenActivity"
    android:configChanges="uiMode"
    android:excludeFromRecents="true"
    android:exported="false"
    android:launchMode="singleInstance"
    android:noHistory="true"
    android:screenOrientation="portrait"
    android:taskAffinity="com.xxx.lockscreen"
    android:theme="@style/LockScreenTheme">


    ```



      3. Home键,Back键和Menu键事件的处理



  • Home键,由于不是用来替代系统锁屏的锁屏软件,不需要处理Home键事件.

  • Back/Menu键,重写onKeyDown让锁屏页不处理这两个事件


      override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
    return when (event?.keyCode) {
    KeyEvent.KEYCODE_BACK -> true

    KeyEvent.KEYCODE_MENU -> true

    else -> super.onKeyDown(keyCode, event)
    }
    }
    ```



    2.3.2 广播


    LockScreenBroadcastReceiver是普通的BroadcastReceiver,不做其他的配置,需要注意两点:



  1. 动态注册/注销

  2. 在广播中启动Activity,需要添加FLAG_ACTIVITY_NEW_TASK,否则会出现“Calling startActivity() from outside of an Activity”的运行时异常



class LockScreenBroadcastReceiver : BroadcastReceiver() {

override fun onReceive(context: Context?, intent: Intent?) {
intent?.let { handleCommandIntent(context, it) }
}

private fun handleCommandIntent(context: Context?, intent: Intent) {
when (intent.action) {
Intent.ACTION_SCREEN_OFF -> {
val lockScreen = Intent(this, LockScreenActivity::class.java)
lockScreen.setPackage("com.xxx.xxx")
lockScreen.addFlags( Intent.FLAG_ACTIVITY_NEW_TASK )
context?.startActivity(lockScreen)
}
Intent.ACTION_USER_PRESENT -> {
// 处理解锁后才显示自定义锁屏Activity
}
}
}
}

    2.3.3 实现效果





3. 注意点


以下是在实践过程中的一些问题小结,供大家参考。


  3.1 权限相关


  不同手机系统上权限的名称,大体分为5种:



  • 后台弹窗

  • 悬浮窗

  • 显示在其他应用的上层

  • 锁屏展示

  • 后台弹出界面


    以及不同的组合效果也不同,以下是已测试过的手机,


品牌型号系统系统版本相关权限权限截图权限截图
华为P50HarmonyOSHarmonyOS 4.0.01. 悬浮窗 2.后台弹窗
oppoOPPO K9 5GColorOS 13Android 131. 悬浮窗 2. 锁屏显示
vivoY52sFuntouch OS 10.5Android 101. 悬浮窗 2. 锁屏显示 3. 后台弹出界面
一加OnePlus Ace ProColorOS 13Android 131. 悬浮窗 2. 锁屏显示
荣耀honor 60magic ui 6.1Android 121. 显示在其他应用的上层
iQOONeo3Origin OSAndroid 121. 悬浮窗 2. 锁屏显示 3. 后台弹出界面
Hi novaHi nova 9Emui 12Android 121. 后台弹窗 2. 悬浮窗 3. 显示在其他应用的上层

  OPPO/一加 手机特殊说明:在默认状态下在系统设置下找不到“锁屏显示”的入口,需要先授权“悬浮窗”权限再次启动应用会在应用启动时弹窗提示授权在锁屏上显示,然后在系统设置中会出现“锁屏显示”的入口。


  3.2 有些手机在未授权时,应用在前台时锁屏可以展示,但是应用退到后台不展示。


  Android 10 (API 级别 29) 及更高版本对后台应用可启动Activity的时间施加限制。这些限制有助于最大限度地减少对用户造成的中断,并且可以让用户更好地控制其屏幕上显示的内容。具体见官方文档


  3.3 在部分手机上,点亮屏幕后不会立即展示自定义的锁屏界面,在解锁系统锁屏后才会展示自定义的锁屏。1. 监听解锁事件主动finish自定义的锁屏页面


    Intent.ACTION_USER_PRESENT->{
ActivityUtils.getActivityList()?.forEach {
if ("com.xxx.lockscreen.LockScreenActivity" == it.componentName.className) {
it.finish()
}
}
}

2. 在自定义锁屏ActivityonResume中监听设备是否已解锁并finish锁屏页


override fun onResume() {
super.onResume()
val isInteractive = (getSystemService(Context.POWER_SERVICE) as PowerManager).isInteractive
val isKeyguardLocked = (getSystemService(KEYGUARD_SERVICE) as KeyguardManager).isKeyguardLocked
if (isInteractive && !isKeyguardLocked) {
finish()
}
}

3.4 当在自定义锁屏页触发Home键事件后,锁屏页Activity不再显示


提示用户根据自己的系统去授予对应的权限,不同系统所需的权限参考上面第1点


3.5 Android 8.0 透明主题造成闪退


  在Android 8.0系统上Activity满足了以下条件:



  1. targetSdkVersion > 26

  2. 透明主题

  3. 固定屏幕方向


会出现java.lang.IllegalStateException: Only fullscreen activities can request orientation


    // ActivityRecord.java
void setRequestedOrientation(int requestedOrientation) {
if (ActivityInfo.isFixedOrientation(requestedOrientation) && !fullscreen
&& appInfo.targetSdkVersion > O) {
throw new IllegalStateException("Only fullscreen activities can request orientation");
}
....
}

  建议针对Android 8.0以外的系统才固定屏幕方向,可参考Android 8.0系统透明主题适配解决办法


4. 总结


从线上最新的数据来看,接近60%的订单在锁屏后可以通过自定义锁屏查看到订单状态。


功能上线后发现比较少用户会主动选择关闭,从最开始的出发点就是为用户提供一个便捷的状态查看的入口,用户下完单等待司机接单以及接单后司机的状态都是用户会重点关注的,同时我们会过滤掉一些不太重要的状态的显示避免对用户带来不必要的干扰。


从实现的角度上来说整体较简单,较麻烦的是国内的ROM对权限的管控越来越严,且不同的系统同一权限的命名和授予方式差异较大,需要用更吸引用户的体验去引导用户授权。


作者:货拉拉技术
来源:juejin.cn/post/7316806159008841767
收起阅读 »

“来同学在用户点击后退的时候加个弹窗做个引导”

web
文章起因这篇文章的起因来源于一个需求,产品:“我需要在浏览器点击后退的时候,加一个弹框引导用户做其他的操作”,老实讲这一块之前研究了很多次通过popstate去监听,但是始终有问题没有达到理想的状态或者说并没有完全研究透彻,本来想直接回复做不了砍掉这里吧,后来...
继续阅读 »

文章起因

这篇文章的起因来源于一个需求,产品:“我需要在浏览器点击后退的时候,加一个弹框引导用户做其他的操作”,老实讲这一块之前研究了很多次通过popstate去监听,但是始终有问题没有达到理想的状态或者说并没有完全研究透彻,本来想直接回复做不了砍掉这里吧,后来出于职业道德的遵守还是说试试看吧!

后来

之后在网上遨游了一段时间找了很多实现方案最后发现有一个Api叫做prompt,他来自于react-router,正好 项目中目前使用的路由就是react-router

import { Prompt } from 'react-router' //v5.2.0版本

我不太清楚看到这篇文章的同学有没有用到过这个Api,我大致介绍一下用法

const App = () =>{
const [isBlocking, setIsBlocking] = useState(true)

return <>
<Prompt
//这里是个Boolean 控制是否监听浏览器的后退 默认监听
when={isBlocking}
message={(_, action) =>
{
if (action === 'POP') {
Dialog.show({ //普普通通的弹框而已,,,,
title: '提示',
actions: [
{
key: 'back',
text: '回到浪浪山',
onClick() {
history.go(-1)
//用户选择按钮之后关闭掉监听
setIsBlocking(false)
},
},
{
key: 'saveAndBack',
text: '去往光明顶',
onClick: async () => null
},
{
key: 'cancle',
text: intl.t('取消'),
},
],
})
return false // 返回false通知该组件需要拦截到后退的操作 将执行权交给用户
}
return true //返回true 正常后退不做拦截
}}
>Prompt>

{/* ...内容部分 */}

}
export default App

上面这样可以实现我的需求,但是因为之前研究过这好一阵子那会并没找到这个Api,现在找到了本着一种知其然知其所以然的态度,深究一下内部到底是怎么实现可以禁止浏览器后退的,如果你不知道就跟着一起寻找一下吧,可能需要占用一杯咖啡的时间☕️

| Prompt

最初的想法就是直接去看Prompt实现的源码就好了,看看是怎么实现的这里的逻辑 其实在看之前内心是有一些猜测的觉得可能是下面这样做的

  • 可能是有一些浏览器提供的api但是我不清楚可以直接做到禁止后退,然后Prompt内部有调用
  • 或者是先添加了浏览器记录然后在后退的时候监听又删除

git上找react-router源码,注意要切换到对应的版本V5.2.0,免得对不上号

react-router5.2.0版本对应的链接🔽

github.com/remix-run/r…

从这个链接点击去之后我们可以看到Prompt方法,主要是下面这一段我们捡重点解析一下

  1. 获取history上面的block
  2. 在初始化阶段将我们的message传递到block中执行,并且获取到当前的unblock
  3. 离开的时候执行self.release()执行卸载操作
/**
* The public API for prompting the user before navigating away from a screen.
*/

function Prompt({ message, when = true }) {
return (

{context => {
invariant(context, "You should not use outside a ");

if (!when || context.staticContext) return null;

// 这个context是当前使用的环境上下文我们内部路由跳转用的history的包
const method = context.history.block;

return (
{
//初始化的阶段执行该方法
self.release = method(message);
}}
onUpdate={(self, prevProps) => {
if (prevProps.message !== message) {
self.release();
self.release = method(message);
}
}}
onUnmount={self => {
self.release();
}}
message={message}
/>
);
}}

);
}

既然看到了这里再继续看下 Lifecycle 方法其实这个里面就是执行了一些生命周期,这里就不解析了贴个图大家自己看下吧,都能看懂

github.com/remix-run/r…

image.png

到了这里可能有些疑惑,这里好像并没有什么其他操作就是调用了一个block,起初我以为block是原生history上面提供的方法后来去查了一下api发现上面并没有这个方法,此刻就更加好奇block内部的实现方式了

因为我们项目的上下文里面使用的history是来自于一个npm包,后面就继续看看这个history内部怎么实现的

import { createBrowserHistory } from 'history'
const history = createBrowserHistory()

createBrowserHistory

传送门在这里👇🏻感兴趣的同学直接去看源码

github.com/remix-run/h…

直接看里面的block方法

let isBlocked = false

const block = (prompt = false) => {
const unblock = transitionManager.setPrompt(prompt)

if (!isBlocked) {
checkDOMListeners(1)
isBlocked = true
}

return () => {
if (isBlocked) {
isBlocked = false
checkDOMListeners(-1)
}

return unblock()
}
}

现在来分析一下上面的代码

  1. prompt是我们传进来的弹框组件或者普通字符串信息
  2. transitionManager是一个工厂函数将prompt存到了函数内部以便后面触发的时候使用
  3. checkDOMListeners去做挂载操作就是监听popstate那一步
  4. 返回出去的函数是在外面在离开的时候做销毁popstate监听的

现在按照上面的步骤在逐步做代码分析,下面会看具体的部分有些不重要的地方会做删减

| transitionManager.setPrompt

  • 可以看到工厂函数里面存储了prompt
  • 销毁的时机是在上面unblock的时候执行重置prompt
  const createTransitionManager = () => {
let prompt = null
const setPrompt = (nextPrompt) => {
prompt = nextPrompt
return () => {
if (prompt === nextPrompt)
prompt = null
}
}
return {
setPrompt,
}
}

| checkDOMListeners

  • 上面默认传了1初始化的时候会进行popstate监听
  • 离开的时候传了-1移除监听
let listenerCount = 0
const checkDOMListeners = (delta) => {
listenerCount += delta
if (listenerCount === 1) {
addEventListener(window, PopStateEvent, handlePopState)
} else if (listenerCount === 0) {
removeEventListener(window, PopStateEvent, handlePopState)
}
}

| handlePopState

  • 调用getDOMLocation获取到一个location
const handlePopState = (event) => {
handlePop(getDOMLocation(event.state))
}
  • getDOMLocation 内部调用createLocation创建了一个
  • createLocation内部大家感兴趣可以自己去看一下,没有什么可讲的就是创建一些常规的属性
  • 比如state、pathname之类的

const getDOMLocation = (historyState) => {
const { key, state } = (historyState || {})
const { pathname, search, hash } = window.location

let path = pathname + search + hash

if (basename)
path = stripBasename(path, basename)

return createLocation(path, state, key)
}

那我们现在知道getDOMLocation是创建一个location并且传递到了handlePop方法内部现在去看看这个内部都干了啥

| handlePop

  • 我们要看的主要在else里面
  • confirmTransitionTo是我们上面提到的工厂函数里面的一个方法
  • 该方法内部执行了Prompt并返回了Prompt执行后的结果

let forceNextPop = false

const handlePop = (location) => {
if (forceNextPop) {
forceNextPop = false
setState()
} else {
const action = 'POP'

transitionManager.confirmTransitionTo(location, action, getUserConfirmation, (ok) => {
if (ok) {
setState({ action, location })
} else {
revertPop(location)
}
})
}
}

敲黑板 重点来了!!!

现在来看下confirmTransitionTo内部的代码

const confirmTransitionTo = (location, action, getUserConfirmation, callback) => {
if (prompt != null) {
const result = typeof prompt === 'function' ? prompt(location, action) : prompt

if (typeof result === 'string') {
if (typeof getUserConfirmation === 'function') {
getUserConfirmation(result, callback)
} else {
callback(true)
}
} else {
// 重点在这里,result是我们调用block时候的返回参数 true or false
// 如果返回false 那浏览器回退将被禁止 反之则正常
callback(result !== false)
}
} else {
callback(true)
}
}

所以现在回到上面的handlePop函数我们就能推测出如果我们回调中返回的false,说明我们想阻止浏览器的回退操作,那么执行的就是revertPop方法(其实名字大家可能也能猜出来 恢复 pop操作😂)

| revertPop

  • delta的逻辑是计算从开始到目前为止走过的路径做个差值计算
  • 这个时候正常来讲delta应该是1
  • 我们看最后一个逻辑就好这里是禁止撤回的重点
  • 当delta为1的时候就执行了go(1)
  • go方法内部实际调用了window.history.go(n)
const revertPop = (fromLocation) => {
const toLocation = history.location

let toIndex = allKeys.indexOf(toLocation.key)

if (toIndex === -1)
toIndex = 0

let fromIndex = allKeys.indexOf(fromLocation.key)

if (fromIndex === -1)
fromIndex = 0

const delta = toIndex - fromIndex

if (delta) {
forceNextPop = true
//window.history.go
go(delta)
}
}

之前我看到这里有个疑问就是如果最后的结果只是调用了go的话,那这个好像我们自己监听也可以实现一下于是就有了以下代码

function History() {
this.handelState = function (event) {
history.go(1)
}

this.block = function (Prompt) {
window.addEventListener('popstate', this.handelState)
return () => {
window.removeEventListener('popstate', this.handelState)
}
}
}

const newHistory = new History()

等到我实验的时候发现页面回退确实阻止住了,但是会闪一下上一个页面,给大家举个例子

Step1
我从PageA页面一路push到PageC
PageA -> PageB -> PageC

Step2
从PageC页面点击返回,之后页面的过程是这样的
PageC -> PageB -> PageC

就是说我本应该在PageC点击撤回,理想的效果是就停留在了PageC页面,但是目前效果是先回到了PageB因为我使用了go(1)就又回到了PageC,相当于在点击回退的时候多加载了PageB页面

这使我又陷入了沉思,其实研究到这里如果不把这个弄懂之前的努力就白费了,抱着这种想法又扎到了history代码中遨游一番

之后光看代码捋逻辑对这确实有些迷茫,没有办法只能开始调试history的源码了,这里比较简单,history源码下载下来之后做几个步骤

  1. 安装history相关依赖package
  2. 启动服务会有一个本地域名

image.png

  1. 之后在你真实项目中引入这个资源开始做调试

后面其实就一直打log和断点不断调试history源码查看执行路径,发现了问题所在

刚才上面提到的handlePop方法内部有一段代码那会忽略掉了,就是ok为true的时候,因为之前一直关注false的情况忽略了这里,后面把这个里面就研究了一下才明白其中的原委

if (ok) {
setState({ action, location })
} else {
revertPop(location)
}

| setState

这个方法做了几件事

  • 更新本地history状态,nextState可以理解为下一个目标地址其中包含location和action
  • 更新本地history的长度这里没有完全搞懂为什么要更新一下长度,但是猜测可能是为了和原生history状态一直保持同步吧防止出现意外情况
  • 这里又看到了transitionManager工厂函数,此时调用的notifyListeners这个就是解决我们上面的谜团所在
const setState = (nextState) => {

// 1.更新本地history状态
Object.assign(history, nextState)

history.length = globalHistory.length

//2.更新依赖history相关的订阅方法
transitionManager.notifyListeners(
history.location,
history.action
)
}

| notifyListeners

notifyListeners更新订阅的方法,我直接把这块代码贴出来了,一个发布订阅模式没什么好讲的

  let listeners = []

const appendListener = (fn) => {
let isActive = true

const listener = (...args) => {
if (isActive)
fn(...args)
}

listeners.push(listener)

return () => {
isActive = false
listeners = listeners.filter(item => item !== listener)
}
}

const notifyListeners = (...args) => {
listeners.forEach(listener => listener(...args))
}

重点的地方是react-router内部会调用history中的listen,这个listen方法会调用上面的appendListener进行存储,之后在合适的时间点执行react-router中传递的方法

这些方法的入参是目标页面的history属性(location,action),在接收到参数的时候根据参数中的location更新当前的页面

现在可以得出结论我们上面的例子不能成功的原因,是因为我们在执行的过程中没有绕过setState(因为此刻没有能让ok返回false的操作)所以当我们页面路径变更的时候自然页面也会更新

最后整体捋一下这个流程吧

到这里其实细心的同学会发现浏览器的回退我们确实是控制不了的只要点击了就一定会执行后退的操作。在history中针对block方法来说做的事情其实就下面这几步

  1. 封装了一个自己本地的history,当然跳转能力等还是依赖原生的history
  2. 在URL路径变更的时候history可以决定是否通知单页面应用的路由
  3. 如果通知了就相当于我们的ok是true,需要页面也更新一下
  4. 如果未通知相当于ok是false,就不需要页面更新

这就是为什么history里面的block为什么可以用go就能实现当前页面回退,本质上浏览器历史记录确实回退了,但是history并没有通知应用页面更新,并且继续执行go(1)这样给用户看到的视角就是页面没有回退并且url也没有变化

结论

其实在使用的时候history还是有一些问题如果当前页面reload了,那么revertPop里面的go就不会执行,因为此时的delta是0,这样就会导致即使页面没有变化但是url更新成了上一个记录

说一下我的看法这可能是这个history遗留的bug也可能是有意而为之但是也不重要了我们搞懂了原理就好。

其实浏览器后退加监听的行为感觉还是一个比较高频的需求操作,后面我打算自己写一个插件专门就做后退拦截的操作~

到底了------

今天圣诞节了 祝你们圣诞节快乐 Merry Christmas🎄🎄🎄 !!!希望都能收到喜欢的礼物🎁


作者:零狐冲
来源:juejin.cn/post/7316202778790477834

收起阅读 »

23岁前端的2023年年度总结(上)分手、离职、旅行

前言 从三个月前就在想年终总结写什么,真的把手放在键盘上就不知道从何开口了,足足在电脑面前发呆了一个小时才开始写..... 前几年可能真的在无限重复一天或者两天,上班 下班 吃饭 打游戏 睡觉 然后无限循环这一天。但今年一年其实真没白活,酸甜苦辣咸全都经历了;...
继续阅读 »

前言


从三个月前就在想年终总结写什么,真的把手放在键盘上就不知道从何开口了,足足在电脑面前发呆了一个小时才开始写.....


前几年可能真的在无限重复一天或者两天,上班 下班 吃饭 打游戏 睡觉 然后无限循环这一天。但今年一年其实真没白活,酸甜苦辣咸全都经历了;


分手


2023年1月末 过完年从大连坐飞机回到南京,这个冰冷的出租屋只剩下我一个人还有三只猫,少了另一个人的存在,我们从2018年6月14号在一起,到1月26号分手。


南京租的是个商水商电的公寓因为她两年前就说想住loft,1块7一度的电费让我只能下班回家的三五个小时舍得开一会空调甚至她不在的时候我就忍着了否则只要人在家就开空调的话一个月估计要三五百的电费。


从确定分手到我找好房子搬走有一个半礼拜,那个屋子还是阴面, 那几天下班回到那个出租屋让我感觉像住在监狱里,阴暗 冰冷 孤独,一切负面心情几乎全部聚在那里那刻。


我们分手的主要原因是双方家庭谈彩礼和买房的时候没有谈妥,聊崩了,她家是安徽的我家是辽宁的,详细的我就不在这说了,简单明了一句话就是 彩礼和房她们要的我们给不起,就这样。


在一起时开玩笑说分手了就学电影里男女主角我剃光头她剪短发,想到这我就拿推子把头剃了 但我没有让她知道 我怕她也真的剪了短发。


1703354451666.png


恢复理智后就是开始给她收拾东西,因为她家里人不让她回来见我怕我们旧情复燃,所以给了我地址我把她的行李都收拾好她的衣服一件件叠好装进箱子,也不知道崩溃了多少次,明明上个月这个时候我还看着她洗澡前把项链拿下来挂在浴室墙壁,现在项链就一动不动的挂在浴室墙壁上 而我站在墙壁面前看着这个项链愣愣的发呆,不知道多久后还是拿了下来小心翼翼的装进盒子里再放进箱子继续收拾。


1703354650104.png


她的行李寄走后随之也就是收拾我的行李、找房子、搬家。彻底结束维持了四年半的恋爱。


新的房间是独门独户的,我自己一个人住,是阳面的房间 而且楼层比较低,在三楼 经常能听见孩子们在小区里喧闹的声音,这给了我极大的治愈。而且我很喜欢收拾屋子,我觉得很解压,收拾干净后换上自己喜欢的四件套、墙壁挂布、地毯、玩偶 我住这也舒服;


也正巧那几天工作也忙,我们办公室算我老大在内就三个前端,老大也基本是干指挥官的活,另一个前端又有些菜,所以又难又累的活基本都是我来干,工作繁忙起来让人也就不会想那么多消耗情绪的事情。


1703354974708.png


(也是这个时候发现了掘金的新大陆——沸点)


这个时候活干的其实正起劲呢,因为项目业务流程都已经轻车熟路了,而且我老大也很器重我。


还在想什么时候有时间系统学习一下react18,现在是会用,但也只是工作推进我才会用的 并没有vue掌握的那么熟练,甚至如果面试让我说react可能都说不上来什么东西。想要技术更精进一些还是要花点时间好好学一下。


22年秋天开始也迷上了撸铁,不敢去健身房怕被健身教练pua买课,怕被大佬笑话,怕器械不会用,就买了一对哑铃在家里自己学自己练,看视频博主很多教的怎么吃 怎么练 胸怎么练胳膊怎么练 肩膀怎么练 分手一个月后缓过来了健身更有动力了。


1703357464564.png


然后自己放假的时候也开始往周边的城市走走,以前放假都是屯在家里,懒得出门,现在发现没事经常出去转转真的挺好的。独自去了杭州、苏州、镇江,拍照片 看看这看看那....


image.png
1703357797167.png
1703357763596.png

似乎生活在往好的方向走....


音乐


鄙人不才,从小喜欢听歌唱歌研究歌,目前在网易云有三首个人单曲,全部是自己作词作曲演唱。


分手这件事也给我很大的情绪冲击,缓和以后就着一点点余动把年前的歌给写完了,名叫《背靠月亮的夜晚》JYM感兴趣可以直接去网易云搜,好听的话也麻烦给个小红心支持下。


image.png


为什么这段落的title叫音乐呢因为不只是发歌,还去听歌;


4月份一个人去看了伏仪的现场
现场听了《昨日记书》《神离少年》《我会在每个有意义的时辰》等


1703357057722.png


5月份很小哥们去看了黄绮珊 黄妈的演唱会
《年轮》《离不开你》《无聊的》等


1703356969387.png


6月份一个人去看了暗杠的现场
《说书人》《走歌人》《童话镇》等


image.png


前几年和前女友在一起生活好像从没有出去旅游,去看演唱会,不是说不去而是当时好像就没有这个意识,放假了就只想两个人买一堆好吃的在家里看电影打游戏,一天一天就这么过去了


离职


我一直觉得工作和谈恋爱有异曲同工之处,比如,找工作要看条件,是否符合预期,看通勤,看薪资,也要看自己能不能干的了这份活,谈恋爱也是要看对方性格,长得好不好看,三观合不合,在一起时候能长久,还有 一点点说不清道不明的缘分。


六月份我已经彻底从分手的阴影种走了出来,并且攒了一小笔钱,打算攒够三万买摩托车,当时想买的款式是GSX250。但天有不测风云,甲方集体转移去广州,听说北京那一拨 还给了外包赔偿 我天真的以为我们也有赔偿 所以当听到甲方要转移的时候我还挺乐呵,因为我干了四年了,从没拿过赔偿,一直是外包 说辞就辞了。


1703358250821.png


但其实还是很不舍的,人对于一个地方呆久了就会有依赖,如果离开这里去新的地方就会有一种陌生的感觉,这感觉会让人不舒服。这家公司从通勤、薪资、办公环境、同事氛围、加班不管各方面看都很满意了,很知足的。如果不开除我我觉得干个三年都不成问题。而且之前老大还跟我说过有个新项目要下来从零到一打算全权让我独立负责,我当时有些高兴又有些担心,高兴是会得到前所未有的挑战,以后简历上也是亮眼的一笔。担心是我其实也怕我技术不顶,倒时候再给惹祸,而且也担心到时候会不会天天加班.....现在好了,不用担心了。


南京的外包没有给赔偿而是给安排其他的甲方公司,没过多久我收到了人事给我安排的面试,当天下午就接到了电话,问的也都不难,但是实在是不想去,因为第一:目前这家公司待了一年多了 同事也混得很熟了去了谁都不认识。第二:这家公司距离我住得地方很远 每天上下班通勤要花费现在的通勤1.5倍的时间


就也没有去,窝囊的选择了自离。


正好也想去西藏很久了,什么青春没有售价这种话都要听烂了,也攒了一点钱,索性就买票去了


1703487161165.png


谁帮我看看图片的上面写的啥(doge)


旅行


网上说去西藏做有意义的事就是捡队友和被捡,正好我也是一个人去的,我就往这一坐(内心: 我看谁来捡我)
甚至做好了没人捡的准备,就打算自己玩了。
不过好在也确实被捡到了,一路上认识了好几个处的不错的朋友 半年多了过去了现在也还经常聊天。


这块要详细写的话太多了写不过来,直接给你们看我当时记录的朋友圈吧 你们有什么问题的话可以评论区或者私信问比如高 去了哪些地方 报团多少钱之类的


1703474524831.png
1703474541584.png
1703474554710.png
image.png
image.png

最后一个朋友圈写错了,应该是第七天了。


到这基本前半年就过完了,欲知下半年如何,请看另章分写


1703474612496.png


作者:牛油果好不好吃
来源:juejin.cn/post/7316115845095817251
收起阅读 »

99年师弟,揭露华为工作的残酷真相

前几天,一位师弟约我吃饭,师弟跟我同校同一个专业,2022年本科毕业,比我低很多届,目前在华为工作。我们聊了很多母校的共同回忆,最近几年应届生求职的情况,也聊到了他在华为的工作经历。 师弟性格开朗,为人谦和,属于在学校各大社团特别混得开的那种,成绩也不错,保研...
继续阅读 »

前几天,一位师弟约我吃饭,师弟跟我同校同一个专业,2022年本科毕业,比我低很多届,目前在华为工作。我们聊了很多母校的共同回忆,最近几年应届生求职的情况,也聊到了他在华为的工作经历。


师弟性格开朗,为人谦和,属于在学校各大社团特别混得开的那种,成绩也不错,保研到本校,但拿到华为offer后,放弃了继续深造,选择直接就业。


聊到当今应届生求职现状时,感慨万千。


我的母校是一所普通211,部分专业比较有特色,在行业属于领先地位,但学校整体名气一般。


记得我们大四毕业时,就业行情还比较好,每个班都有好几个同学进入了华为。读研后,实验室的师兄师姐基本都拿到了华为offer,等我们开始找工作时,大部分同学也都拿了华为offer,华为基本属于大家的保底选择。


师弟去年毕业时,他是专业唯一拿到华为offer的学生。这几年的就业形势,本科生基本找不到什么好工作,除了继续读书深造,没有更好的选择。


短短的几年,社会瞬息万变,就像坐上了一辆过山车,身处其中的人,也只能仅仅抓住车身的杠子,在急速下坡时,不让自己摔下来。


图片


师弟进入华为,原本是一件很开心的事,短短一年,经历职场拷打,再也没有大学时代那份锐气。


华为属于员工人数最多的科技公司之一,每年吸收了大批校招生,但员工离职率也非常高。


师弟反馈,跟他同一批进去的人,很多干了几个月就离开了,他是那一批少数还留下来的人。


刚入职时,每个人都要参加新员工入职培训,那时候大家都很骄傲和自豪,也非常认同企业的价值观和文化。


但当真正的分到项目组开始工作时,发现大家实际工作情况,和价值观上说的,并不不太一样。在基层落地时,价值观更多是挂在墙上的标语。


领导也基本不太关心员工的成长和未来发展,更多是把每一个员工当作生产工具,哪一件工具用的顺手、更能出活儿,就会用谁;还没证明过自己的工具,或者经验匮乏的工具,很有可能会被无情抛弃。


华为的假期还可以,但大家有什么事,都不太敢请假。即便是生病,如果不是非住院不可,也不太会请假;公司支持请假,但大家还是轻伤不下火线,请假会影响出勤系数,各种与之相关的奖金,也会大打折扣。


……


师弟还分享了工作以来其他的一些心得体会,一句话总结:职场很残酷,相比起来,大学轻松太多了。


图片


在我看来,华为还是中国一流的科技公司。大部分人,还是要挤破脑袋才能进得去。


华为有很好的制度,比如员工转岗,门槛还不太高。华为支持不同工种互相转岗,比如,干技术的,有一天不想写代码了,可以转人力,或者转销售。开发太累,也可以转做测试。公司会提供基本的业务培训,还有三个月的考核期,只要达到一定的用人标准,就能转岗成功。


华为还有很好的奖励制度,只要能吃苦,且绩效不太差,能坚持下来,很多人最后都获得了丰厚的回报。比我高一两届的师兄师姐,还有跟我同一届的很多朋友,现在都还在华为,过得还不错。


华为还有比较完善的福利体系。在各地的办公区,远离市区,地价也很便宜,平时在公司会有夜宵、员工餐厅、下午茶和健身房各种福利,也有些地方,会有单身公寓。进入华为后,除了工作,也没什么社交,公司能满足日常的衣食住行。平时挣的钱,基本都能存下来,成家后,在华为附近买房,也基本衣食无忧了。


华为是一家跨国公司,如果是干技术支持或销售,还能享受去各国打卡的福利。在年轻的时候,能去更远的地方走一走,也是一件很宝贵的人生经历。


图片


华为是中国众多私企大厂的一个缩影,也是最早践行狼性文化的一家公司。


如今的字节、阿里等互联网企业,也在走华为的老路。


这些大厂,给了年轻人一个令人向往的机会,但并不适合每个人。如果我们能早些看清现实,自己只是大厂的一颗螺丝钉,需要做的是,尽快掌握螺丝钉需要具备的技能,与此同时,如果借助大厂的势能,多开眼界,多认识一些各个领域的前辈和牛人,把企业能提供的资源用到极致,就更了不起了。


至于在公司里寻找朋友、寻找归属感或者安全感,那并不是一个好选择。同事间的八卦,公司股价的涨跌,CEO的致辞,也并不是我们需要关心的事。


最重要的是,无论在大厂还是小厂,在国企还是私企,都要拥有良好的心态。最怕的是,在国企羡慕华为的工资;在大厂,又羡慕国企的朝九晚五。


人生在世,各有各的不易。每一种选择,都没有对错, 选择了其中一个,在另外一部分,就得有所取舍。


在任何时候,学会“自洽”,与生活和解,与自己和解很重要。


作者:工程师酷里
来源:juejin.cn/post/7316349124600168484
收起阅读 »

浅谈Vue3的逻辑复用

web
Vue3的逻辑复用 使用了 Vue3 Composables 之后,逻辑复用比之前要顺手很多,这里说说前端开发最常用的场景,管理页面的列表查询和增删改查的逻辑复用,欢迎大家共同探讨。 用免费的 render 服务搭建了个在线的预览地址,源码在这里,用了免费的 ...
继续阅读 »

Vue3的逻辑复用


使用了 Vue3 Composables 之后,逻辑复用比之前要顺手很多,这里说说前端开发最常用的场景,管理页面的列表查询和增删改查的逻辑复用,欢迎大家共同探讨。


用免费的 render 服务搭建了个在线的预览地址源码在这里,用了免费的 node 环境和免费的 pg 数据库,对这部分有兴趣的可以看看我以前的分享,我写了个部署 spring-boot 的分享,使用 node 就更简单了。


可能每个人的具体工作内容不一致,但是应该都完成过这样的工作内容:



  1. 列表查询,带分页和过滤条件

  2. 新增,修改,查看,删除

  3. 进行一些快捷操作,比如:激活、通过


这些最基础的工作可能占用了我们很大的时间和精力,下面来讨论下如何逻辑复用,提高工作效率


需求分析


一个后台管理中心,绝大部分都是这种管理页面,那么需要:



  • 首先是封装一些通用的组件,这样代码量最低也容易保持操作逻辑和 UI 的一致性

  • 其次要封装一些逻辑复用,比如进入页面就要进行一次列表查询,翻页的时候需要重新查询

  • 最后需要有一些定制化的能力,最基本的列需要自定义,页面的过滤条件和操作也不一样


统一复用



  1. 发起 http 请求

  2. 展示后端接口返回的信息,有成功或者参数校验失败等信息


列表的查询过程



  1. 页面加载后的首次列表查询

  2. 页面 loading 状态的维护

  3. 翻页的逻辑和翻页后的列表重新查询

  4. 过滤条件和模糊搜索的逻辑,还有对应的列表重新查询


新增 Item、查询 Item、修改 Item



  1. form 在提交过程的禁用状态

  2. 发起网络请求

  3. 后端接口返回的信息提示

  4. 列表重新查询


删除 Item



  1. 删除按钮状态的维护(需要至少一个选中删除按钮才可用)

  2. 发起网络请求

  3. 后端接口返回的信息提示

  4. 列表重新查询


定制化的内容



  1. table 的列数据

  2. item 的属性,也就是具体的表单

  3. 快捷操作:改变 user 激活状态

  4. 列表的过滤条件


成果展示


img



  1. 打开页面会进行一次列表查询

  2. 翻页或者调整页面数量,会进行一次列表查询

  3. 右上角的是否激活筛选状态变更会进行一次列表查询

  4. 右上角模糊搜索,输入内容点击搜索按钮会进行一次列表查询

  5. 点击左上角的新增,弹出表单对话框,进行 item 的新增

  6. 点击操作的“编辑”按钮,弹出表单对话框,对点击的 item 进行编辑

  7. 点击“改变状态”按钮,弹出确认框,改变 user 的激活状态

  8. 选中列表的 checkbox,可以进行删除


代码直接贴在下面了,使用逻辑复用完成以上的内容一共 200 多行,大部分是各种缩进,可以轻松阅读,还写了一个 Work 的管理,也很简单,证明这套东西复用起来没有任何难度。


<template>
<div class="user-mgmt">
<biz-table
:operations="operations"
:filters="filters"
:loading="loading"
:columns="columns"
:data="tableData"
:pagination="pagination"
:row-key="rowKey"
:checked-row-keys="checkedRowKeys"
@operate="onOperate"
@filter-change="onFilterChange"
@update:checked-row-keys="onCheckedRow"
@update:page="onUpdatePage"
@update:page-size="onUpdatePageSize"
/>

<user-item :show="showModel" :item-id="itemId" @model-show-change="onModelShowChange" @refresh-list="queryList" />
</div>
</template>

<script setup name="user-mgmt">
import { h, ref, computed } from 'vue';
import urls from '@/common/urls';
import dayjs from 'dayjs';
import { NButton } from 'naive-ui';
import BizTable from '@/components/table/BizTable.vue';
import UserItem from './UserItem.vue';
import useQueryList from '@/composables/useQueryList';
import useDeleteList from '@/composables/useDeleteList';
import useChangeUserActiveState from './useChangeUserActiveState';

// 自定义列数据
const columns = [
{
type: 'selection'
},
{
title: '姓',
key: 'firstName'
},
{
title: '名',
key: 'lastName'
},
{
title: '是否激活',
key: 'isActive',
render(row) {
return row.isActive ? '已激活' : '未激活';
}
},
{
title: '创建时间',
key: 'createdAt'
},
{
title: '更新时间',
key: 'updatedAt'
},
{
title: '操作',
key: 'actions',
render(row) {
return [
h(
NButton,
{
size: 'small',
onClick: () => onEdit(row),
style: { marginRight: '5px' }
},
{ default: () => '编辑' }
),
h(
NButton,
{
size: 'small',
onClick: () => onChangeUserActiveState(row),
style: { marginRight: '5px' }
},
{ default: () => '改变状态' }
)
];
}
}
];

// 自定义右上角筛选
const filters = ref([
{
label: '是否激活',
type: 'select',
value: '0',
class: 'filter-select',
options: [
{
label: '全部',
value: '0'
},
{
label: '已激活',
value: '1'
},
{
label: '未激活',
value: '2'
}
]
},
{
label: '',
type: 'input',
placeholder: '请输入姓氏',
value: ''
}
]);

// 筛选变化,需要重新查询列表
const onFilterChange = ({ index, type, value }) => {
filters.value[index].value = value;
queryList();
};

// 自定义查询列表参数
const parmas = computed(() => {
return {
isActive: filters.value[0].value,
like: filters.value[1].value
};
});

// 封装好的查询列表方法和返回的数据
const { data, loading, pagination, onUpdatePage, onUpdatePageSize, queryList } = useQueryList(urls.user.user, parmas);

// 经过处理的列表数据,用于在 table 中展示
const tableData = computed(() =>
data.value.list.map(item => {
return {
...item,
createdAt: dayjs(item.createdAt).format('YYYY-MM-DD HH:mm:ss'),
updatedAt: dayjs(item.updatedAt).format('YYYY-MM-DD HH:mm:ss')
};
})
);

// 删除列表相关逻辑封装
const { checkedRowKeys, onCheckedRow, deleteList } = useDeleteList({
content: '确定删除选中的用户?',
url: urls.user.userDelete,
callback: () => {
queryList();
}
});

// 列表中的快捷操作
const operations = computed(() => {
return [
{
name: 'create',
label: '新增',
type: 'primary'
},
{
name: 'delete',
label: '删除',
disabled: checkedRowKeys.value.length === 0
}
];
});

// 触发操作函数
const onOperate = function (name) {
operationFucs.get(name)();
};

// 新创建 item
const create = () => {
showModel.value = true;
itemId.value = 0;
};

const onModelShowChange = () => {
showModel.value = !showModel.value;
};

const itemId = ref(0);

// 控制模态对话框
const showModel = ref(false);

// 编辑 item
const onEdit = row => {
itemId.value = row.id;
showModel.value = true;
};

const { changeUserActiveState } = useChangeUserActiveState();

// 改变激活状态
const onChangeUserActiveState = row => {
changeUserActiveState({
id: row.id,
isActive: !row.isActive,
loading,
callback: () => {
queryList();
}
});
};

// 指定 table 的 rowKey
const rowKey = row => row['id'];

// operation 函数集合
const operationFucs = new Map();
operationFucs.set('create', create);
operationFucs.set('delete', deleteList);
</script>

<style lang="scss">
.user-mgmt {
height: 100%;
.filter-select {
.biz-select {
width: 100px;
}
}
}
</style>



作者:hezf
来源:juejin.cn/post/7316349124600315940
收起阅读 »

h5端调用手机摄像头实现扫一扫功能

web
一、前言 最近有遇到一个需求,在h5浏览器中实现扫码功能,其本质便是打开手机摄像头定时拍照,特此做一个记录。主要技术栈采用的是vue2,使用的开发工具是hbuilderX。 经过测试发现部分浏览器并不支持打开摄像头,测试了果子,华子和米,发现夸克浏览器无法打...
继续阅读 »

一、前言



最近有遇到一个需求,在h5浏览器中实现扫码功能,其本质便是打开手机摄像头定时拍照,特此做一个记录。主要技术栈采用的是vue2,使用的开发工具是hbuilderX。


经过测试发现部分浏览器并不支持打开摄像头,测试了果子,华子和米,发现夸克浏览器无法打开摄像头实现功能。



h5调用摄像头实现扫一扫只能在https环境下,亦或者是本地调试环境!!


image.png


二、技术方案



经过一番了解之后,找到了两个方案


1.使用html5-qrcode(对二维码的精度要求较高,胜在使用比较方便,公司用的是vue2,因此最终采用此方案)


2.使用vue-qrcode-reader(对vue版本和node有一定要求,推荐vue3使用,这里就不展开说了)



三、使用方式


image.png


当点击中间的扫码时,设置isScanning属性为true,即可打开扫码功能,代码复制粘贴即可放心‘食用’。


使用之前做的准备



通过npm install html5-qrcode 下载包


引入 import { Html5Qrcode } from 'html5-qrcode';



html结构
<view class="reader-box" v-if="isScaning">
<view class="reader" id="reader"></view>
</view>

所用数据
data(){
return{
html5Qrcode: null,
isScaning: false,
}
}


methods方法
openQrcode() {
this.isScaning = true;
Html5Qrcode.getCameras().then((devices) => {
if (devices && devices.length) {
this.html5Qrcode = new Html5Qrcode('reader');
this.html5Qrcode.start(
{
facingMode: 'environment'
},
{
focusMode: 'continuous', //设置连续聚焦模式
fps: 5, //设置扫码识别速度
qrbox: 280 //设置二维码扫描框大小
},
(decodeText, decodeResult) => {
if (decodeText) { //这里decodeText就是通过扫描二维码得到的内容
this.action(decodeText) //对二维码逻辑处理
this.stopScan(); //关闭扫码功能
}
},
(err) => {
// console.log(err); //错误信息
}
);
}
});
},

stopScan() {
console.log('停止扫码')
this.isScaning = false;
if(this.html5Qrcode){
this.html5Qrcode.stop();
}
}

css样式
.reader-box {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: rgba(0, 0, 0, 0.5);
}

.reader {
width:100%;
// width: 540rpx;
// height: 540rpx;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}

四、最终效果


image.png


如有问题,欢迎指正,若此文对您有帮助,不要忘记收藏+关注!


作者:极客转
来源:juejin.cn/post/7316795553798815783
收起阅读 »

Java中的双冒号运算符(::)及其应用

在Java 8引入的Lambda表达式和函数式接口之后,双冒号运算符(::)成为了一项重要的功能。它可以将方法或构造函数作为参数传递,简化了编码和提升了代码的可读性。本文将介绍Java中的双冒号运算符及其常见应用场景。 双冒号运算符(::)的语法 双冒号运算符...
继续阅读 »

在Java 8引入的Lambda表达式和函数式接口之后,双冒号运算符(::)成为了一项重要的功能。它可以将方法或构造函数作为参数传递,简化了编码和提升了代码的可读性。本文将介绍Java中的双冒号运算符及其常见应用场景。


双冒号运算符(::)的语法


双冒号运算符的语法是类名/对象名::方法名。具体来说,它有三种不同的使用方式:



  1. 作为静态方法的引用:ClassName::staticMethodName

  2. 作为实例方法的引用:objectReference::instanceMethodName

  3. 作为构造函数的引用:ClassName::new


静态方法引用


首先,我们来看一下如何使用双冒号运算符引用静态方法。假设有一个Utils类,其中有一个静态方法processData


public class Utils {
public static void processData(String data) {
System.out.println("Processing data: " + data);
}
}

我们可以使用双冒号运算符将该方法作为参数传递给其他方法:


List<String> dataList = Arrays.asList("data1", "data2", "data3");
dataList.forEach(Utils::processData);

上述代码等效于使用Lambda表达式的方式:


dataList.forEach(data -> Utils.processData(data));

通过使用双冒号运算符,我们避免了重复写Lambda表达式,使代码更加简洁和易读。


实例方法引用


双冒号运算符还可以用于引用实例方法。假设我们有一个User类,包含了一个实例方法getUserInfo


public class User {
public void getUserInfo() {
System.out.println("Getting user info...");
}
}

我们可以通过双冒号运算符引用该实例方法:


User user = new User();
Runnable getInfo = user::getUserInfo;
getInfo.run();

上述代码中,我们创建了一个Runnable对象,并将user::getUserInfo作为方法引用赋值给它。然后,通过调用run方法来执行该方法引用。


构造函数引用


在Java 8之前,要使用构造函数创建对象,需要通过写出完整的类名以及参数列表来调用构造函数。而使用双冒号运算符,我们可以将构造函数作为方法引用,实现更加简洁的对象创建方式。


假设有一个Person类,拥有一个带有name参数的构造函数:


public class Person {
private String name;

public Person(String name) {
this.name = name;
}

public String getName() {
return name;
}
}

我们可以使用双冒号运算符引用该构造函数并创建对象:


Supplier<Person> personSupplier = Person::new;
Person person = personSupplier.get();
person.getName(); // 调用实例方法

上述代码中,我们使用Person::new将构造函数引用赋值给Supplier接口,然后通过get方法创建了Person对象。


总结


本文介绍了Java中双冒号运算符(::)的语法及其常见的应用场景。通过双冒号运算符,我们可以更方便地引用静态方法、实例方法和构造函数,使得代码更加简洁和可读。双冒号运算符是Java 8引入的重要特性,对于函数式编程和Lambda表达式的使用起到了积极的推动作用。


希望本文能够帮助您理解和应用双冒号运算符,提高Java开发的效率和代码质量。如有任何问题或疑惑,欢迎提问!


作者:每天一个技术点
来源:juejin.cn/post/7316532841923805184
收起阅读 »

关于晚上十点和男生朋友打电话调试vue源码那些事

web
简介朋友昨晚让我帮忙看怎么调试vue源码,我当时就不困了啊。兴致勃勃搞了半小时以失败告终。小白我可是永不言败的,早上鼓捣了俩小时总算写了点东西出来。想调试vue源码的小伙伴可以试试我这个思路fork源码首先肯定是要把vue/core代码fork一份到自己的仓库...
继续阅读 »

简介

朋友昨晚让我帮忙看怎么调试vue源码,我当时就不困了啊。兴致勃勃搞了半小时以失败告终。小白我可是永不言败的,早上鼓捣了俩小时总算写了点东西出来。想调试vue源码的小伙伴可以试试我这个思路

fork源码

首先肯定是要把vue/core代码fork一份到自己的仓库 这样后续有改动可以提交一下 也可以从源码一键同步

ps:github.com/baicie/vuej… 我的代码在这里可以参考一下

装包

pnpm i @pnpm/find-workspace-packages @pnpm/types -wD

ps:可以先看看pnpm与monorepo

在根目录执行上述命令装一下依赖-wD含义是在workspace的根安装开发依赖

脚本编写

首先在packages下执行pnpm creata vite创建一个vue项目

然后在scripts文件夹下创建dev.ts

import type { Project as PnpmProject } from '@pnpm/find-workspace-packages'
import { findWorkspacePackages } from '@pnpm/find-workspace-packages'
import type { ProjectManifest } from '@pnpm/types'
import { execa } from 'execa'
import os from 'node:os'
import path from 'node:path'
import process from 'node:process'
import color from 'picocolors'
import { scanEnums } from './const-enum'

export type Manifest = ProjectManifest & {
buildOptions: {
name?: string
compat?: boolean
env?: string
formats: ('global' | 'cjs' | 'esm-bundler' | 'esm-browser')[]
}
}

interface Project extends PnpmProject {
manifest: Manifest
}

const pkgsPath = path.resolve(process.cwd(), 'packages')
const getWorkspacePackages = () => findWorkspacePackages(pkgsPath)

async function main() {
scanEnums()
// 获取所有的包 除了private与没有buildOptions的包
const pkgs = (
(await getWorkspacePackages()).filter(
item => !item.manifest.private
) as Project[]
).filter(item => item.manifest.buildOptions)

await buildAll(pkgs)
}

async function buildAll(target: Project[]) {
// 并行打包
return runParallel(Number.MAX_SAFE_INTEGER, target, build)
}

async function runParallel(
maxConcurrent:
number,
source: Project[],
buildFn: (project: Project) =>
void
) {
const ret: Promise<void>[] = []
const executing: Promise<void>[] = []
for (const item of source) {
const p = Promise.resolve().then(() => buildFn(item))
// 封装所有打包任务
ret.push(p)

//
if (maxConcurrent <= source.length) {
const e: any = p.then(() => executing.splice(executing.indexOf(e), 1))
executing.push(e)
if (executing.length >= maxConcurrent) await Promise.race(executing)
}
}

return Promise.all(ret)
}

async function build(project: Project) {
const pkg = project.manifest
// 获取相对路径 包名
const target = path.relative(pkgsPath, project.dir)
if (pkg.private) {
return
}

const env = (pkg.buildOptions && pkg.buildOptions.env) || 'development'
await execa(
'rollup',
[
`-c`,
// 给rollup配置文件传递参数 watch 监听文件变化
'--watch',
'--environment',
[`NODE_ENV:${env}`, `TARGET:${target}`, `SOURCE_MAP:true`]
.filter(Boolean)
.join(',')
],
{ stdio: 'inherit' }
)
}

main().catch(err => {
console.error(color.red(err))
})

然后在根目录的package.json scripts 添加如下

"my-dev": "tsx scripts/dev.ts"

上述脚本主要是为了扫描工作目录下所有有意义的包并执行rollup打包命令(主要也就为了加一下watch没毛病)

验证

终端打开上吗创建的vite项目然后修改package.json里面的vue

 "vue": "workspace:*"

修改后根目录执行pnpm i建立软连接

1.根目录终端执行pnpm run my-dev

2.vite-project执行pnpm run dev

3.去runtime-core/src/apiCreateApp.ts createAppAPI 的 createApp 方法加一句打印

4.等待根目录终端打包完毕

5.去看看浏览器控制台有没有打印

按理说走完上述流出应该有打印出来哈哈 

优化更快?

然后就是想要快点因为我电脑不太行每次修改要等1.5~2s,然后我就想到了turbo,看了官网发现可行就试了试

修改如下

1. pnpm i turbo -wD

  1. 修改上述的my-dev "my-dev": "tsx scripts/dev.ts --turbo"
  2. 启动并验证

快了一点但不多

最后

新人多多关照哈哈 如果你想变强 b站 掘金搜索小满zs!


作者:白cie
来源:juejin.cn/post/7316539952475996194

收起阅读 »

前端要对用户的电池负责!

web
前言 我有一个坏习惯:就是下班之后不关电脑,但是电脑一般来说第二天电量不会有什么损失。但是后来突然有一天它开不起来了,上午要罢工?那也不好吧,也不能在光天化日之下划水啊;聪明的我还是找到了原因:电池耗尽了;于是赶紧来查一查原因,到底是什么程序把电池耗尽了呢? ...
继续阅读 »

前言


我有一个坏习惯:就是下班之后不关电脑,但是电脑一般来说第二天电量不会有什么损失。但是后来突然有一天它开不起来了,上午要罢工?那也不好吧,也不能在光天化日之下划水啊;聪明的我还是找到了原因:电池耗尽了;于是赶紧来查一查原因,到底是什么程序把电池耗尽了呢?


这里我直接揭晓答案:是一个Web应用,看来这个锅必须要前端来背了;我们来梳理一下js中有哪些耗电的操作。


js中有哪些耗电的操作


js中有哪些耗电的操作?我们不如问问浏览器有哪些耗电的操作,浏览器在渲染一个页面的时候经历了GPU进程、渲染进程、网络进程,由此可见持续的GPU绘制、持续的网络请求和页面刷新都会导致持续耗电,那么对应到js中有哪些操作呢?


Ajax、Fetch等网络请求


单只是一个AjaxFetch请求不会消耗多少电量,但是如果有一个定时器一直轮询地向服务器发送请求,那么CPU一直持续运转,并且网络进程持续工作,它甚至有可能会阻止电脑进行休眠,就这样它会一直轮询到电脑电池不足而关机;
所以我们应该尽量少地去做轮询查询;


持续的动画


持续的动画会不断地触发GPU重新渲染,如果没有进行优化的话,甚至会导致主线程不断地进行重排重绘等操作,这样会加速电池的消耗,但是js动画与css动画又不相同,这里我们埋下伏笔,等到后文再讲;


定时器


持续的定时器也可能会唤醒CPU,从而导致电池消耗加速;


上面都是一些加速电池消耗的操作,其实大部分场景都是由于定时器导致的电池消耗,那么下面来看看怎么优化定时器的场景;


针对定时器的优化


先试一试定时器,当我们关闭屏幕时,看定时器回调是否还会执行呢?


let num = 0;
let timer = null;
function poll() {
clearTimeout(timer);
timer = setTimeout(()=>{
console.log("测试后台时是否打印",num++);
poll();
},1000*10)
}

结果如下图,即使暂时关闭屏幕,它仍然会不断地执行:


未命名.png


如果把定时器换成requestAnimationFrame呢?


let num = 0;
let lastCallTime = 0;
function poll() {
requestAnimationFrame(() =>{
const now = Date.now();
if(now - lastCallTime > 1000*10){
console.log("测试raf后台时是否打印",num++);
lastCallTime = now;
}
poll();
});
}

屏幕关闭之前打印到了1,屏幕唤醒之后才开始打印2,真神奇!


未命名.png


当屏幕关闭时回调执行停止了,而且当唤醒屏幕时又继续执行。屏幕关闭时我们不断地去轮询请求,刷新页面,执行动画,有什么意义呢?因此才出现了requestAnimationFrame这个API,浏览器对它进行了优化,用它来代替定时器能减少很多不必要的能耗;


requestAnimationFrame的好处还不止这一点:它还能节省CPU,只要当页面处于未激活状态,它就会停止执行,包括屏幕关闭、标签页切换;对于防抖节流函数,由于频繁触发的回调即使执行次数再多,它的结果在一帧的时间内也只会更新一次,因此不如在一帧的时间内只触发一次;它还能优化DOM更新,将多次的重排放在一次完成,提高DOM更新的性能;


但是如果浏览器不支持,我们必须用定时器,那该怎么办呢?


这个时候可以监听页面是否被隐藏,如果隐藏了那么清除定时器,如果重新展示出来再创建定时器:


let num = 0;
let timer = null;
function poll() {
clearTimeout(timer);
timer = setTimeout(()=>{
console.log("测试后台时是否打印",num++);
poll();
},1000*10)
}

document.addEventListener('visibilitychange',()=>{
if(document.visibilityState==="visible"){
console.log("visible");
poll();
} else {
clearTimeout(timer);
}
})

针对动画优化


首先动画有js动画、css动画,它们有什么区别呢?


打开《我的世界》这款游戏官网,可以看到有一个keyframes定义的动画:


未命名.png


切换到其他标签页再切回来,这个扫描二维码的线会突然跳转;


因此可以得出一个结论:css动画在屏幕隐藏时仍然会执行,但是这一点我们不好控制。


js动画又分为三种种:canvas动画、SVG动画、使用js直接操作css的动画,我们今天不讨论SVG动画,先来看一看canvas动画(MuMu官网):


未命名.png


canvas动画在页面切换之后再切回来能够完美衔接,看起来动画在页面隐藏时也并没有执行;


那么js直接操作css的动画呢?动画还是按照原来的方式一直执行,例如大话西游这个官网的”获奖公示“;针对这种情况我们可以将动画放在requestAnimationFrame中执行,这样就能在用户离开屏幕时停止动画执行


上面我们对大部分情况已经进行优化,那么其他情况我们没办法考虑周到,所以可以考虑判断当前用户电池电量来兼容;


Battery Status API兜底


浏览器给我们提供了获取电池电量的API,我们可以用上去,先看看怎么用这个API:


调用navigator.getBattery方法,该方法返回一个promise,在这个promise中返回了一个电池对象,我们可以监听电池剩余量、电池是否在充电;


navigator.getBattery().then((battery) => {
function updateAllBatteryInfo() {
updateChargeInfo();
updateLevelInfo();
updateChargingInfo();
updateDischargingInfo();
}
updateAllBatteryInfo();

battery.addEventListener("chargingchange", () => {
updateChargeInfo();
});
function updateChargeInfo() {
console.log(`Battery charging? ${battery.charging ? "Yes" : "No"}`);
}

battery.addEventListener("levelchange", () => {
updateLevelInfo();
});
function updateLevelInfo() {
console.log(`Battery level: ${battery.level * 100}%`);
}

battery.addEventListener("chargingtimechange", () => {
updateChargingInfo();
});
function updateChargingInfo() {
console.log(`Battery charging time: ${battery.chargingTime} seconds`);
}

battery.addEventListener("dischargingtimechange", () => {
updateDischargingInfo();
});
function updateDischargingInfo() {
console.log(`Battery discharging time: ${battery.dischargingTime} seconds`);
}
});


当电池处于充电状态,那么我们就什么也不做;当电池不在充电状态并且电池电量已经到达一个危险的值得时候我们需要暂时取消我们的轮询,等到电池开始充电我们再恢复操作


后记


电池如果经常放电到0,这会影响电池的使用寿命,我就是亲身经历者;由于Web应用的轮询,又没有充电,于是一晚上就耗完了所有的电,等到再次使用时电池使用时间下降了好多,真是一次痛心的体验;于是后来我每次下班前将Chrome关闭,并关闭所有聊天软件,但是这样做很不方便,而且很有可能遗忘;


如果每一个前端都能关注到自己的Web程序对于用户电池的影响,然后尝试从定时器和动画这两个方面去优化自己的代码,那么我想应该就不会发生这种事情了。注意:桌面端程序也有可能是披着羊皮的狼,就是披着原生程序的Web应用;


参考:
MDN


作者:蚂小蚁
来源:juejin.cn/post/7206331674296746043
收起阅读 »

一个25岁普通非科班杭漂前端程序员的2023总结唠嗑

引言 大家好,我是努力挣钱的小鑫!全网同名就只有我这个ID哈哈哈 一个普通的 25 岁非科班前端程序员,第一次在掘金发文章,看着自己这 2023 年的各种经历,想着还是写篇咕噜文吧,也算是自己对自己的一个总结,至于写什么,想到啥写啥吧,自己也没啥好文采,都是一...
继续阅读 »

引言


大家好,我是努力挣钱的小鑫!全网同名就只有我这个ID哈哈哈


一个普通的 25 岁非科班前端程序员,第一次在掘金发文章,看着自己这 2023 年的各种经历,想着还是写篇咕噜文吧,也算是自己对自己的一个总结,至于写什么,想到啥写啥吧,自己也没啥好文采,都是一些大白话,还望各位不要介意😀


我是怎么入行前端的?


其实,说白了,就是为了赚钱,自己大学专业是机械电子工程的,来自农村,没啥家庭背景,毕业了大概率进厂打螺丝,因此大二其实我就开始准备转行了(为什么是大二?因为大一我根本没想法!)。


当时对于整个计算机行业没啥方向,就随便找了点资料,有人说学 python 简单,就买了书和视频跟着敲,结果后面了解了说,不好找工作。然后,看到 JAVA 好找工作,学了一个月 python 又转 JAVA 了,但是,学到后面好难,只学了半个月就放弃了。后面就有躺平开玩了。


那是什么时候想到学前端的呢?那是 2020 年 1 月 31 号晚上,那是一个我一辈子都忘不了的晚上,当时我就躺在床上想,自己现在大三了,以后能做什么呢?当程序员吧!


这次吸取了半年前盲目的学习方式,先去了解了各种程序员职业,最后了解到适合非科班从零开始学的就是前端,于是马上找了 B 站的免费课程跟着看,现在还记得是黑马的 Pink 老师,哈哈哈算是我的前端引路人,现在还是很感谢黑马当时的免费课程。


于是就这样,我的前端之旅就开始了,整个疫情我都没闲着,在家学了半年。从基础再到 vue2,再到 6 月份找到实习工作,就这样,我算是入行前端了,后面就是一系列找工作秋招了,有机会后面再一点点聊聊😃。


image.png


我在公司干了啥?


去年 9 月份我刚满 1 年工作经验,然后跳槽到了现在的公司,公司不大,主要业务是 web3d 业务方向的,我自己是负责网页前端,大部分的 3d 都是需要专门的 wbegl 工程师来写,自己做的也只是传统前端的活。老实说,真的就前端切图仔,毕竟 webgl 也不是我一个小小前端能够掌握的。


今年是 AI 年嘛,各家公司都在追热点,我们公司也不例外,跟着搞了对话大模型,我就被分到了 AI 组,给他们写 DEMO,各种对话,反正就是模仿 ChatGPT。


感觉自己这一年就是在各种写 demo 中度过的,不能说是好是坏,好处是可以自己用各种新技术,坏处是没啥成熟的上线项目。


但我比较开心的是,在 9 月份干满一年的时候,公司涨薪了,虽然只涨了 1.5k,但我还是很开心的哈哈哈。毕竟,总比没有好吧。


业余我在干什么?


俗话说,大部分人一辈子,除了工作,就是生活和学习了,今年我大大小小做了好多事情,就拿几个比较典型的说下吧。


减肥 30 斤!


工作两年胖了快 30 + ,不健康的饮食+熬夜,今年4月份下定决心开始减肥,方法是:168轻断食+爬楼梯,半年把两年胖的都减回去了,算是小目标完成,分享给需要减肥的朋友,轻断食+爬楼梯,真的很实用,具体教程可以去b站搜索看看。


image.png


image.png


口琴街头卖艺


大学自己就是在大学城那边街头吹口琴的,工作第一年没吹了,没玩了,口琴都快荒废了,今年在减肥的那段时间,想着要不重操旧业吧,于是买了设备,买了话筒,就在滨江+萧山的江边吹起了口琴,顺便放个二维码,还能赚点外快,虽然不多,大部分时间都是自己玩玩哈哈哈。


通过街头卖艺,我也认识了很多喜欢音乐、喜欢唱歌的小伙伴,其实,真的,发自内心地说,去做自己喜欢的事,是真的超级开心的一件事!!!即使是站一两个小时都不觉得累,甚至还觉得时间为什么过得那么快!


image.png


image.png


image.png


image.png


自己也开始经营了自己的小红书账号,虽然人不多,不定期更新口琴视频哈哈哈哈,喜欢听什么可以点歌哟。努力挣钱的小鑫 • 小红书 / RED (xiaohongshu.com)


image.png


两段失败的追求


今年因为街头卖艺认识了喜欢的女生,她喜欢唱歌,我也喜欢唱歌,可惜终究是我单身久了,自己的单相思罢了。


第二段是小红书认识的,她会吉他,我很喜欢会吉他的女孩子,也见了两次面,但是我总感觉她是在吊着我,我说白了,她也说白了,over,早点结束止损,挺好的。


就这样,一直母单 solo 的我,在 2023 年又多了两份失败的追求经验😢(为什么我要说又呢😭呜呜呜)


2024 的未来展望



  • 坚持减肥,争取明年6月份前减到130的标准体重!

  • 坚持周末抽时间去滨江江边,街头卖艺吹吹口琴,个人的爱好绝对不能荒废!

  • 希望能脱单吧,明年 26 岁了不会还是母单吧😢

  • 技术学习方面的,稍微卷卷差不多得了,我个人的核心思想就是:要抱着目的去学,用到在学,自己知道有这个东西就足够了。

  • 也希望自己能够多去做自己喜欢的事,少些焦虑,切记一切的焦虑都是没有意义的,做就完事了!


结语


其实我还想说很多,但是说多了,就感觉很啰嗦了,毕竟我文本也就那样,就说这么多吧。


最后回过头来看,我的 2023 年是有收获,有遗憾的一年,但无论怎么说,这都是我自己选择的结果,无论好坏,它都是过去的经历了,再回忆也只是为了再反省总结,活在当下,展望未来,继续过自己选择的生活!曾仕强老师说得很棒,一切的一切都是自作自受,都是自己选择过的生活,没什么好埋怨的,共勉各位!



最后交个友吧,杭州的朋友、喜欢唱歌、喜欢音乐的,可以一起交流学习玩呀!!!



作者:努力挣钱的小鑫
来源:juejin.cn/post/7316592697464487962
收起阅读 »

糟糕,怎么就摸了一年多了😦

前言 好快啊,00后从22年初入职场转眼间就在工位上摸了一年。也好久没写过年度记录了,这里就把摸了一整年的23年记录一下。年度关键词嘛,就是摸鱼、学习、旅游、减肥。 工作(摸鱼是常态) 今年工作的主旋律是摸鱼,不是自己想摸,是真的太闲了,没什么工作量给到我。 ...
继续阅读 »

前言


好快啊,00后从22年初入职场转眼间就在工位上摸了一年。也好久没写过年度记录了,这里就把摸了一整年的23年记录一下。年度关键词嘛,就是摸鱼、学习、旅游、减肥。


工作(摸鱼是常态)


今年工作的主旋律是摸鱼,不是自己想摸,是真的太闲了,没什么工作量给到我。


主要就是在已有平台代码上的功能迭代和 bug 修复;


开发了一个支付宝小程序还有其对应的嵌入到浙里办平台的精简版 H5;


负责即将交付给我们的使用 qiankun 微前端技术的新平台前端代码的开发文档编写(这里被主管拉出去问问题难住了,专门写了一篇文章记录《主管让我说说 qiankun 是咋回事》)JY们反馈还不错👻);


其余时间就是一些零零散散的小开发和主管让进行的技术调研和小demo,前段时间让我去尝试用 AssemblyScript 写 wasm 实现在前端离线加解密,然后发现没有现成的类似 crypto-js 的工具库,想实现得自己拿 AssemblyScript 重写一遍😦,抄了个 JavaScript 的加解密算法写了个小 demo 交代了,主管也知道让我自己写个 AS 的 crypto 有点忒为难我了,就说了再议。(话说再前端wasm里实现加解密真的安全吗?用户在wasm 中解密的过程可以被恶意脚本获取到吗,期待了解这方面的JY解答🧐)。


学习(学了就忘的菜)


身在国企,一直有的焦虑就是害怕技术没有长进😕。所以整天想着自己学点什么东西,2022一年,因为刚入职才开始接触React、TypeScript、Next、Taro小程序开发这些,所以平时开发中还挺充实的能学到的不少,平时业务中接触到了一些技术也想着学学什么 Solidity、Golang ,但是没有业务让我这个刚毕业的小前端去用这些技术,过两个月就什么也不会了;


于是我在今年转变思路,我学前端方面的东西总行了吧,然后就:Solid、Svelte、Vue3(Nuxt3)、Angular、Astro、Electron... 我全入了一下门,写了点小demo、小工具。再然后还是没业务做,过了没多久就忘得差不多了;


我还想着去了解了下Nest.js、Flutter、ArkTs这些,结果是 Flutter、ArkTs 这些配环境就给我配恼火了😶‍🌫️,Nest.js 被满眼的装饰器难住了,然后又去了解 TypeScript 装饰器,然后看到了掘金里的使用 TypeScript 做前端OOP开发的,想着自己大学里学的一塌糊涂,像面向对象编程这些思想自己很欠缺,又去到处搜面向对象...... 哈哈哈永远闭环不了了🤣。


生活(多图预警)


流水账式记录😎


4月,和女朋友去了一次苏州。参观了留园、狮子林、西园寺、“大裤衩”......西园寺的素面还挺好吃的。


36f42cee549e82ff5b88b8e6bb59656.jpg


a2db0432bb02804c473b0766501601c.jpg


6月,公司疗休养在舟山躺了三天😴,酒店风景巨好,居然有沙滩。


d513ef3d142de096c9b9abe1c69da57.jpg


a3d1fcc8fd86aa80585ab45d0cba4d1.jpg


9月底,参加了第一次年度体检,盲盒开得惊心动魄,不过除了体脂率和内脏脂肪过高,其他都还好,于是决定十一假期最后放纵一把,收假回来开始减肥。
没想到自己减的还挺快,不到2个月减了20斤😙。


ca93265f23a7aefae97741180a0314c.jpg


10月,女朋友备考研究生压力太大,住进医院动了个小手术,好在手术顺利赶在生日前出了院,和她室友一起给她庆祝了生日。


de8783b4b3bb8817f53a2112bf7f2fb.jpg


12月,陪女朋友在临安考研,青山湖风景很好,就是太冷了🥶。考研随缘了,还好手术后趁着秋招末尾投了几份简历,拿到了一个保底offer。


84721c507b743a55185fa0c42d26ee0.jpg


2f725f020a12dcc86460ced41d110be.jpg


展望(平淡是真,绝不折腾)


展望啥呢,计划赶不上变化,这两年的大环境,看着一起毕业的同学一个个离开杭州,也不想着折腾了,平平淡淡挺好,就希望我爱的人和爱我的人都健健康康。车到山前必有路,走到哪步算哪步。先把自己的肥肉减了再说罢!


作者:HyaCinth
来源:juejin.cn/post/7316795553799208999
收起阅读 »