BOE(京东方)领先科技赋能体育产业全面向新 以击剑、电竞、健身三大应用场景诠释未来健康运动新生活
巴黎全球体育盛会虽已闭幕,但世界范围内的运动热潮并未消退。9月12日,在北京恒通国际商务园(UBP)的之所ICC,BOE(京东方)开启了以“屏实力 FUN肆热爱”为主题的“科技赋能体育”互动体验活动。活动现场,BOE(京东方)携手海信、创维、联想、AGON、ROG、一加、红魔等众多全球一线合作伙伴,全面展示了围绕击剑、电竞、健身三大应用场景的尖端科技产品,并打造了“显示视界”、“电子竞技”、“运动健身”三大互动体验区。中国国家击剑队女子重剑运动员余思涵、北京JDG英特尔战队分析师Vusso以及众多在京媒体出席了开幕仪式,并共同探讨“前沿科技赋能体育新生态”的深耕布局与应用趋势。据悉,该活动将全面向公众开放至9月14日,大众将通过现场沉浸式互动体验全方位感受创新科技赋能体育向新的独特魅力,更深度诠释了未来健康运动新生活的全新范式。
BOE(京东方)“科技赋能体育”互动体验活动现场
BOE(京东方)副总裁、首席品牌官司达在现场发言中表示,体育产业是BOE(京东方)“屏之物联”战略赋能应用场景的重要发力方向之一。在当前人工智能等新技术引领的智能化浪潮下,BOE(京东方)的创新科技正在体育产业中发挥着日益重要的作用。从2016年里约全球体育赛事的首次8K超高清实况转播,到2021年携手中国击剑队亮剑东京;到2022年冰雪盛会开闭幕式上的“雪花”绽放、再到2023年助力《梦三国2》电竞项目在杭州赛场奋勇夺金、2024年助力中国国家击剑队亮剑巴黎,BOE(京东方)正在通过全方位的科技赋能推动体育产业向智能化、科技化全面迈进。
BOE(京东方)副总裁、首席品牌官司达现场发言
科技赋能击剑,打造沉浸式赛训观赛新视界
在“显示视界”展区,由BOE(京东方)ADS Pro赋能的创维75英寸A7E Pro壁纸电视可呈现110% DCI-P3电影级超广色域,带来极致绚丽的画面表现力和丰富细腻的层次变化,高达288Hz的极速高刷新率让每一次出剑瞬间都行云流水般流畅丝滑,畅享全新沉浸式大屏观赛视觉盛宴。海信75英寸E8N Ultra ULED超画质电视同样由ADS Pro赋能,5800nits超高亮度配合288Hz超高刷新率呈现清晰锐利、逼真生动、流畅灵动的专业级画质表现,搭载击剑互动游戏让现场观众惊叹于大屏操作的每一个精彩瞬间。不仅如此,现场BOE(京东方)还带来了由高端柔性OLED显示技术解决方案f-OLED赋能的全球一线终端品牌的内折、上下翻折高端旗舰手机,全面解锁未来体育观赛的无限想象空间。
BOE(京东方)ADS Pro赋能创维75英寸A7E Pro壁纸电视
中国国家击剑队女子重剑运动员余思涵、北京JDG英特尔战队分析师Vusso现场体验
作为中国国家击剑队首席战略合作伙伴,多年来,BOE(京东方)的智慧显示、智慧物联、数字健康等物联网创新解决方案已覆盖击剑运动员的训练备战、战术分析、体能监测、健康管理等方方面面,全方位助力中国击剑队征战2018年亚洲体育盛会、2021东京全球体育盛会、2023年杭州亚洲体育盛会、2024年巴黎全球体育盛会等众多荣耀巅峰时刻,以硬核实力为体育注入科技力量。
科技赋能电竞,打造沉浸式竞技极致新体验
在“电子竞技”展区,BOE(京东方)联合AGON重磅打造的电竞显示终端,在ADS Pro加持下可实现高达520Hz的极致超高刷新率,配合千分之一秒的极限响应速度,精准还原职业电竞选手每一帧精妙的操作细节;由ADS Pro强势赋能的联想拯救者R9000P以100%DCI-P3超广色域及240Hz超高刷新率的领先性能,让电竞玩家尽情畅享大圣战斗的极致竞技体验;ROG 绝神27 XG27UCS在ADS Pro加持下可实现接近180°的超广视角,玩家无论正面观看还是侧面观看,都能获得原生态焕彩完美画质。此外,搭载BOE(京东方)高端柔性OLED技术的红魔、一加等多款游戏手机凭借高清、高刷、低蓝光护眼等领先优势,强势助力玩家在手游赛场尽情发挥,克敌制胜。
520Hz超高刷新率电竞显示终端
BOE(京东方)科技赋能专业电竞显示产品
作为电竞领域的科技引领者,BOE(京东方)已携手联想、戴尔、华硕、AOC等全球一线品牌推出众多超高刷、超高清、超高画质的专业电竞显示产品,目前,BOE(京东方)在电竞笔记本、显示器、手机等专业电竞显示领域均已处于全球领先地位。同时,BOE(京东方)还携手京东,与众多全球一线品牌联合成立Best of Esports电竞高阶联盟,并联合虎牙直播及联盟成员共同举办BOE无畏杯《无畏契约》挑战赛,推动构筑覆盖硬件、终端、内容、市场全链路的电竞生态,为我国电竞产业向新发展注入创新动力。
科技赋能健身,打造沉浸式健康生活新空间
在“运动健身”展区,BOE(京东方)更将显示、VR、传感等前沿技术与运动健身场景创新融合,引领全新的健康生活新潮流。展区内,动感单车配备专业VR头显设备,通过高清、高画质的VR显示技术实现虚拟与现实的深度交互,以别开生面的创新骑行模式引领健身运动新风潮;极具科技感的健身“魔镜”融合多种智能化功能于一体,用户可一边观看教程一边同步对镜矫正姿态,让健身更加智能化、可视化、趣味化;此外,现场展出的可穿戴智能健康手表搭载专业健康监测软件系统,为用户带来全方位的健康管理贴心呵护。
中国国家击剑队运动员余思涵现场体验
近年来,BOE(京东方)深入布局健康领域,推出的数字人体终端、智能体征监测系统、远望学习屏等一系列创新科技产品为大众健康生活带来全新体验。同时,基于多年在“医工融合”高潜方向的前瞻布局与深厚积淀,BOE(京东方)还将显示、传感、物联网、人工智能等技术与前沿医学融合创新,聚焦AI+医疗、数字医院、智慧医养社区等全新技术方向及场景形态,为未来医疗健康产业带来深远影响。
当前,随着物联网、人工智能、大数据等前沿技术引领数字化、智能化浪潮奔涌而来,BOE(京东方)的创新科技还将进一步深度融入体育竞技与运动健康等各大应用场景,携手更多顶级体育赛事及产业链合作伙伴,以顶尖科技力量描绘体育产业智能化高质量发展新图景!
收起阅读 »桌面端Electron基础配置
机缘
机缘巧合之下获取到一个桌面端开发的任务。
为了最快的上手速度,最低的开发成本,选择了electron。
介绍
Electron是一个使用 JavaScript、HTML 和 CSS 构建桌面应用程序的框架。 嵌入 Chromium 和 Node.js 到 二进制的 Electron 允许您保持一个 JavaScript 代码代码库并创建 在Windows上运行的跨平台应用 macOS和Linux——不需要本地开发 经验。
主要结构
electron主要有一个主进程和一个或者多个渲染进程组成,方便的脚手架项目有
electron-vite
安装方式
npm i electron-vite -D
electron-vite分为3层结构
main // electron主进程
preload // electron预加载进程 node
renderer // electron渲染进程 vue
创建项目
npm create @quick-start/electron
项目创建完成启动之后
会在目录中生成一个out目录
out目录中会生成项目文件代码,在electron-vite中使用ESmodel来加载文件,启动的时候会被全部打包到out目录中合并在一起。所以一些使用CommonJs的node代码复制进来需要做些修改。npm安装的依赖依然可以使用CommonJs的方式引入。
node的引入
在前面的推荐的几篇文章中都有详细的讲解,无需多言。electron是以chrom+node,所以node的加入也非常的简单。
nodeIntegration: true,
main主进程中的简单配置
preload目录下引入node代码,留一个口子在min主进程中调用。
配置数据库
以sequelize为例
npm install --save sequelize
npm install --save sqlite3
做本地应用使用推荐sqlite3,使用本地数据库,当然了用其他的数据也没问题,用法和node中一样。需要注意的是C++代码编译的问题,可能会存在兼容性问题,如果一直尝试还是报错就换版本吧。electron-vite新版本问题不大,遇到过老版本一直编译失败的问题
测试能让用版本
- "electron": "^25.6.0",
- "electron-vite": "^1.0.27",
- "sequelize": "^6.33.0",
node-gyp vscode 这些安装环境网上找找也很多就不多说了。
import { Sequelize } from 'sequelize'
import log from '../config/log/log'
const path = require('path')
let documentsPath
if (process.env['ELECTRON_RENDERER_URL']) {
documentsPath = './out/config/sqlite/sqlite.db'
} else {
documentsPath = path.join(process.env.USERPROFILE, 'Documents') + '\\sqlite\\sqlite.db'
}
console.log('documentsPath-------------****-----------', documentsPath)
export const seq = new Sequelize({
dialect: 'sqlite',
storage: documentsPath
})
seq
.authenticate()
.then(() => {
log.info('数据库连接成功')
})
.catch((err) => {
log.error('数据库连接失败' + err)
})
终端乱码问题
"dev:win": "chcp 65001 && electron-vite dev",
chcp 65001只在win环境下添加
electron多页签
electron日志
import logger from 'electron-log'
logger.transports.file.level = 'debug'
logger.transports.file.maxSize = 30 * 1024 * 1024 // 最大不超过10M
logger.transports.file.format = '[{y}-{m}-{d} {h}:{i}:{s}.{ms}] [{level}]{scope} {text}' // 设置文件内容格式
var dayjs = require('dayjs')
const date = dayjs().format('YYYY-MM-DD') // 格式化日期为 yyyy-mm-dd
logger.transports.file.fileName = date + '.log' // 创建文件名格式为 '时间.log' (2023-02-01.log)
// 可以将文件放置到指定文件夹中,例如放到安装包文件夹中
const path = require('path')
let logsPath
if (process.env['ELECTRON_RENDERER_URL']) {
logsPath = './out/config/logs/' + date + '.log'
} else {
logsPath = path.join(process.env.USERPROFILE, 'Documents') + '\\logs\\' + date + '.log'
}
console.log('logsPath-------------****-----------', logsPath) // 获取到安装目录的文件夹名称
// 指定日志文件夹位置
logger.transports.file.resolvePath = () => logsPath
// 有六个日志级别error, warn, info, verbose, debug, silly。默认是silly
export default {
info(param) {
logger.info(param)
},
warn(param) {
logger.warn(param)
},
error(param) {
logger.error(param)
},
debug(param) {
logger.debug(param)
},
verbose(param) {
logger.verbose(param)
},
silly(param) {
logger.silly(param)
}
}
对应用做好日志维护是一个很重要的事情
主进程中也可以在main文件下监听
app.on('activate', function () {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
// 渲染进程崩溃
app.on('renderer-process-crashed', (event, webContents, killed) => {
log.error(
`APP-ERROR:renderer-process-crashed; event: ${JSON.stringify(event)}; webContents:${JSON.stringify(
webContents
)}; killed:${JSON.stringify(killed)}`
)
})
// GPU进程崩溃
app.on('gpu-process-crashed', (event, killed) => {
log.error(`APP-ERROR:gpu-process-crashed; event: ${JSON.stringify(event)}; killed: ${JSON.stringify(killed)}`)
})
// 渲染进程结束
app.on('render-process-gone', async (event, webContents, details) => {
log.error(
`APP-ERROR:render-process-gone; event: ${JSON.stringify(event)}; webContents:${JSON.stringify(
webContents
)}; details:${JSON.stringify(details)}`
)
})
// 子进程结束
app.on('child-process-gone', async (event, details) => {
log.error(`APP-ERROR:child-process-gone; event: ${JSON.stringify(event)}; details:${JSON.stringify(details)}`)
})
应用更新
在Electron中实现自动更新,需要使用electron-updater
npm install electron-updater --save
需要知道服务器地址,单版本号有可更新内容的时候可以通过事件监听控制更新功能
provider: generic
url: 'http://localhost:7070/urfiles'
updaterCacheDirName: 111-updater
import { autoUpdater } from 'electron-updater'
import log from '../config/log/log'
export const autoUpdateInit = (mainWindow) => {
let result = {
message: '',
result: {}
}
autoUpdater.setFeedURL('http://localhost:50080/latest.yml')
//设置自动下载
autoUpdater.autoDownload = false
autoUpdater.autoInstallOnAppQuit = false
// 监听error
autoUpdater.on('error', function (error) {
log.info('检测更新失败' + error)
result.message = '检测更新失败'
result.result = error
mainWindow.webContents.send('update', JSON.stringify(result))
})
// 检测开始
autoUpdater.on('checking-for-update', function () {
result.message = '检测更新触发'
result.result = ''
// mainWindow.webContents.send('update', JSON.stringify(result))
log.info(`检测更新触发`)
})
// 更新可用
autoUpdater.on('update-available', (info) => {
result.message = '有新版本可更新'
result.result = info
mainWindow.webContents.send('update', JSON.stringify(result))
log.info(`有新版本可更新${JSON.stringify(info)}${info}`)
})
// 更新不可用
autoUpdater.on('update-not-available', function (info) {
result.message = '检测更新不可用'
result.result = info
mainWindow.webContents.send('update', JSON.stringify(result))
log.info(`检测更新不可用${info}`)
})
// 更新下载进度事件
autoUpdater.on('download-progress', function (progress) {
result.message = '检测更新当前下载进度'
result.result = progress
mainWindow.webContents.send('update', JSON.stringify(result))
log.info(`检测更新当前下载进度${JSON.stringify(progress)}${progress}`)
})
// 更新下载完毕
autoUpdater.on('update-downloaded', function () {
//下载完毕,通知应用层 UI
result.message = '检测更新当前下载完毕'
result.result = {}
mainWindow.webContents.send('update', result)
autoUpdater.quitAndInstall()
log.info('检测更新当前下载完毕,开始安装')
})
}
export const updateApp = (ctx) => {
let message
if (ctx.params == 'inspect') {
console.log('检测是否有新版本')
message = '检测是否有新版本'
autoUpdater.checkForUpdates() // 开始检查是否有更新
}
if (ctx.params == 'update') {
message = '开始更新'
autoUpdater.downloadUpdate() // 开始下载更新
}
return (ctx.body = {
code: 200,
message,
result: {
currentVersion: 0
}
})
}
dev下想测试更新功能,可以在主进程main文件中添加
Object.defineProperty(app, 'isPackaged', {
get() {
return true
}
})
接口封装
eletron中可以像node一样走http的形式编写接口,但是更推荐用IPC走内存直接进行主进程和渲染进程之间的通信
前端
import { ElMessage } from 'element-plus'
import router from '../router/index'
export const getApi = (url: string, params: object) => {
return new Promise(async (resolve, rej) => {
try {
console.log('-------------------url+params', url, params)
// 如果有token的话
let token = sessionStorage.getItem('token')
// 走ipc
if (window.electron) {
const res = await window.electron.ipcRenderer.invoke('getApi', JSON.stringify({ url, params, token }))
console.log('res', res)
if (res?.code == 200) {
return resolve(res.result)
} else {
// token校验不通过退出登录
if (res?.error == 10002 || res?.error == 10002) {
router.push({ name: 'loginPage' })
}
// 添加接口错误的处理
ElMessage.error(res?.message || res || '未知错误')
rej(res)
}
} else {
// 不走ipc
}
} catch (err) {
console.error(url + '接口请求错误----------', err)
rej(err)
}
})
}
后端
ipcMain.handle('getApi', async (event, args) => {
const { url, params, token } = JSON.parse(args)
//
})
electron官方文档中提供的IPC通信的API有好几个,每个使用的场景不一样,根据情况来选择
node中使用的是esmodel和一般的node项目写法上还有些区别,得适应一下。
容易找到的都是渲染进程发消息,也就是vue发消息给node,但是node发消息给vue没有写
这时候就需要使用webContents方法来实现
this.mainWindow.webContents.send('receive-tcp', JSON.stringify({ code: key, data: res.data }))
使用webContents的时候在vue中一样是通过事件监听‘receive-tcp’事件来获取
本地图片读取
// node中IO操作是异步所以得订阅一下
const subscribeImage = new Promise((res, rej) => {
// 读取图片文件进行压缩
sharp(imagePath)
.webp({ quality: 80 })
.toBuffer((err, buffer) => {
if (err) {
console.error('读取本地图片失败Error converting image to buffer:', err)
rej(
(ctx.body = {
error: 10003,
message: '本地图片读取失败'
})
)
} else {
log.info(`读取本地图片成功:${ctx.params}`)
res({
code: 200,
msg: '读取本地图片成功:',
result: buffer.toString('base64')
})
}
})
})
TCP
既然写了桌面端,那数据交互的方式可能就不局限于http,也会有WS,TCP,等等其他的通信协议。
node中提供了Tcp模块,net
const net = require('net')
const server = net.createServer()
server.on('listening', function () {
//获取地址信息
let addr = server.address()
tcpInfo.TcpAddress = `ip:${addr.port}`
log.info(`TCP服务启动成功---------- ip:${addr.port}`)
})
//设置出错时的回调函数
server.on('error', function (err) {
if (err.code === 'EADDRINUSE') {
console.log('地址正被使用,重试中...')
tcpProt++
setTimeout(() => {
server.close()
server.listen(tcpProt, 'ip')
}, 1000)
} else {
console.error('服务器异常:', err)
}
})
TCP链接成功获取到数据之后在data事件中,就可以使用webContents方法来主动传递消息给渲染进程
也得对Tcp数据包进行解析,一般都是和外部系统协商沟通的数据格式。一般是十六进制或者是二进制数据,需要对数据进行解析,切割,缓存。
使用 Bufferdata = Buffer.concat([overageBuffer, data])
对数据进行处理
根据数据的长度对数据进行切割,判断数据的完整性质,对数据进行封包和拆包
粘包处理网上都有
处理完.toString()一下 over
socket.on('data', async (data) => {
...
let buffer = data.slice(0, packageLength) // 取出整个数据包
data = data.slice(packageLength) // 删除已经取出的数据包
// 数据处理
let key = buffer.slice(4, 8).reverse().toString('hex')
console.log('data', key, buffer)
let res = await isFunction[key](buffer)
this.mainWindow.webContents.send('receive-tcpData', JSON.stringify({ code: key, data: res.data }))
})
// 获取包长度的方法
getPackageLen(buffer) {
let bufferCopy = Buffer.alloc(12)
buffer.copy(bufferCopy, 0, 0, 12)
let bufferSize = bufferCopy.slice(8, this.headSize).reverse().readInt32BE(0)
console.log('bufferSize', bufferSize, bufferSize + this.headSize, buffer.length)
if (bufferSize > buffer.length - this.headSize) {
return -1
}
if (buffer.length >= bufferSize + this.headSize) {
return bufferSize + this.headSize // 返回实际长度 = 消息头长度 + 消息体长度
}
}
打完收工
来源:juejin.cn/post/7338265878289301567
日历表格的制作,我竟然选择了这样子来实现...
前言
最近有个日历表格的需求,具体效果如下所示,鼠标经过时表格还有一个十字高亮的效果,在拿到这个设计图的时候,就在想应该用什么来实现,由于我所在的项目用的是vue3 + element
,所以我第一时间想到的就是饿了么里面的表格组件,但是经过一番调式之后,发现在饿了么表格的基础上想要调整我要的样式效果太复杂太麻烦了,所以我决定用原生的div循环来实现!
第一步 初步渲染表格
由于表格的表头是固定的,我们可以先渲染出来
<script setup lang="ts">
const tableFileds = Array.from({ length: 31 }, (_, i) =>
String(i + 1).padStart(2, '0')
)
</script>
<template>
<div class="table">
<div class="tabble-box">
<div class="table-node">
<div class="table-item table-header">
<div class="table-item-content diagonal-cell">
<span class="top-content">日</span>
<span class="bottom-content">月</span>
</div>
<div
class="table-item-content"
v-for="(item, _index) in tableFileds"
:key="item"
:style="{
background: '#EFF5FF'
}"
>
{{ item }}
</div>
</div>
</div>
</div>
</div>
</template>
<style lang="less" scoped>
.table {
flex: 1;
display: flex;
flex-direction: column;
background-color: #fff;
padding: 1.8519vh 0.83vw 2.1296vh 1.09vw;
.tabble-box {
display: flex;
flex: 1;
box-sizing: border-box;
overflow: hidden;
color: #666666;
.table-node {
display: flex;
flex-direction: column;
flex: 1;
.table-header {
.table-item-content {
background-color: #eff5ff;
}
.diagonal-cell {
position: relative; /* 使伪元素相对于此单元格定位 */
padding-bottom: 8px; /* 为对角线下方留出空间以显示内容 */
width: 4.64vw !important;
&::before {
content: ''; /* 必须有内容才能显示伪元素 */
position: absolute;
top: 0;
left: 1px;
width: 5.16vw;
right: 0;
height: 1px; /* 对角线的高度 */
background-color: #e8e8e8; /* 对角线的颜色,可自定义 */
transform-origin: top left;
transform: rotate(30.5deg); /* 斜切角度,可微调 */
}
.top-content {
position: absolute;
top: 0.2778vh;
left: 2.67vw;
font-size: 0.83vw;
}
.bottom-content {
position: absolute;
top: 2.2222vh;
left: 0.83vw;
font-size: 0.83vw;
}
}
}
.table-item {
display: flex;
.table-item-content:first-child {
width: 4.64vw;
padding-top: 1.9444vh;
padding-bottom: 1.2037vh;
}
.table-item-content {
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
// width: calc((100% - 9.53vw) / 15);
padding: 0.1vw;
text-align: center;
font-size: 0.78vw;
border-top: 0.05vw solid #eeeeee;
border-right: 0.05vw solid #eeeeee;
width: 2.48vw;
// flex-grow: 1
}
}
.table-header {
.table-item-content {
padding-top: 1.9444vh;
padding-bottom: 1.5741vh;
}
}
}
}
}
看一下页面效果:
表格的表头初步完成!
第二步 确认接口返回的数据格式
这是接口返回的格式数据 就例如第一个对象代表着3月9号有数据
{
"3": {
"9": 1
},
"4": {
"12": 2
},
"5": {
"11": 1,
"12": 2,
"21": 1
},
"6": {
"6": 5,
"8": 1,
"9": 2,
"10": 1,
"12": 2,
"17": 1,
"20": 1
},
"7": {
"1": 8,
"4": 1,
"7": 1,
"6": 1,
"13": 1,
"22": 1,
"25": 1,
"26": 1,
"27": 1,
"29": 6,
"30": 1
},
"8": {
"1": 1,
"2": 2,
"7": 1,
"20": 1,
"24": 1,
"27": 1,
"31": 1
},
"9": {
"15": 1,
"17": 9,
"21": 2
},
"10": {
"23": 1
}
}
接着我们需要对返回的数据做处理,由于表格的表头已经渲染出来,这意味着表格的每一列都有了,接下来我们就需要渲染表格的每一行与其对应就可以了.十二个月份我们需要十二行,同时每一行的第一个单元格表示的是月份,那我们可以定义一个月份的数据,然后再根据接口数据做处理,返回一个带有对应月份数据的数组.
代码如下:
const tableDataList = [
'一月',
'二月',
'三月',
'四月',
'五月',
'六月',
'七月',
'八月',
'九月',
'十月',
'十一月',
'十二月'
]
// 把接口数据转换为对应的月份数组对象
const parseData = (data: any) => {
const parsedData = new Array(12).fill({}) // 初始化一个包含12个空对象的数组
console.log('parsedData', parsedData)
for (let month = 1; month <= 12; month++) {
// 确保每次循环创建一个新的空对象
parsedData[month - 1] = {} // 从0开始索引
if (data[month]) {
Object.entries(data[month]).forEach(([day, value]) => {
parsedData[month - 1][parseInt(day)] = value
})
}
}
return parsedData
}
const tableData = ref<any[]>([])
onMounted(() => {
tableData.value = parseData(data)
console.log('tableData.value', tableData.value)
})
我们可以看一下控制台,此时的tableData的数据格式是怎么样的
接下来就可以开始渲染表格的内容了,给有数据的单元格做个高亮,同时固定31天,所以可以先遍历出每一行31个单元格出来
<div class="table-list">
<div
class="table-item"
v-for="(item, rowIndex) in tableData"
:key="item"
>
<div
class="table-item-content"
:key="item"
:style="{
background: '#EFF5FF'
}"
>
{{ tableDataList[rowIndex] }}
</div>
<div
class="table-item-content"
:data-col-index="index"
v-for="index in 31"
:key="rowIndex"
:style="{
background: item[index] ? '#6fa7ea' : ''
}"
>
<span v-if="item[index]" style="color: #fff">
{{ item[index] }}
</span>
<span v-else>0</span>
</div>
</div>
</div>
到这里基本就完成了,还差一个鼠标经过表格十字高亮的需求
我们可以给每个单元格加上鼠标的移入移出事件,移入事件函数传两个参数,一个就是行一个就是列,行可以从一开始的tableData那里拿到,列就是遍历31长度的当前项;这样子就可以拿到当前单元格的坐标,再封装一个辅助函数进行判断是否为当前单元格所在的行所在的列就可以了
高亮的时候记住判断的样式需要在之前的有数据高亮的样式的后面,这样子就不会被覆盖,可以保证有数据高亮的样式会一直存在,哪怕鼠标经过也不会被覆盖!
// 表格十字高亮
const highlightedRow = ref<any>()
const highlightedColumn = ref<any>()
const isCurrentCellHighlighted = (rowIndex: number, columnIndex: number) => {
return (
(highlightedRow.value !== null && highlightedRow.value === rowIndex) ||
(highlightedColumn.value !== null &&
highlightedColumn.value === columnIndex)
)
}
//鼠标移入
const onCellMouseOver = (rowIndex: any, columnIndex: any) => {
console.log('坐标', rowIndex, columnIndex)
highlightedRow.value = rowIndex
highlightedColumn.value = columnIndex
}
// 在鼠标移出事件(onCellMouseLeave)触发时,恢复所有单元格的原始背景色
const onCellMouseLeave = () => {
highlightedRow.value = null
highlightedColumn.value = null
}
<div
class="table-item-content"
:data-col-index="index"
v-for="index in 31"
:key="rowIndex"
:style="{
background: item[index]
? '#6fa7ea'
: isCurrentCellHighlighted(rowIndex + 1, index)
? '#EFF5FF'
: ''
}"
@mouseover="onCellMouseOver(rowIndex + 1, index)"
@mouseout="onCellMouseLeave"
>
<span v-if="item[index]" style="color: #fff">
{{ item[index] }}
</span>
<span v-else>0</span>
</div>
最终的效果就是:
以下就是完整的代码:
<script setup lang="ts">
import { onMounted, ref } from 'vue'
const tableFileds = Array.from({ length: 31 }, (_, i) =>
String(i + 1).padStart(2, '0')
)
const tableDataList = [
'一月',
'二月',
'三月',
'四月',
'五月',
'六月',
'七月',
'八月',
'九月',
'十月',
'十一月',
'十二月'
]
const parseData = (data: any) => {
const parsedData = new Array(12).fill({}) // 初始化一个包含12个空对象的数组
console.log('parsedData', parsedData)
for (let month = 1; month <= 12; month++) {
// 确保每次循环创建一个新的空对象
parsedData[month - 1] = {} // 从0开始索引
if (data[month]) {
Object.entries(data[month]).forEach(([day, value]) => {
parsedData[month - 1][parseInt(day)] = value
})
}
}
return parsedData
}
const data = {
'3': {
'9': 1
},
'4': {
'12': 2
},
'5': {
'11': 1,
'12': 2,
'21': 1
},
'6': {
'6': 5,
'8': 1,
'9': 2,
'10': 1,
'12': 2,
'17': 1,
'20': 1
},
'7': {
'1': 8,
'4': 1,
'7': 1,
'6': 1,
'13': 1,
'22': 1,
'25': 1,
'26': 1,
'27': 1,
'29': 6,
'30': 1
},
'8': {
'1': 1,
'2': 2,
'7': 1,
'20': 1,
'24': 1,
'27': 1,
'31': 1
},
'9': {
'15': 1,
'17': 9,
'21': 2
},
'10': {
'23': 1
}
}
const tableData = ref<any[]>([])
// 表格十字高亮
const highlightedRow = ref<any>()
const highlightedColumn = ref<any>()
const isCurrentCellHighlighted = (rowIndex: number, columnIndex: number) => {
return (
(highlightedRow.value !== null && highlightedRow.value === rowIndex) ||
(highlightedColumn.value !== null &&
highlightedColumn.value === columnIndex)
)
}
const onCellMouseOver = (rowIndex: any, columnIndex: any) => {
console.log('坐标', rowIndex, columnIndex)
highlightedRow.value = rowIndex
highlightedColumn.value = columnIndex
}
// 在鼠标移出事件(onCellMouseLeave)触发时,恢复所有单元格的原始背景色
const onCellMouseLeave = () => {
highlightedRow.value = null
highlightedColumn.value = null
}
onMounted(() => {
tableData.value = parseData(data)
console.log('tableData.value', tableData.value)
})
</script>
<template>
<div class="table">
<div class="tabble-box">
<div class="table-node">
<div class="table-item table-header">
<div class="table-item-content diagonal-cell">
<span class="top-content">日</span>
<span class="bottom-content">月</span>
</div>
<div
class="table-item-content"
v-for="(item, _index) in tableFileds"
:key="item"
:style="{
background: '#EFF5FF'
}"
>
{{ item }}
</div>
</div>
<div class="table-list">
<div
class="table-item"
v-for="(item, rowIndex) in tableData"
:key="item"
>
<div
class="table-item-content"
:key="item"
:style="{
background: '#EFF5FF'
}"
>
{{ tableDataList[rowIndex] }}
</div>
<div
class="table-item-content"
:data-col-index="index"
v-for="index in 31"
:key="rowIndex"
:style="{
background: item[index]
? '#6fa7ea'
: isCurrentCellHighlighted(rowIndex + 1, index)
? '#EFF5FF'
: ''
}"
@mouseover="onCellMouseOver(rowIndex + 1, index)"
@mouseout="onCellMouseLeave"
>
<span v-if="item[index]" style="color: #fff">
{{ item[index] }}
</span>
<span v-else>0</span>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<style lang="less" scoped>
.table {
flex: 1;
display: flex;
flex-direction: column;
background-color: #fff;
padding: 1.8519vh 0.83vw 2.1296vh 1.09vw;
.tabble-box {
display: flex;
flex: 1;
box-sizing: border-box;
overflow: hidden;
color: #666666;
.table-node {
display: flex;
flex-direction: column;
flex: 1;
.table-header {
.table-item-content {
background-color: #eff5ff;
}
.diagonal-cell {
position: relative; /* 使伪元素相对于此单元格定位 */
padding-bottom: 8px; /* 为对角线下方留出空间以显示内容 */
width: 4.64vw !important;
&::before {
content: ''; /* 必须有内容才能显示伪元素 */
position: absolute;
top: 0;
left: 1px;
width: 5.16vw;
right: 0;
height: 1px; /* 对角线的高度 */
background-color: #e8e8e8; /* 对角线的颜色,可自定义 */
transform-origin: top left;
transform: rotate(30.5deg); /* 斜切角度,可微调 */
}
.top-content {
position: absolute;
top: 0.2778vh;
left: 2.67vw;
font-size: 0.83vw;
}
.bottom-content {
position: absolute;
top: 2.2222vh;
left: 0.83vw;
font-size: 0.83vw;
}
}
}
.table-item {
display: flex;
.table-item-content:first-child {
width: 4.64vw;
padding-top: 1.9444vh;
padding-bottom: 1.2037vh;
}
.table-item-content {
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
// width: calc((100% - 9.53vw) / 15);
padding: 0.1vw;
text-align: center;
font-size: 0.78vw;
border-top: 0.05vw solid #eeeeee;
border-right: 0.05vw solid #eeeeee;
width: 2.48vw;
// flex-grow: 1
}
}
.table-header {
.table-item-content {
padding-top: 1.9444vh;
padding-bottom: 1.5741vh;
}
}
}
}
}
</style>
如果对你有帮助的话,欢迎点赞留言收藏🌹
来源:juejin.cn/post/7413311432971141160
贼好用!五分钟搭建一个美观且易用的导航页面!
大家好,我是 Java陈序员
。
今天,给大家介绍一个贼好用的导航网站搭建工具,只需通过几步操作,就能搭建出个性化导航网站!
关注微信公众号:【Java陈序员】,获取开源项目分享、AI副业分享、超200本经典计算机电子书籍等。
项目简介
Pintree
是一个开源项目,旨在将浏览器书签导出成导航网站。通过简单的几步操作,就可以将书签转换成一个美观且易用的导航页面。
Pintree
支持使用 GitHub Pages 进行部署,无需购买服务器、域名等资源!
因此,只要有一个 Github 账号,就能快速搭建一个导航网站。接下来我们就来部署实现下!
项目部署
步骤一:Fork 项目
1、访问 pintree
项目地址
https://github.com/Pintree-io/pintree
2、Fork
项目到自己的仓库中
步骤二:启用 Github Pages
1、打开 GitHub 账号中 Fork
的 pintree
项目
2、切换到仓库的 Settings
标签页,点击 Pages
,在 Source
下拉菜单中,选择 gh-pages
分支,然后点击 Save
3、几分钟后,静态导航网站将会在 https://yourusername.github.io/pintree
上可用
yourusername
是你的 Github 账号,如https://chenyl8848.github.io/pintree
.
这样,一个美观且易用的导航网站就搭建好了!
这时,好奇的小明就会问,要怎么个性化修改配置网站内容呢?别急,继续看步骤三。
步骤三:替换 JSON 文件自定义导航内容
1、pintree
渲染的导航网站内容是基于 json/pintree.json
文件里面的配置信息,我们可以通过修改 pintree.json
文件来自定义导航网站内容
2、打开 pintree.json
文件,并点击修改按钮进入编辑模式
3、在修改前,我们需要先了解下具体的语法规则,一个最小化的规则配置如下:
[
{
"//": "folder 表示是一个文件夹,可以配置子模块信息",
"type": "folder",
"//": "添加的时间信息",
"addDate": 1718526477999,
"//": "标题",
"title": "Java 陈序员",
"//": "子模块",
"children": [
{
"//": "link 表示是一个网站链接,最小化的配置单元",
"type": "link",
"//": "添加的时间信息",
"addDate": 1718526687700,
"//": "网站标题",
"title": "个人博客网站",
"//": "网站图标",
"icon": "https://chencoding.top:8090/_media/logo.png",
"//": "网站地址",
"url": "https://chencoding.top/"
},
"//": "依此类推",
{
"type": "folder",
"addDate": 1718526865665,
"title": "编程网站",
"children": [
{
"type": "link",
"addDate": 1718526707006,
"title": "CSDN",
"icon": "https://img-home.csdnimg.cn/images/20201124032511.png",
"url": "https://www.csdn.net/"
},
{
"type": "link",
"addDate": 1718526707006,
"title": "掘金",
"icon": "https://lf-web-assets.juejin.cn/obj/juejin-web/xitu_juejin_web/e08da34488b114bd4c665ba2fa520a31.svg",
"url": "https://juejin.cn/"
},
{
"type": "link",
"addDate": 1718526707006,
"title": "博客园",
"icon": "https://www.cnblogs.com/images/logo.svg?v=2SMrXdIvlZwVoB1akyXm38WIKuTHVqvGD0CweV-B6cY",
"url": "https://www.cnblogs.com/"
}
]
}
]
}
]
4、文件修改完后,点击 Commit changes
保存
5、过几分钟后,再访问 https://yourusername.github.io/pintree
可以看到,网站的内容变成了个性化的配置信息了。
由于浏览器有缓存的原因,如一开始没有变化,可以使用无痕模式访问或者用其他浏览器访问。
浏览器书签导航
通过前面的内容,我们知道 pintree
只需要一个 JSON
文件,就能搭建出一个导航网站。因此我们可以将浏览器中收藏的书签导出成 JSON
文件,再生成一个静态导航网站!
步骤一:导出浏览器书签
1、安装 Pintree Bookmarks Exporter
插件
安装地址:https://chromewebstore.google.com/detail/pintree-bookmarks-exporte/mjcglnkikjidokobpfdcdmcnfdicojce
2、使用插件导出浏览器书签,并保存 JSON
文件到本地
步骤二:替换 JSON 文件
将 JSON
文件替换到 Fork
项目的 json/pintree.json
文件中,保存成功后过几分钟再访问。
pintree
通过简单的配置,只需要几分钟就能快速搭建出一个导航网站,而且不用提供服务器、域名等资源,是一个非常优秀的开源项目!如果你想搭建一个静态导航网站可以去试试哈。
项目地址:https://github.com/Pintree-io/pintree
最后
推荐的开源项目已经收录到 GitHub
项目,欢迎 Star
:
https://github.com/chenyl8848/great-open-source-project
或者访问网站,进行在线浏览:
https://chencoding.top:8090/#/
大家的点赞、收藏和评论都是对作者的支持,如文章对你有帮助还请点赞转发支持下,谢谢!
来源:juejin.cn/post/7413187186132631589
flex 布局中更巧妙的布局方案!比 justify-content 和 align-items 好用多了!
在前端开发中,实现水平垂直居中一直是个热门话题。随着 CSS Flexbox 布局的普及,开发者们开始更多地使用
justify-content
和align-items
这两个属性来解决这个问题。
然而,还有一种更加简洁、灵活的方式——使用
margin: auto;
来实现居中以及更多实际场景下的特定效果。让我们一起回顾一下常见方式:justify-content
和align-items
,然后再来探讨一下使用:margin
的优势,以及如何在实际项目中使用它。
一、常见方式:justify-content
和 align-items
1.1 justify-content
(用于水平对齐)
justify-content
决定主轴(通常是水平方向)上子元素如何分配空间。常见的取值有:
flex-start
:元素排列在容器的起始位置(默认值)。flex-end
:元素排列在容器的末尾。center
:元素在容器内水平居中。space-between
:第一个元素与容器起点对齐,最后一个元素与容器终点对齐,其他元素之间均匀分布空间。space-around
:每个元素左右两侧都分配均等的空白区域(元素两边的空隙会有一半分布在两端)。space-evenly
:所有元素之间、以及与容器两端的空隙都相等。
1.2 align-items
(用于垂直对齐)
align-items
决定交叉轴(通常是垂直方向)上子元素如何对齐。常见的取值有:
stretch
:子元素在交叉轴上填满整个容器高度(默认值,前提是子元素没有设置具体的高度)。flex-start
:子元素在交叉轴的起始位置对齐。flex-end
:子元素在交叉轴的末端对齐。center
:子元素在交叉轴上垂直居中对齐。baseline
:子元素以其文本基线对齐。
1.3 flexbox
的常见用法
下面给出一些常见的 flexbox
的使用案例:
示例 : 公共样式
.container {
width: 800px;
height: 200px;
margin: 50px auto;
display: flex;
border: 1px solid black;
padding: 10px;
box-sizing: border-box;
}
.box {
width: 50px;
height: 50px;
background-color: lightblue;
text-align: center;
line-height: 50px;
border: 1px solid #333;
}
示例 1: 水平居中 + 垂直居中
<div class="container example-1">
<div class="box">1</div>
<div class="box">2</div>
<div class="box">3</div>
</div>
.example-1 {
justify-content: center;
align-items: center;
}
如上图所示,元素在水平和垂直方向都居中了。
示例 2: 水平居中 + 垂直靠顶
<div class="container example-2">
<div class="box">1</div>
<div class="box">2</div>
<div class="box">3</div>
</div>
.example-2 {
justify-content: center;
align-items: flex-start;
}
如上图所示,
justify-content: center;
使元素在水平方向居中;align-items: flex-start;
使元素垂直方向靠近顶部。
示例 3: 水平等间距 + 垂直居中
<div class="container example-3">
<div class="box">1</div>
<div class="box">2</div>
<div class="box">3</div>
</div>
.example-3 {
justify-content: space-between;
align-items: center;
}
如上图所示,
justify-content: space-between;
使元素在垂直方向居中;align-items: center;
使元素在水平方向两端对齐。
示例 4: 水平左对齐 + 垂直底部对齐
<div class="container example-4">
<div class="box">1</div>
<div class="box">2</div>
<div class="box">3</div>
</div>
.example-4 {
justify-content: flex-start;
align-items: flex-end;
}
如上图所示,
justify-content: flex-start;
使元素在水平方向居左;align-items: flex-end;
使元素在垂直方向靠底。
示例 5: 水平等间距 + 垂直拉伸
<div class="container example-5">
<div class="box">1</div>
<div class="box">2</div>
<div class="box">3</div>
</div>
.example-5 {
height: auto;
justify-content: space-evenly;
align-items: stretch;
}
如上图所示,
justify-content: space-evenly;
会使元素会在水平方向等间距;如果不设置元素的高度,使其自适应,align-items: stretch;
会使其垂直方向拉伸铺满。
1.4 思考与延伸
但你有没有想过,这些写法是否是最简洁的?能否实现我们日常开发的需求呢?有没有更优雅、更轻量的方案呢?
实际上在很多情况下这两个属性并不能够满足我们的开发需求。
比如我需要实现子元素部分集中的布局:
单纯依靠
justify-content
和align-items
,很难让几个子元素集中在一起。比如我们希望某些元素靠近并且与其他元素保持一定的间距就会比较麻烦了。
此时为了实现这种布局,通常需要结合
flex-grow
、margin
或者space-between
,甚至需要使用嵌套的flex
布局,增加了复杂性。
又或者是等宽子项的平均分布问题:
比如在导航菜单或展示商品卡片时,可能要求子项无论数量多少,都要从左向右均匀分布,并且保持等宽。
通过
justify-content: space-between
或space-around
可以部分解决这个问题,但是往往会出现无法保证元素从左向右,或者是无法等分的问题。
以及一些其他的情况,如垂直排列的固定间距、复杂的网格布局、混合布局等,justify-content
和 align-items
都无法简洁、优雅的解决问题。
二、更优雅的方式:margin
2.1 下使用 margin: auto
使元素居中
其实,Flexbox 布局下还有另一种更加简洁的方法使元素居中——直接使用 margin: auto;
。你可能会问,这怎么能居中呢?让我们先看一个例子:
<div class="box">
<div class="item"></div>
</div>
.box {
width: 200px;
height: 100px;
border: 2px solid #ccc;
display: flex; /* 启用 Flex 布局 */
margin: 100px auto;
}
.item {
background: red;
width: 50px;
height: 50px;
margin: auto; /* 自动分配外边距 */
}
在这个例子中,我们没有使用 justify-content
和 align-items
,仅通过设置 .item
元素的 margin: auto;
,就实现了水平和垂直居中。
它的工作原理是:在 Flexbox 布局中,
margin: auto;
会根据父容器的剩余空间自动调整元素的外边距,直到子元素居中。
在传统布局中,margin: auto;
主要用于水平居中对齐,不适用于垂直居中。因为普通流布局的垂直方向是由文档流控制的,不支持类似 Flexbox 中的自动调整行为。
.container {
width: 500px;
}
.element {
width: 200px;
margin: 0 auto; /* 左右外边距自动分配,实现水平居中 */
}
相比之下,在 Flexbox 布局中,margin: auto;
具有更多的灵活性,可以同时实现水平和垂直居中对齐。
它不仅可以处理水平居中,还可以在 Flexbox 布局下根据剩余空间自动调整外边距,实现完全的居中对齐。
2.2 实现更多实际开发中的布局
示例 1:实现子元素部分集中
在实际开发中,我们常遇到这样一种需求:将元素水平分布在容器内,其中某些元素需要靠近在一起,与其他元素保持一定的自适应距离。
在这种情况下使用
justify-content: space-between
是一种常见的办法,但这种方法也有一定的局限性:每个元素之间平等分配剩余空间,无法实现特定元素之间紧密靠拢。
代码实现:
<div class="container c2">
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
</div>
* {
margin: 0;
padding: 0;
}
.container {
width: 500px;
background: #eee;
margin: 50px auto;
padding: 10px;
display: flex;
}
.item {
width: 50px;
height: 50px;
border: 1px solid #333;
box-sizing: border-box;
}
.item:nth-child(odd) {
background: #046f4e;
}
.item:nth-child(even) {
background: #d53b3b;
}
.c2 .item:nth-child(2){
margin: 0 0 0 auto; /* 第二个 item 右对齐 */
}
.c2 .item:nth-child(4){
margin: 0 auto 0 0; /* 第四个 item 左对齐 */
}
在上述代码中,其实除掉一些基本样式的设置,实现了这个布局的关键代码就2行。
具体来说,
.c2 .item:nth-child(2)
的margin: 0 0 0 auto;
使得第二个.item
紧贴容器的右边缘,而.c2 .item:nth-child(4)
的margin: 0 auto 0 0;
使得第四个.item
紧贴容器的左边缘。这样就使第二个元素的左侧和第四个元素的右侧将会自适应边距间隔。
因此,我们可以使用
margin
巧妙地通过调整子元素的外边距,实现元素的部分集中和对齐布局。
示例 2:实现等宽子项的平均分布
在很多情况下,我们需要将商品卡片或其他内容等宽地分布在每一行中,使每个子项都具有相同的宽度并且平均分布,每一行都是从左到右。
这种布局通常用于网格展示或商品列表等场景,确保每个子项在视觉上统一且整齐。
在这种情况下直接使用
justify-content
和align-items
可能会出现以下问题:
- 使用
space-between
时如果最后一行的元素数量不足以填满整行,剩余的元素会分散到两侧,留出较大的空白区域,导致布局不整齐。
- 使用
space-around
时如果最后一行的元素数量不满,元素会在行中均匀分布,导致它们集中在中间,而不是靠左或对齐其他行。
大家在遇到这些情况时是不是就在考虑换用grid
布局了呢?先别急,我们其实直接通过margin
就可以直接实现的!
在这里我们可以使用 margin
的动态计算来实现等宽子项的平均分布。
代码实现:
<div class="container c3">
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
</div>
* {
margin: 0;
padding: 0;
}
.container {
width: 500px;
background: #eee;
margin: 50px auto;
padding: 10px;
display: flex;
flex-wrap: wrap;
}
.item {
width: 50px;
height: 50px;
border: 1px solid #333;
box-sizing: border-box;
}
.item:nth-child(odd) {
background: #046f4e;
}
.item:nth-child(even) {
background: #d53b3b;
}
.c3 .item {
--n: 5; /* 每行显示的子项数量 */
--item-width: 50px; /* 子项宽度 */
--space: calc(100% / var(--n) - var(--item-width)); /* 计算子项之间的间距 */
--m: calc(var(--space) / 2); /* 左右间距的一半 */
margin: 10px var(--m); /* 动态计算左右的间距 */
}
在在上述代码中,除掉基础的样式,实现了这个布局的关键代码仅仅5行。通过动态计算 margin,我们能够简单而有效地实现等宽子项的平均分布,使布局更加简洁明了。
三、总结
在前端开发中,实现各种页面布局一直是一个常见的需求。
传统的做法如使用 justify-content
和 align-items
属性已经被广泛采用,但这种方法有时可能显得不够简洁或灵活。
在适当的情况下直接使用 margin
进行布局是一种更优雅、简洁的替代方案,可以在 Flexbox 布局中有效地实现居中对齐和一些复杂的布局需求。掌握并运用这种方法,可以提高开发效率,并使布局更加优雅。快来玩起来吧!
来源:juejin.cn/post/7413222778855964706
告别繁琐的 try-catch:JavaScript 安全赋值运算符 (?= ) 来了!
你是否厌倦了代码中难以阅读和维护的冗长 try-catch
代码块?全新的 ECMAScript 安全赋值运算符 (?=
) 将彻底改变游戏规则!这一突破性的特性简化了错误处理,让你的代码更简洁、更高效。让我们深入了解 ?=
运算符如何彻底改变你的编码体验!
简化代码,轻松处理错误
告别嵌套的 try-catch
混乱
问题: 传统的 try-catch
代码块会导致代码深度嵌套,难以理解和调试。
解决方案: 使用 ?=
运算符,你可以将函数结果转换为一个元组,更优雅地处理错误。如果出现错误,你将得到 [error, null]
,如果一切正常,你将得到 [null, result]
。你的代码将会感谢你!
使用 ?=
之前:
async function fetchData() {
try {
const response = await fetch("https://api.example.com/data");
try {
const data = await response.json();
return data;
} catch (parseError) {
console.error('Failed to parse JSON:', parseError);
}
} catch (networkError) {
console.error('Network error:', networkError);
}
}
使用 ?=
之后:
async function fetchData() {
const [networkError, response] ?= await fetch("https://api.example.com/data");
if (networkError) return console.error('Network error:', networkError);
const [parseError, data] ?= await response.json();
if (parseError) return console.error('Failed to parse JSON:', parseError);
return data;
}
提升代码清晰度:保持代码线性,简洁易懂
问题: try-catch
代码块会打断代码流程,降低可读性。
解决方案: ?=
运算符使错误处理变得简单直观,保持代码线性,易于理解。
示例:
const [error, result] ?= await performAsyncTask();
if (error) handleError(error);
标准化错误处理:跨 API 保持一致性
问题: 不同的 API 通常需要不同的错误处理方法,导致代码不一致。
解决方案: ?=
运算符提供了一种统一的错误处理方式,使你的代码在各种 API 中保持一致。
提升安全性:每次都捕获所有错误
问题: 漏掉错误会导致 bug 和潜在的安全问题。
解决方案: ?=
运算符确保始终捕获错误,降低漏掉关键问题的风险。
Symbol.result
背后的奥秘
自定义错误处理变得简单
概述: 实现 Symbol.result
方法的对象可以使用 ?=
运算符定义自己的错误处理逻辑。
示例:
function customErrorHandler() {
return {
[Symbol.result]() {
return [new Error("Custom error message"), null];
},
};
}
const [error, result] ?= customErrorHandler();
轻松处理嵌套错误:平滑处理复杂场景
概述: ?=
运算符可以处理包含 Symbol.result
的嵌套对象,使复杂错误场景更容易管理。
示例:
const complexObj = {
[Symbol.result]() {
return [
null,
{ [Symbol.result]: () => [new Error("Nested error"), null] }
];
},
};
const [error, data] ?= complexObj;
与 Promise 和异步函数无缝集成
概述: ?=
运算符专门设计用于与 Promise 和 async/await
无缝协作,简化异步错误处理。
示例:
const [error, data] ?= await fetch("https://api.example.com/data");
使用 using
语句简化资源管理
概述: 将 ?=
运算符与 using
语句结合使用,可以更有效地管理资源。
示例:
await using [error, resource] ?= getResource();
优先处理错误:先处理错误,后处理数据
概述: 将错误放在 [error, data] ?=
结构的第一个位置,确保在处理数据之前先处理错误。
示例:
const [error, data] ?= someFunction();
让你的代码面向未来:简化填充
概述: 虽然无法直接填充 ?=
运算符,但你可以使用后处理器在旧环境中模拟其行为。
示例:
const [error, data] = someFunction[Symbol.result]();
汲取灵感:从 Go、Rust 和 Swift 中学习
概述: ?=
运算符借鉴了 Go、Rust 和 Swift 等语言的先进错误处理实践,这些语言以其强大的错误管理功能而闻名。
当前限制和未来方向
仍在发展: ?=
运算符仍在开发中。改进领域包括:
- 命名: 为实现
Symbol.result
的对象提供更好的术语。 - finally 代码块: 没有新的
finally
代码块语法,但传统用法仍然有效。
总结
安全赋值运算符 (?=
) 将通过使 JavaScript 错误处理更加直观和简洁来彻底改变 JavaScript 错误处理。随着该提案的不断发展,它将有望成为每个 JavaScript 开发人员工具箱中的必备工具。准备迎接更干净、更安全的代码吧!🚀
来源:juejin.cn/post/7413284830945493001
两个月写完的校园社交小程序,这是篇uniapp踩坑记录
人员搭配和两个舍友,一前两后,从开发到最终上线,耗时两个月,此篇文章记录一下整个开发流程中踩的坑......
前置准备:
- 资质核验。需要企业认证才能发布社交类的小程序。需要上传营业执照、法人信息等
- 类目选择。小程序中有个类目选择,选择自己小程序涉及的类目,但这里就比较抽象了(下文讲)
- 微信认证。300元,腾子的吃相很难看,但奈何寄人篱下
- 小程序备案。在前面流程完成之后才能进行小程序的备案
审核流程
整个审核流程给我的感觉就是跟便秘一样,交一次吐一句,交一次吐一句,然后打回来重改 这里记录一下几个需要注意的点,如果你和我一样也是做UGC类的小程序的话
- 微信审核。所有但凡涉及言论的,能展示出你的思想的、个性的,对不起,请通通接入微信审核。包括但不限于用户昵称、头像;发布的帖子内容、图片
文字审核算比较快,图片审核就有点慢,且图片审核不是很精准。为了避免等待检测而让用户在界面等待过久的问题,后面无奈自己搭了个后台管理。以致于流程就变成了:用户发点什么东西 -> 后端调用微信检测接口 -> 检测完甩到后台 -> 后台管理员做个二次审核 -> 通过后在小程序获取
- 不能使用微信icon图标。之前做微信快捷登录,想着搞个微信的icon图标好看点,结果给我审核失败的原因就是不能使用微信图标
- 请预留足够的时间和审核慢慢耗。以上讲到的还只是真正发布之前踩的坑,等所有条件都符合后,算可以真正的发布,然后提示你首次提交一般需要7个自然日,然后就真的是7*24小时
5. 第一次代码真正预上线之后,此后更新代码再发布新的包,一半就只需要1~2天
开发过程
- 文件上传。以往网页开发中涉及文件上传的业务都是
new FormData
,然后再append
必要的字段。但是,小程序中使用FormData
会报错,所以,得使用uniapp
自带的uni.uoloadFile
- 消息提示。在帖子发布成功之后等场景都会涉及到消息提示,一般涉及消息提示、页面回退,执行顺序请按
navigateBack
再uni.showToast
,如果二者调用顺序反了的话,只会页面回退,不会显示提示内容 - 分享功能。小程序的分享功能需要在
onShareAppMessage
(分享至好友)或者onShareTimeline
(分享至朋友圈)调用。这两个是和onLoad
同级的,如果你的技术选型也是Vue3的话,可以从@dcloudio/uni-app
中导入 - 消息订阅。小程序涉及到一个需求:当用户发布帖子,微信审核+后台审核都通过之后会通知用户,此时就需要进行消息订阅
先在微信公众号平台开通消息订阅模板,拿到模板ID,前端中再调用 uni.requestSubscribeMessage 传入拿到的模板ID就可以实现消息订阅
- webSocket。小程序中的树洞评论功能我们选用的是
webSocket
,小程序中我们没有使用三方库,调用的是uniapp的uni.connectSocket
,创建一个webSocket实例,然后处理对应的回调。由于用户一段时间内如果不发送消息,服务端也就没东西推送过来,webSocket自己会断掉。所以我们引入了心跳机制和断线重连机制。但是在做短信重连的时候就发现一个问题:断线重连之后
确实是会创建新的实例,心跳包也正常推送给服务端,但是就是接收不到服务端反推回来的东西,后面经过排查,是webSocket实例的onMessage
事件应当写在onOpen
中,而不是独立写到外面
独立写到外面进行处理就会出现:断线重连之后死活接不到最新的实例返回的消息
这里再次吐槽微信的内容审核机制。原先选用webSocket的原因就是看中了它的实时推送,但是接入了内容审核就变得很抽象,时而秒通过,时而得等一下,这也搞得失去了选用webSocket的意义
- 请求池。小程序中使用到了tabs组件,当tab在切换过程中,比如tabA切换至tabB,由于需要请求数据,所以页面会有短暂的白屏时间,这里采用的是请求池,在获取第一个tab的列表数据的时候,由请求池顺便把之后的tab的内容也请求回来,此后在进行tab切换时就可以避免白屏,优化用户体验
- 响应式布局。小程序在个人页模仿了小红书个人页的实现形式,也就是随着页面的滚动,页面的布局(主要是用户头像缩小并由透明度为0逐渐变为1)发生变化。一开始采用的是监听页面
scroll
事件。但是,scroll
涉及大量的计算;后面采用Intersection Observer
。但是注意,uniapp不能直接使用这个API,得调用uni.createIntersectionObserver
,二者语法差不多 - 防抖节流的使用。页面滚动加载下一页使用防抖,按钮点击进行节流,常规操作。
大概暂时先能想到这么多,后面有想到再接着补充......
后记
其实校园小程序这个题材市面上早已烂大街,说不上有任何的创新。此前决定搞这个的原因有以下几点:
- 很多东西只停留于理论,没有实操。就像webSocket的心跳和断线重连,博客教你怎样怎样,始终不如你自己去亲手实现一下,这个算主要驱动原因
- 这个程序刚好拿去参加学校的比赛,拿奖有钱doge
- ......
然后整一个项目跟下来吧,给我的感觉就是:技术重要吗?重要;但也不太重要,技术的重要性就跟我这句废话一样。因为一个东西,推广不起来,没人用,你就没动力去更新、去维护。当然,有没有人用对我们此次的开发来说还不算是必选项。
大家觉得校园社交类的程序还能集成什么功能,也欢迎提出您的宝贵意见
来源:juejin.cn/post/7412665439501844490
短信接口被爆破了,一晚上差点把公司干破产了
背景
某天夜里,你正睡着觉,与周公神游。
老板打来电话:“小李,快看一下,系统出故障了,一个小时发了200条短信,再搞下去,我要破产了..."
巴拉巴拉...
于是,你赶紧跳下床,查了一个后台日志,发送短信API接口5s发送一次
,都已经发送了500条了。在达到每日限额后,自动终止了。很明显被黑客攻击了。
500 * 0.1 * 8 = 400
一晚上约干掉了400元人民币
睡意全无,赶紧起来排查原因
故障分析
我司是做国外业务的,用的短信厂家是RingRing
, 没有阿里云那种自带的强悍的预警和封禁功能。黑客通过伪造IP地址
和手机号
然后攻破了APP的短信接口,然后顺藤摸瓜的拿到相关发布的全部应用。于是,一个晚上,单个APP的每日短信限额和全部短信限额都攻破了。
APP使用的是https双向加密,黑客也不是单纯的爆破,没有大量的验证码错误日志。我们现在都不清楚黑客是通过什么方式绕过我们系统的,或者直接攻破了验证码
可能有懂这方面的掘友,可以分享一下哈
我们先上了一个临时方案,如果10分钟内,发送短信超过30条,且手机号超过60%都是同一个国家
,我们关闭短信发送功能10分钟,并推送告警
然后抓紧时间去升级验证码,提高安全标准
验证码
文字验证码
我司最开始用的就是这种,简单易用。但是任你把噪点和线条铺满
,整的面目全非,都防不住机器的识别,这种验证码直接pass了
优点:简易,具有一定的防爆破功能
缺点:防君子不防小人,在黑客面前,GG
滑块验证码
我司对于滑块验证码有几点考虑:
- 安全有待商榷,
- 背景图片需要符合国外市场和审美,需要UI介入,增加人工成本
- 不确定是否符合国外的习惯
基于这几点考虑,我司放弃了这个方案。但平心而论,国内用滑块验证码的是最多的,原因如下:
- 用户体验好
- 防破解性更强
- 适应移动设备
- 适用性广
npm install rc-slider-captcha
import SliderCaptcha from 'rc-slider-captcha';
const Demo = () => {
return (
<SliderCaptcha
request={async () => {
return {
bgUrl: 'background image url',
puzzleUrl: 'puzzle image url'
};
}}
onVerify={async (data) => {
console.log(data);
// verify data
return Promise.resolve();
}}
/>
);
};
滑块验证码是用的最多的验证码,操作简单,基本平替了图片验证码
图形顺序验证码 & 图形匹配验证码 & 语顺验证码
我司没有采用这种方案的原因如下:
- 我们的APP是多语言,点击文字这种方案不适用
- 没有找到免费且合适的APP插件
- 时间紧,项目紧急,没有功夫就研究
总结:
安全性更强,用户量越大的网站越受青睐
难度相对更大,频繁验证会流失一些用户
reCAPTCHA v3
综上,我司使用了reCAPTCHA
理由如下:
- 集成简单
- 自带控制台,方便管理和查看
- 谷歌出品,值得信赖,且有保障
<script src="https://www.google.com/recaptcha/api.js?render=reCAPTCHA_site_key"></script>
<script>
function onClick(e) {
e.preventDefault();
grecaptcha.ready(function() {
grecaptcha.execute('reCAPTCHA_site_key', {action: 'submit'}).then(function(token) {
// Add your logic to submit to your backend server here.
});
});
}
</script>
// 返回值
{
score: 1 // 评分0 到 1。1:确认为人类,0:确认为机器人。
hostname: "localhost"
success: true,
challenge_ts: "2024-xx-xTxx:xx:xxZ"
action: "homepage"
}
紧急上线后,安全性大大增强,再也没有遭受黑客袭击了
。本以为可以睡个安稳觉了,又有其他的问题了,听我细讲
根据官方文档,建议score取0.5
, 我们根据测试的情况,降低了标准,设置为0.3。上线后,很多用户投诉安全度过低,请30分后重试
。由于我们当时的业务是出行和游乐
, APP受限后,用户生活受到了很大限制,很多用户预约了我们的产品,却用不了,导致收到了大量的投诉。更糟糕的时候,我们的评分标准0.3是写死的,只能重新发布,一来二去,3天过去了。客服被用户骂了后,天天来我们技术部骂我们。哎,想想都是泪
我们紧急发布了一版,将评分标准设置成可配置的,通过API获取
, 暂定0.1。算是勉强度过了这一关
reCAPTCHA v2
把分数调整到0.1后,我们觉得不是很安全,有爆破的风险,于是在下个版本使用了v2
使用v2,一切相对平稳,APP短信验证码风波也算平安度过了
2FA
双因素验证(Two-factor authentication,简称2FA,又名二步验证、双重验证),是保证账户安全的一道有效防线。在登录或进行敏感操作时,需要输入验证器上的动态密码(类似于银行U盾),进一步保护您的帐户免受潜在攻击者的攻击。双因素验证的动态密码生成器分为软件和硬件两种,最常用的软件有OTP Auth和谷歌验证器 (Google Authenticator)
经市场调用,客户要求,后续的APP,我们的都采用2fa方案,一人一码,安全可靠
。
实现起来也比较简单,后端使用sha1加密一串密钥,生成哈希值,用户扫码绑定,然后每次将这个验证码提交给服务器进行比对即可
每次使用都要看一下验证码,感觉有点烦
服务器和手机进行绑定,是同一把密钥,每次输入都找半天。一旦用户更换手机,就必须生成全新的密钥。
总结
参考资料
来源:juejin.cn/post/7413322738315378697
利用CSS延迟动画,打造令人惊艳的复杂动画效果!
动画在前端开发中是经常遇到的场景之一,加入动画后页面可以极大的提升用户体验。
绝大多数简单的动画场景可以直接通过CSS实现,对于一些特殊场景的动画可能会使用到JS计算实现,通过本文的学习,可以让你在一些看似需要使用JS实现的动画场景,使用纯CSS一样可以实现,并且更方便快捷。
先看一个简单的例子:一个方块的位置随着滑条滑动的位置改变
这个场景实现起来很简单,滑条值改变后,使用JS计算方块应该移动的距离,然后将方块定位到指定位置即可。代码如下:
.box {
height: 50px;
width: 50px;
background-color: aquamarine;
}
<div class="box">div>
<input type="range" min="0" max="1" step="0.01"/>
现在稍微增加一些动画效果:
- 方块在中间位置时缩放为原来的一半大小
- 方块在中间位置时变成球形
- 方块从红色变为绿色
对于大小和圆角,同样可以使用简单的JS进行计算实现,但是对于颜色变化,使用JS计算将会是一个非常复杂的过程。
先抛开动画跟随滑条运动这个要求,如果使用CSS实现上面从0-1的动画过程是一个很简单的事:
.box {
height: 50px;
width: 50px;
background-color: aquamarine;
transform: translateX(0);
animation: run 1s linear forwards;
}
@keyframes run {
0% {
transform: translateX(0) scale(1);
border-radius: 0%;
background: red;
}
50% {
transform: translateX(100px) scale(.5);
border-radius: 50%;
}
100% {
transform: translateX(200px) scale(1);
border-radius: 0%;
background: green;
}
}
利用CSS动画帮我们可以很轻松的计算出每个时间点时的状态,现在的问题就变成如何让动画停留在指定的时间点,这就需要使用到动画的两个属性:
annimation-play-state:设置动画是运行还是暂停,有两个属性值runing、paused
annimation-delay:设置动画开始时间的偏移量,如果是正值,则动画会延迟开始;如果是负值(-d),动画会立即开始,开始位置在动画(d)s时所处的位置。
有了这两个属性,现在将上面的动画停留在50%的位置
假设整个动画过程需要1s,50%的位置则需要将延迟值设置为-0.5s,这样动画就会停留在0.5s的位置。
.box {
height: 50px;
width: 50px;
background-color: aquamarine;
transform: translateX(0);
animation: run 1s -0.5s linear forwards infinite paused;
}
接下来只需要将滑条的值与动画延迟的值关联起来即可,这里可以通过CSS变量来实现:
.box {
--duration: -0.5s; // 定义延迟变量
height: 50px;
width: 50px;
background-color: aquamarine;
transform: translateX(0);
animation: run 1s var(--duration) linear forwards infinite paused;
}
@keyframes run {
0% {
transform: translateX(0) scale(1);
border-radius: 0%;
background: red;
}
50% {
transform: translateX(100px) scale(.5);
border-radius: 50%;
}
100% {
transform: translateX(200px) scale(1);
border-radius: 0%;
background: green;
}
}
应用场景
利用CSS延迟动画可以轻松实现很多交互场景,例如:跟随鼠标滚动界面发生反馈动画、根据当天时间界面从日出到日落、根据不同分值出现不同表情变化等等。
来源:juejin.cn/post/7363094767557378099
实现 height: auto 的高度过渡动画
对于一个 height
设置为 auto
的元素,当它的高度发生了不由样式引起的改变时,并不会触发 transition
过渡动画。
容器元素的高度往往是由其内容决定的,如果一个容器元素的内容高度突然发生了改变,而无法进行过渡动画,有时会显得比较生硬,比如下面的登录框组件:
那么这种非样式引起的变化如何实现过渡效果呢?可以借助 FLIP
技术。
FLIP
是什么
FLIP
是 First
,Last
,Invert
,Play
的缩写,其含义是:
First
- 获取元素变化之前的状态Last
- 获取元素变化后的最终状态Invert
- 将元素从Last
状态反转到First
状态,比如通过添加transform
属性,使得元素变化后,看起来仍像是处于First
状态一样Play
- 此时添加过渡动画,再移除Invert
效果(取消transform
),动画就会开始生效,使得元素看起来从First
过渡到了Last
需要用到的 Web API
要实现一个基本的 FLIP
过渡动画,需要使用到以下一些 Web API
:
- Resize Observer API - Web API 接口参考 | MDN (mozilla.org)
- Element.getBoundingClientRect() - Web API 接口参考 | MDN (mozilla.org)
- Window:requestAnimationFrame() 方法 - Web API 接口参考 | MDN (mozilla.org)
基本过渡效果实现
使用以上 API
,就可以初步实现一个监听元素尺寸变化,并对其应用 FLIP
动画的函数 useBoxTransition
,代码如下:
/**
*
* @param {HTMLElement} el 要实现过渡的元素 DOM
* @param {number} duration 过渡动画持续时间,单位 ms
* @returns 返回一个函数,调用后取消对过渡元素尺寸变化的监听
*/
export default function useBoxTransition(el: HTMLElement, duration: number) {
// boxSize 用于记录元素处于 First 状态时的尺寸大小
let boxSize: {
width: number
height: number
} | null = null
const elStyle = el.style // el 的 CSSStyleDeclaration 对象
const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
// 被观察的 box 发生尺寸变化时要进行的操作
// 获取当前回调调用时,box 的宽高
const borderBoxSize = entry.borderBoxSize[0]
const writtingMode = elStyle.getPropertyValue('writing-mode')
const isHorizontal =
writtingMode === 'vertical-rl' ||
writtingMode === 'vertical-lr' ||
writtingMode === 'sideways-rl' ||
writtingMode === 'sideways-lr'
? false
: true
const width = isHorizontal
? borderBoxSize.inlineSize
: borderBoxSize.blockSize
const height = isHorizontal
? borderBoxSize.blockSize
: borderBoxSize.inlineSize
// 当 box 尺寸发生变化时,使用 FLIP 动画技术产生过渡动画,使用过渡效果的是 scale 形变
// 根据 First 和 Last 计算出 Inverse 所需的 scale 大小
// box 首次被观察时会触发一次回调,此时 boxSize 为 null,scale 应为 1
const scaleX = boxSize ? boxSize.width / width : 1
const scaleY = boxSize ? boxSize.height / height : 1
// 尺寸发生变化的瞬间,要使用 scale 变形将其保持变化前的尺寸,要先将 transition 去除
elStyle.setProperty('transition', 'none')
elStyle.setProperty('transform', `scale(${scaleX}, ${scaleY})`)
// 将 scale 移除,并应用 transition 以实现过渡效果
setTimeout(() => {
elStyle.setProperty('transform', 'none')
elStyle.setProperty('transition', `transform ${duration}ms`)
})
// 记录变化后的 boxSize
boxSize = { width, height }
}
})
resizeObserver.observe(el)
const cancelBoxTransition = () => {
resizeObserver.unobserve(el)
}
return cancelBoxTransition
}
效果如下所示:
效果改进
目前已经实现了初步的过渡效果,但在一些场景下会有些瑕疵:
- 如果在过渡动画完成前,元素有了新的状态变化,则动画被打断,无法平滑过渡到新的状态
FLIP
动画过渡过程中,实际上发生变化的是transform
属性,并不影响元素在文档流中占据的位置,如果需要该元素影响周围的元素,那么周围元素无法实现平滑过渡
如下所示:
对于动画打断问题的优化思路
- 使用
Window.requestAnimationFrame()
方法在每一帧中获取元素的尺寸 - 这样做可以实时地获取到元素的尺寸,实时地更新
First
状态
对于元素在文档流中问题的优化思路
- 应用过渡的元素外可以套一个
.outer
元素,其定位为relative
,过渡元素的定位为absolute
,且居中于.outer
元素 - 当过渡元素尺寸发生变化时,通过
resizeObserver
获取其最终的尺寸,将其宽高设置给.outer
元素(实例代码运行于Vue 3
中,因此使用的是Vue
提供的ref api
将其宽高暴露出来,可以方便地监听其变化;如果在React
中则可以将设置.outer
元素宽高的方法作为参数传入useBoxTransition
中,在需要的时候调用),并给.outer
元素设置宽高的过渡效果,使其在文档流中所占的位置与过渡元素的尺寸同步 - 但是也要注意,这样做可能会引起浏览器高频率的重排,在复杂布局中慎用!
改进后的useBoxTransition
函数如下:
import throttle from 'lodash/throttle'
import { ref } from 'vue'
type BoxSize = {
width: number
height: number
}
type BoxSizeRef = globalThis.Ref<BoxSize>
/**
*
* @param {HTMLElement} el 要实现过渡的元素 DOM
* @param {number} duration 过渡动画持续时间,单位 ms
* @param {string} mode 过渡动画缓动速率,同 CSS transition-timing-function 可选值
* @returns 返回一个有两个项的元组:第一项为 keyBoxSizeRef,当元素大小发生变化时会将变化后的目标尺寸发送给 keyBoxSizeRef.value;第二项为一个函数,调用后取消对过渡元素尺寸变化的监听
*/
export default function useBoxTransition(
el: HTMLElement,
duration: number,
mode?: string
) {
let boxSizeList: BoxSize[] = [] // boxSizeList 表示对 box 的尺寸的记录数组;为什么是使用列表:因为当 box 尺寸变化的一瞬间,box 的 transform 效果无法及时移除,此时 box 的尺寸可能是非预期的,因此使用列表来记录 box 的尺寸,在必要的时候尽可能地将非预期的尺寸移除
const keyBoxSizeRef: BoxSizeRef = ref({ width: 0, height: 0 }) // keyBoxSizeRef 是暴露出去的 box 的实时目标尺寸
let isObserved = false // box 是否已经开始被观察
let frameId = 0 // 当前 animationFrame 的 id
let isTransforming = false // 当前是否处于变形过渡中
const elStyle = el.style // el 的 CSSStyleDeclaration 对象
const elComputedStyle = getComputedStyle(el) // el 的只读动态 CSSStyleDeclaration 对象
// 获取当前 boxSize 的函数
function getBoxSize() {
const rect = el.getBoundingClientRect() // el 的 DOMRect 对象
return { width: rect.width, height: rect.height }
}
// 更新 boxSizeList
function updateBoxsize(boxSize: BoxSize) {
boxSizeList.push(boxSize)
// 只保留前最新的 4 条记录
boxSizeList = boxSizeList.slice(-4)
}
// 定义 animationFrame 的回调函数,使得当 box 变形时可以更新 boxSize 记录
const animationFrameCallback = throttle(() => {
// 为避免使用了函数节流后,导致回调函数延迟触发使得 cancelAnimationFrame 失败,因此使用 isTransforming 变量控制回调函数中的操作是否执行
if (isTransforming) {
const boxSize = getBoxSize()
updateBoxsize(boxSize)
frameId = requestAnimationFrame(animationFrameCallback)
}
}, 20)
// 过渡事件的回调函数,在过渡过程中实时更新 boxSize
function onTransitionStart(e: Event) {
if (e.target !== el) return
// 变形中断的一瞬间,boxSize 的尺寸可能是非预期的,因此在变形开始时,将最新的 3 个可能是非预期的 boxSize 移除
if (boxSizeList.length > 1) {
boxSizeList = boxSizeList.slice(0,1)
}
isTransforming = true
frameId = requestAnimationFrame(animationFrameCallback)
// console.log('过渡开始')
}
function onTransitionCancel(e: Event) {
if (e.target !== el) return
isTransforming = false
cancelAnimationFrame(frameId)
// console.log('过渡中断')
}
function onTransitionEnd(e: Event) {
if (e.target !== el) return
isTransforming = false
cancelAnimationFrame(frameId)
// console.log('过渡完成')
}
el.addEventListener('transitionstart', onTransitionStart)
el.addEventListener('transitioncancel', onTransitionCancel)
el.addEventListener('transitionend', onTransitionEnd)
const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
// 被观察的 box 发生尺寸变化时要进行的操作
// 获取当前回调调用时,box 的宽高
const borderBoxSize = entry.borderBoxSize[0]
const writtingMode = elStyle.getPropertyValue('writing-mode')
const isHorizontal =
writtingMode === 'vertical-rl' ||
writtingMode === 'vertical-lr' ||
writtingMode === 'sideways-rl' ||
writtingMode === 'sideways-lr'
? false
: true
const width = isHorizontal
? borderBoxSize.inlineSize
: borderBoxSize.blockSize
const height = isHorizontal
? borderBoxSize.blockSize
: borderBoxSize.inlineSize
const boxSize = { width, height }
// 当 box 尺寸发生变化时以及初次触发回调时,将此刻 box 的目标尺寸暴露给 keyBoxSizeRef
keyBoxSizeRef.value = boxSize
// box 首次被观察时会触发一次回调,此时不需要应用过渡,只需将当前尺寸记录到 boxSizeList 中
if (!isObserved) {
isObserved = true
boxSizeList.push(boxSize)
return
}
// 当 box 尺寸发生变化时,使用 FLIP 动画技术产生过渡动画,使用过渡效果的是 scale 形变
// 根据 First 和 Last 计算出 Inverse 所需的 scale 大小
const scaleX = boxSizeList[0].width / width
const scaleY = boxSizeList[0].height / height
// 尺寸发生变化的瞬间,要使用 scale 变形将其保持变化前的尺寸,要先将 transition 去除
elStyle.setProperty('transition', 'none')
const originalTransform =
elStyle.transform || elComputedStyle.getPropertyValue('--transform')
elStyle.setProperty(
'transform',
`${originalTransform} scale(${scaleX}, ${scaleY})`
)
// 将 scale 移除,并应用 transition 以实现过渡效果
setTimeout(() => {
elStyle.setProperty('transform', originalTransform)
elStyle.setProperty('transition', `transform ${duration}ms ${mode}`)
})
}
})
resizeObserver.observe(el)
const cancelBoxTransition = () => {
resizeObserver.unobserve(el)
cancelAnimationFrame(frameId)
}
const result: [BoxSizeRef, () => void] = [keyBoxSizeRef, cancelBoxTransition]
return result
}
相应的 vue
组件代码如下:
<template>
<div class="outer" ref="outerRef">
<div class="card-container" ref="cardRef">
<div class="card-content">
<slot></slot>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import useBoxTransition from '@/utils/useBoxTransition'
type Props = {
transition?: boolean
duration?: number
mode?: string
}
const props = defineProps<Props>()
const { transition, duration = 200, mode = 'ease' } = props
const cardRef = ref<HTMLElement | null>(null)
const outerRef = ref<HTMLElement | null>(null)
let cancelBoxTransition = () => {} // 取消 boxTransition 效果
onMounted(() => {
if (cardRef.value) {
const cardEl = cardRef.value as HTMLElement
const outerEl = outerRef.value as HTMLElement
if (transition) {
const boxTransition = useBoxTransition(cardEl, duration, mode)
const keyBoxSizeRef = boxTransition[0]
cancelBoxTransition = boxTransition[1]
outerEl.style.setProperty(
'--transition',
`weight ${duration}ms ${mode}, height ${duration}ms ${mode}`
)
watch(keyBoxSizeRef, () => {
outerEl.style.setProperty('--height', keyBoxSizeRef.value.height + 'px')
outerEl.style.setProperty('--width', keyBoxSizeRef.value.width + 'px')
})
}
}
})
onUnmounted(() => {
cancelBoxTransition()
})
</script>
<style scoped lang="less">
.outer {
position: relative;
&::before {
content: '';
display: block;
width: var(--width);
height: var(--height);
transition: var(--transition);
}
.card-container {
position: absolute;
top: 50%;
left: 50%;
width: 100%;
--transform: translate(-50%, -50%);
transform: var(--transform);
box-sizing: border-box;
background-color: rgba(255, 255, 255, 0.7);
border-radius: var(--border-radius, 20px);
overflow: hidden;
backdrop-filter: blur(10px);
padding: 30px;
box-shadow: var(--box-shadow, 0 0 15px 0 rgba(0, 0, 0, 0.3));
}
}
</style>
优化后的效果如下:
注意点
过渡元素本身的 transform
样式属性
useBoxTransition
函数中会覆盖应用过渡的元素的 transform
属性,如果需要额外为元素设置其它的 transform
效果,需要使用 css
变量 --transform
设置,或使用内联样式设置。
这是因为,useBoxTransition
函数中对另外设置的 transform
效果和过渡所需的 transform
效果做了合并。
然而通过 getComputedStyle(Element)
读取到的 transform
的属性值总是会被转化为 matrix()
的形式,使得 transform
属性值无法正常合并;而 CSS
变量和使用 Element.style
获取到的内联样式中 transform
的值是原始的,可以正常合并。
如何选择获取元素宽高的方式
Element.getBoundingClientRect()
获取到的 DOMRect
的宽高包含了 transform
变化,而 Element.offsetWidth
/ Element.offsetHeight
以及 ResizeObserverEntry
对象获取到的宽高是元素本身的占位大小。
因此在需要获取 transition
过程中,包含 transform
效果的元素大小时,使用 Element.getBoundingClientRect()
,否则可以使用 Element.offsetWidth
/ Element.offsetHeight
或 ResizeObserverEntry
对象。
获取元素高度时遇到的 bug
测试案例中使用了 elementPlus
UI
库的 el-tabs
组件,当元素包含该组件时,无论是使用 Element.getBoundingClientRect()
、Element.offsetHeight
还是使用 Element.Style
、getComputedStyle(Element)
获取到的元素高度均缺少了 40px
;而使用 ResizeObserverEntry
对象获取到的高度则是正确的,但是它无法脱离 ResizeObserver API
独立使用。
经过测试验证,缺少的 40px
高度来自于 el-tabs
组件中 .el-tabs__header
元素的高度,也就是说,在获取元素高度时,将 .el-tabs__header
元素的高度忽略了。
测试后找出的解决方法是,手动将 .el-tabs__header
元素样式(注意不要写在带 scoped
属性的 style
标签中,会被判定为局部样式而无法生效)的 height
属性指定为 calc(var(--el-tabs-header-height) - 1px)
,即可恢复正常的高度计算。
至于为什么这样会造成高度计算错误,希望有大神能解惑。
来源:juejin.cn/post/7307894647655759911
驱动产业升级,OpenHarmony赋能千行百业,擘画开源新蓝图
OpenAtom OpenHarmony(简称“OpenHarmony”)凭借其领先的技术创新能力和丰富的商业实践应用,展现了其在推动地方开源生态建设发展方面的强大动力。随着数字化转型的深入,OpenHarmony作为下一代智能终端操作系统的佼佼者,正逐步成为赋能千行百业数字化转型的关键力量。
各行业遍地开花
截至8月底,OpenHarmony社区累计超过8000名贡献者,总计有超过70家的共建企业参与贡献,超过731款软硬件产品通过兼容性测评,覆盖教育、交通、金融、家居、安防等多个行业。得益于来自各行各业共建者的共同努力,OpenHarmony在智能终端领域迅速发展,已成为增速领先的开源操作系统之一。
OpenHarmony应用于实践的成功案例如雨后春笋般涌现。在教育领域,基于OpenHarmony的智能学生证和智能手写板等产品,通过多设备协同体验,为学生和教师提供了高效、便捷的学习与教学工具。在金融领域,云喇叭、POS机等金融终端的推出,不仅丰富了金融市场业务,还显著提升了信息安全性能。此外,OpenHarmony还广泛应用于政务、工业、交通等多个领域,推动了相关行业的数字化转型进程。
赋能地方开源生态建设
OpenHarmony社区不仅在商业实践上取得了显著成果,还积极赋能地方开源生态建设。近年来,深圳,福州,惠州,重庆,南京,成都,无锡等多地政府纷纷出台政策措施,从产业应用、产业集聚、生态体系建设等维度支持OpenHarmony发展,从供给侧和需求侧共同推动生态建设。
开源技术深入融合地方产业进步,持续拓宽合作的新领域。各地持续提升服务保障体系,全力促进开源项目的成长,共同探索和丰富开源生态。这些举措极大地激发了企业和社会各界的参与热情,为OpenHarmony在地方的落地和应用提供了坚实的政策支持。
同时,OpenHarmony社区积极与高校、企业等合作伙伴建立紧密联系,共同推动人才培养和技术创新。通过产学合作、实训基地建设、课程开发等多种形式,OpenHarmony为地方培养了大量具备开源技术能力的专业人才,为地方开源生态的繁荣发展注入了新鲜血液。
构筑根技术人才护城河
开源人才已逐步成为推动信息产业发展的基石与战略支柱。OpenHarmony坚守“培育根技术人才,共筑根社区未来”的宗旨,大力建设OpenHarmony人才认证体系,为社区制定了一套权威的人才能力评估标准。这一举措不仅巩固了开发者的核心竞争力,也促进了根技术人才生态的繁荣发展。
OpenHarmony社区不断深化开发者护城河的建设,同时为社区成员、企业技术人员及院校学生提供了更开阔、更具体的职业成长路径,确保源源不断地向生态产业输送高质量人才。
引领技术革新
展望未来,OpenHarmony将继续秉承开源开放的精神,加强与产业链上下游伙伴的合作,共同推动技术创新和生态建设。随着OpenHarmony版本的不断迭代和生态的日益完善,相信将有更多基于OpenHarmony的创新应用和产品问世,为各行各业的数字化转型提供更加坚实的支撑。
同时,OpenHarmony也将持续加强探索开源技术在地方经济社会发展中的新路径、新模式。通过构建更加完善的开源生态体系,OpenHarmony将助力地方实现高质量发展目标,共同书写数字化转型的新篇章。
在9月25-27日举行的2024开放原子开源生态大会上,OpenHarmony将聚焦技术创新和生态发展,与广大生态伙伴共同见证OpenHarmony最新版本、新能力,以及兼容性、软硬件生态和开发者生态的新进展,一起共享技术实践,使能千行百业,共话商业落地。敬请关注。
9月26日上午,OpenHarmony项目群工作委员会将举办OpenHarmony生态主题演讲,特邀全球开源操作系统产业伙伴、技术大咖和学术专家,面向全球展示OpenHarmony的技术创新和产业落地成果,分享开源社区生态进展,共同见证开源赋能产业的国际盛会!
精准倒计时逻辑:揭秘前端倒计时逻辑的实现策略
在业务运营中,倒计时功能是常见的需求,尤其是在限时秒杀等促销活动中。为了确保时间的精确性和一致性,推荐使用服务器时间作为倒计时的基准。那么,如何在前端实现一个既准确又用户友好的倒计时组件的计时逻辑呢?
传统计时器实现
传统计时器实现倒计时的核心原理很简单,它使用了 setInterval
或 setTimeout
的对计时信息进行更新。类似于如下代码:
import React, { useState, useEffect } from 'react';
const CountdownTimer: React.FC<{ duration: number }> = ({ duration }) => {
const [secondsRemaining, setSecondsRemaining] = useState(duration);
useEffect(() => {
const intervalId = setInterval(() => {
if (secondsRemaining > 0) {
setSecondsRemaining(secondsRemaining - 1);
} else {
clearInterval(intervalId);
}
}, 1000);
// 清理计时器
return () => clearInterval(intervalId);
}, [secondsRemaining]);
return (
<div>
倒计时: {secondsRemaining} 秒
</div>
);
};
export default CountdownTimer;
上述代码实现很好地实现了倒计时逻辑,但是,还是存在一些问题。我们先来讨论一下浏览器事件循环关于延时队列的优先级。我们知道,为了有效地管理任务和事件,事件循环使用了一个队列系统。事件循环主要包含如下两个队列:
- 宏任务队列(Macro Task Queue) :包括如
setTimeout
、setInterval
、I/O、UI 事件等。 - 微任务队列(Micro Task Queue) :包括Promise回调、
MutationObserver
等。
在事件循环中,当一个宏任务执行完毕后,JavaScript 引擎会先清空所有微任务队列中的所有任务,然后再去检查是否需要执行下一个宏任务。这意味着微任务的优先级高于宏任务。
setTimeout
或 setInterval
任务会在指定的延时后被加入到宏任务队列的末尾。当当前的宏任务执行完毕后,如果微任务队列不为空,JavaScript 引擎会先执行完所有微任务,然后才会执行下一个宏任务,也就是 setTimeout
或 setInterval
中的回调函数。因此,setTimeout
或 setInterval
的优先级是相对较低的,因为它们必须等待当前宏任务执行完毕以及所有微任务执行完毕后才能执行。
这种机制可能导致一个问题:如果页面上的其他微任务执行时间较长,倒计时显示可能会出现“跳秒”现象。例如,倒计时可能从 60 秒直接跳到 58 秒,而不是平滑地递减。
requestAnimationFrame 实现
针对上述“跳秒”问题,我们可以改用 requestAnimationFrame
去进行时间的更新逻辑执行。我们将上述代码修改为如下代码:
import React, { useState, useEffect } from 'react';
const CountdownTimer: React.FC<{ duration: number }> = ({ duration }) => {
const [secondsRemaining, setSecondsRemaining] = useState(duration);
useEffect(() => {
let animationFrameId: number;
const updateTimer = () => {
if (secondsRemaining > 0) {
setSecondsRemaining(prev => prev - 1);
animationFrameId = requestAnimationFrame(updateTimer);
} else {
cancelAnimationFrame(animationFrameId);
}
};
// 启动动画帧
animationFrameId = requestAnimationFrame(updateTimer);
// 清理动画帧
return () => cancelAnimationFrame(animationFrameId);
}, [secondsRemaining]);
return (
<div>
倒计时: {secondsRemaining} 秒
</div>
);
};
export default CountdownTimer;
在编写倒计时功能的代码时,我们应当确保在每次更新倒计时秒数后重新启动动画帧。这样做可以避免在动画帧完成后,倒计时逻辑停止更新,导致倒计时在减少一秒后不再继续。同时,为了确保资源的有效管理,我们还需要提供一个函数来清理动画帧,这样当组件不再需要时,可以停止执行动画帧,避免不必要的性能消耗。通过这些措施,我们可以保证倒计时功能的准确性和组件的高效卸载。
优势
要深入理解 requestAnimationFrame
在实现倒计时中的优势,我们首先需要探讨一个问题:在 requestAnimationFrame
中直接修改 DOM 是否合适?requestAnimationFrame
是一个专为动画效果设计的 Web API,它通过在浏览器的下一次重绘之前调用回调函数,帮助我们创建更加流畅且高效的动画。与传统的定时器方法(如 setTimeout
和 setInterval
)相比,requestAnimationFrame
提供了更优的性能和更少的资源消耗。
在 requestAnimationFrame
中修改 DOM 是合适的,尤其是当涉及到动画和视觉更新时。这是因为 requestAnimationFrame
的设计初衷就是为了优化动画性能,确保动画的流畅性和效率。总结来说,requestAnimationFrame
相较于传统的计时器方法,具有以下显著优势:
- 性能优化:通过在浏览器的下一次重绘前调用回调,确保动画的流畅性。
- 节能高效:当浏览器标签页不处于活跃状态时,
requestAnimationFrame
会自动暂停,从而减少 CPU 的使用,延长设备电池寿命。 - 同步刷新:能够与浏览器的刷新率同步,有效避免动画中的跳帧现象。
因此,requestAnimationFrame
不仅适用于复杂的动画场景,也非常适合实现需要精确时间控制的倒计时功能,提供了一种更加高效和节能的解决方案。
劣势
尽管 requestAnimationFrame
在动画制作方面表现出色,但在实现倒计时功能时,它也存在一些局限性:
- 精确度问题:
requestAnimationFrame
并不适用于需要严格时间控制的场景。因为它的调用时机依赖于浏览器的重绘周期,这可能导致时间间隔的不稳定性。 - 管理复杂性:使用
requestAnimationFrame
需要开发者手动管理动画状态和进行资源清理,这增加了实现的复杂度。
正因如此,许多现代前端框架和库,如 ahook 等,在选择实现倒计时功能时,倾向于采用传统的定时器(如 setTimeout
或 setInterval
),而非 requestAnimationFrame
。这些传统方法虽然可能不如 requestAnimationFrame
在动画性能上优化,但它们提供了更稳定和可预测的时间控制,这对于倒计时这类功能来说至关重要。
总结
实现一个倒计时组件的计时逻辑,我们有如下的一些建议:
- 动画与浏览器同步:对于涉及动画或需要与浏览器重绘周期同步的任务,
requestAnimationFrame
是一个理想的选择。它能够确保动画的流畅性和性能优化。 - 体验优化:为了进一步提升用户体验,可以利用
performance.now()
来提高时间控制的精度。这个高精度的时间戳 API 可以帮助你更准确地计算时间间隔,从而优化倒计时的显示效果。 - 时间控制与简易任务:如果你的应用场景需要精确的时间控制或涉及到简单的定时任务,传统的
setTimeout
和setInterval
方法可能更加方便和直观。它们提供了稳定的时间间隔,易于理解和实现。
总结来说,选择最合适的技术方案取决于你的具体需求。无论是 requestAnimationFrame
还是传统的定时器方法,都有其适用场景和优势。关键在于根据项目需求,做出明智的选择,以实现最佳的用户体验。
来源:juejin.cn/post/7412951456549175306
Spring Boot整合Kafka+SSE实现实时数据展示
2024年3月10日
知识积累
为什么使用Kafka?
不使用Rabbitmq或者Rocketmq是因为Kafka是Hadoop集群下的组成部分,对于大数据的相关开发适应性好,且当前业务场景下不需要使用死信队列,不过要注意Kafka对于更新时间慢的数据拉取也较慢,因此对与实时性要求高可以选择其他MQ。
使用消息队列是因为该中间件具有实时性,且可以作为广播进行消息分发。
为什么使用SSE?
使用Websocket传输信息的时候,会转成二进制数据,产生一定的时间损耗,SSE直接传输文本,不存在这个问题
由于Websocket是双向的,读取日志的时候,如果有人连接ws日志,会发送大量异常信息,会给使用段和日志段造成问题;SSE是单向的,不需要考虑这个问题,提高了安全性
另外就是SSE支持断线重连;Websocket协议本身并没有提供心跳机制,所以长时间没有数据发送时,会将这个连接断掉,因此需要手写心跳机制进行实现。
此外,由于是长连接的一个实现方式,所以SSE也可以替代Websocket实现扫码登陆(比如通过SSE的超时组件在实现二维码的超时功能,具体实现我可以整理一下)
另外,如果是普通项目,不需要过高的实时性,则不需要使用Websocket,使用SSE即可
代码实现
Java代码
pom.xml引入SSE和Kafka
<!-- SSE,一般springboot开发web应用的都有 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- kafka,最主要的是第一个,剩下两个是测试用的 -->
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
<dependency
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>3.4.0</version>
</dependency>
application.properties增加Kafka配置信息
# KafkaProperties
spring.kafka.bootstrap-servers=localhost:9092
spring.kafka.consumer.group-id=community-consumer-group
spring.kafka.consumer.auto-offset-reset=earliest
spring.kafka.producer.value-serializer=org.apache.kafka.common.serialization.StringSerializer
spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer
配置Kafka信息
@Configuration
public class KafkaProducerConfig {
@Value("${spring.kafka.bootstrap-servers}")
private String bootstrapServers;
@Bean
public Map<String, Object> producerConfigs() {
Map<String, Object> props = new HashMap<>();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
return props;
}
@Bean
public ProducerFactory<String, String> producerFactory() {
return new DefaultKafkaProducerFactory<>(producerConfigs());
}
@Bean
public KafkaTemplate<String, String> kafkaTemplate() {
return new KafkaTemplate<>(producerFactory());
}
}
配置controller,通过web方式开启效果
@RestController
@RequestMapping(path = "sse")
public class KafkaSSEController {
private static final Map<String, SseEmitter> sseCache = new ConcurrentHashMap<>();
@Resource
private KafkaTemplate<String, String> kafkaTemplate;
@Resource
private SseEmitter sseEmitter;
/**
* @param message
* @apiNote 发送信息到Kafka主题中
*/
@PostMapping("/send")
public void sendMessage(@RequestBody String message) {
kafkaTemplate.send("my-topic", message);
}
/**
* 监听Kafka数据
*
* @param message
*/
@KafkaListener(topics = "my-topic", groupId = "my-group-id")
public void consume(String message) {
System.out.println("Received message: " + message);
//使用接口建立起sse连接后,监听到kafka消息则会发送给对应链接
SseEmitter sseEmitter = sseCache.get(id); if (sseEmitter != null) { sseEmitter.send(content); }
}
/**
* 连接sse服务
*
* @param id
* @return
* @throws IOException
*/
@GetMapping(path = "subscribe", produces = {MediaType.TEXT_EVENT_STREAM_VALUE})
public SseEmitter push(@RequestParam("id") String id) throws IOException {
// 超时时间设置为5分钟,用于演示客户端自动重连
SseEmitter sseEmitter = new SseEmitter(5_60_000L);
// 设置前端的重试时间为1s
// send(): 发送数据,如果传入的是一个非SseEventBuilder对象,那么传递参数会被封装到 data 中
sseEmitter.send(SseEmitter.event().reconnectTime(1000).data("连接成功"));
sseCache.put(id, sseEmitter);
System.out.println("add " + id);
sseEmitter.send("你好", MediaType.APPLICATION_JSON);
SseEmitter.SseEventBuilder data = SseEmitter.event().name("finish").id("6666").data("哈哈");
sseEmitter.send(data);
// onTimeout(): 超时回调触发
sseEmitter.onTimeout(() -> {
System.out.println(id + "超时");
sseCache.remove(id);
});
// onCompletion(): 结束之后的回调触发
sseEmitter.onCompletion(() -> System.out.println("完成!!!"));
return sseEmitter;
}
/**
* http://127.0.0.1:8080/sse/push?id=7777&content=%E4%BD%A0%E5%93%88aaaaaa
* @param id
* @param content
* @return
* @throws IOException
*/
@ResponseBody
@GetMapping(path = "push")
public String push(String id, String content) throws IOException {
SseEmitter sseEmitter = sseCache.get(id);
if (sseEmitter != null) {
sseEmitter.send(content);
}
return "over";
}
@ResponseBody
@GetMapping(path = "over")
public String over(String id) {
SseEmitter sseEmitter = sseCache.get(id);
if (sseEmitter != null) {
// complete(): 表示执行完毕,会断开连接
sseEmitter.complete();
sseCache.remove(id);
}
return "over";
}
}
前端方式
<html>
<head>
<script>
console.log('start')
const clientId = "your_client_id_x"; // 设置客户端ID
const eventSource = new EventSource(`http://localhost:9999/v1/sse/subscribe/${clientId}`); // 订阅服务器端的SSE
eventSource.onmessage = event => {
console.log(event.data)
const message = JSON.parse(event.data);
console.log(`Received message from server: ${message}`);
};
// 发送消息给服务器端 可通过 postman 调用,所以下面 sendMessage() 调用被注释掉了
function sendMessage() {
const message = "hello sse";
fetch(`http://localhost:9999/v1/sse/publish/${clientId}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(message)
});
console.log('dddd'+JSON.stringify(message))
}
// sendMessage()
</script>
</head>
</html>
来源:juejin.cn/post/7356770034180898857
看了Kubernetes 源码后,我的Go水平突飞猛进
接口方式隐藏传入参数的细节
当方法的入参是一个结构体的时候,内部去调用时会看到入参过多的细节,这个时候可以将入参隐式转成结构,让内部只看到需要的方法即可。
type Kubelet struct{}
func (kl *Kubelet) HandlePodAdditions(pods []*Pod) {
for _, pod := range pods {
fmt.Printf("create pods : %s\n", pod.Status)
}
}
func (kl *Kubelet) Run(updates <-chan Pod) {
fmt.Println(" run kubelet")
go kl.syncLoop(updates, kl)
}
func (kl *Kubelet) syncLoop(updates <-chan Pod, handler SyncHandler) {
for {
select {
case pod := <-updates:
handler.HandlePodAdditions([]*Pod{&pod})
}
}
}
type SyncHandler interface {
HandlePodAdditions(pods []*Pod)
}
这里我们可以看到 Kubelet
本身有比较多的方法:
- syncLoop 同步状态的循环
- Run 用来启动监听循环
- HandlePodAdditions 处理Pod增加的逻辑
由于 syncLoop 其实并不需要知道 kubelet
上其他的方法,所以通过 SyncHandler
接口的定义,让 kubelet
实现该接口后,外面作为参数传入给 syncLoop
,它就会将类型转换为 SyncHandler
。
经过转换后 kubelet
上其他的方法在入参里面就看不到了,编码时就可以更加专注在 syncLoop
本身逻辑的编写。
但是这样做同样会带来一些问题,第一次研发的需求肯定是能满足我们的抽象,但是随着需求的增加和迭代,我们在内部需要使用 kubelet
其他未封装成接口的方法时,我们就需要额外传入 kubelet
或者是增加接口的封装,这都会增加我们的编码工作,也破坏了我们最开始的封装。
分层隐藏设计是我们设计的最终目的,在代码设计的过程中让一个局部关注到它需要关注的东西即可。
接口封装方便Mock测试
通过接口的抽象,我们在测试的时候可以把不关注的内容直接实例化成一个 Mock 的结构。
type OrderAPI interface {
GetOrderId() string
}
type realOrderImpl struct{}
func (r *realOrderImpl) GetOrderId() string {
return ""
}
type mockOrderImpl struct{}
func (m *mockOrderImpl) GetOrderId() string {
return "mock"
}
这里如果测试的时候不需要关注 GetOrderId
的方法是否正确,则直接用 mockOrderImpl
初始化 OrderAPI
即可,mock的逻辑也可以进行复杂编码
func TestGetOrderId(t *testing.T) {
orderAPI := &mockOrderImpl{} // 如果要获取订单id,且不是测试的重点,这里直接初始化成mock的结构体
fmt.Println(orderAPI.GetOrderId())
}
gomonkey 也同样能进行测试注入,所以如果以前的代码没能够通过接口封装也同样可以实现mock,而且这种方式更加强大
patches := gomonkey.ApplyFunc(GetOrder, func(orderId string) Order {
return Order{
OrderId: orderId,
OrderState: delivering,
}
})
return func() {
patches.Reset()
}
使用 gomonkey
能够更加灵活的进行 mock
, 它能直接设置一个方法的返回值,而接口的抽象只能够处理结构体实例化出来的内容。
接口封装底层多种实现
iptables 、ipvs等的实现就是通过接口的抽象来实现,因为所有网络设置都需要处理 Service 和 Endpoint ,所以抽象了 ServiceHandler
和 EndpointSliceHandler
// ServiceHandler 是一个抽象接口,用于接收有关服务对象更改的通知。
type ServiceHandler interface {
// OnServiceAdd 在观察到创建新服务对象时调用。
OnServiceAdd(service *v1.Service)
// OnServiceUpdate 在观察到现有服务对象的修改时调用。
OnServiceUpdate(oldService, service *v1.Service)
// OnServiceDelete 在观察到现有服务对象的删除时调用。
OnServiceDelete(service *v1.Service)
// OnServiceSynced 一旦所有初始事件处理程序都被调用并且状态完全传播到本地缓存时调用。
OnServiceSynced()
}
// EndpointSliceHandler 是一个抽象接口,用于接收有关端点切片对象更改的通知。
type EndpointSliceHandler interface {
// OnEndpointSliceAdd 在观察到创建新的端点切片对象时调用。
OnEndpointSliceAdd(endpointSlice *discoveryv1.EndpointSlice)
// OnEndpointSliceUpdate 在观察到现有端点切片对象的修改时调用。
OnEndpointSliceUpdate(oldEndpointSlice, newEndpointSlice *discoveryv1.EndpointSlice)
// OnEndpointSliceDelete 在观察到现有端点切片对象的删除时调用。
OnEndpointSliceDelete(endpointSlice *discoveryv1.EndpointSlice)
// OnEndpointSlicesSynced 一旦所有初始事件处理程序都被调用并且状态完全传播到本地缓存时调用。
OnEndpointSlicesSynced()
}
然后通过 Provider
注入即可,
type Provider interface {
config.EndpointSliceHandler
config.ServiceHandler
}
这个也是我在做组件的时候用的最多的一种编码技巧,通过将类似的操作进行抽象,能够在替换底层实现后,上层代码不发生改变。
封装异常处理
我们开启协程之后如果不对异常进行捕获,则会导致协程出现异常后直接 panic
,但是每次写一个 recover
的逻辑做全局类似的处理未免不太优雅,所以通过封装 HandleCrash
方法来实现。
package runtime
var (
ReallyCrash = true
)
// 全局默认的Panic处理
var PanicHandlers = []func(interface{}){logPanic}
// 允许外部传入额外的异常处理
func HandleCrash(additionalHandlers ...func(interface{})) {
if r := recover(); r != nil {
for _, fn := range PanicHandlers {
fn(r)
}
for _, fn := range additionalHandlers {
fn(r)
}
if ReallyCrash {
panic(r)
}
}
}
这里既支持了内部异常的函数处理,也支持外部传入额外的异常处理,如果不想要 Crash
的话也可以自己进行修改。
package runtime
func Go(fn func()) {
go func() {
defer HandleCrash()
fn()
}()
}
要起协程的时候可以通过 Go
方法来执行,这样也避免了自己忘记增加 panic
的处理。
waitgroup的封装
import "sync"
type Gr0up struct {
wg sync.WaitGr0up
}
func (g *Gr0up) Wait() {
g.wg.Wait()
}
func (g *Gr0up) Start(f func()) {
g.wg.Add(1)
go func() {
defer g.wg.Done()
f()
}()
}
这里最主要的是 Start
方法,内部将 Add
和 Done
进行了封装,虽然只有短短的几行代码,却能够让我们每次使用 waitgroup
的时候不会忘记去对计数器增加一和完成计数器。
信号量触发逻辑封装
type BoundedFrequencyRunner struct {
sync.Mutex
// 主动触发
run chan struct{}
// 定时器限制
timer *time.Timer
// 真正执行的逻辑
fn func()
}
func NewBoundedFrequencyRunner(fn func()) *BoundedFrequencyRunner {
return &BoundedFrequencyRunner{
run: make(chan struct{}, 1),
fn: fn,
timer: time.NewTimer(0),
}
}
// Run 触发执行 ,这里只能够写入一个信号量,多余的直接丢弃,不会阻塞,这里也可以根据自己的需要增加排队的个数
func (b *BoundedFrequencyRunner) Run() {
select {
case b.run <- struct{}{}:
fmt.Println("写入信号量成功")
default:
fmt.Println("已经触发过一次,直接丢弃信号量")
}
}
func (b *BoundedFrequencyRunner) Loop() {
b.timer.Reset(time.Second * 1)
for {
select {
case <-b.run:
fmt.Println("run 信号触发")
b.tryRun()
case <-b.timer.C:
fmt.Println("timer 触发执行")
b.tryRun()
}
}
}
func (b *BoundedFrequencyRunner) tryRun() {
b.Lock()
defer b.Unlock()
// 可以增加限流器等限制逻辑
b.timer.Reset(time.Second * 1)
b.fn()
}
写在最后
感谢你读到这里,如果想要看更多 Kubernetes 的文章可以订阅我的专栏: juejin.cn/column/7321… 。
来源:juejin.cn/post/7347221064429469746
多人开发小程序设置体验版的痛点
抛出痛点
在分配任务时,我们将需求分为三个分支任务,分别由前端A、B、C负责:
- 前端A: HCC-111-实现登录功能
- 前端B: HCC-112-实现用户注册
- 前端C: HCC-113-实现用户删除
相应地,我们创建三个功能分支:
- feature_HCC-111-实现登录功能
- feature_HCC-112-实现用户注册
- feature_HCC-113-实现用户删除
当所有的前端都开发完成了他们的任务,我们就要开始测试小程序了。但是如果按照以往体验版的测试方式,我们就需要排个顺序。比如,前端 A 先将他的小程序设置为体验版,测试把他的功能测试完成之后,再把前端 B 的设置为体验版,以此类推。可以看出真的很麻烦,而且浪费开发时间,我想你肯定不想在开发的时候突然被叫把你的小程序版本设置为体验版。
解决方案
小程序开发助手 这是一个官方提供的小程序,里面有多个版本的小程序可供选择,很方便测试人员的测试,并且也会节省开发人员的时间。点击版本查看就可以看到所有开发人员提交的最近的一次版本了。这样也不用设置体验版就可以测试最新的提交了。
再次抛出痛点
如果前端 A 头上有三个任务单呢?任务单:HCC-121-实现框架搭建,HCC-122-实现在线录屏,HCC-123-实现画板。此时你可能想说, 为啥前端 A 这么多的任务单呢?他命苦啊!
这个时候就需要配合微信的机器人了,我们可以创建多个机器人作为我们提交版本的媒介,这样我们就不受限于微信账号了。
可以在微信的官方文档看到 robot
参数有30个机器人可供选择。
接下来看下微信的机器人的使用方式。
微信官方是这样介绍这个工具的; miniprogram-ci 是从微信开发者工具中抽离的关于小程序/小游戏项目代码的编译模块。它其实是一个自动上传代码的工具,可以帮助我们自动化的编译代码并且上传到微信。
下面是一个大概得使用的示例,具体还是要参考官方文档。
const ci = require('miniprogram-ci');
(async () => {
const project = new ci.Project({
appid: 'wxsomeappid',
type: 'miniProgram',
projectPath: 'the/project/path',
privateKeyPath: 'the/path/to/privatekey',
ignores: ['node_modules/**/*'],
})
const previewResult = await ci.preview({
project,
desc: 'hello', // 此备注将显示在“小程序助手”开发版列表中
setting: {
es6: true,
},
qrcodeFormat: 'image',
qrcodeOutputDest: '/path/to/qrcode/file/destination.jpg',
onProgressUpdate: console.log,
// pagePath: 'pages/index/index', // 预览页面
// searchQuery: 'a=1&b=2', // 预览参数 [注意!]这里的`&`字符在命令行中应写成转义字符`\&`
})
console.log(previewResult)
})()
当我们使用这个脚本上传完代码就可以在小程序开发助手或者小程序管理平台看到以下内容。
微信管理后台
小程序开发助手页面
最后
我们可以使用 miniprogram-ci 配合 Jenkins 实现自动化部署,提交完成代码就可以自动部署了。以下是一个 github 的 actions 示例。当然也可以使用别的方式,例如本地提交,Jenkins提交等。
name: Feature Branch CI
on:
workflow_dispatch:
push:
branches: ['feature_*'] # 使用通配符匹配所有feature分支
jobs:
build_and_deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '20'
cache: 'yarn'
- name: Install dependencies
run: |
npm install -g miniprogram-ci cross-env
yarn install --frozen-lockfile
- name: Build Package
run: yarn cross-env ENV=PROD uni build -p mp-weixin --mode PROD
- name: Create private key file
run: echo "${{ secrets.PRIVATE_KEY }}" > private.key
- name: Deploy Package
env:
APP_ID: ${{ secrets.APP_ID }}
run: |
COMMIT_MESSAGE=$(git log --format=%B -n 1 ${{ github.sha }})
if [[ $COMMIT_MESSAGE =~ VERSION-([A-Za-z0-9_]+-[A-Za-z0-9_-]+)_DEV ]]; then
VERSION=${BASH_REMATCH[1]}
echo "Extracted Version: $VERSION"
miniprogram-ci preview \
--pp ./dist/build/mp-weixin \
--pkp ./private.key \
--appid $APP_ID \
--uv "${VERSION}" \
-r 7 \
--desc "${COMMIT_MESSAGE}" \
--upload-description "${COMMIT_MESSAGE}" \
--enable-es6 true \
--enable-es7 true \
--enable-minifyJS true \
--enable-minifyWXML true \
--enable-minifyWXSS true \
--enable-minify true \
--enable-bigPackageSizeSupport true \
--enable-autoPrefixWXSS true
else
echo "No Version found in commit message. Skipping upload."
fi
来源:juejin.cn/post/7412854873439027240
刚刚,英特尔发布最强CPU,AI PC迎来最高效x86芯片
金磊 发自 柏林
量子位 | 公众号 QbitAI
最高效的x86芯片,应当是一种怎样的“打开方式”?
就在刚刚,英特尔给出了一份答案——
英特尔® 酷睿™ Ultra 200V系列处理器。
△英特尔高级副总裁Jim Johnson
话不多说,直接上亮点:
- 最快的CPU:E核比上一代快68%,P核则快了14%
- 最快的内置(built-in)GPU:首次推出全新Xe2图形微架构,平均提升30% 的移动图形性能
- 最高AI性能:CPU、NPU和GPU整体算力高达120TOPS,直接拉高AI体验
- 最高效x86:整体功耗降低50%
- 超强的兼容性:各种软件应用程序均可兼容
由此,在搭载英特尔® 酷睿™ Ultra 200V系列芯片之后,AI PC们的生产力也迎来了“蜕变”:
- 每线程处理性能提高3倍
- 峰值性能提高80%
- 续航长达20小时
不仅如此,全球顶级的OEM和ISV们纷纷前来站台,例如谷歌、微软、联想、戴尔、惠普等等。
那么英特尔具体又是如何做到的?以及这个“史上最高效”又是如何界定的?我们继续往下看。
史上最高效的x86处理器
首先我们需要说明的是,Ultra 200V系列一共包含9款处理器,CPU均为8核8线程,GPU和NPU的核心数量会有所不同:
在低功耗方面,这里比较重要的一个变化,就是英特尔对低功耗岛(Low Power Island)做出的改变。
它把Lunar Lake核心数量和缓存增加了一倍(达到4MB和4个内核),并将E核从Crestmont更新到Skymont。
然后英特尔使用各种电源管理技术(包括 Thread Director),通过这个低功耗岛以低功耗来实现效率上的大幅提升。
一个比较有意思的点是,从这里开始,英特尔就直接开始向高通猛开炮火了,性能表现如下:
而除了高通之外,AMD也没能逃过被英特尔拿来公开做比较的命运。
例如在电池寿命方面,英特尔就表示已经超过了AMD和高通。
其次,是CPU方面。
英特尔在CPU上采用了全新的架构,即4个Skymont E核和4个Lion Cove P核,官方所展示的核心信息如下:
不过这里有一个关键的问题。
那就是P核不包括超线程,这个技术实际上允许单个CPU内核支持多个任务线程。
根据英特尔的说法,不采用超线程是因为这样会有助于芯片的整体集成。
尽管英特尔这次是“4+4”模型(8个线程,比上一代少很多),但从给出来的性能结果来看,要比AMD和高通要好得多。
在CPU之后,便是英特尔的内置GPU了。
英特尔这次首次亮相了全新的Xe2图形微架构,不仅适用于集成图形领域,而且适用于独立显卡。
在GPU性能的比较上就更有意思了。
英特尔先是用Ultra 7 155H和Ultra 9 288V在众多游戏上进行了PK,在Xe2加持之下,有了31%的性能提升。
然而到了与高通相比较的时候,结果是这样的:
高通X1E-84-100无法运行23款游戏。
在与AMD HX 370的比较过程中,Ultra 9 288V表现出来的结果是要快出16%。
除此之外,光线追踪也是Xe2的另一大亮点,领先于竞争对手,RT性能提高了30%。
最后,便是Ultra 200V在AI方面的性能了。
正如我们在开头提到的,英特尔此次整体算力达到了120TOPS,其中GPU是67TOPS,NPU 48TOPS,以及还有CPU的5 TOPS。
在性能对比上,同样是和高通相比,在使用Adobe Premiere和Lightroom功能等应用程序时,明显是要快得多。
值得一提的是,在量子位与英特尔交流过程中了解到,英特尔是目前与同行相比,唯一一家在CPU、NPU和GPU三个AI Engine都能做到均衡发展的那一个,而这也成为了其核心竞争力之一。
AI PC们都来站台了
除了英特尔此次“最高效x86处理器”的发布之外,现场的OEM和ISV们也是不可忽视的亮点。
以OEM为例,其数量之多,从一张图就能感受到这个feel了:
近乎所有的AI PC们都有所参与:20多个品牌,80多款机型都搭载了最新的Ultra 200V系列处理器。
不仅如此,在发布活动的现场,谷歌、微软、联想、戴尔和惠普等,也上台表达了对Ultra 200V能上机、上服务而感到的期待。
同样的,即便是在场外,PK的味道依旧是非常浓郁。
例如在demo演示区,英特尔就拉着AMD现场以赛车的游戏来比拼了一番:
**,时长00:17
当然,高通依旧是不能落下:
据了解,首批搭载Ultra 200V处理器的笔记本电脑将在9月24日上线。
不过在此之前,我们也不妨蹲一波更多的实测结果。
One More Thing
英特尔除了Ultra 200V系列芯片之外,此次还发布了Evo Edition标识。
据悉,获得标识的电脑必须通过严苛的英特尔Evo Edition OEM系统验证流程。
每款机型须首先通过预评估,并在通过之后的六个月内接受 10 种不同的测试标准。在此期间会对该机型进行调优,以满足英特尔Evo标准。
来源:juejin.cn/post/7410786086580437004
Go 重构:尽量避免使用 else、break 和 continue
今天,我想谈谈相当简单的事情。我不会发明什么,但我在生产代码中经常看到这样的事情,所以我不能回避这个话题。
我经常要解开多个复杂的 if else
结构。多余的缩进、过多的逻辑只会加深理解。首先,这篇文章的主要目的是让代码更透明、更易读。不过,在某些情况下还是必须使用这些操作符。
else 操作
例如,我们有简单的用户处理程序:
func handleRequest(user *User) {
if user != nil {
showUserProfilePage(user)
} else {
showLoginPage()
}
}
如果没有提供用户,则需要将收到的请求重定向到登录页面。If else
似乎是个不错的决定。但我们的主要任务是确保业务逻辑单元在任何输入情况下都能正常工作。因此,让我们使用提前返回来实现这一点。
func handleRequest(user *User) {
if user == nil {
return showLoginPage()
}
showUserProfilePage(user)
}
逻辑是一样的,但是下面的做法可读性会更强。
break 操作
对我来说,Break
和 Continue
语句总是可以分解的信号。
例如,我们有一个简单的搜索任务。找到目标并执行一些业务逻辑,或者什么都不做。
func processData(data []int, target int) {
for i, value := range data {
if value == target {
performActionForTarget(data[i])
break
}
}
}
你应该始终记住,使用 break
操作符并不能保证整个数组都会被处理。这对性能有好处,因为我们丢弃了不必要的迭代,但对代码支持和可读性不利。因为我们永远不知道程序会在列表的开头还是结尾停止。
在某些情况下,带有子任务的简单功能可能会破坏这段代码。
func processData(data []int, target int, subtask int) {
for i, value := range data {
if value == subtask {
performActionForSubTarget(data[i])
}
if value == target {
performActionForTarget(data[i])
break
}
}
}
这样我们实际上可以拆出一个 find
的方法:
func processData(data []int, target int, subTarget int) {
found := findTarget(data, target)
if found > notFound {
performActionForTarget(found)
}
found = findTarget(data, subTarget)
if found > notFound {
performActionForSubTarget(found)
}
}
const notFound = -1
func findTarget(data []int, target int) int {
if len(data) == 0 {
return notFound
}
for _, value := range data {
if value == target {
return value
}
}
return notFound
}
同样的逻辑,但是拆分成更细粒度的方法,也有精确的返回语句,可以很容易地通过测试来实现。
continue 操作
该操作符与 break
类似。为了正确阅读代码,您应该牢记它对操作顺序的具体影响。
func processWords(words []string, substring string) {
for _, word := range words {
if !strings.Contains(word, substring) {
continue
}
// do some buisness logic
performAction(word)
}
}
Continue
使得这种简单的流程变得有点难以理解。
让我们写得更简洁些:
func processWords(words []string, substring string) {
for _, word := range words {
if strings.Contains(word, substring) {
performAction(word)
}
}
}
来源:juejin.cn/post/7290931758786756669
实现基于uni-app的项目自动检查APP更新
我们平时工作中开发APP时,及时为用户提供应用更新是提升用户体验和保证功能完整性的重要一环。本文将通过一段实际的代码示例,详细介绍如何在基于uni-app框架的项目中实现自动检查应用更新的功能,并引导用户完成升级过程。该功能主要涉及与服务器端交互获取最新版本信息、比较版本号、提示用户升级以及处理下载安装流程。
创建一个checkappupdate.js
文件
这个文件是写升级逻辑处理的文件,可以不创建,直接在App.vue中写,但是为了便于维护,还是单独放出来比较好,可以放在common或者util目录中(App.vue能引入到就行,随意放,根目录也行),App.vue中引入该文件,调用升级函数如下图所示:
js完整代码
为了防止一点点代码写,容易让人云里雾里,先放完整代码,稍后再详细解释,其实看注释也就够了。
//这是服务端请求url配置文件,如果你直接卸载下面的请求中,可以不引入
import configService from '@/common/service/config.service.js'
export default function checkappupdate(param = {}) {
// 合并默认参数
param = Object.assign({
title: "A new version has been detected!",
content: "Please upgrade the app to the latest version!",
canceltext: "No upgrade",
oktext: "Upgrade now"
}, param)
plus.runtime.getProperty(plus.runtime.appid, (widgetInfo) => {
let platform = plus.os.name.toLocaleLowerCase() //Android
let os_version = plus.os.version //13 安卓版本
let vendor = plus.device.vendor //Xiaomi
let url = configService.apiUrl
uni.request({
url: url + '/checkAppUpdate',
method: 'GET',
data: {
platform: platform,
os_version: os_version,
vendor: vendor,
cur_version: widgetInfo.version
},
success(result) {
console.log(result)
let versionCode = parseInt(widgetInfo.versionCode)
let data = result.data ? result.data : null;
// console.log(data);
let downAppUrl = data.url
//判断版本是否需要升级
if (versionCode >= data.versionCode) {
return;
}
//升级提示
uni.showModal({
title: param.title,
content: data.log ? data.log : param.content,
showCancel: data.force ? false : true,
confirmText: param.oktext,
cancelText: param.canceltext,
success: res => {
if (!res.confirm) {
console.log('Cancel the upgrade');
// plus.runtime.quit();
return
}
// if (data.shichang === 1) {
// //去应用市场更新
// plus.runtime.openURL(data.shichangurl);
// plus.runtime.restart();
// } else {
// 开始下载
// 创建下载任务
var dtask = plus.downloader.createDownload(downAppUrl, {
filename: "_downloads/"
},
function (d, status) {
// 下载完成
if (status == 200) {
plus.runtime.install(d.filename, {
force: true
}, function () {
//进行重新启动;
plus.runtime.restart();
}, (e) => {
uni.showToast({
title: 'install fail:' + JSON
.stringify(e),
icon: 'none'
})
console.log(JSON.stringify(e))
});
} else {
this.tui.toast("download fail,error code: " +
status);
}
});
let view = new plus.nativeObj.View("maskView", {
backgroundColor: "rgba(0,0,0,.6)",
left: ((plus.screen.resolutionWidth / 2) - 45) +
"px",
bottom: "80px",
width: "90px",
height: "30px"
})
view.drawText('start download...', {}, {
size: '12px',
color: '#FFFFFF'
});
view.show()
// console.log(dtask);
dtask.addEventListener("statechanged", (e) => {
if (e && e.downloadedSize > 0) {
let jindu = ((e.downloadedSize / e.totalSize) *
100).toFixed(2)
view.reset();
view.drawText('Progress:' + jindu + '%', {}, {
size: '12px',
color: '#FFFFFF'
});
}
}, false);
dtask.start();
// }
},
fail(e) {
console.log(e);
uni.showToast({
title: 'Request error'
})
}
})
}
})
});
}
函数定义:checkappupdate
定义核心函数checkappupdate
,它接受一个可选参数param
,用于自定义提示框的文案等信息。函数内部首先通过Object.assign
合并默认参数与传入参数,以确保即使未传入特定参数时也能有良好的用户体验。
获取应用信息与环境变量
利用plus.runtime.getProperty
获取当前应用的详细信息,包括但不限于应用ID、版本号(version
)和版本号代码(versionCode
),以及设备的操作系统名称、版本和厂商信息。这些数据对于后续向服务器请求更新信息至关重要。
请求服务器检查更新
构建包含平台信息、操作系统版本、设备厂商和当前应用版本号的请求参数,发送GET请求至配置好的API地址/checkAppUpdate
,查询是否有新版本可用。后端返回参数参考下面:
/**
* 检测APP升级
*/
public function checkAppUpdate()
{
$data['versionCode'] = 101;//更新的版本号
$data['url'] = 'http://xxx/app/xxx.apk';//下载地址
$data['force'] = true;//是否强制更新
return json_encode($data);//返回json格式数据到前端
}
比较版本与用户提示
一旦收到服务器响应,解析数据并比较当前应用的版本号与服务器提供的最新版本号。如果存在新版本,使用uni.showModal
弹窗提示用户,展示新版本日志(如果有)及升级选项。此步骤充分考虑了是否强制更新的需求,允许开发者灵活配置确认与取消按钮的文案。
下载与安装新版本
用户同意升级后,代码将执行下载逻辑。通过plus.downloader.createDownload
创建下载任务,并监听下载进度,实时更新进度提示。下载完成后,利用plus.runtime.install
安装新APK文件,并在安装成功后调用plus.runtime.restart
重启应用,确保新版本生效。
用户界面反馈
在下载过程中,通过创建原生覆盖层plus.nativeObj.View
展示一个半透明遮罩和下载进度信息,给予用户直观的视觉反馈,增强了交互体验,进度展示稍微有点丑,可以提自己改改哈。
总结
通过上述步骤,我们实现了一个完整的应用自动检查更新流程,不仅能够有效通知用户新版本的存在,还提供了平滑的升级体验。此功能的实现,不仅提升了用户体验,也为产品迭代和功能优化提供了有力支持。开发者可以根据具体需求调整提示文案、下载逻辑、进度样式等细节,以更好地适配自身应用的特点和用户群体。
来源:juejin.cn/post/7367555191337828361
Innodb之buffer pool 图文详解
介绍
数据通常都是持久化在磁盘中的,innodb如果在处理客户端的请求时直接访问磁盘,无论是IO压力还是响应速度都是无法接受的;所以innodb在处理请求时,如果需要某个页的数据就会把整个页都加载到缓存中,数据页会长时间待在缓存中,根据一定策略进行淘汰,后续如果在缓存中的数据就不需要再次加载数据页了,这样即可提高响应时间又可以节省磁盘IO.
buffer pool
上述介绍中我们有提到一个缓存,这个缓存就指的是buffer pool
,通过innodb_buffer_pool_size
进行设置它的大小,默认为128MB,最小为5MB;可以根据各自线上机器的情况来设置它的大小,设置几个G甚至上百个G都是合理的。
内部组成
buffer pool中包含数据页、索引页、change buffer、自适应hash等内容;数据页、索引页在buffer pool中占用了大部分,不能简单的认为缓冲池中只有数据页和索引页;change buffer在较老的版本中叫insert buffer,后面对其进行了升级形成了现在的change buffer;自适应hash可以方便我们快速查询数据;锁信息、数据字典都是占用比较小的一部分;以上就是buffer pool的内部组成。
页数据
数据页、索引页数据在mysql启动的时候,会直接给申请一块连续的内存空间;如图:
上图中的缓冲页对应的就是磁盘中的数据,默认每个页大小为16KB,并且innodb为每个缓冲页都创建了一些控制块,每个控制块占用大小是800字节左右,需要额外付出百分之5的内存,它记录页所属的表空间编号、页号、缓存页在buffer pool中的地址、链表节点信息等。内存中间可能会有碎片进行对齐。
注意:这里只有缓冲页占用的空间是计算在buffer pool中的。
free链表
根据上面的图可以了解到,buffer pool中有一堆缓冲页,但innodb从磁盘中读取数据页时,由于不能直接知道哪些缓冲页是空闲的、哪些页已经被使用了,导致了不知道把要读取的数据页存放到哪里;此时就引入了一个free链表的概念。如图:
上图中可以看到free链表靠一个free节点连接到控制块中,其中free头节点仅占用40字节空间,但它也不计算在buffer pool中;有了这个free链表后每当需要从磁盘中加载一个页到buffer pool中时就可以从free链表上取一个控制块,把控制块所需信息填充上,同时把从磁盘上加载的数据放到对应的缓冲页上,并把该控制块从free链表中移除。此时就把磁盘中的页加载到内存中了,后续查询数据时就会优先查询该内存页,但每次查询时没办法立刻知道该页是在内存中还是磁盘中,上述操作后还会把这个页信息放到一个散列表中,以(表空间号+页号)作为key,以控制块地址作为value。
flush链表
上述介绍了读数据时通过优先读取内存页可以提高我们的响应速度以及节省磁盘io,那么如果是写数据呢?其实在innodb中,更改也会优先在内存中更改,在后续会根据一定规则(会在后续redolog文章中详细介绍)进行刷盘,在刷盘时只需要刷被更改的缓冲页即可,那么哪些缓存页被更改了innodb是不知道的,此时innodb就设计了flush链表,它和free链表几乎一样,如图:
当需要刷盘时会从flush链表中拿出一部分控制块对应的缓冲页进行刷盘,刷盘后控制块会从flush链表中移除,并放到free链表中。
LRU链表
由于buffer pool的内存区域是有限的,如果当来不及刷盘时内存就不够用了;此时innodb采用了LRU淘汰策略,标准的LRU算法:
- 如果数据页已经被加载到buffer pool中了,则直接把对应的控制块移动到LRU链表的头部;
- 如果数据页不在buffer pool中,则从磁盘中加载到buffer pool中,并把其对应的控制块放到LRU头部;此时内存空间已经满了的话,就会从链表中移除最后一个内存页。
但直接采用lru方案,在内存较小或者临时一次性加载到内存中的页过多时会把真正的热点数据刷掉。如预读和全表扫描。
- 线性预读:如果顺序访问的某个区的页面数超过
innodb_read_ahead_threshold
(默认值为56)就会触发一次异步预加载下一个区中的全部页到内存中; - 随机预读:如果某个区的13个连续的页都被加载到young区前1/4的位置中,会触发一次异步预加载本区中的全部页到内存中;
- 全表扫描:把整张表的数据都加载到内存中。
为了解决上述问题,innodb对这个淘汰策略做了一点改变。如图:
innodb根据innodb_old_blocks_pct
(默认37)参数把整个lru分成一定比例,具体的淘汰策略:
- 当数据页第一次加载进来时会先放到old head处,当链表满时会把old tail刷盘并从链表中移除。
- 当再次使用一个数据页时,并且该页在old区,会先判断在old区的停留时间是否超过
innodb_old_blocks_time
(默认1000ms),如果超过则把该数据页移动到young head处,反之移动到old head处。 - 当再次使用一个数据页时,并且该页young区为了节省移动的操作,会判断该缓冲页是否在young区前1/4中,如果在就不进行移动,如果不在则移动到young head处。
多buffer pool实例
对于buffer pool设置很大内存的实例,由于操作各种链表都需要进行加锁这样就比较影响整体的性能,为了提高并发处理能力,可以通过innodb_buffer_pool_instances
来设置buffer pool的实例个数。在mysql5.7.5版本中,会以chunk为单位向系统申请内存空间,每个buffer pool中包含了N个chunk。如图:
可以通过innodb_buffer_pool_chunk_size
(默认128M)来设置chunk的大小,只能在启动前设置好,启动后可以更改innodb_buffer_pool_size
的大小,但必须时innodb_buffer_pool_chunk_size * innodb_buffer_pool_instances的整数倍。
自适应hash
对于b+数来讲,整体的查询时间复杂度为O(logN),而innodb为了进一步提升性能,引入了自适应hash,对热点数据可以做到O(1)的时间复杂度就可以定位到数据在具体哪个页中。
innodb会自动根据访问频率和模式自动为某些热点页在内存中建立自适应哈希索引,规则:
- 模式:例如有一个联合索引(a,b);查询条件
where a = xxx
与where a = xxx and b = xxx
这就属于两种模式,如果交叉使用这两种查询,不会为其建立自适应哈希索引;
- 频率:使用一种默认访问的次数大于Math.min(100,页中数据/16)。
根据官方数据,启动自适应哈希索引读写速度可以提升2倍,辅助索引的连接性能可以提升5倍。
总结
通过上述的介绍,希望能帮助大家对buffer pool有一个基础的了解,想进一步深入了解可以通过执行show engine innodb status
观察下各种参数,通过对每个参数的细致研究可以全方面的掌握buffer pool。
创作不易,觉得文章写得不错帮忙点个赞吧!如转载请标明出处~
来源:juejin.cn/post/7413196978601295899
仿树木生长开花的动画效果
效果介绍
使用 canvas 进行绘制树木生长的效果,会逐渐长出树干,长出树叶,开出花。当窗口大小发生变化时进行重新渲染。
实现效果展示
实现步骤
创建画布
import React, { useEffect, useRef } from 'react'
function TreeCanvas(props: {
width: number;
height: number;
}) {
const { width = 400, height = 400 } = props;
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const canvas = canvasRef.current;
const context = canvas?.getContext('2d');
if (!canvas || !context) return;
context.strokeStyle = '#a06249';
}, [])
return (
<canvas ref={canvasRef} width={width} height={height} />
)
}
export default TreeCanvas
封装创建树枝的方法
- 树枝需要起点,终点,树枝宽度
function lineTo(p1: PointType, p2: PointType, lineWidth: number) {
context?.beginPath();
context?.moveTo(p1.x, p1.y);
context?.lineTo(p2.x, p2.y);
context.lineWidth = lineWidth;
context?.stroke();
}
绘制树叶和花朵的方法封装
- 提前生成图片实例
- 传递图片和坐标进行绘制
// 花的实例
const image = new Image();
image.src ='https://i.postimg.cc/D0LLWwKy/flower1.png';
// 叶子的实例
const imageLeaves = new Image();
imageLeaves.src = 'https://i.postimg.cc/PJShQmH6/leaves.png';
function drawTmg(imageUrl: any, p1: PointType) {
context?.drawImage(imageUrl, p1.x, p1.y, 20 * Math.random(), 20 * Math.random());
}
封装绘制处理
- 提供绘制的起点,计算绘制的终点
- 根据起点和终点进行绘制
// 计算终点
function getEnd(b: BranchType) {
const { start, theta, length } = b;
return {
x: start.x + Math.cos(theta) * length,
y: start.y + Math.sin(theta) * length
};
}
// 绘制整理
function drawBranch(b: BranchType) {
// 绘制树干
lineTo(b.start, getEnd(b), b.lineWidth);
if (Math.random() < 0.4) { // 绘制花朵的密度
drawTmg(image, getEnd(b));
}
if (Math.random() < 0.4) {
drawTmg(imageLeaves, b.start); // 绘制树叶的密度
}
}
绘制树的方法
- 起点和终点的计算及绘制数的角度计算
- 绘制左边树和右边树
- 随机绘制
function step(b: BranchType, depth: number = 0) {
const end = getEnd(b);
drawBranch(b);
if (depth < depthNum || Math.random() < 0.5) {
step(
{
start: end,
theta: b.theta - 0.3 * Math.random(),
length: b.length + (Math.random() * 10 - 5),
lineWidth: depthNum - depth
},
depth + 1
);
}
if (depth < depthNum || Math.random() < 0.5) {
step(
{
start: end,
theta: b.theta + 0.3 * Math.random(),
length: b.length + (Math.random() * 10 - 5),
lineWidth: depthNum - depth
},
depth + 1
);
}
}
动画处理
- 把所有绘制添加到动画处理中
const pendingTasks: Function[] = []; // 动画数组
function step(b: BranchType, depth: number = 0) {
const end = getEnd(b);
drawBranch(b);
if (depth < depthNum || Math.random() < 0.5) {
pendingTasks.push(() => { // 添加左侧动画
step(
{
start: end,
theta: b.theta - 0.3 * Math.random(), // 角度变化
length: b.length + (Math.random() * 10 - 5), // 长度变化
lineWidth: depthNum - depth
},
depth + 1
);
});
}
if (depth < depthNum || Math.random() < 0.5) {
pendingTasks.push(() => { // 添加右侧动画
step(
{
start: end,
theta: b.theta + 0.3 * Math.random(), // 角度变化
length: b.length + (Math.random() * 10 - 5),// 长度变化
lineWidth: depthNum - depth
},
depth + 1
);
});
}
}
function frame() {
const tasks = [...pendingTasks];
pendingTasks.length = 0;
tasks.forEach((fn) => fn());
}
let framesCount = 0;
function satrtFrame() {
requestAnimationFrame(() => {
framesCount += 1;
// if (framesCount % 10 === 0) {
frame();
satrtFrame();
// }
});
}
封装执行方法
useEffect(() => {
function init() {
step(startBranch);
}
satrtFrame();
init();
}, []);
添加常用场景封装
- 宽高获取当前屏幕大小
- 屏幕发生变化时进行重新渲染
export const TreeCanvasInner = () => {
const [innerSize, setInnerSize] = useState({ x: window.innerWidth, y: window.innerHeight });
useEffect(() => {
const resizeFunc = () => {
setInnerSize({ x: window.innerWidth, y: window.innerHeight });
};
window.addEventListener('resize', resizeFunc);
return () => {
window.removeEventListener('resize', resizeFunc);
};
}, []);
return (
<TreeCanvas
key={JSON.stringify(innerSize)}
width={innerSize.x}
height={innerSize.y}
startBranch={{ start: { x: 0, y: 0 }, theta: 20, length: 25, lineWidth: 3 }}
/>
);
};
完整代码
来源:juejin.cn/post/7309061655095361571
为什么很多人不推荐你用JWT?
为什么很多人不推荐你用JWT?
如果你经常看一些网上的带你做项目的教程,你就会发现 有很多的项目都用到了JWT。那么他到底安全吗?为什么那么多人不推荐你去使用。这个文章将会从全方面的带你了解JWT 以及他的优缺点。
什么是JWT?
这个是他的官网JSON Web Tokens - jwt.io
这个就是JWT
JWT 全称JSON Web Token
如果你还不熟悉JWT,不要惊慌!它们并不那么复杂!
你可以把JWT想象成一些JSON数据,你可以验证这些数据是来自你认识的人。
当然如何实现我们在这里不讲,有兴趣的可以去自己了解。
下面我们来说一下他的流程:
- 当你登录到一个网站,网站会生成一个JWT并将其发送给你。
- 这个JWT就像是一个包裹,里面装着一些关于你身份的信息,比如你的用户名、角色、权限等。
- 然后,你在每次与该网站进行通信时都会携带这个JWT。
- 每当你访问一个需要验证身份的页面时,你都会把这个JWT带给网站。
- 网站收到JWT后,会验证它的签名以确保它是由网站签发的,并且检查其中的信息来确认你的身份和权限。
- 如果一切都通过了验证,你就可以继续访问受保护的页面了。
为什么说JWT很烂?
首先我们用JWT应该就是去做这些事情:
- 用户注册网站
- 用户登录网站
- 用户点击并执行操作
- 本网站使用用户信息进行创建、更新和删除 信息
这些事情对于数据库的操作经常是这些方面的
- 记录用户正在执行的操作
- 将用户的一些数据添加到数据库中
- 检查用户的权限,看看他们是否可以执行某些操作
之后我们来逐步说出他的一些缺点
大小
这个方面毋庸置疑。
比如我们需要存储一个用户ID 为xiaou
如果存储到cookie里面,我们的总大小只有5个字节。
如果我们将 ID 存储在 一个 JWT 里。他的大小就会增加大概51倍
这无疑就增大了我们的宽带负担。
冗余签名
JWT的主要卖点之一就是其加密签名。因为JWT被加密签名,接收方可以验证JWT是否有效且可信。
但是,在过去20年里几乎每一个网络框架都可以在使用普通的会话cookie时获得加密签名的好处。
事实上,大多数网络框架会自动为你加密签名(甚至加密!)你的cookie。这意味着你可以获得与使用JWT签名相同的好处,而无需使用JWT本身。
实际上,在大多数网络身份验证情况下,JWT数据都是存储在会话cookie中的,这意味着现在有两个级别的签名。一个在cookie本身上,一个在JWT上。
令牌撤销问题
由于令牌在到期之前一直有效,服务器没有简单的方法来撤销它。
以下是一些可能导致这种情况危险的用例。
注销并不能真正使你注销!
想象一下你在推特上发送推文后注销了登录。你可能会认为自己已经从服务器注销了,但事实并非如此。因为JWT是自包含的,将在到期之前一直有效。这可能是5分钟、30分钟或任何作为令牌一部分设置的持续时间。因此,如果有人在此期间获取了该令牌,他们可以继续访问直到它过期。
可能存在陈旧数据
想象一下用户是管理员,被降级为权限较低的普通用户。同样,这不会立即生效,用户将继续保持管理员身份,直到令牌过期。
JWT通常不加密
因此任何能够执行中间人攻击并嗅探JWT的人都拥有你的身份验证凭据。这变得更容易,因为中间人攻击只需要在服务器和客户端之间的连接上完成
安全问题
对于JWT是否安全。我们可以参考这个文章
JWT (JSON Web Token) (in)security - research.securitum.com
同时我们也可以看到是有专门的如何攻击JWT的教程的
高级漏洞篇之JWT攻击专题 - FreeBuf网络安全行业门户
总结
总的来说,JWT适合作为单次授权令牌,用于在两个实体之间传输声明信息。
但是,JWT不适合作为长期持久数据的存储机制,特别是用于管理用户会话。使用JWT作为会话机制可能会引入一系列严重的安全和实现上的问题,相反,对于长期持久数据的存储,更适合使用传统的会话机制,如会话cookie,以及建立在其上的成熟的实现。
但是写了这么多,我还是想说,如果你作为自己开发学习使用,不考虑安全,不考虑性能的情况下,用JWT是完全没有问题的,但是一旦用到生产环境中,我们就需要避免这些可能存在的问题。
来源:juejin.cn/post/7365533351451672612
保守点,90%的程序员不适合做独立开发
大家好,我卡颂。
近两年互联网行业不景气,很多程序员都在寻找新出路。很自然的,独立开发成为一个充满吸引力的选择 —— 背靠自己的开发技能,不用看老板脸色,靠产品养活自己,想想就很美好。
但恕我直言,保守点说,90%的程序员不适合做独立开发。
这篇文章全是大实话,虽然会打破一些人的幻想,但也提供解决方案,希望对迷茫的同学有些帮助。
独立开发赚钱么?
如果你满足如下画像:
- 程序员工作多年,编程水平不错
- 收入完全来源于工资
- 日常学习的目的是提升技术
那对你来说,独立开发是不赚钱的。不赚钱并不是说做这事儿一分钱赚不到,满足以上画像的大部分独立开发者在持续经营半年到一年产品后,还是能稳定获得几刀~几十刀收益的。只是相比于付出的心血来说,这点收益实在是低。
以至于出海独立开发圈儿在谈收益时的语境都不是我开发了1年,现在每月能赚50刀,而是我开发了1年,现在拥有了等效于3w刀年化2%的货基(3w * 2% / 12 = 50)
这么一换算,欣慰了许多。
为什么不赚钱?因为独立开发的重点并不在于开发,叫独立产品会更准确些。
对于一款形成稳定变现闭环的产品,有3个最重要的环节:
- 流量获取
- 运营转化
- 产品交付
程序员只是产品交付环节下的一个工种,与你同处产品交付环节的工种还包括产品经理、QA、项目经理、运维......
做独立开发的本质就是你一个人抗下上述所有工种。
话又说回来,如果你即会编程又会流量获取,会运营转化,这样的复合人才在公司根本不用担心被裁,也没必要做独立开发。
所以,对于满足以上画像的同学,我劝你不要把独立开发当作失业后的救命稻草。
认识真实的商业世界
虽然我不建议你all in
独立开发,但我建议有空闲时间的同学都去尝试下独立开发。
尝试的目的并不是赚钱,而是更具象的感知流量获取 -> 运营转化 -> 产品交付的路径。
大部分互联网产品往简单了说,都是表格 + 表单的形式,比如推特就是2个大表单(推荐流、关注流)以及描述用户之间关系的表格。
既然如此,当我们有了独立开发的想法时,首先考虑的应该是 —— 我的产品能不能用表格 + 表单 + 高效沟通实现,比如腾讯/飞书文档 + 微信群交流。
像多抓鱼(做二手书业务)早期验证需求时,就是几个用户群 + 保存二手书信息的excel表组成。
如果你发现需求靠微信群交流就能解决,付款靠微信转账就能解决,那还有必要写代码开发项目,对接微信支付API么?
当聊到微信交流时,其实就触碰到另一个工种的工作范围了 —— 私域运营。在私域运营看来,通过微信(或其他社交软件)成交是再正常不过的商业模式,但很多程序员是不知道的。
这就是为什么我不建议你把独立开发当作被裁后的救命稻草,但建议有空闲时间的同学都去尝试下独立开发 —— 涉猎其他工种的工作范围,认识真实的商业世界。
当达到这一步后,我们再考虑下一步 —— 发掘你的长处。
发掘你的长处
当我们认识到一款完整的产品有3个最重要的环节:
- 流量获取
- 运营转化
- 产品交付
就应该明白 —— 如果我们想显著提高独立开发的成功率,最好的方式是找到自己最擅长的环节,再和擅长其他环节的人合作。
这里很多程序员有个误区,会认为程序员擅长的肯定就是产品交付下的开发。
实际上,就我交流过的,或者亲自带出来的跑通变现闭环的程序员中,很多人有编程之外的天赋,只是他们没有意识到罢了。
举几个非常厉害的能力(或者说天赋):
- 向上突破的能力
有一类同学敢于把自己放到当前可能还不胜任的位置,然后通过不断学习让自己完成挑战。举几个例子:
- 在不懂地推的时候,参与到校园外卖团队做地推,学习市场和推广的知识
- 在只看了一本
HTML
书的情况下,敢直接接下学校建设国际会议网站的任务 - 在不懂做运营的时候,有老板找他当公司运营负责人,他也接下来,并也做得很好
这类同学很容易跑出有自己特色的非标服务,再包装成产品售卖。
- 源源不断的心力支持
有位同学看短视频趋势不错,正好大学也玩过一段时间单反,就买了一套专业的影视设备,准备一边学做饭一边拍短视频,想做一名美食博主。
每天下班拍视频、剪辑加后期的,每个视频都需要花 10+ 个小时。熬了半年多,数据一直不行,就放弃了。
虽然他失败了,但很少有人能在没有正反馈的事上坚持半年,这种源源不断的心力支持其实是一种天赋。
靠这个天赋,只要踩到合适的赛道,成功是迟早的事儿。
- 链接人的能力
有些同学特别喜欢在群里唠嗑,与大佬聊天也不犯怵。这就是链接人的天赋。
在如今的时代,有价值的信息通常是在小圈子中传播,再慢慢破圈到大众视野中。这类同学靠链接人的天赋,可以:
- 从小圈子获得有价值的信息,做信息差生意
- 做中间人整合资源
假设你探寻一圈后发现 —— 自己最拿得出手的就是编程能力,那你的当务之急不是发掘需求。
以咱们普通程序员的产品sense
,也就能想出笔记应用、Todo List应用这类点子了......
你需要做的,是多认识其他圈子的人,向他们展示你的编程能力,寻找潜在的需求方。
以我在运营的Symbol社区
举例,这是个帮程序员发展第二曲线的社群。
之前社群有个痛点:每天社群会产生大量有价值的碎片知识,但这些知识分散在大量聊天消息中,爬楼看消息很辛苦。
基于这个痛点出发,我作为产品经理和群里两位小伙伴合作开发了识别、总结、打标签、分发有价值聊天记录的社群机器人:
作为回报,这两位小伙伴将获得付费社群的收入分成。
总结
对于满足如下画像的程序员:
- 程序员工作多年,编程水平不错
- 收入完全来源于工资
- 日常学习的目的是提升技术
不要把独立开发当作被裁后的救命稻草,而应该将其作为认识真实商业世界分工的途径,以及发掘自身优势的手段。
拍脑袋想没有用,只有真正在事儿上修,才能知道自己喜欢什么、擅长什么。
当认清自身优势后,与有其他优势的个体合作,一起构建有稳定收益闭环的产品。
来源:juejin.cn/post/7345756317557047306
前端纯css实现-一个复选框交互展示效果
纯手工写一个复选框前端交互,这样可以自由定制自己想要展示的字段和设计风格
写这篇文章主要是抛砖引玉,可能存在槽点,大家多多担待!
1.交互效果展示
用码上掘金在线简单的写了一下:
2.简要说明
$primary-color:#1e80ff;
// 主题色-掘金蓝
$primary-disable: #7ab0fd;
// 只读或禁用色
可以根据实际需求更改主题色,这里的禁用变量色忘记使用了,sorry!!!
3.布局代码部分
<!-- page start -->
<div class="ui-layout-page">
<h1>请选择关注类型</h1>
<div class="ui-checkbox">
<!-- 复选框 item start -->
<div
:class="{'ui-item-box':true,'ui-item-check': i.isCheck,'ui-item-disable':i.disable}"
v-for="(i,index) in list"
:key="index"
@click="doCheck(i)">
<img :src="i.icon"/>
<span class="span-bar">
<p class="label-bar">{{i.label}}</p>
<p class="desc-bar">{{i.desc}}</p>
</span>
<!-- 选中标识 start -->
<span
v-if="i.isCheck"
class="icon-check">
</span>
<!-- 选中标识 end -->
</div>
<!-- 复选框 item end -->
</div>
<p style="font-size:12px;color:#333">当前选择ids:{{ this.checked.join(',') }}</p>
</div>
<!-- page end -->
4.方法和数据结构部分
checked:['1','2'],
list:[
{
label:'JYM系统消息',
id:'1',
desc:'关注掘金系统消息',
isCheck:true,
icon:'https://gd-hbimg.huaban.com/6f3e3ff111c6c98be6785d9eddd5b13f8979ef9d1719e-Xwo8QB_fw658webp',
disable:true,
},{
label:'JYM后端',
id:'2',
isCheck:true,
desc:'关注后端讨论区新消息',
icon:'https://gd-hbimg.huaban.com/e2622fe339d655bd17de59fed3b0ae0afb9a16c31db25-YNpnGV_fw658webp',
disable:false,
},{
label:'JYM前端',
id:'3',
isCheck:false,
desc:'关注前端讨论区新消息',
icon:'https://gd-hbimg.huaban.com/80765200aa4ffb7683ddea51c3063b0801874fb86324-3OVCQN_fw1200',
disable:false,
},{
label:'JYM开发工具',
id:'4',
isCheck:false,
desc:'关注开发工具讨论区新消息',
icon:'https://gd-hbimg.huaban.com/ef1c0e1fb2eae73d674aae791526a331b45b26d2b78e-r4p1aq_fw1200',
disable:false,
}
]
/**
* 复选点击事件
* el.disable 禁用状态
* */
doCheck(el){
if(el.disable) return
if(this.checked.includes(el.id)){
el.isCheck = false
this.checked=this.checked.filter(item => item !== el.id);
} else{
el.isCheck = true
this.checked.push(el.id)
}
this.checked.join(',')
}
5.样式控制部分
.ui-layout-page{
padding:20px;
h1{
font-size:16px;
}
// 个性化复选框 css start -------------
.ui-checkbox{
width:100%;
$primary-color:#1e80ff; // 主题色-掘金蓝
$primary-disable: #7ab0fd; // 只读或禁用色
// 选中状态css
.ui-item-check{
border:1px solid $primary-color !important;
background:rgba($primary-color,0.05) !important;
}
// 禁用状态css
.ui-item-disable{
border:1px solid #d3d3d3 !important;
background: #f3f3f3 !important;
cursor:not-allowed !important;
.icon-check{
border-top:20px solid #ccc !important;
}
.label-bar{
color:#777 !important;
}
.desc-bar{
color:#a3a3a3 !important;
}
}
// 常规状态css
.ui-item-box{
position:relative;
display:inline-flex;
align-items: center;
width:220px;
height:70px;
border:1px solid #ccc;
cursor: pointer;
margin:0px 8px 8px 0px;
border-radius:4px;
overflow:hidden;
&:hover{
border:1px solid $primary-color;
background:rgba($primary-color,0.05);
}
img{
width:38px;
height:38px;
margin-left:15px;
}
p{
margin:0px;
}
.span-bar{
width:0px;
flex:1 0 auto;
padding:0px 10px;
.label-bar{
font-size:14px;
font-weight:700;
margin-bottom:4px;
color:#333;
}
.desc-bar{
font-size:12px;
color:#999;
}
}
// 绘制圆角斜三角形
.icon-check{
position:absolute;
width:0px;
height:0px;
top:2px;
right:2px;
border-top:20px solid $primary-color;
border-left:25px solid transparent;
border-radius: 5px 3px 5px 0px;
&:after{
content:'✓';
position: relative;
color:#fff;
font-size:12px;
left: -12px;
top: -26px;
}
}
}
}
// 个性化复选框 css end -------------
}
来源:juejin.cn/post/7412545166539128841
CSS 实现呼吸灯
引言
在现代前端开发中,为网站添加吸引人的动画效果是提高用户体验的一种常见方式。其中,呼吸灯效果是一种简单而又引人注目的动画,适用于各种应用场景。本文将深入研究如何使用 CSS 来实现呼吸灯效果,包括基本的实现原理、动画参数调整、以及一些实际应用案例。
第一部分:基本的呼吸灯效果
1. 使用关键帧动画
呼吸灯效果的实现依赖于 CSS 的关键帧动画。我们可以使用 @keyframes
规则定义一个简单的呼吸灯动画。
@keyframes breathe {
0% {
opacity: 0.5;
transform: scale(1);
}
50% {
opacity: 1;
transform: scale(1.2);
}
100% {
opacity: 0.5;
transform: scale(1);
}
}
.breathing-light {
animation: breathe 3s infinite;
}
在这个例子中,我们定义了一个名为 breathe
的关键帧动画,包含三个关键帧(0%、50%、100%)。在不同的关键帧,我们分别调整了透明度和缩放属性,从而形成了呼吸灯效果。
2. 应用到元素
接下来,我们将这个动画应用到一个元素上,例如一个 div
。
<div class="breathing-light"></div>
通过给这个元素添加 breathing-light
类,我们就能够观察到呼吸灯效果的实现。可以根据实际需求调整动画的持续时间、缓动函数等参数。
第二部分:调整动画参数
1. 调整动画持续时间
通过调整 animation
属性的第一个值,我们可以改变动画的持续时间。例如,将动画持续时间改为 5 秒:
.breathing-light {
animation: breathe 5s infinite;
}
2. 调整缓动函数
缓动函数影响动画过渡的方式。可以通过 animation-timing-function
属性来调整。例如,使用 ease-in-out
缓动函数:
.breathing-light {
animation: breathe 3s ease-in-out infinite;
}
3. 调整动画延迟时间
通过 animation-delay
属性,我们可以设置动画的延迟时间。这在创建多个呼吸灯效果不同步的元素时很有用。
.breathing-light {
animation: breathe 3s infinite;
animation-delay: 1s;
}
第三部分:实际应用案例
1. 页面标题的动态效果
在页面的标题上应用呼吸灯效果,使其在页面加载时引起用户的注意。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Breathing Light Title</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<h1 class="breathing-light">Welcome to Our Website</h1>
</body>
</html>
@keyframes breathe {
0% {
opacity: 0.5;
transform: scale(1);
}
50% {
opacity: 1;
transform: scale(1.2);
}
100% {
opacity: 0.5;
transform: scale(1);
}
}
.breathing-light {
animation: breathe 3s infinite;
}
2. 图片边框的动感效果
通过为图片添加呼吸灯效果,为静态图片增加一些生动感。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Breathing Light Image</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="image-container">
<img src="example-image.jpg" alt="Example Image" class="breathing-light">
</div>
</body>
</html>
@keyframes breathe {
0% {
opacity: 0.5;
transform: scale(1);
}
50% {
opacity: 1;
transform: scale(1.2);
}
100% {
opacity: 0.5;
transform: scale(1);
}
}
.breathing-light {
animation: breathe 3s infinite;
}
.image-container {
display: inline-block;
overflow: hidden;
border: 5px solid #fff; /* 图片边框 */
}
结语
通过本文,我们深入探讨了如何使用 CSS 实现呼吸灯效果。从基本原理、动画参数调整到实际应用案例,希望读者能够深刻理解呼吸灯效果的制作过程,并能够在实际项目中灵活运用这一技术,为用户呈现更加生动有趣的页面效果。不仅如此,这也是提升前端开发技能的一种乐趣。
来源:juejin.cn/post/7315314479204581391
文本美学:text-image打造视觉吸引力
当我最近浏览 GitHub 时,偶然发现了一个项目,它能够将文字、图片和视频转化为文本,我觉得非常有趣。于是我就花了一些时间了解了一下,发现它的使用也非常简单方便。今天我打算和家人们分享这个发现。
项目介绍
话不多说,我们先看下作者的demo效果:
_20240420194201.jpg
text-image可以将文字、图片、视频进行「文本化」
只需要通过简单的配置即可使用。
虽然这个项目star数很少,但确实是一个很有意思的项目,使用起来很简单的项目。
_20240420194537.jpg
github地址:https://github.com/Sunny-117/text-image
我也是使用这个项目做了一个简单的web页面,感兴趣的家人可以使用看下效果:
web地址:http://h5.xiuji.mynatapp.cc/text-image/
_20240420211509.jpg
项目使用
这个项目使用起来相对简单,只需按作者的文档使用即可,虽然我前端属于小白的水平,但还是在ai的帮助下做了一个简单的html页面,如果有家人需要的话可以私信我,我发下文件。下边我们就介绍下:
- 文字「文本化」
先看效果:
_20240420195701.jpg
我们在这儿是将配置的一些参数在页面上做了一个可配置的表单,方便我们配置。
家人们想自己尝试的话可以试下以下这个demo。
demo.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Document</title>
</head>
<body>
<canvas id="demo"></canvas>
<script src="http://h5.xiuji.mynatapp.cc/text-image/text-image.iife.js"></script>
<script>
textImage.createTextImage({
canvas: document.getElementById('demo'),
replaceText: '123',
source: {
text: '修己xj',
},
});
</script>
</body>
</html>
- 图片「文本化」
_20240420200651.jpg
demo.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Document</title>
</head>
<body>
<canvas id="demo"></canvas>
<script src="http://h5.xiuji.mynatapp.cc/text-image/text-image.iife.js"></script>
<script>
textImage.createTextImage({
canvas: document.getElementById('demo'),
raduis: 7,
isGray: true,
source: {
img: './assets/1.png',
},
});
</script>
</body>
</html>
- 视频「文本化」
1.gif
demo.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Document</title>
</head>
<body>
<canvas id="demo"></canvas>
<script src="http://h5.xiuji.mynatapp.cc/text-image/text-image.iife.js"></script>
<script>
textImage.createTextImage({
canvas: document.getElementById('demo'),
raduis: 8,
isGray: true,
source: {
video: './assets/1.mp4',
height: 700,
},
});
</script>
</body>
</html>
需要注意的是:作者在项目中提供的视频的demo这个属性值有错误,我们需要改正后方可正常显示:
_20240420211124.jpg
总结
text-image 是一个强大的前端工具,可以帮助用户快速、轻松地将文本、图片、视频转换成文本化的图片,增强文本内容的表现力和吸引力。
来源:juejin.cn/post/7359510120248786971
把哈希表换成 tire 树,居然为公司省下了几千万
你有没有想过,仅仅省下1%的计算资源,能为一家大公司带来多大的影响?你可能觉得,1%听起来微不足道,完全不值得一提。但今天我们聊一下一个技术优化点,就是关于如何通过微小的优化,Cloudflare这样的大型网络公司如何省下了大量的计算资源,背后还有不少值得我们学习的智慧。
你也在为计算资源头疼吗?
如果你是个开发者,尤其是负责维护大规模服务的开发者,你一定对计算资源的消耗有深刻的体会。无论是服务器的 CPU 使用率还是内存消耗,甚至是网络带宽,稍有不慎就可能让成本暴增。而且,问题不止是花钱那么简单,资源浪费还会拖慢你的系统,影响用户体验,最终给公司带来巨大的损失。
我要说的是 Cloudfllare 公司的案例,它不是什么宏大的技术革新或颠覆性的变革,而是从一些不起眼的小地方着手,积少成多,最终实现了1%的节省。这背后到底有什么诀窍?我们一起来看看。
1. 换个数据结构,省时又省力
在Cloudflare的案例中,他们的第一个关键优化是引入了更高效的数据结构。在大规模的数据处理中,数据结构的选择往往是决定性能的关键因素。
他们提到了将原有的哈希表结构换成了更适合他们需求的trie(字典树)结构。
下述是然来的 hash 表结构
// PERF: heavy function: 1.7% CPU time
pub fn clear_internal_headers(request_header: &mut RequestHeader) {
INTERNAL_HEADERS.iter().for_each(|h| {
request_header.remove_header(h);
});
}
这是优化之后的版本
pub fn clear_internal_headers(request_header: &mut RequestHeader) {
let to_remove = request_header
.headers
.keys()
.filter_map(|name| INTERNAL_HEADER_SET.get(name))
.collect::<Vec<_>>();
to_remove.int0_iter().for_each(|k| {
request_header.remove_header(k);
});
可以看到,他是先构造了一颗 tire 树,然后在进行操作
那么,为什么是trie呢?因为它能更高效地存储和处理特定类型的数据,特别是字符串相关的操作。每次的字符串查找、匹配操作都能变得更加快速,减少了不必要的计算消耗。
这就好比我们平时在超市找商品,如果货架排布得井井有条,一目了然,那么找起东西来肯定又快又省力。
2. 不只是省电,它还能加速系统响应
有时候,节省计算资源并不仅仅体现在电费账单上,它还直接影响系统的响应时间。你有没有遇到过访问一个网站时,页面加载缓慢,让你心急如焚?这很大程度上与后台的计算效率有关。
Cloudflare在优化trie结构后,明显提升了系统的响应速度。
举个通俗的例子,如果原本的哈希表是一个在黑夜中摸索东西的场景,那优化后的trie结构就是在白天找东西——路径明确,操作直观,不浪费多余的时间。这种提升,虽然看起来只是毫秒级的,但在每天处理数以亿计的请求时,省下来的时间就变得非常可观了。
3. 别小看每一次小改动
也许你会问:“这1%的优化,真有那么重要吗?” Cloudflare给出的答案是肯定的。别看只是1%,但在他们这种大规模系统中,每天的请求数以亿计,这1%就意味着节省了大量的服务器计算资源。试想,如果你每天的电费能减少1%,一年下来呢?这可是一笔不小的费用。
而且,从另一个角度看,任何微小的改动都有可能是更大优化的开始。Cloudflare的工程师们通过这次的优化,深入分析了系统中的其他潜在问题,发掘出更多可以提升的地方。这就像是修车时,你发现一个小问题,结果一修就发现了更多的隐患,最后车子不仅恢复了正常,还比以前跑得更快。
4. 现在,从你的小项目开始优化吧
当然,Cloudflare的规模可能让很多普通开发者觉得遥不可及,但这并不意味着我们不能从中学到东西。你手上的小项目同样可以从数据结构、代码效率等方面着手进行优化。
比如,如果你正在处理大量的字符串数据,不妨考虑一下是否可以用trie这种结构来提升效率。或者你可以先从代码的性能分析开始,看看有没有哪个部分的计算特别耗时。通过一点点的优化,哪怕最终只提升了1%,也会让你长期受益。不过,注意,过早优化,依然是万恶之源,先使用优雅的代码实现,上线后做性能瓶颈分析才是正道,换句话说,一个只有 10 几个 PV的地方,那么是再好的数据结构和算法,也很难体现出商业成本上的价值。
5. 未来的路:每一秒都很重要
在如今这个对速度要求极高的互联网时代,每一秒、甚至每一毫秒的节省都至关重要。你以为的“小改动”,可能就是让你的服务脱颖而出的关键。
Cloudflare通过这些小优化,每天节省的计算资源不仅让他们的服务更加高效,也给其他同行树立了一个榜样:技术上的进步不一定是依靠大刀阔斧的改革,有时候,从细微处着手,能带来同样惊人的效果。
不知道,你是否也开始思考自己项目中的那些“隐形”问题了呢?或许,你已经意识到了一些可以优化的地方,但还没来得及动手。那为什么不从今天开始,试着优化一两个小地方呢?也许下次的1%提升,就来自于你的一点点努力。
这个世界上没有小优化,只有还没被发现的优化。
来源:juejin.cn/post/7412848251844280360
华为三折叠手机19999元起!全展开10.2寸大屏3.6mm厚度,电池只留1.9mm
梦晨 发自 凹非寺
量子位 | 公众号 QbitAI
华为三折叠新品发布会,弹幕一片的 “报价吧,让我死心” 。
全球首款三折叠手机MateXT,价格正式揭晓:
- 256GB版,19999元
- 512GB版,21999元
- 1TB版,23999元
预计9月20日上午10点08分正式开售,发布会与苹果同一天,正式开卖也与苹果同一天。
看到这里,你是死心了还是动心了?
华为MateXT系列是全球最大折叠屏手机,全展开总计10.2英寸屏幕。
同时,它还是全球最薄折叠屏手机,三屏展开状态只有3.6mm。
余承东手持样机在现场表示,“把平板装在口袋里”,这个梦想已经实现了。
作为对比华为平板在售型号最小尺寸为11英寸。
不过也有许多网友表示: “它和平板的区别在于平板我买得起”。
配合官方配件折叠触控键盘,把电脑装在口袋里也实现了。
余承东表示这是一款“大家都能想得到,但是做不出来的产品”。
这样一款全新形态的手机,究竟有哪些使用场景?
发布会现场也做了演示。
三折叠屏都能怎么用
面向商务人士,出差路上也要办公,是华为MateXT这次主打的场景之一。
单屏折叠,扫码登机便于持握。
双屏展开,适合在线浏览简单资讯。
三屏全展开,连复杂的行业报告都能拿捏。
放在一起对比,余承东称三屏模式的实际可视内容在双屏的两倍以上。
除了严肃的工作场景,三屏展开来看电影视觉效果也更震撼。
三折叠除了常规横向使用,华为也考虑到了竖起来使用这种需求,官方支架配件也支持90度旋转。
竖屏短视频刷起来也更带感了。
对于APP的适配方面,余承东开了个玩笑: “小红书秒变大红书” 。
再接下来还演示了华为系统搭载的一些AI功能。
比如AI总结,可以实现左边原文,右边摘要,无需来回切换。
AI翻译同理,方便跳转回原文。
AI修图能力虽然和三折叠屏没太多联动关系,而且很多人已经上手体验过。
但鉴于前面有几款手机并未召开发布会,华为官方在新机发布会上讲解还是第一次。
三折叠手机还有哪些亮点?
看过三折叠屏都有哪些玩法之后,是时候再了解一下背后的技术突破了。
这是折叠屏铰链系统同时实现内外弯折。
也是超薄大屏第一次实现内外弯折。
余承东讲解“Z”字形内外弯折最大的难点在于,会出现两种互斥的力。
这一次能实现,背后最大助力也在现场揭秘:华为首创的天工铰链系统,实现双轨联动。
具体细节并未透露太多,目前已知采用多向弯折柔性材料,外折部分抗拉伸,内折部分抗挤压。
与之配合,屏幕本身也有特别设计,内侧是业界最大的UTG玻璃,最外层为非牛顿流体材料,双层都有不错的抗冲击能力。
还记得整部手机只有3.6mm厚度么,刨去前后外壳、屏幕、电路板等,留给电池的空间只剩……
1.9mm!
最后,华为高端手机向来重视的影像系统、通信能力这次依然保持高水准。
华为首款三折叠手机,满足你的期待了吗?
来源:juejin.cn/post/7412813889689272383
排行榜--实现点击视图自动滚动到当前用户所在位置.
需求
我们今天来实现一下,点击当前用户的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