注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

百度输入法在候选词区域植入广告,网友:真nb!

web
V2EX 用户发帖称,百度输入法最新版本在候选词区域植入了广告。具体表现为,如果用户要打 “招商银行” 四个字,当输入 “招商” 之后,候选词的首位是 “★热门加盟店排行” 的链接,点击后会进入名为「加盟星榜单」的广告页面。https://www.v2ex.c...
继续阅读 »

V2EX 用户发帖称,百度输入法最新版本在候选词区域植入了广告。

具体表现为,如果用户要打 “招商银行” 四个字,当输入 “招商” 之后,候选词的首位是 “★热门加盟店排行” 的链接,点击后会进入名为「加盟星榜单」的广告页面。

https://www.v2ex.com/t/1011440

别的不说,想出这个功能的产品经理真是个人才,因此评论区有用户感叹道:


不说用户体验怎么样,不得不说这个键盘的候选词广告想法确实超前,不光超前,还实现了。
根据输入内容,直接用候选词的方式推送广告,从源头出发拿到用户的一手数据,直接甩掉了各种中间商。速度也更快,更精确的投送。
可以说是真 nb 呀


知名科技博主阑夕对此评论道:“你都打出招商两个字了,一定是想加盟店铺做生意吧?逻辑极其通顺智能,对不对?这真的是人类能够企及的创新吗,太牛逼了。


作者:架构师大咖
来源:mp.weixin.qq.com/s/0KR2F_a9q2_9JSS8nXtodQ
收起阅读 »

从uni-app中去掉编译后微信小程序的滚动条

web
首先如果你使用的是页面级滚动,即使uni-app中的pages.json中有相关配置,在编译到小程序中也是没有效果的,因为小程序原生不支持,如下: 那么我们去看微信的官方回复: 所以得出一个结论,要想隐藏滚动条,我们必须使用scroll-view视图组件...
继续阅读 »

首先如果你使用的是页面级滚动,即使uni-app中的pages.json中有相关配置,在编译到小程序中也是没有效果的,因为小程序原生不支持,如下:



那么我们去看微信的官方回复:




所以得出一个结论,要想隐藏滚动条,我们必须使用scroll-view视图组件


那么在uni-app页面滚动是不是scroll-view,答案是的,但是我们没办法在顶层设置,因为官方没有暴露相关api,那么要想去掉滚动条,我们就只能在自己的页面使用scroll-view视图组件,取代全局的滚动视图。


下面上简易代码


<template>
<scroll-view scroll-y="true" :show-scrollbar="false" :enhanced="true" class="main">
<view class="list" v-for="iten in 30">列表{{iten}}</view>
</scroll-view>
</template>


<style lang="scss" scoped>
.main{
height: 100vh;
}
.list{
border: 1xp solid black;
margin: 20rpx auto;
text-align: center;
line-height: 100rpx;
}
</style>

效果图:


初版.gif


如果你的组件不是占满全屏,比如有头部导航


这时候有两种做法:


1.将头部标签放到scroll-view内部,然后固定定位


<template>
<scroll-view scroll-y="true" :show-scrollbar="false" :enhanced="true" class="main">
<view class="nav">导航nav</view>
<view class="list-container">
<view class="list" v-for="iten in 30">列表{{iten}}</view>
</view>
</scroll-view>
</template>

<style lang="scss" scoped>
.main{
height: 100vh;
}
.list-container{
margin-top: 200rpx;
}
.list{
border: 1xp solid black;
margin: 20rpx auto;
text-align: center;
line-height: 100rpx;
}
.nav{
position: fixed;
top: 0;
line-height: 200rpx;
padding-top: 20rpx;
width: 100vw;
text-align: center;
border: 1px solid black;
background-color: #fff;
}
</style>

效果图:


230187154229138168229133168229177143.gif


2.将scroll-view的高度设置为视口余下高度


这里注意一下在移动端尽量较少的使用cale()计算高度


所以这里我们使用flex布局


<template>
<view class="content">
<view class="nav">导航nav</view>
<scroll-view scroll-y="true" :show-scrollbar="false" :enhanced="true" class="main">
<view class="list" v-for="iten in 30">列表{{iten}}</view>
</scroll-view>
</view>
</template>

<style lang="scss" scoped>
.content{
height: 100vh;
width: 100vw;
overflow: hidden;
display: flex;
flex-direction: column;
}
.main{
flex-grow: 1;
}
.list{
border: 1xp solid black;
margin: 20rpx auto;
text-align: center;
line-height: 100rpx;
}
.nav{
height: 200rpx;
line-height: 200rpx;
width: 100vw;
text-align: center;
border: 1px solid black;
background-color: #fff;
}
</style>

效果图:


230187154229138168229133168229177143.gif


如果有帮助到你的话,记得点个赞哦!


猫咪.gif


作者:aways
来源:juejin.cn/post/7330655456883654667
收起阅读 »

解锁 JSON.stringify() 7 个鲜为人知的坑

web
在本文中,我们将探讨与JSON.stringify()相关的各种坑。 1. 处理undefined、Function和Symbol值 在前端中 undefined、Function和Symbol值不是有效的JSON值。在转换过程中遇到它们时,它们会被省略(在对...
继续阅读 »

在本文中,我们将探讨与JSON.stringify()相关的各种坑。


1. 处理undefined、Function和Symbol值


在前端中 undefinedFunctionSymbol值不是有效的JSON值。在转换过程中遇到它们时,它们会被省略(在对象中),或者被更改为null(在数组中)。


例如:

const obj = { foo: function() {}, bar: undefined, baz: Symbol('example') };  
const jsonString = JSON.stringify(obj);
console.log(jsonString); // 输出: '{}'

const obj2 = {arr: [function(){}]};
console.log(JSON.stringify(obj2)); // 输出: {"arr":[null]}

2. 布尔、数字和字符串对象


布尔、数字和字符串对象在字符串化过程中会被转换为它们对应的原始值。

const boolObj = new Boolean(true);  
const jsonString = JSON.stringify(boolObj);
console.log(jsonString); // 输出: 'true'

3. 忽略Symbol键的属性


Symbol键属性在字符串化过程中完全被忽略,即使使用替换函数也是如此。这意味着与Symbol键关联的任何数据都将在生成的JSON字符串中被排除。

const obj = { [Symbol('example')]: 'value' };  
const jsonString = JSON.stringify(obj);
console.log(jsonString); // 输出: '{}'

const obj2 = {[Symbol('example')]: [function(){}]};
console.log(JSON.stringify(obj2)); // 输出 '{}'

4. 处理无穷大(Infinity)、NaN和Null值


Infinity、NaN 和 null 值在字符串化过程中都被视为 null。

const obj = { value: Infinity, error: NaN, nothing: null };  
const jsonString = JSON.stringify(obj);
console.log(jsonString); // 输出: '{"value":null,"error":null,"nothing":null}'

5. Date对象被视为字符串


Date实例通过实现toJSON()函数来返回一个字符串(与date.toISOString()相同),因此在字符串化过程中被视为字符串。

const dateObj = new Date();
const jsonString = JSON.stringify(dateObj);
console.log(jsonString); // 输出:"2024-01-31T09:42:00.179Z"

6. 循环引用异常


如果 JSON.stringify() 遇到具有循环引用的对象,它会抛出一个错误。循环引用发生在一个对象在循环中引用自身的情况下。

const circularObj = { self: null };
circularObj.self = circularObj;
JSON.stringify(circularObj); // Uncaught TypeError: Converting circular structure to JSON

7. BigInt转换错误


使用JSON.stringify()转换BigInt类型的值时引发错误。

const bigIntValue = BigInt(42);  
JSON.stringify(bigIntValue); // Uncaught TypeError: Do not know how to serialize a BigInt

各位同学如果在开发中还遇到过不一样的坑,还请评论区补充互相讨论


作者:StriveToY
来源:juejin.cn/post/7330289404731047936
收起阅读 »

分享:一个超实用的文字截断技巧

web
文字截断是 Web 开发中非常普遍的一个需求,原因无他,很多时候我们无法确定展示文本的容器的宽度,如果不使用文字截断,要么文本被生硬的截断隐藏、要么文本破坏了预期中的布局。 Tailwind CSS 提供的文字截断的原子类: .truncate { over...
继续阅读 »

文字截断是 Web 开发中非常普遍的一个需求,原因无他,很多时候我们无法确定展示文本的容器的宽度,如果不使用文字截断,要么文本被生硬的截断隐藏、要么文本破坏了预期中的布局。


Tailwind CSS 提供的文字截断的原子类:


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

这 3 组 CSS 规则共同作用才可实现用 ... 截断文字的效果,缺一不可。



  • overflow: hidden 表示容器空间不足时内容应该隐藏,而非默认的 visible 完全展示

  • white-space: nowrap 表示文本容器宽度不足时,不能换行,其默认值为 normal,该行为不太好预测,但大部分情况下,它等同于 wrap 即文本尽可能的换行

  • text-overflow: ellipsis 指定文本省略使用 ... ,该属性默认值为 clip ,表示文本直接截断什么也不显示,这样展示容易对用户造成误解,因此使用 ... 更合适


接下来介绍一个在 PC Web 上很实用的交互效果:在需要截断的文本后面,紧跟一个鼠标 hover 上去才会展示的按钮, 执行一些和省略文本强相关、轻操作的动作。


Untitled.gif


如图所示,鼠标 hover 表示的按钮可以用来快速的编辑「标题」。下面介绍一下它的纯 CSS 实现。


首先,我们来实现一个基础版的。


Untitled 1.gif


代码:


<div class="container">
<p class="complex-line truncate">
<span class="truncate">海和天相连成为海岸线</span>
<span class="icon">❤️</span>
</p>
<p class="truncate">鱼和水相濡以沫的世界</p>
</div>

<style>
.truncate {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.container {
max-width: 100px;
}
.complex-line {
display: flex;
}
.complex-line:hover .icon {
display: block;
}
.icon {
display: none;
}
</style>

一些重点:



  • 容器 .container 必须是宽度固定、或者最大宽度固定的,以此确保容器不会因为子元素无止境的扩充。比如你可以设置容器的 widthmax-width 。在某些情况下,即使设置了 max-width 但不生效,此时可以尝试添加 overflow: hidden

  • 含有按钮的行 .complex-line 和子元素中展示文字的标签( .complex-line 下的 span.truncate),都要添加文字截断类 .truncate

  • 按钮 .icon 默认不展示,hover 当前行的时候才展示,这个也很常规,主要通过设置其 display 属性实现。

  • 🌟 接下来一步很巧妙,就是为含有按钮的行 .complex-line ,设置其为 Flex 容器,这主要是借助 Flex Item 的 flex-shrink 属性默认值为 1 的特性,允许含有文字的 Flex Item span.truncate 在按钮 span.icon 需要展示的时候进一步缩放,腾出空间,用于完整展示按钮。


这样便实现了基础版的文字截断 + 可以自动显示隐藏按钮的交互。


接下来再看看文章开头提到的这个交互:


Untitled.gif


它和基础版的样式最大的不同就是它是多列的,这里可以使用 Grid 网格布局实现。


<div class="container">
<label>标题:</label>
<p class="complex-line truncate">
<span class="truncate">高质量人才生产总基地</span>
<span class="icon">✏️</span
</p>
<label>编号:</label>
<p class="truncate">No.9781498128959</p>
</div>

<style>
.container {
display: grid;
grid-template-columns: auto 1fr;
/** ... */
}
</style>

其他样式代码和基础版中的一致,不再赘述。


总结


为了实现本文介绍的这种交互,应该确保:



  • 容器的宽度或最大宽度应该是固定的

  • 按钮前面的那个展示文字的元素,和它们的父元素,都应该有 .truncate 文字截断效果

  • 按钮的父元素应该为 Flex 容器,使得按钮显示时,文字所在标签可以进一步缩放


作者:人间观察员
来源:juejin.cn/post/7330464865315094554
收起阅读 »

消息通知文字竖向无缝轮播组件实现历程

web
背景 最近有个需求需要做一个无缝轮播的消息通知,并且需要抽离成通用组件,记录下实现这个组件的历程。 先看效果 实现过程 思考(part-1) 因为刚开始给的设计稿是没有动画效果的,我刚开始想的效果是只有红色加粗的文字轮播,其他文字不变;然后想着看下有没...
继续阅读 »

背景



最近有个需求需要做一个无缝轮播的消息通知,并且需要抽离成通用组件,记录下实现这个组件的历程。



先看效果
noticeBar.gif


实现过程


思考(part-1)



因为刚开始给的设计稿是没有动画效果的,我刚开始想的效果是只有红色加粗的文字轮播,其他文字不变;然后想着看下有没有已经实现好的轮子,找到一个 js 库:react-text-loop;但是经过我的考虑,如果只是部分文字滚动,红色加粗的文字可能宽度不一样,会导致其他文字换位,所以还是想着整条文字滚动会比较好。



使用 antd-m Swiper 实现(part-2)



想到这个滚动效果在移动端应该很常见,antd-m 应该可能会有组件吧,去瞧瞧👀;noticeBar 组件只有横向滚动文字,没有竖直滚动的。既然没有,那就用其他方式实现吧,最简单的就是用 Swiper 组件设置成 竖向滚动,因为我负责的项目基本都用 antd-m,所以就用 antd-m 的 Swiper 来实现了。



实现过程遇到的问题


antd-m 的 Swiper 组件竖向滚动必须指定高度

在使用 antd-m 的 Swiper 组件竖向滚动的方式好像有问题,但是看文档的使用又是正常,结果发现竖向滚动需要指定高度,所以文档还是要仔细看清楚: Swiper 竖向文档


依赖冲突问题

在自己仓库使用很正常,一点问题都没有;然后打算抽离到我们项目的组件库中,然后在把项目中使用替换成组件库的包,过程很顺畅;过了段时间另一个项目要使用我们的组件,然后我就把包发给他,结果他说他项目里用不了,会报错。


然后 clone 他的项目试了下,果然是有问题的,因为他们项目里用的是 antd-m 2.x,2.x 没有 Swiper 组件,而我的组件库依赖的是 antd-m 5.x,看了下他们仓库用的是antd-m 2.x 和 5.x 共存的方式,可以看一下这个 antd-m 迁移指南,如果要两个版本共存且之前用的是组件按需导入,那么组件按需导入的配置也会有问题,因为两个版本的文件差异比较大,所以需要改一下按需导入的配置:


module.exports = {
"plugins": [
// 原配置
// [
// 'import',
// {
// libraryName: 'antd-mobile',
// libraryDirectory: 'lib',
// style: 'css',
// },
// 'antd-mobile',
// ]

// 修改为
[
'import',
{
libraryName: 'antd-mobile',
customName: (name, file) => {
const { filename } = file.opts;
if (filename.includes('/es/')) {
return `antd-mobile/es/components/${name}`;
}
return `antd-mobile/lib/${name}`;
},
style: (name, file) => {
const { filename } = file.opts;
if (filename.includes('/es/')) {
// 如果项目已经有 global 文件,return false 即可
// 如果没有,这样配置后就不需要手动引入 global 文件
return 'antd-mobile/es/global';
}
return `${name}/style/css`;
},
},
'antd-mobile',
]
]
}

想彻底解决这个依赖冲突问题


其实修改完配置之后使用就正常了,但是我考虑到如果之后想使用这个组件,如果 antd-m 版本不是 5.x,那么有一个项目就要改一个配置,很烦人;而且 antd-m Swiper 竖向需要指定高度,如果都需要指定高度了,那么我直接实现一个滚动动画应该也很简单吧,说干就干。



自己手写一个轮播组件(pard-3)



手动实现轮播还是比较简单的,只不过无缝轮播那里需要处理下,传统的方式都是通过在轮播 item 首尾复制 item 插入,当轮播到最后一个,用复制的 item 来承接,之后在回归原位正常滚动。



手写实现思路



  1. 传入轮播容器的高度,使用 transform: translate3d(0px, ${容器高度}, 0px); 每次移动列表 translateY 的距离为容器的高度。

  2. 处理无缝轮播,因为这个组件没有手动滑动轮播,自由自动向下轮播,所以不需要考虑反方向的轮播处理;当轮播到最后一个 item,那么就将第一个 item transform: translateY(${轮播列表高度}),这时候第一个就在最后一个下面,监听轮播列表 onTransitionEnd,判断当前是否轮播到第一个,是的话就将轮播列表的 translateY 的距离归 0。


最终实现代码



其实最后是封装成了一个 react 组件,但是掘金上为了大家看到更好的效果,用原生的方式简单写了下。如果需要封装成 react/vue 组件参考下方代码应该就够了。



容器未 hidden 效果



组件封装的设计



这里放一下我封装组件设计的 props



interface IProps {
/** 轮播通知 list */
list: any[];
/** noticebar 显隐控制 */
visible?: boolean;
/** 单个轮播 item 的高度, 传入 750 设计稿的数字,会转成 vw */
swiperHeight?: number;
/** 每条通知轮播切换的间隔 */
interval?: number;
/** 轮播动画的持续时间 */
animationDuration?: number;
/** 是否展示关闭按钮 */
closeable?: boolean;
/** 关闭按钮点击的回调 */
onClose?: () => void;
/** 自定义轮播 item 的内容 */
renderItem?: (item: any) => React.ReactNode;
/** notice 的标题 */
noticeTitle?: ReactNode;
/** notice 右边自定义 icon */
rightIcon?: ReactNode;
/** 是否展示 notice 左边 icon */
showLeftIcon?: boolean;
/** notice 左边自定义 icon */
leftIcon?: ReactNode;
/** 自定义类名 */
className?: string;
}

作者:wait
来源:juejin.cn/post/7330054489079169065
收起阅读 »

Vue 依赖注入:一种高效的数据共享方法

web
什么是vue依赖注入? Vue是一个用于构建用户界面的渐进式框架。 它提供了一种简单而灵活的方式来管理组件之间的数据流,即依赖注入(Dependency Injection,DI)。 依赖注入是一种设计模式,它允许一个组件从另一个组件获取它所依赖的数据...
继续阅读 »

什么是vue依赖注入?



Vue是一个用于构建用户界面的渐进式框架。




它提供了一种简单而灵活的方式来管理组件之间的数据流,即依赖注入(Dependency Injection,DI)。



依赖注入是一种设计模式,它允许一个组件从另一个组件获取它所依赖的数据或服务,而不需要自己创建或管理它们。这样可以降低组件之间的耦合度,提高代码的可维护性和可测试性。


依赖注入示意图


在这里插入图片描述

provide和inject



在Vue中,依赖注入的方式是通过provide和inject两个选项来实现的。




provide选项允许一个祖先组件向下提供数据或服务给它的所有后代组件。
inject选项允许一个后代组件接收来自祖先组件的数据或服务。
这两个选项都可以是一个对象或一个函数,对象的键是提供或接收的数据或服务的名称,值是对应的数据或服务。函数的返回值是一个对象,具有相同的格式。



下面是一个简单的例子,演示了如何使用依赖注入的方式共享数据:


父组件


<template>
<div>
<h1>我是祖先组件</h1>
<child-component></child-component>
</div>
</template>

<script>
import ChildComponent from './ChildComponent.vue'

export default {
name: 'AncestorComponent',
components: {
ChildComponent
},
// 提供一个名为message的数据
provide: {
message: 'Hello from ancestor'
}
}
</script>

子组件


<template>
<div>
<h2>我是后代组件</h2>
<p>{{ message }}</p>
</div>
</template>


// 后代组件
<script>
export default {
name: 'ChildComponent',
// 接收一个名为message的数据
inject: ['message']
}
</script>

这样,后代组件就可以直接使用祖先组件提供的数据,而不需要通过props或事件来传递。


需要注意的是,依赖注入的数据是不可响应的,也就是说,如果祖先组件修改了提供的数据,后代组件不会自动更新。
如果需要实现响应性,可以使用一个响应式的对象或者一个返回响应式对象的函数作为provide的值。


实现响应式依赖注入的几种方式


一、提供响应式数据



方法是在提供者组件中使用ref或reactive创建响应式数据,然后通过provide提供给后代组件。后代组件通过inject接收后,就可以响应数据的变化。



提供者:


<template>
<div>
<h1>我是提供者组件</h1>
<button @click="count++">增加计数</button>
<child-component></child-component>
</div>

</template>

<script>
import ChildComponent from './ChildComponent.vue'
import { ref, provide } from 'vue'

export default {
name: 'ProviderComponent',
components: {
ChildComponent
},
setup() {
// 使用ref创建一个响应式的计数器
const count = ref(0)
// 提供给后代组件
provide('count', count)
return {
count
}
}
}
</script>


接收者:


<template>
<div>
<h2>我是接收者组件</h2>
<p>计数器的值是:{{ count }}</p>
</div>

</template>

<script>
import { inject } from 'vue'

export default {
name: 'ChildComponent',
setup() {
// 接收提供者组件提供的响应式对象
const count = inject('count')
return {
count
}
}
}
</script>


二、提供修改数据的方法



提供者组件可以提供修改数据的方法函数,接收者组件调用该方法来更改数据,而不是直接修改注入的数据。



提供者:


<template>
<div>
<h1>我是提供者组件</h1>
<p>消息是:{{ message }}</p>
<child-component></child-component>
</div>

</template>

<script>
import ChildComponent from './ChildComponent.vue'
import { ref, provide } from 'vue'

export default {
name: 'ProviderComponent',
components: {
ChildComponent
},
setup() {
// 使用ref创建一个响应式的消息
const message = ref('Hello')
// 定义一个更改消息的方法
function updateMessage() {
message.value = 'Bye'
}
// 提供给后代组件
provide('message', { message, updateMessage })
return {
message
}
}
}
</script>


接收者:


<template>
<div>
<h2>我是接收者组件</h2>
<p>消息是:{{ message }}</p>
<button @click="updateMessage">更改消息</button>
</div>

</template>

<script>
import { inject } from 'vue'

export default {
name: 'ChildComponent',
setup() {
// 接收提供者组件提供的响应式对象和方法
const { message, updateMessage } = inject('message')
return {
message,
updateMessage
}
}
}
</script>


三、使用readonly包装



通过readonly包装provide的数据,可以防止接收者组件修改数据,保证数据流的一致性。



提供者:


<template>
<div>
<h1>我是提供者组件</h1>
<p>姓名是:{{ name }}</p>
<child-component></child-component>
</div>

</template>

<script>
import ChildComponent from './ChildComponent.vue'
import { ref, provide, readonly } from 'vue'

export default {
name: 'ProviderComponent',
components: {
ChildComponent
},
setup() {
// 使用ref创建一个响应式的姓名
const name = ref('Alice')
// 使用readonly包装提供的值,使其不可修改
provide('name', readonly(name))
return {
name
}
}
}
</script>


接收者:


<template>
<div>
<h2>我是接收者组件</h2>
<p>姓名是:{{ name }}</p>
<button @click="name = 'Bob'">尝试修改姓名</button>
</div>

</template>

<script>
import { inject } from 'vue'

export default {
name: 'ChildComponent',
setup() {
// 接收提供者组件提供的只读对象
const name = inject('name')
return {
name
}
}
}
</script>


四、使用<script setup>



<script setup>组合式写法下,provide和inject默认就是响应式的,无需额外处理。



总结



依赖注入的方式共享数据在Vue中是一种高级特性,它主要用于开发插件或库,或者处理一些特殊的场景。



作者:Yoo前端
来源:juejin.cn/post/7329830481722294272
收起阅读 »

你还在使用websocket实现实时消息推送吗?

web
前言 在日常的开发中,我们经常能碰见服务端需要主动推送给客户端数据的业务场景,比如数据大屏的实时数据,比如消息中心的未读消息,比如聊天功能等等。 本文主要介绍SSE的使用场景和如何使用SSE。 服务端向客户端推送数据的实现方案有哪几种? 我们常规实现这些需求...
继续阅读 »

前言


在日常的开发中,我们经常能碰见服务端需要主动推送给客户端数据的业务场景,比如数据大屏的实时数据,比如消息中心的未读消息,比如聊天功能等等。


本文主要介绍SSE的使用场景和如何使用SSE。


image.png


服务端向客户端推送数据的实现方案有哪几种?


我们常规实现这些需求的方案有以下三种



  1. 轮询

  2. websocket

  3. SSE


轮询简介


在很久很久以前,前端一般使用轮询来进行服务端向客户端进行消息的伪推送,为什么说轮询是伪推送?因为轮询本质上还是通过客户端向服务端发起一个单项传输的请求,服务端对这个请求做出响应而已。通过不断的请求来实现服务端向客户端推送数据的错觉。并不是服务端主动向客户端推送数据。显然,轮询一定是上述三个方法里最下策的决定。


轮询的缺点:



  1. 首先轮询需要不断的发起请求,每一个请求都需要经过http建立连接的流程(比如三次握手,四次挥手),是没有必要的消耗。

  2. 客户端需要从页面被打开的那一刻开始就一直处理请求。虽然每次轮询的消耗不大,但是一直处理请求对于客户端来说一定是不友好的。

  3. 浏览器请求并发是有限制的。比如Chrome 最大并发请求数目为 6,这个限制还有一个前提是针对同一域名的,超过这一限制的后续请求将会被阻塞。而轮询意味着会有一个请求长时间的占用并发名额

  4. 而如果轮询时间较长,可能又没有办法非常及时的获取数据


websocket简介


websocket是一个双向通讯的协议,他的优点是,可以同时支持客户端和服务端彼此相互进行通讯。功能上很强大。


缺点也很明显,websocket是一个新的协议,ws/wss。也就是说,支持http协议的浏览器不一定支持ws协议。


相较于SSE来说,websocket因为功能更强大。结构更复杂。所以相对比较


websocket对于各大浏览器的兼容性↓
image.png


SSE简介


sse是一个单向通讯的协议也是一个长链接,它只能支持服务端主动向客户端推送数据,但是无法让客户端向服务端推送消息。


长链接是一种HTTP/1.1的持久连接技术,它允许客户端和服务器在一次TCP连接上进行多个HTTP请求和响应,而不必为每个请求/响应建立和断开一个新的连接。长连接有助于减少服务器的负载和提高性能。

SSE的优点是,它是一个轻量级的协议,相对于websockte来说,他的复杂度就没有那么高,相对于客户端的消耗也比较少。而且SSE使用的是http协议(websocket使用的是ws协议),也就是现有的服务端都支持SSE,无需像websocket一样需要服务端提供额外的支持。


注意:IE大魔王不支持SSE


SSE对于各大浏览器的兼容性↓
image.png


注意哦,上图是SSE对于浏览器的兼容不是对于服务端的兼容。


websocket和SSE有什么区别?


轮询


对于当前计算机的发展来说,几乎很少出现同时不支持websocket和sse的情况,所以轮询是在极端情况下浏览器实在是不支持websocket和see的下策。


Websocket和SSE


我们一般的服务端和客户端的通讯基本上使用这两个方案。首先声明:这两个方案没有绝对的好坏,只有在不同的业务场景下更好的选择。


SSE的官方对于SSE和Websocket的评价是



  1. WebSocket是全双工通道,可以双向通信,功能更强;SSE是单向通道,只能服务器向浏览器端发送。

  2. WebSocket是一个新的协议,需要服务器端支持;SSE则是部署在HTTP协议之上的,现有的服务器软件都支持。

  3. SSE是一个轻量级协议,相对简单;WebSocket是一种较重的协议,相对复杂。

  4. SSE默认支持断线重连,WebSocket则需要额外部署。

  5. SSE支持自定义发送的数据类型。


Websocket和SSE分别适用于什么业务场景?


对于SSE来说,它的优点就是轻,而且对于服务端的支持度要更好。换言之,可以使用SSE完成的功能需求,没有必要使用更重更复杂的websocket。


比如:数据大屏的实时数据,消息中心的消息推送等一系列只需要服务端单方面推送而不需要客户端同时进行反馈的需求,SSE就是不二之选。


对于Websocket来说,他的优点就是可以同时支持客户端和服务端的双向通讯。所适用的业务场景:最典型的就是聊天功能。这种服务端需要主动向客户端推送信息,并且客户端也有向服务端推送消息的需求时,Websocket就是更好的选择。


SSE有哪些主要的API?


建立一个SSE链接 :var source = new EventSource(url);

SSE连接状态


source.readyState



  • 0,相当于常量EventSource.CONNECTING,表示连接还未建立,或者连接断线。

  • 1,相当于常量EventSource.OPEN,表示连接已经建立,可以接受数据。

  • 2,相当于常量EventSource.CLOSED,表示连接已断,且不会重连。


SSE相关事件



  • open事件(连接一旦建立,就会触发open事件,可以定义相应的回调函数)

  • message事件(收到数据就会触发message事件)

  • error事件(如果发生通信错误(比如连接中断),就会触发error事件)


数据格式


Content-Type: text/event-stream //文本返回格式
Cache-Control: no-cache //不要缓存
Connection: keep-alive //长链接标识

image.png


SSE:相关文档,文档入口文档入口文档入口文档入口


显然,如果直接看api介绍不论是看这里还是看官网,大部分同学都是比较懵圈的状态,那么我们写个demo来看一下?


image.png


demo请看下方


我更建议您先把Demo跑起来,然后在看看上面这个w3cschool的SSE文档。两个配合一起看,会更方便理解些。


image.png


如何实操一个SSE链接?Demo↓


这里Demo前端使用的就是最基本的html静态页面连接,没有使用任何框架。
后端选用语言是node,框架是Express。


理论上,把这两段端代码复制过去跑起来就直接可以用了。



  1. 第一步,建立一个index.html文件,然后复制前端代码Demo到index.html文件中,打开文件

  2. 第二步,进入一个新的文件夹,建立一个index.js文件,然后将后端Demo代码复制进去,然后在该文件夹下执行


npm init          //初始化npm       
npm i express //下载node express框架
node index //启动服务

image.png


在这一层文件夹下执行命令。


完成以上操作就可以把项目跑起来了


前端代码Demo


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<ul id="ul">

</ul>
</body>
<script>

//生成li元素
function createLi(data){
let li = document.createElement("li");
li.innerHTML = String(data.message);
return li;
}

//判断当前浏览器是否支持SSE
let source = ''
if (!!window.EventSource) {
source = new EventSource('http://localhost:8088/sse/');
}else{
throw new Error("当前浏览器不支持SSE")
}

//对于建立链接的监听
source.onopen = function(event) {
console.log(source.readyState);
console.log("长连接打开");
};

//对服务端消息的监听
source.onmessage = function(event) {
console.log(JSON.parse(event.data));
console.log("收到长连接信息");
let li = createLi(JSON.parse(event.data));
document.getElementById("ul").appendChild(li)
};

//对断开链接的监听
source.onerror = function(event) {
console.log(source.readyState);
console.log("长连接中断");
};

</script>
</html>

后端代码Demo(node的express)


const express = require('express'); //引用框架
const app = express(); //创建服务
const port = 8088; //项目启动端口

//设置跨域访问
app.all("*", function(req, res, next) {
//设置允许跨域的域名,*代表允许任意域名跨域
res.header("Access-Control-Allow-Origin", '*');
//允许的header类型
res.header("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With");
//跨域允许的请求方式
res.header("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS");
// 可以带cookies
res.header("Access-Control-Allow-Credentials", true);
if (req.method == 'OPTIONS') {
res.sendStatus(200);
} else {
next();
}
})

app.get("/sse",(req,res) => {
res.set({
'Content-Type': 'text/event-stream', //设定数据类型
'Cache-Control': 'no-cache',// 长链接拒绝缓存
'Connection': 'keep-alive' //设置长链接
});

console.log("进入到长连接了")
//持续返回数据
setInterval(() => {
console.log("正在持续返回数据中ing")
const data = {
message: `Current time is ${new Date().toLocaleTimeString()}`
};
res.write(`data: ${JSON.stringify(data)}\n\n`);
}, 1000);
})

//创建项目
app.listen(port, () => {
console.log(`项目启动成功-http://localhost:${port}`)
})

效果


动画3.gif


总结



  1. SSE比websocket更轻

  2. SSE是基于http/https协议的

  3. websocket是一个新的协议,ws/wss协议

  4. 如果只需要服务端向客户端推送消息,推荐使用SSE

  5. 如果需要服务端和客户端双向推送,请选择websocket

  6. 不论是SSE还是websocket,对于浏览器的兼容性都不错

  7. 轮询是下策,很占用客户端资源,不建议使用。(不过偷懒的时候他确实方便)

  8. IE不支持SSE

  9. 小白同学demo如果跑不明白可以私信我


对了,小程序不支持SSE哦


image.png


最后


如果文章对您有帮助的话。


image.png


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

实现一个鼠标框选的功能,要怎么实现和设计 api?

web
前言 前两年在一家做电商的公司做了一个需求:鼠标框选商品卡片,开始拖拽的时候合成一个然后改变位置,页面上有几千个所以还要结合虚拟列表。当时不知道怎么做,就在 github 上到处找现成的库,最后找到了 react-selectable-fast,并结合 rea...
继续阅读 »

285330798-9d463acf-c56b-48d8-b7d5-2dc02b4257e0.gif


前言


前两年在一家做电商的公司做了一个需求:鼠标框选商品卡片,开始拖拽的时候合成一个然后改变位置,页面上有几千个所以还要结合虚拟列表。当时不知道怎么做,就在 github 上到处找现成的库,最后找到了 react-selectable-fast,并结合 react-virtualizedreact-sortable-hoc 完成了需求。虽然该库已经很久不维护了,但大致上能满足我的需求了,尽管它是以 dom 的方式,很不 react,但秉承着能用就行的原则。意料之中,开发过程中遇到了 bug,最后只能 fork 一份修改源码后自己发了个 npm 包来使用。


项目介绍


前几个月在空闲时间突然来了兴致,自己找点事做,就想自己开发一个框选的库吧,万一也有人有这个需求不知道怎么办呢?写完后发到了 antd 的社区共建群里,有的人觉得不错也 star 了。先献上项目地址 react-selectable-box,文档完整,使用 dumi 编写。api 友好,支持自定义一些功能。


api 设计


一个组件在设计时,首先思考的应该是 api 如何去设计,最好符合大家平常的习惯,并具有一定的自定义和拓展能力。再加上了解 react-selectable-fast 这个库的缺点和痛点,对我的设计就更加有帮助了。大家在看下面的文章之前也可以思考一下,如果是你,你会怎么设计?这里只选取几个 api 来进行介绍。


主组件 Selectable


选中的值


defaultValuevalue,类型为 any[],每个方块一般都有一个唯一 id 来标识,2024-1-31 更新后 后支持任意类型,因为考虑到很多情况你可能需要一个对象或数组来标识,文章后面提供了 compareFn 来自定义比较值相等。


禁用


disabled,大部分有值的组件应该都会有此属性,能直接禁用框选功能。


模式


mode,类型为 "add" | "remove" | "reverse"。模式,表明当前框选是增加、减少还是取反。这个 api 感觉是设计的最好的,用户会框选来选择目标,肯定也会需要删除已经框选的目标,可能是按住 shift 来删除等等之类的操作。用户可以自己编写自定义逻辑来修改 mode 的值来控制不同的行为,反观 react-selectable-fast,则是提供了 deselectOnEscallowAltClickallowCtrlClickallowMetaClickallowShiftClick 等多个 api。


开始框选的条件


selectStartRange,类型 "all" | "inside" | "outside",鼠标从方块内部还是外部可以开始框选,或都可以。


可以进行框选的容器


dragContainer,类型 () => HTMLElement,例如你只希望某个卡片内才可以进行框选,不希望整个页面都可以进行框选,这个 api 就会起到作用了。


滚动的容器


scrollContainer,类型 () => HTMLElement,如果你的这些方块是在某个容器中并且可滚动,就需要传入这个属性,就可以在滚动的容器中进行框选操作了。


框选矩形的 style 与 className


boxStyleboxClassName,使用者可以自定义颜色等一些样式。


自定义 value 比较函数


compareFn,类型 (a: any, b: any) => boolean,默认使用 === 进行比较(因为 value 支持任意类型,比如你使用了对象或数组类型,所以你可能需要自定义比较)


框选开始事件


onStart,框选开始时,使用者可能需要做一些事情。


框选结束事件


onEnd,类型为 (selectingValue: (string | number)[], { added: (string | number)[], removed: (string | number)[] }) => voidselectingValue 为本次框选的值,added 为本次增加的值,removed 为本次删除的值。例如你想在每次框选后覆盖之前的操作,直接设置 selectingValue 成 value 即可。如果你想每次框选都是累加,加上 added 的值即可,这里就不再说明了。


方块可选 - useSelectable


怎么让方块可以被选择呢?并且一一绑定上对应的值?react-selectable-fast 则是提供 clickableClassName api,传入可以被选择的目标的 class,这种方式太不 react 了。此时我的脑海里想到了 dnd-kit,我认为是 react 最好用的拖拽库,它是怎么让每个方块可以被拖拽的呢?优秀的东西应该借鉴,于是就有了 useSelectable


const { 
setNodeRef, // 设置可框选元素
isSelected, // 是否已经选中
isAdding, // 当前是否正在添加
isRemoving, // 当前是否正在删除
isSelecting, // 当前是否被框选
isDragging // 是否正在进行框选操作
} = useSelectable({
value, // 每个元素的唯一值,支持任意类型
disabled, // 这个元素是否禁用
rule, // "collision" | "inclusion" | Function,碰撞规则,碰到就算选中还是全部包裹住才算选中,也可以自定义
});

如何使用?


const Item = ({ value }: { value: number }) => {
const { setNodeRef, isSelected, isAdding } = useSelectable({
value,
});

return (
<div
ref={setNodeRef}
style={{
width: 50,
height: 50,
borderRadius: 4,
border: isAdding ? '1px solid #1677ff' : undefined,
background: isSelected ? '#1677ff' : '#ccc',
}}
/>

);
};

实现


这里只简单讲一下思路,有兴趣的同学可以直接前往源码进行阅读。


主组件 Selectable 相当于一个 context,一些状态在这里进行保存,并掌管每个 useSelectable,将其需要的值通过 context 传递过去。


在设置的可被框选的容器内监听鼠标 mousedown 事件,记录其坐标,根据 mousemove 画出框选矩形,再根据 setNodeRef 收集的元素和框选矩形根据碰撞检测函数计算出是否被框选了,并将值更新到 Selectable 中去,最后在 mouseup 时触发 onEnd,将值处理完之后并丢出去。


演示


这里演示一下文章开头所说的框选拖拽功能,配合 dnd-kit 实现,代码在文档的 example 中。
录屏2024-01-23 19.27.43.gif


遇到的坑


这里分享一下遇到的坑的其中之一:框选的过程中会选中文字,很影响体验,怎么让这些文字不能被框选呢?


方案1: 用 user-select: none 来控制文本不可被选中,但是这是在用户侧来做,比较麻烦。并且发现在 chrome 下设置此属性后,拖拽框选到浏览器边缘或容器边缘后不会自动滚动,其它浏览器则正常


方案2: 在 mousedown 时设置 e.preventDefault(),这样选中时文字就不会被选中,但是拖拽框选到浏览器边缘或容器边缘后不会自动滚动,只能自己实现了滚动逻辑。后面又发现在移动端的 touchstart 设置时,会发现页面上的点击事件都失效了,查资料发现没法解决,只能另辟蹊径。


方案3: 在 mousemovetouchmove 时设置 e.preventDefault() 也是可以的,但也需要自己实现滚动逻辑。


最终也是采取了方案3。


后续目标


目前只能进行矩形的碰撞检测,不支持圆形(2024.1.26 更新支持自定义已经可以实现)及一些不规则图形(2024.1.26 更新提供自定义碰撞检测(dom 下太难,canvas 比较好做碰撞检测),剩下的就是使用者的事了!)。这是一个难点,如果有想法的可以在评论区提出或者 pr 也可。


2024-1-24 更新


添加 cancel 方法,试一试。可以调用 ref.current?.cancel() 方法取消操作。这样可以自定义按下某个键来取消当前操作。有想需不需要添加一个属性传入 keyCode 数组内置取消,但是感觉会使 api 太多而臃肿,也欢迎留下你的想法。


2024-1-26 更新一


添加 items api 以优化虚拟滚动滚动时框选范围增加或减小时,已经卸载的 Item 的判断框选。(可选)试一试


优化前:滚动到下面时,加大框选面积,上面已经被卸载的不会被选中


录屏2024-01-26 16.50.31.gif


优化后:滚动到下面时,加大框选面积,上面已经被卸载的会被选中


录屏2024-01-26 16.53.36.gif


2024-1-26 更新二


支持自定义碰撞规则检测,试一试自定义圆形碰撞检测
录屏2024-01-26 17.41.37.gif


2024-1-31 更新


value 支持任意类型 any,不再只是 string | number 类型,因为很多情况需要是一个对象或数组来当唯一标识,并提供了 compareFn 来支持自定义值的比较,默认使用 ===,如果你的 value 是对象或数组,需要此属性来比较值。


总结


开发一个较为复杂的组件,可以提交自己的 api 设计能力和解决问题的能力,可以将平常所学习、所了解、所使用的东西取其精华运用起来。最后希望这个组件能帮助到有需要的人,欢迎大家提出建议!有 issues 才能维护下去!如果觉得不错,帮忙点个 star 吧,地址 react-selectable-box


作者:马格纳斯
来源:juejin.cn/post/7326979670485123110
收起阅读 »

01CSS 实现多行文本“展开收起”

web
最近在开发移动端的评论内容功能时,我遇到了一个需求,需要实现一个展开收起效果。主要目的是让用户能够方便地查看和隐藏评论内容,现在想将我的成果分享给大家 完成效果: 实现思路: 1.准备一个文本的外部容器( content),并将最大高度设置为65px(根据...
继续阅读 »

最近在开发移动端的评论内容功能时,我遇到了一个需求,需要实现一个展开收起效果。主要目的是让用户能够方便地查看和隐藏评论内容,现在想将我的成果分享给大家



完成效果:


展开收起.gif


实现思路:


1.准备一个文本的外部容器( content),并将最大高度设置为65px(根据实际需求而定),超出内容设置不可见


image.png


2.文本容器的高度(text-content)不做样式设置,这个容器是为了获取内容实际高度


image.png


3.通过 js 获取文本容器的高度(text-content),判断文本高度是否超过外部容器(content)的最大高度,控制展开收起按钮是否显示


4.点击按钮时根据条件设置容器(content)的最大高度,css 对通过 transition 对 max-height 设置过渡效果


完整示例代码如下


HTML



<div class="container">
<div class="content">
<div class="text-content">
1月30日上午10时,中国贸促会将召开1月例行新闻发布会,介绍第二届链博会筹备进展情况;
2025大阪世博会中国馆筹备进展;2023年全国贸促系统商事认证数据;2023年贸法通运行情况;
2023年11月全球经贸摩擦指数;2023年12月全球知识产权保护指数月度观察报告;助力培育外贸新动能有关工作考虑等。
</div>
</div>
<button class="btn">展开</button>
</div>


CSS



.container {
width: 260px;
padding: 20px;
border: 1px solid #ccc;
margin: 50px auto;
}

.content {
max-height: 65px;
overflow: hidden;
transition: max-height 0.5s;
}


.btn {
display: flex;
width: 40px;
color: cornflowerblue;
outline: none;
border: none;
background-color: transparent;
}



JS


    const maxHeight=65
const btn = document.querySelector('.btn')
const content = document.querySelector('.content')
const textContent=document.querySelector('.text-content')
const textHeight=textContent.getBoundingClientRect().height // 文本高度
const contentHeight=content.getBoundingClientRect().height // 容器高度
let flag = false
if (textHeight < maxHeight) {
btn.style.display = 'none'
}
btn.addEventListener('click', () => {
if (!flag) {
content.style.maxHeight=textHeight+'px'
btn.innerHTML = '收起'
flag = true
} else {
content.style.maxHeight=contentHeight+'px'
btn.innerHTML = '展开'
flag = false
}
})



实现一个功能的方式往往有多种,你们是怎么解决的呢?


作者:前端小山
来源:juejin.cn/post/7329694104118919195
收起阅读 »

浏览器关闭实现登出(后端清token)

web
实现浏览器关闭后,后端记录用户登出日志的最佳方式是通过前端发送请求来通知后端进行记录。以下是一种常见的实现方式,重点就是如何区分用户行为是页面刷新还是关闭浏览器。 // 写在APP.vue mounted() { window.addEventLi...
继续阅读 »

实现浏览器关闭后,后端记录用户登出日志的最佳方式是通过前端发送请求来通知后端进行记录。以下是一种常见的实现方式,重点就是如何区分用户行为是页面刷新还是关闭浏览器。


// 写在APP.vue
mounted() {
window.addEventListener("beforeunload", () => this.beforeunloadHandler())
window.addEventListener("unload", () => this.unloadHandler())
},

destroyed() {
window.removeEventListener("beforeunload", () => this.beforeunloadHandler())
window.removeEventListener("unload", () => this.unloadHandler())
clearInterval(this.timer)
},

methods:{
beforeunloadHandler() {
this.beforeUnloadTime = new Date().getTime()
},
unloadHandler() {
this.gapTime = new Date().getTime() - this.beforeUnloadTime
if (this.gapTime <= 5) { //判断是窗口关闭还是刷新,小于5代表关闭,否则就是刷新。
// 这里是关闭浏览器
logout()
}
},
}


但是经测试,发现上面这种浏览器关闭事件并不是一种可靠的方式来捕捉用户的登出操作,后端并非百分百接收到logout请求,经查资料得知,在unload阶段发送的异步请求是不可靠的,有可能被cancel。后面又尝试了fetch,设置了keepalive(即使浏览器关闭,请求照样发送), 但是又发现gapTime<=5的判断条件也存在兼容性问题,不同浏览器的时间差存在差异。此外还存在一些特殊情况:用户可能直接关闭浏览器窗口、断开网络连接或发生其他异常情况,导致浏览器关闭事件无法被触发,因此pass掉上述方案。


后面也尝试了心跳机制(websocket),也存在局限性,pass。


最后想到了一种最简单,最朴实的方式:
开启定时器每秒往localStorage写入当前时间lastRecordTime(new Date().getTime()), 在请求拦截器中给每个接口请求头带上两个时间,最后一次写入时间lastRecordTime和当前时间nowTime, 后端只要把两个时间相减, 超过5s(自定义)就算登出,清掉redis里相应的token。


// 写在APP.vue
created (){
// 每秒写入一次时间
this.timer = setInterval(() => {
// 这个判断代表登录成功后才开始写入时间
if(localStorage.getItem('token')) {
localStorage.setItem('lastRecordTime', new Date().getTime())
}
}, 1000)
}

另外需要注意, 在登录成功的地方要立即写入一次时间, 不然有BUG。


  // 写在请求拦截器
const headers = config.headers;
/** 用于判断用户是否关闭过浏览器,如果关闭则跳转至登录页面,以及及时清理redis中的token */
if (localStorage.getItem('lastRecordTime')) {
headers.lastRecordTime = localStorage.getItem('lastRecordTime');
}
headers.nowTime = new Date().getTime();

总结一下,目前没发现哪种方式可以提供一种可靠的通信方式去通知后端清除token, 通过两个时间差的方式相对靠谱。


作者:起床搬砖啦
来源:juejin.cn/post/7328221562817478665
收起阅读 »

🌟前端使用Lottie实现炫酷的开关效果🌟

web
前言 在平时的开发过程中,前端或多或少都会遇到实现动画效果的场景。手写动画是一件相当麻烦的事情,调来调去不仅费时费力,可能还会被产品/UI吐槽:这动画效果也不难呀,为什么就不能实现呢?/为什么就没有还原成我想要的样子呢。 比如说产品让我们实现这样的一个开关动...
继续阅读 »

前言


在平时的开发过程中,前端或多或少都会遇到实现动画效果的场景。手写动画是一件相当麻烦的事情,调来调去不仅费时费力,可能还会被产品/UI吐槽:这动画效果也不难呀,为什么就不能实现呢?/为什么就没有还原成我想要的样子呢。


image.png


比如说产品让我们实现这样的一个开关动效


Kapture 2024-01-20 at 21.53.34.gif


今天我们就用动画的实现方式——Lottie,来百分百还原设计师的动画效果,并且可以大大提高我们的工作效率(摸鱼时间)。


image.png


Lottie简介


首先我们先来看一下,平时我们实现动画都有哪些方式,它们分别有什么优缺点:


动画类型优点缺点
CSS 动画使用简便,通过@keyframestransition创建动画;浏览器原生支持,性能较好控制有限,不适用于复杂动画;复杂动画可能需要大量 CSS 代码,冗长
JavaScript 动画提供更高程度的控制和灵活性;适用于复杂和精细动画效果引入库增加页面负担,可能需要学习曲线;使用不当容器对页面性能造成影响,产生卡顿
GIF 动画制作和使用简单,无需额外代码;几乎所有浏览器原生支持有限颜色深度,不适用于所有场景;清晰度与文件尺寸成正比,无法适应所有分辨率
Lottie支持矢量动画,保持清晰度和流畅性 ;跨平台使用,适用于 iOS、Android 和 Web在一些较旧或性能较低的设备上,播放较大的 Lottie 动画可能会导致性能问题;对设计师要求较高

Lottie是由Airbnb开发的一个开源库,用于在移动端和Web上呈现矢量动画。它基于JSON格式的Bodymovin文件,可以将由设计师在AE中创建的动画导出为可在Lottie库中播放的文件。


相对于手写CSS/JS动画而言,它可以大大减少前端开发的工作量,相对于GIF文件来说,它可以在一个合理的文件体积内保证动画的清晰度以及流畅程度。下面我们就介绍一下如何播放一个Lottie动画,并实现一个炫酷的开关效果。


Hello Lottie


假设我们现在已经有一个Lottiejson文件,那么现在安装一些依赖


npm i react-lottie prop-types

安装完之后我们就可以这样子来播放一个Lottie动画:


import animationData from "../../assets/switch-lottie.json";

const LottieSwitch = () => {
const playing = useRef(false);
const options = {
loop: true,
autoplay: true,
animationData: animationData,
rendererSettings: {
preserveAspectRatio: "xMidYMid slice",
},
};
return (
<Lottie
options={options}
height={20}
width={40}
/>

);
};


Kapture 2024-01-20 at 21.41.20.gif


来解释一下上面的options参数里面各个字段是什么意思:



  • loop:是否循环播放

  • autoplay:是否自动播放

  • animationDataLottie动画json资源

  • rendererSettings.preserveAspectRatio:指定如何在给定容器中渲染Lottie动画

    • xMidYMid: 表示在水平和垂直方向上都在中心对齐

    • 表示保持纵横比,但可能会裁剪超出容器的部分




正/反向播放


正常的把Lottie动画播放出来之后,我们就可以开始实现一个开关的功能。其实就是点击的时候更换Lottie的播放方向,这里对应的是direction字段,direction1时正向播放,direction-1时反向播放。


我们就要实现下面的功能:



  • 点击时切换方向

  • 播放过程中加锁,禁止切换方向

  • 监听播放结束事件,解锁

  • loop改为falseautoplay改为false


实现代码如下:


const LottieSwitch = () => {
const [direction, setDirection] = useState(null);
const playing = useRef(false);
const options = {
loop: false,
autoplay: false,
animationData: animationData,
rendererSettings: {
preserveAspectRatio: "xMidYMid slice",
},
};

const handleClick = () => {
if (playing.current) {
return;
}
playing.current = true;
setDirection((prevState) => (prevState === 1 ? -1 : 1));
};
return (
<div style={{ padding: 40 }}>
<div onClick={handleClick} className={styles.lottieWrapper}>
<Lottie
direction={direction}
options={options}
speed={2}
height={20}
width={40}
eventListeners={[
{
eventName: "complete",
callback: () =>
{
playing.current = false;
},
},
]}
/>
</div>
</div>

);
};

这样我们就是实现了一个开关的效果


Kapture 2024-01-20 at 21.53.34.gif


持续时长


Lottiejson中,有几个关键的字段跟动画的播放时长有关系:



  • fr:帧率,每一秒的帧数

  • ip:开始帧

  • op:结束帧


假如说有下面的一个描述:


{
"fr": 30,
"ip": 0,
"op": 60,
}

则表示帧率是30帧,从第0帧开始,60帧结束,那这个动画的持续时长是 (op-ip)/fr,为2s。那如果我们希望整个动画的播放时长是500ms,则只需要把Lottie的倍速调整为4。对应的就是speed字段:


<Lottie
direction={direction}
options={options}
speed={4}
height={20}
width={40}
eventListeners={[
{
eventName: "complete",
callback: () => {
playing.current = false;
},
},
]}
/>

Kapture 2024-01-20 at 22.06.53.gif


修改Lottie


Lottie json中,描述整个动画的过程以及效果其实对应的就是某个值。在实现的过程中,其实开发是可以去修改这些值的。比如说我们可以修改上面开关的边框颜色以及小球的颜色。


首先在页面中找到小球对应的颜色是rgb(99, 102, 241)


image.png


Lottie JSON文件中,颜色信息通常出现在表示图层样式的字段中。常见的字段是 "c"(color)
"c" 字段表示颜色,通常以RGBA格式(红绿蓝透明度)存储。例如:


"c": {"a":0,"k":[1,0,0,1]}

这表示红色,RGBA值为 [1, 0, 0, 1]


rgb(99, 102, 241)转成上面的写法那就是"c": {"a":0,"k":[99/255,102/255,241/255,1]}。以99/255为例,结果是0.38823529411764707,那么就拿这个结果去json文件中找到对应的节点。


image.png


对应有2个结果,就是小球的颜色以及边框的颜色。当我们找到这个值的时候,如果我们想修改这个值,就必须知道这个值的路径,在一个Lottie中,想肉眼找到这个值的路径是一件很难的事情。所以我们写一个辅助函数:


const updateJsonValue = (json, targetValue, newValue) => {
const find = (json, targetValue, currentPath = []) => {
for (const key in json) {
if (json[key] === targetValue) {
return [...currentPath, key];
} else if (typeof json[key] === "object" && json[key] !== null) {
const path = find(json[key], targetValue, [...currentPath, key]);
if (path) {
return path;
}
}
}
};
const res = JSON.parse(JSON.stringify(json));
const path = find(res, targetValue);
let current = res;

for (let i = 0; i < path.length - 1; i++) {
const key = path[i];
current = current[key];
}

const lastKey = path[path.length - 1];
current[lastKey] = newValue;

return json;
};

上面的辅助函数就帮助我们找到这个值的路径,并修改目标值。比如说我们想把目前的颜色改成绿色(rgb(25, 195, 125)),就可以找到对应的路径,并修改。别忘了替换的时候把rgb对应的值除以255


let newAnimationData = updateJsonValue(animationData, 0.388235300779, 0.09803921568627451)
newAnimationData = updateJsonValue(newAnimationData, 0.388235300779, 0.09803921568627451)
newAnimationData = updateJsonValue(newAnimationData, 0.40000000596, 0.7647058823529411)
newAnimationData = updateJsonValue(newAnimationData, 0.40000000596, 0.7647058823529411)
newAnimationData = updateJsonValue(newAnimationData, 0.945098042488, 0.49019607843137253)
newAnimationData = updateJsonValue(newAnimationData, 0.945098042488, 0.49019607843137253)

image.png


掌握了这种方式之后,我们就能修改Lottie里面的大部分内容,包括文案、资源图片、颜色等等。


最后


以上就是一些Lottie的使用以及修改的介绍,下次再遇到比较麻烦的动画需求。就可以跟产品说:可以做,让UI给我导出一个Lottie


image.png


如果你有一些别的想法,欢迎评论区交流~如果你觉得有意思的话,点点关注点点赞吧~


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

从‘相信前端能做一切’到‘连这个都做不了么’

web
帮助阅读 此篇文章主要是为了实现仪表盘功能,前后过了4种方案,每篇方案从逻辑、代码、效果、问题四个方面出发。最后个人总结。同时非常非常希望有大佬能够提供一个方案,个人实在想不到实现方案了 需求 h5页面中,做一个环形仪表盘(如下图),需要一个从0%到实际百分比...
继续阅读 »

4711705568245_.pic.jpg


帮助阅读


此篇文章主要是为了实现仪表盘功能,前后过了4种方案,每篇方案从逻辑、代码、效果、问题四个方面出发。最后个人总结。同时非常非常希望有大佬能够提供一个方案,个人实在想不到实现方案了


需求


h5页面中,做一个环形仪表盘(如下图),需要一个从0%到实际百分比的增长过渡动效
未命名.png


前提


使用前端原生Html、css、js语言实现, 不打算借助第三方插件。


最初Scheme


将UI图片作为背景,上面放一个白色div作为遮罩,再利用css3将白色div旋转,从而达到过渡效果。


代码如下:


<style>
.light-strip {
width: 500px;
height:500px;
border: 1px solid #efefef;
background-image: url('Frame 29@3x.png');
float: right;
background-size: 100% 100%;
}
.light-strip-top {
margin-top: 0%;
width: 500px;
height: 250px;
background: #fff;
transform-origin: bottom center;
/* transform: rotate 5s ; */
rotate: 0deg;
transition: all 2s ease-in-out;
}
</style>
<body onload="load()">
<div class="light-strip">
<div class="light-strip-top">

</div>
</div>
</body>
<script>
function load() {
setTimeout(() => {
document.querySelectorAll('.light-strip-top')[0].setAttribute('style', "rotate: 180deg")
}, 1000)
}
</script>

效果如下:


屏幕录制2024-01-29 13.50.58.gif


出现问题:


由于仪表盘整体大于180度,所以白色div,在最开始遮挡全部仪表盘,那么旋转一定角度后一定会覆盖UI图。


进化Scheme


根据上面出现的问题,想到与UI沟通将仪表盘改成180度效果(解决不了问题,就把问题解决掉),该方案由于改变了原型之后会导致UI过于丑,就没有进行深度测试。


超进化Scheme


根据上面两个方案的结合,想到将方案1中的白色div换成一张指针图片,利用css3旋转追针,达到过渡效果,但此方案也是改了UI效果。


代码如下:


	<style>
.light-strip {
width: 500px;
height:500px;
border: 1px solid #efefef;
background-image: url('Frame 29@3x.png');
/* background-color: #fff; */
float: right;
background-size: 100% 100%;
}
.light-strip-top {
margin-top: 50%;
width: 49%;
height: 4px;
background: red;
transform-origin: center right;
/* transform: rotate 5s ; */
rotate: -35deg;
transition: all 2s ease-in-out;
}

</style>
<body onload="load()">
<div class="light-strip">
<div class="light-strip-top">

</div>
</div>
</body>
<script>
function load() {
setTimeout(() => {
document.querySelectorAll('.light-strip-top')[0].setAttribute('style', "rotate: 90deg")
}, 1000)
}
</script>

效果如下:


屏幕录制2024-01-29 15.44.31.gif


Now:


此时大脑宕机了,在我的前端知识基础上,想不到能够完美实现UI效果的方案了。于是和同事探讨了一下,了解到element-plus中的进度条有类似的效果,于是打算看一下源码,了解到它是svg实现的。发现新大陆又开始尝试svg实现。


究极进化Scheme


利用svg,做一个带白色的背景圆环A,再做一个带有渐变背景色的进度圆环B, 利用进度圆环的偏移值、显示长度、断口长度配合css3过渡实现过渡效果。


代码如下:


 <style>
body {
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
margin: 0;
background-color: #f5f5f5;
}

.dashboard {
position: relative;
width: 200px;
height: 200px;
background-size: 100% 100%;
}

.circle-background {
fill: none; /* 不填充 */
stroke: #fff; /* 圆环的颜色 */
stroke-width: 10; /* 圆环的宽度 */
stroke-dasharray: 200, 52; /* 圆环断开部分的长度,总长度为周长 */
stroke-dashoffset: 163;
stroke-linecap: round;
border-radius: 10;
transition: all 1s; /* 过渡效果时间 */
}

.circle-progress {
fill: none; /* 不填充 */
stroke: url(#gradient); /* 圆环的颜色 */
stroke-width: 10; /* 圆环的宽度 */
stroke-dasharray: 252, 0; /* 圆环断开部分的长度,总长度为周长 */
stroke-dashoffset: 163;
stroke-linecap: round; /* 圆滑断点 */
transition: all 1s; /* 过渡效果时间 */
}

.percentage {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 24px;
color: #3498db;
}
</style>
</head>
<body>

<svg class="dashboard" viewBox="0 0 100 100">
<!-- 定义渐变色 -->
<defs>
<linearGradient id="gradient" gradientUnits="userSpaceOnUse" x1="50" y1="0" x2="50" y2="100%">
<stop offset="0%" style="stop-color: rgba(111, 232, 191, 1)" />
<stop offset="33%" style="stop-color: rgba(255, 175, 19, 1)" />
<stop offset="70%" style="stop-color: rgba(222, 19, 80, 1)" />
<stop offset="100%" style="stop-color: rgba(133, 14, 205, 1)" />
</linearGradient>
</defs>

<!-- 背景圆环 -->
<circle class="circle-background" cx="50" cy="50" r="40"></circle>

<!-- 进度圆环 -->
<circle class="circle-progress" cx="50" cy="50" r="40"></circle>

</svg>

<!-- 进度百分比显示 -->
<div class="percentage" id="percentage">0%</div>

<script>
function setProgress(percentage) {
const circleProgress = document.querySelector('.circle-progress');
const circleBackground = document.querySelector('.circle-background');
const percentageText = document.getElementById('percentage');

const circumference = 2 * Math.PI * 40; // 圆的周长
const circumNewLength = (percentage / 100) * (circumference - 52);
const dashOffset = 163 - circumNewLength;


// 设置进度圆环的样式
circleBackground.style.strokeDashoffset = dashOffset;
circleBackground.style.strokeDasharray = `${200 - circumNewLength}, ${ 52 + circumNewLength }`
circleProgress.style.strokeDasharray = `${circumNewLength}, ${ circumference - circumNewLength }`

// 更新百分比文本
percentageText.textContent = `${percentage}%`;
}

// 设置初始进度为0%
setProgress(0);

// 模拟过渡效果,从0%到50%
setTimeout(() => {
setProgress(50);
}, 1000); // 过渡时间为1秒,你可以根据需要调整这个值
</script>


效果如下:


屏幕录制2024-01-29 15.46.35.gif


问题:


基本实现,但是还有一个问题是,渐变色是两点之间的线性渐变,无法做到圆环的顺时针渐变。


总结



  • 单纯前端不是万能的😂😂😂😂

  • 个人认为这个需求还是能够实现的

  • 希望有da lao能出个方案

  • 加油,继续搞


作者:Otway
来源:juejin.cn/post/7329310941106356275
收起阅读 »

伪指纹浏览器开发的那些事

web
什么是伪指纹浏览器开发 就是通过开源的chromium浏览器进行二次简单的封装不涉及到重新编译chromium,配合puppeteer进行轻微的指纹修改开发 一、如何操作 本次操作客户端以前端擅长的electron来举例子,至于electron是什么,打开文心...
继续阅读 »

什么是伪指纹浏览器开发


就是通过开源的chromium浏览器进行二次简单的封装不涉及到重新编译chromium,配合puppeteer进行轻微的指纹修改开发


一、如何操作


本次操作客户端以前端擅长的electron来举例子,至于electron是什么,打开文心一言看看...


第一步下载chromium到本地客户端


登录官网,看到如下界面


image.png


可以发现箭头处指定是浏览器对应的版本buildId和系统,这里可以直接手动点击下载到本地,也可以通过@puppeteer/browsers这个库使用js代码去下载。这里说说如何使用它下载


const { app } = require('electron')
const browserApi = require('@puppeteer/browsers')
const axios = require('axios')

// browser缓存路径,避免和electron一起打包占用安装包体积和打包时间
const cacheDir = `${app.getPath('cache')}/myBrowser`

browserApi.install({
cacheDir, // 自己想要下载的路径,用来给puppeteer去调用
browser: browserApi.Browser.CHROMIUM,
// buildId: '1247373',
// baseUrl: 'https://commondatastorage.googleapis.com/chromium-browser-snapshots'
})

耐心的小伙伴肯定发现了这里buildId版本号和baseUrl下载url我打了注释,是因为@puppeteer/browsers默认下载的chromium版本比较旧,那么我们怎么获取这个最新版本buildId和baseUrl呢,还是官网那个界面打开控制台,可以看到如下请求链接


image.png
然后看到请求结果
image.png
这就是最新的buildId了,然后封装成函数调用


// 获取最新的chromium构建ID
function getLastBuildId(platform) {
return axios
.get(
`https://download-chromium.appspot.com/rev/${browserApi.BrowserPlatform.MAC}?type=snapshots`
)
.then((res) => res.data.content)
}

baseUrl可以在界面点击下载时候,看到控制台有一个请求,那就是baseUrl了


image.png
下载好后,可以去我们定义的下载保存地址,通过终端去打开就可以看到了


二、第二步启动chromium


使用puppeteer-core这个库,启动我们下好的chromium


const puppeteer = require('puppeteer-core')
const browserApi = require('@puppeteer/browsers')

// browser缓存路径
const cacheDir = `${app.getPath('cache')}/myBrowser`

// 获取安装的浏览器路径
function getBrowserPath() {
return browserApi
.getInstalledBrowsers({ cacheDir })
.then((list) => list[0]?.executablePath)
}

// 浏览器生成
const createBrowser = async (proxyServer, userAgent) => {
const browser = await puppeteer.launch({
args: [
`--proxy-server=${proxyServer}`,
`--user-agent="${userAgent}"`,
'--no-first-run',
'--no-zygote',
'--disable-extensions',
'--disable-infobars',
'--disable-automation',
'--no-default-browser-check',
'--disable-device-orientation',
'--disable-metrics-reporting',
'--disable-logging'
],
headless: false,
defaultViewport: null,
ignoreHTTPSErrors: true,
ignoreDefaultArgs: [
'--enable-infobars',
'--enable-features=NetworkService,NetworkServiceInProcess',
'--enable-automation',
'about:blank'
],
executablePath: await getBrowserPath()
})

return browser
}

通过puppeteer.launch启动一个浏览器,至于启动参数这里我只说指纹相关的两个参数--proxy-server--user-agent,其他AI一下。


--proxy-server代理服务,浏览器访问的出口IP,即你用自己启动的浏览器访问google时候,那边服务端获取的ip就是你的代理ip,测试时候可以自己在另外一台机器上装个Squid测试。--user-agent即浏览器的window.navigator.userAgent,简单指纹一般都是依赖于它生成


三、开发过程中用到的功能点


看完puppeteer官网,我们知道操作chromium依赖于一套协议chromedevtools.github.io/devtools-pr…


3.1 更换dock图标


比如多开浏览器,我如何更换chromium的桌面dock图标,去标识这是我启动的第几个浏览器。我们可以使用Browser.setDockTile去操作浏览器更换dock图标


const pages = await browser.pages()
const page = pages[0]
const session = await pages[0].target().createCDPSession()
await session.send('Browser.setDockTile', {
image: new Buffer.from(fs.readFileSync(file)).toString('base64')
})

效果如下:


image.png


更多的协议操作需要自己摸索了,提示下,AI搜索chrome cdp协议


3.2 增加默认书签


这里我没找到协议,直接通过类似爬虫的方式,先进入标签管理页面,直接操作js新增,也算是一个技巧性的骚操作


await page.goto('chrome://bookmarks/') // 进入标签管理页面
await page.evaluate(async () => {
// 类似在控制台直接操作一样,下面的代码控制台一样可以达到效果
const defaultBookmarks = [
{
title: "文心一言",
url: "https://yiyan.baidu.com/",
},
{
title: "掘金",
url: "https://juejin.cn/",
},
];

defaultBookmarks.forEach((item) => {
chrome.bookmarks.create({
parentId: "1",
...item,
});
});
});
await page.goto('自己的本来要跳的首页')

3.3 如何使用已经打开的浏览器


const browserWSEndpoint = browser.wsEndpoint() // 获取本次打开的浏览器链接,留作下一次使用
// 保存下来, 比如直接存在一个变量map中,给它定义一个唯一的browserId,下一次好直接获取
browserMap.set(browserId, browserWSEndpoint)

...
// 再次打开新页面,要用到上一次打开的浏览器
const browser = puppeteer.connect({
...launchOptions, // 和自己首次打开浏览器的配置一样
browserWSEndpoint: browserMap.set(browserId)
})

这样就可以使用之前打开的浏览器打开网页了


3.4 如何把浏览器的信息显示在网页上


比如代理、userAgent、地区、浏览器名称等信息,先写个页面,然后轮询从localStorage直到获取信息为止。


// 浏览器代理信息页
await page.goto('浏览器信息页')
// 设置localStorage
await page.evaluate(
(values) => {
window.localStorage.setItem('browserInfo', values)
},
JSON.stringify(browserData)
)

page在打开页面后,并不会在页面中马上能获取到这里注入的browserInfo,可以通过轮询方式去扫描localStorage中是否存在我们注入的变量,这里举个react中的例子,在页面ready后去轮询处理


useEffect(() => {
let loopId = null
const clearLoop = () => {
loopId && clearTimeout(loopId)
}

// 轮询直到获取browserInfo
const loop = () => {
loopId = setTimeout(() => {
const localData = window.localStorage.getItem('browserInfo')
if (localData) {
Promise.resolve()
.then(() => {
setInfo(JSON.parse(localData))
})
.catch(() => {
message.error('获取浏览器信息失败')
})
} else {
loop()
}
}, 1500)
}

loop()

return () => {
clearLoop()
}
})

3.5 校验代理


一般的代理服务为了不让别人也能用都会加上账密校验,所以我们还需要在启动后,调用方法去校验


// 校验proxy
if (proxyData.proxyServer) {
await page.authenticate({
username: proxyData.proxyUser,
password: proxyData.proxyPwd
})
}

四、遇到了哪一些问题


4.1 mac下关闭浏览器关不掉


当我们点击左上角关闭浏览器按钮或者是关闭所有页面时候,底部的dock中依旧存在着,我们不希望像mac其他软件一样保留在dock中,不然下一次打开浏览器时候,会出现相同标识的浏览器,可以这么解决


// 每次页面关闭时候,查看浏览器是不是还有页面了,没有就关闭
browser.on('targetdestroyed', async () => {
const pages = await browser.pages()
if (!pages.length) {
await browser.close()
}
})

4.2 当我们之间关闭电脑屏幕时候,比如盖上电脑,再次打开时候,关闭不了浏览器


打上log,可以发现熄屏时候,会触发puppeteer定义的browser的disconnected事件,但是再次打开电脑时候浏览器是可以正常使用的,也就是说,puppeteer和我们打开的chromium断连了,所以我们需要在disconnected事件里再此尝试链接下chromium,如果不行才认为是浏览器被关闭了


browser.on('disconnected', () => {
const cacheData = browserMap.get(browserId)
puppeteer
.connect({
...launchOptions,
browserWSEndpoint: cacheData.browserWSEndpoint
})
.then((newBrowser) => {
browser = newBrowser
log.info(
'browser disconnected but browser is exist',
)
initEvent()
})
.catch((err) => {
log.info(
'browser disconnected success',
)
})
})

结语


puppeteer很强大,chromium也强大,就是那个官网文档啊,写的真是让人...,所以多问问AI吧


作者:柠檬阳光
来源:juejin.cn/post/7327642905245433891
收起阅读 »

【干货】一文掌握JavaScript检查对象空值的N种技巧!

在开发 JavaScript 应用程序时,经常需要检查对象是否为空。这是因为在处理和操作对象数据时,我们需要确保对象包含有效的值或属性。以下是一些常见情况,我们需要检查 JavaScript 对象是否为空:防止空引用错误:当我们尝试访问或使用一个空对象时,可能...
继续阅读 »

在开发 JavaScript 应用程序时,经常需要检查对象是否为空。这是因为在处理和操作对象数据时,我们需要确保对象包含有效的值或属性。以下是一些常见情况,我们需要检查 JavaScript 对象是否为空:

  1. 防止空引用错误:当我们尝试访问或使用一个空对象时,可能会导致空引用错误(如 TypeError: Cannot read property ‘x’ of null)。通过检查对象是否为空,我们可以避免这些错误的发生,并采取相应的处理措施。
  2. 数据验证和表单提交:在表单提交之前,通常需要验证用户输入的数据是否有效。如果对象为空,表示用户未提供必要的数据或未填写表单字段,我们可以显示错误消息或阻止表单提交。
  3. 条件逻辑和流程控制:根据对象是否为空,可以根据不同的条件逻辑执行不同的操作或采取不同的分支。例如,如果对象为空,可以执行备用的默认操作或返回默认值。
  4. 数据处理和转换:在处理对象数据之前,可能需要对其进行处理或转换。如果对象为空,可以提前终止或跳过数据处理逻辑,以避免不必要的计算或错误发生。
  5. 用户界面交互和显示:在用户界面中,可能需要根据对象的存在与否来显示或隐藏特定的界面元素、更改样式或呈现不同的内容。

通过检查 JavaScript 对象是否为空,可以增加应用程序的健壮性、提升用户体验,并避免潜在的错误和异常情况。因此,检查对象是否为空是编写高质量代码的重要部分。

在本文中,我们将讨论如何检查对象是否为空,其中包括 JavaScript 中检查对象是否为空的不同方法以及如何检查对象是否为空、未定义或为 null。

使用Object.keys()

使用Object.keys()方法可以检查对象是否为空。Object.keys(obj)返回一个包含给定对象所有可枚举属性的数组。
利用这个特性,我们可以通过检查返回的数组长度来确定对象是否为空。如果数组长度为0,则表示对象没有任何属性,即为空。
以下是一个示例代码:

javascriptCopy Codefunction isObjectEmpty(obj) {
return Object.keys(obj).length === 0;
}

// 测试对象是否为空
const obj1 = {};
console.log(isObjectEmpty(obj1)); // true

const obj2 = { name: "John", age: 25 };
console.log(isObjectEmpty(obj2)); // false

在上面的示例中,我们定义了一个isObjectEmpty()函数,它接受一个对象作为参数。函数内部使用Object.keys(obj)获取对象的所有可枚举属性,并检查返回的数组长度是否为0。根据返回结果,判断对象是否为空。

使用Object.values()

使用Object.values()方法来检查对象是否为空,Object.values(obj)方法返回一个包含给定对象所有可枚举属性值的数组。如果返回的数组长度为0,则表示对象没有任何属性值,即为空。

以下是使用Object.values()方法来检查对象是否为空的示例代码:

function isObjectEmpty(obj) {
return Object.values(obj).length === 0;
}

// 测试对象是否为空
const obj1 = {};
console.log(isObjectEmpty(obj1)); // true

const obj2 = { name: "John", age: 25 };
console.log(isObjectEmpty(obj2)); // false

在上面的示例中,我们定义了一个isObjectEmpty()函数,它接受一个对象作为参数。函数内部使用Object.values(obj)获取对象的所有可枚举属性值,并检查返回的数组长度是否为0。根据返回结果,判断对象是否为空。
请注意,Object.values()方法是ES2017(ES8)引入的新方法,因此在一些旧版本的JavaScript引擎中可能不被支持。在使用之前,请确保你的环境支持该方法或使用适当的polyfill来提供支持。

使用 for…in 循环

使用 for…in 循环方法是通过遍历对象的属性来判断对象是否为空。以下是一个示例代码:

javascriptCopy Codefunction isObjectEmpty(obj) {
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
return false; // 只要有一个属性存在,就返回false表示不为空
}
}
return true; // 如果遍历完所有属性后仍然没有返回false,表示对象为空
}

// 测试对象是否为空
const obj1 = {};
console.log(isObjectEmpty(obj1)); // true

const obj2 = { name: "John", age: 25 };
console.log(isObjectEmpty(obj2)); // false

在上面的示例中,isObjectEmpty()函数接受一个对象作为参数。函数内部使用 for…in 循环遍历对象的属性,如果发现任何属性,则返回false表示对象不为空;如果循环结束后仍然没有返回false,则表示对象为空,并返回true。
虽然使用 for…in 循环可以达到同样的目的,但相比起使用 Object.keys() 或 Object.values() 方法,它的实现稍显繁琐。因此,通常情况下,推荐使用 Object.keys() 或 Object.values() 方法来检查对象是否为空,因为它们提供了更简洁和直观的方式。

使用 Object.entries()

Object.entries()方法返回一个给定对象自身可枚举属性的键值对数组。如果返回的数组长度为0,则表示对象没有任何属性,即为空。
以下是使用Object.entries()方法来检查对象是否为空的示例代码:

function isObjectEmpty(obj) {
return Object.entries(obj).length === 0;
}

// 测试对象是否为空
const obj1 = {};
console.log(isObjectEmpty(obj1)); // true

const obj2 = { name: "John", age: 25 };
console.log(isObjectEmpty(obj2)); // false

在上面的示例中,isObjectEmpty()函数接受一个对象作为参数。函数内部使用Object.entries(obj)获取对象的键值对数组,并检查返回的数组长度是否为0。如果数组长度为0,则表示对象没有任何属性,即为空。
请注意,Object.entries()方法是ES2017(ES8)引入的新方法,因此在一些旧版本的JavaScript引擎中可能不被支持。在使用之前,请确保你的环境支持该方法或使用适当的polyfill来提供支持。

使用 JSON.stringify()

使用 JSON.stringify() 方法来检查对象是否为空的方法是将对象转换为 JSON 字符串,然后检查字符串的长度是否为 2。当对象为空时,转换后的字符串为 “{}”,长度为 2。如果对象不为空,则转换后的字符串长度会大于 2。
以下是使用 JSON.stringify() 方法来检查对象是否为空的示例代码:

function isObjectEmpty(obj) {
return JSON.stringify(obj) === "{}";
}

// 测试对象是否为空
const obj1 = {};
console.log(isObjectEmpty(obj1)); // true

const obj2 = { name: "John", age: 25 };
console.log(isObjectEmpty(obj2)); // false

在上述示例中,isObjectEmpty() 函数接受一个对象作为参数。函数内部使用 JSON.stringify(obj) 将对象转换为 JSON 字符串,然后将转换后的字符串与 “{}” 进行比较。如果相等,则表示对象为空。
需要注意的是,这种方式只适用于纯粹的对象,并且不包含任何非原始类型属性(如函数、undefined 等)。如果对象中包含了非原始类型的属性,那么转换后的 JSON 字符串可能不为空,即使对象实际上是空的。

E6使用Object.getOwnPropertyNames()

在ES6中,你可以使用Object.getOwnPropertyNames()方法来检查对象是否为空,但需要注意的是,该方法返回一个数组,其包含给定对象中所有自有属性(包括不可枚举属性,但不包括使用 symbol 值作为名称的属性)。
以下是使用Object.getOwnPropertyNames()方法来检查对象是否为空的示例代码:

function isObjectEmpty(obj) {
return Object.getOwnPropertyNames(obj).length === 0;
}

// 测试对象是否为空
const obj1 = {};
console.log(isObjectEmpty(obj1)); // true

const obj2 = { name: "John", age: 25 };
console.log(isObjectEmpty(obj2)); // false

在上面的示例中,isObjectEmpty()函数接受一个对象作为参数。函数内部使用Object.getOwnPropertyNames(obj)获取对象的所有属性名,并检查返回的数组长度是否为0。如果数组长度为0,则表示对象没有任何属性,即为空。
请注意,Object.getOwnPropertyNames()方法返回的数组只包含对象自身的属性,不包括继承的属性。如果你需要检查继承的属性,请使用for…in循环或其他方法。同样,Object.getOwnPropertyNames()方法在ES5中引入,因此在一些旧版本的JavaScript引擎中可能不被支持。

ES6使用Object.getOwnPropertySymbols()方法

在ES6中,可以使用Object.getOwnPropertySymbols()方法来检查对象是否为空。该方法返回一个数组,其中包含了给定对象自身的所有符号属性。
以下是使用Object.getOwnPropertySymbols()方法来检查对象是否为空的示例代码:

function isObjectEmpty(obj) {
const symbols = Object.getOwnPropertySymbols(obj);
const hasSymbols = symbols.length > 0;
return !hasSymbols;
}

// 测试对象是否为空
const obj1 = {};
console.log(isObjectEmpty(obj1)); // true

const symbol = Symbol("key");
const obj2 = { [symbol]: "value" };
console.log(isObjectEmpty(obj2)); // false

在上面的示例中,isObjectEmpty()函数接受一个对象作为参数。函数内部使用Object.getOwnPropertySymbols(obj)获取对象的所有符号属性,并将它们存储在symbols数组中。然后,通过检查symbols数组的长度是否大于0来判断对象是否具有符号属性。如果symbols数组的长度为0,则表示对象没有任何符号属性,即为空。

你还在苦恼找不到真正免费的编程学习平台吗?可以试试【云端源想】!课程视频、知识库、微实战、云实验室、一对一咨询……你想要的全部学习资源这里都有,重点是现在还是免费的!点这里即可免费学习!

注意,Object.getOwnPropertySymbols()方法只返回对象自身的符号属性,不包括其他类型的属性,例如字符串属性。如果你想同时检查对象的字符串属性和符号属性,可以结合使用Object.getOwnPropertyNames()和Object.getOwnPropertySymbols()方法。

ES6使用Reflect.ownKeys()

在ES6中,你可以使用Reflect.ownKeys()方法来检查对象是否为空。Reflect.ownKeys()返回一个包含了指定对象自身所有属性(包括字符串键和符号键)的数组。
以下是使用Reflect.ownKeys()方法来检查对象是否为空的示例代码:

function isObjectEmpty(obj) {
return Reflect.ownKeys(obj).length === 0;
}

// 测试对象是否为空
const obj1 = {};
console.log(isObjectEmpty(obj1)); // true

const symbol = Symbol("key");
const obj2 = { [symbol]: "value" };
console.log(isObjectEmpty(obj2)); // false

在上面的示例中,isObjectEmpty()函数接受一个对象作为参数。函数内部使用Reflect.ownKeys(obj)获取对象的所有自身属性名(包括字符串键和符号键),并检查返回的数组长度是否为0。如果数组长度为0,则表示对象没有任何属性,即为空。
Reflect.ownKeys()方法提供了一种统一的方式来获取对象的所有键,包括字符串键和符号键。因此,使用Reflect.ownKeys()方法可以更全面地检查对象是否为空。

使用lodash库的isEmpty()函数

如果您使用了lodash库,可以使用其提供的isEmpty()函数来直接判断对象是否为空。
以下是使用 Lodash 的 isEmpty() 函数进行对象空检查的示例代码:

// 导入Lodash库
const _ = require('lodash');

// 检查对象是否为空
const obj1 = {};
console.log(_.isEmpty(obj1)); // true

const obj2 = { name: "John", age: 25 };
console.log(_.isEmpty(obj2)); // false

在上述示例中,_.isEmpty() 函数接受一个对象作为参数,并返回一个布尔值表示对象是否为空。如果对象为空,则返回 true;否则返回 false。
使用 Lodash 的 isEmpty() 函数可以更方便地进行对象空检查,同时处理了各种情况,包括非原始类型的属性、数组、字符串等。

使用jQuery库的$.isEmptyObject()函数

要使用 jQuery 库中的 $.isEmptyObject() 函数来检查 JavaScript 对象是否为空,首先确保已经安装了 jQuery 库,并将其导入到你的项目中。
以下是使用 jQuery 的 $.isEmptyObject() 函数进行对象空检查的示例代码:

// 导入jQuery库
const $ = require('jquery');

// 检查对象是否为空
const obj1 = {};
console.log($.isEmptyObject(obj1)); // true

const obj2 = { name: "John", age: 25 };
console.log($.isEmptyObject(obj2)); // false

在上述示例中,$.isEmptyObject() 函数接受一个对象作为参数,并返回一个布尔值表示对象是否为空。如果对象为空,则返回 true;否则返回 false。
使用 jQuery 的 $.isEmptyObject() 函数可以更方便地进行对象空检查,同时处理了各种情况,包括非原始类型的属性、数组、字符串等。

检查对象是否为空、未定义或为 null

要同时检查对象是否为空、未定义或为 null,你可以使用以下函数来进行判断:

function isObjectEmptyOrNull(obj) {
return obj === undefined || obj === null || Object.getOwnPropertyNames(obj).length === 0;
}

在上述代码中,isObjectEmptyOrNull函数接收一个对象作为参数。它首先检查对象是否为 undefined 或者 null,如果是,则直接返回 true 表示对象为空或者未定义。如果对象不是 undefined 或者 null,则使用 Object.getOwnPropertyNames() 方法获取对象的所有自身属性名,然后判断属性名数组的长度是否为 0。如果属性名数组长度为 0,则表示对象没有任何属性,即为空。
下面是一个示例用法:

const obj1 = {};
console.log(isObjectEmptyOrNull(obj1)); // true

const obj2 = null;
console.log(isObjectEmptyOrNull(obj2)); // true

const obj3 = { name: "John", age: 25 };
console.log(isObjectEmptyOrNull(obj3)); // false

const obj4 = undefined;
console.log(isObjectEmptyOrNull(obj4)); // true

总结和比较

在本文中,我们介绍了多种方法来检查 JavaScript 对象是否为空。下面是这些方法的优缺点总结:

  • 使用 Object.keys() 方法

优点:简单易用,不需要依赖第三方库。
缺点:无法处理非原始类型的属性,如函数、undefined 等。

  • Object.values()

优点:能够将对象的属性值组成一个数组,可以通过判断该数组的长度来判断对象是否为空。
缺点:无法直接判断对象是否为空,只提供了属性值的数组。

  • 使用 for…in 循环遍历对象

优点:可以处理非原始类型的属性。
缺点:代码较为冗长,需要手动判断每个属性是否为对象自身属性。

  • 使用 JSON.stringify() 方法

优点:可以处理非原始类型的属性,并且转换后的字符串长度为 2 表示对象为空。
缺点:当对象包含循环引用时,将抛出异常。

  • Object.entries()

优点:能够将对象的键值对组成一个数组,可以通过判断该数组的长度来判断对象是否为空。
缺点:同样无法直接判断对象是否为空,只提供了键值对数组。

  • Object.getOwnPropertyNames()

优点:能够返回对象自身的所有属性名组成的数组,包括可枚举和不可枚举的属性,可以通过判断该数组的长度来判断对象是否为空。
缺点:同样无法直接判断对象是否为空,只提供了属性名数组。

  • Object.getOwnPropertySymbols()

优点:能够返回对象自身的所有 Symbol 类型的属性组成的数组,可以通过判断该数组的长度来判断对象是否为空。
缺点:仅针对 Symbol 类型的属性,无法判断其他类型的属性是否为空。

  • Reflect.ownKeys()

优点:能够返回对象自身的所有属性(包括字符串键和 Symbol 键)组成的数组,包括可枚举和不可枚举的属性,可以通过判断该数组的长度来判断对象是否为空。
缺点:同样无法直接判断对象是否为空,只提供了所有键的数组。

  • 使用 Lodash 库的 isEmpty() 函数

优点:可以处理各种情况,包括非原始类型的属性、数组、字符串等。
缺点:需要依赖第三方库。

  • 使用 jQuery 库的 $.isEmptyObject() 函数

优点:可以处理各种情况,包括非原始类型的属性、数组、字符串等。
缺点:需要依赖第三方库。

总体来说, 这些方法都提供了一种间接判断对象是否为空的方式,即通过获取对象的属性、属性值或键值对的数组,并判断该数组的长度。然而,它们并不能直接告诉我们对象是否为空,因为它们只提供了属性、属性值或键值对的信息。因此,在使用这些方法判断对象是否为空时,需要结合其他判断条件来综合考虑。

收起阅读 »

JS逐页转pdf文件为图片格式

web
背景 年前的时候,开发一个电子杂志项目,功能需求是通过上传pdf文件,将其转为图片格式,所以杂志的内容其实就是一张张图片 不过当时技术要求用后端实现,所以使用的是PHP实现该功能。项目完成后,寻思着在前端是否也能实现pdf转图片的功能。一番研究后,果真可行。以...
继续阅读 »

背景


年前的时候,开发一个电子杂志项目,功能需求是通过上传pdf文件,将其转为图片格式,所以杂志的内容其实就是一张张图片


不过当时技术要求用后端实现,所以使用的是PHP实现该功能。项目完成后,寻思着在前端是否也能实现pdf转图片的功能。一番研究后,果真可行。以下就分享如何通过前端js将pdf文件转为图片格式,并且支持翻页预览、以及图片打包下载


效果预览


图片

所需工具



  1. pdf.js(负责API解析,可将pdf文件渲染成canvas实现预览)

  2. pdf.worker.js(负责核心解析)

  3. jszip.js(将图片打包成生成.zip文件)

  4. Filesaver.js(保存下载zip文件)


工具下载


一、pdf.js及pdf.worker.js下载地址:


mozilla.github.io/pdf.js/gett…


1.选择稳定版下载


图片


2.解压后将bulid中的pdf.js及pdf.worker.js拷贝到项目中


图片


二、jszip.js及Filesaver.js下载地址:

stuk.github.io/jszip/


1.点击download.JSZip


图片


2.解压后将dist文件夹下的jszip.js文件以及vendor文件夹下的FileSaver.js文件拷贝到项目中


图片


至此,所需工具已齐全。以下直接附上项目完整代码(代码可直接复制使用,查看效果。 对应的文件需自行下载引入)


源代码: 嫌麻烦的小伙伴可以直接在公众号后回复: pdf转图片


代码实现


<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>PDF文件转图片</title>
<script src="https://cdn.staticfile.org/jquery/1.10.2/jquery.min.js"></script>
<script type="text/javascript" src="js/pdf.js"></script>
<script type="text/javascript" src="js/pdf.worker.js"></script>
<script type="text/javascript" src="js/jszip.js"></script>
<script type="text/javascript" src="js/FileSaver.js"></script>
<style type="text/css">

button {
width: 120px;
height: 30px;
background: none;
border: 1px solid #b1afaf;
border-radius: 5px;
font-size: 12px;
font-weight: 1000;
color: #384240;
cursor: pointer;
outline: none;
margin: 0 0.5%
}

button:hover {
background: #ccc;
}

#container {
width: 600px;
height: 780px;
margin-top: 1%;
border-radius: 2px;
border: 2px solid #a29b9b;
}

.pdfInfos {
margin: 0 2%;
}
</style>
</head>

<body>

<div style="margin-top:1%">
<button id="prevpage">上一页</button>
<button id="nextpage">下一页</button>
<button id="exportImg">导出图片</button>
<button onclick="choosePdf()">选择一个pdf文件</button>
<input style="display:none" id='chooseFile' type='file' accept="application/pdf">
</div>

<div style="margin-top:1%">
<span class="pdfInfos">页码:<span id="currentPages"></span><span id="totalPages"></span></span>
<span class="pdfInfos">文件名:<span id="fileName"></span></span>
<span class="pdfInfos">文件大小:<span id="fileSize"></span></span>
</div>

<div style="position: relative;">
<div id="container"></div>
<img id="imgloading" style="position: absolute;top: 20%;left: 2%;display:none" src="loading.gif">
</div>

</body>


<script>

var currentPages,totalPages //声明一个当前页码及总页数变量
var scale = 2; //设置缩放比例,越大生成图片越清晰

$('#chooseFile').change(function() {
var pdfFilePath = $('#chooseFile').val();
if(pdfFilePath) {

$("#imgloading").css('display','block');
$("#container").empty(); //清空上一PDF文件展示图

currentPages=1; //重置当前页数
totalPages=0; //重置总页数

var filesdata = $('#chooseFile')[0].files; //jquery获取到文件 返回属性的值
var fileSize = filesdata[0].size; //文件大小
var mb;

if(fileSize) {
mb = fileSize / 1048576;
if(mb > 10) {
alert("文件大小不能>10M");
return;
}
}

$("#fileName").text(filesdata[0].name);
$("#fileSize").text(mb.toFixed(2) + "Mb");

var reader = new FileReader();
reader.readAsDataURL(filesdata[0]); //将文件读取为 DataURL
reader.onload = function(e) { //文件读取成功完成时触发

pdfjsLib.getDocument(this.result).then(function(pdf) { //调用pdf.js获取文件
if(pdf) {
totalPages = pdf.numPages; //获取pdf文件总页数
$("#currentPages").text("1/");
$("#totalPages").text(totalPages);

//遍历动态创建canvas
for(var i = 1; i <= totalPages; i++) {
var canvas = document.createElement('canvas');
canvas.id = "pageNum" + i;
$("#container").append(canvas);
var context = canvas.getContext('2d');
renderImg(pdf,i,context);
}

}
});

};
}
});

//渲染生成图片
function renderImg(pdfFile,pageNumber,canvasContext) {
pdfFile.getPage(pageNumber).then(function(page) { //逐页解析PDF
var viewport = page.getViewport(scale); // 页面缩放比例
var newcanvas = canvasContext.canvas;

//设置canvas真实宽高
newcanvas.width = viewport.width;
newcanvas.height = viewport.height;

//设置canvas在浏览中宽高
newcanvas.style.width = "100%";
newcanvas.style.height = "100%";

//默认显示第一页,其他页隐藏
if (pageNumber!=1) {
newcanvas.style.display = "none";
}

var renderContext = {
canvasContext: canvasContext,
viewport: viewport
};

page.render(renderContext); //渲染生成
});

$("#imgloading").css('display','none');

return;
};

//上一页
$("#prevpage").click(function(){

if (!currentPages||currentPages <= 1) {
return;
}

nowpage=currentPages;
currentPages--;

$("#currentPages").text(currentPages+"/");

var prevcanvas = document.getElementById("pageNum"+currentPages);
var currentcanvas = document.getElementById("pageNum"+nowpage);
currentcanvas.style.display = "none";
prevcanvas.style.display = "block";

})

//下一页
$("#nextpage").click(function(){

if (!currentPages||currentPages>=totalPages) {
return;
}

nowpage=currentPages;
currentPages++;

$("#currentPages").text(currentPages+"/");

var nextcanvas = document.getElementById("pageNum"+currentPages);
var currentcanvas = document.getElementById("pageNum"+nowpage);
currentcanvas.style.display = "none";
nextcanvas.style.display = "block";

})

//导出图片
$("#exportImg").click(function() {

if (!$('#chooseFile').val()) {
alert('请先上传pdf文件')
return false;
}

$("#imgloading").css('display','block');

var zip = new JSZip(); //创建一个JSZip实例
var images = zip.folder("images"); //创建一个文件夹用来存放图片

//遍历canvas,将其生成图片放进文件夹images中
$("canvas").each(function(index, ele) {
var canvas = document.getElementById("pageNum" + (index + 1));

//将图片放进文件夹images中
//参数1为图片名称,参数2为图片数据(格式为base64,需去除base64前缀 data:image/png;base64)
images.file("" + (index + 1) + ".png", splitBase64(canvas.toDataURL("image/png", 1.0)), {
base64: true
});

})

//打包下载
zip.generateAsync({
type: "blob"
}).then(function(content) {
saveAs(content, "picture.zip"); //saveAs依赖的js文件是FileSaver.js
$("#imgloading").css('display','none');
});

});

//截取base64前缀
function splitBase64(dataurl) {
var arr = dataurl.split(',')
return arr[1]
}

function choosePdf(){
$("#chooseFile").click()
}
</script>
</html>

项目实现原理分析



  1. 首先利用pdf.js将上传的pdf文件转化成canvas

  2. 然后使用jszip.js将canvas打包图片生成.zip文件

  3. 最后使用Filesaver.js将zip文件保存下载


项目注意要点



  1. 由于pdf文件是通过上传的,因此需要通过js的FileReader()对象将其读取为DataURL,pdf.js文件才可读取渲染

  2. JSZip对象的.file()函数中第二个参数传入的是base64格式图片,但是要去掉base64前缀标识


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

JSON.parse记录一次线上bug排查

web
最近项目中有一个匪夷所思的问题,业务在使用的时候,偶发性的会白屏,经常下班的时候骚扰我们,开发苦不堪言,经过长达一周的排查,仍然没有查到bug的存在,最终尝试通过添加埋点日志,记录关键信息。 现状 首先讲述一下现状,首先业务进入后,页面可以认为有两个按钮 跳...
继续阅读 »

最近项目中有一个匪夷所思的问题,业务在使用的时候,偶发性的会白屏,经常下班的时候骚扰我们,开发苦不堪言,经过长达一周的排查,仍然没有查到bug的存在,最终尝试通过添加埋点日志,记录关键信息。


现状


首先讲述一下现状,首先业务进入后,页面可以认为有两个按钮



  • 跳转共享链接

  • 打开表单弹窗按钮,点击后展示表单。


image-20240124132744181


操作顺序是,页面加载后,先点击跳转共享链接,看完链接后再返回点击表单弹窗。



里面有两个重要的时间节点,一个是跳转链接之前,一个是返回到当前页面。




  • 跳转链接之前



    • 需要存储接口数据,接口数据包含了表单的数据



  • 返回当前页面



    • 请求接口数据



      • 本地缓存无,直接使用接口数据

      • 本地缓存有,缓存和接口数据合并,接口数据优先






image-20240124132901079


返回页面的时候,点击表单弹窗


正常上来说弹窗能够正常显示,但是线上环境再点击 展示弹窗的按钮导致白屏了。整个流程如下


image-20240124133213958


初步判断是整合缓存和接口数据问题,于是需要给页面添加两个埋点



  • 页面报错异常时上报

  • 点击打开表单的时,上报缓存数据和聚合之后的数据。



    • 为什么不上报接口数据呢?因为当时修复bug比较紧急,观察代码发现接口直接返回的数据没有在公共变量中存储,如果需要存储改动较大,还有就是接口数据也可以从后端日志去排查




页面报错异常上报


异常上报的方法有很多,通常使用一个gif图片,地址为get的请求地址+上报信息,具体的可以自行百度,此处简单叙述下


使用图片是因为加载资源里面img优先级比较低,不会阻塞其他资源,而且图片请求不会跨域,用gif是因为对比图片类型他是比较小的


//utils/utils.js
/**
* 异常上报方法
* 希望抽离出来同步异常类和异步异常类
*/

function uploadError() {
 //上报处理参数
 const upload = errObj =>{
   const logUrl = 'https://xxx.xxx.com/log.gif'; // 上报接口
   //将obj拼接成url
   const queryStr = Object.entries(errObj)
      .map(([key, value]) => `${key}=${value}`)
      .join('&');
     const oImg = new Image();
     oImg.src = logUrl + '?' + encodeURIComponent(queryStr);
}
 //同步方法
 function handleError(e) {
   try {
     let baseInfo = localStorage.getItem('base_info'); // 域账户
     let masterName = baseInfo ? JSON.parse(baseInfo)?.master_name : ''; // 域账户
     let errObj = {
       masterName: masterName,//域账户
       url: window.location.href,//报错的路由,利于排查
       reason: JSON.stringify({
         message: e?.error?.message, //报错信息
         stack: e?.error?.stack,//调用栈
      }),
       message: e?.message, //报错信息
    };
     upload(errObj)
     console.log('error', errObj);
  } catch (err) {
     console.log('error', err);
  }
}
 window.addEventListener('error', handleError);//调用监听
}

//app.js
//异常上报方法 开发环境禁止上报
if(!['dev'].includes(process.env.BUILD_ENV)){
 uploadError()
}

点击弹窗的异常上报


//打开弹窗的操作  
const open = () => {
   setShow(!show);//控制表单的展示隐藏
   if(!show){
     const logUrl = 'https://xxx.xxx.com/log.gif'; // 上报接口
     const oImg = new Image();
     let initFormVal = localStorage.getItem('initFormVal' + query?.id);
     oImg.src = logUrl + '?' + encodeURIComponent(`initFormVal=${initFormVal}&integratedData=${JSON.stringify(integratedData)}`);
  }
};
//initFormVal为缓存中的数据 integratedData为整合后的数据

发现问题原因


通过添加以上异常上报,业务员进行操作时,又出现了白屏,此时根据业务员token与上报关键字与时间查到了相关日志,其中日志中记录的是


https://xxx.xxx.com/log.gif?initFormVal=&integratedData=null

integratedData是后端接口数据和缓存的融合呀!通过查日志发现当时后端确确实实返回正常的响应了,不可能为null,同时还有一个疑问浮出水面,为什么initFormVal没有值,而不是null


正常来说如果initFormVal从json中取值时,取不到应该默认就是null,此处为'',只说明一个问题,缓存的时候给他赋值了


那么问题大致可以定位到以下两个操作节点



  • 缓存时

  • 返回页面后,缓存和接口数据融合时


//缓存时操作  
const getFormValues = () => {
   let formVal = childRef?.current?.getFormVal() || '';
localStorage.setItem('initFormVal' + query.id, JSON.stringify(formVal));
};

缓存时,如果子节点获取不到,那么childRef?.current?.getFormVal()就为undefind,又由于使用了或运算符,那么此时存储的是'',那么取这个暂时看也没问题呀,然后也写入了缓存



更严格来讲,应该先判断formVal是否存在然后再去缓存,没有就不缓存。



再看一下返回页面,数据融合的代码


const getDataFn = url => {
   dispatch({
     type: url,
     payload: { id: query.id },
     callback: res => {
       if (res.ret === 1) {
         let initFormVal = localStorage.getItem('initFormVal' + query?.id);
         console.log('initFormVal', JSON.parse(initFormVal));
         let cacheFormVal = {};
         
         if (initFormVal) {
           //initFormVal赋值给cacheFormVal,此处省略
        }
         setPricingInfo({
           ...cacheFormVal,
           ...res.data
        });
      }  

发现有一个console.log(),JSON.parse('')会是什么?报错,果然,查异常上报日志的时候,也查到这个错误,真是一失足成千古恨,当时只是为了方便查看,打印了一下缓存数据,没想到是这个地方出现的问题 Uncaught SyntaxError: Unexpected end of JSON input


image-20240124142222982


JSON.parse


那问题来了 json.parse什么情况会报错呢?通过查阅MDN


image-20240124143007732


那么,什么是规范的JSON格式呢?我们此处再去查阅MDN


此处只列出了json的结构 很显然,传入null 是合法的,但是传入空字符是不合法的,


JSON = null
   or true or false
   or JSONNumber
   or JSONString
   or JSONObject
   or JSONArray

吐槽


可能有人要吐槽,直接写JSON存储的时候格式不对不就行了吗?干什么这那么多,又是异常上报,又是贴代码?又是贴MDN的。


我在这里回答一下之所以这么写一是为了记录出错的时候出现的问题,方便下次出现类似问题能够即时复盘。


二是希望贴出自己的排错方式,新手若有不明白的可以模仿这个方式得到一些启发和思考,高手也可指出我的问题,共同成长


同样我也希望大家遇到问题的时候要记得查文档,查文档再查文档,自己遇到的问题,先文档,是不是自己理解错了,如果还不行就去stackoverflow,如果再不济就去github issue看看是否有相同的问题是不是作者的bug,如果都没有,那么好了,这个问题几乎解决不了了,此时有两个选择,要么产品接受,要么 那我走???


作者:傲娇的腾小三
来源:juejin.cn/post/7327227246618476583
收起阅读 »

相见恨晚的前端开发利器-PageSpy

web
今天介绍一个非常有用的前端开发工具。 做前端开发的你,一定有过以下经历: 我这里是好的啊,你截个图给我看看 不会吧,你打开f12,控制台截个图给我看看 录个屏给我看看你是怎么操作的 ... 还有,我们在开发h5的时候,一般为了调试方便,可能会在开发环境和测...
继续阅读 »

今天介绍一个非常有用的前端开发工具。


做前端开发的你,一定有过以下经历:



  1. 我这里是好的啊,你截个图给我看看

  2. 不会吧,你打开f12,控制台截个图给我看看

  3. 录个屏给我看看你是怎么操作的

  4. ...


还有,我们在开发h5的时候,一般为了调试方便,可能会在开发环境和测试环境实例化一个vConsole,遇到问题看一下大概就能定位到错误。


可是如果测试小姐姐在远程呢?如果是线上环境呢?如果有这么一个工具,能让我坐在工位上就能调(窥)试(探)用户的操作,那岂不是美滋滋。


你可能会说,这不就是埋点吗,先别急,今天介绍的这个工具和埋点有着本质区别。


不啰嗦了,有请主角**「PageSpy」**登场。


PageSpy是什么?




PageSpy[1] 是由货拉拉大前端开源的一款用于调试 H5 、或者远程 Web 项目的工具。是一个强大的开源前端远程调试平台,它可以显著提高我们在面对前端问题时的效率。




有什么作用?



  • 一眼查看客户端信息 能识别客户端运行环境,支持Linux/Mac/Window/IOS/Android

  • 实时查看输出 可以实时输出客户端的Element,Console,Network,Storage

  • 网络请求监控 可以捕获和显示页面的网络请求

  • 远程控制台 支持远程调试客户机上的js代码


如何使用?


查看官方文档[2]



  1. 安装npm包


yarn global add @huolala-tech/page-spy-api

# 如果你使用 npm

npm install -@huolala-tech/page-spy-api


  1. 启动服务


直接在命令行执行page-spy-api,部署完成后浏览器访问:6752,页面顶部会出现接入SDK菜单,点击菜单查看如何在业务项目中配置并集成。图片命令行执行后出现这个界面表示服务启动成功了,然后访问我自己的ip+端口,再点击顶部接入SDK图片去创建一个测试项目,建一个最简单的index.html,按照文档接入SDK,然后在浏览器访问这个页面图片图片左下角出现Pagepy的logo说明引入成功了。


此时点击顶部菜单房间列表图片点击调试,就可以看到这个项目的一些实时调试信息,但是还没加什么代码。图片现在改一下我们的代码,加一些输出信息。图片Console控制台的信息图片直接输出用户端代码变量的实时的值图片加个定时器试试,也是实时输出的图片图片再来看看Storage信息图片图片Element信息图片调个接口试试图片图片图片


好了,今天的介绍就到这里,这么牛叉的工具,是不是有种相见恨晚的感觉,感兴趣的小伙伴快去试试吧!


Reference


[1] PageSpy:huolalatech.github.io/page-spy-we…


[2] 官方文档:github.com/HuolalaTech…


作者:丝绒拿铁有点甜
来源:juejin.cn/post/7327691403844665380
收起阅读 »

uniapp云开发--微信登录

web
前言 我们主要使用uniapp + uniCloud 实现小程序登录,并将用户数据存入云数据库。 小程序 wx.getUserProfile 调整,接口将被收回 详情,所以需要用户自己填写资料。 注:填写个人资料是一个组件,覆盖在登录之上而已,还是在同一个页面...
继续阅读 »

前言


我们主要使用uniapp + uniCloud 实现小程序登录,并将用户数据存入云数据库。


小程序 wx.getUserProfile 调整,接口将被收回 详情,所以需要用户自己填写资料。


注:填写个人资料是一个组件,覆盖在登录之上而已,还是在同一个页面



uniCloud


创建 uniapp + uniCloud 项目,创建云数据库 数据表 uniCloud传送门


开始


创建项目


39d23acf47b440e2880f5ccadc1417f9~tplv-k3u1fbpfcp-zoom-in-crop-mark_4536_0_0_0.webp


关联云服务空间




创建云数据库 数据表


不使用模版,输入名称直接创建即可。



编辑表结构,想了解更多可以去看云数据库 DB Schema 数据结构文档 传送门


{
"bsonType": "object",
"required": [],
"permission": {
"read": true,
"create": true,
"update": true,
"delete": false
},
"properties": {
"_id": {
"description": "ID,系统自动生成"
},
"nickName": {
"bsonType": "string",
"label": "昵称",
"description": "用户昵称,登录获取的"
},
"avatarUrl": {
"bsonType": "string",
"label": "头像",
"description": "用户头像图片的 URL,登录获取的"
},
"gender": {
"bsonType": "number",
"label": "性别",
"description": "用户性别,1: 男;2: 女"
},
"personalize": {
"bsonType": "string",
"label": "个性签名",
"description": "个性签名,编辑资料获取"
},
"background": {
"bsonType": "object",
"label": "个人中心背景图",
"description": "个人中心背景图,编辑资料获取"
},
"mp_wx_openid": {
"bsonType": "string",
"description": "微信小程序平台openid"
},
"register_date": {
"bsonType": "timestamp",
"description": "注册时间",
"forceDefaultValue": {
"$env": "now"
}
}
}
}

创建云函数




云函数代码


云函数 将 uni.login 取得的 code 获取到用户 session, 并对 数据库进行 增加、修改、查询 操作,第一次注册必须用户主动填写用户资料。


对云数据库的相关操作 传送门


'use strict';

//小程序的AppID 和 AppSecret
const mp_wx_data = {AppID: '************', AppSecret: '***********************'}

//event为客户端上传的参数
exports.main = async (event, context) => {

//使用云数据库
const db = uniCloud.database();
// 获取 `users` 集合的引用
const pro_user = db.collection('users');
// 通过 action 判断请求对象

let result = {};
switch (event.action) {
// 通过 code 获取用户 session
case 'code2Session':
const res_session = await uniCloud.httpclient.request('https://api.weixin.qq.com/sns/jscode2session', {
method: 'GET', data: {
appid: mp_wx_data.AppID,
secret: mp_wx_data.AppSecret,
js_code: event.js_code,
grant_type: 'authorization_code'
}, dataType: 'json'
}
)
const success = res_session.status === 200 && res_session.data && res_session.data.openid
if (!success) {
return {
status: -2, msg: '从微信获取登录信息失败'
}
}

//从数据库查找是否已注册过
const res_user = await pro_user.where({
mp_wx_openid: res_session.data.openid
}).get()
// 没有用户信息,进入注册
if (res_user.data && res_user.data.length === 0) {
//event.user_info 用户信息
if (event.user_info) {
//有信息则进入注册,向数据库写入数据
const register = await uniCloud.callFunction({
name: 'user',
data: {
action: 'register',
open_id: res_session.data.openid,
user_info: event.user_info
}
}).then(res => {
result = res
})
} else {
//没有信息返回{register: true}
result = {
result: {
result: {register: true}
}
}
}
} else {
result = {
result: {
result: res_user.data[0]
}
}
}
break;
//注册 向数据库写入数据
case 'register':
const res_reg = await pro_user.add({
nickName: event.user_info.nickName,
avatarUrl: event.user_info.avatarUrl,
gender: event.user_info.gender,
mp_wx_openid: event.open_id,
register_date: new Date().getTime()
})
if (res_reg.id) {
const res_reg_val = await uniCloud.callFunction({
name: 'user', data: {
action: 'getUser', open_id: event.open_id
}
}).then(res => {
result = res
})
} else {
result = {
status: -1, msg: '微信登录'
}
}
break;
case 'update':
if (event._id && event.info) {
const res_update = await pro_user.doc(event._id).update(event.info)
if (res_update.updated >= 0) {
result = {status: 200, msg: '修改成功'}
} else {
result = {status: -1, msg: '修改失败'}
}
} else {
result = {status: -1, msg: '修改失败'}
}
break;
case 'getUser':
const res_val = await pro_user.where({
mp_wx_openid: event.open_id
}).get()
return res_val.data[0]
break;
}
return result;
};

微信登录操作


如上面所说,用户需手动上传资料,对于用户头像我们需要上传至云储存。


上传用户头像


上传图片函数参数为微信本地图片路径,我们对路径用/进行分割,取最后的图片名称进行上传


/**
* 上传图片至云存储
*/

export async function uploadImage(url) {
const fileName = url.split('/')
return new Promise(resolve => {
uniCloud.uploadFile({
filePath: url,
cloudPath: fileName[fileName.length - 1],
success(res) {
resolve(res)
},
fail() {
uni.showToast({
title: '图片上传失败!',
icon: 'none'
})
resolve(false)
}
})
})
}

登录函数


如果用户第一次上传资料,我们需要先上传头像并取得图片链接,再将用户资料写入数据库。


async wxLogin() {
if (this.userInfo && this.userInfo.avatarUrl) {
uni.showLoading({
title: '正在上传图片...',
mask: true
});
//上传头像至云储存并返回图片链接
const imageUrl = await uploadImage(this.userInfo.avatarUrl)
if (!imageUrl) {
return
}
this.userInfo = {...this.userInfo, avatarUrl: imageUrl.fileID}
}
uni.showLoading({
title: '登陆中...',
mask: true
});
const _this = this
uni.login({
provider: 'weixin',
success: (res) => {
if (res.code) {
//取得code并调用云函数
uniCloud.callFunction({
name: 'user',
data: {
action: 'code2Session',
js_code: res.code,
user_info: _this.userInfo
},
success: (res) => {
//如register为true,用户未填写资料
if (res.result.result.result.register) {
//_this.showUserInfo 显示填写资料组件
_this.showUserInfo = true
uni.hideLoading();
return
}
if (res.result.result.result._id) {
const data = {
_id: res.result.result.result._id,
mp_wx_openid: res.result.result.result.mp_wx_openid,
register_date: res.result.result.result.register_date
}
this.loginSuccess(data)
}
},
fail: () => {
this.loginFail()
}
})
}
}
})
},

登录成功与失败


在用户登录成功后将数据存入 Storage 中,添加登录过期时间,我这里设置的是七天的登录有效期。


loginSuccess(data) {
updateTokenStorage(data)
updateIsLoginStorage(true)
uni.showToast({
title: '登陆成功!',
icon: 'none'
});
uni.navigateBack()
},

将用户数据存入 Storage,并设置过期时间 expiresTime


export function updateTokenStorage(data = null) {
if (data) {
const expiresTime = new Date().getTime() + 7 * 24 * 60 * 60 * 1000
data = {...data, expiresTime: expiresTime}
}
uni.setStorageSync('user', data)
}

isLogin 用于判断是否是否登录


export function updateIsLoginStorage(data = null) {
uni.setStorageSync('isLogin', data)
}

登录失败


loginFail() {
updateTokenStorage()
updateIsLoginStorage()
uni.showToast({
title: '登陆失败!',
icon: 'none'
});
}

判断是否登录


除了判断 isLogin 还要判断 expiresTime 是否登录过期


//判断是否登陆
export function isLogin() {
try {
const user = uni.getStorageSync('user')
const isLogin = uni.getStorageSync('isLogin')
const nowTime = new Date().getTime()
return !!(isLogin && user && user._id && user.expiresTime > nowTime);
} catch (error) {

}
}

最后


至此就实现了微信登录并将用户信息存入数据库中,我们也可以通过云函数获取用户数据,做出用户个人主页。



以上是我做个人小程序时用的登录流程,整个小程序项目已上传至 GitHub。


GitHub地址


小程序码



作者:Biao
来源:juejin.cn/post/7264592481592705076
收起阅读 »

真的不考虑下grid布局?有时候真的很方便!

web
前言 flex布局大家应该已经运用的炉火纯青了,相信在日常开发中大家和我一样不管遇到什么都是flex一把搜哈。直到我遇到grid,才发现有些场景下,不是说flex实现不了而是使用grid能够更加轻松的完成任务。下面拿几个场景和大家分享一下。 宫格类的布局 比如...
继续阅读 »

前言


flex布局大家应该已经运用的炉火纯青了,相信在日常开发中大家和我一样不管遇到什么都是flex一把搜哈。直到我遇到grid,才发现有些场景下,不是说flex实现不了而是使用grid能够更加轻松的完成任务。下面拿几个场景和大家分享一下。


宫格类的布局


比如我要实现一个布局,最外层元素的宽度是1000px,高度自适应。子元素宽度为300px,一行展示3个,从左到右排列。其中最左边与最右边的元素需要紧挨父元素的左右边框。如下图所示:



使用flex实现


这个页面布局在日常开发中非常常见,考虑下使用flex布局如何实现,横向排列元素,固定宽度300,wrap设置换行显示,设置双端对齐。看起来很简单,来实现一下。


<!DOCTYPE html>
<html lang="en">

<head>
<style>
.box{
width: 1000px;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 10px;
}
.item{
background: pink;
width: 300px;
height: 150px;
}
</style>
</head>

<body>
<div class="box">
<div class="item">1</div>
<div class="item">2</div>
<div class="item">3</div>
<div class="item">4</div>
<div class="item">5</div>
<div class="item">6</div>
<div class="item">7</div>
<div class="item">8</div>
</div>
</body>

</html>


实现之后发现了问题,由于我们设置了双端对齐导致,当最后一行的个数不足三个时,页面展示的效果和我们预期的效果有出入。使用flex实现这个效果就要对这个问题进行额外的处理。


处理的方式有很多种,最常见的处理方式是在元素后面添加空元素,使其成为3的倍数即可。其实这里添加空元素的个数没有限制,因为空元素不会展示到页面上,即使添加100个空元素用户也是感知不到的。个人觉得这并不是一个好办法,在实际处理的时候可能还会遇到别的问题。个人觉得还是把flex下的子元素设置成百分比好一点。


使用grid实现


面对这种布局使用grid是非常方便的,设置3列,每列300px,剩下的元素让它自己往下排即可。几行代码轻松实现该效果,不需要flex那样额外的处理。


<!DOCTYPE html>
<html lang="en">

<head>
<style>
.box {
display: grid;
grid-template-columns: repeat(3, 300px);
justify-content: space-between;
gap: 10px;
width: 1000px;
}

.item {
background: pink;
height: 100px;
}
</style>
</head>

<body>
<div class="box">
<div class="item">1</div>
<div class="item">2</div>
<div class="item">3</div>
<div class="item">4</div>
<div class="item">5</div>
<div class="item">6</div>
<div class="item">7</div>
<div class="item">8</div>

</div>
</body>

</html>


实现后台管理布局



这种后台管理的布局,使用flex实现当然也没有问题。首先需要纵向排列红色的两个div,然后再横向的排列蓝色的两个div,最后再纵向的排列绿色的两个div实现布局。达到效果是没有问题的,但是实现起来较为繁琐,而且需要很多额外的标签嵌套。



由于grid是二维的,所以它不需要额外的标签嵌套。html里面结构清晰,如果需要改变页面结构,只需要改变container的样式就可以了,不需要对html进行修改。


<!DOCTYPE html>
<html lang="en">

<head>
<style>
.container {
display: grid;
grid-template-columns: 250px 1fr;
grid-template-rows: 100px 1fr 100px;
grid-template-areas:
'header header'
'aside main'
'aside footer';
height: 100vh;
}

.header {
grid-area: header;
background: #b3c0d1;
}

.aside {
grid-area: aside;
background: #d3dce6;
}

.main {
grid-area: main;
background: #e9eef3;
}

.footer {
grid-area: footer;
background: #b3c0d1;
}
</style>
</head>

<body>
<div class="container">
<div class="header">Header</div>
<div class="aside">Aside</div>
<div class="main">Main</div>
<div class="footer">Footer</div>
</div>
</body>

</html>

实现响应式布局


借助grid的auto-fillminmax函数可以实现类似响应式布局的效果,可以应用在后台管理的表单布局等场景。



<!DOCTYPE html>
<html lang="en">

<head>
<style>
.box {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
justify-content: space-between;
gap: 10px;
}

.item {
background: pink;
height: 100px;
}
</style>
</head>

<body>
<div class="box">
<div class="item">1</div>
<div class="item">2</div>
<div class="item">3</div>
<div class="item">4</div>
<div class="item">5</div>
<div class="item">6</div>
<div class="item">7</div>
<div class="item">8</div>
</div>
</body>

</html>

兼容性对比


flex的兼容性


image.png


grid的兼容性


image.png


可以看到grid在兼容性上还是不如flex,grid虽然强大,但是在使用前还是需要先考虑一下项目的用户群体。


结尾


除了上述场景外肯定还有许多场景适合使用grid来完成。gridflex都是强大的布局方式,它们并没有明显的优劣之分。关键在于掌握这两种方法,并在开发中根据实际情况选择最合适的方案。


希望大家能有所收获!


作者:欲买炸鸡同载可乐
来源:juejin.cn/post/7326816030042669110
收起阅读 »

一些不被人熟知,但又很好用的HTML属性

web
HTML(超文本标记语言)具有多种属性,可用于增强我们的网页的结构和功能。 下面我就给大家介绍一下,一些很好用的HTML属性,但是不被人熟知的HTML属性 contenteditable: 这个属性使我们的元素变的可编辑。用户可以直接在我们的浏览器中修改元素的...
继续阅读 »

HTML(超文本标记语言)具有多种属性,可用于增强我们的网页的结构和功能。
下面我就给大家介绍一下,一些很好用的HTML属性,但是不被人熟知的HTML属性


contenteditable:


这个属性使我们的元素变的可编辑。用户可以直接在我们的浏览器中修改元素的内容。


<div contenteditable="true">
这段内容可以被编辑。
</div>

使用场景:
-可以用来创建富文本编辑器,使用户能够在网页中创建、编辑和格式化文本,


spellcheck:


该属性用于启用或禁用元素的拼写检查功能。(如果用户输入的单词拼写有误,浏览器通常会标记出来并提供纠正建议)


<textarea spellcheck="true">
这个文本区域启用了拼写检查。
</textarea>

image.png


使用场景:



  • 可以在文章创作者的富文本编辑器中使用,辅助文章创作


代码演示:


draggable:


该属性使元素可拖动。通常与 JavaScript 结合使用,实现拖放功能。


<img src="image.jpg" draggable="true" alt="可拖动的图片">

使用场景:



  • 在电子商务网站中,用户可以拖动产品图像到购物车区域,以便快速添加商品到购物清单。

  • 在可视化数据分析工具中,用户可以通过拖拽图表或数据元素来定制自己的数据可视化图形。

  • 可以创建一个可拖放的低代码平台


代码演示:


sandbox:


与 元素一起使用,sandbox 属性限制了嵌入内容的行为,如阻止执行脚本或提交表单。

<iframe src="sandboxed-page.html" sandbox="allow-same-origin allow-scripts"></iframe>

使用场景:



  • 可以在电子邮件客户端中,通过使用 sandbox 属性限制电子邮件中嵌入内容的行为,以确保安全性并防止恶意代码执行。

  • 可以在需要嵌入第三方内容(如广告、外部应用程序等)但又需要限制其行为的情况下使用。这可以防止嵌入的内容执行恶意脚本或访问敏感信息。


download:


该属性与 <a>(锚点)元素一起使用,指定用户单击链接时应下载的目标。


<a href="document.pdf" download="my-document">下载 PDF</a>

使用场景:



  • 可用于提供下载链接,例如下载文档、图像或其他文件。这使得用户可以通过单击链接直接下载相关内容而无需离开页面。


hidden:


该属性用于隐藏页面上的元素。这是最初隐藏内容的简单方法,可以通过 CSS 或 JavaScript 在后来显示。


<p hidden>这个段落最初是隐藏的。</p>

使用场景:



  • 在网页中使用弹出式模态框或折叠式面板,可以利用 hidden 属性来最初隐藏它们,并在用户点击或触发特定事件时展现。

  • 在网页表单验证中,可以将错误消息初始隐藏,只有当用户提交表单出现错误时才显示出来。


defer:



<script defer src="myscript.js"></script>

使用场景:



  • 在网页底部延迟加载 JavaScript 脚本,以确保页面内容首先加载,提升页面加载速度和性能。

  • 对于异步加载的脚本(async),适用于对网页影响较小的辅助性 JavaScript 脚本,例如网页分析工具或跟踪代码。


async:


类似于 defer,async 属性与

<script async src="myscript.js"></script>

使用场景:



  • 在网页底部延迟加载 JavaScript 脚本,以确保页面内容首先加载,提升页面加载速度和性能。

  • 对于异步加载的脚本(async),适用于对网页影响较小的辅助性 JavaScript 脚本,例如网页分析工具或跟踪代码。


Accept 属性:


你可以将 accept 属性与 元素(仅适用于文件类型)一起使用,以指定服务器可以接受的文件类型。


<input type="file" accept=".jpg, .jpeg, .png">

使用场景:



  • 在上传图片的社交媒体平台中,限制用户只能上传特定格式(如 JPG、PNG)的图片文件,确保图片质量和页面加载速度。

  • 在在线应用程序中,限制用户只能上传特定类型的文件,例如在云存储服务中只允许上传文档文件。


Translate:


该属性用于指定在页面本地化时,元素的内容是否应该被翻译。


<p translate="no">这段内容不应被翻译。</p>

作者:zayyo
来源:juejin.cn/post/7303789262989443083
收起阅读 »

Celeris Web,一套女生都觉得好看的Vue3模板

web
Vue3+Unocss+NaiveUI+Monorepo搭建一套女生觉得好看的前端模板 一年前,我刚刚从后端转入前端的大门,兴奋又迷茫。身边的女性朋友们总是找我帮忙写小工具,但每次都被吐槽UI太丑了。于是,我想,能不能搞点不一样的? 嗯,女生总是很喜欢漂亮的东...
继续阅读 »

Vue3+Unocss+NaiveUI+Monorepo搭建一套女生觉得好看的前端模板


一年前,我刚刚从后端转入前端的大门,兴奋又迷茫。身边的女性朋友们总是找我帮忙写小工具,但每次都被吐槽UI太丑了。于是,我想,能不能搞点不一样的?


嗯,女生总是很喜欢漂亮的东西,对吧?于是我决定写一款前端开发模板,让开发出来的工具她们用起来不仅方便,还得有点美美哒。Vue 3、Unocss、NaiveUI、Monorepo,这些都是我的秘密武器。我取名它为Celeris Web


这个开发框架采用了最新的技术,包括Vue 3、Vite和 TypeScript。而且,这个项目的设计初衷就用了monorepo的方法使得依赖管理和多个项目的协作变得轻松。这可是一套为开发人员提供了构建现代Web应用程序的全面解决方案哦。


不管你是老手还是新手,Celeris Web都能给你提供一个简化的前端开发流程,利用最新的工具和技术。是不是觉得很吸引人?


Snipaste_2024-01-16_14-27-03.png


Celeris Web的特点



  • ⚡ 闪电般快速:使用Vue 3,Vite和pnpm构建 🔥

  • 💪 强类型:使用TypeScript 💻

  • 📂 单库存储:易于管理依赖项和协作多个项目 🤝

  • 🔥 最新语法:使用新的< script setup >语法 🆕

  • 📦 自动导入组件:自动导入组件 🚚

  • 📥 自动导入API:使用unplugin-auto-import直接导入Composition API和其他API 📨

  • 💡 官方路由器:使用Vue Router v4 🛣️

  • 🎉 加载反馈:使用NProgress提供页面加载进度反馈 🔄

  • 🍍 状态管理:使用Pinia进行状态管理 🗃️

  • 📜 中文字体预设:包含中文字体预设 🇨🇳

  • 🌍 国际化就绪:具备使用本地化的国际化功能 🌎

  • ☁️ Netlify准备就绪:在Netlify上零配置部署 ☁️


有了Celeris Web,你的前端开发之路将更加轻松愉快!🚀


中英文双语注释


在Celeris Web的设计中,我们注重代码的可读性和学习性,为此,我们为每个函数都配备了中英文双语注释,以确保无论您的母语是中文还是英文,都能轻松理解和学习代码。


为什么选择中英文双语注释?



  1. 全球协作: 在多语言团队中,中英文双语注释能够促进更好的沟通和协作,确保团队成员都能准确理解代码的功能和实现。

  2. 学习便捷: 对于新手来说,中英文双语注释提供了更友好的学习环境,帮助他们更快速地掌握代码的逻辑和结构。

  3. 开发者友好: 我们致力于构建一个开发者友好的开发环境,中英文双语注释是我们为实现这一目标而采取的一项关键措施。

  4. 示例:


    /**
    * 打开一个新的浏览器窗口
    * Open a new browser window
    *
    * @param {string} url - 要在新窗口中打开的 URL
    * The URL to open in the new window
    *
    * @param {object} options - 打开窗口的选项
    * Options for opening the window
    * @param {string} options.target - 新窗口的名称或特殊选项,默认为 "_blank"
    * @param {string} options.features - 新窗口的特性(大小,位置等),默认为 "noopener=yes,noreferrer=yes"
    */

    export function openWindow(url: string, { target = "_blank", features = "noopener=yes,noreferrer=yes" }: {
    target?: "_blank" | "_self" | "_parent" | "_top"; // 新窗口的名称或特殊选项,默认为 "_blank"
    features?: string; // 新窗口的特性(大小,位置等),默认为 "noopener=yes,noreferrer=yes"
    } = {}
    ) {
    window.open(url, target, features);
    }

    通过这样的中英文双语注释,我们希望为开发者提供更愉悦、更高效的编码体验,让Celeris Web成为一个真正容易上手和深入学习的前端模板。



Monorepo 设计的好处


1. 依赖管理更轻松: Monorepo 将所有项目的依赖项集中管理,避免了不同项目之间版本冲突的问题,使得整体的依赖管理更加清晰和简便。


2. 代码共享与重用: 不同项目之间可以方便地共享和重用代码,减少重复开发的工作量。这对于保持代码一致性和提高开发效率非常有利。


3. 统一的构建和部署: Monorepo 可以通过统一的构建和部署流程,简化整个开发过程,减少了配置和管理的复杂性,提高了开发团队的协作效率。


4. 统一的版本控制: 所有项目都在同一个版本控制仓库中,使得版本管理更加一致和可控。这有助于团队协同开发时更好地追踪和处理版本问题。 Monorepo设计让Celeris Web不仅是一款后台管理系统模板,同时也是一个快速开发C端产品的前端Web模板。有了Celeris Web,前端开发之路将更加轻松愉快!🚀


设计理念:突破Admin管理的局限性,关注C端用户体验


在市面上,大多数前端模板都着眼于满足B端用户的需求,为企业管理系统(Admin)提供了强大的功能和灵活的界面。然而,很少有模板将C端产品的特点纳入设计考虑,这正是我们Celeris Web的创新之处。


突破Admin管理的局限性:


传统的Admin管理系统更注重数据展示和业务管理,但C端产品更加侧重用户体验和视觉吸引力。我们深知C端用户对于界面美观、交互流畅的要求,因此Celeris Web不仅提供了强大的后台管理功能,更注重让前端界面在用户层面上达到更高水平。


关注C端用户体验:


Celeris Web不仅仅是一个后台管理系统的模板,更是一个注重C端用户体验的前端Web模板。我们致力于打破传统Admin系统的束缚,通过引入崭新的设计理念,使得C端产品在前端呈现上具备更为出色的用户体验。


特色亮点:



  • 时尚美观的UI设计: 我们注重界面的美感,采用现代化设计语言,使得Celeris Web的UI不仅仅是功能的堆砌,更是一种视觉盛宴,让C端用户爱不释手。

  • 用户友好的交互体验: 考虑到C端用户的习惯和需求,Celeris Web注重交互体验的设计,通过流畅的动画效果和直观的操作,使用户感受到前所未有的愉悦和便捷。

  • 个性化定制的主题支持: 我们理解C端产品的多样性,因此提供了丰富的主题定制选项,让每个C端项目都能拥有独一无二的外观,更好地满足产品个性化的需求。


通过这一独特的设计理念,Celeris Web致力于在前端开发领域探索全新的可能性,为C端产品注入更多活力和创意。我们相信,这样的创新将带来更广泛的用户认可和更高的产品价值。在Celeris Web的世界里,前端不再局限于Admin系统,而是融入了更多关于用户体验的精彩元素。


后期发展路线:瞄准AIGC,引领互联网产品变革


随着人工智能与图形计算(AIGC)技术的崛起,我们决定将Celeris Web的发展方向更加专注于推动AIGC相关产品的研发和落地。这一战略决策旨在顺应互联网产品的变革浪潮,为未来的科技创新开辟全新的可能性。


AIGC技术引领变革:


AIGC的兴起标志着互联网产业迎来了一场技术变革,为产品带来更加智能、交互性更强的体验。Celeris Web将积极响应这一变革,致力于为开发者提供更优秀的工具,助力他们在AIGC领域创造更具前瞻性的产品。


模板的研发重心:


在后期的发展中,Celeris Web将更加重视AIGC相关产品的研发需求。我们将推出更多针对人工智能的功能模块,使开发者能够更便捷、高效地构建出色的AIGC应用。


专注产品落地:


除了技术研发,我们将加强对AIGC产品落地的支持。通过提供详实的文档、示例和定制化服务,Celeris Web旨在帮助开发者更好地将AIGC技术融入他们的实际项目中,实现技术创新与商业应用的有机结合。


开放合作生态:


为了推动AIGC技术的更广泛应用,Celeris Web将积极构建开放合作生态。与行业内优秀的AIGC技术提供商、开发者社区保持密切合作,共同推动AIGC技术的发展,携手打造更加繁荣的互联网产品生态圈。


Celeris Web未来的发展将以AIGC为核心,我们期待在这个快速发展的技术领域中,与开发者们一同探索、创新,共同引领互联网产品的未来。通过持续的努力和创新,Celeris Web将成为AIGC领域的引领者,助力开发者创造更加智能、引人入胜的互联网产品。


源码


kirklin/celeris-web (github.com)


作者:KirkLin
来源:juejin.cn/post/7324334380373688371
收起阅读 »

揭秘 "mitt" 源码:为什么作者钟情于 `map` 而放弃 `forEach`

web
故事是这样的,半年前我提交了一个 Pull Request(PR),想要将作者在代码中使用的 map 改成 forEach, 而作者的回应却是:map() is used because it is 3 bytes smaller when gzipped. ...
继续阅读 »

故事是这样的,半年前我提交了一个 Pull Request(PR),想要将作者在代码中使用的 map 改成 forEach


而作者的回应却是:map() is used because it is 3 bytes smaller when gzipped. (使用 map 是因为在 Gzip 压缩时可以减小 3 字节的体积。)


咦?为什么会这样呢?


"mitt" 简介


首先,让我们认识一下 "mitt",它是一只小巧灵活的事件发射器(event emitter)库,体积仅有 200 字节,但功能强大。这个小家伙在项目中充当了事件的传播者,有点像是一个小型的邮差,把消息传递给需要它的地方。


developit/mitt: 🥊 Tiny 200 byte functional event emitter / pubsub. (github.com)


作者的选择:map vs forEach


在源码中,我们发现作者选择使用了 Array.prototype.map(),这是一个处理数组每个元素并返回新数组的函数。然而,有趣的地方在于,作者并没有在 map 中返回任何值。这和我对 map 的期望有些出入,因为我们习惯于用它生成一个新的数组。


代码的细微变化


曾经,代码片段是这样的,作者想要用 map 来执行一些操作,但却不生成新数组。


if (handlers) {
(handlers as EventHandlerList<Events[keyof Events]>)
.slice()
.map((handler) => {
handler(evt!);
});
}

我希望修改成这样:


if (handlers) {
(handlers as EventHandlerList<Events[keyof Events]>)
.slice()
.forEach((handler) => {
handler(evt!);
});
}

所以我很快就交了个PR:将map改成了forEach,经过了几个月的等待,PR被拒了,作者的回应是:map() is used because it is 3 bytes smaller when gzipped.(使用 map 是因为在 Gzip 压缩时可以减小 3 字节的体积。)


code.png


pr.png


小技巧背后的逻辑


虽然 map 通常用于生成新数组,但作者在这里使用它更像是在借助压缩的优势,让代码更轻量。


大小对比


通过实验验证,使用 map 的打包大小确实稍微小一些:



  • 使用 map 时,打包大小为:


  - 190 B: mitt.js.gz
- 162 B: mitt.js.br
- 189 B: mitt.mjs.gz
- 160 B: mitt.mjs.br
- 268 B: mitt.umd.js.gz
- 228 B: mitt.umd.js.br


  • 而使用 forEach 后,打包大小为:


  - 192 B: mitt.js.gz
- 164 B: mitt.js.br
- 191 B: mitt.mjs.gz
- 162 B: mitt.mjs.br
- 270 B: mitt.umd.js.gz
- 230 B: mitt.umd.js.br

进一步实验


为了深入了解选择的影响,我又进行了一个实验。有趣的是,当我将代码中的一处使用 map 改为 forEach,而另一处保持不变时,结果居然是打包体积更大了。


experiment_results.png


总结


这个故事让我不仅仅关注于代码表面,还开始注重微小选择可能带来的影响。学到了很多平时容易忽略的点,"mitt" 作者的选择展现了在开发中面对权衡时的智慧,通过选择不同的API,以轻松的方式达到减小代码体积的目标。在编写代码时,无处不充满着权衡的乐趣。


如果你对这个故事有更多的想法或者其他技术话题感兴趣,随时和我分享哦!


作者:KirkLin
来源:juejin.cn/post/7327424955037564965
收起阅读 »

使用pixi.js开发一个智慧路口(车辆轨迹追踪)项目

web
项目效果 项目功能: 位置更新、航向角计算。 debug模式。 位置角度线性补帧。 变道、转弯、碰撞检测。 mock轨迹数据 图片效果: 视频效果: 项目启动 项目地址 github:(github.com/huoguozhang…) 线上:todo...
继续阅读 »

项目效果


项目功能:



  • 位置更新、航向角计算。

  • debug模式。

  • 位置角度线性补帧。

  • 变道、转弯、碰撞检测。

  • mock轨迹数据


图片效果:


result.gif


视频效果:



项目启动


项目地址



(如果觉得项目对你有帮助的话, 可以给我一个star 和 赞,❤️)


启动demo项目



  1. cd car-tracking-2d/demos/react-demo

  2. yarn

  3. yarn start


界面使用


debug 模式


浏览器url ?后面(search部分)加入参数debug=1


例如:http://localhost:3000/home?tunnelNo=tunnel1&debug=1


将会展示调试信息:


image.png


如图:车旁边的白色文字信息为debug模式才会展示的内容(由上到下为:里程、车id、车道id、[x,y]、旋转角度)


实现:


技术栈:


ts+pixi.js+任意前端框架


(前端框架使用vuereact或者其他框架都可以。只需要在mounted阶段,实例化我们暴露出来class即可。然后在destroyed或者unmounted阶段destory示例即可,后面会提到。)


pixi.js


官网介绍:



Create beautiful digital content with the fastest, most flexible 2D WebGL renderer.



pixi.js是一个2D的WebGL的渲染库。但是没有three.js知名度高。一个原因是,我们2D的需求技术路线很多,可以是dom、svg、canvas draw api等,包括本项目也可以使用其他技术方案实现,希望通过本文,大家在实现这种频繁更新元素位置的功能,可以考虑一下pixi.js。



API快速讲解

这里只讲我们项目使用到的


Application

import * as PIXI from 'pixi.js';

const app = new PIXI.Application({
view: canvasDom // canvas dom 对象
});


Container

容器,功能为一个组。
当我们设置容器的scale(缩放)、rotation(旋转)、x、y(位置)时。里面的元素都会收到影响。


(ps:app.stage也是一个Container


每个 Container可以通过addChild(增加子节点)、removeChild(删除子节点),也可以设置子元素的zIndex(和css的功能一致)。子原始的scale(缩放)、rotation(旋转)、x、y(位置)是相对于Container的。


Sprite

精灵,渲染图片对象。


carObj = Sprite.from('/car.svg')

Sprite.from(url),url相同的话,只会加载一次图片。纹理对象也只会创建一次。


anchor属性其他对象也有,设置定位点,类似于csstransform-origin


执行下面代码
carObj.anchor.set(0.5, 0.5)


如果x = 10 y =10,carObj的中心点的坐标就是(10,10),旋转原点也是(10,10),缩放也是如此。


Graphics

绘制几何图形,圆弧,曲线、直线等都可以。也支持fill和stroke,canvas draw api支持的,Graphics都支持。


Text

文本,比较简单。字体、颜色、大小,都支持。



  • 值得注意的是文本内容含有换行符时(\n \r),文本会换行。

  • pixi提供测量文本的width height的方法非常好用。


Tick

this.app.ticker.add(() => {})

类似于requestAnimationFrame


具体实现


分三步,vue/react都一样:


1 获取canvas dom通过ref的方式。


2 创建我们封装Stage Road


3 组件销毁时,执行 stage.destroy(注意stage是我封装的,不是pixi的。使用方不需要使用pixi.js的api)


线性插帧

当有一个对象由坐标 点a(0,0)变换到点b(1000,1000),1秒内完成。
中间的变化值为:
dx =1000 dy=1000
记录每帧的时间差t(当前帧距离第0帧的,单位毫秒)


所以第n帧位置信息为(0+dx / 1000 * t, 0+ dy /1000 *t)


角度变换也是这个道理。


位置坐标获取

如果直线长度为1000px,对应的实际里程为100米。


当跑了50米,当前就是直线的中点坐标。
弯道呢,通过弧度可以推算出坐标。
可以把 Road.ts line 70的注释取消。


 // 方便开发观察 绘制车道线 ---begin----
// this.mount(lane.centerLine.paint())

航向角

直线简单,通过Math.atan2可以求出来。
弯道需要通过解析几何,计算出圆弧切线,然后推测出航向角。


转弯

mark.png
可以查看我们标注的一些点


以1到7的弯道举例,相当于是从新创建一次车道,车道的点是车道1和车道7的组合。
我们通过 circle属性配置,在创建Road


{
uid: '1-2',
x: 1072,
y: 1605,
circle: {
// 编号形式 车道序号-第几个点

linkId: '7-3'
}
},

这条信息表示:车道1的第2个点(uid),有圆弧链接到车道7的第3个点(circle.linkId)


碰撞检测

我们这个项目的特点是,前端展示,实际后端返回什么数据,我们就展示什么数据。(一般不需要前端处理)。
这里我们mock的数据就简单处理一下。判断是否存在相交的线段(当前对象的位置和将要到达的点),如果线段相交,车辆暂停移动。


作者:火锅小王子
来源:juejin.cn/post/7327467832866095130
收起阅读 »

微信小程序开发大坑盘点

web
微信小程序开发大坑盘点 起因 前几天心血来潮,想给学校设计个一站式校园小程序,可以查询成绩,考试信息,课表之类的(本来想法里是还想包括一些社交功能的,但这个因为资质问题暂且搁置了)。其实很久以前就有大概了解过微信小程序的一些概念,那个时候试图用 uni-app...
继续阅读 »

微信小程序开发大坑盘点


起因


前几天心血来潮,想给学校设计个一站式校园小程序,可以查询成绩,考试信息,课表之类的(本来想法里是还想包括一些社交功能的,但这个因为资质问题暂且搁置了)。其实很久以前就有大概了解过微信小程序的一些概念,那个时候试图用 uni-app 做,但是这玩意太难用所以不了了之了。


于是这次打算正经的用微信自己的那套东西做,结果不出意外的是入了深坑......


大坑


微信小程序云函数外部调用异常


微信小程序提供 wx.request 发起 HTTP 请求,由于微信不是浏览器,没有跨域限制,这方便了很多事,但是由于 wx.request 函数只能对 HTTPS 协议的地址发起请求,而我们学校的教务系统又是清一色的 HTTP,因此我需要一个可以用来帮助我发起 HTTP 请求的转发接口。


对于这种简单需求,云函数显然是最好的解决方案,进而我发现微信小程序自带云函数的支持,于是便兴冲冲地写了一段 NodeJS 代码,放上去跑。


结果我发现不知道为什么,请求其他网站都没问题,唯独请求我们教务系统就会原地超时。经过了几个小时的调试,最后以失败告终,转而改用腾讯云的云函数。


代码也十分简单:


const url = require('url')

const express = require('express');
const app = express()
const port = 9000

const rp = require('request-promise')

app.use(express.json());

app.post('/', async (req, res) => {
const jar = rp.jar()

try {
const response = await rp({
...req.body,
resolveWithFullResponse: true,
simple: false,
jar: jar
})
res.json(response)
} catch (e) {
res.json(e)
console.error(e)
}
})

app.listen(port, () => {
console.log("Successfully loaded")
})

其中额外引入了 request-promise 库(express 是默认引入的,腾讯云函数这里做的不错,对 npm 支持很好)。


然后做了一个模仿 wx.request 调用风格的 request 函数,这样我就可以在 wx.request 和我自己的 request 函数中无缝切换(更进阶的是,我自己写的这个还额外支持了以 Promise 风格调用。


export async function request(data) {
try {
const res = await rp({
...data,
uri: data.url,
headers: data.header,
})
let result = {
...res,
data: res.body,
header: res.headers
}
if (result.statusCode != 200) {
throw {
err_msg: "内部错误"
}
}
if (data.dataType === 'json') {
result.body = JSON.parse(result.body)
}
data.success && data.success(result);
data.complete && data.complete({})
return result;
} catch (e) {
data.fail && data.fail(e)
data.complete && data.complete({})
throw e;
}
}

function rp(data) {
return new Promise((resolve, reject) => {
wx.request({
method: 'POST',
url: 'https://service-abcdefg-123456789.gz.apigw.tencentcs.com/release/',
data: data,
success: (res) => {
resolve(res.data)
},
fail: (err) => {
reject(err)
}
})
})
}

ES6 module 和变量作用域支持差


不知道为什么,微信小程序完全不支持 ES6 module,即使它是支持 ES6 语法的。也就是说,你只能使用这种传统的 CommonJS 方式引入:


const module = require('module.js')

而不是 ES6 的 import 语法:


import module from 'module.js'

最离谱的是,微信小程序这个基于 VSCode 的编译器会给你 warn 这段代码,告知你可以转换为使用 import 导入。



于是这又引出了另外一个奇怪的问题:当你在一个界面的逻辑层文件上声明变量时,IDE 会认为这个变量是一个全局变量,因此在其他界面声明同名变量会得到一个 error,即使不会导致任何编译错误。


这导致了,现在我的模块引入必须用一种很奇怪的写法...


const sessionModule = require('../../utils/session');
const tgcModule = require('../../utils/tgc')
const cryptoModule = require('../../miniprogram_npm/crypto-js/index.js')

奇葩的 NPM 支持


在以前,微信小程序是不支持包管理器的,这也就意味着,你得手动把那些库的 JS 复制到你的项目目录里再引用,非常麻烦。但是现在好了,微信可以自动帮你做这件事了。


没错,是自动帮你复制,而不是做了包管理器支持。


怎么说呢...你需要先在你的项目源代码目录中 init 一个 package.jsonadd 你需要的包然后 install,接下来点击 IDE 顶栏的 Tools - Build npm 选项,Weixin Devtools 就会帮你生成一个 miniprogram_npm文件夹,将每个项目各自 combine 到一个 index.js 然后塞到各自名字的文件夹里,然后,你就能通过上面那种方式手动引入使用了。


很奇葩但是... 勉强能用(而且不限制使用的包管理器,比如我用的就是 yarn)。


避免使用双向绑定


微信小程序的 WXML 存在一个有限的双向绑定支持,也是类似 Vue 的那种语法糖:


<input model:value="{{value}}" />

但是这个双向绑定不知道为什么,在某些情况下会认为你没有设置一个 bindinput 事件(但实际上应该是由双向绑定自动设置的),于是不断地在后台刷警告,因此还不如手动实现来的省心。


有限的标准组件支持


如果你觉得微信小程序的开发和前端开发差不多,那就大错特错了。因为微信小程序默认情况下根本不支持任何 HTML 元素,而是套了一层他们自己的元素,比如 view 实际上是 classblock 则和 Vue 的 template 差不多(微信小程序也有 template 元素,只不过那个是给组件用的),不分 h1, h2, span, strong,只有 text 元素等。当然好在 CSS 还是那套,基本都能用。


但是... 微信小程序提供的元素依然太少了,根本没办法满足实际开发需要(比如根本没有表格元素)。于是微信小程序提供了一个 rich-text 元素,可用于渲染 HTML 元素。


但是这个 rich-text 就显得十分鸡肋,他不是通过 slot 传入 HTML 元素,而是通过 string 或者 object。这凭空增加了开发难度,导致我不得不这么写:


<rich-text nodes="{{nodes}}"></rich-text>

this.setData({
nodes: licenses.map(it => {
return `
<div style="margin: 20px 10px;"><strong>${it.projectName}</strong>
is licensed under the <code>${it.licenseName}</code>:</div>
<pre style="overflow: auto; background-color:#F5F6FA;"><code>${it.fullLicense}</code></pre>
${it.sourceRepo?`<div style="margin: 20px 10px;"><span style="color:gray; font-size: 12px;">The source code can be found at: ${it.sourceRepo}</span></div>`:""}
<br/><br/>
`

}).join("")
})

甚至这么写:



完美的回答了知乎有人“为什么不用 JSON 表达页面而是用类似 XML 一样的 HTML”的问题。


最后


虽然吐槽了这么多,但是微信小程序还是有一些不错的点的。除了上面说的宽松的跨域策略以外,微信小程序的 TypeScript 支持很完善,IDE 工具链做的也不错(除了他那个特别容易崩溃的 Simulator),加之微信开放社区的活跃度也不低(问问题一天内就有人回复),也算是能用了。


作者:HikariLan贺兰星辰
来源:juejin.cn/post/7228563544022761509
收起阅读 »

一行代码快速实现全局模糊

web
github 仓库:github.com/astak16/blu…npm 仓库:http://www.npmjs.com/package/blu…页面在展示时,某些敏感的数据不想展示,可以使用该插件,对敏感数据进行模糊处理敏感数据过滤通常是由后端去做的,有时候...
继续阅读 »

github 仓库:github.com/astak16/blu…

npm 仓库:http://www.npmjs.com/package/blu…

页面在展示时,某些敏感的数据不想展示,可以使用该插件,对敏感数据进行模糊处理

敏感数据过滤通常是由后端去做的,有时候在某些场合不想展示某些数据,这时让后端去改代码,再重新部署,这样的成本太高,所以前端也需要有这样的能力,可以在前端对敏感数据进行模糊处理,这样就不需要后端参与了

前端过滤文本,通常有两种方法:

  1. 拦截响应,对文本进行过滤后在渲染在页面上
  2. 渲染在页面上后,对文本进行过滤

对于方案一,需要遍历所有的响应对象的所有属性,而且只能替换或者删除文本,无法实现个性化的配置

所以我选择了方案二,当页面渲染结束后,遍历所有的 dom,对文本进行过滤,这样可以实现个性化的配置

遍历 dom,你能想到最直接的方法是:

  1. document.querySelectorAll("*")
  2. document.body.getElementsByTagName("*")

然后在遍历找到的所有 dom,对文本进行过滤,这样效率会很差

所以我选择了 TreeWalker 遍历 dom,效率会高很多

技术说明

treeWalker 是通过 document.createTreeWalker() 创建的,传入 NodeFilter.SHOW_TEXT 参数,就可以拿到页面中所有的文本节点

然后用正则匹配文本,如果匹配到了,就对文本进行模糊处理:

const wrapTextWithTag = (node: Node) => {
const oldText = node?.textContent;
if (!oldText) return;
const regexNumber = /[-+]?\d{1,3}(?:,\d{3})*(?:\.\d+)?/;
const regexWord = new RegExp(`(${this.words.join("|")})`);
const mergedRegex = new RegExp(
`(${regexNumber.source}|${regexWord.source})`,
"g"
);

if (mergedRegex.test(oldText)) {
const rep = oldText.replace(mergedRegex, "$1");
const arr = rep.split(/<\/?span>/);
let span;
node.textContent = "";
for (let i = 0; i < arr.length; i++) {
const newText = arr[i];
const isContainsWord = this.isContainsWord(newText);
const isContainsNumber = this.isContainsNumber(newText);
if (!this.isVoid(newText) && (isContainsWord || isContainsNumber)) {
span = this.createElementAndSetBlurred(newText);
} else {
span = document.createTextNode(newText);
}
node.parentElement?.insertBefore(span, node);
}
}
};

如果到此结束的话,只能处理静态的文本,如果是异步数据,就无法处理了,所以还需要监听 dom 的变化,对新增的 dom 进行模糊处理

监听 dom 可以使用 MutationObserver,将 subtree 设置为 true,就可以监听到所有的 dom 变化

const observer = new MutationObserver((mutationList) => {
mutationList.map((mutation) => {
const node = mutation.addedNodes[0];
if (!node) return;
const isStyle = this.hasStyle(node);
if (node.nodeType === 1 && !isStyle && !this.isVoid(node.textContent)) {
this.blurryWordsWithTag(node);
}
});
});
observer.observe(document.body, { childList: true, subtree: true });
this.observer = observer;

这里需要说明一点:默认不开启异步数据模糊处理,因为这样会影响性能,如果需要开启,设置 autoBlur 为 true

推荐使用方式:页面设置个按钮,点击按钮开启模糊,这样可以避免一些性能问题

使用

  1. 安装
npm i blurryjs
# 或
yarn add blurryjs
# 或
pnpm i blurryjs
  1. 引入
import Blur from "blurryjs";
  1. 快速使用
const blur = new Blur(); // 默认全局数字模糊

参数说明

5 个参数

  1. blur?: number:模糊程度,单位:px,默认 2px
  2. blurryWords?: string[]:需要模糊的词,默认数字模糊
  3. operator?: string:默认模糊,可以是其他符号,比如 * 等
  4. autoBlur?: boolean:是否自动模糊,默认 false,只处理静态文本,如果需要处理异步数据,需要设置为 true
  5. excludes?: string[]:排除不需要模糊的 dom,类名,比如 ["ant-input"]
const blur = new Blur({
blur: 2,
blurryWords: ["周杰伦", "陈奕迅"],
operator: "*",
autoBlur: false,
excludes: ["ant-input"],
});

3 个方法:

  1. enableBlur(options):开启模糊,options 参数为:blurblurryWordsoperatorautoBlur

    blur.enableBlur({
    blur: 2,
    blurryWords: ["1", "2", "3"],
    operator: "*",
    autoBlur: false,
    });
  2. disableBlur:关闭模糊

    blur.disableBlur();
  3. destroy:销毁

    blur.destroy();

demo

git clone https://github.com/astak16/blurryjs.git

cd blurryjs

pnpm i

pnpm dev

效果

Kapture 2023-06-30 at 14.48.53.gif


作者:uccs
来源:juejin.cn/post/7250311237107122232github 仓库:github.com/astak16/blu…

npm 仓库:http://www.npmjs.com/package/blu…

页面在展示时,某些敏感的数据不想展示,可以使用该插件,对敏感数据进行模糊处理

敏感数据过滤通常是由后端去做的,有时候在某些场合不想展示某些数据,这时让后端去改代码,再重新部署,这样的成本太高,所以前端也需要有这样的能力,可以在前端对敏感数据进行模糊处理,这样就不需要后端参与了

前端过滤文本,通常有两种方法:

  1. 拦截响应,对文本进行过滤后在渲染在页面上
  2. 渲染在页面上后,对文本进行过滤

对于方案一,需要遍历所有的响应对象的所有属性,而且只能替换或者删除文本,无法实现个性化的配置

所以我选择了方案二,当页面渲染结束后,遍历所有的 dom,对文本进行过滤,这样可以实现个性化的配置

遍历 dom,你能想到最直接的方法是:

  1. document.querySelectorAll("*")
  2. document.body.getElementsByTagName("*")

然后在遍历找到的所有 dom,对文本进行过滤,这样效率会很差

所以我选择了 TreeWalker 遍历 dom,效率会高很多

技术说明

treeWalker 是通过 document.createTreeWalker() 创建的,传入 NodeFilter.SHOW_TEXT 参数,就可以拿到页面中所有的文本节点

然后用正则匹配文本,如果匹配到了,就对文本进行模糊处理:

const wrapTextWithTag = (node: Node) => {
const oldText = node?.textContent;
if (!oldText) return;
const regexNumber = /[-+]?\d{1,3}(?:,\d{3})*(?:\.\d+)?/;
const regexWord = new RegExp(`(${this.words.join("|")})`);
const mergedRegex = new RegExp(
`(${regexNumber.source}|${regexWord.source})`,
"g"
);

if (mergedRegex.test(oldText)) {
const rep = oldText.replace(mergedRegex, "$1");
const arr = rep.split(/<\/?span>/);
let span;
node.textContent = "";
for (let i = 0; i < arr.length; i++) {
const newText = arr[i];
const isContainsWord = this.isContainsWord(newText);
const isContainsNumber = this.isContainsNumber(newText);
if (!this.isVoid(newText) && (isContainsWord || isContainsNumber)) {
span = this.createElementAndSetBlurred(newText);
} else {
span = document.createTextNode(newText);
}
node.parentElement?.insertBefore(span, node);
}
}
};

如果到此结束的话,只能处理静态的文本,如果是异步数据,就无法处理了,所以还需要监听 dom 的变化,对新增的 dom 进行模糊处理

监听 dom 可以使用 MutationObserver,将 subtree 设置为 true,就可以监听到所有的 dom 变化

const observer = new MutationObserver((mutationList) => {
mutationList.map((mutation) => {
const node = mutation.addedNodes[0];
if (!node) return;
const isStyle = this.hasStyle(node);
if (node.nodeType === 1 && !isStyle && !this.isVoid(node.textContent)) {
this.blurryWordsWithTag(node);
}
});
});
observer.observe(document.body, { childList: true, subtree: true });
this.observer = observer;

这里需要说明一点:默认不开启异步数据模糊处理,因为这样会影响性能,如果需要开启,设置 autoBlur 为 true

推荐使用方式:页面设置个按钮,点击按钮开启模糊,这样可以避免一些性能问题

使用

  1. 安装
npm i blurryjs
# 或
yarn add blurryjs
# 或
pnpm i blurryjs
  1. 引入
import Blur from "blurryjs";
  1. 快速使用
const blur = new Blur(); // 默认全局数字模糊

参数说明

5 个参数

  1. blur?: number:模糊程度,单位:px,默认 2px
  2. blurryWords?: string[]:需要模糊的词,默认数字模糊
  3. operator?: string:默认模糊,可以是其他符号,比如 * 等
  4. autoBlur?: boolean:是否自动模糊,默认 false,只处理静态文本,如果需要处理异步数据,需要设置为 true
  5. excludes?: string[]:排除不需要模糊的 dom,类名,比如 ["ant-input"]
const blur = new Blur({
blur: 2,
blurryWords: ["周杰伦", "陈奕迅"],
operator: "*",
autoBlur: false,
excludes: ["ant-input"],
});

3 个方法:

  1. enableBlur(options):开启模糊,options 参数为:blurblurryWordsoperatorautoBlur

    blur.enableBlur({
    blur: 2,
    blurryWords: ["1", "2", "3"],
    operator: "*",
    autoBlur: false,
    });
  2. disableBlur:关闭模糊

    blur.disableBlur();
  3. destroy:销毁

    blur.destroy();

demo

git clone https://github.com/astak16/blurryjs.git

cd blurryjs

pnpm i

pnpm dev

效果

Kapture 2023-06-30 at 14.48.53.gif


作者:uccs
来源:juejin.cn/post/7250311237107122232
收起阅读 »

同学,请实现一个扫码登录

web
马上要到春节了,小伙伴们的公司是不是已经可以申请请假调休呢?虽然今年刚入职没有年假(好像国家不是这么规定的,但也不好跟公司硬杠),大小周的我已经攒了7天调休,也可以提前回家过年啦! 即使是年底,打工人的工作量也没有减少,最近leader扔给我一个扫码登录的需求...
继续阅读 »

马上要到春节了,小伙伴们的公司是不是已经可以申请请假调休呢?虽然今年刚入职没有年假(好像国家不是这么规定的,但也不好跟公司硬杠),大小周的我已经攒了7天调休,也可以提前回家过年啦!


即使是年底,打工人的工作量也没有减少,最近leader扔给我一个扫码登录的需求,我一看有点来劲了。一来做了多年前端,类似的需求还没有接触过,平时做的多的页面需求和改改bug对自身能力显然是无法提升的。二来扫码登录的功能很多应用都有做过,常见的微信扫码登录,也挺好奇具体如何实现。我大概看了一遍需求文档,写的挺详细的,流程图也标明了各端的交互流程。由于内网开发,产品流程图也忘记截图了,此处在网上找到的一个大概的流程图:
image.png


主要涉及到的是pc端、手机端和后台服务端。由于听产品同事说手机端由原生端(安卓和IOS)来实现,因此我这边只需要开发pc端就行,工作量直接减半有没有。做过该功能的小伙伴肯定了解,pc端的实现还是比较简单的,主要就是开启轮询查询后台扫码状态,然后做对应的提示或登录成功后跳转首页。


扫码登录的需求在前端主要难点在轮询上


0. 什么叫轮询?


所谓的轮询就是,由后端维护某个状态,或是一种连续多篇的数据(如分页、分段),由前端决定按序访问的方式将所有片段依次查询,直到后端给出终止状态的响应(结束状态、分页的最后一页等)。


1. 轮询的方案?


一般有两种解决方案:一种是使用websocket,可以让后端主动推送数据到前端;还有一种是前端主动轮询(上网查了下细分为长轮询和短轮询),通过大家熟悉的定时器(setIntervalsetTimeout)实现。


由于项目暂未用到websocket,且长轮询需要后台配合,所以直接采用短轮询(定时器)开撸了。


遇到的问题:


1、由于看需求文档上交互流程比较清晰,最开始没去网上查找实现方案,自己直接整了一版setInterval的轮询实现。在跟后台联调的过程中发现定时器每1s请求一次接口,发现很多接口没等响应就开启下一次的请求,很多请求都还在pending中,这样是不对的,对性能是很大消耗。于是想了下,可以通过setTimeout来优化,具体就是用setTimeout递归调用方式模拟setInterval的效果,达到只有上一次请求成功后才开启下一次的请求。


// 开启轮询
async beginPolling() {
if (this.isStop) return;
try {
const status = await this.getQrCodeStatus();
if (!status) return;
this.codeStatus = status;
switch(this.codeStatus) {
case '2':
this.stopPolling();
// 确认登录后,需前端修改状态
this.codeStatus = '5';
this.loading = true;
// 走登录逻辑
this.$emit('login', {
qrcId: this.qrcId,
encryptCSIIStr: this.macAddr
})
break;
case '3':
// 取消登录
this.stopPolling();
await this.getQrCode();
break;
case '4':
// 二维码失效
this.stopPolling();
break;
default:
break;
}
this.timer = setTimeout(this.beginPolling);
} catch(err) {
console.log(err);
this.stopPolling();
}
},

2、在自测了过程中又发现了另外一个问题,stopPolling方法中clearTimeout似乎无法阻止setTimeout的执行,二维码失效后请求仍在不停发出,这就很奇怪了。上网搜索了一番,发现一篇文章(很遗憾,已经找不到是哪篇文章了!)记录了这个问题:大概意思是虽然clearTimeout已经清除了定时器,但此时有请求已经在进行中,导致再次进入了循环体,重新开启了定时器。解决办法就是,需要手动声明一个标识位isStop来阻止循环体的执行。


    stopPolling() {
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
// 标记终止轮询(仅clearTimeout无法阻止)
this.isStop = true;
}
},

试了下确实达到效果了,但其实这个问题产生的具体原因我还是有些模糊的,希望遇到过相关问题的大佬能指点一下,感激不尽!


3、解决了上面提到的问题,就在以为万事大吉,只待提测的时候。后台同事发现了一个问题(点赞后台同事的尽责之心):他在反复切换登录方式(扫码登录<->账号密码登录)的过程中,发现后台日志有一段时间打印的qrcId不是最新的。然后我这边试了下,确实在切换频率过高时,此时有未完成的请求仍在进行中,导致qrcId被重新赋值了。虽然已经在beforeDestroy里调用了stopPolling清除定时器,但此时请求是未停止的。聪明的小伙伴们肯定想到axioscancelToken可以取消未完成的请求,但我实际也并没有用过,而且项目里也没有可以表演Ctrl+CCtrl+V的地方。于是百度了一番,找到一篇掘友的文章,为了表示尊敬我原封不动的搬到我的代码里了,哈哈!


import axios from "axios";
const CancelToken = axios.CancelToken;

const cancelTokenMixin = {
data() {
return {
cancelToken: null, // cancelToken实例
cancel: null, // cancel方法
};
},
created() {
this.newCancelToken();
},
beforeDestroy() {
//离开页面前清空所有请求
this.cancel("取消请求");
},
methods: {
//创建新CancelToken
newCancelToken() {
this.cancelToken = new CancelToken((c) => {
this.cancel = c;
});
},
},
};
export default cancelTokenMixin;

掘友文章[:](在vue项目中取消axios请求(单个和全局) - 掘金 (juejin.cn))


在组件里引入mixin,另外在请求时传入cancelToken实例,确实达到效果了。此时再次切换登录方式,之前的未完成的请求已被取消,也就无法再篡改qrcId。写到此处,我发现问题2也是未完成的请求导致的,那么是否可以不用isStop标识,直接在stopPolling中调用this.cancel("取消请求");不是更好吗?


完整代码如下:


import sunev from 'sunev'; // 全局公共方法库
import cancelTokenMixin from "@/utils/cancelTokenMixin"; // axios取消请求

export default {
props: {
loginType: {
type: String,
default: 'code'
}
},
mixins: [cancelTokenMixin],
data() {
return {
qrcId: '', // 二维码标识
qrcBase64: '', // 二维码base64图片
macAddr: '', // mac地址
loading: false,
isStop: false,
codeStatus: '0',
qrStatusList: [
{
status: '-1',
icon: 'error',
color: '#ed7b2f',
svgClass: 'icon-error-small',
text: '二维码生成失败\n请刷新重试',
refresh: true
},
{ status: '0', icon: '', text: '', refresh: false },
{
status: '1',
icon: 'scan',
color: '#2986ff',
svgClass: 'icon-scan-small',
text: '扫描成功\n请在移动端确认',
refresh: false
},
{
status: '2',
icon: 'confirm',
color: '#2986ff',
svgClass: 'icon-confirm-small',
text: '移动端确认登录',
refresh: false
},
{
status: '3',
icon: 'cancel',
text: '移动端已取消',
refresh: false
},
{
status: '4',
icon: 'error',
color: '#ed7b2f',
svgClass: 'icon-error-small',
text: '二维码已失效\n请刷新重试',
refresh: true
},
{
status: '5',
icon: 'success',
color: '#2986ff',
svgClass: 'icon-success-small',
text: '登录成功',
refresh: false
},
{
status: '6',
icon: 'error',
color: '#ed7b2f',
svgClass: 'icon-error-small',
text: '登录失败\n请刷新重试',
refresh: true
}
],
errMsg: ''
}
},
async created() {
try {
await this.getQrCode();
this.beginPolling();
} catch(err) {
console.log(err);
}
},
computed: {
// 当前状态
curQrStatus() {
const statusObj = this.qrStatusList.find(item => item.status === this.codeStatus);
if (this.errMsg) {
statusObj.text = this.errMsg;
}
return statusObj;
}
},
methods: {
// 开启轮询
async beginPolling() {
if (this.isStop) return;
try {
const status = await this.getQrCodeStatus();
if (!status) return;
this.codeStatus = status;
switch(this.codeStatus) {
case '2':
this.stopPolling();
// 确认登录后,需前端修改状态
this.codeStatus = '5';
this.loading = true;
// 走登录逻辑
this.$emit('login', {
qrcId: this.qrcId,
encryptCSIIStr: this.macAddr
})
break;
case '3':
// 取消登录
this.stopPolling();
await this.getQrCode();
break;
case '4':
// 二维码失效
this.stopPolling();
break;
default:
break;
}
this.timer = setTimeout(this.beginPolling);
} catch(err) {
console.log(err);
this.stopPolling();
}
},
// 暂停轮询
stopPolling() {
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
// 标记终止轮询(仅clearTimeout无法阻止)
this.isStop = true;
}
},
// 获取二维码base64
async getQrCode() {
this.reset();
this.loading = true;
try {
const params = {
encryptCSIIStr: this.macAddr
}
const res = await sunev.$https.post(
'sunev/LoginQRCGen',
{ isLoading: false, cancelToken: this.cancelToken },
params
)
if (res.qrcId) {
this.qrcId = res.qrcId;
this.qrcBase64 = res.qrcBase64;
} else {
this.stopPolling();
}
} catch(err) {
this.errMsg = err.message;
this.stopPolling();
}
},
// 获取二维码状态
async getQrCodeStatus() {
try {
const params = {
encryptCSIIStr: this.macAddr
}
const res = await sunev.$https.post(
'sunev/LoginQRCQry',
{ isLoading: false, cancelToken: this.cancelToken },
params
)
return res.status;
} catch(err) {
this.stopPolling();
}
},
// 刷新二维码
async refresh() {
await this.getQrCode();
this.beginPolling();
},
// 切换登录类型
toggle() {
this.$emit('toggleLoginType');
},
// 重置
reset() {
this.isStop = false;
this.codeStatus = '0';
this.errMsg = '';
},
beforeDestroy() {
this.stopPolling();
}
}
}

ps:


1、由于是老项目了,登录界面逻辑较多,避免臃肿,二维码登录拆分成单独组件实现


2、由于项目组在内网开发,以下代码都是一行行重新手打的,不是很重要的html和css部分就省略了


后记:


由于此需求并不着急上线,暂未提测,所以还不知测试同事会提出怎样的bug。另外掘友们如果发现问题,也欢迎批评指正,感激不尽!


作者:wing98
来源:juejin.cn/post/7326268998490865673
收起阅读 »

前端如何统一开发环境

web
统一不同的同事之间,本地和 CI 之间的开发环境有利于保证运行效果一致和 bug 可复现,本文聚焦于前端最基本的开发环境配置:nodejs 和 包管理器。 nodejs 首先推荐使用 fnm 管理多版本 nodejs。 对比 nvm: 支持 brew 安装,...
继续阅读 »

统一不同的同事之间,本地和 CI 之间的开发环境有利于保证运行效果一致和 bug 可复现,本文聚焦于前端最基本的开发环境配置:nodejs 和 包管理器。


nodejs


首先推荐使用 fnm 管理多版本 nodejs。


对比 nvm



  • 支持 brew 安装,更新方便

  • 跨平台,windows 也能用 winget 安装


使用 fnm 一定要记得开启根据当前 .nvmrc 自动切换对应的 nodejs 版本,也就是在在 .zshrc 中加入:


eval "$(fnm env --use-on-cd)"

包管理器


尽管 npm 一直在进步,甚至 7.x 已经原生支持了 workspace。但是我钟爱 pnpm,理由:



  • 安全,避免幽灵依赖,不会将依赖的依赖平铺到 node_modules 下

  • 快,基于软/硬链接,node_modules 下是软连接,硬链接到 .pnpm 文件夹下的硬链接

  • 省磁盘,公司配的 mac 只有 256G

  • pnpm 的可配置性很强,配置不够用还可以用 .pnpmfile.js 编写 hooks

  • yarn2 node_modules 都看不到,分析依赖太麻烦了

  • 公司用的 vue,而 vue3/vite 用 pnpm(政治正确)


推荐使用 Corepack 管理用户的包管理器,其实我一开始知道有 corepack 这个 nodejs 官方的东西的时候,我就在想:为啥不叫 npmm(node package manager manager) 呢?


corepack 目前官方觉得功能没稳定,所以默认没开启,需要用户通过 corepack enable 手动开启,相关的讨论:enable corepack by default


有了 corepack 我们就可以轻松的在 npm/yarn/pnpm 中切换,安装和更新不同的版本。还有一个非常方便的特性就是通过在 package.json 中声明 packageManager 字段例如 "pnpm@8.14.1",当我们开启了 corepack,cd 到该 package.json 所在的 package 的时候,运行 pnpmcorepack 会使用 8.14.1 版本的 pnpm


corepack 是怎样做到的呢?nodejs 安装文件夹有个的 bin 目录,这个目录会被添加到 path 环境变量,其中包含了 corepack 以及 corepack 支持的包管理器的可执行文件:


❯ tree ../../Library/Caches/fnm_multishells/17992_1705553706619/bin
../../Library/Caches/fnm_multishells/17992_1705553706619/bin
├── corepack -> ../lib/node_modules/corepack/dist/corepack.js
├── node
├── npm -> ../lib/node_modules/npm/bin/npm-cli.js
├── npx -> ../lib/node_modules/npm/bin/npx-cli.js
├── pnpm -> ../lib/node_modules/corepack/dist/pnpm.js
├── pnpx -> ../lib/node_modules/corepack/dist/pnpx.js
├── yarn -> ../lib/node_modules/corepack/dist/yarn.js
└── yarnpkg -> ../lib/node_modules/corepack/dist/yarnpkg.js

可以看到 pnpm 被链接到了 corepack 的一个 js 文件,查看 corepack/dist/pnpm.js 内容:


#!/usr/bin/env node
require('./lib/corepack.cjs').runMain(['pnpm', ...process.argv.slice(2)]);

可以看到其实 corepack 相当于劫持了 pnpmyarn 命令,然后根据 packageManager 字段配置自动切换到对应的包管理器,如果已经安装过就使用缓存,没有就下载。


怎样统一 nodejs 和 包管理器


问题


虽然我在项目中配置了 .nvmrc 文件,在 package.json 中声明了 packageManager 字段,但是用户可能没有安装 fnm 以及配置根据 .nvmrc 自动切换对应的 nodejs,还有可能没有开启 corepack,所以同事的环境还是有可能和要求的不一致。我一向认为,不应该依靠人的自觉去遵守规范,通过工具强制去约束才能提前发现问题和避免争论。


解决办法


最开始是看到项目中使用了 only-allow 用于限制同事开发时只能用 pnpm,由此我引发了我一个灵感,为什么我不干脆把事情做绝一点,把 nodejs 的版本也给统一了


于是我写了一个脚本用于检查用户本地的 nodejs 的版本,包管理器的版本必须和要求一致。最近封装成了一个 cli:check-fe-env。使用方式很简单,增加一个 preinstall script:


{
"scripts": {
"preinstall": "npx check-fe-env"
}
}

工作原理



  • 用户在运行 pnpm install 之后,install 依赖之前,包管理器会执行 preinstall 脚本

  • cli 会检测:

    • 用户当前环境的 nodejs 版本和 .nvmrc 中声明的是否一样

    • 用户当前使用的包管理器种类和版本是否和 package.jsonpackageManager 字段一样




获取当前环境的 nodejs 版本很简单,可以用 process.version。想要获取执行脚本时的包管理器可以通过环境变量:process.env.npm_config_user_agent,如果一个 npm script 是通过 pnpm 运行的,那么这个环境变量会被设置为例如 pnpm/8.14.1 npm/? node/v20.11.0 darwin arm64,由此我们可以获取当前使用的包管理器和版本。


为了加快安装速度,我特意把源码和相关依赖给一起打包了,整个 bundle 大小 8k 左右。


局限性


最新的 npmpnpm 目前貌似都有一个 bug,都是安装完依赖才执行 preinstall hooks,具体看这: Preinstall script runs after installing dependencies


这个方案对于 monorepo 项目或者说不需要发包的项目是没啥问题的,但是不适用于一个要发包的项目。原因是 preinstall script 除了会在本地 pnpm install 时执行,别人安装这个包,也会执行这个 preinstall script,就和 vue-demi 用的 postinstall script 一样。主要是确实没找到一个:只会在本地运行 pnpm install 后且在安装依赖前执行的 hook。


作者:余腾靖
来源:juejin.cn/post/7325069743143878697
收起阅读 »

一个指令实现左右拖动改变布局

web
一个指令实现左右拖动改变布局 一、前言 本文以实现“一个指令实现左右拖动改变页面布局”的需求为例,介绍了: 实现思路 总结关键技术点 完整 demo 二、实现思路 2.1 外层div布局 首先设置4个div元素,一个作为父容器,一个...
继续阅读 »

一个指令实现左右拖动改变布局


一、前言


本文以实现“一个指令实现左右拖动改变页面布局”的需求为例,介绍了:





    1. 实现思路





    1. 总结关键技术点





    1. 完整 demo




二、实现思路


2.1 外层div布局


首先设置4个div元素,一个作为父容器,一个作为左边的容器,一个在中间作为拖动指令承载的元素,最后一个在作为右边容器的元素。


js
复制代码
<div>
<div class="left"></div>
<div class="resize" v-resize="{left: 300, resize: 10}"></div>
<div class="right"></div>
</div>

2.2 获取指令元素的父元素和兄弟元素


首先,接收指令传递的各元素的宽,并进行初始赋值和利用 calc 计算右边元素宽度。


js
复制代码
let leftWidth = binding.value?.left || 300
let resizeWidth = binding.value?.resize || 10
let rightWidth = `calc(100% - ${leftWidth + 10}px - ${resizeWidth}px)`

然后,接收指令传递下来的元素 el,并根据该元素 通过 Element.previousElementSibling 获取当前元素前一个兄弟元素,即是 所在的元素。 通过
Element.nextElementSibling 获取当前元素的后一个兄弟元素,即是 所在的元素。 通过 Element.parentElement 获取当前元素的父元素。


js
复制代码
bind: function (el, binding, vnode) {
let resize = el
let left = resize.previousElementSibling
let right = resize.nextElementSibling
let box = resize.parentElement
}

2.3 利用浮动定位,实现浮动布局


接着,给各个容器元素设置浮动定位 float = 'left'。当然,其实其他方式也可以的,只要能达到类似“行内块”的布局即可。


可以提一下的是,设置 float = 'left' 可以创建一个独立的 BFC 区域,具有“独立隔离性”, 即 BFC 区域内部元素的布局,不会“越界”影响外部元素的布局; 外部元素的布局也不会“穿透”,影响 BFC 区域的内部布局。


js
复制代码
let resize = el
let left = resize.previousElementSibling
let right = resize.nextElementSibling
let box = resize.parentElement

box.style.float = 'left'
left.style.float = 'left'
resize.style.float = 'left'
right.style.float = 'left'

2.4 实现鼠标按下时,将特定元素指定为未来指针事件的捕获目标


通过 onpointerdown 监听,实现实现鼠标按下时,将特定元素指定为未来指针事件的捕获目标,这个特定元素即 v-resize 指令所在的元素。


这样,就可以通过获取 v-resize 指令所在的元素的位置属性,来计算出左右的元素,在拖动时需要设置的宽和位置信息。


js
复制代码
resize.onpointerdown = function (e) {
let startX = e.clientX
resize.left = resize.offsetLeft
resize.setPointerCapture(e.pointerId);
return false
}

2.5 实现鼠标移动时,改变左右的宽度


通过 onpointermove 监听,实现在鼠标指针移动时,获取鼠标事件的位置信息 clientX 等,并由此计算出合适的移动距离 moveLen, resize 的左边距离,left 元素的宽,以及 right
元素的宽。


由此,就实现了每移动一步,就重新计算出新的布局位置信息,并进行了赋值。


js
复制代码
resize.onpointermove = function (e) {
let endX = e.clientX

let moveLen = resize.left + (endX - startX)
let maxT = box.clientWidth - resize.offsetWidth
const step = 100
if (moveLen < step) moveLen = step
if (moveLen > maxT - step) moveLen = maxT - step

resize.style.left = moveLen
resize.style.width = resizeWidth
left.style.width = moveLen + 'px'
right.style.width = (box.clientWidth - moveLen - resizeWidth - 2) + 'px'
}

2.6 鼠标抬起时,将鼠标指针从先前捕获的元素中释放


通过监听 onpointerup,实现在鼠标指针抬起时,通过 releasePointerCapture 将鼠标指针从先前捕获的元素中释放,还给鼠标自由。并将 resize 元素的 onpointermove 事件设置为
null。这样,当鼠标被抬起后,再操作就不会携带此前的绑定操作了。


js
复制代码
resize.onpointerup = function (evt) {
resize.onpointermove = null;
resize.releasePointerCapture(evt.pointerId);
}

经过上诉步骤,我们就实现了,从鼠标按下,到移动计算改变布局,然后鼠标抬起释放绑定,操作完成,改变布局的目标达成。


三、总结关键技术点


实现本需求主要的关键技术点有:


3.1 setPointerCapture 和 releasePointerCapture


Element.setPointerCapture() 用于将特定元素指定为未来指针事件的捕获目标。 指针的后续事件将以捕获元素为目标,直到捕获被释放(通过 Element.releasePointerCapture())。


Element.releasePointerCapture() 则用来将鼠标从先前通过 Element.setPointerCapture() 绑定的元素身上释放出来,还给鼠标自由。


需要注意的是,类似的功能事件还有 setCapture() 和 releaseCapture,但它们已经被标记为弃用,且是非标准的,所以不建议使用。


3.2 onpointerdown,onpointermove 和 onpointerup


与上面配套的关键事件还有,onpointerdown,onpointermove 和 onpointerup。其中 onpointermove 是实现主要改变布局的逻辑的地方。


pointerdown:全局事件处理程序,当鼠标指针按下时触发。返回 pointerdown 事件触发对象的事件处理程序。


onpointermove:全局事件处理程序,当鼠标指针移动时触发。返回 targetElement 元素的 pointermove 事件处理函数。


onpointerup:全局事件处理程序,当鼠标指针抬起时触发。返回 targetElement 元素的pointerup事件处理函数。


3.3 注意事项


① Vue.nextTick 的使用。在 vue 指令定义的 bind 中使用了 Vue.nextTick,是为了解决初次运算时,有些 dom 元素未完成渲染,设置元素属性会报警告或错误。


js
复制代码
Vue.directive('resize', {
bind: function (el, binding, vnode) {
Vue.nextTick(() => {
handler(el, binding, vnode)
})
}
})

② position = 'relative' 的设置。给每个元素 left 和 right 元素设置 position = 'relative',是为了解决 z-index 可能会失效的问题,我们知道有时浮动元素会导致这种情形发生。
当然这并不影响本次需求的实现,是为了其他设计考虑才这样做的。


js
复制代码
left.style.position = 'relative'
resize.style.position = 'relative'
right.style.position = 'relative'

③ cursor = 'col-resize' 的设置。为了获得更友好的体验,使得用户一眼鉴别这个功能,我们使用了 cursor 的 col-resize 属性。


js
复制代码
resize.style.cursor = 'col-resize'

四、完整 demo


// 这是定义指令的完整代码:directive.js


js
复制代码

/**
* 自定义调整宽度指令:添加指令后,可以实现拖拽边线改变页面元素的宽度。
* 指令接收两个参数,left 左边元素的宽度,中间 resize 元素的宽度。数据类型均为 number
* 使用示例:
* <div>
* <div></div>
* <div v-resize="{left: 300, resize: 10}" />
* <div></div>
* </div>
*
* 注意:由于是使用 float 布局,所以需要保证有4个元素作为浮动元素的容器,即父容器 1 个,子容器 3 个。
*
*/

import Vue from 'vue'

const resizeDirective = {}
const handler = (el, binding, vnode) => {

let leftWidth = binding.value?.left || 300
let resizeWidth = binding.value?.resize || 10
let rightWidth = `calc(100% - ${leftWidth + 10}px - ${resizeWidth}px)`

if (binding.value?.left && Object.prototype.toString.call(binding.value?.left) !== '[object Number]') {
console.error(`${binding.value.left} Must be Number`)
}
if (binding.value?.resize && Object.prototype.toString.call(binding.value?.resize) !== '[object Number]') {
console.error(`${binding.value.left} Must be Number`)
}

let resize = el
let left = resize.previousElementSibling
let right = resize.nextElementSibling
let box = resize.parentElement

box.style.float = 'left'
box.style.height = '100%'
box.style.width = '100%'
box.style.overflow = 'hidden'

left.style.float = 'left'
left.style.width = leftWidth + 'px'
left.style.position = 'relative'

resize.style.float = 'left'
resize.style.cursor = 'col-resize'
resize.style.width = resizeWidth + 'px'
resize.style.height = box.offsetHeight + 'px'
resize.style.position = 'relative'

right.style.float = 'left'
right.style.width = rightWidth
right.style.position = 'relative'
right.style.zIndex = 99

resize.onpointerdown = function (e) {
let startX = e.clientX
resize.left = resize.offsetLeft
resize.onpointermove = function (e) {
let endX = e.clientX

let moveLen = resize.left + (endX - startX)
let maxT = box.clientWidth - resize.offsetWidth
const step = 100
if (moveLen < step) moveLen = step
if (moveLen > maxT - step) moveLen = maxT - step

resize.style.left = moveLen
resize.style.width = resizeWidth
left.style.width = moveLen + 'px'
right.style.width = (box.clientWidth - moveLen - resizeWidth - 2) + 'px'
}
resize.onpointerup = function (evt) {
resize.onpointermove = null;
resize.releasePointerCapture(evt.pointerId);
}
resize.setPointerCapture(e.pointerId);
return false
}
}
resizeDirective.install = Vue => {
Vue.directive('resize', {
bind: function (el, binding, vnode) {
Vue.nextTick(() => {
handler(el, binding, vnode)
})
},
update: function (el, binding) {
handler(el, binding)
},
unbind: function (el, binding) {
el.instance && el.instance.$destroy()
}
})
}

export default resizeDirective



// 在 main.js 中使用


js
复制代码
import resizeDirective from './directive'

Vue.use(resizeDirective)

// 在具体页面中使用:ResizeWidth.vue


html
复制代码

<template>
<div>
<div class="left">left</div>
<div class="resize" v-resize="{left: 300, resize: 10}"></div>
<div class="right">right</div>
</div>
</template>

<script>
export default {
name: 'ResizeWidth'
}
</script>

<style scoped>
.left {
background: #42b983;
height: 50vh;
}

.resize {
background: #EEEEEE;
height: 50vh;
}

.right {
background: #1e87f0;
height: 50vh;
}
</style>


作者:灵扁扁
链接:https://juejin.cn/post/7245936314851377210
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

箭头函数太长了,缩短小窍门来了

web
前言 使用箭头语法,你可以定义比函数表达式短的函数。在某些情况下,你可以完全省略: 参数括号 (param1, param2) return 关键字 甚至大括号 { }。 1. 基本语法 完整版本的箭头函数声明包括: 一对带有参数枚举的括号 (param...
继续阅读 »

前言


使用箭头语法,你可以定义比函数表达式短的函数。在某些情况下,你可以完全省略:



  • 参数括号 (param1, param2)

  • return 关键字

  • 甚至大括号 { }


1. 基本语法


完整版本的箭头函数声明包括:



  • 一对带有参数枚举的括号 (param1, param2)

  • 后面跟随箭头 =>

  • 以函数体 {FunctionBody} 结尾


典型的箭头函数如下所示:


const sayMessage = (what, who) => {
  return `${what}${who}!`;
};

sayMessage('Hello''World'); // => 'Hello, World!'

这里有一点需要注意:你不能在参数 (param1, param2) 和箭头 => 之间放置换行符。


接下来我们看看如何缩短箭头函数,在处理回调时,使它更易于阅读。


2. 减少参数括号


以下函数 greet 只有一个参数:


const greet = (who) => {
  return `${who}, Welcome!`
};

greet('Aliens'); // => "Aliens, Welcome!"

greet 箭头函数只有一个参数 who 。该参数被包装在一对圆括号(who) 中。


当箭头函数只有一个参数时,可以省略参数括号。


可以利用这种性质来简化 greet


const greetNoParentheses = who => {
  return `${who}, Welcome!`
};

greetNoParentheses('Aliens'); // => "Aliens, Welcome!"

新版本的箭头函数 greetNoParentheses 在其单个参数 who 的两边没有括号。少两个字符:不过仍然是一个胜利。


尽管这种简化很容易掌握,但是在必须保留括号的情况下也有一些例外。让我们看看这些例外。


2.1 注意默认参数


如果箭头函数有一个带有默认值的参数,则必须保留括号。


const greetDefParam = (who = 'Martians') => {
  return `${who}, Welcome!`
};

greetDefParam(); // => "Martians, Welcome!"

参数 who 的默认值为 Martians。在这种情况下,必须将一对括号放在单个参数(who ='Martians')周围。


2.2 注意参数解构


你还必须将括号括在已解构的参数周围:


const greetDestruct = ({ who }) => {
  return `${who}, Welcome!`;
};

const race = {
  planet'Jupiter',
  who'Jupiterians'
};

greetDestruct(race); // => "Jupiterians, Welcome!"

该函数的唯一参数使用解构 {who} 来访问对象的属性 who。这时必须将解构式用括号括起来:({who {}})


2.3 无参数


当函数没有参数时,也需要括号:


const greetEveryone = () => {
  return 'Everyone, Welcome!';
}

greetEveryone(); // => "Everyone, Welcome!"

greetEveryone 没有任何参数。保留参数括号 ()


3. 减少花括号和 return


当箭头函数主体内仅包含一个表达式时,可以去掉花括号 {} 和 return 关键字。


不必担心会忽略 return,因为箭头函数会隐式返回表达式评估结果。这是我最喜欢的箭头函数语法的简化形式。


没有花括号 {} 和 return 的 greetConcise 函数:


const greetConcise = who => `${who}, Welcome!`;

greetConcise('Friends'); // => "Friends, Welcome!"

greetConcise 是箭头函数语法的最短版本。即使没有 return,也会隐式返回 $ {who},Welcome! 表达式。


3.1 注意对象文字


当使用最短的箭头函数语法并返回对象文字时,可能会遇到意外的结果。


让我们看看这时下会发生些什么事:


const greetObject = who => { message: `${who}, Welcome!` };

greetObject('Klingons'); // => undefined

期望 greetObject 返回一个对象,它实际上返回 undefined


问题在于 JavaScript 将大括号 {} 解释为函数体定界符,而不是对象文字。message: 被解释为标签标识符,而不是属性。


要使该函数返回一个对象,请将对象文字包装在一对括号中:


const greetObject = who => ({ message: `${who}, Welcome!` });

greetObject('Klingons'); // => { message: `Klingons, Welcome!` }

({ message: `${who}, Welcome!` })是一个表达式。现在 JavaScript 将其视为包含对象文字的表达式。


4.粗箭头方法


类字段提案(截至2019年8月,第3阶段)向类中引入了粗箭头方法语法。这种方法中的 this 总是绑定到类实例上。


让我们定义一个包含粗箭头方法的 Greet 类:


class Greet {
  constructor(what) {
    this.what = what;
  }
  getMessage = (who) => {
    return `${who}${this.what}!`;
  }
}
const welcome = new Greet('Welcome');
welcome.getMessage('Romulans'); // => 'Romulans, Welcome!'

getMessage 是 Greet 类中的一个方法,使用粗箭头语法定义。getMessage 方法中的 this 始终绑定到类实例。


你可以编写简洁的粗箭头方法吗?是的你可以!


让我们简化 getMessage 方法:


class Greet {
  constructor(what) {
    this.what = what;
  }
  getMessage = who => `${who}${this.what}!`
}
const welcome = new Greet('Welcome');
welcome.getMessage('Romulans'); // => 'Romulans, Welcome!'

getMessage = who => `${who}, ${this.what}! 是一个简洁的粗箭头方法定义。省略了其单个参数 who 周围的一对括号,以及大括号 {} 和 return关键字。


5. 简洁并不总是意味着可读性好


我喜欢简洁的箭头函数,可以立即展示该函数的功能。


const numbers = [145];
numbers.map(x => x * 2); // => [2, 8, 10]

x => x * 2 很容易暗示一个将数字乘以 2 的函数。


尽管需要尽可能的使用短语法,但是必须明智地使用它。否则你可能会遇到可读性问题,尤其是在多个嵌套的简洁箭头函数的情况下。


我更喜欢可读性而不是简洁,因此有时我会故意保留大括号和 return 关键字。


让我们定义一个简洁的工厂函数:


const multiplyFactory = m => x => x * m;

const double = multiplyFactory(2);
double(5); // => 10

虽然 multiplyFactory 很短,但是乍一看可能很难理解它的作用。


这时我会避免使用最短的语法,并使函数定义更长一些:


const multiplyFactory = m => { 
  return x => x * m;
};

const double = multiplyFactory(2);
double(5); // => 10

在较长的形式中,multiplyFactory 更易于理解,它返回箭头函数。


无论如何,你都可能会进行尝试。但我建议你将可读性放在简洁性之前。


6. 结论


箭头函数以提供简短定义的能力而闻名。


使用上面介绍的诀窍,可以通过删除参数括号、花括号或 return 关键字来缩短箭头函数。


你可以将这些诀窍与粗箭头方法放在一起使用。


简洁是好的,只要它能够增加可读性即可。如果你有许多嵌套的箭头函数,最好避免使用最短的形式。


作者:河马老师
来源:juejin.cn/post/7326758010523697192
收起阅读 »

爆肝手写 · 一镜到底特效· 龙年大吉 【CSS3】

web
前言 作为一名有多年开发经验的前端技术开发人员, 我最爱的还是用前端技术实现各种炫酷的特效,对于我来说,CSS3不仅仅是一种样式语言,更是一种表达情感、对美好事物追求的一种体现吧, 虽然每天要沉浸在代码的海洋里,但我也要寻找着技术与艺术的交汇点,努力把吃饭的...
继续阅读 »

前言


作为一名有多年开发经验的前端技术开发人员, 我最爱的还是用前端技术实现各种炫酷的特效,对于我来说,CSS3不仅仅是一种样式语言,更是一种表达情感、对美好事物追求的一种体现吧, 虽然每天要沉浸在代码的海洋里,但我也要寻找着技术与艺术的交汇点,努力把吃饭的家伙变成自己的热爱的事情。 龙年来临之际, 通宵写了一个全新的CSS3 一镜到底的特效案例,如下图, 希望能与大家分享这份创意与激情, 祝各位掘友们新年快乐, 龙年行大运!


Video_20240120111316[00_00_12--00_00_15].gif


上源码:



整体实现思路介绍



整个案例使用到CSS3 和 HTML技术, 案例的核心知识点 使用到了 CSS3 中的透视 、3D变换、 动画 、无缝滚动等技术要点, 下面我会逐一进行介绍




  • 知识点1: 一镜到底特效的 案例的整体布局、设计、及动画思路

  • 知识点2:CSS3中的3D坐标系

  • 知识点3:CSS3中的3D变换及案例应用

  • 知识点4:CSS3中的3D透视及案例应用

  • 知识点5:CSS3中的 透视及3d变换的异同点

  • 知识点6:CSS3中的 动画及案例应用


1、一镜到底特效 的整体布局、设计、及动画思路


如下图所示,一镜到底的案例特效 最核心的就是要 构成一套 在3D 空间中, 有多个平行的场景, 然后以摄像机的视角 从前往后 移动,在场景中穿梭, 依次穿过每一个场景的页面即可啦,自己闭上眼睛来体验一下吧;
无标题.png


对应到本案例中效果就是这样啦:


image.png


当然有朋友会说看上图,感觉不到明显的3D 立体效果, 那再来看看下面这个图吧;


消失点.png


上面这张图就是 基于人眼 看不同距离的物体呈现出的结果, 也就是透视效果, 透视效果最核心的特点就是近大远小;而影响看到透视物体大小的一个参数就是消失点距离, 比如消失点越近,最远处的物体会越小, 近大远小的效果越明显, 自己闭上眼睛来体验一下吧;


对应到本案例中效果就是这样啦:


image.png



  • 上述框架对应的HTML源码如下, 其中.sence-in 内部的子元素是素材,可以先忽略:


<div class="sence-box sence-box1">
<div class="sence-in">
<div class="text-left text-box">掘金多多</div>
<div class="text-right text-box">大展鸿图</div>
<div class="long long-left"></div>
<div class="long long-right"></div>
<div class="denglong-box"></div>
<div class="long2 long2-left"></div>
<div class="long2 long2-right"></div>
</div>
</div>
<div class="sence-box sence-box2">
<div class="sence-in">
<div class="text-left text-box">步步高升</div>
<div class="text-right text-box">年年有鱼</div>
<div class="long long-left"></div>
<div class="long long-right"></div>
<div class="denglong-box"></div>
<div class="long2 long2-left"></div>
<div class="long2 long2-right"></div>
<div class="xiangyun"></div>
</div>
</div>
<div class="sence-box sence-box3">
<div class="sence-in">
<div class="text-left text-box">心想事成</div>
<div class="text-right text-box">万事如意</div>
<div class="long long-left"></div>
<div class="long long-right"></div>
<div class="denglong-box"></div>
<div class="long2 long2-left"></div>
<div class="long2 long2-right"></div>
<div class="xiangyun"></div>
</div>
</div>
<div class="sence-box sence-box4">
<div class="sence-in">
<div class="text-left text-box">蒸蒸日上</div>
<div class="text-right text-box">一帆风顺</div>
<div class="long long-left"></div>
<div class="long long-right"></div>
<div class="denglong-box"></div>
<div class="long2 long2-left"></div>
<div class="long2 long2-right"></div>
<div class="xiangyun"></div>
</div>
</div>
<div class="sence-box sence-box5">
<div class="sence-in">
<div class="text-left text-box">自强不息</div>
<div class="text-right text-box">恭喜发财</div>
<div class="sence-block">龙年大吉</div>
<div class="denglong-box"></div>
<div class="long long-left"></div>
<div class="long long-right"></div>
<div class="long2 long2-left"></div>
<div class="long2 long2-right"></div>
<div class="xiangyun"></div>
</div>
</div>

知识点一: CSS3中的坐标系


CSS3中的坐标系,是一切3D 效果的基石, 务必熟练掌握 , 如下图所示:



  • x轴坐标:左边负,右边正

  • y轴坐标:上边负,下边正

  • z轴坐标:里面负,外面正

  • 注意: 坐标系的原点在 浏览器的左上角


image.png


知识点二: 透视(perspective)


perspective属性定义了观察者和Z=0平面之间的距离,从而为3D转换元素创建透视效果。上面也说了, 透视的效果就是 近大远小, 上面的截图中也能看到 。这个属性是用来创建3D转换效果的必要属性,因为当我们进行旋转或其他3D转换时,如果透视效果设置得不正确,元素可能会显得很奇怪或不正常。 透视的语法如下:


在CSS中,我们可以通过在父元素上设置perspective属性来控制子元素的3D效果。例如:


	.container {  
perspective: 1000px;
}

在这个例子中,我们为.container元素设置了perspective属性,值为1000px。这意味着任何在这个元素内部的3D转换都会基于这个视距进行透视。


知识点三:3D 变换的核心属性: transform-style


transform-style属性决定了是否保留元素的三维空间布局。当设置为preserve-3d时,它会保留元素内部的三维空间,即使这个元素本身没有进行任何3D转换。这使得子元素可以相对于父元素进行旋转或其他3D转换,而不会影响其他元素。在我们的案例截图中 也能看出在父元素设置了 transform-style: preserve-3d;属性后, 各个场景在 Z轴方向上,已经有了前后距离上的差异了。 需要注意的点就是, transform-style属性一定要设置给发生3D变换元素的父元素


例如:


 /* 透视属性加给了 最外层的元素, 保证所有子元素的透视效果是一致的,协调的*/
.perspective-box {
transform-style: preserve-3d;
}

在这个例子中,我们为.perspective-box元素设置了transform-style属性为preserve-3d,这意味着任何在这个元素内部的3D转换都会保留其三维空间布局。



  • 小技巧:如果你希望自己做的3D场景,立体效果很真实的话, 可以尽量多的给不同的元素,设置在Z轴方向上 设置不同的偏移量, 这样的效果是 摄像机在穿梭的过程中,每一段距离都能看到不同的风景, 层次感会很强, 当然也不要太疯狂, 不然场景会变得混乱哦


知识点四、perspective和transform-style的差异和注意点(炒鸡重要!)



  • perspective属性定义了观察者和Z=0平面之间的距离,通俗的说 就是屏幕 到消失点的距离,从而影响3D元素的透视效果, 而transform-style属性决定了是否保留元素的三维空间布局

  • 当我们只使用perspective属性时,只有被明确设置为3D转换的元素才会显示透视效果。而当我们使用transform-style: preserve-3d时,即使元素本身没有进行任何3D转换,其子元素也可以进行3D转换并保留三维空间布局。


注意:perspective属性,只能带来近大远小的透视视觉效果,并不能构成真正的3D空间布局。真正的3D布局必须依赖于transform-style: preserve-3d属性来实现


知识点五、animation动画的定义和使用


CSS动画是一种使元素从一种样式逐渐改变为另一种样式的方法。这个过程是通过关键帧(keyframes)来定义的,关键帧定义了动画过程中的不同状态。 在一镜到底的案例中, 整个场景的前后移动,用的就是动画属性。


动画的使用分为两步, 具体使用方式如下:



  • 1.使用@keyframes 来定义动画

  • 2.使用animation属性来调用动画,



@keyframes rotate {
from { transform: rotateX(0deg); }
to { transform: rotateX(360deg); }
}

在这个例子中,我们定义了一个名为“rotate”的关键帧动画,使元素从X轴的0度旋转到360度。然后,我们可以通过将这个动画应用到HTML元素上来使用它:


	.perspective-content {  
animation: rotate 5s infinite linear;
}

在这个例子中,我们将“rotate”动画应用到.cube元素上,设置动画时间为5秒,无限循环,并且线性过渡;


在一镜到底的案例中, 我们定义的动画如下:



@keyframes perspective-content {

0% {
transform: translateZ(0px);
}

50% {
transform: translateZ(6000px);
}

50.1% {
transform: translateZ(-6000px);
}

100% {
transform: translateZ(0px);
}
}


上午动画 其实做了一个无线循环轮播的逻辑, 就是当 在Z轴方向上 从 0 移动到 6000距离以后, 在重置到-6000px, 这样就可以在从-6000继续向前移动, 移动到 0 ,达到一个循环, 再开始下一次的循环;



  • 小技巧: 你可以把动画 单独加给每个场景(可能有10多个子元素, 你的重复写10多遍,会很麻烦的),也可以把动画加给公共的父元素,父元素会带着里面的子元素一起动, 这样只用写一次就行哦;


结束语:


以上就是案例用到的所有知识点啦, 整个案例的代码,可以在顶部源码位置查看,我就不一一解释了, 如有疑问和建议,可以留言,一起探讨学习哦, 本人能力有限, 希望大家多多批评指导;


作者:IT大春哥
来源:juejin.cn/post/7325739662033879090
收起阅读 »

前端实现汉堡菜单

web
如果你曾经在浏览网页时看到三条线堆叠在一起,那么你就遇到了汉堡菜单。它是移动和响应式网页设计中使用的一种流行设计元素,用于创建干净、简约的界面。 单击时,这个小菜单会从屏幕的任一侧滑出,显示导航项或选项列表。当菜单打开时,汉堡菜单也会变成“X”或其他形状。 在...
继续阅读 »

如果你曾经在浏览网页时看到三条线堆叠在一起,那么你就遇到了汉堡菜单。它是移动和响应式网页设计中使用的一种流行设计元素,用于创建干净、简约的界面。


单击时,这个小菜单会从屏幕的任一侧滑出,显示导航项或选项列表。当菜单打开时,汉堡菜单也会变成“X”或其他形状。


在这篇文章中,我们将向您展示如何在 CSS 中创建不同的汉堡菜单动画。让我们开始吧!


创建汉堡菜单


要创建汉堡菜单,我们首先需要创建 HTML 。由一个按钮元素和三个嵌套的 div 元素组成,每个元素代表汉堡图标的一行。


<button class="hamburger">
<div class="hamburger__line"></div>
<div class="hamburger__line"></div>
<div class="hamburger__line"></div>
</button>

接下来,我们将为元素添加一些基本样式。我们将从按钮元素中删除任何默认样式,包括背景和边框颜色。


.hamburger {
background: transparent;
border: transparent;
cursor: pointer;
padding: 0;
}

然后,对于每个线元素,我们将设置背景颜色、高度、宽度和每个直线之间的间距。


.hamburger__line {
background: rgb(203 213 225);
margin: 0.25rem 0;
height: 0.25rem;
width: 2rem;
}

X


是时候使用 CSS 创建一个很酷的汉堡菜单动画了。当用户将鼠标悬停在按钮上时,我们希望线条转换为“X”形。


为了实现这一点,我们将使用  :hover  伪类和  nth-child  选择器来访问每一行。我们将使用  translate() 和  rotate() 函数将线条转换为 X 形状。


第一条线将在 y 轴上向下移动并旋转 45 度以创建一条 X 形状的线。第二行将通过将其不透明度设置为零而消失。最后一条线将在 y 轴上向上移动,并逆时针方向旋转 45 度以完成 X 形状。我们将通过在  translate()rotate()  函数中使用负值,将其转换为与第一行相反的方向。


.hamburger:hover .hamburger__line:nth-child(1) {
transform: translateY(9px) rotate(45deg);
}

.hamburger:hover .hamburger__line:nth-child(2) {
opacity: 0;
}

.hamburger:hover .hamburger__line:nth-child(3) {
transform: translateY(-9px) rotate(-45deg);
}

若要应用转换,我们将使用该 transition 属性。动画将使用 ease-out 计时功能运行 300 毫秒 (0.3s)。该 all 值表示将对样式更改进行动画处理,包括 transformopacity 属性。


.hamburger__line {
transition: all 0.3s ease-out;
}

通过将鼠标悬停在按钮上来尝试一下。



形成减号


在这种方法中,当按钮悬停在按钮上时,我们会将其变成减号。我们将使用与上一种方法相同的转换,但我们不会旋转第一行和最后一行。


相反,我们将在 y 轴上向下移动第一行,直到它到达第二行。第三条线将向上移动,直到到达第一行。然后,第二行将关闭可见性,就像在前面的方法中一样。


第一行和最后一行的 `transform` 属性将与前面的方法相同,只是我们将不再使用该 `rotate()` 函数。


.hamburger:hover .hamburger__line:nth-child(1) {
transform: translateY(9px);
}

.hamburger:hover .hamburger__line:nth-child(2) {
opacity: 0;
}

.hamburger:hover .hamburger__line:nth-child(3) {
transform: translateY(-9px);
}

看看它是什么样子的!



要将按钮变成减号,我们可以使用另一种效果,将第一行和最后一行水平移出按钮。我们将使用该 translateX() 函数来指示位置仅在 x 轴上发生了变化。使用 translateX(-100%) ,可以将目标从左向右移出容器,而使用translateX(100%) ,我们可以做相反的事情。


这两种转换都将 opacity 属性设置为零,使第一行和最后一行不可见。因此,动画完成后,只有第二行仍然可见。


.hamburger:hover .hamburger__line:nth-child(1) {
opacity: 0;
transform: translateX(-100%);
}

.hamburger:hover .hamburger__line:nth-child(3) {
opacity: 0;
transform: translateX(100%);
}

看看这如何重现减号。



形成加号


在本节中,我们将向您展示另一种类型的转换。当用户将鼠标悬停在按钮上时,它会变成一个加号。为了达到这种效果,我们将第一条线向下移动,直到它与第二条线相遇,从而形成一条水平线。


然后,我们移动 y 轴上的最后一条线并将其逆时针旋转 90 度,形成加号的垂直线。最后,我们调整 opacity  第二行,使其在动画后不可见。


.hamburger:hover .hamburger__line:nth-child(1) {
transform: translateY(9px);
}

.hamburger:hover .hamburger__line:nth-child(2) {
opacity: 0;
}

.hamburger:hover .hamburger__line:nth-child(3) {
transform: translateY(-9px) rotate(-90deg);
}

查看下面的演示,了解这种方法的实际应用。



形成箭头


为了在按钮上创建箭头,我们使用简单的转换技术。第一条线旋转 45 度并沿 x 轴和 y 轴移动,直到它与第二条线的第一个点相交,形成箭头的顶线。然后,我们减小第一行的宽度,使其看起来更时尚。将相同的转换应用于最后一行,以创建箭头的底线。


如果需要调整箭头的位置,请随意调整传递给 translate() 函数的值。


.hamburger:hover .hamburger__line:nth-child(1) {
transform: translate(-2px, 4px) rotate(-45deg);
width: 16px;
}

.hamburger:hover .hamburger__line:nth-child(3) {
transform: translate(-2px, -4px) rotate(45deg);
width: 16px;
}

当您将鼠标悬停在按钮上时,箭头的样子如下:



要更改箭头的方向,请调整 translate() 函数的参数。这将确保第一行和最后一行到达第二行的末尾,并且箭头将沿相反方向旋转。


.hamburger:hover .hamburger__line:nth-child(1) {
transform: translate(17px, 4px) rotate(45deg);
width: 16px;
}

.hamburger:hover .hamburger__line:nth-child(3) {
transform: translate(17px, -4px) rotate(-45deg);
width: 16px;
}


原文:phuoc.ng/collection/…


作者:关山月
来源:juejin.cn/post/7325040809698656256
收起阅读 »

龙年到~ 我做了一个龙年红包封面,一大堆人问我教程

web
前言 就在昨天微信公众号给了我一个年终总结赠送了我六百的红包额度,那么我心想白送我? 要知道买额度现在都要一块钱一个红包封面了呢,所以我打算自己做一个红包封面但是听说审核很难过诶~ 没关系我已经踩坑完毕做出来了一个红包封面现在我就把流程分享给大家~ 亲测百分之...
继续阅读 »

前言


就在昨天微信公众号给了我一个年终总结赠送了我六百的红包额度,那么我心想白送我? 要知道买额度现在都要一块钱一个红包封面了呢,所以我打算自己做一个红包封面但是听说审核很难过诶~ 没关系我已经踩坑完毕做出来了一个红包封面现在我就把流程分享给大家~ 亲测百分之百通过!


红包封面展示


img


后台数据


img


这是我做的快去领取吧~


制作的第一个龙年红包上线


制作红包封面


制作红包封面需要 PS 等技术,啊? 我不会啊 我就想到了在线制作海报封面的网站(会 PS 也可以自己画图随便画画都可以只要是原创即可)


我使用的是图怪兽自己在线制作完毕之后喊朋友帮我下载的他有VIP 哈哈哈,也可以进行截图懂我意思吧?


这里我就实现制作了一张海报封面图片,大概话费 30 分钟素材网站上面都有发挥你的想象好吗~


img


紧接着无水印下载,没有 VIP 的按上面说的方法或者评论说一下我帮你~


压缩图片


红包封面它的大小只能是 500kb 的大小


img


丢给熊猫压缩压缩,直接给我压缩到 4 百多 KB


img


打开红包封面平台


微信红包封面开放平台: cover.weixin.qq.com/cgi-bin/mmc…


如果没有注册就注册一个


点击定制封面,进去上传图片


img


上传红包封面进行裁剪到你自己喜欢的感觉即可


img


一些选填的我这里就没准备就没去上传了,接着我们继续上传封面故事


大小不能超过 300 kb 我们继续丢给熊猫压缩压缩可能就没作用了,这下要用到 PS 了


img


打开在线 PS


随便找一个都可以我用的是这个 http://www.tuyitu.com/ps/sources/


点击文件 files 打开你的红包封面图片


img


img


点击图像, 图像大小 我们 宽改为 750 高改为 1250 官方要求的哦



如果把握不住那就用这个裁剪图片网站 tinypng.com/



img


img


修改完毕之后我们进行导出


img


将大小调整到 300kb 如果画质不好那么就去图像修改画布的大小与图片温和即可


img


img


前往红包开放平台上传我们的封面故事,在自己写一段故事描述,我这里就使用混元大模型给我生成一个龙年的祝福语~


img


img


我也祝大家: 龙腾盛世展才华,年贺新春喜洋洋 祝福亲友事事顺,心圆梦圆福满堂!!!


最后一步


证据材料 如果不上传这个 百分之 99 会给退回


img


PSD 源文件


使用在线 PS 打开我们的红包封面图片,在进行另存为 PSD 即可


img


直接进行上传,提交之后等待审核即可,百分之百成功!!


img


img


工作日 10 分钟就审核完毕了,耐心等待~ 如果制作成功 可以贴在评论区一起领取使用呀~




作者:杨不易呀
来源:juejin.cn/post/7325647896047583273
收起阅读 »

Linux操作系统简介:为何成为全球开发者热门选择?

Linux是一种自由和开放源代码的操作系统。这意味着任何人都可以查看、修改和分发Linux的源代码,而不需要支付任何费用。这种开放性使得Linux能够快速地发展和进步,吸引了全球数以万计的开发者共同参与其中,形成了一个庞大的开源社区。那么,Linux究竟是什么...
继续阅读 »

Linux是一种自由和开放源代码的操作系统。这意味着任何人都可以查看、修改和分发Linux的源代码,而不需要支付任何费用。这种开放性使得Linux能够快速地发展和进步,吸引了全球数以万计的开发者共同参与其中,形成了一个庞大的开源社区。

那么,Linux究竟是什么?它又是如何影响我们的生活的呢?让我们一起探索一下。

一、Linux操作系统介绍

在介绍Linux之前,先带大家了解一下什么是自由软件。自由软件的自由(free)有两个含义:第一,是可免费提供给任何用户使用;第二,是指它的源代码公开和自由修改。

所谓自由修改是指用户可以对公开的源代码进行修改,以使自由软件更加完善,还可在对自由软件进行修改的基础上开发上层软件。

Description

下面我们再来看看Linux操作系统的概念:

Linux,一般指GNU/Linux(单独的Linux内核并不可直接使用,一般搭配GNU套件,故得此称呼),是一种免费使用和自由传播的类UNIX操作系统,其内核由林纳斯·本纳第克特·托瓦兹(Linus Benedict Torvalds)于1991年10月5日首次发布。

它主要受到Minix和Unix思想的启发,是一个基于POSIX的多用户、多任务、支持多线程和多CPU的操作系统。它支持32位和64位硬件,能运行主要的Unix工具软件、应用程序和网络协议。

二、Linux系统的特点

那么,Linux为什么如此重要呢?这主要得益于它的以下几个特点:

开源免费:

Linux系统是完全免费的,任何人都可以免费使用、修改和分发。这使得Linux得以迅速传播,吸引了大量的开发者参与其中,共同推动其发展。

稳定性高:

Linux系统的稳定性非常高,长时间运行不会出现死机、蓝屏等问题。这也是为什么许多大型企业和政府部门都选择Linux作为服务器操作系统的原因。

兼容性好:

Linux支持几乎所有的硬件平台,包括x86、ARM、PowerPC等。这使得Linux可以在各种不同的设备上运行如个人电脑、手机、路由器等。同时,Linux系统还支持多种编程语言,为开发者提供了广阔的发挥空间。

强大的定制性:

Linux操作系统具有很强的定制性,用户可以根据自己的需求对系统进行深度定制。这使得Linux成为了服务器、嵌入式设备、超级计算机等领域的首选操作系统。

丰富的软件资源:

由于Linux的开源特性,许多优秀的开源软件都选择在Linux平台上发布。这些软件涵盖了从办公应用、图像处理、编程语言到数据库等各种领域,为用户提供了丰富的选择。

社区支持:

Linux拥有一个庞大的开源社区,用户可以在这里寻求帮助、分享经验、讨论问题。这种社区的支持使得Linux用户能够更好地解决问题,提高自己的技能。

三、Linux的应用

Linux的影响力已经远远超出了计算机领域,在服务器、嵌入式、开发、教育等领域都有着广泛应用。

服务器领域:

在服务器领域,Linux已经成为了主流的操作系统。据统计,世界上超过70%的服务器都在运行Linux。

Description
在云计算领域,Linux也占据了主导地位。许多知名的云服务提供商,如Amazon、Google、Microsoft等,都提供了基于Linux的云服务。

嵌入式领域:

由于Linux系统具有高度的可定制性和稳定性,因此在嵌入式领域也有着广泛的应用。

Description

如智能家居设备、无人机、机器人等都使用了Linux作为其操作系统,都离不开Linux系统的支持。这是因为Linux具有高度的可定制性和稳定性,可以满足这些设备的特殊需求。

开发领域:

Linux系统是程序员们的最爱,许多知名的开源项目都是基于Linux系统开发的,如Apache、MySQL、PHP等。

Description

此外,Linux系统还是云计算、大数据等领域的重要基础。

教育领域:

Linux系统在教育领域的应用也日益普及,许多高校和培训机构都开设了Linux相关课程,培养了大量的Linux人才。

你还在苦恼找不到真正免费的编程学习平台吗?可以试试【云端源想】!课程视频、知识库、微实战、云实验室、一对一咨询……你想要的全部学习资源这里都有,重点是现在还是免费的!点这里立即免费学习!

四、Linux系统的组成

Linux系统一般有4个主要部分:内核,Shell,文件系统和应用程序。

Description

Linux内核: 内核是系统的“内脏“,是运行程序和管理像磁盘及打印机等硬件设备的核心程序。

Linux shell: shell是系统的用户界面,提供了用户与内核进行交互操作的一种接口。它接收用户输入的命令并送入内核中执行。实际上shell是一个命令解释器,解释由用户输入命令并且把他们送到内核。

Linux 文件系统: 文件系统是文件存放在磁盘等存储设备上的组织方法。Linux能支持多种目前流行的文件系统,如XFS、EXT2/3/4、FAT、VFAT、ISO9660、NFS、CIFS等。

Linux应用程序: 标准的Linux系统都有一套称为应用程序的程序集,包括文本编辑器、编程语言、X Window、办公软件、Internet工具、数据库等。

五、总结

总的来说,Linux是一个强大、灵活、稳定和安全的操作系统,它正在改变我们的生活和工作方式。无论你是一名开发者,还是一名普通用户,都应该了解和学习Linux,因为它将会给你带来无尽的可能性和机会。

在未来的日子里,我们将会看到Linux在更多的领域发挥其强大的影响力。无论是在数据中心、云计算、物联网,还是在人工智能、机器学习等领域,Linux都将扮演着重要的角色。

收起阅读 »

Object.assign 这算是深拷贝吗

web
在JavaScript中,Object.assign() 是一个用于合并对象属性的常见方法。然而,对于许多开发者来说,关于它是否执行深拷贝的认识可能存在一些混淆。先说答案Object.assign() 不属于深拷贝,我们接着往下看。 Object.assign...
继续阅读 »

在JavaScript中,Object.assign() 是一个用于合并对象属性的常见方法。然而,对于许多开发者来说,关于它是否执行深拷贝的认识可能存在一些混淆。先说答案Object.assign() 不属于深拷贝,我们接着往下看。


Object.assign() 概览


首先,让我们回顾一下 Object.assign() 的基本用法。该方法用于将一个或多个源对象的属性复制到目标对象,并返回目标对象。这一过程是浅拷贝的,即对于嵌套对象或数组,只是拷贝了引用而非创建新的对象。


const obj = { a: 1, b: { c: 2 } };
const obj2 = { d: 3 };

const mergedObj = Object.assign({}, obj, obj2);

console.log(mergedObj);
// 输出: { a: 1, b: { c: 2 }, d: 3 }

浅拷贝的陷阱


浅拷贝的特性意味着如果源对象中包含对象或数组,那么它们的引用将被复制到新的对象中。这可能导致问题,尤其是在修改新对象时,原始对象也会受到影响。


const obj = { a: 1, b: { c: 2 } };
const clonedObj = Object.assign({}, obj);
clonedObj.b.c = 3;

console.log(obj); // { a: 1, b: { c: 3 } }
console.log(clonedObj); // { a: 1, b: { c: 3 } }

在这个例子中,修改 clonedObj 的属性也会影响到原始对象 obj


因此,如果我们需要创建一个全新且独立于原始对象的拷贝,我们就需要进行深拷贝。而 Object.assign() 并不提供深拷贝的功能。


深拷贝的需求


如果你需要进行深拷贝而不仅仅是浅拷贝,就需要使用其他的方法,如使用递归或第三方库来实现深度复制。以下是几种常见的深拷贝方法:


1. 使用 JSON 序列化和反序列化


const obj = { a: 1, b: { c: 2 } };
const deepClonedObj = JSON.parse(JSON.stringify(obj));
deepClonedObj.b.c = 3;

console.log(obj); // { a: 1, b: { c: 2 } }
console.log(deepClonedObj); // { a: 1, b: { c: 3 } }

这种方法利用了 JSON 的序列化反序列化过程,通过将对象转换为字符串,然后再将字符串转换回对象,实现了一个全新的深拷贝对象。


需要注意的是,这种方法有一些限制,例如无法处理包含循环引用的对象,以及一些特殊对象(如 RegExp 对象)可能在序列化和反序列化过程中失去信息。


2. 使用递归实现深拷贝


function deepClone(obj) {
if (obj === null || typeof obj !== 'object') {
return obj;
}

const clonedObj = Array.isArray(obj) ? [] : {};

for (let key in obj) {
if (obj.hasOwnProperty(key)) {
clonedObj[key] = deepClone(obj[key]);
}
}

return clonedObj;
}

const obj = { a: 1, b: { c: 2 } };
const deepClonedObj = deepClone(obj);
deepClonedObj.b.c = 3;

console.log(obj); // { a: 1, b: { c: 2 } }
console.log(deepClonedObj); // { a: 1, b: { c: 3 } }

这是一个递归实现深拷贝的方法。它会递归地遍历对象的属性,并创建它们的副本。这种方法相对灵活,可以处理各种情况。


但需要注意在处理大型对象或深度嵌套的对象时可能会导致栈溢出。


3. 使用第三方库


许多第三方库提供了强大而灵活的深拷贝功能,其中最常用的是 lodash 库中的 _.cloneDeep 方法。


const _ = require('lodash');

const obj = { a: 1, b: { c: 2 } };
const deepClonedObj = _.cloneDeep(obj);
deepClonedObj.b.c = 3;

console.log(obj); // { a: 1, b: { c: 2 } }
console.log(deepClonedObj); // { a: 1, b: { c: 3 } }

使用第三方库的优势在于它们通常经过精心设计和测试,可以处理更多的边界情况,并提供更好的性能。


作者:星光漫步者
来源:juejin.cn/post/7325040809697591296
收起阅读 »

什么,你还不会调试线上 vue 组件?

web
前言 彦祖们,在日常开发中,不知道你们是否遇到过这样的场景 在本地测试开发 vue 组件的时候非常顺畅 一上生产环境,客户说数据展示错误,样式不对... 但是你在本地测试了几次,都难以复现 定位方向 这时候作为老 vuer,自然就想到了 vue devtool...
继续阅读 »

前言


彦祖们,在日常开发中,不知道你们是否遇到过这样的场景


在本地测试开发 vue 组件的时候非常顺畅


一上生产环境,客户说数据展示错误,样式不对...


但是你在本地测试了几次,都难以复现


定位方向


这时候作为老 vuer,自然就想到了 vue devtools


但是新问题又来了,线上环境我们如何开启 vue devtools 呢?


案例演示


让我们以 element-ui 官网为例


先看下此时的 chrome devtools 是没有 Vue 的选项卡的
image.png


一段神奇的代码


其实很简单,我们只需要打开控制台,运行一下以下代码


var Vue, walker, node;
walker = document.createTreeWalker(document.body,1);
while ((node = walker.nextNode())) {
if (node.__vue__) {
Vue = node.__vue__.$options._base;
if (!Vue.config.devtools) {
Vue.config.devtools = true;
if (window.__VUE_DEVTOOLS_GLOBAL_HOOK__) {
window.__VUE_DEVTOOLS_GLOBAL_HOOK__.emit("init", Vue);
console.log("==> vue devtools now is enabled");
}
}
break;
}
}

image.png


显示 vue devtools now is enabled


证明我们已经成功开启了 vue devtools


功能验证


然后再重启一下 chrome devtool 看下效果


image.png


我们会发现此时多了一个 Vue 选项卡,功能也和我们本地调试一样使用


对于遇到 vue 线上问题调试,真的非常好用!


写在最后


本次分享虽然没有什么技术代码,重在白嫖


感谢彦祖们的阅读


个人能力有限


如有不对,欢迎指正🌟 如有帮助,建议小心心大拇指三连🌟


作者:前端手术刀
来源:juejin.cn/post/7324643000700502031
收起阅读 »

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
收起阅读 »

面试官: forEach怎么停止

web
介绍 在准备 JavaScript 面试时,理解数组方法的复杂性至关重要。一个常见的问题是是否可以停止或中断 forEach 循环。本文探讨了 forEach 方法的功能、其局限性以及 JavaScript 中用于突破循环的替代解决方案。我们的目标是通过清晰的...
继续阅读 »

介绍


在准备 JavaScript 面试时,理解数组方法的复杂性至关重要。一个常见的问题是是否可以停止或中断 forEach 循环。本文探讨了 forEach 方法的功能、其局限性以及 JavaScript 中用于突破循环的替代解决方案。我们的目标是通过清晰的解释和实际的代码示例来消除这一概念的神秘感。


在深入探讨之前,请在我的个人网站上探索更多关于 Web 开发的深度文章:


了解 JavaScript 中的 forEach 🤔


JavaScript 的 forEach 方法是迭代数组的流行工具。它为每个数组元素执行一次提供的函数。然而,与传统的 forwhile 循环不同,forEach 旨在为每个元素执行函数,没有内置机制来提前停止或中断循环。


const fruits = ["apple", "banana", "cherry"];
fruits.forEach(function(fruit) {
console.log(fruit);
});

这段代码将输出:


apple
banana
cherry

forEach 的局限性 🚫


1. forEach 中的 break


forEach 的一个关键限制是无法使用传统的控制语句比如 breakreturn 来停止或中断循环。如果您试图在 forEach 内使用 break,将遇到语法错误,因为 break 不适用于回调函数中。


尝试中断 forEach


通常,break 语句用于在满足某个条件时提前退出循环。


const numbers = [1, 2, 3, 4, 5];
numbers.forEach(number => {
if (number > 3) {
break; // 语法错误:非法 break 语句
}
console.log(number);
});

当您试图在 forEach 循环中使用 break 时,JavaScript 抛出一个语法错误。这是因为 break 被设计为在传统循环(如 forwhiledo...while)中使用,在 forEach 的回调函数中不被识别。


2. forEach 中的 return


在其他循环或函数中,return 语句退出循环或函数,如果指定的话返回一个值。


forEach 的上下文中,return 不会跳出循环。相反,它仅仅退出回调函数的当前迭代,并继续下一个数组元素。


尝试返回 forEach


const numbers = [1, 2, 3, 4, 5]; 
numbers.forEach(number => {
if (number === 3) {
return; // 仅退出当前迭代
}
console.log(number);
});

输出


1
2
4
5

在这个例子中,return 跳过了打印 3,但是循环继续剩余的元素。


使用异常中断 forEach 循环 🆕


尽管不建议常规使用,但从技术上来说,通过抛出异常可以停止 forEach 循环。尽管这种方法非正统,一般不建议使用,因为它影响代码的可读性和错误处理,但它可以有效地停止循环。


const numbers = [1, 2, 3, 4, 5];
try {
numbers.forEach(number => {
if (number > 3) {
throw new Error('Loop stopped');
}
console.log(number);
});
} catch (e) {
console.log('Loop was stopped due to an exception.');
}
// 输出: 1, 2, 3, 循环由于异常而停止。

在这个例子中,当满足条件时,抛出一个异常,提前退出 forEach 循环。但是,重要的是要正确处理这些异常,以避免意外的副作用。


用于中断循环的 forEach 替代方法 💡


使用 for...of 循环


for...of 循环是在 ES6(ECMAScript 2015)中引入的,它提供了一种现代的、简洁的和可读的方式来迭代类似数组、字符串、映射、集合等可迭代对象。与 forEach 相比,它的关键优势在于它与 breakcontinue 等控制语句兼容,在循环控制方面提供了更大的灵活性。


for...of 的优点:



  • 灵活性:允许使用 breakcontinuereturn 语句。

  • 可读性:提供清晰简洁的语法,使代码更易读和理解。

  • 通用性:能够迭代各种可迭代对象,不仅仅是数组。


for...of 的实际示例


考虑以下场景,我们需要处理数组的元素,直到满足某个条件:


const numbers = [1, 2, 3, 4, 5];  

for (const number of numbers) {
if (number > 3) {
break; // 成功中断循环
}
console.log(number);
}

输出:


1
2
3

在这个例子中,循环迭代 numbers 数组中的每个元素。一旦遇到大于 3 的数字,它利用 break 语句退出循环。这在 forEach 中是不可能的。


其他方法



  • Array.prototype.some():可以使用它来通过返回 true 来模拟中断循环。

  • Array.prototype.every():当返回 false 值时,此方法停止迭代。


结论 🎓


尽管 JavaScript 中的 forEach 方法提供了直接的数组迭代方式,但它缺乏在循环中段中断或停止的灵活性。理解这个限制对开发人员来说至关重要。幸运的是,像 for...of 循环以及 some()every() 等方法提供了必要的控制来处理更复杂的场景。掌握这些概念不仅可以增强你的 JavaScript 技能,还可以让你为艰巨的面试问题和实际编程任务做好准备。


作者:今天正在MK代码
来源:juejin.cn/post/7324384460136611850
收起阅读 »

html中的lang起到什么作用?

web
今天被lang="en"这玩意给坑了,平时看着不起眼的一个小配置,结果在中文换行的时候出现了不一样的效果…… 在chrome上是这样的 再看一下火狐浏览器的效果,加不加en都一样…… 起初还以为是chrome渲染机制的问题,把所有代码都删了才找到问题所在……...
继续阅读 »

今天被lang="en"这玩意给坑了,平时看着不起眼的一个小配置,结果在中文换行的时候出现了不一样的效果……


在chrome上是这样的


image.png


再看一下火狐浏览器的效果,加不加en都一样…… 起初还以为是chrome渲染机制的问题,把所有代码都删了才找到问题所在……


image.png


记录一下,避坑~


C3C69257283D24A892D56FA7AD82A2B1.png


代码贴在下面,感兴趣的可以去试一下


<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="utf-8">
<title>Document</title>
</head>

<body>
<p class="MsoNormal" style="width: 300px;background: yellow;">这一党章内容增写入宪法第一条第二款。<span lang="EN-US"></span>中国特色</p>
</body>

</html>

作者:lwlcode
来源:juejin.cn/post/7324750286329282597
收起阅读 »

MyBatis实战指南(三):相关注解及使用

在前面的两篇文章中,我们已经详细介绍了MyBatis的工作原理和基本使用。今天,我们将深入探讨MyBatis的一个重要特性——注解。如果你对MyBatis的注解还不熟悉,那么这篇文章将为你打开一扇新的大门。一、什么是注解(Annotation)首先,我们需要明...
继续阅读 »

在前面的两篇文章中,我们已经详细介绍了MyBatis的工作原理和基本使用。今天,我们将深入探讨MyBatis的一个重要特性——注解。如果你对MyBatis的注解还不熟悉,那么这篇文章将为你打开一扇新的大门。

一、什么是注解(Annotation)

首先,我们需要明白什么是注解。注解 Annotation 是从JDK1.5开始引入的新技术。

Description

在Java中,注解是一种用于描述代码的元数据,它可以被编译器、库和其他工具读取和使用。MyBatis的注解就是用来简化XML配置的,它们可以让你的代码更加简洁、易读。

注解的作用:

  • 不是程序本身,对程序作出解释
  • 可以被其他程序读取到

Annotation格式:

注解是以@注解名的方式在代码中实现的,可以添加一些参数值

如:@SuppressWarnings(value=“unchecked”)

注解使用的位置:

package、class、method、field 等上面,相当于给他们添加了额外的辅助信息。

注解的分类:

1.元注解:

  • @Target:用于描述注解的使用范围

  • @Retention:用于描述注解的生命周期

  • @Documented:说明该注解将被包含在javadoc 中

  • @Inherited:说明子类可以继承父类中的该注解

  • @Repeatable:可重复注解

2.内置注解:

  • @Override: 重写检查

  • @Deprecated:过时

  • @SuppressWarnings: 压制警告

  • @FunctionalInterface: 函数式接口

3.自定义注解:

  • public @interface MyAnno{}

二、Mybatis常用注解

首先介绍一下Mybatis注解的使用方法:

第一步,在全局配置文件里的配置映射



    


第二步,在mapper接口的方法的上面添加注解

@Select("select * from user where uid = #{uid}")

    public User findUserById(int uid);

第三步,创建会话调用此方法。

接下来,我们来看看MyBatis中最常用的几个注解:

(1)@Select

作用:标记查询语句。

@Select用于标记查询语句。该注解可以在接口方法上使用,也可以在XML文件中使用。使用@Select注解时,需要在注解中指定SQL语句。

示例:

@Select("SELECT * FROM users WHERE id = #{id}")

User getUserById(@Param("id") Long id);

(2)@Insert

作用:标记插入语句。

@Insert用于标记插入语句。该注解可以在接口方法上使用,也可以在XML文件中使用。使用@Insert注解时,需要在注解中指定SQL语句。

示例:

@Insert("INSERT INTO users(name, age) VALUES(#{name}, #{age})")

int addUser(User user);

(3)@Update

作用:标记更新语句。

@Update用于标记更新语句。该注解可以在接口方法上使用,也可以在XML文件中使用。使用@Update注解时,需要在注解中指定SQL语句。

示例:

@Update("UPDATE users SET name = #{name}, age = #{age} WHERE id = #{id}")

int updateUser(User user);

(4)@Delete

作用:标记删除语句。

@Delete用于标记删除语句。该注解可以在接口方法上使用,也可以在XML文件中使用。使用@Delete注解时,需要在注解中指定SQL语句。

示例:

@Delete("DELETE FROM users WHERE id = #{id}")

int deleteUserById(@Param("id") Long id);

(5)@Results

作用:用于指定多个@Result注解。

@Results用于标记结果集映射,该注解可以用于接口方法或XML文件中,通常与@Select注解一起使用。使用@Results注解时,需要指定映射规则。

示例:


@Select("SELECT * FROM users WHERE id = #{id}")

@Results(id = "userResultMap", value = {

    @Result(property = "id", column = "id"),

    @Result(property = "name", column = "name"),

    @Result(property = "age", column = "age")

})


User getUserById(@Param("id") Long id);

(6)@Result

作用:用于指定查询结果集的映射关系。

@Result用于标记单个属性与结果集中的列之间的映射关系。该注解可以用于接口方法或XML文件中,通常与@Results注解一起使用。使用@Result注解时,需要指定映射规则。

示例:

@Select("SELECT * FROM users WHERE id = #{id}")

@Results(id = "userResultMap", value = {

    @Result(property = "id", column = "id"),

    @Result(property = "name", column = "name"),

    @Result(property = "age", column = "age")

})


User getUserById(@Param("id") Long id);

(7)@ResultMap

作用:用于指定查询结果集的映射关系。

@ResultMap用于标记结果集映射规则。该注解可以用于接口方法或XML文件中,通常与@Select注解一起使用。使用@ResultMap注解时,需要指定映射规则。

示例:

@Select("SELECT * FROM users WHERE id = #{id}")

@ResultMap("userResultMap")

User getUserById(@Param("id") Long id);

(8)@Options

作用:用于指定插入语句的选项。

@Options用于指定一些可选的配置项。该注解可以用于接口方法或XML文件中,通常与@Insert、@Update、@Delete等注解一起使用。使用@Options注解时,可以指定一些可选的配置项。

示例:

@Insert("INSERT INTO users(name, age) VALUES(#{name}, #{age})")

@Options(useGeneratedKeys = true, keyProperty = "id")

int insertUser(User user);

(9)@SelectKey

作用:用于指定查询语句的主键生成方式。

@SelectKey用于在执行INSERT语句后获取自动生成的主键值。该注解可以用于接口方法或XML文件中,通常与@Insert注解一起使用。使用@SelectKey注解时,需要指定生成主键的SQL语句和将主键值赋给Java对象的哪个属性。

示例:

@Insert("INSERT INTO users(name, age) VALUES(#{name}, #{age})")

@SelectKey(statement = "SELECT LAST_INSERT_ID()", keyProperty = "id", before = false, resultType = Long.class)

int insertUser(User user);

(10)@Param

作用:用于指定方法参数名称。

@Param用于为SQL语句中的参数指定参数名称。该注解可以用于接口方法或XML文件中,通常与@Select、@Insert、@Update、@Delete等注解一起使用。使用@Param注解时,需要指定参数名称。

示例:

@Select("SELECT * FROM users WHERE name = #{name} AND age = #{age}")

List getUsersByNameAndAge(@Param("name") String name, @Param("age") Integer age);

(11)@One

作用:用于指定一对一关联关系。

@One用于在一对一关联查询中指定查询结果的映射方式。该注解可以用于XML文件中,通常与和标签一起使用。使用@One注解时,需要指定查询结果映射的Java对象类型和查询结果映射的属性。



  

  

  

  







  

  

  

  


上述代码中,@One注解用于指定查询结果的映射方式,这里使用了嵌套的标签实现了一对一关联查询。在departmentResultMap中,使用@One注解指定了查询结果映射的Java对象类型为User,查询结果映射的属性为manager,resultMap参数指定了查询结果映射的结果集映射规则为userResultMap。

除了使用@One注解之外,还可以使用@Many注解来指定一对多关联查询的映射方式。

总之,@One注解是MyBatis中用于在一对一关联查询中指定查询结果的映射方式的注解之一,可以方便地实现一对一关联查询的结果映射。

(12)@Many

作用:用于指定一对多关联关系。

@Many用于在一对多关联查询中指定查询结果的映射方式。该注解可以用于XML文件中,通常与和标签一起使用。使用@Many注解时,需要指定查询结果映射的Java对象类型和查询结果映射的属性。

示例:



  

  

  

  







  

  

  


上述代码中,@Many注解用于指定查询结果的映射方式,这里使用了嵌套的标签实现了一对多关联查询。在departmentResultMap中,使用@Many注解指定了查询结果映射的Java对象类型为User,查询结果映射的属性为members,ofType参数指定了集合中元素的类型为User,resultMap参数指定了查询结果映射的结果集映射规则为userResultMap。

除了使用@Many注解之外,还可以使用@One注解来指定一对一关联查询的映射方式。

总之,@Many注解是MyBatis中用于在一对多关联查询中指定查询结果的映射方式的注解之一,可以方便地实现一对多关联查询的结果映射。

你还在苦恼找不到真正免费的编程学习平台吗?可以试试【云端源想】!课程视频、知识库、微实战、云实验室、一对一咨询……你想要的全部学习资源这里都有,重点是统统免费!点这里即可查看

(13)@ResultType

作用:用于指定查询结果集的类型。

@ResultType用于指定查询结果的类型。该注解可以用于接口方法或XML文件中,通常与@Select、@Insert、@Update、@Delete等注解一起使用。使用@ResultType注解时,需要指定查询结果的类型。

示例:

@Select("SELECT name, age FROM users WHERE id = #{id}")

@ResultType(User.class)

User getUserById(Long id);

(14)@TypeDiscriminator

作用:用于指定类型鉴别器,用于根据查询结果集的不同类型映射到不同的Java对象。

@TypeDiscriminator用于在自动映射时指定不同子类型的映射方式。该注解可以用于XML文件中,通常与和标签一起使用。使用@TypeDiscriminator注解时,需要指定类型列的名称和不同子类型的映射方式。

示例:



  

  

  

  

    

    

    

  








  

  







  








  


上述代码中,@TypeDiscriminator注解用于指定不同子类型的映射方式。在vehicleResultMap中,使用@TypeDiscriminator注解指定了类型列的名称为type,javaType参数指定了类型列的Java类型为String,标签中的value属性分别对应不同的子类型(car、truck、bus),resultMap属性用于指定不同子类型的结果集映射规则。

除了使用@TypeDiscriminator注解之外,还可以使用标签来指定不同子类型的映射方式。

总之,@TypeDiscriminator注解是MyBatis中用于在自动映射时指定不同子类型的映射方式的注解之一,可以方便地实现自动映射不同子类型的结果集映射规则。

(15)@ConstructorArgs

作用:用于指定Java对象的构造方法参数。

@ConstructorArgs用于指定查询结果映射到Java对象时使用的构造函数和构造函数参数。该注解可以用于XML文件中,通常与标签一起使用。使用@ConstructorArgs注解时,需要指定构造函数参数的映射关系。

示例:



  

  

    

    

  



(16)@Arg

作用:用于指定Java对象的构造方法参数。

@Arg用于指定查询结果映射到Java对象时构造函数或工厂方法的参数映射关系。该注解可以用于接口方法或XML文件中,通常与@Select、@Insert、@Update、@Delete等注解一起使用。使用@Arg注解时,需要指定参数的映射关系。

示例:

@Select("SELECT name, age FROM users WHERE id = #{id}")

User getUserById(@Arg("name") String name, @Arg("age") int age);

(17)@Discriminator

作用:用于指定类型鉴别器的查询结果。

@Discriminator用于在自动映射时指定不同子类型的映射方式。该注解可以用于接口方法或XML文件中,通常与@Select、@Insert、@Update、@Delete等注解一起使用。使用@Discriminator注解时,需要指定类型列的名称和不同子类型的映射方式。

示例:


@Select("SELECT * FROM vehicle WHERE type = #{type}")

@Discriminator(column = "type", javaType = String.class, cases = {

  @Case(value = "car", type = Car.class),

  @Case(value = "truck", type = Truck.class),

  @Case(value = "bus", type = Bus.class)

})


List getVehiclesByType(String type);

(18)@CacheNamespace

作用:用于指定缓存的命名空间。

@CacheNamespace用于指定Mapper接口中的查询结果是否进行缓存。该注解可以用于Mapper接口上,用于指定Mapper接口中所有方法默认的缓存配置。使用@CacheNamespace注解时,需要指定缓存配置的属性。

示例:

@CacheNamespace(

  implementation = MyBatisRedisCache.class,

  eviction = MyBatisRedisCache.Eviction.LRU,

  flushInterval = 60000,

  size = 10000,

  readWrite = true,

  blocking = true

)


public interface UserMapper {

  @Select("SELECT * FROM users WHERE id = #{id}")

  User getUserById(Long id);

  // ...

}

(19)@Flush

作用:用于在插入、更新或删除操作之后自动清空缓存。

@Flush是用于在Mapper接口中指定在执行方法前或方法后刷新缓存。该注解可以用于Mapper接口方法上,通常与@Select、@Insert、@Update、@Delete等注解一起使用。使用@Flush注解时,需要指定刷新缓存的时机。

示例:

@Select("SELECT * FROM users WHERE id = #{id}")

@Flush(flushCache = FetchType.AFTER)

User getUserById(Long id);

(20)@MappedJdbcTypes

作用:用于指定Java对象属性与数据库列的映射关系。

@MappedJdbcTypes用于将Java类型映射到JDBC类型。该注解可以用于JavaBean属性或ResultMap中,用于指定Java类型对应的JDBC类型。使用@MappedJdbcTypes注解时,需要指定Java类型和对应的JDBC类型。

示例:

public class User {

  private Long id;

  @MappedJdbcTypes(JdbcType.VARCHAR)

  private String name;

  private Integer age;

  // ...

}

(21)@MappedTypes

作用:用于指定Java对象与数据库类型的映射关系。

@MappedTypes用于将Java类型映射到JDBC类型。该注解可以用于JavaBean属性或ResultMap中,用于指定Java类型对应的JDBC类型。使用@MappedTypes注解时,需要指定Java类型。

示例:

@MappedTypes(User.class)

public interface UserMapper {

  @Select("SELECT * FROM users WHERE id = #{id}")

  User getUserById(Long id);

  // ...

}

(22)@SelectProvider

作用:用于指定动态生成SQL语句的提供者。

@SelectProvider是用于在Mapper接口中动态生成查询SQL语句。该注解可以用于Mapper接口方法上,用于指定一个提供SQL语句的Provider类。使用@SelectProvider注解时,需要指定Provider类和Provider方法。

示例:

@SelectProvider(type = UserSqlProvider.class, method = "getUserByIdSql")

User getUserById(Long id);

(23)@InsertProvider

作用:用于指定动态生成SQL语句的提供者。

@InsertProvider用于在Mapper接口中动态生成插入SQL语句。该注解可以用于Mapper接口方法上,用于指定一个提供SQL语句的Provider类。使用@InsertProvider注解时,需要指定Provider类和Provider方法。

示例:

@InsertProvider(type = UserSqlProvider.class, method = "insertUserSql")

int insertUser(User user);

(24)@UpdateProvider

作用:用于指定动态生成SQL语句的提供者。

@UpdateProvider用于在Mapper接口中动态生成更新SQL语句。该注解可以用于Mapper接口方法上,用于指定一个提供SQL语句的Provider类。使用@UpdateProvider注解时,需要指定Provider类和Provider方法。

示例:

@UpdateProvider(type = UserSqlProvider.class, method = "updateUserSql")

int updateUser(User user);

(25)@DeleteProvider

作用:用于指定动态生成SQL语句的提供者。

@DeleteProvider用于在Mapper接口中动态生成删除SQL语句。该注解可以用于Mapper接口方法上,用于指定一个提供SQL语句的Provider类。使用@DeleteProvider注解时,需要指定Provider类和Provider方法。

示例:

@DeleteProvider(type = UserSqlProvider.class, method = "deleteUserSql")

int deleteUser(Long id);

以上就是MyBatis的相关注解及使用示例了,实际开发中不一定每个都能用到,但是可以收藏起来,有备无患嘛!

总的来说,MyBatis的注解是一个非常强大的工具,它可以帮助你减少XML配置的工作量,让你的代码更加简洁、易读。但是,它也有一定的学习成本,你需要花一些时间去理解和掌握它。希望这篇文章能帮助你更好地理解和使用MyBatis的注解。

收起阅读 »

总是听说 Vue3 选择 Proxy 的原因是性能更好,不如直接上代码对比对比

web
逛掘金的时候经常能刷到关于 Vue 响应式原理的文章, 经常能看到 Vue3 弃用 Object.defineProperty 转而使用 Proxy 来实现的原因是 Proxy 性能更好 。看的多了还能刷到一些文章认为 Object.definePropert...
继续阅读 »

逛掘金的时候经常能刷到关于 Vue 响应式原理的文章, 经常能看到 Vue3 弃用 Object.defineProperty 转而使用 Proxy 来实现的原因是 Proxy 性能更好 。看的多了还能刷到一些文章认为 Object.defineProperty 性能更好,因此自己创建了一个小 demo 来对比二者在不同场景下的性能。



以下测试仅在 谷歌浏览器 中进行,不同浏览器内核不同,结果可能有差异。可以访问此 在线地址 测试其他环境下的性能。



封装响应式


本文不会详细解析基于 Object.definePropertyProxy 的封装代码,这些内容在多数文章中已有介绍。Vue3 对嵌套对象的响应式处理进行了优化,采用了一种惰性添加的方式,仅在对象被访问时才添加响应式。相比之下,Vue2 采用了一次性递归处理整个对象的方式添加响应式。为了确保比较的公平性,本文下面的 Object.defineProperty 代码也采用了相同的惰性添加策略。


Object.defineProperty


/** Object.defineProperty 深度监听 */
export function deepDefObserve(obj, week) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
let value = obj[key]

Object.defineProperty(obj, key, {
configurable: true,
enumerable: true,
get() {
if (
typeof value === "object" &&
value !== null &&
week &&
!week.has(value)
) {
week.set(value, true)
deepDefObserve(value)
}
return value
},
set(newValue) {
value = newValue
},
})
}
return obj
}

Proxy


/** Proxy 深度监听 */
export function deepProxy(obj, proxyWeek) {
const myProxy = new Proxy(obj, {
get(target, property) {
let res = Reflect.get(target, property)
if (
typeof res === "object" &&
res !== null &&
proxyWeek &&
!proxyWeek.has(res)
) {
proxyWeek.set(res, true)
return deepProxy(res)
}
return res
},
set(target, property, value) {
return Reflect.set(target, property, value)
},
})
return myProxy
}

测试性能


测试场景有五个:



  1. 使用两个 API 创建响应式对象的耗时,即 const obj = reactive({}) 的耗时

  2. 测量对已创建的响应式对象的属性进行访问的速度,即 obj.a 的读取时间。

  3. 测量修改响应式对象属性值的耗时,即执行 obj.a = 1 所需的时间。

  4. 创建多个响应式对象,并模拟访问和修改它们属性的操作,以评估在多对象场景下的性能表现。

  5. 针对嵌套对象进行响应式性能测试,以评估在复杂数据结构下的性能表现。


初始化性能


const _0_calling = {
useObjectDefineProperty() {
const data = { a: 1, b: 1, c: 1, d: 1, e: 1 }
const keys = Object.keys(data)
for (let i = 0; i < keys.length; i++) {
Object.defineProperty(data, keys[i], {
get() {},
set() {},
})
}
},
useProxy() {
const data = { a: 1, b: 1, c: 1, d: 1, e: 1 }
const proxy = new Proxy(data, {
get() {},
set() {},
})
},
}

image.png


很明显,Proxy 的性能优于 Object.defineProperty


读取性能


const readDefData = deepDefObserve({ a: 1, b: 1, c: 1, d: 1, e: 1 })
const readProxyData = deepProxy({ a: 1, b: 1, c: 1, d: 1, e: 1 })
export const _1_read = {
useObjectDefineProperty() {
readDefData.a
readDefData.b
readDefData.e
},
useProxy() {
readProxyData.a
readProxyData.b
readProxyData.e
},
}

image.png


Object.defineProperty 明显优于 Proxy


写入性能


const writeDefData = deepDefObserve({ a: 1, b: 1, c: 1, d: 1, e: 1 })
const writeProxyData = deepProxy({ a: 1, b: 1, c: 1, d: 1, e: 1 })
export const _2_write = {
count: 2,
useObjectDefineProperty() {
writeDefData.a = _2_write.count++
writeDefData.b = _2_write.count++
},
useProxy() {
writeProxyData.a = _2_write.count++
writeProxyData.b = _2_write.count++
},
}

image.png


Object.defineProperty 优于 Proxy,不过差距不大。


多次创建及读写


export const _4_create_read_write = {
count: 2,
useObjectDefineProperty() {
const data = { a: 1, b: 1, c: 1, d: 1, e: 1 }
deepDefObserve(data)
data.a = _4_create_read_write.count++
data.b = _4_create_read_write.count++
data.a
data.c
},
proxyWeek: new WeakMap(),
useProxy() {
const data = { a: 1, b: 1, c: 1, d: 1, e: 1 }
const proxy = deepProxy(data, _4_create_read_write.proxyWeek)
proxy.a = _4_create_read_write.count++
proxy.b = _4_create_read_write.count++
proxy.a
proxy.c
},
}

image.png


Proxy 优势更大,但这个场景并不多见,很少会出现一次性创建大量响应式对象的情况,对属性的读写场景更多。


对嵌套对象的性能


对内部的每个属性都进行读或写操作


const deepProxyWeek = new WeakMap()
const defWeek = new WeakMap()
export const _5_deep_read_write = {
count: 2,
defData: deepDefObserve(
{
res: {
code: 200,
message: {
error: null,
},
data: [
{
id: 1,
name: "1",
},
{
id: 2,
name: "2",
},
],
},
},
defWeek
),
useObjectDefineProperty() {
_5_deep_read_write.defData.res.code = _5_deep_read_write.count++
_5_deep_read_write.defData.res.data[0].id = _5_deep_read_write.count++
_5_deep_read_write.defData.res.message.error
_5_deep_read_write.defData.res.data[0].id
_5_deep_read_write.defData.res.data[0].name
_5_deep_read_write.defData.res.data[1].id
_5_deep_read_write.defData.res.data[1].name
},
proxyData: deepProxy(
{
res: {
code: 200,
message: {
error: null,
},
data: [
{
id: 1,
name: "1",
},
{
id: 2,
name: "2",
},
],
},
},
deepProxyWeek
),
useProxy() {
_5_deep_read_write.proxyData.res.code = _5_deep_read_write.count++
_5_deep_read_write.proxyData.res.data[0].id = _5_deep_read_write.count++
_5_deep_read_write.proxyData.res.message.error
_5_deep_read_write.proxyData.res.data[0].id
_5_deep_read_write.proxyData.res.data[0].name
_5_deep_read_write.proxyData.res.data[1].id
_5_deep_read_write.proxyData.res.data[1].name
},
}

image.png


Object.defineProperty 会稍好一些,但两者的差距不大。


只读取修改嵌套对象的浅层属性


const _6_deepProxyWeek = new WeakMap()
const _6_defWeek = new WeakMap()
export const _6_update_top_level = {
count: 2,
defData: deepDefObserve(
{
res: {
code: 200,
message: {
error: null,
},
data: [
{
id: 1,
name: "1",
},
{
id: 2,
name: "2",
},
],
},
},
_6_deepProxyWeek
),
useObjectDefineProperty() {
_6_update_top_level.defData.res.code = _6_update_top_level.count++
_6_update_top_level.defData.res.message.error
},
proxyData: deepProxy(
{
res: {
code: 200,
message: {
error: null,
},
data: [
{
id: 1,
name: "1",
},
{
id: 2,
name: "2",
},
],
},
},
_6_defWeek
),
useProxy() {
_6_update_top_level.proxyData.res.code = _6_update_top_level.count++
_6_update_top_level.proxyData.res.message.error
},
}

image.png


这个场景 Proxy 略优于 Object.defineProperty


总结


Proxy 在对象创建时的性能明显优于Object.defineProperty。而在浅层对象的读写性能方面,Object.defineProperty 表现更好。但是当对象的嵌套深度增加时,Object.defineProperty 的优势会逐渐减弱。尽管在性能测试中,Object.defineProperty 的读写优势可能更适合实际开发场景,但在 谷歌浏览器 中,Proxy 的性能与 Object.defineProperty 并没有拉开太大差距。因此,Vue3 选择 Proxy 不仅仅基于性能考量,还因为 Proxy 提供了更为友好、现代且强大的 API ,使得操作更加灵活。


作者:clench
来源:juejin.cn/post/7324141201802821672
收起阅读 »

封装v-loading指令 从此释放双手

web
封装v-loading指令 从此释放双手 前言 ​ 大家好, 我是旋风冲锋 - 小瑜, 又到了周六~~ 没错, 是卷王们疯狂成长的日子, 今天早上突发奇想, 想去自习室体验一下敲代码的快感, 心想着卷到下午,面对着窗口看着夕阳西下的场景, 然后可以...
继续阅读 »

封装v-loading指令 从此释放双手


前言


​ 大家好, 我是旋风冲锋 - 小瑜, 又到了周六~~ 没错, 是卷王们疯狂成长的日子, 今天早上突发奇想, 想去自习室体验一下敲代码的快感, 心想着卷到下午,面对着窗口看着夕阳西下的场景, 然后可以拍个照片发朋友圈装逼.


​ 但是想象很美好, 坐着地铁到了自习室, 发现大家伙都是在非常安静, 唯一能出声音的就是翻书或者写字的声音. 为了不影响其他人故意挑一个靠窗户的位置,接着对着电脑开始疯狂进攻. 我的键盘声很快传遍了整见屋子. 虽然别人没有说什么, 但是自己觉得好像是故意来捣乱的, 今天的键盘声音显得格外的大声. 没多久我知趣的溜溜球. 美团体验卷直接gg, 哈哈哈, 问题不大~


​ 言归正传, 今天给大家分享的是利用 VUE3 实现v-loading的加载效果 , 先看一下实现效果吧~


2023-09-24-00-48-48.gif


这一类效果在使用组件库, 例如饿了么中出现的频率很高, 使用方法也很简单, 给对应的结构添加上


v-loading="布尔值"即可, 是不是很好奇是怎么实现的? 那么就和旋风冲锋小瑜开始冲!


实现思路



  • loading肯定也是一个组件, 其中包含加载效果还有提示文字, 并且使用的时候可以去修改文字以及开启或者关闭加载动画

  • 实现的周期是在 异步开始前, 开启loading, 在异步处理[数据加载]完成后 关闭loading

  • 既然是在模版中通过 v-xxx来实现的, 那么肯定就是一个自定义指令, Vue提供指令, 也就是去操作DOM[组件实例]


那么按照以上的实现思路, 一步一步去完成, 首先搭设一个Demo的模版结构和样式


搭设基本模版


利用Vue3搭设demo架子, 头部tab栏, 切换路由 , main区域的显示内容


App.vue


<script setup lang="ts"></script>

<template>
<div class="container">
// Tab栏
<Tabs></Tabs>
// 一级路由出口
<router-view></router-view>
</div>
</template>


<style lang="scss">
.container {
width: 100vw;
height: 100vh;
background-color: #1e1e1e;
}
</style>


router路由


 routes: [
{
path: '/',
redirect: '/huawei'
},
{
path: '/huawei',
component: () => import('@/views/Huawei/index.vue'),
meta: {
title: '华为'
}
},
{
path: '/rongyao',
component: () => import('@/views/Rongyao/index.vue'),
meta: {
title: '荣耀'
}
},
{
path: '/xiaomi',
component: () => import('@/views/Xiaomi/index.vue'),
meta: {
title: '小米'
}
},
{
path: '/oppo',
component: () => import('@/views/Oppo/index.vue'),
meta: {
title: 'oppo'
}
}
]

Tabs组件


<script setup lang="ts">
import { ref } from 'vue'
const tabList = ref([
{ id: 1, text: '华为', path: '/huawei' },
{ id: 2, text: '荣耀', path: '/rongyao' },
{ id: 3, text: '小米', path: '/xiaomi' },
{ id: 4, text: 'oppo', path: '/oppo' }
])
const activeIndex = ref(0)
</script>

<template>
<div class="tabs-box">
<router-link
class="tab-item"
:to="item.path"
v-for="(item, index) in tabList"
:key="item.id"
>

<span
class="tab-link"
:class="{ active: activeIndex === index }"
@click="activeIndex = index"
>

{{ item.text }}
</span>
</router-link>
</div>
</template>


<style lang="scss" scoped>
.tabs-box {
width: 100%;
display: flex;
justify-content: space-around;
.tab-item {
padding: 10px;
&.router-link-active {
.tab-link {
transition: border 0.3s;
color: gold;
padding-bottom: 5px;
border-bottom: 2px solid gold;
&.active {
border-bottom: 2px solid gold;
color: gold;
}
}
}
}
}
</style>


按照路由去创建4个文件夹,这里按照huawei做举例


<script setup lang="ts">
const src = ref('')
</script>

<template>
<div class="box" >
<div class="img-box">
<img :src="src" alt="" />
</div>
</div>
</template>


创建Loading组件


首先按照最直接的方式, 利用 v-if 以及组件通讯, 实现组件方式的实现


注意 这里是通过 position: absolute; 通过定位的方式进行垂直水平居中, 先埋下伏笔


<script setup lang="ts">

defineProps({
title: {
type: String,
default: '正在加载中...'
}
})

</script>

<template>
<div class="loading-box">
<div class="loading-content">
// loading 动图
<img src="./loading.gif" />
// 底部提示文字
<p class="desc">{{ title }}</p>
</div>
</div>
</template>


<style lang="scss" scoped>
.loading-box {
position: absolute;
top: 50%;
left: 50%;
transform: translate3d(-50%, -50%, 0);
.loading-content {
text-align: center;
img {
width: 35px !important;
height: 35px !important;
}
.desc {
line-height: 20px;
font-size: 12px;
color: #fff;
position: relative;
}
}
}
</style>


在对应组件中使用Loading组件, 利用延时器模拟异步操作


<script>
const src = ref('')
const title = ref('华为加载中...')
const showLoading = ref(true) // 控制loading的显示和隐藏

onMounted(() => {
showLoading.value = true
// 模拟异步请求
window.setTimeout(() => {
src.value =
'https://ms.bdimg.com/pacific/0/pic/-1284887113_-1109246585.jpg?x=0&y=0&h=340&w=510&vh=340.00&vw=510.00&oh=340.00&ow=510.00'
showLoading.value = false
}, 1000)
})
</script>

<template>
<div class="box">
<div class="img-box" v-if="!showLoading">
<img :src="src" alt="" />
</div>
</div>
<Loading v-if="showLoading"></Loading>
</template>


效果一样可以出来, 接下来就利用指令的方式来优化


011.png


V-loading 指令实现


思路:



  • 在dom挂载完成后, 创建Loading实例, 需要挂载到写在具体指令结构上

  • loading需要知道传递的显示文字, 这里通过指令动态的参数传递

  • 当loading组件参数更新后卸载, 关闭loading


1. 使用自定义指令的参数


和内置指令类似,自定义指令的参数也可以是动态的, 下面是Vue官网的截图


动态指令参数.png


在模版中使用 v-loading:[title]="showLoading"


const title = ref('华为加载中...')

<template>
<div class="box" v-loading:[title]="showLoading">
...
</div>

<!-- <Loading v-if="showLoading"></Loading> -->
</template>

2. 利用插件注册指令


在Loading文件下创建js文件


import Loading from './index.vue' // 导入.vue文件
const loadingDirective = {
}
export default loadingDirective

components下创建index.js文件


import loading from '@/components/Loading/index'
export default {
install: (app: App) => {
app.directive('loading', loading)
}

入口文件中注册插件


import MyLoading from '@/components/index'
app.use(MyLoading)

3. 指令 - 节点都挂载完成后调用



  • createApp 作用: 创建一个应用实例 - 创建loading

  • app.mount 作用: 将应用实例挂载在一个容器元素中

  • mounted 参数 el=>获取dom

  • mounted 参数 binding.value => 控制开启和关闭loding 也就是 showLoading

  • mounted 参数 binding.arg => loading显示的文字 [例如华为加载中...]


const loadingDirective = {
/* 节点都挂载完成后调用 */
mounted(el: any, binding: DirectiveBinding) {
/*
value 控制开启和关闭loding
arg loading显示的文字
*/

const { value, arg } = binding
/* 创建loading实例,并挂载 */
const app = createApp(Loading)
// 这一步 instance === loading.vue
// 此时就可以视同loading.vue 也就是组件实例的方法和属性
const instance = app.mount(document.createElement('div'))
/* 为了让elAppend获取到创建的div元素 */
el.instance = instance
/* 如果传入了自定义的文字就添加title */
if (arg) {
instance.setTitle(arg)
}
/* 如果showLoading为true将loading实例挂载到指令元素中 */
if (value) {
// 添加方法方法, 看下文
handleAppend(el)
}
},
}

可以从控制台查看binding中的title以及showLoading的值


02.png


instance.setTitle(arg) 这里既然使用到了组件实例的setTitle方法, 就需要在loading中对应的方法


注意: 在vue3中需要利用defineExpose抛出事件, 让外界可以访问或使用


Loading.vue


const title = ref('')
const setTitle = (val: string) => {
title.value = val
}
// defineProps({ 组件通讯就使用不到了, 注释即可
// title: {
// type: String,
// default: '正在加载中...'
// }
// })

defineExpose({
setTitle,
title
})

<template>
<div class="loading-box">
<div class="loading-content">
<img src="./loading.gif" />
<p class="desc">{{ title }}</p>
</div>
</div>

</template>

4. 指令 - handleAppend(el)方法实现


/* 将loading添加到指令所在DOM */
const handleAppend = (el: any) => {
console.log(el.instance.$el, 'el.instance.$el')
el.appendChild(el.instance.$el)
}

04.png


5. 指令 - updated() 更新后挂载还是消除的逻辑


在第四步中, loading已经可以通过指令显示了, 此时还需要让showLoading为false的时候, 或者这么理解: 当新的值不等于老值的是够关闭loading


此时就可以利用指令中updated钩子去执行这一段关闭的逻辑, 一下是官网的说明


05.png


  /* 更新后调用 */
updated(el: any, binding: DirectiveBinding) {
const { value, oldValue, arg } = binding
if (value !== oldValue) {
/* 更新标题 */
if (arg) {
el.instance.setTitle(arg)
}
// 是显示吗? 如果是就添加 : 如果不是就删除
value ? handleAppend(el) : c(el)
}
}

6. 指令 - handleRemove()方法实现


/* 将loading在DOM中移除 */
const handleRemove = (el: any) => {
removeClass(el, relative as any)
el.removeChild(el.instance.$el)
}

此时基本已经完成了需求, 但是上文我提到了坑点, 原因是loading是通过绝对定位的方式进行水平居中, 那么比如我要在图片中显示loading呢? 我们来实现下这个坑点


7. 坑点的说明


<template>
<div class="box" //关闭 v-loading:[title]="showLoading">
<div class="img-box" v-loading:[title]="showLoading">
<img :src="src" alt="" />
</div>
</div>

<!-- <Loading v-if="true"></Loading> -->
</template>

06.png


很明显发现, 执行现在图片这个盒子上, 并没有水平居中, 审查元素其实也很明显, css样式中是根据子绝父相, 但是此时大盒子并没有提供相对定位, 自然就无法水平居中


那么如何修改呢? 其实只要给绑定指令的盒子添加position: relative;属性即可, 当然absolute或者fixed效果一样可以居中


问题已找到了, 那么在appendChild时判断当前是否存在relative | absolute | fixed 的其中一个, 如果没有就需要classList.add进行添加, 同时在removeChild删除添加的relative | absolute | fixed 即可


8. 完善坑点, 实现水平居中


getComputedStyle() 在MDN上的说明:


方法返回一个对象,该对象在应用活动样式表并解析这些值可能包含的任何基本计算后报告元素的所有 CSS 属性的值。私有的 CSS 属性值可以通过对象提供的 API 或通过简单地使用 CSS 属性名称进行索引来访问。


/* 将loading添加到指令所在DOM */
const relative = 'relative'
const handleAppend = (el: any) => {
const style = getComputedStyle(el)
if (!['absolute', 'relative', 'fixed'].includes(style.position)) {
addClass(el, relative as any)
}

el.appendChild(el.instance.$el)
}
// 添加relative
const addClass = (el: any, className: string) => {
if (!el.classList.contains(className)) {
el.classList.add(className)
}
}
// 删除relative
const removeClass = (el: any, className: string) => {
el.classList.remove(className)
}

结尾


夜深人静又是卷到凌晨1点, 只管努力, 其他交给天意~ 旋风冲锋手动撒花 ✿✿ヽ(°▽°)ノ✿


demo地址: gitee.com/tcwty123/v-…


作者:不知名小瑜
来源:juejin.cn/post/7281825352530296843
收起阅读 »

前端对接电子秤、扫码枪设备serialPort 串口使用教程

web
因为最近工作项目中用到了电子秤,需要对接电子秤设备。以前也没有对接过这种设备,当时也是一脸懵逼,脑袋空空。后来就去网上搜了一下前端怎么对接,然后就发现了SerialPort串口。 Serialport 官网地址:serialport.io/ Github:g...
继续阅读 »

因为最近工作项目中用到了电子秤,需要对接电子秤设备。以前也没有对接过这种设备,当时也是一脸懵逼,脑袋空空。后来就去网上搜了一下前端怎么对接,然后就发现了SerialPort串口。



Serialport


官网地址:serialport.io/


Github:github.com/serialport/…


官方描述:使用 JavaScript 访问串行端口。Linux、OSX 和 Windows。



SerialPort是什么?



SerialPort 是一个用于在 Node.js 环境中进行串口通信的库。它允许开发者通过 JavaScript 或 TypeScript 代码与计算机上的串口设备进行交互。SerialPort 库提供了丰富的 API,使得在串口通信中能够方便地进行设置、监听和发送数据。



一般我们的设备(电子秤/扫码枪)会有一根线插入到电脑的USB口或者其他口,电脑上的这些插口就是叫串口。设备上的数据会通过这根线传输到电脑里面,比如电子秤传到电脑里的就是重量数值。那么我们前端怎么接收解析到这些数据的呢?SerialPort的作用就是用来帮我们接收设备传输过来的数据,也可以向设备发送数据。


简单概括一下:SerialPort就是我们前端和设备之间的翻译官,可以接收设备传输过来的数据,也可以向设备发送数据。


SerialPort怎么用?


SerialPort可以在Node项目中使用,也可以在Electron项目中使用,我们一般都是用在Electron项目中,接下来讲一下在Electron项目中SerialPort怎么下载和引入


1、创建Electron项目


mkdir my-electron-app && cd my-electron-app
npm init -y
npm i --save-dev electron

网上有很多Electron教程,这里不再详细说了


在package.json中看一下自己的Electron的版本,下一步会用到


2、下载SerialPort


这里先看一下自己使用的Electron对应的Node版本是什么,打开下面electron官网看表格中的Node那一列


Electron发行时间表:http://www.electronjs.org/zh/docs/lat…


image-20240113215800193.png


如果你Electron对应的Node版本高于v12.0.0,直接下载就行


npm install serialport

如果你Electron对应的Node版本低于或等于v12.0.0,请用对应的Node版本对应下面的serialport版本下载



serialport.io/docs/next/g…




  • 对于 Node.js 版本0.100.12,最后一个正常运行的版本是serialport@4

  • 对于 Node.js 版本4.0,最后一个正常运行的版本是serialport@6.

  • 对于 Node.js 版本8.0,最后一个正常运行的版本是serialport@8.

  • 对于 Node.js 版本10.0,最后一个正常运行的版本是serialport@9.

  • 对于 Node.js 版本12.0,最后一个正常运行的版本是serialport@10.



我项目的Electron版本是11.5.0,对应的Node版本号是12.0,对应的serialport版本号是serialport@10.0.0



3、编译Serialport



  • 安装node-gyp 用于调用其他语言编写的程序(如果已安装过请忽略这一步)


    npm install -g node-gyp




  • 进入@serialport目录


    cd ./node_modules/@serialport/bindings


  • 进行编译,target后面换成当前Electron的版本号


    node-gyp rebuild --target=11.5.0



如果编译的时候报错了就将自己电脑的Node版本切换成当前Electron对应的版本号再编译一次


查看Electron对应Node版本号:http://www.electronjs.org/zh/docs/lat…


编译成功以后就可以在代码里使用Serialport了


4、使用Serialport



serialport官网使用教程:serialport.io/docs/next/g…



4.1、引入Serialport


const { SerialPort } = require('serialport')
// or
import { SerialPort } from 'serialport'

4.2、创建串口(重点!)


创建串口有两种写法,新版本是这样写法new SerialPort(params, callback)


const port = new SerialPort({
 path: 'COM1',  // 串口号
 baudRate: 9600, // 波特率
 autoOpen: true,  // 是否自动打开端口
}, function (err) {
 if (err) {
   return console.log('打开失败: ', err.message)
}
 console.log('打开成功')
})

旧版本是下面这样的写法new Serialport(path, params, callback),我用的是serialport@10.0.0版本就是这样的写法


const port = new Serialport('COM1', {
 baudRate: 9600,
 autoOpen: true,  // 是否自动打开端口
}, function (err) {
 if (err) {
   return console.log('打开失败: ', err.message)
}
 console.log('打开成功')
})

创建串口的时候需要传入两个重要的参数是path和baudRate,path是串口号,baudRate是波特率。最后一个参数是回调函数



不知道怎么查看串口号和波特率看这篇文章


如何查看串口号和波特率?



4.3、手动打开串口


如果autoOpen参数是false,需要使用port.open()方法手动打开


const port = new SerialPort({
 path: 'COM1',  // 串口号
 baudRate: 9600, // 波特率
 autoOpen: false,  // 是否自动打开端口, 默认true
})
// autoOpen参数是false,需要使用port.open()方法手动打开
port.open(function (err) {
 if (err) {
   return console.log('打开失败', err.message)
}
 console.log('打开成功')
})

4.4、接收数据(重点!)


接收到的data是一个Buffer,需要转换为字符串进行查看


port.on('data', function (data) {
 // 接收到的data是一个Buffer,需要转换为字符串进行查看
 console.log('Data:', data.toString('utf-8'))
})

接收过来的data就是设备传输过来的数据,转换后的字符串就是我们需要的数据,字符串里面可能有多个数据,我们把自己需要的数据截取出来就可以了


假设通过电子秤设备获取到的数据就是"205 000 000",中间是四个空格分割的,第一个数字205就是获取的重量,需要把这个重量截取出来。下面是我的示例代码


port.on('data', function (data) {
 try {
     // 获取的data是一个Buffer
     // 1.将 Buffer 转换为字符串 dataString.toString('utf-8')
     let weight = data.toString('utf-8')
     // 2.将字符串分割转换成数组,取数组的第一个值.split('   ')[0]
     weight = weight.split('   ')[0]
     // 3.将取的值 去掉前后空格
     weight = weight.trim()
     // 4.最后转换成数字,获取到的数字就是重量
     weight = Number(weight)
     console.log('获取到重量:'+ weight);
} catch (err) {
   console.error(`
     重量获取报错:${err}
     获取到的Buffer: ${data}
     Buffer转换后的值:${data.toString('utf-8')}
   `
);
}
})

4.5、写入数据


port.write('Hi Mom!')
port.write(Buffer.from('Hi Mom!'))

4.6、实时获取(监听)所有串口


const { SerialPort } = require('serialport')

SerialPort.list().then((ports, err) => {
   // 串口列表
   console.log('获取所有串口列表', ports);
})

更多内容


serialport官网教程:serialport.io/docs/next/g…


作者:Yaoqi
来源:juejin.cn/post/7323464381172301860
收起阅读 »

flex布局之美,以后就靠它来布局了

web
写在前面 在很久很久以前,网页布局基本上通过table 元素来实现。通过操作table 中单元格的align 和valign可以实现水平垂直居中等 再后来,由于CSS 不断完善,便演变出了:标准文档流、浮动布局和定位布局 3种布局 来实现水平垂直居中等各种布局...
继续阅读 »

写在前面


在很久很久以前,网页布局基本上通过table 元素来实现。通过操作table 中单元格的alignvalign可以实现水平垂直居中等


再后来,由于CSS 不断完善,便演变出了:标准文档流浮动布局定位布局 3种布局 来实现水平垂直居中等各种布局需求。


下面我们来看看实现如下效果,各种布局是怎么完成的


image-20240114134424060


实现这样的布局方式很多,为了方便演示效果,我们在html代码种添加一个父元素,一个子元素,css样式种添加一个公共样式来设置盒子大小,背景颜色


<div class="parent">
  <div class="child">我是子元素</div>
</div>

/* css公共样式代码 */
.parent{
   background-color: orange;
   width: 300px;
   height: 300px;
}
.child{
   background-color: lightcoral;
   width: 100px;
   height: 100px;
}

①absolute + 负margin 实现


/* 此处引用上面的公共代码 */

/* 定位代码 */
.parent {
   position: relative;
}
.child {
   position: absolute;;
   top: 50%;
   left: 50%;
   margin-left: -50px;
   margin-top: -50px;
}

②absolute + transform 实现


/* 此处引用上面的公共代码 */

/* 定位代码 */
.parent {
   position: relative;
}
.child {
   position: absolute;
   top: 50%;
   left: 50%;
   transform: translate(-50%, -50%);
}

③ flex实现


.parent {
   display: flex;
   justify-content: center;
   align-items: center;
}

通过上面三种实现来看,我们应该可以发现flex 布局是最简单了吧。


对于一个后端开发人员来说,flex布局算是最友好的了,因为它操作简单方便


一、flex 布局简介



flex 全称是flexible Box,意为弹性布局 ,用来为盒状模型提供布局,任何容器都可以指定为flex布局。


通过给父盒子添加flex属性即可开启弹性布局,来控制子盒子的位置和排列方式。


父容器可以统一设置子容器的排列方式,子容器也可以单独设置自身的排列方式,如果两者同时设置,以子容器的设置为准



flex布局


二、flex基本概念



flex的核心概念是 容器,容器包括外层的 父容器 和内层的 子容器,轴包括 主轴辅轴



<div class="parent">
   <div class="child">我是子元素</div>
</div>

2.1 轴



  • 在 flex 布局中,是分为主轴和侧轴两个方向,同样的叫法有 : 行和列、x 轴和y 轴,主轴和交叉轴

  • 默认主轴方向就是 x 轴方向,水平向右

  • 默认侧轴方向就是 y 轴方向,水平向下


    主轴和侧轴



注:主轴和侧轴是会变化的,就看 flex-direction 设置谁为主轴,剩下的就是侧轴。而我们的子元素是跟着主轴来排列的


--flex-direction 值--含义
row默认值,表示主轴从左到右
row-reverse表示主轴从右到左
column表示主轴从上到下
column-reverse表示主轴从下到上

2.2 容器



容器的属性可以作用于父容器(container)或者子容器(item)上



①父容器(container)-->属性添加在父容器上



  • flex-direction 设置主轴的方向

  • justify-content 设置主轴上的子元素排列方式

  • flex-wrap 设置是否换行

  • align-items 设置侧轴上的子元素排列方式(单行 )

  • align-content 设置侧轴上的子元素的排列方式(多行)


②子容器(item)-->属性添加在子容器上



  • flex 属性 定义子项目分配剩余空间,用flex来表示占多少份数

  • align-self控制子项自己在侧轴上的排列方式

  • order 属性定义项目的排列顺序


三、主轴侧轴设置


3.1 flex-direction: row



flex-direction: row 为默认属性,主轴沿着水平方向向右,元素从左向右排列。



row


3.2 flex-direction: row-reverse



主轴沿着水平方向向左,子元素从右向左排列



row-reverse


3.3 flex-direction: column



主轴垂直向下,元素从上向下排列



column


3.4 flex-direction: column-reverse



主轴垂直向下,元素从下向上排列



column-reverse


四、父容器常见属性设置


4.1 主轴上子元素排列方式


4.1.1 justify-content


justify-content 属性用于定义主轴上子元素排列方式


justify-content: flex-start|flex-end|center|space-between|space-around



flex-start:起始端对齐


flex-start


flex-end:末尾段对齐


flex-end


center:居中对齐


center


space-around:子容器沿主轴均匀分布,位于首尾两端的子容器到父容器的距离是子容器间距的一半。


space-around


space-between:子容器沿主轴均匀分布,位于首尾两端的子容器与父容器相切。


space-between


4.2 侧轴上子元素排列方式


4.2.1 align-items 单行子元素排列


这里我们就以默认的x轴作为主轴



align-items:flex-start:起始端对齐


flex-start


align-items:flex-end:末尾段对齐


flex-end


align-items:center:居中对齐


center


align-items:stretch 侧轴拉伸对齐



如果设置子元素大小后不生效



stretch


4.2.2 align-content 多行子元素排列


设置子项在侧轴上的排列方式 并且只能用于子项出现 换行 的情况(多行),在单行下是没有效果的


我们需要在父容器中添加 flex-wrap: wrap;


flex-wrap: wrap; 是啥意思了,具体会在下一小节中细说,就是当所有子容器的宽度超过父元素时,换行显示



align-content: flex-start 起始端对齐


 /* 父容器添加如下代码 */
display: flex;
align-content: flex-start;
flex-wrap: wrap;

align-content: flex-start


align-content: flex-end :末端对齐


/* 父容器添加如下代码 */
display: flex;
align-content: flex-end;
flex-wrap: wrap;

align-content: flex-end


align-content: center: 中间对齐


/* 父容器添加如下代码 */
display: flex;
align-content: center;
flex-wrap: wrap;

align-content: center


align-content: space-around: 子容器沿侧轴均匀分布,位于首尾两端的子容器到父容器的距离是子容器间距的一半


/* 父容器添加如下代码 */
display: flex;
align-content: space-around;
flex-wrap: wrap;

align-content: space-around


align-content: space-between:子容器沿侧轴均匀分布,位于首尾两端的子容器与父容器相切。


/* 父容器添加如下代码 */
display: flex;
align-content: space-between;
flex-wrap: wrap;

image-20240114171606954


align-content: stretch: 子容器高度平分父容器高度


/* 父容器添加如下代码 */
display: flex;
align-content: stretch;
flex-wrap: wrap;

align-content: stretch


4.3 设置是否换行



默认情况下,项目都排在一条线(又称”轴线”)上。flex-wrap属性定义,flex布局中默认是不换行的。



flex-wrap: nowrap :不换行


/* 父容器添加如下代码 */
display: flex;
flex-wrap: nowrap;

flex-wrap: nowrap


flex-wrap: wrap: 换行


/* 父容器添加如下代码 */
display: flex;
flex-wrap: wrap;

flex-wrap: wrap


4.4 align-content 和align-items区别



  • align-items 适用于单行情况下, 只有上对齐、下对齐、居中和 拉伸

  • align-content适应于换行(多行)的情况下(单行情况下无效), 可以设置 上对齐、下对齐、居中、拉伸以及平均分配剩余空间等属性值。

  • 总结就是单行找align-items 多行找 align-content


五、子容器常见属性设置



  • flex子项目占的份数

  • align-self控制子项自己在侧轴的排列方式

  • order属性定义子项的排列顺序(前后顺序)


5.1 flex 属性



flex 属性定义子项目分配剩余空间,用flex来表示占多少份数。



① 语法


.item {
   flex: <number>; /* 默认值 0 */
}

②将1号、3号子元素宽度设置成80px,其余空间分给2号子元素


flex:1


5.2 align-self 属性



align-self 属性允许单个项目有与其他项目不一样的对齐方式,可覆盖 align-items 属性。


默认值为 auto,表示继承父元素的 align-items 属性,如果没有父元素,则等同于 stretch。



align-self: flex-start 起始端对齐


/* 父容器添加如下代码 */
display: flex;
align-items: center;
/*第一个子元素*/
align-self: flex-start;

align-self: flex-start


align-self: flex-end 末尾段对齐


/* 父容器添加如下代码 */
display: flex;
align-items: center;
/*第一个子元素*/
align-self: flex-end;

align-self: flex-end


align-self: center 居中对齐


/* 父容器添加如下代码 */
display: flex;
align-items: flex-start;
/*第一个子元素*/
align-self: center;

align-self: center


align-self: stretch 拉伸对齐


/* 父容器添加如下代码 */
display: flex;
align-items: flex-start;
/*第一个子元素 未指定高度才生效*/
align-self: stretch;

align-self: stretch


5.3 order 属性



数值越小,排列越靠前,默认为0。



① 语法:


.item {
   order: <number>;
}

② 既然默认是0,那我们将第二个子容器order:-1,那第二个元素就跑到最前面了


/* 父容器添加如下代码 */
display: flex;
/*第二个子元素*/
order: -1;

order


六、小案例


最后我们用flex布局实现下面常见的商品列表布局


商品列表


<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>简单商品布局</title>
   <style>
       .goods{
           display: flex;
           justify-content: center;
      }
       p{
           text-align: center;
      }
       span{
           margin: 0;
           color: red;
           font-weight: bold;
      }
       .goods001{
           width: 230px;
           height: 322px;
           margin-left: 5px;
      }
       .goods002{
           width: 230px;
           height: 322px;
           margin-left: 5px;
      }
       .goods003{
           width: 230px;
           height: 322px;
           margin-left: 5px;
      }
       .goods004{
           width: 230px;
           height: 322px;
           margin-left: 5px;
      }

   
</style>
</head>
<body>

   <div class="goods">
       <div class="goods001">
           <img src="./imgs/goods001.jpg" >
           <p>松下(Panasonic)洗衣机滚筒</p>
           <span>¥3899.00</span>
       </div>
       <div class="goods002">
           <img src="./imgs/goods002.jpg" >
           <p>官方原装浴霸灯泡</p>
           <span>¥17.00</span>
       </div>
       <div class="goods003">
           <img src="./imgs/goods003.jpg" >
           <p>全自动变频滚筒超薄洗衣机</p>
           <span>¥1099.00</span>
       </div>
       <div class="goods004">
           <img src="./imgs/goods004.jpg" >
           <p>绿联 车载充电器</p>
           <span>¥28.90</span>
       </div>
   </div>
</body>
</html>

以上就是本期内容的全部,希望对你有所帮助。我们下期再见 (●'◡'●)


作者:xiezhr
来源:juejin.cn/post/7323539673346375719
收起阅读 »

面试官:你之前的工作发布过npm包吗?

web
背景🌟 我们公司平时在开发的时候,总是会需要开发一些组件库,去提供给组内其他人通用,这样大大提高了复用性,当然大厂会有自己的组件库,不过学无止境嘛,大家可以根据本文学会如何发布npm包!现在一起来吧~ 01、步骤一注册 打开npm官网,如果没有账号就注册账号...
继续阅读 »

背景🌟


我们公司平时在开发的时候,总是会需要开发一些组件库,去提供给组内其他人通用,这样大大提高了复用性,当然大厂会有自己的组件库,不过学无止境嘛,大家可以根据本文学会如何发布npm包!现在一起来吧~


01、步骤一注册



打开npm官网,如果没有账号就注册账号,如果有就登陆。



02、步骤二创建文件夹



按需求创建一个文件夹,本文以test为例。



03、步骤三初始化package.json文件



进入test文件夹里面,使用cmd打开命令行窗口,在命令行窗口里面输入npm init初始化package.json文件。也可以在Visual Studio Coode的终端里面使用npm init命令初始化。



04、步骤四初始化package.json文件的过程



创建package.json的步骤


01、package name: 设置包名,也就是下载时所使用的的命令,设置需谨慎。


02、version: 设置版本号,如果不设置那就默认版本号。


03、description: 包描述,就是对这个包的概括。


04、entry point: 设置入口文件,如果不设置会默认为index.js文件。


05、test command: 设置测试指令,默认值就是一句不能执行的话,可不设置。


06、git repository: 设置或创建git管理库。


07、keywords: 设置关键字,也可以不设置。


08、author: 设置作者名称,可不设置。


09、license: 备案号,可以不设置。


10、回车即可生成package.json文件,然后还有一行需要输入yes命令就推出窗口。


11、测试package.json文件是否创建成功的命令npm install -g。



05、步骤五创建index.js文件



test文件夹根目录下创建index.js文件,接着就是编写index.js文件了,此处不作详细叙述。



06、步骤六初始化package-lock.json文件



test根目录下使用npm link命令创建package-lock.json文件。



07、步骤七登录npm账号



使用npm login链接npm官网账号,此过程需要输入Username、Password和Email,需要提前准备好。连接成功会输出Logged in as [Username] on registry.npmjs.org/ 这句话,账号不同,输出会有不同。



08、步骤八发布包到npm服务器



执行npm publish命令发布包即可。



09、步骤九下载安装



下载安装使用包,此例的下载命令是npm install mj-calculation --save



10、步骤十更新包



更新包的命npm version patch,更新成功会输出版本号,版本号会自动加一,此更新只针对本地而言。



11、步骤十一发布包到npm服务器



更新包至npm服务器的命令npm publish,成功会输出版本,npm服务器的版本也会更新。



12、步骤十二删除指定版本



删除指定版本npm unpublish mj-calculation@1.0.2,成功会输出删除的版本号,对应服务器也会删除。



13、步骤十三删除包



撤销已发布的包npm unpublish mj-calculation使用的命令。



14、步骤十四强制删除包



强制撤销已发布的包npm unpublish mj-calculation --force使用的命令。



作者:泽南Zn
来源:juejin.cn/post/7287425222365364259
收起阅读 »

面试被问到一个css属性,我却只会向面试官输出js解决方案。。。

web
事情是这样的,好不容易约到个面试,虽然是线下,还是开心得屁颠屁颠跑去面了。刚开始都很正常,面试官首先问一些关于css的问题,我都能对答如流,觉得还好,突然面试官说,有这么个场景:如果我现在有个 canvas 的区域,区域下方(重叠那种上下,不是二维的上下)是一...
继续阅读 »

事情是这样的,好不容易约到个面试,虽然是线下,还是开心得屁颠屁颠跑去面了。刚开始都很正常,面试官首先问一些关于css的问题,我都能对答如流,觉得还好,突然面试官说,有这么个场景:如果我现在有个 canvas 的区域,区域下方(重叠那种上下,不是二维的上下)是一些操作按钮,但是按钮在该区域下方,不能显示出来,怎么能够点击到按钮呢?


听到问题我一开始没反应过来,不是还在疯狂问css吗,怎么突然跳跃到canvas了?于是我就回:不好意思,canvas我不是特别熟。。。,面试官就说不关canvas的事,这样吧,你就把canvas想象成一张普通的图片,怎么能点击到图片下方的按钮呢?


思索片刻,难道面试官在考察我的JS基础了?我就很自信的回答:首先把按钮所在标签放到图片所在标签之下,再把图片的层级(z-index)设置比按钮高一点,这样按钮就被图片挡着不会显示出来,再给按钮和图片所在标签都加上点击事件,此时就可以通过事件冒泡处理按钮的事件了。说完我就非常有把握的看向面试官,以为稳了。但是面试官好像不太满意,于是添加条件说,那如果按钮和图片所在标签不是父子节点关系呢,没有这层关系,你就不能使用事件冒泡了,此时怎么处理?答曰:不知道。。。


回来后赶紧查资料,原来一个css属性就搞定了:pointer-events: none; 这才是面试官想要的答案。


pointer-events是一个CSS属性,它定义了在何种情况下元素可以成为鼠标事件(或触摸事件)的目标。这个属性可以控制元素是否可以被点击、是否可以触发鼠标事件,或者是否应该忽略鼠标事件,让事件传递给下面的元素


使用场景


pointer-events属性主要用于以下几种场景:



  • : 元素不会成为鼠标事件的目标。例如,如果想让一个元素透明对用户的点击,可以将其pointer-events设置为none

  • Auto: 默认值。元素正常响应鼠标事件。

  • VisiblePainted: 元素仅在可见部分响应鼠标事件。

  • 其他值: 还有一些其他值用于SVG元素,如visibleFillvisibleStrokepainted, 等。


示例




以下例子和上述试题很像,把mask当做一张图片,为了方便展示,为其设置了透明度,这样能看到具体按钮位置和展示层级关系。正常情况下点击按钮是不会触发click事件的,因为mask的层级更高,完全遮住了按钮,鼠标只会点击到mask,但若此时为其加上pointer-events: none属性,点击事件会“穿透”该元素并可触发下面元素的事件,即按钮点击事件就可以被触发了!!!


<!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>
<style>
.outer{
position: relative;
width: 200px;
height: 200px;
}
.mask{
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
z-index: 1;
background: rgba(0, 0, 0, .7);
pointer-events: none; /* 重要 */
}
</style>
<body>
<div class="outer">
<div class="mask"></div>
<button id="btn">click</button>
</div>
<script>
const btn = document.getElementById('btn')
btn.addEventListener('click', function(e){
console.log('click');
})
</script>
</body>
</html>

image.png


作者:自驱
来源:juejin.cn/post/7320169906221826048
收起阅读 »

不要再滥用可选链运算符(?.)啦!

web
前言 之前整理过 整理下最近做的产品里 比较典型的代码规范问题,里面有一个关于可选链运算符(?.)的规范,当时只是提了一下,今天详细说下想法,欢迎大佬参与讨论。 可选链运算符(?.),大家都很熟悉了,直接看个例子: const result = obj?.a?...
继续阅读 »

前言


之前整理过 整理下最近做的产品里 比较典型的代码规范问题,里面有一个关于可选链运算符(?.)的规范,当时只是提了一下,今天详细说下想法,欢迎大佬参与讨论。


可选链运算符(?.),大家都很熟悉了,直接看个例子:


const result = obj?.a?.b?.c?.d

很简单例子,上面代码?前面的属性如果是空值(null或undefined),则result值是undefined,反之如果都不是空值,则会返回最后一个d属性值。


本文不是讲解这种语法的用法,主要是想分析下日常开发中,这种语法 滥用、乱用 的问题。


滥用、乱用


最近在code review一个公司项目代码,发现代码里用到的可选链运算符,很多滥用,用的很无脑,经常遇到这种代码:


const userName = data?.items?.[0]?.user?.name

↑ 不管对象以及属性有没有可能是空值,无脑加上?.就完了。


// react class component
const name = this.state?.name

// react hooks
const [items, setItems] = useState([])
items?.map(...)
setItems?.([]) // 真有这么写的

↑ React框架下,this.state 值不可能是空值,初始化以及set的值都是数组,都无脑加上?.


const item1 = obj?.item1
console.log(item1.name)

↑ 第一行代码说明obj或item1可能是空值,但第二行也明显说明不可能是空值,否则依然会抛错,第一行的?.也就没意义了。


if (obj?.item1?.item2) {
const item2 = obj?.item1?.item2
const name = obj?.item1?.item2?.name
}

↑ if 里已经判断了非空了,内部就没必要判断非空了。


问题、缺点


如果不考虑 ?. 使用的必要性,无脑滥用其实也没问题,不会影响功能,优点也很多:



  1. 不用考虑是不是非空,每个变量或属性后面加 ?. 就完了。

  2. 由于不用思考,开发效率高。

  3. 不会有空引用错误,不会有页面点点就没反应或弹错问题。


但是问题和缺点也很明显,而且也会很严重。分两点分析下:



  1. 可读性、维护性:给代码维护人员带来了很多分析代码的干扰,代码可读性和维护性都很差。

  2. 隐式过滤了异常:把异常给隐式过滤掉了,导致不能快速定位问题。

  3. 编译后代码冗余。

  4. 护眼:一串?.看着难受,特别是以一个code reviewer 角度看。


1. 可读性、维护性


可读性和维护性其实是一回事,都是指不是源代码作者的开发维护人员,在捋这块代码逻辑、修改bug等情况时,处理问题的效率,代码写的好处理就快,写的烂就处理慢,很简单道理。


const onClick = () => {
const user = props.data?.items?.[0]?.user
if (user) {
// use user to do something
}
}

已这行代码为例,有个bug现象是点击按钮没反应,维护开发看到这块代码,就会想这一串链式属性里,是不是有可能有空值,所以导致了user是空值,没走进if里导致没反应。然后就继续分析上层组件props传输代码,看data值从哪儿传来的,看是不是哪块代码导致data或items空值了。。。


其实呢?从外部传过来的这一串属性里不会有空值的情况,导致bug问题根本不在这儿。


const user = props.data.items[0].user

那把?.都去掉呢?维护开发追踪问题看到这行代码,data items 这些属性肯定不能是空值,不然console就抛错了,但是bug现象里并没有抛错,所以只需要检查user能不能是空值就行了,很容易就排除了很多情况。


总结就是:给代码维护人员带来了很多分析代码的干扰,代码可读性和维护性都很差。


2. 隐式过滤了异常


api.get(...).then(result => {
const id = result?.id
// use id to do something
})

比如有个需求,从后台api获取数据时,需要把结果里id属性获取到,然后进行数据处理,从业务流程上看,这个api返回的result以及id必须有值,如果没值的话后续的流程就会走不通。


然后后台逻辑由于写的有问题,导致个别情况返回的 result=null,但是由于前端这里加了?.,导致页面没有任何反应,js不抛错,console也没有log,后续流程出错了,这时候如果想找原因就会很困难,对代码熟悉还行,如果不是自己写的就只能看代码捋逻辑,如果是生产环境压缩混淆了就更难排查了。


api.get(...).then(result => {
const id = result.id
// use id to do something
})

?.去掉呢?如果api返回值有问题,这里会立即抛错,后面的流程也就不能进行下去了,无论开发还是生产环境都能在console里快速定位问题,即使是压缩混淆的也能从error看出一二,或者在一些前端监控程序里也能监听到。


其实这种现象跟 try catch 里不加 throw 类似,把隐式异常错误完全给过滤掉了,比如下面例子:


// 这个try本意是处理api请求异常
try {
const data = getSaveData() // 这段js逻辑也在try里,所以如果这个方法内部抛错了,页面上就没任何反应,很难追踪问题
const result = await api.post(url, data)
// result 逻辑处理
} catch (e) {
// 好点的给弹个框,打个log,甚至有的啥都不处理
}

总结就是:把异常给隐式过滤掉了,导致不能快速定位问题。


3. 编译后代码冗余


如果代码是ts,并且编译目标是ES2016,编译后代码会很长。可以看下 http://www.typescriptlang.org/play 效果。


image.png


Babel在个别stage下,编译效果一样。


image.png


但并不是说一点都不用,意思是尽量减少滥用,这样使用的频率会少很多,这种编译代码沉余也会少不少。


应该怎么用?


说了这么多,.? 应该怎么用呢?意思是不用吗?当然不是不能用,这个特性对于开发肯定好处很多的,但是得合理用,不能滥用。



  1. 避免盲目用,滥用,有个点儿就加问号,特别是在一个比较长的链式代码里每个属性后面都加。

  2. 只有可能是空值,而且业务逻辑中有空值的情况,就用;其它情况尽量不要用。


其实说白了就是:什么时候需要判断一个变量或属性非空,什么时候不需要。首先在使用的时候得想下,问号前面的变量或属性值,有没有可能是空值:



  1. 很明显不可能是空值,比如 React类组件里的 this.state this.props,不要用;

  2. 自己定义的变量或属性,而且没有赋值为空值情况,不要用;

  3. 某些方法或者组件里,参数和属性不允许是空值,那方法和组件里就不需要判断非空。(对于比较common的,推荐写断言,或者判断空值情况throw error)

  4. 后台api请求结果里,要求result或其内部属性必须有值,那这些值就不需要判断非空。

  5. 按正常流程走,某个数据不会有空值情况,如果是空值说明前面的流程出问题了,这种情况就不需要在逻辑里判断非空。


const userName = data?.items?.[0]?.user?.name // 不要滥用,如果某个属性有可能是空值,则需要?.
const userName = data.items[0].user?.name // 比如data.items数组肯定不是空数组

const items2 = items1.filter(item => item.checked)
if (items2?.length) { } // 不需要?.

// react class component
const name = this.state?.name // 不需要?.

// react hooks
const [items, setItems] = useState([])
items?.map(...) // 如果setItems没有赋值空值情况,则不需要?.
setItems?.([]) // 不需要?.

const item1 = obj?.item1 // 不需要?.
console.log(item1.name)

const id = obj?.id // 下面代码已经说明不能是空值了,不需要?.
const name = obj.name

if (obj?.item1?.item2) {
const item2 = obj?.item1?.item2 // 不需要?.
const name = obj?.item1?.item2?.name // 不需要?.
}

const id = obj?.item?.id // 不需要?.
api.get(id).then(...) // 这个api如果id是空值,则api会抛错

当然,写代码时还得多想一下属性是否可能是空值,会一定程度的影响开发效率,也一定有开发会觉得很烦,不理解,无脑写?.多容易啊,但是我从另外两个角度分析下:



  1. 我觉得一个合格的开发应该对自己的代码逻辑很熟悉,应该有责任知道哪些值可能是空值,哪些不可能是空值(并不是说所有,也有大部分了),否则就是对自己的代码了解很少,觉得代码能跑就行,代码质量自然就低。

  2. 想想在这个新特性出来之前大家是怎么写的,会对每个变量和属性都加if非空判断或者用逻辑与(&&)吗?不会吧。


总结


本文以一个 code reviewer 角度,分析了 可选链运算符(?.) 特性的滥用情况,以及“正确使用方式”,只是代表我本人的看法,欢迎大佬参与讨论,无条件接受任何反驳。


滥用的缺点:



  1. 可读性、维护性:给代码维护人员带来了很多分析代码的干扰,代码可读性和维护性都很差。

  2. 隐式过滤了异常:把异常给隐式过滤掉了,导致不能快速定位问题。

  3. 编译后代码冗余。

  4. 护眼:一串?.看着难受,特别是以一个code reviewer 角度看。


“正确用法”:



  1. 避免盲目用,滥用,有个点儿就加问号,特别是在一个比较长的链式代码里每个属性后面都加。

  2. 只有可能是空值,而且业务逻辑中有空值的情况,就用;其它情况尽量不要用。




后记(09月25日更新)


从评论上看,对于可选链的看法,大多声音是能加就加,多加总比少加好,原因就是不想背锅,不想上线后JS动不动就崩了,无论根本原因是不是前端开发没加判断导致的,第一责任人就会找到你,有的甚至会被上级追责,问题就更严重了,而且很难解释清楚;另一方面就是为了赶工期,可选链的其中一个优点就是简单,提高开发效率。


我再从几个方面浅浅的扩展下我的看法,欢迎参与讨论


总之。。。对对对,你们说的都对!


作者:Mark大熊
来源:juejin.cn/post/7280747572707999799
收起阅读 »

面试官:能不能给 Promise 增加取消功能和进度通知功能... 我:???

web
扯皮 这段时间闲着没事就去翻翻红宝书,已经看到 Promise 篇了,今天又让我翻到两个陌生的知识点。 因为 Promise 业务场景太多了自我感觉掌握的也比较透彻,之前也跟着 Promise A+ 的规范手写过完整的 Promise,所以这部分内容基本上就大...
继续阅读 »

扯皮


这段时间闲着没事就去翻翻红宝书,已经看到 Promise 篇了,今天又让我翻到两个陌生的知识点。


因为 Promise 业务场景太多了自我感觉掌握的也比较透彻,之前也跟着 Promise A+ 的规范手写过完整的 Promise,所以这部分内容基本上就大致过一遍,直到看见关于 Promise 的取消以及监听进度...🤔


只能说以后要是我当上面试官一定让候选人来谈谈这两个点,然后顺势安利我这篇文章🤣


不过好像目前为止也没见哪个面试官出过...


正文


取消功能


我们都知道 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
收起阅读 »

用脚本来写函数式弹窗,更快更爽

web
前言 在业务开发中,弹窗是我们常常能够遇到的开发需求,通常我们使用组件都是通过show变量来控制弹窗的开启或者隐藏。这样面对简单的业务需求的确可以满足了,但是面对深层嵌套,不同地方打开同一个弹窗便会让我们头疼。于是我想通过函数的方式来打开弹窗,通过函数参数的方...
继续阅读 »

前言


在业务开发中,弹窗是我们常常能够遇到的开发需求,通常我们使用组件都是通过show变量来控制弹窗的开启或者隐藏。这样面对简单的业务需求的确可以满足了,但是面对深层嵌套,不同地方打开同一个弹窗便会让我们头疼。于是我想通过函数的方式来打开弹窗,通过函数参数的方式来向弹窗内更新props和处理弹窗的emit事件。


iShot_2023-08-15_10.13.24.gif


<template>
<n-button @click="open">open</n-button>
</template>

<script setup lang="ts">
import { injectTestModal } from "./test-modal/use-test-modal"
const model = injectTestModal()
function open() {
model?.open({
testValue: "传给弹窗组件的值",
onConfirm({ formData }, close, endLoading) {
console.log({ ...formData })
endLoading()
close()
},
})
}
</script>


传统vue编写弹窗


通过变量来直接控制弹窗的开启和关闭。


<template>
<n-button @click="showModal = true">
来吧
</n-button>

<n-modal v-model:show="showModal" preset="dialog" title="Dialog">
<template #header>
<div>标题</div>
</template>
<div>内容</div>
<template #action>
<div>操作</div>
</template>
</n-modal>

</template>

<script lang="ts">
import { defineComponent, ref } from 'vue'

export default defineComponent({
setup () {
return {
showModal: ref(false)
}
}
})
</script>


痛点



  • 深层次的传props让人有很大的心理负担,污染组件props

  • 要关注弹窗show变量的true,false


函数式弹窗


在主页面用Provider包裹一下


// RootPage.vue
<ModalProvider>
<ChildPage></ChildPage>
</ModalProvider>

<script setup lang="ts">
import ModalProvider from "./ModalProvider.vue"
import ChildPage from "./ChildPage.vue"
</script>


在页面内的某个子组件中,直接通过oepn方法打开弹窗


// ChidPage.vue
<template>
<n-button @click="open">open</n-button>
</template>

<script setup lang="ts">
import { injectTestModal } from "./test-modal/use-test-modal"
const model = injectTestModal()
function open() {
model?.open({
testValue: "传给弹窗组件的值",
onConfirm({ formData }, close, endLoading) {
console.log({ ...formData })
endLoading()
close()
},
})
}
</script>


优势



  • 对于使用者来说简单,没有控制show的心理负担

  • 弹窗内容和其他业务代码分离,不会污染其他组件props和结构

  • 能够充分复用同一个弹窗,通过函数参数的方式来更新弹窗内容,在不同地方打开同一个弹窗


劣势



  • 对于一些简单需求的弹窗,用这种函数式的弹窗会有些臃肿

  • 在使用函数式弹窗前需要做一些操作(Provider和Inject),会在后面提到以及解决方案。


如何使用这种函数式的弹窗


原理


通过在根页面将我们的Modal组件挂载上去,然后通过使用hook去管理这个modal组件的状态值(props,ref),然后通过provide将props和event和我们的modal组件联系起来,再在我们需要使用弹窗的地方使用inject更新props来启动弹窗。


步骤1(❌):编写Modal


这里我使用的是Naive Ui的Modal组件,按喜好选择就行。按照你的需求编写弹窗的内容。定义好props和emits,写好他们的类型申明。


// TestModal.vue
<template>
<n-modal
v-model:show="isShowModal"
preset="dialog"
@after-leave="handleClose"
>

...你的弹窗内容
</n-modal>

</template>

<script setup lang="ts">
import { ref, reactive, computed, watch } from "vue"
import { useVModel } from "@vueuse/core"
import { FormRules } from "naive-ui"
interface ModalProps {
show: boolean
testValue: string
}

// 中间区域不要修改
const props = defineProps<ModalProps>()
const formRef = ref()
const loading = ref(false)
const isShowModal = useVModel(props, "show")
// 中间区域不要修改

const rules: FormRules = []

const formData = reactive({
testValue: props.testValue,
})

const callBackData = computed(() => {
return {
formData,
}
})

watch(
() => props.show,
() => {
if (props.show) {
formData.testValue = props.testValue
} else {
formData.testValue = ""
}
}
)

const emits = defineEmits<{
(e: "update:show", value: boolean): void
(e: "close", param: typeof callBackData.value): void
(
e: "confirm",
param: typeof callBackData.value,
close: () => void,
endLoading: () => void
): void
(e: "cancel", param: typeof callBackData.value): void
}>()
function handleCancel() {
// 中间区域不要修改
emits("cancel", callBackData.value)
isShowModal.value = false
// 中间区域不要修改
}

function handleClose() {
// 中间区域不要修改
emits("close", callBackData.value)
isShowModal.value = false
// 中间区域不要修改
}

function handleConfirm() {
// 中间区域不要修改
loading.value = true
emits(
"confirm",
callBackData.value,
() => {
loading.value = false
isShowModal.value = false
},
() => {
loading.value = false
}
)
// 中间区域不要修改
}
</script>

步骤2(❌):编写hook来管理弹窗的状态


在这个文件里面,使用hook管理 TestModal 弹窗需要的props和event事件,然后我们通过ts类型体操来获取TestModal的props类型和event类型,我们向外inject一个 open 函数,这个函数可以更新 TestModal 的props和event,同时打开弹窗,他的参数有完整的类型提示,可以让使用者更加明确的使用我们的弹窗。


// use-test-modal.ts
import {
ref,
provide,
InjectionKey,
inject,
VNodeProps,
AllowedComponentProps,
reactive,
} from "vue";
import Modal from "./TestModal.vue";

/**
* 通过引入弹窗组件来获取组件的除show,update:show以外的props和emits来作为open函数的
*/

type ModalInstance = InstanceType<
typeof Modal extends abstract new (...args: any) => any ? typeof Modal : any
>["$props"];
type OpenParam = Omit<
{
readonly [K in keyof Omit<
ModalInstance,
keyof VNodeProps | keyof AllowedComponentProps
>]: ModalInstance[K];
},
"show" | "onUpdate:show"
>;

interface AnyFileChangeModal {
open: (param?: OpenParam) => Promise<void>;
}

/**
* 通过弹窗实例来获取弹窗组件内需要哪些props
*/

type AllProps = Omit<
OpenParam,
"onClose" | "onCancel" | "onConfirm" | "onUpdate:show"
> & { show: boolean };
const anyModalKey: InjectionKey<AnyFileChangeModal> = Symbol("ModalKey");
export function provideTestModal() {
const allProps: AllProps = reactive({
show: false,
} as AllProps);
const closeCallback = ref();
const cancelCallback = ref();
const confirmCallback = ref();
const handleUpdateShow = (value: boolean) => {
allProps.show = value;
};

/**
* @param param 通过函数来更新props
*/

function updateAllProps(param: OpenParam) {
const excludeKey = ["show", "onClose", "onConfirm", "onCancel"];
for (const [key, value] of Object.entries(param)) {
if (!excludeKey.includes(key)) {
allProps[key] = value;
}
}
}
function clearAllProps() {
for (const [key] of Object.entries(allProps)) {
allProps[key] = undefined;
}
}

async function open(param: OpenParam) {
clearAllProps();
updateAllProps(param);
allProps.show = true;
param.onClose && (closeCallback.value = param.onClose);
param.onConfirm && (confirmCallback.value = param.onConfirm);
param.onCancel && (cancelCallback.value = param.onCancel);
}
provide(anyModalKey, { open });
return {
allProps,
closeCallback,
confirmCallback,
cancelCallback,
handleUpdateShow,
};
}

export function injectTestModal() {
return inject(anyModalKey)
}
Ï

步骤3(❌):提供Provider


在这个文件里,我将TestModal放在了根页面,然后将hook返回的props和event绑定给TestModal


// ModalProvider.vue
<template>
<slot />

<TestModal
v-bind="allTestModalProps"
@update:show="handleTestModalUpdateShow"
@close="closeTestModalCallback"
@confirm="confirmTestModalCallback"
@cancel="cancelTestModalCallback"
/>

<!-- 新增Modal -->
</template>

<script setup lang="ts">
import TestModal from "./test-modal/TestModal.vue";
import { provideTestModal } from "./test-modal/use-test-modal";
/** 新增import */

const {
allProps: allTestModalProps,
handleUpdateShow: handleTestModalUpdateShow,
closeCallback: closeTestModalCallback,
confirmCallback: confirmTestModalCallback,
cancelCallback: cancelTestModalCallback,
} = provideTestModal();
/** 新增provide */
</script>


步骤4(❌):通过函数打开弹窗


<template>
<n-button @click="open">open</n-button>
</template>

<script setup lang="ts">
import { injectTestModal } from "./test-modal/use-test-modal"
const model = injectTestModal()
function open() {
model?.open({
testValue: "传给弹窗组件的值",
onConfirm({ formData }, close, endLoading) {
console.log({ ...formData })
endLoading()
close()
},
})
}
</script>


到这里也就结束了。如果这样写一个弹窗,确实可以达到函数式的打开弹窗,但是他实在是太繁琐了,要写这么一堆东西,不如直接修改弹窗的show来的快。如果看过上面的use-test-modal.ts的话,会发现里面有AllProps,这是为了减少方便使用脚本工具来写通用化的代码,可以大大减少人工的重复代码编写,接下来我们使用一个工具来减少这些繁琐的操作。


步骤1(✅):初始化Provider


通过使用工具生成根页面ModalProvder组件。
具体步骤:在你想放置当前业务版本的弹窗文件夹路径下使用终端执行脚本,选择initProvider


iShot_2023-08-15_15.16.14.gif


步骤2(✅):生成弹窗模板


通过继续使用脚本工具,生成弹窗组件以及hook文件。
具体步骤:在你想放该弹窗的文件夹路径下使用终端,使用脚本工具,选择genModal,然后跟着指令操作就行。比如例子中,我们生产名为test的弹窗,然后告诉脚本我们的ModalProvider组件的绝对路径,脚本帮我生产test-modal的文件夹,里面放着testModal.vueuse-test-modal.ts,这里的use-test-modal.ts文件已经是成品了,不需要你去修改,ModalProvider.vue也不需要你去修改,里面的路径关系也帮你处理好了。


iShot_2023-08-15_15.17.33.gif


步骤3(✅):修改弹窗内容


上一步操作中,脚本帮我写好了TestModal.vue组件,我们可以在他的基础上完善我们的业务需求。


步骤4(✅):调用弹窗


我们找到生成的use-test-modal.ts,里面有一个injectXXXModal的方法,我们在哪个地方用到弹窗就引用执行他,返回的对象中有open方法,通过open方法去开启弹窗并且更新弹窗props。


Demo


预览
Demo地址
里面有完整的demo代码


iShot_2023-08-15_18.32.39.gif


脚本工具


仓库地址
这个是我写的脚本工具,用来减少一些重复代码的工作。用法可看readme


总结


本文先通过比较函数式的弹窗和直接传统编写弹窗的优劣,然后引出本文的主旨(如何编写函数式弹窗),再考虑我这一版函数式弹窗的问题,最后通过脚本工具来解决这一版函数式弹窗的劣势,完整的配合脚本工具,可以极大的加快我们的工作效率。
大家不要看这么写要好几个文件夹,实际使用的时候其实就是脚本生成代码后,我们只需要去修改弹窗主题的内容。有什么疑惑或者建议评论区聊。


作者:恐怖屋
来源:juejin.cn/post/7267418473401057321
收起阅读 »

当别人因为React、Vue吵起来时,我们应该做什么

web
大家好,我卡颂。 最近尤大的一个推文引起了不小热议,大概经过是: 有人在推上夸React文档写的好,把可能的坑点都列出来 尤看到后批评道:框架应该自己处理这些坑点,而不是把他们暴露给用户 尤大在推上的发言一直比较耿直,这次又涉及到React这个老对手,关...
继续阅读 »

大家好,我卡颂。


最近尤大的一个推文引起了不小热议,大概经过是:



  1. 有人在推上夸React文档写的好,把可能的坑点都列出来

  2. 尤看到后批评道:框架应该自己处理这些坑点,而不是把他们暴露给用户



尤大在推上的发言一直比较耿直,这次又涉及到React这个老对手,关注度自然不低。


再加上国内前端自媒体的一波引导发酵,比如知乎下这个话题相关的问题中的措辞是怒喷,懂得都懂。



在这样氛围与二手信源的影响下,会给人一种大佬都亲手下场撕了的感觉,自然会引来ReactVue各自拥趸的一番激烈讨论。


年年都是一样的套路,毫无新意......


面对这样的争吵,我们应该做什么呢?


首先,回到源头本身,尤大diss的有道理么?有。


React的心智负担重么?确实重。比如useEffec这个API,你能想象文档中一个章节居然有6篇文章是教你如何正确使用useEffec的么?



造成这一现象的原因有很多,比如:



  1. Hooks的实现原理使得必须显式声明依赖

  2. 显式声明依赖无法覆盖useEffect所有场景,为此专门提出一个叫Effect Event的概念,以及一个对应的新hook —— useEffectEvent

  3. useEffect承载了太多功能,比如未来Offscreen的显隐回调(类似Vue中的Keep-Alive)也是通过useEffect实现


当我们继续往前回溯,Hooks必须显式声明依赖React更新机制决定的,而React更新机制又是React实现原理的核心。


本质来说,还是React既往的成功、庞大的社区生态让他积重难返,无法从底层重写。


这是历史必然的进程,如果Vue所有新特性都在Vue2基础上迭代(而不是完全重写的Vue3),我相信也是同样的局面。


所以,当前React的迭代方向是 —— 支持上层框架(比如Next.jsRemix),寄希望于靠这些框架的封装能力弥补React自身心智负担重的缺点。这个策略显然也是成功的。


回到这次争吵本身,尤大不知道React文档为什么要花大篇幅帮开发者避坑(以及背后反映的积重难返)么?他显然是知道的。


他如此回复是因为他所处的位置是框架作者React是他的竞争对手。设想一下,如果你的竞争对手在一些方面确实不如你,但他的用户对此的反应不是“太难用了,我要换个好用的”,而是“一定是我用的姿势不对,你快出个文档好好教教我”


面对这样的用户,换谁都得有一肚子牢骚吧~



让我们再把视角转到React的用户(也就是我们这些普通开发者)上。我们为什么选择React呢?


可能有些人是处于喜好。但大部分开发者之所以用React,完全是因为公司要求用React


React的公司多,招React的岗位多,自然选择React的开发者就多了。


那么为什么用React的公司多呢?这显然是多年前React在先发优势、社区生态两场战役取胜后得到的结果。


总结


所以,我们需要尊重两个事实:



  1. React心智负担重是事实

  2. React的公司多也是事实


两者并不矛盾,他们都是历史进程的产物。


VueReact之间的讨论,即使是从技术层面出发,最后也容易陷入“React心智负担这么重,你们还甘之如饴,你们React党是不是傻”这样的争吵中。


这显然就是忽略了历史的进程。


正确的应对方式是多关心关心自己未来的发展:



  • 如果我的重心在海外,那应该给Next.js更多关注。海外远程团队不是Next就是Nest

  • 如果我的重心在国内,国内流量都被小程序分割了。一个长远的增长点应该是鸿蒙


作者:魔术师卡颂
来源:juejin.cn/post/7321589055883427855
收起阅读 »

面试官:手写一个“发布-订阅模式”

web
发布-订阅模式又被称为观察者模式,它是定义在对象之间一对多的关系中,当一个对象发生变化,其他依赖于它的对象收到通知。在javascript的开发中,我们一般用事件模型替代发布-订阅模式。 DOM事件 document.body.addEventListener...
继续阅读 »

发布-订阅模式又被称为观察者模式,它是定义在对象之间一对多的关系中,当一个对象发生变化,其他依赖于它的对象收到通知。在javascript的开发中,我们一般用事件模型替代发布-订阅模式。


DOM事件


document.body.addEventListener('click',function(){

alert(绑定1);

},false);

document.body.click(); //模拟点击

document.body.addEventListener('click',function(){

alert(绑定2);

},false);

document.body.addEventListener('click',function(){

alert(绑定3);

},false);

document.body.click(); //模拟点击

我们可以增加更多订阅者,不会对发布者的代码造成影响。注意,标准浏览器下用dispatchEvent实现。


自定义事件


① 确定发布者。(例如售票处)


② 添加缓存列表,便于通知订阅者。(预订车票列表)


③ 发布消息。遍历缓存列表。依次触发里面存放的订阅者回调函数(遍历列表,逐个发送短信)。


另外,我们还可以在回调函数填入一些参数,例如车票的价格之类信息。


let ticketOffice = {};    //售票处

ticketOffice.clientList = []; //缓存列表,存放订阅者的回调函数

ticketOffice.on = function (fn) { //增加订阅者
this.clientList.push(fn); //订阅的消息添加进缓存列表
};

ticketOffice.emit = function () { //发布消息
for(var i = 0, fn; fn = this.clientList[i++];){
fn.apply(this, arguments); //arguments 是发布消息时带上的参数
}
}

// 下面进行简单测试:

ticketOffice.on(function(time, path){ //小刚订阅消息
console.log('时间:' + time);
console.log('路线:' + path);
});


ticketOffice.on(function(time, path){ //小强订阅消息
console.log('时间:' + time);
console.log('路线:' + path);
});

ticketOffice.emit('晚上8:00','深圳-上海');
ticketOffice.emit('晚上8:10','上海-深圳');

至此,我们实现了一个最简单发布-订阅模式。不过这里存在一些问题,我们运行代码可以看到订阅者接收到了所有发布的消息。


// 时间:晚上8:00
// 路线:深圳-上海
// 时间:晚上8:00
// 路线:深圳-上海
// 时间:晚上8:10
// 路线:上海-深圳
// 时间:晚上8:10
// 路线:上海-深圳

我们有必要加个key让订阅者只订阅 自己感兴趣的消息。改写后的代码如下:


let ticketOffice = {};    //售票处

ticketOffice.clientList = []; //缓存列表,存放订阅者的回调函数

ticketOffice.on = function (key, fn) { //增加订阅者
if (!this.clientList[key]){
this.clientList[key] = [];
}
this.clientList[key].push(fn); //订阅的消息添加进缓存列表
};

ticketOffice.emit = function () { //发布消息
let key = Array.prototype.shift.call(arguments), //取出消息类型
fns = this.clientList[key]; //取出该消息对应的回调函数集合
if (!fns || fns.length === 0) {
return false;
}

fns.forEach(fn => {
fn.apply(this, arguments) //arguments 是发布消息时带上的参数
})
}

// --------- 测试数据 --------------
ticketOffice.on('上海-深圳', function(time){ //小刚订阅消息
console.log('小刚时间:' + time);
});

ticketOffice.on('深圳-上海', function(time){ //小强订阅消息
console.log('小强时间:' + time);
});

ticketOffice.emit('深圳-上海', '晚上8:00');
ticketOffice.emit('上海-深圳', '晚上8:10');

// 小强时间:晚上8:00
// 小刚时间:晚上8:10

这样子,订阅者就可以只订阅自己感兴趣的事件了。


小强临时行程有变,不想订阅对应的消息了,我们还需要再新增一个移除订阅的方法


let ticketOffice = {};    //售票处

ticketOffice.clientList = []; //缓存列表,存放订阅者的回调函数

ticketOffice.on = function (key, fn) { //增加订阅者
if (!this.clientList[key]){
this.clientList[key] = [];
}
this.clientList[key].push(fn); //订阅的消息添加进缓存列表
};

ticketOffice.emit = function () { //发布消息
let key = Array.prototype.shift.call(arguments), //取出消息类型
fns = this.clientList[key]; //取出该消息对应的回调函数集合
if (!fns || fns.length === 0) {
return false;
}

fns.forEach(fn => {
fn.apply(this, arguments) //arguments 是发布消息时带上的参数
})
}

ticketOffice.remove = function (key, fn) {
let fns = this.clientList[key]
if (!fns) return false

if (!fn) {
fns && (fns.length = 0)
} else {
fns.forEach((cb, i) => {
if (cb === fn) {
fns.splice(i, 1)
}
})
}
}

// --------- 测试数据 --------------
const xiaoGangOn = function(time){ //小刚订阅消息
console.log('小刚时间:' + time);
}
const xiaoQiangOn = function(time){ //小强订阅消息
console.log('小强时间:' + time);
}

ticketOffice.on('上海-深圳', xiaoGangOn);
ticketOffice.on('深圳-上海', xiaoQiangOn);

ticketOffice.remove('深圳-上海', xiaoQiangOn);

ticketOffice.emit('深圳-上海', '晚上8:00');
ticketOffice.emit('上海-深圳', '晚上8:10');

// 小刚时间:晚上8:10

至此,我们实现了一个相对完善的发布-订阅模式


但是可以看到使用数组进行时间的push和remove可能绑定相同的事件,且事件remove的效率低,我们可以用Set来替换Array


let ticketOffice = {};    //售票处

ticketOffice.clientList = []; //缓存列表,存放订阅者的回调函数

ticketOffice.on = function (key, fn) { //增加订阅者
if (!this.clientList[key]){
this.clientList[key] = new Set();
}
this.clientList[key].add(fn); //订阅的消息添加进缓存列表
};

ticketOffice.emit = function () { //发布消息
let key = Array.prototype.shift.call(arguments), //取出消息类型
fns = this.clientList[key]; //取出该消息对应的回调函数集合
if (!fns || fns.length === 0) {
return false;
}

fns.forEach(fn => {
fn.apply(this, arguments) //arguments 是发布消息时带上的参数
})
}

// 移除路线的单个订阅
ticketOffice.remove = function (key, fn) {
this.clientList[key]?.delete(fn)
}
// 移除路线的所有订阅
ticketOffice.removeAll = function (key) {
delete this.clientList[key]
}

// --------- 测试数据 --------------
const xiaoGangOn = function(time){ //小刚订阅消息
console.log('小刚时间:' + time);
}
const xiaoQiangOn = function(time){ //小强订阅消息
console.log('小强时间:' + time);
}

ticketOffice.on('上海-深圳', xiaoGangOn);
ticketOffice.on('深圳-上海', xiaoQiangOn);

ticketOffice.remove('深圳-上海', xiaoQiangOn);

ticketOffice.emit('深圳-上海', '晚上8:00');
ticketOffice.emit('上海-深圳', '晚上8:10');

// 小刚时间:晚上8:10

参考资料


《JavaScript 设计模式与开发实践》


作者:dudulala
来源:juejin.cn/post/7320075000702533671
收起阅读 »