注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

前端枚举最佳规范——优雅可能也会过时

web
痛点很久很久以前,我在ts项目中使用枚举是这样的export enum GENDER {      MALE = 1,      FEMALE = 2,  }export const GEN...
继续阅读 »

痛点

很久很久以前,我在ts项目中使用枚举是这样的

export enum GENDER {  
   MALE = 1,  
   FEMALE = 2,  
}

export const GENDER_MAP: Record<GENDER, string> = {  
  [GENDER.MALE]: '男',  
  [GENDER.FEMALE]: '女',  
}

// 可能还会写一个给select组件使用的options
export const GENDER_OPTIONS = [  
  {  
       label: '男',  
       value: GENDER.MALE  
  },  
  {  
       label: '女',  
       value: GENDER.FEMALE,  
  }  
];

封装

淦!好麻烦。封装一下,暂时只封装了一个js版本的,想做成ts版本的请自行更改

import { invert, isArray } from 'lodash';

class Enum {
 constructor(enumsName) {
   this.enumsName = enumsName;
   this.enums = {};
}
 // 设置枚举项
 setItem(desc, value) {
   this.enums[desc] = value;
   return this;
}
 findEnumItem(value) {
   return Object.keys(this.enums).find(
    (desc) => this.getValueFromDesc(desc) === value,
  );
}
 // 根据值获取描述
 getValueFromDesc(desc) {
   return this.enums[desc];
}
 getDescriptionFromValue(value, separator = ',') {
   if (isArray(value)) {
     return value.map((item) => this.findEnumItem(item)).join(separator);
  } else {
     return this.findEnumItem(value);
  }
}
 // 获取枚举第一项的值
 getFirstValue() {
   const enums = this.getEnums();
   return enums[Object.keys(enums)[0]];
}
 // 自定义转换枚举数组格式
 transformEnums(formatTarget) {
   // formatTarget 数组第一项是描述属性名称,第二项是枚举值属性名称
   if (isArray(formatTarget) && formatTarget.length === 2) {
     const [keyName, valueName] = formatTarget;
     return Object.entries(this.enums).map((item) => {
       const [desc, value] = item;
       return {
        [keyName]: desc,
        [valueName]: value,
      };
    });
  } else {
     return [];
  }
}
 // 获取 options
 getOptions() {
   return Object.entries(this.enums).map((item) => {
     const [desc, value] = item;
     return {
       label: desc,
       value,
    };
  });
}
 // 获取描述和值反转对象,输出 {value: desc}
 getInvertEnums() {
   return invert(this.enums);
}
 is(enumsName) {
   return this.enumsName === enumsName;
}
}
export default Enum;

export const getEnums = (enumsName, enums) =>
 enums.find((enumItem) => enumItem.is(enumsName));

使用

ok,咋使用呢?首先当然要创建枚举啦

// commonEnum.js
import Enum from '@/utils/enum';

// 我习惯把同一个模块或功能中的枚举全都塞到一个数组中
export const COMMON_ENUMS = [
   new Enum('性别').setItem('男', 1).setItem('女', 2),
   ...
];

在使用到的地方,先获取你想要的枚举

import {getEnums} from '@/utils/enum';
import {COMMON_ENUMS} from '@/enum/commonEnum';

const genderEnums = getEnums('性别', COMMON_ENUMS);

接下来分一些场景来举例一些使用方法:

  1. 作某个字段的值映射

{
   title: '性别',  
   dataIndex: 'gender',  
   render: (gender) => genderEnums.getDescriptionFromValue(gender),
}
  1. select组件中需要传入选项

<Select  
   name="gender"  
   label="性别"
   options={genderEnums.getOptions}
/>

其余方法就留给大家自己探索吧,我觉得这个封装已经可以涵盖大部分的场景了,你觉得呢?

作者:AliPaPa
来源:juejin.cn/post/7221820151397335077

收起阅读 »

前端实现点击选词功能

web
今天有一个需求,点击选中某个英文单词,然后对这个单词做一些处理,例如高亮背景、查看一些详细信息等等,今天简单实现了一下,效果如下:(支持移动端,chrome和sarafi浏览器均能正常使用。语言🚀 vue3 + typescript) 选词 由于要动态添加给...
继续阅读 »

今天有一个需求,点击选中某个英文单词,然后对这个单词做一些处理,例如高亮背景、查看一些详细信息等等,今天简单实现了一下,效果如下:(支持移动端,chrome和sarafi浏览器均能正常使用。语言🚀 vue3 + typescript)


highlight.gif


选词


由于要动态添加给某些单词动态添加一些标签,我们这里可以考虑使用v-html


首先我们先编写一下简单的结构


<script setup lang="ts">
</script>

<template>
<div class="container" v-html="shortArticle"></div>
</template>


<style>
.container {
font-size: 18px;
}
</style>

然后,我们将需要处理的短文变换为span标签包裹,这里的思路是按照空格划分,然后添加span结构,最后拼接到一起返回。这里有一些边缘条件要考虑,比如can't(whichyes!等等,按照空格划分出来的数据有一点问题。


截屏2023-04-19 20.48.19.png


如果不做处理的话,一些标点符号也会高亮出来,就不太正确了。下面是处理逻辑,整体比较简单,就不解释了。


function addElementSpan(str: string): string {
return str
.split(' ')
.map((item) => {
const { start, word, end } = getWord(item)
return `${start}<span>${word}</span>${end} `
})
.join('')
}

function getWord(str: string) {
let word = ''
let start = ''
let end = ''
let j = str.length - 1
let i = 0

while (i < str.length) {
if (/^[a-zA-Z]$/.test(str[i])) {
break
}
start = start + str[i]
i += 1
}

while (j >= 0) {
if (/^[a-zA-Z]$/.test(str[j])) {
break
}
end = str[j] + end
j -= 1
}

word = str.slice(i, j + 1)

// 处理数字
if (!word && start === end) {
start = ''
}

return {
start,
word,
end
}
}

现在我们来实现效果


<script setup lang="ts">
import { computed } from 'vue'
import { addElementSpan } from './utils'

const str = `It works fine if you move the navbar outside the header. See below. For the reason, according to MDN: The element is positioned according to the normal flow of the document, and then offset
relative to its flow root and containing block based on the values of top, right, bottom, and
left. For the containing block: The containing block is the ancestor to which the element is
relatively positioned So, when I do not misunderstand, the navbar is positioned at offset 0
within the header as soon as it is scrolled outside the viewport (which, clearly, means, you
can't see it anymore).`


const shortArticle = computed(() => {
return addElementSpan(str)
})

function setColor(event: any) {
// console.log(event.target.innerText) 获取选中的文本
event.target?.classList.add('word_highlight')
}
</script>

<template>
<div class="container" @click="setColor($event)" v-html="shortArticle"></div>
</template>


<style>
.word_highlight {
background-color: red;
}
</style>


在父亲元素上添加点击事件,触发事件点击之后,调用setColor函数,高亮背景(添加class)


不过有一点小小的问题,点击div的空白区域或者非英文单词区域会直接整个背景变成红色,控制台打印event.target.innerText可以发现它的值为整个文本,所以我们可以根据判断打印的文本长度和需要设置的文本长度是否一致来解决这个问题。(ps:⬆️面的示例代码str字符串使用了反引号 模板字符串 ,直接使用下面会影响结果)


function setColor(event: any) {
// console.log(event.target.innerText)
if(str !== event.target.innerText){
event.target?.classList.add('word_highlight')
}
}

对于event.target不太了解的伙伴可以看这篇文章 ➡️ Event.target - Web API 接口参考 | MDN (mozilla.org)


(和event.target类似的还有一个属性event.currentTarget,不太了解的伙伴可以看下这篇文章 ➡️ Event.currentTarget - Web API 接口参考 | MDN (mozilla.org),它俩的区别是event.target指的是事件触发的元素,而event.currentTarget指的是事件绑定的元素)


功能拓展


这里只是演示了一下比较简单的背景高亮效果,有需求的伙伴可以自己拓展一下。


比如类似于掘金的拼写错误提示框


截屏2023-04-19 21.16.20.png


如果要实现滑动选词的话,可以参考这个博主的文章 ➡️ 鼠标选中文本划词高亮、再次选中划词取消高亮效果


作者:笨笨狗吞噬者
来源:juejin.cn/post/7223733256688025661
收起阅读 »

因为写不出拖拽移动效果,我恶补了一下Dom中的各种距离

web
背景 最近在项目中要实现一个拖拽头像的移动效果,一直对JS Dom拖拽这一块不太熟悉,甚至在网上找一个示例,都看得云里雾里的,发现遇到最大的拦路虎就是JS Dom各种各样的距离,让人头晕眼花,看到一个距离属性,大脑中的印象极其模糊,如同有一团雾一样,不知其确切...
继续阅读 »

背景


最近在项目中要实现一个拖拽头像的移动效果,一直对JS Dom拖拽这一块不太熟悉,甚至在网上找一个示例,都看得云里雾里的,发现遇到最大的拦路虎就是JS Dom各种各样的距离,让人头晕眼花,看到一个距离属性,大脑中的印象极其模糊,如同有一团雾一样,不知其确切含义。果然是基础不牢,地动山摇。今天决心夯实一下基础,亲自动手验证一遍dom各种距离的含义。


JS Dom各种距离释义


下面我们进入正题, 笔者不善于画图, 主要是借助浏览器开发者工具,通过获取的数值给大家说明一下各种距离的区别。


第一个发现 window.devicePixelRatio 的存在


本打算用截图软件丈量尺寸,结果发现截图软件显示的屏幕宽度与浏览器开发者工具获取的宽度不一致,这是为什么呢?



  • 截图软件显示的屏幕宽度是1920


image.png



  • window.screen.width显示的屏幕宽度是1536


image.png


这是怎么回事?原来在PC端,也存在一个设备像素比的概念。它告诉浏览器一个css像素应该使用多少个物理像素来绘制。要说设备像素比,得先说一下像素和分辨率这两个概念。



  • 像素
    屏幕中最小的色块,每个色块称之为一个像素(Pixel)


image.png



image.png



  • 设备像素比


设备像素比的定义是:


window.devicePixelRatio =显示设备物理像素分辨率显示设备CSS像素分辨率\frac{显示设备物理像素分辨率}{显示设备CSS像素分辨率}


根据设备像素比的定义, 如果知道显示设备横向的css像素值,根据上面的公式,就能计算出显示设备横向的物理像素值。


显示设备宽度物理像素值= window.screen.width * window.devicePixelRatio;

设备像素比在我的笔记本电脑上显示的数值是1.25, 代表一个css逻辑像素对应着1.25个物理像素。


image.png


我前面的公式计算了一下,与截图软件显示的像素数值一致。这也反过来说明,截图软件显示的是物理像素值。


image.png



  • window.devicePixelRatio 是由什么决定的 ?


发现是由笔记本电脑屏幕的缩放设置决定的,如果设置成100%, 此时window.screen.width与笔记本电脑的显示器分辨率X轴方向的数值一致,都是1920(如右侧图所示), 此时屏幕上的字会变得比较小,比较伤视力。





  • 逻辑像素是为了解决什么问题?


逻辑像素是为了解决屏幕相同,分辨率不同的两台显示设备, 显示同一张图片大小明显不一致的问题。比如说两台笔记本都是15英寸的,一个分辨率是1920*1080,一个分辨率是960*540, 在1920*1080分辨率的设备上,每个格子比较小,在960*540分辨率的设备上,每个格子比较大。一张200*200的图片,在高分率的设备上看起来会比较小,在低分辨率的设备上,看起来会比较大。观感不好。为了使同样尺寸的图片,在两台屏幕尺寸一样大的设备上,显示尺寸看起来差不多一样大,发明了逻辑像素这个概念。规定所有电子设备呈现的图片等资源尺寸统一用逻辑像素表示。然后在高分辨率设备上,提高devicePixelRatio, 比如说设置1920*1080设备的devicePixelRatio(dpr)等于2, 一个逻辑像素占用两个格子,在低分辨率设备上,比如说在960*540设备上设置dpr=1, 一个css逻辑像素占一个格子, 这样两张图片在同样的设备上尺寸大小就差不多了。通常设备上的逻辑像素是等于物理像素的,在高分辨率设备上,物理像素是大于逻辑像素数量的。由此也可以看出,物理像素一出厂就是固定的,而设备的逻辑像素会随着设备像素比设置的值不同而改变。但图片的逻辑像素值是不变的。


document.body、document.documentElement和window.screen的宽高区别


差别是很容易辨别的,如下图所示:



  • document.body -- body标签的宽高

  • document.documentElement -- 网页可视区域的宽高(不包括滚动条)

  • window.screen -- 屏幕的宽高


image.png



  • 网页可视区域不包括滚动条


如下图所示,截图时在未把网页可视区域的滚动条高度计算在内的条件下, 截图工具显示的网页可视区域高度是168, 浏览器显示的网页可视区域的高度是167.5, 误差0.5,由于截图工具是手动截图,肯定有误差,结果表明,网页可视区域的高度 不包括滚动条高度。宽度同理。


image.png



  • 屏幕和网页可视区域的宽高区别如下:


屏幕宽高是个固定值,网页可视区域宽高会受到缩放窗口影响。


image.png



  • 屏幕高度和屏幕可用高度区别如下:


屏幕可用高度=屏幕高度-屏幕下方任务栏的高度,也就是:


window.screen.availHeight = window.screen.height - 系统任务栏高度

image.png


scrollWidth, scrollLeft, clientWidth关系


scrollWidth(滚动宽度,包含滚动条的宽度)=scrollLeft(左边卷去的距离)+clientWidth(可见部分宽度);
// 同理
scrollHeight(滚动高度,包含滚动条的高度)=scrollTop(上边卷去的距离)+clientHeight(可见部分高度);

需要注意的是,上面这三个属性,都取的是溢出元素的父级元素属性。而不是溢出元素本身。本例中溢出元素是body(document.body),其父级元素是html(document.documentElement)。另外,


溢出元素的宽度(document.body.scrollWidth)=父级元素的宽度(document.documentElement.scrollWidth) - 滚动条的宽度(在谷歌浏览器上滚动条的宽度是19px)

image.png


<!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>JS Dom各种距离</title>
<style>
html,
body {
margin: 0;
}

body {
width: 110%;
border: 10px solid blue;
}

.rect {
height: 50px;
background-color: green;
}
</style>
</head>

<body>
<div id="rect" class="rect"></div>
</body>

</html>

元素自身和父级元素的scrollWidth和scrollLeft关系?


从下图可以看出:



  • 元素自身没有X轴偏移量,元素自身的滚动宽度不包含滚动条

  • 父级元素有X轴便宜量, 父级元素滚动宽度包含滚动条
    image.png


<!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>JS Dom各种距离</title>
<style>
div {
border: 1px solid #000;
width: 200px;
height: 600px;
padding: 10px;
background-color: green;
margin: 10px;
}
</style>
</head>

<body>
<div class="rect"> 111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
</div>
</body>
<script>
</script>
</html>

offsetWidth和clientWidth的关系?


offsetWidth和clientWidth的共同点是都包括 自身宽度+padding , 不同点是offsetWidth包含border


如下图所示:



  • rect元素的clientWidth=200px(自身宽度) + 20px(左右padding) = 220px

  • rect元素的offsetWidth=200px(自身宽度) + 20px(左右padding) + 2px(左右boder) = 222px


image.png


<!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>JS Dom各种距离</title>
<style>
div {
border: 1px solid #000;
width: 200px;
height: 100px;
padding: 10px;
background-color: green;
margin: 10px;
}
</style>
</head>

<body>
<div class="rect">111111111111111111111111111111111111111111111111</div>
</body>
<script>


</script>

</html>

event.clientX,event.clientY, event.offsetX 和 event.offsetY 关系


代码如下,给rect元素添加一个mousedown事件,打印出事件源的各种位置值。


<!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>JS Dom各种距离</title>
<style>
html,
body {
margin: 0;
}

body {
width: 200px;
padding: 10px;
border: 10px solid blue;
}

.rect {
height: 50px;
background-color: green;
}
</style>
</head>

<body>

<div id="rect" class="rect"></div>


</body>
<script>
const rectDom = document.querySelector('#rect');

rectDom.addEventListener('mousedown', ({ offsetX, offsetY, clientX, clientY, pageX, pageY, screenX, screenY }) => {
console.log({ offsetX, offsetY, clientX, clientY, pageX, pageY, screenX, screenY });
})
</script>

</html>

我们通过y轴方向的高度值,了解一下这几个属性的含义。 绿色块的高度是50px, 我们找个特殊的位置(绿色块的右小角)点击一下,如下图所示:



  • offsetY=49, 反推出这个值是相对于元素自身的顶部的距离

  • clientY=69, body标签的border-top是10,paiding是10, 反推出这个值是相对网页可视区域顶部的距离

  • screenY=140,目测肯定是基于浏览器窗口,


所以它们各自的含义,就很清楚了。


image.png


事件源属性表示的距离
event.offsetX、event.offsetY鼠标相对于事件源元素(srcElement)的X,Y坐标,
event.clientX、event.clientY鼠标相对于浏览器窗口可视区域的X,Y坐标(窗口坐标),可视区域不包括工具栏和滚动偏移量。
event.pageX、event.pageY鼠标相对于文档坐标的x,y坐标,文档坐标系坐标 = 视口坐标系坐标 + 滚动的偏移量
event.screenX、event.screenY鼠标相对于用户显示器屏幕左上角的X,Y坐标


  • pageX和clientX的关系


我们点击下图绿色块的右下角,把pageX和clientX值打印出来。如下图所示:



  • 可视区域的宽度是360,点击点的clientX=359(由于是手动点击,有误差也正常)

  • 水平方向的偏移量是56

  • pageX是415,360+56=416,考虑到点击误差,可以推算出 ele.pageX = ele.clientX + ele.scrollLeft


image.png


getBoundingClientRect获取的top,bottom,left,right的含义


从下图可以看出,上下左右这四个属性,都是相对于浏览器可视区域左上角而言的。



从下图可以看出,当有滚动条出现的时候,right的值是359.6,而不是360+156(x轴的偏移量), 说明通过getBoundingClientRect获取的属性值是不计算滚动偏移量的,是相对浏览器可视区域而言的。


image.png


想移动元素,mouse和drag事件怎么选?


mouse事件相对简单,只有mousedown(开始),mousemove(移动中),mouseup(结束)三种。与之对应的移动端事件是touch事件,也是三种touchstart(手指触摸屏幕), touchmove(手指在屏幕上移动), touchend(手指离开屏幕)。


相对而言, drag事件就要丰富一些。



  • 被拖拽元素事件


事件名触发时机触发次数
dragstart拖拽开始时触发一次1
drag拖拽开始后反复触发多次
dragend拖拽结束后触发一次1


  • 目标容器事件


事件名触发时机触发次数
dragenter被拖拽元素进入目标时触发一次1
dragover被拖拽元素在目标容器范围内时反复触发多次
drop被拖拽元素在目标容器内释放时(前提是设置了dropover事件)1

想要移动一个元素,该如何选择这两种事件类型呢? 选择依据是:


类型选择依据
mouse事件1. 要求丝滑的拖拽体验 2. 无固定的拖拽区域 3. 无需传数据
drag事件1. 拖拽区域有范围限制 2. 对拖拽流畅性要求不高 3. 拖拽时需要传数据

现在让我们写个拖拽效果


光说不练假把式, 扫清了学习障碍后,让我们自信满满地写一个兼容PC端和移动端的拖动效果。不积跬步无以至千里,幻想一口吃个胖子,是不现实的。这一点在股市上体现的淋漓尽致。都是有耐心的人赚急躁的人的钱。所以,要我们沉下心来,打牢基础,硬骨头啃一点就会少一点,步步为营,稳扎稳打,硬骨头也会被啃成渣。



<!DOCTYPE html>
<html lang="en">
<head>
    
<meta charset="UTF-8" />
    
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
    
<title>拖拽水潭</title>
<style>
.water {
position: absolute;
width: 100px;
height: 100px;
border-radius: 100px;
cursor: grab;
z-index: 10;
}
</style>
</head>

<body>
<img class="water" src="./water.png" alt="" />  
</body>

<script>
let evtName = getEventName();
// 确保图片加载完
window.onload = () => {
// 鼠标拖拽图片时,拖拽点距离图片X和Y轴方向的距离
let offsetX = 0, offsetY = 0;
const water = document.querySelector(".water");

const moveAt = ({ pageX, pageY }) => {
water.style.cssText = `left:${pageX - offsetX}px;top:${pageY - offsetY}px;`;
};

water.addEventListener(evtName.start, (event) => {
// 图片的偏移距离是针对图片边界, 不能把图片边界到鼠标点击图片位置的距离算在内
// 否则移动图片结束后,就会出现向下,向右非自然的偏移
offsetX = event.clientX - water.getBoundingClientRect().left;
offsetY = event.clientY - water.getBoundingClientRect().top;

// 设置初始偏移
moveAt(event);

// 监听鼠标相对于可视窗口移动的距离
document.addEventListener(evtName.move, moveAt);
});

// 拖动停止时,释放document上绑定的移动事件
// 不然移动鼠标,不拖拽时白白产生性能开销
water.addEventListener(evtName.end, () =>
document.removeEventListener(evtName.move, moveAt);
});
};

// 区分是移动端还是PC端移动事件
function getEventName() {
if ("ontouchstart" in window) {
return {
start: "touchstart",
move: "touchmove",
end: "touchend",
};
} else {
return {
start: "mousedown",
move: "mousemove",
end: "mouseup",
};
}
}
</script>
</html>

彩蛋


在chrome浏览器上发现一个奇怪的现象,设置的border值是整数,计算出来的值却带有小数


image.png


而当border值是4的整数倍的时候,计算值是正确的


image.png


看了这篇文章解释说,浏览器可能只能渲染具有整数物理像素的border值,不是整数物理像素的值时,计算出的是近似border值。这个解释似乎讲得通,在设备像素比是window.devicePixelRatio=1.25的情况下, 1px对应的是1.25物理像素, 1.25*4的倍数才是整数,所以设置的逻辑像素是4的整数倍数,显示的渲染计算值与设置值一致,唯一让人不理解的地方,为什么padding,margin,width/height却不遵循同样的规则。


作者:去伪存真
来源:juejin.cn/post/7225206098692407355
收起阅读 »

让我看看你们公司的代码规范都是啥样的?

web
我这里提供一份我自己在使用的项目代码规范,当然我这里比较简陋,有补充的可以打在评论区,我丰富到文章里去。 1.组件命名规范 components下的组件命名规范遵循大驼峰命名规范。 示例:conpnents/AlbumItemCard/AlbumItemCar...
继续阅读 »

我这里提供一份我自己在使用的项目代码规范,当然我这里比较简陋,有补充的可以打在评论区,我丰富到文章里去。


1.组件命名规范


components下的组件命名规范遵循大驼峰命名规范。


示例:conpnents/AlbumItemCard/AlbumItemCard.vue



小驼峰式命名法(lower camel case): 第一个单词以小写字母开始;第二个单词的首字母大写,例如:myName




大驼峰式命名法(upper camel case): 每一个单字的首字母都采用大写字母,例如:MyName



2.目录命名规范


pages下的文件命名规范:遵循小驼峰命名规范。


示例:pages/createAlbum/createAlbum.vue


3.CSS命名规范


class命名规范为中划线。


示例:


<template>
<view class="gui-padding">
...
</view>
</template>
<style lang="scss" scoped>
.gui-padding {
...
}
</style>

css使用scss进行书写。


4.代码注释规范


行内注释://


函数注释:


/**
* @description: 加深颜色值
* @param {string} color 颜色值字符串
* @returns {*} 返回处理后的颜色值
*/

export function getDarkColor(color: string, level: number) {
const reg = /^#?[0-9A-Fa-f]{6}$/
if (!reg.test(color)) return ElMessage.warning('输入错误的hex颜色值')
const rgb = hexToRgb(color)
for (let i = 0; i < 3; i++)
rgb[i] = Math.round(20.5 * level + rgb[i] * (1 - level))
return rgbToHex(rgb[0], rgb[1], rgb[2])
}

接口注释:


/**
* @description 获取后台用户分页列表(带搜索)
* @param page
* @param limit
* @param username
* @returns {<PageRes<AclUser.ResAclUserList>>}
* @docs https://xxxx
*/

export function getAclUserList(params: AclUser.ReqAclUserListParams) {
return http.get<PageRes<AclUser.ResAclUserList>>(
`/admin/acl/user/${params.pageNum}/${params.pageSize}`,
{ username: params.username },
)
}

5.接口书写规范


4.1 接口定义规范:


接口全部写在api目录下面,按照功能划分,分为不同的目录。


比如搜索接口,定义在api/search/index.ts下面。


4..2 接口书写规范:


统一使用类方法,内部方法定义每个接口,最后统一export,接口使用到的类型全部定义在同级目录的interfaces.ts文件中。比如搜索相关的接口:


import Service from '../../utils/request'
import { SearchItemInterface, SearchPageResponseInterface, SearchParamsInterface } from "./interfaces"

class CateGory extends Service {

/**
* @description 搜索功能
* @param {SearchParamsInterface} params 二级分类Id
*/

// 搜索
getSearchAlbumInfo(params: SearchParamsInterface) {
return this.post<SearchPageResponseInterface<SearchItemInterface[]>>({
url: '/api/search/albumInfo',
data: params
})
}
/**
* @description: 获取搜索建议
* @param {string} keyword 搜索关键字
* @return {*}
*/

getSearchSuggestions(keyword: string) {
return this.get<string[]>({
url: `/api/search/albumInfo/completeSuggest/${keyword}`,
loading:false
})
}

}

export const search = new CateGory()

4.3 接口类型定义:


// 搜索参数
export interface SearchParamsInterface {
keyword: string;
category1Id?: number | null;
category2Id?: number | null;
category3Id?: number | null;
attributeList?: string[] | null;
order?: string | null;
pageNo?: number;
pageSize?: number;
}
// 搜索结果item向接口
export interface SearchItemInterface {
id: number;
albumTitle: string;
albumIntro: string;
announcerName: string;
coverUrl: string;
includeTrackCount: number;
isFinished: string;
payType: string
createTime: string;
playStatNum: number;
collectStatNum: number;
buyStatNum: number;
albumCommentStatNum: number;
}

4.4 接口引用


所有export的类接口方法都在api/index.ts中统一引入:


export { courseService } from './category/category'
export { albumsService } from './albums/albums'
export { search } from './search/search'

在页面中使用:


<script>
import { courseService } from "../../api"
/**
* @description: 获取所有分类
* @returns {*}
*/

const getCategoryList = async () => {
try {
const res = await courseService.findAllCategory()
} catch (error) {
console.log(error)
}
}
</script>

6.分支命名规范


分支管理命名规范解释
master 主分支master稳定版本分支,上线完成回归后后,由项目技术负责人从 release 分支合并进来,并打 tag
test 测试分支test/版本号示例:test/1.0.0测试人员使用分支,测试时从 feature 分支合并进来,支持多人合并
feature 功能开发分支feature/功能名称示例:feature/blog新功能开发使用分支,基于master建立
bugfix修复分支bugfix/功能名称示例:fix/blog紧急线上bug修复使用分支,基于master建立
release 上线分支release/版本号示例:release/0.1.0用于上线的分支,基于 master 建立,必须对要并入的 分支代码进行 Code review 后,才可并入上线

7.代码提交规范


作者:白哥学前端
来源:juejin.cn/post/7224408845685522492
tbody>
前缀解释示例
feat新功能feat: 添加新功能
fix修复fix: 修改bug
docs文档变更docs: 更新文档
style代码样式变更style: 修改样式
refactor重构refactor: 重构代码
perf性能优化perf: 优化了性能
test增加测试test: 单元测试
revert回退revert: 回退代码
build打包build: 打包代码
chore构建过程或辅助工具的变动chore: 修改构建
收起阅读 »

html手写一个打印机效果-从最基础到学会

web
手写一个打印机效果 啥叫打印机效果,话不多说,直接上效果。我们可以自己写入一段文本然后通过html的方式,让它跟打印机一样,一个一个的打印到页面,并且还可以一个一个的删除。在这里我先浅说一下,我们的实现技巧,定时器setTimeout控制时间,然后for循环遍...
继续阅读 »

手写一个打印机效果


啥叫打印机效果,话不多说,直接上效果。我们可以自己写入一段文本然后通过html的方式,让它跟打印机一样,一个一个的打印到页面,并且还可以一个一个的删除。在这里我先浅说一下,我们的实现技巧,定时器setTimeout控制时间,然后for循环遍历写入到页面上。


封装的打印js
main(str,text)直接传入要写入的数组对象和要写入的元素。
copy.js 下载到本地引入然后调用它就可以了
image.png


代码


先拿到我们要写入的元素,然后设置好我们要写入的内容。


 var text = document.querySelector('.text');
var str = ['你好 ,我是一名刚入坑不久的大三在校生。', '现在学习都是为了将来的工作。', '希望能够得到大家的鼓励,谢谢!']

基础代码一


首先这里,我们先实现一个只有一段文字的实现效果。实现思路就是通过计时器,控制好时间,每次写入的文字通过str[0].substr(0, k)拿到,需要注意的是,因为是异步任务,回退的时候,我们的时间要设置好,加上写入完的时间1000 + 200 * str[0].length)


  写入
for (let j = 0; j < str[0].length; j++) {
// 使用 setTimeout 函数来实现每个字符的延时输出
setTimeout(() => {
text.innerHTML = str[0].substr(0, j) // 显示当前字符串的前 j 个字符
}, 200 * j) // 延迟时间为 200 毫秒乘以 j,即每个字符间隔 200 毫秒
}

// 回退
// 在所有字符输出完成后,等待 1000 毫秒后开始回退
setTimeout(() => {
for (let k = str[0].length, i = 0; k >= 0; k--, i++) {
// 使用 setTimeout 函数来实现每个字符的延时输出
setTimeout(() => {
text.innerHTML = str[0].substr(0, k) // 显示当前字符串的前 k 个字符
}, 200 * i) // 延迟时间为 200 毫秒乘以 i,即每个字符间隔 200 毫秒
}
}, 1000 + 200 * str[0].length) // 等待时间为 1000 毫秒加上所有字符输出的延时时间

基础代码二 错误代码


首先这个代码是错误的
为了能让大家更好的看到错误的效果,于是我把这个代码也上传了。大家可以看到,在这里,页面上的文字总是会莫名奇怪的出现删除,根本不是我们想要的。其实我们也只是对上面一个代码进行了一个for循环遍历,却出现了这样的效果。其实这导致的原因就是setTimeout是异步任务,时间没有控制好。即每个字符串的打印和删除都是异步任务,无法保证它们的执行顺序。因此,可能会出现多个字符串的打印和删除任务交错执行的情况,导致效果不符合预期。



 // 即每个字符串的打印和删除都是异步任务,无法保证它们的执行顺序。因此,可能会出现多个字符串的打印和删除任务交错执行的情况,导致效果不符合预期。
// 整个str 这是一个有问题的代码 因为计算时间太麻烦了 都是异步任务
// for (let s = 0; s < str.length; s++) {
// // 写入
// for (let j = 0; j < str[s].length; j++) {
// setTimeout(() => {
// text.innerHTML = str[s].substr(0, j)
// }, 200 * j)
// }
// // 回退
// setTimeout(() => {
// for (let k = str[s].length, i = 0; k >= 0; k--, i++) {
// setTimeout(() => {
// text.innerHTML = str[s].substr(0, k)
// }, 200 * i)

// }
// }, 1000 + 200 * str[s].length)
// }

基础代码三


为了解决上面的问题,我们使用了函数封装并且使用了回调函数实现我们想要的效果。我们将打印和删除都封装成一个含有回调函数的函数,为什么要含有回调函数呢?这是为了我们下面对一个字符串打印和删除的函数做封装。打我们打印完一个字符串时,我们才会执行删除。所有我们将删除函数放到打印的回调函数中去执行。然后我们将打印整个字符串数组进行封装,因为我们在删除的里面也有一个回调函数,那么我们可以在这个回调函数里去执行打印下一条字符串,这样就防止了控制时间不准确的问题。


 // 打印字符串
function printText(str, callback) {
var i = 0;
var timer = setInterval(function () {
text.innerHTML = str.substr(0, i); // 将字符串的前缀赋值给显示文本的元素
i++;
if (i > str.length) { // 如果已经打印完整个字符串
clearInterval(timer); // 停止定时器
callback && callback(); // 调用回调函数
}
}, 200); // 每 200 毫秒打印一个字符
}

// 删除字符串
function deleteText(str, callback) {
var i = str.length;
var timer = setInterval(function () {
text.innerHTML = str.substr(0, i); // 将字符串的前缀赋值给显示文本的元素
i--;
if (i < 0) { // 如果已经删除到空字符串
clearInterval(timer); // 停止定时器
callback && callback(); // 调用回调函数
}
}, 200); // 每 200 毫秒删除一个字符
}

// 打印和删除字符串
function printAndDeleteText(str, callback) {
printText(str, function () { // 先打印字符串
setTimeout(function () {
deleteText(str, callback); // 等待 1 秒后再删除字符串
}, 1000);
});
}

// 循环遍历字符串数组,依次打印和删除字符串
function printAndDeleteAllText(strArr) {
function printAndDeleteNext(i) {
if (i >= strArr.length) { // 如果已经处理完所有字符串
printAndDeleteNext(0); // 重新从头开始处理
} else {
printAndDeleteText(strArr[i], function () { // 先打印字符串
i++;
printAndDeleteNext(i); // 递归调用自身,处理下一个字符串
});
}
}
printAndDeleteNext(0); // 开始处理第一个字符串
}
// 开始打印和删除字符串数组中的所有字符串
printAndDeleteAllText(str)

最优代码


其实我们做了,这么多,最后就是为了解决异步任务。
所以我这里直接采用Promiseasync await解决上面的问题。我们通过Promise解决实现打印和删除的异步任务。我们通过async await封装整个运行函数,解决了定时器异步问题,不用再计算时间,又难有算不出来。


 // 最终版 封装 解决异步任务
function writeText(t, delay = 200) {
return new Promise((resolve, reject) => {
setTimeout(() => {
text.innerHTML = t; // 显示当前字符串 t
resolve(); // Promise 完成
}, delay) // 延迟 delay 毫秒后执行
})
}

async function main(str) {
while (true) { // 无限循环
for (let j = 0; j < str.length; j++) {
// 写入
for (let i = 0; i <= str[j].length; i++) {
await writeText(str[j].substr(0, i)) // 显示当前字符串的前 i 个字符
}
// 回退
// 回退前先等一秒
await new Promise((resolve, reject) => {
setTimeout(() => {
resolve(); // 等待 1000 毫秒后 Promise 完成
}, 1000) // 等待 1000 毫秒
})
for (let i = str[j].length; i >= 0; i--) {
await writeText(str[j].substr(0, i), 200) // 显示当前字符串的前 i 个字符,间隔 200 毫秒
}
}
}
}
main(str)

源码


<!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>打印机效果</title>
<style>
.container {
display: flex;
/* 使用 flex 布局 */
flex-direction: column;
/* 垂直布局 */
align-items: center;
/* 水平居中 */
justify-content: center;
/* 垂直居中 */
height: 100vh;
/* 高度占满整个视口 */
}

h1 {
font-size: 3rem;
/* 字体大小 */
margin-bottom: 2rem;
/* 底部间距 */
text-align: center;
/* 居中对齐 */
}

.text {
font-size: 2rem;
/* 字体大小 */
font-weight: bold;
/* 字体加粗 */
text-align: center;
/* 居中对齐 */
border-right: 2px solid black;
/* 添加光标效果 */
white-space: nowrap;
/* 不换行 */
overflow: hidden;
/* 隐藏超出部分 */
animation: blink 0.5s step-end infinite;
/* 添加光标闪烁效果 */
height: 3rem;
/* 设置一个固定的高度 */
}


@keyframes blink {

from,
to {
border-color: transparent;
/* 透明边框颜色 */
}

50% {
border-color: black;
/* 黑色边框颜色 */
}
}
</style>
</head>

<body>
<div class="container">
<h1>逐字打印和删除文字效果</h1>
<p class="text"></p>
</div>
</body>
<script>
var text = document.querySelector('.text');
var str = ['你好 ,我是一名刚入坑不久的大三在校生。', '现在学习都是为了将来的工作。', '希望能够得到大家的鼓励,谢谢!']


// 写入
// for (let j = 0; j < str[0].length; j++) {
// // 使用 setTimeout 函数来实现每个字符的延时输出
// setTimeout(() => {
// text.innerHTML = str[0].substr(0, j) // 显示当前字符串的前 j 个字符
// }, 200 * j) // 延迟时间为 200 毫秒乘以 j,即每个字符间隔 200 毫秒
// }

// // 回退
// // 在所有字符输出完成后,等待 1000 毫秒后开始回退
// setTimeout(() => {
// for (let k = str[0].length, i = 0; k >= 0; k--, i++) {
// // 使用 setTimeout 函数来实现每个字符的延时输出
// setTimeout(() => {
// text.innerHTML = str[0].substr(0, k) // 显示当前字符串的前 k 个字符
// }, 200 * i) // 延迟时间为 200 毫秒乘以 i,即每个字符间隔 200 毫秒
// }
// }, 1000 + 200 * str[0].length) // 等待时间为 1000 毫秒加上所有字符输出的延时时间


// 即每个字符串的打印和删除都是异步任务,无法保证它们的执行顺序。因此,可能会出现多个字符串的打印和删除任务交错执行的情况,导致效果不符合预期。
// 整个str 这是一个有问题的代码 因为计算时间太麻烦了 都是异步任务
// for (let s = 0; s < str.length; s++) {
// // 写入
// for (let j = 0; j < str[s].length; j++) {
// setTimeout(() => {
// text.innerHTML = str[s].substr(0, j)
// }, 200 * j)
// }
// // 回退
// setTimeout(() => {
// for (let k = str[s].length, i = 0; k >= 0; k--, i++) {
// setTimeout(() => {
// text.innerHTML = str[s].substr(0, k)
// }, 200 * i)

// }
// }, 1000 + 200 * str[s].length)
// }


// 最终版 封装 解决异步任务
function writeText(t, delay = 200) {
return new Promise((resolve, reject) => {
setTimeout(() => {
text.innerHTML = t; // 显示当前字符串 t
resolve(); // Promise 完成
}, delay) // 延迟 delay 毫秒后执行
})
}

async function main(str) {
while (true) { // 无限循环
for (let j = 0; j < str.length; j++) {
// 写入
for (let i = 0; i <= str[j].length; i++) {
await writeText(str[j].substr(0, i)) // 显示当前字符串的前 i 个字符
}
// 回退
// 回退前先等一秒
await new Promise((resolve, reject) => {
setTimeout(() => {
resolve(); // 等待 1000 毫秒后 Promise 完成
}, 1000) // 等待 1000 毫秒
})
for (let i = str[j].length; i >= 0; i--) {
await writeText(str[j].substr(0, i), 200) // 显示当前字符串的前 i 个字符,间隔 200 毫秒
}
}
}
}
main(str)
</script>

</html>

作者:Mr-Wang-Y-P
来源:juejin.cn/post/7225178555827191868
收起阅读 »

URL刺客现身,竟另有妙用!

web
工作中大家会接触到形形色色的 url,有些完美遵循格式,有些却像刺客一样,冷不丁的给你一刀。 先介绍下我的惨痛经历,给大家避避坑,最后告诉大家一个 url 刺客的妙用。 刺客介绍 1. iOS WKWebview 刺客 此类刺客手段单一,只会影响 iOS WK...
继续阅读 »

工作中大家会接触到形形色色的 url,有些完美遵循格式,有些却像刺客一样,冷不丁的给你一刀。


先介绍下我的惨痛经历,给大家避避坑,最后告诉大家一个 url 刺客的妙用。


刺客介绍


1. iOS WKWebview 刺客


此类刺客手段单一,只会影响 iOS WKWebview



  • 空格


运营人员由于在通讯工具中复制粘贴,导致前面多了一个空格,没有仔细检查,直接录入了后台管理系统。



  • 中文


运营人员为了方便自身统计,直接在url中加入中文,录入了后台管理系统。


现象均为打开一个空白页,常见的处理手段如下:



  • 将参数里的中文URIEncode

  • 去掉首尾空格


const safeUrl = (url: string) => {
const index = url.indexOf('?');

if (index === -1) return url.trim();

// 这行可以用任意解析参数方法替代,仅代表要拿到参数,不考虑兼容性的简单写法
const params = new URLSearchParams(url.substring(index));
const paramStr = Object.keys(params)
.map((key: string) => {
return `${key}=${encodeURIComponent(params[key])}`;
})
.join('&');

const formatUrl = url.substring(0, index + 1) + paramStr;

return formatUrl.trim();
};

可以看到虽然这里提出了一个 safeUrl 方法,但如果业务中大量使用 window.location.href , window.location.replace, 之类的方法进行跳转,替换起来会比较繁琐.


再比如在 Hybrid App 的场景中,虽然都是跳转,打开新的 webview ,还是在本页面跳转会是不同的实现,所以在业务内提取一个公共的跳转方法更有利于健壮性和拓展性。


值得注意的是,如果链接上的中文可能是用于统计的,在上报打点时,应该将其值(前端/服务端处理均可)进行 URIDecode,否则运营人员会在后台看到一串串莫名其妙的 %XX ,会非常崩溃(别问我怎么知道的,可能只是伤害过太多运营)


2. 格式刺客


格式刺客指的是,不管何种原因,不知何种场景,就是不小心配错了,打错了,漏打了等。


比如:https://www.baidu.com 就被打成了 htps://www.baidu.com、www.baidu.com 等。


// 检查URL格式是否正确
function isValidUrl(url: string): boolean {
const urlPattern = new RegExp(
"^(https?:\/\/)?" + // 协议
"(([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,})" + // 域名
"(:[0-9]{1,5})?" + // 端口号
"(\/.*)?$", // 路径
"i"
);
return urlPattern.test(url);
}

以上是一个很基础的判断,但是实际的应用场景中,有可能会需要填写相对路径,或者自定义的 scheme ,比如 wx:// ,所以检验的宽松度可以自行把握。


在校验到 url 配置可能存在问题时,可以上报到 sentry 或者其他异常监控平台,这样就可以比用户上报客服更早的发现潜在问题,避免长时间的运营事故。


3. 异形刺客


这种刺客在视觉上让人无法察觉,只有在跳转后才会让人疑惑不已。他也是最近被产品同学发现的,以下是当时的现场截图:



一段平平无奇的文本,跟着一段链接,视觉上无任何异常。


经过对跳转后的地址进行分析,发现了前面居然有一个这样的字符%E2%80%8B,好奇的在控制台中进行了尝试。




一个好家伙,这是什么,两个单引号吗?并不是,对比了很常用的 '%2B' ,单引号是自带的,那么我到底看到了什么,魔鬼嘛~


在进行了一番检索后知道了这种字符被称为零宽空格,他还有以下兄弟:



  • \u202b-\u202f

  • \ufeff

  • \u202a-\u202e


具体含义可以看看参考资料,这一类字符完全看不见,但是却对程序的运行产生了恶劣的影响。


可以使用这个语句去掉


str.replace(/[\u200b-\u200f\uFEFF\u202a-\u202e]/g, "");

刺客的妙用


头一天还被刺客气的瑟瑟发抖。第二天居然发现刺客的妙用。


场景:




  • 产品要求在微信环境隐藏标题




我方前端工程师:



  • 大手一挥,发功完毕,准备收工


document.title = '';

测试:




  • 来看看,页面A标题隐藏不了




我方前端工程师:


啊?怎么回事,本地调试还是好的,发上去就不行了,为什么页面A不可以,另外一个页面B只是参数变了变就行。


架构师出手:


页面A包含了开放标签,导致设置空Title失效,空,猛然想起了刺客,快用起来!


function setTitle(title: string) {
if (title) {
document.title = title;
} else {
document.title = decodeURIComponent('%E2%80%8B');
}
}

果然有效,成功解决了一个疑难杂症,猜测是微信里有不允许设置标题为空的机制,会在某些标签存在的时候被触发。(以上场景在 Android 微信 Webview 中可复现)


小结


以上只是工作中碰到 url 异常的部分场景和处理方案,如果小伙伴们也有类似的经历,可以在评论区中分享,帮助大家避坑,感谢朋友们的阅读,笔芯~


参考资料:


零宽字符 - 掘金LvLin


什么零宽度字符,以及零宽度字符在JavaScript中的应用 - 掘金whosmeya


作者:windyrain
来源:juejin.cn/post/7225133152490094651
收起阅读 »

关于 Emoji 你不知道的事

web
2022 年,支付宝上线了生僻字键盘,解决了“身份认证”环节中普通输入法经常打不出生僻字的问题。生僻字键盘是蚂蚁集团生僻字解决方案的一部分,本系列将持续分享其背后的字符编码科普文章,包括不限于:《文字是如何显示在屏幕上的?》、《字符编码工作者都在做什么》,敬请...
继续阅读 »

2022 年,支付宝上线了生僻字键盘,解决了“身份认证”环节中普通输入法经常打不出生僻字的问题。生僻字键盘是蚂蚁集团生僻字解决方案的一部分,本系列将持续分享其背后的字符编码科普文章,包括不限于:《文字是如何显示在屏幕上的?》、《字符编码工作者都在做什么》,敬请期待。


本文作者是蚂蚁集团前端工程师醉杭(👉 点击查看醉杭的成长故事),本篇将介绍 Emoji 的编码逻辑,以及如何在代码中正确处理 Emoji 。蚂蚁集团前端工程师七柚封装了字符处理 js 库,已开源,欢迎使用~ github.com/alipay/char…



结论先行



  • 基本 emoji 和常用 Unicode 字符毫无区别


每个 emoji 用对应一个 Unicode 码位,如:🌔 U+1F314 (对应 JS 中 UTF-16 编码是:"\uD83C\uDF14"),汉字 𠇔 U+201D4,对应 JS 中的 UTF-16 编码是"\uD840\uDDD4"



  • emoji 有特殊的修饰、拼接等规则


在某些 emoji 字符后增加一个肤色修饰符改变 emoji 的肤色、可以将多个 emoji 通过连接符拼接成一个emoji,这些特殊规则使得在代码中判定 emoji 的长度、截取和对 emoji 做其他处理都比较困难。需要澄清的是:用一个 Unicode 字符修饰前一个字符不是 emoji 独有的,其他 Unicode 字符也存在,如:Ü,由大写字母U(U+0055),后面跟一个连音符号(U+0308)组成。



  • 术语


码点/码位:Unicode 编码空间中的一个编码,如,汉字𠇔的码位是 201D4,通常表示为:U+201D4


起源


1982 年,卡内基美隆大学是首次在电子公告里中使:-)表情符号。后续在日本手机短信中盛行,并称为颜文字(日语:かおもじ,英文:emoticon),颜文字仍然是普通的文本字符。
1999 年,栗田穰崇 (Shigetaka Kurita) 发明了 e-moji (え-もじ),并设计了 176 个 emoji 符号,共 6 种颜色,分辨率为 12x12。
image.png
纽约博物馆馆藏:最初的 176 个 emoji


2010 年,Unicode 正式收录了 emoji,为每个 emoji 分配了唯一的码点。
2011 年,Apple 在 iOS 中加入了标准的 emoji 输入键盘,2 年后安卓系统也引入了 emoji 键盘。


Unicode


Unicode 中原本就收录了很多有意义的字符,如:㎓、𐦖、☳,大家还可以查看 Unicode 1 号平面的埃及象形文字区 (U+13000–U+1342F)。收录 emoji 对 Unicode 来说没有挑战,技术上是完全兼容的。
image.png
Unicode 象形文字区节选


Emoji 的编码


基本 emoji



基本 emoji 是指在 Unicode 编码表中用 1 个唯一码位表示的 emoji



最简单的 emoji 就是 Unicode 表中的一个字符,和我们常用的 Unicode 字符没有区别。多数基本 emoji 都被分配到 Unicode 编码表 1 号平面的 U+1F300–1F6FFU+1F900–1FAFF 两个区域,完整的列表请看15.0/emoji-sequences.txt
image.png
Unicode 中 emoji 的码位


我们常见的 emoji 是彩色的,而常见的字体是黑色的。字符的颜色取决于字体文件,如果你愿意,你也可以把其常见的汉字字体设计成彩色的。iOS/MacOS 的Apple Color Emoji字体是一种 160x160 的点阵字体, Android 的Noto Emoji是一种 128x128 的点阵字体,而 Windows 使用的 Segoe UI Emoji 是一种矢量彩色字体。


为什么同一个 emoji 在不同设备、不同软件中显示不同?
不同设备、软件使用了不同的 emoji 字体所以显示效果不同。Unicode 只是约定了码点到 emoji 的映射关系,并没有约定 emoji 图形,每个 emoji 字体文件可以按照自己的想法设计 emoji。
image.png
同一个 emoji 在不同软件上的显示效果


为什么在钉钉中发送**[憨笑]**会显示成image.png
早期包含 Unicode emoji 的字体还没广泛普及,你给对方发一个 emoji 符号😄,如果没对方设备有对应的字体看到的会是**?**
为了解决缺失 emoji 字体导致大家显示不一致的问题(或者为了方便自定义自己的**伪emoji**——为了方便描述,把软件自定义的图片称作伪 emoji),很多软件自己开发了能向下兼容的解决方案,如钉钉。该自定义方案与 Unicode 编码没有关系,只是将特殊的字符串与一张图片映射起来,当对方发送[xx]字符串时,将它显示成对应的图片
早期支付宝的转账备注功能中也定义了自己的伪emoji伪emoji的好处是向下兼容,如果使用标准的Unicode emoji 可能会导致别的系统无法处理(如:做了汉字正则校验),导致转账失败;弊端是不通用,别的系统通常不支持另一个系统定义的伪emoji,直接将[xx]文本显示出来,如:收银台在支付界面就会直接显示转账备注的伪 emoji 文本[xx]
image.png


字素集


字素集(grapheme cluster)在 Unicode 中通常一个码点对应一个字符,但是 Unicode 引入了特定的机制允许多个 Unicode 码点组合成一个字形符号。这样由于多个码点组合成的一个字符称作字素集。
比如Ü是一个字素集,是由两个码点组成:大写字母 U(U+0055),后面跟一个连音符号(U+0308)。再比如:'曙󠄀'.length=3'🤦🏼‍♂️'.length=7,前者由基本的字符加上一个变体选择符️ VS-17 (见后文)组成,后者由多个基础 emoji 修饰符、连接符组成。
点开有惊喜Ų̷̡̡̨̫͍̟̯̣͎͓̘̱̖̱̣͈͍̫͖̮̫̹̟̣͉̦̬̬͈͈͔͙͕̩̬̐̏̌̉́̾͑̒͌͊͗́̾̈̈́̆̅̉͌̋̇͆̚̚̚͠ͅ[左边是一个.length 为 65 的字素集,它是不可分割的一个字符]


在 Unicode 的规范中要求所有软件(本编辑器、文本渲染、搜索等)将一个字素集当做不可分割的整体,即:当做一个单一的字符对待。
image.png
Unicode 处理的难点就在于字素集,下文均与该定义有关,开发者的噩梦都源自该概念。不能简单地通过 .length 读取字符串的长度;如果想截取字符串的前 10 个字符,也不能简单的使用.substring(0, 10),因为这可能会截断 emoji 字符;反转字符串也非常困难,U+263A U+FE0F 有意义,反转之后 U+FE0F U+263A 却没有意义,后文会介绍正确的处理方式。


变体选择符️


Variation Selector(又叫异体字选择器),是 Unicode 中定义的一种修饰符机制。一个基本字符后接上一个异体字选择器组成一个异体字。背景是:一个字符可能会有多个不同的变体,这些变体本质上是同一个字符,具有同样的含义,由于地区、文化差异导致他们演变成了不同的书写形式。Unicode 为变体字分配了同一个码点,如果想要显示特定的书写形式可以在字符后紧接着一个异体字选择器指定。
image.pngimage.png就是变体字。需要澄清的是,并非所有相似的字符都按照异性字的形式合并成了一个码点,就是分别分配了不同的码点,理论上这两个字符也可以合并变体字共用一个码点。
在 Unicode 中引入彩色的 emoji 前就已经定义了一些黑色的图形符号,引入彩色 emoji 后,新的 emoji 与黑色的符号具有相同的含义,于是共用了同一个 Unicode 码点,可在字符后接上一个 VS 指定要显示那个版本。
常用的 VS 有 16 个 VS-1 ~ VS-16,对应的 Unicode 是(U+FE00~U+FE0F),其中 VS-15(U+FE0E)用于指定显示黑色的 text 版本,VS-16(U+FE0F)用于指定显示彩色的 emoji 版本。


默认显示VS-15 修饰符VS-16 修饰符
U+2702✂︎U+2702 U+FE0E✂︎U+2702 U+FE0F ✂️
U+2620☠︎U+2620 U+FE0E☠︎U+2620 U+FE0F ☠️
U+26A0⚛︎U+26A0 U+FE0E⚛︎U+26A0 U+FE0F ⚛️
U+2618☘︎U+2618 U+FE0E☘︎U+2618 U+FE0F ☘️

可以动手验证一下



image.png



  • ✂ 不含修饰符'\u2702'

  • ✂︎ 含 VS-15'\u2702\uFE0E'

  • ✂️ 含 VS-16'\u2702\uFE0F'



为什么把黑色的剪刀 ✂︎ 粘贴到 Chrome 搜索栏中显示成彩色,把彩色剪刀 ✂️ 复制到 Chrome 的 Console 中显示成黑色?
image.png image.png
我们通过 VS 符号告诉软件要显示成指定的异体字符,但是软件可以不听我们的,软件可能会强制指定特定的字体,如果该字体中只包含一种异体字符的字形数据那就只会显示该字形。


肤色修饰符


大多数人形相关的 Emoji 默认是黄色的,在 2015 年为 emoji 引入肤色支持。没有为每种肤色的 emoji 组合分配新的码点,而是引入了五个新码点作为修饰符:1F3FB 🏻、1F3FC 🏼、1F3FD 🏽、1F3FE 🏾、1F3FF 🏿 。肤色修饰符追加到现有的 emoji 后面则形成新的变种,如:👋 U+1F44B+ 🏽U+1F3FD= 👋🏽



  • 👋 在 JavaScript 中 UTF-16 值是'\uD83D\uDC4B'

  • **🏽 **在 JavaScript 中 UTF-16 值是'\uD83C\uDFFD'


组合在一起'\uD83D\uDC4B\uD83C\uDFFD'就得到了 👋🏽
image.png


5 种肤色修饰符的取值是基于菲茨帕特里克度量,因此叫做 EMOJI MODIFIER FITZPATRICK。肤色度量共有 6 个取值,但在 emoji 中前两个颜色合并成了一个。
image.png
最终 280 个人形 emoji 就产生了 1680 种肤色变种,这是五种不同肤色的舞者:🕺🕺🏻🕺🏼🕺🏽🕺🏾🕺🏿


零宽度连接符(ZWJ)


Unicode 通过多个基础 emoji 组合的形式表示某些复杂 emoji。组合的方式是在两个 emoji 之间添加一个U+200D,即:零宽度连接符(ZERO-WIDTH JOINER,简写为 ZWJ),如:



  • 👩 + ZWJ+ 🌾 = 👩‍🌾


image.png
下面是一些例子,完整的组合列表参考:Unicode 15.0/emoji-zwj-sequences.txt




  • 👩 + ✈️ → 👩‍✈️

  • 👨 + 💻 → 👨‍💻

  • 👰 + ♂️ → 👰‍♂️

  • 🐻 + ❄️ → 🐻‍❄️

  • 🏴 + ☠️ → 🏴‍☠️

  • 🏳️ + 🌈 → 🏳️‍🌈

  • 👨 + 🦰 → 👨‍🦰 (有意思的是:发色是通过 ZWJ 组合基础 emoji 实现,而肤色则是用肤色修饰符实现)

  • 👨🏻 + 🤝 + 👨🏼 → 👨🏻‍🤝‍👨🏼

  • 👨 + ❤️ + 👨 → 👨‍❤️‍👨

  • 👨 + ❤️ + 💋 + 👨 → 👨‍❤️‍💋‍👨

  • 👨 + 👨 + 👧 → 👨‍👨‍👧

  • 👨 + 👨 + 👧 + 👧 → 👨‍👨‍👧‍👧



可惜,有些 emoji 不是通过 ZWJ 组全 emoji 实现的,可能是因为没有赶上 ZWJ 定义的时机




  • 🌂 + 🌧 ≠ ☔️

  • 💄 + 👄 ≠ 💋

  • 🐴 + 🌈 ≠ 🦄

  • 👁 + 👁 ≠ 👀

  • 👨 + 💀 ≠ 🧟

  • 👩 + 🔍 ≠ 🕵️‍♀️

  • 🦵 + 🦵 + 💪 + 💪 + 👂 + 👂 + 👃 + 👅 + 👀 + 🧠 ≠ 🧍



旗帜·双字母连字


Unicode 中包含国旗符号,每个国旗也没有分配独立的码点,而是由双字符连字(ligature)来表示。(但 Windows 平台因为某些原因不支持显示,如果你是用 Windows 平台的浏览器阅读本文,只能说抱歉了)



  • 🇺 + 🇳 = 🇺🇳

  • 🇷 + 🇺 = 🇷🇺

  • 🇮 + 🇸 = 🇮🇸

  • 🇿 + 🇦 = 🇿🇦

  • 🇯 + 🇵 = 🇯🇵


这里的🇦 ~ 🇿不是字母,而是地区标识符,对应的码点是U+1F1E6~U+1F1FF,可以随意复制并组合,如果是合法的组合会显示成一个国家的旗帜。你可以在 MacOS 的 FontBook 中打开 Apple Color Emoji 查看到这些码点以及各个地区的旗帜符号
image.png image.png
完整地区标识符如下,你可以动手组合试一试:
🇦 🇧 🇨 🇩 🇪 🇫 🇬 🇭 🇮 🇯 🇰 🇱 🇲 🇳 🇴 🇵 🇶 🇷 🇸 🇹 🇺 🇻 🇼 🇽 🇾 🇿


标签序列


在 Unicode 中称作 Emoji Tag Sequence。在 Unicode 中U+E0020~ U+E007F 95 个码点表示的是Unicode 中不可见的控制符号,其中从E0061~E007A的 26 个码点分别表示小写的拉丁字符(不是常规的拉丁字母,而是 emoji 相关的控制字符),对应关系如下:




  • U+E0061 - TAG LATIN SMALL LETTER A

  • U+E0062 - TAG LATIN SMALL LETTER B



...




  • U+E007A - TAG LATIN SMALL LETTER Z



前文的双字母连字机制支持将两个地区标识符连接在一起表示一个旗帜符号。标签序列与之类似,是 Unicode 中定义的一种更复杂的连接方式,格式是:基础emoji+ 一串拉丁标签字符(U+E0061~U+E007A) + 结束符(U+E007F)
如:🏴 + gbeng + U+E007F = 🏴󠁧󠁢󠁥󠁮󠁧󠁿
其中 🏴 是基础 emoji U+1F3F4,_gbeng _分别代表对应的拉丁控制字符: g(U+E0067)b(U+E0062)e(U+E0065) n(U+E006E)g(U+E0067)U+E007F表示结束符,全称是 TAG CANCEL


/**
* 根据地区缩写返回对应的emoji
* 如:flag('gbeng') -> 🏴󠁧󠁢󠁥󠁮󠁧󠁿
*/

function flag(letterStr) {
const BASE_FLAG = '🏴';
const TAG_CANCEL = String.fromCodePoint(0xE007F);

// 将普通字母字符序列转换为"标签拉丁字符"序列
const tagLatinStr = (letterStr.toLowerCase().split('').map(letter => {
const codePoint = letter.charCodeAt(0) - 'a'.charCodeAt(0) + 0xE0061;
return String.fromCodePoint(codePoint);
})).join('');


return BASE_FLAG + tagLatinStr + TAG_CANCEL;
}

目前用这种方式表示的 emoji 共有三个



  • 🏴 + gbeng + U+E007F = 🏴󠁧󠁢󠁥󠁮󠁧󠁿 英格兰旗帜,完整序列:1F3F4 E0067 E0062 E0065 E006E E0067 E007F

  • 🏴 + gbsct + U+E007F = 🏴󠁧󠁢󠁳󠁣󠁴󠁿 苏格兰旗帜,完整序列:1F3F4 E0067 E0062 E0073 E0063 E0074 E007F

  • 🏴 + gbwls + U+E007F = 🏴󠁧󠁢󠁷󠁬󠁳󠁿 威尔士旗帜,完整序列:1F3F4 E0067 E0062 E0077 E006C E0073 E007F


键位符


共有 12 个键位符 #️⃣ *️⃣ 0️⃣ 1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ 6️⃣ 7️⃣ 8️⃣ 9️⃣,规则是这样的:井号、星号和数字,加 U+FE0F 变成 emoji,再加上U+20E3变成带方框的键位符。







      • FE0F + 20E3 = *️⃣






  • + FE0F + 20E3 = #️⃣



  • 0 + FE0F + 20E3 = 0️⃣

  • ...


U+FE0F是前文提到的变体选择符中的VS-16,表示显示为 emoji 形态。JavaScript 中'\u0030'表示数字'0', '\u0030\ufe0f'则表示它的 emoji 变体,两者在 zsh 的 console 中显示效果不同,.length的值也不同。
image.png image.png


小结


一共有七种 emoji 造字法



  1. 基础emoji,单个码点表示一个emoji 🧛 U+1F9DB

  2. 单个码点 + 变体选择符 ⚛️ = ⚛︎ U+26A0 + U+FE0F

  3. 皮肤修饰符 🤵🏽 = 🤵 U+1F935 + 🏽 U+1F3FD

  4. **ZWJ连接符 ** 👨‍💻 = 👨 + ZWJ + 💻

  5. 旗帜符号 🇨🇳 = 🇨 + 🇳

  6. **标签序列 ** 🏴󠁧󠁢󠁳󠁣󠁴󠁿 = 🏴 + gbsct + U+E007F

  7. **键位序列 ** *️⃣ = * + U+FE0F + U+20E3


前四种方法也可以组合使用,可构造非常复杂的 emoji



U+1F6B5 🚵 个人山地骑行



  • U+1F3FB 浅色皮肤

  • U+200D ZWJ

  • U+2640 ♀️女性标志

  • U+FE0F 变体标志
    = 🚵🏻‍♀️ 浅色皮肤的女性山地骑行



/**
* 显示一个字符种所有的Unicode码点
*/

function codePoints(str) {
const result = [];
for(let i = 0; i < str.length; i ++) {
result.push(str.codePointAt(i).toString(16).toUpperCase());
}
return result;
}
codePoints('🚵🏻‍♀️') => ['1F6B5', 'DEB5', '1F3FB', 'DFFB', '200D', '2640', 'FE0F']

如何在代码中正确处理 emoji?


emoji 引入的问题


'中国人123'.length = 6'工作中👨‍💻'.length = 8
emoji 给编程带来的主要问题是视觉上看到的字符长度(后文称作视觉 length)与代码中获取的长度(后文称作技术 length)不相同,使得字符串截取等操作返回非预期内的结果,如:
'工作中👨‍💻'.substr(0,5) => '工作中👨''工作中👨‍💻'.substr(5)' => '‍💻'


本质上在 emoji 出现之前 Unicode 编码就遇到了该问题,只不过 emoji 的普及让该问题更普遍。有的 emoji 长度为 1,有的长度可以达到 15。问题的根源是 Unicode 中可以用多个码点表示一个 emoji,如果所有 emoji 都用一个 Unicode 码点表示就不存在该问题。
image.png


解法:视觉 length VS. 技术 length


解法显而易见,只要能将字符串中所有的字符元素按照视觉上看到的情况准确拆分,即:准确拆解字符串中的所有字素集
下述伪代码是要实现的效果,很多开源工具库就在做同样的事情,搜:Grapheme Cluster 即可。找到一个JavaScript版的grapheme-splitter,但是数据已经过时(勿用)。


const vs = new VisualString('工作中👨‍💻');
// vs.length => 4; // 视觉长度
// vs.physicalLength => 8; // 字符串长度
// vs[0] => 工
// vs[3] => 👨‍💻 // 按照所见即所得的方式拆分字符

// 字素集方法
// vs.substr(3,1) => 👨‍💻 // 截取字符

// 字素集属性
// vs[3].physicalLength => 5 // 物理长度
// vs[3].isEmoji => true // 是否是emoji

我们将产出工具库中将要提供这些能力



  1. 判断一个字符串中是否包含 emoji

  2. 将一个字符串准确拆分成若干个字素集

    • 每个字素集包含这些属性:isEmojiphysicalLength



  3. 按照字素集对字符串做截取操作

    • 基础截取: new VisualString('👨123👨‍💻').substr(1, 4) => '123👨‍💻'

    • 限定物理长度截取:new VisualString('👨123👨‍💻').substr(1, 4, 6) => '123',最后一个参数6代表最大物理长度,其中'123👨‍💻'.length = 8,如果限定最大物理长度6则只能截取到'123'备注:在产品体验上我们遵循“所见即所得”,但是在后端系统中传输和存储时候要遵循物理长度的限制,因此需要提供限定物理长度的截取能力。




版本兼容问题


如果 A 向 B 发送了一个组合 emoji「工作👨‍💻123」,B 的系统或软件中版本低(兼容的 Unicode 版本低)不支持该组合 emoji,看到的可能会是「工作👨💻123」。
用看到的是👨‍💻还是👨💻取决于用户的操作系统、软件和字体,我们提供的 JS 库无法感知到用户最终看到的是什么。我们提供的 JS 库会按照最新 Unicode 规范实现,无论用户看到的是什么都会把它当成一个字符(准确地说是字素集),即:
const vs = new VisualString('工作👨💻123'); vs.length => 6; vs[2] => '👨💻'
有办法可以一定程度上解决上述问题,但是我们觉得可能不解决才是正确的做法。


一个彩蛋


最后希望你使用 emoji 愉快 😄
发现 emoji 的维护者彻底贯彻「众生平等」,除了推出了不同肤色的 emoji 外,竟还设计了一个 Pregnant Man :)
image.png 🤰🫃🫄🏼
以上是分别是 woman、man、person,emoji 的新趋势是设计中性的 emoji




参考



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

一个神奇的小工具,让URL地址都变成了"ooooooooo"

web
发现一个很有创意的小工具网站,如封面图所示功能很简单,就是将一个URL地址转换为都是 ooooooooo 的样子,通过转换后的地址访问可以转换回到原始地址,简单流程如下图所示。转换的逻辑有点像短链平台一样,只不过这个是将你的URL地址变的很长长长长,但是看着都...
继续阅读 »

发现一个很有创意的小工具网站,如封面图所示功能很简单,就是将一个URL地址转换为都是 ooooooooo 的样子,通过转换后的地址访问可以转换回到原始地址,简单流程如下图所示。转换的逻辑有点像短链平台一样,只不过这个是将你的URL地址变的很长长长长,但是看着都是 ooooooooo,很好奇是如何实现的,所以查阅了源码,本文解读其核心实现逻辑,很有趣且巧妙的实现了这个功能。



前置知识点


在正式开始前,先了解一些需要学习的知识点。因为涉及到两个地址其实也就是字符串之间的转换,会用到一些编码和解码的能力。


将字符转为utf8数组,转换后的每个字符都有一个特定的唯一数值,比如 http 转换后的 utf8 格式数组即是 [104, 116, 116, 112]


    toUTF8Array(str) {
var utf8 = [];
for (var i = 0; i < str.length; i++) {
var charcode = str.charCodeAt(i);
if (charcode < 0x80) utf8.push(charcode);
else if (charcode < 0x800) {
utf8.push(0xc0 | (charcode >> 6),
0x80 | (charcode & 0x3f));
}
else if (charcode < 0xd800 || charcode >= 0xe000) {
utf8.push(0xe0 | (charcode >> 12),
0x80 | ((charcode >> 6) & 0x3f),
0x80 | (charcode & 0x3f));
}
else {
i++;
charcode = ((charcode & 0x3ff) << 10) | (str.charCodeAt(i) & 0x3ff)
utf8.push(0xf0 | (charcode >> 18),
0x80 | ((charcode >> 12) & 0x3f),
0x80 | ((charcode >> 6) & 0x3f),
0x80 | (charcode & 0x3f));
}
}
console.log(utf8, 'utf8');
return utf8;
}

上面是编码,对应下面的则是解码,将utf8数组转换为字符串,比如 [99, 111, 109] 转换后的 utf8 格式数组即是 com


    Utf8ArrayToStr(array) {
var out, i, len, c;
var char2, char3;

out = "";
len = array.length;
i = 0;
while (i < len) {
c = array[i++];
switch (c >> 4) {
case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7:
// 0xxxxxxx
out += String.fromCharCode(c);
break;
case 12: case 13:
// 110x xxxx 10xx xxxx
char2 = array[i++];
out += String.fromCharCode(((c & 0x1F) << 6) | (char2 & 0x3F));
break;
case 14:
// 1110 xxxx 10xx xxxx 10xx xxxx
char2 = array[i++];
char3 = array[i++];
out += String.fromCharCode(((c & 0x0F) << 12) |
((char2 & 0x3F) << 6) |
((char3 & 0x3F) << 0));
break;
}
}

return out;
}

将 Number 对象以 4 进制的形式表示为字符串,toString 用的比较多,但是里面传入参数的场景比较少,这个参数 radix 是一个可选的参数,用于指定转换的进制数,范围为 2 ~ 36,如果未传入该参数,则默认使用 10 进制。


n.toString(4)

在字符串左侧填充指定字符,直到字符串达到指定长度。基本语法为 str.padStart(targetLength [, padString])



  • targetLength:必需,指定期望字符串的最小长度,如果当前字符串小于这个长度,则会在左侧使用 padString 进行填充,直到字符串达到指定长度。

  • padString:可选,指定用于填充字符串的字符,默认为 " "(空格)。


str.padStart(4, '0')

URL 编码/解码


下面正式开始URL编码的逻辑,核心的逻辑如下:



  • 转换为utf8数组

  • 转换为4进制并左侧补0到4位数

  • 分割转换为字符串数组

  • 映射到o的不同形式

  • 再次拼接为字符串,即转换完成后的URL


// 获取utf8数组
let unversioned = this.toUTF8Array(url)
// 转换为base 4字符串
// padstart非常重要!否则会丢失前导0
.map(n => n.toString(4).padStart(4, "0"))
// 转换为字符数组
.join("").split("")
// 映射到o的不同形式
.map(x => this.enc[parseInt(x)])
// 连接成单个字符串
.join("")

上面有两个关键点解释一下,首先映射到o的不同形式这个是什么意思呢?其实转换后的o并不是一种“o”,而是4种,只不过我们肉眼看到的效果很像,通过 encodeURI 转换后的字符可以看出来。


encodeURI('o-ο-о-ᴏ')
// o-%CE%BF-%D0%BE-%E1%B4%8F

这里其实也解释了为什么上面为什么是转换为4进制和左侧补0到四位数。因为上面代码定义的 this.enc 如下,因为总共只有四种“o”,4进制只会产生0,1,2,3,这样就可以将转换后的utf8字符一一对应上这几种特殊的“o”。


enc = ["o", "ο", "о", "ᴏ"] 

最后的效果举例转换 http 这个字符:



  • 转换为utf8数组:[ 104, 116, 116, 112 ]

  • 转换为4进制并左侧补0到4位数:['1220', '1310', '1310', '1300']

  • 分割转换为字符串数组:['1', '2', '2', '0', '1', '3', '1', '0', '1', '3', '1', '0', '1', '3', '0', '0']

  • 映射到o的不同形式:[ 'ο', 'о', 'о', 'o', 'ο', 'ᴏ', 'ο', 'o', 'ο', 'ᴏ', 'ο', 'o', 'ο', 'ᴏ', 'o', 'o' ]

  • 再次拼接为字符串,即转换完成后的URL:οооoοᴏοoοᴏοoοᴏoo


到此整个转换编码的过程就结束了,看完后是不是觉得设计的很不错,编码完后就是解码,解码就是将上面的过程倒序来一遍,恢复到最原始的URL地址。这里要注意一点的是每次解析4个字符且parseInt以4进制的方式进行解析。


// 获取url的base 4字符串表示
let b4str = ooo.split("").map(x => this.dec[x]).join("")

let utf8arr = []
// 每次解析4个字符
// 记住添加前导0的填充
for (let i = 0; i < b4str.length; i += 4)
utf8arr.push(parseInt(b4str.substring(i, i + 4), 4))
// 返回解码后的字符串
return this.Utf8ArrayToStr(utf8arr)

最后


到此就核心实现代码就分享结束了,看完是不是感觉并没有很复杂,基于此设计或许可以延伸出其他的字符效果,有兴趣的也可以试试看。将转码后的地址分享给你的朋友们一定会带来不一样的惊喜。


以下将官网源码运行在码上掘金,方便大家体验。



下面是我转换的一个AI小工具地址,点击看看效果吧~


ooooooooooooooooooooooo.ooo/ooooοооoοᴏο…


看完本文如果觉得有用,记得点个赞支持,收藏起来说不定哪天就用上啦~


专注前端开发,分享前端相关技术干货,公众号:南城大前端(ID: nanchengfe)


作者:南城FE
来源:juejin.cn/post/7225573912670191677
收起阅读 »

十分钟,带你了解 Vue3 的新写法

web
最近因为项目需要,不得不学习一下 Vue3。于是花了 4 个小时,把 Vue3 过了一遍。现在我来带你快速了解 Vue3 的写法。 本文的目的,是为了让已经有 Vue2 开发经验的 人 ,快速掌握 Vue3 的写法。 因此, 本篇假定你已经掌握 Vue 的核心...
继续阅读 »

最近因为项目需要,不得不学习一下 Vue3。于是花了 4 个小时,把 Vue3 过了一遍。现在我来带你快速了解 Vue3 的写法。


本文的目的,是为了让已经有 Vue2 开发经验的 ,快速掌握 Vue3 的写法。


因此, 本篇假定你已经掌握 Vue 的核心内容 ,只为你介绍编写 Vue3 代码,需要了解的内容。


一、Vue3 里 script 的三种写法


首先,Vue3 新增了一个叫做组合式 api 的东西,英文名叫 Composition API。因此 Vue3 的 script 现在支持三种写法,


1、最基本的 Vue2 写法


<template>
<div>{{ count }}</div>
<button @click="onClick">
增加 1
</button>
</template>

<script>
export default {
data() {
return {
count: 1,
};
},
methods: {
onClick() {
this.count += 1;
},
},
}
</script>

2、setup() 属性


<template>
<div>{{ count }}</div>
<button @click="onClick">
增加 1
</button>
</template>

<script>
import { ref } from 'vue';
export default {

// 注意这部分
setup() {
let count = ref(1);
const onClick = () => {
count.value += 1;
};
return {
count,
onClick,
};
},

}
</script>

3、<script setup>


<template>
<div>{{ count }}</div>
<button @click="onClick">
增加 1
</button>
</template>

<script setup>
import { ref } from 'vue';

const count = ref(1);
const onClick = () => {
count.value += 1;
};
</script>

正如你看到的那样,无论是代码行数,还是代码的精简度,<script setup> 的方式是最简单的形式。


如果你对 Vue 很熟悉,那么,我推荐你使用 <script setup> 的方式。


这种写法,让 Vue3 成了我最喜欢的前端框架。


如果你还是前端新人,那么,我推荐你先学习第一种写法。


因为第一种写法的学习负担更小,先学第一种方式,掌握最基本的 Vue 用法,然后再根据我这篇文章,快速掌握 Vue3 里最需要关心的内容。


第一种写法,跟过去 Vue2 的写法是一样的,所以我们不过多介绍。


第二种写法,所有的对象和方法都需要 return 才能使用,太啰嗦。除了旧项目,可以用这种方式体验 Vue3 的新特性以外,我个人不建议了解这种方式。反正我自己暂时不打算精进这部分。


所以,接下来,我们主要介绍的,也就是 <script setup> ,这种写法里需要了解的内容。


注意: <script setup> 本质上是第二种写法的语法糖,掌握了这种写法,其实第二种写法也基本上就会了。(又多了一个不学第二种写法的理由)。


二、如何使用 <script setup> 编写组件


学习 Vue3 并不代表你需要新学习一个技术,Vue3 的底层开发思想,跟 Vue2 是没有差别的。


V3 和 V2 的区别就像是,你用不同的语言或者方言说同一句话。


所以我们需要关心的,就是 Vue2 里的内容,怎么用 Vue3 的方式写出来。


1、data——唯一需要注意的地方


整个 data 这一部分的内容,你只需要记住下面这一点。


以前在 data 中创建的属性,现在全都用 ref() 声明。


template 中直接用,在 script 中记得加 .value


在开头,我就已经写了一个简单的例子,我们直接拿过来做对比。


1)写法对比


 // Vue2 的写法

<template>
<div>{{ count }}</div>
<button @click="onClick">
增加 1
</button>
</template>

<script>
export default {
data() {
return {
count: 1,
};
},
methods: {
onClick() {
this.count += 1;
},
},
}
</script>

 // Vue3 的写法

<template>
<div>{{ count }}</div>
<button @click="onClick">
增加 1
</button>
</template>

<script setup>
import { ref } from 'vue';

// 用这种方式声明
const count = ref(1);

const onClick = () => {
// 使用的时候记得 .value
count.value += 1;
};
</script>

2)注意事项——组合式 api 的心智负担


a、ref 和 reactive

Vue3 里,还提供了一个叫做 reactiveapi


但是我的建议是,你不需要关心它。绝大多数场景下,ref 都够用了。


b、什么时候用 ref() 包裹,什么时候不用。

要不要用ref,就看你的这个变量的值改变了以后,页面要不要跟着变。


当然,你可以完全不需要关心这一点,跟过去写 data 一样就行。


只不过这样做,你在使用的时候,需要一直 .value


c、不要解构使用

在使用时,不要像下面这样去写,会丢失响应性。


也就是会出现更新了值,但是页面没有更新的情况


// Vue3 的写法
<template>
<div>{{ count }}</div>
<button @click="onClick">
增加 1
</button>
</template>

<script setup>
import { ref } from 'vue';

const count = ref(1);
const onClick = () => {
// 不要这样写!!
const { value } = count;
value += 1;
};
</script>

注意: 学习 Vue3 就需要考虑像这样的内容,徒增了学习成本。实际上这些心智负担,在学习的过程中,是可以完全不需要考虑的。


这也是为什么我推荐新人先学习 Vue2 的写法。


2、methods


声明事件方法,我们只需要在 script 标签里,创建一个方法对象即可。


剩下的在 Vue2 里是怎么写的,Vue3 是同样的写法。


// Vue2 的写法
<template>
<div @click="onClick">
这是一个div
</div>
</template>

<script>
export default {
methods: {
onClick() {
console.log('clicked')
},
},
}
</script>

// Vue3 的写法
<template>
<div @click="onClick">
这是一个div
</div>
</template>

<script setup>

// 注意这部分
const onClick = () => {
console.log('clicked')
}

</script>

3、props


声明 props 我们可以用 defineProps(),具体写法,我们看代码。


1)写法对比


// Vue2 的写法
<template>
<div>{{ foo }}</div>
</template>

<script>
export default {
props: {
foo: String,
},
created() {
console.log(this.foo);
},
}
</script>

// Vue3 的写法
<template>
<div>{{ foo }}</div>
</template>

<script setup>

// 注意这里
const props = defineProps({
foo: String
})

// 在 script 标签里使用
console.log(props.foo)
</script>

2)注意事项——组合式 api 的心智负担


使用 props 时,同样注意不要使用解构的方式。


<script setup>
const props = defineProps({
foo: String
})

// 不要这样写
const { foo } = props;
console.log(foo)
</script>

4、emits 事件


props 相同,声明 emits 我们可以用 defineEmits(),具体写法,我们看代码。


// Vue2 的写法
<template>
<div @click="onClick">
这是一个div
</div>
</template>

<script>
export default {

emits: ['click'], // 注意这里
methods: {
onClick() {
this.$emit('click'); // 注意这里
},
},

}
</script>

// Vue3 的写法
<template>
<div @click="onClick">
这是一个div
</div>
</template>

<script setup>

// 注意这里
const emit = defineEmits(['click']);

const onClick = () => {
emit('click') // 注意这里
}

</script>

5、computed


直接上写法对比。


// Vue2 的写法
<template>
<div>
<span>{{ value }}</span>
<span>{{ reversedValue }}</span>
</div>
</template>

<script>
export default {
data() {
return {
value: 'this is a value',
};
},
computed: {
reversedValue() {
return value
.split('').reverse().join('');
},
},
}
</script>

// Vue3 的写法
<template>
<div>
<span>{{ value }}</span>
<span>{{ reversedValue }}</span>
</div>
</template>

<script setup>
import {ref, computed} from 'vue'
const value = ref('this is a value')

// 注意这里
const reversedValue = computed(() => {
// 使用 ref 需要 .value
return value.value
.split('').reverse().join('');
})

</script>

6、watch


这一部分,我们需要注意一下了,Vue3 中,watch 有两种写法。一种是直接使用 watch,还有一种是使用 watchEffect


两种写法的区别是:




  • watch 需要你明确指定依赖的变量,才能做到监听效果。




  • watchEffect 会根据你使用的变量,自动的实现监听效果。




1)直接使用 watch


// Vue2 的写法
<template>
<div>{{ count }}</div>
<div>{{ anotherCount }}</div>
<button @click="onClick">
增加 1
</button>
</template>

<script>
export default {
data() {
return {
count: 1,
anotherCount: 0,
};
},
methods: {
onClick() {
this.count += 1;
},
},
watch: {
count(newValue) {
this.anotherCount = newValue - 1;
},
},
}
</script>

// Vue3 的写法
<template>
<div>{{ count }}</div>
<div>{{ anotherCount }}</div>
<button @click="onClick">
增加 1
</button>
</template>

<script setup>
import { ref, watch } from 'vue';

const count = ref(1);
const onClick = () => {
count.value += 1;
};

const anotherCount = ref(0);

// 注意这里
// 需要在这里,
// 明确指定依赖的是 count 这个变量
watch(count, (newValue) => {
anotherCount.value = newValue - 1;
})

</script>

2)使用 watchEffect


// Vue2 的写法
<template>
<div>{{ count }}</div>
<div>{{ anotherCount }}</div>
<button @click="onClick">
增加 1
</button>
</template>

<script>
export default {
data() {
return {
count: 1,
anotherCount: 0,
};
},
methods: {
onClick() {
this.count += 1;
},
},
watch: {
count(newValue) {
this.anotherCount = newValue - 1;
},
},
}
</script>

// Vue3 的写法
<template>
<div>{{ count }}</div>
<div>{{ anotherCount }}</div>
<button @click="onClick">
增加 1
</button>
</template>

<script setup>
import { ref, watchEffect } from 'vue';

const count = ref(1);
const onClick = () => {
count.value += 1;
};

const anotherCount = ref(0);

// 注意这里
watchEffect(() => {
// 会自动根据 count.value 的变化,
// 触发下面的操作
anotherCount.value = count.value - 1;
})

</script>

7、生命周期


Vue3 里,除了将两个 destroy 相关的钩子,改成了 unmount,剩下的需要注意的,就是在 <script setup> 中,不能使用 beforeCreatecreated 两个钩子。


如果你熟悉相关的生命周期,只需要记得在 setup 里,用 on 开头,加上大写首字母就行。


// 选项式 api 写法
<template>
<div></div>
</template>

<script>
export default {
beforeCreate() {},
created() {},

beforeMount() {},
mounted() {},

beforeUpdate() {},
updated() {},

// Vue2 里叫 beforeDestroy
beforeUnmount() {},
// Vue2 里叫 destroyed
unmounted() {},

// 其他钩子不常用,所以不列了。
}
</script>

// 组合式 api 写法
<template>
<div></div>
</template>


<script setup>
import {
onBeforeMount,
onMounted,

onBeforeUpdate,
onUpdated,

onBeforeUnmount,
onUnmounted,
} from 'vue'

onBeforeMount(() => {})
onMounted(() => {})

onBeforeUpdate(() => {})
onUpdated(() => {})

onBeforeUnmount(() => {})
onUnmounted(() => {})
</script>

三、结语


好了,对于快速上手 Vue3 来说,以上内容基本已经足够了。


这篇文章本身不能做到帮你理解所有 Vue3 的内容,但是能帮你快速掌握 Vue3 的写法。


如果想做到对 Vue3 的整个内容心里有数,还需要你自己多看看 V

作者:Wetoria
来源:juejin.cn/post/7225267685763907621
ue3 的官方文档。

收起阅读 »

九个超级好用的 Javascript 技巧

web
作者:shichuan 文末彩蛋等你揭晓 🤫 前言 在实际的开发工作过程中,积累了一些常见又超级好用的 Javascript 技巧和代码片段,包括整理的其他大神的 JS 使用技巧,今天筛选了 9 个,以供大家参考。 1、动态加载 JS 文件 在一些特殊的场景...
继续阅读 »

作者:shichuan


文末彩蛋等你揭晓 🤫



前言


在实际的开发工作过程中,积累了一些常见又超级好用的 Javascript 技巧和代码片段,包括整理的其他大神的 JS 使用技巧,今天筛选了 9 个,以供大家参考。


1、动态加载 JS 文件


在一些特殊的场景下,特别是一些库和框架的开发中,我们有时会去动态的加载 JS 文件并执行,下面是利用 Promise 进行了简单的封装。


function loadJS(files, done) {
// 获取head标签
const head = document.getElementsByTagName('head')[0];
Promise.all(files.map(file => {
return new Promise(resolve => {
// 创建script标签并添加到head
const s = document.createElement('script');
s.type = "text/javascript";
s.async = true;
s.src = file;
// 监听load事件,如果加载完成则resolve
s.addEventListener('load', (e) => resolve(), false);
head.appendChild(s);
});
})).then(done); // 所有均完成,执行用户的回调事件
}

loadJS(["test1.js", "test2.js"], () => {
// 用户的回调逻辑
});

上面代码核心有两点,一是利用 Promise 处理异步的逻辑,而是利用 script 标签进行 js 的加载并执行。


2、实现模板引擎


下面示例用了极少的代码实现了动态的模板渲染引擎,不仅支持普通的动态变量的替换,还支持包含 for 循环,if 判断等的动态的 JS 语法逻辑,具体实现逻辑在笔者另外一篇文章《面试官问:你能手写一个模版引擎吗?》做了非常详详尽的说明,感兴趣的小伙伴可自行阅读。


// 这是包含了js代码的动态模板
var template =
'My avorite sports:' +
'<%if(this.showSports) {%>' +
'<% for(var index in this.sports) { %>' +
'<a><%this.sports[index]%></a>' +
'<%}%>' +
'<%} else {%>' +
'<p>none</p>' +
'<%}%>';
// 这是我们要拼接的函数字符串
const code = `with(obj) {
var r=[];
r.push("My avorite sports:");
if(this.showSports) {
for(var index in this.sports) {
r.push("<a>");
r.push(this.sports[index]);
r.push("</a>");
}
} else {
r.push("<span>none</span>");
}
return r.join("");
}`

// 动态渲染的数据
const options = {
sports: ["swimming", "basketball", "football"],
showSports: true
}
// 构建可行的函数并传入参数,改变函数执行时this的指向
result = new Function("obj", code).apply(options, [options]);
console.log(result);

3、利用 reduce 进行数据结构的转换


有时候前端需要对后端传来的数据进行转换,以适配前端的业务逻辑,或者对组件的数据格式进行转换再传给后端进行处理,而 reduce 是一个非常强大的工具。


const arr = [
{ classId: "1", name: "张三", age: 16 },
{ classId: "1", name: "李四", age: 15 },
{ classId: "2", name: "王五", age: 16 },
{ classId: "3", name: "赵六", age: 15 },
{ classId: "2", name: "孔七", age: 16 }
];

groupArrayByKey(arr, "classId");

function groupArrayByKey(arr = [], key) {
return arr.reduce((t, v) => (!t[v[key]] && (t[v[key]] = []), t[v[key]].push(v), t), {})
}

很多很复杂的逻辑如果用 reduce 去处理,都非常的简洁。


4、添加默认值


有时候一个方法需要用户传入一个参数,通常情况下我们有两种处理方式,如果用户不传,我们通常会给一个默认值,亦或是用户必须要传一个参数,不传直接抛错。


function double() {
return value *2
}

// 不传的话给一个默认值0
function double(value = 0) {
return value * 2
}

// 用户必须要传一个参数,不传参数就抛出一个错误

const required = () => {
throw new Error("This function requires one parameter.")
}
function double(value = required()) {
return value * 2
}

double(3) // 6
double() // throw Error

listen 方法用来创建一个 NodeJS 的原生 http 服务并监听端口,在服务的回调函数中创建 context,然后调用用户注册的回调函数并传递生成的 context。下面我们以前看下 createContext 和 handleRequest 的实现。


5、函数只执行一次


有些情况下我们有一些特殊的场景,某一个函数只允许执行一次,或者绑定的某一个方法只允许执行一次。


export function once (fn) {
// 利用闭包判断函数是否执行过
let called = false
return function () {
if (!called) {
called = true
fn.apply(this, arguments)
}
}
}

6、实现 Curring


JavaScript 的柯里化是指将接受多个参数的函数转换为一系列只接受一个参数的函数的过程。这样可以更加灵活地使用函数,减少重复代码,并增加代码的可读性。


function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
} else {
return function(...args2) {
return curried.apply(this, args.concat(args2));
};
}
};
}

function add(x, y) {
return x + y;
}

const curriedAdd = curry(add);

console.log(curriedAdd(1)(2)); // 输出 3
console.log(curriedAdd(1, 2)); // 输出 3

通过柯里化,我们可以将一些常见的功能模块化,例如验证、缓存等等。这样可以提高代码的可维护性和可读性,减少出错的机会。


7、实现单例模式


JavaScript 的单例模式是一种常用的设计模式,它可以确保一个类只有一个实例,并提供对该实例的全局访问点,在 JS 中有广泛的应用场景,如购物车,缓存对象,全局的状态管理等等。


let cache;
class A {
// ...
}

function getInstance() {
if (cache) return cache;
return cache = new A();
}

const x = getInstance();
const y = getInstance();

console.log(x === y); // true

8、实现 CommonJs 规范


CommonJS 规范的核心思想是将每个文件都看作一个模块,每个模块都有自己的作用域,其中的变量、函数和对象都是私有的,不能被外部访问。要访问模块中的数据,必须通过导出(exports)和导入(require)的方式。


// id:完整的文件名
const path = require('path');
const fs = require('fs');
function Module(id){
// 用来唯一标识模块
this.id = id;
// 用来导出模块的属性和方法
this.exports = {};
}

function myRequire(filePath) {
// 直接调用Module的静态方法进行文件的加载
return Module._load(filePath);
}

Module._cache = {};
Module._load = function(filePath) {
// 首先通过用户传入的filePath寻址文件的绝对路径
// 因为再CommnJS中,模块的唯一标识是文件的绝对路径
const realPath = Module._resoleveFilename(filePath);
// 缓存优先,如果缓存中存在即直接返回模块的exports属性
let cacheModule = Module._cache[realPath];
if(cacheModule) return cacheModule.exports;
// 如果第一次加载,需要new一个模块,参数是文件的绝对路径
let module = new Module(realPath);
// 调用模块的load方法去编译模块
module.load(realPath);
return module.exports;
}

// node文件暂不讨论
Module._extensions = {
// 对js文件处理
".js": handleJS,
// 对json文件处理
".json": handleJSON
}

function handleJSON(module) {
// 如果是json文件,直接用fs.readFileSync进行读取,
// 然后用JSON.parse进行转化,直接返回即可
const json = fs.readFileSync(module.id, 'utf-8')
module.exports = JSON.parse(json)
}

function handleJS(module) {
const js = fs.readFileSync(module.id, 'utf-8')
let fn = new Function('exports', 'myRequire', 'module', '__filename', '__dirname', js)
let exports = module.exports;
// 组装后的函数直接执行即可
fn.call(exports, exports, myRequire, module,module.id,path.dirname(module.id))
}

Module._resolveFilename = function (filePath) {
// 拼接绝对路径,然后去查找,存在即返回
let absPath = path.resolve(__dirname, filePath);
let exists = fs.existsSync(absPath);
if (exists) return absPath;
// 如果不存在,依次拼接.js,.json,.node进行尝试
let keys = Object.keys(Module._extensions);
for (let i = 0; i < keys.length; i++) {
let currentPath = absPath + keys[i];
if (fs.existsSync(currentPath)) return currentPath;
}
};

Module.prototype.load = function(realPath) {
// 获取文件扩展名,交由相对应的方法进行处理
let extname = path.extname(realPath)
Module._extensions[extname](this)
}

上面对 CommonJs 规范进行了简单的实现,核心解决了作用域的隔离,并提供了 Myrequire 方法进行方法和属性的加载,对于上面的实现,笔者专门有一篇文章《38 行代码带你实现 CommonJS 规范》进行了详细的说明,感兴趣的小伙伴可自行阅读。


9、递归获取对象属性


如果让我挑选一个用的最广泛的设计模式,我会选观察者模式,如果让我挑一个我所遇到的最多的算法思维,那肯定是递归,递归通过将原始问题分割为结构相同的子问题,然后依次解决这些子问题,组合子问题的结果最终获得原问题的答案。


const user = {
info: {
name: "张三",
address: { home: "Shaanxi", company: "Xian" },
},
};

// obj是获取属性的对象,path是路径,fallback是默认值
function get(obj, path, fallback) {
const parts = path.split(".");
const key = parts.shift();
if (typeof obj[key] !== "undefined") {
return parts.length > 0 ?
get(obj[key], parts.join("."), fallback) :
obj[key];
}
// 如果没有找到key返回fallback
return fallback;
}

console.log(get(user, "info.name")); // 张三
console.log(get(user, "info.address.home")); // Shaanxi
console.log(get(user, "info.address.company")); // Xian
console.log(get(user, "info.address.abc", "fallback")); // fallback

上面挑选了 9 个笔者认为比较有用的 JS 技巧,希望对大家有所帮助。


🎁 文末彩蛋 >>


码上掘金编程比赛火热进行中,同时为大家推出「报名礼 & 完赛奖」活动~

报名即有机会瓜分上百万掘金矿石奖池!提交作品更可参与精美奖品的抽取哦!


🎁 抽奖攻略请戳这里

🎡 更多大赛特别活动请看这里




尾部关注.gif


扫码关注公众号 👆 追更不迷路


作者:字节前端
来源:juejin.cn/post/7223938976158957624
收起阅读 »

用CSS给健身的女朋友做一个喝水记录本

web
我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第2篇文章,点击查看活动详情 前言 事情是这样的,由于七八月份的晚上时不时就坐在地摊上开始了喝酒撸串的一系列放肆的长肉肉项目。 这不,前段时间女朋友痛下决心(心血来潮)地就去报了一个健身的私教班,按照教练给...
继续阅读 »

我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第2篇文章,点击查看活动详情


前言


事情是这样的,由于七八月份的晚上时不时就坐在地摊上开始了喝酒撸串的一系列放肆的长肉肉项目。
这不,前段时间女朋友痛下决心(心血来潮)地就去报了一个健身的私教班,按照教练给的饮食计划中,其中有一项是每天需要喝 2.6L 的水来促进体内的新陈代谢。
作为伴侣肯定要十分支持的呀,不过因为平时工作也是十分费脑筋的,不会专门去记录每天喝了多少水,特别容易忘记。所以做了这个喝水记录本给她。


开发需求


整体的开发需求和前言里描述的差不多,整体功能拆分一下就非常清晰了。


一、定义变量



  1. 大杯子:我们需要一个总量目标,用于定义每天的计划值。

  2. 小杯子:一个单次目标,我们不会一次接一大桶水来喝,即使用小杯子喝水时,每个杯子的刻度值。


二、逻辑整合



  1. 点击每个小杯子时,从大杯子的总量中扣除小杯子的刻度并记录,对应UI水位升高。

  2. 首次点击小杯子时,展示百分率刻度值,提升水位。

  3. 当完成目标值后,隐藏剩余水量的文字。

  4. "清空"按钮,消除本地记录值,恢复UI水位,展示剩余量。


创建流程和主要代码


 此模块代码是应用于小程序使用的,所以代码部分使用wx框架。(下面有普通代码部分)


wxml


构造整体布局,布局和制作大杯子和小杯子。


在上一段开发需求部分中提到的隐藏内容时,注意不要使用 wx:if 直接删除整个标签,这样会导致画面跳动,无法实现动画的平滑过渡。


用三元运算符隐藏文字可以实现较好的过渡


<view class="body">
<text class="h1">喝水记录本</text>
<text class="h3">今日目标: 2.6</text>

<view class="cup">
<view class="remained" style="height: {{remainedH}}px">
<text class="span">{{isRemained ? liters : ''}}</text>
<text class="small">{{isRemained ? '剩余' : ''}}</text>
</view>

<view class="percentage" style="{{percentageH}}">{{isPercentage ? percentage : ''}}</view>
</view>

<text class="text">请选择喝水的杯子</text>

<view class="cups">
<view class="cup cup-small" bindtap="cups" data-ml="700">700 ml</view>
<view class="cup cup-small" bindtap="cups" data-ml="400">400 ml</view>
<view class="cup cup-small" bindtap="cups" data-ml="600">600 ml</view>
<view class="cup cup-small" bindtap="cups" data-ml="500">500 ml</view>
<view class="cup cup-small" bindtap="cups" data-ml="50">50 ml</view>
<view class="cup cup-small" bindtap="cups" data-ml="100">100 ml</view>
<view class="cup cup-small" bindtap="cups" data-ml="150">150 ml</view>
<view class="cup cup-small" bindtap="cups" data-ml="300">300 ml</view>
</view>

<view class="cancle" bindtap="update">清空</view>
</view>

wxss


css就是简单的画杯子和布局,值得说的就是往大杯子里加水的动画 transition 一下就可以了


.body {
height: 108vh;
background-color: #3494e4;
color: #fff;
display: flex;
flex-direction: column;
align-items: center;
box-sizing: border-box;
}

.h1 {
margin: 10px 0 0;
}

.h3 {
font-weight: 400;
margin: 10px 0;
}

.cup {
background-color: #fff;
border: 4px solid #144fc6;
color: #144fc6;
border-radius: 0 0 40px 40px;
height: 330px;
width: 150px;
margin: 30px 0;
display: flex;
flex-direction: column;
overflow: hidden;
}

.cup.cup-small {
height: 95px;
width: 50px;
border-radius: 0 0 15px 15px;
background-color: rgba(255, 255, 255, 0.9);
cursor: pointer;
font-size: 14px;
align-items: center;
justify-content: center;
text-align: center;
margin: 5px;
transition: 0.3s ease;
}

.cup.cup-small.full {
background-color: #6ab3f8;
color: #fff;
}

.cups {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
width: 280px;
}

.remained {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
flex: 1;
transition: 0.3s ease;
}

.remained .span {
font-size: 20px;
font-weight: bold;
}

.remained .small {
font-size: 12px;
}

.percentage {
background-color: #6ab3f8;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 30px;
height: 0;
transition: 0.3s ease;
box-sizing: border-box;
}

.text {
text-align: center;
margin: 0 0 5px;
}

.cancle {
cursor: pointer;
}

js


逻辑注释写在了代码中


Page({
data: {
liters: '2.6L',
isPercentage: true,
isRemained: true,
percentage: '',
percentageH: 'height: 0',
RemainedH: 0,
goal: 2600
},

// 每次进入页面后加载记录的值,执行动画
onShow() {
this.setData({ goal: Number(wx.getStorageSync('goal')) })
this.updateBigCup(2600 - this.data.goal)
},

// 点击小杯子时的触发逻辑
cups(data) {
const ml = Number(data.currentTarget.dataset.ml);
const goal = this.data.goal - ml;
const total = 2600 - goal;
this.setData({ goal })
wx.setStorageSync("goal", goal);
this.updateBigCup(total)
},

// 更新 UI 数据
updateBigCup(total) {
const { goal } = this.data;
if (goal != 2600) {
this.setData({
isPercentage: true,
percentage: `${(total / 2600 * 100).toFixed(0)}%`,
percentageH: `height: ${total / 2600 * 330}px`
})
}

if (goal <= 0) {
this.setData({
remainedH: 0,
isRemained: false,
})
} else {
this.setData({
isRemained: true,
liters: `${goal / 1000}L`
})
}
},

// 清空记录值
update() {
wx.removeStorage({ key: 'goal' })
this.setData({
goal: 2600,
isPercentage: false,
isRemained: true,
remainedH: 0,
percentageH: 'height: 0px',
liters: '2.6L'
})
}
})

码上掘金


  上面的代码部分主要用于小程序使用,码上掘金可在网页中使用。



结语


  感谢大家能看到这里!!本篇的代码本身没有什么技术含量,可能是比较会偏向实用性的一篇,对!是有一些的对吧!可以自己改装成Chrome插件使用会更方便更实用。啥?你问我为什么不直接写Chrome插件?有没有一种可能不是我不想,而是😭。


  好啦,如果你身边有健身的朋友也可以给他做

作者:dudoit
来源:juejin.cn/post/7147529288164573192
一个哦~再次谢谢大家

收起阅读 »

制作了一个图片像素风转换器

web
制作了一个图片像素风转换器,可以将图片转换成像素风格,并可转换为 css box-shadow 进行输出。前排先放效果图、转换器地址和 GitHub 地址: 转化器地址:pixel.heyfe.org/ GitHub 地址:github.com/ZxBing...
继续阅读 »

制作了一个图片像素风转换器,可以将图片转换成像素风格,并可转换为 css box-shadow 进行输出。前排先放效果图、转换器地址和 GitHub 地址:


blog-mosaic-converter-44.gif


转化器地址:pixel.heyfe.org/


GitHub 地址:github.com/ZxBing0066/…


转换器功能


转换器会将传入的图片转换为像素风格,并将像素风格的图片以 box-shadow 进行转换,借助 box-shadow,我们可以直接用 css 来渲染该图片,且可以通过 box-shadow 的一些特性来达成一些比较好玩的效果,比如用间隙来加重像素风格:


blog-mosaic-converter-84.png


或者直接将间隙拉到顶,达成类似点阵图的效果:


blog-mosaic-converter-55.png


又或者借助 border-radius,实现圆点图效果:


blog-mosaic-converter-71.png


制作出想要的效果后,可以在右侧点击 复制 box-shadow 样式 按钮复制其样式。


实现原理


关于 box-shadow 实现像素图的原理之前有一篇文章中有提到,这里不再赘述。此处大概说一下图片转换为像素图再转为 box-shadow 的过程。


转换器在拿到图片后,会将图片绘制在一个非常小的画布中,以此来降低图片的精度,然后将画布中绘制的低精度图片进行二次渲染,渲染到较大的画布中,此时由于图片被拉伸,就会形成一定的像素效果。随后为了将像素效果图转换为 box-shadow,转换器会去读取画布中的绘制信息,将其生成为一组二维数组,再根据其中的颜色转换为 box-shadow 中的属性。至此转换器的功能就完成了。


当然其中还有一些细节(浏览器会默认启用平滑绘制导致像素效果消失等问题),本篇不打算细说,会在下篇专门写一篇来讲一下具体实现。


最后


本转换器原先是在码上掘金挑战赛某次文章中构想 ,然后在第二次制作类似效果时干脆使用脚本来完成了,最近有空就将其稍微优化了一下进行开源。目前一些细节还有点欠缺,待改进。


再贴一下地址:


转化器地址:pixel.heyfe.org/


GitHub 地址:github.com/ZxBing0066/…


相关文章



作者:嘿嘿Z
来源:juejin.cn/post/7150465824690536484
收起阅读 »

【记】滑动拼图验证码在搜索中的作用

开头验证码应用于我们生活、工作的方方面面,比如注册登录账号、支付订单、修改密码等。下面我是在一次项目中利用滑动拼图验证码和搜索功能“合作共赢”的记录。验证码展示具体实现前端代码// 引入js<script src="captcha.js?appid=XX...
继续阅读 »

开头

验证码应用于我们生活、工作的方方面面,比如注册登录账号、支付订单、修改密码等。下面我是在一次项目中利用滑动拼图验证码和搜索功能“合作共赢”的记录。

验证码展示



具体实现

前端代码

// 引入js
<script src="captcha.js?appid=XXX"></script>
<script>
kg.captcha({
// 绑定弹窗按钮
button: "#captchaButton",

// 验证成功事务处理
success: function (e) {
// 验证成功,直接提交表单
// form1.submit();
console.log(e);
},

// 验证失败事务处理
failure: function (e) {
console.log(e);
},

// 点击刷新按钮时触发
refresh: function (e) {
console.log(e);
}
});
</script>

<a id="captchaButton"></a>


验证结果说明


 

字段名
数据类型   描述   
 

code
 

number
 

返回code信息
 

msg
 

string
 

验证结果信息
 

rid
 

number
 

用户的验证码应用id
 

sense
 

number
 

是否开启无感验证,0-关闭,1-开启
 

token
 

string
 

验证成功才有:token
 

weight
 

number
 

错误严重性,0正常错误,可以继续操作,1一般错误,刷新/重新加载拼图,2严重错误,错误次数过多拒绝访问


Python代码

from wsgiref.simple_server import make_server
from KgCaptchaSDK import KgCaptcha
def start(environ, response):
# 填写你的 AppId,在应用管理中获取
AppID = "AppId"
# 填写你的 AppSecret,在应用管理中获取
AppSecret = "AppSecret"
request = KgCaptcha(AppID, AppSecret)
# 填写应用服务域名,在应用管理中获取
request.appCdn = "https://cdn.kgcaptcha.com"
# 请求超时时间,秒
request.connectTimeout = 10
# 用户id/登录名/手机号等信息,当安全策略中的防控等级为3时必须填写
request.userId = "kgCaptchaDemo"
# 使用其它 WEB 框架时请删除 request.parse,使用框架提供的方法获取以下相关参数
parseEnviron = request.parse(environ)
# 前端验证成功后颁发的 token,有效期为两分钟
request.token = parseEnviron["post"].get("kgCaptchaToken", "") # 前端 _POST["kgCaptchaToken"]
# 客户端IP地址
request.clientIp = parseEnviron["ip"]
# 客户端浏览器信息
request.clientBrowser = parseEnviron["browser"]
# 来路域名
request.domain = parseEnviron["domain"]
# 发送请求
requestResult = request.sendRequest()
if requestResult.code == 0:
# 验证通过逻辑处理
html = "验证通过"
else:
# 验证失败逻辑处理
html = f"{requestResult.msg} - {requestResult.code}"
response("200 OK", [("Content-type", "text/html; charset=utf-8")])
return [bytes(str(html), encoding="utf-8")]
httpd = make_server("0.0.0.0", 8088, start) # 设置调试端口 http://localhost:8088/
httpd.serve_forever()


最后

SDK开源地址:KgCaptcha (KgCaptcha) · GitHub,顺便做了一个演示:凯格行为验证码在线体验

收起阅读 »

仿抖音左右歪头图片选择

web
在线体验 项目 github 仓库 前一阵子在刷抖音时,看到一个通过左右歪头选择两侧图片的视频,感觉很有趣。顿时想到了 n 年前的face-api.js,那就基于这个来做吧。总体做好后,有很多细节需要改进,不够细腻丝滑。 1. 需求分析 直接开搞吧! ...
继续阅读 »

在线体验


项目 github 仓库


ezgif-4-7883a8f8e5.gif



前一阵子在刷抖音时,看到一个通过左右歪头选择两侧图片的视频,感觉很有趣。顿时想到了 n 年前的face-api.js,那就基于这个来做吧。总体做好后,有很多细节需要改进,不够细腻丝滑。



1. 需求分析


直接开搞吧!



  1. 页面基本布局,左右两侧图片,而且有缩放和移动动画

  2. 需要打开摄像头,获取视频流,通过 video 展现出来

  3. 需要检测人脸是向哪一侧歪头


2. 具体实现


2.1 页面布局和 animation 动画


这个不难,布局好后,就是添加 css 动画,我这里写的很粗糙,不细腻,但勉强能用,例如下面 leftHeartMove 为中间的小爱心向左侧移动动画


.heart {
width: 30px;
height: 30px;
padding: 4px;
box-sizing: border-box;
border-radius: 50%;
background-color: #fff;
position: absolute;
top: -15px;
left: 50%;
transform: translateX(-50%) rotateZ(0deg) scale(1);
animation: leftHeartMove 0.5s linear;
animation-fill-mode: forwards;
z-index: 2;
}

@keyframes leftHeartMove {
from {
top: -15px;
left: 50%;
transform: translateX(-50%) rotateZ(0deg) scale(1);
}

to {
top: 65px;
left: -13%;
transform: translateX(-50%) rotateZ(-15deg) scale(1.2);
}
}

2.2 打开摄像头并显示


注意点



  1. 关于 h5navigator.mediaDevices.getUserMedia 这个 api,本地开发localhost是可以拉起摄像头打开提示的,线上部署必须是https节点才行,http不能唤起打开摄像头


WX20221128-221028@2x.png




  1. 关于获取到视频流后,video视频播放,需要镜面翻转,这个可以通过 css 的transform: rotateY(180deg)来翻转




  2. 关于video播放不能在手机上竖屏全屏,可以给 video 设置 cssobject-fit:cover来充满屏幕




<video id="video" class="video" playsinline autoplay muted></video>

.video {
width: 100%;
height: 100%;
transform: rotateY(180deg);
object-fit: cover;
}


  • 获取摄像头视频流


async getUserMedia() {
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
try {
// https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia#examples
const stream = await navigator.mediaDevices.getUserMedia({
audio: false,
video: true,
video: {
facingMode: "user", // 前置摄像头
// facingMode: { exact: "environment" },// 后置摄像头
width: { min: 1280, max: 1920 },
height: { min: 720, max: 1080 },
},
});

return Promise.resolve(stream);
} catch (error) {
return Promise.reject();
}
}

const errorMessage =
"This browser does not support video capture, or this device does not have a camera";
alert(errorMessage);
}


  • video 播放视频流


async openCamera(e) {
try {
const stream = await this.getUserMedia();
this.video.srcObject = stream;
this.video.onloadedmetadata = async () => {
this.video.play();
};
} catch (error) {
console.log(error);
alert("打开摄像头失败");
}
}


  • 关闭视频


async closeCamera() {
// https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamTrack/stop
const tracks = this.video.srcObject.getTracks();

tracks.forEach((track) => {
track.stop();
});

this.video.srcObject.srcObject = null;
}

2.3 检测人脸左右倾斜


landmarks.png


通过face-api.js拿到人脸landmarks特征数据后,可以直接拿到左右眼的数据,分别通过求 Y 轴方向的平均值,然后比较这个平均值,便可以简单得出人脸向左还是向右倾斜,简单吧,角度都不用求了!


<div style="position: relative;width: 100%;height: 100%;">
<video
id="video"
class="video"
playsinline
autoplay
muted
style="object-fit:cover"
>
</video>
<canvas id="overlay" class="overlay"></canvas>
</div>

.video {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
z-index: 0;
transform: rotateY(180deg);
}

.overlay {
position: absolute;
top: 0;
left: 0;
}


  • 加载模型


import * as faceapi from "face-api.js";

async loadWeight() {
// 加载模型
await faceapi.nets.ssdMobilenetv1.load(
"./static/weights/ssd_mobilenetv1_model-weights_manifest.json"
);
// 加载人脸68特征模型数据
await faceapi.nets.faceLandmark68Net.load(
"./static/weights/face_landmark_68_model-weights_manifest.json"
);
// await faceapi.nets.faceExpressionNet.load(
// "/static/weights/face_expression_model-weights_manifest.json"
// );
// await faceapi.nets.faceRecognitionNet.load(
// "./static/weights/face_recognition_model-weights_manifest.json"
// );
await faceapi.nets.ageGenderNet.load(
"./static/weights/age_gender_model-weights_manifest.json"
);

console.log("模型加载完成");
}


  • 计算人脸左右倾斜


handleFaceLeftOrRight(landmarks) {
const DIFF_NUM = 15; // 偏差
let leftEye = landmarks.getLeftEye(); // 左眼数据
let rightEye = landmarks.getRightEye(); // 右眼数据
// let nose = landmarks.getNose();

let leftEyeSumPoint = leftEye.reduce((prev, cur) => ({
x: prev.x + cur.x,
y: prev.y + cur.y,
}));

let rightEyeSumPoint = rightEye.reduce((prev, cur) => ({
x: prev.x + cur.x,
y: prev.y + cur.y,
}));

// let noseSumPoint = nose.reduce((prev, cur) => ({
// x: prev.x + cur.x,
// y: prev.y + cur.y,
// }));

let leftEyeAvgPoint = {
x: leftEyeSumPoint.x / leftEye.length,
y: leftEyeSumPoint.y / leftEye.length,
};

let rightEyeAvgPoint = {
x: rightEyeSumPoint.x / leftEye.length,
y: rightEyeSumPoint.y / leftEye.length,
};

// let noseAvgPoint = {
// x: noseSumPoint.x / leftEye.length,
// y: noseSumPoint.y / leftEye.length,
// };

// console.log(leftEyeAvgPoint, rightEyeAvgPoint, noseAvgPoint);
let diff = Math.abs(leftEyeAvgPoint.y - rightEyeAvgPoint.y);

return diff > DIFF_NUM
? leftEyeAvgPoint.y > rightEyeAvgPoint.y
? "left"
: "right"
: "center";
}


  • 处理 video 视频


async handleVideoFaceTracking(cb) {
if (this.closed) {
window.cancelAnimationFrame(this.raf);
return;
}

const options = new faceapi.SsdMobilenetv1Options();

let task = faceapi.detectAllFaces(this.video, options);
task = task.withFaceLandmarks().withAgeAndGender();
const results = await task;

// overlay为canvas元素
// video即为video元素
const dims = faceapi.matchDimensions(this.overlay, this.video, true);
const resizedResults = faceapi.resizeResults(results, dims);

// console.log("options==>", options);
// console.log("resizedResults==>", resizedResults);
cb && cb(resizedResults);

this.raf = requestAnimationFrame(() => this.handleVideoFaceTracking(cb));
}

3. 参考资料




  1. face-api.js




  2. getUserMedia MDN




作者:sRect
来源:juejin.cn/post/7171081395551338503
收起阅读 »

假如:a===1 && a===2 && a===3; 那么 a 是什么?

web
前言 文章提供视频版啦,点击直接查看 hello,大家好,我是 sunday。 今天遇到了一个非常有意思的问题,跟大家分享一下。 咱们来看这段代码: a===1 && a===2 && a===3 假设上面的表达式成立,...
继续阅读 »

前言



文章提供视频版啦,点击直接查看



hello,大家好,我是 sunday


今天遇到了一个非常有意思的问题,跟大家分享一下。


咱们来看这段代码:


a===1 && a===2 && a===3 

假设上面的表达式成立,那么问:a 是什么?


正文


ok,我们来说一下这个问题的解答。


想要解决这个问题,那么我们首先要知道 JavaScript 中的类型转换和比较运算符的优先级。


JavaScript 中,表达式的运算顺序是 从左到右。因此,在这个表达式中,先执行 a===1 的比较运算符,如果它返回 false,整个表达式就会返回 false,也就是逻辑中断。


如果 a 的值是 1,则比较运算符返回 true,那么就会继续执行下一个逻辑运算符 &&,接着执行 a===2 的比较运算符,如果它返回 false,则整个表达式返回 false,逻辑中断。


以此类推,以此类推,所以 a 的值应该是动态变化的,并且应该依次为 1、2、3。只有这样才会出现 a===1 && a===2 && a===3; 返回 true 的情况。


那么 如何让 a 的值动态变化,就是咱们解决这个问题的关键。


我们在 一小时读完《JavaScript权威指南(第7版)》上一小时读完《深入理解现代 JavaScript》,彻底掌握 ES6 之后 JavaScript 新特性! 中都讲到过,对象的方法存在 get 标记,一旦方法存在 get 标记,那么我们就可以像调用对象的属性一样,调用这个方法。


那么说到这里,肯定很多小伙伴都想到这个问题怎么解决了。


我们直接来看代码:


 const obj = {
 // get 标记
 get a() {
   this.value = this.value || 1;
   return this.value++;
}
};

console.log(obj.a === 1 && obj.a === 2 && obj.a === 3); // true

在这段代码中,我们创建了一个对象 obj,它包含一个被 get 标记的方法 a。那么此时只要执行 obj.a 就会调用 a 方法,完成 value 自增的操作。从而得到咱们期望的结果。


总结


这是一个非常有意思的问题。除了上面这种方案之后,还有很多其他的实现方案。大家可以开动脑筋,想一想别的方案都有什么呢?


答案留在评论区,咱们

作者:LGD_Sunday
来源:juejin.cn/post/7223586933881421861
一起来讨论下哦~~~

收起阅读 »

浅析小程序蓝牙技术

web
认识蓝牙 蓝牙技术是一种无线数据和语音通信开放的全球规范,它是基于低成本的近距离无线连接,为固定和移动设备建立通信环境的一种特殊的近距离无线技术连接。 传统蓝牙和低功耗蓝牙 根据蓝牙的发展历程,将蓝牙普遍分为两种规格,即传统蓝牙模块(BT) 和低功耗蓝牙模...
继续阅读 »

认识蓝牙



蓝牙技术是一种无线数据和语音通信开放的全球规范,它是基于低成本的近距离无线连接,为固定和移动设备建立通信环境的一种特殊的近距离无线技术连接。



传统蓝牙和低功耗蓝牙


根据蓝牙的发展历程,将蓝牙普遍分为两种规格,即传统蓝牙模块(BT)低功耗蓝牙模块(BLE)。传统蓝牙模块常用在对数据传输带宽有一定要求的场景上。低功耗蓝牙是从蓝牙4.0起支持的协议,特点是耗电极低、传输速度更快,常用在对续航要求较高且只需小数据量传输的各种智能电子产品中。


技术指标经典蓝牙BT低功耗蓝牙BLE
无线电频率2.4GHz2.4GHz
距离10米最大100米
发送数据所需时间100ms<3ms
响应延时约100ms6ms
安全性64/128-bit及用户自定义的应用层128-bitAES及用户自定义的应用层
能耗100%(ref)1%-50%
空中传输数据速率1-3Mb/s1Mb/s
主要用途手机,游戏机,耳机,音箱,汽车和PC等鼠标,键盘,手表,体育健身,医疗保健,智能穿戴设备,汽车,家用电子等
适用场景较高数据量传输、对传输带宽有要求续航要求较高、数据量小

蓝牙技术目前已经发展到5.0+版本,为现阶段最高级的蓝牙协议标准。BLE技术更契合新时代物联网的需求:更快、更省、更远、更便捷,也是我们小程序开发者在物联网项目最常用的技术。


蓝牙通信概述


低功耗蓝牙协议给设备定义了若干角色,其中最主要的角色是:外围设备(Peripheral)中心设备(Central)。




  • 外围设备:用来提供数据,通过不停地向外广播数据,让中心设备发现自己。




  • 中心设备:扫描外围设备,发现有外围设备存在后,可以与之建立连接,之后就可以使用外围设备提供的服务(Service)。




在两个蓝牙设备建立连接之后,双方的数据交互是基于一个叫做 GATT (Generic Attribute Profile,通用属性配置文件) 的规范,根据该规范可以定义出一个配置文件(Profile),描述该蓝牙设备提供的服务(Service)。


在整个通信过程中,有三个最主要的概念:配置文件(Profile)服务(Service)特征(Characteristic)


Characteristic:在 GATT 规范中最小的逻辑数据单元。实际上,在与蓝牙设备打交道,主要就是通过读写 Characteristic 的 value 完成。Characteristic 是通过一个 16bit 或 128bit 的 UUID 唯一标识。


Service:可以理解为蓝牙设备提供的服务,一个蓝牙设备可以提供多个服务,比如电量信息服务、系统信息服务等。每个 Service 又包含多个 Characteristic 特性值,比如电量信息服务就会有个 Characteristic 表示电量数据。同时也有一个 16bit 或 128bit 的 UUID 唯一标识该服务。


Profile:并不真实存在于蓝牙设备中,它只是被蓝牙标准预先定义的一些 Service 的集合。如果蓝牙设备之间要相互兼容,它们只要支持相同的 Profile 即可。一个蓝牙设备可以支持多个 Profile。


Desciptor: 描述符是描述特征值的已定义属性。例如,Desciptor 可指定人类可读的描述、特征值的取值范围或特定于特征值的度量单位。每个 Desciptor 由一个 UUID 唯一标识。


总结:每个蓝牙设备可能提供多个 Service,每个 Service 可能有多个 Characteristic,根据蓝牙设备的协议,用对应的 Characteristic 进行读写,即可达到与其通信的目的。


蓝牙开发实践


蓝牙通信过程介绍



整体上看,蓝牙通信的开发主要分为三部分:



  1. 蓝牙资源和状态管理:包括蓝牙生命周期管理、蓝牙状态管理(开关、适配器、设备连接、数据接收等)、错误异常处理。

  2. 搜寻外围设备并建立连接:包括搜寻设备、监听设备发现、处理获取到的设备信息、连接/断开设备等。

  3. 读写数据:包括寻找目标服务和特征值、订阅特征值、监听并接收设备数据、分包处理数据等。


蓝牙数据读写


在小程序蓝牙开发联调中,推荐使用TLV协议对数据进行封包,TLV协议(Tag、Length、Value)是常见的一种面向物联网的通讯协议,对于不同的传输场景,甚至演变出混合型、指针型、循环型等不同类型的格式。


比如,在实践中往往只需要最简单的L-TLV格式,以下使用十六进制(Hex)表示:



  • 数据包总长(L)

  • 数据的类型Tag/Type(T)

  • Value的长度Length(L)

  • 数据的值Value(V)


[0x07, 0x01, 0x01, 0x01, 0x02, 0x01, 0x01]
[数据总长,typelength,value,typelength,value]

举例


假设业务规定各字段type如下


字段名称type字段类型备注
account0x00String账号
Password0x01String密码

想要向设备传输一条写入account的指令,value为ABC。


ABC 通过 UTF-8 编码转 Hex String 分别是0x41、0x42、0x43。


那么数据包总长6字节,type是0,value总长3字节。


字符集编码


实际业务场景中,如果需要传输中文字符,则需要通过协商好的字符集进行转换。


常见字符集有:ASCII字符集、GB2312字符集、GBK字符集、 GB18030字符集、Unicode字符集等。


字符集描述
ASCII美国信息交换标准代码是基于拉丁字母的一套电脑编码系统,主要用于显示现代英语和其他西欧语言
GB2312中国人民通过对 ASCII 编码的中文扩充改造,产生了 GB2312 编码,可以表示6000多个常用汉字。
GBK汉字实在是太多了,包括繁体和各种字符,于是产生了 GBK 编码,它包括了 GB2312 中的编码,同时扩充了很多。
GB18030中国是个多民族国家,各个民族几乎都有自己独立的语言系统,为了表示那些字符,继续把 GBK 编码扩充为 GB18030 编码。
Unicode每个国家都像中国一样,把自己的语言进行编码,于是出现了各种各样的编码,如果你不安装相应的编码,就无法解释相应编码想表达的内容。终于,有个叫 ISO 的组织看不下去了。他们一起创造了一种编码 Unicode ,这种编码非常大,大到可以容纳世界上任何一个文字和标志。
UTF-8、 UTF-16Unicode 在网络传输中,出现了两个标准 UTF-8 和 UTF-16,分别每次传输 8个位和 16个位。

比如小写字母a,ASCII编码对应的Hex值是0x61,而GB2312字符集编码对应的Hex值是253631


将文本字符串转换为Hex字符串的时候,不同的字符集编码对应的Hex值不一样,所以小程序与蓝牙设备应当使用同一套字符集编码。推荐统一使用Unicode的UTF-8标准。


以下是字符转换示例:


// 中文转UTF-8
encodeURI('好').replace(/%/g, ''); // 'E5A5BD'

// UTF-8转中文
hex2String('E5A5BD'); // '好'

/**
* * read UTF-8
* @param { number[] } arr
* @returns {string}
*/

const readUTF = (arr: number [] ) => {
let UTF = '';
const _arr = arr;
for (let i = 0; i < _arr.length; i++) {
// 10进制转2进制
const one = _arr[i].toString(2);
const v = one.match(/^1+?(?=0)/);
if (v && one.length == 8) {
const bytesLength = v[0].length;
let store = _arr[i].toString(2).slice(7 - bytesLength);
for (let st = 1; st < bytesLength; st++) {
store += _arr[st + i].toString(2).slice(2);
}
// 二进制序列转charCode,再拼接
UTF += String.fromCharCode(parseInt(store, 2));
i += bytesLength - 1;
} else {
UTF += String.fromCharCode(_arr[i]);
}
}
return UTF;
};

/**
* * transfer hex to string
* @param { string } str
* @returns {string}
*/

const hex2String = (hex: string) => {
const buf = [];
// 转10进制数组
for (let i = 0; i < hex.length; i += 2) {
buf.push(parseInt(hex.substring(i, i + 2), 16));
}

return readUTF(buf);
};

蓝牙分包


但是实际场景往往不是传输几个字母这么简单。虽然小程序不会对写入数据包大小做限制,但与蓝牙设备传输数据时,数据量超过 MTU (最大传输单元) 容易导致系统错误,所以要主动对数据进行分片传输。


参考各小程序开放平台文档:


开放平台文档描述
微信小程序在与蓝牙设备传输数据时,需要注意 MTU(最大传输单元)。如果数据量超过 MTU 会导致错误,建议根据蓝牙设备协议进行分片传输。Android设备可以调用 wx.setBLEMTU 进行 MTU 协商。在 MTU 未知的情况下,建议使用 20 字节为单位传输。
飞书小程序蓝牙设备特征值对应的值,为 16 进制字符串,限制在 20 字节内
支付宝小程序写入特征值需要使用 16 进制的字符串,并限制在 20 字节内。
Taro小程序不会对写入数据包大小做限制,但系统与蓝牙设备会限制蓝牙4.0单次传输的数据大小,超过最大字节数后会发生写入错误,建议每次写入不超过20字节。若单次写入数据过长,iOS 上存在系统不会有任何回调的情况(包括错误回调)。

分包的过程,需要用到 ArrayBuffer



ArrayBuffer 对象用来表示通用的、固定长度的原始二进制数据缓冲区。


ArrayBuffer 是对固定长度的连续内存空间的引用。



在 Web 开发中,当我们处理文件时(创建,上传,下载),经常会遇到二进制数据。另一个典型的应用场景是图像处理。这些都可以通过 JavaScript 进行处理,而且二进制操作性能更高。


ArrayBuffer 只是一个内存区域,里面存储着一些原始的字节序列,它和普通的Array完全不是一个概念,它的长度是固定的,无法增加或减少,也无法直接用buffer[index]进行访问。


要想写入值、遍历它或者访问单个字节,需要使用视图(View) 进行操作,以下为一些常用的视图:


Uint8Array :将 ArrayBuffer 中的每个字节视为 0 到 255 之间的单个数字(每个字节是 8 位,因此只能容纳那么多)。称为 “8 位无符号整数”。


Uint16Array:将每 2 个字节视为一个 0 到 65535 之间的整数。称为 “16 位无符号整数”。


所有这些视图(Uint8Array,Uint32Array 等)的通用术语是 TypedArray(类型化数组)。它们都享有同一组方法和属性,类似于常规数组,具有索引,并且是可迭代的。


实际上,不同平台的小程序API定义的数据接口,都多少会用到ArrayBuffer



微信小程序-写入特征值



飞书小程序-获取设备信息


但也不排除有些操作,开发平台已经帮忙处理了



飞书小程序-写入特征值


因此学习并使用 ArrayBuffer,可以:




  1. 方便操作分包,方便读取设备返回的数据、向设备写入数据。




  2. 在不同小程序平台灵活处理,更好地兼容




回到主题,蓝牙分包的思路是:


Text String --> Hex String --> ArrayBuffer(分包)


举个例子,上文中想要向设备传输一条写入password的指令,value为bytedance123456789ABC


[数据总长,type,length,value] MTU为20字节
[0x14, 0x01, 0x11, 0x62, 0x79, 0x74, ...] 第一个包 bytedance12345678
[0x07, 0x01, 0x04, 0x39, 0x41, 0x42, ...] 第二个包 9ABC

设备端会将多个相同type的包的值追加,而不是覆盖。


如何与设备端协商分包交互机制?



  1. 规定服务、特征值UUID,建议不同操作使用不同的UUID,读、写、订阅分开。

  2. 遵循TLV协议,双方协商好Type对应的字段类型和含义。

  3. 双方使用同一套字符编码集。

  4. 约定连在一起的两次(或多次)相同类型的设置,应该把它们的值追加连接,而不是覆盖

  5. 可约定在一次涉及业务逻辑的通信过程中,发送“开始”和“结束”的蓝牙包,告知设备处于这两个信号之间的蓝牙包为一次完整的通信数据流。

  6. 双方共同约定一个超时时间,若在此时间内由于各种原因未能完成读/写通信,则认为通信失败,小程序端必须给予用户友好提示。


问题排查手段


在开发过程中可能会遇到调用API失败、连接断开等问题



  1. 检查API调用顺序


小程序的蓝牙API使用起来比较简单,但是需要严格遵循一定的调用顺序(参考上文的流程图)。比如检查是否在开关蓝牙适配器之外进行操作,或者是否在特征值发生变化后才进行事件监听等



  1. 对比测试



  • 业务小程序、开放平台官方蓝牙demo 对比

  • 开放平台(非微信)官方蓝牙demo、微信官方demo 对比

  • 同厂商设备、同芯片、同蓝牙模组,多台设备对比

  • iOS、Android,蓝牙调试软件 与小程序的对比 (iOS:LightBlue,Android:BLE调试宝、nRF Connect)


经过以上对比测试,基本可以缩小问题范围,定位问题究竟是出在哪一方。但并不百分之百准确。




  1. 一些Tips:



    • 设备Server端在自定义特征值UUID时未遵循GATT的Attribute Structure,而蓝牙服务iOS的实现会比Android更严格。

    • 外围设备使用deviceId作为唯一标识,但iOS 和 Android在拿到的信息上有所差异。Android上获取到的deviceId为设备MAC地址,iOS上则是系统根据外围设备 MAC 地址及发现设备的时间生成的 UUID,因此deviceId不能硬编码。

    • 蓝牙模块比较耗费系统资源,做好生命周期管理必不可少,比如建立连接和断开连接应该成对出现,如果未能及时关闭连接释放资源,容易导致连接异常。另外,大多数蓝牙模组只支持单链路,最大连接数量为1,若未能及时断开连接,必然出现设备搜寻不到或连接不上的情况。




  2. 日志排查




作为小程序的开发者,很多疑难问题往往不能直观看出。如果你有对应的资源可以联系到开放平台的维护人员,即可拿到日志。我们项目组曾与飞书开放平台建立蓝牙专项问题解决渠道,结合开平和设备端同学捕获的日志,可以加快排查速度。


参考文章


http://www.bluetooth.com/learn-about…
http://www.cnblogs.com/chusiyong/p…
http://www.jianshu.com/p/62eb2f540…
zh.javascript.info/arraybuffer…


作者:HenryZheng
来源:juejin.cn/post/7221794170868351034
收起阅读 »

HTML5+CSS3小实例:闪亮的玻璃图标悬浮效果

web
HTML5+CSS3实现闪亮的玻璃图标悬浮效果,光与玻璃的碰撞,好有质感的玻璃图标。 先看效果: 源代码: <!DOCTYPE html> <html> <head> <meta http-equiv="c...
继续阅读 »

HTML5+CSS3实现闪亮的玻璃图标悬浮效果,光与玻璃的碰撞,好有质感的玻璃图标。


先看效果:



源代码:


<!DOCTYPE html>
<html>

<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">

<title>闪亮的玻璃图标悬浮效果</title>
<link href="https://cdn.bootcdn.net/ajax/libs/font-awesome/4.7.0/css/font-awesome.css" rel="stylesheet">
<link rel="stylesheet" href="../css/5.css">
</head>

<body>
<div class="container">
<div class="color"></div>
<div class="color"></div>
<div class="color"></div>
<ul>
<li>
<a href="#"><i class="fa fa-qq" aria-hidden="true"></i></a>
</li>
<li>
<a href="#"><i class="fa fa-weixin" aria-hidden="true"></i></a>
</li>
<li>
<a href="#"><i class="fa fa-weibo" aria-hidden="true"></i></a>
</li>
<li>
<a href="#"><i class="fa fa-tencent-weibo" aria-hidden="true"></i></a>
</li>
<li>
<a href="#"><i class="fa fa-telegram" aria-hidden="true"></i></a>
</li>
</ul>
</div>
</body>

</html>

*{
margin: 0;
padding: 0;
/* 这个是告诉浏览器:你想要设置的边框和内边距的值是包含在总宽高内的 */
box-sizing: border-box;
}
body{
/* 溢出隐藏 */
overflow: hidden;
}
.container{
position: absolute;
width: 100%;
/* 100%窗口高度 */
height: 100vh;
/* 弹性布局 水平垂直居中 */
display: flex;
justify-content: center;
align-items: center;
/* 渐变背景 */
background: linear-gradient(to bottom,#2193b0,#6dd5ed);
}
.container::before{
content: "";
position: absolute;
bottom: 0px;
width: 100%;
height: 50%;
z-index: 1;
/* 背景模糊 */
backdrop-filter: blur(5px);
border-top: 1px solid rgba(255,255,255,0.5);
}
.container .color{
position: absolute;
/* 模糊滤镜 数值越大越模糊 */
filter: blur(200px);
}
.container .color:nth-child(1){
background-color: #fd746c;
width: 800px;
height: 800px;
top: -450px;
}
.container .color:nth-child(2){
background-color: #cf8bf3;
width: 600px;
height: 600px;
bottom: -150px;
left: 100px;
}
.container .color:nth-child(3){
background-color: #fdb99b;
width: 400px;
height: 400px;
bottom:50px;
right:100px;
}
ul{
position: relative;
display: flex;
z-index: 2;
}
ul li{
position: relative;
list-style: none;
margin: 10px;
}
ul li a{
position: relative;
width: 80px;
height: 80px;
display: inline-block;
border-radius: 10px;
display: flex;
justify-content: center;
align-items: center;
text-decoration: none;
color: #fff;
font-size: 32px;
border: 1px solid rgba(255,255,255,0.4);
border-right: 1px solid rgba(255,255,255,0.2);
border-bottom: 1px solid rgba(255,255,255,0.2);
/* 阴影 */
box-shadow: 0px 5px 45px rgba(0,0,0,0.1);
/* 背景模糊 */
backdrop-filter: blur(2px);
/* 动画过渡 */
transition: all 0.5s;
overflow: hidden;
}
ul li a:hover{
/* 鼠标移入元素沿Y轴上移 */
transform: translateY(-20px);
}
ul li a::before{
content: "";
position: absolute;
top: 0px;
left: 0px;
width: 50px;
height: 100%;
background-color: rgba(255,255,255,0.5);
/* 元素沿X轴45度横切,沿X轴右移150px */
transform: skewX(45deg) translateX(150px);
/* 动画过渡 */
transition: all 0.5s;
}
ul li a:hover::before{
/* 元素沿X轴45度横切,沿X轴左移150px */
transform: skewX(45deg) translateX(-150px);
}

作者:艾恩小灰灰
来源:juejin.cn/post/7091339314352619557
收起阅读 »

前端获取电池信息

web
今日正能量: 当奇怪的需求越做越多的时候,证明你的眼光也正在变得广阔。 产品经理:加个需求,用户电脑设备如果快没电,我要暖心的告诉他该插上电源。 前端攻城狮:。。。他电脑不会自己提醒吗? 产品经理:你做不做? 前端攻城狮:做! 前言 随着技术的日益发展,w...
继续阅读 »

今日正能量: 当奇怪的需求越做越多的时候,证明你的眼光也正在变得广阔。



产品经理:加个需求,用户电脑设备如果快没电,我要暖心的告诉他该插上电源。


前端攻城狮:。。。他电脑不会自己提醒吗?


产品经理:你做不做?


前端攻城狮:做!


屏幕截图 2023-04-17 221002.png


前言


随着技术的日益发展,web前端技术远比我们想象的强大。浏览器允许网站获取用户设备的电池状态信息,例如电量百分比,剩余电量,充电状态等等。我们可以使用这些信息,根据用户设备的电量调整我们的应用行为。在这篇中,我们将探讨如何在前端中获取电池信息,用到的就是关于 Battery Status API。


Battery Status API的使用


Battery Status API 是一个 Web API,允许 Web 应用程序访问用户设备的电池状态信息。使用这个 API,我们可以在不安装任何应用程序的情况下,从 Web 浏览器直接读取设备的电量信息。


获取设备电池信息的主要步骤如下:


// 请求电池信息
navigator.getBattery().then(function (battery) {
// 后续代码
})

将返回一个 Promise 对象,它会解析为一个 BatteryManager 对象,我们可以使用它来读取设备的电池属性。


navigator.getBattery().then(function (battery) {
// 获取设备电量剩余百分比
var level = battery.level //最大值为1,对应电量100%
console.log('Level: ' + level * 100 + '%')

// 获取设备充电状态
var charging = battery.charging
console.log('充电状态: ' + charging)

// 获取设备完全充电需要的时间
var chargingTime = battery.chargingTime
console.log('完全充电需要的时间: ' + chargingTime)

// 获取设备完全放电需要的时间
var dischargingTime = battery.dischargingTime
console.log('完全放电需要的时间: ' + dischargingTime)
})

监听电池状态变化


为了更好地反映用户设备的电池状态,我们可以在前端中添加事件来监视电池状态的变化。例如,当设备的电池电量改变时,会触发事件。一些给大家列举几个常用事件:


navigator.getBattery().then(function (battery) {
// 添加事件,当设备电量改变时触发
battery.addEventListener('levelchange', function () {
console.log('电量改变: ' + battery.level)
})

// 添加事件,当设备充电状态改变时触发
battery.addEventListener('chargingchange', function () {
console.log('充电状态改变: ' + battery.charging)
})

// 添加事件,当设备完全充电需要时间改变时触发
battery.addEventListener('chargingtimechange', function () {
console.log('完全充电需要时间: ' + battery.chargingTime)
})

// 添加事件,当设备完全放电需要时间改变时触发
battery.addEventListener('dischargingtimechange', function () {
console.log('完全放电需要时间: ' + battery.dischargingTime)
})
})

兼容性


兼容性方面,Battery Status API 并不适用于所有的设备和操作系统,开发人员需要进行兼容性处理,以确保我们的应用可以在所有的设备上运行。以下是该API对应的兼容性视图:


屏幕截图 2023-04-17 220020.png


通过 Battery Status API 获取设备电池信息是一种很强大的方法,可以根据设备电池状态来优化应用程序的行为。需要注意的是,此 API 不适用于所有设备和操作系统,并且某些设备生产商可能不允许共享电池信息。


作者:白椰子
来源:juejin.cn/post/7222996459833622565
收起阅读 »

情侣空间动态时间效果,你学废了吗?

web
前言 中秋这天刚好碰上和女朋友在一起的 五周年 了,想来五年风风雨雨仍然好好的,挺是感慨,也挺满足的。qq情侣空间也毫不意外的准点报时了,闲来无事点进去看了看,瞥到一个动效,觉得很有意思,于是打算自己动手实现一下,也算是尝试了。 动效长这样: 码上掘金 动态...
继续阅读 »

前言


中秋这天刚好碰上和女朋友在一起的 五周年 了,想来五年风风雨雨仍然好好的,挺是感慨,也挺满足的。qq情侣空间也毫不意外的准点报时了,闲来无事点进去看了看,瞥到一个动效,觉得很有意思,于是打算自己动手实现一下,也算是尝试了。


动效长这样:


Video_20220912_014016_213.gif


码上掘金


动态日期特效 - 码上掘金 (juejin.cn)


思路解析


日期函数的使用频率可以说是很高了,不管是原生手写也好,还是用的 day.js 这种第三方库,在业务开发中我们经常需要处理日期进行展示。由于这边的时间处理不复杂,因此我们直接手写一个就好了。


获取年月日我们分别使用 getFullYear()getMonth() 还有 getDate() ,需要注意的是,很多新手小伙伴经常会把 getDay() 误以为是获取日的功能,实际上是用的 getDate() 实现的,还有一个需要注意的地方是,获取月份的函数 getMonth() ,获取的时间范围是 (0,11) ,没错,它是从 0 开始的,最后为了展示我们还需要让它 +1


时分秒我们分别使用 getHours()getMinutes() 还有 getSeconds() 三个方法,其中,分和秒的函数返回的是 (0, 59),也就是说不超过两位数的话,输出形式是个位数,我们需要手动补 0,可以通过字符串拼接过 padStart() 实现。


最后,我们观察一下这个效果,实际上它只有最后一个数字是向上淡出的,也就是说我们只要处理这个数字就好了,那么问题就简单了。


我们先将秒的个位和十位分开,将它们分为两个部分单独展示,这样我们就可以单独处理这个数字的特效了。


向上淡出,你第一思路是什么?


对了,是定位+透明,我们就用这个思路试一试。


一开始给它设置为相对定位 position: relative。接下来实现动效,为了让它不断的有这么个淡出效果,我们自然而然想到要使用动画,从当前位置开始,结束的时候增大透明度并且向上移动,逻辑很快就写好了。


涉及知识点


1. Date 日期类




  • Date.prototype.getDate():根据本地时间,返回一个指定的 Date 对象为一个月中的哪一日(1-31)。




  • Date.prototype.getFullYear():根据本地时间,返回一个指定的 Date 对象的完整年份(四位数年份)。




  • Date.prototype.getHours():根据本地时间,返回一个指定的 Date 对象的小时(023)。




  • Date.prototype.getMinutes():根据本地时间,返回一个指定的 Date 对象的分钟数(059)。




  • Date.prototype.getMonth():根据本地时间,返回一个指定的 Date 对象的月份(011),0 表示一年中的第一月。




  • Date.prototype.getSeconds():根据本地时间,返回一个指定的 Date 对象的秒数(059)。




2. 时间补零


getMinutes()getSeconds() 获取的时间是没有前缀零的,我们可以判断一下,如果时间小于 10 ,则用 0 拼接。


也可以使用 padStart() 进行补零操作。



关于 padStart() 的更多用法,详见:String.prototype.padStart() - JavaScript | MDN (mozilla.org)



结束语


相信不少小伙伴像我一样,因为行业原因,工作中动效开发频率很低,这块的实战经验也很薄弱,为了以后能更好的搬砖,我们应该从小 demo 开始,不断的去练习提升,基础进阶两手抓。


作者:CatWatermelon
来源:juejin.cn/post/7142412506815250445
收起阅读 »

如何接入小程序订阅消息?

web
更新完微信服务号的模板消息之后,我又赶紧把微信小程序的订阅消息给实现了!之前我一直以为微信小程序也是要企业才能申请,没想到小程序个人就能申请。 消息推送平台🔥推送下发【邮件】【短信】【微信服务号】【微信小程序】【企业微信】【钉钉】等消息类型。 gitee....
继续阅读 »

更新完微信服务号的模板消息之后,我又赶紧把微信小程序的订阅消息给实现了!之前我一直以为微信小程序也是要企业才能申请,没想到小程序个人就能申请。



消息推送平台🔥推送下发【邮件】【短信】【微信服务号】【微信小程序】【企业微信】【钉钉】等消息类型




本想着跟微信服务号的模板消息一样,我去申请一个「测试号」,就能下发微信小程序的订阅消息了。但微信小程序的订阅消息一直不支持「测试号」下发。


于是,我就注册了一个微信小程序,拿到我的小程序appIdsecret,并在微信后台创建了几个订阅消息的模板。



紧接着,这个把小程序的账号就接入到消息推送平台的账号管理体系下:



写几行代码得到刚才创建的模板,顺便跟前端来个简单的交互:




改几行代码,把具体调用微信的逻辑给补上,有SDK的加持下这种代码就是10min就完成了,非常简单。


image-20221208202228360


实现小程序的订阅消息推送,我花的时间最长就花在这下面啦:


1、拥有自己的小程序(拥有调试基础)


2、让自己的登录到这个小程序里(得到openId)


3、小程序弹窗让我能授权给微信发送订阅消息(得到推送权限)


小程序的账号我已经创建好了,但是小程序是没有任何内容的。于是我就在小程序的后台点点点,顺便看看小程序一般是怎么开发的。于是,我就看到了微信小程序的开发者工具:


developers.weixin.qq.com/miniprogram…


下载了之后,这个小工具给我推荐「云开发」,只要花点钱我就可以调用云函数了。虽然要19.9块,有点肉疼。但省时间的事,我咬咬牙就上了。



完了以后,我在小程序工具箱里翻了好几个模板,看看有没有我想要的功能:登录小程序获取openId、弹窗让我授权模板发送消息。你别说,真给我翻到一个:



我是没做过小程序的,自然就不会小程序开发,于是就只能摸石头过河了。花了一天多,发现我在这个demo项目里获取的openId就是死活的调不通小程序(报错就说不合法openId)。


经过漫长的调试,我忍不了了,再这样耗下去是不行的了。我直接去GitHub看看有没有现成的demo,随便一搜,还真的有。



github.com/zhangkaizha…


直接将「wxapp」文件下导入到小程序的开发工具里,一看,还真能用,代码又少。回看同步openId的代码,原来是要调接口请求微信做鉴权的呀。



于是我在消息推送平台里也临时写了接口进行鉴权,在小程序调用登录的时候改下入参就完事咯。




经过登录凭证校验之后,我们就能拿到openId,把订阅消息的权限界面给唤起,点击允许,就能在消息推送平台下发送一条小程序的订阅消息啦。




代码方面我就不细说啦,感兴趣的同学可以把项目搞下来玩玩,源码都是有的。这几天还在疯狂更新中,看看目前的消息渠道接入的情况吧?


如果想学Java项目的,强烈推荐我的开源项目消息推送平台Austin(8K stars) ,可以用作毕业设计,可以用作校招,可以看看生产环境是怎么推送消息的。开源项目消息推送平台austin仓库地址:



消息推送平台🔥推送下发【邮件】【短信】【微信服务号】【微信小程序】【企业微信】【钉钉】等消息类型





作者:Java3y
来源:juejin.cn/post/7223728273007493176
收起阅读 »

关于如何写一个有层次感的css遮罩效果

web
前言 写了这么多天游戏了,最近也想换换口味写一点有趣的CSS动效,这次我为大家带来的就是一个纯css的动态效果,层次感遮罩。在现在审美疲劳的大时代环境背景下,对页面的设计也开始越来越追求他的层次感和立体感,有时候因为一个好的板块动效,可以拉高整体的页面颜值增添...
继续阅读 »

前言


写了这么多天游戏了,最近也想换换口味写一点有趣的CSS动效,这次我为大家带来的就是一个纯css的动态效果,层次感遮罩。在现在审美疲劳的大时代环境背景下,对页面的设计也开始越来越追求他的层次感和立体感,有时候因为一个好的板块动效,可以拉高整体的页面颜值增添出页面的高级感,让网页用户有一种油然而生的成为这个页面用户是一种非常自豪事情的感觉。(当然如果甲方觉得麻垮你说的再牛B也是白搭)


那么接下来我们马上开始今天的代码解析


实现步骤


创建出基本元素


不难看出页面元素其实就是一个边框加一段文字,但这个边框并非一个div加上border属性这么简单,这里我用的是伪元素


先写好html标签,这里用阿a标签什么的都可以。


<a href="#">荆棘鸟QAQ</a>

用css把整个页面变成灰色的,并且为了突出文字,把a标签居中,文字变为白色,再加上一个文字阴影。


body {
font-family: "黑体";
background-color: #555;
}

a {
color: #fffbf1;
text-shadow: 0 20px 25px #2e2e31;
font-size: 80px;
font-weight: bold;
text-decoration: none;
position: absolute;
top: 50%;
left: 50%;
transform: translateX(-50%) translateY(-50%);
}

image.png


添加伪类


这个就是重头戏了,因为层次效果全靠after和before这两个伪类表现出来。


减少冗余代码,所以先把两个伪类的共同样式写出来。伪类的高读要根据文字大小进行改变,所以直接用padding配合em来规定伪类的高度。宽度直接用100%


a:before,
a:after {
content: '';
padding: .9em .4em;
position: absolute;
left: 50%;
width: 100%;
top: 50%;
display: block;
border: 15px solid skyblue;
transform: translateX(-50%) translateY(-50%) rotate(0deg);
animation: 10s infinite alternate ease-in-out tipsy;
}

image.png


边框有了,现在就需要把动态效果给加上,让它先动起来


@keyframes tipsy {
0%{
transform: translateX(-50%) translateY(-50%) rotate(0deg);
}
100% {
transform: translateX(-50%) translateY(-50%) rotate(360deg);
}
}

遮罩效果.gif


如动图所示,现在表框完全就是盖在文字上的,还没有什么特别之处。因为并没有将层级给区分开来,这也很简单,把before这个伪元素的层级给下调就行啦
a:before {
z-index: -1;
}

当然层级改变了,但上一层的边框会把下一层的边框给覆盖住,所以还得让部分边框变得透明


a:before {
border-color: skyblue skyblue rgba(0, 0, 0, 0) rgba(0, 0, 0, 0);
z-index: -1;
}

a:after {
border-color: rgba(0, 0, 0, 0) rgba(0, 0, 0, 0) skyblue skyblue;
}

遮罩效果2.gif


现在就可以看到已经有层次感啦,但是还可以加上一点边框阴影让他更加立体


a:before {
border-color: skyblue skyblue rgba(0, 0, 0, 0) rgba(0, 0, 0, 0);
z-index: -1;
}

a:after {
border-color: rgba(0, 0, 0, 0) rgba(0, 0, 0, 0) skyblue skyblue;
box-shadow: 25px 25px 25px rgba(46, 46, 49, .8);
}

遮罩效果3.gif


那么到这就已经全部完成啦,以上就是本次代码的全部解析,下面我将把所有代码放在在线代码里供大家修改体验

在线代码



往期精彩


关于我写了一个海底掘金挑战游戏juejin.cn/post/714464…


关于我随手写了个掘金相关的游戏juejin.cn/post/714232…


关于我帮领导的孩子写了一个小游戏参赛这种事juejin.cn/post/714115…


关于我抽不到月饼礼盒于是用代码做了一个(纯代码文本) juejin.cn/post/714047…


关于我仿做了个steam很火的《Helltaker》游戏juejin.cn/post/712149…


作者:Gatsby
来源:juejin.cn/post/7144912266855940132
收起阅读 »

前端应该知道的浏览器中的内存知识

web
为了保证我们网页的稳定性,浏览器的内存知识对我们来说是十分必要的,我们不应该只考虑网页打开时的性能,也应该考虑网页长时间挂载下的稳定性。 本次梳理以Chrome为例。 chrome的内存限制 堆内存的限制是由 V8 来设置的。 存在限制 64位系统 物理内存...
继续阅读 »

为了保证我们网页的稳定性,浏览器的内存知识对我们来说是十分必要的,我们不应该只考虑网页打开时的性能,也应该考虑网页长时间挂载下的稳定性。


本次梳理以Chrome为例。


chrome的内存限制


堆内存的限制是由 V8 来设置的。


存在限制


64位系统
物理内存 > 16G => 最大堆内存限制为4G
物理内存 <= 16G => 最大堆内存限制为2G

32位系统
最大堆内存限制为1G


堆内存是计算机系统中,当多个程序同时运行时,为了这些进程能够共享数据、交换信息而把它们的数据存放在一个连续的区域。它是一个连续的内存区域,在物理上并不存在。



何为内存


内存(Memory)是计算机的重要部件,也称内存储器和主存储器,它用于暂时存放CPU中的运算数据,以及与硬盘等外部存储器交换的数据。它是外部存储器与CPU进行沟通的桥梁,计算机中所有程序的运行都在内存中进行,内存性能的强弱影响计算机整体发挥的水平。只要计算机开始运行,操作系统就会把需要运算的数据从内存调到CPU中进行运算,当运算完成,CPU将结果传送出来。


所以内存的运行决定计算机整体运行快慢。


为何限制


Chrome之所以限制了内存的大小,表面上的原因是V8最初是作为浏览器的JavaScript引擎而设计,不太可能遇到大量内存的场景,而深层次的原因:则是由于V8的垃圾回收机制的限制。


由于V8需要保证JavaScript应用逻辑与垃圾回收器所看到的不一样,V8在执行垃圾回收时会阻塞 JavaScript应用逻辑,直到垃圾回收结束再重新执行JavaScript应用逻辑,这种行为被称为“全停顿”(stop-the-world)。


若V8的堆内存为1.5GB,V8做一次小的垃圾回收需要50ms以上,做一次非增量式的垃圾回收甚至要1秒以上。这样浏览器将在1s内失去对用户的响应,造成假死现象。如果有动画效果的话,动画的展现也将显著受到影响。


chrome网页是如何占用内存的


chrome之所以很吃内存,是因为chrome使用了多进程机制,每一个chrome的标签页以及每一个扩展,都是独立的进程。在目前的chrome进程架构里,访问一个网站至少包含四个进程:一个浏览器进程、一个GPU进程、一个渲染进程和一个网络进程。除此之外还有包含多个插件进程组成chrome的进程架构。



1. V8


V8 是google 开发的开源高性能 javascript引擎,V8引擎用C++语言开发,被用在Google的chrome浏览器,android 浏览器js引擎默认也用V8。


​ V8最初是为了提高web浏览器中的JavaScript运行性能设计的。为了提升性能,V8将JavaScript代码翻译为更高效的机器语言,而不是使用解释程序。它通过实现一个JIT(Just-In-Time,即时) 编译器来将JavaScript代码编译为机器语言,就像很多现代JavaScript引擎如SpiderMonkey或Rhino(Mozilla)做的那样。V8和它们主要的区别是它不会生成字节码或其他中间代码。


1.1 V8如何执行JavaScript



V8执行js的主要流程如下:



  • 准备执行JS需要的基础环境

  • 解析源码生成ast和作用域

  • 依据ast和作用域生成字节码

  • 解释器解释执行字节码

  • 监听热点代码

  • 编译器优化热点代码为二进制的机器码

  • 反优化二进制机器代码


1.1.1 准备执行JS需要的基础环境


这些基础环境包括:



  • 堆空间和栈空间

  • 全局执行上下文

  • 全局作用域

  • 内置函数

  • 宿主环境提供的扩展函数和对象

  • 事件循环系统


1. 堆空间


堆空间是一种树形的存储结构,用来存储对象类型的离散的数据,以及一些占用内存比较大的数据。


存在堆空间的:



  • 函数

  • 数组

  • 在浏览器中还有 window 对象

  • document 对象等


2. 栈空间


栈空间主要是用来管理 JavaScript 函数调用的,栈是内存中连续的一块空间,同时栈结构是“先进后出”的策略。


特点:



  • 先进后出

  • 空间连续

  • 查找效率非常高


函数调用过程中,什么会存在栈里:



  • 原生类型

  • 引用到的对象的地址

  • 函数的执行状态

  • this 值等


3. 全局执行上下文


V8 初始化了基础的存储空间之后,接下来就需要初始化全局执行上下文和全局作用域。 当 V8 开始执行一段可执行代码时,会生成一个执行上下文来维护执行当前代码所需要的变量声明、this 指向等。


执行上下文中主要包含:



  • 变量环境

  • 词法环境:包含了使用 let、const 等变量的内容

  • this 关键字


全局执行上下文在 V8 的生存周期内是不会被销毁的,它会一直保存在堆中。


4. 全局作用域


var x = 5
{
let y = 2
const z = 3
}

这段代码在执行时,会有两个对应的作用域,一个是全局作用域,另外一个是括号内部的作用域,但是这些内容都会保存到全局执行上下文中。


5. 内置函数


JavaScript的内置函数是浏览器内核自带的,不用任何函数库引入就可以直接使用的函数。JavaScript内置函数一共可分为五类:



  • 常规函数

  • 数组函数

  • 日期函数

  • 数学函数

  • 字符串函数


6. 宿主环境提供的扩展函数和对象


什么是宿主环境?


宿主环境可以是浏览器中的渲染进程,可以是 Node.js 进程, 也可以是其他的定制开发的环境,而这些宿主则提供了很多 V8 执行 JavaScript 时所需的基础功能部件。


7. 事件循环系统(Event Loop)


V8 还需要有一个主线程,用来执行 JavaScript 和执行垃圾回收等工作。


V8 是寄生在宿主环境中的,它并没有自己的主线程,而是使用宿主所提供的主线程,V8 所执行的代码都是在宿主的主线程上执行的。


在执行完代码之后,为了让线程继续运行,通常的做法是在代码中添加一个循环语句,在循环语句中监听下个事件。


如果主线程正在执行一个任务,这时候又来了一个新任务,那么这种情况下就需要引入一个任务队列,这个任务队列是放在了事件触发线程,让新任务暂存到任务队列中,等当前的任务执行结束之后,再从消息队列中取出正在排队的任务。当执行完一个任务之后,我们的事件循环系统会重复这个过程,继续从消息队列中取出并执行下个任务。


事件循环系统主要用来处理任务的排队和任务的调度。


1.1.2 解析源码生成ast和作用域


V8接收到JavaScript源代码后,解析器(Parser)会对其进行词法分析和语法分析,结构化JavaScript字符串,生成AST(抽象语法树)。


解析代码需要时间,所以 JavaScript 引擎会尽可能避免完全解析源代码文件。另一方面,在一次用户访问中,页面中会有很多代码不会被执行到,比如,通过用户交互行为触发的动作。


正因为如此,所有主流浏览器都实现了惰性解析(Lazy Parsing)。解析器不必为每个函数生成 AST(Abstract Syntax tree,抽象语法树),而是可以决定“预解析”(Pre-parsing)或“完全解析”它所遇到的函数。


预解析会检查源代码的语法并抛出语法错误,但不会解析函数中变量的作用域或生成 AST。完全解析则将分析函数体并生成源代码对应的 AST 数据结构。相比正常解析,预解析的速度快了 2 倍。


生成 AST 主要经过两个阶段:分词和语义分析。AST 旨在通过一种结构化的树形数据结构来描述源代码的具体语法组成,常用于语法检查(静态代码分析)、代码混淆、代码优化等。


V8 的 AST 表示方式


1.1.3 依据ast和作用域生成字节码


V8 引入 JIT(Just In Time,即时编译)技术,通过 Ignition 基线编译器快速生成字节码进行执行。


字节码是机器码的抽象,字节码可以直接被优化编译器 TurboFan 用于生成图(TurboFan 对代码的优化基于图),避免优化编译器在优化代码时需要对 JavaScript 源代码重新进行解析。


1.1.4 优化编译器 TurboFan


解释器执行字节码过程中,如果发现代码被重复执行,监控机器人会把这段代码标记为热点代码。热点代码会丢给优化编译器编译成二进制代码,然后优化。下次再执行时就执行这段优化后的二进制代码。


1.1.5 反优化


JS 语言是动态语言,非常之灵活,对象的结构和属性在运行时是可以发生改变的,设想一个问题,如果热代码在某次执行的时候,突然其中的某个属性被修改了,那么编译成机器码的热代码还能继续执行吗?


答案是肯定不能。这个时候就要使用到优化编译器的反优化了,他会将热代码退回到 AST 这一步,这个时候解释器会重新解释执行被修改的代码,如果代码再次被标记为热代码,那么会重复执行优化编译器的这个步骤。


1.2 内存管理


内存是计算机中重要的部件之一,它是与CPU进行沟通的桥梁。


计算机中所有程序的运行都是在内存中进行的,因此内存的性能对计算机的影响非常大。


高效的程序离不开内存的有效管理,内存管理的优势:



  • 减少内存分配

  • 回收开销

  • 避免内存碎片

  • 定位内存位置

  • 方便内存整理

  • 跟踪内存使用


1.2.1 V8 引擎的内存结构


因为 JavaScript 是单线程,所以 V8 在每个上下文都使用一个进程,如果你使用 Service Worker ,它也会为每个 Service Worker 生成一个新的进程。



Service Worker:一个服务器与浏览器之间的中间人角色,如果网站中注册了service worker那么它可以拦截当前网站所有的请求,进行判断(需要编写相应的判断程序),如果需要向服务器发起请求的就转给服务器,如果可以直接使用缓存的就直接返回缓存不再转给服务器,从而大大提高浏览体验。



一个正在运行的程序是由 V8 进程分配的内存来表示的,这被称为 Resident Set(常驻集)。这些内存会进一步划分成不同的部分。


一个 V8 进程的内存通常由以下几个块构成:



  1. **新生代内存区(new space)

    **大多数的对象都会被分配在这里,这个区域很小但是垃圾回收比较频繁;

  2. 老生代内存区(old space)

    属于老生代,这里只保存原始数据对象,这些对象没有指向其他对象的指针;

  3. **大对象区(large object space)

    **这里存放体积超越其他区大小的对象,每个对象有自己的内存,垃圾回收其不会移动大对象区;

  4. 代码区(code space)

    代码对象,会被分配在这里。唯一拥有执行权限的内存;

  5. map 区(map space)

    存放 Cell 和 Map,每个区域都是存放相同大小的元素,结构简单。


如下图:



Heap Memory(堆内存)


这是 V8 引擎存储对象(Object)和动态数据(Dynamic Data)的地方。这也是程序对于内存区域中最大的一块地方,同时**垃圾回收( GC )**也发生在这里。并不是整个 Heap (堆)内存都进行垃圾回收,只有新空间(New Space)和旧空间(Old Space)由垃圾回收管理。


整个堆内存被划分为以下几个部分:




  • 新空间:是新对象存活的地方,这些对象的生命周期都很短。这个空间很小,由两个 Semi-Space 组成,类似与 JVM 中的 S0 和 S1。

    我们将会在后面的内容看到它。新空间的大小是由两个 V8 中的标志位来控制: min_semi_space_size(Initial) 和 max_semi_space_size(Max) 。




  • 旧空间:在新空间中存活了两个 minor GC 周期的对象,会被迁移到这里。

    这个空间由 Major GC(Mark-Sweep & Mark-Compact) 管理。我们也会在后面内容中看到它。旧空间的大小也是由两个 V8 中的标志位来控制:nitial_old_space_size(Initial) 和 max_old_space_size(Max) 。

    旧空间被分成两个部分:




  • 旧指针空间:这些存活下来的的对象都包含了指向其他对象的指针。




  • 旧数据空间:这些对象只包含数据,没有指向其他对象的指针。在新空间中存活两个 minor GC 周期后,String,已经装箱的数字,未装箱的双精度数组会被迁移到这里。




  • 大型对象空间(Large object space):大于其他空间大小限制的对象存放在这里。每个对象都有自己的内存区域,这里的对象不会被垃圾回收器移动。




  • 代码空间(Code-space):这是即时编译器(JIT)存储已经编译的代码块的地方。这是唯一可执行内存的空间(尽管代码可能被分配到大型对象空间(Large object space),那也是可以执行的)。




  • 单元空间(Cell Space),属性单元空间(Property Cell Space)和映射空间(Map Space):这些空间分别存放 Cell,PropertyCell 和 Map。这些空间包含的对象大小相同,并且对对象类型有些限制,可以简化回收工作。




每个空间(除了大型对象空间)都由一组 Page 组成。一个 page 是由操作系统分配的一个连续内存块,大小为 1MB。


Stack(栈)


每个 V8 进程都有一个栈(Stack),这里保存静态数据的地方,比如:方法/函数框架,原型对象的值(Primitive value)和指针。栈(Stack)内存的大小由 V8 的标志位来设置:stack_size。



  1. 全局作用域被保存在 Stack 中的 Global frame 中。

  2. 每个函数调用都做为 frame 块添加到 Stack 中。

  3. 所有的局部变量,包括参数和返回值都保持在 Stack 的函数 frame 上。

  4. 所有的原型类型的数据,比如 int 和 String,都直接保持在 Stack 上。(是的,JavaScript 中 String 是原型数据)

  5. 所有的对象类型,比如 Employee 和 Function 都保存在 Heap 中,并且通过 Stack 上的指针来引用。(函数在 JavaScript 中也是对象。)

  6. 从当前函数中调用的函数被压入了 Stack 的顶部。

  7. 当函数返回是,它的 frame 块将会从 Stack 中移除。

  8. 一旦主进程完成,Heap 上的对象就不再有任何来自 Stack 的指针,这些对象将成为孤儿。

  9. 除非显式的复制,否则其他对象中的所有对象引用都是通过指针完成的。


正如你所看到的,Stack 是自动管理的,而且是由操作系统而不是 V8 本身完成的。因此我们不必担心 Stack 的问题。另一方面,Heap 不是由操作系统自动管理的,由于 Heap 是程序内存块中最大的内存空间,并且保存动态数据,所以它的空间使用会指数级增长,从而导致我们的程序内存耗尽。


1.2.2 V8 内存的使用


我们通过一段代码来看JS程序被执行时是如何使用内存的。


class Employee {
constructor(name, salary, sales) {
this.name = name;
this.salary = salary;
this.sales = sales;
}
}

const BONUS_PERCENTAGE = 10;

function getBonusPercentage(salary) {
const percentage = (salary * BONUS_PERCENTAGE) / 100;
return percentage;
}

function findEmployeeBonus(salary, noOfSales) {
const bonusPercentage = getBonusPercentage(salary);
const bonus = bonusPercentage * noOfSales;
return bonus;
}

let john = new Employee("John", 5000, 5);
john.bonus = findEmployeeBonus(john.salary, john.sales);
console.log(john.bonus);

动图封面


就像你看到的那样:



  1. 全局作用域被保存在 Stack 中的 Global frame 中。

  2. 每个函数调用都做为 frame 块添加到 Stack 中。

  3. 所有的局部变量,包括参数和返回值都保持在 Stack 的函数 frame 上。

  4. 所有的原型类型的数据,比如 int 和 String,都直接保持在 Stack 上。

  5. 所有的对象类型,比如 Employee 和 Function 都保存在 Heap 中,并且通过 Stack 上的指针来引用。

  6. 从当前函数中调用的函数被压入了 Stack 的顶部。

  7. 当函数返回是,它的 frame 块将会从 Stack 中移除。

  8. 一旦主进程完成,Heap 上的对象就不再有任何来自 Stack 的指针,这些对象将成为孤儿。

  9. 除非显式的复制,否则其他对象中的所有对象引用都是通过指针完成的


Stack 是自动管理的,而且是由操作系统而不是 V8 本身完成的。因此我们不必担心 Stack 的问题。另一方面,Heap 不是由操作系统自动管理的,由于 Heap 是程序内存块中最大的内存空间,并且保存动态数据,所以它的空间使用会指数级增长,从而导致我们的程序内存耗尽。


而且 Heap 中的内存也会随着时间的推移,变得支离破碎,从而拖慢程序。这时候就需要垃圾回收发挥作用了。


1.3 垃圾回收 Garbage collection



垃圾回收是指回收那些在应用程序中不再引用的对象,当一个对象无法从根节点访问这个对象就会做为垃圾回收的候选对象。这里的根对象可以为全局对象、局部变量,无法从根节点访问指的也就是不会在被任何其它活动对象所引用。



我们知道了 V8 是如何分配内存的,现在让我们来看看它是如何自动管理 Heap 内存的,这对程序的性能非常重要。


当程序试图在 Heap 中分配超过可用的内存时,就会遇到内存不足的错误,整个页面都会崩溃。


一个不正确的 Heap 内存管理也可能导致内存泄漏。


V8 引擎通过垃圾回收来管理 Heap 内存。简单来说,就是释放孤立(orphan)对象使用的内存。比如,一个对象并没有直接或者间接被 Stack 中的指针所引用,就会释放相应内存为新对象腾出空间。


V8 的垃圾回收器负责回收未使用的内存,以便 V8 进程重新使用。


1.3.1 如何判断非活跃对象


判断对象是否是活跃的一般有两种方法,引用计数法和可访问性分析法。


1. 引用计数法


V8中并没有使用这种方法,因为每当有引用对象的地方,就加1,去掉引用的地方就减1,这种方式无法解决A与B循环引用的情况,引用计数都无法为0,导致无法完成gc。


2. 可访问性分析法


V8中采用了这种方法,将一个称为GC Roots的对象(在浏览器环境中,GC Roots可以包括:全局的window对象,所有原生dom节点集合等等)作为所有初始存活的对象集合,从这个对象出发,进行遍历,遍历到的就认为是可访问的,为活动对象,需要保留;如果没有遍历到的对象,就是不可访问的,这些就是非活动对象,可能就会被垃圾回收


在浏览器环境中,GC Root 有很多,通常包括了以下几种 (但是不止于这几种):



  • 全局的 window 对象(位于每个 iframe 中)。

  • 文档 DOM 树,由可以通过遍历文档到达的所有原生 DOM 节点组成。

  • 存放栈上变量。



1.3.2 代际假说


代际假说(The Generational Hypothesis)是垃圾回收领域中的一个重要术语,它有两个特点




  1. 大部分对象在内存中存活时间很短,比如函数内部声明变量,块级作用域中的变量等,这些代码块执行完分配的内存就会被清掉。




  2. 不死的对象会活得更久,比如全局的window、Dom、全局api等对象。




基于代际假说的理论,在V8引擎中,垃圾回收算法被分为两种,一个是Major GC,主要使用了Mark-Sweep & Mark-Compact算法,针对的是堆内存中的老生代进行垃圾回收;


另外一个是Minor GC,主要使用了Scavenger算法,针对于堆内存中的新生代进行垃圾回收。



注:

所谓老生代指的就是那些存活时间很久没有被清理的对象,而新生代指的是存活时间很短的对象。



1.3.3 Scavenger算法


是在新生代内存中使用的算法,速度更快,空间占用更多的算法。New space区域分为了两个半区,分别为from-space和to-space。不断经过下图中的过程,在两个空间的角色互换中,完成垃圾回收的过程。每次都会有对象复制的操作,为了控制这里产生的时间成本和执行效率,往往新生代的空间并不大。同时为了避免长时间之后,某些对象会一直积压在新生代区域,V8制定了晋升机制,满足任一条件就会被分配到老生代的内存区中。




  1. 经历一次Scavenger算法后,仍未被标记清除的对象。




  2. 进行复制的对象大于to space空间大小的25%。





1.3.4 Mark-Sweep & Mark-Compact算法


是老生代内存中的垃圾回收算法,标记-清除 & 标记-整理,老生代里面的对象一般占用空间大,而且存活时间长,如果也用Scavenger算法,复制会花费大量时间,而且还需要浪费一半的空间。



  • 标记-清除过程:也就是可访问性分析法,从GC Root开始遍历,标记完成后,就直接进行垃圾数据的清理工作。




  • 标记-整理过程:清除算法后会产生大量不连续的内存碎片,碎片过多会导致后面大对象无法分配到足够的空间,所以需要进行整理,第一步的标记是一样的,但标记完成活跃对象后,并不是进行清理,而是将所有存活的对象向一端移动,然后清理掉这端之外的内存。



1.3.5 优化策略


由于 JavaScript 是运行在主线程之上的,因此,一旦执行垃圾回收算法,都需要将正在执行的 JavaScript 脚本暂停下来,待垃圾回收完毕后再恢复脚本执行。这种行为叫做全停顿(Stop-The-World)。


STW(全停顿)会造成系统周期性的卡顿,对实时性高的和与时间相关的任务执行成功率会有非常大的影响,例如:js逻辑需要执行动画,刚好碰到gc的过程,会导致整个动画卡顿,用户体验极差。


为了降低这种STW导致的卡顿和性能不佳,V8引擎中目前的垃圾回收器名为Orinoco,经过多年的不断精细化打磨和优化,已经具备了多种优化手段,极大地提升了GC整个过程的性能及体验。



Orinoco 是 V8 GC 项目的代号,它利用并行,增量和并发的技术进行垃圾回收,来释放主线程。



1.3.6 并行回收


简单来讲,就是主线程执行一次完整的垃圾回收时间比较长,开启多个辅助线程(web-worker)来并行处理,整体的耗时会变少,所有线程执行GC的时间点是一致的,js代码也不会有影响,不同线程只需要一点同步的时间,在新生代里面执行的就是并行策略。



1.3.7 增量回收


并行策略说到底还是STW(全停顿)的机制,如果老生代里面存放一些大对象,处理这些依然很耗时,Orinoco又增加了增量回收的策略。将标记工作分解成小块,插在主线程不同的任务之间执行,类似于React fiber的分片机制,等待空闲时间分配。这里需要满足两个实现条件:


1. 随时可以暂停和启动,暂停要保存当前的结果,等下一次空闲时机来才能启动。


2. 暂停时间内,如果已经标记好的数据被js代码修改了,回收器要能正确地处理。



下面要讲到的就是Orinoco引入了三色标记法来解决随时启动或者暂停且不丢之前标记结果的问题。


1.3.8 三色标记法


三色标记法的规则如下:


1. 最开始所有对象都是白色状态


2. 从GC Root遍历所有可到达的对象,标记为灰色,放入待处理队列


3. 从待处理队列中取出灰色对象,将其引用的对象标记为灰色放入待处理队列,自身标记为黑色


4. 重复3中动作,直到灰色对象队列为空,此时白色对象就是垃圾,进行回收。



垃圾回收器可以依据当前内存中有没有灰色节点,来判断整个标记是否完成,如果没有灰色节点了,就可以进行清理工作了。如果还有灰色标记,当下次恢复垃圾回收器时,便从灰色的节点开始继续执行。


下面将要解决由于js代码导致对象引用发生变化的情况,Orinoco借鉴了写屏障的处理办法。


1.3.9 写屏障(write-barrier)


一旦对象发生变化时,如何精确地更新标记的结果,我们可以分析下一般js执行过程中带来的对象的变化有哪些,其实主要有2种:


1. 标记过的黑色或者灰色的对象不再被其他对象所引用。


2. 引入新的对象,新的对象可能是白色的,面临随时被清除的危险,导致代码异常。


第一种问题不大,在下次执行gc的过程中会被再次标记为白色,最后会被清空掉;


第二种就使用到了写屏障策略,一旦有黑色对象引用到了白色对象,系统会强制将白色对象标记成为灰色对象,从而保证了下次gc执行时状态的正确,这种模式也称为强三色原则。


1.3.10 并发回收


虽说三色标记法和写屏障保证了增量回收的机制可以实现,但依然改变不了需要占用主线程的情况,一旦主线程繁忙,垃圾回收依然会影响性能,所以增加了并发回收的机制。


V8里面的并发机制相对复杂,简化来看,当主线程运行代码时,辅助线程并发进行标记,当标记完成后,主线程执行清理的过程时,辅助线程也并行执行。



1.4 D8


D8 是一个十分有用的调试工具,你能够把它看成是 debug for V8 的缩写。我们可以应用 d8 来查看 V8 在执行 JavaScript 过程中的各种两头数据,例如作用域、AST、字节码、优化的二进制代码、垃圾回收的状态,还能够应用 d8 提供的公有 API 查看一些外部信息。


该工具的下载教程和使用方式:blog.csdn.net/heyYouU/art…


2. memory cache


在我们使用强缓存+协商缓存的时候,我们会将一部分资源放在内存中缓存起来。


内存中的缓存,主要包含的是当前中页面中已经抓取到的资源,例如页面上已经下载的样式、脚本、图片等。读取内存中的数据肯定比磁盘快,内存缓存虽然读取高效,可是缓存持续性很短,会随着进程的释放而释放。一旦我们关闭 Tab 页面,内存中的缓存也就被释放了。


我们上面谈到了,V8对堆内存的大小做了限制,如果超过了限制会导致网络崩溃的现象,那么我们的memory cache的占用内存受不受V8的约束呢。


当然是受约束的,如果要缓存大量的资源,还得需要用到磁盘缓存。


参考: blog.csdn.net/qiwoo_weekl…


作者:黑色的枫
来源:juejin.cn/post/7221793823704514620
收起阅读 »

必须会的前端基础通用优化方法

web
为什么要做优化? 虽然现在网速越来越快,客户端性能越来越好。但是还是有很多人在使用老旧的设备,不稳定的或者2G、3G网络。另外用户现在对应用的体验要求也越来越高,用户不仅会拿你的应用和同行业的竞争对手去做比较,并且会跟使用过的做的最好的应用去比较。 优化应用的...
继续阅读 »

为什么要做优化?


虽然现在网速越来越快,客户端性能越来越好。但是还是有很多人在使用老旧的设备,不稳定的或者2G、3G网络。另外用户现在对应用的体验要求也越来越高,用户不仅会拿你的应用和同行业的竞争对手去做比较,并且会跟使用过的做的最好的应用去比较。


优化应用的性能可以提升用户体验从而提高留存率,转化率。谷歌、微软和亚马逊的研究都表明,性能可以直接转换成收入。比如,Bing搜索网页时延迟2000ms会导致每用户收入减少4.3%。BBC发现他们的网站加载时间每增加一秒,他们就会失去10%的用户。


下面分享一些操作简单但是效果明显的优化方法。


1、使用HTTP 2.0


HTTP 2.0通过支持首部字段压缩和多路复用技术,让应用更有效地利用网络资源,减少感知的延迟时间。


二进制分帧机制是HTTP 2.0大幅度提高网页性能的核心,它定义了如何封装HTTP消息并在客户端与服务器之间传输。HTTP 1.x的版本都是通过文本的方式传递数据,而HTTP 2.0将传输的信息分割为更小的消息和帧,并对它们采用二进制格式的编码。


HTTP 2.0在二进制分帧的基础上实现了多路复用技术,可以在同一连接上同时发送多个请求和响应,解决了HTTP 1.x的队头阻塞问题,提高了并行处理能力和性能,突破了HTTP 1.x中每个连接每次只交付一个响应的限制。


HTTP 2.0使用HPACK算法对请求和响应头部进行压缩,减少了数据传输量, 可以显著减少每个请求的开销,提高了网络传输速度。而且,它还支持服务器到客户端的主动推动推送机制。


体验demo:http2.akamai.com/demo


开启http2的方法也非常简单,下面以nginx为例


server {
-      listen       443 ssl;
+      listen       443 ssl http2;
       ...
}

2、缓存资源


浏览器发出的所有HTTP请求首先会转至浏览器缓存,用于检查是否存在可满足请求的有效缓存响应。如果存在匹配,则从缓存中读取响应,从而消除网络延迟和传输产生的数据成本。


HTTP缓存是一种提高负载性能的有效方式,因为它减少了不必要的网络请求。所有浏览器都支持该功能,并且不需要太多设置。默认情况下,大部分Web服务器内置支持设置缓存相关表头的设置。


3、缩小和压缩传输的资源


对传输的资源进行缩小和压缩可以有效减少负载大小,进而缩短页面加载时间。


像webpack中已经内置了缩小代码的插件,不需要做额外的工作就可以直接使用,可以删除空格和不需要的代码。


压缩是使用压缩算法修改数据的过程。目前使用最广泛的压缩格式是Gzip,但可以有限考虑使用Brotli,2015年谷歌推出的Brotli压缩算法能在Gzip的基础上将数据再压缩20~25%,现在大部分的浏览器已经支持这种压缩格式,国外很多站点已经开始使用,但是国内还没有开始大规模的应用。很多托管平台、CDN和反向代理服务器默认情况下都会对资产进行压缩编码,或者经过简单的配置就可以轻松实现。下面以Express为例配置一下动态压缩。


const express = require('express');
const compression = require('compression');

const app = express();

app.use(compression());
app.use(express.static('public'));

const listener = app.listen(process.env.PORT, () => {
    console.log(`Your app is listening on port ${listener.address().port}`)
})


4、使用CDN(内容分发网络)


由离用户更近的服务器向用户提供数据,可以显著减少每次TCP连接的网络延迟,增大吞吐量。选择一个可靠的CDN服务提供商进行简单的配置就可以,如阿里云、腾讯云、百度云等。


5、图片处理


对图片处理可以很好得对图片就行优化,经过图片处理优化的图像可以节省40%~80%的大小。虽然通过构建脚本也可以实现图片处理的效果,但在实践中一般使用第三方提供的图像CDN,第三方图像CDN也可以提供更多形式的图像处理方式。通过向文件地址传递参数来获取合适的图像,而不是直接获取原文件。


比如在chrome浏览器中使用WebP格式图片,WebP是由谷歌开发的一种新型图片格式,相比JPEG和PNG格式,WebP图片可以更好地压缩图片大小,从而提高页面加载速度。


6、优先加载关键资源


优先加载关键资源,延迟加载次要资源。优先加载关键资源可以减少页面加载时间,加快页面的渲染速度,提高用户体验。可以对网站进行分析,确定哪些资源是关键资源,然后将非关键资源设置为延迟加载。


7、利用chrome性能工具


Chrome浏览器的Lighthouse扩展程序可以对网站进行测试并生成一个性能报告。Lighthouse生成的报告包含了网站性能、可访问性、最佳实践和SEO等方面的评估结果,以及优化建议。分析测试结果,找出需要改进的方面,并根据建议进行优化。


总之,前端优化是提高用户体验、提高网站性能、减少成本和支持更多设备的关键因素之一。上述优化方法可以帮助开发人员优化应用程序的性能,提高用户体验和满意度,从而提高留存率和转化率,增加收入。


作者:liupl
来源:juejin.cn/post/7219241334926180410
收起阅读 »

项目很大,得忍一下

web
背景 常和我们的客户端厮混,也经常陪他们发版,每次发版编译打包都可以在那边玩一局游戏了。一边幸灾乐祸,一边庆幸h5编译还好挺快的,直到我们的项目也发展成了*山,巨石项目。由于线上要给用户查看历史的推广活动,所以很多老的业务项目都还是留在项目中,导致我们的rou...
继续阅读 »

背景


常和我们的客户端厮混,也经常陪他们发版,每次发版编译打包都可以在那边玩一局游戏了。一边幸灾乐祸,一边庆幸h5编译还好挺快的,直到我们的项目也发展成了*山,巨石项目。由于线上要给用户查看历史的推广活动,所以很多老的业务项目都还是留在项目中,导致我们的router层爆炸,打包速度直线下降,开发过程中,开了hmr稍微有点改动也要等个几秒钟,恨不得立刻重启一个新项目。但是现实告诉你,忍住,别吐,后面还有更多的业务活动加进来。那么怎么解决这个问题呢,这个时候mp的思路是个不错的选择。


关键点


打包慢,本质原因是依赖庞大,组件过多。开发过程中,我们开新的业务组件时,往往和其他业务组件是隔离的,那么我们打包的时候是不是可以把那些不相干的业务组件隔离出去,当然可以。打包工具,从入口开始进行扫描,单页面的模块引入基本都是借助router,所以,关键的是如果我们能够控制router的数量,其实就能够控制编译和打包规模了。


问题


router在vue项目中我们常用的是全家桶的库vue-router,vue-router最多提供了懒加载,动态引入功能并不支持。有小伙伴说router的引入路径可不可以动态传入,我只能说小伙子你很机智,但是vue-router并不支持动态的引入路径。因此我们换个思路,就是在入口的位置控制router的规模,通过不同规模的router实例来实现router的动态引入。当然这需要我们对router库进行一定改造,使其变的灵活易用


一般的router


通常的router如下:



// router.js

/*global require*/

const Vue = require('vue')

const Router = require('vue-router')

Vue.use(Router)

const routes = [

{

path: '/routermap',

component: (resolve) => require(['../containers/map.vue'], resolve),

name: 'routermap',

desc: '路由列表'

},

{

path: '/',

component: (resolve) => require(['../containers/index.vue'], resolve)

},

{

path: '*',

component: (resolve) => require(['../containers/nofound.vue'], resolve),

name: 'defaultPage',

desc: '默认页'

}

]

const router = new Router({

mode: 'history',

routes

})

router.afterEach((to, from) => {

///

})

export default router

// 引入 entry.js

import router from './router.js'

router.beforeEach((to, from, next) => {

///

next()

})

router.afterEach(function(to, from) {

///

})

new Vue({

el: '#app',

template: '<App/>',

router,

})


我们可以不断的往routes数组中添加新的router item来添加新的业务组件,这也是我们的项目不断变大的根本,这样既不好维护,也会导致后面的编译效率


易于维护和管理的router


其实好的管理和维护本质就是分门别类,把类似功能的放在一起,而不是一锅粥都放在一起,这样基本就能解决追踪维护的功能,对应router管理其实也不是很复杂,多建几个文件夹就行如下:


router.png


对应routes/index.js代码如下:



import testRouter from './test.js'

const routes = [

{

path: '/map',

component: (resolve) => require(['../containers/map.vue'], resolve),

name: 'map',

desc: '路由列表'

},

{

path: '/',

component: (resolve) => require(['../containers/index.vue'], resolve)

},

...testRouter,

// 可以扩展其他router

{

path: '*',

component: (resolve) => require(['../containers/nofound.vue'], resolve),

name: 'defaultPage',

desc: '默认页'

}

]

// test.js

/**

* 测试相关页面路由映射

*/


/*global require*/

export default [

{

path: '/test/tools',

name: 'testTools',

component: resolve => require(['@test/tools/index.vue'], resolve),

desc: '测试工具'

}

]


我们通过把router分为几个类别的js,然后在通过router item的数组展开合并,就做到了分门别类,虽然看似简单,但是可以把管理和维护效果提升几个数量级。


支持mp的router


虽然上面支持了易于管理和维护,但是实际上我们如果只是单一入口的话,导出的还是一个巨大的router。那么如何支持多入口呢,其实也不用想的过于复杂,我们让类似test.js的router文件既支持router item的数组导出,也支持类似routes/index.js一样的router实例导出即可。所谓既能分也能合才是最灵活的,这里我们可以利用工厂模式做一个factory.js,如下:



/**

* app 内的页面路由映射

*/


/*global require*/

const Vue = require('vue')

const Router = require('vue-router')

Vue.use(Router)

const RouterFactory = (routes) => {

return new Router({

mode: 'history',

routes: [

{

path: '/map',

component: (resolve) => require(['../containers/map.vue'], resolve),

name: 'map',

desc: '路由列表'

},

{

path: '/',

component: (resolve) => require(['../containers/index.vue'], resolve)

},

...routes,

{

path: '*',

component: (resolve) => require(['../containers/nofound.vue'], resolve),

name: 'defaultPage',

desc: '默认页'

}

]

})

}

export default RouterFactory


这个factory.js产出的router实例和routes/index.js一模一样所以我们只需组装一下test.js即可,如下:



/*global require*/

import RouterFactory from './factory'

export const testRouter = [

{

path: '/test/tools',

name: 'testTools',

component: resolve => require(['@test/tools/index.vue'], resolve),

desc: '测试工具'

}

]

export default RouterFactory(developRouter)

// routes/index.js的引入变化一下即可

import testRouter from './test.js'

// 修改为=》

import { testRouter } from './test.js'


那么我们的入口该如何修改呢?也很简单:



// testEntry.js

import router from './routes/test.js'

router.beforeEach((to, from, next) => {

///

next()

})

router.afterEach(function(to, from) {

///

})

new Vue({

el: '#app',

template: '<App/>',

router,

})


我们建立了一个新的入口文件 testEntry.js 这个入口只引入了test相关的模块组件


如何灵活的和编译命令做配合呢


根据上面,我们进行mp改造的基础已经做好,关于如何多入口编译webpack或者其他打包里面都是基础知识,这里就不多赘述。这里主要聊一下如何灵活的配合命令做编译和部署。


既然router我们都可以分为不同的文件,编译文件我们同样可以拆分为不同的文件,这也使得我们的命令可以灵活多变,这里我们以webpack做为示例:


config.png


config1.png


config2.png


config3.png


根据上图示例 我们的webpack的配置文件仅仅改动了entry,我们稍微改造一下build.js,使其能够接受不同的编译命令:



// build.js

let page = 'all'

if (process.argv[2]) {

page = process.argv[2]

}

let configMap = {

'all': require('./webpack.prod.conf'),

'app': require('./webpack.app.conf')

}

let webpackConfig = configMap[page]

// dev-server.js

let page = 'all'

if (process.argv[2]) {

page = process.argv[2]

}

let configMap = {

'all': require('./webpack.dev.conf'),

'app': require('./webpack.app.dev.conf')

}

let webpackConfig = configMap[page]


对应的脚本配置:



// package.json

"scripts": {

"dev": "node build/dev-server.js",

"build": "node build/build.js",

"build:app": "node build/build.js app",

"dev:app": "node build/dev-server.js app"

},


以上app对应test。最后,我们只需要在命令行执行相应命令,即可实现我们可控的router规模的开发,基本随便来新的需求,咱都可以夜夜做新郎,怎么搞都是飞速。当然部署的话我们也可以单独执行一部分页面的部署命令到单独的域名,换个思路也可以作为一种预发测试的部署方法。



#
整体项目的开发编译

npm run dev

#
单独的app,也即test项目的开发编译

npm run dev:app

#
整体项目的部署

npm run build

#
单独的app,也即test项目的部署

npm run build:app


结语


以上,即如何利用mp思路,提高我们的编译开发效率。时常有人会在提高网页性能的时候说到mp,但mp本质上并不能提高页面的性能,比如白屏优化。而路由中使用懒加载其实才是提高部分网页性能的出力者,关于白屏优化,本篇文章不作展开讨论。


作者:CodePlayer
来源:juejin.cn/post/7218866717739696183
收起阅读 »

周末闲来无事,做了一个能动的宣传页

web
最近在用可画(canva),制作一些素材,海报活动页面,这不情人节快到了吗?基于海报模版,设计自己的页面倒是简单,但是都是静态页面,想着能不能让页面的元素都动起来(Everybody跟我一起嗨嗨嗨!!)。 两个方案 纯CSS animate库 CSS基于ani...
继续阅读 »

创建项目

最近在用可画(canva),制作一些素材,海报活动页面,这不情人节快到了吗?基于海报模版,设计自己的页面倒是简单,但是都是静态页面,想着能不能让页面的元素都动起来(Everybody跟我一起嗨嗨嗨!!)。


两个方案


纯CSS animate库


CSS基于animate库



  1. 利用animate动效,给页面上所有的image和text元素加上className,借助--var全局css变量属性,给元素依次加上delay、duration、index序号、初始化信息rotate、offset、easing等等,我会在码上掘金给一个css的demo版本。CSS版本相对简单一些,只需要循环给所有元素加上对应动画,计算执行时间,延迟时间,页面就可以动起来了。


// 定义的数据结构 Image\Text
[{
"id": "Image/Text-xx",
"type": "Image/Text",
"name": "图片/文本",
"css": {
"top": 0,
"left": 0,
"width": 414,
"height": 736,
"zIndex": 1,
"opacity": 1,
"fontSize": 18,
},
"animationObj": {
{
"delay": 1000,
"duration": 3030,
"type": "flipInY",
"easing": '',
"index": 8,
"rotate_angle": -6.6,
"offset": -112.5,
}
},
"value": "文本内容",
"src": "https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/935920813a0c4151bbf452ef3c53ab7f~tplv-k3u1fbpfcp-watermark.image"
}]


码上掘金-CSS版


下面是纯css的版本:
code.juejin.cn/pen/7123482…


JS animejs库


animejs库


使用JS的关键就是编写对应帧属性,通过时间轴timeline方法给元素加上动画。现在js版本还只是一个demo中的demo,下次再给jym,感兴趣的jy可以自己想想。


时间轴可让你将多个动画同步在一起。
默认情况下,添加到时间轴的每个动画都会在上一个动画结束时开始。

<div class="demo-content params-inheritance-demo">
<div class="line">
<div class="square shadow"></div>
<div class="square el" style="transform: translateX(0px) scale(1); opacity: 0.5;"></div>
</div>
<div class="line">
<div class="circle shadow"></div>
<div class="circle el" style="transform: translateX(7.22878e-10px) scale(1); opacity: 0.5;"></div>
</div>
<div class="line">
<div class="triangle shadow"></div>
<div class="triangle el" style="transform: translateX(2.30924px) scale(1.00924) rotate(180deg); opacity: 0.5;"></div>
</div>
</div>

<script src="https://lib.baomitu.com/animejs/3.2.1/anime.min.js"></script>


.demo-content {
position: relative;
display: flex;
flex-direction: column;
justify-content: center;
text-align: center;
width: 290px;
height: 100%;
}
.line {
width: 100%;
padding: 1px 0px;
}
.square,
.circle {
pointer-events: none;
position: relative;
width: 28px;
height: 28px;
margin: 1px;
background-color: #005bb7;
font-size: 14px;
}
.triangle {
pointer-events: none;
position: relative;
width: 0;
height: 0;
border-style: solid;
border-width: 0 14px 24px 14px;
border-color: transparent transparent #005bb7 transparent;
}
.shadow {
position: absolute;
opacity: .2;
}

var tl = anime.timeline({
targets: '.params-inheritance-demo .el',
delay: function(el, i) { return i * 200 },
duration: 500,
easing: 'easeOutExpo',
direction: 'alternate',
loop: true
});

tl
.add({
translateX: 250,
// override the easing parameter
easing: 'spring',
})
.add({
opacity: .5,
scale: 2
})
.add({
// override the targets parameter
targets: '.params-inheritance-demo .el.triangle',
rotate: 180
})
.add({
translateX: 0,
scale: 1
});

code.juejin.cn/pen/7123478…


码上掘金太卡了吧,能不能优化下


作者:一起重学前端
来源:juejin.cn/post/7123482707983613965
收起阅读 »

本地运行的前端代码,如何让他人访问

web
有时候,我前端写好了项目,想要给其他人看一下效果,可以选择将代码部署到test环境,也可以选择让外部通过ip来访问,不过前提是在同一个局域网下(比如连接同一个WiFi),下面介绍第二种方式。 修改dev命令 首先我们需要先修改host地址,此处以vue3项目举...
继续阅读 »

有时候,我前端写好了项目,想要给其他人看一下效果,可以选择将代码部署到test环境,也可以选择让外部通过ip来访问,不过前提是在同一个局域网下(比如连接同一个WiFi),下面介绍第二种方式。


修改dev命令


首先我们需要先修改host地址,此处以vue3项目举例


image.png


页面启动之后如下


image.png


正常情况下,script下的dev命令是不会指定host的,我们可以在下面看到Local的地址为默认的127.0.0.1,此时把这个网址发给别人肯定跑不起来。


所以我们可以指定host,比如0.0.0.0,允许所有ip访问


"dev": "vite --host=0.0.0.0",

修改完host后,windows系统的话,我们还需要关闭防火墙(苹果不需要)。重新启动项目可以看到


QQ截图20230406204123(1)(1).png


Network那里的网址,打马赛克的地方其实就是本机的ip地址,window输入cmd打开命令提示符,然后输入ipconfig即可查到ip地址,苹果的话,点击wifi小图标,同时按住option键即可查到ip地址。


在其他电脑或者手机访问


浏览器中输入url即可看到相关页面,此方法也适用于手机端调试


Screenshot_2023-04-06-20-51-03-21_439a3fec0400f89.jpg


作者:笨笨狗吞噬者
来源:juejin.cn/post/7218916720323706935
收起阅读 »

知道尤雨溪为什么要放弃 $ 语法糖提案么?

web
前言 最近看到一篇文章: 《最新,Vue 中的响应性语法糖已废弃》 本文标题中的 $ 语法糖指的就是上文中的响应式语法糖 (Reactivity Transform),那为什么不写 Reactivity Transform 呢?因为这个名实在是太长了… 看了一...
继续阅读 »

前言


最近看到一篇文章:


《最新,Vue 中的响应性语法糖已废弃》


本文标题中的 $ 语法糖指的就是上文中的响应式语法糖 (Reactivity Transform),那为什么不写 Reactivity Transform 呢?因为这个名实在是太长了…


看了一圈评论发现大家觉得被废弃是因为分不清是正常变量还是响应式变量的居多:



下面这个评论说的有一定道理:



Vue 的官网现在已经变成这样了:



以后会不会变成这样:



23次方,一共8种不一样的写法。不对,无虚拟 DOM 模式只能用 Composition API,所以应该不到 8 种写法,你看这不就分裂了嘛!虽说这几种不同的写法也能看懂吧,但每个人都有不同的偏好不同的写法总归不太好。而且你能保证 Vue 不会又改写法吗?Vue 总是受人启发:受 Angular 启发的双向绑定、受 React 启发的虚拟 DOM、受 React Hooks 启发的 Composition API、受 Svelte 启发的语法糖(一开始用的是 Svelte 的 label 写法)、受 Solid 启发的 Vapor Mode无虚拟 DOM 模式




  • 高情商:集百家之长

  • 低情商:方案整合商




开玩笑的哈~ Vue 还是有很多自己的东西的,不过它确实老是抄袭各种框架受各种框架的启发,太杂糅了。今天受这个框架启发做出来这种新 feature、明天又受那个框架启发做出来了另一种新 feature… 估计等 Vue4 出来的时候肯定又是受到了什么其他框架的启发…


我在《无虚拟 DOM 版 Vue 即将到来》这篇文章下看到这样一条评论:



大家觉得这个人说的有没有道理呢?反正我现在感觉 Vue 的各个方案有点太杂糅了,有点像是方案整合商集百家之长,以后指不定就发展成这样了:



当你去网上搜索一些解决方案时,能看到数十种不同的写法是一种什么体验……


不过这条评论真的是高情商:





  • 低情商:Vue 这是啥流行抄啥

  • 高情商:只用 Vue 就能体会到各种流行的技术趋势




跑题了,咱们来说一说 $ 语法糖,它可绝不只有分不清到底是不是响应式变量这一个缺点,它的缺点比优点多得多,我们来具体分析一下。


分析


我们也不要一上来就说这个语法糖有多么多么的不好,如果真这么不好的话尤总也不至于费这么大劲来推动这个提案了对不?这个语法糖在某些情况下确实会大幅改善我们的开发体验,但在另一些情况下不仅不会帮助我们改善体验,反而会增加我们的心智负担,我们来看下面这个案例:


let x = $(0)
let y = $(0)

const update = e => {
 x = e.x
 y = e.y
}
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))

$watch([x, y], ([x, y]) => console.log(x, y))

看上去很美好是不是,我们终于不用再写 .value 了。



如果看不明白这种写法的话可能是之前没有对其进行过了解


建议先阅读一下这篇《Vue3又出新语法 到底何时才能折腾完?》



不过像这种逻辑我们通常都会提取出去封装成一个函数,因为有可能有很多个组件都用到了获取鼠标位置这个逻辑,你不想在每个用到该逻辑的组件里都复制一遍相同的逻辑吧?那我们就这样:


// useMouse.js
export const useMouse = (dom = window) => {
  let x = $(0)
  let y = $(0)

  const update = e => {
    x = e.x
    y = e.y
}
  onMounted(() => dom.addEventListener('mousemove', update))
  onUnmounted(() => dom.removeEventListener('mousemove', update))

  return { x, y }
}

import { useMouse } from './useMouse.js'

let { x, y } = useMouse()

$watch([x, y], ([x, y]) => console.log(x, y))

如果这么写你就会惊讶的发现根本不生效,因为编译过后就相当于:


import { ref } from 'vue'

export const useMouse = (dom = window) => {
 let x = ref(0)
 let y = ref(0)

 const update = e => {
   x.value = e.x
   y.value = e.y
}
 onMounted(() => dom.addEventListener('mousemove', update))
 onUnmounted(() => dom.removeEventListener('mousemove', update))

 return {
   x: x.value,
   y: y.value
}
}

这就相当于把一个普通值给 return 出去了,普通值是没法在取值或改值时运行一些其他逻辑的,所以我们还不能把值直接 return 出去,而是把这个响应式变量本身给 return 出去:


import { ref } from 'vue'

export const useMouse = (dom = window) => {
 let x = ref(0)
  let y = ref(0)

  const update = e => {
   x.value = e.x
    y.value = e.y
}
  onMounted(() => dom.addEventListener('mousemove', update))
  onUnmounted(() => dom.removeEventListener('mousemove', update))

  return { x, y }
}

所以编译必须还要有还原的功能,把响应式的值给还原成响应式变量:


export const useMouse = (dom = window) => {
 let x = $(0)
 let y = $(0)

 const update = e => {
   x = e.x
   y = e.y
}
 onMounted(() => dom.addEventListener('mousemove', update))
 onUnmounted(() => dom.removeEventListener('mousemove', update))

 return $$({ x, y })
}

但这样又要写 .value 了:


import { useMouse } from './useMouse.js'

let { x, y } = useMouse()

console.log(x.value, y.value)

因为编译器是分析不出来一个函数的返回值到底是不是响应式变量的,所以就又得引入一个 API 来告诉编译器这个函数的返回值有响应式变量:


import { useMouse } from './useMouse.js'

let { x, y } = $fromRefs(useMouse())

console.log(x, y)

大家不觉得这样很麻烦吗?而且搞出那么多莫名其妙的 $ 、$$ 变量。写一堆这玩意真的没感觉比 .value 好到哪去,而且我们还要随时记得某个变量是响应式的,不然在传递的过程中就有可能失去响应性:


// logValue.js
// 接收一个响应式变量并在其变化时将其打印出来

export const logValue = arg => { // 在提案中并未找到如何用语法糖转换函数的参数
 // 也就是说在这种情况下可能没有什么完美的解决方案 那就又要写 .value 了:
 console.log(arg.value)
 // 不过也不是没有解决方案 我们可以用 $computed 来关联一下:
 let argument = $computed(() => arg.value)
 // 这样就可以不用写 .value 了:
 console.log(argument)
 // 但缺点就是太麻烦了 参数少的时候还可以 参数多的时候还能每个都这么写吗?
 // 而且还要为变量取个不同的名字 这对于我们这些英文不好的人来说简直就是场灾难
 $watch(argument, value => console.log(value))
}

import { logValue } from './logValue.js'

let a = $(0)

logValue(a) // 这么传就错啦
logValue($$(a)) // 一定要写成这样

// 假如有函数是需要响应式变量和普通变量混着传的:
let b = 0
logValue($$(a), b, { a: $$(a), b }) // 写成这样真的很乱

还有需要把 ref 变量传给 reactive 字段的情况:


let a = $(0)

const obj = reactive({ a })

console.log(obj.a) // 0
a++
console.log(obj.a) // 还是 0


// 必须写成这样
const obj = reactive({ a: $$(a) })
console.log(obj.a) // 0
a++
console.log(obj.a) // 1

所以说语法糖只能某些情况下改善我们的开发体验,前提就是你不要把响应式变量传来传去的。但 Vue3 的核心卖点之一不就是 Composition API 么?中文官网管这个叫组合式 API,关键词是组合Vue 还把提取出去的可复用函数叫 Composables,翻译过来就是可组合的,如果不把响应式变量传来传去那还组合个P呀!


这个问题可不是只有 Vue 有,来看下 Solid.js 吧:


import { createSignal } from 'solid'

export const useMouse = (dom = window) => {
 const [x, setX] = createSignal(0)
 const [y, setY] = createSignal(0)

 dom.addEventListener('mousemove', ({ x, y }) => {
   setX(x)
   setY(y)
})

 return {
   x: x(),
   y: y()
}
}

同样会有响应式值与响应式变量的问题,只不过就是把 .value 变成了 ()


// 假如有个响应式变量 a

// 打印的是响应式值
console.log(a.value) // Vue
console.log(a()) // Solid

//打印的是响应式变量
console.log(a) // Vue & Solid

是不是看过很多文章说 Solid.js 和 React Hooks 很像、写起来很舒服、什么比 React 还 react 之类的文章?实际上真的就只是 API 设计的相似而已,只要我们想,我们同样也可以把 Vue 的 API 封装成 React 那样:


import { ref } from 'vue'

const useState = value => {
 const result = ref(value)
 const getter = () => result.value
 const setter = newValue => result.value = newValue
 return [getter, setter]
}

const [num, setNum] = useState(0)
setNum(1)

那是不是这样封装一下,Vue 也变得比 React 还 react 了?应该不难看出这只是在自欺欺人罢了,我们传值时照样还得区分到底应该传的是响应式变量本身还是响应式变量的值。


Vue2 为何没这个问题


不知大家有没有思考过:为什么 Vue2 时代大家从来就没听说过丢失响应性、没听过要出什么语法糖之类的问题呢?听过最多有关于语法糖的可能就是 v-model 的双向绑定功能其实就是 @input="xxx" + :value="xxx" 的语法糖。


这是因为 Vue2 时代用的都是 this.xxx,咱们所有的响应式变量全都挂载到了 this 上。取值时 this.xxx 会触发 getter、改值时 this.xxx = xxx 会触发 setter


你可以简单的理解成这样:


// 用 Vue3 来写一段伪代码
import { reactive, watchEffect } from 'vue'

const this = reactive({
a: 1,
b: 2,
c: 3
})

watchEffect(() => console.log(this.a))
this.a++

当然这只是一段伪代码,真这么写是会报错的:



因为 this 是一个关键字,正因为它是一个关键字所以咱们用 this.xxx 才会显得这么的自然。而我们现在的响应式变量都需要自己起名,自己起的名不是关键字,所以用 xx.xxx 就老觉得麻烦,就老想给它解构:


import { reactive, watchEffect, toRefs } from 'vue'

const user = reactive({
name: 'AngularBaby',
age: 34,
beautiful: true
})

console.log(user.name) // 有些人觉得这样写很麻烦
const { name } = user // 就老想给它解构
console.log(name) // 结果就是失去了响应性

// 想要保持响应性 写法就变得更麻烦了
const { name } = toRefs(user)
console.log(name.value)

而且之前用 this 还有一个显著的好处就是只要写法正确,操作 this 上的属性就不用担心响应式的问题,没有那么多心智负担。甚至有人会简单的理解为只要是 this.xxx 就一定会有响应:


export default {
data () {
return { a: 1 }
},
mounted () {
this.a = 2 // 没有心智负担 因为我们知道自己是在改变 this 上的属性
this.a++ // 正确改变 this 上的属性就会存在响应

let b = 2 // 也没有心智负担 因为我们知道这不是 this 上的属性
b++ // 我们不会期待这段代码会有任何的响应
}
}

这样很容易区分哪些是响应式变量而哪些不是,即使有人真的写成了这样:


export default {
data () {
return { a: 1 }
},
mounted () {
let { a } = this
a++ // 我们不会期待这段代码会有任何的响应
}
}

这里也很容易能够看出来我们这样并没有修改 this 上的属性,所以并不会正确响应也是理所应当的一件事。


还有复用逻辑,Vue2 时代有很多人用 Mixins 来复用逻辑:


import mouse from 'mouse.mixin.js'
import position from 'position.mixin.js'

export default {
mixins: [mouse, position],
mounted () {
this.x // 哪来的 x ?
this.y // 哪来的 y ?
// 除了 xy 还有没有其他的未知 this.xxx ?
}
}

可以看到 Mixins 存在很多的弊端,比方说数据来源不清晰、容易产生冲突变量之类的。如果不去看源码的话谁能知道 this.x 到底是 mouse 中的 x 还是 position 的 x 呢?正是由于 Vue2 没有一个完美的复用机制,所以尤大才下定决心将 Vue3 改造成函数式。但函数式没了 this 就又失去了 Vue2 时期的那种… 我不知该怎么形容 Vue2时期的 this.xxx 哈,舒服?自然?反正我是比较喜欢 this.xxx 这种写法的,虽然这种写法是受 Angular 启发(集百家之长)


而且我还比较喜欢的一点就是一些全局挂载的属性:


this.$el
this.$refs
this.$nextTick(() => { /* ... */ })

直接 this.$xxx 就出来了,不用引,既方便又快捷。当然这种方式也有不少坏处,比方说容易被覆盖、不利于 Tree Shaking 之类的…


但我还真的蛮喜欢这种写法的:


// main.js
import Vue from 'vue'

Vue.prototype.$toast = msg => { /* ... */ }

this.$toast('Success!')

如今就会变得就稍麻烦一些:


import toast from './toast.js'

toast('Success!')

虽说后者其实更好,但有没有这样一种可能:既恢复到 Vue2 时期用 this 的便捷、又能享受到 Vue3 组合式的好处:


// 幻想中的写法

this.$data.a = 1 // 相当于 Vue2 时期的 data: { a: 1 } 最终会挂载到 this 上变成 this.a
this.$computed.b = () => this.a * 2 // 相当于 Vue2 时期的 computed: { b () { return this.a * 2 } } 最终会挂载到 this 上变成 this.b

this.$watch.b = value => console.log(value) // 相当于 Vue2 时期的 watch: { b: value => console.log(value) }

let timer
this.$mounted = () => {
timer = setInterval(() => this.a++, 1000)
}
this.$unMounted = () => clearInterval(timer)

复用逻辑:


// 幻想中的写法

import useMouse from './useMouse.js'

({ x: this.$computed.x, y: this.$computed.y } = useMouse())
this.$effect = () => console.log(this.x, this.y)

// 如果用数组解构将会更加的便捷
[this.$computed.x, this.$computed.y] = useMouse()
this.$effect = () => console.log(this.x, this.y)

这样我们的心智负担就又能回到 this 时期了:只要改变 this 属性就会存在响应,否则就无响应,那这个方案有实现的可能吗?在 ES5 时代无可能,但在 ES6 Proxy 的加持下我认为还是可以实现的,那么接下来我们就来试一下。


实验


首先我们回顾一下 Vue3.0 没有 setup 语法糖时期的写法:


<template></template>

<script>
import { defineComponent } from 'vue'

export default defineComponent({
setup () {
console.log(this) // undefined
}
})
</script>

原版的 this 指向为 undefined,那我们怎么改变它的指向呢?我们可以自己写一个 defineComponent


// defineComponent.js

import { defineComponent, reactive } from 'vue'

export default options => {
const { setup } = options
if (typeof setup === 'function') {
options.setup = setup.bind(reactive({}))
}
return defineComponent(options)
}

这样 setup 的指向就变成了 reactive({}),当我们在操作 this 的时候就相当于在操作 reactive({})。但这样并不能满足我们的需求,我们想要的是当我们 this.$data.a 的时候会在 this 上挂载个 a 属性,所以我们要把 reactive 换成一个 Proxy


// createThis.js
import { defineComponent, reactive } from 'vue'

const createData = target => new Proxy({}, {
get: (_, key) => Reflect.get(target, key),
set (_, key, value) {
if (Reflect.getOwnPropertyDescriptor(target, key)) {
console.error(`this.$data.${key} is already defined!`)
return false
}
return Reflect.set(target, key, value)
}
})

export default () => {
const that = reactive({})
const $data = createData(that)
return new Proxy(that, {
get (target, key) {
if (key === '$data') {
return $data
}
return Reflect.get(target, key)
},
set (target, key, value) {
if (key === '$data') {
return console.warn('this.$data is readonly!')
}
return Reflect.set(target, key, value)
}
})
}

// defineComponent.js

import { defineComponent } from 'vue'
import createThis from './createThis.js'

export default options => {
const { setup } = options
if (typeof setup === 'function') {
const that = createThis()
options.setup = (...args) => {
setup.apply(that, args)
return that
}
}
return defineComponent(options)
}

也就是说我们利用 Proxy 来把 $data 给代理出去了,当我们访问 $data 的时候其实已经是另一个代理对象了,在这个代理对象上设置的属性全部都设置到 this 上。this 现在就相当于 reactive({}),所以 this.$data.a = 1就相当于 reactive({ a: 1 }),我们来试一下:



完美运行,只要你能搞懂上面的那段代码,那么接下来的 $computed$watch$watchEffect$readonly$shallow$nextTick$mounted$unMounted 等一大堆 API 相信你也知道该怎么做了,我就不在这里占用过多的篇幅了。这里直接用码上掘金贴上源码及用法,向大家展示一下可行性:



当然这源码并不是把所有 API 都实现了,目前只实现了 this.$datathis.$computedthis.$watchthis.$mounted 等几个常用的 API 供大家参考,感兴趣的可以去把全部的 API 都实现一下,我这里犯懒就先不实现那么全乎了。



这么好的东西为啥犯懒不实现呢?因为这玩意有一定的弊端。对了,掘金好像在文章中屏蔽了来自码上掘金alert,必须点查看详情才能看到。为了防止大家也犯懒不点进去看,这里直接给大家贴上动图:



我们的写法类似于下面这样:


export default defineComponent({
setup () {
this.$data.count = 0
this.$watch.count = (value, oldValue) => alert(`验证 this.$watch:按钮上的值将会从 ${oldValue} 变为 ${value}`)

this.$computed.doubleCount = () => this.count * 2
this.$watch.doubleCount = value => alert(`验证 this.$computed:${this.count} 的双倍是 ${value}`

this.$mounted = () => alert('验证 this.$mounted:已挂载')
}
})

怎么样,是不是很好玩?我是蛮喜欢这种 this 混合着函数式的写法。但刚刚说了这玩意有一定的弊端,只能拿来当玩具玩玩所以我才懒得实现的那么全乎。那么它究竟有多大的弊端呢?


弊端


Vue3 比 Vue2 更优秀的一个点是支持 tree shaking,在你仅仅只用了 Vue 的某几项功能的情况下打包体积会小很多。但我们刚刚的做法无疑是开了历史的倒车,又回去了!并且随着 Vue3.2 的崛起,setup 语法糖得到了大多数人的认可,因为它确实很方便。但这样我们就无法修改 this 指向了:


<template>
<h1>{{ a }}</h1>
</template>

<script setup>
this.$data.a = 1 // 怎么修改 this 指向
</script>

有人可能会说加个函数不就得了:


<template>
<h1>{{ a }}</h1>
</template>

<script setup>
import setup from './setup.js'

setup(() => {
this.$data.a = 1
})
</script>

这样虽然可以修改 this 指向,但随之而来的就是 <template> 模板里面访问不到 a 这个变量了,除非我们写成这样:


<template>
<h1>{{ a }}</h1>
</template>

<script setup>
import { toRefs } from 'vue'
import setup from './setup.js'

const { a, b, c, d, e, f } = toRefs(setup(() => {
this.$data.a = 1
this.$data.b = 2
this.$data.c = 3
this.$data.d = 4
this.$data.e = 5
this.$data.f = 6
}))
</script>

我相信没人会愿意写成这样,所以我们必须借助 babel 插件来完成编译,思路是把 this 编译成 reactive({}),类似于下面这样:


// 编译前
this.$data.a = 1
this.$data.b = 2
this.$data.c = 3
this.$data.d = 4
this.$data.e = 5
this.$data.f = 6

// 编译后
import { reactive } from 'vue'
import createThis from 'createThis.js'
import createData from 'createData.js'

const that = createThis(reactive({}))
createData(that)

that.$data.a = 1
that.$data.b = 2
that.$data.c = 3
that.$data.d = 4
that.$data.e = 5
that.$data.f = 6

不过这样还是会引入我们刚刚写的那些代码,虽然代码量并不高,但如果压根就不引入任何额外的代码才好,所以如果能编译成这样才是最完美的:


// 编译前
this.$data.a = 1
this.$data.b = 2
this.$data.c = 3
this.$data.d = 4
this.$data.e = 5
this.$data.f = 6

console.log(this.a)

// 编译后
import { reactive } from 'vue'

const that = reactive({
a: 1,
b: 2,
c: 3,
d: 4,
e: 5,
f: 6
})

console.log(that.a)

但如果这样编译的话又有可能发生如下情况:


import useXxx from './useXxx'

this.$data.a = 1

useXxx.call(this)

这样会被编译成:


import { reactive } from 'vue'
import useXxx from './useXxx'

const that = reactive({ a: 1 })

useXxx.call(that)

万一这个 useXxx 里写了这样一段逻辑:


// useXxx.js

expurt default function () {
this.$watch.a = value => console.log(value)
}

这样就不会按照我们所期待方式去运行了,因为在编译后就相当于:


// 伪代码

const obj = reactive({ a: 1 })

useXxx.call(obj)

function useXxx () {
this.$watch.a = value => console.log(value)
}

这样会直接报错,因为 reactive({ a: 1 }).$watch 是 undefinedundefined.a 会报错,所以并没有特别完美的解决方案。最好是检测如果没把 this 作为参数传走或者没有哪个函数用了 fn.call(this) 来把 this 指向当前上下文的话,就按照最完美的方式(不引入任何杂七杂八的代码)编译。否则就引入一点运行时,反正也没多少:


// 编译前
import useMouse from 'useMouse'

this.$data.a = 1
this.$watch.a = value => console.log(value)

this.$mounted = () => window.addEventListener(...)
this.$unmounted = () => window.removeEventListener(...)

[this.$computed.x, this.$computed.y] = useMouse.call(this)

// 编译后
import { reactive } from 'vue'
import createThis from 'createThis'
import createData from 'createData'
import createWatch from 'createWatch'
import createMounted from 'createMounted'
import createUnmounted from 'createUnmounted'

const that = createThis(reactive({
a: 1
}))
createData(that)
createWatch(that)
createMounted(that)
createUnmounted(that)

that.$data.a = 1
that.$watch.a = value => console.log(value)

that.$mounted = () => window.addEventListener(...)
that.$unmounted = () => window.removeEventListener(...)

[that.$computed.x, that.$computed.y] = useMouse.call(that)

但仔细一想还是有可能有 bug,比方说你这个组件里没用到 this.$readonly,但 useMouse 用了的话,那岂不是又要报错。那就在 Vue 组件之外也编译,如果在外面有用到 this.$xxx,那就在相应的位置:


// 编译前
export default function useMouse () {
this.$readonly.a = 1
}

// 编译后
import createReadonly from 'createReadonly'

export default function useMouse () {
createReadonly(this)
this.$readonly.a = 1
}

缺陷


这种写法不仅仅是有弊端,还有一个非常严重的缺陷。虽然刚刚我们设想了一下用编译的方案来解决弊端的可能,但有个最大的缺陷是连编译都无法解决的。这个最大的缺陷就是对 TS 的支持,如果不用 TS 还好,但如果你的项目里有用 TS,那么这种写法就完全没法用:



不知怎么才能让 TS 也支持这种想法,查了国内外很多资料,最后找到了这两篇文章:



《TypeScript plugin 实践 —— 类型,编辑器与业务价值》


《基于 TypeScript 的开发者体验增强 - 朝夕相处却始终被忽视的领域》



也不知道这个 TS Language Service 有没有可能能够实现我们这种语法,感兴趣的小伙伴可以好好研究一下。我们目前只实现了运行时方案,但编译方案才是未来。写这篇文章的目的是希望给大家提供一个思路,看看大家觉得这个想法怎么样。万一大家觉得这个想法非常好,把它推给官方,官方实现了呢?



当然上述的那些话也可能仅仅只是过于美好的想象,现实很有可能是压根儿就没有人对这个想法感兴趣,官方也认为这是在开历史的倒车并且对 TS 支持不好不予实现。



往期精彩文章



作者:Veev
来源:juejin.cn/post/7222874734185922597
收起阅读 »

Low-Code,一定“low”吗?

web
作者:京东保险 吴凯 前言 低代码是一组数字技术工具平台,基于图形化拖拽、参数化配置等更为高效的方式,实现快速构建、数据编排、连接生态、中台服务。通过少量代码或不用代码实现数字化转型中的场景应用创新。本文将重点介绍低代码相关知识,包括低代码的定义与意义、相关概...
继续阅读 »

作者:京东保险 吴凯


前言


低代码是一组数字技术工具平台,基于图形化拖拽、参数化配置等更为高效的方式,实现快速构建、数据编排、连接生态、中台服务。通过少量代码或不用代码实现数字化转型中的场景应用创新。本文将重点介绍低代码相关知识,包括低代码的定义与意义、相关概念、行业发展等,同时介绍京东的低代码工具,期望能帮助大家更好地认识与理解低代码。


一、低代码介绍


2014年,Forrester(著名研究咨询机构)提出“低代码”的术语,定义为“利用很少或几乎不需要写代码就可以快速开发应用,并可以快速配置和部署的一种技术和工具”。或者说是“(能力)多(出品)快(质量)好(功夫)省”。



这个定义体现出低代码的核心价值:


1、低代码开发平台能够实现业务应用的快速交付。低代码开发的重点是开发应用快,不像传统意义上仅仅是一个应用的开发,而是通过可视化的开发,达到“设计及交付”的目的,提高开发效率。


2、低代码开发平台能够降低业务应用的开发成本。低代码开发投入更低,主要体现在开发时间短,可以快速配置和部署,同时也更容易使非开发人员上手。


二、我们为什么用低代码


低代码可以降本增效,一方面低代码的出现避免了“反复造轮子”的问题,其通过可视化的编程方式实现“千人千面”的效果,驱使技术回归本源--支持业务。另一方面低代码的生命周期贯穿整个软件开发周期(设计、开发、测试、交付),周期上的各角色都可以在同一个低代码开发平台上紧密协作,由传统的开发方式变为敏捷开发,实现了快速交付的目的。


低代码的使用场景:


1、构建新的SaaS应用,而借助低代码平台可以快速有效地构建、测试和推出应用。低代码与SaaS的结合,可以为企业提供独特的业务解决方案。


2、基于Web的门户网站是提供自助服务的数字化工具。使用低代码开发平台,更简单、更快速地构建个性化应用,打造数字化平台。


3、历史系统的迁移或升级。基于低代码技术:一方面,最大限度地保留遗留系统的代码,保留其“公共数据服务”;另一方面,基于遗留系统的开发环境和能力构建相应的“功能适配器”,然后在此基础上,通过低代码技术快速定制新业务和流程的交互式UI与业务逻辑。


4、应用复杂性低,业务流程相对简单,95%的应用场景可以通过低代码完成。



三、低代码会使程序员失业吗


回答这个问题,我们首先需要搞明白:低代码和零代码的区别。作为程序员,大家都会把低代码认为是零代码,这也是会被误解程序员失业的原因之一。


低代码,意味着反复迭代的代码质量高,在必要的时候,也会进行代码的编写;BUG更少,减少了测试环节的工作量。


零代码,字面意思:完全不需要任何代码即可完成应用开发,从软件开发效率看,**零代码是低代码的最终形态。**零代码平台由于采用全部都是封装模块进行搭建,所有控件都已经被固化了,所以用零代码平台搭建的系统想要进行扩展是有些困难的。


现实是,编码的最终目的是支持业务,业务逻辑的复杂与否依旧需要人来掌握,低代码只是写的少,并不是不写代码,这并不会导致程序员的失业



四、低代码的行业现状


2021年11月11日,Forrester发布《The State Of Low-Code Platforms In China》,这是低代码概念提出者第一次将视角聚焦在中国。Forrester认为,低代码目前在国内主要应用于银行、保险、零售、医疗、政府、制造、电信和建筑行业。比如,为了针对各个业务单元量身定制各种业务需求,中国建设银行采用云枢为其分布式开发团队构建统一的低代码开发平台(LCDP)。另外,报告指出:中国企业数字化转型过程中,有58%的决策者正在采用低代码工具进行软件构建,另有16%的决策者计划采用低代码。


目前,国内的低代码开发平台不断涌现,Forrester划分了9类低代码平台厂商:


▪数字流程自动化(BPM):炎黄盈动(AWS PaaS)、奥哲(云枢)


▪公有云:阿里巴巴(宜搭)、百度(爱速搭)、华为(应用魔方)、微软(Power Platform)、腾讯(微搭)


▪面向专业开发者的低代码开发平台:ClickPaaS、葡萄城(活字格)、Mendix、Outsystems


▪面向业务开发者的低代码开发平台:捷德(Joget DX)、轻流


▪AI/机器学习:第四范式(HyperCycle)


▪BI:帆软(简道云)


▪协作管理:泛微(E-Builder)


▪流程自动化机器人(RPA):云扩(ViCode)、来也(流程创造者)


▪数字化运营平台:博科(Yigo)、金蝶(金蝶云·苍穹)、浪潮(iGIX)、用友(YonBIP)


由此可知,中国的低代码市场正在飞速发展,各种低代码工具的发布问世,也意味着低代码未来将成为主流的开发方式。


五、业内的低代码平台


1、Out-System


OutSytems 作为国外著名的低代码开发平台,出发点就是简化整个应用开发和交付的过程,让开发人员可以快速响应市场的需求变化。通过可视化和模型驱动的开发方式,大幅减少时间和成本。并通过预构建的连接器加速集成后端系统,同时还提供了一个集中式的控制台来管理应用的版本、发布以及部署。


OutSytems 生成的应用可以不依赖于 OutSytems 运行。数据是直接存储到数据库,这样就可以通过任何标准的 ETL、 BI或其他第三方数据工具来访问数据。


官网:

http://www.outsystems.com/demos/


2、阿里-云凤蝶


云凤蝶是蚂蚁金服体验技术部的重点研发项目,是面向中后台产品的快速研发平台,主要用户面向工程师,使用场景专注在标准化的中后台产品研发,目标是为了提高效率。


云凤蝶的核心思路是将组件生产和组件组装这两部分工作进行职责分离,通过建立一条组件组装流水线,打通 npm 组件的一键导入流程,从而完成一条产业链式的分工协作,最终实现规模化的快速生产。


淘系的“乐高”系统以及蚂蚁金服的“金蝉”系统、“云凤蝶”系统成微阿里系主要的低代码开发工具。


3、京东-星链


星链是京东科技消金基础研发部开发的一款研发效能提升工具,主要为面向后端服务研发需求,因此前端简洁可视化开发界面需要满足极致的细节,并依赖其自身后端的能力来实现用户的低代码。


核心概念:


VMS可视化微服务应用,是星链的基本单元,同时VMS也是一种模型,各种配置均在模型中。支持京东中间件(JSF、定时任务、JMQ,缓存服务、分布式配置等),服务流程编排,DEBUG调试等;


Serverless部署,星链的部署及配置均由系统自动分配。用户只需关注系统的开发,资源的使用情况。


地址:jddlink.jd.com/


结论


低代码,一定不“low”,却更low-code。


参考:


2021年低代码平台中国市场现状分析报告

http://www.authine.com/report/56.h…



作者:京东云开发者
来源:juejin.cn/post/7217449801633808439
收起阅读 »

使用fabric从零开始打造互动白板(一)

web
最近由于公司业务需要,原本使用第三方的互动白板功能,准备自行实现。一方面为了节省开支,另一方面自行实现定制化也更强一些,能够和现有业务更好的结合,用以满足第三方互动白板无法实现的一些功能。 一、功能整理 既然需求明确了,于是就开始着手整理白板所需的功能。由于...
继续阅读 »

最近由于公司业务需要,原本使用第三方的互动白板功能,准备自行实现。一方面为了节省开支,另一方面自行实现定制化也更强一些,能够和现有业务更好的结合,用以满足第三方互动白板无法实现的一些功能。



一、功能整理


既然需求明确了,于是就开始着手整理白板所需的功能。由于我们的直播课都是大班课,只需要讲师在白板上操作,用户端只用来展示,也就不需要太多复杂的功能,结合几个第三方互动白板,归纳整理出了如下几个需要实现的功能点:



  • 自由画笔

  • 文字书写

  • 橡皮擦

  • 画三角、圆形、矩形

  • 画直线和箭头

  • 清空画布

  • 撤销重做

  • 画布缩放

  • 插入PPT图片及切换控制


二、技术选择


观察了现有的互动白板,都是在Canvas进行操作,为了节约开发时间于是找到了fabric这个库,这个库本身就实现了不少功能,可以大大的减少开发时间。


结合我熟悉的技术栈,最终选定了使用Vite+Vue3+TypeScript进行demo版本的构建。


相关代码放在github上,链接地址:使用vite+typescript+fabric创建的互动白板项目


三、页面结构


参考其他白板的布局进行了页面结构的搭建。白板使用一个容器进行包裹,左侧是工具区域,包括画笔、橡皮擦、画线、清空画布等各种绘制工具;左下角是撤销、重做和画布缩放控制区域;右上角是插入PPT文件控制区域;右下角是PPT控制区域。最后提供了一个容器进行的白板预览。


效果图如下:


demo.png


页面结构代码如下:


<template>
<div>
<div class="canvas-wrap">
<div class="tool-box-out">
<ToolBox></ToolBox>
</div>
<div class="redo-undo-box">
<RedoUndo></RedoUndo>
</div>
<div class="zoom-controller-box">
<ZoomController></ZoomController>
</div>
<div class="room-controller-box" v-show="!isPreviewShow">
<div class="page-controller-mid-box">
<div className="page-preview-cell" @click="insertPPT">
<img style="width: 28px" :src="folder" alt="文件"/>
</div>
</div>
</div>
<div class="page-controller-box" v-show="isShowPPTControl">
<div className="page-controller-mid-box">
<PageController></PageController>
<div className="page-preview-cell" @click="handlePreviewState(true)">
<img :src="pages" alt="PPT预览"/>
</div>
</div>
</div>
<div class="preview-controller-box" v-show="isShowPPTControl&&isPreviewShow">
<PreviewController @handlePreviewState="handlePreviewState"></PreviewController>
</div>
<canvas id="canvas" width="800" height="450"></canvas>
</div>
<div class="canvas-wrap">
<canvas id="canvas2" width="800" height="450"></canvas>
</div>
</div>
</template>

四、初始化白板


为了方便后续使用,这里对fabric进行封装,后续拓展也能更加灵活。相关代码如下:


import { fabric } from "fabric";

class FabricCanvas {
constructor(canvasId: string) {

// 初始化画布,默认可绘制
this.canvas = new fabric.Canvas(canvasId, {
isDrawingMode: true,
selection: false,
includeDefaultValues: false, // 转换成json对象,不包含默认值
});
}
}

使用示例:


const canvas = new FabricCanvas('canvas');

五、工具栏相关功能实现


页面框架搭建完成之后,就开始各种功能的开发。这里将fabric封装成一个类,所有需要实现的方法在类中实现,方便后续灵活调用。


选择


选择功能实现比较简单,只需要关闭绘制模式,然后将画布设置可选中。相关代码如下:


this.canvas.isDrawingMode = false;
this.canvas.selection = true;
// 设置鼠标光标
this.canvas.defaultCursor = 'auto';

自由画笔


fabric提供了各种丰富的笔刷,实现自由绘制这里调用了PencilBrush类,并将isDrawingMode设置为true即可。相关代码如下:


  public drawFreeDraw() {
this.canvas.freeDrawingBrush = new fabric.PencilBrush(this.canvas);
this.canvas.freeDrawingBrush.color = '#ff0000'
this.canvas.freeDrawingBrush.width = 5
this.canvas.freeDrawingCursor = 'default'
this.canvas.isDrawingMode = true;
}

使用示例:


const canvas = new FabricCanvas('canvas');
// 自由画笔
canvas.drawFreeDraw();

文字书写


文字输入使用fabric提供的IText方法,实现文字编辑和修改,并且让输入框自动获取焦点,方便输入。相关代码如下:


  public drawText(text: string, options?: ITextOptions): void {
const textObj = new fabric.IText(text, {
editingBorderColor: '#ff0000',
padding: 5,
...options
});
this.canvas.add(textObj);
this.canvas.defaultCursor = 'text'
this.currentShape = textObj;
// 文本打开编辑模式
textObj.enterEditing();
// 文本编辑框获取焦点
textObj.hiddenTextarea.focus()
this.setActiveObject(textObj);
}

使用示例:


const canvas = new FabricCanvas('canvas');
// 绘制文本
canvas.drawText('Hello World!', { left: 50, top: 250, fontSize: 24, fill: 'red' })

橡皮擦


fabric内置了EraserBrush用来实现橡皮擦功能,不过默认构建排除了该功能,避免库文件过大。如需使用,需要进入node_modules/fabric目录执行下面的命令重新构建:


node build.js modules=ALL exclude=gestures,accessors requirejs minifier=uglifyjs

构建完成之后就可以使用EraserBrush来实现橡皮擦功能了,相关代码如下:


public eraser(options?: any): void {
this.canvas.freeDrawingBrush = new fabric.EraserBrush(this.canvas, options);
this.canvas.freeDrawingBrush.width = 10
this.canvas.freeDrawingCursor = 'default'
this.canvas.isDrawingMode = true;
}

使用示例:


const canvas = new FabricCanvas('canvas');
// 橡皮擦
canvas.erase({ width: 10 });

画三角、圆形、矩形


画三角形、圆形、矩形方法相似,直接调用fabric封装的对应方法即可。


这里以绘制矩形为例,相关代码实现如下:


public drawRect(options: IRectOptions): void {
const rect = new fabric.Rect({ ...this.options, ...options });
this.canvas.add(rect);
this.currentShape = rect;
this.canvas.defaultCursor = 'crosshair'
}

使用示例:


const canvas = new FabricCanvas('canvas');
// 绘制矩形
canvas.drawRect({ left: 50, top: 150, width: 100, height: 50, fill: 'green', stroke: 'black' });

画直线和箭头


画直线功能fabric直接内置了,调用对应的方法即可,这里重点讲画箭头。画箭头的本质是在直线的一端加上一个三角形,根据起始点使用三角函数计算好三角形的方向,这样就组合成一个箭头了。这里搬运网友的画箭头方法,直接对画直线功能进行拓展,并封装成fabric中的功能模块,方便后续调用。相关代码如下:


import { fabric } from 'fabric';

fabric.Arrow = fabric.util.createClass(fabric.Line, {
type: 'arrow',
superType: 'drawing',
initialize(points: number[], options: any) {
if (!points) {
const { x1, x2, y1, y2 } = options;
points = [x1, y1, x2, y2];
}
options = options || {};
this.callSuper('initialize', points, options);
},
_render(ctx: any) {
this.callSuper('_render', ctx);
ctx.save();
const xDiff = this.x2 - this.x1;
const yDiff = this.y2 - this.y1;
const angle = Math.atan2(yDiff, xDiff);
ctx.translate((this.x2 - this.x1) / 2, (this.y2 - this.y1) / 2);
ctx.rotate(angle);
ctx.beginPath();
// Move 5px in front of line to start the arrow so it does not have the square line end showing in front (0,0)
ctx.moveTo(5, 0);
ctx.lineTo(-5, 5);
ctx.lineTo(-5, -5);
ctx.closePath();
ctx.fillStyle = this.stroke;
ctx.fill();
ctx.restore();
},
});

fabric.Arrow.fromObject = (options: any, callback: any) => {
const { x1, x2, y1, y2 } = options;
return callback(new fabric.Arrow([x1, y1, x2, y2], options));
};

export default fabric.Arrow;

封装好的代码,直接导入调用即可。相关代码如下:


import Arrow from "./objects/Arrow";
// 绘制箭头
public drawArrow(x1: number, y1: number, x2: number, y2: number, options?: ILineOptions) {
const arrow = new Arrow([x1, y1, x2, y2], { ...this.options, ...options });
this.canvas.add(arrow);
this.currentShape = arrow;
this.canvas.defaultCursor = 'crosshair'
}

使用示例:


const canvas = new FabricCanvas('canvas');
// 绘制矩形
canvas.drawArrow(10, 50, 100, 50, { stroke: 'blue', strokeWidth: 2 })

通过鼠标绘制图形


实际使用白板过程中,上面这些图形、线条的绘制都是通过鼠标拖动进行,这样更加灵活一些。


通过鼠标绘制图形,需要对鼠标的mouse:downmouse:movemouse:up事件进行监听,相关代码如下:


// 监听鼠标事件
this.canvas.on("mouse:down", this.onMouseDown.bind(this));
this.canvas.on("mouse:move", this.onMouseMove.bind(this));
this.canvas.on("mouse:up", this.onMouseUp.bind(this));

这里以绘制矩形为例,实现通过通过鼠标绘制一个矩形框。



  1. 当鼠标按下时,在鼠标按下的地方绘制一个宽高为0的矩形。相关代码如下:


// 是否处于绘制状态
private isDrawing = false;
// 鼠标起点坐标x
private startX = 0;
// 鼠标起点多表y
private startY = 0;

// 鼠标按下事件处理函数
private onMouseDown(event: IEvent) {
// 如果当前有活动的元素则不进行后续绘制
const activeObject = this.canvas.getActiveObject();
if (!event.pointer || activeObject) return;

// 切换成绘制状态
this.isDrawing = true;
// 记录当前坐标点
const { x, y } = event.pointer;
this.startX = x;
this.startY = y;

// 在当前坐标绘制一个矩形
this.drawRect({
left: x,
top: y,
width: 0,
height: 0,
});
}


  1. 在鼠标移动的过程中,动态的修改矩形的宽高,并实时渲染。相关代码如下:


// 鼠标移动事件处理函数
private onMouseMove(event: IEvent) {
if (!this.isDrawing || !event.pointer || !this.currentShape) return;

// 计算宽高
const { x, y } = event.pointer;
const width = x - this.startX;
const height = y - this.startY;

// 设置宽高
this.currentShape.set({
width,
height,
});

// 更新画布
this.canvas.renderAll();
}


  1. 当鼠标抬起后,改变绘制状态。相关代码如下:


// 鼠标抬起事件处理函数
private onMouseUp() {
this.isDrawing = false;
this.currentShape = null;
}

如果想要更加灵活的在各种图形和线条中自由的进行切换,并通过鼠标绘制,在提供的demo中也进行了对应的封装。
相关代码请在github中进行查看,对fabric的各种功能封装


清空画布


清空画布直接调用画布的清除方法即可,相关代码如下:


// 清空画布
public clearCanvas() {
this.canvas.clear();
}

不过该方法会清除画布上包含背景的所有内容,如果不想画布背景也被清除,可以遍历画布上的所有对象进行移除。相关代码如下:


// 移除所有对象
public removeAllObject() {
this.canvas.getObjects().forEach((obj) => {
this.canvas.remove(obj);
});
}

六、工具栏布局
将工具栏封装成ToolBox组件,并在组件中实现各种工具的切换。


工具栏显示效果
组件布局代码如下:


<template>
<div class="tool-mid-box-left">
<div class="tool-box-cell-box-left" v-for="item in tools" :key="item.shapeType">
<div class="tool-box-cell"
@click="clickAppliance(item.shapeType)">
<img :src="item.shapeType === currentShapType ? item.iconActive : item.icon" :alt="item.name"/>
</div>
</div>
<div class="tool-box-cell-box-left">
<div class="tool-box-cell"
@click="clickClear">
<img :src="clear" alt="清屏"/>
</div>
</div>
</div>
</template>

相关功能事件实现的代码如下:


const currentShapType = ref<string>("pencil")

// 设置当前工具
function clickAppliance(type: DrawingTool) {
currentShapType.value = type;
canvas?.value.setDrawingTool(type)
}

// 清屏事件处理
function clickClear() {
canvas?.value.clearCanvas()
}

设置当前绘制工具


// 设置绘图工具
public setDrawingTool(tool: DrawingTool) {
if(this.drawingTool === tool) return;
this.canvas.isDrawingMode = false;
this.canvas.selection = false;

this.drawingTool = tool;
if (tool === "pencil") {
this.drawFreeDraw();
} else if (tool === "eraser") {
this.eraser();
} else if (tool === "select") {
this.canvas.selection = true;
this.canvas.defaultCursor = 'auto'
}
}

其他功能说明


为避免文章太长,撤销重做、画布缩放、插入PPT图片及切换控制等功能的实现在后续文章中介绍。


如果等不及,可以直接在github上查看相关代码实现:使用vite+typescript+fabric创建的互动白板项目


六、参考资料



作者:江阳小道
来源:juejin.cn/post/7221348552513077305
收起阅读 »

new 一个对象时,js 做了什么?

web
前言 在 JavaScript 中, 通过 new 操作符可以创建一个实例对象,而这个实例对象继承了原对象的属性和方法。因此,new 存在的意义在于它实现了 JavaScript 中的继承,而不仅仅是实例化了一个对象。 new 的作用 我们先通过例子来了解 n...
继续阅读 »

前言


在 JavaScript 中, 通过 new 操作符可以创建一个实例对象,而这个实例对象继承了原对象的属性和方法。因此,new 存在的意义在于它实现了 JavaScript 中的继承,而不仅仅是实例化了一个对象。


new 的作用


我们先通过例子来了解 new 的作用,示例如下:


function Person(name) {
this.name = name
}
Person.prototype.sayName = function () {
console.log(this.name)
}
const t = new Person('小明')
console.log(t.name) // 小明
t.sayName() // 小明

从上面的例子中我们可以得出以下结论:





  • new 通过构造函数 Person 创建出来的实例对象可以访问到构造函数中的属性。




  • new 通过构造函数 Person 创建出来的实例可以访问到构造函数原型链中的属性,也就是说通过 new 操作符,实例与构造函数通过原型链连接了起来。





构造函数 Person 并没有显式 return 任何值(默认返回 undefined),如果我们让它返回值会发生什么事情呢?


function Person(name) {
this.name = name
return 1
}
const t = new Person('小明')
console.log(t.name) // 小明

在上述例子中的构造函数中返回了 1,但是这个返回值并没有任何的用处,得到的结果还是和之前的例子完全一样。我们又可以得出一个结论:



构造函数如果返回原始值,那么这个返回值毫无意义。



我们再来试试返回对象会发生什么:


function Person(name) {
this.name = name
return {age: 23}
}
const t = new Person('小明')
console.log(t) // { age: 23 }
console.log(t.name) // undefined

通过上面这个例子我们可以发现,当返回值为对象时,这个返回值就会被正常的返回出去。我们再次得出了一个结论:



构造函数如果返回值为对象,那么这个返回值会被正常使用。



总结:这两个例子告诉我们,构造函数尽量不要返回值。因为返回原始值不会生效,返回对象会导致 new 操作符没有作用。


实现 new


首先我们要清楚,在使用 new 操作符时,js 做了哪些事情:



  1. js 在内部创建了一个对象

  2. 这个对象可以访问到构造函数原型上的属性,所以需要将对象与构造函数连接起来

  3. 构造函数内部的this被赋值为这个新对象(即this指向新对象)

  4. 返回原始值需要忽略,返回对象需要正常处理


知道了步骤后,我们就可以着手来实现 new 的功能了:


function _new(fn, ...args) {
const newObj = Object.create(fn.prototype);
const value = fn.apply(newObj, args);
return value instanceof Object ? value : newObj;
}

测试示例如下:


function Person(name) {
this.name = name;
}
Person.prototype.sayName = function () {
console.log(this.name);
};

const t = _new(Person, "小明");
console.log(t.name); // 小明
t.sayName(); // 小明

以上就是关于 JavaScript 中 new 操作符的作用,以及如何来实现一

作者:codinglin
来源:juejin.cn/post/7222274630395379771
个 new 操作符。

收起阅读 »

CSS链接悬停效果的的小创意

web
前言 每次写a标签的时候我都烦躁,为什么默认是蓝色的,就跟奇怪,关键他那个颜色也用不上,今天想让平凡的a标签也能做出令人眼前一亮的效果,大家以后直接cv多好 悬停滑动高亮链接效果 鼠标悬停后,链接有一个过渡动画,填充背景色,我们从链接周围的填充开始,然后添加相...
继续阅读 »

前言


每次写a标签的时候我都烦躁,为什么默认是蓝色的,就跟奇怪,关键他那个颜色也用不上,今天想让平凡的a标签也能做出令人眼前一亮的效果,大家以后直接cv多好


悬停滑动高亮链接效果


鼠标悬停后,链接有一个过渡动画,填充背景色,我们从链接周围的填充开始,然后添加相同值的负边距以防止填充破坏文本流。我们将使用box-shadow而不是 background 属性,因为它允许我们转换。


a { 
box-shadow: inset 0 0 0 0 #54b3d6;
color: #54b3d6;
margin: 0 -.25rem;
padding: 0 .25rem;
transition: color .3s ease-in-out, box-shadow .3s ease-in-out;
}
a:hover {
box-shadow: inset 100px 0 0 0 #54b3d6;
color: white;
}


悬停链接文本交换效果


我们在悬停时将链接的文本与其他一些文本交换。将鼠标悬停在文本上,链接的文本会随着新文本的滑入而滑出。


 <p><a href="#" data-replace="给个三连,好不好嘛"><span>鼠标放到这里试一试</span></a></p>

让我们给链接一些基本样式。我们需要给它相对定位来固定伪元素,确保它的显示是inline-block为了获得盒子元素样式的可供性,并隐藏伪元素可能导致的任何溢出。


  a {
overflow: hidden;
position: relative;
display: inline-block;
}

::before,::after设置为链接的全宽,左侧位置为零,并且绝对定位。


a::before,
a::after {
content: '';
position: absolute;
width: 100%;
left: 0;
}

::after伪元素从 HTML 标记中的链接数据属性获取内容:


a::after {
content: attr(data-replace);
}

transform: translate3d()::after伪元素元素向右移动 200%,悬停再回到以前的位置。


a::after {
content: attr(data-replace);
top: 0;
transform-origin: 100% 50%;
transform: translate3d(200%, 0, 0);
}

a:hover::after,
a:focus::after {
transform: translate3d(0, 0, 0);
}

我们使用transform: scaleX(0)::before伪元素,因此默认情况下它是隐藏的。悬停后我们将使它显示出来,就像2px高度一样,并将其固定到 上bottom,使其看起来像文本上的下划线那种感觉,看一下代码就理解我说的意思了


a::before {
background-color: #54b3d6;
height: 2px;
bottom: 0;
transform-origin: 100% 50%;
transform: scaleX(0);
}

a:hover::before,
a:focus::before {
transform-origin: 0% 50%;
transform: scaleX(1);
}

随后加入了transform效果、一些颜色等等以获得完整的效果。

作者:前端高级工程师宋
来源:juejin.cn/post/7143596588579946503
an>

收起阅读 »

五分钟实现一个chatGPT打字效果

web
由于chatGPT最近大火,甲方爸爸觉得这样的打字效果很酷,必须要在项目中安排一下,所以动手实现了这个效果 打字状态分析 loading - 在等待打字内容的时候光标会一直显示且闪烁 tyeing - 在打字中光标会显示但不闪烁 end - 在打字结束后光标...
继续阅读 »

由于chatGPT最近大火,甲方爸爸觉得这样的打字效果很酷,必须要在项目中安排一下,所以动手实现了这个效果


打字状态分析



  1. loading - 在等待打字内容的时候光标会一直显示且闪烁

  2. tyeing - 在打字中光标会显示但不闪烁

  3. end - 在打字结束后光标隐藏


样式


// 光标字符显示
.typing::after {
content: '▌';
}
// 光标闪烁动画
.blinker::after {
animation: blinker 1s step-end infinite;
}
@keyframes blinker {
0% {
visibility: visible;
}
50% {
visibility: hidden;
}
100% {
visibility: visible;
}
}

内容打印功能实现


结合定时器和光标样式设置


**
* @description:
* @param {HTMLElement} dom - 打印内容的dom
* @param {string} content - 打印文本内容
* @param {number} speed - 打印速度
* @return {void}
*/
function printText(dom, content, speed = 50) {
let index = 0
setCursorStatus(dom, 'typing')
let printInterval = setInterval(() => {
dom.innerText += content[index]
index++
if (index >= content.length) {
setCursorStatus(dom, 'end')
clearInterval(printInterval)
}
}, speed)
}

/**
* @description: 设置dom的光标状态
* @param {HTMLElement} dom - 打印内容的dom
* @param {"loading"|"typing"|"end"} status - 打印状态
* @return {void}
*/

function setCursorStatus(dom, status) {
const classList = {
loading: 'typing blinker',
typing: 'typing',
end: '',
}
dom.className = classList[status]
}

效果预览


作者:chansee97
来源:juejin.cn/post/7221368910139113531
an>

收起阅读 »

同一页面多次调用图形验证码

缘由一个页面需要两个验证码,使用同一个验证码调用两次会导致有前一个失效。那么我们需要创建不同的两个验证码,分别做验证。截图展示具体实现同时引入多个KgCaptcha的js。引入多个JS时,请定义 plural 参数;通过该参数区分定义对象名...
继续阅读 »

缘由

一个页面需要两个验证码,使用同一个验证码调用两次会导致有前一个失效。那么我们需要创建不同的两个验证码,分别做验证。


截图展示



具体实现

  • 同时引入多个KgCaptcha的js。
  • 引入多个JS时,请定义 plural 参数;通过该参数区分定义对象名,如plural=1,则对象名为kg1,以此类推。
<script src="captcha.js?appid=XXX&plural=1" id="KgCaptcha1"></script>
<script src="captcha.js?appid=XXX&plural=2" id="KgCaptcha2"></script>
  • 初始化验证码
<script type="text/javascript">

// 第一个验证码
kg1.captcha({
// 绑定元素,验证框显示区域
bind: "#captchaBox1",
// 验证成功事务处理
success: function(e) {
console.log(e);
},
// 验证失败事务处理
failure: function(e) {
console.log(e);
},
// 点击刷新按钮时触发
refresh: function(e) {
console.log(e);
}
});

// 第二个验证码
kg2.captcha({
// 绑定元素,验证框显示区域
bind: "#captchaBox2",
// 验证成功事务处理
success: function(e) {
console.log(e);
},
// 验证失败事务处理
failure: function(e) {
console.log(e);
},
// 点击刷新按钮时触发
refresh: function(e) {
console.log(e);
}
});

</script>

  • 创建验证码框区域
<!-- 第一个验证码 -->
<div id="captchaBox1"></div>
<!-- 第二个验证码 -->
<div id="captchaBox2"></div>


总结

SDK开源地址:https://github.com/KgCaptcha,顺便做了一个演示:https://www.kgcaptcha.com/demo/

收起阅读 »

一个Node.js图形验证码的生成

效果图准备访问KgCaptcha网站,注册账号后登录控制台,访问“无感验证”模块,申请开通后系统会分配给应用一个唯一的AppId、AppSecret。提供后端SDK来校验token(即安全凭据)是否合法 ,目前支持PHP版、Python版、Java/JSP版、...
继续阅读 »

效果图


准备

  • 访问KgCaptcha网站,注册账号后登录控制台,访问“无感验证”模块,申请开通后系统会分配给应用一个唯一的AppId、AppSecret。
  • 提供后端SDK来校验token(即安全凭据)是否合法 ,目前支持PHP版、Python版、Java/JSP版、.Net C#版。
  • 访问Node.js官网,下载Node.js运行环境,访问Vue.js中文官网,安装下载Vue.js,创建一个Vue项目,具体操作请查看Vue.js中文官网。

项目目录


index.html

项目根目录index.html文件,头部引用KgCaptcha的js。

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<!--引入凯格行为验证码js-->
<script id="KgCaptcha" src="captcha.js?appid=XXX"></script>
<!--引入凯格行为验证码js-->
</head>
<body>
<!--Vue主体-->
<div id="app"></div>
<!--Vue主体-->
</body>
</html>

main.js

src/main.js文件中,配置路由。

import Vue from 'vue'
import App from './App'
import router from './router'
Vue.config.productionTip = false
// 配置全局路由、组件
new Vue({
el: '#app',
router,
components: { App },
template: ''
})

App.vue

src/App.vue文件中,定义html。

<template>
<div id="app">
<!--自定义组件、内容-->
<form id="form">
token: <input name="token" _cke_saved_name="token" _cke_saved_name="token" _cke_saved_name="token" id="token">
<!--凯格行为验证码组件-->
<div id="captchaBox"></div>
<!--凯格行为验证码组件-->
<button type="submit">提交</button>
</form>
<!--自定义组件、内容-->
</div>
</template>

<script>
export default {
name: 'App',
}
//初始化凯格行为验证码
kg.captcha({
// 绑定元素,验证框显示区域
bind: "#captchaBox",
// 验证成功事务处理
success: function(e) {
console.log(e);
kg.$('#token').value = e['token']
},
// 验证失败事务处理
failure: function(e) {
console.log(e);
},
// 点击刷新按钮时触发
refresh: function(e) {
console.log(e);
}
});
</script>


总结

SDK开源地址:https://github.com/KgCaptcha,顺便做了一个演示:https://www.kgcaptcha.com/demo/


收起阅读 »

Vue.js 滑动拼图验证码实现笔记

背景关于验证码的使用场景还是非常多的,很多网站上的验证码可谓是五花八门,下面是我使用Vue.js实现滑动拼图验证码做的一个笔记。效果展示准备工作访问KgCaptcha网站,注册账号后登录控制台,访问“无感验证”模块,申请开通后系统会分配给应用一个唯一的AppI...
继续阅读 »

背景

关于验证码的使用场景还是非常多的,很多网站上的验证码可谓是五花八门,下面是我使用Vue.js实现滑动拼图验证码做的一个笔记。

效果展示



准备工作

  • 访问KgCaptcha网站,注册账号后登录控制台,访问“无感验证”模块,申请开通后系统会分配给应用一个唯一的AppId、AppSecret。
  • 提供后端SDK来校验token(即安全凭据)是否合法 ,目前支持PHP版、Python版、Java/JSP版、.Net C#版。
  • 访问Vue.js中文官网,复制Vue.js插件链接。
  • 注意:先HTML头部初始化行为验证码,然后HTML底部初始化Vue.js,否则KgCaptcha的js部分函数与被Vue.js发生冲突,导致失效。

实现代码

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<!--头部引入Vue.js插件-->
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<!--头部引入Vue.js插件-->
<!--头部引入行为验证码js插件-->
<script id="KgCaptcha" src="captcha.js?appid=XXX"></script>
<script>
kg.captcha({
// 绑定元素,验证框显示区域
bind: "#captchaBox",
// 验证成功事务处理
success: function(e) {
console.log(e);
kg.$('#token').value = e['token'];
},
// 验证失败事务处理
failure: function(e) {
console.log(e);
},
// 点击刷新按钮时触发
refresh: function(e) {
console.log(e);
}
});
</script>
<!--头部引入行为验证码js插件-->
</head>

<body>
<div id="app">
<!--自定义内容、Vue组件-->
token: <input name="token" id="token" />
<!--行为验证码组件-->
<div id="captchaBox"></div>
<!--行为验证码组件-->
<button type="button">提交</button>
<!--自定义内容、Vue组件-->
</div>
</body>

<!--底部运行Vue.js代码-->
<script>
var app = new Vue({
el: '#app',
})
</script>
<!--底部运行Vue.js代码-->

</html>


最后

SDK开源地址:https://github.com/KgCaptcha,顺便做了一个演示:https://www.kgcaptcha.com/demo/

收起阅读 »

整个活儿~永远加载不满的进度条

web
前言各位开发大佬,平时肯定见到过这种进度条吧,一直在加载,但等了好久都是在99% 如下所示: 有没有好奇这个玩意儿咋做的呢? 细听分说 (需要看使用:直接看实践即可)fake-progress如果需要实现上面的这个需求,其实会涉及到fake-progre...
继续阅读 »

前言

各位开发大佬,平时肯定见到过这种进度条吧,一直在加载,但等了好久都是在99%

如下所示:

有没有好奇这个玩意儿咋做的呢?
细听分说 (需要看使用:直接看实践即可)

fake-progress

如果需要实现上面的这个需求,其实会涉及到fake-progress这个库,具体是干嘛的呢?
这个库会提供一个构造函数,创建一个实例对象后,里面的属性会给我们进度条需要的数据等信息。
如图所示:


fake-progress库的源码如下:

/**
* Represents a fakeProgress
* @constructor
* @param {object} options - options of the contructor
* @param {object} [options.timeConstant=1000] - the timeConstant in milliseconds (see https://en.wikipedia.org/wiki/Time_constant)
* @param {object} [options.autoStart=false] - if true then the progress auto start
*/

const FakeProgress = function (opts) {
 if (!opts) {
   opts = {};
}
 // 时间快慢
 this.timeConstant = opts.timeConstant || 1000;
 // 自动开始
 this.autoStart = opts.autoStart || false;
 this.parent = opts.parent;
 this.parentStart = opts.parentStart;
 this.parentEnd = opts.parentEnd;
 this.progress = 0;
 this._intervalFrequency = 100;
 this._running = false;
 if (this.autoStart) {
   this.start();
}
};

/**
* Start fakeProgress instance
* @method
*/

FakeProgress.prototype.start = function () {
 this._time = 0;
 this._intervalId = setInterval(
   this._onInterval.bind(this),
   this._intervalFrequency
);
};

FakeProgress.prototype._onInterval = function () {
 this._time += this._intervalFrequency;
 this.setProgress(1 - Math.exp((-1 * this._time) / this.timeConstant));
};

/**
* Stop fakeProgress instance and set progress to 1
* @method
*/

FakeProgress.prototype.end = function () {
 this.stop();
 this.setProgress(1);
};

/**
* Stop fakeProgress instance
* @method
*/

FakeProgress.prototype.stop = function () {
 clearInterval(this._intervalId);
 this._intervalId = null;
};

/**
* Create a sub progress bar under the first progres
* @method
* @param {object} options - options of the FakeProgress contructor
* @param {object} [options.end=1] - the progress in the parent that correspond of 100% of the child
* @param {object} [options.start=fakeprogress.progress] - the progress in the parent that correspond of 0% of the child
*/

FakeProgress.prototype.createSubProgress = function (opts) {
 const parentStart = opts.start || this.progress;
 const parentEnd = opts.end || 1;
 const options = Object.assign({}, opts, {
   parent: this,
   parentStart: parentStart,
   parentEnd: parentEnd,
   start: null,
   end: null,
});

 const subProgress = new FakeProgress(options);
 return subProgress;
};

/**
* SetProgress of the fakeProgress instance and updtae the parent
* @method
* @param {number} progress - the progress
*/

FakeProgress.prototype.setProgress = function (progress) {
 this.progress = progress;
 if (this.parent) {
   this.parent.setProgress(
    (this.parentEnd - this.parentStart) * this.progress + this.parentStart
  );
}
};

我们需要核心关注的参数只有timeConstant,autoStart这两个参数,通过阅读源码可以知道timeConstant相当于分母,分母越大则加的越少,而autoStart则是一个开关,如果开启了直接执行start方法,开启累计的定时器。
通过这个库,我们实现一个虚拟的进度条,永远到达不了100%的进度条。
但是如果这时候像接口数据或其他什么资源加载完了,要到100%了怎么办呢?可以看到代码中有end()方法,因此显示的调用下实例的end()方法即可。

实践

上面讲了这么多下面结合圆形进度条(后面再出个手写圆形进度条)来实操一下,效果如下:


代码如下所示:

<template>
 <div ref="main" class="home">
   </br>
   <div>{{ fake.progress }}</div>
   </br>
   <Progress type="circle" :percentage="parseInt(fake.progress*100)"/>
   </br></br>
   <el-button @click="stop">停止</el-button>
   </br></br>
   <el-button @click="close">关闭</el-button>
 </div>
</template>

<script>
import FakeProgress from "fake-progress";

export default {
 data() {
   return {
     fake: new FakeProgress({
       timeConstant : 6000,
       autoStart : true
    })
  };
},
 methods:{
   close() {
     this.fake.end()
  },
   stop() {
     this.fake.stop()
  }
},
};
</script>

总结

如果需要实现一个永远不满的进度条,那么你可以借助fake-progress
核心是1 - Math.exp((-1 * this._time) / this.timeConstant) 这个公式
涉及到一个数据公式: e的负无穷次方 趋近于0。所以1-e^-x永远到不了1,但趋近于1

核心原理就是:用时间做分子,传入的timeConstant做分母,通过Math.exp((-1 * this._time) / this.timeConstant) 可知,如果时间不断累积且为负值,那么Math.exp((-1 * this._time) / this.timeConstant) 就无限趋近于0。所以1 - Math.exp((-1 * this._time) / this.timeConstant) 就可以得到无限趋近于1 的值

总结,如果需要使用的话,在使用的地方创建一个实例即可(配置autoStart之后就会自动累加):

new FakeProgress({
   timeConstant : 6000,
   autoStart : true
})

如果需要操作停止或介绍使用其实例下的对应方法即可

this.fake.end()
this.fake.stop()

作者:前端xs
来源:juejin.cn/post/7219195850539057212

收起阅读 »

低代码开发,是稳扎稳打还是饮鸩止渴?

web
2023年,从业者对低代码的发展充满了想象,人们认为,未来低代码它的商业价值不可估量。据Gartner的最新报告显示,到2023年,全球低代码开发技术市场规模预计将达到269亿美元,比2022年增长19.6%。 随着数字化进入深水区,企业碎片化、个性化、临时...
继续阅读 »

2023年,从业者对低代码的发展充满了想象,人们认为,未来低代码它的商业价值不可估量。据Gartner的最新报告显示,到2023年,全球低代码开发技术市场规模预计将达到269亿美元,比2022年增长19.6%。



随着数字化进入深水区,企业碎片化、个性化、临时化的需求不断涌现,而无论传统应用还是SaaS服务,都无法满足企业的全部需求,企业组织越来越多地转向低代码开发技术,以满足对快速应用交付和高度定制的自动化工作流程不断增长的需求。


image.png


中小企业的IT基础薄弱,人才有限,自研难度很大;中大型企业虽然有专门的IT部门,但审核流程长,业务部门的需求也无法立马满足。而低代码开发,只需编写少量代码或无需代码,就可以快速生成应用程序,在理论上刚好是解决这类问题的钥匙。


全民开发


低代码确实可以满足企业大部分IT需求,普通的业务人员也能进行应用搭建,成为平台的最终用户,写更少的代码,花更少的钱,干更多的事。就算是拥有独立IT部门的中大型企业,也会存在大量临时性边缘的业务需求,低代码可以很好的应对。


image.png


目前市场上有三种类型的低代码厂家:原生厂商、应用软件厂商、云厂商。随着低代码玩家越来越多,整个赛道的竞争将越来越激烈,有从业者发出呐喊:低代码产品未来到底是继续加功能,让更多开发者进来,以此满足客户普遍需求?还是通过一些其他模块或者应用市场的方式来解决客户专业需求?


一些厂商认为应该细分领域,比如深耕CRM、进销存、OKR、人事管理等热门应用模板;还有一部分厂商认为低代码的发展应该要走一条农村包围城市的路,从小处着眼,走普遍路线,主协作,帮助产研内部进行更高效的协同和项目管理,帮助IT部门更好地与业务部门建立起协作关系即可。


image.png


所以,在低代码赛道上,未来的“分流”趋势或将越来越明显。以JNPF为代表的“轻应用”派,由表单所驱动,重视数据处理能力、快速开发能力、低门槛等。


JNPF,立足于低代码开发技术,采用主流的两大技术Java/.Net开发,专注低代码开发,有拖拽式的代码生成器,灵活的权限配置、SaaS服务,强大的接口对接,随心可变的工作流引擎。支持多端协同操作,100%提供源码,支持多种云环境部署、本地部署。


image.png


基于代码生成器,可一站式开发多端使用Web、Android、IOS、微信小程序。代码自动生成后可以下载本地,进行二次开发,有效提高整体开发效率。


开源入口:http://www.yinmaisoft.com/?from=jeuji…


已经覆盖零售、医疗、制造、银行、建筑、教育、社会治理等主流行业,一站式搭建:生产管理系统、项目管理系统、进销存管理系统、OA办公系统、人事财务等等。可以节省开发人员80%时间成本,并且有以构建业务流程、逻辑和数据模型等所需的功能。



这是看得见的价值,但也有看不见的顾虑


有人认为,低代码应用是一种“饮鸩止渴”的行为,会让部分企业觉得,数字化转型就那样,哪些业务需要,就采用低代码应用“缝缝补补”即可,最终浅尝辄止,公司的整个数字化转型停在半道,欠缺完备性、统一性以及系统性。类似的问题,或许在未来会出现,也可能会在低代码应用的迭代过程中被解决。



2023,行至水深处,低代码的路会越来越难走,但这也是黎明前必经的黑暗。稻盛和夫曾说,人生如粥,熬出至味,相信在穿过重重迷雾后,2023年低代

作者:jnpfsoft
来源:juejin.cn/post/7220696541308436541
码也将迎来新的发展。

收起阅读 »

产品说要让excel在线编辑,我是这样做的。

web
背景 最近公司项目有需求, 某导入功能, 想让客户选完excel文件, 直接将加载到web的excel编辑器中, 修改、确认, 之后上传导入。 效果查看 选择 Luckysheet(dream-num.github.io/LuckysheetD…) ,一款...
继续阅读 »

背景


最近公司项目有需求, 某导入功能, 想让客户选完excel文件, 直接将加载到web的excel编辑器中, 修改、确认, 之后上传导入。


效果查看


Kapture 2023-04-13 at 13.37.05.gif


选择



就看到了这两个, 最后选择了Luckysheet, 看他的star比较多, 哈哈。


需求实现分析


分析一下整个流程。


其实大体就两步, 搞进去,抽离出来。


一、加载本地excel到web编辑器中


1、拿到本地excel文件流


2、转换为 Luckysheet 要的格式


3、new 一个 Luckysheet 实例, 挂在到对应标签上


完成以上就把excel加载进去了, 显示出来了。


在线编辑的事就是这个库帮咱们搞定了.


二、 从web编辑器导出文件流 上传


等客户在线编辑完成, 就需要点击一个按钮, 导出文件流, 确认并调接口上传


1、获取 Luckysheet里工作表的数据


image.png


luckysheet.getAllSheets()

2、将数据加工并使用xlsx或者exceljs导出文件流


导出为为arrayBuffer, 再将arrayBuffer转为Blob


3、调后端接口上传


开发实践


一、引入 lucky-sheet


有两种方式


1、官方文档里的cdn


这种加载有点慢


<link rel='stylesheet' href='https://cdn.jsdelivr.net/npm/luckysheet/dist/plugins/css/pluginsCss.css' />
<link rel='stylesheet' href='https://cdn.jsdelivr.net/npm/luckysheet/dist/plugins/plugins.css' />
<link rel='stylesheet' href='https://cdn.jsdelivr.net/npm/luckysheet/dist/css/luckysheet.css' />
<link rel='stylesheet' href='https://cdn.jsdelivr.net/npm/luckysheet/dist/assets/iconfont/iconfont.css' />
<script src="https://cdn.jsdelivr.net/npm/luckysheet/dist/plugins/js/plugin.js"></script>
<script src="https://cdn.jsdelivr.net/npm/luckysheet/dist/luckysheet.umd.js"></script>

2、自己打包, 传到oss, 引入(推荐)



第一种第三方的cdn不稳定, 有时候很慢,还是建议,拉他的仓库,然后打个包,传到自己静态资源库, 来使用



npm run builddist 传上去使用


二、指定容器


<div id="luckysheet"></div>

三、导入本地文件


1、 用elment的上传文件组件 选择文件


但是这里不上传,仅仅是用它选择文件拿到文件对象File


<div class="import-okr">
<!-- ,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet -->
<el-upload
v-model:file-list="fileList"
accept="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
class="upload-demo"
:before-upload="beforeUpload"
action=""
:show-file-list="false"
>
<button @click="uploadFile">上传数据</button>
</el-upload>
</div>

2、beforeUpload 方法拿到文件


const beforeUpload = (file) => {
console.log(file)
}

image.png


3、将文件流转换为lucky要的格式


github.com/dream-num/L…


安装转换工具


npm install luckyexcel

使用


// After getting the xlsx file
LuckyExcel.transformExcelToLucky(file,
function(exportJson, luckysheetfile){
// exportJson就是转换后的数据
},
function(error){
// handle error if any thrown
}

4、将转换后的数据创建表格


// 将拿到的数据创建表格
luckysheet.create({
container: 'luckysheet', // luckysheet is the container id
data:exportJson.sheets,
title:exportJson.info.name,
userInfo:exportJson.info.creator,
lang: 'zh', // 设定表格语言
myFolderUrl: window.location.href,
showtoolbarConfig: {
pivotTable: false, //'数据透视表'
// protection: false, // '工作表保护'
print:false, // '打印'
image: false, // 插入图片
},
showinfobar: false,
options: {
// 其他配置
userImage:'http://qzz-static.forwe.store/public-assets/pgy_kj_pic_logo.png?x-oss-process=image/resize,m_fill,w_72,h_72', // 头像url
userName:'Lucky', // 用户名
}
});

完整代码


const beforeUpload = (file) => {
console.log(file)
// 转换工具, 将文件流转换为lucky要的格式
LuckyExcel2.transformExcelToLucky(
file,
function(exportJson, luckysheetfile){
isShowExcel.value = true
console.log(exportJson)
nextTick(() => {
window.luckysheet.destroy();
// 将拿到的数据创建表格
luckysheet.create({
container: 'luckysheet', // luckysheet is the container id
data:exportJson.sheets,
title:exportJson.info.name,
userInfo:exportJson.info.creator,
lang: 'zh', // 设定表格语言
myFolderUrl: window.location.href,
showtoolbarConfig: {
pivotTable: false, //'数据透视表'
// protection: false, // '工作表保护'
print:false, // '打印'
image: false, // 插入图片
},
showinfobar: false,
options: {
// 其他配置
userImage:'http://qzz-static.forwe.store/public-assets/pgy_kj_pic_logo.png?x-oss-process=image/resize,m_fill,w_72,h_72', // 头像url
userName:'Lucky', // 用户名
}
});
})
},
function(err){
logger.error('Import failed. Is your fail a valid xlsx?');
});
}

四、导出


1、利用 luckysheet.getAllSheets() 获取表数据


console.log(luckysheet.getAllSheets())

image.png


2、exceljs将上述对象转换为excel文件流


import Excel  from 'exceljs'
// 导出excel
const exportExcel = async function (luckysheet) { // 参数为luckysheet.getluckysheetfile()获取的对象
// 1.创建工作簿,可以为工作簿添加属性
const workbook = new Excel.Workbook()
// 2.创建表格,第二个参数可以配置创建什么样的工作表
luckysheet.every(function (table) {
if (table.data.length === 0) return true
const worksheet = workbook.addWorksheet(table.name)
// 3.设置单元格合并,设置单元格边框,设置单元格样式,设置值
setStyleAndValue(table.data, worksheet)
setMerge(table.config.merge, worksheet)
setBorder(table.config.borderInfo, worksheet)
return true
})
// 4.写入 buffer
const buffer = await workbook.xlsx.writeBuffer()
return buffer
}

3、 写个方法,执行上述两步


// 保存文件
const onClickSaveFile = async ( ) => {
console.log(luckysheet.getAllSheets())
const buf = await exportExcel(luckysheet.getAllSheets())
const blob = new Blob([buf]);
// $emit('file', blob)
handleUpload(blob)
}

4、上传方法


利用formData, 将生成的文件二进制流发给后端


const handleUpload = async(file) => {
// isShowExcel.value = false
const loading = ElLoading.service({
fullscreen: true,
text: '上传中,请稍等',
background: 'rgba(0,0,0,0.1)'
});
try {
const formData = new FormData()
formData.append('file', file)
const {code, data, message } = await IMPORT_OKR(formData)
if(code === 1) {
//...
}
loading.close()
} catch (error) {
console.log(error)
loading.close()
}
}

遇到问题


1、iconfont冲突


lucky-sheet这个项目里的iconfont类名和我项目里一样,导致有些被覆盖了.
image.png


解决: 将他项目里 iconfont 换成 lucky-sheet, 相关类名也全部替换, 然后重新打包,再引入,即可解决


2、lucky-sheet层级不够高,无法编辑


image.png


elmentui和antd的一些组件层级比较高,所以, 让kucky的层级更高即可


解决: 增加下述css即可


.luckysheet-input-box { z-index: 2000; } .luckysheet-cols-menu { z-index: 2001; }

最后


妥妥的都是站在巨人的肩膀上


求赞


作者:浏览器API调用工程师
来源:juejin.cn/post/7221368910139342907
收起阅读 »

KgCaptcha滑动拼图验证码在搜索中的作用

开头验证码应用于我们生活、工作的方方面面,比如注册登录账号、支付订单、修改密码等。下面我是在一次项目中利用滑动拼图验证码和搜索功能“合作共赢”的记录。验证码展示具体实现前端代码// 引入js<script src="captcha.js?appid=XX...
继续阅读 »

开头

验证码应用于我们生活、工作的方方面面,比如注册登录账号、支付订单、修改密码等。下面我是在一次项目中利用滑动拼图验证码和搜索功能“合作共赢”的记录。

验证码展示



具体实现

前端代码
// 引入js
<script src="captcha.js?appid=XXX"></script>
<script>
kg.captcha({
// 绑定弹窗按钮
button: "#captchaButton",

// 验证成功事务处理
success: function (e) {
// 验证成功,直接提交表单
// form1.submit();
console.log(e);
},

// 验证失败事务处理
failure: function (e) {
console.log(e);
},

// 点击刷新按钮时触发
refresh: function (e) {
console.log(e);
}
});
</script>

<a id="captchaButton"></a>



验证结果说明

 

字段名
数据类型描述
 

code
 

number
 

返回code信息
 

msg
 

string
 

验证结果信息
 

rid
 

number
 

用户的验证码应用id
 

sense
 

number
 

是否开启无感验证,0-关闭,1-开启
 

token
 

string
 

验证成功才有:token
 

weight
 

number
 

错误严重性,0正常错误,可以继续操作,1一般错误,刷新/重新加载拼图,2严重错误,错误次数过多拒绝访问


Python代码

from wsgiref.simple_server import make_server
from KgCaptchaSDK import KgCaptcha
def start(environ, response):
# 填写你的 AppId,在应用管理中获取
AppID = "AppId"
# 填写你的 AppSecret,在应用管理中获取
AppSecret = "AppSecret"
request = KgCaptcha(AppID, AppSecret)
# 填写应用服务域名,在应用管理中获取
request.appCdn = "https://cdn.kgcaptcha.com"
# 请求超时时间,秒
request.connectTimeout = 10
# 用户id/登录名/手机号等信息,当安全策略中的防控等级为3时必须填写
request.userId = "kgCaptchaDemo"
# 使用其它 WEB 框架时请删除 request.parse,使用框架提供的方法获取以下相关参数
parseEnviron = request.parse(environ)
# 前端验证成功后颁发的 token,有效期为两分钟
request.token = parseEnviron["post"].get("kgCaptchaToken", "") # 前端 _POST["kgCaptchaToken"]
# 客户端IP地址
request.clientIp = parseEnviron["ip"]
# 客户端浏览器信息
request.clientBrowser = parseEnviron["browser"]
# 来路域名
request.domain = parseEnviron["domain"]
# 发送请求
requestResult = request.sendRequest()
if requestResult.code == 0:
# 验证通过逻辑处理
html = "验证通过"
else:
# 验证失败逻辑处理
html = f"{requestResult.msg} - {requestResult.code}"
response("200 OK", [("Content-type", "text/html; charset=utf-8")])
return [bytes(str(html), encoding="utf-8")]
httpd = make_server("0.0.0.0", 8088, start) # 设置调试端口 http://localhost:8088/
httpd.serve_forever()


最后

SDK开源地址:KgCaptcha (KgCaptcha) · GitHub,顺便做了一个演示:凯格行为验证码在线体验

收起阅读 »

Java实现KgCaptcha短信验证码

背景Java是一种流行的编程语言,验证码是一种常用的网络安全技术。Java发展至今,网上也出现了各种各样的验证码,本人初学Java,下面是我用Java实现短信验证码的总结。截图展示实现代码后台接收前台的kgCaptchaToken进行验证,验证成功执行成功处理...
继续阅读 »

背景

Java是一种流行的编程语言,验证码是一种常用的网络安全技术。Java发展至今,网上也出现了各种各样的验证码,本人初学Java,下面是我用Java实现短信验证码的总结。

截图展示



实现代码

后台接收前台的kgCaptchaToken进行验证,验证成功执行成功处理,验证失败返回错误代码及信息。

package com.kyger;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;

public class demo extends HttpServlet {
private static final long serialVersionUID = 1L;

public demo() {
super();
}

protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

// 编码
request.setCharacterEncoding("utf-8");
response.setCharacterEncoding("utf-8");;
response.setContentType("text/html; charset=utf-8");

// 后台处理
if (request.getMethod().equals("POST")){
String html, appId, appSecret, Token;

// 设置 AppId 及 AppSecret,在应用管理中获取
appId = "appId";
appSecret = "appSecret";

// 填写你的 AppId 和 AppSecret,在应用管理中获取
KgCaptchaSDK KgRequest = new KgCaptchaSDK(appId, appSecret);


// 前端验证成功后颁发的 token,有效期为两分钟
KgRequest.token = request.getParameter("kgCaptchaToken");
// System.out.print(KgRequest.token);

// 填写应用服务域名,在应用管理中获取
KgRequest.appCdn = "https://cdn.kgcaptcha.com";

// 请求超时时间,秒
KgRequest.connectTimeout = 5;

// 用户登录或尝试帐号,当安全策略中的防控等级为3时必须填写,一般情况下可以忽略
// 可以填写用户输入的登录帐号(如:request.getParameter("username"),可拦截同一帐号多次尝试等行为
KgRequest.userId = "kgCaptchaDemo";

// request 对象,当安全策略中的防控等级为3时必须填写,一般情况下可以忽略
KgRequest.request = request;
// java 环境中无法提供 request 对象,请分别定义:clientIp|clientBrowser|domain 参数,即:
// KgRequest.clientIp = "127.0.0.1"; // 填写客户端IP
// KgRequest.clientBrowser = ""; // 客户端浏览器信息
// KgRequest.domain = "http://localhost"; // 你的授权域名或服务IP

// 发送验证请求
Map requestResult = KgRequest.sendRequest();
if("0".toString().equals(requestResult.get("code"))) {
// 验签成功逻辑处理 ***

// 这里做验证通过后的数据处理
// 如登录/注册场景,这里通常查询数据库、校验密码、进行登录或注册等动作处理
// 如短信场景,这里可以开始向用户发送短信等动作处理
// ...

html = "";
} else {
// 验签失败逻辑处理
html = "";
}

response.getWriter().append(html);
} else {
response.sendRedirect("index.html");
}
}

protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}

}


后端检测

后台接收数据,同时对来源及应用进行检测。

# 服务器黑名单检测
if self.auth.client_blacklist():
return self.r_code(20017) # 服务器黑名单

# 验签次数限制检测
excess = self.auth.excess(2)
if excess:
return self.r_code(code=[20020, 20021, 20022][excess - 1])

# 来路域名检测
if not self.kg["HTTP_REFERER"]: return self.r_code(20004) # 域名不合法,无法获取来路域名
if not self.auth.domain_auth(): return self.r_code(20005) # 来源域名未授权

# 应用有效时间检测
validity = self.auth.app_validity()
if validity[0] == 1: return self.r_code(20006) # 授权未开始
if validity[0] == 2: return self.r_code(20007) # 授权已结束

if self.auth.app_state(): return self.r_code(20008) # 当前应用/域名被禁用


结尾

SDK开源地址:KgCaptcha (KgCaptcha) · GitHub,顺便做了一个演示:凯格行为验证码在线体验

收起阅读 »

🚀 我用一小时实现的娃娃机,你敢信?

web
生活不止眼前的苟且,还有诗和远方 掘友们,大家好我是前端奶爸,入行5年的前端小学生🥜~ 工作八小时摸鱼四小时,喜欢跑步但不是为了让自己更瘦,而是为了让自己活得更久~ 活到九十九,卷到九十九~ 前言 前段时间去商场吃饭的时候看到一个有趣的娃娃机,一个密封的机...
继续阅读 »

生活不止眼前的苟且,还有诗和远方



掘友们,大家好我是前端奶爸,入行5年的前端小学生🥜~

工作八小时摸鱼四小时,喜欢跑步但不是为了让自己更瘦,而是为了让自己活得更久~

活到九十九,卷到九十九~



前言


前段时间去商场吃饭的时候看到一个有趣的娃娃机,一个密封的机器里底部放着一些被捆绑好的龙虾,可以买币去抓龙虾,抓到以后可以初加工费找附近的商家给做成龙虾大餐,感觉很有意思,把抓抓玩出了一个新的高度~


主要是抓到以后还可以出手工费进行烹饪,很吸引人,周边围观的人也很多,观察了一会发现。爪子的抓力不够,龙虾在水里还能移动,而且感觉每一个个头都不小,那小爪感觉根本抓不起来~~


到家后孩子就说爸爸你可不可以做一个娃娃机呢?


身为一个程序员,这点要求我感觉还是难不倒我,然后就突发奇想,给孩子在手机上做一个简易娃娃机。起初的想法是哄她开心,看到掘金最近有小游戏的活动,顺便分享给大家~~


效果


简易娃娃机.gif


如上图,一个移动的抓手,以及几个礼物样品,还有左右移动,抓起按钮,素材很简单,但是做出来的效果还是有娃娃机的感觉的~


地址


代码托管地址在:github在线预览地址资源路径不对无法访问,如果有需要源码的同学可以自行去git仓库获取~


布局


布局部分比较简单,直接贴代码了。可以根据自己的需求不同自定义即可~


<div class="page-portrait" id="page-portrait">
<div id="pageContainer" class="page-container game-box">
<div class="poster-main">
<ul class="poster-list">
<li class="item lw1"><img src="images/dx-lw1.png" alt=""></li>
<li class="item lw2"><img src="images/dx-lw2.png" alt=""></li>
<li class="item lw3"><img src="images/dx-lw3.png" alt=""></li>
<li class="item lw4"><img src="images/dx-lw4.png" alt=""></li>
<li class="item lw5"><img src="images/dx-lw5.png" alt=""></li>
<li class="item lw6"><img src="images/dx-lw6.png" alt=""></li>
</ul>
</div>
<div id="stop" class="button"></div>
<div id="left" class="left-btn"></div>
<div id="right" class="right-btn"></div>
<div class="zhua-top">
<span class="zhua-zuo"></span>
<span class="zhua-zhu"></span>
<div class="zhua zhuamove"></div>
</div>
</div>
</div>

css用到了几个运动处理了爪子的动效,如下方代码所示


@keyframes run {
0% {
background-image: url(../images/dx-zhua3.png);
}
25% {
background-image: url(../images/dx-zhua2.png);
}
50% {
background-image: url(../images/dx-zhua1.png);
}
75% {
background-image: url(../images/dx-zhua2.png);
}
100% {
background-image: url(../images/dx-zhua3.png);
}
}
@keyframes zhuashou {
0% {
top: 360px;
background-image: url(../images/dx-zhua1.png);
}
100% {
top: 360px;
background-image: url(../images/dx-zhua2.png);
}
}
@keyframes zhuadown {
0% {
top: 138px;
background-image: url(../images/dx-zhua1.png);
}
100% {
top: 360px;
background-image: url(../images/dx-zhua1.png);
}
}
@keyframes zhua-slideUp {
0% {
top: 360px;
background-image: url(../images/dx-zhua2.png);
}
100% {
top: 138px;
background-image: url(../images/dx-zhua2.png);
}
}
@keyframes img-slideUp {
0% {
top: 23px;
}
100% {
top: -200px;
}
}

js代码创建了一个控制器类,处理事件以及动画效果的交替等。


var Carousel = {
data: {
result: 1
},
init: function () {
Carousel.control();
},
stop: function () {
$(".zhua").removeClass("zhuamove").addClass("zhuadown");
$(".zhua-zhu").addClass("zhudown");
var timer01 = setTimeout(function () {
$(".zhua").removeClass("zhuadown").addClass("zhuashou");
var timer03 = setTimeout(function () {
$(".zhua").removeClass("zhuashou").addClass("zhuaup");
$(".zhua-zhu").removeClass("zhudown").addClass("zhuup");
$(".poster-list .lw" + (Carousel.data.result + 1)).addClass("img-slideUp");
clearTimeout(timer03);
timer03 = null;
}, 800);
var timer02 = setTimeout(function () {
$(".zhua").removeClass("zhuaup").removeClass("zhuaup1");
$(".zhua-zhu").removeClass("zhuup");
clearTimeout(timer02);
timer02 = null;
alert("恭喜您抽中一等奖~");
Carousel.start();
}, 2500);
clearTimeout(timer01);
timer01 = null;
}, 1000);
},
start: function () {
$(".zhua").addClass("zhuamove");
$(".zhua").removeClass("zhuadown").removeClass("zhuaup1").removeClass("zhuaup");
$(".poster-list .item").removeClass("img-slideUp").removeClass("img-slideOutUp");
},
zhuaMove: function (num) {
switch (num) {
case 0:
$(".zhua-top").animate({
left: -145,
},300);
break;
case 1:
$(".zhua-top").animate({
left: 0,
},300);
break;
case 2:
$(".zhua-top").animate({
left: 145,
},300);
break;
}
},
control: function () {
$("#left").on("click", function () {
Carousel.data.result--;
if (Carousel.data.result <= 0) {
Carousel.data.result = 0;
}
Carousel.zhuaMove(Carousel.data.result);
});
$("#stop").click(Carousel.stop);
$("#right").on("click", function () {
Carousel.data.result++;
if (Carousel.data.result >= 2) {
Carousel.data.result = 2;
}
Carousel.zhuaMove(Carousel.data.result);
});
},
};

总结


css现在有很多的新的特性可以解决我们工作中遇到的动效以及兼容问题,有心的同学可以多多查阅文档,写一写自己感兴趣的小demo,或者给孩子做一个小游戏来玩,何尝不是一件有成就的事呢~


我是奶爸,喜欢我的可以关注我,有什么新的想法或者意见也可以在评论区留言,我们共同学习,共同进步~



最后希望疫情早早结束,微风袭来,春暖花开~~~



作者:前端奶爸
来源:juejin.cn/post/7089371535588196366
收起阅读 »

【404】你访问的页面需要关灯后查看!

web
前言 今天在掘金首页刷到一篇文章,就是那种文字根据不同的色块显示不同的颜色,我想着能不能做一个探照灯似的 404 页面呢。毕竟也可以根据不同的白色光照来改变文字颜色的。 为了酷炫一点,先来个背景 👉 背景相对来说比较简单了,就是一些纯粹的漂浮点 <div...
继续阅读 »

前言


今天在掘金首页刷到一篇文章,就是那种文字根据不同的色块显示不同的颜色,我想着能不能做一个探照灯似的 404 页面呢。毕竟也可以根据不同的白色光照来改变文字颜色的。


为了酷炫一点,先来个背景


👉 背景相对来说比较简单了,就是一些纯粹的漂浮点


<div>
<div class="starsec"></div>
<div class="starthird"></div>
<div class="starfourth"></div>
<div class="starfifth"></div>
</div>

👉 为了显得与众不同,我们就用四个不同的 div 元素来写样式


.starsec {
content: " ";
position: absolute;
width: 3px;
height: 3px;
background: transparent;
box-shadow: 571px 173px #00BCD4, 1732px 143px #00BCD4, 1745px 454px #FF5722, 234px 784px #00BCD4, 1793px 1123px #FF9800, 1076px 504px #03A9F4, 633px 601px #FF5722, 350px 630px #FFEB3B, 1164px 782px #00BCD4, 76px 690px #3F51B5, 1825px 701px #CDDC39, 1646px 578px #FFEB3B, 544px 293px #2196F3, 445px 1061px #673AB7, 928px 47px #00BCD4, 168px 1410px #8BC34A, 777px 782px #9C27B0, 1235px 1941px #9C27B0, 104px 1690px #8BC34A, 1167px 1338px #E91E63, 345px 1652px #009688, 1682px 1196px #F44336, 1995px 494px #8BC34A, 428px 798px #FF5722, 340px 1623px #F44336, 605px 349px #9C27B0, 1339px 1344px #673AB7, 1102px 1745px #3F51B5, 1592px 1676px #2196F3, 419px 1024px #FF9800, 630px 1033px #4CAF50, 1995px 1644px #00BCD4, 1092px 712px #9C27B0, 1355px 606px #F44336, 622px 1881px #CDDC39, 1481px 621px #9E9E9E, 19px 1348px #8BC34A, 864px 1780px #E91E63, 442px 1136px #2196F3, 67px 712px #FF5722, 89px 1406px #F44336, 275px 321px #009688, 592px 630px #E91E63, 1012px 1690px #9C27B0, 1749px 23px #673AB7, 94px 1542px #FFEB3B, 1201px 1657px #3F51B5, 1505px 692px #2196F3, 1799px 601px #03A9F4, 656px 811px #00BCD4, 701px 597px #00BCD4, 1202px 46px #FF5722, 890px 569px #FF5722, 1613px 813px #2196F3, 223px 252px #FF9800, 983px 1093px #F44336, 726px 1029px #FFC107, 1764px 778px #CDDC39, 622px 1643px #F44336, 174px 1559px #673AB7, 212px 517px #00BCD4, 340px 505px #FFF, 1700px 39px #FFF, 1768px 516px #F44336, 849px 391px #FF9800, 228px 1824px #FFF, 1119px 1680px #FFC107, 812px 1480px #3F51B5, 1438px 1585px #CDDC39, 137px 1397px #FFF, 1080px 456px #673AB7, 1208px 1437px #03A9F4, 857px 281px #F44336, 1254px 1306px #CDDC39, 987px 990px #4CAF50, 1655px 911px #00BCD4, 1102px 1216px #FF5722, 1807px 1044px #FFF, 660px 435px #03A9F4, 299px 678px #4CAF50, 1193px 115px #FF9800, 918px 290px #CDDC39, 1447px 1422px #FFEB3B, 91px 1273px #9C27B0, 108px 223px #FFEB3B, 146px 754px #00BCD4, 461px 1446px #FF5722, 1004px 391px #673AB7, 1529px 516px #F44336, 1206px 845px #CDDC39, 347px 583px #009688, 1102px 1332px #F44336, 709px 1756px #00BCD4, 1972px 248px #FFF, 1669px 1344px #FF5722, 1132px 406px #F44336, 320px 1076px #CDDC39, 126px 943px #FFEB3B, 263px 604px #FF5722, 1546px 692px #F44336;
animation: animStar 150s linear infinite;
}

👉 颜色阴影部分都是一样的,不一样的地方就在于宽高和动画时长。


👉 大家可以根据自己的想法去修改不同的宽高和时长哦


👉 动画效果需要额外写一下的哦


@keyframes animStar {
0% {
transform: translateY(0px);
}

100% {
transform: translateY(-2000px);
}
}

screenshots.gif


画灯杆(电线)


👉 一般探照灯都是在顶上的,所以就需要用一根电线连接在顶部


<div class="lamp__wrap">
<div class="lamp">
<div class="cable"></div>
</div>
</div>


  • 后面的灯元素相关内容都会在 lamp 样式标签下面哦!


.lamp__wrap {
max-height: 100vh;
overflow: hidden;
max-width: 100vw;
}
.lamp {
position: absolute;
left: 0px;
right: 0px;
top: 0px;
margin: 0px auto;
width: 300px;
display: flex;
flex-direction: column;
align-items: center;
transform-origin: center top;
animation-timing-function: cubic-bezier(0.6, 0, 0.38, 1);
animation: move 5.1s infinite;
}


  • 在处理动画的时候,使用了一个 cubic-bezier 方法,它是用来定义贝塞尔曲线的


@keyframes move {
0% {
transform: rotate(40deg);
}

50% {
transform: rotate(-40deg);
}

100% {
transform: rotate(40deg);
}
}


  • 动画效果就是将灯杆旋转不同的角度



注意一下,动画效果是在整个灯的样式中完成的,所以后面的都只需要写各自的样式就行了,不需要补充动画效果。



.cable {
width: 8px;
height: 248px;
background-image: linear-gradient(rgb(32 148 218 / 70%), rgb(193 65 25)), linear-gradient(rgba(0, 0, 0, 0.7), rgba(0, 0, 0, 0.7)), linear-gradient(rgba(0, 0, 0, 0.7), rgba(0, 0, 0, 0.7));
}


  • 灯杆给了一个渐变色的样式效果


screenshots.gif


画灯罩


👉 灯杆已经有了,那就加一个灯罩就行了


<div class="cover"></div>

.cover {
width: 200px;
height: 80px;
background: #0bd5e8;
border-top-left-radius: 50%;
border-top-right-radius: 50%;
position: relative;
z-index: 200;
}


  • 灯罩是通过不同的 border-radius 的效果画出来的


screenshots.gif


画灯泡


👉 灯泡也是比较简单的样式,一个半圆加一部分阴影即可


<div class="in-cover">
<div class="bulb"></div>
</div>

.in-cover {
width: 100%;
max-width: 200px;
height: 20px;
border-radius: 100%;
background: #08ffff;
position: absolute;
left: 0px;
right: 0px;
margin: 0px auto;
bottom: -9px;
z-index: 100;
}

.in-cover .bulb {
width: 50px;
height: 50px;
background-color: #08fffa;
border-radius: 50%;
position: absolute;
left: 0px;
right: 0px;
bottom: -20px;
margin: 0px auto;
-webkit-box-shadow: 0 0 15px 7px rgba(0, 255, 255, 0.8), 0 0 40px 25px rgba(0, 255, 255, 0.5), -75px 0 30px 15px rgba(0, 255, 255, 0.2);
box-shadow: 0 0 25px 7px rgb(127 255 255 / 80%), 0 0 64px 47px rgba(0, 255, 255, 0.5), 0px 0 30px 15px rgba(0, 255, 255, 0.2);
}

screenshots.gif


来一束追光效果吧


👉 追光就是通过一个边框线画出来的


<div class="light"></div>

.light {
width: 200px;
height: 0px;
border-bottom: 900px solid rgb(44 255 255 / 24%);
border-left: 50px solid transparent;
border-right: 50px solid transparent;
position: absolute;
left: 0px;
right: 0px;
top: 270px;
margin: 0px auto;
z-index: 1;
border-radius: 90px 90px 0px 0px;
}


  • 给边框的宽度和背景透明色就可以看出追光的效果了。


screenshots.gif


文字


👉 文字通过定位居中之后,刚好显示在灯光动画效果范围之内


<section class="error">
<div class="error__content">
<div class="error__message message">
<h1 class="message__title">掘金错误页面</h1>
<p class="message__text">不好意思,你访问的页面不存在,请关灯后重新尝试</p>
</div>
</div>
</section>

👉 文字颜色和背景色一致之后,通过灯光的透明度效果就可以实现文字显隐了。


.error {
min-height: 100vh;
position: relative;
padding: 240px 0;
box-sizing: border-box;
width: 100%;
height: 100%;
text-align: center;
margin-top: 70px;
}

.error__overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: hidden;
}

.error__content {
position: absolute;
top: 50%;
left: 50%;
width: 100%;
-webkit-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
}

.error__message {
text-align: center;
color: #181828;
}

.message__title {
font-family: 'Montserrat', sans-serif;
font-weight: 900;
text-transform: uppercase;
letter-spacing: 5px;
font-size: 5.6rem;
padding-bottom: 40px;
max-width: 960px;
margin: 0 auto;
}

.message__text {
font-family: 'Montserrat', sans-serif;
line-height: 42px;
font-size: 18px;
padding: 0 60px;
max-width: 680px;
margin: auto;
}

screenshots.gif


码上掘金查看效果



作者:蜡笔小心_
来源:juejin.cn/post/7150950812489875469
收起阅读 »

记录一次机器学习模型部署

web
简介:做了一个语音识别心脏病的机器学习模型,想要实现前后端简单的部署 用到的技术栈:Python、Flask、uni-app 前端 使用uni-app做一个小程序。需要具有语音采集、录音回放、录音上传等功能。使用uni.getRecorderManager()...
继续阅读 »

简介:做了一个语音识别心脏病的机器学习模型,想要实现前后端简单的部署


用到的技术栈:Python、Flask、uni-app


前端


使用uni-app做一个小程序。需要具有语音采集、录音回放、录音上传等功能。使用uni.getRecorderManager()实现对录音全局的控制。


屏幕截图 2023-04-04 155350.png


这里给出实现上传录音并接收请求结果的主要代码


upload() {
console.log(this.voicePath)
uni.uploadFile({
url: 'http://202.115.52.33:9500/process_data',
filePath: this.voicePath,
name: 'file',
fileType: "audio", //文件类型
success: (res) => {
console.log('success',res.data)
uni.showToast({
title: '上传成功',
icon: 'success',
});
if(res.data*1 < 0.35){
uni.showModal({
title: '检测结果',
content: '您患心脏病概率为:'+ Number(res.data*100).toFixed(2) + '%'+',心脏健康请继续保持',
success: function (res) {
if (res.confirm) {
console.log('用户点击确定');
} else if (res.cancel) {
console.log('用户点击取消');
}
}
});
}else{
uni.showModal({
title: '检测结果',
content: '您患心脏病概率为:'+ Number(res.data*100).toFixed(2) + '%'+',请及时到医院检查',
success: function (res) {
if (res.confirm) {
console.log('用户点击确定');
} else if (res.cancel) {
console.log('用户点击取消');
}
}
});
}

},
fail: (err) => {
console.log((err))
uni.showToast({
title: '上传失败',
icon: 'none',
});
},
});
}

这里有个坑需要注意,微信开发者工具模拟器录音上传到服务器,服务器无法正常使用录音(一直以为是前端上传语音的问题)。开发者工具录音文件为silk格式,说是silk其实是base64加密后的webm格式,不是普通的wav格式(貌似只能用chrome浏览器打开)。可以参考这篇文章微信小程序-录音文件无法播放问题 - 知乎 (zhihu.com),真机调试则不会出现这个问题。


后端


采用Flask来进行机器学习或者深度学习模型的部署。


# app.py
from flask import Flask, request
from predict import predict

app = Flask(__name__)

@app.route('/process_data', methods=['POST'])
def process_data():
    # 从前端接收音频文件
    fileStorage = request.files['file']  # 视频文件
    buffer_data = fileStorage.read()
    filename = request.files['file'].filename
    temp_path = 'upload/'+filename
    with open(temp_path, 'wb+') as f:
        f.write(buffer_data)  # 二进制转为音频文件
    # 模型推理
    predict_outcome = round(predict(temp_path), 4)
    # 返回结果
    return str(predict_outcome)


if __name__ == "__main__":
    app.run()

部署


使用宝塔面板实现Flask项目快速部署。



  1. 在宝塔面板中安装Python项目管理器软件


屏幕截图 2023-04-04 162932.png



  1. 上传Flask项目到服务器相应目录

  2. 在Python项目管理选择Flask框架,安装Flask项目中需要的第三方包
    这里有个需要注意的问题,我修改了第三方包的源码,下载的第三方包存放目录:上传项目文件夹/一串数字_venv/lib/python3.7/site-packages,在这里修改源码重启Python服务才能生效。

  3. Python项目管理器配置参考


bind = '0.0.0.0:5000'
user = 'scu611'
workers = 1
threads = 2
backlog = 512
daemon = True
chdir = '/www/server/phpmyadmin/heartbroken'
access_log_format = '%(t)s %(p)s %(h)s "%(r)s" %(s)s %(L)s %(b)s %(f)s" "%(a)s"'
loglevel = 'info'
worker_class = 'geventwebsocket.gunicorn.workers.GeventWebSocketWorker'
errorlog = chdir + '/logs/error.log'
accesslog = chdir + '/logs/access.log'
pidfile = chdir + '/logs/heartbroken.pid'

作者:用户7850680667062
来源:juejin.cn/post/7218098727608549432
收起阅读 »

GeoJSON:地理信息的JSON表示法

web
简介 GeoJSON 是一种使用 JSON 来编码各种地理数据结构的格式,是一种轻量级的数据交换格式,可以用来表示几何对象、属性数据、空间参考系统等信息 由两种对象组成:Geometry(几何对象)和 Feature(空间行状) 几何对象用来描述地理空间中的...
继续阅读 »

简介


GeoJSON 是一种使用 JSON 来编码各种地理数据结构的格式,是一种轻量级的数据交换格式,可以用来表示几何对象、属性数据、空间参考系统等信息


由两种对象组成:Geometry(几何对象)和 Feature(空间行状)



  • 几何对象用来描述地理空间中的点、线、面等几何形状

  • 空间行状用来描述一个有界的实体,包括几何对象和其他属性信息


几何对象类型有:



  • 点:Point

  • 多点:MultiPoint

  • 线:LineString

  • 多线:MultiLineString

  • 面:Polygon

  • 多面:MultiPolygon

  • 几何集合:GeometryCollection


空间行状类型有:



  • 空间行状:Feature

  • 空间形状集合:FeatureCollection


举例


几何对象和空间行状可以相互嵌套


const GeoJSON = {
type: "FeatureCollection",
features: [
{
type: "Feature",
geometry: { type: "Point", coordinates: [121.4737, 31.2304] },
properties: { id: 1 },
},
{
type: "Feature",
geometry: { type: "Point", coordinates: [121.4837, 31.2504] },
properties: { id: 2 },
},
],
};

空间行状


FeatureCollection


FeatureCollectionFeature 对象的集合,用来表示一组 Feature 对象


typefeatures 两个属性组成:



  • type 属性的值为 FeatureCollection

  • features 属性的值为 Feature 对象的数组


const FeatureCollectionJSON = {
type: "FeatureCollection",
features: [feature],
};

Feature


Feature 对象用来表示几何对象的属性信息


typegeometryproperties 三个属性组成:



  • type 属性的值为 Feature

  • geometry 属性的值为 Geometry 几何对象

  • properties 属性的值为属性对象(可选)


const FeatureJSON = {
type: "Feature",
geometry: { type: "Point", coordinates: [121.4737, 31.2304] },
properties: { id: 1 },
};

几何对象


Point


Point 用来表示一个点


typecoordinates 两个属性组成:



  • type 属性的值为 Point

  • coordinates 属性的值为一个数组,数组的第一个元素为经度,第二个元素为纬度


const PointJSON = {
type: "Point",
coordinates: [121.4737, 31.2304],
};

MultiPoint


MultiPoint 用来表示多个点


typecoordinates 两个属性组成:



  • type 属性的值为 MultiPoint

  • coordinates 属性的值为一个数组,数组的每个元素都是一个点的坐标


const MultiPointJSON = {
type: "MultiPoint",
coordinates: [
[121.4737, 31.2304],
[121.4837, 31.2504],
],
};

LineString


LineString 用来表示一条线


typecoordinates 两个属性组成:



  • type 属性的值为 LineString

  • coordinates 属性的值为一个数组,数组的每个元素都是一个点的坐标


const LineStringJSON = {
type: "LineString",
coordinates: [
[121.4737, 31.2304],
[121.4837, 31.2504],
],
};

MultiLineString


MultiLineString 用来表示多条线


typecoordinates 两个属性组成:



  • type 属性的值为 MultiLineString

  • coordinates 属性的值为一个数组,数组的每个元素都是一个线的坐标数组


const MultiLineStringJSON = {
type: "MultiLineString",
coordinates: [
[
[121.4737, 31.2304],
[121.4837, 31.2504],
],
[
[121.4727, 31.2314],
[121.4827, 31.2514],
],
],
};

Polygon


Polygon 用来表示一个面


typecoordinates 两个属性组成:



  • type 属性的值为 Polygon

  • coordinates 属性的值为一个数组,数组的第一个元素为外环的坐标数组,后面的元素为内环的坐标数组


polygon 的坐标数组的第一个元素和最后一个元素是相同的,表示闭合


const PolygonJSON = {
type: "Polygon",
coordinates: [
[
[121.4737, 31.2304],
[121.4837, 31.2504],
[121.4937, 31.2304],
[121.4737, 31.2304],
],
[
[121.4717, 31.2314],
[121.4827, 31.2524],
[121.4937, 31.2334],
[121.4757, 31.2344],
],
],
};

MultiPolygon


MultiPolygon 用来表示多个面


typecoordinates 两个属性组成:



  • type 属性的值为 MultiPolygon

  • coordinates 属性的值为一个数组,数组的每个元素都是一个面的坐标数组


const MultiPolygonJSON = {
type: "MultiPolygon",
coordinates: [
[
[
[121.4737, 31.2304],
[121.4837, 31.2504],
[121.4937, 31.2304],
[121.4737, 31.2304],
],
[
[121.4737, 31.2304],
[121.4837, 31.2504],
[121.4937, 31.2304],
[121.4737, 31.2304],
],
],
[
[
[121.4737, 31.2304],
[121.4837, 31.2504],
[121.4937, 31.2304],
[121.4737, 31.2304],
],
[
[121.4737, 31.2304],
[121.4837, 31.2504],
[121.4937, 31.2304],
[121.4737, 31.2304],
],
],
],
};

GeometryCollection


GeometryCollection 用来表示几何对象的集合


typegeometries 两个属性组成:



  • type 属性的值为 GeometryCollection

  • geometries 属性的值为几何对象的数组


const GeometryCollectionJSON = {
type: "GeometryCollection",
geometries: [
{ type: "Point", coordinates: [121.4737, 31.2304] },
{
type: "LineString",
coordinates: [
[121.4737, 31.2304],
[121.4837, 31.2504],
],
},
],
};

可选属性


这些属性都是 GeoJSON 的扩展属性,不是 GeoJSON 规范的一部分



  • id 属性,用来描述 FeatureCollection 的唯一标识

  • bbox 属性,用来描述 FeatureCollection 的边界框

    • 四至坐标,一般用来做数据裁剪

    • 这是一组左上角和右下角的坐标,示例:[minLon, minLat, maxLon, maxLat]



  • properties 属性,用来描述 FeatureCollection 的属性

  • crs 属性,用来描述坐标参考系


其他


coordinate


coordinate 是一个数组,表示一个点的坐标,数组的长度表示坐标的维度,一般是 2 维或 3



  • 2 维:[lon, lat]

  • 3 维:[lon, lat, height]


coordinate 的第一个元素表示经度,第二个元素表示纬度,第三个元素表示高度


坐标顺序是 [lon, lat],这个是推荐顺序,可由 crs 属性指定


coordinates 是多维数组:



  • 点:[lon, lat]

  • 线:[[lon, lat], [lon, lat]]

  • 面:[[[lon, lat], [lon, lat]]]

  • 多面:[[[[lon, lat], [lon, lat]]]]


坐标参考系


最常使用的坐标系是 EPSG:4326EPSG:3857



  • EPSG:4326WGS84(CGCS2000,大地) 坐标系,是 GeoJSON 规范的默认坐标系

  • EPSG:3857Web Mercator(墨卡托) 坐标系,是 OpenLayers 的默认坐标系


它们的区别:



  • EPSG:4326 是经纬度坐标系,EPSG:3857 是投影坐标系

  • EPSG:4326 的坐标范围是 [-180, -90, 180, 90]EPSG:3857 的坐标范围是 [-20037508.342789244, -20037508.342789244, 20037508.342789244, 20037508.342789244]

  • EPSG:4326 的坐标单位是度,EPSG:3857 的坐标单位是米

  • EPSG:4326 的坐标原点是 [0, 0]EPSG:3857 的坐标原点是 [-20037508.342789244, -20037508.342789244]

  • EPSG:4326 的坐标轴方向是 [x, y]EPSG:3857 的坐标轴方向是 [x, -y]


在 ts 中使用


为了在 ts 使用 GeoJSON 能够有类型约束,我整理整理了一些 GeoJSONts 类型定义和创建 GeoJSON 的方法:



举例:




  1. 表示一个点和多个点的 GeoJSON 集合:


    使用geojson.d.ts


    type PointType = FeatureCollection<Point | MultiPoint, GeoJsonProperties<T>>;

    const point2Geojson: PointType<{ id: string; name?: string }> = {
    type: "FeatureCollection",
    features: [
    {
    type: "Feature",
    geometry: {
    type: "Point",
    coordinates: [120.4737, 31.2304],
    },
    properties: { id: "12", name: "uccs" },
    },
    {
    type: "Feature",
    geometry: {
    type: "MultiPoint",
    coordinates: [
    [121.4737, 31.2304],
    [111.4737, 31.2204],
    ],
    },
    properties: { id: "1" },
    },
    ],
    };



  2. 创建一个几何对象


    使用geojson.helper.ts


    const pointGeometry = point<{ id: string }>([120.4737, 31.2304], {
    id: "1",
    });
    const featureGeoJSON = feature<Point>(pointGeometry);



参考


收起阅读 »

css是你永远学不会的语言

web
在网上冲浪的时候,看到有这么一个网页效果;如下图: 分析: 从图中我们可以看出,当我们鼠标移入的时候,下划线冲左侧开始展示;当鼠标移出的时候,下划线从左侧开始消失 既然我们知道了大致的效果,那我们就得想想怎么实现这个效果了。 我是没有想出来的,我用F12查看...
继续阅读 »

在网上冲浪的时候,看到有这么一个网页效果;如下图:


20230330_10:43:33_1.gif


分析:


从图中我们可以看出,当我们鼠标移入的时候,下划线冲左侧开始展示;当鼠标移出的时候,下划线从左侧开始消失


既然我们知道了大致的效果,那我们就得想想怎么实现这个效果了。


我是没有想出来的,我用F12查看了一下;如下图代码:


未移入样式:


微信图片_20230330105126.png


移入样式(hover):


微信图片_20230330105126.png


代码分析



  • 背景色渐变以及方向(移入移出)

  • 背景大小

  • 过度时间


示列代码


解释:



  • 我们在css中写span的样式时;background需要禁止平铺,然后是靠右(right)并且是底部的;原因是以为收回去的时候需要方向是右侧(right);然后background-size需要将宽度设置为0 高度为2(你可以根据自己的需要设置);最后是给background-size一个过度效果既可以

  • 然后hover事件的时候需要将定位给到左侧(left)并且将background-size宽度百分之百;这样就会根据过度时间显示完成


<!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>
<style>
.title {
color: #333;
line-height: 2px;
}

.title span {
background: linear-gradient(to right, #ec695c,#61c454) no-repeat right bottom;
background-size: 0 2px;
transition: background-size 1300ms;
}
.title span:hover {
background-position-x: left;
background-size: 100% 2px;
}
</style>
</head>
<body>
<h2 class="title">
<span>
Migrating From MySQL to YugabyteDB Using YugabyteDB Voyager</span>
</h2>
</body>
</html>

以上就是今天的全部内容了!大家可以复制以上代码,即可展现效果。


当我做了以后发现,css真的是我的弱势;或者说大部分人都不怎么关注css;毕竟面试的时候大部分公司都不是很要求这个,从而我们就忽略了个语言;css真的我永远学不会的语言啊


后面我也就开一个专辑,我所遇到的css相关的一些东西


往期文章



作者:雾恋
来源:juejin.cn/post/7216163778059550757
收起阅读 »

纯前端就可以实现的两个需求

web
一:多文件下载并压缩成zip形式   我们一般看到这个需求,大多前端都会觉得这不就是一个接口的事嘛,咱们直接window.open()就搞定了,emm...,我之前也是这样想的,可惜现在是在三线城市的小互联网公司,人家后端说服务器不行,带宽不够,出不来接口0....
继续阅读 »

一:多文件下载并压缩成zip形式


  我们一般看到这个需求,大多前端都会觉得这不就是一个接口的事嘛,咱们直接window.open()就搞定了,emm...,我之前也是这样想的,可惜现在是在三线城市的小互联网公司,人家后端说服务器不行,带宽不够,出不来接口0.0,所以我就尝试着寻找……最终找到了解决办法——


  前端可以直接从cos资源服务器中下载多个文件,并放进压缩包中哦,这样就省去了后端在中间中转的那个环节,实现方式如下:


1.安装依赖


  我们需要用到两个第三方依赖哦,分别执行以下安装


npm i jszip
npm i file-saver

2.引入 


  在需要使用的时候,我们引入


import JSZip from "jszip";
import FileSaver from "file-saver";

3.实现过程


  我这里是在vue框架中,所以方法是在methods中哦,先在data里声明一个文件demo数据


data () {
return {
fileList: [ //这里的数据 实际中 应该是从后端接口中get { name: "test1.doc", url: "https://collaboration-pat-1253575580.cos.ap-shenzhen-fsi.myqcloud.com/baseFile/202303/bdf4303f-4e8d-4d95-977c-4fd5ef28d354/new.doc" }, { name: "test2.doc", url:"https://collaboration-pat-1253575580.cos.ap-shenzhen-fsi.myqcloud.com/baseFile/202303/bdf4303f-4e8d-4d95-977c-4fd5ef28d354/new.doc" } ],
}
}

methods中的方法:


handleClickDownAll () { //实现多文件压缩包下载
let _this = this
let zip = new JSZip()
let cache = {}
let promises = []
for(let item of this.fileList) {
const promise = _this.getFileArrBuffer(item.url).then(res => {
//下载文件, 并存成ArrayBuffer对象(blob)
zip.file(item.name,res,{binary:true}) //逐个添加文件
cache[item.name] = data
})
promises.push(promise) }
Promese.all(promises).then(() => {
zip.generateAsync({type:'blob'}).then(content => {
FileSaver.saveAs(content,"压缩包名字") //利用file-saver保存文件 自定义文件名
})
})
},
getFileArrBuffer(url) {
return new Promise((resolve) => {
let xmlhttp = new XMLHttpRequest()
xmlhttp.open('GET',url,true)
xmlhttp.responseType = 'blob'
xml.onload = function () {
resolve(this.response)
}
xmlhttp.send()
})
}

二:electron-vue中,生成二维码,并支持复制二维码图片


要实现的功能如下,就是点击这个“复制二维码”,可以直接把二维码图片复制下来



1.安装依赖


npm i qrcodejs2

2.引入


import QRCode from 'qrcodejs2';
import { clipboard, nativeImage} = require('electron')

3.实现


  要先在template中写一个这样的元素,用来显示二维码图片框


<div id="qrcodeImg" class="qrcode" style="height: 120px"></div>

然后再写一个画二维码的方法,如下:


drawQrcode() {
new QRCode("qrcodeImg",{
width:120,
height:120,
text:"http://www.baidu.com",
colorDark:"#000",
colorLight:"#fff"
})
}

复制二维码的方法如下:


copyCode() {
let src = document.getElementById("qrcodeImg").getElementsByTagName("img")[0].src
const image = nativeImage.createFromDataURL(src)
clipboard.writeImage(image)
this.$Message.success('复制成功')
}

4.使用


要先确保dom元素已经有了,所以在mounted中调用drawQrcode()这个方法,然后点击“复制二维码”时,调用 copyCode()这个方法就可以实现啦




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

再也不用手动改package.json的版本号

web
本文的起因是有在代码仓库发包后,同事问我“为什么package.json 里的版本还是原来的,有没有更新?”,这个时候我意识到,我们完全没有必要在每次发布的时候还特意去关注这个仓库的版本号,只要在发布打tag的时候同步一下即可,于是有了本文的实践。 node....
继续阅读 »

本文的起因是有在代码仓库发包后,同事问我“为什么package.json 里的版本还是原来的,有没有更新?”,这个时候我意识到,我们完全没有必要在每次发布的时候还特意去关注这个仓库的版本号,只要在发布打tag的时候同步一下即可,于是有了本文的实践。


node.js 部分,我们得有一个更改仓库代码的脚本留给ci执行


我们首先需要在工程目录中的 ./script/..目录下增加一个 update-version.js脚本



//update-version.js

const path = require('path');
const fs = require('fs');
const newVersion = process.argv[2].replace(/^v/, '');; // 获取命令行参数中的新版本号,并过滤v字头

if (!newVersion) {
console.log('请传入新版本号,版本号遵循semver规范 .eg: 1.0.0, 1.0.1, 1.1.0');
process.exit(1);

}

// 获取当前命令行上下文路径

const currentDirectory = process.cwd();

// 获取 package.json 文件中的版本号
const packageJsonPath = path.join(currentDirectory, 'package.json');
const packageJsonContent = fs.readFileSync(packageJsonPath, 'utf8');
const packageJson = JSON.parse(packageJsonContent);
const currentVersion = packageJson.version;

// 更新 package.json 文件中的版本号

packageJson.version = newVersion;
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
console.log(`版本号已从 ${currentVersion} 更新为 ${newVersion}`);


接下来在 package.json script 配置后可以直接使用 npm run version <version> 中触发变更版本号脚本。当然这个前提是想要让这个脚本保留给开发者命令行使用。



{

"name": "version workflow",
"version": "1.0.0",
"description": "version update demo",
"main": "index.js",
"scripts": {
//...
"version": "node ./scripts/update-version.js"
},
//...

}


CI :如何让发布包的行为直接和代码仓库中的版本号同步?


接下来算重头戏,如何让发布包的行为直接和代码仓库中的版本号同步?这里我们使用的是github 提供的github action,具体操作和语法可以查看一下官方文档,本文就不过多展开。


我们需要在仓库根目录增加如下路径的文件 .github/workflows/update-action.yml



name: Update Package Version

on:
release:
types: [released]

jobs:
update:
runs-on: ubuntu-latest
steps:

- name: Checkout code
uses: actions/checkout@v3

- name: Update package.json
run: |
node ./scripts/update-version.js ${{ github.event.release.tag_name }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Commit changes
run: |
git config user.name "Your github name"
git config user.email "your github email"
git add .
git commit -m "Update version to ${{ github.event.release.tag_name }} for release ${{ github.ref }}"

- name: Push changes
uses: ad-m/github-push-action@master
with:
github_token: ${{ secrets.GITHUB_TOKEN }}


我们在 release hook 中的 released 状态下增加了一个 update job。 它会做下面几件事情(在脚本步骤中有)



  1. 【Checkout code】 切出新的代码分支;

  2. 【 Update package.json】在新分支执行 update-version.js 传入tag_name更新我们的工程版本号;

  3. 【Commit changes】以你定制的 git config user 信息创建一个新提交;

  4. 【Push changes】推送变更回到主干;


ps:正确来说应该在发布执行动作前prereleased执行我们的 job 但是没用这个的原因如下:



Note:  The prereleased type will not trigger for pre-releases published from draft releases, but the published type will trigger. If you want a workflow to run when stable and pre-releases publish, subscribe to published instead of released and prereleased.



当这个脚本推送后,执行发布后自动更新版本,不用在关注这个版本修改问题。
你会得到下面的效果。


在你的仓库发布界面填写正确tag后发布
image.png


触发update job 更改完成
image.png


你可能遇到最多的坑



  1. action 执行失败



Process completed with exit code 129." Node.js 12 actions are deprecated. Please update the following actions to use Node.js 16: actions/checkout@v2. For more information, see https://github.blog/changelog/2022-09-22-github-actions-all-actions-will-begin-running-on-node16-instead-of-node12/.



这是由于默认action job 执行环境的nodejs 版本与actions 包中执行脚本不匹配导致,所以一定要使用 checkout@v3 版本 actions/checkout@v3



  1. 各种不熟悉 action 语法取值导致的问题


可以优化的地方


我们前面提交的这个流程发布还是有个问题,你永远有个更超前的 commit hash 在你发布的 tag 之后


image.png
所以这个action 还有需要继续优化的地方,那就是同步更新tag hash


name: Update Package Version

on:
release:
types: [released]

jobs:
update:
runs-on: ubuntu-latest
steps:

- name: Checkout code
uses: actions/checkout@v3

- name: Update package.json
run: |
node ./scripts/update-version.js ${{ github.event.release.tag_name }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Commit changes
run: |
git config user.name "Your github name"
git config user.email "your github email"
git add .
git commit -m "Update version to ${{ github.event.release.tag_name }} for release ${{ github.ref }}"
git_hash=$(git rev-parse --short HEAD)

- name: Push changes
uses: ad-m/github-push-action@master
with:
github_token: ${{ secrets.GITHUB_TOKEN }}

- name: Tag Push changes
run: |
git tag -f ${{ github.event.release.tag_name }} $git_hash
git push --force origin ${{ github.event.release.tag_name }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}


这里相比之前的版本增加了
Tag Push changes 这个步骤,在最后获取这个版本更新产生的 $git_hash强制更新到发布的 tag 上。


我们看看效果
image.png


最后我们看版本发布管理中的 tag hash
image.png
搞定!


可以再优化的地方


现在我们还有个问题,就是在执行 Commit changes 这个步骤时每次 git config user.name "Your github name" git config user.email "your github email" 这里是写死的,我们可以根据 GitHub Actions 中有一些预设的环境变量可以读取到当前用户的账号和邮箱信息。通过 ${{ env.GITHUB_ACTOR }} 获取到当前执行的 Actions 的用户账号,通过 ${{ env.GITHUB_ACTOR }}@users.noreply.github.com 获取到当前执行的 Actions 的用户邮箱(该邮箱为 noreply 邮箱,用于 GitHub 的通知,无法发送邮件)。注意,该邮箱不一定是用户本身的真实邮箱,可能是 GitHub 默认的邮箱。



如果需要获取当前 GitHub 账号的真实邮箱地址,可以通过 GitHub REST API 进行查询,具体可以参考官方文档:



这样我们就需要在Commit Changes之前再加一个Set Git user步骤


- name: Set Git user
env:
GITHUB_ACTOR: ${{ github.actor }}
GITHUB_EMAIL: ${{ github.actor }}@users.noreply.github.com
run: |
git config --global user.name "${{ env.GITHUB_ACTOR }}"
git config --global user.email "${{ env.GITHUB_EMAIL }}"

这样我们最终的 Github action 脚本长这样



name: Update Package Version

on:
release:
types: [released]

jobs:
update:
runs-on: ubuntu-latest
steps:

- name: Checkout code
uses: actions/checkout@v3

- name: Update package.json
run: |
node ./scripts/update-version.js ${{ github.event.release.tag_name }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Set Git user
env:
GITHUB_ACTOR: ${{ github.actor }}
GITHUB_EMAIL: ${{ github.actor }}@users.noreply.github.com
run: |
git config --global user.name "${{ env.GITHUB_ACTOR }}"
git config --global user.email "${{ env.GITHUB_EMAIL }}"

- name: Commit changes
run: |
git add .
git commit -m "Update version to ${{ github.event.release.tag_name }} for release ${{ github.ref }}"
git_hash=$(git rev-parse --short HEAD)

- name: Push changes
uses: ad-m/github-push-action@master
with:
github_token: ${{ secrets.GITHUB_TOKEN }}

- name: Tag Push changes
run: |
git tag -f ${{ github.event.release.tag_name }} $git_hash
git push --force origin ${{ github.event.release.tag_name }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}


最后


如果我的文章对你有帮助欢迎点赞+收藏支持


作者:Jervis_cen
来源:juejin.cn/post/7220164534316433467
收起阅读 »

nginx带宽限制 limit_rate limit_rate_after

web
知识梳理 在高负载的网络环境下,为了保持服务的稳定性,限速 (download rate) 是一种必要的控制访问量的手段。Nginx 是一款高性能的 Web 服务器和反向代理服务器,可以使用 limit_rate_after 和 limit_rate 两个主要...
继续阅读 »

知识梳理


在高负载的网络环境下,为了保持服务的稳定性,限速 (download rate) 是一种必要的控制访问量的手段。Nginx 是一款高性能的 Web 服务器和反向代理服务器,可以使用 limit_rate_after 和 limit_rate 两个主要指令来完成流量控制和限速。


limit_rate_after 指令


指令 limit_rate_after 会在客户端成功建立连接之后,指定的大小后开始限制发送速度。这个指令的含义就是在连接建立后的 limit_rate_after 大小之后,数据发送速率将被限制。


以下是limit_rate_after 的语法和示例:


Syntax:	limit_rate_after size;
Default:
limit_rate_after 0;
Context: http, server, location, if in location

limit_rate_after 50m;

这个指令可以帮助您限制连接的初始流量,以便于服务器的带宽资源分配更为合理。


limit_rate 指令


limit_rate 指令是用来控制发送至客户端的数据传输速度的,它可以限制整个连接的流量,也可以限制单个客户端访问速度。


以下是 limit_rate 的语法和示例:


syntax:		limit_rate rate;
default: —
context: http, server, location

limit_rate 1k;

这个配置的作用是:在与客户端建立连接之后的 10 秒内,限制每秒发送的数据量不超过 50kB;之后如果连接仍然打开,则限制与该客户端的速率为 50kB/s。


需要提醒的一点是,尽管 limit_rate 可以一定程度上保护服务器资源,但是并不足以完全阻止恶意饱和攻击。因此,在考虑流量控制和限速的同时,还应该结合其他安全和防护机制来更好地保护服务器。


实验


配置传输速度为 1k



  • nginx配置


location / {
limit_rate 1k;
root html;
}



配置下载50m后开始限制传输速度



  • nginx配置


location / {
limit_rate_after 50m;
limit_rate 1k;
root html;
}


可以看到开始下载速度很快


在这里插入图片描述
在下载50m后,速度限制在1k以内
在这里插入图片描述


我遇到的坑



因为我的portal.tar文件没有读的权限,导致浏览器下载报403,使用 chmod 755 portal.tar 修改portal.tar文件的权限,如下图:


在这里插入图片描述


总结


Nginx 的限速功能对于控制访问量、防止恶意攻击具有很高的研究价值和实际意义。limit_rate 和 limit_rate_after 是 Nginx 常见的两个限速指令,它们可以配置在 http、server、location 等区块中,实现不同级别的流量限制和控制。一般情况下为了取得更好的限速效果,我们会同时使用两个指令,通过多事件流的限速进行灵活的控制。


希望这篇文章能够对 Nginx 限速功能有更深入的理解,帮助开发者在实际的生产环境中使用它来进行更好的流量控制和管理。


参考


nginx.org/en/docs/htt…
nginx.org/en/docs/htt…


作者:黄娟
来源:juejin.cn/post/7219889814115811388
收起阅读 »

藏在微信里的温度,无障碍开发框架分享

web
👉 腾小云导读 现我国现有4471w视障/听障人士,60岁及以上人群达2.6亿规模。微信作为国民级应用,实现无障碍迫在眉睫。为了帮助他们更好地使用微信 App,Android微信完成了适老化及无障碍改造。本文主要介绍Android 微信开发团队根据适老化及无障...
继续阅读 »

图片


图片


👉 腾小云导读


现我国现有4471w视障/听障人士,60岁及以上人群达2.6亿规模。微信作为国民级应用,实现无障碍迫在眉睫。为了帮助他们更好地使用微信 App,Android微信完成了适老化及无障碍改造。本文主要介绍Android 微信开发团队根据适老化及无障碍需求,完成的一个协助业务侧进行无障碍功能开发的框架。希望能给广大开发爱好者带来帮助和启发!




👉 看目录,点收藏


1 无障碍需求框架背景


1.1 无障碍需求


1.2 框架简介


2 无障碍开发基础知识


2.1 读屏软件识别View原理


2.2 读屏软件后的事件分发原理


3 框架实现的整体流程和执行原理


3.1 整体流程


3.2 执行原理


4 核心说明:全局热区补足机制


4.1 背景说明


4.2 具体实现


4.3 额外说明


5 走查工具


6 总结


01、无障碍需求框架


目前,业界已经有共识性的无障碍开发守则。例如 Web Content Accessibility Guidelines (WCAG) 2.0,它是由互联网的主要国际标准组织万维网联盟 (W3C) 的Web可访问性倡议 (WAI) 发布的一系列 Web 可访问性指南的一部分。


此外,WAI-ARIA(可访问的富Internet应用程序套件)是由万维网联盟(W3C)发布的一项关于 A11 Y技术应用规范。该规范定义了一种使残障人士更易于访问 Web 内容和 Web 应用程序的方法,增加 HTML、JavaScript 和相关技术开发的网站动态内容以及用户界面组件的可访问性。


目前,Android没有官方统一、方便的框架,官方提供的原生api并不是特别好用,所以微信团队对其进行参考,开发了一个无障碍框架,基于原生的api进行了再封装,将繁琐的无障碍适配逻辑封装在底层,以声明式接口的形式,让上层业务能以更简便更解耦的代码,完成无障碍的适配。接下来我们进行分享:


1.1无障碍需求


本框架主要具备以下特性:



  • 可感知性 :包括大字体适配,颜色对比度等 。

  • 可操作性 :主要是过小热区的放大,提高老年人/残疾人的交互体验 。

  • 可理解性 :微信应提供读屏文案等信息,帮助盲人在开启 Talkback 等读屏软件的情况下,正常使用微信。


下面给出一些较为典型的需求:



  • 需求1:过小热区的放大


需求是要求微信内的所有可交互控件,可点击范围不得低于 44dp * 44dp。


大小不合规的控件,如果一个个进行排查、布局修改。工程量庞大。



  • 需求2:响应区域会随无障碍开关发生变化


图片


该 Item 由一个 SwitchButton + TextView 组成。


开启 Talkback 时,整个 Item 识别为一个焦点,选中双击是触发点击 switch的逻辑。在无障碍模式下,选中双击是直接触发相应控件的 Click 事件。但是在不开 Talkback 的情况下点击 Item 又无需响应,只响应 SwitchButton 。也就是点击区域会随 Talkback 开关发生变化。


实现可能是:在 ItemClick 中进行 if 判断。但这样写侵入性高,难维护。



  • 需求3:读屏文案由其他的控件的值组合


图片


选中头像,读屏文案:腾讯行政的头像,有 2 条未读消息。需要读出列表中其他关联内容,这种只能把适配代码侵入到 Adapter中。


1.2 框架简介


框架将不同的无障碍需求的实现进行封装,抽象成不同的规则。


业务侧可以将一个页面/业务的无障碍需求,在一个配置类里使用规则表达出来,再由框架进行处理。实现相应的效果。


class ChatAccessibility(activity: AppCompatActivity) :  
BaseAccessibilityConfig(activity) {
  override fun initConfig() {
        // 设置 contentDesc
   view(rootId,viewId).desc(R.string.send_smiley)
        // ...
  }
}

框架基类 BaseAccessibilityConfig 提供了一系列用于表达规则的 api,包括但不限于如下功能:




  • 通过配置统一设置 contentDescription




  • 支持把多个 View 组合成一体进行读屏




  • 通过配置禁用某个View被 Talkback 聚焦的能力




  • 支持按指定顺序进行读屏,支持局部控制 Talkback 聚焦顺序




  • 支持设定在 Activity 启动后的第一个读屏控件




  • 支持对某个父 View 的 disableChildren 功能




  • 在某个 View 满足条件时,对其进行读屏,但不聚焦




  • 在某个 View 满足条件时,读出提前设定的 string,但不聚焦




  • 全局热区宽高补齐至 44dp,并提供自定义热区放大/禁用热区放大的功能 ...




02、无障碍开发基础知识


在深入了解框架的设计前,先来介绍一些无障碍功能开发的基础知识。


2.1 基础知识1:读屏软件识别 View 原理


图片


读屏软件无法直接识别到View,只能识别到View提供的虚拟节点「Node」,View 和虚拟节点一般是一一对应的。当页面内容发生变化,比如 View 被设值,或者发生滚动等情况,View 会向无障碍系统发送一个事件,通知系统。


然后系统就回头向 View 索取节点,组成页面更新后新的节点树,而 「节点树 和 ViewTree 是一一对应的」。此时读屏软件拿到的就是新的内容了。


2.2 基础知识2:读屏软件后的事件分发流程


分为上下两部分:读屏软件拦截处理行为、读屏软件接受事件。


图片


流程如下:




  • 读屏软件拦截用户 Touch 事件,根据事件的坐标去定位到目标节点。




  • 将 Touch 事件解释为节点行为,这里以触摸选中为例,那么就是聚焦行为。




  • 读屏软件通过该节点向无障碍系统发送,无障碍系统又转发给View(聚焦产生的绿框就是在View的内部处理里去绘制的)。




  • 生成新的虚拟节点并提供给读屏软件后,读屏软件组合信息,通过 TTS 语音引擎的 api 读出。




读屏软件展示给用户的所有信息,全部来自虚拟节点。可以在节点生成的过程中,修改节点的信息,所以这里是一个绝佳的**「信息自定义」**的地方。


采用将所有的 View 都 「Wrap 一层 AccessibilityDelegate」 的方式,「在 onInitializeAccessibilityNodeInfo 方法中修改节点信息」。


03、框架实现整体流程与执行原理


3.1 整体流程


图片




  1. 业务侧实现规则配置类,编写的规则会进入配置池。




  2. 框架在View生成节点给系统的时候进行拦截 「(onInitializeAccessibilityNodeInfo)」




  3. 在配置池中寻找匹配的规则。




  4. 根据匹配的规则对节点进行修改。




  5. 最后生成的节点就会由系统交由给读屏软件进行读屏。




3.2 执行原理


图片


核心原理:采用基于责任链的流水线来处理。整体流程主要分为两部分:




  • View 预处理责任链(图示左边):执行预出来操作,如异步生成缓存、View标记等;




  • 节点处理责任链(图示右边):节点处理的同时会同步查找规则进行设置。




接下来主要简单介绍下框架的一个核心功能的实现:「全局热区补足机制」 (位于框架流程中的预处理责任链中的一环)。


04、核心说明:全局热区补足机制


4.1 背景说明



  • 需求说明


过小热区放大,即微信内的所有可交互控件可点击范围不得低于 44dp * 44dp,像一些大小不合规的控件,如果一个个进行排查、布局修改,工程量太庞大。还有热区其他一些需求 etc。



  • 问题难点


一般会选择直接修改 padding,有些甚至需要改动相应布局,但这样的改动工作量太大且容易影响原来视图布局。



  • 解决方案


需要一个全局的热区补足机制,将过小热区补足至规范。


4.2 具体实现


「创建 View 的统一入口」 去设置 TouchDelegate 代理,由父 View 作为TouchDelegate 的承载 View 去代理 Touch 事件,这里有几个问题需要解决:




  • 如何找到合适的承载View




  • 热区及时更新




  • 性能优化




  • 读屏模式下的热区扩大




下面我们分别展开讲。




  • 重点问题1:如何找到合适的承载 View




从目标 View 向上冒泡,找到一个合适的父 View。那么需要 「冒泡终止条件」。 首先条件一肯定是 「足够大」。当前 View 够大了就没必要再往上冒了。


图片


但是这样会存在问题:子 View 的 Click优先级高于父View的TouchDelegate。事件派发机制:


从父 View 往子 View 派发,从子 View 向上处理。View 的事件处理顺序是先 OnTouchListener,然后是 TouchDelegate,再是Click、LongClick。


所以会导致下图的情况:


图片


目前进行了折中处理,相比上图,显然是下图的放大后的体验更佳:


图片


同时加入了条件二:「该承载 View 是 Clickable、LongClickable」。最终方案流程确定如下:


图片




  • 重点问题2:热区及时更新




背景: 承载 View 的 TouchDelegate 需要的参数包含一个 Rect,也就是对扩大的热区进行响应。


问题: 这个矩阵是提前传入,且和 小 View 没有直接的关系。如果小 View 的布局发生变动,会导致扩大后热区没有及时跟上变化。导致热区错位。


解决方案: 在 小 View 的 onLayoutChange 中重新进行一遍 ·View 扩大方案· 的处理。同时为了防止 onLayoutChange  执行过于频繁,将 onLayoutChange 包装成 View 的一个事件。如果短时间内多次 onLayoutChange  ,则只在最后一次 onLayoutChange 的时候进行  「View扩大方案」处理。



  • 重点问题3:性能优化


背景 :最初的 View 扩大方案执行时机是在创建 View 的统一入口,也就是在 LayoutInflate 的 onCreateView 中同步执行,每个 View 都得执行。


问题:由于 View 数量较为庞大,所以存在较大的性能隐患。


解决方案:采用了异步方案并同时对 View 处理任务进行收拢。将执行时机提前到 LayoutInflate.inflate 并异步处理,在异步任务中去遍历该 inflate 的根 View的所有子 View。尽量不去阻塞主线程的运行。




  • 重点问题4:读屏模式下的热区扩大




通过上面的实现,点击热区确实是扩大了。但是在读屏模式下选中的时候,选中的框并没有扩大。那么首先需要知道,选中时的框是以什么作为 Bound。


绿框的绘制核心逻辑位于 ViewRootImpl 中的一个 drawAccessibilityFocusedDrawableIfNeeded(),该方法的调用时机是用户触摸选中某个View后,传递到 ViewRootImpl 时进行调用,也就是读屏选中的绿框是由系统绘制的,而不是由读屏软件绘制的。从源码中能够得知的是,绿框的Bound 根据是否有虚拟节点,分为两种情况:


private void drawAccessibilityFocusedDrawableIfNeeded(Canvas canvas) {  
    final Rect bounds = mAttachInfo.mTmpInvalRect;
    if (getAccessibilityFocusedRect(bounds)) {
        final Drawable drawable = getAccessibilityFocusedDrawable();
        if (drawable != null) {
            drawable.setBounds(bounds);
            drawable.draw(canvas);
        }
    } else if (mAttachInfo.mAccessibilityFocusDrawable != null) {
        mAttachInfo.mAccessibilityFocusDrawable.setBounds(0000);
    }
}

private boolean getAccessibilityFocusedRect(Rect bounds) {
    ...
    final AccessibilityNodeProvider provider = host.getAccessibilityNodeProvider();
    if (provider == null) {
        host.getBoundsOnScreen(bounds, true);
    } else if (mAccessibilityFocusedVirtualView != null) {
        mAccessibilityFocusedVirtualView.getBoundsInScreen(bounds);
    } else {
        return false;
    }
  ...
    return !bounds.isEmpty();
}

经过跟踪源码发现,这是因为 「绿框的绘制」 是根据 View.getBoundInScreen 获取的矩阵来做到的。而 TouchDelegate 的设置无法改变 View.getBoundInScreen 获取到的矩阵。在使用虚拟节点的情况下,才会使用虚拟节点的Bound进行绘制。


对于这个问题,我们的解决思路是:




  • 对每个 View 设置自定义的 AccessibilityDelegate, 并实现其中的 getAccessibilityNodeProvider 方法。




  • 如果判断 View 需要扩大,在 getAccessibilityNodeProvider 中返回自定义的 Provider。




  • 在自定义的 Provider 中,计算 View 的扩大后的矩阵在屏幕上的位置。




  • 将矩阵设置给虚拟节点,并返回给系统。




4.3 额外说明



  • 如何匹配规则与View?


框架将配置池按 Activity 划分,极大减少冲突概率,同时减少配置池大小,加快查找规则的速度,提供 layoutId + viewId,rootId + viewId 两种形式的 View 定位机制。由两个 Id 确定一个 View,减少冲突。



  • 查找规则时间长可能导致的主线程卡顿?


由于查找规则的时机是在生成节点,是由系统触发且无法异步。在查找规则的过程中,使用预处理的时候提前生成的缓存进行查找,尽可能减少耗时。


05、走查工具


5.1 背景


当完成无障碍需求的开发后,需进行验证。在验证过程中发现开启验证效率低下,需开启读屏软件后,逐个元素验证。


5.1.1 解决方案与原理


基于无障碍服务(AccessibilityService)开发、集成了在不开启 Talkback 的情况下能展现读屏区域一个无障碍功能走查工具,无需开启 Talkback 逐个手动触摸,就能高效检查无障碍适配情况。


图片


实现原理如下:




  • 自定义实现一个 AccessibilityService 用于获取到当前活跃窗口的根节点。




  • 每隔 0.5s 进行一次节点的获取:从当前活跃窗口的根节点遍历所有的节点,逐个进行判断是否会被聚焦。




  • 对通过允许聚焦的节点进行信息收集,在一次遍历完成后通知到 DrawService。




  • 提前在window中添加一个 View 用于绘制信息,由 DrawService 进行绘制。




5.2 具体实现


关键实现:如何判断一个节点能否被聚焦,即需理解 Talkback 是如何聚焦,流程如下:


1、如果是支持 WebView 中 Html 无障碍,特殊判断。


2、如果不可见,则不聚焦。


3、判断是否是画中画,像下图的红框这种就是画中画,如果是画中画,这个就是焦点。


图片


4、该节点是否和 window 边界重合等大。对于这种和 window 等大的节点,Talkback 选择不做聚焦。


5、检查该节点是否 clickable/longClickable/focusable 或者是列表的“会说话的” 顶层视图(满足->6 不满足->7)列表(ListView/RecycleView)的顶层视图例子如下:


图片


但是聚焦的前提是“会说话的”。“会说话的”包括以下几个条件:




  • HasText:包括 contentDescription、text、hintText(包括 Button 的 Text)。




  • hasStateDescription:包括 CheckBox 的已选未选状态、进度条的进度状态等。




  • hasNonActionableSpeakingChildren:含有无法聚焦、点击但是 HasText 的子 View(如上图通讯录中的 “新的朋友” TextView,就是无法聚焦、点击但是 HasText 的子 View)。




6、基本上满足了步骤5就可以视为可聚焦了,但是有一些View仅仅是 Focusable,但是却 ”什么话都没得说“ ,对于这种 View 应该是要排除的。故按如下步骤做判断:只要是没有子节点的 focusable/clickable/longclickable 的 View,全部聚焦 、“会说话的” 全部聚焦 6.3 剩下的就不聚焦了(“不会说话”、“有子节点”)。


7、能到这一步,说明步骤 5 不满足,即该节点是普通的不可聚焦的 View。但是防止错过一些没有点击事件的 TextView 之类的需要聚焦,需要再最后做一步判断(这一步也是啥为了保证所有的信息都可以不遗漏);如果没有可聚焦父节点,但仍然 hasText 或 hasStateDescription,聚集该节点。


8、一路闯关到这的 View,就终于逃离 TalkBack 的聚焦了。


06、总结


为了帮助老年人、视障/听障人群等更好地使用微信 App,Android微信完成了适老化及无障碍改造如上。本文主要介绍 Android 微信开发团队根据适老化及无障碍需求,完成的一个协助业务侧进行无障碍功能开发的框架。我们在介绍了无障碍开发所涉及的2大重点基础知识(读屏识别View原理和读屏软件后的事件分发原理)之后,为各位展开回顾了我们框架具体细节和方法。


以上是本次分享全部内容,欢迎大家在评论区分享交流。如果觉得内容有用,欢迎转发~


-End-


原创作者|许怀鑫


技术责编|许怀鑫


图片


现我国现有4471w视障/听障人士,60岁及以上人群达到2.6亿规模。信息无障碍(Web Accessibility)的概念在近几年受到关注。 信息无障碍是指通过信息化手段弥补身体机能、所处环境等存在的差异,使任何人(无论是健全人还是残疾人、无论是年轻人还是老年人)都能平等、方便、安全地获取、交互、使用信息。微信、QQ、腾讯新闻和腾讯地图等应用加适老化元素,配备为老人而设的“关怀模式”;搜狗输入法推出为视障群体量身打造的“保益盲人输入法”......


当说到无障碍,大家第一反应是弱势群体。实际上,无障碍是适用于全民的。每个人都可能有遇障时刻。当你手提重物或受伤时,你可能会选择乘坐无障碍电梯;当你处在嘈杂的环境下看视频时,你可能需要通过字幕获取信息……每个人都是无障碍环境的受益者,视障、听障人群、含残疾人、老年人是信息无障碍的重点受益群体。


事件分享:你还见到过哪些让你眼前一亮的信息无障碍案例?


脑洞时刻:程序员还可以为信息无障碍做些什么?


欢迎在公众号评论区聊一聊你的看法。在4月10日前将你的评论记录截图,发送给腾讯云开发者公众号后台,可领取腾讯云「开发者春季限定红包封面」一个,数量有限先到先得😄。我们还将选取点赞量最高的1位朋友,送出腾讯QQ公仔1个。4月10日中午12点开奖。快邀请你的开发者朋友们一起来参与吧!


回复「微信」,领取更多微信的技术case和论文资源


图片


阅读原文


作者:腾讯云开发者
来源:juejin.cn/post/7218015602769133625
收起阅读 »

接地气的前端代码规范

web
背景: 技术栈为 vue全家桶 更细节、更符合公司现状的一些约定、规范 优先级 A:必要的 这些规则会帮你规避错误,减少可能会产生的缺陷或者性能隐患。 JavaScript 在使用变量前,必须进行判空,必要时还需进行类型判断;若是对象,建议使用可选链 我们...
继续阅读 »

背景:



  • 技术栈为 vue全家桶

  • 更细节、更符合公司现状的一些约定、规范


优先级 A:必要的


这些规则会帮你规避错误,减少可能会产生的缺陷或者性能隐患。


JavaScript


在使用变量前,必须进行判空,必要时还需进行类型判断;若是对象,建议使用可选链


我们经常会遇到这样的情况:在定义变量时未赋默认值;根据接口返回值进行赋值,因数据等问题导致字段有缺失。若我们在使用这些变量时,未进行必要的判断,理所当然地去使用变量的属性、方法等,轻则导致console上出现一些error信息,再则出现功能无法正确运行,重则直接出现整个系统白屏!
注:在<template>中使用的变量,出现undefined而未进行判空,会导致系统白屏。目前vue@2.6.x还未支持<template>中使用可选链,后续会考虑是否升级到2.7.x。


// 反例
let a, b, c;

a = res.data.data.a;

b = JSON.parse(a);

c = b.includes("1");

// 正例
let a, b, c;

a = res?.data?.data?.a;

if (!!a) {
b = JSON.parse(a);
}

if (Array.isArray(b)) {
c = b.includes("1");
}

必须对接口报错进行处理,至少需进行错误提示


目前系统中对接口错误状态码、错误提示的处理良莠不齐,导致部分接口一旦出错,页面无任何反应,对用户很不友好。



  • 针对接口出现一些错误状态码(如status: 500),后续会在组件库的interceptor中对所有axios进行统一处理,给出错误提示,并往外抛。各个业务层可以对组件库抛出的信息进行进一步的处理,如关闭loading,回退处理等等。

  • 针对接口status: 200``success: false,需要在各个调用接口的地方给出提示语。优先以后端返回为准,否则提示语默认为:系统异常,请联系管理员。

  • 针对接口返回blob文件或其他可能会出现异常的情况,建议使用try...catch来捕获异常。


// 反例
function fetchUser (userId) {
return fetch(`/xxx/xxx/${userId}`);
}

// Promise的实现
function updateUserInfo (userId) {
fetchUser(userId).then(res => {
if (res.data.success) {
doSuccessAction();
}
})
}

// async/await的实现
async function updateUserInfo (userId) {
const res = await fetchUser(userId);
if (res.data.success) {
doSuccessAction();
}
}

// 正例
function fetchUser (userId) {
return fetch(`/xxx/xxx/${userId}`);
}

// Promise的实现
function updateUserInfo (userId) {
fetchUser(userId).then(res => {
if (res.data.success) {
doSuccessAction();
} else {
const errorInfo = res.data.error || "系统异常,请联系管理员";
this.$Message.error(errorInfo);
}
}).catch (error => {
this.$Message.error(error);
});
}

// async/await的实现
async function updateUserInfo (userId) {
try {
const res = await fetchUser(userId);
if (res.data.success) {
doSuccessAction();
} else {
const errorInfo = res.data.error || "系统异常,请联系管理员";
this.$Message.error(errorInfo);
}
} catch (error) {
this.$Message.error(error);
}
}

禁止频繁调用同一个接口,包括循环、监听、或未做节流防抖的按钮等情况下调用接口


频繁调用接口,会产生很多问题,列举如下:



  • 接口耗时长,页面白屏,用户体验不好

  • 对后端服务器造成一定压力

  • 同一个接口,在短时间内同时发出,因为网络延迟等因素,会造成接口返回不一定按照接口发起的顺序,导致最终结果与预期不符


目前代码中会有这些常见情况导致频繁调用,以下给出对应的解决方法:



  • 循环中调用:进行接口聚合,比如原先是每一次给后端一个key,后端返回对应的枚举值,可以改为将这些key组合成数组,一次性请求,获取所有对应的枚举值。

  • 监听中调用:这种情况最大的问题是对watch或者computed的触发场景或次数未知。这个没有统一的解法,需要具体情况具体分析。

  • 按钮中调用:点击按钮后调用接口,是一个特别常见的场景,一般情况下我们不会主动去在接口点击后频繁调用同一个接口,但是要防止用户频繁点击按钮。我们需要在按钮点击后,进入loading状态,或者加上节流或防抖,以避免上述用户操作导致的问题。

  • 表单中调用:在input、select、cascader组件的on-change 事件中调用接口,可以改为在输入框失焦,下拉面板收起时触发,即on-blur、on-open-change、visible-change。


Prop 定义应该尽量详尽,至少指定类型


细致的 prop 定义有两个好处:



  • 它们写明了组件的 API,所以很容易看懂组件的用法;

  • 在开发环境下,如果向一个组件提供格式不正确的 prop,Vue 将会告警,以帮助你捕获潜在的错误来源。


// 这样做只有开发原型系统时可以接受
props: ['status']

props: {
status: String
}

// 更好的做法!
props: {
status: {
type: String,
required: true,
validator: function (value) {
return [
'syncing',
'synced',
'version-conflict',
'error'
].indexOf(value) !== -1
}
}
}

拒绝硬编码值;拒绝魔法数字和字符串;


硬编码值和魔法数字和字符串在编程中往往代表着不好的编码习惯,缺点也很明显:



  1. 值的意义难以了解。

  2. 值需要变动时,需要频繁变更,而且可能要改不只一个地方。


// 反例
for (let i = 0; i < 10; i++) {
//...
}

// 正例
const numApples = 10;
for (let i = 0; i < numApples; i++) {
//...
}


// 反例
<template>
<section class="demo-page">
<span v-if="status === '0'">待付款</span>
<span v-if="status === '1'">待发货</span>
<span v-if="status === '2'">待收货</span>
<span v-if="status === '3'">待评价</span>
</section>
</template>
<script>
export default {
data() {
return {
status: "0",
};
},
...
}
</script>

// 正例
<template>
<section class="demo-page">
<span>{{ statusMap[status] }}</span>
</section>
</template>
<script>
import { getStatusMapApi } from "@/api/index";
export default {
data() {
return {
status: "0",
statusMap:{}
};
},
mounted() {
getStatusMapApi().then(res => {
/* {
"0": 待付款,
"1": 待发货,
"2": 待收货,
"3": 待评价,
} */
this.statusMap = ...
})
}
}
</script>

禁止使用refs.children[i]获取子组件,建议用ref属性;不建议使用ref直接调用子组件的api,以保持组件的独立性


refs是一个对象,持有注册过 ref attribute 的所有 DOM 元素和组件实例。children是当前实例的直接子组件,它并不保证顺序,也不是响应式的。因此使用refs.children[i]获取子组件,是一种不稳定的操作。
ref属性可以访问子组件实例或子元素,但这仅仅是一个直接操作子组件的应急方案;为了保持组件的独立性、稳定性,建议不要直接使用子组件的方法、变量等。


禁止在watch和computed中用$route


由于我们目前都是keep-alive模式,若是在watch和computed中用$route,那么在包括tab页签打开、切换等操作在内的每一次路由变化,都会触发watch和computed,不论是否跟本页面本组件有关系。这样子带来了巨大的性能损耗和一些奇奇怪怪的缺陷产生。


禁止增删改JavaScript 对象或Vue的原型,造成原型污染


原型上的属性可以通过遍历访问到的,原型污染会引起性能消耗或意外BUG。
实际上,大多数在写业务代码的场景下,修改原型的方式都可以采用别的方式替代。


注释要保证详细、完整


推荐使用vscode的koroFileHeader插件,进行快捷注释操作。
文件注释去掉,可以留一个description;
代码有更新,注释记得也要更新;


/**
* @description 这个方法是干嘛用的
* @param {*}
* @return {*}
*/


/**
* @description 这个接口是干嘛用的
* @param {*}
* @see yapi地址
*/


// 这个变量是干嘛用的

工程目录、文件(夹)命名、组件内部命名等需遵循以下内部规范


因篇幅过长,单独整理


CSS


必须为组件样式设置作用域,建议采用scoped属性或者class策略。


设置样式的作用域可以有效确保你的样式只会运用在你想要作用的组件上,而不会造成”污染“。



  • scoped 属性:控制CSS 只作用于当前组件中的元素,需要给<style> 标签加上 scoped属性。

  • class策略:不止要使用 scoped属性,使用唯一的 class 名可以帮你确保那些三方库的 CSS或者其他组件的CSS 不会运用在你自己的 HTML 上。


// 反例
<template>
<span class="title">xxxxxxxxxx</span>
</template>

<style>
.title {
color: red;
}
</style>

// 正例
<template>
<div class="xxx-mgr-page">
<span class="title">xxxxxxxxxx</span>
</div>
</template>

<!-- 使用 `scoped` attribute -->
<style scoped>
.xxx-mgr-page{
.title {
color: red;
}
}
</style>

禁止使用全局选择器、类型选择器等作用范围太大的选择器添加css规则,推荐使用类选择器进行精细化控制。


简单说一下这两种被禁止的选择器:



  • 全局选择器,是由一个星号(*)代指的,它选中了文档中的所有内容。

  • 类型选择器,也叫做”标签名选择器“或者是”元素选择器“,因为它在文档中选择了一个 HTML 标签/元素的缘故。


使用他们添加css规则,会造成以下影响:



  • 作用范围太大,会造成一些不想作用的地方却误伤到了

  • 从性能角度考虑,标签选择器的性能比类选择器要慢


禁止通过css选择器的权重和优先规则来覆盖样式


在项目中,可能一个简单的按钮,它的样式会取决于很多地方很多层:组件库为它定义了最底层、最基本的外观 -> 业务项目中的公共样式为它定义了本项目中的统一样式 -> 页面样式为它定义了布局 -> 具体到这个按钮的样式定义了它的独特样式。正是由于这么多层这么复杂的样式组成,导致在需要更新样式的时候,会出现一些很”偷懒“的做法——通过直接覆盖样式,而不是去找到原先写样式的地方去修改。


// 反例
<template>
<button class="ivu-btn btn-close" style="color: white;">X</button>
</template>

<style>
.btn-close {
color: red;
}
</style>

// 正例
<template>
<button class="ivu-btn btn-close">X</button>
</template>

<style>
.btn-close {
color: white;
}
</style>

使用不常用的js api 和 css attribute,注意确认下浏览器兼容性


本条推荐理由很简单。我们推荐使用了chrome浏览器版本号为80+,那兼容性就需要考虑。常用属性已经验证过了没问题,不常用的就需要自行验证。建议可以通过mdn web docs(developer.mozilla.org/zh-CN/docs/…)来查询。
image.png


超长溢出统一用title,而不是tooltip,以提高性能


推荐理由如下:



  • 由于我们全平台中产品设计倾向于单行文本显示,包括标题文本、下拉表单项、表格单元格等等,一个页面中有可能就有上千个。

  • tooltip是iview组件,样式美观可调整,但包含了多个DOM节点;title是HTML属性,样式无法变更。两者性能差异大。


因为涉及范围之广、两者性能差异之大,所以我们建议用title来处理超长溢出。




优先级 B:推荐的


这些规则能够在绝大多数工程中改善可读性和开发体验。即使你违反了,代码还是能照常运行,但例外应该尽可能少且有合理的理由。


禁止单个vue文件超过1000行,尽量500行;禁止复制黏贴超20行的代码


在平常项目开发中,大家都深深体会到了:一个文件太长,维护起来头很大,开发模式下编译时间也很长;大段相似的代码,很不优雅,若产生问题也很容易只改一处,造成缺陷。
之所以限制1000行、500行、20行,凭以往经验决定;只要有充分理由,可以灵活应变。不断地去抽象,去提炼,去封装,也是很考验开发者的功力,很有助于我们的成长。
注:后续会考虑通过eslint+git-hooks阻止超过1000行的文件被提交。


建议不要在html中有超过两个条件的逻辑判断;在js中不要超过两个并列的if,可以考虑优雅的if-else


html中,不要有超过两句话(尽量一个操作符)的逻辑,否则就用computed
js中,一段逻辑不要超过两个if(如果你是第三个应该评估优化一下),优雅的维护if-else。嵌套的if尽可能减少或者注释清楚判断逻辑


代码优化之后,确定不需要的代码建议直接删除,不确定的代码进行注释并写明注释原因;注释或删除一段代码,要把相关的代码一并处理干净


现有情况是存在很多大段注释的代码,太过冗余杂乱,影响代码阅读,因此建议不需要的代码直接删除。
但又存在部分情况是产品提出的要求暂时隐藏某个功能,后续可能会重新启用,因此只需进行注释即可。建议这种情况下,写明注释原因,供他人后续阅读代码或者优化代码提供指引。
注释或删除一段代码时,现在会存在部分情况下,只删除直接相关代码,其他相关代码放任不管。举个例子,比如产品要求隐藏”保存并启用“功能,最差最直接的做法是隐藏这个按钮就完成,但是发现要获取这个按钮权限,需要watch中调用接口,因此导致这个功能被隐藏了,但是接口调用仍在频繁调用。


布局嵌套尽量不要层级太深;不加没有必要的DOM节点;




优先级 C:小tips


这个分类下的是一些项目开发的小技能、小知识点或业务相关的点。


路由组件一定要有name ,以确保keep-alive生效


<keep-alive>includeexclude prop 允许组件有条件地缓存。匹配首先检查组件自身的 name 选项,如果 name 选项不可用,则匹配它的局部注册名称 (父组件 components 选项的键值)。匿名组件不能被匹配。


弹窗或其他未激活的tab别在mounted阶段加载,以减小白屏时间


操作闭环、逻辑闭环考虑


举个栗子:



  • 比如弹窗要考虑确定、取消、关闭、重新打开等一系列闭环操作的正确性;

  • 比如详情的新建、查看、编辑;

  • 比如表格分页,考虑页码跳转,分页器大小变化,过滤条件变化时初始化页码等;

  • 比如v-if v-else-if v-else;


使用every和some方法时,记得排除空数组;every返回始终为true,some始终false


let a = [];
a.every(i => { ... }) // true
a.some(i => { ... }) // false

let a = [];
if (a.length > 0) {
a.every(i => { ... }) // true
a.some(i => { ... }) // false
} else {
...
}

对象浅拷贝时,分清对象展开运算符和Object.assign的区别


let aa = { a : 1, b : 2, c : 3};
let bb = Object.assign(aa, {d : 4});

// 修改aa
delete aa.a;
// 结果bb也发生了变化
console.log(bb); // {b: 2, c: 3, d: 4}

let aa = { a : 1, b : 2, c : 3};
// 解法1
let bb = Object.assign({}, aa, {d : 4});
// 解法2
let cc = {...aa, d : 4};
// 修改aa
delete aa.a;
console.log(bb); // {a: 1, b: 2, c: 3, d: 4}
console.log(cc); // {a: 1, b: 2, c: 3, d: 4}

在mounted或created阶段获取路由信息


连续调用接口的方法中如果有路由这种会变化的传参时,不能使用this.$route获取路由,避免执行方法时用户通过点击页签切换路由导致后续接口报错


样式尽可能考虑不同分辨率的自适应,如1366、1920


作者:是秋天啊
来源:juejin.cn/post/7216526817371504697
收起阅读 »