注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

使用小程序中的 observe 实现数据监听

web
小程序开发中,数据的监听和响应是非常重要的。为了更方便地监听数据的变化,小程序提供了 observe 方法。本文将详细介绍如何在小程序中使用 observe 实现数据监听,以及一些常见的应用场景。 什么是 observe? observe 是小程序中的一个方法...
继续阅读 »

小程序开发中,数据的监听和响应是非常重要的。为了更方便地监听数据的变化,小程序提供了 observe 方法。本文将详细介绍如何在小程序中使用 observe 实现数据监听,以及一些常见的应用场景。


什么是 observe


observe 是小程序中的一个方法,用于监听数据的变化并触发相应的回调函数。它可以用于监听页面数据、组件数据以及其他数据对象。


如何使用 observe


监听页面数据


在页面的 .js 文件中,可以使用 Page 函数中的 data 中的 observe 字段来监听数据的变化。以下是一个示例:


// pages/index/index.js
Page({
data: {
count: 0,
},

// 监听 count 数据的变化
observe: {
'count': function (newVal, oldVal) {
console.log('count 的值从 ' + oldVal + ' 变为 ' + newVal);
}
},

// 增加 count 值的函数
increaseCount() {
this.setData({
count: this.data.count + 1,
});
},
});

在上述示例中,我们在 observe 字段中定义了一个监听器,当 count 数据发生变化时,会触发相应的回调函数。


监听组件数据


在小程序的组件中,也可以使用 observe 来监听组件数据的变化。以下是一个示例:


// components/my-component/my-component.js
Component({
data: {
message: 'Hello, World!',
},

methods: {
changeMessage() {
this.setData({
message: 'New Message!',
});
},
},

// 监听 message 数据的变化
observe: {
'message': function (newVal, oldVal) {
console.log('message 的值从 ' + oldVal + ' 变为 ' + newVal);
}
},
});

在组件的 observe 字段中同样定义了一个监听器,用于监听 message 数据的变化。


常见应用场景


1. 数据绑定


observe 可以用于在数据变化时自动更新视图,实现数据绑定。这对于构建响应式的页面和组件非常有用。


2. 数据校验


通过监听数据的变化,可以在数据变化时进行校验,确保数据的合法性。例如,监听输入框中的文本变化并验证其格式。


3. 事件通知


当某个数据变化时,可以通过 observe 触发相关的事件,通知其他部分的代码执行相应的操作。


4. 数据持久化


在某些情况下,需要将数据持久化到本地存储或服务器,可以在数据变化时触发数据保存操作。


注意事项和最佳实践


在使用 observe 进行数据监听时,有一些注意事项和最佳实践需要考虑:




  1. 数据引用类型的监听:当监听对象是引用类型(例如对象或数组)时,需要注意对象的引用是否发生变化。observe 监听的是对象的引用,而不是对象内部属性的变化。如果需要监听对象内部属性的变化,可以使用深度监听或手动触发。




  2. 避免过多监听器:不要过度使用 observe,因为过多的监听器可能会导致性能问题。只监听那些真正需要监控的数据。




  3. 监听器的性能开销:监听器的回调函数在数据变化时会被频繁调用,因此要确保回调函数的执行效率较高,以避免影响应用的性能。




  4. 避免循环引用:在监听器回调函数中不要再次修改被监听的数据,以防止循环引用和无限循环触发监听器。




  5. 生命周期管理:在页面或组件销毁时,要记得取消监听以防止内存泄漏。可以在 onUnload 生命周期中取消监听。




onUnload() {
this.setData({
observe: null, // 取消监听
});
}

通过谨慎使用 observe,你可以实现有效的数据监听和响应,提高小程序应用的可维护性和性能。


结语


observe 是小程序中非常有用的功能,它允许你监听数据的变化并执行相应的操作。无论是在页面中还是在组件中,都可以使用 observe 来实现数据的监听和响应。通过合理利用 observe,你可以构建更加动态和交互性的小程序应用。


作者:依旧_99
来源:juejin.cn/post/7295237661618438196
收起阅读 »

用canvas画出一片星空

web
前言 由于最近用了挺多Echarts的,所以突然想学习学习它的底层原理Canvas。Canvas对于我们前端来说是一个非常强大的工具,它可以实现各种复杂的图形和动画效果,我们如果能够熟练掌握它,我们就可以做很多炫酷的效果。 Canvas 介绍 首先我们来介绍介...
继续阅读 »

前言


由于最近用了挺多Echarts的,所以突然想学习学习它的底层原理CanvasCanvas对于我们前端来说是一个非常强大的工具,它可以实现各种复杂的图形和动画效果,我们如果能够熟练掌握它,我们就可以做很多炫酷的效果。


Canvas 介绍


首先我们来介绍介绍CanvansCanvasHTML5提供的一个绘图API,它允许通过JavaScriptHTML元素创建和操作图形。Canvas提供的是一个矩形画布,我们可以在上面绘制各种图形、动画与交互效果。


功能


上面了解了Canvas是什么之后, 我们再来和大家展开说说Canvas 一些主要功能:



  1. 画布:Canvas提供了一个矩形的画布区域, 我们可以通过HTML中的<canvas>标签来创建并通过设置画布的宽高来确定绘图区域的大小。

  2. 绘画API:Canvas提供了丰富的绘图API,包括绘制路径、直线、曲线、矩形、圆形、文本等。 我们使用这些API来创建各种图形,并自定义样式、颜色、透明度等属性。

  3. 动画:Canvas可以与JavaScript的动画函数结合使用,实现动态的图形效果。通过在每一帧中更新画布上的内容,能创建平滑的动画效果。

  4. 图像处理:Canvas可以加载和绘制图像。我们可以使用Canvas的API对图像进行裁剪、缩放、旋转等操作,从而实现图像处理的功能。

  5. 事件处理:Canvas支持鼠标和触摸事件的处理,我们可以通过监听这些事件来实现交互效果,例如点击、拖拽、缩放等等。


注意:Canvas 绘制的内容是即时生成的,它不会被浏览器缓存,所以每次页面加载Canvas都需要重新绘制,所以我们在使用时需要考虑性能问题。


星空


在介绍完Canvas之后,我们再来用CanvasJS结合,实现一片星空的效果。


第一步,我们先创建好html 结构,代码如下:


  <div class="landscape">
</div>
<canvas id="canvas"></canvas>
<div class="filter"></div>
<script src="./round_item.js"></script>

现在是没有效果的,我们在给它加上css代码,将其美化,同时我们再在css上加一些动画效果,完整代码如下:


* {
margin: 0;
padding: 0;
}
html,
body {
width: 100%;
height: 100%;
}

body {
background: linear-gradient(to bottom, #000 0%, #5788fe 100%);

}

.landscape {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 100%;
background-image: url(./star/xkbg.png);
background-repeat: repeat-x;
background-size: 1000px 250px;
background-position: center bottom;
}
.filter{
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
z-index: 2;
background: #fa7575;
animation: colorChange 30s ease-in infinite;
}
/* 渐变动画*/
@keyframes colorChange {
0%,100%{
opacity: 0;
}
50%{
opacity: 0.7;
}

}

这时候,我们看到的效果是这样的:


1698327282484.gif


这效果好像只有黄昏的颜色变化,少了星空。那这最后一步,就该我们Canvas上场了,我们要用Canvas画出来一片星空,并配合css的颜色变化,实现一个夜晚到清晨的感觉。


我们 js 代码这样写:


//创建星星的函数
function RoundItem(index, x, y, ctx) {
this.index = index
this.x = x
this.y = y
this.ctx = ctx
this.r = Math.random() * 2 + 1
this.color = 'rgba(255,255,255,1)'
}
// 绘制
RoundItem.prototype.draw = function () {
this.ctx.fillStyle = this.color //指定颜色
this.ctx.beginPath() // 开始绘制
this.ctx.arc(this.x, this.y, this.r, 0, 2 * Math.PI, false) // 绘制圆形
this.ctx.closePath() // 结束绘制
this.ctx.fill() //填充形状
}
//移动
RoundItem.prototype.move = function () {
this.y -= 0.5
this.draw()
}


let canvas = document.getElementById('canvas'),
ctx = canvas.getContext('2d'),
round = [],
initRoundPopulation = 200; //星星的个数

const WIDTH = document.documentElement.clientWidth,
HEIGHT = document.documentElement.clientHeight;

canvas.width = WIDTH
canvas.height = HEIGHT

init()
// setInterval(animate, 1700)
animate()
function init() {
for (var i = 0; i < initRoundPopulation; i++) {
round[i] = new RoundItem(i, Math.random() * WIDTH, Math.random() * HEIGHT, ctx)
round[i].draw()
}
}
function animate() {
ctx.clearRect(0, 0, WIDTH, HEIGHT) //清除画布
for (let star of round) {
star.move()
}
requestAnimationFrame(animate) //通过刷新帧数来调用函数
}


将上述代码添加上之后,我们也就完成星星的绘制并添加到了页面上,最终的效果如下:
1698327645625.gif


到这里,我们就用canvas 画出了一片美丽的星空。同时我们也看到了canvas的强大功能,感兴趣的小伙伴可以深入的了解它更多的用法哦。


作者:潘小七
来源:juejin.cn/post/7294103091019612212
收起阅读 »

2D的雪碧图已经够炫了,那么3D的呢?

web
前言 前2篇文章,分别介绍了dat.gui和纹理贴图,老是理论没有实战也是没有什么意思的,今天我们就来着手一个小案例,赶紧实现起来,让你的博客更加炫酷! 这个案例包含了tweenjs动画库的使用,该动画库已在three.js中内置,路径为: examples/...
继续阅读 »

output-16_6_11.gif


前言


前2篇文章,分别介绍了dat.gui纹理贴图,老是理论没有实战也是没有什么意思的,今天我们就来着手一个小案例,赶紧实现起来,让你的博客更加炫酷!


这个案例包含了tweenjs动画库的使用,该动画库已在three.js中内置,路径为: examples/jsm/libs/tween.module.js,使用起来也是比较简单。


初始化


老样子,场景、相机、渲染器三要素初始化,并导入需要的插件库,插件库都已在three中内置:


import * as THREE from 'three';
// tween动画库
import TWEEN from 'three/addons/libs/tween.module.js';
//通过轨迹球控件TrackballControls 我们可以实现场景的旋转、缩放、平移等功能
import { TrackballControls } from 'three/addons/controls/TrackballControls.js'
// 雪碧图
import { CSS3DRenderer, CSS3DSprite } from 'three/addons/renderers/CSS3DRenderer.js'

// 定义场景、相机、渲染器
let scene, camera, renderer;

// 初始化
init()
// 渲染
animate();

function init() {
// 透视相机 远端距离最好设大一点 不然会展示不全
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 10000);
// 相机位置
camera.position.set(600, 400, 1500);
// 相机朝向位置
camera.lookAt(0, 0, 0);

// 场景
scene = new THREE.Scene();

// 渲染画布
renderer = new CSS3DRenderer();
renderer.setSize(innerWidth, window.innerHeight);
document.getElementById('container').appendChild(renderer.domElement);
}

// 渲染
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera)
}

CSS3DSprite创建521个水球


// 定义小球数量
const particlesTotal = 512;
// 定义位置
const positions = [];
// 定义物体
const objects = []
const image = document.createElement('img');
image.addEventListener('load', () => {
for (let i = 0; i < particlesTotal; i++) {
// cloneNode() 方法可创建指定的节点的精确拷贝
const object = new CSS3DSprite(image.cloneNode())
// 随机分布位置 -2000 2000的立方体内
object.position.x = Math.random() * 4000 - 2000,
object.position.y = Math.random() * 4000 - 2000,
object.position.z = Math.random() * 4000 - 2000,
scene.add(object)

objects.push(object);
}
})
image.src = './static/img/sprite.png';


上面的代码中,我们创建了img标签,并使用CSS3DSpriteHTML元素转化为threejs的CSS3精灵模型,类似与转换成了three中的网格,并随机分布在-2000,2000的立方体中。


看下效果:


three04-1.jpg


添加控制器


关于控制器,前面也已经介绍过啦,通过控制器,我们就可以改变相机的位置,观察不同角度的物体。


// 定义控制器
let controls;
controls = new TrackballControls( camera, renderer.domElement );

// 渲染
function animate() {
...
controls.update();
...
}

注意哦,controls.update需要防止在animate中,每帧执行。


有了控制器。我们就可以实现交互啦:


output-15_37_12.gif


让小球按规律放大缩小


让小球按照正弦时间,放大缩小,即有一种闪烁的效果:


const time = performance.now();

for (let i = 0, l = objects.length; i < l; i++) {
const object = objects[i];
const scale = Math.sin((Math.floor(object.position.x) + time) * 0.002) * 0.3 + 1;
object.scale.set(scale, scale, scale);
}

output-15_37_59.gif


让小球生成特定图形


生成矩形,对应的每个小球坐标:


const amount = 8;
const separationCube = 150;
const offset = ( ( amount - 1 ) * separationCube ) / 2;

for ( let i = 0; i < particlesTotal; i ++ ) {

const x = ( i % amount ) * separationCube;
const y = Math.floor( ( i / amount ) % amount ) * separationCube;
const z = Math.floor( i / ( amount * amount ) ) * separationCube;

positions.push( x - offset, y - offset, z - offset );

}

tween.js使用


const position = {x: 0,y: 0};
;//创建一段tween动画
const tween = new TWEEN.Tween(position)
//经过2秒,position对象的x和y属性分别从零变化为100、50
tween.to({x: 100,y: 50}, 2000);
//tween动画开始执行
tween.start();
// 动画效果 类似annimation
tween.easing()
// 完成时执行的钩子,里面可以继续执行下一个操作
tween.onComplete()

使杂乱的小球变成矩形


import * as THREE from 'three';
// tween动画库
import TWEEN from 'three/addons/libs/tween.module.js';
//通过轨迹球控件TrackballControls 我们可以实现场景的旋转、缩放、平移等功能
import { TrackballControls } from 'three/addons/controls/TrackballControls.js'
// 雪碧图
import { CSS3DRenderer, CSS3DSprite } from 'three/addons/renderers/CSS3DRenderer.js'

// 定义场景、相机、渲染器
let scene, camera, renderer;
// 定义控制器
let controls;

// 定义小球数量
const particlesTotal = 512;
// 定义位置
const positions = [];
// 定义物体
const objects = []
let current = 0;
// 初始化
init()
// 渲染
animate();

function init() {
// 透视相机 远端距离最好设大一点 不然会展示不全
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 10000);
// 相机位置
camera.position.set(600, 400, 1500);
// 相机朝向位置
camera.lookAt(0, 0, 0);


scene = new THREE.Scene();

const image = document.createElement('img');
image.addEventListener('load', () => {
for (let i = 0; i < particlesTotal; i++) {
// cloneNode() 方法可创建指定的节点的精确拷贝
const object = new CSS3DSprite(image.cloneNode())
// 随机分布位置 -2000 2000的立方体内
object.position.x = Math.random() * 4000 - 2000,
object.position.y = Math.random() * 4000 - 2000,
object.position.z = Math.random() * 4000 - 2000,
scene.add(object)

objects.push(object);
}
transition()
})
image.src = './static/img/sprite.png';

// cube
const amount = 8;
const separationCube = 150;
const offset = ((amount - 1) * separationCube) / 2;

for (let i = 0; i < particlesTotal; i++) {

const x = (i % amount) * separationCube;
const y = Math.floor((i / amount) % amount) * separationCube;
const z = Math.floor(i / (amount * amount)) * separationCube;

positions.push(x - offset, y - offset, z - offset);

}

// 渲染画布
renderer = new CSS3DRenderer();
renderer.setSize(innerWidth, window.innerHeight);
document.getElementById('container').appendChild(renderer.domElement);

controls = new TrackballControls(camera, renderer.domElement);

}


// 动画
function transition() {
const offset = current * particlesTotal * 3;
const duration = 2000;
for (let i = 0, j = offset; i < particlesTotal; i++, j += 3) {
const object = objects[i];
new TWEEN.Tween(object.position)
.to({
x: positions[j],
y: positions[j + 1],
z: positions[j + 2]
}, Math.random() * duration + duration)
.easing(TWEEN.Easing.Exponential.InOut)
.start();
}
new TWEEN.Tween(this)
.to({}, duration * 3)
.onComplete(transition)
.start();
current = (current + 1) % 4;
}

// 渲染
function animate() {
requestAnimationFrame(animate);
controls.update();
TWEEN.update();

// 让小球按照正弦时间,放大缩小
const time = performance.now();

for (let i = 0, l = objects.length; i < l; i++) {

const object = objects[i];
const scale = Math.sin((Math.floor(object.position.x) + time) * 0.002) * 0.3 + 1;
object.scale.set(scale, scale, scale);

}
renderer.render(scene, camera)
}

杂乱的小球变成多种形态完整代码


限制文件大小啦,没法完全展示,大家自行运行看看吧!


output-15_49_5.gif


<div id="container"></div>
<script type="module">
import * as THREE from 'three';
// tween动画库
import TWEEN from 'three/addons/libs/tween.module.js';
//通过轨迹球控件TrackballControls 我们可以实现场景的旋转、缩放、平移等功能
import { TrackballControls } from 'three/addons/controls/TrackballControls.js'
// 雪碧图
import { CSS3DRenderer, CSS3DSprite } from 'three/addons/renderers/CSS3DRenderer.js'

// 定义场景、相机、渲染器
let scene, camera, renderer;
// 定义控制器
let controls;

// 定义小球数量
const particlesTotal = 512;
// 定义位置
const positions = [];
// 定义物体
const objects = []
let current = 0;
// 初始化
init()
// 渲染
animate();

function init() {
// 透视相机 远端距离最好设大一点 不然会展示不全
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 10000);
// 相机位置
camera.position.set(600, 400, 1500);
// 相机朝向位置
camera.lookAt(0, 0, 0);


scene = new THREE.Scene();

const image = document.createElement('img');
image.addEventListener('load', () => {
for (let i = 0; i < particlesTotal; i++) {
// cloneNode() 方法可创建指定的节点的精确拷贝
const object = new CSS3DSprite(image.cloneNode())
// 随机分布位置 -2000 2000的立方体内
object.position.x = Math.random() * 4000 - 2000,
object.position.y = Math.random() * 4000 - 2000,
object.position.z = Math.random() * 4000 - 2000,
scene.add(object)

objects.push(object);
}
transition()
})
image.src = './static/img/sprite.png';

// Plane
const amountX = 16;
const amountZ = 32;
const separationPlane = 150;
const offsetX = ((amountX - 1) * separationPlane) / 2;
const offsetZ = ((amountZ - 1) * separationPlane) / 2;
for (let i = 0; i < particlesTotal; i++) {
const x = (i % amountX) * separationPlane;
const z = Math.floor(i / amountX) * separationPlane;
const y = (Math.sin(x * 0.5) + Math.sin(z * 0.5)) * 200;
positions.push(x - offsetX, y, z - offsetZ);
}

// Cube
const amount = 8;
const separationCube = 150;
const offset = ((amount - 1) * separationCube) / 2;
for (let i = 0; i < particlesTotal; i++) {
const x = (i % amount) * separationCube;
const y = Math.floor((i / amount) % amount) * separationCube;
const z = Math.floor(i / (amount * amount)) * separationCube;
positions.push(x - offset, y - offset, z - offset);
}

// Random
for (let i = 0; i < particlesTotal; i++) {
positions.push(
Math.random() * 4000 - 2000,
Math.random() * 4000 - 2000,
Math.random() * 4000 - 2000
);
}

// Sphere
const radius = 750;
for (let i = 0; i < particlesTotal; i++) {
const phi = Math.acos(- 1 + (2 * i) / particlesTotal);
const theta = Math.sqrt(particlesTotal * Math.PI) * phi;
positions.push(
radius * Math.cos(theta) * Math.sin(phi),
radius * Math.sin(theta) * Math.sin(phi),
radius * Math.cos(phi)
);
}

// 渲染画布
renderer = new CSS3DRenderer();
renderer.setSize(innerWidth, window.innerHeight);
document.getElementById('container').appendChild(renderer.domElement);
controls = new TrackballControls(camera, renderer.domElement);
}


// 动画
function transition() {
const offset = current * particlesTotal * 3;
const duration = 2000;
for (let i = 0, j = offset; i < particlesTotal; i++, j += 3) {
const object = objects[i];
new TWEEN.Tween(object.position)
.to({
x: positions[j],
y: positions[j + 1],
z: positions[j + 2]
}, Math.random() * duration + duration)
.easing(TWEEN.Easing.Exponential.InOut)
.start();
}
new TWEEN.Tween(this)
.to({}, duration * 3)
.onComplete(transition)
.start();
current = (current + 1) % 4;
}

// 渲染
function animate() {
requestAnimationFrame(animate);
controls.update();
TWEEN.update();

// 让小球按照正弦时间,放大缩小
const time = performance.now();

for (let i = 0, l = objects.length; i < l; i++) {

const object = objects[i];
const scale = Math.sin((Math.floor(object.position.x) + time) * 0.002) * 0.3 + 1;
object.scale.set(scale, scale, scale);

}
renderer.render(scene, camera)
}

</script>

作者:八月十八
来源:juejin.cn/post/7294301361835147290
收起阅读 »

重复请求优化

web
设想一种场景,有两个组件,这两个组件在初始化阶段,都需要调用同一个 API 接口去获取数据。为了防止请求冗余,可以把两个组件的请求都挪到父组件中,由父组件统一调用一次请求,然后再将响应的数据结果传给两个子组件。这种方法应该是最常见的,不过它也有一个局限性条件:...
继续阅读 »

设想一种场景,有两个组件,这两个组件在初始化阶段,都需要调用同一个 API 接口去获取数据。为了防止请求冗余,可以把两个组件的请求都挪到父组件中,由父组件统一调用一次请求,然后再将响应的数据结果传给两个子组件。这种方法应该是最常见的,不过它也有一个局限性条件:两个组件必须有一个共同的祖先组件,如果这两个组件是同级的兄弟组件倒也还好,如果非同级,那么数据的传参就会有些麻烦了。那么还有其他办法吗?当然是有的。


我们可以换个思路,每个组件还是保持原有的业务逻辑不变,从请求接口处做文章。既然是同一个接口调用了两次,而且还是返回了相同的请求结果,那么不妨在第一次时调用成功时,就把请求结果缓存起来,等到第二次再调用时,直接返回缓存的数据。按照这个思路可以写出第一版的代码(这里用了 TS 方便查看参数的类型):


/**
* 缓存请求的响应结果
* 把发起请求的 Promise 对象挂载在原型对象上
* @param request 请求函数
*/

function cacheRequest<T>(request: (...args: any[]) => Promise<T>) {
const cache = Symbol("cache")
return function (...args: any[]): Promise<T> {
if (!request.prototype[cache]) {
request.prototype[cache] = request(...args)
}
return request.prototype[cache]
}
}


  • 首先 cacheRequest 函数,需要接收一个参数 requestrequest 是一个返回结果为 Promise 对象的函数。cacheRequest 执行完后返回一个新的匿名函数。

  • 然后,在匿名函数的内部,先判断 request 的原型对象上是否有 cache(这里的 cache 使用了 Symbol 类型,确保键名唯一)。也即,是否有缓存过的请求结果,如果没有,说明是第一次调用,则将 request 的执行结果存到缓存里。如果有缓存,则直接返回缓存。

  • 可以看到,缓存也是一个 Promise 类型。在同时调用多次请求时,只要在第一次调用执行后,已经把 Promise 存到缓存里了,后续的请求返回的也是缓存里的 Promise,从而保证多个请求都指向同一个 Promise ,也即只会调用一次 API 接口。

  • 这里需要注意一点,由于需要往 request 的原型对象上挂载缓存,所以 request 不能是箭头函数。因为箭头函数没有 this,也就意味着没有原型对象。


小测一下:


function cacheRequest(request) {
const cache = Symbol("cache")
return function (...args) {
if (!request.prototype[cache]) {
request.prototype[cache] = request(...args)
}
return request.prototype[cache]
}
}

const request = function () {
return new Promise(resolve => {
console.log("fetch request")
setTimeout(resolve, 2000)
})
}

const newRequest = cacheRequest(request)

newRequest()
newRequest()
newRequest()

version1.png


可以看到虽然 newRequest 调用了三次,但是 fetch request 只打印了一次,也就是说 request 只调用了一次,符合预期!但是,最后一次 newRequest 的调用,是在 3 秒后调用的,也是走的缓存,没有重新执行。仔细思考一下,后续无论什么时候调用 newRequest 都会使用缓存里的数据,不会重新调用请求了,这显然是不合理的。我们还需要加个缓存的过期时间,超过这个时间,就重新发起新的请求。第二版如下:


/**
* 缓存请求的响应结果
* 把发起请求的 Promise 对象挂载在原型对象上
* 保证在 cacheTime 时间间隔内的多次请求,只会调用一次
* @param request 请求函数
* @param cacheTime 最大缓存时间(单位毫秒)
*/

export function cacheRequest<T>(request: (...args: any[]) => Promise<T>, cacheTime = 1000) {
const cache = Symbol("cache")
const lastTime = Symbol("lastTime")
return function (...args: any[]): Promise<T> {
if (!request.prototype[cache] || Date.now() - request.prototype[lastTime] >= cacheTime) {
request.prototype[cache] = request(...args)
request.prototype[lastTime] = Date.now()
}
return request.prototype[cache]
}
}


  • 首先,cacheRequest 新增一个入参 cacheTime,用于设置过期时间,默认为 1 秒。

  • 其次,在原型对象上新增了一个 lastTime 属性,用来记录最后一次调用的时间。

  • 当缓存为空,或者当前时间距离上一次调用时间超过缓存过期时间时,更新 cachelastTime


再来小测一下:


function cacheRequest(request) {
const cache = Symbol("cache")
const lastTime = Symbol("lastTime")
return function (...args) {
if (!request.prototype[cache] || Date.now() - request.prototype[lastTime] >= cacheTime) {
request.prototype[cache] = request(...args)
request.prototype[lastTime] = Date.now()
}
return request.prototype[cache]
}
}

const request = function () {
return new Promise(resolve => {
console.log("fetch request")
setTimeout(resolve, 2000)
})
}

const newRequest = cacheRequest(request)

newRequest()
newRequest()
setTimeout(newRequest, 3000)

version2.png


这一次,fetch request 打印了两次,符合预期,完美!


作者:showlotus
来源:juejin.cn/post/7294597695478333476
收起阅读 »

【Java集合】了解集合的框架体系结构及常用实现类,从入门到精通!

嗨~ 今天的你过得还好吗?以不同的方式长大谁都没有轻轻松松🌞- 2023.10.27 -通过Java基础的学习,我们掌握了Java语言主要的基本的语法,同时了解学习了Java语言的核心——面向对象编程思想。这篇文章就来带大家深入了解集合的框架体系结构...
继续阅读 »

640 (13).jpg

嗨~ 今天的你过得还好吗?

以不同的方式长大

谁都没有轻轻松松

🌞

- 2023.10.27 -

通过Java基础的学习,我们掌握了Java语言主要的基本的语法,同时了解学习了Java语言的核心——面向对象编程思想。这篇文章就来带大家深入了解集合的框架体系结构

从集合框架开始,也就是进入了java这些基础知识及面向对象思想进入实际应用编码的过程,通过jdk中集合这部分代码的阅读学习,就能发现这一点。


5bbd4f4683e25cfda6c3946e9925c48f.gif


本计划在这篇中把框架体系和一些集合的常用方法一起编写。仔细考虑之后,本着突出重点,结构清晰的思路,所以把框架体系单独拉出来,为让各位看官对java的集合框架有个清晰的认识,最起码记住常用的几种常用实现类!


好的,下面我们进入正题。


集合的框架体系结构

可以在很多JAVAEE进阶知识的学习书籍或者教程中看到,JDK中提供了满足各种需求的API,主要是让我们去学习和了解它提供的各种API。

54f18513580ae2f9b13d1b648e6c2380.jpeg

在使用这些API之前,我们往往需要先了解其继承与接口架构,才能了解何时采用哪个实现类,以及类之间如何彼此合作,从而达到灵活应用。


查看api文档,集合按照存储结构可以分为两大类,

  • 单列集合 java.util.Collection

  • 双列集合 java.util.Map


通过jdk api 来看在 JDK中 提供了丰富的集合类库,为了便于初学者进行系统地学习,我们通过结构图来分别描述集合类的继承体系。

编程学习,从云端源想开始,课程视频、在线书籍、在线编程、一对一咨询……你想要的全部学习资源这里都有,重点是统统免费!点这里即可查看


640 (27).gif


Collection

Description

Collection: 单列集合类的根接口,用于存储一系列符合某种规则的元素,它有两个重要的子接口:

  • java.util.List List的特点是 元素有序,元素可重复。

  • java.util.Set Set的特点是 元素无序(不全是),而且不可重复


List 接口主要的实现类有 java.util.ArrayList 和 java.util.LinkedList,Set 接口的主要实现类有 java.util.HashSet 和 java.util.TreeSet。



640 (27).gif

Map

Description

Map: 双列集合,用于存储具有映射关系的对象。常用的实现类有

  • java.util.HashMap

  • java.util.LinkedHashMap

注:图片中 小标中有 I的都是接口类型,而 C 的都是具体的实现类。


好的,框架的介绍就到这里了。本文中主要介绍了框架的两大类,以及我们在开发工作中使用的几种常见的接口和实现类,在后面的文章中,一一介绍吧。



收起阅读 »

俞敏洪:我曾走在崩溃的边缘

web
大家在人生的经历中遇到过很崩溃的事情吗? 我遇到过,遇到这类事情的时候,我会读读名人传记,看看他们有没有遇到我和我类似的事情;他们是怎么处理这些事情的;或者说他们的心路历程是怎么样的。他们的应对方式可能会对我有所启发。 长时间下来,这个习惯让我对名人的苦难经历...
继续阅读 »

大家在人生的经历中遇到过很崩溃的事情吗?


我遇到过,遇到这类事情的时候,我会读读名人传记,看看他们有没有遇到我和我类似的事情;他们是怎么处理这些事情的;或者说他们的心路历程是怎么样的。他们的应对方式可能会对我有所启发。


长时间下来,这个习惯让我对名人的苦难经历或者处理棘手问题的经历有强烈的好奇心。最近,读了俞敏洪的自传《我曾走在崩溃的边缘》,感觉挺有意思。


俞敏洪是新东方的老板,在“双减”政策之后,新东方转型做了直播,也就是大家熟知的东方甄选,可能很多人还买过他们直播间的大米。当然,我没有买过,因为理智促使我很少为情怀买单。


离开北大


俞敏洪曾经是北大的老师,他的梦想是出国留学。但老师的工资低,很难赚够出国的学费。作为南方人的他,天生的商人基因让他找到了赚钱的路子——开英语培训班。这条路子获得的收入比工资高十几倍,利润十分丰厚。


于是,他打着北大的招牌私下招生,这意味着和北大“官方”的托福培训班形成了竞争关系。学校当然不会允许北大老师和北大抢生意,况且学校禁止老师私下办培训班。俞老师无法避免地和校领导发生了冲突,并因此被处分。


图片


处分的通告在学校的高音喇叭上足足播了一个礼拜,这件事情闹得人尽皆知,对俞敏洪名声的伤害极大。后来,学校分房自然没有俞老师的份了。在中国的社会体系下,名声对一个人来说极其重要。这种“德治”社会虽然在人口大国里对秩序起着巨大的作用,但也给一些人带来了巨大伤害。一遭名声败坏,要背一辈子,这对当事人是多大的打击。


那时俞敏洪已经结婚,本可以在大学教书过安稳的生活,但这一纸处分,让他决定从北大离职。最后,他骑着三轮车拉着家当离开了北大,开启了新东方的事业。


图片


死磕办学证


办培训班需要办学证,类似于现在的牌照。如果没有就无法公开招生,这意味着无法扩大规模。俞敏洪没办法,找了当时一个叫东方大学的机构联合办培训班,条件是支付总收入的25%给东方大学。


东方大学不参与招生、培训等所有事情,却要分掉一大笔钱。随着培训班的规模越来越大,俞敏洪意识到这不是长久之计,他决定就算再难,死磕也要把办学证拿到手。


要拿到办学证要符合两个条件:一是必须有大学副教授以上职称,二是要经原单位同意。


俞敏洪在北大只是讲师,没有副教授职称,而且北京大学处分了他,不可能同意他办学。两个条件都不符合,教育局直接拒绝,并叫他不要来了。


不得不说,俞老师的脸皮是够厚的,每隔一两星期就去教育局和办事的人聊天,久了大家就混熟了。


大概耗了半年,教育局放低了办学的要求,只要他能够在人才交流中心拿到允许办学证明就放行。可是人才交流中心的工作人员根本不给他开证明。直到遇见他一个在这里工作的学生,在她的帮助下才拿到证明。


办学证到手后,俞敏洪离开东方大学,开始独立办培训班。原来的“东方大学外语培训部”这块招牌积累了相当的名气,新东方成立后,大量学生还去那边报名。为了顺利切换品牌,新的培训机构起名叫新东方,而且从东方大学买断了“东方大学外语培训部”三年的使用权,每年支付20万。


这一系列的操作,可见俞敏洪有相当不错的商业头脑。


被赶下董事长的位置


中国是一个人情社会,比如亲情、友情、同学情。在这种社会成长起来的人,自然会想到找自己熟悉的人一起做事业。俞敏洪也不例外。新东方的培训班办得风生水起,俞敏洪开始寻找人才。


除了拉亲人朋友入伙,他还出国把大学同学王强、徐小平拉回来一起跟他干事业。这三人被称为“东方三驾马车”,也就是电影《中国合伙人》的原型。


image.png


亲人、同学、朋友之间,天然有信任感,在事业的初创阶段一起工作沟通效率非常高,而且为了共同的目标,凝聚力非常强。


当公司到了一定的规模,这种人情关系构建起来的团队,会使公司的人事关系变得非常复杂。


一是,团队没有组织架构,决策效率低下;二是,老板没有话语权,下面的人不知道该听谁的,却谁都不敢得罪。


后来,在新东方改革期间,创始团队出现各种矛盾,俞敏洪无法短期内处理好这些矛盾,被管理层认为是不合格的董事长。于是,俞敏洪从位置上退了下来。


退位期间,其他几个领导轮流做主,也无法处理好团队的矛盾。俞敏洪开始大量阅读公司管理、股权管理的书籍,积累比其他领导更丰富的管理知识。两三年后,他重新回到董事长的位置上。


他能回到位置上,管理知识是一方面,我斗胆猜测,运气的成分占比很大。毕竟被自己的公司赶走的大有人在。


结尾


除了上面3个故事,俞敏洪还有很多非常精彩的故事,比如“被抢劫险些丧命”、“知识产权侵权风波”、“新东方上市”、“遭遇浑水公司做空”等等。


语言是思想的外衣。他来自农村,《我曾走在崩溃的边缘》这本书语言坦诚,像他本人一样。他的人生非常精彩,展现了他强大的韧性。


他的成功,有时代的机遇,也有个人的努力。我们可能无法准确把握时代的机遇,但可以学习他的努力和韧性,在崩溃之时屹立不倒。


作者:华仔很忙
来源:juejin.cn/post/7218487123212091450
收起阅读 »

JS小白请看!一招让你的面试成功率大大提高——规范代码

web
前言 规范的代码是可以帮你进入优秀的企业的。一个规范的代码,通常能起到事半功倍的作用。并非规范了就代表高水平,实际上是规范的代码更有利于帮助你理解开发语言理解模式理解架构,能够帮助你快速提升开发水平。今天我们就来聊聊,如何规范我们的代码,如何优化我们的代码,如...
继续阅读 »

前言


规范的代码是可以帮你进入优秀的企业的。一个规范的代码,通常能起到事半功倍的作用。并非规范了就代表高水平,实际上是规范的代码更有利于帮助你理解开发语言理解模式理解架构,能够帮助你快速提升开发水平。今天我们就来聊聊,如何规范我们的代码,如何优化我们的代码,如何使我们的代码可读性提高。


正文



  • 这里我们先放出一道面试题


输入一个数组,例如:array [1, 2, 3, 4, 5, 6, 7, 8, 9, 0],
返回一个固定格式的电话号码 例如:(123456-789

function phoneNumber(numbers){

}


  • 注释


当我们拿到这道题时,我们需要先做什么呢?是一上来直接实现这个函数的功能吗?一般人可能就这样上了,但是,如果面试官看到你这样去写代码,这样会显现你的编程素养特别差,面试官对你的好感度肯定也会随之下降。那我们应该怎么做呢?我们需要先写一个良好的注释


/**
* @func 返回固定格式的电话号码 函数功能
* @params array [1, 2, 3, 4, 5, 6, 7, 8, 9, 0]
* @return (123) 456-7890
* @author jser
*/

// 函数定义
function phoneNumber(numbers) {

}

良好的注释可以提高代码的可读性。小伙伴们要记住,代码的可读性高于一切!


在大公司中,一份代码可能会经过许多的程序员阅读或修改。如果你写了良好的注释,当你的代码被他人阅读时,其他程序员可以快速读懂这份代码,或者根据自己的需要去修改这份代码,这样大大节省了时间,也提高了团队的效率。



  • 换行


当我们把良好的注释写好之后,就可以写代码去实现功能了。


function phoneNumber(numbers) {
return "(" + numbers[0] + numbers[1] + numbers[2]+ ")" + " " + numbers[3]+numbers[4]+numbers[5] + "-" + numbers[6] + numbers[7] + numbers[8] + numbers[9]+""
}

“什么?这是什么代码,还需要我拖动去查看后面的,你可以走了!”


相信很多小伙伴看到这串代码时,也跟小编一样的头疼,这代码也看的太费劲了吧,怎么全部都挤在一行里去了。运行了一下之后,发现运行结果是对的,但是小编是不建议大家这样写代码的,我们要适当的进行换行操作,这样同样提高了代码的可读性。


function phoneNumber(numbers) {
return "(" + numbers[0] + numbers[1] + numbers[2]
+ ")" + " " + numbers[3] + numbers[4] + numbers[5]
+ "-" + numbers[6] + numbers[7] + numbers[8] + numbers[9] + ""
}

ES6


ES5和ES6都是JavaScript语言的版本。ES5是ECMAScript 5的简称。自ES6(ECMAScript2015)出来后,ES6引入了一些新的语法和关键字,使得代码更加易读、易写,提高了可维护性,例如解构赋值、箭头函数、模板字符串等。



  • 箭头函数


在ES5旧版本中,很多小伙伴会觉得function很繁琐,而且到处都是function。而箭头函数可以算是函数的简版,它的结构变得比函数简单。那我们如何使用箭头函数呢?



  1. 去掉function,在()和{}之间加=>

  2. 如果参数列表只有一个形参,可省略()

  3. 如果函数体只有一句话,可省略{},如果仅有的一句话函数体是return xxx,就必须省略return


在上面那个例子里,我们可以这样写箭头函数


phoneNumber = (numbers) =>"(" + numbers[0] + numbers[1] + numbers[2]
+ ")" + " " + numbers[3] + numbers[4] + numbers[5]
+ "-" + numbers[6] + numbers[7] + numbers[8] + numbers[9] + ""


可以看出,箭头函数的结构比我们使用旧版的函数会简单许多,小伙伴们可以选择使用啊。



  • 模板字符串


模板字符串,也称为模板字面量,是 ECMAScript 6(ES6)引入的一种新的字符串表示法。它允许在字符串中嵌入变量和表达式,使用反引号(``)包围字符串内容。与传统字符串拼接相比,模板字符串具有以下优势:


模板字符串允许在字符串中插入变量值或表达式,使用 ${} 语法。这使得代码更加清晰和可读,不需要繁琐的字符串拼接。


const name = 'junjun'; 
const greeting = `Hello, ${name}!`; // 使用模板字符串插入变量
console.log(greeting);// 输出:Hello, junjun!

那机灵的小伙伴就问了,电话号码那个例子是不是也可以使用模板字符串,我们直接上代码


phoneNumber = (numbers) => `(${numbers[0]}${numbers[1]}${numbers[2]})
${numbers[3]}${numbers[4]}${numbers[5]}
-${numbers[6]}${numbers[7]}${numbers[8]}${numbers[9]}`

//输出 (123)
// 456
// -7890

根据结果可以看到,使用模板字符串时,当我们进行换行操作时,模板字符串也会换行。模板字符串虽然方便,但小伙伴们要记住,不是在所有情况下,都可以使用模板字符串的。


总结


代码的可读性高于一切。我们作为小白,在慢慢成长的过程中,一定要尽早的规范自己的代码,提高代码的可读性,学习ES6的语法。当我们以后进入企业工作之后,公司会统一我们的代码风格,并且规定使用哪些语句。


作者:来颗奇趣蛋
来源:juejin.cn/post/7294080876827901993
收起阅读 »

Echarts添加水印

web
如果直接说水印,很难在官方找到一些痕迹,但是换个词【纹理】就能找到了。水印就是一种特殊的纹理背景。 Echarts-backgroundColor backgroundColor 支持使用rgb(255,255,255),rgba(255,255,255,...
继续阅读 »

如果直接说水印,很难在官方找到一些痕迹,但是换个词【纹理】就能找到了。水印就是一种特殊的纹理背景。



Echarts-backgroundColor


backgroundColor



支持使用rgb(255,255,255)rgba(255,255,255,1)#fff等方式设置为纯色,也支持设置为渐变色和纹理填充,具体见option.color



color


支持的颜色格式:




  • 使用 RGB 表示颜色,比如 'rgb(128, 128, 128)',如果想要加上 alpha 通道表示不透明度,可以使用 RGBA,比如 'rgba(128, 128, 128, 0.5)',也可以使用十六进制格式,比如 '#ccc'




  • 渐变色或者纹理填充


    // 线性渐变,前四个参数分别是 x0, y0, x2, y2, 范围从 0 - 1,相当于在图形包围盒中的百分比,如果 globalCoord  `true`,则该四个值是绝对的像素位置
    {
    type: 'linear',
    x: 0,
    y: 0,
    x2: 0,
    y2: 1,
    colorStops: [{
    offset: 0, color: 'red' // 0% 处的颜色
    }, {
    offset: 1, color: 'blue' // 100% 处的颜色
    }],
    global: false // 缺省为 false
    }
    // 径向渐变,前三个参数分别是圆心 x, y 和半径,取值同线性渐变
    {
    type: 'radial',
    x: 0.5,
    y: 0.5,
    r: 0.5,
    colorStops: [{
    offset: 0, color: 'red' // 0% 处的颜色
    }, {
    offset: 1, color: 'blue' // 100% 处的颜色
    }],
    global: false // 缺省为 false
    }
    // 纹理填充
    {
    image: imageDom, // 支持为 HTMLImageElement, HTMLCanvasElement,不支持路径字符串
    repeat: 'repeat' // 是否平铺,可以是 'repeat-x', 'repeat-y', 'no-repeat'
    }



水印


通过一个新的canvas绘制水印,然后在backgroundColor中添加


const waterMarkText = 'YJFicon'; // 水印
const canvas = document.createElement('canvas'); // 绘制水印的canvas
const ctx = canvas.getContext('2d');
canvas.width = canvas.height = 100; // canvas大小 - 控制水印间距
ctx.textAlign = 'center'; // 文字水平对齐
ctx.textBaseline = 'middle'; // 文字对齐方式
ctx.globalAlpha = 0.08; // 透明度
ctx.font = '20px Microsoft Yahei'; // 文字格式 style size family
ctx.translate(50, 50); // 偏移
ctx.rotate(-Math.PI / 4); // 旋转
ctx.fillText(waterMarkText, 0, 0); // 绘制水印

option = {
//...
backgroundColor: {//在背景属性中添加
// type: 'pattern',
image: canvas,
repeat: 'repeat'
}
...
}

image.png


如果只想在 toolbox.saveAsImage 下载的图片才展示水印,toolbox.feature.saveAsImage 支持配置backgroundColor,将其设置为水印【纹理】即可


option = {
//...
toolbox: {
show: true,
feature: {
...
saveAsImage: {
type: 'png',
backgroundColor: {
// type: 'pattern',
image: canvas,
repeat: 'repeat'
}
}
}
}
//...
}

graphic


除了使用纹理背景,还可以 graphic 添加图形元素,其中包括 text,可用于绘制水印。


option = {
//...
graphic: [
{
type: 'group',
rotation: Math.PI / 4,
bounding: 'raw',
top: 100,
left: 100,
z: 100,
children: [
{
type: 'text',
left: 0,
top: 0,
z: 100,
style: {
fill: 'rgba(0,0,0,.2)',
text: 'ECHARTS',
font: 'italic 12px sans-serif'
}
},
{
type: 'text',
left: 40,
top: 40,
z: 100,
style: {
fill: 'rgba(0,0,0,.2)',
text: 'ECHARTS',
font: 'italic 12px sans-serif'
}
}
]
},
],
//...
}

比较繁琐,需要自己设置平铺规律,建议封装一个平铺方法,不能控制区分 saveAsImage。


最初想象


预研


通过源码可以知道 saveAsImage 的实现也是通过内置 API getConnectedDataURL 获取url(base64格式),然后赋值到 a 标签上(带download属性)实现下载。


class SaveAsImage extends ToolboxFeature<ToolboxSaveAsImageFeatureOption> {

onclick(ecModel: GlobalModel, api: ExtensionAPI) {
const model = this.model;
const title = model.get('name') || ecModel.get('title.0.text') || 'echarts';
const isSvg = api.getZr().painter.getType() === 'svg';
const type = isSvg ? 'svg' : model.get('type', true) || 'png';
const url = api.getConnectedDataURL({
type: type,
backgroundColor: model.get('backgroundColor', true)
|| ecModel.get('backgroundColor') || '#fff',
connectedBackgroundColor: model.get('connectedBackgroundColor'),
excludeComponents: model.get('excludeComponents'),
pixelRatio: model.get('pixelRatio')
});
const browser = env.browser;
// Chrome, Firefox, New Edge
if (isFunction(MouseEvent) && (browser.newEdge || (!browser.ie && !browser.edge))) {
const $a = document.createElement('a');
$a.download = title + '.' + type;
$a.target = '_blank';
$a.href = url;
const evt = new MouseEvent('click', {
// some micro front-end framework, window maybe is a Proxy
view: document.defaultView,
bubbles: true,
cancelable: false
});
$a.dispatchEvent(evt);
}
// IE or old Edge
else {
// ...
}
}
// ...
}

思路


可以通过扩展此方法,先获取原始的图片url,基于这个图片重新绘制一个canvas,然后在这个基础上覆盖水印,最后将canvas再次转成url返回。



  • echartsInstance._api.getConnectedDataURL

  • echartsInstance.__proto__.getConnectedDataURL


含有两处实例方法需要处理。


结果



  • ✅可以拦截默认行为获取到添加水印后的图片url

  • ❌通过原api方法获取到url后绘制到新的canvas,涉及到异步处理(img标签需要等待load),由于 saveAsImage 调用 getConnectedDataURL 获取url是同步过程,因此无法正确读取到异步处理完的最终url,导致下载失败;不过,手动调用实例getConnectedDataURL 可以使用,需要配置Promise语法使用。


伪代码:


const originGetConnectedDataURL = echartsInstance._api.getConnectedDataURL
echartsInstance._api.getConnectedDataURL = async function () {
const origin = originGetConnectedDataURL.call(echartsInstance, ...arguments)
const result = await toWaterUrl(origin)
return result
}

function toWaterUrl (url) {
return new Promise(resolve => {
const img = new Image()
img.src = url + '?v=' + Math.random();
img.setAttribute('crossOrigin', 'Anonymous');
img.onload = function() {
// drawCanvas img 转 canvas
// afterWater canvas 绘制水印
resolve(afterWater(drawCanvas(img))
}
})
}

附带drawCanvas/afterWater


function drawCanvas(img) {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const context = canvas.getContext('2d');
context.drawImage(img, 0, 0);
return canvas;
}

function afterWater(canvas, { text = 'WangSu', font = 'italic 10px', fillStyle = 'red', rotate = -30 }, type = 'png') {
return new Promise((resolve, reject) => {
let context = canvas.getContext('2d');
context.font = font;
context.fillStyle = fillStyle;
context.rotate(rotate * Math.PI / 180);
context.textAlign = 'center';
context.textBaseline = 'Middle';
const textWidth = context.measureText(text).width;
for (let i = (canvas.height * 0.5) * -1; i < 800; i += (textWidth + (textWidth / 5))) {
for (let j = 0; j < canvas.height * 1.5; j += 128) {
// 填充文字,i 间距, j 间距
context.fillText(text, i, j);
}
}
resolve(canvas.toDataURL('image/' + type))
})
}

作者:wangsd
来源:juejin.cn/post/7290417906840322106
收起阅读 »

前端开发,微信公众号静默网页授权,本地调试及上线

web
1、前言 基于今天有个朋友问了我相关公众号授权的一些流程问题,今日来记录记录以及讲解一下如果是前端,没怎么了解后端,一些情况下前端自己调试静默授权一些东西,或者想自己试试自己公众号来玩一玩授权。 2、工具 既然在本地调试,那自然少不了微信开发者工具了,对,...
继续阅读 »

1、前言



基于今天有个朋友问了我相关公众号授权的一些流程问题,今日来记录记录以及讲解一下如果是前端,没怎么了解后端,一些情况下前端自己调试静默授权一些东西,或者想自己试试自己公众号来玩一玩授权。



2、工具


既然在本地调试,那自然少不了微信开发者工具了,对,没错,就是那个微信开发者工具。这玩意是微信公众号官方推出来的一款专门给用户制作微信小程序小游戏和公众号的软件。


image.png






概念: 用户在微信客户端访问第三方网页,公众号可以通过微信网页授权机制,来获取用户基本信息,进而实现业务逻辑。



题外话: 每个用户在不同的公众号openid不同。如果需要在多个公众号统一用户的账号的话,就需要UnionID在这里配同一用户,对同一个微信开放平台下的不同应用,unionid是相同的。


3、流程




🔴 微信开发者工具


前期我们需要在本地进行调试,也就是在微信开发者工具里面调试。下载完成之后,打开软件出现二维码:


image.png


微信扫码进去。


手机端出现是否确定登录微信开发者工具,点击确认登录进去。


image.png


进来,点击公众号网页项目进去,


image.png




🔴 网页授权解剖


我们来看一下文档网页授权这块,文档当中提及两种授权,


一种是不弹出授权页面,直接跳转,就能获取用户openid(scope为snsapi_base),


另外一种是弹出授权页面,可通过openid拿到昵称、性别、所在地。并且呢,即使在未关注的情况下,只要用户授权,也能获取这个用户相关信息


1、scope=snsapi_base(静默,无感知):


snsapi_basescope发起的网页授权,是用来获取进入页面的用户的openid的,并且是静默授权并自动跳转到回调页面。[用户感知的就是直接进入回调页(往往是业务页面)]


2、snsapi_userinfo(弹框,需要用户手动同意):


snsapi_userinfoscope发起的网页授权,是用来获取用户的基本信息的。[需要用户手动同意,无需关注,用户同意授权给我们去拿他们的相关基本信息]


至于第3种就是需要用户关注了公众号之后,才能得到用户的openid去得到用户的一些基本信息的,这里只讲前面两种。


无论是上面1还是2,都需要条件为已认证服务号,需要引导用户打开地址



注意:



如果在地址栏中输入https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect这个地址,直接提示改链接无法访问的话,解决(1、看参数正确与否,2、看认证没认证,也就是scope参数授权作用域权限有没有)。


🔴 链接格式


1、静默授权snsapi_base


https://open.weixin.qq.com/connect/oauth2/authorize?appid=这个公众号的appid&redirect_uri=后端跳转拿code接口地址&response_type=code&scope=snsapi_base&state=123#wechat_redirect


2、弹窗用户同意授权 snsapi_userinfo:


https://open.weixin.qq.com/connect/oauth2/authorize?appid=这个公众号的appid&redirect_uri=后端跳转拿code接口地址&response_type=code&scope=snsapi_userinfo&state=STATE#wechat_redirect


参数说明
appid公众号的appid
redirect_uri授权后重定向的回调链接地址,使用urlEncode处理
response_type返回类型,就填code就可以了
scope两个,上面👆讲过,一个静默snsapi_base,一个需同意snsapi_userinfo
state重定向会带上state参数,可填a-zA-Z0-9
#wechat_redirect必带


到最后会跳转到redirect_uri/?code=CODE&state=STATE


我们做这些授权的目的就是为了得到code,code这个玩意就是得到access_token的敲门砖,code每次授权都不一样,每次的code只能使用一次,5分钟过期



4、沙盒测试(本地调试)


🔴 配置公众号平台测试账号


image.png


在【设置与开发】-【开发者工具】-【公众平台测试账号】,点进去。


这里呢,微信官方为我们提供了一个测试号,我们本地调试的话,先这个测试号来调网页授权功能。后期部署到线上,再换成我们自己这个公众号的appid和配置线上后端的域名,这是后面本地调试没问题了,再放到线上到这一步。


测试号的appid和appsecret,到后面有用:


image.png


你就看到这里就行了,其他的不用管,看到网页服务-网页账号那里,去授权网页授权获取用户基本信息


image.png


image.png


点击修改,进去网页授权域名填写,就是你希望跳转的地址的域名,这里本地调试可以填ip:port (ip:端口)这样。本地开发不用域名,当然如果你host映射重定向到一个域名(这种就是简单东西复杂化)也不是不行,就是没必要。


做完这些配置,就到代码部分了。


🔴 代码


前端,写一个页面:


代码如下:


auth.html js部分 (直接跳)



<script>
window.location.href = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=测试号的appid&redirect_uri=" +
"http://192.168.0.57:8001/(注释:本地的调试获取code的地址)" +
"&response_type=code&scope=snsapi_base&state=1#wechat_redirect";
</script>

这里我用vscode的插件live_server跑起来的,地址是http:127.0.0.1:5501/auth.html


接收的话也是这个页面来接收code,得到code之后,就可以为所欲为了(bushi),就可以传给后端就获取token了,前端也可以。


获取token的过程:



获取code后,请求以下链接获取access_token:




请求这个接口 https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code



传参是 appidsecretcodegrant_type写authorization_code。


最后得到的json数据就是下面👇这个:


image.png


具体页面代码是这样的:


image.png


🔴 效果


Kapture 2023-10-25 at 17.55.46.gif


5、上线



此文讲的是本地调试怎么调,上线之后呢,我们就换,appid要换,把测试appid换成线上公众号appid(后台拿),网页授权域名要加(在【设置与开发】-【公众号设置】-【功能设置】-【网页授权域名】那里改成我们线上的域名,一个月只能改5次,可加两个域名)。



6、总结


没有太多花里胡哨的东西,就是通过code拿token,code怎么来,code通过跳转得来,这就完事了。



☎️ 希望对大家有所帮助,如有错误,望不吝赐教,欢迎评论区留言互相学习。感谢阅读,祝您开发有乐趣。



作者:盏灯
来源:juejin.cn/post/7293804736754106377
收起阅读 »

个人创业中的全栈开发经验

web
前言 个人项目开发创业半年有余,两个项目全部扑街,一无所获。 仔细想来其实也不是什么都没有得到,因为现状就是,我创业开始前能预想到的最坏情况,哪怕一毛钱都挣不到,但是也可以从中积累一些经验,比如微信小程序的开发经验。 我过去7年的工作都是在从事前端开发,从最开...
继续阅读 »

前言


个人项目开发创业半年有余,两个项目全部扑街,一无所获。


仔细想来其实也不是什么都没有得到,因为现状就是,我创业开始前能预想到的最坏情况,哪怕一毛钱都挣不到,但是也可以从中积累一些经验,比如微信小程序的开发经验。


我过去7年的工作都是在从事前端开发,从最开始从事IPTV 开发,用原生JS、JQuery 开发运行在机顶盒上JSP 页面;到18年,创建了项目组的第一个Vue 项目,那时候我才算是开始“现代”前端的工作;21年到上海,在新公司开始全面使用React + TS,也就是时至创业开始,我所有的工作技能,都是前端技术,后端相关的只有自己瞎折腾的项目,没有真正应用到实际项目中的,这次也算是逼着自己进步了一把。


技术选型


前端 - 后台管理系统:React + TS,用了Antd 的组件库提供的模板直接创建项目


前端 - 微信小程序:原生微信小程序开发 + Vant Weapp


服务端:微信云开发


为什么要选用以上技术栈,只有有一个原因,就是成本极低!非常低!并且很稳定,前后端全部用JS 搞定;后台管理系统部署在腾讯云的Web 应用托管上,直接免去运维工作。说个题外话,前几年自己搞个人网站的时候,服务器是薅的阿里云的羊毛,结果就是啥活都得自己干,用Express 框架开发的后端服务,用Nginx 做代理,结果并发超过100个 服务器直接挂掉。。


现在这一套技术栈,几乎没有学习成本,腾讯云的Web 应用托管集成了CI 工具,提交代码到线上分之后,自动部署,用了半年多,网站、小程序都没有挂掉过。(我真不是腾讯的托。。)


后台管理系统


React、TS、Antd 业务开发技术不多赘述,讲讲怎么在Web 端请求微信云开发的接口吧。


微信云开发提供了可访问云服务的Web Sdk,引入sdk 后,只需要进行简单的初始化,即可访问接口。


云开发登录授权配置,打开匿名登录


image.png


示例代码

处理请求


import cloudbase from "@cloudbase/js-sdk";
...
const env = ""; // 环境id
const clientId = ""; 终端id

// 创建实例
const app: any = cloudbase.init({
env,
clientId,
});

const auth = app.auth({
persistence: "local",
});

...
// 请求方法
export const cloudFn = async (
type: string,
params?: any
): Promise<any> => {
// 判断登录态
if (!auth?.hasLoginState()) {
localStorage.clear();
await auth.signInAnonymously();
}

const res = await app.callFunction({
name: "xxxx", // 云函数名称
data: { type, data: options?. }, // 传参
parse: isDev, // 环境
});

// 根据自己的业务方式处理返回数据
...


处理接口


import { cloudFn } from "@/utils";

export const xxx = (params: API.xxx) => {
return cloudFn("name", params);
};

Web Sdk 官方文档:docs.cloudbase.net/api-referen…


部署

提交代码到部署分之后,会自动部署,访问web 应用托管,会提供一个默认访问的域名,可以直接访问,但是不推荐生产使用,只需要再配置一个域名就好了。


微信小程序


如果没有开发过微信小程序,去看一下官方文档,前端基本可以无成本上手,参照官方文档开发就好;为什么组件库选择Vant Weapp,基本补全了官方没有提供的组件,使用方式也很简单,实际使用后体验不错,值得推荐。


微信云开发


我用Java、python、node 都写过后端接口,对于一个前端来说,单论简单、好上手而言,微信云开发,我愿称之为YYDS!就两个字,简单!


官方提供了请求的方法,我对其简单的封装了一下,如果觉得不错,尽管拿去用,如果有不完善的,还请指正


示例代码

云函数入口 index.js

const user = require('./user);

exports.main = async (event, context) => {
switch (event.type) {
case "userGet":
case "userUpdate":
return await user.main(event, context);


default:
return {
code: -1,
msg: '
接口不存在'
}
}
};

user 入口

const get = require("./get");
const update = require("./update");

exports.main = async (event) => {
const apiType = event.type
const data = event.data || {};

if (apiType === 'userGet') {
return await get.main(data);
};

if (apiType === 'userUpdate') {
return await update.main(data);
};
};

user/get.js

const {
dbGet, // 通用get 方法 (见后问)
filterParams, // 清除异常参数,比如空字符串,null 等
} = require("../../utils");
const check = require('./check');

exports.main = async (data) => {
// 校验参数
if (check(data)) return {
code: -1,
msg: check(data),
}

const params = {
offset: data.offset,
limit: data.limit,
name: data.name,
};


// 模糊搜索
if (params.name) {
params.name = {
$regex: ".*" + params.name,
$options: "i",
};
}

return await dbGet("user", filterParams(params));
};

user/update.js

const {
dbUpdate
} = require("../../utils");
const check = require('./check');

exports.main = async (data) => {
if (check(data)) return {
code: -1,
msg: check(data),
}


const params = {
_id: data._id,
name: data.name
};

return await dbUpdate("user", params);
};

utils.js

const cloud = require("wx-server-sdk");

// 初始化云环境
cloud.init({
env: cloud.DYNAMIC_CURRENT_ENV,
});

const db = cloud.database();

async function dbGet(
databaseTable, // 表名
params, // 参数
orderByKey = "", // 排序参数
order = "desc" // 排序方式
) {
const pageInfo = {
offset: params.offset || 1,
limit: params.limit || 10,
};
delete params.offset;
delete params.limit;

try {
// 获取总数
const resCount = await db
.collection(databaseTable)
.where(params)
.count();

const resCountData = formatRes(resCount)
if(resCount?.code !== 0) {
return resCountData
}

// 总数是0,直接返回数据
if(resCount?.data === 0) {
return {
code: 0,
data: { data: [] },
total: 0,
}
}

// 获取数据
const res = await db
.collection(databaseTable)
.where(params)
.skip((pageInfo.offset - 1) * pageInfo.limit) // 分页
.limit(pageInfo.limit) // 最多几条
.orderBy(orderByKey, order) // 排序
.get();

// 处理返回数据
const resData = formatRes(res);
if(resData?.code === 0) {
return {
code: 0,
data: {
data: resData?.data || [],
total: resCountData?.data || 0
},
}
} else {
return resData
}
} catch (error) {
return {
code: -2,
data: null,
msg: "请求失败",
};
}
}

async function dbUpdate(databaseTable, updateData) {
let res;
let isSuccess = false;
try {
const params = updateData;

if (updateData._id) {
// 编辑
delete params._id;
res = await db.collection(databaseTable).doc(updateData._id).update({
data: params,
});

if (res.errMsg === "document.update:ok") {
isSuccess = true;
}
} else {
// 新增
res = await db.collection(databaseTable).add({
data: params,
});

if (res.errMsg === "collection.add:ok") {
isSuccess = true;
}
}
if (isSuccess) {
return {
code: 0,
_id: res._id,
msg: `${updateData._id ? "更新" : "新增"}数据成功`,
};
} else {
return {
code: -1,
data: null,
msg: `${updateData._id ? "更新" : "新增"}数据失败`,
};
}
} catch (error) {
return {
code: -1,
data: null,
msg: `请求服务器失败,${updateData._id ? "新增" : "更新"}数据失败`,
};
}
}

function formatRes(res) {
const cloudFnMsgList = ["document.get:ok", "collection.get:ok", "collection.count:ok"];

if (cloudFnMsgList.includes(res?.errMsg)) {
return {
code: 0,
data: res.data || res.total,
};
} else {
return {
code: -1,
data: null,
msg: "请求服务器失败",
};
}
}

module.exports = {
dbGet,
dbUpdate,
formatRes,
};

差不多到此就结束了,Web 端的后台管理系统,微信小程序的后端接口实现了,并且可以互通,这种方式是我实践过,在保证业务、性能、稳定的前提下,最低成本的全栈开发方案,如果有其他更好的方案,欢迎讨论。


作者:鹿林秋月
来源:juejin.cn/post/7294056563631079424
收起阅读 »

说说js代码写到html里还是单独写到js文件里哪个好?为什么?

web
"将 JavaScript 代码写入 HTML 文件与将其单独写入 JavaScript 文件之间有一些考虑因素。下面是一些关于两种做法的优缺点和适用场景的讨论: 将 JavaScript 代码写入 HTML 文件的优点: 方便快捷:将 JavaScript...
继续阅读 »

"将 JavaScript 代码写入 HTML 文件与将其单独写入 JavaScript 文件之间有一些考虑因素。下面是一些关于两种做法的优缺点和适用场景的讨论:


将 JavaScript 代码写入 HTML 文件的优点:



  • 方便快捷:将 JavaScript 代码直接嵌入到 HTML 文件中可以节省创建和加载额外文件的时间,特别是对于小型项目或仅需少量 JavaScript 代码的情况。

  • 直观可见:通过将 JavaScript 代码嵌入到 HTML 文件中,可以更容易地将其与相关的 HTML 元素和结构联系起来,使代码的逻辑更加清晰明了。


将 JavaScript 代码单独写入 JavaScript 文件的优点:



  • 结构清晰:将 JavaScript 代码与 HTML 分离可以使代码结构更加清晰,提高代码的可读性和可维护性。这样做有助于保持 HTML 文件的简洁和专注于内容。

  • 可重用性:将 JavaScript 代码存储在单独的文件中,可以使其在多个 HTML 文件中重复使用,提高代码的可重用性和一致性。

  • 缓存优化:当 JavaScript 代码被单独提取到外部文件中时,浏览器可以将其缓存起来,从而提高页面加载速度并节省带宽。


综上所述,将 JavaScript 代码写入 HTML 文件适合小型项目或仅需少量 JavaScript 代码的情况,以及需要快速原型设计或简单交互的情况。而将 JavaScript 代码单独写入 JavaScript 文件适合大型项目或需要复杂的逻辑和结构的情况,以及需要提高代码的可读性、可维护性和可重用性的情况。根据项目的需求和规模,我们可以灵活选择适合的方式来组织和管理 JavaScript 代码。"


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

规范化注释你的代码,成为一名优秀程序员的必经之路!

web
前言 想要成为一名优秀的程序员,首先应该具有的是良好的编程素养,而规范化地写注释则是里面非常重要的一环。 正文 function phoneNumber(numbers) { return"("+numbers[0]+numbers[1]+numb...
继续阅读 »

前言


想要成为一名优秀的程序员,首先应该具有的是良好的编程素养,而规范化地写注释则是里面非常重要的一环。


正文


 function phoneNumber(numbers) {
return"("+numbers[0]+numbers[1]+numbers[2]+")"+' '+numbers[3]+numbers[4]+numbers[5]+'-'+numbers[6]+numbers[7]+numbers[8]+numbers[9]
}

如果我直接丢出这一串代码,你第一眼看过来,心里肯定会想“什么玩意?这一坨代码是干什么用的!居然还需要我拖动横条?!”
但是我如果在它的上方加上这样一段注释,并中途给它换行两次,它就会变成这样


/**
* @func 返回固定格式的电话号码, (123) 456-7890
* @param array [1,2,3,4,5,6,7,8,9,0]
* @return (123) 456-7890
* @author xsir
*/

函数定义
function phoneNumber(numbers) {
return"("+numbers[0]+numbers[1]+numbers[2]+")"
+' '+numbers[3]+numbers[4]+numbers[5]
+'-'+numbers[6]+numbers[7]+numbers[8]+numbers[9]
}

你一眼看过去就能清楚的看到,这个函数的作用是返回固定格式的电话号码,
调用函数需要输入的形参的样式是一个数组,返回值为固定格式的电话号码,函数的作者是xsir。


在大公司做程序开发的时候,一整个大的项目需要多人协作一同完成,所以代码的可读性就显得尤为重要,甚至可以说,代码的可读性高于一切,因为在这个时候你的代码不仅仅是写给你自己看和用的,而是整个开发团队的同志们都需要能快速看懂并且调用的。


如果别人看你写的代码时,仅仅只有代码而没有任何其他注释,那么他就需要整体的阅读你写的所有代码,才能知道你写的函数是干什么用的,这就会浪费很多时间。“Time is money, efficiency is life!”


顺带一提,如果你使用的是'${}'的格式


phoneNumber = (numbers) => `(${numbers[0]}${numbers[1]}${numbers[2]}) ${numbers[3]}${numbers[4]}${numbers[5]}-${numbers[6]}${numbers[7]}${numbers[8]}${numbers[9]}`

运行上述代码,输出的结果是这样的:


image.png


为了提高代码可读性,你对它进行了换行


phoneNumber = (numbers) => `(${numbers[0]}${numbers[1]}${numbers[2]})
${numbers[3]}${numbers[4]}${numbers[5]}
-${numbers[6]}${numbers[7]}${numbers[8]}${numbers[9]}`


那么输出结果就会变成这样


image.png


并没有达到预期的效果。所以在实战过程中使用'${}'需谨慎。


总结


在我们开发学习的过程时就要养成良好的编程素养,每次写完一块代码就写好这块代码的注释,做到看“码”知意。同时也要避免单行代码写的过长,尽量使你的代码不需要拖动横条就能看完。


作者:阡陌206
来源:juejin.cn/post/7293789288725889078
收起阅读 »

如何写出让同事崩溃的代码

web
    废话不多说,咱直接进入主题。手把手教你如何写出让帮你维护代码的同时瞬间崩溃的代码。 一、方法或变了名字随便取     首先,让同事看不懂自己代码的第一步就是,想尽办法让他看不出来我定义的变量或...
继续阅读 »

    废话不多说,咱直接进入主题。手把手教你如何写出让帮你维护代码的同时瞬间崩溃的代码。


一、方法或变了名字随便取


    首先,让同事看不懂自己代码的第一步就是,想尽办法让他看不出来我定义的变量或者方法到底是干嘛用的。哎!对,就是让他去猜,哈哈哈。
来来来,空说没意思,举个栗子图片


    假设现在想要点击某个input框时,显示一个自定义的组件用于选择选择时间。


    正常的写法如下:定义一个 toggleDatePicker 方法
image.png


    这个一看就知道是时间选择器的显示切换方法。


    但是呢,我偏不,定义成下面这样:让同事摸不着头脑,哈哈哈


image.png
当看到很多这样的方法名或变量名时,同事的表情估计时这样的图片图片图片


接下来,第二招图片图片图片


二、方法体尽可能的长,长到不能在长


    这一步至关重要,将所有逻辑全部放在一个方法中写完,坚决不分步骤,不按逻辑一步步拆分方法。让同事为我的超长方法体感到叹为观止,默默流泪。


    老规矩,上栗子


    假设现在有个方法需要处理比较复杂(需要递归,而且每层数据有不同的类型)的json格式的数据回显到页面上(这是用于拼一些条件)。数据格式大概是这样的


[
{
type: "group",
relation: "or",
conditions: [
{
type: "condition",
field: {
name: "员工状态",
id: 12345678
},
logic: "=",
val: 1,
relation: "and"
},
{
type: "condition",
field: {
name: "入职时间",
id: 12345678
},
logic: "<",
val: "2011-07-09",
relation: "and"
}
]
},
{
type: "condition",
field: {
name: "入职时间",
id: 12345678
},
logic: "<",
val: "2001-07-09",
relation: "and"
}
]

    由于上面的 json 数组格式中是一个对象,对象都有 type 属性,一种是 group 类型(表示是一组条件),一种是 condition 类型(表示是真正的条件内容)。


    因此,正常情况下,我们应该是遍历数组,不同的类型分别用不同的方法去处理,并且在处理条件中各个部分的时候分别处理。如下所示:


image.png


image.png


    但是呢?咱主题是啥,让同时崩溃啊,怎么能把方法拆成这样清晰的逻辑呢。图片图片图片


    来给你看看怎么让同事对你的代码叹为观止


image.png


image.png


image.png


image.png


image.png


image.png


    怎么样,牛不牛,一个方法写完所以逻辑,从1825行一口气写到2103行,足足... 2103 - 1825 是多少来着,3减5 不够,向前借位 ,嗯。。。278 行。
****怎么样,有没有被哥的机智震惊到,如此代码。同事看到肯定心生敬佩,连连称绝。此时想到得到同事应该是这样的表情 图片图片图片


    同事还没进到方法体里面,就已经被我的 迷之方法名 和 超长方法体 所折服,接下来就让他在方法体里面快乐的遨游吧
    


    接下来,继续让同时崩溃。


三、坚决不定义统一的变量


    这个怎么说呢,就是因为有点懒,所有很多代码直接复制粘贴,多么的方便快捷。


    正常情况下,如果某个我们需要的对象在是其他对象的属性,并且层次很深,我们先定义一个变量来接收这个对象,再对这个对象操作。


例如:


let a = {
b: {
c: {
d: {
name: "我是最里面的对象"
}
}
}
}

    我们要对d对象进行很多次的操作时,一般先将d赋值给一个变量,然后对变量操作。如下:


var dOfA = a.b.c.d;
dOfA.name = "我现在被赋值给dOfA ";
dOfA.data = 1;
dOfA.other = false;

    但是呢,我就不这么干,就是要写得整整齐齐


a.b.c.d.name = "就要这么干,你打我呀";
a.b.c.d.data = 1;
a.b.c.d.other = false;

    老规矩,没有 实际的 栗子 怎么能说的形象呢,上图


    正常写法:


image.png


    我偏要这么写


image.png


    多么的整齐划一,


    全场动作必须跟我整齐划一


    来左边儿 跟我一起画个龙


    在你右边儿 画一道彩虹


    来左边儿 跟我一起画彩虹...


    咋突然哼起歌来了,不对,咱是要整同事的,怎么能偏题。


    继续,此时同事应该是这个表情图片图片图片


    然后,方法体里面只有这么点东西怎么够玩,继续 come on


四、代码能复制就复制,坚决不提成公用的方法


    代码能 CV ,干嘛费劲封装成方法,而且这样不是显得我代码行数多吗?图片图片图片


    就是玩儿,就是不封装


    来,上栗子


image.png


image.png


image.png


    看到没有,相同的代码。我在 1411行 - 1428行 写了一遍, 后面要用,在1459行-1476行复制一遍


    这还不够?那我在1504-1521行再复制一遍


    这下,爽了吧,哈哈哈


    就是不提方法,就是玩儿,哎! 有意思


    这个时候同事估计是这样的吧图片图片图片


    怎么样,是不是很绝?不不不,这算啥


    虽然以上这些会让看我代码的同事头疼,但是,只要我在公司,他们还会让我改啊。怎么能搞自己呢。


    最后一步


五、离职


    洋洋洒洒的写完代码,尽早离开。够不够绝,哈哈哈


六、申明:


    以上场景纯属个人虚构的,单纯为了给文章增加点乐趣。写这个文章的目的是让各位程序员兄弟尽量避免写这种难以维护的代码。真的太痛苦了!代码质量、代码重构真的是编程过程中很重要的一个步骤。不能抱着能用就行的心态。还是要对自己有一定的要求。只有你看得起自己的代码,别人才有可能看得起你的代码。加油吧!各位


作者:猩兵哥哥
来源:juejin.cn/post/7293888785400856628
收起阅读 »

一文学会请求中断、请求重发、请求排队、请求并发

web
大家好,今天我们来聊聊前端开发中的网络请求,顺便也来体验一下promise的神奇之处! 以下示例是基于axios@1.5.1进行开发,在一些低版本中的一些用法可能不太一样,建议安装新版进行测试。 阅读下文需要了解前置知识:promise、class、axios...
继续阅读 »

大家好,今天我们来聊聊前端开发中的网络请求,顺便也来体验一下promise的神奇之处!

以下示例是基于axios@1.5.1进行开发,在一些低版本中的一些用法可能不太一样,建议安装新版进行测试。

阅读下文需要了解前置知识:promise、class、axios


请求中断


1.判定相同请求:请求url、请求方法、请求params参数、请求 body参数,四个值都相同,则认为是一个相同的请求。

2.判断请求中断:在上一个相同请求还没有得到响应前,再次请求,则会自动中断。


image.png


请求重发(无感刷新token)


1.当前请求返回401时,执行刷新token。

2.当同时存在多个请求返回401时,可在类中维护一个静态变量保存请求刷新接口的promise,防止多次调用刷新token。

3.RetryRequest类实例化参数:

   instance:请求实例对象

   success:刷新成功回调函数

   error:刷新失败回调函数

image.png


请求排队



  1. queue:请求等待队列。

  2. isWating:是否正在等待上个请求响应。

  3. add:向队列里加入一个等待请求的promise的resolve方法,执行该方法可立刻发送下一个请求 。

  4. next:执行下一个请求方法,在上一个请求响应后调用。

    image.png


响应处理


image.png


axios请求实例


image.png


测试


1.请求中断测试


快速点击test请求按钮多次


image.png


2.刷新token请求重发测试


(1)当用户没有登录请求接口时


image.png


(2)当用户登录后,accessToken过期,但refreshToken还没过期调用接口时


image.png
在调用刷新token接口成功后,将重发失败的test接口


(3)当refreshToken过期后调用接口时


image.png
这时已经无法刷新token了,只能乖乖跳转到登录页面了。


3.请求排队测试


(1)没有使用请求排队时


场景:当输入框输入关键字实时查找内容时,由于网络原因,可能会出现先请求的后响应的请求,导致请求错乱。

如下,模拟网络请求延迟:
image.png


当输入框依次输入1、2、3、4、5时,期望的返回结果应该是1,12,123,1234,12345。
但确得到了以下的结果:
image.png


(2)使用请求队列时


在网络请求的waterfall列可以清晰看到,当上一请求完成才会执行下一请求,直到等待队列执行完成。
image.png


源码


后端接口


var express = require('express');
var router = express.Router();

const access_token = 'access_token'
const refresh_token = 'refresh_token'
// token有效期(单位毫秒)
const tokenValidTime = 1000*2
// 刷新token有效期
const refreshTokenValidTime = 1000*5
// 登录时间,模拟token过期
let loginTime;
// 模拟判断token是否过期
const IsTokenExpired = () => {
if(new Date().getTime() > loginTime + tokenValidTime) {
return true
}
return false
}
router.post('/login', (req, res) => {
loginTime = new Date().getTime()
res.json({
access_token,
refresh_token
})
})

router.post('/refresh-token', (req, res, next) => {
const refreshToken = req.headers.authorization
console.log('refresh-token', refreshToken)
if(refreshToken !== refresh_token) {
return res.status(401).json({
msg: ' refreshToken不正确!'
})
}
if(new Date().getTime() > loginTime + refreshTokenValidTime) {
return res.status(401).json({
msg: ' refreshToken已过期,请重新登录!'
})
}
loginTime = new Date().getTime()
res.json({
access_token: 'access_token',
refresh_token: 'refresh_token'
})
})

router.get('/test', (req, res, next) => {
const token = req.headers.authorization
if(token !== access_token) {
return res.status(401).json({
msg: '没有访问权限'
})
}
if(IsTokenExpired()) {
return res.status(401).json({
msg: 'token已过期'
})
}
res.json({
name: '哈哈'
})
})

router.get('/random', (req, res) => {
const keyword = req.query.keyword
setTimeout(() => {
res.json({
value: keyword
})
// 5秒内随机返回,测试网络请求延迟效果
}, Math.random()*5000);
})

module.exports = router;

前端


Index.js


import axios from "axios"
import AbortRequest from './hooks/AbortRequest'
import ResponseHanlder from "./hooks/ResponseHanlder"
import { getAccessToken, getRefreshToken } from '@/utils'
import { refreshTokenUrl } from '@/api/urls'
import { useRequestKey } from "./hooks/useRequestKey"
import RequestQueue from "./hooks/RequestQueue"
import { isAddQueue } from '@/api'

export const baseURL = '/api'
const timeout = 6000

// 创建axios实例
const instance = axios.create({
baseURL,
timeout
});

// 创建中断请求控制器
const abortRequest = new AbortRequest()
// 创建响应处理器
const responseHandler = new ResponseHanlder(instance)
// 创建请求队列排队实例
const requestQueue = new RequestQueue()

// 添加请求拦截器
instance.interceptors.request.use(function (config) {
console.log('在发送请求之前做些什么', config)
// 在发送请求之前做些什么
if(config.url !== '/login') {
const token = config.url === refreshTokenUrl ? getRefreshToken() : getAccessToken()
config.headers.Authorization = token
}
// 刷新token接口不用创建取消请求,已经再RetryRequest类维护静态属性
if(config.url !== refreshTokenUrl) {
abortRequest.create(useRequestKey(config), config)
}
// 加入请求等待队列
if(isAddQueue(config)) {
return requestQueue.add(config.url, config)
}
return config
}, function (error) {
// 对请求错误做些什么
return Promise.reject(error);
});

// 添加响应拦截器
instance.interceptors.response.use(function (response) {
const config = response.config
console.log('响应成功', response)
abortRequest.remove(useRequestKey(config))
if(isAddQueue(config)) {
requestQueue.next(config.url, config)
}
// 2xx 范围内的状态码都会触发该函数。
// 对响应数据做点什么
return responseHandler.success(response)
}, function (error) {
const config = error.config
console.log('响应错误', config)
if(config) {
abortRequest.remove(useRequestKey(config))
}
if(isAddQueue(config)) {
requestQueue.next(config.url, config)
}
// 超出 2xx 范围的状态码都会触发该函数。
// 对响应错误做点什么
if(axios.isCancel(error)) {
return Promise.reject('已取消重复请求!')
}
return responseHandler.error(error)
});

export default instance;

AbortRequest.js


// 重复请求中断类
class AbortRequest {
constructor() {
// 请求中断控制器集合
this.list = new Map()
}
// 创建中断请求控制器
create(key, config) {
const controller = new AbortController();
config.signal = controller.signal
// 集合中存在当前一样的请求,直接中断
if(this.list.has(key)) {
controller.abort()
} else {
this.list.set(key, controller)
}
}
// 请求完成后移除集合中的请求
remove(key) {
this.list.delete(key)
}
}
export default AbortRequest

RequestQueue.js


/**
* 相同url请求队列,排队执行维护类
*/

class RequestQueue {
constructor() {
// 请求等待队列
this.queue = {}
// 正在等待上一请求执行中
this.isWating = false
}
add(url, config) {
return new Promise((resolve) => {
const list = this.queue[url] || []
if(this.isWating) {
// 当前请求url存在等待发送的请求,则放入请求队列
list.push({ resolve: () => resolve(config) })
} else {
// 没有等待请求,直接发送
resolve(config)
this.isWating = true
}
this.queue[url] = list
console.log('list', list)
})
}
// 响应处理
next(url) {
this.isWating = false
// 拿出当前请求url的下一个请求对象
if(this.queue[url]?.length > 0) {
const nextRequest = this.queue[url].shift()
// 执行请求
nextRequest.resolve()
}
}
}

export default RequestQueue

RetryRequest.js


/**
* 无感刷新token类
*/

class RetryRequest {
// 解决存在多个并发请求时,重复调用刷新token接口问题
static refreshTokenPromise = null
constructor({
instance, // axios实例
success, // 刷新token成功执行的回调函数
error // 刷新token失败执行的回调函数
}
) {
this.instance = instance
this.success = success
this.error = error
}
/**
* @param config 当前请求对象,等待token刷新完成再重复执行
* @param refreshTokenApi 刷新token方法
*/

useRefreshToken(config, refreshTokenApi) {
if(!config.headers.Authorization) {
this.error()
return Promise.reject('token不存在!')
}
return new Promise((resolve, reject) => {
if(!RetryRequest.refreshTokenPromise) {
// refreshTokenPromise不为null,则当前正在执行刷新token方法,不再重复调用
RetryRequest.refreshTokenPromise = refreshTokenApi()
}
RetryRequest.refreshTokenPromise.then(res => {
// 刷新token成功
this.success(res)
// 重新发送请求
this.instance(config).then(data => {
resolve(data)
}).catch(err => {
// 重发失败
reject(err)
})
}).catch(err => {
// refreshToken失效或刷新token失败
this.error()
reject(err)
}).finally(() => {
// 刷新token调用完成,重置
RetryRequest.refreshTokenPromise = null
})
})
}
}

export default RetryRequest

ResponseHanlder.js


import RetryRequest from './RetryRequest'
import { refreshToken as refreshTokenApi } from '@/api/index'
import { getRefreshToken, setRefreshToken, setAccessToken } from '@/utils'
import { refreshTokenUrl } from '@/api/urls'

/**
* 响应处理类
*/

class ResponseHanlder {
constructor(instance) {
this.retryRequest = new RetryRequest({
instance,
success: (res) => {
const { access_token, refresh_token } = res
setAccessToken(access_token)
setRefreshToken(refresh_token)
},
error: () => {
console.log('刷新token失败!')
// 执行失败逻辑...
}
})
}
// 请求正常响应方法
success(response) {
// 对响应数据做处理
return response.data
}
// 请求错误响应方法
error(error) {
const status = error.response?.status
// 当前返回401,且不是调用刷新token接口响应的(避免后端刷新token失败返回401导致死循环的情况)
if(status === 401 && error.config.url !== refreshTokenUrl) {
return this.retryRequest.useRefreshToken(
error.config,
() => refreshTokenApi(getRefreshToken())
)
} else {
return Promise.reject(error.response)
}
}
}

export default ResponseHanlder

request.js


import instance from './index'

class Request {
constructor() {

}
get(url, params, args) {
return instance.get(url, {
params,
...args
})
}
delete(url, params) {
return instance.get(url, {
params
})
}
post(url, data) {
return instance.post(url, data)
}
put(url, data) {
return instance.put(url, data)
}
}

export default new Request();

结语


还有一个控制请求并发数量还没进行扩展,相信大家了解了请求排队的思路后,实现请求并发控制也不是什么难事了。



无感刷新token参考文章:juejin.cn/post/728974…



作者:云上客人
来源:juejin.cn/post/7293806405650464808
收起阅读 »

判断鼠标从哪个方向进入元素

web
我们需要实现的效果图 理清需求 拿到效果图的第一步,理清下需求~ 首先,元素有左右上下四个方向。这边的问题在我如何在一个元素上划分上下左右四个区域? 然后,鼠标进入元素和离开元素会有触发一个事件,这个简单js就自带了监听事件。 最后,最难的是如何判断鼠标进...
继续阅读 »

我们需要实现的效果图


image.png


理清需求


拿到效果图的第一步,理清下需求~



  • 首先,元素有左右上下四个方向。这边的问题在我如何在一个元素上划分上下左右四个区域?

  • 然后,鼠标进入元素和离开元素会有触发一个事件,这个简单js就自带了监听事件。

  • 最后,最难的是如何判断鼠标进入的时候会落在我们划分好的上下左右四个区域?


思路



  • 首先我们先来划分下四个区域,一般划分的都如下图


image.png




  • 图里面有四个三角形,每个三角形代表的是一个方向,所以问题简化为如何在一个矩形里,根据对角线划分区域。由于元素存在坐标系,也就是X、Y轴,所以问题再次抽象成,如何得到两条对角线的线性函数。(初高中数学问题。)




  • 最后的问题我们就要来搞定判断鼠标落点的问题,首先我们知道我们可以在元素的鼠标事件中通过event得到鼠标的pageX和pageY,再配合元素的offsetLeft和offsetTop就可以得到鼠标在元素中的坐标。综合一下就变成了,我有一个坐标,且我知道对角线的函数表达式,请问我如何知道我这个坐标是在函数的下面还是上面?




  • 当然也许描述的比较抽象,我们可以类比一个例子,我现在有一个坐标(2,1),有一个函数y=x,值域大于0(既y>0),定义域大于0(既x>0),求该坐标在y=x的函数下面还是上面?(是不是感觉到了线性规划得到最优解的味道,对,少年,没有错,就是这样。)这里我们只要把坐标中的x值代入函数,然后判断代入的结果是否大于坐标的y值,如果大于则在函数下面,小于则在函数上面,什么?你问等于怎么办?当然是在函数上面,该坐标即在上面又在下面,所谓薛定谔的坐标是也(当然是在函数上了)。




  • 然后我们是不是可以扩展下,如果存在多个函数,再加上逻辑判断经常用的交集,并集是不是又有新的思维出现了呢?好了,这边就不再扩展了,下面直接上实现代码吧。




实现代码


注意:该demo只是简单的demo,其中有很多可以优化的地方,比如组件化,变量优化,利用发布订阅模式,实现事件联动


<!DOCTYPE html>
<html lang="ch">
<head>
<meta charset="UTF-8">
<title>Document</title>
<style type="text/css" >
.ct{
height: 100px;
width: 100px;
border:1px red solid;
}
</style>
</head>
<body>
<div class="ct" onmouseover="fun1(event);" onmouseout="fun2(event);">

</div>
<script type="text/javascript">
//当然这样绑定事件函数是不对的
var div=document.getElementsByTagName("div")[0];
function fun1(event){
var x=event.pageX-div.offsetLeft;//(得到鼠标在框中的坐标)
var y=event.pageY-div.offsetTop;//(得到鼠标在框中的坐标)
var H=div.clientHeight;
var W=div.clientWidth;
var k=Math.floor(H/W);//为了防止不能整除
//得到2个斜边函数
//设y=ax+c
//(0,0) (width,height)其中一个斜边过这两点
//a=height/width,c=0
//y=(height/width)*x;
//(0,height) (width,0)另外一个斜边过这两点
//a=-height/width,c=height
//y=-(height/width)*x+height
if((k*x)>=y && (H-k*x)>=y){//这是判断从上方进入,这边简化处理不对等于情况做特别处理
console.log("从上方进入");
//todo
}

if((k*x)<y && (H-k*x)<y){
console.log("从下方进入");
//todo
}

if((k*x)<y && (H-k*x)>y){
console.log("从左边进入");
//todo
}

if((k*x)>y && (H-k*x)<y){
console.log("从右边进入");
//todo
}

}
function fun2(event){
var x=event.pageX-div.offsetLeft;//(得到鼠标在框中的坐标)
var y=event.pageY-div.offsetTop;//(得到鼠标在框中的坐标)
var H=div.clientHeight;
var W=div.clientWidth;
var k=Math.floor(H/W);//为了防止不能整除
//得到2个斜边函数
//设y=ax+c
//(0,0) (width,height)其中一个斜边过这两点
//a=height/width,c=0
//y=(height/width)*x;
//(0,height) (width,0)另外一个斜边过这两点
//a=-height/width,c=height
//y=-(height/width)*x+height
if((k*x)>=y && (H-k*x)>=y){//这是判断从上方进入,这边简化处理不对等于情况做特别处理
console.log("从上方离开");
//todo
}

if((k*x)<y && (H-k*x)<y){
console.log("从下方离开");
//todo
}

if((k*x)<y && (H-k*x)>y){
console.log("从左边离开");
//todo
}

if((k*x)>y && (H-k*x)<y){
console.log("从右边离开");
//todo
}

}
</script>
</body>
</html>

作者:洛漓
来源:juejin.cn/post/7293820517374820352
收起阅读 »

彻底搞懂闭包

web
每次面试都问,每次都背;每次都背的不错,每次都不太理解。 定义 闭包是一个函数和对其周围状态的引用捆绑在一起,这样的组合就是闭包。闭包让我们可以在一个内层函数中访问到其外层函数的作用域。 一个简单的例子认识闭包: function init() { var...
继续阅读 »

每次面试都问,每次都背;每次都背的不错,每次都不太理解。


定义


闭包是一个函数和对其周围状态的引用捆绑在一起,这样的组合就是闭包。闭包让我们可以在一个内层函数中访问到其外层函数的作用域。


一个简单的例子认识闭包:


function init() {
var name = 'wendZzzoo';
function getName() {
console.log(name)
}
getName()
}
init()


使用场景


那闭包有什么作用呢?但从上面这个简单的例子中,似乎很难发现这样写,也就是闭包这样的写法的用途。


数据封装和隐藏


通过使用闭包,可以创建一个作用域限定的环境,以保护变量不受外部的访问和修改。这样可以防止变量被意外修改,避免命名冲突和全局污染,提高代码的可维护性和可读性。


举个例子,定义一个计数器函数,用来某些场景下计算次数。


没有使用闭包的示例


let count = 0
function increment() {
count++
console.log(count)
}
increment()
increment()


上述代码是实现了计数器的需求,但是代码存在风险,count变量是全局定义的,在后续开发中或者是其他人维护时可以轻易修改这个变量,导致bug出现。


使用闭包的示例


function createCounter() {
let count = 0;
function increment() {
count++;
console.log(count);
}
return increment;
}

const counter = createCounter();
counter();
counter();


在上述示例中,createCounter函数返回了一个内层函数increment,该函数可以访问并递增count变量。外层函数的作用域被封装在闭包中,外部无法直接访问和修改count变量。


这里可以衍生思考一下,count变量封装在闭包中只能递增,外部无法修改,那该如何重置或者递减count呢?


其实需要新增的逻辑也可以封装到闭包里,以重置count为例:


const counterModule = (function() {
let count = 0;
function increment() {
return ++count;
}
function reset() {
count = 0;
}
return {
increment,
reset
};
})();

console.log(counterModule.increment());
console.log(counterModule.increment());
counterModule.reset();
console.log(counterModule.increment());


在上面的例子中,使用立即调用函数表达式(IIFE)创建了一个闭包,内部定义了count变量和两个操作它的函数incrementreset。通过返回一个包含这些函数的对象,实现了对count变量的封装和控制。


保持数据状态


通过闭包,内层函数可以访问和持有外层函数的变量,即使外层函数执行完毕,这些变量依然存在于内层函数的词法环境中,从而实现了数据状态的保持。


这个使用场景可以算是上一个的延申,在上述示例代码中添加传参,就可以起到了数据状态保持的目的。


函数柯里化


闭包使得函数可以返回另一个函数作为结果,从而形成函数工厂的模式。通过在内层函数中访问外层函数的参数或变量,可以创建具有不同参数或上下文的函数。这种技术称为柯里化,其目的在于避免频繁调用具有相同参数函数的同时,又能够轻松的重用。


举个例子,定义一个求矩形面积的函数。


不使用柯里化的示例


function getArea(width, height) {
return width * height
}
const area1 = getArea(10, 20)
console.log(area1)
const area2 = getArea(10, 30)
console.log(area2)
const area3 = getArea(10, 40)
console.log(area3)


上面代码里,假设我们需要这个计算矩形面积的函数,来计算宽度总是10的多种情况,那就需要多次调用getArea函数传入相同的宽度参数,且在维护的时候,假设需要统一修改宽度为20,就需要重复修改每一次调用时宽度的传参,这样重复的工作在力求优雅的情况下看来是不合适的。


使用闭包柯里化的示例


function getArea(width) {
return height => {
return width * height
}
}

const getTenWidthArea = getArea(10)
const area1 = getTenWidthArea(20)
console.log(area1)
const area2 = getTenWidthArea(30)
console.log(area2)
const area3 = getTenWidthArea(40)
console.log(area3)


如果有需要宽度改变的情况,也可以轻松复用


const getTwentyWidthArea = getArea(20)


再举个例子,定义一个打印日志的函数。


function createLogger(prefix) {
function logger(log) {
console.log(`${prefix}: ${log}`);
}
return logger;
}

const exportWarnning = createLogger('warnning');
exportWarnning('这是一个警告日志');

const exportError = createLogger('error');
exportError('这是一个错误日志');


通过调用createLogger函数并传递不同的参数,可以创建具有不同日志前缀的logger函数。


通过上述两个柯里化的例子,可以发现柯里化是一种技术更多是一种提倡,使用这样的技术可以让你的代码更有维护性。


模拟私有化方法


私有方法是将某些函数或变量限定在一个作用域内,外部无法直接访问。


function makeCounter() {
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCounter;
}
}
}

var Counter1 = makeCounter();
var Counter2 = makeCounter();
console.log(Counter1.value());
Counter1.increment();
Counter1.increment();
console.log(Counter1.value());
Counter1.decrement();
console.log(Counter1.value());
console.log(Counter2.value());


上述代码通过使用闭包来定义公共函数,并令其可以访问私有函数和变量,这种方式也叫模块方式


两个计数器 Counter1 和 Counter2 是维护它们各自的独立性的,每次调用其中一个计数器时,通过改变这个变量的值,会改变这个闭包的词法环境,不会影响另一个闭包中的变量


注意事项


闭包是一种强大的特性,但滥用闭包可能导致代码可读性和性能方面的问题,因此需要注意的是:



  1. 避免不必要的闭包,只有在确实需要保留状态或隐藏数据时才使用闭包。不要为了使用闭包而创建不必要的函数嵌套,盲目使用闭包并不会让你的代码看起来更高级。

  2. 要注意内存管理,闭包会持有对外部作用域的引用,可能导致内存泄漏。确保在不再需要闭包时,手动解除对外部作用域的引用,以便垃圾回收器能够正确处理。

  3. 特别要小心循环中的闭包,闭包会捕获循环变量的引用,可能导致意外结果。可以使用立即调用函数表达式(IIFE)或函数绑定来解决。

  4. 换一种解决方案,可以使用模块模式,如果需要封装私有方法和变量,考虑使用模块模式或其他模块化工具,如ES6模块。这样可以更清晰地定义私有和公共部分,并提供更好的可维护性和可测试性。


内存泄漏


闭包可以引起内存泄漏的情况,通常是涉及对外部作用域的引用。当函数形成闭包时,它会持有对其包含作用域的引用,这可能导致无法释放被闭包引用的内存。


可能导致内存泄漏的情况:



  1. 未及时释放闭包,如果闭包持有对外部作用域的引用,但不再需要使用闭包时,如果没有显式地解除对外部作用域的引用,闭包将继续存在并持有外部作用域中的变量。

  2. 当闭包和其包含作用域之间存在循环引用时,可能导致内存泄漏。例如,如果闭包中引用了一个对象,而该对象又持有对闭包的引用,这将导致它们互相引用,无法被垃圾回收。

  3. 闭包中引用了全局变量,闭包将一直存在,即使在不再需要闭包时也无法释放。这种情况下,全局变量将一直保持活动状态,无法被垃圾回收。


为避免闭包引起的内存泄漏,建议:



  1. 及时解除引用,当不再需要使用闭包时,确保手动解除对外部作用域的引用。只需要将闭包中引用的变量设置为 null 或重新分配其他值,以便垃圾回收器能够正确处理。

  2. 尽量避免闭包和其包含作用域之间的循环引用。确保在闭包中不引用外部对象,或者在外部对象中不引用闭包,以避免循环引用导致的内存泄漏。

  3. 只在确实需要保留状态或隐藏数据时使用闭包,在不需要闭包的情况下,使用适当的作用域(例如局部变量或模块作用域)来防止不必要的内存占用。


内存泄漏的发生并不一定是由闭包引起的,还可能涉及其他因素,但是,闭包在不正确使用的情况下容易导致内存泄漏问题。


作者:wendZzoo
来源:juejin.cn/post/7293805895918207026
收起阅读 »

📷纯前端也可以实现「用户无感知录屏」?

web
前言 要在 JavaScript 中实现屏幕录制,可以使用 navigator.mediaDevices.getDisplayMedia() 方法来获取屏幕的媒体流。然后,可使用 MediaRecorder 对象将媒体流录制为视频文件。 但该方法会在浏览器弹出...
继续阅读 »

前言


要在 JavaScript 中实现屏幕录制,可以使用 navigator.mediaDevices.getDisplayMedia() 方法来获取屏幕的媒体流。然后,可使用 MediaRecorder 对象将媒体流录制为视频文件。


但该方法会在浏览器弹出一个授权窗口,让用户选择要分享的内容,这不可实现“无感知”。


image.png


如果真正做到无感知,那我们就不能借助浏览器或者系统系统的能力了。我们能做的就只能是通过js去操作了。


要在页面内直接录制视频似乎并不容易,没有现成的开源库可以使用,也没有很好的想法。


那我们换一个思路,视频是由帧组成的,我们是否可以不断的截图,然后组合成一段视频?好像是可以的。


下载.jpeg


效果


mp4.gif


页面


先写一个简单的页面:


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

<head>
<meta charset="UTF-8">
<title>Canvas视频录制</title>
<link rel="stylesheet" href="styles.css">
</head>

<body>
<main>
<div class="buttons">
<button class="start-btn">开始录制</button>
<button class="pause-btn">暂停录制</button>
<button class="resume-btn">继续录制</button>
<button class="stop-btn">结束录制</button>
</div>
<div id="box">
<section class="content">
<h2>TODO LIST</h2>
<div class="background-div">
<button class="background-btn">切换背景颜色</button>
</div>
<div id="todo-form">
<input type="text" class="input-field" placeholder="输入待办事项">
<button type="submit" class="submit-btn">提交</button>
</div>
<div class="list"></div>
</section>
</div>
<img src="" alt="" class="hidden">
</main>

<script src="<https://cdn.bootcss.com/html2canvas/0.5.0-beta4/html2canvas.min.js>" defer></script>
<script src="canvas.js" defer></script>
</body>

</html>

截图


实现网页的截图操作,最常用的库是 html2canvas用,它可以将网页中的 HTML 元素转换为 Canvas 元素,并将其导出为图像文件。在浏览器中捕获整个页面或特定区域的截图,包括 CSS 样式和渲染效果。


const canvasFunction = () => {
html2canvas(box).then(canvas => {
const imgStr = canvas.toDataURL("image/png");
img.src = imgStr;
img.onload = function () {
ctx.drawImage(img, 0, 0, w, h);
}
});
}

合成视频


这里我们要使用到一个 API MediaRecorder ,用于在浏览器中进行音频和视频的录制。它提供了一种简单的方式来捕获来自麦克风、摄像头或屏幕的媒体数据,并将其保存为文件或进行实时流传输。


它有以下几个常用的方法:



  • isTypeSupported() 返回一个 Boolean 值,来表示设置的 MIME type 是否被当前用户的设备支持。

  • start() 开始录制媒体,这个方法调用时可以通过给 timeslice 参数设置一个毫秒值,如果设置这个毫秒值,那么录制的媒体会按照你设置的值进行分割成一个个单独的区块,而不是以默认的方式录制一个非常大的整块内容。

  • pause() 暂停媒体录制。

  • resume() 继续录制之前被暂停的录制动作。

  • stop() 停止录制。同时触发 dataavailable 事件,返回一个存储 Blob 内容的录制数据。之后不再记录。


首先创建一个 canvas 元素,用来保存 html2canvas 的截图,然后通过 captureStream 方法实时截取媒体流。


const w = boxBoundingClientRect.width;
const h = boxBoundingClientRect.height;
const canvas = document.createElement('canvas');
canvas.setAttribute('id', 'canvas');
canvas.setAttribute('width', w);
canvas.setAttribute('height', h);
canvas.style.display = 'none';
box.appendChild(canvas);

const img = document.querySelector('img');
const ctx = canvas.getContext("2d");
const allChunks = [];
const stream = canvas.captureStream(60); // 60 FPS recording 1秒60帧

通过 canvas 的流来创建一个 MediaRecorder 实例,并在 ondataavailable 事件中保存视频信息:


const recorder = new MediaRecorder(stream, {
mimeType: 'video/webm;codecs=vp9'
});

recorder.ondataavailable = (e) => {
allChunks.push(e.data);
};

最后,在停止录制时将帧信息创建 blob 并插入到页面上:


recorder.stop();
const fullBlob = new Blob(allChunks);
const videoUrl = window.URL.createObjectURL(fullBlob);

const video = document.createElement('video');
video.controls = true;
video.src = videoUrl;
video.muted = true;
video.autoplay = true;
document.body.appendChild(video);

或者可以将视频下载


recorder.stop();
const fullBlob = new Blob(allChunks);
const videoUrl = window.URL.createObjectURL(fullBlob);

let link = document.createElement('a');
link.style.display = 'none';
let fullBlob = new Blob(allChunks);
let downloadUrl = window.URL.createObjectURL(fullBlob);
link.href = downloadUrl;
link.download = 'canvas-video.mp4';
document.body.appendChild(link);
link.click();
link.remove();

这里,为了节省资源,只在点击按钮、输入等事件发生时才调用 html2canvas 截图 DOM。


如果实时记录屏也可以使用 requestAnimationFrame


最后


虽然实现了无感知录制屏幕,但也仅限于网页内,没有办法录制网页以外的部分。


以上的 demo 中只实现了 DOM 的录制,如果想要录制鼠标轨迹,可以增加一个跟随鼠标的元素~


作者:Mengke
来源:juejin.cn/post/7293462197386592283
收起阅读 »

流金岁月

web
本人是95前端菜鸟一枚,目前在广州打工混口饭吃。刚好换了工作,感觉生活节奏变得慢了下来,打了这么多年工总觉得想纪录些什么,怕以后自己老了忘记自己还有这么一些风流往事。书接上回。 小聚 “这里!这里!”我朝着声音望去,便看到小白兴奋的向我招着手,我小步快跑的走...
继续阅读 »

本人是95前端菜鸟一枚,目前在广州打工混口饭吃。刚好换了工作,感觉生活节奏变得慢了下来,打了这么多年工总觉得想纪录些什么,怕以后自己老了忘记自己还有这么一些风流往事。书接上回。



小聚


“这里!这里!”我朝着声音望去,便看到小白兴奋的向我招着手,我小步快跑的走了过去,在小白对面落了座。“少爷阔气,今天怎么请我来这里吃饭?”我问出心中疑虑,璇玑地中海自助旋转餐厅,位于广州塔106层,从窗户放眼望去,晚霞与珠江美景浑天然一色,无数高楼一览无遗,万家灯火如星光皆纳入眼前,最要命的是,大众点评人均525/人,还好不是我掏钱。


小白不以为然的笑了笑,“等到核污水传遍全球,你想吃都不敢吃了,人生短短几十年,要懂得及时行乐。而且,咱两的关系也非同一般啊~”。打从有记忆起,我和小白就认识了,年龄跟我差不多,性格跟我差不多,爱好也跟我差不多,好巧不巧,如今他跟我一样也是在广州做IT,所以我们经常联系,关系特别好。“还是你会享受,走吧,拿吃的。”随即我和小白分头寻宝,不一会儿的功夫,桌子上便摆满了芝士波士顿、芝士生蚝、海螺、北极贝、不知名海虾各种海鲜。


我抓起一个芝士生蚝就往里炫,甜中带嫩,入口即化,香味从味蕾刺激我的脑海,当我准备再抓一个,小白轻飘飘的说了句:“我破产了。”我顿时一僵,尴尬地把手收了回去,突然想到了什么,小心翼翼地问,“你不会找我借钱吧?我可没钱哦,这顿AA也行”。


互诉


“你想的倒是挺多,只是这芝士生蚝我才拿了两个,你吃了一个还想再拿,我只能技术性打断你了”,我沉默良久,直至小白把另一只芝士生蚝炫完,露出了他满足的嘴脸


04.png


我才询问道“是你那家自助预约舞蹈室吗?”



破产



小白点了点头,“自从前年疫情过后,收入就一直不太乐观了,且竞争愈来愈激烈,到了暑假最旺期间扣完水电和物业租金,我竟还要倒贴两百元。这样亏本坚持了大概8个月后,合伙人L总她终于决定要解散了。之前成立公司和几位合伙人签订的股份分配书感觉自己犹如走上人生巅峰,随着几家分店加盟,畅想无限美好未来,好日子越来越有,越来越甜。未曾想两年不到,昨日便签了注销公司的文件,现在搞一份副业,付出了大量心血,最后也未必落得一个美好的结局。”


“唉~”我长叹了一声,刚刚吃完这个芝士生蚝,嘴巴有点渴,顺手拿起手边的茶杯抿了一口,这铁观音茶,清淡清香、浓郁扑鼻,喝了一口后自有淡淡回甘。“大局已定,失败乃成功之母,不过这次你收获也不少吧?”之前我也听过小白的舞蹈室副业,记得他刚成立之初前几个月每月都有几万流水,扣除杂七杂八到手还能有个两三千块钱,没想到还是经受不住岁月的考验。


“的确有所收获,L总把舞蹈室的哈曼卡顿音响送我了,这音响听着贼带劲~”。或许是我安慰,小白皱着的双眉似乎舒展几分,“那你呢,你最近在搞什么?”


“我上王者了。”我漫不经心的回了一句。


“王者是谁?”,我白了他一眼,他笑了笑,“没想到你还在玩啊?”。“嗯,现在机制60星就能王者了,有时间便利用下elo机制完成下十年前的梦想,最近练了一手刀妹,真的万军丛中过,片叶不沾身,得心应手。”说罢,我便给他看了看我上王者的截图。


06.jpg


“羡慕你,你还是一如既往的追寻你想要的东西。”小白一边说一边用筷子把半截波士顿龙虾连根拔起,张开血盆大口,吞入嘴中。


“切,就算舞蹈室倒闭也不影响你现在的生活吧,你看你现在一样过的挺滋润啊。”我不屑的说到。


“本来还挺滋润的,但最近压力有点大了,现在每个月发工资后都往家里打几千。”我不解,问道“怎么了,是家里出什么事了吗?”



暴雷



“我家资产暴雷了,你之前也应该知道我家里主要收入吧”。我点了点头,“现在存放到那里的资产因为公司负责人喝酒脑溢血离世了,剩下一大堆债务没法处理,本来每月提供的利息也没有了,本金还被冻结住,现在暂时拿不回来,这些都是我妈跟我说的。”我听完,大吃一惊,不过这种依靠大资金存放赚取高额利息的盈利,本来就是极高风险的,正所谓高风险高回报。但我看着小白失落的神情,也只能安慰说,“有什么要帮忙的跟我说,还有这顿哥请你”。


就等你这句话”。小白乐呵呵的看着我,看着他这副表情,我硬了,拳头硬了。


“现在主要靠负责人的弟弟处理家中事务,他也承诺两年后慢慢还回本金,但是家里主要的收入来源没有了,我也不想家里人看不开,毕竟健康最重要。之前本来就有向家里人打钱,只不过现在翻了一倍,自己花钱也不能像之前大手大脚了,不过请你这顿饭倒是不成问题”。小白见我长舒一口气,好奇问我,“怎么请我吃顿饭好像要你命似的,最近手头很紧吗?”


“你小子,我听你这件事也不是死局,才放心下来,如果承诺两年之内将本金还清并落实到位,也已经算是万幸了。不过我最近手头的确不充裕”。“怎么?之前大礼包十几万全买皮肤去了?”。我摇了摇头,给他打开了我的小鹏APP,“我买了小鹏G9”。


05.jpg

小白看了看我的订单,便看向我,满是不解,“咦,之前你的梦中情车不是宝马5系,连在掘金写小说用的头图都是盗的百度,怎么买了小鹏G9啊,你不是看不上这种牌子的车吗?”


我笑了笑,说“所以这篇小说用的头图是小鹏G9”。“啊?”


“以前我总天真的以为,只要自己不断的存钱,总有一天能够实现自己买下宝马5系的梦想,但梦想终归是梦想,现实毕竟是现实,钱真的很难赚,我不想贷款,不靠父母支持,要自己一边打工一边存钱,凑够这五十万谈何容易。当然,毕竟我也是能力不行,能力配不上自己的野心,转眼之间便差不多到了而立之年,往后的日子还要准备结婚,生娃等等世俗制定好的人生阶梯,之前我老是看懂车帝推文,什么5系,E级,是普通人的天花板,那时候觉得天花板离我触手可及,而现在,我每天睁开眼都觉得天花板离我越来越远。慢慢的我也认清了自己,知道自己是个什么水平,也学会放下,但是,梦想永远会存在我的心中,不会灭去”。


“你爸妈不反对你?”。


“嗐,他们吵上天了,什么电车不安全呀,电车只能买特斯拉啊,小鹏都要倒闭之类的。但是又如何呢,毕竟钱是我的,他们做不了主。我也有试驾过宝马iX3,只是觉得当下,这台车更适合我”。小白听完我的赘述,点了点头,“嗯,我了解你这个人,一旦认定了某些事,别人很难去改变你的想法。”我嘿嘿一笑,“你不也是吗?”


边聊边吃时间总是过得特别快,不一会儿我们两人的桌碟上放满了残骸,堆积如山,刚好服务员经过帮我们更换了新碟子,我们异口同声的说了声谢谢。


进入了短暂的沉默,我率先问小白,“最近工作情况如何?”。


小白听完,擦了擦嘴,表情也正经了起来,“其实今天我主要目的就是来跟你分享一下我最近的经历的,看看你有啥想法。”。


“哦?细说。”



曙光



“前两周我之前的leader让我去他公司面试。”


“你去了?”


“别打岔”。小白没好气的看我一眼,继续说道“一开始我是拒绝的,你也知道,我最近都比较躺平了,拿着一份在广州过的不差的薪资,浑浑噩噩的过着日子。谁知道,那位大佬开口便以35 * 14邀约,我本是性情中人,路见不平便拔刀相助,朋友有困难,我都会鼎力相助,何况这是贵人”。随即,他给我看了看聊天内容。


02.jpg


01.jpg


03.jpg

“听到这个数,我真的是垂死病中惊坐起,手上的switch瞬间丢到一边,立刻屁颠屁颠准备简历了。他说我是人才,其实我知道,贵人的实力,才称得上真正的人才”


“因为base要去深圳,如果薪资涨幅不是太高的话,其实我心里还是不太乐意的,因为现在在公司其实也得到leader青睐了,而且临近年终了,说实话,工作了6年多,之前跳来跳去,为了眼前那点利益,年终奖都没拿过几次,实属心累。但是这次如果薪资到胃,能给到这么高,虽然脉脉都劝别人不要去,但是我还是想尝试一下,毕竟这是更大的平台,为自己以后职业规划着想,现在累点真不算啥。”


我表示理解,“毕竟脉脉都是问就是别去,劝就是不走”。


小白笑着点了点头,“所以我去了,一面二面都过了。”


“牛逼啊,面了什么,分享一下啊~”


“牛逼个啥啊,问的都比较浅,一面印象比较深主要下面几点”。



  • typescript中 interface 和 type 的区别


1. type类型使用范围更广, 接口类型只能用来声明对象
2. 在声明对象时, interface可以多次声明
3. 区别三 interface支持继承的,type不支持
4. interface可以被类实现


  • typescript中 字面量类型


这个我真忘了,回头你自己整理一下


  • redis中如何处理大key


这个我不会,面试官也很nice的讲解了一波


  • 服务器容灾方案


这个我不会,面试官也很nice的讲解了一波


  • service worker作用


这个我不会,面试官也很nice的讲解了一波

“不是,你咋啥都不会啊?”。我超大声的问。


“那你会吗?”小白反问我。


“我也不会”。


“那我就放心了,然后就是问项目细节,我稀里糊涂的答,他稀里糊涂的听,二面的话就是更深入的项目细节了。”


“然后呢然后呢?”


“然后便是昨天的HR终面,HR让我从春秋时期讲起,阐述我的工作经历,我也是乖巧的从百草园讲到三味书屋,其中声情并茂,对细节说到尽情之处,心中也不免感慨之前的辉煌。不过,往事已成云烟。待烟消云散,HR便说重点,问我期望薪资。因为期间我也听HR说现在节奏很紧,加班已是常态,10-10-6稀松平常,我见HR诚心待我,我便以真心待她”。


“你说了多少?”


“98K”


“她同意了?”


“她说你现在的薪资过低,申请不到这个数,如果降低你是否会考虑?”


“那你怎么回?”


“我怎么回,难道你不知道吗?”


“我怎么会知道?”


past lives couldn't ever hold me down, lost love is sweeter when it's finally


"i've got the strangest feelin, this isn't out first time around"


“怎么我的闹钟响了?”突然,周遭的一切突然模糊了起来,旋即进入黑暗。


梦醒


我肌肉记忆般的按停了手机的闹铃,这首歌是我最喜欢的歌,但是自从做了起床闹钟铃声后,我便没有再听过。


我机械般地刷牙洗脸漱口,坐在有点老旧的餐桌椅上,打开昨天晚上下班临时买的方块原味面包,吃了两片,肚子好受一些,看来平日加班还是要按时吃晚饭。


我出门走去,阳光落在我的脸上,小白是我,我亦是我。


作者:很饿的男朋友
来源:juejin.cn/post/7293786784127090715
收起阅读 »

曹贼,莫要动‘我’网站 —— MutationObserver

web
前言 本篇主要讲述了如何禁止阻止控制台调试以及使用了MutationObserver构造方法禁止修改DOM样式。 正文 话说在三国时期,大都督周瑜为了向世人宣布自己盛世容颜的妻子小乔,并专门创建了一个网站来展示自己的妻子 这么好看的看的小乔,谁看谁不糊,更何...
继续阅读 »

前言


本篇主要讲述了如何禁止阻止控制台调试以及使用了MutationObserver构造方法禁止修改DOM样式。


正文


话说在三国时期,大都督周瑜为了向世人宣布自己盛世容颜的妻子小乔,并专门创建了一个网站来展示自己的妻子


image.png
这么好看的看的小乔,谁看谁不糊,更何况曹老板。这天,曹操在浏览网页的时候,无意间发现了周瑜的这个网站,看着美若天仙的小乔,曹操的眼泪止不住的从嘴角流了下来。赶紧将网站上的照片保存了下来。

这个消息最后传到了周瑜的耳朵里,他只是想展示小乔,可不是为了让别人下载的。于是在自己的网站上做了一些预防措施。

为了防止他人直接在网站上直接下载图片,周瑜将右键的默认事件给关闭了,并且为了防止有人打开控制台,并对图片保存,采取了以下方法:


禁用右键和F12键


//给整个document添加右击事件,并阻止默认行为
document.addEventListener("contextmenu", function (e) {
e.preventDefault();
return false;
});

//给整个页面禁用f12按键 keyCode即将被禁用 不再推荐使用 但仍可以使用
document.addEventListener("keydown", function (e) {
//当点了f3\f6\f10之后,即使禁用了f12键依旧可以打开控制台,所以一并禁用
if (
[115, 118, 121, 123].includes(e.keyCode) ||
["F3", "F6", "F10", "F12"].includes(e.key) ||
["F3", "F6", "F10", "F12"].includes(e.code) ||
//ctrl+f 效果和f3效果一样 点开搜索之后依旧可以点击f12 打开控制台 所以一并禁用
//缺点是此网站不再能够 **全局搜索**
(e.ctrlKey && (e.key == "f" || e.code == "KeyF" || e.keyCode == 70))||
//禁用专门用于打开控制台的组合键
(e.ctrlKey && e.shiftKey && (e.key == "i" || e.code == "KeyI" || e.keyCode == 73))
) {
e.preventDefault();
return false;
}
});

当曹操再次想保存小乔照片的时候,发现使用网页的另存了已经没用了。这能难倒曹老板吗,破解方法,在浏览器的右上角进行操作就可打开控制台,这个地方是浏览器自带的,没办法禁用


image.png
这番操作之后,曹操可以选择元素保存那个图片了。周瑜的得知了自己的禁用措施被破解后,赶忙连夜加班打补丁,于是又加了一些操作,禁止打开控制台后进行操作


禁用控制台


如何判定控制台被打开了,可以使用窗口大小来判定


function resize() {
var threshold = 100;
//窗口的外部减窗口内超过100就判定窗口被打开了
var widthThreshold = window.outerWidth - window.innerWidth > threshold;
var heightThreshold = window.outerHeight - window.innerHeight > threshold;
if (widthThreshold || heightThreshold) {
console.log("控制台打开了");
}
}
window.addEventListener("resize", resize);

但是也容易被破解,只要让控制台变成弹窗窗口就可以了


也可以使用定时器进行无限debugger,因为只有在控制台打开的时候debugger才会生效。关闭控制台的时候,并不会影响功能。当前网页内存占用比较大的时候,定时器的占用并不明显。在当前网页占用比较小的时候,一直开着定时器才会有较为明显的提升


  setInterval(() => {
(function () {})["constructor"]("debugger")();
}, 500);

破解方法一样有,在debugger的位置右键禁用调试就可以了。这样控制台就可以正常操作了


image.png
既然有方法破解,就还要做一层措施,既然是要保存图片,那就把img转成canvas,这样即使打开控制台也没办法进行对图片的保存


//获取dom
const img = document.querySelector(".img");
const canvas = document.querySelector("#canvas");
//img转成canvas
canvas.width = img.width;
canvas.height = img.height;
ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0, img.width, img.height);
document.body.removeChild(img);

经过一夜的努力,该加的措施都加上了。周瑜心想这下就没办法保存我的小乔了吧。

来到曹操这边,再次打开周瑜的小破站,还想故技重施时,发现已经有了各种显示,最后也没难倒曹操,那些阻碍也都被破解了。但是到保存图片的时候傻眼了,竟然已经不是图片格式了,那就没办法下载了呀。但是小乔真的很养神,曹操心有不甘,于是使用了最后一招,既然没办法下载那就截图,虽然有损画质,但是依旧能看。


得知如此情况的大都督周瑜不淡定了,从未见过如此厚颜无耻之人,竟然使用截图。


006APoFYly1g2qcclw1frg308w06ox2t.gif
话说魔高一尺,道高一丈,周瑜再次熬夜加班进行对网站的优化。于是使用了全屏水印+MutationObserver监听水印dom的方法。即使截图也让他看着不舒服。


MutationObserver


MutationObserver是一个构造函数,接口提供了监视对 DOM 树所做更改的能力。它被设计为旧的 Mutation Events 功能的替代品,该功能是 DOM3 Events 规范的一部分。

它接收一个回调函数,每当监听的dom发生改变时,就会调用这个函数,函数传入一个参数,数组包对象的格式,里面记录着dom的变化以及dom的信息。


image.png
返回的实例是一个新的、包含监听 DOM 变化回调函数的 MutationObserver 对象。有三个方法observedisconnecttakeRecords



  • observe接收两个参数,第一个为要监听的dom元素,第二个则是一些配置对象,当调用 observe() 时,childListattributes 和 characterData 中,必须有一个参数为 true。否则会抛出 TypeError 异常。配置对象如下:

    • subtree:当为 true 时,将会监听以 target 为根节点的整个子树。包括子树中所有节点的属性,而不仅仅是针对 target。默认值为 false

    • childList:当为 true 时,监听 target 节点中发生的节点的新增与删除(同时,如果 subtree 为 true,会针对整个子树生效)。默认值为 false

    • attributes:当为 true 时观察所有监听的节点属性值的变化。默认值为 true,当声明了 attributeFilter 或 attributeOldValue,默认值则为 false

    • attributeFilter:一个用于声明哪些属性名会被监听的数组。如果不声明该属性,所有属性的变化都将触发通知。

    • attributeOldValue:当为 true 时,记录上一次被监听的节点的属性变化;可查阅监听属性值了解关于观察属性变化和属性值记录的详情。默认值为 false

    • characterDate:当为 true 时,监听声明的 target 节点上所有字符的变化。默认值为 true,如果声明了 characterDataOldValue,默认值则为 false

    • characterDateOldValue:当为 true 时,记录前一个被监听的节点中发生的文本变化。默认值为 false



  • disconnect方法用来停止观察(当被观察dom节点被删除后,会自动停止对该dom的观察),不接受任何参数

  • takeRecords:方法返回已检测到但尚未由观察者的回调函数处理的所有匹配 DOM 更改的列表,使变更队列保持为空。此方法最常见的使用场景是在断开观察者之前立即获取所有未处理的更改记录,以便在停止观察者时可以处理任何未处理的更改。


该构造函数监听的dom即使在控制台中被更改属性或值,也会被监听到。


使用MutationObserver对水印dom进行监听,并限制更改。


<style>
//定义水印的样式
#watermark {
width: 100vw;
height: 100vh;
position: absolute;
left: 0;
top: 0;
font-size: 34px;
color: #32323238;
font-weight: 700;
display: flex;
flex-wrap: wrap;
justify-content: space-evenly;
align-content: space-evenly;
z-index: 9999999;
}
#watermark span {
transform: rotate(45deg);
}
</style>

<script>
//获取水印dom
const watermark = document.querySelector("#watermark");
//克隆水印dom ,用作后备,永远不要改变
const _watermark = watermark.cloneNode(true);
//获取水印dom的父节点
const d = watermark.parentNode;
//获取水印dom的后一个节点
let referenceNode;
[...d.children].forEach((item, index) => {
if (item == watermark) referenceNode = d.children[index + 1];
});
//定义MutationObserver实例observe方法的配置对象
const prop = {
childList: true,//针对整个子树
attributes: true,//属性变化
characterData: true,//监听节点上字符变化
subtree: true,//监听以target为根节点的整个dom树
};
//定义MutationObserver
const observer = new MutationObserver(function (mutations) {
//在这里每次坚挺的dom发生改变时 都会运行,传入的参数为数组对象格式
mutations.forEach((item) => {
//这里可以只针对监听dom的样式来判断
if (item.attributeName === "style") {
//获取父节点的所有子节点,因为时伪数组,使用扩展运算符转以下
[...d.children].forEach((v) => {
//判断一下,是父节点里的那个节点被改变了,并且删除那个被改变的节点(也就是删除水印节点)
if (item.target.id && v == document.querySelector(`#${item.target.id}`)) {
v.remove();
}
});
//原水印节点被删除了,这里使用克隆的水印节点,再次克隆
const __watermark = _watermark.cloneNode(true);
//这里的this指向是MutationObserver的实例对象,所以同样可以使用observe监听dom
//监听第二次克隆的dom
this.observe(__watermark, prop);
//因为水印dom被删除了,再将克隆的水印dom添加到原来的位置 就是referenceNode节点的前面
d.insertBefore(__watermark, referenceNode);
}
});
});
在初始化的时候监听初始化的水印dom
observer.observe(watermark, prop);
</script>



这样,每当对水印dom进行更改样式的时候,就会删除该节点,并重新添加一个初始的水印dom,即使突破重重困难打开开控制台,用户也是无法对dom 进行操作。


视频转Gif_爱给网_aigei_com.gif


隔天曹操再次打开网页,发现网页上的水印,心里不足为惧,心想区区水印能难倒自己?操作到最后却发现,不论如何对水印dom进行操作,都无法改变样式。虽说只是为了保存图片,但是截图有着这样水印,任谁也不舒服呀。曹操大怒,刚吃了两口的饭啪的一下就盖在了桌子上......


20230508094549_33500.gif
然而曹操不知道的是,在控制台中,获取dom节点右键是可以只下载获取的那个节点的......


image.png


结尾


文章主要是以鬼畜恶搞的方式讲述了,如何禁止用户打开控制台(还有重写toSring,consloe.log等一些方法,但我并没有没有实现,所以这里并没有写上),并且如何使用MutationObserver构造函数来监听页面中的dom元素。其实大多情况下并没有这方面的项目需求,完全可以当扩展知识看了。


写的不好的地方可以提出意见,虚心请教!


作者:iceCode
来源:juejin.cn/post/7290862554657423396
收起阅读 »

在高德地图实现后期效果

web
介绍 最近在做可视化图层开发的时候,发现我们自己开发的图层一些优秀的案例比起来,总是有一定的差距。差了后期效果合成环节,就比如个人晒图前忘了用美图秀秀修图。于是花了些时间研究了高德地图JSAPI2.0和GLCustomLayer,探索如何将后期特效接入到3D图...
继续阅读 »

介绍


最近在做可视化图层开发的时候,发现我们自己开发的图层一些优秀的案例比起来,总是有一定的差距。差了后期效果合成环节,就比如个人晒图前忘了用美图秀秀修图。于是花了些时间研究了高德地图JSAPI2.0和GLCustomLayer,探索如何将后期特效接入到3D图层中。
Effect-PointsLayer2.gif


后期特效其实有点类似照片的后期滤镜处理,是对渲染结果的二次处理,可以实现发光、模糊、色调调整、镜头暗角、模拟环境光遮蔽等各种效果,为了方便理解,下面的讲解我将以辉光效果为例,学会了一种其他效果思路类似。


方案调研


Three官方提供了非常简单的方法实现后期特效,貌似仅需要完成以下两个步骤就可以完成我们想要的需求,代码也非常清晰简单:


import * as THREE from 'three'
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js'
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js'
import { UnrealBloomPass } from 'three/examples/jsm/postprocessing//UnrealBloomPass.js'
import { OutputPass } from 'three/examples/jsm/postprocessing/OutputPass.js'

...

// 1.在图层初始化完成后,创建效果合成器
onLayerInit(){
const { scene, camera, renderer } = this

const renderScene = new RenderPass(scene, camera)

// 后期泛光特效
bloomPass = new UnrealBloomPass(new THREE.Vector2(this.container.clientWidth, this.container.clientHeight), 1, 0, 0)
bloomPass.threshold = params.threshold
bloomPass.strength = params.strength
bloomPass.radius = params.radius

composer = new EffectComposer(renderer)
// 以下代码会遮盖地图
composer.addPass(renderScene)
composer.addPass(bloomPass)
}

// 2.更新合成器
onRender () {
if (composer) {
composer.render()
}
}

本以为这样做就可以开心收工了,燃鹅事情并没有那么简单,把这套方案移入高德的GLCustomLayer中,出现了这样的情况,后期效果直接把地图底图盖住了。
Untitled.png


出现这种情况的原因是实现辉光效果而编写的着色器,它会直接修改整个画面的alpha通道而导致透明效果丢失,因此需要单独修改UnrealBloomPass.js。


然而光是这样还不够,经过各种尝试,仍无法直接在GLCustomLayer上解决地图被遮盖的问题,后来咨询了高德地图开发团队的技术大佬,他给我的建议是后期效果层独立展示,于是就沿着这个思路进行了第二轮尝试。
Untitled 1.png


这里面有几个关键步骤是必须的:



  1. 修改UnrealBloomPass着色器代码

  2. 使用输出通道new OutputPass()置于特效通道的后面

  3. 在customLayer图层中,每次渲染就更新特效合成器EffectComposer


由于我这边是不希望之前开发的可视化图层做太多的修改去迁就这个后期效果的,也有对性能较差的终端机器优雅降级的考虑,索性把后期效果独立为EffectLayer层,以方便灵活地装载或剥离,最终实现了这个效果。


Effect-POI3dLayer1.gif


实现步骤




  1. 修改 UnrealBloomPass.js,由于这个文件在npm包中不能随意修改,我另外写了一个UnrealBloomPass1 继承并覆盖了UnrealBloomPass的方法


    import * as THREE from 'three'
    import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js'

    class UnrealBloomPass1 extends UnrealBloomPass {
    constructor (resolution, strength, radius, threshold) {
    super(resolution, strength, radius, threshold)
    }

    getSeperableBlurMaterial (kernelRadius) {
    ...
    fragmentShader:
    `#include <common>
    varying vec2 vUv;
    uniform sampler2D colorTexture;
    uniform vec2 invSize;
    uniform vec2 direction;
    uniform float gaussianCoefficients[KERNEL_RADIUS];

    void main() {
    float weightSum = gaussianCoefficients[0];
    vec3 diffuseSum = texture2D( colorTexture, vUv ).rgb * weightSum;
    float alphaSum;
    for( int i = 1; i < KERNEL_RADIUS; i ++ ) {
    float x = float(i);
    float w = gaussianCoefficients[i];
    vec2 uvOffset = direction * invSize * x;
    vec4 sample1 = texture2D( colorTexture, vUv + uvOffset );
    vec4 sample2 = texture2D( colorTexture, vUv - uvOffset );
    diffuseSum += (sample1.rgb + sample2.rgb) * w;
    alphaSum += (sample1.a + sample2.a) * w; //
    weightSum += 2.0 * w;
    }
    // gyrate: overwrite this line for alpha pass
    // gl_FragColor = vec4(diffuseSum/weightSum, 1.0);
    gl_FragColor = vec4(diffuseSum/weightSum, alphaSum/weightSum);
    }`

    })
    }
    }

    export { UnrealBloomPass1 }



  2. 编写EffectLayer


    import * as THREE from 'three'
    import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js'
    import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js'
    import { UnrealBloomPass1 } from '../plugins/three/examples/jsm/postprocessing/UnrealBloomPass.js'
    import { OutputPass } from 'three/examples/jsm/postprocessing/OutputPass.js'
    import _ from 'lodash'

    class EffectLayer {

    // 此处省去一些内部变量

    _style = {
    // 光照强度阈值
    threshold: 0.0,
    // 泛光强度
    strength: 1.0,
    // 泛光半径
    radius: 1.5
    }

    /**
    * 创建一个实例
    * @param {Object} config
    * @param {Layer} config.layer 目标图层,要求是Layer的相关子类
    * @param {Number} [config.zIndex=120] 图层的层级
    * @param {EffectStyle} [config.style] 后期特效的配置项
    */

    constructor (config) {
    const conf = _.merge(this._conf, config)
    this._style = _.merge(this._style, conf.style)

    if (!conf.layer.scene || !conf.layer.camera) {
    console.error('缺少场景和相机')
    return
    }
    this.init()
    }

    init () {
    this.createLayer()
    this.addEffect()
    }
    }



  3. 创建自定义图层customLayer


    createLayer () {
    const canvas = document.createElement('canvas')
    this._customLayer = new AMap.CustomLayer(canvas, {
    zooms: [3, 22],
    zIndex: this._conf.zIndex,
    alwaysRender: true
    })

    this._canvas = canvas
    }



  4. 创建特效合成器


    addEffect () {
    const { scene, camera, container, renderer, map } = this._conf.layer
    const { clientWidth, clientHeight } = container

    // 创建渲染器
    const effectRender = new THREE.WebGLRenderer({
    canvas: this._canvas,
    alpha: true,
    antialias: false,
    stencil: false,
    depth: false
    })
    // renderer.setClearColor(0xff0000);
    effectRender.autoClear = false
    effectRender.setSize(clientWidth, clientHeight)

    // 后期效果
    const renderScene = new RenderPass(scene, camera)

    // 后期辉光特效
    const bloomPass = new UnrealBloomPass1(new THREE.Vector2(clientWidth, clientHeight), 1, 0, 0)
    bloomPass.clear = false

    // 输出通道
    const outputPass = new OutputPass()
    outputPass.clear = false

    this.updatePass()

    const composer = new EffectComposer(effectRender)
    composer.addPass(renderScene)
    composer.addPass(bloomPass)
    composer.addPass(outputPass)

    this._composer = composer
    this._bloomPass = bloomPass

    this._customLayer.render = function () {
    if (composer) {
    // 每次渲染就更新特效合成器
    composer.render()
    }
    }

    map.add(this._customLayer)
    }

    updatePass() {
    const {_bloomPass} = this
    if (_bloomPass) {
    _bloomPass.threshold = this._style.threshold
    _bloomPass.strength = this._style.strength
    _bloomPass.radius = this._style.radius
    }
    // 添加其他特效通道...
    }



  5. 使用EffectLayer


    //之前编写的可视化图层
    const layer = new GLlayers.POI3dLayer({
    map: getMap(),
    zooms: [10, 22]
    })

    layer.on('complete', (layer) => {
    let effectLayer = new GLlayers.EffectLayer({
    layer: layer, //把图层传入effectLayer
    style:{
    threshold: 0.0,
    strength: 1.0,
    radius: 0.5,
    }
    })
    })




注意:以上方案three.js版本为0.157, 该版本对three/example/jsm/postprocessing目录中的后期效果通道相关文件做了较多调整,如果是用之前的three.js版本,修改内容可能有所不同。



至此我们就可以在之前的可视化图层基础上,加入几行代码实现辉光效果,以下是挑选一部分图层加上EffectLayer之后的效果,肉眼可见还是有很明显区别的。当然在使用过程中也发现了个别图层原有的问题需要做进一步优化。


Effect-BorderLayer1.gif


Effect-PointsLayer1.gif


Effect-SpriteLayer1.gif


Effect-cakeLayer1.gif


待解决问题


使用独立图层展示后期特效层有个明显缺点,无法关联默认基本图层的场景要素深度信息,最主要的影响是高德的建筑白模图层和自定义可视化图层的远近遮挡关系会丢失,导致可视化图层永远在最前面。比如下面这个城市主要道路的辉光效果,这个是需要后面花时间去解决的,写这篇文章的时候又找到几个方案,有时间再试一把,毕竟上面留给我的时间不多了。


Effect-FlowlineLayer2.gif


相关链接


three.js后期处理


three.js效果合成器文档和示例


实现模型材质局部辉光效果和解决辉光影响场景背景图显示的问题


Three.js带Depth实现分区辉光


作者:gyratesky
来源:juejin.cn/post/7293788726235365426
收起阅读 »

自研框架跻身全球 JS 框架榜单,排名紧随 React、Angular 之后!

web
前言 终于实现了一个重要目标!我独立研发的 JavaScript 框架 Strve,最近发布了重大版本 6.0.2。距离上次大版本发布已经接近两个月,期间进行了大量的优化,使得框架性能和稳定性都得到了大幅度的提升。在上次的大版本更新中,成功实现了对 JSX 语...
继续阅读 »

前言


终于实现了一个重要目标!我独立研发的 JavaScript 框架 Strve,最近发布了重大版本 6.0.2。距离上次大版本发布已经接近两个月,期间进行了大量的优化,使得框架性能和稳定性都得到了大幅度的提升。在上次的大版本更新中,成功实现了对 JSX 语法的全面支持,使得 Strve 在代码智能提示和代码格式化方面更加友好,进一步提高了开发效率。


介绍


相信有些小伙伴没有听说过 Strve 到底是什么,那我这里就大体介绍一下。


Strve 是一个可以将字符串转换为视图(用户界面)的 JavaScript 库。Strve 不仅易于使用,而且可以灵活地拆解不同的代码块。使用模板字符串开发用户界面,主要是利用 JavaScript 的能力,只关注 JavaScript 文件。Strve 又是一个易用性的 JavaScript 框架,它提供了很多实用的功能与生态工具。


我们可以通过一些简单的示例来了解 Strve 的使用方法。


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Strve.js</title>
</head>

<body>
<script src="https://cdn.jsdelivr.net/npm/strve-js@6.0.2/dist/strve.full.prod.js"></script>
<script>
const { html, setData, createApp } = Strve;
const state = {
count: 0,
};

function add() {
setData(() => {
state.count++;
});
}

function App() {
return html`<h1 onClick=${add}>${state.count}</h1>`;
}

const app = createApp(App);
app.mount('#app');
</script>
</body>
</html>

在上述代码中,我们通过引入 Strve 库,并使用 createApp 方法创建了一个 App 组件,然后通过 mount 方法挂载到页面上,这里的 App 组件就是通过模板字符串来定义的。这样就可以在 JS 代码中编写用户界面,是不是很方便呢?我们发现,在模板字符串中,我们使用 ${} 来引用数据,并且使用 onClick 方法来绑定事件。这样就可以实现一个计数器的功能。


除了这种简单的示例,Strve 还支持很多复杂的功能,我们可以使用 JSX 语法来编写组件,也可以使用函数式组件来编写组件,还可以使用组件来编写组件,甚至可以编写一些自定义的组件。


如果想了解更多关于 Strve 的信息,稍后可以到文章末尾处查阅官方文档。


性能评估


我们既然发布了 Strve,那么肯定需要对其性能进行评估,我们评估的工具就用js-framework-benchmarkjs-framework-benchmark 是什么?我们这里就简单介绍下 js-framework-benchmark,它是一个用于比较 JavaScript 框架性能的项目。它旨在通过执行一系列基准测试来评估不同框架在各种场景下的性能表现。这些基准测试包括渲染大量数据、更新数据、处理复杂的 UI 组件等。通过运行这些基准测试,可以比较不同框架在各种方面的性能优劣,并帮助开发人员选择最适合其需求的框架。js-framework-benchmark 项目提供了一个包含多个流行 JavaScript 框架的基准测试套件。这些框架包括 Angular、React、Vue 等。每个框架都会在相同的测试场景下运行,然后记录下执行时间和内存使用情况等性能指标。通过比较这些指标,可以得出不同框架的性能差异。这个项目的目标是帮助开发人员了解不同 JavaScript 框架的性能特点,以便在选择框架时能够做出更加明智的决策。同时,它也可以促进框架开发者之间的竞争,推动框架的不断改进和优化


在评估之前,我们必须要了解 js-framework-benchmark 中有两种模式。一种是 keyed,另一种是 non-keyed。在 js-framework-benchmark 中,"keyed" 模式是指通过给数据项分配一个唯一标识符作为 "key" 属性,从而实现数据项与 DOM 节点之间的一对一关系。当数据发生变化时,与之相关联的 DOM 节点也会相应更新。而 non-keyed 模式是指当数据项发生变化时,可能会修改之前与其他数据项关联的 DOM 节点。


因为 Strve 支持keyed模式,所以我们将使用此模式来评估 Strve 的性能。


对以下操作进行了基准测试:



  • 创建行:页面加载后创建 1,000 行的持续时间(无预热)。

  • 替换所有行:替换表中所有 1,000 行的持续时间(5 次预热迭代)。

  • 部分更新:对于具有 10,000 行的表,每 10 行更新一次文本(进行 5 次预热迭代)。

  • 选择行:响应单击该行而突出显示该行的持续时间。 (5 次预热迭代)。

  • 交换行:在包含 1,000 行的表中交换 2 行的时间。 (5 次预热迭代)。

  • 删除行:删除具有 1,000 行的表的行的持续时间。 (5 次预热迭代)。

  • 创建多行:创建 10,000 行的持续时间(无预热)

  • 将行追加到大型表:在包含 10,000 行的表中添加 1,000 行的持续时间(无预热)。

  • 清除行:清除填充有 10,000 行的表的持续时间。 (无热身)

  • 就绪内存:页面加载后的内存使用情况。

  • 运行内存:添加 1,000 行后的内存使用情况。

  • 更新内存:1000 行的表点击 5 次更新后的内存使用情况。

  • 替换内存:点击 5 次创建 1000 行后的内存使用情况。

  • 重复清除内存:创建并清除 1,000 行 5 次后的内存使用情况。

  • 更新内存:1000 行的表点击 5 次更新后的内存使用情况。

  • 启动时间:加载和解析 javascript 代码以及渲染页面的持续时间。

  • 持续交互:灯塔指标 TimeToConstantlyInteractive:悲观 TTI - 当 CPU 和网络都非常空闲时。 (不再有超过 50 毫秒的 CPU 任务)

  • 脚本启动时间:灯塔指标 ScriptBootUpTtime:解析/编译/评估所有页面脚本所需的总毫秒数

  • 主线程工作成本:灯塔指标 MainThreadWorkCost:在主线程上工作所花费的总时间包括样式/布局等。

  • 总字节权重:灯塔指标 TotalByteWeight:加载到页面中的所有资源的网络传输成本(压缩后)。


对于所有基准测试,都会测量持续时间,包括渲染时间。


因为js-framework-benchmark是一个自动化测试的工具,只需要符合标准的代码就可以进行测试。Strve 支持 JSX 语法,所以我们将使用 JSX 语法来编写测试代码。


import { setData, createApp } from 'strve-js';
import { buildData } from './data.js';

let selected;
let rows = [];

function setRows(update = rows.slice()) {
setData(
() => {
rows = update;
},
{
name: TbodyComponent,
}
);
}

function add() {
const data = rows.concat(buildData(1000));
setData(
() => {
rows = data;
},
{
name: TbodyComponent,
}
);
}

function remove(id) {
rows.splice(
rows.findIndex((d) => d.id === id),
1
);
setRows();
}

function select(id) {
setData(
() => {
selected = id;
},
{
name: TbodyComponent,
}
);
}

function run() {
setRows(buildData());
selected = undefined;
}

function update() {
for (let i = 0; i < rows.length; i += 10) {
rows[i].label += ' !!!';
}
setRows();
}

function runLots() {
setRows(buildData(10000));
selected = undefined;
}

function clear() {
setRows([]);
selected = undefined;
}

function swapRows() {
if (rows.length > 998) {
const d1 = rows[1];
const d998 = rows[998];
rows[1] = d998;
rows[998] = d1;
setRows();
}
}

function TbodyComponent() {
return (
<tbody>
{rows.map((item) => (
<tr class={item.id === selected ? 'danger' : ''} data-label={item.label} key={item.id}>
<td class='col-md-1'>{item.id}</td>
<td class='col-md-4'>
<a onClick={() => select(item.id)}>{item.label}</a>
</td>
<td class='col-md-1'>
<a onClick={() => remove(item.id)}>
<span class='glyphicon glyphicon-remove' aria-hidden='true'></span>
</a>
</td>
<td class='col-md-6'></td>
</tr>
))}
</tbody>

);
}

function MainBody() {
return (
<fragment>
<div class='jumbotron'>
<div class='row'>
<div class='col-md-6'>
<h1>Strve-keyed</h1>
</div>
<div class='col-md-6'>
<div class='row'>
<div class='col-sm-6 smallpad'>
<button type='button' class='btn btn-primary btn-block' id='run' onClick={run}>
Create 1,000 rows
</button>
</div>
<div class='col-sm-6 smallpad'>
<button
type='button'
class='btn btn-primary btn-block'
id='runlots'
onClick={runLots}
>

Create 10,000 rows
</button>
</div>
<div class='col-sm-6 smallpad'>
<button type='button' class='btn btn-primary btn-block' id='add' onClick={add}>
Append 1,000 rows
</button>
</div>
<div class='col-sm-6 smallpad'>
<button
type='button'
class='btn btn-primary btn-block'
id='update'
onClick={update}
>

Update every 10th row
</button>
</div>
<div class='col-sm-6 smallpad'>
<button type='button' class='btn btn-primary btn-block' id='clear' onClick={clear}>
Clear
</button>
</div>
<div class='col-sm-6 smallpad'>
<button
type='button'
class='btn btn-primary btn-block'
id='swaprows'
onClick={swapRows}
>

Swap Rows
</button>
</div>
</div>
</div>
</div>
</div>
<table class='table table-hover table-striped test-data'>
<component $name={TbodyComponent.name}>{TbodyComponent()}</component>
</table>
<span class='preloadicon glyphicon glyphicon-remove' aria-hidden='true'></span>
</fragment>

);
}

createApp(() => MainBody()).mount('#main');

以下页面就是将进行基准测试的页面:


01.png


我们大体看下测试过程,我们将使用动图来展示页面效果,这样会觉得更加直观。


02.gif


最终,Strve 通过了压力测试!


08.png


基准测试结果


既然我们通过测试,我们就需要提交到js-framework-benchmark官方项目中,进行综合评估,与全球其他框架进行比较。


我们提交的 PR 在 2023 年 9 月 18 号被作者合并了。


03.png


在接下来的时间里,作者进行了一系列的测试。最终,Chrome 118 版本于上周发布,并在 GitHub 上公布了官方的测试结果。


04.png


我们打开下面的网址,看下 Strve 的官方测试结果:


krausest.github.io/js-framewor…


经过查询,全球 JavaScript 框架榜单中共有 142 个框架。


性能测试基准分为三类:



  • 持续时间

  • 启动指标

  • 内存分配


【持续时间】


在此测试基准中,Strve 平均值 1.42,排名第 90 位。


React、Angular 和 Vue,平均值分别为1.401.381.20,分别排名第 85 位、第 83 位和第 51 位。



平均值越小,排名则越靠前。颜色越绿代表越优。



05.png


【启动指标】


在此测试基准中,Strve 平均值 1.07


React、Angular 和 Vue,平均值分别为 1.681.801.30



平均值越小,排名则越靠前。颜色越绿代表越优。



06.png


【内存分配】


在此测试基准中,Strve 平均值 1.33


React、Angular 和 Vue,平均值分别为 2.462.821.86



平均值越小,排名则越靠前。颜色越绿代表越优。



07.png


新特性


我们在上面的测试中,可以看到 Strve 性能表现非常不错。


这次我们发布的大版本号为 6.0.2,我们将这个具有里程碑意义的大版本命名为 Strve6,而 “Strve6,从芯出发!” 这个口号正是 Strve6 的核心理念。这一版本象征着我们从底层技术出发,致力于为用户提供更优质、更高效的开发体验。


此次版本我们在性能与体验之间做了权衡。在源码层面,我们将普通 Diff 算法升级为 双端 Diff 算法,大大提升了性能。另外,我们在用户体验层面也做了很大的改进。


这里,我们提到了双端 Diff 算法,我们在面试中经常提到这个概念,但是很少用到实际项目中去。那么,为了更好地理解双端 Diff 算法如何提高性能,我们来看一个关于 Strve 简单的示例。


我们来遍历一个数组,并且每次点击按钮,往数组头部中添加一个元素。


【普通 Diff 算法】


<script type="module">
import {
html,
setData,
createApp,
} from 'https://cdn.jsdelivr.net/npm/strve-js@6.0.2/dist/strve.full-esm.js';

const state = {
arr: [1, 2],
count: 3,
};

function useUnshift() {
setData(() => {
state.count++;
state.arr.unshift(state.count);
});
}

function App() {
return html`
<fragment>
<button onClick=
${useUnshift}>Unshift</button>
<ul>
${state.arr.map((todo) => html`<li>${todo}</li>`)}
</ul>
</fragment>
`
;
}

const app = createApp(App);
app.mount('#app');
</script>

我们可以看到右侧 DOM 树,每次点击按钮,都会重新渲染整个列表。这样是肯定耗损浏览器性能的。


09.gif


【双端 Diff 算法】


<script type="module">
import {
html,
setData,
createApp,
} from 'https://cdn.jsdelivr.net/npm/strve-js@6.0.2/dist/strve.full-esm.js';

const state = {
arr: [1, 2],
count: 3,
};

function useUnshift() {
setData(() => {
state.count++;
state.arr.unshift(state.count);
});
}

function App() {
return html`
<fragment>
<button onClick=
${useUnshift}>Unshift</button>
<ul>
${state.arr.map((todo) => html`<li key=${todo}>${todo}</li>`)}
</ul>
</fragment>
`
;
}

const app = createApp(App);
app.mount('#app');
</script>

我们可以看到右侧 DOM 树,每次点击按钮,仅添加必要的元素,而不是重新渲染整个列表。这是因为我们在每个列表项中添加了 key 属性,并且这个 key 是唯一的。key 这个特殊的 attribute 主要作为 Strve 的虚拟 DOM 算法提示,在比较新旧节点列表时用于识别 vnode。只要标签类型与 key 值都相等,就说明当前元素可以被复用。


10.gif


热门话题


文章接近尾声,让我们来回顾一下最近社区的几个热门话题。



  1. 为什么要开发这个框架?初衷是什么?


答:其实,我的动机特别简单,完全受 JSX 语法的影响。刚接触 JSX 语法的时候,就被它那种魔法深深地吸引住了,可以在 JS 中写 HTML。所以,我就想我自己可不可以也搞一个类似 JSX 语法的库或者框架呢!一方面可以锻炼自己的代码能力,另一方面体验开发框架的整个流程,也方便我以后更全面的学习其他框架(Vue.js、React.js 等)。


做自己喜欢的事情是特别有意义的!



  1. 为什么选择 Strve 作为框架的名字?


答:Strve 最初定位是可以将字符串转换为视图(用户界面)的 JavaScript 库,所以是由 StringView 两个单词缩减组成的新单词。



  1. 跟前端热门框架比较,是想超过它们吗?


答:不是,我主要是想学习一下前端热门框架的实现原理,然后自己实现一个框架。有一句话说得好:“只有站在巨人的肩膀上才能望得更远!”。



  1. 记得之前也写过登上框架榜单的文章,这次为什么还要写?


答:之前,Strve 测评的模式是使用"non-keyed"。现在,Strve 新的版本支持"keyed"模式,所以,我重新写了一篇文章,来介绍 Strve 的新特性。



  1. Strve 6.0.2 版本发布,普通 Diff 算法升级为双端 Diff 算法,可以简单讲下双端 Diff 算法的概念吗?


答:双端 diff 算法就是头尾指针向中间移动,分别判断头尾节点是否可以复用,如果没有找到可复用的节点再去遍历查找对应节点的下标,然后移动。全部处理完之后要对剩下的节点进行批量的新增和删除。



  1. Strve 是个 JavaScript 库还是 JavaScript 框架?


答:首先,我们来看下框架与库有什么区别?库更多是一个封装好的特定的集合,提供给开发者使用,而且是特定于某一方面的集合(方法和函数),库没有控制权,控制权在使用者手中,在库中查询需要的功能在自己的应用中使用,我们可以从封装的角度理解库;框架顾名思义就是一套架构,会基于自身的特点向用户提供一套相当于叫完整的解决方案,而且控制权的在框架本身,使用者要找框架所规定的某种规范进行开发。Strve 可以是框架,因为 Strve 提供了路由、插件等生态工具;Strve 也可以是库, 因为 Strve 可以单独作为一个渲染库。



  1. Strve 你还要继续维护下去吗?


答:是的,我还会继续维护下去,因为我也想学习下去,也希望能帮助到更多前端开发者。


关于


Strve 我是从 2021 年下半年开始开发,到现在也快两年了。在这两年中,从一个之前只会 调用 API 的码农,到现在可以独立开发一个框架,让我收获了很多。学习了如何去分析一个框架的实现原理,也学习了如何去设计一个框架。



Strve 源码仓库:github.com/maomincodin…


Strve 中文文档:maomincoding.gitee.io/strve-doc-z…



如果大家觉得 Strve 还不错,麻烦帮我点下 Star 吧,谢谢!


结语


感谢各位读者的阅读,希望本文能对你有所帮助,如果喜欢本文,欢迎点赞,欢迎关注!


最后,分享一段话给大家:



很多时候


不是有希望才去坚持


而是在坚持的过程中慢慢看到希望


我们都是在暗夜里赶路的人


纵使满身疲惫也不肯轻言放弃


愿你所坚持的东西


终有一天反过来拥抱你



作者:前端历劫之路
来源:juejin.cn/post/7293786784127025179
收起阅读 »

【JavaScript】【表达式和运算符】instanceof

web
前言 在JavaScript中,判断变量的类型,常常使用的是typeof运算符 typeof的痛点: 所有的引用类型结果都是 Object; 空值null的结果也是Object; 为此,引入 instanceof 一、instanceof 1.1 作用 ...
继续阅读 »

前言


在JavaScript中,判断变量的类型,常常使用的是typeof运算符


typeof的痛点



  • 所有的引用类型结果都是 Object

  • 空值null的结果也是Object


image.png


为此,引入 instanceof


一、instanceof


1.1 作用



  • 用于判断某个实例是否属于某构造函数

  • 在继承关系中,用来判断一个实例是否属于它的父类型或祖先类型的实例


1.2 使用




  • 语法object instanceof constructor




  • 参数



    • object:某个实例对象

    • constructor:某个构造函数




  • 示例:




// 类
class Maomi {} // 定义类
let fuLai = new Maomi() // fuLai是Maomi类的实例对象
fuLai instanceof Maomi // true

// 时间
new Date() instanceof Date // true

// 构造函数
function SetMaomi() {}
let fulai = new SetMaomi();
fulai instanceof SetMaomi // true

// 函数
function getMaomi() {}
getMaomi instanceof Function // true

1.3 涉及的构造函数



  • 基础类型:String、Number、 Boolean、 Undefind、Null、Symbol

  • 引用类型:Object(Array、RegExp、Date、Function...)


1.3 实现原理


instanceof 的内部实现机制是:通过判断对象的原型链上是否能找到对象的 prototype,来确定 instanceof 返回值。


function instance_of(L, R) {
var O = R.prototype;
L = L.__proto__;
while (true) {
if (L === null)
return false;
if (O === L)
return true;
L = L.__proto__;
}
}

代码释义

① L表示对象实例,R表示构造函数或者父类型实例

② 取R的显式原型,取L的隐式原型

③ 循环遍历,进行判断②中的两个值是否相等,相等返回true,不相等继续查找L的原型链


instanceof 运算符用来检测 constructor.prototype 是否存在于参数 object 的原型链上。



  • 示例:


function SetMaomi() {}
let fulai = new SetMaomi();
fulai instanceof SetMaomi

观察fulai.__proto__SetMaomi.prototype的结构:

image.png


image.png



注意点fulai instanceof SetMaomi 返回 true,则并不意味着该表达式会永远返回 trueSetMaomi.prototype 属性的值有可能会改变,改变之后的值很有可能不存在于 fulai 的原型链上,这时原表达式的值就会成为 false



二、instanceof产生继承关系


function Cat(name,age,type){
this.name = name;
this.age = age;
this.type = type;
}
function YingDuan(name,age,type,sex){
Cat.call(this,name,age,type);
this.sex = sex;
}
YingDuan.prototype = new Cat(); // 这里改变了原型指向,实现继承
var yd = new YingDuan("有鱼",2,"金渐层","男"); //创建了英短对象yd
console.log(yd instanceof YingDuan); // true
console.log(yd instanceof Cat); // true
console.log(yd instanceof Object); // true

下面为了直观的观察,我就不采用循环的方式,直接一个一个的打印出来:



  • yd instanceof YingDuan:

    image.png

  • yd instanceof Cat:

    image.png

  • yd instanceof Object:

    image.png


三、注意问题



  1. fulai instanceof SetMaomi 返回 true,则并不意味着该表达式会永远返回 true
    SetMaomi.prototype 属性的值有可能会改变,改变之后的值很有可能不存在于 fulai 的原型链上,这时原表达式的值就会成为 false

  2. instanceof 用于判断对象类型,但以下情况的结果都为false,请注意。


console.log(Number instanceof Number)  // false
console.log(String instanceof String) // false
console.log(null instanceof Object) // false,null不具有任何对象的特性,也没有__proto__属性

参考



作者:旺仔小猪
来源:juejin.cn/post/7293348107517001739
收起阅读 »

抓包调试工具的终极答案-whistle

web
前言 抓包工具是一种在计算机网络中进行调试和分析的强大工具,它能够拦截、查看和分析在网络中传输的数据包内容。通过捕获这些数据包,我们可以深入挖掘其中包含的大量有用信息。抓包工具不仅提供了直观和可视化的界面,而且具有强大的过滤和分析功能,使用户能够轻松地掌握网络...
继续阅读 »

前言


抓包工具是一种在计算机网络中进行调试和分析的强大工具,它能够拦截、查看和分析在网络中传输的数据包内容。通过捕获这些数据包,我们可以深入挖掘其中包含的大量有用信息。抓包工具不仅提供了直观和可视化的界面,而且具有强大的过滤和分析功能,使用户能够轻松地掌握网络数据流量的细节。


在计算机通信中,数据包是由发送端(如浏览器或应用程序)构建的,并通过互联网传输到接收端(如服务器或另一台计算机)。通常情况下,这些数据包由发送和接收的应用程序自行处理,用户往往无法直接观察这些数据包的内容。然而,抓包工具可以截取这些数据包,并将其内容以明文或加密的形式展示给用户。如果数据包是以明文形式发送的,或者我们可以推断出其加密方法,那么我们就可以对这些数据包进行深入的分析和解密。这样,我们便可以了解这些数据包的内容、用途和意义。


通过使用抓包工具,开发人员和系统管理员可以调试和分析网络应用程序和通信协议,以便更好地了解它们的性能、安全性和可靠性。此外,普通用户也可以利用抓包工具来了解他们正在使用的应用程序和网络服务的内部工作原理,并保障他们的网络安全和隐私。总之,抓包工具是计算机网络中不可或缺的一部分,它为我们提供了深入洞察和分析网络数据流量的能力。


抓包工具更多的分析网络流量以及数据包内容、检测网络问题、获取数据传输的详细信息、功能调试等。市面上常见的抓包工具有很多,比如说我们网页上常用到的浏览器开发者工具、移动端常用的vConsole以及市面上大家都在用的Charleswhistle,今天就简单给大家分享一下whistle的基本使用。


1. 简介


whistle主要是基于node来实现的一个跨平台web调试代理工具,whistle采用的是类似配置系统hosts的方式,一切操作都可以通过配置实现,支持域名、路径、正则表达式、通配符、通配路径等多种匹配方式,且可以通过Node模块扩展功能


2. 安装启动


wproxy.org/whistle/ins…


1. 环境


首先要使用whistle的话,必须要具备node环境(下载地址),下载成功后可以通过命令行来查看是否已安装


node -v // 查询当前node版本信息

2.安装whistle


后面也会提到桌面端应用LightProxy,也是基于Electron和whistle的桌面端代理软件。


npm install -g whistle // Windows
sudo npm install -g whistle // Mac(非root权限)

安装成功可以使用命令whistle help 或者w2 help查看whistle的相关帮助信息


3.启动whistle


最新版本的whistle支持三种等价的命令whistlew2wproxy


w2 start // 启动whistle
w2 restart // 重启whistle
w2 stop // 停止whistle
w2 run // 调试模式启动whistle

4.配置代理


这里就以Mac配置举例,可以在设置->Wi-Fi->详细信息->代理中选择网页代理(HTTP)填入对应的ip以及端口号;


移动端这里同样以IOS为例,在Wi-Fi->HTTP代理中打开配置代理为手动,同时填入对应ip以及端口号.


我们也可以通过chrome浏览器插件SwitchyOmega来进行网页代理



  1. 点击新建情景模式

  2. 选择选项代理服务器

  3. 配置代理协议、代理ip、代理端口

  4. 点击应用选项保存并切换到代理模式


switchyOmega


5.证书安装


最后我们只需要安装根证书即可。我们打开whistle生成的浏览器生成页,点开HTTPS选项,点击二维码下载证书,这里同样以MAC和IOS为例。



1.Mac我们打开钥匙串访问,这里要注意,当我们添加完成后依旧属于不被信任状态,我们需要双击证书,在信任的里面找到使用此证书时选中始终信任,配置证书完成后我们选中Capture TUNNEL CONNECTS即可代理成功,捕捉传输内容。


证书
2.IOS同样我们通过扫码打开证书,允许描述配置文件下载,在设置已下载描述文件中安装描述文件,安装完成后我们打开通用->关于本机->证书信任设置中选择对下载的whistle证书启用完全信任即可。



如果是windows系统出现证书无法下载的情况,进入系统设置 - 网络 - Windows防火墙 - 高级设置,设置入站规则Node.js开头的都允许连接,保存确定下载;


手机端偶尔可能会遇到无法找到证书的情况,可以连接同一个局域网,访问电脑ip代理对应ip地址,扫码HTTPS进行证书下载




3.使用


whistle官网这里详细介绍了whistle相关的命令行参数,这里我们就不过多赘述。我们只介绍几个常用的功能。


1.HTTPS请求抓取:


所有配置完成后,我们打开whistle页面,浏览器或手机发起HTTPS请求后即可看到.


image.png
那么问题来了,这么多请求同时包含了预检等众多请求,我们怎么快速找到我们需要看到的接口呢?
我们可以在下方的Type filter text来进行简单的搜索功能,默认是按照url来进行搜索的,我们也可以按照固定的分类规则来进行快速查询



  1. 默认搜索url

  2. h: {word}搜索头部

  3. c: {word}搜索内容

  4. p: {word}搜索请求协议

  5. o: {word}搜索ip

  6. m: {word}搜索方法

  7. s: {word}搜索状态码


如果我们依旧觉得不够清晰该怎么办呢,我们就可以用到whistle的Rules功能,Rules支持我们通过扩展系统host配置来进行所有操作。


// whistle将通过pattern匹配来完成对应uri的操作
pattern operatorURI

pattern的匹配规则支持域名、路径、正则、精准匹配和通配符匹配


api.juejin.cn style://color=@fff&fontStyle=italic&bgColor=red

这样我们就可以更清晰的来找到我们想捕捉的内容。


image.png


2.请求响应修改


我们可以通过固定的Rules配置来对请求或者返回来进行修改测试


{pattern} method://get 请求方式修改为get
{pattern} statusCode://500 请求状态码返回500
{pattern} log:// 日志打印
{pattern} resCors:// 跨域

以上提到的是我们部分的简单修改,如果我们需要修改请求的请求体以及相应内容,我们就需要用到whistle提供的Values板块来进行配置


{pattern} reqHeaders://filepath 修改请求头 //filepath: Values里面的key或者本地文件
// reqHeaders example
{pattern} reqHeaders://{testReqHeadersJSON}
// resBody example
{pattern} resBody://{testResBodyJSON}

Values模版中配置testReqHeadersJSON和testResBodyJSON


image.png


image.png


这样就可以添加或者修改请求头内容,修改或添加响应内容同理。


image.png


3.移动端调试


whistle不仅提供强大的web调试功能,对于移动端的调试也是十分友好的。


由于移动端需要适配众多不同的浏览器和设备,包括各种尺寸的屏幕、各种操作系统和不同的设备载体,因此相对于PC端的页面调试,移动端的调试难度更加复杂。在出现问题时,排查的过程也涉及更多因素,需要我们及时发现并修复问题。对于一个合格的开发人员来说,强大的开发能力是基础,同时还需要拥有快速解决问题的能力和精准定位问题的技能,这样才能够在面对不同的问题时应对自如、犹游刃有余。


像我们在测试环境常用到的vConsole一般是不会在生产环境以一般方式展现给用户的,但是whistle提供注入js的方法能够让我们通过js代码以及vConsole等工具来进行页面调试以及问题的快速排查。


1.接口调试

这里我们就简单的以掘金首页为例



  1. 我们在Values中配置好vConsole以及对应生成实例代码;

  2. 在Rules中通过jsPrepend进行js注入
    这样我们就可以成功生成vConsole来进行调试了



vConsole.min.js的源码我们可以去github上自行下载,或者也可以通过插件来解决。


// 集成 vConsole、eruda、mdebug 等调试H5页面工具的插件
sudo npm install -g whistle.inspect

{pattern} whistle.inspect://vConsole
{pattern} whistle.inspect://eruda
{pattern} whistle.inspect://mdebug

2. 元素样式调试

whistle同时内置了Weinre和Chii来帮助我们进行调试


{pattern} weinre://{yourName}

配置Rules后我们在Whistle下拉选项下选中对应name,重新打开页面后即可进行elment调试


image.png{pattern} 同样配置Rules后我们在Whistle选项下的Plugins选中对应Chii,点击打开后选择inspect来进行element调试


{pattern} whistle.chii://

image-20230921140713086.png


4.LightProxy


下载地址


LightProxy是基于whistle使用electron开发的一个桌面端代理软件,从操作以及证书代理配置上更加简单灵活,更好的满足开发者的需求,基本使用规则同whistle一致;同时也帮我们继承了常用的像inspectvase等插件,更加方便快捷。


5.总结


Whistle作为常用的的网络调试工具之一,它不仅具备常规的抓包功能,还在跨域代理等方面展现了多样化的应用。通过巧妙的配置和强大的功能,我们可以进行深度定制和扩展,以满足各种复杂的调试需求。


这个工具的应用场景非常广泛,从简单的HTTP/HTTPS请求拦截,到复杂的爬虫和自动化测试,都可以借助Whistle实现。同时,它还支持JavaScript、TypeScript等多种编程语言,以及各种浏览器和Node.js环境。


使用Whistle进行调试非常简单,只需要简单地设置和配置,就可以轻松地实现对网络请求的拦截和修改。无论是排查问题、测试接口还是调试前端代码,Whistle都能够帮助我们快速定位问题并解决问题。它的易用性和灵活性也使得它成为了前端开发人员的得力助手。


通过使用Whistle,我们可以更好地了解网络请求的细节,掌握API接口的调用和数据传输的规律。这有助于我们优化代码、提高程序的稳定性和性能。因此,无论是初学者还是经验丰富的开发者,都应该尝试使用Whistle来提升自己的调试技能和开发效率。


参考


1.whistle官网: wproxy.org/whistle/


作者:洞窝技术
来源:juejin.cn/post/7293180747400134706
收起阅读 »

手把手教你打造一个“蚊香”式加载

web
前言 这次给大家带来的是一个类似蚊香加载一样的效果,这个效果还是非常具有视觉欣赏性的,相比前几篇文章的CSS特效,这一次的会比较震撼一点。 效果预览 从效果上看感觉像是一层层蚊香摞在一起,通过动画来使得它们达到3D金钟罩的效果。 HTML布局 首先我们通过1...
继续阅读 »

前言


这次给大家带来的是一个类似蚊香加载一样的效果,这个效果还是非常具有视觉欣赏性的,相比前几篇文章的CSS特效,这一次的会比较震撼一点。


效果预览



从效果上看感觉像是一层层蚊香摞在一起,通过动画来使得它们达到3D金钟罩的效果。


HTML布局


首先我们通过15span子元素来实现金钟罩的每一层,用于创建基本结构。从专业术语上讲,每个span元素都代表加载动画中的一个旋转的小点。通过添加多个span元素,可以创建出一串连续旋转的小点,形成一个加载动画的效果。


<div class="loader">
<span></span>
// 以下省略15span元素
</div>

CSS设计


完成了基本的结构布局,接下来就是为它设计CSS样式了。我们一步一步来分析:


首先是类名为loaderCSS类,相关代码如下。


.loader{
position: relative;
width: 300px;
height: 300px;
transform-style: preserve-3d;
transform: perspective(500px) rotateX(60deg);
}

我们将元素的定位方式设置为相对定位,使其相对于其正常位置进行定位。然后定义好宽度和高度之后,设置元素的变换样式为preserve-3d,这样可以元素的子元素也会受到3D变换的影响。除此之外,还需要transform属性来设置元素的变换效果。这里的perspective(500px)表示以500像素的视角来观察元素,rotateX(60deg)则表示绕X轴顺时针旋转60度。


这样就将一个宽高都定义好的元素进行了透视效果的3D旋转,使其以60度角度绕X轴旋转。


loader类可以理解为父容器,接下来就是loader类中的子元素span


.loader span{
position: absolute;
display: block;
border: 5px solid #fff;
box-shadow: 0 5px 0 #ccc,
inset 0 5px 0 #ccc;
box-sizing: border-box;
border-radius: 50%;
animation: animate 3s ease-in-out infinite;
}

通过以上样式,我们可以创建一个圆形的动画效果,边框有阴影效果,并且以动画的方式不断旋转。关于CSS部分大部分都是一样的,这里主要介绍一下这里定义的动画效果。名称为animate,持续时间为3秒,缓动函数为ease-in-out,并且动画无限循环播放。


@keyframes animate {
0%,100%{
transform: translateZ(-100px);
}
50%{
transform: translateZ(100px);
}
}

这是一个关键帧动画。关键帧是指动画在不同时间点上的状态或样式。首先该动画名为animate,它包含了三个时间点的样式变化:


0%100% 的时间点,元素通过transform: translateZ(-100px)样式将在Z轴上向后移动100像素,这将使元素远离视图。


50% 的时间点,元素通过transform: translateZ(100px)样式将在Z轴上向前移动100像素。这将使元素靠近视图。


通过应用这个动画,span元素将在动画的持续时间内以一定的速率来回移动,从而产生一个视觉上的动态效果。


最后就是单独为每个子元素span赋予样式了。


.loader span:nth-child(1){
top: 0;
left: 0;
bottom: 0;
right: 0;
animation-delay: 1.4s;
}
.loader span:nth-child(2){
top: 10px;
left: 10px;
bottom: 10px;
right: 10px;
animation-delay: 1.3s;
}
......
以下省略到第15span元素

第一个span元素的样式设置了top、left、bottom和right属性为0,这意味着它会填充父元素的整个空间。它还设置了animation-delay属性为1.4秒,表示在加载动画开始之后1.4秒才开始播放动画。


后面14span元素都是按照这个道理,以此类推即可。通过给span元素的动画延迟属性的不同设置,可以实现加载动画的错落感和流畅的过渡效果。


总结


以上就是整个效果的实现过程了,通过设计的动画来实现这个蚊香式加载,整体还是比较简单的。大家可以去码上掘金看看完整代码,然后自己去尝试一下,如果有什么创新的地方或者遇到了什么问题欢迎在评论区告诉我~


作者:一条会coding的Shark
来源:juejin.cn/post/7291951762948259851
收起阅读 »

【Java集合】来了两个“插班生”如何打印花名册,以数组案例带你搞懂Collection集合概念

嗨~ 今天的你过得还好吗?到那时风变得软绵绵的🌞1.1 数组的特点步骤:有三个学生,放在一个长度为3的数组花名册打印学生突然来了两个插班生,请放在数组花名册中无法插入,通过重新定义一个新的数组,组成新的花名册下面我们来实现这个案例:2.输入...
继续阅读 »


嗨~ 今天的你过得还好吗?

每件事情都会好起来的

到那时风变得软绵绵的

阳光也会为你而温暖

🌞


这个系列是我在学习Java集合这部分时候,结合书籍和Java提供的api整理的部分知识,也参考了一些网络上的文章,如果错误,望大家指出。希望本系列文章对大家学习Java有所帮助,也可以回顾下这部分的基础知识,温故而知新。


集合概述

1.1 数组的特点

Java是一种面向对象语言,对一个事物的描述都是以对象的形式存在,为了方便操作这些对象,就需要把这些对象存储起来。为容纳一组对象,我们最适宜的选择就是Array数组;而且容纳一系列的基础数据类型的话,更是必须采用数组。


我们通过一个小案例来回顾一下之前的数组知识。数组不仅可以存放基本数据类型也可以容纳属于同一种类型的对象。


数组案例:一个小班有三个学生,请打印学生的姓名和年龄?

步骤:

  • 有三个学生,放在一个长度为3的数组花名册

  • 打印学生

  • 突然来了两个插班生,请放在数组花名册中

  • 无法插入,通过重新定义一个新的数组,组成新的花名册


下面我们来实现这个案例:

1.首页创建一个 javaee 的项目

Description


2.输入名称 collectPractice,选择对应的 JDK 版本 1.8

Description


3.新增 Class  student

Description

4.文件内容如下:

public class Student {
private String name;
private int age;

public Student(String name, int age) {
this.name = name;
this.age = age;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}

编程学习,从云端源想开始,课程视频、在线书籍、在线编程、一对一咨询……你想要的全部学习资源这里都有,重点是统统免费!点这里可以查看

5.在 Main.java 中,我们将要完成的需求写到注释中,逐行去打印结果

public class Main {
public static void main(String[] args) {

/*
我们的业务需求是这样的:
1. 有三个学生,放在一个长度为3的数组花名册
2. 打印学生
3. 突然来了两个插班生,请放在数组花名册中
4. 请获取第三个学生的姓名
* */

//定义一个数组,存储我们的数据
Student[] students = new Student[3];
Student student1 = new Student("麦迪", 29);
Student student2 = new Student("库里", 29);
Student student3 = new Student("杜兰特", 29);


//记录设置到数组中
students[0] = student1;
students[1] = student2;
students[2] = student3;

//打印 花名册 通过数组工具
System.out.println("学生花名册---"+ Arrays.toString(students));
}
}

结果:

Description

6.来了两个新学生,也要加入到花名册中,直接使用数组添加,打印花名册,发现报错

import java.util.Arrays;

public class Main {
public static void main(String[] args) {

/*
我们的业务需求是这样的:
1. 有三个学生,放在一个长度为3的数组花名册
2. 打印学生
3. 突然来了两个插班生,请放在数组花名册中
4. 请获取第三个学生的姓名
* */

//定义一个数组,存储我们的数据
Student[] students = new Student[3];
Student student1 = new Student("麦迪", 29);
Student student2 = new Student("库里", 29);
Student student3 = new Student("杜兰特", 29);


//记录设置到数组中
students[0] = student1;
students[1] = student2;
students[2] = student3;

//打印 花名册 通过数组工具
System.out.println("学生花名册---"+ Arrays.toString(students));

//两个插班生
Student student4 = new Student("科比", 40);
Student student5 = new Student("欧文", 25);

students[3] = student4;
students[4] = student5;

System.out.println("学生花名册---"+ Arrays.toString(students));
}
}

打印结果

Description


7.所以在此时,我们需要重新 new 一个 长度为 5 的数组,重新设置新的花名册

public class Main {
public static void main(String[] args) {

/*
我们的业务需求是这样的:
1. 有三个学生,放在一个长度为3的数组花名册
2. 打印学生
3. 突然来了两个插班生,请放在数组花名册中
4. 请获取第三个学生的姓名
* */

//定义一个数组,存储我们的数据
Student[] students = new Student[3];
Student student1 = new Student("麦迪", 29);
Student student2 = new Student("库里", 29);
Student student3 = new Student("杜兰特", 29);


//记录设置到数组中
students[0] = student1;
students[1] = student2;
students[2] = student3;

//打印 花名册 通过数组工具
System.out.println("学生花名册---"+ Arrays.toString(students));

//两个插班生
Student student4 = new Student("科比", 40);
Student student5 = new Student("欧文", 25);

// students[3] = student4;
// students[4] = student5;
//
// System.out.println("学生花名册---"+ Arrays.toString(students));

Student[] studentsNew = new Student[5];
for (int i = 0; i < students.length; i++) {
studentsNew[i] = students[i];
}
studentsNew[3] = student4;
studentsNew[4] = student5;


System.out.println("学生花名册---"+Arrays.toString(students));
System.out.println("学生新的花名册---"+Arrays.toString(studentsNew));
}
}

打印结果:

Description


分析结论

  • 数组长度在初始化后,就确定了,不能更改,不便于存储数量的扩展。比如我们再来了两个插班生,直接往元素组添加元素,会报错误信息。

  • 数组提供的属性和方法少,不便于操作。比如我们在打印花名册时,需要借助工具类的toString方法。

  • 存储的类型可以是基本类型,也可以是对象,但是必须是同一类型。


因为数组存在的这些缺点,Java语言又为我们提供了一种新的存储数据并且存储空间可变的容器,这就是我们Java集合的概念。它和数组一样,都是可以存储数据的容器,一种存储空间可变的存储模型,并且存储的数据容量可以随时发生改变。


1.2 集合的特点

最后我们来总结一下集合的特点:

  • 可以动态保存任意多的对象,使用方便;

  • 集合提供了一系列操作元素的方法,使集合元素的添加和修改等操作变得简单;

  • 集合还可以保存具有映射关系的关联数据;

  • 集合只能保存对象,实际上保存的是对象的引用地址。


文章就写到这里了,觉得不错的话点个赞支持一下吧!


收起阅读 »

为了方便写文章,我开发了一个目录树🌲生成器

web
这个工具主要是为了方便在写文章的时候,展示自己的项目的目录结构,或者在README文件中介绍项目使用的,上传文件夹后可以生成目录结构,支持一键复制。 您可以通过以下链接访问:目录树生成器 - 在线使用 Next.js 是一个React全栈框架,它不仅可以用于...
继续阅读 »

这个工具主要是为了方便在写文章的时候,展示自己的项目的目录结构,或者在README文件中介绍项目使用的,上传文件夹后可以生成目录结构,支持一键复制。


image.png


您可以通过以下链接访问:目录树生成器 - 在线使用


Next.js


是一个React全栈框架,它不仅可以用于构建服务器端渲染(SSR),也支持支持静态渲染。


webkitdirectory



HTMLInputElement.webkitdirectory 是一个反应了 HTML 属性 webkitdirectory 的属性,其指示 <input> 元素应该让用户选择文件目录而非文件。在选择文件目录后,该目录及其整个内容层次结构将包含在所选项目集内。可以使用 webkitEntries (en-US) 属性获取选定的文件系统条目。

———————MDN



简而言之 利用这属性,我们可以在浏览器中上传文件夹,并获取到文件的目录结构。


可以看一个简单的栗子🌰


这个功能,也有一个兼容问题,具体参考这个:


image.png


有一些老版本的浏览器和安卓端火狐浏览器不支持的无法使用该功能。


数据转换


我们要将原数据转换一下


-   Java/main/main.java
- Java/main/main.class
- Java/hello/HelloWorld.class
- Java/hello/HelloWorld.java
- Java/OOP/xx.js
- Java/OOP/Person.class
- Java/OOP/oop.class
- Java/OOP/oop.java

转换为:


{
"name": "Java",
"type": "folder",
"contents": [
{
"name": "main",
"type": "folder",
"contents": [
{
"name": "main.java",
"type": "file"
},
{
"name": "main.class",
"type": "file"
}
]
},
{
"name": "hello",
"type": "folder",
"contents": [
{
"name": "HelloWorld.class",
"type": "file"
},
{
"name": "HelloWorld.java",
"type": "file"
}
]
},
{
"name": "OOP",
"type": "folder",
"contents": [
{
"name": "xx.js",
"type": "file"
},
{
"name": "Person.class",
"type": "file"
},
{
"name": "oop.class",
"type": "file"
},
{
"name": "oop.java",
"type": "file"
}
]
}
]
}

将路径结构转化为对象结构,方便我们的后续逻辑处理,转化方法是:



function convertToDirectoryStructure(fileList) {
const directory = {
name: "App",
type: "folder",
contents: [],
};

for (let i = 0; i < fileList.length; i++) {
const pathSegments = fileList[i].webkitRelativePath.split("/");
let currentDirectory = directory;

for (let j = 0; j < pathSegments.length; j++) {
const segment = pathSegments[j];
const isDirectory = j < pathSegments.length - 1;

let existingEntry = currentDirectory.contents.find((entry) => entry.name === segment);
if (!existingEntry) {
existingEntry = { name: segment };
if (isDirectory) {
existingEntry.type = "folder";
existingEntry.contents = [];
} else {
existingEntry.type = "file";
}
currentDirectory.contents.push(existingEntry);
}

currentDirectory = existingEntry;
}
}

return directory.contents[0];
}

最终效果


最后我们再加上一个一键复制的功能,就完成了。



最后我是将功能优化后部署到了GitHub Pagas上,如何将Next.js部署到GitHub Pages,可以看看我的这篇 如何将Next.js部署到Github Pages


最后希望大家多多使用,给个star,GitHub地址:dir-tree


作者:九旬
来源:juejin.cn/post/7292955000454692875
收起阅读 »

ElectronEgg 快速开发一个桌面应用

web
大家好,我是哆啦好梦。electron-egg 3.8.0 终于发布了。 近3个月的累积更新,让 electron-egg 框架的开发体验更加丝滑。框架也终于到了一个功能完善且非常稳定的版本。 目前,框架已经广泛应用于记账、政务、企业、医疗、学校、股票交易、E...
继续阅读 »

大家好,我是哆啦好梦。electron-egg 3.8.0 终于发布了。


近3个月的累积更新,让 electron-egg 框架的开发体验更加丝滑。框架也终于到了一个功能完善且非常稳定的版本。


目前,框架已经广泛应用于记账、政务、企业、医疗、学校、股票交易、ERP、娱乐、视频等领域客户端,请放心使用!


home.png


为什么使用


桌面软件(办公方向、 个人工具),仍然是未来十几年 PC 端需求之一,提高工作效率


electron 技术是流行趋势,QQ、百度翻译、阿里网盘、迅雷、有道云笔记 ......


开源


gitee:gitee.com/dromara/ele… 3900+


github:github.com/dromara/ele… 1200+


本次更新


3.8.0



  1. 【增加】新增 ee-bin exec 命令,支持自定义命令。

  2. 【增加】新增 ee-core jobs 配置,打开/关闭 messageLog。

  3. 【优化】优化 ee-core jsondb 异常处理。

  4. 【优化】优化 ee-core controller/services 异常捕获并写log。

  5. 【优化】优化 ee-bin loading 动画居中。

  6. 【优化】优化 electron-egg logo,优化mac图标,优化Linux系统图标。

  7. 【优化】优化 electron-egg loading 动画居中。

  8. 【升级】升级ee-core v2.6.0,升级ee-bin v1.3.0


下载


# gitee
git clone https://gitee.com/dromara/electron-egg.git

# github
git clone https://github.com/dromara/electron-egg.git

安装


# 设置国内镜像源(加速)
npm config set registry=https://registry.npmmirror.com

#如果下载electron慢,配置如下
npm config set electron_mirror=https://registry.npmmirror.com/-/binary/electron/

# 根目录,安装 electron 依赖
npm i

# 进入【前端目录】安装 frontend 依赖
cd frontend
npm i

运行项目


npm run start

example.png


用户案例


aw-3.png


p1.png


p3.png


更多


访问官网:electron-egg: 一个入门简单、跨平台、企业级桌面软件开发框架


作者:哆啦好梦
来源:juejin.cn/post/7292961931509186595
收起阅读 »

菜鸟前端故事之翅膀硬了

web
2019的故事 最近太忙再加上掘金对此类文章并不推荐,所以写作热情有所消退,停了一段时间没更新,现趁着有点空再继续写一下2019的故事。 话接前文,在之前度过了还算完美的2018,收获了比较满意的工作,也找到了对象,可谓是事业爱情小丰收。 在过年的时候,我人生...
继续阅读 »

2019的故事


最近太忙再加上掘金对此类文章并不推荐,所以写作热情有所消退,停了一段时间没更新,现趁着有点空再继续写一下2019的故事。


话接前文,在之前度过了还算完美的2018,收获了比较满意的工作,也找到了对象,可谓是事业爱情小丰收。


在过年的时候,我人生第一次有了上万存款,发了不少红包给家里老人和晚辈,那种回报家人的感觉真的很好。
现在看来9k的薪资不算什么,但算是很快达到了老爸的预期。若不是计算机专业和互联网的崛起,我这破学校不可能刚毕业就有这种薪资,我庆幸生对了时代,做了对的选择。


加薪


小美妆没有年终奖,年末聚餐的时候发了800块红包,我收到很开心只觉得这是一笔意外的收入。当时也不懂什么13薪之类的,更不知道、也不敢想象在互联网还有四五个月的年终奖,觉得没有很正常。即使有听说腾讯给全体发iphone、游戏工作室数十个月年终,也觉得很遥远,那远不是我能触及的层次,又何苦去比较。


包括小美妆的976作息,因为自己的菜,便也觉得不合理的事也是合理。


可喜的是刚过完年不久,小张总就给我们加了工资,1000块,我开心了很久,不是仅仅是因为多了1000块,另一层含义是我的月薪上万了,传说中的月薪过万我终于达到了,我还记得我当时激动的跟我爸妈还有女友分享,爸妈直呼老板良心让我跟着好好干,在他们眼里能主动加薪还加这么多的太难得了,回想起来还是觉得那一刻很幸福。


躺不平


今年我们有个任务就是要把小美妆的网站做成一个小程序,一度令当时的我头疼,因为我只会jquery连vue都不会更别说小程序了。一开始甚至想直接用小程序包裹webview直接套壳做好的网站了,但是理智和直觉告诉我不能那样,可能会挖大坑。


没办法只有埋头苦学,没想到仅仅两天我就可以上手了,并且把网站最难的特效部分转为用小程序实现了,原来小程序也没那么难嘛,心想我还是有点天赋的。


到后来的其他网站我开始尝试用vue去做,发现跟小程序几乎都是一样,也就顺风顺水了。估计这个顺序跟大多数前端都是反着来的,但也总算是会了一些主流现代的技术。
可惜那时候我还是用的引入CDN的方式引入vue,没有尝试构建工具,这也让我在后面的职业经历中吃了瘪。


在那一年我们做了很多小程序,移动端H5,后台管理系统,内部外部用的都有,对移动端开发、兼容有了一定的技术积累,后面也做了支付宝的小程序。


其实大多都是重复的技术,除了熟练度没有什么提高,到年中的时候已经算是进入平稳开发期,没有什么水花,也没有了深入学习的心思,因为大部分需求已经不需要动脑子
就能实现。用现在流行的一个词来说就是:躺平了。


小张总讲情义,只要我认真完成任务他应该不会赶我走,小作坊朋友式的公司氛围舒服,薪水也不错,不禁开始妄想熬成老员工躺平一辈子多好。


我突然意识到,自己好像成了守哥,可这似乎也没错。


幸运的是接下来发现的一件事让我意识到技术必须不断进步,而不幸的是明白这个道理让我们付出了惨重的代价。


昂贵的教训


在我们来之前公司是有一台旧的云服务器,是某财务软件公司给我们部署财务系统用,使用windowsServer系统,里面存放了相当多的财务信息,以及我们的公众号服务端项目、数据库。


在一天早上我们突然接到用户反馈公众号报错服务故障了,起初我们还以为又是夜间突发流量导致宕机了,心想着重启一下就好。可进入服务器一看却傻了眼,各种文件都被清空,变成了加密文件,连数据库也被删得一干二净只留下大约这么一段话:


recovery data connect email xxx


我们才意识到服务器被攻击入侵了,黑客将我们的数据全都加密,并勒索要求支付比特币才能解锁。


我们的财务数据、公众号用户的充值信息全没了。我清晰的记得,那一刻我的心凉到了冰点,因为我们的数据库已近半年没有备份。即使宽宏大量的小张总知道后也有些生气了,是啊,这件事带来的后果太麻烦了,毫无疑问技术部门负全责。


面对小张总的追问,我异常艰难的开口告诉了实情,在写这段的时候我仍然感觉令人窒息。
报警?支付赎金?找专业人士解密?那段时间我忙得团团转,急得像热锅上的蚂蚁,只想能够挽回损失。比特币赎金的价格高达十几万,而且也非常冒险很可能被再次勒索或者解不开,咨询专业人士都说解不了,甚至还有人冒出来说知道谁干的,可以作为中间人担保帮我们付费解锁。


我才明白原来这种事早已经形成了一条完整的黑色产业链,同时也对黑客这个词不再敬畏,而是深恶痛绝。随意的一次攻击可能毁掉一家公司,让上百无辜的人失业。


还记得有个坊间传闻说阿里面试某低学历高手,高手当面破解阿里系统,被破格录取并重用。有人如法炮制面试鹅厂,刚破解完腾讯系统当场报警被抓。以此来传言阿里格局大,腾讯格局小。虽然这个流言多半是假的,但我的态度在经历被勒索之后也变成了支持腾讯的一方,违法的事就是违法,不因场合而改变性质,永远也不应鼓励这种行为。不然谁都去破解一下,总有出大事的一天。


后来经过深思熟虑小张总还是决定不屈服,而是在现在基础上补救、向用户致歉补偿等措施。而我也开始拼命恶补网络安全方面的知识,这种情况要是再出现一次,即使老板不开除我,我自己也没脸待下去了。后来我们将系统换成了linux,把不该打开的端口也都关闭了,设置了强密码,安排了各种云服务商的安全服务,最重要的是做了db的定时备份。从那以后我们没再出过安全上的问题。


这件事给我好好的上了一课,在解决问题的过程中,面对很多技术一抹黑的境况也让我觉得心里没底,同时给了我要不断学习深入研究的决心。


技术态度的转变


自那以后,我意识到了技术是片汪洋大海,而之前的我不过是在小水坑里扑腾。我开始学习很多工作中暂时用不到的技术,或者在工作中尽量使用新的技术,这也是小团队的好处,没有历史包袱想怎么搞就怎么搞。


我开始学了react,学了php做自己厌恶的服务端,学了linux买了自己的服务器做了自己的网站,也用RN开始写App,第一次了解到了算法题的存在并开始练习,我疯狂的想要提升自己,不允许存在任何技术上的短板。我给自己列了长长的一串学习清单,还因为有其他公司看中我们的小程序想嵌入,去跟上市公司谈合作,签合同。那是除了在小外包入职时期以外的另一段高速汲取知识并成长的时光。



如果有前端初学的朋友在看,我想顺便提一下,个人认为想成为一名优秀的软件工程师,服务端是你无法避开的一环,虽然不需要精通,但是也不要一味的躲避。



2019在经历波折后归于平淡,在平淡的年末却又爆发了疫情,平时总是和女友两个城市来回跑,因为疫情居家被关到了一起,她还担心总被封闭在一起两个人会矛盾显现,结果没想到住一起那么快乐,感情越来越好,給枯燥的广漂生活增加了很甜蜜的一段时光。哈哈抱歉秀了一段,那时候真是觉得糟了,我陷入爱河啦。


离开小美妆


没有不散的宴席。


偶尔会听到小张总吐槽我们部门一年成本几十万好肉疼,不管是不是玩笑话,我也理解这种体量的公司养几个全职的程序员是极为奢侈的。


后来的近一年内我的工资逐渐涨到了12k左右,那时候的我对市面上的行情开始有了些了解,我同时兼任产品、ui、前端、项目管理的工作量,这个水平还是偏低的。


再加上当时的我也开始为未来做打算,我想要的东西很多,我也想给我爱的人更多,因为后来我接过了武汉房子的房贷,当时一个月扣除花费后只能存下三千块,照这个水平我猴年马月能过上我想要的生活啊。而小张总的话也侧面反应了我的薪水已经不会再有什么增长空间。说得更严重一点,技术部门甚至已经成为了公司的负担,后面小张总已经打算接外面的项目来做了。


有了爱人,有了技术,随之而来的便是动力与野心。


虽然我理解守哥,但我却不再想成为守哥。


总结下离开的3大原因:



  • 翅膀硬了觉得没有成长空间了

  • 真心希望给公司减负

  • 不想接外包项目
    基于以上的因素,我决定去到深圳,开始面试,我想要进入更强大的公司。


面试吃瘪之旅


第一次打开了招聘软件参与社招(找实习只用了下实习僧),被hr打招呼的热情搞得受宠若惊,后来才知道全是外包.....


很快就又遇到了某软,没错就是那个实习要录用又不留用我的某软,上来就要我学位证,没有社招经验的我还以为都是这样,啥也没干就老老实实的交了资料。


那时候不觉得外包经历是扣分项,得意洋洋的把小外包的实习经历写在了简历上,充了充工作年限,还写了自己兼任各种岗位(现在看来无疑是扣分项),估计hr心想这人真是个二笔。


hr先问了我现在的薪资,听完说他们这边最多给我13k,他们是按经验算薪资,你再牛不好意思你练习时长都还不够两年半,13k封顶了。我还是试着面了一下非常容易就过了,但是没选择去,只是试了试面试的感觉,我还是不傻的,这个涨幅和公司不值得。后来又面了一些公司比如万科、宝能之类的大型企业,基本有一半的通过率,那个阶段真是处于互联网的黄金年代,岗位多经济好。


当时很喜欢万科,可惜面试官问我微服务我根本答不上来,遗憾挂掉。至于bat之流的公司,还是不敢想。后来还是忍不住试着投了下腾讯,居然拿到了面试机会,我望着印着腾讯logo的面试邀请邮件,充满了渴望。至于面试结果当然不必期待,被一番吊打后挂掉,因为差距太大我也就对互联网大厂死了心。不过当时面试的小鹅拼拼部门据说非常累,而且后面整体被裁撤了。


之后通过了宝能的面试,在当时的我眼里可是大公司,公积金顶格交满,还有双休和食堂,虽然把我薪资压到了13k但我还是兴致勃勃准备去,可是offer一直没审批下来,当时搞得我一度很失落。


后来才知道宝能当时处于内部动荡期,已经锁了hc。


涨幅70%


宝能一直没消息我只能继续面,后来有一家大小周跨境电商公司给我了16x13,还有一家双休金融公司给了18x14,果断选择了后者,这个涨幅令当时的我激动不已,在接到oc后差点没跳起来,也算是正式年薪突破二十万了。


而且还给我了高级前端工程师的title,虽然也是在后来的经历中才知道非名企的职级就是个鸡肋,但当时可以说满足至极。意外的是宝能offer卡了其实是一件大好事,也或许是命运的眷顾。


因为过后不久宝能hr说愿意立即给我发offer,但是当时我已经入职金融公司就拒绝了。再后来就是宝能公积金断缴的消息,hr自己也离职了,再到前两天还看到宝能的姚总拖欠薪资被打的新闻,没进去走了狗屎运。


在拿到offer当天我跟小张总提了离职,他很惊讶,挽留了我并且说愿意加薪,但是我知道不可能在小美妆发展了,离开公司对双方都是好事,小张总的挽留可能只是出于客套或者情义。


其他几个小伙伴也很震惊,不理解这么舒服的地方为什么要离开,去外面肯定受拘束还要加班。可我已执意离开。


因为不好说那些肉麻的话,我在微信给小张总发了长长的感谢的文字。离开那月的工资多了几千奖金,我明白那是小张总对我的肯定,离职后的一天我仍然去了公司,把一个新项目的需求框架谈好才走,免得小伙伴们一时没法适应要自己去处理需求的情况。


至此,我正式离开了小美妆,而跟我一起来广州的小伙伴现在2023年仍然在那里就职。
非常感谢小张总的信任和包容,愿意给一个不认识的菜鸟机会,让他去组建团队,管理项目,而我也回报了一个两年流水过亿的项目。


这个项目也成为了我简历上闪耀的一段背书,或许也是之前大厂愿意给我面试机会的原因。


巅峰跃升


写得有点多了,下次再继续写在金融公司呆了不久又离开的故事,以及我职业经历的的再一次巅峰跃升,欢迎各位小伙伴留言交流~


作者:鹅猴
来源:juejin.cn/post/7264383071318671421
收起阅读 »

如何制作 GitHub 个人主页

web
原文链接:http://www.bengreenberg.dev/posts/2023-… 人们在网上首先发现你的地方是哪里?也许你的社交媒体是人们搜索你时首先发现的东西,亦也许是你为自己创建的投资组合网站。然而,如果你使用GitHub来分享你的代码并参与开源...
继续阅读 »

原文链接:http://www.bengreenberg.dev/posts/2023-…


人们在网上首先发现你的地方是哪里?也许你的社交媒体是人们搜索你时首先发现的东西,亦也许是你为自己创建的投资组合网站。然而,如果你使用GitHub来分享你的代码并参与开源项目,那么你的GitHub个人主页可能是人们为了了解你而去的第一个地方。


你希望你的GitHub个人主页说些什么?你希望如何以简明易读的方式向访客表达对你的重要性以及你是谁?无论他们是未来的雇主还是开源项目的潜在合作伙伴,你都必须拥有一个引人注目的个人主页。


使用GitHub Actions,你可以把一个静态的markdown文档变成一个动态的、保持对你最新信息更新的良好体验。那么如何做到这一点呢?


我将向你展示一个例子,告诉你如何在不费吹灰之力的情况下迅速做到这一点。在这个例子中,你将学习如何抓取一个网站并使用这些数据来动态更新你的GitHub个人主页。我们将在Ruby中展示这个例子,但你也可以用JavaScript、TypeScript、Python或其他语言来做。


GitHub个人主页如何运作


你的GitHub个人主页可以通过在网页浏览器中访问github.com/[你的用户名]找到。那么该页面的内容来自哪里?


它存在于你账户中一个特殊的仓库中,名称为你的账户用户名。如果你还没有这个仓库,当你访问github.com/[你的用户名]时,你不会看到任何特殊的内容,所以第一步是确保你已经创建了这个仓库,如果你还没有,就去创建它。


探索仓库中的文件


仓库中唯一需要的文件是README.md文件,它是你的个人主页页面的来源。


./
├── README.md

继续在这个文件中添加一些内容并保存,刷新你的用户名主页,你会看到这些内容反映在那里。


为动态内容添加正确的文件夹


在我们创建代码以使我们的个人主页动态化之前,让我们先添加文件夹结构。


在顶层添加一个名为.github的新文件夹,在.github内部添加两个新的子文件夹:scripts/workflows/


你的文件结构现在应该是这样的:


./
├── .github/
│ ├── scripts/
│ └── workflows/
└── README.md

制作一个动态个人主页


对于这个例子,我们需要做三件事:



  • README中定义一个放置动态内容的地方

  • scripts/中添加一个脚本,用来完成爬取工作

  • workflows/中为GitHub Actions添加一个工作流,按计划运行该脚本


现在让我们逐步实现。


更新README


我们需要在README中增加一个部分,可以用正则来抓取脚本进行修改。它可以是你的具体使用情况所需要的任何内容。在这个例子中,我们将在README中添加一个最近博客文章的部分。


在代码编辑器中打开README.md文件,添加以下内容:


### Recent blog posts

现在我们有了一个供脚本查找的区域。


创建脚本


我们正在构建的示例脚本是用Ruby编写的,使用GitHub gem octokit与你的仓库进行交互,使用nokogiri gem爬取网站,并使用httparty gem进行HTTP请求。


在下面这个例子中,要爬取的元素已经被确定了。在你自己的用例中,你需要明确你想爬取的网站上的元素的路径,毫无疑问它将不同于下面显示的在 posts 变量中定义的,以及每个post的每个titlelink


下面是示例代码,将其放在scripts/文件夹中:


require 'httparty'
require 'nokogiri'
require 'octokit'

# Scrape blog posts from the website
url = "<https://www.bengreenberg.dev/blog/>"
response = HTTParty.get(url)
parsed_page = Nokogiri::HTML(response.body)
posts = parsed_page.css('.flex.flex-col.rounded-lg.shadow-lg.overflow-hidden')

# Generate the updated blog posts list (top 5)
posts_list = ["\n### Recent Blog Posts\n\n"]
posts.first(5).each do |post|
title = post.css('p.text-xl.font-semibold.text-gray-900').text.strip
link = "<https://www.bengreenberg.dev#{post.at_css('a')[:href]}>"
posts_list << "* [#{title}](#{link})"
end

# Update the README.md file
client = Octokit::Client.new(access_token: ENV['GITHUB_TOKEN'])
repo = ENV['GITHUB_REPOSITORY']
readme = client.readme(repo)
readme_content = Base64.decode64(readme[:content]).force_encoding('UTF-8')

# Replace the existing blog posts section
posts_regex = /### Recent Blog Posts\n\n[\s\S]*?(?=<\/td>)/m
updated_content = readme_content.sub(posts_regex, "#{posts_list.join("\n")}\n")

client.update_contents(repo, 'README.md', 'Update recent blog posts', readme[:sha], updated_content)

正如你所看到的,首先向网站发出一个HTTP请求,然后收集有博客文章的部分,并将数据分配给一个posts变量。然后,脚本在posts变量中遍历博客文章,并收集其中的前5个。你可能想根据自己的需要改变这个数字。每循环一次博文,就有一篇博文被添加到post_list的数组中,其中有该博文的标题和URL。


最后,README文件被更新,首先使用octokit gem找到它,然后在README中找到要更新的地方,并使用一些正则: posts_regex = /### Recent Blog Posts\n\n[\s\S]*?(?=<\/td>)/m


这个脚本将完成工作,但实际上没有任何东西在调用这个脚本。它是如何被运行的呢?这就轮到GitHub Actions出场了!


创建Action工作流


现在我们已经有了脚本,我们需要一种方法来按计划自动运行它。GitHub Actions 提供了一种强大的方式来自动化各种任务,包括运行脚本。在这种情况下,我们将创建一个GitHub Actions工作流,每周在周日午夜运行一次该脚本。


工作流文件应该放在.github/workflows/目录下,可以命名为update_blog_posts.yml之类的。以下是工作流文件的内容:


name: Update Recent Blog Posts

on:
schedule:
- cron: '0 0 * * 0' # Run once a week at 00:00 (midnight) on Sunday
workflow_dispatch:

jobs:
update_posts:
runs-on: ubuntu-latest

steps:
- name: Check out repository
uses: actions/checkout@v2

- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.1

- name: Install dependencies
run: gem install httparty nokogiri octokit

- name: Scrape posts and update README
run: ruby ./.github/scripts/update_posts.rb
env:
GITHUB_TOKEN: $
GITHUB_REPOSITORY: $

这个工作流是根据cron语法定义的时间表触发的,该时间表指定它应该在每个星期天的00:00(午夜)运行。此外,还可以使用workflow_dispatch事件来手动触发该工作流。


update_posts工作由几个步骤组成:



  • 使用 actions/checkout@v2操作来签出仓库。

  • 使用 ruby/setup-ruby@v1 操作来设置 Ruby,指定的 Ruby 版本为 3.1。

  • 使用 gem install 命令安装所需的 Ruby 依赖(httpartynokogirioctokit)。

  • 运行位于.github/scripts/目录下的脚本 update_posts.rbGITHUB_TOKENGITHUB_REPOSITORY环境变量被提供给脚本,使其能够与仓库进行交互。


有了这个工作流程,你的脚本就会每周自动运行,抓取博客文章并更新README文件。GitHub Actions负责所有的调度和执行工作,使整个过程无缝且高效。


将所有的东西放在一起


如今,你的网络形象往往是人们与你联系的第一个接触点--无论他们是潜在的雇主、合作者,还是开源项目的贡献者。尤其是你的GitHub个人主页,是一个展示你的技能、项目和兴趣的宝贵平台。那么,如何确保你的GitHub个人主页是最新的、相关的,并能真正反映出你是谁?


通过利用 GitHub Actions 的力量,我们展示了如何将你的 GitHub 配置文件从一个静态的 Markdown 文档转变为一个动态的、不断变化关于你是谁的例子。通过本指南提供的例子,你已经学会了如何从网站上抓取数据,并利用它来动态更新你的 GitHub个人主页。虽然我们的例子是用Ruby实现的,但同样的原则也可以用JavaScript、TypeScript、Python或你选择的任何其他语言来应用。


回顾一下,我们完成了创建一个Ruby脚本的过程,该脚本可以从网站上抓取博客文章,提取相关信息,并更新你的README.md文件中的"最近博客文章"部分。然后,我们使用GitHub Actions设置了一个工作流,定期运行该脚本,确保你的个人主页中保持最新的内容。


但我们的旅程并没有就此结束。本指南中分享的技术和方法可以作为进一步探索和创造的基础。无论是从其他来源拉取数据,与API集成,还是尝试不同的内容格式,都有无限的可能性。


因此,行动起来让你的 GitHub 个人主页成为你自己的一个充满活力的扩展。让它讲述你的故事,突出你的成就,并邀请你与他人合作。


以上就是本文的全部内容,如果对你有所启发,欢迎点赞、收藏、转发~


作者:chuck
来源:juejin.cn/post/7251884086537650232
收起阅读 »

什么!一个项目给了8个字体包???

web
🙋 遇到的问题 在一个新项目中,设计统一了项目中所有的字体,并提供了字体包。在项目中需要按需引入这些字体包。 首先,字体包的使用分为了以下几种情况: 无特殊要求的语言使用字体A,阿拉伯语言使用字体B; 加粗、中等、常规、偏细四种样式,AB两种字体分别对应使用...
继续阅读 »

🙋 遇到的问题


在一个新项目中,设计统一了项目中所有的字体,并提供了字体包。在项目中需要按需引入这些字体包。


首先,字体包的使用分为了以下几种情况:



  1. 无特殊要求的语言使用字体A,阿拉伯语言使用字体B;

  2. 加粗、中等、常规、偏细四种样式,AB两种字体分别对应使用 BoldMediumRegularThin 四种字体包;


所以,我现在桌面上摆着 8 个字体包:



  • A-Bold.tff

  • A-Medium.tff

  • A-Regular.tff

  • A-Thin.tff

  • B-Bold.tff

  • B-Medium.tff

  • B-Regular.tff

  • B-Thin.tff


image.png
不同语言要使用不同的字体包,不同粗细也要使用不同的字体包!


还有一个前提是,设计给的设计图都是以字体A为准,所以在 Figma 中复制出来的 CSS 代码中字体名称都是A。


刚接到这个需求时还是比较懵的,一时想不出来怎么样才能以最少的逻辑判断最少的文件下载最少的代码改动去实现在不同情况下自动的去选择对应的字体包。


因为要涉及到语言的判断,最先想到的还是通过 JS,然后去添加相应的类名。但这样也只能判断语言使用A或B,粗细还是解决不了。


image.png


看来还是要用 CSS 解决。


首先我将所有的8个字体先定义好:


@font-face {
font-family: A-Bold;
src: url('./fonts/A-Bold.ttf');
}

/* ... */

@font-face {
font-family: B-Thin;
src: url('./fonts/B-Thin.ttf');
}

image.png


🤲🏼 如何根据粗细程度自动选择对应字体包


有同学可能会问,为什么不直接使用 font-weight 来控制粗细而是用不同的字体包呢?


我们来看下面这个例子,我们使用同一个字体, font-weight 分别设置为900、500、100,结果我们看到的字体粗细是一样的。


对的,很多字体不支持 font-weight 所以我们需要用不同粗细的字体包。


image.png


所以,我们可以通过 @font-face 中的 font-weight 属性来设置字体的宽度:


@font-face {
font-family: A;
src: url('./fonts/A-Bold.ttf');
font-weight: 600;
}
@font-face {
font-family: A;
src: url('./fonts/A-Medium.ttf');
font-weight: 500;
}
@font-face {
font-family: A;
src: url('./fonts/A-Regular.ttf');
font-weight: 400;
}
@font-face {
font-family: A;
src: url('./fonts/A-Thin.ttf');
font-weight: 300;
}

注意,这里我们把字体名字都设为相同的,如下图所示,这样我们就成功的解决了第一个问题:不同粗细也要使用不同的字体包;


image.png


并且,如果我们只是定义而未真正使用时,不会去下载未使用的字体包,再加上字体包的缓存策略,就可以最大程度节省带宽:


image.png


🔤 如何根据不同语言自动选择字体包?


通过张鑫旭的博客找到了解决办法,使用 unicode-range 设置字符 unicode 范围,从而自定义字体包。


unicode-range 是一个 CSS 属性,用于指定字体文件所支持的 Unicode 字符范围,以便在显示文本时选择适合的字体。


它的语法如下:


@font-face {
font-family: "Font Name";
src: url("font.woff2") format("woff2");
unicode-range: U+0020-007E, U+4E00-9FFF;
}

在上述例子中,unicode-range 属性指定了字体文件支持的字符范围。使用逗号分隔不同的范围,并使用 U+XXXX-XXXX 的形式表示 Unicode 字符代码的范围。


通过设置 unicode-range 属性,可以优化字体加载和页面渲染性能,只加载所需的字符范围,减少不必要的网络请求和资源占用。


通过查表得知阿拉伯语的 unicode 的范围为:U+06??, U+0750-077F, U+08A0-08FF, U+FB50-FDFF, U+FE70-FEFF, U+10A60-10A7F, U+10A80-10A9F 这么几个区间。所以我们设置字体如下,因为设计以 A 字体为准,所以在 Figma 中给出的样式代码字体名均为 A,所以我们把 B 字体的字体名也设置为 A:


image.png


当使用字体的字符中命中 unicode-rang 的范围时,自动下载相应的字体包。


@font-face {
font-family: A;
src: url('./fonts/A-Bold.ttf');
font-weight: 600;
}

@font-face {
font-family: A;
src: url('./fonts/A-Medium.ttf');
font-weight: 500;
}

@font-face {
font-family: A;
src: url('./fonts/A-Regular.ttf');
font-weight: 400;
}

@font-face {
font-family: A;
src: url('./fonts/A-Thin.ttf');
font-weight: 300;
}

:root {
--ARABIC_UNICODE_RANGE: U+06??, U+0750-077F, U+08A0-08FF, U+FB50-FDFF, U+FE70-FEFF, U+10A60-10A7F, U+10A80-10A9F;
}
@font-face {
font-family: A;
src: url('./fonts/B-Bold.ttf');
font-weight: 600;
unicode-range: var(--ARABIC_UNICODE_RANGE);
}
@font-face {
font-family: A;
src: url('./fonts/B-Medium.ttf');
font-weight: 500;
unicode-range: var(--ARABIC_UNICODE_RANGE);
}
@font-face {
font-family: A;
src: url('./fonts/B-Regular.ttf');
font-weight: 400;
unicode-range: var(--ARABIC_UNICODE_RANGE);
}
@font-face {
font-family: A;
src: url('./fonts/B-Thin.ttf');
font-weight: 300;
unicode-range: var(--ARABIC_UNICODE_RANGE);
}
p {
font-family: A;
}

总结


遇到的问题:



  1. 两种字体,B 字体为阿拉伯语使用,A 字体其他语言使用。根据语言自动选择。

  2. 根据字宽自动选择相应的字体包。

  3. 可以直接使用 Figma 中生成的样式而不必每次手动改动。

  4. 尽可能节省带宽。


我们通过 font-weight 解决了问题2,并通过 unicode-range 解决了问题1。


并且实现了按需下载相应字体包,不使用时不下载。


Figma 中的代码可以直接复制粘贴,无需任何修改即可根据语言和自宽自动使用相应字体包。




参考资料:http://www.zhangxinxu.com/wordpress/2…


作者:Mengke
来源:juejin.cn/post/7251884086536781880
收起阅读 »

前端地位Up!

web
背景 大家好,我是一名前端,我有一天做了一个梦,梦见前端供不应求、梦见大家看到前端就想说:卧槽这个人是前端真牛逼、梦见Javascript突破瓶颈吊打Rust、C... 然后梦醒了,是沉溺于框架、是中台的表格的增删改查、是层出不穷的无效轮子和集成、是无趣的各种...
继续阅读 »

背景


大家好,我是一名前端,我有一天做了一个梦,梦见前端供不应求、梦见大家看到前端就想说:卧槽这个人是前端真牛逼、梦见Javascript突破瓶颈吊打Rust、C...


然后梦醒了,是沉溺于框架、是中台的表格的增删改查、是层出不穷的无效轮子和集成、是无趣的各种小白进阶培训班、是业务形态的致命一击。最终是前端在技术圈子里不如狗的地位。


何至于此?


框架-砒霜or蜜糖?


说说框架


前段跟一个google的算法大佬聊天,他说你们前端很奇怪,现在浏览器已经非常快了,但你们非要搞这样那样的框架,然后去算这个算那个,搞些hooks虚拟dom,完全看不懂在干嘛。


也许我想他说的是对于web的性能层面是对的。


首先虚拟dom也就是中间层如果纯论性能在我看来其实是并不适合现在的时代的,它在这个时代的作用就是作为多端统一以及在真实dom操作前的数据缓冲/计算层...这可能也是这个时代出现了SvelteSolid为代表的此类框架的原因。我倒是希望现在web前端的方向能走向SolidSvelte这种框架的周边社区完善开发。


可是时代的潮流不会随着个人的意志改变,果然时代是分分合合吗,现在ssr为代表的next越来越火(可能有一部分vecel的商业原因),但更重要的一个原因是去改善开发人员的体验(在多端和最佳实践方面),也就是卷颗粒度


以next举例子:我对于next其实是这么理解的,粗颗粒度reactcsr已经进无可进了,改也不好改,那么转化一下方向把,在ssr的领域去降低粒度,就像流式渲染等。


说说人


啊真糟糕,怎么情不自禁就在说框架了,是不是发现我们前端情不自禁在开发or说某个技术的时候就会与框架挂钩。就像是面试我懂某个框架的原理、我学了几个框架诸如此类的,于是我们就从一个框架到另外个框架反反复复的在路上走、打包工具也是一样的(我真的需要这么快的打包工具吗?)。


于是我们就在框架中沉溺了,也许后面会出现一些5年vue工程师10年react工程师,我们整日沉浸在框架之中,日复一日,用着固定的写法(其实我在说Vue,React在这方面会好一些),做着相似的事情,技术的成长变为了我某个框架用得怎么样。


前端工程师or框架工程师


我想啊,前端的潮流很快(娱乐圈),但其实我们要明白一个道理,我用这个东西学这个东西对我有没有收益,对用户体验是不是有很大的提升,对团队开发有没有效率的进步。如果没有的话,不如搞浏览器,当然也可以学学当个PPT天才(纯褒义)或者业务、算法、其他语言等(好堕落啊现在不是PPT就是搞业务),着实没必要把绝大多数时间留着框架上,看多了就会觉得自己很牛逼,然后开摆。


毕竟我们不是框架开发工程师(也就是资源型工具人)、我们是前端开发工程师,我们是面向屏幕开发,我们是人机交互工程师,也就是现在的一个词终端工程师,如果你不能把你的应用在所有屏幕(安卓、Ios、桌面、PC、平板)跑那应该是不合格的。


业务形态


害,说这个之前闲聊一下,我们可以看到一些产品诸如语雀云凤蝶Antd等。蚂蚁体验技术部真的把前端的地位上拉了一截,他们真的很好,可能是未来5年在国内都不会再有这么好的标志性的前端产品,可惜没有一个闭环的商业业务形态,就类似next这种,我在这里不讨论具体的一些原因和后面发生的一些事情。


进入正文,产品和业务形态决定了前端的地位,后端开发通常被认为是应用程序的基础和核心。但其实怎么说那,有的时候其实是因为国内产品思维的局限于和上文提到的沉溺于框架和搜索工具


因为我做过不少国外的产品,有一些很有创造力和创新思维的产品会提出很多天马行空,极具艺术的产品交互效果和体验,在这类产品中其实前端的地位并不算低。


但是在国内就会有这种情况:



  1. 产品不会有这种想法,他的脑子里也是一些国内的那些很普通的竞品,和数据流转逻辑

  2. 前端自己拒绝,一般来说心路历程是这样的,我先看看能不能做 => 去百度掘金搜索 => 搜不到或者框架里没有,好感觉不好做,太复杂了 => 我们换一个普通一点的效果(理由五花八门)。

  3. 大家都是这么做的,那我们这个也这么做吧。


但其实我们自己作为科技触达用户的桥梁,是有能力去推动这个事情的,一个炫酷的配色、合理的交互效果、好看的页面,是可以去给产品去给设计说的,比如我自己有时候会figma或者lottie去自己画一些图和动画效果,去主动纠正设计的颜色和间距。难道产品会拒绝让产品变得更好?设计会拒绝更好看?


说白了,自己不想去做不想去推不会也不想学,觉得很复杂,当然如果实在没有这个土壤果断跑路。


Javascript本身的问题


Javascript吊打RustC估计我是这辈子都看不到了。


Javascript是解释性语言肯定没法跟一些编译语言竞天生就不行,再加上单线程即使有解决方案也就那样。这意味着前端掌握更多语言几乎是一个必要的事情,JavaSwift,Oc这些本来就会用到的不必多说,RustPython选一门掌握也很好。


会得越多你越强,当然我还是建议大家去当PPT天才


前端自信


以后,大伙自信一点,别觉得前端就不如其他技术岗位,地位都是自己争取的,前端优势很大,语言统一、前端立马可见的效果、前端基建相对较小、前端宿主环境统一Docker和容器配置相对统一等。主要是时间,有更多的时间意味着可以做更多的事学更多的东西更多的钱~。


总结


So,改变前端环境从你我做起,你卷一波我卷一波,前端的门槛就提起来了,以后面试的基本要求就是:前端要会Js、Ts、Java、Swift、混合框架、PWA,然后薪资30k起步。


作者:溪饱鱼
来源:juejin.cn/post/7283642910301192244
收起阅读 »

一次性弄清前端上线和生产环境地址

web
💡Tips:不知道小伙伴在前端开发的时候有没有这样的困惑 前端项目打包后,我打包过的静态资源是如何访问后端服务的? 访问后端服务的时候难道不存在跨域的问题吗?如何解决的? 假如我想自己简单修改下部署后的目录该如何去做? 🥲其实不仅仅是你会有这样的疑惑,包...
继续阅读 »

💡Tips:不知道小伙伴在前端开发的时候有没有这样的困惑




  1. 前端项目打包后,我打包过的静态资源是如何访问后端服务的?

  2. 访问后端服务的时候难道不存在跨域的问题吗?如何解决的?

  3. 假如我想自己简单修改下部署后的目录该如何去做?


🥲其实不仅仅是你会有这样的疑惑,包括我在内刚接触前端的时候,由于没有后端的开发经验也会被这些问题所困扰,但是今天我们将一次性弄清楚这个问题,让我们前端的知识体系由点成线,由线成面~


一.明确问题




我们知道,我们平时在开发的时候一般都是使用proxy进行代理的,它的原理是:浏览器会先去访问本地的node服务器,然后node服务器再去代理到你要访问的后端api接口,但是我们可能平时没有node服务器的概念,因为node服务器在webpack中,我们一般是通过下面这种方式来设置



但是我们的项目上线后这种方式就不能用了,(因为Node是我们本地开发的环境,并没有办法在线上使用。)其实,我们一般会通过后端的Nginx代理来解决跨域的问题,但是你知道前端的生产地址配置是什么吗?如何通过Nginx访问后端接口呢?是直接配置的类似于http://www.xxxx.com/api/aaa这样的路径呢?还是直接是一个相对路径/prod?要想搞清楚这些,首先就要了解什么是Nginx


二.什么是Nginx




🐻是一个开源的高性能、轻量级的Web服务器和反向代理服务器,它具有事件驱动,异步非阻塞的架构,被广泛应用于构建高性能的网站,应用程序和服务。


🤡在平时开发中我们经常听到正向代理反向代理这两个名词,那么什么是反向代理,什么是正向代理哪?



  1. 反向代理:服务器的IP是被屏蔽的,也就是说客户端不知道服务器真实的地址是哪个,客户端访问的地址或者域名是访问的Nginx服务器的地址。




  1. 正向代理:和反向代理刚好相反,这个时候服务器不知道真正的客户端是哪个,也就是相当于直接访问服务器的是nginx代理服务器。



三.前端使用Nginx解决跨域




🤗什么是跨域,跨域是指在浏览器的环境下,当一个网页的JavaScript代码通过Ajax Websocket或其他技术发送HTTP请求的目标资源位于不同的域名,端口或者协议下,就会发生跨域。


🐻Nginx如何解决跨域,因为服务器和服务器之间互相请求不发生跨域,所以解决跨域的方法之一就是使用这种方案



  1. 浏览器或者客户端发送请求:http:www.xxx.com:80Nginx服务器对80端口进行监听,Nginx服务器将请求转发到后端真实的服务器地址,这样就实现了代理。




  1. Nginx基本配置项解析


server {
listen 80;
server_name yourdomain.com;

location / { // 无论访问所有路径都返回前端静态资源dist内容
root /path/to/your/frontend;
index index.html;
try_files $uri $uri/ /index.html;
}

location /api/ {
proxy_pass http://backend-server-address/api/; // 真实后端api地址
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}

当在前端代码中使用相对路径/api/users发起请求时候Nginx会将这个请求转发到真实的后端地址,不过你发现了没很多前端项目种生产环境地址都仅仅是一个/api类似这样的相对路径,很少直接是一个绝对路径。


当你请求 yourdomain.com 时,Nginx 会将前端静态资源返回给前端。前端代码中使用的相对路径 /api会基于当前域名 yourdomain.com构建完整的请求 URL。因此,前端代码请求后端地址的完整 URL 将是 yourdomain.com/api/xxx,其中 /xxx表示具体的后端接口路径。


Nginx 的反向代理配置中的 location /api/ 指令将匹配以 /api/ 开头的请求,并将这些请求代理到后端服务器上。因此,当前端代码发起相对路径请求 /api/xxx 时,Nginx 会将请求转发到 yourdomain.com/api/xxx,实现与后端接口的通信。


总结来说,前端代码中的相对路径 /api会根据当前域名构建完整的请求 URL,而 Nginx 的反向代理配置将这些请求转发到后端服务器上的相应路径。这样,前端代码就能够与后端接口进行通信。


四.前端生产环境配置




🥲既然Nginx如何代理的,以及前端打包的路径一般是什么样的我们知道了,那么我们就来唠唠作为一个前端小白该如何快速的完整的构建一个基础项目吧,其实如果基础开发的话,我们往往会使用脚手架,就以Vue开发的话,我们可以使用vuecli来快速构建项目,其实构建完之后你就可以直接npm run build打出的包就可以部署在后端服务器的,这个打出的包的根路径是默认的/,通过上面的Nginx的知识我们应该不难理解。


🤡如果我们要自己想去修改一个路径哪?我们可以在vue.config.js中进行配置,配置如下


module.exports = {
publicPath: process.env.NODE_ENV === 'production' ? '/prod' : '/'
};

👹这样打出的包的静态资源的路径就是下边这样的



🥰如果是直接使用的默认的打包路径就是如下这种



五.总结




🤡最后总结一下,前端上线打包完就是一个静态文件,是一个相对路径,后端会通过Nginx来监听这个资源的请求,当匹配到/就返回静态资源,当匹配到某个/prod就将请求反向代理到后端真实服务器的地址,前端打包的是一个相对路径,Nginx会在前面拼上去具体的域名或者ip,这样就打通了线上前端访问的基本内容。


作者:一溪风月
来源:juejin.cn/post/7291952951048060940
收起阅读 »

同事看到我填写bug原因的表单, 惊呆了, 那么多字段怎么自动填充了?

web
在敏捷开发协作工具中, 公司会要求在关闭bug的时候必须填写一些字段. 其实每次填写的内容都一样的, 要选择bug原因, 是否选择, 填写根因等. 同事在旁边看我关闭了个bug, 看到我的表单是自动填充的, 就问我咋那么方便呢? 实现难度非常简单, 而在日常工...
继续阅读 »

在敏捷开发协作工具中, 公司会要求在关闭bug的时候必须填写一些字段.
其实每次填写的内容都一样的, 要选择bug原因, 是否选择, 填写根因等.


同事在旁边看我关闭了个bug, 看到我的表单是自动填充的, 就问我咋那么方便呢?


实现难度非常简单, 而在日常工作中非常有用, 并且有点小帅. 所以分享给大家.


用什么工具来修改你的网页


我选择的是arc浏览器的boost功能. 在网页上新建一个boost, 点击code, 选到js的tab就可以把编写的js插入到当前host的网页里运行了. 还有辅助功能zag可以帮你抓dom.


对于没有用arc浏览器的大家, 可以写一个chrome extension, 只需要使用content_scripts功能, 可以实现和arc的boost类似的功能: match网址url并且加载一段js. (其实功能是比arc多且灵活的)


修改jira页面


jira是个必须用, 且很多重复操作的网站. 我做了这些修改:


站会看板过滤器顺序调整


每天站会轮到的人的顺序和jira看板上不一致, 导致站会轮下一个人的时候得去找下一个人的位置. 只要获取一下看板过滤器, 调整一下子元素就行了.


const container = document.getElementsByClassName('aui-expander-content ghx-quick-content')[0]
container.children[6].remove()
container.children[10].remove()
container.children[6].after(container.children[2])
container.children[9].after(container.children[1])

看板过滤器多选改单选


jira看板的过滤器是多选的, 所以切换下一个人的时候必须把前一个人取消了, 这样每次都多一次操作.


我们只要给每个过滤器加一个点击事件, 把其他active的过滤器都点击一下就行了.


let child = null
container.onclick = function (e) {
if (child) return
child = e.target
for (let i = 0;i < container.children.length; i++) {
if (container.children[i].children[0] && container.children[i].children[0].classList.contains('ghx-active') && container.children[i].children[0].innerHTML !== child.innerHTML) {
container.children[i].children[0].click()
}
}
child = null
}

关闭bug的时候必须填写原因


公司有个规定, 关闭jira必须填写一些字段. 其实每次填写的内容都一样的, 自动填写可以节省非常多时间.


实现也非常简单, 定时器来寻找指定dom, 然后为这些dom附上指定的值.


const setInputValue = (id, value) => {
if (document.getElementById(id)) {
document.getElementById(id).value = value
}
}

setInterval(() => {
setInputValue('resolution', 10000)
setInputValue('customfield_10903', 12502)
setInputValue('customfield_12301', `故障原因:代码错误
解决方式:修复
影响范围:界面
故障处理人:yo-cwj.com`
)
}, 1000)

获取vue应用的实例来修改界面


老婆画了几套微信表情, 于是我经常登录上去看数据.


但dashboard上信息很少, 需要点到每个表情的详情中才能查看.


通过网络请求, 我看到其实在dashboard的界面, 数据已经请求到了, 于是开始我们的修改.


从dom中寻找vue实例


通过基础的vue知识, 我们知道vue实例是会挂在dom上的.


(vue作者说可以认为他是可用的, 因为vue的devtool也是依赖这个特性的, 那我们一个小脚本是更可用了)


那么哪些dom上有vue实例, 有点像个面试题, 写个简单的脚本就可以找到:


let traverse = (dom) => {
if (dom.__vue__) {
console.log(dom.__vue__._data)
}
for (let i = 0; i < dom.children.length; i++) {
traverse(dom.children[i])
}
}
traverse()

找到目标数据所在的dom, 正式的脚本就这样获取vue实例就可以了.


编写脚本


首先通过vue实例的_data属性获取到数据:


const list = document.querySelector('.page_mod_page_simple.page_home').__vue__.$parent.currentList;

然后把数据贴到对应的dom上:


const emotion_dom = document.querySelector('.table_wrp_emotion_list').querySelector('.table_body');
for (let i = 0; i < emotion_dom.children.length; i++) {
emotion_dom.children[i].children[2].innerHTML += `(${list[i].SendNum} - ${list[i].DataTime})`
}

到这里脚本就写完了, 其他的vue应用其实还可以调用vue实例中的方法获取数据, 或自己获取数据放进vue实例.


解决执行环境的问题


但把这段代码放到boost中会出现拿不到dom的\_\_vue\_\_实例的问题, 因为boost和chrome extension的执行环境并不是浏览器执行环境. 可以通过创建script并执行的方式.


let script = document.createElement('script');
script.textContent = "const list = document.querySelector('.page_mod_page_simple.page_home').__vue__.$parent.currentList;" +
"const emotion_dom = document.querySelector('.table_wrp_emotion_list').querySelector('.table_body');" +
"for (let i = 0; i < emotion_dom.children.length; i++) {" +
" emotion_dom.children[i].children[2].innerHTML += `(${list[i].SendNum} - ${list[i].DataTime})`" +
"}";
setTimeout(() => {
document.documentElement.appendChild(script);
}, 1000)

作者:nujnewnehc
来源:juejin.cn/post/7288628985322307599
收起阅读 »

关于我调部门感觉又重新面试一次这件事,做出知识总结

web
前言 这篇文章的起因是,当时上周部门调整,要调动到其他部门,最开始我以为就走个流程意思意思,一点准备都没有。没想到,去其他部门还经过了3面,感觉挺正式的,在这期间问的问题有些令我印象深刻,发现了许多不足吧,我是去年毕业的,工作了1年多了,本来以为一些基础知识...
继续阅读 »

前言



这篇文章的起因是,当时上周部门调整,要调动到其他部门,最开始我以为就走个流程意思意思,一点准备都没有。没想到,去其他部门还经过了3面,感觉挺正式的,在这期间问的问题有些令我印象深刻,发现了许多不足吧,我是去年毕业的,工作了1年多了,本来以为一些基础知识掌握的差不多了,路还远着,还得学啊!本来那天我还准备一下班就回去玩战地2042,免费周啊!啪的一下兴趣全无,总结一下知识吧,指不定什么时候用上(手动狗头)



节流


节流是指一定时间内只触发一次函数调用,如果在指定时间内多次触发,执行第一次,其他的触发将会被忽略,直到过了设定的时间间隔才触发。


function throttle (fn,delay) {
let timer;
retrun function (...args) {
if(!timer) {
fn(this,args)
timer = settimeout(()=>{
timer=null
},delay)
}
}
}

防抖


防抖是在函数调用后,在指定时间间隔后才触发一次。如果在这个时间间隔内再次触发函数,将重新计时,直到过了设定的时间间隔才会触发最后一次函数调用。


function debounce (fn,delay) {
let timer;
retrun function (...args) {
if(timer) {
clearTimetout(timer)
}
timer = settimeout(()=>{
fn(this,args)
timer=null
},delay)
}
}

数据扁平化


数组


function flatter(arr) {
let result = []
for(let i =0;i<arr.length;i++) {
if(Array.isArray(arr[i]) {
result = result.concat(flatter(arr[i]))
} esle {
result.push(arr[i])
}
}
return result
}

去重


const arr1 = [...new Set(arr)]

const arr1 = arr.map((item,index)=>{
return arr.indexof(item)==index
})

查找字符串中出现最多的字符


当时手写了一半,str.split(item).length应该还要-1才是当前字符出现的次数


  const str = ref<string>('sdfgsgdd');
const fn = (str: string) => {
const arr = Array.from(str);
let maxCount = 0;
let mostFrequentChar = '';
const Nsome = [...new Set(arr)];
Nsome.forEach((item) => {
const count = str.split(item).length - 1;
if (count > maxCount) {
maxCount = count;
mostFrequentChar = item;
}
});
console.log('出现最多的次数,字符', maxCount, mostFrequentChar);
};

闭包及其应用场景


我的回答是:
函数里面嵌套函数,并且内部函数引用了外部函数的变量,就是函数能访问其作用域外的变量


应用场景:
我的回答其中之一是:vueX中状态共享是使用了闭包,节流,防抖
但在 Vuex 中,闭包主要用于封装和共享状态,而不是用于访问和操作外部函数的变量。它使用了闭包的概念,但不是严格意义上的闭包。


1.模块化开发 2.回调函数 3.延迟执行(节流,防抖)


原型&原型链及其应用场景



  1. 原型(Prototype):



  • 每个 JavaScript 对象都有一个原型(prototype),它是一个对象。

  • 对象的原型用于共享属性和方法,当我们访问一个对象的属性或方法时,如果对象本身没有该属性或方法,JavaScript 会沿着原型链向上查找,直到找到该属性或方法或者到达原型链的顶端。

  • 原型可以通过 proto 属性访问,也可以通过 Object.getPrototypeOf() 方法获取。



  1. 原型链(Prototype Chain):



  • 原型链是由对象的原型组成的链式结构,它用于实现对象之间的继承。

  • 当我们访问一个对象的属性或方法时,如果对象本身没有该属性或方法,JavaScript 会沿着原型链向上查找,直到找到该属性或方法或者到达原型链的顶端。

  • 原型链的顶端是 Object.prototype,它是所有对象的原型。


应用场景:



  • 继承:通过原型链,我们可以实现对象之间的继承,一个对象可以继承另一个对象的属性和方法。这样可以避免重复定义和维护相似的代码,提高代码的重用性和可维护性。

  • 共享属性和方法:通过原型链,我们可以将属性和方法定义在原型上,从而实现对象之间的共享。这样可以节省内存空间,避免重复创建相同的属性和方法。

  • 扩展原生对象:通过修改原型链,我们可以扩展 JavaScript 的原生对象,为其添加新的方法和属性。这样可以为原生对象添加自定义的功能,满足特定的需求。


在没有class之前,js是怎么做面向对象的


没答出来,只知道js可以通过class实现面向对象,然后又被问在没有class之前,js是怎么做面向对象的。这也是原型链的应用场景之一,可能是前面原型链的应用场景没说这个,想给我一个提示。


在没有class关键字之前,JavaScript使用原型继承来实现面向对象编程。
javaScript 中的每个对象都有一个原型(prototype),原型是一个对象,它包含了共享的属性和方法。当我们访问一个对象的属性或方法时,如果对象本身没有该属性或方法,JavaScript 会沿着原型链向上查找,直到找到该属性或方法或者到达原型链的顶端。


通过原型链,我们可以实现对象之间的继承和共享属性和方法。下面是一个使用原型继承的示例:


// 创建一个构造函数
function Person(name, age) {
this.name = name;
this.age = age;
}
// 在构造函数的原型上定义方法
Person.prototype.sayHello = function() {
console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
};
// 创建一个 Person 对象
const person1 = new Person('Alice', 25);
// 调用对象的方法
person1.sayHello(); // 输出 "Hello, my name is Alice and I am 25 years old."

node是什么,express是什么,node服务中的中间件是用来干什么的


Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行时环境,可以用于构建高性能的网络应用程序。它允许使用 JavaScript 在服务器端运行代码,而不仅仅局限于在浏览器中运行。


Express 是一个基于 Node.js 的 Web 应用程序框架,它提供了一组简洁而灵活的功能,用于构建 Web 应用程序和 API。Express 提供了路由、中间件、模板引擎等功能,使得构建 Web 应用程序变得更加简单和高效。


中间件的作用是增强和扩展 Node.js 服务的功能,使得处理请求和响应的过程更加灵活和可定制。通过使用中间件,可以将常见的功能模块化,提高代码的可维护性和可重用性。


Express 提供了一些内置的中间件,同时也支持自定义中间件。您可以使用内置的中间件,如 express.json()、express.urlencoded() 来处理请求体的解析,或者编写自己的中间件来满足特定的需求。


你h5怎么处理兼容性


因为是vite+v3项目,vite官方有推荐的插件库,在插件库中有一个关于浏览器兼容支持的插件:@vitejs/plugin-legacy


插件@vitejs/plugin-legacy的作用是为打包后的文件提供传统浏览器兼容性支持



  1. 首先安装插件:npm i @vitejs/plugin-legacy -D

  2. 然后在vite.config.js中配置


import legacyPlugin from '@vitejs/plugin-legacy'
export default defineConfig( {
plugins: [
legacyPlugin({
targets:['chrome 52'], // 需要兼容的目标列表,可以设置多个
additionalLegacyPolyfills:['regenerator-runtime/runtime'] // 面向IE11时需要此插件
})
]
})

rem,px,em这些有什么区别




  1. px(像素):px 是绝对单位,表示屏幕上的一个物理像素点。它是最常用的单位,具有固定的大小,不会根据其他因素而改变。例如,font-size: 16px; 表示字体大小为 16 像素。




  2. rem(根元素字体大小的倍数):rem 是相对单位,相对于根元素(即 元素)的字体大小。如果根元素的字体大小为 16 像素,那么 1rem 就等于 16 像素。如果根元素的字体大小为 20 像素,那么 1rem 就等于 20 像素。通过设置根元素的字体大小,可以方便地调整整个页面的大小。例如,font-size: 1.5rem; 表示字体大小为根元素字体大小的 1.5 倍。




  3. em(相对于父元素字体大小的倍数):em 也是相对单位,相对于父元素的字体大小。如果父元素的字体大小为 16 像素,那么 1em 就等于 16 像素。如果父元素的字体大小为 20 像素,那么 1em 就等于 20 像素。通过设置父元素的字体大小,可以影响其子元素的大小。例如,font-size: 1.2em; 表示字体大小为父元素字体大小的 1.2 倍。




总结来说,px 是绝对单位,不会随其他因素改变;rem 是相对于根元素字体大小的倍数,可以方便地调整整个页面的大小;em 是相对于父元素字体大小的倍数,可以影响子元素的大小。


在实际使用中,可以根据需求选择合适的单位。对于响应式设计,使用 rem 可以方便地调整整个页面的大小;对于局部样式,可以使用 px 或 em 来控制具体的大小。


你工作中遇到了什么坑或者解决什么让自己印象深刻的问题



  • element-plus的el-table表格的二次封装(可以使用tsx)

  • el-table表格的动态合并

  • h5 ios时调起键盘会把整个布局往上推

  • h5调用封装的app JSbrige完成返回

  • 登录的拼图验证

  • h5嵌套在微信小程序中时,由我们h5跳到三方提供的安全验证h5页面,返回时,本地存储的东西没了

  • 利用git hooks+husky+eslint完成前端代码规范和提交规范

  • 银行卡拖拽排序,把排完的顺序返回服务端


上面这些都是我解决了,也不仅仅只有这些,回头想了了下明明自己有很多可以说的,在当时就说了2,3个,然后负责人问我还有吗时,我卡壳了,居然不知道还要说什么。后面我感觉也是根据这个展开来问的


V2混入和V3的hooks,为什么V3要改成hooks的方式


感觉应该是问hooks的好处吧?反正我是答的不太对的,以下是总结:


Vue 3 引入了 Composition API(包括 setup 函数和 hooks),这是一个新的方式来组织和复用代码,与 Vue 2 的混入(mixins)有所不同。

混入在 Vue 2 中被广泛使用,它们允许你在多个组件之间共享行为。然而,混入有一些问题:



  1. 命名冲突:如果混入和组件有相同的方法或数据属性,可能会导致冲突。

  2. 来源不明:当一个组件使用了多个混入时,可能很难确定一个特定的方法或数据属性来自哪个混入。

  3. 复杂性:混入可以包含生命周期钩子、方法、数据等,这可能会增加理解和维护组件的复杂性。

    相比之下,Vue 3 的 Composition API(包括 hooks)提供了一种更灵活、更可控的方式来组织和复用代码:

  4. 更好的逻辑复用和代码组织:你可以将相关的代码(如数据、方法和生命周期钩子)组织在一起,而不是强制按照 Vue 的选项(data、methods、created 等)来组织代码。

  5. 更好的类型推断:对于使用 TypeScript 的项目,Composition API 提供了更好的类型推断。

  6. 更清晰的来源:每个函数和响应式数据的来源都非常明确,因为它们都是从特定的 hook 或 setup 函数返回的。
    因此,虽然 Vue 3 仍然支持混入,但推荐使用 Composition API 来组织和复用代码。


vue3中怎么封装一个自定义指令



  • 通过app.directive()方法注册指令,该方法接受两个参数,第一个参数是指令的名称,第二个参数是一个对象,包含指令的各个生命周期的钩子函数

  • 然后我们就可以在生命周期的钩子函数中定义指令的行为,根据指令的需求,在相应的生命周期钩子函数中编写逻辑代码


什么情况下会使用自定义指令


我的回答是:想要操作dom元素时并且这种类似情况经常出现,如节流和防抖指令,就是给dom加上disabled。按钮权限指令,给当前按钮dom一个显示和隐藏


拖拽排序


拖拽排序的实现原理主要涉及一下几个步骤:



  • 1.监听拖拽事件: 浏览器提供了一系列的拖拽事件,设置draggable="true"



    1. 开始拖拽:当用户开始拖拽一个元素时,会触发 dragstart 事件。在这个事件的处理函数中,我们可以通过 传入的dragstart(e,index) ,中的index来设置当前被拖拽元素的下标。





    1. 拖拽过程:当用户拖拽元素时,会不断触发 dragover 事件。在这个事件的处理函数中,我们需要调用 event.preventDefault 方法来阻止浏览器的默认行为,否则无法触发 拖拽 事件。





    1. 拖拽到另一个元素区域时:当用户拖拽到另一个元素时,会触发 dragenter 事件。在这个事件的处理函数中,我们可以通过 dragente(e,index)方法来获取拖拽到的元素的下标,然后根据获取的两下标来更新列表的排序。




表格动态合并


element-plus表格合并(例如前两列合并) | 耀耀切克闹 (yaoyaoqiekenao.com)


模拟new实例创建的过程



  • 1.创建了新对象并将._proto_指向构造函数.prototype

  • 2.将this指向新创建的对象

  • 3.返回新对象


function newSimulator() {
//1.创建新对象
const obj = new Object()
//2.设置_proto_为构造函数prototype
const constructor = [].shift.call(arguments)
obj._proto_ = constructor.prototype
//3.this指向新对象,也就是改变this的指向
const ret = constructor.apply(obj,arguments)
//4.返回对象或this
return typeof ret = 'object' ? ret : obj
}

冒泡排序


const arr = [1,7,9,2,3,5]
for(let i=0;i<arr.length;i++){
for(let j=0;j<arr.length-i-1;j++){
let a = []
if(arr[j]<arr[j+1]){
a =arr[j]
arr[j]=arr[j+1]
arr[j+1]=a
}
}
}


深拷贝


1.使用 JSON 序列化和反序列化


const obj={
arr:[1,2]
}
const clone = JSON.parse(JSON.stringify(obj))

2.使用递归完成深拷贝


这种方式通过递归地遍历原始对象,并对该对象的的属性进行逐一的深拷贝,以创建一个原对象的独立副本。


function deepCloneObject(obj) {
if(obj ===null||typeof object !='object') {
return obj
}
const clone = Array.isArray(obj)?[]:{}
for(let key in obj) {
if(object.prototype.hasOwnProperty.call(obj,key))
clone[key] = deepClone(obj[key])
}
retrun clone
}

函数柯里化


函数柯里化是一种将具有多个参数的函数转换为逐个应用参数的函数序列的技术。通过柯里化,我们可以将一个函数的多个参数转化为一系列嵌套的函数调用。


柯里化的优点是可以创建可复用的函数模板。通过部分应用来生成新的函数。这样可以更灵活地使用函数,并且可以方便的创建更专注于特定功能的函数。
简单的函数柯里化例子:


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

// 使用柯里化的add函数
var add5 = add(5);
console.log(add5(3)); // 输出 8
console.log(add5(7)); // 输出 12

封装一下


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

数组API的实现


forEach


Array.portotype.my_forEach = function(callback) {
for(let i=0;i<this.length;i++) {
callback(this[i],i,this)
}
}

map


Array.portotype.my_map = function(callback) {
let res= []
for(let i=0;i<this.length;i++) {
callback(this[i],i,this)&&res.push( callback(this[i],i,this))
}
return res
}

filter


Array.portotype.my_filter = function(callback) {
let res= []
for(let i=0;i<this.length;i++) {
callback(this[i], i, this) && res.push(this[i])
}
return res
}

前端模块化


问:你讲讲前端模块化吧
答:模块化的开发方式可以提高代码复用率,方便进行代码的管理,通常一个文件就是一个模块,有自己的作用域,只向外暴露特定的变量和函数。


问:模块化有哪几种标准?
答:目前流行的js模块化规范有CommonJS、AMD、CMD以及Es6的模块系统


问:ES Modules 和CommonJS的一些区别
答:
1.使用语法层面,CommonJs是通过modules.exports,exports导出,require导入;ES Modules则是export导出,import导入
2.CommonJs是运行时加载模块,EsModules是在静态编译期间就确定模块的依赖
3.EsModulse在编译期间会将所有import提升到顶部,CommonJs不会提升require
4.CommonJs导出是一个值拷贝,会对加载结果进行缓存,一但内部再修改这个值,则不会同步到外部。ESModule是导出的一个引用,内部修改可以同步到外部
5. CommonJs中顶层的this指向这个模块本身,而ESModule中顶层this指向undefined
6. CommonJS加载的是整个模块,将所有的接口全部加载进来,ESModule可以单独加载其中的某个接口


vue的数据双向绑定的原理


vue的响应式原理是采用‘发布-订阅’的设计模式结合object.defineProperty()劫持各个属性的getter和setter,在数据发生变动时通过调用Deo.notity函数发布订阅给观察者watcher,让其更新响应的视图。


虚拟dom


虚拟dom是用来表现真实dom结果的javaScript对象树,是构建在浏览器真实dom上的抽象层,虚拟dom是可以直接在内存中操作的,可以通过diff算法来新旧dom的差异,将最终变化应用到真实dom上


diff算法


diff算法又称虚拟Dom的周界算法,vue的diff算法是通过深度优先、先序遍历的方式进行的,它将前后两个虚拟Dom树进行逐层比较,当找到某一层不一样的节点时,停止下降,然后比较这些节点的子节点,当所有的子节点都完成比较之后,算法会由下至上进行回溯,此过程被称为执行patch操作。在执行patch操作时,Vue对于不同类型的节点的更新方式也不同,对于元素节点可以更新他的属性和子节点;对于文本节点,只能更新它的文本内容;对于每个子节点,如果key值相同,可以进行复用或者重新排序,或者将其他的节点移动到这个位置。


vue中nextTick的理解及作用


使用场景描述:更改一个数据,导致dom元素的width发生了更改,但又要获取这个更新后的dom元素的width,可以用nextTick
vue2 中的nextTick是在下次Dom更新循环之后执行回调函数,并且是作为vue实例的方法调用的


this.$nextTick(() => { // 组件的DOM已经更新完毕,可以进行相应操作 // ... });

Vue 3的nextTick作为一个独立的函数导入,返回一个Promise,并且可以直接传递回调函数作为参数。这些变化使得Vue 3中的nextTick更加灵活和易于使用。


// Vue 3 
import { nextTick } from 'vue';
nextTick(() => { // 在下次DOM更新循环之后执行 });

vue在实例挂载的过程中发生了什么?




  1. 实例化:首先,Vue.js会创建一个新的Vue实例。在这个过程中,Vue.js会设置实例的各种属性和方法,包括数据对象、计算属性、方法、指令等。




  2. 编译模板:Vue.js会将模板编译成渲染函数。模板就是包含Vue特定语法的HTML代码。编译过程中,Vue.js会解析模板中的指令(如v-if、v-for等)和插值表达式(如{{ message }}),并将它们转换为JavaScript代码。




  3. 创建虚拟DOM:渲染函数会被调用,生成一个虚拟DOM树。虚拟DOM是对真实DOM的轻量级表示,它可以更高效地处理DOM的更新。




  4. 挂载:最后,Vue.js会将虚拟DOM渲染为真实DOM,并将其挂载到指定的元素上。这个过程通常在调用vm.$mount()方法或者在实例化Vue时传入el选项后发生。




  5. 更新:当数据变化时,Vue.js会重新执行渲染函数,生成新的虚拟DOM,并与旧的虚拟DOM进行对比(这个过程称为diff)。然后,Vue.js会根据diff结果,以最小的代价更新真实DOM。




这个过程中还会触发一系列的生命周期钩子,如created、mounted等,开发者可以在这些钩子中执行自己的代码。


vue2中data是一个函数而不是对象的原因


data之所以是一个函数,是因为一个组件可能会多处调用,而每一次调用就会执行data函数并返回新的数据对象,这样,可以避免多处调用之间的数据污染


vue2中给对象添加新属性界面页面不刷新


vue2是用Object.defineProperty实现数据响应式,而后面新增的属性,并没有通过Object.defineProperty设置成响应式数据,所以页面没变化,常用解决方式:



  • Vue.set()

  • Object.assign()

  • $forcecUpdated()


Vue SSR的实现原理


vue.js的ssR是一种在服务器上预渲染Vue.js应用程序的技术。



  1. 服务器接收请求:当服务器接收一个请求时,它会创建一个新的Vue实例。

  2. 创建渲染器:使用vue-server-renderer包创建一个渲染器。

  3. 渲染页面:服务器使用渲染器将Vue实例渲染为Html字符串。

  4. 发送响应:服务器将渲染后的Html字符串作为响应发送给客户端。

  5. 客户端接收响应:客户端接收到服务器的响应后,将HTML字符串解析为DOM并显示给用户。

  6. 激活(Hydration): Vue在客户端创建一个新的Vue实例,将其挂载到服务器收到的Dom上


keep-alive的使用


keep-alive的主要作用是缓存路由组件,以提高性能


<router-view v-slot="{ Component }">  
<keep-alive :include="permissionStore.keepAliveName">
<component :is="Component" :key="$route.path" />
</keep-alive>

</router-view>



  1. router-view是 Vue Router 的一个组件,用于渲染当前路由对应的组件。




  2. v-slot="{ Component }" 是一个插槽,用于获取当前路由对应的组件。




  3. keep-alive 是 Vue 的一个内置组件,用于缓存组件,避免重复渲染。




  4. :include="permissionStore.keepAliveName" 是 的一个属性,表示只有名称在 permissionStore.keepAliveName 中的组件会被缓存。




  5. 是一个动态组件,:is="Component" 表示组件的类型由 Component 决定,:key="$route.path" 表示每个路由路径对应一个唯一的组件实例。




Vue项目中有封装axios吗?主要是封装哪方面的?



  • 1.封装前需要和后端协商好一些约定,请求头,状态码,请求时间....

  • 2.设置接口请求前缀:根据开发、测试、生产环境的不同,前缀需要加以区分

  • 3.移除重复的请求,如果请求在pending中,提示'操作太频繁,请稍后再试'

  • 4.用map结构根据相应状态码处理错误信息

  • 5.请求拦截,若headers中没有token的,移除请求

  • 6.响应拦截器,例如服务端返回的message中有'message',提示'请求超时,请刷新网页重试'

  • 7.请求方法的封装,封装get、post请求方法,使用起来更为方便


css预处理器


css预处理器扩充了css语言,增加了诸如变量、混合(mixin)、函数等功能,让css更易维护、方便。本质上。预处理是css的超集。包含一套自定义的语法及一个解析器,根据这些语法定义自己的样式规则,这些规则最终会通过解析器编译生成对应的css文件。


如何实现上拉加载


image.png
触底公式:


scrollTop + clientHeight >= scrollHeight

简单实现:


    let clientHeight = document.documentElement.clientHeight;//浏览器高度
let scrollHigiht = documnet.body.scrollHeight;//元素内容高度的度量,包括由于溢出导致的视图中不可见内容
let scrollTop = documnet.body.scrollTop; //滚动视窗的高度距离`window`顶部的距离
let distance = 50; //距离视窗还用50的时候,开始触发;

if ((scrollTop + clientHeight) >= (scrollHeight - distance)) {
console.log("开始加载数据");
}

如何实现下拉刷新


关于下拉刷新的原生实现,主要分成三步:



  1. 监听原生touchstart事件,记录其初始位置的值,e.touches[0].pageY;

  2. 监听原生touchmove事件,记录并计算当前滑动的位置值与初始位置值的差值,大于0表示向下拉动,并借助CSS3的3. translateY属性使元素跟随手势向下滑动对应的差值,同时也应设置一个允许滑动的最大值

  3. 监听原生touchend事件,若此时元素滑动达到最大值,则触发callback,同时将translateY重设为0,元素回到初始位置。


封装和使用JSBrige




  1. 定义协议:首先,需要定义一种协议,用于约定H5页面与App之间的通信规则。这可以是一组自定义的URL Scheme或JavaScript函数。




  2. 注册事件监听:在H5页面中,通过JavaScript代码注册事件监听器,用于接收来自App的消息或回调。可以使用window.addEventListener或其他类似的方法来监听特定的事件。




  3. 发送消息给App:在H5页面中,通过调用JSBridge提供的方法,将消息发送给App。这可以是通过修改URL Scheme的方式,或者调用App提供的JavaScript接口。




  4. 处理App的消息或回调:在App原生代码中,通过监听URL Scheme或执行JavaScript代码的方式,接收来自H5页面的消息或回调。根据协议约定,处理相应的逻辑或调用相应的功能。




  5. 回调结果给H5页面:在App原生代码中,根据协议约定,将处理结果或回调信息发送回H5页面。可以通过修改URL Scheme的方式,或者调用H5页面中注册的JavaScript回调函数。




个人博客


耀耀切克闹 (yaoyaoqiekenao.com)


gitHub


DarknessZY (zhangyao) (github.com)


作者:耀耀切克闹灬
来源:juejin.cn/post/7291834381315719220
收起阅读 »

分享一个Java小项目:Java实现超级马里奥的冒险之旅

引言超级马里奥,这个名字对于游戏迷来说一定不陌生。它是一款经典的游戏系列,以一个勇敢的水管工人——马里奥为主角,讲述了他在蘑菇王国中的冒险故事。在这个充满挑战和刺激的游戏中,玩家需要控制马里奥跳跃、躲避障碍物,并与邪恶的蘑菇和食人花敌人战斗,最终抵达城堡的胜利...
继续阅读 »

引言

超级马里奥,这个名字对于游戏迷来说一定不陌生。它是一款经典的游戏系列,以一个勇敢的水管工人——马里奥为主角,讲述了他在蘑菇王国中的冒险故事。在这个充满挑战和刺激的游戏中,玩家需要控制马里奥跳跃、躲避障碍物,并与邪恶的蘑菇和食人花敌人战斗,最终抵达城堡的胜利之地。

游戏目标

在这款游戏中,我们的目标是通过控制马里奥完成三个关卡的挑战。每个关卡都有不同的难度和障碍物,玩家需要灵活运用跳跃技巧和反应能力,才能成功通关。同时,消灭普通砖块还可以赚取积分,增加游戏的趣味性和挑战性。

Java实现

为了实现这个经典的游戏,我们将使用Java编程语言进行开发。Java是一种功能强大且广泛使用的编程语言,它具有丰富的图形界面库和游戏开发工具,非常适合用于制作平台跳跃类游戏。

在实现过程中,我们可以使用Java的Swing库来创建游戏的图形界面,包括游戏窗口、角色、背景等元素。同时,我们还需要处理用户的输入操作,例如键盘按键的监听和处理,以便玩家能够控制马里奥的移动和跳跃。

此外,我们还需要考虑游戏的物理引擎和碰撞检测机制,以确保马里奥能够与障碍物和敌人进行正确的交互。这可以通过使用Java的物理引擎库或自己编写相应的算法来实现。

总结

通过使用Java编程语言和相关库,我们可以成功地实现一个经典的超级马里奥小游戏。这将是一个非常有趣和有挑战性的项目,不仅可以锻炼我们的编程技能,还能够让我们体验到游戏开发的乐趣。让我们一起踏上这段冒险之旅吧!

收起阅读 »

Vue 实现 PDF 导出功能

web
笨文旨在通过 html2canvas 和 jspdf,先将页面的 html 转成 canvas,再将 canvas 转成 pdf,同时解决了分页截断的问题。 安装依赖 yarn add html2canvas yarn add jspdf 思路 通过网上的一...
继续阅读 »

笨文旨在通过 html2canvas 和 jspdf,先将页面的 html 转成 canvas,再将 canvas 转成 pdf,同时解决了分页截断的问题。


安装依赖


yarn add html2canvas
yarn add jspdf

思路


通过网上的一些教程,初步实现了 html 转 pdf 的功能,将一整个 DOM 元素放进去,虽然可以粗糙实现,但是出现了很多地方被分页截断的情况,这个时候就需要在某一张图片被截断时,将其自动切换到下一页中。


1.拆解父节点


所以第一步:拆解父节点,一行一行细分为很多子节点,循环遍历这些子节点,累加这些子节点的高度,如果超出了 a4 纸(210*297)的高度,则分页。


import html2Canvas from "html2canvas";
import JsPDF from "jspdf";

export function oneNodeMultipleChildren(title, node) {
html2Canvas(node, {
scale: 2, // 清晰度
}).then(function (canvas) {
let PDF = new JsPDF("", "mm", "a4"); // 以mm为单位
let position = 0; // 页面偏移
let contentWidth = canvas.width; // 转换成canvas后的宽度
let contentHeight = canvas.height; // 转换成canvas后的高度
let proportion = 210 / node.offsetWidth; // html缩小至a4纸大小时的比例
let currentHeight = 0; // 当前高度
let imgWidth = 210; // canvas缩小至a4纸大小时的宽度
let imgHeight = (210 / contentWidth) * contentHeight; // canvas缩小至a4纸大小时的高度
let pageData = canvas.toDataURL("image/jpeg", 1.0); // 将canvas转成图片

for (let j = 0; j < node.children.length; j++) {
let childHeight = (node.children[j].offsetHeight + 8) * proportion; // 页面中每行的间距 margin-bottom: 8px

if (currentHeight + childHeight > 297) {
// 如果加上这个子节点后内容超出a4纸高度,就从当前位置开始分页
addImage(PDF, pageData, position, imgWidth, imgHeight, currentHeight);
position -= currentHeight; // 这一页放了多少高度的内容,下一页就从这个高度开始偏移
if (position >= -contentHeight) {
PDF.addPage(); // 添加新pdf页
}
currentHeight = childHeight; // 下一页第一个元素的高度
} else {
currentHeight += childHeight;
}
}
addImage(PDF, pageData, position, imgWidth, imgHeight, currentHeight); // 最后一页
PDF.save(title + ".pdf");
});
}

function addImage(PDF, pageData, position, imgWidth, imgHeight, currentHeight) {
PDF.addImage(pageData, "JPEG", 0, position, imgWidth, imgHeight); // 在当前pdf页添加图片
PDF.setFillColor(255, 255, 255); // 遮挡的颜色
PDF.rect(0, currentHeight, 210, Math.ceil(297 - currentHeight), "F"); // 添加空白遮挡
// PDF.rect参数分别为:起始横坐标、起始纵坐标、绘制宽度、绘制高度、填充色
}

2.合并父节点


经过上述步骤,一个父节点多个子节点,并且每个子节点独占一行的布局可以实现分页,那要是有很多父节点呢?就需要遍历每个父节点,合并所有子节点,进行分页截断。


import html2Canvas from "html2canvas";
import JsPDF from "jspdf";

export function exportPdf(title, id) {
let content = document.querySelector(`#${id}`);
let first = content.firstElementChild.firstElementChild;
let second = content.lastElementChild;
oneNodeMultipleChildren(title, content, [first, second]);
}

export function oneNodeMultipleChildren(title, content, nodes) {
html2Canvas(content, {
scale: 2,
}).then(function (canvas) {
let PDF = new JsPDF("", "mm", "a4");
let position = 0;
let contentWidth = canvas.width;
let contentHeight = canvas.height;
let proportion = 200 / content.offsetWidth;
let currentHeight = 0;
let imgWidth = 200;
let imgHeight = (200 / contentWidth) * contentHeight;
let pageData = canvas.toDataURL("image/jpeg", 1.0);

for (let i = 0; i < nodes.length; i++) {
// 根据传入的父节点数量进行循环,遍历父节点,合并所有子节点
for (let j = 0; j < nodes[i].children.length; j++) {
let childHeight = (nodes[i].children[j].offsetHeight + 8) * proportion;

if (currentHeight + childHeight > 287) {
addImage(PDF, pageData, position, imgWidth, imgHeight, currentHeight);
position -= currentHeight;
if (position >= -contentHeight) {
PDF.addPage();
}
currentHeight = childHeight;
} else {
currentHeight += childHeight;
}
}
}
addImage(PDF, pageData, position, imgWidth, imgHeight, currentHeight);
PDF.save(title + ".pdf");
});
}

function addImage(PDF, pageData, position, imgWidth, imgHeight, currentHeight) {
PDF.addImage(pageData, "JPEG", 0, position, imgWidth, imgHeight); // 在当前pdf页添加图片
PDF.setFillColor(255, 255, 255); // 遮挡的颜色
PDF.rect(0, currentHeight, 210, Math.ceil(297 - currentHeight), "F"); // 添加空白遮挡
}

3.每行多个元素


这个时候新的问题出现了,由于页面布局为 flex 布局,不同缩放下,每行的元素数量会出现变化。所以我们获取第一个子元素与 a4 纸宽度关系,如果为 n 倍,那后面 n-1 个子元素的高度不进行累加。


warning 注意
这里只解决了一行 n 个子元素宽度相等,且近似等于 a4 纸宽度的 1/n 的情况。


import html2Canvas from "html2canvas";
import JsPDF from "jspdf";

export function exportAssetPdf(title, id) {
let content = document.querySelector(`#${id}`);
let first = content.firstElementChild.firstElementChild;
let second = content.lastElementChild;
oneNodeMultipleChildren(title, content, [first, second]);
}

export function oneNodeMultipleChildren(title, content, nodes) {
html2Canvas(content, {
scale: 2,
}).then(function (canvas) {
let PDF = new JsPDF("", "mm", "a4");
let position = 0;
let contentWidth = canvas.width;
let contentHeight = canvas.height;
let proportion = 200 / content.offsetWidth;
let currentHeight = 0;
let imgWidth = 200;
let imgHeight = (200 / contentWidth) * contentHeight;
let pageData = canvas.toDataURL("image/jpeg", 1.0);
let sameIndex = 1;
let widthX = 1;

for (let i = 0; i < nodes.length; i++) {
for (let j = 0; j < nodes[i].children.length; j++) {
let childHeight = (nodes[i].children[j].offsetHeight + 8) * proportion;
let childWidth = nodes[i].children[j].offsetWidth * proportion;
if (sameIndex === 1) {
widthX = Math.round(200 / childWidth);
}
if (sameIndex < widthX) {
childHeight = 0;
sameIndex++;
} else {
sameIndex = 1;
}

if (currentHeight + childHeight > 287) {
addImage(PDF, pageData, position, imgWidth, imgHeight, currentHeight);
position -= currentHeight;
if (position >= -contentHeight) {
PDF.addPage();
}
currentHeight = childHeight;
} else {
currentHeight += childHeight;
}
}
}
addImage(PDF, pageData, position, imgWidth, imgHeight, currentHeight);
PDF.save(title + ".pdf");
});
}

function addImage(PDF, pageData, position, imgWidth, imgHeight, currentHeight) {
PDF.addImage(pageData, "JPEG", 0, position, imgWidth, imgHeight); // 在当前pdf页添加图片
PDF.setFillColor(255, 255, 255); // 遮挡的颜色
PDF.rect(0, currentHeight, 210, Math.ceil(297 - currentHeight), "F"); // 添加空白遮挡
}

4.添加左右间距和页眉页脚


为了美化 pdf 布局,上下左右留白,就需要添加左右间距和页眉页脚:减少 html 缩小至 a4 纸大小时的比例和 canvas 缩小至 a4 纸大小时宽高,增加偏移量,并对页眉页脚进行空白遮挡。


import html2Canvas from "html2canvas";
import JsPDF from "jspdf";

export function exportAssetPdf(title, id) {
let content = document.querySelector(`#${id}`);
let first = content.firstElementChild.firstElementChild;
let second = content.lastElementChild;
oneNodeMultipleChildren(title, content, [first, second]);
}

export function oneNodeMultipleChildren(title, fNode, sNode) {
html2Canvas(fNode, {
scale: 2,
}).then(function (canvas) {
let PDF = new JsPDF("", "mm", "a4");
let position = 0;
let contentWidth = canvas.width;
let contentHeight = canvas.height;
let proportion = 200 / fNode.offsetWidth; // 减少10mm
let currentHeight = 0;
let imgWidth = 200; // 减少10mm
let imgHeight = (200 / contentWidth) * contentHeight; // 减少10mm
let pageData = canvas.toDataURL("image/jpeg", 1.0);
let sameIndex = 1;
let widthX = 1;

for (let i = 0; i < sNode.length; i++) {
for (let j = 0; j < sNode[i].children.length; j++) {
let childHeight = (sNode[i].children[j].offsetHeight + 8) * proportion;
let childWidth = sNode[i].children[j].offsetWidth * proportion;
if (sameIndex === 1) {
widthX = Math.round(200 / childWidth); // 减少10mm
}
if (sameIndex < widthX) {
childHeight = 0;
sameIndex++;
} else {
sameIndex = 1;
}

if (currentHeight + childHeight > 287) {
// 减小10mm
addImage(PDF, pageData, position, imgWidth, imgHeight, currentHeight);
position -= currentHeight;
if (position >= -contentHeight) {
PDF.addPage();
}
currentHeight = childHeight;
} else {
currentHeight += childHeight;
}
}
}
addImage(PDF, pageData, position, imgWidth, imgHeight, currentHeight);
PDF.save(title + ".pdf");
});
}

function addImage(PDF, pageData, position, imgWidth, imgHeight, currentHeight) {
PDF.addImage(pageData, "JPEG", 5, position + 5, imgWidth, imgHeight); // 增加偏移量
PDF.setFillColor(255, 255, 255);
PDF.rect(0, 0, 210, 4, "F"); // 添加页眉遮挡
PDF.rect(0, currentHeight + 5, 210, Math.ceil(292 - currentHeight), "F"); // 添加页脚遮挡
}

成果展示


不同缩放下导出 PDF 对比:


每行一个子元素


每行多个子元素


作者:虚惊一场
来源:juejin.cn/post/7291142504123875364
收起阅读 »

如何创建五彩纸屑效果

web
前言 很多网站会在一些按钮上面加上不同的动画效果,这些动感的效果能够更加容易的创建具有视觉吸引力的用户界面。 本文将介绍一个小巧的 JavaScript 库,它能够用非常短的时间以及简短的代码量创建我们想要的五彩纸屑效果 简单使用 我们可以通过 npm 安装或...
继续阅读 »

前言


很多网站会在一些按钮上面加上不同的动画效果,这些动感的效果能够更加容易的创建具有视觉吸引力的用户界面。


本文将介绍一个小巧的 JavaScript 库,它能够用非常短的时间以及简短的代码量创建我们想要的五彩纸屑效果


简单使用


我们可以通过 npm 安装或从 cdn 导入两种方式来使用这个库。


这里我采用的是导入的方式。


在你导入完成这个库之后,我们需要一个按钮


<button onclick="myClick()">button</button>

最简单的特效只需要在点击函数当中调用 confetti 函数即可


function myClick () {
confetti()
}

动画4.gif


细节配置参数


通过传入 options 属性,我们可以在特效上自定义很多我们需要的部分,下面是部分配置属性的作用,后面我们会挑出部分属性来展示一下效果:


属性名作用
particleCount飞出的纸屑的数量,默认 50
angle飞出的纸屑的角度 90 是向上,默认 90
spread飞出的纸屑偏离中心的角度,默认 45
startVelocity飞出的纸屑的初始速度,默认 45
decay飞出的纸屑的减速度,范围 0-1 之间,默认 0.9
gravity重力,默认 1
decay飞出的纸屑的减速度,范围 0-1 之间,默认 0.9
flat是否关闭纸屑的翻转,默认 false
ticks纸屑消失速度,默认 200
origin对象,设置发射纸屑的原点,有 x y 两个参数,取值都是 0 - 1 对应上下边缘,默认 0.5 0.5
colors数组:十六进制格式的颜色字符串数组
shapes数组:纸屑的形状
scalar每个五彩纸屑粒子的比例,默认 1
zIndex纸屑的z-index,默认 100
disableForReducedMotion禁用五彩纸屑,默认 false

根据上面的参数,你可以很容易的定义自己想要的效果,下面我们随意定义部分例子:


<body>
  <button onclick="myClick1()">左倾斜</button>
  <button onclick="myClick2()">全是黑色</button>
  <button onclick="myClick3()">数量很多多多</button>
 <script src="https://cdn.jsdelivr.net/npm/canvas-confetti@1.9.0/dist/confetti.browser.min.js"></script>
  <script>
    function myClick1 () {
      confetti({
        angle: 135,
      });
    }
    function myClick2 () {
      confetti({
        colors: ['#000000']
      });
    }
    function myClick3 () {
      confetti({
        particleCount: 500,
      });
    }
 
</script>

</body>

动画4 1.gif


详细定义纸屑形状


在上面的配置当中我们已经可以定义纸屑的大部分配置,其中 shapes 可以用于定义纸屑的形状,官方为我们预留了三个形状 'square', 'circle', 'star' 分别对应 方形,圆形,星星,这个字段传入的是一个数组,这个数组中元素的数量,决定了这个形状在所有纸屑中的比例,比如说你要是传入了 ['square', 'circle', 'star'] 那么三种形状就都是占比三分之一。


除了通过官方预留的形状,我们还可以通过两个函数来进行形状自定义,分别是 confetti.shapeFromPathconfetti.shapeFromText


confetti.shapeFromPath


这个函数可以传入一个对象,对象中存在一个 pathkey,这个就决定了创建出来的形状长什么样子


下面的代码可以创建一个三角形的纸屑


var triangle = confetti.shapeFromPath({ path: 'M0 10 L5 0 L10 10z' });

confetti({
shapes: [triangle],
});

confetti.shapeFromText


这个函数同样传入一个对象,对象存在 textscalartext 可以传入任何文字,甚至是一些文字表情也可以使用, scalar 配合 optionsscalar 使用,两个相差过大会导致字体变得很模糊。


下面的代码你就可以创建一个字符串纸屑


var scalar = 2;
var pineapple = confetti.shapeFromText({ text: '🍍🍍🍍', scalar });
confetti({
shapes : [pineapple],
scalar,
});

逻辑事件


纸屑在生成的时候,我们可以会需要一些事件,比如说在我们想要的时候清除掉屏幕上还未消失的纸屑,又或者在一次纸屑彻底消失的时候执行某些逻辑。


消除纸屑


我们可以通过调用 confetti.reset(); 来消除屏幕上的纸屑


监听纸屑消失事件


在我们调用 confetti() 的时候会返回一个 promise 对象,这个对象将会在纸屑完全消失的时候回调。


自定义纸屑产生的位置


在上面的例子当中,我们可能会发现,纸屑都只能在屏幕的正中心产生,这是因为 options 里的 origin 属性的默认值,我们可以通过一些方式来自定义这个产生的位置


动态设置 origin


我们可以通过动态的来设置这个值来做到自定义产生的位置。


在点击事件当中,我们能够拿到当前的点击对象,通过这个 event 对象以及 window 上获取到可视区域的宽高,我们不难算出当前按钮相对于左右的位置。


const windowHeight =
  window.innerHeight ||
  document.documentElement.clientHeight ||
  document.body.clientHeight;
// 获取浏览器高度
const windowWidth =
  window.innerWidth ||
  document.documentElement.clientWidth ||
  document.body.clientWidth;
// 获取浏览器宽度
const origin = {
x: event.pageX/windowWidth,
y: event.pageY/windowHeight,
}
// 获取比例

自定义 canvas


官方支持我们创建自定义画布的  confetti 实例,这样创建的  confetti 不会超出定义的这个画布的范围,这在某些时候可能会起到很重要的作用。


并且官方强调了,一个画布最好只和一个  confetti 实例做关联,所以说当我们创建多个  confetti 实例的时候,就也需要创建多个画布。


因为是自定义添加的画布,所以我们需要在不需要的时候手动去删除这个画布,避免产生多余的元素。


var myCanvas = document.createElement('canvas');
var container = document.querySelector('.container')
container.appendChild(myCanvas)
var myConfetti = confetti.create(myCanvas).then(()=>{
container.removeChild(myCanvas)
});

然后设定位置的操作我这里就不多做了,只需要定义这个 canvas 的生成位置就好了。


示例代码



作者:14332223
来源:juejin.cn/post/7290769553572397056
收起阅读 »

使用a标签下载文件

web
引言 HTML中  <a>  元素(或称锚元素)可以通过它的 href 属性创建通向其他网页、文件、电子邮件地址、同一页面内的位置或任何其他 URL 的超链接。 <a> 中的内容应该指明链接的目标。如果存在 href 属性,当 <...
继续阅读 »

引言


HTML中  <a>  元素(或称元素)可以通过它的 href 属性创建通向其他网页、文件、电子邮件地址、同一页面内的位置或任何其他 URL 的超链接。


<a> 中的内容应该指明链接的目标。如果存在 href 属性,当 <a> 元素聚焦时按下回车键就会激活它。


本文主要讲解如何通过a标签来下载文件。


download属性


浏览器将链接的 URL 视为下载资源。可以使用或不使用 filename 值:




  • 如果没有指定值,浏览器会从多个来源决定文件名和扩展名:



    • Content-DispositionHTTP 标头。

    • URL的最后一段。

    • 媒体类型。来自 Content-Type 标头,data: URL的开头,或 blob: URL 的 Blob.type




  • filename:决定文件名的值。/ 和 \ 被转化为下划线(_)。文件系统可能会阻止文件名中其他的字符,因此浏览器会在必要时适当调整文件名。





备注:



  • download 只在同源 URL或 blob:data: 协议起作用。

  • 浏览器对待下载的方式因浏览器、用户设置和其他因素而异。在下载开始之前,可能会提示用户,或者自动保存文件,或者自动打开。自动打开要么在外部应用程序中,要么在浏览器本身中。

  • 如果 Content-Disposition 标头的信息与 download 属性不同,产生的行为可能不同:

  • 如果文件头指定了一个 filename,它将优先于 download 属性中指定的文件名。

  • 如果标头指定了 inline 的处置方式,Chrome 和 Firefox 会优先考虑该属性并将其视为下载资源。旧的 Firefox 浏览器(版本 82 之前)优先考虑该标头,并将内联显示内容。



下载方式


1. 直接使用a标签的href属性指定文件的URL


可以在a标签中使用href属性指定文件的URL,点击链接时会直接下载文件。


<a href="file_url">Download</a>

优点:简单易用,只需在a标签中指定文件的URL即可。


缺点:无法控制下载文件的名称和保存位置。


2. 使用download属性指定下载文件的名称


可以在a标签中使用download属性指定下载文件的名称,点击链接时会将文件以该名称保存到本地。


<a href="file_url" download="file_name">Download</a>

优点:可以控制下载文件的名称。


缺点:无法控制下载文件的保存位置。


3. 将文件数据转为Blob进行下载


当需要将文件数据转为Blob或Base64进行下载时,可以使用以下方法:


1. 将文件数据转为Blob进行下载


function downloadFile(data, filename, type) {
const blob = new Blob([data], { type: type });
const url = URL.createObjectURL(blob);

const link = document.createElement('a');
link.href = url;
link.download = filename;

document.body.appendChild(link);
link.click();

document.body.removeChild(link);
URL.revokeObjectURL(url);
}

function fileToBlob(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();

reader.onloadend = () => {
resolve(new Blob([reader.result], { type: file.type }));
};

reader.onerror = reject;

reader.readAsArrayBuffer(file);
});
}

// 使用示例
const fileData = ...; // 文件数据
const fileName = 'example.txt';
const fileType = 'text/plain';

fileToBlob(fileData).then(blob => {
downloadFile(blob, fileName, fileType);
});

首先,我们定义了一个名为downloadFile的函数,它接受三个参数:文件数据(data),文件名(filename)和文件类型(type)。 在函数内部,我们使用Blob构造函数将文件数据和类型传递给它,从而创建一个Blob对象。然后,我们使用URL.createObjectURL()方法创建一个URL,该URL指向Blob对象。 接下来,我们创建一个<a>元素,并设置其href属性为之前创建的URL,并将下载属性设置为指定的文件名。然后将该元素添加到文档的body中。 最后,我们模拟用户点击该链接进行下载,并在完成后清理相关资源。


在使用时,我们首先调用fileToBlob函数将文件数据转换为Blob对象。该函数返回一个Promise对象,在Promise的resolve回调中返回了转换后的Blob对象。 然后,在Promise的回调中调用了downloadFile函数来进行下载。


2. 将文件数据转为Base64进行下载


function downloadBase64File(base64Data, filename, type) {
const byteCharacters = atob(base64Data);
const byteArrays = [];

for (let i = 0; i < byteCharacters.length; i++) {
byteArrays.push(byteCharacters.charCodeAt(i));
}

const blob = new Blob([new Uint8Array(byteArrays)], { type: type });
const url = URL.createObjectURL(blob);

const link = document.createElement('a');
link.href = url;
link.download = filename;

document.body.appendChild(link);
link.click();

document.body.removeChild(link);
}

function fileToBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();

reader.onloadend = () => {
resolve(reader.result.split(',')[1]);
};

reader.onerror = reject;

reader.readAsDataURL(file);
});
}

// 使用示例
const fileData = ...; // 文件数据
const fileName = 'example.txt';
const fileType = 'text/plain';

fileToBase64(fileData).then(base64Data => {
downloadBase64File(base64Data, fileName, fileType);
});

首先,我们定义了一个名为downloadBase64File的函数,它接受三个参数:Base64字符串(base64Data),文件名(filename)和文件类型(type)。 在函数内部,我们首先将Base64字符串解码为字节数组,并将其存储在byteArrays数组中。然后,我们使用这些字节数组创建一个Blob对象,并使用URL.createObjectURL()方法创建一个URL。 接下来,我们创建一个<a>元素,并设置其href属性为之前创建的URL,并将下载属性设置为指定的文件名。然后将该元素添加到文档的body中。 最后,我们模拟用户点击该链接进行下载,并在完成后清理相关资源。


在使用时,我们首先调用fileToBase64函数将文件数据转换为Base64字符串。该函数返回一个Promise对象,在Promise的resolve回调中返回了转换后的Base64字符串。 然后,在Promise的回调中调用了downloadBase64File函数来进行下载。


总结


您可以根据需要选择将文件数据转为Blob或Base64进行下载。如果您已经有文件数据,可以使用fileToBlob函数将其转为Blob对象并进行下载。如果您希望将文件数据转为Base64进行下载,可以使用fileToBase64函数将其转为Base64字符串,并使用downloadBase64File函数进行下载。


作者:前端俊刚
来源:juejin.cn/post/7291200719683502120
收起阅读 »

前端实现蜂巢布局思路

web
效果图如下 上图的组成可以分为两部分,一个为底图六边形的形成,一个为内容六边形的形成。 要生成对应的六边形,首先要获得需要绘制的六边形的中心坐标。 观察不难得出结论,以中心的六边形为基点,第二圈很明显能排6个,第三圈能排12个,以此进行类推。 这里可以以中...
继续阅读 »

效果图如下


image.png
上图的组成可以分为两部分,一个为底图六边形的形成,一个为内容六边形的形成。


要生成对应的六边形,首先要获得需要绘制的六边形的中心坐标。


观察不难得出结论,以中心的六边形为基点,第二圈很明显能排6个,第三圈能排12个,以此进行类推。


image.png


这里可以以中心点为坐标原点[0,0],以[1,0], [1,1],[-1,1],[-1,0],[-1,-1],[1,-1]这种形式来表示第二圈在轴线上的六个六边形的中心点关系(这是一种形式,并非真实的坐标系坐标)。


// 第二圈时的对应圆上的坐标值
const pixiArr = [
[1, 0],
[1, 1],
[-1, 1],
[-1, 0],
[-1, -1],
[1, -1]
]

// 根据圈数生成对应轴线上的坐标
function generatePixiArrByWeight(weight) {
if (weight === 2) {
return pixiArr
}
const multiple = weight - 1
const tempPixiArr = pixiArr.map((x) => {
return [x[0] * multiple, x[1] * multiple]
})
return tempPixiArr
}

进一步观察,可知,第三圈时两条发散的轴中间夹了一个六边形,第四圈时两条发散的轴中间夹了两个六边形,依次类推。


六条发散轴上的六边形中心点坐标是最容易计算的,不过要计算三圈及其开外的,就得有那么一点点的数学基础,知道sin60度cos60度的意思。


const sin60 = Math.sin(Math.PI / 3)
const cos60 = Math.cos(Math.PI / 3)

有了上面的铺垫后就可以开始了,定义一个函数,传入的参数为六边形总个数和六边形的边长


// 生成六边形中心坐标
function getHexagonCoordinateArrayByTotal(total = 0, radius = 0){
// 1、获取圈数weight
if (total === 0) return []
let tierList = [] // 用于存放每圈的个数
let tierIndex = 0
while (total > 0) {
if (tierIndex === 0) {
tierList.push(1)
total = total - 1
} else {
let n = 6 * tierIndex
total = total - n
if (total < 0) {
tierList.push(total + n)
} else {
tierList.push(n)
}
}
tierIndex++
}
const weight = tierList.length
// 2、根据圈数去获取coordinateArray坐标列表
// getHexagonCoordinateArrayByWeight:根据圈数和边长返回对应的坐标点
const weight = tierList.length
let coordinateArray = []
for (let i = 0; i < weight; i++) {
if (i + 1 === weight) {
coordinateArray = [
...coordinateArray,
...getHexagonCoordinateArrayByWeight(i + 1, radius).slice(
0,
tierList[weight - 1]
)
]
} else {
coordinateArray = [
...coordinateArray,
...getHexagonCoordinateArrayByWeight(i + 1, radius)
]
}
}

return coordinateArray
}

有个getHexagonCoordinateArrayByWeight需要实现其,方式为


function _abs(val = 0) {
return Math.abs(val)
}

function getHexagonCoordinateArrayByWeight(weight = 1, radius = 0) {
if (weight === 0) return []
if (weight === 1) return [[0, 0]]
const addNum = weight - 2
const addArr = generatePixiArrByWeight(weight)
const hypotenuse = radius * sin60 * 2 // 两倍的边心距长度
let offsetArr = []
let offsetX
let offsetY
for (let i = 0; i < addArr.length; i++) {
const t = addArr[i]
if (t[1] !== 0) {
offsetX = t[0] * hypotenuse * cos60
offsetY = t[1] * hypotenuse * sin60
} else {
offsetX = t[0] * hypotenuse
offsetY = 0
}
offsetArr.push([offsetX, offsetY])
}
const tempOffsetArr = JSON.parse(JSON.stringify(offsetArr))
let resArr = new Array(6 * (weight - 1))
let lineArr = []
for (let i = 0; i < 6; i++) {
let lindex = i * (weight - 1)
resArr[lindex] = tempOffsetArr[i]
lineArr.push(lindex)
}
// 利用已知的六个发散轴上的中心坐标点推出剩余的中心坐标点
if (addNum > 0) {
for (let i = 0; i < 6; i++) {
let s = tempOffsetArr[i]
let e = i + 1 === 6 ? tempOffsetArr[0] : tempOffsetArr[i + 1]
let si = lineArr[i]
let sp = addNum + 1
let fx
let fy
if (i === 0) {
fx = (s[0] - e[0]) / sp
fy = (e[1] - s[1]) / sp
}
if (i === 1) {
fx = (_abs(s[0]) + _abs(e[0])) / sp
fy = 0

}
if (i === 2) {
fx = (_abs(e[0]) - _abs(s[0])) / sp
fy = (_abs(s[1]) - _abs(e[1])) / sp
}
if (i === 3) {
fx = (_abs(s[0]) - _abs(e[0])) / sp
fy = (_abs(e[1]) - _abs(s[1])) / sp
}
if (i === 4) {
fx = (_abs(s[0]) + _abs(e[0])) / sp
fy = 0
}
if (i === 5) {
fx = _abs(s[0]) / sp
fy = (_abs(e[1]) - _abs(s[1])) / sp
}
let mr = []
for (let j = 0; j < addNum; j++) {
if (i === 0 || i === 1) {
mr.push([s[0] - fx * (j + 1), s[1] + fy * (j + 1)])
}
if (i === 2) {
mr.push([s[0] - fx * (j + 1), s[1] - fy * (j + 1)])
}
if (i === 3) {
mr.push([s[0] + fx * (j + 1), s[1] - fy * (j + 1)])
}
if (i === 4) {
mr.push([s[0] + fx * (j + 1), s[1] - fy * (j + 1)])
}
if (i === 5) {
mr.push([s[0] + fx * (j + 1), s[1] - fy * (j + 1)])
}
}
mr.forEach((x, index) => {
resArr[si + index + 1] = x
})
}
}
return resArr
}

至此,生成六边形中心坐标点的方法完成。
有了中心坐标生成方式之后,就可以使用Konva这种辅助绘图的库来进行效果绘制了。


作者:前端_六一
来源:juejin.cn/post/7291125785796018236
收起阅读 »

前端调取摄像头并实现拍照功能

web
前言: 最近接到的一个需求十分有意思,设计整体实现了前端仿微信扫一扫的功能。整理了一下思路,做一个分享。 tips: 如果想要实现完整扫一扫的功能,你需要掌握一些前置知识,这次我们先讲如何实现拍照并且保存的功能。 一. window.navigator 你...
继续阅读 »

前言: 最近接到的一个需求十分有意思,设计整体实现了前端仿微信扫一扫的功能。整理了一下思路,做一个分享。


tips: 如果想要实现完整扫一扫的功能,你需要掌握一些前置知识,这次我们先讲如何实现拍照并且保存的功能。


一. window.navigator




  1. 你想调取手机的摄像头,首先你得先检验当前设备是否有摄像设备,window 身上自带了一个 navigator 属性,这个对象有一个叫做 mediaDevices 的属性是我们即将用到的。




  2. 于是我们就可以先设计一个叫做 checkCamera 的函数,用来在页面刚开始加载的时候执行。。

    image.png




  3. 我们先看一下这个对象有哪些方法,你也许会看到下面的场景,会发现这个属性身上只有一个值为 nullondevicechange 属性,不要怕,真正要用的方法其实在它的原型身上。
    image.png




  4. 让我们点开它的原型属性,注意下面这两个方法,这是我们本章节的主角。

    image.png




  5. 我们到这一步只是需要判断当前设备是否有摄像头,我们先调取 enumerateDevices 函数来查看当前媒体设备是否存在。它的返回值是一个 promise 类型,我们直接用 asyncawait 来简化一下代码。
    image.png
    image.png
    从上图可以看出,我的电脑有两个音频设备和一个视频设备,那么我们就可以放下进行下一步了。




二. 获取摄像头




  1. 接下来就需要用到上面提到的第二个函数,navigator.getUserMedia。这个函数接收一个对象作为参数,这个对象可以预设一些值,来作为我们请求摄像头的一些参数。




  2. 这里我们的重点是 facingMode 这个属性,因为我们扫一扫一般都是后置摄像头
    image.png
    当你执行了这个函数以后,你会看到浏览器有如下提示:

    image.png




  3. 于是你高兴的点击了允许,却发现页面没有任何变化。

    image.png




  4. 这里你需要知道,这个函数只是返回了一个媒体流信息给你,你可以这样简单理解刚刚我们干了什么,首先浏览器向手机申请我想用一下摄像头可以吗?在得到了你本人的确认以后,手机将摄像头的数据线递给了浏览器,:“诺,给你。”




  5. 浏览器现在仅仅拿到了一根数据线,然而浏览器不知道需要将这个摄像头渲染到哪里,它不可能自动帮你接上这根线,你需要自己找地方接上这根数据线。




  6. 这里不卖关子,我们需要请到我们的 Video 标签。我没听错吧?那个播放视频的 video 标签?没错,就是原生的 video 标签。




  7. 这里创建一个 video 标签,然后打上 ref 来获取这个元素。

    image.png




  8. 这里的关键点在于将流数据赋值给 video 标签的 srcObject 属性。就好像你拿到了数据线,插到了显示器上。

    (tips: 这里需要特别注意,不是 video.src 而是 video.srcObject 请务必注意)

    image.png




  9. 现在你应该会看到摄像头已经在屏幕上展示了,这里是我用电脑前置摄像头录制的一段视频做成了gif。(脉动请给我打钱,哼)
    camera.gif




三. 截取当前画面




  1. 这里我随手写了一个按钮当作拍摄键,接下来我们将实现点击这个按钮截取当前画面。

    image.png




  2. 这里你需要知道一个前提,虽然我们现在看到的视频是连贯的,但其实在浏览器渲染的时候,它其实是一帧一帧渲染的。就像宫崎骏有些动漫一样,是一张一张手写画。




  3. 让我们打开 Performance 标签卡,记录一下打开掘金首页的过程,可以看到浏览器的整个渲染过程其实也是一帧一帧拼接到一起,才完成了整个页面的渲染。

    11.gif




  4. 知道了这个前提,那么举一反三,我们就可以明白,虽然我们现在已经打开了摄像头,看到的视频好像是在连贯展示,但其实它也是一帧一帧拼到一起的。那现在我们要做的事情就非常明了,当我按下按钮的时候,想办法将 video 标签当前的画面保存下来。




  5. 这里不是特别容易想到,我就直接说答案了,在这个场景,我们需要用到 canvas 的一些能力。不要害怕,我目前对 canvas 的使用也不是特别熟练,今天也不会用到特别复杂的功能。




  6. 首先创建一个空白的 canvas 元素,元素的宽高设置为和 video 标签一致。

    image.png




  7. 接下来是重点: 我们需要用到 canvasgetContext 方法,先别着急头晕,这里你只需要知道,它接受一个字符串 "2d" 作为参数就行了,它会把这个画布的上下文返回给你。
    tips 如果这里还不清楚上下文的概念,也不用担心,这里你就简单理解为把这个 canvas 这个元素加工了一下,帮你在它身上添加了一些新的方法而已。)
    image.png




  8. 在这个 ctx 对象身上,我们只需要用到一个 drawImage 方法即可,不需要关心其它属性。

    image.png




  9. 感觉参数有点多?没关系,我们再精简一下,我们只需要考虑第二个用法,也就是5参数的写法。(sx,sy 是做裁切用到的,本文用不到,感兴趣可以自行了解。)

    image.png




  10. 这里先简单解释一下 dxdy 是什么意思。在 canvas 里也存在一个看不见的坐标系,起点也是左上角。设想你想在一个 HTMLbody 元素里写一个距离左边距离 100px 距离顶部 100px的画面,是不是得写 margin-left:100px margin-top:100px 这样的代码?没错,这里的 dydx 也是同样的道理。

    image.png




  11. 我们再看 dwidth,和 dheight,从这个名字你就能才出来,肯定和我们将要在画笔里画画的元素的宽度和高度有关,是的,你猜的没错,它就好像你设置一个 div 元素的高度和宽度一样,代表着你将在画布上画的截图的宽高属性。




  12. 现在只剩下第一个参数还没解释,这里直接说答案,我们可以直接将 video 标签填进去,ctx 会自动将当前 video 标签的这一帧画面填写进去。现在按钮的代码应该是这个样子。


    function shoot() {
    if (!videoEl.value || !wrapper.value) return;
    const canvas = document.createElement("canvas");
    canvas.width = videoEl.value.videoWidth;
    canvas.height = videoEl.value.videoHeight;
    //拿到 canvas 上下文对象
    const ctx = canvas.getContext("2d");
    ctx?.drawImage(videoEl.value, 0, 0, canvas.width, canvas.height);
    wrapper.value.appendChild(canvas);//将 canvas 投到页面上
    }



  13. 测试一下效果。

    112.gif




四. 源码


<script lang="ts" setup>
import { ref, onMounted } from "vue";

const wrapper = ref<HTMLDivElement>();
const videoEl = ref<HTMLVideoElement>();

async function checkCamera() {
const navigator = window.navigator.mediaDevices;
const devices = await navigator.enumerateDevices();
if (devices) {
const stream = await navigator.getUserMedia({
audio: false,
video: {
width: 300,
height: 300,
// facingMode: { exact: "environment" }, //强制后置摄像头
facingMode: "user", //前置摄像头
},
});
if (!videoEl.value) return;

videoEl.value.srcObject = stream;
videoEl.value.play();
}
}

function shoot() {
if (!videoEl.value || !wrapper.value) return;
const canvas = document.createElement("canvas");
canvas.width = videoEl.value.videoWidth;
canvas.height = videoEl.value.videoHeight;
//拿到 canvas 上下文对象
const ctx = canvas.getContext("2d");
ctx?.drawImage(videoEl.value, 0, 0, canvas.width, canvas.height);
wrapper.value.appendChild(canvas);
}

onMounted(() => {
checkCamera();
});
</script>
<template>
<div ref="wrapper" class="w-full h-full bg-red flex flex-col items-center">
<video ref="videoEl" />
<div
@click="shoot"
class="w-100px leading-100px text-center bg-black text-30px"
>
拍摄
</div>
</div>
</template>



五. 总结


实现拍照的整体思路其实很简单,仅仅需要了解到视频其实也是一帧一帧画面构成的,而 canvas 恰好有捕捉当前帧的能力。


预告:在下一篇会讲解如何实现扫一扫的功能,需要用到插件,感兴趣的同学可以先预习一下功课。🎁二维码扫码插件


趁热打铁🧭:前端实现微信扫一扫的思路


作者:韩振方
来源:juejin.cn/post/7289662055183597603
收起阅读 »

前端实现微信扫一扫的思路

web
前言: 在有了获取手机摄像头权限并且记录当前画面的前置知识以后,我们现在已经可以进行下一步实现一个仿微信扫一扫的功能了。 tips: 如果你是直接看的本文,对如何打开摄像头拍照这个功能还不太熟悉,请移步 🎁前端如何打开摄像头拍照。这是你阅读本篇的必需...
继续阅读 »

前言: 在有了获取手机摄像头权限并且记录当前画面的前置知识以后,我们现在已经可以进行下一步实现一个仿微信扫一扫的功能了。


tips: 如果你是直接看的本文,对如何打开摄像头拍照这个功能还不太熟悉,请移步 🎁前端如何打开摄像头拍照。这是你阅读本篇的必需前置知识。


一. 效果预览


这里先简单放一下整体界面效果,接下来带大家一步一步分析其中的功能如何实现。


2.gif


本篇将重点讲解多张二维码识别的处理场景。


二. 简单了解二维码




  1. 现在流行使用的二维码是 qrcode,其中 qr 两个字母其实就是 quick response 的缩写,简单来说就是快速响应的意思。三个角用来定位,黑点表示二进制的1,白色点代表0。(这里感兴趣可以自行了解) 它的本质其实就是将一个地址链接利用某种约定好的规则隐藏到一个图片当中,

    image.png




  2. 我们可以利用 chrome 自带的创建当前网站二维码的功能快速体验一下。
    qr.gif




  3. 你可以用手机自带的二维码扫码软件扫一下这个二维码,它将会将你引导到我掘金的个人主页。

    qrcode_juejin.cn.png




  4. 细心的你可能会发现二维码下面已经给你提示了你准备保存的链接地址,现在你观察一下浏览器地址栏是否正对应下面这个地址呢?
    image.png




三. 实现扫码本地图片功能




  1. 我们不需要深入了解这个二维码的转换规则,我们可以直接选用现有的插件即可完成识别功能。 这里我们选用 antfu 大佬的轮子。这里我们不过多介绍,你只需要它可以识别出图片中的二维码即可。如果感兴趣,这是具体仓库地址 qr-sanner-wechat




  2. 首先安装 npm i qr-scanner-wechat




  3. 它的使用方法也十分简单,这个依赖导出了一个方法,我们直接引入这个方法即 import { scan } from 'qr-scanner-wechat




  4. 这个函数可以接收一个 image 元素或者 canvas 元素作为参数,并且返回一个 promise 类型的值。




  5. 我们先来测试最简单的,传入一个 image 元素,利用 input 标签的 type=file 属性,就可以从本地选择图片,比较基础的知识,不过多赘述,代码如下。


    function getImageFromLocal(e: Event) {
    const inputEl = e.target as HTMLInputElement;
    if (!inputEl) return;
    console.log("inputEl.files", inputEl.files);
    }



    然后我们可以通过 input 元素绑定的 onChange 回调中拿到 input 元素身上的 files 属性,就可以获取到刚刚我们选择的文件信息。

    ee.gif




  6. 但是目前这个数据对象我们还无法使用,需要借助 URL.createObjectUrl 方法来创建一个普通的 url 地址。

    image.png




  7. 当拿到这个 url 地址以后该如何使用呢?🤔
    image.png




  8. 一个熟悉的老朋友,有请 img 标签出场,👏,我们只需要将 img 标签的 src 替换成刚刚保存的 url 地址即可。

    image.png

    现在整体效果应该是这样的:

    code.gif




  9. 有了 img 元素,我们直接将这个元素赋值给 qr-scanner-wechat 插件提供的 scan 函数即可。

    image.png




  10. 我们来测试一下整体流程。

    qw.gif




  11. 可以看到,scan 函数返回了一个对象,这个对象身上有两个十分重要的属性。一个叫做 rect (rectangle 长方形的单词缩写),这个属性描述了这个插件在什么位置扫描到了二维码,另外一个属性就是 text,也就是这个图片上隐藏的字符串地址

    image.png




  12. 这里我们再讲解一下 rect 属性,因为后面的功能需要你对这个属性有比较清晰的理解。我们对比一个现实世界的例子。当你掏出手机扫描二维码的时候,往往并不会正好对准一个二维码的图片,或者会遇到一个图片中存在两个二维码的情况,如下图:

    image.png




  13. 这个 qr-scanner 插件会帮你把二维码所在整张图片的相对位置告诉你,因为这个插件每次调用 scan 函数只会返回一次结果。并不是说图片上有两个二维码,它的识别结果就会有两个,所以说这个 qr-scanner 插件的识别效果也并不是百分之一百准确的。




四. 理清思路




  1. 说了这么多,那么这个 rect 我们该如何利用起来呢?别着急,我们先理清思路再动手写代码,到了目前这一步会出现两种情况。

    image.png




  2. 第一种是图片压根就没有二维码,这个简单,提示用户重新放置图片即可。




  3. 关键点就在于第二张情况,当图片上扫码到存在一个二维码后,我们该如何判断是否存在第二个或多个维码呢?

    image.png




  4. 我们看一下微信的实现效果,当只有一张二维码的时候,它会直接跳转,当有多个二维码的时候,它会将整个页面暂停,并且提示用户有两张二维码,请点击选择一个进行跳转。

    image.png




  5. 但是我们上面提到了,scan 函数每次只会扫描一次图片,返回一个识别结果,它并不能准确知道图片上到底有几个二维码。那放到现实生活我们会怎么做呢?




  6. 举个例子,假如我们现在掏出手机扫一扫的功能,现在给你的图片上有两个二维码,但是我明确的知道我就想扫第二个,你会怎么做?

    image.png




  7. 这不是很简单的道理吗?我拿手挡住第一个二维码不就可以了吗?

    image.png




  8. 那么利用同样的思路,我们可以再扫描到一张二维码的时候,想办法把当前识别到的这个二维码位置给遮挡住,然后将被遮挡后的照片传递给 scan 函数再次扫描。




  9. 那么整个过程就是,我们首先将完整的照片传给 scan,然后 scan 觉得第一张二维码比较帅,就先识别了它。(tips: 这里需要提醒一下,scan 有时候会觉得第二张二维码比较帅,那我就识别第二张二维码,要注意的它的顺序性是随机的)

    image.png




  10. 然后我们想办法盖上遮挡物,然后将这个图片传给 scan,让它再次确认是否有第二个二维码。

    image.png




  11. 在哪覆盖?还记不记 rect 属性保留有这个二维码的位置信息?现在的问题就转变为如何覆盖了?




  12. 这里需要用到 canvas 元素的一丢丢基础知识,这是 mdn canvas 基础知识的介绍,十分简单的就画出了一个绿色长方体。

    image.png

    ctx.filleRect可以接收四个参数,分别是相对于画布起始轴的 xy 的距离。

    简单来讲就可以理解为每一个 canvas 就相当于一个独立的 HTML 文件,也有自己的独立坐标系系统,x,y 就相当于 margin,至于后面两个参数,其实就代表着你要画的长方形宽度高度
    image.png




13.那这不巧了吗,scan 的返回值 rect 恰好就有这几个值。

image.png



  1. 话不多说,马上开始实践。⛽️


五. 处理存在多张二维码的图片




  1. 注意: 以下内容我统一选用从本地照片上传作为演示,从摄像头获取图片是同样的道理,详细介绍请移步 🎁前端如何打开摄像头拍照。在下面的讲解过程,我会默认你已经阅读了前置知识。




  2. 这里我就继续沿用之前提到的图片,我将他们拼接到了一张图片上。

    二.png




  3. 下面应该是你目前从本地选择二维码图片识别的代码。


    async function getImageFromLocal(e: Event) {
    const inputEl = e.target as HTMLInputElement;
    if (!inputEl) return;
    if (!inputEl.files?.length) return;
    const image = inputEl.files[0];
    const url = URL.createObjectURL(image);
    src.value = url;
    const result = await scan(imgEl.value!);
    console.log("result", result);
    }



  4. 接下来我们需要先创建一个 canvas 来将当前的照片拷贝到画布上,然后准备利用得到的 rect 信息在这个 canvas 元素上绘画。

    image.png




  5. 为了方便展示,我调用 appendChildren 方法将这个 canvas 元素打印到了界面上。

    1.gif




  6. 然后用 resultrect坐标宽度信息,去调用我们提到的 canvasfillStyle fillRect 方法。

    image.png

    下面是目前实现的效果:

    1.gif




  7. 注意scan 函数不仅仅可以接受 imgElment 作为扫描的参数,它还可以接受 canvas 元素作为扫描的参数。聪明的你看到这里,或许已经猜到我们下一步准备做什么了。




  8. 那么此时我们就可以将这个已经被黑色涂鸦覆盖过的 canvas 进行二次扫描。(暂时不要考虑代码的优雅性,这里只是更清晰的说明我们在干什么,之后我们会封装几个方法,然后整理一下代码)

    image.png

    让我们再看一下效果:

    2.gif




  9. 通过多次重复上面的操作,就可以将图片上所有的二维码都尽量识别出来。

    image.png

    现在实现的效果:

    11.gif

    同时图片上相对应的识别内容也全都被正确的被获取到了。

    image.png




  10. 此时我们创建一个 Map 来保存这些数据。Mapkey 就是 text ,对应的 value 就是 rect 坐标信息。

    image.png

    image.png




六. 弹出可以点击的小蓝块




  1. 有了坐标信息和位置信息,并且我们的 canvasimg 元素的坐标轴系统是一一对应的,那么我们就可以写一个函数来遍历这个 resultMap,然后根据位置信息在 img 元素所在的 div 上打印出我们想要的样式。




  2. 首先在 img 元素外面包一层 div,打上 ref 叫做 imgWrapper 。因为之后我们要用它当作小蓝块的定位元素,所以先设置 position:relative

    image.png




  3. 绘画代码如下,都是基础的方法,不再过多赘述。


    //多个二维码时添加动态小蓝点
    function draw() {
    resultMap.forEach((rect, link) => {
    if (!imgWrapper.value) return;
    const dom = document.createElement("div");
    const { x, y, width, height } = rect;
    const _x = (x || 0) + width / 2 - 20;
    const _y = (y || 0) + height / 2 - 20;
    dom.className = "blue-chunk";
    dom.style.width = "40px";
    dom.style.height = "40px";
    dom.style.background = "#2ec1cc";
    dom.style.position = "absolute";
    dom.style.zIndex = "9999999";
    dom.style.top = _y + "px";
    dom.style.left = _x + "px";
    dom.style.color = "#fff";
    dom.style.textAlign = "center";
    dom.style.borderRadius = "100px";
    dom.style.borderBlockColor = "#fff";
    dom.style.borderColor = "unset";
    dom.style.borderRightStyle = "solid";
    dom.style.borderWidth = "3px";
    dom.style.animation = "scale-animation 2s infinite";
    dom.addEventListener("click", () => {
    console.log(link);
    });
    imgWrapper.value.appendChild(dom);
    });
    }



  4. 然后再 for 循环以后开始绘画小蓝块。

    image.png




  5. 让我们预览一下现在的效果:

    112.gif




  6. 让我们测试一下相对应的点击事件

    3.gif




七. 源码





八.总结


本篇文章的关键点就是讲解了我在实现处理多张二维码的场景时的思路,利用 canvas 遮挡识别过的二维码这个思路是pbk-bin大佬最先想到的,在实现这个需求以后还是很感叹这个思路的巧妙。👏


再次特别感谢pbk-bin🎁~


如果文章对你有帮助,不妨赠人玫瑰,手有余香,预计将会在下篇更新较为完整的微信扫一扫界面和功能。


作者:韩振方
来源:juejin.cn/post/7290813210276724771
收起阅读 »

🤔公司实习生居然还在手动切换node版本?

web
前段时间看了实习生的新项目,发现他启动不了项目,因为node版本太低,我让他去用nvm来管理node的版本,然后看到他切换版本的时候是这样的,先用nvm下载安装目标的node版本,然后在把安装好的node版本替换掉原先的node路径下的node_modules...
继续阅读 »

前段时间看了实习生的新项目,发现他启动不了项目,因为node版本太低,我让他去用nvm来管理node的版本,然后看到他切换版本的时候是这样的,先用nvm下载安装目标的node版本,然后在把安装好的node版本替换掉原先的node路径下的node_modules,而不是用命令行进行版本切换,才发现原来他使用nvm来切换node版本虽然显示切换成功,但全局的node版本一直是不变的,所以才用文件覆盖的方式强制进行解决,对于这个问题进行解决并且梳理



可以直接跳到第四步查看解决方案


1️⃣ 安装nvm


where nvm

找不到nvm路径的朋友可以用这个命令来找到nvm的安装路径,默认的安装路径都是差不多的


image.png


2️⃣ 查看目前node版本


可以看到目前的版本是node V16.14.2


image.png


3️⃣ nvm安装目标node版本



nvm的主要作用就是用来切换node的版本,为什么要切换node的版本,就是因为你的当前node版本和你要启动的项目不兼容,有两种可能,要么是你的项目太旧,你的node版本相对来说比较高,需要用到向下兼容;另外一种可能就是你项目用到的node比较新,你需要进行升级



先安装需要安装的目标版本,用isntall来安装你需要的对应node版本


image.png


回到你的nvm安装路径,就可以看到你已经安装的各种版本的node文件夹
image.png


当然也可以用命令行


nvm list

image.png


4️⃣ nvm切换到目标node版本


切换到目标node版本使用nvm use


nvm use

查看目前nvm安装了哪些版本 然后use来进行切换


image.png


到切换的时候发现了问题,这里无论怎么切换,node的版本依然不会变


image.png


可以看到我用的use来切换到15的版本,但是再次查看nvm的node历史版本,可以看到还是位于16.14.2的node版本,明明就是这么顺利的问题,出了一个让人摸不到头脑的事情


5️⃣寻找问题


既然nvm切换版本已经成功,那么为什么node版本不会变,有没有可能根本改的不是同一个node,或者是存在两个node,直到我打开环境变量一看,为啥会存在两个node的路径,可能的原因就是之前的node版本没有删除,node -v一直输出的是安装前的node


image.png


原来已经安装了一个node的,全局的node指向的node路径和nvm切换node的路径是不一样的


nvm切换的node是基于他文件夹中的nodejs


image.png


image.png



点进去看你会发现他也是有一个node.exe的程序的,所以问题是已经找到的了,目前系统上出现了两个node,并且nvm切换的node版本并不是全局的node,因为环境变量已经指向了旧的node,他的版本不会改变,那么nvm去怎么切换都是没有用的



6️⃣解决方案


image.png


看了网上的一些解决方案都是要在nvm中新建两个文件夹的方式来解决,但是其实直接把nodejs删除也是一个很直接的办法,先通过where node找到当前的node的安装目录,直接进行删除


image.png


最后是通过把另外一个目录的node进行删除,重新看一下node的安装路径,也就是重新执行一下 where node


image.png


可以看到在nvm配置正确的情况下是能直接指向nvm下的node的


最后重新切换一下node的版本,也就是上文的操作


image.png


PS


我指的手动切换是nvm下载node版本之后手动去替换node_modules,原来大家觉得用nvm use也是手动替换(确实是我的问题),经过评论区广大jym指正,可以尝试一下volta这个工具来进行切换版本,真正做到不用手动替换,后续我会亲自去体验一下并且发文,感谢评论区小伙伴


🙏 感谢您花时间阅读这篇文章!如果觉得有趣或有收获,请关注我的更新,给个喜欢和分享。您的支持是我写作的最大动力!


作者:一只大加号
来源:juejin.cn/post/7291096702021304354
收起阅读 »

推荐一款“自学编程”的宝藏网站!详解版~(在线编程练习,项目实战,免费Gpt等)

🌟云端源想学习平台,一站式编程服务网站🌟 云端源想官网传送门 ⭐ 📚精品课程:由项目实战为导向的视频课程,知识点讲解配套编程练习,让初学者有方向有目标。🎯 📈课程阶段:每门课程都分多个阶段进行,由浅入深,很适合零基础和有基础的金友们自由选择阶段进行练习学...
继续阅读 »

🌟云端源想学习平台,一站式编程服务网站🌟


云端源想官网传送门


📚精品课程:由项目实战为导向的视频课程,知识点讲解配套编程练习,让初学者有方向有目标。🎯


📈课程阶段:每门课程都分多个阶段进行,由浅入深,很适合零基础和有基础的金友们自由选择阶段进行练习学习。🌈


🎯章节实战:每一章课程都配有完整的项目实战,帮助初学者巩固所学的理论知识,在实战中运用知识点,不断积累项目经验。🔥


💼项目实战:企业级项目实战和小型项目实战结合,帮助初学者积累实战经验,为就业打下坚实的基础,成为实战型人才。🏆




🧩配套练习:根据课程小节设置配套选择练习或编程练习,帮助学习者边学边练,让学习事半功倍。💪


💻在线编程:支持多种编程语言,创建Web前端、Java后端、PHP等多种项目,开发项目、编程练习、数据存储、虚拟机等都可在线使用,并可免费扩展内存,为你创造更加简洁的编程环境。🖥


🤝协作编程:可邀请站内的好友、大佬快速进入你的项目,协助完成当前项目。与他人一起讨论交流,快速解决问题。👥


📂项目导入导出:可导入自己在在线编辑过的项目再次编辑,完成项目后也可以一键导出项目,降低试错成本。🔗


🤖AI协助编程:AI智能代码协助完成编程项目,随时提问,一键复制代码即可使用。💡



🔧插件工具:在使用在线编程时,可在插件工具广场使用常用的插件,安装后即可使用,帮助你提高编程效率,带来更多便捷。🛠


📞一对一咨询:编程过程中,遇到问题随时提问,网站1V1服务(在职程序员接线,不是客服),实时解决你在项目中遇到的问题。📬


🛠工具广场:提供一些好用的在线智能工具,让你能够快速找到各种实用工具和应用。覆盖了多个领域,包括智能AI问答等。🔍



收起阅读 »

面试官:如何判断两个数组的内容是否相等

web
题目 给定两个数组,判断两数组内容是否相等。 不使用排序 不考虑元素位置 例: [1, 2, 3] 和 [1, 3, 2] // true [1, 2, 3] 和 [1, 2, 4] // false 思考几秒:有了😀😀 1. 直接遍历✍ 直接遍历第...
继续阅读 »

题目


给定两个数组,判断两数组内容是否相等。



  • 不使用排序

  • 不考虑元素位置


例:


[1, 2, 3] 和 [1, 3, 2] // true
[1, 2, 3] 和 [1, 2, 4] // false


思考几秒:有了😀😀


1. 直接遍历✍



  • 直接遍历第一个数组,并判断是否存在于在第二个数组中

  • 求差集, 如果差集数组有长度,也说明两数组不等(个人感觉比上面的麻烦就不举例了)


const arr1 =  ["apple", "banana", 1]
const arr2 = ["apple", 1, "banana"]

function fn(arr1, arr2) {
// Arrary.some: 有一项不满足 返回false
// Arrary.indexOf: 查到返回下标,查不到返回 -1
if (arr1.length !== arr2.length) {
return false;
}
return !arr1.some(item => arr2.indexOf(item)===-1)
}

fn(arr1,arr2) // true


  • 细心的小伙伴就会发现:NaN 会有问题


const arr1 =  ["apple", "banana", NaN]
const arr2 = ["apple", NaN, "banana"]

function fn(arr1, arr2) {
if (arr1.length !== arr2.length) {
return false;
}
return !arr1.some(item => arr2.indexOf(item)===-1)
}

fn(arr1,arr2) // false


Arrary.prototype.indexOf() 是使用的严格相等算法 => NaN值永远不相等


Array.prototype.includes() 是使用的零值相等算法 => NaN值视作相等




  • 严格相等算法: 与 === 运算符使用的算法相同

  • 零值相等不作为 JavaScript API 公开, -0和0 视作相等,NaN值视作相等,具体参考mdn文档:


image.png



  • 使用includes


const arr1 =  ["apple", "banana", NaN]
const arr2 = ["apple", NaN, "banana"]

function fn(arr1, arr2) {
if (arr1.length !== arr2.length) {
return false;
}
return !arr1.some(item => !arr2.includes(item))
}

fn(arr1,arr2) // true

使用includes 确实可以判断NaN了,如果数组元素有重复呢?


// 重复的元素都是banana
const array1 = ["apple", "banana", "cherry", "banana"];
const array2 = ["banana", "apple", "banana", "cherry"];
// 或者
// 一个重复的元素是banana, 一个是apple
const array1 = ["apple", "banana", "cherry", "banana"];
const array2 = ["banana", "apple", "apple", "cherry"];


由上可知:这种行不通,接下来看看是否能从给数组元素添加标识入手


2. 把重复元素标识编号✍


这个简单:数组 元素重复 转换成val1, val2


function areArraysContentEqual(arr1, arr2) {
if (arr1.length !== arr2.length) {
return false;
}

// 重复数组元素 加1、2、3
const countArr1 = updateArray(arr1)
const countArr2 = updateArray(arr2)

/**
*
* @param {*} arr 数组 元素重复 转换成val1, val2
* @returns
*/

function updateArray(arr) {
const countMap = new Map();
const updatedArr = [];

for (const item of arr) {
if (!countMap.has(item)) {
// 如果元素是第一次出现,直接添加到结果数组
countMap.set(item, 0);
updatedArr.push(item);
} else {
// 如果元素已经出现过,添加带有编号的新元素到结果数组
const count = countMap.get(item) + 1;
countMap.set(item, count);
updatedArr.push(`${item}${count}`);
}
}
return updatedArr;
}
const flag = countArr1.some(item => !countArr2.includes(item))
return !flag
}

const array1 = ["apple", "banana", "cherry", "banana"];
const array2 = ["banana", "apple", "banana", "cherry"];

areArraysContentEqual(array1, array2) // true

// 其实这种存在漏洞的
const array3 = ["apple", "banana", "cherry", "banana", 1, '1', '1' ];
const array4 = ["banana", "apple", "banana", "cherry", '1', 1, 1];
// 应该是false
areArraysContentEqual(array3, array4) // true

因为把判断的 转为了字符串 updatedArr.push(${item}${count}) 所以出问题了


3. 统计元素次数(最终方案)✍


function areArraysContentEqual(arr1, arr2) {
if (arr1.length !== arr2.length) {
return false;
}

// 创建计数对象,用于记录每个元素在数组中的出现次数
const countMap1 = count(arr1)
const countMap2 = count(arr2)

// 统计数组中的元素出现次数
function count(arr = []) {
const resMap = new Map();
for (const item of arr) {
resMap.set(item, (resMap.get(item) || 0) + 1);
}
return resMap
}
// 检查计数对象是否相等
for (const [key, count] of countMap1) {
if (countMap2.get(key) !== count) {
return false;
}
}

return true;
}

const array1 = ["apple", "banana", "cherry", "banana", 1, '1', '11', 11];
const array2 = ["banana", "apple", "banana", "cherry", '1', 1, '11', 11];

areArraysContentEqual(array1, array2) // true


注意事项


这个题需要注意:



  • 先判断长度,长度不等 必然不等

  • 元素可重复

  • 边界情况考虑

    • '1' 和 1 (Object的key是字符串, Map的key没有限制)

    • NaN

    • null undefined




结语:


如果本文对你有收获,麻烦动动发财的小手,点点关注、点点赞!!!👻👻👻


因为收藏===会了


如果有不对、更好的方式实现、可以优化的地方欢迎在评论区指出,谢谢👾👾👾


作者:程序员小易
来源:juejin.cn/post/7290786959441117243
收起阅读 »

JavaScript 基础(一)

web
变量 变量概述 数据类型 运算符 算数运算符 递增递减运算符 比较运算符 逻辑运算符 赋值运算符 运算符优先级 选择结构 流程控制(顺序,分支,循环) 小结 数组 函数 函数基本概念 arguments 函数声明方式 Java...
继续阅读 »

变量


变量概述


image.png


数据类型


image.png
image.png


运算符


算数运算符


image.png


递增递减运算符


image.png


比较运算符


image.png


逻辑运算符


image.png


赋值运算符


image.png


运算符优先级


image.png


选择结构


流程控制(顺序,分支,循环)


image.png


小结


image.png


数组


image.png


函数


函数基本概念


image.png
image.png
image.png


arguments


image.png


函数声明方式


image.png


JavaScript作用域


image.png


预解析


image.png
image.png


sum(1)(2,3)(4,5,6) js的链式调用


function sum(){
let arr = Array.prototype.slice.call(arguments);
fn = function(){
let arr1 = Array.prototype.slice.call(arguements)
return sum.apply(null,arr.concat(arr1))
}
//`reduce()` 方法将数组缩减为单个值。
//`reduce()` 方法为数组的每个值(从左到右)执行提供的函数。
//函数的返回值存储在累加器中(结果/总计)。
//注释:对没有值的数组元素,不执行 `reduce()` 方法。
//注释:`reduce()` 方法不会改变原始数组。
fn.toString = function(){
return arr.reduce((value,n)=>{
return value+n
})
}
return fn
}

//柯里化的高级实现
function curry(func){
return curried(...args){
if(args.length >= func.length){
return func.apply(this,args);
}else{
return function(...args2){
return curried.apply(this,args.concat(args2))
}
}
}
}

面试点


image.png


sum(1)(2)(3)怎么实现


面试的时候,面试官让我讲一个这个怎么实现
当时想到的是嵌套闭包,调用返回值,进行累加


//这个是我在其他文章上看到的代码
function sum(a){
function add(b){
a =a+b
return add;
}
add.toString = function(){
return a;
}
return sum;


查询了一下,涉及到了一个知识点,函数柯里化


//正常的函数
function sum(a,b){
var sum = 0;
sum = a+ b;
console.log(sum);
}

//柯里化函数
function curry(a){
return function(b){
console.log(a+b)
}
}
const sum = curry(1);
// 这个过程分为三步
// step1:
// addCurry(1)
// 返回下面的函数
// ƒ (arg) {
// return judge(1, arg);
// }
// step2:
// addCurry(1)(2)
// 返回下面的函数
// ƒ (arg) {
// return judge(1,2, arg);
// }
// step3:
// addCurry(1)(2)(3)
// 返回并执行下面的函数
// return fn(1,2,3);
// 最终得到结果6

函数柯里化


把接收多个参数的函数变换成接受一个单一参数的函数,并且但会接收余下的采纳数并且返回结果的新函数的技术。eg:它是指将一个函数从可调用的 f(a, b, c) 转换为可调用的 f(a)(b)(c)


参数复用


用于正则表达式的检验


//记录思路
柯里化检验函数curry
const check = curry();
生成工具化函数,进行验证
const checkphone = check(/'正则表达式'/)
检验电话号码
checkphone('122333232');

延迟计算


返回的函数都不会立即执行,而是等待调用(开发者调用)

动态生成函数


比如说,在dom结点中每每绑定一次事件,都需要对环境进行判断,再去绑定这个事件;将这个过程进行柯里化,在使用前进行一次判断

const addEvent = (function() {
if (window.addEventListener) {
return function(ele) {
return function(type) {
return function(fn) {
return function(capture) {
ele.addEventListener(type, (e) => fn.call(ele, e), capture);
}
}
}
}
} else if (window.attachEvent) {
return function(ele) {
return function(type) {
return function(fn) {
return function(capture) {
ele.addEventListener(type, (e) => fn.call(ele, e), capture);
}
}
}
}
}
})();

// 调用
addEvent(document.getElementById('app'))('click')((e) => {console.log('click function has been call:', e);})(false);

// 分步骤调用会更加清晰
const ele = document.getElementById('app');
// get environment
const environment = addEvent(ele)
// bind event
environment('click')((e) => {console.log(e)})(false);
//来自于[一文搞懂Javascript中的函数柯里化(currying) - 知乎 (zhihu.com)](https://zhuanlan.zhihu.com/p/120735088)

柯里化函数后虽然代码比较冗长,但是它的适用性增加了

柯里化封装


eg : sum(1,2,3...)


    //函数柯里化封装(这个封装可以直接复制走使用)
function curry(fn, args) {
var length = fn.length;
var args = args || [];
return function () {
newArgs = args.concat(Array.prototype.slice.call(arguments));
if (newArgs.length < length) {
return curry.call(this, fn, newArgs);
} else {
return fn.apply(this, newArgs);
}
}
}

//需要被柯里化的函数
function multiFn(a, b, c) {
return a * b * c;
}

//multi是柯里化之后的函数
var multi = curry(multiFn);
console.log(multi(2)(3)(4));
console.log(multi(2, 3, 4));
console.log(multi(2)(3, 4));
console.log(multi(2, 3)(4));
//转载自[Javascript高级篇之函数柯里化 - 掘金 (juejin.cn)](https://juejin.cn/post/7111902909796712455)


Function.prototype


四个方法:apply,bind,call,toString


Function.prototype.bind()


定义


bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。(MDN)


const module = {
x: 42,
getX: function () {
return this.x;
},
};

const unboundGetX = module.getX;
console.log(unboundGetX()); // The function gets invoked at the global scope
// Expected output: undefined

const boundGetX = unboundGetX.bind(module);
console.log(boundGetX());
// Expected output: 42


Function.prototype.apply()


function 的实例的apply()方法会以给定的this值和作为数组(或类数组对象)提供arguments调用该函数


const numbers = [5, 6, 2, 3, 7];
const max = Math.max.apply(null, numbers);
console.log(max);
// Expected output: 7
const min = Math.min.apply(null, numbers);
console.log(min);
// Expected output: 2

语法


apply(thisArg) apply(thisArg, argsArray)


参数


//argsArray --- 用于指定调用func时的参数,或者如果不需要向函数提供参数,则为null或undefined

//thisArg --- 调用func 时提供的this值,如果函数不处于严格对象,则null 和undefined 会被替换成全局对象,原始值会被转换成对象

返回值


使用指定的this值和参数调用函数的结果


Function.prototype.call()


Function 实例的 call() 方法会以给定的 this 值和逐个提供的参数调用该函数


//示例
function Product(name, price) {
this.name = name;
this.price = price;
}

function Food(name, price) {
Product.call(this, name, price);
this.category = 'food';
}

console.log(new Food('cheese', 5).name);
// Expected output: "cheese"

//语法
call(thisArg)
call(thisArg, arg1)
call(thisArg, arg1, arg2)
call(thisArg, arg1, arg2, /* …, */ argN)


Function.prototype.toString()


Function 实例的 toString() 方法返回一个表示该函数源码的字符串。


function sum(a, b) {
return a + b;
}

console.log(sum.toString());
// Expected output: "function sum(a, b) {
// return a + b;
// }"

console.log(Math.abs.toString());
// Expected output: "function abs() { [native code] }"

放一个JavaScript的链接


转载自:http://www.cnblogs.com/wangdan0915…


作者:有玺鹤呖
来源:juejin.cn/post/7288962917399035956
收起阅读 »

你写的 CSS 太过冗余,以至于我对它下手了!

web
:is() 你是否曾经写过下方这样冗余的CSS选择器: .active a, .active button, .active label { color: steelblue; } 其实上面这段代码可以这样写: .active :is(a, button...
继续阅读 »

:is()


你是否曾经写过下方这样冗余的CSS选择器:


.active a,
.active button,
.active label {
color: steelblue;
}

其实上面这段代码可以这样写:


.active :is(a, button, label) {
color: steelblue;
}

看~是不是简洁了很多!


是的,你可以使用 :is() 对选择器的任何部分进行分组,例如,你可以对如下代码:


.section h2,
.aside h2,
.nav h2 {
color: steelblue;
}

进行转换:


:is(.section, .aside, .nav) h2 {
color: steelblue;
}

但是 :is() 不仅对父选择器和子选择器有用,它也可以选择多个相邻的选择器,比如:


button:is(:focus, :hover, :active) {
color: steelblue;
}

button:is(.active, .pressed) {
color: lightsteelblue;
}

上述代码等价于:


button:focus, button:hover, button:active {
color: steelblue;
}

button.active, button.pressed {
color: lightsteelblue;
}

:where()


:where() 是一个与 :is() 非常相似的伪类,也值得注意。它们看起来非常相似:


:where(.section, .aside, .nav) h2 {
color: steelblue;
}

但区别在于 :where 的权重为 0,而:is() 总是会采用列表中最特高的选择器的权重。例如,你知道下面的 CSS 代码中的按钮是什么颜色吗?


:is(html) button {
color: red;
}

:where(html) button {
color: blue;
}

在上面的例子中,虽然以 :where() 开头的块在以 :is() 开头的块下面,但 :is() 块具有更高的权重


:has()


一个相关但非常不同的伪类是:has():has() 允许选择包含匹配选择器(或选择器集)的子元素的父元素


:has() 的一个示例是不显示下划线的情况下包含图像或视频的链接:


a { text-decoration: underline }

/* 链接有下划线,除非它们包含图像或视频 */
a:has(img, video) {
text-decoration: none;
}

现在,如果默认情况下我们的 a 标记有下划线文本,但其中有图像或视频,则任何匹配的锚元素的下划线将被删除。


你也可以结合 :is() 使用:



:is(a, button):has(img, video) {
text-decoration: none;
}

我们还需要预处理器吗?


现在你可能会说“SCSS可以做到这一点!,你甚至可能更喜欢它的语法:


.active {
button, label, a {
color: steelblue;
}
}

说的没错,这很优雅。但是,CSS 似乎现在已经都能获取到我们曾经需要SCSS(或其他预处理器)才能获得的特性。


CSS 变量也是 CSS 本身的另一个不可思议的补充,它回避了一个问题:就是什么时候或者多久你真的需要预处理程序:


.active :is(a, button, label) {
--color: steelblue;
color: var(--steelblue);
}

这并不是说预处理器没有它们的用例和优点。


但我认为在某个时间点上,它们确实是处理任何重要CSS的强制要求,而现在情况不再如此了。


惊喜


我想说的是,CSS的未来仍然是光明的。CSS 工作组正积极致力于直接向CSS中添加嵌套选择器。他们正在积极地在3种可能的语法之间进行选择:


/* 1 */
article {
font-family: avenir;
& aside {
font-size: 1rem;
}
}

/* 2 */
article {
font-family: avenir;
} {
aside {
font-size: 1rem;
}
}

/* 3 */
@nest article {
& {
font-family: avenir;
}
aside {
font-size: 1rem;
}
}

你最喜欢哪一个?


庆幸的是,第 1 种已经被官方采纳!所以我们可能很快就会看到一个非常像 scss 的嵌套语法。


浏览器支持


目前所有主流浏览器都支持 :is():where() 伪类:


image.png
但是,需要注意,我们在这里提到的 :has() 伪类没有相同级别的支持,所以使用 :has() 时要小心:


image.png


作者:编程轨迹
来源:juejin.cn/post/7212079828480016442
收起阅读 »

Token无感知刷新,说说我对解决方案的理解~

web
前言 大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心。 大家设想一下,如果有一个超级大的表单页面,用户好不容易填完了,然后点击提交,这个时候请求接口居然返回401,然后跳转到登录页。。。那用户心里肯定是一万个草泥马...
继续阅读 »

前言


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



大家设想一下,如果有一个超级大的表单页面,用户好不容易填完了,然后点击提交,这个时候请求接口居然返回401,然后跳转到登录页。。。那用户心里肯定是一万个草泥马~~~


所以项目里实现token无感知刷新是很有必要的~



这几天在项目中实践了一套token无感知刷新的方案,其实也有看了一下网上那些解决方案,也知道这类的方案已经烂大街了,但是感觉不太符合我想要的效果,主要体现在以下几个方面:



  • 逻辑都写拦截器里,耦合性高,不太好

  • 接口重试的机制做的不太好

  • 接口并发时的逻辑处理做的不太好


我为什么不想要让这套逻辑耦合在拦截器里呢?一方面是因为,我想要写一套代码,在很多的项目里面可以用,把代码侵入性降到最低


另一方面,因为我觉得token无感知刷新涉及到了接口重发,我理解是接口维度的,不应该把这套逻辑放在响应拦截器里去做。。我理解重发之后就是一个独立的新接口请求了,不想让两个独立的接口请求相互有交集~


所以我还是决定自己写一套方案,并分享给大家,希望大家可以提提意见啥的,共同进步~



温馨提示:需要有一些Promise基础



思路


其实大体思路是一样的,只不过实现可能有差别~就是需要有两个 token



  • accessToken:普通 token,时效短

  • refreshToken:刷新 token,时效长


accessToken用来充当接口请求的令牌,当accessToken过期时效的时候,会使用refreshToken去请求后端,重新获取一个有效的accessToken,然后让接口重新发起请求,从而达到用户无感知 token 刷新的效果


具体分为几步:



  • 1、登录时,拿到accessTokenrefreshToken,并存起来

  • 2、请求接口时,带着accessToken去请求

  • 3、如果accessToken过期失效了,后端会返回401

  • 4、401时,前端会使用refreshToken去请求后端再给一个有效的accessToken

  • 5、重新拿到有效的accessToken后,将刚刚的请求重新发起

  • 6、重复1/2/3/4/5



有人会问:那如果refreshToken也过期了呢?


好问题,如果refreshToken也过期了,那就真的过期了,就只能乖乖跳转到登录页了~


Nodejs 模拟 token


为了方便给大家演示,我用 express 模拟了后端的 token 缓存与获取,代码如下图(文末有完整代码)由于这里只是演示作用,所以我设置了



  • accessToken:10秒失效

  • refreshToken:30秒失效



前端模拟请求


先创建一个constants.ts来储存一些常量(文末有完整源码)



接着我们需要对axios进行简单封装,并且模拟:



  • 模拟登录之后获取双 token 并存储

  • 模拟10s后accessToken失效了

  • 模拟30s后refreshToken失效了



理想状态下,用户无感知的话,那么控制台应该会打印


test-1
test-2
test-3
test-4

打印test-1、test-2比较好理解


打印test-3、test-4是因为虽然accessToken失效了,但我用refreshToken去重新获取有效的accessToken,然后重新发起3、4的请求,所以会照常打印test-3、test-4


不会打印test-5、test-6是因为此时refreshToken已经过期了,所以这个时候双token都过期了,任何请求都不会成功了~


但是我们看到现状是,只打印了test-1、test-2




不急,我们接下来就实现token无感知刷新这个功能~


实现


我的期望是封装一个class,这个类提供了以下几个功能:



  • 1、能带着refreshToken去获取新accessToken

  • 2、不跟axios拦截器耦合

  • 3、当获取到新accessToken时,可以重新发起刚刚失败了的请求,无缝衔接,达到无感知的效果

  • 4、当有多个请求并发时,要做好拦截,不要让多次去获取accessToken


针对这几点我做了以下这些事情:



  • 1、类提供一个方法,可以发起请求,带着refreshToken去获取新accessToken

  • 2、提供一个wrapper高阶函数,对每一个请求进行额外处理

  • 3/4、维护一个promise,这个promise只有在请求到新accessToken时才会fulfilled


并且这个类还需要支持配置化,能传入以下参数:



  • baseUrl:基础url

  • url:请求新accessToken的url

  • getRefreshToken:获取refreshToken的函数

  • unauthorizedCode:无权限的状态码,默认 401

  • onSuccess:获取新accessToken成功后的回调

  • onError:获取新accessToken失败后的回调


以下是代码(文末有完整源码)



使用示例如下



最后实现了最终效果,打印出了这四个文本




完整代码


constants.ts


// constants.ts

// localStorage 存储的 key
export const LOCAL_ACCESS_KEY = 'access_token';
export const LOCAL_REFRESH_KEY = 'refresh_token';

// 请求的baseUrl
export const BASE_URL = 'http://localhost:8888';
// 路径
export const LOGIN_URL = '/login';
export const TEST_URL = '/test';
export const FETCH_TOKEN_URL = '/token';


retry.ts


// retry.ts

import { Axios } from 'axios';

export class AxiosRetry {
// 维护一个promise
private fetchNewTokenPromise: Promise<any> | null = null;

// 一些必须的配置
private baseUrl: string;
private url: string;
private getRefreshToken: () => string | null;
private unauthorizedCode: string | number;
private onSuccess: (res: any) => any;
private onError: () => any;

constructor({
baseUrl,
url,
getRefreshToken,
unauthorizedCode = 401,
onSuccess,
onError,
}: {
baseUrl: string;
url: string;
getRefreshToken: () => string | null;
unauthorizedCode?: number | string;
onSuccess: (res: any) => any;
onError: () => any;
}
) {
this.baseUrl = baseUrl;
this.url = url;
this.getRefreshToken = getRefreshToken;
this.unauthorizedCode = unauthorizedCode;
this.onSuccess = onSuccess;
this.onError = onError;
}

requestWrapper<T>(request: () => Promise<T>): Promise<T> {
return new Promise((resolve, reject) => {
// 先把请求函数保存下来
const requestFn = request;
return request()
.then(resolve)
.catch(err => {
if (err?.status === this.unauthorizedCode && !(err?.config?.url === this.url)) {
if (!this.fetchNewTokenPromise) {
this.fetchNewTokenPromise = this.fetchNewToken();
}
this.fetchNewTokenPromise
.then(() => {
// 获取token成功后,重新执行请求
requestFn().then(resolve).catch(reject);
})
.finally(() => {
// 置空
this.fetchNewTokenPromise = null;
});
} else {
reject(err);
}
});
});
}

// 获取token的函数
fetchNewToken() {
return new Axios({
baseURL: this.baseUrl,
})
.get(this.url, {
headers: {
Authorization: this.getRefreshToken(),
},
})
.then(this.onSuccess)
.catch(() => {
this.onError();
return Promise.reject();
});
}
}


index.ts


import { Axios } from 'axios';
import {
LOCAL_ACCESS_KEY,
LOCAL_REFRESH_KEY,
BASE_URL,
LOGIN_URL,
TEST_URL,
FETCH_TOKEN_URL,
} from './constants';
import { AxiosRetry } from './retry';

const axios = new Axios({
baseURL: 'http://localhost:8888',
});

axios.interceptors.request.use(config => {
const url = config.url;
if (url !== 'login') {
config.headers.Authorization = localStorage.getItem(LOCAL_ACCESS_KEY);
}
return config;
});

axios.interceptors.response.use(res => {
if (res.status !== 200) {
return Promise.reject(res);
}
return JSON.parse(res.data);
});

const axiosRetry = new AxiosRetry({
baseUrl: BASE_URL,
url: FETCH_TOKEN_URL,
unauthorizedCode: 401,
getRefreshToken: () => localStorage.getItem(LOCAL_REFRESH_KEY),
onSuccess: res => {
const accessToken = JSON.parse(res.data).accessToken;
localStorage.setItem(LOCAL_ACCESS_KEY, accessToken);
},
onError: () => {
console.log('refreshToken 过期了,乖乖去登录页');
},
});

const get = (url, options?) => {
return axiosRetry.requestWrapper(() => axios.get(url, options));
};

const post = (url, options?) => {
return axiosRetry.requestWrapper(() => axios.post(url, options));
};

const login = (): any => {
return post(LOGIN_URL);
};
const test = (): any => {
return get(TEST_URL);
};

// 模拟页面函数
const doing = async () => {
// 模拟登录
const loginRes = await login();
localStorage.setItem(LOCAL_ACCESS_KEY, loginRes.accessToken);
localStorage.setItem(LOCAL_REFRESH_KEY, loginRes.refreshToken);

// 模拟10s内请求
test().then(res => console.log(`${res.name}-1`));
test().then(res => console.log(`${res.name}-2`));

// 模拟10s后请求,accessToken失效
setTimeout(() => {
test().then(res => console.log(`${res.name}-3`));
test().then(res => console.log(`${res.name}-4`));
}, 10000);

// 模拟30s后请求,refreshToken失效
setTimeout(() => {
test().then(res => console.log(`${res.name}-5`));
test().then(res => console.log(`${res.name}-6`));
}, 30000);
};

// 执行函数
doing();


结语 & 加学习群 & 摸鱼群


我是林三心



  • 一个待过小型toG型外包公司、大型外包公司、小公司、潜力型创业公司、大公司的作死型前端选手;

  • 一个偏前端的全干工程师;

  • 一个不正经的掘金作者;

  • 一个逗比的B站up主;

  • 一个不帅的小红书博主;

  • 一个喜欢打铁的篮球菜鸟;

  • 一个喜欢历史的乏味少年;

  • 一个喜欢rap的五音不全弱鸡


如果你想一起学习前端,一起摸鱼,一起研究简历优化,一起研究面试进步,一起交流历史音乐篮球rap,可以来俺的摸鱼学习群哈哈,点这个,有7000多名前端小伙伴在等着一起学习哦 --> 摸鱼沸点


image.png


作者:Sunshine_Lin

链接:juejin.cn/post/728169…

来源:稀土掘金

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


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

5分钟回顾webpack的前世今生

web
引言 模块化编程是软件设计的一个重要思想。在JavaScript中,处理模块一直是个问题,由于浏览器只能执行JavaScrip、CSS、HTML 代码,所以模块化的前端代码必须进行转换后才能运行。例如 CommonJS 或 AMD,甚至是ECMA 提出的 Ja...
继续阅读 »

引言


模块化编程是软件设计的一个重要思想。在JavaScript中,处理模块一直是个问题,由于浏览器只能执行JavaScrip、CSS、HTML 代码,所以模块化的前端代码必须进行转换后才能运行。例如 CommonJS 或 AMD,甚至是ECMA 提出的 JavaScript 模块化规范——ES6 模块,这些模块系统要么是在浏览器无法运行,要么是无法被浏览器识别和加载,所以针对不同的模块系统,就需要使用专门的工具将源代码转换成浏览器能执行的代码。


整个转化过程被称为构建,构建过程就是“模块捆绑器”或“模块加载器”发挥作用的地方。


Webpack是JavaScript模块捆绑器。在Webpack之前,已经有针对各类型的代码进行编译和构建的流程,例如使用Browserify对CommonJS模块进行编译和打包,然后将打包的资源通过HTML去加载;或者通过gulp进行任务组排来完成整个前端自动化构建。


但是这些方式的缺点是构建环节脱离,编译、打包以及各类资源的任务都分离开。


Webpack模块系统的出现,能将应用程序的所有资源(例如JavaScript、CSS、HTML、图像等)作为模块进行管理,并将它们打包成一个或多个文件并进行优化。Webpack的强大和灵活性使得其能够处理复杂的依赖关系和资源管理,已经成为了构建工具中的首选。


本文主要来扒一扒Webpack的发展进阶史,一起来看看Webpack是如何逐渐从一个简单的模块打包工具,发展成一个全面的前端构建工具和生态系统。


webpack发展历程


webpack从2012年9月发布第一个大版本至2020年10月一共诞生了5个大的版本,我们从下面一张图可以清晰具体地看到每一个版本的主要变化

Webpack发展史.png

Webpack 版本变化方向



  1. Webpack 1:在此之前多是用gulp对各个类型的编译任务进行编排,最后在Html文件中将各种资源引用进来,而Webpack的初始版本横空出世,凭借如下其功能、理念、内核等优点成为众多前端构建工具的最新选择。



  • 理念:一切皆资源,在代码中就能能对Html、Js、Css、图片、文本、JSON等各类资源进行模块化处理。

  • 内核:实现了独有的模块加载机制,引入了模块化打包和代码分割的概念。

  • 功能:集合了编译、打包、代码优化、性能改进等以前各类单一工具的功能,成为前端构建工具标准选择。

  • 特点:通过配置即可完成前端构建任务,同时支持开发者自定义LoaderPlugin对Webpack的生态进行更多的扩展。



  1. Webpack 2: Webpack 2的在第一个版本后足足过了4年,其重点在于满足更多的打包需求以及少量对打包产物的优化



  • 引入对ES6模块的本地支持。

  • 引入import语法,支持按需加载模块。

  • 支持Tree Shaking(无用代码消除)。



  1. Webpack 3:Webpack 3提供了一些优化打包速度的配置,同时对打包体积的优化再次精益求精



  • 引入Scope Hoisting(作用域提升),用于减小打包文件体积。

  • 引入module.noParse选项,用于跳过不需要解析的模块。



  1. Webpack 4:Webpack 4带来了显著的性能提升,同时侧重于用户体验,倡导开箱即用



  • 引入了mode选项,用于配置开发模式或生成模式,减少用户的配置成本,开箱即用

  • 内置Web Workers支持,以提高性能



  1. Webpack 5:Webpack 5继续在构建性能和构建输出上进行了改进,且带来跨应用运行时模块共享的方案



  • 支持WebAssembly模块,使前端能够更高效地执行计算密集型任务。

  • 引入了文件系统持久缓存,提高构建速度

  • 引入Module Federation(模块联邦),允许多个Webpack应用共享模块


webpack打包后的代码分析


为了更方便理解后续章节,我们先看一下Webpack打包后的代码长什么样(为了方便理解,这里以低版本Webpack为例,且不做过多描述)


jsx
复制代码
/******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};

/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/ /* 省略 */
/******/ }

/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;

/******/ // expose the module cache
/******/ __webpack_require__.c = installedModules;

/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "";

/******/ // Load entry module and return exports
/******/ return __webpack_require__(0);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {/*省略*/})
/* 1 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {/*省略*/})
/* 2 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {/*省略*/})
/******/ ]);

可以看到其实入口文件就是一个IIFE(立即执行函数),在这个IIFE里核心包括两块:



  1. 模块系统:Webpack 在IIFE里实现了模块系统所需要的Module、Require、export等方法组织代码。每个模块都被包装在一个函数内,这个函数形成了一个闭包,模块的作用域在这个闭包内。

  2. 模块闭包IIFE的入参即是Modules,它是一个数组,数组的每一项则是一个模块,每个模块都有自己的作用域。模块和模块之间通过Webpack的模块系统可以进行引用。


webpack的发展长河中,笑到最后和沦为历史

笑到最后:OccurrenceOrderPlugin



有趣的是该插件在Webpack 1叫做OccurenceOrderPluginWebpack 2才更名为OccurrenceOrderPluginWebpack 3则不需要手动配置该插件了。


插件作用:用于优化模块的顺序,以减小输出文件的体积。其原理基于模块的使用频率,将最常用的模块排在前面,以便更好地利用浏览器的缓存机制。


有了前面对于Webpack打包后的代码分析,OcurrenceOrderPlugin的优化效果也就很好理解了。它的原理主要基于两个概念:模块的使用频率模块的ID



  1. 模块的使用频率:OccurrenceOrderPlugin 插件会分析在编译过程中每个模块的出现次数。这个出现次数是指模块在其他模块中被引用的次数。插件会统计模块的出现次数,通常情况下,被引用次数更多的模块将被认为更重要,因此会更早地被加载和执行。

  2. 模块的 ID:Webpack 使用数字作为模块的 ID,OccurrenceOrderPlugin 插件会根据模块的出现次数,为每个模块分配一个优化的 ID。这些 ID 的分配是按照出现次数从高到低的顺序进行的,以便出现次数较多的模块获得较短的 ID,这可以减小生成的 JavaScript 文件的大小。假设一共有100个模块,最高的频率为被引用100次,则减小文件体积200B。(确实好像作用很小,但是作为最贴近用户体验的前端er,不应该是追求精益求精嘛)


这个插件的主要目标是减小 JavaScript 文件的体积,并提高加载性能,因为浏览器通常更倾向于缓存较小的文件。通过将频繁使用的模块分配到较短的 ID,可以减小输出文件的体积,并提高缓存的效率。


笑到最后:Scope Hoisting


过去 Webpack 打包时的一个取舍是将 bundle 中各个模块单独打包成闭包。这些打包函数使你的 JavaScript 在浏览器中处理的更慢。相比之下,一些工具像 Closure Compiler 和 RollupJS 可以提升(hoist)或者预编译所有模块到一个闭包中,提升你的代码在浏览器中的执行速度。


而Scope Hoisting 就是实现以上的预编译功能,通过静态分析代码,确定哪些模块之间的依赖关系,然后将这些模块合并到一个函数作用域中。这样,多个模块之间的函数调用关系被转化为更紧凑的代码,减少了函数调用的开销。这样不仅减小了代码体积,同时也提升了运行时性能。


Scope Hoisting 的原理是在 Webpack 的编译过程中自动进行的,开发人员无需手动干预。要启用 Scope Hoisting,你可以使用 Webpack 4 版本中引入的 moduleConcatenation 插件。在 Webpack 5 及更高版本中,Scope Hoisting 是默认启用的,不需要额外的配置。


CommonsChunkPlugin的作用和不足,为何会被optimization.splitChunks所取代


CommonsChunkPlugin 插件,是一个可选的用于建立一个独立chunk的功能,这个文件包括多个入口 chunk 的公共模块。主要配置项包含


json
复制代码
{
name: string, // or
names: string[],
filename: string,
minChunks: number|Infinity|function(module, count) => boolean,
chunks: string[],
// 通过 chunk name 去选择 chunks 的来源。chunk 必须是 公共chunk 的子模块。
// 如果被忽略,所有的,所有的 入口chunk (entry chunk) 都会被选择。

children: boolean,
deepChildren: boolean,
}

通过上面的配置项可以看到虽然CommonsChunkPlugin将一些重复的模块传入到一个公共的chunk,以减少重复加载的情况,尤其是将第三方库提取到一个单独的文件中,但是其首要依赖是通过Entry Chunk进行的。在Webpack4以及更高的版本当中被optimization.splitChunks所替代,其提供了配置让webpack根据策略来自动进行拆分,被替代的原因主要有以下几点:



  1. 灵活度不足:在配置上相对固定,只能将指定 Entry Chunk的共享模块提取到一个单独的chunk中,可能无法满足复杂的代码拆分需求。

  2. 配置复杂:需要手动指定要提取的模块和插件的顺序,配置起来相对复杂,开发者需要约定好哪些chunk可以被传入,有较高的心智负担。而optimization.splitChunks只需要配置好策略就能够帮你自动拆分。


因此在Webpack 4这个配置和开箱即用的版本里,它自然也就“香消玉损”。只能遗憾地看到一句:


the CommonsChunkPlugin 已经从 Webpack v4 legato 中移除。想要了解在最新版本中如何处理 chunk,请查看 SplitChunksPlugin


被移除的DedupePlugin


这是 Webpack 1.x 版本中的插件,用于在打包过程中去除重复的模块(deduplication),其原理不知道是通过内容hash,还是依赖调用关系图。但是在Webpack 2中引入了Tree Shaking功能,则不再需要了。原因有以下几点:



  • Tree Shaking控制更精确:能通过静态分析来判断哪些代码是不需要的,实现了更细力度的优化。

  • Scope Hositing减少了重复模块:Webpack 3引入了Scope Hositing,将模块包裹在函数闭包中,进一步减少了重复模块的依赖


因此我们在Webpack的文档中看到:



DedupePlugin has been removed


不再需要 Webpack.optimize.DedupePlugin。请从配置中移除。



总结


或许有些插件你已经看不到它的身影,有些特性早已被webpack内置其中。webpack从第一个版本诞生后一直致力于以下几个方面的提升:



  1. 性能优化:通过去除重复代码、作用域提升、压缩等方式减少代码体积和提高运行时性能。

  2. 构建提效:通过增量编译、缓存机制、并行处理等提升打包速度。

  3. 配置简化:通过内置必要的特性和插件以及简化配置提升易用性。


作者:古茗前端团队
来源:juejin.cn/post/7289718324858355769
收起阅读 »

”调试小技巧,让未来更美好“

web
① 自动打断点(抛异常时自动断点) 偶然一次可能不小心打开某个设置选项,可能设置了英文又不知道是打开了什么,只知道当每次打开F11打开控制台调试看数据的时候,就是不会自动停在某个位置,又不知道怎么停掉,怀疑会不会是安装了什么谷歌插件或者是油猴哪个脚本代码写错写...
继续阅读 »

自动打断点(抛异常时自动断点)


偶然一次可能不小心打开某个设置选项,可能设置了英文又不知道是打开了什么,只知道当每次打开F11打开控制台调试看数据的时候,就是不会自动停在某个位置,又不知道怎么停掉,怀疑会不会是安装了什么谷歌插件或者是油猴哪个脚本代码写错写了什么。


不小心打钩了断点调试的遇到未捕获的异常时暂停,或者在遇到异常时暂停这两个选项其中一个。就有可能导致了谷歌的调试器暂停,取决于这个网站有没有一些异常触发到这一点,勾选上每次异常浏览器会帮我们打断点。


image.png


所以解决办法就是把谷歌浏览器中的这两个勾去掉,如果不是你本意打开想要调试网站中一些异常的报错。


image.png


一键重发请求(不用每次重新请求就刷新页面)


排查接口的时候,需要重新请求该接口,不必要每次重新刷新页面去请求试接口里面传参对不对返回来的数据对不对。重发请求很简单,右击该接口重发xhr即可。


image.png


image.png


③ 断点调试+debugger+console+try...catch


(1) console.log


找bug解决bug是很重要滴。console.log-输出某个变量值是非常非常常用的,只要做码农一定得会各种语言的输出消息和变量的语句,方便我们查看和调试。


(2) debugger(不用每次都console)


在代码某个语句后面或者前面输debugger


在我入行到在学校生涯那段时间都不知道debugger;这玩意,有一次项目有一个比较棘手不知道怎么解决的问题,甲方公司项目负责人开会重点讲了那个问题,就见他这里输一下dubugger,那里输一个debugger,当时就觉得那玩意很神(反正意识上只要我们不懂的东西刚开始接触都是这样,这里神那里神的,接触久了就觉的也就那样不过如此,很平常),最后也没看出什么来。


debugger就是在某个状态下,用这个debugger;语句在那里断一下点,然后当下,上下文的状态和值都可以在查看,哪个分支导致变量状态错误。


使用debugger可以查看:



  • 作用域变量

  • 函数参数

  • 函数调用堆栈

  • 代码整个执行过程(从哪一句到哪一句的)

  • 如果是异步promise async...await 等这种的话就需要在then和catch里面debugger去调试


(3) try...catch 捕获异常


try {
// 可能会抛出异常的代码
} catch {
// 处理所有异常的代码
}

try...catch捕获异常,包括运行时错误和自定义以及语法错误。


try...catch中还可以在某些情况下用throw在代码中主动抛出异常。


try {
// 可能会抛出异常的代码

if (某某情况下) throw '某某错误提示信息'

} catch {
// 处理所有异常的代码
} finally {
// 结束处理用于清理操作
}

image.png


④ 联调查看接口数据


image.png


如上图这个接口,如果想要复制接口preview里面的数据,


除了去Responese里面去找我们需要的某个值去选择复制之外(这个有个缺点就是要找值不直观),还可以右击某个值,然后通过点击store object as global variable(存储为全局变量) 获取。


image.png


当我们点击了之后,控制台就会出现tempXX这个变量。


image.png


我们就只需要在控制台输入copy(temp3)copy(要复制的json名),在粘贴板上就有这个json数据了。



💡
全局方法copy()在console里copy任何你能拿到的数据资源。



image.png


⑤ 后端接口数据返回json


这个有时候有的同学有可能碰到类似这种的JSON数据{\"name\":\"John\",\"address\":\"123 Main St, City\"}


解决方法


直接打开控制台console,输入 JSON.parse("{"name":"John","address":"123 Main St, City"}"),这样


image.png


如果你想复制下来用,直接跟上面我们用copy这好碰上,赋值加上一个copy就可以了。


image.png


这样这个值就在粘贴板上了。


总结


报错和bug,多多少少会贯穿我们的职业生涯中,如何定位问题、解决问题,加以调试,是我们必须也是不能不必备的技能。


当你捕获bug的时候.gif



☎️ 希望对大家有所帮助,如有错误,望不吝赐教,欢迎评论区留言互相学习。



作者:盏灯
来源:juejin.cn/post/7288963208396603450
收起阅读 »

谁还没个靠bug才能正常运行的程序😌

web
最近遇到一个问题,计算滚动距离,滚动比例达到某界定值时,显示mask,很常见吧^ _ ^ 这里讲的不是这个需求的实现,是其中遇到了一个比较有意思的bug,靠这个bug才达到了正确效果,以及这个bug是如何暴露的(很重要)。 下面是演示代码和动图 <!DO...
继续阅读 »

最近遇到一个问题,计算滚动距离,滚动比例达到某界定值时,显示mask,很常见吧^ _ ^


这里讲的不是这个需求的实现,是其中遇到了一个比较有意思的bug,靠这个bug才达到了正确效果,以及这个bug是如何暴露的(很重要)。


下面是演示代码和动图


<!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>
.container {
width: 300px;
max-height: 300px;
background-color: black;
position: absolute;
top: 60px;
left: 50%;
transform: translateX(-50%);
overflow-y: auto;
}
.child {
width: 260px;
height: 600px;
margin: 0px 20px;
background-color: pink;
position: relative;
}
.flag {
position: absolute;
width: 100%;
height: 25px;
background-color: blueviolet;
color: aliceblue;
text-align: center;
line-height: 25px;
font-size: 14px;
left: 0;
right: 0;
}
.top {
top: 0;
}
.bottom {
bottom: 0px;
}
</style>
</head>

<body>
<div class="container">
<div class="child">
<div class="flag top">top</div>
<div class="flag bottom">bottom</div>
</div>
</div>
</body>
</html>


20230927105849_rec_.gif




开始计算啦,公式:滚动比例 = 滚动距离 / 可滚动距离


滚动距离: $0.scrollTop


可滚动距离: $0.scrollHeight - $0.offsetHeight


即:scrollRatio = scrollTop / (scrollHeight - offsetHeight)


滚动到底部,计算结果是 300 / (600 - 300) = 1


image.png


我们需要拿scrollRatio某界定值(比如0.1)作大小的比较,计算是true还是false(用isShow = scrollRatio < 某界定值来保存)。


这里一切正常。




不正常的情况出现了


就是没有出现滚动条的情况,即.child的高度没有超过.container的高度时,把.child的高度设成.containermax-height,就没有滚动条了(下面讲的情景也都是没有滚动条的情况)。


image.png


这个时候再去计算,得到了NaN,以至于 NaN < 0.1 = false


image.png


因为isShow的预期就是false,所以一直都没有发现这个bug。




那么它是如何暴露的呢?


后来新的需求给.container加了border。演示一下加border,然后再去计算:


image.png


发现没,这时候$0.offsetHeight的高度把border的高度也算进去了,结果就成了true,这不是想要的结果 ❌。




然后就是一番查验


offsetHeight是一个元素的总高度,包括可见内容的高度、内边距(padding)、滚动条的高度(如果存在)以及边框(border)的高度。


而我们这里只需要可见的高度,就可以用到另一个属性了clientHeight


clientHeight是指元素的可见内容区域的高度,不包括滚动条的高度和边框的高度。它仅包括元素的内部空间,即内容加上内边距。


image.png


当然这也只是继续使除数为0,然后得到结果为NaN,不过bug已经暴露出来了,后面就是一些其他的优化啦~




总结 + 复习(盒模型 box-sizing)


发现没有,offsetHeightclientHeight的区别,就像盒模型中的标准盒模型怪异盒模型的区别:


box-sizing: content-box(默认,标准盒模型):宽度和高度的计算值都 不包含 内容的边框(border)和内边距(padding)。添加padding和border时, 使整个div的宽高变大。


box-sizing: border-box(怪异盒模型):宽度和高度的计算值都 包含 内容的边框(border)和内边距(padding)。添加padding和border时, 不会 使整个div的宽高变大。


这样讲是不是加深一下对这两种属性的印象


^ - ^


作者:aomyh
来源:juejin.cn/post/7283087306603823116
收起阅读 »