注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

每一个前端,都要拥有属于自己的埋点库~

web
前言 大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心~ 简介 sunshine-track 应用于前端监控, 基于 行为上报,实现了 用户行为、错误监控、页面跳转、页面白屏检测、页面性能检测等上报功能。适用于 Vu...
继续阅读 »

前言


大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心~



简介


sunshine-track 应用于前端监控, 基于 行为上报,实现了 用户行为、错误监控、页面跳转、页面白屏检测、页面性能检测等上报功能。适用于 Vue、React、Angular 等框架



本项目源码:github.com/sanxin-lin/…
各位兄弟姐妹如果觉得喜欢的话,可以点个star 哦~



功能


sunshine-track具备以下功能:



  • ✅ 用户行为上报:包括 点击、跳转页面、跳转页面记录数组、请求

  • ✅ 用户手动上报:提供 Vue 自定义指令 以及add、report函数,实现用户手动上报

  • ✅ 自定义上报:提供 格式化上报数据、自定义上报函数、自定义决定上不上报 等配置项,更灵活地上报数据

  • ✅ 请求数据上报:提供 检测请求返回、过滤请求 等配置项,让用户决定上报哪些请求数据

  • ✅ 上报方式:提供 上报方式 配置项,用户可选择 img、http、beacon 三种方式,http方式又支持 xhr、fetch 两种,且支持 自定义headers

  • ✅ 上报数据缓存:可配置 本地缓存、浏览器本地缓存、IndexedDB 三种方式

  • ✅ 上报数据阈值:可配置上报数据 阈值 ,达到 阈值 后进行上报操作

  • ✅ 全局点击上报:可通过配置 选择器、元素文本,对全局DOM节点进行点击上报

  • ✅ 页面的性能检测,包括 白屏、FP、FCP、LCP、CLS、TTFB、FID


上报数据格式


选项描述类型
uuid   上报数据的idstring
type   上报数据的类型string
data   上报数据any
time    上报时间number
status    上报状态string
domain    当前域名string
href    当前网页路径string
userAgent    当前user-agentstring
deviceInfo   设备的相关信息object

安装



使用



全局点击监听


可以通过配置globalClickListeners来对于某些DOM节点进行点击监听上报



配置上报阈值


上报分为几种:



  • 用户行为上报:点击、跳转页面、请求,这些上报数据会缓存着,当达到阈值时再进行上报

  • 错误上报:请求报错、代码报错、异步错误,这些是立即上报

  • 页面性能上报:白屏、FP、FCP、LCP、CLS、TTFB、FID,这些是立即上报


用户行为上报的阈值默认是 10,支持自定义 maxEvents



配置缓存方式


如果你想要避免用户重新打开网页之后,造成上报数据的丢失,那么你可以配置缓存方式,通过配置cacheType



  • normal:默认,本地缓存

  • storage:浏览器 localStorage 本地缓存

  • db:浏览器 IndexedDB 本地缓存


app.use(Track, {
...options,
cacheType: 'storage' // 配置缓存方式
})

打印上报数据


可以通过配置 log ,开启打印上报数据



灵活上报请求数据


请求也是一种行为,也是需要上报的,或许我们有这个需求



  • 过滤:某些请求我们并不想上报

  • 自定义校验请求响应数据:每个项目的响应规则可能都不同,我们想自己判断哪些响应是成功,哪些是失败



格式化上报数据、自定义决定上不上报、自定义上报


如果你想在数据上报之前,格式化上报数据的话,可以配置report中的format



如果你想要自己决定某次上报的时候,进行取消,可以配置report中的isReport



如果你不想用这个库自带的上报功能,想要自己上报,可以配置report中的customReport



手动上报


手动上报分为三种:



  • 手动添加上报数据:添加到缓存中,等到达到阈值再上报

  • 手动执行数据上报:立即上报

  • 自定义指令上报:如果你是 Vue 项目,支持指令上报



如果你是 Vue 项目,可以使用指令v-track进行上报



配置参数


选项描述类型
projectKey   项目keystring
userId   用户idstring
report.url   上报urlstring
report.reportType  上报方式img、http、beacon
report.headers  上报自定义请求头,http 上报模式生效object
report.format  上报数据格式化function
report.customReport  自定义上报function
report.isReport  自定义决定上不上报function
cacheType   数据缓存方式normal、storage、db
globalClickListeners   上报状态array
log   当前域名boolean
maxEvents   上报阈值number
historyUrlsNum   需要记录的url跳转数组number
checkHttpStatus   判断响应数据function
filterHttpUrl   过滤上报请求数据function
switchs.xhr   是否开启xhr请求上报boolean
switchs.fetch   是否开启fetch请求上报boolean
switchs.error   是否开启错误上报boolean
switchs.whitescreen   是否开启白屏检测上报boolean
switchs.hashchange   是否开启hash变化请求上报boolean
switchs.history   是否开启history变化上报boolean
switchs.performance   是否开启页面性能上报boolean


本项目源码:github.com/sanxin-lin/…
各位兄弟姐妹如果觉得喜欢的话,可以点个star 哦~





作者:Sunshine_Lin
来源:juejin.cn/post/7377901375001198655
收起阅读 »

给圆点添加呼吸动画,老板说我很有想法

web
需求简介 这几天老板安排了一个活:要实现一些异常信息点的展示,展示的方式就是画一个红色的点。 需求很简单,我也快速实现了。但是想着我刚入职不久,所以得想办法表现一下自己。于是,我自作主张,决定给这个小圆点实现一个呼吸的效果动画。 实现方案 要实现这样一个小...
继续阅读 »

需求简介


这几天老板安排了一个活:要实现一些异常信息点的展示,展示的方式就是画一个红色的点



需求很简单,我也快速实现了。但是想着我刚入职不久,所以得想办法表现一下自己。于是,我自作主张,决定给这个小圆点实现一个呼吸的效果动画



实现方案


要实现这样一个小圆点的动画非常简单,借助css的animation实现即可


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Breathing Circle Animation</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
// 白边红色小圆点
<div class="dot">
// 小圆点的背景元素
<div class="breathing-background"></div>
</div>
</body>
</html>

.dot {
display: inline-block;
width: 10px;
height: 10px;
border: 2px solid #fff;
border-radius: 50%;
background-color: red;
position: relative;
z-index: 1;
}
.breathing-background {
position: absolute;
width: 10px;
height: 10px;
border-radius: 50%;
opacity: 0.2;
animation: breathing 2s cubic-bezier(0, 0, 0.25, 1) infinite;
}
// 动画 变大再变小
@keyframes breathing {
0% {
transform: scale(1);
}
50% {
transform: scale(5);
opacity: 0.2;
}
100% {
transform: scale(5);
opacity: 0;
}
}

上面的动画实现主要依赖于CSS关键帧动画和定位属性。



  • 定位:通过设置.dot为相对定位(position: relative)和.breathing-background为绝对定位(position: absolute),确保两个元素在同一个位置上重叠。

  • 层叠顺序:使用z-index属性确保.dot在.breathing-background的前面,从而保证红色小圆点在呼吸动画背景上显示。

  • 动画效果:@keyframes breathing定义了从正常尺寸到放大再到透明的动画过程,通过transform: scale和opacity属性的变化来实现呼吸效果。

  • 动画循环:通过animation属性设置动画的持续时间、缓动函数和无限循环,使呼吸动画效果持续进行。


上面的代码很简单,实现的效果也简单粗暴



老板反应


做完之后,我很高兴的就提交代码了,我很满意自己小改动。

过了很久,老板看后,把我叫到办公室,深色凝重的说了一句:你很有想法


随后老板又问我,你加这个闪烁的背景想表达啥?


我一时语塞,解释:这样不是看起来更好看,更能清晰的表达这个异常的状态吗?


老板又怼我,谁让你乱加动画了?时间多的没处用是吧?删了。


我不太理解老板为啥生气,回去后也是默默地删除了代码。。。。。



后来我反思了一下,程序员还是别乱加自己的想法在需求里,毕竟我们还是不懂产品,做的越多,错的越多。做好本分工作就行了。



作者:快乐就是哈哈哈
来源:juejin.cn/post/7376172288977879091
收起阅读 »

🚀独立开发,做的页面不好看?我总结了一些工具与方法🚀

web
前言 我有时候会自己开发一些项目,但是不比在公司里面,自己开发项目的时候没有设计稿,所以做出来的页面比较难看。 开发了几个项目之后,我也总结了以下的一些画页面的资源或者方法,希望对大家有帮助~ 颜色&字体 这一部分主要参考的是antd的方案,主要包括颜...
继续阅读 »

前言


我有时候会自己开发一些项目,但是不比在公司里面,自己开发项目的时候没有设计稿,所以做出来的页面比较难看。


开发了几个项目之后,我也总结了以下的一些画页面的资源或者方法,希望对大家有帮助~


颜色&字体


这一部分主要参考的是antd的方案,主要包括颜色与字体(包括字体的颜色、大小)的使用与搭配。


颜色


对于颜色来说,整个站点最好有一个主题色,然后有一种色彩生成算法,基于这个主题色去生成一套色板。在 antd 的官网中共计 120 个颜色,包含 12 个主色以及衍生色。


image.png


12 种颜色方案都是比较好看的,如果你想定义自己的主题色,这里也有一个色板生成工具


image.png


同样你也可以将这套色板生成算法引入到你的程序中,这是他的npm包


确认好主题色之后,再来看看中性色。


image.png


这里它也提供了我们相对常用的一些中性色,有了主题色与中性色之后,我们就可以定义一个 less/sass 文件,把我们常用的这些颜色写成变量导入使用,确保我们的站点色彩是保持统一的。


@primary-color: #1890ff;
@primary-text-color: #000000e0;
@first-text-color: #000000e0;
@sceond-text-color: #000000a6;
@border-color: #d9d9d9ff;
@disabled-color: #00000040;
@divider-color: #0505050f;
@background-color: #f5f5f5ff;

这几种色彩看起来如下:


image.png


字号


image.png


antd 中,它同样对字体大小也有着十分深厚的研究,我们这里就简单一点,大多数浏览器的默认字体大小是 16px,我们就以这个值为基准,来设计 5 个字号如下:


@smallest-size: 12px;
@small-size: 14px;
@size: 16px;
@large-size: 20px;
@largest-size: 24px;

这五种字号看起来如下:


image.png


渐变


UI 设计中,渐变是一种将两种或多种颜色逐渐过渡或混合在一起的效果。渐变可以增加界面的视觉吸引力、深度和层次感,并帮助引导用户的视线,提高用户体验。


渐变在以下几个方面有着重要的意义:



  1. 引导视线:通过渐变的色彩变化,可以引导用户的视线,突出重要内容或者引导用户进行特定的操作。

  2. 增加层次感:渐变可以使界面元素看起来更具立体感和深度,提高UI设计的质感和视觉吸引力。

  3. 提升品牌形象:使用特定颜色的渐变可以帮助强化品牌形象,让界面更具有品牌特色和辨识度。

  4. 增强用户体验:合理使用渐变可以使界面更加舒适和美观,从而提升用户体验和用户满意度。


这里我一般用的是这个渐变生成工具,可以比较方便的调出来需要的渐变色,支持生成多种渐变色+代码,并支持实时预览。


image.png


阴影


同时,阴影在UI设计中也是不可或缺的部分,它有如下几个重要的意义:



  1. 层次感和深度感:阴影可以帮助界面元素之间建立层次感和深度感。通过添加阴影,设计师可以模拟光源的位置和界面元素之间的距离,使得用户能够更清晰地理解界面的结构。

  2. 突出重点:阴影可以用来突出重点,比如突出显示某个按钮或者卡片。适当的阴影可以使重要的元素脱颖而出,引导用户的注意力。

  3. 视觉吸引力:精心设计的阴影可以增加界面的美感和吸引力。合适的阴影可以使界面看起来更加立体和生动,从而提升用户的体验。

  4. 可视化元素状态:阴影还可以用来表达界面元素的状态,比如悬停或者按下状态。通过微调阴影的属性,可以使用户更清晰地感知到界面元素的交互状态。


我一般用这个阴影生成工具,它同样也支持在线修改多个阴影及预览,同时支持复制代码。


image.png


字体图标


想让我们的网页更生动,那怎么能少的了一个个可爱的 icon 呢,下面就是几个开源 icon 的网站。



image.png



image.png



image.png



image.png



image.png


图片素材


除了 icon 之外,图片素材也是必不可少的,这里介绍我主要用的两个网站。


第一个是花瓣网,这个网站可能找过素材的同学都不会陌生,上面确实有大量的素材供你选择。


image.png


另外一个是可画,它是一个图像编辑器,但是提供了大量的模版,我们也很轻松可以从中提取素材。


image.png


组件库


最后要介绍的是组件库,组件库一来可以提供大量的基础组件,降低开发成本,而来也可以让我们站点的交互更加统一。以下是我常用的组件库:



最后


以上就是我独立开发项目时会思考以及参照的工具,如果你有一些其他想法,欢迎评论区交流。觉得有意思的话,点点关注点点赞吧~


作者:可乐鸡翅kele
来源:juejin.cn/post/7359854125912227894
收起阅读 »

【技巧】JS代码这么写,前端小姐姐都会爱上你

web
前言 🍊缘由 JS代码小技巧,教你如何守株待妹 🍍你想听的故事: 顶着『前端小王子』的称号,却无法施展自己的才能。 想当年本狗赤手空拳打入前端阵地,就是想通过技术的制高点来带动前端妹子。奈何时不待我,前端妹子成了稀有资源,只剩下抠脚大汉前端大叔。 秉承没有妹...
继续阅读 »

前言


🍊缘由


JS代码小技巧,教你如何守株待妹



🍍你想听的故事:


顶着『前端小王子』的称号,却无法施展自己的才能


想当年本狗赤手空拳打入前端阵地,就是想通过技术的制高点来带动前端妹子。奈何时不待我,前端妹子成了稀有资源,只剩下抠脚大汉前端大叔。


秉承没有妹子也得继续学习的态度,本狗将实际代码编写中JS使用技巧总结。分享给小伙伴们,希望这些姿势知识 能够成为吸引妹子的引路石。


正文


一.JS解构赋值妙用


1.采用短路语法防止报错



解构时加入短路语法兜底,防止解构对象如果为 undefined 、null 时,会报错



const user = null;
// 短路语法,如果user为undefined 、null则以{}作为解构对象
const {name, age, sex} = user || {};

举例🌰


通过接口获取用户user对象,解构对象信息


❌错误示例


未使用短路语法兜底,不严谨写法


// 模拟后端接口返回user为null时
const user = null;
const {name, age, sex} = user;
console.log("用户信息name=", name, "age=", age, "sex=", sex);

// 控制台直接报错
// Cannot destructure property 'name' of 'user' as it is null.


✅正确示例


使用短路语法兜底,严谨写法


// 模拟后端接口返回user为null时
const user = null;
// 加入短路语法,意思为如果user为空则以{}作为解构对象
const {name, age, sex} = user || {};
console.log("用户信息name=", name, "age=", age, "sex=", sex);

// 控制台打印
// 用户信息name= undefined age= undefined sex= undefined


2.深度解构



解构赋值可以深度解构:嵌套的对象也可以通过解构进行赋值



举例🌰


通过模拟接口获取用户user对象,解构user对象中联系人concat信息


// 深度解构
const user = {
name:'波',
age:'18',
// 联系人
concat: {
concatName:'霸',
concatAge:'20',
},
};
const {concat: {concatName, concatAge}} = user || {};
console.log("用户联系人concatName=", concatName, "concatAge=", concatAge);

// 控制台打印
// 用户联系人concatName= 霸 concatAge= 20


3.解构时赋值默认值



解构赋值时可以采取默认值填充



举例🌰


通过模拟接口获取用户user对象,解构user对象时,没有dept科室字段时,可以加入默认值


// 解构时设置默认值
const user = {
name:'波',
age:'18',
};
const {name, age, dept = '信息科'} = user || {};
console.log("用户信息name=", name, "age=", age, "dept=", dept);

// 控制台打印
// 用户信息name= 波 age= 18 dept= 信息科




二.数组小技巧


1.按条件向数组添加数据



根据条件向数组中添加数据



举例🌰


设置一个路径白名单数组列表,当是开发环境添加部分白名单路径,若生产环境则不需要添加



// 不是生产环境
const isEnvProduction = false;

// 基础白名单路径
const baseUrl = [
'/login',
'/register'
]

// 开发环境白名单路径
const devUrl = [
'/test',
'/demo'
]
// 如果是生产环境则不添加开发白名单
const whiteList = [...baseUrl, ...(isEnvProduction? [] : devUrl)];

console.table(whiteList)


// 控制台打印
// Array(4) ["/login", "/register", "/test", "/demo"]


// 是生产环境
const isEnvProduction = true;

// 基础白名单路径
const baseUrl = [
'/login',
'/register'
]

// 开发环境白名单路径
const devUrl = [
'/test',
'/demo'
]
// 如果是生产环境则不添加开发白名单
const whiteList = [...baseUrl, ...(isEnvProduction? [] : devUrl)];

console.table(whiteList)
// 控制台打印
// Array(2) ["/login", "/register"]


2.获取数组最后一个元素



给到一个数组,然后访问最后一个元素



举例🌰


获取一个数组中最后一个值


const arr = [1, 2, 3, 4];
// 通过slice(-1) 获取只包含最后一个元素的数组,通过解构获取值
const [last] = arr.slice(-1) || {};
console.log('last=',last)

// 控制台打印
// last= 4


3.使用 includes 优化 if



灵活使用数组中方法includes可以对if-else进行优化



举例🌰


如果条件a值是 1,2,3时,打印有个男孩叫小帅


一般写法


const a = 1;

// 基本写法
if(a==1 || a==2 || a==3){
console.log('基本写法:有个男孩叫小帅');
}

// 优化写法
if([1, 2, 3].includes(a)){
console.log('优化写法:有个男孩叫小帅');
}

// 控制台打印
// 基本写法:有个男孩叫小帅
// 优化写法:有个男孩叫小帅





三.JS常用功能片段


1.通过URL解析搜索参数



通过页面URL获取解析挂参参数,适用于当前页面需要使用到URL参数时解析使用




// 通过URL解析搜索参数

const getQueryParamByName = (key) => {
const query = new URLSearchParams(location.search)
return decodeURIComponent(query.get(key))
}

const url = "http://javadog.net?user=javadog&age=31"

// 模拟浏览器参数(此处是模拟浏览器参数!!!)
const location = {
search: '?user=javadog&age=31'
}

console.log('狗哥名称:', getQueryParamByName('user'));
console.log('狗哥年龄:', getQueryParamByName('age'));

// 控制台打印
// 狗哥名称: javadog
// 狗哥年龄: 31


2.页面滚动回到顶部



页面浏览到某处,点击返回顶部



// 页面滚动回到顶部
const scrollTop = () => {
// 该函数用于获取当前网页滚动条垂直方向的滚动距离
const range = document.documentElement.scrollTop || document.body.scrollTop
// 如果大于0
if (range > 0) {
// 该函数用于实现页面的平滑滚动效果
window.requestAnimationFrame(scrollTop)
window.scrollTo(0, range - range / 8)
}
}



3.获取页面滚动距离



获取页面滚动距离,根据滚动需求处理业务



// 该函数用于获取当前页面滚动的位置,可选参数target默认为window对象
const getPageScrollPosition = (target = window) => ({
// 函数返回一个包含x和y属性的对象,分别表示页面在水平和垂直方向上的滚动位置。函数内部通过判断target对象是否具有pageXOffset和pageYOffset属性来确定滚动位置的获取方式,如果存在则使用该属性值,否则使用scrollLeft和scrollTop属性。
x: target.pageXOffset !== undefined ? target.pageXOffset : target.scrollLeft,
y: target.pageYOffset !== undefined ? target.pageYOffset : target.scrollTop,
})

getPageScrollPosition()



总结


这篇文章主要介绍了JavaScript编程中的几个实用技巧,包括解构赋值的妙用、数组操作以及一些常用的JS功能片段,总结如下:


解构赋值妙用



  • 短路语法防止报错:在解构可能为undefined或null的对象时,使用短路语法(|| {})来避免错误。

  • 深度解构:可以解构嵌套的对象,方便地获取深层属性。

  • 解构时赋值默认值:在解构时可以为未定义的属性提供默认值。


数组小技巧



  • 按条件向数组添加数据:根据条件动态地决定是否向数组添加特定元素。

  • 获取数组最后一个元素:使用slice(-1)获取数组的最后一个元素。

  • 使用includes优化if语句:用includes检查元素是否在数组中,简化条件判断。


JS常用功能片段



  • 通过URL解析搜索参数:创建函数解析URL的查询参数,便于获取URL中的参数值。

  • 页面滚动回到顶部:实现页面平滑滚动回顶部的函数。

  • 获取页面滚动距离:获取页面滚动位置的函数,可用于处理滚动相关的业务逻辑。


🍈猜你想问


如何与狗哥联系进行探讨


关注公众号【JavaDog程序狗】

公众号回复【入群】或者【加入】,便可成为【程序员学习交流摸鱼群】的一员,问题随便问,牛逼随便吹,目前群内已有超过200+个小伙伴啦!!!


2.踩踩狗哥博客

javadog.net



大家可以在里面留言,随意发挥,有问必答






🍯猜你喜欢


文章推荐


【工具】珍藏免费宝藏工具,不好用你来捶我


【插件】IDEA这款插件,爱到无法自拔


【规范】看看人家Git提交描述,那叫一个规矩


【工具】用nvm管理nodejs版本切换,真香!


【项目实战】SpringBoot+uniapp+uview2打造H5+小程序+APP入门学习的聊天小项目


【项目实战】SpringBoot+uniapp+uview2打造一个企业黑红名单吐槽小程序


【模块分层】还不会SpringBoot项目模块分层?来这手把手教你!


【ChatGPT】SpringBoot+uniapp+uview2对接OpenAI,带你开发玩转ChatGPT



作者:JavaDog程序狗
来源:juejin.cn/post/7376532114105663539
收起阅读 »

解决vite项目首次打开页面卡顿的问题

web
问题描述 在vite项目中我们可能会遇到这样一种情况。 在我们本地开发,第一次进入页面的时候,页面会卡顿很长时间。越是复杂的卡顿时间越久。 要是我们一天只专注于一两个页面 那这个就不是问题。 但有的时候我们要开发一个流程性的东西,要进入各种各样的页面查看。这样...
继续阅读 »

问题描述


在vite项目中我们可能会遇到这样一种情况。


在我们本地开发,第一次进入页面的时候,页面会卡顿很长时间。越是复杂的卡顿时间越久。


要是我们一天只专注于一两个页面 那这个就不是问题。


但有的时候我们要开发一个流程性的东西,要进入各种各样的页面查看。这样就很痛苦了。


问题原因


为什么会出现这种情况呢?因为路由的懒加载与vite的编译机制。


路由的懒加载:没有进入过的页面不加载


vite的编译机制:没有加载的不编译。


这样就会出现 我们在进入一个新页面的时候他才会编译。我们感觉卡顿的过程就是他编译的过程。


解决思路


问题找到了,那么解决起来就简单了。我们本地开发的时候,取消路由的懒加载就可以了。


const routes = [
{
path: `/home`,
name: `Home`,
component: () => import(`@/views/home/HomePage.vue`),
meta: { title: `首页` },
},
{
path: `/test1`,
name: `test1`,
component: () => import(`@/views/demo/Test1.vue`),
meta: { title: `测试1` },
},
{
path: `/test2`,
name: `test2`,
component: () => import(`@/views/demo/Test2.vue`),
meta: { title: `测试2` },
}
]

if (import.meta.env.MODE === `development`) {
routes.forEach(item => item.component())
}

示例代码如上。上述的问题是解决了,但是又产生了新的问题。项目太大的时候启动会非常慢。


于是我想了一个折中的方案。初始打开项目的时候路由还是懒加载的,然后我在浏览器网络空闲的时候去加载资源。这样你首次进系统打开的第一个页面可能还是需要等待,但是之后的所有页面就不需要等待了。


那么问题又来了?怎么监听浏览器的网络空闲呢?这就要用的浏览器的一个api PerformanceObserver。这个api可能很多小伙伴都不知道,它主要是帮助你监控和观察与性能相关的各种指标。想要详细了解的可以点击这里查看


我们今天用的就是resource类型监听所有的网络请求,代码示例如下


 const observer: PerformanceObserver = new PerformanceObserver((list: PerformanceObserverEntryList) => {
const entries: PerformanceEntryList = list.getEntries()
for (const entry of entries) {
if (entry.entryType === `resource`) {
//网络请求结束
}
}
})
observer.observe({ entryTypes: [`resource`] })

监听到网络请求后,我们怎么判断是否空闲呢?也很简单,只要一秒钟以内没有新的网络请求出现我们就认为当前网络是空闲的。这不就防抖函数嘛。


const routes = [
{
path: `/home`,
name: `Home`,
component: () => import(`@/views/home/HomePage.vue`),
meta: { title: `首页` },
},
{
path: `/test1`,
name: `test1`,
component: () => import(`@/views/demo/Test1.vue`),
meta: { title: `测试1` },
},
{
path: `/test2`,
name: `test2`,
component: () => import(`@/views/demo/Test2.vue`),
meta: { title: `测试2` },
}
]

if (import.meta.env.MODE === `development`) {
const componentsToLoad = routes.map(item => item.component)
const loadComponentsWhenNetworkIdle = debounce(
() => {
if (componentsToLoad.length > 0) {
const componentLoader = componentsToLoad.pop()
componentLoader && componentLoader()
// eslint-disable-next-line
console.log(`剩余${componentsToLoad.length}个路由未加载`, componentsToLoad)
}
},
1000,
false
)

const observer: PerformanceObserver = new PerformanceObserver((list: PerformanceObserverEntryList) => {
const entries: PerformanceEntryList = list.getEntries()
for (const entry of entries) {
if (entry.entryType === `resource`) {
loadComponentsWhenNetworkIdle()
}
}
})
observer.observe({ entryTypes: [`resource`] })
}

完整的代码如上。当我们判断出网络空闲后,就从componentsToLoad数组中删除一个组件,并加载删除的这个组件,然后就会重新触发网络请求。一直重复这个流程,直到componentsToLoad数组为空。


这只是个示例的代码,获取componentsToLoad变量防抖函数的配置(初始化不执行,无操作后1秒钟后执行)还要根据你的实际项目进行修改!


可优化项


以上方法确实是按照我们的预期实现了,但是还有一些小小的问题。例如:



  1. 我们在加载组件的时候如果恰好是当前打开的页面,是不会重新触发网络请求的。因此可能会断掉componentsToLoad数组的删除,加载组件,触发网络请求这个流程。不过问题不大,你在当前页面如果有操作重新触发网络请求了,这个流程还会继续走下去,直到componentsToLoad数组为空。

  2. 每次刷新页面componentsToLoad数组都是会重新获取到值的,也就是我们走过的流程会重新走。不过问题不大,第二次走都是走缓存了,执行速度很快,而且也是本地开发那点性能损坏可以忽略不计。


这些问题影响都不是很大,所以我就没继续做优化。有兴趣的小伙伴可以继续研究下去。


作者:热心市民王某
来源:juejin.cn/post/7280745727160811579
收起阅读 »

前端大师课:“鬼剑士,听我指令,砍碎屏幕”是怎么实现的?

web
前言:属于我们那个年代的"地下城与勇士"的手游上线了,为了做好推广和裂变,有个特别游戏意思的效果你可能在各个微信群里都看到了:你只需要在微信群里发送"鬼剑士,听我指令,砍碎屏幕"、“鬼剑士”、“地下城与勇士”这些关键词,就会触发特别炫酷的动画效果。那这种效果如...
继续阅读 »

前言:属于我们那个年代的"地下城与勇士"的手游上线了,为了做好推广和裂变,有个特别游戏意思的效果你可能在各个微信群里都看到了:你只需要在微信群里发送"鬼剑士,听我指令,砍碎屏幕"、“鬼剑士”、“地下城与勇士”这些关键词,就会触发特别炫酷的动画效果。
那这种效果如果让我们技术来做:
1.要怎么实现呢?
2.有几种实现方法呢?
3.关键代码能给我看看吗?

方案简述

为了提供更详细的解析,我们可以进一步探讨“地下城与勇士手游”(DNF手游)在微信聊天中实现“鬼剑士,听我指令,砍碎屏幕”这一互动特效的可能技术细节。虽然没有直接的源码分析,我们可以基于现有的技术框架和前端开发实践来构建一个理论上的实现模型。

前端监听设计

  • 关键词识别: 微信聊天界面的输入检测可能是通过前端JavaScript监听input事件,配合正则表达式匹配用户输入的关键词(如“鬼剑士,听我指令,砍碎屏幕”)。一旦匹配成功,就向后端发送请求或直接触发前端动画逻辑。

后端交互

  • 请求处理: 用户输入关键词后,前端可能通过Ajax请求或WebSocket向服务器发送一个事件。服务器确认后,返回一个响应,指示前端继续执行动画展示或直接携带福袋奖励信息。

前端动画实现

  • 动画序列: 利用HTML5 元素或WebGL技术,开发者可以创建复杂的2D或3D动画。对于“砍碎屏幕”的效果,可能事先设计好一系列帧动画或使用骨骼动画技术来展现鬼剑士的动作和屏幕碎裂的过程。
  • 碎片生成与物理模拟: 通过JavaScript库(如Three.js的粒子系统或matter.js)模拟屏幕碎裂后的碎片效果,包括碎片的随机分布、速度、旋转和重力影响等,增加真实感。
  • 音频同步: 使用Web Audio API同步播放砍击和碎裂的音效,增强用户的沉浸感。

福袋奖励机制

  • • 动画结束后展示福袋: 动画播放完毕后,前端动态插入一个福袋图标或弹窗,作为用户交互元素。这可能是通过DOM操作实现的,如创建一个新的
    元素并应用CSS样式使其表现为福袋。
  • • 点击事件处理: 给福袋元素绑定点击事件,触发领奖逻辑。这可能涉及再次向服务器发送请求验证用户资格,并根据响应展示奖励内容。

优化与兼容性

  • 性能优化: 动画应考虑在不同设备上的流畅度,可能采用分层渲染、帧率限制、资源按需加载等策略。
  • 跨平台兼容: 确保在微信内置浏览器上的表现良好,需要对微信环境下的特定API和限制有深入了解,比如微信小程序的Canvas组件和其特定的适配要求。

安全与隐私

  • 数据保护: 在处理用户交互和服务器通信时,确保遵循数据保护法规,比如加密传输敏感信息,避免泄露用户隐私。

综上所述,这个互动特效的实现是一个从用户输入监测、前后端交互、动画设计与渲染、到用户反馈与奖励领取的全链路流程,需要综合运用多种前端技术和良好的产品设计思路。

微信聊天界面元素震动效果设计及API应用

虽然微信没有直接公开针对UI元素震动的特定API,但在微信小程序或基于微信环境的H5游戏中设计类似聊天界面元素的震动效果,利用一些基础的动画技术和微信小程序提供的动画库来模拟这种效果。比如通过CSS动画与微信小程序的动画接口来实现这一功能。以下是两种主流实现方式:

1. CSS动画实现震动效果(H5环境)

核心概念

  • @keyframes: CSS的关键帧动画,用于定义一个动画序列中不同时间点的样式变化。
  • transform: CSS属性,用于改变元素的形状、大小和位置。其中,translateX()用于水平移动元素。

实现步骤

  1.  定义动画样式:在CSS中,创建一个名为.shake的类,利用@keyframes定义震动序列。动画包括了元素在原位置与左右轻微偏移之间的快速切换,营造出震动感。

    .shake {
      animation: shake 0.5s/* 动画名称与持续时间 */
      transform-origin: center center; /* 设置变换中心点 */
    }

    @keyframes shake {
      0%100% { transformtranslateX(0); } /* 开始与结束位置 */
      10%30%50%70%90% { transformtranslateX(-5px); } /* 向左偏移 */
      20%40%60%80% { transformtranslateX(5px); } /* 向右偏移 */
    }

2. 应用动画:在JavaScript中,通过动态添加或移除.shake类到目标元素上,触发这个震动动画。

2. 微信小程序wx.createAnimation实现震动

核心概念

  • wx.createAnimation: 微信小程序提供的动画实例创建方法,允许更精细地控制动画过程。
  • step() : 动画实例的方法,用于生成当前动画状态的数据,用于在setData中更新视图。

实现步骤

  1. 初始化动画数据:在Page的data中定义一个空的animationData对象,用于存储动画实例导出的状态数据。

    data: {
      animationData: {},
    },

2. 创建震动动画逻辑:定义一个函数,如shakeElement,使用wx.createAnimation创建动画实例,并定义震动序列。通过连续的translateX操作模拟震动,然后通过step()函数记录每个阶段的状态,并通过setData更新到视图上。

```
shakeElement: function () {
  let animation = wx.createAnimation({
    duration: 300// 动画持续时间
    timingFunction: 'ease'// 动画速度曲线
  });

  // 震动序列定义
  animation.translateX(-5).step(); // 向左偏移
  this.setData({ animationData: animation.export() });
  setTimeout(() => {
    animation.translateX(5).step(); // 向右偏移
    this.setData({ animationData: animation.export() });
  }, 100);
  setTimeout(() => {
    animation.translateX(0).step(); // 回到原位
    this.setData({ animationData: animation.export() });
  }, 200);
},
```

3. 应用动画数据:在WXML模板中,为目标元素绑定动画数据。

```
style="{{animationData}}" class="your-element-class">震动的文字或图标
```

注意事项与最佳实践

  • 性能监控:频繁或长时间的动画可能影响应用性能,尤其是低配置设备。适时停止或限制动画触发频率。
  • 用户体验:震动效果应适度且符合用户预期,过度使用可能造成用户反感。
  • 跨平台兼容性:虽然上述方法主要针对微信环境,但在实现时也应考虑浏览器的兼容性问题,特别是对于H5应用。
  • 动画细节调整:根据实际需求调整震动幅度、频率和持续时间,以达到最佳视觉效果。

动手能力强的你,可以去试试,下一节,将讲一个具体的demo给大家演示一下哈。


    作者:蜡笔小新爱学习
    来源:juejin.cn/post/7371423076661542952
    收起阅读 »

    带你从0到1部署nestjs项目

    web
    前言 最近跟着一个掘金大佬做了一个全栈项目,前端react,后端我是用的nest,大佬用的midway 大佬博客地址(前端小付 的个人主页 - 动态 - 掘金 (juejin.cn) 最近项目也是部署上线了,因为域名还没备案,地址就先不发出来了,这篇文章就讲讲...
    继续阅读 »

    前言


    最近跟着一个掘金大佬做了一个全栈项目,前端react,后端我是用的nest,大佬用的midway


    大佬博客地址(前端小付 的个人主页 - 动态 - 掘金 (juejin.cn)


    最近项目也是部署上线了,因为域名还没备案,地址就先不发出来了,这篇文章就讲讲如何部署。一直有兄弟问prisma如何部署,这篇文章就帮你扫清障碍,文章可能比较长,希望耐心看完


    后端技术栈



    • nestjs

    • mysql

    • redis

    • minio

    • prisma


    部署需要掌握的知识



    • docker

    • github actions

    • 服务器


    实战


    nestjs打包镜像


    我们部署的时候用的docker,docker需要拉镜像,然后生成容器,docker的知识可以去学习下,这里就默认大家会了,我们在打包的时候要写Dockerfile文件,后端项目是需要保留node_modules的,所以打包的时候一起打进去,我的项目用的pnpm包管理工具,我的文件挂载时有点点问题,我就没有用pm2去执行多阶段打包,多阶段打包速度会比较快,还有就是比如开发环境的依赖可以不打,当然这都是优化的地方,暂时没有去做,大家可以自行尝试


    # 因为我们项目使用的是pnpm安装依赖,所以找了个支持pnpm的基础镜像,如果你们使用npm,这里可以替换成node镜像
    # FROM nginx:alpine
    FROM gplane/pnpm:8 as builder

    # 设置时区
    ENV TZ=Asia/Shanghai \
    DEBIAN_FRONTEND=noninteractive
    RUN ln -fs /usr/share/zoneinfo/${TZ} /etc/localtime && echo ${TZ} > /etc/timezone && dpkg-reconfigure --frontend noninteractive tzdata && rm -rf /var/lib/apt/lists/*

    # 创建工作目录
    RUN mkdir -p /app

    # 指定工作目录
    WORKDIR /app

    # 复制当前代码到/app工作目录
    COPY . ./

    RUN npm config set registry https://registry.npm.taobao.org/
    # pnpm 安装依赖
    COPY package.json /app/package.json

    RUN rm -rf /app/pnpm-lock.yml
    RUN cd /app && rm -rf /app/node_modules && pnpm install

    RUN cd /app && rm -rf /app/dist && pnpm build

    EXPOSE 3000
    # 启动服务
    CMD pnpm run start:prod


    这样后端镜像就构建好了,接下来去编写github action的文件,github actions是做ci/cd的,让我们每次的部署走自动化流程,不要每次手动去做这些工作


    github actions


    在我们的根目录下面创建这样一个文件,这个文件名字可以随便取


    12.png


    然后在里面编写逻辑


    name: Docker

    on:
    push:
    branches: ['main']

    env:
    REGISTRY: ghcr.io
    IMAGE_NAME: ${{ github.repository }}

    jobs:
    build:
    runs-on: ubuntu-latest
    permissions:
    contents: read
    packages: write
    id-token: write

    steps:
    - name: Checkout repository
    uses: actions/checkout@v3

    - name: Setup Docker buildx
    uses: docker/setup-buildx-action@79abd3f86f79a9d68a23c75a09a9a85889262adf

    - name: Cache Docker layers
    uses: actions/cache@v2
    with:
    path: /tmp/.buildx-cache
    key: ${{ runner.os }}-buildx-${{ github.sha }}
    restore-keys: |
    ${{ runner.os }}-buildx-

    - name: Log int0 registry ${{ env.REGISTRY }}
    if: github.event_name != 'pull_request'
    uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c
    with:
    registry: ${{ env.REGISTRY }}
    username: ${{ github.actor }}
    password: ${{ secrets.GITHUB_TOKEN }}

    - name: Extract Docker metadata
    id: meta
    uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
    with:
    images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}

    - name: Build and push Docker image
    id: build-and-push
    uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
    with:
    context: .
    push: ${{ github.event_name != 'pull_request' }}
    tags: ${{ steps.meta.outputs.tags }}
    labels: ${{ steps.meta.outputs.labels }}
    cache-from: type=local,src=/tmp/.buildx-cache
    cache-to: type=local,dest=/tmp/.buildx-cache-new

    - name: Move cache
    run: |
    rm -rf /tmp/.buildx-cache
    mv /tmp/.buildx-cache-new /tmp/.buildx-cache

    - name: SSH Command
    uses: D3rHase/ssh-command-action@v0.2.1
    with:
    HOST: ${{ secrets.SERVER_IP }}
    PORT: 22
    USER: root
    PRIVATE_SSH_KEY: ${{ secrets.SERVER_KEY }}
    COMMAND: cd /root && ./run.sh

    这里的['main']就是我们要执行哪个分支,你不是main分支,那就改成你的分支就可以,其他都是固定的模板,直接用


    SSH Command 这个是我们取做ci/cd的时候,每次我们提交代码,然后配置了ssh密钥,就可以让服务器执行run.sh命令,这个shell脚本我们后面可以用到,这里就记住是让服务器去执行拉取镜像以及执行启动容器的。


    当我们做到这一步之后,我们提交代码的时候,应该会出现这样的情况


    13.png


    因为还没有去配置ssh密钥,这个肯定跑不起来,看到我们上面ssh command里面有两个变量,就是我们要配置的,接下来我们去搞服务器。


    服务器


    最近双十一活动,买个服务器还是挺香的,我买的阿里云2核2g的99/年,买的时候选操作系统,随便一个都可以,我因为对ubuntu熟悉一下,就买了ubuntu操作系统的,买好之后,记得重置密码


    14.png


    后面我们用shell工具连接的时候需要用到密码的


    之后我们去下载一个shell工具,连接服务器用的,常见的有xshell finalshell,我用的第二个。


    15.png


    就傻瓜式安装,下一步就可以,然后我们去连接一下服务器,去下载宝塔。


    16.png


    第二步那里选择ssh连接就可以了,然后主机就是你的服务器公网ip,密码就是刚刚的,用户名就是root


    连接上了之后,去下载宝塔,这个是ubuntu的命令,其他的操作系统有差别,可以去搜一下就有


    wget -O install.sh http://download.bt.cn/install/install-ubuntu_6.0.sh && sudo bash install.sh


    下载好之后输入bt default命令就可以打开了


    17.png


    因为宝塔是个可视化操作面板,比较方便,所以先弄好。


    接下来我们去搞服务器密钥


    18.png


    我们在这里创建好密钥对,记得它只有一次机会,所以下载好了记得保存在你记得住的地方,然后创建好,记得要绑定,不然没效果,然后我们就要得用ssh密钥来连接服务器了


    20.png


    至此,我们的服务器也弄好了


    github绑定密钥


    21.png


    这个是settings界面的,然后大家按照步骤创建就可以,到这里我们的配置就结束了。


    创建shell脚本


    我们上面不是说了,我们要写一个bash文件吗,现在就要来写,这个bash文件我们要执行拉镜像和跑容器


    23.png


    我们可以选择在宝塔中操作


    docker-compose pull && docker-compose up --remove-orphans

    然后我们在同目录下也就是root目录下面新建一个docker-compose.yml文件,来启动容器的,这个文件就不要展示了,也就是创建了哪些服务,挂载哪些卷,如果有需要评论区说一下就行,很简单,因为我们用了很多服务,mysql redis minio nginx 这些多镜像,就得多个容器来跑,docker-compose无疑就好


    到这里后端项目就部署完了,我们还得迁移数据库对吧


    数据库部署


    pirsma迁移

    因为我用的mysql和prisma,typeorm思路差不多,可以一样用。我们的prisma以及typeorm迁移的时候只可以同步表结构,数据不会同步过去,所以我们跑迁移命令的时候,跑完会发现没有数据,我们需要手动导入数据


    另外注意点,我们docker-compose.yml里面的mysql容器名字对应我们连接的主机名,这里记得更改prisma连接,不然你的prisma还连接在localhost肯定找不到


    我们来上手操作


    24.png


    这是我现在在跑的容器,我要找到我的后端项目对应的容器id,进去执行命令


    docker exec -it <容器id> /bin/sh 跑这个我们就可以在容器内部执行命令


    25.png


    然后就可以把表结构同步过去了,我们也可以在生成Dockerfile的时候写迁移命令也是可以的,这样就不用手动同步了


    数据库导出

    我们需要将本地的数据迁移上去,需要先导出sql文件,这个就不用在这里展开说了,很简单,不会可以去找个博客教程,不到30s就完了,导出后我们需要将那个sql文件


    然后我们在宝塔操作,找到你正在运行的mysql容器目录


    26.png


    将你的sql文件上传上去,放哪里都无所谓,记得路径就行


    然后我们进入mysql容器里面,跑上面的那个命令



    1. 登录账号 mysql -u root -p

    2. 输入密码 ******* 输入你数据库连接的那个密码

    3. 进入之后 USE <database_name> 就选中了那张表

    4. 然后执行 source 刚刚的那个sql文件路径


    这样操作数据就同步上去了,注意,数据同步前是一定要有表结构的,所以有先后顺序,这个地方注意。


    也可以用这个命令, 将sql文件拷贝到你的容器内,然后跑上面的步骤,看个人喜好了。
    docker cp /本地路径/your_file.sql 容器名称:/容器路径/your_file.sql


    到这里我们的部署就结束了,等项目正式上线的时候,还有其他注意点还会再写一篇博客的


    最后


    项目是跟着开头提到的小付大佬学习的,主要想学下react,没想到是个全栈项目,就用nestjs写了后端,也学到了很多前端,后端,部署的知识,强烈推荐大家可以去看看。最后 觉得不错的话,可以给个点赞加关注😘


    作者:西檬
    来源:juejin.cn/post/7299859799780655155
    收起阅读 »

    webview预加载的技术原理和注意点

    web
    此文章介绍webview预加载的技术原理和注意点 背景 网页优化,对网页的webview进行预加载,用户点开页面达到秒开效果 原理 即空间换时间,提前加载页面url 由于首页就有网页入口,所以需要在首页Activity进行预加载。 创建webview Web...
    继续阅读 »

    此文章介绍webview预加载的技术原理和注意点


    背景


    网页优化,对网页的webview进行预加载,用户点开页面达到秒开效果


    原理


    即空间换时间,提前加载页面url


    由于首页就有网页入口,所以需要在首页Activity进行预加载。


    创建webview



    • WebView(MutableContextWrapper(context),使用MutableContextWrapper替换Context,,方便后续复用时替换为webview容器Activity的上下文对象

    • WebSettings、原生方法初始化,保证预加载时功能正常,因为后续不会再进行loadUrl,必须保证h5页面正常显示

    • WebViewClient、WebChromeClient监听



      • 重写onPageFinished、onReceivedTitle方法,主要为了title的接收,并且记录下来,后续webview复用时直接显示title

      • 重写onReceivedSslError方法,避免证书错误加载显示失败



    • 首页预加载容器Layout,置于最底层,宽度全屏,高度设置为全屏高度 - 顶部导航栏高度 - 状态栏高度


    viewGr0up.addView(WebView(MutableContextWrapper(context)).also { web ->  // 初始化webview 
    }, ViewGr0up.LayoutParams(ViewGr0up.LayoutParams.MATCH_PARENT, ViewGr0up.LayoutParams.MATCH_PARENT))


    • 刷新逻辑,绑码状态或登录信息改变时,刷新已经预加载好的webview的url


    复用webview



    • webview容器整改

      • 判断是否需要使用已预加载的webview,如果需要复用,则根布局添加预加载webview进来,注意布局层级,避免覆盖了其他控件





    webView?.let { web ->
    (web.context as MutableContextWrapper).baseContext = activity
    }

    container.addView(it, 0, ViewGr0up.LayoutParams(ViewGr0up.LayoutParams.MATCH_PARENT, ViewGr0up.LayoutParams.MATCH_PARENT))


    • 原webview容器使用ViewStub代替,如果不需要复用则将ViewStub inflate,进行正常的h5页面加载

    • 添加预加载webview后,直接显示,不需要loadUrl,但是白屏分析之类的逻辑别忘了手动调用


    页面关闭



    • webview跟随Activity一起进行销毁,但是需要通知首页重新生成一个webview,准备给下一次用户点击使用

    • 首页关闭,页面栈清空时,需要清空单例中webview对象,并且调用destroy

    • 不推荐回收webview继续使用,因为在实际测试中表现并不好,重建webview可以规避很多问题场景


    如果用户点击比较快时,依然会看到加载过程和骨架屏


    问题点和解决



    • 复用webview时,页面视觉残留上一次h5页面状态



      • 原因:页面关闭时,触发Activity的onStop方法,webview调用onPause,此时webview被暂停,webview的reload也不会立即刷新

      • 解决:回收webview时,对webview重新恢复交互状态(onResume)



    • 页面关闭,迅速点开,页面先显示上一次h5页面状态,然后开始reload



      • 原因:当Activity反复打开关闭时,Activity的回收、对象GC会滞后,导致webview已经被复用并且上屏了,webview才开始触发reload

      • 解决:webview不进行回收,每次页面关闭都进行销毁,重新创建webview



    • webview多次reload后,网络请求失败
      axios网络请求失败,response报文为空,暂未找到原因,了解的大佬麻烦解答一下,谢谢。当不回收webview后,此场景不存在了

    • h5页面正常显示后,又刷新一次页面



      • 原因:webview复用时,对webview重新进行初始化(重新创建原生能力、重置上下文对象等)时,会重新对UserAgent进行赋值,导致重新刷新了一次。

      • 排查过程
        发现网页骨架屏刚出现时点开不会重复刷新;骨架屏消失后点开也不会重复刷新;唯独骨架屏时,刚出现vConsole绿色块时点开会出现重复刷新。
        对webview的shouldOverrideUrlLoading方法进行debug,发现并没有进入断点,说明并不是调用了reload,推测有什么逻辑导致网页重新刷新了一次。
        随即用傻子办法,一段一段代码注释,发现友盟组件attach webview和通用webview容器设置userAgent的代码会导致重复刷新,难道友盟组件也有设置userAgent的代码?
        然后查看友盟组件源码,不出所料,发现友盟组件中反射了setUserAgentString方法,并且对userAgent拼接了"Umeng4Aplus/1.0.0字符串,如下图所示。


        那是否设置的userAgent有什么敏感字符串导致刷新?随即将userAgent只设置为空字符串,发现也会导致重复刷新。
        到这里水落石处,但为什么userAgent发现变化会导致网页重复刷新?
        询问前端大佬,回复没有监听userAgent,userAgent变化也不会做什么操作,故而没有得到具体答案,了解的大佬麻烦解答一下,感谢。

      • 解决:webview复用时,不进行userAgent的重复赋值




    IMG20240529101049.png



    • 复用webview时,页面白屏等待一会后秒开h5页面

      • 原因:预加载时webview在1像素的layout中加载,复用到通用webview容器中,webview控件的布局已经结束,但需要时间对H5进行渲染,在重复打开关闭后或性能低下的手机表现更甚

      • 解决:首页预加载webview时,已通用webview容器同大小进行布局



    • 内存泄漏问题

      • 原因:部分原生方法对象中对Activity和Fragment有强引用,当原生方法对象被addJavascriptInterface添加进webview时,复用的webview生命周期长如Application,就会强引用到Activity,导致无法回收,内存泄漏

      • 解决:webview回收时清空Activity、Fragment的引用

      • 不复用webview后此问题不存在了




    作者:聪本尊18680
    来源:juejin.cn/post/7373937820179005478
    收起阅读 »

    设计问能不能实现这种碎片化的效果

    web
    前言 某天设计发来一个 网站,问我能不能实现这种类似的效果。 不知你们什么感想,反正我当时第一次看到这个网站的效果时,心里第一反应就是这做个锤子啊。 F12 调试 让我们打开调试,瞅瞅别人是如何实现。 可以看到在该层级下,页面有很多个 shard-wrap ...
    继续阅读 »

    前言


    某天设计发来一个 网站,问我能不能实现这种类似的效果。


    shard-img-reverse-xs.gif

    不知你们什么感想,反正我当时第一次看到这个网站的效果时,心里第一反应就是这做个锤子啊。


    F12 调试


    让我们打开调试,瞅瞅别人是如何实现。


    可以看到在该层级下,页面有很多个 shard-wrap 元素,而每一个元素都是定位覆盖其父元素的。


    image.png

    当我们添加 display: none; 后,可以看到嘴角这里少了一块。


    image.png

    而继续展开其内部的元素就可以看到主要的实现原理了:clip-path: polygon();


    image.png

    clip-pathpolygon 更详细的解释可以看 MDN,简单来说就是在一个 div 里面绘制一个多边形。


    比如上图的意思就是:选取 div 内部坐标为 (9.38%,59.35%),(13.4%,58.95%),(9.28%,61.08%) 这三个点,并将其连起来,所以就能构成一个三角形了。然后再填充 backgroundColor 该三角形就有对应颜色了。


    实现过程


    调试看完别人的实现后发现,好像也不是很难。但是数据又如何搞来呢?


    当然我们可以直接在接口那里找别人的数据,但是我找了一会发现不太好找。


    于是想到咱们可是一名前端啊,简单写段 js 扒拉下来不就好了吗,想要更多,就滑一下滚轮,切换下一个碎片图像,再执行一次即可。


    function getShardDomData() {
    const doms = document.getElementsByClassName('shard')
    const list = []
    for (let i = 0; i < doms.length; i++) {
    const style = window.getComputedStyle(doms[i])
    let str = style.clipPath.replace('polygon(', '').replace(')', '')
    list.push({
    polygon: str,
    color: style.backgroundColor,
    })
    }
    return list
    }
    console.log('res: ', getShardDomData());

    image.png

    碎片化组件


    简单封装一个碎片化组件,通过 transitiondelay 增加动画效果以及延迟,即可实现切换时的碎片化动画效果。我这里是用的 tailwindcss 开发的,大家可以换成对应 css 即可。


    export type ShardComItem = {
    color: string
    polygon: string
    }

    export type ShardComProps = {
    items: ShardComItem[]
    }

    export default function ShardCom({items}: ShardComProps) {
    return (
    <div className="relative w-full h-full min-w-20">
    {items?.map((item, index) => (
    <div className="absolute w-full h-full" key={`${index}`}>
    <div
    className="w-full h-full transition-all duration-1000 ease-in-out"
    style={{
    backgroundColor: item.color,
    clipPath: `polygon(${item.polygon})`,
    transitionDelay: `${index * 15}ms`,
    }}
    >
    </div>
    </div>
    ))}
    </div>

    )
    }

    模仿实现的 demo 地址


    组件的代码放码上掘金了,感兴趣可以自提。



    自制画板绘画 clip-path


    当然只扒拉别人的数据,这肯定是不行的,假如设计师想自己设计一份碎片化效果该怎么办呢?


    解决方法也很简单:那就自己简单开发一个绘图界面,让设计师自己来拖拽生成想要的效果即可。


    线上画板地址


    image.png

    画板的实现就放到 下篇文章 中讲述了。


    最后


    当然最终只是简陋的实现了一部分效果罢了,还是缺少很多动画的,和 原网站 存在不少差距的,大家看个乐就行。


    作者:滑动变滚动的蜗牛
    来源:juejin.cn/post/7372013979467333643
    收起阅读 »

    我是如何把个人网站首屏加载时间从18秒优化到5秒的

    web
    起因是这样的,自己做了一个网站,开发的时候好好的,部署到服务器上去后,打开的时候白屏了好长时间才展示内容, 这可不能忍,必须找出原因优化掉!服务器配置CPU:1核,内存:2GiB,带宽:1Mbps这上来就找到原因了啊,这配置这么低,肯定慢啊,怎么办?...
    继续阅读 »

    起因是这样的,自己做了一个网站,开发的时候好好的,部署到服务器上去后,打开的时候白屏了好长时间才展示内容, 这可不能忍,必须找出原因优化掉!

    服务器配置

    CPU:1核,内存:2GiB,带宽:1Mbps

    这上来就找到原因了啊,这配置这么低,肯定慢啊,怎么办?

    换!!!

    然而贫穷像是一万多条无形的枷锁束缚住了我,让我换服务器的双手动弹不得。

    此路不通,只能另寻他法解决了。

    优化前首屏加载测试

    测试结果分析

    1. 从截图可以看到,首屏加载耗时19.15秒,主要是chunk-vendors.2daba5b2.js这个文件加载耗时最长,为17.6秒,大小为1.9M,其他文件均在4秒内加载完毕。通常页面加载的一个文件大小超过300k,已经算比较大了。第二个比较耗时的文件是chunk-vendors.62bee483.css,这个应该是样式文件。其他的文件加载耗时都不超过1秒,所以后面优化先从那两个文件下手。
    2. 重新编译项目,看下项目生成的文件

    可以看到前面提到的两个文件比较大,后面列出了每个文件使用gz压缩后的大小,但是浏览器实际并没有加载压缩后的文件,而是原始文件。再打开打包文件夹,发现实际生成的js文件夹中除了js文件,还有js.map文件,js.map文件通常用于开发环境调试用,方便我们查找错误,在生成环境是不需要用到的,而且都比较大,这也是一个优化的点。

    分析项目依赖情况

    运行vue ui,编译查看chunk-vendors中的结构发现,主要是element-ui依赖比较大,其次是vue和mavon-editor

    整个项目的情况如下

    那么如何优化呢

    开启nginx压缩配置

    修改nginx配置,启用gzip压缩

    gzip on;
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_buffers 16 8k;
    gzip_http_version 1.1;
    gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

    测试页面加载时间缩短到5.2秒,chunk-vendors.js传输大小为556k,加载时间为4秒,其他文件加载时间基本不超过200毫秒

    生产配置不生成js.map

    修改项目根目录中vue.config.js配置文件,设置productionSourceMap: false

    module.exports = {
    runtimeCompiler: true,
    productionSourceMap: false
    }

    打包测试文件夹大小由9.1M减小到2.26M

    配置gzip压缩插件

    执行npm i compression-webpack-plugin@5.0.1 -D安装插件,在vue.config.js中修改打包配置

    const CompressionPlugin = require("compression-webpack-plugin");
    const productionGzipExt = /\.(js|css|json|txt|html|ico|svg)(\?.*)?$/i;
    module.exports = {
    runtimeCompiler: true,

    productionSourceMap: false,

    configureWebpack: () => {
    if (process.env.NODE_ENV === "production") {
    return {
    plugins: [
    new CompressionPlugin({
    filename: "[path].gz[query]",
    algorithm: "gzip",
    test: productionGzipExt,
    threshold: 1024, // 大于1024字节的资源才处理
    minRatio: 0.8, // 压缩率要高于80%
    deleteOriginalAssets: false, // 删除原始资源,如果不支持gzip压缩的浏览器无法正常加载则关闭此项
    }),
    ],
    };
    }
    },
    };

    插件需要指定版本,最新版本的会报错这个和nginx压缩配置感觉重复了,实际测试和nginx压缩配置的速度差不多,如果两个压缩都有,速度并没有提升

    修改elementui组件按需引入

    1. 执行npm install babel-plugin-component -D安装 babel-plugin-component2. 修改.babelrc内容如下:
    {
    "presets": [["@babel/preset-env", { "modules": false}]],
    "plugins": [
    [
    "component",
    {
    "libraryName": "element-ui",
    "styleLibraryName": "theme-chalk"
    }
    ]
    ]
    }
    1. 在main.js中引入需要用到的组件,示例如下:
    import Vue from "vue";
    import App from "./App.vue";
    import router from "./router";
    import store from "./store";
    import "element-ui/lib/theme-chalk/index.css";
    import mavonEditor from "mavon-editor";
    import "mavon-editor/dist/css/index.css";
    import axios from "axios";
    import {
    Avatar,
    Button,
    Container,
    DatePicker,
    Dialog,
    Dropdown,
    DropdownItem,
    DropdownMenu,
    Footer,
    Form,
    FormItem,
    Header,
    Image,
    Input,
    Main,
    Message,
    MessageBox,
    Notification,
    Option,
    Select,
    Table,
    TableColumn,
    TabPane,
    Tabs,
    Timeline,
    TimelineItem,
    } from "element-ui";

    Vue.use(Button);
    Vue.use(Dialog);
    Vue.use(Dropdown);
    Vue.use(DropdownMenu);
    Vue.use(DropdownItem);
    Vue.use(Input);
    Vue.use(Select);
    Vue.use(Table);
    Vue.use(TableColumn);
    Vue.use(DatePicker);
    Vue.use(Form);
    Vue.use(FormItem);
    Vue.use(Tabs);
    Vue.use(TabPane);
    Vue.use(Header);
    Vue.use(Main);
    Vue.use(Footer);
    Vue.use(Timeline);
    Vue.use(TimelineItem);
    Vue.use(Image);
    Vue.use(Avatar);
    Vue.use(Container);
    Vue.use(Option);
    Vue.use(mavonEditor);
    Vue.prototype.$notify = Notification;
    Vue.prototype.$message = Message;
    Vue.prototype.$confirm = MessageBox.confirm;
    Vue.prototype.$axios = axios;
    Vue.config.productionTip = false;

    axios.interceptors.request.use(
    (config) => {
    config.url = "/api/" + config.url;
    config.headers.token = sessionStorage.getItem("identityId");
    return config;
    },
    (error) => {
    return Promise.reject(error);
    }
    );

    axios.interceptors.response.use(
    (response) => {
    if (response.data && response.data.exceptionCode) {
    const exceptionType = response.data.exceptionType;
    Notification({ title: response.data.exceptionMessage, type: exceptionType.toLowerCase() });
    return Promise.reject(response.data);
    }
    return response;
    },
    (error) => {
    return Promise.reject(error);
    }
    );

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

    修改按需引入后elementui依赖大小约为1.3M

    修改组件局部引入为异步组件

    在一个组件中引入其他组件时使用异步的方式引入,如

    export default {
    components: {
    register: () => import('./views/Register.vue'),
    login: () => import('./views/Login.vue')
    }
    };

    完成后此时chunk-vendors.js这个文件已经从优化前的1.9M缩小到890k

    页面加载约3秒可以显示出来,其他资源在页面显示后继续后台加载,全部加载完总耗时约5秒,请求数68次

    组件按组分块

    使用命名chunk语法webpackChunkName: "块名"将某个路由下的组件打包在同一个异步块中,如

    import Vue from "vue";
    import VueRouter from "vue-router";

    Vue.use(VueRouter);

    const routes = [
    {
    path: "/",
    redirect: 'home'
    },
    {
    path: '/home',
    component: () => import(/* webpackChunkName: "home-page" */ '../views/Home.vue')
    },
    {
    path: '/documents',
    component: () => import(/* webpackChunkName: "home-page" */ '../views/documents/DocumentList.vue')
    },
    {
    path: '/documentcontent',
    component: () => import(/* webpackChunkName: "home-page" */ '../views/documents/DocumentContent.vue')
    },
    {
    path: '/write',
    component: () => import(/* webpackChunkName: "home-page" */ '../views/WriteMarkdown.vue')
    },
    {
    path: '/about',
    component: () => import(/* webpackChunkName: "home-page" */ '../views/About.vue')
    },
    {
    path: '/management',
    component: () => import(/* webpackChunkName: "management" */ '../views/management/Management.vue'),
    children: [
    { path: '', component: () => import(/* webpackChunkName: "management" */ '../views/management/ManagementOptions.vue') },
    { path: 'developplan', component: () => import(/* webpackChunkName: "management" */ '../views/management/DevelopmentPlan.vue') },
    { path: 'tags', component: () => import(/* webpackChunkName: "management" */ '../views/management/TagsManage.vue') },
    { path: 'documents', component: () => import(/* webpackChunkName: "management" */ '../views/management/DocumentsManage.vue') }
    ]
    },
    {
    path: '/games',
    component: () => import(/* webpackChunkName: "games" */ '../views/games/Games.vue'),
    children: [
    { path: '', component: () => import(/* webpackChunkName: "games" */ '../views/games/GameList.vue') },
    { path: 'minesweeper', component: () => import(/* webpackChunkName: "games" */ '../views/games/minesweeper/MineSweeper.vue') }
    ]
    },
    {
    path: '/tools',
    component: () => import(/* webpackChunkName: "tools" */ '../views/tools/ToolsView.vue'),
    children: [
    { path: '', component: () => import(/* webpackChunkName: "tools" */ '../views/tools/ToolsList.vue') },
    { path: 'imageconverter', component: () => import(/* webpackChunkName: "tools" */ '../views/tools/ImageConverter.vue') }
    ]
    }
    ];

    const router = new VueRouter({
    mode: "history",
    base: process.env.BASE_URL,
    routes,
    });

    export default router;

    打包编译后文件比之前要减少了一部分,并且合并后的文件资源也不大,完全可以接受

    页面加载耗时基本没变,但是请求数减少到51次

    总结

    1. nginx压缩对性能的提升最大,通过压缩文件缩短资源加载时间
    2. gzip压缩插件会将文件压缩成gz格式,暂时不知道怎么用
    3. elementui按需引入会减小依赖资源的大小,chunk-vendors.js文件体积会减小
    4. 使用异步组件可以在后台加载渲染不到的资源,优先加载渲染需要的资源,缩短页面响应时间,但同时会增加打包后的文件数量,导致页面请求数量增加。
    5. 组件按路由分组,打包的时候会将相同组名的资源打包到一个文件中,可以减小请求数


    作者:宠老婆的程序员
    来源:juejin.cn/post/7351292656633331747
    收起阅读 »

    接了个私活,分享下我是如何从 0 到 1 交付项目的

    web
    大家好,我是阿杆,不是阿轩。 最近有个校友找到我,他自己办了个公司,想做个微信小程序,于是找我帮他开发,当然不是免费的。 我一想,那挺好呀,虽然我没接过私活吧,但不代表我以后不会接私活,这不正好可以练习一下子。 前前后后弄了一个半月到两个月,也算是积累了一点经...
    继续阅读 »

    大家好,我是阿杆,不是阿轩。


    最近有个校友找到我,他自己办了个公司,想做个微信小程序,于是找我帮他开发,当然不是免费的。


    我一想,那挺好呀,虽然我没接过私活吧,但不代表我以后不会接私活,这不正好可以练习一下子。


    前前后后弄了一个半月到两个月,也算是积累了一点经验,分享给大家,如果以后也接到私活,可以参考一下我的开展方式。


    由于文中涉及到实际业务的东西不方便透露, 下面我将用图书管理系统来代替实际业务,并且称这位校友为“老板”。


    image-20240421154347807


    总览


    我接手的这个项目是完完全全从0开始的,老板只有一个idea,然后说他的idea是来自于另一个小程序的,有部分内容可以参考那个小程序,其他什么都没有了。


    先讲一下我的总体流程:



    1. 确定老板的大致需求,以及预期费用

    2. 详细梳理开发功能点,并简单画下原型图

    3. 工时评估,确定费用

    4. 出正式的UI设计稿

    5. 拟定合同、签合同

    6. 开发阶段

    7. 验收、上线


    大概就是这么些步骤,也对应本文的目录,如果你有想看的部分,可以直接跳转到对应的部分。


    下面我会详细讲讲每一步我都做了些什么。


    确定需求


    首先老板找到我,跟我说他想做一个图书管理的微信小程序,然后讲了几个小程序内的功能点。


    我也向他提了几个问题:



    1. 预算有多少?



      这个肯定得问的,要是预算和工作量严重不匹配,那肯定做不了的。毕竟都是出来赚钱的,总不能让咱用爱发电吧?




    2. 预计一年内会有多少用户量?会有多少数据量?



      这个问题我主要是想知道并发量会有多少、数据量会有多少?这样方便我后续判断系统需要的配置,也便于我后续对整个系统的设计。


      好在整体用户量和数据量都不大,这对我来说也就没什么压力了,至于以后会发展到如何,这不是我该考虑的事情,我顶多把代码写好看点,他后续发展壮大了肯定是把项目接到公司里雇人做的,跟我也没什么关系。




    3. 你那边能够提供什么?



      这个主要是看对方有什么资源,是否能够对项目开发有一定的帮助。


      在我这个项目里,老板那边是什么都没有的,没有设计图、没有服务器资源、也没有辅助人员,所有内容都包揽在我这边,然后有什么问题就直接问他。




    4. 你希望多久完成?



      如果老板很急的话,那可能得多叫几个人一起做,如果时间充足的话,自己一个人做也不是不可以。





    好了,第一次对话大概就是这么些内容,但仅靠一次对话肯定是无法确定需求的,只能了解个大概。


    我根据老板的想法,写了一份 需求分析 出来,首先列出了几个大概的功能点:


    大致功能点列举


    然后根据这些功能点进行扩展,把所有功能列举出来,画成一个思维导图(打码比较多,大家将就将就😅):


    延伸的思维导图


    好,那么第一版的需求分析差不多就出来了,接着我打电话给老板,对着这个思维导图,一个一个的跟老板确认,需不需要这些功能。


    老板一般会提出一些异议,我再继续修改思维导图,直到老板觉得这些功能他都满意了。当然这过程中我也会给一些自己的建议,有些超预算的功能,还是建议他不要做。


    到这里,需求就基本确定好了。


    梳理开发功能点、绘制原型图


    由于我不会前端开发,只是个简单的后端仔,所以我还找了一个前端同学一起做。


    我和前端两个人根据前面的需求文档,详细的梳理出了 小程序 和 后台管理系统 的功能,这个部分是比较重要的,因为后续画设计稿和开发都会以这份文档为主:


    小程序功能梳理文档


    还画了一些简单的原型图,这玩意丑点没事,能让人看懂就行🤣🤣:


    小程序原型图-我的信息


    后台管理系统原型图


    这些东西弄完之后,再找老板进行一遍确认,把里面每个点都确认下来,达成共识。


    工时评估,确定费用


    老板觉得OK了,就到了该谈钱的时候了,前面只是聊了预算,并不是正式的确定费用。


    那咱们也不能张嘴就要,要多了老板不乐意,要少了自己吃亏。


    所以咱们先评估下工时,这边我分了几个部分分别进行工时评估:



    • 需求分析、功能梳理(就是前面做的那些,还没收钱的呢)

    • UI设计、交互设计

    • 前端开发

    • 后端开发

    • 系统运维(包含服务器购买、搭建、认证、配置等)

    • 后期维护


    其中设计稿是找另一位朋友做的,钱单独算,然后其他部分都是我和前端同学两个人评估的,评估的粒度还是比较细的,是以小时为单位进行计算的,给大家大概看一下:


    前端开发工时评估


    后端开发工时评估


    评估完之后汇总一下,然后根据我们自己工作的时薪,给老板一个最终的定价,正常的话还需要在这个定价上再乘一个接单系数(1.2~1.5),但是我们这个老板是校友啊,而且预算也不多,所以就没乘这个系数了(还给他打了折😂,交个朋友)。


    定价报出去之后,老板觉得贵了怎么办?很简单,砍功能呗,要么你去找别人做也行。



    预付订金



    我觉得正常应该在梳理功能之前就要付一部分订金,也不用多少,几百块就行,算是给我们梳理功能的钱。



    这里接下来就要画UI图了,我们先找老板付个订金,订金分为三部分:



    • 给前端的订金

    • 给后端的订金

    • 给UI同学画设计稿的完整费用


    因为UI设计是我这边联系的,所以我肯定得先保障她的费用能完整到手,不然到时候画完图跟我说不做了,那我咋对得起画图的人。


    画UI图


    这部分就不用咱们操心了,把文档交给设计同学,然后等她出图就行。


    这个过程中也可以时不时去看看她画的内容符不符合咱们的预期,当个小小的监工。


    盯着干活


    画完稿子需要跟老板、开发都对一遍,看看有没有出入,符不符合预期,有问题及时修改下,没问题就按照这份稿子进行开发了。


    拟定合同、签合同



    合同也是我来拟定的,其实是先到网上找了个软件开发的合同模板,然后再根据自己的想法进行合理的调整。



    为什么我要到这一步才签合同呢?我觉得合同内容越细致越好,最好是能够把要开发的内容、样式都写在合同上,这样省得后面扯皮。


    现在文档也出了,图也画完了,那咱们把这些东西都贴在和合同的附件上,然后附上这些条约:



    • 乙方将严格按照经过甲方审核的《软件功能设计书》的要求进行软件的开发设计。

    • 甲方托付乙方开发的软件在签订合同之后如需增加其它功能,必须以书面形式呈交给乙方,乙方做改动并酌情收取适当费用。


    这样就可以保障我们在开发完后不会被恶意的增加或者修改功能了。


    再改一次


    这里我再列一些其他需要特别注意的点:



    1. 乙方交付日期,以及最多延期多久,如果超时怎么办?

    2. 甲方付款方式和日期(我们是用的 442 ,开工付 40%,中期验收付 40%,开发完验收付 20%)。

    3. 甲方拖欠项目款的处理方式(支付迟延履行金等)。

    4. 服务器费用是谁出?如果是乙方,需要注意包服务器的时限。

    5. 项目维护期,一般一年半年的吧。

    6. 乙方不保证项目 100% 可用,只能保障支撑 多少人 使用,支撑同时在线人数 多少人 ,如果遇到恶意攻击,不归乙方负责。

    7. 软件归属权是谁的?(如果项目款比较少的话,乙方可以要求要软件归属权,之后甲方如果想把项目接回去自己公司维护的话,需要从乙方手里买,这样乙方可以回点血)


    大概就是这些吧,还有其他的东西基本都是按照模板的,没怎么改。


    弄完给老板看看,没问题就签了,有问题两方再协商一下,我们这边是直接签了的。



    开发阶段


    开发没什么好说的,跟你在公司开发一样。


    不过你接私活可不能在公司开发🚫,只能回家了自己干,不然被抓到上班干私活,你看老板裁不裁你就完事了。


    微信小程序上线注意事项


    微信小程序对请求的接口有三个基本要求:



    1. 必须是有备案的域名。

    2. 必须是有SSL证书(https)。

    3. 域名不得带端口号。


    这个域名的问题必须要尽早解决,不然后面开发完了再去弄的话,工信部备案审核都要挺久的,不延期都难。


    还有一种方式,我在逛微信开放社区看到的,使用云函数进行中转,间接请求ip接口,感觉是可行的,也比较省事,具体操作大家可以自己去探索一下。


    我也是吃了没有经验的亏,买域名 + 工信部备案 + 公安备案 + 小程序备案,这一套操作下来真给我整难受死了,直接用云函数省事多了。



    验收、上线


    这部分也没什么好说的,大家在公司也经常经历这个步骤。


    多沟通,多确认,


    唯一需要提醒的是,验收的时候咱不能无条件接收老板的任何要求,毕竟价格和开发内容都是已经定好的,如果要加内容或者改内容,记得酌情要一点工时费,可不能亏待了自己。



    后记


    整个过程中,其实沟通是最重要的,写代码谁不会是吧?但是得让老板觉得OK才行,如果有什么疑问或者觉得不合理的地方啊,最好是尽早沟通,不然越到后面只会让问题变的越来越大。


    最近刚做完这个项目,说实话没赚什么钱,甚至有点小亏😅。而且这个老板还有点拖欠工资的感觉,中期项目款拖到了项目交付才给,项目尾款到目前还没付😅😅。不过还好合同里写到了关于这块的处理方式,倒也不担心他不付这个钱。


    (虽然我也不知道在哪能接到靠谱的私活🤣,但也可以先收藏本文,万一之后来活了,还能翻出来看看)


    最后,希望各位都能接到 very good 的私活,祝大家早日实现财富自由!


    webwxgetmsgimg (1)


    作者:阿杆
    来源:juejin.cn/post/7359764922727333939
    收起阅读 »

    仿今日头条,H5 循环播放的通知栏如何实现?

    web
    我们在各大 App 活动页面,经常会看到循环播放的通知栏。比如春节期间,我就在今日头条 App 中看到了如下通知:「春节期间,部分商品受物流影响延迟发货,请耐心等待,祝你新春快乐!」。 那么,这种循环播放的通知栏如何实现呢?本文我会先介绍它的布局、再介绍它的...
    继续阅读 »

    我们在各大 App 活动页面,经常会看到循环播放的通知栏。比如春节期间,我就在今日头条 App 中看到了如下通知:「春节期间,部分商品受物流影响延迟发货,请耐心等待,祝你新春快乐!」。


    toutiao.gif


    那么,这种循环播放的通知栏如何实现呢?本文我会先介绍它的布局、再介绍它的逻辑,并给出完整的代码。最终我实现的效果如下:


    loop-notice.gif


    拳打 H5,脚踢小程序。我是「小霖家的混江龙」,关注我,带你了解更多实用的 H5、小程序武学。


    布局代码


    我们先看布局,如下图所示,循环播放的布局不是普通的左中右布局。可以看到,当文字向左移动时,左边的通知 Icon 和右边的留白会把文字挡住一部分。


    block-out.png


    为了实现这样的效果,我们给容器 box 设置一个相对定位,并把 box 中的 HTML 代码分为三部分:



    • 第一部分是 content,它包裹着需要循环播放的文字;

    • 第二部分是 left,它是左边的通知 Icon,我们给它设置绝对定位和 left: 0;

    • 第三部分是 right,它是右边的留白,我们给它设置绝对定位和 right: 0;


    <div class="box">
    <div class="content">

    div>
    <div class="left">🔔div>
    <div class="right">div>
    div>

    .box {
    position: relative;
    overflow: hidden;
    /* ... 省略 */
    }
    .left {
    position: absolute;
    left: 0;
    /* ... 省略 */
    }
    .right {
    position: absolute;
    right: 0;
    /* ... 省略 */
    }

    现在我们来看包裹文字的 content。content 内部包裹了三段一模一样的文字 notice,每段 notice 之间还有一个 space 元素作为间距。



    <div id="content">
    <div class="notice">春节期间,部分商品...div>
    <div class="space">div>
    <div class="notice">春节期间,部分商品...div>
    <div class="space">div>
    <div class="notice">春节期间,部分商品...div>
    div>


    为什么要放置三段一模一样的文字呢?这和循环播放的逻辑有关。


    逻辑代码


    我们并没有实现真正的循环播放,而是欺骗了用户的视觉。如下图所示:



    • 播放通知时,content 从 0 开始向左移动。

    • 向左移动 2 * noticeWidth + spaceWidth 时,继续向左移动便会露馅。因为第 3 段文字后不会有第 4 段文字。


      如果我们把 content 向左移动的距离强行从 2 * noticeWidth + spaceWidth 改为 noticeWidth,不难看出,用户在 box 可视区域内看到的情况基本一致的。


      然后 content 继续向左移动,向左移动的距离大于等于 2 * noticeWidth + spaceWidth 时,就把距离重新设为 noticeWidth。循环往复,就能欺骗用户视觉,让用户认为 content 能无休无止向左移动。



    no-overflow-with-comment.png


    欺骗视觉的代码如下:



    • 我们通过修改 translateX,让 content 不断地向左移动,每次向左移动 1.5px;

    • translateX >= noticeWidth * 2 + spaceWidth 时,我们又会把 translateX 强制设为 noticeWidth

    • 为了保证移动动画更丝滑,我们并没有采用 setInterval,而是使用 requestAnimationFrame。


    const content = document.getElementById("content");
    const notice = document.getElementsByClassName("notice");
    const space = document.getElementsByClassName("space");
    const noticeWidth = notice[0].offsetWidth;
    const spaceWidth = space[0].offsetWidth;

    let translateX = 0;
    function move() {
    translateX += 1.5;
    if (translateX >= noticeWidth * 2 + spaceWidth) {
    translateX = noticeWidth;
    }
    content.style.transform = `translateX(${-translateX}px)`;
    requestAnimationFrame(move);
    }

    move();

    完整代码


    完整代码如下,你可以在 codepen 或者码上掘金上查看。



    总结


    本文我介绍了如何用 H5 实现循环播放的通知栏:



    • 布局方面,我们需要用绝对定位的通知 Icon、留白挡住循环文字的左侧和右侧;此外,循环播放的文字我们额外复制 2 份。

    • 逻辑方面,通知栏向左移动 2 * noticeWidth + spaceWidth 后,我们需要强制把通知栏向左移动的距离从 2 * noticeWidth + spaceWidth 变为 noticeWidth,以此来欺骗用户视觉。




    作者:小霖家的混江龙
    来源:juejin.cn/post/7372765277460496394
    收起阅读 »

    完美代替节假日API,查询中国特色日期

    web
    马上端午节到了,趁着前夕,写了个关于中国特色日期查询的库;由于中国的节假日不固定,以及阴历日期较为特殊,尽可能的做了代码上压缩,做到了 .min.js 源文件尺寸小于 16kb,gzip 压缩后只有 7kb 大小,欢迎大家 PR 使用。 关于中国节假日,后面会...
    继续阅读 »

    马上端午节到了,趁着前夕,写了个关于中国特色日期查询的库;由于中国的节假日不固定,以及阴历日期较为特殊,尽可能的做了代码上压缩,做到了 .min.js 源文件尺寸小于 16kbgzip 压缩后只有 7kb 大小,欢迎大家 PR 使用。


    关于中国节假日,后面会跟随国务院发布进行更新,一言既出,驷马难追。


    当前版本


    NPM Version
    GitHub License
    README


    项目地址:github.com/vsme/chines…


    提供的功能



    1. 中国节假日(含调休日、工作日)查询,支持 2004年 至 2024年,包括 2020年 的春节延长;

    2. 24节气查询;

    3. 农历 阳历 互相转换,含有生肖和天干地支。


    还需要的功能欢迎补充。


    对于非 JS 语言


    提供了中国节假日的 JSON 文件,通过链接 chinese-days.json 可以直接引用。


    https://cdn.jsdelivr.net/npm/chinese-days/dist/chinese-days.json


    快速开始


    推荐方式:直接浏览器引入,会跟随国务院发布更新。


    <script src="https://cdn.jsdelivr.net/npm/chinese-days/dist/index.min.js"></script>
    <script>
    const { isHoliday } = chineseDays
    console.log(isHoliday('2024-01-01'))
    </script>

    其他方式安装


    npm i chinese-days

    使用 ESM 导入


    import chineseDays from 'chinese-days'
    console.log(chineseDays)

    在 Node 中使用


    const { isWorkday, isHoliday } = require('chinese-days');
    console.log(isWorkday('2020-01-01'));
    console.log(isHoliday('2020-01-01'));

    节假日模块


    isWorkday 检查某个日期是否为工作日


    console.log(isWorkday('2023-01-01')); // false

    isHoliday 检查某个日期是否为节假日


    console.log(isHoliday('2023-01-01')); // true

    isInLieu 检查某个日期是否为调休日(in lieu day)


    在中国的节假日安排中,调休日是为了连休假期或补班而调整的工作日或休息日。例如,当某个法定假日与周末相连时,可能会将某个周末调整为工作日,或者将某个工作日调整为休息日,以便连休更多天。


    // 检查 2024-05-02 返回 `true` 则表示是一个调休日。
    console.log(isInLieu('2024-05-02')); // true

    // 检查 2024-05-01 返回 `false` 则表示不是一个调休日。
    console.log(isInLieu('2024-05-01')); // false

    getDayDetail 检查指定日期是否是工作日


    函数用于检查指定日期是否是工作日,并返回一个是否工作日的布尔值和日期的详情。



    1. 如果指定日期是工作日,则返回 true 和工作日名称,如果是被调休的工作日,返回 true 和节假日详情。

    2. 如果是节假日,则返回 false 和节假日详情。


    // 示例用法

    // 正常工作日 周五
    console.log(getDayDetail('2024-02-02')); // { "date": "2024-02-02", "work":true,"name":"Friday"}
    // 节假日 周末
    console.log(getDayDetail('2024-02-03')); // { "date": "2024-02-03", "work":false,"name":"Saturday"}
    // 调休需要上班
    console.log(getDayDetail('2024-02-04')); // { "date": "2024-02-04", "work":true,"name":"Spring Festival,春节,3"}
    // 节假日 春节
    console.log(getDayDetail('2024-02-17')); // { "date": "2024-02-17", "work":false,"name":"Spring Festival,春节,3"}

    getHolidaysInRange 获取指定日期范围内的所有节假日


    接收起始日期和结束日期,并可选地决定是否包括周末。如果包括周末,则函数会返回包括周末在内的所有节假日;否则,只返回工作日的节假日。



    tip: 即使不包括周末,周末的节假日仍然会被返回



    // 示例用法
    const start = '2024-04-26';
    const end = '2024-05-06';

    // 获取从 2024-05-01 到 2024-05-10 的所有节假日,包括周末
    const holidaysIncludingWeekends = getHolidaysInRange(start, end, true);
    console.log('Holidays including weekends:', holidaysIncludingWeekends.map(d => getDayDetail(d)));

    // 获取从 2024-05-01 到 2024-05-10 的节假日,不包括周末
    const holidaysExcludingWeekends = getHolidaysInRange(start, end, false);
    console.log('Holidays excluding weekends:', holidaysExcludingWeekends.map(d => getDayDetail(d)));

    getWorkdaysInRange 取指定日期范围内的工作日列表


    接收起始日期和结束日期,并可选地决定是否包括周末。如果包括周末,则函数会返回包括周末在内的所有工作日;否则,只返回周一到周五的工作日。


    // 示例用法
    const start = '2024-04-26';
    const end = '2024-05-06';

    // 获取从 2024-05-01 到 2024-05-10 的所有工作日,包括周末
    const workdaysIncludingWeekends = getWorkdaysInRange(start, end, true);
    console.log('Workdays including weekends:', workdaysIncludingWeekends);

    // 获取从 2024-05-01 到 2024-05-10 的工作日,不包括周末
    const workdaysExcludingWeekends = getWorkdaysInRange(start, end, false);
    console.log('Workdays excluding weekends:', workdaysExcludingWeekends);

    findWorkday 查找工作日


    查找从今天开始 未来的第 {deltaDays} 个工作日。


    // 查找从今天开始 未来的第 {deltaDays} 个工作日
    // 如果 deltaDays 为 0,首先检查当前日期是否为工作日。如果是,则直接返回当前日期。
    // 如果当前日期不是工作日,会查找下一个工作日。
    const currentWorkday = findWorkday(0);
    console.log(currentWorkday);

    // 查找从今天开始未来的第一个工作日
    const nextWorkday = findWorkday(1);
    console.log(nextWorkday);

    // 查找从今天开始之前的前一个工作日
    const previousWorkday = findWorkday(-1);
    console.log(previousWorkday);

    // 可以传第二个参数 查找具体日期的上下工作日
    // 查找从 2024-05-18 开始,未来的第二个工作日
    const secondNextWorkday = findWorkday(2, '2024-05-18');
    console.log(secondNextWorkday);

    节气模块


    获取 24 节气的日期


    import { getSolarTerms } from "chinese-days";

    /** 获取范围内 节气日期数组 */
    const solarTerms = getSolarTerms("2024-05-01", "2024-05-20");
    solarTerms.forEach(({ date, term, name }) => {
    console.log(`${name}: ${date}, ${term}`);
    });
    // 立夏: 2024-05-05, the_beginning_of_summer
    // 小满: 2024-05-20, lesser_fullness_of_grain

    // 没有节气 返回 []
    getSolarTerms("2024-05-21", "2024-05-25");
    // return []

    /* 不传 end 参数, 获取某天 节气 */
    getSolarTerms("2024-05-20");
    // return: [{date: '2024-05-20', term: 'lesser_fullness_of_grain', name: '小满'}]

    阳历农历互转


    特别说明,此库中:



    1. 2057-09-28 为:农历丁丑(牛)年八月三十;

    2. 2097-08-07 为:农历丁巳(蛇)年七月初一。


    阳历转换农历


    // 2097-8-7
    console.log(getLunarDate('2097-08-07'))

    // 2057-9-28
    console.log(getLunarDate('2057-09-28'))
    // {
    // date: "2057-09-28",
    // lunarYear: 2057,
    // lunarMon: 8,
    // lunarDay: 30,
    // isLeap: false,
    // lunarDayCN: "三十",
    // lunarMonCN: "八月",
    // lunarYearCN: "二零五七",
    // yearCyl: "丁丑",
    // monCyl: "己酉",
    // dayCyl: "戊子",
    // zodiac: "牛"
    // }

    // 非闰月 和 闰月例子
    console.log(getLunarDate('2001-04-27'))
    console.log(getLunarDate('2001-05-27'))

    根据阳历日期区间,批量获取农历日期


    console.log(getLunarDatesInRange('2001-05-21', '2001-05-26'))

    农历转换阳历


    当为阴历闰月的时候,会出现一个农历日期对应两个阳历日期的情况,所以返回对象形式。


    console.log(getSolarDateFromLunar('2001-03-05'))
    // {date: '2001-03-29', leapMonthDate: undefined}

    console.log(getSolarDateFromLunar('2001-04-05'))
    // {date: '2001-04-27', leapMonthDate: '2001-05-27'}

    欢迎贡献代码



    1. Fork + Clone 项目到本地;

    2. 节假日: 修改 节假日定义

    3. 农历定义: 修改 农历定义

    4. 其他修改需要自己查看源码;

    5. 执行命令 npm run generate 自动生成 节假日常量文件

    6. 提交PR。


    致谢



    1. 农历数据来自于 Bigkoo/Android-PickerView 项目。

    2. 中国节假日数据参考了 Python 版本的 LKI/chinese-calendar 项目。


    作者:Yaavi
    来源:juejin.cn/post/7371815617462714402
    收起阅读 »

    在滴滴开发H5一年了,我遇到了这些问题

    web
    IOS圆角不生效 ios中使用border-radius配合overflow:hidden出现了失效的情况: 出现此问题的原因是因为ios手机会在transform的时候导致border-radius失效 解决方法:在使用动画效果带transform的元...
    继续阅读 »

    IOS圆角不生效


    ios中使用border-radius配合overflow:hidden出现了失效的情况:


    image.png



    出现此问题的原因是因为ios手机会在transform的时候导致border-radius失效



    解决方法:在使用动画效果带transform的元素的上一级div元素的css加上下面语句:


    -webkit-transform:rotate(0deg);

    IOS文本省略溢出问题


    在部分ios手机上会出现以下情况:


    image.png


    原因


    在目标元素上设置font-size = line-height,并加上以下单行省略代码:


    .text-overflow {
    display: -webkit-box;
    overflow : hidden;
    text-overflow: ellipsis;
    word-break: break-all;
    -webkit-line-clamp: 1;
    -webkit-box-orient: vertical;
    }

    或者:


    .text-overflow {
    overflow : hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
    }

    由于不同系统包含的字体的行高不一致,即使设置了height = line-height 一样会有以上问题


    解决方案


    经过测试,在height = line-height = font-szie的情况下,加上padding-top: 1px可以解决这个问题,即在需要使用单行省略的地方加上:


    .demo {
    height: 28px;
    line-height: 28px;
    font-size: 28px;
    padding-top: 1px;
    }

    如:<div class="text-overflow demo">我是需要进行单行省略的文案</div>


    安卓手机按钮点击后有橙色边框


    image.png


    解决方案:


    button:focus {
    outline: none;
    }

    优惠券打孔效果


    需求中经常需要实现一类效果:优惠券打孔,如下图所示:


    image.png


    通常情况下会找设计采用图片的的形式,但这个方案最大的缺陷是无法适配背景的变化。
    因此,我们可以采用如下方案,左右两侧各打一个孔,且穿透背景:


    image.png


    具体细节可以参考这篇文章:纯 CSS 实现优惠券透明圆形镂空打孔效果


    Clipboard兼容性问题


    navigator.clipboard兼容性不是很好,低版本浏览器不支持


    image.png


    解决方案:


    const copyText = (text: string) => {
    return new Promise(resolve => {
    if (navigator.clipboard?.writeText) {
    return resolve(navigator.clipboard.writeText(text))
    }
    // 创建输入框
    const textarea = document.createElement('textarea')
    document.body.appendChild(textarea)
    // 隐藏此输入框
    textarea.style.position = 'absolute'
    textarea.style.clip = 'rect(0 0 0 0)'
    // 赋值
    textarea.value = text
    // 选中
    textarea.select()
    // 复制
    document.execCommand('copy', true)
    textarea.remove()
    return resolve(true)
    })
    }

    Unocss打包后样式不生效


    这个问题是由webpack缓存导致的,在vue.config.js中添加以下代码:


    config.module.rule('vue').uses.delete('cache-loader')

    具体原因见:UnoCSS webpack插件原理


    低端机型options请求不过问题


    在我们的业务需求中,覆盖的人群很广,涉及到的机型也很多。于是我们发现在部分低端机型下(oppo R11、R9等),有很多请求只有options请求,没有真正的业务请求。导致用户拿不到数据,报network error错误,我们的埋点数据也记录到了这一异常。


    在我们的这个项目中,我们的后台有两个,一个提供物料,一个提供别的数据。但是奇怪的是,物料后台是可以正常获取数据,但业务后台就不行!


    经过仔细对比二者发送的options请求,发现了问题所在:


    image.png


    发现二者主要存在以下差异:



    1. Access-Control-Allow-Headers: *

    2. Access-Control-Allow-origin: *


    于是我便开始排查两个响应头的兼容性,发现在这些低端机型上,Access-Control-Allow-Headers: *确实会有问题,这些旧手机无法识别这个通配符,或者直接进行了忽略,导致options请求没过,自然就没有后续真正的请求了。


    image.png


    解决方案:由后台枚举前端需要的headers,在Access-Control-Allow-Headers中返回。


    此外,将Access-Control-Allow-Origin设置为*也有一些别的限制:



    参考



    作者:WeilinerL
    来源:juejin.cn/post/7372396174249459750
    收起阅读 »

    前端命令行部署:再也不用把dist包给后端部署服务了!

    web
    好物推荐 超简单的命令行部署。给在小公司部署还是给后端dist包的萌新小伙伴们~ 这边项目本身就是使用命令行部署到,不过那个命令行工具是自己写的,是嵌入到公司某一个私有npm包里,和其他依赖耦合在一起。灵活性不是很好。 这两天发现了一个别人写的一个deploy...
    继续阅读 »

    好物推荐


    超简单的命令行部署。给在小公司部署还是给后端dist包的萌新小伙伴们~


    这边项目本身就是使用命令行部署到,不过那个命令行工具是自己写的,是嵌入到公司某一个私有npm包里,和其他依赖耦合在一起。灵活性不是很好。
    这两天发现了一个别人写的一个deploy cli。感觉蛮好用的。分享一下。


    希望可以帮助更多刚入行小伙伴了解更多前端玩法。


    前端命令行部署


    很多公司的前端部署流程都是先打一个dist包。然后给后端同事帮忙部署。


    前端:::
    1714281510854.png


    后端:::


    529ae5c36b03377bf116bafea2e95f1.png


    (开玩笑的,工作中的后端同事都没那么调皮)


    本文的内容就是如何使用命令行进行前端自动部署。


    我们整个网站的读取,其实就是我们上传一个静态的文件包到服务器,然后服务器上的后台服务读取我们的静态包,来进行页面的展示。所以,前端自动化部署的关键,就是,能把dist包传到服务器的指定目录下就OK了。


    部署流程


    推荐一个deploy cli工具(deploy-cli-service)


    安装


    1. 执行 npm install deploy-cli-service -g 进行全局安装 。

    2. 执行 deploy-cli-service - v 查看版本


    初始化配置文件

    在项目根目录执行 deploy-cli-service init 进行初始化


    deploy-cli-service init命令执行后项目目录下会出现一个名为deploy.config的文件


    image.png


    deploy-cli-service init初始化的内容会被默认输入到 deploy.config


    修改配置文件

    deploy-cli-service init初始化之后输入的内容都会默认被写入deploy.config文件中。


    image.png


    然后看看相关的属性有没有什么需要修改的就ok。


    配置部署命令


    image.png


    "deploy:test": "deploy-cli-service deploy --mode test"," 写入到 package.json中的script里。


    然后在命令行执行 "npm run deploy:test"


    成功部署后会如下显示


    image.png


    image.png


    注意


    配置 deploy.config.js时尽量使用ssh证书登录,不要使用服务器密码,把服务器密码写在前端代码里是一件非常不好的操作。


    deploy-cli-service npm地址


    luck


    作者:工边页字
    来源:juejin.cn/post/7362924623825256463
    收起阅读 »

    互联网+《周易》:我在github学算卦

    web
    前言 《周易》乃周文王姬昌所作,是中国传统思想文化中自然哲学与人文实践的理论根源,是古代汉民族思想、智慧的结晶,被誉为“大道之源”。内容极其丰富,对中国几千年来的政治、经济、文化等各个领域都产生了极其深刻的影响。 像这种千古奇书,每个中国人都应该读一读,一是因...
    继续阅读 »

    前言


    《周易》乃周文王姬昌所作,是中国传统思想文化中自然哲学与人文实践的理论根源,是古代汉民族思想、智慧的结晶,被誉为“大道之源”。内容极其丰富,对中国几千年来的政治、经济、文化等各个领域都产生了极其深刻的影响。


    像这种千古奇书,每个中国人都应该读一读,一是因为这是老祖宗的智慧,我们不能丢弃;二是因为《周易》蕴含宇宙人文的运行规律,浅读可修身养性,熟读可明自我,深究可知未来,参透就可知天命了。


    东汉著名史学家、文学家班固在《汉书•艺文志》中提出《周易》的成书是:人更三圣,世历三古


    那么在哪里才可以读到呢?


    其实易经的完本在网上随便就可以找到,但是都不适合在摸鱼的时候读 (!🤡),打开花花绿绿或者神神叨叨的小网站,你的 leader 肯定一眼就看出你在摸鱼。


    既然没有这种网站,那干脆自己做一个。


    vitePress + github pages 快速搭建


    vitePress 快速开始


    pnpm add -D vitepress

    pnpm vitepress init

    填写完 cli 里的几个问题,项目就可以直接运行了。可以看到网站直接解析了几个 示例的 md 文件,非常的神奇。


    处理《周易》文本


    那么哪里才可以找到《周易》的 markdown 版本呢,找了一圈也没有找到,最后找到了一个 txt 的,我觉得写个脚本转换一下。


    首先,我拿 vscode 的正则给每个标题加上井号,使其成为一级标题


    QQ2024511-183935.webp


    此时,所有的标题都被改成了md格式的一级标题,然后直接将后缀名从 .txt 改为 .md 即可。


    看过 vitepress 的文档并经过实操后发现,它的目录是一个一个的小 markdown 文件组成的,而单个 markdown 内的标题等在右侧显示


    image.png


    那么此时就需要把《周易》完本,按照六十四卦分为六十四个 md 文件。


    我写了一个node脚本:


    const fs = require('fs');

    // 读取zhouyi.md文件
    fs.readFile('zhouyi.md', 'utf8', (err, data) => {
     if (err) {
       console.error('读取文件出错:', err);
       return;
    }

     // 按一级标题进行分割
     const sections = data.split('\n# ');

     // 循环处理每个一级标题的内容
     sections.forEach((section, index) => {
       // 提取标题和内容
       const lines = section.split('\n');
       const title = lines[0];
       const content = lines.slice(1).join('\n');

       // 写入到单独的文件中
       const fileName = `zhouyi_${index + 1}.md`;
       fs.writeFile(fileName, `# ${title}\n\n${content}`, err => {
         if (err) {
           console.error(`写入文件 ${fileName} 出错:`, err);
        } else {
           console.log(`已创建文件: ${fileName}`);
        }
      });
    });
    });


    取名为md-slicer.js ,在控制台输入


    node md-slicer.js

    即可生成


    image.png


    然后写一个在 .vitepress/config.mtssidebar的生成函数:


    let itemsLength = 64
    function getSidebar() {
     let items: {}[] = [{
       text: '《周易》是什么?',
       link: '/what.md'
    }]
     for (let i = 1; i <= itemsLength; i++) {
       items.push({ text: `第${numberToChinese(i)}卦`, link: `/zhouyi_${i}.md` })
    }
     return items
    }

    numberToChinese函数用来将阿拉伯数字转为中文数字,因为周易只有六十四卦,所以不用考虑很多,够用即可


    // numberToChinese
    function numberToChinese(number) {
     const chineseNumbers = ['零', '一', '二', '三', '四', '五', '六', '七', '八', '九'];
     const chineseUnits = ['', '十', '百', '千', '万', '亿'];

     // 将数字转换为字符串,以便于处理每一位
     const numStr = String(number);

     let result = '';
     let zeroFlag = false; // 用于标记是否需要加上“零”

     for (let i = 0; i < numStr.length; i++) {
       const digit = parseInt(numStr[i]); // 当前位的数字
       const unit = chineseUnits[numStr.length - i - 1]; // 当前位的单位

       if (digit !== 0) {
         if (zeroFlag) {
           result += chineseNumbers[0]; // 如果前一位是零,则在当前位加上“零”
           zeroFlag = false;
        }
         result += chineseNumbers[digit] == "一" && unit == "十" ? unit : chineseNumbers[digit] + unit; // 加上当前位的数字和单位,当一十时,省略前面的一
      } else {
         zeroFlag = true; // 如果当前位是零,则标记为需要加上“零”
      }
    }
     return result;
    }

    然后,设置一下vitepress基础配置和打包输出路径


    export default defineConfig({
     title: "周易",
     description: "周易",
     base: "/thebookofchanges/",
     head: [
      ['link', { rel: 'icon', href: 'yi.svg' }] // 这里是你的 Logo 图片路径
    ],
     outDir: 'docs', // 输出到docs ,可以直接在 github pages 使用
     themeConfig: {
       // https://vitepress.dev/reference/default-theme-config
       nav: [
        { text: '首页', link: '/' },
        { text: '阅读', link: '/zhouyi_1.md' }
      ],
       logo: '/yi.svg',
       sidebar: [
        {
           text: '目录',
           items: getSidebar()
        }
      ],

       socialLinks: [
        { icon: 'github', link: 'https://github.com/LarryZhu-dev/thebookofchanges' }
      ]
    }
    })


    然后简单给网站设计一个logo


    image.png


    字体是华文隶书,转化为路径后,将它拉瘦一点,再导出为 svg。


    最后,用 pnpm run docs:build打包即可,打包时注意设置基本路径为 github pages 的仓库名。


    发布


    push到github后,在 Setting/Pages 页面发布即可。


    image.png


    效果预览


    最后,网站运行在:larryzhu-dev.github.io/thebookofch…


    image.png


    image.png


    仓库地址:github.com/LarryZhu-de… 来点star🤣


    结语


    现在只有简单的原文,如有 《周易》大佬,欢迎大佬提交注解PR。


    作者:德莱厄斯
    来源:juejin.cn/post/7367659849101312015
    收起阅读 »

    7个Js async/await高级用法

    web
    7个Js async/await高级用法 JavaScript的异步编程已经从回调(Callback)演进到Promise,再到如今广泛使用的async/await语法。后者不仅让异步代码更加简洁,而且更贴近同步代码的逻辑与结构,大大增强了代码的可读性与可维护...
    继续阅读 »

    7个Js async/await高级用法


    JavaScript的异步编程已经从回调(Callback)演进到Promise,再到如今广泛使用的async/await语法。后者不仅让异步代码更加简洁,而且更贴近同步代码的逻辑与结构,大大增强了代码的可读性与可维护性。在掌握了基础用法之后,下面将介绍一些高级用法,以便充分利用async/await实现更复杂的异步流程控制。


    1. async/await与高阶函数


    当需要对数组中的元素执行异步操作时,可结合async/await与数组的高阶函数(如mapfilter等)。


    // 异步过滤函数
    async function asyncFilter(array, predicate) {
    const results = await Promise.all(array.map(predicate));

    return array.filter((_value, index) => results[index]);
    }

    // 示例
    async function isOddNumber(n) {
    await delay(100); // 模拟异步操作
    return n % 2 !== 0;
    }

    async function filterOddNumbers(numbers) {
    return asyncFilter(numbers, isOddNumber);
    }

    filterOddNumbers([1, 2, 3, 4, 5]).then(console.log); // 输出: [1, 3, 5]

    2. 控制并发数


    在处理诸如文件上传等场景时,可能需要限制同时进行的异步操作数量以避免系统资源耗尽。


    async function asyncPool(poolLimit, array, iteratorFn) {
    const result = [];
    const executing = [];

    for (const item of array) {
    const p = Promise.resolve().then(() => iteratorFn(item, array));
    result.push(p);

    if (poolLimit <= array.length) {
    const e = p.then(() => executing.splice(executing.indexOf(e), 1));
    executing.push(e);
    if (executing.length >= poolLimit) {
    await Promise.race(executing);
    }
    }
    }

    return Promise.all(result);
    }

    // 示例
    async function uploadFile(file) {
    // 文件上传逻辑
    }

    async function limitedFileUpload(files) {
    return asyncPool(3, files, uploadFile);
    }

    3. 使用async/await优化递归


    递归函数是编程中的一种常用技术,async/await可以很容易地使递归函数进行异步操作。


    // 异步递归函数
    async function asyncRecursiveSearch(nodes) {
    for (const node of nodes) {
    await asyncProcess(node);
    if (node.children) {
    await asyncRecursiveSearch(node.children);
    }
    }
    }

    // 示例
    async function asyncProcess(node) {
    // 对节点进行异步处理逻辑
    }

    4. 异步初始化类实例


    在JavaScript中,类的构造器(constructor)不能是异步的。但可以通过工厂函数模式来实现类实例的异步初始化。


    class Example {
    constructor(data) {
    this.data = data;
    }

    static async create() {
    const data = await fetchData(); // 异步获取数据
    return new Example(data);
    }
    }

    // 使用方式
    Example.create().then((exampleInstance) => {
    // 使用异步初始化的类实例
    });

    5. 在async函数中使用await链式调用


    使用await可以直观地按顺序执行链式调用中的异步操作。


    class ApiClient {
    constructor() {
    this.value = null;
    }

    async firstMethod() {
    this.value = await fetch('/first-url').then(r => r.json());
    return this;
    }

    async secondMethod() {
    this.value = await fetch('/second-url').then(r => r.json());
    return this;
    }
    }

    // 使用方式
    const client = new ApiClient();
    const result = await client.firstMethod().then(c => c.secondMethod());

    6. 结合async/await和事件循环


    使用async/await可以更好地控制事件循环,像处理DOM事件或定时器等场合。


    // 异步定时器函数
    async function asyncSetTimeout(fn, ms) {
    await new Promise(resolve => setTimeout(resolve, ms));
    fn();
    }

    // 示例
    asyncSetTimeout(() => console.log('Timeout after 2 seconds'), 2000);

    7. 使用async/await简化错误处理


    错误处理是异步编程中的重要部分。通过async/await,可以将错误处理的逻辑更自然地集成到同步代码中。


    async function asyncOperation() {
    try {
    const result = await mightFailOperation();
    return result;
    } catch (error) {
    handleAsyncError(error);
    }
    }

    async function mightFailOperation() {
    // 有可能失败的异步操作
    }

    function handleAsyncError(error) {
    // 错误处理逻辑
    }

    通过以上七个async/await的高级用法,开发者可以在JavaScript中以更加声明式和直观的方式处理复杂的异步逻辑,同时保持代码整洁和可维护性。在实践中不断应用和掌握这些用法,能够有效地提升编程效率和项目的质量。


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

    Google 如果把 Go 团队给裁了会怎么样?

    大家好,我是煎鱼。 节前有一则劲爆消息,Google 把 Python 基础团队和 flutter/dart 团队里相当多的开发人员给解雇了,据说可能是要换个城市重组(真是熟悉的 CY 套路)。 据悉被解雇的人中基本都是负责了 Python 重要维护的相关核心...
    继续阅读 »

    大家好,我是煎鱼。


    节前有一则劲爆消息,Google 把 Python 基础团队和 flutter/dart 团队里相当多的开发人员给解雇了,据说可能是要换个城市重组(真是熟悉的 CY 套路)。


    据悉被解雇的人中基本都是负责了 Python 重要维护的相关核心成员。


    如下图所示:



    此时引发了国内外社区一个较大的担忧,如果 Google 如法炮制,要放弃 Go 核心团队。会发生什么事,会不会有什么问题?



    现在有什么


    先知道可能会失去什么,那得先盘点一下 Go 这一门编程语言和 Go 核心团队在 Google 获得了什么。


    根据我们以往对 @Lance Taylor 所澄清以及各处的描述,可以估算 Go 在 Google 大概获得了什么。


    其至少包含以下内容:



    1. 工作岗位:Go 核心团队相关成员的工作岗位,包含薪资、福利等各种薪酬内容。

    2. 软硬件资源:Go 相关的软硬件资源(例如:知识产权、服务器、域名、模块管理镜像)等网上冲浪所需信息。

    3. 线下活动:Go 世界各地部分大会的开展可能会变少,或缩减规模(资金、背书等)。

    4. 大厂内部资源:因为失去 Google 内部的资源,可能逐步失去一些先进项目的熏陶和引入使用 Go 这一门编程语言的机会。

    5. 推广和反馈渠道:Go 一些显著问题和特性的发现、响应,可能会变慢。因为 Go 对于 Google 内部的问题处理和特性需要,历史上来看都是按最高优先级处理。


    可能会发生什么事


    如果真的一刀切,Google 把 Go 核心团队干没了,基础设施全部都不提供了。


    大家普遍认为,会出现如下几种情况:



    1. 如果 Go 团队中的很多人被裁员,他们会另谋高就。各散东西。维护积极性和组织性会大幅下降。

    2. 如果 Google 决定完全停止对 Go 的投资,Go 的维护可能会变得更加复杂,因为它需要运行大量的基础设施。在这种情况下,可能会出现 Go 由 Google 转移到一个外部的基金会,会有明显的阶段性维护波动。

    3. 如果 Google 选择在内部其他团队对 Go 继续投入,较差的情况是 Google 会灵活运用他们对知识产权的所有权 --Go 很可能会更名为其他东西。


    基金会方面,另外大家认为最有可能接受 Go 的基金会是:CNCF,因为 Go 项目在 CNCF 中基于数量来讲是最大的。


    如下图部分所示:



    同时 CNCF 和 Go 的云原生属性最为强烈,契合度非常高。


    参考 Rust 发展史


    @azuled 根据 Rust 的发展历史,给出了自己的一些见解。如下所表述:


    1、Rust 被踢出 Mozilla 核心,成为一个独立的基金会,但它仍然存活了下来。事实上,它后来可能做得更好。


    2、我认为很有可能围绕 Go 成立一个非营利组织,而且很有可能有足够多的大公司使用它来支持它,至少在一段时间内是这样。


    总结


    在目前这个大行情下,Go 作为 Google Cloud 团队的一员,和云原生的故事捆绑在一起。如果 Google 业绩出现波动,或者要继续降本增效。


    这类没有直接营收的基础部门或团队还是比较危险的,因为其会在企业中根据利润中心、成本中心进行分摊和计算人效成本等。


    如果真的强硬切割,势必会对 Go 这门编程语言产生阶段性的冲击。但未来是好是坏,就不好说了。



    作者:煎鱼eddycjy
    来源:juejin.cn/post/7366070642047008783
    收起阅读 »

    20个你不得不知道的Js async/await用法

    web
    20个你不得不知道的Js async/await用法 JavaScript的async和await关键词是现代JavaScript异步编程的核心。它们让异步代码看起来和同步代码几乎一样,使得异步编程变得更加直观和易于管理。本文介绍20个关于async/awai...
    继续阅读 »

    20个你不得不知道的Js async/await用法


    JavaScript的asyncawait关键词是现代JavaScript异步编程的核心。它们让异步代码看起来和同步代码几乎一样,使得异步编程变得更加直观和易于管理。本文介绍20个关于async/await的实用技巧,将大大提升编程效率和代码的清晰度。


    1. 基础用法


    async函数返回一个Promise,而await关键词可以暂停async函数的执行,等待Promise解决。


    async function fetchData() {
    let data = await fetch('url');
    data = await data.json();
    return data;
    }

    2. 错误处理


    使用try...catch结构处理async/await中的错误。


    async function fetchData() {
    try {
    let response = await fetch('url');
    response = await response.json();
    return response;
    } catch (error) {
    console.error('Fetching data error:', error);
    }
    }

    3. 并行执行


    Promise.all()可以用来并行执行多个await操作。


    async function fetchMultipleUrls(urls) {
    const promises = urls.map(url => fetch(url).then(r => r.json()));
    return await Promise.all(promises);
    }

    4. 条件异步


    根据条件执行await


    async function fetchData(condition) {
    if (condition) {
    return await fetch('url');
    }
    return 'No fetch needed';
    }

    5. 循环中的await


    在循环中使用await时,每次迭代都会等待。


    async function sequentialStart(urls) {
    for (const url of urls) {
    const response = await fetch(url);
    console.log(await response.json());
    }
    }

    6. 异步迭代器


    对于异步迭代器(例如Node.js中的Streams),可以使用for-await-of循环。


    async function processStream(stream) {
    for await (const chunk of stream) {
    console.log(chunk);
    }
    }

    7. await之后立即解构


    直接在await表达式后使用解构。


    async function getUser() {
    const { data: user } = await fetch('user-url').then(r => r.json());
    return user;
    }

    8. 使用默认参数避免无效的await


    如果await可能是不必要的,可以使用默认参数避免等待。


    async function fetchData(url = 'default-url') {
    const response = await fetch(url);
    return response.json();
    }

    9. await在类的方法中


    在类的方法中使用async/await


    class DataFetcher {
    async getData() {
    const data = await fetch('url').then(r => r.json());
    return data;
    }
    }

    10. 立刻执行的async箭头函数


    可以立即执行的async箭头函数。


    (async () => {
    const data = await fetch('url').then(r => r.json());
    console.log(data);
    })();

    11. 使用async/await进行延时


    利用async/await实现延时。


    function delay(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
    }

    async function delayedLog(item) {
    await delay(1000);
    console.log(item);
    }

    12. 使用async/await处理事件监听器


    在事件处理函数中使用async/await


    document.getElementById('button').addEventListener('click', async (event) => {
    event.preventDefault();
    const data = await fetch('url').then(r => r.json());
    console.log(data);
    });

    13. 以顺序方式处理数组


    使用async/await以确定的顺序处理数组。


    async function processArray(array) {
    for (const item of array) {
    await delayedLog(item);
    }
    console.log('Done!');
    }

    14. 组合async/awaitdestructuring以及spread运算符


    结合使用async/await,解构和展开操作符。


    async function getConfig() {
    const { data, ...rest } = await fetch('config-url').then(r => r.json());
    return { config: data, ...rest };
    }

    15. 在对象方法中使用async/await


    async方法作为对象的属性。


    const dataRetriever = {
    async fetchData() {
    return await fetch('url').then(r => r.json());
    }
    };

    16. 异步生成器函数


    使用async生成器函数结合yield


    async function* asyncGenerator(array) {
    for (const item of array) {
    yield await processItem(item);
    }
    }

    17. 使用顶级await


    在模块顶层使用await(需要特定的JavaScript环境支持)。


    // ECMAScript 2020引入顶级await特性, 部署时注意兼容性
    const config = await fetch('config-url').then(r => r.json());

    18. async/await与IIFE结合


    async函数与立即执行函数表达式(IIFE)结合。


    (async function() {
    const data = await fetch('url').then(r => r.json());
    console.log(data);
    })();

    19. 使用async/await优化递归调用


    优化递归函数。


    async function asyncRecursiveFunction(items) {
    if (items.length === 0) return 'done';
    const currentItem = items.shift();
    await delay(1000);
    console.log(currentItem);
    return asyncRecursiveFunction(items);
    }

    20. 在switch语句中使用await


    switch语句的每个case中使用await


    async function fetchDataBasedOnType(type) {
    switch (type) {
    case 'user':
    return await fetch('user-url').then(r => r.json());
    case 'post':
    return await fetch('post-url').then(r => r.json());
    default:
    throw new Error('Unknown type');
    }
    }

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

    Shadcn UI 现代 UI 组件库

    web
    前言 不知道大家是否使用过 Shadcn UI,它在Github 上拥有了 35k star,它与大多数 UI 组件库(如 Ant desgin 和 Chakra UI)不同,一般组件库都是通过 npm 的方式给项目使用,代码都是存在 node_modules...
    继续阅读 »

    image.png


    前言


    不知道大家是否使用过 Shadcn UI,它在Github 上拥有了 35k star,它与大多数 UI 组件库(如 Ant desgin 和 Chakra UI)不同,一般组件库都是通过 npm 的方式给项目使用,代码都是存在 node_modules 中,而 Shadcn UI 可以将单个 UI 组件的源代码下载到项目源代码中(src 目录下),开发者可以自由的修改和使用想要的 UI 组件,它已经被一些知名的网站(vercel.combestofjs.org)等使用。那么它到底有什么优势呢? 一起来来探讨下。


    Shadcn UI 介绍


    Shadcn UI 实际上并不是组件库或 UI 框架。相反,它是可以根据文档“让我们复制并粘贴到应用程序中的可复用组件的集合”。它是由 vercel 的工程师Shadcn创建的,他还创建了一些知名的开源项目,如 TaxonomyNext.js for DrupalReflexjs


    Radix UI - 是一个无头 UI 库。也就是说,它有组件 API,但没有样式。Shadcn UI 建立在 Tailwind CSS 和 Radix UI 之上,目前支持 Next.js、Gatsby、Remix、Astro、Laravel 和 Vite,并且拥有与其他项目快速集成的能力——安装指南


    Shadcn UI 功能特点


    多主题和主题编辑器



    在 Shadcn UI 的官网上有一个主题编辑器,我们可以点击 Customize 按钮实时切换风格和主题颜色,设计完成后,我们只需要拷贝 css 主要变量到我们的程序中即可。 下图是需要拷贝的 css 颜色变量。



    颜色使用 hls 表示,主题变量分为背景色(background) 和 前景色(foreground),Shadcn UI 约定 css 变量省略 background,比如 --card 就是表示的是 card 组件的背景颜色。


    深色模式


    可以看到复制的 css 变量支持生成深色模式,如果你使用 react, 可以使用 next-themes,这个包来实现主题切换,当然也可以通过 js 在 html 上切换 dark 这个样式来实现。 除了 react 版,社区还自发实现了 vuesvelte 版本


    CLI


    除了手动从文档中复制组件代码到项目中,还可以使用 cli 来自动生成代码



    • 初始化配置


    npx shadcn-ui@latest init



    • 添加组件


    npx shadcn-ui@latest add


    按空格选择想要的组件,按回车就会下载选中的 UI 组件代码



    下载的源码在 components/ui 目录下,并且自动安装 Radix UI 对应的组件。


    丰富的组件库


    Shadcn UI 拥有丰富的组件,包括 常见的 Form、 Table、 Tab 等 40+ 组件。





    使用 Shadcn UI 创建登录表单


    接下来我们一起实战下,使用 Shadcn UI 创建登录表单, 由于 Shadcn UI 是一个纯 UI 组件,对于复杂的表单,我们还需要使用 react-hook-form 和 zod。


    首先下载 UI


    npx shadcn-ui@latest add form

    安装 react-hook-form 以及 zod 验证相关的包


    yarn add add react-hook-form zod @hookform/resolvers

    zod 用于格式验证


    下面代码是最基本的 Form 结构


    import {
    Form,
    FormControl,
    FormDescription,
    FormField,
    FormItem,
    FormLabel,
    FormMessage,
    } from "@/components/ui/form"
    import { Input } from "@/components/ui/input"

    <FormField
    control={form.control}
    name="username"
    render={({ field }) => (
    <FormItem>
    <FormLabel>Username</FormLabel>
    <FormControl>
    <Input placeholder="shadcn" {...field} />
    </FormControl>
    <FormDescription>This is your public display name.</FormDescription>
    <FormMessage />
    </FormItem>

    )}
    />


    • FormField 用于生成受控的表单字段

    • FormMessage 显示表单错误信息


    登录表单代码


    "use client"

    import { zodResolver } from "@hookform/resolvers/zod"
    import { useForm } from "react-hook-form"
    import * as z from "zod"

    import { Button } from "@/components/ui/button"
    import {
    Form,
    FormControl,
    FormField,
    FormItem,
    FormLabel,
    FormMessage,
    } from "@/components/ui/form"
    import { Input } from "@/components/ui/input"

    const formSchema = z.object({
    email: z.string().email({message:'邮箱格式不正确'}),
    password: z.string({required_error:'不能为空'}).min(6, {
    message: "密码必须大于6位",
    }),
    })

    export default function ProfileForm() {
    // 1. Define your form.
    const form = useForm<z.infer<typeof formSchema>>({
    resolver: zodResolver(formSchema),
    defaultValues: {
    email: "",
    },
    })

    // 2. Define a submit handler.
    function onSubmit(values: z.infer<typeof formSchema>) {
    // Do something with the form values.
    // ✅ This will be type-safe and validated.
    console.log(values)
    }

    return (
    <Form {...form}>
    <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4 w-80 mx-auto mt-10">
    <FormField
    control={form.control}
    name="email"
    render={({ field }) =>
    (
    <FormItem>
    <FormLabel>邮箱</FormLabel>
    <FormControl>
    <Input placeholder="请输入邮箱" {...field} />
    </FormControl>
    <FormMessage />
    </FormItem>
    )}
    />
    <FormField
    control={form.control}
    name="password"
    render={({ field }) =>
    (
    <FormItem>
    <FormLabel>密码</FormLabel>
    <FormControl>
    <Input placeholder="请输入密码" {...field} />
    </FormControl>
    <FormMessage />
    </FormItem>
    )}
    />
    <Button type="submit">登录</Button>
    </form>
    </Form>

    )
    }


    展示效果



    小结


    与其他组件库相比,Shadcn UI 提供了几个好处。



    • 易用性:使用复制和粘贴或 CLI 安装方法可以轻松访问其组件.

    • 可访问性:Shadcn UI 的组件是完全可访问的,并符合 Web 内容可访问性指南 (WCAG) 标准,它支持屏幕阅读器、键盘导航和其他辅助设备。

    • 灵活和可扩展性:Shadcn UI 只会下载需要使用的组件在源码中,并且开发者可以灵活定制和修改。


    当然需要手动拷贝安装每一个组件可能是一件麻烦的事情,这也会导致源码量的增加,因此是否使用 Shadcn UI 还得开发者自行决定,总的来说 Shadcn UI,我还是非常看好,我将配合 next.js 在一些新项目中使用。


    作者:狂奔滴小马
    来源:juejin.cn/post/7301573649328668687
    收起阅读 »

    产品经理:为什么你做的地图比以前丝滑了许多?

    web
    从业多年第一次接触地图相关的需求,开发过程中产生了一些思考,遂记录下来,欢迎讨论Vue3 + 高德地图 JS API 2.0 + 高德地图 AMapUI组件库近两年前端大家是真的不好混,在职的人呢被极限压榨,待业的人呢投简历都是【未读不回】。照常理来说,地图相...
    继续阅读 »

    从业多年第一次接触地图相关的需求,开发过程中产生了一些思考,遂记录下来,欢迎讨论

    Vue3 + 高德地图 JS API 2.0 + 高德地图 AMapUI组件库

    近两年前端大家是真的不好混,在职的人呢被极限压榨,待业的人呢投简历都是【未读不回】。

    1.gif

    照常理来说,地图相关的需求都是由组内的地图大佬负责的,但眼瞅着公司里前端同学越来越少,这“泼天的富贵”终于有一天也落到了我头上。

    需求的内容倒是很简单:要在地图上绘制一些轨迹点和一条轨迹线,以及一个目标点KeyPoint,让使用者来审查轨迹线是否经过KeyPoint,以及系统中记录KeyPoint的信息是否正确。当轨迹未经过 或 KeyPoint信息不正确时,会再提供一些辅助点SubPoint供用户选择,替换掉KeyPoint。(轨迹点也属于一种SubPoint

    本着能CV就不手写的原则,我打开了项目代码(Vue2)寻找之前类似的地图需求,看看能不能套用一下然后快速下班,结果我看到了若干个大几千行的文件,以及这样的渲染效果(轨迹点上的箭头表示当前移动的方向):

    1.png

    2.png

    大哥喂,咱就是说,方向盘打不正的话要抓紧去修,上路是要出事故的 3.jpg

    得,言归正传,且不说那加起来几万行的代码我能不能捋顺喽,就是这个效果,干脆我还是用Vue3重新实现一下吧。

    别忘了,前端的老本行是什么


    业务的关注点

    开始之前,我们先思考一个问题:业务的关注点是什么?

    想明白了这个问题,我们在设计地图样式以及一些交互细节时,才能有更好的针对性。

    (让我看看有多少人是默认样式+内置组件一把梭的)

    image.png

    ok那既然涉及到了地图,归根结底我们的关注点无非是这三方面:

    • 线
    • 区域

    如果按照关注点的归属粗略的分为两类:外部添加的地图自身的

    当业务更关注外部添加的元素时(如maker、轨迹),随着地图缩放、地形改变、POI显隐,我们添加的元素是否始终有一个比较醒目的显示效果?

    当业务更关注地图自身的元素时(如兴趣点),对于POI的 pick 动作,是否贴合业务流程?是否足够智能与便捷?(可参考高德自己的效果)

    这里针对第一类推荐两个初始化地图的可选配置项:

    1. features:地图显示要素(查看效果
    2. mapStyle:地图主题(查看效果

    相信很多人可能都没关注过这两个配置项,而这两个东西组合起来使用,不仅能使你添加的外部元素始终处于一个高醒目的level,也可以与你项目本身的风格主题更搭,如何抉择,诸君自行思量。

    (浅浅吐槽一下,高德提供的功能和配置项非常丰富,但文档真的是一言难尽。。。一样的功能在不同的地方都有文档,有些内容还不一致)

    4.png

    选择画点的方法

    当你大概明白自己要做什么样的地图之后,让我们稍微进入一点正题:怎么选择合理的画点方法?

    高德提供了哪些画点的方法呢?

    • JS API
      1. 默认点标记Marker
      2. 圆形标记CircleMarker
      3. 灵活点标记ElasticMarker
      4. 海量标注LabelMarker:需要维护图层、维护避让等级、自定义样式实现起来比较麻烦
      5. 海量点标记MassMarker:无法显示文字label
      6. 点聚合
        • 按距离聚合MarkerCluster:需要维护权重
        • 按索引聚合IndexCluster:需要维护索引规则
    • JS API UI组件库
      1. 简单标注SimpleMarker
      2. 字体图标标注AwesomeMarker
      3. 矢量标注SvgMarker
      4. 海量点PointSimplifier:可使用Canvas

    至于像文本标记、折线、多边形等等一些通过某些黑科技实现类似点标记的方法(GPT说的),和Native端的画点方法,不在本文的讨论范围中。


    美国五星上将麦克阿瑟曾说过,一切抛开实际背景去讨论问题的行为都是耍流氓。

    画点的方法找到了很多,那我们要画什么样的点呢?

    1. 从数量上看

    动辄上万

    为什么我要先看数量呢,因为可自定义样式的画点方法很多,但是要支持大数量级渲染且性能良好,就把上边一多半方法给pass掉了。

    还剩下这些可供选择:海量标注LabelMarker海量点标记MassMarker按距离聚合MarkerCluster按索引聚合IndexCluster海量点PointSimplifier

    2. 从样式上看

    需要自定义。从上边的截图中可以看出,点的形状为圆形,黑边黄底,中心有个箭头,且整体随着当前运动方向有一个rotate deg

    上述画点方法至少都支持(图片 或 HTML String 或 CSS Style)中的一种方式,而这三种方式理论上也都能实现我们想要的效果,所以下一个。

    3. 特性

    test.gif

    虽然我们需要关注轨迹点,但并不是所有状态下都需要。比如在地图的缩放等级很小时(看到的是省、国家级别),并不需要把每一个轨迹点都展示出来。所以可以看到,之前的实现效果中,放大缩小都会重新适应尺寸,并且临近的点有自动合并的效果。

    海量标注LabelMarker海量点标记MassMarker退出了游戏,他俩是全量绘制并且没有外部接入的话点是始终展示的。

    至此,只有按距离聚合MarkerCluster按索引聚合IndexCluster海量点PointSimplifier三者进入了决赛圈。

    现在来综合对比一下这三种方法:

    方法1w+点渲染性能自定义样式合并逻辑
    按距离聚合MarkerCluster渲染迅速,操作不卡顿HTML String或图片距离+权重就近合并
    按索引聚合IndexCluster渲染迅速,操作不卡顿HTML String或图片距离+索引分组合并
    海量点PointSimplifier渲染迅速,操作不卡顿Canvas或图片TopN

    渲染方面在1w+点的竞赛中大家表现得都不错,官网示例中心可以看到,这里不再赘述。

    自定义样式则有三种途径,图片、HTML字符串和新出现的Canvas。图片和Canvas比较简单,我们先讲一讲这个HTML字符串,也就是原生的HTML。

    假如你用了某个现代化的前端框架在开发你的系统,用到了高德地图,并且想画一些漂漂亮亮的点在你需要标注的地方。在翻阅了文档之后,发现似乎直接传入HTML字符串这种方法是最快的,于是你开开心心的输入了一个

    My Marker
    试试水,接着,保存、等待hot reload,并把期待的目光投向了屏幕...

    353ad3a27c3ac95c86c30e66f1b4f15.png

    好家伙,这小Marker不仔细看,还真有点找不到呢...你决定继续添加一些样式

    2M2we.gif

    之后代码可能逐渐变成了这样...

    微信图片_20240514153858.png

    你:

    WzgGg.png

    把手指从ctrl C V三个键拿下来之后,你陷入了沉思:我能不能用XXX UI组件库来自定义Marker?

    答案当然是肯定的~下面请允许我用Vue@3.3.4 + ant-design-vue@3.2.20来做个示范~~

    首先,他接收的是HTML字符串,所以直接传进去一个vue组件肯定是不行的

    import MyComponent from './MyComponent.vue'

    // 以普通点标记举例
    new Marker({
    content: MyComponent,
    // ...other configs
    })

    // not work

    所以我们要做的就是把MyComponent给转成原生HTML,最简单的办法当然就是Vue实例的mount()API啦:

    MyComponent作为根组件创建一个新的Vue实例

    import {createApp} from 'vue'
    import MyComponent from './MyComponent.vue'

    const app = craeteApp(MyComponent)

    将实例挂载到一个div上,得到原生的HTML

    const div = document.createElement("div")
    app.mount(div)

    console.log('div: ', div)

    打印一下:

    223.png

    使用:

    // 以普通点标记举例
    const marker = new Marker({
    content: div,
    // ...other configs
    });

    效果图就不放啦,有几个注意的点要提一下:

    1. 最重要的放在最前边:如果你的点在整个页面的生命周期内仅会绘制一次,那你可以跳过这一条。否则一定要记得app.unmount()。一种比较好的实践是,把点数据画点的方法移除点的方法写进一个hook里。
    // example
    import { createGlobalState } from "@vueuse/core";
    import { ref, createApp } from "vue";
    import Map from './Map.js' // 地图实例
    import MyComponent from "./MyComponent.vue";

    export const useCustomPoints = createGlobalState(() => {
    const pointData = ref([]);
    const removePointsCb = [];

    const setPoint = () => {
    const data = pointData.value.map(point => {
    const div = document.createElement("div");
    const app = createApp(MyComponent);
    app.mount(div);

    removePointsCb.push(() => app.unmount()) // 清除图标实例的回调

    // 以普通点标记举例
    const marker = new Map.Constructors.Marker({
    map: Map,
    content: div,
    // ...other config
    });

    return marker
    })

    Map.add(data); // 将点添加到地图上

    removePointsCb.push(() => Map.remove(data)); // 移除点的回调
    }

    const removePoints = () => {
    while (removePointsCb.length) {
    removePointsCb.pop()();
    }
    };

    return {
    pointData,
    setPoint,
    removePoint
    }
    })
    1. 可以通过createApp的第二个参数传递props进去,这些props是响应式的
    2. 新创建的Vue实例与你项目自身的实例不共享全局的配置,比如路由组件Store等,需要单独配置
    3. Vue2以及其他的一些框架,实现思路类似

    好了,言归正传。

    看起来似乎三种方法都可以实现需求,但是仔细翻看点聚合方法的文档,发现使用图片自定义点时没有提供旋转的配置,也就是说我们可能需要准备n张图片(取决于你想实现角度渲染的精确度),不,这太不优雅了。而如果使用原生HTML去自定义,要么接受丑炸的效果(纯手工css),要么面临着卡顿的风险(大量的app实例)

    没办法,只好被(xin)迫(ran)接受用海量点PointSimplifier的Canvas去做了~毕竟能用Canvas画就约等于能画一切嘛~~

    抱着视死如归的心情去翻了一下JS API UI组件库的海量点PointSimplifiercanvas绘制function文档,发现了一句了不得的话:

    微信图片_20240514173923.png

    划重点:通常只是描绘路径尽量不要fill或者stroke引擎自己一次性

    翻译:该函数通常只是描绘路径,但是也能描绘形状。尽量不要fill或者stroke,除非你能搞明白我们的描绘机制。所有点的路径描绘完成后,引擎自己会在尾部调用fill以及stroke,一次性绘出所有路径,所以你要注意尾部的这次操作,避免冲突。

    三个字总结海量点PointSimplifier的描绘机制就是:连笔画

    不是每个点都创建一个新的Canvas画布,绘制完成后立即渲染;而是所有的点都共用一个Canvas画布,以当次你能绘制的区域坐标作为参数,重复绘制n(点的数量)次,最后一把全渲染出来。

    微信图片_20240515171523.png

    微信图片_20240516093003.png

    明白了这个,我们在书写function的逻辑的时候,只要注意保证每次绘制开始、结束时笔触的落点和绘制上下文状态即可。绘制一个有旋转角度的、中间有箭头的圆形(圆形的背景色是通过海量点PointSimplifier的lineStyle配置的),示例代码如下:

    由于叠加了变换,处理状态时偷懒使用了save()、restore()

    renderOptions: {
    // 这里使用了样式分组引擎:https://lbs.amap.com/demo/amap-ui/demos/amap-ui-pointsimplifier/group-style-render
    // 以点的旋转角作为组id入参,方便操作
    // 无需分组时,renderOptions.pointStyle.content = renderOptions.groupStyleOptions.pointStyle.content 逻辑一致
    groupStyleOptions: function (gid) {
    return {
    pointStyle: {
    content: function (ctx, x, y, width, height) {
    // 存了一个坐标,画箭头的时候用
    const startX = x + width / 2;
    const startY = y + height / 4;

    // 移动到画布的最右侧、中间位置
    ctx.moveTo(x + width, y + height / 2);

    // 画圆
    ctx.arc(
    x + width / 2,
    y + height / 2,
    width / 2,
    0,
    Math.PI * 2,
    true
    );

    // 变换前保存一下状态
    ctx.save();

    // 以圆心为旋转的中心点
    ctx.translate(x + width / 2, y + height / 2);
    // 按照轨迹方向旋转
    ctx.rotate((Math.PI / 180) * gid);
    // 重置中心点
    ctx.translate(-(x + width / 2), -(y + height / 2));

    // 画箭头
    ctx.moveTo(startX, startY);
    ctx.lineTo(x + width / 4, y + height / 2);
    ctx.moveTo(startX, startY);
    ctx.lineTo(startX, y + (height * 3) / 4);
    ctx.moveTo(startX, startY);
    ctx.lineTo(x + (width * 3) / 4, y + height / 2);

    // 由于箭头需要在旋转的状态下绘制,所以在箭头绘制完成后再恢复状态
    ctx.restore();
    },
    },
    };
    },
    }

    来一个无旋转时的笔触顺序动图,我尽力了6j4l.png

    test.gif

    画完之后,看一下对比效果:




    OK,点画出来了。

    上边特性中有提到:当我们距离很远时,就不需要再关注某个具体的轨迹点。所以可以再进一步优化,当地图的缩放等级zoom小于某个阈值时,清空point:

    import {computed, watch, ref} from 'vue'
    import {Map, PointSimplifierIns} from 'Map.js' // 地图实例、海量点实例

    const zoom = ref(null);

    const showPoint = computed(() => zoom.value > 10);

    const pointData = ref([ /* ...赋值逻辑省略 */]);

    Map.on("zoomchange",
    debounce(() => {
    zoom.value = Map.getZoom();
    }, 200)
    );

    watch(showPoint, (show) => {
    PointSimplifierIns.setData(show ? pointData.value : []);
    })

    效果如下:

    test.gif

    控制显示隐藏没有用自带的show()hide()方法,而是选择直接重设数据源,是因为:海量点PointSimplifiershow状态下时对地图进行缩放,会自动重绘适应尺寸;hide状态下则不会。从show变为hide时,会保存当前zoom下点的尺寸,供下次hideshow时用。如果地图缩放的太快,当前的zoom与上次保存尺寸时的zoom跨度太大,可能会导致点位不匹配现象。


    选择画轨迹的方法

    画线的选择过程就简单了很多,之前需求中是用折线Polyline实现的,画出来的效果总感觉差点意思,所以就去翻了翻高德的文档,共找到常规画线方法3种:

    • JS API
      1. 折线Polyline
      2. 贝塞尔曲线BesizerCurve
    • JS API UI组件库
      1. 轨迹展示PathSimplifier

    基本上毫无疑问了嘛~我们本身就是要画轨迹,还有什么好选的~~ 必须用轨迹展示

    不过这里还是分享一些对三种方法实际体验之后的感受:

    1. 折线Polyline:无法识别线上的点。如果轨迹数据没有经过噪点清除,画出来之后在细节处会有比较严重的锯齿。不过整体上感觉,倒也不是不能用~
    2. 贝塞尔曲线BesizerCurve:无法识别线上的点。但理论上是唯一可以绘制出完全符合真实运动轨迹的、贴合地图路线的方法了,代价也是相当的大——至少要在原本轨迹点的基础上额外维护n-1个控制点,放弃~~
    3. 轨迹展示PathSimplifier:性能好,相同数据量下的显示效果要比折线画出来的平滑许多。以及来自官网的优点罗列:
      • 使用Simplify.js进行点的简化处理,提高性能
      • 支持识别线上的点做信息展示
      • 内置巡航器
      • 样式配置更加丰富

    实现过程比较简单,照着文档撸就行,可以对比下折线和轨迹展示两种方式,在拐角细节处的差异:

    折线


    轨迹展示


    小tips: 适当增加线宽lineWidth可以有效的缓解锯齿现象


    Loading的区域与时机

    当我第一次打开上文提到的老版本地图页面时,除了渲染效果不够理想外,最大的一个感受就是:Loading太长

    不是想像中那样常规的:打开页面,给一个满屏Spin等待加载各种数据、等待绘制点、线的动作,所有准备工作完成后,取消Spin允许用户开始操作。

    咱就是说,像这样的交互逻辑,其实也没啥问题。毕竟谁还没个业务繁忙的时候,最简单最原始最暴力的满屏Spin虽然在体验上不尽如人意,但我觉得是符合上线标准的。

    但您猜我看到了什么?

    微信图片_20240520171349.png

    Form、Map、Action Bar三个区域各自一个小Spin,整体有个大Spin,可以透过大Spin的透明遮罩层看到下面的小spin们反复交替进行,以及大Spin自己也时不时的闪现一下...

    7D91.gif

    Spin为什么会闪现?回到需求当中来:

    地图上点、线的绘制依赖了多个数据源

    • 轨迹点、轨迹线数据源
    • KeyPoint数据源
    • SubPoint数据源
      • Type 1
      • Type 2

    这些接口一部分是并发请求,但也有个别的接口请求参数依赖于其他接口的返回值

    以及,使用高德地图提供的API绘制点、线时,也共用了接口请求时的Spin。

    还有诸多类似这样的代码:

    setTimeout(() => {
    loading = false
    }, 2000)

    对渲染流程管理混乱、对数据流向不了解、对自己代码不自信,故意延迟loading的结束时机,防止用户过早操作导致报错


    const interval = setInterval(() => {
    if(conditon) {
    clearInterval(interval);
    loading = false
    }
    }, 1000)

    依赖第三方的内容加载,或将多个小loading合并为一个大loading


    最终的结果就是让人一整个loading住...

    pj7dW.png

    而我做了哪些改变

    首先,将单个loading覆盖的区域尽可能的缩小

    举个例子,上边提到的Action Bar,假设里边既有展示KeyPoint信息的列表,又有展示所有SubPoint信息的列表。在之前的处理方案中,Action Bar区域只有一个整体的Spin,所以整个区域loading的流程大概是:

    %%{init: { 'theme': 'base', 'themeVariables': {
    'cScale0': '#996666', 'cScaleLabel0': '#ffffff',
    'cScale1': '#996633','cScaleLabel1': '#ffffff',
    'cScale2': '#999999', 'cScaleLabel2': '#ffffff'
    }}}%%

    timeline
    title Loading 状态

    section 阶段一
    show : request for KeyPoint data
    hide : request success

    section 阶段二
    show : request for SubPoint type1 data
    hide : request success

    section 阶段三
    show : request for SubPoint type2 data
    hide : request success

    而我则是把每个数据源对应的列表都单独分配了一个loading组件

    聪明的看官老爷可能会问,同时存在多个Spin,不也很奇怪吗?

    test.gif

    所以我选择了骨架屏Skeleton作为loading组件:

    test.gif

    受gif图的帧率影响,实际效果还是很丝滑的。(但使用骨架屏时也有一个注意的点:骨架屏的占位高度需要配置段落占位图行数来调整,避免loading结束时真实的渲染内容与骨架屏高度相差太大产生视差)

    然后,在Map区域用其他形式的提示代替传统的loading

    与上边类似,Map区域不仅同时用到了KeyPointSubPoint数据源,而且在绘制点、线时也有loading。并且也是一个整体的Spin,你应该能想象出每次数据初始化时,Map上闪来闪去的Spin。。。

    地图本身,是高德提供出来可以开箱即用的组件,我们所添加的点、线只是附加属性,并不应该使用整体的Spin遮罩阻止用户使用地图的其他功能。在某些附加属性成功添加之前,用户只需要知道与之相关的功能是不可用状态即可。

    我的方案是:图例化提供一个loading-box,里边展示了每个数据源的加载状态

    test.gif

    为了不遮挡地图,loading-box不是始终展示的,基础显示逻辑是:

    1. watch监听loadings 数组
    2. 只要有一个数据源loading中,则显示。
    3. 全部数据源都不在loading中,则debounce n秒后隐藏。

    显示动作是实时的,只要有一个数据源在loading中,就应该立刻让用户感知到。

    而隐藏动作如果也是实时的,loading-box的显隐切换会比较频繁,显得很突兀。

    • 如果使用setTimeout做延时,期望是发出hide指令n秒后执行,但无法保证n秒后没有新的loading正在进行,导致显隐切换逻辑紊乱。
    • 如果使用throttle做延时,导致的问题与setTimeout相同,只是发生概率会小一些。
    • 相比之下debounce最适合做这个场景的解决方案。

    再结合上边提到的hook写法,把loading状态也放进去,方便loading-box使用:

    // example
    import { createGlobalState } from "@vueuse/core";
    import { ref, createApp } from "vue";

    export const useCustomPoints = createGlobalState(() => {
    const pointData = ref([]);
    const pointLoading = ref(false);
    const removePointsCb = [];

    const getPoint = async () => {
    pointLoading.value = true;
    const data = await requestPointData();
    pointLoading.value = false;

    pointData.value = data;
    }

    const setPoint = () => {
    // ...
    }

    const removePoint = () => {
    // ...
    };

    return {
    pointData,
    pointLoading,
    getPoint,
    setPoint,
    removePoint
    }
    })
    // loading-box.vue

    <script setup>
    import { watch } from 'vue';
    import { useCustomPoints } from 'useCustomPoints.js';

    const { pointLoading } = useCustomPoints();

    watch([pointLoading, /* and other loadings */], () => {
    // do loading-box show/hide logic
    })
    script>

    应用了上述loading相关的优化后,虽然跟核心业务逻辑相关的代码改动几乎为0,但用户的体验却有相当大的提升,究其原因:

    在老版本的实现中,因为全屏Spin的存在,任何一项页面准备工作完成前,页面都无可交互区域;拆分loading后,把一大部分无可交互区域的时间变成了局部可交互区域的时间,甚至在Map模块替换了loading的形式,完全避免了Spin遮罩层这种阻隔用户的效果。加上Spin动画本身的耗时、显示/隐藏Spin的耗时,积少成多,产生质变。

    image.png

    可以看到,在局部loading耗时完全一样的情况下,老版本中:

    无可交互区域时间 = 全屏Spin时间 = 局部loading的最大时间

    而在新版本中:

    无可交互区域时间 = 几个all loading片段的时间之和

    而这,也是一些复杂应用做体验优化的思路之一。


    结语

    okok,先写到这,毕竟马上就要下班了

    1b006c53e15945a09253df289b6192cc~tplv-k3u1fbpfcp-jj-mark_3024_0_0_0_q75.awebp

    没什么高大上理论也没什么八股文,只是一个从业多年一事无成小前端在重构需求时的一些感想~~

    还是那句话,欢迎真诚交流,但如果你来抬杠?阿,对对对~ 你说的都对~


    彩蛋

    文章标题来自需求上线后,产品经理的真实评价

    image.png


    作者:Elecat
    来源:juejin.cn/post/7371633297153687606
    收起阅读 »

    2024 前端趋势:全栈也许已经是必选项

    web
    过年期间,回想起来,似乎感觉到这次有一点不一样:也许,全栈的时代已经到来了。 React 与 Vue 生态对比 首先,我们来看看 React 与 Vue 生态的星趋势对比: 上图中,React 整个生态的星星数远超于 Vue,第十名都要比 Vue 第一名的多...
    继续阅读 »

    过年期间,回想起来,似乎感觉到这次有一点不一样:也许,全栈的时代已经到来了。


    React 与 Vue 生态对比


    首先,我们来看看 React 与 Vue 生态的星趋势对比:


    截屏2024-02-29 10.05.39转存失败,建议直接上传图片文件


    上图中,React 整个生态的星星数远超于 Vue,第十名都要比 Vue 第一名的多。我们将其做一个分类:


    排名ReactVue
    1UI全栈
    2白板演示文稿
    3全栈后台管理系统
    4状态管理hook
    5后台管理系统UI
    6文档文档
    7全栈框架集成UI
    8全栈框架UI框架
    9后台管理系统UI
    10无服务栈状态管理

    可以看到 React 这边的生态链基本成熟,几乎每一个分类都有一个上榜的库,不再像 Vue 那样还在卷 UI 框架。


    在全栈方面,Vue 的首位就是全栈 Nuxt。


    React 的 Next.js 虽然不在首位,但是服务端/全栈相关的内容就占了 4 个,其中包含第 10 名的无服务栈。另外值得注意的是,React 这边还有服务端组件的概念。Shadcn/ui 能占到第一位,因为它基于无头 UI Radix 实现的,在服务端组件也能运用。所以,服务端/全栈在 React 中占的比重相当大的。


    这样看来,前端往服务端进发已经成为一个必然趋势。


    htmx 框架的倒退


    再看看框架这边,htmx 在星趋势里,排行第二位,2023增长的星星数为 15.6K,与第一位的 React 颇为相近。


    而 htmx 也是今年讨论度最高的。


    在我经历过前后端不分离的阶段中,使用 jsp 生成前端页面,js 更多是页面炫技的工具。然后在 jQuery + Ajax 得到广泛应用之后,才真正有前后端分离的项目。


    htmx 的出现,不了解的人,可能觉得是倒退到 Java + jQuery + Ajax 的前后端分离状态。但是,写过例子之后,我发现,它其实是倒退到了前后端不分离的阶段。


    用 java 也好,世界上最好的 php 也好,或者用现在的 nodejs 服务,都能接入 htmx。你只要在服务端返回 html 即可。


    /** nodejs fastity 写的一个例子 **/
    import fastify from 'fastify'
    import fastifyHtml from 'fastify-html'
    import formbody from '@fastify/formbody';

    const app = fastify()
    await app.register(fastifyHtml)
    await app.register(formbody);
    // 省略首页引入 htmx

    // 首页的模板,提供一个按钮,点击后请求 html,然后将请求返回的内容渲染到 parent-div 中
    app.get('/', async (req, reply) => {
    const name = req.query.name || 'World'
    return reply.html`

    Hello ${name}


    `
    , reply
    })

    // 请求返回 html
    app.post('/clicked', (req, reply) => {
    reply.html`

    Clicked!

    `
    ;
    })

    await app.listen({ port: 3000 })

    也许大家会觉得离谱,但是很显然,事情已经开始发生了变化,后端也来抢前端饭碗了。


    截屏2024-02-29 10.32.24.png


    htmx 在 github 上已经有不少跟随者,能搜出前端代码已有不少,前三就有基于 Python 语言的 Django 服务端框架。


    jQuery 见势头不错,今年也更新了 4.0 的 beta 版本,对现代浏览器提供了更好的支持。这一切似乎为旧架构重回大众视野做好了准备。


    企业角度


    站在企业角度来看,一个人把前后端都干了不是更好吗?


    的确如此。前后端一把撸更符合企业的利益。国外的小公司更以全栈作为首选项。


    也许有人觉得国情不同,但是在我接触的前端群里,这两年都有人在群里说他们公司前后端分离的情况。


    还有的人还喜欢大厂那一套,注意分工合作,但是其实大厂里遗留项目也不少,有的甚至是 php;还有新的实验项目,如果能投入最少人力,快速试错,这种全栈的框架自然也是最优选择。


    我并不是说,前后端分离不值得。但是目前已经进入 AI 赛道,企业对后台系统的开发,并不愿意投入更多了。能用就行已经成为当前企业的目标,自然我们也应该跟着变化。


    全栈破局


    再说说前端已死的论调。我恰恰觉得这是最好做改变的时机。


    在浏览器对新技术支持稳定,UI 框架趋同,UI 组件库稳定之后,前端不再需要为浏览器不兼容素手无策了,不再需要苦哈哈地为1个像素争辩不停了,也不再需要为产品莫名其妙的交互焦头烂额了。


    这并不意味着前端已死,反而可能我们某个阶段的任务完成了,后面有更重要的任务交给我们。也许,全栈就是一个破局。


    在云服务/云原生如此普遍的情况下,语言不再是企业开发考虑的主要因素,这也为 nodejs 全栈铺平了道路。


    前端一直拣最苦最脏的话来做,从 UI 中拿到了切图的工作,然后接手了浏览器兼容的活,后来又从后端拿到了渲染页面的工作。


    那我们为何不再进一步,主动把 API 开发的工作也拿过来?


    作者:陈佬昔没带相机
    来源:juejin.cn/post/7340603873604599843
    收起阅读 »

    8个小而美的前端库

    web
    前端有很多小而美的库,接入成本很低又能满足日常开发需求,同时无论是 npm 方式引入还是直接复制到本地使用都可以。 2024年推荐以下小而美的库。 radash 实用的工具库,相比与 lodash,更加面向现代,提供更多新功能(tryit,retry 等函数)...
    继续阅读 »

    前端有很多小而美的库,接入成本很低又能满足日常开发需求,同时无论是 npm 方式引入还是直接复制到本地使用都可以。


    2024年推荐以下小而美的库。


    radash


    实用的工具库,相比与 lodash,更加面向现代,提供更多新功能(tryit,retry 等函数),源码可读性高,如果不想安装它,大部分函数可以直接复制到本地使用。



    use-debounce


    React Hook Debouce 库,让你不再为使用防抖烦恼。库的特点:体积小 < 1 Kb、与 underscore / lodash impl 兼容 - 一次学习,随处使用、服务器渲染友好。



    timeago.js


    格式化日期时间库,比如:“3 hours ago”,支持多语言,仅 2Kb 大小。同时提供了 React 版本 timeago-react。


    timeage.format(1544666010224, 'zh_CN') // 输出 “5 年前”
    timeage.format(Date.now() - 1000, 'zh_CN') // 输出 “刚刚”
    timeage.format(Date.now() - 1000 * 60 * 5, 'zh_CN') // 输出 “5 分钟前”

    react-use


    实用 Hook 大合集 - 内容丰富,从跟踪电池状态和地理位置,到设置收藏夹、防抖和播放视频,无所不包。



    dayjs


    Day.js 是一个简约的 JavaScript 库,仅 2 Kb 大小。它可以使用基本兼容 Moment.js,为你提供日期的解析、处理和显示,支持多语言能力。



    filesize


    filesize.js 提供了一种简单方法,便于从数字(浮点数或整数)或字符串转换成可读性高的文件大小。


    import {filesize} from "filesize";
    filesize(265318, {standard: "jedec"}); // "259.1 KB"
    driver.js:driver.js 是一款用原生 js 实现的页面引导库,上手非常简单,体积在 gzip 压缩下仅仅 5kb。

    driver.js


    driver.js 是一款用原生 js 实现的页面引导库,上手非常简单,体积在 gzip 压缩下仅仅 5kb。



    @formkit/drag-and-drop


    FormKit DnD 是一个小型库,它简单、灵活、与框架无关,压缩后只有 4Kb 左右,设计理念为数据优先。



    小结


    前端小而美的库使用起来一般都比较顺手,欢迎在评论区推荐你们开发中的使用小而美的库。


    作者:晓得迷路了
    来源:juejin.cn/post/7350140676615798824
    收起阅读 »

    我为展开收起功能做了动画,被老板称赞!

    web
    需求简介 这几天接了个新项目,需要实现下图中左侧边栏的菜单切换。这种功能其实就是一个折叠面板,实现方式多种多样。 实现上面的功能,无非就是一个v-show的事儿,但没有过渡,会显得非常生硬。想添加一些过渡效果, 最简单的就是使用element ui、或者a...
    继续阅读 »

    需求简介


    这几天接了个新项目,需要实现下图中左侧边栏的菜单切换。这种功能其实就是一个折叠面板,实现方式多种多样。



    实现上面的功能,无非就是一个v-show的事儿,但没有过渡,会显得非常生硬。想添加一些过渡效果,



    最简单的就是使用element ui、或者ant的折叠面板组件了。但可惜的是,我们的项目不能使用任何第三方组件库。



    为了做好产品,我还是略施拳脚,实现了一个简单且丝滑的过渡效果:



    老板看后,觉得我的细节处理的很好,给我一顿画饼,承诺只要我好好坚持,一定可以等到升职加薪!当然,我胃口小,老板的饼消化不了。我还是分享一下自己在不借助第三方组件的情况下,如何快速的实现这样一个效果。


    技术实现方案


    业务分析


    仔细观察需求,我们可以分析出其实动画主要是两个部分:一级标题的箭头旋转二级标题区域的折叠展开



    我们先实现一下基本的html结构:


    <template>
    <div class="nav-bar-content">

    <div class="header-wrap" @click="open = !open">
    <span class="text">自动化需求计算条件输如</span>
    <span class="arrow">
    >
    </span>
    </div>

    <div v-show="open" class="content">
    <p>算法及跃变计算条件</p>
    <p>空间品质判断条件</p>
    <p>需求自动计算条件</p>
    <p>通风系统</p>
    </div>

    </div>

    </template>

    <script setup>
    const open = ref(false);
    </script>


    上述代码非常简单,点击一级标题时,更改open的值,从而实现二级标题的内容区域展示与隐藏。


    箭头旋转动画



    实现箭头旋转动画其实非常容易,我们只要在红色面板展开时,给箭头添加一个新的类名,在这个类名中做一些动画处理即可。


    <template>
    <div class="header-wrap" @click="open = !open">
    <span class="text">自动化需求计算条件输如</span>
    <span class="arrow flex-be-ce" :class="{ open: open }">
    >
    </span>
    </div>

    </template>
    <style lang="less" scoped>
    .arrow {
    width: 16px;
    height: 16px;
    cursor: pointer;
    margin-left: 1px;
    transition: transform 0.2s ease;
    }
    .open {
    transform: rotate(90deg);
    transition: transform 0.2s ease;
    }
    </style>


    上述的代码通过 CSS 的 transform 属性和动态绑定open类名实现了箭头的旋转效果。



    注意:arrow也需要定义过渡效果



    折叠区域动画效果


    要实现折叠区域的动画效果,大致思路和上面一样。


    使用vue的transition组件实现


    借助vue的transition组件,我们可以实现折叠区域进入(v-show='true')和消失(v-show='fasle')的动画。一种可行的动画方案就是让面板进入前位置在y轴-100%的位置,进入后处于正常位置。



    <template>
    <div class="nav-bar-content">

    <div class="header-wrap" @click="open = !open">
    <span class="text">自动化需求计算条件输如</span>
    <span class="arrow" :class="{ open: open }">
    >
    </span>
    </div>

    <div class="content-wrap">
    <Transition>
    <div v-show="open" class="content">
    <p>算法及跃变计算条件</p>
    <p>空间品质判断条件</p>
    <p>需求自动计算条件</p>
    <p>通风系统</p>
    </div>
    </Transition>
    </div>

    </div>

    </template>

    <script setup>

    const open = ref(false);
    </script>


    <style lang="less" scoped>
    .v-enter-active,
    .v-leave-active {
    transition: transform 0.5s ease;
    }
    .v-enter-from,
    .v-leave-to {
    transform: translateY(-100%);
    }
    </style>


    上述效果有一点瑕疵,就是出现位置把一级标题盖住了,我们稍微修改下


    <div class="content-wrap">
    <Transition>
    <div v-show="open" class="content">
    <p>算法及跃变计算条件</p>
    <p>空间品质判断条件</p>
    <p>需求自动计算条件</p>
    <p>通风系统</p>
    </div>
    </Transition>

    </div>

    .content-wrap {
    overflow: hidden;
    }


    使用动态类名的方式实现


    效果好很多!但这种效果和第三方组件库的效果不太一致,我们以element的折叠面板效果为例:



    我们可以发现,它的这种动画,是折叠面板的高度从0逐渐增高的一个过程。所以最简单的就是,如果我们知道折叠面板的高度,一个类名就可以搞定!


    <template>
    <div class="nav-bar-content">

    <div class="header-wrap" @click="open = !open">
    <span class="text">自动化需求计算条件输如</span>
    <span class="arrow flex-be-ce" :class="{ open: open }">
    >
    </span>
    </div>

    <div class="content-wrap" :style="{ height: open ? '300px' : 0 }">
    <div class="content">
    <p>算法及跃变计算条件</p>
    <p>空间品质判断条件</p>
    <p>需求自动计算条件</p>
    <p>通风系统</p>
    </div>
    </div>

    </div>

    </template>

    <script setup>
    const open = ref(false);
    </script>


    <style lang="less" scoped>
    .content-wrap {
    height: 0;
    transition: height 0.5s ease;
    }
    </style>



    如果这个折叠面板的内容通过父组件传递,高度是动态的,我们只需要使用js计算这里的高度即可:


    <template>
    <div class="nav-bar-content">

    <div class="header-wrap" @click="open = !open">
    <span class="text">自动化需求计算条件输如</span>
    <span class="arrow flex-be-ce" :class="{ open: open }">
    >
    </span>
    </div>

    <div class="content-wrap" :style="{ height: open ? '300px' : 0 }">
    <div class="content" ref="contentRef">
    <slot></slot>
    </div>
    </div>

    </div>

    </template>

    <script setup>
    const open = ref(false);
    const contentRef = ref();
    const height = ref(0);
    onMounted(() => {
    height.value = contentRef.value.offsetHeight + 'px';
    });
    </script>


    <style lang="less" scoped>
    .content-wrap {
    height: 0;
    transition: height 0.5s ease;
    }
    </style>


    这样,我们就通过几行代码就实现了一个非常简单的折叠面板手风琴效果!



    总结


    要想实现一个折叠面板的效果,最简单的还是直接使用第三方组件库,但是如果项目不能使用其他组件库的话,手写一个也是非常简单的!也希望大家能在评论区给出更好的实现方式,供大家学习!


    作者:石小石Orz
    来源:juejin.cn/post/7369029201579278351
    收起阅读 »

    产品经理:实现一个微信输入框

    web
    近期在开发AI对话产品的时候为了提升用户体验增强了对话输入框的相关能力,产品初期阶段对话框只是一个单行输入框,导致在文本内容很多的时候体验很不好,所以进行体验升级,类似还原了微信输入框的功能(只是其中的一点点哈🤏)。 初期认为这应该改动不大,就是把input换...
    继续阅读 »


    近期在开发AI对话产品的时候为了提升用户体验增强了对话输入框的相关能力,产品初期阶段对话框只是一个单行输入框,导致在文本内容很多的时候体验很不好,所以进行体验升级,类似还原了微信输入框的功能(只是其中的一点点哈🤏)。


    初期认为这应该改动不大,就是把input换成textarea吧。但是实际开发过程发现并没有这么简单,本文仅作为开发过程的记录,因为是基于uniapp开发,相关实现代码都是基于uniapp


    简单分析我们大概需要实现以下几个功能点:



    • 默认单行输入

    • 可多行输入,但有最大行数限制

    • 超过限制行术后内容在内部滚动

    • 支持回车发送内容

    • 支持常见组合键在输入框内换行输入

    • 多行输入时高度自适应 & 页面整体自适应


    单行输入


    默认单行输入比较简单直接使用input输入框即可,使用textarea的时候目前的实现方式是通过设置行内样式的高度控制,如我们的行内高度是36px,那么就设置其高度为36px。为什么要通过这种方式设置呢?因为要考虑后续多行输入超出最大行数的限制,需要通过高度来控制textarea的最大高度。


    <textarea style="{ height: 36px }" />


    多行输入


    多行输入核心要注意的就是控制元素的高度,因为不能随着用户的输入一直增加高度,我们需要设置一个最大的行数限制,超出限制后就不再增加高度,内容可以继续输入,可以在输入框内上下滚动查看内容。


    这里需要借助于uniapp内置在textarea@linechange事件,输入框行数变化时调用并回传高度和行数。如果不使用uniapp则需要对输入文字的长度及当前行高计算出对应的行数,这种情况还需要考虑单行文本没有满一行且换行的情况。


    代码如下,在linechange事件中获取到最新的高度设置为textarea的高度,当超出最大的行数限制后则不处理。


    linechange(event) {
    const { height, lineCount } = event.detail
    if (lineCount < maxLine) {
    this.textareaHeight = height
    }
    }

    这是正常的输入,还有一种情况是用户直接粘贴内容输入的场景,这种时候不会触发@linechange事件,需要手动处理,根据粘贴文本后的textarea的滚动高度进行计算出对应的行数,如超出限制行数则设置为最大高度,否则就设置为实际的行数所对应的高度。代码如下:


    const paddingTop = parseInt(getComputedStyle(textarea).paddingTop);
    const paddingBottom = parseInt(getComputedStyle(textarea).paddingBottom);
    const textHeight = textarea.scrollHeight - paddingTop - paddingBottom;
    const numberOfLines = Math.floor(textHeight / lineHeight);

    if (numberOfLines > 1 && this.lineCount === 1) {
    const lineCount = numberOfLines < maxLine ? numberOfLines : maxLine
    this.textareaHeight = lineCount * lineHeight
    }

    键盘发送内容


    正常我们使用电脑聊天时发送内容都是使用回车键发送内容,使用ctrlshiftalt等和回车键的组合键将输入框的文本进行换行处理。所以接下来要实现的就是对键盘事件的监听,基于事件进行发送内容和内容换行输入处理。


    首先是事件的监听,uniapp不支持keydown的事件监听,所以这里使用了原生JS做监听处理,为了避免重复监听,对每次开始监听前先进行移除事件的监听,代码如下:


    this.$refs.textarea.$el.removeEventListener('keydown', this.textareaKeydownHandle)
    this.$refs.textarea.$el.addEventListener('keydown', this.textareaKeydownHandle)

    然后是对textareaKeydownHandle方法的实现,这里需要注意的是组合键对内容换行的处理,需要获取到当前光标的位置,使用textarea.selectionStart可获取,基于光标位置增加一个换行\n的输入即可实现换行,核心代码如下:


    const cursorPosition = textarea.selectionStart;
    if(
    (e.keyCode == 13 && e.ctrlKey) ||
    (e.keyCode == 13 && e.metaKey) ||
    (e.keyCode == 13 && e.shiftKey) ||
    (e.keyCode == 13 && e.altKey)
    ){
    // 换行
    this.content = `${this.content.substring(0, cursorPosition)}\n${this.content.substring(cursorPosition)}`
    }else if(e.keyCode == 13){
    // 发送
    this.onSend();
    e.preventDefault();
    }

    高度自适应


    当多行输入内容时输入框所占据的高度增加,导致页面实际内容区域的高度减小,如果不进行动态处理会导致实际内容会被遮挡。如下图所示,红色区域就是需要动态处理的高度。



    主要需要处理的场景就是输入内容行数变化的时候和用户粘贴文本的时候,这两种情况都会基于当前的可视行数计算输入框的高度,那么内容区域的高度就好计算了,使用整个窗口的高度减去输入框的高度和其他固定的高度如导航高度和底部安全距离高度即是真实内容的高度。


    this.contentHeight = this.windowHeight - this.navBarHeight - this.fixedBottomHeight - this.textareaHeight;

    最后


    到此整个输入框的体验优化核心实现过程就结束了,增加了多行输入,组合键换行输入内容,键盘发送内容,整体内容高度自适应等。整体实现过程的细节功能点还是比较多,有实现过类似需求的同学欢迎留言交流~


    看完本文如果觉得有用,记得点个赞支持,收藏起来说不定哪天就用上啦~




    作者:南城FE
    来源:juejin.cn/post/7267791228872753167
    收起阅读 »

    前端视角下的鸿蒙开发

    web
    前言 鸿蒙系统,一个从诞生就一直处于舆论风口浪尖上的系统,从最开始的“套壳”安卓的说法,到去年的不再兼容安卓的NEXT版本的技术预览版发布,对于鸿蒙到底是什么,以及鸿蒙的应用开发的讨论从来没停止过。 这次我们就从一个前端开发的角度来了解一下鸿蒙,学习一下鸿蒙...
    继续阅读 »

    前言



    鸿蒙系统,一个从诞生就一直处于舆论风口浪尖上的系统,从最开始的“套壳”安卓的说法,到去年的不再兼容安卓的NEXT版本的技术预览版发布,对于鸿蒙到底是什么,以及鸿蒙的应用开发的讨论从来没停止过。


    这次我们就从一个前端开发的角度来了解一下鸿蒙,学习一下鸿蒙应用的开发。



    一、 什么是鸿蒙


    在开始之前,先问大家一个问题,大家听说过几种鸿蒙?


    其实到目前为止,我们经常听到的鸿蒙系统,总共有三种,分别是:


    OpenHarmony,HarmonyOS,以及HarmonyOS NEXT。


    1. OpenHarmony


    OpenHarmony


    OpenHarmony(开源鸿蒙系统),由开放原子开源基金会进行管理。开放原子开源基金会由华为、阿里、腾讯、百度、浪潮、招商银行、360等十家互联网企业共同发起组建。包含了“鸿蒙操作系统”的基础能力,是“纯血”鸿蒙的底座。


    这个版本的鸿蒙是开源的,代码仓库的地址在这里:gitee.com/openharmony


    从我个人的一些粗浅理解来看,OpenHarmony类似于Android里的AOSP,可以装到各种设备上,比如手表、电视甚至是一些嵌入式设备上,详见可见官网的一些例子


    2. HarmonyOS


    HarmonyOS


    基于 OpenHarmony、AOSP等开源项目,同时加入了自己的HMS(因为被美国限制后无法使用GMS)的商用版本,可以兼容安卓,也可以运行部分OpenHarmony开发的鸿蒙原生应用。


    这个也是目前经常被吐槽是“套壳”安卓的系统,截止到目前(2024.04)已经更新到了HarmonyOS 4.2。


    3. HarmonyOS NEXT


    HarmonyOS NEXT


    2023年秋季发布的技术预览版,在当前HarmonyOS的基础上去除了AOSP甚至是JVM,不再兼容安卓,只能运行鸿蒙原生应用,同时对OpenHarmony的能里进行了大量的更新,增加和修改了很多API。


    这个也就是所谓的“纯血”鸿蒙系统,可惜的是这个目前我们用不到,需要以公司名义找华为合作开权限,或者个人开发者使用一台Mate60 Pro做专门的开发机。并且目前由于有保密协议,网上也没有太多关于最新API的消息。



    NEXT版本文档:developer.huawei.com/consumer/cn…



    无法直接访问的NEXT版本的开发文档


    据说目前HarmonyOS NEXT使用的API版本已经到了API12,目前官网可以访问的最新文档还是API9,所以接下来的内容也都是基于API9的版本来的。


    4. 小结


    所以一个粗略的视角来看,OpenHarmony、HarmonyOS以及HarmonyOS NEXT这三者之间的关系是这样的:


    三者之间的关系


    二、 初识鸿蒙开发


    在大概知道了什么是鸿蒙之后,我们先来简单看一下鸿蒙开发的套件。下图是官网所描述的一些开发套件,包括了设计、开发、测试、上架所涉及到的技术和产品。


    鸿蒙开发套件


    我们这篇文章里主要讨论右下角的三个:ArkTSArkUIArkCompiler


    ArkTS&ArkUI


    ArkCompiler


    三、 关于ArkTS的一些疑惑


    作为一个前端开发,最常用的编程语言就是JavaScript或者TypeScript,那么在看到鸿蒙应用开发用到的编程语言是ArkTS之后,我脑子里最先蹦出来的就是下面这几个问题:


    1. ArkTS语言的运行时是啥?


    既然编程语言是TS(TS的拓展,ArkTS),那么它的运行时是什么呢?是V8?JSC?Hermes?还是其他什么呢?


    2. ArkTS还是单线程语言吗?


    ArkTS还是和JS一样,是单线程语言吗?


    3. 基于TS拓展了什么?


    TS是JS的超集,对JS进行了拓展,增加了开发时的类型支持。而ArkTS对对TS又进行了拓展,是TS的超集,那它基于TS拓展了什么内容呢?


    下面我们一个一个来看。


    1. Question1 - ArkTS语言的运行时


    先说结论,ArkTS的运行时不是V8,不是JSC、Hermes,不是目前任何一种JS引擎。ArkTS的运行时是一个自研的运行时,叫做方舟语言运行时(简称方舟运行时)。


    方舟运行时


    而这个运行时,执行的也不是JS/TS/ArkTS代码,而是执行的字节码和机器码
    这是因为方舟运行时是ArkCompiler(方舟编译器)的一部分,对于JS/TS/ArkTS的编译在运行前就进行了(和Hermes有点像,下面会讲到)。


    方舟开发框架示意图


    我们来简单了解一下ArkCompiler,从官网的描述可以看到,ArkCompiler关注的重点主要有三个方面:



    • AOT 编译模式

    • LiteActor 轻量化并发

    • 源码安全


    AOT 编译模式


    首先是编译模式,我们知道,目前编程语言大多以下几方式运行:



    • 机器码AOT编译


      在程序运行之前进行AST生成和代码编译,编译为机器码,在运行的时候无需编译,直接运行,比如C语言。


    • 中间产物AOT编译


      在程序运行前进行AST生成并进行编译,但不是编译为机器码,而是编译为中间产物,之后在运行时将字节码解释为机器码再执行。比如Hermes或Java编译为字节码,之后运行时由Hermes引擎或JVM解释执行字节码。


    • 完全的解释执行


      在程序运行前不进行任何编译,在运行时动态地根据源码生成AST,再编译为字节码,最后解释执行字节码。比如没有开启JIT的V8引擎执行JS代码时的流程。


    • 混合的JIT编译


      在通过解释执行字节码时(运行时动态生成或者AOT编译生成),对多次执行的热点代码进行进一步的优化编译,生成机器码,后续执行到这部分逻辑时,直接执行优化后的机器码。比如开启JIT的V8引擎运行JS或者支持JIT的JVM运行class文件。




    当然,以上仅考虑生产环境下的运行方式,不考虑部分语言在生产和开发阶段不同的运行方式。比如Dart和Swift,一般是开发阶段通过JIT实时编译快速启动,生产环境下为了性能通过AOT编译。



    在V8 JIT出现之前,所有的JS虚拟机所采用的都是采用的完全解释执行的方式,在运行时把源码生成AST语法树,之后生成字节码,然后将字节码解释为机器码执行,这是JS执行速度过慢的主要原因之一。


    而这么做有以下两个方面的原因:



    • JS是动态语言,变量类型在运行时可能改变

    • JS主要用于Web应用,Web应用如果提前编译为字节码将导致体积增大很多,对网络资源的消耗会更大


    我们一个一个来说。


    a. JS变量类型在运行时可能改变

    首先我们来看一张图,这张图描述了现在V8引擎的工作流程,目前Chrome和Node里的JS引擎都是这个:


    V8现有工作流程


    从上面可以看到,V8在拿到JS源码后,会先解析成AST,之后经过Ignition解释器把语法树编译成字节码,然后再解释字节码执行。


    于此同时还会收集热点代码,比如代码一共运行了多少次、如何运行的等信息,也就是上面的Feedback的流程。


    如果发现一段代码会被重复执行,则监视器会将此段代码标记为热点代码,交给V8的Turbofan编译器对这段字节码进行编译,编译为对应平台(Intel、ARM、MIPS等)的二进制机器码,并执行机器码,也就是图里的Optimize流程。


    等后面V8再次执行这段代码,则会跳过解释器,直接运行优化后的机器码,从而提升这段代码的运行效率。


    但是我们发现,图里面除了Optimize外,还有一个Deoptimize,反优化,也就是说被优化成机器码的代码逻辑,可能还会被反优化回字节码,这是为什么呢?


    其实原因就是上面提到的“JS变量类型在运行时可能改变”,我们来看一个例子:


    JS变量类型在运行时可能改变


    比如一个add函数,因为JS没有类型信息,底层编译成字节码后伪代码逻辑大概如这张图所示。会判断xy的各种类型,逻辑比较复杂。


    在Ignition解释器执行add(1, 2)时,已经知道add函数的两个参数都是整数,那么TurboFan在进一步编译字节码时,就可以假定add函数的参数是整数,这样可以极大地简化生成的汇编代码,不再判断各种类型,伪代码如第三张图里所示。


    接下来的add(3, 4)add(5, 6)由于入参也是整数,可以直接执行之前编译的机器码,但是add("7", "8")时,发现并不是整数,这时候就只能将这段逻辑Deoptimize为字节码,然后解释执行字节码。


    这就是所谓的Deoptimize,反优化。可以看出,如果我们的JS代码中变量的类型变来变去,是会给V8引擎增加不少麻烦,为了提高性能,我们可以尽量不要去改变变量的类型。


    虽然说使用TS可以部分缓解这个问题,但是TS只能约束开发时的类型,运行的时候TS的类型信息是会被丢弃的,也无法约束,V8还是要做上面的一些假定类型的优化,无法一开始就编译为机器码。


    TS类型信息运行时被丢弃


    可以说TS的类型信息被浪费了,没有给运行时代码特别大的好处。


    b. JS编译为字节码将导致体积增大

    上面说到JS主要用于Web应用,Web应用如果提前编译为字节码将导致体积增大很多,对网络资源的消耗会更大。那么对于非Web应用,其实是可以做到提前编译为字节码的,比如Hermes引擎。


    Hermes作为React Native的运行时,是作为App预装到用户的设备上,除了热更新这种场景外,绝大部分情况下是不需要打开App时动态下载代码资源的,所以体积增大的问题影响不是很大,但是预编译带来的运行时效率提升的好处却比较明显。


    所以相对于V8,Hermes去掉了JIT,支持了生成字节码,在构建App的时候,就把JS代码进行了预编译,预编译为了Hermes运行时可以直接处理的字节码,省去了在运行阶段解析AST语法树、编译为字节码的工作。


    Hermes对JS编译和执行流程的改进



    一句题外话,Hermes去除了对JIT的支持,除了因为JIT会导致JS引擎启动时间变长、内存占用增大外,还有一部分可能的原因是,在iOS上,苹果为了安全考虑,不允许除了Safari和WebView(只有WKWebView支持JIT,UIWebView不支持)之外的第三方应用里直接使用JSC的JIT能力,也不允许第三方JS运行时支持JIT(相关问题)。


    甚至V8专门出了一个去掉JIT的JIT-less V8版本来在iOS上集成,Hermes似乎也不太可能完全没考虑到这一点。



    c. 取长补短

    在讨论了V8的JIT和Hermes的预编译之后,我们再来看看ArkCompiler,截取一段官方博客里的描述


    博客描述


    还记得上面说的“TS的类型信息被浪费了”吗?TS的类型信息只在开发时有用,在编译阶段就被丢弃了,而ArkCompiler就是利用了这一点,直接在App构建阶段,利用TS的类型信息直接预编译为字节码以及优化机器码。


    即在ArkCompiler中,不存在TS->JS的这一步转译,而是直接从TS编译为了字节码和优化机器码(这里存疑,官网文档没有找到很明确的说法,不是特别确定是否有TS->JS的转译。详见评论区,如果有知道的大佬可以在评论区交流一下)。


    同时由于鸿蒙应用也是一个App而不是Web应用,所以ArkCompiler和Hermes一样,也是在构建App时就进行了预编译,而不是在运行阶段做这个事情。


    ArkCompiler对JS/TS编译和执行流程的改进


    简单总结下来,ArkCompiler像Hermes一样支持生成字节码,同时又将V8引擎JIT生成机器码的工作也提前在预编译阶段做了。是比Hermes只生成字节码的AOT更进一步的AOT(同时生成字节码和部分优化后的机器码)。


    LiteActor轻量化并发


    到这里其实已经可以回答上面讲到的第二个问题了,ArkTS还是单线程语言吗?


    答案是:是的,还是单线程语言。但是ArkTS里通过Worker和TaskTool这两种方式支持并发。


    同时ArkCompiler对现有的Worker进行了一些优化,直接看官网博客


    LiteActor轻量化并发


    LiteActor轻量化并发博客描述


    这里的Actor是什么呢?Actor是一种并发编程里的线程模型。


    线程模型比较常见的就是共享内存模型,多个线程之间共享内存,比如Java里多个线程共享内存数据,需要通过synchronized同步锁之类的来防止数据一致性的问题。


    Actor模型是另一种线程模型,“Actor”是处理并发计算的基本单位,每个Actor都有自己的状态,并且可以接收和发送消息。当一个Actor接收到消息时,它可以改变自己的状态,发送消息给其他Actor,或者创建新的Actor。


    这种模型可以帮助开发者更好地管理复杂的状态和并发问题,因为每个Actor都是独立的,它们之间不会共享状态,这可以避免很多并发问题。同时,Actor模型也使得代码更易于理解和维护,因为每个Actor都是独立的,它们的行为可以被清晰地定义和隔离。


    到这里大家应该已经比较明白了,前端里的Web Worker就是这种线程模型的一种体现,通过Worker来开启不同的线程。


    源码安全


    按照官网的说法,ArkCompiler会把ArkTS编译为字节码,并且ArkCompiler使用多种混淆技术提供更高强度的混淆与保护,使得HarmonyOS应用包中装载的是多重混淆后的字节码,有效提高了应用代码安全的强度。


    源码安全


    2. Question2 - ArkTS还是单线程语言吗


    这个刚刚已经回答了,还是单线程语言,借用官网的描述:



    HarmonyOS应用中每个进程都会有一个主线程,主线程有如下职责:



    1. 执行UI绘制;

    2. 管理主线程的ArkTS引擎实例,使多个UIAbility组件能够运行在其之上;

    3. 管理其他线程(例如Worker线程)的ArkTS引擎实例,例如启动和终止其他线程;

    4. 分发交互事件;

    5. 处理应用代码的回调,包括事件处理和生命周期管理;

    6. 接收Worker线程发送的消息;


    除主线程外,还有一类与主线程并行的独立线程Worker,主要用于执行耗时操作,但不可以直接操作UI。Worker线程在主线程中创建,与主线程相互独立。最多可以创建8个Worker。



    ArkTS线程模型


    3. Question3 - 基于TS拓展了什么


    当前,ArkTS在TS的基础上主要扩展了如下能力:



    • 基本语法:ArkTS定义了声明式UI描述、自定义组件和动态扩展UI元素的能力,再配合ArkUI开发框架中的系统组件及其相关的事件方法、属性方法等共同构成了UI开发的主体。

    • 状态管理:ArkTS提供了多维度的状态管理机制。在UI开发框架中,与UI相关联的数据可以在组件内使用,也可以在不同组件层级间传递,比如父子组件之间、爷孙组件之间,还可以在应用全局范围内传递或跨设备传递。另外,从数据的传递形式来看,可分为只读的单向传递和可变更的双向传递。开发者可以灵活地利用这些能力来实现数据和UI的联动。

    • 渲染控制:ArkTS提供了渲染控制的能力。条件渲染可根据应用的不同状态,渲染对应状态下的UI内容。循环渲染可从数据源中迭代获取数据,并在每次迭代过程中创建相应的组件。数据懒加载从数据源中按需迭代数据,并在每次迭代过程中创建相应的组件。


    而上面这些,也就是我们接下来要介绍的ArkTS+ArkUI的语法。


    四、 ArkTS & ArkUI


    首先,在聊ArkUI之前,还有一个问题大家可能比较感兴趣:ArkUI是怎么渲染我们写的UI呢?


    答案是自绘,类似于Flutter,使用自己的渲染引擎(应该是发展于Skia),而不是像RN那样将UI转为不同平台上的底层UI。


    不管是从官网的描述[1]、[2]来看,还是社区里的讨论来看,ArkUI的渲染无疑是自绘制的,并且ArkUI和Flutter之间的联系很密切:


    社区里的一些讨论


    1. 基本语法


    从前端的角度来看,ArkTS和ArkUI的定位其实就是类似于前端中TS+React+配套状态管理工具(如Redux),可以用TS写声明式UI(有点像写jsx),下面是基本语法:


    基本语法



    • 装饰器


      用于装饰类、结构、方法以及变量,并赋予其特殊的含义。


      如上述示例中@Entry、@Component和@State都是装饰器,@Component表示自定义组件,@Entry表示该自定义组件为入口组件,@State表示组件中的状态变量,状态变量变化会触发UI刷新


    • 自定义组件


      可复用的UI单元,可组合其他组件,如上述被@Component装饰的struct Hello


    • UI描述


      以声明式的方式来描述UI的结构,例如build()方法中的代码块


    • 系统组件


      ArkUI框架中默认内置的基础和容器组件,可直接被开发者调用,比如示例中的ColumnTextDividerButton


    • 事件方法


      组件可以通过链式调用设置多个事件的响应逻辑,如跟随在Button后面的onClick()


    • 属性方法


      组件可以通过链式调用配置多项属性,如fontSize()width()height()backgroundColor()



    2. 数据驱动UI


    作为一个声明式的UI框架,ArkUI和其他众多UI框架(比如React、Vue)一样,都是通过数据来驱动UI变化的,即UI = f(State)。我们这里引用一下官网的描述:



    在声明式UI编程框架中,UI是程序状态的运行结果,用户构建了一个UI模型,其中应用的运行时的状态是参数。当参数改变时,UI作为返回结果,也将进行对应的改变。这些运行时的状态变化所带来的UI的重新渲染,在ArkUI中统称为状态管理机制。


    自定义组件拥有变量,变量必须被装饰器装饰才可以成为状态变量,状态变量的改变会引起UI的渲染刷新。如果不使用状态变量,UI只能在初始化时渲染,后续将不会再刷新。 下图展示了State和View(UI)之间的关系。



    State和UI



    View(UI):UI渲染,指将build方法内的UI描述和@Builder装饰的方法内的UI描述映射到界面。
    State:状态,指驱动UI更新的数据。用户通过触发组件的事件方法,改变状态数据。状态数据的改变,引起UI的重新渲染。



    在ArkUI中,提供了大量的状态管理相关的装饰器,比如@State@Prop@Link等。


    ArkTS & ArkUI的状态管理总览


    更多细节详见状态管理


    3. 渲染控制


    在ArkUI中,可以像React那样,通过if elsefor each等进行跳转渲染、列表渲染等,更多细节详见渲染控制



    ArkUI通过自定义组件build()函数和@builder装饰器中的声明式UI描述语句构建相应的UI。在声明式描述语句中开发者除了使用系统组件外,还可以使用渲染控制语句来辅助UI的构建,这些渲染控制语句包括控制组件是否显示的条件渲染语句,基于数组数据快速生成组件的循环渲染语句以及针对大数据量场景的数据懒加载语句。



    4. 更多语法


    语法其实不是我们这篇文章的重点,上面是一些大概的介绍,更多语法可以详见官网,或者我的另外一篇专门讲解语法的笔记《前端视角下的ArkTS语法》(先留个占位符,有时间了补充一下)。


    5. ArkTS & ArkUI小结


    从前面的内容其实可以看到,ArkUI和RN相似点还挺多的:



    1. 都是使用JS/TS作为语言(ArkTS)

    2. 都有自己的JS引擎/运行时(ArkCompiler,方舟运行时)

    3. 引擎还都支持直接AOT编译成字节码


    不同的是RN是将JS声明的UI,转换成iOS、Android原生的组件来渲染,而ArkUI则是采用自绘制的渲染引擎来自绘UI。


    从这点来看,鸿蒙更像是Flutter,只不过把开发语言从Dart换成了JS/TS(ArkTS),和Flutter同样是自绘制的渲染引擎。


    社区里其实也有类似的思考:其它方向的探索:JS Engine + Flutter RenderPipeLine。而ArkUI则是对这种思路的实现。


    感觉这也可以从侧面解释为什么ArkUI的语法和Flutter比较像,应该参考了不少Flutter的实现(比如渲染引擎)。


    而华为宣称鸿蒙可以反向兼容Flutter甚至是RN也就没有那么难以理解了,毕竟ArkUI里Flutter和RN的影子确实不少。


    另外,除了ArkUI以外,华为还提供了一个跨平台的开发框架ArkUI-X,可以像Flutter那样,跨HarmonyOS、Android、iOS三个平台。


    这么看来,ArkTS&ArkUI从开发语言、声明式UI的语法、设计思想来看,不管是前端、iOS、安卓、或者Flutter、RN,鸿蒙应用开发都是比较入门友好的。


    五、 其他


    1. 包管理工具


    HarmonyOS开发中,使用的包管理工具是ohpm,目前看来像是一个借鉴pnpm的三方包管理工具,详见官方文档


    另外,鸿蒙也提供了第三方包发布的仓库:ohpm.openharmony.cn


    2. 应用程序结构


    在鸿蒙系统中,一个应用包含一个或者多个Module,每一个Module都可以独立进行编译和运行。


    应用程序结构


    发布时,每个Module编译为一个.hap后缀的文件,即HAP。每个HarmonyOS应用可以包含多个.hap文件。


    在应用上架到应用市场时,需要把应用包含的所有.hap文件打包为一个.app后缀的文件用于上架。


    但是.app包不能直接安装到设备上,只是上架应用市场的单元,安装到设备上的是.hap


    打包结构


    开发态和打包后视图


    鸿蒙应用的整体开发调试与发布部署流程大概是这样的:


    开发-调试-发布-部署


    HAP可分为Entry和Feature两种类型:



    • Entry类型的HAP:是应用的主模块
      在同一个应用中,同一设备类型只支持一个Entry类型的HAP,通常用于实现应用的入口界面、入口图标、主特性功能等。

    • Feature类型的HAP:是应用的动态特性模块
      一个应用程序包可以包含一个或多个Feature类型的HAP,也可以不包含;Feature类型的HAP通常用于实现应用的特性功能,可按需下载安装


    而设计成多hap,主要是有3个目标:



    1. 为了解耦应用的各个模块,比如一个支付类型的App,Entry类型的hap可以是首页主界面,上面的扫一扫、消息、理财等可以的feature类型的HAP

    2. 方便开发者将多HAP合理地组合并部署到不同的设备上,比如有三个HAP,Entry、Feature1和Feature2,其中A类型的设备只能部署Entry和Feature1。B类型的设备只能部署Entry和Feature2

    3. 方便应用资源共享,减少程序包大小。多个HAP都需要用到的资源文件可以放到单独的HAP中



    多说一句:从这些描述来看,给我的感觉是每个.hap有点类似于前端项目中Mono-repo仓库中的一个package,各个package之间有一定的依赖,同时每个package可以独立发布。



    另外,HarmonyOS也支持类似RN热更新的功能,叫做快速修复(quick fix)。


    六、 总结


    现在再回到最开始那个问题:什么是鸿蒙?从前端视角来看,它是这样一个系统:



    • ArkTS作为应用开发语言

    • 类Flutter、Compose、Swift的声明式UI语法

    • 和React有些相似的数组驱动UI的设计思想

    • ArkCompiler进行字节码和机器码的AOT编译 + 方舟运行时

    • 类似Flutter Skia渲染引擎的自绘制渲染引擎

    • 通过提供一系列ohos.xxx的系统内置包来提供TS访问系统底层的能力(比如网络、媒体、文件、USB等)


    所以关于HarmonyOS是不是安卓套壳,个人感觉其实已经比较明了了:以前应该是,但快要发布的HarmonyOS NEXT大概率不再是了。


    其他一些讨论


    其实在华为宣布了HarmonyOS NEXT不再兼容安卓后,安卓套壳的声音越来越少了,但现在网上另外一种声音越来越多了:




    1. HarmonyOS NEXT是一个大号的小程序底座,上面的应用都是网页应用,应用可以直接右键查看源码,没有安全性可言

    2. HarmonyOS NEXT上的微信小程序就是在小程序里运行小程序

    3. 因为使用的是ArkTS开发,所以的HarmonyOS NEXT上的应用性能必然很差



    这种说法往往来自于只知道鸿蒙系统应用开发语言是TS,但是没有去进一步了解的人,而且这种说法还有很多人信。其实只要稍微看下文档,就知道这种说法是完全错误的


    首先它的View层不是DOM,而是类似Flutter的自绘制的渲染引擎,不能因为使用了TS就说是网页,就像可以说React Web是网页应用,但不能说React Native是网页应用,同样也不是说Flutter是网页应用。


    另外开发语言本身并不能决定最终运行性能,还是要看编译器和运行时的优化。同样是JS,从完全的解释执行(JS->AST->字节码->执行),到开启JIT的V8,性能都会有质的飞跃。从一些编程语言性能测试中可以看到,开启JIT的NodeJs的性能,甚至和Flutter所使用的Dart差不多。


    而ArkCompiler是结合了Hermes和V8 JIT的特点,AOT编译为字节码和机器码,所以理论上讲性能应该相当不错。


    (当然我也没有实机可以测试,只能根据文档来分析)。


    上面这种HarmonyOS NEXT是网页应用的说法还有可能是由于,最早鸿蒙应用支持使用HTML、CSS、JS三件套进行兼容Web的开发,导致了刻板印象。这种开发方式使用的是FA模型,而目前这种方式已经不是鸿蒙主推的开发方式了。


    到这里这篇文章就结束了,整体上是站在一个前端开发的视角下来认识和了解鸿蒙开发的,希望能帮助一些像我一样对鸿蒙开发感兴趣的前端开发入门。大家如果感兴趣可以到鸿蒙官网查看更多的了解。


    如果感觉对你有帮助,可以点个赞哦~


    作者:酥风
    来源:juejin.cn/post/7366948087129309220
    收起阅读 »

    责任链模式最强工具res-chain🚀

    web
    上面的logo是由ai生成 责任链模式介绍 责任链模式(Chain of Responsibility Pattern)是一种行为型设计模式,它通过把请求的发送者和接收者解耦,将多个对象连接成一个链,并沿着这条链传递请求,直到有一个对象能够处理它为止,从而避...
    继续阅读 »
    image.png

    上面的logo是由ai生成



    责任链模式介绍


    责任链模式(Chain of Responsibility Pattern)是一种行为型设计模式,它通过把请求的发送者和接收者解耦,将多个对象连接成一个链,并沿着这条链传递请求,直到有一个对象能够处理它为止,从而避免了请求的发送者和接收者之间的直接耦合


    在责任链模式中,每个处理者都持有对下一个处理者的引用,即构成一个链表结构。当请求从链头开始流经链上的每个处理者时,如果某个处理者能够处理该请求,就直接处理,否则将请求发送给下一个处理者,直到有一个处理者能够处理为止。


    这种方式可以灵活地动态添加或修改请求的处理流程,同时也避免了由于请求类型过多而导致类的爆炸性增长的问题。


    看完以上责任链的描述,有没有发现跟Node.js的某些库特别的像,没错,就是koa。什么?没用过koa?那我建议你立马学起来,因为它用起来特别的简单。


    下面来一个简单使用koa的例子:


    const Koa = require('koa');
    const app = new Koa();

    app.use(async (ctx, next) => {
    if (ctx.request.url === '/') {
    ctx.body = 'home';
    return;
    }

    next(); // 执行下面的回调函数
    });

    app.use(async (ctx, next) => {
    if (ctx.request.url === '/hello') {
    ctx.body = 'hello world';
    return;
    }
    });

    app.listen(3000);

    通过node运行上面的代码,在浏览器请求localhost:3000,接口就会返回home,当我们请求localhost:3000/hello,接口就会返回hello world


    上面对请求的处理过程就很符合职责链模式的思想,我们可以清楚的知道每个链做的工作,并且链条的顺序流程也很清晰。


    有人就会问,只在一个回调里面也能处理呀,比如下面的代码:


    app.use(async (ctx, next) => {
    if (ctx.request.url === '/') {
    ctx.body = 'home';
    return;
    } else if (ctx.request.url === '/home') {
    ctx.body = 'hello world';
    return
    }
    });

    是的,上面的代码确实可以实现,但是这就要回到我们使用责任链模式的初衷了:为了逻辑解耦。


    责任链解决的问题


    我们继续接着聊上一节的问题,使用if确实可以实现相同效果,但是在某些场景中,if并没有职责链那么好用,为什么这么说呢。


    我们找一个应用案例举个例子:



    假设我们负责一个售卖手机的网站,需求的定义是:经过分别缴纳500元定金和200元定金的两轮预订,现在到了正式购买阶段。公司对于交了定金的用户有一定的优惠政策,规则如下:




    • 缴纳500元定金的用户可以收到100元优惠券;

    • 缴纳200元定金的用户可以收到50元优惠券;

    • 没有缴纳定金的用户进入普通购买模式,没有优惠券。

    • 而且在库存不足的情况下,不一定能保证买得到。


    下面开始设计几个字段,解释它们的含义:



    • orderType:表示订单类型,值为1表示500元定金用户,值为2表示200元定金用户,值为3表示普通用户。

    • pay:表示用户是否支付定金,值为布尔值true和false,就算用户下了500元定金的订单,但是如果没有支付定金,那也会降级为普通用户购买模式。

    • stock:表示当前用户普通购买的手机库存数量,已经支付过定金的用户不受限制。


    下面我们分别用if和职责链模式来实现:


    使用if:


    const order = function (orderType, pay, stock) {
    if (orderType === 1) {
    if (pay === true) {
    console.log('500元定金预购,得到100元优惠券')
    } else {
    if (stock > 0) {
    console.log('普通用户购买,无优惠券')
    } else {
    console.log('手机库存不足')
    }
    } else if (orderType === 2) {
    if (pay === true) {
    console.log('200元定金预购,得到50元优惠券')
    } else {
    if (stock > 0) {
    console.log('普通用户购买,无优惠券')
    } else {
    console.log('手机库存不足')
    }
    }
    } else if (orderType === 3) {
    if (stock > 0) {
    console.log('普通用户购买,无优惠券')
    } else {
    console.log('手机库存不足')
    }
    }
    }

    order(1, true, 500) // 输出:500元定金预购,得到100元优惠券'

    虽然上面的代码也可以实现需求,但是代码实在是难以阅读,维护起来更是困难,如果继续在这个代码上开发,未来肯定会成为一座很大的屎山。


    下面我们使用责任链模式来实现:


    function printResult(orderType, pay, stock) {
    // 这里ResChain类是模拟koa的写法,后面会讲如何实现ResChain
    // 请先耐心看完它是如何处理的
    const resChain = new ResChain();
    // 针对500元定金的情况
    resChain.add('order500', (_, next) => {
    if (orderType === 1 && pay === true) {
    console.log('500元定金预购,拿到100元优惠券');
    return;
    }
    next(); // 这里将会调用order200对应的回调函数
    });
    // 针对200元定金的情况
    resChain.add('order200', (_, next) => {
    if (orderType === 2 && pay === true) {
    console.log('200元定金预购,拿到50元优惠券');
    return;
    }
    next(); // 这里会调用noOrder对应回调函数
    });
    // 针对普通用户购买的情况
    resChain.add('noOrder', (_, next) => {
    if (stock > 0) {
    console.log('普通用户购买,无优惠券');
    } else {
    console.log('手机库存不足');
    }
    });

    resChain.run(); // 开始执行order500对应的回调函数
    }

    // 测试
    printResult(1, true, 500); // 500元定金预购,得到100元优惠券
    printResult(1, false, 500); // 普通用户购买,无优惠券
    printResult(2, true, 500); // 200元定金预购,得到50元优惠券
    printResult(3, false, 500); // 普通用户购买,无优惠券
    printResult(3, false, 0); // 手机库存不足

    以上的代码经过责任链处理之后特别的清晰,并且减少了大量的if-else嵌套,每个链的职责分,我们可以看出责任链模式存在的优点:



    1. 降低了代码之间的耦合,很好的对每个处理逻辑进行封装。在每个链条内,只需要关注自身的逻辑实现。

    2. 增强了代码的可维护性。我们可以很轻易在原有链条内的任何位置添加新的节点,或者对链条内的节点进行替换或者删除。


    责任链还特别的灵活,如果说后面pm找我们加需求,需要加多一个预付定金400,返回80元优惠券,处理起来也是易如反掌,只需要怼回去,只需要在order500下面加多一个节点处理即可:


    ... 
    resChain.add('order500', (_, next) => {
    if (orderType === 1 && pay === true) {
    console.log('500元定金预购,拿到100元优惠券');
    return;
    }
    next();
    })
    + // 加上这一块
    + resChain.add('order400', (_, next) => {
    + if (orderType === 3 && pay === true) {
    + console.log('400元定金预购,拿80元优惠券');
    + return;
    + }
    + next();
    + })

    resChain.add('order200', (_, next) => {
    if (orderType === 2 && pay === true) {
    console.log('200元定金预购,拿到50元优惠券');
    return;
    }
    next();
    })
    ...

    就是这么简单。那这个ResChain是如何实现的呢?


    封装ResChain


    先别急,首先我们来了解一下koa是如何实现:在链节点的回调函数内调用next就可以跳到下一个节点的呢?


    话不多说,直接看源码,参考的库是koa-compose,代码也是特别的简洁:


    function compose (middleware) {
    // 这里传入的middleware是函数数组,例如: [fn1, fn2, fn3, fn4, ...]
    if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
    // 判断数组里的元素是不是函数类型
    for (const fn of middleware) {
    if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
    }

    return function (context, next) {
    // last called middleware #
    let index = -1
    return dispatch(0);

    // 这里利用了函数申明提升的特性
    function dispatch (i) {
    // 这里是防止重复调用next
    if (i <= index) return Promise.reject(new Error('next() called multiple times'))
    index = i

    // 从middleware中取出回调函数
    let fn = middleware[i]
    if (i === middleware.length) fn = next

    // 如果fn为空了,则结束运行
    if (!fn) return Promise.resolve()

    try {
    // next函数其实就是middleware的下一个函数,执行next就是执行下一个函数
    return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
    } catch (err) {
    return Promise.reject(err)
    }
    }
    }
    }

    看完源代码,我们接着来实现ResChain类,首先整理一下应该要有的方法:



    • add方法。可以添加回调函数,并按添加的顺序执行。

    • run方法。开始按顺序执行责任链。


    add方法执行的时候,把回调函数按顺序push进一个数组中。


    export class ResChain {

    /**
    * 按顺序存放链的key
    */

    keyOrder = [];
    /**
    * key对应的函数
    */

    key2FnMap = new Map();
    /**
    * 每个节点都可以拿到的对象
    */

    ctx = {}
    constructor(ctx) {
    this.ctx = ctx;
    }

    // 这里用key来标识当前callback的唯一性,后面重复添加可以区分。
    add(key, callback) {
    if (this.key2FnMap.has(key)) {
    throw new Error(`Chain ${key} already exists`);
    }

    this.keyOrder.push(key);
    this.key2FnMap.set(key, callback);
    return this;
    }

    async run() {
    let index = -1;
    const dispatch = (i) => {
    if (i <= index) {
    return Promise.reject(new Error('next() called multiple times'));
    }

    index = i;
    const fn = this.key2FnMap.get(this.keyOrder[i]);
    if (!fn) {
    return Promise.resolve(void 0);
    }

    return fn(this.ctx, dispatch.bind(null, i + 1));
    };

    return dispatch(0);
    }
    }

    add方法的第一个参数key可以用来判断是否已经添加过相同的回调。


    有人会说,koa的中间件是异步函数的,你这个行不行?


    当然可以,接下来看个异步的例子:


    const resChain = new ResChain();

    resChain.add('async1', async (_, next) => {
    console.log('async1');
    await next();
    });


    resChain.add('async2', async (_, next) => {
    console.log('async2')
    // 这里可以执行一些异步处理函数
    await new Promise((resolve, reject) => {
    setTimeOut(() => {
    resolve();
    }, 1000)
    });

    await next();
    });


    resChain.add('key3', async (_, next) => {
    console.log('key3');
    await next();
    });


    // 执行责任链
    await resChain.run();

    console.log('finished');

    // 先输出 async1 async2 然后停顿了1秒钟之后,才输出async3 finished


    🚧 需要注意:如果是异步模式,则链上的每个回调函数必须要 await next(),因为next函数代表下一个环的异步函数。



    koa的中间件方式简直一毛一样。


    有人可能还注意到了,ResChain实例化的时候可以传入对象,比如下面的代码:


    const resChain = new ResChain({ interrupt: false });

    传入对象具体有什么用法呢?可以用来获取一些在链中处理好的数据,来实现发送者和处理者的解耦。可能比较抽象,我们来举个例子。


    比如需要进行数据校验的场景,如果不通过,则中断提交:


    const ctx = {
    // 表单项
    model: {
    name: '',
    phone: '',
    },
    // 错误提示
    error: '',
    // 是否中断
    interrupt: false,
    }
    const resChain = new ResChain(ctx);

    resChain.add('校验name', (ctx, next) => {
    const { name = '' } = ctx;
    if (name === '') {
    ctx.error = '请填写name';
    ctx.interrupt = true;
    return;
    }

    next();
    })

    resChain.add('校验phone', (ctx, next) => {
    const { phone = '' } = ctx;
    if (phone === '') {
    ctx.error = '请填写手机号';
    ctx.interrupt = true;
    return;
    }

    next();
    })

    // 执行责任链
    resChain.run();

    // 如果需要中断,则提示
    if (resChain.ctx.interrupt) {
    alert(resChain.ctx.error);
    return;
    }

    如果是使用if来实现:


    const ctx = {
    // 表单项
    model: {
    name: '',
    phone: '',
    },
    // 错误提示
    error: '',
    // 是否中断
    interrupt: false,
    }

    if(ctx.model.name === '') {
    ctx.error = '请填写用户名';
    ctx.interrupt = true;
    }

    if (!ctx.interrupt && ctx.model.phone === '') {
    ctx.error = '请填写手机号';
    ctx.interrupt = true;
    }

    // 如果需要中断,则提示
    if (resChain.ctx.interrupt) {
    alert(resChain.ctx.error);
    return;
    }

    可以发现,对phone的判断逻辑,就要先判断interrupt是否为false才能继续,而且如果下面还有其他的字段校验,那必须都走一遍if。


    这也是责任链的一个优势,可以在某个环节按自己的想法停止,不用继续走后面的节点。


    目前我已经把这个工具上传到npm了,如果想要在自己的项目中使用,直接安装:
    res-chain即可使用:


    npm install res-chain

    # 或者
    # yarn add res-chain

    引入:


    import { ResChain } from 'res-chain';
    // CommonJS方式的引入也是支持的
    // const { ResChain } = 'res-chain';

    const resChain = new ResChain();

    resChain.add('key1', (_, next) => {
    console.log('key1');
    next();
    });

    resChain.add('key2', (_, next) => {
    console.log('key2');
    // 这里没有调用next,则不会执行key3
    });

    resChain.add('key3', (_, next) => {
    console.log('key3');
    next();
    });

    // 执行职责链
    resChain.run(); // => 将会按顺序输出 key1 key2

    芜湖起飞🚀。


    有了这个工具函数,我们就可以视场景去优化项目中的一大坨if-else嵌套,或者直接使用它来实现一些业务中比较复杂的逻辑。


    起源


    这个工具诞生的过程还挺巧合的,某一天周六我在公司加班赶需求,发现需要在一堆旧逻辑if-else中添加新的逻辑,强迫症的我实在是无法忍受在💩山上继续堆💩。。。


    我陷入沉思,用什么方式去优化呢?看了网上责任链模式的实现,感觉还是不够优雅。


    无意中翻到了之前用koa写的项目,突然灵光乍现💡,koa的中间件不就是一个很棒的实践。调用next就能够往下一个节点走,不调用的话就可以终止。


    于是立即动工,三下五除二就完成了。我还推荐给部门的其他前端小伙伴,他们也在一些需求的复杂逻辑中有运用。


    总结



    过去无意学到的某个知识,或者某个概念,在未来也许会发挥作用,你只需要做的就是等待。



    没有koa这么棒的库,估计也不会有这工具了,所以还是得感谢它的作者如此聪明。😁


    如果你也喜欢这个工具,欢迎去github里给个🌟,感谢。


    如果有什么更好的建议,在底下留言,一起探讨。


    工具链接


    res-chain


    参考



    作者:Johnhom
    来源:juejin.cn/post/7368662916151377959
    收起阅读 »

    你没见过的【只读模式】,被我玩出花了

    web
    前言 不是标题党,不是标题党,不是标题党,重要的话说三遍!大家常见的【只读模式】,下面简称 readonly,可能最常用的是在 表单场景中,除了正常的表单场景,你还会想象到它可以应用在我们中后台场景的 编辑表格、描述列表、查询表格 吗?先看看效果吧 ~ 表单场...
    继续阅读 »

    前言


    不是标题党,不是标题党,不是标题党,重要的话说三遍!大家常见的【只读模式】,下面简称 readonly,可能最常用的是在 表单场景中,除了正常的表单场景,你还会想象到它可以应用在我们中后台场景的 编辑表格描述列表查询表格 吗?先看看效果吧 ~


    表单场景


    form-readonly.gif


    表单列表场景


    form-list-readonly.gif


    描述列表场景


    description-readonly.gif


    查询表格场景


    table-readonly.gif


    编辑表格场景


    edit-table-readonly.gif


    上面看到的所有效果,背后都有 readonly 的存在



    1. 表单场景示例中表单列表场景示例中 使用 readonly,在实际业务中可能会应用到 编辑详情

    2. 描述列表场景示例中 使用 readonly,在实际业务中可能会应用到 单独的详情页 页面中

    3. 查询表格场景示例中 使用 readonly,在实际业务中应用很广泛,比如常见的日期,后端可能会返回字符串、空、时间戳,就不需要用户单独处理了 (挺麻烦的,不是吗)

    4. 编辑表格场景示例中 使用 readonly,在做一些类似 行编辑单元格编辑 功能中常用


    下面就以 实现思路 + 伪代码 的方式和大家分享 readonly 的玩法


    以 Date 组件为例


    我们这里说的 Date 就是单纯的日期组件,不包含 pickermonth(月份)quarter(季度) 等,我们先思考一下,如何让 日期组件 可以在多处公用(查询表格、表单、编辑表格、描述列表)


    多处公用


    我们可以将 Date 组件进行封装,变成 ProDate,我们在 ProDate 中扩展一个属性为 readonly,在扩展一个插槽 readonly,方便用户自定义,以下为伪代码


    <script lang="tsx">
    import { DatePicker, TypographyText } from 'ant-design-vue'

    export default defineComponent({
    name: 'ProDate',
    inheritAttrs: false,
    props: {
    readonly:{
    type: Boolean,
    default:false
    }
    },
    slots: {
    readonly: { rawValue: any }
    },
    setup(props, { slots, expose }) {
    const getReadonlyText = computed(() => {
    const value = toValue(dateValue)
    return getDateText(value, {
    format: toValue(valueFormat),
    defaultValue: toValue(emptyText),
    })
    })

    return {
    readonly,
    getReadonlyText,
    }
    },
    render() {
    const {
    readonly,
    getReadonlyText,
    } = this

    if (readonly)
    return $slots.readonly?.({ rawValue: xxx }) ?? getReadonlyText

    return <DatePicker {...xxx} v-slots={...xxx} />
    },
    })
    </script>


    上面的伪代码中,我们扩展了 readonly 属性和 readonly 插槽,我们 readonly 模式下会调用 getDateText 方法返回值,下面代码是 getDateText 的实现


    interface GetDateTextOptions {
    format: string | ((date: number | string | Dayjs) => any)
    defaultValue: any
    }

    // 工具函数
    export function getDateText(date: Dayjs | number | string | undefined | null, options: GetDateTextOptions) {
    const {
    format,
    defaultValue,
    } = options

    if (isNull(date) || isUndefined(date))
    return defaultValue

    if (isNumber(date) || isString(date)) {
    // 可能为时间戳或者字符串
    return isFunction(format)
    ? format(date)
    : dayjs(date).format(format)
    }

    if (isDayjs(date)) {
    return isFunction(format)
    ? format(date)
    : date.format(format)
    }

    return defaultValue
    }

    好了,伪代码我们实现完了,现在我们就假设我们的 ProDate 就是加强版的 DatePicker,这样我们就能很方便的集成到各个组件中了


    集成到 表单中


    因为我们是加强版的 DatePicker,还应该支持原来的 DatePicker 用法,我们上面伪代码没有写出来的,但是如果使用的话,还是如下使用


    <template>
    <AForm>
    <AFormItem>
    <ProDate v-model:value="xxxx" />
    </AFormItem>
    </AForm>

    </template>

    这样的话,我们如果是只读模式,可以在 ProDate 中增加 readonly 属性或插槽即可,当然,为了方便,我们实际上应该给 Form 组件也扩展一个 readonly 属性,然后 ProDatereadonly 属性的默认值应该是从 Form 中去取,这里实现我就不写出来了,思路的话可以通过在 Formprovide 注入默认值,然后 ProDate 中通过 inject


    好了,我们集成到 表单中 就说这么多,实际上还是有很多的细节的,如果大家想看的话,后面再写吧


    集成到 描述列表中


    描述列表用的是 Descriptions 组件,因为大部分用来做详情页,比较简单,所以这里我将它封装成了 json 方式,用 schemas 属性来描述每一项的内容,大概是以下用法


    <ProDescriptions
    title="详情页"
    :data-source="{time:'2023-01-30'}"
    :schemas="[
    {
    label:'日期',
    name:'time',
    component:'ProDate'
    }
    ]"

    />

    解释一下:


    上面的 schemas 中的项可以简单看成如下代码


    <DescriptionsItem>
    <ProDate
    readonly
    :value="get(dataSource,'time')"
    />

    </DescriptionsItem>

    我们在描述组件中应该始终传递 readonly: true,这样渲染出来虽然也是一个文本,但是经过了 ProDate 组件的日期处理,这样就可以很方便的直接展示了,而不用去写一个 render 函数自己去处理


    集成到 查询表格中


    实际上是和 集成到描述列表中 一样的思路,无非是将 ProDescriptions 组件换成 ProTable 组件,schemas 我们用同一套就可以,伪代码如下


    <ProTable
    title="详情页"
    :data-source="{time:'2023-01-30'}"
    :schemas="[
    {
    label:'日期',
    name:'time',
    component:'ProDate'
    }
    ]"

    />

    当然我们在 ProTable 内部对 schemas 的处理就要在 customRender 函数中去渲染了,内部实现的伪代码如下


    <Table 
    :columns="[
    {
    title:'日期',
    dataIndex:'time',
    customRender:({record}) =>{
    return <ProDate
    readonly
    value={get(record,'time')}
    />
    }
    }
    ]"

    />

    ProTableProDescriptions 的处理方式是类似的


    集成到 编辑表格中


    没啥好说的,实际上是和 集成到表单中 一样的思路,伪代码用法如下


    <ProForm>
    <ProEditTable
    :data-source="{time:'2023-01-30'}"
    :schemas="[
    {
    label:'日期',
    name:'time',
    component:'ProDate'
    }
    ]"

    />

    </ProForm>

    我们还是复用同一套的 schemas,只不过组件换成了 ProEditTable,不同的是,我们在内部就不能写死 readonly 了,因为可能会 全局切换成编辑或者只读某一行切换成编辑或者只读某个单元格切换成编辑或者只读,所以我们这里应该对每一个单元格都需要定义一个 readonly 的响应式属性,方便切换,具体的实现就不细说了,因为偏题了


    结语


    好了,我们分享了 只读模式 下不同组件下的表现,而不是简单的在 表单中 为了好看而实现的,下期再见 ~


    作者:一名爱小惠的前端
    来源:juejin.cn/post/7329691357211361318
    收起阅读 »

    面试官:能不能给 Promise 增加取消功能和进度通知功能... 我:???

    web
    扯皮 这段时间闲着没事就去翻翻红宝书,已经看到 Promise 篇了,今天又让我翻到两个陌生的知识点。 因为 Promise 业务场景太多了自我感觉掌握的也比较透彻,之前也跟着 Promise A+ 的规范手写过完整的 Promise,所以这部分内容基本上就大...
    继续阅读 »

    扯皮


    这段时间闲着没事就去翻翻红宝书,已经看到 Promise 篇了,今天又让我翻到两个陌生的知识点。


    因为 Promise 业务场景太多了自我感觉掌握的也比较透彻,之前也跟着 Promise A+ 的规范手写过完整的 Promise,所以这部分内容基本上就大致过一遍,直到看见关于 Promise 的取消以及监听进度...🤔


    只能说以后要是我当上面试官一定让候选人来谈谈这两个点,然后顺势安利我这篇文章🤣


    不过好像目前为止也没见哪个面试官出过...


    2024-04-03 更新:这段时间在刷牛客,无意间看到了 25 届佬: 收心檬 的个人主页 - 文章 - 掘金 (juejin.cn) 美团暑期实习的面经,绷不住了🤣


    pic.png


    原面经链接:美团暑期一面_牛客网 (nowcoder.com)


    不知道这位面试官是不是看了我的文章出的题,例子举的都大差不差🤣


    我确实标题党了想整个活,没想到大厂面试官真出啊,还是实习生,刁难人有一手的🙃...


    正文


    取消功能


    我们都知道 Promise 的状态是不可逆的,也就是说只能从 pending -> fulfilled 或 pending -> rejected,这一点是毋庸置疑的。


    但现在可能会有这样的需求,在状态转换过程当中我们可能不再想让它进行下去了,也就是说让它永远停留至 pending 状态


    奇怪了,想要一直停留在 pending,那我不调用 resolve 和 reject 不就行了🤔


     const p = new Promise((resolve, reject) => {
    setTimeout(() => {
    // handler data, no resolve and reject
    }, 1000);
    });
    console.log(p); // Promise {<pending>} 💡

    但注意我们的需求条件,是在状态转换过程中,也就是说必须有调用 resolve 和 reject,只不过中间可能由于某种条件,阻止了这两个调用。


    其实这个场景和超时中断有点类似但还是不太一样,我们先利用 Promise.race 来看看:模拟一个发送请求,如果超时则提示超时错误:


    const getData = () =>
    new Promise((resolve) => {
    setTimeout(() => {
    console.log("发送网络请求获取数据"); // ❗
    resolve("success get Data");
    }, 2500);
    });

    const timer = () =>
    new Promise((_, reject) => {
    setTimeout(() => {
    reject("timeout");
    }, 2000);
    });

    const p = Promise.race([getData(), timer()])
    .then((res) => {
    console.log("获取数据:", res);
    })
    .catch((err) => {
    console.log("超时: ", err);
    });

    问题是现在确实能够确认超时了,但 race 的本质是内部会遍历传入的 promise 数组对它们的结果进行判断,那好像并没有实现网络请求的中断哎🤔,即使超时网络请求还会发出:


    超时中断.png


    而我们想要实现的取消功能是希望不借助 race 等其他方法并且不发送请求。


    比如让用户进行控制,一个按钮用来表示发送请求,一个按钮表示取消,来中断 promise 的流程:



    当然这里我们不讨论关于请求的取消操作,重点在 Promise 上



    取消请求.png


    其实按照我们的理解只用 Promise 是不可能实现这样的效果的,因为从一开始接触 Promise 就知道一旦调用了 resolve/reject 就代表着要进行状态转换。不过 取消 这两个字相信一定不会陌生,clearTimeoutclearInterval 嘛。


    OK,如果你想到了这一点这个功能就出来了,我们直接先来看红宝书上给出的答案:


    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    </head>
    <body>
    <button id="send">Send</button>
    <button id="cancel">Cancel</button>

    <script>
    class CancelToken {
    constructor(cancelFn) {
    this.promise = new Promise((resolve, reject) => {
    cancelFn(() => {
    console.log("delay cancelled");
    resolve();
    });
    });
    }
    }
    const sendButton = document.querySelector("#send");
    const cancelButton = document.querySelector("#cancel");

    function cancellableDelayedResolve(delay) {
    console.log("prepare send request");
    return new Promise((resolve, reject) => {
    const id = setTimeout(() => {
    console.log("ajax get data");
    resolve();
    }, delay);

    const cancelToken = new CancelToken((cancelCallback) =>
    cancelButton.addEventListener("click", cancelCallback)
    );
    cancelToken.promise.then(() => clearTimeout(id));
    });
    }
    sendButton.addEventListener("click", () => cancellableDelayedResolve(1000));
    </script>
    </body>
    </html>

    这段代码说实话是有一点绕的,而且个人觉得是有多余的地方,我们一点一点来看:


    首先针对于 sendButton 的事件处理函数,这里传入了一个 delay,可以把它理解为取消功能期限,超过期限就要真的发送请求了。我们看该处理函数内部返回了一个 Promise,而 Promise 的 executor 中首先开启了定时器,并且实例化了一个 CancelToken,而在 CancelToken 中才给 cancelButton 添加点击事件。


    这里的 CancelToken 就是我觉得最奇怪的地方,可能没有体会到这个封装的技巧,路过的大佬如果有理解的希望能帮忙解释一下。它的内部创建了一个 Promise,绕了一圈后相当于 cancelButton 的点击处理函数是调用这个 Promise 的 resolve,最终是在其 pending -> fuilfilled,即 then 方法里才去取消定时器,那为什么不直接在事件处理函数中取消呢?难道是为了不影响主执行栈的执行所以才将其推到微任务处理🤔?


    介于自己没理解,我就按照自己的思路封装个不一样的🤣:


    const sendButton = document.querySelector("#send");
    const cancelButton = document.querySelector("#cancel");

    class CancelPromise {

    // delay: 取消功能期限 request:获取数据请求(必须返回 promise)
    constructor(delay, request) {
    this.req = request;
    this.delay = delay;
    this.timer = null;
    }

    delayResolve() {
    return new Promise((resolve, reject) => {
    console.log("prepare request");
    this.timer = setTimeout(() => {
    console.log("send request");
    this.timer = null;
    this.req().then(
    (res) => resolve(res),
    (err) => reject(err)
    );
    }, this.delay);
    });
    }

    cancelResolve() {
    console.log("cancel promise");
    this.timer && clearTimeout(this.timer);
    }
    }

    // 模拟网络请求
    function getData() {
    return new Promise((resolve) => {
    setTimeout(() => {
    resolve("this is data");
    }, 2000);
    });
    }

    const cp = new CancelPromise(1000, getData);

    sendButton.addEventListener("click", () =>
    cp.delayResolve().then((res) => {
    console.log("拿到数据:", res);
    })
    );
    cancelButton.addEventListener("click", () => cp.cancelResolve());

    正常发送请求获取数据:


    发送请求.gif


    中断 promise:


    取消请求.gif


    没啥大毛病捏~


    进度通知功能


    进度通知?那不就是类似发布订阅嘛?还真是,我们来看红宝书针对这块的描述:



    执行中的 Promise 可能会有不少离散的“阶段”,在最终解决之前必须依次经过。某些情况下,监控 Promise 的执行进度会很有用



    这个需求就比较明确了,我们直接来看红宝书的实现吧,核心思想就是扩展之前的 Promise,为其添加 notify 方法作为监听,并且在 executor 中增加额外的参数来让用户进行通知操作:


    class TrackablePromise extends Promise {
    constructor(executor) {
    const notifyHandlers = [];
    super((resolve, reject) => {
    return executor(resolve, reject, (status) => {
    notifyHandlers.map((handler) => handler(status));
    });
    });
    this.notifyHandlers = notifyHandlers;
    }
    notify(notifyHandler) {
    this.notifyHandlers.push(notifyHandler);
    return this;
    }
    }
    let p = new TrackablePromise((resolve, reject, notify) => {
    function countdown(x) {
    if (x > 0) {
    notify(`${20 * x}% remaining`);
    setTimeout(() => countdown(x - 1), 1000);
    } else {
    resolve();
    }
    }
    countdown(5);
    });

    p.notify((x) => setTimeout(console.log, 0, "progress:", x));
    p.then(() => setTimeout(console.log, 0, "completed"));


    emm 就是这个例子总感觉不太好,为了演示这种效果还用了递归,大伙们觉得呢?


    不好就自己再写一个🤣!不过这次的实现就没有多大问题了,基本功能都具备也没有什么阅读障碍,我们再添加一个稍微带点实际场景的例子吧:



    // 模拟数据请求
    function getData(timer, value) {
    return new Promise((resolve) => {
    setTimeout(() => {
    resolve(value);
    }, timer);
    });
    }

    let p = new TrackablePromise(async (resolve, reject, notify) => {
    try {
    const res1 = await getData1();
    notify("已获取到一阶段数据");
    const res2 = await getData2();
    notify("已获取到二阶段数据");
    const res3 = await getData3();
    notify("已获取到三阶段数据");
    resolve([res1, res2, res3]);
    } catch (error) {
    notify("出错!");
    reject(error);
    }
    });

    p.notify((x) => console.log(x));
    p.then((res) => console.log("Get All Data:", res));


    notify获取数据.gif


    对味儿了~😀


    End


    关于取消功能在红宝书上 TC39 委员会也曾准备增加这个特性,但相关提案最终被撤回了。结果 ES6 Promise 被认为是“激进的”:只要 Promise 的逻辑开始执行,就没有办法阻止它执行到完成。


    实际上我们学了这么久的 Promise 也默认了这一点,因此这个取消功能反而就不太符合常理,而且十分鸡肋。比如说我们有使用 then 回调接收数据,但因为你点击了取消按钮造成 then 回调不执行,我们知道 Promise 支持链式调用,那如果还有后续操作都将会被中断,这种中断行为 debug 时也十分痛苦,更何况最麻烦的一点是你还需要传入一个 delay 来表示取消的期限,而这个期限到底要设置多少才合适呢...


    至于说进度通知功能,仁者见仁智者见智吧...


    但不管怎么样两个功能实现的思路都是比较有趣的,而且不太常见,不考虑实用性确实能够成为一道考题,只能说很符合面试官的口味😏


    作者:討厭吃香菜
    来源:juejin.cn/post/7312349904046735400
    收起阅读 »

    检测图片是否cmyk

    web
    引入 最近业务上有要求,要求如果是 Jpeg 格式文件, 前端在上传的时候要求判断一下这个文件是否 CMYK 颜色模式(color mode/ color space)。 这个颜色模式是打印行业需要的。如果不是则禁止上传,并提示用户。 一开始我以为这个应该存储...
    继续阅读 »

    引入


    最近业务上有要求,要求如果是 Jpeg 格式文件, 前端在上传的时候要求判断一下这个文件是否 CMYK 颜色模式(color mode/ color space)。 这个颜色模式是打印行业需要的。如果不是则禁止上传,并提示用户。


    一开始我以为这个应该存储在 exif 文件信息中, 去拿一下就好了, 但是简单测试发现两个问题:



    1. 文件是否携带 exif 信息是不确定的, 即便出自设计师导出文件, 有可能也是不携带颜色模式信息的。

    2. 除此之外, 依靠 exif 信息去判断,严格来说,即便携带,也是不准确的, 因为这个信息是可以被人为修改的。


    经过一番研究, 我暂时发现可能有两种方式,去达成目的。 但是这篇文章实际不是以解决问题为导向,而是期望尽可能的深入一丢丢。 如果急于找到解决方案, 直接翻到文章底部查看具体 编码实现 即可。


    什么是 CMYK 颜色模式?



    了解 Photoshop 颜色模式 (adobe.com)



    CMYK 是一种颜色模式,它表示四种颜色通道:青色(Cyan)、品红色(Magenta)、黄色(Yellow)和黑色(Key,通常表示黑色)。这种颜色模式主要用于印刷和彩色印刷工作中。


    以下是 CMYK 颜色模式中各颜色通道的简要介绍:



    1. 青色 (Cyan): 表示蓝绿色。在印刷中,它用于调整蓝色和绿色的浓度。

    2. 品红色 (Magenta): 表示品红或洋红色。在印刷中,它用于调整红色和蓝色的浓度。

    3. 黄色 (Yellow): 表示黄色。在印刷中,它用于调整红色和绿色的浓度。

    4. 黑色 (Key): 通常表示黑色。在印刷中,黑色是通过使用黑色油墨单独添加的,以增加图像的深度和对比度。在 CMYK 模式中,K 代表 Key,以避免与蓝色 (B) 冲突。


    这四个颜色通道可以叠加在一起以创建各种颜色。通过调整每个通道的浓度,可以实现广泛的颜色表达。CMYK 被广泛用于印刷领域,因为它能够准确地模拟很多颜色,并提供了在印刷过程中需要的色彩控制。


    与 RGB(红绿蓝)颜色模式不同,CMYK 是一种适合印刷的颜色模式,因为它更好地反映了油墨混合的方式,并考虑到印刷物质上的光的特性


    怎么在web判断一个 jpeg/jpg 文件 颜色模式是否 cmyk ?


    简单说一下这两种方法, 实际上是同一种原理, 因为对于一张图片而言, 它除了携带有 exif 文件元信息之外, 还有文件头信息。


    既然不能通过 exif 元信息去判断, 那么我们可以通过文件头信息去做判断。


    首先,简单测试可以发现, 即便一个 cmyk 图片没有 exif 描述元信息标识这是一个 cmyk 颜色模式的图片, 但是 各种设计类软件都能够标记出来。 以ps为例:


    image-20231128163932682.png
    但是 exif 信息中是没有的:


    image-20231128164033843.png


    甚至一些解析库,就连最基本携带的元信息都没读出来:



    stackblitz.com/edit/exif-j…



    image-20231128164214625.png


    为什么设计软件可以标记出这个图片是否是 cmyk 颜色模式?


    这个问题, 我在网上翻了很久,确实是找不到相关文章有阐述设计软件的原理。 不过Ai 的回答是这样的, 具备一定的参考性:



    有朋友找到了记得踢我一脚,这里提前感谢啦~



    image-20231128174834089.png


    用 ImageMagic 解析图片文件



    什么是 imageMagic ?


    ImageMagick 主要由大量的命令行程序组成,而不提供像 Adobe Photoshop、GIMP 这样的图形界面。它还为很多程序语言提供了 API 库。


    ImageMagick 的功能包括:



    • 查看、编辑位图文件

    • 进行图像格式转换

    • 图像特效处理

    • 图像合成

    • 图像批处理


    ImageMagick 广泛用于图像处理、图形设计、Web 开发等领域。它是许多开源软件项目的重要组成部分,例如 GIMP、Inkscape、Linux 系统中的图像工具等。


    ImageMagick 的优势包括:



    • 功能强大,支持多种图像格式和图像处理功能

    • 开放源代码,免费使用

    • 、、可移植性强,支持多种操作系统



    @jayce: imageMagic 类似于 ffmpeg, 只不过它专注图像处理




    我们可以利用 ImageMagic 的 identify 工具命令 去解析图片以查看一些信息:


    image-20231128180008082.png


    加上 -verbose 选项可以查看更多详细信息:


    $ ./magick identify -verbose ./CMYK.jpg
    $ ./magick identify -verbose ./RGB.jpg

    image-20231129092244504.png


    这些数据是什么? 从哪里解析出来的呢? 这个需要看一下 jpeg 文件的一些标准文件结构


    ISO/IEC 10918-1 和 ISO/IEC 10918-5


    这两个文件都是 JPEG 的标准文档,只是不同的部分,wiki 上对二者描述大致是 5 是 对 1 的很多细节的展开和补充。是补充规范


    JPEG File Interchange Format (JFIF) 和 Exif


    JFIF(JPEG File Interchange Format)和 EXIF(Exchangeable image file format)是两种与 JPEG 图像相关的标准,但它们具有不同的目的和功能。


    JFIF 是一个图片文件格式标准, 它被发布于 10918-5, 是对 10918-1 的细节补充。



    1. JFIF (JPEG File Interchange Format):

      • 目的: JFIF 是一种用于在不同设备和平台之间交换 JPEG 图像的简单格式。它定义了 JPEG 文件的基本结构,以确保文件在不同系统中的一致性和可互操作性。

      • 特点: JFIF 文件通常包含了基本的图像数据,但不一定包含元数据信息。它主要关注图像的编码和解码,而不太关心图像的其他详细信息。JFIF 文件通常使用 .jpg 或 .jpeg 扩展名。



    2. EXIF (Exchangeable image file format):

      • 目的: EXIF 是一种在数字摄影中广泛使用的标准,用于嵌入图像文件中的元数据信息。这些元数据可以包括拍摄日期、相机型号、曝光时间、光圈值等。EXIF 提供了更丰富的信息,有助于记录和存储与拍摄有关的详细数据。

      • 特点: EXIF 数据以二进制格式嵌入在 JPEG 图像中,提供了关于图像和拍摄条件的详细信息。这对于数字相机和其他支持 EXIF 的设备非常有用。EXIF 文件通常使用 .jpg 或 .jpeg 扩展名。




    JPEG 文件标准结构语法


    jpeg 作为压缩数据结构, 是一个非常复杂的数据组织, 我们的关注点只在关系到我们想要解决的问题。 标准文档 ISO/IEC 10918-1 : 1993(E).中有部分相关说明。


    概要:


    结构上来说, jpeg 的数据格式由以下几个部分,有序组成: parameters, markers, 以及 entropy-coded data segments, 其中 parameters 和 markers 部分通常被组织到 marker segments, 因为它们都是用字节对齐的代码表, 都是由8位字节的有序序列组成。


    Parameters


    这部分携带有参数编码关键信息, 是图片成功被解析的关键。


    Markers


    Markers 标记用于标识压缩数据格式的各种结构部分。大多数标记开始包含一组相关参数的标记段;有些标记是单独存在的。所有标记都被分配了两个字节的代码


    例如 SOI : 从 0xFF,0xD8这两个字节开始,标记为图片文件的文件头开始, SOF0: 从 0xFF, 0xD8这两个字节开始,标记了 ”帧“ 的开始,它实际上会携带有图片的一些基本信息, 例如宽高,以及颜色通道等。 这个颜色通道其实也是我们主要需要关注的地方。


    下表是完整的标记代码:


    image-20231129095415255.png



    @refer:


    http://www.digicamsoft.com/itu/itu-t81…
    http://www.digicamsoft.com/itu/itu-t81…



    wiki 上也有相关的帧头部字段说明:


    Short nameBytesPayloadNameComments
    SOI0xFF, 0xD8noneStart Of Image
    SOF00xFF, 0xC0variable sizeStart Of Frame (baseline DCT)Indicates that this is a baseline DCT-based JPEG, and specifies the width, height, number of components, and component subsampling (e.g., 4:2:0).
    SOF20xFF, 0xC2variable sizeStart Of Frame (progressive DCT)Indicates that this is a progressive DCT-based JPEG, and specifies the width, height, number of components, and component subsampling (e.g., 4:2:0).
    DHT0xFF, 0xC4variable sizeDefine Huffman Table(s)Specifies one or more Huffman tables.
    DQT0xFF, 0xDBvariable sizeDefine Quantization Table(s)Specifies one or more quantization tables.
    DRI0xFF, 0xDD4 bytesDefine Restart IntervalSpecifies the interval between RSTn markers, in Minimum Coded Units (MCUs). This marker is followed by two bytes indicating the fixed size so it can be treated like any other variable size segment.
    SOS0xFF, 0xDAvariable sizeStart Of ScanBegins a top-to-bottom scan of the image. In baseline DCT JPEG images, there is generally a single scan. Progressive DCT JPEG images usually contain multiple scans. This marker specifies which slice of data it will contain, and is immediately followed by entropy-coded data.
    RSTn0xFF, 0xDn (n=0..7)noneRestartInserted every r macroblocks, where r is the restart interval set by a DRI marker. Not used if there was no DRI marker. The low three bits of the marker code cycle in value from 0 to 7.
    APPn0xFF, 0xEnvariable sizeApplication-specificFor example, an Exif JPEG file uses an APP1 marker to store metadata, laid out in a structure based closely on TIFF.
    COM0xFF, 0xFEvariable sizeCommentContains a text comment.
    EOI0xFF, 0xD9noneEnd Of Image


    Syntax and structure



    整体结构


    image-20231129111546427.png



    @refer: http://www.digicamsoft.com/itu/itu-t81…



    Frame Header


    image-20231129111645470.png


    image-20231129112439294.png


    image-20231129112306831.png



    @refer: http://www.digicamsoft.com/itu/itu-t81…



    SOFn : 帧开始标记标记帧参数的开始。下标n标识编码过程是基线顺序、扩展顺序、渐进还是无损,以及使用哪种熵编码过程。


    在其标准文档中,我们有找到 SOFn 的子字段说明,不过在其他地方,倒是看到了不少描述:


    特别是在这里 JPEG File Layout and Format


    image-20231129141121729.png


    可以看到,在 SOFn 这个标记中, 有一个字段为会指明 components 的数量,它代表的实际上颜色通道, 如果是 1,那么就是灰度图, 如果是3,那就是RGB, 如果是 4 就是 CMYK.


    到这里我们就知道了, 我们可以读取到这个对应的字节段,从而判断一个图片的颜色模式了。


    怎么读取呢?


    这篇资料说了明了 Jpeg 文件格式中字节和上述字段的关联关系: Anatomy of a JPEG


    注意这篇资料中有一段描述,会影响到我们后续的逻辑判断:


    image-20231129142053598.png



    就是 SOF0 是必须的,但是可以被 SOFn>=1 替换。 所以在做逻辑判断的时候,后续的也要判断。



    我们可以先大概看看一个图片文件的字节流数据长什么样子:(因为所有的字段都是 FF 字节位开头,所以高亮了)


    1701248911601.png



    以上页面可以在这里访问: jaycethanks.github.io/demos/DemoP…



    但样太不便于阅读了, 而且实在太长了。 这里有个网站 here,可以将关键的字节段截取出来:


    image-20231129171534152.png


    我们主要看这里:


    image-20231129171621345.png
    可以看到 components 为 4.


    如果是 RGB:


    image-20231129171722071.png


    这里就是 3,


    如果是灰度图,components 就会是1


    image-20231129172500605.png


    EXIF 在哪里?


    一个额外的小问题, 我们常见的 exif 元信息存储在哪里呢?


    其实上面的 Markers 部分给出的表格中也说明了 ,在 Appn 中可以找到 exif 信息, 但是wiki 上说的是 App0, 在这个解析网站中,我们可以看到:


    image-20231201113938640.png


    编码实现


    有了上述具体的分析, 我们就能有大致思路, 这里直接给出相关代码:



    代码参考 github.com/zengming00/node-jpg-is-cmyk




    /**
    *
    @refer https://github.com/zengming00/node-jpg-is-cmyk/blob/master/src/index.ts

    *
    @refer https://cyber.meme.tips/jpdump/#
    *
    @refer https://mykb.cipindanci.com/archive/SuperKB/1294/JPEG%20File%20Layout%20and%20Format.htm
    *
    @refer https://www.ccoderun.ca/programming/2017-01-31_jpeg/
    *
    * 通过 jpg 文件头判断是否是 CMYK 颜色模式
    *
    @param { Uint8Array } data
    */

    function checkCmyk(data: Uint8Array) {
    let pos = 0;
    while (pos < data.length) {
    pos++;
    switch (data[pos]) {
    case 0xd8: {// SOI - Start of Image
    pos++;
    break;
    }
    case 0xd9: {// EOI - End of Image
    pos++;
    break;
    }
    case 0xc0: // SOF0 - Start of Frame, Baseline DCT
    case 0xc1: // SOF1 - Start of Frame, Extended Sequential DCT
    case 0xc2: { // SOF2 - Start of Frame, Progressive DCT
    pos++;
    const len = (data[pos] << 8) | data[pos + 1];
    const compoNum = data[pos + 7];
    if (compoNum === 4) {
    // 如果components 数量为4, 那么就认为是 cmyk
    return true;
    }
    pos += len;
    break;
    }
    case 0xc4: { // DHT - Define Huffman Table
    pos++;
    const len = (data[pos] << 8) | data[pos + 1];
    pos += len;
    break;
    }
    case 0xda: { // SOS - Start of Scan
    pos++;
    const len = (data[pos] << 8) | data[pos + 1];
    pos += len;
    break;
    }
    case 0xdb: { // DQT - Define Quantization Table
    pos++;
    const len = (data[pos] << 8) | data[pos + 1];
    pos += len;
    break;
    }
    case 0xdd: { // DRI - Define Restart Interval
    pos++;
    const len = (data[pos] << 8) | data[pos + 1];
    pos += len;
    break;
    }
    case 0xe0: { // APP0 - Application-specific marker
    pos++;
    const len = (data[pos] << 8) | data[pos + 1];
    pos += len;
    break;
    }
    case 0xfe: { // COM - Comment
    pos++;
    const len = (data[pos] << 8) | data[pos + 1];
    pos += len;
    break;
    }
    default: {
    pos++;
    const len = (data[pos] << 8) | data[pos + 1];
    pos += len;
    }
    }
    }
    return false;
    }

    有没有其他的方法?


    既然 imageMagic 这么成熟且强大, 我们有办法利用它来做判断吗?


    我们可以通过 wasm, 在web中去利用这些工具, 我找到了 WASM-ImageMagick 这个, 但是他的打包好像有些问题 vite 引入的时候会报错,看着好像也没有要修复的意思, issue 里面有老哥自己修改了打包配置进行了修复在这里: image-magick


    我们就写的demo测试函数:


    import * as Magick from '@xn-sakina/image-magick'

    export default function (file: File) {
    if (!file) return;
    // 创建FileReader对象
    var reader = new FileReader();
    // 当读取完成时的回调函数
    reader.onload = async function (e) {
    // 获取ArrayBuffer
    var arrayBuffer = e.target?.result as ArrayBuffer;
    if (arrayBuffer) {
    // 将 ArrayBuffer 转换为 Uint8Array
    const sourceBytes = new Uint8Array(arrayBuffer);
    const inputFiles = [{ name: 'srcFile.png', content: sourceBytes }]
    let commands: string[] = ["identify srcFile.png"]
    const { stdout } = await Magick.execute({inputFiles, commands});

    // 这里打印一下结果
    console.log('stdout:',stdout[0])

    }
    };
    // 读取文件为ArrayBuffer
    reader.readAsArrayBuffer(file);
    }

    import isCmyk from '../utils/isCmyk.ts'
    const handleFileChange = (e: Event) => {
    const file = (e.target as HTMLInputElement)?.files?.[0]
    isCmyk(file) // 这里文件上传调用一下
    ......

    测试几个文件


    image-20231130104941592.png


    可以看到, Gray, RGB, CMYK 检测都可以正常输出, 说明可以这么干。



    但是这个库, 文档写的太乱了。 - -



    这个库的大小有 5 m之大 - -, npm 上找了下, 目前相关的包,也没有比这个更小的好像。


    作者:sun_zy
    来源:jaycethanks.github.io/blog_11ty/posts/Others/%E6%A3%80%E6%B5%8B%E5%9B%BE%E7%89%87%E6%98%AF%E5%90%A6cmyk/
    收起阅读 »

    需求小能手——拦截浏览器窗口关闭

    web
    前言 最近碰到一个需求,网页端页面有评价功能,要求用户点击关闭浏览器时强制弹出评价对话框让用户评价。刚听到这个需求我大意了没有闪,以为很简单,没想到很难实现,很多需求果然不能想当然啊,接下来我们来看一下该功能实现的一些思路。 窗口关闭 要想实现该功能最简单的想...
    继续阅读 »

    前言


    最近碰到一个需求,网页端页面有评价功能,要求用户点击关闭浏览器时强制弹出评价对话框让用户评价。刚听到这个需求我大意了没有闪,以为很简单,没想到很难实现,很多需求果然不能想当然啊,接下来我们来看一下该功能实现的一些思路。


    窗口关闭


    要想实现该功能最简单的想法就是监听浏览器关闭事件,然后阻止默认事件,执行自定义的事件。整个思路核心就是监听事件,搜索一番果然有浏览器关闭触发的事件。


    事件



    • onunload:资源被卸载时触发,浏览器窗口关闭时卸载资源就会触发,我们监听一下该事件看能不能阻止窗口关闭。


        window.addEventListener('unload', function (e) {
    console.log(e);
    e.preventDefault()
    });

    打开页面再关闭,会发现控制台打印出了e然后就关闭了,看来在onunload事件中并不能阻止窗口关闭,得另找方法,刚好在onunload事件介绍中还链接了一个事件——beforeonunlaod。



    • beforeunload :当窗口关闭或刷新时触发,该事件在onunload之前触发。并且在该事件中可以弹出对话框,询问用户是否确认离开或者重新加载,这不是正是我们想要的效果。根据mdn上的介绍,要想出现弹出对话看需要用preventDefault()事件,并且为了兼容性我们最好再加上以下方法中的一个:

      1.将e.renturenValue赋一个字符串。

      2.事件函数返回一个字符串。
      接下来让我们试一试:


        window.addEventListener('beforeunload', function (e) {
    e.preventDefault()
    e.returnValue = ''
    });

    打开关闭未生效,再检查下代码没问题呀,这是因为浏览器本身安全机制导致的,在ie浏览器中没有任何限制,但是在chrome、edge等浏览器中用户必须在短时间操作过页面才能触发。打开页面点几个文字在关闭窗口,这次就能出现弹窗了。

    2(W_WV8AVWRT3(4R1HWBRR7.png

    当我们点击离开页面就会关闭,点击取消继续停留,上面提到过刷线也能触发,我们再点下刷新。

    T456)ZI7MJ2XK1X3M%BE7SN.png

    出现的提示有所改变,我们知道浏览器的刷新有好几种方式,我们可以都尝试一下:



    • ctrl+R:本身就是浏览器刷新按钮的快捷键,能够触发。

    • f5:能否触发。

    • 前进、后退:能够触发。

      这三种方式提示内容跟点击刷新按钮一样。回到我们的需求,虽然已经能够阻止窗口关闭,但是刷新依旧能阻止,我们需求是用户关闭,所以我们要区分用户操作是刷新还是关闭。


    区分


    要想区分就要找到以下两者之间的区别,两者都会执行onbeforeunload与onunload两个事件,不能直接通过某个事件区分。但是两个事件之间的时间差是不同的。刷新时两者时间差在10毫秒左右,而关闭时在3毫秒左右,判断以下时间差就能区分出来。


           var time = null;
    window.addEventListener('beforeunload', function (e) {
    time = new Date().getTime();
    });
    window.addEventListener('unload', function (e) {
    const nowTime = new Date().getTime();
    if (nowTime - time < 5) {
    console.log('窗口关闭');
    }
    });

    用此方法就能区分出来,但是此判断是在onunload事件中的,而窗口弹出是在beforeunlaod,这方法只适用于在关闭时执行某个函数,但不能满足我们的需求。除此之外还有一个问题就是刷新默认弹出对话框的内容是不能修改的,所以如果我们想要弹出自定义的对话框是不可能的。经过分析操作能够做到的就是,在用户刷新或关闭时出现系统自带对话框,同时在下方弹出自定义对话框,然后用户点击取消再去操作自定义对话框。


    总结


    总的来说要想拦截浏览器窗口关闭并且弹出自定义对话框,目前我还没有完美的实现方案,只能带有众多缺陷的去实现。如果我们只是想在关闭窗口前执行函数那就使用时间差区分即可。


    作者:躺平使者
    来源:juejin.cn/post/7281912738862481448
    收起阅读 »

    前端实现文件预览img、docx、xlsx、ppt、pdf、md、txt、audio、video

    web
    前言 最近有接到一个需求,要求前端支持上传制定后缀文件,且支持页面预览,上传简单,那么预览该怎么实现呢,尤其是不同类型的文件预览方案,那么下面就我这个需求的实现,分不同情况来讲解一下👇 具体的预览需求: 预览需要支持的文件类型有: png、jpg、jpeg...
    继续阅读 »

    前言



    最近有接到一个需求,要求前端支持上传制定后缀文件,且支持页面预览,上传简单,那么预览该怎么实现呢,尤其是不同类型的文件预览方案,那么下面就我这个需求的实现,分不同情况来讲解一下👇



    具体的预览需求:
    预览需要支持的文件类型有: png、jpg、jpeg、docx、xlsx、ppt、pdf、md、txt、audio、video,另外对于不同文档还需要有定位的功能。例如:pdf 定位到页码,txtmarkdown定位到文字并滚动到指定的位置,音视频定位到具体的时间等等。




    ⚠️ 补充: 我的需求是需要先将文件上传到后台,然后我拿到url地址去展示,对于markdowntxt的文件需要先用fetch获取,其他的展示则直接使用url链接就可以。


    不同文件的实现方式不同,下面分类讲解,总共分为以下几类:



    1. 自有标签文件:png、jpg、jpeg、audio、video

    2. 纯文字的文件: markdown & txt

    3. office 类型的文件: docx、xlsx、ppt

    4. embed 引入文件:pdf

    5. iframe:引入外部完整的网站




    自有标签文件:png、jpg、jpeg、audio、video



    对于图片、音视频的预览,直接使用对应的标签即可,如下:



    图片:png、jpg、jpeg


    示例代码:


     <img src={url} key={docId} alt={name} width="100%" />;

    预览效果如下:


    截屏2024-04-30 11.18.01.png


    音频:audio


    示例代码:


    <audio ref={audioRef} controls controlsList="nodownload" style={{ width: '100%' }}>
    <track kind="captions" />
    <source src={url} type="audio/mpeg" />
    </audio>

    预览效果如下:


    截屏2024-04-30 11.18.45.png


    视频:video


    示例代码:


    <video ref={videoRef} controls muted controlsList="nodownload" style={{ width: '100%' }}>
    <track kind="captions" />
    <source src={url} type="video/mp4" />
    </video>

    预览效果如下:


    截屏2024-05-13 18.21.13.png


    关于音视频的定位的完整代码:


    import React, { useRef, useEffect } from 'react';

    interface IProps {
    type: 'audio' | 'video';
    url: string;
    timeInSeconds: number;
    }

    function AudioAndVideo(props: IProps) {
    const { type, url, timeInSeconds } = props;
    const videoRef = useRef<HTMLVideoElement>(null);
    const audioRef = useRef<HTMLAudioElement>(null);

    useEffect(() => {
    // 音视频定位
    const secondsTime = timeInSeconds / 1000;
    if (type === 'audio' && audioRef.current) {
    audioRef.current.currentTime = secondsTime;
    }
    if (type === 'video' && videoRef.current) {
    videoRef.current.currentTime = secondsTime;
    }
    }, [type, timeInSeconds]);

    return (
    <div>
    {type === 'audio' ? (
    <audio ref={audioRef} controls controlsList="nodownload" style={{ width: '100%' }}>
    <track kind="captions" />
    <source src={url} type="audio/mpeg" />
    </audio>
    ) : (
    <video ref={videoRef} controls muted controlsList="nodownload" style={{ width: '100%' }}>
    <track kind="captions" />
    <source src={url} type="video/mp4" />
    </video>
    )}
    </div>
    );
    }

    export default AudioAndVideo;



    纯文字的文件: markdown & txt



    对于markdown、txt类型的文件,如果拿到的是文件的url的话,则无法直接显示,需要请求到内容,再进行展示。



    markdown 文件



    在展示markdown文件时,需要满足字体高亮、代码高亮、如果有字体高亮,需要滚动到字体所在位置、如果有外部链接,需要新开tab页面再打开。



    需要引入两个库:


    marked:它的作用是将markdown文本转换(解析)为HTML


    highlight: 它允许开发者在网页上高亮显示代码。


    字体高亮的代码实现:



    高亮的样式,可以在行间样式定义



      const highlightAndMarkFirst = (text: string, highlightText: string) => {
    let firstMatchDone = false;
    const regex = new RegExp(`(${highlightText})`, 'gi');
    return text.replace(regex, (match) => {
    if (!firstMatchDone) {
    firstMatchDone = true;
    return `<span id='first-match' style="color: red;">${match}</span>`;
    }
    return `<span style="color: red;">${match}</span>`;
    });
    };

    代码高亮的代码实现:



    需要借助hljs这个库进行转换



    marked.use({
    renderer: {
    code(code, infostring) {
    const validLang = !!(infostring && hljs.getLanguage(infostring));
    const highlighted = validLang
    ? hljs.highlight(code, { language: infostring, ignoreIllegals: true }).value
    : code;
    return `<pre><code class="hljs ${infostring}">${highlighted}</code></pre>`;
    }
    },
    });

    链接跳转新tab页的代码实现:


    marked.use({
    renderer: {
    // 链接跳转
    link(href, title, text) {
    const isExternal = !href.startsWith('/') && !href.startsWith('#');
    if (isExternal) {
    return `<a href="${href}" title="${title}" target="_blank" rel="noopener noreferrer">${text}</a>`;
    }
    return `<a href="${href}" title="${title}">${text}</a>`;
    },
    },
    });

    滚动到高亮的位置的代码实现:



    需要配合上面的代码高亮的方法



    const firstMatchElement = document.getElementById('first-match');
    if (firstMatchElement) {
    firstMatchElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
    }

    完整的代码如下:



    入参的docUrlmarkdown文件的线上url地址,searchText 是需要高亮的内容。



    import React, { useEffect, useState, useRef } from 'react';
    import { marked } from 'marked';
    import hljs from 'highlight.js';

    const preStyle = {
    width: '100%',
    maxHeight: '64vh',
    minHeight: '64vh',
    overflow: 'auto',
    };

    // Markdown展示组件
    function MarkdownViewer({ docUrl, searchText }: { docUrl: string; searchText: string }) {
    const [markdown, setMarkdown] = useState('');
    const markdownRef = useRef<HTMLDivElement | null>(null);

    const highlightAndMarkFirst = (text: string, highlightText: string) => {
    let firstMatchDone = false;
    const regex = new RegExp(`(${highlightText})`, 'gi');
    return text.replace(regex, (match) => {
    if (!firstMatchDone) {
    firstMatchDone = true;
    return `<span id='first-match' style="color: red;">${match}</span>`;
    }
    return `<span style="color: red;">${match}</span>`;
    });
    };

    useEffect(() => {
    // 如果没有搜索内容,直接加载原始Markdown文本
    fetch(docUrl)
    .then((response) => response.text())
    .then((text) => {
    const highlightedText = searchText ? highlightAndMarkFirst(text, searchText) : text;
    setMarkdown(highlightedText);
    })
    .catch((error) => console.error('加载Markdown文件失败:', error));
    }, [searchText, docUrl]);

    useEffect(() => {
    if (markdownRef.current) {
    // 支持代码高亮
    marked.use({
    renderer: {
    code(code, infostring) {
    const validLang = !!(infostring && hljs.getLanguage(infostring));
    const highlighted = validLang
    ? hljs.highlight(code, { language: infostring, ignoreIllegals: true }).value
    : code;
    return `<pre><code class="hljs ${infostring}">${highlighted}</code></pre>`;
    },
    // 链接跳转
    link(href, title, text) {
    const isExternal = !href.startsWith('/') && !href.startsWith('#');
    if (isExternal) {
    return `<a href="${href}" title="${title}" target="_blank" rel="noopener noreferrer">${text}</a>`;
    }
    return `<a href="${href}" title="${title}">${text}</a>`;
    },
    },
    });
    const htmlContent = marked.parse(markdown);
    markdownRef.current!.innerHTML = htmlContent as string;
    // 当markdown更新后,检查是否需要滚动到高亮位置
    const firstMatchElement = document.getElementById('first-match');
    if (firstMatchElement) {
    firstMatchElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
    }
    }
    }, [markdown]);

    return (
    <div style={preStyle}>
    <div ref={markdownRef} />
    </div>

    );
    }

    export default MarkdownViewer;

    预览效果如下:


    截屏2024-05-13 17.59.04.png


    txt 文件预览展示



    支持高亮和滚动到指定位置



    支持高亮的代码:


      function highlightText(text: string) {
    if (!searchText.trim()) return text;
    const regex = new RegExp(`(${searchText})`, 'gi');
    return text.replace(regex, `<span style="color: red">$1</span>`);
    }

    完整代码:


    import React, { useEffect, useState, useRef } from 'react';
    import { preStyle } from './config';

    function TextFileViewer({ docurl, searchText }: { docurl: string; searchText: string }) {
    const [paragraphs, setParagraphs] = useState<string[]>([]);
    const targetRef = useRef<HTMLDivElement | null>(null);

    function highlightText(text: string) {
    if (!searchText.trim()) return text;
    const regex = new RegExp(`(${searchText})`, 'gi');
    return text.replace(regex, `<span style="color: red">$1</span>`);
    }

    useEffect(() => {
    fetch(docurl)
    .then((response) => response.text())
    .then((text) => {
    const highlightedText = highlightText(text);
    const paras = highlightedText
    .split('\n')
    .map((para) => para.trim())
    .filter((para) => para);
    setParagraphs(paras);
    })
    .catch((error) => {
    console.error('加载文本文件出错:', error);
    });
    // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [docurl, searchText]);

    useEffect(() => {
    // 处理高亮段落的滚动逻辑
    const timer = setTimeout(() => {
    if (targetRef.current) {
    targetRef.current.scrollIntoView({ behavior: 'smooth', block: 'center' });
    }
    }, 100);

    return () => clearTimeout(timer);
    }, [paragraphs]);

    return (
    <div style={preStyle}>
    {paragraphs.map((para: string, index: number) => {
    const paraKey = para + index;

    // 确定这个段落是否包含高亮文本
    const isTarget = para.includes(`>${searchText}<`);
    return (
    <p key={paraKey} ref={isTarget && !targetRef.current ? targetRef : null}>
    <div dangerouslySetInnerHTML={{ __html: para }} />
    </p>
    );
    })}
    </div>

    );
    }

    export default TextFileViewer;

    预览效果如下:


    截屏2024-05-13 18.34.27.png




    office 类型的文件: docx、xlsx、ppt



    docx、xlsx、ppt 文件的预览,用的是office的线上预览链接 + 我们文件的线上url即可。




    关于定位:用这种方法我暂时尝试是无法定位页码的,所以定位的功能我采取的是后端将office 文件转成pdf,再进行定位,如果只是纯展示,忽略这个问题即可。



    示例代码:


    <iframe
    src={`https://view.officeapps.live.com/op/view.aspx?src=${url}`}
    width="100%"
    height="500px"
    frameBorder="0"
    ></iframe>

    预览效果如下:


    截屏2024-05-07 17.58.45.png




    embed 引入文件:pdf



    pdf文档预览时,可以采用embed的方式,这个httpsUrl就是你的pdf文档的链接地址



    示例代码:


     <embed src={`${httpsUrl}`} style={preStyle} key={`${httpsUrl}`} />;

    关于定位,其实是地址上拼接的页码sourcePage,如下:


     const httpsUrl = sourcePage
    ? `${doc.url}#page=${sourcePage}`
    : doc.url;

    <embed src={`${httpsUrl}`} style={preStyle} key={`${httpsUrl}`} />;


    预览效果如下:


    截屏2024-05-07 17.50.07.png




    iframe:引入外部完整的网站



    除了上面的各种文件,我们还需要预览一些外部的网址,那就要用到iframe的方式



    示例代码:


     <iframe
    title="网址"
    width="100%"
    height="100%"
    src={doc.url}
    allow="microphone;camera;midi;encrypted-media;"/>


    预览效果如下:


    截屏2024-05-07 17.51.26.png




    总结: 到这里我们支持的所有文件都讲述完了,有什么问题,欢迎评论区留言!


    作者:玖月晴空
    来源:juejin.cn/post/7366432628440924170
    收起阅读 »

    28个令人惊艳的JavaScript单行代码

    web
    JavaScript作为一种强大而灵活的脚本语言,充满了许多令人惊艳的特性。本文将带你探索28个令人惊艳的JavaScript单行代码,展示它们的神奇魅力。 1. 阶乘计算 使用递归函数计算给定数字的阶乘。 const factorial = n => ...
    继续阅读 »

    JavaScript作为一种强大而灵活的脚本语言,充满了许多令人惊艳的特性。本文将带你探索28个令人惊艳的JavaScript单行代码,展示它们的神奇魅力。


    1. 阶乘计算


    使用递归函数计算给定数字的阶乘。


    const factorial = n => n === 0 ? 1 : n * factorial(n - 1);
    console.log(factorial(5)); // 输出 120

    2. 判断一个变量是否为对象类型


    const isObject = variable === Object(variable);

    3. 数组去重


    利用Set数据结构的特性,去除数组中的重复元素。


    const uniqueArray = [...new Set(array)];

    4. 数组合并


    合并多个数组,创建一个新的数组。


    const mergedArray = [].concat(...arrays);

    5. 快速最大值和最小值


    获取数组中的最大值和最小值。


    const max = Math.max(...array);
    const min = Math.min(...array);

    6. 数组求和


    快速计算数组中所有元素的和。


    const sum = array.reduce((acc, cur) => acc + cur, 0);

    7. 获取随机整数


    生成一个指定范围内的随机整数。


    const randomInt = Math.floor(Math.random() * (max - min + 1)) + min;

    8. 反转字符串


    将字符串反转。


    const reversedString = string.split('').reverse().join('');

    9. 检查回文字符串


    判断一个字符串是否为回文字符串。


    const isPalindrome = string === string.split('').reverse().join('');

    10. 扁平化数组


    将多维数组转换为一维数组。


    const flattenedArray = array.flat(Infinity);

    11. 取随机数组元素


    从数组中随机取出一个元素。


    const randomElement = array[Math.floor(Math.random() * array.length)];

    12. 判断数组元素唯一


    检查数组中的元素是否唯一。


    const isUnique = array.length === new Set(array).size;

    13. 字符串压缩


    将字符串中重复的字符进行压缩。


    const compressedString = string.replace(/(.)\1+/g, match => match[0] + match.length);

    14. 生成斐波那契数列


    生成斐波那契数列的前n项。


    const fibonacci = Array(n).fill().map((_, i, arr) => i <= 1 ? i : arr[i - 1] + arr[i - 2]);

    15. 数组求交集


    获取多个数组的交集。


    const intersection = arrays.reduce((acc, cur) => acc.filter(value => cur.includes(value)));

    16. 验证邮箱格式


    检查字符串是否符合邮箱格式。


    const isValidEmail = /^[\w-]+(\.[\w-]+)*@([\w-]+\.)+[a-zA-Z]{2,7}$/.test(email);

    17. 数组去除假值


    移除数组中的所有假值,如falsenull0""undefined


    const truthyArray = array.filter(Boolean);

    18. 求阶乘


    计算一个数的阶乘。


    const factorial = n => n <= 1 ? 1 : n * factorial(n - 1);

    19. 判断质数


    检查一个数是否为质数。


    const isPrime = n => ![...Array(n).keys()].slice(2).some(i => n % i === 0);

    20. 检查对象是空对象


    判断对象是否为空对象。


    const isEmptyObject = Object.keys(object).length === 0 && object.constructor === Object;

    21. 判断回调函数为真


    检查数组中的每个元素是否满足特定条件。


    const allTrue = array.every(condition);

    22. 检查回调函数为假


    检查数组中是否有元素满足特定条件。


    const anyFalse = array.some(condition);

    23. 数组排序


    对数组进行排序。


    const sortedArray = array.sort((a, b) => a - b);

    24. 日期格式化


    将日期对象格式化为指定格式的字符串。


    const formattedDate = new Date().toISOString().slice(0, 10);

    25. 将字符串转为整数类型


    const intValue = +str;

    26. 计算数组中元素出现的次数


    统计数组中各元素的出现次数。


    const countOccurrences = array.reduce((acc, cur) => (acc[cur] ? acc[cur]++ : acc[cur] = 1, acc), {});

    27. 交换两个变量的值


    [a, b] = [b, a];

    28. 利用逗号运算符分隔多个表达式


    const result = (expression1, expression2, ..., expressionN);

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

    如何快速实现多行文本擦除效果

    web
    今天来实现一个多行文本擦除的效果,有种经典咏流传节目中表演开始前阅读诗句的一些既视感,在工作中其实也遇到过这样的需求当时是用的其他方法来实现的,现在发现了更简单的一种方法并且里面也涵盖了不少的知识点。 以上就是最终要实现的效果,比较敏感的同学呢应该能看到文本...
    继续阅读 »

    今天来实现一个多行文本擦除的效果,有种经典咏流传节目中表演开始前阅读诗句的一些既视感,在工作中其实也遇到过这样的需求当时是用的其他方法来实现的,现在发现了更简单的一种方法并且里面也涵盖了不少的知识点。


    img1.gif


    以上就是最终要实现的效果,比较敏感的同学呢应该能看到文本是由歌词组成的哈哈,没错今天是我偶像发新歌的一天,就用歌词来致敬一下吧!


    思路


    首先先来捋一下思路,乍一看效果好像只有一段文本,但其实是由两段相同文本组成的。



    1. 两段相同文本组成,这是为了让它们实现重合,第二段文本会覆盖在第一段文本上。

    2. 修改第二段文本背景色为渐变色。

    3. 最后对渐变颜色的背景色添加动画效果。


    先来搭建一下结构部分:


    <body>
    <div class="container">
    <p>
    失去你以来 万物在摇摆 你指的山海 像玩具一块一块 我是你缔造又提防的AI 如果我存在 是某种伤害
    不被你所爱 也不能具象出来 我想拥有你说失去过谁的 那种痛感 失去你以来 万物在摇摆 你指的山海 像玩具一块一块我是你缔造又提防的AI 如果我存在 只对你无害 想做你所爱 再造你要的时代 执行你最初设计我的大概
    成为主宰 失去你以来 万物在摇摆 你指的山海 像玩具一块一块 也许我本来 就是种伤害 我终于明白 我根本就不存在 谁不在造物主设置的循环 活去死来
    </p>
    <p class="eraser">
    <span class="text">
    失去你以来 万物在摇摆 你指的山海 像玩具一块一块 我是你缔造又提防的AI 如果我存在 是某种伤害
    不被你所爱 也不能具象出来 我想拥有你说失去过谁的 那种痛感 失去你以来 万物在摇摆 你指的山海 像玩具一块一块我是你缔造又提防的AI 如果我存在 只对你无害 想做你所爱 再造你要的时代
    执行你最初设计我的大概
    成为主宰 失去你以来 万物在摇摆 你指的山海 像玩具一块一块 也许我本来 就是种伤害 我终于明白 我根本就不存在 谁不在造物主设置的循环 活去死来
    </span>
    </p>
    </div>
    </body>

    代码中两段文本都是由p标签包裹,第二段中加入了一个span标签是因为后面修改背景色的时候凸显出行的效果,这个下面加上样式后就看到了。


    添加样式:


    * {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
    }

    body {
    background: #000;
    color: #fff;
    }

    .container {
    width: 60%;
    text-indent: 20px;
    line-height: 2;
    font-size: 18px;
    margin: 30px auto;
    }

    img2.png


    现在只需要给第二段增加一个定位效果即可实现文本的覆盖:


    * {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
    }

    body {
    background: #000;
    color: #fff;
    }

    .container {
    width: 60%;
    /* 直接加在父元素中即可对所有块级元素的子元素进行首行缩进 */
    text-indent: 20px;
    line-height: 2;
    font-size: 18px;
    margin: 30px auto;
    position: relative;
    }

    .eraser {
    position: absolute;
    /* 这里等同于top:0 right:0 bottom:0 left:0 */
    inset: 0;
    /*
    这里解释一下inset属性,inset属性用作定位元素的top、right、bottom 、left这些属性的简写
    依照的也是上右下左的顺序。
    例如:inset:1px 2px 等同于 top:1px right:2px bottom:1px left:2px
    */

    }

    image.png


    那接下来就应该修改背景颜色了。


    以上重复代码省略......

    .text {
    background: #fff;
    }

    这时候给span标签加上背景颜色后会看到:


    image.png


    而不是这样的效果,这就是为什么需要加一个span标签的原因了。


    image.png


    以上重复代码省略......

    .text {
    background: linear-gradient(to right, #0000 10%, #000 10%);
    color:transparent;
    }

    image.png


    下面要调整的就是将渐变里面的百分比变为动态的,我们可以声明一个变量:


    以上重复代码省略......

    .text {
    --p:0%;
    background: linear-gradient(to right, #0000 var(--p), #000 calc( var(--p) + 30px)); // 加上30px显示一个默认的渐变区域
    color:transparent;
    }

    image.png


    下面就该加上动画效果了,在设置动画时改变--p变量的值为100%


    以上重复代码省略......

    .text {
    --p:0%;
    background: linear-gradient(to right, #0000 var(--p), #000 calc( var(--p) + 30px));
    color:transparent;
    animation: erase 8s linear;
    }

    @keyframes erase{
    to{
    --p:100%;
    }
    }

    但是这样写完之后发现并没有出现动画的效果,这是因为css动画中只有数值类的css属性才会生效,这里已经是一个数值了但--p还不是一个属性,所以我们要把他变成一个css属性,可以利用@property规则来帮助我们生成一个-xxx的自定义,它的结构:


    @property 属性名称 {
    syntax: '<类型>'; // 必须
    initial-value: 默认值; // 必须
    inherits: false; // 是否可继承 非必须
    }

    以上重复代码省略......

    .text {
    --p:0%;
    background: linear-gradient(to right, #0000 var(--p), #000 calc( var(--p) + 30px));
    color:transparent;
    animation: erase 8s linear;
    }

    @property --p {
    syntax: '<percentage>';
    initial-value: 0%;
    inherits: false;
    }

    @keyframes erase{
    to{
    --p:100%;
    }
    }

    到此为止也就实现开头的效果了!!!


    作者:孤独的根号_
    来源:juejin.cn/post/7333761832472838144
    收起阅读 »

    超级离谱的前端需求:搜索图片里的文字!!难倒我了!

    web
    前言 大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心~ 背景 是这样的,我们公司有一个平台,这个平台上面有一个页面,是一个我们公司内部存放一些字幕图片的,图片很多,差不多每一页有100张的样子,类似于下面这样的图片 ...
    继续阅读 »

    前言


    大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心~


    背景


    是这样的,我们公司有一个平台,这个平台上面有一个页面,是一个我们公司内部存放一些字幕图片的,图片很多,差不多每一页有100张的样子,类似于下面这样的图片



    前几天上面大佬们说想要更加方便快捷地找到某一张图片,怎么个快捷法呢?就是通过搜索文字,能搜索到包含这些文字的图片。。。我一想,这需求简直逆天啊!!!!平时只做过搜索文字的,没做过根据文字搜索出图片的。。。。



    思路


    其实思路很清晰,分析出每一张图片上的文字,并存在对象的keyword中,搜搜的时候去过滤出keyword包含搜索文字的图片即可。


    但是难就难在,我要怎么分析出图片上的文字并存起来呢?


    tesseract.js


    于是我就去网上找找有哪些库可以实现这个功能,你还真别说,还真有!!这个库就是tesseract.js



    tesseract.js 是一个可以分析出图片上文字的一个库,我们通过一个小例子来看看他的使用方式


    首先需要安装这个库


    npm i tesseract.js

    接着引入并使用它解析图片文字,它识别后会返回一个 Promise,成功的话会走 then



    可以看出他直接能把图片上的结果解析出来!!!真的牛逼!!!有了这个,那我轻轻松松就可以完成上面交代的任务了!!!



    实现功能


    我们需要解析每一张图片的文字,并存入 keyword属性中,以供过滤筛选



    可以看到每一张图片都解析得到keyword



    那么搜索效果自然可以完成



    搜索中文呢?


    上面只能解析英文,可以看到有 eng 这个参数,那怎么才能解析中文呢?只需要改成chi_sim即可




    如果你想要中文和英文一起解析,可以这么写eng+chi_sim





    作者:Sunshine_Lin
    来源:juejin.cn/post/7355554711167369268
    收起阅读 »

    设计呀,你是真会给前端找事呀!!!

    web
    背景 设计:我想要的你听明白了吗,你做出来的和我想要的差距很大,你怎么没有一点审美(你个臭男人,你怎么不按我画的做)! 我:啊?这样自适应不是很好吗,适配了大部分机型呀,而且不会有啥显示的兼容性,避免不必要的客户咨询和客户投诉。 设计: 你上一家公司就是因为...
    继续阅读 »

    背景



    • 设计:我想要的你听明白了吗,你做出来的和我想要的差距很大,你怎么没有一点审美(你个臭男人,你怎么不按我画的做)!

    • :啊?这样自适应不是很好吗,适配了大部分机型呀,而且不会有啥显示的兼容性,避免不必要的客户咨询和客户投诉。

    • 设计: 你上一家公司就是因为有你这样的优秀员工才倒闭的吧?!

    • :啊?ntm和产品是一家的是吗?





    我该如何应对


    先看我实现的


    b0nh2-9h1qy.gif


    在看看设计想要的


    9e2b0572-aff4-4644-9eeb-33a9ea76265c.gif
    总结一下:



    • 1.一个的时候宽度固定,不管屏幕多大都占屏幕的一半。

    • 2.俩个的时候,各占屏幕的一半,当屏幕过小的时候两个并排展示换行。

    • 3.三个的时候,上面俩,下面一个,且宽度要一样。

    • 4.大于三个的时候,以此类推。



    有句话叫做什么,乍一看很合理,细想一下,这不是扯淡么。



    所以我又和设计进行了亲切的对话



    • :两个的时候你能考虑到小屏的问题,那一个和三个的时候你为啥不考虑,难道你脑袋有泡,在想一个和三个的时候泡刚好堵住了?

    • 设计: 你天天屌不拉几的,我就要这样,这样好看,你懂个毛的设计,你知道什么是美感和人体工学设计,视觉效果拉满吗?

    • :啊?我的姑奶奶耶,你是不是和产品一个学校毕业的,咋就一根筋呢?

    • 产品:ui说的对,我听ui的。汪汪汪(🐶)


    当时那个画面就像是,就像是:





    而我就像是
    1b761c13b4439463a77ac8abf563677d.png


    那咋办,写呗,我能咋办?



    我月黑风夜,
    黑衣傍我身,
    潜入尔等房,
    打你小屁屁?



    代码实现


       class={[
    'group-even-number' : this.evenNumber,
    'group-odd-number' : this.oddNumber,
    'themeSelectBtnBg'
    ]}
    value={this.currentValue}
    onInput={(value: any) => {
    this.click(value)
    }}
    >
    ...


       .themeSelectBtnBg {
    display: flex;
    &:nth-child(2n - 1) {
    margin-left: 0;
    margin-right: 10px;
    }
    &:nth-child(2n) {
    margin-left: 0;
    margin-right: 0;
    }

    }
    // 奇数的情况,宽度动态计算,将元素挤下去
    .group-odd-number {
    // 需要减去padding的宽度
    width: calc(50% - 7.5px);
    }

    .group-even-number {
    justify-content: space-between;
    @media screen and (max-width:360px) {
    justify-content: unset;
    margin-right: unset;
    flex: 1;
    flex-wrap: wrap;
    }
    }

    行吧,咱就这样吧




    作者:顾昂_
    来源:juejin.cn/post/7304268647101939731
    收起阅读 »

    一个排查了一天的BUG,你在摸鱼🐟吧!

    web
    站会 在一次日常站会上,组员们轮流分享昨天的工作进展。一个组员提到:“昨天我整天都在排查一个BUG,今天还得继续。” 出于好奇,我问:“是什么BUG让你排查了这么久还没解决呢?” 他解释说:“是关于一个数据选择弹窗的问题。这个弹窗用表格展示数据,并且表格具有选...
    继续阅读 »

    站会


    在一次日常站会上,组员们轮流分享昨天的工作进展。一个组员提到:“昨天我整天都在排查一个BUG,今天还得继续。”


    出于好奇,我问:“是什么BUG让你排查了这么久还没解决呢?”


    他解释说:“是关于一个数据选择弹窗的问题。这个弹窗用表格展示数据,并且表格具有选择功能。问题在于,编辑这个弹窗时,表格中原本应该显示为已选状态的数据并没有正确显示已选状态。”


    我猜测道:“是不是因为表格中数据的主键ID是大数值导致的?”


    他回答说:“大数值?我不太确定。”


    我有些质疑地问:“那你昨天都是怎么排查的?需要花一整天的时间,难道是在摸鱼吗?”


    “没有摸鱼,只是这个BUG真得有点难搞,那个什么是大数值?”


    “行吧,姑且信你,我待会给你看看。”


    排查


    表格使用的是 Ant Design 4.0 提供的 Table 组件。我检查了组件的 rowKey 属性配置,如下所示:


    <Table rowKey={record => record.obj_id}></Table>

    这表明表格行的 key 是通过数据中的 obj_id 字段来指定的。随后,我进一步查看了服务端返回的数据。


    image.png

    可以看到一条数据中的 obj_id 字段值为 "898186844400803840",这是一个18位的数值。



    在ES6(ECMAScript 2015)之前,JavaScript没有专门的整数类型,所有的数字都被表示为双精度64位浮点数(遵循IEEE 754标准)。这意味着在这种情况下,JavaScript能够安全地表示的整数范围是从253+1-2^{53} + 125312^{53} - 1(即-9,007,199,254,740,991到9,007,199,254,740,991)。可以简单地认为超过16位的数值就是大数值。



    JavaScript中很多操作处理大数值时会导致大数值失去精度。比如 Number("898186844400803840")


    image.png


    可以看到 "898186844400803840""898186844400803800" 的区别在第16位后,从 40 变成 00 这就是大数值失去精度的表现。


    在看一下表格的数据展示,如下图所示:


    image.png


    可以确定的是,从服务端返回的数据到在表格中的渲染过程是没有问题的。那么,可能出现问题的地方还有两个:一是在选择数据后,数据被传递到父组件的过程中;二是父组件将已选数据发送回选择数据组件的过程中。


    定位


    我检查了他将数据传递给父组件的逻辑代码,发现了一个可疑点。


    image.png

    在上述代码中,JSON.parse 被用来转换数据中的每个值。在这个转换过程中,如果 item[key] 是以字符串形式出现的数值,并且这个字符串能够被 JSON.parse() 解析为 JSON 中的数值类型,那么 JSON.parse() 将会把它转换为 JavaScript 的 Number 类型。


    这种转换过程中可能会出现精度丢失的问题。因为一旦字符串表示的数值的位数超过16位后,在转换为 Number 类型时就无法保证其精度完整无损。


    解决


    我们通过正则表达式排除了这种情况,如下所示:


    newItem[key] = typeof item[key] === 'string' && /^\d{16,}$/.test(item[key]) ? 
    item[key] :
    JSON.parse(item[key]);

    经过修改并重新验证,问题得到了解决,数据选择弹窗现在可以正确展示已选择状态。


    image.png


    反思


    这个表面上不起眼的BUG为何花费了如此长的时间来排查?除了对大数值的概念不甚了解外,还有一个关键原因是对JavaScript中可能导致大数值失去精度的操作缺乏深入理解。


    大数值通常由两种表示方式,一个是用数值类型表示,一个是字符串类型表示。


    如果用数值类型表示一个大数值,而且你不能直接修改源代码或源数据,这种情况比较棘手,因为一旦 JavaScript 解析器处理这个数值,它可能已经失去了精度。


    这种情况通常发生在你从某个源(比如一个API或者外部数据文件)接收到一个数值类型的大数值,如果数据源头不能修改,只能使用第三方库lossless-json、json-bigint来解决。


    如果用字符串类型表示一个大数值,在JS中只要有把其转成Number类型的值就会失去精度,不管是显式转换还是隐式转换。


    显式转换,比如 Number()parseInt()parseFloat()Math.floorMath.ceilMath.round等等。


    隐式转换,比如除了加法外的算术运算符、JSON.parseswitch 语句、sort的回调函数等等。


    作者:前端大骆
    来源:juejin.cn/post/7348712837849284644
    收起阅读 »

    Node拒绝当咸鱼,Node 22大进步

    web
    这几年,deno和bun风头正盛,大有你方唱罢我登场的态势,deno和bun的每一次更新版本,Node都会被拿来比较,比较结果总是Node落后了。 这种比较是不是非常熟悉,就像卖手机的跟iPhone比,卖汽车的跟特斯拉比,比较的时候有时候还得来个「比一分钱硬币...
    继续阅读 »

    这几年,deno和bun风头正盛,大有你方唱罢我登场的态势,deno和bun的每一次更新版本,Node都会被拿来比较,比较结果总是Node落后了。


    这种比较是不是非常熟悉,就像卖手机的跟iPhone比,卖汽车的跟特斯拉比,比较的时候有时候还得来个「比一分钱硬币还薄」的套路。


    1.png


    Node虽然没有落后了,但是确实有点压力了,所以20和22版本都大跨步前进,拒绝当咸鱼了。


    因为Node官网对22版本特性的介绍太过简单,所以我决定来一篇详细介绍新特性的文章,让学习Node的朋友们知道,Node现在在第几层。


    首先我把新特性分为两类,分别是:开发者可能直接用到的特性、开发者相对无感知的底层更新。本文重点介绍前者,简单介绍后者。先来一个概览:


    开发者可能直接用到的特性:



    1. 支持通过 require() 引入ESM

    2. 运行 package.json 中的脚本

    3. 监视模式(--watch)稳定化

    4. 内置 WebSocket 客户端

    5. 增加流的默认高水位线

    6. 文件模式匹配功能


    开发者相对无感知的底层更新:



    1. V8 引擎升级至 12.4 版本

    2. Maglev 编译器默认启用

    3. 改进 AbortSignal 的创建性能


    接下来开始介绍。


    支持通过 require() 导入 ESM


    以前,我们认为 CommonJS 与 ESM 是分离的。


    例如,在 CommonJS里,我们用并使用 module.exports 导出模块,用 require() 导入模块:


    // CommonJS

    // math.js
    function add(a, b) {
    return a + b;
    }
    module.exports.add = add;

    // useMath.js
    const math = require('./math');
    console.log(math.add(2, 3));

    在 ECMAScript Modules (ESM) **** 里,我们使用 export 导出模块,用 import 导入模块:


    // ESM

    // math.mjs
    export function add(a, b) {
    return a + b;
    }

    // useMath.js
    import { add } from './math.mjs';
    console.log(add(2, 3));

    Node 22 支持新的方式——用 require() 导入 ESM:


    // Node 22

    // math.mjs
    export function add(a, b) {
    return a + b;
    }

    // useMath.js
    const { add } = require('./mathModule.mjs');
    console.log(add(2, 3));

    这么设计的原因是为了给大型项目和遗留系统提供一个平滑过渡的方案,因为这类项目难以快速全部迁移到 ESM,通过允许 require() 导入 ESM,开发者就可以逐个模块迁移,而不是一次性对整个项目进行修改。


    目前这种写法还是实验性功能,所以使用是有“门槛”的:



    • 启动命令需要添加 -experimental-require-module 参数,如:node --experimental-require-module app.js

    • 模块标记:确保 ESM 模块通过 package.json 中的 "type": "module" 或文件扩展名是 .mjs

    • 完全同步:只有完全同步的ESM才能被 require() 导入,任何含有顶级 await 的ESM都不能使用这种方式加载。


    运行package.json中的脚本


    假设我们的 package.json 里有一个脚本:


    "scripts": {
    "test": "jest"
    }

    在此之前,我们必须依赖 npm 或者 yanr 这样的包管理器来执行命令,比如:npm run test


    Node 22 添加了一个新命令行标志 --run,允许直接从命令行执行 package.json 中定义的脚本,可以直接使用 node --run test 这样的命令来运行脚本。


    刚开始我还疑惑这是不是脱裤子放屁的行为,因为有 node 的地方一般都有 npm,我要这 node —run 有何用?


    后来思考了一下,主要原因应该还是统一运行环境和提升性能。不同的包管理器在处理脚本时可能会有微小的差异,Node 提供一个标准化的方式执行脚本,有助于统一这些行为;而且直接使用 node 执行脚本要比通过 npm 执行脚本更快,因为绕过了 npm 这个中间层。


    监视模式(--watch)稳定化


    在 19 版本里,Node 引入了 —watch 指令,用于监视文件系统的变动,并自动重启。22 版本开始,这个指令成为稳定功能了。


    要启用监视模式,只需要在启动 Node 应用时加上 --watch ****参数。例如:


    node --watch app.js

    正在用 nodemon 做自动重启的朋友们可以正式转战 --watch 了~


    内置 WebSocket 客户端


    以前,要用 Node 开发一个 socket 服务,必须使用 ws、socket.io 这样的第三方库来实现。第三方库虽然稳如老狗帮助开发者许多年,但是终究是有点不方便。


    Node 22 正式内置了 WebSocket,并且属于稳定功能,不再需要 -experimental-websocket 来启用了。


    除此之外,WebScoket 的实现还遵循了浏览器中 WebSocket API 的标准,这意味着在 Node 中使用 WebSocket 的方式将与在 JavaScript 中使用 WebSocket 的方式非常相似,有助于减少学习成本并提高代码的一致性。


    用法示例:


    const socket = new WebSocket("ws://localhost:8080");

    socket.addEventListener("open", (event) => {
    socket.send("Hello Server!");
    });

    增加流(streams)的默认高水位线(High Water Mark)


    streams 在 Node 中有举足轻重的作用,读写数据都得要 streams 来完成。而 streams 可以设置 highWaterMark 参数,用于表示缓冲区的大小。highWaterMark 越大,缓冲区越大,占用内存越多,I/O 操作就减少,highWaterMark 越小,其他信息也对应相反。


    用法如下:


    const fs = require('fs');

    const readStream = fs.createReadStream('example-large-file.txt', {
    highWaterMark: 1024 * 1024 // 设置高水位线为1MB
    });

    readStream.on('data', (chunk) => {
    console.log(`Received chunk of size: ${chunk.length}`);
    });

    readStream.on('end', () => {
    console.log('End of file has been reached.');
    });

    虽然 highWaterMark 是可配置的,但通常情况下,我们是使用默认值。在以前的版本里,highWaterMark 的默认值是 16k,Node 22 版本开始,默认值被提升到 64k 了。


    文件模式匹配——glob 和 globSync


    Node 22 版本在 fs 模块中新增了 globglobSync 函数,它们用于根据指定模式匹配文件路径。


    文件模式匹配允许开发者定义一个匹配模式,以找出符合特定规则的文件路径集合。模式定义通常包括通配符,如 *(匹配任何字符)和 ?(匹配单个字符),以及其他特定的模式字符。


    glob 函数(异步)


    glob 函数是一个异步的函数,它不会阻塞 Node.js 的事件循环。这意味着它在搜索文件时不会停止其他代码的执行。glob 函数的基本用法如下:


    const { glob } = require('fs');

    glob('**/*.js', (err, files) => {
    if (err) {
    throw err;
    }
    console.log(files); // 输出所有匹配的.js文件路径
    });

    在这个示例中,glob 函数用来查找所有子目录中以 .js 结尾的文件。它接受两个参数:



    • 第一个参数是一个字符串,表示文件匹配模式。

    • 第二个参数是一个回调函数,当文件搜索完成后,这个函数会被调用。如果搜索成功,err 将为 null,而 files 将包含一个包含所有匹配文件路径的数组。


    globSync 函数(同步)


    globSyncglob 的同步版本,它会阻塞事件循环,直到所有匹配的文件都被找到。这使得代码更简单,但在处理大量文件或在需要高响应性的应用中可能会导致性能问题。其基本用法如下:


    const { globSync } = require('fs');

    const files = globSync('**/*.js');
    console.log(files); // 同样输出所有匹配的.js文件路径

    这个函数直接返回匹配的文件数组,适用于脚本和简单的应用,其中执行速度不是主要关注点。


    使用场景


    这两个函数适用于:



    • 自动化构建过程,如自动寻找和处理项目中的 JavaScript 文件。

    • 开发工具和脚本,需要对项目目录中的文件进行批量操作。

    • 任何需要从大量文件中快速筛选出符合特定模式的文件集的应用。


    V8 引擎升级至 12.4 版本


    从这一节开始,我们了解一下开发者相对无感知的底层更新,第一个就是 V8 引擎升级到 12.4 版本了,有了以下特性升级:



    • WebAssembly 垃圾回收:这一特性将改善 WebAssembly 在内存管理方面的能力。

    • Array.fromAsync:这个新方法允许从异步迭代器创建数组。

    • Set 方法和迭代器帮助程序:提供了更多内建的Set操作和迭代器操作的方法,增强了数据结构的操作性和灵活性。


    Maglev 编译器默认启用


    Maglev 是 V8 的新编译器,现在在支持的架构上默认启用。它主要针对短生命周期的命令行程序(CLI程序)性能进行优化,通过改进JIT(即时编译)的效率来提升性能。这对开发者编写的工具和脚本将带来明显的速度提升。


    改进AbortSignal的创建性能


    在这次更新中,Node 提高了 AbortSignal 实例的创建效率。AbortSignal 是用于中断正在进行的操作(如网络请求或任何长时间运行的异步任务)的一种机制。通过提升这一过程的效率,可以加快任何依赖这一功能的应用,如使用 fetch 进行HTTP请求或在测试运行器中处理中断的场景。


    AbortSignal 的工作方式是通过 AbortController 实例来管理。AbortController 提供一个 signal 属性和一个 abort() 方法。signal 属性返回一个 AbortSignal 对象,可以传递给任何接受 AbortSignal 的API(如fetch)来监听取消事件。当调用abort()方法时,与该控制器关联的所有操作将被取消。


    const controller = new AbortController();
    const signal = controller.signal;

    fetch(url, { signal })
    .then(response => response.json())
    .catch(err => {
    if (err.name === 'AbortError') {
    console.log('Fetch aborted');
    } else {
    console.error('Fetch error:', err);
    }
    });

    // 取消请求
    controller.abort();

    总结


    最后,我只替 Node 说一句:Node 没有这么容易被 deno 和 bun 打败~


    3.jpeg


    关于我


    全栈工程师,Next.js 开源手艺人,AI降临派。


    今年致力于 Next.js 和 Node.js 领域的开源项目开发和知识分享。




    作者:程普
    来源:juejin.cn/post/7366185272768036883
    收起阅读 »

    Vue3 新项目,没必要再用 Pinia 了!

    web
    最近弄了一个新的 Vue3 项目,页面不多,其中有三四个页面需要共享状态,我几乎条件反射般地安装了 Pinia 来做状态管理。后来一想,我只需要一个仓库,存放几个状态而已,有必要单独接一套 Pinia 吗?其实不需要,我差点忘记了 Vue3...
    继续阅读 »

    最近弄了一个新的 Vue3 项目,页面不多,其中有三四个页面需要共享状态,我几乎条件反射般地安装了 Pinia 来做状态管理。

    后来一想,我只需要一个仓库,存放几个状态而已,有必要单独接一套 Pinia 吗?

    其实不需要,我差点忘记了 Vue3 的一个重要特性,那就是 组合式函数

    组合式 API 大家都知道,组合式函数可能大家没有特别留意。但是它功能强大,足矣实现全局状态管理。

    组合式函数

    什么是组合式函数?以下是官网介绍:

    在 Vue 应用的概念中,“组合式函数”(Composables) 是一个利用 Vue 的组合式 API 来封装和复用有状态逻辑的函数。

    从这段介绍中可以看出,组合式函数要满足两个关键点:

    1. 组合式 API。
    2. 有状态逻辑的函数。

    在 Vue 组件中,状态通常定义在组件内部。比如典型的选项式 API,状态定义在组件的 data() 方法下,因此这个状态只能在组件内使用。

    Vue3 出现之后,有了组合式 API。但对于大部分人来说,只是定义状态的方式从 data()变成了 ref(),貌似没有多大的区别。

    实际上,区别大了去了。

    组合式 API 提供的 ref() 等方法,不是只可以在 Vue 组件内使用,而是在任意 JS 文件中都可以使用。

    这就意味着,组合式 API 可以将 组件与状态分离,状态可以定义在组件之外,并在组件中使用。当我们使用组合式 API 定义了一个有状态的函数,这就是组合式函数。

    因此,组合式函数,完全可以实现全局状态管理。

    举个例子:假设将用户信息状态定义在一个组合式函数中,方法如下:

    // user.js
    import { ref } from 'vue'

    export function useUinfo() {
    // 用户信息
    const user_info = ref(null)
    // 修改信息
    const setUserInfo = (data) => {
    user_info.value = data
    }
    return { user_info, setUserInfo }
    }

    代码中的 useUinfo() 就是一个组合式函数,里面使用 ref() 定义了状态,并将状态和方法抛出。

    在 Vue3 组件之中,我们就可以导入并使用这个状态:


    仔细看组合式函数的使用方法,像不像 React 中的 Hook?完全可以将它看作一个 Hook。

    在多个组件中使用上述方法导入状态,跨组件的状态管理方式也就实现了。

    模块化的使用方法

    组合式函数在多个组件中调用,可能会出现重复创建状态的问题。其实我们可以用模块化的方法,更简单。

    将上方 user.js 文件中的组合式函数去掉,改造如下:

    import { ref } from 'vue'

    // 用户信息
    export const user_info = ref(null)
    // 修改信息
    export const setUserInfo = (data) => {
    user_info.value = data
    }

    这样在组件中使用时,直接导入即可:


    经过测试,这种方式是可以的。

    使用模块化的方法,也就是一个文件定义一组状态,可以看作是 Pinia 的仓库。这样状态模块化的问题也解决了。

    Pinia 中最常用的功能还有 getters,基于某个状态动态计算的另一个状态。在组合式函数中用计算属性完全可以实现。

    import { ref, computed } from 'vue'

    export const num1 = ref(3)

    export const num2 = computed(()=> {
    return num1 * num1
    }

    所以思考一下,对于使用 Vue3 组合式 API 开发的项目,是不是完全可以用组合式函数来替代状态管理(Pinia,Vuex)呢?

    当然,以上方案仅适用于组合式 API 开发的普通项目。对于选项式 API 开发的项目,或者需要 SSR,还是乖乖用 Pinia 吧 ~

    最重要的是!如果面试官问你:除了 Pinia 和 Vuex 还有没有别的状态管理方案?

    你可别说不知道,记住这几个字:组合式函数!


    作者:杨成功
    来源:juejin.cn/post/7348680291937435682
    收起阅读 »

    不容错过的秘籍:JavaScript数组的创建和使用详解

    在编程的世界里,数据是构建一切的基础。而在JavaScript中,有一种特殊且强大的数据结构,它就是——数组。今天,我们就来一起探索数组的奥秘,从创建到使用,一步步掌握这个重要的工具。一、什么是数组数组(Array)是一种按顺序存储多个值的数据结构。你可以把它...
    继续阅读 »

    在编程的世界里,数据是构建一切的基础。而在JavaScript中,有一种特殊且强大的数据结构,它就是——数组。

    今天,我们就来一起探索数组的奥秘,从创建到使用,一步步掌握这个重要的工具。

    一、什么是数组

    数组(Array)是一种按顺序存储多个值的数据结构。你可以把它想象成一个盒子,这个盒子可以存放多个物品,而且每个物品都有一个编号,我们可以通过这个编号来找到或者修改这个物品。

    在JavaScript中,数组是一种特殊的对象,用于存储和操作多个值。与其他编程语言不同,JavaScript的数组可以同时存储不同类型的值,并且长度是动态的,可以根据需要随时添加或删除元素。

    Description

    JavaScript数组使用方括号([])来表示,其中的每个元素用逗号分隔。例如,以下是一个包含不同类型元素的数组的示例:

    var myArray = [1, "two", true, [3, 4, 5]];

    数组中的元素可以通过索引来访问和修改,索引从0开始。例如,要访问数组中的第一个元素,可以使用以下代码:

    var firstElement = myArray[0];

    JavaScript也提供了一些内置方法来操作数组,如push()、pop()、shift()、unshift()等,用于添加、删除和修改数组中的元素。

    二、数组的作用

    数组在编程中扮演着非常重要的角色。它可以帮助我们:

    • 存储多个值:我们可以在一个变量中存储多个值,而不需要为每个值创建单独的变量。

    • 操作数据:我们可以对数组中的元素进行添加、删除、修改和查找等操作。

    • 实现各种算法:通过数组,我们可以实现排序、搜索等常见算法。

    • 循环遍历:数组的元素是有序的,可以使用循环结构遍历数组的每个元素,从而对每个元素进行相同或类似的操作。这在处理大量数据时非常有用。

    三、创建数组的方法

    在JavaScript中,有多种方法可以创建数组,下面列出常见的三种:

    1)字面量方式:

    这是最常见的创建数组的方式,只需要在一对方括号[]中放入元素即可,如

    var arr = [];

    2)使用Array构造函数:

    通过new Array()也可以创建数组,如

    var arr = new Array();

    3)使用Array.of()方法:

    这个方法可以创建一个具有相同元素的新数组实例,如

    var arr = Array.of(1, 2, 3);

    四、使用数组的方法

    创建了数组后,我们就可以对它进行各种操作了:

    1、访问和修改数组元素

    要访问和修改数组元素,需要使用数组的索引。数组的索引从0开始,依次递增。要访问数组元素,可以使用以下语法:

    console.log(arr[0]); // 输出第一个元素
    arr[1] = 4; // 修改第二个元素的值

    2、向数组末尾添加元素

    要向数组的末尾添加一个元素,可以使用数组的push()方法。该方法 会在数组的末尾添加指定的元素。以下是使用push()方法向数组末尾添加元素的示例:

    arr.push(5);

    3、从数组末尾移除元素

    要从数组的末尾移除一个元素,可以使用数组的pop()方法。该方法 会移除并返回数组中的最后一个元素。以下是使用pop()方法从数组末尾移除元素的示例:

    arr.pop();

    4、从数组末尾移除元素
    要从数组的末尾移除一个元素,可以使用数组的unshift()方法。该方法 会移除并返回数组中的最后一个元素。以下是使用unshift()方法从数组末尾移除元素的示例:

    arr.unshift(0);

    5、从数组开头移除元素
    要从数组的开头移除一个元素,可以使用数组的shift()方法,并将索引值设置为0。该方法 会移除并返回数组中的第一个元素。以下是使用shift()方法从数组开头移除元素的示例:

    arr.shift();

    6、获取数组的长度
    要获取数组的长度,可以使用内置函数length()。length()函数返回数组中元素的个数。以下是获取数组长度的示例:

    console.log(arr.length);

    7、遍历数组

    要遍历数组的所有元素,可以使用for循环。下面是遍历数组的示例:

    for (var i = 0; i < arr.length; i++) {
    console.log(arr[i]);
    }

    8、数组排序

    要对数组进行排序,可以使用JavaScript内置的sort()方法。下面是对数组进行排序的示例:

    arr.sort();

    9、数组反转

    要对数组进行反转,可以使用JavaScript内置的reverse()方法。下面是对数组进行反转的示例:

    arr.reverse();

    10、数组搜索
    要在数组中搜索特定的元素,可以使用循环遍历数组,逐个比较每个元素与目标值,找到目标值后返回其索引。下面是一个示例代码:

    console.log(arr.indexOf(3)); // 返回3在数组中的索引位置
    console.log(arr.includes(4)); // 检查数组中是否包含4

    以上就是一些常见的数组操作方法,可以根据需要使用适当的方法来操作数组中的元素。

    想要快速入门前端开发吗?推荐一个免费前端开发基础课程,这个老师讲的特别好,零基础学习无压力,知识点结合代码,边学边练,可以免费试看试学,还有各种辅助工具和资料,非常适合新手!点这里前往学习哦!

    五、使用数组方法的注意事项

    • 数组方法是JavaScript中针对数组对象的内置方法,可以方便地对数组进行操作和处理。

    • 使用数组方法之前,需要先创建一个数组对象。可以使用数组字面量创建一个数组,也可以使用Array()构造函数来创建一个数组。

    • 数组方法可以改变原始数组,也可以返回一个新的数组。需要根据实际需求来选择使用具体的方法。

    • 改变原始数组的方法包括:push()、pop()、shift()、unshift()、splice()、sort()、reverse()等。

    • 不改变原始数组的方法包括:slice()、concat()、join()、map()、filter()、reduce()、forEach()等。

    • 使用数组方法时需要注意方法的参数和返回值,不同的方法可能需要不同的参数,并且返回值类型也可能不同。

    • 数组方法的具体用法可以参考JavaScript官方文档或者其他相关教程和资源。熟练掌握数组方法可以提高代码的效率和可读性。

    以上就是JavaScript数组的创建和使用方法,希望对你有所帮助。记住,数组是JavaScript中非常重要的一部分,掌握好它可以让我们的编程工作更加高效。

    收起阅读 »

    你还以为前端无法操作文件吗

    web
    这里面有个值得说明一点的问题是,我一直以为(可能有人跟我一样)前端是无法操作文件的,可实际上自从HTML5标准出现之后,前端虽然无法像后端那样能灵活的进行文件处理,但因为有了File System Api这套接口,前端也能够进行简单的文件处理操作(不只是读,还...
    继续阅读 »

    这里面有个值得说明一点的问题是,我一直以为(可能有人跟我一样)前端是无法操作文件的,可实际上自从HTML5标准出现之后,前端虽然无法像后端那样能灵活的进行文件处理,但因为有了File System Api这套接口,前端也能够进行简单的文件处理操作(不只是读,还有写)。当然,网络环境鱼龙混杂,为防止不法网站任意获取和修改用户数据,所有本地文件操作都需要用户手动操作,不能自动保存或打开。

    1. 使用场景

      File System Api为浏览器应用增加了无限可能,比如我们经常用到的一些流程图工具,上面的保存到本地的功能,就不用再依赖后端,可以直接将数据保存到本地的文件系统中,下次打开时选中本地的指定文件,可以直接加载到浏览器中,大大提高的前端的能力边界。

    2. 功能描述

      我们就利用File Access Api搞一个简单的在线编辑器,能实现的功能如下:

      第一步,新建一个文件,命名为hello.txt,并填写初始信息 "hello world"

      第二步,打开文件,修改文件内容为“hello world,hello you!”

      第三步,保存文件

    editfile.gif

    1. 实现方式概述

      直接看代码:

      <template>
       <div>
         <el-button type="primary" @click="editFile">编辑文件el-button>
         <el-button type="primary" @click="saveFile">保存文件el-button>
         <el-input
             type="textarea"
             :rows="20"
             placeholder="请输入内容"
             v-model="textarea">
      el-input>
       
       div>
      template>

      <script>
      export default {
         data() {
             return {
                 textarea: ''
            }
        },
         methods: {
             editFile: async function() {
                 // 选择文件
                 let [fileHandle] = await window.showOpenFilePicker()
                 // 复显文件内容
                 fileHandle.getFile().then(blob => {
                     blob.text().then(val => {
                         this.textarea = val
                    })
                })
            },
             saveFile: async function() {
                 // 新建一个文件
                 const fileHandle = await window.showSaveFilePicker({
                     types: [
                        {
                             description: 'hello',
                             accept: {
                                 'text/plain': ['.txt']
        // 对于一些非常用的后缀,均使用这种方式进行定义
                                 // 参考:https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
                                 // 'application/octet-stream': ['.a','.b']
                            }
                        }
                    ]
                })
                 // 在文件内写入内容,写入内容用的是Stream Api,流式写入
                 const writable = await fileHandle.createWritable();
                 await writable.write(this.textarea);
                 await writable.close();
            }
        }
      }
      script>

      可以看到,只需要短短的几行代码就可以完成本地文件的修改,需要注意的是,文件的保存不是实际意义上的修改,而是新建一个文件,进行替换,然后在新的文件里写入最新信息进行的修改。

      另:File System Api目前支持程度还不够普遍,从mdn上来看,大多数api上还有Experimental: This is an experimental technology Check the Browser compatibility table carefully before using this in production.的描述,使用前需要确认好是否满足浏览器要求。


    作者:DaEar图图
    来源:juejin.cn/post/7365679089811947561
    收起阅读 »

    这么炫酷的换肤动画,看一眼你就会爱上

    web
    实现过程 我们先创建下 vue 项目 npm init vite-app vue3-vite-animation 进入文件夹中 cd vue3-vite-animation 安装下依赖 npm install 启动 npm run dev 重新修改 ...
    继续阅读 »

    动画.gif


    实现过程


    我们先创建下 vue 项目


    npm init vite-app vue3-vite-animation

    进入文件夹中


    cd vue3-vite-animation

    安装下依赖


    npm install

    启动


    npm run dev

    image-20240503171537954.png


    重新修改 App.vue


    <template>
    <div class="info-box">
    <div class="change-theme-btn">改变主题</div>
    <h1>Element Plus</h1>
    <p>基于 Vue 3,面向设计师和开发者的组件库</p>
    </div>

    </template>

    <script setup lang="ts">

    </script>



    <style>

    .change-theme-btn {
    width: 80px;
    height: 40px;
    background-color: #fff;
    text-align: center;
    line-height: 40px;
    color: #282c34;
    cursor: pointer;
    border-radius: 8px;
    border: 2px solid #282c34;
    }

    .info-box {
    width: 100vw;
    height: 100vh;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    }
    </style>


    基本样式出来了,但是页面出现了滚动条,我们需要去掉原有样式


    image-20240503175456039.png


    src/index.css,里的所有样式都删除了,再到 index.html 中将 bodymargin 属性去掉


    <body style="margin: 0;">
    <div id="app"></div>
    <script type="module" src="/src/main.js"></script>
    </body>

    接下来,我们来实现下换肤功能


    使用 css 变量,先定义下一套黑暗主题、一套白色主题


    :root {
    --background-color: #fff;
    --color: #282c34;
    background-color: var(--background-color);
    color: var(--color);
    }

    :root.dark {
    --background-color: #282c34;
    --color: #fff;
    }

    再定义点击事件 changeColor,点击 "改变主题" 就会改变主题颜色


    classList.toggle 这个方法的第一个参数是类名,第二个参数是布尔值,表示是否添加类


    如果第二个参数为 true,则添加类;如果第二个参数为 false,则移除类


    <div class="change-theme-btn" @click="changeColor">改变主题</div>

    /* 改变颜色 */
    const changeColor = () => {
    document.documentElement.classList.toggle('dark')
    }

    image-20240503180914393.png


    按钮背景颜色、边框、字体颜色都没有改变


    调整下按钮样式,把背景颜色、边框、字体颜色这些都用 css 变量代替


    .change-theme-btn {
    width: 80px;
    height: 40px;
    background-color: var(--background-color);
    text-align: center;
    line-height: 40px;
    color: var(--color);
    cursor: pointer;
    border-radius: 8px;
    border: 2px solid var(--color);
    }

    image-20240503181138545.png


    这个效果不是我们想要的,需要一个过渡动画对不对


    使用 startViewTransition,这个 API 会生成一个屏幕截图,将新旧屏幕截图进行替换


    截图分别对应两个伪元素 ::view-transition-new(root)::view-transition-old(root)


     // 创建一个过渡对象
    document.startViewTransition(() => {
    document.documentElement.classList.toggle('dark')
    })

    可以看到,一个淡入淡出的效果,但是我们需要的是一个圆向外扩散的效果


    用剪切效果就可以实现,其中 circle(动画进度 at 动画初始x坐标 动画初始y坐标)


    设置动画时间为 1秒,作用在新的伪元素上,也即是作用在新的截图上


    const transition = document.startViewTransition(() => {
    document.documentElement.classList.toggle('dark')
    })

    transition.ready.then(() => {
    document.documentElement.animate({
    clipPath: ['circle(0% at 50% 50%)', 'circle(100% at 100% 100%)']
    }, {
    duration: 1000,
    pseudoElement: '::view-transition-new(root)'
    })
    })

    动画-1714752074132-6.gif


    为什么动画效果和预期的不一样


    因为,默认的动画效果,把当前动画覆盖了,我们把默认动画效果去掉


    /* 隐藏默认的过渡效果 */
    ::view-transition-new(root),
    ::view-transition-old(root) {
    animation: none;
    }

    动画-1714752309164-8.gif


    效果出来了,但是圆的扩散不是从按钮中心扩散的


    那么,通过 ref="btn" 来获取 “改变主题” 按钮的坐标位置


    再获取按钮坐标减去宽高,就能得到按钮的中心坐标了


    <div ref="btn" class="change-theme-btn" @click="changeColor">改变主题</div>

    <script setup>
    import { ref } from 'vue';
    const btn = ref<any>(null)

    /* 改变颜色 */
    const changeColor = () => {
    // 创建一个过渡对象
    const transition = document.startViewTransition(() => {
    document.documentElement.classList.toggle('dark')
    })

    const width = btn.value.getBoundingClientRect().width // 按钮的宽度
    const height = btn.value.getBoundingClientRect().height // 按钮的高度
    const x = btn.value.getBoundingClientRect().x + width / 2 // 按钮的中心x坐标
    const y = btn.value.getBoundingClientRect().y + height / 2 // 按钮的中心y坐标

    transition.ready.then(() => {
    document.documentElement.animate({
    clipPath: [`circle(0% at ${x}px ${y}px)`, `circle(100% at ${x}px ${y}px)`]
    }, {
    duration: 1000,
    pseudoElement: '::view-transition-new(root)',
    })
    })
    }
    </script>

    扩展,如果,我不要从中心扩展,要从左上角开始动画呢,右上角呢...


    我们把按钮放在左上角,看看效果


    修改下样式、与模板


    <template>
    <div ref="btn" class="change-theme-btn" @click="changeColor">改变主题</div>
    <div class="info-box">
    <h1>Element Plus</h1>
    <p>基于 Vue 3,面向设计师和开发者的组件库</p>
    </div>

    </template>

    .info-box {
    width: 100vw;
    height: calc(100vh - 44px);
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    }

    动画这个圆的半径不对,导致动画到快末尾的时候,直接就结束了


    动画-1714753474905-10.gif


    动画的圆的半径 = 按钮中心坐标 到 对角点的坐标


    可以使用三角函数计算,两短边平方 = 斜边平方


    image-20240504002759638.png


    // 计算展开圆的半径
    const tragetRadius = Math.hypot(
    window.innerWidth - x,
    innerHeight - y
    )

    // 设置过渡的动画效果
    transition.ready.then(() => {
    document.documentElement.animate({
    clipPath: [`circle(0% at ${x}px ${y}px)`, `circle(${tragetRadius}px at ${x}px ${y}px)`]
    }, {
    duration: 1000,
    // pseudoElement
    // 设置过渡效果的伪元素,这里设置为根元素的伪元素
    // 这样过渡效果就会作用在根元素上
    pseudoElement: '::view-transition-new(root)',
    })
    })

    动画-1714754131456-15.gif


    如果是右上角呢


    .change-theme-btn {
    float: right;
    width: 80px;
    height: 40px;
    background-color: var(--background-color);
    text-align: center;
    line-height: 40px;
    color: var(--color);
    cursor: pointer;
    border-radius: 8px;
    border: 2px solid var(--color);
    }

    动画-1714754468881-23.gif


    在右边的话,使用三角函数计算,其中一个短边就不能是 屏幕宽度 - 按钮x坐标,直接是 x 坐标就对了


    那要怎么实现呢,直接取 屏幕宽度 - 按钮x坐标 与 按钮x坐标 的最大值就可以了


    y 也是同理


    const tragetRadius = Math.hypot(
    Math.max(x, window.innerWidth - x),
    Math.max(y, window.innerHeight - y)
    )

    动画-1714754788538-25.gif


    你可以试试其他位置,是否也是可行的


    完整代码


    <template>
    <div ref="btn" class="change-theme-btn" @click="changeColor">改变主题</div>
    <div class="info-box">
    <h1>Element Plus</h1>
    <p>基于 Vue 3,面向设计师和开发者的组件库</p>
    </div>

    </template>

    <script setup lang="ts">
    import { ref } from 'vue';
    const btn = ref<any>(null)

    /* 改变颜色 */
    const changeColor = () => {
    // 创建一个过渡对象
    const transition = document.startViewTransition(() => {
    document.documentElement.classList.toggle('dark')
    })

    const width = btn.value.getBoundingClientRect().width // 按钮的宽度
    const height = btn.value.getBoundingClientRect().height // 按钮的高度
    const x = btn.value.getBoundingClientRect().x + width / 2 // 按钮的中心x坐标
    const y = btn.value.getBoundingClientRect().y + height / 2 // 按钮的中心y坐标

    // 计算展开圆的半径
    const tragetRadius = Math.hypot(
    Math.max(x, window.innerWidth - x),
    Math.max(y, window.innerHeight - y)
    )

    // 设置过渡的动画效果
    transition.ready.then(() => {
    document.documentElement.animate({
    clipPath: [`circle(0% at ${x}px ${y}px)`, `circle(${tragetRadius}px at ${x}px ${y}px)`]
    }, {
    duration: 1000,
    // pseudoElement
    // 设置过渡效果的伪元素,这里设置为根元素的伪元素
    // 这样过渡效果就会作用在根元素上
    pseudoElement: '::view-transition-new(root)',
    })
    })
    }
    </script>


    <style>

    :root {
    --background-color: #fff;
    --color: #282c34;
    background-color: var(--background-color);
    color: var(--color);
    }

    :root.dark {
    --background-color: #282c34;
    --color: #fff;
    }

    /* 隐藏默认的过渡效果 */
    ::view-transition-new(root),
    ::view-transition-old(root) {
    animation: none;
    }

    .change-theme-btn {
    float: right;
    width: 80px;
    height: 40px;
    background-color: var(--background-color);
    text-align: center;
    line-height: 40px;
    color: var(--color);
    cursor: pointer;
    border-radius: 8px;
    border: 2px solid var(--color);
    }

    .info-box {
    width: 100vw;
    height: calc(100vh - 44px);
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    }
    </style>

    换肤动画源码


    小结


    换肤功能,主要靠 css 变量 与 classList.toggle


    startViewTransition 这个 API 来实现过渡动画效果,注意需要清除默认动画


    圆点扩散效果,主要运用剪切的方式进行实现,计算过程运用了三角函数运算


    作者:大麦大麦
    来源:juejin.cn/post/7363836438935552035
    收起阅读 »

    background简写,真细啊!

    web
    背景原因 今天写需求,需要使用background简写属性,心想这还不简单吗,真男人写样式只需要两秒: background: url('./bg.png') no-repeat center contain ; 搞定! 上面设置的依次是 背景图片 背...
    继续阅读 »

    背景原因


    今天写需求,需要使用background简写属性,心想这还不简单吗,真男人写样式只需要两秒:


    background:  url('./bg.png') no-repeat center contain ;

    搞定!


    上面设置的依次是 背景图片 背景平铺模式 背景位置 背景图片是保有其原有的尺寸还是拉伸到新的尺寸。


    so easy~


    看我ctrl + s 保存代码,编译。


    嗯? 怎么不生效? 俺的背景呢?
    打开控制台一看,好家伙,压根没生效:


    image.png


    问题排查


    第一反应是这些属性有固定顺序,但是凭我练习两年半的经验,不应该啊,之前也是这样用的啊,遂打开MDN,仔细翻阅....


    发现了下面这段话:


    image.png


    这让我更加确信 写的没毛病啊!!


    background-attachment、background-color、background-image、background-position、background-repeat、background-size
    这些属性可以以任意顺序书写。


    见了鬼了,待我排查两小时(摸鱼...)


    原因浮现


    在仔细阅读文档后发现,其实在文档的上面,还有另外一段话:


    image.png


    我恍然大悟,索嘎,以后看文档不能马虎了,得仔细查阅,过于经验主义了,这都是细节啊!


    background使用注意事项和总结


    其实,使用background时,大部分时候 属性的顺序是可以任意位置书写的,
    但是有两个属性有点特殊,那就是background-size和background-position,


    当background简写同时有这两个属性时,那么必须background-position在前,background-size在后,且两者只能紧挨着书写并且以 "/"分隔。
    例如:


    错误: background: url('./bg.png') no-repeat center  contain ; // 没有以 "/"分隔
    错误: background: url('./bg.png') center no-repeat contain ; // 没有紧挨着书写
    错误: background: url('./bg.png') no-repeat contain / center; //background-size写在了 background-position的前面

    正确: background: url('./bg.png') no-repeat center / contain ;


    写在最后


    其实MDN在关于background的文档最开头的例子中就有写:


    image.png


    只不过没有用语言描述出来,一般没有认真看很难发现,所以有时候能够静下心来认真查阅文档,真的会发现很多细节(甩锅:这tm是谁写的文档,出来挨打).


    作者:可狗可乐
    来源:juejin.cn/post/7234825495333158949
    收起阅读 »

    JavaScript 流程控制语句详解:if语句、switch语句、while循环、for循环等

    JavaScript,作为一种广泛使用的编程语言,它的流程控制语句是构建逻辑和实现功能的基础。流程控制语句包括条件语句、循环语句和转向语句,它们是编程中不可或缺的部分。接下来,我们将一一解析这些语句,带你走进JavaScript的世界。一、什么是流程控制语句流...
    继续阅读 »

    JavaScript,作为一种广泛使用的编程语言,它的流程控制语句是构建逻辑和实现功能的基础。流程控制语句包括条件语句、循环语句和转向语句,它们是编程中不可或缺的部分。

    接下来,我们将一一解析这些语句,带你走进JavaScript的世界。

    一、什么是流程控制语句

    流程控制语句是用来控制程序中语句执行顺序的语句,它们可以影响程序的流程,从而实现不同的逻辑。流程控制语句主要分为以下三类:
    Description

    顺序结构: 这是最基本的流程控制,代表代码按照书写的顺序从上到下依次执行。通常程序都是从第一行代码开始顺序执行到结束的。

    选择结构: 用于根据特定条件来控制代码的执行路径。常见的选择结构包括if、else、if-else if和switch等。这些语句允许程序在满足某些条件时执行特定的代码块,而在其他条件下执行另外的代码块或跳过某些代码。

    循环结构: 用于重复执行某段代码直到满足退出条件为止。循环语句包括for、foreach、while和do-while等。通过这些语句,可以实现固定次数的循环或者当某个条件成立时的持续循环。

    此外,还有跳转语句如break、continue和return等,它们可以改变正常的控制流程,例如跳出当前循环或者返回函数的结果。

    二、条件判断语句

    使用条件判断语句可以在执行某个语句之前进行判断,如果条件成立才会执行语句,条件不成立,则语句不执行。

    语法一:if(条件表达式){语句…};

    执行流程:
    if语句在执行时,会先对条件表达式进行求值判断,

    • 如果条件表达式的值为true,则执行if后的语句,

    • 如果条件表达式的值为false,则不会执行if后的语句if语句只能控制紧随其后的那个语句。

    如果希望if语句可以控制多条语句,可以将这些语句统一放在代码块中,如果就一条if语句,代码块不是必须的,但在开发中尽量写清楚。

    代码演示:

    <script>
    if(true) console.log('好好学习,天天向上');
    // 加上条件运算符 && ||
    var a=20;
    if(a>10&&a<=20){
    alert('a在10-20之间');
    alert("4567")
    }
    </script>

    语法二:if…else…语句

    语法:

    if(条件表达式){
    语句....
    }else{
    语句....
    }

    执行流程:
    当该语句执行时,会先对if后的条件进行判断,

    • 如果该值为true,则执行if后的语句,

    • 如果该值为false,则执行else后的语句,两者选其一执行。

    语法三:if…else if…else

    语法:

    if(条件表达式){
    语句....
    }else if(条件表达式){

    语句....
    }else{
    语句....
    }

    执行流程:
    当该语句执行时,会从上到下依次对条件表达式进行求值,

    • 如果值为true,则执行当前语句。

    • 如果值为false,则继续向下判断,如果所有的条件都不满意,就执行最后一个else或者不执行,该语句中,只会有一个代码块被执行,一旦代码块执行了, 则直接结束语句。

    <script>
    var age=16;
    /* if(age>=60){
    alert("你已经退休了~~~")
    }else{
    alert("你还没退休~~~")
    } */


    if(age>=100){
    alert("您老高寿呀~~~");
    }else if(age>=80){
    alert("你也不小了");
    } else if(age>=60){
    alert("你刚退休呀~~~");
    }else if(age>=30){
    alert("你已经中年了");
    }else if(age>=17){
    alert("你刚成年呀~~~");
    }else{
    alert("你还是个小孩子~~")
    };
    </script>

    三、条件分支语句

    switch语句是一种多分支选择结构,它可以根据表达式的值,来选择执行不同的代码块。

    语法:switch…case…

    switch(条件表达式){
    case 表达式:
    语句....
    break;
    case 表达式:
    语句....
    break;
    default:
    语句...
    break;
    }

    执行流程:

    在执行时,会依次将case后的表达式的值和switch后的条件表达式的值进行全等比较。

    • 如果比较结果为true,则从当前case处开始执行代码,当前case后的所有代码都会执行;

    • 在case的后边跟着一个break关键字,这样可以确保只会执行当前case后的语句,而不会执行其他的case;

    • 如果比较结果为false,则继续向下比较;

    • 如果所有的比较结果都为false,则只执行default后的语句;

    注意: switch语句和if语句的功能实际上有重复的,使用switch可以实现if的功能,同样使用if也可以实现switch的功能,所以我们使用时,可以根据自己的习惯选择。
    代码演示:

    <script>
    var num=2;
    switch(num){
    case 1:
    console.log("壹");
    //使用break可以退出switch语句
    break;
    case 2:
    console.log("贰");
    break;
    case 3:
    console.log("叁")
    break;

    default:
    console.log("非法数字~~~");
    break;
    }
    </script>

    四、循环语句

    循环语句,就是让某段代码反复执行。在JavaScript中,主要有for循环、while循环、do…while循环等。

    1) while循环

    语法:

    while(条件表达式){
    语句
    }

    while语句执行流程:

    先对条件表示式进行求值判断,如果值为true,则执行循环体,循环体执行完毕以后,继续对表达式进行判断,如果值为false,则终止循环。

    2) do…while循环

    语法:

    do{

    语句....

    }while(条件表达式)

    执行流程:

    do…while 语句在执行时,会先执行循环体,循环体执行完毕后,再对while后的条件表示式进行判断,如果结果为true,则继续执行,执行完毕继续判断,如果结果为false,则停止执行。

    注意: 实际上以上两个语句功能类似,不同的是while 是先判断后执行,而do…while会先执行后判断,do…while可以保证循环体至少执行一次,而while不行。


    想要快速入门前端开发吗?推荐一个前端开发基础课程,这个老师讲的特别好,零基础学习无压力,知识点结合代码,边学边练,可以免费试看试学,还有各种辅助工具和资料,非常适合新手!点这里前往学习哦!

    3)for语句(for循环)

    在for循环中,为我们提供了专门的位置,用来放三个表达式。

    • 初始化表达式
    • 条件表达式
    • 更新表达式

    for循环的语法:

    for(初始化表达式;条件表达式;更新表达式){

    语句....

    }

    for循环的执行流程:

    • 初始化表达式,初始化变量(初始化表达式,只会执行一次);

    • 条件表达式,判断是否执行循环;

    • 如果为true,则执行循环,如果为false,终止循环;

    • 执行更新表达式,更新表达式执行完毕继续重复。

    <script>
    //第一种写法
    for(var i=0;i<10;i++){
    alert(i);
    }
    //第二种写法 for循环中的三个部分都可以省略,也都可以写在外部
    var i=0;
    for(;i<10;){
    alert(i++);
    }

    //如果在for循环中,不写任何的表达式,只写两个;
    //此时循环是一个死循环,会一直执行下去,慎用
    for(;;){
    alert("hello");
    }
    </script>

    五、break和continue语句

    break关键字

    可以用来退出switch或循环语句,不能在if语句中使用break和continue,break关键字,会立即终止离它最近的那个循环语句。

    continue关键字

    可以用来跳过当次循环,同样continue也是默认只会对离它最近的循环起作用。

    终止指定循环

    可以为循环语句创建一个label(标签),来标识当前的循环。

    语法:

    label(给起的标签名字):循环语句

    使用break语句时,可以在break后跟着一个label,这样break可以结束指定的循环,而不是最近的。

    代码演示

    <script>

    /* for(var i=0;i<5;i++){
    console.log(i);
    //break;//用来结束for的循环语句,for只会循环一次
    if(i==2){
    break;//这个break是对整个for循环起作用的
    }
    } */


    /* for (var i = 0; i < 5; i++) {
    console.log("@外层循环" + i);
    for (var j = 0; j < 5; j++) {
    break;//只会结束离他最近的内层循环
    console.log("内层循环" + j);
    }
    } */



    /* outer: for (var i = 0; i < 5; i++) {
    console.log("@外层循环" + i);
    for (var j = 0; j < 5; j++) {
    break outer; //指定结束外层的for循环
    console.log("内层循环" + j);
    }
    } */



    for (var i = 0; i < 5; i++) {
    if (i == 2) {
    continue;
    }
    console.log(i);
    }
    </script>

    JavaScript的流程控制语句,就像是一把魔法棒,它能让我们的代码按照我们的意愿去运行。掌握了这些语句,我们就可以在编程的世界里自由翱翔。

    希望这篇文章能帮助你更好地理解和使用JavaScript的流程控制语句,让我们一起在编程的道路上,探索更多的可能性。

    收起阅读 »

    前端使用a链接下载内容增加loading效果

    web
    问题描述:最近工作中出现一个需求,纯前端下载 Excel 数据,并且有的下载内容很多,这时需要给下载增加一个 loading 效果。代码如下:// utils.jsconst XLSX = require('xlsx')// 将一个sheet转成最终的exce...
    继续阅读 »
    1. 问题描述:最近工作中出现一个需求,纯前端下载 Excel 数据,并且有的下载内容很多,这时需要给下载增加一个 loading 效果。
    2. 代码如下:
    // utils.js
    const XLSX = require('xlsx')
    // 将一个sheet转成最终的excel文件的blob对象,然后利用URL.createObjectURL下载
    export const sheet2blob = (sheet, sheetName) => {
    sheetName = sheetName || 'sheet1'
    var workbook = {
    SheetNames: [sheetName],
    Sheets: {}
    }
    workbook.Sheets[sheetName] = sheet
    // 生成excel的配置项
    var wopts = {
    bookType: 'xlsx', // 要生成的文件类型
    bookSST: false, // 是否生成Shared String Table,官方解释是,如果开启生成速度会下降,但在低版本IOS设备上有更好的兼容性
    type: 'binary'
    }
    var wbout = XLSX.write(workbook, wopts)
    var blob = new Blob([s2ab(wbout)], { type: 'application/octet-stream' })
    // 字符串转ArrayBuffer
    function s2ab(s) {
    var buf = new ArrayBuffer(s.length)
    var view = new Uint8Array(buf)
    for (var i = 0; i !== s.length; ++i) view[i] = s.charCodeAt(i) & 0xff
    return buf
    }
    return blob
    }

    /**
    * 通用的打开下载对话框方法,没有测试过具体兼容性
    * @param url 下载地址,也可以是一个blob对象,必选
    * @param saveName 保存文件名,可选
    */
    export const openDownloadDialog = (url, saveName) => {
    if (typeof url === 'object' && url instanceof Blob) {
    url = URL.createObjectURL(url) // 创建blob地址
    }
    var aLink = document.createElement('a')
    aLink.href = url
    aLink.download = saveName + '.xlsx' || '1.xlsx' // HTML5新增的属性,指定保存文件名,可以不要后缀,注意,file:///模式下不会生效
    var event
    if (window.MouseEvent) event = new MouseEvent('click')
    else {
    event = document.createEvent('MouseEvents')
    event.initMouseEvent(
    'click',
    true,
    false,
    window,
    0,
    0,
    0,
    0,
    0,
    false,
    false,
    false,
    false,
    0,
    null
    )
    }
    aLink.dispatchEvent(event)
    }

    <el-button
    @click="clickExportBtn"
    >
    <i class="el-icon-download"></i>下载数据
    </el-button>
    <div class="mongolia" v-if="loadingSummaryData">
    <el-icon class="el-icon-loading loading-icon">
    <Loading />
    </el-icon>
    <p>loading...</p>
    </div>

    clickExportBtn: _.throttle(async function() {
    const downloadDatas = []
    const summaryDataForDownloads = this.optimizeHPPCDownload(this.summaryDataForDownloads)
    summaryDataForDownloads.map(summaryItem =>
    downloadDatas.push(this.parseSummaryDataToBlobData(summaryItem))
    )
    // donwloadDatas 数组是一个三维数组,而 json2sheet 需要的数据是一个二维数组
    this.loadingSummaryData = true
    const downloadBlob = aoa2sheet(downloadDatas.flat(1))
    openDownloadDialog(downloadBlob, `${this.testItem}报告数据`)
    this.loadingSummaryData = false
    }, 2000),

    // css
    .mongolia {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background-color: rgba(0, 0, 0, 0.9);
    display: flex;
    justify-content: center;
    align-items: center;
    font-size: 1.5rem;
    color: #409eff;
    z-index: 9999;
    }
    .loading-icon {
    color: #409eff;
    font-size: 32px;
    }
    1. 解决方案探究:
    • 在尝试了使用 $nextTick、将 openDownloadDialog 改写成 Promise 异步函数,或者使用 async/await、在 openDownloadDialog 中添加 loadingSummaryData 逻辑,发现依旧无法解决问题,因此怀疑是 document 添加新元素与 vue 的 v-if 渲染产生冲突,即 document 添加新元素会阻塞 v-if 的执性。查阅资料发现,问题可能有以下几种:

      • openDownloadDialog 在执行过程中执行了较为耗时的同步操作,阻塞了主线程,导致了页面渲染的停滞。
      • openDownloadDialog 的 click 事件出发逻辑存在问题,阻塞了事件循环(Event Loop)。
      • 浏览器在执行 openDownloadDialog 时,将其脚本任务的优先级设置得较高,导致占用主线程时间片,推迟了其他渲染任务。
      • Vue 的批量更新策略导致了 v-if 内容的显示被延迟。
    • 查阅资料后找到了如下几种方案:

        1. 使用 setTimeout 使 openDownloadDialog 异步执行
        clickExport() {
        this.loadingSummaryData = true;

        setTimeout(() => {
        openDownloadDialog(downloadBlob, `${this.testItem}报告数据`);

        this.loadingSummaryData = false;
        });
        }


        1. 对 openDownloadDialog 内部进行优化
        • 避免大循环或递归逻辑
        • 将计算工作分批进行
        • 使用 Web Worker 隔离耗时任务
          • 在编写 downloadWorker.js 中的代码时,要明确这部分代码是运行在一个独立的 Worker 线程内部,而不是主线程中。

              1. 不要直接依赖或者访问主线程的全局对象,比如 window、document 等。这些在 Worker 内都无法直接使用。
              1. 不要依赖 DOM 操作,比如获取某个 DOM 元素。Worker 线程无法访问页面的 DOM。
              1. 代码执行的入口是 onmessage 回调函数,在其中编写业务逻辑。
              1. 和主线程的通信只能通过 postMessage 和 onmessage 发送消息事件。
              1. 代码应该是自包含的,不依赖外部变量或状态。
              1. 可以导入其他脚本依赖,比如用 import 引入工具函数等。
              1. 避免修改或依赖全局作用域,比如定义全局变量等。
          • 总之,编写 downloadWorker.js 时要注意它执行在一个独立环境中,只能通过消息事件与主线程通信。
      // 创建 Worker
      const worker = new Worker('downloadWorker.js');

      // 点击下载时向 Worker 发送消息
      function clickDownload() {

      showLoading();

      worker.postMessage({
      url: fileURL,
      filename: 'report.xlsx'
      });

      worker.onmessage = function(e) {
      // 收到下载完成的消息
      hideLoading();
      }

      }

      // 显示 loading
      function showLoading() {
      loading.style.display = 'block';
      }

      // 隐藏 loading
      function hideLoading() {
      loading.style.display = 'none';
      }

      // downloadWorker.js

      onmessage = function(e) {
      const url = e.data.url;
      const filename = e.data.filename;

      // 创建并点击链接触发下载
      const a = document.createElement('a');
      a.href = url;
      a.download = filename;
      a.click();

      postMessage('下载完成');
      }

      <div id="loading" style="display:none;">
      Downloading...
      </div>

      <button onclick="clickDownload()">
      Download
      </button>

      <script src="downloadWorker.js"></script>


        1. 使用 requestIdleCallback 进行调度
        clickExport() {
        this.loadingSummaryData = true;

        requestIdleCallback(() => {
        openDownloadDialog(downloadBlob, `${this.testItem}报告数据`);

        this.loadingSummaryData = false;
        });
        }
      • 当使用上述方案时发现里面的内容变得异常的慢,于是去问GPT后,得到以下答案:

        使用 requestIdleCallback 来执行 openDownloadDialog 确实可能会需要更多时间。

        因为 requestIdleCallback 会在主线程空闲时才执行回调函数,所以不像 setTimeout 可以直接按指定时间异步执行。

        requestIdleCallback 的优先级较低,只在主线程空闲时才会执行,所以 download 的速度会比 setTimeout 慢一些。

        但使用 requestIdleCallback 的优点是它不会占用主线程时间,可以避免阻塞页面渲染。

        所以需要权衡执行速度和避免阻塞之间的关系:

        • 如果下载速度是优先考虑的因素,可以继续使用 setTimeout 异步执行。
        • 如果避免阻塞渲染更重要,则 requestIdleCallback 是更好的选择。

        偶尔下载任务被延迟不是大问题,但频繁的下载任务阻塞会严重影响页面体验。

        此外,可以结合两者试试:

        setTimeout 先异步调度,然后在回调中使用 requestIdleCallback 避免阻塞。这样就可以兼顾下载速度和避免渲染阻塞了。

      clickExport() {

      this.loadingSummaryData = true;

      setTimeout(() => {

      requestIdleCallback(() => {
      openDownloadDialog(downloadBlob);
      });

      this.loadingSummaryData = false;

      }, 200);

      }


        1. 分析线程模型,找到具体的阻塞点
        • 使用 Performance 工具分析线程
        • debugger 及 console.log 打印关键函数的执行时间
        • 检查是否有非主线程的任务阻塞了主线程
        1. 调整 vue 的批量更新策略
      new Vue({
      config: {
      // 修改批量更新的队列长度
      batchUpdateDuration: 100
      }
      })



    作者:李卫泽
    来源:juejin.cn/post/7268050036474609683
    收起阅读 »

    我改进了数据滚动方式!老板直接加薪

    web
    需求背景 前几天,甲方提了个需求,想让下面的数据循环展示,准备放在他们集团首页给他们领导演示用。 我们领导很重视这个事儿,拍了拍我,语重心长的说,小伙子,好好做。 我啪的一下就兴奋了,老板居然如此器重我,我必当鞠躬尽瘁,减少摸鱼,我要让老板拜倒在精湛的技术下...
    继续阅读 »

    需求背景


    前几天,甲方提了个需求,想让下面的数据循环展示,准备放在他们集团首页给他们领导演示用。



    我们领导很重视这个事儿,拍了拍我,语重心长的说,小伙子,好好做。


    我啪的一下就兴奋了,老板居然如此器重我,我必当鞠躬尽瘁,减少摸鱼,我要让老板拜倒在精湛的技术下!


    于是,我搬出自己的库存代码,仅2min就实现了数据的滚动:


    没错,我直接照搬了自己以前写过的文章:JS实现可滚动区域自动滚动展示 - 掘金


    就在我准备告诉老板我做完了的时候,我突然想了想,这么快做完,老板一定觉得我没好好做,我以后还怎么升职加薪,赢取白富美?


    于是,我连夜研究,终于改进了数据滚动方式,赢得了老板的大饼(以后涨500)。最终效果:



    技术方案


    技术选型


    观察最终效果图,可以发现这其实就是一个数据循环滚动的效果,每条内容之间间隔1000ms,每条出现动的时间为500ms。用术语来说,这就是一个单步停顿滚动效果。


    我百度了一下,社区还是有这个实现的现成方案的:vue-seamless-scroll,周下载也还行。



    于是,我果断试了试,结果不知道什么原因,并不生效...


    既然如此,直接手写一个吧!


    实现思路


    要实现上述效果其实很简单,如图



    我们创造一个含有六个值的数组,每隔一段时间循环更改黄色区域的数据,当黄色区域数据变成最新的时候,红色区域整体向下移动,当有数值超出滚动区域后,在删除这个数据即可。


    数据更新


    如果不考虑动画,我们的代码应该这么写


    <template>
    <div class="item-wrap" v-for="(item, index) in animationData">
    <!-- 模块内容 -->
    </div>

    </template>
    <script setup lang="ts">
    // #假设这是接口请求的10条最新数据
    const allCarouseData = ref([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
    // #需要轮播的数据
    const animationData = ref<any>([])
    // *定时器
    const animationTimerMeta: any = {
    timer: null,
    // 这个函数负责设置轮播数据的更新逻辑。
    timeFuc() {
    let setTimeoutId: any = null
    if (this.timer) return
    this.timer = setInterval(() => {
    // 取轮播数据的第一条id
    let firstId = animationData.value[0].id
    // 为轮播数据添加最新的第一项数据
    let index = allCarouseData.value.findIndex((res: any) => res.id === firstId)
    let addIndex = index - 1 < 0 ? allCarouseData.value.length - 1 : index - 1
    animationData.value.unshift(allCarouseData.value[addIndex])
    setTimeout(() => {
    // 删除数组的最后一项
    animationData.value.pop()
    }, 1000)

    }, 1500)
    }
    }
    animationData.value = allCarouseData.value.slice(-5)
    animationTimerMeta.timeFuc()
    </script>

    上述代码的主要功能是:



    1. 从 allCarouseData 中取出最后5个元素作为初始的轮播数据。

    2. 每1.5秒更新一次轮播数据,具体逻辑是:移除当前 animationData 的第一个元素,并从 allCarouseData 中取出前一个元素(如果已经是第一个元素,则取最后一个)添加到 animationData 的开头。

    3. 每1秒从 animationData 的末尾移除一个元素。


    上述代码没有实现动画,他的效果是这样的:



    动画添加


    <template>
    <div class="item-wrap" v-for="(item, index) in animationData"
    :class="[{ moveToBottom: animationActive }, { show: animationActive && index === 0 }]"
    >

    <!-- 模块内容 -->
    </div>

    </template>
    <script setup lang="ts">
    // #假设这是接口请求的10条最新数据
    const allCarouseData = ref([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
    // #需要轮播的数据
    const animationData = ref<any>([])
    // #是否开启动画
    const animationActive = ref(false)
    // *定时器
    const animationTimerMeta: any = {
    timer: null,
    // 这个函数负责设置轮播数据的更新逻辑。
    timeFuc() {
    let setTimeoutId: any = null
    if (this.timer) return
    this.timer = setInterval(() => {
    // 取轮播数据的第一条id
    let firstId = animationData.value[0].id
    // 为轮播数据添加最新的第一项数据
    let index = allCarouseData.value.findIndex((res: any) => res.id === firstId)
    let addIndex = index - 1 < 0 ? allCarouseData.value.length - 1 : index - 1
    animationData.value.unshift(allCarouseData.value[addIndex])
    setTimeout(() => {
    // 删除数组的最后一项
    animationData.value.pop()
    }, 1000)

    }, 1500)
    }
    }
    animationData.value = allCarouseData.value.slice(-5)
    animationTimerMeta.timeFuc()
    </script>


    @keyframes moveToBottom {
    0% {
    transform: translateY(-47px);
    }

    100% {
    transform: translateY(0);
    }
    }

    .moveToBottom {
    animation: moveToBottom 500ms ease-in-out forwards;
    }

    @keyframes fadeInFromTop {
    0% {
    opacity: 0;
    transform: translateY(-47px);
    }

    100% {
    opacity: 1;
    transform: translateY(0);
    color: #683BD6;
    }
    }

    .show {
    animation: fadeInFromTop 500ms ease-in-out forwards;
    }

    上述代码中,为了实现动画效果,采用了动态添加类名的技术方案。


    animationData 数组中的元素会按照一定顺序进行显示和隐藏,同时伴随有动画效果。当第一个元素进入视图时,它会应用 fadeInFromTop 动画;其他元素会应用 moveToBottom 动画。通过定时器,元素会定期从 allCarouseData 中获取新的数据并更新 animationData。


    代码释义:



    • moveToBottom: 当 animationActive 为真值时,此类名会被添加到 div 上。

    • show: 当 animationActive 为真值且当前元素是数组的第一个元素时,此类名会被添加到 div 上。


    CSS 释义:



    • moveToBottom 动画:


    定义一个名为 moveToBottom 的关键帧动画,使元素从上方移动到其原始位置。


    moveToBottom 类将此动画应用到元素上。



    • fadeInFromTop 动画:


    定义一个名为 fadeInFromTop 的关键帧动画,使元素从上方淡入并改变颜色。


    show 类将此动画应用到元素上。


    通过上述简单的实现方式,就能最终实现我们想要的效果



    相比于普通滚动,这种方式看起来要好很多!


    结语


    要想实现这种单步停帧的效果,其实有很多实现方式,这只是笔者实现的一种,核心逻辑就是动态改变数据、增添类名。如果大家还有更好的方式,也欢迎大家指点。


    作者:石小石Orz
    来源:juejin.cn/post/7348433631944556555
    收起阅读 »

    只写后台管理的前端要怎么提升自己

    web
    本人写了五年的后台管理。每次面试前就会头疼,因为写的页面除了表单就是表格。抱怨过苦恼过也后悔过(虽然我现在已经心安理得的摆烂),但是站在现在的时间点回想以前,发现有很多事情可以做的更好,于是有了这篇文章。 写优雅的代码 一道面试题 大概两年以前,面试美团的时候...
    继续阅读 »

    本人写了五年的后台管理。每次面试前就会头疼,因为写的页面除了表单就是表格。抱怨过苦恼过也后悔过(虽然我现在已经心安理得的摆烂),但是站在现在的时间点回想以前,发现有很多事情可以做的更好,于是有了这篇文章。


    写优雅的代码


    一道面试题


    大概两年以前,面试美团的时候,面试官让我写一道代码题,时间单位转换。具体的题目我忘记了。


    原题目我没做过,但是我写的业务代码代码里有类似的单位转换,后端返回一个数字,单位是kb,而我要展示成 KBMB 等形式。大概写一个工具函数(具体怎么写的忘记了,不过功能比这个复杂点):


    function formatSizeUnits(kb) {
    let units = ['KB', 'MB', 'GB', 'TB', 'PB'];
    let unitIndex = 0;

    while (kb >= 1024 && unitIndex < units.length - 1) {
    kb /= 1024;
    unitIndex++;
    }

    return `${kb.toFixed(2)} ${units[unitIndex]}`;
    }

    而在此之前,是一个后端同学写的代码(说起来,后台管理系统前端人力不够后端帮忙写好像是常规操作,后端总觉得写前端简单,而我只能说你们那代码写的,一言难尽……),如下:


    function formatSizeUnits(kb) {
    var result = '';

    if (kb < 1024) {
    result = kb + ' KB';
    } else if (kb < 1024 * 1024) {
    result = (kb / 1024).toFixed(2) + ' MB';
    } else if (kb < 1024 * 1024 * 1024) {
    result = (kb / 1024 / 1024).toFixed(2) + ' GB';
    } else {
    result = (kb / 1024 / 1024 / 1024).toFixed(2) + ' TB';
    }

    return result;
    }

    虽然功能都一样,但是我写在面试的时候写了上面的代码得到了面试官的赞扬,也因此非常顺利的通过了面试。而如果我写成了下面的样子,估计不会有这种效果。


    如何提升代码质量


    想提升代码质量,最理想的是靠 code review ,但实际上这玩意在大多数公司根本就不存在。其次就只能多看看大佬们写的代码,但是一来后台管理本来就没太大难度很少有大佬,二来很多时候一个后台管理系统就一两个人负责。


    还好现在有了各种 AI 工具,可以非常方便的优化自己的代码了。


    还是上面的问题,看看 GPT 给的答案


    // 提问:写一个工具函数,对文件大小的单位进行转换,加注释
    // 回答:下面是一个JavaScript工具函数,它接受文件大小、初始单位、目标单位和小数位数,然后返回目标单位下的文件大小。

    /**
    * 将文件大小从一个单位转换为另一个单位。
    *
    * @param {number} size 文件大小。
    * @param {string} fromUnit 初始单位('B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB')。
    * @param {string} toUnit 目标单位('B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB')。
    * @param {number} [decimalPoint=2] 结果保留的小数位数,默认为2。
    * @return {string} 转换后的文件大小,带单位。
    */

    function convertFileSize(size, fromUnit, toUnit, decimalPoint = 2) {
    // 定义单位与字节之间的转换关系
    const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
    // 获取初始单位和目标单位的索引
    const fromIndex = units.indexOf(fromUnit);
    const toIndex = units.indexOf(toUnit);

    // 如果单位不在列表中,抛出错误
    if (fromIndex === -1 || toIndex === -1) {
    throw new Error('Invalid units');
    }

    // 计算初始单位与目标单位之间的转换系数
    const exponent = toIndex - fromIndex;
    // 计算结果大小
    const resultSize = size / Math.pow(1024, exponent);

    // 返回格式化后的结果
    return parseFloat(resultSize.toFixed(decimalPoint)) + ' ' + toUnit;
    }

    // 示例使用
    console.log(convertFileSize(1, 'GB', 'MB')); // 输出: 1024.00 MB
    console.log(convertFileSize(1, 'MB', 'KB')); // 输出: 1024.00 KB
    console.log(convertFileSize(1, 'KB', 'B')); // 输出: 1024.00 B
    console.log(convertFileSize(1, 'MB', 'GB', 5)); // 输出: 0.00098 GB

    还能说什么,我只能说 NB!(经评论区提示,此段代码可能有问题,建议大家在使用AI工具时,重点是借鉴思路,直接使用代码需要谨慎~)


    我现在每次写大段逻辑之后都习惯拿给 AI 看看,有什么更好的实现方式,或者用什么设计模式。AI 是非常低成本且高效提升代码质量的工具。


    学会封装


    一个功能用到了好多次,为什么不封装成组件?一个组件用到了好几个项目,为什么不单独写个npm包?差不多的项目创建了好几个,为什么不封装成脚手架?


    你说,没时间,没必要,复制粘贴反而更快。


    那你就完全没理解,这么做不一定是为了让工作更快完成,而是可以让你在年年终述职时更有话说(你就算写了一百个表单表格没有写一个脚手架更值得炫耀),如果不会写可以问问 AI。


    而当你真正开始封装组件,开始写工具库了,你会发现你需要思考的确实比之前多了。


    关注业务


    对于前端业务重要吗?


    相比于后端来说,前端一般不会太关注业务。就算出了问题大部分也是后端的问题。


    但是就我找工作的经验,业务非常重要!


    如果你做的工作很有技术含量,比如你在做低代码,你可以面试时讲一个小时的技术难点。但是你只是一个破写后台管理,你什么都没有的说。这个时候,了解业务就成为了你的亮点。


    一场面试


    还是拿真实的面试场景举例,当时前同事推我字节,也是我面试过N次的梦中情厂了,刚好那个组做的业务和我之前呆的组做的一模一样。



    • 同事:“做的东西和咱们之前都是一样的,你随便走个过场就能过,我在前端组长面前都夸过你了!”

    • 我:“好嘞!”


    等到面试的时候:



    • 前端ld:“你知道xxx吗?(业务名词)”

    • 我:“我……”

    • 前端ld:“那xxxx呢?(业务名词)”

    • 我:“不……”

    • 前端ld:“那xxxxx呢??(业务名词)”

    • 我:“造……”


    然后我就挂了………………


    如何了解业务



    1. 每次接需求的时候,都要了解需求背景,并主动去理解


      我们写一个表格简简单单,把数据展示出来就好,但是表格中的数据是什么意思呢?比如我之前写一个 kafka 管理平台,里面有表格表单,涉及什么 cluster controller topic broker partition…… 我真的完全不了解,很后悔我几年时间也没有耐下心来去了解。


    2. 每次做完一个需求,都需要了解结果


      有些时候,后台管理的团队可能根本没有PM,那你也要和业务方了解,这个功能做了之后,多少人使用,效率提高了吗?数据是怎样的?


    3. 理解需求,并主动去优化


      产品要展示一千条数据,你要考虑要不要分页,不分页会不会卡,要不要上虚拟表格?


      产品要做一个可拖拽表单,你要考虑是否需要拖动,是否需要配置。


      其实很多时候,产品的思维可能会被局限在竞品的实现方式,而前端可以给TA更多选项。在和产品沟通的时候,你不仅是沟通页面的实现,也更能理解业务。



    关注源码


    说到源码, Vue,React 这些基本是每次必问,也有太多大佬们的总结我就不多说了。


    除此之外,我想大家每次面试应该都会被问,你写了什么亮点的项目,困难的项目,你会说什么?哦,都是表单表格,都用 Element UI 实现的,没什么难度。那估计你面试也就到这里了。


    那说什么,后台管理就这些啊?!


    如果你用了 Element UI 你可以说写了什么自定义 Element UI 表单组件,Element UI 表单源码是什么原理,怎么通信的?用了 Axios 可以说 Axios 的原理,它的拦截器怎么实现的?用了编辑器 diff 那你知道 diff 算法是什么吗?用了 ECharts 那你知道他底层是用什么绘制的吗?用了 husky ?husky的原理是什么?用了Vite,它为什么快?是怎么打包的?写过插件吗?


    可能有人觉得我离谱,事实上上面每一个问题都是我在面试中真实被问到的。当然,如果全部去了解,肯定不现实,每个人的精力有限,不可能每个用到的每个工具每个库都去了解原理,不过只要有一两个你去了解过,就可以在面试中引导面试官来问,要学会表现出你擅长的点


    至于怎么读源码?以前我不知道,现在嘛…… 为什么不问问神奇的 ChatGPT 呢?



    讲一下 Axios 源码中,拦截器是怎么实现的?


    Axios 的拦截器实现基于 Axios 的核心原理,即 Axios 实例是一个包含请求和响应拦截器堆栈的对象。当发出请求或接收响应时,Axios 会遍历这些拦截器,并按照添加的顺序执行请求拦截器,以及按照相反的顺序执行响应拦截器。


    在 Axios 的源码中,拦截器是通过一个 AxiosInterceptorManager 实例来管理的,它维护了一个拦截器数组。每个拦截器都是一个包含 fulfilledrejected 函数的对象。这两个函数分别对应于拦截器成功处理和拦截器处理出错的情况。


    以下是 Axios 拦截器管理器的一个简化版本,展示了其核心实现思路:


    class InterceptorManager {
    constructor() {
    this.handlers = []; // 存储拦截器的数组
    }

    use(fulfilled, rejected) {
    this.handlers.push({
    fulfilled: fulfilled,
    rejected: rejected
    });
    return this.handlers.length - 1; // 返回拦截器的ID
    }

    eject(id) {
    if (this.handlers[id]) {
    this.handlers[id] = null; // 移除拦截器
    }
    }

    forEach(fn) {
    this.handlers.forEach((h) => {
    if (h !== null) {
    fn(h);
    }
    });
    }
    }

    在发送请求或接收响应时,Axios 会创建一个 promise 链,并通过 forEach 方法将拦截器中的 fulfilledrejected 函数添加到这个链中。这样,每个拦截器都可以对请求或响应进行处理,然后将结果传递到链的下一个拦截器,或者在出错时结束链的执行。


    axios.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    chain.unshift(interceptor.fulfilled, interceptor.rejected);
    });

    axios.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    chain.push(interceptor.fulfilled, interceptor.rejected);
    });

    在 Axios 的完整实现中,这个拦截器机制被集成到了 Axios 的请求发送和响应处理流程中。通过这种方式,Axios 可以在发送请求之前和接收响应之后,但在用户定义的 .then.catch 执行之前,插入自定义的逻辑。


    请注意,这里提供的代码只是为了说明 Axios 拦截器的实现原理,并不是 Axios 源码的完整复制。如果你对 Axios 的拦截器实现细节感兴趣,建议查看 Axios 的官方 GitHub 仓库中的源码。



    前端基建


    当我们工作时间久了面试难免会遇到这些问题,前端工程化,前端监控,工作流,部署,性能等等。其实我们在工作中绝大部分时间都在写代码,对于这些不是所有人都有机会接触到,不过这些和所做的业务无关,是我们提升自己很好的一个思路。


    技术选型


    技术栈选 Vue 还是 React?Vue 选 Vue2 还是 Vue3?组件库选 ElementUI 还是 Ant Design?微前端有没有使用过?打包工具用 Vite 还是 Webpack?有那么多表单怎么实现的,有没有什么表达配置化方案,比如Formily?


    对于我这种菜鸡,我这种只写简单的表单表格的人,这些都……无所谓……


    image.png

    不过为了应对面试我们还是需要了解下未选择技术栈的缺点,和已选择技术栈的优点(有点本末倒置…但是常规操作啦)


    Vue 你可以说简单高效轻量级,面试必会问你为什么,你就开始说 Vue 的响应式系统,依赖收集等。


    React 你可以说 JSX、Hooks 很灵活,那你必然要考虑 JSX 怎么编译, Hooks 实现方式等。


    总体而言,对于技术选型,依赖于我们对所有可选项的理解,做选择可能很容易,给出合理的理由还是需要花费一些精力的。


    开发规范


    这个方面,在面试的时候我被问到的不多,我们可以在创建项目的时候,配置下 ESlintstylelintprettiercommitlint 等。


    前端监控


    干了这么多年前端,前端监控我是……一点没做过。


    image.png

    前端监控,简单来说就是我们在前端程序中记录一些信息并上报,一般是错误信息,来方便我们及时发现问题并解决问题。除此之外也会有性能监控,用户行为的监控(埋点)等。之前也听过有些团队分享前端监控,为了出现问题明确责任(方便甩锅)。


    对于实现方案,无论使用第三方库还是自己实现,重要的都是理解实现原理。


    对于错误监控,可以了解一下 Sentry,原理简单来说就是通过 window.onerrorwindow.addEventListener('unhandledrejection', ...) 去分别捕获同步和异步错误,然后通过错误信息和 sourceMap 来定位到源码。


    对于性能监控,我们可以通过 window.performancePerformanceObserver 等 API 收集页面性能相关的指标,除此之外,还需要关注接口的响应时间。


    最后,收集到信息之后,还要考虑数据上报的方案,比如使用 navigator.sendBeacon 还是 Fetch、AJAX?是批量上报,实时上报,还是延迟上报?上报的数据格式等等。


    CI/CD


    持续集成(Continuous Integration, CI)和 持续部署(Continuous Deployment, CD),主要包括版本控制,代码合并,构建,单测,部署等一系列前端工作流。


    场景的工作流有 Jenkins、 Gitlab CI 等。我们可以配置在合并代码时自动打包部署,在提交代码时自动构建并发布包等。


    这块我了解不多,但感觉这些工具层面的东西,不太会涉及到原理,基本上就是使用的问题。还是需要自己亲自动手试一下,才能知道细节。比如在 Gitlab CI 中, Pipeline 、 Stage 和 Job 分别是什么,怎么配置,如何在不同环境配置不同工作流等。


    了解技术动态


    这个可能还是比较依赖信息收集能力,虽然我个人觉得很烦,但好像很多领导级别的面试很愿意问。


    比如近几年很火的低代码,很多面试官都会问,你用过就问你细节,你没用过也会问你有什么设计思路。


    还有最近的两年爆火的 AI,又或者 Vue React的最新功能,WebAssembly,还有一些新的打包工具 Vite Bun 什么的,还有鸿蒙开发……


    虽然不可能学完每一项新技术,但是可以多去了解下。


    总结


    写了这么多,可能有人会问,如果能回到过去,你会怎么做。


    啊,我只能说,说是一回事,做又是另一回事,事实上我并不希望回到过去去卷一遍,菜点没关系,快乐就好,一切都是最好的安排。


    image.png

    作者:我不吃饼干
    来源:juejin.cn/post/7360528073631318027
    收起阅读 »

    JavaScript运算符及优先级全攻略,点击立刻升级你的编程水平!

    在编程的世界里,运算符是构建逻辑、实现功能的重要工具。它能帮助我们完成各种复杂的计算和操作。今天,我们就来深入探索JavaScript中运算符的奥秘,掌握它们的种类和优先级,让你的代码更加高效、简洁!一、什么是运算符运算符,顾名思义,就是用于执行特定操作的符号...
    继续阅读 »

    在编程的世界里,运算符是构建逻辑、实现功能的重要工具。它能帮助我们完成各种复杂的计算和操作。

    今天,我们就来深入探索JavaScript中运算符的奥秘,掌握它们的种类和优先级,让你的代码更加高效、简洁!

    一、什么是运算符

    运算符,顾名思义,就是用于执行特定操作的符号。

    Description

    在JavaScript中,运算符用于对一个或多个值进行操作,并返回一个新的值。它们是编程语言中的基础构件,帮助我们完成各种复杂的计算和逻辑判断。

    运算符可以分为多种类型,如算术运算符、关系运算符、逻辑运算符等。通过使用不同的运算符,我们可以实现各种复杂的计算和逻辑判断,让程序更加灵活、强大。


    二、运算符的分类

    1、算术运算符

    用于执行数学计算,如加法、减法、乘法、除法等。常见的算术运算符有:+、-、*、/、%、++、–等。

    Description

    + 加法运算

    • 两个字符串进行加法运算,则作用是连接字符串,并返回;

    • 任何字符串 + “ ”空串做运算,都将转换为字符串,由浏览器自动完成,相当于调用了String ( )。

    -减法运算 *乘法运算 /除法运算

    • 先转换为 Number 再进行正常的运算。

    注意: 可以通过为一个值 -0 *1 /1 来将其转换为Number数据类型,原理和Number ( )函数一样。

    %求余运算

    对一个数进行求余运算

    代码示例:

    var num1 = 1;
    var num2 = 2;
    var res = num1-num2; //返回值为 -1
    var res = num1*num2; //返回值为 2
    var res = num1/num2; //返回值为 0.5——js中的除法为真除法
    var res = num1%num2; //返回值为 1
    console.log(res);


    2、关系运算符

    通过关系运算符可以比较两个值之间的大小关系,如果关系成立它会返回true,如果关系不成立则返回false。常见的比较运算符有:==、!=、>、<、>=、<=等。

    > 大于号

    • 判断符号左侧的值是否大于右侧的值;

    • 如果关系成立,返回true,如果关系不成立则返回false。

    >= 大于等于

    • 判断符号左侧的值是否大于或等于右侧的值。

    < 小于号

    • 判断符号左侧的值是否小于右侧的值;

    • 如果关系成立,返回true,如果关系不成立则返回false。

    <= 小于等于

    • 判断符号左侧的值是否小于或等于右侧的值。

    非数值的情况

    • 对于非数值进行比较时,会将其转换为数字然后再比较。

    • 如果符号两侧的值都是字符串时,不会将其转换为数字进行比较,而会分别比较字符串中字符的Unicode编码。

    == 相等运算符

    • 两者的值相等即可。

    • 比较两个值是否相等,相等返回 true,否则返回 flase。

    • 使用==来做相等运算

    特殊:

    console.log(null==0);  //返回 false
    console.log(undefined == null); //返回true 因为 undefined衍生自null
    console.log(NaN == NaN); //返回 false NaN不和任何值相等

    isNan() 函数来判断一个值是否是NaN,是返回 true ,否则返回 false。

    Description

    === 全等

    • 两者的值不仅要相等,而且数据类型也要相等。

    • 判断两个值是否全等, 全等返回 true 否则返回 false 。

    != 不相等运算符

    • 只考量两者的数据是否不等。

    • 比较两个值是否不相等,不相等返回 true,否则返回 flas。

    • 使用==来做相等运算。

    !== 不全等运算符

    • 两者的值不仅要不等,而且数据类型也要不等,才会返回true,否则返回false;

    • 判断两个值是否不全等,不全等返回true,如果两个值的类型不同,不做类型转换直接返回true。

    var num1 = 1;
    var num2 = '2';
    var res =(num1 !== num2); //返回值 true
    console.log(res);


    3、逻辑运算符

    用于连接多个条件判断,如与、或、非等。常见的逻辑运算符有:&&、||、!等。

    Description

    && 与

    &&可以对符号两侧的值进行与运算并返回结果。

    运算规则:

    • 两个值中只要有一个值为false就返回false,只有两个值都为true时,才会返回true;

    • JS中的“与”属于短路的与,如果第一个值为false,则不会看第二个值。

    || 或

    • ||可以对符号两侧的值进行或运算并返回结果

    • 两个值中只要有一个true,就返回true;

    • 如果两个值都为false,才返回false。

    JS中的“或”属于短路的或,如果第一个值为true,则不会检查第二个值。

    ! 非

    !可以用来对一个值进行非运算,所谓非运算就是值对一个布尔值进行取反操作,true变false,false变true。

    • 如果对一个值进行两次取反,它不会变化;

    • 如果对非布尔值进行元素,则会将其转换为布尔值,然后再取反;

    • 所以我们可以利用该特点,来将一个其他的数据类型转换为布尔值;

    • 可以为一个任意数据类型取两次反,来将其转换为布尔值;原理和Boolean()函数一样;

    非布尔值的与 或 非

    非布尔值的与 或 非( 会将其先转换为布尔值, 再进行运算 )

    代码示例如下:

    var b1 = true;
    var b2 = false;
    var res = b1 && b2; //返回值为 false
    var res = b1 || b2; //返回值为true
    console.log(res);


    4、赋值运算符

    用于给变量赋值,如等于、加等于、减等于等。常见的赋值运算符有:=、+=、-=等。

    将右侧的值赋值给符号左侧的变量。

    =   右赋给左
    += a+=5 等价于 a=a +5;
    -= a-=5 等价于 a=a-5;
    *= a*=5 等价于 a=a*5;
    /= a/=5 等价于 a=a/5;
    %= a%=5 等价于 a=%+5;


    5、其他运算符

    还有一些特殊的运算符,如类型转换运算符、位运算符等。这些运算符虽然不常用,但在特定场景下会发挥重要作用。

    想要快速入门前端开发吗?推荐一个前端开发基础课程,这个老师讲的特别好,零基础学习无压力,知识点结合代码,边学边练,可以免费试看试学,还有各种辅助工具和资料,非常适合新手!点这里前往学习哦!


    三、运算符的优先级

    在JavaScript中,不同类型的运算符具有不同的优先级。优先级高的运算符会先于优先级低的运算符进行计算。了解运算符的优先级,有助于我们编写出正确、高效的代码。

    以下是一些常见运算符的优先级(从高到低):

    • 括号:( )
    • 单目运算符:++、–、!、+、-、~、typeof等
    • 算术运算符:*、/、%、+、-等
    • 比较运算符:<、>、<=、>=、in、instanceof等
    • 相等运算符:==、!=、===、!==等
    • 逻辑运算符:&&、||等
    • 赋值运算符:=、+=、-=等

    掌握了这些运算符及其优先级,我们就可以根据实际需求灵活运用,编写出更加高效、简洁的代码。

    通过了解JavaScript中的运算符及其优先级,我们可以更好地编写和理解代码。掌握这些知识,你将能更加自如地操纵数据,实现你想要的功能。

    收起阅读 »

    揭秘JavaScript数据世界:一文通晓基本类型和引用类型的精髓!

    在编程的世界里,数据是构建一切的基础。就像建筑师需要了解不同材料的强度和特性一样,程序员也必须熟悉各种数据类型。今天,我们就来深入探讨JavaScript中的数据类型,看看它们如何塑造我们的代码世界。一、JavaScript数据类型简介数据类型是计算机语言的基...
    继续阅读 »

    在编程的世界里,数据是构建一切的基础。就像建筑师需要了解不同材料的强度和特性一样,程序员也必须熟悉各种数据类型。

    今天,我们就来深入探讨JavaScript中的数据类型,看看它们如何塑造我们的代码世界。

    一、JavaScript数据类型简介

    数据类型是计算机语言的基础知识,数据类型广泛用于变量、函数参数、表达式、函数返回值等场合。JavaScript语言的每一个值,都属于某一种数据类型。

    Description

    JavaScript的数据类型主要分为两大类:基本数据类型引用数据类型。下面就来详细介绍这两类数据类型中都包含哪些及如何使用它们。

    二、基本(值类型)数据类型

    首先,让我们从最基本的数据类型开始。JavaScript的基本数据类型包括:字符串(String)、数字(Number)、布尔(Boolean)、空(Null)、未定义(Undefined)、符号(Symbol)。

    1、字符串(String)

    tring类型用于表示由零或多个16位的Unicode字符组成的字符序列,即字符串。至于用单引号,还是双引号,在js中还是没有差别的。记得成对出现。

    let name1 = '张三'
    let name2 = "李四"
    let name3 = `王五`

    1.转换为字符串有2个方法:toString()、String()
    let n = 100
    n.toString() // '100' 数值类型转换为字符串类型
    String(200) // '200' 数值类型转换为字符串类型

    2.模板字符串相当于加强版的字符串,可以定义多行字符串。还可以利用${}在字符串中插入变量和表达式
    let name = '张三丰'
    let age = 180
    `我叫${name},今年${age}岁啦!` // 我叫张三丰,今年180岁啦!

    2、数字(Number)

    该类型的表示方法有两种形式,第一种是整数,第二种为浮点数。整数:可以通过十进制,八进制,十六进制的字面值来表示。

    浮点数:就是该数值中必须包含一个小数点,且小数点后必须有一位数字。

    let num = 100  // 整数
    let floatNum = 3.14 // 浮点数
    // toFixed() 方法可以对计算结果进行四舍五入
    let pi = Math.PI // 3.141592653589793
    pi.toFixed(2) // 3.14 保留2位小数

    // 八进制的值第一位必须是零0,后面每一位数的范围在0~7。如果某一位数超出范围,首位的0会被忽略,后面的数值会按照十进制来解析
    let octalNum1 = 076 // 八进制的 63
    let octalNum2 = 083 // 八进制 83
    let octalNum3 = 06 // 八进制 6

    // 十六进制的值前两位必须是0x,后面每一位十六进制数字的范围在0~9及A~F,字母A~F可以大写也可以小写。
    let hexNum1 = 0xA // 十六进制 10
    let hexNum2 = 0x3f // 十六进制 63

    // 数值转换的三个方法 Number()、parseInt()、parseFloat()

    1.Number() // 可以将字符串、布尔值、null、undefined 等转换为对应的数值,如果无法转换返回NaN
    Number("123") // 输出123
    Number("hello") // 输出NaN


    2.parseInt() // 可以将字符串转换为整数,如果无法转换返回NaN
    parseInt("123") // 输出123
    parseInt("123.45") // 输出:123
    parseInt("hello") // 输出NaN


    3.parseFloat() // 可以将字符串转换为浮点数,如果无法转换返回NaN
    parseFloat("123.45") // 输出123.45
    parseFloat("hello") // 输出NaN

    3、布尔(Boolean)

    Boolean 数据类型只有两个值:true 和 false,分别代表真和假。很多时候我们需要将各种表达式和变量转换成 Boolean 数据类型来当作判断条件。


    1.数值运算判断

    1 + 2 === 3 // true
    1 + 1 > 3 // false


    2.数值类型转换
    let bool1 = Boolean(0); // 数值转换为布尔值
    let bool2 = Boolean(""); // 字符串转换为布尔值
    let bool3 = Boolean(null); // null 转换为布尔值
    let bool4 = Boolean(undefined); // undefined 转换为布尔值
    let bool5 = Boolean(NaN); // NaN 转换为布尔值
    let bool6 = Boolean([]); // 空数组转换为布尔值
    let bool7 = Boolean({}); // 空对象转换为布尔值

    ECMAScript 类型的值都有与布尔值等价的形式。可以调用 Boolean() 函数来将其他类型转换为布尔值。不同类型转换为布尔值的规则如下表

    Description

    4、未定义(Undefined)

    在 JavaScript 中,undefined 是一个特殊的值和数据类型。当一个变量声明但未赋值时,该变量的值就是 undefined。它表示一个未定义或未初始化的值。

    1.声明但未赋值的变量

    // 当使用 var、let 或 const 声明一个变量但未对其赋值时,该变量的初始值为 undefined。
    let n;
    console.log(n) // 输出 undefined


    2.未定义的属性

    // 当访问一个不存在的属性时,该属性的值为undefined
    let obj = { name: '张三丰' }
    console.log(obj.age) // 输出 undefined


    3.函数没有返回值

    // 如果函数没有明确返回值或者使用 return 语句返回一个未定义的值,函数的返回值将是 undefined
    function getName() {
    // 没有返回值
    }
    console.log(foo()) // 输出 undefined


    4.函数参数未传递

    // 如果函数定义了参数但未传递相应的值,那么该参数的值将是 undefined
    function getName(name) {
    console.log("Hello, " + name)
    }
    getName() // 输出:Hello, undefined

    5、空(Null)

    在 JavaScript 中,null 是一个特殊的值和数据类型。它表示一个空值或者不存在的对象。

    与undefined不同,null是JavaScript 保留关键字,而 undefined 只是一个常量。也就是说可以声明名称为 undefined 的变量,但将 null 作为变量使用时则会报错。

    1.空值

    // null 表示一个空值,用于表示变量的值为空
    let name = null
    console.log(name) // 输出 null


    2.不存在的对象

    // 当使用 typeof 运算符检测一个值为 null 的对象时,会返回 "object"
    let obj = null
    console.log(typeof obj) // 输出:object

    null 与 undefined 区别

    • undefined 是表示一个未定义或未初始化的值,常用于声明但未赋值的变量,或者访问不存在的属性。

    • null 是一个被赋予的值,用于表示变量被故意赋值为空。

    • 在判断变量是否为空时,使用严格相等运算符(===),因为 undefined 和 null 在非严格相等运算符(==)下会相等。

    let x;
    let y = null;
    console.log(x === undefined) // 输出:true
    console.log(x === null) // 输出:false
    console.log(y === null) // 输出:true
    console.log(y === undefined) // 输出:false

    6、符号(Symbol)

    符号 (Symbols) 是 ECMAScript 第 6 版新定义的。符号类型是唯一的并且是不可修改的。

    1.创建Symbol

    // 使用全局函数 Symbol() 可以创建一个唯一的 Symbol 值
    let s = Symbol()
    console.log(typeof s) // 输出 symbol


    2.唯一性

    // 每个通过 Symbol() 创建的 Symbol 值都是唯一的,不会与其他 Symbol 值相等,即使它们的描述相同
    let s1 = Symbol()
    let s2 = Symbol()
    console.log(s1 == s2) // 输出 false
    let s3 = Symbol('hello')
    let s4 = Symbol('hello')
    console.log(s3 == s4) // 输出 false


    3.Symbol 常量

    // 通过 Symbol.for() 方法可以创建全局共享的 Symbol 值,称为 Symbol 常量
    let s5 = Symbol.for('key')
    let s6 = Symbol.for('key')
    console.log(s5 === s6) // 输出 true

    Symbol 的主要作用是创建独一无二的标识符,用于定义对象的属性名或者作为一些特殊的标记。它在一些特定的应用场景中非常有用,如在迭代器和生成器中使用 Symbol.iterator 标识可迭代对象。

    三、引用数据类型

    除了基本数据类型,JavaScript还有引用数据类型:对象(Object)、数组(Array)和函数(Function)

    1、对象(Object)

    Object 是一个内置的基本数据类型和构造函数。是一组由键、值组成的无序集合,定义对象类型需要使用花括号{ },它是 JavaScript 中最基本的对象类型,也是其他对象类型的基础。

    1.创建对象

    // Object 类型可以用于创建新的对象。可以使用对象字面量 {} 或者通过调用 Object() 构造函数来创建对象
    let obj1 = {} // 使用对象字面量创建空对象
    let obj2 = new Object() // 使用 Object() 构造函数创建空对象


    2.添加、修改、删除属性

    let obj = {}
    obj.name = '张三丰' // 添加属性
    obj.age = 30 // 添加属性
    obj.name = '张无忌' // 修改属性
    delete obj.age // 删除属性

    2、数组(Array)

    JavaScript 中,数组(Array)是一组按顺序排列的数据的集合,数组中的每个值都称为元素,而且数组中可以包含任意类型的数据。

    在 JavaScript 中定义数组需要使用方括号[ ],数组中的每个元素使用逗号进行分隔。

    数组的特点有哪些?

    • 有序集合: 数组是一种有序的数据集合,每个元素在数组中都有一个对应的索引,通过索引可以访问和操作数组中的元素。

    • 可变长度: 数组的长度是可变的,可以根据需要动态添加或删除元素,或者修改数组的长度。可以使用 push()、pop()、shift()、unshift() 等方法来添加或删除元素,也可以直接修改数组的 length 属性来改变数组的长度。

    • 存储不同类型的值: 数组可以存储任意类型的值,包括基本类型和对象类型。同一个数组中可以混合存储不同类型的值。

    • 索引访问: 通过索引来访问数组中的元素,索引从 0 开始。可以使用方括号语法 [] 或者点号语法 . 来访问数组的元素。

    • 内置方法: 数组提供了许多内置的方法,用于对数组进行常见的操作和处理,如添加、删除、查找、排序、遍历等。常用的数组方法包括 push()、pop()、shift()、unshift()、concat()、slice()、splice()、indexOf()、forEach()、map()、filter()、reduce() 等。

    • 可迭代性: 数组是可迭代的,可以使用 for…of 循环或者 forEach() 方法遍历数组中的元素。

    1.创建数组

    // 可以使用数组字面量 [] 或者通过调用 Array() 构造函数来创建数组。
    let arr1 = [] // 使用数组字面量创建空数组
    let arr2 = new Array() // 使用 Array() 构造函数创建空数组
    let arr3 = [1, 2, 3] // 使用数组字面量创建包含初始值的数组


    2.访问和修改数组元素

    // 数组的元素通过索引访问,索引从 0 开始。可以使用索引来读取或修改数组的元素。
    let arr = [1, 2, 3]
    console.log(arr[0]) // 访问数组的第一个元素,输出:1
    arr[1] = 5 // 修改数组的第二个元素
    arr.length // 获取数组长度,输出:3

    3、函数(Function)

    ECMAScript中的函数是对象,与其他引用类型一样具有属性和方法。因此,函数名实际是一个指向函数对象的指针。

    1.创建函数

    // 可以使用函数声明或函数表达式来创建函数。函数声明使用 function 关键字,后面跟着函数名称和函数体,而函数表达式将函数赋值给一个变量。
    // 函数声明
    function add(a, b) {
    return a + b
    }

    // 函数表达式
    let multiply = function(a, b) {
    return a * b
    }


    2.函数调用

    // 函数可以通过函数名后面加括号 () 进行调用。调用函数时,可以传递参数给函数,函数可以接收参数并进行相应的处理。
    let result = add(3, 5) // 调用 add 函数并传递参数
    console.log(result) // 输出:8


    3.函数返回值

    // 函数可以使用 return 语句返回一个值,也可以不返回任何值。当函数执行到 return 语句时,会立即停止执行,并将返回值传递给函数调用者。
    function calculateSum(a, b) {
    return a + b
    }
    let result = calculateSum(2, 3)
    console.log(result) // 输出:5


    4.函数作用域

    // 函数作用域是指函数内部声明的变量在函数内部有效,外部无法访问。函数内部定义的变量只能在函数内部被访问和使用,在函数外部是不可见的。

    function myFunction() {
    var x = 10 // 局部变量
    console.log(x) // 在函数内部可见
    }
    myFunction() // 输出:10
    console.log(x) // 报错:x is not defined

    此外,JavaScript还有一些特殊的数据类型,如Date(表示日期和时间)、RegExp(表示正则表达式),以及ES6新增的Map、Set、WeakMap和WeakSet,用于存储特定类型的数据。


    想要快速入门前端开发吗?推荐一个前端开发基础课程,这个老师讲的特别好,零基础学习无压力,知识点结合代码,边学边练,可以免费试看试学,还有各种辅助工具和资料,非常适合新手!点这里前往学习哦!


    四、数据类型检测

    检测数据类型可以使用typeof操作符,它可以检测基本数据类型和function,但无法区分不同的引用数据类型。

    var arr = [
    null, // object
    undefined, // undefined
    true, // boolean
    12, // number
    'haha', // string
    Symbol(), // symbol
    20n, // bigint
    function(){}, // function
    {}, // object
    [], // object
    ]
    for (let i = 0; i < arr.length; i++) {
    console.log(typeof arr[i])
    }

    掌握JavaScript数据类型是成为一名高效开发者的关键。它们是构建程序的砖石,理解它们的用法和限制将使你能够构建更稳健、更可维护的代码。

    现在,你已经了解了JavaScript的数据类型,是时候在你的代码中运用这些知识了。记住,实践是学习的最佳方式,所以动手尝试吧!

    如果觉得本文对你有所帮助,别忘了点赞和分享哦!

    收起阅读 »

    商品 sku 在库存影响下的选中与禁用

    web
    分享一下,最近使用 React 封装的一个 Skus 组件,主要用于处理商品的sku在受到库存的影响下,sku项的选中和禁用问题; 需求分析 需要展示商品各规格下的sku信息,以及根据该sku的库存是否为空,判断是否禁用该sku的选择。 以下讲解将按照我的 ...
    继续阅读 »

    分享一下,最近使用 React 封装的一个 Skus 组件,主要用于处理商品的sku在受到库存的影响下,sku项的选中和禁用问题;


    需求分析


    需要展示商品各规格下的sku信息,以及根据该sku的库存是否为空,判断是否禁用该sku的选择。


    sku-2.gif

    以下讲解将按照我的 Skus组件 来,我这里放上我组件库中的线上 demo 和码上掘金的一个 demo 供大家体验;由于码上掘金导入不了组件库,我就上传了一份开发组件前的一份类似的代码,功能和代码思路是差不多的,大家也可以自己尝试写一下,可能你的思路会更优;


    线上 Demo 地址


    码上掘金



    传入的sku数据结构


    需要传入的商品的sku数据类型大致如下:


    type SkusProps = { 
    /** 传入的skus数据列表 */
    data: SkusItem[]
    // ... 其他的props
    }

    type SkusItem = {
    /** 库存 */
    stock?: number;
    /** 该sku下的所有参数 */
    params: SkusItemParam[];
    };

    type SkusItemParam = {
    name: string;
    value: string;
    }

    转化成需要的数据类型:


    type SkuStateItem = {
    value: string;
    /** 与该sku搭配时,该禁用的sku组合 */
    disabledSkus: string[][];
    }[];

    生成数据


    定义 sku 分类


    首先假装请求接口,造一些假数据出来,我这里自定义了最多 6^6 = 46656 种 sku。


    sku-66.gif

    下面的是自定义的一些数据:


    const skuData: Record<string, string[]> = {
    '颜色': ['红','绿','蓝','黑','白','黄'],
    '大小': ['S','M','L','XL','XXL','MAX'],
    '款式': ['圆领','V领','条纹','渐变','轻薄','休闲'],
    '面料': ['纯棉','涤纶','丝绸','蚕丝','麻','鹅绒'],
    '群体': ['男','女','中性','童装','老年','青少年'],
    '价位': ['<30','<50','<100','<300','<800','<1500'],
    }
    const skuNames = Object.keys(skuData)

    页面初始化



    • checkValArr: 需要展示的sku分类是哪些;

    • skusList: 接口获取的skus数据;

    • noStockSkus: 库存为零对应的skus(方便查看)。


    export default () => {
    // 这个是选中项对应的sku类型分别是哪几个。
    const [checkValArr, setCheckValArr] = useState<number[]>([4, 5, 2, 3, 0, 0]);
    // 接口请求到的skus数据
    const [skusList, setSkusList] = useState<SkusItem[]>([]);
    // 库存为零对应的sku数组
    const [noStockSkus, setNoStockSkus] = useState<string[][]>([])

    useEffect(() => {
    const checkValTrueArr = checkValArr.filter(Boolean)
    const _noStockSkus: string[][] = [[]]
    const list = getSkusData(checkValTrueArr, _noStockSkus)
    setSkusList(list)
    setNoStockSkus([..._noStockSkus])
    }, [checkValArr])

    // ....

    return <>...</>
    }

    根据上方的初始化sku数据,生成一一对应的sku,并随机生成对应sku的库存。


    getSkusData 函数讲解


    先看总数(total)为当前需要的各sku分类的乘积;比如这里就是上面传入的 checkValArr 数组 [4,5,2,3]120种sku选择。对应的就是 skuData 中的 [颜色前四项,大小前五项,款式前两项,面料前三项] 即下图的展示。


    image.png

    遍历 120 次,每次生成一个sku,并随机生成库存数量,40%的概率库存为0;然后遍历 skuNames 然后找到当前对应的sku分类即 [颜色,大小,款式,面料] 4项;


    接下来就是较为关键的如何根据 sku的分类顺序 生成对应的 120个相应的sku。


    请看下面代码中注释为 LHH-1 的地方,该 value 的获取是通过 indexArr 数组取出来的。可以看到上面 indexArr 数组的初始值为 [0,0,0,0] 4个零的索引,分别对应 4 个sku的分类;



    • 第一次遍历:


    indexArr: [0,0,0,0] -> skuName.forEach -> 红,S,圆领,纯棉


    看LHH-2标记处: 索引+1 -> indexArr: [0,0,0,1];



    • 第二次遍历:


    indexArr: [0,0,0,1] -> skuName.forEach -> 红,S,圆领,涤纶


    看LHH-2标记处: 索引+1 -> indexArr: [0,0,0,2];



    • 第三次遍历:


    indexArr: [0,0,0,2] -> skuName.forEach -> 红,S,圆领,丝绸


    看LHH-2标记处: 由于已经到达该分类下的最后一个,所以前一个索引加一,后一个重新置为0 -> indexArr: [0,0,1,0];



    • 第四次遍历:


    indexArr: [0,0,1,0] -> skuName.forEach -> 红,S,V领,纯棉


    看LHH-2标记处: 索引+1 -> indexArr: [0,0,1,1];



    • 接下来的一百多次遍历跟上面的遍历同理


    image.png
    function getSkusData(skuCategorys: number[], noStockSkus?: string[][]) {
    // 最终生成的skus数据;
    const skusList: SkusItem[] = []
    // 对应 skuState 中各 sku ,主要用于下面遍历时,对 product 中 skus 的索引操作
    const indexArr = Array.from({length: skuCategorys.length}, () => 0);
    // 需要遍历的总次数
    const total = skuCategorys.reduce((pre, cur) => pre * (cur || 1), 1)
    for(let i = 1; i <= total; i++) {
    const sku: SkusItem = {
    // 库存:60%的几率为0-50,40%几率为0
    stock: Math.floor(Math.random() * 10) >= 4 ? Math.floor(Math.random() * 50) : 0,
    params: [],
    }
    // 生成每个 sku 对应的 params
    let skuI = 0;
    skuNames.forEach((name, j) => {
    if(skuCategorys[j]) {
    // 注意:LHH-1
    const value = skuData[name][indexArr[skuI]]
    sku.params.push({
    name,
    value,
    })
    skuI++;
    }
    })
    skusList.push(sku)

    // 注意: LHH-2
    indexArr[indexArr.length - 1]++;
    for(let j = indexArr.length - 1; j >= 0; j--) {
    if(indexArr[j] >= skuCategorys[j] && j !== 0) {
    indexArr[j - 1]++
    indexArr[j] = 0
    }
    }

    if(noStockSkus) {
    if(!sku.stock) {
    noStockSkus.at(-1)?.push(sku.params.map(p => p.value).join(' / '))
    }
    if(indexArr[0] === noStockSkus.length && noStockSkus.length < skuCategorys[0]) {
    noStockSkus.push([])
    }
    }
    }
    return skusList
    }

    Skus 组件的核心部分的实现


    初始化数据


    需要将上面生成的数据转化为以下结构:


    type SkuStateItem = {
    value: string;
    /** 与该sku搭配时,该禁用的sku组合 */
    disabledSkus: string[][];
    }[];

    export default function Skus() {
    // 转化成遍历判断用的数据类型
    const [skuState, setSkuState] = useState<Record<string, SkuStateItem>>({});
    // 当前选中的sku值
    const [checkSkus, setCheckSkus] = useState<Record<string, string>>({});

    // ...
    }

    将初始sku数据生成目标结构


    根据 data (即上面的假数据)生成该数据结构。


    第一次遍历是对skus第一项进行的,会生成如下结构:


    const _skuState = {
    '颜色': [{value: '红', disabledSkus: []}],
    '大小': [{value: 'S', disabledSkus: []}],
    '款式': [{value: '圆领', disabledSkus: []}],
    '面料': [{value: '纯棉', disabledSkus: []}],
    }

    第二次遍历则会完整遍历剩下的skus数据,并往该对象中填充完整。


    export default function Skus() {
    // ...
    useEffect(() => {
    if(!data?.length) return
    // 第一次对skus第一项的遍历
    const _checkSkus: Record<string, string> = {}
    const _skuState = data[0].params.reduce((pre, cur) => {
    pre[cur.name] = [{value: cur.value, disabledSkus: []}]
    _checkSkus[cur.name] = ''
    return pre
    }, {} as Record<string, SkuStateItem>)
    setCheckSkus(_checkSkus)

    // 第二次遍历
    data.slice(1).forEach(item => {
    const skuParams = item.params
    skuParams.forEach((p, i) => {
    // 当前 params 不在 _skuState 中
    if(!_skuState[p.name]?.find(params => params.value === p.value)) {
    _skuState[p.name].push({value: p.value, disabledSkus: []})
    }
    })
    })

    // ...接下面
    }, [data])
    }

    第三次遍历主要用于为每个 sku的可点击项 生成一个对应的禁用sku数组 disabledSkus ,只要当前选择的sku项,满足该数组中的任一项,该sku选项就会被禁用。之所以保存这样的一个二维数组,是为了方便后面点击时的条件判断(有点空间换时间的概念)。


    遍历 data 当库存小于等于0时,将当前的sku的所有参数传入 disabledSkus 中。


    例:第一项 sku(红,S,圆领,纯棉)库存假设为0,则该选项会被添加到 disabledSkus 数组中,那么该sku选择时,勾选前三个后,第四个 纯棉 的勾选会被禁用。


    image.png
    export default function Skus() {
    // ...
    useEffect(() => {
    // ... 接上面
    // 第三次遍历
    data.forEach(sku => {
    // 遍历获取库存需要禁用的sku
    const stock = sku.stock!
    // stockLimitValue 是一个传参 代表库存的限制值,默认为0
    // isStockGreaterThan 是一个传参,用来判断限制值是大于还是小于,默认为false
    if(
    typeof stock === 'number' &&
    isStockGreaterThan ? stock >= stockLimitValue : stock <= stockLimitValue
    ) {
    const curSkuArr = sku.params.map(p => p.value)
    for(const name in _skuState) {
    const curSkuItem = _skuState[name].find(v => curSkuArr.includes(v.value))
    curSkuItem?.disabledSkus?.push(
    sku.params.reduce((pre, p) => {
    if(p.name !== name) {
    pre.push(p.value)
    }
    return pre
    }, [] as string[])
    )
    }
    }
    })

    setSkuState(_skuState)
    }, [data])
    }

    遍历渲染 skus 列表


    根据上面的 skuState,生成用于渲染的列表,渲染列表的类型如下:


    type RenderSkuItem = {
    name: string;
    values: RenderSkuItemValue[];
    }
    type RenderSkuItemValue = {
    /** sku的值 */
    value: string;
    /** 选中状态 */
    isChecked: boolean
    /** 禁用状态 */
    disabled: boolean;
    }

    export default function Skus() {
    // ...
    /** 用于渲染的列表 */
    const list: RenderSkuItem[] = []
    for(const name in skuState) {
    list.push({
    name,
    values: skuState[name].map(sku => {
    const isChecked = sku.value === checkSkus[name]
    const disabled = isChecked ? false : isSkuDisable(name, sku)
    return { value: sku.value, disabled, isChecked }
    })
    })
    }
    // ...
    }

    html css 大家都会,以下就简单展示了。最外层遍历sku的分类,第二次遍历遍历每个sku分类下的名称,第二次遍历的 item(类型为:RenderSkuItemValue),里面会有sku的值,选中状态和禁用状态的属性。


    export default function Skus() {
    // ...
    return list?.map((p) => (
    <div key={p.name}>
    {/* 例:颜色、大小、款式、面料 */}
    <div>{p.name}</div>
    <div>
    {p.values.map((sku) => (
    <div
    key={p.name + sku.value}
    onClick={() =>
    selectSkus(p.name, sku)}
    >
    {/* classBem 是用来判断当前状态,增加类名的一个方法而已 */}
    <span className={classBem(`sku`, {active: sku.isChecked, disabled: sku.disabled})}>
    {/* 例:红、绿、蓝、黑 */}
    {sku.value}
    </span>
    </div>
    ))}
    </div>
    </div>

    ))
    }

    selectSkus 点击选择 sku


    通过 checkSkus 设置 sku 对应分类下的 sku 选中项,同时触发 onChange 给父组件传递一些信息出去。


    const selectSkus = (skuName: string, {value, disabled, isChecked}: RenderSkuItemValue) => {
    const _checkSkus = {...checkSkus}
    _checkSkus[skuName] = isChecked ? '' : value;
    const curSkuItem = getCurSkuItem(_checkSkus)
    // 该方法主要是 sku 组件点击后触发的回调,用于给父组件获取到一些信息。
    onChange?.(_checkSkus, {
    skuName,
    value,
    disabled,
    isChecked: disabled ? false : !isChecked,
    dataItem: curSkuItem,
    stock: curSkuItem?.stock
    })
    if(!disabled) {
    setCheckSkus(_checkSkus)
    }
    }

    getCurSkuItem 获取当前选中的是哪个sku



    • isInOrder.current 是用来判断当前的 skus 数据是否是整齐排列的,这里当成 true 就好,判断该值的过程就不放到本文了,感兴趣可以看 源码


    由于sku是按顺序排列的,所以只需按顺序遍历上面生成的 skuState,找出当前sku选中项对应的索引位置,然后通过 就可以直接得出对应的索引位置。这样的好处是能减少很多次遍历。


    如果直接遍历原来那份填充所有 sku 的 data 数据,则需要很多次的遍历,当sku是 6^6 时, 则每次变换选中的sku时最多需要 46656 * 6 (data总长度 * 里面 sku 的 params) 次。


    const getCurSkuItem = (_checkSkus: Record<string, string>) => {
    const length = Object.keys(skuState).length
    if(!length || Object.values(_checkSkus).filter(Boolean).length < length) return void 0
    if(isInOrder.current) {
    let skuI = 0;
    // 由于sku是按顺序排列的,所以索引可以通过计算得出
    Object.keys(_checkSkus).forEach((name, i) => {
    const index = skuState[name].findIndex(v => v.value === _checkSkus[name])
    const othTotal = Object.values(skuState).slice(i + 1).reduce((pre, cur) => (pre *= cur.length), 1)
    skuI += index * othTotal;
    })
    return data?.[skuI]
    }
    // 这样需要遍历太多次
    return data.find(s => (
    s.params.every(p => _checkSkus[p.name] === getSkuParamValue(p))
    ))
    }

    isSkuDisable 判断该 sku 是否是禁用的


    该方法是在上面 遍历渲染 skus 列表 时使用的。



    1. 开始还未有选中值时,需要校验 disabledSkus 的数组长度,是否等于该sku参数可以组合的sku总数,如果相等则表示禁用。

    2. 判断当前选中的 sku 还能组成多少种组合。例:当前选中 红,S ,而 isSkuDisable 方法当前判断的 sku 为 款式 中的 圆领,则还有三种组合 红\S\圆领\纯棉红\S\圆领\涤纶红\S\圆领\丝绸

    3. 如果当前判断的 sku 的 disabledSkus 数组中存在这三项,则表示该 sku 选项会被禁用,无法点击。


    const isCheckValue = !!Object.keys(checkSkus).length

    const isSkuDisable = (skuName: string, sku: SkuStateItem[number]) => {
    if(!sku.disabledSkus.length) return false
    // 1.当一开始没有选中值时,判断某个sku是否为禁用
    if(!isCheckValue) {
    let checkTotal = 1;
    for(const name in skuState) {
    if(name !== skuName) {
    checkTotal *= skuState[name].length
    }
    }
    return sku.disabledSkus.length === checkTotal
    }

    // 排除当前的传入的 sku 那一行
    const newCheckSkus: Record<string, string> = {...checkSkus}
    delete newCheckSkus[skuName]

    // 2.当前选中的 sku 一共能有多少种组合
    let total = 1;
    for(const name in newCheckSkus) {
    if(!newCheckSkus[name]) {
    total *= skuState[name].length
    }
    }

    // 3.选中的 sku 在禁用数组中有多少组
    let num = 0;
    for(const strArr of sku.disabledSkus) {
    if(Object.values(newCheckSkus).every(str => !str ? true : strArr.includes(str))) {
    num++;
    }
    }

    return num === total
    }

    至此整个商品sku从生成假数据到sku的选中和禁用的处理的核心代码就完毕了。还有更多的细节问题可以直接查看 源码 会更清晰。


    作者:滑动变滚动的蜗牛
    来源:juejin.cn/post/7313979106890842139
    收起阅读 »