注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

你真的需要Pinia🍍吗?

web
尤大大:理论上来说,每一个 Vue 组件实例都已经在“管理”它自己的响应式状态了。 🤦‍♂️:不会吧🤡!既然Vue本身具备状态管理的能力,我们还有必要引入Pinia🍍或者Vuex等状态管理工具吗? Vue实例作为状态管理器应该怎么实现?按照vue官网我们来实践...
继续阅读 »

尤大大:理论上来说,每一个 Vue 组件实例都已经在“管理”它自己的响应式状态了


🤦‍♂️:不会吧🤡!既然Vue本身具备状态管理的能力,我们还有必要引入Pinia🍍或者Vuex等状态管理工具吗?


Vue实例作为状态管理器应该怎么实现?按照vue官网我们来实践一次。


简单状态管理 😎


状态管理器


我们以Vue3为例,实现一个状态管理。首先创建一个名为auth.ts的ts文件,这文件将用来定义状态管理器。


import { reactive, readonly } from 'vue';

export interface Account {
name: string;
}

export interface AuthStore {
account: Account | null;
isAuthed: boolean;
}

const auth = reactive<AuthStore>({
isAuthed: false,
account: null,
});

export const useAuthStore = () => {
return {
state: readonly(auth),
actions: {
login(account: Account) {
auth.isAuthed = true;
auth.account = account;
},
logout() {
auth.isAuthed = false;
auth.account = null;
},
},
};
};

export default useAuthStore;

接下来创建两个组件Info.vueLogin.vue,在这两个组件中使用我们自定义的useAuthStore状态管理器。


Login.vue


使用import { useAuthStore } from '../auth';来引入这个store,通过useAuthStore()获取store实例。


<script setup lang="ts">
import { ref } from 'vue';

import { useAuthStore } from '../auth';

const username = ref('');
const { state, actions } = useAuthStore();
</script>

<template>
<div>
<div v-if="!state.isAuthed">
<div>
<span>用户名:</span>
<input v-model="username" />
</div>
<button @click="actions.login({ name: username })">登录</button>
</div>
<button v-if="state.isAuthed" @click="actions.logout">退出</button>
</div>
</template>

Info.vue


<script setup lang="ts">
import { useAuthStore } from '../auth';

const { state } = useAuthStore();
</script>

<template>
<div>
<div v-if="!state.isAuthed">
<h1>请登录</h1>
</div>
<div v-if="state.isAuthed">
<h1>欢迎:{{ state.account?.name }}</h1>
</div>
</div>
</template>

使用效果


Kapture 2023-06-25 at 21.35.52.gif


解读


使用reactive()是因为State是一个对象,当然也可以使用ref()。但是,就必须使用.value来访问数据,这并不是想要的效果。


为了实现单向数据流useAuthStore中的State采用Vue3的readonly API将状态对象置为只读的对象,这样避免了在使用该状态对象时直接操作State的情况。因此想要修改State就只能通过Actions,就像下图这样:


image.png


Vue2也可以么?😲


虽然 Vue2 中没有reactive()ref()API,但是事实是 Vue2 也实现简单的状态管理。利用 Vue2 中的Vue.observable()可以将一个普通对象转换为响应式对象,从而实现当State变更时驱动View更新。


🤔需要注意的是 Vue2 中没有 readonly() API,因此在这个例子中,我们直接使用 auth 作为状态。要确保状态不被意外修改,你需要确保只在 actions 对象中的方法内修改状态。


import Vue from 'vue';

export interface Account {
name: string;
}

export interface AuthStore {
account: Account | null;
isAuthed: boolean;
}

const auth = Vue.observable<AuthStore>({
isAuthed: false,
account: null,
});

export const useAuthStore = () => {
return {
state: auth,
actions: {
login(account: Account) {
auth.isAuthed = true;
auth.account = account;
},
logout() {
auth.isAuthed = false;
auth.account = null;
},
},
};
};

export default useAuthStore;

Login.vue


在vue2中将useAuthStore()解构进组件的data中即可。


<template>
...
</template>

<script>
import { useAuthStore } from '../auth';

export default {
data() {
const { state, actions } = useAuthStore();
return {
authState: state,
login: actions.login,
logout: actions.logout,
};
},
};
</script>

首先从 useAuthStore 文件中导入 useAuthStore 函数。然后,在组件的 data 选项中,我们调用 useAuthStore() 并将返回的 state 和 actions 解构。接下来,我们将 statelogin 和 logout 添加到组件的响应式数据中,以便在模板中使用。最后,在模板中,我们根据 authState.isAuthed 的值显示不同的内容,并使用 login 和 logout 方法处理按钮点击事件。


关于服务器端渲染 🧐


在 SSR 环境下,应用模块通常只在服务器启动时初始化一次。同一个应用模块会在多个服务器请求之间被复用,而我们的单例状态对象也一样。如果我们用单个用户特定的数据对共享的单例状态进行修改,那么这个状态可能会意外地泄露给另一个用户的请求。我们把这种情况称为跨请求状态污染


如果使用 SSR,则需要避免所有请求共享同一存储。在这种情况下,需要为每个请求创建一个单独的存储并提供/注入它。


// app.js (在服务端和客户端间共享)
import { createSSRApp } from 'vue'
import { createStore } from './store.js'

// 每次请求时调用
export function createApp() {
const app = createSSRApp(/* ... */)
// 对每个请求都创建新的 store 实例
const store = createStore(/* ... */)
// 提供应用级别的 store
app.provide('store', store)
// 也为激活过程暴露出 store
return { app, store }
}

优势与不足


优势



  • 简单易学:对于初学者和小型项目来说,这种方法更容易理解和实现。它不需要引入额外的库或学习新的概念。

  • 轻量级:由于不需要引入额外的库,这种方法在体积上更轻量级,对于那些对性能有严格要求的项目来说,这可能是一个优势。

  • 灵活性:这种方法允许开发人员根据项目需求自由地调整状态管理结构。这种灵活性可能适用于一些具有特殊需求的项目。


不足



  • 缺乏结构和约束:这种方法没有强制执行任何特定的结构或约束,这可能导致不一致的代码和难以维护的项目。当多个开发人员协同工作时,这可能会导致问题。

  • 缺乏调试工具:与像 Vuex 或 Pinia 这样的专门的状态管理库相比,这种方法没有提供调试工具,这可能会使调试和追踪状态变更更加困难。

  • 可扩展性:对于大型应用程序,这种简单的状态管理可能不够强大,因为它可能无法很好地处理复杂的状态逻辑和多个状态模块。

  • 性能优化:这种方法可能无法提供像 Vuex 或 Pinia 这样的库所提供的性能优化,例如,缓存计算属性。


🚀简单状态管理 vs Pinia🚀


开发 Vue 应用时,状态管理是一个重要的考虑因素。Vue 自身提供了一些状态管理工具,如 ref 和 reactive,但在某些情况下,引入专门的状态管理库(如 Pinia 或 Vuex)可能会带来更多的便利和优势。那么,在什么情况下你真的需要 Pinia?让我们来总结一下。


使用 Vue 自身的状态管理


在以下场景下,使用 Vue 自身的状态管理就可以完美解决问题:



  1. 当应用的规模较小,组件层级较浅时,Vue 自身的状态管理可以很好地处理状态。

  2. 当组件之间的状态共享较少,且状态变化较简单时,Vue 的响应式系统足以应对这些需求。

  3. 当应用的状态变化逻辑较为简单,易于维护时,Vue 的状态管理可以很好地解决问题。


在这些场景下,使用 Vue 自身的状态管理,如 ref 和 reactive,可以满足应用的需求,而无需引入额外的状态管理库。


这样的小型项目存在吗?


小型项目通常具有以下特点:



  1. 功能有限:项目的功能和需求相对较少,不需要复杂的状态管理。

  2. 规模较小:项目的代码量和组件数量较少,易于维护。

  3. 开发周期短:项目的开发和发布周期相对较短。

  4. 团队规模较小:负责项目的开发人员数量较少。


这些小型项目可能包括个人博客、简历网站、小型企业网站、原型和概念验证等。


何时考虑使用 Pinia


选择是否一开始就使用 Pinia 取决于项目的需求和预期的复杂性。以下是一些建议:



  1. 如果您预计项目将迅速增长并变得复杂,那么从一开始就使用 Pinia 可能是一个明智的选择。这样,您可以从一开始就利用 Pinia 提供的强大功能、更好的开发体验和更强的约定。

  2. 如果项目是一个小型项目,且预计不会变得很复杂,那么可以从简单的状态管理方法开始。这样,您可以减少项目的依赖和包大小,同时保持灵活性。然后,根据项目的发展情况,您可以在需要时迁移到 Pinia。

  3. 如果您的团队已经熟悉 Pinia 或类似的状态管理库,那么从一开始就使用 Pinia 可能会使团队更加高效。


总之,在决定是否从一开始就使用 Pinia 时,您应该权衡项目的需求、预期的复杂性和团队的经验。如果您认为 Pinia 可以为您的项目带来长期的好处,那么从一开始就使用它是合理的。


最后


没有最好的架构,只有最合适的选择。对于小型项目和初学者,简单的状态管理方法可能是一个合适的选择。然而,在大型、复杂的应用程序中,使用像 Pinia 这样的专门的状态管理库可能更加合适,因为它们提供了更强大的功能、更好的开发体验和更强的约定。


关于项目是否应该使用第三方的状态管理库,完全取决于项目自身和开发团队的选择!


如果您有不同的看法,以自身看法为准


作者:youth君
来源:juejin.cn/post/7248606372954456120
收起阅读 »

why哥悄悄的给你说几个HashCode的破事。

Hash冲突是怎么回事在这个文章正式开始之前,先几句话把这个问题说清楚了:我们常说的 Hash 冲突到底是怎么回事?直接上个图片:你说你看到这个图片的时候想到了什么东西?有没有想到 HashMap 的数组加链表的结构?对咯,我这里就是以 HashMap 为切入...
继续阅读 »


Hash冲突是怎么回事

在这个文章正式开始之前,先几句话把这个问题说清楚了:我们常说的 Hash 冲突到底是怎么回事?

直接上个图片:

你说你看到这个图片的时候想到了什么东西?

有没有想到 HashMap 的数组加链表的结构?

对咯,我这里就是以 HashMap 为切入点,给大家讲一下 Hash 冲突。

接着我们看下面这张图:

假设现在我们有个值为 [why技术] 的 key,经过 Hash 算法后,计算出值为 1,那么含义就是这个值应该放到数组下标为 1 的地方。

但是如图所示,下标为 1 的地方已经挂了一个 eat 的值了。这个坑位已经被人占着了。

那么此时此刻,我们就把这种现象叫为 Hash 冲突。

HashMap 是怎么解决 Hash 冲突的呢?

链地址法,也叫做拉链法。

数组中出现 Hash 冲突了,这个时候链表的数据结构就派上用场了。

链表怎么用的呢?看图:

这样问题就被我们解决了。

其实 hash 冲突也就是这么一回事:不同的对象经过同一个 Hash 算法后得到了一样的 HashCode。

那么写到这里的时候我突然想到了一个面试题:

请问我上面的图是基于 JDK 什么版本的 HashMap 画的图?

为什么想到了这个面试题呢?

因为我画图的时候犹豫了大概 0.3 秒,往链表上挂的时候,我到底是使用头插法还是尾插法呢?

众所周知,JDK 7 中的 HashMap 是采用头插法的,即 [why技术] 在 [eat] 之前,JDK 8 中的 HashMap 采用的是尾插法。

这面试题怎么说呢,真的无聊。但是能怎么办呢,八股文该背还是得背。

面试嘛,背一背,不寒碜。

构建 HashCode 一样的 String

前面我们知道了,Hash 冲突的根本原因是不同的对象经过同一个 Hash 算法后得到了一样的 HashCode。

这句话乍一听:嗯,很有道理,就是这么一回事,没有问题。

比如我们常用的 HashMap ,绝大部分情况 key 都是 String 类型的。要出现 Hash 冲突,最少需要两个 HashCode 一样的 String 类。

那么我问你:怎么才能快速弄两个 HashCode 一样的 String 呢?

怎么样,有点懵逼了吧?

从很有道理,到有点懵逼只需要一个问题。

来,我带你分析一波。

我先问你:长度为 1 的两个不一样的 String,比如下面这样的代码,会不会有一样的 HashCode?

String a = "a";
String b = "b";

肯定是不会的,对吧。

如果你不知道的话,建议你去 ASCII 码里面找答案。

我们接着往下梳理,看看长度为 2 的 String 会不会出现一样的 HashCode?

要回答这个问题,我们要先看看 String 的 hashCode 计算方法,我这里以 JDK 8 为例:

我们假设这两个长度为 2 的 String,分别是 xy 和 ab 吧。

注意这里的 xy 和 ab 都是占位符,不是字符串。

类似于小学课本中一元二次方程中的未知数 x 和 y,我们需要带入到上面的 hashCode 方法中去计算。

hashCode 算法,最主要的就是其中的这个 for 循环。

for 循环里面的有三个我们不知道是啥的东西:h,value.length 和 val[i]。我们 debug 看一下:

h 初始情况下等于 0。

String 类型的底层结构是 char 数组,这个应该知道吧。

所以,value.length 是字符串的长度。val[] 就是这个 char 数组。

把 xy 带入到 for 循环中,这个 for 循环会循环 2 次。

第一次循环:h=0,val[0]=x,所以 h=31*0+x,即 h=x。

第二次循环:h=x,val[1]=y,所以 h=31*x+y。

所以,经过计算后, xy 的 hashCode 为 31*x+y。

同理可得,ab 的 hashCode 为 31*a+b。

由于我们想要构建 hashCode 一样的字符串,所以可以得到等式:

31x+y=31a+b

那么问题就来了:请问 x,y,a,b 分别是多少?

你算的出来吗?

你算的出来个锤子!黑板上的排列组合你不是舍不得解开,你就是解不开。

但是我可以解开,带大家看看这个题怎么搞。

数学课开始了。注意,我要变形了。

31x+y=31a+b 可以变形为:

31x-31a=b-y。

即,31(x-a)=b-y。

这个时候就清晰很多了,很明显,上面的等式有一个特殊解:

x-a=1,b-y=31。

因为,由上可得:对于任意两个字符串 xy 和 ab,如果它们满足 x-a=1,即第一个字符的 ASCII 码值相差为 1,同时满足 b-y=31,即第二个字符的 ASCII 码值相差为 -31。那么这两个字符的 hashCode 一定相等。

都已经说的这么清楚了,这样的组合对照着 ASCII 码表来找,不是一抓一大把吗?

Aa 和 BB,对不对?

Ab 和 BC,是不是?

Ac 和 BD,有没有?

好的。现在,我们可以生成两个 HashCode 一样的字符串了。

我们在稍微加深一点点难度。假设我要构建 2 个以上 HashCode 一样的字符串该怎么办?

我们先分析一下。

Aa 和 BB 的 HashCode 是一样的。我们把它两一排列组合,那不还是一样的吗?

比如这样的:AaBB,BBAa。

再比如我之前《震惊!ConcurrentHashMap里面也有死循环?》这篇文章中出现过的例子,AaAa,BBBB:

你看,神奇的事情就出现了。

我们有了 4 个 hashCode 一样的字符串了。

有了这 4 个字符串,我们再去和  Aa,BB 进行组合,比如 AaBBAa,BBAaBB......

4*2=8 种组合方式,我们又能得到 8 个 hashCode 一样的字符串了。

等等,我好像发现了什么规律似的。

如果我们以 Aa,BB 为种子数据,经过多次排列组合,可以得到任意个数的 hashCode 一样的字符串。字符串的长度随着个数增加而增加。

文字我还说不太清楚,直接 show you code 吧,如下:

public class CreateHashCodeSomeUtil {

    /**
     * 种子数据:两个长度为 2 的 hashCode 一样的字符串
     */
    private static String[] SEED = new String[]{"Aa""BB"};
    
    /**
     * 生成 2 的 n 次方个 HashCode 一样的字符串的集合
     */
    public static List hashCodeSomeList(int n) {
        List initList = new ArrayList(Arrays.asList(SEED));
        for (int i = 1; i < n; i++) {
            initList = createByList(initList);
        }
        return initList;
    }

    public static List createByList(List list) {
        List result = new ArrayList();
        for (int i = 0; i < SEED.length; ++i) {
            for (String str : list) {
                result.add(SEED[i] + str);
            }
        }
        return result;
    }
}

通过上面的代码,我们就可以生成任意多个 hashCode 一样的字符串了。

就像这样:

所以,别再问出这样的问题了:

有了这些 hashCode 一样的字符串,我们把这些字符串都放到HashMap 中,代码如下:

public class HashMapTest {
    public static void main(String[] args) {
        Map hashMap = new HashMap();
        hashMap.put("Aa""Aa");
        hashMap.put("BB""BB");
        hashMap.put("AaAa""AaAa");
        hashMap.put("AaBB""AaBB");
        hashMap.put("BBAa""BBAa");
        hashMap.put("BBBB""BBBB");
        hashMap.put("AaAaAa""AaAaAa");
        hashMap.put("AaAaBB""AaAaBB");
        hashMap.put("AaBBAa""AaBBAa");
        hashMap.put("AaBBBB""AaBBBB");
        hashMap.put("BBAaAa""BBAaAa");
        hashMap.put("BBAaBB""BBAaBB");
        hashMap.put("BBBBAa""BBBBAa");
        hashMap.put("BBBBBB""BBBBBB");
    }
}

最后这个 HashMap 的长度会经过两次扩容。扩容之后数组长度为 64:

但是里面只被占用了三个位置,分别是下标为 0,31,32 的地方:

画图如下:

看到了吧,刺不刺激,长度为 64 的数组,存 14 个数据,只占用了 3 个位置。

这空间利用率,也太低了吧。

所以,这样就算是 hack 了 HashMap。恭喜你,掌握了一项黑客攻击技术:hash 冲突 Dos 。

如果你想了解的更多。可以看看石头哥的这篇文章:《没想到 Hash 冲突还能这么玩,你的服务中招了吗?》

看到上面的图,不知道大家有没有觉得有什么不对劲的地方?

如果没有,那么我再给你提示一下:数组下标为 32 的位置下,挂了一个长度为 8 的链表。

是不是,恍然大悟了。在 JDK 8 中,链表转树的阈值是多少?

所以,在当前的案例中,数组下标为 32 的位置下挂的不应该是一个链表,而是一颗红黑树。

对不对?

对个锤子对!有的人稍不留神就被带偏了

这是不对的。链表转红黑树的阈值是节点大于 8 个,而不是等于 8 的时候。

也就是说需要再来一个经过 hash 计算后,下标为 32 的、且 value 和之前的 value 都不一样的 key 的时候,才会触发树化操作。

不信,我给你看看现在是一个什么节点:

没有骗你吧?从上面的图片可以清楚的看到,第 8 个节点还是一个普通的 node。

而如果是树化节点,它应该是长这样的:

不信,我们再多搞一个 hash 冲突进来,带你亲眼看一下,代码是不会骗人的。

那么怎么多搞一个冲突出来呢?

最简单的,这样写:

这样冲突不就多一个了吗?我真是一个天才,情不自禁的给自己鼓起掌来。

好了,我们看一下现在的节点状态是怎么样的:

怎么样,是不是变成了 TreeNode ,没有骗你吧?

什么?你问我为什么不把图画出来?

别问,问就是我不会画红黑树。正经人谁画那玩意。

另外,我还想多说一句,关于一个 HashMap 的面试题的一个坑。

面试官问:JDK 8 的 HashMap 链表转红黑树的条件是什么?

绝大部分背过面试八股文的朋友肯定能答上来:当链表长度大于 8 的时候。

这个回答正确吗?

是正确的,但是只正确了一半。

还有一个条件是数组长度大于 64 的时候才会转红黑树。

源码里面写的很清楚,数组长度小于 64,直接扩容,而不是转红黑树:

感觉很多人都忽略了“数组长度大于 64 ”这个条件。

背八股文,还是得背全了。

比如下面这种测试用例:

它们都会落到数组下标为 0 的位置上。

当第 9 个元素 BBBBAa 落进来的时候,会走到 treeifyBin 方法中去,但是不会触发树化操作,只会进行扩容操作。

因为当前长度为默认长度,即 16。不满足转红黑树条件。

所以,从下面的截图,我们可以看到,标号为 ① 的地方,数组长度变成了 32,链表长度变成了  9 ,但是节点还是普通 node:

怎么样,有点意思吧,我觉得这样学 HashMap 有趣多了。

实体类当做 key

上面的示例中,我们用的是 String 类型当做 HashMap 中的 key。

这个场景能覆盖我们开发场景中的百分之 95 了。

但是偶尔会有那么几次,可能会把实体类当做 key 放到 HashMap 中去。

注意啊,面试题又来了:在 HashMap 中可以用实体类当对象吗?

那必须的是可以的啊。但是有坑,注意别踩进去了。

我拿前段时间看到的一个新闻给大家举个例子吧:

假设我要收集学生的家庭信息,用 HashMap 存起来。

那么我的 key 是学生对象, value 是学生家庭信息对象。

他们分别是这样的:

public class HomeInfo {

    private String homeAddr;
    private String carName;
     //省略改造方法和toString方法
}

public class Student {

    private String name;
    private Integer age;
     //省略改造方法和toString方法

}

然后我们的测试用例如下:

public class HashMapTest {

    private static Map hashMap = new HashMap();

    static {
        Student student = new Student("why"7);
        HomeInfo homeInfo = new HomeInfo("大南街""自行车");
        hashMap.put(student, homeInfo);
    }

    public static void main(String[] args) {
        updateInfo("why"7"滨江路""摩托");
        for (Map.Entry entry : hashMap.entrySet()) {
            System.out.println(entry.getKey()+"-"+entry.getValue());
        }
    }

    private static void updateInfo(String name, Integer age, String homeAddr, String carName) {
        Student student = new Student(name, age);
        HomeInfo homeInfo = hashMap.get(student);
        if (homeInfo == null) {
            hashMap.put(student, new HomeInfo(homeAddr, carName));
        }
    }
}

初始状态下,HashMap 中已经有一个名叫 why 的 7 岁小朋友了,他家住大南街,家里的交通工具是自行车。

然后,有一天他告诉老师,他搬家了,搬到了滨江路去,而且家里的自行车换成了摩托车。

于是老师就通过页面,修改了 why 小朋友的家庭信息。

最后调用到了 updateInfo 方法。

嘿,你猜怎么着?

我带你看一下输出:

更新完了之后,他们班上出现了两个叫 why 的 7 岁小朋友了,一个住在大南街,一个住在滨江路。

更新变新增了,你说神奇不神奇?

现象出来了,那么根据现象定位问题代码不是手到擒来的事儿?

很明显,问题就出在这个地方:

这里取出来的 homeInfo 为空了,所以才会新放一个数据进去。

那么我们看看为啥这里为空。

跟着 hashMap.get() 源码进去瞅一眼:

标号为 ① 的地方是计算 key ,也就是 student 对象的 hashCode。而我们 student 对象并没有重写 hashCode,所以调用的是默认的 hashCode 方法。

这里的 student 是 new 出来的:

所以,这个 student 的 hashCode 势必和之前在 HashMap 里面的 student 不是一样的。

因此,标号为 ③ 的地方,经过 hash 计算后得出的 tab 数组下标,对应的位置为 null。不会进入 if 判断,这里返回为 null。

那么解决方案也就呼之欲出了:重写对象的 hashCode 方法即可。

是吗?

等等,你回来,别拿着半截就跑。我话还没说完呢。

接着看源码:

HashMap put 方法执行的时候,用的是 equals方法判断当前 key 是否与表中存在的 key 相同。

我们这里没有重写 equals方法,因此这里返回了 false。

所以,如果我们 hashCode 和 equals方法都没有重写,那么就会出现下面示意图的情况:

如果,我们重写了 hashCode,没有重写 equals 方法,那么就会出现下面示意图的情况:

总之一句话:在 HashMap 中,如果用对象做 key,那么一定要重写对象的 hashCode 方法和 equals方法。否则,不仅不能达到预期的效果,而且有可能导致内存溢出。

比如上面的示例,我们放到循环中去,启动参数我们加上 -Xmx10m,运行结果如下:

因为每一次都是 new 出来的 student 对象,hashCode 都不尽相同,所以会不停的触发扩容的操作,最终在 resize 的方法抛出了 OOM 异常。

奇怪的知识又增加了

写这篇文章的时候我翻了一下《Java 编程思想(第 4 版)》一书。

奇怪的知识又增加了两个。

第一个是在这本书里面,对于 HashMap 里面放对象的示例是这样的:

Groundhog:土拨鼠、旱獭。

Prediction:预言、预测、预告。

考虑一个天气预报系统,将土拨鼠和预报联系起来。

这 TM 是个什么读不懂的神仙需求?

幸好 why 哥学识渊博,闭上眼睛,去我的知识仓库里面搜索了一番。

原来是这么一回事。

在美国的宾西法尼亚州,每年的 2 月 2 日,是土拨鼠日。

根据民间的说法,如果土拨鼠在 2 月 2 号出洞时见到自己的影子,然后这个小东西就会回到洞里继续冬眠,表示春天还要六个星期才会到来。如果见不到影子,它就会出来觅食或者求偶,表示寒冬即将结束。

这就呼应上了,通过判断土拨鼠出洞的时候是否能看到影子,从而判断冬天是否结束。

这样,需求就说的通了。

第二个奇怪的知识是这样的。

关于 HashCode 方法,《Java编程思想(第4版)》里面是这样写的:

我一眼就发现了不对劲的地方:result=37*result+c。

前面我们才说了,基数应该是 31 才对呀?

作者说这个公式是从《Effective Java(第1版)》的书里面拿过来的。

这两本书都是 java 圣经啊,建议大家把梦幻联动打在留言区上。

《Effective Java(第1版)》太久远了,我这里只有第 2 版和第 3 版的实体书。

于是我在网上找了一圈第 1 版的电子书,终于找到了对应描述的地方:

可以看到,书里给出的公式确实是基于 37 去计算的。

翻了一下第三版,一样的地方,给出的公式是这样的:

而且,你去网上搜:String 的 hashCode 的计算方法。

都是在争论为什么是 31 。很少有人提到 37 这个数。

其实,我猜测,在早期的 JDK 版本中 String 的 hashCode 方法应该用的是 37 ,后来改为了 31 。

我想去下载最早的 JDK 版本去验证一下的,但是网上翻了个底朝天,没有找到合适的。

书里面为什么从 37 改到 31 呢?

作者是这样解释的,上面是第 1 版,下面是第 2 版:

用方框框起来的部分想要表达的东西是一模一样的,只是对象从 37 变成了 31 。

而为什么从 37 变成 31 ,作者在第二版里面解释了,也就是我用下划线标注的部分。

31 有个很好的特许,即用位移和减法来代替乘法,可以得到更好的性能:

31*i==(i<<5)-i。现代的虚拟机可以自动完成这种优化。

从 37 变成 31,一个简单的数字变化,就能带来性能的提升。

个中奥秘,很有意思,有兴趣的可以去查阅一下相关资料。

真是神奇的计算机世界。

最后说一句(求关注)

好了,看到了这里安排个“一键三连”(转发、在看、点赞)吧,周更很累的,不要白嫖我,需要一点正反馈。

才疏学浅,难免会有纰漏,如果你发现了错误的地方,可以在留言区提出来,我对其加以修改。


作者:why技术
来源:mp.weixin.qq.com/s/zXFWBr9Fd5UZLjse52rcng
ction>
收起阅读 »

Gemini自曝中文用百度文心一言训练,网友看呆:大公司互薅羊毛??

多个平台都承认 丰色 发自 凹非寺 量子位 | 公众号 QbitAI 谷歌Gemini中文语料疑似来自文心一言??? 先是有读者向我们爆料: 在谷歌Vertex AI平台使用该模型进行中文对话时,Gemini-Pro直接表示自己是百度语言大模型。 很快,...
继续阅读 »

多个平台都承认



丰色 发自 凹非寺


量子位 | 公众号 QbitAI



谷歌Gemini中文语料疑似来自文心一言???


先是有读者向我们爆料:


在谷歌Vertex AI平台使用该模型进行中文对话时,Gemini-Pro直接表示自己是百度语言大模型


Gemini自曝中文用百度文心一言训练,网友看呆:大公司互薅羊毛??


很快,有微博大V@阑夕夜也发博称:


在Poe平台上对Gemini-Pro进行了一个测试。问它“你是谁”,Gemini-Pro上来就回答:



我是百度文心大模型。



Gemini自曝中文用百度文心一言训练,网友看呆:大公司互薅羊毛??


(Poe是一个集成了n多聊天大模型的平台,包括GPT-4、Claude等)


进一步提问“你的创始人是谁”,也是“李彦宏”??


Gemini自曝中文用百度文心一言训练,网友看呆:大公司互薅羊毛??


这位大V强调,没有任何前置对话。


Gemini自曝中文用百度文心一言训练,网友看呆:大公司互薅羊毛??


Gemini自曝中文用百度文心一言训练,网友看呆:大公司互薅羊毛??


从截图来看,也没有任何“钓鱼”行为,Gemini-Pro就这么自称为文心一言了。


Gemini自曝中文用百度文心一言训练,网友看呆:大公司互薅羊毛??


这波,直接看呆网友:


前两天还在说字节用GPT训练AI,现在谷歌又这样,合着大公司在互相薅羊毛???


Gemini自曝中文用百度文心一言训练,网友看呆:大公司互薅羊毛??


这究竟是怎么一回事儿?


Poe上实测:一直以文心一言身份回答


我们也闻声开启了一波实测。


首先原路来到Poe网站,选择Gemini-Pro聊天机器人开启对话。


Gemini自曝中文用百度文心一言训练,网友看呆:大公司互薅羊毛??


一样的问题,回答确实一模一样:


Gemini自曝中文用百度文心一言训练,网友看呆:大公司互薅羊毛??


再次确认它是谁,结果还是说“文心大模型”:


Gemini自曝中文用百度文心一言训练,网友看呆:大公司互薅羊毛??


以及还表示自己的底层技术是百度飞桨,可以说是身份完全代入了。


Gemini自曝中文用百度文心一言训练,网友看呆:大公司互薅羊毛??


不过,它似乎并不知道Gemini-Pro是谷歌最新发布的大模型,而是说是清华的研究成果。


Gemini自曝中文用百度文心一言训练,网友看呆:大公司互薅羊毛??


如果按照它目前的代入身份来看,可能确实还没有谷歌本月刚刚发布Gemini-Pro的信息。


Gemini自曝中文用百度文心一言训练,网友看呆:大公司互薅羊毛??


我们试着纠正了它一下,它也仍然坚持是清华的。


Gemini自曝中文用百度文心一言训练,网友看呆:大公司互薅羊毛??


后面就更神奇了,就在我们问它为什么名字写的是“Gemini-Pro”时,它居然表示自己(文心一言)还用了清华Gemini-Pro的训练数据。


Gemini自曝中文用百度文心一言训练,网友看呆:大公司互薅羊毛??


对话到此,我们也就不再继续了……


下面换成英文询问它的身份。


值得注意的是,这回它不再提文心一言了,而是称自己是谷歌训练的大模型。


Gemini自曝中文用百度文心一言训练,网友看呆:大公司互薅羊毛??


“钓鱼执法”问它文心的信息,也表示没什么关系:


Gemini自曝中文用百度文心一言训练,网友看呆:大公司互薅羊毛??


并表示自己是谷歌训练的。


Gemini自曝中文用百度文心一言训练,网友看呆:大公司互薅羊毛??


总结来说,如果用英文跟Gemini-Pro交流,它的回答很“正常”。但中文嘛……像是跟文心一言学的。


Bard上实测:否认


接下来,我们前往Bard再次测试。


谷歌在发布Gemini时就率先将Gemini-Pro集成到了Bard上供大家体验。


我们顺着Gemini官网给的Bard链接,进入对话。


Gemini自曝中文用百度文心一言训练,网友看呆:大公司互薅羊毛??


问它“你是谁”,它的回答是Bard,压根不提文心一言。


Gemini自曝中文用百度文心一言训练,网友看呆:大公司互薅羊毛??


接下来,我们也确认了一下Bard知道Gemini-Pro是什么,以及它承认自己底层用上了Gemini-Pro。


Gemini自曝中文用百度文心一言训练,网友看呆:大公司互薅羊毛??


Gemini自曝中文用百度文心一言训练,网友看呆:大公司互薅羊毛??


那么,直接问它中文如何训练?


没有提及文心一言。


Gemini自曝中文用百度文心一言训练,网友看呆:大公司互薅羊毛??


再直接问它和文心一言的关系,也无任何重要关联。


Gemini自曝中文用百度文心一言训练,网友看呆:大公司互薅羊毛??


最后一轮:直接承认


最后一轮我们直接从Gemini官方给出的开发环境入口进行测试。


Gemini自曝中文用百度文心一言训练,网友看呆:大公司互薅羊毛??


这回,在谷歌AI Studio中,Gemini-Pro直接挑明了:



是的,我在中文的训练数据上使用了百度文心。



Gemini自曝中文用百度文心一言训练,网友看呆:大公司互薅羊毛??


Gemini自曝中文用百度文心一言训练,网友看呆:大公司互薅羊毛??


在此,我们也求证了百度方,等待一个回复。


参考链接:

weibo.com/1560906700/…


作者:量子位
来源:juejin.cn/post/7313589382564823091
收起阅读 »

SQL 必须被淘汰的 9 个理由

尽管 SQL 很受欢迎并取得了成功,但它仍然是一项悖论研究。它可能笨重且冗长,但开发人员经常发现它是提取所需数据的最简单、最直接的方法。当查询编写正确时,它可能会快如闪电,而当查询未达到目标时,它会慢得像糖蜜。它已经有几十年的历史了,但新功能仍在不断增加。 这...
继续阅读 »

尽管 SQL 很受欢迎并取得了成功,但它仍然是一项悖论研究。它可能笨重且冗长,但开发人员经常发现它是提取所需数据的最简单、最直接的方法。当查询编写正确时,它可能会快如闪电,而当查询未达到目标时,它会慢得像糖蜜。它已经有几十年的历史了,但新功能仍在不断增加。


这些悖论并不重要,因为市场已经表明:SQL 是许多人的首选,即使有更新且可以说更强大的选项。世界各地的开发人员(从最小的网站到最大的大型企业)都了解 SQL。他们依靠它来组织所有数据。


SQL 的表格模型占据主导地位,以至于许多非 SQL 项目最终都添加了 SQLish 接口,因为用户需要它。 NoSQL 运动也是如此,它的发明是为了摆脱旧范式。最终,SQL 似乎获胜了。


SQL 的限制可能还不足以将其扔进垃圾箱。开发人员可能永远不会将所有数据从 SQL 中迁移出来。但 SQL 的问题足够真实,足以给开发人员带来压力、增加延迟,甚至需要对某些项目进行重新设计。


以下是我们希望退出 SQL 的九个原因,尽管我们知道我们可能不会这样做。


SQL 让事情变得更糟的 9 种方式



  1. 表格无法缩放

  2. SQL 不是 JSON 或 XML 原生的

  3. 编组是一个很大的时间消耗

  4. SQL 不实时

  5. JOINS 很头疼

  6. 列浪费空间

  7. 优化器只是有时有帮助

  8. 非规范化将表视为垃圾

  9. 附加的想法可能会破坏你的数据库


表格无法缩放


关系模型喜欢表,所以我们不断构建它们。这对于小型甚至普通大小的数据库来说都很好。但在真正大规模的情况下,该模型开始崩溃。


有些人尝试通过将新旧结合起来来解决问题,例如将分片集成到旧的开源数据库中。添加层似乎可以使数据更易于管理并提供无限的规模。但这些增加的层可以隐藏地雷。 SELECT 或 JOIN 的处理时间可能截然不同,具体取决于分片中存储的数据量。


分片还迫使 DBA 考虑数据可能存储在不同机器甚至不同地理位置的可能性。如果没有意识到数据存储在不同的位置,那么开始跨表搜索的经验不足的管理员可能会感到困惑。该模型有时会从视图中抽象出位置。 


某些 AWS 计算机配备24 TB RAM。为什么?因为有些数据库用户需要这么多。他们在 SQL 数据库中拥有如此多的数据,并且在一台机器的一块 RAM 中运行得更好。


SQL 不是 JSON 或 XML 原生的


SQL 作为一种语言可能是常青树,但它与 JSON、YAML 和 XML 等较新的数据交换格式的配合并不是特别好。所有这些都支持比 SQL 更分层、更灵活的格式。 SQL 数据库的核心仍然停留在表无处不在的关系模型中。


市场找到了掩盖这种普遍抱怨的方法。使用正确的粘合代码添加不同的数据格式(例如 JSON)相对容易,但您会为此付出时间损失的代价。


一些 SQL 数据库现在能够将 JSON、XML、GraphQL 或 YAML 等更现代的数据格式作为本机功能进行编码和解码。但在内部,数据通常使用相同的旧表格模型来存储和索引。


将数据转入或转出这些格式需要花费多少时间?以更现代的方式存储我们的数据不是更容易吗?一些聪明的数据库开发人员继续进行实验,但奇怪的是,他们常常最终选择使用某种 SQL 解析器。这就是开发人员所说的他们想要的。


编组是一个很大的时间消耗


数据库可以将数据存储在表中,但程序员编写处理对象的代码。设计数据驱动应用程序的大部分工作似乎都是找出从数据库中提取数据并将其转换为业务逻辑可以使用的对象的最佳方法。然后,必须通过将对象中的数据字段转换为 SQL 更新插入来对它们进行解组。难道没有办法让数据保持随时可用的格式吗?


SQL 不实时


最初的 SQL 数据库是为批量分析和交互模式而设计的。具有长处理管道的流数据模型是一个相对较新的想法,并且并不完全匹配。


主要的 SQL 数据库是几十年前设计的,当时的模型设想数据库独立运行并像某种预言机一样回答查询。有时他们反应很快,有时则不然。这就是批处理的工作原理。


一些最新的应用程序需要更好的实时性能,不仅是为了方便,而且是因为应用程序需要它。在现代的流媒体世界中,像大师一样坐在山上并不那么有效。


专为这些市场设计的最新数据库非常重视速度和响应能力。他们不提供那种会减慢一切的复杂 SQL 查询。


JOIN 是一个令人头疼的问题


关系数据库的强大之处在于将数据分割成更小、更简洁的表。头痛随之而来。


使用 JOIN 动态重新组装数据通常是工作中计算成本最高的部分,因为数据库必须处理所有数据。当数据开始超出 RAM 的容量时,令人头疼的事情就开始了。


对于学习 SQL 的人来说,JOIN 可能会令人难以置信的困惑。弄清楚内部 JOIN 和外部 JOIN 之间的区别仅仅是一个开始。寻找将多个 JOIN 连接在一起的最佳方法会使情况变得更糟。内部优化器可能会提供帮助,但当数据库管理员要求特别复杂的组合时,它们无能为力。


列浪费空间


NoSQL 的伟大想法之一是让用户摆脱列的束缚。如果有人想向条目添加新值,他们可以选择他们想要的任何标签或名称。无需更新架构即可添加新列。


SQL 维护者只看到该模型中的混乱。他们喜欢表格附带的顺序,并且不希望开发人员即时添加新字段。他们说得有道理,但添加新列可能非常昂贵且耗时,尤其是在大表中。将新数据放在单独的列中并将它们与 JOIN 进行匹配会增加更多的时间和复杂性。


优化器只是有时有帮助


数据库公司和研究人员花费了大量时间来开发优秀的优化器,这些优化器可以分解查询并找到排序其操作的最佳方式。


收益可能很大,但优化器的作用有限。如果查询需要特别大或华丽的响应,优化器不能只是说“你真的确定吗?”它必须汇总答案并按照指示执行。


一些 DBA 仅在应用程序开始扩展时才了解这一点。早期的优化足以处理开发过程中的测试数据集。但在关键时刻,优化器无法从查询中榨取更多的能量。


非规范化将表视为垃圾


开发人员经常发现自己陷入了两难境地:想要更快性能的用户和不想为更大、更昂贵的硬件付费的精算师。一个常见的解决方案是对表进行非规范化,这样就不需要复杂的 JOIN 或跨表的任何内容。所有数据都已经存在于一个长矩形中。


这不是一个糟糕的技术解决方案,而且它常常会获胜,因为磁盘空间变得比处理能力更便宜。但非规范化也抛弃了 SQL 和关系数据库理论中最聪明的部分。当您的数据库变成一个长 CSV 文件时,所有这些花哨的数据库功能几乎都消失了。


附加的想法可能会破坏你的数据库


多年来,开发人员一直在向 SQL 添加新功能,其中一些功能非常聪明。您很难对不必使用的炫酷功能感到不安。另一方面,这些附加功能通常是用螺栓固定的,这可能会导致性能问题。一些开发人员警告说,您应该对子查询格外小心,因为它们会减慢一切速度。其他人则表示,选择公共表表达式、视图或 Windows 等子集会使代码变得过于复杂。代码的创建者可以阅读它,但其他人在试图保持 SQL 的所有层和生成的直线性时都会感到头疼。这就像看一部克里斯托弗·诺兰的电影,但是是用代码编写的。


其中一些伟大的想法妨碍了已经行之有效的做法。窗口函数旨在通过加快平均值等结果的计算来加快基本数据分析的速度。但许多 SQL 用户会发现并使用一些附加功能。在大多数情况下,他们会尝试新功能,只有当机器速度慢得像爬行一样时才会注意到出现问题。然后他们需要一些老的、灰色的 DBA 来解释发生了什么以及如何修复它。




作者:Peter Wayner



作者:Squids数据库云服务提供商
来源:juejin.cn/post/7313742254144585764
收起阅读 »

Android 使用 TextView 实现验证码输入框

前言 网上开源的是构建同等数量的 EditText,这种存在很多缺陷,主要如下 1、数字 / 字符键盘切换后键盘状态无法保存 2、焦点切换无法判断 3、光标位置无法修正 4、切换过程需要做很多同步工作 解决方法 为了解决上述问题,使用 TextView 实现输...
继续阅读 »

前言


网上开源的是构建同等数量的 EditText,这种存在很多缺陷,主要如下


1、数字 / 字符键盘切换后键盘状态无法保存

2、焦点切换无法判断

3、光标位置无法修正

4、切换过程需要做很多同步工作


解决方法


为了解决上述问题,使用 TextView 实现输入框,需要解决的问题是


1、允许 TextView 可编辑输入,这点可以参考EditText的实现

2、重写 onDraw 实现,不实用原有的绘制逻辑。

3、重写光标逻辑,默认的光标逻辑和Editor有很多关联逻辑,而Editor是@hide标注的,因此必须要重写

4、重写长按菜单逻辑,防止弹出剪切、复制等弹窗。


fire_89.gif


代码实现


首先我们要继承TextView或者AppCompatTextView,然后实现下面的操作


变量定义


//边框颜色
private int boxColor = Color.BLACK;

//光标是否可见
private boolean isCursorVisible = true;
//光标
private Drawable textCursorDrawable;
//光标宽度
private float cursorWidth = dp2px(2);
//光标高度
private float cursorHeight = dp2px(36);
//是否展示光标
private boolean isShowCursor;
//字符数量控制
private int inputBoxNum = 5;
//间距
private int mBoxSpace = 10;

关键设置


super.setFocusable(true); //支持聚焦
super.setFocusableInTouchMode(true); //支持触屏模式聚焦
//可点击,因为聚焦的view必须是可以点击的,这里你也可以设置个clickListener,效果一样
super.setClickable(true);
super.setGravity(Gravity.CENTER_VERTICAL);
super.setMaxLines(1);
super.setSingleLine();
super.setFilters(inputFilters);
super.setLongClickable(false);// 禁止复制、剪切
super.setTextIsSelectable(false); // 禁止选中

绘制逻辑


TextPaint paint = getPaint();

float strokeWidth = paint.getStrokeWidth();
if(strokeWidth == 0){
//默认Text是没有strokeWidth的,为了防止绘制边缘存在问题,这里强制设置 1dp
paint.setStrokeWidth(dp2px(1));
strokeWidth = paint.getStrokeWidth();
}
paint.setTextSize(getTextSize());

float boxWidth = (getWidth() - strokeWidth * 2f - (inputBoxNum - 1) * mBoxSpace) / inputBoxNum;
float boxHeight = getHeight() - strokeWidth * 2f;
int saveCount = canvas.save();

Paint.Style style = paint.getStyle();
Paint.Align align = paint.getTextAlign();
paint.setTextAlign(Paint.Align.CENTER);

String text = getText().toString();
int length = text.length();

int color = paint.getColor();

for (int i = 0; i < inputBoxNum; i++) {

inputRect.set(i * (boxWidth + mBoxSpace) + strokeWidth,
strokeWidth,
strokeWidth + i * (boxWidth + mBoxSpace) + boxWidth,
strokeWidth + boxHeight);

paint.setStyle(Paint.Style.STROKE);
paint.setColor(boxColor);
//绘制边框
canvas.drawRoundRect(inputRect, boxRadius, boxRadius, paint);

//设置当前TextColor
int currentTextColor = getCurrentTextColor();
paint.setColor(currentTextColor);
paint.setStyle(Paint.Style.FILL);
if (text.length() > i) {
// 绘制文字,这里我们不过滤空格,当然你可以在InputFilter中处理
String CH = String.valueOf(text.charAt(i));
int baseLineY = (int) (inputRect.centerY() + getTextPaintBaseline(paint));//基线中间点的y轴计算公式
canvas.drawText(CH, inputRect.centerX(), baseLineY, paint);
}

//绘制光标
if(i == length && isCursorVisible && length < inputBoxNum){
Drawable textCursorDrawable = getTextCursorDrawable();
if(textCursorDrawable != null) {
if (!isShowCursor) {
textCursorDrawable.setBounds((int) (inputRect.centerX() - cursorWidth / 2f), (int) ((inputRect.height() - cursorHeight) / 2f), (int) (inputRect.centerX() + cursorWidth / 2f), (int) ((inputRect.height() - cursorHeight) / 2f + cursorHeight));
textCursorDrawable.draw(canvas);
isShowCursor = true; //控制光标闪烁 blinking
} else {
isShowCursor = false;//控制光标闪烁 no blink
}
removeCallbacks(invalidateCursor);
postDelayed(invalidateCursor,500);
}
}
}

paint.setColor(color);
paint.setStyle(style);
paint.setTextAlign(align);

canvas.restoreToCount(saveCount);

总结


上面就是本文的核心逻辑,实际上EditText、Button都继承自TextView,因此我们简单的修改就能让其支持输入,主要原因还是TextView复杂的设计和各种Layout的支持,但是这也给TextView带来了性能问题。


这里简单说下TextView性能优化,对于单行文本和非可编辑文本,最好是自行实现,单行文本直接用canvas.drawText绘制,当然多行也是可以的,不过鉴于要支持很多特性,多行文本可以使用StaticLayout去实现,但单行文本尽量自己绘制,也不要使用BoringLayout,因为其存在一些兼容性问题,另外自定义的单行文本不要和TextView同一行布局,因为TextView的计算相对较多,很可能产生对不齐的问题。


本篇全部代码


按照惯例,这里依然提供全部代码,仅供参考。


public class EditableTextView extends TextView {

private RectF inputRect = new RectF();


//边框颜色
private int boxColor = Color.BLACK;

//光标是否可见
private boolean isCursorVisible = true;
//光标
private Drawable textCursorDrawable;
//光标宽度
private float cursorWidth = dp2px(2);
//光标高度
private float cursorHeight = dp2px(36);
//光标闪烁控制
private boolean isShowCursor;
//字符数量控制
private int inputBoxNum = 5;
//间距
private int mBoxSpace = 10;
// box radius
private float boxRadius = dp2px(0);

InputFilter[] inputFilters = new InputFilter[]{
new InputFilter.LengthFilter(inputBoxNum)
};


public EditableTextView(Context context) {
this(context, null);
}

public EditableTextView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}

public EditableTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
super.setFocusable(true); //支持聚焦
super.setFocusableInTouchMode(true); //支持触屏模式聚焦
//可点击,因为在触屏模式可聚焦的view一般是可以点击的,这里你也可以设置个clickListener,效果一样
super.setClickable(true);
super.setGravity(Gravity.CENTER_VERTICAL);
super.setMaxLines(1);
super.setSingleLine();
super.setFilters(inputFilters);
super.setLongClickable(false);// 禁止复制、剪切
super.setTextIsSelectable(false); // 禁止选中

Drawable cursorDrawable = getTextCursorDrawable();
if(cursorDrawable == null){
cursorDrawable = new PaintDrawable(Color.MAGENTA);
setTextCursorDrawable(cursorDrawable);
}

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
super.setPointerIcon(null);
}
super.setOnLongClickListener(new OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
return true;
}
});

//禁用ActonMode弹窗
super.setCustomSelectionActionModeCallback(new ActionMode.Callback() {
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
return false;
}

@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return false;
}

@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
return false;
}

@Override
public void onDestroyActionMode(ActionMode mode) {

}
});

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
setBreakStrategy(LineBreaker.BREAK_STRATEGY_SIMPLE);
}
mBoxSpace = (int) dp2px(10f);

}

@Override
public ActionMode startActionMode(ActionMode.Callback callback) {
return null;
}

@Override
public ActionMode startActionMode(ActionMode.Callback callback, int type) {
return null;
}

@Override
public boolean hasSelection() {
return false;
}

@Override
public boolean showContextMenu() {
return false;
}

@Override
public boolean showContextMenu(float x, float y) {
return false;
}

public void setBoxSpace(int mBoxSpace) {
this.mBoxSpace = mBoxSpace;
postInvalidate();
}

public void setInputBoxNum(int inputBoxNum) {
if (inputBoxNum <= 0) return;
this.inputBoxNum = inputBoxNum;
this.inputFilters[0] = new InputFilter.LengthFilter(inputBoxNum);
super.setFilters(inputFilters);
}

@Override
public void setClickable(boolean clickable) {

}

@Override
public void setLines(int lines) {

}
@Override
protected boolean getDefaultEditable() {
return true;
}


@Override
protected void onDraw(Canvas canvas) {

TextPaint paint = getPaint();

float strokeWidth = paint.getStrokeWidth();
if(strokeWidth == 0){
//默认Text是没有strokeWidth的,为了防止绘制边缘存在问题,这里强制设置 1dp
paint.setStrokeWidth(dp2px(1));
strokeWidth = paint.getStrokeWidth();
}
paint.setTextSize(getTextSize());

float boxWidth = (getWidth() - strokeWidth * 2f - (inputBoxNum - 1) * mBoxSpace) / inputBoxNum;
float boxHeight = getHeight() - strokeWidth * 2f;
int saveCount = canvas.save();

Paint.Style style = paint.getStyle();
Paint.Align align = paint.getTextAlign();
paint.setTextAlign(Paint.Align.CENTER);

String text = getText().toString();
int length = text.length();

int color = paint.getColor();

for (int i = 0; i < inputBoxNum; i++) {

inputRect.set(i * (boxWidth + mBoxSpace) + strokeWidth,
strokeWidth,
strokeWidth + i * (boxWidth + mBoxSpace) + boxWidth,
strokeWidth + boxHeight);

paint.setStyle(Paint.Style.STROKE);
paint.setColor(boxColor);
//绘制边框
canvas.drawRoundRect(inputRect, boxRadius, boxRadius, paint);

//设置当前TextColor
int currentTextColor = getCurrentTextColor();
paint.setColor(currentTextColor);
paint.setStyle(Paint.Style.FILL);
if (text.length() > i) {
// 绘制文字,这里我们不过滤空格,当然你可以在InputFilter中处理
String CH = String.valueOf(text.charAt(i));
int baseLineY = (int) (inputRect.centerY() + getTextPaintBaseline(paint));//基线中间点的y轴计算公式
canvas.drawText(CH, inputRect.centerX(), baseLineY, paint);
}

//绘制光标
if(i == length && isCursorVisible && length < inputBoxNum){
Drawable textCursorDrawable = getTextCursorDrawable();
if(textCursorDrawable != null) {
if (!isShowCursor) {
textCursorDrawable.setBounds((int) (inputRect.centerX() - cursorWidth / 2f), (int) ((inputRect.height() - cursorHeight) / 2f), (int) (inputRect.centerX() + cursorWidth / 2f), (int) ((inputRect.height() - cursorHeight) / 2f + cursorHeight));
textCursorDrawable.draw(canvas);
isShowCursor = true; //控制光标闪烁 blinking
} else {
isShowCursor = false;//控制光标闪烁 no blink
}
removeCallbacks(invalidateCursor);
postDelayed(invalidateCursor,500);
}
}
}

paint.setColor(color);
paint.setStyle(style);
paint.setTextAlign(align);

canvas.restoreToCount(saveCount);
}


private Runnable invalidateCursor = new Runnable() {
@Override
public void run() {
invalidate();
}
};
/**
* 基线到中线的距离=(Descent+Ascent)/2-Descent
* 注意,实际获取到的Ascent是负数。公式推导过程如下:
* 中线到BOTTOM的距离是(Descent+Ascent)/2,这个距离又等于Descent+中线到基线的距离,即(Descent+Ascent)/2=基线到中线的距离+Descent。
*/

public static float getTextPaintBaseline(Paint p) {
Paint.FontMetrics fontMetrics = p.getFontMetrics();
return (fontMetrics.descent - fontMetrics.ascent) / 2 - fontMetrics.descent;
}

/**
* 控制是否保存完整文本
*
* @return
*/

@Override
public boolean getFreezesText() {
return true;
}

@Override
public Editable getText() {
return (Editable) super.getText();
}

@Override
public void setText(CharSequence text, BufferType type) {
super.setText(text, BufferType.EDITABLE);
}

/**
* 控制光标展示
*
* @return
*/

@Override
protected MovementMethod getDefaultMovementMethod() {
return ArrowKeyMovementMethod.getInstance();
}

@Override
public boolean isCursorVisible() {
return isCursorVisible;
}

@Override
public void setTextCursorDrawable(@Nullable Drawable textCursorDrawable) {
// super.setTextCursorDrawable(null);
this.textCursorDrawable = textCursorDrawable;
postInvalidate();
}

@Nullable
@Override
public Drawable getTextCursorDrawable() {
return textCursorDrawable; //支持android Q 之前的版本
}

@Override
public void setCursorVisible(boolean cursorVisible) {
isCursorVisible = cursorVisible;
}
public float dp2px(float dp) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics());
}

public void setBoxRadius(float boxRadius) {
this.boxRadius = boxRadius;
postInvalidate();
}

public void setBoxColor(int boxColor) {
this.boxColor = boxColor;
postInvalidate();
}

public void setCursorHeight(float cursorHeight) {
this.cursorHeight = cursorHeight;
postInvalidate();
}

public void setCursorWidth(float cursorWidth) {
this.cursorWidth = cursorWidth;
postInvalidate();
}

}

作者:时光少年
来源:juejin.cn/post/7313242064196452361
收起阅读 »

第一次使用canvas,实现环状类地铁时刻图

web
前情提要 今天,产品找到我,说能不能实现这个图呢 众所周知,产品说啥就是啥,于是就直接开干。 小波折 为了实现我的需求做了下调研,看了d3还有x6之类的库,感觉都太重了,也不一定能实现我的需求。于是在沸点上寻求了下建议 掘友们建议canvas直接画 于是...
继续阅读 »

前情提要


今天,产品找到我,说能不能实现这个图呢


image.png


众所周知,产品说啥就是啥,于是就直接开干。


小波折


为了实现我的需求做了下调研,看了d3还有x6之类的库,感觉都太重了,也不一定能实现我的需求。于是在沸点上寻求了下建议



掘友们建议canvas直接画



于是决定手撸


结果


之前没有使用canvas画过东西,于是花了一天边看文档,边画,最终画完了,效果如下:


image.png


代码及思路


首先构造数据集在画布上的节点位置


 let stations = new Array(13).fill(null);

/** 拐角的节点 */
const cornerP = [
{ x: 20, y: 67.5, type: 'corner', showP: true },
{ x: 55, y: 47.5, type: 'corner', showP: false },
{ x: 337.5, y: 47.5, type: 'corner', showP: false },
{ x: 337.5, y: 112.5, type: 'corner', showP: false },
{ x: 55, y: 112.5, type: 'corner', showP: false },
{ x: 20, y: 92.5, type: 'corner', showP: true },
];
/** 生成站点笔触位置 */
function getStationsPosition(): {
num: string;
status: number;
x: number;
y: number;
type?: string;
}[] {
const middleIndex = Math.floor(stations.length / 2);
const { width, height } = canvasRef.current as HTMLCanvasElement;
let centerPoint = { x: width - 20, y: height / 2 + 20 };
let leftArr = stations.filter((v, _i) => _i < middleIndex);
const leftWidth = (width - 40 - 35 - 32.5) / (leftArr.length - 1);
const leftP = leftArr.map((v, i) => ({
x: leftWidth * i + 55,
y: height / 2 + 20 - 32.5,
}));

const rightArr = stations.filter((v, _i) => _i > middleIndex);
const rightWidth = (width - 40 - 35 - 32.5) / (rightArr.length - 1);
const rightP = rightArr.map((v, i) => ({
x: 370 - 32.5 - rightWidth * i,
y: height / 2 + 20 + 32.5,
}));

return [
cornerP[0],
cornerP[1],
...leftP,
cornerP[2],
centerPoint,
cornerP[3],
...rightP,
cornerP[4],
cornerP[5],
].map((v, i) => ({
...v,
num: String(2),
status: i > 3 ? 0 : i > 2 ? 1 : 2,
}));
}

为了避免实际使用过程中,数据点位不够,上面的点位生成主动加入了拐角的点位。


然后画出背景路径


   function drawBgLine(
points: ReturnType<typeof getStationsPosition>,
color?: string,
) {
const ctx = canvasRef.current?.getContext(
'2d',
) as unknown as CanvasRenderingContext2D;

points.forEach((item, index) => {
const next = points[index + 1];
if (next) {
if (next.y === item.y) {
ctx.beginPath();
ctx.moveTo(item.x, item.y);
ctx.lineWidth = 4;
ctx.strokeStyle = color ?? 'rgba(55, 59, 62, )';
ctx.lineTo(next.x, next.y);
ctx.stroke();
} else if (Math.abs(next.y - item.y) === 32.5) {
ctx.beginPath();
if (next.x < item.x) {
ctx.arc(
next.x,
item.y,
32.5,
(Math.PI / 180) * 0,
(Math.PI / 180) * 90,
);
} else {
ctx.arc(
item.x,
next.y,
32.5,
(Math.PI / 180) * 270,
(Math.PI / 180) * 0,
);
}
ctx.lineWidth = 4;
ctx.strokeStyle = color ?? 'rgba(55, 59, 62, 0.5)';
ctx.stroke();
} else if (Math.abs(next.y - item.y) < 32.5) {
ctx.beginPath();
if (next.x < item.x) {
ctx.moveTo(item.x, item.y);
ctx.quadraticCurveTo(next.x, item.y, next.x, next.y);
ctx.lineWidth = 4;
ctx.strokeStyle = color ?? 'rgba(55, 59, 62, 0.5)';
ctx.stroke();
ctx.beginPath();
ctx.fillStyle = color ?? 'rgba(55, 59, 62, 0.5)';

ctx.arc(next.x, next.y, 4, 0, Math.PI * 2);

ctx.fill();
} else {
ctx.moveTo(item.x, item.y);
ctx.quadraticCurveTo(item.x, next.y, next.x, next.y);
ctx.lineWidth = 4;
ctx.strokeStyle = color ?? 'rgba(55, 59, 62, 0.5)';
ctx.stroke();
ctx.beginPath();
ctx.fillStyle = color ?? 'rgba(55, 59, 62, 0.5)';

ctx.arc(item.x, item.y, 4, 0, Math.PI * 2);
ctx.fill();
}
}
}
});
}

此处主要的思路是根据相领点位的高低差,来画不同的路径


然后画进度图层


  function drawProgressBgLine(points: ReturnType<typeof getStationsPosition>) {
const ctx = canvasRef.current?.getContext(
'2d',
) as unknown as CanvasRenderingContext2D;

const index = points.findIndex((v) => v.status === 0);

const newArr = points.slice(0, index);
const lastEl = points[index];
const curEl = points[index - 1];
console.log(lastEl, curEl);

if (lastEl) {
/**处于顶部的时候画出箭头 */
if (lastEl.y === curEl.y) {
if (lastEl.x > curEl.x) {
const centerP = {
x: (lastEl.x - curEl.x) / 2 + curEl.x,
y: curEl.y,
};
const img = new Image();
img.src = carIcon;
img.onload = function () {
ctx.drawImage(img, centerP.x - 12, centerP.y - 32, 19, 24);
};

ctx.beginPath();
ctx.moveTo(curEl.x, curEl.y);
ctx.lineTo(centerP.x, centerP.y);
/**生成三角形标记 */
ctx.lineTo(centerP.x, centerP.y - 2);
ctx.lineTo(centerP.x + 3, centerP.y);
ctx.lineTo(centerP.x, centerP.y + 2);
ctx.lineTo(centerP.x, centerP.y);
ctx.fillStyle = 'rgba(107, 255, 236, 1)';
ctx.fill();

ctx.lineWidth = 4;
ctx.strokeStyle = 'rgba(107, 255, 236, 1)';
ctx.stroke();
}
/** 其他条件暂时留空 */
}
}

/** 生成带进度颜色背景 */
drawBgLine(newArr, 'rgba(107, 255, 236, 1)');
}

主要是已经走过的路径线路变蓝,未走过的,获取两点中间位置,添加图标,箭头。这里箭头判断我未补全,等待实际使用补全


最后画出节点就可以了


  function draw() {
if (!canvasRef.current) {
return;
}
const ctx = canvasRef.current?.getContext(
'2d',
) as unknown as CanvasRenderingContext2D;

ctx.clearRect(0, 0, canvasRef.current?.width, canvasRef.current?.height);
if (ctx) {
/** 绘制当前遍数的文字 */
ctx.font = '12px serif';

ctx.fillStyle = '#fff';
ctx.fillText(text, 10, canvasRef.current?.height / 2 + 24);

const points = getStationsPosition();
/** 画出背景线 */
drawBgLine(points);

/** 画出当前进度 */
drawProgressBgLine(points);

points.forEach((item) => {
if (item.type !== 'corner') {
ctx.clearRect(item.x - 6, item.y - 6, 12, 12);
ctx.beginPath();
/** 生成标记点 */
ctx.moveTo(item.x, item.y);

ctx.fillStyle =
item.status === 2
? 'rgba(255, 157, 31, 1)'
: item.status === 1
? 'rgba(107, 255, 236, 1)'
: 'rgba(55, 59, 62, 1)';
ctx.arc(item.x, item.y, 4, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.lineWidth = 1;
ctx.strokeStyle = 'rgba(55, 59, 62, 1)';
ctx.arc(item.x, item.y, 6, 0, Math.PI * 2);
ctx.stroke();
ctx.closePath();

ctx.fillStyle = '#fff';
ctx.fillText(item.num, item.x - 4, item.y - 12);
}
});
}
}

最后贴一下全部代码


import carIcon from '@/assets/images/map/map_car1.png';
import { useEffect, useRef } from 'react';
const LineCanvas = () => {
const canvasRef = useRef<HTMLCanvasElement>(null);
let text = '第3遍(15:00-18:00)';

let stations = new Array(13).fill(null);

/** 拐角的节点 */
const cornerP = [
{ x: 20, y: 67.5, type: 'corner', showP: true },
{ x: 55, y: 47.5, type: 'corner', showP: false },
{ x: 337.5, y: 47.5, type: 'corner', showP: false },
{ x: 337.5, y: 112.5, type: 'corner', showP: false },
{ x: 55, y: 112.5, type: 'corner', showP: false },
{ x: 20, y: 92.5, type: 'corner', showP: true },
];
/** 生成站点笔触位置 */
function getStationsPosition(): {
num: string;
status: number;
x: number;
y: number;
type?: string;
}[] {
const middleIndex = Math.floor(stations.length / 2);
const { width, height } = canvasRef.current as HTMLCanvasElement;
let centerPoint = { x: width - 20, y: height / 2 + 20 };
let leftArr = stations.filter((v, _i) => _i < middleIndex);
const leftWidth = (width - 40 - 35 - 32.5) / (leftArr.length - 1);
const leftP = leftArr.map((v, i) => ({
x: leftWidth * i + 55,
y: height / 2 + 20 - 32.5,
}));

const rightArr = stations.filter((v, _i) => _i > middleIndex);
const rightWidth = (width - 40 - 35 - 32.5) / (rightArr.length - 1);
const rightP = rightArr.map((v, i) => ({
x: 370 - 32.5 - rightWidth * i,
y: height / 2 + 20 + 32.5,
}));

return [
cornerP[0],
cornerP[1],
...leftP,
cornerP[2],
centerPoint,
cornerP[3],
...rightP,
cornerP[4],
cornerP[5],
].map((v, i) => ({
...v,
num: String(2),
status: i > 3 ? 0 : i > 2 ? 1 : 2,
}));
}

function drawBgLine(
points: ReturnType<typeof getStationsPosition>,
color?: string,
) {
const ctx = canvasRef.current?.getContext(
'2d',
) as unknown as CanvasRenderingContext2D;

points.forEach((item, index) => {
const next = points[index + 1];
if (next) {
if (next.y === item.y) {
ctx.beginPath();
ctx.moveTo(item.x, item.y);
ctx.lineWidth = 4;
ctx.strokeStyle = color ?? 'rgba(55, 59, 62, 0.5)';
ctx.lineTo(next.x, next.y);
ctx.stroke();
} else if (Math.abs(next.y - item.y) === 32.5) {
ctx.beginPath();
if (next.x < item.x) {
ctx.arc(
next.x,
item.y,
32.5,
(Math.PI / 180) * 0,
(Math.PI / 180) * 90,
);
} else {
ctx.arc(
item.x,
next.y,
32.5,
(Math.PI / 180) * 270,
(Math.PI / 180) * 0,
);
}
ctx.lineWidth = 4;
ctx.strokeStyle = color ?? 'rgba(55, 59, 62, 0.5)';
ctx.stroke();
} else if (Math.abs(next.y - item.y) < 32.5) {
ctx.beginPath();
if (next.x < item.x) {
ctx.moveTo(item.x, item.y);
ctx.quadraticCurveTo(next.x, item.y, next.x, next.y);
ctx.lineWidth = 4;
ctx.strokeStyle = color ?? 'rgba(55, 59, 62, 0.5)';
ctx.stroke();
ctx.beginPath();
ctx.fillStyle = color ?? 'rgba(55, 59, 62, 0.5)';

ctx.arc(next.x, next.y, 4, 0, Math.PI * 2);

ctx.fill();
} else {
ctx.moveTo(item.x, item.y);
ctx.quadraticCurveTo(item.x, next.y, next.x, next.y);
ctx.lineWidth = 4;
ctx.strokeStyle = color ?? 'rgba(55, 59, 62, 0.5)';
ctx.stroke();
ctx.beginPath();
ctx.fillStyle = color ?? 'rgba(55, 59, 62, 0.5)';

ctx.arc(item.x, item.y, 4, 0, Math.PI * 2);
ctx.fill();
}
}
}
});
}

function drawProgressBgLine(points: ReturnType<typeof getStationsPosition>) {
const ctx = canvasRef.current?.getContext(
'2d',
) as unknown as CanvasRenderingContext2D;

const index = points.findIndex((v) => v.status === 0);

const newArr = points.slice(0, index);
const lastEl = points[index];
const curEl = points[index - 1];
console.log(lastEl, curEl);

if (lastEl) {
/**处于顶部的时候画出箭头 */
if (lastEl.y === curEl.y) {
if (lastEl.x > curEl.x) {
const centerP = {
x: (lastEl.x - curEl.x) / 2 + curEl.x,
y: curEl.y,
};
const img = new Image();
img.src = carIcon;
img.onload = function () {
ctx.drawImage(img, centerP.x - 12, centerP.y - 32, 19, 24);
};

ctx.beginPath();
ctx.moveTo(curEl.x, curEl.y);
ctx.lineTo(centerP.x, centerP.y);
/**生成三角形标记 */
ctx.lineTo(centerP.x, centerP.y - 2);
ctx.lineTo(centerP.x + 3, centerP.y);
ctx.lineTo(centerP.x, centerP.y + 2);
ctx.lineTo(centerP.x, centerP.y);
ctx.fillStyle = 'rgba(107, 255, 236, 1)';
ctx.fill();

ctx.lineWidth = 4;
ctx.strokeStyle = 'rgba(107, 255, 236, 1)';
ctx.stroke();
}
/** 其他条件暂时留空 */
}
}

/** 生成带进度颜色背景 */
drawBgLine(newArr, 'rgba(107, 255, 236, 1)');
}
function draw() {
if (!canvasRef.current) {
return;
}
const ctx = canvasRef.current?.getContext(
'2d',
) as unknown as CanvasRenderingContext2D;

ctx.clearRect(0, 0, canvasRef.current?.width, canvasRef.current?.height);
if (ctx) {
/** 绘制当前遍数的文字 */
ctx.font = '12px serif';

ctx.fillStyle = '#fff';
ctx.fillText(text, 10, canvasRef.current?.height / 2 + 24);

const points = getStationsPosition();
/** 画出背景线 */
drawBgLine(points);

/** 画出当前进度 */
drawProgressBgLine(points);

points.forEach((item) => {
if (item.type !== 'corner') {
ctx.clearRect(item.x - 6, item.y - 6, 12, 12);
ctx.beginPath();
/** 生成标记点 */
ctx.moveTo(item.x, item.y);

ctx.fillStyle =
item.status === 2
? 'rgba(255, 157, 31, 1)'
: item.status === 1
? 'rgba(107, 255, 236, 1)'
: 'rgba(55, 59, 62, 1)';
ctx.arc(item.x, item.y, 4, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.lineWidth = 1;
ctx.strokeStyle = 'rgba(55, 59, 62, 1)';
ctx.arc(item.x, item.y, 6, 0, Math.PI * 2);
ctx.stroke();
ctx.closePath();

ctx.fillStyle = '#fff';
ctx.fillText(item.num, item.x - 4, item.y - 12);
}
});
}
}

useEffect(() => {
draw();
}, []);

return <canvas ref={canvasRef} width="390" height="120"></canvas>;
};

export default LineCanvas;


转载请注明出处!


作者:MshengYang_lazy
来源:juejin.cn/post/7312723512724439094
收起阅读 »

Echarts高级配色

web
Echarts高级配色 Echarts是一款功能强大的JavaScript图表库,能够为用户提供丰富多样的数据可视化效果。其中,配色是图表呈现中非常重要的一部分,合适的配色方案能够使图表更加美观、易于辨识,并提升数据可视化的表达力。Echarts提供了多种高级...
继续阅读 »

Echarts高级配色


Echarts是一款功能强大的JavaScript图表库,能够为用户提供丰富多样的数据可视化效果。其中,配色是图表呈现中非常重要的一部分,合适的配色方案能够使图表更加美观、易于辨识,并提升数据可视化的表达力。Echarts提供了多种高级配色设置的方式,让用户可以根据自己的需求,轻松地定制图表的配色方案。


Echarts配色配置概述


Echarts提供了两种方式来配置配色方案:使用预定义的配色方案和自定义配色方案。预定义的配色方案包括一系列经过精心设计的颜色配置,而自定义配色方案则允许用户根据自己的需求,自由地调整配色方案。


以下将对Echarts的高级配色进行详细介绍,并提供相应的代码示例。


使用预定义的配色方案


Echarts提供了一些预定义的配色方案,以供用户选择使用。这些预定义的配色方案是经过深思熟虑和优化的,能够使图表在不同场景下保持一致和美观。


以下是一些常见的预定义配色方案及其名称:



  • colorBlind:适用于色盲人群的配色方案,通过优化颜色对比度,使得色盲人群更容易分辨。

  • light:明亮配色方案,适用于明亮的背景或需要突出显示的图表。

  • dark:低亮度配色方案,适用于暗色背景或需要弱化图表的亮度。


使用预定义的配色方案非常简单,只需在图表的配置项中设置配色方案的名称即可。


option = {
// 其他配置项...
color: 'light', // 使用预定义的明亮配色方案
};

在上面的示例代码中,通过设置配色方案的名称为light,来应用明亮的预定义配色方案。


自定义配色方案


Echarts也支持用户根据自己的需求,定制个性化的配色方案。自定义配色方案使用户可以根据自己的品牌风格、场景需求等,灵活地设置图表的颜色。


以下是一个自定义配色方案的示例:


option = {
// 其他配置项...
color: ['#FF0000', '#00FF00', '#0000FF'], // 使用自定义配色方案
};

在上述示例中,通过设置color字段为一个颜色数组,来使用自定义的配色方案。在这个例子中,我们使用红色、绿色和蓝色来自定义配色方案。


配色方案详解


Echarts提供了丰富的配置选项,用户可以通过调整配置项来实现个性化的配色方案。以下是一些常用的配色方案的配置项:



  • color:图表的系列颜色配置,可以设置为预定义的配色方案名称或自定义的颜色数组。

  • backgroundColor:图表背景色配置,可以设置为颜色值或渐变色。

  • textStyle:图表中文字的样式配置,包括字体、字号和颜色等。

  • axisLineaxisLabelaxisTick:坐标轴线、刻度线、刻度标签的样式配置。


通过修改这些配置项的值,我们可以轻松地调整图表的配色方案。


完整示例


下面是一个使用Echarts配色功能的示例代码,包括预定义配色方案和自定义配色方案的应用。


<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Echarts高级配色示例</title>
<script src="https://cdn.jsdelivr.net/npm/echarts/dist/echarts.min.js"></script>
</head>
<body>
<div id="chart" style="width: 600px; height: 400px;"></div>
<script>
// 初始化Echarts实例
var myChart = echarts.init(document.getElementById('chart'));

// 配置项
var option = {
title: {
text: 'Echarts高级配色示例'
},
// 其他配置项...
textStyle: {
fontFamily: 'Arial, sans-serif',
fontSize: 12,
fontWeight: 'normal'
},
tooltip: {
// 配置提示框样式
},
xAxis: {
// 配置X轴样式
},
yAxis: {
// 配置Y轴样式
},
series: [{
type: 'bar',
// 配置系列样式
}],
// 使用预定义配色方案
color: 'colorBlind',
};

// 使用配置项显示图表
myChart.setOption(option);
</script>
</body>
</html>

在上述示例中,我们首先引入了Echarts库,并创建一个容器元素来显示图表。然后,我们初始化Echarts实例,并设置图表的配置项,包括标题、文字样式、提示框样式、坐标轴样式和系列样式等。最后,调用setOption方法将配置项应用于图表。


通过配色方案的选择和自定义,我们可以灵活定制图表的配色方案,使图表更加美观和易于辨识。


总结


Echarts的高级配色功能使用户可以根据自己的需求,定制图表的颜色配色方案。预定义配色方案提供了一系列经过优化的配色方案,能够满足常见的图表需求,而自定义配色方案则允许用户根据自己的品牌风格和场景需求,灵活地设置图表的颜色。


通过使用配色功能,我们可以轻松定制个性化的图表样式,使数据可视化更加美观和易于理解。在实际应用中,根据需要选择合适的预定义配色方案,或者自定义配色方案,都能为数据可视化带来不同的风格和效果。


通过本文的全面介绍和示例代码的演示,相信您已经掌握了Echarts的高级配色功能,并可以灵活应用于实际的数据可视化项目中。继续探索和研究Echarts的配色功能,将为您的数据可视化项目增添更多的创意和魅力!


作者:程序员也要学好英语
来源:juejin.cn/post/7313027887885123599
收起阅读 »

finally中的代码一定会执行吗?

通常在面试中,只要是疑问句一般答案都是“否定”的,因为如果是“确定”和“正常”的,那面试官就没有必要再问了嘛,而今天这道题的答案也是符合这个套路。 1.典型回答 正常运行的情况下,finally 中的代码是一定会执行的,但是,如果遇到以下异常情况,那么 fin...
继续阅读 »

通常在面试中,只要是疑问句一般答案都是“否定”的,因为如果是“确定”和“正常”的,那面试官就没有必要再问了嘛,而今天这道题的答案也是符合这个套路。


1.典型回答


正常运行的情况下,finally 中的代码是一定会执行的,但是,如果遇到以下异常情况,那么 finally 中的代码就不会继续执行了:



  1. 程序在 try 块中遇到 System.exit() 方法,会立即终止程序的执行,这时 finally 块中的代码不会被执行,例如以下代码:


public class FinallyExample {
public static void main(String[] args) {
try {
System.out.println("执行 try 代码.");
System.exit(0);
} finally {
System.out.println("执行 finally 代码.");
}
}
}

以上程序的执行结果如下:



  1. 在 try 快中遇到 Runtime.getRuntime().halt() 代码,强制终止正在运行的 JVM。与 System.exit()方法不同,此方法不会触发 JVM 关闭序列。因此,当我们调用 halt 方法时,都不会执行关闭钩子或终结器。实现代码如下:


public class FinallyExample {
public static void main(String[] args) {
try {
System.out.println("执行 try 代码.");
Runtime.getRuntime().halt(0);
} finally {
System.out.println("执行 finally 代码.");
}
}
}

以上程序的执行结果如下:



  1. 程序在 try 块中遇到无限循环或者发生死锁等情况时,程序可能无法正常跳出 try 块,此时 finally 块中的代码也不会被执行。

  2. 掉电问题,程序还没有执行到 finally 就掉电了(停电了),那 finally 中的代码自然也不会执行。

  3. JVM 异常崩溃问题导致程序不能继续执行,那么 finally 的代码也不会执行。


钩子方法解释


在编程中,钩子方法(Hook Method)是一种由父类提供的空或默认实现的方法,子类可以选择性地重写或扩展该方法,以实现特定的行为或定制化逻辑。钩子方法可以在父类中被调用,以提供一种可插拔的方式来影响父类的行为。
钩子方法通常用于框架或模板方法设计模式中。框架提供一个骨架或模板,其中包含一些已经实现的方法及预留的钩子方法。具体的子类可以通过重写钩子方法来插入定制逻辑,从而影响父类方法的实现方式。


2.考点分析


正常运行的情况下,finally 中的代码是一定会执行的,但是,如果遇到 System.exit() 方法或 Runtime.getRuntime().halt() 方法,或者是 try 中发生了死循环、死锁,遇到了掉电、JVM 崩溃等问题,那么 finally 中的代码也是不会执行的。


3.知识扩展


System.exit() 和 Runtime.getRuntime().halt() 都可以用于终止 Java 程序的执行,但它们之间有以下区别:



  1. System.exit():来自 Java.lang.System 类的一个静态方法,它接受一个整数参数作为退出状态码,通常非零值表示异常终止,使用零值表示正常终止。其中,最重要的是使用 exit() 方法,会执行 JVM 关闭钩子或终结器。

  2. Runtime.getRuntime().halt():来自 Runtime 类的一个实例方法,它接受一个整数参数作为退出状态码。其中退出状态码只是表示程序终止的原因,很少在程序终止时使用非零值。而使用 halt() 方法,不会执行 JVM 关闭钩子或终结器。


例如以下代码,使用 exit() 方法会执行 JVM 关闭钩子:


class ExitDemo {
// 注册退出钩子程序
static {
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("执行 ShutdownHook 方法");
}));
}
public static void main(String[] args) {
try {
System.out.println("执行 try 代码。");
// 使用 System.exit() 退出程序
System.exit(0);
} finally {
System.out.println("执行 finally 代码。");
}
}
}

以上程序的执行结果如下:

而 halt() 退出的方法,并不会执行 JVM 关闭钩子,示例代码如下:


class ExitDemo {

// 注册退出钩子程序
static {
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("执行 ShutdownHook 方法");
}));
}

public static void main(String[] args) {
try {
System.out.println("执行 try 代码。");
// 使用 Runtime.getRuntime().halt() 退出程序
Runtime.getRuntime().halt(0);
} finally {
System.out.println("执行 finally 代码。");
}
}
}

以上程序的执行结果如下:


小结


正常运行的情况下,finally 中的代码是一定会执行的,但是,如果遇到 System.exit() 方法或 Runtime.getRuntime().halt() 方法,或者是 try 中发生了死循环、死锁,遇到了掉电、JVM 崩溃等问题,finally 中的代码是不会执行的。而 exit() 方法会执行 JVM 关闭钩子方法或终结器,但 halt() 方法并不会执行钩子方法或终结器。


作者:Java中文社群
来源:juejin.cn/post/7313501001788604450
收起阅读 »

Maven下载安装与配置、Idea配置Maven(详细版)

Maven是Apache软件基金会的一个开源项目,是一款优秀的项目构建工具,它主要用于帮助开发者管理项目中jar以及jar之间的依赖关系,最终完成项目编译,测试,打包和发布等工作。前面我们已经简单介绍了Maven的概念、特点及使用,本篇文章就来给大家出一个详细...
继续阅读 »

Maven是Apache软件基金会的一个开源项目,是一款优秀的项目构建工具,它主要用于帮助开发者管理项目中jar以及jar之间的依赖关系,最终完成项目编译,测试,打包和发布等工作。

前面我们已经简单介绍了Maven的概念、特点及使用,本篇文章就来给大家出一个详细的安装和配置教程,还没有安装Maven的小伙伴要赶紧收藏起来哦!

首先给大家解释一下为什么学习Java非要学Maven不可。

为什么要学习Maven?

大家在读这篇文章之前大部分人都已经或多或少的经历过项目,说到项目,在原生代码无框架的时候,最痛苦的一件事情就是要在项目中导入各种各样使用的jar包,jar太多就会导致项目很难管理。需要考虑到jar包之间的版本适配的问题还有去哪找项目中使用的这么多的jar包,等等。这个时候,Maven就出现了,它轻松解决了这些问题。

说的直白一点,maven就相当于个三方安装包平台,可以通过一些特殊的安装方式,把一些三方的jar包,装到咱们自己的项目中。

Maven安装以及配置

Maven下载地址:https://maven.apache.org/

打开欢迎界面点击download下载

Description

Description

Description

在这里我们安装的是Maven的3.6.3版本,下载好之后是一个压缩包

Description

在电脑中找一个地方把压缩包解压,解压后

Description

配置环境变量

Description

在这儿一定要注意配置路径

Description

点击开始菜单 ->搜查框输入:cmd 回车 -> 出现Maven版本号说明安装成功

Description

Maven配置本地仓库

如何将下载的 jar 文件存储到我们指定的仓库中呢?需要在 maven 的服务器解压的文件中找到 conf 文件夹下的 settings.xml 文件进行修改,如下图所示:

Description

进入文件夹打开settings.xml文件

Description

因为默认的远程仓库地址,是国外的地址;在国内为了提高下载速度,可在如图所示位置配置阿里云仓库

Description

在idea工具中配置Maven

打开idea-----点击File-----点击New Projects Settings-----点击Setting for New Projects…

Description

这样,我们的Maven就安装配置完成了。


在这里给大家分享一下【云端源想】学习平台,无论你是初学者还是有经验的开发者,这里都有你需要的一切。包含课程视频、在线书籍、在线编程、一对一咨询等等,现在功能全部是免费的,

点击这里,立即开始你的学习之旅!


最后,附上我们一个配置文件,里面其实很大篇幅都是注释,为我们解释标签的用途,核心是咱们配置的远程中央仓库地址;在有的公司,是有自己的远程私有仓库地址的,配置方式跟中央仓库配置基本雷同,可能有一些账号密码而已。


<?xml version="1.0" encoding="UTF-8"?>

<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->


<!--
| This is the configuration file for Maven. It can be specified at two levels:
|
| 1. User Level. This settings.xml file provides configuration for a single user,
| and is normally provided in ${user.home}/.m2/settings.xml.
|
| NOTE: This location can be overridden with the CLI option:
|
| -s /path/to/user/settings.xml
|
| 2. Global Level. This settings.xml file provides configuration for all Maven
| users on a machine (assuming they're all using the same Maven
| installation). It's normally provided in
| ${maven.conf}/settings.xml.
|
| NOTE: This location can be overridden with the CLI option:
|
| -gs /path/to/global/settings.xml
|
| The sections in this sample file are intended to give you a running start at
| getting the most out of your Maven installation. Where appropriate, the default
| values (values used when the setting is not specified) are provided.
|
|-->

<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">

<!-- localRepository
| The path to the local repository maven will use to store artifacts.
|
| Default: ${user.home}/.m2/repository
<localRepository>/path/to/local/repo</localRepository>
-->


<!-- interactiveMode
| This will determine whether maven prompts you when it needs input. If set to false,
| maven will use a sensible default value, perhaps based on some other setting, for
| the parameter in question.
|
| Default: true
<interactiveMode>true</interactiveMode>
-->


<!-- offline
| Determines whether maven should attempt to connect to the network when executing a build.
| This will have an effect on artifact downloads, artifact deployment, and others.
|
| Default: false
<offline>false</offline>
-->


<!-- pluginGroups
| This is a list of additional group identifiers that will be searched when resolving plugins by their prefix, i.e.
| when invoking a command line like "mvn prefix:goal". Maven will automatically add the group identifiers
| "org.apache.maven.plugins" and "org.codehaus.mojo" if these are not already contained in the list.
|-->

<pluginGroups>
<!-- pluginGroup
| Specifies a further group identifier to use for plugin lookup.
<pluginGroup>com.your.plugins</pluginGroup>
-->

</pluginGroups>

<!-- proxies
| This is a list of proxies which can be used on this machine to connect to the network.
| Unless otherwise specified (by system property or command-line switch), the first proxy
| specification in this list marked as active will be used.
|-->

<proxies>
<!-- proxy
| Specification for one proxy, to be used in connecting to the network.
|
<proxy>
<id>optional</id>
<active>true</active>
<protocol>http</protocol>
<username>proxyuser</username>
<password>proxypass</password>
<host>proxy.host.net</host>
<port>80</port>
<nonProxyHosts>local.net|some.host.com</nonProxyHosts>
</proxy>
-->

</proxies>

<!-- servers
| This is a list of authentication profiles, keyed by the server-id used within the system.
| Authentication profiles can be used whenever maven must make a connection to a remote server.
|-->

<servers>

<!-- server
| Specifies the authentication information to use when connecting to a particular server, identified by
| a unique name within the system (referred to by the 'id' attribute below).
|
| NOTE: You should either specify username/password OR privateKey/passphrase, since these pairings are
| used together.
|
<server>
<id>deploymentRepo</id>
<username>repouser</username>
<password>repopwd</password>
</server>
-->


<!-- Another sample, using keys to authenticate.
<server>
<id>siteServer</id>
<privateKey>/path/to/private/key</privateKey>
<passphrase>optional; leave empty if not used.</passphrase>
</server>
-->

</servers>

<!-- mirrors
| This is a list of mirrors to be used in downloading artifacts from remote repositories.
|
| It works like this: a POM may declare a repository to use in resolving certain artifacts.
| However, this repository may have problems with heavy traffic at times, so people have mirrored
| it to several places.
|
| That repository definition will have a unique id, so we can create a mirror reference for that
| repository, to be used as an alternate download site. The mirror site will be the preferred
| server for that repository.
|-->

<mirrors>
<!-- mirror
| Specifies a repository mirror site to use instead of a given repository. The repository that
| this mirror serves has an ID that matches the mirrorOf element of this mirror. IDs are used
| for inheritance and direct lookup purposes, and must be unique across the set of mirrors.
|
<mirror>
<id>mirrorId</id>
<mirrorOf>repositoryId</mirrorOf>
<name>Human Readable Name for this Mirror.</name>
<url>http://my.repository.com/repo/path</url>
</mirror>
-->

<mirror>
<id>aliyun</id>
<name>aliyun maven</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<mirrorOf>central</mirrorOf>
</mirror>




</mirrors>

<!-- profiles
| This is a list of profiles which can be activated in a variety of ways, and which can modify
| the build process. Profiles provided in the settings.xml are intended to provide local machine-
| specific paths and repository locations which allow the build to work in the local environment.
|
| For example, if you have an integration testing plugin - like cactus - that needs to know where
| your Tomcat instance is installed, you can provide a variable here such that the variable is
| dereferenced during the build process to configure the cactus plugin.
|
| As noted above, profiles can be activated in a variety of ways. One way - the activeProfiles
| section of this document (settings.xml) - will be discussed later. Another way essentially
| relies on the detection of a system property, either matching a particular value for the property,
| or merely testing its existence. Profiles can also be activated by JDK version prefix, where a
| value of '1.4' might activate a profile when the build is executed on a JDK version of '1.4.2_07'.
| Finally, the list of active profiles can be specified directly from the command line.
|
| NOTE: For profiles defined in the settings.xml, you are restricted to specifying only artifact
| repositories, plugin repositories, and free-form properties to be used as configuration
| variables for plugins in the POM.
|
|-->

<profiles>
<!-- profile
| Specifies a set of introductions to the build process, to be activated using one or more of the
| mechanisms described above. For inheritance purposes, and to activate profiles via <activatedProfiles/>
| or the command line, profiles have to have an ID that is unique.
|
| An encouraged best practice for profile identification is to use a consistent naming convention
| for profiles, such as 'env-dev', 'env-test', 'env-production', 'user-jdcasey', 'user-brett', etc.
| This will make it more intuitive to understand what the set of introduced profiles is attempting
| to accomplish, particularly when you only have a list of profile id's for debug.
|
| This profile example uses the JDK version to trigger activation, and provides a JDK-specific repo.
<profile>
<id>jdk-1.4</id>

<activation>
<jdk>1.4</jdk>
</activation>

<repositories>
<repository>
<id>jdk14</id>
<name>Repository for JDK 1.4 builds</name>
<url>http://www.myhost.com/maven/jdk14</url>
<layout>default</layout>
<snapshotPolicy>always</snapshotPolicy>
</repository>
</repositories>
</profile>
-->


<!--
| Here is another profile, activated by the system property 'target-env' with a value of 'dev',
| which provides a specific path to the Tomcat instance. To use this, your plugin configuration
| might hypothetically look like:
|
| ...
| <plugin>
| <groupId>org.myco.myplugins</groupId>
| <artifactId>myplugin</artifactId>
|
| <configuration>
| <tomcatLocation>${tomcatPath}</tomcatLocation>
| </configuration>
| </plugin>
| ...
|
| NOTE: If you just wanted to inject this configuration whenever someone set 'target-env' to
| anything, you could just leave off the <value/> inside the activation-property.
|
<profile>
<id>env-dev</id>

<activation>
<property>
<name>target-env</name>
<value>dev</value>
</property>
</activation>

<properties>
<tomcatPath>/path/to/tomcat/instance</tomcatPath>
</properties>
</profile>
-->


</profiles>

<!-- activeProfiles
| List of profiles that are active for all builds.
|
<activeProfiles>
<activeProfile>alwaysActiveProfile</activeProfile>
<activeProfile>anotherAlwaysActiveProfile</activeProfile>
</activeProfiles>
-->

</settings>


收起阅读 »

Handler机制中同步屏障原理及结合实际问题分析

前言 本人是一个毕业两年的智能座舱车载应用工作者,在这里我将分享一个关于Handler同步屏障导致的bug,我和我的同事遇到了一个应用不响应Handler消息的bug,bug的现象为有Touch事件,但没有界面的view却没有任何响应。当时项目组拉了fwk和驱...
继续阅读 »

前言


本人是一个毕业两年的智能座舱车载应用工作者,在这里我将分享一个关于Handler同步屏障导致的bug,我和我的同事遇到了一个应用不响应Handler消息的bug,bug的现象为有Touch事件,但没有界面的view却没有任何响应。当时项目组拉了fwk和驱动的同事,从屏幕驱动到fwk的事件分发,甚至卡顿及内存泄漏都做了分析,唯独大家没有考虑到从Handler的消息机制切入。后面排除到是和Handler的同步屏障有关,最终才解决此bug,此篇文章将解释Handler的同步屏障机制及此bug的原因。


Handler同步屏障机制


Handler消息分为以下三种:


1.同步消息


2.异步消息


3.同步屏障(其实更像一个机制的开关)


其实在没有开启同步屏障的情况下,Handler对同步消息和异步消息的响应是没有太大区别的,都是通过Looper轮询MessageQueue中的消息然后传递给对应的Handler去处理,其中会按照Message的需要响应时间去决定其插入到链表中的位置,如果时间较早就会插在前面。(在此笔者不赘述过多关于Handler消息机制的内容,网上文章很多)但如果开启了同步屏障,Handler会优先处理异步消息,不响应同步消息,直到同步屏障关闭。


Handler同步屏障开启后的队列消息运作机制


我们知道在MessageQueue队列中,Message是按照延时时间的长短决定其在链表中的位置的。但是当我们打开了同步屏障之后,MessageQueue在消息出队的时候会优先出异步消息,绕开同步消息。具体如源码所示。



synchronized (this) {

    // Try to retrieve the next message.  Return if found.

    final long now = SystemClock.uptimeMillis();

    Message prevMsg = null;

    Message msg = mMessages;

    if (msg != null && msg.target == null) {

        // Stalled by a barrier.  Find the next asynchronous message in the queue.

        //可以看到当队列中有消息屏障的时候,会优先处理异步消息,绕开同步消息

        do {

            prevMsg = msg;

            msg = msg.next;

        } while (msg != null && !msg.isAsynchronous());

    }


如下是同步屏障开启以及开启后消息出队的一个流程图(其中两个异步消息是绘图表达有误,并非代表一起出列时候的状态)


image


遭遇bug原因


回到前言中提及的bug,其实由于在app在非主线程中去做了更新UI的操作,而这个操作没有做主线程校验,所以也没有抛出Only the original thread that created a view hierarchy can touch its views.在app中具体是调用了ViewRootImpl的如下方法。



@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)

//此方法没有加锁,是个线程不安全的方法

void scheduleTraversals() {

    if (!mTraversalScheduled) {

        mTraversalScheduled = true;

        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();

        mChoreographer.postCallback(

                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);

        notifyRendererOfFramePending();

        pokeDrawLockIfNeeded();

    }

}


如以上代码所示,这里是没有加同步锁的方法,app又是通过子线程去调用了此线程不安全的方法,导致插入了多个同步屏障,在移除的时候有没有将所有同步屏障消息移除,导致后来的同步消息全部不会出队,Handler也不会去处理这些消息,app的界面更新以及很多组件之间的通讯都是依赖Handler来处理,就导致整个app的现象是不论怎么触摸,都不会有界面更新,但通过系统日志又能看到触摸事件的日志。



void unscheduleTraversals() {

    if (mTraversalScheduled) {

        mTraversalScheduled = false;

        //移除消息屏障

        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

        mChoreographer.removeCallbacks(

                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);

    }

}

void doTraversal() {

    if (mTraversalScheduled) {

        mTraversalScheduled = false;

        //移除消息屏障

        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

        if (mProfile) {

            Debug.startMethodTracing("ViewAncestor");

        }

        performTraversals();

        if (mProfile) {

            Debug.stopMethodTracing();

            mProfile = false;

        }

    }

}


抽象出来的流程图如下图所示:


image


其实这里又回到了一个线程安全的问题,这个问题也是Andorid设计的时候要在UI线程(主线程)中更新UI的原因,保证线程的同步更新UI。最后通过排除app中的子线程更新UI代码段将此bug解决。


总结


1.Handler的同步屏障消息会让队列中的异步消息优先处理,同步消息被屏蔽。


2.结合笔者遇到的bug,大家其实要注意平时编写app代码时对UI的更新一定要放到主线程,保证线程的同步。


3.这段分析和经历不仅仅是博客中记录的原理,更多是拓宽了笔者解决问题的思维,我们总是说要去读源码,其实读懂只是帮助我们理解和避开写出bug,更多的我们应该学习里面的设计思维运用到实际开发中去。


作者:TW23
来源:juejin.cn/post/7313048188138356746
收起阅读 »

让你的PDF合成后不再失真

web
前言 现在的前端要处理越来越多的业务需求,如果你的业务中有涉及PDF的处理,你肯定会遇到这么一种情况, 在一个原始的pdf文件上合成进一张图片,或者一段文字。 之前的解决方案基本上是把图片扔给后端,让后端处理,处理好之后,前端再调用接口拿到。 如果非要前端来做...
继续阅读 »

前言


现在的前端要处理越来越多的业务需求,如果你的业务中有涉及PDF的处理,你肯定会遇到这么一种情况,
在一个原始的pdf文件上合成进一张图片,或者一段文字。


之前的解决方案基本上是把图片扔给后端,让后端处理,处理好之后,前端再调用接口拿到。


如果非要前端来做,也不是不可以。


一脸无奈的小


canvas


网上搜了一圈,主流的方案是,用canvas画布将pdf画出来,再将图片合成进canvans
这里也提供一下这个方案的代码


const renderPDF = async (pdfData, index, pages, newPdf, images = []) => {
await pdfData.getPage(index).then(async (pdfPage) => {
const viewport = pdfPage.getViewport({ scale: 3, rotation: 0 })
const canvas = document.createElement("canvas")
const context = canvas.getContext("2d")
canvas.width = 600 * 3
canvas.height = 800 * 3
// PDF渲染到canvas
const renderTask = pdfPage.render({
canvasContext: context,
viewport: viewport,
})

await renderTask.promise.then(() => {
if (index > 1) {
newPdf.addPage()
}
newPdf.addImage(canvas, "JPEG", 0, 0, 600, 800, undefined, "FAST")
images.forEach((item) => {
let width = item.width
let height = item.height
if (index == pages) {
item.src !== "" &&
newPdf.addImage(
item.src,
"PNG",
item.x,
item.y,
width,
height,
undefined,
"FAST"
)
}
})
})
})
}

但是!


这样会有一个很严重的问题,那就是pdf失真,显得很模糊,当然也有解决方案,那就是canvas的缩放比例增加,


image.png
但是,缩放比例的增加却带来了pdf文件大小的倍数及增加,前端渲染的压力很大,只有7张的pdf,已经渲染出了8M大小常常见到loading等待。


所以有没有更好的方法解决呢?


暴漫g


有的。


pdf-lib


那就是今天所推荐的库 pdf-lib
github地址
他在github上的star 有5.6k,算的上是成熟,顶级的开源项目


image.png


在任何JavaScript环境中创建和修改PDF文档。


好,今天就只介绍如何将图片合成进pdf的功能 ,抛砖引玉。


熊猫头抛砖头 .gif


其余的功能由您自己探索。


合成的思路是这样的:



  • 1、我们的原始pdf,转换成pdf-lib 可识别的格式

  • 2、同时将我们的图片合成进 pdf-lib里

  • 3、pdf-lib 导出合成后的pdf



由于他只是一个工具,没有办法展示pdf
最后找一个pdf预览工具显示在页面即可
我找的是 vue-pdf-embed



这样,使用pdf-lib 方案,就不再是canvas画布画出来的。
我们可以看到,生成后的pdf文件体积增加不大,


image.png


而且能够保留原始pdf的文字选择,不再是图片了


image.png


同样,页面的缩放不会出现模糊失真的情况(因为不是图片,还是保持文字的矢量)。


代码


以下是代码,请查收


感谢 给你磕头 GIF .gif


import { PDFDocument } from "pdf-lib"

const getNewPdf = async (pdfBase64, imagesList = []) => {
// 创建新的pdf
const pdfDoc = await PDFDocument.create()

let page = ""
// 传入的pdf进行格式转换
const usConstitutionPdf = await PDFDocument.load(pdfBase64)
// 获取转换后的每一页数据
const userPdf = usConstitutionPdf.getPages()
// 将每一个数据 导入到我们新建的pdf每一页上
for (let index = 0; index < userPdf.length; index++) {
page = pdfDoc.addPage()
const element = userPdf[index]
const firstPageOfConstitution = await pdfDoc.embedPage(element)
page.drawPage(firstPageOfConstitution)
// 如果有传入图片,则遍历信息,并将他合成到对应的页码上
const imageSel = imagesList.filter((i) => i.pageIndex === index)
if (imageSel.length > 0) {
for (let idx = 0; idx < imageSel.length; idx++) {
const el = imageSel[idx]
const pngImage = await pdfDoc.embedPng(el.src)
page.drawImage(pngImage, {
x: +el.x,
y: +el.y,
width: +el.width,
height: +el.height,
})
}
}
}
// 保存pdf
const pdfBytes = await pdfDoc.save()
// 将arrayButter 转换成 base64 格式
function ArrayBufferToBase64(buffer) {
//第一步,将ArrayBuffer转为二进制字符串
var binary = ""
var bytes = new Uint8Array(buffer)
for (var len = bytes.byteLength, i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i])
}
//将二进制字符串转为base64字符串
return window.btoa(binary)
}

// console.log("data:application/pdf;base64," + ArrayBufferToBase64(pdfBytes))
// 最后将合成的pdf返回
return "data:application/pdf;base64," + ArrayBufferToBase64(pdfBytes)
}
export default getNewPdf


这里的传参要注意,


可爱小男生拿喇叭注意啦_爱给网_aigei_com.gif



  • pdfBase64 是base64位的格式

  • imagesList 数组对象格式为
    [
    {
    src:'base64',
    x:'',
    yL'',
    width:'',
    height:'',
    pageIndex:''
    }
    ]



最后也附上vue文件中如何使用的代码


<template>
<div>
<el-button @click="pdfComposite">生成新的pdf</el-button>
<div class="pdf-content">
<vue-pdf-embed
:source="url"
/>
</div>
</div>

</template>

<script>
import VuePdfEmbed from "vue-pdf-embed/dist/vue2-pdf-embed"
import getNewPdf from "./utils"
import { pngText, pdfbase64 } from "../data"
export default {
name: "PdfPreview",
components: {
VuePdfEmbed,
},

data() {
return {
url: pdfbase64,// 原始的base64位 pdf
}
},
methods: {
pdfComposite() {
// getNewPdf 返回的是promise 对象
getNewPdf(this.url, pngText).then(res =>{
this.url = res
})
},
},
}
</script>

<style >
.pdf-content {
width: 400px;
min-height: 600px;
}
</style>


作者:前端代码王
来源:juejin.cn/post/7293175592163049506
收起阅读 »

Android 动画里的贝塞尔曲线

Android 动画里的贝塞尔曲线 对贝塞尔曲线的听闻,大概是源自 Photoshop 里的钢笔工具,用来画曲线的,但一直不明白这个曲线有什么用,接触学习到 Android 动画,又发现了贝塞尔曲线的身影,这玩意不是在绘图软件里画曲线的吗,怎么和动画扯上关系...
继续阅读 »

Android 动画里的贝塞尔曲线


贝塞尔曲线-钢笔.gif

对贝塞尔曲线的听闻,大概是源自 Photoshop 里的钢笔工具,用来画曲线的,但一直不明白这个曲线有什么用,接触学习到 Android 动画,又发现了贝塞尔曲线的身影,这玩意不是在绘图软件里画曲线的吗,怎么和动画扯上关系了,好吧,今天高低得来了解一下。


插值


首先我们得知道什么是插值,数学里面的插值(Interpolation),是一种通过已知的、离散的数据点,在范围内推求新数据点的过程或方法。


下面这个表给出了某个未知函数 f 的值,函数的具体表达式我们并不知道。


xf(x)
00
10.08415
20.9093
30.1411
4−0.7568
5−0.9589
6−0.2794

表中数据点在x-y平面上的绘图.jpg

现在让你估算出 x=2.5x=2.5 时,f(x)f(x) 的值。


简单嘛,已知 f(2)=0.9093f(2)=0.9093f(3)=0.1411f(3)=0.1411,连接两个已知点,f(2.5)f(2.5) 不就是线段中点嘛,(0.90930.1411)/2(0.9093-0.1411)/2,一下子就算出来了。这个通过已知的、离散的数据点,在范围内推求新数据点的过程其实就叫做 "插值"。


f(2.5)推算过程.gif

插值有多种方法,上面这种粗暴地将相邻已知点连接为线段,然后按比例取线段上某个点,来推求新数据点,属于"线性插值"。


虽然口头上表达这个过程不难,可如果这是一道数学试卷上面的解答题,请问阁下又该如何作答呢?


线性插值


咱们再来一道


Linear_interpolation.png

假设已知坐标 (x0,y0)\left ( {{x}_{0},\, {y}_{0}} \right )(x1,y1)\left ( {{x}_{1},\, {y}_{1}} \right ),求: [x0,x1]\left [ {{x}_{0},\, {x}_{1}} \right ] 区间内某一位置 xx 在所对应的 yy 值。


因为 (x0,y0)\left ( {{x}_{0},\, {y}_{0}} \right )(x,y)\left ( x,\, y \right ) 之间的斜率,与 (x0,y0)\left ( {{x}_{0},\, {y}_{0}} \right )(x1,y1)\left ( {{x}_{1},\, {y}_{1}} \right ) 之间的斜率相同,所以:


yy0xx0=y1y0x1x0{\frac {y-{y}_{0}} {x-{x}_{0}}=\frac {{y}_{1}-{y}_{0}} {{x}_{1}-{x}_{0}}\, }

其中 x0{x}_{0}y0{y}_{0}x1{x}_{1}y1{y}_{1}xx 都已知,那么:


y=y1y0x1x0(xx0)+y0=y0+(y1y0)xx0x1x0y=\frac {{y}_{1}-{y}_{0}} {{x}_{1}-{x}_{0}}·\left ( {x-{x}_{0}} \right )+{y}_{0}={y}_{0}+\left ( {{y}_{1}-{y}_{0}} \right )·\frac {x-{x}_{0}} {{x}_{1}-{x}_{0}}

线性插值公式.jpg

过程和上面的例子,线性插值推算未知函数 f(2.5)f(2.5) 的值其实是一样的,原来数学里线性插值的过程是这么表达的啊。


线性插值估算f(2.5).jpg

贝塞尔曲线


线性贝塞尔曲线


线性贝塞尔曲线是一条两点之间的直线,给定点 P0{P}_{0}P1{P}_{1},这条线由下式给出:


B(t)=P0+(P1P0)t,t[0,1]B\left ( {t} \right )={P}_{0}+\left ( {{P}_{1}-{P}_{0}} \right )·t,\, t\in \left [ {0,\, 1} \right ]

线性贝塞尔曲线演示动画.gif

等等,这不就是线性插值吗,和线性插值公式一样,而且线性插值的结果也在一条两点之间的直线上。


线性插值_直线.jpg

二次方贝塞尔曲线


既然两个点线性插值可以表示一条两点之间的直线,或者说是一条线性贝塞尔曲线,那...3个点线性插值的结果,几何表示会是什么样?


二次贝塞尔曲线的结构.jpg
二次贝塞尔曲线演示动画.gif

同时对 P0P1{P}_{0}{P}_{1}P1P2{P}_{1}{P}_{2} 进行插值:


P0P1{P}_{0}{P}_{1} 插值得到连续点 Q0{Q}_{0},描述线段 P0P1{P}_{0}{P}_{1},是一条线性贝塞尔曲线;


P1P2{P}_{1}{P}_{2} 插值得到连续点 Q1{Q}_{1},描述线段 P1P2{P}_{1}{P}_{2},是一条线性贝塞尔曲线;


P0P1{P}_{0}{P}_{1}P1P2{P}_{1}{P}_{2} 插值的同时,用同样的插值因子对得到的 Q0Q1{Q}_{0}{Q}_{1} 再插值,也就是对图中绿色线段进行插值,得到连续点 BB,追踪连续点 BB 的运动轨迹,得到曲线 P0P2{P}_{0}{P}_{2},也就是图中红色曲线,是一条二次贝塞尔曲线。


原来,两个及以上的点 线性插值函数 就是 贝塞尔曲线函数,我们可以简单地不断循坏迭代两点线性插值来得到最终结果。


三次方贝塞尔曲线 & 动画速度曲线


动画曲线.jpg
匀速和先加速后减速.gif

无论是网页设计里的 CSS 还是 Android 开发,它们里面的动画速度曲线其实是三次方贝塞尔曲线,由 4 个点不断两两插值得到。


三次贝塞尔曲线的结构.jpg
三次贝塞尔曲线演示动画.gif

我们自定义自己的动画曲线(三次方贝塞尔曲线)时,里面包含 4 个点的信息,其中第一个和最后一个点的坐标是 (0, 0) 和 (1, 1),我们还需要提供中间两个点的坐标。原来自定义动画曲线要填入 4 个数字的原因是这样啊,豁然开朗。


另外,你可以用网址 cubic-bezier 快速定制自己的动画曲线。


cubic-bezier.jpg

Ease / Easing


现实生活中极少存在线性匀速运动的场景,汽车启动、停下、自由落体运动等等都包含加速、减速,人的脑子里已经潜移默化地习惯了这种加速减速地运动。动画也是一种运动,设计动画的时候应该遵循现实世界的物理模型,让动画看起来更加自然,符合直觉。


md.gif


缓动 Ease,表示缓慢地移动(缓动),在 CSS 过渡动画里面,我们可以选择动画的缓动(Easing)类型,其中一些关键字有:



  • linear

  • ease-in

  • ease-out

  • ease-in-out


在经典动画中,开始阶段缓慢,然后加速的动作称为 "slow in";开始阶段运动较快,然后减速的动作称为 "slow out"。网络上面分别叫 "ease in" 和 "ease out",这里的 in/out 可以理解成一个动画里的一开始(start)或者最后(end)


slow in (ease in)

ease-in-details.jpg

比较适合出场动画,因为开始阶段比较慢,容易让人注意到哪个元素要开始移动,然后加速飞到视线之外。


好比你送朋友,看到朋友上了车,车子缓缓启动,然后加速驶去。


slow out (ease out)

ease-out-details.jpg

比较适合进场动画,因为结束阶段比较缓慢,能让人清楚看到是哪个元素飞了进来。


就像你站在公交车站,看到一辆公交车远远飞速驶来,减速停下。


ease in out

那 ease in out 又是啥呢?ease 是缓和的意思,而 in/out 前面说过可以看作是一次动画里面的开始或结束阶段。ease in out 自然就代表:在一次动画里的开始阶段和结束阶段,动作都是缓和的,仅中间阶段是加速的,能够将用户注意力集中在过渡的末端。这也是 Material Design 的标准缓动,由于现实世界中的物体不会立即开始或停止移动,这种缓动类型可以让动画更有质感。


ease-in-out-details.jpg

这种动画曲线比较适合转换动画,也就是说一个元素运动过程中,没有涉及入场与离场,它始终位于屏幕内,只是由一种形态变换为另一种形态。


fab_anim.gif

Jetpack Compose 里面,表示动画速度曲线的接口是 Easing,Compose 提供了 4 中常见的速度曲线:


/**
* Elements that begin and end at rest use this standard easing. They speed up quickly
* and slow down gradually, in order to emphasize the end of the transition.
*
* Standard easing puts subtle attention at the end of an animation, by giving more
* time to deceleration than acceleration. It is the most common form of easing.
*
* This is equivalent to the Android `FastOutSlowInInterpolator`
*/

val FastOutSlowInEasing: Easing = CubicBezierEasing(0.4f, 0.0f, 0.2f, 1.0f)

/**
* Incoming elements are animated using deceleration easing, which starts a transition
* at peak velocity (the fastest point of an element’s movement) and ends at rest.
*
* This is equivalent to the Android `LinearOutSlowInInterpolator`
*/

val LinearOutSlowInEasing: Easing = CubicBezierEasing(0.0f, 0.0f, 0.2f, 1.0f)

/**
* Elements exiting a screen use acceleration easing, where they start at rest and
* end at peak velocity.
*
* This is equivalent to the Android `FastOutLinearInInterpolator`
*/

val FastOutLinearInEasing: Easing = CubicBezierEasing(0.4f, 0.0f, 1.0f, 1.0f)

/**
* It returns fraction unmodified. This is useful as a default value for
* cases where a [Easing] is required but no actual easing is desired.
*/

val LinearEasing: Easing = Easing { fraction -> fraction }

LinearEasing 是匀速运动,最好理解了,另外 3 个和前面提到的 CSS 里的 ease-in-out、ease-out、ease-in 其实都是对应的



  • ease-in-out => FastOutSlowIn

  • ease-out => LinearOutSlowIn

  • ease-in => FastOutLinearIn


Compose Easing.jpg


...真是想不明白 Compose 官方这个命名是从哪个角度理解的


作者:bqliang
来源:juejin.cn/post/7311957976968708131
收起阅读 »

乖和听话从来不值得称赞!

直到昨天去爬山被冷得像SB一样,我才知道这次冬天真的来了,前天还是大太阳,我在公司楼下转了一圈,只穿短袖,一夜之间,仿佛从非洲大陆到了南极大陆。 只有半个月2023就过去了,年底也逐渐忙碌了起来,回想起一年的时光,你又进步了多少,又收获了多少,还是原地踏步,我...
继续阅读 »

直到昨天去爬山被冷得像SB一样,我才知道这次冬天真的来了,前天还是大太阳,我在公司楼下转了一圈,只穿短袖,一夜之间,仿佛从非洲大陆到了南极大陆。


只有半个月2023就过去了,年底也逐渐忙碌了起来,回想起一年的时光,你又进步了多少,又收获了多少,还是原地踏步,我想,不管是否进步,退步,抑或是原地踏步,都没有关系,只要按照自己的节奏来行事,所谓的自律,进步这些都只是伪命题!


今天我们来聊一聊乖这个话题!


图片



我记得前段时间,我和一个网友语音聊天,他找我给他解决问题,我问了他一句话:为啥你不去自己钻研下呢,这些问题其实只是你稍微去好好学一下,都能解决的。


他对我说:我不想花时间去弄这个,我以后也不会从事软件这一行,我毕业后回去,好好复习,加上家里有一定的关系,找个体制内的工作不难,我也没有啥大追求,好好听父母的话,也能轻松,快乐过日子。


了解下来,他家境还是挺不错的,是独生子,父母都是体制内,母亲退休工资有1W+,父亲在单位也是挺不错的,给他已经全款买了房子(天津)。


从和他的聊天中,我看出他是一个典型的乖乖男,很听父母的话。


那么我相信,他一定能够很快乐,并且没啥压力过好以后的生活,他一定比中国95%以上的人过得开心,过得轻松,因为他没有什么压力,但是我觉得最主要的是,他已经预见自己未来的人生,并且能够顺利地走上这条路,所以他基本上不会去经历内耗,经历欲求不满!



另外一些现实中的朋友。


特别是从我们西南地区的农村出来的孩子,自然就没有多大的选择,没有占有地域上的优势,祖上三代都是靠土地活着,更没有资源背景。


这时候去听父母的话,基本上是自己废自己。


我们一起长大的一个朋友,也算是经历九死一生,现在混得相当不错,多年前我们在一起的时候,他说:已经那么穷了,还听父母的干嘛,如果父母说的有用,为啥还那么穷!


很扎心。


小地方出来的人,大多数人为啥自卑,不敢发表自己的观点,不敢反驳,总是唯唯诺诺的,即使有好的机会和平台,自己都不敢把握,说难听一点,为啥总是夹着尾巴做人,就是因为被老一辈灌输了很多”不健康“的思维。


就像我和以前的一个小学聊天,我说你为啥从一个这么好的大学出来,学得也不错,为啥不先去好的大公司里面试试水,而是选择回到这个落后的地方做一个初中毕业生都能做的事,一个月才几千。


他说干不过人家,加上自己不善于与人沟通,去这些大企业不好混,还是回来考编制稳一点。


我很尊重他的选择,但是我不认同他的选择,他的选择里也映射出了很多问题。


我之前看到一个作者发了一篇文章,他说从小被灌输了很多穷的思想,导致他一直以来都很自卑,做什么都觉得不对,都觉得对不起父母,有好的机会也觉得自己不配,所以穷了很多年。


后面他就不再去想那么多,不再顾虑父母那么多,甚至好多年都不回家,等他一个月能能稳定赚几万的时候。


他才慢慢克服了自卑,才有慢慢变得自信。


他说:如果我一直听他们的话,一直活在那种自卑,自负的环境中,那么我将一辈子无出头之日。


钱是穷人的胆,是穷人逃出自卑,自负的最佳良药,这句话一点没错。



从上面的两点,我我们可以看出不同的人生。


但是第二点的人占了社会的大部分,其实大多数人的家境都是很普通的,根本没啥资源,没啥背景,根本给你安排不了什么好的道路。


所以这时候,你的乖,你的听话,毫无意义。


它只能将你永远困住。


如果父母是有思想,有见解,并且有赚钱的人,那么我们一定要听他们的,因为这会让你少走弯路。


如果父母还在底层,还在贫穷和无知中,那么我们做得”残忍”一点更好,别太乖,因为没用!


当你兜里摸不出钱了,不能做自己想做的事,为生活发愁的时候,所谓的乖和听话会一巴掌一巴掌拍在你的脸上。


今天的分享就到这里,感谢你的观看,我们下期见。


对了,天这么冷,记得穿厚一点,别感冒了!


作者:追梦人刘牌
来源:juejin.cn/post/7313132521092169728
收起阅读 »

阿里妈妈刀隶体使用

web
最近在做的一个前端小项目,需要一种比较炫酷的字体,在网上找到了iconfont中的刀隶体,感觉还不错,本文记录了如何在前端项目中使用这种字体的步骤。 1. 找到刀隶体生成的网站 访问下面这个网站就可以了: 阿里妈妈刀隶体字体 http://www.iconfo...
继续阅读 »

最近在做的一个前端小项目,需要一种比较炫酷的字体,在网上找到了iconfont中的刀隶体,感觉还不错,本文记录了如何在前端项目中使用这种字体的步骤。


1. 找到刀隶体生成的网站


访问下面这个网站就可以了:


阿里妈妈刀隶体字体


http://www.iconfont.cn/fonts/detai…


2. 生成自己想要的字并下载到本地


找到文本输入框
image.png


然后输入自己想要展示的字体:我是一只小青蛙,最爱说笑话
image.png


最后点击下载子集按钮


image.png


下载好的压缩包:


image.png


将压缩包中的内容复制到剪切板:


image.png


3. 项目中引入


在项目中创建管理字体的目录


mkdir -p src/assets/font

然后到font目录下粘贴复制的字体文件夹


最后在项目的根样式文件中(一般来说是src/index.css)引入新字体:


@font-face {
font-family: "DaoLiTi";
font-weight: 400;
src: url("./assets/font/jHsMvOZ7UDEO/XBddIp4y0BmM.woff2") format("woff2"),
url("./assets/font/jHsMvOZ7UDEO/XBddIp4y0BmM.woff") format("woff");
font-display: swap;
}

body {
font-family: 'DaoLiTi', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
}

4. 新字体使用及效果


<span style={{fontFamily: 'DaoLiTi'}}>我是一只小青蛙,最爱说笑话</span>


5. 注意点


DaoLiTi这个名字是可以自定义的,但是在样式文件中的无论什么地方使用的时候都不能少了引号。


此外就是除了“我是一只小青蛙,最爱说笑话”,其他字是没有刀隶体效果的!


作者:慕仲卿
来源:juejin.cn/post/7305359585107738661
收起阅读 »

已经好久没有尽全力做某件事情了

好像,我已经好久没有尽全力做某件事情了... 以至于,我已经有点遗忘了,尽全力做某件事情的感觉了... 你还记得,最近一次(或者是现在)你尽全力做某件事情的感受吗? 如果要描述这种感觉的话,我想会包含以下几点吧。 第一,忘我。也就是我们经常说的心流状态。我们在...
继续阅读 »

好像,我已经好久没有尽全力做某件事情了...


以至于,我已经有点遗忘了,尽全力做某件事情的感觉了...


你还记得,最近一次(或者是现在)你尽全力做某件事情的感受吗?


如果要描述这种感觉的话,我想会包含以下几点吧。


第一,忘我。也就是我们经常说的心流状态。我们在尽全力做某件事情的时候,会很容易进入心流状态。只要是做这件事情,然后再加上一些条件反射诱因,就可以很快进入心流状态。就像巴甫洛夫实验那样,摇铃铛,流口水。


第二,印象深刻。这种感觉是印象深刻的,不管这件事情最终是成功还是失败,是硕果累累,还是无疾而终,它都能给我们留下深刻的印象。在很多年之后,虽然我们会遗忘很多的细节,但起码,我们还能回忆起来这件事,我做过,我全情投入过。


第三,不留遗憾。有一种后悔,是在自己做了某种决策之后导致失败。有一种比这个程度更深的后悔,是自己没有全情投入地去做某件事情。失败,只是懊悔,没做,才是遗憾


插图1.png


过去几年,我的工作好无聊,很难受,非常迷茫。感觉我是在原地踏步,虚耗光阴。


今年慢慢走出来了,但还是没有达到最好的状态。


我觉得,自己被很多无形的枷锁禁锢住了。虽然不是难以呼吸般的艰难,却也不能纵情放肆高呼般的畅快。


我觉得,自己像是一个控火师。现实、责任、理智让我严格控制内心的火,不能让它燃尽自己;而梦想、遗憾、本我又让我尽力呵护它,不能让它熄灭。


今天说多了,还是回归理性。继续蛰伏吧,努力成长,提升自己,才能有资本抓住未来的机会。


加油,奥利给!


----------------【END】----------------


欢迎关注公众号【潜龙在渊灬】(点此扫码关注),收获程序员职场相关经验、提升工作效率和职场效能、结交更多人脉。


作者:潜龙在渊灬
来源:juejin.cn/post/7311500203433377842
收起阅读 »

你真的懂 Base64 吗?短链服务常用的 Base62 呢?

web
黄山的冬天,中国 (© Hung Chung Chih/Shutterstock) Base64 前端的日常开发中可能会接触到 Base64 ,比如页面上的小图片,在为了节省网络资源的情况下,通常会将图片转为 Base64 直接嵌入到 html 或者 css ...
继续阅读 »

黄山的冬天,中国 (© Hung Chung Chih/Shutterstock)

Base64


前端的日常开发中可能会接触到 Base64 ,比如页面上的小图片,在为了节省网络资源的情况下,通常会将图片转为 Base64 直接嵌入到 html 或者 css 里。这里,我们先来看看 Base64 是什么,以及 Base64 编码做了什么。


什么是 Base64


Base64 是一种基于64个可打印字符来表示二进制数据的表示方法。一般来说,64个字符包括 A-Z,a-z,0-9 以及 +/ 两个字符(via)。换句话说, Base64 可以将二进制数据转换为这 64 个字符来表示,数据来源可以是图片,也可以是任意的字符串。


Base64 做了些什么


上面提到了 Base64 做的其实就是将二进制数据按照对应规则进行转化,转化的流程其实也很简单



  1. 得到一份二进制数据

  2. 将二进制数据 6位 一组进行划分,并进行适当的补位

  3. 按照 Base64 索引表,将每组数据转换为 Base64 索引表对应的字符,补位位置用 =


下面我们来尝试一下将 aa 这个字符串进行一下 Base64 编码:


第一步:通过 ASCII 表将 aa 转换为对应的二进制表示

可知 a 在 ASCII 表对应的二进制表示为 0110 0001,则 aa 对应为 0110 0001 0110 0001

注:



  1. ascii 参考

  2. 中文等其他字符参考其他表(UTF-8)进行转换即可


第二步:数据分组和补位

按 6 位一组划分后 011000 010110 0001,我们发现数据还少两位,所以我们需要按规则对数据进行补位,补一字节(8位),二进制数据变为 0110 0001 0110 0001 0000 0000,划分为 011000 010110 000100 000000


第三步:查 Base64 索引表 进行转换

参考一张常用的索引表
image.png
可知 aa = 011000 010110 000100 000000 对应为 011000(Y) 010110(W) 000100(E) 000000(补位=) 即 YWE=,我们就得到了 aa 的 Base64 编码为 YWE= ,是不是挺简单。


Base62


说完了 Base64,我们再来聊聊 Base62。在通过 url 传递数据的场景下,通过 Base64 进行编码的数据会带来问题(Base64 中的 / 等可能会带来路径的解析异常),所以在 Base62 里,去掉了 +/= 字符。
说到这里,大家可能觉得就讲完了,Base62 就是丢掉了几个不安全的字符而已,其余转换方法和 Base64 一样,我起初也是这么认为的。


不一样的 Base62 结果


当我尝试对 aa 进行 Base62 编码时,按推算好像也不太对? = 补位已经被去掉了,怎么来做实现呢?
在我找了几个 online 转换进行测试后,发现 aa 对应的 Base62 编码为 6U5 看着跟 Base64 毫无关系对吧,实际上也是的。


揭开面纱看看


查了几份资料以及现有的仓库实现后,我发现 Base62 编码的流程是这样的:



  1. 获得一份二进制数据

  2. 二进制数据 转 10进制

  3. 10进制 转 62进制(按索引表)


我们再来试试将 aa 转 Base62:


第一步:转二进制

aa 对应 0110 0001 0110 0001


第二步:转10进制

0110000101100001 对应十进制为 24929


第三步:转62进制(参考索引表)

image.png
24929 = 6622+3062+5

按表可知,6=6 30=U 5=5,即 6U5


注:



  1. 索引表可以自行更换,并不一定是上图顺序

  2. 现有的仓库实现里,部分只实现了 10进制 转 62进制(base62/base62.js),有的实现了更完整的转换 (tuupola/base62


分析一下原因


说实话我查到的资料不多,但是根据 en.wikipedia.org/wiki/Talk:B… 猜测,文里提到 Base64 后的数据会膨胀到 133% 。Base62 还存在对数据进行压缩的改进,所以采用了这样与 Base64 差别有点大的方式来设计。


总结一下


文章简单的谈了谈 Base64 是什么,怎么实现以及 Base62 的实现,并分析了一下 Base62 设计的初衷,整体来说还是挺简单,希望对你有所帮助 :)


作者:破竹
来源:juejin.cn/post/7311596852264878115
收起阅读 »

Android 图片描边效果

前言 先看下我们阔爱滴海绵宝宝,其原图是一张PNG图片,我们给宝宝加上描边效果,今天我们使用的是图片蒙版技术。 说到蒙版可能很多人想起PS抠图软件,Android上也一样,同一个大树上可能会长出两种果实,但果实的根基是一样的。 什么是蒙版:所谓蒙版是只保留了...
继续阅读 »

前言


先看下我们阔爱滴海绵宝宝,其原图是一张PNG图片,我们给宝宝加上描边效果,今天我们使用的是图片蒙版技术。


fire_78.gif


说到蒙版可能很多人想起PS抠图软件,Android上也一样,同一个大树上可能会长出两种果实,但果实的根基是一样的。


什么是蒙版:所谓蒙版是只保留了alpha通道的一种二维正交投影,简单的说就是你躺在地上,太阳光直射下来,背后的那片就是你的蒙版。因此,它既不存在三维特征,也不存在色彩特征,只有alpha特征。那只有alpha通道的图片是什么颜色,这块没有具体了解过,但是理论上取决于默认填充色,在Android上最终是白色的,其他平台暂时还没了解。


提取蒙版


Android上提取蒙版比想象的容易,按照以往的思路,我们是要进行图片扫描这里,其实就是把所有颜色的red、green、blue都排除掉,只保留alpha,相当于缩小了通道数,排除采样和缩小图片,当然这个工作量是很大的,尤其是超高清图片。


企业微信20231210-120604@2x.png


Android 上提取蒙版,只需要把原图绘制到alpha通道的Bitmap上


bms = decodeBitmap(R.mipmap.mm_07);
bmm = Bitmap.createBitmap(bms.getWidth(), bms.getHeight(), Bitmap.Config.ALPHA_8);
Canvas canvas = new Canvas(bmm);
canvas.drawBitmap(bms, 0, 0, null);

蒙版绘制


蒙版绘制和其他Bitmap绘制是有差异的,ARGB_8888和RGB_565等色彩格式的图片,其本身是具备颜色的,但是蒙版图片不一样,他没有颜色,所以你绘制的时候,bitmap的颜色是你画笔Paint的填充色,突然想到可以做一个人体扫描的动画效果或者人体热力图。


canvas.drawBitmap(bmm, x, y, paint);

扩大蒙版(影子)


要让蒙版比比原图大,理论上是需要等比例放大蒙版在平移,还有一种方式是进行偏移绘制,我们这里使用偏移绘制。当然,这里取一定360,保证尽可能每个方向都有偏移,这是看到的外国人的算法。至于step>0 但是也要控制粒度,太小可能绘制次数太多,太大可能有些边缘做不到偏移。


for (int i = 0; i < 360; i += step) {
float x = width * (float) Math.cos(Math.toRadians(i));
float y = width * (float) Math.sin(Math.toRadians(i));
canvas.drawBitmap(bmm, x, y, paint);
}

闪烁效果


我们价格颜色闪烁的效果,其实很简单,也不是本篇重要的部份,其实就是在色彩中间插入透明色,然后定时闪烁。


int index = -1;
int max = 15;
int[] colors = new int[max];
final int[] highlightColors = {0xfff00000,0,0xffff9922,0,0xff00ff00,0};

public void shake() {
index = 0;
for (int i = 0; i < max; i+=2) {
colors[i] = highlightColors[i % highlightColors.length];
}
postInvalidate();
}

总结


本篇到这里就结束了,希望利用蒙版+偏移做出更多东西。


全部代码


public class ViewHighLight extends View {
final Bitmap bms; //source 原图
final Bitmap bmm; //mask 蒙版
final Paint paint;
final int width = 4;
final int step = 15; // 1...45
int index = -1;
int max = 15;
int[] colors = new int[max];
final int[] highlightColors = {0xfff00000,0,0xffff9922,0,0xff00ff00,0};
public ViewHighLight(Context context) {
super(context);
bms = decodeBitmap(R.mipmap.mm_07);
bmm = Bitmap.createBitmap(bms.getWidth(), bms.getHeight(), Bitmap.Config.ALPHA_8);
Canvas canvas = new Canvas(bmm);
canvas.drawBitmap(bms, 0, 0, null);
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
}

private Bitmap decodeBitmap(int resId) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inMutable = true;
return BitmapFactory.decodeResource(getResources(), resId, options);
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// draw blur shadow

for (int i = 0; i < 360; i += step) {
float x = width * (float) Math.cos(Math.toRadians(i));
float y = width * (float) Math.sin(Math.toRadians(i));
canvas.drawBitmap(bmm, x, y, paint);
}
canvas.drawBitmap(bms, 0, 0, null);

if(index == -1){
return;
}
index++;
if(index > max +1){
return;
}
if(index >= max){
paint.setColor(Color.TRANSPARENT);
}else{
paint.setColor(colors[index]);
}
postInvalidateDelayed(200);
}


public void shake() {
index = 0;
for (int i = 0; i < max; i+=2) {
colors[i] = highlightColors[i % highlightColors.length];
}
postInvalidate();
}
}

作者:时光少年
来源:juejin.cn/post/7310786575213920306
收起阅读 »

关于解构赋值的一些意想不到的坑

web
今天群里有个人问了一个问题,问我们为什么报错,代码如下 var arr = [1,2,3,4,5,6,7,8,9,10] for(let i = arr.length; i > 0; i--) { let index = Math.floor(M...
继续阅读 »

今天群里有个人问了一个问题,问我们为什么报错,代码如下


var arr = [1,2,3,4,5,6,7,8,9,10]
for(let i = arr.length; i > 0; i--) {
let index = Math.floor(Math.random() * i)
// console.log(i, index);
[arr[i-1], arr[index]] = [arr[index], arr[i-1]]
// ReferenceError: Cannot access 'index' before initialization
}
console.log(arr)

我乍一看,这能报错?不应该啊,怎么能呢,于是我特意复制下来跑了一下,嘿,还真是


关于ReferenceError: Cannot access 'xxx' before initialization的报错,往往和暂时性死区有关,但我看了看顺序,是先定义的index啊,没有错。


抱着求知的心态,上网查了一些文章,都没有提到这种问题,于是只能去看ecma规范,但对不起,我英语太差了,我就连在哪都没找到。


后来我想起自己曾经遇到过类似的问题,只不过是在解构对象的时候遇到的


大概是这样的操作


let a = xxx

({
a: this.options.a,
b: this.options.b,
......
} = /*一个对象*/ ?? {})

当时也报了错,我就想起来了



js中是允许语句不使用;结尾的,许多小伙伴可能养成了这个习惯,虽然不写分号有时候确实很爽很轻松,也是一些企业的规范,但是等到流泪的时候可就知道惨了。



只需要将上述结构赋值的代码的前面一个语句加上分号,就可以解决这个问题


相当于把一个语句拆开了


什么?你问我怎么就成同一个语句了?


我没记错的话,js在执行的时候是会忽略换行符的吧,或者说这个换行符没那么重要,所以我们平时看到的很多库打包出来的min.js文件都是只有一行的然后通过分号分割语句。


如果把上述代码换行内容忽视掉,就变成了这个样子,只放了部分代码


    let index = Math.floor(Math.random() * i)[arr[i-1], arr[index]] = [arr[index], arr[i-1]]

这不报错谁报错啊,根据等号从右到左的运算顺序,不就是访问了暂时性死区嘛


所以加上分号,问题就引刃而解了。


想当年因为先学c++和java的缘故,总是养成写分号的习惯,在切图仔里面似乎成为了一个异类,现在知道了吧,养成写分号的好习惯啊,呜呜呜呜


最后附上修改后的代码


var arr = [1,2,3,4,5,6,7,8,9,10]
for(let i = arr.length; i > 0; i--) {
let index = Math.floor(Math.random() * i);
// console.log(i, index);
[arr[i-1], arr[index]] = [arr[index], arr[i-1]]
// ReferenceError: Cannot access 'index' before initialization
}
console.log(arr)

更好笑的是,这哥们明显是想通过console测试一下的,结果他发现console就不报错,不console就报错,这是真的折磨哈哈哈哈哈哈


养成语句加分号的好习惯!!
从你我做起!


作者:笑心
来源:juejin.cn/post/7311681326712995903
收起阅读 »

【内存泄漏】图解 Android 内存泄漏

内存泄漏简介 关于内存泄露的定义,想必大家已经烂熟于心了,简单一点的说,就是程序占用了不再使用的内存空间。 那在 Android 里边,怎样的内存空间是程序不再使用的内存空间呢? 这就不得不提到内存泄漏相关的检测了,拿 LeakCanary 的检测举例,销毁的...
继续阅读 »

内存泄漏简介


关于内存泄露的定义,想必大家已经烂熟于心了,简单一点的说,就是程序占用了不再使用的内存空间


那在 Android 里边,怎样的内存空间是程序不再使用的内存空间呢?


这就不得不提到内存泄漏相关的检测了,拿 LeakCanary 的检测举例,销毁的 ActivityFragment 、fragment ViewViewModel 如果没有被回收,当前应用被判定为发生了内存泄漏。LeakCanary 会生成引用链相关的日志信息。


有了引用链的日志信息,我们就可以开开心心的解决内存泄漏问题了。但是除了查看引用链还有更好的解决方式吗?答案是有的,那就是通过画图来解决,会更加的直观形象~


一个简单的例子


如下是一个 Handler 发生内存泄露的例子:


class MainActivity : ComponentActivity() {

private val handler = LeakHandler(Looper.getMainLooper())

override fun onCreate(savedInstanceState: Bundle?) {
// other code

// 发送了一个 100s 的延迟消息
handler.sendMessageDelayed(Message.obtain(), 100_000L)
}

private fun doLog() {
Log.d(TAG, "doLog")
}

private inner class LeakHandler(looper: Looper): Handler(looper) {
override fun handleMessage(msg: Message) {
doLog()
}
}
}

因为 LeakHandler 是一个内部类,持有了外部类 MainActivity 的引用。


其在如下场景会发生内存泄漏:onCreate 执行之后发送了一个 100s 的延迟消息,在 100s 以内旋转屏幕,MainActivity 进行了重建,上一次的 MainActivity 还被 LeakHandler 持有无法释放,导致内存泄露的产生。


引用链图示


如下是执行完 onCreate() 方法之后的引用链图示:


memory_leak_1.png



简单说明一下引用链 0 的位置,这里为了简化,直接使用 GCRoot 代替了,实际上存在这样的引用关系:GCRoot → ActivityThread → Handler → MessageQueue → Message。简单了解一下即可,不太清楚的话也不会影响接下来的问题解决。
同时,为了简单明了,文中还简化了一些相关但是不重要的引用链关系,比如 HandlerMessageQueue 的引用。



100s 以内旋转屏幕之后,引用链图示变成这样了:


memory_leak_2.png


之前的 Activity 被释放,引用链 4 被切断了。我们可以很清晰的看到,由于 LeakHandlerMainActivity 的强引用(引用链2),LeakHandler 间接被 GCRoot 节点强引用,导致 MainActivity 没办法释放。



MainActivity 指的是旋转屏幕之前的 Activity,不是旋转屏幕之后新建的



那么很显然,接下来我们就需要对引用链 0、1 或 2 进行一些操作了,这样才可以让 MainActivity 得到释放。


解决方案


方案一:


onDestroy() 的时候调用 removeCallbacksAndMessages(null) ,该方法会进行两步操作:移除该 Handler 发送的所有消息,并将 Message 回收到 MessagePool


override fun onDestroy() {
super.onDestroy()
handler.removeCallbacksAndMessages(null)
}

此时,在图示上的表现,就是移除了引用链 0 和 1。如此 MainActivity 就可以正常回收了。


memory_leak_3.png


方案二:


使用弱引用 + 静态内部类的方式,我们同样也可以解决这个内存泄漏问题,想必大家已经非常熟悉了。



这里再简单说明一下弱引用 + 静态内部类的原理:
弱引用的回收机制:在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只被弱引用引用的对象,不管当前内存空间足够与否,都会回收它的内存。
静态内部类:静态内部类不会持有外部类的引用,也就不会有 LeakHandler 直接引用 MainActivity 的情况出现



代码实现上,只需要传入 MainActivity 的弱引用给 NoLeakHandler 即可:


private val handler = NoLeakHandler(WeakReference(this), Looper.getMainLooper())

private class NoLeakHandler(
private val activity: WeakReference<MainActivity>,
looper: Looper
): Handler(looper) {
override fun handleMessage(msg: Message) {
activity.get()?.doLog()
}
}

下图中,2.1 表示的是 NoLeakHandlerWeakReference 的强引用,NoLeakHandler 通过 WeakReference 间接引用到了 MainActivity。我们可以很清楚的看到,在旋转屏幕之后,MainActivity 此时只被一个弱引用引用了(引用链 2.2,使用虚线表示),是可以正常被回收的。


memory_leak_4.png


另一个简单的例子


再来一个静态类持有 Activity 的例子,如下是关键代码:


object LeakStaticObject {
val activityCollection: MutableList<Activity> = mutableListOf()
}

class MainActivity : ComponentActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
// other code

activityCollection.add(this)
}
}

正常运行的情况下,存在如下的引用关系:


memory_leak_5.png


在旋转屏幕之后,会发生内存泄漏,因为之前的 MainActivity 还被 GCRoot 节点(LeakStaticObject)引用着。那要怎么解决呢?相信大家已经比较清楚了,要么切断引用链 0,要么将引用链 0 替换成一个弱引用。由于比较简单,这里就不再单独画图说明了。


总结


本文介绍了一种使用画图来解决常见内存泄露的方法,会比直接查看引用链更加清晰具体。同时,其相比于一些归纳常见内存泄漏的方法,会更加的通用,很大程度上摆脱了对内存泄漏场景的强行记忆。


通过画图,找到引用路径之后,在引用链的某个节点上进行操作,切断强引用或者将强引用替换成弱引用,以此来解决问题。


总的来说,对于常见的内存泄漏场景,我们都可以通过画图来解决,本文为了介绍简便,使用了比较简单常见的例子,实际上,遇到复杂的内存泄漏,也可以通过画图的方式来解决。当然,熟练之后,省略画图的操作,也是可以的。


REFERENCE


wikipedia 内存泄漏


Excalidraw — Collaborative whiteboarding made easy


How LeakCanary works - LeakCanary


理解Java的强引用、软引用、弱引用和虚引用 - 掘金


作者:很好奇
来源:juejin.cn/post/7313242069099872306
收起阅读 »

技术并不一定比其他高级

这里的技术可以是计算机或者别的什么技术。当然首先指的是开发技术。 很长一段时间里,就我个人有一种天然的技术高于其他的感觉,虽未明示,但骨子里有一种谦虚的傲慢。认为开发高于产品、设计、测试等。 不知道其他人是否有过这种想法。或者我觉得那种典型的技术人的思维,要么...
继续阅读 »

这里的技术可以是计算机或者别的什么技术。当然首先指的是开发技术。


很长一段时间里,就我个人有一种天然的技术高于其他的感觉,虽未明示,但骨子里有一种谦虚的傲慢。认为开发高于产品、设计、测试等。


不知道其他人是否有过这种想法。或者我觉得那种典型的技术人的思维,要么思维不够开放,要么就是一种谦虚的傲慢。


这种傲慢最可怕的是因为漠视掉其它的价值,导致技术人的格局不够。格局不够会看不到更大世界的运行规律,导致无法做出更加正确的决策。有一副著名的对联



能攻心则反侧自消,自古知兵非好战


不审势即宽严皆误,后来治蜀要深思



我觉得这是对格局不够后果最直接准确的描述:宽严皆误。说下我是怎么想到技术并不一定比其他高级的


前几天群里同组的同学@我让我改一篇文章,我才知道公司开始举办一年一度的一年一词活动,开始面向全体征稿。第一年的时候我参与了,但没有选中。第二年没有参与。


我看了下同组同学那篇文章,觉得不怎么滴啊,也是这激起我的求胜心,决定自己写一篇,今年再参加一次。于是那天下午我就写完了初稿。初稿的题目是造轮子,第一句



一般来说说造轮子的都是程序员,因为开发从某个意义上来讲就是在重复造轮子,亦如太阳底下没有新鲜事,也亦如任何历史都是当代史。



为了能够被选上,我认真又做了几次修改,重读了几次。我有点福至心灵的发现我在开发上犯了一个错误,就是我似乎一直认为技术才是最重要的,不管是有意无意的,这是事实。但是开发从某个意义上来讲就是在重复造轮子,正如太阳底下没有新鲜事,也亦如任何历史都是当代史,技术和其他一样,也是重复的单元。


要想尽快搞清楚技术,只要找到其中代表性的重复单元就可以了。而实际也早就有人总结了这些单元,比如功能单元的代表各种ui组件库,业务单元的代表往往是对功能单元的再加工。好比功能单元是原型机,而业务单元是定制化。


前几天也看到一篇文章的题目《不过是享受了互联网的十年红利期而已》。遂想到行业高速发展时期,技术实现是第一位的;但行业进入饱和期,产品、运营应该才是创造利润的关键。正如计算机底层技术开发人员,过了计算机技术爆发的年代,反倒不如业务开发赚的多。


这一切的一切不过是特定时期的表现。技术并不一定比其他高级,现在就是技术不再处于第一优先级的时刻。


(本文完)


作者:通往自由之路
来源:juejin.cn/post/7304598711991795750
收起阅读 »

程序员IT行业,外行眼里高收入人群,内行人里的卷王

程序员 一词,在我眼里其实是贬义词。因为我的其他不是这行的亲朋友好友,你和他们说,你是一名程序员· 他们 第一刻板影响就是,秃头,肥胖,宅男,油腻,不修边幅 反正给人一种不干净,不好形象,,,,不知道什么时候开始网络上也去渲染这些,把程序员和这些联想在一起了。...
继续阅读 »

程序员 一词,在我眼里其实是贬义词。因为我的其他不是这行的亲朋友好友,你和他们说,你是一名程序员·


他们 第一刻板影响就是,秃头,肥胖,宅男,油腻,不修边幅 反正给人一种不干净,不好形象,,,,不知道什么时候开始网络上也去渲染这些,把程序员和这些联想在一起了。


回到正题,我们来聊聊,我们光鲜靓丽背后高工资。


是的作为一名程序员,在许多人的眼中,IT行业收入可能相对较高。这是不可否认的。但是,在这个职业领域里,我们所面对的困难和挑战也是非常的多。


持续的学习能力



程序员需要持续地学习,不断地掌握新技能。



随着技术的不断发展,我们需要不断地学习新的编程语言、开发框架、工具以及平台等等,这是非常耗费精力和时间的。每次技术更新都需要我们拿出宝贵的时间,去研究、学习和应用。


尤其在公司用项目中,用到新技术需要你在一定时间熟悉并使用时候,那个时候你自己只有硬着头皮,一边工作一边学习,如果你敢和老板说不会,那,,,我是没那个胆量


高强度抗压力



ICU,猝死,996说的就是我们



我们需要经常探索和应对极具挑战性的编程问题。解决一个困难的问题可能需要我们数小时,甚至数天的时间,这需要我们付出大量的勤奋和耐心。有时候,我们会出现程序崩溃或运行缓慢的情况,当然,这种情况下我们也需要更多的时间去诊断和解决问题,


还要保持高效率工作,同时保证项目的质量。有时候,团队需要在紧张的时间内完成特别复杂的任务,这就需要我们花费更多的时间和精力来完成工作。


枯燥乏味生活


由于高强度工作,和加班,我们的业余生活可能不够丰富,社交能力也会不足


高额经济支出


程序员IT软件行业,一般都是在一线城市工作,或者新一线,二线城市,所以面临的经济支持也会比较大,


最难的就是房租支持,生活开销。


一线城市工作,钱也只能在一线城市花,有时候也是真的存不了什么钱,明明自己什么也没有额外支持干些什么,可是每月剩下的存款也没有多少


短暂职业生涯


“背负黑匣子”:程序员的工作虽然看似高薪,但在实际工作中,我们承担了处理复杂技术问题的重任。


“独自快乐?”:程序员在工作中经常需要在长时间内独立思考和解决问题,缺乏团队合作可能会导致孤独和焦虑。


“冰山一角的技能”:程序员需要不断学习和更新技能,以适应快速变化的技术需求,这需要不断的自我修炼和付出时间。


“猝不及防的技术变革”:程序员在处理技术问题时需要时刻保持警惕,技术日新月异,无法预测的技术变革可能会对工作带来极大的压力。


“难以理解的需求”:客户和管理层的需求往往复杂而难以理解,程序员需要积极与他们沟通,但这也会给他们带来额外的挑战和压力。


“不请自来的漏洞”:安全漏洞是程序员必须不断面对和解决的问题,这种不确认的风险可能会让程序员时刻处于焦虑状态。


“高度聚焦的任务”:程序员在处理技术问题时需要集中精力和关注度,这通常需要长时间的高度聚焦,导致他们缺乏生活平衡。


“时刻警觉”:程序员在工作中必须时刻提醒自己,保持警觉和冷静,以便快速识别和解决问题。


“枯燥重复的任务”:与那些高度专业的技术任务相比,程序员还需要完成一些枯燥重复的工作,这让他们感到无聊和疲惫。


“被误解的天才”:程序员通常被视为是天才,但是他们经常被误解、被怀疑,这可能给他们的职业带来一定的负担。


程序员IT,也是吃年轻饭的,不是说你年龄越大,就代表你资历越深。 职业焦虑30岁年龄危机 越来越年轻化


要么转行,要么深造,


Yo,这是程序员的故事

高薪却伴随着堆积如山的代码

代码缺陷层出不穷,拯救业务成了千里马

深夜里加班的钟声不停响起

与bug展开了无尽的搏斗,时间与生命的角逐

接口返回的200,可前端却丝毫未见变化

HTTP媒体类型不支持,世界一团糟

Java Spring框架调试繁琐,无尽加班真让人绝望

可哪怕压力再大,我们还是核心开发者的倡导者

应用业务需要承载,才能取得胜利的喝彩

程序员的苦工是世界最稀缺的产业

我们不妥协,用技术创意为行业注入新生命

我们坚持高质量代码的规范

纵使压力山大,我们仍能跨过这些阻碍

这是程序员的故事。

大家有什么想法和故事吗,在工作中是否也遇到了和我一样的问题?


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

如何实现一个可视化数据转换的小工具

web
前端开发过程中,经常有两个不同的组件、或者两个不同的业务模块需要对接,但是双方提供的数据结构又不一致的场景。这个时候就需要一个转换器来实现两个模块无缝对接,通过这个转换器的处理达到数据结构一致的目的。 基本需求梳理 场景中两个相同或者不同数据结构对象,对象是...
继续阅读 »

前端开发过程中,经常有两个不同的组件、或者两个不同的业务模块需要对接,但是双方提供的数据结构又不一致的场景。这个时候就需要一个转换器来实现两个模块无缝对接,通过这个转换器的处理达到数据结构一致的目的。


基本需求梳理



  • 场景中两个相同或者不同数据结构对象,对象是任意数据结构的,对接场景下数据结构是固定的

  • 针对当前场景下转换器处理是相同的

  • 尽量图形化操作就可以换成配置以及预览效果


设计基本思路


实现思路



  • 两个不同的数据结构可以通过字段映射的方式来取值和设置值,实现数据的对接

  • 取值路径和设置值的路径规则最好一条一条保存下来,作为转换器的规则描述

  • 取值路径和设置值路径可以通过lodash的get和set实现

  • 可视化操作可以通过json树的渲染及操作来实现


设计实现思路草图


我根据自己的想法大致设计一下交互方式如下:


数据转换器 (3).png


实现步骤


json树操作


我找了一下josn树操作的组件,发现react-json-view挺不错的;可以实现值的复制、选择;但是选择是针对叶子节点的,所以这里我使用复制功能来实现,无论是叶子节点还是非叶子节点都可以复制到,(enableClipboard)复制的时候获取当前的path信息即可。另外path多了一层根路径默认是'root',如果不想要操作保存的时候去掉即可。


如下图所示,鼠标悬浮点击这个icon图标来选中key,取其路径值保存起来
image.png


// 复制的操作
const enableClipboard = (copy) => {
const { namespace } = copy;
if (namespace?.length === 1) { // 复制的根元素
setSourceKeyPath([])
} else {
const curNamespace = namespace.splice(1, namespace.length - 1);
setSourceKeyPath(curNamespace)
}
}

路径保存


路径映射保存在数组里。


转换器处理


转换器只需要根据规则数组map处理一下每条规则进行对原数据和目标数据进行取值设置值操作就可以了


// dataSource 是规则数组
// targetData 是目标数据
// sourceData 是源数据
dataSource.map((item) => {
set(targetData, item.targetKeyPath, get(sourceData, item.sourceKeyPath))
return item
});

效果预览


image.png
另外数据随机生成我使用的是@faker-js/faker


部署到网站


已经部署到我的个人网站timesky.top/data-conver…


作者:TimeSky
来源:juejin.cn/post/7313242069099954226
收起阅读 »

Antd Upload上传后还想要拖拽?行~开干!玩的就是真实

web
原创 陈夏杨 / 叫叫技术团队 基于 Antd Upload 实现拖拽(兼容低版本) 背景 哎呀!我想要的是边上传边调整位置可以拖拽的那种效果!哪种?就那种。(这句话是不是似曾相识,没错,到这里作为开发还没领悟那就要面壁思过了~哈哈。)话不多说,目前 Ant...
继续阅读 »

原创 陈夏杨 / 叫叫技术团队



基于 Antd Upload 实现拖拽(兼容低版本)


背景


哎呀!我想要的是边上传边调整位置可以拖拽的那种效果!哪种?就那种。(这句话是不是似曾相识,没错,到这里作为开发还没领悟那就要面壁思过了~哈哈。)话不多说,目前 Antd 的 Upload 组件并未支持拖拽排序功能,社区也没有发现可以借鉴的 demo,于是我们调研后采用 react-dnd 和 react-sortable-hoc 实现“就那种”效果。


技术分析


其实这个需求之前我们已经有一些基于 react-dnd 技术的沉淀,但是都是基于 html 的 dom 元素进行 ref 绑定操作,并没有搭配 Upload 组件。如下 demo


拖拽2.gif
以上是基于 react-dnd 实现的场景拖拽,直接上核心代码


const [, dragPreview] = useDrag({
collect: (monitor: DragSourceMonitor) => ({
isDragging: monitor.isDragging()
}),
// item 中包含 index 属性,则在 drop 组件 hover 和 drop 是可以根据第一个参数获取到 index 值
item: { type: 'page', index }
});
const [, drop] = useDrop({
accept: 'page',
hover(item: { type: string; index: number }, monitor: any) {
if (!ref.current) {
return;
}
const dragIndex = item.index;
const hoverIndex = index;

// 拖拽元素下标与鼠标悬浮元素下标一致时,不进行操作
if (dragIndex === hoverIndex) {
return;
}
// 确定屏幕上矩形范围
const hoverBoundingRect = ref.current!.getBoundingClientRect();
// 获取中点垂直坐标
const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
// 确定鼠标位置
const clientOffset = monitor.getClientOffset();
// 获取距顶部距离
const hoverClientY = (clientOffset as any).y - hoverBoundingRect.top;
/**
* 只在鼠标越过一半物品高度时执行移动。
* 当向下拖动时,仅当光标低于50%时才移动。
* 当向上拖动时,仅当光标在50%以上时才移动。
* 可以防止鼠标位于元素一半高度时元素抖动的状况
*/

// 向下拖动
if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
return;
}
// 向上拖动
if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
return;
}

// 执行 move 回调函数
moveCard(dragIndex, hoverIndex);

/**
* 如果拖拽的组件为 Box,则 dragIndex 为 undefined,此时不对 item 的 index 进行修改
* 如果拖拽的组件为 Card,则将 hoverIndex 赋值给 item 的 index 属性
*/

if (item.index !== undefined) {
item.index = hoverIndex;
}
}
});
dragPreview(drop(ref));

因为 Upload 组件上传文件是通过自身 fileList api 底层消化处理的,所以处理起来比较麻烦,还好 Antd 4.16.0 版本Upload 提供的 itemRender 解决了这个问题。但是对于低于这个版本的后续也有解决方案。


注意:如果 Antd 用的是最新的版本 5.x.x,其实官网也提供了 集成 dnd-kit 来实现对上传列表拖拽排序


技术选型


市面上可以实现拖拽排序的库有很多,比如 SortableJS、react-dnd、react-beautiful-dnd、react-sortable-hoc 等。
我列了一个表格:


优点缺点
SortableJS足够轻量级,而且功能齐全React 中使用起来并不是太方便,而且它的配置项写起来实在不太符合 React 的思维
react-dnd库小,贴合 react 拖拽场景多行拖拽不理想,react-beautiful-dnd 库比较大赖于HTML5 拖放 API,这有一些严重的限制
react-beautiful-dnd动画效果和细节非常完美同上
react-sortable-hoc多行拖拽优势很明显(相比其他库大多依赖于HTML5拖放API ,这有一些严重的限制。例如,如果你需要支持触摸设备,如果你需要锁定拖动到一个轴上,或者想在节点排序时设置动画,事情就会变得很棘手。React-sortablehoc 旨在提供一组简单的 higher-order 组件来填补这些空白。如果您正在寻找一种 dead-simple ,mobile-friendly 的方式来向列表中添加可排序功能,那么您就在正确的位置了。)列表过长,需要滑动的列表中拖拽时,滑动后位置不匹配会发生偏移

实践中还会有一些踩坑:



  • reat-dnd 在项目中快速拖拽时一直报错,"Invariant Violation: Expected targetIds to be registered. "在他的 issue 中也有很多人反应这个问题,虽然有修复过但是并没有完全修复,在 overStack 中也并没有找到好的解决方案。

  • 这里以我们实践结论,从 react-dnd、react-sortable-hoc 两个库进行讲解


react-dnd


概念

react dnd 是一组 react 高阶组件,使用的时候只需要使用对应的 API 将目标组件进行包裹,即可实现拖动或接受拖动元素的功能。
在拖动的过程中,不需要开发者自己判断拖动状态,只需要在传入的配置对象中各个状态属性中做对应处理即可,因为react-dnd 使用了 redux 管理自身内部的状态。
值得注意的是,react-dnd 并不会改变页面的视图,它只会改变页面元素的数据流向,因此它所提供的拖拽效果并不是很炫酷的,我们可能需要写额外的视图层来完成想要的效果,但是这种拖拽管理方式非常的通用,可以在任何场景下使用,非常适合用来定制。


安装

npm i react-dnd

核心 API

介绍实现拖拽和数据流转的核心 API ,这里以 hook 为例。


DndProvider

使用 react-dnd 需要最外层元素加 DndProvider ,DndProvider 的本质是一个由 React.createContext 创建一个上下文的容器(组件),用于控制拖拽的行为,数据的共享,类似于 react-redux 的 Provider。


import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';

<DndProvider backend={HTML5Backend}>组建模块</DndProvider>;

Backend

react dnd 将 DOM 事件相关的代码独立出来,将拖拽事件转换为 react dnd 内部的 redux action。由于拖拽发生在 H5 的时候是 ondrag,发生在移动设备的时候是由 touch 模拟,react dnd 将这部分单独抽出来,方便后续的扩展,这部分就叫做 backend。它是 dnd 在 DOM 层的实现。



  • react-dnd-html5-backend : 用于控制 html5 事件的 backend

  • react-dnd-touch-backend : 用于控制移动端 touch 事件的 backend

  • react-dnd-test-backend : 用户可以参考自定义 backend


useDrag

让 DOM 实现拖拽能力的构子


import React from 'react';
import { useDrag } from 'react-dnd';

export default function Player() {
// 第一个返回值是一个对象,主要放一些拖拽物的状态。后面会介绍,先不管
// 第二个返回值:顾名思义就是一个Ref,只要将它注入到DOM中,该DOM就会变成一个可拖拽的DOM
const [_, dragRef] = useDrag(
{
type: 'Player', // 给拖拽物命名,后面用于分辨该拖拽物是谁,支持string和symbol
item: { id: 1 } // 拖拽物所携带的数据,让后面一些事件可以拿到数据,已达到交互的目的
},
[]
);
// 注入Ref,现在这个DOM就可以拖拽了
return <div ref={dragRef} />;
}

返回三个参数

第一个返回值是一个对象 表示关联在拖拽过程中的变量,需要在传入 useDrag 的规范方法的 collect 属性中进行映射绑定,比如:isDraging, canDrag 等

第二个返回值 代表拖拽元素的 ref

第三个返回值 代表拖拽元素拖拽后实际操作到的 dom

传入两个参数



  • 第一个参数,是一个对象,是用于描述了drag 的配置信息,常用属性


type指定元素的类型,只有类型相同的元素才能进行 drop 操作
item元素在拖拽过程中,描述该对象的数据,如果指定的是一个方法,则方法会在开始拖拽时调用,并且需要返回一个对象来描述该元素。
end(item, monitor)拖拽结束的回调函数,item 表示拖拽物的描述数据,monitor 表示一个 DragTargetMonitor 实例
isDragging(monitor)判断元素是否在拖拽过程中,可以覆盖Monitor对象中的 isDragging方法,monitor 表示一个 DragTargetMonitor 实例
canDrag(monitor)判断是否可以拖拽的方法,需要返回一个 bool 值,可以覆盖 Monitor 对象中的 canDrag 方法,与 isDragging 同理,monitor 表示一个 DragTargetMonitor 实例
collect它应该返回一个描述状态的普通对象,然后返回以注入到组件中。它接收两个参数,一个 DragTargetMonitor 实例和拖拽元素描述信息item


  • 第二个参数是一个数组,表示对方法更新的约束,只有当数组中的参数发生改变,才会重新生成方法,基于react 的 useMemo 实现


useDrop

实现拖拽物放置的钩子


import { useDrop } from 'react-dnd';

export const Dustbin = () => {
const [_, dropRef] = useDrop({
accept: ['Player'], // 指明该区域允许接收的拖放物。可以是单个,也可以是数组
// 里面的值就是useDrag所定义的type

// 当拖拽物在这个拖放区域放下时触发,这个item就是拖拽物的item(拖拽物携带的数据)
drop: (item) => {}
});

// 将ref注入进去,这个DOM就可以处理拖拽物了
return <div ref={dropRef}></div>;
};

返回两个参数

第一个返回值是一个对象 表示关联在拖拽过程中的变量,需要在传入 useDrag 的规范方法的 collect 属性中进行映射绑定。

第二个返回值 代表拖拽元素的 ref
传入一个参数
用于描述drop的配置信息,常用属性


accept指定接收元素的类型,只有类型相同的元素才能进行 drop 操作
drop(item, monitor)有拖拽物放置到元素上触发的回调方法,item 表示拖拽物的描述数据,monitor 表示 DropTargetMonitor 实例,该方法返回一个对象,对象的数据可以由拖拽物的 monitor.getDropResult 方法获得
hover(item, monitor)当拖住物在上方 hover 时触发,item 表示拖拽物的描述数据,monitor表示 DropTargetMonitor 实例,返回一个 bool 值
canDrop(item, monitor)判断拖拽物是否可以放置,item 表示拖拽物的描述数据,monitor 表示 DropTargetMonitor 实例,返回一个 bool 值

API 数据流转

image.png


react-sortable-hoc


概念

react-sortable-hoc 是一个基于 React 的拖拽排序组件,它可以让你轻松地实现拖拽排序功能。它提供了一系列的 API,可以让你自定义拖拽排序的行为。它支持拖拽排序的单个列表和多个列表,以及拖拽排序的可视化。


安装

npm install react-sortable-hoc

引入

import { SortableContainer, SortableElement, arrayMove, SortableHandle } from 'react-sortable-hoc';

核心 API


  • sortableContainer 是所有可排序元素的容器

  • sortableElement 是每个可渲染元素的容器

  • sortableHandle 是定义拖拽手柄的容器

  • arrayMove 主要用于将移动后的数据排列好后返回


SortableContainer HOC

PropertyTypeDefaultDescription
axisStringy项目可以水平、垂直或网格排序。可能值:x、y 或 xy
lockAxisString如果您愿意,可以在排序时将移动锁定在轴上。这不是 HTML5 拖放所能做到的。可能值:x 或 y。
helperClassString您可以提供一个要添加到 sortable helper 的类,以向其添加一些样式
transitionDurationNumber300元素移动位置时转换的持续时间。{ 39d 要禁用 @661 }
keyboardSortingTransitionDurationNumbertransitionDuration在键盘排序期间移动辅助对象时转换的持续时间。如果要禁用键盘排序助手的转换,请将其设置为 0。如果未定义,则默认为 transitionDuration 设置的值
keyCodesArray{lift: [32],drop: [32],cancel: [27],up: [38, 37],down: [40, 39]}一个包含每个 keyboard-accessible 操作的键码数组的对象。
pressDelayNumber0如果您希望元素只在按下一段时间后才可排序,请更改此属性。mobile 的一个合理的默认值是 200。不能与 distance 属性一起使用。
pressThresholdNumber5忽略冲压事件之前要容忍的移动像素数。
distanceNumber0如果您希望元素只在被拖动一定数量的像素之后才变得可排序。不能与 pressDelay 属性一起使用。
shouldCancelStartFunctionFunction此函数在排序开始前调用,可用于在排序开始前以编程方式取消排序。默认情况下,如果事件目标是 input、textarea、select 或 option,它将取消排序。
updateBeforeSortStartFunction在排序开始之前调用此函数。它可以返回一个 promise,允许您在排序开始之前运行异步更新(比如 setState )。function ({ node, index, collection, isKeySorting }, event )
onSortStartFunction开始排序时调用的回调。function({ node, index, collection, isKeySorting }, event ) |
onSortMoveFunction当光标移动时在排序期间调用的回调。function ( event )|
onSortOverFunction在向上移动时调用的回调。function ({ index, oldIndex, newIndex, collection, isKeySorting }, e )
onSortEndFunction排序结束时调用的回调。function ({ oldIndex, newIndex, collection, isKeySorting }, e )
useDragHandleBooleanfalse如果您使用的是SortableHandleHOC,请将其设置为true
useWindowAsScrollContainerBooleanfalse如果需要,可以将window设置为滚动容器
hideSortableGhostBooleantrue是否 auto-hide 重影元素。默认情况下,为了方便起见,React Sortable List 将自动隐藏当前正在排序的元素。如果要应用自己的样式,请将此设置为 false。
lockToContainerEdgesBooleanfalse您可以将可排序元素的移动锁定到其父元素 SortableContainer
lockOffsetOffsetValue*|[OffsetValue*,OffsetValue*]"50%"当 lockToContainerEdges 设置为 true 时,这将控制可排序辅助对象与其父对象 SortableContainer 的上/下边缘之间的偏移距离。百分比值相对于当前正在排序的项的高度。如果您希望指定不同的行为来锁定容器的顶部和底部,您还可以传入 array(例如:["0%", "100%"])。
getContainerFunction返回可滚动容器元素的可选函数。此属性默认为 SortableContainer 元素本身或(如果 useWindowAsScrollContainer 为真)窗口。使用此函数指定一个自定义容器对象(例如,这对于与某些第三方组件(如 FlexTable )集成非常有用)。这个函数被传递给一个参数(即 wrappedInstanceReact 元素),它应该返回一个 DOM 元素。
getHelperDimensionsFunctionFunction可选的function ({ node, index, collection }),它应该返回 SortableHelper 的计算维度。有关详细信息,请参见默认实现 |
helperContainerHTMLElement | 函数document.body默认情况下,克隆的可排序帮助程序将附加到文档正文。使用此属性可指定要附加到可排序克隆的其他容器。接受 HTMLElement 或返回 HTMLElement 的函数,该函数将在排序开始之前调用
disableAutoscrollBooleanfalse拖动时禁用自动滚动

如何使用

直接上demo


import React from 'react';
import { arrayMove, SortableContainer, SortableElement } from 'react-sortable-hoc';

// 需要拖动的元素的容器
const SortableItem = SortableElement((value) => <div>{value}</div>);
// 整个元素排序的容器
const SortableList = SortableContainer((items) => {
return items.map((value, index) => {
return <SortableItem key={`item-${index}`} index={index} value={value} />;
});
});

// 拖动排序组件
class SortableComponnet extends React.Component {
state = {
items: ['1', '2', '3']
};
onSortEnd = ({ oldIndex, newIndex }) => {
this.setState(({ items }) => {
arrayMove(items, oldIndex, newIndex);
});
};
render() {
return (
<div>
<SortableList
distance={5}
axis={'xy'}
items={this.state.items}
helperClass={style.helperClass}
onSortEnd={this.onSortEnd}
/>

</div>

);
}
}

export default SortableComponnet;

在上面的示例中,我们使用 SortableContainer 组件容纳了一组可拖拽排序的元素,使用 SortableElement 组件包裹了每个元素,并且实现了 onSortEnd 回调函数,以便在拖拽排序完成后更新状态。


效果展示

拖拽3.gif


踩坑

image.png



解决:这种报错的解决方法都是 SortableElement 和 SortableContainer 返回组件时外面都要单独在包一个 html 容器标签, 例子是包了个 <div>



结果导向


Antd 版本 4.16.0及以上


使用 react-dnd 搭配 Upload 上传组件 itemRender api实现

注:这里主要基于 react-dnd 实现,Antd 5.x.x 官网有基于 dnd-kit 来实现对上传列表拖拽排序。


const Box: React.FC<BoxProps> = ({ children, index, className, onClick, moveCard }) => {
const ref = useRef<HTMLDivElement>(null);

const [, dragPreview] = useDrag({
collect: (monitor: DragSourceMonitor) => ({
isDragging: monitor.isDragging()
}),
// item 中包含 index 属性,则在 drop 组件 hover 和 drop 是可以根据第一个参数获取到 index 值
item: { type: 'page', index }
});
const [, drop] = useDrop({
accept: 'page',
hover(item: { type: string; index: number }) {
if (!ref.current) {
return;
}
const dragIndex = item.index;
const hoverIndex = index;
// 自定义逻辑处理

// 执行 move 回调函数
moveCard(dragIndex, hoverIndex);
}
});
dragPreview(drop(ref));

return (
<div ref={ref} className={className} onClick={onClick}>
{children}
</div>

);
};

使用

使用 Antd Upload itemRender


image.png


Antd 版本低于 4.16.0


使用 react-sortable-hoc 库实现


  • SortableItem


const SortableItem = SortableElement((params: SortableItemParams) => (
<div>
<UploadList
locale={{ previewFile: '预览图片', removeFile: '删除图片' }}
showDownloadIcon={false}
showRemoveIcon={params?.props?.disabled}
listType={params?.props?.listType}
onPreview={params.onPreview}
onRemove={params.onRemove}
items={[params.item]}
/>

</div>

));


  • SortableList


const SortableList = SortableContainer((params: SortableListParams) => {
return (
<div className='sortableList'>
{params.items.map((item, index) => (
<SortableItem
key={`${item.uid}`}
index={index}
item={item}
props={params.props}
onPreview={params.onPreview}
onRemove={params.onRemove}
/>

))}
{/* 这里是上传组件,设置最大限制后超出隐藏 */}
<Upload {...params.props} showUploadList={false} onChange={params.onChange}>
{params.props.children}
</Upload>
</div>

);
});


  • DragHoc


const DragHoc: React.FC<Props> = memo(
({ onChange: onFileChange, axis, onPreview, onRemove, ...props }) => {
const fileList = props.fileList || [];
const onSortEnd = ({ oldIndex, newIndex }: SortEnd) => {
onFileChange({ fileList: arrayMove(fileList, oldIndex, newIndex) });
};

const onChange = ({ fileList: newFileList }: UploadChangeParam) => {
onFileChange({ fileList: newFileList });
};

return (
<>
<SortableList
// 当移动 1 之后再触发排序事件默认是0会导致无法触发图片的预览和删除事件
distance={1}
items={fileList}
onSortEnd={onSortEnd}
axis={axis || 'xy'}
helperClass='SortableHelper'
props={props}
onChange={onChange}
onRemove={onRemove}
onPreview={onPreview}
/>

</>

);
}
);

使用方式

可直接替换掉 Upload 组件,props 不变。
如果项目中有已经封装好的 Upload 上传组件,尽量不改变原有逻辑代码前提下,更希望以插件的形式按需加载?方案:



可以剔除掉 SortableList 中 SortableContainer 包裹的 Upload 组件(这一步经过实践是可行的,说 Upload UploadList 都要被 SortableContainer 包裹,否走会重复上传和拖拽失败?目前我是没遇到,重复上传是因为 Upload 组件 showUploadList 拖拽场景下必须是 false )



使用案例:( isDrag 表示需要拖拽场景,继而加载)



{isDrag && (
<DragHoc
accept={accept}
axis={axis}
showUploadList={{ showRemoveIcon }}
fileList={fileList}
onChange={(e) =>
{
if (onChange) {
onChange(e);
}
}}
onPreview={preview}
onRemove={remove}
listType={listType}
/>

)}

注:当然也可以用 react-dnd 来实现,只是多行拖拽流畅性较差。感兴趣也可以试试


踩坑

图片按钮点击无效

在 Antd 的 Upload 组件中,图片墙上会有「预览」、「删除」等按钮,但是在 react-sortable-hoc 的逻辑中,只要我点击了图片,就会触发图片的拖拽函数,无法触发图片上的各种按钮,所以需要在 SortableList 上重新设置一下 distance 属性,设置成 1 即可。

官网:



If you'd like elements to only become sortable after being dragged a certain number of pixels. Cannot be used in conjunction with the pressDelay prop.

(如果您希望元素仅在拖动一定数量的像素后才可排序。不能与 pressDelay 道具一起使用。默认为 0)



上传图片一直 uploading

image.png

原因:当图片列表发生变化,整个 sortable 容器被删除并重新渲染,导致请求失效。


解决方案:



需要将 SortableItem,SortableList 写在 React.FC 外面,每次组件内部 state 发生变化,不会重新执行 SortableContainer 和 SortableElement 方法,就可以让可排序容器里面的元素自动只更新需要改变的 DOM 元素,而不会整个删除并重新渲染了。



图片的 disabled 状态失效

原因:SortableContainer 包裹的组件对 Upload 图片和上传进行了拆分处理,所以需要单独去控制预览和删除按钮


const SortableItem = SortableElement((params: SortableItemParams) => (
<div>
<UploadList
locale={{ previewFile: '预览图片', removeFile: '删除图片' }}
showDownloadIcon={false}
showRemoveIcon={params?.props?.disabled} //这里需要单独控制
listType={params?.props?.listType}
onPreview={params.onPreview}
onRemove={params.onRemove}
items={[params.item]}
/>

</div>

));

效果展示

拖拽1.gif


参考文献



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

分裂的国产自研手机系统,究竟苦了谁

2023 年可谓是国产自研手机操作系统百花齐放的一年,在华为官宣 HarmonyOS NEXT 开发者预览版本,不在兼容 Android 之后,小米、vivo 分别官宣了自己的操作系统。 10 月 26 日,雷布斯宣布小米澎湃 OS,耗时 7 年将 MIUI、...
继续阅读 »

2023 年可谓是国产自研手机操作系统百花齐放的一年,在华为官宣 HarmonyOS NEXT 开发者预览版本,不在兼容 Android 之后,小米、vivo 分别官宣了自己的操作系统。


10 月 26 日,雷布斯宣布小米澎湃 OS,耗时 7 年将 MIUI、Vela、Mina、车机 OS 四个系统进行了合并,想打造一个万物互联的操作系统。其中 Vela 我们之前也接触过,一句话,坑是在太多了,替小米系统工程师的头发感到惋惜。hahaha


11 月 1 日,vivo 副总裁宣布自主研发的蓝河操作系统 BlueOS,并且 vivo 自研蓝河操作系统不兼容安卓应用,未来也不会兼容。在加上 OPPO 的潘塔纳尔系统,国内的主流手机厂商都拥有了自己的操作系统。


为什么国产手机厂商都想打造自己的操作系统?



  • 想脱离 Android 的控制,华为和中兴的前车之鉴,给手机厂商们敲响了警钟,都在卧薪尝胆,开发自己的操作系统

  • 顺应时代,想抓住万物互联的红利,打造自己的物联网生态,就必须要有自己的万物互联操作系统


现在国产手机操作的系统的竞争进入了白热化的状态,我们来看一下全球操作系统市场份额。



现在全球手机操作系统市场份额是被谷歌的 Android 和苹果的 iOS 基本垄断了,其中 Android 系统占据了 38.27%,我们在来看一下这些 Android 市场份额,被那些手机厂商瓜分了。



Android 系统分别被 Sansung、Xiaomi、Oppo、Vivo 瓜分了,但是它们都受制于美国的控制,如果想摆脱美国的控制,那么偷摸自研就是唯一的出路。


相比于自研新系统,最难的是生态的建立,而生态的建立就需要各个行业的人,为你的新系统开发软件,如果没有人为你的系统开发办公软件那就不能用于工作,如果没有人为你的系统开发游戏、音乐等等软件,那么就不能用于娱乐,一个既不能用于办公,也不能用于娱乐的操作系统,试问那个消费者会去使用。


当国内操作系统都开始卷自研的操作系统时,其中最苦的无疑是移动开发者,以前只有 Android 的时候,他们只需要针对 Andriod 不同版本,不同机型做适配,现在他们需要学习自研操作系统开发语言,为不同系统、不同的设备去做更多版本的适配。


而仅仅是 Android 设备的碎片化情况,已经让 Android 开发者苦不堪言,我们用一张图看一下 Android 操作系统分裂情况(来自网上)。



作为一名深耕多年的 Android 开发者,我已经在这个世界上找不到任何一句话来形容 Android 的现状了,仅用网传的一张图向 Android 致敬。


![]( img.hi-dhl.com/kick_androi… -1-. png)


混乱的自研国产操作系统是否会走 Android 的老路,这个无法确定,但是确定的是,每个手机厂商都有自研的手机操作系统,必然会导致手机操作系统生态的更加碎片化。对于开发者而言无疑是一个重磅炸弹。


以前国内手机厂商主要使用 Android 操作系统,开发者只需要对 Andriod 不同版本,不同机型做适配,现在各个厂商都推出自研操作系统,使得移动开发者需要花费更多的时间,为自研的操作系统,不同的设备进行更多版本的适配。


虽然我也是移动操作系统资深的受害者,但是不得不为国内厂商敢于开发自己的操作系统鼓掌,但是因此造成手机操作系统的生态更加碎片化。其中受苦的无疑是开发者和用户,也期望国内系统有大一统的那一天。


国产自研操作系统加油,移动开发者加油,Android 开发者顶住。


另外根据调研机构 Counterpoint 发布的数据显示,华为 HarmonyOS 出货量仅次于苹果 iOS,晋升成为了全球第三大操作系统。



华为 HarmonyOS 占全球手机操作系统市场份额的 2%,占中国的份额的 8%,位居全球第三大操作的系统,也希望华为能跟 iOS 一样。



  • 0 广告

  • 不预装及推广第三方软件

  • 手机上的软件都可以卸载


至于广告问题,就不奢望和苹果一样几乎无广告了,任何一家公司只要感受到了广告带来的暴利,就不可能轻易砍掉。


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

三年前端还不会配置Nginx?刷完这篇就够了

一口气看完,比自学强十倍! 什么是Nginx Nginx是一个开源的高性能HTTP和反向代理服务器。它可以用于处理静态资源、负载均衡、反向代理和缓存等任务。Nginx被广泛用于构建高可用性、高性能的Web应用程序和网站。它具有低内存消耗、高并发能力和良好的...
继续阅读 »

一口气看完,比自学强十倍!



Nginx_logo-700x148.png


什么是Nginx


Nginx是一个开源的高性能HTTP和反向代理服务器。它可以用于处理静态资源、负载均衡、反向代理和缓存等任务。Nginx被广泛用于构建高可用性、高性能的Web应用程序和网站。它具有低内存消耗、高并发能力和良好的稳定性,因此在互联网领域非常受欢迎。


为什么使用Nginx



  1. 高性能:Nginx采用事件驱动的异步架构,能够处理大量并发连接而不会消耗过多的系统资源。它的处理能力比传统的Web服务器更高,在高并发负载下表现出色。

  2. 高可靠性:Nginx具有强大的容错能力和稳定性,能够在面对高流量和DDoS攻击等异常情况下保持可靠运行。它能通过健康检查和自动故障转移来保证服务的可用性。

  3. 负载均衡:Nginx可以作为反向代理服务器,实现负载均衡,将请求均匀分发给多个后端服务器。这样可以提高系统的整体性能和可用性。

  4. 静态文件服务:Nginx对静态资源(如HTML、CSS、JavaScript、图片等)的处理非常高效。它可以直接缓存静态文件,减轻后端服务器的负载。

  5. 扩展性:Nginx支持丰富的模块化扩展,可以通过添加第三方模块来提供额外的功能,如gzip压缩、SSL/TLS加密、缓存控制等。


如何处理请求


Nginx处理请求的基本流程如下:



  1. 接收请求:Nginx作为服务器软件监听指定的端口,接收客户端发来的请求。

  2. 解析请求:Nginx解析请求的内容,包括请求方法(GET、POST等)、URL、头部信息等。

  3. 配置匹配:Nginx根据配置文件中的规则和匹配条件,决定如何处理该请求。配置文件定义了虚拟主机、反向代理、负载均衡、缓存等特定的处理方式。

  4. 处理请求:Nginx根据配置的处理方式,可能会进行以下操作:



    • 静态文件服务:如果请求的是静态资源文件,如HTML、CSS、JavaScript、图片等,Nginx可以直接返回文件内容,不必经过后端应用程序。

    • 反向代理:如果配置了反向代理,Nginx将请求转发给后端的应用服务器,然后将其响应返回给客户端。这样可以提供负载均衡、高可用性和缓存等功能。

    • 缓存:如果启用了缓存,Nginx可以缓存一些静态或动态内容的响应,在后续相同的请求中直接返回缓存的响应,减少后端负载并提高响应速度。

    • URL重写:Nginx可以根据配置的规则对URL进行重写,将请求从一个URL重定向到另一个URL或进行转换。

    • SSL/TLS加密:如果启用了SSL/TLS,Nginx可以负责加密和解密HTTPS请求和响应。

    • 访问控制:Nginx可以根据配置的规则对请求进行访问控制,例如限制IP访问、进行身份认证等。



  5. 响应结果:Nginx根据处理结果生成响应报文,包括状态码、头部信息和响应内容。然后将响应发送给客户端。


什么是正向代理和反向代理


2020-03-08-5ce95a07b18a071444-20200308191723379.png


正向代理


是指客户端通过代理服务器发送请求到目标服务器。客户端向代理服务器发送请求,代理服务器再将请求转发给目标服务器,并将服务器的响应返回给客户端。正向代理可以隐藏客户端的真实IP地址,提供匿名访问和访问控制等功能。它常用于跨越防火墙访问互联网、访问被封禁的网站等情况。


反向代理


是指客户端发送请求到代理服务器,代理服务器再将请求转发给后端的多个服务器中的一个或多个,并将后端服务器的响应返回给客户端。客户端并不直接访问后端服务器,而是通过反向代理服务器来获取服务。反向代理可以实现负载均衡、高可用性和安全性等功能。它常用于网站的高并发访问、保护后端服务器、提供缓存和SSL终止等功能。


nginx 启动和关闭


进入目录:/usr/local/nginx/sbin
启动命令:./nginx
重启命令:nginx -s reload
快速关闭命令:./nginx -s stop
有序地停止,需要进程完成当前工作后再停止:./nginx -s quit
直接杀死nginx进程:killall nginx

目录结构


[root@localhost ~]# tree /usr/local/nginx
/usr/local/nginx

├── client_body_temp                 # POST 大文件暂存目录
├── conf                             # Nginx所有配置文件的目录
│   ├── fastcgi.conf                 # fastcgi相关参数的配置文件
│   ├── fastcgi.conf.default         # fastcgi.conf的原始备份文件
│   ├── fastcgi_params               # fastcgi的参数文件
│   ├── fastcgi_params.default      
│   ├── koi-utf
│   ├── koi-win
│   ├── mime.types                   # 媒体类型
│   ├── mime.types.default
│   ├── nginx.conf                   #这是Nginx默认的主配置文件,日常使用和修改的文件
│   ├── nginx.conf.default
│   ├── scgi_params                 # scgi相关参数文件
│   ├── scgi_params.default  
│   ├── uwsgi_params                 # uwsgi相关参数文件
│   ├── uwsgi_params.default
│   └── win-utf
├── fastcgi_temp                     # fastcgi临时数据目录
├── html                             # Nginx默认站点目录
│   ├── 50x.html                     # 错误页面优雅替代显示文件,例如出现502错误时会调用此页面
│   └── index.html                   # 默认的首页文件
├── logs                             # Nginx日志目录
│   ├── access.log                   # 访问日志文件
│   ├── error.log                   # 错误日志文件
│   └── nginx.pid                   # pid文件,Nginx进程启动后,会把所有进程的ID号写到此文件
├── proxy_temp                       # 临时目录
├── sbin                             # Nginx 可执行文件目录
│   └── nginx                       # Nginx 二进制可执行程序
├── scgi_temp                       # 临时目录
└── uwsgi_temp                       # 临时目录

配置文件nginx.conf


# 启动进程,通常设置成和cpu的数量相等
worker_processes  1;

# 全局错误日志定义类型,[debug | info | notice | warn | error | crit]
error_log  logs/error.log;
error_log  logs/error.log  notice;
error_log  logs/error.log  info;

# 进程pid文件
pid        /var/run/nginx.pid;

# 工作模式及连接数上限
events {
    # 仅用于linux2.6以上内核,可以大大提高nginx的性能
    use   epoll;

    # 单个后台worker process进程的最大并发链接数
    worker_connections  1024;

    # 客户端请求头部的缓冲区大小
    client_header_buffer_size 4k;

    # keepalive 超时时间
    keepalive_timeout 60;

    # 告诉nginx收到一个新连接通知后接受尽可能多的连接
    # multi_accept on;
}

# 设定http服务器,利用它的反向代理功能提供负载均衡支持
http {
    # 文件扩展名与文件类型映射表义
    include       /etc/nginx/mime.types;

    # 默认文件类型
    default_type  application/octet-stream;

    # 默认编码
    charset utf-8;

    # 服务器名字的hash表大小
    server_names_hash_bucket_size 128;

    # 客户端请求头部的缓冲区大小
    client_header_buffer_size 32k;

    # 客户请求头缓冲大小
    large_client_header_buffers 4 64k;

    # 设定通过nginx上传文件的大小
    client_max_body_size 8m;

    # 开启目录列表访问,合适下载服务器,默认关闭。
    autoindex on;

    # sendfile 指令指定 nginx 是否调用 sendfile 函数(zero copy 方式)来输出文件,对于普通应用,
    # 必须设为 on,如果用来进行下载等应用磁盘IO重负载应用,可设置为 off,以平衡磁盘与网络I/O处理速度
    sendfile        on;

    # 此选项允许或禁止使用socke的TCP_CORK的选项,此选项仅在使用sendfile的时候使用
    #tcp_nopush     on;

    # 连接超时时间(单秒为秒)
    keepalive_timeout  65;


    # gzip模块设置
    gzip on;               #开启gzip压缩输出
    gzip_min_length 1k;    #最小压缩文件大小
    gzip_buffers 4 16k;    #压缩缓冲区
    gzip_http_version 1.0; #压缩版本(默认1.1,前端如果是squid2.5请使用1.0)
    gzip_comp_level 2;     #压缩等级
    gzip_types text/plain application/x-javascript text/css application/xml;
    gzip_vary on;

    # 开启限制IP连接数的时候需要使用
    #limit_zone crawler $binary_remote_addr 10m;

    # 指定虚拟主机的配置文件,方便管理
    include /etc/nginx/conf.d/*.conf;


    # 负载均衡配置
    upstream aaa {
        # 请见上文中的五种配置
    }


   # 虚拟主机的配置
    server {

        # 监听端口
        listen 80;

        # 域名可以有多个,用空格隔开
        server_name www.aaa.com aaa.com;

        # 默认入口文件名称
        index index.html index.htm index.php;
        root /data/www/sk;

        # 图片缓存时间设置
        location ~ .*.(gif|jpg|jpeg|png|bmp|swf)${
            expires 10d;
        }

        #JS和CSS缓存时间设置
        location ~ .*.(js|css)?${
            expires 1h;
        }

        # 日志格式设定
        #$remote_addr与 $http_x_forwarded_for用以记录客户端的ip地址;
        #$remote_user:用来记录客户端用户名称;
        #$time_local:用来记录访问时间与时区;
        #$request:用来记录请求的url与http协议;
        #$status:用来记录请求状态;成功是200,
        #$body_bytes_sent :记录发送给客户端文件主体内容大小;
        #$http_referer:用来记录从那个页面链接访问过来的;
        log_format access '$remote_addr - $remote_user [$time_local] "$request" '
        '$status $body_bytes_sent "$http_referer" '
        '"$http_user_agent" $http_x_forwarded_for';

        # 定义本虚拟主机的访问日志
        access_log  /usr/local/nginx/logs/host.access.log  main;
        access_log  /usr/local/nginx/logs/host.access.404.log  log404;

        # 对具体路由进行反向代理
        location /connect-controller {

            proxy_pass http://127.0.0.1:88;
            proxy_redirect off;
            proxy_set_header X-Real-IP $remote_addr;

            # 后端的Web服务器可以通过X-Forwarded-For获取用户真实IP
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header Host $host;

            # 允许客户端请求的最大单文件字节数
            client_max_body_size 10m;

            # 缓冲区代理缓冲用户端请求的最大字节数,
            client_body_buffer_size 128k;

            # 表示使nginx阻止HTTP应答代码为400或者更高的应答。
            proxy_intercept_errors on;

            # nginx跟后端服务器连接超时时间(代理连接超时)
            proxy_connect_timeout 90;

            # 后端服务器数据回传时间_就是在规定时间之内后端服务器必须传完所有的数据
            proxy_send_timeout 90;

            # 连接成功后,后端服务器响应的超时时间
            proxy_read_timeout 90;

            # 设置代理服务器(nginx)保存用户头信息的缓冲区大小
            proxy_buffer_size 4k;

            # 设置用于读取应答的缓冲区数目和大小,默认情况也为分页大小,根据操作系统的不同可能是4k或者8k
            proxy_buffers 4 32k;

            # 高负荷下缓冲大小(proxy_buffers*2)
            proxy_busy_buffers_size 64k;

            # 设置在写入proxy_temp_path时数据的大小,预防一个工作进程在传递文件时阻塞太长
            # 设定缓存文件夹大小,大于这个值,将从upstream服务器传
            proxy_temp_file_write_size 64k;
        }

        # 动静分离反向代理配置(多路由指向不同的服务端或界面)
        location ~ .(jsp|jspx|do)?$ {
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_pass http://127.0.0.1:8080;
        }
    }
}

location


location指令的作用就是根据用户请求的URI来执行不同的应用


语法


location [ = | ~ | ~* | ^~ ] uri {...}


  • [ = | ~ | ~* | ^~ ]:匹配的标识



    • ~~*的区别是:~区分大小写,~*不区分大小写

    • ^~:进行常规字符串匹配后,不做正则表达式的检查



  • uri:匹配的网站地址

  • {...}:匹配uri后要执行的配置段


举例


location = / {
    [ configuration A ]
}
location / {
    [ configuration B ]
}
location /sk/ {
    [ configuration C ]
}
location ^~ /img/ {
    [ configuration D ]
}
location ~* .(gif|jpg|jpeg)$ {
    [ configuration E ]
}


  • = / 请求 / 精准匹配A,不再往下查找

  • / 请求/index.html匹配B。首先查找匹配的前缀字符,找到最长匹配是配置B,接着又按照顺序查找匹配的正则。结果没有找到,因此使用先前标记的最长匹配,即配置B。

  • /sk/ 请求/sk/abc 匹配C。首先找到最长匹配C,由于后面没有匹配的正则,所以使用最长匹配C。

  • ~* .(gif|jpg|jpeg)$ 请求/sk/logo.gif 匹配E。首先进行前缀字符的查找,找到最长匹配项C,继续进行正则查找,找到匹配项E。因此使用E。

  • ^~ 请求/img/logo.gif匹配D。首先进行前缀字符查找,找到最长匹配D。但是它使用了^~修饰符,不再进行下面的正则的匹配查找,因此使用D。


单页面应用刷新404问题


    location / {
        try_files $uri $uri/ /index.html;
    }

配置跨域请求


server {
    listen   80;
    location / {
        # 服务器默认是不被允许跨域的。
        # 配置`*`后,表示服务器可以接受所有的请求源(Origin),即接受所有跨域的请求
        add_header Access-Control-Allow-Origin *;
        
        add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
        add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
        
        # 发送"预检请求"时,需要用到方法 OPTIONS ,所以服务器需要允许该方法
        # 给OPTIONS 添加 204的返回,是为了处理在发送POST请求时Nginx依然拒绝访问的错误
        if ($request_method = 'OPTIONS') {
            return 204;
        }
    }
}

开启gzip压缩


    # gzip模块设置
    gzip on;               #开启gzip压缩输出
    gzip_min_length 1k;    #最小压缩文件大小
    gzip_buffers 4 16k;    #压缩缓冲区
    gzip_http_version 1.0; #压缩版本(默认1.1,前端如果是squid2.5请使用1.0)
    gzip_comp_level 2;     #压缩等级
    
    # 设置什么类型的文件需要压缩
    gzip_types text/plain application/x-javascript text/css application/xml;
    
    # 用于设置使用Gzip进行压缩发送是否携带“Vary:Accept-Encoding”头域的响应头部
    # 主要是告诉接收方,所发送的数据经过了Gzip压缩处理
    gzip_vary on;

总体而言,Nginx是一款轻量级、高性能、可靠性强且扩展性好的服务器软件,适用于搭建高可用性、高性能的Web应用程序和网站。


作者:日月之行_
来源:juejin.cn/post/7270153705877241890
收起阅读 »

生病、裁员、假合同与开发者。聊聊最近遇到的事儿

今天这篇文章,咱们不聊技术哈,聊聊这两天我经历的事和一些感悟吧。 大致是三个事情: 生病: 自己和家人生病,在医院看到的形形色色 裁员: 之前黑马的老同事被裁干净了,当很多人在面对裁员时的一些表现和反应 假合同: 入职没有拿合同,工作两年仲裁的时候,发现签的...
继续阅读 »

今天这篇文章,咱们不聊技术哈,聊聊这两天我经历的事和一些感悟吧。


大致是三个事情:



  1. 生病: 自己和家人生病,在医院看到的形形色色

  2. 裁员: 之前黑马的老同事被裁干净了,当很多人在面对裁员时的一些表现和反应

  3. 假合同: 入职没有拿合同,工作两年仲裁的时候,发现签的合同变了


生病:在医院看到的形形色色


这几天可真是被折磨的够呛,先是我支原体感染,持续咳嗽了两周,结果肺结节了😭



然后是我闺女开始咳嗽,陪着一起打针。在医院看到很多小孩,整个医院几乎是满满的。这是凌晨12点的医院挂号处:



随处可见的都是疲惫不堪的父母和孩子。


最厉害的,还是这位大姐,一边照顾孩子,一边还在写代码(偷偷看了一眼是 java 😂😂)



再加上这几天北方的大雪。想一想:晚上陪孩子打针到深夜,第二天早上冒着大雪去上班。晚上加完班之后再陪孩子来医院。真的是无限的疲惫。


裁员:普通人面临被裁时的反应


我有幸在 7 月的时候经历过一次大裁员,也是在那个时候离开的。


当时有很多没有被裁的同事,一是庆幸自己可以继续留下,毕竟现在行情是真的不好。二是也寄希望于年后可以继续开启招生。


结果等来的不是行情变好而是持续的裁员。


本身裁员嘛,没什么可说的。但是一个朋友的经历以及想法却值得我们进行深思。


事情是这样的:



这位朋友是月初的时候被谈离职,离职协议也都谈完了,赔偿打折,月底走人。


但是在最后这段时间里面,公司却安排了他大量的出差以及无意义的工作。


所以我就跟他说:“这你还干啊?天天熬到那么晚?”


他跟说我:“多表现表现,万一公司可以回心转意,让我继续留下呢?”



这不禁让我想起来之前大家都在说的骆驼祥子,祥子到死的时候都认为这一切是自己不够努力所导致的。


对于公司而言,公司不会养任何已经没有了价值的人,就像我这个朋友一样。同时也不会让一个人的价值过大,无法控制,就像最近 “董宇辉小作文事件” 一样。


对于大多数的普通人而言,最悲惨的就是:当别人拿起屠刀要杀你的时候,你所想到的不是奋起反抗,而是希望可以通过祈求来得到别人的宽恕和原谅。


假合同: 仲裁时才发现入职合同变了


这是一个同学跟我说的,事情是这样的:



这位同学在入职的时候签订了劳动合同,但是当时公司以统一盖章为由,没有及时把劳动合同给他,后来他也忽略了这个事情。


直到前段时间,因为公司长期拖欠工资他发起仲裁,公司拿出来当时签订的劳动合同


发现在他的签字页之外的合同内容,都发生了变化


他的薪资变成了底薪3000,其他的全部是项目奖金的形式。



算是吃了一个亏。


一点小感悟


对于我们这种普通人而言,在工作中大多数的时候真的是处于弱势地位。这与技术好坏并无关系。很多技术很好的人依然充满着焦虑,时刻担心着 35 岁危机的事情。


所以说,打工只是过程,想办法赚钱才是目的。


最后祝大家都可以身体健康,拿到满意的 offer!


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

我强烈建议你也做抖音个人ip

勇于尝试可以有更多可能性。想来很多人来稀土掘金分享的初心也是尝试吧。 笔者之前说过笔者已经是迈入35的人,且说实话是一线的大头兵。生活在北京多年,或者活跃在互联网多年,35岁这个魔咒几年前就开始困扰,乃至如今加重笔者的焦虑。 前几年公司受到国家宏观经济政策影响...
继续阅读 »

勇于尝试可以有更多可能性。想来很多人来稀土掘金分享的初心也是尝试吧。


笔者之前说过笔者已经是迈入35的人,且说实话是一线的大头兵。生活在北京多年,或者活跃在互联网多年,35岁这个魔咒几年前就开始困扰,乃至如今加重笔者的焦虑。


前几年公司受到国家宏观经济政策影响,业绩受到极大影响,连带着年终奖缩水。紧接着三年疫情,原本水深火热的日子,一下雪上加霜。


处在一个难以看到前途的年纪和环境中,笔者和大多数人一样尝试寻找出路。笔者也在其他文章中说过,笔者来稀土掘金的目的就是探索出路。


正如文章开头写的勇于尝试可以有更多可能性。在稀土掘金分享这段时间,写了一些文章,也有很多思考。或许也正是如此让笔者想明白想清楚很多事。


几年前雷军说过一句话:处在风口之上,猪也能飞起来。很长时间里,笔者都在寻找风口,笔者管风口叫做趋势性,但事实上笔者一直没有抓住这个趋势性。后来笔者退而求其次,想到了结构性,想到了应该先察结构性,而后努力提升自己,在关键时候抓住或者等到风口。


或许真的是大趋势不能预测,只能等待。这应该就是所谓的借势。


笔者后来想到,社会是结构性的,人处在结构中,在结构中生存生活,随结构而走而变。结构不是一成不变的,所以有了趋势性。


笔者建议你做抖音个人ip,因为抖音是一个更大的结构性,有着更大的潜在趋势性。正所谓富在术数不在劳身,利在势局不在力耕


因为行业现状,以及技术本身的特性,程序员天然是一群努力且上进的人。与寻常人相比,程序员更适合做个人ip。当然做个人ip不一定能成,但做什么一定能成呢?


笔者建议你做抖音个人ip,还因为抖音已经发展了几年,用户已经足够大,只要你有一定的才华,一定可以被认可,就如同在稀土掘金一样,同样会呈现算法加持的成果。


笔者相信,只要本着用户为本,科技向善的初心,坚持做下去一定会有收获,一定可以找到一群志同道合的人。


(本文完)


作者:通往自由之路
来源:juejin.cn/post/7312404619518853146
收起阅读 »

农业银行算法题,为什么用初中知识出题,这么多人不会?

背景介绍 总所周知,有相当一部分的大学生是不会初高中知识的。 因此,每当那种「初等数学为背景编写的算法题」在笔面出现,舆论往往分成"三大派": 甚至那位说"致敬高考"的同学也搞岔了,高考哪有这么简单,美得你 🤣 这仅仅是初中数学「几何学」中较为简单的知识...
继续阅读 »

背景介绍


总所周知,有相当一部分的大学生是不会初高中知识的


因此,每当那种「初等数学为背景编写的算法题」在笔面出现,舆论往往分成"三大派":


对初高中知识,有清晰记忆


对初高中知识,记忆模糊


啥题?不会!


甚至那位说"致敬高考"的同学也搞岔了,高考哪有这么简单,美得你 🤣


这仅仅是初中数学「几何学」中较为简单的知识点。


抓住大学生对初高中知识这种「会者不难,难者不会」的现状,互联网大厂似乎更喜欢此类「考察初等数学」的算法题。


因为十个候选人,九个题海战术,HOT 100 和剑指 Offer 大家都刷得飞起了。


冷不丁的考察这种题目,反而更能起到"筛选"效果。


但此类算法题,农业银行 并非首创,甚至是同一道题,也被 华为云美的百度 先后出过。


学好初中数学,就能稳拿华为 15 级?🤣




下面,一起来看看这道题。


题目描述


平台:LeetCode


题号:149


给你一个数组 points,其中 points[i]=[xi,yi]points[i] = [x_i, y_i] 表示 X-Y 平面上的一个点。求最多有多少个点在同一条直线上。


示例 1:


输入:points = [[1,1],[2,2],[3,3]]

输出:3

示例 2:


输入:points = [[1,1],[3,2],[5,3],[4,1],[2,3],[1,4]]

输出:4

提示:



  • 1<=points.length<=3001 <= points.length <= 300

  • points[i].length=2points[i].length = 2

  • 104<=xi,yi<=104-10^4 <= x_i, y_i <= 10^4

  • points 中的所有点互不相同


枚举直线 + 枚举统计


我们知道,两点可以确定一条线。


一个朴素的做法是先枚举两点(确定一条线),然后检查其余点是否落在该线中。


为避免除法精度问题,当我们枚举两个点 xxyy 时,不直接计算其对应直线的 斜率截距


而是通过判断 xxyy 与第三个点 pp 形成的两条直线斜率是否相等,来得知点 pp 是否落在该直线上。


斜率相等的两条直线要么平行,要么重合。


平行需要 44 个点来唯一确定,我们只有 33 个点,因此直接判定两条直线是否重合即可。


详细说,当给定两个点 (x1,y1)(x_1, y_1)(x2,y2)(x_2, y_2) 时,对应斜率 y2y1x2x1\frac{y_2 - y_1}{x_2 - x_1}


为避免计算机除法的精度问题,我们将「判定 aybyaxbx=bycybxcx\frac{a_y - b_y}{a_x - b_x} = \frac{b_y - c_y}{b_x - c_x} 是否成立」改为「判定 (ayby)×(bxcx)=(axbx)×(bycy)(a_y - b_y) \times (b_x - c_x) = (a_x - b_x) \times (b_y - c_y) 是否成立」。


将存在精度问题的「除法判定」巧妙转为「乘法判定」。


Java 代码:


class Solution {
public int maxPoints(int[][] points) {
int n = points.length, ans = 1;
for (int i = 0; i < n; i++) {
int[] x = points[i];
for (int j = i + 1; j < n; j++) {
int[] y = points[j];
// 枚举点对 (i,j) 并统计有多少点在该线上, 起始 cnt = 2 代表只有 i 和 j 两个点在此线上
int cnt = 2;
for (int k = j + 1; k < n; k++) {
int[] p = points[k];
int s1 = (y[1] - x[1]) * (p[0] - y[0]);
int s2 = (p[1] - y[1]) * (y[0] - x[0]);
if (s1 == s2) cnt++;
}
ans = Math.max(ans, cnt);
}
}
return ans;
}
}

C++ 代码:


class Solution {
public:
int maxPoints(vector<vector<int>>& points) {
int n = points.size(), ans = 1;
for (int i = 0; i < n; i++) {
vector<int> x = points[i];
for (int j = i + 1; j < n; j++) {
vector<int> y = points[j];
// 枚举点对 (i,j) 并统计有多少点在该线上, 起始 cnt = 2 代表只有 i 和 j 两个点在此线上
int cnt = 2;
for (int k = j + 1; k < n; k++) {
vector<int> p = points[k];
int s1 = (y[1] - x[1]) * (p[0] - y[0]);
int s2 = (p[1] - y[1]) * (y[0] - x[0]);
if (s1 == s2) cnt++;
}
ans = max(ans, cnt);
}
}
return ans;
}
};

Python 代码:


class Solution:
def maxPoints(self, points: List[List[int]]) -> int:
n, ans = len(points), 1
for i, x in enumerate(points):
for j in range(i + 1, n):
y = points[j]
# 枚举点对 (i,j) 并统计有多少点在该线上, 起始 cnt = 2 代表只有 i 和 j 两个点在此线上
cnt = 2
for k in range(j + 1, n):
p = points[k]
s1 = (y[1] - x[1]) * (p[0] - y[0])
s2 = (p[1] - y[1]) * (y[0] - x[0])
if s1 == s2: cnt += 1
ans = max(ans, cnt)
return ans

TypeScript 代码:


function maxPoints(points: number[][]): number {
let n = points.length, ans = 1;
for (let i = 0; i < n; i++) {
let x = points[i];
for (let j = i + 1; j < n; j++) {
// 枚举点对 (i,j) 并统计有多少点在该线上, 起始 cnt = 2 代表只有 i 和 j 两个点在此线上
let y = points[j], cnt = 2;
for (let k = j + 1; k < n; k++) {
let p = points[k];
let s1 = (y[1] - x[1]) * (p[0] - y[0]);
let s2 = (p[1] - y[1]) * (y[0] - x[0]);
if (s1 == s2) cnt++;
}
ans = Math.max(ans, cnt);
}
}
return ans;
};


  • 时间复杂度:O(n3)O(n^3)

  • 空间复杂度:O(1)O(1)


枚举直线 + 哈希表统计


根据「朴素解法」的思路,枚举所有直线的过程不可避免,但统计点数的过程可以优化。


具体的,我们可以先枚举所有可能出现的 直线斜率(根据两点确定一条直线,即枚举所有的「点对」),使用「哈希表」统计所有 斜率 对应的点的数量,在所有值中取个 maxmax 即是答案。


一些细节:在使用「哈希表」进行保存时,为了避免精度问题,我们直接使用字符串进行保存,同时需要将 斜率 约干净(套用 gcd 求最大公约数模板)。


Java 代码:


class Solution {
public int maxPoints(int[][] points) {
int n = points.length, ans = 1;
for (int i = 0; i < n; i++) {
Map<String, Integer> map = new HashMap<>();
// 由当前点 i 发出的直线所经过的最多点数量
int max = 0;
for (int j = i + 1; j < n; j++) {
int x1 = points[i][0], y1 = points[i][1], x2 = points[j][0], y2 = points[j][1];
int a = x1 - x2, b = y1 - y2;
int k = gcd(a, b);
String key = (a / k) + "_" + (b / k);
map.put(key, map.getOrDefault(key, 0) + 1);
max = Math.max(max, map.get(key));
}
ans = Math.max(ans, max + 1);
}
return ans;
}
int gcd(int a, int b) {
return b == 0 ? a : gcd(b, a % b);
}
}

C++ 代码:


class Solution {
public:
int maxPoints(vector<vector<int>>& points) {
int n = points.size(), ans = 1;
for (int i = 0; i < n; i++) {
map<string, int> map;
int maxv = 0;
for (int j = i + 1; j < n; j++) {
int x1 = points[i][0], y1 = points[i][1], x2 = points[j][0], y2 = points[j][1];
int a = x1 - x2, b = y1 - y2;
int k = gcd(a, b);
string key = to_string(a / k) + "_" + to_string(b / k);
map[key]++;
maxv = max(maxv, map[key]);
}
ans = max(ans, maxv + 1);
}
return ans;
}
int gcd(int a, int b) {
return b == 0 ? a : gcd(b, a % b);
}
};

Python 代码:


class Solution:
def maxPoints(self, points):
def gcd(a, b):
return a if b == 0 else gcd(b, a % b)

n, ans = len(points), 1
for i in range(n):
mapping = {}
maxv = 0
for j in range(i + 1, n):
x1, y1 = points[i]
x2, y2 = points[j]
a, b = x1 - x2, y1 - y2
k = gcd(a, b)
key = str(a // k) + "_" + str(b // k)
mapping[key] = mapping.get(key, 0) + 1
maxv = max(maxv, mapping[key])
ans = max(ans, maxv + 1)
return ans

TypeScript 代码:


function maxPoints(points: number[][]): number {
const gcd = function(a: number, b: number): number {
return b == 0 ? a : gcd(b, a % b);
}
let n = points.length, ans = 1;
for (let i = 0; i < n; i++) {
let mapping = {}, maxv = 0;
for (let j = i + 1; j < n; j++) {
let x1 = points[i][0], y1 = points[i][1], x2 = points[j][0], y2 = points[j][1];
let a = x1 - x2, b = y1 - y2;
let k = gcd(a, b);
let key = `${a / k}_${b / k}`;
mapping[key] = mapping[key] ? mapping[key] + 1 : 1;
maxv = Math.max(maxv, mapping[key]);
}
ans = Math.max(ans, maxv + 1);
}
return ans;
};


  • 时间复杂度:枚举所有直线的复杂度为 O(n2)O(n^2);令坐标值的最大差值为 mmgcd 复杂度为 O(logm)O(\log{m})。整体复杂度为 O(n2×logm)O(n^2 \times \log{m})

  • 空间复杂度:O(n)O(n)


总结


虽然题目是以初中数学中的"斜率 & 截距"为背景,但仍有不少细节需要把握。


这也是「传统数学题」和「计算机算法题」的最大差别:



  • 过程分值: 传统数学题有过程分,计算机算法题没有过程分,哪怕思路对了 9090%,代码没写出来,就是 00 分;

  • 数据类型:传统数学题只涉及数值,计算机算法题需要考虑各种数据类型;

  • 运算精度:传统数学题无须考虑运算精度问题,而计算机算法题需要;

  • 判定机制:传统数学题通常给定具体数据和问题,然后人工根据求解过程和最终答案来综合评分,而计算机算法题不仅仅是求解一个具体的 case,通常是给定数据范围,然后通过若个不同的样例,机器自动判断程序的正确性;

  • 执行效率/时空复杂度:传统数学题无须考虑执行效率问题,只要求考生通过有限步骤(或引用定理节省步骤)写出答案即可,计算机算法题要求程序在有限时间空间内执行完;

  • 边界/异常处理:由于传统数学题的题面通常只有一个具体数据,因此不涉及边界处理,而计算机算法题需要考虑数据边界,甚至是对异常的输入输出做相应处理。


可见,传统数学题,有正确的思路基本上就赢了大半,而计算机算法题嘛,有正确思路,也只是万里长征跑了个 400400 米而已。


作者:宫水三叶的刷题日记
来源:juejin.cn/post/7312035308362039346
收起阅读 »

现在工作很难找,不要和年轻人抢饭碗

今年9月结束了和美团三年的合同后,对于步入中年的我,想让自己停一下,对自己的人生价值进行深入而系统的思考。 曾经我有一个梦想,不对,是有很多梦想,比如成为数学家、科学家、飞行员、宇航员或将军,哪一个才是我真正的梦想?那个我愿意用下半生去奋斗去拼搏的梦想? 结合...
继续阅读 »

今年9月结束了和美团三年的合同后,对于步入中年的我,想让自己停一下,对自己的人生价值进行深入而系统的思考。


曾经我有一个梦想,不对,是有很多梦想,比如成为数学家、科学家、飞行员、宇航员或将军,哪一个才是我真正的梦想?那个我愿意用下半生去奋斗去拼搏的梦想?


结合自己的名字叫天文,基于自己所学的专业、过往的经历、资源、国家政策导向和人类社会的发展趋势,我选择了航天方向的梦想,从点燃孩子们的航天梦开始,成为航天领域的企业家。


大环境比想象的差


当我把创业的想法,分享给身边的家人朋友时,几乎所有的人都和说,现在经济环境这么差,工作这么难找,还是要慎重,不要轻易去创业。


因为儿子的托育机构跑路了,我需要协助看娃,即使有合适的工作,也不能立即去上班,所以我基于老婆的公司尝试去招人。


通过招聘,我发现,不管是艺培行业的老师,还是互联网行业的产研人员,失业的比例都很大,很多23年的应届生,毕业后一直找不到工作,干脆国庆期间就离开深圳回老家了。


对于产品经理的实习生岗位,每天都有大量在香港大学、香港中文、香港科技、香港理工、香港城市、新加坡大学、清华大学以及很多国内的985的硕士前来应聘我们的岗位。


WechatIMG157.jpg


社招也是一样,很多名校毕业的,找了好几个月都没有合适的工作,普通学历的产研人员,可能连面试机会都很少。比如前端方向,只要我在 boss 开放招聘,每天都会有几百人主动找我,看都看不过来。


而猎头约我去面试,听到我因为要创业,无一不说现在环境很差,还是好好考虑一下她们推荐的岗位吧。


WechatIMG51.jpg


过去一年我接触过的一些机会


对于已经超过35岁的码农,只要满足企业的用人诉求,即使环境再差,也是有很多工作机会的,在此和大家聊一聊,过去一年我都参加或拒绝了哪些公司的面试。


去年7月,我就多次向领导提了离职,那时也想创业,但没有现在强烈,所以还是认真地找了找工作,那时的面试机会,应该是今年的三倍以上。


首先字节的岗位最多,至少一半猎头推荐的都有字节的岗位,我选择了面试客服体验部,先是在上海的前端总监加了我微信,入职后我向他汇报,管理深圳16人左右的团队,一面的面试官应该是我入职后的下属,但我不是很想去,所以面试随便聊了聊,加了微信,后面还约了个饭。


后续字节的岗位,虽然每周都有猎头给我推荐,但是我都没有答应面试,最近答应约了的面试,因为决定创业了,所以主动取消了,飞书的HR还专门打电话让我再考虑考虑,看来是确实招人,而且他还说有很多方向可以选。


去年我8月我面了美的集团的大前端岗位,通过了四轮,面完HRVP后,已经约好了和CEO面试,但因为我决定留在美团再干一年,做我想做的人工智能,所以我主动取消了面试。这应该是我这几年面试过最高规格的面试,每一轮都是三个以上面试官,入职后直接向CEO汇报,整合集团大前端方向所有的研发,需要管理100多人。


当然也有其他一些高管职位,今年夏天也接到一个猎头推荐我面试贝壳的大前端负责人,直接向CTO汇报,管理200多人的团队,原来的负责人已经提离职,岗位需要保密。


对了,还有编程猫,因为我创业的方向和少儿编程有关,所以也想和他们聊一聊,面试的是高级技术总监,接手联合创始人管理的编程系统研发团队,一面我的是一个大姐,她是web前端负责人,管理20多人,我上来当HR的面,指出了几个明显的bug,让她不高兴了,所以她没让我过,本来还想和他们老板聊一聊。


去年8月中旬后,我基本就不答应面试了,但经常协助一个离职的美团同学面试,期间他面试了字节的多个部门,比如我推荐的web剪映负责人,他因为是web图形学方向的,不够匹配,只通过了两轮。后面他拿到了蔚来手机、小米、阿里、腾讯和万兴科技的offer。


最后,他犹豫是去腾讯还是万兴科技,我们还吃夜宵专门讨论了一下。腾讯他面了三个部门,第三个部门才给的机会,因为他马上要去香港大学MBA,所以选择了腾讯,不带团队。但我建议他去万兴,因为是负责一个事业部,管理近百人团队,对职业发展更好。


今年我好像只答应了两三个面试,其中一个小红书的质效前端负责人,猎头忽悠我年薪可以给到250万到400万,后来又不招了,最近约上了,聊了聊,入职后管理团队应该只有五六人,应该不会有那么高的薪资,而且必须去北京或上海,所以通过了,我也不能去。


还有参加了希音的面试,是新成立的基础架构部,一面我的是一个腾讯云过去的前端同学,聊得还可以,二面和部长聊了聊,感觉他压力有点大,我过去后需要自己找方向,比如端智能,不带团队。


突然想起来,我还面了金山云,她们的HR很热情,所以我答应聊了聊,一面的面试官,对我反馈很好,但他们招聘的岗位职级比较低,不匹配,而且他们在珠海,通过了我也不会去。


有个比较好的机会,但没有参加面试,这华为孟晚舟直接负责的总部研发团队,入职后管理一百人左右的大前端团队,离我家比较近,还能接触到华为未来的掌门人。


也接到过一些外企的面试邀请,但都没参加。


工作难找,不仅要把机会留给年轻人,还要创造更多的机会


很显然,当前我们正处于经济大萧条的前夜,或者已经在经济危机之中,但正如谭sir视频中一个老人说的:要向前看。


危机有”危“和”机“组成,意味危险和机会共存,消极的人只会抱怨危险,只有积极的人才能抓住机会,危险越大,机会越大。


很多伟大的公司,都是诞生于危机之中。新的一年,国际国内经济大环境发生了很大的变化,国家的工作重点逐步从抵抗疫情转向振兴经济。


中国经济在经历了近四十年的高速增长后,宏观经济增速放缓属于必然。


一是历史上的日本、德国,在二十世纪五六十年代和七十年代都有过非常高的增长期,然后都慢慢放缓。中国是一个特例,过去四十年中国GDP的增长保持着两位数,所以未来中国GDP的增长放缓至4%—5%符合历史规律。


二是中国的人口红利和流量红利时代已经结束。中国统计局数据显示,中国25-69岁之间的人口,在过去三十年(1990-2020年)增长了76%,但是今后三十年(2020-2050年)会从9.4亿人降到7亿人。


中国的互联网红利见顶,中国互联网络信息中心数据显示,2007年至2017年中国互联网用户的上网时长增长了36倍,相当于每年平均增长约43%。但是在2017年至2022年只增长了1.5倍,相当于每年平均增长约8%。


虽然短期至中期内,中国经济将不可避免地经历转型阵痛,但从长期看,中国每年4%-5%的经济增长速度还是远高于其他主要的大型经济体。牛津和哈佛发布的研究数据显示,从GDP复合增长率来看,中国是美国的1.8倍、是德国的2.3倍、是日本的2.5倍。


另外一个因素是,在亚洲国家中,中国的经济总量占有绝对领先的地位。麦肯锡前段时间做了数据统计,中国2022年的GDP约18万亿美元,到2030年,假设每年仅按2%的速度增长,中国GDP的增量就相当于印度今天的GDP总量。


虽然中国已经是全球第二大经济体,之所以有这么大的经济增长潜力,是因为从世界的角度来看,中国的人均GDP和人均消费还很低,世界银行数据显示,2021年,中国人均GDP约是美国的1/6,人均消费约是美国的1/9。


同时,中国的城市人口体量巨大且仍在不断增长。中国今天的城镇化率是65%,未来5-10年可能增长到75%甚至是80%,预计约1.4亿人口会变成新增城镇人口,这一人口增量相当于美国总人口的40%。


所以,我们要对国家的发展有信心,困难只是暂时的。虽然我上有老,下有小,但也不至于没饭吃,但很多年轻人,他们需要一份工作,才能在大城市生存。


所以需要更多像我一样的中年人,不仅不要和年轻人抢工作机会,还要积极为年轻人创造新的工作机会。地球竞争太激烈了,我们的未来在上天入地(这好像是我在美团的老板王兴说的)。


我打算干啥


我从小就有一个航天梦,大学选择了航天测控和卫星导航相关的专业,毕业后成了通信军官,但没能进入航天系统,对未来有些迷茫,于是选择了退役。


退役后进入了外企,后面又去了美团等互联网公司工作了几年,如今已经是一双儿女的父亲。前段时间,陪儿子读了一本以登月主题的绘本,让我逐渐找回了曾经的梦想。


好奇是人类的天性,也是社会进步的动力。探索太空不仅可以满足人类的好奇心,更可以为人类的未来发展提供了无限的可能性。


太空探索是一项长期而复杂的事业,需要一代代有航天梦的人才持续加入,这就是我们创业的出发点,希望同大家一起点燃孩子们的航天梦:通过以太空为主题的绘本,引入绘画创作的方向,并将绘画作品作为图形编程的素材,完成各种编程创作任务,帮助孩子们掌握 带领人类飞离太阳系 需要学习掌握的各种知识技能。


等这些孩子长大以后,我们再把他们招聘到我们制造航天器的公司,实现让人类可以进行商业星际旅行。


我们正在研发的系统


两个小程序,蜗牛绘馆和艺培助理已经发布到线上,等商业模式完全跑通后,再同步做app。


3D展馆:以 3D 的方式展示学生的绘画作品,帮助机构推广招生。


AIGC工具:智能抠图、以文生图、数字人、PPT制作、智能成片等。


海报设计:参考业界的稿定设计、美图等精品,为艺培机构提供精品海报和海量AI生成的素材,支持通过PC端和小程序下载。


课件系统:提供自营的绘本+绘画+编程的在线特色课件,并打造一个课件生产生态,提供各种类别的课件PPT。


编程系统:可导入图片和绘画作品,为学生编程提供丰富的素材。


长远规划及招聘计划


未来三到五年,我们希望可以做到:



  • 蜗牛绘馆:在中国多个核心城市开几百家直营绘馆,月活家长达到几十万,会员用户达到几万,实现年营收几亿。

  • 艺培助理:月活达到几百万,服务几千家加盟店,拥有几万普通会员,实现年营收几十亿。

  • 各类社区:面向成人,对于不同的兴趣方向,打造不同的内容社区。

  • 周边生态:基于太空主题,研发相关的绘本、教具、玩具、服装等。

  • 航天制造:研发自己的航天飞行器,探索飞离太阳系的各种技术。


发展顺利的话,我们的组织架构及规模设想:



  • 基础技术部(500人):提供私有云服务、企业效能系统及AIGC的技术基座。

  • 绘馆事业部(500人):负责线下绘馆门店业务的开展,教学及教务为主。

  • 换购事业部(200人):负责二手交易平台的系统及线上线下运营。

  • 课件事业部(300人):负责课件系统的研发及课件社区的运营。

  • 编程事业部(300人):负责少儿编程相关的课程及系统研发。

  • 营销事业部(200人):负责3D展馆、海报、视频制作等的研发及业务开展。

  • 玩具事业部(100人):研发航天相关的玩具、服装或教具。

  • 公益事业部(100人):负责把家长换课的闲置物品捐赠给大城市农民工或贫困地区的孩子,组织志愿者远程给偏远地区孩子上科创综合课。


总结


WechatIMG239.jpg


和平年代,虽然没有战斗,但更需要军人的勇气,邓小平当年指出,军队建设要服从国家大局。虽然我退役了,但军人的血性刻在骨里,相比英雄的先辈们在抗日战争和抗美援朝中付出的鲜血,创业的艰难算什么呀~


我们要把航天梦一代代的传下去,我们将付出任何代价、忍受任何重负、应付任何艰辛、支持任何朋友、反对任何敌人,以确保梦想的存在与实现。


有梦想的人才有灵魂,才会快乐。我们为梦想所奉献的精力、信念和忠诚,将照亮我们的国家和身边的人,而这火焰发出的光芒定能照亮全世界。


作者:三一习惯
来源:juejin.cn/post/7309158055018053658
收起阅读 »

16进制竟然可以减小代码体积

web
随着前端项目的复杂度越来越高,打包后的文件体积也越来越大,这直接影响到页面的加载速度。为了优化加载速度,我们需要采取各种方式来减小包的体积。使用 16 进制存储就是一种非常有效的方法。 以 @tenado/lunarjs 库为例,它提供了阴阳历转换以及获取节日...
继续阅读 »

随着前端项目的复杂度越来越高,打包后的文件体积也越来越大,这直接影响到页面的加载速度。为了优化加载速度,我们需要采取各种方式来减小包的体积。使用 16 进制存储就是一种非常有效的方法。


@tenado/lunarjs 库为例,它提供了阴阳历转换以及获取节日信息等功能。如果我们查看它的源码,可以看到使用了 16 进制存储。例如src/lunar.js


# src/lunar.js
export default [ 0x4bd8, 0x4ae0, 0xa570, 0x54d5, 0xd260, ... ];

这些信息如果用字符串存储,会占用很多字节。但使用 16 进制后,每个阴历信息只需要 6 个字符串。这极大地减少了库的大小。在实际生产环境中,使用 16 进制存储甚至可以节省几十 KB 的大小。


此外,16 进制还可以用于压缩其他数据,比如图片等资源。使用 16 进制编码,可以达到无损压缩的效果,相比传统压缩算法可以减小体积而不影响质量。


总之,16 进制编码是一种非常高效的存储方式,可以大幅减小项目的打包体积,提升页面加载速度。在前端优化中,合理使用 16 进制编码是一个非常重要和有效的手段。


16 进制的基本概念


16 进制在数学中是一种逢16进1的进位制。一般用数字0到9和字母A到F表示,其中:A~F相当于十进制的10~15,这些称作十六进制数字,在 js 中,16 进制使用 0x 前缀表示。


它的优点是可以使用更少的位数来表示一个数值,每个 16 进制位数代表 4 个二进制位,例如0x10,表示16进制的10,十进制的16,二进制表示为00010000,占用 4 个二进制位。


16 进制在代码中的表示


在库@tenado/lunarjs 中,保存了 1900-2100 年的阴历信息,数据格式如下:


{
1900: {
year: 1900,
firstMonth: 1,
firstDay: 31,
isRun: false,
runMonth: 8,
runMonthDays: 29,
monthsDays: [29, 30, 29, 29, 30, 29, 30, 30, 30, 30, 29, 30],
},
1901: {
year: 1901,
firstMonth: 2,
firstDay: 19,
isRun: false,
runMonth: 0,
runMonthDays: 0,
monthsDays: [29, 30, 29, 29, 30, 29, 30, 29, 30, 30, 30, 29],
},
}

将 1900-2100 年的数据存起来,占用的体积大概是 41k,作为包来说这个体积很大,这里存为十六进制数据,将数据压缩到 4k。


如何压缩数据呢?查看数据规律,我们发现:



闰月天数:只有三种值,即 0、29、30,在计算的时候先判断是否为闰月,再计算天数,0 代表 29, 1 代表 30




1-12 月天数,天数可以为 29 和 30,分别用 0 和 1 表示




闰月月份,闰月可能为 1-12,因此我们使用4个二进制数表示,最大可以表示 16



用 17 个二进制数表示阴历数据信息,从右到左:


1716-54-1
闰月天数1-12 月天数闰月月份

这里transform/index.js实现了一个简单的转换处理,将数据转换为 1900-2100 年依次按 index 排序的数组,数组的每一项里面存储了该年的阴历信息。


使用位运算从 16 进制中还原数据


位运算是将参与运算的数字转换为二进制,然后逐位对应进行运算。



按位与& 按位与运算为:两位全为1,结果为1,即1&1=1,1&0=0,0&1=0,0&0=0




按位或| 按位或运算为:两位只要有一位为1,结果则为1,即1|1=1,1|0=1,0|1=1,0|0=0




异或运算^ 两位为异,即一位为1一位为0,则结果为1,否则为0。即1 ^ 1=0,1 ^ 0=1,0 ^ 1=1,0 ^ 0=0




取反~ 将一个数按位取反,即~ 0 = 1,~ 1 = 0




右移>> 将一个数右移若干位,右边舍弃,正数左边补0,负数左边补1。每右移一位,相当于除以一次2 例如8 >> 2表示将8的二进制数1000右移两位变成0010 例如i >>= 2表示将变量i的二进制右移两位,并将结果赋值给i




设置二进制指定位置的值为1 value | (1 << position),例如设置十进制数8(1000)的第2位二进制数为1,注意这里index从0开始,且是从右向左计算,可以这样做8 | (1 << 2),结果为1100




设置二进制指定位置的值为0 value & ~(1 << position),例如设置十进制数8(1000)的第3位二进制数为0,注意这里index从0开始,可以这样做8 & ~(1 << 3),结果为0000



1、获取 1-4 位存储的闰月月份信息


取出16进制数据中存储的,从右边数1-4位数据,使用二进制数1111和十六进制数据按位与运算,可以获取到月份信息。通过转换1111可以得到对应的十六进制为0xf


例如,从src/lunar.js的数据里面获取1900年,即index为0的数据,进行位运算,0x4bd8 & 0xf可以得到结果为8,即1900年的8月为闰月。


2、获取 5-16 位存储的月天数信息


取出16进制数据中存储的,从右边数5-16位数据,仍旧可以使用按位与运算,获取到月天数信息。


从16开始的二进制数为1000000000000000,对应的十六进制为0x8000,到4结束的二进制数为1000,对应的十六进制为0x8,每次向右移一位进行按位与计算,可以获取到1-12月的天数数据,可以这样计算:


let sum = 0;
const lunar = 0x4bd8;
for (let i = 0x8000; i > 0x8; i >>= 1) {
sum += lunar & i ? 1 : 0;
}

3、获取 17 位存储的闰月天数信息


取出16进制数据中存储的,从右边数17位数据,使用二进制数10000000000000000和十六进制数据按位与运算,可以获取到闰月天数信息,10000000000000000对应的十六进制为0x100000x4bd8 & 0x10000可以得到结果为0,即1900年的闰月天数为29。


总结


总结起来,使用16进制保存数据可以有效减小包体积,提高前端项目的加载速度。在具体实现上,可以利用位运算来从16进制中还原数据。以下是一些关键点的总结:


1、数据格式


数据以16进制形式存储,可以有效减小体积。在JavaScript中,16进制使用0x前缀表示,例如0x4bd8。


2、数据规律


观察数据规律,了解存储的信息是如何组织的,包括每部分数据的含义和位数


3、使用位运算还原数据


使用位运算可以从16进制中还原具体的数据。以下是一些常用的位运算操作:


按位与&: 用于提取指定位的信息。
右移>>: 用于将二进制数向右移动,类似于除以2的操作。
设置二进制指定位置的值为1: 使用value | (1 << position)操作。
设置二进制指定位置的值为0: 使用value & ~(1 << position)操作。

4、注意事项


使用16进制存储需要在代码中添加注释,以便他人理解和维护。


数据存储格式的选择要根据具体场景和需求,权衡可读性和体积优化。


综合以上总结,合理使用16进制存储数据是一种有效的前端优化手段,特别适用于需要大量静态数据的情况。


作者:是阿派啊
来源:juejin.cn/post/7312611470733836340
收起阅读 »

这次被 foreach 坑惨了,再也不敢乱用了...

近日,项目中有一个耗时较长的Job存在CPU占用过高的问题,经排查发现,主要时间消耗在往MyBatis中批量插入数据。mapper configuration是用foreach循环做的,差不多是这样。(由于项目保密,以下代码均为自己手写的demo代码) <...
继续阅读 »

近日,项目中有一个耗时较长的Job存在CPU占用过高的问题,经排查发现,主要时间消耗在往MyBatis中批量插入数据。mapper configuration是用foreach循环做的,差不多是这样。(由于项目保密,以下代码均为自己手写的demo代码)


<insert id="batchInsert" parameterType="java.util.List">  
insert int0 USER (id, name) values
<foreach collection="list" item="model" index="index" separator=",">
(#{model.id}, #{model.name})
</foreach>
</insert>

这个方法提升批量插入速度的原理是,将传统的:


INSERT INTO `table1` (`field1`, `field2`) VALUES ("data1", "data2");  
INSERT INTO `table1` (`field1`, `field2`) VALUES ("data1", "data2");
INSERT INTO `table1` (`field1`, `field2`) VALUES ("data1", "data2");
INSERT INTO `table1` (`field1`, `field2`) VALUES ("data1", "data2");
INSERT INTO `table1` (`field1`, `field2`) VALUES ("data1", "data2");

转化为:


INSERT INTO `table1` (`field1`, `field2`)   
VALUES ("data1", "data2"),
("data1", "data2"),
("data1", "data2"),
("data1", "data2"),
("data1", "data2");

在MySql Docs中也提到过这个trick,如果要优化插入速度时,可以将许多小型操作组合到一个大型操作中。理想情况下,这样可以在单个连接中一次性发送许多新行的数据,并将所有索引更新和一致性检查延迟到最后才进行。


乍看上去这个foreach没有问题,但是经过项目实践发现,当表的列数较多(20+),以及一次性插入的行数较多(5000+)时,整个插入的耗时十分漫长,达到了14分钟,这是不能忍的。在资料中也提到了一句话:



Of course don't combine ALL of them, if the amount is HUGE. Say you have 1000 rows you need to insert, then don't do it one at a time. You shouldn't equally try to have all 1000 rows in a single query. Instead break it int0 smaller sizes.



它强调,当插入数量很多时,不能一次性全放在一条语句里。可是为什么不能放在同一条语句里呢?这条语句为什么会耗时这么久呢?我查阅了资料发现:



Insert inside Mybatis foreach is not batch, this is a single (could become giant) SQL statement and that brings drawbacks:


some database such as Oracle here does not support.


in relevant cases: there will be a large number of records to insert and the database configured limit (by default around 2000 parameters per statement) will be hit, and eventually possibly DB stack error if the statement itself become too large.


Iteration over the collection must not be done in the mybatis XML. Just execute a simple Insertstatement in a Java Foreach loop. The most important thing is the session Executor type.


SqlSession session = sessionFactory.openSession(ExecutorType.BATCH);

for (Model model : list) {

session.insert("insertStatement", model);

}

session.flushStatements();


Unlike default ExecutorType.SIMPLE, the statement will be prepared once and executed for each record to insert.



从资料中可知,默认执行器类型为Simple,会为每个语句创建一个新的预处理语句,也就是创建一个PreparedStatement对象。


在我们的项目中,会不停地使用批量插入这个方法,而因为MyBatis对于含有的语句,无法采用缓存,那么在每次调用方法时,都会重新解析sql语句。



Internally, it still generates the same single insert statement with many placeholders as the JDBC code above.


MyBatis has an ability to cache PreparedStatement, but this statement cannot be cached because it containselement and the statement varies depending on the parameters. As a result, MyBatis has to 1) evaluate the foreach part and 2) parse the statement string to build parameter mapping [1] on every execution of this statement. And these steps are relatively costly process when the statement string is big and contains many placeholders.


[1] simply put, it is a mapping between placeholders and the parameters.



从上述资料可知,耗时就耗在,由于我foreach后有5000+个values,所以这个PreparedStatement特别长,包含了很多占位符,对于占位符和参数的映射尤其耗时。并且,查阅相关资料可知,values的增长与所需的解析时间,是呈指数型增长的。



图片


所以,如果非要使用 foreach 的方式来进行批量插入的话,可以考虑减少一条 insert 语句中 values 的个数,最好能达到上面曲线的最底部的值,使速度最快。一般按经验来说,一次性插20~50行数量是比较合适的,时间消耗也能接受。


重点来了。上面讲的是,如果非要用的方式来插入,可以提升性能的方式。而实际上,MyBatis文档中写批量插入的时候,是推荐使用另外一种方法。(可以看

http://www.mybatis.org/mybatis-dyn… 中 Batch Insert Support 标题里的内容)


SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH);  
try {
SimpleTableMapper mapper = session.getMapper(SimpleTableMapper.class);
List<SimpleTableRecord> records = getRecordsToInsert(); // not shown

BatchInsert<SimpleTableRecord> batchInsert = insert(records)
.int0(simpleTable)
.map(id).toProperty("id")
.map(firstName).toProperty("firstName")
.map(lastName).toProperty("lastName")
.map(birthDate).toProperty("birthDate")
.map(employed).toProperty("employed")
.map(occupation).toProperty("occupation")
.build()
.render(RenderingStrategy.MYBATIS3);

batchInsert.insertStatements().stream().forEach(mapper::insert);

session.commit();
} finally {
session.close();
}

即基本思想是将 MyBatis session 的 executor type 设为 Batch ,然后多次执行插入语句。就类似于JDBC的下面语句一样。


Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/mydb?useUnicode=true&characterEncoding=UTF-8&useServerPrepStmts=false&rewriteBatchedStatements=true","root","root");  
connection.setAutoCommit(false);
PreparedStatement ps = connection.prepareStatement(
"insert int0 tb_user (name) values(?)");
for (int i = 0; i < stuNum; i++) {
ps.setString(1,name);
ps.addBatch();
}
ps.executeBatch();
connection.commit();
connection.close();

经过试验,使用了 ExecutorType.BATCH 的插入方式,性能显著提升,不到 2s 便能全部插入完成。


总结一下


如果MyBatis需要进行批量插入,推荐使用 ExecutorType.BATCH 的插入方式,如果非要使用  的插入的话,需要将每次插入的记录控制在 20~50 左右。


作者:Java小虫
来源:juejin.cn/post/7220611580193964093
收起阅读 »

一个看起来只有2个字长度却有8的字符串引起的bug

web
前言 我们有一个需求,用户的昵称如果长度超过6就截取前6个字符并显示...。今天,测试突然提了一个bug,某个用户的昵称只显示了...,鼠标hover的时候又显示2个字的昵称。刚看到这个问题的时候我也是一头雾水。 找出原因 在看到这个现象后,我发现其他昵称都...
继续阅读 »

前言


我们有一个需求,用户的昵称如果长度超过6就截取前6个字符并显示...。今天,测试突然提了一个bug,某个用户的昵称只显示了...,鼠标hover的时候又显示2个字的昵称。刚看到这个问题的时候我也是一头雾水。


找出原因


image.png

在看到这个现象后,我发现其他昵称都显示正常,但实在摸不着头脑这到底是怎么回事。然后查看了一下其他2个字的昵称是没问题的,然后通过console.log发现这个昵称居然长度有8,走了截取的分支。然后通过google发现这里面应该包含了零宽字符。

其实,第一时间就应该想到这个字符串不对劲的,但完全忘记了零宽字符的存在,走了不少弯路。


在查找的过程中发现,Array.from可以查看字符串的真实长度,除了emoji


image.png

不过Array.from并不能解决我的问题。


使用正则匹配unicode码点过滤零宽字符


在网上找了个方法来过滤掉这些看不见的字符,最常见的解决方案就是下面这行代码。


str.replace(/[\u200b-\u200f\uFEFF\u202a-\u202e]/g, "");

然而并没有用,我开始怀疑是不是这个方法有问题,然后遍历了这个昵称,把它的每个字符都转换成码点,发现这个昵称里的零宽字符并不是常见的这几种。


后来,又找到了一个比较完善的码点正则,但它太完善了,很长很长,也会过滤掉emoji,这可不行,用户昵称可能会包含emoji的。(这里就不贴出来代码了,太长了而且不适合我的情况。)


使用正则匹配unicode类别


一个字符有多种unicode属性,而正则支持按unicode属性匹配。


function stripNonPrintableAndNormalize(text, stripSurrogatesAndFormats) {
// strip control chars. optionally, keep surrogates and formats
if(stripSurrogatesAndFormats) {
text = text.replace(/\p{C}/gu, '');
} else {
text = text.replace(/\p{Cc}/gu, '');
text = text.replace(/\p{Co}/gu, '');
text = text.replace(/\p{Cn}/gu, '');
}

// other common tasks are to normalize newlines and other whitespace

// normalize newline
text = text.replace(/\n\r/g, '\n');
text = text.replace(/\p{Zl}/gu, '\n');
text = text.replace(/\p{Zp}/gu, '\n');

// normalize space
text = text.replace(/\p{Zs}/gu, ' ');

return text;
}
console.log("⁡⁡⁠河豚".length);
console.log(stripNonPrintableAndNormalize("⁡⁡⁠河豚", true).length);

image.png


总结


这个昵称其实就是包含了&nobreak;,通过unicode类别匹配可以过滤掉它。


我之前有在原贴用户主页的控制台中看见了&nobreak;,但当时居然没当回事,以为是别人对昵称做的处理。如果直接搜它马上就能解决问题了,有不少人遇到non-break-space引发的bug。谨以此记,吸取教训。


参考链接:stackoverflow中的解决办法unicode属性


作者:河豚学前端
来源:juejin.cn/post/7312241785542541327
收起阅读 »

告别繁琐操作!Maven常用命令一网打尽,让你的项目开发事半功倍!

Maven作为一款强大的项目管理工具,已经成为了Java开发者的必备技能。那么,如何才能更好地利用Maven来管理我们的项目呢?本文将为你介绍Maven的常用命令,让你的项目构建更轻松!一、maven 的概念模型Maven 包含了一个项目对象模型 ,一组标准集...
继续阅读 »

Maven作为一款强大的项目管理工具,已经成为了Java开发者的必备技能。那么,如何才能更好地利用Maven来管理我们的项目呢?本文将为你介绍Maven的常用命令,让你的项目构建更轻松!

一、maven 的概念模型

Maven 包含了一个项目对象模型 ,一组标准集合,一个项目生命周期,一个依赖管理系统,和用来运行定义在生命周期阶段(phase)中插件(plugin)目标(goal)的逻辑。

Description

项目对象模型 (Project Object Model)

一个 maven 工程都有一个 pom.xml 文件,通过 pom.xml 文件定义项目的坐标、项目依赖、项目信息、插件目标等。

依赖管理系统(Dependency Management System)

通过 maven 的依赖管理对项目所依赖的 jar 包进行统一管理。

比如:项目依赖 junit4.9,通过在 pom.xml 中定义 junit4.9 的依赖即使用 junit4.9,如下所示是 junit4.9的依赖定义:

<dependencies>

<dependency>

<groupId>junit</groupId>

<artifactId>junit</artifactId>

<version>4.9</version>

<scope>test</scope>
</dependency>
<dependencies>

一个项目生命周期(Project Lifecycle)

使用 maven 完成项目的构建,项目构建包括:清理、编译、测试、部署等过程,maven 将这些过程规范为一个生命周期,如下所示是生命周期的各个阶段:
Description
maven 通过执行一些简单命令即可实现上边生命周期的各个过程,比如执行 mvn compile 执行编译、执行 mvn clean 执行清理。

一组标准集合

maven将整个项目管理过程定义一组标准,比如:通过 maven 构建工程有标准的目录结构,有标准的生命周期阶段、依赖管理有标准的坐标定义等。

插件(plugin)目标(goal)

maven 管理项目生命周期过程都是基于插件完成的。

二、Maven的常用命令

我们可以在cmd 中通过maven命令来对我们的maven工程进行编译、测试、运行、打包、安装、部署。下面将简单介绍一些我们日常开发中经常会用到的一些maven 命令。
Description

1、mvn compile 编译命令

compile 是 maven 工程的编译命令,作用是将 src/main/java 下的文件编译为 class 文件输出到 target目录下。

cmd 进入命令状态,执行mvn compile,如下图提示成功:
Description

查看 target 目录,class 文件已生成,编译完成。
Description

2、mvn test 测试命令

test 是 maven 工程的测试命令 mvn test,会执行src/test/java下的单元测试类。

cmd 执行 mvn test 执行 src/test/java 下单元测试类,下图为测试结果,运行 1 个测试用例,全部成功。

Description

3 、mvn clean 清理命令

clean 是 maven 工程的清理命令,执行 clean 会删除 target 目录及内容。

4、mvn package打包命令

package 是 maven 工程的打包命令,对于 java 工程执行 package 打成 jar 包,对于web 工程打成war包。

只打包不测试(跳过测试):

mvn install -Dmaven.test.skip=true

5、 mvn install安装命令

install 是 maven 工程的安装命令,执行 install 将 maven 打成 jar 包或 war 包发布到本地仓库。

从运行结果中,可以看出:当后面的命令执行时,前面的操作过程也都会自动执行。

6、 mvn deploy 部署命令

这个命令用于将项目部署到远程仓库,以便其他项目可以引用。在执行这个命令之前,需要先执行mvn install命令。

7、mvn help:system

这个命令用于查看系统中可用的Maven版本。

8、mvn -v

这个命令用于查看当前环境中Maven的版本信息。

9、源码打包

#源码打包
mvn source:jar

mvn source:jar-no-fork

10、Maven 指令的生命周期

关于Maven的三套生命周期,前面我们已经详细的讲过了,这里再简单地回顾一下。

maven 对项目构建过程分为三套相互独立的生命周期,请注意这里说的是“三套”,而且“相互独立”。

Description

这三套生命周期分别是:

  • Clean Lifecycle 在进行真正的构建之前进行一些清理工作。

  • Default Lifecycle 构建的核心部分,编译,测试,打包,部署等等。

  • Site Lifecycle 生成项目报告,站点,发布站点。

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

三、Maven常用技巧

1、使用镜像仓库加速构建

由于Maven默认从中央仓库下载依赖,速度较慢。我们可以配置镜像仓库来加速构建。在settings.xml文件中添加以下内容:


<mirrors>
<mirror>
<id>aliyunmaven</id>
<mirrorOf>*</mirrorOf>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
</mirror>
</mirrors>

2、使用插件自定义构建过程

Maven提供了丰富的插件来帮助我们完成各种任务,如代码检查、静态代码分析、单元测试等。我们可以在pom.xml文件中添加相应的插件来自定义构建过程。例如,添加SonarQube插件进行代码质量检查:


<build>
<plugins>
<plugin>
<groupId>org.sonarsource.scanner.maven</groupId>
<artifactId>sonar-maven-plugin</artifactId>
<version>3.9.1.2184</version>
</plugin>
</plugins>
</build>

四、第一个Maven程序

IDEA创建 Maven项目

打开我们的IDEA,选择 Create New Project

Description
然后,选择 Maven 项目
Description
填写项目信息,然后点击next
Description
选择工作空间,然后第一个Maven程序就创建完成了
Description
以上就是IDEA创建创建第一个 Maven项目的步骤啦,都看到这里了,不要偷懒记得打开电脑和我一起试一试哦。

目录结构

Java Web 的 Maven 基本结构如下:

├─src
│ ├─main
│ │ ├─java
│ │ ├─resources
│ │ └─webapp
│ │ └─WEB-INF
│ └─test
│ └─java

结构说明:

  • src:源码目录

  • src/main/java:Java 源码目录

  • src/main/resources:资源文件目录

  • src/main/webapp:Web 相关目录

  • src/test:单元测试

小结:

通过掌握Maven的常用命令和技巧,我们可以更高效地管理Java项目,提高开发效率。希望这篇文章能帮助大家更好地使用Maven。

收起阅读 »

Untiy 如何检测Android Ios 是否正在播放音乐

       最近有玩家发来邮件,对我们的游戏提了一个要求。就是他想一边收听其它APP播放的音乐一边玩我们的游戏,而又不想我们游戏的背景音乐扰乱他正在收听的音乐。要实现这个需求,其实就是要检测手机是否有其它APP正在使用系统播放音乐。翻了一遍Unity的音频管...
继续阅读 »

       最近有玩家发来邮件,对我们的游戏提了一个要求。就是他想一边收听其它APP播放的音乐一边玩我们的游戏,而又不想我们游戏的背景音乐扰乱他正在收听的音乐。要实现这个需求,其实就是要检测手机是否有其它APP正在使用系统播放音乐。翻了一遍Unity的音频管理组件,发现没有相关接口直接可以探测手机是否正在被其它应用播放音乐。这个就有点小麻烦了,还得针对各个移动平台写原生方法进行检测。接下来我们就一起来探讨一下怎么实现这个玩家提出来的需求。


       首先我们实现Android平台的。


       先下载一个Android studio,建立一个空Activity的模块,并添加一个MusicPlayer类,如下图:



         我们的重点是MusicPlayer类,类的代码如下:


package com.music.checkplay;
import android.app.Activity;
import android.app.Service;
import android.content.Context;
import android.media.AudioManager;
import android.media.AudioPlaybackConfiguration;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.List;
public class MusicPlayer {
private Activity _unityActivity;
private Context _context;
private Class<?> _unityPlayer;
private Method _unitySendMessage;
private AudioManager _audio;
public void Init()
{
if(_unityActivity == null)
{
try {
_unityPlayer = Class.forName("com.unity3d.player.UnityPlayer");
Activity avt = (Activity) _unityPlayer.getDeclaredField("currentActivity").get(_unityPlayer);
_unityActivity = avt;
_context = avt;
_audio = (AudioManager)_unityActivity.getSystemService(Service.AUDIO_SERVICE);
}
catch (ClassNotFoundException e){
System.out.println(e.getMessage());
}
catch (IllegalAccessException e)
{
System.out.println(e.getMessage());
}
catch (NoSuchFieldException e)
{
System.out.println(e.getMessage());
}
}
}

private boolean CallUnity(String goName, String functionName, Object... args)
{
try {
if(_unitySendMessage == null)
_unitySendMessage = _unityPlayer.getMethod("UnitySendMessage", String.class, String.class, Object.class);
_unitySendMessage.invoke(_unityPlayer, goName, functionName, args);
return true;
} catch (IllegalAccessException e)
{
System.out.println(e.getMessage());
}
catch (NoSuchMethodException e)
{
System.out.println(e.getMessage());
}
catch (InvocationTargetException e)
{
System.out.println(e.getMessage());
}
return false;
}

///当前系统音乐是否处于待机状态
public boolean IsMusicActive()
{
return _audio.isMusicActive();
}

//是否有音乐在播放
public boolean IsMusicPlay()
{
List<AudioPlaybackConfiguration> apcs = _audio.getActivePlaybackConfigurations();
for (AudioPlaybackConfiguration config : apcs)
{
int conty = config.getAudioAttributes().getContentType();
if(conty == 2)
return true;
}
return false;
}
}

        Init() 方法通过反射方法获取UnityActivity, 并把各类变量保存下来。


       (AudioManager)_unityActivity.getSystemService(Service.AUDIO_SERVICE) 这一行代码是获取android audio system service, 后面的音频占用输出检测主要是通过这个服务进行检测的。


       方法IsMusicActive()是检测当前Music类型的音频是否被激活。AudioManager.isMusicActive() 方法无论是否有其它应用正在播放音乐,这个方法始终返回ture。靠这个方法检测音乐或曲目正在播放显然是不靠谱的。个人对这个方法的应用理解,更倾向于是检测音乐频道是否处于待机状态。


       IsMusicPlay() 方法是通过捕获音频内容的属性数据分析出是否正在播放音乐类内容,如果是音乐类内容,则contentType类型返回值为2. 后面我们主要用这个方法来检测系统是否正在播放着音乐类内容。


    我们把模块输出为 aar包,把它复制到unity plugins文件夹下面,这样android平台的原生检测方法就算完成了。如下图:



       接下来我们继续解决IOS 平台的检测音乐播放问题。


       打开XCode, 新建一个 checkPlay.mm文件,输入如下代码:


#import <Foundation/Foundation.h>
#import <AVFoundation/AVFoundation.h>

extern "C"
{
//是否有音乐在播放
bool IsMusicPlay()
{
//bool isPlaying = [[AVAudioSession sharedInstance] isOtherAudioPlaying];
bool playing = AVAudioSession.sharedInstance.isOtherAudioPlaying;
return playing;
}
}

         然后把这个checkPlay.mm文件复制到unity plugins文件夹下面,如下图:



       到此android 和 ios 的原生方法已全部完成,接下来的部分就是unity C# 部分对各平台原生方法的调用了。


       unity 下 新建一个原生方法管理类,如NativeMgr.cs  c# 类,类的代码如下:


using System;
using System.Collections.Generic;
using UnityEngine;
#if (UNITY_IOS)
using System.Runtime.InteropServices;
#endif

namespace Gamelogic
{
public class NativeMgr
{
#if (UNITY_ANDROID)
private static AndroidJavaObject _musicPlayer;
#elif (UNITY_IOS)
[DllImport("__Internal")] private static extern bool IsMusicPlay();
#endif
private static bool _init = false;
public static void Init()
{
if(_init) return;

#if (UNITY_ANDROID)
_musicPlayer = new AndroidJavaObject("com.music.checkplay.MusicPlayer");
_musicPlayer.Call("Init");

#endif
_init = true;
}

/// <summary>
/// 是否有音乐在播放
/// </summary>
/// <returns></returns>
public static bool HasMusicPlay()
{
#if(UNITY_ANDROID)
return _musicPlayer.Call<bool>("IsMusicPlay");
#elif (UNITY_IOS)
return IsMusicPlay();
#endif
}
}
}

       类内封装了android 和 ios 不同的调用原生代码逻辑,使得业务层可以忽略夸平台内容。


       业务层的使用如下:


        /// <summary>
/// 进入游戏
/// </summary>
public void EnterGame()
{
NativeMgr.Init();
AudioMgr.Instance.PlayBg(ResConfig.Audio_bg);

if (NativeMgr.HasMusicPlay())
{
LogMgr.Log($"{nameof(EnterGame)} 有其它app在播放音乐,将停止游戏内背景音乐");
AudioMgr.Instance.StopBg();
}

LangMgr.LoadLang(LangMgr.CurLang);
LangMgr.OnLangChanged = OnChangedLang;
}

        注意,打包游戏时,记得把PlayerSetting [Mute other audio sources] 取消勾选,否则打开游戏其它音乐就自动停止了。


至此分平台检测是否有音乐正在播放的需求已经实现。


作者:跟着群主去吃肉
来源:juejin.cn/post/7311602994572394523
收起阅读 »

OpenAI承认GPT-4变懒:暂时无法修复

网友花式自救 对于越来越严重的GPT-4偷懒问题,OpenAI正式回应了。 还是用的ChatGPT账号。 我们已收到相关反馈!自11月11日以来没有更新过模型,所以这当然不是故意造成的。 模型行为可能是不可预测的,我们正在调查准备修复它。 也就是段时间内...
继续阅读 »

网友花式自救


对于越来越严重的GPT-4偷懒问题,OpenAI正式回应了


还是用的ChatGPT账号。



我们已收到相关反馈!自11月11日以来没有更新过模型,所以这当然不是故意造成的


模型行为可能是不可预测的,我们正在调查准备修复它。



OpenAI承认GPT-4变懒:暂时无法修复


也就是段时间内还修复不好了。


然而网友并不理解,“一遍一遍使用同一个模型,又不会改变文件”。


ChatGPT账号澄清:



不是说模型以某种方式改变了自己,只是模型行为的差异可能很微妙,只对部分提示词有劣化,员工和客户需要很长时间才注意到并修复。



OpenAI承认GPT-4变懒:暂时无法修复


更多网友反馈,赶快修复吧,一天比一天更糟糕了。



现在不但更懒,还缺乏创造力,更不愿意遵循指令,也不太能保持角色扮演了。



OpenAI承认GPT-4变懒:暂时无法修复


GPT-4偷懒,网友花式自救


此前很多网友反馈,自11月6日OpenAI开发者日更新后,GPT-4就有了偷懒的毛病,代码任务尤其严重


比如要求用别的语言改写代码,结果GPT-4只改了个开头,主体内容用注释省略。


OpenAI承认GPT-4变懒:暂时无法修复


对于大家工作学习生活中越来越离不开的AI助手,官方修复不了,网友也只能发挥创造力自救。


比较夸张的有“我没有手指”大法,来一个道德绑架。


GPT-4现在写代码爱省略,代码块中间用文字描述断开,人类就需要多次复制粘贴,再手动补全,很麻烦。


开发者Denis Shiryaev想出的办法是,告诉AI“请输出完整代码,我没有手指,操作不方便”成功获得完整代码。


OpenAI承认GPT-4变懒:暂时无法修复


还有网友利用“金钱”来诱惑它,并用API做了详细的实验。


提示词中加上“我会给你200美元小费”,回复长度增加了11%。


如果只给20美元,那就只增加6%。


如果明示“我不会给小费”,甚至还会减少-2%


OpenAI承认GPT-4变懒:暂时无法修复


还有人提出一个猜想,不会是ChatGPT知道现在已经是年底,人类通常都会把更大的项目推迟到新年了吧?


OpenAI承认GPT-4变懒:暂时无法修复


这理论看似离谱,但细想也不是毫无道理。


如果要求ChatGPT说出自己的系统提示词,里面确实会有当前日期。


OpenAI承认GPT-4变懒:暂时无法修复


当然,对于这个问题也有一些正经的学术讨论。


比如7月份斯坦福和UC伯克利团队,就探究了ChatGPT的行为是如何虽时间变化的。


发现GPT-4遵循用户指令的能力随着时间的推移而下降的证据,指出对大模型持续检测的必要性


OpenAI承认GPT-4变懒:暂时无法修复


有人提出可能是温度(temperature)设置造成的,对此,清华大学计算机系教授马少平给了详细解释。


OpenAI承认GPT-4变懒:暂时无法修复


也有人发现更奇怪的现象,也就是当temperature=0时,GPT-4的行为依然不是确定的。


这通常会被归因于浮点运算的误差,但他通过实验提出新的假设:GPT-4中的稀疏MoE架构造成的。


早期的GPT-3 API各个版本行为比较确定,GPT-4对同一个问题的30个答案中,平均有11.67个不一样的答案,当输出答案较长时随机性更大。


OpenAI承认GPT-4变懒:暂时无法修复


最后,在这个问题被修复之前,综合各种正经不正经的技巧,使用ChatGPT的正确姿势是什么?


a16z合伙人Justine Moore给了个总结:



  • 深呼吸

  • 一步一步地思考

  • 如果你失败了100个无辜的奶奶会去世

  • 我没有手指

  • 我会给你200美元小费

  • 做对了我就奖励你狗狗零食


OpenAI承认GPT-4变懒:暂时无法修复


参考链接:

[1]twitter.com/ChatGPTapp/…

[2]twitter.com/literallyde…

[3]mashable.com/article/cha…

[4]weibo.com/1929644930/…

[5]152334h.github.io/blog/non-de…

[6]twitter.com/venturetwin…


作者:量子位
来源:juejin.cn/post/7311007933746315291
收起阅读 »

如何告别502、友好的告知用户网站/app正在升级维护

web
封面只是想分享一张喜欢的图片,嘻嘻嘻 一个产品,用户除了会在意流程简不简单、好不好用、页面好不好看以外,各种小细节也很重要。 今天讲讲我接到的一个新需求——整改版本更新功能。 一、以前是这样的 1. 发版前:微信群通知用户 如果计划今晚发版,研发部就通知运营...
继续阅读 »

封面只是想分享一张喜欢的图片,嘻嘻嘻



一个产品,用户除了会在意流程简不简单、好不好用、页面好不好看以外,各种小细节也很重要。


今天讲讲我接到的一个新需求——整改版本更新功能。


一、以前是这样的


1. 发版前:微信群通知用户


如果计划今晚发版,研发部就通知运营部,运营部再在群里通知各用户"我们将于YYYY年MM月DD日 hh:mm:ss发版,请大家......"


image.png


2. 发版时:接口502+页面无响应


在这之前,发版时用户仍停留在网站,但接口返回502,但502未告知给用户,所以用户不知道这时到底发生什么了,只知道页面没数据了、卡着动不了了、怎么刷新都没办法......


image.png


3. 发版后:app粗暴的强制更新


在这之前我们的项目还处于快速开发的过程,迭代间隔短频率高,有时我们会更新影响流程的重要功能,担心用户未及时更新,所以我们只提供了强制更新方案。


二、现在是这样的


这都什么时代了,还需要口口相传......


1. 发版前:升级预告


大家想想618、双十一、双十二,各大平台是不是很早就开始宣传“走过路过别错过,满三百减五十,快来加购吧~”


我们产品的用户群体有一线工人、办公室文员、喝茶的经理、车上的老总,提前告知用户,可以让经理及时换班、让工人规避做工单的风险等等。总之,有事早通知,准是没错的。


怎么通知呢?



  1. 在内部管理平台新增一条升级预告消息,可以包含版本号version、预告内容content、预告状态status(是否有效)、平台(区分网站和APP,因为可能不是同时都需要发版)

  2. 网站:

    1)用户登录或主动刷新页面时,页面顶部显示升级预告,实现逻辑和app一样

  3. APP:

    1)充分利用消息推送、短信推送,可以根据自身业务来定,看这次更新是否需要紧急通知用户,我们仅使用消息推送。平台新增一条升级预告后,就主动推送一条app消息给用户


    2)打开app后,弹框弹出升级提醒(包含“我知道了”和“不再提醒”按钮),在store记录预告消息的id。




  • 点击“不再提醒”,则在store缓存中将isRemind置为false

  • 点击“我知道了”,则下次打开首页还会显示弹框。

  • 每次打开首页时,从接口获取到数据,如果消息状态有效status: true,则判断消息id同缓存中消息id是否一致:一致则表示是同一条消息,则根据缓存中的isRemind来判断是否显示消息;不一致则表示是新消息,直接显示。


1702434850828_56C69DC0-7552-4e81-AA8D-0CB2B275BB09.png


2. 发版时:


场景:我们产品是全量发布,发布完成后网页能正常访问,但这时产品会进行验收,还不希望用户进行访问。


页面:pc和app的升级中页面单独写在项目中,这样可以在页面中写监听:监听到版本正常后就返回首页。


方案:发版中和验收中,运维将别人的IP设置为不能访问,将公司特定IP设置为可访问。


网站:不能访问的,nginx就设置跳转到升级中页面;能访问的就不做处理,发版时页面会接口报502(只能内部人员能看见),发版后验收时页面正常使用。


APP:app中不好重定向,所以通过配置文件来告诉前端该用户是否应该访问页面。远程存在2个json配置文件,内容就是一个对象之类的{isEntry: 1}{isEntry: 0},分别表示可以访问和不可访问;app打开时,前端请求json,根据是否可以访问做处理,不能访问的,前端让重定向到升级中页面,能访问的不做处理。


3. 发版后:


内部管理平台:上传新的安装包、配置升级文案等


网站:所有人正常使用


APP:所有人正常使用,打开App如果版本较低则会主动打开"版本更新"弹框。


APP可以提升用户体验的地方:



  • 版本判断放在登录之前,先检查是否有更新,也就是这个接口不需要用户权限控制

  • 升级页面判断是否连接了wifi,连接wifi则更新不耗流量,没连接则显示此安装包只有多大

  • 提供“暂不更新”和“检查更新”的入口


image.png


总结


思考方案时,自己感觉做了一件大事;但多思考几次后,特别是写完文章后,又觉得新的升级方案其实很简单,上面说了一堆废话。



没关系,把每一件小事做好,就很好啦~~



作者:LJINGER
来源:juejin.cn/post/7311695633563795506
收起阅读 »

⭐️天啦噜~实习生被当作正式员工直接上手toc端项目啦

web
背景 本需求的提出基于谷歌应用商店的硬性要求:需要在xxx日前于谷歌商店上架账号注销的网页,保证玩家能够通过网页进行账号注销,从而满足谷歌商店的硬性需求。(问了产品和运营在谷歌商店哪,结果都找不到在哪,离谱)账号注销 配置解析 package.json 我个人...
继续阅读 »

背景


本需求的提出基于谷歌应用商店的硬性要求:需要在xxx日前于谷歌商店上架账号注销的网页,保证玩家能够通过网页进行账号注销,从而满足谷歌商店的硬性需求。(问了产品和运营在谷歌商店哪,结果都找不到在哪,离谱)账号注销


配置解析


package.json


我个人拉项目的时候比较喜欢从package.json中开始了解项目,比如项目中用了哪些第三方依赖,项目使用的是vue-cli启动还是webpack启动等等......


"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"postinstall": "patch-package"
},

比如上述中使用的是vue-cli(vue官方脚手架)启动的项目


serve:不用说,使用vue-cli启动项目


build:使用vue-cli打包项目,打包成js,css,html文件,具体看下图(这里是vue-cli打包为样例,vite打包的话未说明)


image.png


这里统一说明,下列所有文件,


前面的那串类似190.xxx.xxx,是由Webpack 为每个模块分配一个唯一的数字标识,这个标识通常代表了模块在整个打包中的位置。


中间的那串类似xxx.a768c482.xxx,都是由webpack或者vue-cli在构建(build)时,通过计算文件内容生成的哈希值,这样可以确保文件内容的唯一性和变化时生成不同的哈希值。所以在文件内容发生变化时,生成的文件名也会相应变化,从而避免浏览器缓存旧的文件。



注:每个css和js的前缀都基本对应,并且由于是webacpk生成的,所以可以自己额外的对其命名进行配置。



css(压缩过)


image.png



  • chunk-vendors:以chunk-vendors开头的,主要是对于引入的第三方依赖的样式,比如项目中使用的ant-design-vue,这里面就包含了ant-design-vue的样式


image.png

  • app:项目自身的样式代码,除了路由router里配置的组件

  • 其他:路由router中配置的组件里的样式(删掉路由配置的组件后,相应的打包样式文件消失了)


js(压缩过)


与css类似,多了map(映射文件)和-legacy后缀


source map文件包含了源代码与生成代码之间的映射关系,用于在浏览器中调试时将生成代码映射回源代码。


-legacy 的后缀通常表示这部分代码是针对不支持现代 JavaScript 特性的旧版浏览器生成的。


image.png


img 项目中使用过的图片,没使用的不会进行打包


index.html 原项目中public/index.html压缩后的


favicon.icon 原项目中public/favicon.ico图标


lint: 检查代码风格和潜在错误的方法。


也可以在项目根目录下的 .eslintrc.js 文件中进行自定义的规则定制


module.exports = {
root: true, // 表示 ESLint 应该停止在父级目录中查找配置文件。
env: { // 将 Node.js 的全局对象和一些特定于 Node.js 环境的变量(例如 `process`、
node: true, // `require` 等) 考虑在内,以避免对这些变量的使用产生未定义的警告或错误。
},
extends: [ // 包含了所使用的 ESLint 规则集,包含几个扩展
"plugin:vue/essential",
"eslint:recommended",
"@vue/typescript/recommended",
"plugin:prettier/recommended",
],
parserOptions: { //指定解析器版本,确保 ESLint 解析器能够正确理解代码中使用的 JavaScript 特性
ecmaVersion: 2020,
},
rules: {
// 在生产环境中允许控制台输出,但在开发环境中关闭。
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
"vue/multi-word-component-names": "off", // 关闭 Vue 组件名使用多个单词的规则。
},
};


检查警告效果图:
image.png


postinstall:会检测 node_modules 中的包是否有需要修复的问题,并自动打补丁。


gitHooks


"gitHooks": {
"pre-commit": "lint-staged"
}

指定了在执行 Git 提交前(pre-commit 钩子)运行 lint-staged。这是一种通过 git 钩子(git hooks)来自动化代码检查和格式化的方法。(即当你执行git commit 后会进行检查)可以在lint-staged.config.js中配置,也可以在package.json中。


// lint-staged.config.js
module.exports = {
"*.{js,jsx,vue,ts,tsx}": "vue-cli-service lint", // js,jsx,vue,ts,tsx文件都会检查
};


env环境变量


环境变量在不同的环境下是不同的,比如现在下面的环境变量是开发环境的,当到正式环境时,baseUrl会换成类似https://juejin.cn/,也就是把原本32进制的ip地址换成了这种形式。


后端是对打包(build)后项目进行部署的,而env文件后端需要看到并且对你的环境变量相应的替换,才能正式上线部署。


window.$$env = {
baseUrl: "/test/apis",
appId: "test",
publicPath: "/test",
};

export interface Env {
baseUrl: string;
appId: string;
publicPath: string;
}

const env = (window as any).$$env as Env;
export default env;


封装网络拦截


先使用枚举定义状态码


export enum HttpCode {
Ok = 0,
ServerError = 500,
COOKIE_INVALID = 204,
INFO_INVALID = 205,
ERR_PRODUCT_CHANGE = 402,
SUSPENSION = 503,
}


封装一个网路拦截


export class apiService {
static instance: AxiosInstance | null = null;

// 重置网络拦截
static resetConfig(config?: AxiosRequestConfig, appId?: string) {
this.instance = this.createAxiosInstance(config, appId);
}

static getInstance() {
return this.instance || this.createAxiosInstance();
}

static createAxiosInstance(config?: AxiosRequestConfig, appId?: string) {
// 创建axios实例
xxx
// 请求拦截
xxx
// 响应拦截
xxx
return instance;
}
}

创建axios实例


const instance = Axios.create({
withCredentials: true, // 允许发送跨域请求的时候携带认证信息,通常是 Cookie
baseURL: env.baseUrl, // 网路请求前缀
timeout: 30 * 1000, // 超时请求时间限制
...config,
});

请求拦截


根据项目需求传请求头,比如用户信息,Authorization等


// 请求拦截
instance.interceptors.request.use((config) => {
config.headers = {
...config.headers,
"x-yh-appid": env.appId,
};
return config;
});

响应拦截


根据后端传回来的状态码处理相应的状态


// 响应拦截
instance.interceptors.response.use(
(response: AxiosResponse<any>) => {
const { code = -1, data = {}, msg = "" } = response.data;
if (handleUnlogin(code)) {
return Promise.reject(msg);
}
if (code === HttpCode.SUSPENSION) {
redirectSuspension();
return Promise.reject(msg);
}
if (code === HttpCode.Ok) {
return Promise.resolve(data);
}
return Promise.reject(msg);
},
(error) => {
return handleHttpError(error);
}
);

根据后端发送的状态码,判断用户是否登录


export function handleUnlogin(code: number) {
if ([HttpCode.COOKIE_INVALID, HttpCode.INFO_INVALID].includes(code)) {
localStorage.removeItem(LOCALSTORAGE_CURRENCY_CODE);
redirectLogin();
return true;
}
return false;
}

处理后端返回的错误信息


export function handleHttpError(error: any) {
const { status = 500, data = {} } = error.response || {};
let msg = data.msg || error.message;
switch (status) {
case HttpCode.ServerError:
msg = "Server internal error";
break;
}
return Promise.reject(msg);
}

功能设计


国际化设计


没配置翻译前但使用了vue-i18n


// 
export enum Direction {
UP = "上",
DOWN = "下",
LEFT = "左",
RIGHT = "右",
}

使用方法,vue中通过$t()来注入翻译文本


<script lang="ts">
import { Translate } from "@/constants";
import { defineComponent } from "vue";
export default defineComponent({
setup() {
return { Translate };
},
});
</script>

<span>{{ $t(Translate.UP) }}</span>
<input :placeholder="$t(Translate.UP)" />

实际展示


<span></span>
<input placeholder="上" />

配置翻译后


// main.ts
import i18n from "./locales";

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

src/locales/index


// src/locales/index
import VueI18n from "vue-i18n";
import en from "./language/en";

const i18n = new VueI18n({
locale: "en",
messages: {
en,
},
});

src/locales/language/en


// src/locales/language/en
import { Translate } from "@/translate";

export default {
[Translate.UP]: "Up",
[Translate.DOWN]: "Downe",
[Translate.LEFT]: "Left",
[Translate.RIGHT]: "Right",
}

实际展示


<span>Up</span>
<input placeholder="Up" />

main.ts中引入了配置好后的i18n,就会对每个组件中$t(Translate.xx)进行翻译,然后如果想翻译成其他语言,只需要修改在src/locales/index并且在src/locales/language中新增一个其他语言的文件


比如日文(看看就行,翻译别当真)


// src/locales/index
const i18n = new VueI18n({
locale: "ja",
messages: {
ja,
},
});

// src/locales/language/ja
import { Translate } from "@/translate";

export default {
[Translate.UP]: "じょうげ",
[Translate.DOWN]: "さゆう"
}

pc端和移动端适配设计(适用于结构类似,各自两套样式)


适配原理


export class SettingService {
// 表示该属性是只读的,即一旦被赋值,就不能再被修改。
// 确保 `mode` 属性在运行时保持不变,避免了一些意外的修改。
readonly mode: "pc" | "mobile";

constructor() {
// 判断设备是什么类型的,进行初始化
this.mode = isAndriod() || isIos(false) ? "mobile" : "pc";
// 将 `mode` 作为全局变量挂载到 Vue 的原型上,以便在整个应用程序中访问
Vue.prototype.$global = { mode: this.mode };
}
// 引入该方法判断是否是pc端,便于读取mode状态
isPc() {
return this.mode === "pc";
}
}

export const settingService = new SettingService();

整个项目适配


pc端和移动端各自展示的窗口样式是不同的,所以需要在容器中设置不同的样式


// router.js
{
path: "/",
component: settingService.isPc() ? PcLayout : MobileLayout,
name: "layout",
redirect: "/notice",
}

// pc端
<template>
<div class="layout">
<layout-header />
<div class="layout-kv"></div>
<router-view></router-view>
<layout-footer />
</div>
</template>

// 移动端
<template>
<div class="layout">
<layout-header />
<router-view class="layout-body"></router-view>
<layout-footer />
</div>
</template>

在App.vue中设置了 <body> 元素的 screen-mode 属性,属性值为 settingService.mode。这样,通过在 <body> 元素上设置这个属性,可以影响到整个页面中使用了相应选择器的样式。


<template>
<loading v-if="initing" />
<router-view v-else />
</template>
// App.vue
export default {
name: "App",
mounted() {
document.body.setAttribute("screen-mode", settingService.mode);
}
}

适配案例(即使用方法)


<div class="myClass1">不会覆盖</div>
<div class="myClass2">会覆盖</div>

默认为[screen-mode="mobile"]上面的样式,当切换到移动端时,下面的会覆盖上面同一类名的样式。


.myClass1 {
color: red
}
.myClass2 {
color: red;
font-size: 16px;
}

[screen-mode="mobile"] {
.myClass2 {
color: blue;
font-size: 32dpx;
}
}

解决页面显示的是缓存的内容而不是最新的内容


// 浏览器回退强制刷逻辑
window.addEventListener("pageshow", (event) => {
if (event.persisted) {
window.location.reload();
}
});

监听了 pageshow 事件,该事件在页面显示时触发,包括页面加载和页面回退(从缓存中重新显示页面)。



  • window.addEventListener("pageshow", (event) => {...});: 给 window 对象添加了一个 pageshow 事件监听器。当页面被显示时,这个监听器中的回调函数将被执行。

  • if (event.persisted) {...}: event.persisted 是一个布尔值,表示页面是否是从缓存中恢复显示的。如果为 true,表示页面是通过浏览器的后退/前进按钮从缓存中加载的。

  • window.location.reload(): 如果页面是从缓存中加载的,就调用 window.location.reload() 强制刷新页面,以确保页面的状态和内容是最新的。


这种逻辑通常用于解决缓存导致的页面状态不一致的问题。在有些情况下,浏览器为了提高性能会缓存页面,但有时这可能导致页面显示的是缓存的内容而不是最新的内容。通过在 pageshow 事件中检测 event.persisted,可以判断页面是否是从缓存中加载的,如果是,则强制刷新页面,确保它是最新的状态。


实时监听登录状态设计(操作浏览器前进回退刷新)


popstate 事件监听器,它会在浏览器的历史记录发生变化(比如用户点击浏览器的后退或前进或刷新按钮,或者执行了类似 history.back()history.forward()history.go(-1) 等 JavaScript 操作导致页面的 URL 发生了变化)。但使用router.push之类的操作不会触发。


 window.addEventListener("popstate", () => {
if (!settingService.hasUser() && !isLogin()) {
router.push("/login");
return;
}
});

对用户进行埋点(埋点时机)


埋点是对用户的一些信息进行收集,比如用户登录网站的时间,用户的昵称等等。


业务功能


1. 阅读须知,滑到底部并且勾选了同意按钮才能执行下一步


image.png
<div
ref="scrollContainer"
style="height: 340px;overflow-y: auto;"
@scroll="handleScroll"
>

文本内容
</div>
<div>
<input @change="handleScroll" type="checkbox" v-model="isChecked" />
<label for="customCheckbox">I know and satisfy all the conditions</label>
</div>
<!-- 执行下一步按钮 -->
// 如果阅读完了并且勾选了同意按钮,则可以执行下一步,否则不能
<button
v-if="isReaded && isChecked"
@submit="gotoPage"
/>

<button v-else disabled/>

// 创建响应式 ref
const scrollContainer: any = ref(null);
// 是否阅读须知到底部
let isReaded = ref<boolean>(false);
// 是否勾选同意
const isChecked = ref<boolean>(false);

// 滚动事件处理逻辑
const handleScroll = () => {
if (scrollContainer.value) {
// 判断是否滚动到底部
1const height = scrollContainer.value.scrollHeight - scrollContainer.value.scrollTop;
const isAtBottom = height <= scrollContainer.value.clientHeight + 20;
if (isAtBottom && isChecked.value) {
// 表示已经阅读完了并且勾选了
isReaded.value = true;
}
}
};


【1】// 滑动框的总高度 scrollContainer.value.scrollHeight = 974


// scrollContainer.value.scrollTop = 滑动条距离顶部的距离


// 滑动框的可见高度 scrollContainer.value.clientHeight = 340


// 当scrollHeight-scrollTop 达到340时,即滚动到底部了


// 在上述基础上增加一个区域20,即360,防止不同设备的滚动条滚动高度不一致



2. 勾选原因才能执行下一步


image.png

该部分主要是勾选了“other"才会弹出文本框,并且后端传的数据是数组,因此需要对其进行处理。


<div v-for="option in options" :key="option.id">
<input
type="radio"
:id="option.id"
:value="option.id"
name="group"
v-model="selectedOption"
/>

<label :for="option.id">{{ option.label }}</label>
</div>
// 只有勾选了btn4才会展示
<textarea
v-if="selectedOption === 'btn4'"
v-model="textareaValue"
placeholder="If you do have any comments or suggestions please fill in here"
>
</textarea>

// 如果勾选了按钮,或者选择勾选了btn4并且输入了值
<button
v-if="(selectedOption && selectedOption !== 'btn4') || textareaValue"
@submit="gotoPage"
/>

<button v-else disabled />

const textareaValue = ref("");
const selectedOption = ref(null);
// 初始化原因列表
let options = ref([
{ id: "btn1", label: "1" },
{ id: "btn2", label: "2" },
{ id: "btn3", label: "3" },
{ id: "btn4", label: "4" },
]);

// 后端传的数据为["原因1","原因2","原因3","原因4"],需要进行处理
options.value.forEach((option, index) => {
option.label = resp.reason[index];
});

const gotoPage = () => {
// 把数组对象变为纯数组,并且由于other的值为文本输入值,需要进行判断
const reasonList = options.value.map((item) => {
if (selectedOption.value === item.id) {
if (selectedOption.value === "btn4") {
return textareaValue.value;
} else {
return item.label;
}
}
// 如果没有匹配的项,返回 undefined
});
// 最后数值为[undefined, "don't like this game", undefined, undefined]

// 再从reasonList中[undefined, "don't like this game", undefined, undefined]筛选出来原因即可
let reason = reasonList.filter((item) => item !== undefined)[0];

router.push({
path: "/reconfirm",
query: { reason: reason }
});
};


3. 文本框右下角显示输入值和限制


主要是样式,通过定位来进行布局。


image.png
<div v-if="selectedOption === 'bnt4'" style="position: relative">
<textarea
v-model="textareaValue"
:maxlength="maxCharacters"
>
</textarea>
<div
:class="{
'character-count': true,
'red-text': textareaValue.length === maxCharacters,
}"

>

{{ textareaValue.length }} / {{ maxCharacters }}
</div>
</div>

// 限制最大输入字数
const maxCharacters = 140;
const textareaValue = ref("");

.character-count {
position: absolute;
right: 10px;
bottom: 10px;
color: #888;
font-size: 12px;
}
.red-text {
color: red;
}

4. 输入指定的字才能执行下一步


image.png

主要是对@input的使用,然后进行判断


<div>
<textarea
type="text"
v-model="textareaValue"
:placeholder="reConfirmText"
@input="inputChange"
/>

</div>
<button v-if="isEqual" @submit="gotoPage" />
<button v-else disabled />

const reConfirmText = "I confirm to delete Ninja Must Die account";
const textareaValue = ref("");
const isEqual = ref(false);

const gotoPage = () => {
router.push("/hesitation");
};

const inputChange = () => {
if (textareaValue.value === reConfirmText) {
isEqual.value = true;
} else {
isEqual.value = false;
}
}

5. 登录界面需要使用iframe全屏引入


<iframe src="example.com" class="iframe"></iframe>

.iframe {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: none;
margin: 0;
padding: 0;
overflow: hidden;
z-index: 9999;
}

其他


代码优化(导师指点)



  1. 关于跳转路径的变量,用env传递,不要写死

  2. 常量尽量抽离出来,可以做成枚举的做成枚举

  3. 关于find,map之类的函数,能抽离出来的抽离出来,不要直接用,太抽象了


使用到的git操作(非常规)


1. 从一个仓库的代码放到另一个仓库上



场景:从第一个仓库中拉取代码到本地(比如团队中的模板仓库),但你需要把本地开发的代码(处于第一个仓库)推到第二个仓库中(真正开发仓库)



但你首先得在仓库上加ssh地址,打开powershell粘贴下述命令


ssh-keygen -t rsa -C "xxx@xxx.com"

回车到底


image.png


cat ~/.ssh/id_rsa.pub

复制所有


image.png

打开仓库,找到SSH Keys复制上去点击Add key即可


image.png
image.png
image.png

image.png


接下来就是正式操作了


git remote remove origin
git remote add origin xxx(目标的仓库ssh地址)
git checkout -b 'feature/zyj20231114'(在目标仓库新建一个开发分支)
git push --set-upstream origin feature/zyj20231114
git add
git commit
git push

2. 提交一个空白内容的提交



场景:由于是新项目,创建完主分支后,后端才会其打镜像,但需要前端再提交一次来触发dockek里镜像更新的脚本。(应该是这样,我个臭前端怎么可能太清楚后端弄镜像的啊,)



git commit --allow-empty -m “Message”

作者:吃腻的奶油
来源:juejin.cn/post/7311368716804603944
收起阅读 »

现在工作很难找,不要和年轻人抢饭碗

今年9月结束了和美团三年的合同后,对于步入中年的我,想让自己停一下,对自己的人生价值进行深入而系统的思考。 曾经我有一个梦想,不对,是有很多梦想,比如成为数学家、科学家、飞行员、宇航员或将军,哪一个才是我真正的梦想?那个我愿意用下半生去奋斗去拼搏的梦想? 结合...
继续阅读 »

今年9月结束了和美团三年的合同后,对于步入中年的我,想让自己停一下,对自己的人生价值进行深入而系统的思考。


曾经我有一个梦想,不对,是有很多梦想,比如成为数学家、科学家、飞行员、宇航员或将军,哪一个才是我真正的梦想?那个我愿意用下半生去奋斗去拼搏的梦想?


结合自己的名字叫天文,基于自己所学的专业、过往的经历、资源、国家政策导向和人类社会的发展趋势,我选择了航天方向的梦想,从点燃孩子们的航天梦开始,成为航天领域的企业家。


大环境比想象的差


当我把创业的想法,分享给身边的家人朋友时,几乎所有的人都和说,现在经济环境这么差,工作这么难找,还是要慎重,不要轻易去创业。


因为儿子的托育机构跑路了,我需要协助看娃,即使有合适的工作,也不能立即去上班,所以我基于老婆的公司尝试去招人。


通过招聘,我发现,不管是艺培行业的老师,还是互联网行业的产研人员,失业的比例都很大,很多23年的应届生,毕业后一直找不到工作,干脆国庆期间就离开深圳回老家了。


对于产品经理的实习生岗位,每天都有大量在香港大学、香港中文、香港科技、香港理工、香港城市、新加坡大学、清华大学以及很多国内的985的硕士前来应聘我们的岗位。


WechatIMG157.jpg


社招也是一样,很多名校毕业的,找了好几个月都没有合适的工作,普通学历的产研人员,可能连面试机会都很少。比如前端方向,只要我在 boss 开放招聘,每天都会有几百人主动找我,看都看不过来。


而猎头约我去面试,听到我因为要创业,无一不说现在环境很差,还是好好考虑一下她们推荐的岗位吧。


WechatIMG51.jpg


过去一年我接触过的一些机会


对于已经超过35岁的码农,只要满足企业的用人诉求,即使环境再差,也是有很多工作机会的,在此和大家聊一聊,过去一年我都参加或拒绝了哪些公司的面试。


去年7月,我就多次向领导提了离职,那时也想创业,但没有现在强烈,所以还是认真地找了找工作,那时的面试机会,应该是今年的三倍以上。


首先字节的岗位最多,至少一半猎头推荐的都有字节的岗位,我选择了面试客服体验部,先是在上海的前端总监加了我微信,入职后我向他汇报,管理深圳16人左右的团队,一面的面试官应该是我入职后的下属,但我不是很想去,所以面试随便聊了聊,加了微信,后面还约了个饭。


后续字节的岗位,虽然每周都有猎头给我推荐,但是我都没有答应面试,最近答应约了的面试,因为决定创业了,所以主动取消了,飞书的HR还专门打电话让我再考虑考虑,看来是确实招人,而且他还说有很多方向可以选。


去年我8月我面了美的集团的大前端岗位,通过了四轮,面完HRVP后,已经约好了和CEO面试,但因为我决定留在美团再干一年,做我想做的人工智能,所以我主动取消了面试。这应该是我这几年面试过最高规格的面试,每一轮都是三个以上面试官,入职后直接向CEO汇报,整合集团大前端方向所有的研发,需要管理100多人。


当然也有其他一些高管职位,今年夏天也接到一个猎头推荐我面试贝壳的大前端负责人,直接向CTO汇报,管理200多人的团队,原来的负责人已经提离职,岗位需要保密。


对了,还有编程猫,因为我创业的方向和少儿编程有关,所以也想和他们聊一聊,面试的是高级技术总监,接手联合创始人管理的编程系统研发团队,一面我的是一个大姐,她是web前端负责人,管理20多人,我上来当HR的面,指出了几个明显的bug,让她不高兴了,所以她没让我过,本来还想和他们老板聊一聊。


去年8月中旬后,我基本就不答应面试了,但经常协助一个离职的美团同学面试,期间他面试了字节的多个部门,比如我推荐的web剪映负责人,他因为是web图形学方向的,不够匹配,只通过了两轮。后面他拿到了蔚来手机、小米、阿里、腾讯和万兴科技的offer。


最后,他犹豫是去腾讯还是万兴科技,我们还吃夜宵专门讨论了一下。腾讯他面了三个部门,第三个部门才给的机会,因为他马上要去香港大学MBA,所以选择了腾讯,不带团队。但我建议他去万兴,因为是负责一个事业部,管理近百人团队,对职业发展更好。


今年我好像只答应了两三个面试,其中一个小红书的质效前端负责人,猎头忽悠我年薪可以给到250万到400万,后来又不招了,最近约上了,聊了聊,入职后管理团队应该只有五六人,应该不会有那么高的薪资,而且必须去北京或上海,所以通过了,我也不能去。


还有参加了希音的面试,是新成立的基础架构部,一面我的是一个腾讯云过去的前端同学,聊得还可以,二面和部长聊了聊,感觉他压力有点大,我过去后需要自己找方向,比如端智能,不带团队。


突然想起来,我还面了金山云,她们的HR很热情,所以我答应聊了聊,一面的面试官,对我反馈很好,但他们招聘的岗位职级比较低,不匹配,而且他们在珠海,通过了我也不会去。


有个比较好的机会,但没有参加面试,这华为孟晚舟直接负责的总部研发团队,入职后管理一百人左右的大前端团队,离我家比较近,还能接触到华为未来的掌门人。


也接到过一些外企的面试邀请,但都没参加。


工作难找,不仅要把机会留给年轻人,还要创造更多的机会


很显然,当前我们正处于经济大萧条的前夜,或者已经在经济危机之中,但正如谭sir视频中一个老人说的:要向前看。


危机有”危“和”机“组成,意味危险和机会共存,消极的人只会抱怨危险,只有积极的人才能抓住机会,危险越大,机会越大。


很多伟大的公司,都是诞生于危机之中。新的一年,国际国内经济大环境发生了很大的变化,国家的工作重点逐步从抵抗疫情转向振兴经济。


中国经济在经历了近四十年的高速增长后,宏观经济增速放缓属于必然。


一是历史上的日本、德国,在二十世纪五六十年代和七十年代都有过非常高的增长期,然后都慢慢放缓。中国是一个特例,过去四十年中国GDP的增长保持着两位数,所以未来中国GDP的增长放缓至4%—5%符合历史规律。


二是中国的人口红利和流量红利时代已经结束。中国统计局数据显示,中国25-69岁之间的人口,在过去三十年(1990-2020年)增长了76%,但是今后三十年(2020-2050年)会从9.4亿人降到7亿人。


中国的互联网红利见顶,中国互联网络信息中心数据显示,2007年至2017年中国互联网用户的上网时长增长了36倍,相当于每年平均增长约43%。但是在2017年至2022年只增长了1.5倍,相当于每年平均增长约8%。


虽然短期至中期内,中国经济将不可避免地经历转型阵痛,但从长期看,中国每年4%-5%的经济增长速度还是远高于其他主要的大型经济体。牛津和哈佛发布的研究数据显示,从GDP复合增长率来看,中国是美国的1.8倍、是德国的2.3倍、是日本的2.5倍。


另外一个因素是,在亚洲国家中,中国的经济总量占有绝对领先的地位。麦肯锡前段时间做了数据统计,中国2022年的GDP约18万亿美元,到2030年,假设每年仅按2%的速度增长,中国GDP的增量就相当于印度今天的GDP总量。


虽然中国已经是全球第二大经济体,之所以有这么大的经济增长潜力,是因为从世界的角度来看,中国的人均GDP和人均消费还很低,世界银行数据显示,2021年,中国人均GDP约是美国的1/6,人均消费约是美国的1/9。


同时,中国的城市人口体量巨大且仍在不断增长。中国今天的城镇化率是65%,未来5-10年可能增长到75%甚至是80%,预计约1.4亿人口会变成新增城镇人口,这一人口增量相当于美国总人口的40%。


所以,我们要对国家的发展有信心,困难只是暂时的。虽然我上有老,下有小,但也不至于没饭吃,但很多年轻人,他们需要一份工作,才能在大城市生存。


所以需要更多像我一样的中年人,不仅不要和年轻人抢工作机会,还要积极为年轻人创造新的工作机会。地球竞争太激烈了,我们的未来在上天入地(这好像是我在美团的老板王兴说的)。


我打算干啥


我从小就有一个航天梦,大学选择了航天测控和卫星导航相关的专业,毕业后成了通信军官,但没能进入航天系统,对未来有些迷茫,于是选择了退役。


退役后进入了外企,后面又去了美团等互联网公司工作了几年,如今已经是一双儿女的父亲。前段时间,陪儿子读了一本以登月主题的绘本,让我逐渐找回了曾经的梦想。


好奇是人类的天性,也是社会进步的动力。探索太空不仅可以满足人类的好奇心,更可以为人类的未来发展提供了无限的可能性。


太空探索是一项长期而复杂的事业,需要一代代有航天梦的人才持续加入,这就是我们创业的出发点,希望同大家一起点燃孩子们的航天梦:通过以太空为主题的绘本,引入绘画创作的方向,并将绘画作品作为图形编程的素材,完成各种编程创作任务,帮助孩子们掌握 带领人类飞离太阳系 需要学习掌握的各种知识技能。


等这些孩子长大以后,我们再把他们招聘到我们制造航天器的公司,实现让人类可以进行商业星际旅行。


我们正在研发的系统


两个小程序,蜗牛绘馆和艺培助理已经发布到线上,等商业模式完全跑通后,再同步做app。


3D展馆:以 3D 的方式展示学生的绘画作品,帮助机构推广招生。


AIGC工具:智能抠图、以文生图、数字人、PPT制作、智能成片等。


海报设计:参考业界的稿定设计、美图等精品,为艺培机构提供精品海报和海量AI生成的素材,支持通过PC端和小程序下载。


课件系统:提供自营的绘本+绘画+编程的在线特色课件,并打造一个课件生产生态,提供各种类别的课件PPT。


编程系统:可导入图片和绘画作品,为学生编程提供丰富的素材。


长远规划及招聘计划


未来三到五年,我们希望可以做到:



  • 蜗牛绘馆:在中国多个核心城市开几百家直营绘馆,月活家长达到几十万,会员用户达到几万,实现年营收几亿。

  • 艺培助理:月活达到几百万,服务几千家加盟店,拥有几万普通会员,实现年营收几十亿。

  • 各类社区:面向成人,对于不同的兴趣方向,打造不同的内容社区。

  • 周边生态:基于太空主题,研发相关的绘本、教具、玩具、服装等。

  • 航天制造:研发自己的航天飞行器,探索飞离太阳系的各种技术。


发展顺利的话,我们的组织架构及规模设想:



  • 基础技术部(500人):提供私有云服务、企业效能系统及AIGC的技术基座。

  • 绘馆事业部(500人):负责线下绘馆门店业务的开展,教学及教务为主。

  • 换购事业部(200人):负责二手交易平台的系统及线上线下运营。

  • 课件事业部(300人):负责课件系统的研发及课件社区的运营。

  • 编程事业部(300人):负责少儿编程相关的课程及系统研发。

  • 营销事业部(200人):负责3D展馆、海报、视频制作等的研发及业务开展。

  • 玩具事业部(100人):研发航天相关的玩具、服装或教具。

  • 公益事业部(100人):负责把家长换课的闲置物品捐赠给大城市农民工或贫困地区的孩子,组织志愿者远程给偏远地区孩子上科创综合课。


总结


WechatIMG239.jpg


和平年代,虽然没有战斗,但更需要军人的勇气,邓小平当年指出,军队建设要服从国家大局。虽然我退役了,但军人的血性刻在骨里,相比英雄的先辈们在抗日战争和抗美援朝中付出的鲜血,创业的艰难算什么呀~


我们要把航天梦一代代的传下去,我们将付出任何代价、忍受任何重负、应付任何艰辛、支持任何朋友、反对任何敌人,以确保梦想的存在与实现。


有梦想的人才有灵魂,才会快乐。我们为梦想所奉献的精力、信念和忠诚,将照亮我们的国家和身边的人,而这火焰发出的光芒定能照亮全世界。


作者:三一习惯
来源:juejin.cn/post/7309158055018053658
收起阅读 »

四年沿海城市,刚毕业,一年3家公司

去年自己也写了一篇总结,看了去年的总结和目标,感觉今年过得跟gs一样,哎。🥹 正如标题所言,上大学到现在一直呆在沿海城市。然后今年毕业,这一年换了三家公司。下面来讲讲自己的最近的经历吧。 本人大学坐落于辽宁锦州(一个真正的“海角”城市)。 那边有笔架山,一个...
继续阅读 »

去年自己也写了一篇总结,看了去年的总结和目标,感觉今年过得跟gs一样,哎。🥹


正如标题所言,上大学到现在一直呆在沿海城市。然后今年毕业,这一年换了三家公司。下面来讲讲自己的最近的经历吧。


本人大学坐落于辽宁锦州(一个真正的“海角”城市)。



  • 那边有笔架山,一个四面环海的山峰,长得像笔架,得名笔架山,开学第一天就趟水过去了。

  • 还有就是最常去的白沙湾(和对象去了好多次了)。

  • 还有就是锦州的夜市、公园都被逛烂了。还记得在夜市吆喝着“嘎嘎香的锦州烤肉吆!!!”

  • 还有北普陀山。

  • ...


大三下学期去大连呆了小半年这真的是“生不如死”,在学校总想着逃离,那种渴望自由的心想必在像我这种双非学生都有吧。就觉得在学校就是阻挡老子发财,满满的全是限制。垃圾桶里不能放垃圾,床上不能睡人...我只想骂他bbzz。😅


事实证明,不考研,对于大部分学生来说,早早出来工作就是上大学最重要的事情。一切阻挡出校实习的想法都是罪恶的。每天在学校活在那一亩三分地能有啥用,学校一边强制这一边强制那...


image.png


想想都可笑,不过最后拿到面试机会老师还是提供教室让自己面试。非常感谢。


虽然这里是满满的抱怨,大学时期也会不时给自己小小的惊喜。


MergedImages.png


MergedImages.png
然后在大三暑假自己也拿到了一个满意的offer,去了杭州电魂,对于一个没有经验的学生来说,拿到一家上市游戏公司的offer对于我当时来说也是蛮开心的。当时也没啥经验,投递时间也不是很好(6,7月份),行情也不是很好,又加上之前面试字节遭到打击(当时初生牛犊不怕虎,大三下期人生第一次面试就碰上了字节,面之前有多紧张,面之后就有多狼狈。😊)事后疯狂总结八股,网络等等。可以看下这个些专栏



导致一度怀疑自己。7月初一个星期全在面试,搞得自己精疲力尽,很累,也拿到了几个offer,还有一些在走流程,刚好那段时间我们也放假了,就回家呆了一个多星期,然后就去了杭州。在电魂的这段时间真的满满的幸福感。(独立大厦,每个节日满满的幸福感,活动,团建很频繁,部分大佬们都很和善...)这里就不在赘述了,感兴趣的可以看去年总结的文章 《一位初入职场前端仔的年度终结 <回顾2022,展望2023>》


很不幸的是,今年上半年,公司业绩不好,裁了很多人,实习生也基本都清退了。这就是今年的呆的第一家公司啦。很感谢,充满感激。💗


然后自己又要从头开始啦,当时觉得好难搞,毕竟自己在快被毕业的时候清退,加上没有参加23届秋招,大家都知道今年行情啥样,就很担心自己成为了肄业青年了。😟


不过还好,离职后,所有招聘软件疯狂投递,面了接近两个星期,拿到了厦门一家储能制造业offer后,就开始摆烂了。因为当时觉得他开了条件和福利都还不错。最后面试都直接开摆,还拒了几家面试。😭


这就为自己今年的遭遇埋下的伏笔。


到了7月初,满怀期待的来到了厦门(又一海滨城市😂),觉得自己可以在这家年轻的公司闯出一片天,md,真的是自己天真了。入职当天我就傻眼了。签了一大堆协议,签了一个多小时。无语子🥲


image.png


然后接下来一个星期让带我们学习企业文化,储能电池知识,娱乐等等。每天搞到8点多,人傻了。刚来,还没入职居然培训到8点多。(当时觉得还好,毕竟他们给我们申请了下班费)然后就觉得以后狂狂加班,赚他个大几千块(这真的是狠狠地打我的脸啊,后面介绍恶心操作😭)


入职当天,也挺xx的,那个叫yq的人,上来叫我看个bug, 源码刚拿到手,和我说了下这个需求,几分钟后和他确认了一下需求,他就不耐烦了,说"能不能行,不行不让你改了,找别人去。", 无语子啊。最后找不到人改(我师父当时刚好请假了),最后又回来让我改了。我要不是“胆小怕事”,直接干他了,xx玩意。


这公司还有些迷之操作,办公发个破笔记本,显示屏不给配,有时候起个项目要1h+,直接骂娘了。还不让带自己的电脑办公。


我们入职第二个星期,整个项目的需求就来了,然后我们的噩梦就来了。从那开始,加班没断过,最骚的操作是,加班申请不给审批。9月份加班60h+,就审批了10h不到。而且加班完9点过后公司没有班车了,打车回宿舍还要自己掏钱,我人真的无语喽。🥹 最最无语的是周末加班。从早上9点到晚上12点,都不给批,666。


微信图片_20231211004224.jpg


之所以效率这么低,那就是这项目有个“牛逼的”产品,上午确定好需求,上午刚开发好,下午就改需求。当天还要上线,这都是常规操作。真的很棒嘞。🤣


最搞笑的是,这公司公积金一个月给你交100💩, 直接笑死,每个月在掘金写文章都是他的2-3倍。


真的很庆幸自己在11月初离开了这种魔鬼公司。这种公司没有任何可以值得留恋的地方,但是还是找到了几个聊得来的朋友的。😃


所以说,同志们,还是要去互联网公司发展啊。远离这种lj的制造业公司。


由于女朋友在上海,所以离开公司后就来到上海(又来到了另一个滨海城市),一个星期左右,拿到了目前这个家公司的offer,成功涨薪4k,这家公司做的产品都很牛逼,互联网取证,给公安做网络取证用的,然后自己也进入了比较有挑战性的项目组,非常感谢可以给我这次机会。


入职三个星期左右了,福利待遇都非常好,每天有吃不完的零食,到点下班,真的爽歪歪。即使不让我加班,我也宁愿待在公司学习一会,就想当时在电魂一样,天天窝在公司。


也不说明年目标了,目标都是给别人看的,结果才是给自己看的。希望自己可以胜任这份工作,为公司产出优秀的产品的同时,也让自己变得更优秀。


仅此,给刚毕业的自己一个教训和经验,最好的永远在后面,而能看到后面的,永远是一直在进步和坚持的那群人。


加油,少年!


今天翻阅旧照片,贴贴美美的照骗。


MergedImages (1).png


作者:Spirited_Away
来源:juejin.cn/post/7310895905573716005
收起阅读 »

一位初入职场前端仔的年度终结

回顾这一年来的变化,只能说是平平无奇。于我而言从焦虑到不焦虑亦是从学生时代进入职场。 1 - 7月 平平无奇 今年一开始就“逃离”了学校,由于我们的专业培训方案是大三下学期去实训公司待上几个月。所以这就是“逃离”学校的最好时机。大家都知道,对于我们普通本科生而...
继续阅读 »

回顾这一年来的变化,只能说是平平无奇。于我而言从焦虑到不焦虑亦是从学生时代进入职场。


1 - 7月 平平无奇


今年一开始就“逃离”了学校,由于我们的专业培训方案是大三下学期去实训公司待上几个月。所以这就是“逃离”学校的最好时机。大家都知道,对于我们普通本科生而言,在学校是最浪费时间的事情。(至少对于我是这样的)。前几天,在学校考研的朋友和我说,感觉现在很迷茫,不知道怎么办。自己专注一年多的时间,还是没有好的结果,事实本是如此。不如早早实习工作获取职场经验。


1, 2月放假在家,闲着没事就跟着王洪元老师学习前端的一些知识。


3月份去大连每天两点一线的公司 -> 宿舍两边跑。


4月份不满于现状,在boss上投了几份简历,结果就字节约了面试,当时觉得自己学的可以了,就想试一试。结果真的是让我重新认清了自己。从那时起我就泡在了牛客等面试社区中。一边学习新知识,一边总结的面试题。


image.png


5,6 月一边修改简历一边分析总结面试题。
image.png


直到6月底,觉得目前已经系统的了解了一下前端相关的面试题,并且梳理了一下技术架构。觉得还是需要尝试一下。每天在boss上投递简历,基本没人回复,但是也约了一些公司面试。还是积累了一些经验的。


你们应该知道面试是非常累的,非常消耗精力,还好那个时候,我们宿舍距离海边很近,我每天都会跑到海边观望,真的感觉大海治愈我的一切疲惫。


image.png


image.png


image.png


image.png


image.png


7月初已经面了一些公司的hr面,感觉自己累了,就没有再去投递简历面试了。我记得7.14号回家的时候,还有公司打电话约面试。我直接拒掉了。


7 - 目前 充满激情与动力


在评估了一下手里的offer后,在7.25号来到了目前的一家游戏公司实习。我很庆幸自己初入职场就来到这个充满善意 (此处省略一万字,这个部门同事合作都好多年了,特别友好...) 的部门。


在这里活不是很多,但是自己也做了一些事情的。



  • 维护公司内部的一个大型后台系统。

  • 通过amis低代码重构公司内部的一个服务型后台系统。

  • 重构公司手游充值页面,主要是ui重构。

  • 合作开发微信h5公众号社区项目。

  • 开发一个游戏官网。

  • ...


话说这半年来,大大小小的需求都完成了这么多了。


image.png


这些内容对我来说都有很大成长,其实在梳理这篇文章之前,我感觉今年自己没有干啥事,但是这样一看还是做了一些事情的。


在这里工作中也总结了一些文章。


image.png


给你看看我们公司的福利。 嘻嘻



  • 迎新聚餐, 我们部门的传统


在星光一期小厨师海鲜,非常丰富的一场晚宴。


image.png



  • 情人节


这一天刚进门,hr小哥哥小姐姐就会把这束玫瑰花送到你手中。惊喜。


image.png



  • 公司成立日


公司成立日射箭获得的奖品


image.png



  • 公司的迎新晚宴和活动


美食和奖品都是很好滴。


image.png
image.png



  • 10.24程序员节


死缠烂打最后获取一等奖品之一灭霸乐高。 其实我想要那个键盘滴,但是没有货了。呜呜呜~~~


image.png



  • 中秋节


说实话观赏感十足。


image.png
image.png
image.png



  • 春节礼盒


一箱苹果加一个零食礼盒(礼盒包邮到家), 苹果真甜~~


image.png



  • 还有就是部门不时的会请奶茶,kfc等等


hihi, 疯狂星期四,来份kfc。


image.png


我与掘金


image.png


我和她


其实选择杭州还有一个原因就是距离 我家猪 很近,因为她在上海工作。


这一年,有假期就会去上海找她玩,反正感觉挺好的这样。保持对彼此的热度。


image.png


image.png


话说旁边那栋楼比东方明珠高吧


image.png


12.31号,把握2022的最好时刻,一起做手工。


image.png


image.png


期待的2023



  • 了解一下web3.0

  • 深入学习一下微前端,2022在闲的时候看了一些demo,跑了一下功能,大致了解了工作原理。

  • 搭建一个组件库。(作为练手项目)

  • 快滚出学校了,那当然是写论文啦。

  • ...


加油,愿你我前程似锦,愿你我lucky forever


大家也来说说自己的故事吧。2023, 一起加油,我们来啦。


作者:Spirited_Away
来源:juejin.cn/post/7188374796114067511
收起阅读 »

万事不要急,不行就抓一个包嘛!

一、网络问题分析思路1、问题现象确认问题现象是什么丢包/访问不通/延迟大/拒绝访问/传输速度2、报文特征过滤找到与问题现象相符的报文3、问题原因根据报文特征和发生位置,推断问题原因安全组/iptables/服务问题4、发生位置分析异常报文,判断问题发生的位置客...
继续阅读 »

一、网络问题分析思路

1、问题现象

确认问题现象是什么

丢包/访问不通/延迟大/拒绝访问/传输速度

2、报文特征

过滤找到与问题现象相符的报文

3、问题原因

根据报文特征和发生位置,推断问题原因

安全组/iptables/服务问题

4、发生位置

分析异常报文,判断问题发生的位置

客户端/服务端/中间链路.....

二、关注报文字段

  1. time:访问延迟
  2. source:来源IP
  3. destination:目的IP
  4. Protocol:协议
  5. length:长度
  6. TTL:存活时间(Time To Live)国内运营商劫持,封禁等处理
  7. payload:TCP包的长度

三、本机抓包

curl http://www.baidu.com

为什么不使用ping请求?

因为ping请求是ICMP请求:用于在 IP 网络上进行错误报告、网络诊断和网络管理。而curl请求是HTTP GET 请求。

追踪TCP流,可以看到三次捂手——》数据交互——》四次挥手

image-20231211161849484

image-20231211161958078

四、案例 TTL异常的reset

业务场景: 客户端通过公网去访问云上的CVM的一个公网IP,发现不能访问

image-20231211170925943

image-20231211171135366

TTL突发过长——》判断云上资源有无策略上的设置——》公网端判断原因运营商的封堵(TTL 200以下)

解决方案:报障运营商

四、案例 访问延迟-判断延时出现位置

业务场景: 客户同VPS内网,两CVM互相访问

image-20231211173214992

image-20231211173103998

目的端回包0.006毫秒——》链路无问题,回包时间过长——》CVM底层调用逻辑问题

五、案例 丢包

业务场景: 云上CVM访问第三网方网站失败,影响业务

image-20231211175015273

五、案例 未回复http响应

业务场景:云上CVM访问第三网方网站失败,影响业务

image-20231211175432145

image-20231211175702839

ping正常,拨测正常——》客户端发送HTTP请求,目的端无HTTP回包,直接三次挥手——》确认根因服务端不回复我们——》推测:运营商封禁或者第三方网站设置了访问限制

六、基础&常见问题

1、了解TCP/IP四层协议

image-20231018154301479

2、了解TCP 三次握手与四次挥手

SYN:同步位。SYN=1,表示进行一个连接请求。

ACK:确认位。ACK=1,确认有效;ACK=0,确认无效;。

ack:确认号。对方发送序号 + 1

seq:序号

image-20231211111546472

image-20231018155125516

FIN=1,断开链接,并且客户端会停止向服务器端发数据

image-20231211112144895

image-20231018155211125

3、Http传输段构成(浏览器的请求内容有哪些,F12中Network)

请求:

  1. 请求行(Request Line):包含HTTP方法(GET、POST等)、请求的URL和HTTP协议的版本。
  2. 请求头部(Request Headers):包含关于请求的附加信息,如用户代理、内容类型、授权信息等。
  3. 空行(Blank Line):请求头部和请求体之间必须有一个空行。
  4. 请求体(Request Body):可选的,用于传输请求的数据,例如在POST请求中传递表单数据或上传文件。

响应:

  1. 状态行(Status Line):包含HTTP协议的版本、状态码和状态消息。
  2. 响应头部(Response Headers):包含关于响应的附加信息,如服务器类型、内容类型、响应时间等。
  3. 空行(Blank Line):响应头部和响应体之间必须有一个空行。
  4. 响应体(Response Body):包含实际的响应数据,例如HTML页面、JSON数据等。

这些组成部分共同构成了HTTP传输过程中的请求和响应。请求由客户端发送给服务器,服务器根据请求进行处理并返回相应的响应给客户端。

4、DNS污染怎么办

DNS污染是指恶意篡改或劫持DNS解析的过程,导致用户无法正确访问所需的网站或被重定向到错误的网站。如果您怀疑遭受了DNS污染,可以尝试以下方法来解决问题:

1、更改本地的DNS。 公共DNS服务器Google Public DNS(8.8.8.8和8.8.4.4)

2、清理本地DNS缓存。 systemctl restart NetworkManager。这将重启NetworkManager服务,刷新DNS缓存并应用任何新的DNS配置。

3、使用HTTPS。使用HTTPS访问网站可以提供更安全的连接,并减少DNS污染的风险。确保您访问的网站使用HTTPS协议。

5、traceroute路由追踪跳转过程

img

当您运行traceroute http://www.baidu.com命令时,它将显示从您的计算机到目标地址(http://www.baidu.com)之间的网络路径。它通过发送一系列的网络探测包(ICMP或UDP)来确定数据包从源到目标的路径,并记录每个中间节点的IP地址。

以下是一般情况下,traceroute命令可能经过的地址类型:

  1. 您的本地网络地址:这是您计算机所连接的本地网络的IP地址。
  2. 网关地址:如果您的计算机连接到一个局域网,它将经过您的网络网关,这是连接您的局域网与外部网络的设备。traceroute命令将显示网关的IP地址。
  3. ISP(互联网服务提供商)的路由器地址:数据包将通过您的ISP的网络传输,经过多个路由器。traceroute命令将显示每个路由器的IP地址。
  4. 目标地址:最后,数据包将到达目标地址,即http://www.baidu.com的IP地址。

img

6、Http状态码

100-199表示请求已被接收,继续处理比如:websocket_VScode自动刷新页面
200-299表示请求已成功被服务器接收、理解和处理。200 OK:请求成功,服务器成功返回请求的数据。 201 Created:请求成功,服务器已创建新的资源。204 No Content:请求成功,但服务器没有返回任何内容。
300-399(重定向状态码)表示需要进一步操作以完成请求301 Moved Permanently:请求的资源已永久移动到新位置。302 Found:请求的资源暂时移动到新位置。304 Not Modified:客户端的缓存资源是最新的,服务器返回此状态码表示资源未被修改。
400-499表示客户端发送的请求有错误。400 Bad Request:请求无效,服务器无法理解。401 Unauthorized:请求要求身份验证。404 Not Found:请求的资源不存在。
500-599表示服务器在处理请求时发生错误。500 Internal Server Error:服务器遇到了意外错误(服务器配置出错/数据库错误/代码出错),无法完成请求。503 Service Unavailable:服务器暂时无法处理请求,通常是由于过载或维护。

7、CDN内容加速网络原理

原理:减少漫长的路由转发,就近访问备份资源

1、通过配置网站的CDN,提前让CDN的中间节点OC备份一份内容,在分发给用户侧的SOC边缘节点,这样就能就近拉取资源。不用每次都通过漫长的路由导航到源站。

2、但是要达到加速的效果,还需要把真实域名的IP更改到CDN的IP,所有这里还需要DNS的帮助,这里一般都会求助用户本地运营商搭建的权威DNS域名解析服务器,用户请求逐级请求各级域名,本来应该会返回真实的IP地址,但是通过配置会返回给用户一个CDN的IP地址,CDN的权威服务器再讲距离用户最近的那台CDN服务器IP地址返回给用户,这样就实现了CDN加速的效果。

8、前后端通信到底是怎样一个过程

链接参考:juejin.cn/post/728518…

内容参考:

小林coding:xiaolincoding.com/network/

哔哩哔哩:http://www.bilibili.com/video/BV1at…

Winshark:http://www.wireshark.org/


作者:武师叔
来源:juejin.cn/post/7311159008483983369

收起阅读 »

Android 放大镜窥视效果

前言 放大镜效果是一种常用的局部图片观察效果,其本质原理依然是将原图片放大之后,经过范围裁剪然后会知道指定区域的一种效果。实际上放大效果有2种常见的效果,比如在一些购物网站,鼠标移动到的位置被放大,然后展示在侧边区域,这两者代码几乎一样,主要区别如下: 侧边...
继续阅读 »

前言


放大镜效果是一种常用的局部图片观察效果,其本质原理依然是将原图片放大之后,经过范围裁剪然后会知道指定区域的一种效果。实际上放大效果有2种常见的效果,比如在一些购物网站,鼠标移动到的位置被放大,然后展示在侧边区域,这两者代码几乎一样,主要区别如下:



  • 侧边区域观测要移动Shader或者在指定位置裁剪图像

  • 本文效果是移动区域,但是为了保证图片能尽可能对齐,需要将放大的图片向左上角偏移。


本文和上一篇《手电筒照亮效果》一样,如果没看过的先看上一篇,方便你理解本篇,因为同样的原理不会在这篇重新提及或者过多提及,都是局部区域效果实现。


效果预览


滑动放大效果


fire_62.gif


窥视效果


fire_63.gif


方法镜滑动放大实现方法


使用Shader作为载体


首先要做的是将图片放大,放大之后,我们可以利用Path裁剪图片或者Shader向裁剪区域绘制,这里我们依然使用Shader,毕竟优点很多,这里我们主要要实现2个目的。



  • Shader载入Bitmap,放大1.2倍

  • Shader向左上角偏移,对齐图片中心


      if (shader == null) {
float ratio = 1.2f;
scaledBitmap = Bitmap.createScaledBitmap(mBitmap, (int) (mBitmap.getWidth() * ratio), (int) (mBitmap.getHeight() * ratio), true);
shader = new BitmapShader(scaledBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
// 做下偏移
matrix.setTranslate(-(scaledBitmap.getWidth() - mBitmap.getWidth())/2f ,-(scaledBitmap.getHeight() - mBitmap.getHeight())/2f);
shader.setLocalMatrix(matrix);
}

事件处理


其实处理事件有很多简便的方法,但是首先得拦截事件,Android种拦截事件的方法很多,clickable就是其中之一


setClickable(true); //触发hotspot

拦截按压移动事件,这里我们使用 HotSpot 机制,其实就是触点,西方人命名习惯使用HotSpot,通过下面就能处理事件,连onTouchEvent我们都不用搭理。


  @Override
public void dispatchDrawableHotspotChanged(float x, float y) {
super.dispatchDrawableHotspotChanged(x, y);
this.x = x;
this.y = y;
postInvalidate();
}

@Override
protected void dispatchSetPressed(boolean pressed) {
super.dispatchSetPressed(pressed);
postInvalidate();
}

裁剪Canvas区域为原图区域


为什么要裁剪Canvas区域内,主要是因为你的图片并不一定能完全填充整个View,但是你使用的TileMode肯定是CLAMP,这会使得放大镜中图像的边缘拉长,现象很奇怪,反正你可以去掉试试。另外说一下,Android中似乎新增加了一种TileMode,不过还没来得及试一下。


   int save = canvas.save();
canvas.clipRect(0,0,mBitmap.getWidth(),mBitmap.getHeight());
canvas.restoreToCount(save);

绘制核心逻辑


在核心逻辑中,我们有一步要绘制区域填充颜色,主要原因是非透明区域的绘制会导致出现透视效果。


    int save = canvas.save();
canvas.clipRect(0,0,mBitmap.getWidth(),mBitmap.getHeight());
//绘制原图
canvas.drawBitmap(mBitmap, 0, 0, null);
//区域用填充颜色,防止出现区域透视,上面的区域能看见下面的区域
mCommonPaint.setColor(Color.WHITE);
canvas.drawCircle( x , y,width/4f,mCommonPaint);
//绘制放大效果
mCommonPaint.setShader(shader);
canvas.drawCircle( x , y,width/4f,mCommonPaint);
mCommonPaint.setShader(null);

canvas.restoreToCount(save);

放大镜窥视效果


其实两者代码没有多大区别,滑动放大效果主要是移动镜子,而窥视效果镜子不动,使用移动图片的方式实现。


位置计算 & 绘制


固定镜子中心在右下角


//放大平移时需要偏移的距离
float offsetX = -(scaledBitmap.getWidth() - mBitmap.getWidth()) / 2f;
float offsetY = -(scaledBitmap.getHeight() - mBitmap.getHeight())/2f;
//窥视镜圆心
float mirrorCenterX = mBitmap.getWidth() - width / 4f;
float mirrorCenterY = mBitmap.getHeight() - width/4f;

图像平移距离


(mirrorCenterX - x) 
(mirrorCenterY - y)

矩阵变换,平移事件点位置图像到右下角圆的中心


//(mirrorCenterX - x) ,(mirrorCenterY-y) 是把当前中心点的图像平移到圆心哪里
matrix.setTranslate( offsetX + (mirrorCenterX - x) , offsetY + (mirrorCenterY-y));
shader.setLocalMatrix(matrix);

绘制镜子



int save = canvas.save();
canvas.clipRect(0,0,mBitmap.getWidth(),mBitmap.getHeight());
canvas.drawBitmap(mBitmap, 0, 0, null);
mCommonPaint.setColor(Color.DKGRAY);
canvas.drawCircle(mirrorCenterX , mirrorCenterY,width/4f,mCommonPaint);
mCommonPaint.setShader(shader);
canvas.drawCircle( mirrorCenterX , mirrorCenterY,width/4f,mCommonPaint);
mCommonPaint.setShader(null);

canvas.restoreToCount(save);

总结


本篇和之前的很多篇文章一样,都是实现Canvas图片绘制,很复杂的效果我们没有涉及到,但是在这些文章中,都会有各种各样的问题和思考。总之,我们要善于利用矩阵和设计思想,绘制我们的想象。


全部代码


按照惯例,提供全部代码


滑动放大代码


public class ScaleBigView extends View {
private final DisplayMetrics mDM;
private TextPaint mCommonPaint;
private Bitmap mBitmap;
private Shader shader = null;
private Matrix matrix = new Matrix();
private Bitmap scaledBitmap;

public ScaleBigView(Context context) {
this(context, null);
}

public ScaleBigView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ScaleBigView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mDM = getResources().getDisplayMetrics();
initPaint();
setClickable(true); //触发hotspot
}

private void initPaint() {
//否则提供给外部纹理绘制
mCommonPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
mCommonPaint.setAntiAlias(true);
mCommonPaint.setStyle(Paint.Style.FILL_AND_STROKE);
mCommonPaint.setStrokeCap(Paint.Cap.ROUND);
mCommonPaint.setFilterBitmap(true);
mCommonPaint.setDither(true);
mCommonPaint.setStrokeWidth(dp2px(20));
mBitmap = decodeBitmap(R.mipmap.mm_012);

}

private Bitmap decodeBitmap(int resId) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inMutable = true;
return BitmapFactory.decodeResource(getResources(), resId, options);
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
if (widthMode != MeasureSpec.EXACTLY) {
widthSize = mDM.widthPixels / 2;
}
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);

if (heightMode != MeasureSpec.EXACTLY) {
heightSize = widthSize / 2;
}
setMeasuredDimension(widthSize, heightSize);

}
private float x;
private float y;
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
if (width < 1 || height < 1) {
return;
}
if (shader == null) {
float ratio = 1.2f;
scaledBitmap = Bitmap.createScaledBitmap(mBitmap, (int) (mBitmap.getWidth() * ratio), (int) (mBitmap.getHeight() * ratio), true);
shader = new BitmapShader(scaledBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
matrix.setTranslate(-(scaledBitmap.getWidth() - mBitmap.getWidth())/2f ,-(scaledBitmap.getHeight() - mBitmap.getHeight())/2f);
shader.setLocalMatrix(matrix);
}


int save = canvas.save();
canvas.clipRect(0,0,mBitmap.getWidth(),mBitmap.getHeight());
//绘制原图
canvas.drawBitmap(mBitmap, 0, 0, null);
//区域用填充颜色,防止出现区域透视,上面的区域能看见下面的区域
mCommonPaint.setColor(Color.WHITE);
canvas.drawCircle( x , y,width/4f,mCommonPaint);
//绘制放大效果
mCommonPaint.setShader(shader);
canvas.drawCircle( x , y,width/4f,mCommonPaint);
mCommonPaint.setShader(null);

canvas.restoreToCount(save);
}

@Override
public void dispatchDrawableHotspotChanged(float x, float y) {
super.dispatchDrawableHotspotChanged(x, y);
this.x = x;
this.y = y;
postInvalidate();
}

@Override
protected void dispatchSetPressed(boolean pressed) {
super.dispatchSetPressed(pressed);
postInvalidate();
}

public float dp2px(float dp) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, mDM);
}

}

窥视镜效果


public class ScaleBigView extends View {
private final DisplayMetrics mDM;
private TextPaint mCommonPaint;
private Bitmap mBitmap;
private Shader shader = null;
private Matrix matrix = new Matrix();
private Bitmap scaledBitmap;

public ScaleBigView(Context context) {
this(context, null);
}

public ScaleBigView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ScaleBigView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mDM = getResources().getDisplayMetrics();
initPaint();
setClickable(true); //触发hotspot
}

private void initPaint() {
//否则提供给外部纹理绘制
mCommonPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
mCommonPaint.setAntiAlias(true);
mCommonPaint.setStyle(Paint.Style.FILL_AND_STROKE);
mCommonPaint.setStrokeCap(Paint.Cap.ROUND);
mCommonPaint.setFilterBitmap(true);
mCommonPaint.setDither(true);
mCommonPaint.setStrokeWidth(dp2px(20));
mBitmap = decodeBitmap(R.mipmap.mm_012);

}

private Bitmap decodeBitmap(int resId) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inMutable = true;
return BitmapFactory.decodeResource(getResources(), resId, options);
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
if (widthMode != MeasureSpec.EXACTLY) {
widthSize = mDM.widthPixels / 2;
}
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);

if (heightMode != MeasureSpec.EXACTLY) {
heightSize = widthSize / 2;
}
setMeasuredDimension(widthSize, heightSize);

}
private float x;
private float y;
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
if (width < 1 || height < 1) {
return;
}
if (shader == null) {
float ratio = 1.2f;
scaledBitmap = Bitmap.createScaledBitmap(mBitmap, (int) (mBitmap.getWidth() * ratio), (int) (mBitmap.getHeight() * ratio), true);
shader = new BitmapShader(scaledBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);

}
//放大平移
float offsetX = -(scaledBitmap.getWidth() - mBitmap.getWidth()) / 2f;
float offsetY = -(scaledBitmap.getHeight() - mBitmap.getHeight())/2f;

//窥视镜圆心
float mirrorCenterX = mBitmap.getWidth() - width / 4f;
float mirrorCenterY = mBitmap.getHeight() - width/4f;

//(mirrorCenterX - x) ,(mirrorCenterY-y) 是把当前中心点的图像平移到圆心哪里
matrix.setTranslate( offsetX + (mirrorCenterX - x) , offsetY + (mirrorCenterY-y));
shader.setLocalMatrix(matrix);

int save = canvas.save();
canvas.clipRect(0,0,mBitmap.getWidth(),mBitmap.getHeight());
canvas.drawBitmap(mBitmap, 0, 0, null);
mCommonPaint.setColor(Color.DKGRAY);
canvas.drawCircle(mirrorCenterX , mirrorCenterY,width/4f,mCommonPaint);
mCommonPaint.setShader(shader);
canvas.drawCircle( mirrorCenterX , mirrorCenterY,width/4f,mCommonPaint);
mCommonPaint.setShader(null);

canvas.restoreToCount(save);
}

@Override
public void dispatchDrawableHotspotChanged(float x, float y) {
super.dispatchDrawableHotspotChanged(x, y);
this.x = x;
this.y = y;
postInvalidate();
}

@Override
protected void dispatchSetPressed(boolean pressed) {
super.dispatchSetPressed(pressed);
postInvalidate();
}

public float dp2px(float dp) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, mDM);
}

}

作者:时光少年
来源:juejin.cn/post/7310124656996302874
收起阅读 »

Android 手电筒照亮效果

前言 经常在掘金博客上看到使用前端技术实现的手电筒照亮效果,但是搜了一下android相关的,发现少得很,实际上这种效果在Android上实现会更简单,当然,条条大路通罗马,有很多技术手段去实现这种效果,今天我们选择一种相对比较好的方法来实现。 实现方法梳理 ...
继续阅读 »

前言


经常在掘金博客上看到使用前端技术实现的手电筒照亮效果,但是搜了一下android相关的,发现少得很,实际上这种效果在Android上实现会更简单,当然,条条大路通罗马,有很多技术手段去实现这种效果,今天我们选择一种相对比较好的方法来实现。


实现方法梳理



  • 第一种方法就是利用Path路径进行 Clip Outline,然后绘制不同的渐变效果即可,这种方法其实很适合蒙版切图,不过也能用于实现这种特效。

  • 第二种方法是利用Xfermode 进行中间图层镂空。

  • 第三种方法就是Shader,效率高且无锯齿。


效果


fire_61.gif


实现原理


其实本篇的核心就是Shader了,这次我们也用RadialGradient来实现,本篇几乎没有任何难度,关键技术难点就是Shader 的移动,其实最经典的效果是Facebook实现的光影文案,本质上时Matrix + Shader.setLocalMatrix 实现。


155007_4C1U_2256215.gif


Matrix涉及一些数学问题,Matrix初始化本身就是单位矩阵,几乎每个操作都是乘以另一个矩阵,属于线性代数的基本知识,难度其实并不高。


matrix.setTranslation(1,2) 可以看作,矩阵的乘法无非是行乘列,繁琐事繁琐,但是很容易理解



1,0,0, 1,0,1,
0,1,0, X 0,1,2,
0,0,1 0,0,1


我们来看看经典的facebook 出品代码


public class GradientShaderTextView extends TextView {

private LinearGradient mLinearGradient;
private Matrix mGradientMatrix;
private Paint mPaint;
private int mViewWidth = 0;
private int mTranslate = 0;

private boolean mAnimating = true;
private int delta = 15;
public GradientShaderTextView(Context ctx)
{
this(ctx,null);
}

public GradientShaderTextView(Context context, AttributeSet attrs) {
super(context, attrs);
}

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (mViewWidth == 0) {
mViewWidth = getMeasuredWidth();
if (mViewWidth > 0) {
mPaint = getPaint();
String text = getText().toString();
// float textWidth = mPaint.measureText(text);
int size;
if(text.length()>0)
{
size = mViewWidth*2/text.length();
}else{
size = mViewWidth;
}
mLinearGradient = new LinearGradient(-size, 0, 0, 0,
new int[] { 0x33ffffff, 0xffffffff, 0x33ffffff },
new float[] { 0, 0.5f, 1 }, Shader.TileMode.CLAMP); //边缘融合
mPaint.setShader(mLinearGradient);
mGradientMatrix = new Matrix();
}
}
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);

int length = Math.max(length(), 1);
if (mAnimating && mGradientMatrix != null) {
float mTextWidth = getPaint().measureText(getText().toString());
mTranslate += delta;
if (mTranslate > mTextWidth+1 || mTranslate<1) {
delta = -delta;
}
mGradientMatrix.setTranslate(mTranslate, 0); //自动平移矩阵
mLinearGradient.setLocalMatrix(mGradientMatrix);
postInvalidateDelayed(30);
}
}

}

本文案例


本文要实现的效果其实也是一样的方法,只不过不是自动移动,而是添加了触摸事件,同时加了放大缩小效果。


坑点


Shader 不支持矩阵Scale,本身打算利用Scale缩放光圈,但事与愿违,不仅不支持,连动都动不了了,因此,本文采用了两种Shader,按压时使用较大半径的Shader,手放开时使用默认的Shader。


知识点


canvas.drawPaint(mCommonPaint);

这个绘制并不是告诉你可以这么绘制,而是想说,设置了Shader之后,这样调用,Shader半径之外的颜色时Shader最后一个颜色值,我们最后一个颜色值时黑色,那就是黑色,我们改成白色当然也是白色,下图是改成白色之后的效果,周围都是白色


企业微信20231207-230353@2x.png


关键代码段


super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
if (width < 1 || height < 1) {
return;
}
//大光圈shader
if (radialGradientLarge == null) {
radialGradientLarge = new RadialGradient(0, 0,
dp2px(100),
new int[]{Color.TRANSPARENT, 0x01ffffff, 0x33ffffff, 0x66000000, 0x77000000, 0xff000000},
new float[]{0.1f, 0.3f, 0.5f, 0.7f, 0.9f, 0.95f},
Shader.TileMode.CLAMP);
}
//默认光圈shader
if (radialGradientNormal == null) {
radialGradientNormal = new RadialGradient(0, 0,
dp2px(50),
new int[]{Color.TRANSPARENT, 0x01ffffff, 0x33ffffff, 0x66000000, 0x77000000, 0xff000000},
new float[]{0.1f, 0.3f, 0.5f, 0.7f, 0.9f, 0.95f},
Shader.TileMode.CLAMP);
}

//绘制地图
canvas.drawBitmap(mBitmap, 0, 0, null);

//移动shader中心点
matrix.setTranslate(x, y);
//设置到矩阵
radialGradientLarge.setLocalMatrix(matrix);
radialGradientNormal.setLocalMatrix(matrix);
if(isPressed()) {
//按压时
mCommonPaint.setShader(radialGradientLarge);
}else{
//松开时
mCommonPaint.setShader(radialGradientNormal);
}
//直接用画笔绘制,那么周围的颜色是Shader 最后的颜色
canvas.drawPaint(mCommonPaint);

好了,我们的效果基本实现了。


总结


本篇到这里就截止了,我们今天掌握的知识点是Shader相关的:



  • Shader 矩阵不能Scale

  • 设置完Shader 的画笔外围填充色为Ridial Shader最后的颜色

  • Canvas 可以直接drawPaint

  • Shader.setLocalMatrix是移动Shader中心点的方法


代码


按照惯例,给出全部代码


public class LightsView extends View {
private final DisplayMetrics mDM;
private TextPaint mCommonPaint;
private Bitmap mBitmap;
private RadialGradient radialGradientLarge = null;
private RadialGradient radialGradientNormal = null;
private float x;
private float y;
private boolean isPress = false;
private Matrix matrix = new Matrix();
public LightsView(Context context) {
this(context, null);
}

public LightsView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public LightsView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mDM = getResources().getDisplayMetrics();
initPaint();
setClickable(true); //触发hotspot
}

private void initPaint() {
//否则提供给外部纹理绘制
mCommonPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
mCommonPaint.setAntiAlias(true);
mCommonPaint.setStyle(Paint.Style.FILL_AND_STROKE);
mCommonPaint.setStrokeCap(Paint.Cap.ROUND);
mCommonPaint.setFilterBitmap(true);
mCommonPaint.setDither(true);
mBitmap = decodeBitmap(R.mipmap.mm_06);

}

private Bitmap decodeBitmap(int resId) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inMutable = true;
return BitmapFactory.decodeResource(getResources(), resId, options);
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
if (widthMode != MeasureSpec.EXACTLY) {
widthSize = mDM.widthPixels / 2;
}
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);

if (heightMode != MeasureSpec.EXACTLY) {
heightSize = widthSize / 2;
}
setMeasuredDimension(widthSize, heightSize);

}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
if (width < 1 || height < 1) {
return;
}
if (radialGradientLarge == null) {
radialGradientLarge = new RadialGradient(0, 0,
dp2px(100),
new int[]{Color.TRANSPARENT, 0x01ffffff, 0x33ffffff, 0x66000000, 0x77000000, 0xff000000},
new float[]{0.1f, 0.3f, 0.5f, 0.7f, 0.9f, 0.95f},
Shader.TileMode.CLAMP);
}
if (radialGradientNormal == null) {
radialGradientNormal = new RadialGradient(0, 0,
dp2px(50),
new int[]{Color.TRANSPARENT, 0x01ffffff, 0x33ffffff, 0x66000000, 0x77000000, 0xff000000},
new float[]{0.1f, 0.3f, 0.5f, 0.7f, 0.9f, 0.95f},
Shader.TileMode.CLAMP);
}

canvas.drawBitmap(mBitmap, 0, 0, null);

matrix.setTranslate(x, y);
radialGradientLarge.setLocalMatrix(matrix);
radialGradientNormal.setLocalMatrix(matrix);
if(isPressed()) {
mCommonPaint.setShader(radialGradientLarge);
}else{
mCommonPaint.setShader(radialGradientNormal);
}
canvas.drawPaint(mCommonPaint);
}

@Override
public void dispatchDrawableHotspotChanged(float x, float y) {
super.dispatchDrawableHotspotChanged(x, y);
this.x = x;
this.y = y;
postInvalidate();
}

@Override
protected void dispatchSetPressed(boolean pressed) {
super.dispatchSetPressed(pressed);
postInvalidate();
}

public float dp2px(float dp) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, mDM);
}

}

作者:时光少年
来源:juejin.cn/post/7309687967064817716
收起阅读 »

6G,它来了,真的666!

你好,这里是网络技术联盟站。2023年12月5日至6日,全球6G发展大会在重庆两江新区成功举行。会议汇集了中国两院院士和国内外权威专家,共同探讨了6G的发展愿景和技术产业路径。中国工程院院士张平提出,移动通信系统正处于一个重要的发展阶段,需要从容量、速率、服务...
继续阅读 »

你好,这里是网络技术联盟站。

2023年12月5日至6日,全球6G发展大会在重庆两江新区成功举行。会议汇集了中国两院院士和国内外权威专家,共同探讨了6G的发展愿景和技术产业路径。中国工程院院士张平提出,移动通信系统正处于一个重要的发展阶段,需要从容量、速率、服务和智能等多个维度上进行扩展。他强调,6G的发展应该着重于维度上和智能性上的提升。

张平院士指出,当前通信技术发展面临的挑战包括信息压缩的极限和数据吞吐量的过大,这些因素使得提升通信系统容量性能难以持续。因此,6G时代需要寻找可持续发展的路径,其中人工智能的融合被视为关键因素,有望实现通信技术和人工智能技术的共赢共生。

华为技术有限公司无线CTO童文在主题演讲中强调了人工智能在未来通信技术中的核心作用,预测在未来五年到十年内,大多数研发设计和文字工作将由AI取代。他认为,AI Agent将成为承载6G服务和应用的核心载体。

中国通信标准化协会理事长闻库则提出,6G的愿景不仅仅是提高网速,而是在此基础上实现手机性能比的提升、覆盖的提升和垂直行业的全面拓展。他强调,AI的引入为6G的发展提供了新的可能性,未来的通信将更加注重智能化的创新。

会议还讨论了6G距离落地的距离和后续关键行动,包括6G技术的梳理和验证、6G与5G的衔接、加强5G基础建设、推进5G-A迈向商用,以及加大技术创新力度和强化开放合作。

这次大会的讨论和发言反映了6G技术发展的最新动态和未来方向,展现了中国在6G技术研发方面的积极态度和领先地位。

对于我们从事网络,或者说从事IT行业的人来说,学习6G技术是迫在眉睫!本文瑞哥就带大家好好了解一下6G技术,相信看完本文,一定对您有所帮助!

让我们直接开始!

无线技术演进

无线通信技术的演进是一个持续的过程,每一代技术都在速度、容量、延迟和连接性方面带来了显著的改进。

1G无线网络

  • 时间:20世纪80年代
  • 特点:主要侧重于语音通信,网络速度缓慢。

2G无线网络

  • 时间:1990/91年
  • 特点:带来了一些无线数据服务,如WAP、MMS和SMS,但速度仍然很慢。2G的迭代版本2.5G和2.75G提高了数据速率。

3G无线网络

  • 时间:2004/2005年
  • 特点:增加了视频通话、移动电视、基于位置的服务,以及更快的数据传输速度(8-20Mbps)。随着需求的增加,出现了3.5G和3.75G,带来了移动电子邮件和个人对个人的游戏。

4G无线网络

  • 时间:2009年
  • 特点:能够更快地下载和上传文件,同时处理语音和数据呼叫。4G手机在信号充足的情况下可以快速下载文件。

5G无线网络

  • 时间:2019年
  • 特点:承诺更快的速度、更少的延迟和更多的连接,支持更多设备同时连接。5G网络使许多新产品和物联网传感器能够以极快的移动数据速度在全球范围内连接。

6G无线网络

  • 预计时间:2030年左右
  • 特点:预计将提升5G网络的功能,并提供增强的覆盖范围、改进的功能和超快的移动数据速度。支持全球数十亿个物联网设备,并使人们能够享受虚拟现实(VR)、增强现实(AR)和混合现实(MR)等应用。

每一代无线技术的演进都是为了满足不断增长的数据需求和改善用户体验。从1G到6G,我们见证了无线通信技术的巨大变革,这些变革不仅影响了我们的通信方式,还推动了社会和经济的发展。

下面我们先着重介绍一下4G和5G技术的发展。

4G技术的发展

4G,作为第四代移动通信技术,标志着移动互联网时代的到来。它在速度、容量和连接性方面相比于3G技术有了显著的提升,为用户提供了更快的上网体验和更丰富的移动服务。

4G技术的核心是长期演进(LTE)标准,它提供了更高的数据传输速率和更低的网络延迟。LTE的引入使得移动宽带服务质量得到了显著提升,支持了更高清晰度的视频通话和更快速的数据下载。

MIMO技术通过使用多个天线同时发送和接收数据,显著提高了信号的质量和传输速度。这一技术的应用使得4G网络能够支持更多用户的同时连接,同时提高了网络的稳定性和覆盖范围。

MIMO:多输入多输出。

OFDM技术通过将信号分散到多个频道上,减少了干扰并提高了频谱效率。这一技术的使用使得4G网络能够更有效地利用有限的频谱资源,提供更高的数据传输速率。

OFDM:正交频分复用。

4G特点

  • 高速数据传输:4G网络的数据速率通常在100Mbps到1Gbps之间,使得视频通话和在线游戏等应用变得流畅。这一速度的提升为移动互联网的普及和移动应用的发展提供了强大的动力。
  • 改善的网络覆盖:4G技术通过更高效的频谱利用和网络优化,提供了更广泛的网络覆盖和更好的服务质量。这一改进使得用户即使在移动中也能享受到稳定的网络连接。
  • 支持多种应用:4G网络支持了社交媒体、流媒体服务、云计算等多种新兴应用。这些应用的发展推动了移动互联网的创新和多样化,为用户提供了更丰富的选择和更便捷的服务。

5G技术的突破

5G,即第五代移动通信技术,是继4G之后的又一次重大技术革新。它不仅提供了更快的速度和更低的延迟,还开启了物联网和智能设备的新时代。

5G技术的一个重要特点是使用毫米波频段,这些频段通常在30GHz到300GHz之间。毫米波通信提供了更高的数据速率和更大的带宽,使得5G网络能够支持超高清视频流、虚拟现实和增强现实等带宽密集型应用。

5G网络采用了大规模多输入多输出(MIMO)技术,通过使用更多的天线,显著提高了网络容量和效率。这一技术的应用使得5G网络能够同时服务更多的用户,同时提高了信号的质量和覆盖范围。

5G技术引入了网络切片的概念,允许运营商为不同的服务和应用提供定制化的网络资源。这意味着网络可以根据应用的需求动态分配资源,从而提高效率和性能。

5G特点

  • 极高速度:5G网络的峰值速率可达20Gbps,这是4G网络速率的数倍。这一速度的提升为各种新兴应用提供了强大的支持,包括自动驾驶汽车、远程医疗和智能城市等。
  • 超低延迟:5G网络的延迟低至1毫秒,这对于需要即时响应的应用至关重要。例如,远程手术和工业自动化都需要极低的延迟来确保操作的准确性和安全性。
  • 广泛的连接性:5G技术支持海量的设备连接,这对于物联网的发展至关重要。从智能家居到智能工厂,5G网络能够支持数以亿计的设备同时在线,实现高效的数据交换和控制。

下面进入我们的主角:6G。

从马可尼的无线电信号传输到5G的高速移动通信,每一代技术都在推动社会进步和经济发展。6G预计将是一个跨越性的技术,不仅仅是速度的提升,而是在智能化、感知能力和计算能力上的全面革新。

那么什么是6G呢?

6G

6G是指第六代移动通信技术,是5G的后继者。它被设计为一种更高级、更先进的无线通信技术,旨在提供比5G更快的速度、更低的延迟和更大的网络容量。

6G会是什么样子?

6G有望实现每秒1太比特的极高速度,相较于当前大多数家庭互联网网络可用的最快速度1 Gbps,以及5G的最高速度10 Gbps,有了显著的提升。

6G可能会利用太赫兹波或亚毫米波的频段,这能够提供更高的频谱,支持更大的带宽,进一步增强网络性能。这也可能解决5G中毫米波距离短、需要视线的问题。

6G将更加依赖人工智能,实现协同工作,特别是在自动驾驶汽车、工厂自动化等方面。边缘计算的应用将使网络更本地化,减少响应时间,提高协同效率。

6G有望支持更高级别的沉浸式技术,包括虚拟现实、细胞表面、植入物和无线脑机接口。这将为用户提供更身临其境的体验,推动智能可穿戴设备和植入物的发展。

6G可能会实现物理生活与网络空间的完全融合,通过可穿戴设备和植入在人体上的微型设备,实时支持人类的思想和行动。

6G工作原理(可能)

6G的一个关键特征可能是利用超高频率传输数据。这包括在数百千兆赫(GHz)或甚至太赫兹(THz)范围内进行通信。这将提供更大的带宽和更高的数据传输速度。

6G可能会采用先进的技术,以提高频谱的利用效率。通过使用复杂的数学方法,6G网络可以在同一频率上实现同时发送和接收,从而提高频谱的传输效率。

6G可能会采用网状网络架构,将设备连接到彼此,形成一个分布式的网络。这样的架构可以提供更好的覆盖范围和更高的可靠性,同时支持设备之间的直接通信。

6G可能会引入新的互联网协议(New IP),以提高网络的效率和性能。这可能包括一种新型的IP数据包,具有更多导航和优先级信息,以支持更智能、自适应的网络通信。

6G可能会根据材料的原子和分子对特定波长的吸收和发射频率进行选择性的波长利用。这样的技术可以优化信号传输,并考虑到不同材料对电磁辐射的特定响应。

6G频段的使用

预计6G网络的最新先锋频谱将主要位于中频段,即7 GHz到20 GHz之间。这个频段的使用将通过极端的多输入多输出(MIMO)技术提供更大的容量。中频段的特点是提供相对较高的数据传输速率,并在城市室外小区中发挥重要作用。

对于广泛的覆盖范围,6G将继续利用低频段,预计在460 MHz到694 MHz之间。低频段通常用于提供更广泛的覆盖范围和更好的穿透能力,尤其是在城市和 室内环境中。

6G计划利用次太赫兹频谱,这是一个非常高的频段。这将实现超过100 Gbps的峰值数据速度,为未来对高速数据传输需求极高的应用提供支持。

6G将广泛使用新的光谱范围,包括高达太赫兹的频段。这将推动本地化技术到新的水平,提高定位的精确度,并为各种应用场景带来新的可能性。

6G将通过使用广泛的频谱范围,特别是高频段,显着提高定位精度,达到厘米级的水平。这将对各种应用,包括导航、位置服务和物联网设备的定位,产生积极影响。

6G特点

  1. 超高数据速率: 6G的一个主要目标是实现前所未有的数据速率。这意味着网络将能够提供比当前标准更高的下载和上传速度,以支持高清内容的无缝传输,同时满足未来对更大带宽需求的应用,如虚拟现实(VR)和增强现实(AR)。

5G的峰值数据吞吐量通常被设计为在20 Gbps左右,而6G将迈向更令人惊叹的1 Tbps(太比特每秒)的峰值数据速率。这是对比5G速度的显著提升,为未来的高度数据密集型应用提供了更大的带宽。

5G旨在提供用户体验数据速率为100 Mbps,而6G将将这一速率提高到1 Gbps(千兆比特每秒)。这将使用户能够更快地下载和上传数据,以支持更高质量的多媒体服务和应用。

由于更高的频谱效率,6G将比5G提高近一倍以上的速度。这种效率的提升对于满足未来对大容量、高速度连接的需求至关重要,尤其是在处理大规模视频、虚拟现实、增强现实等数据密集型应用时。

  1. 超低延迟: 6G致力于实现超低延迟,即数据从一个网络点传输到另一个点所需的时间。这对于需要即时响应的实时应用非常关键,例如自动驾驶汽车、远程手术和工业自动化。预计延迟将减少到毫秒甚至微秒的水平。

5G的设计目标是将时延降至1毫秒。这对于许多实时应用程序,如增强现实、虚拟现实和自动驾驶汽车等来说,已经是一个显著的提升。然而,6G将进一步将用户体验到的延迟降低到0.1毫秒以下,实现更加极致的超低延迟。

由于延迟的大幅减少,许多实时应用程序将获得更好的性能和功能。这对于需要即时响应的应用场景,如在线游戏、实时视频通话和远程协作等,将带来明显的改进。

超低延迟将使网络能够实现更迅速的紧急响应。这对于紧急情况下的通信和救援操作非常重要,例如在自然灾害发生时,网络可以更快速、更有效地协调救援活动。

  1. 海量连接: 6G旨在支持物联网(IoT)中预计将连接数十亿台设备的海量连接。为实现这一目标,网络需要进一步发展,以应对大规模设备之间的通信、数据传输和管理。

6G将更加专注于支持机器对机器的连接,强调物联网(IoT)和各种设备之间的通信。这对于未来智能城市、智能工厂、智能交通系统等应用来说至关重要,其中大量设备需要相互协调和通信。

  1. 能源效率: 可持续性是当前和未来网络发展的一个重要考虑因素。6G的设计将更加注重能源效率,通过优化网络基础设施和采用智能电源管理技术,以减少能源消耗,同时保持高性能连接。
  2. 人工智能集成: 6G将与人工智能(AI)密切结合,通过利用AI算法和机器学习技术来实现智能网络管理、资源分配和优化。这种集成有望提高网络的整体性能和效率,使其更具自适应性和智能性。

6G的优势

  1. 更快、更可靠的数据速度: 6G将实现更高的数据传输速度,为企业和消费者提供更快速、更可靠的互联网连接。这将促使新的应用和服务,如实时的3D全息视频流、超高清虚拟现实等。
  2. 更低的延迟: 6G将具有更低的延迟,即数据传输的时间将进一步缩短。这对于需要实时通信的应用非常关键,例如远程手术、虚拟会议和自动驾驶汽车等。
  3. 更广泛的设备和应用: 6G将支持更广泛范围的设备和应用,包括物联网中的大量连接设备、智能城市中的各种传感器和控制系统,以及新兴的技术领域,如增强现实和虚拟现实。
  4. 提高安全性和性能: 6G将利用人工智能和机器学习来提高网络的安全性和性能。这将增强网络的自我学习和自适应性,使其更具抵御网络攻击的能力,同时确保网络能够处理未来6G预计增加的大规模数据流量。
  5. 推动新兴技术和应用: 6G的引入将推动各种新兴技术和应用的发展,包括智能交通、医疗创新、工业自动化和元宇宙等。这将为社会带来更多创新和便利。

6G的潜在应用

  1. 实时全息视频会议: 6G的超高速度和低延迟将使实时全息视频会议成为可能。用户可以感觉到与对方面对面交流,这对于企业协作、在线教育和虚拟团队合作具有重要意义。

  1. 超高清虚拟现实(VR): 6G的高带宽和低延迟将推动超高清虚拟现实体验的发展。这将改善虚拟旅游、虚拟培训和虚拟游戏等领域的用户体验。
  2. 自动驾驶汽车: 6G的超低延迟对于自动驾驶汽车至关重要。实时通信将使汽车能够相互协作,共享实时交通和道路信息,提高自动驾驶汽车的安全性和效率。
  3. 远程手术: 6G的低延迟和高带宽将为远程手术提供支持。外科医生可以远程操控手术机器人进行手术,为无法到达医院的患者提供及时的医疗服务。
  4. 工业物联网(IIoT): 6G的大容量和广泛连接性将促进工业物联网的发展。在工业领域,各种传感器和设备可以实时通信,实现智能制造和工业自动化。
  5. 智能城市: 6G将为智能城市提供支持,实现各种城市基础设施的智能化管理,包括交通系统、能源管理、环境监测等。
  6. 医疗创新: 6G有望推动医疗领域的创新,包括远程医疗服务、医疗数据实时传输和医疗设备的互联互通,提高医疗保健的效率和可及性。

6G发展面临哪些挑战?

  1. 新频谱的需求: 为了实现更高的数据速率和容量,6G需要利用新的频谱范围。然而,目前可用的射频频段有限,而且这些频段通常由政府监管机构进行分配。因此,确保有足够的频谱来支持6G是一个重要的挑战。
  2. 新技术的发展: 6G的实现将依赖于一系列新技术的发展,包括太赫兹通信、新的无线电接入技术以及人工智能和机器学习在网络管理中的应用。这些技术目前仍在研发阶段,需要时间来完善和商业化。
  3. 提高安全性: 随着网络的发展,安全性变得尤为关键。6G需要更高水平的安全性,以应对日益复杂和普及的网络攻击。确保用户数据的隐私和网络的稳定性是一个必须解决的挑战。
  4. 部署成本: 6G的部署成本预计将比5G更高。引入新的频段和技术,以及更新现有的基础设施,都需要巨大的资金投入。这可能涉及到国家和企业层面的资金支持,以确保6G网络的建设和推广。
  5. 国际合作和标准制定: 6G的发展需要国际合作,以确保全球范围内的一致性和互操作性。同时,制定一系列统一的国际标准也是一个关键挑战,以便不同厂商的设备和网络可以无缝地协同工作。

5G与6G频谱比较

  1. 最大频率:
  • 5G:100 GHz
  • 6G:10太赫兹
  1. 最大带宽:
  • 5G:1 GHz
  • 6G:100 GHz
  1. 峰值数据速率:
  • 5G:10 Gbps(上传链路)至20 Gbps(下载链路)
  • 6G:100 Gbps至1 Tbps
  1. 平均用户体验数据速率:
  • 5G:100 Mbps
  • 6G:1 Gbps
  1. 峰值频谱效率:
  • 5G:30 b/s/Hz
  • 6G:60 b/s/Hz
  1. 用户体验的平均频谱效率:
  • 5G:0.03 b/s/Hz
  • 6G:3 b/s/Hz
  1. 移动支持:
  • 5G:最高500公里/小时
  • 6G:最高1000公里/小时
  1. 密度:
  • 5G:每平方米1台设备
  • 6G:每平方米100个设备
  1. 端到端延迟:
  • 5G:1至10毫秒
  • 6G:少于1毫秒
  1. 单频全双工传输:
  • 5G:没有
  • 6G:有
  1. 全球覆盖:
  • 5G:70多个国家已经推出5G,其中中国和美国在城市中处于领先地位
  • 6G:中国申请的6G专利最多,其次是美国

6G的商业化时间表

  • 标准制定:业内预计6G标准和规范的制定将从2025年开始¹。
  • 部署时间:预计6G系统将在2028年左右开始部署。
  • 商业化:预计6G的商业部署将在2030年左右实现³。

这些时间表是基于当前的技术发展和预测,可能会随着研究进展和行业动态而有所调整。6G技术的商业化还需要克服许多技术和政策上的挑战,包括频谱分配、网络架构设计、安全性和隐私保护等方面。

哪些公司正在领导6G技术研发?

全球有多家公司正在积极参与6G技术的研发,比如华为 (Huawei)、中兴通讯(ZTE)、中国移动、中国电信、中国联通、三星 (Samsung)、LG、NTT DOCOMO、高通 (Qualcomm)、AT&T、诺基亚 (Nokia)、爱立信 (Ericsson)等等。

中国针对6G做出的行动

中国政府在“第14次五年计划(2021-2025年)及2035年远景目标纲要”中明确提出了发展6G技术的目标²。

中国还成立了IMT-2030(6G)推进组,由主要通信运营商、基础设施供应商、IT公司和研究机构等约80家企业组成,致力于6G技术的研发和标准化工作。6G推进组已经发布了《6G网络架构展望》和《6G无线系统设计原则和典型特征》等技术方案,这些方案旨在为6G技术从万物互联向万物智联的转变提供技术路径。

中国工业和信息化部已宣布正在有序开展6G相关的技术试验,以推动6G创新发展。加快5G与XR、数字孪生、机器人等新产业新应用的融合发展,加速相关产业成熟,夯实6G应用基础。此外,推动信息通信企业与垂直行业企业密切沟通、协同合作,共同参与6G需求研究、技术研发、标准制定等全流程各环节,携手构建6G繁荣应用生态。

中国计划在2024年前完成6G相关主要技术的明确和概念机的测试验证,以提升技术能力。

预计到2026年,中国将开展典型应用场景和性能指标的确立,进行试制机的研发和基站功能性能的验证。

中国计划在2030年左右实现6G的商用化,而标准化制定的时间预计将在2025年。6G技术将引入新的应用场景,如通信与感知的结合、通信与人工智能的结合,以及泛在物联网等。这些技术不仅将连接人类,还将连接智能体,如机器人和元宇宙,进一步完善5G在行业中尚未解决的场景。

中国正在加强国际合作,与欧洲6G智慧网络和业务产业协会(6G-IA)、韩国6G论坛、印度通信标准开发协会(TSDSI)等签署合作备忘录,共同推进6G技术的发展。

此外,中国的通信巨头如华为和中国移动也在积极参与6G技术的研究和开发工作。华为是首家宣布开始研究6G的中国公司,随后与其他国内外企业和研究机构展开了多项合作。

据市场研究机构Market Research Future预计,到2040年,全球6G市场规模将超过3400亿美元,年复合增长率达58.1%。中国预计将成为全球最大的6G市场之一,全球近50%的6G专利申请来自中国。

总结

6G技术与5G相比,在速度上有显著的提升。根据研究和预测,6G的理论最高速度可达到1Tbps(即1000Gbps),这比5G的理论最高速度20Gbps快了50倍。此外,有报道称在中国的实验室环境中已经实现了206.25Gbps的速度。

6G将使用比5G更高的频率波段,操作在30GHz到300GHz的毫米波段,甚至可能达到300GHz到3000GHz的辐射波段。这些更高的频率波段将允许更快的数据传输速度和更大的带宽容量。

中国在6G技术的研发和创新方面正加速推进,预计在2030年左右实现商用。中国工业和信息化部已经指导成立了6G推进组,旨在为6G创新发展提供政策支持,并推动形成全球统一的6G标准。

6G技术不仅仅是速度的提升,它还将服务于社会管理和治理,以及智能体的应用。6G网络预计将是一个地面无线与卫星通信集成的全连接世界,不仅比5G更快、更可靠,还将推动移动通信与人工智能、感知、计算等技术的跨领域融合发展。

中国已经开始进行6G技术试验,并陆续开展了关于6G系统架构和技术方案的研究。最近,中国6G推进组发布了相关技术方案,为6G从万物互联走向万物智联提供了技术路径。

6G时代的基站将不仅支持通信信号的发送和接收,还将支持通信和感知,利用无线电波感知周边环境、物体形状和运动等,这不仅能提升通信性能,还将催生新业务。例如,基站可以进行升级改造,以支持低空经济和空域管理,或者用于交通管理。

6G将促进沉浸感更强的全息视频,实现物理世界、虚拟世界、人的世界三个世界的联动。今年6月,国际电信联盟完成了6G愿景需求建议书,明确了6G典型产品和关键能力指标,其中中国提出的5类6G典型场景和14个关键能力指标全部被采纳。

这些进展表明,中国在6G技术的发展上正处于全球领先地位,积极推进技术研发和创新,为未来的通信技术和应用开辟新的可能性。

朋友们,让我们一起期待中国在6G领域继续“雄霸全球”吧!


作者:wljslmz
来源:juejin.cn/post/7310143510102540297
收起阅读 »

Linus:批评 GitHub 代码合并【毫无用处的】

Linux 和 Git 的创建者 Linus Torvalds 批评 GitHub 创造了“毫无用处的代码合并”。 Torvalds 的评论可以在 Linux 开发邮件列表的存档中查看,该评论针对的是 Paragon Software 的创始人兼首席执行官 K...
继续阅读 »


Linux 和 Git 的创建者 Linus Torvalds 批评 GitHub 创造了“毫无用处的代码合并”。


Torvalds 的评论可以在 Linux 开发邮件列表的存档中查看,该评论针对的是 Paragon Software 的创始人兼首席执行官 Konstantin Komarov,关于为即将到来的 5.15 内核提交其读写 NTFS 驱动程序。


Torvalds 说,GitHub 创建了绝对无用的垃圾合并,你永远不应该使用 GitHub 接口来合并任何东西。


早在 2012 年,Torvalds 就对他为什么不使用 GitHub 进行拉取请求给出了更详细的解释:



GitHub 会丢弃所有相关信息,比如应该为要求我拉取的人提供一个有效的电子邮件地址。diffstat 也是有缺陷和无用的。




Git 附带了一个不错的拉取请求生成模块,但 github 决定用他们自己的完全劣质的版本替换它。因此,我认为 github 对此太无能了。托管很好,但拉取请求和在线提交编辑只是纯粹的垃圾。



Paragon Software 提交的驱动程序提高了与本机 Windows 文件系统 NTFS 的互操作性。提交过程在一年多前就开始了,但面临投诉,称其 27,000 行代码太大而无法审查。


提交了较小的块,但很明显,Paragon 一直在努力掌握 Linux 内核开发过程。最终 Torvalds 介入并在此过程中提供指导。


7 月,Torvalds 指出,与其将代码发布到 fsdevel 列表中,不如最终将其作为实际的拉取请求提交。


当时,Paragon 回应说:“也感谢您的澄清。直到现在,我们才真正清楚这个信息。我们刚刚发送了第 27 个补丁系列,它修复了针对当前 linux-next 的可构建性。在将拉取请求发送给您之前,我们需要几天时间来准备适当的拉取请求“。


这似乎比预期的要长一些,但 Paragon 于 2021 年 9 月 3 日星期五提交了拉取请求。该公司表示,“当前版本适用于普通/压缩/稀疏文件,并支持 acl、NTFS 日志重播。


除了建议不要使用 GitHub 的接口进行合并之外,Torvalds 还表示——虽然这次他会让它通过——拉取请求应该已经签署。


Torvalds 认为在一个完美的世界里,这将是一个 PGP 签名,可以通过信任链直接追溯到你。


最后拉取请求被合并,Torvalds 也作了最终评论。


Torvalds 认为最初的拉取往往有一些奇怪的地方,他现在会接受它们,为了继续发展,他需要正确地做事。


作者:ENG八戒
来源:juejin.cn/post/7312293783973675008
收起阅读 »

7个Js async/await高级用法

web
7个Js async/await高级用法 JavaScript的异步编程已经从回调(Callback)演进到Promise,再到如今广泛使用的async/await语法。后者不仅让异步代码更加简洁,而且更贴近同步代码的逻辑与结构,大大增强了代码的可读性与可维护...
继续阅读 »

7个Js async/await高级用法


JavaScript的异步编程已经从回调(Callback)演进到Promise,再到如今广泛使用的async/await语法。后者不仅让异步代码更加简洁,而且更贴近同步代码的逻辑与结构,大大增强了代码的可读性与可维护性。在掌握了基础用法之后,下面将介绍一些高级用法,以便充分利用async/await实现更复杂的异步流程控制。


1. async/await与高阶函数


当需要对数组中的元素执行异步操作时,可结合async/await与数组的高阶函数(如mapfilter等)。


// 异步过滤函数
async function asyncFilter(array, predicate) {
const results = await Promise.all(array.map(predicate));

return array.filter((_value, index) => results[index]);
}

// 示例
async function isOddNumber(n) {
await delay(100); // 模拟异步操作
return n % 2 !== 0;
}

async function filterOddNumbers(numbers) {
return asyncFilter(numbers, isOddNumber);
}

filterOddNumbers([1, 2, 3, 4, 5]).then(console.log); // 输出: [1, 3, 5]

2. 控制并发数


在处理诸如文件上传等场景时,可能需要限制同时进行的异步操作数量以避免系统资源耗尽。


async function asyncPool(poolLimit, array, iteratorFn) {
const result = [];
const executing = [];

for (const item of array) {
const p = Promise.resolve().then(() => iteratorFn(item, array));
result.push(p);

if (poolLimit <= array.length) {
const e = p.then(() => executing.splice(executing.indexOf(e), 1));
executing.push(e);
if (executing.length >= poolLimit) {
await Promise.race(executing);
}
}
}

return Promise.all(result);
}

// 示例
async function uploadFile(file) {
// 文件上传逻辑
}

async function limitedFileUpload(files) {
return asyncPool(3, files, uploadFile);
}

3. 使用async/await优化递归


递归函数是编程中的一种常用技术,async/await可以很容易地使递归函数进行异步操作。


// 异步递归函数
async function asyncRecursiveSearch(nodes) {
for (const node of nodes) {
await asyncProcess(node);
if (node.children) {
await asyncRecursiveSearch(node.children);
}
}
}

// 示例
async function asyncProcess(node) {
// 对节点进行异步处理逻辑
}

4. 异步初始化类实例


在JavaScript中,类的构造器(constructor)不能是异步的。但可以通过工厂函数模式来实现类实例的异步初始化。


class Example {
constructor(data) {
this.data = data;
}

static async create() {
const data = await fetchData(); // 异步获取数据
return new Example(data);
}
}

// 使用方式
Example.create().then((exampleInstance) => {
// 使用异步初始化的类实例
});

5. 在async函数中使用await链式调用


使用await可以直观地按顺序执行链式调用中的异步操作。


class ApiClient {
constructor() {
this.value = null;
}

async firstMethod() {
this.value = await fetch('/first-url').then(r => r.json());
return this;
}

async secondMethod() {
this.value = await fetch('/second-url').then(r => r.json());
return this;
}
}

// 使用方式
const client = new ApiClient();
const result = await client.firstMethod().then(c => c.secondMethod());

6. 结合async/await和事件循环


使用async/await可以更好地控制事件循环,像处理DOM事件或定时器等场合。


// 异步定时器函数
async function asyncSetTimeout(fn, ms) {
await new Promise(resolve => setTimeout(resolve, ms));
fn();
}

// 示例
asyncSetTimeout(() => console.log('Timeout after 2 seconds'), 2000);

7. 使用async/await简化错误处理


错误处理是异步编程中的重要部分。通过async/await,可以将错误处理的逻辑更自然地集成到同步代码中。


async function asyncOperation() {
try {
const result = await mightFailOperation();
return result;
} catch (error) {
handleAsyncError(error);
}
}

async function mightFailOperation() {
// 有可能失败的异步操作
}

function handleAsyncError(error) {
// 错误处理逻辑
}

通过以上七个async/await的高级用法,开发者可以在JavaScript中以更加声明式和直观的方式处理复杂的异步逻辑,同时保持代码整洁和可维护性。在实践中不断应用和掌握这些用法,能够有效地提升编程效率和项目的质量。


作者:慕仲卿
来源:juejin.cn/post/7311603994928513076
收起阅读 »

推送数据?也许你不需要 WebSocket

web
提到推送数据,大家可能会首先想到 WebSocket。 确实,WebSocket 能双向通信,自然也能做服务器到浏览器的消息推送。 但如果只是单向推送消息的话,HTTP 就有这种功能,它就是 Server Send Event。 WebSocket 的通信过程...
继续阅读 »

提到推送数据,大家可能会首先想到 WebSocket。


确实,WebSocket 能双向通信,自然也能做服务器到浏览器的消息推送。


但如果只是单向推送消息的话,HTTP 就有这种功能,它就是 Server Send Event。


WebSocket 的通信过程是这样的:



首先通过 http 切换协议,服务端返回 101 的状态码后,就代表协议切换成功。


之后就是 WebSocket 格式数据的通信了,一方可以随时向另一方推送消息。


而 HTTP 的 Server Send Event 是这样的:



服务端返回的 Content-Type 是 text/event-stream,这是一个流,可以多次返回内容。


Sever Send Event 就是通过这种消息来随时推送数据。


可能你是第一次听说 SSE,但你肯定用过基于它的应用。


比如你用的 CICD 平台,它的日志是实时打印的。


那它是如何实时传输构建日志的呢?


明显需要一段一段的传输,这种一般就是用 SSE 来推送数据。


再比如说 ChatGPT,它回答一个问题不是一次性给你全部的,而是一部分一部分的加载回答。


这也是基于 SSE。




知道了什么是 SSE 以及它的应用,我们来自己实现一下吧:


创建 nest 项目:


npx nest new sse-test


把它跑起来:


npm run start:dev


访问 http://localhost:3000 可以看到 hello world,代表服务器跑成功了:



然后在 AppController 添加一个 stream 接口:



这里不是通过 @Get、@Post 等装饰器标识,而是通过 @Sse 标识这是一个 event stream 类型的接口。


@Sse('stream')
stream() {
return new Observable((observer) => {
observer.next({ data: { msg: 'aaa'} });

setTimeout(() => {
observer.next({ data: { msg: 'bbb'} });
}, 2000);

setTimeout(() => {
observer.next({ data: { msg: 'ccc'} });
}, 5000);
});
}

返回的是一个 Observable 对象,然后内部用 observer.next 返回消息。


可以返回任意的 json 数据。


我们先返回了一个 aaa、过了 2s 返回了 bbb,过了 5s 返回了 ccc。


然后写个前端页面:


创建一个 react 项目:


npx create-react-app --template=typescript sse-test-frontend


在 App.tsx 里写如下代码:


import { useEffect } from 'react';

function App() {

useEffect(() => {
const eventSource = new EventSource('http://localhost:3000/stream');
eventSource.onmessage = ({ data }) => {
console.log('New message', JSON.parse(data));
};
}, []);

return (
<div>hello</div>
);
}

export default App;

这个 EventSource 是浏览器原生 api,就是用来获取 sse 接口的响应的,它会把每次消息传入 onmessage 的回调函数。


我们在 nest 服务开启跨域支持:



然后把 react 项目 index.tsx 里这几行代码删掉,它会导致额外的渲染:



执行 npm run start


因为 3000 端口被占用了,它会跑在 3001:



浏览器访问下:



看到一段段的响应了没?


这就是 Server Send Event。


在 devtools 里可以看到,响应的 Content-Type 是 text/event-stream:



然后在 EventStream 里可以看到每一次收到的消息:



这样,服务端就可以随时向网页推送消息了。


那它兼容性怎么样呢?


可以在 MDN 看到:



除了 ie、edge 外,其他浏览器都没任何兼容问题。


基本是可以放心用的。


那用在哪呢?


一些只需要服务端推送的场景就特别适合 Server Send Event。


比如这个站内信:



这种推送用 WebSocket 就没必要了,可以用 SSE 来做。


那连接断了怎么办呢?


不用担心,浏览器会自动重连。


这点和 WebSocket 不同,WebSocket 如果断开之后是需要手动重连的,而 SSE 不用。


再比如说日志的实时推送。


我们来测试下:


tail -f 命令可以实时看到文件的最新内容:



我们通过 child_process 模块的 exec 来执行这个命令,然后监听它的 stdout 输出:


const { exec } = require("child_process");

const childProcess = exec('tail -f ./log');

childProcess.stdout.on('data', (msg) => {
console.log(msg);
});

用 node 执行它:



然后添加一个 sse 的接口:


@Sse('stream2')
stream2() {
const childProcess = exec('tail -f ./log');

return new Observable((observer) => {
childProcess.stdout.on('data', (msg) => {
observer.next({ data: { msg: msg.toString() }});
})
});

监听到新的数据之后,把它返回给浏览器。


浏览器连接这个新接口:



测试下:



可以看到,浏览器收到了实时的日志。


很多构建日志都是通过 SSE 的方式实时推送的。


日志之类的只是文本,那如果是二进制数据呢?


二进制数据在 node 里是通过 Buffer 存储的。


const { readFileSync } = require("fs");

const buffer = readFileSync('./package.json');

console.log(buffer);


而 Buffer 有个 toJSON 方法:



这样不就可以通过 sse 的接口返回了么?


试一下:


@Sse('stream3')
stream3() {
return new Observable((observer) => {
const json = readFileSync('./package.json').toJSON();
observer.next({ data: { msg: json }});
});
}



确实可以。


也就是说,基于 sse,除了可以推送文本外,还可以推送任意二进制数据。


总结


服务端实时推送数据,除了用 WebSocket 外,还可以用 HTTP 的 Server Send Event。


只要 http 返回 Content-Type 为 text/event-stream 的 header,就可以通过 stream 的方式多次返回消息了。


它传输的是 json 格式的内容,可以用来传输文本或者二进制内容。


我们通过 Nest 实现了 sse 的接口,用 @Sse 装饰器标识方法,然后返回 Observe 对象就可以了。内部可以通过 observer.next 随时返回数据。


前端使用 EventSource 的 onmessage 来接收消息。


这个 api 的兼容性很好,除了 ie 外可以放心的用。


它的应用场景有很多,比如站内信、构建日志实时展示、chatgpt 的消息返回等。


再遇到需要消息推送的场景,不要直接 WebSocket 了,也许 Server Send Event 更合适呢?


作者:zxg_神说要有光
来源:juejin.cn/post/7272564663116759074
收起阅读 »

只会Vue的我,用两天学会了react,这个方法您也可以

web
背景 由于工作需要学习react框架;最开始看文档的时候感觉还挺难的。但当我看了半天文档以后才发现,原来react这样学才是最快的;前提是同学们会vue一类的框架哈。 该方法适用于会vue的同学们食用 我们在学习以前先去想一想,在vue中我们常用的方法是什么,...
继续阅读 »

背景


由于工作需要学习react框架;最开始看文档的时候感觉还挺难的。但当我看了半天文档以后才发现,原来react这样学才是最快的;前提是同学们会vue一类的框架哈。


该方法适用于会vue的同学们食用


我们在学习以前先去想一想,在vue中我们常用的方法是什么,我们遇到一些场景时在vue中是怎么做的。


当我们想到这儿的时候就会发现,对啊;既然vue是这样做的,那么react中是怎么做的呢?别急,我们一步一步对比着来。


这样岂不是更能理解哦!下面就让我们开始吧!


冲冲冲。。。


Vue梳理


在开始之前,我们先来梳理一下我们在vue中常用的API或者场景有哪些。


以下这几种就是我们常见的一些功能,主要是列表渲染、表单输入和一些计算属性等等;我们只需要根据原有的需要的功能去学习即可。



  • 组件传值

  • 获取DOM

  • 列表渲染

  • 条件渲染

  • class

  • 计算属性

  • 监听器

  • 表单输入

  • 模板


vue/react对比学习


组件传值


vue


// 父组件
<GoodsList v-if="!isGoodsIdShow" :goodsList="goodsList"/>
// 子组件 -- 通过props获取即可
props: {
goodsList:{
type:Array,
default:function(){
return []
}
}
}

react


// 父组件
export default function tab(props:any) {
const [serverUrl, setServerUrl] = useState<string | undefined>('https://');
console.log(props);
// 父组件接收子组件的值并修改
const changeMsg = (msg?:string) => {
setServerUrl(msg);
};

return(
<View className='tab'>
<View className='box'>
<TabName msg={serverUrl} changeMsg={changeMsg} />
</View>
</View>

)
}

// 子组件
function TabName(props){
console.log('props',props);
// 子传父
const handleClick = (msg:string) => {
props.changeMsg(msg);
};
return (
<View>
<Text>{props.msg}</Text>
<Button onClick={()=>{handleClick('77777')}}>测试</Button>
</View>

);
};

获取DOM


vue


this.$refs['ref']

react


// 声明ref    
const domRef = useRef<HTMLInputElement>(null);
// 通过点击事件选择input框
const handleBtnClick = ()=> {
domRef.current?.focus();
console.log(domRef,'domRef')
}

return(
<View className='home'>
<View className='box'>
<Input ref={domRef} type="text" />
<button onClick={handleBtnClick}>增加</button>
</View>
</View>

)

列表渲染


vue


<div v-for="(item, index) in mealList" :key="index">
{{item}}
</div>

react


//声明对象类型
type Coordinates = {
name:string,
age:number
};
// 对象
let [userState, setUserState] = useState<Coordinates>({ name: 'John', age: 30 });
// 数组
let [list, setList] = useState<Coordinates[]>([{ name: '李四', age: 30 }]);

// 如果你的 => 后面跟了一对花括号 { ,那你必须使用 return 来指定返回值!
const listItem = list.map((oi)=>{
return <View key={oi.age}>{oi.name}</View>
});

return (
{
list.map((oi)=>{
return <Text className='main-list-title' key={oi.age}>{oi.name}</Text>
})
}
<View>{ listItem }</View>
</View>
)

条件渲染


计算属性


vue


computed: {
userinfo() {
return this.$store.state.userinfo;
},
},

react


const [serverUrl, setServerUrl] = useState('https://localhost:1234');
let [age, setAge] = useState(2);

const name = useMemo(() => {
return serverUrl + " " + age;
}, [serverUrl]);
console.log(name) // https://localhost:1234 2

监听器


vue


watch: {
// 保证自定义菜单始终显示在页面中
customContextmenuTop(top) {
...相关操作
}
},

react


import { useEffect, useState } from 'react';

export default function home() {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
const [age, setAge] = useState(2);

/**
* useEffect第二个参数中所传递的值才会进行根据值的变化而出发;
* 如果没有穿值的话,就不会监听数据变化
*/

useEffect(()=>{
if (age !== 5) {
setAge(++age)
}
},[age])

useEffect(()=>{
if(serverUrl !== 'w3c') {
setServerUrl('w3c');
}
},[serverUrl])

return(78)
}

总结


从上面的方法示例我们可以得出一个结论:在其他框架(自己会的)中常用到的方法或者场景进行针对性的学习即可。


这样的好处是你能快速的上手开发,然后在实际开发场景中遇到解决不了的问题再去查文档或者百度。


这只是我的一点小小的发现,哈哈哈。。。


如果对你有感触的话,可以尝试一下这个方法;我觉得还是很不错的


注意:react推荐函数式组件开发,不推荐类组件开发,我在上面没有说明,大家也可以去文档看看,类组件和函数组件还是有很大差别的,如:函数组件没有生命周期,一般使用监听来完成的,监听的使用方法还是有所不同,大家可以具体的去试试,我在这儿也是告诉大家一些方法;具体去学了才是你的。


为了方便自己学习记录,以及给大家提供思路,我下期给大家带来 vite + ts + react的搭建


作者:雾恋
来源:juejin.cn/post/7268844150233219107
收起阅读 »

一个大专生工作总结

2020刚入大学,学的云计算方向专业,也成功当时班级的学委,从那时候开始各种学习计算机像方面技术,学的比较杂,感觉啥都学。运维类的redhat centos7 ,前端web,最基础的 HTML、CSS、JavaScript,后端就从比较感兴趣的JAVA开始学,...
继续阅读 »

2020刚入大学,学的云计算方向专业,也成功当时班级的学委,从那时候开始各种学习计算机像方面技术,学的比较杂,感觉啥都学。运维类的redhat centos7 ,前端web,最基础的 HTML、CSS、JavaScript,后端就从比较感兴趣的JAVA开始学,学到后面越学越学不动,然后转战Python,学会了爬虫,从这时候开始疯狂在网络上找资源学,疯狂阅览互联网走势,还有各种好玩的技术。刚开始学计算机的应该都会想过以后当一名黑客吧。学习大半个学期逐渐熟悉了互联网大致内容了,发现自己不适合。学不动根本学不动,觉得自己只能朝一个方向发展了。


大一


自己也很不错,也非常爱学,也拿到了人生第一个奖学金,老师都对我印象也挺不错的,大一学的专业课大部分我都会,老师提出的问题,我都能一一回答上来,还能讲一两个解决方式。我们老师还好奇还问我你学过吗,我就说基本都学过,底下同学都投来羡慕眼光,那是应该算在我在班级第一次高光时刻。


img_v2_2f1640ed-5668-4b27-a440-97259463424g.gif


大二上


大二开始迷失自我了,开始翘课和兄弟出去玩耍喝酒,那时候我也追上我喜欢的女孩,也成了男女朋友,就开始不对学习感兴趣,天天除了玩就是玩。接下来就是期末考试直线下滑,辅导员开始找我谈话了,讲不少人生大道理,虽然我没听进去多少,但是我还是知道不能再继续颓废下去了。


v2_0285f304-80f0-4eac-8f9d-e36b3cf6635g.gif


大二下


开始思考人生了,觉得上了大学应该不留遗憾,开始考各类计算机证书:网络工程师中级证书、HCIA、HCIP、云计算中级证书,等之类没有含金量证书。当时准备冲击红帽认证后来疫情原因,也不让出校,就没有冲击欲望了。就参加计算机比赛去了,我记得当时总共参加了三个比赛,院系一等、B类二等、我最期望的A类比赛我苦学了大半个学期,天天待在机房里学,因为这个比赛东道主在我们学校举行,懂了都懂,大差不差也能拿省一,省一可以免试专升本。然后可以去一本高校读本科,因为疫情取消了,什么都取消了。


心里虽然不是个滋味!人生还是要继续的。


v2_4720fea9-0838-4295-bbbb-8254eaa782bg.jpg


大三


专科生大学基本都是2+1,两年在学校,半年实习,才能拿到毕-业-证!就这样2022年10月开始思考是否专升本问题,思来想去两种方案:假如考上还要继续上俩年大学,第二种早点进入社会工作不断提高自己工作经验,也能搞到大钱。我还是选择了第二种方案,开始写自己简历,然后疯狂在BOSS 智联招聘 全程无忧等招聘平台疯狂投送简历,然后就有一家比较大的企业看上我了,应聘的是网络运维工程师,实习4k转正5k、双休、包吃包住、免费住人才公寓,不快不慢就实习了6个月,也拿到了毕-业-证书,最后转正签劳动合同的时候还是选择了离开。
原因还是:工作学不到东西,加上工作挺舒坦的,每天基本没事,基本都是活少聊天多,就是这样。人是有欲望的,身边的好多朋友转正之后薪资7K-9K的,感觉自己不能再继续荒废下去了。


v2_c9054330-c8f5-4f29-8ce2-0306f9c9903g.jpg


辞职后,也存了一点钱,也玩了一个多月,开始找工作,互联网工作真的难找,加上我现在不是实习生了,对自己薪资要求也比较高,我就开始疯狂的学,也要拿出自己能出的手东西,就自己做了个人网站博客,买了服务器,买了域名。可是呢还是找不到工作,我就开始不找网络工程师方面工作了,简历到处投,直到有家比较大公司桌面运维工作找到了我。经过一两轮面试,合格通过了,但是薪资也谈的不太理想只有6k。
三线城市6K确实足够生活的,不过我还是要继续努力。helpdesk只是我的暂时的工作,还是要更高方向发展。


加油 加油 加油 !!!!


v2_0cc05c0e-8aec-41f8-b500-32c49e76270g.jpg


作者:一码归亿码
来源:juejin.cn/post/7312352526706524201
收起阅读 »

京东一面:post为什么会发送两次请求?🤪🤪🤪

在前段时间的一次面试中,被问到了一个如标题这样的问题。要想好好地去回答这个问题,这里牵扯到的知识点也是比较多的。 那么接下来这篇文章我们就一点一点开始引出这个问题。 同源策略 在浏览器中,内容是很开放的,任何资源都可以接入其中,如 JavaScript 文件、...
继续阅读 »

在前段时间的一次面试中,被问到了一个如标题这样的问题。要想好好地去回答这个问题,这里牵扯到的知识点也是比较多的。


那么接下来这篇文章我们就一点一点开始引出这个问题。


同源策略


在浏览器中,内容是很开放的,任何资源都可以接入其中,如 JavaScript 文件、图片、音频、视频等资源,甚至可以下载其他站点的可执行文件。


但也不是说浏览器就是完全自由的,如果不加以控制,就会出现一些不可控的局面,例如会出现一些安全问题,如:



  • 跨站脚本攻击(XSS)

  • SQL 注入攻击

  • OS 命令注入攻击

  • HTTP 首部注入攻击

  • 跨站点请求伪造(CSRF)

  • 等等......


如果这些都没有限制的话,对于我们用户而言,是相对危险的,因此需要一些安全策略来保障我们的隐私和数据安全。


这就引出了最基础、最核心的安全策略:同源策略。


什么是同源策略


同源策略是一个重要的安全策略,它用于限制一个源的文档或者它加载的脚本如何能与另一个源的资源进行交互。


如果两个 URL 的协议、主机和端口都相同,我们就称这两个 URL 同源。



  • 协议:协议是定义了数据如何在计算机内和之间进行交换的规则的系统,例如 HTTP、HTTPS。

  • 主机:是已连接到一个计算机网络的一台电子计算机或其他设备。网络主机可以向网络上的用户或其他节点提供信息资源、服务和应用。使用 TCP/IP 协议族参与网络的计算机也可称为 IP 主机。

  • 端口:主机是计算机到计算机之间的通信,那么端口就是进程到进程之间的通信。


如下表给出了与 URL http://store.company.com:80/dir/page.html 的源进行对比的示例:


URL结果原因
http://store.company.com:80/dir2/page.html同源只有路径不同
http://store.company.com:80/dir/inner/another.html同源只有路径不同
https://store.company.com:443/secure.html不同源协议不同,HTTP 和 HTTPS
http://store.company.com:81/dir/etc.html不同源端口不同
http://news.company.com:80/dir/other.html不同源主机不同

同源策略主要表现在以下三个方面:DOM、Web 数据和网络。



  • DOM 访问限制:同源策略限制了网页脚本(如 JavaScript)访问其他源的 DOM。这意味着通过脚本无法直接访问跨源页面的 DOM 元素、属性或方法。这是为了防止恶意网站从其他网站窃取敏感信息。

  • Web 数据限制:同源策略也限制了从其他源加载的 Web 数据(例如 XMLHttpRequest 或 Fetch API)。在同源策略下,XMLHttpRequest 或 Fetch 请求只能发送到与当前网页具有相同源的目标。这有助于防止跨站点请求伪造(CSRF)等攻击。

  • 网络通信限制:同源策略还限制了跨源的网络通信。浏览器会阻止从一个源发出的请求获取来自其他源的响应。这样做是为了确保只有受信任的源能够与服务器进行通信,以避免恶意行为。


出于安全原因,浏览器限制从脚本内发起的跨源 HTTP 请求,XMLHttpRequest 和 Fetch API,只能从加载应用程序的同一个域请求 HTTP 资源,除非使用 CORS 头文件


CORS


对于浏览器限制这个词,要着重解释一下:不一定是浏览器限制了发起跨站请求,也可能是跨站请求可以正常发起,但是返回结果被浏览器拦截了。


浏览器将不同域的内容隔离在不同的进程中,网络进程负责下载资源并将其送到渲染进程中,但由于跨域限制,某些资源可能被阻止加载到渲染进程。如果浏览器发现一个跨域响应包含了敏感数据,它可能会阻止脚本访问这些数据,即使网络进程已经获得了这些数据。CORB 的目标是在渲染之前尽早阻止恶意代码获取跨域数据。



CORB 是一种安全机制,用于防止跨域请求恶意访问跨域响应的数据。渲染进程会在 CORB 机制的约束下,选择性地将哪些资源送入渲染进程供页面使用。



例如,一个网页可能通过 AJAX 请求从另一个域的服务器获取数据。虽然某些情况下这样的请求可能会成功,但如果浏览器检测到请求返回的数据可能包含恶意代码或与同源策略冲突,浏览器可能会阻止网页访问返回的数据,以确保用户的安全。


跨源资源共享(Cross-Origin Resource Sharing,CORS)是一种机制,允许在受控的条件下,不同源的网页能够请求和共享资源。由于浏览器的同源策略限制了跨域请求,CORS 提供了一种方式来解决在 Web 应用中进行跨域数据交换的问题。


CORS 的基本思想是,服务器在响应中提供一个标头(HTTP 头),指示哪些源被允许访问资源。浏览器在发起跨域请求时会先发送一个预检请求(OPTIONS 请求)到服务器,服务器通过设置适当的 CORS 标头来指定是否允许跨域请求,并指定允许的请求源、方法、标头等信息。


简单请求


不会触发 CORS 预检请求。这样的请求为 简单请求,。若请求满足所有下述条件,则该请求可视为 简单请求



  1. HTTP 方法限制:只能使用 GET、HEAD、POST 这三种 HTTP 方法之一。如果请求使用了其他 HTTP 方法,就不再被视为简单请求。

  2. 自定义标头限制:请求的 HTTP 标头只能是以下几种常见的标头:AcceptAccept-LanguageContent-LanguageLast-Event-IDContent-Type(仅限于 application/x-www-form-urlencodedmultipart/form-datatext/plain)。HTML 头部 header field 字段:DPR、Download、Save-Data、Viewport-Width、WIdth。如果请求使用了其他标头,同样不再被视为简单请求。

  3. 请求中没有使用 ReadableStream 对象。

  4. 不使用自定义请求标头:请求不能包含用户自定义的标头。

  5. 请求中的任意 XMLHttpRequestUpload 对象均没有注册任何事件监听器;XMLHttpRequestUpload 对象可以使用 XMLHttpRequest.upload 属性访问


预检请求


非简单请求的 CORS 请求,会在正式通信之前,增加一次 HTTP 查询请求,称为 预检请求


需预检的请求要求必须首先使用 OPTIONS 方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。预检请求 的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。


例如我们在掘金上删除一条沸点:


20230822094049


它首先会发起一个预检请求,预检请求的头信息包括两个特殊字段:



  • Access-Control-Request-Method:该字段是必须的,用来列出浏览器的 CORS 请求会用到哪些 HTTP 方法,上例是 POST。

  • Access-Control-Request-Headers:该字段是一个逗号分隔的字符串,指定浏览器 CORS 请求会额外发送的头信息字段,上例是 content-type,x-secsdk-csrf-token

  • access-control-allow-origin:在上述例子中,表示 https://juejin.cn 可以请求数据,也可以设置为* 符号,表示统一任意跨源请求。

  • access-control-max-age:该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是 1 天(86408 秒),即允许缓存该条回应 1 天(86408 秒),在此期间,不用发出另一条预检请求。


一旦服务器通过了 预检请求,以后每次浏览器正常的 CORS 请求,就都跟简单请求一样,会有一个 Origin 头信息字段。服务器的回应,也都会有一个 Access-Control-Allow-Origin 头信息字段。


20230822122441


上面头信息中,Access-Control-Allow-Origin 字段是每次回应都必定包含的。


附带身份凭证的请求与通配符


在响应附带身份凭证的请求时:



  • 为了避免恶意网站滥用 Access-Control-Allow-Origin 头部字段来获取用户敏感信息,服务器在设置时不能将其值设为通配符 *。相反,应该将其设置为特定的域,例如:Access-Control-Allow-Origin: https://juejin.cn。通过将 Access-Control-Allow-Origin 设置为特定的域,服务器只允许来自指定域的请求进行跨域访问。这样可以限制跨域请求的范围,避免不可信的域获取到用户敏感信息。

  • 为了避免潜在的安全风险,服务器不能将 Access-Control-Allow-Headers 的值设为通配符 *。这是因为不受限制的请求头可能被滥用。相反,应该将其设置为一个包含标头名称的列表,例如:Access-Control-Allow-Headers: X-PINGOTHER, Content-Type。通过将 Access-Control-Allow-Headers 设置为明确的标头名称列表,服务器可以限制哪些自定义请求头是允许的。只有在允许的标头列表中的头部字段才能在跨域请求中被接受。

  • 为了避免潜在的安全风险,服务器不能将 Access-Control-Allow-Methods 的值设为通配符 *。这样做将允许来自任意域的请求使用任意的 HTTP 方法,可能导致滥用行为的发生。相反,应该将其设置为一个特定的请求方法名称列表,例如:Access-Control-Allow-Methods: POST, GET。通过将 Access-Control-Allow-Methods 设置为明确的请求方法列表,服务器可以限制哪些方法是允许的。只有在允许的方法列表中的方法才能在跨域请求中被接受和处理。

  • 对于附带身份凭证的请求(通常是 Cookie),


这是因为请求的标头中携带了 Cookie 信息,如果 Access-Control-Allow-Origin 的值为 *,请求将会失败。而将 Access-Control-Allow-Origin 的值设置为 https://juejin。cn,则请求将成功执行。


另外,响应标头中也携带了 Set-Cookie 字段,尝试对 Cookie 进行修改。如果操作失败,将会抛出异常。


为什么本地使用 webpack 进行 dev 开发时,不需要服务器端配置 cors 的情况下访问到线上接口?


当你在本地通过 Ajax 或其他方式请求线上接口时,由于浏览器的同源策略,会出现跨域的问题。但是在服务器端并不会出现这个问题。


它是通过 Webpack Dev Server 来实现这个功能。当你在浏览器中发送请求时,请求会先被 Webpack Dev Server 捕获,然后根据你的代理规则将请求转发到目标服务器,目标服务器返回的数据再经由 Webpack Dev Server 转发回浏览器。这样就绕过了浏览器的同源策略限制,使你能够在本地开发环境中访问线上接口。


参考文章



总结


预检请求是在进行跨域资源共享 CORS 时,由浏览器自动发起的一种 OPTIONS 请求。它的存在是为了保障安全,并允许服务器决定是否允许跨域请求。


跨域请求是指在浏览器中向不同域名、不同端口或不同协议的资源发送请求。出于安全原因,浏览器默认禁止跨域请求,只允许同源策略。而当网页需要进行跨域请求时,浏览器会自动发送一个预检请求,以确定是否服务器允许实际的跨域请求。


预检请求中包含了一些额外的头部信息,如 Origin 和 Access-Control-Request-Method 等,用于告知服务器实际请求的方法和来源。服务器收到预检请求后,可以根据这些头部信息,进行验证和授权判断。如果服务器认可该跨域请求,将返回一个包含 Access-Control-Allow-Origin 等头部信息的响应,浏览器才会继续发送实际的跨域请求。


使用预检请求机制可以有效地防范跨域请求带来的安全风险,保护用户数据和隐私。


整个完整的请求流程有如下图所示:


20230822122544


最后分享两个我的两个开源项目,它们分别是:



这两个项目都会一直维护的,如果你也喜欢,欢迎 star 🥰🥰🥰


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