排行榜--实现点击视图自动滚动到当前用户所在位置.
需求
我们今天来实现一下,点击当前用户的div, 自动滚动到用户在排行榜中的位置.
效果
大家可以先看一下下面的GIF, 所实现的效果.
实现
1. 准备DOM 结构
首先,我们在进行列表建设的时候, 需要准备好一个数据. 因为此处我们是使用的vue3
来进行编写. 对于列表我们使用的是v-for列表渲染
来做的. 在渲染的时候, 我们需要给每一个列表项(当前就是每一个用户项
)添加一个自定义属性. 具体的话, 可以看下 下方的关键代码.
核心代码就是
<div v-for="(item, index) in rankingData" :key="item.user.id" :data-key="item.user.id"
</div>
因为数据是后端返回的, 是包含的user_id,而且这个user_id 是不可能重复的. 我们只要保证每个列表的自定义的属性是唯一的即可.
2. 绑定方法,实现方法
接下来,我们需要考虑的是,在点击的时候,如何获取到当前的dom. 这对我们目前来说就很容易了, 因为我们可以根据据user_id
拿到我们当前点击的dom.
添加一个方法
<!-- 当前用户排名情况 -->
<div class="text-white w-[100%] ...." @click="scrollToCurrentRankingPosition(userId)">
实现方法.
第一步: 拿到rankingList的dom实例.
这里我们通过vue3提供ref拿到dom. 可以看下
模板引用
<div v-else class=" overflow-auto bg-white" ref="rankingList">
const rankingList = ref(null);
第二步: 根据userId获取到具体的DOM
const currentItem = rankingList.value.querySelector(`[data-key="${id}"]`);
第三步: 使用scrollIntoView方法滚动视图到当前选中的元素
// 平滑滚动到当前元素
currentItem.scrollIntoView({ behavior: 'smooth', block: 'center' });
scrollIntoView方法 讲解:
Element
接口的scrollIntoView()
方法会滚动元素的父容器,使被调用scrollIntoView()
的元素对用户可见。
简单来讲就是被调用的者的元素出现在用户的视线里面.
scrollIntoView()
方法有三种调用形式:
scrollIntoView()
:无参数调用,元素将滚动到可视区域顶部,如果它是第一个可见元素。scrollIntoView(alignToTop)
:接受一个布尔值参数,决定元素是与滚动区的顶部还是底部对齐。scrollIntoView(scrollIntoViewOptions)
:接受一个对象作为参数,提供了更多的滚动选项。
参数
alignToTop
(可选):布尔值,控制元素滚动到顶部还是底部对齐。默认为true
(顶部对齐)。scrollIntoViewOptions
(可选实验性):对象,包含以下属性:
behavior
:定义滚动行为是平滑动画还是立即发生。可取值有smooth
(平滑动画)、instant
(立即发生)或auto
(由CSS的scroll-behavior
属性决定)。block
:定义垂直方向的对齐方式,可取值有start
、center
、end
或nearest
。默认为start
。inline
:定义水平方向的对齐方式,可取值有start
、center
、end
或nearest
。默认为nearest
。
目前我们实现了效果.
但是我们发现,还可以继续改进, 目前我们虽然滚动到了屏幕的中间, 但是我们很难去发现. 所以我们可以继续完善一下这个方法. 就是滚动到视图的中间的同时高亮选中的DOM.
3. 额外扩展, 高亮当前的元素
定义一个两个方法,一个用于应用样式
, 一个应用于移除样式
.
const applyHighlightStyles = (element) => {
element.style.transition = 'background-color 1s ease, border-color 1s ease';
element.style.border = '1px solid transparent'; // 预定义边框样式
element.style.borderColor = '#006cfe'; // 设置边框颜色
element.style.backgroundColor = '#cfe5ff'; // 设置背景色为浅蓝色
};
const removeHighlightStyles = (element) => {
element.style.backgroundColor = ''; // 移除背景色
element.style.borderColor = 'transparent'; // 移除边框颜色
};
然后再在我们之前的方法的后面加入代码
// 设置高亮显示的样式
applyHighlightStyles(currentItem);
// 清除之前的定时器(如果有)
if (currentItem._highlightTimer) {
clearTimeout(currentItem._highlightTimer);
}
// 设置定时器,2秒后移除高亮显示
currentItem._highlightTimer = setTimeout(() => {
removeHighlightStyles(currentItem);
currentItem._highlightTimer = null;
}, 2000);
然后在组件卸载前记得清除定时器.
onUnmounted(() => {
if (rankingList.value) {
// 遍历所有项目,清除定时器
rankingList.value.querySelectorAll('[data-key]').forEach(item => {
if (item._highlightTimer) {
clearTimeout(item._highlightTimer);
item._highlightTimer = null;
}
});
}
});
效果:
总结
整体下来的思路就是:
- v-for的时候, 给每个循环的元素添加一个自定义的属性.(value:user_id), 不重复且能标识每个元素.
- 点击之后,拿到id,透传给方法,然后通过id获取到当前的元素.
- 使用
Element.scrollIntoView()
, 将当前的选中的DOM自动滚动视图的中间. - 高亮显示当前的元素之后(2s)进行取消高亮.
来源:juejin.cn/post/7403576996393910308
用rust写个flutter插件并上传 pub.dev
今天收到一个需求,要求在flutter端把任意类型的图片,转换成bmp类型的图片,然后把 bmp位图发送到条码打印机,生成的 bmp图片是 1 位深度的,只有黑白两种像素颜色
包已经上传到 pub.dev,pub.dev/packages/ld…
效果图
1.生成插件包
crates.io地址: crates.io/crates/frb_…
安装命令
cargo install frb_plugin_tool
使用很简单,输入frb_plugin_tool即可
按照提示输入插件名
创建后的项目目录大概像这样
2. 编写 rust代码
我这里图片转 bmp工具用的是rust image这个包
添加依赖
cd rust && cargo add image
然后在 src/api
目录下添加image.rs
文件
use std::{io::Cursor, time::Instant};
use bytesize::ByteSize;
use humantime::format_duration;
use image::{GrayImage, Luma};
use indicatif::ProgressBar;
use log::debug;
use super::entitys::{LddImageType, ResizeOpt};
///任意图像转 1 位深度的数据
pub fn convert_to_1bit_bmp(
input_data: &[u8],
image_type: LddImageType,
resize: Option<ResizeOpt>,
is_apply_ordered_dithering: Option<bool>,
) -> Vec<u8> {
let use_ordered_dithering = is_apply_ordered_dithering.map_or(false, |v| v);
let start = Instant::now();
debug!("开始转换,数据大小:{:?}", ByteSize(input_data.len() as u64));
let mut img =
image::load(Cursor::new(input_data), image_type.int0()).expect("Failed to load image");
if let Some(size) = resize {
debug!("开始格式化尺寸:{:?}", size);
img = img.resize(size.width, size.height, size.filter.int0());
debug!("✅格式化尺寸完成");
}
let mut gray_img = img.to_luma8(); // 转换为灰度图像
if use_ordered_dithering {
debug!("✅使用 h4x4a 抖动算法");
gray_img = apply_ordered_dithering(&gray_img);
}
let (width, height) = gray_img.dimensions();
let row_size = ((width + 31) / 32) * 4; // 每行字节数 4 字节对齐
let mut bmp_data = vec![0u8; row_size as usize * height as usize];
// 创建进度条
let progress_bar = ProgressBar::new(height as u64);
// 二值化处理并填充 BMP 数据(1 位深度)
let threshold = 128;
for y in 0..height {
let inverted_y = height - 1 - y; // 倒置行顺序
for x in 0..width {
let pixel = gray_img.get_pixel(x, y)[0];
if pixel >= threshold {
bmp_data[inverted_y as usize * (row_size as usize) + (x / 8) as usize] |=
1 << (7 - (x % 8));
}
}
progress_bar.inc(1); // 每处理一行,进度条增加一格
}
progress_bar.finish_with_message("Conversion complete!");
// BMP 文件头和 DIB 信息头
let file_size = 14 + 40 + 8 + bmp_data.len(); // 文件头 + DIB 头 + 调色板 + 位图数据
let bmp_header = vec![
0x42,
0x4D, // "BM"
(file_size & 0xFF) as u8,
((file_size >> 8) & 0xFF) as u8,
((file_size >> 16) & 0xFF) as u8,
((file_size >> 24) & 0xFF) as u8,
0x00,
0x00, // 保留字段
0x00,
0x00, // 保留字段
54 + 8,
0x00,
0x00,
0x00, // 数据偏移(54 字节 + 调色板大小)
];
let dib_header = vec![
40,
0x00,
0x00,
0x00, // DIB 头大小(40 字节)
(width & 0xFF) as u8,
((width >> 8) & 0xFF) as u8,
((width >> 16) & 0xFF) as u8,
((width >> 24) & 0xFF) as u8,
(height & 0xFF) as u8,
((height >> 8) & 0xFF) as u8,
((height >> 16) & 0xFF) as u8,
((height >> 24) & 0xFF) as u8,
1,
0x00, // 颜色平面数
1,
0x00, // 位深度(1 位)
0x00,
0x00,
0x00,
0x00, // 无压缩
0x00,
0x00,
0x00,
0x00, // 图像大小(可为 0,表示无压缩)
0x13,
0x0B,
0x00,
0x00, // 水平分辨率(2835 像素/米)
0x13,
0x0B,
0x00,
0x00, // 垂直分辨率(2835 像素/米)
0x02,
0x00,
0x00,
0x00, // 调色板颜色数(2)
0x00,
0x00,
0x00,
0x00, // 重要颜色数(0 表示所有颜色都重要)
];
// 调色板(黑白)
let palette = vec![
0x00, 0x00, 0x00, 0x00, // 黑色
0xFF, 0xFF, 0xFF, 0x00, // 白色
];
// 将所有部分组合成 BMP 文件数据
let mut bmp_file_data = Vec::with_capacity(file_size);
bmp_file_data.extend(bmp_header);
bmp_file_data.extend(dib_header);
bmp_file_data.extend(palette);
bmp_file_data.extend(bmp_data);
let duration = start.elapsed(); // 计算耗时
debug!(
"✅转换完成,数据大小:{:?},耗时:{}",
ByteSize(bmp_file_data.len() as u64),
format_duration(duration)
);
bmp_file_data
}
// 有序抖动矩阵(4x4 Bayer 矩阵)
const DITHER_MATRIX: [[f32; 4]; 4] = [
[0.0, 8.0, 2.0, 10.0],
[12.0, 4.0, 14.0, 6.0],
[3.0, 11.0, 1.0, 9.0],
[15.0, 7.0, 13.0, 5.0],
];
//h4x4a 抖动算法
fn apply_ordered_dithering(image: &GrayImage) -> GrayImage {
let (width, height) = image.dimensions();
let mut dithered_image = GrayImage::new(width, height);
for y in 0..height {
for x in 0..width {
let pixel = image.get_pixel(x, y)[0];
let threshold = DITHER_MATRIX[(y % 4) as usize][(x % 4) as usize] / 16.0 * 255.0;
let new_pixel_value = if pixel as f32 > threshold { 255 } else { 0 };
dithered_image.put_pixel(x, y, Luma([new_pixel_value]));
}
}
dithered_image
}
生成 dart代码,在项目根目录下执行
flutter_rust_bridge_codegen generate
会在dart lib下生成对应的文件
在项目中使用
编写 example , main.dart
.
import 'dart:io';
import 'dart:typed_data';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:ldd_bmp/api/entitys.dart';
import 'package:ldd_bmp/api/image.dart';
import 'package:ldd_bmp/ldd_bmp.dart';
import 'dart:async';
import 'package:path_provider/path_provider.dart';
const reSize = ResizeOpt(
width: 200,
height: 200,
filter: LddFilterType.nearest,
);
Future<void> main() async {
await bmpSdkInit();
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
File? file;
Uint8List? bmpData;
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Native Packages'),
),
body: SingleChildScrollView(
child: Column(
children: [
FilledButton(onPressed: selectFile, child: const Text('选择文件')),
if (file != null)
Image.file(
file!,
width: 200,
height: 200,
),
ElevatedButton(
onPressed: file == null
? null
: () async {
final bts = await file!.readAsBytes();
bmpData = await convertTo1BitBmp(
inputData: bts,
imageType: LddImageType.jpeg,
isApplyOrderedDithering: true,
resize: const ResizeOpt(
width: 200,
height: 200,
filter: LddFilterType.nearest,
));
setState(() {});
},
child: const Text("转换")),
ElevatedButton(
onPressed: bmpData == null
? null
: () {
saveImageToFile(bmpData!);
},
child: const Text("保存图片"))
],
),
),
floatingActionButton: bmpData != null
? ConstrainedBox(
constraints:
const BoxConstraints(maxHeight: 300, maxWidth: 300),
child: Card(
elevation: 10,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
const Text('转换结果'),
Image.memory(bmpData!),
],
),
),
),
)
: null,
),
);
}
Future<void> selectFile() async {
setState(() {
file = null;
});
FilePickerResult? result = await FilePicker.platform.pickFiles();
if (result != null) {
file = File(result.files.single.path!);
setState(() {});
} else {
// User canceled the picker
}
}
}
Future<void> saveImageToFile(Uint8List imageData) async {
// 获取应用程序的文档目录
final directory = await getApplicationDocumentsDirectory();
// 设置文件路径和文件名
final filePath = '${directory.path}/image.bmp';
// 创建一个文件对象
final file = File(filePath);
// 将Uint8List数据写入文件
await file.writeAsBytes(imageData);
print('Image saved to $filePath');
}
转换速度还是挺快的,运行效果
上传到 pub.dev
这个包已经上传到仓库了,可以直接使用
pub.dev/packages/ld…
来源:juejin.cn/post/7412486655862734874
开箱即用的web打印和下载,自动分页不截断
哈喽哈喽🌈
哈喽大家好!我是小周🤪🤪🤪。相信各位前端小伙伴都知道可以用window.print()
这个方法来调用打印机实现打印功能,但是直接下载功能window.print()
还是无法实现的。今天我来介绍另外一种实现方式,真正的开箱即用,既可以实现打印和直接下载,也可以防止内容截断。
技术栈
1、html2canvas
html2canvas
一个可以将html转换成canvas的三方库
2、jsPDF
jsPDF
生成pdf文件的三方库
一些用到的方法介绍
1、canvas.getContext('2d').getImageData()
canvas.getContext('2d').getImageData()
是 HTML5 Canvas API 中用于获取指定区域的像素数据的方法。它返回一个 ImageData
对象,该对象包含了指定区域中每个像素的颜色和透明度信息。
canvas.getContext('2d').getImageData(x, y, width, height)
参数说明:
x
: 采集的图像区域的左上角的水平坐标。y
: 采集的图像区域的左上角的垂直坐标。width
: 采集的图像区域的宽度(以像素为单位)。height
: 采集的图像区域的高度(以像素为单位)。
返回值:
返回一个 ImageData
对象,包含以下属性:
width
: 图像数据的宽度。height
: 图像数据的高度。data
: 一个Uint8ClampedArray
类型的数组,存储了区域中每个像素的颜色和透明度信息。每个像素占用 4 个元素,分别对应:
data[n]
: 红色通道的值(0 到 255)data[n+1]
: 绿色通道的值(0 到 255)data[n+2]
: 蓝色通道的值(0 到 255)data[n+3]
: 透明度(alpha)通道的值(0 到 255),255 表示完全不透明,0 表示完全透明。
代码实现
1、设置 DomToPdf 类
export class DomToPdf {
_rootDom = null
_title = 'pdf' //生成的pdf标题
_a4Width = 595.266
_a4Height = 841.89
_pageBackground = 'rgba(255,255,255)' //页面背景色
_hex = [0xff, 0xff, 0xff] //用于检测分页的颜色标识
//初始化状态
constructor(rootDom, title, color = [255, 255, 255]) {
this._rootDom = rootDom
this._title = title
this._pageBackground = `rgb(${color[0]},${color[1]},${color[2]})`
this._hex = color
}
}
2、设置 pdf的生成函数
async savePdf() {
const a4Width = this._a4Width
const a4Height = this._a4Height
const hex = this._hex
return new Promise(async (resolve, reject) => {
try {
const canvas = await html2canvas(this._rootDom, {
useCORS: true,
allowTaint: true,
scale: 0.8,
backgroundColor: this._pageBackground,
})
const pdf = new jsPDF('p', 'pt', 'a4')
let index = 1,
canvas1 = document.createElement('canvas'),
height
let leftHeight = canvas.height
let a4HeightRef = Math.floor((canvas.width / a4Width) * a4Height)
let position = 0
let pageData = canvas.toDataURL('image/jpeg', 0.7)
pdf.setDisplayMode('fullwidth', 'continuous', 'FullScreen')
function createImpl(canvas) {
if (leftHeight > 0) {
index++
let checkCount = 0
if (leftHeight > a4HeightRef) {
let i = position + a4HeightRef
for (i = position + a4HeightRef; i >= position; i--) {
let isWrite = true
for (let j = 0; j < canvas.width; j++) {
let c = canvas.getContext('2d').getImageData(j, i, 1, 1).data
if (c[0] !== hex[0] || c[1] !== hex[1] || c[2] !== hex[2]) {
isWrite = false
break
}
}
if (isWrite) {
checkCount++
if (checkCount >= 10) {
break
}
} else {
checkCount = 0
}
}
height =
Math.round(i - position) || Math.min(leftHeight, a4HeightRef)
if (height <= 0) {
height = a4HeightRef
}
} else {
height = leftHeight
}
canvas1.width = canvas.width
canvas1.height = height
let ctx = canvas1.getContext('2d')
ctx.drawImage(
canvas,
0,
position,
canvas.width,
height,
0,
0,
canvas.width,
height,
)
if (position !== 0) {
pdf.addPage()
}
pdf.setFillColor(hex[0], hex[1], hex[2])
pdf.rect(0, 0, a4Width, a4Height, 'F')
pdf.addImage(
canvas1.toDataURL('image/jpeg', 1.0),
'JPEG',
0,
0,
a4Width,
(a4Width / canvas1.width) * height,
)
leftHeight -= height
position += height
if (leftHeight > 0) {
setTimeout(createImpl, 500, canvas)
} else {
resolve(pdf)
}
}
}
if (leftHeight < a4HeightRef) {
pdf.setFillColor(hex[0], hex[1], hex[2])
pdf.rect(0, 0, a4Width, a4Height, 'F')
pdf.addImage(
pageData,
'JPEG',
0,
0,
a4Width,
(a4Width / canvas.width) * leftHeight,
)
resolve(pdf)
} else {
try {
pdf.deletePage(0)
setTimeout(createImpl, 500, canvas)
} catch (err) {
reject(err)
}
}
} catch (error) {
reject(error)
}
})
}
3、设置承接pdf的方法
//直接下载pdf
async downToPdf(setLoadParent) {
const newPdf = await this.savePdf()
const title = this._title
newPdf.save(title + '.pdf')
setLoadParent(false)
}
//通过构造链接的形式去跳转打印页面
async printToPdf(setLoadParent) {
const newPdf = await this.savePdf()
const pdfBlob = newPdf.output('blob')
const pdfUrl = URL.createObjectURL(pdfBlob)
setLoadParent(false)
window.open(pdfUrl)
}
完整代码
import html2canvas from 'html2canvas'
import jsPDF from 'jspdf'
export class DomToPdf {
_rootDom = null
_title = 'pdf'
_a4Width = 595.266
_a4Height = 841.89
_pageBackground = 'rgba(255,255,255)'
_hex = [0xff, 0xff, 0xff]
constructor(rootDom, title, color = [255, 255, 255]) {
this._rootDom = rootDom
this._title = title
this._pageBackground = `rgb(${color[0]},${color[1]},${color[2]})`
this._hex = color
}
async savePdf() {
const a4Width = this._a4Width
const a4Height = this._a4Height
const hex = this._hex
return new Promise(async (resolve, reject) => {
try {
const canvas = await html2canvas(this._rootDom, {
useCORS: true,
allowTaint: true,
scale: 0.8,
backgroundColor: this._pageBackground,
})
const pdf = new jsPDF('p', 'pt', 'a4')
let index = 1,
canvas1 = document.createElement('canvas'),
height
let leftHeight = canvas.height
let a4HeightRef = Math.floor((canvas.width / a4Width) * a4Height)
let position = 0
let pageData = canvas.toDataURL('image/jpeg', 0.7)
pdf.setDisplayMode('fullwidth', 'continuous', 'FullScreen')
function createImpl(canvas) {
if (leftHeight > 0) {
index++
let checkCount = 0
if (leftHeight > a4HeightRef) {
let i = position + a4HeightRef
for (i = position + a4HeightRef; i >= position; i--) {
let isWrite = true
for (let j = 0; j < canvas.width; j++) {
let c = canvas.getContext('2d').getImageData(j, i, 1, 1).data
if (c[0] !== hex[0] || c[1] !== hex[1] || c[2] !== hex[2]) {
isWrite = false
break
}
}
if (isWrite) {
checkCount++
if (checkCount >= 10) {
break
}
} else {
checkCount = 0
}
}
height =
Math.round(i - position) || Math.min(leftHeight, a4HeightRef)
if (height <= 0) {
height = a4HeightRef
}
} else {
height = leftHeight
}
canvas1.width = canvas.width
canvas1.height = height
let ctx = canvas1.getContext('2d')
ctx.drawImage(
canvas,
0,
position,
canvas.width,
height,
0,
0,
canvas.width,
height,
)
if (position !== 0) {
pdf.addPage()
}
pdf.setFillColor(hex[0], hex[1], hex[2])
pdf.rect(0, 0, a4Width, a4Height, 'F')
pdf.addImage(
canvas1.toDataURL('image/jpeg', 1.0),
'JPEG',
0,
0,
a4Width,
(a4Width / canvas1.width) * height,
)
leftHeight -= height
position += height
if (leftHeight > 0) {
setTimeout(createImpl, 500, canvas)
} else {
resolve(pdf)
}
}
}
if (leftHeight < a4HeightRef) {
pdf.setFillColor(hex[0], hex[1], hex[2])
pdf.rect(0, 0, a4Width, a4Height, 'F')
pdf.addImage(
pageData,
'JPEG',
0,
0,
a4Width,
(a4Width / canvas.width) * leftHeight,
)
resolve(pdf)
} else {
try {
pdf.deletePage(0)
setTimeout(createImpl, 500, canvas)
} catch (err) {
reject(err)
}
}
} catch (error) {
reject(error)
}
})
}
async downToPdf(setLoadParent) {
const newPdf = await this.savePdf()
const title = this._title
newPdf.save(title + '.pdf')
setLoadParent(false)
}
async printToPdf(setLoadParent) {
const newPdf = await this.savePdf()
const pdfBlob = newPdf.output('blob')
const pdfUrl = URL.createObjectURL(pdfBlob)
setLoadParent(false)
window.open(pdfUrl)
}
结束
好喽,开箱即用的打印和下载功能的实现就完成了。欢迎大家阅读,我是小周🤪🤪🤪
来源:juejin.cn/post/7412672713376497727
基于 Letterize.js + Anime.js 实现炫酷文本特效
如上面gif动图所示,这是一个很炫酷的文字动画效果,文字的每个字符呈波浪式的扩散式展开。本次文章将解读如何实现这个炫酷的文字效果。
基于以上的截图效果可以分析出以下是本次要实现的主要几点:
- 文案呈圆环状扩散开,扩散的同时文字变小
- 文字之间的间距从中心逐个扩散开,间距变大
- 文案呈圆环状扩散开,扩散的同时文字变大
- 文字之间的间距从中心逐个聚拢,间距变小
- 动画重复执行以上4个步骤
实现过程
核心代码实现需要基于一下两个库:
Letterize.js
是一个轻量级的JavaScript库,它可以将文本内容分解为单个字母,以便可以对每个字母进行动画处理。这对于创建复杂的文本动画效果非常有用。使用Letterize.js,你可以轻松地将一个字符串或HTML元素中的文本分解为单个字母,并为每个字母创建一个包含类名和数据属性的新HTML元素。这使得你可以使用CSS或JavaScript来控制每个字母的样式和动画。
anime.js
是一个强大的JavaScript动画库,它提供了一种简单而灵活的方式来创建各种动画效果。它可以用于HTML元素、SVG、DOM属性和JavaScript对象的动画。
通过使用Letterize.js
以便可以对每个字母进行动画处理,再结合anime.js
即可创建各种动画效果。本文不对这两个库做更多的详细介绍,只对本次特效实现做介绍,有兴趣的可以看看官网完整的使用文档。
界面布局
html
就是简单的本文标签,也不需要额外的样式,只需要在外层使用flex
布局将内容居中,因为本文的长度都是一样的,所以完成后的文本内容就像一个正方形。
<div>
<div class="animate-me">
letterize.js&anime.js
div>
<div class="animate-me">
anime.js&letterize.js
div>
......
<div class="animate-me">
letterize.js&anime.js
div>
<div class="animate-me">
anime.js&letterize.js
div>
div>
动画实现
- 初始化
Letterize.js
,只需要传入targets
目标元素,元素即是上面的.animate-me
文本标签。返回的letterize
是包含所有选中的.animate-me
元素组数。
const letterize = new Letterize({
targets: ".animate-me"
});
- 接下来初始化
anime
库的使用,下面的代码即创建了一个新的anime.js
时间线动画。目标是Letterize
对象的所有字母。动画将以100毫秒的间隔从中心开始,形成一个网格。loop: true
动画将无限循环。
const animation = anime.timeline({
targets: letterize.listAll,
delay: anime.stagger(100, {
grid: [letterize.list[0].length, letterize.list.length],
from: "center"
}),
loop: true
});
- 开始执行动画,首先设置 「文案呈圆环状扩散开,扩散的同时文字变小」,这里其实就是将字母的大小缩小。
animation
.add({
scale: 0.5
})
此时的效果如下所示:
- 继续处理下一步动画,「文字之间的间距从中心逐个扩散开,间距变大」,这里处理的其实就是将字母的间距加大,通过设置
letterSpacing
即可,代码如下:
animation
.add({
scale: 0.5
})
.add({
letterSpacing: "10px"
})
此时的效果如下所示:
- 后面还有2个步骤,「文案呈圆环状扩散开,扩散的同时文字变大;文字之间的间距从中心逐个聚拢,间距变小」,换做上面的思路也就是将文字变大和将文字间距变小,增加相应的代码如下:
.add({
scale: 1
})
.add({
letterSpacing: "6px"
});
在线预览
码上掘金地址:
最后
本文通过 Letterize.js + Anime.js 实现了一个很炫酷的文字动画效果,文字的每个字符呈波浪式的扩散式展开和收起。anime.js
还有很多的参数可以尝试,有兴趣的朋友可以尝试探索看看~
看完本文如果觉得有用,记得点个赞支持,收藏起来说不定哪天就用上啦~
专注前端开发,分享前端相关技术干货,公众号:南城大前端(ID: nanchengfe)
参考
动画效果发布者 Wojciech Krakowiak
:https://codepen.io/WojciechWKROPCE/pen/VwLePLy
来源:juejin.cn/post/7300847292974071859
CSS萤火虫按钮特效
如图所示,这是一个很炫酷的按钮悬浮特效,鼠标悬停时,按钮呈现发光的效果,周边还出现类型萤火虫的效果。本文将解析如何实现这个按钮特效,基于这个动图可以分析出需要实现的要点:
- 有一个跟随鼠标移动的圆点
- 按钮悬停时有高亮发光的效果
- 悬停时按钮周边的萤火中效果
实现过程
跟随鼠标移动的圆点
这个部分需要基于JS实现,但不是最主要的实现代码
如果单纯做一个跟随鼠标移动的点很简单,只需要监听鼠标事件获取坐标实时更新到需要移动的元素上即可。但是仔细看这里的效果并不是这样,圆点是跟随在鼠标后面,鼠标移动停止后圆点才会和鼠标重合。这里是使用了一个名为 Kinet
的库来实现的这个鼠标移动动画效果,具体实现如下:
- 创建 Kinet 实例,传入了自定义设置:
- acceleration: 加速度,控制动画的加速程度。
- friction: 摩擦力,控制动画的减速程度。
- names: 定义了两个属性 x 和 y,用于表示动画的两个维度。
var kinet = new Kinet({
acceleration: 0.02,
friction: 0.25,
names: ["x", "y"],
});
- 通过 document.getElementById 获取页面中 ID 为
circle
的元素,以便后续进行动画处理。
var circle = document.getElementById('circle');
- 设置 Kinet 的
tick
事件处理:
- 监听
tick
事件,每当 Kinet 更新时执行该函数。 instances
参数包含当前的 x 和 y 值及其速度。- 使用
style.transform
属性来更新圆形元素的位置和旋转: translate3d
用于在 3D 空间中移动元素。rotateX
和rotateY
用于根据当前速度旋转元素。
kinet.on('tick', function(instances) {
circle.style.transform = `translate3d(${ (instances.x.current) }px, ${ (instances.y.current) }px, 0) rotateX(${ (instances.x.velocity/2) }deg) rotateY(${ (instances.y.velocity/2) }deg)`;
});
- 听 mousemove 事件,
kinet.animate
方法用于更新 x 和 y 的目标值,计算方式是将鼠标的当前位置减去窗口的中心位置,使动画围绕窗口中心进行。
document.addEventListener('mousemove', function (event) {
kinet.animate('x', event.clientX - window.innerWidth/2);
kinet.animate('y', event.clientY - window.innerHeight/2);
});
随着鼠标的移动这个圆点元素将在页面上进行平滑的动画。通过 Kinet 库的加速度和摩擦力设置,动画效果显得更加自然,用户体验更加生动。有兴趣的可以尝试调整参数解锁其他玩法,此时我们的页面效果如下:
按钮悬停时发光效果
这里主要通过悬停时设置transition
过渡改变按钮的内外阴影效果,阴影效果通过伪元素实现,默认透明度为0,按钮样式代码如下:
.button {
z-index: 1;
position: relative;
text-decoration: none;
text-align: center;
appearance: none;
display: inline-block;
}
.button::before, .button::after {
content: "";
position: absolute;
top: 0;
right: 0;
left: 0;
bottom: 0;
border-radius: 999px;
opacity: 0;
transition: opacity 0.3s;
}
.button::before {
box-shadow: 0px 0px 24px 0px #FFEB3B;
}
.button::after {
box-shadow: 0px 0px 23px 0px #FDFCA9 inset, 0px 0px 8px 0px #FFFFFF42;
}
当鼠标悬停在按钮上时,通过改变伪元素的透明度,使发光效果在鼠标悬停时变得可见:
.button-wrapper:hover .button::before,
.button-wrapper:hover .button::after {
opacity: 1;
}
此时的按钮效果如下:
悬停时萤火中效果
如头部图片所展示,萤火虫效果是有多个圆点散开,所以这里我们添加多个圆点元素。
class="dot dot-1">
<span class="dot dot-2">span>
<span class="dot dot-3">span>
<span class="dot dot-4">span>
<span class="dot dot-5">span>
<span class="dot dot-6">span>
<span class="dot dot-7">span>
设置元素样式,这里的CSS变量(如 --speed, --size, --starting-x, --starting-y, --rotatation)用于控制圆点的动画速度、大小、起始位置和旋转角度。
.dot {
display: block;
position: absolute;
transition: transform calc(var(--speed) / 12) ease;
width: var(--size);
height: var(--size);
transform: translate(var(--starting-x), var(--starting-y)) rotate(var(--rotatation));
}
给圆点设置动画效果,使用 @keyframes
定义了两个动画:dimFirefly
和 hoverFirefly
,为圆点添加了闪烁和移动效果:
@keyframes dimFirefly {
0% { opacity: 1; }
25% { opacity: 0.4; }
50% { opacity: 0.8; }
75% { opacity: 0.5; }
100% { opacity: 1; }
}
@keyframes hoverFirefly {
0% { transform: translate(0, 0); }
12% { transform: translate(3px, 1px); }
24% { transform: translate(-2px, 3px); }
37% { transform: translate(2px, -2px); }
55% { transform: translate(-1px, 0); }
74% { transform: translate(0, 2px); }
88% { transform: translate(-3px, -1px); }
100% { transform: translate(0, 0); }
}
在圆点的伪元素上关联动画效果:
.dot::after {
content: "";
animation: hoverFirefly var(--speed) infinite, dimFirefly calc(var(--speed) / 2) infinite calc(var(--speed) / 3);
animation-play-state: paused;
display: block;
border-radius: 100%;
background: yellow;
width: 100%;
height: 100%;
box-shadow: 0px 0px 6px 0px #FFEB3B, 0px 0px 4px 0px #FDFCA9 inset, 0px 0px 2px 1px #FFFFFF42;
}
给每个圆点设置不同的动画参数,通过使用 CSS 变量,开发者可以灵活地控制每个 .dot
元素的旋转角度,进一步丰富视觉效果。
.dot-1 {
--rotatation: 0deg;
--speed: 14s;
--size: 6px;
--starting-x: 30px;
--starting-y: 20px;
top: 2px;
left: -16px;
opacity: 0.7;
}
.dot-2 {
--rotatation: 122deg;
--speed: 16s;
--size: 3px;
--starting-x: 40px;
--starting-y: 10px;
top: 1px;
left: 0px;
opacity: 0.7;
}
...
此时只要在父元素.button-wrapper
悬停时,则触发 .dot
元素的旋转效果,并使其伪元素的动画开始运行,此时萤火中悬停效果就会开始运行。
.button-wrapper:hover {
.dot {
transform: translate(0, 0) rotate(var(--rotatation));
}
.dot::after {
animation-play-state: running;
}
}
最后完成的悬停效果如下:
在线预览
最后
通过以上步骤,结合现代 CSS 的强大功能,我们实现了一个发光的萤火虫圆点悬停按钮效果。这样的效果不仅提升了视觉吸引力,还增强了用户的交互体验。利用 CSS 变量和动画,设计师可以灵活地控制每个元素的表现,使得网页更加生动和引人注目。有兴趣的可以调整相关参数体验其他的视觉效果。
来源:juejin.cn/post/7401144423563444276
横扫鸿蒙弹窗乱象,SmartDialog出世
前言
但凡用过鸿蒙原生弹窗的小伙伴,就能体会到它们是有多么的难用和奇葩,什么AlertDialog,CustomDialog,SubWindow,bindXxx,只要大家用心去体验,就能发现他们有很多离谱的设计和限制,时常就是一边用,一边骂骂咧咧的吐槽
实属无奈,就把鸿蒙版的SmartDialog写出来了
flutter自带的dialog是可以应对日常场景,例如:简单的打开一个弹窗,非UI模块使用,跨页面交互之类;flutter_smart_dialog 是补齐了大多数的业务场景和一些强大的特殊能力,flutter_smart_dialog 对于flutter而言,日常场景是锦上添花,特殊场景是雪中送炭
但是 ohos_smart_dialog 对于鸿蒙而言,日常场景就是雪中送炭!单单一个使用方式而言,就是吊打鸿蒙的CustomDialog,CustomDialog的各种限制和使用方式,我不想再去提及和吐槽了
有时候,简洁的使用,才是最大的魅力
鸿蒙版的SmartDialog有什么优势?
- 单次初始化后即可使用,无需多处配置相关Component
- 优雅,极简的用法
- 非UI区域内使用,自定义Component
- 返回事件处理,优化的跨页面交互
- 多弹窗能力,多位置弹窗:上下左右中间
- 定位弹窗:自动定位目标Component
- 极简用法的loading弹窗
- 等等......
目前 flutter_smart_dialog 的代码量16w+,完整复刻其功能,工作量非常大,目前只能逐步实现一些基础能力,由于鸿蒙api的设计和相关限制,用法和相关初始化都有一定程度的妥协
鸿蒙版本的SmartDialog,功能会逐步和 flutter_smart_dialog 对齐(长期),api会尽量保持一致
效果
- Tablet 模拟器目前有些问题,会导致动画闪烁,请忽略;注:真机动画丝滑流畅,无任何问题
极简用法
// dialog
SmartDialog.show({
builder: dialogArgs,
builderArgs: Math.random(),
})
@Builder
function dialogArgs(args: number) {
Text(args.toString()).padding(50).backgroundColor(Color.White)
}
// loading
SmartDialog.showLoading()
安装
ohpm install ohos_smart_dialog
配置
下述的配置项,可能会有一点多,但,这也是为了极致的体验;同时也是无奈之举,相关配置难以在内部去闭环处理,只能在外部去配置
这些配置,只需要配置一次,后续无需关心
完成下述的配置后,你将可以在任何地方使用弹窗,没有任何限制
初始化
- 因为弹窗需要处理跨页面交互,必须要监控路由
@Entry
@Component
struct Index {
navPathStack: NavPathStack = new NavPathStack()
build() {
Stack() {
// here: monitor router
Navigation(OhosSmartDialog.registerRouter(this.navPathStack)) {
MainPage()
}
.mode(NavigationMode.Stack)
.hideTitleBar(true)
.navDestination(pageMap)
// here
OhosSmartDialog()
}.height('100%').width('100%')
}
}
返回事件监听
别问我为啥返回事件的监听,处理的这么不优雅,鸿蒙里面没找全局返回事件监听,我也没辙。。。
- 如果你无需处理返回事件,可以使用下述写法
// Entry页面处理
@Entry
@Component
struct Index {
onBackPress(): boolean | void {
return OhosSmartDialog.onBackPressed()()
}
}
// 路由子页面
struct JumpPage {
build() {
NavDestination() {
// ....
}
.onBackPressed(OhosSmartDialog.onBackPressed())
}
}
- 如果你需要处理返回事件,在OhosSmartDialog.onBackPressed()中传入你的方法即可
// Entry页面处理
@Entry
@Component
struct Index {
onBackPress(): boolean | void {
return OhosSmartDialog.onBackPressed(this.onCustomBackPress)()
}
onCustomBackPress(): boolean {
return false
}
}
// 路由子页面
@Component
struct JumpPage {
build() {
NavDestination() {
// ...
}
.onBackPressed(OhosSmartDialog.onBackPressed(this.onCustomBackPress))
}
onCustomBackPress(): boolean {
return false
}
}
路由监听
- 一般来说,你无需关注SmartDialog的路由监听,因为内部已经设置了路由监听拦截器
- 但是,NavPathStack仅支持单拦截器(setInterception),如果业务代码也使用了这个api,会导致SmartDialog的路由监听被覆盖,从而失效
如果出现该情况,请参照下述解决方案
- 在你的路由监听类中手动调用
OhosSmartDialog.observe
export default class YourNavigatorObserver implements NavigationInterception {
willShow?: InterceptionShowCallback = (from, to, operation, isAnimated) => {
OhosSmartDialog.observe.willShow?.(from, to, operation, isAnimated)
// ...
}
didShow?: InterceptionShowCallback = (from, to, operation, isAnimated) => {
OhosSmartDialog.observe.didShow?.(from, to, operation, isAnimated)
// ...
}
}
适配暗黑模式
- 为了极致的体验,深色模式切换时,打开态弹窗也应刷新为对应模式的样式,故需要进行下述配置
export default class EntryAbility extends UIAbility {
onConfigurationUpdate(newConfig: Configuration): void {
OhosSmartDialog.onConfigurationUpdate(newConfig)
}
}
SmartConfig
- 支持全局配置弹窗的默认属性
function init() {
// show
SmartDialog.config.custom.maskColor = "#75000000"
SmartDialog.config.custom.alignment = Alignment.Center
// showAttach
SmartDialog.config.attach.attachAlignmentType = SmartAttachAlignmentType.center
}
- 检查弹窗是否存在
// 检查当前是否有CustomDialog,AttachDialog或LoadingDialog处于打开状态
let isExist = SmartDialog.checkExist()
// 检查当前是否有AttachDialog处于打开状态
let isExist = SmartDialog.checkExist({ dialogTypes: [SmartAllDialogType.attach] })
// 检查当前是否有tag为“xxx”的dialog处于打开状态
let isExist = SmartDialog.checkExist({ tag: "xxx" })
配置全局默认样式
- ShowLoading 自定样式十分简单
SmartDialog.showLoading({ builder: customLoading })
但是对于大家来说,肯定是想用 SmartDialog.showLoading()
这种简单写法,所以支持自定义全局默认样式
- 需要在 OhosSmartDialog 上配置自定义的全局默认样式
@Entry
@Component
struct Index {
build() {
Stack() {
OhosSmartDialog({
// custom global loading
loadingBuilder: customLoading,
})
}.height('100%').width('100%')
}
}
@Builder
export function customLoading(args: ESObject) {
LoadingProgress().width(80).height(80).color(Color.White)
}
- 配置完你的自定样式后,使用下述代码,就会显示你的 loading 样式
SmartDialog.showLoading()
// 支持入参,可以在特殊场景下灵活配置
SSmartDialog.showLoading({ builderArgs: 1 })
CustomDialog
- 下方会共用的方法
export function randomColor(): string {
const letters: string = '0123456789ABCDEF';
let color = '#';
for (let i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)];
}
return color;
}
export function delay(ms?: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
传参弹窗
export function customUseArgs() {
SmartDialog.show({
builder: dialogArgs,
// 支持任何类型
builderArgs: Math.random(),
})
}
@Builder
function dialogArgs(args: number) {
Text(`${args}`).fontColor(Color.White).padding(50)
.borderRadius(12).backgroundColor(randomColor())
}
多位置弹窗
export async function customLocation() {
const animationTime = 1000
SmartDialog.show({
builder: dialogLocationHorizontal,
alignment: Alignment.Start,
})
await delay(animationTime)
SmartDialog.show({
builder: dialogLocationVertical,
alignment: Alignment.Top,
})
}
@Builder
function dialogLocationVertical() {
Text("location")
.width("100%")
.height("20%")
.fontSize(20)
.fontColor(Color.White)
.textAlign(TextAlign.Center)
.padding(50)
.backgroundColor(randomColor())
}
@Builder
function dialogLocationHorizontal() {
Text("location")
.width("30%")
.height("100%")
.fontSize(20)
.fontColor(Color.White)
.textAlign(TextAlign.Center)
.padding(50)
.backgroundColor(randomColor())
}
跨页面交互
- 正常使用,无需设置什么参数
export function customJumpPage() {
SmartDialog.show({
builder: dialogJumpPage,
})
}
@Builder
function dialogJumpPage() {
Text("JumPage")
.fontSize(30)
.padding(50)
.borderRadius(12)
.fontColor(Color.White)
.backgroundColor(randomColor())
.onClick(() => {
// 跳转页面
})
}
关闭指定弹窗
export async function customTag() {
const animationTime = 1000
SmartDialog.show({
builder: dialogTagA,
alignment: Alignment.Start,
tag: "A",
})
await delay(animationTime)
SmartDialog.show({
builder: dialogTagB,
alignment: Alignment.Top,
tag: "B",
})
}
@Builder
function dialogTagA() {
Text("A")
.width("20%")
.height("100%")
.fontSize(20)
.fontColor(Color.White)
.textAlign(TextAlign.Center)
.padding(50)
.backgroundColor(randomColor())
}
@Builder
function dialogTagB() {
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(["closA", "closeSelf"], (item: string, index: number) => {
Button(item)
.backgroundColor("#4169E1")
.margin(10)
.onClick(() => {
if (index === 0) {
SmartDialog.dismiss({ tag: "A" })
} else if (index === 1) {
SmartDialog.dismiss({ tag: "B" })
}
})
})
}.backgroundColor(Color.White).width(350).margin({ left: 30, right: 30 }).padding(10).borderRadius(10)
}
自定义遮罩
export function customMask() {
SmartDialog.show({
builder: dialogShowDialog,
maskBuilder: dialogCustomMask,
})
}
@Builder
function dialogCustomMask() {
Stack().width("100%").height("100%").backgroundColor(randomColor()).opacity(0.6)
}
@Builder
function dialogShowDialog() {
Text("showDialog")
.fontSize(30)
.padding(50)
.fontColor(Color.White)
.borderRadius(12)
.backgroundColor(randomColor())
.onClick(() => customMask())
}
AttachDialog
默认定位
export function attachEasy() {
SmartDialog.show({
builder: dialog
})
}
@Builder
function dialog() {
Stack() {
Text("Attach")
.backgroundColor(randomColor())
.padding(20)
.fontColor(Color.White)
.borderRadius(5)
.onClick(() => {
SmartDialog.showAttach({
targetId: "Attach",
builder: targetLocationDialog,
})
})
.id("Attach")
}
.borderRadius(12)
.padding(50)
.backgroundColor(Color.White)
}
@Builder
function targetLocationDialog() {
Text("targetIdDialog")
.fontSize(20)
.fontColor(Color.White)
.textAlign(TextAlign.Center)
.padding(50)
.borderRadius(12)
.backgroundColor(randomColor())
}
多方向定位
export function attachLocation() {
SmartDialog.show({
builder: dialog
})
}
class AttachLocation {
title: string = ""
alignment?: Alignment
}
const locationList: Array<AttachLocation> = [
{ title: "TopStart", alignment: Alignment.TopStart },
{ title: "Top", alignment: Alignment.Top },
{ title: "TopEnd", alignment: Alignment.TopEnd },
{ title: "Start", alignment: Alignment.Start },
{ title: "Center", alignment: Alignment.Center },
{ title: "End", alignment: Alignment.End },
{ title: "BottomStart", alignment: Alignment.BottomStart },
{ title: "Bottom", alignment: Alignment.Bottom },
{ title: "BottomEnd", alignment: Alignment.BottomEnd },
]
@Builder
function dialog() {
Column() {
Grid() {
ForEach(locationList, (item: AttachLocation) => {
GridItem() {
buildButton(item.title, () => {
SmartDialog.showAttach({
targetId: item.title,
alignment: item.alignment,
maskColor: Color.Transparent,
builder: targetLocationDialog
})
})
}
})
}.columnsTemplate('1fr 1fr 1fr').height(220)
buildButton("allOpen", async () => {
for (let index = 0; index < locationList.length; index++) {
let item = locationList[index]
SmartDialog.showAttach({
targetId: item.title,
alignment: item.alignment,
maskColor: Color.Transparent,
builder: targetLocationDialog,
})
await delay(300)
}
}, randomColor())
}
.borderRadius(12)
.width(700)
.padding(30)
.backgroundColor(Color.White)
}
@Builder
function buildButton(title: string, onClick?: VoidCallback, bgColor?: ResourceColor) {
Text(title)
.backgroundColor(bgColor ?? "#4169E1")
.constraintSize({ minWidth: 120, minHeight: 46 })
.margin(10)
.textAlign(TextAlign.Center)
.fontColor(Color.White)
.borderRadius(5)
.onClick(onClick)
.id(title)
}
@Builder
function targetLocationDialog() {
Text("targetIdDialog")
.fontSize(20)
.fontColor(Color.White)
.textAlign(TextAlign.Center)
.padding(50)
.borderRadius(12)
.backgroundColor(randomColor())
}
Loading
对于Loading而言,应该有几个比较明显的特性
- loading和dialog都存在页面上,哪怕dialog打开,loading都应该显示dialog之上
- loading应该具有单一特性,多次打开loading,页面也应该只存在一个loading
- 刷新特性,多次打开loading,后续打开的loading样式,应该覆盖之前打开的loading样式
- loading使用频率非常高,应该支持强大的拓展和极简的使用
从上面列举几个特性而言,loading是一个非常特殊的dialog,所以需要针对其特性,进行定制化的实现
当然了,内部已经屏蔽了细节,在使用上,和dialog的使用没什么区别
默认loading
SmartDialog.showLoading()
自定义Loading
- 点击loading后,会再次打开一个loading,从效果图可以看出它的单一刷新特性
export function loadingCustom() {
SmartDialog.showLoading({
builder: customLoading,
})
}
@Builder
export function customLoading() {
Column({ space: 5 }) {
Text("again open loading").fontSize(16).fontColor(Color.White)
LoadingProgress().width(80).height(80).color(Color.White)
}
.padding(20)
.borderRadius(12)
.onClick(() => loadingCustom())
.backgroundColor(randomColor())
}
最后
鸿蒙版的SmartDialog,相信会对开发鸿蒙的小伙伴们有一些帮助~.~
现在就业环境真是让人头皮发麻,现在的各种技术群里,看到好多人公司各种拖欠工资,各种失业半年的情况
淦,不知道还能写多长时间代码!
来源:juejin.cn/post/7401056900878368807
我的第一个独立产品,废了,大家来看看热闹哈哈
产品想法萌生背景:
我孩子4岁,很喜欢画画,平常在家里,画在纸上,墙的画板上,学习机上,每一次画都很专注,而外出时,有时候会无聊,比如就餐等位,长时间坐高铁,等爸爸剪头发等等场景,一时之间也不好找东西给他玩,于是有了做一个画画小程序给他的想法,同时也觉得会有同样需求的家长,尽管需求场景很小,用的频率很低,但这小程序也许是可以救急的
产品实施:
1.梳理初步想要实现的功能
2.开发实现
需求想要的效果都实现了,可随意改变颜色在白板上随意画画,效果如下
3.更多的想法
实现了画画功能,感觉太单一,于是想到涂色卡和字帖功能,具体如下
其实都是“画”这个功能的延伸,实现起来也比较顺利,实现效果如下
4.为什么废了?
- 想要在外出时画画,可以买一个小小的画板,一键清除那种,这样既能画画,还不用看手机,蛮多家长介意看手机的
- 需要场景很小,很多替代方案,各种小型的益智玩具,绘本等
- 字帖功能,一般是打印纸质版,练习握笔和书写习惯
- 欢迎补充哈哈哈
最后想说
虽然产品废了,但从0到1实现了自己的想法,收获还是很多的,我从事Java开发,因为实现这个想法,自学了小程序开发,AI抠图等,在开发过程中,解决了一个又一个开发障碍,最终达到想要的效果,对这个产品实现有任何疑问都可以留言哈,我都会解答!对了,搜索小程序“小乐画板”,就可以体验这款小程序
来源:juejin.cn/post/7412505754382696448
人人都可配置的大屏可视化
大屏主要是为了展示数据和酷炫的效果,布局大部分是9宫格,或者在9宫格上做的延伸,现在介绍下 泛积木-低代码 提供的大屏可视化配置。
首先查看效果展示:泛积木-低代码大屏展示,泛积木-低代码大屏展示 此页面注册登录之后可编辑(会定期恢复至演示版本)。
创建页面之后,点击进入编辑页面,在可视化编辑器左侧组件往下翻,找到自定义组件中的 大屏布局组件 ,将 大屏布局组件 拖入页面,可以看到下面的成果:
拖入的 大屏布局组件 将使用基础配置,并且已经自带了缩放容器组件。
缩放容器组件
缩放容器组件主要用于适配不同的尺寸大小,实现原理:缩放容器组件是以该组件的宽度和设计稿的宽度求比例,然后等比例缩放。
缩放容器组件支持配置 设计稿宽度、设计稿高度、样式名称、背景颜色,当要适配不同尺寸的屏幕时,我们只需要修改 设计稿宽度、设计稿高度 为对应尺寸即可。样式名称是添加您需要设置的 样式 或添加唯一的className
,className
作用的元素将作为后续全屏按钮点击时全屏的元素。
全屏按钮组件
全屏按钮组件主要用于配置全屏按钮加全屏元素等。在全屏元素中配置 缩放容器组件 的 唯一className
。
全屏按钮组件还支持配置 样式名称、字体颜色、字体大小、间距。字体颜色未配置时,会默认从 大屏布局组件 的字体颜色继承。
说完上述两个小组件之后,我们再来说说关键的 大屏布局组件。
大屏布局组件
大屏布局组件的配置项可以概括为两部分:
- 总体配置:
- 总体内容:
- 样式名称;
- 字体颜色;
- 背景颜色;
- 背景图片(不想写链接,也可以直接上传);
- 是否显示头部;
- 模块样式模板;
- 样式覆盖;
- 页面内容:
- 样式名称;
- 内间距;
- 总体内容:
- 头部配置:
- 头部总体配置:
- 标题名称;
- 头部背景图片(支持上传);
- 样式名称;
- 头部左侧:
- 左侧内容;
- 样式名称;
- 头部右侧:
- 右侧内容;
- 样式名称;
- 头部时间:
- 是否显示;
- 字体大小;
- 显示格式。
- 头部总体配置:
样式覆盖 填入 css 之后,会自动在组件内创建 style
标签添加样式,这个时候需要使用 css 优先级去覆盖默认展示内容,例如:
.large-screen-layout .large-screen-layout-header {
height: 100px;
}
此时页面头部的高度将由默认的 80px 调整为 100px 。
头部背景图片 未设置时,头部高度默认为 80px ,设置之后,高度为背景图片按照宽度整体缩放之后的高度。
头部左/右侧内容 是配置 SchemaNode , SchemaNode 是指每一个 amis 配置节点的类型,支持模板、Schema(配置)以及SchemaArray(配置数组)三种类型。
例如:
{
...
"headerLeft": {
"type": "tpl",
"tpl": "公司名称",
"id": "u:3dc2c3411ae1"
},
"headerRight": {
"type": "fan-screenfull-button",
"targetClass": "largeScreenLayout",
"fontSize": "22px",
"id": "u:be46114da702"
},
...
}
模块样式模板 用于统一设置 大屏单块模板组件 的样式模板,样式模板是事先定义好的一些简单样式。
大屏单块模板组件
大屏单块模板组件 是用于配置大屏每块内容,大屏布局组件 和 大屏单块模板组件 之间还有一层 grid-2d 组件。
grid-2d 组件 是使用 grid 布局,支持配置 外层 Dom 的类名、格子划分、格子垂直高度、格子间距、格子行间距,建议 大屏布局组件 -> 总体配置 -> 页面内容 -> 内边距 和格子间距设置一致,格子划分 指定 划分为几列,格子间距统一设置横向和纵向的间距,格子行间距可以设置横向间距,优先级高于格子间距。
格子垂直高度 = (缩放容器组件的设计稿高度 - 大屏布局组件头部高度 - 大屏布局组件头部高度页面内容内边距 * 2 - (格子行间距 || 格子间距) * 2) / 3
例如默认的: (1080 - 80 - 20 * 2 - 20 * 2) / 3 = 306.667px
大屏单块模板组件 支持如下配置:
- 总体内容:
- 样式名称;
- 样式模板;
- 位置配置;
- 起始位置X;
- 起始位置Y;
- 宽度W;
- 高度H;
- 是否显示头部;
- 样式覆盖;
- 模块标题:
- 标题名称;
- 标题样式;
- 字体颜色;
- 模块头部右侧:
- 右侧内容;
- 样式名称;
- 模块内容:
- 样式名称;
- 内边距。
样式覆盖 填入 css 之后,会自动在组件内创建 style
标签添加样式,这个时候需要使用 css 优先级去覆盖默认展示内容,例如:
.fan-screen-card .fan-screen-card-header {
height: 80px;
}
此时模块头部的高度将由默认的 50px 调整为 80px 。 css 会作用于符合 css 的所有DOM元素,如果需要唯一设置,请在前面添加特殊的前缀,例如:
.fan-screen-card-1.fan-screen-card .fan-screen-card-header {
height: 80px;
}
样式模板 可单独设置每个模块的样式。
模块头部右侧内容 是配置 SchemaNode , SchemaNode 是指每一个 amis 配置节点的类型,支持模板、Schema(配置)以及SchemaArray(配置数组)三种类型。
位置配置 每项的值都是数值,比如默认的 9 宫格就是 3 * 3,此时设置的值就是 1/2/3 ,宽度1就代表一列,高度1就代表一行。可以调整初始位置、宽度、高度等配置出多种布局方式。
大屏单块模板内容首先嵌套 Service 功能型容器 用于获取数据,再使用 Chart 图表 进行图表渲染。
如果需要轮流高亮 Chart 图表的每个数据,例如 大屏动态展示 可以使用如下配置:
- 在 Chart 图表 上添加唯一的
className
; - 配置 Chart 图表的
config
; - 配置 Chart 图表的
dataFilter
。
dataFilter
:
const curFlag = 'lineCharts';
if (window.fanEchartsIntervals && window.fanEchartsIntervals.get(curFlag)) {
clearInterval(window.fanEchartsIntervals.get(curFlag)[0]);
window.fanEchartsIntervals.get(curFlag)[1] && window.fanEchartsIntervals.get(curFlag)[1].dispose();
}
const myChart = echarts.init(document.getElementsByClassName(curFlag)[0]);
let currentIndex = -1;
myChart.setOption({
...config,
series: [
{
...config.series[0],
data: data.line
}
]
});
const interval = setInterval(function () {
const dataLen = data.line.length;
// 取消之前高亮的图形
myChart.dispatchAction({
type: 'downplay',
seriesIndex: 0,
dataIndex: currentIndex
});
currentIndex = (currentIndex + 1) % dataLen;
// 高亮当前图形
myChart.dispatchAction({
type: 'highlight',
seriesIndex: 0,
dataIndex: currentIndex
});
// 显示 tooltip
myChart.dispatchAction({
type: 'showTip',
seriesIndex: 0,
dataIndex: currentIndex
});
}, 1000);
if (window.fanEchartsIntervals) {
window.fanEchartsIntervals.set(curFlag, [interval, myChart]);
} else {
window.fanEchartsIntervals = new Map();
window.fanEchartsIntervals.set(curFlag, [interval, myChart]);
}
return config;
修改高亮行 1 curFlag
设置为对应的 Chart 图表的 className
,12-17 行是插入数据,22-39 为对应数据的切换展示方式。
当添加第二个 大屏单块模板 时,直接把第一个复制一份,调整位置、service组件的接口、dataFilter配置等。
至此大屏就配置完成了。
更详细的使用文档可以查看 泛积木-低代码 。
来源:juejin.cn/post/7329767824200810534
Java 语法糖,你用过几个?
你好,我是猿java。
这篇文章,我们来聊聊 Java 语法糖。
什么是语法糖?
语法糖(Syntactic Sugar)是编程语言中的一种设计概念,它指的是在语法层面上对某些操作提供更简洁、更易读的表示方式。这种表示方式并不会新增语言的功能,而只是使代码更简洁、更直观,便于开发者理解和维护。
语法糖的作用:
- 提高代码可读性:语法糖可以使代码更加贴近自然语言或开发者的思维方式,从而更容易理解。
- 减少样板代码:语法糖可以减少重复的样板代码,使得开发者可以更专注于业务逻辑。
- 降低出错率:简化的语法可以减少代码量,从而降低出错的概率。
因此,语法糖不是 Java 语言特有的,它是很多编程语言设计中的一些语法特性,这些特性使代码更加简洁易读,但并不会引入新的功能或能力。
那么,Java中有哪些语法糖呢?
Java 语法糖
1. 自动装箱与拆箱
自动装箱和拆箱 (Autoboxing and Unboxing)是 Java 5 引入的特性,用于在基本数据类型和它们对应的包装类之间自动转换。
// 自动装箱
Integer num = 10; // 实际上是 Integer.valueOf(10)
// 自动拆箱
int n = num; // 实际上是 num.intValue()
2. 增强型 for 循环
增强型 for 循环(也称为 for-each 循环)用于遍历数组或集合。
int[] numbers = {1, 2, 3, 4, 5};
for (int number : numbers) {
System.out.println(number);
}
3. 泛型
泛型(Generics)使得类、接口和方法可以操作指定类型的对象,提供了类型安全的检查和消除了类型转换的需要。
List<String> list = new ArrayList<>();
list.add("Hello");
String s = list.get(0); // 不需要类型转换
4. 可变参数
可变参数(Varargs)允许在方法中传递任意数量的参数。
public void printNumbers(int... numbers) {
for (int number : numbers) {
System.out.println(number);
}
}
printNumbers(1, 2, 3, 4, 5);
5. try-with-resources
try-with-resources 语句用于自动关闭资源,实现了 AutoCloseable
接口的资源会在语句结束时自动关闭。
try (BufferedReader br = new BufferedReader(new FileReader("file.txt"))) {
System.out.println(br.readLine());
} catch (IOException e) {
e.printStackTrace();
}
6. Lambda 表达式
Lambda 表达式是 Java 8 引入的特性,使得可以使用更简洁的语法来实现函数式接口(只有一个抽象方法的接口)。
List<String> list = Arrays.asList("a", "b", "c");
list.forEach(s -> System.out.println(s));
7. 方法引用
方法引用(Method References)是 Lambda 表达式的一种简写形式,用于直接引用已有的方法。
list.forEach(System.out::println);
8. 字符串连接
从 Java 5 开始,Java 编译器会将字符串的连接优化为 StringBuilder
操作。
String message = "Hello, " + "world!"; // 实际上是 new StringBuilder().append("Hello, ").append("world!").toString();
9. Switch 表达式
Java 12 引入的 Switch 表达式使得 Switch 语句更加简洁和灵活。
int day = 5;
String dayName = switch (day) {
case 1 -> "Sunday";
case 2 -> "Monday";
case 3 -> "Tuesday";
case 4 -> "Wednesday";
case 5 -> "Thursday";
case 6 -> "Friday";
case 7 -> "Saturday";
default -> "Invalid day";
};
10. 类型推断 (Type Inference)
Java 10 引入了局部变量类型推断,通过 var
关键字来声明变量,编译器会自动推断变量的类型。
var list = new ArrayList<String>();
list.add("Hello");
这些语法糖使得 Java 代码更加简洁和易读,但需要注意的是,它们并不会增加语言本身的功能,只是对已有功能的一种简化和封装。
总结
本文,我们介绍了 Java 语言中的一些语法糖,从上面的例子可以看出,Java 语法糖只是一些简化的语法,可以使代码更简洁易读,而本身并不增加新的功能。
学习交流
如果你觉得文章有帮助,请帮忙转发给更多的好友,或关注公众号:猿java,持续输出硬核文章。
来源:juejin.cn/post/7412672643633791039
前端实现:页面滚动时,元素缓慢上升效果
效果
实现方式
- 自定义指令
- 封装组件
两种方式均可以在SSR页面中使用
方式1:自定义指令实现
import Vue from 'vue';
const DISTANCE = 100; // y轴移动距离
const DURATION = 1000; // 动画持续时间
const THRESHOLD_FOR_TRIGGERING_ANIMATION = 0.1; // 当元素一部分可见时触发动画
const animationMap = new WeakMap();
function handleIntersection(entries, observer) { // IntersectionObserver 回调函数, 处理元素的可见性变化
for (const entry of entries) { // 遍历所有观察目标
if (entry.isIntersecting) { // 如果目标可见
const animation = animationMap.get(entry.target); // 获取动画对象
if (animation) {
animation.play(); // 播放动画
} else {
// 如果不支持 Web Animations API,则使用 CSS 动画回退方案
entry.target.classList.add('active');
}
observer.unobserve(entry.target); // 播放一次后停止监听
}
}
}
let ob;
if ('IntersectionObserver' in window) { // 如果浏览器支持 IntersectionObserver
ob = new IntersectionObserver(handleIntersection, { // 创建 IntersectionObserver 对象
threshold: THRESHOLD_FOR_TRIGGERING_ANIMATION // 当元素一部分可见时触发动画
});
} else {
// 回退机制:如果不支持 IntersectionObserver
ob = {
observe(el) { // IntersectionObserver 接口的 observe 方法
el.__onScroll__ = () => { // 监听元素的滚动事件
if (isInViewport(el)) { // 如果元素在视窗内
const animation = animationMap.get(el); // 获取动画对象
if (animation) {
animation.play();
} else {
// 如果不支持 Web Animations API,则使用 CSS 动画回退方案
el.classList.add('active');
}
window.removeEventListener('scroll', el.__onScroll__); // 停止监听
}
};
window.addEventListener('scroll', el.__onScroll__); // 监听元素的滚动事件
},
unobserve(el) { // IntersectionObserver 接口的 unobserve 方法
if (el.__onScroll__) { // 如果元素有滚动事件监听
window.removeEventListener('scroll', el.__onScroll__); // 停止监听
delete el.__onScroll__; // 清理引用
}
}
};
}
function isBelowViewport(el) { // 判断元素是否在视窗下方
const rect = el.getBoundingClientRect();
return rect.top > window.innerHeight;
}
function isInViewport(el) { // 判断元素是否在视窗内
const rect = el.getBoundingClientRect();
return rect.top < window.innerHeight && rect.bottom > 0;
}
const directive = {
name: 'slide-in',
inserted(el, binding) { // 元素插入到 DOM 时触发
if (!isBelowViewport(el)) { // 如果元素在视窗下方,则不执行动画
console.log('Element is not below viewport');
return;
}
const duration = binding.value && binding.value.duration ? binding.value.duration : DURATION; // 动画持续时间
const animationOptions = { // 动画选项: 目标位置、持续时间、缓动函数
duration: duration,
easing: binding.value && binding.value.easing ? binding.value.easing : 'ease'
};
// 检查是否支持 Web Animations API
let animation;
if (el.animate) { // 如果支持 Web Animations API
animation = el.animate([ // 创建动画对象
{
transform: `translateY(${DISTANCE}px)`,
opacity: 0.5
},
{
transform: 'translateY(0)',
opacity: 1
}
], animationOptions);
animation.pause(); // 初始化时暂停动画
animationMap.set(el, animation); // 保存动画对象
} else {
// 如果不支持 Web Animations API,则添加 CSS 动画回退类
el.classList.add('animate-fallback'); // animate-fallback在下面SCSS中有定义
}
ob.observe(el); // 开始监听元素的可见性变化
},
unbind(el) { // 元素从 DOM 中移除时触发
ob.unobserve(el); // 停止监听元素的可见性变化
}
};
Vue.directive(directive.name, directive);
注册指令
directives/index.js
import './slide-in' // 元素缓慢上升效果
main.js
import './directives'
在页面中使用
<template>
<div class="boxs .scroll-container">
<div class="slide-box" v-slide-in="{ duration: 500, easing: 'ease-in-out' }">0 - slide-directives</div>
<div class="slide-box" v-slide-in>1 - slide-directives</div>
<div class="slide-box" v-slide-in>2 - slide-directives</div>
<div v-slide-in>3 - slide-directives</div>
<div v-slide-in="{ duration: 500, easing: 'linear' }">4 - slide-directives</div>
<div v-slide-in>5 - slide-directives</div>
<div v-slide-in="{ duration: 500 }">6 - slide-directives</div>
</div>
</template>
<style lang="scss" scoped>
.boxs {
div {
text-align: center;
width: 800px;
height: 300px;
background-color: #f2f2f2;
margin: 0 auto;
margin-top: 20px;
}
}
<!-- 兼容性处理(可放到全局style中) -->
.animate-fallback {
opacity: 0;
transform: translateY(100px);
transition: transform 1s ease, opacity 1s ease;
}
.animate-fallback.active {
opacity: 1;
transform: translateY(0);
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(100px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-fallback-keyframes {
opacity: 0;
animation: slideIn 1s ease forwards;
}
</style>
方式2: 封装为组件
<template>
<div ref="animatedElement" :style="computedStyle">
<slot></slot>
</div>
</template>
<script>
export default {
name: 'slideIn',
props: {
duration: { // 动画持续时间
type: Number,
default: 1000
},
easing: { // 动画缓动效果
type: String,
default: 'ease'
},
distance: { // 动画距离
type: Number,
default: 100
}
},
data() {
return {
hasAnimated: false // 是否已经动画过
}
},
computed: {
computedStyle() {
return {
opacity: this.hasAnimated ? 1 : 0,
transform: this.hasAnimated ? 'translateY(0)' : `translateY(${this.distance}px)`,
transition: `transform ${this.duration}ms ${this.easing}, opacity ${this.duration}ms ${this.easing}`
}
}
},
mounted() {
if (typeof window !== 'undefined' && 'IntersectionObserver' in window) { // 检测是否支持IntersectionObserver
this.createObserver() // 创建IntersectionObserver
} else {
// 如果不支持IntersectionObserver,则使用scroll事件来实现动画
this.observeScroll()
}
},
methods: {
createObserver() {
const observer = new IntersectionObserver(entries => { // IntersectionObserver回调函数
entries.forEach(entry => { // 遍历每个观察目标
if (entry.isIntersecting && !this.hasAnimated) { // 如果目标进入视口并且没有动画过
this.hasAnimated = true // 标记动画过
observer.unobserve(entry.target) // 停止观察
}
})
}, { threshold: 0.1 }) // 观察阈值,表示目标在视口的百分比
observer.observe(this.$refs.animatedElement) // 观察目标
},
observeScroll() {
const onScroll = () => { // scroll事件回调函数
if (this.isInViewport(this.$refs.animatedElement) && !this.hasAnimated) { // 如果目标在视口并且没有动画过
this.hasAnimated = true // 标记动画过
window.removeEventListener('scroll', onScroll) // 停止监听scroll事件
}
}
window.addEventListener('scroll', onScroll) // 监听scroll事件
},
isInViewport(el) { // 判断目标是否在视口
const rect = el.getBoundingClientRect()
return rect.top < window.innerHeight && rect.bottom > 0
}
}
}
</script>
页面使用
<div class="text-slide-in-vue">
<slide-comp v-for="(s ,idx) in list" :key="idx">
<p>{{ s.text }} - slide-comp</p>
</slide-comp>
</div>
<div class="level-slide">
<slide-comp v-for="(s, idx) in list" :key="idx" :duration="500 * idx + 500">
<p>{{ s.text }} - slide-comp</p>
</slide-comp>
</div>
<style>
.text-slide-in-vue {
p {
text-align: center;
width: 400px;
height: 200px;
background-color: goldenrod;
margin: 0 auto;
margin-top: 20px;
}
}
.level-slide {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 20px;
p {
text-align: center;
width: 200px;
height: 200px;
background-color: blueviolet;
margin: 0 auto;
margin-top: 20px;
}
}
</style>
来源:juejin.cn/post/7401042923490836480
低谷期,什么是最好的理财方式?买房、炒股、存钱?
2024年宏观环境肉眼可见地恶化之下,程序员等普通人如何度过这次危机?
如何度过危机?对于个人最优的方案,是郭嘉最不想看到的方案
对于普通人,度过危机最好的办法是 降低消费、降低负债和多存钱。对于郭嘉而言,这是最不想看到的行为。 个人的最优方案和郭嘉的最优方案是相反的。 在经济下行期,郭嘉和个人的利益不一致也很正常。(上行期当然一致了)
记住这一点,不要认为郭嘉都已经提倡了,就认为对自己是最好的,作为理智的成年人要有独立判断能力。
郭嘉希望大家多带款、多消费、少存钱,只有如此需求端提振后,经济才能复苏。但是对于个人而言,外部环境的危机让我们对未来充满不安全感,多带款、多消费、少存钱就是作死的行为……
非必要不要买房
在中国34个省市区和直辖市,我相信绝大部分城市的房产已经是垃圾资产,拿到手里就会成为传家宝,可能永远也卖不出去。
只有极少数一线城市和 优质地段、优质物业、优质小区质量的少量小区或者别墅区存在增值空间。参照日本的经验,经济泡沫破裂后,虽然人口快速向一线城市群东京和大阪聚集,但是东京的房价依然持续在下跌
记住下跌趋势不要抄底,宁可追高,绝不抄底!
务必远离股市,尤其是A股
这是我的炒股心路历程,虽然赚了钱,但是差一点点就倾家荡产,万劫不复
虽然炒股赚了十多万,但差点倾家荡产!劝你别入坑
2019年,我开始涉足股市,在2021年中旬为了购房,将持有的股票全部卖出,赚了十多万元。在最高峰时期,我获利超过了二十多万元,但后来又回吐了一部分利润。虽然我的炒股成绩不是最出色的,但也超过了很多人。因为大多数股民都是亏损的,能够在股市长期盈利的人真的是凤毛麟角。
股市中普遍流传的七亏二平一赚的说法并不只是传闻,事实上,现实中的比例更加残酷,能够长期赚钱的人可能连10%都达不到。
炒股就是赌博
我想告诉大家,无论你在股市赚了多少钱,迟早都会还回去,越炒股越上头,赚的越多越上头。
赌徒不是一天造成的,谁都有赢的时候,无论赚多少,最终都会因为人性的贪婪 走上赌徒的道路。迟早倾家荡产。
之前炒股的经历分享
虽然炒股赚了十多万,但差点倾家荡产!劝你别入坑
多存钱,降低杠杆和负债率
虽然存钱的利率不到2%,但是总比亏本强很多啊,要明白现在中国的经济困境是 经济通缩,什么意思呢?就是各种工业品都在降价(例如汽车一直在降价),未来的钱更加值钱,所以利率低,希望大家赶紧花出去。
经济通胀的时候,物价在飞速上涨,虽然利率高,但是物价涨得更高,钱越来越不值钱,所以银行希望大家都去存钱。
记住银行希望你干什么,大概率对你是不好的。 银行赚的就是你的钱~
现在越是利率低,越是要存在银行这样才保险。 如果你的路子特别野,可以考虑将资产转成美元或港币,这两种货币的存款利率更加高,可以达到4%+
有房贷的提前还房贷
存量房贷的利率比存款利率高了将近3%, 100万的带款,每年就多3万元的利息,长达30年,将近五六十万元的利差,千万不可小觑。
但是银行会有各种手段限制大家提前还房贷。归根结底,经济下行期,银行的利益和个人的利益是对立的~
第一次提前还房贷,就尝到了甜头,使用6万块钱,起到了18万的效果
我在工行App上,申请 提前还贷,选择缩短 18个月的房贷,只需要 6万2,而我每个月房贷才1万,相当于是用 6 万 顶 18 万的房贷。还有比这更划算的事情吗?
提前还房贷的经历
买房后,害怕失业,更不敢裸辞,心情不好就提前还房贷,缓解焦虑
对于房奴而言,提前还房贷就是最好的投资方式,没有之一,就是最好的投资方式。
欲买桂花同载酒,人生要及时行乐
虽然推荐大家降低消费,但是不建议大家为了省钱,牺牲青春。
100块钱对于一个10岁孩子的快乐,和对于30岁成年人的快乐是完全不对等的。
小时候有10块钱,够我买三四个玩具,可以和小伙伴开开心心的玩一个暑假。现在我有1000张10块钱,也找不回儿时的快乐。
人活着是为了享受人生的,不是为了受罪来的。建议大家 可持续性的及时行乐,该玩还是要玩。不要老了感慨道:欲买桂花同载酒,终不似,少年游
好好学习,提高自己,度过危机期,遍地是机会
经济危机过后,资产的价格一定一落千丈,各行各业都非常萧条,但是随着需求复苏,这意味着遍地都是机会。前提是你有发现机会的眼光、抓住机会的能力和勇于行动的魄力。
想一想,危机过后,我们手握大量的现金,面对遍地的廉价资产,面对日渐热情的消费需求,再加上更加成熟强大的自我,一定大有可为。 前提是好好存钱,好好积累提高自己。(如何判断危机过去,是一门学问,不要太冲动)
要想做到这一切,一定要注重低谷期、危机期的积累。
祝各位长期有耐心,把未来的信心全部放在投资自己上,不要把未来的信心投资在股票和、产和奢侈消费上哦~
祝未来的大家纵情四海、前途似锦
2024 七夕随笔
来源:juejin.cn/post/7402141246176428095
手把手教你打造一个“蚊香”式加载
前言
这次给大家带来的是一个类似蚊香加载一样的效果,这个效果还是非常具有视觉欣赏性的,相比前几篇文章的CSS
特效,这一次的会比较震撼一点。
效果预览
从效果上看感觉像是一层层蚊香摞在一起,通过动画来使得它们达到3D金钟罩的效果。
HTML布局
首先我们通过15个span
子元素来实现金钟罩的每一层,用于创建基本结构。从专业术语上讲,每个span
元素都代表加载动画中的一个旋转的小点。通过添加多个span
元素,可以创建出一串连续旋转的小点,形成一个加载动画的效果。
<div class="loader">
<span></span>
// 以下省略15个span元素
</div>
CSS设计
完成了基本的结构布局,接下来就是为它设计CSS
样式了。我们一步一步来分析:
首先是类名为loader
的CSS
类,相关代码如下。
.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;
}
......
以下省略到第15个span元素
第一个span
元素的样式设置了top、left、bottom和right
属性为0,这意味着它会填充父元素的整个空间。它还设置了animation-delay
属性为1.4秒,表示在加载动画开始之后1.4秒才开始播放动画。
后面14个span
元素都是按照这个道理,以此类推即可。通过给span
元素的动画延迟属性的不同设置,可以实现加载动画的错落感和流畅的过渡效果。
总结
以上就是整个效果的实现过程了,通过设计的动画来实现这个蚊香式加载,整体还是比较简单的。大家可以去码上掘金看看完整代码,然后自己去尝试一下,如果有什么创新的地方或者遇到了什么问题欢迎在评论区告诉我~
来源:juejin.cn/post/7291951762948259851
2024 年排名前 5 的 Node.js 后端框架
自 2009 年以来,Node.js 一直是人们谈论的话题,大多数后端开发人员都倾向于使用 Node.js。在过去的几年里,它的受欢迎程度有所增加。它被认为是美国最受欢迎的网络开发工具,包括 Netflix 和 PayPal 等客户。
受欢迎程度增加的原因是加载时间的减少和性能的提高。因此,分析 2024 年排名前 5 的 Node.js 后端框架至关重要。本文将介绍 2024 年排名前 5 的 Node.js 后端框架、它们的功能和常见用例。
Express.js:久经考验的冠军
Express.js 是 Node.js 最著名的后端框架之一。它是一个开源 Web 应用程序框架,可免费使用并构建在 Node.js 平台上。由于它是一个最小的框架,新手和经验丰富的 Web 开发人员都倾向于使用 Express.js。它主要用于创建 Web 应用程序和 RESTful API。
高效路由:Express.js 提供了一种干净、简单的方法来管理各种 HTTP 请求并将它们分配给特定任务,让我们看一个例子。
中间件支持:Express.js 允许中间件支持处理 HTTP 请求。让我们看一个创建用于记录 HTTP 请求详细信息的中间件的简单示例。
轻松的数据库集成:Express.js 与数据库无关。它不强制执行特定的数据库选择。开发者可以选择自己喜欢的数据库。将数据库与 Express.js 集成很容易,因为它具有模块化和灵活的特性,以及提供数据库连接的丰富的 npm 包生态系统。
简单易学:Express.js 以其简单和简约的设计而闻名,这使得开发人员很容易学习,特别是如果他们已经熟悉 JavaScript 和 Node.js。
另外,您可以使用 Bit 等工具轻松开始使用 Express.js 。如果您以前没有使用过 Bit,那么它是可组合软件的下一代构建系统。Express.js 本身本质上是可组合的,您可以在应用程序中的任何位置即插即用组件。
Nest.js:现代且结构化的方法
Nest.js 是一个以构建可扩展且高效的 Node.js 服务器端应用程序而闻名的框架。它使用渐进式 JavaScript,并具有用 TypeScript 编写代码的能力。尽管它完全支持 TypeScript,但它可以用纯 JavaScript 编写代码,包括面向对象编程、函数式编程和函数式响应式编程。
模块化:Nest.js 允许将代码分解为单独的可管理模块,从而使其更易于维护。让我们看一下下面的模块。
这个 PaymentModule 可以导出到其他模块。在此示例中,我们在该模块内导出了通用的缓存模块。由于 Nest.js 具有模块结构,因此易于维护。
可扩展:Nest.js 通过将应用程序分解为可管理的模块、支持灵活的组件替换以及通过微服务和异步操作容纳高流量来实现无缝扩展。它确保有效处理增加的工作量,同时保持可靠性。
依赖注入:依赖注入只是向类添加外部依赖项的方法,而不是在类本身中创建它。让我们看一个例子。
我们创建 PaymentService 并添加了 @Injectable()
注释以使其可注入。我们可以使用创建的服务,如下所示。
类型安全:Nest.js 使用 TypeScript 提供类型安全,可用于捕获开发过程中潜在的错误并提高代码的可维护性。
Koa.js:优雅且轻量
Koa.js 是一个更小、更具表现力的 Web 框架,也是由 Express.js 团队设计的。它允许您通过利用异步函数来放弃回调并处理错误。
上下文对象(ctx):Koa.js 包含一个名为 ctx 的功能来捕获请求和响应详细信息。该上下文被传递给每个中间件。在此示例中,我们从 ctx 对象记录了method 和 request。
中间件组成:与 Express.js 非常相似,Koa 支持处理 HTTP 请求和响应的中间件功能。在此示例中,我们创建了一个简单的中间件。
async/await 支持:Koa 使用 async/await 语法以更同步的方式编写异步代码。下面的示例包含使用 async/await 关键字。
Hapi.js
Hapi.js 是 Http-API 的缩写,是一个用于开发可扩展 Web 应用程序的开源框架。Hapi.js 最基本的用例之一是构建 REST API。沃尔玛实验室创建了 hapi js 来处理黑色星期五等活动的流量,黑色星期五是美国日历上在线购物最繁忙的日子之一。
配置驱动设计:使用配置对象,我们可以在 Hapi.js 中配置路由、设置和插件。
强大的插件系统:Hapi.js 允许轻松集成插件,让我们看一个例子。在这个例子中,我们集成了两个插件,可以使用 key 将选项传递给插件 options
。
认证与授权:Hapi.js 提供了对各种身份验证策略的内置支持,并允许开发人员轻松定义访问控制策略。
输入验证:输入验证是 Hapi.js 的另一个重要方面。在 options 路由的对象中,我们可以定义哪些输入需要验证。默认 validate 对象由以下值组成。
Adonis.js
Adonis.js 是 Node.js 的全功能 MVC 框架。它具有构建可扩展且可维护的应用程序的能力。 Adonis.js 遵循与 Laravel 类似的结构,并包含 ORM、身份验证和开箱即用的路由等功能。
全栈 MVC 框架:Adonis.js 遵循 MVC 架构模式。拥有 MVC 框架有助于组织代码并使其更易于维护和扩展。
数据库集成 ORM:Adonis.js 有自己的 ORM,称为 Lucid。 Lucid 提供了一个富有表现力的查询构建器并支持各种数据库系统。在 Lucid 中,我们可以创建模型来读取和写入数据库。让我们看下面的例子。
认证系统:Adonis.js 内置了对用户身份验证和授权的支持。它提供了一组用于处理用户会话、密码散列和访问控制的方法和中间件。
结论
2024年,上述后端框架在市场上屹立不倒。无论您选择 Express.js 是为了简单,Nest.js 是为了结构,Adonis.js 是为了生产力,还是 Koa.js 是为了优雅,选择正确的框架至关重要。这始终取决于您的要求。
了解您的项目需求至关重要,并在此基础上选择合适的框架。此外,寻找最新趋势、现有框架的新功能和新框架对于 2024 年后端开发之旅的成功至关重要。
来源:juejin.cn/post/7350581011262373928
BOE·IPC电竞大赛暨BOE无畏杯S2完美收官 BOE(京东方)竖立电竞产业生态新标杆
在开幕环节,高文宝博士表示:“电竞是年轻人的活动,年轻人有着活跃的思想和强大的创造力。近年来,BOE(京东方)通过BOE无畏杯和ChinaJoy等活动加深了和年轻人的沟通,与年轻人成为了真诚的伙伴和挚友,在这个过程中,BOE(京东方)也激发了新的创造灵感,做出更好更惊艳的产品。未来BOE(京东方)还会持续在技术、产品、活动等方面,与合作伙伴一起带来异彩纷呈的电竞体验,带动电竞产业链的价值提升,助力中国电竞再创高峰。”
今年举办的BOE无畏杯总决赛活动上,BOE(京东方)还特别打造"Best of Esports电竞高阶联盟"产品展区,集中展现了联盟伙伴们最新的电竞产品和尖端技术。依托于BOE(京东方)自主研发的ADS Pro、a-MLED等创新技术赋能,AGON爱攻 AG275QZW显示器支持260Hz超高刷新率,以1ms GTG疾速响应时间为玩家提供高帧率、低延迟的游戏画质,确保流畅丝滑的游戏体验;ROG枪神8 Plus超竞版笔记本,支持60-240Hz动态调频刷新率及3ms极速响应,玩家操作无比顺畅……一系列电竞黑科技产品凭借高清流畅的显示画面和酷炫的科技外观吸引了现场粉丝纷纷体验,为观众们呈现了一场融合竞技与科技的盛宴。
总决赛现场更是异彩纷呈,现场Coser开场秀、无畏契约水友赛等丰富的互动环节点燃了现场观众的热情,更有BOE无畏契约战队对战JDG无畏契约战队表演赛,BOE战队面对职业战队分毫不让、竞出风采,让决赛前的氛围达到了高潮。在总决赛启动仪式上,BOE(京东方)副总裁刘毅、虎牙直播商业化副总裁焦阳、京东集团3C数码事业群电脑组件业务部总经理蔡欣洋一起揭开BOE无畏杯《无畏契约》2024挑战赛总决赛的帷幕,总决赛最终在上一届亚军津门飞鹰战队与CCG战队的较量中展开对决,经过3局激战,CCG队获得最终胜利,拿下本届赛事的冠军。
多年来,BOE(京东方)以技术创新为驱动,通过高刷新率、护眼科技等技术产品优势、广泛的合作以及强大的品牌影响力,从技术、产品、生态等多个方面助力电竞产业发展,获得了众多全球一线客户的支持和好评,引领了整个电竞产业的升级和变革。未来,BOE(京东方)将继续秉持"Powered by BOE"的生态理念,充分发挥"Best of Esports电竞高阶联盟"在全业态布局、资源聚合和技术领先等方面的优势,通过持续不断的技术创新和产业链整合,为我国电竞生态贡献力量,为数字经济的高质量发展注入新的动力。
收起阅读 »uni-app微信小程序动态切换tabBar,根据不同用户角色展示不同的tabBar
前言
在UniApp的开发小程序过程中,为了针对不同角色用户登录后的个性化需求。通过动态权限配置机制,能够根据用户的角色展示不同的TabBar。此项目是通过Uni-App命令行的方式搭建的
Vue3+Vite+Ts+Pinia+Uni-ui
的小程序项目
最终效果
- 1、司机角色:
- 2、供应商角色:
- 3、司机且供应商角色:
目前常规的实现方式,大多数都是封装一个tabbar
组件,在需要显示tabbar的页面添加这个组件,在根据一个选中的index值来切换选中效果。
而我的实现方式:把所有有
tabbar
的页面全部引入在一个tabbarPage
页面,根据角色userType
,来动态显示页面
实现思路
1、常规登录:通过微信登录获取code
2、根据code获取openId
3、根据openId获取token,若token存在表:此用户已经登陆/绑定过,则根据token获取用户信息,根据角色直接进入项目页面;若token不存在,则跳转到登录页面
4、登录成功后,调用用户信息接口,根据角色直接进入项目页面
1、以下是封装了一个useLogin的hooks
export const useLogin = () => {
const { proxy } = getCurrentInstance() as any
//常规登录
const judgmentLogin = () => {
uni.login({
provider: 'weixin', //使用微信登录
success: async (loginRes) => {
// 根据微信登录的code获取openid
const res = await proxy.$api.getOpenid({ code: loginRes.code })
if (res.success) {
// console.log('res.data.openid----', res.data.openId)
// 根据openid获取token
openidLogin(res.data.openId)
// 存储openid
uni.setStorageSync('openId', res.data.openId)
}
}
});
}
// 登录过的用户再次进入根据openid获取token,有token则直接进入当前用户的页面,没有则进入登录页面
const openidLogin = (async (openId: string) => {
// console.log('openId----', openId)
const res = await proxy.$api.openIdLogin({ openId })
if (res.success) {
if (res.data) {
// 存储token
uni.setStorageSync('token', res.data)
userInfo(openId)
} else {
uni.navigateTo({
url: '/pages/login/login'
})
}
}
})
// 登录成功后(有token后)根据openid获取用户信息
const userInfo = (async (openId: any) => {
const res = await proxy.$api.getUserInfo({ openId })
if (res.success) {
console.log('获取登陆用户信息', res.data)
uni.setStorageSync('userInfo', JSON.stringify(res.data))
const userTypeList = ['scm_driver', 'scm_supplier', 'supplier_and_driver']
// 遍历角色数组来存储当前用户的角色。此角色为userTypeList中的某一个并且此数组只能存在一个userTypeList里面的角色,不会同时存在两个
res.data.roles.map((item: any) => {
if (userTypeList.includes(item.roleKey)) {
uni.setStorageSync('userType', item.roleKey)
}
})
// 判断角色数组中只要有一个角色在userTypeList中,则进入当前用户的角色页面,否则进入无权限页面
const flag = res.data.roles.some((item: any) => {
return userTypeList.includes(item.roleKey)
})
// console.log('flag----', flag)
if (flag && userTypeList.includes(uni.getStorageSync('userType'))) {
setTimeout(() => {
uni.reLaunch({
url: '/pages/tabbarPage/tabbarPage'
})
}, 500)
} else {
uni.showToast({
icon: 'none',
title: '当前用户角色没有权限!'
})
}
}
})
return {
judgmentLogin,
userInfo
}
}
2、修改page.json中的tabBar
"tabBar": {
"color": "#a6b9cb",
"selectedColor": "#355db4",
"list": [
{
"pagePath": "pages/supplierMyorder/supplierMyorder"
},
{
"pagePath": "pages/driverMyorder/driverMyorder"
},
{
"pagePath": "pages/mycar/mycar"
},
{
"pagePath": "pages/driverPersonal/driverPersonal"
}
]
},
3、关键页面tabbarPage.vue
<template>
<div class="tabbar_page flex-box flex-col">
<div
class="page_wrap"
v-if="userType === 'scm_supplier'"
v-show="active === 'supplierMyorder'"
>
<supplier-myorder
ref="supplierMyorder"
:show="active === 'supplierMyorder'"
/>
div>
<div
class="page_wrap"
v-if="userType === 'scm_supplier'"
v-show="active === 'supplierPersonal'"
>
<supplier-personal
ref="supplierPersonal"
:show="active === 'supplierPersonal'"
/>
div>
<div
class="page_wrap"
v-if="userType === 'scm_driver'"
v-show="active === 'driverMyorder'"
>
<driver-myorder ref="driverMyorder" :show="active === 'driverMyorder'" />
div>
<div
class="page_wrap"
v-if="userType === 'scm_driver'"
v-show="active === 'mycar'"
>
<mycar ref="mycar" :show="active === 'mycar'" />
div>
<div
class="page_wrap"
v-if="userType === 'scm_driver'"
v-show="active === 'driverPersonal'"
>
<driver-personal
ref="driverPersonal"
:show="active === 'driverPersonal'"
/>
div>
<div
class="page_wrap"
v-if="userType === 'supplier_and_driver'"
v-show="active === 'supplierMyorder'"
>
<supplier-myorder
ref="supplierMyorder"
:show="active === 'supplierMyorder'"
/>
div>
<div
class="page_wrap"
v-if="userType === 'supplier_and_driver'"
v-show="active === 'driverMyorder'"
>
<driver-myorder ref="driverMyorder" :show="active === 'driverMyorder'" />
div>
<div
class="page_wrap"
v-if="userType === 'supplier_and_driver'"
v-show="active === 'mycar'"
>
<mycar ref="mycar" :show="active === 'mycar'" />
div>
<div
class="page_wrap"
v-if="userType === 'supplier_and_driver'"
v-show="active === 'supplierPersonal'"
>
<supplier-personal
ref="supplierPersonal"
:show="active === 'supplierPersonal'"
/>
div>
<view class="tab">
<view
v-for="(item, index) in tabbarOptions"
:key="index"
class="tab-item"
@click="switchTab(item, index)"
>
<image
class="tab_img"
:src="currentIndex == index ? item.selectedIconPath : item.iconPath"
>image>
<view
class="tab_text"
:style="{ color: currentIndex == index ? selectedColor : color }"
>{{ item.text }}
view>
view>
div>
template>
<script lang="ts" setup>
import supplierMyorder from '@/pages/supplierMyorder/supplierMyorder.vue'
import supplierPersonal from '@/pages/supplierPersonal/supplierPersonal.vue'
import driverMyorder from '@/pages/driverMyorder/driverMyorder.vue'
import mycar from '@/pages/mycar/mycar.vue'
import driverPersonal from '@/pages/driverPersonal/driverPersonal.vue'
let color = ref('#666666')
let selectedColor = ref('#355db4')
let currentIndex = ref(0)
const active = ref('')
const switchTab = (item: any, index: any) => {
// console.log('tabbar----switchTab-----list', item, index)
currentIndex.value = index
active.value = item.name
}
onLoad((option: any) => {
currentIndex.value = option.index || 0
active.value = option.name || tabbarOptions.value[0].name
})
onShow(() => {
active.value = active.value || tabbarOptions.value[0].name
currentIndex.value = currentIndex.value || 0
})
const userType = computed(() => {
return uni.getStorageSync('userType')
})
const tabbarOptions = computed(() => {
return {
scm_supplier: [
{
name: 'supplierMyorder',
pagePath: '/pages/supplierMyorder/supplierMyorder',
iconPath: '/static/tabbar/order.png',
selectedIconPath: '/static/tabbar/order_active.png',
text: '我的订单'
},
{
name: 'supplierPersonal',
pagePath: '/pages/supplierPersonal/supplierPersonal',
iconPath: '/static/tabbar/my.png',
selectedIconPath: '/static/tabbar/my_active.png',
text: '个人中心'
}
],
scm_driver: [
{
name: 'driverMyorder',
pagePath: '/pages/driverMyorder/driverMyorder',
iconPath: '/static/tabbar/waybill.png',
selectedIconPath: '/static/tabbar/waybill_active.png',
text: '我的运单'
},
{
name: 'mycar',
pagePath: '/pages/mycar/mycar',
iconPath: '/static/tabbar/car.png',
selectedIconPath: '/static/tabbar/car_active.png',
text: '我的车辆'
},
{
name: 'driverPersonal',
pagePath: '/pages/driverPersonal/driverPersonal',
iconPath: '/static/tabbar/my.png',
selectedIconPath: '/static/tabbar/my_active.png',
text: '个人中心'
}
],
supplier_and_driver: [
{
name: 'supplierMyorder',
pagePath: '/pages/supplierMyorder/supplierMyorder',
iconPath: '/static/tabbar/order.png',
selectedIconPath: '/static/tabbar/order_active.png',
text: '我的订单'
},
{
name: 'driverMyorder',
pagePath: '/pages/driverMyorder/driverMyorder',
iconPath: '/static/tabbar/order.png',
selectedIconPath: '/static/tabbar/order_active.png',
text: '我的运单'
},
{
name: 'mycar',
pagePath: '/pages/mycar/mycar',
iconPath: '/static/tabbar/car.png',
selectedIconPath: '/static/tabbar/car_active.png',
text: '我的车辆'
},
{
name: 'supplierPersonal',
pagePath: '/pages/supplierPersonal/supplierPersonal',
iconPath: '/static/tabbar/my.png',
selectedIconPath: '/static/tabbar/my_active.png',
text: '个人中心'
}
]
}[userType.value]
})
script>
<style lang="scss" scoped>
.tabbar_page {
height: 100%;
.page_wrap {
height: calc(100% - 84px);
&.hidden {
display: none;
}
}
.tab {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 100rpx;
background: white;
display: flex;
justify-content: center;
align-items: center;
padding-bottom: env(safe-area-inset-bottom); // 适配iphoneX的底部
.tab-item {
flex: 1;
text-align: center;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
.tab_img {
width: 45rpx;
height: 45rpx;
}
.tab_text {
font-size: 25rpx;
margin: 9rpx 0;
}
}
}
}
.flex-box {
display: -webkit-box;
display: -webkit-flex;
display: flex;
}
.flex-col {
flex-direction: column
}
style>
来源:juejin.cn/post/7372366198099886090
聊聊 CSS 的 ::marker
::marker
是一个 CSS 的另一个伪元素,有点类似于 CSS 的 ::before
和 ::after
伪元素。只不过,它常用于给列表标记框定制样式。简而言之,使用::marker
伪元素,可以对列表做一些有趣的事情,在本文中,我们将深入的聊聊该伪元素。
初识 CSS 的 ::marker
::marker
是 CSS 的伪元素,现在被纳入到 CSS Lists Module Level 3 规范中。在该规范中涵盖了列表和计算数器相关的属性,比如我们熟悉的list-style-type
、list-style-position
、list-style
、list-item
、counter-increment、counter-reset、counter()和counters()
等属性。
在 CSS 中 display 设置 list-item
值之后就会生成一个 Markers 标记以及控制标记位置和样式的几个属性,而且还定义了计数器(计数器是一种特殊的数值对象),而且该计数器通常用于生成标记(Markers)的默认内容。
一时之间,估计大家对于Markers标记并不熟悉,但对于一个列表所涉及到的相关属性应该较为熟悉,对于一个CSS List,它可以涵盖了下图所涉及到的相关属性:
如果你对CSS List中所涉及到的属性不是很了解的话,可以暂时忽略,随着后续的知识,你会越来越清楚的。
解构一个列表
虽然我们在 Web 的制作中经常会用到列表,但大家可能不会过多的考虑列表相关的属性或使用。就 HTML语义化出发,如果遇到无序列表的时候会使用
- ,遇到有序列表的时候会使用
- 非列表项
li
元素需要显式的设置display:list-item
(内联列表项需要使用display: inline list-item
) - 需要显式设置
list-style-type
为none
- 使用
content
添加内容(也可以通过attr()
配合data-*
来添加内容) counter-reset
:设置一个计数器,定义计数器名称,用来标识计数器作用域counter-set
:将计数器设置为给定的值。它操作现有计数器的值,并且只在元素上还没有给定名称的计数器时才创建新的计数器counter-increment
:用来标识计数器与实际关联元素范围,可接受两个值,第一个值必须是counter-reset
定义的标识符,第二个值是可选值,是一个整数值(正负值都可以),用来预设递增的值counter()
:主要配合content
一起使用,用来调用定义好的计数器标识符counters()
:支持嵌套计数器,如果有指定计数器的当前值,则返回一个表示这些计数器的当前值的串联字符串。counters()
有两种形式counters(name, string)
和counters(name, string, style)
。通常和伪元素一起使用,但理论上可以支持
值的任何地方使用- 调整HTML结构
- 伪元素
::before
和content
- 伪元素
::marker
和content
,但在有些场景(或不追求语义化的同学)会采用其他的标签元素,比如说
。针对这个场景,会采用 display
设置为list-item
。如此一来会创建一个块级别的框,以及一个附加的标记框。同时也会自动增加一个隐含列表项计算数器。ul
和 ol
元素默认情况之下会带有list-style-type
、list-style-image
和list-style-position
属性,可以用来设置列表项的标记样式。同样的,带有display:list-item
的元素生成的标记框,也可以使用这几个属性来设置标记项样式。
list-style-type
的属性有很多个值:
取值不同时,列表符号(也就是Marker标识符)会有不同的效果,比如下面这个示例所示:
Demo 地址:codepen.io/airen/full/…
在 CSS 中给列表项设置类型的样式风格可以通过 list-style-type
和 list-style-image
来实现,但这两个属性设置列表项的样式风格会有所限制。比如要实现类似下图的列表项样式风格:
值得庆幸的是,CSS 的 ::marker
给予我们更大的灵活性,可以让我们实现更多的列表样式风格,而且灵活性也更大。
创建 marker 标记框
HTML 中的 ul
和 ol
元素会自动创建 marker
标记框。如果通过浏览器调试器来查看的话,你会发现,不管是 ul
还是 ol
的子元素 li
会自带 display:list-item
属性设计(客户端默认属性),另外会带有一个默认的list-style-type
样式设置:
这样一来,它自身就默认创建了一个 marker
标记框,同时我们可以通过 ::marker
伪元素来设置列表项的样式风格,比如下面这个示例:
ul ::marker,
ol ::marker {
font-size: 200%;
color: #00b7a8;
font-family: "Comic Sans MS", cursive, sans-serif;
}
你会看到效果如下所示:
Demo 地址:codepen.io/airen/full/…
对于非列表元素,可以通过display: list-item
来创建 Marker 标记,这样就可以在元素上使用 ::marker
伪元素来设置项目符号的样式。虽然通过display:list-item
在形式上看上去像列表项,但在语义化上并没有起到任何的作用。
在深入探讨 ::marker
使用之前,大家要知道,元素必须要具备一个Marker标记框,对于非列表项的元素需要显式的使用 display:list-item
来创建Marker标记框。
CSS的display属性是一个非常重要的属性,现在被纳入在CSS Display Module Level 3中。CSS的display
属性可以改变任何一个元素的框模型。而且在Level 3规范中给display
引用了两个值的语法,比如使用display: inline list-item
可以创建一个内联列表项。
::marker
的基本使用
前面的小示例中,其实我们已经领略到了::marker
的魅力。在列表项li
中,其实已经带有Marker标记框,可以借助::marker
伪元素来设置列表标记的样式。
我们先来回忆一下,CSS的::marker
还未出现(或者说不支持的浏览器)时,要对列表项设置不同的样式,都是通过li
上来控制(看上去继承了li
上的样式)。虽然能设置列表样式,但还是具有一定的局限性,灵活度不够大 —— 特别是当列表项标记样式和内容要区分时。
CSS的::marker
会让我们变得容易的多。从前面的示例中我们可以了解到, ::marker
伪元素和列表项内容是分开的,正因此,我们可以独立为两个部分设计不同的样式。这在以前的CSS版本中是不可能的(除非借助::before
伪元素来模拟,稍后也会介绍这一部分)。比如说,我们更改ul
或li
的color
或font-size
时也会更改标记的color
和font-size
。为了达到两者的区分,往往需要在HTML中做一些结构上的调整,比如列表项用一个子元素来包裹(比如span
元素或::before
伪元素)。
更了大家更易于理解::marker
的作用,我们在上面的示例基础上做一些调整:
.box:nth-child(odd) li {
font-size: 200%;
color: #00b7a8;
font-family: "Comic Sans MS", cursive, sans-serif;
}
.box:nth-child(even) ::marker {
font-size: 200%;
color: #00b7a8;
font-family: "Comic Sans MS", cursive, sans-serif;
}
代码中的具体作用不做介绍,很简单的代码,但效果却有很大的差异性,如下图所示:
很神奇吧!在浏览器中查看源码,你会发现使用::marker
和未使用::marker
的差异性:
虽然::marker
易于帮助我们控制标记的样式风格,但有一点需要特别注意,如果显式的设置了list-style-type: none
时,::marker
标记内容就会丢失不可见。在这个时候,不管是否显式的设置了::marker
的样式都将会看不到。比如:
大家是否还记得,在::marker
还没有出现之前,要对列表项设置别的标记符,比如Emoji。我们就需要通过别的方式来完成,最为常见的是修改HTML的结构,或者借助CSS伪元素::before
和CSS的content
属性,例如下面这个示例:
Demo 地址:codepen.io/airen/full/…
事实上,CSS的::marker
和伪元素::before
类似,也可以通过content
和attr()
一起来控制Marker标记的效果。需要记住,生成个性化Marker标记内容需要做到几下几点:
来看一个小示例:
li::marker {
content: attr(data-emoji);
}
::marker
伪元素自从可以使用content
来添加内容之后,让我们可操作的空间更大了。对于列表标记(即,带有Marker标记)的元素再也不需要额外的通过::before
伪元素和content
来生成标记内容。而且,我们还可以结合计算数器相关的特性,让列表标记可造性空间更大。如果你感兴趣的话,请继续往下阅读。
::marker
与计数器的结合
对于无序列表,或者说统一使用同样的标记符,那么::marker
和content
结合就可以解决。但是如果面对的是一个有顺列表,那么我们就需要用到CSS计数器的相关特性。
先来回忆一下CSS的计数器相关的特性。在CSS中计数器有三个属性:
以及两个相关的函数:
一般情况之下,
counter-reset
、counter-increment
和counter()
即可满足一个计数器的需求。
CSS的计数器使用非常的简单。在元素的父元素上显式设置:
body {
counter-reset: section
}
使用counter-reset
声明了一个计数器标识符叫section
。然后再需要使用计算器的元素上(一般配合伪元素::before
)使用counter-increment
来调用counter-reset
已声明的计数器标识符,然后使用counter(section)
来计数:
h3::before {
counter-increment: section
content: "Section " counter(section) ": "
}
下图会更详尽一些,把计数器可能会用的值都罗列出来了,可供参考:
回到我们的列表设置中来。::marker
还没有得到浏览器支持之前,一般都是使用CSS的计数器来实现一些带有个性化的有顺序列表,比如下面这样的效果:
也可以借助计数器做一些其他的效果比如:
Demo 地址:codepen.io/snookca/ful…
更为厉害的是,CSS的计数器配合复选框或单选按钮还可以做一些小游戏,比如 @una教程中向我们展示的一个效果:
Demo 地址:codepen.io/jak_e/full/…
@kizmarh使用同样的原理,做了一个黑白棋的小游戏:
是不是很有意思,有关于CSS计数器相关的特性暂且搁置。我们回到::marker
的世界中来。
::marker
配合content
可以定制个性化Marker标记风格。借助CSS计数器,可以更轻易的构建带有顺序的Marker标记。同样可以让Marker标记和内容分离。更易于实现可定制化的样式风格。
接下来,我们来看一个简单的示例,看看::marker
生成的标记符和以往生成的标记符效果上有何差异没。
结果很简单,这里使用的是一个无序列表:
<ul>
<li>
Item1
<ul>
<li>Item 1-1li>
<li>Item 1-2li>
<li>Item 1-3li>
ul>
li>
<li>Item2li>
<li>Item3li>
<li>Item4li>
<li>Item5li>
ul>
你可以根据自己的爱好来选择标签元素。先来看::before
和content
配合counter()
和counters()
的一个效果:
/* counter() */
.box:nth-child(1) {
ul {
counter-reset: item;
}
li {
counter-increment: item;
&::before{
content: counter(item);
/* ... */
}
}
}
/* counters() */
.box:nth-child(2) {
ul {
counter-reset: item;
}
li {
counter-increment: item;
&::before{
content: counters(item, '.');
/* ... */
}
}
}
对于上面的效果,大家可能也猜到了。我们再来看一下::marker
的使用:
/* counter() */
.box:nth-child(3) {
ul {
counter-reset: item;
}
li {
counter-increment: item;
}
::marker {
content: counter(item);
/* ... */
}
}
/* counters() */
.box:nth-child(4) {
ul {
counter-reset: item;
}
li {
counter-increment: item;
}
::marker {
content: counters(item, '.');
/* ... */
}
}
可以看到::marker
和前面::before
效果是一样的:
另外使用::marker
还有一特殊之处。不管是列表元素还是设置了display:list-item
的非列表元素,不需要显式的使用counter-reset
声明计数器标识符,也无需使用counter-increment
调用已声明的计数器标识符。它可以直接在 ::marker
伪元素的 content
中使用 counter(list-item)
或 counters(list-item, '.')
。
但是非列表元素,哪怕是设置了display:list-item
,直接在::marker
的content
中使用counters(list-item, '.')
所起的效果和我们预期的有所不同。如果在非列表元素的::marker
的content
中使用counters()
达到我们想要的效果,需要使counter-reset
先声明计数器标识符,然后counter-increment
调用已声明的计数器标识符(回归到以前::before
的使用)。具本的可以看下面的示例代码:
::marker {
content: counter(list-item);
padding: 5px 30px 5px 12px;
background: linear-gradient(to right, #f36, #f09);
font-size: 2rem;
clip-path: polygon(0% 0%, 75% 0, 75% 51%, 100% 52%, 75% 65%, 75% 100%, 0 100%);
border-radius: 5px;
color: #fff;
text-shadow: 1px 1px 1px rgba(#09f, .5);
}
.box:nth-child(2n) ::marker {
content: counters(list-item, '.');
}
.box:nth-child(3) {
section {
counter-reset: item;
}
article {
counter-increment: item;
}
::marker {
content: counters(item, '.');
}
}
具体效果如下:
是不是觉得::marker
非常有意思,特别是给元素添加Marker标记的时候。换句话说,就是在定制个性化列表符号时,使用::marker
伪元素要比::before
之类的较为方便。而且::marker
是元素原生的列表标记符(::marker
)。
一旦::marker
伪元素得到所有浏览器支持之后,我们要让列表标记符和内容分离就会多了一种方案:
前面也向大家展示了,::marker
也可以像::before
一样,借助CSS计数器属性,可以更好的实现有序列表,甚至是嵌套的列表。
写在最后
虽然 ::marker
的出现允许我们为列表标记定制样式,但它也有一定的限制性,至少到目前为止是这样。比如,我们在 ::marker
伪元素上可控样式还是有限,要实现下面这样的个性化效果是不可能的:
庆幸的是,CSS 中除了 ::marker
伪元素之外,还可以使用 ::before
或 ::after
来生成内容,然后通过 CSS 来实现更具个性化的列表标记样式。
来源:juejin.cn/post/7358348786843959336
Mysql中各种日志、缓冲区都是干嘛的?
介绍
本篇文章主要以innodb存储引擎为主;在了解mysql的过程中经常能听到它内部有各种log以及缓冲区,他们在mysql中具有重要作用,例如binlog
可以进行主从恢复,undo log
可以进行数据回滚等。这篇文章主要讲解在mysql运气期间每个区域都是用来做什么的。
写入数据流程
对于mysql来讲,读写任何数据都是在内存中进行操作的;下图为mysql写入数据的详细流程:
- 写入undo log,为了实现回滚的功能,在写入真实数据前需要记录它的回滚日志,防止写入完数据后无法进行回滚;
- 写入
buffer pool
或change buffer
,在缓存中记录下数据内容; - 为了防止mysql崩溃内存中的数据丢失,此时会记录下redo log,记录redo log时也是写入它的buffer,通过不同的刷盘策略刷入到磁盘redo log文件中;
- 为了实现主从同步,数据恢复功能,mysql提供了binlog日志,写入完redo log后写binlog文件;
- 为了使binlog和redo log保持数据一致,这里采用的二阶段提交,写入binlog成功会再在redo log buffer中写入commit;
- 对redo log进行刷盘,这里有三种刷盘策略,介绍一下刷盘策略;
- 对buffer pool中的数据进行刷盘。
undo log
undo log记录事务开始前的数据状态,它主要用于数据回滚和实现MVCC:
- 回滚操作:undo log记录了事务开始前的数据状态,当事务需要回滚时,以便可以恢复到原始状态。
- 多版本并发控制(MVCC) :在读取历史数据时,undo log允许读取到事务开始前的数据版本,从而实现非锁定读取。
MVCC的具体实现可以查看:MVCC实现
buffer pool
innodb中无论是查询还是写绝大部分都是在buffer pool中进行操作的,它相当于innodb的缓存区,可以通过show engine innodb status
来查看buffer pool的使用情况;可以通过innodb_buffer_pool_size
来设置buffer pool的大小,线上不要吝啬给几个G内存都是正常的,但无论给多大内存都会有不够的时候,innodb采用了变种的LRU算法对数据页进行淘汰;如下图:
传统的LRU算法当碰到扫描一张大表时可能会直接把buffer pool中的所有页都更换为该表的数据,但这张表可能就使用一次,并不是热点数据;
innodb为了避免这种场景发生,会把整个buffer pool按照 5:3分成了young区域和old区域;其中绿色区域就是young区域也就是热点数据区域,紫色区域就是old区域也就是冷数据区域;整体的淘汰流程为:
- 如果想访问绿色区域内的数据,会把访问页直接放在young head处;
- 如果想访问一个不存在的页,会把tail页淘汰掉,并且把新访问的数据页插入在old head处;
- 如果访问old区域的数据页,并且这个数据页在LRU链表中存在的时间超过了
innodb_old_blocks_time
(默认1000毫秒),就把它移动到yound head处; - 如果访问old区域的数据页,并且这个数据页在LRU链表中存在的时间短于
innodb_old_blocks_time
,把该页移动到old head处。
在上图中可以看到除了LRU链表还有一个Flush链表,它是用来管理脏页的;在写入数据时绝大部分都会先写入buffer pool中,再更改buffer pool中的页数据时,该页就变成了脏页,此时就会被加入到flush链表中,定时会把flush中的脏页刷到.idb数据文件中。
change buffer
在介绍buffer pool时用的是绝大部分
操作,是因为在innodb中还存在change buffer,还有一部分操作是写入change buffer的。change buffer
的定义是当需要更新一个数据页时,如果数据页在内存中就直接更新,而如果这个数据页还没有在内存中的话,在不影响数据一致性的前提下,innodb会将这些更新操作缓存在change buffer中,这样就不需要从磁盘中加载这个数据页了,如果有查询需要访问这个数据页的时候,再将数据页读到内存中,然后执行change buffer中与这个页有关的操作,这样就能保证这个数据的正确性。change buffer
用的是buffer pool中的内存,可以通过innodb_change_buffer_max_size
来设置它占用buffer pool的内存比例。使用change buffer的前提条件是该数据页还没被加载到buffer pool中,并且如果是根据唯一索引进行更新,由于要检查数据的唯一性,必须把数据页加载到buffer pool中是无法享受change buffer带来的收益的。
redo log 与 redo log buffer
redo log是为了防止由于mysql异常退出导致buffer pool中还未持久化的数据丢失而诞生的;
它也是一个环形文件写数据写满时会覆盖历史的数据,它记录了数据页的物理变化,并且是顺序写入的提升了写入的性能;当mysql重启时可以使用redo log来恢复数据。
每次写redo log时并不是直接写入redo log文件,而是写入redo log buffer中,通过三种刷盘策略把数据同步到redo log中,可以通过innodb_flush_log_at_trx_commit
参数来控制刷盘的时机
- 0:事务提交时,日志缓冲(log buffer)被写入到日志文件,但并不立即刷新到磁盘。日志文件的刷新操作由后台线程每隔一秒执行一次;
- 1:事务提交时,日志缓冲被写入到日志文件,并立即刷新到磁盘;
- 2:事务提交时,日志缓冲被写入到日志文件,但不立即刷新到磁盘。而是每秒由后台线程将日志文件刷新到磁盘。
如果对数据的正确性要求很高应该设置为1。
注:第一张图流程中,在第5步有二次commit,在数据恢复如果发现一个事务没有commit,则去binlog日志中查询,如果发现binlog中有相应数据则直接恢复,如果没有则丢弃。
binlog
binlog为了高效地记录和传输数据更改信息,它采用了二进制格式存储数据库的更改操作,这样还可以占用更小的存储空间;它可以实现数据恢复、数据同步等功能。默认mysql是关闭binlog日志的,可以通过
在[mysqlId]部分中设置log-bin
和server-id
来开启binlog日志。它也是在事务提交时才进行数据记录,它有以下三种数据格式:
- Statement:记录每一条执行的sql,但由于mysql中存在一些函数,例如一些随机生成函数,此时数据同步时会发生同步过去的数据不一致;
- Row:记录每行被修改成什么,这样可以解决statement带来的数据不一致问题,但由于记录的太详细如果出现了全表更新,那记录的数据量就会特别大;
- Mixed:Statement和Row的混合体,mysql会根据执行的每一条具体的SQL语句来区别对待记录的日志格式。
总结
为了实现更高的性能,在innodb中的任何操作都是优先在内存中操作的;为了支持数据的数据回滚、MVVC引入了undo log,进而可以实现查询历史版本或数据回滚;同时为了防止异常退出导致的数据丢失引入了redo log;为了支持数据同步等功能mysql引入了binlog日志。这就是各个区域的作用,由于篇幅原因本篇文章只对每个区域做了简单介绍,后续会写各个区域详细内容的文章。
创作不易,觉得文章写得不错帮忙点个赞吧!如转载请标明出处~
来源:juejin.cn/post/7411489477283856419
一次接手远古Android项目终于运行起来了
我也没做过安卓开发,2020年外包开发的app在客户新手机上安装不上,搞呗。apk安装报错此应用与最新版Android不兼容
,试了同事的Android 14 确实同样报错
网上查到解决方案。
http://www.duidaima.com/Gr0up/Topic…
按照第一点增加64位指令集后,重新打包apk解决问题了
1、【成功并上线】在build.gradel文件的ndk部分添加arm64-v8a的指令集
2、【未实验】targetSdkVersion最少为29就能在安卓14上避免异常弹框
安装Android开发环境过程很曲折,重点是要安装项目需要的开发环境版本,不然各种错误失败
。
第一步确认项目开发环境版本
最开始下载Android Studio 2024最新版,2021版等等,JDK21最新版,JDK17都失败。
得出结论:
- 确认Android Studio 版本要看根目录build.gradle中gradle版本,再去官网下载对应版本号
- 确认JDK版本要看另一个build.gradle中targetCompatibility的版本号
JDK 版本
http://www.oracle.com/java/techno…
根据build.gradle中看出要JDK8,而且jdk8安装后默认有jre目录,不像jdk21要手动生成jre目录
注意上面网站用Chrome打开登录Oracle后报错Cookie太长,改为360极速版正常下载
登录或注册oracle账号才能下载
配置环境变量
新建JAVA_HOME C:\Program Files\Java\jdk-1.8
修改PATH %JAVA_HOME%\bin ;%JAVA_HOME%\jre\bin
网上说前面第二个前面一定要带分号
测试正常
额外补充 JDK17 和 JDK21 生成 jre目录
上面用的JDK8在安装好后默认是生成jre目录的,但是如果JDK17和JDK21没有默认生成jre目录,需要手动生成
必须管理员权限打开CMD
进入到jdk-21目录执行命令就可以生成jre文件夹了
bin\jlink.exe --module-path jmods --add-modules java.desktop --output jre
Android Studio 版本
developer.android.google.cn/studio/arch…
根据build.gradle中gradle:4.1.1看出要下载Android Studio 4.1.1 , 其他新版本项目有各种报错
再去官网下载对应版本
第二步 Android Studio 安装过程中问题解决
正常安装Android Studio
初始化设置sdk代理
启动后报错 Unable to access Android SDK add-on list
,点 Setup Proxy
修改Automatic proxy configuration URL设置为:mirrors.neusoft.edu.cn
因为后面都是google的域名,不设置sdk代理多半是下载不了的
可能设置Proxy再报错同样Unable这个错,就点 Cancel 跳过,后面都点 Next 直达 Finiash
安装sdk版本
第一次进入启动页面,在Configure选择SDK Manager,我把API Level的28,29,30都勾选上,因为我看老项目代码里面targetSdkVersion 28,而我找到的解决方法说最少29,干脆我就勾上这三个
后面点Accept,就直接下载到Finish呗
后面遇到报错 Installed Build Tools revision 35.0.0 is corrupted. Remove and install again using the SDK Manager.
那打开工具条 File -> Settings 找到 Android SDK 项,在 Android SDK Location
点 Edit 重新点Next安装后报错消失
再把 build.gradle 中35都改成28
具体看Build Tools有哪些版本,可以查看SDK安装目录build-tools有哪些,改成有的版本即可
安装 avd 模拟器
Android项目要运行是要模拟器的,avd就是官方调试模拟器,也可以用第三方的逍遥模拟器,夜游模拟器等
进去后随便选个 Pixel 4 XL,再进去我老项目是API Level 28的,就需要点 Download 下载
安装 HAXM
运行项目要求安装 HAXM,默认安装即可
第三步运行老项目解决问题
打开项目
SDK目录与原项目不匹配,点OK自动更新,估计原项目是苹果电脑开发,我这是windows环境
设置Gradle阿里云代理
找到 gradle-wrapper.properties 文件修改 distributionUrl 为国内代理,国外域名下载gradle超时失败了
替换域名后点 Sync Now
distributionUrl=https\://mirrors.aliyun.com/macports/distfiles/gradle/gradle-6.5-all.zip
如果 sync now点了出现proxy settings弹窗,那在第一个HOST name填写 mirrors.neusoft.edu.cn
设置 maven 阿里云代理
在根目录build.gradle的allproject下面增加
maven { url 'https://maven.aliyun.com/repository/jcenter' }
maven { url 'https://maven.aliyun.com/repository/google' }
maven { url 'https://maven.aliyun.com/repository/gradle-plugin' }
maven { url 'https://maven.aliyun.com/repository/public' }
设置 64位指令集
在 app/build.gradle的ndk下增加 arm64-v8a
运行项目,如果遇到问题可以点工具栏 build -> clean project 再 rebuild project
第四步签名打包apk
工具栏 build -> Generate Signed Bundle / Apk ...
选 APK
选择签名文件输入 password这三个输入框,如果没有就create new新建
选择打包apk存放目录,Finish就完成了
右下角显示成功
来源:juejin.cn/post/7410711559964229682
用Three.js搞个炫酷雷达扩散和扫描特效
1.画点建筑模型
添加光照,开启阴影
//开启renderer阴影
this.renderer.shadowMap.enabled = true;
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
//设置环境光
const light = new THREE.AmbientLight(0xffffff, 0.6); // soft white light
this.scene.add(light);
//夜晚天空蓝色,假设成蓝色的平行光
const dirLight = new THREE.DirectionalLight(0x0000ff, 3);
dirLight.position.set(50, 50, 50);
this.scene.add(dirLight);
平行光设置阴影
//开启阴影
dirLight.castShadow = true;
//阴影相机范围
dirLight.shadow.camera.top = 100;
dirLight.shadow.camera.bottom = -100;
dirLight.shadow.camera.left = -100;
dirLight.shadow.camera.right = 100;
//阴影影相机远近
dirLight.shadow.camera.near = 1;
dirLight.shadow.camera.far = 200;
//阴影贴图大小
dirLight.shadow.mapSize.set(1024, 1024);
- 平行光的阴影相机跟正交相机一样,因为平行光的光线是平行的,就跟视线是平行一样,切割出合适的阴影视角范围,用于计算阴影。
- shadow.mapSize设置阴影贴图的宽度和高度,值越高,阴影的质量越好,但要花费计算时间更多。
增加建筑
//添加一个平面
const pg = new THREE.PlaneGeometry(100, 100);
//一定要用受光材质才有阴影效果
const pm = new THREE.MeshStandardMaterial({
color: new THREE.Color('gray'),
transparent: true,//开启透明
side: THREE.FrontSide//只有渲染前面
});
const plane = new THREE.Mesh(pg, pm);
plane.rotateX(-Math.PI * 0.5);
plane.receiveShadow = true;//平面接收阴影
this.scene.add(plane);
//随机生成建筑
this.geometries = [];
const helper = new THREE.Object3D();
for (let i = 0; i < 100; i++) {
const h = Math.round(Math.random() * 15) + 5;
const x = Math.round(Math.random() * 50);
const y = Math.round(Math.random() * 50);
helper.position.set((x % 2 ? -1 : 1) * x, h * 0.5, (y % 2 ? -1 : 1) * y);
const geometry = new THREE.BoxGeometry(5, h, 5);
helper.updateWorldMatrix(true, false);
geometry.applyMatrix4(helper.matrixWorld);
this.geometries.push(geometry);
}
//长方体合成一个形状
const mergedGeometry = BufferGeometryUtils.mergeGeometries(this.geometries, false);
//建筑贴图
const texture = new THREE.TextureLoader().load('assets/image.jpg');
texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
const material = new THREE.MeshStandardMaterial({ map: texture,transparent: true });
const cube = new THREE.Mesh(mergedGeometry, material);
//形状产生阴影
cube.castShadow = true;
//形状接收阴影
cube.receiveShadow = true;
this.scene.add(cube);
效果就是很多高楼大厦的样子,为什么楼顶有窗?别在意这些细节,有的人就喜欢开天窗呢~
2.搞个雷达扩散和扫描特效
改变建筑材质shader,计算建筑的俯视uv
material.onBeforeCompile = (shader, render) => {
this.shaders.push(shader);
//范围大小
shader.uniforms.uSize = { value: 50 };
shader.uniforms.uTime = { value: 0 };
//修改顶点着色器
shader.vertexShader = shader.vertexShader.replace(
'void main() {',
` uniform float uSize;
varying vec2 vUv;
void main() {`
);
shader.vertexShader = shader.vertexShader.replace(
'#include <fog_vertex>',
`#include <fog_vertex>
//计算相对于原点的俯视uv
vUv=position.xz/uSize;`
);
//修改片元着色器
shader.fragmentShader = shader.fragmentShader.replace(
'void main() {',
`varying vec2 vUv;
uniform float uTime;
void main() {`
);
shader.fragmentShader = shader.fragmentShader.replace(
'#include <dithering_fragment>',
`#include <dithering_fragment>
//渐变颜色叠加
gl_FragColor.rgb=gl_FragColor.rgb+mix(vec3(0,0.5,0.5),vec3(1,1,0),vUv.y);`
);
};
然后你将同样的onBeforeCompile函数赋值给平面的时候,没有对应的效果。
因为平面没有z,只有xy,而且经过了-90度旋转后,坐标位置也要对应反转,由此可以得出平面的uv计算公式
vUv=vec2(position.x,-position.y)/uSize;
至此,建筑和平面的俯视uv一致了。
雷达扩散特效
- 雷达扩散就是一段渐变的环,随着时间扩大。
- 顶点着色器不变,改一下片元着色器,增加扩散环颜色uColor,对应shader.uniforms也要添加
shader.uniforms.uColor = { value: new THREE.Color('#00FFFF') };
const fragmentShader1 = `varying vec2 vUv;
uniform float uTime;
uniform vec3 uColor;
uniform float uSize;
void main() {`;
const fragmentShader2 = `#include <dithering_fragment>
//计算与中心的距离
float d=length(vUv);
if(d >= uTime&&d<=uTime+ 0.1) {
//扩散圈
gl_FragColor.rgb = gl_FragColor.rgb+mix(uColor,gl_FragColor.rgb,1.0-(d-uTime)*10.0 )*0.5 ;
}`;
shader.fragmentShader = shader.fragmentShader.replace('void main() {', fragmentShader1);
shader.fragmentShader = shader.fragmentShader.replace(
'#include <dithering_fragment>',
fragmentShader2);
//改变shader的时间变量,动起来
animateAction() {
if (this.shaders?.length) {
this.shaders.forEach((shader) => {
shader.uniforms.uTime.value += 0.005;
if (shader.uniforms.uTime.value >= 1) {
shader.uniforms.uTime.value = 0;
}
});
}
}
噔噔噔噔,完成啦!是立体化的雷达扩散,看起来很酷的样子。
雷达扫描特效
跟上面雷达扩散差不多,只要修改一下片元着色器
- 雷达扫描是通过扇形渐变形成的,还要随着时间旋转角度
const fragmentShader1 = `varying vec2 vUv;
uniform float uTime;
uniform vec3 uColor;
uniform float uSize;
//旋转角度矩阵
mat2 rotate2d(float angle)
{
return mat2(cos(angle), - sin(angle),
sin(angle), cos(angle));
}
//雷达扫描渐变扇形
float vertical_line(in vec2 uv)
{
if (uv.y > 0.0 && length(uv) < 1.2)
{
float theta = mod(180.0 * atan(uv.y, uv.x)/3.14, 360.0);
float gradient = clamp(1.0-theta/90.0,0.0,1.0);
return 0.5 * gradient;
}
return 0.0;
}
void main() {`;
const fragmentShader2 = `#include <dithering_fragment>
mat2 rotation_matrix = rotate2d(- uTime*PI*2.0);
//将雷达扫描扇形渐变混合到颜色中
gl_FragColor.rgb= mix( gl_FragColor.rgb, uColor, vertical_line(rotation_matrix * vUv)); `;
GitHub地址
来源:juejin.cn/post/7349837128508964873
震惊!🐿浏览器居然下毒!
发生什么事了
某天,我正在愉快的摸鱼,然后我看到测试给我发了条消息,说我们这个系统在UC浏览器中有问题,没办法操作,点了经常没反应(测试用得iPhone14,是一个h5的项目)。我直接懵了,这不是都测了好久了吗,虽然不是在uc上测得,chrome、safari、自带浏览器等,都没这个问题,代码应该是没问题的,uc上为啥会没有反应呢?难道是有什么隐藏的bug,需要一定的操作顺序才能触发?我就去找了测试,让他重新操作一下,看看是啥样的没反应。结果就是,正常进入列表页(首页),正常点某一项,正常进入详情页,然后点左上角返回,没反应。我上手试了下,确实,打开vconsole看了下,也没有报错。在uc上看起来还是必现的,我都麻了,这能是啥引起的啊。
找问题
在其他浏览器上都是好好的,uc上也不报错,完全看不出来代码有啥bug,完全没有头绪啊!那怎么办,刷新看看:遇事不决,先刷新
,还不行就清空缓存刷新
。刷新了之后,哎,好了!虽然不知道是什么问题,但现在已经好了,就当作遇到了灵异事件,我就去做其他事了。
过了一会,测试来找我了,说又出现了,不止是详情页,进其他页面也返回不了。这就难受住了呀,说明肯定是有问题的,只是还没找到原因。我就只好打开vconsole,一遍一遍的进入详情页点返回;刷新再进;清掉缓存,再进。
然后,我就发现,network中,出现了一个没有见过的请求
根据track、collect
这些单词来判断,这应该是uc在跟踪、记录某些操作,requestType还是个ping;我就在想,难道是这个请求的问题?但是请求为啥会导致,我页面跳转产生问题?然后我又看到了intercept(拦截)、pushState(history添加记录)
,拦截了pushState?
这个项目确实使用的是history路由,问题也确实出在路由跳转的时候;而且出现问题的时候,路由跳转,浏览器地址栏中的地址是没有变化的,返回就g了(看起来是后退没有反应,实际是前进时G了)
。这样看,uc确实拦截了pushState的操作。那它是咋做到的?
原来如此
然后,我想起来,前段时间在掘金上看到了一篇,讲某些第三方cdn投毒的事情,那么uc是不是在我们不知情的情况下,改了我们的代码。然后我切到了vconsole的element,展开head,发现了一个不属于我们项目的script,外链引入了一段js,就挂在head的最顶上。通过阅读,发现它在window的history上加了点料,覆写了forward和pushState(forward和pushState是继承来的方法)
正常的history应该是这样:
复写的类似这样:
当然,有些系统或框架,为了实现某些功能,比如实现触发popstate的效果,也会复写
但uc是纯纯的为了记录你的操作,它这玩意主要还有bug,会导致路由跳转出问题,真是闹麻了
如何做
删掉就好了,只要删掉uc添加的,当我们调用相关方法时,history就会去继承里找
// 判断是否是uc浏览器
if (navigator.userAgent.indexOf('UCBrowser') > -1) {
if (history.hasOwnProperty('pushState')) {
delete window.history.forward
delete window.history.pushState
}
// 找到注入的script
const ucScript = document.querySelector('script[src*="ucbrowser_script"]')
if (ucScript) {
document.head.removeChild(ucScript)}
}
}
吐槽
你说你一个搞浏览器的,就不能在底层去记录用户行为吗,还不容易被发现。主要是你这玩意它有bug呀,这不是更容易被发现吗。(这是23年11月份遇到的问题,当时产品要求在qq/百度/uc这些浏览器上也都测一下才发现的,现在记录一下,希望能帮助到其他同学)
来源:juejin.cn/post/7411358506048766006
CSS 终于在 2024 年增加了垂直居中功能
本文翻译自 CSS finally adds vertical centering in 2024,作者:James Smith, 略有删改。
在 2024 年的 CSS 原生属性中允许使用 1 个 CSS 属性 align-content: center
进行垂直居中。
<div style="align-content: center; height: 100px;">
<code>align-content</code> 就是这么简单!
</div>
支持情况:
Chrome: 123 | Firefox: 125 | Safari: 17.4 |
---|
CSS 对齐一般会使用 flexbox
或 grid
布局,因为 align-content
在默认的流式布局中不起作用。在 2024 年,浏览器实现了 align-content
。
- 你不需要 flexbox 或 grid,只需要 1 个 CSS 属性就可以进行对齐。
- 因此内容不需要包裹在 div 中。
<!-- 有效 -->
<div style="display: grid; align-content: center;">
内容。
</div>
<!-- 失败!-->
<div style="display: grid; align-content: center;">
包含 <em>多个</em> 节点的内容。
</div>
<!-- 包装div有效 -->
<div style="display: grid; align-content: center;">
<div> <!-- 额外的包装器 -->
包含 <em>多个</em> 节点的内容。
</div>
</div>
<!-- 无需包装div即可工作 -->
<div style="align-content: center;">
包含 <em>多个</em> 节点的内容。
</div>
令人惊讶的是,经过几十年的发展,CSS 终于有了 一个属性 来控制垂直对齐!
垂直居中的历史
浏览器很有趣,像对齐这样的基本需求长期以来都没有简单的答案。以下是在浏览器中垂直居中的方法(水平居中是另一个话题):
方法 1: 表格单元格
星级:★★★☆☆
有 4 种主要布局:流(默认)、表格、flexbox、grid。如何对齐取决于容器的布局。Flexbox 和 grid 相对较晚添加,所以表格是第一种方式。
<div style="display: table;">
<div style="display: table-cell; vertical-align: middle;">
内容。
</div>
</div>
方法 2: 绝对定位
星级:☆☆☆☆☆
通过绝对定位间接的方式来实现这个效果。
<div style="position: relative;">
<div style="position: absolute; top: 50%; transform: translateY(-50%);">
内容。
</div>
</div>
这个方式通过绝对定位来绕过流式布局:
- 用
position: relative
标记参考容器。 - 用
position: absolute; top: 50%
将内容的边缘放置在中心。 - 用
transform: translateY(-50%)
将内容中心偏移到边缘。
方法 3: 内联内容
星级:☆☆☆☆☆
虽然流布局对内容对齐没有帮助。它允许在一行内进行垂直对齐。那么为什么不使一行和容器一样高呢?
<div class="container">
::before
<div class="content">内容。</div>
</div>
.container::before {
content: '';
height: 100%;
display: inline-block;
vertical-align: middle;
}
.content {
display: inline-block;
vertical-align: middle;
}
这个方式有一个缺陷,需要额外创建一个伪元素。
方法 4: 单行 flexbox
星级:★★★☆☆
现在布局中的 Flexbox 变得广泛可用。它有两种模式:单行和多行。在单行模式(默认)中,行填充垂直空间,align-items
对齐行内的内容。
<div style="display: flex; align-items: center;">
<div>内容。</div>
</div>
或者调整行为列,并用 justify-content
对齐内容。
<div style="display: flex; flex-flow: column; justify-content: center;">
<div>内容。</div>
</div>
方法 5: 多行 flexbox
星级:★★★☆☆
在多行 flexbox 中,行不再填充垂直空间,所以行(只有一个项目)可以用 align-content
对齐。
<div style="display: flex; flex-flow: row wrap; align-content: center;">
<div>内容。</div>
</div>
方法 6: grid
星级:★★★★☆
Grid 出来的更晚,对齐变得更简单。
<div style="display: grid; align-content: center;">
<div>内容。</div>
</div>
方法 7: grid 单元格
星级:★★★★☆
注意与前一个方法的微妙区别:
align-content
将单元格居中到容器。align-items
将内容居中到单元格,同时单元格拉伸以适应容器。
<div style="display: grid; align-items: center;">
<div>内容。</div>
</div>
似乎有很多方法可以做同一件事。
方法 8: margin:auto
星级:★★★☆☆
在流布局中,margin:auto
可以水平居中,但不是垂直居中。使用 margin-block: auto
可以设置垂直居中。
<div style="display: grid;">
<div style="margin-block: auto;">
内容。
</div>
</div>
方法 9: 这篇文章的开头
星级:★★★★★
为什么浏览器最初没有添加这个?
<div style="align-content: center;">
<code>align-content</code> 就是这么简单!
</div>
总结
CSS 的新特性 align-content
提供了一个简单且直接的方式来实现垂直居中,无需使用额外的div包装或复杂的布局模式即可完成垂直居中。但注意这个属性还存在一定的浏览器兼容性,在线上使用需谨慎。
看完本文如果觉得有用,记得点个赞支持,收藏起来说不定哪天就用上啦~
专注前端开发,分享前端相关技术干货,公众号:南城大前端(ID: nanchengfe)
来源:juejin.cn/post/7408097468796551220
用SQL写游戏,可能吗?看看大佬是如何使用 SQL 写一个俄罗斯方块亮瞎你的钛合金狗眼的!
大家好,今天我要带你们一起来开开眼界。你知道SQL吗?就是那个我们平时用来和数据库打交道的语言——查询数据、插入数据、删除数据,嗯,数据库管理员的必备技能。但你能想象到有人用SQL做了什么吗?他用SQL做了一款俄罗斯方块!对,就是那个曾经风靡全球的经典游戏。
你可能会想,“这怎么可能?SQL不就是查查数据嘛,最多写点复杂的查询语句,能做游戏?”其实我一开始也是这个想法,但看了这个项目后,真的不得不感叹程序员的脑洞太大了!这篇文章就来和你聊聊,这个疯狂的项目到底是怎么实现的,以及为什么这个看似“不务正业”的尝试背后,可能藏着编程世界的一些终极奥秘。
还是先上一下项目地址吧:
1. Turing完备性,SQL到底有多强大?
首先,让我们聊聊一个稍微专业一点的概念:图灵完备性(Turing completeness) 。简单来说,如果一门编程语言是图灵完备的,那它理论上可以实现任何计算。我们平时接触的编程语言,比如Python、Java、C++,都是图灵完备的。但SQL呢?你可能想象不到,SQL也是图灵完备的,这意味着它也具备和其他编程语言一样的能力,只是我们平时大多只用它进行数据库操作。
项目的开发者正是看中了SQL的图灵完备性,才想出了用它来实现俄罗斯方块这个创意。虽然SQL天生并不是为游戏设计的,但通过一些巧妙的设计,开发者硬是把这个“不可能的任务”完成了。不得不说,这不仅仅是技术上的一种挑战,更是一种极致的创意和智慧的碰撞。
2. 用SQL写游戏,可能吗?
接下来,你可能很好奇了,具体怎么实现的呢?其实,开发者在SQL中用了一些非常“刁钻”的技巧。他利用了SQL中的递归查询(Common Table Expressions,简称CTE)和一些复杂的数学操作,来模拟俄罗斯方块的游戏逻辑。
WITH RECURSIVE t(i) AS (
-- non-recursive term
SELECT 1
UNION ALL
-- recursive term
SELECT i + 1 -- takes i of the previous row and adds 1
FROM t -- self-reference that enables recursion
WHERE i < 5 -- when i = 5, the CTE stops
)
SELECT *
FROM t;
i
----
1
2
3
4
5
(5 rows)
举个简单的例子,当俄罗斯方块下落时,我们需要判断它是否与底部或其他方块发生碰撞。通常这种逻辑我们会在游戏开发中使用循环来处理,而在SQL中,开发者通过递归查询来实现类似的循环效果。每次查询都相当于让方块“动”一下,并判断它是否碰到边界。
-- without i appended
...
-> Memoize (loops=999)
...
Hits: 998 Misses: 1 ...
-> Function Scan on dblink input (loops=1) -- only called once
...
-- with i appended
...
-> Nested Loop (loops=999)
-> WorkTable Scan on main main_1 (loops=999)
-> Function Scan on dblink input (loops=999) -- called every iteration
...
虽然说这个过程比传统的编程语言要复杂得多,但实际上,通过SQL,也能够非常清晰地描述出游戏的规则和状态变化。这其实也证明了图灵完备性的一个非常有趣的应用场景——我们可以用SQL来做的不仅仅是数据库操作,甚至是一些我们平时想都不敢想的事情。
3. 疯狂背后的深思:编程的边界在哪里?
或许你会觉得,用SQL做一个俄罗斯方块游戏纯粹是“哗众取宠”,为了博取眼球,没什么实际意义。但深入思考一下,这个项目实际上揭示了编程的一些非常深刻的哲学问题:编程的边界在哪里?
我们习惯性地把SQL、Python、Java等语言分门别类,用它们来解决不同类型的问题。但这个项目提醒我们,编程的真正边界,或许并不是由语言的设计来决定的,而是由开发者的想象力来定义的。一个看似“不合适”的工具,通过创意和技巧,也可以实现出乎意料的结果。这或许也是编程最迷人之处:没有什么是绝对不可能的。
4. 我们可以从这些疯狂的想法中能学到什么?
看完这个项目,你可能会想,“那我能从中学到什么呢?” 其实,除了技术上的启发之外,这个项目还给我们提供了一些更为重要的思维方式。
第一点,敢于挑战常规。 当我们学习编程时,往往会被一些固定的思维框架束缚住,比如SQL只能用于数据库操作,JavaScript才是做前端的。但这个项目告诉我们,有时候打破常规、尝试一些看似不可能的事情,可能会有意外的收获。
第二点,深入理解工具的本质。 学习一门编程语言不仅仅是掌握语法和基本操作,更重要的是理解它背后的能力和局限。这个项目通过SQL的图灵完备性展示了它的潜力,这种对工具的深刻理解,往往能帮助我们在关键时刻找到突破口。
第三点,保持对编程的好奇心。 编程是一门技术,但同时也是一门艺术。正如这位开发者一样,保持好奇心,不断尝试新东西,能够让我们在编程的世界里走得更远。
5. 最后,尝试一下吧!
看完了这篇文章,我猜你可能已经对这个项目充满了好奇。那就别犹豫了,去看看GitHub项目,甚至可以自己动手试试。即使你并不是SQL的高手,但通过这个项目,你一定能收获一些不一样的编程灵感。毕竟,编程的世界永远充满了无限可能,而这些可能性,就等待着你去探索和创造。
最后送你一句话:编程的乐趣,不在于完成任务,而在于不断发现和实现那些看似不可能的创意!
来源:juejin.cn/post/7411354460969222159
写css没灵感,那是你没用到这几个开源库
你是否遇到过写css没灵感,写不出酷炫的效果,那这篇文章你一定要看完。知道这几个开源库,它能让你写出炸天的效果并且有效地增加你的摸鱼时长。
1.CSS Inspiration
网址:
chokcoco.github.io/CSS-Inspira…
CSS Inspiration 上面有各种天马行空的css教程,涵盖了css的许多常见的特效。以分类的形式展示不同的css属性或者不同的课题,例如布局方式、border、伪元素、滤镜、背景3D等。这些都是css里面十分重要的知识点,不管是用于学习还是项目中实际运用都是不错的选择。
当然你也可以用来巩固基础知识,可以利用此项目来制作一些常用的特效,可以看到有上百个经典案例供我们参考,重点是提供源代码,复制粘贴即可使用。
2.Neumorphism
地址:
Neumorphism属于新拟态ui风格,是目前比较新颖的一种前端css设计风格。它的格调简单,基本颜色比较浅,如米白、浅灰、浅蓝等。再利用阴影呈现出凹凸效果,看起来很简单舒适且有3D效果,因此我们可以通过拟态设计出很多优美的页面,拖动效果控制条即可秒生成css样式。
3.AnimXYZ
地址:
如果说你热衷于动画,那animxyz绝对是你的不二之选。你可以使用animxyz组合和混合不同的动画来创建自己的高度可定制的css动画,而无需编写一个单一的关键帧。
相比于animate css,它的强大之处在于你可以在这里根据自己的想法来手动配置动画。实现的动画代码实例,我们可以复制迁移到项目中使用。
4.CodePen
最后要推荐的则是我最常用也是我最推荐的,它就是codepen。codepen是一个完全免费的前端代码托管服务,上面云集了各路大神,拥有全世界前端达人经典项目进行展示,让你从中获取到很多的创作灵感。
它可以实现即时预览,你甚至可以在线修改并及时预览别人的作品。支持多种主流预处理器,快速添加外部资源文件,只需在输入框里输入库名,codepen就会从cdn上寻找匹配的css或js库。
来源:juejin.cn/post/7278238985448177704
为什么 2!=false 和 2!=true 返回的都是true
前言
今天突然想起一个奇怪的问题,记录一下,我在控制台执行内容如下:
由上图可见,2 != false
和 2 != true
返回的值竟然都是true
,那么为什么呢,请看下文:
1 !=
操作符的作用
!=
是“不等于”操作符。它会在比较前执行类型转换,然后再比较两个值是否不相等。
在 JavaScript 中,
2 != false
和2 != true
返回true
的原因涉及到 JavaScript 中的类型转换和比较规则。
2 类型转换
当使用 !=
进行比较时,JavaScript 会尝试将比较的两个值转换为相同的类型,然后再进行比较。以下是 2 != false
和 2 != true
的过程:
2 != false
false
会被转换为数字类型。根据 JavaScript 的转换规则,false
被转换为0
。- 现在表达式变成了
2 != 0
。 2
和0
不相等,因此返回true
。
2 != true
true
会被转换为数字类型。根据 JavaScript 的转换规则,true
被转换为1
。- 现在表达式变成了
2 != 1
。 2
和1
不相等,因此返回true
。
总结
2 != false
返回true
是因为2
和0
不相等。2 != true
返回true
是因为2
和1
不相等。
这就是为什么 2 != false
和 2 != true
都会返回 true
。
来源:juejin.cn/post/7411168461500563468
一个失败的独立开发的300多天的苦果
历史是成功者书写的,所以我们能看到的成功的独立开发者,正所谓一将功成万骨枯,其实失败的才是大多数。从2023年7月14到现在2024年5月22,10个多月,一个313天总共的收入只有652元(😭😭😭)
appStore的收入($72.14=¥522)
微软商店的收入($17.97=¥130)
总结一下失败原因
- 做了一堆垃圾,没有聚焦的做好一款产品
- 没有扬长避短,其实前端开发最适合的产品方向应该是web和微信小程序,在electron上架appStore上花费了大量的时间(15天真实的时间)
- 归根结底还是在做产品这方面的储备不够,做产品没有定力,心静不下来,如果其他的都不做把全部的精力都拿来做aweb浏览器(包括研发和宣传),结果也不至于这么差。
分享一下失败的经验吧
- 全职独立开发初期很难沉下来打磨产品,还是建议边工作边搞,沉不下来就会原来越乱
- 如果感觉效率低,还是不要在家里办公了,咖啡馆、图书管、公创空间(武汉这边500一个公位)都是不错的选择
- 有单还是接吧,不然真的是太难了
来源:juejin.cn/post/7371638121279848499
哦!该死的您瞧瞧这箭头
今天和大家分享一个小思路,用于实现箭头步骤条效果。
在我们项目中,有一个需求,想实现一个步骤条,默认的时候是 边框和文字 需要有特定的颜色,但是选中时,背景需要有特定颜色,边框颜色消失,文字显示白色,具体效果如下图:
可以看到,步骤一是默认样式,步骤二是选中样式,即选中背景颜色需要变成默认样式的边框颜色
使用div思路(无法实现默认效果)
当时第一次想的是使用div来实现这个逻辑,因为看到elementui有个差不多的(但是实现不了上面的效果,实现一个箭头倒是可以的,下面为大家简单介绍div的实现思路)
-- 饿了么效果图
搭建dom结构
首先我们先创建一个矩形
然后像这样使用一个伪元素,盖在矩形的开头,并修改其border-color的颜色即可,操作方式如下图
<!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>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
height: 100vh;
width: 100vw;
display: grid;
place-content: center;
overflow: hidden;
}
.arrow {
position: relative;
display: flex;
align-items: center;
justify-content: center;
min-width: 100px;
max-width: max-content;
height: 30px;
background: salmon;
}
.arrow::after {
position: absolute;
content: "";
left: 0px;
border: 15px solid transparent;
border-left-color: white;
}
.arrow::before {
position: absolute;
content: "";
right: 0px;
border: 15px solid transparent;
border-top-color: white;
border-right-color: white;
border-bottom-color: white;
}
.content {
text-align: center;
padding: 0px 20px;
width: 140px;
box-sizing: border-box;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.content-inner {
display: inline-block;
width: 100%;
transform: translateX(-5px);
}
</style>
</head>
<body>
<div class="arrow">
<div class="content">
<span class="content-inner">1</span>
</div>
</div>
</body>
</html>
这样就实现了一个箭头啦。
但是使用div实现箭头,并不太好实现我们开头想要的那种效果,如果非要实现也要费很大劲,得不偿失,所以接下来,介绍第二种方案
使用SVG标签(可缩放矢量图形)
实现思路即标签介绍
polyline
polyline
元素是 SVG 的一个基本形状,用来创建一系列直线连接多个点。
使用到的属性:
- stroke-width: 用于设置绘制的线段宽度
- fill: 填充色
- stroke: 线段颜色,
- points:绘制一个元素点的数列 (
0,0 22,22
)
接下来我们尝试使用该元素绘制一个箭头,先看看需要多少点位(如下图需6个,但是元素需要闭合,所以需要7个)
所以我们就可以很轻松的绘制出一个箭头,具体代码如下
<svg
:viewBox="0 0 130 26"
width="130px"
height="26"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
>
<polyline
class="polyline"
points="0,0 115,0 130,13 115,26 0,26 15,13 0,0"
fill="green"
stroke-width="1"
></polyline>
</svg>
此时我们得到了类似选中后的颜色,那默认颜色呢,只需要修改其 fill, stroke属性即可,具体逻辑如下
<svg
:viewBox="0 0 130 26"
width="130px"
height="26"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
>
<polyline
class="polyline"
points="0,0 115,0 130,13 115,26 0,26 15,13 0,0"
fill="transparent"
stroke="red"
stroke-width="1"
></polyline>
</svg>
此时那文中的内容怎么办,没法直接放标签内,此时需要借助另一个标签。
foreignObject
SVG中的
<foreignObject>
元素允许包含来自不同的 XML 命名空间的元素。在浏览器的上下文中,很可能是 XHTML / HTML。
所以我们可以使用该标签来作为放置内容的容器
属性介绍:
- x:设置 foreignObject 的 x 坐标
- y:设置 foreignObject 的 y 坐标
- width:设置 foreignObject 的宽度
- height:设置 foreignObject 的高度
具体代码如下
<svg
:viewBox="0 0 130 26"
width="130px"
height="26"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
>
<polyline
class="polyline"
points="0,0 115,0 130,13 115,26 0,26 15,13 0,0"
fill="transparent"
stroke="red"
stroke-width="1"
>
</polyline>
<foreignObject
x="0"
y="0"
width="130"
height="26"
>
<span
style="line-height: 26px; transform: translateX(14px); display: inline-block;"
>
步骤1111111
</span>
</foreignObject>
</svg>
这样就实现了默认样式,文字颜色可以自己调整
完整代码
由于需要遍历数据,所以完整代码是 vue3 风格
<template>
<div class="next-step-item" @click="stepClick">
<svg
:viewBox="`0 0 ${arrowStaticData.width} ${arrowStaticData.height}`"
:width="arrowStaticData.width"
:height="arrowStaticData.height"
:style="{
transform:
index === 0
? 'translate(0px,0px)'
: `translate(${arrowStaticData.offsetLeft * index}px,0)`,
}"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
>
<polyline
class="polyline"
:points="points"
v-bind="color"
stroke-width="1"
></polyline>
<foreignObject
x="0"
y="0"
:width="arrowStaticData.width"
:height="arrowStaticData.height"
>
<span
class="svg-title"
:style="{
color: fontColor,
lineHeight: arrowStaticData.height + 'px',
}"
:title="title"
>
{{ title }}
</span>
</foreignObject>
</svg>
</div>
</template>
<script lang="ts" setup>
import { computed } from "vue";
const defaultFontColor = "#fff";
const defaultColor = "transparent";
// 主题颜色
const colorObj = Object.freeze({
finish: {
default: {
stroke: "#16BB60",
fill: defaultColor,
color: "#16BB60",
},
active: {
stroke: "#16BB60",
fill: "#16BB60",
color: defaultFontColor,
},
}, // 绿色
await: {
default: {
stroke: "#edf1f3",
fill: defaultColor,
color: "#333",
},
active: {
stroke: "#edf1f3",
fill: "#edf1f3",
color: "#333",
},
}, // 灰色
process: {
default: {
stroke: "#0A82E5",
fill: defaultColor,
color: "#0A82E5",
},
active: {
stroke: "#0A82E5",
fill: "#0A82E5",
color: defaultFontColor,
},
}, // 蓝色
});
const arrowStaticData = Object.freeze({
width: 130,
height: 26,
hornWidth: 15, // 箭头的大小
offsetLeft: -7, // step离左侧step的距离,-15则左间距为0
});
const emits = defineEmits(["stepClick"]);
const props = defineProps({
title: {
type: String,
default: "",
},
// 类型名称
typeName: {
type: String,
default: "",
},
// 是否点中当前的svg
current: {
type: Boolean,
default: false,
},
// 当前是第几个step
index: {
type: Number,
default: 0,
},
});
const points = computed(() => {
const { width, hornWidth, height } = arrowStaticData;
return props.index === 0
? `0,0 ${width - hornWidth},0
${width},${height / 2}
${width - hornWidth},${height}
0,${height} 0,0`
: `0,0 ${width - hornWidth},0
${width},${height / 2}
${width - hornWidth},${height}
0,${height}
${hornWidth},${height / 2} 0,0`;
});
const color = computed(() => {
let color = {};
const currentStyleConfig: any = colorObj[props.typeName];
// 如果当前是被选中的,颜色需要区分
if (props.current) {
color = {
fill: currentStyleConfig.active.fill,
stroke: currentStyleConfig.active.stroke,
};
} else {
color = {
stroke: currentStyleConfig.default.stroke,
fill: currentStyleConfig.default.fill,
};
}
return color;
});
const fontColor = computed(() => {
const currentStyleConfig: any = colorObj[props.typeName];
let fontColor = "";
if (props.current) {
fontColor = currentStyleConfig.active.color;
} else {
fontColor = currentStyleConfig.default.color;
}
return fontColor;
});
const stepClick = () => {
emits("stepClick", props.index);
};
</script>
<style lang="scss" scoped>
.next-step-item {
cursor: pointer;
.polyline {
transition: 0.3s;
}
.svg-title {
padding: 0 15px;
display: block;
position: relative;
width: 100%;
text-align: center;
font-weight: bold;
font-size: 14px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
transition: 0.3s;
box-sizing: border-box;
}
}
</style>
使用方式
<template>
<div>
<div class="arrow-container">
<arrow
v-for="item of arrowList"
:key="item.index"
v-bind="item"
:current="arrowCurrent === item.index"
@stepClick="changeStepCurrent"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, reactive, ref } from "vue";
import Arrow from "components/Arrow/index.vue";
const arrowCurrent = ref<number>(0);
const arrowList = [
{
index: 0,
title: "步骤一一一一一一一一一",
typeName: "process",
},
{
index: 1,
title: "步骤二一一一一一一一一一",
typeName: "finish",
},
{
index: 2,
title: "步骤三",
typeName: "await",
},
];
</script>
<style lang="scss">
.arrow-container {
padding: 30px;
display: flex;
width: 800px;
height: 400px;
border: 1px solid #ccc;
margin-top: 100px;
box-sizing: border-box;
}
</style>
完整效果
来源:juejin.cn/post/7350695708074344498
uniapp实现背景颜色跟随图片主题色变化(多端兼容)
最近做uniapp项目时遇到一个需求,要求模仿腾讯视频app首页的背景颜色跟随banner图片的主题颜色变化,并且还要兼容H5、APP、微信小程序三端。
由于项目的技术栈为uniapp,所以以下使用uni-ui的组件库作为栗子。
需求分析
腾讯视频app效果如下:
从上图看出,大致分为两步:
1.获取图片主题色
2.设置从上到下的
主题色
to白色
的渐变:
background: linear-gradient(to bottom, 主题色, 白色)
获取主题色主要采用canvas
绘图,绘制完成后获取r、g、b
三个通道的颜色像素累加值,最后再分别除以画布大小,得到每个颜色通道的平均值即可。
搭建页面结构
page.vue
<script>
import {
getImageThemeColor
} from '@/utils/index'
export default {
data() {
return {
// 图片列表
list: [],
// 当前轮播图索引
current: 0,
// 缓存banner图片主题色
colors: [],
// 记录当前提取到第几张banner图片
count: 0
}
},
computed: {
// 动态设置banner主题颜色背景
getStyle() {
const color = this.colors[this.current]
return {
background: color ? `linear-gradient(to bottom, rgb(${color}), #fff)` : '#fff'
}
}
},
methods: {
// banner改变
onChange(e) {
this.current = e.target.current
},
getList() {
this.list = [
'https://img.zcool.cn/community/0121e65c3d83bda8012090dbb6566c.jpg@3000w_1l_0o_100sh.jpg',
'https://img.zcool.cn/community/010ff956cc53d86ac7252ce64c31ff.jpg@900w_1l_2o_100sh.jpg',
'https://img.zcool.cn/community/017fc25ee25221a801215aa050fab5.jpg@1280w_1l_2o_100sh.jpg',
]
},
// 获取主题颜色
getThemColor() {
getImageThemeColor(this, this.list[this.count], 'canvas', (color) => {
const colors = [...this.colors]
colors[this.count] = color
this.colors = colors
this.count++
if (this.count < this.list.length) {
this.getThemColor()
}
})
}
},
onLoad() {
this.getList()
// banner图片请求完成后,获取主题色
this.getThemColor()
}
}
script>
<style>
.box {
display: flex;
flex-direction: column;
background-color: deeppink;
padding: 10px;
}
.tabs {
height: 100px;
color: #fff;
}
.swiper {
width: 95%;
height: 200px;
margin: auto;
border-radius: 10px;
overflow: hidden;
}
image {
width: 100%;
height: 100%;
}
style>
封装获取图片主题颜色函数
先简单讲下思路 (想直接看源码可直接跳到下面) 。先通过request请求图片地址,获取图片的二进制数据,再将图片资源其转换成base64,调用drawImage
进行绘图,最后调用draw
方法绘制到画布上。
CanvasContext.draw介绍
更多api使用方法可参考:uniapp官方文档
getImageThemeColor.js
/**
* 获取图片主题颜色
* @param path 图片的路径
* @param canvasId 画布id
* @param success 获取图片颜色成功回调,主题色的RGB颜色值
* @param fail 获取图片颜色失败回调
*/
export const getImageThemeColor = (that, path, canvasId, success = () => {}, fail = () => {}) => {
// 获取图片后缀名
const suffix = path.split('.').slice(-1)[0]
// uni.getImageInfo({
// src: path,
// success: (e) => {
// console.log(e.path) // 在安卓app端,不管src路径怎样变化,path路径始终为第一次调用的图片路径
// }
// })
// 由于getImageInfo存在问题,所以改用base64
uni.request({
url: path,
responseType: 'arraybuffer',
success: (res) => {
let base64 = uni.arrayBufferToBase64(res.data);
const img = {
path: `data:image/${suffix};base64,${base64}`
}
// 创建canvas对象
const ctx = uni.createCanvasContext(canvasId, that);
// 图片绘制尺寸
const imgWidth = 300;
const imgHeight = 150;
ctx.drawImage(img.path, 0, 0, imgWidth, imgHeight);
ctx.save();
ctx.draw(true, () => {
uni.canvasGetImageData({
canvasId: canvasId,
x: 0,
y: 0,
width: imgWidth,
height: imgHeight,
fail: fail,
success(res) {
let data = res.data;
let r = 1,
g = 1,
b = 1;
// 获取所有像素的累加值
for (let row = 0; row < imgHeight; row++) {
for (let col = 0; col < imgWidth; col++) {
if (row == 0) {
r += data[imgWidth * row + col];
g += data[imgWidth * row + col + 1];
b += data[imgWidth * row + col + 2];
} else {
r += data[(imgWidth * row + col) * 4];
g += data[(imgWidth * row + col) * 4 + 1];
b += data[(imgWidth * row + col) * 4 + 2];
}
}
}
// 求rgb平均值
r /= imgWidth * imgHeight;
g /= imgWidth * imgHeight;
b /= imgWidth * imgHeight;
// 四舍五入
r = Math.round(r);
g = Math.round(g);
b = Math.round(b);
success([r, g, b].join(','));
},
}, that);
});
}
});
}
主题色计算公式
计算图片主题色的公式主要有两种常见的方法:平均法和主成分分析法。
平均法:
平均法是最简单的一种方法,它通过对图片中所有像素点的颜色进行平均来计算主题色。具体步骤如下:
- 遍历图片的每个像素点,获取其RGB颜色值。
- 将所有像素点的R、G、B分量分别求和,并除以像素点的总数,得到平均的R、G、B值。
- 最终的主题色即为平均的R、G、B值。
主成分分析法
主成分分析法是一种更复杂但更准确的方法,它通过对图片中的颜色数据进行降维处理,提取出最能代表整个图片颜色分布的主要特征。具体步骤如下:
- 将图片的所有像素点的颜色值转换为Lab颜色空间(Lab颜色空间是一种与人眼感知相关的颜色空间)。
- 对转换后的颜色数据进行主成分分析,找出相应的主成分。
- 根据主成分的权重,计算得到最能代表整个图片颜色分布的主题色。
需要注意的是,计算图片主题色的方法可以根据具体需求和算法的实现方式有所不同,上述方法只是其中的两种常见做法。
结语
大家有更好的实现方式,欢迎评论区留言哦!
来源:juejin.cn/post/7313979304513044531
和妹子逛完街,写了个 AI 智能穿搭系统
想直接看成品演示的可以直接划到文章底部
背景
故事起源在和一个妹子去逛衣服店的时候,试来试去的难以取舍,最终消耗了我一个小时。虽然这个时间不多,
但这个时间黑神话悟空足矣让我打完虎先锋
回家我就灵光一闪,是不是可以搞一个AI智能穿搭,只需要上传自己的照片和对应的衣服图片就能实现在线试衣服呢?
说干就干,我就开始构思方案,画原型。
俗话说万事开头难,事实上这个构思到动工就耗费了我一个礼拜,因为一直在构思怎么样的交互场景会让用户使用起来比较丝滑,并且容易上手。
目前实现的功能有:
- ✅ 用户信息展示
- ✅ AI 生成穿搭
- ✅ 风格大厅
待完成:
- 私人衣柜
- AI 换鞋
经过
1. 画产品原型
起初第一个版本的产品原型由于是自己构思没有任何参考,直接上手撸代码的,想到啥就画啥,所以布局非常传统,配色也非常普通(蚂蚁蓝),所以感觉没有太多的时尚气息(个人觉得丑的一逼,不像是互联网的产物)。因为重构掉了,老的现在没有了,我懒就不重新找回来截图了,直接画个当时的样子,大概长成下面这样:

丑的我忍不了,我就去设计师专门用的网站参(chao)考(xi)了一下,找来找去,终于有了下面的最终版原型图

2. 配色选择
大家知道,所有的UI设计,都离不开主题色的选择,比如:淘宝橙、飞猪橙、果粒橙...,目的一方面是为了打造品牌形象,另一方面也是为了提升品牌辨识度,让你看到这个颜色就会想起它
那我必须也得跟上时代的潮流,选了 #c1a57b 这款低调而又不失奢华的色值作为主题色,英雄不问出处,问就是借鉴。
3. 技术选型
我对技术的定义是:技术永远服务于产品,能高效全面帮助我开发出一款应用,并且能保证后续的稳定性和可维护性,啥技术我都行。当然如果这门技术我优先会从我属性的板块去找。
经过各种权衡和比较,最后敲定下来了技术选型方案:
- 前端:taro (为了后续可能会有小程序端做准备)
- 后端:koajs (实际使用的是midway,基于koajs,主要是比较喜欢koa的轻量化架构)
- 数据库:mongodb (别问,问就是简单易上手)
- 代码仓库:gitea
- CI:gitea-runner
- 部署工具:pm2
- 静态文件托管:阿里云OSS
4. 撸代码
这里我只挑一些个人感觉相对需要注意的地方展开讲讲
4.1 图片转存
由于我生成图片的API图片链接会在一天之后失效,所以我需要在调用任务详情的时候,把这个文件转存到我自己的oss服务器,这里我总结出来的思路是:【1. 保存在本地暂存文件夹】-【2. 调用node流式读取接口】-【3. 保存到oss】-【4. 返回替换原来的链接】
具体代码参考如下:
const tempDir = path.join(tmpdir(), 'temp-upload-files')
const link = url.parse(src);
const fileName = path.basename(link.pathname)
const localPath = path.join(tempDir, `/${fileName}`); // 生成保存路径
let request
if (link.protocol === 'https:') {
request = https
} else {
request = http
}
request.get(src, async (response) => {
const fileStream = await fs.createWriteStream(localPath); // 保存到本地暂存路径
await response.pipe(fileStream);
fileStream.on("error", (error) => {
console.error("保存图片出错:", error);
reject(error)
});
fileStream.on('finish', async res => {
console.log('暂存完成,开始上传:', res)
let result = await this.ossService.put(`/${params.saveDir || 'tmp'}/${fileName}`, localPath);
if (!result) return
resolve(result)
});
});
这里的request因为我不想引入其它的库所以这样写,如果有更好的方案,可以在评论区告知一下。
这里需要注意的一个地方是,上传的这个 localPath 最好是自己做一下处理,我这边没有处理,因为可能两个用户同时上传,他们的文件名称相同的时候,可能会出现覆盖的情况,包括后面的oss保存也是。
4.2 文件流式上传中间件
因为默认的接口处理是不处理流式调用的,所以需要自己创建一个中间件来拦截处理一下,下面给出我的参考代码:
class SSE {
ctx: Context
constructor(ctx: Context) {
ctx.status = 200;
ctx.set('Content-Type', 'text/event-stream');
ctx.set('Cache-Control', 'no-cache');
ctx.set('Connection', 'keep-alive');
ctx.res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Transfer-Encoding': 'chunked'
});
ctx.res.flushHeaders();
this.ctx = ctx;
}
send(data: any) {
// string
if (typeof data === "string") {
this.push(data);
} else if (data.id) {
this.push(`id: ${data.id}\n`);
} else if (data.event) {
this.push(`event: ${data.event}\n`);
} else {
const text = JSON.stringify(data)
this.push(`data: ${text}\n\n`);
}
}
push(data: any) {
this.ctx.res.write(data);
this.ctx.res.flushHeaders();
}
close() {
this.ctx.res.end();
}
}
@Middleware()
export class StreamMiddleware implements IMiddleware<Context, NextFunction> {
// ?------------ 中间件处理逻辑 -----------------
resolve() {
return async (ctx: Context, next: NextFunction) => {
if (ctx.res.headersSent) {
if (!ctx.sse) {
console.error('[sse]: response headers already sent, unable to create sse stream');
}
return await next();
}
const sse = new SSE(ctx);
ctx.sse = sse;
await next();
if (!ctx.body) {
ctx.body = ctx.sse;
} else {
ctx.sse.send(ctx.body);
ctx.body = sse;
}
};
}
public match(ctx: Context): boolean {
// ?------------ 不带 stream 前缀默认都不是流式接口 -----------------
if (ctx.path.indexOf('stream') < 0) return false
}
static getName(): string {
return 'stream';
}
}
4.3 mongodb 数据库的权限
这里尽量不要使用root权限的数据库角色,可以创建一个只有当前数据库权限的角色,具体可以网上找相关文档,怎么为某个collection创建账户。
实机演示
1. 提交素材,创建任务

2. 获取生成图片

3. 展示大厅(待完善)

结语
当然现在目前这个还是内测版本,功能还不够健全,还有很多地方需要打磨,包括用户信息页面的展示是否合理,UI的排版,数据库表的设计等等
通过观察生活用现有的技术创造一些价值,对我来说就是一种幸福且有意义的事儿。
如果想要体验的可以后台私信我。如果你也有很棒的想法想交流一下,也可以私我。
我是dev,下期见(太懒了我,更新频率太低)
来源:juejin.cn/post/7407374655109283851
Linux新系统正式发布,易用性直接向Windows看齐!
提到 Linux Mint 这个系统,相信不少喜欢折腾Linux系统的小伙伴可能之前有尝试过。
该系统旨在为普通用户提供一个免费、易用、舒适、优雅的桌面操作系统。
就在不久前,备受期待的 Linux Mint 22(代号为“Wilma”)正式官宣发布,这一消息也在对应的爱好者圈子里引起了一阵关注和讨论。
作为Linux Mint系列的一个重要里程碑,Linux Mint 22不仅继承了Ubuntu 24.04 LTS的稳定性和安全性,还在此基础上进行了大量的改进和优化,为用户带来了全新的桌面体验。
长期支持
本次发布的全新Linux Mint 22作为一个难得的长期支持版(LTS),将更新支持到2029年,期间将会定期推送安全更新。
这意味着在未来几年中,用户可以享受到稳定且持续的安全更新。
内核升级
新版Linux Mint 22与时俱进,同样采用了Linux 6.8内核,这一更新不仅提升了与现代硬件、应用程序和软件包的兼容性,还带来了更好的系统性能和稳定性。
此外,内核的升级也为后续的维护和升级提供了更广阔的空间。
桌面环境
Linux Mint 22默认搭载了Cinnamon 6.2桌面环境,为用户带来更加流畅、智能和高效的桌面体验,并且同时提供了Xfce和MATE版本供用户选择。
Cinnamon 6.2带来了诸多新特性和改进,从而进一步提升用户体验。
- 启动应用管理更便捷:添加启动应用时,搜索栏默认显示,方便用户快速定位所需应用。
- 工作区管理更灵活:工作区切换器支持用鼠标中键删除工作区,操作更加直观;Cornerbar小程序允许自定义点击操作,提升效率。
- 快捷键和Spices功能增强:支持可配置的快捷键绑定,键盘快捷方式编辑器新增搜索功能,设置更便捷。
- 界面优化:用户小程序可在面板显示个人头像,提升个性化程度;Cinnamon会话界面新增欢迎徽章,提升用户体验;屏幕键盘添加关闭按钮,使用更方便,等等。
软件管理器
Linux Mint 22的一大重点更新就是针对mintinstall软件管理器的改进。
新版本不仅提升了加载速度,还增加了多线程支持、新的偏好设置页面和横幅幻灯片。
新的软件管理器默认禁用未验证的Flatpak软件包,以提高系统的安全性。同时,已验证的Flatpak软件包会显示维护者姓名,以增加用户信任度,而如果要启用未经验证的 Flatpak 软件包,它们将被清楚地标记出来。
其他更新
Linux Mint 22还带来了其他诸多更新,比如:
- 高分辨率屏幕支持得到增强,确保在不同分辨率下都能获得最佳显示效果;
- 默认音频服务器切换为了PipeWire,以提供更好的音频处理和兼容性;
- 所有使用libsoup2的软件均迁移到libsoup3;
- 支持GTK4;
- Matrix加持;
- ……等等。
此外,Linux Mint 22还包含了大量底层的Bug修复、稳定性提升和性能优化,这些都是为了确保系统运行更加流畅、稳定。
后记
总而言之,这次Linux Mint 22的发布,使得Linux桌面系统的易用性又进了一步。
感兴趣的小伙伴也可以直接去官网下载ISO镜像来安装使用。
我也特地看了一下新版本安装要求,对机器配置要求还真不高,最近有时间我也准备收拾出来一台老电脑来安装试试。
文章的最后也期待Linux桌面系统在未来能百花齐放,发展得越来越好。
来源:juejin.cn/post/7411032557074710543
多语言翻译你还在一个个改?我不允许你不知道这个工具
最近在做项目的多语言翻译,由于是老项目,里面需要翻译的文本太多了,如果一个个翻译的话,一个人可能一个月也做不完,因此考虑使用自动化工具实现。
我也从网上搜索了很多现有的自动化翻译工具,有vsc插件、webpack或vite插件、webpack loader等前人实现的方案,但是安装之后发现,要不脱离不了一个个点击生成的繁琐,要不插件安装太麻烦,安装报错依赖报错等等问题层出不穷。因此,决定自己写一个,它需要满足:
1.不需要手动改代码,自动运行生成
2.不需要查翻译,自动调用翻译接口生成翻译内容
3.不需要安装,避免安装问题、环境问题等
i18n-cli 工具的产生
经过一个星期左右的开发和调试,i18n-cli自动化翻译工具实现了,详情可以看@tenado/i18n-cli。先来看看使用效果:
转换前:
<template>
<div class="empty-data">
<div class="name">{{ name }}</div>
<template>
<div class="empty-image-wrap">
<img class="empty-image" :src="emptyImage" />
</div>
<div class="empty-title">暂无数据</div>
</template>
</div>
</template>
<script lang="js">
import Vue from "vue";
export default Vue.extend({
data(){
return {
name: "测试"
}
},
});
</script>
转换后:
<template>
<div class="empty-data">
<div class="name">{{ name }}</div>
<template>
<div class="empty-image-wrap">
<img class="empty-image" :src="emptyImage" />
</div>
<div class="empty-title">{{ $t("zan-wu-shu-ju") }}</div>
</template>
</div>
</template>
<script lang="js">
import { i18n } from 'i18n';
import Vue from "vue";
export default Vue.extend({
data() {
return {
name: i18n.t('ce-shi')
};
}
});
</script>
@tenado/i18n-cli翻译,不受语言类型限制,目前vue、react、vue3等代码都能完美的支持,它通过分析语法树,自动匹配中文内容,并生成翻译后的代码。
如何使用 i18n-cli 工具
1.下载@tenado/i18n-cli项目代码,例如存作i18n-cli
2.将需要翻译的代码文件,拷贝到i18n-cli项目下的目录下
3.修改 i18n.config.js 配置,修改入口entry为你刚复制的文件的位置,修改你需要翻译的语言列表langs,例如英文、繁体['en-US', 'zh-TW'],修改引入i18n的方法i18nImport、i18nObject、i18nMethod,修改翻译的类型和秘钥,一个简单的配置如下:
module.exports = {
// 入口位置
entry: ['example/transform-i-tag'],
// 翻译后的文件存放位置
localPath: './example/transform-i-tag/locales',
// 需要翻译的语言列表
langs: ['en-US'],
// 引入i18n
i18nImport: "import { t } from 'i18n';",
i18nObject: '',
i18nMethod: 't',
// 翻译配置,例如百度
translate: {
type: 'baidu',
appId: '2023088292121',
secretKey: 'J1ArqOof1s8kree',
interval: 1000,
},
};
4.在i18n-cli项目下执行命令,npm run sync
,将会修改你刚复制的文件里面的代码,并在locales下生成翻译内容,这里如果没有百度翻译api key,那你可以先收集,后面在翻译,先执行 npm run extract
,再执行 npm run translate
5.将修改后的文件复制回你的项目下
当然,i18n-cli 的配置不是仅仅这些,更多配置你可以去 @tenado/i18n-cli 对应的 github 仓库上查看
i18n-cli 是怎么实现的
1、收集文件
根据入口,获取需要处理的文件列表,主要代码如下:
// 根据入口获取文件列表
const getSourceFiles = (entry, exclude) => {
return glob.sync(`${entry}/**/*.{js,ts,tsx,jsx,vue}`, {
ignore: exclude || [],
})
}
// 例如 getSourceFiles('src/components')
// 结果 ['src/components/Select/index.vue', 'src/components/Select/options.vue', 'src/components/Select/index.js']
2、转换文件
根据文件类型,生成不同文件的语法树,例如.vue文件分别解析vue的template、style、script三个部分,例如.ts、.tsx文件,例如html,解析成ast语法树后,针对不同类型的中文分别处理,如下是babel转换ast时候里面的一部分核心代码:
const { declare } = require("@babel/helper-plugin-utils");
const generate = require("@babel/generator").default;
module.exports = declare((api, options) => {
return {
visitor: {
// 针对不同类型的中文,进行转换
// 代码太多,这里不贴全部,具体的可以去github上查看源码
DirectiveLiteral() {},
StringLiteral() {},
TemplateLiteral() {},
CallExpression() {},
ObjectExpression() {},
},
};
});
3、调用接口翻译
根据locales文件存放位置,把收集到的中文都存在./locales/zh-CN.json
里面,收集中文和key是在文件转换过程处理的。
这个过程,会根据生成的中文json,去请求接口,拿到中文对应语言的翻译,实现代码如下:
const fetch = require("node-fetch");
const md5 = require('md5');
const createHttpError = require('http-errors');
const langMap = require("./langMap.js");
const defaultOptions = {
from: "auto",
to: "en",
appid: "",
salt: "wgb236hj",
sign: "",
}
module.exports = async (text, lang, options) => {
const hostUrl = "http://api.fanyi.baidu.com/api/trans/vip/translate";
let _options = {
q: text,
...defaultOptions,
}
const { local } = options ?? {};
const { appId, secretKey } = options?.translate ?? {};
if(local) {
_options.from = langMap('baidu', local);
}
if(lang) {
_options.to = langMap('baidu', lang);
}
_options.appid = appId;
const str = `${_options.appid}${_options.q}${_options.salt}${secretKey}`;
_options.sign = md5(str);
const buildBody = () => {
return new URLSearchParams(_options).toString();
}
const buildOption = () => {
const opt = {};
opt.method = 'POST';
opt.headers = {
'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8',
}
opt.body = buildBody();
return opt;
}
const buildError = async (res) => {
const extractTooManyRequestsInfo = (html) => {
const ip = html.match(/IP address: (.+?)<br>/)?.[1] || '';
const time = html.match(/Time: (.+?)<br>/)?.[1] || '';
const url = (html.match(/URL: (.+?)<br>/)?.[1] || '').replace(/&/g, '&');
return { ip, time, url };
}
if (res.status === 429) {
const text = await res.text();
const { ip, time, url } = extractTooManyRequestsInfo(text);
const message = `${res.statusText} IP: ${ip}, Time: ${time}, Url: ${url}`;
return createHttpError(res.status, message);
} else {
return createHttpError(res.status, res.statusText);
}
}
const buildText = ({ error_code, error_msg, trans_result }) => {
if(!error_code) {
return trans_result?.map(item => item.dst);
} else {
console.error(`百度翻译报错: ${error_code}, ${error_msg}`)
return '';
}
}
const fetchOption = buildOption();
const res = await fetch(hostUrl, fetchOption)
if(!res.ok) {
throw await buildError(res)
}
const raw = await res.json();
const _text = buildText(raw);
return _text;
}
总结
i18n-cli 是一个全自动的国际化插件,可以一键翻译多国语言,同时不会影响项目的业务代码,对于国际化场景是一个很强大的工具。
使用 i18n-cli 可以大大减少项目多语言翻译的工作量,这个插件已经在我们项目中使用很久了,是一个成熟的方案,欢迎大家使用,欢迎提交issues,欢迎star。
来源:juejin.cn/post/7327969921309065216
移动开发者终于失去了往日荣耀?
部分原因在于AI浪潮. 但可能还有其他原因.
作为一名资深移动开发者, 我渴望了解苹果, 安卓和跨平台生态系统的最新趋势. 然而, 最近业界发出的信号并不令人振奋.
这篇文章并不是要讨论在最新的 WWDC 或 Google I/O 上开发者们被灌输了什么. 我们要讨论的是当前的趋势是如何影响开发者的职业生涯的.
移动开发人员的工作岗位正在迅速消失 -- 至少我在 脉脉 上搜索到的情况是这样. 关于这一点, 还有一个有趣的Reddit讨论, 以及StackOverflow 开发人员调查开发人员数量统计: 移动开发调查受访者人数从 12.45% 降至 3.38%.
虽然行业领先论坛的结果不足以详细反映真实问题, 但线上+线下论坛的集体反馈也能反映些许蛛丝马迹.
我们是如何走到这一步的?
就在不到十年前, 移动开发还是最酷的技术. 每家领先的科技公司都在努力征服 iOS 和 Android 这两个平台.
近十年来, 原生平台和跨平台之争持续不断. 在此之前, Cordova, Xamarin 和 Titanium 是每个技术Manager口中的热门词汇. 这些框架在技术上并不出众, 但它们成功地维持了业界最喜欢的“一次开发, 到处发布, 节省资金”的信念.
同类产品的第二波浪潮以 React Native 和 Flutter 等跨平台框架的形式出现. Facebook 和谷歌称赞它们是完全原生的.
但是, 只有每天与它们打交道的程序员才知道, 在开发具有流畅性, 高性能和可玩性(利用传感器功能--使移动体验更加深入和个性化)用户体验(智能手机人机交互的最大组成部分)的大型应用时, 它们是多么的力不从心.
虽然用户体验是由 Facebook 和谷歌开发的, 但它们的整个发展历程都是由那些必须向大客户推销开发人员的代理商传播的. 在以降低开发成本为价值主张时, 他们开始声嘶力竭地高唱跨平台的大戏. 为了提高代理公司的投资组合, 他们还通过开发组件来扩大各自框架的 GitHub 代码仓库. Facebook 和谷歌的开发人员看着自己的边缘项目蓬勃发展, 乐得合不拢嘴. 大家都很高兴.
实际上, 从长远来看, 跨平台项目让拥有项目的公司付出了更大的代价, 因为这种方法存在明显的缺陷:
- 只提供两个平台的最大公约数.
- 开发人员疲劳(除了《Hello World》, 开发人员无论如何都得学习本地程序)
- 开源(因此没有问责制)开发.
在完全原生的跨平台工具(Unity 及其朋友)与忠实于原生的 XCode 和 Eclipse 之间也发生了有趣的采用战争, 不过, 根据设计, 这场战争仅限于游戏开发.
这些战争是否削弱了移动开发事业? 也不尽然. 但它们确实割裂了普通开发人员对行业的认知. 新手急于跳槽, “得学那个东西(跨平台)”, 这将是他们进入IT行业的单程票, 但后来却失望了. 老手们还在坚持使用那些经过时间考验的东西(C++, Java, 以及后来的 Swift, Kotlin), 但他们常常发现, 由于这样或那样的原因, 他们很难在不断摇摆不定的市场中立足.
尤其是, 摇摆不定.
是AI, 还是其他原因?
似乎随着 GenAI 的到来, 关于移动开发的讨论已经酝酿成型.
然而, 移动开发的第一块多米诺骨牌倒在了 2017 年, 当时剑桥分析的宝贝竟然是从 Facebook 的壁橱里走出来的.
西方世界的民主理念与隐私紧密相连. 未经同意追踪用户成了至今不被承认的罪过. 剑桥分析丑闻引发了政府在全球范围内对大型科技公司的攻击.
Facebook就是最直接的受害者. 谷歌作为智能手机市场最大的利益相关者, 同时也是全球著名的雇主和政府服务提供商(GCP, 谷歌教育等), 成功地争取到了时间. 为了遵守规定, 谷歌对其广告产品和货币化 SDK 进行了多次修改. 安卓开发者的收入来源主要是广告, 他们不得不拖着不做, 否则就会失去市场.
苹果公司站在隐私保护的制高点上, 选择成为手握大棒的人, 将广告追踪的权力交到用户手中. Facebook 成为此举的最大受害者. 为了支持这一立场, 苹果开始将其备受推崇的订阅模式奉为应用开发的黄金标准. 2020 年是苹果开始改进其备受诟病的订阅 API 支持的第一年.
隐私合规对移动开发行业的冲击是前所未有的. 独立开发者受到的冲击最大, 但公司级的开发也放慢了脚步. 开发人员本可用于创建新框架的时间开始被浪费在与支持和法律专业人士进行无休止的问答, 快速修复以及与企业主进行无用的讨论上.
与此同时, 无代码也在兴起. 虽然它的成功还远未得到验证, 但它已成为管理讨论中的一个主要观点, 并成为主流开发的有力竞争者.
一些小型开发公司选择转向无代码/网页产品开发上. 大公司不再在移动交付方面冒大的风险. 预算不再流向开源项目. 维护商店评级成了新的焦点, 为此, 预先开发的功能大多已经足够了.
“不要破坏已有的工作. 不要冒险去创造什么了不起的东西, 因为我们不知道什么叫了不起."成了新的口头禅.
移动开发人员无法应对这种对他们有利的转变. 为什么? 这就引出了我们的下一个问题.
移动开发者的真正力量在哪里?
这要看什么是优秀移动开发者的真正定义.
优秀开发人员的标准定义(“设计师, 编码员, 测试员”)并不能完全定义优秀的移动开发人员.
关于“怎样才能成为一名优秀的移动开发人员”, 人们几乎没有达成共识. 移动开发技能与网页开发技能几乎没有区别.
一个优秀的移动应用应该更多地与UE和UI有关, 还 应该与智能手机传感器的巧妙使用有关. 优秀开发人员的标准定义(“程序设计师, 编码员, 测试员”)并不能完全定义优秀的移动开发人员. 一个人首先必须是一名优秀的开发人员, 但还不止于这些.
绑定(UI + 数据库 + API)必须弄清楚. 必须选择在架构上最合适的. MVC 是否足够? 还是需要ViewModel? Coordinator等顽固模式呢?
设计的坏处在于没有经过验证的验证器. 衡量一个好设计的唯一指标是, 未来的开发人员是否能在其能力和主动性的基础上进行改进.
说到移动开发, 还必须掌握硬件集成的工作流程. 例如, 蓝牙, 加速计, 光传感器, 陀螺仪等. 这正是嵌入式工程师的优势所在. 金融技术(苹果和谷歌支付)增加了另一个维度: NFC.
虽然成功集成所有这些功能需要付出大量的精力和时间, 但从管理/领导的角度来看, 它们只是具有相同即插即用接口的简单盒子. 尽管 “Mobile First”的口号已经深入人心, 个人体验也越来越丰富, 但在公司的 IT 战略中, 移动仍然 只是一个辅助出口.
一个像样的移动应用代码库需要一个专门的架构师, 但小型团队很少有这样的人. 即使是大公司的团队也会因为平台分散而在此方面偷工减料.
只有大公司才有能力开发可重复使用的库和框架. 中小型公司的团队没有护城河, 无法证明在平台巨头随时可能破坏任何东西的情况下, 他们的长期努力是有道理的.
与网络不同的是, 网络上的伟大感知来自于个人创作者, 而移动应用的伟大则是由平台决定的: 苹果和谷歌. 《人机界面指南》和《材料设计》标准引导着业界的期望.
然而, 在大多数情况下, 这些标准仍未得到充分展示. 这是因为定义优秀应用的标准是在没有此类范例的情况下制定的. 它们的示例大多是开发人员必须在此基础上构建的简易版.
这种设计是一种简化主义, 阻碍了许多可能的创新, 只有那些获得平台奖励(安卓和苹果编辑选择的舞台)的创新才会不断被复制.
超越标准的杰出移动体验只是一个例外(设计师在其中同样发挥着重要作用). 规则就是要萧规曹随, 即使是最杰出的移动开发者也不得不这样做.
如果一个独立移动开发者向世界展示了他/她能为这些平庸的平台带来惊喜, 那么这些平台就会刺探他/她的作品, 并将其打上自己的烙印.
总结一下
AI(特别是 GenAI)正在吸走其他各个部门的资金. Manager们想证明自己没有错过潮流,即使他们知道这只是错失良机的焦虑。.
在移动端大语言模型成为现实之前, 移动开发人员几乎不可能再次大放异彩. 在那里, 他们将不得不与数据科学家分享荣耀.
当形势一片大好时, 所有开发人员都会有机地成长为镇上最大的赢家. 当经济不景气时, 有能力的开发人员会通过创建框架, 库和可重复使用的组件来倍增他们的影响力. 中低技能的开发人员受到顶级开发人员的启发, 开始提升自己的技能. 当他们再也找不到这条路时, 他们就会转换领域. 他们离开狭窄的溪流(移动开发), 开始在海洋中游泳, 模式如下:
同一平台的所有设备(Mac, Vision pro) => 跨平台(iOS + Android) => 网页开发(+web).
当情况变得更糟时, 大多数水手都会下船. 船长会留下来. 但当海盗占据上风时, 船长们也会被屠杀. 如果船还算幸运, 海盗们将会考虑重建.
入门级开发人员或来自边缘领域的开发人员就是这些海盗. 他们受雇充当临时工, 让这艘船保持漂浮, 直到老手重新发现它.
这就是今天移动开发的现状. 它还没有被毁灭. 也永远不会. 中级入门级开发人员将移动开发保持在 2015 年的水平.
这些人热衷于通过构建组合应用来炫耀自己的技能, 他们不想购买域名/托管服务, 并且对 500 多个 NPM/Yarn 软件包望而却步.
这些新手能否夺回平台? 更重要的是, 他们能找到值得热爱的创作领域吗? 唯一的办法就是超越单纯的软件开发, 成为硬件 + 设计的大师.
在AI重塑每个人期望的时代, 这种可能性并不存在.
一家之言, 欢迎拍砖!
Happy Coding! Stay GOLDEN!
来源:juejin.cn/post/7410989416866955291
pnpm 的崛起:如何降维打击 npm 和 Yarn🫡
今天研究了一下 pnpm
的机制,发现它确实很强大,甚至可以说对 yarn
和 npm
形成了降维打击
我们从包管理工具的发展历史,一起看下到底好在哪里?
npm2
在 npm 3.0 版本之前,项目的 node_modules
会呈现出嵌套结构,也就是说,我安装的依赖、依赖的依赖、依赖的依赖的依赖...,都是递归嵌套的
node_modules
├─ express
│ ├─ index.js
│ ├─ package.json
│ └─ node_modules
│ ├─ accepts
│ │ ├─ index.js
│ │ ├─ package.json
│ │ └─ node_modules
│ │ ├─ mime-types
| | | └─ node_modules
| | | └─ mime-db
| │ └─ negotiator
│ ├─ array-flatten
│ ├─ ...
│ └─ ...
└─ A
├─ index.js
├─ package.json
└─ node_modules
└─ accepts
├─ index.js
├─ package.json
└─ node_modules
├─ mime-types
| └─ node_modules
| └─ mime-db
└─ negotiator
设计缺陷
这种嵌套依赖树的设计确实存在几个严重的问题
- 路径过长问题: 由于包的嵌套结构 ,
node_modules
的目录结构可能会变得非常深,甚至可能会超出系统路径长度上限 ,毕竟 windows 系统的文件路径默认最多支持 256 个字符 - 磁盘空间浪费: 多个包之间难免会有公共的依赖,公共依赖会被多次安装在不同的包目录下,导致磁盘空间被大量浪费 。比如上面
express
和 A 都依赖了accepts
,它就被安装了两次 - 安装速度慢:由于依赖包之间的嵌套结构,
npm
在安装包时需要多次处理和下载相同的包,导致安装速度变慢,尤其是在依赖关系复杂的项目中
当时 npm 还没解决这些问题, 社区便推出了新的解决方案 ,就是 yarn。 它引入了一种新的依赖管理方式——扁平化依赖。
看到 yarn 的成功,npm 在 3.0 版本中也引入了类似的扁平化依赖结构
yarn
yarn 的主要改进之一就是通过扁平化依赖结构来解决嵌套依赖树的问题
具体来说铺平,yarn 尽量将所有依赖包安装在项目的顶层 node_modules
目录下,而不是嵌套在各自的 node_modules
目录中。
这样一来,减少了目录的深度,避免了路径过长的问题 ,也尽可能避免了依赖被多次重复安装的问题
我们可以在 yarn-example 看到整个目录,全部铺平在了顶层 node_modules
目录下,展开下面的包大部分是没有二层 node_modules
的
然而,有些依赖包还是会在自己的目录下有一个 node_modules
文件夹,出现嵌套的情况,例如 yarn-example 下的http-errors
依赖包就有自己的 node_modules
,原因是:
当一个项目的多个依赖包需要同一个库的不同版本时,yarn 只能将一个版本的库提升到顶层 node_modules
目录中。 对于需要这个库其他版本的依赖,yarn 仍然需要在这些依赖包的目录下创建一个嵌套的 node_modules
来存放不同版本的包
比如,包 A 依赖于 lodash@4.0.0
,而包 B 依赖于 lodash@3.0.0
。由于这两个版本的 lodash
不能合并,yarn
会将 lodash@4.0.0
提升到顶层 node_modules
,而 lodash@3.0.0
则被嵌套在包 B 的 node_modules
目录下。
幽灵依赖
虽然 yarn 和 npm 都采用了扁平化的方案来解决依赖嵌套的问题,但这种方案本身也有一些缺陷,其中幽灵依赖是一个主要问题。
幽灵依赖,也就是你明明没有在 package.json
文件中声明的依赖项,但在项目代码里却可以 require
进来
这个也很容易理解,因为依赖的依赖被扁平化安装在顶层 node_modules
中,所以我们能访问到依赖的依赖
但是这样是有隐患的,因为没有显式依赖,未来某个时候这些包可能会因为某些原因消失(例如新版本库不再引用这个包了,然后我们更新了库),就会引发代码运行错误
浪费磁盘空间
而且还有一个问题,就是上面提到的依赖包有多个版本的时候,只会提升一个,那其余版本的包不还是复制了很多次么,依然有浪费磁盘空间的问题
那社区有没有解决这俩问题的思路呢? pnpm 就是其中最成功的一个
pnpm
pnpm 通过全局存储和符号链接机制从根源上解决了依赖重复安装和路径长度问题,同时也避免了扁平化依赖结构带来的幽灵依赖问题
pnpm 的优势概括来说就是“快、准、狠”:
- 快:安装速度快
- 准:安装过的依赖会准确复用缓存,甚至包版本升级带来的变化都只 diff,绝不浪费一点空间
- 狠:直接废掉了幽灵依赖
执行 npm add express
,我们可以在 pnpm-example 看到整个目录,由于只安装了 express
,那 node_modules
下就只有 express
那么所有的(次级)依赖去哪了呢? binggo,在node_modules/.pnpm/
目录下,.pnpm/
以平铺的形式储存着所有的包
三层寻址
- 所有 npm 包都安装在全局目录
~/.pnpm-store/v3/files
下,同一版本的包仅存储一份内容,甚至不同版本的包也仅存储 diff 内容。 - 顶层
node_modules
下有.pnpm
目录以打平结构管理每个版本包的源码内容,以硬链接方式指向 pnpm-store 中的文件地址。 - 每个项目
node_modules
下安装的包以软链接方式将内容指向node_modules/.pnpm
中的包。
所以每个包的寻找都要经过三层结构:node_modules/package-a
> 软链接node_modules/.pnpm/package-a@1.0.0/node_modules/package-a
> 硬链接~/.pnpm-store/v3/files/00/xxxxxx
。
这就是 pnpm 的实现原理。官方给了一张原理图,可以搭配食用
前面说过,npm 包都被安装在全局
pnpm store
,默认情况下,会创建多个存储(每个驱动器(盘符)一个),并在项目所在盘符的根目录
所以,同一个盘符下的不同项目,都可以共用同一个全局
pnpm store
,绝绝子啊👏,大大节省了磁盘空间,提高了安装速度
软硬链接
也就是说,所有的依赖都是从全局 store 硬连接到了 node_modules/.pnpm
下,然后之间通过软链接来相互依赖。
那么,这里的软连接、硬链接到底是什么东西?
硬链接是指向磁盘上原始文件所在的同一位置 (直接指向相同的数据块)
软连接可以理解为新建一个文件,它包含一个指向另一个文件或目录的路径 (指向目标路径)
总结
npm2 的嵌套结构: 每个依赖项都会有自己的 node_modules
目录,导致了依赖被重复安装,严重浪费了磁盘空间💣;在依赖层级比较深的项目中,甚至会超出 windows 系统的文件路径长度💣
npm3+ 和 Yarn 的扁平化策略: 尽量将所有依赖包安装在项目的顶层 node_modules
目录下,解决了 npm2
嵌套依赖的问题。但是该方案有一个重大缺陷就是“幽灵依赖”💣;而且依赖包有多个版本时,只会提升一个,那其余版本依然会被重复安装,还是有浪费磁盘空间的问题💣
pnpm全局存储和符号链接机制: 结合软硬链和三层寻址,解决了依赖被重复安装的问题,更加变态的是,同一盘符下的不同项目都可以共用一个全局 pnpm store
。节省了磁盘空间,并且根本不存在“幽灵依赖”,安装速度还贼快💪💪💪
来源:juejin.cn/post/7410923898647461938
代码与蓝湖ui颜色值一致!但页面效果出现色差问题?
前言
最近在开发新需求,按照蓝湖的ui图进行开发,但是在开发完部署后发现做出来的页面部分元素的颜色和设计图有出入,有色差!经过一步步的排查最终破案,解决。仅以此篇记录自己踩坑、学习的过程,也希望可以帮助到其他同学。
发现问题
事情是这样的,那是一个愉快的周五的下午,和往常一样我开心的提交了代码后进行打包发版,然后通知负责人查看我的工作成果。
但是,过了不久后,负责人找到了我,说我做出来的效果和ui有点出入,有的颜色有点不一样。我一脸懵逼,心想怎么可能呢,我是根据ui图来的,ui的颜色可是手把手从蓝湖复制到代码中的啊。
随后他就把页面和ui的对比效果图发了出来:
上图中左侧是蓝湖ui图,右侧是页面效果图。我定睛一看,哇趣!!!好像是有点不一样啊。 感觉右侧的比左侧的更亮一些。于是我赶紧本地查看我的页面和ui,果然也是同样问题! 开发时真的没注意,没发现这个问题!!!
排查问题
于是,我迅速开始进行问题排查,看看到底是什么问题,是值写错了?还是那里的问题。
ui、页面、代码对比
下图中:最上面部分是蓝湖ui图、下面左侧是我的页面、右侧是我的页面代码样式
仔细检查后发现颜色的值没错啊,我的代码中背景颜色、边框颜色的值都和ui的颜色值是一致的! 但这是什么问题呢??? 值都一样为什么渲染到页面会出现色差?
起初,我想到的是屏幕的问题,因为不同分辨率下展示出来的页面效果是会有差距的。但是经过查看发现同事的win10笔记本、我的mac笔记本、外接显示器上都存在颜色有色差这个问题!!!
ui、页面、源文件对比
通过对比ui、页面、颜色值,不同设备展示效果可以初步确认:和显示器关系不大。当我在百思不解的时候,我突然想到了ui设计师!ui提供的ui图是蓝湖上切出来的,那么她的源文件颜色是什么呢?
于是我火急火燎的联系到了公司ui小姐姐,让她发我源文件该元素的颜色值,结果值确实是一样的,但是!!! 源文件展示出来的效果好像和蓝湖上的不太一样!
然后我进行了对比(左侧蓝湖、右上页面、右下源文件):
可以看到源文件和我页面的效果基本一致!到这一步基本可以确定我的代码是没问题的!
尝试解决
首先去网上找了半天没有找到想要的答案,于是我灵光一现,想到了蓝湖客服!然后就询问了客服,为什么上传后的ui图内容和源文件有色差?
沟通了很久,期间我又和ui小姐姐在询问她的软件版本、电脑版本、源文件效果、设置等内容就不贴了,最终得到如下解答:
解决方式
下载最新版蓝湖插件,由于我们的ui小姐姐用的 sketch
切图工具,然后操作如下:
1.下载安装最新版蓝湖插件: lanhuapp.com/mac?formHea…
2.安装新版插件后--插件重置
3.后台程序退出 sketch
,重新启动再次尝试打开蓝湖插件.
4.插件设置打开高清导出上传(重要!)
5.重新切图上传蓝湖
最终效果
左侧ui源文件、右侧蓝湖ui:
页面效果:
可以看到我的页面元素的border
好像比ui粗一些,感觉设置0.5px就可以了,字体效果的话是因为我还没来得及下载ui对应的字体文件。
但是走到这一步发现整体效果已经和ui图到达了95%以上相似了,不至于和开始有那么明显的色差。
总结
至此,问题已经基本是解决。遇到问题不能怕,多想一想,然后有思路后就一步一步排查、尝试解决问题。当解决完问题后会发现心情舒畅!整个人都好起来了,也会增加自信心!
来源:juejin.cn/post/7410712345226035200
只因把 https 改成 http,带宽减少了 70%!
起因
是一个高并发的采集服务上线后,100m的上行很快就被打满了。
因为这是一条专线,并且只有这一个服务在使用,所以可以确定就是它导致的。
但是!这个请求只是一个 GET 请求,同时并没有很大的请求体,这是为什么呢?
于是使用 charles 重新抓包后发现,一个 request 的请求居然要占用 1.68kb 的大小!
其中TLS Handshake 就占了 1.27kb。
这种情况下,需要的上行带宽就是:1.68*20000/1024*8=262.5mbps
也就说明100mbps的上行为何被轻松打满
TLS Handshake是什么来头,竟然如此大?
首先要知道HTTPS全称是:HTTP over TLS,每次建立新的TCP连接通常需要进行一次完整的TLS Handshake。在握手过程中,客户端和服务器需要交换证书、公钥、加密算法等信息,这些数据占用了较多的字节数。
TLS Handshake的内容主要包括:
- 客户端和服务器的随机数
- 支持的加密算法和TLS版本信息
- 服务器的数字证书(包含公钥)
- 用于生成对称密钥的“Pre-Master Secret”
这个过程不仅耗时,还会消耗带宽和CPU资源。
因此想到最粗暴的解决方案也比较简单,就是直接使用 HTTP,省去TLS Handshake的过程,那么自然就不会有 TLS 的传输了。
那么是否真的有效呢?验证一下就知道。
将请求协议改成 http 后:
可以看到请求头确实不包含 TLS Handshake了!
整个请求只有 0.4kb,节省了 70% 的大小
目标达成
因此可以说明:在一些不是必须使用 https 的场景下,使用 http 会更加节省带宽。
同时因为减少了加密的这个过程,可以观察到的是,在相同的并发下,服务器的负载有明显降低。
那么问题来了
如果接口必须使用 https那怎么办呢?
当然还有另外一个解决方案,那就使用使用 Keep-Alive
。
headers 中添加 Connection: keep-alive
即可食用。
通过启用 Keep-Alive,
可以在同一TCP连接上发送多个HTTPS请求,
而无需每次都进行完整的TLS Handshake,
但第一次握手时仍然需要传输证书和完成密钥交换。
对于高并发的场景也非常适用。
要注意的是
keep-alive 是有超时时间的,超过时间连接会被关闭,再次请求需要重新建立链接。
Nginx 默认的 keep-alive
超时是 75 秒,
Apache HTTP 服务器 通常默认的 keep-alive
超时是 5 秒。
ps:
如果你的采集程序使用了大量的代理 ip那么 keep-alive 的效果并不明显~~
最好的还是使用 http
来源:juejin.cn/post/7409138396792881186
数据可视化工具库比较与应用:ECharts、AntV、D3、Zrender
ECharts
ECharts是一个由百度开发的强大的数据可视化库,它提供了丰富的图表类型和灵活的配置选项。以下是一个简单的示例,展示如何使用Echarts创建一个折线图:
import * as echarts from 'echarts';
const chartDom = document.getElementById('main');
const myChart = echarts.init(chartDom);
const option = {
xAxis: {
type: 'category',
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
},
yAxis: {
type: 'value'
},
series: [
{
data: [150, 230, 224, 218, 135, 147, 260],
type: 'line'
}
]
};
option && myChart.setOption(option);
如上步骤,简单易用,难点都封装好了,只需要配置数据即可。如果需要在网页中快速展示图表信息,刚好这个图表是比较常规的,不需要过多地调整和配置,就可以采用ECharts。
Antv
Antv是蚂蚁金服开发的数据可视化库,它基于G2和G6,提供了一系列强大的图表和可视化组件。下面是一个使用Antv使用G2产品创建折线图的示例:
import { Chart } from "@antv/g2";
const chart = new Chart({ container: "container" });
chart.options({
type: "view",
autoFit: true,
data: [
{ year: "1991", value: 3 },
{ year: "1992", value: 4 },
{ year: "1993", value: 3.5 },
{ year: "1994", value: 5 },
{ year: "1995", value: 4.9 },
{ year: "1996", value: 6 },
{ year: "1997", value: 7 },
{ year: "1998", value: 9 },
{ year: "1999", value: 13 },
],
encode: { x: "year", y: "value" },
scale: { x: { range: [0, 1] }, y: { domainMin: 0, nice: true } },
children: [
{ type: "line", labels: [{ text: "value", style: { dx: -10, dy: -12 } }] },
{ type: "point", style: { fill: "white" }, tooltip: false },
],
});
chart.render();
Antv提供了简单易用的API和丰富的图表组件,可以帮助开发者快速构建各种类型的数据可视化图表。在官网可以看到由七个模块产品,分别是:
G2|G2Plot:可视化图形语法和通用图表库
S2:多维可视分析表格
G6|Graphin:关系数据可视化分析工具和图分析组件
X6|XFlow:流程图相关分图表和组件
L7|L7Plot:地理空间数据可视化框架和地理图表
F2|F6:移动端的可视化解决方案
AVA:可视分析技术框架
D3
import * as d3 from "d3";
import {useRef, useEffect} from "react";
export default function LinePlot({
data,
width = 640,
height = 400,
marginTop = 20,
marginRight = 20,
marginBottom = 30,
marginLeft = 40
}) {
const gx = useRef();
const gy = useRef();
const x = d3.scaleLinear([0, data.length - 1], [marginLeft, width - marginRight]);
const y = d3.scaleLinear(d3.extent(data), [height - marginBottom, marginTop]);
const line = d3.line((d, i) => x(i), y);
useEffect(() => void d3.select(gx.current).call(d3.axisBottom(x)), [gx, x]);
useEffect(() => void d3.select(gy.current).call(d3.axisLeft(y)), [gy, y]);
return (
<svg width={width} height={height}>
<g ref={gx} transform={`translate(0,${height - marginBottom})`} />
<g ref={gy} transform={`translate(${marginLeft},0)`} />
<path fill="none" stroke="currentColor" strokeWidth="1.5" d={line(data)} />
<g fill="white" stroke="currentColor" strokeWidth="1.5">
{data.map((d, i) => (<circle key={i} cx={x(i)} cy={y(d)} r="2.5" />))}
</g>
</svg>
);
}
D3不是传统意义上的图表库,是由30个离散库或者模块组成的套件。如果你对其它高级图表库不满意,想使用SVG或Canvas、甚至WebGL滚动自己的图表,那么可以使用D3工具库。
ZRender
ZRender是2D绘图引擎。它提供Canvas、SVG、等多种渲染方式,也是ECharts的渲染器。
import zrender from 'zrender';
var zr = zrender.init(document.getElementById('main'));
var circle = new zrender.Circle({
shape: {
cx: 150,
cy: 50,
r: 40
},
style: {
fill: 'none',
stroke: '#F00'
}
});
zr.add(circle);
console.log(circle.shape.r); // 40
circle.attr('shape', {
r: 50 // 只更新 r。cx、cy 将保持不变。
});
通过 a = new zrender.XXX
方法创建了图形元素之后,可以用 a.shape
等形式获取到创建时输入的属性,但是如果需要对其进行修改,应该使用 a.attr(key, value)
的形式修改,否则不会触发图形的重绘。
从代码规范看,Echarts和D3官网的案例有用到es5的语法,Antv遵循了es6的语法规范,更专业。从灵活程度和使用难易程度来看,ECharts<Antv<D3<ZRender。还有使用到其它图表工具库的,欢迎留言探讨📒
来源:juejin.cn/post/7345105846341648438
前端实现文件预览img、docx、xlsx、pptx、pdf、md、txt、audio、video
前言
最近有接到一个需求,要求前端支持上传制定后缀文件,且支持页面预览,上传简单,那么预览该怎么实现呢,尤其是不同类型的文件预览方案,那么下面就我这个需求的实现,分不同情况来讲解一下👇
具体的预览需求: 预览需要支持的文件类型有: png、jpg、jpeg、docx、xlsx、pptx、pdf、md、txt、audio、video
,另外对于不同文档还需要有定位的功能。例如:pdf
定位到页码,txt
和markdown
定位到文字并滚动到指定的位置,音视频定位到具体的时间等等。
⚠️ 补充: 我的需求是需要先将文件上传到后台,然后我拿到url
地址去展示,对于markdown
和txt
的文件需要先用fetch
获取,其他的展示则直接使用url
链接就可以。
不同文件的实现方式不同,下面分类讲解,总共分为以下几类:
- 自有标签文件:
png、jpg、jpeg、audio、video
- 纯文字的文件:
markdown 、txt
office
类型的文件:docx、xlsx、pptx
embed
引入文件:pdf
iframe
:引入外部完整的网站,例如:https://www.baidu.com/
自有标签文件:png、jpg、jpeg、audio、video
对于图片、音视频的预览,直接使用对应的标签即可,如下:
图片:png、jpg、jpeg
示例代码:
<img src={url} key={docId} alt={name} width="100%" />;
预览效果如下:
音频:audio
示例代码:
预览效果如下:
视频:video
示例代码:
预览效果如下:
关于音视频的定位的完整代码:
import React, { useRef, useEffect } from 'react';
interface IProps {
type: 'audio' | 'video';
url: string;
timeInSeconds: number;
}
function AudioAndVideo(props: IProps) {
const { type, url, timeInSeconds } = props;
const videoRef = useRef(null);
const audioRef = useRef(null);
useEffect(() => {
// 音视频定位
const secondsTime = timeInSeconds / 1000;
if (type === 'audio' && audioRef.current) {
audioRef.current.currentTime = secondsTime;
}
if (type === 'video' && videoRef.current) {
videoRef.current.currentTime = secondsTime;
}
}, [type, timeInSeconds]);
return (
{type === 'audio' ? (
) : (
)}
);
}
export default AudioAndVideo;
纯文字的文件: markdown & txt
对于
markdown、txt
类型的文件,如果拿到的是文件的url
的话,则无法直接显示,需要请求到内容,再进行展示。
markdown
文件
在展示
markdown
文件时,需要满足字体高亮、代码高亮
,如果有字体高亮,需要滚动到字体所在位置
,如果有外部链接,需要新开tab页面
再打开。
需要引入两个库:
marked
:它的作用是将markdown
文本转换(解析)为HTML
。
highlight
: 它允许开发者在网页上高亮显示代码。
字体高亮的代码实现:
高亮的样式,可以在行间样式定义
const highlightAndMarkFirst = (text: string, highlightText: string) => {
let firstMatchDone = false;
const regex = new RegExp(`(${highlightText})`, 'gi');
return text.replace(regex, (match) => {
if (!firstMatchDone) {
firstMatchDone = true;
return `id='first-match' style="color: red;">${match}`;
}
return `style="color: red;">${match}`;
});
};
代码高亮的代码实现:
需要借助
hljs
这个库进行转换
marked.use({
renderer: {
code(code, infostring) {
const validLang = !!(infostring && hljs.getLanguage(infostring));
const highlighted = validLang
? hljs.highlight(code, { language: infostring, ignoreIllegals: true }).value
: code;
return `class="hljs ${infostring}">${highlighted}
`;
}
},
});
链接跳转新tab
页的代码实现:
marked.use({
renderer: {
// 链接跳转
link(href, title, text) {
const isExternal = !href.startsWith('/') && !href.startsWith('#');
if (isExternal) {
return `href="${href}" title="${title}" target="_blank" rel="noopener noreferrer">${text}`;
}
return `href="${href}" title="${title}">${text}`;
},
},
});
滚动到高亮的位置的代码实现:
需要配合上面的代码高亮的方法
const firstMatchElement = document.getElementById('first-match');
if (firstMatchElement) {
firstMatchElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
完整的代码如下:
入参的
docUrl
是markdown
文件的线上ur
l地址,searchText
是需要高亮的内容。
import React, { useEffect, useState, useRef } from 'react';
import { marked } from 'marked';
import hljs from 'highlight.js';
const preStyle = {
width: '100%',
maxHeight: '64vh',
minHeight: '64vh',
overflow: 'auto',
};
// Markdown展示组件
function MarkdownViewer({ docUrl, searchText }: { docUrl: string; searchText: string }) {
const [markdown, setMarkdown] = useState('');
const markdownRef = useRef<HTMLDivElement | null>(null);
const highlightAndMarkFirst = (text: string, highlightText: string) => {
let firstMatchDone = false;
const regex = new RegExp(`(${highlightText})`, 'gi');
return text.replace(regex, (match) => {
if (!firstMatchDone) {
firstMatchDone = true;
return `${match}`;
}
return `${match}`;
});
};
useEffect(() => {
// 如果没有搜索内容,直接加载原始Markdown文本
fetch(docUrl)
.then((response) => response.text())
.then((text) => {
const highlightedText = searchText ? highlightAndMarkFirst(text, searchText) : text;
setMarkdown(highlightedText);
})
.catch((error) => console.error('加载Markdown文件失败:', error));
}, [searchText, docUrl]);
useEffect(() => {
if (markdownRef.current) {
// 支持代码高亮
marked.use({
renderer: {
code(code, infostring) {
const validLang = !!(infostring && hljs.getLanguage(infostring));
const highlighted = validLang
? hljs.highlight(code, { language: infostring, ignoreIllegals: true }).value
: code;
return `${infostring}">${highlighted}
`;
},
// 链接跳转
link(href, title, text) {
const isExternal = !href.startsWith('/') && !href.startsWith('#');
if (isExternal) {
return `${href}" title="${title}" target="_blank" rel="noopener noreferrer">${text}`;
}
return `${href}" title="${title}">${text}`;
},
},
});
const htmlContent = marked.parse(markdown);
markdownRef.current!.innerHTML = htmlContent as string;
// 当markdown更新后,检查是否需要滚动到高亮位置
const firstMatchElement = document.getElementById('first-match');
if (firstMatchElement) {
firstMatchElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}
}, [markdown]);
return (
<div style={preStyle}>
<div ref={markdownRef} />
div>
);
}
export default MarkdownViewer;
预览效果如下:
txt
文件预览展示
支持高亮和滚动到指定位置
支持文字高亮的代码:
function highlightText(text: string) {
if (!searchText.trim()) return text;
const regex = new RegExp(`(${searchText})`, 'gi');
return text.replace(regex, `style="color: red">$1`);
}
完整代码:
import React, { useEffect, useState, useRef } from 'react';
import { preStyle } from './config';
function TextFileViewer({ docurl, searchText }: { docurl: string; searchText: string }) {
const [paragraphs, setParagraphs] = useState<string[]>([]);
const targetRef = useRef<HTMLDivElement | null>(null);
function highlightText(text: string) {
if (!searchText.trim()) return text;
const regex = new RegExp(`(${searchText})`, 'gi');
return text.replace(regex, `$1`);
}
useEffect(() => {
fetch(docurl)
.then((response) => response.text())
.then((text) => {
const highlightedText = highlightText(text);
const paras = highlightedText
.split('\n')
.map((para) => para.trim())
.filter((para) => para);
setParagraphs(paras);
})
.catch((error) => {
console.error('加载文本文件出错:', error);
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [docurl, searchText]);
useEffect(() => {
// 处理高亮段落的滚动逻辑
const timer = setTimeout(() => {
if (targetRef.current) {
targetRef.current.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}, 100);
return () => clearTimeout(timer);
}, [paragraphs]);
return (
<div style={preStyle}>
{paragraphs.map((para: string, index: number) => {
const paraKey = para + index;
// 确定这个段落是否包含高亮文本
const isTarget = para.includes(`>${searchText}<`);
return (
<p key={paraKey} ref={isTarget && !targetRef.current ? targetRef : null}>
<div dangerouslySetInnerHTML={{ __html: para }} />
p>
);
})}
div>
);
}
export default TextFileViewer;
预览效果如下:
office
类型的文件: docx、xlsx、pptx
docx、xlsx、pptx
文件的预览,用的是office
的线上预览链接 + 我们文件的线上url
即可。
关于定位:用这种方法我暂时尝试是无法定位页码的,所以定位的功能我采取的是后端将
office
文件转成
示例代码:
预览效果如下:
embed
引入文件:pdf
在
embed
的方式,这个httpsUrl
就是你的
示例代码:
src={`${httpsUrl}`} style={preStyle} key={`${httpsUrl}`} />;
关于定位,其实是地址上拼接的页码sourcePage
,如下:
const httpsUrl = sourcePage
? `${doc.url}#page=${sourcePage}`
: doc.url;
src={`${httpsUrl}`} style={preStyle} key={`${httpsUrl}`} />;
预览效果如下:
iframe
:引入外部完整的网站
除了上面的各种文件,我们还需要预览一些外部的网址,那就要用到
iframe
的方式
示例代码:
< iframe
title="网址"
width="100%"
height="100%"
src={doc.url}
allow="microphone;camera;midi;encrypted-media;"/>
预览效果如下:
课后附加题:
有些网站设置了
X-Frame-Options
不允许其他网站嵌入,X-Frame-Options
是一个HTTP
响应头,用于控制浏览器是否允许一个页面在<frame>
、<iframe>
、<embed>
、 或<object>
中被嵌入。
X-Frame-Options
有以下三种配置:
- DENY:完全禁止该页面被嵌入到任何框架中,无论嵌入页面的来源是什么。
- SAMEORIGIN:允许同源的页面嵌入该页面。
- ALLOW-FROM uri:允许指定的来源嵌入该页面。这个选项允许你指定一个 URI,只有来自该 URI 的页面可以嵌入当前页面。
但是无论是哪种配置,我们作为非同源的网站,都无法将其嵌入到页面中,且在前端也是拿不到这个报错的信息。
此时我们的解决方案是:
当文档为网址时,由后端服务去请求,检测响应头里是否携带
X-Frame-Options
字段,由后端将是否携带的信息返回前端,前端再根据是否可以嵌入进行页面的个性化展示。
预览效果如下:
总结: 到这里我们支持的所有文件都讲述完了,有什么问题,欢迎评论区留言!
链接:juejin.cn/post/7366432628440924170
作为技术Leader如何带散一个团队
大家好,我是程序员凌览。
这个话题本身就很有趣——如何有效地带散一个团队,精选了两位网友的回答让我们一起来看看。
第一位网友的回答
1938年10月14日,毛泽东谈了如何把团队带好。你反着来,肯定能把团队带散。
毛泽东说,要带好团队,必须善于爱护干部。爱护的办法是:
“第一,指导他们。这就是让他们放手工作,使他们敢于负责;同时,又适时地给以指示,使他们能在党的政治路线下发挥其创造性。”
你反着来,你就处处摆你下属的谱,不管自己会不会,都要装着自己会的样子。同时要求团队的人不能有任何主观能动性,什么事都要跟你汇报。谁敢改变你的任何看法,就处理谁。说一不二。
“第二,提高他们。这就是给以学习的机会,教育他们,使他们在理论上在工作能力上提高一步。”
你反着来,不要给他们任何学习机会,也不进行任何业务培训,绝不多花一分钱在他们的学习上,因为学好了,他们就跳槽了,这不是浪费吗?
“第三,检查他们的工作,帮助他们总结经验,发扬成绩,纠正错误。有委托而无检查,及至犯了严重的错误,方才加以注意,不是爱护干部的办法。”
你反着来,平时不管不顾,听之任之,一旦发生了问题,就把犯错误的人骂得狗血喷头。该扣工资的扣工资,该开除的开除。谁让你们自己要犯错呢。
“第四,对于犯错误的干部,一般地应采取说服的方法,帮助他们改正错误。只有对犯了严重错误而又不接受指导的人们,才应当采取斗争的方法。在这里,耐心是必要的;轻易地给人们戴上‘机会主义’的大帽子,轻易地采用‘开展斗争’的方法,是不对的。”
你反着来,犯了错误就扣帽子,就人身攻击,就骂人家蠢,“这点事都能搞砸,你干什么吃的。”
“第五,照顾他们的困难。干部有疾病、生活、家庭等项困难问题者,必须在可能限度内用心给以照顾。”
你反着来,生病的只要没有三甲医院医生开的证明,就不能请假,家里有事的,能克服也要克服,不能克服的也要克服,遇到重要的工作,即使孕妇要生孩子,也要她晚几天生,工作重要。
你要这么干下去,你团队不散,你就去法院告我。
本文来源:《毛泽东选集》第二卷文章《中国共产党在民族战争中的地位》(1938年10月14日)
第二位网友的回答
1、开会!
早会,汇报会,进度会,总结会,推进会复盘会,总之不要闲着。
不管什么会,在中午吃饭前,下午下班前开! 晚上回家后再整个线上会议就可着吃饭点,线上会议还要开摄像头,效果满分!
2、做表
统计表,进度表,复盘报告,问题报告,项目总结。
不管什么事,就一句话先做张表给我,要抓重点,也能看出细节,同时手上事情不能停!
效果最好的就是同样的内容用不同的模板来做,还要突出不一样的重点!
3、打官腔
维度,抓手,组合拳,底层逻辑,赛道,载体。
总之就是不说人话! 这个不好总结举个例子
项目管理底层逻辑是打通信息屏障,创建项目新生态,顶层实际是聚焦用户感知赛道,通过差异化和颗粒度达到引爆点,交付价值是在采用复用打法达成持久受益,抽离透传归因分析作为抓手为产品赋能,体验度量作为闭环的评判标准,亮点为载体,优势为链路,思考整个项目生命周期,完善逻辑考虑资源倾斜,是组合拳,最终达到平台标准化
4、吃饼
薪资翻倍,奖金十万,三年买房。
激励政策一发放,人人都打鸡血,月中发政策大家努力来不及。
次月大家拼命干,公司一看卧槽要给这么多奖金?老板签批,同意发放但不是现在。然后就没有然后了。
5、突发情况。
项目重大问题,大老板要审查,明天xxx要看项目进度。
别管在干嘛,吃饭?睡觉?OOXX? 也别管几点钟,就一句话,赶紧把ppt做好,明天要。赶紧去客户现场安抚客户。
这种事情越多效果越好。
6、团建
团建这个就有讲究了。
一定要选在节假日,周末随便选一天,三天选中间,五天搞长点,七天去外地。
胡吃海喝肯定达不到效果,所以什么马拉松?爬山?参观什么展览?等等等。 形式不限重点是时间。
比如十一放假,10.2早上出发去XXX,下午布置场地,10.3早上跑个十几二十公里健身,跑完开个会做个动员,下午统一服装去xxx观光。10.4上午大合照,下午回家。
我补充一下
1、制度、制度、制度
别管是什么日常琐事,一率立制度。从工作流程到个人习惯,如用餐和使用洗手间。不是在立制度就是在立制度的路上。
2、Pua
Pua的P要懂的12种不同的写法。做错就扣帽子,就人身攻击,就骂人家蠢;做对就打压,就贬低,就泼冷水。
3、日报、周报、月报、双周报、季度报等等
日报、周报、月报、双周报、季度报等等必不可少,要求写个四五千字的。至于我作为领导看不看报告?那我当然看的啦,你管我啥时候看呢
来源:juejin.cn/post/7410710728783413299
30分钟搞懂JS沙箱隔离
什么是沙箱环境
在计算机安全中,沙箱(Sandbox)是一种用于隔离正在运行程序的安全机制,通常用于执行未经测试或不受信任的程序或代码,它会为待执行的程序创建一个独立的执行环境,内部程序的执行不会影响到外部程序的运行。
其实在前端世界里,沙箱环境无处不在!
例如以下几个场景:
- Chrome本身就是一个沙箱环境
Chrome 浏览器中的每一个标签页都是一个沙箱(sandbox)。渲染进程被沙箱(Sandbox)隔离,网页 web 代码内容必须通过 IPC 通道才能与浏览器内核进程通信,通信过程会进行安全的检查。
- 在线代码编辑器(码上掘金、CodeSandbox、CodePen等)
在线代码编辑器在执行脚本时都会将程序放置在一个沙箱中,防止程序访问/影响主页面。
- Vue的 服务端渲染
在 Node.js 中有一个模块叫做 VM,它提供了几个 API,允许代码在 V8 虚拟机上下文中运行。
const vm = require('vm');
const sandbox = { a: 1, b: 2 };
const script = new vm.Script('a + b');
const context = new vm.createContext(sandbox);
script.runInContext(context);
vue的服务端渲染实现中,通过创建沙箱执行前端的bundle文件;在调用createBundleRenderer方法时候,允许配置runInNewContext为true或false的形式,判断是否传入一个新创建的sandbox对象以供vm使用。
- Figma 插件
出于安全和性能等方面的考虑,Figma将插件代码分成两个部分:main 和 ui。其中 main 代码运行在沙箱之中,ui 部分代码运行在 iframe 之中,两者通过 postMessage 通信。
- 微前端
典型代表是
Garfish
和qiankun
从0开始实现一个JS沙箱环境
1. 最简陋的沙箱(eval)
问题:
- 要求源程序在获取任意变量时都要加上执行上下文对象的前缀
- eval的性能问题
- 源程序可以访问闭包作用域变量
- 源程序可以访问全局变量
2. eval + with
问题:
- eval的性能问题
- 源程序可以访问闭包作用域变量
- 源程序可以访问全局变量
3. new Function + with
问题:
- 源程序可以访问全局变量
4. ES6 Proxy
我们先看Proxy的使用
Proxy
给 {}
设置了属性访问拦截器,倘若访问的属性为 a
则返回 1,否则走正常程序。
Proxy 支持的拦截操作,一共 13 种:
- get(target, propKey, receiver) :拦截对象属性的读取,比如
proxy.foo
和proxy['foo']
。 - set(target, propKey, value, receiver) :拦截对象属性的设置,比如
proxy.foo = v
或proxy['foo'] = v
,返回一个布尔值。 - has(target, propKey) :拦截
propKey in proxy
的操作,返回一个布尔值。 - deleteProperty(target, propKey) :拦截
delete proxy[propKey]
的操作,返回一个布尔值。 - ownKeys(target) :拦截
Object.getOwnPropertyNames(proxy)
、Object.getOwnPropertySymbols(proxy)
、Object.keys(proxy)
、for...in
循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()
的返回结果仅包括目标对象自身的可遍历属性。 - getOwnPropertyDescriptor(target, propKey) :拦截
Object.getOwnPropertyDescriptor(proxy, propKey)
,返回属性的描述对象。 - defineProperty(target, propKey, propDesc) :拦截
Object.defineProperty(proxy, propKey, propDesc)
、Object.defineProperties(proxy, propDescs)
,返回一个布尔值。 - preventExtensions(target) :拦截
Object.preventExtensions(proxy)
,返回一个布尔值。 - getPrototypeOf(target) :拦截
Object.getPrototypeOf(proxy)
,返回一个对象。 - isExtensible(target) :拦截
Object.isExtensible(proxy)
,返回一个布尔值。 - setPrototypeOf(target, proto) :拦截
Object.setPrototypeOf(proxy, proto)
,返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。 - apply(target, object, args) :拦截 Proxy 实例作为函数调用的操作,比如
proxy(...args)
、proxy.call(object, ...args)
、proxy.apply(...)
。 - construct(target, args) :拦截 Proxy 实例作为构造函数调用的操作,比如
new proxy(...args)
。
在沙箱环境中,对本身不存在的变量会追溯到全局变量上访问,此时我们可以使用 Proxy "欺骗" 程序,告诉它这个「不存在的变量」是存在的。
报错了,因为我们阻止了所有全局变量的访问。
继续改造:
Symbol.unscopables
Symbol
是 JS 的第七种数据类型,它能够产生一个唯一的值,同时也具备一些内建属性,这些属性可以用来进行元编程(meta programming),即对语言本身编程,影响语言行为。其中一个内建属性 Symbol.unscopables
,通过它可以影响 with
的行为,从而造成沙箱逃逸。
对这种情况做一层加固,防止沙箱逃逸
到这一步,其实很多较为简单的场景就可以覆盖了(比如: Vue 的模板字符串)。
仍然有很多漏洞:
code
中可以提前关闭sandbox
的with
语境,如'} alert(this); {'
code
中可以使用eval
和new Function
直接逃逸code
中可以通过访问原型链实现逃逸- 更为复杂的场景,如何实现任意使用诸如
document
、location
等全局变量且不会影响主页面。
5. iframe是天然的优质沙箱
iframe
标签可以创造一个独立的浏览器原生级别的运行环境,这个环境由浏览器实现了与主环境的隔离。在 iframe
中运行的脚本程序访问到的全局对象均是当前 iframe
执行上下文提供的,不会影响其父页面的主体功能,因此使用 iframe
来实现一个沙箱是目前最方便、简单、安全的方法。
如果只考虑浏览器环境,可以用 With + Proxy + iframe 构建出一个比较好的沙箱:
- 利用
iframe
对全局对象的天然隔离性,将iframe.contentWindow
取出作为当前沙箱执行的全局对象 - 将上述沙箱全局对象作为
with
的参数限制内部执行程序的访问,同时使用Proxy
监听程序内部的访问。 - 维护一个共享状态列表,列出需要与外部共享的全局状态,在
Proxy
内部实现访问控制。
6. 基于ShadowRealm 提案的实现
ShadowRealm API 是一个新的 JavaScript 提案,它允许一个 JS 运行时创建多个高度隔离的 JS 运行环境(realm),每个 realm 具有独立的全局对象和内建对象。
这项特性提案时间为 2021 年 12 月,目前在Stage 3阶段 tc39.es/proposal-sh…
evaluate(sourceText: string)
同步执行代码字符串,类似 eval()importValue(specifier: string, bindingName: string)
异步执行代码字符串
7. Web Workers
Web Workers
代码运行在独立的进程中,通信是异步的,无法获取当前程序一些属性或共享状态,且有一点无法不支持 DOM 操作,必须通过 postMessage 通知 UI 主线程来实现。
以上就是实现JS沙箱隔离的一些思考点。在真实的业务应用中,没有最完美的方案,只有最合适的方案
,还需要结合自身业务的特性做适合自己的选型。
来源:juejin.cn/post/7410347763898597388
uniapp截取视频画面帧
前言
最近开发中遇到这么一个需求,上传视频文件的时候需要截取视频的一部分画面来供选择,作为视频的封面,截取画面可以使用canvas来实现,将视频画面画进canvas里,再通过canvas来生成文件对象和一个预览的url
逻辑层和视图层
想要将视频画进canvas里就需要操作dom,但是在uniapp中我们是无法操作dom的,uniapp的app端逻辑层和视图层是分离的,renderjs运行的层叫【视图层】,uniapp原生script叫【逻辑层】,会产生一个副作用就是是在造成了两层之间通信阻塞
所以uniapp推出了renderjs,renderjs
是一个运行在视图层的js,可以让我们在视图层操作dom,但是不能直接调用,需要在dom元素中绑定某个值,当值发生改变就会触发视图层的事件
// 视图层
// module为renderjs模块的名称,通过 模块名.事件 来调用事件
<script module="capture" lang="renderjs"></script>
// 逻辑层
// prop为绑定的值,名字可以随便起,但是要和change后面一致
// 当prop绑定的值发生改变就会触发capture模块的captures事件
<view style="display: none;" :prop="tempFilePath" :change:prop="capture.captures"></view>
<template>
<view class="container">
<view style="display: none;" :prop="tempFilePath" :change:prop="capture.captures"></view>
</view>
</template>
<script>
export default {
data() {
return {
tempFilePath: ''
}
},
mounted() {
this.tempFilePath = 'aaaaaaaaaaaaaaaa'
}
}
</script>
<script module="capture" lang="renderjs">
export default {
methods: {
captures(tempFilePath) {
console.log(tempFilePath);
},
}
}
</script>
截取画面帧
我们先获取到视频的信息,通过uni.chooseVideo(OBJECT)
这个api我们可以拿到视频的临时文件路径,然后再将临时路径交给renderjs去进行截取操作
定义一个captureFrame方法,方法接收两个参数:视频的临时文件路径和截取的时间。先创建video元素,设置video元素的currentTime属性(视频播放的当前位置)的值为截取的时间,由于video元素没有加到dom上,video播放到currentTime结束
并设置video元素的autoplay自动播放属性为true,但是由于浏览器的限制video无法自动播放,但是静音状态下可以自动播放,所以还要将video元素的muted属性设为true,最后再将src属性设置为视频的临时文件路径,当video元素可以播放的时候就可以将video元素画进canvas里了
captureFrame(vdoSrc, time = 0) {
return new Promise((resolve) => {
const vdo = document.createElement('video')
// video元素没有加到dom上,video播放到currentTime(当前帧)结束
// 定格时间,截取帧
vdo.currentTime = time
// 设置自动播放,不播放是黑屏状态,截取不到帧画面
// 静音状态下允许自动播放
vdo.muted = true
vdo.autoplay = true
vdo.src = vdoSrc
vdo.oncanplay = async () => {
const frame = await this.drawVideo(vdo)
resolve(frame)
}
})
},
然后再定义一个drawVideo方法用于绘制视频,接收一个video元素参数,在方法中创建一个canvas元素,将canvas元素的宽高设置为video元素的宽高,通过drawImage方法将视频画进canvas里,再通过toBlob方法创建Blob对象,然后通过URL.createObjectURL() 创建一个指向blob对象的临时url,blob对象可以用来上传,url可以用来预览
drawVideo(vdo) {
return new Promise((resolve) => {
const cvs = document.createElement('canvas')
const ctx = cvs.getContext('2d')
cvs.width = vdo.videoWidth
cvs.height = vdo.videoHeight
ctx.drawImage(vdo, 0, 0, cvs.width, cvs.height)
// 创建blob对象
cvs.toBlob((blob) => {
resolve({
blob,
url: URL.createObjectURL(blob),
})
})
})
}
最后我们就可以在触发视图层的事件里去使用这两个方法来截取视频画面帧了,最后将数据传递返回给逻辑层,通过this.$ownerInstance.callMethod() 向逻辑层发送消息并将数据传递过去
// 视图层
async captures(tempFilePath) {
let duration = await this.getDuration(tempFilePath)
let list = []
for (let i = 0; i < duration; i++) {
const frame = await this.captureFrame(tempFilePath, duration / 10 * i)
list.push(frame)
}
this.$ownerInstance.callMethod('captureList', {
list,
duration
})
},
getDuration(tempFilePath) {
return new Promise(resolve => {
const vdo = document.createElement('video')
vdo.src = tempFilePath
vdo.addEventListener('loadedmetadata', () => {
const duration = Math.floor(vdo.duration);
vdo.remove();
resolve(duration)
});
})
},
// 逻辑层
captureList({
list,
duration
}) {
// 操作......
}
运行
最后运行起来,发现报了一个错误:Failed to execute 'toBlob' on 'HTMLCanvasElement': Tainted canvases may not be exported,这是因为由于浏览器的安全考虑,如果在使用canvas绘图的过程中,使用到了外域的资源,那么在toBlob()时会抛出异常,设置video元素的crossOrigin属性值为anonymous就行了
app端 | h5 |
---|---|
![]() | ![]() |
来源:juejin.cn/post/7281912738863087656
面试必问,防抖函数的核心是什么?
防抖节流的作用是什么?
节流(throttle)与 防抖(debounce)都是为了限制函数的执行频次,以优化函数触发频率过高导致的响应速度跟不上触发频率,出现延迟,假死或卡顿的现象。
防抖:是指在一定时间内,在动作被连续频繁触发的情况下,动作只会被执行一次,也就是说当调用动作过n毫秒后,才会执行该动作,若在这n毫秒内又调用此动作则将重新计算执行时间,所以短时间内的连续动作永远只会触发一次,比如说用手指一直按住一个弹簧,它将不会弹起直到你松手为止。
节流:是指一定时间内执行的操作只执行一次,也就是说即预先设定一个执行周期,当调用动作的时刻大于等于执行周期则执行该动作,然后进入下一个新周期,一个比较形象的例子是人的眨眼睛,就是一定时间内眨一次。
防抖函数应用场景:
就比如说这段代码:
let btn = document.getElementById('btn')
btn.addEventListener('click', function() {
console.log('提交'); // 换成ajax请求
})
当你点击按钮N下,它就会打印N次“提交”,但如果把 console 换成 ajax 请求,可想而知后端接受到触发频率如此之高的请求,造成的页面卡顿甚至瘫痪的后果。
防抖函数的核心:
面对此种情形,我们必须在原有的基础上作出改进,做到在规定的时间内没有下一次的触发,才执行的效果。
那么首先我们要做的,就是创建一个防抖函数,这个函数的功能是设置一个定时器,每次点击都会触发一个定时器输出,但如果两次点击的间隔小于1s,则销毁上一个定时器,达到最后只有一个定时器输出的效果。
定时器:
在防抖节流中,最为重要的一个部分就是定时器,就比如下面这段代码,
setTimeout
的功能就是设置一个定时器,让setTimeout
内部的代码延迟执行在 1000 毫秒后。
setTimeout(function(){
console.log('提交');
}, 1000)
特别需要注意一点的是,定时器中回调函数里的 this 指向会更改成指向 window。
于是我们创建专门的debounce
函数用于实现防抖,把handle
交给debounce
处理,再在debounce
内部设置一个setTimeout
定时器,将handle
的执行推迟到点击事件发生的一秒后,这样一来,我们就实现了初步的想法。
let btn = document.getElementById('btn')
function handle(){
console.log('提交', this); // 换成ajax请求
}
// 创建专门的debounce函数用于防抖,把handle交给debounce处理
btn.addEventListener('click', debounce(handle))
// 将点击事件推迟一秒
function debounce(fn){
return function() {
// 设置定时器
setTimeout(fn, 1000)
}
}
那么关键来了,我们又在原基础上添加一个timer
用于接收定时器返回的值(通常称为定时器的ID),然后设置clearTimeout(timer)
通过timer
取消之前通过 setTimeout
创建的定时器。
通过这段代码,我们便实现了如果在 1s 内频繁点击的话,上一次点击的事件都会被下一次点击取消,从而达到规定的时间内没有下一次的触发,再执行的防抖目的!
let btn = document.getElementById('btn')
function handle(){
console.log('提交', this); // 换成ajax请求
}
// 创建专门的debounce函数用于防抖,把handle交给debounce处理
btn.addEventListener('click', debounce(handle))
// 防抖函数
function debounce(fn){
let timer = null; // 接收定时器返回的ID
return function() {
// 设置定时器
clearTimeout(timer); // 取消之前通过 `setTimeout` 创建的定时器
timer = setTimeout(fn, 1000);
}
}
但是别忘了,我们之前提到过,定时器改变了handle
中 this 指向,要做到尽善尽美,我们必须通过显示绑定修正 this 的指向。
同时别忘记还原原函数的参数。
利用箭头函数不承认 this 的特性,我们将代码修改成这样:
let btn = document.getElementById('btn')
function handle(e){
console.log('提交'); // 换成ajax请求
}
// 创建专门的debounce函数用于防抖,把handle交给debounce处理
btn.addEventListener('click', debounce(handle))
// 防抖函数
function debounce(fn){
let timer = null; // 接收定时器返回的ID
return function(e) {
// 设置定时器
clearTimeout(timer);
timer = setTimeout(() => {
fn.call(this,e); // 修正this的同时归还原函数的参数
}, 1000)
}
}
至此,大功告成!
防抖函数核心机制:
同时需要理解的是:防抖函数的核心机制就是闭包,当每一次点击会产生debounce
执行上下文,随后debounce
执行完其上下文又被反复销毁,但是其中的变量timer
又始终保持着对function
外部的引用,于是由此形成了闭包。
关于 this 的指向可以参考这篇文章:juejin.cn/post/739763…
关于闭包概念可以参考这篇文章:juejin.cn/post/739762…
最后:
那么现在我们可以总结出这个防抖函数的核心理念和四大要点。
核心理念:点击按钮后,做到在规定的时间内没有下一次的触发,才执行
- 其中
debounce
返回一个函数体,跟debounce
形成了一个闭包。 - 子函数体中每次先销毁上一个
setTimeout
,再创建一个新的setTimeout
。 - 最后需要 还原原函数的 this 指向。
- 最后需要 还原原函数的参数。
来源:juejin.cn/post/7400253623790272547
关于微信小程序(uniapp)的优化
前言
开篇雷击
好害怕
怎么办
不要慌
仔细看完文章,彻底解决代码包大小超过限制
提示:以下是本篇文章正文内容,下面案例可供参考
一、微信小程序上传时的规则
微信小程序官方规定主包大小不能超过2M,单个分包大小也不能超过2M,多个分包总大小不能超过8M,文件过大会增加启动耗时,对用户体验不友好。
官方解释:
二、分析、整理项目中的文件
1.正常来说一个小程序该有以下目录构成:
│
│——.hbuilderx
│
│——api // 接口路径及请求配置
│
│——components // 公共组件
│
│——config // 全局配置
│
│——node_modules // 项目依赖
│
│——pages // 项目主包
│
│——order // 项目分包
│
│——static // 静态资源
│ │
│ ├─scss // 主包css样式
│ │
│ ├─js // 全局js方法
│ │
│ └─image // tabBar图标目录
│
│——store // Vuex全局状态管理
│
│——utils // 封装的特定过滤器
│
│——error-log // 错误日志
│......
│
2.和自己本地的文件目录对比一下,分析后根据实际情况整理出规范的目录,相同文件规整至一起,删除多余的文件,检查每个页面中是否存在未使用的引用资源
三、按以下思路调整
1.图标资源建议只留下tabBar图标(注意:tabBar图标的大小,控制在30-40k左右最优)
,其余资源通过网络路径访问,有条件的就上个CDN加速好吧。
2.主包目录建议只留下tabBar相关的页面,其余文件按分包处理(注意:单个分包大小也不能超过2M,多个分包总大小不能超过8M,根据业务划分出合理的分包:例如:order、pay、login、setting、user...)
3.公共组件,公共方法的使用(建议:把分包理解成一个单独的项目,scss,js,components,小程序插件...这些都是仅限于这个分包内使用,后期也方便维护)
4.避免使用过大的js文件,或替换为压缩版或者mini版
5.检查是否存在冗余代码,抽出可复用的进行封装
6.小程序插件(建议:挂载在分包上使用,挂载主包上会影响体积)
{
// 分包order
"root": "order",
"pages": [{
"path": "index",
"style": {
"navigationStyle": "custom",
"usingComponents": {
"healthcode": "plugin://xxxxx"
}
}
}
],
//插件引入
"plugins": {
"healthcode-plugin": {
"version": "0.2.3",
"provider": "插件appid"
}
}
}
7.根据官方建议开启按需引入、分包优化
manifest.json-源码视图
"mp-weixin" : {
"appid" : "xxxxx",
"setting" : {
"urlCheck" : false,
"minified" : true
},
// 按需引入
"lazyCodeLoading" : "requiredComponents",
"permission" : {
"scope.userLocation" : {
"desc" : "获取您的位置信息,用于查询数据"
}
},
"requiredPrivateInfos" : [ "getLocation", "chooseLocation" ],
// 分包优化
"optimization" : {
"subPackages" : true
}
}
8.Hbuilderx工具点击发行-微信小程序 (注意:运行默认是不会压缩代码)
四、终极办法
如果按以上步骤下来,还是提示代码大小超过限制的话,不妨从微信开发工具上试试
按图勾选上相关配置(注意:不要勾选上传代码时样式自动补全,会增加代码体积)
五、写在最后
1.提升小程序首页渲染速度 官方给出的代码注入优化
首页代码避免出现复杂的逻辑,控制代码量,去除无用的引入,合理的接口数量
2.小程序加载分包时可能会遇到等待的情况,解决这个问题的办法:
pages.json文件开启分包预下载
"preloadRule": {
"pages/index": { // 访问首页的时候就开始下载order、pay分包的内容
"network": "all", // 网络环境 all全部网络,wifi仅wifi下预下载
"packages": ["order","pay"] // 要下载的分包
}
}
总结
本文介绍了开发微信小程序时,遇到的代码包大小超过限制的问题,希望可以帮助到你。
来源:juejin.cn/post/7325132133168185381
多人游戏帧同步策略
介绍解决该问题的基本概念和常见解决方案。
- Lockstep state update 锁步状态更新
- Client prediction 客户端预测
- server reconcilation 服务端和解
多人游戏的运作方式
游戏程序的玩家当前状态随时间和玩家的输入会进行变化。也就是说游戏是有状态的程序。多人游戏也不例外,但由于多人玩家之间存在交互,复杂性会更高。
例如贪吃蛇游戏,我们假设它的操作会发送到服务器,那它的核心游戏逻辑应该是:
- 客户端读取用户输入改变蛇的方向,也可以没有输入,然后发送给服务端
- 服务端接收消息,根据消息改变蛇的方向,将蛇的“头”移动一个单位空间
- 服务端检查蛇是否撞到了墙壁或者自己,如果撞到了游戏结束,给客户端发送响应消息,更新客户端的画面。如果没有撞到,则继续接收客户端发送的消息,同时也要响应给客户端消息,告诉客户端,蛇目前的状态。
服务端接收该消息做出对应的动作,这个过程会以固定的间隔运行。每一次循环都被称为 frame 或 tick。
客户端将解析服务端发送的消息,也就是每一帧的动作,渲染到游戏华中中。
锁步状态更新
为了确保所有客户端都同步帧,最简单的方法是让客户端以固定的间隔向服务器发送更新。发送的消息包含用户的输入,当然也可以发送 no user input。
服务器收集“所有用户”的输入后,就可以生成下一次 frame 帧。
上图演示了客户端与服务端的交互过程。T0 ~ T1 时间段,客户端保持等待,或者说空闲状态,直到服务器响应 frame,等待时间的大小取决于网络质量,约 50 毫秒到 500 毫秒,人眼能够注意到任何超过 100 毫秒的延迟,因此这个等待时间对于某些游戏来说是不可接受的。
锁步状态更新,还有一个问题。游戏的延迟来自最慢的用户 。
上图有两个客户端。客户端 B 的网络比较差,A 和 B 都在 T0 时间点向服务器发送了用户输入,A 的请求在 T1 到达服务端,B 的请求在 T2 到达服务端,前面我们提到,服务器需要收集“所有用户”的请求后才开始工作,因此需要到 T2 时间点才开始生成 frame。
因为 Client B 比较慢,我们“惩罚”了所有的玩家。
假如我们不等待所有客户端的用户输入,低延迟玩家又会获得优势,因为它的输入到达服务器的时间更短,会更快处理。例如,两个玩家 A、B 同时互相射击预期是同时死亡,但是 A 玩家延迟比 B 玩家更低,因此在处理 B 玩家的用户输入时,A 玩家已经干掉 B 玩家了。
小结一下,锁步状态更新存在的问题,如下。
- 游戏画面是否卡顿,取决于最慢的玩家
- 客户端需要等待来自服务器的响应,否则不会渲染画面
- 连接非常活跃,客户端需要定期发送一些无用的心跳包,以便服务器可以确定它拥有生成 frame 所需的所有信息
回合制类型的游戏大多数使用这种方法,因为玩家确实需要等待,例如《炉石传说》。
对于慢节奏的游戏,少量延迟也是可以接受的,例如《QQ农场》。
但是对于快节奏的游戏,锁步状态更新的这些问题都是致命的,不可能操纵游戏人物进入某一个建筑,500 毫秒后,我才能进入。我们一起来看看下一种方法。
客户端预测
客户端预测,在玩家的计算机上,运行游戏逻辑,来模拟游戏的行为,而不是等待服务器更新。
例如我们生成 Tn 时间点的游戏状态,我们需要 Tn-1 时间点的所有玩家状态和 Tn-1 时间点所有玩家的输入。
假设,我们现在的固定频率为 1 s,每 1s 需要给服务器发送一个请求,获取玩家状态并更新玩家的状态。
在 T0 时间点,客户端将用户的输入发送到服务器,用于获取 T1 时间点的游戏状态。在 T1 时间点,客户端已经可以渲染画面了,实际上客户端的响应是在 T3 时刻,也就是说客户端没有等待来自服务器的响应。
使用这个方法,需要满足一些前置条件:
- 客户端拥有游戏运行逻辑所需的所有条件
- 玩家状态的更新逻辑是确定性的,即没有随机性,或者可以以某种方式保证确定性,例如客户端和服务器使用同样的公式以及随机种子,可以保证具有随机性的同时,产生的结果具有确定性。这样保证了客户端和服务器在给定相同输入的情况下产生相同的游戏状态
满足这两点,客户端预测的结果也不一定总是对的。就比如刚提到的,使用相同的公式以及相同的随机种子,进行伪随机算法,但不同平台的浮点计算,可能会存在微小的差异。
再设想一个场景,如下图。
客户端 A 尝试使用 T0 时间点的信息模拟 T1 时间点上的游戏状态,但客户端 B 也在 T0 时间点提交了用户输入,客户端 A 并不知道这个用户输入。
这意味着客户端 A 对 T1 时间的预测将是错误的是,但!由于客户端 A 仍然从服务器接收 T1 时间点的状态,因此客户端有机会在 T3 时间点修正错误。
客户端需要知道,自己的预测是否正确,以及如何修正错误。
修正错误通常叫做 Reconcilation 和解。
需要根据上下文来实现和解部分,下面我们通过一个简单的例子来理解这个概念。这个例子只是抛弃我们的预测,并将其游戏状态替换为服务器响应的正确状态。
- 客户端需要维护 2 个缓冲区,一个用于预测 PredictionBuffer,一个用于用户输入 InputBuffer 。它们是预测这个行为需要的上下文,请记住,预测 Tn 时刻,需要 Tn-1 的状态和 Tn-1 时刻的用户输入。它们一开始都为空
- 玩家点击鼠标,移动游戏角色到下一个位置。此时,玩家输入的移动信息 Input 0 存储在 InputBuffer 中,客户端将生成预测 Prediction 1,存储在 PredictionBuffer 中,预测将展示在玩家画面中
- 客户端收到服务器响应的 State0 ,发现与客户端的预测不匹配,我们将Prediction 1 替换为 State 0,并使用 Input 0 和 State 0 重新计算,得到 Prediction 2,这个重新计算的过程,就是 Reconcilation 和解
- 和解后,我们从缓冲区中删除 State 0 和 Input 0
这种和解的方式有一个明显的缺点,如果服务器响应的游戏状态和客户端预测差异太大,则游戏画面可能会出现错误。例如我们预测敌人在 T0 时间点向南移动,但在 T3 时间点,我们意识到它在向北移动,然后通过使用服务器的响应进行和解,敌人将从北“飞到”正确的位置。
有一些方法可以解决此问题,这里不展开讨论,感兴趣可以搜一下实体插值 Entity Interpolation。
小结一下,客户端预测技术,让客户端以自己的更新频率运行,与服务器的更新频率无关,所以服务器如果出现阻塞,不会影响客户端的帧。
但它也带来复杂性,如下。
- 需要在客户端处理更多的状态和逻辑,比如我们前面提到的缓冲区和预测逻辑
- 需要和解来自服务器的状态(正确的游戏状态)与预测之前的冲突
还给我们带来了敌人从南飞到北的问题。
目前为止,我们都在讨论客户端,接下来看看服务端如何解决帧同步。
服务端和解
利用服务端解决帧同步问题,首先需要解决的是网络延迟带来的问题。如下图。
用户 A 在 T 处进行了操作(比如按下了一个技能键),该操作应该在 T+20ms 处理,但由于延迟,服务器在 T+120ms 才接收到输入。
在游戏中,用户做出指定操作后,应该立即有反应。立即有反应,这个立即是多久,取决于游戏的类型,比如之前我们提到的回合制,它的立即可能是几十秒。我们可以通过 T + X,表示立即反应的时间,T 代表用户的输入时刻,X 代表的是延迟。X 可以为 0,这代表真正的立即 :-)
解决这个问题的思路,与之前客户端预测中使用的办法类似,就是通过客户端的用户输入,来和解服务器中的玩家游戏状态。
所有的用户输入,都需要时间戳进行标记,该时间戳用于告诉服务器,什么时刻处理此用户输入。
为什么在同一水平线上,Client A 的时间是 Time X,而 Server 的时间是 Time Y?
因为客户端和服务端独立运行,通常时间会有所不同,在多人游戏中,我们可以特殊处理其中的差异。在特殊处理时,我们应该使客户端的时间大于服务端的时间,因为这样可以存在更大的灵活性
上图演示了一个客户端与服务端之间的交互。
- 客户端发送带有时间戳的输入。客户端告诉服务器在 X 时间点应该发生用户输入的效果
- 服务端在 Y 时间点收到请求
- 在 Y+1 时间点,即红色框的地方,服务端开始和解,服务端将 X 时间点的用户输入应用于最新的游戏状态,以保证 X 的 Input 发生在 X 时间点
- 服务端发送响应,该响应中包含时间戳
服务端和解部分(上图红色底色部分),主要维护 3 个部分,如下。
- GameStateHistory,在一定时间范围内玩家在游戏中的状态
- ProcessedUserInput,在一定时间范围内处理的用户输入的历史记录
- UnprocessedUserInput,已收到但未处理的用户输入,也是在一定的时间内
服务端和解过程,如下。
- 当服务端收到来自用户的输入时,首先将其放入 UnprocessedUserInput 中
- 等待服务端开始同步帧,检查 UnprocessedUserInput 中是否存在任何早于当前帧的用户输入
- 如果没有,只需要将最新的 GameState 更新为当前用户的输入,并执行游戏逻辑,然后广播到客户端
- 如果有,则表示之前生成的某些游戏状态由于缺少部分用户输入而出错,需要和解,也就是更正。首先需要找到最早的,未处理的用户输入,假设它在时间 N 上,我们需要从 GameStateHistory 中获取时间 N 对应的 GameState 以及从 ProcessedUserInput 获取时间 N 上用户的输入
- 使用这 3 条数据,就可以创建一个准确的游戏状态,然后将未处理的输入 N 移动到 ProcessingUserInput,用于之后的和解
- 更新 GameStateHistory 中的游戏状态
- 重复步骤 4 ~ 6,直到从 N 的时间点到最新的游戏状态
- 服务端将最新帧广播给所有玩家
我并没有做过这些工作,分享的知识都是我对它感兴趣,在网上看了许多经验后整理的。
来源:juejin.cn/post/7277489569958821900
我用这10招,能减少了80%的BUG
前言
对于大部分程序员来说,主要的工作时间是在开发和修复BUG。
有可能修改了一个BUG,会导致几个新BUG的产生,不断循环。
那么,有没有办法能够减少BUG,保证代码质量,提升工作效率?
答案是肯定的。
如果能做到,我们多出来的时间,多摸点鱼,做点自己喜欢的事情,不香吗?
这篇文章跟大家一起聊聊减少代码BUG的10个小技巧,希望对你会有所帮助。
1 找个好用的开发工具
在日常工作中,找一款好用的开发工具,对于开发人员来说非常重要。
不光可以提升开发效率,更重要的是它可以帮助我们减少BUG。
有些好的开发工具,比如:idea
中,对于包没有引入,会在相关的类上面标红
。
并且idea还有自动补全
的功能,可以有效减少我们在日常开发的过程中,有些单词手动输入的时候敲错的情况发生。
2 引入Findbugs插件
Findbugs是一款Java静态代码分析工具,它专注于寻找真正的缺陷或者潜在的性能问题,它可以帮助java工程师提高代码质量以及排除隐含的缺陷。
Findbugs运用Apache BCEL 库分析类文件,而不是源代码,将字节码与一组缺陷模式进行对比以发现可能的问题。
可以直接在idea中安装FindBugs插件:
之后可以选择分析哪些代码:
分析结果:
点击对应的问题项,可以找到具体的代码行,进行修复。
Findbugs的检测器已增至300多条,被分为不同的类型,常见的类型如下:
- Correctness:这种归类下的问题在某种情况下会导致bug,比如错误的强制类型转换等。
- Bad practice:这种类别下的代码违反了公认的最佳实践标准,比如某个类实现了equals方法但未实现hashCode方法等。
- Multithreaded correctness:关注于同步和多线程问题。
- Performance:潜在的性能问题。
- Security:安全相关。
- Dodgy:Findbugs团队认为该类型下的问题代码导致bug的可能性很高。
3 引入CheckStyle插件
CheckStyle作为检验代码规范的插件,除了可以使用配置默认给定的开发规范,如Sun、Google的开发规范之外,还可以使用像阿里的开发规范的插件。
目前国内用的比较多的是阿里的代码开发规范,我们可以直接通过idea下载插件:
如果想检测某个文件:
可以看到结果:
阿里巴巴规约扫描包括:
- OOP规约
- 并发处理
- 控制语句
- 命名规约
- 常量定义
- 注释规范
Alibaba Java Coding Guidelines 专注于Java代码规范,目的是让开发者更加方便、快速规范代码格式。
该插件在扫描代码后,将不符合规约的代码按 Blocker、Critical、Major 三个等级显示出来,并且大部分可以自动修复。
它还基于Inspection机制提供了实时检测功能,编写代码的同时也能快速发现问题。
4 用SonarQube扫描代码
SonarQube是一种自动代码审查工具,用于检测代码中的错误,漏洞和代码格式上的问题。
它可以与用户现有的工作流程集成,以实现跨项目分支和提取请求的连续代码检查,同时也提供了可视化的管理页面,用于查看检测出的结果。
SonarQube通过配置的代码分析规则,从可靠性、安全性、可维护性、覆盖率、重复率等方面分析项目,风险等级从A~E划分为5个等级;
同时,SonarQube可以集成pmd、findbugs、checkstyle等插件来扩展使用其他规则来检验代码质量。
一般推荐它跟Jenkins集成,做成每天定时扫描项目中test分支中的代码问题。
5 用Fortify扫描代码
Fortify 是一款广泛使用的静态应用程序安全测试(SAST)工具。
它具有代码扫描、漏斗扫描和渗透测试等功能。它的设计目的是有效地检测和定位源代码中的漏洞。
它能帮助开发人员识别和修复代码中的安全漏洞。
Fortify的主要功能:
- 静态代码分析:它会对源代码进行静态分析,找出可能导致安全漏洞的代码片段。它能识别多种类型的安全漏洞,如 SQL 注入、跨站脚本(XSS)、缓冲区溢出等。
- 数据流分析:它不仅分析单个代码文件,还跟踪应用程序的数据流。这有助于找到更复杂的漏洞,如未经验证的用户输入在应用程序中的传播路径。
- 漏洞修复建议:发现潜在的安全漏洞时,它会为开发人员提供修复建议。
- 集成支持:它可以与多种持续集成(CI)工具(如 Jenkins)和应用生命周期管理(ALM)工具(如 Jira)集成,实现自动化的代码扫描和漏洞跟踪。
- 报告和度量:它提供了丰富的报告功能,帮助团队了解项目的安全状况和漏洞趋势。
使用Fortify扫描代码的结果:
一般推荐它跟Jenkins集成,定期扫描项目中test分支中的代码安全问题。
6 写单元测试
有些小伙伴可能会问:写单元测试可以减少代码的BUG?
答案是肯定的。
我之前有同事,使用的测试驱动开发模式,开发一个功能模块之前,先把单元测试写好,然后再真正的开发业务代码。
后面发现他写的代码速度很快,而且代码质量很高,是一个开发牛人。
如果你后期要做系统的代码重构,你只是重写了相关的业务代码,但业务逻辑并没有修改。
这时,因为有了之前写好的单位测试,你会发现测试起来非常方便。
可以帮你减少很多BUG。
7 功能自测
功能自测,是程序员的基本要求。
但有些程序员自测之后,BUG还是比较多,而有些程序员自测之后,BUG非常少,这是什么原因呢?
可能有些人比较粗心,有些人比较细心。
其实更重要的是测试的策略。
有些人喜欢把所有相关的功能都开发完,然后一起测试。
这种情况下,相当于一个黑盒测试,需要花费大量的时间,梳理业务逻辑才能测试完整,大部分情况下,开发人员是没法测试完整的,可能会有很多bug测试不出来。
这种做法是没有经过单元测试,直接进行了集成测试。
看似节省了很多单元测试的时间,但其实后面修复BUG的时间可能会花费更多。
比较推荐的自测方式是:一步一个脚印。
比如:你写了一个工具类的一个方法,就测试一下。如果这个方法中,调用了另外一个关键方法,我们可以先测试一下这个关键方法。
这样可以写出BUG更少的代码。
最近就业形式比较困难,为了感谢各位小伙伴对苏三一直以来的支持,我特地创建了一些工作内推群, 看看能不能帮助到大家。你可以在群里发布招聘信息,也可以内推工作,也可以在群里投递简历找工作,也可以在群里交流面试或者工作的话题。
8 自动化测试
有些公司引入了自动化测试的功能。
每天都会自动测试,保证系统的核心流程没有问题。
因为我们的日常开发中,经常需要调整核心流程的代码。
不可能每调整一次,都需要把所有的核心流程都测试一遍吧,这样会浪费大量的时间,而且也容易遗漏一些细节。
如果引入了自动化测试的功能,可以帮助我们把核心流程都测试一下。
避免代码重构,或者修改核心流程,测试时间不够,或者测试不完全的尴尬。
自动化测试,可以有效的减少核心流程调整,或者代码重构中的BUG。
9 代码review
很多公司都有代码review机制。
我之前也参与多次代码review的会议,发现代码review确实可以找出很多BUG。
比如:一些代码的逻辑错误,语法的问题,不规范的命名等。
这样问题通过组内的代码review一般可以检查出来。
有些国外的大厂,采用结对编程
的模式。
同一个组的两个人A和B一起开发,开发完之后,A reivew B的代码,同时B review A的代码。
因为同组的A和B对项目比较熟,对对方开发的功能更有了解,可以快速找出对外代码中的一些问题。
能够有效减少一些BUG。
10 多看别人的踩坑分享
如果你想减少日常工作中的代码BUG,或者线上事故,少犯错,少踩坑。
经常看别人真实的踩坑分享,是一个非常不错的选择,可以学到一些别人的工作经验,帮助你少走很多弯路。
最后说一句,本文总结了10种减少代码BUG的小技巧,但我们要根据实际情况选择使用,并非所有的场景都适合。
来源:juejin.cn/post/7359083483237859367
如何让你喜欢的原神角色陪你写代码
如何让你喜欢的原神角色陪你写代码
每天上班,脑子里面就想着一件事,原神,啊不对,VsCode!启动!(doge 狗头保命),那么如何将这两件事情结合起来呢?特别是原神里面有那么多我喜欢的角色。
最终效果预览
- 右下角固定一只小可爱
- 全屏一只小可爱
省流直接抄作业版
vscode
下载background
插件
- 第一次使用这个插件会提示
vscode
已损坏让你restart vscode
,不要慌,因为插件底层是改 vscode 的样式 - 需要管理员权限,如果你安装到了 C 盘
copy
下面的配置到你的settings.json
文件
2.1 右下角固定图片配置
{
"background.customImages": [
"https://upload-bbs.miyoushe.com/upload/2023/02/24/196231062/8540249f2c0dd72f34c8236925ef45bc_3880538836005347690.png",
"https://user-images.githubusercontent.com/41776735/163673800-0e575aa3-afab-405b-a833-212474a51adc.png"
],
"background.enabled": true,
"background.style": {
"content": "''",
"width": "30%",
"height": "30%",
"opacity": 0.3,
"position": "absolute",
"bottom": "0",
"right": "7%",
"z-index": "99999",
"background-repeat": "no-repeat",
"background-size": "contain",
"pointer-events": "none"
},
"background.useDefault": false,
"background.useFront": true
}
2.2 全屏图片配置
{
"background.customImages": ["https://upload-bbs.miyoushe.com/upload/2024/01/15/196231062/1145c3a2f56b2f789a9be086b546305d_3972870625465222006.png"],
"background.enabled": true,
"background.style": {
"content": "''",
"pointer-events": "none",
"position": "absolute",
"z-index": "99999",
"height": "100%",
"width": "100%",
"background-repeat": "no-repeat",
"background-size": "cover",
"background-position": "content",
"opacity": 0.3
},
"background.useDefault": false,
"background.useFront": true
}
详细说明版
第一步,你还是需要下载 background
这个插件。
background
插件的官方中文文档
第二步,配置解析
我们看官方文档里面,有两个配置是需要说明一下的
background.customImages
:
- 是一个数组,默认是用第一张图片做背景图,在分屏的时候第二屏就用的是数组的第二个元素,以此类推
- 支持 https 协议和本地的 file 协议
- 因为我有公司机器和自己电脑,所以本地协议不太适用我,但是我之前一直用的是本地协议,并且下面会教大家如何白嫖图床
background.style
:
- 作为一个前端,看到上面的内容是不是十分的熟悉,其实这个插件的底层原理就是去改 vscode 的 css,所以你可以想想成一个需求,在一个界面上显示一张图片,css 可以怎么写,背景图就可以是啥样的
- 如果你看不懂 css 代码,你可以直接复制我上面提供的两种配置,一个全屏的,一个是在右下角固定一小只的
- 如果你觉得背景图太亮了,有点看不清代码,你可以修改
background.style.opacity
这个属性,降低图片的透明度。 - 发现最新版本支持
background.interval
来轮播背景图了
高清社死图片
OK,其实到这就没啥了,但是到这里和原神好像并没有什么太大的关系,那现在我就教你怎么白嫖米游社的图床,并且获取高清的原神图片
- 第一步,你需要有一个米游社的账号,当然,玩原神的不能没有吧
- 第二步,随便找个帖子进行评论回复,如果你怕麻烦别人,可以自己发一贴
- 第三步:新标签页打开
- 到这里,你就能白嫖米游社的图床了,如果你是评论的自己的图片,新标签页打开之后,删除 url 中
?
后面所有的内容,就可以得到原图 https 的链接了 - 但是又遇到一个问题,我就是很喜欢米游社的图片,但是图片的分辨率太低了怎么办?
- 提供两个用 ai 来放大图片的网站
- waifu2x.udp.jp/
- bigjpg.com/
- 把图片放大之后,在用上面白嫖图床的方法就可以了
最后
水一期,求三连 + 可以在评论区分享你的背景图嘛
来源:juejin.cn/post/7324143986759303194
NestJs: 定时任务+redis实现阅读量功能
抛个砖头
不知道大家用了这么久的掘金,有没有对它文章中的阅读量的实现有过好奇呢?
想象一下,你每次打开一篇文章时,都会有一个数字告诉你多少人已经读过这篇文章。那么这个数字是怎么得出来的呢?
有些人可能会认为,每次有人打开文章,数字就加1,不是很简单吗? 起初我也以为这样,哈哈(和大家站在同一个高度),但(好了,我不说了,继续往下看吧!)
引个玉
文章阅读量的统计看似简单,实则蕴含着巧妙的逻辑和高效的技术实现。我们想要得到的阅读量,并非简单的页面刷新次数,而是真正独立阅读过文章的人数。因此,传统的每次页面刷新加1的方法显然不够准确,它忽略了用户的重复访问。
同时,阅读作为一个高频操作,如果每次都直接写入数据库,无疑会给数据库带来巨大的压力,甚至可能影响到整个系统的性能和稳定性。这就需要我们寻找一种既能准确统计阅读量,又能减轻数据库压力的方法。
Redis,这个高性能的内存数据库,为我们提供了解决方案。我们可以利用Redis的键值对存储特性,将用户ID和文章ID的组合作为键,设置一个短暂的过期时间,比如15分钟。当用户首次访问文章时,我们在Redis中为这个键设置一个值,表示该用户已经阅读过这篇文章。如果用户在15分钟内再次访问,我们可以直接判断该键是否存在,如果存在,则不再增加阅读量,否则进行增加。
这种方法的优点在于,它能够准确地统计出真正阅读过文章的人数,而不是简单的页面刷新次数。同时,通过将阅读量先存储在Redis中,我们避免了频繁地写入数据库,从而大大减轻了数据库的压力。
最后,我们还需要考虑如何将Redis中的阅读量最终写入数据库。由于数据库的写入操作相对较重,我们不宜频繁进行。因此,我们可以选择在业务低峰期,比如凌晨2到4点,使用定时任务将Redis中的阅读量批量写入数据库。这样,既保证了阅读量的准确统计,又避免了频繁的数据库写入操作,实现了高效的系统运行。
思路梳理
- 😎Redis 助力阅读量统计,方法超好用!✨
- 🧐在 Redis 存用户和文章关系,轻松解决多次无效阅读!👏
- 💪定时任务来帮忙,Redis 数据写入数据库,不再
那么接下来就是实现环节
代码层面
项目使用的后端框架为NestJS
配置下redis
一、安装redis plugin
npm install --save redis
二、创建redis模块
三、初始化连接redis相关配置
@Module({
providers: [
RedisService,
{
provide: 'REDIS_CLIENT',
async useFactory(configService: ConfigService) {
console.log(configService.get('redis_server_host'));
const client = createClient({
socket: {
host: configService.get('redis_server_host'),
port: configService.get('redis_server_port'),
},
database: configService.get('redis_server_db'),
});
await client.connect();
return client;
},
inject: [ConfigService],
},
],
exports: [RedisService],
})
Redis是一个Key-Value型数据库,可以用作数据库,所有的数据以Key-Value的形式存在服务器的内存中,其中Value可以是多种数据结构,如字符串(String)、哈希(hashes)、列表(list)、集合(sets)和有序集合(sorted sets)等类型
在这里会用到字符串和哈希两种。
创建文章表和用户表
我的项目中创建有post.entity和user.entity这两个实体表,并为post文章表添加以下
这三个字段,在这里我们只拿 阅读量 说事。
访问文章详情接口-阅读量+1
/**
* @description 增加阅读量
* @param id
* @returns
*/
@Get('xxx/:id')
@RequireLogin()
async frontIncreViews(@Param('id') id: string, @Req() _req: any,) {
console.log('frontFindOne');
return await this.postService.frontIncreViews(+id, _req?.user);
}
前文已经说过,同一个用户多次刷新,如果不做处理,就会产生多次无效的阅读量。 因此,为了避免这种情况方式,我们需要为其增加一个用户文章id组合而成的标记,并设置在有效时间内不产生多次阅读量。
那么,有的掘友可能会产生一个疑问,如果用户未登录,那么以游客的身份去访问文章就不产生阅读记录了吗?
其实同理!
在我的项目中只是,要求需要用户登录后才能访问,
那么我这就会以 userID_postID_ 来组成标识区分用户和文章罢了。
而如果想以游客身份,我们可以获取用户 IP_postID 这样的组合来做标识即可
接下来说下postService
中调用的frontIncreViews
方法
直接贴代码:
const res = await this.redisService.hashGet(`post_${id}`);
if (res.viewCount === undefined) {
const post = await this.postRepository.findOne({ where: { id } });
post.viewCount++;
await this.postRepository.update(id, { viewCount: post.viewCount });
await this.redisService.hashSet(`post_${id}`, {
viewCount: post.viewCount,
likeCount: post.likeCount,
collectCount: post.collectCount,
});
// 在用户访问文章的时候在 redis 存一个 10 分钟过期的标记,有这个标记的时候阅读量不增加
await this.redisService.set(`user_${user.id}_post_${id}`, 1, 10);
return post.viewCount;
} else {
const flag = await this.redisService.get(`user_${user.id}_post_${id}`);
console.log(flag);
if (flag) {
return res.viewCount;
}
await this.redisService.hashSet(`post_${id}`, {
...res,
viewCount: +res.viewCount + 1,
});
await this.redisService.set(`user_${user.id}_post_${id}`, 1, 10);
return +res.viewCount + 1;
}
}
- 从Redis获取文章阅读量:
const res = await this.redisService.hashGet(`post_${id}`);
使用Redis的哈希表结构,从键post_${id}
中获取文章的信息,其中可能包含阅读量(viewCount
)、点赞数(likeCount
)和收藏数(collectCount
)。
2. 检查Redis中是否存在阅读量:
if (res.viewCount === undefined) {
如果Redis中没有阅读量数据,说明这篇文章的阅读量还没有被初始化。
3. 从数据库中获取文章并增加阅读量:
const post = await this.postRepository.findOne({ where: { id } });
post.viewCount++;
await this.postRepository.update(id, { viewCount: post.viewCount });
从数据库中获取文章,然后增加阅读量,并更新数据库中的文章阅读量。
4. 将更新后的文章信息存回Redis:
await this.redisService.hashSet(`post_${id}`, {
viewCount: post.viewCount,
likeCount: post.likeCount,
collectCount: post.collectCount,
});
将更新后的文章信息(包括新的阅读量、点赞数和收藏数)存回Redis的哈希表中。
5. 设置用户访问标记:
await this.redisService.set(`user_${user.id}_post_${id}`, 1, 10);
在用户访问文章时,在Redis中设置一个带有10分钟过期时间的标记,用于防止在10分钟内重复增加阅读量。
6. 返回阅读量:
return post.viewCount;
返回更新后的阅读量。
7. 如果Redis中存在阅读量:
} else {
const flag = await this.redisService.get(`user_${user.id}_post_${id}`);
console.log(flag);
如果Redis中存在阅读量数据,则检查用户是否已经访问过该文章。
8. 检查用户访问标记:
if (flag) {
return res.viewCount;
}
如果用户已经访问过该文章(标记存在),则直接返回当前阅读量,不增加。
9. 如果用户未访问过文章:
await this.redisService.hashSet(`post_${id}`, {
...res,
viewCount: +res.viewCount + 1,
});
await this.redisService.set(`user_${user.id}_post_${id}`, 1, 10);
return +res.viewCount + 1;
如果用户未访问过该文章,则增加阅读量,并重新设置用户访问标记。然后返回更新后的阅读量。
简而言之,目的是在用户访问文章时,确保文章阅读量只增加一次,即使用户在短时间内多次访问。
NestJS使用定时任务包,实现redis数据同步到数据库中
有的掘友可能疑问,既然已经用redis来做阅读量记录了,为什么还要同步到数据库中,前文开始的时候,就已经提到过了,一旦我们的项目重启, redis 数据就没了,而数据库却有着“数据持久性的优良品质”。不像redis重启后,又是个新生儿。但是它们的互补,又是1+1大于2的那种。
好了,不废话了
一、引入定时任务包 @nestjs/schedule
npm install --save @nestjs/schedule
在 app.module.ts
引入
二、创建定时任务模块和服务
nest g module task
nest g service task
你可以在同一个服务里面声明多个定时任务方法。在 NestJS 中,使用 @nestjs/schedule
库时,你只需要在服务类中为每个定时任务方法添加 @Cron()
装饰器,并指定相应的 cron 表达式。以下是一个示例,展示了如何在同一个服务中声明两个定时任务:
import { Injectable } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
@Injectable()
export class TasksService {
// 第一个定时任务,每5秒执行一次
@Cron(CronExpression.EVERY_5_SECONDS)
handleEvery5Seconds() {
console.log('Every 5 seconds task executed');
}
// 第二个定时任务,每10秒执行一次
@Cron(CronExpression.EVERY_10_SECONDS)
handleEvery10Seconds() {
console.log('Every 10 seconds task executed');
}
}
三、实现定时任务中同步文章阅读量的任务
更新文章的阅读数据
await this.postService.flushRedisToDB();
// 查询出 key 对应的值,更新到数据库。 做定时任务的时候加上
async flushRedisToDB() {
const keys = await this.redisService.keys(`post_*`);
console.log(keys);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
const res = await this.redisService.hashGet(key);
const [, id] = key.split('_');
await this.postRepository.update(
{
id: +id,
},
{
viewCount: +res.viewCount,
},
);
}
}
- 从 Redis 获取键:
const keys = await this.redisService.keys(
post_*);
: 使用 Redis 服务的keys
方法查询所有以post_
开头的键,并将这些键存储在keys
数组中。
console.log(keys);
: 打印出所有查询到的键。 - 遍历 Redis 键:
使用
for
循环遍历所有查询到的键。 - 从 Redis 获取哈希值:
const res = await this.redisService.hashGet(key);
: 对于每一个键,使用 Redis 服务的hashGet
方法获取其对应的哈希值,并将结果存储在res
中。 - 解析键以获取 ID:
const [, id] = key.split('_');
: 将键字符串按照_
分割,并取出第二个元素(索引为 1)作为id
。这假设键的格式是post_<id>
。 - 更新数据库:
使用
postRepository.update
方法更新数据库中的记录。
{ id: +id, }
: 指定要更新的记录的id
。+id
是将id
字符串转换为数字。
{ viewCount: +res.viewCount, }
: 指定要更新的字段及其值。这里将viewCount
字段更新为 Redis 中存储的值,并使用+res.viewCount
将字符串转换为数字。
等到第二天,哈,数据就同步来了
访问:

而产生的后台数据:
抛出问题
如果能看到这里的掘友,若能接下这个问题,说明你已经掌握了吖
问题1:
如何实现一个批量返回redis键值对的方法(这个方法问题2需要用到)
问题2:
用户查询文章列表的时候,如何整理数据后返回文章阅读量呈现给用户查看
来源:juejin.cn/post/7355554711166271540
骚操作:如何让一个网页一直处于空白情况?
好了,周末闲来无事,突然有个诡异想法!
如题,惯性思路很简单,就是直接撸上一个空内容的html。
注:以下都是在现代浏览器中执行,主要为**Chrome 版本 120.0.6099.217(正式版本) (64 位)和Firefox123.0.1 (64 位) **
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>demo</title>
</head>
<body>
</body>
</html>
؏؏☝ᖗ乛◡乛ᖘ☝؏؏~
但是,要优雅~咱玩的花一点,如果这个HTML中加入一行文字,比如下面这样,如何让这行文字一直不显示出来呢?
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>demo</title>
</head>
<body>
<div>放我出去!!!</div>
</body>
</html>
思考几秒~有了,江湖一直传言,Javascrip代码执行不是影响Render树生成么,上循环!于是如下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>demo</title>
</head>
<body>
<div>放我出去!!!</div>
<script>
while (1) {
let a;
}
// 或者这样
/*(function stop() {
var message = confirm("我不想让文字出来!");
if (message == true) {
stop()
} else {
stop()
}
})()*/
</script>
</body>
</html>
```一下一下
bingo,可以实现,那再换个思路呢?加载资源?
说干就干,在开发者工具上,设置上下载速度为1kb/s,测试了以下三种类型资源
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>demo</title>
</head>
<body>
<!-- <link rel="stylesheet" href="https://lf3-cdn-tos.bytescm.com/obj/static/xitu_juejin_web/static/bytedesign.min.css" as="style"/> -->
<!-- <img src="https://lf3-cdn-tos.bytescm.com/obj/static/xitu_juejin_web/static/bytedesign.min.css"/> -->
<div class="let-it-go">放我出去!!!</div>
<script src="https://lf3-cdn-tos.bytescm.com/obj/static/log-sdk/collect/5.1/collect.js"></script>
<style>
.let-it-go {
color: red;
}
</style>
</body>
</html>
总得来说,JS和CSS文件,需要排在.let-it-go元素前面或者样式前面,才会影响到渲染DOM或者CSSOM,图片或者影片之类的,不管放前面还是后面,都无影响。如果在css文件中,一直有import外部CSS,也是有很大影响!
但正如题目,这种只能影响一时,却不能一直影响,就算你在代码里写一个在头部不停插入脚本,也没有用,比如如下这么写,按,依旧无效:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>demo</title>
</head>
<body>
<link rel="stylesheet" href="https://lf3-cdn-tos.bytescm.com/obj/static/xitu_juejin_web/static/bytedesign.min.css"
as="style" />
<!-- <img src="https://lf3-cdn-tos.bytescm.com/obj/static/xitu_juejin_web/static/bytedesign.min.css"/> -->
<script>
// setInterval(()=>{
// 不停插入script脚本 或者css文件
let index = '';
(function fetchFile() {
var script = document.createElement('script');
script.src = `https://lf3-cdn-tos.bytescm.com/obj/static/log-sdk/collect/5.1/collect${index}.js`;
document.head.appendChild(script);
script.onload = () => {
fetchFile()
}
script.onerror = () => {
fetchFile()
}
index+=1
// 创建一个 link 元素
//var link = document.createElement('link');
// 设置 link 元素的属性
// link.rel = 'stylesheet';
// link.type = 'text/css';
// link.href = 'https://lf3-cdn-tos.bytescm.com/obj/static/xitu_juejin_web/app.f81e9f9${index}.css';
// 将 link 元素添加到文档的头部
//document.head.appendChild(link);
})()
// },1000)
</script>
<div class="let-it-go">放我出去!!!</div>
<style>
.let-it-go {
color: red;
}
</style>
<!-- <script src="https://lf3-cdn-tos.bytescm.com/obj/static/log-sdk/collect/5.1/collect.js"></script> -->
</body>
</html>
那么,还有别的方法吗?暂时没有啥想法了,等后续再在这篇上续接~
另外,在实验过程中,有一个方式让我很意外,以为以下代码也会造成页面一直空白,但好像不行。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>demo</title>
</head>
<body>
<div id="appp"></div>
<script>
(function createElement() {
var parentElement = document.getElementById('appp');
// 创建新的子元素
var newElement = document.createElement('div');
// 添加文本内容(可选)
newElement.textContent = '这是新的子元素';
// 将新元素添加到父元素的子元素列表的末尾
parentElement.appendChild(newElement);
createElement()
})()
</script>
<div class="let-it-go">放我出去!!!</div>
</body>
</html>
这可以很好的证明,插入DOM元素这个任务,会在主HTML渲染之后再执行。
祝周末愉快~
来源:juejin.cn/post/7344164779629985818
js运算精度丢失,用这个库试试?
简述
当js
进行算术运算时,有时候会遇到以下几个问题:
// 控制台可以尝试以下代码
0.1 + 0.2 // 0.30000000000000004
0.3 - 0.1 // 0.19999999999999998
19.9 * 100 // 1989.9999999999998
为什么会遇到这个问题呢?
由于在计算机运算过程中,十进制的数会被转化为二进制来运算,有些浮点数用二进制表示是无穷的,浮点数运算标准(IEEE 754)64位双精度的小数部分最多支持53位二进制位,运算过程中超出的二进制位会被截断。运算完后再转为十进制。所以产生了精度问题。
为了解决此问题,整理了一些第三方的js
库。
相关js
库推荐
js库名称 | 备注 |
---|---|
Math.js | JavaScript 和 Node.js 的扩展数学库 |
decimal.js | javaScript 任意精度的库 |
big.js | 一个轻量的任意精度库 |
big.js
版本介绍
本次用的big.js
版本为6.2.1
页面引入
下载big.js
:
访问以下页面,在网页中右键另存为即可
// 因为作为本地测试,就不下载压缩版本了
https://cdn.jsdelivr.net/npm/big.js@6.2.1/big.js
// 若需要压缩版本
https://cdn.jsdelivr.net/npm/big.js@6.2.1/big.min.js
引入到html
页面:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Big js</title>
</head>
<body>
<!-- 引入页面 -->
<script src="./js/big.js"></script>
<script>
// 尝试Big构造方法
console.log('Big', Big)
</script>
</body>
</html>
工程化项目
npm install big.js
在所需页面引入:
// 现在一般用 es 模块引入
import Big from 'big.js';
使用
基本演示:
// 加
let a = new Big(0.1)
a = a.plus(0.2)
// 由于运算结果是个对象,所以展示以下值
console.log('a', a) // {s: 1, e: -1, c: Array(1)}
// 可以使用 Number || toNumber() 转为我们需要的数值
console.log('a', a.toNumber) || console.log('a', Number(a)) // 0.3
可以链式调用:
x.div(y).plus(z).times(9)
参考文档
// big.js 项目 github地址
https://mikemcl.github.io/big.js
// big.js 官方文档地址
https://mikemcl.github.io/big.js/
// 这篇文档将api写的很全了
https://blog.csdn.net/a123456234/article/details/132305810
来源:juejin.cn/post/7356531073469825033
了解这四个心理陷阱,让你摆脱心理上的“贫穷”
背景
Hi 你好,我是东东拿铁,一个正在探索个人IP的后端程序员。
我发现自己的经济情况正在处于一个紧张的状态。上个月的信用卡需要还款8600块钱,仔细看了下账单,分期支付的却有3000多,占比将近4成,这很可怕。
明明我已经每个月都尽量的省吃俭用,克制消费,但看到月底还款金额仍然居高不下,便会不由自主的有一些焦虑的情绪,甚至影响到日常的消费。
比如消耗更多精力去克制自己买想要东西的欲望,消费时去对比不同渠道的价格,变得对价格更加敏感。
我意识到我陷入到了“稀缺”的情绪中,并且这个情绪总是出现,并且影响着我做事的思路和方式。我想摆脱它,因为我们有很多重要的事情等着我去做,比如去看书学习更多的东西,又或者持续输出写下一篇篇文章。
上周看了《稀缺》这本书,罗振宇评价说:“我们的肉身刚步入富足的时代,但我们的精神还滞留在稀缺的恐惧之中。穷人思维,根植于人类的基因。率先用理性击碎它的人,也将率先获得心与灵的富足。”
我已经陷入了金钱稀缺的状态,这是是因为之前的各类分期导致的。那么稀缺是什么,它是如何影响我们的,又该如何摆脱它?
看完这篇文章,相信你会有答案。
稀缺
什么是稀缺
先说第一点,什么是稀缺?稀缺是长期缺乏,而导致的一种稀缺心态,是“拥有”少与“需要”的感觉。
- 我们觉着积蓄太少,我们需要更好的房、车,永远都有还不完的账单
- 我们觉着时间太少,我们需要时间旅游、健身、陪伴家人
- 我们觉着精力太少,我们需要关注工作、关注个人成长 、关注国家大事
举个例子,老板给你安排了一项周五要完成的任务,然而到了周四还没有做完,这时候你会变的心急如焚,这是你就会全神贯注的地开始工作,相比于其他事情,你会优先处理这项紧急任务,只关注手头的工作,这就是进入到了一种稀缺状态。
幸运的是,稀缺带给你了专注红利,让你你在老板问你进度之前,完成了工作并顺利交差,你长舒一口气。就像开学前最后一天写完了暑假作业,像毕业设计答辩前终于写完了论文一般,这种场景似乎已经经历了很多次。
专注红利:就是短时间内集中精力爆发出高度的注意力,让我们高产出地工作,我们会在专注红利的帮助下把剩下的资源用得淋漓尽致。
话又说回来,虽然这样的专注在短时间内能够带来好处,但如果长时间处于稀缺心态中,并不是什么好事。它会把一个人拖向“贫穷”,进入一个匮乏的恶性循环。要理解这个循环,我们需要看看稀缺会对我们产生什么样的影响。
管窥
第一种叫做管窥效应。管窥就好像你通过一根管子看东西,这时你只能看到管子里面的东西,而管子外面的什么都看不见。中国有句成语:一叶障目,不见泰山。
管窥效应会改变我们的决策方式,举个例子,程序员日常的工作很忙碌,不但要写代码、改bug,还要参加需求评审的各种各样的会议。我们平常都会在工作疲惫的时候,选择站起来走做,接杯水,活动一下身体。但是在忙碌的时候,就会觉着喝水、 散步也没有这么重要了,赶紧把手头的工作做完吧,半天不喝水,一坐一整天似乎也没什么关系。(你多久没有起来活动活动身体了?)
我们虽然都知道,久坐在电脑前,对颈椎、眼睛的伤害很大,工作一个小时我们至少要起来活动五分钟,眼睛远眺一下。长远来看,你很明显知道对身体的投资是重要的,但是你在稀缺状态下,你就会做出损害长期价值的决定。
这是一段我自己很真实的经历,在上家公司时我接到了一个比较大的项目,面临新入职和换语言的情况,项目的排期显得尤其紧张,那段时间我几乎白天不怎么站起来活动,即使是在眼睛干涩的情况下,我也不得不写下一行行代码,改一个个难以排查的bug。
我的眼睛很早之前做过激光手术,其实并不适合一直盯着电脑工作。但那段时间在高压的工作环境下,最终项目上线了,却对眼睛造成了很大的伤害。
借用
再说稀缺导致的第二种影响,是借用。借用非常好理解,就是习惯性地透支未来的资源。
比如说,开头提到的我的情况,信用卡。我通过信用卡分期来解决之前遇到的现金问题,但造成影响却是长期的。每次收到工资,我就需要去还房贷、信用卡等一系列的债务。
这时候,如果我的一个同学或者朋友需要结婚,我需要给他们包一个大红包,那这个月就会更加紧张。而且银行不当人,每个月在账单出来之后,都会打电话问是否需要办理分期。
我们说到的借用,其实不止是钱。忙碌的时候,我们忙碌时也会对时间做借用。比如说,这周的工作没做完,我们就放到下一周。但下一周会有下一周的工作,所以我们会长期处于一种稀缺状态里。
再比如说,白天繁忙的工作让我们缺少娱乐时间,这时候我们就会选择熬夜。但熬夜会导致第二天的工作效率更低,结果我们的工作时间可能会更长。
没有余闲
稀缺导致的第三个影响是没有余闲,余闲就是我们剩余下来的,没有利用的时间和空间。
举一个例子,之前在外地工作的时候,假期结束前,总会带不少东西回北京,临走之前,我会把所有需要装的东西放进去,比如鞋、衣服、数码装备,甚至是一些吃吃喝喝。如果我这次用一个大的箱子,那么装完必须的用品之后,我们可能还会发现有很多剩余的空间。我们就可以放进去一些不那么需要的东西,比如珍藏的几本小说,甚至是一些下个季度才需要穿的衣物。在这个过程中,我们的心情会非常舒畅,有极大的”富裕感“。
但是,如果我们只有一个背包,那就不能像刚刚那样舒服了。我们需要开始权衡和比较:鞋子到底带哪一双?换洗的衣服要带多少?游戏机和书本要不要带呢?为了保证空间的使用,我们甚至需要我妈把里面的东西全部拿出来,通过分门别类的方式再塞回去。这样反反复复地收拾几次,直到包变的满满当当。
也许你会说,这样是不是更高效呀?毕竟我们用一个并不大的背包装完了所有需要的行李。从某种角度上来说是这样的,但是在这个过程中,我耗费了大量的精力和时间,去权衡一些无关紧要的东西。这种思维看起来很高效,但会让人产生大量的心智负担。这些心智负担会消耗注意力和精力,从而进一步产生管窥效应,只让你注重当前的事情,而忽略真正重要的事情。
带宽不足
稀缺导致的第四个影响,也是我认为最重要的一个点,那就是带宽不足。这里的带宽,指的是我们的认知能力和执行控制力。
我们从处理日常问题到思考问题都需要带宽,但是我们一般最多只能关注七件事。比如日常使用的APP不会超过七个,经常交往的联系人也不会超过七个。“七”通常是人类认知所能承受的一个临界点。超过这个数字,一个人就会产生严重的带宽负担,这个时候就会感觉精力不够用。
如果说在物质不够丰富的时代,养家糊口让人的带宽降低。可目前科技、社会发展这么快,我们这一代年轻人,需要考虑的事情也变得越来越多,比如,工作的压力,结婚,房车,再到各种风口什么小红书、AI、国家政策。
我们或许不贫穷了,但是我们的带宽一样被占的很满。
作者在书中提到了一个实验,实验者进行了两次测试,让被测试者去做测试题。
第一次测试,让他们什么都不想的去做测试题。第二次测试,提前对他们进行诱导,让他们思考自己的经济状况和他们关注自己缺乏的东西。这两组测试结果显示,第一组的分数要高很多,基本上是第二组的两倍,表明带宽会影响我们的智力水平。
比如说,不少年轻人喜欢熬夜(我也是),晚上睡不好,白天可能就会进入一种游离迟钝的状态。遇到难题的时候,选择去去抽烟喝酒,然后让自己陷入更大的问题中。其实绝大部分人缺的不是时间,而是带宽。
你有多长时间没有做一份自己的长期规划了?最近有学习什么新的技能吗?有去定期投资、理财吗?你需要更多的注意力放在长远的事情上,这样才能最终跳出稀缺的怪圈。
跳出稀缺陷阱
我们要明白稀缺并非个人特质,而是自身创造的环境条件所引发的结果,而这些条件是可以进行管理的。我们越是深入了解稀缺在大脑中的发展历程,就越有可能找到办法去避免稀缺陷阱,或至少去减轻稀缺陷阱的影响程度。
节约带宽
如何节约带宽?这里介绍几个我在生活中亲身实践并感觉有所收益的方式。
减少决策
马克·扎克伯格的标志性穿衣风格是灰色T恤和拉链连帽衫。
他这样的穿衣风格主要是为了节省时间,避免每天早上花费时间选择衣服。他曾经在接受采访时表示,他希望生活尽可能简单,减少不必要的决策,以便将更多的精力集中在重要的事情上。
我的穿衣风格都是休闲类,所以没有太大的选择空间,鞋子的话空军一号有两双且都是白色,买新衣服和新鞋我也会尽量从已有的风格里去挑选,这样无论是买东西还是日常穿衣,都不需要做太多决策。
减少信息过载
控制社交媒体和新闻资讯的浏览时间,过多的信息会占据我们的大脑,消耗带宽。我个人实践比较有效的方式是手机上卸载抖音,但是在iPad保留,想刷的话只能等下班孩子睡了再刷一会。这样白天即使是碎片时间也不会被浪费,其余时间专注于其他事情。
关闭大部分APP通知,屏蔽不重要的群。目前我的手机通知开启的APP大概只有银行类、微信、短信等的通知。微信里面不重要的微信群都会屏蔽掉,甚至是工作群,如果实在怕错过信息,微信群可以设置特别关注的用户,这样也能帮你过滤掉大部分信息。
缓解压力
定期进行体育锻炼,不开心的事情,和朋友们交流,可以减轻心理压力,释放带宽。
有一些心里的事情,也可以通过文字的方式记录下来,比如我很多想说的话就会通过写作的方式写下来,分享出来,心里的带宽也就释放了。
留有余闲
在金钱上,再没有钱,也要留出一小部分来投资、储蓄,投资也可以是投资自己。定期储蓄如果你总是忘记,银行的APP都提供了自动储蓄的功能,每当发工资就自动把一部分钱自动存起来。
其实现在的公积金、五险一金也是同理,自动帮我们对未来进行储蓄,让忽视变成默许。
在时间上,当天的工作当天完成,未来还有未来的事情要做。
这里分享一个小技巧,之前在字节工作的时候,如果你的日历上面没有日程的话,很容易就会被别人约会或者约面试。你可以在需要余闲的时间,自己给自己约一个2个小时的会议,这段时间就可以确保不会有人来打扰你。
设置提醒
现在的打工人,都会因为工作的繁忙,白天一坐一整天,甚至连喝水的时间都没有,直到体检时候颈椎生理曲度变直,发现有肾结石才开始重视身体。
晚上不停的熬夜,10点钟打开抖音,回过神来时已经凌晨1点,第二天上班昏昏欲睡才懊悔不已。
善用提醒, 比如类似Eye Monitor的监控工具,可以监控你使用电脑的时常,在你疲劳的时候提醒你,或者给自己配一个智能手表,久坐时给予提示。
手机上设置睡眠时间,短视频上设置休息时间,那么过了那个时间,就会不断的提醒你,该睡觉了。
本质上提醒就是让你从管窥的视角中拽出来,让更多重要的事情进入到你的视角里,让你无法忽视那些更重要的事情。
说在最后
好了,文章到这里就要结束了,感谢你能看到最后。
总结一下,稀缺是一种心态,短暂的稀缺有一定好处,会让我们产生专注红利,但是长期的稀缺,这种稀缺心态就会掉进稀缺的陷阱里。让我们产生管窥效应,就是只关注紧急的东西,而忽视重要的东西。它会让我们没有余闲,让自己的工作、生活缺乏弹性;而且它会让我们容易借用,去透支未来的资源;它还会减少我们的带宽,增加做出错误决定的几率,最终让我们进入一个稀缺怪圈的恶性循环。
如果想跳出这个怪圈,方法有三个:一是节约带宽,减少权衡式的思维;二是留有余闲,让自己的效率更高;三是设置提醒,让重要的事情及时出现在视野当中。
来源:juejin.cn/post/7410220431821111296
前端实现图片压缩方案总结
前文提要
在Web开发中,图片压缩是一个常见且重要的需求。随着高清图片和多媒体内容的普及,如何在保证图片质量的同时减少其文件大小,对于提升网页加载速度、优化用户体验至关重要。前端作为用户与服务器之间的桥梁,实现图片压缩的功能可以显著减轻服务器的负担,加快页面渲染速度。本文将探讨前端实现图片压缩的几种方法和技术。
1. 使用HTML5的<canvas>元素
HTML5的<canvas>元素为前端图片处理提供了强大的能力。通过JavaScript操作<canvas>,我们可以读取图片数据,对其进行处理(如缩放、裁剪、转换格式等),然后输出压缩后的图片。
步骤概述:
- 读取图片:使用
FileReader
或Image
对象加载图片。 - 绘制到<canvas>:将图片绘制到<canvas>上,通过调整<canvas>的尺寸或绘图参数来控制压缩效果。
- 导出图片:使用
canvas.toDataURL()
方法将<canvas>内容转换为Base64编码的图片,或使用canvas.toBlob()
方法获取Blob对象,以便进一步处理或上传。
示例代码:
function compressImage(file, quality, callback) {
const reader = new FileReader();
reader.onload = function(e) {
const img = new Image();
img.onload = function() {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// 设置canvas的尺寸,这里可以根据需要调整
const MAX_WIDTH = 800;
const MAX_HEIGHT = 600;
let width = img.width;
let height = img.height;
if (width > height) {
if (width > MAX_WIDTH) {
height *= MAX_WIDTH / width;
width = MAX_WIDTH;
}
} else {
if (height > MAX_HEIGHT) {
width *= MAX_HEIGHT / height;
height = MAX_HEIGHT;
}
}
canvas.width = width;
canvas.height = height;
ctx.drawImage(img, 0, 0, width, height);
// 转换为压缩后的图片
canvas.toBlob(function(blob) {
callback(blob);
}, 'image/jpeg', quality);
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
}
// 使用示例
const fileInput = document.querySelector('input[type="file"]');
fileInput.addEventListener('change', function(e) {
const file = e.target.files[0];
compressImage(file, 0.7, function(blob) {
// 处理压缩后的图片,如上传或显示
console.log(blob);
});
});
2. 利用第三方库(推荐)
除了原生JavaScript和HTML5外,还有许多优秀的第三方库可以帮助我们更方便地实现图片压缩,如image-magic-adapter、compressorjs
、pica
等。这些库通常提供了更丰富的配置选项和更好的兼容性支持。
特别推荐的库: image-magic-adapter
这个三方库是国内开发者提供的,他集成许多图片处理能力,包括“图片压缩”、“图片格式转换”、“图片加水印”等等,非常方便,而且这个库还有官网也可以直接使用这些功能.
库官网:http://www.npmjs.com/package/ima…
在线图片处理工具官网:luckycola.com.cn/public/dist…
使用 image-magic-adapter示例:
// 引入image-magic-adapter
import ImageMagicAdapter from 'image-magic-adapter';
let ImageCompressorCls = ImageMagicAdapter.ImageCompressorCls;
const imageCompressor = new ImageCompressorCls(); // 默认压缩质量
// -----------------------------------------图片压缩-----------------------------------------
document.getElementById('quality').addEventListener('input', () => {
const quality = parseFloat(document.getElementById('quality').value);
imageCompressor.quality = 1 - quality; // 更新压缩质量
console.log('更新后的压缩质量:', imageCompressor.quality);
});
document.getElementById('compress').addEventListener('click', async () => {
const fileInput = document.getElementById('upload');
if (!fileInput.files.length) {
alert('请上传图片');
return;
}
const files = Array.from(fileInput.files);
const progress = document.getElementById('progress');
const outputContainer = document.getElementById('outputContainer');
const downloadButton = document.getElementById('download');
const progressText = document.getElementById('progressText');
outputContainer.innerHTML = '';
downloadButton.style.display = 'none';
progress.style.display = 'block';
progress.value = 0;
progressText.innerText = '';
// compressImages参数说明:
// 第一个参数: files:需要压缩的文件数组
// 第二个参数: callback:压缩完成后的回调函数
// 第三个参数: 若是压缩png/bmp格式,输出是否保留png/bmp格式,默认为true(建议设置为false)
// 注意:如果 第三个参数设置true压缩png/bmp格式后的输出的文件为原格式(png/bmp)且压缩效果不佳,就需要依赖设置scaleFactor来调整压缩比例(0-1);如果设置为false,输出为image/jpeg格式且压缩效果更好。
// 设置caleFactor为0-1,值越大,压缩比例越小,值越小,压缩比例越大(本质是改变图片的尺寸),例: imageCompressor.scaleFactor = 0.5;
await imageCompressor.compressImages(files, (completed, total) => {
const outputImg = document.createElement('img');
outputImg.src = imageCompressor.compressedImages[completed - 1];
outputContainer.appendChild(outputImg);
progress.value = (completed / total) * 100;
progressText.innerText = `已完成文件数: ${completed} / 总文件数: ${total}`;
if (completed === total) {
downloadButton.style.display = 'inline-block';
}
}, false);
downloadButton.onclick = () => {
if (imageCompressor.compressedImages.length > 0) {
imageCompressor.downloadZip(imageCompressor.compressedImages);
}
};
});
<h4>图片压缩Demoh4>
<input type="file" id="upload" accept="image/*" multiple />
<br>
<label for="quality">压缩比率:(比率越大压缩越大,图片质量越低)label>
<input type="range" id="quality" min="0" max="0.9" step="0.1" required value="0.5"/>
<br>
<button id="compress">压缩图片button>
<br>
<progress id="progress" value="0" max="100" style="display: none;">progress>
<br />
<span id="progressText">span>
<br>
<div id="outputContainer">div>
<br>
<button id="download" style="display: none;">下载已压缩图片button>
3. gif图片压缩(拓展)
GIF(Graphics Interchange Format)图片是一种广泛使用的图像文件格式,特别适合用于显示索引颜色图像(如简单的图形、图标和某些类型的图片),同时也支持动画。尽管GIF图片本身可以具有压缩特性,但在前端和后端进行压缩处理时,存在几个关键考虑因素,这些因素可能导致在前端直接压缩GIF不如在后端处理更为有效或合理。
下面提供一个厚后端通过node实现gif压缩的方案:
1、下载imagemin、imagemin-gifsicle和image-size库
2、注意依赖的库的版本,不然可能会报错
"image-size": "^1.1.1",
"imagemin": "7.0.1",
"imagemin-gifsicle": "^7.0.0",
node压缩gif实现如下:
const imagemin = require('imagemin');
const imageminGifsicle = require('imagemin-gifsicle');
const sizeOf = require('image-size');
// 压缩 GIF colors[0-256]
const compressGifImgFn = async (inputBase64, colors = 200, successFn = () => {}, failFn = () => {}) => {
try {
if (inputBase64.length <= 10) {
failFn && failFn('inputBase64 无效')
return;
}
// 获取输入 GIF 的尺寸
const originalSize = getBase64Size(inputBase64);
console.log('Original Size:', originalSize);
// 转换 Base64 为 Buffer
const inputBuffer = base64ToBuffer(inputBase64);
const outputBuffer = await imagemin.buffer(inputBuffer, {
plugins: [
imageminGifsicle({
// interlaced的作用 是,是否对 GIF 进行隔行扫描
interlaced: true,
// optimizationLevel的作用是,设置压缩的质量,0-3
optimizationLevel: 3,
// // progressive的作用是,是否对 GIF 进行渐进式压缩
// progressive: true,
// // palette的作用是,是否对 GIF 进行调色板优化
// palette: true,
// // colorspace的作用是,是否对 GIF 进行色彩空间转换
// colorspace: true,
colors
})
]
});
// 转换压缩后的 Buffer 为 Base64
const outputBase64 = bufferToBase64(outputBuffer);
// 获取压缩后 GIF 的尺寸
const compressedSize = getBase64Size(outputBase64);
console.log('Compressed Size:', compressedSize);
// 输出压缩后的 Base64 GIF
// console.log(outputBase64);
let gifCompressRes = {
outputBase64,
compressedSize,
originalSize
}
successFn && successFn(gifCompressRes);
} catch (error) {
console.error('Error compressing GIF:', error);
failFn && failFn(error)
}
};
// 将 Base64 字符串转换为 Buffer
function base64ToBuffer(base64) {
const base64Data = base64.split(',')[1]; // 如果是 data URL, 删除前缀
return Buffer.from(base64Data, 'base64');
}
// 将 Buffer 转换为 Base64 字符串
function bufferToBase64(buffer) {
return `data:image/gif;base64,${buffer.toString('base64')}`;
}
//获取base64图片大小,返回kb数字
function getBase64Size(base64url) {
try {
//把头部去掉
let str = base64url.replace('data:image/png;base64,', '');
// 找到等号,把等号也去掉
let equalIndex = str.indexOf('=');
if (str.indexOf('=') > 0) {
str = str.substring(0, equalIndex);
}
// 原来的字符流大小,单位为字节
let strLength = str.length;
// 计算后得到的文件流大小,单位为字节
let fileLength = parseInt(strLength - (strLength / 8) * 2);
// 由字节转换为kb
let size = "";
size = (fileLength / 1024).toFixed(2);
let sizeStr = size + ""; //转成字符串
let index = sizeStr.indexOf("."); //获取小数点处的索引
let dou = sizeStr.substr(index + 1, 2) //获取小数点后两位的值
if (dou == "00") { //判断后两位是否为00,如果是则删除00
return sizeStr.substring(0, index) + sizeStr.substr(index + 3, 2)
}
return size;
} catch (error) {
console.log('getBase64Size error:', error);
return 0;
}
};
注意事项
- 压缩质量与文件大小:压缩质量越高,图片质量越好,但文件大小也越大;反之亦然。需要根据实际需求调整。
- 兼容性:虽然现代浏览器普遍支持<canvas>和Blob等特性,但在一些老旧浏览器上可能存在问题,需要做好兼容性处理。
- 性能考虑:对于大图片或高频率的图片处理,前端压缩可能会占用较多CPU资源,影响页面性能。
来源:juejin.cn/post/7409869765176475686
vue3连接mqtt
什么是MQTT?
MQTT(Message Queuing Telemetry Transport)是一种轻量级的、基于发布/订阅模式的通信协议,通常用于连接物联网设备和应用程序之间的通信。它最初由IBM开发,现在由OASIS(Organization for the Advancement of Structured Information Standards)进行标准化。
MQTT的工作原理很简单:它采用发布/订阅模式,其中设备(称为客户端)可以发布消息到特定的主题(topics),而其他设备可以订阅这些主题以接收消息。这种模式使得通信非常灵活,因为发送者和接收者之间的耦合度很低。MQTT还支持负载消息(payload message)的传输,这意味着可以发送各种类型的数据,如传感器读数、控制指令等。
MQTT的轻量级设计使其在网络带宽和资源受限的环境中表现出色,因此在物联网应用中得到了广泛应用。它可以在低带宽、不稳定的网络环境下可靠地运行,同时保持较低的能耗。MQTT也有许多开源实现和客户端库,使得它在各种平台上都能方便地使用。
MQTT在项目的运用
官网使用指南:docs.emqx.com/zh/cloud/la…
(1)安装MQTT
npm install mqtt
(2)本项目Vite和Vue版本(包括但不限于)
"vue":"^3.3.11"
"vite": "^5.0.10"
(3)引入MQTT文件
import mqtt from "mqtt";
(4)MQTT的具体使用
本文将使用 EMQ X 提供的 免费公共 MQTT 服务器,该服务基于 EMQ X 的 MQTT 物联网云平台 创建。服务器接入信息如下:
Broker: broker.emqx.io
Port: 8083
export const connectMqtt = ({host, name, pwd, theme},onMessageArrived) => {
let client = null
let url = `${host}/mqtt`
let options={
username: name, // 用户名字
password: pwd, // 密码
// clientId: 'clientId'
}
try {
client = mqtt.connect(url, options)
}catch (error) {
console.log('mqtt.connect error', error)
}
// 订阅主题
client.subscribe(theme, (topic) => {
console.log(topic); // 此处打印出订阅的主题名称
});
// 推送消息
// client.publish(theme, JSON.stringify({one: '1', two: '2'}));
//接受消息
client.on("message", (topic, data) => {
// 这里有可能拿到的数据格式是Uint8Array格式,所以可以直接用toString转成字符串
let dataArr = data.toString();
console.log('mqtt收到的消息', dataArr);
onMessageArrived(data)
});
// 重连
client.on("reconnect", (error) => {
console.log("正在重连mqtt:", error);
});
// 错误回调
client.on("error", (error) => {
console.log("MQTT连接发生错误已关闭");
});
}
参考链接:
来源:juejin.cn/post/7410017851626913833