注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

工作6年了日期时间格式化还在写YYYY疯狂给队友埋雷

前言 哈喽小伙伴们好久不见,今天来个有意思的雷,看你有没有埋过。 正文 不多说废话,公司最近来了个外地回来的小伙伴,在广州工作过6年,也是一名挺有经验的开发。 他提交的代码被小组长发现有问题,给打回了,原因是里面日期格式化的用法有问题,用的Simpl...
继续阅读 »

前言



哈喽小伙伴们好久不见,今天来个有意思的雷,看你有没有埋过。



正文



不多说废话,公司最近来了个外地回来的小伙伴,在广州工作过6年,也是一名挺有经验的开发。




他提交的代码被小组长发现有问题,给打回了,原因是里面日期格式化的用法有问题,用的SimpleDateFormat,但不知道是手误还是什么原因,格式用了YYYY-MM-dd。




这种写法埋了一个不大不小的雷。




用一段测试代码就可以展示出来问题



1.jpg



打印结果如下:



2.jpg



很明显,使用YYYY时,2023年变成了2024年,在正常情况下可能没问题,但是在跨年的时候大概率就会有问题了。




原因比较简单,与小写的yyyy不同,大写的YYYY表示一个基于周的年份。它是根据周计算的年份,而不是基于日历的年份。通常情况下,两者的结果是相同的,但在跨年的第一周或最后一周可能会有差异。




比如我如果换成2023-12-30又不会有问题了



3.jpg



另外,Hutool工具类本身是对Java一些工具的封装,DateUtil里面也有用到SimpleDateFormat,因此也会存在类似的问题。



4.jpg



避免这个问题的方法也十分简单,要有公用的格式类,所有使用日期格式的地方都引用这个类,这个类中就定义好yyyy-MM-dd想给的格式即可,这样就不会出现有人手误给大家埋雷了。



总结




  1. 日期时间格式统一使用yyyy小写;

  2. 日期格式要规定大家都引用定义好的工具类,避免有人手误打错。




最后再回头想一想,这种小问题并不会马上暴露出来,倘若没有被发现,到了明年元旦,刚好跨年的时候,是不是就要坑死一堆人了。



作者:程序员济癫
来源:juejin.cn/post/7269013062677823528
收起阅读 »

HTML问题:如何实现分享URL预览?

web
前端功能问题系列文章,点击上方合集↑ 序言 大家好,我是大澈! 本文约2100+字,整篇阅读大约需要3分钟。 本文主要内容分三部分,如果您只需要解决问题,请阅读第一、二部分即可。如果您有更多时间,进一步学习问题相关知识点,请阅读至第三部分。 感谢关注微信公众号...
继续阅读 »

前端功能问题系列文章,点击上方合集↑


序言


大家好,我是大澈!


本文约2100+字,整篇阅读大约需要3分钟。


本文主要内容分三部分,如果您只需要解决问题,请阅读第一、二部分即可。如果您有更多时间,进一步学习问题相关知识点,请阅读至第三部分。


感谢关注微信公众号:“程序员大澈”,然后加入问答群,从此让解决问题的你不再孤单!


1. 需求分析


为了提高用户对页面链接分享的体验,需要对分享链接做一些处理。


以 Telegram(国外某一通讯软件) 为例,当在 Telegram 上分享已做过处理的链接时,它会自动尝试获取链接的预览信息,包括标题、描述和图片。


如此当接收者看到时,可以立即获取到分享链接的一些重要信息。这有助于接收者更好地了解链接的内容,决定是否点击查看详细内容。


图片


2. 实现步骤


2.1 实现前的说明


对于URL分享预览这个功能问题,在项目中挺常用的,只不过今天我们是以一些框架分享API的底层原理角度来讲的。


实现这种功能的关键,是在分享的链接中嵌入适当的元数据信息,应用软件会自动解析,请求分享链接的预览信息,并根据返回的元数据生成预览卡片。


对于国内的应用软件,目前我试过抖音,它可以实现分享和复制粘贴都自动解析,而微信、QQ等只能实现分享的自动解析。


对于国外的应用软件,我只实验过Telegram,它可以实现分享和复制粘贴都自动解析,但我想FacebookTwitterInstagram这些应用应该也都是可以的。


2.2 实现代码


实现URL链接的分享预览,你可以使用 Open Graph协议或 Twitter Cards,然后在 HTML 的 标签中,添加以下 meta 标签来定义链接预览的信息。


使用时,将所有meta全部复制过去,然后根据需求进行自定义即可。


还要注意两点,确保你页面的服务器正确配置了 SSL 证书,以及确保链接的URL有效(即:服务器没有做白名单限制)。


<head>
  
  <meta property="og:title" content="预览标题">
  <meta property="og:description" content="预览描述">
  <meta property="og:image:width" content="图片宽度">
  <meta property="og:image:height" content="图片高度">
  <meta property="og:image" content="预览图片的URL">
  <meta property="og:url" content="链接的URL">
  
  
  <meta name="twitter:card" content="summary">
  <meta name="twitter:title" content="预览标题">
  <meta name="twitter:description" content="预览描述">
  <meta property="twitter:image:width" content="图片宽度">
  <meta property="twitter:image:height" content="图片高度">
  <meta name="twitter:image" content="预览图片的URL">
  <meta name="twitter:url" content="链接的URL">
head>

下面我们做一些概念的整理、总结和学习。


3. 问题详解


3.1 什么是Open Graph协议?


Open Graph协议是一种用于在社交媒体平台上定义和传递网页元数据的协议。它由 Facebook 提出,并得到了其他社交媒体平台的支持和采纳。Open Graph 协议旨在标准化网页上的元数据,使网页在社交媒体上的分享和预览更加一致和可控。


通过在网页的 HTML  标签中添加特定的 meta 标签,使用 Open Graph 协议可以定义和传递与网页相关的元数据信息,如标题、描述、图片等。这些元数据信息可以被社交媒体平台解析和使用,用于生成链接预览、分享内容和提供更丰富的社交图谱。


使用 Open Graph 协议,网页的所有者可以控制链接在社交媒体上的预览内容,确保链接在分享时显示的标题、描述和图片等信息准确、有吸引力,并能够准确传达链接的主题和内容。这有助于提高链接的点击率、转化率和用户体验。


Open Graph 协议定义了一组标准的 meta 标签属性,如 og:titleog:descriptionog:image 等,用于提供链接预览所需的元数据信息。通过在网页中添加这些 meta 标签并设置相应的属性值,可以实现链接预览在社交媒体平台上的一致展示。


需要注意的是,Open Graph 协议是一种开放的标准,并不限于 Facebook 平台。其他社交媒体平台,如 Twitter、LinkedIn 等,也支持使用 Open Graph 协议定义和传递网页元数据,以实现链接预览的一致性。


图片


3.2 什么是Twitter Cards?


Twitter Cards 是一种由 Twitter 推出的功能,它允许网站所有者在他们的网页上定义和传递特定的元数据,以便在 Twitter 上分享链接时生成更丰富和吸引人的预览卡片。通过使用 Twitter Cards,网页链接在 Twitter 上的分享可以展示标题、描述、图片、链接和其他相关信息,以提供更具吸引力和信息丰富的链接预览。


Twitter Cards 提供了多种类型的卡片,以适应不同类型的内容和需求。以下是 Twitter Cards 的一些常见类型:



  • Summary CardSummary Card 类型的卡片包含一个标题、描述和可选的图片。它适用于分享文章、博客帖子等内容。

  • Summary Card with Large ImageSummary Card with Large Image 类型的卡片与 Summary Card 类型类似,但图片尺寸更大,更突出地展示在卡片上。

  • App CardApp Card 类型的卡片用于分享移动应用程序的信息。它包含应用的名称、图标、描述和下载按钮,以便用户可以直接从预览卡片中下载应用。

  • Player CardPlayer Card 类型的卡片用于分享包含媒体播放器的内容,如音频文件、视频等。它允许在预览卡片上直接播放媒体内容。


通过在网页的 HTML  标签中添加特定的 meta 标签,使用 Twitter Cards 可以定义和传递与链接预览相关的元数据信息,如标题、描述、图片、链接等。这些元数据信息将被 Twitter 解析和使用,用于生成链接预览卡片。


使用 Twitter Cards 可以使链接在 Twitter 上的分享更加吸引人和信息丰富,提高链接的点击率和用户参与度。它为网站所有者提供了更多控制链接在 Twitter 上展示的能力,并提供了一种更好的方式来呈现他们的内容。


图片


图片


作者:程序员大澈
来源:juejin.cn/post/7310112330663231515
收起阅读 »

拼多多算法题,是清华考研真题!

写在前面 在 LeetCode 上有一道"备受争议"的题目。 该题长期作为 拼多多题库中的打榜题 : 据同学们反映,该题还是 清华大学 和 南京大学 考研专业课中的算法题。 其中南京大学的出题人,还真贴心地针对不同解法,划分不同分值: 细翻评论区。 不...
继续阅读 »

写在前面


在 LeetCode 上有一道"备受争议"的题目。


该题长期作为 拼多多题库中的打榜题


出现频率拉满


据同学们反映,该题还是 清华大学南京大学 考研专业课中的算法题。



其中南京大学的出题人,还真贴心地针对不同解法,划分不同分值:




细翻评论区。


不仅是拼多多,该题还在诸如 神州信息滴滴出行 这样的互联网大厂笔试中出现过:





但,这都不是这道题"备受争议"的原因。


这道题最魔幻的地方是:常见解法可做到 O(n)O(n) 时间,O(1)O(1) 空间,而进阶做法最快也只能做到 O(n)O(n) 时间,O(logn)O(\log{n}) 空间


称作"反向进阶"也不为过。


接下来,我将从常规解法的两种理解入手,逐步进阶到考研/笔面中分值更高的进阶做法,帮助大家在这题上做到尽善尽美。


毕竟在一道算法题上做到极致,比背一段大家都会"八股文",在笔面中更显价值。


题目描述


平台:LeetCode


题号:LCR 161 或 53


给你一个整数数组 nums,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。


子数组是数组中的一个连续部分。


示例 1:


输入:nums = [-2,1,-3,4,-1,2,1,-5,4]

输出:6

解释:连续子数组 [4,-1,2,1] 的和最大,为 6

示例 2:


输入:nums = [1]

输出:1

示例 3:


输入:nums = [5,4,-1,7,8]

输出:23

提示:



  • 1<=nums.length<=1051 <= nums.length <= 10^5

  • 104<=nums[i]<=104-10^4 <= nums[i] <= 10^4


进阶:如果你已经实现复杂度为 O(n)O(n) 的解法,尝试使用更为精妙的分治法求解。


前缀和 or 线性 DP


当要我们求「连续段」区域和的时候,要很自然的想到「前缀和」。


所谓前缀和,是指对原数组“累计和”的描述,通常是指一个与原数组等长的数组。


设前缀和数组为 sumsum 的每一位记录的是从「起始位置」到「当前位置」的元素和。


例如 sum[x]sum[x] 是指原数组中“起始位置”到“位置 x”这一连续段的元素和。


有了前缀和数组 sum,当我们求连续段 [i,j][i, j] 的区域和时,利用「容斥原理」,便可进行快速求解。


通用公式:ans = sum[j] - sum[i - 1]



由于涉及 -1 操作,为减少边界处理,我们可让前缀和数组下标从 11 开始。在进行快速求和时,再根据原数组下标是否从 11 开始,决定是否进行相应的下标偏移。


学习完一维前缀和后,回到本题。


先用 nums 预处理出前缀和数组 sum,然后在遍历子数组右端点 j 的过程中,通过变量 m 动态记录已访问的左端点 i 的前缀和最小值。最终,在所有 sum[j] - m 的取值中选取最大值作为答案。


代码实现上,我们无需明确计算前缀和数组 sum,而是使用变量 s 表示当前累计的前缀和(充当右端点),并利用变量 m 记录已访问的前缀和的最小值(充当左端点)即可。


本题除了将其看作为「前缀和裸题用有限变量进行空间优化」以外,还能以「线性 DP」角度进行理解。


定义 f[i]f[i] 为考虑前 ii 个元素,且第 nums[i]nums[i] 必选的情况下,形成子数组的最大和。


不难发现,仅考虑前 ii 个元素,且 nums[i]nums[i] 必然参与的子数组中。要么是 nums[i]nums[i] 自己一个成为子数组,要么与前面的元素共同组成子数组。


因此,状态转移方程:


f[i]=max(f[i1]+nums[i],nums[i])f[i] = \max(f[i - 1] + nums[i], nums[i])

由于 f[i]f[i] 仅依赖于 f[i1]f[i - 1] 进行转移,可使用有限变量进行优化,因此写出来的代码也是和上述前缀和角度分析的类似。


Java 代码:


class Solution {
public int maxSubArray(int[] nums) {
int s = 0, m = 0, ans = -10010;
for (int x : nums) {
s += x;
ans = Math.max(ans, s - m);
m = Math.min(m, s);
}
return ans;
}
}

C++ 代码:


class Solution {
public:
int maxSubArray(vector<int>& nums) {
int s = 0, m = 0, ans = -10010;
for (int x : nums) {
s += x;
ans = max(ans, s - m);
m = min(m, s);
}
return ans;
}
};

Python 代码:


class Solution:
def maxSubArray(self, nums: List[int]) -> int:
s, m, ans = 0, 0, -10010
for x in nums:
s += x
ans = max(ans, s - m)
m = min(m, s)
return ans

TypeScript 代码:


function maxSubArray(nums: number[]): number {
let s = 0, m = 0, ans = -10010;
for (let x of nums) {
s += x;
ans = Math.max(ans, s - m);
m = Math.min(m, s);
}
return ans;
};


  • 时间复杂度:O(n)O(n)

  • 空间复杂度:O(1)O(1)


分治


“分治法”的核心思路是将大问题拆分成更小且相似的子问题,通过递归解决这些子问题,最终合并子问题的解来得到原问题的解。


实现分治,关键在于对“递归函数”的设计(入参 & 返回值)。


在涉及数组的分治题中,左右下标 lr 必然会作为函数入参,因为它能用于表示当前所处理的区间,即小问题的范围。


对于本题,仅将最大子数组和(答案)作为返回值并不足够,因为单纯从小区间的解无法直接推导出大区间的解,我们需要一些额外信息来辅助求解。


具体的,我们可以将返回值设计成四元组,分别代表 区间和前缀最大值后缀最大值最大子数组和,用 [sum, lm, rm, max] 表示。


有了完整的函数签名 int[] dfs(int[] nums, int l, int r),考虑如何实现分治:



  1. 根据当前区间 [l,r][l, r] 的长度进行分情况讨论:

    1. l=rl = r,只有一个元素,区间和为 nums[l]nums[l],而 最大子数组和、前缀最大值 和 后缀最大值 由于允许“空数组”,因此均为 max(nums[l],0)\max(nums[l], 0)

    2. 否则,将当前问题划分为两个子问题,通常会划分为两个相同大小的子问题,划分为 [l,mid][l, mid][mid+1,r][mid + 1, r] 两份,递归求解,其中 mid=l+r2mid = \left \lfloor \frac{l + r}2{} \right \rfloor




随后考虑如何用“子问题”的解合并成“原问题”的解:



  1. 合并区间和 (sum): 当前问题的区间和等于左右两个子问题的区间和之和,即 sum = left[0] + right[0]

  2. 合并前缀最大值 (lm): 当前问题的前缀最大值可以是左子问题的前缀最大值,或者左子问题的区间和加上右子问题的前缀最大值。即 lm = max(left[1], left[0] + right[1])

  3. 合并后缀最大值 (rm): 当前问题的后缀最大值可以是右子问题的后缀最大值,或者右子问题的区间和加上左子问题的后缀最大值。即 rm = max(right[2], right[0] + left[2])

  4. 合并最大子数组和 (max): 当前问题的最大子数组和可能出现在左子问题、右子问题,或者跨越左右两个子问题的边界。因此,max 可以通过 max(left[3], right[3], left[2] + right[1]) 来得到。


一些细节:由于我们在计算 lmrmmax 的时候允许数组为空,而答案对子数组的要求是至少包含一个元素。因此对于 nums 全为负数的情况,我们会错误得出最大子数组和为 0 的答案。针对该情况,需特殊处理,遍历一遍 nums,若最大值为负数,直接返回最大值。


Java 代码:


class Solution {
// 返回值: [sum, lm, rm, max] = [区间和, 前缀最大值, 后缀最大值, 最大子数组和]
int[] dfs(int[] nums, int l, int r) {
if (l == r) {
int t = Math.max(nums[l], 0);
return new int[]{nums[l], t, t, t};
}
// 划分成两个子区间,分别求解
int mid = l + r >> 1;
int[] left = dfs(nums, l, mid), right = dfs(nums, mid + 1, r);
// 组合左右子区间的信息,得到当前区间的信息
int[] ans = new int[4];
ans[0] = left[0] + right[0]; // 当前区间和
ans[1] = Math.max(left[1], left[0] + right[1]); // 当前区间前缀最大值
ans[2] = Math.max(right[2], right[0] + left[2]); // 当前区间后缀最大值
ans[3] = Math.max(Math.max(left[3], right[3]), left[2] + right[1]); // 最大子数组和
return ans;
}
public int maxSubArray(int[] nums) {
int m = nums[0];
for (int x : nums) m = Math.max(m, x);
if (m <= 0) return m;
return dfs(nums, 0, nums.length - 1)[3];
}
}

C++ 代码:


class Solution {
public:
// 返回值: [sum, lm, rm, max] = [区间和, 前缀最大值, 后缀最大值, 最大子数组和]
vector<int> dfs(vector<int>& nums, int l, int r) {
if (l == r) {
int t = max(nums[l], 0);
return {nums[l], t, t, t};
}
// 划分成两个子区间,分别求解
int mid = l + r >> 1;
auto left = dfs(nums, l, mid), right = dfs(nums, mid + 1, r);
// 组合左右子区间的信息,得到当前区间的信息
vector<int> ans(4);
ans[0] = left[0] + right[0]; // 当前区间和
ans[1] = max(left[1], left[0] + right[1]); // 当前区间前缀最大值
ans[2] = max(right[2], right[0] + left[2]); // 当前区间后缀最大值
ans[3] = max({left[3], right[3], left[2] + right[1]}); // 最大子数组和
return ans;
}
int maxSubArray(vector<int>& nums) {
int m = nums[0];
for (int x : nums) m = max(m, x);
if (m <= 0) return m;
return dfs(nums, 0, nums.size() - 1)[3];
}
};

Python 代码:


class Solution:
def maxSubArray(self, nums: List[int]) -> int:
def dfs(l, r):
if l == r:
t = max(nums[l], 0)
return [nums[l], t, t, t]
# 划分成两个子区间,分别求解
mid = (l + r) // 2
left, right = dfs(l, mid), dfs(mid + 1, r)
# 组合左右子区间的信息,得到当前区间的信息
ans = [0] * 4
ans[0] = left[0] + right[0] # 当前区间和
ans[1] = max(left[1], left[0] + right[1]) # 当前区间前缀最大值
ans[2] = max(right[2], right[0] + left[2]) # 当前区间后缀最大值
ans[3] = max(left[3], right[3], left[2] + right[1]) # 最大子数组和
return ans

m = max(nums)
if m <= 0:
return m
return dfs(0, len(nums) - 1)[3]

TypeScript 代码:


function maxSubArray(nums: number[]): number {
const dfs = function (l: number, r: number): number[] {
if (l == r) {
const t = Math.max(nums[l], 0);
return [nums[l], t, t, t];
}
// 划分成两个子区间,分别求解
const mid = (l + r) >> 1;
const left = dfs(l, mid), right = dfs(mid + 1, r);
// 组合左右子区间的信息,得到当前区间的信息
const ans = Array(4).fill(0);
ans[0] = left[0] + right[0]; // 当前区间和
ans[1] = Math.max(left[1], left[0] + right[1]); // 当前区间前缀最大值
ans[2] = Math.max(right[2], right[0] + left[2]); // 当前区间后缀最大值
ans[3] = Math.max(left[3], right[3], left[2] + right[1]); // 最大子数组和
return ans;
}

const m = Math.max(...nums);
if (m <= 0) return m;
return dfs(0, nums.length - 1)[3];
};


  • 时间复杂度:O(n)O(n)

  • 空间复杂度:递归需要函数栈空间,算法每次将当前数组一分为二,进行递归处理,递归层数为 logn\log{n},即函数栈最多有 logn\log{n} 个函数栈帧,复杂度为 O(logn)O(\log{n})


总结


虽然,这道题的进阶做法相比常规做法,在时空复杂度上没有优势。


但进阶做法的分治法更具有 进一步拓展 的价值,容易展开为支持「区间修改,区间查询」的高级数据结构 - 线段树。


实际上,上述的进阶「分治法」就是线段树的"建树"过程。


这也是为什么「分治法」在名校考研课中分值更大,在大厂笔面中属于必选解法的原因,希望大家重点掌握。


作者:宫水三叶的刷题日记
来源:juejin.cn/post/7310104657211293723
收起阅读 »

大厂是怎么封装api层的?ts,axios 基于网易公开课

web
先看一下使用方法 先声明,这套写法是我在一次网易公开课学来的,说是大厂内部是怎么封装请求的,本人只是给它改成ts版本。 挺香的。 上核心代码 代码一:utils/request/getrequest.ts import axios, { type Axi...
继续阅读 »

先看一下使用方法
请求封装2.png


先声明,这套写法是我在一次网易公开课学来的,说是大厂内部是怎么封装请求的,本人只是给它改成ts版本。
挺香的。


上核心代码


WX20231124-143933@2x.png


代码一:utils/request/getrequest.ts



import axios, { type AxiosRequestConfig, type CancelTokenSource } from "axios";
import { manualStopProgram } from '@/utils/index';

import server from "./server";
import type { RequestConfig, ApiRouter, ServerRes } from './server.types'

class Requestextends keyof ApiRouter = keyof ApiRouter> {
requestRouter: ApiRouter = {} as ApiRouter
requestTimes = 0;
requestMap: Record<string, CancelTokenSource> = {};
toLogined = false

/**
*
@feat <注册请求路由>
*/

parseRouter(routerName: T, defaultAxiosConfigMap: Record) {
const apiModule = this.requestRouter[routerName] = {} as ApiRouter[T]

Object.entries(defaultAxiosConfigMap).forEach((item) => {
type ApiName = keyof ApiRouter[T]

const [apiName, defaultRequestConfig] = item as [ApiName, RequestConfig];

const request = this.sendMes.bind(
this,
routerName,
apiName,
defaultRequestConfig,
);

apiModule[apiName] = request as ApiRouter[T][ApiName]
apiModule[apiName].state = "ready";
});
}
async sendMes<ApiName extends keyof ApiRouter[T] = keyof ApiRouter[T]>(
routerName: T,
apiName: ApiName,
defaultRequestConfig: RequestConfig,
requestParams: Record<string, any>,
otherAxiosConfig?: RequestConfig
): Promise<ServerRes> {
this.requestTimes += 1;

return new Promise(async (resolve, reject) => {
try {
const selfMe = this.requestRouter[routerName][apiName];
const requstConfig: RequestConfig = {
...defaultRequestConfig,
...otherAxiosConfig,
data: requestParams,
};

/**
*
@feat <取消上一个同url请求>
*
@remarks [注:
* 个别页面需要同个api地址,多次请求,请传uniKey 作为区分,
* 不然有概率出现上一个请求被干掉
* ]
*/

if (selfMe.state === 'pending') this.cancelLastSameUrlRequest(requstConfig);

// 保险方案,传了 uniKey 才取消请求
// if (selfMe.state === 'pending' && requstConfig.uniKey) this.cancelLastSameUrlRequest(requstConfig);

const successCb = (res: ServerRes) => {
const ret = this.responseHandle(res, requstConfig)
resolve(ret);
};
const failCb = (error: unknown) => {
console.error("接口报错: " + requstConfig.url, error);
// 处理错误逻辑
throw error;
};
const complete = () => {
selfMe.state = "ready";
this.requestTimes -= 1;

if (this.requestTimes === 0) {
this.toLogined = false;
}
};

selfMe.state = "pending";
requstConfig.cancelToken = this.axiosSourceHandle(requstConfig).token;

await server(requstConfig).then(successCb).catch(failCb).finally(complete);

} catch (error) {
reject(error);
}
})
}

responseHandle(res: ServerRes, config: RequestConfig) {
const { code } = res;
console.warn(`请求返回: ${config.url}`, res);

if (code === 405) throw String("405 检查请求方式");
if (code === 401) this.toLogin();
if (code !== 200) throw String(res.message);

return res;
}

toLogin() {
if (this.toLogined) return;
throw String("请先登录");
}

generateReqKey(requestConfig: RequestConfig) {
return `${requestConfig.url}__${requestConfig.uniKey || ''}`
}
axiosSourceHandle(requestConfig: RequestConfig) {
const cancelToken = axios.CancelToken;
const source = cancelToken.source();

const reqKey = this.generateReqKey(requestConfig);
this.requestMap[reqKey] = source;

return source;
}
// 处理取消上一个请求
cancelLastSameUrlRequest(requestConfig: RequestConfig) {
const reqKey = this.generateReqKey(requestConfig);
const currentReqKey = this.requestMap[reqKey];

currentReqKey.cancel(`${manualStopProgram} reqKey: ${reqKey}`); // manualStopProgram 是一个标识,让外面的提示框忽略报错
}
}


export default new Request();



代码二:utils/request/server.ts


import axios from "axios";
import { UserInfo } from "@/utils/index";
import type { RequestConfig, ServerRes } from "./server.types";

export default async function server(
axiosRequestConfig: RequestConfig
): Promise<ServerRes> {
const token = UserInfo.getToken() || "";
const reqData = (() => {
const data = axiosRequestConfig.data;
const isFormData = data instanceof FormData;

if (isFormData) {
data.
append("token", token);
return data;
}
return {
...data,
token
};
}
)();

const { data: resBody, status } = await axios({
...axiosRequestConfig,
withCredentials: true,
data: reqData
}
).
catch((err) => {
const errMsg = err && typeof err === "object" && err !== null && "message" in err
if (errMsg) throw err.message;
throw err;
}
);

return resBody;
}

export {
server
}

import type { AxiosRequestConfig } from "axios";
import type { Api } from "@/apis/index";

export type RequestConfigany> = AxiosRequestConfig & {
uniKey?: string | number;
};

export type ApiConfig = {
params: T;
return: K;
};

export type List_pagiantion = {
page: number;
page_size: number;
};

// 这里有点绕,把各个api的参数和返回值 合成一个个特定的函数
export type ApiRouter = {
[K in keyof Api]: {
[T in keyof Api[K]]: Api[K][T] extends ApiConfig<any, any>
? {
(params: Api[K][T]["params"], otherRequestConfig?: RequestConfig): Promise<{
message: string;
code: number;
data: Api[K][T]["return"];
}>;
state?: 'pending' | 'ready'
}
: never;
};
}

export type ApiRouter__requestConfig = {
[K in keyof Api]: {
[T in keyof Api[K]]: RequestConfig;
};
}
export type ServerRes = {
code: number,
message: string,
data: any
};

接下来是apis文件夹(即开头的那个图片),在这里配置接口信息,日常业务代码在这里写


接口.png


代码一 写api配置 :src/apis/modules/admin-admin/index.ts


import type { ApiRouter__requestConfig } from "@/utils/modules/request/server.types.d";

const indexAdmin: ApiRouter__requestConfig["adminAdmin"] = {
getList: {
method: "post",
url: "/admin/admin/getList"
},
};

export default indexAdmin;

代码二 写接口类型声明 :src/apis/modules/admin-admin/index.types.d.ts


import type { ApiConfig } from "@/utils/modules/request/server.types.d";

export type AdminAdmin = {
getList: ApiConfig<
{
page: number,
page_size: number,
phone?: string,
status?: ManagerStatus,
groups_id?: number
},
{
count: number,
list: {
id: number,
phone: string,
groups_id: number,
create_at: string,
status: number,
status_txt: string,
groups_txt: string
}[]
}
>,
};


代码三 注册路由 :src/apis/index.ts


import { request } from "@/utils/index";
import indexAdmin from "./modules/index-admin/index";

request.parseRouter("indexAdmin", indexAdmin);

export type Api = {
indexAdmin: IndexAdmin;
}

// 这个是另一个作用,到处配置项,配合接口做权限控制,下面在说
export function getApiConfigMap() {
return {
indexAdmin,
};
}

下面说说这个封装方式好在哪里:


1. 解决的痛点:


以往我们看到最常见的封装方式,就这种

export function Api1() {
return axios.get('xx')
}

export function Api2() {
return axios.get('xx')
}

export function Api3() {
return axios.get('xx')
}

export function Api4() {
return axios.get('xx')
}

这种就非常麻木,一直写函数,每一个都要写配置项,没有数据结构结构=(无法复用)。
如果换成上面的 const indexAdmin: ApiRouter__requestConfig["adminAdmin"] = {},这种写法,就有数据结构了,有了结构之后就可以进行组合复用
比如上面提到的 getApiConfigMap 可以把数据结构直接导出,配合接口做按钮级权限控制,
接口会返回一份配置项{authen1: '/admin/admin/getList'}。
我们二者一比对,就能判断出是否有权限了。


比如看下面代码
PermissionWrapper 是一个权限容器组件 hasPermission=true就显示按钮
store.state.myPermission?.enterpriseAlarm?.edit 是用 getApiConfigMap 和结构权限表配合生成的


        <PermissionWrapper
hasPermission={store.
state.myPermission?.enterpriseAlarm?.edit}
>

<el-button type="primary" size="small" text onClick={openEditDialog}>
编辑
el-button>

PermissionWrapper>

ts直接提示,写起来很舒服,快准狠
ts提示.png


2. 请求函数封闭又开放


经过上面的 parseRouter 注册路由之后,sendMes 生成了N个请求函数,独一无二的函数,里面的fail success 可以做的事情很多,不如限制登录,取消上一个请求等等。大家有啥想法欢迎评论区写出来,我们一起优化它。


sendMes 最后一个参数,保持了开放性,在调用的时候我们传入uniKey就可以取消上一个请求了,还有一些特殊的参数,随便造。


3. 方便提取Api类型的参数和返回值类型(这是我额外拓展的)


我们经常会需要把参数和返回值的类型拿出来到页面上使用,这时候,就可以通过下面这个XX全局声明拿到。


declare namespace XX {
export type PromiseParams = T extends Promise ? T : never;

/**
*
@feat < 提取 api 参数 >
*
@describe <
type GetListParams = XX.ExtractApiParams

* >
*/

export type ExtractApiParams<Function_type> = Function_type extends (e: infer U) => any
? Excludeundefined>
: never;

/**
*
@feat < 提取 api 返回值 >
*
@describe <
type GetListReturn = XX.ExtractApiReturnType

* >
*/

export type ExtractApiReturnType<Function_type> = PromiseParams<
ReturnType<Function_type>
>["data"];

/**
*
@feat < 提取 api 为分页的 list item>
*
@describe <
type TableRow = XX.ExtractPromiseListItem

* >
*/

export type ExtractPromiseListItem<Function_type> =
ExtractApiReturnType<Function_type>["list"][number];
}

下面是一些使用方法举例
image.png


image.png


image.png


image.png


用上面的写法很方便就能在页面中把具体的类型拿出来。做到一次类型声明,到处使用。


api封装是一个长期的话题,axios很好用,但其实它就是一个请求方法而已。相信大家也见过很多乱七八糟的写法。特别是一些老项目,想新增api都不知道放在哪个文件夹。


很幸运无意中看到网易公开课的老师们讲解,那时候他们写的是js版本,看到这种由配置对象直接生成api函数的做法瞬间眼前一亮,这不就是我一直在找的封装方式,满足了我所有的想象。感谢感谢


后来我花了点时间,让它变成ts版本,还封装XX这个全局声明,让它彻底好用起来。希望这个封装能让大家受益。


细心的读者可能会发现上面的代码,一直抛错误,但是却没有拦截提示。 这是笔者推崇的报错终止程序,而不是用return的方式。(js终止程序,我常用throw 替代 return


如果您有什么好的建议或想法,欢迎评论区留言。有用请点点赞,还有更多经验总结在路上。
嘴下留情,骂我倒无所谓,重要的是别把评论区搞得乌烟瘴气


原课程链接:js es5版本,有兴趣的可以看看。但我觉得它那个取消请求,得再升级一下不然万一同个页面调用同个接口2次,就会取消第一个请求了。所以我加装了uniKey作为标识。
live.study.163.com/live/index.…


作者:闸蟹
来源:juejin.cn/post/7304594468157849640
收起阅读 »

前端学一点Docker,不信你学不会

虽然前端很少跟docker打交道,但随着工作流程的自动化现代化,docker正变得越来越重要。无论你是希望扩展技能到全栈领域,还是想要炫技,掌握Docker基本知识都是前端小伙伴重要的一步。 什么是Docker Docker 是一个开源的应用容器引擎,可以让...
继续阅读 »

虽然前端很少跟docker打交道,但随着工作流程的自动化现代化,docker正变得越来越重要。无论你是希望扩展技能到全栈领域,还是想要炫技,掌握Docker基本知识都是前端小伙伴重要的一步。


什么是Docker



Docker 是一个开源的应用容器引擎,可以让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中,然后发布到任何流行的 Linux 机器上,也可以实现虚拟化。



我们知道,软件的安装是要区分系统环境的;即使是运行环境相同,依赖的版本一旦有所出入,也容易出现“我这里明明好使,你那里为啥不行“的问题。容器技术解决了环境一致性依赖管理的问题。


因此,容器是我们的项目的运行环境,而docker是容器的运行环境与管理平台。


关键词


镜像 (Image)


镜像是构建容器的模板,可以简单理解为类似Js中的class类或构造函数。


镜像中详细记录了应用所需的运行环境、应用代码、如何操作系统文件等等信息;


容器 (Container)


容器是镜像的运行实例。可以简单理解为”new 镜像()“的实例,通过docker命令可以任意创建容器。


当前操作系统(宿主机)与容器之间的关系,可以参照浏览器页面与iframe之间的关系。容器可以拥有独立的IP地址,网络,文件系统以及指令等;容器与容器、容器与”宿主机“之间以隔离的方式运行,每个容器中通常运行着一个(或多个)应用。


仓库 (Registry)


仓库是集中管理镜像的地方。类似于npm平台与npm包之间的关系。


如果我们将搭建项目环境的详细过程以及具体的依赖记录进镜像中,每当需要部署新服务时,就可以很容易的通过镜像,创建出一个个完整的项目运行环境,完成部署。


示例——安装启动Mysql


1. 安装Docker


具体过程可参考菜鸟教程,下面以macOS系统作为例子进行演示。


启动docker客户端如下:



打开系统终端(下面是在vscode的终端中演示),输入命令:


docker -v

效果如下:



说明docker已经安装并启动。


2. 下载Mysql镜像


下载镜像有点类似于安装npm包:npm install <包名>,这里输入docker镜像的安装命令:docker pull mysql来下载安装mysql的镜像:



安装结束后,输入镜像列表的查看命令:docker images



当然,通过docker的客户端App也可以看到:



3. 创建mysql镜像的容器,启动Mysql


输入启动容器命令:


docker run -d -p 3308:3306 -e MYSQL_ROOT_PASSWORD=123456 mysql

先来看下启动结果,下面的一堆数字是完整的容器id:



输入的这一串命令是什么意思?



  • docker run: 这是启动新容器的命令。

  • -d--detach 是使mysql服务在后台运行,而不是占用当前终端界面。

  • -p 3308:3306: 这是端口映射参数:

    • 创建容器会默认创建一个子网,与宿主机所处的网络互相隔离;mysql服务默认端口3306,如果要通过宿主机所在网络访问容器所处的子网络中的服务,就需要进行端口映射(不熟悉网络的可以看下《如何跟小白解释网络》)。

    • 宿主机的端口在前(左边),容器的端口在后(右边)。



  • -e MYSQL_ROOT_PASSWORD=123456: 设置环境变量 MYSQL_ROOT_PASSWORD=123456;也就是将mysql服务的root用户的密码为123456

  • mysql: 这是上面刚刚pull的镜像的名称。


通过上面的命令,我们启动了一个mysql镜像的容器,并将主机的3308端口映射到了容器所在子网中ip的3306端口,这样我们就可以通过访问主机的localhost:3308来访问容器中的mysql服务了。


4.访问Mysql服务


下面写一段nodeJs代码:


// mysql.js
const mysql = require('mysql');
const connection = mysql.createConnection({
host: 'localhost',
port: '3308',
user: 'root',
password: '123456',
database: '',
});
connection.connect();
// 显示全部数据库
connection.query('show databases;', function (err, rows, fields) {
if (err) {
console.log('[SELECT ERROR] - ', err.message);
return;
}
console.log('--------------------------SELECT----------------------------');
console.log(rows);
console.log('------------------------------------------------------------');
});

这里调用了nodeJs的mysql包,访问localhost:3308,用户名为root,密码为123456,运行结果下:


$ node mysql.js;
[SELECT ERROR] - ER_NOT_SUPPORTED_AUTH_MODE: Client does not support authentication
protocol requested by server; consider upgrading MySQL client

这里报错了,原因是mysql服务的认证协议与我们代码中的不同导致的。这里我们需要对mysql服务进行一些修改。


为此,我们需要进入mysql容器,对mysql进行一些直接的命令行操作。


5.进入容器


首先,我们需要知道容器的id,输入容器查看命令:docker ps,展示容器列表如下:



其中55cbcc600353就是我们需要的容器的短id,然后执行命令:docker exec -it 55cbcc600353 bash,以下是命令的解析:



  • docker exec:用于向运行中的容器发布命令。

  • -it:分配一个终端(伪终端),允许用shell命令进行交互。也就是将容器中的终端界面映射到宿主机终端界面下,从而对容器进行直接的命令行操作。

  • 55cbcc600353:容器ID或容器名称。

  • bash:这是要在容器内执行的命令。这里是启动了容器的Bash shell程序。


运行结果如下:



我们看到bash-4.4#  后闪烁的光标。这就是容器的bash shell命令提示符,这里输入的shell命令将会在容器环境中执行。


我们输入mysql登录命令,以root用户身份登录:mysql -uroot -p123456



成功登录mysql后,在mysql>命令提示符下输入:ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password By '123456';


这条命令用来修改’root’用户的认证方式为mysql_native_password ,将密码设置为123456,并允许来自任何主机(‘%’)的连接。


输入exit;命令退出mysql>命令提示符:



再按下:ctl+D退出容器终端,回到宿主机系统终端下。再次运行上面的js代码,效果如下:



这样我们就完成了本地mysql服务的部署。


结束


通过上面的简介以及安装部署mysql服务的例子,相信不了解docker的前端小伙伴已经有了一些概念;感兴趣的小伙伴可以继续深入,学习相关的知识。


作者:硬毛巾
来源:juejin.cn/post/7304538094782808105
收起阅读 »

社会现实告诉我,00后整顿职场就是个笑话

00后整顿职场,也算是我之前的关键词吧。 我硬怼老板要加班费和提成,和他们辩论什么是我的财产,什么是公司的财产。 甚至还能在即将被开除的时候,反将一军把老板开除。 而正是因为这一次把老板开除,让我得到了机会。可以站在了相对于之前更高的位置,来俯瞰整个职场。 也...
继续阅读 »

00后整顿职场,也算是我之前的关键词吧。


我硬怼老板要加班费和提成,和他们辩论什么是我的财产,什么是公司的财产。


甚至还能在即将被开除的时候,反将一军把老板开除。


而正是因为这一次把老板开除,让我得到了机会。可以站在了相对于之前更高的位置,来俯瞰整个职场。


也真正意义上让我感受到了,00后整顿职场,就是一个互联网笑话罢了。


1、职场宫斗,成功上位


我之前在苏州工作,堪称工作中的宫斗,并且在这场宫斗大戏中胜出,将原有的项目负责人开除,成功上位。


而这个项目存在的问题非常多,我就在六月被派遣去项目的总部合肥进行学习,等到打通项目的全部链路后,再回到苏州。


届时我将以这个项目的负责人,重新搭建团队,开展这个项目。所以我在合肥那边,以员工的身份深入各个工作组进行学习。


在市场部,运营部的办公大厅工作过,也在各部门的独立办公室工作过。


我感觉自己像个间谍,一边在以平级的打工人身份和我的同事们相处,一边又以苏州负责人的身份,参与那些领导才能参与的内部会议。


2、内心变化的开端


我在合肥总部工作中,接触了很多躺平摆烂的同事,但这个“躺平摆烂“要加上双引号。


他们是00后,90后,甚至有85后。如果放在三个月前,我可以不假思索地说,他们全都是我最讨厌的人。他们如同牛羊一般任人宰割,上级让加班,他们就加班,有时候加班甚至超过四五个小时也没有怨言。


我甚至从来没听他们感慨过为什么没有加班费。亲眼看着他们被自己的上级用一些与工作无关的鸡毛蒜皮之事骂得狗血淋头,但他们也只会在被骂完之后,背地里吐槽那个领导估计是在家被老婆骂了,才来拿他们泄愤。


我打听了他们的工资,只能说中规中矩,起码不是能让人当牛做马的数字。偶尔我见到一两个有骨气的人,觉得拿这么点钱就应该干这么点事。干不爽就马上离职,但马上就会有下一个人替补他的位置,形成闭环。


我惊讶于怎么有人能惹到这个地步,但后来和他们日渐熟落,我们一起吃饭,一起打游戏,一起下班顺路回家,还参加了他们的生日聚会。我发现他们活得其实真的很洒脱。一切都是随遇而安,下班时间一到,他们就真的可以无忧无虑。


因为他们有一份工资还行的工作,养活自己。他们没有啃老,也没有用卑鄙的手段,去抢想要努力的人应该分到的蛋糕,也压根不去想要赚很多钱,因为没有什么需要太高消费的需求。


加上现在的环境,找到一份可观收入的工作确实很难。所以公司偶尔的加班,领导偶尔的泄愤,这些毕竟还是少数时候的偶尔,也都没有超过他们的心理承受阈值,那也就得过且过了。


所以我们其实都一样,只是个普通人罢了。而像我们这样的普通人,取之不尽,用之不竭。这到底是好事还是坏事呢?


3、复杂的职场生态环境


建立在这个基础上,视觉转换到高层领导们这里。他们当着我的面说,这样的人就是个底层打工仔,缺人就招,加班照旧,心情不好还要扣他们的全勤绩效。


压根就不怕这些底层打工仔闹事,纵使有一两个所谓的决心者辞职,也能在很快时间找到下一位。


两者形成互补,共同铸就了这样恶劣的职场生态环境。但我说职场无法改变,远不止这么一点原因。


在这个项目中,我说好听一些只能算是项目负责人,在此之上还有着项目股东,这还要细分成大股东和小股东。而我所在的项目属于互联网赛道,也就是说需要一些新鲜事物的眼光和思维来对待。


但这些股东们经常提出一些奇怪的意见,就如同用微商时代的卖货思维,来指点直播带货,并且他们是出钱的股东,他们提出的战略方针不容我驳回,因为在他们的光辉历史中,有大量的成功案例,来佐证他们的思路是对的。


我刚开始觉得也有道理。他们能有钱投资,肯定是有什么过人的本领能让他们赚到钱,但是随着相处下来,我发现不过是他们本身家里条件就优越,在九几年就能拿出一百万给他们创业。


他们把这一百万分散到二十个领域,每个投资五万总能撞上那么一两个风口,让他们实现钱生钱。


九几年的五万也算是一笔不少的投资。他们这样的发财经历,让我很难不产生质疑,这不是给我我也行吗?


毕竟他们如果真的有什么过人的本领和远见,也不至于在每次内部开会之前,都要组织喊这样的口号:“好,很好,非常好,越来越好“


甚至试图把这样的口号,带到每一次迎接客户的项目介绍会上。我以自曝式要挟制止他们这个行为,我说如果你们这么干,那我当天就辞职,内部都是自己人,我可以陪你们这样弄,但如果对外这么搞,被录下来说我们是传销,我都不知道怎么辩解。


4、职场中的背锅人


他们就是这样坚信着自己能成功,是因为自己有过人的才华。所以自我洗脑着自己提出的方向没有错。如果出错了,亏损了,那一定是负责人的问题。


但好巧不巧,我就是那个负责人。我已经无数次告诉他们,我们这个项目压根就不需要穿黑丝短裙跳舞的小姐姐。


我甚至写了一篇报告给他们,分析我们的项目为什么不能用擦边这种手段引流。但他们执意要,说这样来流量快,我都有点分不清到底是他们自己想看,还是深信这样做确实是可行。


但如果最后这样还是没成功,导致项目亏损,大概率还是在我身上找原因吧。


面对他们这样的大佬,我心里很清楚,这已经远远不是宫斗了,这也绝对不是靠几个心计,或者有实力撑腰就能取胜上位了。这场权力的游戏,不是我等草民玩得起的。


5、换个思路,创造属于自己的职场


一边是被提供资金,但是瞎指挥的股东们摧残,一边是在有限的预算下,我作为负责人,确实很难做到尊重打工人的内心挣扎,回到苏州我虽然能身居高位,但我终将成为我曾经最鄙视的人。


我不要当这个背锅侠,我也不想在这个环境中,去逐渐接受这样的价值观。


这样看来确实如此,00后整顿职场不过是一场互联网的狂欢罢了。


这个题材的故事,也永远只能发生在职场的最底层。由一群家境优越,体验生活的公子哥和我这种不知好歹的普通人共同出演。


大部分人只是在手机屏幕前把我们当个乐子,成了扣个666,然后一起吃胜利的果实。没成,那就确实是看了个乐子。


或许是因为他们心里也清楚,凭我们压根就做不到。


00后现在确实整顿不了职场,因为社会的资源和命脉还不掌握在00后手上。


但就止步于此了吗?我曾说过我想有一个自己的小工作室,遵守劳动法,双休,按时发工资,交纳五险一金。


是的,换个思路,也许00后不需要整顿职场,而是直接创造属于自己的职场,那么接下来我就要向着这个目标去努力了,毕竟二十年后我也还是00后,不如到时候再来说00后整顿职场吧。


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

我在酷家乐这 4 年,项目成败与反思

引言 2023-12-06 是我在酷家乐的最后一天,想把我在酷家乐这 4 年主导落地的项目做个总结,聊聊每个项目的创立背景、结果成败,以及反思。为了防止业务敏感信息泄漏,文中不会涉及到任何业务情况,项目结果数据,项目截图等内容。 19 相遇 时间来到 19 年...
继续阅读 »

引言


2023-12-06 是我在酷家乐的最后一天,想把我在酷家乐这 4 年主导落地的项目做个总结,聊聊每个项目的创立背景、结果成败,以及反思。
为了防止业务敏感信息泄漏,文中不会涉及到任何业务情况,项目结果数据,项目截图等内容。


19 相遇


时间来到 19 年 8 月,那是我加入酷家乐的日子。作为 IC 投入到酷家乐用户增长团队,当时团队主要在做激励体系、积分抽奖、酷签到、勋章等 To C 促活业务。


新业务


19 年 10 月,用户平台线成立 “设计圈” 新项目,是个 To B 的 SaaS 业务,目的是打通企业内部设计孤岛,让企业内部设计师共享、共建、共成长。后被大家戏称 “小主站”,即主站的子功能 SaaS 化。
过程中我统一所有前台页面启动逻辑,增加启动的中间件机制,中间件机制也是首次被引入到页面启动流程中,对于多页、统一的场景至关重要;对于管理后台,引入当时比较前沿的 UForm(即现在的 Formily),并进行业务定制的封装,目的是简化表单、表格等场景的开发工作,而此动作也提效明显。在此感谢阿里对开源的贡献。
反思:



  1. 多页应用,需要有入口做全局逻辑的管控。而落地做法很多:html 入口/JS 统一使用固定的 boot 逻辑等等

  2. 垂直领域能做一定的技术轮子。例如:表单表格的管理后台场景,需要有垂直领域的组件来做提效,基础组件还不够

  3. 过程中担任 SM ,反推自己以全局视角考虑问题,并且关注团队成员的任务与过程


20 回归


20 年 2 月一半精力回归用户增长团队,直到 6 月份完全回归。


小程序平台


20 年 1 月,公司内部小程序业务增多,需要做一定的基础设施,以提升整体的开发效率。前端团队老大,推动成立“小程序平台”专项虚拟小组,由几个小程序的业务团队同学(设计圈也有小程序业务),以及基架组转岗过来的同学组成。
我主动负责其中的 CI/CD 部分,接入 Def(公司前端统一 CLI 工具),完成套件、构建、部署等功能。当时微信小程序还不支持 CLI 部署,只能借助 “微信小程序开发者” 工具,在 Windows/Mac 上使用,而公司已有工程化 Linux 相关的基建,完全用不了,故在 Windows 虚拟机上安装“微信小程序开发者”工具,并且本地启动 proxy server 与开发者工具互通,CLI 再调用 Windows 本地的 proxy server 完成互通。
反思:



  1. 微信开发者工具客户端等以 UI 的形式提供给使用方,对于小团队很友好,对于想集成到大团队自有工作流的系统中,很差(好在微信现在已提供 SDK 跨平台发布,以便于集成到现有系统中)

  2. 我只参与了“小程序平台”不到半年,随后平台越发庞大:包括微信公众号管理、用户管理,甚至域名管理、人员管理、运维中心、营销工具等微信本身已经提供的能力包装。投入了非常多的人力,但是我个人认为过于超前。主要原因是:

    1. 酷家乐本身各业务小程序并没有太多增长

    2. 边缘功能太多,绝大多数场景根本用不到。我理解仅需要这些核心能力:模板分发多商家小程序、CI/CD、组件库、脚手架。



  3. 基建不应太过超前,优先满足最核心、提效最高的能力。


TMS


对于 To C 的产品,用户增长当时主要靠运营驱动,借助一些营销获客、促留存的手段,而产品需求绝大部分来自于运营同学。而面向运营同学的工具,有 2 款:



  1. TMS:仿淘宝店铺装修的页面搭建平台,主要可以完成产品介绍页、营销页等功能的搭建;

  2. 云台:运营平台,面向 To C 用户的营销推送(短信、公众号、邮箱、站内信等场景)、广告位管理等核心应用场景。JS 全栈开发,包括对 MySQL、Redis 等持久层的直接调用。


对于 TMS 页面搭建平台,有个极大地痛点:所有的模块(前端开发的定制模块)很难开发与发布,所有模块都杂糅在一个 NPM 包内,所以开发一个模块的流程是这样:



  1. TMS 管理后台创建一个模块,就是元信息了拿到模块 ID

  2. Mod Package 里开发一个模块,包含展示场景和编辑场景的组件

  3. 将 NPM 包 Link 到 TMS 管理后台的仓库

  4. 启动仓库本地 debug mode (体验很差)



  5. 开发阶段结束,开始发布阶段




  6. 发布 NPM 包

  7. 分别安装到对外渲染、对内后台管理的 Repo 上

  8. 分别发布对外、对内系统


整体流程很长,导致业务开发同学更愿意 0-1 写一个静态页,而不是开发一个 TMS 模块,进而造成了业务模块并不是很多,生态不丰富。
我在开发 TMS 模块时也深感痛苦,故过程中对 “新开发一个模块” 的流程进行了改进。
整体原则就是将模块的安装、加载从主体中剥离,从 NPM 包转变为浏览器端运行时注入的模块。当时已经有了 SEED,它比较基础,且全局都有安装,它是一个运行时模块加载、管理器,可以简单类比 SeaJS。通过维护一个 Alias ,模块 key 与 JS/CSS CDN 列表的映射关系,来决定如何加载模块,而这个 Alias 本身也是通过一个大 JSON 进行保存。
那么解决思路很直观了,只需要保存一个 TMS 模块 Key 与模块打包后的 JS/CSS 产物,即可做到将模块的安装由 buildtime -> runtime,进而能做到模块的调试、打包、发布与 TMS 主系统完全隔离。
优化后的流程是这样的:



  1. TMS 管理后台创建一个模块,就是元信息了拿到模块 ID

  2. 各自业务仓库开发一个符合一定 Interface 的模块

  3. 业务仓库本地 debug (打开 TMS 测试环境,直接把模块请求代理到本地即可)



  4. 开发阶段结束,开始发布阶段




  5. 各业务仓库构建模块,产出 JS/CSS,并自动上传 CDN,修改 Alias JSON 完成发布


本地调试由于仅需要构建当前模块,所以开发体验很棒。


SEED 与微应用


但是 SEED 也有它自己的问题,Alias JSON 独立于现有其他发布平台维护,且无灰度、无回滚,是个很大的稳定性隐患。
当时公司基建(Pub)已初步成型,比较超前,核心是以最小可发布粒度的一站式解决方案,而首推的就是页面单元,能将传统的以 Repo 多页为发布单元,转为以独立页面为发布单元,且秒级发布、秒级回滚。前端微应用当时也初步成型,主要目的是拆分酷家乐工具的巨石应用,提升开发效能。
当时主站还在继续使用 SEED,前端微应用和 SEED 其实目标非常类似,核心都是独立开发、独立发布。这时产生了一个想法 “能否让前端微应用支持浏览器端运行时加载,以替代掉 SEED 模块管理部分的能力”,达到 “All in micro” 的效果。
此时,前端微应用的输出模式是 html 片段,此片段可以注入到 page 中,最终输出完整的 page html 给到浏览器,即拼接形式。页面与微应用可独立发布,在统一的 Node.js Page Render 层进行服务端拼接,以组装成一个可以由多团队共建的完整应用。
那么,做法很清晰了,需要将仅支持在服务端 html 拼接形式使用的微应用,扩展为支持浏览器端运行时动态获取微应用 html 片段,并注入到 DOM 中去,并解决 Script 等标签无法执行与如何同步有序执行的问题,这就诞生了“Pub 微应用加载器”。
此时已存在 Single SPA 或乾坤等库,独立发布的功能是大家共有的,沙箱&路由联动等特性是不需要的,所以也没有参考这些开源库实现。
此阶段之后就顺势推动 SEED 历史模块全量迁移 Pub 微应用,相对的好处是:



  1. 拥抱同样的基建(CI/CD),灰发&回滚等机制

  2. 无需页面预置环境

  3. 去中心化,微应用加载器分布式安装在各个微应用 or 页面 bundle 内,不到 8K (未压缩)


而 TMS 的新模块开发方式也由 SEED 模块过渡到使用 Pub 微应用模块。


公共包


基建相对比较成熟了,但是主站业务的公共包却一直比较混乱,质量也不高。“磨刀不误砍柴工”,工具库、业务组件库的重要性不言而喻,这半年也开启了公共库的创建和规范:



  1. types:以业务域划分,定义业务通用的类型单元,例如方案等

  2. utils:工具函数

  3. rc:业务特定的组件库

  4. etc...


这部分内容大多数公司做的事情类似,不细讲了。


反思



  1. HTML 是组成页面的基本单元,以它为切入点,相对以 JS Entry 能做更多事;

  2. 跨团队协作,独立发布,低耦合是效能王道

  3. 开源产品能解决部分通用问题,工作流的串联,整体架构还需独立设计

  4. 秒级发布&回滚,能解决绝大多数稳定性问题

  5. 发布卡点 or 审批对于新手是保护,对于老手是枷锁


H2 开始,也带来一些新的挑战:



  1. 如何快速搭建新站点

  2. 类似的区块如何复用,是否复制是个更好的选择?


21 创新


Star


基于 20 下半年业务上各种新站点搭建带来的效率以及质量的综合挑战,21 年初我在思考“是否要造一个全司共建共享的物料共享平台”,以打破团队间信息壁垒。
在此阶段我已经是敏捷组 TO,并且有一定的影响力,所以大家愿意跟着我的想法一起干,包括隔壁组同学。此时恰好 UED 团队同学有“设计物料共享”的想法,所以一拍即合,前端 5 人 + 设计 2 人,自建组成虚拟小组,利用业余时间创建:Star 物料平台
平台设想大而全:



  1. 开发物料:Web 端、VSCode 插件、物料开发 CLI;分为 2 大类:区块、页面模板

  2. 设计物料:Web 端、Sketch 插件


这里主要讨论下开发物料,区块和页面模板都是参考自“飞冰”的设计,利用“复制”的手段,达到复用的目的。好处就是可以任意修改,不会因为 Interface 不满足而无法使用或扩展,相似的视觉效果都能直接拿来用。
而 VSCode 和 Sketch 插件的代码分别 Fork 自开源项目 IceWork、Kitchen(好像是),进行自有系统以及物料库的集成。
整个系统全栈 TS 开发,包括 Sketch 插件,服务端采用 NestJS+MySQL+Serverless 完成。
反思:



  1. 现在看来,区块的复用方式不如组件的形式,而且也没有用起来

  2. 页面模板倒是用来做初始化页面 or 微应用的规范了,也是一种将各业务线规范落地的平台

  3. 设计物料和 Sketch 插件使用量可观,相对于原始 File 下载分发,借助 Sketch 插件自动享受最新的设计物料比较高效


所以就区块来说,Star 是失败的,所以后来又逐步优化,新增了微应用文档的接入,因为微应用的使用方必然是需要阅读文档的,Star 就是一个比较好的集成微应用使用文档的平台,直接关联微应用的唯一 Key。


登录注册


在这之前我也兼账号体系(UIC)的前端负责人。酷家乐的账号体系也许是互联网行业最复杂的系统之一,它的复杂性来源于:



  1. 面向多种产品:To C、To B

  2. 面向多种身份:设计师、业主、从业主,在这之下又有很多细分行业


登录注册链路也有一定的复杂性:



  1. 注册链路极长,三方绑定 -> 手机验证 -> 选择身份 -> 推荐设计师/业主 -> 发放奖励

  2. 登录的形式:三方、扫码,弹窗登录、登录页面

  3. 登录过程中的风控拦截,图片验证,

  4. 登录过程中的 C & B 多账号绑定

  5. etc.. 还有很多没有列出来的


面临的挑战:



  1. 整体偏过程式的写法:你可以想想一个回调函数内部写了非常长的逻辑,且牵一发动全身

  2. 数据流与执行流的混乱:Promise 可能存在一直 Pending 的状态,例如某个 callback(resolve) 一直不执行,流程中的数据传递混乱,没有一条主线

  3. 以上带来的结果就是,涉及登录注册的任务估时 x2

  4. 美间、模袋等业务的加入,需要打通账号体系,并且复用同一套登录注册能力(但是不接受走同一个页面完成 SSO,这决定了后续的架构模式)


基于此,对登录注册组件进行了彻底的重构:



  1. 更合理的分层:基础包(通用 UIC 逻辑)、核心能力(支持配置化的形式确定外部需要何种登录注册方式)、酷家乐业务场景下的微应用 以及 其他业务场景下的页面

  2. 插件化的架构模式:借助 Tapable 完成异步串行的执行场景,增加登录注册前后等超 10 个 Hook,为后续扩展奠定了基础,并解决执行流问题

  3. 全局 Store:解决数据流问题

  4. 将原有非核心链路的逻辑拆分出接近 10 个插件,完成业务逻辑


结果:



  1. 扩展性:最初设想就是未来至少 3 年不需要重构登录注册模块,目前我认为至少 5 年是没有问题的

  2. 研发效率的提升:统一群核之下的几乎所有业务线的登录注册;后续几年的实战中,对于登录注册业务上的各种大需求,都没有对核心部分造成影响,通过插件都能满足需求

  3. 整体的 ROI 还是很高的


反思:



  1. 对核心业务的架构优化是值得投入的

  2. 插件化不仅用于工程化领域,也可用于业务,需要一定复用性、扩展性的场景都可考虑

  3. 架构是为了不让复杂度指数级爆炸


开发效率与规范


21 年的以上 2 个偏全司基建或特定业务,开发效率与规范也在持续进行:



  1. 为了多仓库共享代码,造了 Rocket CLI,定位是基于 subtree 的业务线级别的代码共享

  2. 规范了业务域为单位的 Owner 机制,并且不分端(PC、H5、小程序)

  3. 规范了 lint/babel/postcss/ts config,并且基于 Rocket 可以做到及时的共享更新

  4. 规范了所有 page 的启动方式,也基于 Rocket 进行共享

  5. 规范了全局弹窗的管理器,支持优先级队列机制

  6. etc...


基于 Rocket 的基建能力,做了到所有业务仓库共享同一套 xxx config,共享同一套业务启动逻辑。
但是也带来了一些棘手的问题:



  1. Git subtree 的机制,会让 Repo 历史记录混乱,掺杂很多不相干的 commit

  2. 高版本 Git subtree 提交时,部分同学总是无法 push 上去

  3. 随着时间推移,2 年后的今天,commit 已达 2k 多条(中间应是某些同学误操作带上去的),导致后期又增加了 reset 的机制,并且把 shared Repo 给重置了,进而又导致 shared Repo 与业务 Repo history 对不上...


这些问题只能通过比较懂的同学人肉操作下,以达到可以正常 push pull。所以后期会弃用 Rocket,改回 Npm Package,但是增加一些功能让他能保持定期更新。
反思:



  1. Subtree 有其局限性,最好的协作模式我认为一个业务线采用单一的 monorepo,通过基建去直面单 Repo 的构建性能问题,部署效率问题;

  2. 对于业务线的开发团队,最优先的是制定规范、落地规范到代码里、及时更新规范,以达到开发者同一套开发思路,对于协同开发效率是极好的;

  3. 不要分端,其他端的开发成本相对团队多人的沟通成本低很多;


22 再创新


22 年底有写过《2022 年终总结》,所以这里尽量谈的更宽泛一些, 有一定的相似处。
职位的变化,21 年中开始担任 Action Mgr,22 年初转为正式 Mgr。也会有一些管理思考,但是本文不会涉及。


客户端打包平台


没错,又开始造平台了。背景是酷家乐的主要用户在 PC 端,且绝大多数都使用客户端(基于 Electron),而且其他业务线也会开发自己的客户端(例如美间)。
所以除了一些基础 Electron 扩展能力的复用之外,长远来看最好能有个工程化平台,集成端侧的构建、打包、发布、分发等一系列的能力。这就是“客户端打包平台” 也可以称之为“客户端 DevOps 平台”。
做了如下事情:



  1. 首先需要一个打包环境,不仅要打包 Windows/Mac 上的 Electron 应用,后续还支持了 Android App 的打包

  2. 其次需要一个打包管理后台,包括:应用管理、构建管理、版本管理、发布管理以及权限

  3. 最后定义一套接入规范,以 Node.js 脚本形式接入,脚本接收一些入参,根据参数构建、打包、签名产出最终的安装包(固定目录),平台进行上传并回调更新 CDN URL、版本等信息


整体逻辑并不复杂,说一些它和 Web 页面发布的区别:



  1. 存在版本,线上版本碎片

  2. 存在复杂的更新机制,也有灰发机制

  3. 存在不同渠道分发不同安装包,便于后续的安装来源统计

  4. 多种打包目标:Windows/Mac/Android,不同目标会有提供不同的打包环境


对于平台,还有很多事没做,例如:数据看板,版本分布等,但是对于近几年足够了。
有些同学可能会不理解,和 Gitlab CI 有啥区别?
借助 CI 仅能完成任务的触发,而任务是需要特定的运行环境的,除此之外:版本的管理、灰发、渠道分发都是平台特有的能力。
反思:



  1. 针对核心业务做基建更不易出错


SSR


过去几年,随着基建升级,老的 FreeMarker(JAVA) + JQuery ,慢慢转变为 Nunjucks(Node.js)+ JQuery,再转变为 Nunjucks(Node.js)+ React。而到 React 阶段,服务端直出页面关键 HTML(SSR)已不存在。产生的结果就是来自搜索引擎的流量逐渐下滑,而 SEO 对于酷家乐来说至关重要,是个非常重要的流量窗口。为了拯救 SEO,22 年上半年开始了一些 SSR 的尝试。
但是,要做 SSR ,会和业界常用方案有所不同:不会采用 Next.js 类似全栈框架,因为此类全栈框架带来的问题是每个业务都需要独立的 Node.js 服务,还需要持续的观测稳定性,出问题对于业务开发者来说是非常棘手的,对开发者的要求极高。
所以 SSR 服务需要做到的效果:



  1. 每个 SSR 页面都可以独立发布,即使他们在一个 Repo

  2. 创建 SSR 容器服务,由 SSR 服务的开发者管理服务的稳定性,业务开发者无需关心

  3. 所有 SSR 页面都运行在这个容器内

  4. 所有 SSR 页面需要有沙箱,运行上下文隔离

  5. 需要有降级到 CSR 的策略


除了 SSR 服务本身之外,也需要有其周边的工具链:



  • 针对每个 SSR 页面构建打包为独立的 Server Entry

  • Server Entry 需要符合约定的 Interface,输入请求上下文,输出渲染结果

  • TS type 包,便于接入


方案详情页是第一个接入的页面,上线前借助 Apache Benchmark 工具做了一定的压测,上线结果也是很好的,此阶段 22 年中上线。
到此阶段,还有一些工程化问题需要联合基架组一起解决,深入集成到 Pub 系统内:



  1. 本地开发:支持 SSR 和 CSR 的同步开发,以及规范的本地开发调用链

  2. 构建:自动识别哪些页面需要走 SSR ,完成 Server Entry 构建

  3. 发布:发布后,对于页面信息的变更,秒级同步到 PR 与 SSR,完成应用的自动更新

  4. 运行:集成请求链路 浏览器 -> PR -> SSR,自动降级能力;以及 Pub 上配置包、语言包等能力的打通


此阶段在 22 年下半年完成,完成后对于业务开发者来说,开发一个 SSR 页面和 CSR 一样简单,不仅是 SEO 的提升,对于首屏优化也有效。
过程中也遇到了各种问题:



  1. 一些二方包实现时没有考虑 Node 端运行场景,例如使用了很多 window/navigator 等浏览器端全局变量,使用 jsdom 注入到每个页面上下文里解决(但也带了问预料之外问题,见 3)

  2. OOM 问题:随着 SSR 流量增多,有一天触发了一端代码的临界点,即运行几天后内存溢出,服务被 K8s Pod 自动重启,反复;排查下来是一个非常基础的监控模块,在并发的 HTTP 链接达到一定数量后进入另一个分支,这个分支对缓存的清理有问题,导致持续增长的内存没有被回收

  3. GC 频繁导致 CPU 增高:根因是有个页面使用到了 react-helmet 库管理 document head 信息,helmet 又使用了 react-side-effect 来处理 props 变化引发的副作用,问题就出现在我们 Mock 了 window/document 等信息,让库误认为当前运行环境在浏览器端,进而将本应无副作用的 string 处理,变成了 DOM 处理,让 Node.js 内 new space 的空间增多,进而引发频繁 GC。


可以看到目前的 SSR 方案也并不是完美的,虽然做了沙箱,但是本质他们还是运行在同一个线程之内的,共享同一个 CPU 资源和内存资源。当出现 GC 引发的 CPU 问题,或 OOM 引发的问题,就会导致整体的不可用。
所以解决这些问题的方案就只能做多进程,一个页面的 SSR 就启动一个独立进程,由主进程管控数据分发、应用更新,这样能充分利用多核 CPU,不至于一个页面 Bug 引发整体的雪崩。
反思:



  1. 之前做的平台更偏研发效率,SSR 能解决一定的业务问题

  2. 不一定一开始就要最完美的方案,保留一定扩展性,能解决当下问题就是最好的


其他


22 年也有一些效果不错的优化:



  • 帮助中心的核心 API 性能提升 70%,帮助中心 JS 全站开发,主要优化的是对 MySQL 调用相关的业务层的逻辑优化。同时也解决其稳定性问题,之前总会因为流量的翻倍导致服务短时间不可用;

  • Star 部分也在持续优化:新增了一些页面模板,微应用的文档是在这一年做的

  • 客户端的可观测行:本地日志优化,以及用户的一键上报

  • 重写富文本编辑器,并应用在多条业务线

  • etc


23 优化


23 年主要是对现有系统的优化,年中也由主站转岗到了国际站。


SSR & Star & 客户端



  • SSR 一些接入文档,最佳实践之类的文档编写

  • Star 权限管理;支持培训物料类目

  • 客户端监控体系的建设,接入现有监控平台

  • 客户端打包平台的持续优化等


国际站


国际站的技术沉淀基本等价于 3 年前的主站,所以还有很多问题需要解决,以及一些提效工具都没用上,感觉和大多数业务线有些脱节。
国际站有很大的特点:多语言、多货币、多集群,依赖的很多三方也不同,例如登录、支付场景。以上都和前端息息相关,其中和开发方式密切度非常高的,就是多语言。
而多语言,目前公司已有基建也比较完善:语言包 CDN 化 + 配置后台 + 项目粒度管理 + VSCode 插件提效。但是也由很多问题:治理困难,例如如何清理一些无用词条;验证困难,例如如何验证其他语种的有效性等。我目前还没有想到比较好的解决手段。


Magi 配置平台


除此之外,页面配置化的能力,对于运营可快速尝试各种增长手段也至关重要。目前运营会采用上文提到的 TMS 来搭建一些营销页、产品介绍页。但也有一些是不满足需求的:SEO、性能、多语言等。
除了 SEO 之外,另外 2 条通过优化 TMS 都还能解决。因为 TMS 的整体架构决定了,想要能支持 SSR 很难,更不必说内置的或二方的组件了。除了页面编排需求之外,还有这些诉求:



  1. 开发者编写的页面也需要有配置化的能力,而针对特定功能开发特定的后台,成本极高

  2. 页面配置化需要能根据国家、人群维度进行不同的展示

  3. 分站点,例如不同国家不同站点


为了满足以上需求,计划造一个低代码配置平台,以及低代码引擎。目前还处于非常早期的阶段,仅完成整体的架构设计和部分 Core & Editor 逻辑的编写。


总结


至此,酷家乐的旅程告一段落。
这段旅程里,做了很多针对研发效率、质量方面的工作,也为其他岗位角色(UED、运营、市场)带来了人效的提升。我相信每一份努力和效率的提升,都会让酷家乐进步一点点,让我们在这个竞争激烈的市场上赢得胜利的机会多一点点。在这里收获满满,未来祝愿群核科技越来越好!
再额外聊一下关于离职,我的看法。我们常看到某些同学因为个别同事的离职,而内心动摇,也决定离职,我曾经也这样。但是在加入酷家乐前,就告诉自己,直面自己内心,不要在乎他人的去留,只要能确定自己能有成长、有收获、与自己规划相符就足够了,共勉。



2023-12-08
于 良渚


作者:洋葱x
来源:juejin.cn/post/7310028335480619027
收起阅读 »

喊话各大流行UI库,你们的Select组件到底行不行啊?

web
各种 UI 库的 Select,你们能不能人性化一点! 最近在云效上合并代码,本想着懒的目的输入了非连续的关键字搜索分支,结果... 大概逻辑就是,搜索时必须输入连续的字母,比如,要找 “master-alpha”分支,非要输入 master-al 才能搜到...
继续阅读 »

各种 UI 库的 Select,你们能不能人性化一点!


最近在云效上合并代码,本想着懒的目的输入了非连续的关键字搜索分支,结果...


1.gif


大概逻辑就是,搜索时必须输入连续的字母,比如,要找 “master-alpha”分支,非要输入 master-al 才能搜到,像图中输入 “masal” 就完全搜索不到。这导致了很多场景下使用起来很不方便,例如我们只记得几个非连续的关键字,或者懒得打那么多连续的关键字来搜索,用户体验较差。


然后我又看了几个流行组件库的 Select。


Element-ui


2.gif


Antd


3.gif


Naive-ui


4.gif


全军覆没!


那我们来自己实现一个吧!先来两个实战图。


不带高亮的非连续搜索


6.gif


带高亮的非连续搜索


5.gif


实现不带高亮的非连续搜索


以vue3+ElementUI为例,在这里将会用到一个小小的js库叫sdm2来实现非连续的字符串匹配。


视图部分


<el-select
v-model="value"
size="large"
placeholder="Filter options"
filterable
:filter-method="q => (query = q)">

<el-option
v-for="item in optionsComputed"
:key="item.value"
:value="item.value"
:label="item.label">

el-option>
el-select>

没有什么特别的,就是加了个filterMethod函数将关键词赋值给query状态,然后optionsComputedquery值根据关键词进行筛选


import { match } from 'sdm2';

const options = [/* 选项列表 */];
const query = ref('');
const optionsComputed = computed(() => options.filter(({ label }) =>
// 使用sdm2的match函数筛选
match(label, query.value, {
// 忽略大小写匹配
ignoreCase: true,
}
)));

就这么简单完成了。


实现带高亮的非连续搜索


视图部分


高亮部分使用v-html动态解析html字符串。


<el-select
v-model="value"
size="large"
placeholder="Filter options"
filterable
:filter-method="q => (query = q)">

<el-option
v-for="item in optionsComputed"
:key="item.value"
:value="item.value"
:label="item.label">

<div v-html="item.highlight">div>
el-option>
el-select>

为了让匹配到的关键字高亮,我们需要将匹配到的关键字转换为html字符串,并将高亮部分用包裹,最后用v-html动态解析。


import { filterMap } from 'sdm2';

const options = [/* 选项列表 */];
const query = ref('');
const optionsComputed = computed(() =>
// 为了过滤出选项,并将到的转换为html字符串,此时我们要用sdm2的filterMap函数
filterMap(options, query.value, {
ignoreCase: true,

// 把matchStr返回的字符串作为被匹配项
matchStr: ({ label }) => label,

// 匹配到后转换为html高亮字符串
onMatched: (matchedStr) => `${matchedStr}`,

// 将匹配到的项转换转换为需要的格式,str为onMatched转换后的字符串,origin为数组的每项原始值
onMap: ({ str, origin }) => {
return {
highlight: str,
...origin
}
}
})
);

然后一个带高亮的非连续搜索就完成了。


总结


这样你的搜索库就又更智能点了吧,然后各位 UI 库作者,你们也可以考虑考虑这个方案,或者有哪位朋友愿意的话也可以去为他们提一个issue或PR。

作者:古韵
来源:juejin.cn/post/7310104657212178459
收起阅读 »

从一线城市回老家后的2023“躺平”生活

归家 22年的十月份,在上海工作了三年多的我回到了老家。 前端,20年二本毕业的,当时在上海看老家的招聘信息,感觉很棒,很心动。又因为公司在大裁员,刚刚好在最后一轮裁员的时候,被裁了,拿了赔偿金,因为房租没有到期,又最后玩了玩,回了老家。 现实的落差感    ...
继续阅读 »

归家


22年的十月份,在上海工作了三年多的我回到了老家。


前端,20年二本毕业的,当时在上海看老家的招聘信息,感觉很棒,很心动。又因为公司在大裁员,刚刚好在最后一轮裁员的时候,被裁了,拿了赔偿金,因为房租没有到期,又最后玩了玩,回了老家。


现实的落差感


    回到老家后,又休息了十几天吧,就开始看招聘的信息,之前在上海看着很心动的岗位,简历投了又投,要么回复你,岗位已招满,要么压根不理你(后来我才知道,是学历的问题,老家这边的国企,最低学历就是研究生了,压根不看你工作履历)。剩余那些搭理你的公司,大都是小公司,可能只有十个人左右,而且大多都是单休或者大小周,有些甚至五险一金都没有,工资也低的可怜,是之前的三分之一差不多。


    心里难免有很强烈的落差感,但是由于我们(们:我老公,那个时候我们还是情侣,从大学开始的,在一起快7年了)家都是在这边的,两边父母都在这,觉得我们之后就是要在这里发展的,我俩硬着头皮,每天划拉着招聘信息,投着简历,适当地去面试。


    中间有一家公司,我感觉还可以,然后想着去试试,干了两天半。


    刚去的第一天,技术团队是:一个后端和一个外包的后端,以及一个跟我一样刚入职的前端,一共就我们四个人,然后就是老板,只有我是女生。除此之外,还有保洁阿姨(中午会做饭,公司中午管饭)、人事小姐姐等一些非开发人员。下午开会,老板居然直接在会议室抽起了烟(熏得我不要不要的)!然后项目用的是ruoyi的架子,里面有些代码是那俩后端暂时写的,看起来有些乱。就这样干了两天,那俩后端,很爱抽烟,再加上老板也带头会议室开会还抽烟,整天感觉身边烟熏火燎的。


    到第三天的时候,中午开了个会,意思是,之前我们开发的好像需求都不行,并且又提了一堆新需求,还告诉我们说只有两三天的时间搞完。我就意识到不对劲,是逼着人加班,没死没活的干的那种。然后再加上被熏了三天,于是开完会,我就赶紧收拾着我的东西,跑路了,干不了,根本干不了。既不合理,而且办公环境很糟糕(每天烟熏火燎),还没有社保,据说年后才缴纳。下午他们打来电话,问我咋回事,还要给我加薪让我再回去,但我已决心不去那干了。后来的我一点都不后悔这样做,甚至觉得很明智。


    就这样继续在招聘软件上看着,有新岗位咯,就投,就面试。


     突然有一天的周日,我接到了一个电话,说我可以来上班,他们缴纳五险,是双休,还有餐补,并且薪资也比之前面试的也差不多(之前还有个公司给的薪资和他一样,但是他是大小周,我不想去),这种待遇的公司,对于目前的我来说,已经很可以了,然后我就同意了,并且两天后去入职,这家公司就是我现在的公司。相比之前那家“烟熏火燎”的公司,这家就正规了许多,可能因为总部在深圳吧。


我们订婚啦


既然工作稳定了,那就开始丰富生活。2023年02月05日,我们举办了订婚宴~




工作


    这边的前端工作不太是普通的传统前端,而是electron打包出来是个exe啊,或者是针对模型3d渲染引擎啊,依托于基于threeJs二次开发出来的一些第三方,之类的,总之跟之前做的不一样,之前的我做的都是h5、微信小程序、或者接入一些公众号之类的。所以与其说是在工作,不如说是一直在学习吧。公司也知道我不太会,于是乎就给我很长时间先学,先熟悉,然后再去一点点开发。并且我几乎没加过班。


我们结婚啦


后面一切按照计划进行,拍摄婚纱照、男方那边在忙着新房装修,我们这边在置办嫁妆、买车车等。


在2023年10月10日,我们举行了典礼。



安稳且平淡


现在的我们每天安安稳稳,我想着适当提升下自己的学历(因为我们这边的好单位,现在好像都要研究生毕业了),在看着咱们计算机考研408的一些科目(双11心血来潮,一下子买了六七百块的书,不看总觉得买书钱白瞎了TAT),但是每天下班回家,还是忍不住看一些电视剧啥的,佛系考研,阿弥陀佛,哈哈哈哈


我们从上海一直养到现在的猫猫~



这就是我跟大家分享的我的这2023年的一年的经历。说实话,回老家的确比在一线城市更真实,因为身边有父母,有家人,每个周末都可充实。一线城市是素质高、节奏快,人的整个思想境界感觉都跟老家这边的人不一样。但兜兜转转,回老家似乎也并不是“躺平”,有落差感,因为接触过好的了。反正无论怎样,感觉简单、安稳、快乐的过好每一天就挺好。我们一起加油吧~


作者:wenLi
来源:juejin.cn/post/7311206584205869096
收起阅读 »

年底事故频发,做前端会不会出大型事故?

炽天使-S-蛇女-甜甜果实 前言 一些乐子,一些思考,不喜勿喷,欢迎交流 最近崩的有点多,来看看都有哪些 语雀崩了... 阿里云崩了... 滴滴崩了... 腾讯视频崩了... ...... 刚看完《三体》三部曲,最后一部《三体3:死神永生》里面,宇宙因为...
继续阅读 »

炽天使-S-蛇女-甜甜果实



前言



一些乐子,一些思考,不喜勿喷,欢迎交流



最近崩的有点多,来看看都有哪些


语雀崩了...


阿里云崩了...


滴滴崩了...


腾讯视频崩了...


......


刚看完《三体》三部曲,最后一部《三体3:死神永生》里面,宇宙因为质量流失过大,也快崩溃死亡了,也不知道会不会出个后续,归零者把宇宙的宏观维度重新回到十维的情况...


......


似乎什么都在崩,哪哪都在崩,好在掘金没有崩,不然掘友都看不到文章了,给掘金点赞一波,服务稳住老狗



继续回到主题上,做个前端开发工程师会不会导致项目出现大型事故?


前端都包含的职责


关于前端这个岗位负责的部分,分三个大类来分析讨论



  1. 只负责开发页面的前端

  2. 负责页面开发,也负责基础架子搭建和组件开发的前端

  3. 负责开发、基础建设、服务部署等等相关工作的前端


1. 只负责开发页面的前端


项目架子是别人搭好的,大部分核心或共同组件是别人写好的,或者负责维护的


日常工作具体内容包括但不限于,使用 vue, react 等框架开发业务,实现各种与后端的数据交互,展示效果,以及用一些库或者手写一些组件库之类的实现某些特定的业务效果或场景


如果项目经过的必要的功能测试,并运行稳定的项目,如果出现突发大型事故,百分之九十九点九的问题,都是运维或后端服务方面的,需要背锅侠大概率也轮不到前端


咱该吃吃该喝喝,遇事别往心里搁



2. 负责页面开发,也负责基础架子搭建和组件开发的前端


在稍有规模的团队中,负责前端工程搭建,一些通用组件封装,以及打包相关的配置,包括一些基础的性能优化等工作的一般是前端老鸟,或者项目中的资深技术选手做的,这种角色一般是比较熟悉业务了或者是团队中的主力选手了


如果项目中遇到突发大型事故,一般是冲在一线,虽然问题百分之九十的可能不是前端范畴,剩下的那百分之十还是很有可能的,例如那一年的开源项目 Ant Design 圣诞节彩蛋事件,在某些公司直接彩蛋变炸弹💣


由于官方没有通知,也排查不出问题,谁知道这玩意儿竟然是个官方出的圣诞节彩蛋,如果需要背锅侠,很可能直接就是前端开发相关负责人背锅了



3. 负责开发、基础建设、服务部署等等相关工作的前端


这种前端一般多少涉及点全栈了,相关服务的操作权限可能也有。一般微小型公司这种情况比较常见,一人多职,稍微人多点的中大型公司服务器运维相关岗位,这种一般都有专门的部门或单独岗位的人负责统一管理,这种角色可能是一线负责人或者技术团队负责人等等


在中大型公司,这种什么都参与,也有相关权限的,可能资深大拿,或者技术部负责人之类的,如果是负责人一般也很少直接参与业务开发了


如果项目遇到突发大型事故,一线负责人一般有事儿都是第一时间知道,然后第一时间协调资源或者参与问题解决,小公司中可能老板都会直接参与协调解决,这种核心选手在小公司中一般不会受到什么处罚,如果是大公司中需要有人承担责任的,一般也可以甩锅出去,找个具体干活的开发背锅



分享身边发生过的一件事


小创业公司,一天去潜在客户那边演示项目,由于做的是 toB 的项目,目前还是自用阶段,没对外开放,日常都在公司是内网开发测试使用,然后老板有天安排去xx公司演示,然后团队中俩人就去了,俩人提前一个小时多到的,在他们连好投屏显示器后准备先点点看看,结果发现项目不能登录😅😅😅


赶紧排查原因,原来后端接口服务域名地址没有对外开放,外网不能访问


当时那俩人拿的是公司的mac演示本,没有装远程工具之类的,负责运维的那个人不在公司,在公司的只了解一点点运维,也就会点 Linux 了解 Nginx 那种,操作方面和专业运维差远了,运维电话联系指挥操作弄了约半小时也没好,后来放弃了现场演示了,改成只讲一下 PPT 了,最后不知道和那边是怎么交代的,反正后来没和那边的公司升级成商业合作伙伴


这时候背不背锅可能意义不大了,小创业公司如果是重要的演示直接搞砸了,公司的业务可能直接就没有了,公司活着都是个问题,员工工资能不能正常发都得看工资的家底支不支持了


怎么做能大概率不背锅


明确职责,积极沟通解决问题,多产出等等这些因素是一方面,人是社会动物,有人的地方就有江湖,有江湖的地方也可能需要下面的因素



  • 在团队中保持较强的竞争力,让领导觉的你性价比高

  • 大量参与核心业务开发,让领导觉的替换了你以后其他人上手代价高

  • 成为组织或团队的核心圈人员,和领导混熟了,凡事好商量(重要!!!)


写在最后


一般来说是做的越多,责任越大;认真对待工作,出问题也不用怕,兵来将挡,水来土掩...


这年头各路公司都在 “开猿劫留,减猿增笑”,日常中的我们也需要进行一些准备,以应对突如其来的事故或者变故,以不变应万变...


每个人的经历,认知都是不一样的,同样的人不同角度下的世界也是不一样的,有不同意见是非常正常的,欢迎探讨交流不一样的心得,互相学习,共同进步



如果喜欢本文章或感觉文章有用,用你那发财的小手点赞、收藏、关注再走呗 ^_^ 


微信公众号:草帽Lufei




作者:草帽lufei
来源:juejin.cn/post/7311602153783705627
收起阅读 »

Ant Design Mini 支持微信小程序啦!

web
Ant Design Mini 经过一段时间的开发与不断的努力,我们兴奋地宣布,Ant Desigin Mini 组件库中已有 16 个核心组件完成了微信小程序的适配工作!现在你不仅可以在支付宝小程序中使用 Ant Desigin Mini 组件库,也可以在微...
继续阅读 »

Ant Design Mini


经过一段时间的开发与不断的努力,我们兴奋地宣布,Ant Desigin Mini 组件库中已有 16 个核心组件完成了微信小程序的适配工作!现在你不仅可以在支付宝小程序中使用 Ant Desigin Mini 组件库,也可以在微信小程序中使用了!


目前这项适配正处于 Beta 阶段,我们诚挚地邀请大家前来体验。首批适配的组件包括:ButtonSliderContainerIconLoadingSwitchTagInputCalendarListResultPopoverMaskStepperPopupCheckbox


先来看看 Demo 效果:
image.png


我们的官网文档、Demo 都已同步更新双端切换能力:
2023-11-28 14.44.51.gif


你可以参考以下文档进行接入:



双平台无法对齐的特性


受制于各平台框架设计,以下这些特性存在差异:



  • 两个平台的事件 API 不同。支付宝小程序可以把实例上通过 props 传递给子组件,而微信需要在 data 里传递函数。视图层的写法也所有不同。

    • 下面是 Calendar 在两种平台的使用方式。

      • 微信小程序






Page({
data:{
handleFormat() {
}
}
})

<calendar onFormat="{{handleFormat}}" />

  - 支付宝小程序

Page({
handleFormat() {

}
})
<calendar onFormat="handleFormat" />


  • 微信小程序不支持在 slot 为空时显示默认值, 所以我们在微信平台取消了部分 slot,对应会损失一些定制能力。 比如说 Calendar 组件, 在支付宝可以通过 calendarTitle 这个 slot 定制标题,在微信端只能通过 css 来控制样式。


<View class="ant-calendar-title-container">
{/* #if ALIPAY */}
<Slot name="calendarTitle">
{/* #endif */}
<View class="ant-calendar-title">{currentMonth.title}</View>
{/* #if ALIPAY */}
</Slot>
{/* #endif */}
</View>


  • 微信小程序不支持循环渲染 slot , 所以部分组件无法迁移到微信, 比如说 IndexBar 组件, 使用了 Slot 递归渲染整个组件的内容。这种写法无法迁移到微信。


<view a:for="{{items}}">
<slot
value="{{item}}"
index="{{index}}"
name="labelPreview" />

</view>

双平台适配背后的工程技术


下面我们为大家介绍一下 Antd Mini 支持多平台背后的一些工程方案。


中立的模板语法: 使用 tsx 开发小程序


由于支付宝和微信的小程序的语法各有差异,为了解决让 Antd Mini 同时支持两个端,我们团队选择的 tsx 的一个子集作为小程序的模板语言。
使用 tsx 具有以下优势:



  • 可以直接使用 babel 解析代码,无需自己开发编译器。

  • 各个 IDE 原生支持 TSX 的类型推导与代码提示。

  • 视图层和逻辑层可以复用同一份 props 类型。

  • 可以直接通过 import 导入其他的小程序组件,使用 typescript 进行类型检查。

  • 视图层脚本也可以享受类型校验,无需依赖平台 IDE


由于小程序的视图语法比较受限,从 tsx 向跨平台视图语法转换是比较容易的。我们基于 babel 开发了一个简单的编译器,解析原先 tsx 的语法树以后,将 React 的语法平行转换为可读性比较强的小程序视图语法。
具体举例来看:



  • 条件判断 : 我们使用了 &&以及 ?: 三元表达式替代了之前的 :if 标签。

    • tsx: !!a && <Text>a</Text>

    • 微信小程序:<text wx:if="{{ !!a }}" />

    • 支付宝小程序:<text wx:if="{{ !!a }}" />



  • 循环: 我们使用了 map 代替之前的 :for 标签,从源码里自动分析出 :for-item :for-index :key 等标签。

    • tsx:




{todoList.map((task, taskIndex) => (
<Text
hidden={!mixin.value}
key={task.id}
data-item-id={taskIndex}
data-num={20}
>

{taskIndex} {task}
</Text>

))}


  • 微信小程序:


<block
wx:for="{{ todoList }}"
wx:for-index="taskIndex"
wx:for-item="task"
wx:key="{{ task.id }}">
<!-- display: inline -->
<text
hidden="{{ !mixin.value }}"
data-item-id="{{ taskIndex }}"
data-num="{{ 20 }}"
>
{{ taskIndex }}{{ task }}</text
>
</block>



  • 支付宝小程序


  <block
a:for="{{ todoList }}"
a:for-index="taskIndex"
a:for-item="task"
a:key="{{ task.id }}">
<!-- display: inline -->
<text
hidden="{{ !mixin.value }}"
data-item-id="{{ taskIndex }}"
data-num="{{ 20 }}"
>
{{ taskIndex }}{{ task }}</text
>
</block>



  • 事件绑定: 我们会按照配置,自动将直接转换为两个平台的格式。

    • tsx:<Text onClick="handleClick" />

    • 微信小程序: <text bind:click="handleClick" />

    • 支付宝小程序: <text onClick="handleClick" />



  • 视图层脚本

    • 我们还规定了以 sjs.ts 作为视图层脚本的文件格式。 在编译时候转换为对应平台的文件格式。

      • tsx: import helper from './helper.sjs.ts'

      • 微信小程序: <wxs src="./helper.wxs" module="helper" />

      • 支付宝小程序: <import-sjs src="./helper.sjs" module="helper" />





  • 类型方案

    • 为了让逻辑层类型与视图层关联,我们设计了一些工具类型。 比如说下面使用的 TSXMLProps,将 IProps 的 onClick 转换成了字符串。




// Calendar.axml.tsx
import { TSXMLProps, View } from 'tsxml';

interface IProps {
className?: string;
style?: string;
onClick: (e) => void;
}

interface InternalData {
size: number;
}

export default (
{ className, style }: TSXMLProps<IProps>,
{ size }: InternalData
) => (
<View class={`ant-calendar ${className ? className : ''}`} style={style}>
{size}
</View>

);

// Page.axml.tsx

import Calendar from './Calendar.axml.tsx'

export default () => (<Calendar onClick="handleClick" />)

目前使用 tsx 的这套方案还存在一些限制:



  • 和小程序相同,一个文件内只能定义一个组件。

  • 如果使用自定义组件,需要配置组件事件在各个平台的写法。


老组件语法转换?用 AI 就行了


在决定使用 tsx 语法之后,我们还面临一个很棘手的工作量问题:如何把历史组件库 axml 代码全量转换为最新的 tsx 语法?
这时候就该 ChatGPT 出场了,我们请 AI 来帮助我们完成这个一次性转换工作。
为了让转换结果更靠谱,我们使用了一些技巧:



  • 使用了 tsx 编译器等测试用例作为 prompt ,让 AI 可以更好的了解 tsx 的写法。

  • 除了 tsx 文件以外,我们还将组件的 props.ts 与 config.json 加到了 propmt 里,可以帮助 AI 生成更好的 import 导入。


在这里,你可以看到这份转换的完整 prompt。


确保 AI 产出的正确性?再用我们的编译器转回来


为了确保 AI 产出的代码是正确的,我们使用编译器将 AI 编写的 tsx 重新编译回 axml ,再用 git diff 对原始代码做比对,由此即可核查 AI 转换的正确性。


当然,这两次转换的过程不会完全等价,比如转换 map 的过程中会出现一层嵌套的 <block/>。好在这样的差异不多,一般肉眼看一遍就能确认正确性了。


跨平台通用的组件逻辑:小程序函数式组件(functional-mini)


除了视图,我们还需要确保组件逻辑适配到双端。这里我们使用了小程序函数式组件( functional-mini )的形式来编写,functional-mini 的源码及文档放置均在 ant-design/functional-mini


使用了函数式组件后,Antd Mini 用上了计算属性、useEffect 等特性,也能通过 hooks 来替换原有的大量 mixin 实现,让代码的可维护性提升了一个台阶。


以典型的 Popover 组件为例,逻辑部分适配完成后,它的代码完全变成了 React 风格,数据变更流程一目了然:


const Popover = (props: IPopoverProps) => {
const [value] = useMergedState(props.defaultVisible, {
value: props.visible,
});
const [popoverStyle, setPopoverStyle] = useState({
popoverContentStyle: '',
adjustedPlacement: '',
});

useEffect(() => {
setPopoverStyle({
popoverContentStyle: '',
adjustedPlacement: '',
});
}, [value, props.autoAdjustOverflow, props.placement]);

return {
adjustedPlacement: popoverStyle.adjustedPlacement,
popoverContentStyle: popoverStyle.popoverContentStyle,
};
};

关于小程序函数式组件的原理、特性介绍,我们将在后续的分享中另行展开。


写在最后


欢迎大家一起来尝试 Ant Design Mini 的跨平台能力,你可以在 issue 区提出宝贵的建议与 Bug 反馈。


官网: mini.ant.design/


国内镜像:ant-design-mini.antgroup.com/


作者:支付宝体验科技
来源:juejin.cn/post/7311603519570952246
收起阅读 »

最全面包交友养网站甜蜜定制APP 学生老总竞争激烈

婚恋网站交朋友APP里白领学生礼模信息经常更新 ,为双方约星巴克咖啡厅见面谈了解清楚满意后开始相处包养的网站,女士大多上班或上学见面聊天相处可以看出来 都可长或短期相处私人助理 看缘分和新鲜感,微信993153133 为双方搭建一个相识的平台保护双方权利,张家...
继续阅读 »

婚恋网站交朋友APP里白领学生礼模信息经常更新 ,为双方约星巴克咖啡厅见面谈了解清楚满意后开始相处包养的网站,女士大多上班或上学见面聊天相处可以看出来 都可长或短期相处私人助理 看缘分和新鲜感,微信993153133 为双方搭建一个相识的平台保护双方权利,张家口北京每个城市都可提供双方认识的机会”

收起阅读 »

大厂前端开发规定,你也能写成诗一样的代码(保姆级教程)

web
BEM 使用起来很多人不晓得BEM是什么东西 我来解释给你们听  BEM是一种前端开发的命名方法论,全称为Block-Element-Modifier,中文翻译为块、元素、修饰符。BEM的主要思想是将页面分解成多个可重用、独立的模块,每个模块由一个主块(Blo...
继续阅读 »

BEM 使用起来

很多人不晓得BEM是什么东西 我来解释给你们听

  BEM是一种前端开发的命名方法论,全称为Block-Element-Modifier,中文翻译为块、元素、修饰符。BEM的主要思想是将页面分解成多个可重用、独立的模块,每个模块由一个主块(Block)和零个或多个元素(Element)组成,可以使用修饰符(Modifier)来描述模块的不同状态和变化。

BEM的命名规则如下:

  • 块(Block):一个独立的、可重用的组件,通常由一个或多个元素组成,并且有一个命名空间作为标识符。通常以单个单词命名,使用连字符分割单词,例如:menu、button、header等。
  • 元素(Element):块的组成部分,不能独立存在,必须隶属于某个块。元素的命名使用两个连字符“__”与块名分隔,例如:menu__item、button__text、header__logo等。
  • 修饰符(Modifier):描述块或元素的某种状态或变化,以单个单词或多个单词组成,使用两个连字符“--”与块或元素名分隔。例如:menu--horizontal、button--disabled、header__logo--small等。

  通过使用BEM命名方法论,可以实现更好的代码复用性、可维护性和可扩展性。BEM的命名规则清晰明了,易于理解和使用,可以有效地提高团队开发效率和代码质量。

page+ hd/bd/ft 用起来

"page+ hd/bd/ft" 是一种简化的命名约定,常用于网页布局中。下面是对这些缩写的解释:

  • page:页面的整体容器,表示整个页面的最外层包裹元素。
  • hd:代表页头(header),用于放置页面的标题、导航栏等顶部内容。
  • bd:代表页体(body),用于放置页面的主要内容,如文章、图片、表格等。
  • ft:代表页脚(footer),用于放置页面的底部内容,如版权信息、联系方式等。

  这种命名约定的好处是简洁明了,可以快速理解页面的结构和布局。通过将页面划分为页头、页体和页脚,可以更好地组织和管理页面的各个部分,提高代码的可读性和可维护性。

更好的使用工具(stylus插件)

,Stylus 是一种 CSS 预处理器,它允许你使用更加简洁、优雅的语法编写 CSS。通过在命令行中运行 npm i -g stylus 命令,你可以在全局范围内安装 Stylus,并开始使用它来编写样式文件。 .styl 是 Stylus 文件的扩展名,你可以使用 Stylus 编写样式规则。然后,你可以将这些编写好的 Stylus 文件转换为普通的 CSS 文件,以便在网页中使用。

  具体地说,你可以创建一个名为 common.styl 的文件,并在其中编写 Stylus 样式规则。然后,通过运行 stylus -w common.styl -o common.css 命令,你可以让 Stylus 监听 common.styl 文件的变化,并自动将其编译为 common.css 文件。

  以下是一份示例代码来说明这个过程:

  1. 创建 common.styl 文件,并在其中编写样式规则:
// common.styl
$primary-color = #ff0000

body
font-family Arial, sans-serif
background-color $primary-color

h1
color white
  1. 打开终端,进入 common.styl 文件所在的目录,运行以下命令:
Copy Code
stylus -w common.styl -o common.css

  这将启动 Stylus 监听模式,并将 common.styl 文件编译为 common.css 文件。每当你在 common.styl 文件中进行更改时,Stylus 将自动重新编译 common.css 文件,以反映出最新的样式更改。 请注意,为了运行上述命令,你需要先在全局范围内安装 Stylus,可以使用 npm i -g stylus 命令进行安装。

stylus的优点

  Stylus 作为一种 CSS 预处理器,在实际开发中有以下几个优点:

  1. 更加简洁、优雅的语法:Stylus 的语法比原生 CSS 更加简洁,可以让我们更快地编写样式规则,同时保持代码的可读性和可维护性。
  2. 变量和函数支持:Stylus 支持变量和函数,可以提高样式表的重用性和可维护性。通过使用变量和函数,我们可以在整个样式表中轻松更改颜色、字体等属性,而无需手动修改每个样式规则。
  3. 混合(Mixins)支持:Stylus 的混合功能允许我们将一个样式规则集合包装在一个可重用的块中,并将其应用于多个元素。这可以大大简化样式表的编写,并减少重复代码。
  4. 自动前缀处理:Stylus 可以自动添加适当的浏览器前缀,以确保样式规则在不同的浏览器中得到正确的渲染。
  5. 非常灵活的配置:Stylus 提供了非常灵活的配置选项,可以根据项目的需要启用或禁用不同的功能,例如自动压缩、源映射等。

  总之,Stylus 通过提供更加简洁、灵活的语法和功能,可以使我们更加高效地编写 CSS 样式表,并提高代码的可重用性和可维护性。

最后一个大招阿里的适配神器 flexible.js

  flexible.js 是一款由阿里巴巴的前端团队开发的移动端适配解决方案。它通过对 Viewport 的缩放和 rem 单位的使用,实现了在不同设备上的自适应布局。

具体来说,flexible.js 主要包括以下几个步骤:

  1. 根据屏幕的宽度计算出一个缩放比例,并将该值设置到 Viewport 的 meta 标签中。
  2. 计算出 1rem 对应的像素值,并将其动态设置到 HTML 元素的 font-size 属性中。
  3. 在 CSS 中使用 rem 单位来定义样式规则。这些规则会自动根据 HTML 元素的 font-size 属性进行适配。

  通过这种方式,我们可以实现在不同设备上的自适应布局。具体来说,我们只需要在 CSS 中使用 rem 单位来定义样式规则,而不需要关注具体的像素值。当页面在不同设备上打开时,flexible.js 会自动根据屏幕宽度和像素密度等信息进行适配,从而保证页面的布局和样式在不同设备上都可以得到正确的显示。

  需要注意的是,flexible.js 并不能完全解决移动端适配的所有问题,还有一些特殊情况需要我们手动处理。例如,一些图片或者 Canvas 等元素可能需要根据不同设备的像素密度进行缩放,而这些操作需要我们手动实现。不过,flexible.js 可以帮助我们简化移动端适配的工作,提高开发效率。

  下期我来教大家手动写适配器 喜欢的来个关注 点赞 这个也是以后写文章的动力所在 谢谢大家能观看我的文章 咱下期在见 拜拜


作者:扯蛋438
来源:juejin.cn/post/7303126570323443722

收起阅读 »

JS问题:简单的console.log不要再用了!试试这个

web
前端功能问题系列文章,点击上方合集↑ 序言 大家好,我是大澈! 本文约1500+字,整篇阅读大约需要3分钟。 本文主要内容分三部分,如果您只需要解决问题,请阅读第一、二部分即可。如果您有更多时间,进一步学习问题相关知识点,请阅读至第三部分。 1. 需求分析 一...
继续阅读 »

前端功能问题系列文章,点击上方合集↑


序言


大家好,我是大澈!


本文约1500+字,整篇阅读大约需要3分钟。


本文主要内容分三部分,如果您只需要解决问题,请阅读第一、二部分即可。如果您有更多时间,进一步学习问题相关知识点,请阅读至第三部分。


1. 需求分析


一般情况下,我们在项目中进行代码调试时,往往只会在逻辑中使用console.log进行控制台打印调试。


这种方式虽然比较常规直接,但是如果打印数据多了,就会导致你的控制台消息变得异常混乱。


所以,我们有了更好的选择,那就是console对象提供的其它API,来让我们能够更清晰的区分打印信息。


图片


2. 实现步骤


2.1 console.warn


当我们需要区分一些比较重要的打印信息时,可以使用warn进行警告提示。


图片



2.2 console.error


当我们需要区分一些异常错误的打印信息时,可以使用error进行错误提示。


图片


2.3 console.time/timeEnd


想看看一段代码运行需要多长时间,可以使用time


这对于需要一些时间的CPU密集型应用程序非常有用,例如神经网络或 HTML Canvas读取。


下面执行这段代码:


console.time("Loop timer")
for(let i = 0; i < 10000; i++){
    // Some code here
}
console.timeEnd("Loop timer")


结果如下:图片


2.4 console.trace


想看看函数的调用顺序是怎样的吗?可以使用trace


下面执行这段代码:



  function trace(){
    console.trace()
  }
  function randomFunction(){
      trace();
  }
  randomFunction()


setup中,randomFunction 调用trace,然后又调用console.trace


因此,当您调用 randomFunction 时,您将得到类似的输出,结果如下:


图片


2.5 console.group/groupEnd


当我们需要将一类打印信息进行分组时,可以使用group


下面执行这段代码:


console.group("My message group");

console.log("Test2!");
console.log("Test2!");
console.log("Test2!");

console.groupEnd()

结果如下:


图片



2.6 console.table


在控制台中打印表格信息,可以使用table


对!你没听错,就是让我们以表格形式展示打印信息。


如果使用log打印:


var person1 = {name: "Weirdo", age : "-23", hobby: "singing"}
var person2 = {name: "SomeName", age : "Infinity", hobby: "programming"}

console.log(person1, person2);

结果如下:


这样做是不是让数据看起来很混乱。


图片


反之,如果我们使用table输出:


var person1 = {name: "Weirdo", age : "-23", hobby: "singing"}
var person2 = {name: "SomeName", age : "Infinity", hobby: "programming"}

console.table({person1, person2})

结果如下:


怎么样!从来不知道控制台可以看起来如此干净,对吧!


图片


2.7 console.clear


最后,使用clear把控制台清空吧!


图片


3. 问题详解


3.1 可以自定义log的样式吗?


答案当然是可以的,只需要借助%c这个占位符。


%c 是console的占位符,用于指定输出样式或应用 CSS 样式到特定的输出文本。


但请注意,%c 占位符只在部分浏览器中支持,如 Chrome、Firefox 等。


通过使用 %c 占位符,可以在 console.log 中为特定的文本应用自定义的 CSS 样式。这样可以改变输出文本的颜色、字体、背景等样式属性,以便在控制台中以不同的样式突出显示特定的信息。


以下是使用%c 占位符应用样式的示例:


console.log("%c Hello, World!", 
  "color: red; font-weight: bold;border1px solid red;");

结果如下:


图片


通过使用 %c 占位符和自定义的样式规则,可以在控制台输出中以不同的样式突出显示特定的文本,使得输出更加清晰和易于识别。


这在调试和日志记录过程中非常有用,特别是当需要突出显示特定类型的信息或错误时。


结语


建立这个平台的初衷:



  • 打造一个仅包含前端问题的问答平台,让大家高效搜索处理同样问题。

  • 通过不断积累问题,一起练习逻辑思维,并顺便学习相关的知识点。

  • 遇到难题,遇到有共鸣的问题,一起讨论,一起沉淀,一起成长。

作者:程序员大澈
来源:juejin.cn/post/7310102466570321958
收起阅读 »

2023 闲聊开猿节流 降本增笑

前言 2023年大环境的影响,互联网行业真是难啊,裁员风声四起,言论不一,说互联网增长不如预期,存量运营也不再需要多少人手。各大互联网公司,一直持续着开源节流,降本增效的策略,裁掉一批人,直接降低固定成本。这真是一剂猛药啊,效果也是杠杠的。最后变成了 开猿节...
继续阅读 »

前言


2023年大环境的影响,互联网行业真是难啊,裁员风声四起,言论不一,说互联网增长不如预期,存量运营也不再需要多少人手。各大互联网公司,一直持续着开源节流,降本增效的策略,裁掉一批人,直接降低固定成本。这真是一剂猛药啊,效果也是杠杠的。最后变成了



开猿节流 降本增笑



语雀宕机、阿里云几次宕机,滴滴宕机,最近腾讯也宕机,难道剩下的都是写ppt汇报的吗!

哎,当决策者不懂”岁月静好”是怎么得来的时候,就是”负重前行”的人越来越少的时候。

最后,雪崩的时候,没有一片雪花是无辜的。


经典的笑话



一群年薪百万的在加班讨论给年薪10w不到降本增效



image.png

说点经历


说说我以前的一家公司的降本增效案例


背景:好几年前了,那时候环境没这么差,但是公司的盈利点,增长不如预期,老板很焦虑,带着高管团队去了趟延安,主题:重走长征路,学习先辈创业的艰辛。回来之后,在公司发起,“赚一块钱活动”,就是开动你聪明的脑瓜子,出出金点子,能达到降本增效的目的,所有部门,所有员工都需要参与,那活动就轰轰烈烈的开始了,相对还好,没开猿节流,达到降本增效!


插曲


行政部门,怎么做的呢?为了达到开源节流的目的,他们也是花了心思的。


控制电灯,比如办公区10个电灯泡,他们拿掉5个灯泡,那电费不是可以少支出一半,他们还真是这样执行的,直接整个办公区的电灯泡,拿掉了一半。没过多少天,引起了整个公司对行政部门做法的不满,闹到老板那里去了,老板还是比较务实的,直接把行政的负责人臭骂一顿,让恢复原样。这是降本增效吗,这是牺牲员工的利益达到的,如果这样,还不如在家办公,是不是房租 水电费都省了呢。


技术贴近业务


我在的部门主要是负责供应链系统的开发,比如:订单履约系统、库存系统、商品系统......,做技术的,怎么才能做到降本增效,想想挺难的。我们搞开发的,不就通过代码实现产品,同时保障系统的稳定运行,就OK了。不管怎么样,做技术的,还是得从技术的角度想想,能不能完成公司给的任务?


优化系统


复盘部门负责的所有系统,找出系统的性能瓶颈点,通过技术手段、一定的策略进行优化,比如:以前需要三台机器才能支撑目前的流量,系统优化后,两台就行,同时系统比优化前前性能还好,支撑的流量更大。那减少了一台机器,变相的减少了硬件支出的固定成本。


贴近业务,提高人效


没事找业务的人聊聊天,喝喝咖啡,你会得到意想不到的收获。开发一般获取的需求如下:


image.png


产品对业务提出的需求,也不一定能准确描述,提供比较好的解决方案,需求经过产品理解然后再输出到开发,开发如果不深层次挖掘,只是按照产品的设计,进行开发,跟现实还是又一定的差距的。借用黄晓明的经典名言:



我不要你觉得,我要我觉得



在跟业务聊天中,谈到我们的订单履约系统



  1. 用户一个订单包含多个商品,商品不在同一个仓库,需要多仓发货;商品库存不足,有库存的先发。好多订单,都需要人工进行查看,进行手工拆单,系统自动化吗?减轻点我们的工作压力。有了,自动化拆单

  2. 业务还发现一个用户特点,同一个用户,在时间间隔不到30分钟,连续下两个或多个订单,用户、地址、收货人姓名、电话、信息都一样。有了,合单,节约物流成本


通过以上交谈,我得出了两个需求点:



  • 自菜单拆单 当收到订单信息,查看是否在同一个仓库,如果不在,自动拆分单个仓库进行发货。如果订单商品中,有库存不足的商品,拆分订单,有库存的先发货。注意:用户看到的还是一个订单,只是商品对应的发货单不一样而已

  • 订单自动化合并 根据用户的下单规律,我们在订单下发仓库进行发货的时候,我们先延迟半个小时,看看在这半小时,是否有用户,再次下单,并且满足(买家ID、收货人姓名、电话、地址信息都一样)的订单合并到一个发货单里发货,订单与发货单对应关系N:1


通过上面的策略,自动化拆单,提供了人效,订单自动化合并,降低了物流成本,真正达到了降本增效,不是降本增笑,得到了公司的一致好评,技术人不单单会写代码,也能搞产品


提高个人的技术能力


能力:技术+沟通


沟通能力强,才能准确把握需求


技术能力强,写出高质量的代码,提高系统性能、稳定性。


这个单纯的提高人效,不怎么好衡量,周期比较长



总结:上面几点,是我们部门,通过技术能力,赋能业务,达到降本增效的目的。



应对开源节流 降本增效


提高个人能力


技术人立命之本:技术,先精后广,比如:我是Java开发,那Java这门语言好好的研究,熟练掌握,源码读一读。各种框架的使用、原理,什么场景使用什么技术做为解决方案,起码你掌握了这些,面试能过吧。接下来,有时间、有精力学学其他语言,多门语言,多一种优势吧。再说了,现在貌似又回到了过去,全栈这次词,提的越来越多了。当年诺基亚很火的时候,一大堆搞塞班开发的,后面诺基亚哑火了,你如果还坚守塞班,不学学安卓、ios 是不是基本就GG了?


chatgpt,真心强大,完全可以替代初级工程师,你还有什么理由,不提高自己的技术力呢!


沟通


有的时候,沟通比技术更重要。有人的地方就有江湖,江湖是什么,江湖是人情世故,不是打打杀杀。如果说技术能力是智商,那语言艺术就是情商。会说话,对于程序员来说,真的是硬伤,大部分程序员的世界,都是机器的世界, 0 1 世界 除了 0 1 哪来的2啊?为什么说干技术的干不过写ppt的,因为人家把你的功劳抢了啊,会说话,会汇报啊,别看不起这些人,这也是一种软实力。


没事多去领导面前刷刷脸,混个脸熟,这个比起你做了多少个需求,重要很多。起码提起你的时候,领导知道这个人是谁。


不要认死理,程序员的世界 不应该只有 0 1,应该有更多可能性,2 3 4 5...都可以有。领导就算放个屁,你也要觉得是香的(有点跪舔的意思了,但事实就是这么残酷,虽然我也没做到)领导的面子一定要给,好处不知道有没有,起码没坏处,起码领导觉得你态度端正,执行力强。


跟你工作上接触的人员,多沟通,处理好关系。第一:从别人那里可能得到一些你不知道的有用信息,也有可能收获好基友吧 第二:让周边的人认可你,公司也发展壮大,你的部门大领导可能都没跟你沟通过,如果要了解你,你的信息来源可能是别人对你的评价,有好的有坏的。如果刚好有升职加薪的机会给到你,结果因为别人的几句话,你就被否决了,是不是很亏。所以搞好同事间的关系很重要。


贴近业务


技术都是为业务服务的,再牛逼的技术脱离了业务,只能等死。因为业务不盈利啊,持续亏损,你说老板还留着你过年吗?不要说,我们技术都是按要求按质量根据产品的需求,去做的,系统稳定,线上也没出现过问题,业务不行跟我们技术有什么关系。我在一家公司,业务不好,技术也得分担一部分责任,why?你们开发的东西,是不是没达到业务的目标,这是真实的存在的,产品想的不一定是业务想的,技术理解的也不一定是产品想的。


以前我也一直以为,只要技术好,在哪里不是干。其实真不是这样的,你再好的技术,如果没有一些场景的解决方案,真是纸上谈兵,理论跟落地,差距太大了。比如,阿里云经常提到的的是异地多活,还不几次宕机,造成的损失,真不是金钱能衡量的,理论说的头头是道,但落地的时候,难度远远超过我们的想象,所以要贴近业务,真正做到技术落地,服务好业务


好多大厂出来的人,在细分行业自己创业,其实就是在公司的时候,就很关注业务,技术赋能业务,业务反哺技术。
当你懂技术,懂业务,这样的人,能开源掉吗?


防御性编程


防御性编程,貌似今年技术人应对开猿节流提出的。说是代码不写注释、文档不写,代码能有多烂就多烂,最好写成屎山代码。离开你,换个人根本没法维护,要不重构,别无他法。还有就是不要尽力,能做到100分,我只做到60分就好,剩下的40分是你的保命符,留一点个人上升的空间。带新人,随便带带,教会徒弟饿死师傅不能全部教会他,不然你离走人也不远了。还有很多说法,我就不一一列举了


这种观点,我不支持也不反对,根据自身的实际情况来决定是否使用,过河拆桥的事情也不少。


发展副业


俗话说:猫有九条命,形容猫的生存能力很强,没那么容易死


发展副业,发展副业,发展副业


image.png

发展副业,真的很重要。不要一味只知道工作,拼死累活的给干。不要被公司轻松拿捏你,副业好处如下:



  1. 增加收入:通过副业,你可以获得额外的收入来源,增加财务稳定性,改善生活品质。

  2. 提升技能:副业需要学习和掌握新的技能,这对你的个人和职业发展都是有益的。你可以通过副业开拓新的领域,提高自己的专业能力。

  3. 备用职业选择:副业可以成为你的备用职业选择,当主业遇到困难或变故时,你有一个备选的收入来源和职业发展路径。

  4. 实现梦想和兴趣:副业可以让你追求自己的梦想和兴趣。你可以选择从事自己喜欢的工作,追求个人的创造力和热情。

  5. 社交机会和网络拓展:通过副业,你可以结识更多的人,与更多领域的专业人士交流和合作。这有助于扩大你的人脉和拓展人际关系。


以上观点中,我认为 备用职业选择,这个最重要,起码在公司在开展降本增效,开源节流的时候,你心不慌吧,没有这份工作,我也能活的好好的,起码能保证生活吧


怎么发展副业呢,可能有人看到这,会问,有什么副业途径呢?送外卖算副业吗?只要体力好,能干得了也算。开滴滴算吗?算啊。摆地摊算吗?算啊,除了主业,通过其他赚钱的途径,都是副业。


复盘自己,审视自己,找一个相对适合自己的。我个人也在找,也在尝试。


总结


公司发展到一定阶段,肯定会遇到瓶颈期,如果过不去,开源节流,降本增效,势在必行,公司也要活下去啊,如果公司不在了,全部一起手拉手走,还能怎么办?只是在执行的过程中,人为因素太大了,有能力的可能走了,写ppt、嘴活好的留下了,结果公司的线上服务宕机了,阿里、滴滴宕机事故损失的,比起裁员省的那点钱,简直没可比对性。



雪崩的时候,没有一片雪花是无辜的



作者:柯柏技术笔记
来源:juejin.cn/post/7310787455112495139
收起阅读 »

为啥IoT(物联网)选择了MQTT协议?

物联网设备要实现互相通信,须一套标准通信协议,MQTT(Message Queuing Telemetry Transport)专为物联网设备设计的一套标准消息队列通信协议。使用MQTT协议的IoT设备,可以连接到任何支持MQTT协议的消息队列上,进行通信。 ...
继续阅读 »

物联网设备要实现互相通信,须一套标准通信协议,MQTT(Message Queuing Telemetry Transport)专为物联网设备设计的一套标准消息队列通信协议。使用MQTT协议的IoT设备,可以连接到任何支持MQTT协议的消息队列上,进行通信。



  • 宏观,MQTT和其他MQ传输协议差不多。也是“发布-订阅”消息模型

  • 网络结构,也是C/S架构,IoT设备是客户端,Broker是服务端,客户端与Broker通信进行收发消息


但毕竟使用场景不同,所以,MQTT和普通MQ比,还有很多区别。


1 客户端都运行在IoT设备


1.1 IoT设备特点


① 便宜


最大特点,一个水杯才几十块钱,它上面智能模块成本十块钱最多,再贵就卖不出去。十块钱的智能设备内存都是按KB计算,可能都没有CPU,也不一定有os,整个设备就一个SoC(System on a Chip)。这样的设备就需要通信协议不能复杂,功能不能太多。


② 无线连接


IoT设备一般采用无线连接,很多设备经常移动,导致IoT设备网络连接不稳定,且是常态。


MQTT协议设计上充分考虑这些特点。协议的报文设计极简,惜字如金。协议功能也非常简单,基本就只有:



  • 发布订阅主题

  • 收发消息


这两个最核心功能。为应对网络连接不稳定问题,MQTT增加机制:



  • 心跳机制,可让客户端和服务端双方都能随时掌握当前连接状态,一旦发现连接中断,可尽快重连

  • 会话机制,在服务端来保存会话状态,客户端重连后就可恢复之前会话,继续收发消息。这样,把复杂度转移到服务端,客户端实现更简单


2 服务端高要求


MQTT面临的使用场景中,服务端需支撑海量IoT设备同时在线。


普通的消息队列集群,服务的客户端都运行在性能强大的服务器,所以客户端数量不会特别多。如京东的JMQ集群,日常在线客户端数量大概十万左右,就足够支撑全国人民在京东买买买。


而MQTT使用场景中,需支撑的客户端数量,远不止几万几十万。如北京交通委若要把全市车辆都接入进来,就是个几百万客户端的规模。路侧的摄像头,每家每户的电视、冰箱,每个人随身携带的各种穿戴设备,这些设备规模都是百万、千万级甚至上亿级。


3 不支持点对点通信


MQTT协议的设计目标是支持发布-订阅(Publish-Subscribe)模型,而不是点对点通信。


MQTT的主要特点之一是支持发布者(Publisher)将消息发布到一个主题(Topic),而订阅者(Subscriber)则可以通过订阅相关主题来接收这些消息。这种模型在大规模的分布式系统中具有很好的可扩展性和灵活性。因此,MQTT更适合用于多对多、多对一的通信场景,例如物联网(IoT)应用、消息中间件等。


虽然MQTT的设计目标不是点对点通信,但在实际使用中,你仍然可以通过一些设计来模拟点对点通信。例如,使用不同的主题来模拟点对点通信,或者在应用层进行一些额外的协议和逻辑以实现点对点通信的效果。


一般做法都是,每个客户端都创建一个以自己ID为名字的主题,然后客户端来订阅自己的专属主题,用于接收专门发给这个客户端的消息。即MQTT集群中,主题数量和客户端数量基本是同一量级。


4 MQTT产品选型


如何支持海量在线IoT设备和海量主题,是每个支持MQTT协议的MQ面临最大挑战。也是做MQTT服务端技术选型时,需重点考察技术点。


开源MQTT产品


有些是传统MQ,通过官方或非官方扩展,实现MQTT协议支持。也有一些专门的MQTT Server产品,这些MQTT Server在协议支持层面,大多没问题,性能和稳定性方面也都满足要求。但还没发现能很好支撑海量客户端和主题的开源产品。why?


传统MQ


虽可通过扩展来支持MQTT协议,但整体架构设计之初,并未考虑支撑海量客户端和主题。如RocketMQ元数据保存在NameServer的内存,Kafka是保存在zk,这些存储都不擅长保存大量数据,所以也支撑不了过多客户端和主题。


另外一些开源MQTT Server


很多就没集群功能或集群功能不完善。集群功能做的好的产品,大多都把集群功能放到企业版卖。


所以做MQTT Server技术选型,若你接入IoT设备数量在10w内,可选择开源产品,选型原则和选择普通消息队列一样,优先选择一个流行、熟悉的开源产品即可。


若客户端规模超过10w量级,需支撑这么大规模客户端数量,服务端只有单节点肯定不够,须用集群,并且这集群要支持水平扩容。这时就几乎没开源产品了,此时只能建议选择一些云平台厂商提供的MQTT云服务,价格相对较低,也可选择价格更高商业版MQTT Server。


另外一个选择就是,基于已有开源MQTT Server,通过一些集成和开发,自行构建MQTT集群。


5 构建一个支持海量客户端的MQTT集群


MQTT集群如何支持海量在线的IoT设备?
一般来说,一个MQTT集群它的架构应该是这样的:



从左向右看,首先接入的地址最好是一个域名,这样域名后面可配置多个IP地址做负载均衡,当然这域名不是必需。也可直接连负载均衡器。负载均衡可选F5这种专用的负载均衡硬件,也可Nginx这样软件,只要是四层或支持MQTT协议的七层负载均衡设备,都可。


负载均衡器后面要部署一个Proxy集群


Proxy集群作用



  • 承接海量IoT设备连接

  • 维护与客户端的会话

  • 作为代理,在客户端和Broker之间进行消息转发


在Proxy集群后是Broker集群,负责保存和收发消息。


有的MQTT Server集群架构:



架构中没Proxy。实际上,只是把Proxy和Broker功能集成到一个进程,这两种架构本质没有太大区别。可认为就是同一种架构来分析。


前置Proxy,易解决海量连接问题,由于Proxy可水平扩展,只要用足够多的Proxy节点,就可抗海量客户端同时连接。每个Proxy和每个Broker只用一个连接通信即可,这对每个Broker来说,其连接数量最多不会超过Proxy节点的数量。


Proxy对于会话的处理,可借鉴Tomcat处理会话的两种方式:



  • 将会话保存在Proxy本地,每个Proxy节点都只维护连接到自己的这些客户端的会话。但这要配合负载均衡来使用,负载均衡设备需支持sticky session,保证将相同会话的连接总是转发到同一Proxy节点

  • 将会话保存在一个外置存储集群,如Redis集群或MySQL集群。这样Proxy就可设计成完全无状态,对负载均衡设备也没特殊要求。但这要求外置存储集群具备存储千万级数据能力,同时具有很好性能


如何支持海量主题?


较可行的解决方案,在Proxy集群的后端,部署多组Broker小集群,如可以是多组Kafka小集群,每个小集群只负责存储一部分主题。这样对每个Broker小集群,主题数量就可控制在可接受范围内。由于消息是通过Proxy进行转发,可在Proxy中采用一些像一致性哈希等分片算法,根据主题名称找到对应Broker小集群。这就解决支持海量主题的问题。


UML


Proxy的UML图:


@startuml
package "MQTT Proxy Cluster" {
class MQTTProxy {
+handleIncomingMessage()
+handleOutgoingMessage()
+produceMessage()
+consumeMessage()
}

class Client {
+sendMessage()
+receiveMessage()
}

class Broker {
+publish()
+subscribe()
}

Client --> MQTTProxy
MQTTProxy --> Broker
}
@enduml

@startuml
actor Client
entity MQTTProxy
entity Broker

Client -> MQTTProxy : sendMessage()
activate MQTTProxy
MQTTProxy -> Broker : produceMessage()
deactivate MQTTProxy
@enduml

@startuml
entity MQTTProxy
entity Broker
actor Client

Broker -> MQTTProxy : publishMessage()
activate MQTTProxy
MQTTProxy -> Client : consumeMessage()
deactivate MQTTProxy
@enduml


Proxy收发消息的时序图:



Proxy生产消息流程的时序图:



Proxy消费消息流程的时序图:


image-20231208134111361

6 总结


MQTT是专门为物联网设备设计的一套标准的通信协议。这套协议在消息模型和功能上与普通的消息队列协议是差不多的,最大的区别在于应用场景不同。在物联网应用场景中,IoT设备性能差,网络连接不稳定。服务端面临的挑战主要是,需要支撑海量的客户端和主题。


已有的开源的MQTT产品,对于协议的支持都不错,在客户端数量小于十万级别的情况下,可以选择。对于海量客户端的场景,服务端必须使用集群来支撑,可以选择收费的云服务和企业版产品。也可以选择自行来构建MQTT集群。


自行构建集群,最关键技术点,就是通过前置Proxy集群解决海量连接、会话管理和海量主题:



  • 前置Proxy负责在Broker和客户端之间转发消息,通过这种方式,将海量客户端连接收敛为少量的Proxy与Broker之间的连接,解决了海量客户端连接数的问题

  • 维护会话的实现原理,和Tomcat维护HTTP会话一样

  • 海量主题,可在后端部署多组Broker小集群,每个小集群分担一部分主题这样的方式来解决


参考:



作者:JavaEdge在掘金
来源:juejin.cn/post/7310786611805929499
收起阅读 »

uniapp日常总结--uniapp页面传值

uniapp日常总结--uniapp页面传值在Uniapp中,不同页面之间传值可以通过以下几种方式实现:URL参数传递:可以通过在跳转链接中添加参数,然后在目标页面通过this.$route.params或this.$route.query来获取传递的参数。 ...
继续阅读 »

uniapp日常总结--uniapp页面传值

在Uniapp中,不同页面之间传值可以通过以下几种方式实现:

  1. URL参数传递:

    可以通过在跳转链接中添加参数,然后在目标页面通过this.$route.paramsthis.$route.query来获取传递的参数。


    <uni-link :url="'/pages/targetPage/targetPage?param1=' + value1 + '¶m2=' + value2">跳转到目标页面uni-link>
    // 在目标页面获取参数
    export default {
    mounted() {
    const param1 = this.$route.params.param1;
    const param2 = this.$route.params.param2;
    console.log(param1, param2);
    }
    }
  2. 使用页面参数(Query):

    1. 在触发页面跳转的地方,例如在一个按钮的点击事件中:
    // 在当前页面的某个事件触发时跳转到目标页面,并传递参数
    uni.navigateTo({
    url: '/pages/targetPage/targetPage',
    // 传递的参数,可以是一个对象
    success(res) {
    console.log(res);
    },
    fail(err) {
    console.error(err);
    },
    // 参数传递方式,query 表示通过 URL 参数传递
    // params 表示通过 path 参数传递
    // 一般情况下使用 query 就可以了
    // 使用 params 时,目标页面的路径需要定义成带参数的形式
    // 如 '/pages/targetPage/targetPage/:param1/:param2'
    method: 'query',
    // 要传递的参数
    query: {
    key1: 'value1',
    key2: 'value2'
    }
    });



    //简写 在当前页面的某个事件触发时跳转到目标页面,并传递参数
    uni.navigateTo({
    url: '/pages/targetPage/targetPage?key1=value1&key2=value2',
    });
    1. 在目标页面中,可以通过this.$route.query来获取传递的参数:
export default {
onLoad(query) {
// 获取传递的参数
const key1 = this.$route.query.key1;
const key2 = this.$route.query.key2;

console.log(key1, key2);
},
// 其他页面生命周期或方法等
};

在目标页面的onLoad生命周期中,this.$route.query可以获取到传递的参数。key1key2就是在跳转时传递的参数。如果使用uni.switchTab方法进行页面跳转,是无法直接传递参数的。因为uni.switchTab用于跳转到 tabBar 页面,而 tabBar 页面是在底部显示的固定页面,不支持传递参数。如果需要在 tabBar 页面之间传递参数,可以考虑使用全局变量、本地存储等方式进行参数传递。

  • Vuex状态管理:

    使用Vuex进行全局状态管理,可以在一个页面中修改状态,而在另一个页面中获取最新的状态。

    适用于需要在多个页面之间共享数据的情况。

    如果你的应用使用了Vuex,可以在一个页面的computed属性或methods中触发commit,然后在另一个页面通过this.$store.state获取值。

    在第一个页面:

    // 在页面中触发commit
    this.$store.commit('setValue', value);

    在第二个页面:

    // 在另一个页面获取值
    const value = this.$store.state.value;
    console.log(value);
  • 使用本地存储(Storage):

    使用本地存储(localStorage或uni提供的存储API)将数据存储到本地,然后在另一个页面中读取。适用于需要持久保存数据的情况。如果数据不大,你也可以将数据存储在本地存储中,然后在目标页面读取。

    其中根据使用情景可以使用同步StorageSync或者异步Storage来实现。

    两者存在一定的区别,简单介绍可以查看下方链接:

    uniapp日常总结--setStorageSync和setStorage区别

    同步:使用uni.setStorageSyncuni.getStorageSync等方法,将数据存储在本地,然后在另一个页面读取。

    // 在页面A中保存数据到本地存储
    uni.setStorageSync('key', value);
    // 在页面B中从本地存储中读取数据
    const value = uni.getStorageSync('key');
    console.log(value);

    异步:使用uni.setStorageuni.getStorage等方法,将数据存储在本地,然后在另一个页面读取。

    // 在页面A中保存数据到本地存储
    uni.setStorage({
    key: 'yourDataKey',
    data: yourData,
    });
    // 在页面B中从本地存储中读取数据
    uni.getStorage({
    key: 'yourDataKey',
    success: function (res) {
    const pageData = res.data;
    },
    });
  • 事件总线:

    使用uni提供的API进行页面传值,如uni.$emituni.$on

    通过事件触发和监听的方式在页面之间传递数据。

    使用Uniapp的事件总线来进行组件之间的通信。在发送组件中,使用uni.$emit触发一个自定义事件,并在接收组件中使用uni.$on监听这个事件。

    在发送组件:

    uni.$emit('customEvent', data);

    在接收组件:

    uni.$on('customEvent', (data) => {
    console.log(data);
    });
  • 应用全局对象:

    通过uni.$app访问应用全局对象,从而在不同页面之间共享数据。

    在发送页面:

    uni.$app.globalData.value = data;

    在接收页面:

    const value = uni.$app.globalData.value;
    console.log(value);
  • URL参数传递对于简单的场景比较方便。Vuex适用于较大的应用状态管理。本地存储适用于需要在页面刷新后仍然保持的简单数据。事件总线方法适用于简单的组件通信。页面参数相对常用于跳转。根据具体需求和应用场景,选择合适的方式进行数据传递。不同的场景可能需要不同的方法。


    作者:狐说狐有理
    来源:juejin.cn/post/7310786618390855717

    收起阅读 »

    TS中,到底用`type`还是`interface`呢?

    web
    结论 直接说结论,用type一把梭即可,除非你要发布到npm,接下来我一一解释 为什么定义对象都要使用type呢? 如图所示,我鼠标悬浮后,并不知道里面是什么东西 只能获取结果时调出代码提示,或者ctrl + 鼠标左键进入,查看类型定义 那么我用type呢? ...
    继续阅读 »

    结论


    直接说结论,用type一把梭即可,除非你要发布到npm,接下来我一一解释


    为什么定义对象都要使用type呢?


    如图所示,我鼠标悬浮后,并不知道里面是什么东西


    只能获取结果时调出代码提示,或者ctrl + 鼠标左键进入,查看类型定义


    那么我用type呢?


    image.png


    可以看到,现在鼠标悬浮能直接查看类型定义了


    这一点是让我最受不了的,所以直接选择type即可


    image.png


    区别


    1. 如何继承



    先看看interface,通过extends关键字



    image.png



    type,则通过交叉类型。不过我认为interface好看点



    image.png


    2. 其他特性



    interface重写时



    • 如果有不同的属性,则会添加;

    • 如果是相同的属性但是类型不同,则会报错;



    这点有好有坏,当你不小心名字重复了,那你就容易出问题


    但同时利于扩展,不过没有人会这么写吧?


    直接去原来的接口添加属性不行吗?


    唯一的场景,就是开发工具库后。别人使用你的工具时,可以为你扩展类型


    image.png


    3. type独有的优势


    除了上面的悬浮能查看具体类型外,type还提供了很多的关键字使用,这是interface不具备的


    比如in关键字,用来枚举类型


    这里我写个删除属性的泛型,和Omit一样的,但是interface不支持


    此外还有很多TS特有的关键字,都只能通过type使用,比如infer


    不过这也符合直觉,因为interface就是定义一个类型而已


    image.png


    经过以上探讨,可以得出一个结论


    平时开发可以都用type


    发布工具库给别人用时,用interface


    作者:寅时码
    来源:juejin.cn/post/7304867327752912906
    收起阅读 »

    个人代码优化技巧

    web
    背景 贴近目前公司的业务,做的增删改查比较多。基本上都是做一些表格的业务系统比较多,因此在写的过程中,都会遇到一些优化的细点,仅供参考,觉得好的可以采纳,不好的欢迎提出来。 一、import按需加载 很多小伙伴肯定不少看到过,性能优化路由要用import(...
    继续阅读 »

    背景



    贴近目前公司的业务,做的增删改查比较多。基本上都是做一些表格的业务系统比较多,因此在写的过程中,都会遇到一些优化的细点,仅供参考,觉得好的可以采纳,不好的欢迎提出来。



    一、import按需加载


    很多小伙伴肯定不少看到过,性能优化路由要用import('@/views/xxxx.vue')这样就可以按需加载了。
    本身的vue-cli自动创建出来的时候也会有这一条语句。除了给路由优化之外呢,还有别的场景优化空间呢?那肯定有的啦。那就是结合<component/>自带的组件去一起实现。


    场景呈现


    正常情况下,做一个业务模块,都会分为【基础表】、【业务表】,一般情况下,用户维护好了基础表信息了之后,剩下的就是信息交叉复用,有可能在某个业务页面,我需要点击某个按钮后根据某个值到某个基础表的页面进行搜索信息,并勾选行信息。


    <template>
    <div>
    <div class="count" @click="showDynamicComponent">按需加载页面</div>
    <Modal title="动态数据" :visible="visible" @ok="()=>dynamicComponent=null">
    <component :is="dynamicComponent" ref="dynamicRef"/>
    </Modal>
    </div>

    </template>

    <script>
    import { Modal } from 'ant-design-vue'
    export default {
    components: {
    Modal
    },
    data() {
    return {
    dynamicComponent: null,
    visible: false
    };
    },
    methods: {
    showDynamicComponent() {
    this.visible = true
    import('@/views/baseInfo/a.vue').then(res=>{
    this.dynamicComponent = res.module
    })
    },
    },
    };
    </script>


    最后通过this.$refs.dynamicRef这个方式来拿到组件的信息和方法。




    二、表格维护


    因为公司的做的系统报表比较多,这时候表头的数量和表单都是比较多的,恰好公司使用的UI框架是ant-design-vue,表头的数量达到40-50的时候,那么代码的占用函数就很大,而且在产品经常在开发阶段,定义的表头位置顺序变来变去,于是为了方便维护和开发,我封装成一个函数,我还没考虑过这个性能损耗问题,但是维护起来确实方便很多。


    业务场景


    举个例子,一个表头有用户姓名年龄,正常情况下,ant-design-vue表头是这么写的。


    const columns = [{
    dataIndex: 'username',
    title: '用户'
    }, {
    dataIndex: 'realname',
    title: '姓名'
    }, {
    dataIndex: 'age',
    title: '年龄'
    }]

    数据少的时候,维护没有什么问题,倒是表头数量很多的时候,可能40-50个,一百个?大概是这个数,看起来就很费劲。因为自己业务确实遇到过这个问题,维护起来要么单独创建一个文件大概一百多行一点点找,要么就放在业务代码里,但是无论如何阅读性都很差。所以我想了个办法,把它平铺变成数组形式。


    import { genTableColumnsUtil } from '@/utils/tableJs'
    const columns = genTableColumnsUtil([
    ['username', '用户'],
    ['realname', '姓名'],
    ['age', '年龄'],
    ])

    这时候是不是就好看多了?甚至这个可以做成二级表头,递归做嵌套。那额外的配置项拓展项怎么搞?


    const columns = genTableColumnsUtil([
    ['username', '用户'],
    ['realname', '姓名'],
    ['age', '年龄'],
    ],
    {username: { width: '20%' }})

    我的做法就是在函数里面在穿多一个对象,这样就可以填充上去了。毕竟大多数字段只是展示而已,没有做太多的单元格定制化,如果要定制化,搜索对应的dataIndex就好了。


    image.png


    image.png


    这时候调整顺序的时候,还有定制化的时候就阅读性就好很多。




    三、依赖包单独抽离


    性能优化不只是代码层面的优化,除了nginx配置http2,gzip...
    单独抽离chunk包也可以达到加快访问速度的目的。


    业务场景


    // 在vue.config.js加入这段代码
    module.exports = {
    configureWebpack: config => {
    // 分包,打包时将node_modules中的代码单独打包成一个chunk
    config.optimization.splitChunks = {
    maxInitialRequests: Infinity, // 一个入口最大的并行请求数,默认为3
    minSize: 0, // 一个入口最小的请求数,默认为0
    chunks: 'all', // async只针对异步chunk生效,all针对所有chunk生效,initial只针对初始chunk生效
    cacheGr0ups: { // 这里开始设置缓存的 chunks
    packVendor: { // key 为entry中定义的 入口名称
    test: /[\\/]node_modules[\\/]/, // 正则规则验证,如果符合就提取 chunk
    name(module) {
    const packageName = module.context.match(
    /[\\/]node_modules[\\/](.*?)([\\/]|$)/
    )[1]
    return `${packageName.replace('@', '')}`
    }
    }
    }
    }
    }
    }
    }

    最后在打包完了之后。可以查看一下。


    image.png




    四、thread-loader打包


    业务场景


    充分利用cpu核心数,进行快速打包、其实我也没感觉有多快。


     // 开启多线程打包
    config.module
    .rule('js')
    .test(/\.js$/)
    .use('thread-loader')
    .loader('thread-loader')
    .options({
    // worker使用cpu核心数减1
    workers: require('os').cpus().length - 1,
    // 设置cacheDirectory
    cacheDirectory: '.cache/thread-loader'
    })
    .end()



    五、ECharts按需使用


    业务场景


    数字化是趋势,图形可视化在所难免,但往往我们有时候没做那么复杂的图形,可能只用到了饼图和柱状图,或者别的,怎么样都用不完ECharts更多的图形,ECharts是大家常用的图形化之一,ECharts第一步教程都是告诉我们,在
    vue文件里


    import * as echarts from 'echarts'

    殊不知,我们用不到的图形都加载进来,打包的时候就可以看到,这玩意,3M多。
    所以,看情况来加载图形配置


    import * as echarts from 'echarts/core'

    import { BarChart, LineChart, PieChart } from 'echarts/charts'

    import {
    TitleComponent,
    TooltipComponent,
    GridComponent,
    LegendComponent,
    ToolboxComponent,
    } from 'echarts/components'

    import { CanvasRenderer } from 'echarts/renderers'

    echarts.use([
    TitleComponent,
    TooltipComponent,
    GridComponent,
    BarChart,
    LineChart,
    PieChart,
    CanvasRenderer,
    LegendComponent,
    ToolboxComponent
    ])

    export default echarts

    通过vscode的包插件,可以看到引入的模块大小


    image.png


    作者:hhope
    来源:juejin.cn/post/7309791510873784372
    收起阅读 »

    学会Grid之后,我觉得再也没有我搞不定的布局了

    web
    说到布局很多人的感觉应该都是恐惧,为此很多人都背过一些很经典的布局方案,例如:圣杯布局、双飞翼布局等非常耳熟的名词; 为了实现这些布局我们有很多种实现方案,例如:table布局、float布局、定位布局等,当然现在比较流行的肯定是flex布局; flex布局属...
    继续阅读 »

    说到布局很多人的感觉应该都是恐惧,为此很多人都背过一些很经典的布局方案,例如:圣杯布局双飞翼布局等非常耳熟的名词;


    为了实现这些布局我们有很多种实现方案,例如:table布局float布局定位布局等,当然现在比较流行的肯定是flex布局


    flex布局属于弹性布局,所谓弹性也可以理解为响应式布局,而同为响应式布局的还有Grid布局


    Grid布局是一种二维布局,可以理解为flex布局的升级版,它的出现让我们在布局方面有了更多的选择,废话不多说,下面开始全程高能;



    本篇不会过多介绍grid的基础内容,更多的是一些布局的实现方案和一些小技巧;



    常见布局


    所谓的常见布局只是我们在日常开发中经常会遇到的布局,例如:圣杯布局双飞翼布局这种名词我个人觉得不用太过于去在意;


    因为这类布局最后的解释都会变成几行几列,内容在哪一行哪一列,而这些就非常直观的对标了grid的特性;


    接下来我们来一起看看一些非常常见的布局,并且用grid来实现;


    1. 顶部 + 内容


    image.png


    html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>Titletitle>
    <style>
    html, body {
    margin: 0;
    padding: 0;
    }

    body {
    display: grid;
    grid-template-rows: 60px 1fr;
    height: 100vh;
    }

    .header {
    background-color: #039BE5;
    }

    .content {
    background-color: #4FC3F7;
    }

    .header,
    .content {
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 24px;
    }
    style>
    head>
    <body>
    <div class="header">Headerdiv>
    <div class="content">Contentdiv>
    body>
    html>


    2. 顶部 + 内容 + 底部


    image.png


    html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>Titletitle>
    <style>
    html, body {
    margin: 0;
    padding: 0;
    }

    body {
    display: grid;
    grid-template-rows: 60px 1fr 60px;
    height: 100vh;
    }

    .header {
    background-color: #039BE5;
    }

    .content {
    background-color: #4FC3F7;
    }

    .footer {
    background-color: #039BE5;
    }

    .header,
    .content,
    .footer {
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 24px;
    }
    style>
    head>
    <body>
    <div class="header">Headerdiv>
    <div class="content">Contentdiv>
    <div class="footer">Footerdiv>
    body>
    html>


    这里示例和上面的示例唯一的区别就是多了一个footer,但是我们可以看到代码并没有多少变化,这就是grid的强大之处;


    可以看码上掘金的效果,这里的内容区域是单独滚动的,从而实现了headerfooter固定,内容区域滚动的效果;


    实现这个效果也非常简单,只需要在content上加上overflow: auto即可;




    3. 左侧 + 内容


    image.png


    html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>Titletitle>
    <style>
    html, body {
    margin: 0;
    padding: 0;
    }

    body {
    display: grid;
    grid-template-columns: 240px 1fr;
    height: 100vh;
    }

    .left {
    background-color: #039BE5;
    }

    .content {
    background-color: #4FC3F7;
    }

    .left,
    .content {
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 24px;
    }


    style>
    head>
    <body>
    <div class="left">Leftdiv>
    <div class="content">Contentdiv>
    body>
    html>


    这个示例效果其实和第一个是类似的,只不过是把grid-template-rows换成了grid-template-columns,这里就不提供码上掘金的示例了;



    4. 顶部 + 左侧 + 内容


    image.png


    html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>Titletitle>
    <style>
    html, body {
    margin: 0;
    padding: 0;
    }

    body {
    display: grid;
    grid-template-rows: 60px 1fr;
    grid-template-columns: 240px 1fr;
    height: 100vh;
    }

    .header {
    grid-column: 1 / 3;
    background-color: #039BE5;
    }

    .left {
    background-color: #4FC3F7;
    }

    .content {
    background-color: #99CCFF;
    }

    .header,
    .left,
    .content {
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 24px;
    }

    style>
    head>
    <body>
    <div class="header">Headerdiv>
    <div class="left">Leftdiv>
    <div class="content">Contentdiv>
    body>
    html>


    这个示例不同点在于header占据了两列,这里我们可以使用grid-column来实现,grid-column的值是start / end,例如:1 / 3表示从第一列到第三列;


    如果确定这一列是占满整行的,那么我们可以使用1 / -1来表示,这样如果后续变成顶部 + 左侧 + 内容 + 右侧的布局,那么header就不需要修改了;



    5. 顶部 + 左侧 + 内容 + 底部


    image.png


    html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>Titletitle>
    <style>
    html, body {
    margin: 0;
    padding: 0;
    }

    body {
    display: grid;
    grid-template-areas:
    "header header"
    "left content"
    "left footer";
    grid-template-rows: 60px 1fr 60px;
    grid-template-columns: 240px 1fr;
    height: 100vh;
    }

    .header {
    grid-area: header;
    background-color: #039BE5;
    }

    .left {
    grid-area: left;
    background-color: #4FC3F7;
    }

    .content {
    grid-area: content;
    background-color: #99CCFF;
    }

    .footer {
    grid-area: footer;
    background-color: #6699CC;
    }

    .header,
    .left,
    .content,
    .footer {
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 24px;
    }
    style>
    head>
    <body>
    <div class="header">Headerdiv>
    <div class="left">Leftdiv>
    <div class="content">Contentdiv>
    <div class="footer">Footerdiv>
    body>
    html>


    这个示例的小技巧是使用了grid-template-areas,使用这个属性可以让我们通过代码来直观的看到布局的样式;


    这里的值是一个字符串,每一行代表一行,每个字符代表一列,例如:"header header"表示第一行的两列都是header,这里的header是我们自己定义的,可以是任意值;


    定义好了之后就可以在对应的元素上使用grid-area来指定对应的区域,这里的值就是我们在grid-template-areas中定义的值;





    码上掘金中的效果可以看到,左侧的菜单和内容区域都是单独滚动的,这里的实现方式和第二个示例是一样的,只需要需要滚动的元素上加上overflow: auto即可;



    响应式布局


    响应式布局指的是页面的布局会随着屏幕的大小而变化,这里的变化可以是内容区域大小可以自动调整,也可以是页面布局随着屏幕大小进行自动调整;


    这里我就用掘金的页面来举例,这里只提供一个思路,所以不会像上面那样提供那么多示例;


    1. 基础布局实现


    移动端布局


    image.png



    以移动端的效果开始,掘金的移动端的布局就是上面的效果,这里我简单的将页面分为了三个部分,分别是headernavigationcontent


    注:这里不是要100%还原掘金的页面,只是为了演示grid布局,具体页面结构和最后实现的效果会有非常大的差异,这里只会实现一些基础的布局;



    html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>Titletitle>
    <style>
    html, body {
    margin: 0;
    padding: 0;
    }

    body {
    display: grid;
    grid-template-areas:
    "header"
    "navigation"
    "content";
    grid-template-rows: 60px 48px 1fr;
    height: 100vh;
    }

    .header {
    grid-area: header;
    background-color: #039BE5;
    }

    .navigation {
    grid-area: navigation;
    background-color: #4FC3F7;
    }

    .content {
    grid-area: content;
    background-color: #99CCFF;
    }


    .header,
    .navigation,
    .content {
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 24px;
    }

    style>
    head>
    <body>
    <div class="header">Headerdiv>
    <div class="navigation">Navigationdiv>
    <div class="content">Contentdiv>
    body>
    html>

    iPad布局


    image.png



    这里是需要借助媒体查询来实现的,在媒体查询中只需要调整一下grid-template-rowsgrid-template-columns的值即可;


    由于这里的效果是上面一个的延伸,为了阅读体验会移除上面相关的css代码,只保留需要修改的代码;



    html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>Titletitle>
    <style>

    .right {
    display: none;
    background-color: #6699CC;
    }

    @media (min-width: 1000px) {
    body {
    grid-template-areas:
    "header header"
    "navigation navigation"
    "content right";
    grid-template-columns: 1fr 260px;
    }

    .right {
    grid-area: right;

    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 24px;
    }
    }
    style>
    head>
    <body>
    <div class="header">Headerdiv>
    <div class="navigation">Navigationdiv>
    <div class="content">Contentdiv>
    <div class="right">Rightdiv>
    body>
    html>

    PC端布局


    image.png



    和上面处理方式相同,由于Navigation移动到了左侧,所以还要额外的修改一下grid-template-areas的值;


    这里就可以体现grid的强大之处了,我们可以简单的修改grid-template-areas就可以实现一个完全不同的布局,而且代码量非常少;


    为了居中显示内容,我们需要在左右两侧加上一些空白区域,可以简单的使用.来实现,这里的.表示一个空白区域;


    由于内容的宽度基本上是固定的,所以留白区域简单的使用1fr进行占位即可,这样就可以平均的分配剩余的空间;



    html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>Titletitle>
    <style>
    @media (min-width: 1220px) {
    body {
    grid-template-areas:
    "header header header header header"
    ". navigation content right .";
    grid-template-columns: 1fr 180px minmax(0, 720px) 260px 1fr;
    grid-template-rows: 60px 1fr;
    }
    }
    style>
    head>
    <body>
    <div class="header">Headerdiv>
    <div class="navigation">Navigationdiv>
    <div class="content">Contentdiv>
    <div class="right">Rightdiv>
    body>
    html>

    完善一些细节


    QQ录屏20231210000552.gif



    最终的布局大概就是上图这样,这里主要处理的各个版块的间距和响应式内容区域的大小,这里的处理方式主要是使用column-gap和一个空的区域进行占位来实现的;


    这里的column-gap表示列与列之间的间距,值可以是pxemrem等基本的长度属性值,也可以使用计算函数,但是不能使用弹性值fr


    空区域进行占位留间距其实我并不推荐,这里只是演示grid布局可以实现的一些功能,具体的实现方式还是要根据实际情况来定,这里我更推荐使用margin来实现;


    完整代码如下:



    html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>Titletitle>
    <style>
    html, body {
    margin: 0;
    padding: 0;
    }

    body {
    display: grid;
    grid-template-areas:
    "header header header"
    "navigation navigation navigation"
    ". . ."
    ". content .";
    grid-template-columns: 1fr minmax(0, 720px) 1fr;
    grid-template-rows: 60px 48px 10px 1fr;
    column-gap: 10px;
    height: 100vh;
    }

    .header {
    grid-area: header;
    background-color: #039BE5;
    }

    .navigation {
    grid-area: navigation;
    background-color: #4FC3F7;
    }

    .content {
    grid-area: content;
    background-color: #99CCFF;
    }

    .right {
    display: none;
    background-color: #6699CC;
    }

    @media (min-width: 1000px) {
    body {
    grid-template-areas:
    "header header header header"
    "navigation navigation navigation navigation"
    ". . . ."
    ". content right .";
    grid-template-columns: 1fr minmax(0, 720px) 260px 1fr;
    }

    .right {
    grid-area: right;

    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 24px;
    }
    }

    @media (min-width: 1220px) {
    body {
    grid-template-areas:
    "header header header header header"
    ". . . . ."
    ". navigation content right .";
    grid-template-columns: 1fr 180px minmax(0, 720px) 260px 1fr;
    grid-template-rows: 60px 10px 1fr;
    }
    }

    .header,
    .navigation,
    .content {
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 24px;
    }

    style>
    head>
    <body>
    <div class="header">Headerdiv>
    <div class="navigation">Navigationdiv>
    <div class="content">Contentdiv>
    <div class="right">Rightdiv>
    body>
    html>

    简单复刻版




    码上掘金上的效果来说已经完成了大部分的布局和一些效果,目前来说就是还差一些交互,还有一些细节上的处理,感兴趣的同学可以自行完善;



    异型布局


    异性布局指的是页面中的元素不是按照常规的流式布局进行排版,又或者说不规则的布局,这里我简单的列出几个布局,来看看grid是如何实现的;


    1. 照片墙


    image.png


    html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>Titletitle>
    <style>
    html, body {
    margin: 0;
    padding: 0;
    background: #f2f3f5;
    overflow: auto;
    }

    body {
    display: grid;
    grid-template-columns: repeat(12, 100px);
    grid-auto-rows: 100px;
    place-content: center;
    gap: 6px;
    height: 100vh;
    }

    .photo-item {
    width: 200px;
    height: 200px;
    clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%);
    }

    style>
    head>
    <body>

    body>
    <script>
    function randomColor() {
    return '#' + Math.random().toString(16).substr(-6);
    }

    let row = 1;
    let col = 1;
    for (let i = 0; i < 28; i++) {
    const div = document.createElement('div');
    div.className = 'photo-item';
    div.style.backgroundColor = randomColor();
    div.style.gridRow = `${row} / ${row + 2}`;
    div.style.gridColumn = `${col} / ${col + 2}`;

    document.body.appendChild(div);
    col += 2;
    if (col > 11) {
    row += 1;
    col = row % 2 === 0 ? 2 : 1;
    }
    }
    script>
    html>


    这是一个非常简单的照片墙效果,如果不使用grid的话,我们大概率是会使用定位去实现这个效果,但是换成grid的话就非常简单了;


    而且代码量是非常少的,这里就不提供码上掘金的 demo 了,感兴趣的同学可以将代码复制下来自行查看效果;



    2. 漫画效果


    image.png




    在漫画中有很多类似这种不规则的漫画框,如果使用定位的话,那么代码量会非常大,而且还需要计算一些位置,使用grid的话就非常简单了;


    可以看到这里还有一个气泡文字显示的效果,按照页面书写顺序,气泡是不会显示的,这里我们可以使用z-index来实现,这里的z-index值越大,元素就越靠前;


    而且气泡文字效果也是通过grid来进行排版的,并没有使用其他的布局来实现,代码量也并不多,感兴趣的同学可以自行查看;



    3. 画报效果


    image.png




    在一个画报中,我们经常会看到文字和图片混合排版的效果,由于这里直接使用的是渐变的背景,而且我的文字都是随意进行排列的,没有什么规律,所以看起来会比较混乱;


    在画报效果中看似文字排版非常混乱不规则,但是实际上设计师在设计的时候也是会划分区域的,当然用定位也是没问题的,但是使用grid的话就会简单很多;


    我这里将页面划分为12 * 12区域的网格,然后依次对不同的元素进行单独排列和样式的设置;



    流式布局


    流式布局指的是页面的内容会随着屏幕的大小而变化,流式布局也可以理解为响应式布局;


    但是不同于响应式布局的是,流式布局的布局不会像响应式布局那样发生变化,只是内容会随着轴进行流动;


    通常这种指的是grid-template-columns: repeat(auto-fit, minmax(0, 1fr))这种;


    直接看效果:


    QQ录屏20231210222012.gif




    这里有两个关键字,一个是auto-fit,还有一个是auto-fill,在行为上它们是相同的,不同的是它们在网格创建的不同,



    image.png



    就像上面图中看到的一样,使用auto-fit会将空的网格进行折叠,可以看到他们的结束colum的数字都是6;


    像我们上面的实例中不会出现这个问题,因为我们使用了响应式单位fr,只有使用固定单位才会出现这个现象;


    感兴趣的同学可以将minmax(200px, 1fr)换成200px尝试;



    对比 Flex 布局


    在我上面介绍了这么多的布局场景和案例,其实可以很明显的发现一件事,那就是我使用grid进行的布局基本上都是大框架;


    当然上面也有一些布局使用flex也是可以实现的,但是我们再换个思路,除了flex可以做到上面的一些布局,float布局、table布局、定位布局其实也都能实现;


    不同的是float布局、table布局、定位布局基本上都是一些hack的方案,就拿table布局来说,table本身就是一个html标签,作用就是用来绘制表格,被拿来当做布局的一种方案也是迫不得已;


    web布局发展到现在的我们有了正儿八经可以布局的方案flex,为什么又要出一个grid呢?


    grid的出现绝对不是用来替代flex的,在我上面的实现的一些布局案例中,也可以看到我还会使用flex


    我个人理解的是使用grid进行主体的大框架的搭建,flex作为一些小组件的布局控制,两者搭配使用;


    flex能实现一些grid不好实现的布局,同样grid也可以实现flex实现困难的布局;


    本身它们的定位就不痛,flex作为一维布局的首选,grid定位就是比flex高一个维度,它的定位是二维布局,所以他们之间没有必要进行对比,合理使用就好;


    总结


    上面介绍的这么多基于grid布局实现的布局方案,足以看出grid布局的强大;


    grid布局的体系非常庞大,本文只是梳理出一些常见的布局场景,通过grid布局去实现这些布局,来体会grid带来的便利;


    可能需要完全理解我上面的全部示例需要对grid有一定的了解才可以,但是都看到这里了,不妨去深挖一下;


    grid布局作为一项强大的布局技术,有望在未来继续发展,除了我上面说到的布局,grid还有很多小技巧来实现非常多的布局场景;


    碍于我的见识和文笔的限制,我这次介绍grid肯定是有很多不足的,但是还是希望这篇文章能为你对于布局相关能有新的认识;


    作者:田八
    来源:juejin.cn/post/7310423470546354239
    收起阅读 »

    相比拼多多市值一路狂奔,阿里巴巴究竟输在哪里?

    相信最近在互联网界最热门的事情就是拼多多的市值超过了阿里。 这个事情为什么有这么大的轰动?想当年阿里可是电商的一个阿里巴巴在2017年的时候市值超过了亚马逊。成为了中国乃至世界互联网电商界不可小觑的力量。 然而短短在八年的时间里面万亿市值就被一个在2015年...
    继续阅读 »

    相信最近在互联网界最热门的事情就是拼多多的市值超过了阿里。



    这个事情为什么有这么大的轰动?想当年阿里可是电商的一个阿里巴巴在2017年的时候市值超过了亚马逊。成为了中国乃至世界互联网电商界不可小觑的力量。


    然而短短在八年的时间里面万亿市值就被一个在2015年9月份创建的拼多多给反超了,而且据公开数据,阿里现在员工有20万多同学,拼多多只有1万多。


    与此同时,阿里巴巴的国内外电商份额在急速的下降,而拼多多不仅在国内增速一骑绝尘,在全球范围已经开始输出拼多多的低价力量,据统计拼多多已经占据国内26%的市场份额,旗下的temu在欧美澳加如入无人之境,充分让这些外国人了解什么是“兄弟就砍我一刀”的消费降级的乐趣。


    与此同时,令人担忧的是,阿里巴巴目前没有任何能够快速绝地逢生的迹象。


    就如当年百度出了魏则西事件后,我们振聋发聩发馈的一问,谷歌退出中国后的百度到底怎么了?到底发生了什么,一个原来在中国互联网市值排名第一的公司,到底为什么在短短的几年间就到了道德沦丧不争气的地步?


    虽然百度和阿里面对问题性质截然不同,而今天我们相似的也可以问一句,为什么阿里巴巴到了目前这个境遇?


    网上已经有很多文章来谈论为什么阿里巴巴会被拼多多反超。理由有很多,比如说在战略上的决策失误,阿里坚持了新零售升级的消费主义,比如说拼多多非常聚焦收敛,而阿里巴巴投资收购了不少业务,业务分散,比如说拼多多比较低调,而阿里巴巴出了很多公关事件。比如说阿里巴巴的内部味道过重,而拼多多就是拿员工时间换钱不谈价值观。


    这些固然都是阿里巴巴为什么现在业绩下滑被拼多多反超的原因。但今天我想换一个思路来用拟人的方式,从情绪上所以说根本原因。


    我觉得根本原因就是阿里巴巴太过于傲慢。也就是傲慢这个本质上的原因,才导致了一系列战略决策的失误,用人的失误,情报的失误。


    为什么这么说?在原来一个庞然大物下居然还能存在一个拼多多能够釜底抽薪,难道阿里巴没有任何人能够觉察到拼多多从零开始的这种号召力和变革力吗?难道阿里巴巴没有牛逼的人物能够反制拼多多吗?难道阿里巴巴没有人才了吗?


    显然这些都是否定的,成立于1999年,历时已经24年,阿里巴巴能够从零做到全球目前的这个地步,意味着它就有一个强大的管理团队,强大的人才以及强大的组织力,那为什么依然没有阻止拼多多的起来呢?


    这个企业竞争形势变化在《创新者的窘境》里面说的非常的明确,这个就是所谓的小公司对大公司的颠覆式创新。也就是一个小公司,往往能够从新的维度,新的方向,形成快速的行动力,终究在不起眼的地方,再造一个大市场。往往小公司利用到了更新的一些理念和价值,使得小公司能够在短时间内在大公司的眼皮底下快速的形成大规模的创新力量,从小起步,犹如积蓄力量的蚂蚁,最终掀翻步履维艰的大象。


    而大公司往往在成功之后就会有自己的一个路径依赖。在路径依赖的情况下的话,就往往会主观上忽视掉最弱小的竞争对手,甚至完全不把竞争对手当回事儿。


    换一句话来说就是公司太大,大的极度傲慢,历史上已经有数见不鲜的例子,比如刚刚倒下的全球手机霸主诺基亚。


    在拼多多刚刚起来的时候,内部同学已经有很多人都感知到拼多多的竞争。但是阿里巴巴犹如一条非常大的一个航船,在让商家没有难做的生意愿景上,新零售升级,在双十一GMV增长方向上无法停止。于是慢慢导致高管乃至最底层的执行的人都有意无意忽略了拼多多的增长。


    我之前和一个天猫的研究生同学吐槽他的双十一优惠券计算复杂度之高。我这位同学骄傲的告诉我们,只是我不是天猫的目标客户,他说,其实你不知道有多少客户非常喜欢我们的搭楼游戏,喜欢我们的复杂的优惠计算,说完一脸傲娇。



    于是就在这种情况下,拼多多一路狂奔,简单粗暴的后续界面,简单粗暴的退款逻辑,一刀刀砍向了原来忠诚的淘宝用户。从开始抢走了淘宝的低端羊毛用户,到抢走了淘宝的中间用户,直到现在的强力补贴,连高端消费用户都抢走了。


    拼多多说秉承的客户第一理念,让所有消费者如沐春风,在被淘宝商家歧视的价值主张里,好像找到了另外一个发泄口。


    这个就是颠覆式创新的力量。也是无数大公司单纯的血的教训。只不过诸如淘宝这样的大公司还是依然没有躲过这样的故事。


    当然我依然相信阿里巴巴是一个有韧性,有希望的公司。毕竟阿里巴巴原来就从最艰难的路子里面杀出一条血路。此次确实是淘宝面对的最大危机,但我相信也是新希望的开始。


    正如微软CEO纳德拉所说,人们往往高估了短期的影响力而低估了长期的影响力。


    胜者坚持长期主义,鹿死谁手,犹未知之。


    作者:ali老蒋
    来源:juejin.cn/post/7308643782376570934
    收起阅读 »

    今天还要用 React 吗:利弊权衡

    web
    免责声明 本文属于是语冰的直男翻译了属于是,略有删改,仅供粉丝参考,英文原味版请临幸 The Pros and Cons of Using React Today。 在过去的十年中,React 因辅助整个行业的开发者构建顶尖 UI 的超能力,为大家所熟知。...
    继续阅读 »




    免责声明


    本文属于是语冰的直男翻译了属于是,略有删改,仅供粉丝参考,英文原味版请临幸 The Pros and Cons of Using React Today



    00-wall.jpg


    在过去的十年中,React 因辅助整个行业的开发者构建顶尖 UI 的超能力,为大家所熟知。


    本文在 2023 年底和 2024 年对 React 进行了深入而平衡的展望。我们将看看它值得称道的优势、明显的短板,以及对当今开发者的可靠性。


    让我们先从 React 与众不同的创新功能开始,然后再将注意力转向它给开发者带来的挑战。


    React JS 是什么鬼物?


    ReactJS 是一个组件筑基的 JS 库,最初由 Facebook 创建,并在十年前发布。该库简化了开发者创建交互式 UI,同时有效管理组件状态。它能够为复杂 App 编写多个组件,而不会丢失它们在浏览器的 DOM(文档对象模型)中的状态,这对一大坨开发者而言是一个明显的福利。


    虽然 React 主要是一个用于 Web App 的工具,但其多功能性通过 React Native 扩展到移动 App 开发。这个强大的开源库允许开发 Android、iOS 和 Windows App,展示了 React 跨平台开发的灵活性。


    React 生态系统


    React 最大的资源之一是其庞大的生态系统,其中充满了第三方库和工具,极大地扩展了其功能。这对于路线规划 App 等复杂项目尤其有利,这些项目通常依赖集成大量外部服务,比如地图 API 和路径算法。


    React 的灵活性和与各种第三方服务的兼容性简化了集成过程,允许开发者使用高级功能增强其 App,而不会产生过多的开销。


    其核心是基本的库和工具,比如 React Router,用于 SPA(单页应用程序)中的动态路由,确保无缝的 UX(用户体验)过渡。Redux 是一个关键的状态管理工具,它为状态创建了一个中心化 store,使不同的组件能够一致地访问和更新它,这在大型 App 中尤为重要。


    React.js:不仅仅是复杂性


    虽然 React 在 UI 创建方面表现出色,但在状态管理和 SEO 优化等领域存在不足。幸运的是,更广泛的 JS 生态系统提供了许多工具,这些工具好处多多,比如更简化的状态管理方案、通过 SSR(服务器端渲染)增强的 SEO 和数据库管理。让我们瞄一下 React 若干更突出的集成选项。


    对于那些寻求更简单替代方案的人而言,MobX 提供了一种直观的状态管理方案,并且样板更少。此外,Next.js 通过提供 SSR 和 SSG(静态站点生成)解决了客户端渲染 App 的当前 SEO 限制。在开发和测试方面,CRA(Create React App)简化了设置新前端构建管道的过程,使开发者能够立即开始运行,而不会受到配置的困扰。


    同时,Storybook 作为一个 UI 开发环境,开发者可以在其中独立可视化其 UI 组件的不同状态。Jest 在单元和快照测试中很受欢迎,它与 React 无缝集成。由 Airbnb 开发的 Enzyme 是一个测试工具,它简化了断言、操作和遍历 React 组件输出的过程。


    额外的库和工具进一步丰富了 React 生态系统;Material-UI 和 Ant Design 提供了全面的 UI 框架,可以满足美学和功能要求,而 Axios 则提供了一个 Promise 筑基的 HTTP 客户端来发送 HTTP 请求。React Query 简化了获取、缓存和更新异步数据的过程,React Helmet 有助于管理对文档头的更改,这对于 SPA 中的 SEO 至关重要。


    React 与其他技术的集成 —— 比如后端框架,包括 Node.js 和 Django;状态管理库,比如 Apollo for GraphQL,增强了其灵活性。如今,开发者甚至可以将 PDF 查看器嵌入到网站中,并大大优化 UX。


    然而,React 的不断发展要求开发者跟上最新的变化和进步,React 为试图制作高质量、可扩展和可维护的 Web App 的开发者提供的无数解决方案抵消了这一挑战。


    React 之利


    React 已经将自己确立为构建动态和响应式 Web App 的关键库,原因如下:


    组件筑基架构


    传统的 JS App 在扩展时经常会遇到状态管理问题。虽然但是,React 提供了复杂的、独立维护的可复用组件,允许开发者在不影响其他页面的情况下更新网页的局部 —— 确保松耦合和协作功能。


    当然,这个概念并不是 React 独有的;举个栗子,Angular 也使用组件作为基本构建块。尽管如此,React 庞大的社区、Meta 的支持和相对丝滑的学习曲线使其成为开发者的最爱。


    开发中的增强定制


    React 的多功能性在构建针对特定业务需求量身定制的 App 时大放异彩。尤其是其组件筑基架构允许在 App 中无缝组装复杂结构。


    举个栗子,在构建集成仪表板时,React 的生态系统有助于将各种模块(比如图表、小部件和实时数据源)集成到一个有凝聚力的 UI 中,使开发者能够打造不仅功能强大,而且直观且具有视觉吸引力的 UX。


    这种强大的适应性恰恰凸显了为什么 React 仍然是旨在创建多功能和健壮的 Web App 的开发者的首选。


    面向未来的开发者选项


    React 面向未来的特性是它为开发者提供的最引人注目的优势之一。React 灵活的架构迎合了当前的 Web 开发需求,同时也无缝地适应了将塑造行业近期的新兴技术。


    值得注意的是,机器学习正在向 Web 开发领域取得重大进展,2022 年全球 ML 市场价值已经达到 210 亿美元,这凸显了 React 面向未来的特性以及与此类进步相协调的能力的重要性。


    其中一个比较突出的例子是 TensorFlow.js,一个用于图像和模式识别的 ML 库。同样,React 允许集成 ML 驱动的聊天机器人甚至推荐功能。此外,WebAssembly 可以帮助允许用 Rust、Python 或 C++ 编码的 ML 应用程序存在于原生 App 中。


    用于状态管理的 Redux


    在 SPA 中,多个组件驻留在单个页面上,管理状态和组件间通信很快就会变得具有挑战性 —— 这正是 Redux for React 的亮点。


    作为 React 不可或缺的一部分,它充当“管理器”,确保组件之间的数据流一致且准确,集中状态管理并促进组件自治性,显着提高数据稳定性和 UX。


    React 之弊


    虽然 React 为不同技能水平的开发者提供了许多优势,但它并非没有各自的缺点,包括以下内容:



    • 复杂的概念和高级模式:React 引入了若干高级概念和模式,这些概念和模式一开始可能会让初学者不知所措。要了解 JSX、组件、props、状态管理、生命周期方法和钩子,需要扎实掌握 JS 基础知识。

    • 与其他技术的集成复杂性:React 经常与其他工具和技术结合使用——如 Redux、React Router 和各种中间件 —— 对于新手来说,了解如何将这些技术与 React 集成可能极具挑战。

    • 非 JS 开发者的障碍:React 对 JS 的严重依赖对于不精通 JS 的开发者而言可能是一个障碍。虽然 JS 是一种通用且广泛使用的语言,但来自不同编程背景的开发者可能会发现适应 JS 的范式和 React 的使用方式极具挑战。

    • 不是一个成熟的框架:React 主要处理 MVC 的“视图”部分,也称为模型视图控制器架构。对于“模型”和“控制器”,需要额外的库,与 Angular 等功能齐全的框架相比,这最终会导致结构化程度较低且可能更加混乱的代码。

    • 代码膨胀:React.js 的特点是其大量的库和依赖需求,因其臃肿的 App 而臭名昭著。这种膨胀通常表现为较长的加载时间,尤其是在复杂的项目中。该框架的结构严重依赖其虚拟 DOM,即使是次要功能也需要加载整个库,这大大增加了 App 的数字足迹并降低了其效率。

    • 在传统设备和弱网络上的性能下降:React.js App 的性能在较旧的硬件和互联网连接较差的地区往往会下降。这主要是由于框架的客户端渲染模型和密集的 JS 处理。这些因素可能会导致渲染交互式元素的延迟,这在计算能力有限的设备或带宽有限的环境中尤为明显,这会对 UX 产生不利影响。


    最终裁决


    随着 Web 开发领域的不断发展,React 的灵活性和强大的生态系统使其处于有利地位。它将继续使开发者能够将尖端功能无缝地整合到其 App 中。虽然但是,虽然 React 为开发者提供了很多好处,但它仍然有其缺点。


    React 的复杂性和对高级 JS 概念的依赖带来了曲折的学习曲线,尤其是对于新手或尚未精通 JS 的人。它还主要解决了 MVC 架构的“视图”方面,需要额外的工具来进行完整的 App 开发,这可能会导致更复杂和结构化更少的代码库。


    尽管存在这些挑战,但庞大而活跃的 React 社区在其持续发展中发挥着至关重要的作用。在可预见的未来,它将继续成为 Web 和移动 App 开发的关键库。


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

    一个30岁老前端的人生经历(学习+工作+婚姻+孩子),给迷茫的朋友一点激励。

    web
    前言 我93年的,还差几天就到30周岁生日。做前端开发大概也有6、7年,算是老前端了。 2023 写作 在掘金看文章已经很多年了,学到了很多东西。今年三月份在掘金写下了第一篇文章,开始的想法是把自己的开发笔记整理一下写成文章,来巩固一下自己的知识,没想到在写文...
    继续阅读 »

    前言


    我93年的,还差几天就到30周岁生日。做前端开发大概也有6、7年,算是老前端了。


    2023


    写作


    在掘金看文章已经很多年了,学到了很多东西。今年三月份在掘金写下了第一篇文章,开始的想法是把自己的开发笔记整理一下写成文章,来巩固一下自己的知识,没想到在写文章分享的过程中帮助了很多人,也得到了很多人认可,让我写文章分享的动力也越来越强,基本每周都会写一篇,闲在没事的时候基本都是在构思下一篇文章写什么。希望明年能写更多的文章,帮助更多的人。


    写作给我带来了什么



    1. 巩固知识。把自己学到的东西,分享出去,印象更深刻了。

    2. 更多的机会。写作过程中,有大厂私信我,给我一些面试机会,这个时候的面试机会还是很宝贵的,不过因为一些原因都拒绝了。

    3. 快乐。很多人私信我说很感谢我分享的东西,让他们学到了很多东西。看到这些感谢的话,自己得到认可,还是很开心的。

    4. 钱。写文章参与掘金的金石计划活动,陆陆续续差不多获得了接近1000的收入,每次拿到钱,带着老婆孩子去吃顿好的,还是不错的。


    降薪


    今年公司受大环境影响,裁了一部分人,留下来的人也都降薪了。开始有点接受不了,想跑,但是因为是在上海嘉定郊区,附近找不到好的工作,最近的也要1个小时的地铁,并且小孩刚上一年级,上海基本不可能跨区转校,所以打消了换工作的念头,只能在公司了干下去,相信公司会好起来的。


    家庭


    看了上面肯定有倔友怀疑,30岁小孩怎么能上一年级?不错我大四结的婚,还是奉子成婚,所以早早的有了孩子。


    因为自己小时候是留守儿童,不想让自己孩子过留守儿童的生活,所以孩子一直是和我们在一起,记得我刚出来工作的时候,一个月才3500,我老婆全职带孩子,这些薪资刚够花销,生活过的比较拮据,有时候还要靠我父母接济。


    现在收入稍微好了一些,但是我们还没有买房子,存款也不多,在别人看来压力可能会有点大,但是我心比较大,平时消费欲望也比较低,对钱不是那么渴望,一家人在一起也是开开心心的,不过还是想给老婆孩子一个自己的家,努力奋斗吧。


    孩子今年上一年级了,再上一年级后,作业明显变多了,每天都要写到很晚,看着孩子很累,也没办法,不写好作业,第二天老师就会在群里点名说。


    孩子比较调皮,经常在学校里和同学打架,最多的时候,一周被班主任叫了三次家长,有时候是他的错,有时候是别人的错。因为老婆全职在家带孩子,这些都是我老婆处理的,她最近有点焦虑,每天都担惊受怕的,害怕孩子在学校又闯祸,整的我也有点焦虑,工作状态有点差。


    关于孩子打架的事,我和老婆猜测可能是学习压力太大了,每天放学回来就开始写作业,一直写到睡觉,平时还有一些兴趣班要上,玩的时间太少了,积累了很多怨气没地方发泄,所以比较暴躁。现在每天放学后先让他玩半个小时再写作业,并且和他多次沟通,告诉他暴力解决问题是不对的,目前稍微好了一些。有这方面经验的兄弟,可以在评论区指点一下。


    健康


    今年五月份的时候,身体有点不舒服,平时熬夜比较多,人也比较胖,就想着去体检一下,体检结果肝功能有一项转氨酶比正常高三倍,然后到医院做了一次全身体检,抽了9管血,结果还好没啥大问题,可能是脂肪肝导致的转氨酶很高,医生建议要减肥。


    因为平时比较忙,没有时间健身,就搞了个自行车上下班骑,公司离家大概5公里左右,上下班每天骑10公里左右,从6月份买车到现在基本没断过,虽然体重没有降下来,但是精神状态和体力好了不少,以前稍微有点运动量,就气喘吁吁全身冒汗,现在好多了。


    希望倔友们多注意健康,少熬夜,身体才是最重要的。


    2024展望


    关于2024,立几个flag吧



    1. 最少分享40篇文章

    2. 完善fluxy-admin平台,把前后端低代码平台集成进来,做出一个企业级低代码平台开源出去。

    3. 看react源码,并做个专栏分享。每次面试,被面试官问react底层一些东西的时候,回答的都不是很好,就是因为没有彻底了解底层,所以回答的都很片面,明年一定要把react吃透。今年年初买了卡颂大佬的react设计原理书籍,现在在床头吃灰呢。

    4. 减肥


    个人真实经历分享


    给大家分享一下我的个人真实经历,与君共勉。


    我出生在一个很普通的农村家庭,有点小聪明,但是贪玩,高中三年基本都是看电子书度过,天天上课把手机放在书下面,装作看书,实际上都是在看小说,现在回想起来,想不通老师为啥重来没有发现过。


    开始决定好好学习是高三上学期,有次上课和同桌说话,被老师说你自己不学,不要影响别人学习,还说了一些很难听的话(当时我在班里大概倒数10几名的样子,同桌10几名左右。),虽然我贪玩,但是我自尊心比较强,我就不服气,然后上课开始好好听课,后面一次月考竟然考到了10几名,和同桌成绩差不多,然后就开始飘了,上课又开始看小说,下次月考又考的很差,然后难受,又开始好好听课,就这样成绩一会好一会坏,不过拿了几次进步奖,同学笑话我是不是为了拿进去奖,故意退步的。


    真正让我决定好好学习的是高三下学期开学的前一天晚上,我家庭条件不是特别好,而我当时因为中考考的很差,只能上一个学费比较贵的私立高中,高三下学期开学前一天晚上我爸还在为我筹学费(家里没有穷到付不起学费的地步,只是当前家里钱被其他地方占用了,拿不出来。),最终从亲戚那里借了点钱,然后我爸把钱交到我手里,让我明天交学费,看着我爸粗糙的手(我爸是干工地的),这一刻我决定好好学习,不然都对不起这学费。高三下学期上课就没看过手机了,由于底子太差,高考离二本线差了几分,最终上了个三本。


    高考结束,暑假期间迷上了英雄联盟。大学的时候,室友也玩,经常和室友一块包夜,第二天要么旷课在宿舍睡觉,要么在教室最后一排睡觉,导致第一学期就挂了三科,不过后面补考都过了。后面还是继续玩,大二下学期突然觉得不能这样浑浑噩噩了,还不如出去打工,给家里省点学费还能挣点钱(不知道当时为啥有这想法),然后就和父母说了一下,不上学了出去打工,当时是想退学的,还好我好朋友和我说先休学吧,以后后悔还有机会。


    在苏州找了一个工厂,干了一个星期干不下去了,身体上的劳累倒是其次,主要是看不到生活的希望,每天就像一个机器一样,后面就回去上学了,然后学习非常努力,后面还得了奖学金,毕业论文也被评上了优秀论文,也是优秀毕业生,但是毕业学校没有给学位证,只给了毕-业-证,因为挂科超过5门(补考过了也没用,只要挂科超过5门,就完了,我们那一届有不少没有学位证的。),这个政策最开始都不知道,没有学位证后问学校,学校才说的,也不能怨学校,算是自食其果吧。没有学位证对找工作还是有很大影响的,后面有几次面试通过大厂了,因为没有学位证而被拒。


    实习的时候,实习单位和学校是有合作的,学校知道我的事迹也知道我在实习单位表现的不错,所以就邀请我回去给学弟学妹们分享我的经历。当时分享完后,有几个学弟加我微信说,他们现在也是这个状态,我的经历让他们有了重新开始的信心。


    后面的工作之旅也是一路坎坷,不过最后的结果是好的,目前在公司里做前端负责人,收入还不错。工作之旅明年年终总结再和大家分享吧。


    和大家分享我的经历,就是想告诉大家永远不要放弃,只要坚持,就会有希望,同时也想告诉大家每个人都要为自己做过的事负责,因为贪玩我没考上好一点的大学,因为贪玩我没有学位证,但是我后面迷途知返,通过自己的努力,还是得到了一份不错的工作,一个美满的家庭。


    最后


    很喜欢deft在夺冠时说的一句话:我唯一会的仅剩英雄联盟,如果在这条路上我不能成功,那我的人生将没有任何意义。


    而我唯一会的就是写代码,我不一定能成功,但是我想努力做到更好。


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

    惊!27岁程序媛的一年竟然干了这些事

    hello铁铁们,这是继年中总结之后的又一篇年底回顾。 首先,真的很高兴我的文章能被很多很多的小伙伴看到,每一条评论我都有认真看,有一部分评论也让我对未来产生了新的感悟,也从未想到,会有这么多朋友同我一样有着类似的困惑,也很开心在文章发布之后收获了志同道合的伙...
    继续阅读 »

    hello铁铁们,这是继年中总结之后的又一篇年底回顾。

    首先,真的很高兴我的文章能被很多很多的小伙伴看到,每一条评论我都有认真看,有一部分评论也让我对未来产生了新的感悟,也从未想到,会有这么多朋友同我一样有着类似的困惑,也很开心在文章发布之后收获了志同道合的伙伴。

    这篇文章,我将为我上一篇文章中还未产生结果的问题画上一个句号,并且浅浅思考一下我即将到来的28岁的人生。


    f0c09162ef99abf035453ada2e742d5e.jpeg

    我好像一只温水中的青蛙


    我依然在北京这座城市漂泊,我没有勇气或者说没有足够的能力与底气回到老家扎根,我依然过着普普通通的周中上班周末摆烂的人生,两点一线的在舒适圈中挣扎,不愿逃脱。

    再加上现在经济依然低迷,对于无房无车的普通的我来说,我依然不敢潇洒的离开这个岗位,经常和朋友说“等什么时候裁了我给我n+1”,但是如果那一天真的到来,我依然不知道,我的未来在哪

    (虽然但是,不思考人生时候的自己还是蛮开朗的)


    给2023画上句号


    我很喜欢27岁的我。


    因为这一年没什么坏消息,平淡的生活很快乐,偶尔周六和朋友聚会,周日在家休息,偶尔和朋友去临近的城市旅游,和闺蜜吐槽公司发生的种种奇葩事件,和同事的关系还算融洽,家人也依然健康。
    唯一的遗憾就是持续性的鼻炎让我查出了猫子过敏,不得不把我养了三年的猫子送回了老家,好在猫子回家之后心情还算不错,家人也很喜欢它。

    附上一张在东北看雪的傻猫


    微信图片_20231208154352.png

    然后我不甘寂寞又养了鼠子,哈哈


    微信图片_20231208160621.png

    学习方面


    1.软考通过!


    这是今年中旬定好了目标的任务,所以一直都有很认真的在复习,虽然今年笔试改成了机试,通过率降低了很多,并且很认真复习的内容好多都没有考到,但努力依然得到了好的结果,紧张的成绩查询之后带来了好的消息,真的很令人开心!
    我依然相信,努力就会有结果,哪怕最后可能不尽人意,但一定会有所收获。
    由于明年几个计划的优先级较高所以还没有考高级的打算,另外考过的小伙伴透露一下是不是真的很难啊(听说高级上了好几个level)


    QQ图片20231208155133.jpg

    2.开始参与开源项目


    无聊的生活每天枯燥的工作,通过上一篇文章有一个大哥联系了我,邀请我参与他的开源项目,因为他的项目体系已经很完整,所以我毫不犹豫的参与到其中去,接触到了一些新的思想,对于很久没有长进的我来说,做做新东西的免费劳动力也是很开心的事(虽然也没贡献几行代码哈哈)。


    3.接触web3、区块链


    开源的大哥还带我接触了web3,区块链等领域,也有机会一起合作区块链开发的兼职项目(虽然目前还是启动阶段没我啥事)。


    4.开始学习java


    程序员这个行业太卷了,与其不如别人卷我不如我先发制人,于是我决定努力向全栈发展,哪怕不能全栈也总归比只会前端要好,所以在软考结束之后,我就开始了java学习计划,目前还处于基础语法学习阶段,本来这个任务是放在明年开始的,因为有了一定的空闲时间所以比计划有所提前。


    5.依然坚持每周1~2道LeetCode


    leetcode依然在刷,但是已经没什么精力去突破困难的题了,而且由于长时间不动脑子,导致好多中等难度的题也不会做了,所以这方面还是要维持住,不然退步的实在太快。


    最后附上每天学习进度表,做计划真的很有用,不然我每天真的什么都不想做,每当计划完成打上√的时候还是会有成就感。


    微信图片_20231208160823.png
    微信图片_20231208160917.png
    微信图片_20231208161127.png

    好多人问我这个日历工具是什么,其实就是excel找个模板,然后自己写内容,不够智能但是这种自己逐渐调整出来的时常能带来更多思考。


    6.天气转凉,骑行搁置


    由于天冷,我的车锁都被冻得梆硬,虽然是东北人但是我怕冷,后来就没有再骑行,等春暖花开时我一定继续践行我的骑行计划!


    2024目标


    28岁大龄女中年坚持在程序员团队中浑水摸鱼,LeetCode继续刷,计划表继续做,坚持学习,励志坚决不做家庭主妇!


    学习java,向全栈发展


    坚持每天学习英语


    持续参与开源,接触兼职工作


    先附上12月定的计划表,如果有新的的临时计划依然会做补充。


    微信图片_20231208165407.png

    flag还是少立,最好一段时间认真做好一件事,有计划的践行目标,完成的概率会提升许多。


    真想永远做温水中的青蛙不被煮烂


    好像已经很努力了,但是依然很普通,逐渐饱和的市场让我这种螺丝钉即使做点什么努力也不会改变任何结果,我知道,这个世界上有很多很多更加优秀的人。

    我好像有一些居安思危的改变,但骨子里依然没变,我不知道当危险来临的那天我该怎么办,我预测不到未来,比如每次面试时面试官问我“未来的规划”时,我还是不知道如何回答他,就好像在我很小的时候立过的那些flag一个都没有实现,但是我现在依然不温不火的过着还算不错的人生,也许这就是普通人的一生

    确实,永远有人更好,当下便是最好。

    加油吧,为了梦想,曾经立过的flag,哪怕他们不会实现,也给日子一点奔头,一起努力吧,加油!


    2648ff5ff16d83fcde8e1f6117c4f472.jpeg
    作者:毛毛裤
    来源:juejin.cn/post/7310560146623021090
    收起阅读 »

    客户要一次加载上千张轮播图如何丝滑地实现?不用第三方(没找到),十来行核心代码实现

    web
    引言 最近再做3D的大屏,然后客户发来1G的图片压缩包,让我全放上去当轮播图 这不得卡死啊,现成且现代化好用的第三方库没找到 于是又到了我最爱的实现源码环节,核心代码十多行即可 底部有源码 思路 压缩图片 轮播只需要两张,来回交换,用点障眼法就是无缝了 批...
    继续阅读 »

    引言


    最近再做3D的大屏,然后客户发来1G的图片压缩包,让我全放上去当轮播图


    这不得卡死啊,现成且现代化好用的第三方库没找到


    于是又到了我最爱的实现源码环节,核心代码十多行即可


    底部有源码


    思路



    • 压缩图片

    • 轮播只需要两张,来回交换,用点障眼法就是无缝了


    批量压缩


    这个用canvas就能实现,于是我写了个HTML来批量压缩


    canvas转存图片时,使用jpeg | webp的格式即可压缩,MDN上有


    使用canvas.toBlob可以压缩更多空间,这个不是重点,提一嘴而已


    image.png


    虚拟无缝轮播实现


    直接一张动图,清晰明了的解决问题


    t.gif


    是不是看起来很简单,加载前两张图,当动画结束时,改变移动那张图的src,同时位移


    再加个overflow: hidden不就行了吗


    e.gif


    编码实现


    HTMLCSS我就不贴了,这个没什么难度,容器固定大小,子元素排列一行


    然后给包装的容器添加个transform即可


    下面是用vue3写的,定义了一个imgIndexArr数组装当前要显示的索引


    _data为计算属性,根据imgIndexArr自动变化,里面放的就是图片


    我们只需要修改imgIndexArr即可实现数据切换


    image.png


    我们需要在动画完成时改变,即添加ontransitionend事件


    当触发next方法,图片滚动停止后,就要执行onTransitionEnd


    定义俩变量,一个代表最左边的图,一个为右边的图


    这里根据变量,决定谁会更新src,并且改变left值实现位移,不好描述啊


    transform会一直向右位移,left值也是,所以他们会形成永动机


    image.png


    HTML里写上他们位移的样式即可自动更新


    image.png


    Bug


    至此,看着已完成,似乎没有任何问题


    但是你把页面隐藏了过后,过一会图片全都不见了,我们打开控制台看看为什么


    可以看到,left停止更新了,也就是说,onTransitionEnd没有执行


    image.png


    transitionend在你浏览器隐藏页面时,就会停止执行


    这时需要在页面隐藏时,停止执行,执行如下代码即可


    /** 离开浏览器时 不会执行`transitionend` 所以要停止 */
    function bindEvent() {
    window.addEventListener('visibilitychange', () => {
    document.hidden
    ? stop()
    : play()
    })
    }

    这时一定有人会说,你这不能往左啊,没有控制组件啊


    如果要往左的话,只需要把两张图轮流交换改成4张图即可


    具体逻辑都是差不多的



    源码: gitee.com/cjl2385/dig…



    作者:寅时码
    来源:juejin.cn/post/7310111620368597011
    收起阅读 »

    前段时间面试了一些人,有这些槽点跟大家说说

    大家好,我是拭心。 前段时间组里有岗位招人,花了些时间面试,趁着周末把过程中的感悟和槽点总结成文和大家讲讲。 简历书写和自我介绍 今年的竞争很激烈:找工作的人数量比去年多、平均质量比去年高。裸辞的慎重,要做好和好学校、有大厂经历人竞争的准备 去年工作...
    继续阅读 »

    大家好,我是拭心。


    前段时间组里有岗位招人,花了些时间面试,趁着周末把过程中的感悟和槽点总结成文和大家讲讲。


    image.png


    简历书写和自我介绍



    1. 今年的竞争很激烈:找工作的人数量比去年多、平均质量比去年高。裸辞的慎重,要做好和好学校、有大厂经历人竞争的准备


    image.png



    1. 去年工作经历都是小公司的还有几个进了面试,今年基本没有,在 HR 第一关就被刷掉了

    2. 这种情况的,一定要走内推,让内推的人跟 HR 打个招呼:这人技术不错,让用人部门看看符不符合要求

    3. 用人部门筛简历也看学历经历,但更关注这几点:过去做了什么项目、项目经验和岗位对不对口、项目的复杂度怎么样、用到的技术栈如何、他在里面是什么角色

    4. 如果项目经历不太出彩,简历上可以补充些学习博客、GitHub,有这两点的简历我都会点开仔细查看,印象分会好很多

    5. 现在基本都视频面试,面试的时候一定要找个安静的环境、体态认真的回答。最好别用手机,否则会让人觉得不尊重!

    6. 我面过两个神人,一个在马路上边走边视频;另一个聊着聊着进了卫生间,坐在马桶上和我讲话(别问我怎么知道在卫生间的,他努力的声音太大了。。。)

    7. 自我介绍要自然一点,别像背课文一样好吗亲。面试官不是考你背诵,是想多了解你一点,就当普通聊天一样自然点

    8. 介绍的时候不要过于细节,讲重点、结果、数据,细节等问了再说

    9. 准备介绍语的时候问问自己,别人可以得到什么有用的信息、亮点能不能让对方快速 get 到

    10. 实在不知道怎么介绍,翻上去看第 4 点和第 5 点

    11. 出于各种原因,很多面试官在面试前没看过你的简历,在你做自我介绍时,他们也在一心二用 快速地浏览你的简历。所以你的自我介绍最好有吸引人的点,否则很容易被忽略

    12. 你可以这样审视自己的简历和自我介绍:


      a. 整体:是否能清晰的介绍你的学历、工作经历和技能擅长点


      b. 工作经历:是否有可以证明你有能力、有结果的案例,能否从中看出你的能力和思考


      c. 技能擅长点:是否有岗位需要的大部分技能,是否有匹配工作年限的复杂能力,是否有区别于其他人的突出点



    面试问题


    image.png



    1. 根据公司规模、岗位级别、面试轮数和面试官风格,面试的问题各有不同,我们可以把它们简单归类为:项目经历、技能知识点和软素质

    2. 一般公司至少有两轮技术面试 + HR 面试,第一轮面试官由比岗位略高一级的人担任,第二轮面试官由用人部门领导担任

    3. 不同轮数考察侧重点不同。第一轮面试主要确认简历真实性和基础技术能力,所以主要会围绕项目经历和技能知识点;第二轮面试则要确认这个人是否适合岗位、团队,所以更偏重过往经历和软素质


    项目经历


    项目经历就是我们过往做过的项目。


    项目经历是最能体现一个程序员能力的部分,因此面试里大部分时间都在聊这个。


    有朋友可能会说:胡说,为什么我的面试大部分时候都是八股文呢?


    大部分都是八股文有两种可能:要么是初级岗位、要么是你的经历没什么好问的。哦还有第三种可能,面试官不知道问什么,从网上搜的题。


    image.png


    在项目经历上,面试者常见的问题有这些:



    1. 不重要的经历占比过多(比如刚毕业的时候做的简单项目花了半页纸)

    2. 经历普通,没有什么亮点(比如都是不知名项目,项目周期短、复杂度低)

    3. 都是同质化的经历,看不出有成长和沉淀(比如都是 CRUD、if visible else gone)


    出现这种情况,是因为我们没有从面试官的角度思考,不知道面试的时候对方都关注什么。


    在看面试者的项目经历时,面试官主要关注这三点:


    1. 之前做的项目有没有难度


    2. 项目经验和当前岗位需要的是否匹配


    3. 经过这些项目,这个人的能力有哪些成长


    因此,我们在日常工作和准备面试时,可以这样做:



    1. 工作时有意识地选择更有复杂度的,虽然可能花的时间更多,但对自己的简历和以后发展都有好处

    2. 主动去解决项目里的问题,解决问题是能力提升的快车道,解决的问题越多、能力会越强

    3. 解决典型的问题后,及时思考问题的本质是什么、如何解决同一类问题、沉淀为文章、记录到简历,这些都是你的亮点

    4. 经常复盘,除了公司要求的复盘,更要做自己的复盘,复盘这段时间里有没有成长

    5. 简历上,要凸显自己在项目面试的挑战、解决的问题,写出自己如何解决的、用到什么技术方案

    6. 投简历时,根据对方业务类型和岗位要求,适当的调整项目经历里的重点,突出匹配的部分

    7. 面试时,要强调自己在项目里的取得的成果、在其中的角色、得到什么可复制的经验


    技能知识点


    技能知识点就是我们掌握的编程语言、技术框架和工具。


    相较于项目经历,技能知识点更关键,因为它决定了面试者是否能够胜任岗位。


    image.png


    在技能知识点方面,面试者常见的问题有这些:



    1. 不胜任岗位:基础不扎实,不熟悉常用库的原理

    2. 技术不对口:没有岗位需要的领域技术

    3. 技术过剩:能力远远超出岗位要求


    第一种情况就是我们常说的“技术不行”。很多人仅仅在工作里遇到不会的才学习,工作多年也没有自己的知识体系,在面试的时候很容易被基础知识点问倒,还给自己找理由说“我是高级开发还问这么细节的,面试官只会八股文”。框架也是浅尝辄止,会用就不再深入学了,这在面试的时候也很容易被问住。


    第二种情况,是岗位工作内容属于细分领域,但面试者不具备这方面的经验,比如音视频、跨端等。为了避免这种情况,我们需要打造自己的细分领域技能,最好有一个擅长的方向,越早越好。


    第三种情况简单的来说就是“太贵了”。有时候一些资深点的开发面试被挂掉,并不是因为你的能力有问题,而是因为岗位的预算有限。大部分业务需求都是增删改查和界面展示,并不需要多复杂的经验。这种情况下,要么再去看看更高级的岗位,要么降低预期。


    在我面试的人里,通过面试的都有这些特点:



    1. 技术扎实:不仅仅基础好,还有深度

    2. 解决过复杂的问题:项目经验里除了完成业务需求,也有做一些有挑战的事


    有些人的简历上只写项目经历不写技能知识点,对此我是反对的,这样做增加了面试官了解你的成本。问项目经历的目的还是想确认你有什么能力,为什么不直接明了的写清楚呢?


    软素质


    这里的「软素质」指面试时考察的、技术以外的点。


    程序员的日常工作里,除了写代码还需要做这些事:



    1. 理解业务的重点和不同需求的核心点,和其他同事协作完成

    2. 从技术角度,对需求提出自己的思考和建议,反馈给其他人

    3. 负责某个具体的业务/方向,成为这个方面所有问题的处理者


    image.png


    因此,面试官或者 HR 还会考察这些点,以确保面试者具备完成以上事情的能力:



    1. 理解能力和沟通表达能力

    2. 业务能力

    3. 稳定性


    第一点是指面试者理解问题和讲清楚答案的能力。遇到过一些面试者,面试的时候过于紧张,讲话都讲不清楚,这种就让人担心“会不会是个社恐”、“工作里该不会也这样说不清楚吧”;还有的人爱抢答,问题都没听明白就开始抢答,让人怀疑是不是性格太急躁太自大;还有的人过于能讲,但讲不到重点,东扯西扯,让人对他的经历和理解能力产生了怀疑。


    第二点是指在实现业务目标的过程中可以提供的能力。 业务发展是需要团队共同努力的,但有的人从来没这么想过,觉得自己上班的任务就是写代码,来什么活干什么活,和外包一样。


    业务发展中可能有各种问题。定方向的领导有时候会过于乐观、跨部门协作项目可能会迟迟推进不动、产品经理有时候也会脑子进水提无用需求、质量保障的测试同学可能会大意漏掉某个细节测试。这个时候,程序员是否能够主动站出来出把力,帮助事情向好的方向发展,就很重要了。


    遇到过一些面试者,在一家公司干了好几年,问起来业务发展情况语焉不详,让人感觉平时只知道写代码;还有的面试者,说起业务问题抱怨指责一大堆,“领导太傻逼”、“产品经理尽提蠢需求”,负能量满满😂。


    第三点是指面试者能不能在一家公司长久干下去。 对于级别越高的人,这点要求就越高,因为他的离开对业务的发展会有直接影响。即使级别不高,频繁换工作也会让人对你有担心:会不会抗压能力很差、会不会一不涨工资就要跑路。一般来说,五年三跳就算是临界线,比这个频繁就算是真的“跳的有点多”。


    针对以上这三点,我们可以这样做:



    1. 面试时调整心态,当作普通交流,就算不会也坦然说出,不必过于紧张

    2. 回答问题时有逻辑条理,可以采用类似总分总的策略

    3. 工作时多关注开发以外的事,多体验公司产品和竞品,在需求评审时不摸鱼、多听听为什么做、思考是否合理、提出自己的想法

    4. 定好自己的职业规划(三年小进步、五年大进步),在每次换工作时都认真问问自己:下一份工作能否帮助自己达到目标


    总结


    好了,这就是我前段时间面试的感悟和吐槽。


    总的来说,今年找工作的人不少,市面上的岗位没有往年那么多。如果你最近要换工作,最好做足准备。做好后面的规划再换、做好准备再投简历、经历整理清楚再面试。


    作者:张拭心
    来源:juejin.cn/post/7261604248319918136
    收起阅读 »

    吐槽大会,来瞧瞧资深老前端写的代码

    web
    忍无可忍,不吐不快。 本期不写技术文章,单纯来吐槽下公司项目里的奇葩代码,还都是一些资深老前端写的,希望各位对号入座。 知道了什么是烂代码,才能写出好代码。 别说什么代码和人有一个能跑就行的话,玩笑归玩笑。 人都有菜的时候,写出垃圾代码无可厚非,但是工作几年...
    继续阅读 »

    忍无可忍,不吐不快。



    本期不写技术文章,单纯来吐槽下公司项目里的奇葩代码,还都是一些资深老前端写的,希望各位对号入座。


    知道了什么是烂代码,才能写出好代码。


    别说什么代码和人有一个能跑就行的话,玩笑归玩笑。


    人都有菜的时候,写出垃圾代码无可厚非,但是工作几年了还是写垃圾代码,有点说不过去。


    我认为的垃圾代码,不是你写不出深度优先、广度优先的算法,也不是你写不出发布订阅、策略模式的 if else


    我认为垃圾代码都有一个共性:看了反胃。就是你看了天然的不舒服,看了就头疼的代码,比如排版极丑,没有空格、没有换行、没有缩进。


    优秀的代码就是简洁清晰,不能用策略模式优化 if else 没关系,多几行代码影响不大,你只要清晰,就是好代码。


    有了差代码、好代码的基本标准,那我们也废话不多说,来盘盘这些资深前端的代码,到底差在哪里。


    -------------更新------------


    集中回答一下评论区的问题:


    1、项目是原来某大厂的项目,是当时的外包团队写的,整体的代码是还行的。eslintcommitlint 之类的也是有的,只不过这些都是可以禁用的,作用全靠个人自觉。


    2、后来项目被我司收购,原来的人也转为我司员工,去支撑其他项目了。这个项目代码也就换成我们这批新来的维护了。很多代码是三几年前留下的确实没错,谁年轻的时候没写过烂代码,这是可以理解的。只不过同为三年前的代码,为啥有人写的简单清晰,拆分清楚,有的就这么脏乱差呢?


    3、文中提到的很多代码其实是最近一年写的,写这些功能的前端都是六七年经验的,当时因为项目团队刚组建,没有前端,抽调这些前端过来支援,写的东西真的非常非常不规范,这是让人难以理解的,他们在原来的项目组(核心产品,前端基建和规范都很厉害)也是这么写代码的吗?


    4、关于团队规范,现在的团队是刚成立没多久的,都是些年轻人,领导也不是专业前端,基本属于无前端 leader 的状态,我倒是很想推规范,但是我不在其位,不好去推行这些东西,提了建议,领导是希望你简单写写业务就行(说白了我不是 leader 说啥都没用)。而且我们这些年轻前端大家都默认打开 eslint,自觉性都很高,所以暂时都安于现状,代码 review 做过几次,都没太大的毛病,也就没继续做了。


    5、评论区还有人说想看看我写的代码,我前面文章有,我有几个开源项目,感兴趣可以自己去看。项目上的代码因为涉及公司机密,没法展示。


    6、本文就是篇吐槽,也不针对任何人,要是有人看了不舒服,没错,说的就是你。要是没事,大家看了乐呵乐呵就行。轻喷~


    文件命名千奇百怪


    同一个功能,都是他写的,三个文件夹三种奇葩命名法,最重要的是第三个,有人能知道它这个文件夹是什么功能吗?这完全不知道写的什么玩意儿,每次我来看代码都得重新理一遍逻辑。


    image.png


    组件职责不清


    还是刚才那个组件,这个 components 文件夹应该是放各个组件的,但是他又在底下放一个 RecordDialog.jsx 作为 components 的根组件,那 components 作为文件名有什么意义呢?


    image.png


    条件渲染逻辑置于底层


    这里其实他写了几个渲染函数,根据客户和需求的不同条件性地渲染不同内容,但是判断条件应该放在函数外,和函数调用放在一起,根据不同条件调用不同渲染函数,而不是函数内就写条件,这里就导致逻辑内置,过于分散,不利于维护。违反了我们常说的高内聚、低耦合的编程理念。


    image.png


    滥用、乱用 TS


    项目是三四年前的项目了,主要是类组件,资深前端们是属于内部借调来支援维护的,发现项目连个 TS 都没有,不像话,赶紧把 TS 配上。配上了,写了个 tsx 后缀,然后全是无类型或者 anyscript。甚至于完全忽视 TSESlint 的代码检查,代码里留下一堆红色报错。忍无可忍,我自己加了类型。


    image.png


    留下大量无用注释代码和报错代码


    感受下这个注释代码量,我每次做需求都删,但是几十个工程,真的删不过来。


    image.png


    image.png


    丑陋的、隐患的、无效的、混乱的 css


    丑陋的:没有空格,没有换行,没有缩进


    隐患的:大量覆盖组件库原有样式的 css 代码,不写类名实现 namespace


    无效的:大量注释的 css 代码,各种写了类名不写样式的垃圾类名


    混乱的:从全局、局部、其他组件各种引入 css 文件,导致样式代码过于耦合


    image.png


    一个文件 6 个槽点


    槽点1:代码的空行格式混乱,影响代码阅读


    槽点2:空函数,写了函数名不写函数体,但是还调用了!


    槽点3:函数参数过多不优化


    槽点4:链式调用过长,属性可能存在 undefined 或者 null 的情况,代码容易崩,导致线上白屏


    槽点5:双循环嵌套,影响性能,可以替换为 reduce 处理


    槽点6:参数、变量、函数的命名随意而不语义化,完全不知道有何作用,且不使用小驼峰命名


    image.png


    变态的链式取值和赋值


    都懒得说了,各位观众自己看吧。


    image.png


    代码拆分不合理或者不拆分导致代码行数超标


    能写出这么多行数的代码的绝对是人才。


    尽管说后来维护的人加了一些代码,但是其初始代码最起码也有近 2000 行。


    image.png


    这是某个功能的数据流文件,使用 Dva 维护本身代码量就比 Redux 少很多了,还是能一个文件写出这么多。导致到现在根本不敢拆出去,没人敢动。


    image.png


    杂七杂八的无用 js、md、txt 文件


    在我治理之前,充斥着非常多的无用的、散乱的 md 文档,只有一个函数却没有调用的 js 文件,还有一堆测试用的 html 文件。


    实在受不了干脆建个文件夹放一块,看起来也要舒服多了。


    image.png


    less、scss 混用


    这是最奇葩的。


    image.png


    特殊变量重命名


    这是真大佬,整个项目基建都是他搭的。写的东西也非常牛,bug 很少。但是大佬有一点个人癖好,例如喜欢给 window 重命名为 G。这虽然算不上大缺点,但真心不建议大家这么做,window 是特殊意义变量,还请别重命名。


    const G = window;
    const doc = G.document;

    混乱的 import


    规范的 import 顺序,应该是框架、组件等第三方库优先,其次是内部的组件库、包等,然后是一些工具函数,辅助函数等文件,其次再是样式文件。乱七八糟的引入顺序看着都烦,还有这个奇葩的引入方式,直接去 lib 文件夹下引入组件,也是够奇葩了。


    image.png


    写在最后


    就先吐槽这么多吧,这些都是平时开发过程中容易犯的错误。


    要想保持一个好的编码习惯,写代码的时候就得时刻告诉自己,你的代码后面是会有人来看的,不想被骂就写干净点。


    我觉得什么算法、设计模式都是次要的,代码清晰,数据流向清晰,变量名起好,基本 80% 不会太差。


    写代码就像做人,现实总是千难万苦,各种妥协和无奈,但是这不意味着我们可以无底线的做事。给自己设个底线,不论做人还是做事。


    共勉。


    作者:北岛贰
    来源:juejin.cn/post/7265505732158472249
    收起阅读 »

    博客园又崩了,这个锅要不要阿里云背?

    昨天下午博客园又崩了,不过与其它大厂的崩溃不同,博客园出现崩溃的频率好像有点高。 这是怎么回事呢?和阿里云又有什么关系,这篇文章就带大家来一探究竟。 到底是谁的问题? 昨天下午(2023年12月8日)博客园官方发布了一个故障公告,官网截图如下: 博客园的故障...
    继续阅读 »

    昨天下午博客园又崩了,不过与其它大厂的崩溃不同,博客园出现崩溃的频率好像有点高。


    这是怎么回事呢?和阿里云又有什么关系,这篇文章就带大家来一探究竟。


    到底是谁的问题?


    昨天下午(2023年12月8日)博客园官方发布了一个故障公告,官网截图如下:



    博客园的故障是数据库CPU 100%,今年已经出现了7次,根据我这个不经常上博客园的人的观察,往年也有出现,好像频率没这么高。


    出现了7次都不能解决,这是个什么问题呢?


    根据我的技术经验,数据库CPU百分之百,一般是某些SQL写的质量不佳,在某些情况下可能出现了大数据量全表扫描的情况,迟迟不能执行完毕,长期霸占CPU资源导致的。


    按说这种问题只要定位到对应的SQL,改掉相关语句就可以了,但是就是这个问题把博客园难住了。


    参数嗅探问题?


    看看官方针对此次问题的说明:





    这里有两个重要的信息:博客园的数据库使用的是 SQL Server;博客园的主要查询使用的是存储过程。博客园是.NET技术体系的,使用SQL Server比较顺其自然;使用存储过程可以提高SQL执行的效率,博客园是08年创立的,这在十几年前也比较流行;看它使用的分页方法也是比较新的,这说明它也一直在优化。


    官方怀疑是参数嗅探问题造成 SQL Server 缓存了性能极差的执行计划,这句话中有两个名词:参数嗅探问题和执行计划,没接触过的同学可能会比较蒙,我先给大家普及一下。


    执行计划:每条SQL在数据库内部执行时都会有一个执行计划,主要就是先查询哪张表、表之间怎么关联、执行的时候使用哪些索引,等等。


    参数嗅探问题:存储过程在首次执行时会先进行编译,后续执行的时候都使用这个编译的结果,而不是每次都解释执行,因为编译相对比较耗时。编译时,数据库还会根据当前使用的存储过程参数确定一个最优的执行计划,并把这个执行计划也一并缓存起来,后续再执行的时候就会直接使用这个执行计划。


    问题主要就出现在这个缓存的执行计划,因为对于不同的参数来说,执行计划的效率可能差别很大,这主要是查询数据分布不均匀的问题造成的。


    我在公司的业务中也经常遇到这个问题,有的用户数据多,有的用户数据少,即使我们为用户Id字段设置了索引,数据库有时仍旧会认为不使用这个索引的效率更高,它会自己选择一个自认为更优的查询路径,比如全表扫描,实际执行时就出现了慢SQL的情况。


    到博客园这里,官方认为就是自己的某个存储过程因为参数嗅探问题导致某些慢SQL,慢SQL导致CPU使用过高,最后导致数据库崩溃。


    而官方一直没有定位到出现问题的SQL或者出现问题的存储过程,可能博客园的SQL太多了吧,出现问题的不止一个SQL。又或者是 SQL Server 的问题,或者阿里云的锅?


    SQL Server的问题?


    SQL Server 作为一款商业数据库,能活到现在,而且价格还不低,其产品能力是经过了残酷的市场考验的。虽然任何产品都不可避免的存在一些BUG,但是导致这种问题的BUG应该不会持续这么久。所以 SQL Server 本身的问题应该不大,或者说 SQL Server 的数据查询方式没有问题。


    还有很多同学提到 SQL Server 性能不行,单纯根据我的使用经验来说,类似的场景 SQL Server的查询性能往往比 MySQL 要好不少,其它很多用户也有类似的反馈:



    我也专门找了一些 SQL Server 和其它数据库的性能对比,截图如下:



    文章和数据来源:


    segmentfault.com/q/101000002…


    http://www.ijarcce.com/upload/2015…


    另外我们也可以从博客园分享的数据库的监控日志中略窥一二:



    从图上可以看出,出现问题的时间比较随机,也不是什么高峰期。博客园也提到过凌晨4-5点钟出现类似问题。看这个CPU使用率只有20%多一点,所以并非是遇到了性能瓶颈。



    阿里云的问题?


    阿里云为什么可能背锅?因为博客园部署在阿里云上,服务器和数据库都用的阿里云产品。


    记得之前出现这个问题时,博客园官方对阿里云颇多微词,后来双方可能进行了深入交流,博客园接受了参数嗅探问题,此后就一直在这块查找。


    那么阿里云能不能彻底撇清关系呢?


    正常情况下,阿里云上部署的 SQL Server 应该是从微软购买的,微软应该也要提供一些技术支持,包括安装和日常的运行维护支持。这个 SQL Server 可能和 Azure 上部署的有些差别,但微软也不会砸自己的招牌,数据库版本不应该有大问题。


    阿里云只是部署和运维 SQL Server,说白了阿里云只是搞了底层的存储、网络、操作系统等服务,上层的数据库应用完全是微软的,他插不上手,这种数据库程序的CPU百分百的故障很难和阿里云干的事挂上钩。


    再者阿里云自己也开发数据库,虽然 SQL Server 不开源,但是高手们对于一些底层的设计,或者可能存在问题的地方,应该也是门清的。阿里云上 SQL Server 服务使用者众多,如果很多企业都遇到这个问题,应该也早就爆出来并解决了。


    所以这个问题甩锅到阿里云身上的难度比较大。当然也没办法完全排除,毕竟总有些极端情况,阿里云最近也崩了很多次,会不会在某些方面有些幺蛾子?大家也不知道。


    怎么解决问题?


    换数据库?



    正如上文所说,问题出现在数据库自身上的可能性不大,而且换数据库要重写所有的SQL,还可能要修改表结构,这个工作量不是一星半点。


    如果真的是参数嗅探问题,换了数据库一样存在执行计划效率不一致的问题。


    换云?


    这基本是认为阿里云能力不行。


    如果真的怀疑是这方面的问题,倒是可以试试,不过不是直接迁移过去,而是把数据导出来一份,放到别的公有云上,或者本地部署一套SQL Server。


    然后采集SQL执行日志,在测试的数据库中进行重放执行,如果问题还会发生,那就不是云厂商的问题,如果跑了很久,问题都没有出现过,那才有根据说云服务的问题概率比较大一些。


    当然这个测试的成本比较高,也许可以通过精简样本或者提高SQL执行频率加速一下测试。


    作为技术人,甩锅时一定要有理有据。


    再或者就不讲理,博客园死磕阿里云,要么就是你的问题,要么就是你帮我找出问题来。有时候云厂商的技术团队也是可以上门或者以其他方式进行亲密沟通的。再不行花点钱找个高手呢?可能还是博客园太老实了?或者阿里云太傲慢了?又或者博客园太穷了?


    解决参数嗅探问题


    阿里云的问题只能是猜测,参数嗅探的问题确是能够实实在在抓住的,阿里云的数据库产品是提供了慢SQL日志查询的。


    只需要找出出现问题时的慢SQL,看博客园以往的故障公告也是曾经抓到过一些问题SQL的。


    但是问题为什么还会一直出现呢?


    有可能是问题SQL太多了。经过十几年的迭代,博客园的代码量可能十分庞大,再加上博客园这两年经营比较困难,没有人力和精力投入到这方面,只能问题出现了再去反查,然后改正。能活着就不错了,估计团队内部也没有技术牛人,精力都放到了活下来的事情上。


    具体为什么一直解决不了,咱们就说到这里。


    下面给大家聊聊怎么解决参数嗅探的问题,我想这个对于搞技术的同学来说才是最重要的.


    上面我们已经说过参数嗅探问题就是数据库使用了效率不高的执行计划,那么解决这个问题的核心思路就是让数据库不去使用这些低效计划。这里分享一些我了解的方法。


    暴力清理


    重启服务器、重启数据库,博客园采用的处理方法差不多都是这个。



    还有一个稍微优雅点的方案,清除所有的执行计划缓存:DBCC FREEPROCCACHE,不管这些执行计划是不是有问题。但是不确定这个指令能不能在阿里云的数据库服务上执行。


    这些都是强制重新创建执行计划的方法,坏处就是影响都比较大,很可能会影响用户使用服务,比较暴力。


    而且这些方法不能治本,只能短时间的缓解一下,说不定在某个时刻,执行计划又被重建了,或者SQL执行又超时了。


    优雅机制


    SQL Server本身也有一些优雅的方案来缓解这个问题。比如:



    • 不缓存执行计划,虽然缓存能带来一些效率上的提升,但相比参数嗅探问题带来的性能损失就是小巫见大巫了。可以在存储过程中使用WITH RECOMPILE,让查询每次都重新编译。

    • 强制使用某个查询计划,比如强制使用某个索引,这个索引对于所有的查询都不会太差;SQL Server中还可以强制使用某个条件的查询计划。不过找到这个索引或者条件的难度可能比较大,因为数据一直在变化,现在是好的并不代表一直好。

    • 只清除特定语句或存储过程的查询缓存,使用 DBCC FREEPROCCACHE(@plan_id) 指定执行计划,这样影响更小。

    • 另外表统计信息陈旧、索引碎片、缺少索引都可能导致参数嗅探问题,遇到问题时可以从这几个方面调查一下。


    详情可参考阿里的这篇文章: mysql.taobao.org/monthly/201…


    谨慎评估


    在我们设计表、编写SQL的时候,需要考虑数据会如何分布,查询有哪些条件,特别是数据可能分布不均匀的情况。


    比如有的用户的数据量可能是大部分用户的10倍、甚至百倍,排序的字段可能导致不使用包含条件字段的索引,查询可能在多个索引之间飘移。


    如果可能存在问题,就要考虑表如何设计、数据如何查询,普通关系数据库难以解决时,我们还可以考虑采用NoSQL、分布式数据库等方案,以稳定查询效率。




    以上就是本文的主要内容了,因本人才疏学浅,不免存在错漏,如有问题还望指正。


    关注微/信/公/众/号:萤火架构,技术提升不迷路。


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

    【Java集合】双列集合HashMap的概念、特点及使用

    HashMap是Java中的一个集合类,它实现了Map接口,提供了一种存储键值对的方式。你可以把它想象成一个没有固定大小和形状的储物柜,你可以随时往里面放东西,也可以随时取出东西。而且,这个储物柜还有一个神奇的功能,那就是无论你放进去的是什么,取出来的总是你放...
    继续阅读 »

    HashMap是Java中的一个集合类,它实现了Map接口,提供了一种存储键值对的方式。你可以把它想象成一个没有固定大小和形状的储物柜,你可以随时往里面放东西,也可以随时取出东西。而且,这个储物柜还有一个神奇的功能,那就是无论你放进去的是什么,取出来的总是你放进去的那个。

    上篇文章讲了Map接口的概念,以及Map接口中的常用方法和对Map集合的遍历,本篇文章我们将继续介绍另一个十分重要的双列集合—HashMap。


    HashMap 概念

    HashMap集合是Map接口的一个实现类,它用于存储键值映射关系,该集合的键和值允许为空,但键不能重复,且集合中的元素是无序的。

    特点

    HashMap底层是由哈希表结构组成的,其实就是“数组+链表”的组合体,数组是HashMap的主体结构,链表则主要是为了解决哈希值冲突而存在的分支结构。正因为这样特殊的存储结构,HashMap集合对于元素的增、删、改、查操作表现出的效率都比较高。

    结构

    在java1.8以后采用数组+链表+红黑树的形势来进行存储,通过散列映射来存储键值对,如下图:

    Description

    • 在初始化时将会给定默认容量为16

    • 对key的hashcode进行一个取模操作得到数组下标

    • 数组存储的是一个单链表

    • 数组下标相同将会被放在同一个链表中进行存储

    • 元素是无序排列的

    • 链表超过一定长度(TREEIFY_THRESHOLD=8)会转化为红黑树

    • 红黑树在满足一定条件会再次退回链表

    看到这个图,是不是挺熟悉!没错,这个就是我们在讲Set时,它的内存结构图,当时我们说 HashSet的底层就是 Map集合,只不过Set只使用了Map集合中的Key,没有使用Value而已。

    小练习

    在之前我们已经讲了不少Map的使用方法,本篇中就不做过多解释了,来上了个小练习,在体会下它的使用。

    每位学生(姓名,年龄)都有自己的家庭住址。那么,既然有对应关系,则将学生对象和家庭住址存储到map集合中。学生作为键, 家庭住址作为值。

    注意,学生姓名相同并且年龄相同视为同一名学生。

    编写学生类:

        public class Student {
    private String name;
    private int age;

    public Student() {
    }

    public Student(String name, int age) {
    this.name = name;
    this.age = age;
    }

    public String getName() {
    return name;
    }

    public void setName(String name) {
    this.name = name;
    }

    public int getAge() {
    return age;
    }

    public void setAge(int age) {
    this.age = age;
    }

    @Override
    public boolean equals(Object o) {
    if (this == o)
    return true;
    if (o == null || getClass() != o.getClass())
    return false;
    Student student = (Student) o;
    return age == student.age && Objects.equals(name, student.name);
    }

    @Override
    public int hashCode() {
    return Objects.hash(name, age);
    }
    }

    编写测试类:

        public class HashMapTest {
    public static void main(String[] args) {
    //1,创建Hashmap集合对象。
    Map<Student,String>map = new HashMap<Student,String>();
    //2,添加元素。
    map.put(newStudent("lisi",28), "上海");
    map.put(newStudent("wangwu",22), "北京");
    map.put(newStudent("zhaoliu",24), "成都");
    map.put(newStudent("zhouqi",25), "广州");
    map.put(newStudent("wangwu",22), "南京");

    //3,取出元素。键找值方式
    Set<Student>keySet = map.keySet();
    for(Student key: keySet){
    Stringvalue = map.get(key);
    System.out.println(key.toString()+"....."+value);
    }
    }
    }
    • 当给HashMap中存放自定义对象时,如果自定义对象作为key存在,这时要保证对象唯一,必须复写对象的hashCode和equals方法(如果忘记,请回顾HashSet存放自定义对象)。

    • 如果要保证map中存放的key和取出的顺序一致,可以使用java.util.LinkedHashMap集合来存放。

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

    LinkedHashMap

    我们知道HashMap保证成对元素唯一,并且查询速度很快,可是成对元素存放进去是没有顺序的,那么我们要保证有序,还要速度快怎么办呢?

    在HashMap下面有一个子类LinkedHashMap,它继承自HashMap。特别的是,LinkedHashMap在HashMap的基础上维护了一个双向链表,可以按照插入顺序或者访问顺序来迭代元素。此外,LinkedHashMap结合了HashMap的数据操作和LinkedList的插入顺序维护的特性,因此也可以被看做是HashMap与LinkedList的结合。它是链表和哈希表组合的一个数据存储结构。把上个练习使用LinkedHashMap的使用一下

        publicclass LinkedHashMapDemo {
    publicstaticvoid main(String[] args) {

    //Map<String, String> map = new HashMap<String, String>();

    LinkedHashMap<String, String> map = new LinkedHashMap<String, String>();
    map.put("马云", "阿里巴巴");
    map.put("马化腾", "腾讯");
    map.put("李彦宏", "百度");
    Set<Entry<String, String>> entrySet = map.entrySet();
    for (Entry<String, String> entry : entrySet) {
    System.out.println(entry.getKey() + " " + entry.getValue());
    }
    }
    }

    总结

    总的来说,HashMap是Java中的一个强大工具,它可以帮助我们高效地处理大量的数据。但是,我们也需要注意,虽然HashMap的性能很高,但如果不正确地使用它,可能会导致内存泄漏或者数据丢失的问题。因此,我们需要正确地理解和使用HashMap,才能充分发挥它的强大功能。

    本系列文章写到这里,为大家介绍集合家族的知识,基本上就可以告一段落了。

    在这个系列文章中,我们讲述了单列和双列集合的家族体系以及简单的使用。集合中不少的实现类,我们并未讲述,大家下来可以通过java的API文档,去学习使用。还是那句话,熟能生巧!只看不练,假把式!

    本系列以上内容,都是在实际项目中,会经常碰到这些概念的使用,当然了,文中的内容可能也不是尽善尽美的,如有错误,可以私信,探讨!
    happy ending!

    收起阅读 »

    教你如何实现一个页面自动打字效果

    web
    前言: 最近在写一个类似于 windows 启动页的项目,不知道大家是否还记的 windows 很经典的小光标加载页面,我又稍微改造了一下效果如下: 一. 光标闪烁效果的实现 tips: 在这里我们使用了 UnoCSS,如果你不清楚 UnoCSS 的使用方...
    继续阅读 »

    前言: 最近在写一个类似于 windows 启动页的项目,不知道大家是否还记的 windows 很经典的小光标加载页面,我又稍微改造了一下效果如下:


    loading.gif




    一. 光标闪烁效果的实现


    tips: 在这里我们使用了 UnoCSS,如果你不清楚 UnoCSS 的使用方法,那你可以点击下面这篇文章。

    🫱 🎁手把手教你创建自己的代码仓库



    1. 首先准备一块黑色的背景。

      image.png

    2. 其实光标的样式非常非常简单,仅仅只需要你创建一个宽高合适的 div,然后创建一个底部的 border 效果即可。

      image.png
      下面应该是你目前的效果。

      image.png

    3. 现在需要清楚的知道,这个白块的展示其实就是我们控制展示这个 divborder 的显示还是隐藏。那么现在我们的思路就很清晰了,所以这里我们只需要写一个变量来动态的切换这个 border 值即可。

      image.png

    4. 现在你的页面效果应该是漆黑一片,那交给谁来动态的切换这个状态呢?这里其实很简单,当页面挂载的时候,我们只需要开启一个定时器来动态切换即可。

      image.png

      这时候我们其实就能看到一丢丢效果了:

      flash.gif


    二. 自动打字效果的实现



    1. 首先我们应该明确一个概念,我们目前要做的事很简单,只需要在百块 div 的前面插入文字其实就是在向后推白块

      image.png

      image.png

      所以白块的移动是我们无需关心的,我们仅仅只需要去处理如何插入字体的问题。

    2. 这里我们先准备一个常量来书写一段字符串文字,然后还需要给准备放文字的 div 打上 ref 为后面的工作做准备,之后我们需要用到它身上相关的属性。

      image.png

    3. 接下来我们要编写一个函数去处理这个问题,名字起的就随意点吧,就叫做 autoPrint

      image.png

    4. 这里我们仍需要开启一个循环定时器去控制,因为我们无法得知文字具体有多少,不考虑使用 setTimeout

      image.png

    5. 还需要准备两个变量,来存放接下来我们要处理的文字信息。

      image.png

    6. 下面代码的思路就比较简单了,其实就是调用了 substring 方法来一直切割获取下一个字符串的值。substring本身也是不改变原字符串的,所以我们只需要控制 index 就可以很轻松的获取到最后的值。

      image.png

      效果如下:

      3.gif

    7. 最后别忘了在合适的时机清除这个定时器。

      image.png


    三. 更优雅的实现小方块闪烁


    更新于 2023/02/22



    1. 在写上面的代码之前我没有考虑文字过长的问题,导致小光标不会换行的问题。

    2. 今天更新一下,修复这个 bug

      自动.gif

    3. 我们删除上面之前控制 border 的显示与否而展示的小光标样式。

      image.png

    4. 在放置文字的 div 添加一个伪元素来实现这个效果,更加简洁一点。

      image.png

    5. 并且使用动画来替换之前的 flicker
      image.png


    四. 源码



    <script>

    //tips: automatic printing welcome words.
    function autoPrintText(text: string) {
    let _str = ""
    let _index = 0
    const _timerID = window.setInterval(() => {
    if (!textAreas.value) return
    if (_index > text.length - 1) {
    clearInterval(_timerID)
    return
    }
    _str = _str + text.substring(_index, _index + 1)
    textAreas.value!.innerText = _str
    _index++
    }, printSpeed)
    }

    </script>

    <template>

    <div v-if="isFlicker" class="w-full h-full">
    <div class="text-box w-fit">
    <span ref="textAreas" class="text-1.8rem font-600"></span>
    </div>
    </template>

    <style scoped>
    .text-box::after {
    display: inline-block;
    content: "";
    width: 2rem;
    vertical-align: text-bottom;
    border-bottom: 3px solid white;
    margin-left: 8px;
    animation: flicker 0.5s linear infinite;
    }

    @keyframes flicker {
    from {
    opacity: 0;
    }
    to {
    opacity: 1;
    }
    }
    </style>

    预告


    最近在实现一个 window 的全套 UI ,PC 和移动端的效果是完全自适应的,两者有两套 UI

    4.gif

    我会在本周更新拖拽这个经典面试题的实现,仍会使用费曼学习法通俗易懂的讲解。如果你有兴趣,不妨保持关注。🎁


    作者:韩振方
    来源:juejin.cn/post/7200773486796914725
    收起阅读 »

    全网显示IP归属地,免费可用,快来看看

    前言 经常浏览小视频或各类帖子的朋友们可能已经注意到,目前许多网络平台都会显示作者和评论区用户的IP归属地。那么,这个功能是如何实现的呢? 某些收费平台的API 我们可以利用一些付费平台的API来实现这一功能,比如一些导航软件的开放平台API等。然而,这些服...
    继续阅读 »

    前言


    经常浏览小视频或各类帖子的朋友们可能已经注意到,目前许多网络平台都会显示作者和评论区用户的IP归属地。那么,这个功能是如何实现的呢?



    某些收费平台的API


    我们可以利用一些付费平台的API来实现这一功能,比如一些导航软件的开放平台API等。然而,这些服务通常是收费的,而且免费额度有限,适合测试使用,但如果要在生产环境中使用,很可能不够支撑需求。



    离线库推荐


    那么,有没有免费的离线API库呢?UP现在推荐一个强大的离线库给大家,一个准确率高达99.9%的离线IP地址定位库,查询速度仅需0.0x毫秒,而且数据库仅10兆字节大小。此库提供了Java、PHP、C、Python、Node.js、Golang、C#等多种查询绑定,同时支持Binary、B树和内存三种查询算法。



    这个库大家可以在GitHub上搜索:ip2region,即可找到该开源库。


    使用


    下面使用Java代码给大家演示下如何使用这个IP库,该库目前支持多重主流语言。


    1、引入依赖


    <dependency>
       <groupId>org.lionsoul</groupId>
       <artifactId>ip2region</artifactId>
       <version>2.7.0</version>
    </dependency>

    2、下载离线库文件 ip2region.xdb



    3、简单使用代码


    下面,我们通过Java代码,挑选某个国内的IP进行测试,看看会输出什么样的结果


    public class IpTest {

       public static void main(String[] args) throws Exception {
           // 1、创建 searcher 对象 (修改为离线库路径)
           String dbPath = "C:\Users\Administrator\Desktop\ip2region.xdb";
           Searcher searcher = null;
           try {
               searcher = Searcher.newWithFileOnly(dbPath);
          } catch (Exception e) {
               System.out.printf("failed to create searcher with `%s`: %s\n", dbPath, e);
               return;
          }

           // 2、查询
           String ip = "110.242.68.66";
           try {
               long sTime = System.nanoTime(); // Happyjava
               String region = searcher.search(ip);
               long cost = TimeUnit.NANOSECONDS.toMicros((long) (System.nanoTime() - sTime));
               System.out.printf("{region: %s, ioCount: %d, took: %d μs}\n", region, searcher.getIOCount(), cost);
          } catch (Exception e) {
               System.out.printf("failed to search(%s): %s\n", ip, e);
          }

           // 3、关闭资源
           searcher.close();

           // 备注:并发使用,每个线程需要创建一个独立的 searcher 对象单独使用。
      }

    }


    输出结果为:


    {region: 中国|0|河北省|保定市|联通, ioCount: 3, took: 1192 μs}

    其中,region的格式为 国家|区域|省份|城市|ISP,缺省的地域信息默认是0。


    当然,这个库不只是支持国内的IP,也支持国外的IP。



    其他语言可以参考该开源库的说明文档。


    总结


    这是一个准确率非常高的离线库,如果项目里有IP定位需求的,可以试下该库。


    作者:happyjava
    来源:juejin.cn/post/7306334713992708122
    收起阅读 »

    前端如何使用websocket发送消息

    web
    1 基础介绍 1.1 什么是WebSocket WebSocket 是一种在单个 TCP 连接上进行 全双工 通信的协议,它可以让客户端和服务器之间进行实时的双向通信。与传统的 HTTP 请求不同,WebSocket 使用了一个长连接,在客户端和服务器之间保...
    继续阅读 »

    1 基础介绍


    1.1 什么是WebSocket



    WebSocket 是一种在单个 TCP 连接上进行 全双工 通信的协议,它可以让客户端和服务器之间进行实时的双向通信。与传统的 HTTP 请求不同,WebSocket 使用了一个长连接,在客户端和服务器之间保持持久的连接,从而可以实时地发送和接收数据。




    在 WebSocket 中,客户端和服务器之间可以互相发送消息。
    客户端可以使用 JavaScript 中的 WebSocket API 发送消息到服务器,也可以接收服务器发送的消息。



    1.2 WebSocket与HTTP的区别



    WebSocket与HTTP的区别在于连接的性质和通信方式。WebSocket是一种双向通信的协议,通过一次握手即可建立持久性的连接,服务器和客户端可以随时发送和接收数据。而HTTP协议是一种请求-响应模式的协议,每次通信都需要发送一条请求并等待服务器的响应。WebSocket的实时性更好,延迟更低,并且在服务器和客户端之间提供双向的即时通信能力,适用于需要实时数据传输的场景。



    1.3 代码示例



    下面是一个使用 WebSocket API 发送消息的代码示例:



    var socket = new WebSocket("ws://example.com/socketserver");

    socket.onopen = function(event) {
    socket.send("Hello server!");
    };

    socket.onmessage = function(event) {
    console.log("Received message from server: " + event.data);
    };

    socket.onerror = function(event) {
    console.log("WebSocket error: " + event.error);
    };

    socket.onclose = function(event) {
    console.log("WebSocket connection closed with code " + event.code);
    };


    在上面的代码中,首先创建了一个 WebSocket 对象,指定了服务器的地址。然后在 onopen 回调函数中,发送了一个消息到服务器。当服务器发送消息到客户端时,onmessage 回调函数会被触发,从而可以处理服务器发送的消息。如果出现错误或者连接被关闭,onerror 和 onclose 回调函数会被触发,从而可以处理这些事件。


    需要注意的是,在使用 WebSocket 发送消息之前,必须先建立 WebSocket 连接。在上面的代码中,通过创建一个 WebSocket 对象来建立连接,然后在 onopen 回调函数中发送消息到服务器。如果在连接建立之前就尝试发送消息,那么这些消息将无法发送成功。



    2 前端使用WebSocket的流程


    2.1 创建WebSocket对象


    通过JavaScript中的new WebSocket(URL)方法创建WebSocket对象,其中URL是WebSocket服务器的地址。根据实际情况修改URL以与特定的WebSocket服务器进行连接。例如:


    const socket = new WebSocket('ws://localhost:8000');

    2.2 监听WebSocket事件


    WebSocket对象提供多种事件用于监听连接状态和接收消息,例如:open、message、close、error等。



    • open:当与服务器建立连接时触发。

    • message:当收到服务器发送的消息时触发。

    • close:当与服务器断开连接时触发。

    • error:当连接或通信过程中发生错误时触发。


    通过添加事件监听器,可以在相应事件发生时执行特定的逻辑。例如:


    socket.addEventListener('open', () => {
    console.log('WebSocket连接已建立');
    });

    socket.addEventListener('message', (event) => {
    const message = event.data;
    console.log('收到消息:', message);
    });

    socket.addEventListener('close', () => {
    console.log('WebSocket连接已断开');
    });

    socket.addEventListener('error', (error) => {
    console.error('发生错误:', error);
    });

    2.3 发送消息


    通过WebSocket对象的send(data)方法发送消息,其中data是要发送的数据,可以是字符串、JSON对象等。可以根据实际需求将数据格式化成特定的类型进行发送。例如:


    const message = 'Hello, server!';
    socket.send(message);

    2.4 关闭WebSocket连接


    当通信结束或不再需要与服务器通信时,需要关闭WebSocket连接以释放资源。通过调用WebSocket对象的close()方法可以主动关闭连接,也可以根据业务需求设置自动关闭连接的条件。例如:


    socket.close();

    3 前端发送消息的应用实例


    一个常见的前端发送消息的应用实例是在线聊天应用。在这种应用中,前端通过WebSocket与后端服务器建立连接,并实时发送和接收聊天消息。


    以下是一个简单的前端发送消息的示例代码:


    const socket = new WebSocket('ws://localhost:8000');

    // 连接建立事件
    socket.addEventListener('open', () => {
    console.log('WebSocket连接已建立');
    });

    // 消息接收事件
    socket.addEventListener('message', (event) => {
    const message = event.data;
    console.log('收到消息:', message);
    // 处理接收到的消息,将其显示在前端界面上
    });

    // 发送消息
    function sendMessage(message) {
    socket.send(message);
    }

    // 调用发送消息的函数,例如在点击按钮后发送消息
    const sendButton = document.getElementById('sendBtn');
    sendButton.addEventListener('click', () => {
    const messageInput = document.getElementById('messageInput');
    const message = messageInput.value;
    sendMessage(message);
    messageInput.value = ''; // 清空输入框
    });

    // 连接关闭事件
    socket.addEventListener('close', () => {
    console.log('WebSocket连接已断开');
    });

    // 连接错误事件
    socket.addEventListener('error', (error) => {
    console.error('发生错误:', error);
    });


    该示例中,通过创建WebSocket对象,监听连接建立事件、消息接收事件、连接关闭事件和错误事件,从而实现与服务器的实时通信。通过构建界面和处理消息的逻辑,可以实现实时聊天功能。


    这只是一个简单的示例,实际上,前端发送消息的应用可以更广泛,如实时数据更新、多人协作编辑、实时游戏等。具体的实现方式和功能根据实际需求而定,可以灵活调整和扩展。



    4 WebSocket的应用场景


    WebSocket的应用场景包括但不限于以下几个方面:



    1. 实时聊天应用:WebSocket能够提供双向、实时的通信机制,使得实时聊天应用能够快速、高效地发送和接收消息,实现即时通信。

    2. 实时协作应用:WebSocket可以用于实时协作工具,如协同编辑文档、白板绘画、团队任务管理等,团队成员可以实时地在同一页面上进行互动和实时更新。

    3. 实时数据推送:WebSocket可以用于实时数据推送场景,如股票行情、新闻快讯、实时天气信息等,服务器可以实时将数据推送给客户端,确保数据的及时性和准确性。

    4. 多人在线游戏:WebSocket提供了实时的双向通信机制,适用于多人在线游戏应用,使得游戏服务器能够实时地将游戏状态和玩家行为传输给客户端,实现游戏的实时互动。

    5. 在线客服和客户支持:WebSocket可以用于在线客服和客户支持系统,实现实时的客户沟通和问题解决,提供更好的用户体验,减少等待时间。



    WebSocket适用于需要实时双向通信的场景,在这些场景中,它能够提供更好的实时性、低延迟和高效性能,为Web应用程序带来更好的交互性和用户体验。



    作者:李泽南
    来源:juejin.cn/post/7277835425959886882
    收起阅读 »

    面试官:你知道websocket的心跳机制吗?

    web
    前言 哈喽,大家好,我是泽南Zn👨‍🎓。在之前的一篇文章写到, 前端如何使用websocket发送消息,websocket是怎么建立连接的呢?如果断开了会怎样?如何一直保持长连接呢?接下来,本篇文章将会带你了解--- WebSocket心跳机制 一、...
    继续阅读 »

    前言


    哈喽,大家好,我是泽南Zn👨‍🎓。在之前的一篇文章写到, 前端如何使用websocket发送消息,websocket是怎么建立连接的呢?如果断开了会怎样?如何一直保持长连接呢?接下来,本篇文章将会带你了解--- WebSocket心跳机制


    一、WebSocket心跳机制


    前端实现WebSocket心跳机制的方式主要有两种:




    1. 使用setInterval定时发送心跳包。

    2. 在前端监听到WebSocket的onclose()事件时,重新创建WebSocket连接。



    第一种方式会对服务器造成很大的压力,因为即使WebSocket连接正常,也要定时发送心跳包,从而消耗服务器资源。第二种方式虽然减轻了服务器的负担,但是在重连时可能会丢失一些数据。


    二、WebSocket心跳包机制


    WebSocket心跳包是WebSocket协议的保活机制,用于维持长连接。有效的心跳包可以防止长时间不通讯时,WebSocket自动断开连接。


    心跳包是指在一定时间间隔内,WebSocket发送的空数据包。常见的WebSocket心跳包机制如下:




    1. 客户端定时向服务器发送心跳数据包,以保持长连接。

    2. 服务器定时向客户端发送心跳数据包,以检测客户端连接是否正常。

    3. 双向发送心跳数据包。



    三、WebSocket心跳机制原理


    WebSocket心跳机制的原理是利用心跳包及时发送和接收数据,保证WebSocket长连接不被断开。WebSocket心跳机制的原理可以用下面的流程来说明:




    1. 客户端建立WebSocket连接。

    2. 客户端向服务器发送心跳数据包,服务器接收并返回一个表示接收到心跳数据包的响应。

    3. 当服务器没有及时接收到客户端发送的心跳数据包时,服务器会发送一个关闭连接的请求。

    4. 服务器定时向客户端发送心跳数据包,客户端接收并返回一个表示接收到心跳数据包的响应。

    5. 当客户端没有及时接收到服务器发送的心跳数据包时,客户端会重新连接WebSocket



    四、WebSocket心跳机制必要吗


    WebSocket心跳机制是必要的,它可以使 WebSocket 连接保持长连接,避免断开连接的情况发生。同时,心跳机制也可以检查WebSocket连接的状态,及时处理异常情况。


    五、WebSocket心跳机制作用


    WebSocket心跳机制的作用主要有以下几点:



    1. 保持WebSocket连接不被断开。

    2. 检测WebSocket连接状态,及时处理异常情况。

    3. 减少WebSocket连接及服务器资源的消耗。


    六、WebSocket重连机制


    WebSocket在发送和接收数据时,可能会因为网络原因、服务器宕机等因素而断开连接,此时需要使用WebSocket重连机制进行重新连接。


    WebSocket重连机制可以通过以下几种方式实现:




    1. 前端监听WebSocket的onclose()事件,重新创建WebSocket连接。

    2. 使用WebSocket插件或库,例如Sockjs、Stompjs等。

    3. 使用心跳机制检测WebSocket连接状态,自动重连。

    4. 使用断线重连插件或库,例如ReconnectingWebSocket等。



    七、WebSocket的缺点和不足


    WebSocket的缺点和不足主要有以下几点:




    1. WebSocket需要浏览器和服务器端都支持该协议。

    2. WebSocket会增加服务器的负担,不适合大规模连接的应用场景。



    八、关键代码


      // 开启心跳
    const start = () => {
    clearTimeout(timeoutObj);
    // serverTimeoutObj && clearTimeout(serverTimeoutObj);
    timeoutObj = setTimeout(function () {
    if (websocketRef.current?.readyState === 1) {
    //连接正常
    sendMessage('hello');
    }
    }, timeout);
    };
    const reset = () => {
    // 重置心跳 清除时间
    clearTimeout(timeoutObj);
    // 重启心跳
    start();
    };

    ws.onopen = (event) => {
    onOpenRef.current?.(event, ws);
    reconnectTimesRef.current = 0;
    start(); // 开启心跳
    setReadyState(ws.readyState || ReadyState.Open);
    };
    ws.onmessage = (message: WebSocketEventMap['message']) => {
    const { data } = message;

    if (data === '收到,hello') {
    reset();
    return;
    }
    if (JSON.parse(data).status === 408) {
    reconnect();
    return;
    }
    onMessageRef.current?.(message, ws);
    setLatestMessage(message);
    };
    const connect = () => {
    reconnectTimesRef.current = 0;
    connectWs();
    };

    主要思路:在建立长连接的时候开启心跳,通过和服务端发送信息,得到服务端给返回的信息,然后重置心跳,清楚时间,再重新开启心跳。如果网络断开的话,会执行方法,重新连接。


    作者:泽南Zn
    来源:juejin.cn/post/7290005438153867283
    收起阅读 »

    转转的Flutter实践之路

    前言 跨端技术一直是移动端开发领域的热门话题,Flutter 作为一种领先的移动跨端技术之一,凭借其快速的渲染引擎、丰富的UI组件库和强大的开发工具,成为了开发人员的首选之一。 从 Flutter 诞生之初,我们就一直关注着它的发展,Flutter 早期版本变...
    继续阅读 »

    前言


    跨端技术一直是移动端开发领域的热门话题,Flutter 作为一种领先的移动跨端技术之一,凭借其快速的渲染引擎、丰富的UI组件库和强大的开发工具,成为了开发人员的首选之一。


    从 Flutter 诞生之初,我们就一直关注着它的发展,Flutter 早期版本变更较为频繁,并且经常伴随着 Breaking Change,另外可用的三方插件较少且不稳定。直到2019年,Flutter 的热度暴涨,国内不少团队陆续把 Flutter 引入到了生产环境使用,社区也涌现出不少优秀的开源项目,我们也决定在这个时候做一些技术上的尝试。


    经过这几年在 Flutter 技术上的不断学习、探索和积累,Flutter 已经成为了客户端技术体系中的重要组成部分。


    回顾整个过程,我们大致经历了这么几个阶段:可行性验证、基建一期建设、小范围试验、基建二期建设、大范围推广、前端生态的探索,下文将分别对每个阶段展开进行介绍。


    可行性验证


    其实在这之前我们已经做过了一些调研,但许多结论都是来源于网上的一些文章或者其它团队的实践,这些结论是否靠谱是否真实还有待商榷,另外,网上的文章大都千篇一律,要么使劲吹捧,要么使劲贬低,要得出相对客观的结论还是得需要我们自己通过实践才能得出。


    目标


    我们确定了以下几个维度,用来评估 Flutter 是否值得我们进一步投入:



    • 开发效率

    • UI一致性

    • 性能体验

    • 学习成本

    • 发展趋势


    由于前期对 Flutter 的熟练度不高,基础设施也还没有搭建起来,所以在开发效率上,我们期望的 Flutter 的开发耗时能保持在原生开发耗时的 1.5 倍以内,不然虽然实现了跨端,但是需求的开发周期反而被拉长了,这样得不偿失。在UI一致性上,我们期望同一份代码在两端的表现要基本达到一致,不需要额外的适配成本。在性能方面,尽量保证崩溃、卡顿、内存、帧率这些指标在可控范围内。


    方案


    我们希望用较小的代价完成上述维度的评估,所以在试验期间的架构及基础设施方面我们做的比较简单。


    测试目标


    当时我们正在做一个叫切克的 App,用户量级比较小,工程架构也相对简单一些,正好可以用来做一些技术方面的探索和验证。


    我们选择的是切克的商品详情页,用 Flutter 技术实现了一个一模一样的商详,按1:1的流量分配给 Native 和 Flutter。


    项目架构


    由于我们的工程不是一个全新的项目,所以采用的是 Native 与 Flutter 混合开发的方式,Native 主工程只依赖 Flutter 产物即可,同时也尽量避免对原有工程的影响。


    关于混合页面栈的问题,我们没有额外处理,因为暂时只测试一个页面,不会涉及到多页面混合栈的问题,所以暂时先忽略。


    构建流程


    为了降低验证成本,我们没有对接现有的 Native 的持续集成流程,而是直接在本地构建 Flutter 产物,然后上传到远程仓库。


    结论


    经过一段时间的线上验证,我对 Flutter 技术基本有了一个比较全面的了解:


    在开发效率上由于基础库和基建的缺失,在处理 Flutter 业务跟 Native 业务的交互时需要更多的适配成本,包括像页面跳转、埋点上报、接口请求、图片加载等也需要额外的处理,但我们评估随着后续基建的不断完善,这部分的效率是可以逐步得到改善的;而在涉及UI开发方面,得益于热重载等技术,Flutter 的开发效率是要优于原生开发的。整体评估下来,在开发效率方面 Flutter 是符合我们的预期的。


    在UI一致性上,除了在状态栏控制和文本在某些情况下需要特殊适配下外,其它控件在两端的表现基本一致。


    在性能表现上,Flutter 会额外引入一些崩溃,内存占用也有所上涨,但还在可接受范围内。


    Flutter 的学习成本相对还是比较高,毕竟需要单独学习一门语言,另外 Flutter 的渲染原理也跟原生有很多差异,需要转变思维才能更快的适应,此外 Flutter 还提供了众多的 Widget 组件,也需要较长时间学习。


    在发展趋势上,Flutter 无疑是当时增长最快的跨端技术之一,社区的活跃程度以及官方的投入都非常高,国内不少团队也都在积极推进 Flutter 技术的发展,Flutter 正处在一个快速的上升期。


    整体来说,Flutter 是满足我们团队对跨平台技术的需求的,我们计划在接下来的一段时间投入更多资源,把 Flutter 的基础设施逐渐建立起来。


    基建一期建设


    基建一期内容主要包括以下几个方面:



    • 工程架构

    • 开发框架

    • 脚本工具

    • 自动化构建


    在基建一期完成后,我们的目标是要达到:



    • 基础能力足够支撑普通业务开发

    • 开发效率接近原生开发

    • 开发过程要基本顺畅


    工程架构


    工程架构指的是原生工程与 Flutter 工程之间的关系,以及 Flutter 工程与 Flutter 工程之间的关系。


    原生工程与Flutter工程的关系


    我们知道,使用 Flutter 开发通常有两种情况,一种是直接使用 Flutter 开发一个新的App,属于纯 Flutter 开发;一种是在已有的 Native 工程中引入,属于混合开发。我们当然属于后者。


    而混合开发又可分为两种:源码集成和产物集成。源码集成需要改变原工程的项目结构,并且需要 Flutter 开发环境才能编译,而产物集成则不需要改动原工程的项目结构,只需把 Flutter 的构建产物当作普通的依赖库引入即可,原有 Native 工程和 Flutter 工程从物理上完全独立。显而易见的我们选择产物集成的方式,引入 Flutter对于原工程以及非 Flutter 开发人员来说,基本上是毫无感知的。


    所以原生工程与 Flutter 工程之间的关系如下图所示:


    原生工程与Flutter工程之间的关系


    Flutter工程之间的关系


    根据已有的客户端基建的开发经验,我们将所有 Flutter 工程分为了四层:



    • 壳工程

    • 业务层

    • 公共层

    • 容器层


    容器层负责提供 Flutter 的基础运行环境,包括 Flutter 引擎管理、页面栈管理、网络框架、KV存储、数据库访问、埋点框架、Native 与 Flutter 通信通道和其它基础功能。


    公共层包含一些通用的开源库、自定义UI组件、部分通用业务等。


    业务层包含用户信息、商品、发布等业务组件。


    壳工程负责集成各业务组件,最终构建出产物集成到 Native 主工程。


    其中业务层、公共层、容器层都是由若干个独立的工程所组成,整体结构如下:


    Flutter分层架构


    开发框架


    开发框架是为了提高开发效率、规范代码结构、减少维护成本等考虑而设计的一套软件框架,包括:基础能力、状态管理、页面栈管理等。


    基础能力


    开发框架需要提供各种必要的能力,比如:页面跳转、埋点、网络请求、图片加载、数据存储等,为了最大化减少研发成本,我们在底层定义了一套通用的数据交互协议,直接复用了现有的 Native 的各项能力,也使得 Native 的各种状态与 Flutter 侧能够保持统一。


    状态管理


    相信了解 Flutter 的同学一定知道状态管理,这也是跟 Native 开发区别较大的地方。在开发较为复杂的页面时,状态维护是非常繁琐的,在不引入状态管理框架的情况下,开发效率会受很大影响,后期的维护成本以及业务交接都是很大的问题。


    另外,在开发框架设计之初,我们就期望从框架上能够在一定程度上限定代码结构、模块之间的交互方式、状态更新方式等,我们期望的是不同的人写出来的代码在逻辑、结构和风格上都能保持比较统一,即在提高开发效率的同时,也能保证项目后续的可维护性和扩展性,减少不同业务间的交接成本。


    基于上述这些需求,在我们对比了多个开源项目后,FishRedux 的整体使用感受正好符合我们的要求。


    如下图,两个页面的代码结构基本一致:


    收藏详情和个人主页


    页面栈管理


    在早期版本,Flutter 引擎的实例占用内存较高,为了减少内存消耗,大家普遍采用单实例的模式,而在 Native 和 Flutter 混合开发的场景下就会存在一个问题,就是 Native 有自己的页面栈,而 Flutter 也维护着一套自己的页面栈,如果 Native 页面与 Flutter 页面穿插着打开,在没有特殊处理的情况下,页面栈会发生错乱。在调研了业内的各种开源方案后,我们选择引入 FlutterBoost 用来管理页面混合栈。


    脚本工具


    为了方便开发同学搭建 Flutter 的开发环境,同时能够管理使用的 Flutter 版本,我们开发了 zflutter 命令行工具,包含以下主要功能:



    • Flutter开发环境安装

    • Flutter版本管理

    • 创建模版工程(主工程、组件工程)

    • 创建模版页面(常规页面、列表页、瀑布流页面)

    • 创建页面模块

    • 组件工程发布

    • 构建Flutter产物

    • 脚本自更新


    如图:


    zflutter


    自动化构建


    客户端使用的是自研的 Beetle 平台(集工程管理、分支管理、编译、发布于一体),短时间内要支持上 Flutter 不太现实,基于此,我们先临时自己搭台服务器,通过 gitlab 的 webhook 功能结合 zflutter 工具简单实现了一套自动化构建的服务,待 Beetle 支持 Flutter 组件化开发功能后,再将工作流切回到 Beetle 平台。


    小范围试验


    在完成基建一期的开发工作后,我们决定通过开发几个实际业务来试验目前的基础设施是否达到既定目标。


    我们以不影响主流程、能覆盖常见UI功能、并且能跟 Native 页面做AB测试(主要是方便在出问题时能够切换到 Native 版本)为条件挑选了个人资料页和留言列表页进行了 Flutter 化改造,如下图所示:


    个人资料页/留言列表页


    这两个页面涵盖了网络请求、图片加载、弹窗、列表、下拉刷新、上拉加载更多、左滑删除、埋点上报、页面跳转等常见功能,足以覆盖日常开发所需的基础能力。


    经过完整的开发流程以及一段时间的线上观察,我们得出如下结论:


    基础能力


    目前已具备的基础能力已经足够支撑普通业务开发(开发过程中补足了一些缺失的能力)。


    工作流


    整个开发过程在工程依赖管理和分支管理方面的支持还比较缺失,比较依赖人工处理。


    开发效率


    我们在开发前根据页面功能同时做了纯 Native 开发排期和 Flutter 开发排期,按单人日的成本来对比的话,Flutter 实际开发耗时跟 Native 排期耗时比为 1.25:2,Native 是按照 Android+iOS 两端各一人算的,也就是1.25人/日比2人/日,如果后续对 Flutter 技术熟悉度提升后相信效率还可以进一步提升。


    性能体验


    线上两个 Flutter 页面的体验效果跟 Native 对比基本感觉不到差别,但是首次进入 Flutter 页面时会有短暂的白屏等待时间,这个是由于 Flutter 环境初始化导致的延迟,后续可以想办法优化。


    包体积


    在引入 Flutter 之后,转转的安装包体积在两端都分别有所增加:



    • Android增加6.1M

    • iOS增加14M


    试验结果基本符合预期,包体积的增量也在我们的可接受范围内,接下来将进行基建二期的建设,补足目前缺失的能力。


    基建二期建设


    基建二期的内容主要包含以下工作:



    • 配合工程效率组完成 Beetle 对 Flutter 项目的支持

    • 组织客户端内部进行 Flutter 技术培训


    Beetle支持Flutter


    为了能让大家更清晰的了解 Beetle 的工程管理机制,这里先简单介绍下客户端的工程类型:



    • Native主工程(又分为 Android 和 iOS)

    • Native组件工程(又分为 Android 和 iOS)

    • Flutter主工程

    • Flutter组件工程(即 Flutter 插件工程)


    举个例子,当有一个新版本需要开发时,先从 Native 主工程创建一个版本同时创建一个 Release 分支,即版本分支,然后从版本分支根据具体需求创建对应 Native 组件的版本分支,Flutter 主工程此时可看作是一个 Native 组件,比如此时创建了一个 Flutter 主工程的版本分支后,可以进入 Flutter 主工程再根据需要创建对应的 Flutter 组件工程的版本分支。


    Beetle 目前已支持 Flutter 工程管理、分支管理、组件依赖管理以及组件的发布、Flutter 产物的构建等,Beetle 的作用贯穿从开发到上线的整个工作流。


    Flutter技术培训


    为了让大家更快的熟悉 Flutter 开发,我们在客户端内部组织了5次 Flutter 快速入门的系列分享:


    Flutter快速入门系列


    同时也逐步完善内部文档的建设,包括:FlutterSdk 源码维护策略、Flutter 入门指南、Flutter 混合开发方案、Flutter 与 Native 通信方案、Flutter 开发环境配置、Flutter 组件化工程结构、Flutter 开发与调试、Flutter 开发工作流、ZFlutter 工具使用介绍、Flutter 开发之 Beetle 使用指南等,涵盖了从环境搭建、开发调试到构建发布的整个过程。


    大范围推广


    在完成基建二期的建设后,整体基础设施已经能够支撑我们常见的业务,开发工作流也基本顺畅,于是我们开始了在内部大范围推广计划。


    我们先后改造和新开发了个人主页、我发布的页面、微商详、奇趣数码页等业务,基本涵盖了常见的各种类型的页面和功能,整体开发效率与原生单端开发效率持平,但是在特别复杂的页面的性能表现上,Flutter 的表现相对要差一些。


    部分页面如下图所示:


    个人主页


    微详情页/我发布的/奇趣数码


    探索前端生态


    在跨端技术领域我们知道 Web 技术是天然支持的,如果能把前端生态引入到 Flutter 中,那么对客户端来说,在业务的支持度上会更上一个台阶,Web 的体验得到提升的同时客户端也具备了动态化,基于此背景我们开始探索 Flutter 在 Web 上的可能性。


    技术调研


    当时可选的开源方案有:Kraken、MXFlutter、Flutter For Web。


    Kraken


    Kraken 是一款基于 W3C 标准的高性能渲染引擎。Kraken 底层基于 Flutter 进行渲染,通过其自绘渲染的特性,保证多端一致性。上层基于 W3C 标准实现,拥有非常庞大的前端开发者生态。


    Kraken 的最上层是一个基于 W3C 标准而构建的 DOM API,在下层是所依赖的 JS 引擎,通过 C++ 构建一个 Bridge 与 Dart 通信。然后这个 C++ Bridge 把 JS 所调用的一些信息,转发到 Dart 层。Dart 层通过接收这些信息,会去调用 Flutter 所提供的一些渲染能力来进行渲染。


    Kraken 是不依赖 Flutter Widget,而是依赖 Flutter Widget 的底层渲染数据结构 —— RenderObject。Kraken 实现了很多 CSS 相关的能力和一些自定义的 RenderObject,直接将生成的 RenderObject 挂载在 Flutter RenderView 上来进行渲染,通过这样的方式能够做到非常高效的渲染性能。


    MXFlutter


    MXFlutter 是一套使用 TypeScript/JavaScript 来开发 Flutter 应用的框架。


    MXFlutter 把 Flutter 的渲染逻辑中的三棵树(即:WidgetTree、Element、RenderObject )中的第一棵(即:WidgetTree),放到 JavaScript 中生成。用 JavaScript 完整实现了 Flutter 控件层封装,实现了轻量的响应式 UI 框架,支撑JS WidgetTree 的 build逻辑,build 过程生成的UI描述, 通过Flutter 层的 UI 引擎转换成真正的 Flutter 控件显示出来。


    Flutter For Web


    Flutter 在 Web 平台上以浏览器的标准 API 重新实现了引擎。目前有两种在 Web 上呈现内容的选项:HTML 和 WebGL。



    • 在 HTML 模式下,Flutter 使用 HTML、CSS、Canvas 和 SVG 进行渲染。

    • 在 WebGL 模式下,Flutter 使用了一个编译为 WebAssembly 的 Skia 版本,名为 CanvasKit。


    HTML 模式提供了最佳的代码大小,CanvasKit 则提供了浏览器图形堆栈渲染的最快途径,并为原生平台的内容提供了更高的图形保真度。


    结论


    我们对以上方案从接入成本、渲染性能、包体积、开发生态、学习成本等多维度进行了对比:



    • 接入成本:Kraken ≈ MXFlutter ≈ Flutter For Web

    • 渲染性能:Kraken > MXFlutter > Flutter For Web

    • 包体积增量:Flutter For Web < Kraken < MXFlutter

    • 开发生态:Kraken ≈ MXFlutter > Flutter For Web

    • 学习成本:Flutter For Web < Kraken ≈ MXFlutter


    最终选择了 Kraken 作为我们的首选方案。


    上线验证


    为了使 Kraken 顺利接入转转App,我们做了以下几个方面的工作:



    • 升级 FlutterSdk 到最新版,满足接入 Kraken 的基础条件

    • 统一客户端容器接口,使得 Kraken 容器能够完美继承 Web 容器的能力

    • 自己维护 Kraken 源码,及时修复官方来不及修复的问题,方便增加转转特有的扩展能力

    • 制定 Kraken 容器与 Web 容器的降级机制

    • 兼容 HTML 加载,保持跟 Web 容器一致的加载方式

    • 添加监控埋点,量化指标,指导后续优化方向

    • 选择一个简单 Web 页并协助前端同学适配


    上线后,我们对页面的各项指标进行了对比,使用 Kraken 容器加载比使用 WebView 加载,在首屏加载耗时的指标上平均增加了281毫秒,原因为:当前版本的 Kraken 容器不支持直接加载 HTML,且只能加载单个 JsBundle,导致加载效率比 WebView 差。


    通过跟前端同学沟通,从开发效率上来看,Kraken 工程的开发周期会比实现同样需求的普通 Web 工程增加1.5到2倍的时间,主要原因是受到 CSS 样式、Api 差异,无法使用现有UI组件,另外 Kraken 的调试工具目前还不够完善,使用浏览器调试后还须在客户端容器中调试,整体下来导致开发 Kraken 工程会比开发普通Web工程耗费更多时间。


    再次验证


    由于之前选择的 Web 页面太过简单,不具备代表性,所以我们重新选定了“附近的人”页面做为改造目标,再次验证 Kraken 在实际开发过程中的效率及性能体验。页面如图所示:


    附近的人


    最终因为部分问题得不到解决,并且整体性能较差,导致页面没能成功上线。


    存在的问题包括但不限于下面列举的一些:



    • 表现不一致问题

      1. CSS 定位、布局表现与浏览器表现不一致

      2. 部分 API 表现与浏览器不一致(getBoundingClientRect等)

      3. iOS,Android系统表现不一致



    • 重大 Bug

      1. 页面初始化渲染完成,动态修改元素样式,DOM不重新渲染

      2. 滑动监听计算导致 APP 崩溃



    • 调试成本高

      1. 不支持 vue-router,单项目单路由

      2. 不支持热更新,npm run build 预览

      3. 不支持 sourceMap,无法定位源代码

      4. 真机调试只支持 element 和 network;dom 和 element 无法互相选中;无法动态修改 dom 结构,无法直接修改样式.......

      5. 页面白屏,假死



    • 安全性问题

      1. 无浏览器中的“同源策略”限制



    • 兼容性

      1. npm 包不兼容等




    通过这一系列的探索和尝试,我们了解到了 Kraken 目前还存在许多不足,如果继续应用会带来高额的开发调试以及维护成本,所以暂时停止了在 Kraken 方向上的投入,但我们仍然在这个方向上保持着关注。


    结尾


    目前转转在Flutter方向上的实践和探索只是一个起点,我们意识到仍然有很多工作需要去做。我们坚信Flutter作为一项领先的跨端技术,将为转转业务的发展带来巨大的潜力和机会。我们将持续努力,加强技术建设,不断完善实践经验,推动Flutter在转转的应用和发展,为用户提供更好的产品和体验。


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

    Android 绘制你最爱的马赛克

    前言 我们之前写过《Android 实现LED 展示效果》,在这篇文章中,我们使用了图像分块(或者是分片)的算法,这样做的目的是降低像素扫描的时间复杂度,并且也利于均色采样。其实图像分块是很常见的图像动效处理手段,一般主要用于场景变换,这项技术也和图像光栅化类...
    继续阅读 »

    前言


    我们之前写过《Android 实现LED 展示效果》,在这篇文章中,我们使用了图像分块(或者是分片)的算法,这样做的目的是降低像素扫描的时间复杂度,并且也利于均色采样。其实图像分块是很常见的图像动效处理手段,一般主要用于场景变换,这项技术也和图像光栅化类似。


    什么是光栅化


    光栅化渲染(Rasterized Rendering)直译过来是栅格化渲染。寻找图像中被几何图形占据的所有像素的过程称为栅格化,因此对象顺序渲染(Object-order rendering)也可以称为栅格化渲染。


    Qu-es-la-rasterizacin-y-cual-es-su-diferencia-con-el-Ray-Tracing.jpg


    我们今天的所要用到的技术也是栅格化和像素采样技术。


    LED原理简述


    马赛克是一种图像编辑技术,广泛应用于隐私保护和涂鸦渲染,很多手机系统自带了这种效果,那如何才能实现这种技术呢?


    了解过我之前的文章的知道,我们制作LED有几个特征



    • 每个LED单元要么亮要么不亮

    • 每个LED单元只有一种颜色

    • 每个LED单元和其他LED单元存在一定的间距

    • 所有LED的单元成网格排列

    • 每个LED单元大小一致


    以上相当于顶点坐标信息,我们拿到网格的位置,就能拿到LED整个区域的片段,知道这个区域的片段我们就可以修改其像素。


    着色采样:


    即便是每个矩形区域,也有很多像素点,如果每个矩形区域每个像素都要进行均色计算的话,那10x10的也要100此,因此为了更快的效率,需要对LED 范围内的像素点采样,求出颜色均值,均值色就是LED最终展示的颜色。


    避坑——修改像素


    上一篇我们知道,通过Bitmap.setPixel方法修改像素效率是极低的,我曾经写过一篇通过修改像素生成圆形图片的文章,在那篇文章里我们看到,像素本身也是有size的,导致最终的圆形图片存在大量锯齿,主要原因是通过这种方式没法做到双线性过滤(图片放大之后会对边缘优化),还有另一个问题,就是效率极差。
    总结一下修改像素的问题:



    • 无法抗锯齿

    • 效率低


    避坑——透明色


    像素中往往存在 color为0或者alpha通道为0的情况,甚至有的区域因为采样原因导致清晰度急剧下降,甚至出现了透明区域噪点,这些问题主要来自于alpha 通道引发的颜色稀释问题,因此在采样时一定要规避这两种情况,至于会不会失真?答案是如果采用alpha失真只会更严重。


    清晰度问题


    同样,清晰度也容易受到这olor为0或者alpha通道为0的情况情况干扰,除了这两种就是采样区域的大小了,理论上采样网格密度越密,清晰度越高,越接近原始图片,因此一定要权衡,太清晰不就很原图一样了么,还制作什么LED呢?


    马赛克原理


    实际上,马赛克原理和LED展示方式类似,为什么这么说呢?从特征来看,几乎一样,马赛克和LED效果只在两部分存在区别



    1. 马赛克网格之间不存在间距

    2. 马赛克采样次数比LED要少


    马赛克没有LED间距很好理解,至于次数少的好处第一肯定是效率高,其次是采样太多容易接近原色,而LED是要有一定程度接近原色。


    技术实现


    本篇我们邀请一位可爱的猫猫,老师们太耀眼的图片就算了,不利于大家阅读。


    ic_cat.png


    我们接下来的任务是把给猫脸打上马赛克,了解完这项技术实现后,其实你不仅可以给猫脸打马赛克,自行涂鸦,指哪儿打哪儿。


    基本信息


    private Bitmap mBitmap; //猫图
    private float blockWidth = 30; //30x30的像素快
    private RectF blockRect = new RectF(); //猫头区域
    private RectF gridRect = new RectF(); //网格区域

    Canvas 包裹Bitmap


    主要方便绘制和内存回收


    static class BitmapCanvas extends Canvas {
    Bitmap bitmap;
    public BitmapCanvas(Bitmap bitmap) {
    super(bitmap);
    this.bitmap = bitmap;
    }
    public Bitmap getBitmap() {
    return bitmap;
    }
    }


    定位猫头位置


    由于时间关系,我没有做TOUCH事件处理,就写了这个猫头区域


    / 这个区域到时候可以自己调制,想让什么部位有马赛克那就会有马赛克
    blockRect.set(bitmapCanvas.bitmap.getWidth() - 400, 0, bitmapCanvas.bitmap.getWidth() - 100, 300);

    网格分割


    //根据平分猫头矩形区域
    int col = (int) (blockRect.width() / blockWidth);
    int row = (int) (blockRect.height() / blockWidth);

    网格定位


    float startX = blockRect.left;
    float startY = blockRect.top;
    for (int i = 0; i < row * col; i++) {
    int x = i % col;
    int y = (i / col);
    gridRect.set(startX + x * blockWidth, startY + y * blockWidth, startX + x * blockWidth + blockWidth, startY + y * blockWidth + blockWidth);

    }

    采样和着色


    float startX = blockRect.left;
    float startY = blockRect.top;
    for (int i = 0; i < row * col; i++) {
    int x = i % col;
    int y = (i / col);
    gridRect.set(startX + x * blockWidth, startY + y * blockWidth, startX + x * blockWidth + blockWidth, startY + y * blockWidth + blockWidth);
    //采样
    int sampleColor = mBitmap.getPixel((int) gridRect.centerX(), (int) gridRect.centerY());
    mCommonPaint.setColor(sampleColor);
    //着色,我们这里不修改像素,而是drawRect,避免性能问题和锯齿问题
    bitmapCanvas.drawRect(gridRect, mCommonPaint);
    }

    渲染到View上


    canvas.drawBitmap(bitmapCanvas.bitmap, null, mainRect, mCommonPaint);

    效果预览


    fire_56.gif


    避坑点


    网格区间不易过小,和LED一样,越小清晰度越高,就会失去了处理的意义。


    全部代码


    public class MosaicView extends View {
    private final DisplayMetrics mDM;
    private TextPaint mCommonPaint;
    private RectF mainRect = new RectF();

    private BitmapCanvas bitmapCanvas; //Canvas 封装的
    private Bitmap mBitmap; //猫图
    private float blockWidth = 30; //30x30的像素快
    private RectF blockRect = new RectF(); //猫头区域
    private RectF gridRect = new RectF(); //网格区域
    private boolean showMask = false;

    public MosaicView(Context context) {
    this(context, null);
    }
    public MosaicView(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
    }
    public MosaicView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    mDM = getResources().getDisplayMetrics();
    initPaint();
    }

    private void initPaint() {
    //否则提供给外部纹理绘制
    mCommonPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
    mCommonPaint.setAntiAlias(true);
    mCommonPaint.setStyle(Paint.Style.FILL_AND_STROKE);
    mCommonPaint.setStrokeCap(Paint.Cap.ROUND);
    mBitmap = decodeBitmap(R.mipmap.ic_cat);

    }
    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);
    mBitmap = decodeBitmap(R.mipmap.ic_cat);

    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
    if (bitmapCanvas != null && bitmapCanvas.bitmap != null && !bitmapCanvas.bitmap.isRecycled()) {
    bitmapCanvas.bitmap.recycle();
    }
    bitmapCanvas = null;

    }

    @Override
    protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    int width = getWidth();
    int height = getHeight();
    if (width < 1 || height < 1) {
    return;
    }
    if (bitmapCanvas == null || bitmapCanvas.bitmap == null) {
    bitmapCanvas = new BitmapCanvas(Bitmap.createBitmap(mBitmap.getWidth(), mBitmap.getHeight(), Bitmap.Config.ARGB_8888));
    } else {
    bitmapCanvas.bitmap.eraseColor(Color.TRANSPARENT);
    }
    float radius = Math.min(width / 2f, height / 2f);

    //关闭双线性过滤
    // int flags = mCommonPaint.getFlags();
    // mCommonPaint.setFlags(flags &~ Paint.FILTER_BITMAP_FLAG);
    // mCommonPaint.setFilterBitmap(false);

    int save = bitmapCanvas.save();
    bitmapCanvas.drawBitmap(mBitmap, 0, 0, mCommonPaint);


    // 这个区域到时候可以自己调制,想让什么部位有马赛克那就会有马赛克
    blockRect.set(bitmapCanvas.bitmap.getWidth() - 560, 10, bitmapCanvas.bitmap.getWidth() - 100, 410);

    if(showMask) {
    //根据平分猫头矩形区域
    int col = (int) (blockRect.width() / blockWidth);
    int row = (int) (blockRect.height() / blockWidth);

    float startX = blockRect.left;
    float startY = blockRect.top;

    for (int i = 0; i < row * col; i++) {
    int x = i % col;
    int y = (i / col);
    gridRect.set(startX + x * blockWidth, startY + y * blockWidth, startX + x * blockWidth + blockWidth, startY + y * blockWidth + blockWidth);
    //采样
    int sampleColor = mBitmap.getPixel((int) gridRect.centerX(), (int) gridRect.centerY());
    mCommonPaint.setColor(sampleColor);
    //着色,我们这里不修改像素,而是drawRect,避免性能问题和锯齿问题
    bitmapCanvas.drawRect(gridRect, mCommonPaint);
    }
    }else{
    Paint.Style style = mCommonPaint.getStyle();
    mCommonPaint.setStyle(Paint.Style.STROKE);
    mCommonPaint.setColor(Color.MAGENTA);
    mCommonPaint.setStrokeWidth(8);
    bitmapCanvas.drawRect(blockRect, mCommonPaint);
    mCommonPaint.setStyle(style);

    }

    bitmapCanvas.restoreToCount(save);
    int saveCount = canvas.save();
    canvas.translate(width / 2f, height / 2f);
    mainRect.set(-radius, -radius, radius, radius);
    canvas.drawBitmap(bitmapCanvas.bitmap, null, mainRect, mCommonPaint);
    canvas.restoreToCount(saveCount);

    }

    public void openMask() {
    showMask = true;
    postInvalidate();
    }

    public void closeMask() {
    showMask = false;
    postInvalidate();

    }

    static class BitmapCanvas extends Canvas {
    Bitmap bitmap;
    public BitmapCanvas(Bitmap bitmap) {
    super(bitmap);
    //继承在Canvas的绘制是软绘制,因此理论上可以绘制出阴影
    this.bitmap = bitmap;
    }
    public Bitmap getBitmap() {
    return bitmap;
    }
    }
    }

    总结


    实际上还有另一种方法,我们绘制图片时关闭双线性过滤


    //关闭双线性过滤
    // int flags = mCommonPaint.getFlags();
    // mCommonPaint.setFlags(flags &~ Paint.FILTER_BITMAP_FLAG);
    // mCommonPaint.setFilterBitmap(false);

    然后将图片放到很大,这个时候你的图片就会产生一定的网格区域,截图然后进行一系列矩阵转换,最后把图贴到原处就出现了马赛克,但是这个有个问题,超高像素的图片得先缩小,然后再放大,显然处理步骤比较多。


    下图是先缩小20倍然后画到原来大小的效果

    企业微信20231205-221500@2x.png


    实现代码


    本来不打算放代码的,想想还是放上吧


    public class BitmapMosaicView extends View {
    private final DisplayMetrics mDM;
    private TextPaint mCommonPaint;
    private RectF mainRect = new RectF();
    private BitmapCanvas bitmapCanvas; //Canvas 封装的
    private BitmapCanvas srcThumbCanvas; //Canvas 封装的
    private Bitmap mBitmap; //猫图
    private RectF blockRect = new RectF(); //猫头区域

    public BitmapMosaicView(Context context) {
    this(context, null);
    }
    public BitmapMosaicView(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
    }
    public BitmapMosaicView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    mDM = getResources().getDisplayMetrics();
    initPaint();
    }

    private void initPaint() {
    //否则提供给外部纹理绘制
    mCommonPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
    mCommonPaint.setAntiAlias(true);
    mCommonPaint.setStyle(Paint.Style.FILL_AND_STROKE);
    mCommonPaint.setStrokeCap(Paint.Cap.ROUND);
    mBitmap = decodeBitmap(R.mipmap.ic_cat);

    }
    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 onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
    if (bitmapCanvas != null && bitmapCanvas.bitmap != null && !bitmapCanvas.bitmap.isRecycled()) {
    bitmapCanvas.bitmap.recycle();
    }
    if (srcThumbCanvas != null && srcThumbCanvas.bitmap != null && !srcThumbCanvas.bitmap.isRecycled()) {
    srcThumbCanvas.bitmap.recycle();
    }
    bitmapCanvas = null;

    }

    private Rect srcRectF = new Rect();
    private Rect dstRectF = new Rect();

    @Override
    protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    int width = getWidth();
    int height = getHeight();
    if (width < 1 || height < 1) {
    return;
    }
    if (bitmapCanvas == null || bitmapCanvas.bitmap == null) {
    bitmapCanvas = new BitmapCanvas(Bitmap.createBitmap(mBitmap.getWidth(), mBitmap.getHeight(), Bitmap.Config.ARGB_8888));
    } else {
    bitmapCanvas.bitmap.eraseColor(Color.TRANSPARENT);
    }
    if (srcThumbCanvas == null || srcThumbCanvas.bitmap == null) {
    srcThumbCanvas = new BitmapCanvas(Bitmap.createBitmap(mBitmap.getWidth()/35, mBitmap.getHeight()/35, Bitmap.Config.ARGB_8888));
    } else {
    srcThumbCanvas.bitmap.eraseColor(Color.TRANSPARENT);
    }
    float radius = Math.min(width / 2f, height / 2f);

    //关闭双线性过滤
    int flags = mCommonPaint.getFlags();
    mCommonPaint.setFlags(flags &~ Paint.FILTER_BITMAP_FLAG);
    mCommonPaint.setFilterBitmap(false);
    mCommonPaint.setDither(false);


    srcRectF.set(0,0,mBitmap.getWidth(),mBitmap.getHeight());
    dstRectF.set(0,0, srcThumbCanvas.bitmap.getWidth(), srcThumbCanvas.bitmap.getHeight());

    int save = bitmapCanvas.save();
    srcThumbCanvas.drawBitmap(mBitmap, srcRectF, dstRectF, mCommonPaint);

    srcRectF.set(dstRectF);
    dstRectF.set(0,0,bitmapCanvas.bitmap.getWidth(),bitmapCanvas.bitmap.getHeight());
    bitmapCanvas.drawBitmap(srcThumbCanvas.bitmap, srcRectF,dstRectF, mCommonPaint);
    // 这个区域到时候可以自己调制,想让什么部位有马赛克那就会有马赛克
    blockRect.set(bitmapCanvas.bitmap.getWidth() - 560, 10, bitmapCanvas.bitmap.getWidth() - 100, 410);
    bitmapCanvas.restoreToCount(save);
    int saveCount = canvas.save();
    canvas.translate(width / 2f, height / 2f);
    mainRect.set(-radius, -radius, radius, radius);
    canvas.drawBitmap(bitmapCanvas.bitmap, null, mainRect, mCommonPaint);
    canvas.restoreToCount(saveCount);

    }
    static class BitmapCanvas extends Canvas {
    Bitmap bitmap;
    public BitmapCanvas(Bitmap bitmap) {
    super(bitmap);
    //继承在Canvas的绘制是软绘制,因此理论上可以绘制出阴影
    this.bitmap = bitmap;
    }
    public Bitmap getBitmap() {
    return bitmap;
    }
    }
    }

    总结下本文分享技术特点:

    • 网格化
    • 采样
    • canvas着色,不要去修改像素


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

    Android 实现LED 展示效果

    一、前言 LED以其卓越的亮度和醒目的文字和图案,已成为车水马龙的城市中充满烟火气息的象征,深层次的是您红灯的闪烁唤醒着人们的娱乐、怀旧、童年的记忆。当然对新时代来说这显然格格不入的,因此这种霓虹灯能存在多久显然还是个问题。 效果预览 二、实现原理 最初的设...
    继续阅读 »

    一、前言


    LED以其卓越的亮度和醒目的文字和图案,已成为车水马龙的城市中充满烟火气息的象征,深层次的是您红灯的闪烁唤醒着人们的娱乐、怀旧、童年的记忆。当然对新时代来说这显然格格不入的,因此这种霓虹灯能存在多久显然还是个问题。


    效果预览



    二、实现原理


    最初的设想是利用BitmapShader  + Shader 实现网格图片,但是最终是失败的,因此绘制出的网格不是纯色。


    为什么是需要网格纯色呢 ,主要原因是LED等作为单独的实体,单个LED智能发出一种光,电视也是一样的道理,微小的发光单元不可能同时发出多种光源,这也是LED显示屏的制作原理。至于我们的自定义View,本身是细腻的屏幕上发出的,如果一个LED发出多种光,就会显得很假。但事实上,在绘制View时一个区域可能会出现多种颜色,如何平衡这种颜色也是个问题,优化方式当然是增加采样点;但是采样点多了也会带来新的副作用,一是性能问题,而是过多的全透明和alpha为0的情况,因为这种情况会过度稀释真是的颜色,造成模糊不清的问题,其次和View本身的背景穿透,形成较大范围的噪点,所以绘制过程中一定要控制采样点的数量,其次对alpha为0或者过小的的情况剔除,当然不用担心失真,因为过度的透明人眼会认为是全透明,没有太多意义,我们来做个总结:



    • LED 单元智能发出一种光,因此不适合BitampShader做风格渲染

    • 颜色逼真程度和采样点有关,采样点越多越逼近真色

    • 清晰程度和LED单元大小相关,LED单元越小越清晰

    • 剔除alpha通道过小和颜色值为0的采样点颜色 


    三、核心逻辑


    生成刷子纹理


         if (brushBitmap == null) {
    brushBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
    brushCanvas = new Canvas(brushBitmap);
    }

    for (int i = 0; i < drawers.size(); i++) {
    int saveCount = brushCanvas.save();
    drawers.get(i).draw(brushCanvas, width, height, mCommonPaint);
    brushCanvas.restoreToCount(saveCount);
    }

    生成网格数据


            float blockWidth = (squareWidth + padding);
    int w = width;
    int h = height;
    int columNum = (int) Math.ceil(w / blockWidth);
    int rowNum = (int) Math.ceil(h / blockWidth);

    if (gridRects.isEmpty() && squareWidth > 1f) {
    //通过rowNum * columNum方式降低时间复杂度
    for (int i = 0; i < rowNum * columNum; i++) {

    int col = i % columNum;
    int row = (i / columNum);

    Rect rect = new Rect();
    rect.left = (int) (col * blockWidth);
    rect.top = (int) (row * blockWidth);
    rect.right = (int) (col * blockWidth + squareWidth);
    rect.bottom = (int) (row * blockWidth + squareWidth);
    //记录网格点
    gridRects.add(rect);
    }

    }

    采样绘制


        //这里是重点 ,LED等可以看作一只灯泡,灯泡区域要么全亮,要么全不亮
    for (int i = 0; i < gridRects.size(); i++) {
    Rect rect = gridRects.get(i);

    if (brushBitmap.getWidth() <= rect.right) {
    continue;
    }
    if (brushBitmap.getHeight() <= rect.bottom) {
    continue;
    }

    if (sampleColors == null) {
    sampleColors = new int[9];
    }

    //取7个点采样,纯粹是为了性能考虑,如果想要更准确的颜色,可以多采样几个点

    sampleColors[0] = brushBitmap.getPixel(rect.left, rect.top); // left-top
    sampleColors[1] = brushBitmap.getPixel(rect.right, rect.top); // right-top
    sampleColors[2] = brushBitmap.getPixel(rect.right, rect.bottom); // right-bottom
    sampleColors[3] = brushBitmap.getPixel(rect.left, rect.bottom); // left-bottom
    sampleColors[4] = brushBitmap.getPixel(rect.left + rect.width() / 2, rect.top + rect.height() / 2); //center

    sampleColors[5] = brushBitmap.getPixel(rect.left + rect.width() / 2, rect.top + rect.height() / 4); //top line
    sampleColors[6] = brushBitmap.getPixel(rect.left + rect.width() * 3 / 4, rect.top + rect.height() / 2); //right line
    sampleColors[7] = brushBitmap.getPixel(rect.left + rect.width() / 4, rect.top + rect.height() / 2); // left line
    sampleColors[8] = brushBitmap.getPixel(rect.left + rect.width() / 2, rect.top + rect.height() * 3 / 4); // bottom line

    int alpha = 0;
    int red = 0;
    int green = 0;
    int blue = 0;
    int num = 0;

    for (int c : sampleColors) {
    if (c == Color.TRANSPARENT) {
    //剔除全透明的颜色,必须剔除
    continue;
    }
    int alphaC = Color.alpha(c);
    if (alphaC <= 0) {
    //剔除alpha为0的颜色,当然可以改大一点,防止降低清晰度
    continue; }
    alpha += alphaC;
    red += Color.red(c);
    green += Color.green(c);
    blue += Color.blue(c);
    num++;
    }

    if (num < 1) {
    continue;
    }

    //求出平均值
    int rectColor = Color.argb(alpha / num, red / num, green / num, blue / num);
    if (rectColor != Color.TRANSPARENT) {
    mGridPaint.setColor(rectColor);
    // canvas.drawRect(rect, mGridPaint); //绘制矩形
    canvas.drawCircle(rect.centerX(), rect.centerY(), squareWidth / 2, mGridPaint); //绘制圆
    }
    }

    如果不剔除颜色,那么就会有噪点和清晰度问题



    全部代码


    public class LedDisplayView extends View {
    private final DisplayMetrics mDM;
    private TextPaint mGridPaint;
    private TextPaint mCommonPaint;
    private List<IDrawer> drawers = new ArrayList<>();
    private Bitmap brushBitmap = null;
    private float padding = 2; //分界线大小
    private float squareWidth = 5; //网格大小
    private List<Rect> gridRects = new ArrayList<>();
    int[] sampleColors = null;
    private Canvas brushCanvas = null;

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

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

    public LedDisplayView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    mDM = getResources().getDisplayMetrics();
    initPaint();

    }


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, 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);
    }


    public float dp2px(float dp) {
    return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, mDM);
    }

    public float sp2px(float dp) {
    return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, dp, mDM);
    }


    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
    if (brushBitmap != null && !brushBitmap.isRecycled()) {
    brushBitmap.recycle();
    }
    brushBitmap = null;
    }


    @Override
    protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    int width = getWidth();
    int height = getHeight();
    if (width <= padding || height <= padding) {
    return;
    }

    if (brushBitmap == null) {
    brushBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
    brushCanvas = new Canvas(brushBitmap);
    }

    for (int i = 0; i < drawers.size(); i++) {
    int saveCount = brushCanvas.save();
    drawers.get(i).draw(brushCanvas, width, height, mCommonPaint);
    brushCanvas.restoreToCount(saveCount);
    }


    float blockWidth = (squareWidth + padding);
    int w = width;
    int h = height;
    int columNum = (int) Math.ceil(w / blockWidth);
    int rowNum = (int) Math.ceil(h / blockWidth);

    if (gridRects.isEmpty() && squareWidth > 1f) {
    //通过rowNum * columNum方式降低时间复杂度
    for (int i = 0; i < rowNum * columNum; i++) {

    int col = i % columNum;
    int row = (i / columNum);

    Rect rect = new Rect();
    rect.left = (int) (col * blockWidth);
    rect.top = (int) (row * blockWidth);
    rect.right = (int) (col * blockWidth + squareWidth);
    rect.bottom = (int) (row * blockWidth + squareWidth);
    //记录网格点
    gridRects.add(rect);
    }

    }
    int color = mGridPaint.getColor();

    //这里是重点 ,LED等可以看作一只灯泡,灯泡区域要么全亮,要们全不亮
    for (int i = 0; i < gridRects.size(); i++) {
    Rect rect = gridRects.get(i);

    if (brushBitmap.getWidth() <= rect.right) {
    continue;
    }
    if (brushBitmap.getHeight() <= rect.bottom) {
    continue;
    }

    if (sampleColors == null) {
    sampleColors = new int[9];
    }

    //取7个点采样,纯粹是为了性能考虑,如果想要更准确的颜色,可以多采样几个点

    sampleColors[0] = brushBitmap.getPixel(rect.left, rect.top); // left-top
    sampleColors[1] = brushBitmap.getPixel(rect.right, rect.top); // right-top
    sampleColors[2] = brushBitmap.getPixel(rect.right, rect.bottom); // right-bottom
    sampleColors[3] = brushBitmap.getPixel(rect.left, rect.bottom); // left-bottom
    sampleColors[4] = brushBitmap.getPixel(rect.left + rect.width() / 2, rect.top + rect.height() / 2); //center

    sampleColors[5] = brushBitmap.getPixel(rect.left + rect.width() / 2, rect.top + rect.height() / 4); //top line
    sampleColors[6] = brushBitmap.getPixel(rect.left + rect.width() * 3 / 4, rect.top + rect.height() / 2); //right line
    sampleColors[7] = brushBitmap.getPixel(rect.left + rect.width() / 4, rect.top + rect.height() / 2); // left line
    sampleColors[8] = brushBitmap.getPixel(rect.left + rect.width() / 2, rect.top + rect.height() * 3 / 4); // bottom line

    int alpha = 0;
    int red = 0;
    int green = 0;
    int blue = 0;
    int num = 0;

    for (int c : sampleColors) {
    if (c == Color.TRANSPARENT) {
    //剔除全透明的颜色,必须剔除
    continue;
    }
    int alphaC = Color.alpha(c);
    if (alphaC <= 0) {
    //剔除alpha为0的颜色,当然可以改大一点,防止降低清晰度
    continue;
    }
    alpha += alphaC;
    red += Color.red(c);
    green += Color.green(c);
    blue += Color.blue(c);
    num++;
    }

    if (num < 1) {
    continue;
    }

    //求出平均值
    int rectColor = Color.argb(alpha / num, red / num, green / num, blue / num);
    if (rectColor != Color.TRANSPARENT) {
    mGridPaint.setColor(rectColor);
    // canvas.drawRect(rect, mGridPaint); //绘制矩形
    canvas.drawCircle(rect.centerX(), rect.centerY(), squareWidth / 2, mGridPaint); //绘制圆
    }
    }
    mGridPaint.setColor(color);

    }


    private void initPaint() {
    // 实例化画笔并打开抗锯齿
    mGridPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
    mGridPaint.setAntiAlias(true);
    mGridPaint.setColor(Color.LTGRAY);
    mGridPaint.setStyle(Paint.Style.FILL);
    mGridPaint.setStrokeCap(Paint.Cap.ROUND); //否则网格绘制

    //否则提供给外部纹理绘制
    mCommonPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
    mCommonPaint.setAntiAlias(true);
    mCommonPaint.setStyle(Paint.Style.FILL_AND_STROKE);
    mCommonPaint.setStrokeCap(Paint.Cap.ROUND);

    }

    public void addDrawer(IDrawer drawer) {
    if (drawer == null) return;
    this.drawers.add(drawer);
    gridRects.clear();
    postInvalidate();
    }

    public void removeDrawer(IDrawer drawer) {
    if (drawer == null) return;
    this.drawers.remove(drawer);
    gridRects.clear();
    postInvalidate();
    }

    public void clearDrawer() {
    this.drawers.clear();
    gridRects.clear();
    postInvalidate();
    }

    public List<IDrawer> getDrawers() {
    return new ArrayList<>(drawers);
    }

    public interface IDrawer {
    void draw(Canvas canvas, int width, int height, Paint paint);
    }

    }

    使用方式


           LedDisplayView displayView = findViewById(R.id.ledview);
    final BitmapDrawable bitmapDrawable1 = (BitmapDrawable)getResources().getDrawable(R.mipmap.mm_07);
    final BitmapDrawable bitmapDrawable2 = (BitmapDrawable)getResources().getDrawable(R.mipmap.mm_08);
    ledDisplayView.addDrawer(new LedDisplayView.IDrawer() {

    Matrix matrix = new Matrix();
    @Override
    public void draw(Canvas canvas, int width, int height, Paint paint) {
    canvas.translate(width/2,height/2);
    matrix.preTranslate(-width/2,-height/4);
    Bitmap bitmap1 = bitmapDrawable1.getBitmap();
    canvas.drawBitmap(bitmap1,matrix,paint);

    matrix.postTranslate(width/2,height/4);
    Bitmap bitmap2 = bitmapDrawable2.getBitmap();
    canvas.drawBitmap(bitmap2,matrix,paint);
    }
    });
    ledDisplayView.addDrawer(new LedDisplayView.IDrawer() {
    @Override
    public void draw(Canvas canvas, int width, int height, Paint paint) {
    paint.setColor(Color.CYAN);
    float textSize = paint.getTextSize();
    paint.setTextSize(sp2px(50));
    canvas.drawText("你好,L E D", 100, 200, paint);
    canvas.drawText("85%", 100, 350, paint);

    paint.setColor(Color.YELLOW);
    canvas.drawCircle(width*3 / 4, height / 4, 100, paint);

    paint.setTextSize(textSize);
    }
    });

    四、总结


    这个本质上的核心就是采样,通过采样我们最终实现了纹理贴图,这点类似open gl中的光栅化,将图形分割成小三角形一样,最后着色,理解本篇也能帮助大家理解open gl和led显示原理。


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

    Android 图片分片过渡效果

    前言 在之前的文章中,通过LED效果、马赛克效果两篇文章,介绍了分片绘制的效果的方法和原理,通过这两篇文章,相信大家都已经熟悉了分片绘制的思路。其实分片绘制不仅仅能实现LED、马赛克等特殊效果,实际上类似百叶窗、图片对角线锯齿过渡等,很多PPT中存在的特效,基...
    继续阅读 »

    前言


    在之前的文章中,通过LED效果马赛克效果两篇文章,介绍了分片绘制的效果的方法和原理,通过这两篇文章,相信大家都已经熟悉了分片绘制的思路。其实分片绘制不仅仅能实现LED、马赛克等特殊效果,实际上类似百叶窗、图片对角线锯齿过渡等,很多PPT中存在的特效,基本上也是按照这种原理来实现的。


    分片可以有很多种意想不到的效,我们再来说一下分片特点:



    • [1] 按一定的距离、大小、角度对区域进行对一张图片或者区域裁剪或者提取区域图像

    • [2] 对提取出来的区域进行一系列变换,如百叶窗、微信摇一摇等

    • [3] 被裁剪的区域可以还原回去


    技术前景


    其实单纯的分片可以做一些瓦片效果,当然还可以做一些组合效果,下面是一个github开源项目(Camera2DApplication)利用Camera和图片分片实现的效果,这个过程中对一张图片进行分片绘制。


    fire_58.gif


    代码中的逻辑不是很复杂,本质上就是利用2张图片实现的,我们先来看下代码实现,作者的代码很认真,注释都写了,涉及postTranslate比较难懂的操作我也进行了微调。


    /**
    * 3d旋转效果
    *
    * @param canvas
    */

    private void drawModeNormal(Canvas canvas) {
    //VERTICAL时使用rotateY,HORIZONTAL时使用rotateX
    if (orientation == VERTICAL) {
    //如果是前进,则画当前图,后退则画上一张图,注释用的是前进情况
    matrix.reset();
    camera.save();
    //旋转角度 0 - -maxDegress
    camera.rotateX(-degress);
    camera.getMatrix(matrix);
    camera.restore();

    //绕着图片top旋转
    matrix.preTranslate(-viewWidth / 2f, 0);
    //旋转轴向下平移,则图片也向下平移
    matrix.postTranslate(viewWidth / 2f, rotatePivotY);
    //如果是前进,则画当前图,后退则画上一张图,因为后退时,这里画的是动画下方出来的图片,而下方的图片是前一张图
    canvas.drawBitmap(getBitmapScale(bitmapResourceIds.get(isForward ? currentIndex : preIndex), viewWidth, viewHeight),
    matrix, mPaint);

    //在处理下一张图片
    matrix.reset();
    camera.save();
    //旋转角度 maxDegress - 0
    camera.rotateX(maxDegress - degress);
    camera.getMatrix(matrix);
    camera.restore();

    //绕着图片bottom旋转
    matrix.preTranslate(-viewWidth / 2f, -viewHeight);
    //旋转轴向下平移,则图片也向下平移
    matrix.postTranslate(viewWidth / 2f, rotatePivotY);
    //如果是前进,则画下一张图,后退则画当前图,后退时,这边代码画的是动画上方的图片,上方的图片是当前图片
    canvas.drawBitmap(getBitmapScale(bitmapResourceIds.get(isForward ? nextIndex : currentIndex), viewWidth, viewHeight),
    matrix, mPaint);
    } else {
    //如果是前进,则画当前图,后退则画上一张图,注释用的是前进情况
    matrix.reset();
    camera.save();
    //旋转角度 0 - maxDegress
    camera.rotateY(degress);
    camera.getMatrix(matrix);
    camera.restore();

    //绕着图片left旋转
    matrix.preTranslate(0, -viewHeight / 2);
    //旋转轴向右平移,则图片也向右平移
    matrix.postTranslate(rotatePivotX, viewHeight / 2);
    //如果是前进,则画当前图,后退则画上一张图,因为后退时,这里画的是动画右方出来的图片,而右方的图片是前一张图
    canvas.drawBitmap(getBitmapScale(bitmapResourceIds.get(isForward ? currentIndex : preIndex), viewWidth, viewHeight),
    matrix, mPaint);

    //在处理下一张图片
    matrix.reset();
    camera.save();
    //旋转角度 -maxDegress - 0
    camera.rotateY(-maxDegress + degress);
    camera.getMatrix(matrix);
    camera.restore();

    //绕着图片right旋转
    matrix.preTranslate(-viewWidth, -viewHeight / 2f);
    //旋转轴向右平移,则图片也向右平移
    matrix.postTranslate(rotatePivotX, viewHeight / 2f);
    //如果是前进,则画下一张图,后退则画当前图,后退时,这边代码画的是动画左方的图片,左方的图片是当前图片
    canvas.drawBitmap(getBitmapScale(bitmapResourceIds.get(isForward ? nextIndex : currentIndex), viewWidth, viewHeight),
    matrix, mPaint);
    }
    }

    分片操作


    下面是分片操作,这个地方其实可以不用创建Bitmap缓存,创建Path就行,绘制时对Path区域利用Shader贴图即可。


    private Bitmap getBitmapScale(int resId, float width, float height) {
    if (ImageCache.getInstance().getBitmapFromMemCache(String.valueOf(resId)) != null) {
    return ImageCache.getInstance().getBitmapFromMemCache(String.valueOf(resId));
    }
    Bitmap bitmap = BitmapFactory.decodeResource(getResources(), resId);
    //创建分片
    Bitmap bitmapDst = Bitmap.createScaledBitmap(bitmap, (int) width, (int) height, false);
    bitmap.recycle();

    ImageCache.getInstance().addBitmapToMemoryCache(String.valueOf(resId)
    , bitmapDst);
    return bitmapDst;
    }

    小试一下


    我们这里通过一个简单的Demo,实现一种特效,这次我们利用网格矩阵分片。说到矩阵,很多人面试的时候都会遇到一些算法题,比较幸运的人遇到的是矩阵旋转90度、逆时针打印矩阵、矩阵孤岛问题、从左上角开始进行矩阵元素搜索,运气稍差的会遇到由外到里顺时针打印矩阵和斜对角打印矩阵,后面两种看似简单的问题实际上做起来并不顺手,有点扯远了,我们来看看效果。


    fire_59.gif


    你没看错,这次遇到了算法问题,我这边用的空间换取时间的方法。


    图像分片


    将图片分片,计算出网格的列和行


    int col = (int) Math.ceil(mBitmaps[index].getWidth() / blockWidth);
    int row = (int) Math.ceil(mBitmaps[index].getHeight() / blockWidth);

    分片算法


    这个算法实际上是每次将列数 +1,然后按对角分割,把符合的区域添加到path中


    int x = xPosition;
    int y = 0;
    while (x >= 0 && y <= row) {
    if (x < col && y < row) {
    dstRect.set((int) (x * blockWidth), (int) (y * blockWidth), (int) (x * blockWidth + blockWidth), (int) (y * blockWidth + blockWidth));
    // bitmapCanvas.drawBitmap(mBitmaps[index], dstRect, dstRect, mCommonPaint);
    path.addRect(dstRect, Path.Direction.CCW); //加入网格分片
    }
    x--;
    y++;
    }

    Path 路径贴图



    • Path过程中我们添加的rect是闭合区域,是可以贴图的,当然,一般有三种方法:

    • Path的贴图一般使用 clipPath对图片裁剪然后贴图,当然还有将对应的图片区域绘制到View上

    • Path 是Rect,按照Rect将图片区域绘制到Rect区域

    • 使用BitmapShader一次性绘制


    实际上我们应该尽可能使用Bitmap,因为BitmapShader唯一是不存在锯齿性能比较好的绘制方法。


    int save = bitmapCanvas.save();
    mCommonPaint.setShader(new BitmapShader(mBitmaps[index], Shader.TileMode.CLAMP, Shader.TileMode.CLAMP));
    bitmapCanvas.drawPath(path,mCommonPaint);
    bitmapCanvas.restoreToCount(save);

    其实我们的核心代码到这里就结束了,我们可以看到,分片可以的意义很重要的,当然,借助其他工具也可以实现,不过代码实现的好处是可以编辑和交互,不是所有的动画都可以产生交互。


    到此,我们还可以对今天的demo添加一些想象



    • 从中间外扩效果

    • 奇偶行切换效果

    • 国际象棋黑白格子变换效果

    • ......


    总结


    这是我们的第三篇关于图片分片特效的博客,希望通过一些了的文章,熟悉一些技术,往往看似高大上的效果,其实就是通过普普通通的方法叠加在一起的,当然,让你的技术承载你的想象,才是最重要的。


    本篇demo全部代码


    实际上代码贴太多很可能没人看,但是依照惯例,我们给出完整代码。


    public class TilesView extends View {
    private final DisplayMetrics mDM;
    private TextPaint mCommonPaint;
    private RectF mainRect = new RectF();
    private BitmapCanvas bitmapCanvas; //Canvas 封装的
    private Bitmap[] mBitmaps;
    private RectF dstRect = new RectF();
    Path path = new Path();
    private float blockWidth = 50f;
    private int xPosition = -2;
    private int index = 0;
    private boolean isTicking = false;
    public TilesView(Context context) {
    this(context, null);
    }
    public TilesView(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
    }

    public TilesView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    mDM = getResources().getDisplayMetrics();
    initPaint();
    }

    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);
    mBitmaps = new Bitmap[3];
    mBitmaps[0] = decodeBitmap(R.mipmap.mm_013);
    mBitmaps[1] = decodeBitmap(R.mipmap.mm_014);
    mBitmaps[2] = decodeBitmap(R.mipmap.mm_015);
    }

    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 onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
    if (bitmapCanvas != null && bitmapCanvas.bitmap != null && !bitmapCanvas.bitmap.isRecycled()) {
    bitmapCanvas.bitmap.recycle();
    }
    bitmapCanvas = null;

    }


    @Override
    protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    int width = getWidth();
    int height = getHeight();
    if (width < 1 || height < 1) {
    return;
    }
    if (bitmapCanvas == null || bitmapCanvas.bitmap == null || bitmapCanvas.bitmap.isRecycled()) {
    bitmapCanvas = new BitmapCanvas(Bitmap.createBitmap(mBitmaps[index].getWidth(), mBitmaps[index].getHeight(), Bitmap.Config.ARGB_8888));
    }
    int nextIndex = (index + 1) % mBitmaps.length;
    canvas.drawBitmap(mBitmaps[nextIndex],0,0,mCommonPaint);

    int col = (int) Math.ceil(mBitmaps[index].getWidth() / blockWidth);
    int row = (int) Math.ceil(mBitmaps[index].getHeight() / blockWidth);
    mCommonPaint.setStyle(Paint.Style.FILL);

    // path.reset();
    // for (int x = 0; x < row; x++) {
    // for (int y = 0; y < col; y++) {
    // gridRectF.set(x * blockWidth, y * blockWidth, x * blockWidth + blockWidth, y * blockWidth + blockWidth);
    // canvas.drawRect(gridRectF, mCommonPaint);
    // path.addRect(gridRectF, Path.Direction.CCW);
    // }
    // }

    diagonalEffect(col,row,xPosition,path);
    canvas.drawBitmap(bitmapCanvas.bitmap, 0, 0, mCommonPaint);

    if (isTicking && xPosition >= 0 && xPosition < col * 2) {
    clockTick();
    } else if(isTicking){
    xPosition = -1;
    index = nextIndex;
    isTicking = false;
    }
    }

    private void diagonalEffect(int col, int row, int xPosition,Path path) {
    int x = xPosition;
    int y = 0;
    while (x >= 0 && y <= row) {
    if (x < col && y < row) {
    dstRect.set((int) (x * blockWidth), (int) (y * blockWidth), (int) (x * blockWidth + blockWidth), (int) (y * blockWidth + blockWidth));
    // bitmapCanvas.drawBitmap(mBitmaps[index], dstRect, dstRect, mCommonPaint);
    path.addRect(dstRect, Path.Direction.CCW); //加入网格分片
    }
    x--;
    y++;
    }
    int save = bitmapCanvas.save();
    mCommonPaint.setShader(new BitmapShader(mBitmaps[index], Shader.TileMode.CLAMP, Shader.TileMode.CLAMP));
    bitmapCanvas.drawPath(path,mCommonPaint);
    bitmapCanvas.restoreToCount(save);

    }

    public void tick() {
    isTicking = true;
    xPosition = -1;
    path.reset();
    clockTick();
    }

    private void clockTick() {
    xPosition += 1;
    postInvalidateDelayed(16);
    }


    public float dp2px(float dp) {
    return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, mDM);
    }

    public float sp2px(float dp) {
    return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, dp, mDM);
    }

    static class BitmapCanvas extends Canvas {
    Bitmap bitmap;
    public BitmapCanvas(Bitmap bitmap) {
    super(bitmap);
    //继承在Canvas的绘制是软绘制,因此理论上可以绘制出阴影
    this.bitmap = bitmap;
    }

    public Bitmap getBitmap() {
    return bitmap;
    }
    }
    }

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

    qq农场私信我,您菜死了🥬

    web
    最近在写代码的时候发现自己总是有这样几种症状: 脸红心跳,像发烧一样😳; 口干舌燥、咳嗽不停😮‍💨; 脑袋放空,像刚通宵了一般👀; ...... 我逐渐怀疑,自己有没有可能是🐑了,甚至时不时就拿起体温计量一量,拿起自测试纸测一测,这样的情况一直没有得到好转...
    继续阅读 »

    最近在写代码的时候发现自己总是有这样几种症状



    1. 脸红心跳,像发烧一样😳;

    2. 口干舌燥、咳嗽不停😮‍💨;

    3. 脑袋放空,像刚通宵了一般👀;

    4. ......


    我逐渐怀疑,自己有没有可能是🐑了,甚至时不时就拿起体温计量一量,拿起自测试纸测一测,这样的情况一直没有得到好转,直到收到QQ农场给我发来这样的一条信息:


    尊敬的QQ农场主,您去年和今年菜死了!🥬🥬🥬


    🤔于是,我开始分析我症状根因是什么:



    1. 脸红心跳:是因为自己脑海中想象好的实现方案,但实际却写不出一行代码,或者各种Error,导致我心里落差很大,自我怀疑,或者是被人看穿菜的窘迫、害羞?

    2. 口干舌燥:是因为自己陷入了 写不出代码 => 憋着气接着写,不休息喝水 => 写不出代码 这样的闭环🐶里面;

    3. 脑袋放空:摆脱了内耗,很容易得出结论,就是看的技术不够多,写的代码不够多


    痛定思痛,决定在这里立下FLAG,要多看多实践,学习和思考好的代码写法,看得多,写得多。


    今天分享的主要是:用好发布订阅、偏函数的一对多 & 多对一关系工厂函数


    发布订阅 & 偏函数(一对多/多对一关系)


    是一种一对多的模式,或者说多对多的模式;一个事件对应多个处理函数,多个事件对应各自对应的处理函数



    那假如我们想实现一个多对一的关系呢?我们可以使用偏函数


    偏函数个人理解类似工厂函数,利用了闭包的特性


    // 偏函数
    function after(times, cb) {
    let count = 0;
    const result = {};
    return inner(key, value) => {
    result[key] = value;
    count++;
    if (count === times) {
    cb(result);
    }
    };
    }

    结合代码看此处相当于多个inner函数对应一个callback函数,由count来控制是否触发callback,这种模式常常用于异步编程,比如Promise.all



    综合一对多和多对一模式:


    // 偏函数
    function after(times, cb) {
    let count = 0;
    const result = {};
    return (key, value) => {
    result[key] = value;
    count++;
    if (count === times) {
    cb(result);
    }
    };
    }

    // 发布订阅
    const emitter = new (require("events").EventEmitter)();
    const done = after(3, render);

    emitter.on("done", done);
    emitter.on("done", other);

    fs.readFile(file, (err, template) => {
    emitter.emit("done", "template", template);
    });

    fs.readFile(file, (err, data) => {
    emitter.emit("done", "data", data);
    });

    fs.readFile(file, (err, str) => {
    emitter.emit("done", "str", str);
    });



    工厂函数


    类似现实工厂,在代码中用来生产特定结构函数/对象等的函数


    比如想实现一个生成校验函数的工厂函数:


    /**
    * config里可以包含一般的描述性属性,钩子函数等
    **/
    export function factory(config) {
    config.before = config.before || ((d) => d);
    // pre钩子
    handlersMap[config.type]?.pre(config);

    return function (data) {
    // before钩子函数
    data = config.before(data);
    return handlersMap[config.type].check(data);
    };
    }

    // 通过该方法注册不同的校验函数
    const handlersMap = {};
    factory.registerHandler = function (type, handler) {
    handlersMap[type] = handler;
    };

    在项目中的实现可如图:



    🌊总结:


    阅读好的代码,并学习一些好的写法,才是比较实际提高代码能力的方式,我也将💪持续阅读好的代码库,思考学习好的代码,把自己的成长分享出来。


    作者:Kuroo
    来源:juejin.cn/post/7182545613282623549
    收起阅读 »

    flutter 响应式观察值并更新UI

    响应式编程是一种以对数据随时间变化做出反应为中心的范式。它有助于自动传播更新,从而可确保 UI 和数据保持同步。用 Flutter 术语来说,这意味着只要状态发生变化就会自动触发重建。 Observables Observables是响应式编程的核心。这些数据...
    继续阅读 »

    响应式编程是一种以对数据随时间变化做出反应为中心的范式。它有助于自动传播更新,从而可确保 UI 和数据保持同步。用 Flutter 术语来说,这意味着只要状态发生变化就会自动触发重建。


    Observables


    Observables是响应式编程的核心。这些数据源会在数据发生变化时向订阅者发出更新。Dart 的核心可观察类型是 Stream。


    当状态发生变化时,可观察对象会通知侦听器。从用户交互到数据获取操作的任何事情都可以触发此操作。这有助于 Flutter 应用程序实时响应用户输入和其他更改。


    Flutter 有两种类型:ValueNotifierChangeNotifier,它们是类似 observable 的类型,但不提供任何真正的可组合性计算。


    ValueNotifier


    Flutter 中的类ValueNotifier在某种意义上是响应式的,因为当值发生变化时它会通知观察者,但您需要手动监听所有值的变化来计算完整的值。


    1、监听


      // 初始化
    final ValueNotifier<String> fristName = ValueNotifier('Tom');
    final ValueNotifier<String> secondName = ValueNotifier('Joy');
    late final ValueNotifier<String> fullName;

    @override
    void initState() {
    super.initState();
    fullName = ValueNotifier('${fristName.value} ${secondName.value}');

    fristName.addListener(_updateFullName);
    secondName.addListener(_updateFullName);
    }

    void _updateFullName() {
    fullName.value = '${fristName.value} ${secondName.value}';
    }


    //更改值得时候
    firstName.value = 'Jane'
    secondName.value = 'Jane'

    2、使用ValueListenableBuilder更新UI


     //通知观察者
    ValueListenableBuilder<String>(
    valueListenable: fullName,
    builder: (context, value, child) => Text(
    '${fristName.value} ${secondName.value}',
    style: Theme.of(context).textTheme.headlineMedium,
    ),
    ),

    ChangeNotifier


    1、监听


      String _firstName = 'Jane';
    String _secondName = 'Doe';

    String get firstName => _firstName;
    String get secondName => _secondName;
    String get fullName => '$_firstName $_secondName';

    set firstName(String newName) {
    if (newName != _firstName) {
    _firstName = newName;
    // Triggers rebuild
    notifyListeners();
    }
    }

    set secondName(String newSecondName) {
    if (newSecondName != _secondName) {
    _secondName = newSecondName;
    // Triggers rebuild
    notifyListeners();
    }
    }

    //更改值得时候
    firstName.value = 'Jane'
    secondName.value = 'Jane'

    2、使用AnimatedBuilder更新UI


    //通知观察者
    AnimatedBuilder(
    animation: fullName,
    builder: (context, child) => Text(
    fullName,
    style: Theme.of(context).textTheme.headlineMedium,
    ),
    ),

    get


    GetX将响应式编程变得非常简单。



    • 您不需要创建 StreamController。

    • 您不需要为每个变量创建一个 StreamBuilder。

    • 你不需要为每个状态创建一个类。

    • 你不需要创造一个终极价值。


    使用 Get 的响应式编程就像使用 setState 一样简单。
    让我们想象一下,您有一个名称变量,并且希望每次更改它时,所有使用它的小组件都会自动刷新。


    1、监听以及更新UI


    //这是一个普通的字符串
    var name = 'Jonatas Borges';
    为了使观察变得更加可观察,你只需要在它的附加上添加“.obs”。
    var name = 'Jonatas Borges'.obs;
    而在UI中,当你想显示该值并在值变化时更新页面时,只需这样做。
    Obx(() => Text("${controller.name}"));

    Riverpod


    final fristNameProvider = StateProvider<String>((ref) => 'Tom');
    final secondNameProvider = StateProvider<String>((ref) => 'Joy');
    final fullNameProvider = StateProvider<String>((ref) {
    final fristName = ref.watch(fristNameProvider);
    final secondName = ref.watch(secondNameProvider);
    return '$fristName $secondName';
    });

    //更改值得时候
    ref.read(fristNameProvider.notifier).state =
    'Jane'
    ref.read(secondName.notifier).state =
    'BB'


    2、使用ConsumerWidget更新UI


    ref.read(surnameProvider) 读取某个值


    ref.read(nameProvider.notifier).state 更新某个值的状态


    class MyHomePage extends ConsumerWidget {
    const MyHomePage({super.key});

    @override
    Widget build(BuildContext context, WidgetRef ref) =>
    Scaffold(
    appBar: AppBar(
    title: const Text('Riverpod Example'),
    ),
    body: Text(
    ref.watch(fullNameProvider),
    style: Theme.of(context).textTheme.headlineMedium,
    ),
    );
    }


    这里Consumer组件是与状态交互所必需的,Consumer有一个非标准build方法,这意味着如果您需要更改状态管理解决方案,您还必须更改组件而不仅仅是状态。


    RxDart


    RxDart将ReactiveX的强大功能引入Flutter,需要明确的逻辑来组合不同的数据流并对其做出反应。


    存储计算值:它不会以有状态的方式直接存储计算值,但它确实提供了有用的运算符(例如distinctUnique)来帮助您最大限度地减少重新计算。


    RxDart 库还有一个流行的类型被称为BehaviorSubject。响应式编程试图解决的核心问题是当依赖图中的任何值(依赖项)发生变化时自动触发计算。如果有多个可观察值,并且您需要将它们合并到计算中,Rx 库自动为我们执行此操作并且自动最小化重新计算以提高性能。


    该库向 Dart 的现有流添加了功能。它不会重新发明轮子,并使用其他平台上的开发人员熟悉的模式。


    1、监听


     final fristName = BehaviorSubject.seeded('Tom');
    final secondName = BehaviorSubject.seeded('Joy');

    /// 更新值
    fristName.add('Jane'),
    secondName.add('Jane'),


    2、使用StreamBuilder更新UI


     StreamBuilder<String>(
    stream: Rx.combineLatest2(
    fristName,
    secondName,
    (fristName, secondName) => '$fristName $secondName',
    ),
    builder: (context, snapshot) => Text(
    snapshot.data ?? '',
    style: Theme.of(context).textTheme.headlineMedium,
    ),
    ),

    Signals


    Signals以其computed功能介绍了一种创新、优雅的解决方案。它会自动创建反应式计算,当任何依赖值发生变化时,反应式计算就会更新。


    1、监听


      final name = signal('Jane');
    final surname = signal('Doe');
    late final ReadonlySignal<String> fullName =
    computed(() => '${name.value} ${surname.value}');
    late final void Function() _dispose;

    @override
    void initState() {
    super.initState();
    _dispose = effect(() => fullName.value);
    }

    2、使用watch更新UI


    Text(
    fullName.watch(context),
    style: Theme.of(context).textTheme.headlineMedium,
    ),

    作者:icc_tips
    来源:juejin.cn/post/7309131109740724259
    收起阅读 »

    拥抱华为,困难重重,第一天开始学习 ArkUI,踩坑踩了一天

    今天第一天正式开始学习鸿蒙应用开发。 本来想着,作为一个拥有 10 来年工作经验的资深开发,对 React/Vue,React Native,Android 开发都有丰富的实践经验,对 Swift UI 也有所涉猎,在大前端这一块可以说是信心满满,学习 Ark...
    继续阅读 »

    今天第一天正式开始学习鸿蒙应用开发。


    本来想着,作为一个拥有 10 来年工作经验的资深开发,对 React/Vue,React Native,Android 开发都有丰富的实践经验,对 Swift UI 也有所涉猎,在大前端这一块可以说是信心满满,学习 ArkUI 应该信手拈来才对,谁知道学习的第一天,我就发现我太天真了。


    HarmonyOS 与 ArkUI 给我了沉痛一击


    学习第一天一点都不顺利,上午还算有所收获,下午直接毫无建树,踩在一个坑里出不来,人直接裂开,差点以为自己要创业未半而中道崩殂了。不过好在晚饭后,侥幸解决了下午遇到的坑


    最终今天学习的成果如下


    scroll.gif


    导航栏的4个图标都是用了 lottie 的动画,因为使用了 gif 录制,可能有点感觉不太明显,真机上的感受非常舒适,用户体验极佳


    今天已经学习过的内容包括



    • 基础项目结构

    • 基础布局组件

    • 容器布局组件

    • 滚动组件

    • 导航组件

    • ohpm 安装

    • 引入 lottie 动画

    • 属性动画

    • 配置 hot reload

    • 组件状态管理 @state @props @link

    • 组件逻辑表达式

    • 沉浸式状态栏

    • 真机调试


    我的开发设备具体情况如下


    MacOS M1
    HarmonyOS API 9
    华为 P40 pro+,已安装 HarmonyOS 4

    作为一个把主要精力放在前端的开发者,做个记录分享一下学习体会


    01


    组件概念


    在前端开发中,不管你是用 React 还是使用 Vue,我们只需要掌握一个概念:组件。复杂的组件是由小的组件组成,页面是由组件组成,项目是由组件组成,超大项目也是由组件组成。组件可以组成一切。因此 React/Vue 的学习会相对更简单一些


    和 Android 一样,由于 HarmonyOS 有更复杂的应用场景、多端、分屏等,因此在这一块的概念也更多一些,目前我接触到的几个概念包括


    Window 一个项目好像可以有多个窗口,由于接触的时间太短了暂时不是很确定,可以创建子窗口,可以管理窗口的相关属性,创建,销毁等


    Ability 用户与应用交互的入口点,一个 app 可以有一个或者对个 Ability


    page 页面,一个应用可以由多个 page 组成


    Component 组件,可以组合成页面


    由于目前接触的内容不够全面,因此对这几个概念的理解还不够笃定,只是根据自己以往的开发经验推测大概可能是什么情况,因此介绍得比较简单,但是可以肯定的是理解这些概念是必备的


    02


    基础布局


    虽然 HarmonyOS 目前也支持 web 那一套逻辑开发,不过官方文档已经明确表示未来将会主推 arkUI,因此我个人觉得还是应该把主要学习重心放在 arkUI 上来


    arkUI 的布局思路跟 html + css 有很大不同。


    html + css 采用的是结构样式分离的方式,再通过 class/id 关联起来。因此,html + css 的布局写起来会简单很多,我们只需要先写结构,然后慢慢补充样式即可


    arkUI 并未采用分离思路,而是把样式和结构紧密结合在一起,这样做的好处就是性能更强了,因为底层渲染引擎不用专门写一套逻辑去匹配结构和样式然后重新计算 render-tree,坏处就是...


    代码看着有点糟心


    比如下面这行代码,表示两段文字


    Column() {
    Text('一行文字')
    .textAlign(TextAlign.Center)
    .fontSize(30)
    .width('100%')
    .backgroundColor('#aabbcc')
    Text('二行文字')
    .textAlign(TextAlign.Center)
    .fontSize(30)
    .width('100%')
    .backgroundColor('#aabbcc')
    }.width('100%')
    .height('100%')
    .backgroundColor('red')

    如果用 html 来表示的话....


    <div>
    <p>一行文字p>
    <p>一行文字p>
    div>

    当然我期望能找到一种方式去支持属性的继承和复用。目前简单找了一下没找到,希望有吧 ~


    由于 html 中 div 足以应付一切,因此许多前端开发者会在思考过程中忽视或者弱化容器组件的存在,反而 arkUI 的学习偏偏要从容器组件开始理解


    我觉得这种思路会对解耦思路有更明确的训练。许多前端开发在布局时不去思考解耦问题,我认为这是一个坏处。


    arkUI 的布局思路是:先考虑容器,再考虑子元素,并且要把样式特性结合起来一起思考。而不是只先思考结构,再考虑样式应该怎么写。


    例如,上面的 GIF 图中, nav 导航区域是由 4 按钮组成。先考虑容器得是一个横向的布局


    然后每一个按钮,包括一个图标和一个文字,他们是纵向的布局,于是结构就应该这样写


    Row: 横向布局
    Column: 竖向布局
    Row() {
    Column() { Lottie() Text() }
    Column() { Lottie() Text() }
    Column() { Lottie() Text() }
    Column() { Lottie() Text() }
    }

    按照这个思路去学习,几个容器组件 Row/Column/FLex/Stack/GridContainer/SideBarContainer ... 很快就能掌握


    03


    引入 lottie


    在引入 lottie 的时候遇到了几个坑。


    一个是有一篇最容易找到的文章介绍如何在 arkUI 中引入 lottie,结果这篇文章是错误的。 ~ ~,这篇文章是在官方博客里首发,让我走了不少弯路。


    image.png


    这里面有两个坑,一个坑是 @ohos/lottie-ohos-ets 的好像库不见了。另外一个坑就是文章里指引我用 npm 下载这个库。但是当我用 npm 下载之后,文件会跑到项目中的 node_modules 目录下,不过如何在 arkUI 的项目中引入 node_modules 中的库,我还没找到方法,应该是要在哪里配置一下


    最后在 gitee 的三方仓库里,找到了如下三方库


    import lottie from '@ohos/lottie';

    这里遇到的一个坑就是我的电脑上的环境变量不知道咋回事被改了,导致 ohpm 没了,找了半天才找到原因,又重新安装 ohpm,然后把环境变量改回来



    1. 到官方文档下载对应的工具包

    2. 把工具包放到你想要放的安装目录,然后解压,进去 ohpm/bin 目录,在该目录下执行 init 脚本开始安装


    > init


    1. 然后使用如下指令查看当前文件路径


    > pwd

    然后执行如下指令


    // OHPM_HOME 指的是你自己的安装路径
    > export OHPM_HOME=/home/xx/Downloads/ohpm
    > export PATH=${OHPM_HOME}/bin:${PATH}


    1. 执行如下指令检查是否安装成功


    > ohpm -v

    @ohos/lottie


    使用如下指令下载 lottie


    ohpm install @ohos/lottie

    然后在 page 中引入


    import lottie from '@ohos/lottie'

    在类中通过定义私有变量的方式构建上下文


    private mrs: RenderingContextSettings = new RenderingContextSettings(true)
    private ctx: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.mrs)

    并且用私有变量保存 lottie 数据路径或者内容


    private path: string = 'common/lottie/home.json'

    然后在 build 中,结合 Canvas 组件绘制


    Canvas(this.ctx).onReady(() => {
    lottie.loadAnimation({
    container: this.ctx,
    renderer: 'canvas',
    loop: false,
    autoplay: true,
    path: this.path
    })
    })

    参考文章:@ohos/lottie


    04


    hot reload


    使用 commond + , 调出配置页面,然后通过如下路径找到配置选中 Perform hot reload


    Tools -> Actions on Save -> Perform hot reload

    image.png


    然后在项目运行的入口处,选择 entry -> edit configrations,弹出如下界面,选中 Hot Reload 的 entry,做好与下图中一致的勾选,点击 apply 按钮之后启动项目即可实现 hot reload


    image.png


    不过呢,hot reload 在调试样式的时候还能勉强用一用,涉及到代码逻辑的更改,往往没什么用,属实是食之无味,弃之可惜


    除此之外,也许 Previewer 更适合开发时使用


    image.png


    05


    沉浸式状态栏


    沉浸式状态栏是一款体验良好的 app 必备能力。因此我学会了基础知识之后,第一时间就想要研究一下再 HarmonyOS 中如何达到这个目的。


    沉浸式状态栏指的就是下图中位置能够做到与页面标题栏,或者页面背景一致的样式。或者简单来说,可以由我们开发者来控制这一块样式。布局进入全屏模式。


    image.png


    在我们创建入口 Ability 时,可以在生命周期 onWindowStageCreate 中设置全屏模式


    onWindowStageCreate(windowStage: window.WindowStage) {
    windowStage.getMainWindow(err, mainWindow: window.Window) {
    if (err.code) {
    return
    }
    mainWindow.setWindowLayoutFullScreen(true)
    }
    }

    setWindowLayoutFullScreen 是一个异步函数,因此如果你想要修改状态栏样式的话,可以在它的回调里,通过 setWindowSystemBarProperties 去设置


    mainWindow.setWindowLayoutFullScreen(true, (err) => {
    if (err) { return }
    mainWindow.setWindowSystemBarProperties({ statusBarColor: '#FFF' })
    })

    具体的参数配置,可以在代码中,查看类型声明获悉。


    这里有一个巨大的坑,就是在我的开发环境启动的模拟器中 API 9,当你设置了全屏模式之后,布局会发生混乱。真机调试又是正常的。


    我刚开始以为是我的代码哪里没搞对,为了解决这个问题花了一个多小时的时间,结果最后才确定是模拟器的布局 bug...


    真机调试


    真机调试的设置方式主要跟其他 app 开发都一样,把手机设置为开发者模式即可。不过你需要通过如下方式,配置好一个应用签名才可以。因此你首先需要注册成为华为开发者


    File -> Project Structure -> Signing Configs -> Sign in

    跟着指引在后台创建项目,然后再回到开发者工具这个页面自动生成签名即可


    image.png


    真机调试有一个巨大无比的坑,那就是 API 9 创建的项目,在老版本的麒麟芯片上巨卡无比。连基本的点击都无法响应。


    这就要了命了。如果连真机调试都做不到,那还拥抱个啥啊?


    研究了很久,找到了几个解决这个问题的方法


    1、换新机,只要你的手机不是华为被制裁之前的麒麟芯片,都不会存在这个问题


    2、创建项目时,选择 API 8


    3、在开发者选项的配置中,选择 显示面(surface)更新,虽然不卡了,不过闪瞎了我的狗眼


    4、等明年 HarmonyOS next 出来之后再来学,官方说,API 10 将会解决这个问题


    上面的解决办法或多或少都有一些坑点。我选择了一种方式可以很好的解决这个问题


    那就是:投屏


    如果你有一台华为电脑,这个投屏会非常简单。不过由于我是 mac M1,因此我选择的投屏方案是 scrcpy


    使用 brew 安装


    > brew install scrcpy

    然后继续安装


    > brew install android-platform-tools

    启动


    > scrcpy

    启动之前确保只有一台手机已经通过 USB 连接到电脑,并允许电脑调试手机就可以成功投屏。在投屏中操作手机,就变得非常流畅了


    不过目前我通过这种方式投屏之后,运行起来的项目经常闪退,具体是什么原因我还没找到,只能先忍了


    总之就是坑是一个接一个 ~ ~


    06


    总结


    一整天的学习,整体感受下就如标题说的那样:拥抱华为,困难重重。 还好我电脑性能强悍,要是内存少一点,又是虚拟机,又是投屏的,搞不好内存都不够用,可以预想,其他开发者还会遇到比我更多的坑 ~ ~


    image.png


    个人感觉华为相关的官方文档写得不是很友好,比较混乱,找资料很困难。反而在官方上把一堆莫名其妙的教学视频放在了最重要的位置,我不是很明白,到底是官方文档,还是视频教程网站 ~ ~


    官方文档里还涉及了 FA mode 到 Stage mode 的更新,因此通过搜索引擎经常找到 FA mode 的相关内容,可是 FA mode 又是被弃用的,因为这个问题也给我的学习带来了不少的麻烦。由于遇到的坑太多了,以致于我到现在尝试点什么新东西都紧张兮兮的,生怕又是坑


    总的来说,自学困难重重,扛得住坑的,才能成为最后的赢家,红利不是那么好吃的


    作者:这波能反杀
    来源:juejin.cn/post/7309734518586523657
    收起阅读 »

    WebSocket 从入门到入土

    web
    前言因新部门需求有一个后台管理需要一个右上角的实时的消息提醒功能,第一时间想到的就是使用WebSocket建立实时通信了,之前没整过,于是只能学习了。和原部门相比现在太忙了,快乐的日子一去不复返了。经典的加量不加薪啊!!!一.WebSocket 基本概念1.W...
    继续阅读 »

    前言

    因新部门需求有一个后台管理需要一个右上角的实时的消息提醒功能,第一时间想到的就是使用WebSocket建立实时通信了,之前没整过,于是只能学习了。和原部门相比现在太忙了,快乐的日子一去不复返了。经典的加量不加薪啊!!!

    一.WebSocket 基本概念

    1.WebSocket是什么?

    WebSocket 是基于 TCP 的一种新的应用层网络协议。它提供了一个全双工的通道,允许服务器和客户端之间实时双向通信。因此,在 WebSocket 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输,客户端和服务器之间的数据交换变得更加简单。 WebSocket

    2.与 HTTP 协议的区别

    与 HTTP 协议相比,WebSocket 具有以下优点:

    1. 更高的实时性能:WebSocket 允许服务器和客户端之间实时双向通信,从而提高了实时通信场景中的性能。
    2. 更少的网络开销:HTTP 请求和响应之间需要额外的数据传输,而 WebSocket 通过在同一个连接上双向通信,减少了网络开销。
    3. 更灵活的通信方式:HTTP 请求和响应通常是一一对应的,而 WebSocket 允许服务器和客户端之间以多种方式进行通信,例如消息 Push、事件推送等。
    4. 更简洁的 API:WebSocket 提供了简洁的 API,使得客户端开发人员可以更轻松地进行实时通信。

    当然肯定有缺点的:

    1. 不支持无连接: WebSocket 是一种持久化的协议,这意味着连接不会在一次请求之后立即断开。这是有利的,因为它消除了建立连接的开销,但是也可能导致一些资源泄漏的问题。
    2. 不支持广泛: WebSocket 是 HTML5 中的一种标准协议,虽然现代浏览器都支持,但是一些旧的浏览器可能不支持 WebSocket。
    3. 需要特殊的服务器支持: WebSocket 需要服务端支持,只有特定的服务器才能够实现 WebSocket 协议。这可能会增加系统的复杂性和部署的难度。
    4. 数据流不兼容: WebSocket 的数据流格式与 HTTP 不同,这意味着在不同的网络环境下,WebSocket 的表现可能会有所不同。

    3.WebSocket工作原理

    1. 握手阶段

    WebSocket在建立连接时需要进行握手阶段。握手阶段包括以下几个步骤:

    • 客户端向服务端发送请求,请求建立WebSocket连接。请求中包含一个Sec-WebSocket-Key参数,用于生成WebSocket的随机密钥。
    • 服务端接收到请求后,生成一个随机密钥,并使用随机密钥生成一个新的Sec-WebSocket-Accept参数。
    • 客户端接收到服务端发送的新的Sec-WebSocket-Accept参数后,使用原来的随机密钥和新的Sec-WebSocket-Accept参数共同生成一个新的Sec-WebSocket-Key参数,用于加密数据传输。
    • 客户端将新的Sec-WebSocket-Key参数发送给服务端,服务端接收到后,使用该参数加密数据传输。

    2. 数据传输阶段

    建立连接后,客户端和服务端就可以通过WebSocket进行实时双向通信。数据传输阶段包括以下几个步骤:

    • 客户端向服务端发送数据,服务端收到数据后将其转发给其他客户端。
    • 服务端向客户端发送数据,客户端收到数据后进行处理。

    双方如何进行相互传输数据的 具体的数据格式是怎么样的呢?WebSocket 的每条消息可能会被切分成多个数据帧(最小单位)。发送端会将消息切割成多个帧发送给接收端,接收端接收消息帧,并将关联的帧重新组装成完整的消息。

    发送方 -> 接收方:ping。

    接收方 -> 发送方:pong。

    ping 、pong 的操作,对应的是 WebSocket 的两个控制帧

    3. 关闭阶段

    当不再需要WebSocket连接时,需要进行关闭阶段。关闭阶段包括以下几个步骤:

    • 客户端向服务端发送关闭请求,请求中包含一个WebSocket的随机密钥。
    • 服务端接收到关闭请求后,向客户端发送关闭响应,关闭响应中包含服务端生成的随机密钥。
    • 客户端收到关闭响应后,关闭WebSocket连接。

    总的来说,WebSocket通过握手阶段、数据传输阶段和关闭阶段实现了服务器和客户端之间的实时双向通信。

    二.WebSocket 数据帧结构和控制帧结构。

    1. 数据帧结构

    WebSocket 数据帧主要包括两个部分:帧头和有效载荷。以下是 WebSocket 数据帧结构的简要介绍:

    • 帧头:帧头包括四个部分:fin、rsv1、rsv2、rsv3、opcode、masked 和 payload_length。其中,fin 表示数据帧的结束标志,rsv1、rsv2、rsv3 表示保留字段,opcode 表示数据帧的类型,masked 表示是否进行掩码处理,payload_length 表示有效载荷的长度。
    • 有效载荷:有效载荷是数据帧中实际的数据部分,它由客户端和服务端进行数据传输。

    2. 控制帧结构

    除了数据帧之外,WebSocket 协议还包括一些控制帧,主要包括 Ping、Pong 和 Close 帧。以下是 WebSocket 控制帧结构的简要介绍:

    • Ping 帧:Ping 帧用于测试客户端和服务端之间的连接状态,客户端向服务端发送 Ping 帧,服务端收到后需要向客户端发送 Pong 帧进行响应。
    • Pong 帧:Pong 帧用于响应客户端的 Ping 帧,它用于测试客户端和服务端之间的连接状态。
    • Close 帧:Close 帧用于关闭客户端和服务端之间的连接,它包括四个部分:fin、rsv1、rsv2、rsv3、opcode、masked 和 payload_length。其中,opcode 的值为 8,表示 Close 帧。

    三. JavaScript 中 WebSocket 对象的属性和方法,以及如何创建和连接 WebSocket。

    WebSocket 对象的属性和方法:

    1. WebSocket 对象:WebSocket 对象表示一个新的 WebSocket 连接。
    2. WebSocket.onopen 事件处理程序:当 WebSocket 连接打开时触发。
    3. WebSocket.onmessage 事件处理程序:当接收到来自 WebSocket 的消息时触发。
    4. WebSocket.onerror 事件处理程序:当 WebSocket 发生错误时触发。
    5. WebSocket.onclose 事件处理程序:当 WebSocket 连接关闭时触发。
    6. WebSocket.send 方法:向 WebSocket 发送数据。
    7. WebSocket.close 方法:关闭 WebSocket 连接。

    创建和连接 WebSocket:

    1. 创建 WebSocket 对象:
    var socket = new WebSocket('ws://example.com');

    其中,ws://example.com 是 WebSocket 的 URL,表示要连接的服务器。

    1. 连接 WebSocket:

    使用 WebSocket.onopen 事件处理程序检查 WebSocket 是否成功连接。

    socket.onopen = function() {
    console.log('WebSocket connected');
    };
    1. 接收来自 WebSocket 的消息:

    使用 WebSocket.onmessage 事件处理程序接收来自 WebSocket 的消息。

    socket.onmessage = function(event) {
    console.log('WebSocket message:', event.data);
    };
    1. 向 WebSocket 发送消息:

    使用 WebSocket.send 方法向 WebSocket 发送消息。

    socket.send('Hello, WebSocket!');
    1. 关闭 WebSocket:

    当需要关闭 WebSocket 时,使用 WebSocket.close 方法。

    socket.close();

    注意:在 WebSocket 连接成功打开和关闭时,会分别触发 WebSocket.onopen 和 WebSocket.onclose 事件。在接收到来自 WebSocket 的消息时,会触发 WebSocket.onmessage 事件。当 WebSocket 发生错误时,会触发 WebSocket.onerror 事件。

    四.webSocket简单示例

    以下是一个简单的 WebSocket 编程示例,通过 WebSocket 向服务器发送数据,并接收服务器返回的数据:

    1. 首先,创建一个 HTML 文件,添加一个按钮和一个用于显示消息的文本框:
    html>
    <html>
    <head>
    <meta charset="UTF-8">
    <title>WebSocket 示例title>
    head>
    <body>
    <button id="sendBtn">发送消息button>
    <textarea id="messageBox" readonly>textarea>
    <script src="main.js">script>
    body>
    html>
    1. 接下来,创建一个 JavaScript 文件(例如 main.js),并在其中编写以下代码:
    // 获取按钮和文本框元素
    const sendBtn = document.getElementById('sendBtn');
    const messageBox = document.getElementById('messageBox');

    // 创建 WebSocket 对象
    const socket = new WebSocket('ws://echo.websocket.org'); // 使用一个 WebSocket 服务器进行测试

    // 设置 WebSocket 连接打开时的回调函数
    socket.onopen = function() {
    console.log('WebSocket 连接已打开');
    };

    // 设置 WebSocket 接收到消息时的回调函数
    socket.onmessage = function(event) {
    console.log('WebSocket 接收到消息:', event.data);
    messageBox.value += event.data + '\n';
    };

    // 设置 WebSocket 发生错误时的回调函数
    socket.onerror = function() {
    console.log('WebSocket 发生错误');
    };

    // 设置 WebSocket 连接关闭时的回调函数
    socket.onclose = function() {
    console.log('WebSocket 连接已关闭');
    };

    // 点击按钮时发送消息
    sendBtn.onclick = function() {
    const message = 'Hello, WebSocket!';
    socket.send(message);
    messageBox.value += '发送消息: ' + message + '\n';
    };

    五.webSocket应用场景

    1. 实时通信:WebSocket 非常适合实时通信场景,例如聊天室、在线游戏、实时数据传输等。通过 WebSocket,客户端和服务器之间可以实时通信,无需依赖轮询,从而提高通信效率和减少网络延迟。
    2. 监控数据传输:WebSocket 可以在监控系统中实现实时数据传输,例如通过 WebSocket,客户端可以实时接收和处理监控数据,而无需等待轮询数据。
    3. 自动化控制:WebSocket 可以在自动化系统中实现远程控制,例如通过 WebSocket,客户端可以远程控制设备或系统,而无需直接操作。
    4. 数据分析:WebSocket 可以在数据分析场景中实现实时数据传输和处理,例如通过 WebSocket,客户端可以实时接收和处理数据,而无需等待数据存储和分析。
    5. 人工智能:WebSocket 可以在人工智能场景中实现实时数据传输和处理,例如通过 WebSocket,客户端可以实时接收和处理数据,而无需等待数据处理和分析。

    六.WebSocket 错误处理

    WebSocket 的错误处理

    1. WebSocket is not supported:当浏览器不支持 WebSocket 时,会出现此错误。解决方法是在浏览器兼容性列表中检查是否支持 WebSocket。
    2. WebSocket connection closed:当 WebSocket 连接被关闭时,会出现此错误。解决方法是在 WebSocket.onclose 事件处理程序中进行错误处理。
    3. WebSocket error:当 WebSocket 发生错误时,会出现此错误。解决方法是在 WebSocket.onerror 事件处理程序中进行错误处理。
    4. WebSocket timeout:当 WebSocket 连接超时时,会出现此错误。解决方法是在 WebSocket.ontimeout 事件处理程序中进行错误处理。
    5. WebSocket handshake error:当 WebSocket 握手失败时,会出现此错误。解决方法是在 WebSocket.onerror 事件处理程序中进行错误处理。
    6. WebSocket closed by server:当 WebSocket 连接被服务器关闭时,会出现此错误。解决方法是在 WebSocket.onclose 事件处理程序中进行错误处理。
    7. WebSocket closed by protocol:当 WebSocket 连接被协议错误关闭时,会出现此错误。解决方法是在 WebSocket.onclose 事件处理程序中进行错误处理。
    8. WebSocket closed by network:当 WebSocket 连接被网络错误关闭时,会出现此错误。解决方法是在 WebSocket.onclose 事件处理程序中进行错误处理。
    9. WebSocket closed by server:当 WebSocket 连接被服务器错误关闭时,会出现此错误。解决方法是在 WebSocket.onclose 事件处理程序中进行错误处理。

    通过为 WebSocket 对象的 oncloseonerror 和 ontimeout 事件添加处理程序,可以及时捕获和处理 WebSocket 错误,从而确保程序的稳定性和可靠性。

    七.利用单例模式创建完整的wesocket连接

    class webSocketClass {
    constructor(thatVue) {
    this.lockReconnect = false;
    this.localUrl = process.env.NODE_ENV === 'production' ? 你的websocket生产地址' : '测试地址';
    this.globalCallback = null;
    this.userClose = false;
    this.createWebSocket();
    this.webSocketState = false
    this.thatVue = thatVue
    }

    createWebSocket() {
    let that = this;
    // console.log('
    开始创建websocket新的实例', new Date().toLocaleString())
    if( typeof(WebSocket) != "function" ) {
    alert("您的浏览器不支持Websocket通信协议,请更换浏览器为Chrome或者Firefox再次使用!")
    }
    try {
    that.ws = new WebSocket(that.localUrl);
    that.initEventHandle();
    that.startHeartBeat()
    } catch (e) {
    that.reconnect();
    }
    }

    //初始化
    initEventHandle() {
    let that = this;
    // //连接成功建立后响应
    that.ws.onopen = function() {
    console.log("连接成功");
    };
    //连接关闭后响应
    that.ws.onclose = function() {
    // console.log('
    websocket连接断开', new Date().toLocaleString())
    if (!that.userClose) {
    that.reconnect(); //重连
    }
    };
    that.ws.onerror = function() {
    // console.log('
    websocket连接发生错误', new Date().toLocaleString())
    if (!that.userClose) {
    that.reconnect(); //重连
    }
    };
    that.ws.onmessage = function(event) {
    that.getWebSocketMsg(that.globalCallback);
    // console.log('
    socket server return '+ event.data);
    };
    }
    startHeartBeat () {
    // console.log('
    心跳开始建立', new Date().toLocaleString())
    setTimeout(() => {
    let params = {
    request: '
    ping',
    }
    this.webSocketSendMsg(JSON.stringify(params))
    this.waitingServer()
    }, 30000)
    }
    //延时等待服务端响应,通过webSocketState判断是否连线成功
    waitingServer () {
    this.webSocketState = false//在线状态
    setTimeout(() => {
    if(this.webSocketState) {
    this.startHeartBeat()
    return
    }
    // console.log('
    心跳无响应,已断线', new Date().toLocaleString())
    try {
    this.closeSocket()
    } catch(e) {
    console.log('
    连接已关闭,无需关闭', new Date().toLocaleString())
    }
    this.reconnect()
    //重连操作
    }, 5000)
    }
    reconnect() {
    let that = this;
    if (that.lockReconnect) return;
    that.lockReconnect = true; //没连接上会一直重连,设置延迟避免请求过多
    setTimeout(function() {
    that.createWebSocket();
    that.thatVue.openSuccess(that) //重连之后做一些事情
    that.thatVue.getSocketMsg(that)
    that.lockReconnect = false;
    }, 15000);
    }

    webSocketSendMsg(msg) {
    this.ws.send(msg);
    }

    getWebSocketMsg(callback) {
    this.ws.onmessage = ev => {
    callback && callback(ev);
    };
    }
    onopenSuccess(callback) {
    this.ws.onopen = () => {
    // console.log("连接成功", new Date().toLocaleString())
    callback && callback()
    }
    }
    closeSocket() {
    let that = this;
    if (that.ws) {
    that.userClose = true;
    that.ws.close();
    }
    }
    }
    export default webSocketClass;

    作者:耀耀切克闹灬
    来源:juejin.cn/post/7309687967063818292

    收起阅读 »

    按钮点击的水波效果

    web
    实现思路:水波效果可以用一个 span 来模拟,动画效果是缩放从 0 到大于零的值(比如 4),同时透明度从 1 到 0。点击 button 后,我们把这个 span 添加到 button 里即可。 HTML 结构比较简单,我们用 div 来表示 button...
    继续阅读 »

    image


    实现思路:水波效果可以用一个 span 来模拟,动画效果是缩放从 0 到大于零的值(比如 4),同时透明度从 1 到 0。点击 button 后,我们把这个 span 添加到 button 里即可。


    HTML


    结构比较简单,我们用 div 来表示 button:


    <div class="button">
    Click Me
    </div>

    CSS


    给 div 加点样式,让它看起来像个 button:


    image


    .button {
    margin-left: 100px;
    position: relative;
    width: 100px;
    padding: 8px 10px;
    border: 1px solid lightgray;
    border-radius: 5px;
    cursor: pointer;
    overflow: hidden;
    user-select: none;
    }

    定义水波样式,默认 scale 为 0:


    .ripple {
    position: absolute;
    border-radius: 50%;
    transform: scale(0);
    animation: ripple 600ms linear;
    background-color: rgba(30, 184, 245, 0.7);
    }

    水波动画:


    @keyframes ripple {
    to {
    transform: scale(4);
    opacity: 0;
    }
    }

    javascript


    点击按钮时,生成水波效果,先把结构加上:


    function playRipple(event) {
    // TODO:生成水波效果
    }

    // 为 button 添加点击事件
    document
    .querySelector('.button')
    .addEventListener('click', event => {
    playRipple(event);
    })

    我们看一下水波如何生成,为了方便理解,可以结合图来看,其中黑点表示鼠标点击的位置,蓝色的圆是点击后水波默认大小的圆,** ?**就表示要计算的 circle.style.left:


    image


    function playRipple(event) {
    const button = event.currentTarget;
    const buttonRect = button.getBoundingClientRect();

    const circle = document.createElement("span");
    // 圆的直径
    const diameter = Math.max(button.clientWidth, button.clientHeight);
    // 圆的半径
    const radius = diameter / 2;

    // 计算 ripple 的位置
    circle.style.width = circle.style.height = `${diameter}px`;
    circle.style.left = `${event.clientX - (buttonRect.left + radius)}px`;
    circle.style.top = `${event.clientY - (buttonRect.top + radius)}px`;
    // 添加 ripple 样式
    circle.classList.add("ripple");
    // 移除已存在的 ripple
    removeRipple(button);
    // 将 ripple 添加到 button 上
    button.appendChild(circle);
    }

    // 移除 ripple
    function removeRipple(button) {
    const ripple = button.querySelector(".ripple");

    if (ripple) {
    ripple.remove();
    }
    }

    看下效果:
    image


    总结


    又水了一篇文章😂,如果对你有启发,欢迎点赞、评论。


    参考


    css-tricks.com/how-to-recr…


    作者:探险家火焱
    来源:juejin.cn/post/7224063449617383485
    收起阅读 »

    如何做好前端项目组组长

    前言 唠嗑 俺自己弄自己写博客是为了记录自己的脚步,走成功就留下近道,方便其他兴趣者抄近道提升;走失败了就留下血迹(魂类游戏の特色),方便其他人看看我是这么寄的。 我曾经给自己规定,一个月最少留下一片技术性的或者经验性值的博客,方便自己自我总结。结果十月底后,...
    继续阅读 »



    前言 唠嗑


    俺自己弄自己写博客是为了记录自己的脚步,走成功就留下近道,方便其他兴趣者抄近道提升;走失败了就留下血迹(魂类游戏の特色),方便其他人看看我是这么寄的。


    我曾经给自己规定,一个月最少留下一片技术性的或者经验性值的博客,方便自己自我总结。结果十月底后,咱忙得不可开交~~,都没时间水群~~,写博客的规划就一拖再拖,最后都十二月了,emmmm,不能再拖了。今天就写完。
    6号的今儿,加个班,努力写完吧


    一、个人方面


    角色转变


    以前是组员,会追求极致的代码逻辑或写出最优性能的算法。但现在你是组长了,你得学会接纳不完美,比如每次mr的时候不能太过计较组员代码性能或者代码逻辑(个人经验,可能不用于大厂)。


    其二,在团队中,平常心非常重要。无论是组长还是组员,大家都是打工人,没有高人一等的态度。


    学习方向


    学习方向要从原来的学得深改为看得广。这样方便给组员提供解决问题思路或者功能实现方案。



    当组员的时候我会专研得很深,甚至会深入专研vue2底层代码甚至去自己手写一个自己的vue2 demo。


    当组长后,我很少专研底层代码或者底层架构了,大多都是看其他作者如何解决没见过的业务的问题,亦或者是使用某个依赖出现的模块出现问题以及避免方法。积累新模块使用以及新的业务解决方案。



    a620e57a0ae162f0e4aa34bb1d4d8ecb5ce17e72eede4e8d024a8d68d3859602.png


    二、组内安排


    统筹和分配


    产品给的需求、后端配合人员、bug转交等等,这些都归属于任务类型,要记得如何分配任务以及实时跟踪进度(按天跟踪最好)。


    Weixin Screenshot_20231130222156.png


    分配任务时候请注意:



    • 产品需求方面一定要记住划分模块,再记住模块对应的组员,方便后续QA多轮轮测试时候bug指向对应的组员,亦或者编写《XXXX技术规格书》时将其划分给对应组员

    • 对每个任务划分好难度,根据组员能力差异给到最优解


    学会做自己组的产品(建议)


    注意,这个只是建议,不是必须!


    前端组长也要会当产品?是,也不是。比如说在项目立项前期,有些东西必须前端自己规划好,如框架搭建指南、二次封装的公共组件(如搜索表单,公共列表,echarts的各类型图表),这个时候就需要你自己做自己的产品经理,自己写相关的需求文档或者技术规格文档。


    可以不写么?如果你能让组员明白你的规划或者明白你的思路,你可以不用写,只需要交代就行。否则还是建议写一下。


    提供一定的情绪价值


    这个只可意会不可言传的,需要自己把握好度,平衡好自己的情绪以及组员的情绪。


    7176b207911683222628d044b6fdf104cccacda7bc9c0f98646bc80d0d30a894.png


    三、项目组角色


    前端组长还是前端开发,所以说本职前端工作要有,还得担当一些其他任务。


    做好项目组副手


    虽然是前端组长,虽然入手的是js、ts、node,但你还是要了解一些其他与前端开发或者与项目组相关的东西,这里是我经历过的一些事儿,可以借鉴一下:



    • 学一些基础的PS平面设计概念,便于和UI统一意见

    • linux 虚机,需要本地VMware或者公司服务器

    • CI/CD 流程

    • docker 配置文件、基础指令

    • nginx 设置反向代理

    • shell 脚本编写

    • 手写case,方便开发自测

    • 了解公司发布流程,准备好补充缺失的文件

    • 学会公司文件管理方式,如SVN、企业级Visual Studio


    与UI配合


    以下是我根据个人经验总结的一些建议:



    • 组长层面

      • 确认公共组件统一样式

        • 公共列表样式

        • 搜索表单样式

        • Dialog/Modal对话框 宽度和最大高度以及高度是否固定

        • Description 统一样式

        • 滚动条样式(ChromeFirefox)

        • Button/Tag 边框弧度

        • Layout框架样式,如菜单padding距离、

        • 文本/内容超出部分处理方案

        • 图片使用格式 png/svg

        • Notification通知框出现位置、按钮、存在时间

        • 统一图表获取方式,如提供手动图表库或者使用三方图表库



      • 参与设计图评审

        • 创建编辑操作时注意其标注必填项以及对应选项框是否一致

        • 首页/门户页面/欢迎页面/列表 处理文本过长,内容过多的方案

        • 交互/大屏 动画效果确认





    • 开发层面

      • 学会自己切图,如使用国内的'蓝湖','即使设计',亦或者是adobe的XD

      • 让UI帮忙修图时候尽量让UI用上SVG图片

        • SVG是矢量图,可以提供图层信息,方便UI调整



      • 如果涉及动画效果之类的(如告警闪烁效果),可以给UI写个可调整页面,让UI自己寻找合适的感觉




    与产品配合


    以下是我根据个人经验总结的一些建议:



    • 组长层面:

      • 需求评审时

        • 建议记录每个具体的模块以及其大概功能点(比如创建,编辑,删除这类操作性的,如果详情里也有的话同步记录),方便后续分配任务以及自测时写case

        • 这个算是空话,但还是记下来吧:仔细听产品报告,确认功能可行性



      • 帮产品搭建原型图服务,方便UI和自己组员查阅



    • 开发层面

      • 功能时间过于耗时并且不是主要功能时,及时告诉产品,协商解决方案

      • 集成系统并且无法从三方系统/三方厂商获取数据或者是,必须及时告诉产品




    与后端配合


    唯一一个跟咱一样是开发的,懂逻辑的童鞋们~~,感觉我可以偷个懒不写建议~~,还是要写一下建议:



    • 组长层面:

      • 及时告知后端童鞋配合一起开发的前端童鞋

      • 协助后端更新服务器上的容器,或者帮其完善CI/CD




    eed8adb174843fb8e32281a925c8d392955e1ce405eaf0bb132f42fab52e1364.png


    尾声


    如果不嫌弃,请大佬们在评论区教我做人。


    9efa601e7dcfa58e1135bde96bd2a83fb3d3c33acf2bc376272a2c7e749a2740.png


    作者:望远镜
    来源:juejin.cn/post/7309301549154779171
    收起阅读 »

    “浏览器切换到其他页面或最小化时,倒计时不准确“问题解析

    web
    背景 我最近修复了一个倒计时延迟的bug,情况是用户10:00设置了10分钟倒计时,10:06查看时发现倒计时还有8分钟,倒计时出不准确、延迟的情况。 倒计时大概逻辑如下: const leftTime = 600; //单位为秒 const timer = ...
    继续阅读 »

    背景


    我最近修复了一个倒计时延迟的bug,情况是用户10:00设置了10分钟倒计时,10:06查看时发现倒计时还有8分钟,倒计时出不准确、延迟的情况。


    倒计时大概逻辑如下:


    const leftTime = 600; //单位为秒
    const timer = setInterval(() => {
    leftTime -= 1;
    if(leftTime === 0) {
    clearInterval(timer);
    }
    }, 1000);

    通过排查是浏览器的优化策略导致的。


    为什么浏览器优化策略会造成定时器不准时?又该怎么解决这个问题?本文会围绕这两个问题展开说明!


    浏览器优化策略对定时器的影响


    浏览器的优化策略是指浏览器为了提高性能和节省资源而对特定任务进行的优化。在后台标签页中,浏览器可能会对一些任务进行节流或延迟执行,以减少CPU和电池的消耗。


    而定时器setIntervalsetTimeout就是受浏览器优化策略的影响,导致定时器的执行时间间隔被延长。所以在浏览器切换到其他页面或者最小化时,当前页面的定时器可能不会按照预期的时间间隔准时执行。


    我们实验一下:设置一个定时器,每500ms在控制台输出当前时间;然后再监听该标签页的visibilitychange事件,当其选项卡的内容变得可见或被隐藏时,会触发该事件。


    // 设置定时器
    const leftTime = 600; // 倒计时剩余时间
    setInterval(() => {
    const date = new Date();
    leftTime.value -= 1;
    console.log(`倒计时剩余秒数:${ leftTime.value }`, `当前时间秒数:${ date.getSeconds() }`);
    }, 1000);
    // 通过监听 visibilitychange 事件来判别该页面是否可见
    document.addEventListener('visibilitychange', function () {
    if(document.hidden) {
    console.log('页面不可见')
    }
    })

    执行结果如下:


    image.png


    我们观察执行结果会发现,在标签页处于不可见状态后,setInterval从1000ms的时间间隔延长成了2000ms。


    由此可见,当浏览器切换其他页面或者最小化时,倒计时的误差就出现了,setInterval定时器也不会在1000ms后减去1。对于时间较长的倒计时来说,误差会更大。


    解决思路


    既然浏览器的定时器有问题,那我们就不依赖定时器去计算剩余时间。


    我们可以在用户配置倒计时后,立即计算出结束时间并保存,随后通过结束时间减去本地时间就得出了剩余时间,而且不会受定时器延迟的影响。将最上面提及到的倒计时伪代码修改如下:


    // ......
    const leftTime = 600 * 1000
    const endTime = Date.now() + leftTime; // 倒计时结束时间
    setInterval(() => {
    const date = new Date();
    leftTime = Math.round((endTime - Date.now()) / 1000);
    console.log(`倒计时剩余秒数:${ leftTime }`, `当前时间秒数:${ date.getSeconds() }`);
    if(leftTime <= 0) {
    clearInterval(timer);
    }
    }, 1000)

    根据以上代码进行计算,即使标签页不处于可见状态,setInterval延迟执行,对leftTime也没有影响。
    执行结果如下(标签页处于不可见状态时):
    image.png


    题外话


    用 setTimeout 实现 setInterval


    实现思路是setTimeout的递归调用。以上面的举例代码为例作修改:


    const leftTime = 600 * 1000;
    const endTime = Date.now() + leftTime; // 倒计时结束时间
    function setTimer() {
    leftTime = Math.round((endTime - Date.now()) / 1000);
    if ( leftTime <= 0 ) {
    endTime = 0;
    leftTime = 0;
    } else {
    setTimeout(setTimer, 1000);
    }
    }

    本次分享就到这,希望可以帮助到有同样困扰的小伙伴哦~


    作者:Swance
    来源:juejin.cn/post/7309693162369171507
    收起阅读 »

    初中都没念完的我,是怎么从IT这行坚持下去的...

    大家好,我是一名二线(伪三线,毕竟连续两年二线城市了)的程序员。 现阶段状态在职,28岁,工作了10年左右,码农从事了5年左右,现薪资9k左右。如文章标题所说,初二辍学,第一学历中专,自己报的成人大专。 在掘金也看了不少经历性质的文章,大多都是很多大牛的文章,...
    继续阅读 »

    大家好,我是一名二线(伪三线,毕竟连续两年二线城市了)的程序员。


    现阶段状态在职,28岁,工作了10年左右,码农从事了5年左右,现薪资9k左右。如文章标题所说,初二辍学,第一学历中专,自己报的成人大专。


    在掘金也看了不少经历性质的文章,大多都是很多大牛的文章,在大城市的焦虑,在大厂的烦恼,所以今天换换口味,看一看我这个没有学历的二线的程序员的经历。


    1.jpg


    1.辍学


    我是在初二的时候辍学不上的,原因很简单,太二笔了。


    现在想来当时的我非常的der,刚从村里的小学出来上中学之后(我还是年级第7名进中学,殊不知这就是我这辈子最好的成绩了),认为别人欺负我我就一定要还回来,完全不知道那是别人的地盘,嚣张的一批,不出意外就被锤了,但是当时个人武力还是很充沛的,按着一个往地上锤,1V7的战绩也算可以了。自此之后,我就开始走上了不良的道路,抽烟喝酒打架,直到中专毕业那天。



    我清楚的记得我推着电车望着天,心里只想着一个问题,我毕业了,要工作了,我除了打游戏还会什么呢,我要拿什么生存呢...



    这是当时我心里真实的想法,我好像就在这一刻、这一瞬间长大了。


    2.jpg


    2.深圳之旅


    因为我特别喜欢玩游戏,而且家里电脑总是出问题,所以我就来到了我们这当地的一个电脑城打工,打了半年工左右想学习一下真正的维修技术,也就是芯片级维修,毅然决然踏上了深圳的路。


    在深圳有一家机构叫做迅维的机构,还算是在业内比较出名的这么一个机构,学习主板显卡的维修,学习电路知识,学习手机维修的技术。现在的我想想当时也不太明白我怎么敢自己一个人就往深圳冲,家里人怎么拦着我都没用,当时我就好像着了魔一样必须要去...


    不过在深圳的生活真的很不错,那一年的时光仍旧是我现在非常怀念的,早晨有便宜好吃的肠粉、米粉、甜包,中午有猪脚饭、汤饭、叉烧饭,晚上偶尔还会吃一顿火锅,来自五湖四海的朋友也是非常的友好,教会了我很多东西,生活非常的不错。


    3.jpg


    3.回家开店


    为什么说我工作了10年左右呢,因为我清楚记得我18岁那年在本地开了一个小店,一个电脑手机维修的小店。现在想想我当时也是非常的二笔,以下列举几个事件:



    1. 修了一个显示器因为没接地线烧了,还跟人家顾客吵了一架。

    2. 修苹果手机翘芯片主板线都翘出来了,赔了一块。

    3. 自己说过要给人家上门保修,也忘了,人家一打电话还怼了一顿。

    4. 因为打游戏不接活儿。


    以上这几种情况比比皆是,哪怕我当时这么二笔也是赚了一些钱,还是可以维持的,唯一让我毅然决然转行的就是店被偷了,大概损失了顾客机器、我的机器、图纸、二手电脑等一系列的商品,共计7万元左右,至今仍没找回!


    4.jpg


    4.迷茫


    接下来这三年就是迷茫的几年了,第一件事就是报成人大专,主要从事的行业就杂乱无章了,跟我爸跑过车,当过网吧网管,超市里的理货员,但是这些都不是很满意,也是从这时候开始接触了C和C++开始正式踏入自学编程的路,直到有一次在招聘信息里看到java,于是在b站开始自学java,当时学的时候jdk还是1.6,学习资料也比较古老,但是好歹是入了门了。


    5.jpg


    5.入职


    在入门以后自我感觉非常良好,去应聘了一个外包公司,当时那个经理就问了我一句话,会SSM吗,我说会,于是我就这么入职了,现在想想还是非常幸运的。


    当时的我连SSM都用不明白,就懂一些java基础,会一些线程知识,前端更是一窍不通,在外包公司这两年也是感谢前辈带我做一些项目,当时自己也是非常争气,不懂就学,回去百度、b站、csdn各种网站开始学习,前端学习了H5、JS、CSS还有一个经典前端框架,贤心的Layui。


    干的这两年我除了学习态度非常认真,工作还是非常不在意,工作两年从来没有任何一个月满勤过,拖延症严重,出现问题从来就是逃避问题,职场的知识是一点也不懂,当时的领导也很包容我,老板都主持了我的婚礼哈哈哈。但是后来我也为我的嚣张买了单,怀着侥幸心理喝了酒开车,这一次事情真真正正的打醒了我,我以后不能这样了...


    6.jpg


    6.第二家公司


    在第二家公司我的态度就变了很多很多 当时已经25岁了,开始真真正正是一个大人了,遵纪守法,为了父母和家人考虑,生活方面也慢慢的好了起来(在刚结婚两年和老婆经常吵架,从这时候开始到现在没有吵过任何架了就),生活非常和睦。工作方面也是从来不迟到早退,听领导的安排,认真工作,认真学习,认识了很多同行,也得到了一些人的认可,从那开始才开始学习springboot、mq、redis、ES一些中间件,学习了很多知识,线程知识、堆栈、微服务等一系列的知识,也算是能独当一面了。但好景不长,当时我的薪资已经到13K左右了,也是因为我们部门的薪资成本、服务器成本太大,入不敷出,公司决定代理大厂的产品而不是自研了,所以当时一个部门就这么毕业了...


    7.png


    7.现阶段公司


    再一次找工作就希望去一些自研的大公司去做事情了,但是也是碍于学历,一直没有合适的,可以说是人厌狗嫌,好的公司看不上我,小公司我又不想去,直到在面试现在公司的时候聊得非常的好,也是给我个机会,说走个特批,让我降薪入职,大概每个月平均薪资10K左右(年终奖是大头),我也是本着这个公司非常的大也就来了,工作至今。


    8.jpg


    总结



    1. 任何时候想改变都不晚,改变不了别人改变自己。

    2. 面对问题绝对不能逃避,逃避没有任何用,只有面对才能更好的继续下去。

    3. 不要忘了自己为什么踏入这行,因为我想做游戏。

    4. 解决问题不要为了解决而解决,一定要从头学到尾,要不然以后出现并发问题无从下手。

    5. 任何事情都要合规合法。

    6. 工作了不要脱产做任何事情,我是因为家里非常支持,我妈至今都难以相信我能走到今天(我认为我大部分是运气好,加上赶上互联网浪潮的尾巴)。

    7. 最重要的,任何事情都没有家人重要,想回家就回家吧,挣钱多少放一边,IT行业找个副业还是非常简单的,多陪陪他们!


    作者:妄也
    来源:juejin.cn/post/7309645869644480522
    收起阅读 »

    Java开发者必备:Maven简介及使用方法详解!

    今天我们来介绍一个在Java开发中非常重要的工具——Maven。如果你是一名Java开发者,那么你一定不会对Maven感到陌生。但是,对于一些新手来说,可能还不太了解Maven是什么,它有什么作用,以及如何使用它。接下来,就让我们一起来深入了解一下Maven吧...
    继续阅读 »

    今天我们来介绍一个在Java开发中非常重要的工具——Maven。如果你是一名Java开发者,那么你一定不会对Maven感到陌生。但是,对于一些新手来说,可能还不太了解Maven是什么,它有什么作用,以及如何使用它。接下来,就让我们一起来深入了解一下Maven吧!

    一、maven简介

    Maven是什么

    Maven是一个项目管理工具,它包含了一个项目对象模型 (Project Object Model),一组标准集合,一个项目生命周期(Project Lifecycle),一个依赖管理系统(Dependency Management System),和用来运行定义在生命周期阶段(phase)中插件(plugin)目标(goal)的逻辑。maven是基于Ant 的构建工具,Ant 有的功能Maven 都有,额外添加了其他功能。

    Maven提供了一套标准化的项目结构,所有IDE使用Maven构建的项目结构完全一样,所有IDE创建的Maven项目可以通用。

    Maven是专门用于管理和构建Java项目的工具,它的主要功能有:

    • 提供了一套标准化的项目结构

    • 提供了一套标准化的构建流程(编译、测试、打包、发布 …)

    • 提供了一套依赖管理机制

    Maven作用

    • 项目构建管理:maven提供一套对项目生命周期管理的标准,开发人员、和测试人员统一使用maven进行项目构建。项目生命周期管理:编译、测试、打包、部署、运行。

    • 管理依赖(jar包):maven能够帮我们统一管理项目开发中需要的jar包。

    • 管理插件:maven能够帮我们统一管理项目开发过程中需要的插件。

    二、maven仓库

    用过maven的同学,都知道maven可以通过pom.xml中的配置,就能够获取到想要的jar包,但是这些jar是在哪里呢?就是我们从哪里获取到的这些jar包?答案就是仓库。

    仓库分为:本地仓库、第三方仓库(私服)和中央仓库。
    Description

    1、本地仓库

    本地仓库:计算机中一个文件夹,自己定义是哪个文件夹。Maven会将工程中依赖的构件(Jar包)从远程下载到本机的该目录下进行管理。

    maven默认的仓库是$user.home/.m2/repository目录。

    本地仓库的位置可以在$MAVEN_HOME/conf/setting.xml文件中修改。

    在文件中找到localRepository目录,修改对应内容即可
    <localRepository>D:/maven/r2/myrepository</localRepository>

    2、中央仓库

    中央仓库:网上地址https://repo1.maven.org/maven2/

    这个公共仓库是由Maven自己维护,里面有大量的常用类库,并包含了世界上大部分流行的开源项目构件。工程依赖的jar包如果本地仓库没有,默认从中央仓库下载。

    由于maven的中央仓库在国外,所以下载速度比较慢,所以需要配置国内的镜像地址。

    在配置文件中找到mirror标签,添加以下内容即可。

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

    3、第三方仓库(私服)

    第三方仓库,又称为内部中心仓库,也称为私服。

    私服:一般是由公司自己设立的,只为本公司内部共享使用。它既可以作为公司内部构件协作和存档,也可作为公用类库镜像缓存,减少在外部访问和下载的频率,公司单独开发的私有jar可放置到私服中。(使用私服为了减少对中央仓库的访问)

    注意:连接私服,需要单独配置。如果没有配置私服,默认不使用

    三、Maven的坐标

    什么是坐标?
    Maven中的坐标是资源的唯一标识,使用坐标来定义项目或引入项目中需要的依赖。
    Description

    Maven坐标的主要组成:

    • groupId:定义当前Maven项目隶属组织名称(通常是域名反写,例如:com.baidu)

    • artifactId:定义当前Maven项目名称(通常是模块名称,例如 order-service、goods-service)

    • version:定义当前项目版本号

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

    四、Maven的三套生命周期

    什么是生命周期

    在Maven出现之前,项目构建的生命周期就已经存在,软件开发人员每天都在对项目进行清理,编译,测试及部署。虽然大家都在不停地做构建工作,但公司和公司间,项目和项目间,往往使用不同的方式做类似的工作。
    Description
    Maven的生命周期就是为了对所有的构建过程进行抽象和统一。Maven从大量项目和构建工具中学习和反思,然后总结了一套高度完美的,易扩展的生命周期。

    这个生命周期包含了项目的清理,初始化,编译,测试,打包,集成测试,验证,部署和站点生成等几乎所有构建步骤。

    Maven的生命周期是抽象的,这意味着生命周期本身不做任何实际工作,在Maven的设计中,实际任务(如源代码编译)都交由插件来完成。

    Maven的三套生命周期

    Maven拥有三套相互独立的生命周期,分别是clean,default和site。

    Description

    clean生命周期

    clean生命周期的目的是清理项目,它包含三个阶段:

    • pre-clean 执行一些清理前需要完成的工作

    • clean 清理上一次构建生成的文件

    • post-clean 执行一些清理后需要完成的工作

    default生命周期

    default生命周期定义了真正构建项目需要执行的所有步骤,它是所有生命周期中最核心的部分。其中的重要阶段如下:

    • compile :编译项目的源码,一般来说编译的是src/main/java目录下的java文件至项目输出的主classpath目录中

    • test :使用单元测试框架运行测试,测试代码不会被打包或部署

    • package :接收编译好的代码,打包成可以发布的格式,如jar和war

    • install:将包安装到本地仓库,供其他maven项目使用

    • deploy :将最终的包复制到远程仓库,供其他开发人员或maven项目使用

    site生命周期

    site生命周期的目的是建立和发布项目站点,maven能够基于pom文件所包含的项目信息,自动生成一个友好站点,方便团队交流和发布项目信息。该生命周期中最重要的阶段如下:

    • site :生成项目站点文档

    • Maven生命周期相关命令

    • mvn clean:调用clean生命周期的clean阶段,清理上一次构建项目生成的文件

    • mvn compile :编译src/main/java中的java代码

    • mvn test :编译并运行了test中内容

    • mvn package:将项目打包成可发布的文件,如jar或者war包;

    • mvn install :发布项目到本地仓库

    Maven生命周期相关插件

    Maven的核心包只有几兆大小,核心包中仅仅定义了抽象的生命周期。生命周期的各个阶段都是由插件完成的,它会在需要的时候下载并使用插件,例如我们在执行mvn compile命令时实际是在调用Maven的compile插件来编译。

    我们使用IDEA创建maven项目后,就不需要再手动输入maven的命令来构建maven的生命周期了。IDEA给每个maven构建项目生命周期各个阶段都提供了图形化界面的操作方式。

    具体操作如下:

    • 打开Maven视图:依次打开Tool Windows–>Maven Projects

    • 执行命令:双击Lifecycle下的相关命令图标即可执行对应的命令(或者点击运行按钮)

    Description

    五、maven的版本规范

    maven使用如下几个要素来唯一定位某一个jar:

    • Group ID:公司名。公司域名倒着写

    • Artifact ID:项目名

    • Version:版本

    发布的项目有一个固定的版本标识来指向该项目的某一个特定的版本。maven在版本管理时候可以使用几个特殊的字符串SNAPSHOT,LATEST ,RELEASE。比如"1.0-SNAPSHOT"。

    各个部分的含义和处理逻辑如下说明:

    • SNAPSHOT 正在开发中的项目可以用一个特殊的标识,这种标识给版本加上一个"SNAPSHOT"的标记。

    • LATEST 指某个特定构件的最新发布,这个发布可能是一个发布版,也可能是一个snapshot版,具体看哪个时间最后。

    • RELEASE 指最后一个发布版。

    六、maven项目之间的关系

    依赖关系

    • 标签把另一个项目的jar引入到当过前项目

    • 自动下载另一个项目所依赖的其他项目

    Description

    继承关系

    • 父项目是pom类型,子项目jar 或war,如果子项目还是其他项目的父项目,子项目也是pom 类型。

    • 有继承关系后,子项目中出现标签

    • 如果子项目和和与父项目相同,在子项目中可以不配置和父项目pom.xml 中是看不到有哪些子项目,在逻辑上具有父子项目关系。

    父项目
    <groupId>cn.zanezz.cn</groupId>
    <artifactId>demoparent</artifactId>
    <packaging>pom</packaging>
    <version>1.0-SNAPSHOT</version>
    子项目
    <parent>
    <artifactId>demoparent</artifactId>
    <groupId>cn.zanezz.cn</groupId>
    <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <packaging>war</packaging>
    <artifactId>child2</artifactId>

    聚合关系

    • 前提是继承关系,父项目会把子项目包含到父项目中。

    • 子项目的类型必须是Maven Module 而不是maven project

    • 新建聚合项目的子项目时,点击父项目右键新建Maven Module

    子项目中的pom.xml
    <parent>
    <artifactId>demoparent</artifactId>
    <groupId>cn.zanezz.cn</groupId>
    <version>1.0-SNAPSHOT</version>
    </parent>
    父项目中的pom.xml
    <groupId>cn.zanezz.cn</groupId>
    <artifactId>demoparent</artifactId>
    <packaging>pom</packaging>
    <version>1.0-SNAPSHOT</version>
    <modules>
    <module>child1</module>
    <module>child2</module>
    </modules>

    聚合项目和继承项目区别

    • 在语意上聚合项目父项目和子项目关系性较强;

    • 在语意上单纯继承项目父项目和子项目关系性较弱。

    Maven是一个非常强大的工具,它可以帮助我们更好地管理和构建Java项目。如果你是Java开发者,那么你一定不能错过这个工具。希望这篇文章能帮助你更好地理解和使用Maven,祝你在Java开发的道路上越走越远!

    收起阅读 »

    JS: function前面加!,引发思考🤔

    web
    简介 我们基本都知道,函数的声明方式有这两种 function msg(){alert('msg');}//声明式定义函数 var msg = function(){alert('msg');}//函数赋值表达式定义函数 但其实还有第三种声明方式,Func...
    继续阅读 »

    简介


    我们基本都知道,函数的声明方式有这两种


    function msg(){alert('msg');}//声明式定义函数

    var msg = function(){alert('msg');}//函数赋值表达式定义函数

    但其实还有第三种声明方式,Function构造函数


    var msg = new function(msg) {
    alert('msg')
    }

    等同于


    function msg(msg) {
    alert('msg')
    }

    函数的调用方式通常是方法名()

    但是,如果我们尝试为一个“定义函数”末尾加上(),解析器是无法理解的。


    function msg(){
    alert('message');
    }();//解析器是无法理解的

    定义函数的调用方式应该是 print(); 那为什么将函数体部分用()包裹起来就可以了呢?

    原来,使用括号包裹定义函数体,解析器将会以函数表达式的方式去调用定义函数。 也就是说,任何能将函数变成一个函数表达式的作法,都可以使解析器正确的调用定义函数。而 ! 就是其中一个,而 + - || ~ 都有这样的功能。


    但是请注意如果用括号包裹函数体,然后立即执行。这种方式只适用一次性调用该函数,涉及到了一个作用域问题,当你想复用该函数的时候,会如下问题:


    image.png

    可如果你想复用该函数的话,就可按先声明函数,然后再调用函数,在同一个父级作用域下,可以复用该函数,如下:


    var msg = function(msg) {}
    msg();

    关于这个问题,后面会进一步分析


    function前面加 ! ?


    自执行匿名函数:


    在很多js代码中我们常常会看见这样一种写法:


    (function( window, undefined ) {
    // code
    })(window);

    这种写法我们称之为自执行匿名函数。正如它的名字一样,它是自己执行自己的,前一个括号是一个匿名函数,后一个括号代表立即执行


    前面也提到 + - || ~这些运算符也同样有这样的功能


    (function () { /* code */ } ()); 
    !function () { /* code */ } ();
    ~function () { /* code */ } ();
    -function () { /* code */ } ();
    +function () { /* code */ } ();

    image.png

    ① ( ) 没什么实际意义,不操作返回值


    ② ! 对返回值的真假取反


    ③ 对返回值进行按位取反(所有正整数的按位取反是其本身+1的负数,所有负整数的按位取反是其本身+1的绝对值,零的按位取反是 -1。其中,按位取反也会对返回值进行强制转换,将字符串5转化为数字5,然后再按位取反。
    false被转化为0,true会被转化为1。
    其他非数字或不能转化为数字类型的返回值,统一当做0处理)


    ④ ~
    +、- 是对返回值进行数学运算 ( 可见返回值不是数字类型的时候 +、- 会将返回值进行强制转换,字符串强制转换后为NaN)


    先从IIFE开始介绍 (注:这个例子是参考网上


    IIFE(Imdiately Invoked Function Expression 立即执行的函数表达式)


    function(){
    alert('IIFE');
    }

    把这个代码放在console中执行会报错


    image.png


    因为这是一个匿名函数,要想让它正常运行就必须给个函数名,然后通过函数名调用。

    其实在匿名函数前面加上这些符号后,就把一个函数声明语句变成了一个函数表达式,是表达式就会在script标签中自动执行


    所以现在很多对代码压缩和编译后,导出的js文件通常如下:


    (function(e,t){"use strict";function n(e){var t=e.length,n=st.type(e);return st.isWindow(e)?!1:1===e.nodeType&&t?!0:"array"===n||"function"!==n&&(0===t||"number"==typeof t&&t>0&&t-1 in e)}function r(e){var t=Tt[e]={};return st.each(e.match(lt)||[],function(e,n){t[n]=!0}),t}function i(e,n,r,i){if(st.acceptData(e)){var o,a,s=st.expando,u="string"==typeof n,l=e.nodeType,c=l?st.cache:e,f=l?e[s]:e[s]&&s;if(f&&c[f]&&(i||c[f].data)||!u||r!==t)return f||(l?e[s]=f=K.pop()||st.guid++:f=s),c[f]||(c[f]={},l||(c[f].toJSON=st.noop)),("object"==typeof n||"function"==typeof n)&&(i?c[f]=st.extend(c[f],n):c[f].data=st.extend(c[f].data,n)),o=c[f],i||(o.data||(o.data={}),o=o.data),r!==t&&(o[st.camelCase(n)]=r),u?(a=o[n],null==a&&(a=o[st.camelCase(n)])):a=o,a}}function o(e,t,n){if(st.acceptData(e)){var r,i,o,a=e.nodeType,u=a?st.cache:e,l=a?e[st.expando]:st.expando;if(u[l]){if(t&&(r=n?u[l]:u[l].data)){st.isArray(t)?t=t.concat(st.map(t,st.camelCase)):t in r?t=[t]:(t=st.camelCase(t),t=t in r?[t]:t.split(" "));for(i=0,o=t.length;o>i;i++)delete r[t[i]];if(!(n?s:st.isEmptyObject)(r))return}(n||(delete u[l].data,s(u[l])))&&(a?st.cleanData([e],!0):st.support.deleteExpando||u!=u.window?delete u[l]:u[l]=null)}}}function a(e,n,r){if(r===t&&1===e.nodeType){var i=""+n.replace(Nt,"-$1").toLowerCase();if(r=e.getAttribute(i),"string"==typeof r){try{r="true"===r?!0:"false"===r?!1:"null"===r?null:+r+""===r?+r:wt.test(r)?st.parseJSON(r):r}catch(o){}st.data(e,n,r)}else r=t}return r}function s(e){var t;for(t in e)if(("data"!==t||!st.isEmptyObject(e[t]))&&"toJSON"!==t)return!1;return!0}function u(){return!0}function l(){return!1}function c(e,t){do 

    运算符


    也许这里有人会疑惑,运算符为何能将声明式函数,转译成函数表达式,这里就涉及到了一个概念解析器


    程序在运行之前需要经过编译或解释的过程,把源程序翻译成为字节码,但是在翻译之前,需要把字符串形式的程序源码解析为语法树或者抽象语法树等数据结构,这就需要用到解析器


    那么什么是解析器?


    所谓解析器(Parser),一般是指把某种格式的文本(字符串)转换成某种数据结构的过程。最常见的解析器(Parser),是把程序文本转换成编译器内部的一种叫做抽象语法树(AST)的数据结构,此时也叫做语法分析器(Parser)。也有一些简单的解析器(Parser),用于处理CSV、JSON,XML之类的格式


    JS解析器在执行第一步预解析的时候,会从代码的开始搜索直到结尾,只去查找var、function和参数等内容。一般把第一步称之为“JavaScript的预解析”。而且,当找到这些内容时,所有的变量,在正式运行代码之前,都提前赋了一个值:未定义;所有的函数,在正式运行代码之前,都是整个函数块。让解析器识别到是一个表达式,那就得加上特殊符号来让其解析器识别出来,比如刚才提到的特殊运算符。


    解析过程大致如下:


    1、“找一些东西”: var、 function、 参数;(也被称之为预解析)


    备注:如果遇到重名分为以下两种情况:遇到变量和函数重名了,只留下函数;遇到函数重名了,根据代码的上下文顺序,留下最后一个。


    2、逐行解读代码。


    备注:表达式可以修改预解析的值 (可以自行查阅文档,这就是后面说到的内容)


    函数声明与函数定义


    函数声明
    一般相对规范的声明形式为:fucntion msg(void) 注意是有分号


    function msg() 

    函数定义 function msg()注意没有分号


    {
    alert('IIFE');
    }

    函数调用


    这样是一个函数调用


    msg();

    函数声明加一个()就可以调用函数了


    function msg(){
    alert('IIFE');
    }()

    就这样

    但是我们按上面在console中执行发现出错了


    image.png


    因为这样的代码混淆了函数声明和函数调用,以这种方式声明的函数 `msg`,就应该以 `msg()` 的方式调用。

    若改成(function msg())()就是这样的一个结构体: (函数体)(IIFE),能被Javascript的解析器识别并正常执行


    从Js解析器的预解析过程了解到:


    解析器都能识别一种模式:使用括号封装函数。对于解析器来说,这几乎总是一个积极的信号,即函数需要立即执行。如果解析器看到一个左括号,紧接着是一个函数声明,它将立即解析这个函数。可以通过显式地声明立即执行的函数来帮助解析器加快解析速度


    那么也就是说,括号的作用,就是将一个函数声明,让解析器识别为一个表达式,最后由程序执行这个函数


    总结


    任何消除函数声明和函数表达式间歧义的方法,都可以被Javascript解析器正确识别


    赋值,逻辑,甚至是逗号,各种操作符,只要是解析器支持且用来识别的特殊符号都可以用作消除歧义的方式方法,而!function()(function()), 都是其中转换成表达式的一种方式。


    测试


    至于优先使用哪一个,推荐(), 而其他运算符,相对于多了一步执行步骤,比如+(表达式),那就是,立即执行+运算符运算,
    大致测了一下:


    image.png


    结论


    从测试结果的截图中我们能大致的看到,(IIFE)方式,比运算符快的是一个级别(进一位数的速度),如果说立即执行()的时间复杂度是O(n),那么运算符就是O(10n),当然这也只是粗略的测试,而且在现有的浏览器解析速度,时间基数小到可以忽略不计,所以看个人需求,写法就是萝卜白菜,大家各有所好,看个人


    作者:糖墨夕
    来源:juejin.cn/post/7203734711780081722
    收起阅读 »

    重新认识下网页水印

    web
    使用背景图图片 单独使用 css 实现,使用 backgroundImage,backgroundRepeat 将背景图片平铺到需要加水印的容器中即可。 如果希望实现旋转效果,可以借助伪元素,将背景样式放到伪元素中,旋转伪元素实现: <style>...
    继续阅读 »

    使用背景图图片


    单独使用 css 实现,使用 backgroundImage,backgroundRepeat 将背景图片平铺到需要加水印的容器中即可。
    如果希望实现旋转效果,可以借助伪元素,将背景样式放到伪元素中,旋转伪元素实现:


    <style>
    .watermark {
    position: relative;
    overflow: hidden;
    background-color: transparent;
    }
    .watermark::before {
    content: '';
    position: absolute;
    width: 160%;
    height: 160%;
    top: -20%;
    left: -20%;
    z-index: -1;
    background-image: url('./watermark.png');
    background-position: 0 0;
    background-origin: content-box;
    background-attachment: scroll;
    transform: rotate(-20deg);
    background-size: auto;
    background-repeat: round;
    opacity: 0.3;
    pointer-events: none;
    }
    </style>

    动态生成div


    根据水印容器的大小动态生成div,div内可以任意设置文本样式和图片,借助userSelect禁止用户选中文本水印;


    const addDivWaterMark = (el, text) => {
    const { clientWidth, clientHeight } = el;
    const waterWrapper = document.createElement('div');
    waterWrapper.className = "waterWrapper";
    const column = Math.ceil(clientWidth / 100);
    const rows = Math.ceil(clientHeight / 100);
    // 根据容器宽高动态生成div
    for (let i = 0; i < column * rows; i++) {
    const wrap = document.createElement('div');
    wrap.className = "water";
    wrap.innerHTML = `<div class="water-item">${text}</div>`
    waterWrapper.appendChild(wrap)
    }
    el.append(waterWrapper)
    }

    Canvas写入图片做背景水印


    将图片写入Canvas然后将Canvas作为背景图


      const img = new Image();
    const { ctx, canvas } = createWaterMark(config);
    img.onload = function () {
    ctx.globalAlpha = 0.2;
    ctx.rotate(Math.PI / 180 * 20);
    ctx.drawImage(img, 0, 16, 180, 100);
    canvasRef.value.style.backgroundImage = `url(${canvas.toDataURL()})`
    };
    img.src = ImageBg;

    Canvas写入文字做背景水印


    将文字写入Canvas然后将Canvas作为背景图


     const canvas = document.createElement('canvas');
    canvas.width = width;
    canvas.height = height;
    const ctx = canvas.getContext('2d');
    ctx.clearRect(0, 0, width, height);
    ctx.fillStyle = fillStyle;
    ctx.globalAlpha = opacity;
    ctx.font = font
    ctx.rotate(Math.PI / 180 * rotate);
    ctx.fillText(text, 0, 50);
    return canvas

    Svg做水印


    通过svg样式来控制水印样式,再将svg转换成base64的背景图


      const svgStr =
    `<svg xmlns="http://www.w3.org/2000/svg" width="180px" height="100px">
    <text x="0px" y="30px" dy="16px"
    text-anchor="start"
    stroke="#000"
    stroke-opacity="0.1"
    fill="none"
    transform="rotate(-20)"
    font-weight="100"
    font-size="16"> 前端小书童</text>
    </svg>`
    ;
    return `data:image/svg+xml;base64,${window.btoa(unescape(encodeURIComponent(svgStr)))}`;

    shadowDom水印


    使用customElements自定义个一个标签(可以使用其他任意标签,不过注意shadow DOM会使起同级的元素不显示。)
    可以像shadow DOM写入style样式和水印节点(可以使用背景或者div形式)
    shadow DOM内部实现的样式隔离不用担心写入的style影响页面其他元素样式,这个特性在微前端的实现中也被广泛使用。


     class ShadowMark extends HTMLElement {
    constructor() {
    super();
    const shadowRoot = this.attachShadow({ mode: 'open' });
    const wrapContainer = document.createElement('div')
    const style = document.createElement('style');
    style.textContent = `
    .wrapContainer {
    width: 100%;
    height: 100%;
    display: flex;
    flex-wrap: wrap;
    }
    .watermark-item {
    display: flex;
    font-size: 16px;
    opacity: .3;
    transform: rotate(-20deg);
    user-select: none;
    white-space: nowrap;
    justify-content: center;
    align-items: center;
    }`
    ;
    const waterHeight = 100
    const waterWidth = 100
    const { clientWidth, clientHeight } = document.querySelector('.shadow-watermark')
    const column = Math.ceil(clientWidth / waterWidth)
    const rows = Math.ceil(clientHeight / waterHeight)
    wrapContainer.setAttribute('class', "wrapContainer")
    for (let i = 0; i < column * rows; i++) {
    const wrap = document.createElement('div')
    wrap.setAttribute('class', 'watermark-item')
    wrap.style.width = waterWidth + 'px'
    wrap.style.height = waterHeight + 'px'
    wrap.textContent = "前端小书童"
    wrapContainer.appendChild(wrap)
    }
    shadowRoot.appendChild(style);
    shadowRoot.appendChild(wrapContainer)
    }
    }
    customElements.define('shadow-mark', ShadowMark);

    盲水印


    canvas画布(canvas.getContext('2d'))调用 getImageData 得到一个 ArrayBuffer,用于记录画布每个像素的 rgba 值


    r: Red取值范围0255
    g: Green取值范围0
    255
    b:Blue取值范围0255
    a:Alpha 透明度取值范围0
    1,0代表全透明
    可以理解为每个像素都是通过红、绿、蓝三个颜色金额透明度来合成颜色


    方案一:低透明度方案的暗水印


    当把水印内容的透明度 opacity 设置很低时,视觉上基本无法看到水印内容,但是通过修改画布的 rgba 值,可以使水印内容显示出来。
    选择固定的一个色值例如R,判断画布R值的奇偶,将其重置为0或者255,低透明的内容就便可以显示出来了。


    const decode = (canvas, colorKey, flag, otherColorValue) => {
    const ctx = canvas.getContext('2d');
    const originalData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
    let data = originalData.data;
    for (let i = 0; i < data.length; i++) {
    //筛选每个像素点的R值
    if (i % 4 == colorKey) {
    if (data[i] % 2 == 0) {
    //如果色值为偶数
    data[i] = flag ? 255 : 0;
    } else {
    //如果色值为奇数
    data[i] = flag ? 0 : 255;
    }
    } else if (i % 4 == 3) {
    //透明度不作处理
    continue;
    } else {
    // 关闭其他色值
    if (otherColorValue !== undefined) {
    data[i] = otherColorValue
    }
    }
    }
    ctx.putImageData(originalData, 0, 0);
    }

    方案二:将水印内容以像素偏差记录到画布中


    用画布和水印后的画布绘制的像素进行ArrayBuffer对比,在存在水印像素的位置(水印画布透明度不为0)修改图片画布的奇偶,这样通过上面指定色值和奇偶去解码时,修改的文本像素就会被显示出来;


    const encode = (ctx, textData, color, originalData) => {
    for (let i = 0; i < originalData.data.length; i++) {
    // 只处理目标色值
    if (i % 4 == color) {
    // 水印画布透明度为0
    if (textData[i + offset] === 0 && (originalData.data[i] % 2 === 1)) {
    // 放置越界
    if (originalData.data[i] === 255) {
    originalData.data[i]--;
    } else {
    originalData.data[i]++;
    }
    // 水印画布透明度不为0
    } else if (textData[i + offset] !== 0 && (originalData.data[i] % 2 === 0)) {
    originalData.data[i]++;
    }
    }
    }
    ctx.putImageData(originalData, 0, 0);
    }

    方案三:数字加密


    在图像信号的频域(变换域)中隐藏信息要比在空间域(上面得到的像素颜色的ArrayBuffer)中隐藏信息具有更好的防攻击性。
    这部分暗水印的实现,可以直接使用阿里云提供给的api,不过需要图片资源藏到的阿里云的OSS下;


    MutationObserver


    可以发现上面水印基本都是通过增加节点或者背景图的形式来实现,那用户其实可以通过屏蔽样式或者删除Dom来消除水印,那么我们可以借用MutationObserver来监听下水印dom的变化,来阻止用户以这种形式来消除水印;



    代码



    以上代码见:github.com/wenjuGao/wa…


    线上效果:watermark-demo.vercel.app/



    参考:



    http://www.cnblogs.com/88223100/p/…


    blog.csdn.net/bluebluesky…


    developer.mozilla.org/zh-CN/docs/…


    作者:前端小书童
    来源:juejin.cn/post/7208465670991872061
    收起阅读 »

    人走茶凉?勾心斗角?职场无友谊?

    你和同事之间存在竞争关系 要不要把工作关系维护成伙伴关系 明枪暗箭防不胜防 背后捅刀子往往最不设防 大家是否在职场上交友是有也遇到过以上困扰呢? 不要在职场上交“朋友”,而是要寻找“盟友”。 这两者的区别在于应对策略: 我们会愿意为“朋友”牺牲自己的利益,像是...
    继续阅读 »

    你和同事之间存在竞争关系


    要不要把工作关系维护成伙伴关系


    明枪暗箭防不胜防


    背后捅刀子往往最不设防


    大家是否在职场上交友是有也遇到过以上困扰呢?


    不要在职场上交“朋友”,而是要寻找“盟友”。


    这两者的区别在于应对策略:


    我们会愿意为“朋友”牺牲自己的利益,像是一张年卡。


    而结交“盟友”就是为了一起争取更多利益,《孔乙己》说得好:“这次是现钱,酒要好。”


    所以,在职场上的“受欢迎”和社交场、朋友圈上的“受欢迎”之间有着本质的区别:


    你和你的同事未必真心喜欢彼此,但在日常相处当中能够客气、友善地交往。


    大家需要寻找盟友时会第一个想到你,在争斗冲突时会尽量绕开你,这就是一种非常理想的“受欢迎”状态。 不要在职场上寻求友谊和爱,这件事是不对的。


    在这里给大家列出一个在职场上受欢迎的清单。


    1.实力在及格线以上


    这是一切的前提。职场新人要“先活下来,再做兄弟”,稳住了工作能力这个基本面,才有资格和同事谈交情。


    实力不够的人会拖累整个团队、增加所有人的工作量,大家恨都来不及,绝对不会和他称兄道弟。


    实力强可以表现为实力本身,在初级职位上,也可以表现为潜力。


    极少数特别强大的人可能从一开始就能很好地完成工作,但是大部分人在新加入一个团队时都需要经过一段时间的磨合,在这个过程中有欠缺和不足都是正常的,你所表现出来的敬业精神、学习能力和进步的速度才是大家对你进行评价的关键。


    刚入职的新人,对于要做的事情完全没有概念,但是为人极勤奋又上进,给他布置的任务会完成得特别扎实,每一天都在飞快地进步。这样的人在职场上永远都能收获一大把来自他人的橄榄枝。


    2.比较高的自尊水平


    高自尊的人对自己评价高,要求也高,又能够带着欣赏的眼光去看周围的人,他们不光是很好的父母、伴侣和朋友,同时也是职场上最好的结盟对象。


    高自尊的人往往拥有很多优秀的品质,同时他们也能够理解“大局”,和他们合作不用在鸡毛蒜皮的细节上纠缠推诿,可以把精力全部用来开疆拓土,极大地降低团队的内耗。


    如果你是一个高自尊的人,在日常生活中表现出了自律和很好的品行,就会收获高自尊同类的赞赏。有些低自尊的人可能会认为你的言行是在“装X”,别犹豫,把他们从你的结交名单当中划掉,高自尊会帮你筛掉一批最糟糕的潜在合作者。


    如果你是一个部门的领导者,记得要维护高自尊的下属,他们都是潜在的优秀带队者,给他们一个位子就可以坐上来自己动,给他们一点精神鼓励和支持,他们就会变得无所不能。


    即使高自尊的手下可能某些地方让你感到嫉妒或者冒犯(这是常见的,嫉妒是每个人都一定会有的情感),也绝对不要默许或者纵容低自尊的妄人跑去伤害他们,否则会伤了大家的心,事业就难以成功了。


    “朕可以敲打丞相,但你算什么东西”就是对这种低自尊妄人最好的态度。


    3.嘴严,可靠


    在任何一个群体当中,多嘴多舌的人都不会受到尊重,而在职场上,嘴不严尤其危险。


    如果你是一个爱说是非的人,围绕在你周围的只会是一帮同样没正事、低级趣味的家伙。你会被打上“不可靠”的标记,愿意和你交流的人越来越少,大家等着看你什么时候因为多嘴闯祸,而强者根本不会和你为伍。


    有些同学曾经给我留言说,自己很内向,不知道如何跟同事拉近关系。内向的人最适合强调自己的“嘴严”和“可靠”,在职场上,这两项品质远比“能说会道”更让人喜欢。


    4.随和,有分寸


    体面的人不传闲话,也不会轻易对旁人发表议论。


    “思想可以特立独行,生活方式最好随大流”,这是对自己的要求,而他人的生活方式是不是合理,不是我们能评价的。


    哪怕是最亲近的人,都未必能知晓对方的全部经历和心里藏着的每一件小事。在职场上大家保持着客气有礼的距离,就更不可能了解每个人做事的出发点和逻辑,“看不懂”是正常的,但是完全没有必要“看不惯”。如果还要大发议论,把自己的“看不惯”到处传播,你的伙伴就只会越来越少。


    有人说在北上广深这样的大城市,人和人之间距离遥远,缺人情味,太冷漠。


    这不是冷漠,而是对“和自己不一样”的宽容,这份宽容就是我们在向文明社会靠拢的标志。


    5.懂得如何打扮


    还记得斯大林的故事吗?在他离开校园之后,从头到脚都经过精心设计,不是为了精神好看,而是要让自己看起来就像一位投身革命事业的进步青年。


    有句老话叫做“先敬罗衣后敬人”,本意是讽刺那些根据衣饰打扮来评价一个人的现象。我们自己在做判断的时候要尽量避免受到这类偏见的影响,但是对他人可能存在的偏见一定要心中有数。人是视觉动物,穿着打扮是“人设(人物设定)”的一部分,在我们开口说话之前,衣饰鞋袜就已经传达了无数信息。


    想要成为职场当中受欢迎的人,穿着打扮的风格就要和公司的调性保持一致,最安全的做法是向你的同事靠拢。


    在一个风格统一的群体当中,“与众不同”这件事自带攻击性。如果在事业单位之类的上年纪同事比较多的地方上班,马卡龙色的衣服和颜色夸张的口红,最好等到下班时间再上身。


    这不是压抑天性,而是自我保护和职业精神。


    6.和优秀的人站在一起


    在职场上,优秀的人品质都是相似的:勤奋,自律,不断精进。如果发现了这样的同事,就要尽量和他们保持良好关系。


    但是,单纯的日常沟通并不足以让你们成为盟友,正式结盟往往是通过利益交换和分享:当你遇到棘手的工作任务,就可以主动邀请对方共同跟进,同时将一部分利益让出去。愉快的合作是关系飞跃的最好契机。


    优秀的人能认可的,通常也都是自己的同类。如果你能获得他们的称许和背书,在同事当中的地位自然会有所提升。


    7.知道如何求助


    前两天有一位关系户同学留言说,自己即将去实习,因为家人的关系可以得到一些行业资深专家的指点,问自己应该如何表现,是不是不懂就要问,像“好奇宝宝”一样,对方就会觉得自己好学上进。


    我告诉她说,不要上去就问,有任何疑惑都先用搜索引擎找一下答案,如果找不出来,再带着你搜到的细节去询问那些资深前辈。


    互联网时代有个很大的变化,就是人们获取信息的成本大大降低。善用搜索引擎寻找答案,就能更快、更精准、更全面地找到自己想要的东西,这种方式比跑到对方工位边用嘴问效率高得多。


    凡事都问,只会让人觉得你的文字阅读能力有限,同时既不把自己的时间当回事,也不尊重别人的时间。尤其对方还是行业中的专家,他们的时间一定比实习生的宝贵多了。如果网上找不到答案,再带着细节去仔细咨询,这样的请教才是高效的,才能证明你是一个“好学上进”的人。


    职场不是校园,不会再有一群老师专门负责手把手地教你,不轻易占用其他同事的时间会让你成为一个自立、有分寸、受尊重的人。毕业之后,你取得进步的速度、最终的上升空间,都和使用搜索引擎寻找答案的能力呈正相关。


    8.技巧地送出小恩小惠


    小恩小惠带两个“小”字,并不意味着这是一种微末小技。事实上,即使是最普通的零食,只要讲究得法,都可以送到人心里。


    你的同事当中有没有因为宗教信仰而忌口的情况?


    甲和乙爱吃辣,丙和丁爱吃甜,是不是两种口味都来上一点?


    要留心同事的自我暴露,最好是用一个小本本记下来,关键时刻可能派上大用场。大家都是成年人,不会像孩子一样轻易被小恩小惠打动,打动我们的往往是“你把我放在心上”的温暖。


    9.良好的情绪管理能力


    很多时候这是个隐藏特征,但是自带“一票否决”属性:平时表现得沉着稳重,周围同事们不会有特别明显的感觉,然而歇斯底里和失控只要有一次,之前苦心经营的人设就会全面崩塌。情绪不稳定的人一般没人敢惹,但是也没人会在意了:你会被视为一个“病人”,很难再有大的发展。


    已经发泄出去的情绪不能收回来,这个时候不要反复陷入纠结和悔恨,待在情绪里不出来,钱花出去了就不要去想,不要去比价。


    如果情绪失控了,应该立刻做到的是原谅自己,然后考虑如何不再有下一次失控。要知道大多数人一辈子都至少会换三四次工作,了不起是换个地方,重新再来。


    有的人特别幸运,天生长得好看,容易被人喜欢。


    如果不是让人眼前一亮的高颜值人士,就不要太心急了。


    成为一个自律、行为可以预期的人,也能慢慢地被别人喜欢。


    人生很长,被人喜欢这件事,我们不用赶时间。


    作者:程序员小高
    来源:juejin.cn/post/7255589558996992059
    收起阅读 »

    你的代码着色好看吗?来这里看看吧!

    web
    如果你想在网页上展示一些代码,你可能会遇到一个问题:代码看起来很单调,没有任何颜色或格式,这样的代码不仅不美观,也不利于阅读和理解。 那么,有没有什么办法可以让代码变得更漂亮呢?答案是有的,而且很简单。 你只需要使用一个叫做 highlight.js 的第三方...
    继续阅读 »

    如果你想在网页上展示一些代码,你可能会遇到一个问题:代码看起来很单调,没有任何颜色或格式,这样的代码不仅不美观,也不利于阅读和理解。


    那么,有没有什么办法可以让代码变得更漂亮呢?答案是有的,而且很简单。


    你只需要使用一个叫做 highlight.js 的第三方库,就可以轻松实现代码着色的效果。



    highlight.js 是一个非常强大和流行的库,它可以自动识别和着色超过 190 种编程语言。


    它支持多种主题和样式,让你可以根据自己的喜好选择合适的配色方案。


    在本文中,子辰将向你介绍如何使用 highlight.js 来为你的代码着色,以及它的基本原理和优势。


    让我们开始吧!


    如何使用 highlight.js


    使用 highlight.js 的方法有两种:一种是通过 npm 下载并安装到你的项目中,另一种是通过 CDN 引入到你的网页中。


    这里我们以 CDN 的方式为例,如果你想使用 npm 的方式,可以参考官方文档。


    首先,我们需要在网页中引入 highlight.js 的 JS 文件和 CSS 文件。


    JS 文件是核心文件,负责识别和着色代码,CSS 文件是样式文件,负责定义代码的颜色和格式。



    我们可以从 CDN 中选择一个合适的 JS 文件和 CSS 文件。


    highlight.js 提供了多个 CDN 服务商,你可以根据自己的需求选择一个,这里我们以 jsDelivr 为例。


    JS 文件的链接如下:


    <script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release/build/highlight.min.js"></script>

    CSS 文件的链接则需要根据你想要的主题来选择。


    highlight.js 提供了很多主题,你可以在官网上预览每个主题的效果,并找到对应的 CSS 文件名,这里我们以 github-dark 为例。


    CSS 文件的链接如下:


    <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release/build/styles/github-dark.min.css">

    将上面两个链接分别放到网页的 head 标签中,就完成了引入 highlight.js 的步骤。


    接下来,我们需要在网页中写一些代码,并用 pre 标签和 code 标签包裹起来。


    pre 标签用于保留代码的格式,code 标签用于标识代码内容。例如:


    <pre>
    <code id="code-area">
    * {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
    }
    body {
    display: flex;
    justify-content: center;
    align-items: center;
    gap: 20px;
    height: 100vh;
    width: 100vw;
    }
    </code>
    </pre>

    注意,我们给 code 标签添加了一个 id 属性,方便后面通过 JS 获取它。


    最后,我们需要在网页中添加一些 JS 代码,来调用 highlight.js 的方法,实现代码着色的功能。


    highlight.js 提供了两个主要的方法:highlightElement 和 highlight。


    这两个方法都可以实现代码着色的效果,但是适用于不同的场景。


    highlightElement


    highlightElement 方法适用于当你的代码是直接写在网页中的情况。


    这个方法接受一个元素作为参数,并将该元素内部的文本内容进行着色处理。例如:


    // 获取 code 元素
    const codeEle = document.getElementById("code-area");
    // 调用 highlightElement 方法,传入 code 元素
    hljs.highlightElement(codeEle);

    如果一切顺利,你应该能看到类似下图的效果:



    代码已经被着色了,并且你可以看到代码被替换成了一个个标签,标签被加上了样式。


    在最后的原理里我们在详细的说一下。


    highlight


    highlight 方法适用于当你的代码是通过 Ajax 请求获取到的纯文本数据的情况。


    这个方法接受一个字符串作为参数,并返回一个对象,包含着色后的文本内容和代码的语言。例如:


    <script>
    const codeEle = document.getElementById('code-area')
    // 比如说现在 code 就是 Ajax 返回的数据,lang 就是代码语言,content 就是代码内容
    const code = {
    lang: 'css',
    content: `
    * {
    margin: 0;
    padding: 0;
    }`

    }
    // 我们接下来可以使用 hljs.highlight,将代码内容与代码语言传入进去
    const result = hljs.highlight(code.content, {
    language: code.lang
    })
    // 它会返回一个结果,我们打印到控制台看看
    console.log('result >>> ', result)
    </script>


    我们可以看到,打印出来的是一个对象,code 是它原始的代码,language 是它的语言,而 value 就是它着色后的代码。


    那么现在要做的就是将 value 添加到 code 元素里边去。


    <script>
    const code = {
    lang: 'css',
    content: `
    * {
    margin: 0;
    padding: 0;
    }`

    }
    const result = hljs.highlight(code.content, {
    language: code.lang
    })
    const codeEle = document.getElementById('code-area')
    codeEle.innerHTML = result.value
    </script>


    我们可以看到,代码确实被着色了,但是和之前的有所差别,我们看一下是什么原因。



    打开控制后我们发现,用这种方式 code 元素就没有办法被自动加上类样式了,所以说我们就需要手动给 code 加上类样式才可以。


    // 通过 className 为 code 手动添加类样式,并添加类的语言
    codeEle.className = `hljs language-${code.lang}`

    highlight.js 的语言支持


    无论使用哪种方法,都需要注意指定代码所属的语言。


    如果不指定语言,highlight.js 会尝试自动识别语言,并可能出现错误或不准确的结。


    指定语言可以通过两种方式:



    • 在 code 标签中添加 class 属性,并设置为 language-xxx 的形式,其中 xxx 是语言名称。

    • 在调用 highlightElement 或 highlight 方法时,在第二个参数中传入一个对象,并设置 language 属性为语言名称。



    上图是 highlight.js 支持的语言,可以看到有很多种,需要用其他语言的时候,language 设置成指定的语言名称就可以了。


    原理


    它的原理你可能已经猜到了,在 highlightElement 里我们简单说了一下,现在再看下图:



    之所以可以实现着色,其实就是查找和替换的过程,将原来的纯文本替换为元素标签包裹文本,元素是可以加上样式的,而样式就是我们引入的 css 文件。


    这就是它的基本原理了。


    总结


    其实有时候我们去设置 Mackdown 的自定义样式呢,在代码区域设置的时候也是这样设置的,当然类样式的名字呢,基本上都是标准的格式。


    好了,这个库分享介绍给你了,库的原理也为你做了简单的科普,希望对你有所帮助。


    如果你有什么问题或建议,请在评论区留言,如果你觉得这篇文章有用,请点赞收藏或分享给你的朋友!


    作者:子辰Web草庐
    来源:juejin.cn/post/7245584147456507965
    收起阅读 »