注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

前端可以玩“锁”🔐了

web
大家好,我是CC,在这里欢迎大家的到来~ “锁”经常使用在多进程的语言理和数据库事务的架构当中,现在 Web API 当中也提供了“锁”- Web Locks API。 领域 在浏览器多标签页或 worker 中运行的脚本中获取锁,执行工作时保持锁,最后释放锁...
继续阅读 »

大家好,我是CC,在这里欢迎大家的到来~


“锁”经常使用在多进程的语言理和数据库事务的架构当中,现在 Web API 当中也提供了“锁”- Web Locks API


领域


在浏览器多标签页或 worker 中运行的脚本中获取锁,执行工作时保持锁,最后释放锁。



锁的范围仅限于同一源内



请求锁


同一源下,当持有锁时,其他相同锁的请求将排队,仅当锁被释放时第一个排队的请求才会被授予锁。



回调函数执行完毕后锁会自动释放



navigator.locks.request('mylock', {}, async (lock) => {
console.log(lock);
});

在这里我们能看到 request 方法的第二个参数(可选),可以在请求锁时传递一些选项,这个我们在后边会介绍到。


监控锁


判断锁管理器的状态,有利于调试;返回结果是一个锁管理器状态的快照,标识了持有锁和请求中的锁的有关数据,像名称、client_id和模式。


navigator.locks.query().then((locks) => {
console.log(locks);
});

实现


接下来将使用请求锁的可选参数实现以下内容:


从异步任务返回值


request() 方法本身返回一个 Promise,一旦锁被释放,该 Promise 就会 resolve。


const result = await navigator.locks.request('ccmama'}, async (lock) => {
// 任务
return data;
});
// 拿到内部回调函数返回的 data
console.log(result);

共享锁和独占锁模式


配置项 mode 默认是 'exclusive',可选项还有 'shared'。



锁只能有一个持有者,但是可以同时授权多个共享。


在读写模式中经常使用 'shared' 模式进行读取,'exclusive' 模式用于写入。



navigator.locks.request('ccmama', {
mode: 'shared',
}, async (lock) => {
// 任务
});


📢


持有 'exclusive' 锁,同名 'exclusive' 锁排队等候


持有 'exclusive' 锁,同名 'shared' 锁排队等候


持有 'shared' 锁,同名 'shared' 锁也可访问同一资源


持有 'shared' 锁,同名 'exclusive' 锁排队等候



条件获取


配置项 ifAvailable 默认 false,当设置 true 时锁请求仅在不需要排队时才会被授予,也就是说在任务没有其他等待的情况下锁请求才会被授予,否则返回 null。


navigator.locks.request('ccmama', { ifAvailable: true }, async lock => {
if (!lock) return;
// 任务
});


注意:同名锁



防止死锁的应急通道


配置项 steal 默认 false,当设置为 true 时任何持有的同名锁将被释放,并且请求将被授权,抢占任何排队中的锁请求。


navigator.locks.request('ccmama', { steal: true }, async lock => {
// 任务
});


⚠️


使用要小心。之前在锁内运行的代码会继续运行,并且可能与现在持有锁的代码发生冲突。



中止锁定请求


配置项 signal 是 AbortSignal 类型;如果指定并且 AbortController 被中止,则锁请求将被丢弃。


try {
const controller = new AbortController();
setTimeout(() => controller.abort(), 400);
navigator.locks.request('ccmama', { signal: controller.signal }, async lock => {
// 任务
});
} catch(ex) {}

// 或

try {
navigator.locks.request('ccmama', { signal: AbortSignal.timeout(1000) }, async lock => {
// 任务
});
} catch(ex) {}


⚠️


超时时会报出一个异常错误,需要使用 try catch 捕获



参考文章



可能理解并不一定到位,欢迎交流。


作者:CC码码
来源:juejin.cn/post/7382640456109490211
收起阅读 »

向全栈靠齐的前端分享

web
背景与思考 前端在很多后端开发人员中,总是觉得没啥技术含量。尤其是在老java眼中,深深感觉存在严重的鄙视链。然后就是自己的职业规划,也不想一直做前端敲代码。毕竟自己的付出不少,也想收获属于自己的成就感。然后自己的横向发展就成了必然。 后端技术首推Node ...
继续阅读 »

背景与思考


前端在很多后端开发人员中,总是觉得没啥技术含量。尤其是在老java眼中,深深感觉存在严重的鄙视链。然后就是自己的职业规划,也不想一直做前端敲代码。毕竟自己的付出不少,也想收获属于自己的成就感。然后自己的横向发展就成了必然。


后端技术首推Node



  1. 前后端编程环境和语法一致,上手非常快。

  2. 轻量级,部署简单。

  3. 生态丰富,文档颇多,碰到问题,百度查询方便。

  4. 高效的异步I/O模型,易处理大并发和连接。


Node框架推荐Koa


koa.png



  1. 相对于express,Koa更加的轻便,上手主打一个简单易学好用。

  2. 语法上它的中间件和前端的模块化很像,开发思路一致。

  3. 前端熟悉的async await,promise方式,很好的解决了多层嵌套,地狱回调问题。

  4. 借助 co 和 generator,很好地解决了异步流程控制和异常捕获问题。


学习推荐


我当初学习也是想看了一下官网,发现确实如介绍般的简单,但是对于入门者来说,有点简单的过分了。在此推荐阮一峰老师的网络日志(不是打广告,确实是我当初前端起步阶段的老师之一,受益匪浅)。


主要代码解析


项目结构

微信截图_20240918135658.png


app.js源码

const Koa = require('koa');
const Router = require('koa-router');

// 跨域模块
var cors = require('koa2-cors');
//文件模块
const fs = require('fs');

const { historyApiFallback } = require('koa2-connect-history-api-fallback');
//静态文件加载
const serve = require('koa-static');
//路径管理
const path = require('path');

//koa-body对文件上传进行配置
const koaBody = require('koa-body')

//实例化koa
const app = new Koa();

app.use(historyApiFallback());

app.use(cors());
const router = new Router();
const bodyParser = require('koa-bodyparser');
const controller = require('./controller');
app.use(async (ctx, next) => {
ctx.set("Access-Control-Allow-Origin", "*")
await next()
})
app.use(bodyParser());
// 处理跨域

app.use(controller());

app.use(koaBody({
multipart:true,
formidable:{
maxFileSize:50000*1024*1024, //设置上传文件大小最大限制,默认为2m,2000*1024*1024
keepExtensions: true // 保留文件拓展名
}
}))


// 1.主页静态网页 把静态页统一放到public中管理
const main = serve(path.join(__dirname) + '/build');

//配置路由
app.use(router.routes()).use(router.allowedMethods());

const port = 5000;
app.use(main)

app.listen(port, () => {
console.log(`server started on ${port}`)
});


依赖包讲解

const Koa = require('koa');

这是引入koa框架,这是重中之重,只有引入了才能够在项目中使用。在项目中会通过new来实例化,比如代码中的const app = new Koa();。然后再定义一个监听的端口,app.listen()方法来进行监听。


const fs = require('fs');

这是koa自带的文件模块,如果你想对系统文件进行读取,修改。或者文件上传保存,都离不开整个fs模块,fs.readFilefs.readFileSync


const koaBody = require('koa-body')

Koa-body是基于Koa的中间件模型构建的,主要用于文件上传,以及在中间件中对请求体的解析。对请求体的解析中,我们主要使用koa-bodyparser,它可以将http请求中的数据,解析成我们需要的JavaScript对象。


const Router = require('koa-router');
```门口
Router模块就是路由,此路由和前端路由有差异,此路由可以理解为前端理解的api接口,只是叫法不一样而已。

`
``js
const { historyApiFallback } = require('koa2-connect-history-api-fallback');

koa2-connect-history-api-fallback是一个专门为 Koa2 框架设计的中间件,它的主要目的是在SPA应用中处理URL重定向,尤其是在用户直接输入或者通过后退按钮访问非根URL时。 这个中间件会将所有未匹配到特定路由的请求转发到默认HTML文件(通常是 index.html),确保SPA可以正常启动并处理路由。还记得当初自己终于完成了一整套的项目线上部署,可把自己开心坏了,但是同事在一次用着发现,刷新页面时,页面直接变成了404,你说吓不吓人。盘查一下发现自己在vue前端中的路由为何在后端中变成了一个get请求。


404.png


controller.js源码

const fs = require('fs')

// add url-route in /controllers:

function addMapping (router, mapping) {
for (var url in mapping) {
if (url.startsWith('GET ')) {
var path = url.substring(4)
router.get(path, mapping[url])
// console.log(`register URL mapping: GET ${path}`);
} else if (url.startsWith('POST ')) {
var path = url.substring(5)
router.post(path, mapping[url])
// console.log(`register URL mapping: POST ${path}`);
} else if (url.startsWith('PUT ')) {
var path = url.substring(4)
router.put(path, mapping[url])
// console.log(`register URL mapping: PUT ${path}`);
} else if (url.startsWith('DELETE ')) {
var path = url.substring(7)
router.del(path, mapping[url])
// console.log(`register URL mapping: DELETE ${path}`);
} else {
// console.log(`invalid URL: ${url}`);
}
}
}

function addControllers (router, dir) {
fs.readdirSync(__dirname + '/' + dir)
.filter(f => {
return f.endsWith('.js')
})
.forEach(f => {
// console.log(`process controller: ${f}...`);
let mapping = require(__dirname + '/' + dir + '/' + f)
addMapping(router, mapping)
})
}

module.exports = function (dir) {
let controllers_dir = dir || 'controllers',
router = require('koa-router')()
addControllers(router, controllers_dir)
return router.routes`()`
}


controller讲解

在这个controller中,我们主要做了一件事,那就是路由映射逻辑处理。


function addControllers()

这个方法用于自动加载指定目录下的js文件,它使用fs.readdirSync读取目录,然后通过filterforEach方法来处理每个文件名,只选择以.js结尾的文件,并将这些文件的路由映射添加到router


function addMapping()

这个函数用于将HTTP方法(如GET、POST、PUT、DELETE)和对应的URL路径映射到处理函数上。它遍历传入的mapping对象,根据URL的前缀(如GET POST 等)来确定使用哪个HTTP方法,并将路径和处理函数注册到router上。


controllers下路由POST方法

const jwt = require('jsonwebtoken')
module.exports = {
'POST /login': async (ctx, next) => {
var key = ctx.request.body
if (key.username && key.password) {
return new Promise(function (resolve, reject) {
var MongoClient = require('mongodb').MongoClient
var MG_URL = 'mongodb://***********/'
MongoClient.connect(
MG_URL,
{ useUnifiedTopology: true },
function (err, db) {
if (err) throw err
var dbo = db.db('website')
dbo
.collection('user')
.find({ username: key.username, password: key.password })
.toArray(function (err, result) {
if (result.length) {
const TOKEN = jwt.sign(
{
name: result[0].username
},
'MY_TOKEN',
{ expiresIn: '24h' }
)
let data = {
username: result[0].username,
token: TOKEN
}
ctx.response.body = {
result: 1,
status: 200,
code: 200,
data: data
}
} else {
ctx.response.body = {
result: 0,
status: 200,
code: 0,
msg: '该用户不存在'
}
}

if (err) throw err
resolve(result)
db.close()
})
}
)
})
} else {
ctx.response.body = {
result: 0,
status: 200,
code: 0,
msg: 'error'
}
}
}
}


这是一个登录的login方法,用POST进行请求。在这个地方用了一下mongodb数据库存储。在api接口请求login方法时,获取请求中所携带的参数进行解析,并判断此用户以及密码是否在我们的数据库中,如果存在返回成功的提示以及相关数据,如果错误,则提示错误。当然如果还不会数据库的使用,可以去除数据库相关部分,直接用本地json数据,这个比较简单,就是fs读取本地json文件,然后返回给api接口。不在此做详细说明。


controllers下路由GET方法

module.exports = {
'GET /getNews': async (ctx, next) => {
return new Promise(function (resolve, reject) {
var MongoClient = require('mongodb').MongoClient
var MG_URL = 'mongodb://********'
MongoClient.connect(
MG_URL,
{ useUnifiedTopology: true },
function (err, db) {
if (err) throw err
var dbo = db.db('website')
dbo
.collection('news')
.find({})
.toArray(function (err, result) {
if (result.length) {
ctx.response.body = {
result: 1,
status: 200,
code: 1,
data: result
}
} else {
ctx.response.body = {
result: 0,
status: 200,
code: 0,
msg: '暂无数据'
}
}
if (err) throw err
resolve(result)
db.close()
})
}
)
})
}
}


这是一个获取新闻的getNews方法,用GET请求。主要用来查询数据库中的list的信息。DELETE,PUT等方法不在此处贴出更多源码。


数据库首推mondodb


1498119609539254.png



  1. 面向集合存储,易存储对象类型的数据。

  2. 模式自由。

  3. 高性能、易部署、易使用。

  4. 文档型数据结构灵活,适应不同类型的数据。

  5. 支持动态查询。

  6. 非关系型数据库。


学习推荐


为啥选择MongoDB数据库,相对来说操作还是比较简单,而且存储的数据类型都是对象的形式,前端可以轻松拿捏。在这里直接推荐菜鸟的mongodb教程,看名字就知道,这是一个适合菜鸟初步学习的地方。讲解也比较详细,学完上面的内容,用mongodb数据库进行基本的数据存储和操作已经没有问题了。


总结


通过以上的分享,其实对大多数前端来说,开启一个简单的后端服务和接口请求,已经可以开箱即用了。想要完整的学习代码,也可以私信我。虽然不是很完善,但麻雀虽小五脏俱全。


思考


在前端行业已经接7载。曾经害怕java的恐惧而转入前端行业,所有受到鄙视也是有一部分原因吧,毕竟自己曾经年少无知,害怕吃苦选择了一个稍微简单的前端就稀里糊涂的就业了,保命要紧。但是在后来又想改变这个鄙视链,自己就开始了nodejs的学习,python的学习,数据库MongoDB,MySQL,PostgreSQL。学不完,压根学不完。
后面再无尽的内卷中,有的做开发不是自己的路,也想做做管理,毕竟前端做到前端组长就已经是极限了,在公司以java为尊的环境下,想做更高的级别几乎不可能。毕竟自己算是耿直死宅,不善交际,讨不到大领导的喜爱。然后又开始了原型的学习,PMP项目管理证书的考取(进行中),也曾有单独出去做产品的想法,面试过一个,但是与自己的预期薪资相差太大,没去。


作者:Kier
来源:juejin.cn/post/7415654362993639439
收起阅读 »

如果你使用的第三方库有bug,你会怎么办

web
早上好,中午好,晚上好在当今的前端工程化领域,第三方库的使用已经成为标配。然而,不可避免的是,这些库可能会存在bug,或者是库的一些功能并不能满足需要,需要修改库的某个功能,或添加功能。当遇到这种情况时,我们应该如何应对?本文将介绍三种解决第三方库bug的方法...
继续阅读 »

早上好,中午好,晚上好

在当今的前端工程化领域,第三方库的使用已经成为标配。然而,不可避免的是,这些库可能会存在bug,或者是库的一些功能并不能满足需要,需要修改库的某个功能,或添加功能。当遇到这种情况时,我们应该如何应对?本文将介绍三种解决第三方库bug的方法,并重点介绍使用patch-package库来修复bug的全过程。

方法一:提issues给第三方库的作者,让作者修复

这个方式是比较常见的解决方式了,但有几个缺点:

  • 库作者不维护这个库了,那提issues自然就没有人close了,gg
  • 库作者很忙,或者项目缺乏活跃的贡献者,导致问题可能长时间都不懂响应,那如果你这个项目很急的话,那gg
  • bug或者功能的优先级不高,库作者先解决其他高优先级的,或者他不接受你的建议或者及时修复问题,那gg
  • 还有可能出现的沟通成本,以确保库作者完全理解了问题的本质和重要性。

那如果库作者很勤奋,每天都在维护,对issues的问题,都满怀热情的进行解决,那我们可以按照以下流程进行提issues:

  1. 发现bug:在使用第三方库时,发现了一个bug。
  2. 复现bug:在本地环境中尝试复现该bug,并记录详细的复现步骤。
  3. 提交issues:访问第三方库的GitHub仓库,点击“New issue”按钮,填写以下信息:
    • 标题:简洁地描述bug现象。
    • 描述:详细描述bug的复现步骤、预期结果和实际结果。
    • 环境:列出你的操作系统、浏览器版本、库的版本等信息。
  4. 等待回复:作者可能会要求你提供更多信息,或者告诉你解决方案。耐心等待并积极配合。nice

方法二:fork第三方库,修复好bug后,发布到npm,项目下载自己发布的npm包

这个方式也有局限性:

  1. 维护负担:一旦你fork了库,你需要负责维护这个分支,包括合并上游的更新和修复新出现的bug。
  2. 长期兼容性:随着时间的推移,原库和新fork的库可能会出现分歧,使得合并更新变得更加困难。
  3. 版本管理:需要管理自己发布的npm包版本,确保它与其他依赖的兼容性。
  4. 社区隔离:使用自己的fork可能会减少与原社区的合作机会,错过原库的其他改进和特性。

那如果你觉得这个方式很不错,那最佳实践是这样的:

步骤 1: Fork 原始库

  1. 访问原始库的GitHub页面。
  2. 点击页面上的“Fork”按钮,将库复制到你的GitHub账户下。

步骤 2: 克隆你的Fork

git clone https://github.com/your-username/original-repo.git
cd original-repo

步骤 3: 设置上游仓库

git remote add upstream https://github.com/original-owner/original-repo.git

这样当作者更新维护库的时候,可以获取上游仓库的最新更新。

步骤 4: 创建特性分支

git checkout -b fix-bug-branch

步骤 5: 修复Bug

在这个分支上,进行必要的代码更改来修复bug。

步骤 6: 测试更改

在本地环境中测试你的更改,确保修复了bug并且没有引入新的问题。

步骤 7: 提交并推送更改

git add .
git commit -m "Fix bug description"
git push origin fix-bug-branch

步骤 8: 创建Pull Request(可选)

如果你希望原始库接受你的修复,可以向上游仓库创建一个Pull Request。

步骤 9: 发布到NPM

如果原始库没有接受你的PR,或者你需要立即使用修复,可以发布到NPM:

  1. 登录到NPM。
npm login

这个地方有个坑点,就是你使用了npm镜像需要将镜像更改为npm官方仓库:

npm config set registry https://registry.npmjs.org
  1. 修改package.json中的名称,避免与原始库冲突,例如添加你的用户名前缀。
{
"name": "@your-username/original-repo",
// ...
}
  1. 更新版本号。
npm version patch
  1. 发布到NPM。
npm publish

步骤 10: 在你的项目中使用Forked库

在你的项目package.json中,将依赖项更改为你的forked版本。

{
"dependencies": {
"original-repo": "^1.0.0",
"@your-username/original-repo": "1.0.1"
}
}

步骤 11: 维护你的Fork

定期从上游仓库合并更新到你的fork,以保持与原始库的同步。

git checkout master
git pull upstream master
git push origin master

最佳实践总结

  • 保持与上游仓库的同步。
  • 清晰地记录你的更改和发布。
  • 为你的fork创建文档,说明它与原始库的区别。
  • 考虑长期维护策略,如果可能,尽量回归到官方版本。

方法三:使用patch-package库来修复

patch-package 是一个非常有用的 npm 包,它允许我们在没有修改原始 npm 依赖包的情况下,对 npm 依赖进行修复或自定义。这在以下场景中特别有用:

  • 当你发现一个第三方库的 bug,但作者还没有修复它,或者修复后的版本尚未发布。
  • 当你需要对第三方库进行微小的定制,而不想维护一个完整的分支或分叉。

patch-package 的工作原理

patch-package 的工作流程通常如下:

  1. 修改 node_modules 中的依赖包文件。
  2. 运行 patch-package 命令,它会生成一个补丁文件,通常是 .patch 文件,保存在项目根目录下的 patches 文件夹中。
  3. 在 package.json 的 scripts 部分添加一个脚本来应用这些补丁,通常是在 postinstall 阶段。
  4. 将生成的 .patch 文件提交到版本控制系统中。
  5. 当其他开发者运行 npm install 或 yarn 安装依赖时,或者 CI/CD 系统构建项目时,这些补丁会被自动应用。

但使用这种方式也有前提

1. 潜在冲突:如果第三方库的官方更新解决了相同的bug,但采用了不同的方法,那么你的补丁可能会与这些更新冲突

2. 库没有源码:这种方式是在node_modules里对应的包进行修改,如果包是压缩后的,那就没办法改了,所以只能针对node_modules里的包有源码的情况下。

最佳实践:

步骤 1:安装patch-package postinstall-postinstall

postinstall-postinstall,作用是 postinstall 脚本在 Yarn 安装过程中运行。

yarn add patch-package postinstall-postinstall --dev

步骤 2:配置 package.json

在你的 package.json 文件中,添加一个 postinstall 脚本来确保在安装依赖后应用补丁:

"scripts": {
"postinstall": "patch-package"
}

步骤 3:修复依赖包中的 bug

假如vue3有个bug,我们直接在 node_modules/vue/xxx 中修复这个 bug。

步骤 4:创建补丁

修复完成后,我们运行以下命令来生成补丁:

npx patch-package example-lib

这会在项目根目录下创建一个 patches 文件夹,并在其中生成一个名为 vue+3.4.29.patch 的文件(假设vue当前库的版本是3.4.29)。

步骤 5:提交补丁文件到代码库中

现在,我们将 patches 文件夹和里面的 .patch 文件提交到版本控制系统中。

git add patches/example-lib+1.0.0.patch
git commit -m "Add patch for vue3.4.29"
git push

步骤 6:安装依赖并应用补丁

就是其他同事在下载项目或者更新依赖后,postinstall 脚本会自动运行,并应用补丁。

npm install
# 或者
yarn install

当 npm install 或 yarn install 完成后,patch-package 会自动检测 patches 文件夹中的补丁,并将其应用到对应的依赖上。

志哥我想说

遇到第三方库的bug时,我们可以选择提issues、fork并发布自己的npm包,或者使用patch-package进行本地修复。当然你还可以有:

  • 使用替代库
  • 社区支持

每种方法都有其适用场景,根据实际情况选择最合适的方法。希望本文能帮助你更好地应对第三方库的bug问题,或者面试或者技术分享等。


作者:_志哥_
来源:juejin.cn/post/7418797840796254271

收起阅读 »

抖音自动进入直播间的动画挺有意思的,看看有多少种方式可以实现

web
在刷抖音的时候,发现有一个直播的专属导航页签,切换到这个页签之后,刷出来的内容全都是直播,不过都是在“门外”观看,没有进入直播间; 短暂的停留之后,会出现一个自动进入直播间的提示,并且有一个描边动画,动画结束之后,就会进入直播间,今天我就尝试通过多种方式来实现...
继续阅读 »

在刷抖音的时候,发现有一个直播的专属导航页签,切换到这个页签之后,刷出来的内容全都是直播,不过都是在“门外”观看,没有进入直播间;


短暂的停留之后,会出现一个自动进入直播间的提示,并且有一个描边动画,动画结束之后,就会进入直播间,今天我就尝试通过多种方式来实现这个动画效果。



1. 渐变实现


渐变.gif


效果如上图所示,渐变需要使用到的是conic-gradient锥形渐变,文档地址:conic-gradient


代码如下:


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
html,
body {
height: 100%;
margin: 0;
}

body {
display: flex;
justify-content: center;
align-items: center;
background: linear-gradient(90deg, #1F1C2C 0%, #928DAB 100%);
color: #fff;
}

.wrap {
position: relative;
background-color: rgba(255, 255, 255, 0.2);
width: fit-content;
padding: 10px 20px;
border-radius: calc(1em + 10px);
}

/*使用自定义属性来控制进度*/
@property --offset {
syntax: "<length-percentage>";
inherits: false;
initial-value: 0;
}

.wrap.gradient-animation {
overflow: hidden;
/*和普通 css 变量一样使用即可*/
background-image:
conic-gradient(
#fff 0%,
#fff var(--offset),
transparent var(--offset) 100%
);
}

/*需要使用一个遮挡来挡住多余的部分,只保留描边部分*/
.wrap.gradient-animation::before {
content: ' ';
position: absolute;
top: 2px;
left: 2px;
right: 2px;
bottom: 2px;
background: #1F1C2C;
border-radius: inherit;
z-index: 0;
}

.wrap.gradient-animation:hover {
animation: gradient 5s linear 1 forwards;
}

@keyframes gradient {
0% {
--offset: 0;
}
100% {
--offset: 100%;
}
}

</style>
</head>
<body>
<div class="wrap gradient-animation">
<!-- 需要控制层级显示 -->
<span style="position: relative; z-index: 1;">自动进入直播间</span>
</div>
</body>
</html>

conic-gradient的技术细节就不展开了,感兴趣的可以自行查阅文档,这里主要的技术点在于--offset这个自定义属性,因为渐变本身是不支持动画的,所以需要借助这个自定义属性来实现动画效果,文档地址:@property


这里的效果其并不是很理想,因为conic-gradient的渐变是一个圆形的渐变,而实际效果是边框的一个描边,所以需要使用一个遮罩来挡住多余的部分,只保留描边部分。


由于使用了伪元素来实现遮罩,所以还需要控制层级显示,避免遮罩挡住了文字,并且原效果是透明的背景,这里使用遮罩层之后背景就不能是透明的了,而且动画在每一个部分执行的时间都不连贯。


可以说这种方式有很多的局限性,所以我们来看看下一种方式。


2. 渐变加 mask 实现


mask.gif


渐变加mask的实现思路和上面的类似,主要是解决了上面的背景半透明的问题,文档地址:mask


代码如下:


<div class="wrap gradient-mask">
<span style="position: relative; z-index: 1;">自动进入直播间</span>
</div>

.wrap.gradient-mask::before {
content: ' ';
position: absolute;
inset: 0;
background-color: rgba(255, 255, 255, 0.1);
background-image: conic-gradient(
#fff 0%,
#fff var(--offset),
transparent var(--offset) 100%
);
border-radius: inherit;

mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg'><rect width='150' x='1' y='1' height='40' rx='20' fill='transparent' stroke='red' stroke-width='2' stroke-alignment='outside'/></svg>");
mask-size: 100% 100%;
mask-repeat: no-repeat;
}

.wrap.gradient-mask:hover::before {
animation: gradient 5s linear 1 forwards;
}

这里把效果整体迁移到了::before伪元素上,使用mask-image里面加了一个svg来处理描边的问题,这样就不需要使用遮罩来挡住多余的部分,只保留描边部分。


但是这里的问题也很明显,那就是svg并不能很好的响应式,而且因为svg的其他原因,导致描边的边宽有点被裁剪,这里也只是提供一个思路,并不是最佳实践。


3. 使用 svg 实现


svg.gif


上面都已经使用到了svg作为遮罩,那么直接使用svg更简单直接,这种情况个人也比较推荐,代码如下:


<div class="wrap svg">
<svg xmlns="http://www.w3.org/2000/svg">
<rect width="150" x="1" y="1" height="40" rx="20" stroke="rgba(255, 255, 255, 0.1)" stroke-width="2" />
<rect width="150" x="1" y="1" height="40" rx="20" class="rect" />
</svg>
<span style="position: relative; z-index: 1;">自动进入直播间</span>
</div>

.wrap.svg svg {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}

.wrap.svg svg > rect {
width: calc(100% - 2px);
height: calc(100% - 2px);
fill: transparent;
stroke-width: 2;
}

.wrap.svg svg > .rect {
stroke-dasharray: 350;
stroke-dashoffset: 350;
stroke: white;
stroke-width: 2;
}

.wrap.svg:hover svg > .rect {
transition: stroke-dashoffset 5s linear;
stroke-dashoffset: 0;
}

svg的描边效果主要是通过stroke-dasharraystroke-dashoffset来实现的,svg描边效果也是一个非常有趣的实现。


stroke-dasharray是用来控制虚线的,这个值越大,虚线之间的间隔也越大,大到一定程度这个虚线就正好将整个形状包裹住。


stroke-dashoffset是用来控制虚线的偏移量,当这个值等于stroke-dasharray的时候,虚线就会完全消失,等于0的时候,虚线就会完全显示。


根据svg的特性,描边是在形状的外部,所以最外层有一个半透明的边框路径显示的并不完全,需要通过一些技巧来处理。


image.png


所以上面的代码会有两个rect,一个是用来描边的,一个是用来做半透明的边框的,这样就可以实现一个比较完美的描边动画效果。


但是上面的描边起点和终点都是在左上角,如果需要在中间的话,可以通过path来实现,感兴趣的可以自行尝试,这里提供path的代码:


<svg viewBox="0 0 600 150" xmlns="http://www.w3.org/2000/svg">
<path d="M 100 0 L 500 0 A 50 50 0 1 1 500 150 L 100 150 A 50 50 0 1 1 100 0 Z" fill="transparent" stroke="rgba(255, 255, 255, 0.1)" stroke-width="2" />
</svg>

path需要微调,因为没有贴边,可以使用一些在线网站来进行调整,比如:svg-path-editor,可自行探索。


总结


这一篇文章通过多种方式来实现一个描边动画效果,主要技术有:



  1. conic-gradient锥形渐变,本身渐变是不支持动画的,但是我们可以通过自定义属性来实现动画效果;

  2. mask遮罩,mask本身其实和背景图的使用方式差不多,但是mask主要用来遮挡多余的部分,在这里我们使用mask来遮挡主要部分,只保留描边部分来实现动画效果;

  3. svg描边,svg描边是一个非常有趣的技术,通过stroke-dasharraystroke-dashoffset来实现描边动画效果,这种方式是最推荐的。


当然肯定还有其他的方式来实现这个效果,这里只是提供了一些思路,希望对大家有所帮助。


作者:田八
来源:juejin.cn/post/7420814883576414259
收起阅读 »

抛弃 `!important` 吧,一个更友好的技巧让你的 CSS 优先级变大

web
原文:Double your specificity with this one weird trick 在一个理想的世界里,我们的 CSS 代码组织得井井有条,易于维护。然而,现实往往大相径庭。你的 CSS 代码是完美的,但其他人那些烦人的 CSS 可能会...
继续阅读 »

image.png



原文:Double your specificity with this one weird trick



在一个理想的世界里,我们的 CSS 代码组织得井井有条,易于维护。然而,现实往往大相径庭。你的 CSS 代码是完美的,但其他人那些烦人的 CSS 可能会与你的风格冲突,或者应用了你不需要的样式。


此外,你可能也无法修改那些 CSS。也许它来自你正在使用的 UI 库,也许是一些第三方的小组件。


更糟糕的是,HTML 也不受你控制,添加一些额外的 classid 属性来覆盖样式也并不可行。


不知不觉中,你被卷入了一场 CSS 优先级之战。你的选择器需要优先于他们的选择器。开发者很容易被『诱惑 😈』去使用 !important,但你知道这是不好的实践,我们能不能有一种更优雅的方式来实现我们覆盖的诉求?


本文将教给你一个技巧,可以用一种不是很 hacky 的方式应对这些情况 👩‍💻。


示例 🔮


假设你正在开发一个网站,该网站有一个新闻订阅表单。它包含一个复选框,但复选框的位置有点偏。你需要修正这个问题,但注册表单是一个嵌入到页面上的第三方组件,你无法直接修改它的 CSS。


通过浏览器检查复选框,确定只要改变它的 top 位置即可。当前的位置是通过选择器.newsletter .newsletter__checkbox .checkbox__icon 设置的,它的权重为 (0,3,0)


image.png


一开始你可能会使用相同的选择器来修改 top 值:


/* 覆盖新闻通讯复选框的顶部位置 */
.newsletter .newsletter__checkbox .checkbox__icon {
top: 5px;
}

当 CSS 的顺序是固定的,并且你可以保证你的 CSS 规则一定在他们的后面的情况下,这足够了。因为『后来居上』:即如果有多个相同的 CSS 选择器选择了同一个DOM元素,那么最后出现的将“获胜”


然而,大多数时候你无法保证代码顺序。此时你需要增加选择器的优先级。你可以在 DOM 中寻找一些额外的类名,一般从父元素中添加:


/* 更多的类名!权重现在是(0,4,0) */
.parent-thing .newsletter .newsletter__checkbox .checkbox__icon {
top: 5px;
}

或者你发现这个元素恰好是一个 ,可以将其加入选择器提高优先级:


/* 权重现在是 (0,3,1) */
.newsletter .newsletter__checkbox span.checkbox__icon {
top: 5px;
}

但所有这些方法都有副作用,都会使你的代码变得脆弱。如果 .parent-thing 突然不见了呢,比如你升级了某个外部依赖(比如 antd 😅)?或者如果 .checkbox__iconspan 改成了不同的元素怎么办?突然间,你的高优先级选择器什么也选不到了!


当浏览器计算 CSS 选择器优先级时,它们本质上是在计算你组合了多少 ID元素或等效选择器。实际上可以多次重复同一个选择器,每次重复都会增加权重CSS 选择器 Level 4 规范 写到:



CSS 选择器允许多次出现相同的简单选择器,而且可以增加权重。



因此,你可以通过重复(三次、四次……)相同的选择器提高权重:


/* 双重 .checkbox__icon!权重现在是 (0,4,0) */
.newsletter .newsletter__checkbox .checkbox__icon.checkbox__icon {
top: 5px;
}


注意 .checkbox__icon.checkbox__icon 中没有空格!它是一个选择器,因为你针对的是具有那个类的单个元素



现在你可以简单地重复几次选择器来提升优先级!



译者注:该技巧其实在 MDN !important 章节 有示例(以下示例重复了3次#myElement):


#myElement#myElement#myElement .myClass.myClass p:hover {
color: blue;
}

p {
color: red !important;
}


在 HTML 中重复 🚫


注意,这个技巧只在 CSS 中有效!在 HTML 中重复相同的类名对优先级没有任何影响。


<div class="badger badger badger">
Mushroom!
div>


总结 🎯



CSS 可以多次重复同一个选择器,每次重复都会增加权重 🏋️‍♂️



这种 CSS 技巧是否有点 hack?也许是。然而我认为它让我们:



  • 避免诉诸于 !important

  • 『就近原则』提高可读性:重复多次的选择器,这样代码的意图对读者来说更清晰

  • 这种模式让你很容易在代码中找到其他人的 CSS 覆盖,如果不再需要我们可以放心删除


只要你不过度使用它,我认为这是一个完全合法且 Robust 的技巧至少相比我们之前学会的所有技巧,下次处理棘手的覆盖三方样式情况时可以考虑用一用。


是否还有更好的解决办法?其实有,@layer 是官方推荐的最佳实践但是兼容性不好 Chrome>=99,而且使用场景有限。


作者:Legend80s
来源:juejin.cn/post/7411686792342618153
收起阅读 »

想弄一个节日头像,结果全是广告!带你用 Canvas 自己制作节日头像

web
一、为什么要自己制作节日头像?很多人想为节日换上特别的头像,尤其是在国庆这样的节日气氛中,给自己的WX头像添加节日元素成为了不少人的选择。最初我也以为只需通过一些WX公众号简单操作,就能轻松给头像加上节日图案,比如国庆节、圣诞节头像等。然而,实际体验却很糟糕—...
继续阅读 »

一、为什么要自己制作节日头像?

很多人想为节日换上特别的头像,尤其是在国庆这样的节日气氛中,给自己的WX头像添加节日元素成为了不少人的选择。最初我也以为只需通过一些WX公众号简单操作,就能轻松给头像加上节日图案,比如国庆节、圣诞节头像等。然而,实际体验却很糟糕——广告无处不在!每一步操作几乎都被强制插入广告打断,不仅浪费时间,体验也非常差。

为了避开这些广告,享受更自由、更个性化的制作过程,我决定分享一个不用看广告的好方法:使用 Canvas 自己动手制作一个专属的节日头像!

二、源码 & 在线体验

👀 在线体验 | 📖 源码地址 | 欢迎start、欢迎共同交流

注意事项

  • demo_admin 为体验用户,项目一人一号 ,如果体验人数过多,请自行选中项目中的登录方式进行登录
  • 本文源码在 yf/ yf-vue-admin / src / views / demo / festival-avatar

三、 实现的功能与后续发展

在解决了广告干扰的问题后,我通过 Canvas 实现了多个实用功能,让大家可以轻松制作个性化的节日头像:

  1. 头像裁剪功能
  2. 头像与框架的拼接
  3. 头像框透明度调节
  4. 头像框颜色过滤(可自定义头像框)
  5. 后续发展:Fabric.js 自定义贴图功能
  6. 后续发展:更新更多节日的头像 & 贴图

四、当前素材及投稿征集

展示目前头像框素材,也欢迎大家投稿,我也会陆续更进头像框(项目中头像框已进行分类,这里为了方便展示,也可以自定义头像框)

1. 头像框

image.png

2. 贴图

五、代码实现

整体逻辑非常简单 : 头像 + 头像框 = 所需头像

1. 头像裁剪功能

页面部分

  • 使用 :width 来根据设备类型设置宽度(device === DeviceEnum.MOBILE ? '95%' : '42%')。
  •  用于图像裁剪功能。
  • 底部有文件上传和旋转按钮。
<template>
<el-dialog
v-model="dialog.visible"
:width="device === DeviceEnum.MOBILE ? '95%' : '42%'"
class="festival-avatar-upload-dialog"
destroy-on-close
draggable
overflow
title="上传头像"
>

<div style="height: 45vh">
<vue-cropper
ref="cropper"
:autoCrop="true"
:centerBox="true"
:fixed="true"
:fixedNumber="[1,1]"
:img="imgTemp"
:outputType="'png'"
/>

div>
<template #footer>
<div class="festival-avatar-dialog-options">
<el-button @click="uploadAvatar">
<el-icon style="margin-right: 5px;">
<UploadFilled/>
el-icon>
上传头像
<input ref="avatarUploaderRef" accept="image/*" class="avatar-uploader" name="file" type="file"
@
change="handleFileChange">

el-button>
<el-button @click="rotateLeft">
<el-icon><RefreshLeft/>el-icon>
el-button>
<el-button @click="rotateRight">
<el-icon><RefreshRight/>el-icon>
el-button>
<el-button type="primary" @click="submitForm">提 交el-button>
div>
template>
el-dialog>
template>

代码逻辑部分(核心部分)

  • imgTemp 用来存储上传的临时图片数据。
  • handleFileChange 处理文件上传事件,校验文件类型并使用 FileReader 读取图片数据,并本地存储
  • rotateLeft 和 rotateRight 分别用于左旋和右旋图片。
// 省略部分属性定义
const imgTemp = ref<string>("") // 临时图片数据
const cropper = ref(); // 裁剪实例
const avatarUploaderRef = ref<HTMLInputElement | null>(null); // 上传头像 input 引用

// 上传头像功能
function uploadAvatar() {
avatarUploaderRef.value?.click(); // 点击 input 触发上传
}

// 上传文件前校验 : 略

// 处理文件上传
function handleFileChange(event: Event) {
const input = event.target as HTMLInputElement;
if (input.files && input.files[0]) {
const file = input.files[0];

if (!beforeAvatarUpload(file)) return;

const reader = new FileReader();
reader.onload = (e: ProgressEvent) => {
imgTemp.value = e.target?.result as string; // 读取的图片数据赋给 imgTemp
};
reader.readAsDataURL(file);
}
}

// 旋转功能
function rotateLeft() {
cropper.value?.rotateLeft();
}

const rotateRight = () => {
cropper.value?.rotateRight();
};

实现效果图

image.png

2. 头像与头像框合并

页面部分 (核心部分)

  • compositeAvatar 为组合头像 , avatarData 为头像数据 ,compositeCanvas 头像 Canvas , avatarFrameCanvas 头像框 Canvas
  • 在没有 compositeAvatar 的时候展示 avatarData , 没有 avatarData 提示用户点击 PLUS 的图片


<div class="festival-avatar-preview">
<div class="festival-avatar-preview__plus" @click="openAvatarDialog">

<img v-if="compositeAvatar" :src="compositeAvatar" alt="合成头像"/>


<img v-else-if="avatarData" :src="avatarData" alt="头像"/>


<el-icon v-else color="#8c939d" size="28">
<Plus>Plus>
el-icon>
div>
div>



<canvas ref="compositeCanvas" style="display: none;">canvas>

<canvas ref="avatarFrameCanvas" style="display: none;">canvas>

逻辑部分 (核心部分)

  • 通过 toDataURL 转换后合成为组合头像 , 通过 drawImage 合并 avatarFrameCanvas 和上文中avatarData 进行合并
const context = getCanvasContext(compositeCanvas); // 获取主 Canvas 上下文
const frameContext = getCanvasContext(avatarFrameCanvas); // 获取头像框 Canvas 上下文

// 省略非相关逻辑 , context 中写入 avatarData 内容

// 将处理后的头像框绘制到主 Canvas 上
context.drawImage(avatarFrameCanvas.value, 0, 0, avatarImg.width, avatarImg.height);

// 将合成后的图片转换为数据 URL
compositeAvatar.value = compositeCanvas.value!.toDataURL('image/png');

实现效果

当我们点击头像框的时候,合并头像

QQ录屏20240928110300.gif

3. 头像框透明度调整

页面部分 与上文一样 , 通过调整 avatarFrameCanvas 的内容而调整头像框

逻辑部分 (核心部分)

通过 context  globalAlpha 属性设置全局透明度。

setFrameOpacity(frameContext, frameOpacity.value); // 设置头像框透明度

/**
* 设置 Canvas 的透明度
*
@param context Canvas 的 2D 上下文
*
@param opacity 透明度值
*/

function setFrameOpacity(context: CanvasRenderingContext2D, opacity: number) {
context.globalAlpha = opacity; // 设置全局透明度
}

实现效果

QQ录屏20240928110300.gif

4. 头像框颜色过滤

页面部分 与上文一样 , 通过调整 avatarFrameCanvas 的内容而调整头像框

服务对象 我们有自定义头像框功能,但是自己找的头像很容易有白底的问题,所以更新此功能。

逻辑部分 (核心部分)

filterColorToTransparent 函数

  • 作用:将与指定颜色相近的像素变为透明。

colorDistance 函数

  • 作用:计算两种颜色(RGB 值)之间的距离。距离越小,颜色越相似。
  • 计算方式:使用欧几里得距离公式计算两个 RGB 颜色向量之间的距离,如果距离小于一定的容差值(tolerance),则认为两种颜色足够接近。 image.png

rgbStringToArray 函数

  • 作用:将 RGB 字符串(例如 'rgb(255,255,255)')转换为包含 r, g, b 值的对象。
/**
* 将指定颜色过滤为透明
*
@param context Canvas 的 2D 上下文
*
@param width Canvas 宽度
*
@param height Canvas 高度
*/

function filterColorToTransparent(context: CanvasRenderingContext2D, width: number, height: number) {
const frameImageData = context.getImageData(0, 0, width, height);
const data = frameImageData.data;

const targetColor = rgbStringToArray(colorFilter.value.color); // 将目标颜色转换为 RGB 数组

// 遍历所有像素点
for (let i = 0; i < data.length; i += 4) {
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
const distance = colorDistance({r, g, b}, targetColor); // 计算当前颜色与目标颜色的差距

// 如果颜色差距在容差范围内,则将其透明度设为 0
if (distance <= colorFilter.value.tolerance) {
data[i + 3] = 0; // 设置 alpha 通道为 0(透明)
}
}

// 将处理后的图像数据放回 Canvas
context.putImageData(frameImageData, 0, 0);
}

/**
* 计算两种颜色之间的距离(欧几里得距离)
*
@param color1 颜色 1,包含 r、g、b 属性
*
@param color2 颜色 2,包含 r、g、b 属性
*
@returns number 返回颜色之间的距离
*/

function colorDistance(color1: { r: number; g: number; b: number }, color2: { r: number; g: number; b: number }) {
return Math.sqrt(
(color1.r - color2.r) ** 2 +
(color1.g - color2.g) ** 2 +
(color1.b - color2.b) ** 2
);
}

/**
* 将 RGB 字符串转换为 RGB 数组
*
@param rgbString RGB 字符串(例如 'rgb(255,255,255)')
*
@returns 返回一个包含 r、g、b 值的对象
*/

function rgbStringToArray(rgbString: string) {
const result = rgbString.match(/\d+/g)?.map(Number) || [0, 0, 0]; // 匹配并转换 RGB 值
return {r: result[0], g: result[1], b: result[2]}; // 返回 r、g、b 对象
}

实现效果

  1. 在 Canva 自己制作一个头像

image.png

  1. 上传头像框,制作头像 ( 过滤白色 )

QQ图片20240707160518.gif

六、结束语

开发很容易,祝大家各个节日快乐 !!!


作者:翼飞
来源:juejin.cn/post/7419223935005605914

收起阅读 »

我的车被划了,看我实现简易监控拿捏他!node+DroidCam+ffmpeg

web
某天我骑着我的小电驴下班回到我那出租屋,习惯性的看了一眼我那停在门口的二手奥拓,突然发现有点不对劲,走近一看引擎盖上多了一大条划痕,顿时恶向胆边生,是谁!!!为此我决定用现有条件做一套简易的监控系统来应对日后的情况,于是有了这篇文章。 一 准备工作 由于是要...
继续阅读 »

某天我骑着我的小电驴下班回到我那出租屋,习惯性的看了一眼我那停在门口的二手奥拓,突然发现有点不对劲,走近一看引擎盖上多了一大条划痕,顿时恶向胆边生,是谁!!!为此我决定用现有条件做一套简易的监控系统来应对日后的情况,于是有了这篇文章。



一 准备工作


由于是要做监控,硬件是必不可少的,所以首先想到的就是闲置的手机了,找了一台安卓8.1的古董出来,就决定是你了。因为之前在公司使用过
DroidCam这款软件用来进行webRTC的开发,所以这次就顺理成章的装了这款软件,连上家里的wifi后打开就相当于有了一台简易的视频服务器。那么硬件搞定了,接下来的就是软件了。梳理下来的话只有以下几点了



  1. 拉取DroidCam上的视频流

  2. 将拉取到的内容做存储


由于本人是个前端,因此这里就顺理成章的使用node来作为软件实现的第一方案了。


二 获取视频流,啊?怎么是这玩意儿


image.png
怎么获取它传过来的视频流呢?看了一下上打开的软件界面,发现给了两个地址,ip端口 和 ip端口/video,不出意料的这两个里面肯定是有能用的东西,挨个打开后发现不带video的地址是相当于一个控制台,带video的是视频的接口地址。那就好办了,我满怀激动的以为一切都很容易的时候,打开控制台一看,咦,这是啥玩意儿?它的所谓的视频是现在img标签里的,这在之前可是没见过哦,再看一眼接口地址,咦,这是一个长链接?点开详情看了一眼,好吧,又学到新东西了。它的Content-Type是multipart/x-mixed-replace;boundary='xxxx',这是啥呀,搜索了一下资料后如下。



MJPEG(Motion Joint Photographic Experts Gr0up)是一种视频压缩格式,其中每一帧图像都分别使用JPEG编码,不使用帧间编码,压缩率通常在20:1-50:1范围内。它的原理是把视频镜头拍成的视频分解成一张张分离的jpg数据发送到客户端。当客户端不断显示图片,即可形成相应的图像。



大致意思懂了,就是这就是一张张的图像呗。后面又看了一下服务端是如何生成这玩意儿的,这里就不细说了。


知道了是啥东西,那就要想怎么把它搞出来了


三 使用ffmpeg 获取每一帧


ffmpeg相信大家都不陌生,使用前需要先在本机上安装,安装方法的话这里就不赘述了。
安装后在系统环境变量高级设置中,增加path变量的值为ffmpeg在电脑上的路径。后续就可以使用了。


image.png


image.png
随便新建一个js文件


    const fs = require('fs')
const path = require('path')
//截取的视频帧的存储路径和命名方式
const outputFilePattern = path.join(__dirname + '/newFrame', 'd.jpg');
//视频服务器地址
    const mjpegUrl = 'http://192.168.2.101:4747/video?1920x1080';
//通过child_process的spawn调用外部文件,执行ffmpeg,并传入参数
//下方代码执行后在连接到服务后不手动停止的情况下期间会不断的在指定目录下生成获取到的图片
    const ffmpeg = require('child_process').spawn('ffmpeg', [
        '-i',
        mjpegUrl,
        '-vf',
        'fps=24',//设置帧率
        '-q:v',
        '1', // 调整此值以更改输出质量(较低的值表示更好的质量)  
        outputFilePattern // %d 将被替换为帧编号  
    ], { windowsHide: true });//调用时不显示cmd窗口

//错误监听
ffmpeg.on('error', function (err) {
        throw err;
    });

//关闭监听
    ffmpeg.on('close', function (code) {
        console.log('ffmpeg exited with code ' + code);
    });

//数据
    ffmpeg.stderr.on('data', function (data) {
        console.log('stderr: ' + data.toString());
//执行合并图片操作
//....
    });

上述代码运行后如果能正常连接上服务的话你会在指定目录下看到不断生成的图片。


四 将图片生成为视频


光有图片是不够的,我最终的预期是生成视频以供查看,所以添加以下的代码将图片合并为视频


//上面生成图片后存放的位置
let filePath = path.join(__dirname + '/newFrame', 'd.jpg');
let comd = [
        '-framerate',
        '24',
        '-i',
        filePath,
        '-c:v',
        'libx264',
        '-pix_fmt',
        'yuv420p',
        `${__dirname}/outVideo/${new Date().getFullYear()}-${(new Date().getMonth() + 1).toString().padStart(2, '0')}-${new Date().getDate().toString().padStart(2, '0')}_${new Date().getHours().toString().padStart(2, '0')}${new Date().getMinutes().toString().padStart(2, '0')}${new Date().getSeconds().toString().padStart(2, '0')}.mp4`

    ]
    const ffmpeg = require('child_process').spawn('ffmpeg', comd,{ windowsHide: true });
    ffmpeg.on('close', function (code) {
        console.log('ffmpeg exited with code ' + code);
        console.log('任务执行结束,开始删除')
    });

我这里定的是每2000张图片组合成视频,因此将第三步中的


ffmpeg.stderr.on('data', function (data) {
        console.log('stderr: ' + data.toString());
    });

改成


image.png


ffmpeg.stderr.on('data', function (data) {
        console.log('stderr: ' + data.toString());
//打印结果 =>>frame= 1474 fps= 14 q=1.0 size=N/A time=00:01:01.41 bitrate=N/A speed=0.57x

        let arr = data.toString().split('fps')

        try {
//获取frame数量用来计数
            frameCount = arr[0].split('=')[1].trim()

            console.log(frameCount)

//为什么这里用大于而不是等于呢,因为获取frame可能不是总会计数到我们想要的值,踩过坑,注意
            if (frameCount > 2000) {

                console.log('数量满足')
//关闭本次获取流
                ffmpeg.kill('SIGKILL');

                //这里执行合并文件的操作
//...

            }

        } catch (e) { }

    });

到这里如果你一切顺利的话就能在指定的文件夹里看到合并完成后的MP4视频了。


image.png


五 合并完成后删除上次获取的图片


第四步


 ffmpeg.on('close', function (code) {
        console.log('ffmpeg exited with code ' + code);
        console.log('任务执行结束,开始删除')
    });

改为


 ffmpeg.on('close', async function (code) {
        console.log('ffmpeg exited with code ' + code);
        console.log('任务执行结束,开始删除')
try {
        await fsE.emptyDir('your folderPath');
        console.log(`已清空文件夹`);
       
           //重新执行第二步
//...
     
    } catch (err) {
        console.error(`无法清空文件夹: ${err}`);
    }
    });

这里的fsEconst fsE = require('fs-extra');,需要安装并且导入


到这里为止,整个基本的流程就完成了


六 总结


整个程序到目前为止已经能基本满足我的需求,但是还存在不足,比如频繁的往硬盘上读写文件、容错处理等等,后续我的想法是把图片保存到内存中,在满足条件后再写入硬盘,减少文件的I/O操作,加入对人体的识别,接入之前写过的邮件通知,有人靠近自动记录时间点并发送到邮箱。当然了,我这个肯定比不了市面上的那些成熟产品,就是自己写着好玩的,请各位大佬轻喷!有错误和意见欢迎指正!


image.png


作者:长长韭韭
来源:juejin.cn/post/7419887017164767268
收起阅读 »

强大的一笔的Hermes引擎,是如何让你的 App 脱颖而出的!

web
Hermes 是一款由 Facebook 开源的轻量级 JavaScript 引擎,专门针对 React Native 应用进行了优化。与传统的 JavaScript 引擎(例如 JavaScriptCore 和 V8)相比,Hermes 具有以下优势: 启动...
继续阅读 »

Hermes 是一款由 Facebook 开源的轻量级 JavaScript 引擎,专门针对 React Native 应用进行了优化。与传统的 JavaScript 引擎(例如 JavaScriptCore 和 V8)相比,Hermes 具有以下优势:


启动时间更快: Hermes 使用预编译字节码(AOT),而不是即时编译(JIT),这可以显著缩短应用的启动时间。


更小的内存占用: Hermes 的体积小巧,占用内存更少,这对于移动设备尤为重要。


更小的应用包大小: 由于 Hermes 的体积小巧,因此可以减小 React Native 应用的包大小。


Hermes


高效的性能的原因


先看下面这幅图:
AOT


Hermes Engine 的设计初衷是为了优化 React Native 应用的性能。它通过对 JavaScript 代码的提前编译,将其转化为字节码,从而减少了运行时的解析时间。这种预编译机制使得应用启动速度显著提升,用户体验更加流畅。


CPU 的利用率


在 CPU 利用率方面,Hermes 也有显著的优势。
通过优化 JavaScript 执行和垃圾回收过程,Hermes 提供了更快的启动时间和更低的内存占用。研究表明,使用 Hermes 的应用在性能上有显著提升,用户体验更加流畅


内存占用和包大小优化


内存优化


Hermes 采用了优化的内存管理机制,如内存池和高效的垃圾回收算法,能够减少应用在运行时的内存占用。这对于资源受限的移动设备尤为关键。使用 Hermes 编译的应用包体积通常更小。这对于需要快速下载安装的应用很有优势,也有助于提高应用在应用商店的排名。上图就是 Stock RN 应用基于 Hermes 引擎的内存优化后的实际效果。


良好的兼容性


Hermes 提供了强大的调试工具,帮助开发者快速定位和解决问题。其集成的调试功能使得开发者能够实时监控应用的性能,及时发现并修复潜在的性能瓶颈。
Hermes 得到了 Facebook 和开源社区的广泛支持,拥有丰富的文档和活跃的开发者社区。开发者可以轻松获取资源和支持,促进了 Hermes 的快速发展和普及。


一些小众第三方库不支持 Hermes 引擎


虽然,大多数比较有名的第三方库都是支持 Hermes引擎的,但是有一个小小的问题,有些比较小众的第三方库,是不支持 hermes 引擎的,这个时候,你可需要想办法自己改写下这个第三方库,或者给作者提建议。
如,腾讯云 cos ,React Native 的库,就是不支持 Hermes 引擎的。相关issue 在这里:


github.com/TencentClou…


image.png


不过,对于这个问题,你完全可以使用 restful api 呀,所以,解决问题的方式太多了,不要因为一个小众的三方库而放弃恐怖的性能提升,多少有点不值当


实际应用案例

许多知名应用已经开始采用 Hermes Engine,以提升其性能。例如,Facebook 和 Instagram 的部分功能已成功迁移至 Hermes,用户反馈显示应用的启动时间和流畅度均有显著改善。这些成功案例进一步验证了 Hermes 的强大实力。


如何用上Hermes 引擎


如果你在使用 Expo 做移动端跨端研发,那么恭喜你,默认就是使用的 Hermes 引擎,无需任何配置,如果你想显式配置,也无妨,甚至你可以指定 ios 使用jsc 引擎。


{
"expo": {
"jsEngine": "hermes",
"ios": {
"jsEngine": "jsc"
}
}
}

如果你使用的是 React Native 0.70 或更高版本,则 Hermes 引擎将默认启用。如果你使用的是较早版本的 React Native,则可以按照 React Native 文档 中的说明启用 Hermes 引擎。配置简单的就啰嗦。小伙伴们,React Native 要吊打 Flutter了 吗?拍拍砖?


作者:brzhang
来源:juejin.cn/post/7394095950383743015
收起阅读 »

Vite 为何短短几年内变成这样?

web
给前端以福利,给编程以复利。大家好,我是大家的林语冰。 00. 观前须知 在 Web 开发领域,Vite 如今已如雷贯耳。 自 2020 年 4 月发布以来,Vite 的人气蒸蒸日上。目前 Vite 在 GitHub 上的收藏数量已超过 64k,每周下载量超过...
继续阅读 »

给前端以福利,给编程以复利。大家好,我是大家的林语冰。


00. 观前须知


在 Web 开发领域,Vite 如今已如雷贯耳。


自 2020 年 4 月发布以来,Vite 的人气蒸蒸日上。目前 Vite 在 GitHub 上的收藏数量已超过 64k,每周下载量超过 1200 万次,现在为 Nuxt、Remix、Astro 等大多数开源框架提供支持。


尽管众口嚣嚣,我们意识到许多开发者可能仍然不熟悉 Vite 是什么鬼物,也不熟悉 Vite 在推动现代 Web 框架和工具的开发中扮演的重要角色。


在本文中,我们将科普 Vite 的知识储备,以及 Vite 如何在短短几年后发展成为现代 Web 的重量级角色。


00-trend.png



免责声明


本文属于是语冰的直男翻译了属于是,略有删改,仅供粉丝参考。英文原味版请传送 What is Vite (and why is it so popular)?



01. Vite 是什么鬼物?


Vite 的发音为 /vit/,在法语中是“快速”或“迅捷”的意思,不得不说 Vite 名副其实。


简而言之,Vite 是一种现代 JS 构建工具,为常见 Web 模式提供开箱即用的支持和构建优化,兼具 rollup 的自由度和成熟度。


Vite 还与 esbuild 和原生 ES 模块强强联手,实现快速无打包开发服务器。


Vite 是由“Vue 之父”尤雨溪(Evan You)构思出来的,旨在通过减少开发者在启动开发服务器和处理文件编辑后重载时遭遇的性能瓶颈,简化打包过程。


02. Vite 的核心特性


00-wall.png


运行 Vite 时,你会注意到的第一个区别在于,开发服务器会即时启动。


这是因为,Vite 采用按需方法将你的应用程序提供给浏览器。Vite 不会首先打包整个源码,而是响应浏览器请求,将你编写的模块即时转换为浏览器可以理解的原生 ESM 模块。


Vite 为 TS、PostCSS、CSS 预处理器等提供开箱即用的支持,且可以通过不断增长的插件生态系统进行扩展,支持所有你喜欢的框架和工具。


每当你在开发期间更改项目中的任意文件时,Vite 都会使用应用程序的模块图,只热重载受影响的模块(HMR)。这允许开发者预览他们的更改,及其对应用程序的影响。


Vite 的 HMR 速度惊人,可以让编辑器自动保存,并获得类似于在浏览器开发工具中修改 CSS 时的反馈循环。


Vite 还执行 依赖预构建(dependency pre-bundling)。在开发过程中,Vite 使用 esbuild 来打包你的依赖并缓存,加快未来服务器的启动速度。


此优化步骤还有助于加快 lodash 等导出许多迷你模块的依赖的加载时间,因为浏览器只加载每个依赖的代码块(chunk)。这还允许 Vite 在依赖中支持 CJS 和 UMD 代码,因为它们被打包到原生 ESM 模块中。


当你准备好部署时,Vite 将使用优化的 rollup 设置来构建你的应用程序。Vite 会执行 CSS 代码分割,添加预加载指令,并优化异步块的加载,无需任何配置。


Vite 提供了一个通用的 rollup 兼容插件 API,适用于开发和生产,使你可以更轻松地扩展和自定义构建过程。


03. Vite 的优势


使用 Vite 有若干主要优势,包括但不限于:


03-1. 开源且独立


Vite 由开源开发者社区“用爱发电”,由来自不同背景的开发者团队领导,Vite 核心仓库最近贡献者数量已突破 900 人。


Vite 得到积极的开发和维护,不断实现新功能并解决错误。


03-2. 本地敏捷开发


开发体验是 Vite 的核心,每次点击保存时,你都能感受到延迟。我们常常认为重载速度是理所当然的。


但随着您的应用程序增长,且重载速度逐渐停止,你将感恩 Vite 几乎能够保持瞬间重载,而无论应用程序大小如何。


03-3. 广泛的生态系统支持


Vite 的方案人气爆棚,大多数框架和工具都默认使用 Vite 或拥有一流的支持。通过选择使用 Vite 作为构建工具,这些项目维护者可以在它们之间共享一个统一基建,且随着时间的推移共同改良 Vite。


因此,它们可以花更多的时间开发用户需要的功能,而减少重新造轮子的时间。


03-4. 易于扩展


Vite 对 rollup 插件 API 的押注得到了回报。插件允许下游项目共享 Vite 核心提供的功能。


我们有很多高质量的插件可供使用,例如 vite-plugin-pwavite-imagetools


03-5. 框架构建难题中的重要角色


Vite 是现代元框架构建的重要组成部分之一,这是一个更大的工具生态系统的一部分。


Volar 提供了在代码编辑器中为 Vue、MDX 和 Astro 等自定义编程语言构建可靠且高性能的编辑体验所需的工具。Volar 允许框架向用户提供悬停信息、诊断和自动补全等功能,并共享 Volar 作为为它们提供支持的通用基建。


另一个很好的例子是 Nitro,它是一个服务器工具包,用于创建功能齐全的 Web 服务器,开箱即用地支持每个主要部署平台。Nitro 是一个与框架无关的库 UnJS 的奇妙集合的一部分。


04. Vite 的未来


evan-vite5.png


在最近的 ViteConf 大会的演讲中,尤雨溪表示,虽然 Vite 取得了巨大进展,但仍面临一些已知的问题和挑战。


Vite 目前使用 rollup 进行生产构建,这比 esbuildBun 等原生打包器慢得多。


Vite 还尽可能减少开发和生产环境之间的不一致性,但考虑到 rollupesbuild 之间的差异,某些不一致性无法避免。


尤雨溪现在领导一个新团队开发 rolldown,这是一个基于 Rust 的 rollup 移植,在 “JS 氧化编译器 OXC”之上构建了最大的兼容性。


这个主意是用 rolldown 替代 Vite 中的 rollupesbuild。Vite 将拥有一个单独基建,兼具 rollup 的自由度和 esbuild 的速度,消除不一致性,使代码库更易于维护,并加快构建时间。


rolldown 目前处于早期阶段,但已经显示出有希望的结果。rolldown 现已开源,rolldown 团队正在寻找贡献者来辅助实现这一愿景。


与此同时,Vite 团队在每个版本中不断改良 Vite。这项工作从上游的为 Vitest 和 Nuxt Dev SSR 提供​​动力的引擎 vite-node 开始,现已发展成为框架作者对 Vite API 的完整修订版。


新版 Environment API 预计在 Vite 6 中发布,这将是自 Vite 2 发布以来 Vite 最大的变化之一。这将允许在任意数量的环境中通过 Vite 插件管道运行代码,解锁对 worker、RSC 等的一流支持。


Vite 正在开辟一条前进的道路,并迅速成为 JS 生态系统事实上的构建工具。


参考文献



粉丝互动


本期话题是:如何评价人气爆棚的 Vite,你最喜欢或期待 Vite 的哪个功能?你可以在本文下方自由言论,文明科普。


欢迎持续关注“前端俱乐部”,给前端以福利,给编程以复利。


坚持阅读的小伙伴可以给自己点赞!谢谢大家的点赞,掰掰~


26-cat.gif


作者:前端俱乐部
来源:juejin.cn/post/7368836713965486119
收起阅读 »

因为编辑器没做草稿,老板崩溃了。。。

web
现场 大家好,我是多喝热水。 事情是这样的,那天晚上老板在群里吐槽说他在手机上写了将近 1000 字的评论不小心点了一下黑屏,然后内容就突然没了,如下: 原来是我们编辑器没有做草稿能力,导致关闭后原本编辑的内容都消失了,确实这个体验不太好,想想怎么把这里优化...
继续阅读 »

现场


大家好,我是多喝热水。


事情是这样的,那天晚上老板在群里吐槽说他在手机上写了将近 1000 字的评论不小心点了一下黑屏,然后内容就突然没了,如下:



原来是我们编辑器没有做草稿能力,导致关闭后原本编辑的内容都消失了,确实这个体验不太好,想想怎么把这里优化一下。


调研


像我们平时用得比较多的社交平台,比如某音、某书等,先从它们的评论区入手,看看主流的平台是怎么做的。


1)某音


某音的效果是,在某条视频下评论后划走,再划回来编辑的内容就不在了,看样子是没有做草稿能力,如图:



2)某书


某书的效果是,在某个笔记下面评论然后划走,再回来的时候内容是还在的。而且每条评论都有自己的编辑态,互不干扰,如图:



好看真好看,呸好用真好用,既然体验上某书更好,我决定仿照某书的方案来实现。



既然要做成某书的效果,那我们就需要解决两个问题


1)他们评论区草稿内容是怎么存的?


2)存在哪里了?


内容怎么存?


先说说我的看法,如果要让每条评论都拥有独立的编辑态,那么肯定是需要一个唯一标识的,那我能想到的唯一标识就是ID


内容存哪里?


存后端还是存前端?存前端的话又存哪里?这里我简单总结了一下:


存后端

优势:数据真正的持久化、安全性高


缺陷:需要网络连接,依赖后端,开发成本高


存前端

优势:简单易用、性能好、脱机可用


缺陷:无法真正持久化、存储空间有限、不安全


方案选择


回归到需求本身,我们不需要实时性多么高,所以存前端就已经可以满足我们的需求了。


但在前端存储还有一个存储空间问题,需要考虑一下存储内容的有效时间,过期了就得删除,不然会存在很多冗余数据,所以我们又面临新的问题,前端用什么来存


浏览器常用的存储方案:cookie、localStorage、sessionStorage


1)cookie 是可以设置过期时间的,但如果存 cookie,那它的容量只有5kb,有点太小了,并且每次发请求 cookie 都会被携带上,无疑是增加了额外的带宽开销


2)sessionStorage 存储空间最大支持5MB,但窗口被关闭后数据就过期了,有效期仅仅是窗口会话期间,万一用户不小心关闭了窗口,数据也消失了,所以这个方案也不太妥当


3)相比之下 localStorage 的容量也有 5MB,足够大,但是它本身不支持设置过期时间(默认永久有效),需要人为去控制,好在这个成本并不高,综合之下我们还是选择存 localStorage 了


开发


选好方案后,就可以开始动手开发了!先把支持控制过期时间 的 localStorage 逻辑写一下。


写之前我们需要考虑一下代码的复用性,因为在我们网站中,有很多地方都用到了编辑器,比如评论区、交流内容发布等,如果每一处都写一遍的话,那这个代码就太冗余了,所以将它封装为一个 hook 是一个不错的选择,代码如下:


import { CACHE_TYPE, EXPIRES_TIME } from './constants';

/**
* 缓存数据
* @param key
* @returns
*/

export default function useCache(key: string = CACHE_TYPE.ESSAY_CONTENT) {
/**
* 删除缓存数据
*/

const removeCache = () => {
localStorage.removeItem(key);
};

/**
* 设置缓存数据
* @param data 数据内容
* @param expires 过期时间(毫秒)
*/

const setCache = (data: any, expires: number = EXPIRES_TIME) => {
const cacheData = {
value: data,
expires: expires ? Date.now() + expires : null, // 计算过期时间戳
};
localStorage.setItem(key, JSON.stringify(cacheData));
};

/**
* 获取缓存数据
* @returns 缓存数据或 null
*/

const getCache = () => {
const cachedString = localStorage.getItem(key);
if (!cachedString) {
return null;
}
const cachedObject = JSON.parse(cachedString);
// 检查是否设置了过期时间并且是否已经过期
if (cachedObject.expires && Date.now() > cachedObject.expires) {
removeCache(); // 删除已过期的数据
return null;
}
return cachedObject.value;
};

return { removeCache, setCache, getCache };
}

简单解释一下上面的代码:


1)useCache 函数主要接收一个 KEY,删除、获取、设置草稿数据都会用到这个 KEY,且我们保证它是唯一的


2)在设置需要缓存内容时(setCache),会给出一个 expires 的参数用于控制该数据的有效时间


3)获取数据的时候会校验一下有效时间,如果已经过期了则返回 null


在编辑器中应用


最后我们需要在用到编辑器的地方使用这个 hook。


可能有些小伙伴会觉得我们网站中用到编辑器的地方很多,这一步才是一个大工程,其实不然,因为我们所有用到编辑器的地方都是用的同一个组件,我们需要改动的地方就是那个公共的编辑器组件!


这时候封装带来的便捷性就体现的淋漓尽致,省去了不少时间用来摸鱼!!!


改动代码如下(伪代码):


type GeneralContentEditorProps = {
targetId?: string; // 缓存ID
// 省略不相关代码...
};

/**
* 通用的内容编辑器
* @param props
* @returns
*/

export default function GeneralContentEditor({
targetId,
// 省略不相关代码...
}: GeneralContentEditorProps
) {
// 省略不相关代码...
const [content, setContent] = useState('')
const { getCache, setCache, removeCache } = useCache(targetId);

useEffect(() => {
setContent(getCache() ?? '')
}, [])
}

简单解释一下上面的代码:


1)给编辑器新增了一个属性 targetId,这个 targetId 用来作为缓存的唯一标识,由使用方提供给我们


2)初始化的时候去调 getCache 函数读取缓存的数据


3)有内容变更的时候调 setCache 函数去更新缓存的数据


到这里流程已经跑通了,但还缺少重要的一步,需要定时清空一下缓存的数据,因为现在的逻辑是如果我们不主动去获取这个数据,它还是占据着存储空间


清空冗余数据


其实我们也不需要专门去写定时器来清空,只需要在编辑器初始化的时候去检测一遍就可以,所以代码还需加点料,如下图:



到这一步编辑器草稿能力就完善的差不多了,已经能够正常使用了,我们看看效果,如下:



nice,没有什么问题,好了,我要去摸鱼了 😋



作者:上班多喝热水
来源:juejin.cn/post/7419598991119532043
收起阅读 »

老板想集成地图又不想花钱,于是让我...

web
前言 在数字化时代,地图服务已成为各类应用的标配,无论是导航、位置分享还是商业分析,地图都扮演着不可或缺的角色。然而,高质量的地图服务往往伴随着不菲的授权费用。公司原先使用的是国内某知名地图服务,但随着业务的扩展和成本的考量,老板决定寻找一种成本更低的解决方案...
继续阅读 »

前言


在数字化时代,地图服务已成为各类应用的标配,无论是导航、位置分享还是商业分析,地图都扮演着不可或缺的角色。然而,高质量的地图服务往往伴随着不菲的授权费用。公司原先使用的是国内某知名地图服务,但随着业务的扩展和成本的考量,老板决定寻找一种成本更低的解决方案。于是,我们的目光转向了免费的地图服务——天地图。


天地图简介


天地图(lbs.tianditu.gov.cn/server/guid…
是中国领先的在线地图服务之一,提供全面的地理信息服务。它的API支持地理编码、逆地理编码、周边搜索等多种功能,且完全免费。这正是我们需要的。


具体实现代码


为了将天地图集成到我们的系统中,我们需要进行一系列的开发工作。以下是实现过程中的关键代码段。


1. 逆地理编码


逆地理编码是将经纬度转换为可读的地址。在天地图中,这一功能可以通过以下代码实现:


public static MapLocation reverseGeocode(String longitude, String latitude) {
Request request = new Request();
LocateInfo locateInfo = GCJ02_WGS84Utils.gcj02_To_Wgs84(Double.valueOf(latitude), Double.valueOf(longitude));
longitude = String.valueOf(locateInfo.getLongitude());
latitude = String.valueOf(locateInfo.getLatitude());
String postStr = String.format(REVERSE_GEOCODE_POST_STR, longitude, latitude);
String encodedPostStr = null;
try {
encodedPostStr = URLEncoder.encode(postStr, "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
String url = REVERSE_GEOCODE_URL + "?tk=" + TK + "&type=" + GEOCODE + "&postStr=" + encodedPostStr;
request.setUrl(url);
Response<String> response = HttpClientHelper.getWithoutUserAgent(request, null);
if (response.getSuccess()) {
String body = response.getBody();
JSONObject jsonObject = JSON.parseObject(body);
String status = jsonObject.getString("status");
if (!"0".equals(status)) {
return null;
}
JSONObject resultObject = jsonObject.getJSONObject("result");
MapLocation mapLocation = new MapLocation();
String formattedAddress = resultObject.getString("formatted_address");
mapLocation.setAddress(formattedAddress);
String locationStr = resultObject.getString("location");
JSONObject location = JSON.parseObject(locationStr);
String lon = location.getString("lon");
String lat = location.getString("lat");
locateInfo = GCJ02_WGS84Utils.wgs84_To_Gcj02(Double.valueOf(lat), Double.valueOf(lon));
lon = String.valueOf(locateInfo.getLongitude());
lat = String.valueOf(locateInfo.getLatitude());
mapLocation.setLongitude(lon);
mapLocation.setLatitude(lat);
JSONObject addressComponent = resultObject.getJSONObject("addressComponent");
String address = addressComponent.getString("address");
mapLocation.setName(address);
mapLocation.setCity(addressComponent.getString("city"));
return mapLocation;
}
return null;
}

2. 周边搜索


周边搜索允许我们根据一个地点的经纬度搜索附近的其他地点。实现代码如下:


public static List<MapLocation> nearbySearch(String query, String longitude, String latitude, String radius) {
LocateInfo locateInfo = GCJ02_WGS84Utils.gcj02_To_Wgs84(Double.valueOf(latitude), Double.valueOf(longitude));
longitude = String.valueOf(locateInfo.getLongitude());
latitude = String.valueOf(locateInfo.getLatitude());
Request request = new Request();
String longLat = longitude + "," + latitude;
String postStr = String.format(NEARBY_SEARCH_POST_STR, query, Integer.valueOf(radius), longLat);
String encodedPostStr = null;
try {
encodedPostStr = URLEncoder.encode(postStr, "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
String url = SEARCH_URL + "?tk=" + TK + "&type=" + QUERY + "&postStr=" + encodedPostStr;
request.setUrl(url);
Response<String> response = HttpClientHelper.getWithoutUserAgent(request, null);
List<MapLocation> list = new ArrayList<>();
if (response.getSuccess()) {
String body = response.getBody();
JSONObject jsonObject = JSON.parseObject(body);
JSONObject statusObject = jsonObject.getJSONObject("status");
String infoCode = statusObject.getString("infocode");
if (!"1000".equals(infoCode)) {
return new ArrayList<>();
}
String resultType = jsonObject.getString("resultType");
String count = jsonObject.getString("count");
if (!"1".equals(resultType) || "0".equals(count)) {
return new ArrayList<>();
}
JSONArray poisArray = jsonObject.getJSONArray("pois");
for (int i = 0; i < poisArray.size(); i++) {
JSONObject poiObject = poisArray.getJSONObject(i);
MapLocation mapLocation = new MapLocation();
mapLocation.setName(poiObject.getString("name"));
mapLocation.setAddress(poiObject.getString("address"));
String lonlat = poiObject.getString("lonlat");
String[] lonlatArr = lonlat.split(",");
locateInfo = GCJ02_WGS84Utils.wgs84_To_Gcj02(Double.valueOf(lonlatArr[1]), Double.valueOf(lonlatArr[0]));
String lon = String.valueOf(locateInfo.getLongitude());
String lat = String.valueOf(locateInfo.getLatitude());
mapLocation.setLongitude(lon);
mapLocation.setLatitude(lat);
list.add(mapLocation);
}
}
return list;
}

3. 文本搜索


文本搜索功能允许用户根据关键词搜索地点。实现代码如下:


public static List<MapLocation> searchByText(String query, String mapBound) {
Request request = new Request();
String postStr = String.format(SEARCH_BY_TEXT_POST_STR, query, mapBound);
String encodedPostStr = null;
try {
encodedPostStr = URLEncoder.encode(postStr, "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
String url = SEARCH_URL + "?tk=" + TK + "&type=" + QUERY + "&postStr=" + encodedPostStr;
request.setUrl(url);
Response<String> response = HttpClientHelper.getWithoutUserAgent(request, null);
List<MapLocation> list = new ArrayList<>();
if (response.getSuccess()) {
String body = response.getBody();
JSONObject jsonObject = JSON.parseObject(body);
JSONObject statusObject = jsonObject.getJSONObject("status");
String infoCode = statusObject.getString("infocode");
if (!"1000".equals(infoCode)) {
return new ArrayList<>();
}
String resultType = jsonObject.getString("resultType");
String count = jsonObject.getString("count");

if (!"1".equals(resultType) || "0".equals(count)) {
return new ArrayList<>();
}
JSONArray poisArray = jsonObject.getJSONArray("pois");
for (int i = 0; i < poisArray.size(); i++) {
JSONObject poiObject = poisArray.getJSONObject(i);
MapLocation mapLocation = new MapLocation();
mapLocation.setName(poiObject.getString("name"));
mapLocation.setAddress(poiObject.getString("address"));
String lonlat = poiObject.getString("lonlat");
String[] lonlatArr = lonlat.split(",");
LocateInfo locateInfo = GCJ02_WGS84Utils.wgs84_To_Gcj02(Double.valueOf(lonlatArr[1]), Double.valueOf(lonlatArr[0]));
String lon = String.valueOf(locateInfo.getLongitude());
String lat = String.valueOf(locateInfo.getLatitude());
mapLocation.setLongitude(lon);
mapLocation.setLatitude(lat);
list.add(mapLocation);
}
}
return list;
}

4. 坐标系转换


由于天地图使用的是WGS84坐标系,而国内常用的是GCJ-02坐标系,因此我们需要进行坐标转换。以下是坐标转换的工具类:



/**
* WGS-84:是国际标准,GPS坐标(Google Earth使用、或者GPS模块)
* GCJ-02:中国坐标偏移标准,Google Map、高德、腾讯使用
* BD-09:百度坐标偏移标准,Baidu Map使用(经由GCJ-02加密而来)
* <p>
* 这些坐标系是对真实坐标系统进行人为的加偏处理,按照特殊的算法,将真实的坐标加密成虚假的坐标,
* 而这个加偏并不是线性的加偏,所以各地的偏移情况都会有所不同,具体的内部实现是没有对外开放的,
* 但是坐标之间的转换算法是对外开放,在网上可以查到的,此算法的误差在0.1-0.4之间。
*/

public class GCJ02_WGS84Utils {

public static double pi = 3.1415926535897932384626;//圆周率
public static double a = 6378245.0;//克拉索夫斯基椭球参数长半轴a
public static double ee = 0.00669342162296594323;//克拉索夫斯基椭球参数第一偏心率平方

/**
* 从GPS转高德
* isOutOfChina 方法用于判断经纬度是否在中国范围内,如果不在中国范围内,则直接返回原始的WGS-84坐标。
* transformLat 和 transformLon 是辅助函数,用于进行经纬度的转换计算。
* 最终,wgs84ToGcj02 方法返回转换后的GCJ-02坐标系下的经纬度。
*/

public static LocateInfo wgs84_To_Gcj02(double lat, double lon) {
LocateInfo info = new LocateInfo();
if (isOutOfChina(lat, lon)) {
info.setChina(false);
info.setLatitude(lat);
info.setLongitude(lon);
} else {
double dLat = transformLat(lon - 105.0, lat - 35.0);
double dLon = transformLon(lon - 105.0, lat - 35.0);
double radLat = lat / 180.0 * pi;
double magic = Math.sin(radLat);
magic = 1 - ee * magic * magic;
double sqrtMagic = Math.sqrt(magic);
dLat = (dLat * 180.0) / ((a * (1 - ee)) / (magic * sqrtMagic) * pi);
dLon = (dLon * 180.0) / (a / sqrtMagic * Math.cos(radLat) * pi);
double mgLat = lat + dLat;
double mgLon = lon + dLon;
info.setChina(true);
info.setLatitude(mgLat);
info.setLongitude(mgLon);
}
return info;
}

//从高德转到GPS
public static LocateInfo gcj02_To_Wgs84(double lat, double lon) {
LocateInfo info = new LocateInfo();
LocateInfo gps = transform(lat, lon);
double lontitude = lon * 2 - gps.getLongitude();
double latitude = lat * 2 - gps.getLatitude();
info.setChina(gps.isChina());
info.setLatitude(latitude);
info.setLongitude(lontitude);
return info;
}

// 判断坐标是否在国外
private static boolean isOutOfChina(double lat, double lon) {
if (lon < 72.004 || lon > 137.8347)
return true;
if (lat < 0.8293 || lat > 55.8271)
return true;
return false;
}

//转换
private static LocateInfo transform(double lat, double lon) {
LocateInfo info = new LocateInfo();
if (isOutOfChina(lat, lon)) {
info.setChina(false);
info.setLatitude(lat);
info.setLongitude(lon);
return info;
}
double dLat = transformLat(lon - 105.0, lat - 35.0);
double dLon = transformLon(lon - 105.0, lat - 35.0);
double radLat = lat / 180.0 * pi;
double magic = Math.sin(radLat);
magic = 1 - ee * magic * magic;
double sqrtMagic = Math.sqrt(magic);
dLat = (dLat * 180.0) / ((a * (1 - ee)) / (magic * sqrtMagic) * pi);
dLon = (dLon * 180.0) / (a / sqrtMagic * Math.cos(radLat) * pi);
double mgLat = lat + dLat;
double mgLon = lon + dLon;
info.setChina(true);
info.setLatitude(mgLat);
info.setLongitude(mgLon);

return info;
}

//转换纬度所需
private static double transformLat(double x, double y) {
double ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y
+ 0.2 * Math.sqrt(Math.abs(x));
ret += (20.0 * Math.sin(6.0 * x * pi) + 20.0 * Math.sin(2.0 * x * pi)) * 2.0 / 3.0;
ret += (20.0 * Math.sin(y * pi) + 40.0 * Math.sin(y / 3.0 * pi)) * 2.0 / 3.0;
ret += (160.0 * Math.sin(y / 12.0 * pi) + 320 * Math.sin(y * pi / 30.0)) * 2.0 / 3.0;
return ret;
}

//转换经度所需
private static double transformLon(double x, double y) {
double ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1
* Math.sqrt(Math.abs(x));
ret += (20.0 * Math.sin(6.0 * x * pi) + 20.0 * Math.sin(2.0 * x * pi)) * 2.0 / 3.0;
ret += (20.0 * Math.sin(x * pi) + 40.0 * Math.sin(x / 3.0 * pi)) * 2.0 / 3.0;
ret += (150.0 * Math.sin(x / 12.0 * pi) + 300.0 * Math.sin(x / 30.0 * pi)) * 2.0 / 3.0;
return ret;
}
}

结论


通过上述代码,我们成功地将天地图集成到了我们的系统中,不仅满足了功能需求,还大幅降低了成本。这一过程中,我们深入理解了地图服务的工作原理,也提升了团队的技术能力。


注意事项



  • 确保在使用天地图API时遵守其服务条款,尤其是在商业用途中。

  • 由于网络或其他原因,天地图API可能存在访问延迟或不稳定的情况,建议在生产环境中做好异常处理和备用方案。

  • 坐标系转换是一个复杂的过程,确保使用可靠的算法和工具进行转换,以保证定位的准确性。


通过这次集成,我们不仅为公司节省了成本,还提升了系统的稳定性和用户体验。在未来的开发中,我们将继续探索更多高效、低成本的技术解决方案。


作者:JustinNeil
来源:juejin.cn/post/7419524888041472009
收起阅读 »

js中的finally一定会执行吗?

web
背景 在我们程序开发中,我们的代码会出现这种或那种的错误,我们使用try...catch进行捕获。如果需要不管是成功还是失败都需要执行,我们可能需要finally。 那么有一个问题,无论是否发生错误,在finally中的代码一定会执行吗? 下面我们看一个案例:...
继续阅读 »

背景


在我们程序开发中,我们的代码会出现这种或那种的错误,我们使用try...catch进行捕获。如果需要不管是成功还是失败都需要执行,我们可能需要finally


那么有一个问题,无论是否发生错误,在finally中的代码一定会执行吗?


下面我们看一个案例:


1. 案例


场景:请求一个接口,如果接口没有正确返回,我们使用try...finally包裹代码,代码如下:


function getMember(num) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (num === 1) {
resolve(num)
}
if (num === 0) {
reject()
}
}, 2000)
})
}

async function init() {
try {
console.log('打印***start')
await getMember(0)
console.log('打印***end')
} catch (err) {
console.log('打印***err')
} finally {
console.log('打印***finally')
}
}

结果如下:


image.png


上述案例中,如果请求传入的num由另外一个接口返回,num的值不是0或者1,上述的getMember就一直处于pengding状态,接下来的finally也不会执行。


我们也可以这样理解,当在处理Promise问题时,我们需要确保Promise始终得到结果,不管是成功还是失败。


上述代码可以完善如下:


function getMember(num) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (num === 1) {
resolve(num);
} else if (num === 0) {
reject(new Error('Num is 0'));
} else {
// 默认情况,也解决Promise
resolve('Some default value');
}
}, 2000);
});
}

async function init() {
try {
console.log('打印***start');
const result = await getMember(2); // 传递一个非0非1的值
console.log('打印***end', result);
} catch (err) {
console.log('打印***err', err);
} finally {
console.log('打印***finally'); // 这行总是会被执行
}
}

init();

修改后的例子中,无论num的值是什么,Promise都会被解决(要么通过resolve,要么通过reject),,确保Promise被正常处理,才能确保finally执行。


2. try...catch注意点


2.1 仅对运行时的 error 有效


要使得 try...catch 能工作,代码必须是可执行的。换句话说,它必须是有效的 JavaScript 代码。


如果代码包含语法错误,那么 try..catch 将无法正常工作,例如含有不匹配的花括号:


try {
{
{
} catch (err) {
alert("引擎无法理解这段代码,它是无效的");
}

结果如下:


image.png


JavaScript 引擎首先会读取代码,然后运行它。在读取阶段发生的错误被称为“解析时间(parse-time)”错误,并且无法恢复(从该代码内部)。这是因为引擎无法理解该代码。


所以,try...catch 只能处理有效代码中出现的错误。这类错误被称为“运行时的错误(runtime errors)”,有时被称为“异常(exceptions)”。


2.2 try...catch 同步执行


如果在定时代码中发生异常,例如在 setTimeout 中,则 try...catch 不会捕获到异常:


try {
setTimeout(function () {
noSuchVariable; // 脚本将在这里停止运行
}, 1000);
} catch (err) {
alert("不工作");
} finally {
console.log('打印***finally')
}

结果如下:


image.png


因为 try...catch 包裹了计划要执行的函数,该函数本身要稍后才执行,这时引擎已经离开了 try...catch 结构。


为了捕获到计划的(scheduled)函数中的异常,那么 try...catch 必须在这个函数内:


try {
setTimeout(function () {
try {
noSuchVariable; // 脚本将在这里停止运行
} catch (error) {
console.log(error)
}
}, 1000);
} catch (err) {
alert("不工作");
} finally {
console.log('打印***finally')
}


结果如下:
image.png


总结


在使用try...catch...finally的时候,无论是否发生异常(即是否执行了catch块),finally块中的代码总是会被执行,除非在trycatchfinally块中发生了阻止程序继续执行的情况(如Promsie一直处理pending状态)。


如有错误,请指正O^O!


作者:一诺滚雪球
来源:juejin.cn/post/7419524503200677898
收起阅读 »

iframe嵌入页面实现免登录思路(以vue为例)

web
背景: 最近实现一个功能需要使用iframe嵌入其它系统内部的一个页面,但嵌入后出现一个问题,就是一打开这个页面就会自动跳转到登录页,原因是被嵌入系统没有登录(没有token)肯定不让访问内部页面的,本文就是解决这个问题的。 附带相关文章:只要用iframe必...
继续阅读 »

背景:


最近实现一个功能需要使用iframe嵌入其它系统内部的一个页面,但嵌入后出现一个问题,就是一打开这个页面就会自动跳转到登录页,原因是被嵌入系统没有登录(没有token)肯定不让访问内部页面的,本文就是解决这个问题的。


附带相关文章:只要用iframe必遇到这6种"坑"之一(以Vue为例)


选择的技术方案:


本地系统使用iframe嵌入某个系统内部页面,那就证明被嵌入系统是安全的可使用的,所以可以通过通讯方式带一个token过去实现免登录,我用vue项目作为例子具体如下:


方法一通过url传:


// 发送方(本地系统):
<div>
<iframe :src="url" id="childFrame" importance="high" name="demo" ></iframe>
</div>

//被嵌入页面进行接收
url = `http://localhost:8080/dudu?mytoken={mytoken}` //

接收方:直接使用window.location.search接收,然后对接收到的进行处理


注意:



  • 如果使用这个方法最好把token加密一下,要不然直接显示在url是非常危险的行为,所以我更推荐下面方法二

  • 上面接收方要在在APP.vue文件的created生命周期接收,在嵌入页面接收是不行的,这里与VUE的执行流程有关就不多说了


方法二通过iframe的通讯方式传(推荐):

// 发送方(本地系统):
var params = {
type: "setToken",
token: "这是伟过去的token"
}
window.parent.postMessage(params, "*");



// 接收方(被嵌入系统):在APP.vue文件的created生命周期接收
window.addEventListener( "message",
(e)=>{
if(e.data.type === 'setToken'){
//这里拿到token,然后放入缓存实在免登录即可
}
}
false);


注意: 上面接收方要在在APP.vue文件的created生命周期接收,在嵌入页面接收是不行的,这里与VUE的执行流程有关就不多说了


补充:



看着评论不少疑问,所以我就按我个人的思路去补充回答一下,但不绝对实用,欢迎互相指导



(1)如果不同源系统怎么办?


正常使用上述方法二进行通迅,但不带token过去因为不同源根本无法通用,直接在被嵌入页面请求token,这个要和后端沟通好怎么获取


// 接收方(被嵌入系统):在APP.vue文件的created生命周期接收
window.addEventListener( "message",
(e)=>{
if(e.data.type === 'setToken'){
//这里在被嵌入页面请求接口获取这个系统的token,然后放到缓存中免登录
}
}
false);

(2)如果两个系统保存token字段相当怎么办?


例如:主系统本地存储的token叫:access_token , iframe嵌入的系统采用的token也叫:access_token



这分为两种情况:(1)同源并且token字段相同 (2)不同源并且token字段相当



(1)同源并且token字段相同

这种情况同源+token字段相同,根本不会出现需要登录的情况,因为同一个浏览器缓存都能拿到并且又是通用token


(2)不同源并且token字段相当

这种情况只有嵌入系统本地系统两种情况它们并不会同时出现的,那么只要判断当前是那个情况就行,然后给对应的token


方案:请求在拦截器那里判断当前请求来自那个系统的页面,然后给对应的token


例如:两个系统都要传my_token字段给后端,如果都放缓存就会覆盖,所以直接本地系统放到token1缓存,嵌入系统放到token2缓存,拦截器判断后如果本来系统页面 my_token=token1,嵌入页面 my_token=token2


作者:天天鸭
来源:juejin.cn/post/7350876924393209894
收起阅读 »

啊,富文本没做安全处理被XSS攻击了啊

web
前言 相信很多前端小伙伴项目中都用到了富文本,但你们有没有做防XSS攻击处理?最近的项目由于比较紧急我也没有处理而是直接正常使用,但公司内部有专门的安全部门针对测试,然后测出来富文本被XSS攻击了,而且危险级别为高。 啊这....,那我就去解决一下吧,顺便从X...
继续阅读 »

前言


相信很多前端小伙伴项目中都用到了富文本,但你们有没有做防XSS攻击处理?最近的项目由于比较紧急我也没有处理而是直接正常使用,但公司内部有专门的安全部门针对测试,然后测出来富文本被XSS攻击了,而且危险级别为高。


啊这....,那我就去解决一下吧,顺便从XSS和解决方案两个角度记录到下来毕竟好久没更新文章了。


先说说什么是XSS攻击?


简述XSS全称Cross-Site Scripting也叫跨站脚本攻击,是最最最常见的网络安全漏洞,其实就是攻击者在受害者的浏览器中注入恶意脚本执行。这种攻击通常发生在 Web 应用程序未能正确过滤用户输入的情况下,导致恶意脚本被嵌入到合法的网页中。
执行后会产生窃取信息、篡改网页、和传播病毒与木马等危害,后果相当严重。


XSS又有三大类


1、存储型 XSS即Stored XSS


恶意的脚本被放置在目标服务器上面,通过正常的网页请求返回给用户端执行。


例如 在观看某个私人博客评论中插入恶意脚本,当其他用户访问该页面时,脚本会执行危险操作。


2、反射型 XSS即Reflected XSS


恶意的脚本通过 URL 参数或一些输入的字段传递给目标的服务器,用户在正常请求时会返回并且执行。


例如 通过链接中的参数后面注入脚本,当用户点击此链接时,脚本就会在用户的浏览器中执行危险操作。


3、DOM 基于的 XSS即DOM-based XSS


恶意的脚本利用 DOM(Document Object Model)操作来修改页面内容。
这种类型的 XSS 攻击不涉及服务器端的代码操作,仅仅是通过客户端插入 JavaScript 代码实现操作。


富文本就是属于第一种,把脚本藏在代码中存到数据库,然后用户获取时会执行。


富文本防XSS的方式?


网上一大堆不明不白的方法还有各种插件可以用,但其实自己转义一下就行,根本不需要复杂化。


当我们不做处理时传给后台的富文本数据是这样的。


image.png
上面带有标签,甚至有srcscript之类的操作,在里面放一些脚本真的太简单了。


因此,我们创建富文本成功提交给后台的时候把各种<>/\之类危险符号转义成指定的字符就能防止脚本了。


如下所示,方法参数value就是要传递给后台的富文本内容。


  export const getXssFilter = (value: string): string => {
// 定义一个对象来存储特殊字符及其对应的 HTML 实体
const htmlEntities = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
'\'': '&#39;',
'\\': '&#92;',
'|': '&#124;',
';': '&#59;',
'$': '&#36;',
'%': '&#37;',
'@': '&#64;',
'(': '&#40;',
')': '&#41;',
'+': '&#43;',
'\r': '&#13;',
'\n': '&#10;',
',': '&#44;',
};

// 使用正则表达式替换所有特殊字符
let result = value.replace(/[&<>"'\\|;$%@()+,]/g, function (match) {
return htmlEntities[match] || match;
});

return result;
};


此时传给后台的富文本参数是这样的,把敏感符号全部转义。


image.png


但展现给用户看肯定要看正常的内容啊,这里就要把内容重新还原了,这步操作可以在前端完成,也可以在后端完成。


如果是前端完成可以用以下方法把获取到的数据进行转义。


// 还原特殊字符
export const setXssFilter = (input) => {
return input
.replace(/&#124;/g, '|')
.replace(/&amp;/g, '&')
.replace(/&#59;/g, ';')
.replace(/&#36;/g, '$')
.replace(/&#37;/g, '%')
.replace(/&#64;/g, '@')
.replace(/&#39;/g, '\'')
.replace(/&quot;/g, '"')
.replace(/&#92;/g, '\\')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&#40;/g, '(')
.replace(/&#41;/g, ')')
.replace(/&#43;/g, '+')
.replace(/&#13;/g, '\r')
.replace(/&#10;/g, '\n')
.replace(/&#44;/g, ',');
}

但是。。。。


上面只适合使用于纯富文本的场景,如果在普通文本的地方回显会依然触发危险脚本。如下所示


image.png


其实直接转义后不还原即可解决,但由于是富文本这种情况比较特殊情况,不还原就失去文本样式了,怎么办??


最终解决方案是对部分可能造成XSS攻击的特殊字符和标签进行转义处理,例如:script、iframe等。


示例代码


  export const getXssFilter = (value: string): string => {
// 定义一个对象来存储特殊字符及其对应的 HTML 实体
const htmlEntities = {
'&': '&amp;',
'\'': '&#39;',
'\r': '&#13;',
'\n': '&#10;',
'script': '&#115;&#99;&#114;&#105;&#112;&#116;',
'iframe': '&#105;&#102;&#114;&#97;&#109;&#101;',
// 'img': '&#105;&#109;&#103;',
'object': '&#111;&#106;&#115;&#116;',
'embed': '&#101;&#109;&#98;&#101;&#100;',
'on': '&#111;&#110;',
'javascript': '&#106;&#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;',
'expression': '&#101;&#120;&#112;&#114;&#101;&#115;&#115;&#115;&#105;&#111;&#110;',
'video': '&#118;&#105;&#100;&#101;&#111;',
'audio': '&#97;&#117;&#100;&#105;&#111;',
'svg': '&#115;&#118;&#103;',
'background-image': '&#98;&#97;&#99;&#107;&#103;&#114;&#111;&#117;&#110;&#100;-&#105;&#109;&#97;&#103;&#101;',
};

// 使用正则表达式替换所有特殊字符
let result = value.replace(/[&<>"'\\|;$%@()+,]/g, function (match) {
return htmlEntities[match] || match;
});

// 额外处理 `script`、`iframe`、`img` 等关键词
result = result.replace(/script|iframe|object|embed|on|javascript|expression|background-image/gi, function (match) {
return htmlEntities[match] || match;
});

return result;
};

效果只会对敏感部分转义


image.png
但这种方案不用还原转义,因为做的针对性限制。


小结


其实就是对特殊符号转换后还原的思路,相当的简单。


如果那里写的不好或者有更好的建议,欢迎大佬指点啦。


作者:天天鸭
来源:juejin.cn/post/7415911762128404480
收起阅读 »

现在前端组长都是这样做 Code Review

web
前言 Code Review 是什么? Code Review 通常也简称 CR,中文意思就是 代码审查 一般来说 CR只关心代码规范和代码逻辑,不关心业务 但是,如果CR的人是组长,建议有时间还是看下与自己组内相关业务,能避免一些生产事故的发生 作为前端组长...
继续阅读 »

前言


Code Review 是什么?


Code Review 通常也简称 CR,中文意思就是 代码审查


一般来说 CR只关心代码规范和代码逻辑,不关心业务


但是,如果CR的人是组长,建议有时间还是看下与自己组内相关业务,能避免一些生产事故的发生


作为前端组长做 Code Review 有必要吗?


主要还是看公司业务情况吧,如果前端组长需求不多的情况,是可以做下CR,能避免一些生产事故



  • 锻炼自己的 CR 能力

  • 看看别人的代码哪方面写的更好,学习总结

  • 和同事交流,加深联系

  • 你做了 CR,晋升和面试,不就有东西吹了不是


那要怎么去做Code Review呢?


可以从几个方面入手



  • 项目架构规范

  • 代码编写规范

  • 代码逻辑、代码优化

  • 业务需求


具体要怎么做呢?


传统的做法是PR时查看,对于不合理的地方,打回并在PR中备注原因或优化方案


每隔一段时间,和组员开一个简短的CR分享会,把一些平时CR过程中遇到的问题做下总结


当然,不要直接指出是谁写出的代码有问题,毕竟这不是目的,分享会的目的是交流学习


人工CR需要很大的时间精力,与心智负担


随着 AI 的发展,我们可以借助一些 AI 来帮我们完成CR


接下来,我们来看下,vscode中是怎么借助 AI 工具来 CR


安装插件 CodeGeex
image-20240723191918678.png


新建一个项目


mkdir code-review
cd code-review

创建 test.js 并用 vscode 打开


cd .>test.js
code ./

image-20240723192853589.png


编写下 test.js


function checkStatus() {
if (isLogin()) {
if (isVip()) {
if (isDoubleCheck()) {
done();
} else {
throw new Error("不要重复点击");
}
} else {
throw new Error("不是会员");
}
} else {
throw new Error("未登录");
}
}

这是连续嵌套的判断逻辑,要怎么优化呢?


侧边栏选择这个 AI 插件,选择我们需要CR的代码


输入 codeRiview,回车


动画.gif


我们来看下 AI 给出的建议


image-20240723194729540.png


AI 给出的建议还是很不错的,我们可以通过更多的提示词,优化它给出的修改建议,这里就不过多赘述了


通常我们优化这种类型的代码,基本优化思路也是,前置校验逻辑,正常逻辑后置


除了CodeGeex外,还有一些比较专业的 codeRiview 的 AI 工具


比如:CodeRabbit


那既然都有 AI 工具了,我们还需要自己去CR 吗?


还是有必要的,借助 AI 工具我们可以减少一些阅读大量代码环节,提高效率,减少 CR 的时间


但是仍然需要我们根据 AI 工具的建议进行改进,并且总结,有利于拓宽我们见识,从而写出更优质的代码


具体 CR 实践


判断逻辑优化


1. 深层对象判空


// 深层对象
if (
store.getters &&
store.getters.userInfo &&
store.getters.userInfo.menus
) {}

// 可以使用 可选链进行优化
if (store?.getters?.userInfo?.menus) {}

2. 空函数判断


优化之前


props.onChange && props.onChange(e)

支持 ES11 可选链写法,可这样优化,js 中需要这样,ts 因为有属性校验,可以不需要判断,当然也特殊情况


props?.onChange?.(e)

老项目,不支持 ES11 可以这样写


const NOOP = () => 8
const { onChange = NOOP } = props
onChange(e)

3. 复杂判断逻辑抽离成单独函数


// 复杂判断逻辑
function checkGameStatus() {
if (remaining === 0 ||
(remaining === 1 && remainingPlayers === 1) ||
remainingPlayers === 0) {
quitGame()
}
}

// 复杂判断逻辑抽离成单独函数,更方便阅读
function isGameOver() {
return (
remaining === 0 ||
(remaining === 1 && remainingPlayers === 1) ||
remainingPlayers === 0
);
}

function checkGameStatus() {
if (isGameOver()) {
quitGame();
}
}

4. 判断处理逻辑正确的梳理方式


// 判断逻辑不要嵌套太深
function checkStatus() {
if (isLogin()) {
if (isVip()) {
if (isDoubleCheck()) {
done();
} else {
throw new Error('不要重复点击');
}
} else {
throw new Error('不是会员');
}
} else {
throw new Error('未登录');
}
}

这个是不是很熟悉呀~


没错,这就是使用 AI 工具 CR的代码片段


通常这种,为了处理特殊状况,所实现的判断逻辑,都可以采用 “异常逻辑前置,正常逻辑后置” 的方式进行梳理优化


// 将判断逻辑的异常逻辑提前,将正常逻辑后置
function checkStatus() {
if (!isLogin()) {
throw new Error('未登录');
}

if (!isVip()) {
throw new Error('不是会员');
}

if (!isDoubleCheck()) {
throw new Error('不要重复点击');
}

done();
}

函数传参优化


// 形参有非常多个
const getMyInfo = (
name,
age,
gender,
address,
phone,
email,
) => {
// ...
}

有时,形参有非常多个,这会造成什么问题呢?



  • 传实参是的时候,不仅需要知道传入参数的个数,还得知道传入顺序

  • 有些参数非必传,还要注意添加默认值,且编写的时候只能从形参的后面添加,很不方便

  • 所以啊,那么多的形参,会有很大的心智负担


怎么优化呢?


// 行参封装成对象,对象函数内部解构
const getMyInfo = (options) => {
const { name, age, gender, address, phone, email } = options;
// ...
}

getMyInfo(
{
name: '张三',
age: 18,
gender: '男',
address: '北京',
phone: '123456789',
email: '123456789@qq.com'
}
)

你看这样是不是就清爽了很多了


命名注释优化


1. 避免魔法数字


// 魔法数字
if (state === 1 || state === 2) {
// ...
} else if (state === 3) {
// ...
}

咋一看,这 1、2、3 又是什么意思啊?这是判断啥的?


语义就很不明确,当然,你也可以在旁边写注释


更优雅的做法是,将魔法数字改用常量


这样,其他人一看到常量名大概就知道,判断的是啥了


// 魔法数字改用常量
const UNPUBLISHED = 1;
const PUBLISHED = 2;
const DELETED = 3;

if (state === UNPUBLISHED || state === PUBLISHED) {
// ...
} else if (state === DELETED) {
// ...
}

2. 注释别写只表面意思


注释的作用:提供代码没有提供的额外信息


// 无效注释
let id = 1 // id 赋值为 1

// 有效注释,写业务逻辑 what & why
let id = 1 // 赋值文章 id 为 1

3. 合理利用命名空间缩短属性前缀


// 过长命名前缀
class User {
userName;
userAge;
userPwd;

userLogin() { };
userRegister() { };
}

如果我们把前面的类里面,变量名、函数名前面的 user 去掉


似乎,也一样能理解变量和函数名称所代表的意思


代码却,清爽了不少


// 利用命名空间缩短属性前缀
class User {
name;
age;
pwd;

login() {};
register() {};
}

分支逻辑优化


什么是分支逻辑呢?


使用 if else、switch case ...,这些都是分支逻辑


// switch case
const statusMap = (status: string) => {
switch(status) {
case 'success':
return 'SuccessFully'
case 'fail':
return 'failed'
case 'danger'
return 'dangerous'
case 'info'
return 'information'
case 'text'
return 'texts'
default:
return status
}
}

// if else
const statusMap = (status: string) => {
if(status === 'success') return 'SuccessFully'
else if (status === 'fail') return 'failed'
else if (status === 'danger') return 'dangerous'
else if (status === 'info') return 'information'
else if (status === 'text') return 'texts'
else return status
}

这些处理逻辑,我们可以采用 映射代替分支逻辑


// 使用映射进行优化
const STATUS_MAP = {
'success': 'Successfull',
'fail': 'failed',
'warn': 'warning',
'danger': 'dangerous',
'info': 'information',
'text': 'texts'
}

return STATUS_MAP[status] ?? status

【扩展】


??TypeScript 中的 “空值合并操作符”


当前面的值为 null 或者 undefined 时,取后面的值


对象赋值优化


// 多个对像属性赋值
const setStyle = () => {
content.body.head_style.style.color = 'red'
content.body.head_style.style.background = 'yellow'
content.body.head_style.style.width = '100px'
content.body.head_style.style.height = '300px'
// ...
}

这样一个个赋值太麻烦了,全部放一起赋值不就行了


可能,有些同学就这样写


const setStyle = () => {
content.body.head_style.style = {
color: 'red',
background: 'yellow',
width: '100px',
height: '300px'
}
}

咋一看,好像没问题了呀?那 style 要是有其他属性呢,其他属性不就直接没了吗~


const setStyle = () => {
content.body.head_style.style = {
...content.body.head_style.style
color: 'red',
background: 'yellow',
width: '100px',
height: '300px'
}
}

采用展开运算符,将原属性插入,然后从后面覆盖新属性,这样原属性就不会丢了


隐式耦合优化


// 隐式耦合
function responseInterceptor(response) {
const token = response.headers.get("authorization");
if (token) {
localStorage.setItem('token', token);
}
}

function requestInterceptor(response) {
const token = localStorage.getItem('token');
if (token) {
response.headers.set("authorization", token);
}
}

这个上面两个函数有耦合的地方,但是不太明显


比如这样的情况,有一天,我不想在 responseInterceptor 函数中保存 tokenlocalStorage


function responseInterceptor(response) {
const token = response.headers.get("authorization");
}

function requestInterceptor(response) {
const token = localStorage.getItem('token');
if (token) {
response.headers.set("authorization", token);
}
}

会发生什么?


localStorage.getItem('token')一直拿不到数据,requestInterceptor 这个函数就报废了,没用了


函数 responseInterceptor改动,影响到函数 requestInterceptor 了,隐式耦合了


怎么优化呢?


// 将隐式耦合的常数抽离成常量
const TOKEN_KEY = "authorization";
const TOKEN = 'token';

function responseInterceptor(response) {
const token = response.headers.get(TOKEN_KEY);
if (token) {
localStorage.setItem(TOKEN_KEY, token);
}
}

function requestInterceptor(response) {
const token = localStorage.getItem(TOKEN_KEY);
if (token) {
response.headers.set(TOKEN_KEY, token);
}
}

这样做有什么好处呢?比刚才好在哪里?


还是刚才的例子,我去掉了保存 localStorage.setItem(TOKEN_KEY, token)


我可以根据TOKEN_KEY这个常量来查找还有哪些地方用到了这个 TOKEN_KEY,从而进行修改,就不会出现冗余,或错误


不对啊,那我不用常量,用token也可以查找啊,但你想想 token 这个词是不是得全局查找,其他地方也会出现token


查找起来比较费时间,有时可能还会改错了


用常量的话,全局查找出现重复的概率很小


而且如果你是用 ts 的话,window 下鼠标停在常量上,按 ALT 键就能看到使用到这个常量的地方了,非常方便


小结


codeRiview(代码审查)不仅对个人技能的成长有帮助,也对我们在升职加薪、面试有所裨益


CR 除了传统的方式外,也可以借助 AI 工具,来简化其中流程,提高效率


上述的优化案例,虽然优化方式不同,但是核心思想都是一样,都是为了代码 更简洁、更容易理解、更容易维护


当然了,优化方式还有很多,如果后期遇到了也会继续补充进来


作者:大麦大麦
来源:juejin.cn/post/7394792228215128098
收起阅读 »

简单的 Web 端实时日志实现

web
背景 cron service 在执行定时任务时,需要能够实时查看该任务的执行日志以确保程序正确的工作。为了能够在尽可能短的时间内实现该功能,我们需要一个足够简单的方案。 方案如何选择? 我相信大多数开发者第一个想到的就是 WebSocket ,然后是 HTT...
继续阅读 »

Feb-20-2024 21-40-36.gif


背景


cron service 在执行定时任务时,需要能够实时查看该任务的执行日志以确保程序正确的工作。为了能够在尽可能短的时间内实现该功能,我们需要一个足够简单的方案。


方案如何选择?


我相信大多数开发者第一个想到的就是 WebSocket ,然后是 HTTP/SSE 。WebSocket 在很多情况下是实至名归的万金油选择。但这这里他太重了,对服务端和客户端具有侵入性需要花费额外的时间来集成到项目中。


那么 SSE 呢,为什么不是他?
虽然 SSE 即轻量又实时,但 SSE 无法设置请求头。这导致无法使用缓存和通过请求头设置的 Token。而且作为长链接他还一直占用并发额度。



  • WebSocket:❌

    • 优势:

      • 实时性高

      • 不会占用 HTTP 并发额度



    • 劣势:

      • 复杂度较高,需要在客户端和服务器端都进行特殊的处理

      • 消耗更多的服务器资源。





  • SSE(Server-Sent Events):❌

    • 优势:

      • 基于HTTP协议,不需要在服务端和客户端做额外的处理

      • 实时性高



    • 劣势:

      • 无法设置请求头

      • 占用 HTTP 并发额度





  • HTTP:✅

    • 优势:

      • 简单易用,不需要在服务端和客户端做额外的处理。

      • 支持的功能丰富,如缓存,压缩,认证等功能。



    • 劣势:

      • 实时性差,取决于轮询时间间隔。

      • 每次HTTP请求都需要建立新的连接(仅针对 HTTP/0.x 而言)并可能阻塞同源的其他请求而导致性能问题。12

        • HTTP/1.x 支持持久连接以避免每次请求都重新建立 TCP 连接,但数据通信是串行的。

        • HTTP/2.x 支持持久连接且支持并行的数据通信。









以上列出的优缺点是仅对于本文所讨论的场景(即Web 端的实时日志)而言,这三种数据交互技术在其他场景中的优缺点不在本文讨论范围内。



实现


HTTP 轮询已经是老熟人了,不再做介绍。本文的着重于实时日志的实现及优化。


sequenceDiagram

participant 浏览器

participant 服务器

Note right of 浏览器: 首先,获取日志文件最后 X bytes 的内容

浏览器->>服务器: 最新的日志文件有多大?

服务器->>浏览器: 日志文件大小: Y bytes

浏览器->>服务器: 从 Y - X bytes 处返回内容

服务器->>浏览器: 日志文件大小: Y1 bytes, 日志内容: XXXX



loop 持续轮询获取最新的日志

浏览器->>服务器: 从 Y1 bytes 处返回内容

服务器->>浏览器: 日志文件大小: Y2 bytes, 日志内容: XXXX

end

上方是基本工作原理的流程图。
实现的关键点在于



  • 前端如何知道日志文件当前的大小

  • 服务端如何从指定位置获取日志文件内容


这两点都可以通过 HTTP Header 来解决。Content-Range HTTP 响应头会包含完整文件的大小,而 Range HTTP 请求头可以指示服务器返回指定位置的文件内容。


因此在服务端不需要额外的逻辑,仅通过 Web 端代码就可以实现实时日志功能。


代码实现



篇幅限制,代码不会处理异常情况



首先,根据上述流程图。我们需要获取日志文件的大小。


const res = await fetch(URL, {  
method: "GET",
headers: { Range: "bytes=0-0" }
});

const contentRange = res.headers.get("Content-Range") || "";
const match = /bytes (\d+)-(\d+)\/(\d+)/.exec(contentRange);

// 日志文件大小
const total = Number.parseInt(match[3], 10);


Range: "bytes=0-0" 指定仅获取第一个字节的内容,以免较大的日志文件导致响应时间过长。



我们发起了一个 GET 请求并将 Range 请求头设置为 bytes=0-0 如果服务器能够正确处理 Range 请求头,响应中将包含一个 Content-Range 列其中将包含日志文件的完整大小,可以通过正则解析拿到。


现在我们已经拿到了日志文件的大小并存储在名为 total 的变量中。然后根据 total 获取到最后 10 KB 的日志内容。


const res = await fetch(url, {  
method: "GET",
headers: {
Range: `bytes=${total - 1000 * 10}-`
}
});

const contentRange = res.headers.get("Content-Range") || "";
const match = /bytes (\d+)-(\d+)\/(\d+)/.exec(contentRange);

// 下一次请求的起始位置
const start = Number.parseInt(match[2], 10) + 1;

// 日志内容
const content = await res.text();

现在我们发起了一个 GET 请求并将 Range 请求头设置为 bytes=${total - 1000 * 10}- 以获取最后 10 KB 的日志内容。并且通过正则解析拿到了下一次请求的起始位置。


现在我们已经拿到了日志文件的大小和最后 10 KB 的日志内容。接下来就是持续轮询去获取最新的日志。轮询代码区别仅在于将 Range 标头设置为 bytes=${start}- 以便获取最新的日志。


const res = await fetch(url, {  
method: "GET",
headers: {
Range: `bytes=${start}-`
}
});

const contentRange = res.headers.get("Content-Range") || "";
const match = /bytes (\d+)-(\d+)\/(\d+)/.exec(contentRange);

// 下一次请求的起始位置
start = Number.parseInt(match[2], 10) + 1;

// 日志内容
const content = await res.text();

以上,基本的功能已经实现了。日志内容保存在名为 content 的变量中。


优化


HTTP 轮询常因其高延迟为人诟病,我们可以通过指数退避的方式来尽可能的降低延时且不会显著的增加服务器负担。


指数退避是一种常见的网络重试策略,它会在每次重试时将等待时间乘以一个固定的倍数。这样做的好处是,当网络出现问题时,重试的时间间隔会逐渐增加,直到达到最大值。


一个简单的实现如下:


/**
* 使用指数退避策略获取日志.
*/

export class ExponentialBackoff {
private readonly base: number;
private readonly max: number;
private readonly factor: number;
private retries: number;

/**
* @param base 基础延迟时间 默认 1000ms (1秒)
* @param max 最大延迟时间 默认 60000ms (1分钟)
* @param factor 延迟时间增长因子 默认 2
*/

constructor(base: number = 1000, max: number = 60000, factor: number = 2) {
this.base = base;
this.max = max;
this.factor = factor;
this.retries = 0;
}

/**
* 获取下一次重试的延迟时间.
*/

next() {
const delay = Math.min(this.base * Math.pow(this.factor, this.retries), this.max);
this.retries++;
return delay;
}

/**
* 重置重试次数.
*/

reset() {
this.retries = 0;
}
}

值得一提的是带有 Range 标头的请求成功时会返回 206 Partial Content 状态码。而在请求的范围超出文件大小时会返回 416 Range Not Satisfiable 状态码。我们可以通过这两个状态码来判断请求是否成功。


成功时调用 reset 方法重置重试次数,失败时调用 next 方法获取下一次重试的延迟时间。


总结


即使再优秀的方案也不是银弹,真正合适的方案需要考虑的不仅仅是技术本身,还有业务场景,团队技术栈,团队技术水平等等。在选择技术方案时,我们需要权衡各种因素,而不是盲目的选择最流行的技术。


当我们责备现有的技术方案为何如此糟糕时,或许在那个时间点上,这个方案是最合适的。


Pasted image 20240220213326.png


感谢以下网友的帮助和建议:齐洛格德


Footnotes




作者:siaikin
来源:juejin.cn/post/7337519776796295177
收起阅读 »

方寸之间窥万象——这样的Tooltip,你会开发吗?

web
序言 提示信息(tooltip)是一种常见的 GUI 元素。在可视化领域,tooltip 通常指用户将鼠标悬停在图元上或者图表区域时弹出的明细数据信息框。如果是桌面环境,通常会在用户将指针悬停在元素上而不单击它时显示 tooltip;如果是移动环境,通常会在长...
继续阅读 »

序言


提示信息(tooltip)是一种常见的 GUI 元素。在可视化领域,tooltip 通常指用户将鼠标悬停在图元上或者图表区域时弹出的明细数据信息框。如果是桌面环境,通常会在用户将指针悬停在元素上而不单击它时显示 tooltip;如果是移动环境,通常会在长按(即点击并按住)元素时显示 tooltip。


这样一个小小的组件,却可以十分有效地丰富图表的数据展现能力和图表交互效果,同时在实际业务领域的用途也非常广泛。


近些年来,业界主要图表库(如ECharts、G2等)都提供了 tooltip 的配置能力和默认渲染能力,以达到开箱即用的效果。VChart 更不例外,提供了更加灵活的 tooltip 展示与配置方案。


通过使用 VChart,你既可以显示图表中任何系列的图元所携带的数据信息(mark tooltip):



也可以显示某个特定维度项下的所有图元的数据信息(dimension tooltip):



乃至可以灵活地自定义 tooltip,甚至在其中插入子图表,拓展交互的边界:



本文将通过一些实战案例,详细讲述 VChart 提示信息的重点用法、自定义方式以及设计细节。


示例一:可触及的 tooltip,与 Amazon 的安全三角形


为了不对用户的鼠标交互进行干扰,VChart 的 tooltip 默认不会响应鼠标事件。但是在某些情况,用户却希望鼠标可以移到 tooltip 中进行一些额外的交互行为,比如点击 tooltip 中的按钮和链接,或者选取并复制一些数据。


为了满足这类需求,tooltip 支持在 spec 中配置 enterable 属性。如果不配置或者配置 enterable: false,默认效果是这样的,鼠标无法移到 tooltip 元素内:



而如果配置 enterable: true,效果如以下截图所示:



图表简化版 spec 为:


const spec = {
type: 'waterfall',
data: [], // 数据略
legends: { visible: true, orient: 'bottom' },
xField: 'x',
yField: 'y',
seriesField: 'type',
total: {
type: 'field',
tagField: 'total'
},
title: {
visible: true,
text: 'Chinese quarterly GDP in 2022'
},
tooltip: {
enterable: true // TOOLTIP SPEC
}
};

const vchart = new VChart(spec, { dom: CONTAINER_ID });
vchart.renderSync();

简单对比两个 tooltip 的效果,可以发现后者在鼠标靠近 tooltip 时,tooltip 便适时地停住了。


这个小小的交互细节里却有些文章,灵感来源直接来自 Amazon 的官网实现。


这个例子也许已经为人熟知,我们简单回顾一下。这首先要从普通网站的下拉菜单开始讲起:



在一个设计欠佳的菜单组件里(如 bootstrap),鼠标从一级菜单移入二级菜单往往是很困难的,很容易触发二级菜单的隐藏策略,从而变成一场无聊的打地鼠游戏。


但是 Amazon 早期官网的菜单,由于用户使用频率高,根本无法接受这样的体验。于是他们完美地解决了这个问题,并成为一个交互优化的经典案例。


其思路的核心,便是检测鼠标移动的方向。如果鼠标移动到下图的蓝色三角形中,当前显示的子菜单将继续打开一小会儿:



在鼠标的每个位置,你可以想象鼠标当前位置与下拉菜单的右上角和右下角之间形成一个三角形。如果下一个鼠标位置在该三角形中,说明用户可能正在将鼠标移向当前显示的子菜单。Amazon 利用这一点实现了很好的效果。只要鼠标停留在该蓝色三角形中,当前子菜单就会保持打开。如果鼠标移出该三角形,他们会立即切换子菜单,使其感觉非常敏捷。


整体效果类似于下图所示:



正所谓,上帝在细节中(God is in the details)。从这个交互优化里,我们看到的不仅是一个精妙的算法,而是一个科技巨头对于产品和用户体验的态度。Amazon 的数百亿市值有多少是从这些很小很小,但是明显很用心的产品细节中积累起来的呢?



VChart 的 tooltip 也一样,着重参考了 Amazon 的交互优化。如果配置 enterable: true,在每个时刻,都会存在一个这样的“安全三角形”,三个顶点分别是鼠标光标以及 tooltip 的两个端点,取面积最大的三角形:



如果鼠标在下一刻滑到这个三角形区域中, tooltip 便为鼠标“停留一会儿”,直到鼠标移到 tooltip 区域内。



但是在鼠标移到 tooltip 区域之前,tooltip 并不会永远停下来等待鼠标。如果鼠标过于缓慢地靠近 tooltip,tooltip 还是会离开的(变成一场失败、却又在意料之中的奔赴)。这样便可以同时保证用户鼠标有足够的行动自由度。以下示例特地将鼠标移动速度放慢,便可以实现既进入三角形区域,又不会被 tooltip “挡路”:



作为对比,ECharts 的 tooltip 虽然同样支持 enterable 属性,但是 ECharts 主要通过简单的 tooltip 缓动来支持鼠标移入,鼠标仍需要不停地“追逐” tooltip 才能移至其中,灵活性便打了折扣。以下为 ECharts 的效果:



示例二:灵活的 pattern,内容与样式的自由配置


为了尽最大可能满足更多业务方的需求,VChart 的 tooltip 支持比较灵活的内容和样式配置。下文将以官网 demo(http://www.visactor.io/vchart/demo…



在这个图表中,用户配置了一条 y=10000 的标注线。同时要求在 dimension tooltip 中实现:



  • 数据项从大到小排序;

  • 比标注线高的数据项标红(条件格式);

  • 在 tooltip 内容的最后一行加上标注线所代表的数据。


同时,这个 tooltip 的位置还拥有以下特征:



  • dimension tooltip 的位置固定在光标上方;

  • mark tooltip 的位置固定在数据项下方。


如以下动图所示:



这个示例实际上代表了很多不同类型的业务需求。下面拆解来看一下:


基本 tooltip 内容配置


首先,剥去自定义内容和样式的部分,这个图表的最简 spec 和基本 tooltip 配置如下:


const markLineValue = 10000;
const spec = {
type: 'line',
data: {
values: [
{ type: 'Nail polish', country: 'Africa', value: 4229 },
// 其他数据略
]
},
stack: false,
xField: 'type',
yField: 'value',
seriesField: 'country',
legends: [{ visible: true, position: 'middle', orient: 'bottom' }],
markLine: [
{
y: markLineValue,
endSymbol: { visible: false },
line: { style: { /* 样式配置略 */ }}
}
],
tooltip: { // TOOLTIP SPEC
mark: {
title: {
value: datum => datum.type
},
content: [
{
key: datum => datum.country,
value: datum => datum.value
}
]
},
dimension: {
title: {
value: datum => datum.type
},
content: [
{
key: datum => datum.country,
value: datum => datum.value
}
]
}
}
};

const vchart = new VChart(spec, { dom: CONTAINER_ID });
vchart.renderSync();

显示效果如下:



观察 spec 不难发现,mark tooltip 和 dimension tooltip 分别用回调方式配置了 tooltip 的显示内容。其中:



  • title.value 显示的是数据项中对应于 xField 的内容;

  • content.key 显示的是数据项中对应于 seriesField(也是区分出图例项的 field)的内容;

  • content.value 显示的是数据项中对应于 yField 的内容。


回调是 tooltip 内容的基本配置方式,用户可以在 title 和 content 中自由配置回调函数来实现数据绑定和字符串的格式化。


Tooltip 内容的排序、增删、条件格式


我们再来看一下 dimension tooltip 的 spec:


{
tooltip: { // TOOLTIP SPEC
mark: { /* ...略 */ },
dimension: {
title: {
value: datum => datum.type
},
content: [
{
key: datum => datum.country,
value: datum => datum.value
}
]
}
}
}

不难发现,content 配置的数组只包含 1 个对象,但是上图显示出来却有 4 行内容。为什么呢?


其实在 dimension tooltip 中发生了和折线图元类似的 data join 过程:由于在数据中,seriesField 划分出了 4 个数据组(图例项有 4 个),因此在经过笛卡尔积后,真实 tooltip 内容行数为 content 数组成员数量乘以 4。在数据组装过程中,每个数据组都要依次走一遍 content 数组成员里的回调。


我们把 spec 中的 tooltip 内容配置称为 TooltipPattern,tooltip 所需数据称为 TooltipData,最终的 tooltip 结构称为 TooltipActual。数据组装过程可以表示为:


MakeTooltip(TooltipPattern,TooltipData) = TooltipActualMakeTooltip(TooltipPattern, TooltipData) = TooltipActual


在本例中,经过这个过程,TooltipPattern 中的回调在 TooltipActual 中消失(回调已被执行 4 次),且由 1 行变成了 4 行。


这个过程完整的执行流程如下:



那么回到示例中的用户需求,用户希望将 tooltip 内容行由大到小排序。那么这个步骤自然要在 TooltipActual 生成之后执行,也就是上图中的 “updateTooltipContent” 过程。


Tooltip spec 中支持配置 updateContent 回调来对 TooltipActual 的 content 部分进行操作。排序可以这样写:


{
tooltip: { // TOOLTIP SPEC
mark: { /* ...略 */ },
dimension: {
title: {
value: datum => datum.type
},
content: [
{
key: datum => datum.country,
value: datum => datum.value
}
],
updateContent: prev => {
// 排序
prev.sort((a, b) => b.value - a.value);
}
}
}
}

updateContent 回调的第一个参数为已经计算好的 TooltipActual。加上回调以后,排序生效:



在 tooltip 中实现条件格式以及新增一行也是一样的方法,可以直接在 updateContent 回调中处理:


{
updateContent: prev => {
// 排序
prev.sort((a, b) => b.value - a.value);
// 条件格式:比标注线高的数据项标红
prev.forEach(item => {
if (item.value >= markLineValue) {
item.valueStyle = {
fill: 'red'
};
}
});
// 新增一行
prev.push({
key: 'Mark Line',
value: markLineValue,
keyStyle: { fill: 'orange' },
valueStyle: { fill: 'orange' },
// 自定义 shape 的 svg path
shapeType: 'M44.3,22.1H25.6V3.3h18.8V22.1z M76.8,3.3H58v18.8h18.8V3.3z M99.8,3.3h-9.4v18.8h9.4V3.3z M12.9,3.3H3.5v18.8h9.4V3.3z',
shapeColor: 'orange',
hasShape: true
});
}
}

调试 spec,回调生效,最后效果如下:



Tooltip 样式和位置


VChart tooltip 支持将 tooltip 固定于某图元附近或者鼠标光标附近。在本例中,mark tooltip 固定于图元的下方,而 dimension tooltip 固定于鼠标光标的上方,可以这样配置:


{
tooltip: { // TOOLTIP SPEC
mark: {
// 其他配置略
position: 'bottom' // 显示在下方
positionAt: 'mark' // 固定在图元附近,由于这是默认值,这行可以删掉
},
dimension: {
// 其他配置略
position: 'top', // 显示在上方
positionAt: 'pointer' // 固定在鼠标光标附近
}
}
}

而样式配置可以在 tooltip spec 上的 style 配置项下进行自定义。style 支持配置 tooltip 组件各个组成部分的统一样式,详细配置项可参考官网文档(http://www.visactor.io/vchart/opti…


最后效果如下,完整 spec 可见官网 demo(http://www.visactor.io/vchart/demo…



示例三:锦上添花,可按需修改的 tooltip dom 树


VChart 的 tooltip 共支持两种渲染模式:



  • Dom 渲染,适用于桌面或移动端浏览器环境;

  • Canvas 渲染,适用于移动端小程序、node 环境等非浏览器环境。


对于 dom 版本的 tooltip,为了更好的支持业务方的自定义需求,VChart 开放了对 tooltip dom 树的修改接口。下文将以官网 demo(http://www.visactor.io/vchart/demo…



在这个示例中,用户要求在 tooltip 的底部增加一个超链接,用户点击链接后,便可以自动跳转到 Google,对 tooltip 标题进行进一步搜索。这个示例要求两个能力:



  • 示例一介绍的 enterable 能力,开启后鼠标会被允许滑入 tooltip 区域,这是在 tooltip 中实现交互的前提;

  • 在默认 tooltip 上绘制自定义的 dom 元素。


为了实现第二个能力,tooltip 支持了回调 updateElement,这个回调配在 tooltip spec 顶层。这个示例的 tooltip 配置如下:


{
tooltip: { // TOOLTIP SPEC
enterable: true,
updateElement: (el, actualTooltip, params) => {
// 自定义元素只加在 dimension tooltip 上
if (actualTooltip.activeType === 'dimension') {
const { changePositionOnly, dimensionInfo } = params;
// 判断本次 tooltip 显示是否仅改变了位置,如果是的话退出
if (changePositionOnly) { return; }
// 改变默认 tooltip dom 的宽高策略
el.style.width = 'auto';
el.style.height = 'auto';
el.style.minHeight = 'auto';
el.getElementsByClassName('value-box')[0].style.flex = '1';
for (const valueLabel of el.getElementsByClassName('value')) {
valueLabel.style.maxWidth = 'none';
}
// 删除上次执行回调添加的自定义元素
if (el.lastElementChild?.id === 'button-container') {
el.lastElementChild.remove();
}
// 添加新的自定义元素
const div = document.createElement('div');
div.id = 'button-container';
div.style.margin = '10px -10px 0px';
div.style.padding = '10px 0px 0px';
div.style.borderTop = '1px solid #cccccc';
div.style.textAlign = 'center';
div.innerHTML = `href="https://www.google.com/search?q=${dimensionInfo[0]?.value}"
style="text-decoration: none"
target="_blank"
>Search with Google
`;
el.appendChild(div);
} else {
// 对于 mark tooltip,删除上次执行回调添加的自定义元素
if (el.lastElementChild?.id === 'button-container') {
el.lastElementChild.remove();
}
}
}
}
}

updateElement在每次 tooltip 被激活或者更新时触发,在触发时,TooltipActual 已经计算完毕,且 dom 节点也已经准备好。回调的第一个参数便是本次将要显示的 tooltip dom 根节点。目前不支持替换该节点,只支持对该节点以及其孩子进行修改。


这个配置的设计最大限度地复用了 VChart tooltip 的内置逻辑,同时提供了足够自由的自定义功能。你可以随心所欲地定制 tooltip 显示内容,并且复用任何你没有覆盖的逻辑。


比如,你可以对 tooltip 的大小进行重新定义,而不用关心窗口边界的躲避策略是否会出问题。事实上 VChart 会自动把 tooltip 定位逻辑复用在修改过的 dom 上:



这个回调还可以进一步封装,比如在 react-vchart 中将用户侧的 react 组件插入 tooltip。目前这个封装主要由业务侧自主进行,后续 VChart 也有计划提供官方支持。


示例四:完全自定义,由业务托管 tooltip 渲染


若要更进一步,VChart tooltip 最高级别的自定义,便是让 VChart 完全将 tooltip 渲染交给用户。有以下两种方式可以选择:



  • 用户自定义 tooltip handler

  • 用户使默认 tooltip 失效,监听 tooltip 事件


再结合示例二、示例三的铺垫,便可以带出整个 tooltip 模块的设计架构。熟悉了架构便更容易了解每条渲染路径以及各个层级的关系。



由上图可见,示例三对应的是 “Custom DOM Render” 的自定义,示例二对应了 “Custom TooltipActual” 部分的自定义。而示例四,便是对应整个 “Tooltip Events” 以及 “Custom TooltipHandler” 的自定义。


由于上图中,“Tooltip Events” 和 “Custom TooltipHandler” 纵跨了多个层级,因此它覆盖的默认逻辑是最多的,体现在:



  • 当给图表设置了自定义 tooltip handler 后,内置的 tooltip 将不再起作用。

  • VChart 不感知、不托管自定义 tooltip 的渲染,需要自行实现 tooltip 渲染,包括处理原始数据、tooltip 内容设计,以及根据项目环境创建组件并设置样式。

  • 当图表删除时会调用当前 tooltip handler 的release函数,需要自行实现删除。


目前,火山引擎 DataWind 正是使用自定义 tooltip handler 的方式实现了自己的图表 tooltip。DataWind 支持用户对tooltip 进行富文本渲染,甚至支持了 tooltip 内渲染图表的能力。



另外,也可以参考官网示例(http://www.visactor.io/vchart/demo…



自定义 tooltip handler 的核心是调用 VChart 实例方法 setTooltipHandler,部分示例代码如下:


vchart.setTooltipHandler({
showTooltip: (activeType, tooltipData, params) => {
const tooltip = document.getElementById('tooltip');
tooltip.style.left = params.event.x + 'px';
tooltip.style.top = params.event.y + 'px';
let data = [];
if (activeType === 'dimension') {
data = tooltipData[0]?.data[0]?.datum ?? [];
} else if (activeType === 'mark') {
data = tooltipData[0]?.datum ?? [];
}
tooltipChart.updateData(
'tooltipData',
data.map(({ type, value, month }) => ({ type, value, month }))
);
tooltip.style.visibility = 'visible';
},
hideTooltip: () => {
const tooltip = document.getElementById('tooltip');
tooltip.style.visibility = 'hidden';
},
release: () => {
tooltipChart.release();
const tooltip = document.getElementById('tooltip');
tooltip.remove();
}
});

其他特性一览


VChart tooltip 包含一些其他的高级特性,下文将简要介绍。


在任意轴上触发 dimension tooltip


Dimension tooltip 一般最适合用于离散轴,ECharts 同时支持连续轴上的 dimension tooltip(axis tooltip)。而 VChart 支持了在连续轴、时间轴乃至在一个图表中的任意一个轴上触发 dimension tooltip。


以下示例展示了 dimension tooltip 在连续轴(时间轴)上汇总离散数据的能力(这个 case 和一般的 dimension tooltip 刚好相反):



一般的 dimension tooltip 会在离散轴(纵轴)触发 tooltip,汇总连续数据(对应于时间轴)。而 VChart 同时支持这两种方式的 tooltip。


Demo 地址:http://www.visactor.io/vchart/demo…


长内容支持:换行和局部滚动


过长的内容在 tooltip 上一般是 bad case。但是为了使长内容的浏览体验更好,VChart tooltip 可以配置多行文本以及内容区域局部滚动。如以下示例:



局部滚动 Demo 地址:http://www.visactor.io/vchart/demo…


多行文本配置项:http://www.visactor.io/vchart/opti…


结语


Tooltip 在提升用户浏览图表的体验中扮演着重要的角色。本文介绍了 VChart tooltip 的基本使用方法、技术设计以及多层面的自定义方案。然而为了保证行文清晰,VChart tooltip 还有一些其他的用法细节本文没有涉及,想了解更多可以查阅官网 demo 以及文档。


然而需要提醒的是,虽然 tooltip 能够有效传递数据与信息、以及增加图表的互动能力,但过分依赖它们可能会导致用户体验下降。合理地利用 tooltip,让它们在需要时出现而不干扰用户的主要任务,是设计和开发中应保持的平衡。


希望本文能为你在配置 VChart tooltip 时提供有用的指导。愿你在图表中创造更加直观、轻松且愉快的用户体验时,VChart 能成为你强大的伙伴。




Feb-22-2024 10-11-30.gif


github:github.com/VisActor/VC…


相关参考:



作者:玄魂
来源:juejin.cn/post/7337963242416422924
收起阅读 »

总算体会到jsx写法为啥灵活

web
前言 大家好,我是你不会困,写代码就不会困,今天分享的是总算体会到jsx写法为啥灵活 什么是jsx写法? 当谈到JavaScript中的JSX写法时,人们往往会想到React和Vue这样的流行前端框架。JSX作为一种在JavaScript中编写类似于HTML的...
继续阅读 »

前言


大家好,我是你不会困,写代码就不会困,今天分享的是总算体会到jsx写法为啥灵活


什么是jsx写法?


当谈到JavaScript中的JSX写法时,人们往往会想到React和Vue这样的流行前端框架。JSX作为一种在JavaScript中编写类似于HTML的语法,为前端开发者提供了更灵活和直观的方式来构建用户界面。


JSX的灵活性体现在多个方面。首先,JSX允许开发者在JavaScript中嵌入HTML标记,使得代码更易读和维护。通过使用JSX,开发者可以在同一个文件中编写JavaScript逻辑和界面布局,而无需频繁切换不同的文件。这种混合编程风格提高了开发效率,同时也方便了代码的组织和调试。


其次,JSX支持在标记中使用JavaScript表达式,这使得动态生成界面变得更加简单。开发者可以在JSX中直接使用JavaScript变量、函数调用和逻辑控制语句,从而动态地渲染页面内容。这种灵活性使得开发者能够根据不同的数据状态和条件来动态展示内容,提升了用户体验。


另外,JSX还支持在标记中使用循环和条件语句,比如map函数和条件渲染,从而实现列表展示、条件展示等常见的UI需求。这种功能使得开发者可以更方便地处理复杂的UI逻辑,同时简化了代码的编写和维护。


此外,JSX的组件化特性也为前端开发带来了很多好处。通过将UI拆分成独立的组件,开发者可以更好地组织和管理代码,提高代码的重用性和可维护性。JSX中的组件可以嵌套使用,形成复杂的UI结构,同时每个组件可以单独管理自己的状态和逻辑,使得代码更加清晰和可扩展。


今天在开发的时候发现,这两个即可开启总计列


show-summary
:summary-method="getSummaries"

但是产品的需求比较麻烦,需要渲染多行,查了相关的文档,好像没有这种渲染的demo,翻看项目的代码,有一部分代码的实现比较巧妙,使用的是jsx写法,然后就尝试着去实现


要在vue里面使用jsx写法,在script标签使用<script lang="jsx">,即可使用


getSummaries(param) {
const { columns } = param
const sums = []
const nullHtml = '-'
columns.forEach((column, index) => {
if (index === 0) {
sums[index] = '总计'
return
}
if (this.totalSum.summaryReceivableComparisons) {
sums[index] = (
<div>
{this.totalSum.summaryReceivableComparisons.map((item) => (
<div class='cell-item' key={item.invoiceCurrency}>
<p>
{this.formatValue(
item[column.property],
column.property.includes('Ratio')
? 'percentage'
: 'thousandth'
)}
</p>
</div>
))}
</div>
)
} else {
sums[index] = nullHtml
return
}
})
return sums
},

上面的代码使用了map来遍历,将对应的html返回,el-table的总计列即可生效,来应对不同的需求


总结


总的来说,JSX作为JavaScript中的一种扩展语法,为前端开发带来了更灵活、直观和高效的开发体验。通过使用JSX,开发者可以更轻松地构建交互丰富、动态变化的用户界面,同时提高了代码的可读性和可维护性。JSX的灵活性和表现力使其成为现代前端开发中不可或缺的一部分。


作者:你不会困
来源:juejin.cn/post/7410672790020800548
收起阅读 »

Electron实现静默打印小票

web
Electron实现静默打印小票 静默打印流程 1.渲染进程通知主进程打印 //渲染进程 data是打印需要的数据 window.electron.ipcRenderer.send('handlePrint', data) 2.主进程接收消息,创建打印页面...
继续阅读 »

Electron实现静默打印小票


静默打印流程


09c00eb5-f171-4090-a178-37e149d1d0f7.png


1.渲染进程通知主进程打印


//渲染进程 data是打印需要的数据
window.electron.ipcRenderer.send('handlePrint', data)

2.主进程接收消息,创建打印页面


//main.ts
/* 打印页面 */
let printWindow: BrowserWindow | undefined
/**
* @Author: yaoyaolei
* @Date: 2024-06-07 09:27:22
* @LastEditors: yaoyaolei
* @description: 创建打印页面
*/

const createPrintWindow = () => {
return new Promise<void>((resolve) => {
printWindow = new BrowserWindow({
...BASE_WINDOW_CONFIG,
title: 'printWindow',
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: false,
nodeIntegration: true,
contextIsolation: false
}
})

printWindow.on('ready-to-show', () => {
//打印页面创建完成后不需要显示,测试时可以调用show查看页面样式(下面有我处理的样式图片)
// printWindow?.show()
resolve()
})

printWindow.webContents.setWindowOpenHandler((details: { url: string }) => {
shell.openExternal(details.url)
return { action: 'deny' }
})

if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
printWindow.loadURL(`${process.env['ELECTRON_RENDERER_URL']}/print.html`)
} else {
printWindow.loadFile(join(__dirname, `../renderer/print.html`))
}
})
}

ipcMain.on('handlePrint', (_, obj) => {
//主进程接受渲染进程消息,向打印页面传递数据
if (printWindow) {
printWindow!.webContents.send('data', obj)
} else {
createPrintWindow().then(() => {
printWindow!.webContents.send('data', obj)
})
}
})

3.打印页面接收消息,拿到数据渲染页面完成后通知主进程开始打印


<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<title>打印</title>
<style>
</style>
</head>

<body>

</body>
<script>
window.electron.ipcRenderer.on('data', (_, obj) => {
//这里是接受的消息,处理完成后将html片段放在body里面完成后就可以开始打印了
//样式可以写在style里,也可以内联
console.log('event, data: ', obj);
//这里自由发挥
document.body.innerHTML = '处理的数据'
//通知主进程开始打印
window.electron.ipcRenderer.send('startPrint')
})
</script>
</html>



这个是我处理完的数据样式,这个就是print.html
9f17ea7e-3f83-408f-a780-05d50da305de.png
微信图片_20240609102325.jpg



4,5.主进程接收消息开始打印,并且通知渲染进程打印状态


ipcMain.on('startPrint', () => {
//这里如果不指定打印机使用的是系统默认打印机,如果需要指定打印机,
//可以在初始化的时候使用webContents.getPrintersAsync()获取打印机列表,
//然后让用户选择一个打印机,打印的时候将打印机名称传过来
printWindow!.webContents.print(
{
silent: true,
margins: { marginType: 'none' }
//deviceName:如果要指定打印机传入打印机名称
},
(success) => {
//通知渲染进程打印状态
if (success) {
mainWindow.webContents.send('printStatus', 'success')
} else {
mainWindow.webContents.send('printStatus', 'error')
}
}
)
})

aa.jpg



完毕~



作者:彷徨的耗子
来源:juejin.cn/post/7377645747448365091
收起阅读 »

我的 Electron 客户端被第三方页面入侵了...

web
问题描述 公司有个内部项目是用 Electron 来开发的,有个功能需要像浏览器一样加载第三方站点。 本来一切安好,但是某天打开某个站点的链接,导致 整个客户端直接变成了该站点的页面。 这一看就是该站点做了特殊的处理,经排查网页源码后,果然发现了有这么一句代码...
继续阅读 »

问题描述


公司有个内部项目是用 Electron 来开发的,有个功能需要像浏览器一样加载第三方站点。


本来一切安好,但是某天打开某个站点的链接,导致 整个客户端直接变成了该站点的页面


这一看就是该站点做了特殊的处理,经排查网页源码后,果然发现了有这么一句代码。


    if (window.top !== window.self) {
window.top.location = window.location;
}

翻译一下就是:如果当前窗口不是顶级窗口的话,将当前窗口设置为顶级窗口。


奇怪的是两者不是 跨域 了吗,为什么 iframe 还可以影响顶级窗口。


先说一下我当时的一些解决办法:



  1. webview 替换 iframe

  2. iframe 添加 sandbox 属性


后续内容就是一点复盘工作。


场景复现(Web端)


一开始怀疑是客户端的问题,所以我用在纯 Web 上进行了一次对比验证。


这里我们新建两个文件:1.html2.html,我们称之为 页面A页面B


然后起了两个本地服务器来模拟同源与跨域的情况。


页面A:http://127.0.0.1:5500/1.html


页面B:http://127.0.0.1:5500/2.htmlhttp://localhost:3000/2.html


符合同源策略


<body>
<h1>这是页面A</h1>
<!-- 这是同源的情况 -->
<iframe id="iframe" src="http://127.0.0.1:5500/2.html" />

<script>
iframe.onload = () => {
console.log('iframe loaded..')
console.log('子窗口路径', iframe.contentWindow.location.href)
}
</script>
</body>

<body>
<h2>这是页面B</h2>

<script>
console.log('page2...')
console.log(window === window.top)
console.log('顶部窗口路径', window.top.location.href)
</script>
</body>

我们打开控制台可以看到 页面A 和 页面B 是可以 互相访问 到对方窗口的路径。


image.png


如果这个时候在 页面B 加上文章开头提到的 代码片段,那么显然页面将会发生变化。


image.png


跨域的情况


这时候我们修改 页面A 加载 页面B 的地址,使其不符合同源策略。


image.png


理所应当的是,两个页面不能够相互访问了,这才是正常的,否则内嵌第三方页面可以互相修改,那就太不安全了。


场景复现(客户端)


既然 Web 端是符合预期的,那是不是 Electron 自己的问题呢?


我们通过 electron-vite 快速搭建了一个 React模板的electron应用,版本为:electron@22.3.27,并且在 App 中也嵌入了刚才的 页面B。


function App(): JSX.Element {
return (
<>
<h1>这是Electron页面</h1>
<iframe id="iframe" src="http://localhost:3000/2.html"/>
</>

)
}
export default App

image.png


对不起,干干净净的 Electron 根本不背这个锅,在它身上的表现如同 Web端 一样,也受同源策略的限制。


那么肯定是我的项目里有什么特殊的配置,通过对比主进程的代码,答案终于揭晓。


new BrowserWindow({
...,
webPreferences: {
...,
webSecurity: false // 就是因为它
}
})

Electron 官方文档 里是这么描述 webSecurity 这个配置的。



webSecurity boolean (可选) - 当设置为 false, 它将禁用同源策略 (通常用来测试网站), 如果此选项不是由开发者设置的,还会把 allowRunningInsecureContent设置为 true. 默认值为 true



也就是说,Electron本身是有一层屏障的,但当该属性设置为 false 的时候,我们的客户端将会绕过同源策略的限制,这层屏障也就消失了,因此 iframe 的行为表现得像是嵌套了同源的站点一样。


解决方案


把这个配置去掉,确实是可以解决这个问题,但考虑到可能对其他功能造成的影响,只能采取其他方案。


如文章开头提到的,用 webview 替换 iframe


webviewElectron的一个自定义元素(标签),可用于在应用程序中嵌入第三方网页,它默认开启安全策略,直接实现了主应用与嵌入页面的隔离。


因为目前这个需求是仅作展示,不需要与嵌套页面进行交互以及复杂的通信,因此在一开始的开发过程中,并没有使用它,而是直接采用了 iframe


iframe 也能够实现类似的效果,只需要添加一个 sandbox 属性可以解决。


MDN 中提到,sandbox 控制应用于嵌入在 <iframe> 中的内容的限制。该属性的值可以为空以应用所有限制,也可以为空格分隔的标记以解除特定的限制。


如此一来,就算是同源的,两者也不会互相干扰。


总结


这不是一个复杂的问题,发现后及时修复了,并没有造成很大的影响(还好是自己人用的平台)。


写这篇文章的主要目的是为了记录这次事件,让我意识到在平时开发过程中,把注意力过多的放在了 业务样式性能等这些看得见的问题上,可能很少关注甚至忽略了 安全 这一要素,以为前端框架能够防御像 XSS 这样的攻击就能安枕无忧。


谨记,永远不要相信第三方,距离产生美。


如有纰漏,欢迎在评论区指出。


作者:小陈同学吗
来源:juejin.cn/post/7398418805971877914
收起阅读 »

如何将用户输入的名称转成艺术字体-fontmin.js

web
写在开头 日常我们在页面中使用特殊字体,一般操作都是直接由前端来全量引入设计师提供的整个字体包即可,具体操作如下: <template> <div class="font">橙某人</div> </template...
继续阅读 »

写在开头


日常我们在页面中使用特殊字体,一般操作都是直接由前端来全量引入设计师提供的整个字体包即可,具体操作如下:


<template>
<div class="font">橙某人</div>
</template>

<style scoped>
@font-face {
font-family: "orange";
src: url("./orange.ttf");
}
.font {
font-family: "orange";
}
</style>


很简单吧🤡,但有时应用场景不同,可能需要我们考虑一下性能问题。



一般来说,我们常见的字体包整个是非常大的,小的有几M到十几M,大的可能去到上百M都有,特别是中文类的字体包会相对英文类的要更大一些。



如本文案例,我们仅需在用户输入完后加载对应的字体包即可,这样能避免性能的损耗。


为此,我们需要把整个字体包拆分、细致化、子集化,让它能达到按需引入的效果。


那么这要如何来做这个事情呢?这个方案单单前端可做不了,我们需要配合后端一起,下面就来看看具体的实现过程吧。😗


前端


前端小编用 Vue 来编写,具体如下:


<template>
<div>
<input v-model="name" />
<button @click="handleClick">生成</button>
<div v-if="showName" class="font">{{ showName }}</div>
</div>

</template>

<script>
export default {
data() {
return {
name: "",
showName: "",
};
},
methods: {
handleClick() {
// 创建link标签
const linkElement = document.createElement("link");
linkElement.setAttribute("rel", "stylesheet");
linkElement.setAttribute("type", "text/css");
linkElement.href = `http://localhost:3000?name=${encodeURIComponent(this.name)}`;
document.body.appendChild(linkElement);
// 优化显示效果
setTimeout(() => {
this.showName = this.name;
}, 300);
},
},
};
</script>


<style>
.font {
font-family: orange;
font-size: 50px;
}
</style>


应该都能看懂吧,主要就是生成了一个 <link /> 标签并插入到文档中,标签的请求地址指向我们服务端,至于服务端会返回什么你可以先猜一猜。👻


服务端


服务端小编选择用 Koa2 来编写,你也可以选择 Express 或者 Egg ,甚至 Node 也是可以的,差异不大,具体逻辑如下:


const koa = require("koa2");
const fs = require("fs");
const FontMin = require("fontmin");

const app = new koa();

/** @name 优化,缓存已经加载过的字体包进内存 **/
const fontFamilyMap = {};

/** @name 加载字体包 **/
function loadFontLibrary(fontPath, fontFamily) {
if (fontFamilyMap[fontFamily]) return fontFamilyMap[fontFamily];
return new Promise((resolve, reject) => {
fs.readFile(fontPath, (error, file) => {
if (error) {
reject(new Error(error.message));
} else {
fontFamilyMap[fontFamily] = file;
resolve(file);
}
});
});
}

app.use(async (ctx) => {
const { name } = ctx.query;
// 设置返回文件类型
ctx.set("Content-Type", "text/css");

const fontPath = "./font/orange.ttf";
const fontFamily = "orange";
if (!fs.existsSync(fontPath)) return (ctx.body = "字体包读取失败");

const fontMin = new FontMin();
const fontFile = await loadFontLibrary(fontPath, fontFamily);
fontMin.src(fontFile);

const getFontCSS = () => {
return new Promise((resolve) => {
fontMin
.use(FontMin.glyph({ text: name }))
.use(FontMin.css({ base64: true, fontFamily }))
.run((error, files) => {
if (error) {
console.log("error", error.message);
} else {
const fontContent = files?.[1]?.contents;
resolve(fontContent);
}
});
});
};

const fontCSS = await getFontCSS();

ctx.body = fontCSS;
});

app.listen(3000);

console.log("服务器开启: http://localhost:3000/");

我们主要是采用了 Fontmin 库来完成整个字体包的按需加载功能,这个库是第一个纯 JavaScript 字体子集化方案。



可能有后端是 Java 或者其他技术栈的小伙伴,你们也不用担心,据小编和公司后端同事了解,不同技术栈也是有对应的库可以解决的,需要的可以自行查查看。










至此,本篇文章就写完啦,撒花撒花。


image.png


希望本文对你有所帮助,如有任何疑问,期待你的留言哦。

老样子,点赞+评论=你会了,收藏=你精通了。


作者:橙某人
来源:juejin.cn/post/7293151700869038099
收起阅读 »

登录问题——web端

问题描述:在集成环信SDK的过程中,大家可能会遇到一个令人困惑的问题:明明已经通过open登录成功了,但是在调用api时却总是报错,错误类型为type28或者type700或者type39 not login。本文将详细分析这个问题的原因,并提供相应的解决方案...
继续阅读 »

问题描述:

在集成环信SDK的过程中,大家可能会遇到一个令人困惑的问题:明明已经通过open登录成功了,但是在调用api时却总是报错,错误类型为type28或者type700或者type39 not login。本文将详细分析这个问题的原因,并提供相应的解决方案。


原因分析:


要解决这个问题,我们首先需要了解环信SDK的登录机制。登录过程实际上分为两个步骤:

1. 请求Token:这是open登录操作的第一步,即在open.then或者success回调中返回token。
2. 建立长连接:即建立WebSocket连接,触发onOpened或者onConnected回调。只有当onOpened或者onConnected回调被触发,才算是真正与环信服务器建立了连接。
SDK在拿到token后,会将其设置进入SDK并尝试建立连接。如果在onOpened或者onConnected回调触发之前就执行了api的调用,那么token可能还没有被正确设置进入SDK,从而导致后续的HTTP请求报token无效的错误。也就是出现type28或者type700或者type 39的报错。

解决方案:


为了避免这个问题,我们需要调整代码逻辑,确保在onOpened或者onConnected回调触发后再去请求一系列的接口。以下是具体的调整步骤:

1. 监听连接状态:在SDK初始化后,监听onOpened或者onConnected回调

2. 延迟调用api操作:不要在open.then或者success回调中立即执行api的调用,而是等待onOpened或者onConnected回调触发后再执行。
3. 检查SDK状态:调用api前检查SDK是否已经成功建立连接。
可以用以下三种方法中的一种判断检查SDK是否已经成功建立连接~
1、WebIM.conn方法下有一个logOut字段,该字段为true时表明未登录状态,该字段为false时表明登录;
2、WebIM.conn.isOpened () 方法有三个状态,undefined为未登录状态,true为已登录状态,false为未登录状态,可以根据这三个状态去判断是否登录;
3、通过onOpened 这个回调来判断,只要执行了就说明登录成功了,输出的话,输出的是undefined

收起阅读 »

183天打造行业新标杆!BOE(京东方)国内首条第8.6代AMOLED生产线提前全面封顶

2024年9月25日,BOE(京东方)投建的国内首条第8.6代AMOLED生产线全面封顶仪式在成都市高新区举行,该生产线从开工到封顶仅用183天,以科学、高效、高质的速度再树行业新标杆。这不仅是BOE(京东方)创新突破、打造新质生产力的又一重大举措,也是OLE...
继续阅读 »

2024年9月25日,BOE(京东方)投建的国内首条第8.6代AMOLED生产线全面封顶仪式在成都市高新区举行,该生产线从开工到封顶仅用183天,以科学、高效、高质的速度再树行业新标杆。这不仅是BOE(京东方)创新突破、打造新质生产力的又一重大举措,也是OLED领域的里程碑事件,极大推动OLED显示产业快速迈进中尺寸发展阶段,对促进半导体显示产业优化升级、引领行业高质量发展具有重要意义。京东方科技集团董事长陈炎顺出席并宣布仪式启动,项目总指挥刘晓东、项目执行总指挥杨国波等领导及中建三局集团有限公司、中国建筑一局(集团)有限公司、中国电子工程设计院股份有限公司、四川华凯工程项目管理有限公司等相关单位领导共同出席封顶仪式。

BOE(京东方)第8.6代AMOLED生产线项目总指挥刘晓东在致辞中表示:“BOE(京东方)第8.6代AMOLED生产线自今年初正式开工以来,始终秉持‘五同时、五确保、五典范’建设原则,以坚韧不拔的意志和团结协作的精神,历时183天,提前达成全面封顶目标,标志着该生产线正式迈入新阶段。BOE(京东方)第8.6代AMOLED生产线必将成为行业标杆工程,为企业发展注入新的活力与动力。我们有信心、有能力打造全球最具竞争力的第8.6代AMOLED生产线,为全球显示产业进步贡献重要力量。”

BOE(京东方)第8.6代AMOLED生产线总投资630亿元,是四川省迄今投资体量最大的单体工业项目,设计产能每月3.2万片玻璃基板(尺寸2290mm×2620mm),主要生产笔记本电脑、平板电脑等智能终端高端触控OLED显示屏。BOE(京东方)通过采用低温多晶硅氧化物(LTPO)背板技术与叠层发光器件制备工艺,使OLED屏幕实现更低的功耗和更长的使用寿命,也将带动下游笔记本及平板电脑产品的迭代升级。目前,BOE(京东方)已在成都、重庆、绵阳投建了三条第6代柔性AMOLED生产线,再加上国内首条第8.6代AMOLED生产线的投建,全面展现了其全球领先的技术实力和行业影响力。值得关注的是,截至2023年,BOE(京东方)柔性OLED出货量已连续多年稳居国内第一,全球第二(数据来源:Omdia),柔性OLED相关专利申请超3万件。BOE(京东方)柔性显示技术不仅应用于手机领域,还持续拓展笔记本、车载、可穿戴等领域,折叠屏、滑卷屏、全面屏等柔性显示解决方案已覆盖国内外众多头部终端品牌,进一步确立BOE(京东方)在OLED领域的全球领先地位。

2024年,BOE(京东方)面向下一个三十年的新征程全新出发,公司将始终坚持“传承、创新、发展”的企业文化内核,坚定信念、创新变革,持续探索契合市场需求的企业发展“第N曲线”。BOE(京东方)第8.6代AMOLED生产线也将汇聚新型显示产业人才,发挥引擎作用,打造以柔性显示为核心的“世界柔谷”,在持续提升竞争力的同时,谱写行业高质发展的新篇章。

收起阅读 »

iframe的基本使用与注意点

web
iframe(Inline Frame)是一种在网页中嵌套其他网页的 HTML 元素。通过 iframe,开发者可以在一个页面中加载另一个页面的内容,提升用户体验和功能性。下面将详细探讨 iframe 的原理、使用场景以及注意事项,并提供相应的代码示例。 一、...
继续阅读 »


iframe(Inline Frame)是一种在网页中嵌套其他网页的 HTML 元素。通过 iframe,开发者可以在一个页面中加载另一个页面的内容,提升用户体验和功能性。下面将详细探讨 iframe 的原理、使用场景以及注意事项,并提供相应的代码示例。


一、iframe 的原理


iframe 是一种 HTML 标签,其基本语法如下:


<iframe src="https://example.com" width="600" height="400" frameborder="0"></iframe>


  • src:指定要加载的网页地址。

  • widthheight:定义 iframe 的宽度和高度。

  • frameborder:控制边框显示(在 HTML5 中不推荐使用)。


当浏览器遇到 iframe 标签时,会发起一个独立的网络请求来加载指定的 URL。这使得嵌入的内容在主文档之外独立渲染。


二、使用场景



  1. 广告展示



    • iframe 经常用于展示广告内容,允许网站在不影响主页面的情况下,灵活更新广告。


    <iframe src="https://ad.example.com" width="300" height="250" frameborder="0"></iframe>


  2. 第三方内容集成



    • 嵌入社交媒体帖子、视频播放器或地图等内容。例如,嵌入 YouTube 视频:


    <iframe src="https://www.youtube.com/embed/VIDEO_ID" width="560" height="315" frameborder="0" allowfullscreen></iframe>


  3. 内容隔离



    • 当需要展示用户生成的内容(如评论或论坛)时,可以使用 iframe 进行内容隔离,避免对主页面造成影响。



  4. 安全性




    • 使用 sandbox 属性,可以限制 iframe 的功能,增加安全性。



      1. allow-forms


        允许 iframe 内部的表单提交。默认情况下,表单提交被禁止。


      2. allow-same-origin


        允许 iframe 中的文档以相同来源访问其父页面。这允许脚本与同源的内容交互。


      3. allow-scripts


        允许 iframe 中的脚本执行。默认情况下,脚本执行被禁止。


      4. allow-top-navigation


        允许 iframe 中的内容导航到父页面。这使得嵌入页面可以改变主页面的 URL。


      5. allow-popups


        允许 iframe 中的内容打开新窗口或标签页。默认情况下,这种操作被禁止。


      6. allow-modals


        允许 iframe 显示模态对话框,例如 alertpromptconfirm


      7. allow-presentation


        允许 iframe 进入展示模式,例如全屏模式。





    <iframe src="https://example.com" width="600" height="400" sandbox="allow-scripts"></iframe>



三、注意点



  1. 安全性问题



    • 由于跨站点脚本攻击(XSS)的风险,很多网站设置了 X-Frame-OptionsContent-Security-Policy 来限制 iframe 的嵌入。这会导致“拒绝了我们的连接请求”的错误提示。




image.png


```http
X-Frame-Options: DENY
`
``


  1. 性能影响



    • 嵌套多个 iframe 会增加页面的加载时间和复杂性,影响性能。因此,建议合理使用。



  2. 跨域限制



    • 由于同源策略,iframe 中加载的页面不能与主页面进行直接交互。这意味着无法访问嵌入页面的 DOM 或 JavaScript。



  3. SEO 考虑



    • 搜索引擎可能不会索引 iframe 内的内容,从而影响整体的 SEO 表现。避免将重要内容仅放在 iframe 中。



  4. 响应式设计



    • 确保 iframe 在不同设备和屏幕尺寸下表现良好,可以通过 CSS 设置其宽度为百分比。例如:


    iframe {
    width: 100%;
    height: auto;
    }



四、示例代码


以下是一个综合示例,展示了如何使用 iframe 加载一个 YouTube 视频并应用响应式设计:


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Iframe Example</title>
<style>
.responsive-iframe {
position: relative;
padding-bottom: 56.25%; /* 16:9 Aspect Ratio */
height: 0;
overflow: hidden;
}
.responsive-iframe iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
</style>
</head>
<body>

<h1>嵌入 YouTube 视频</h1>
<div class="responsive-iframe">
<iframe src="https://www.youtube.com/embed/VIDEO_ID" frameborder="0" allowfullscreen></iframe>
</div>

</body>
</html>

结论


iframe 是一种强大的网页嵌入技术,能够增强网页功能和用户体验。在使用时,需要充分考虑安全性、性能和跨域问题,以确保良好的用户体验。通过合理配置和使用,iframe 可以为网页增加更多的互动性和功能性。


---09/19ヾ( ̄▽ ̄)ByeBye


再见.png


作者:不爱说话郭德纲
来源:juejin.cn/post/7415914059106533439
收起阅读 »

get请求参数放在body中?

web
1、背景 与后端对接口时,看到有一个get请求的接口,它的参数是放在body中的 ******get请求参数可以放在body中?? 随即问了后端,后端大哥说在postman上是可以的,还给我看了截图 可我传参怎么也调不通! 下面就来探究到底是怎么回事 2、...
继续阅读 »

1、背景


与后端对接口时,看到有一个get请求的接口,它的参数是放在body中的



******get请求参数可以放在body中??


随即问了后端,后端大哥说在postman上是可以的,还给我看了截图



可我传参怎么也调不通!


下面就来探究到底是怎么回事


2、能否发送带有body参数的get请求


项目中使用axios来进行http请求,使用get请求传参的基本姿势:


// 参数拼接在url上
axios.get(url, {
params: {}
})

如果想要将参数放在body中,应该怎么做呢?


查看axios的文档并没有看到对应说明,去github上翻看下axios源码看看


lib/core/Axios.js文件中



可以看到像deletegetheadoptions方法,它们只接收两个参数,不过在config中有一个data



熟悉的post请求,它接收的第二个参数data就是放在body的,然后一起作为给this.request作为参数


所以看样子get请求应该可以在第二个参数添加data属性,它会等同于post请求的data参数


顺着源码,再看看lib/adapters/xhr.js,上面的this.request最终会调用这个文件封装的XMLHttpRequest


export default isXHRAdapterSupported && function (config) {
return new Promise(function dispatchXhrRequest(resolve, reject) {
let requestData = config.data

// 将config.params拼接在url上
request.open(config.method.toUpperCase(),
buildURL(fullPath, config.params, config.paramsSerializer), true);

// 省略若干代码
...

// Send the request
request.send(requestData || null);
});
}

最终会将data数据发送出去


所以只要我们传递了data数据,其实axios会将其放在body发送出去的


2.1 实战


本地起一个koa服务,弄一个简单的接口,看看后端能否接收到get请求的body参数


router.get('/api/json', async (ctx, next) => {
console.log('get请求获取body: ', ctx.request.body)

ctx.body = ctx.request.body
})

router.post('/api/json', async (ctx, next) => {
console.log('post请求获取body: ', ctx.request.body)

ctx.body = ctx.request.body
})

为了更好地比较,分别弄了一个getpost接口


前端调用接口:


const res = await axios.get('/api/json', {
data: {
id: 1,
type: 'GET'
}
})


const res = await axios.post('/api/json', {
data: {
id: 2,
type: 'POST'
}
})
console.log('res--> ', res)

axiossend处打一个断点



可以看到数据已经被放到body中了


后端已经接收到请求了,但是get请求无法获取到body



结论:



  • 前端可以发送带body参数的get请求,但是后端接收不到

  • 这就是接口一直调不通的原因


3、这是为何呢?


我们查看WHATGW标准,在XMLHttpRequest中有这么一个说明:



大概意思:如果请求方法是GETHEAD ,那么body会被忽略的


所以我们虽然传递了,但是会被浏览器给忽略掉


这也是为什么使用postman可以正常请求,但是前端调不通的原因了


因为postman并没有遵循WHATWG的标准,body参数没有被忽略



3.1 fetch是否可以?


fetch.spec.whatwg.org/#request-cl…


答案:也不可以,fetch会直接报错



总结



  1. 结论:浏览器并不支持get请求将参数放在body

  2. XMLHTTPRequest会忽略body参数,而fetch则会直接报错


作者:蝼蚁之行
来源:juejin.cn/post/7283367128195055651
收起阅读 »

Systeminformation.js: 为什么不试试最强的系统信息获取工具?

web
大家好,我是徐徐。今天跟大家分享一款获取系统信息的工具库:systeminformation。前言在现代开发环境中,跨平台获取系统信息已经成为许多应用程序的重要需求。无论你是在开发需要详细系统信息的应用,还是需要获取硬件和软件的状态信息,一个强大且灵活的工具库...
继续阅读 »

大家好,我是徐徐。今天跟大家分享一款获取系统信息的工具库:systeminformation。

前言

在现代开发环境中,跨平台获取系统信息已经成为许多应用程序的重要需求。无论你是在开发需要详细系统信息的应用,还是需要获取硬件和软件的状态信息,一个强大且灵活的工具库可以显著提升你的开发效率。今天,我们要分享的是systeminformation这个 Node.js 库,可以帮你轻松获取到你想要的各种系统信息。

基本信息

什么是 systeminformation?

systeminformation 是一个轻量级的 Node.js 库,旨在提供跨平台的系统信息获取功能。无论是在 Windows、macOS 还是 Linux 上,它都能为你提供一致的接口,获取系统的硬件和软件信息。自2015年发布以来,systeminformation 已经成为开发者们获取系统信息的首选工具之一。

它提供了超过 50 个函数,用于检索详细的硬件、系统和操作系统信息。该库支持 Linux、macOS、部分 Windows、FreeBSD、OpenBSD、NetBSD、SunOS 以及 Android 系统,并且完全无依赖。无论你需要全面了解系统状况,还是仅仅想获取特定的数据,systeminformation 都能满足你的需求,帮助你在各个平台上轻松获取系统信息。

主要特点

  • 跨平台支持:支持 Windows、macOS 和 Linux 系统,提供一致的接口。
  • 全面的信息获取:能够获取 CPU、内存、磁盘、网络、操作系统等详细信息。
  • 实时监控:支持获取实时的系统性能数据,如 CPU 使用率、内存使用率、网络速度等。
  • 易于集成:通过简单的 API 调用即可获取所需信息,便于集成到各种应用程序中。

使用场景

  • 服务器监控:实时监控服务器性能,获取 CPU、内存、磁盘等硬件信息。
  • 桌面应用:获取本地系统信息,展示系统状态和性能数据。
  • IoT 设备:在物联网设备上获取系统信息,进行设备管理和监控。

快速上手

要在你的 Node.js 项目中使用 systeminformation,只需以下简单步骤:

    1. 安装 systeminformation
npm install systeminformation
    1. 获取系统信息示例
const si = require('systeminformation');

// 获取 CPU 信息
si.cpu()
.then(data => console.log(data))
.catch(error => console.error(error));

// 获取内存信息
si.mem()
.then(data => console.log(data))
.catch(error => console.error(error));

// 获取操作系统信息
si.osInfo()
.then(data => console.log(data))
.catch(error => console.error(error));
    1. 实时监控示例
const si = require('systeminformation');

// 实时监控 CPU 使用率
setInterval(() => {
si.currentLoad()
.then(data => console.log(`CPU Load: ${data.currentload}%`))
.catch(error => console.error(error));
}, 1000);

// 实时监控内存使用情况
setInterval(() => {
si.mem()
.then(data => console.log(`Memory Usage: ${data.used / data.total * 100}%`))
.catch(error => console.error(error));
}, 1000);

结语

systeminformation 是一个功能强大且灵活的 Node.js 库,能够帮助你轻松获取系统的各种信息。无论你是需要实时监控服务器性能,还是需要获取本地系统的详细信息,systeminformation 都能为你提供稳定且易用的解决方案。

希望这篇文章能帮助你了解 systeminformation 的强大功能,并激发你在项目中使用它的灵感。赶快分享给你的朋友们吧!


作者:前端徐徐
来源:juejin.cn/post/7413643760771072015
收起阅读 »

axios VS alova.js,谁是真正的通信王者?

web
新年快乐!在这个快速发展的前端世界里,咱们工程师面临的挑战也是一个接一个。今天,咱们就来聊聊前端实时通信这个话题。 想想看,你在使用那些传统的HTTP客户端时,比如axios,是否遇到过这样的问题:与React、Vue等框架的结合不够紧密,导致开发效率低下;...
继续阅读 »

新年快乐!在这个快速发展的前端世界里,咱们工程师面临的挑战也是一个接一个。今天,咱们就来聊聊前端实时通信这个话题。



想想看,你在使用那些传统的HTTP客户端时,比如axios,是否遇到过这样的问题:与React、Vue等框架的结合不够紧密,导致开发效率低下;在性能方面表现不佳,尤其是在处理频繁或重复的请求时;还有那略显臃肿的体积,以及混乱的响应数据类型定义?


哎呀妈呀,这些问题听着就让人头大。但别急,有个叫做alovajs的工具,可能会让你眼前一亮。


alovajs是一个轻量级的请求策略库,它不仅提供了与axios相似的API设计,让你能更快上手,还解决了上述的那些问题。它如何解决?咱们来一探究竟。


首先,alovajs能够与UI框架深度融合,自动管理请求相关的数据。这意味着你在Vue或React等框架中使用alovajs时,不再需要手动创建和维护请求状态,大大提高了开发效率。


其次,alovajs默认开启了内存缓存和请求共享,这些功能可以在提高请求性能的同时,提升用户体验并降低服务端的压力。比如,当你实现一个列表页,用户点击列表项进入详情页时,alovajs可以智能地使用缓存数据,避免不必要的重复请求。


最后,alovajs的体积只有4kb+,仅是axios的30%左右,而且它提供了更加直观的响应数据TS类型定义,对于重度使用Typescript的同学来说,这绝对是个福音。


说了这么多,是不是有点心动了?如果你对alovajs感兴趣,可以访问它的官网查看更多详细信息:alovajs官网。也欢迎你在评论区分享你对alovajs的看法和使用经验,让我们一起交流学习吧!
有任何问题,你可以加入以下群聊咨询,也可以在github 仓库中发布 Discussions,如果遇到问题,也请在github 的 issues中提交,我们会在最快的时间解决。


作者:胡镇alovajs
来源:juejin.cn/post/7334503381200437299
收起阅读 »

一文搞懂JS类型判断的四种方法

web
前言 在JavaScript中,类型判断是一个非常基础但也十分重要的知识点。不同的类型判断方法适用于不同的场景,掌握这些方法可以帮助我们更好地理解和使用JavaScript。本文将详细介绍typeof、instanceof、Object.prototype.t...
继续阅读 »

前言


在JavaScript中,类型判断是一个非常基础但也十分重要的知识点。不同的类型判断方法适用于不同的场景,掌握这些方法可以帮助我们更好地理解和使用JavaScript。本文将详细介绍typeofinstanceofObject.prototype.toString以及Array.isArray这四种常用的类型判断方法,并通过实例代码帮助大家加深理解。


正文


typeof


typeof操作符可以用来判断基本数据类型,如stringnumberbooleanundefinedsymbolbigint等。它对于null和所有引用类型的判断会返回"object",而对于函数则会返回"function"


特点:



  1. 可以判断除null之外的所有原始类型。

  2. 除了function,其他所有的引用类型都会被判断成object

  3. typeof是通过将值转换为二进制后判断其二进制前三位是否为0,是则为object


示例代码:


let s = '123'; // string
let n = 123; // number
let f = true; // boolean
let u = undefined; // undefined
let nu = null; // null
let sy = Symbol(123); // Symbol
let big = 1234n; // BigInt

console.log(typeof s); // "string"
console.log(typeof n); // "number"
console.log(typeof f); // "boolean"
console.log(typeof u); // "undefined"
console.log(typeof sy); // "symbol"
console.log(typeof big); // "bigint"
console.log(typeof nu); // "object" - 特殊情况

let obj = {};
let arr = [];
let fn = function() {};
let date = new Date();

console.log(typeof obj); // "object"
console.log(typeof arr); // "object"
console.log(typeof date); // "object"
console.log(typeof fn); // "function"

function isObject(o) {
if (typeof o === 'object' && o !== null) {
return true;
}
return false;
}

let res = isObject({a: 1});
console.log(res); // true

instanceof


instanceof用于检测构造函数的prototype属性是否出现在某个实例对象的原型链上。因此,它主要用于判断引用类型


特点:



  1. 只能判断引用类型。

  2. 通过原型链查找来判断类型。


示例代码:


let obj = {};
let arr = [];
let fn = function() {};
let date = new Date();

console.log(obj instanceof Object); // true
console.log(arr instanceof Array); // true
console.log(fn instanceof Function); // true
console.log(date instanceof Date); // true
console.log(arr instanceof Object); // true

console.log(arr instanceof String); // false
console.log(n instanceof Number); // false

因为原始类型没有原型而引用类型有原型,所有instanceof主要用于判断引用类型,那么根据这个我们是不是可以手写一个instanceof


手写·instanceof实现:


首先我们要知道v8创建对象自变量是这样的,拿let arr = []举例子:


function createArray() {
// 创建一个新的对象
let arr = new Array();
// 设置原型
arr.__proto__ = Array.prototype;
// 返回创建的数组对象
return arr;
}

V8 引擎会调用 Array 构造函数来创建一个新的数组对象,Array 构造函数的内部实现会创建一个新的空数组对象,并初始化其内部属性并且将新创建的数组对象的 __proto__ 属性设置为 Array.prototype,这意味着数组对象会继承 Array.prototype 上的所有方法和属性,最后,新创建的数组对象会被赋值给变量 arr


那么我们是不是可以通过实例对象的隐式原型等于其构造函数的显式原型来判断类型,代码如下:


function myInstanceOf(L,R){
if(L.__proto__ === R.prototype){
return true;
}
return false;
}

但是我们看到console.log([] instanceof Object); // true,所有还要改进一下:


我们要知道这么一件事情:



  1. 内置构造函数的原型链



    • 大多数内置构造函数(如 ArrayFunctionDateRegExpErrorNumberStringBooleanMapSetWeakMapWeakSet 等)的原型(Constructor.prototype)都会直接或间接地继承自 Object.prototype

    • 这意味着这些构造函数创建的对象的原型链最终会指向 Object.prototype



  2. Object.prototype 的原型



    • Object.prototype 的隐式原型(即 __proto__)为 null。这是原型链的终点,表示没有更多的原型可以继承。




所以我们是不是可以这样:


function myinstanceof(L, R) {
while (L !== null) {
if (L.__proto__ === R.prototype) {
return true;
}
L = L.__proto__;
}
return false;
}

console.log(myinstanceof([], Array)); // true
console.log(myinstanceof([], Object)); // true
console.log(myinstanceof({}, Array)); // false

所以就完美实现了。


Object.prototype.toString.call


Object.prototype.toString.call 是一个非常有用的工具,可以用来获取任何 JavaScript 值的类型信息。它结合了 Object.prototype.toStringFunction.prototype.call 两个方法的功能。


特点:



  1. 可以判断任何类型


代码示例


console.log(Object.prototype.toString.call(null));       // [object Null]
console.log(Object.prototype.toString.call(123)); // [object Number]
console.log(Object.prototype.toString.call([])); // [object Array]
console.log(Object.prototype.toString.call({})); // [object Object]
console.log(Object.prototype.toString.call('hello')); // [object String]
console.log(Object.prototype.toString.call(true)); // [object Boolean]
console.log(Object.prototype.toString.call(undefined)); // [object Undefined]
console.log(Object.prototype.toString.call(Symbol())); // [object Symbol]
console.log(Object.prototype.toString.call(123n)); // [object BigInt]

Object.prototype.toString


底层逻辑


根据官方文档,Object.prototype.toString 方法的执行步骤如下:



  1. 如果此值未定义,则返回 "[object undefined]"

  2. 如果此值为 null,则返回 "[object Null]"

  3. 定义 O 是调用 ToObject (该方法作用是把 O 转换为对象) 的结果,将 this 值作为参数传递

  4. 定义 class 是 O 的 [[Class]] 内部属性的值

  5. 返回 "[object" 和 class 和 "]" 组成的字符串的结果


关键点解释



  • ToObject 方法:将传入的值转换为对象。对于原始类型(如 stringnumberboolean),会创建对应的包装对象(如 StringNumberBoolean)。对于 null 和 undefined,会有特殊处理。

  • [[Class]] 内部属性:每个对象都有一个 [[Class]] 内部属性,表示对象的类型。例如,数组的 [[Class]] 值为 "Array",对象的 [[Class]] 值为 "Object"


console.log(Object.prototype.toString(123));//[object Object]
console.log(Object.prototype.toString('123'));//[object Object]
console.log(Object.prototype.toString({}));//[object Object]
console.log(Object.prototype.toString([]));//[object Object]

为什么需要 call


Object.prototype.toString 方法默认的 this 值是 Object.prototype 本身。如果我们直接调用 Object.prototype.toString(123)this 值仍然是 Object.prototype,而不是我们传入的值。因此,我们需要使用 call 方法来改变 this 值,使其指向我们传入的值。


手写call


obj = {
a:1,
}

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

//我们需要将foo中的this指向obj里面
Function.prototype.myCall = function(context){
if(!(this instanceof Function)){ 在构造函数原型上,this指向的是实例对象,这里即foo
return new TypeError(this+'is not function')
}

const fn = Symbol('key'); //使用symbol作为key是因为可能会同名
context[fn] = this;//添加变量名为fn,值为上面的,context={Symbol('key'): foo}
context[fn](); // 触发了隐式绑定
delete context[fn]; //删除这个方法
}

foo.myCall(obj) // 1
console.log(obj); // {a:1}

我们知道call方法是将函数里面的this强行掰弯到我们传入的对象里面去,它的原理是这样的,首先判断你传入的参数是不是一个函数,因为只有函数身上才有call方法,函数调用然后通过隐式绑定规则,将this指向这个对象,那么不就强行更改了this的指向,[不知道this的可以看这篇文章](你不知道的JavaScript(核心知识点概念详细整理-掘金 (juejin.cn))


Array.isArray


Array.isArray是一个静态方法,用于检测给定的值是否为数组。


示例代码:


let arr = [];
let obj = {};

console.log(Array.isArray(arr)); // true
console.log(Array.isArray(obj)); // false

手写Array.isArray实现:


function myIsArray(value) {
return Object.prototype.toString.call(value) === '[object Array]';
}

console.log(myIsArray(arr)); // true
console.log(myIsArray(obj)); // false

总结



  • typeof适合用于检查基本数据类型,但对于null和对象类型的判断不够准确。

  • instanceof用于检查对象的构造函数,适用于引用类型的判断。

  • Object.prototype.toString提供了一种更通用的方法来判断所有类型的值。

  • Array.isArray专门用于判断一个值是否为数组。


希望这篇文章能够帮助你更好地理解和使用JavaScript中的类型判断方法,感谢你的阅读!


image.png


作者:反应热
来源:juejin.cn/post/7416657615369388084
收起阅读 »

大屏页面崩溃排查(Too many active WebGL contexts. Oldest context will be lost)

web
1 问题背景 顾问反馈大屏在反复切换的过程中,页面会出现白屏,导致后续的操作无法进行,只能通过刷新页面才能恢复正常。 我们的页面类似于这样的布局(下方的是直接从网络上找的截图) 点击下方红线框住的区域,可以展示不同的图表(echarts图表) 区别在于我们的...
继续阅读 »

1 问题背景


顾问反馈大屏在反复切换的过程中,页面会出现白屏,导致后续的操作无法进行,只能通过刷新页面才能恢复正常。



  1. 我们的页面类似于这样的布局(下方的是直接从网络上找的截图

  2. 点击下方红线框住的区域,可以展示不同的图表(echarts图表)

  3. 区别在于我们的主区域不是图片,用的是基于cesium封装的地图(webgl


image.png


2 问题复现


测试同事经过几分钟的快速切换导航后,复现了,报错了如下内容
image.png


问题如果复现了,其实就解决了一半了


3 查找问题


经过复现后,发现除了上面的报错,每当页面崩溃前,chrome总会有下方的warning。然后基于cesium封装的地图就会崩溃。


image.png


翻译成中文:警告:目前有了太多激活的webgl上下文,最早的上下文将会丢失


4 排查问题


经过和地图组的人沟通,得到一个结论WebGL一个页面上最多有16个实例



  • 怀疑echarts在下方菜单切换过程中,没有进行销毁


检查了代码中的echats的页面在销毁的时候,发现都进行了dispose,排除了这个的原因



  • 怀疑起echarts的3d的饼状图
    之前设计师设计了一个3d的饼状图,参考了 3d柄图环图,一个页面上有多这个组件。
    效果如下:


f8a015b3-0b94-4fd2-a3b3-07a745fa401a.gif


5 锁定组件进行验证



  1. 先把一个页面上的所有组件改为上方的饼状图,然后点击导航栏,频繁进行切换,

  2. 页面很快就崩溃了,然后检查这个组件在页面销毁的时候,是否进行dispose
    检查后,发现没有,添加后进行测试,问题依旧

  3. 继续检查发现这个组件导入了echarts-gl,就去ecahrts的github的issues进行搜索,终于找到了一个类似的问题
    github.com/ecomfe/echa…


image.png


加入了类似的代码,进行验证后解决了此问题


6 总结



  1. chrome浏览器中最多有16个webgl的实例。当过多的时候,会把最早创建的实例销毁

  2. 当使用echarts在页面销毁的时候及时进行dispose,释放上下文

  3. 当使用echarts-gl的时候,调用dispose的时候是不生效的,需要找到页面上的canvas,然后手动将上下文释放,类似下方的代码


const canvasArr = myChart.getDom().getElementsByTagName('canvas');
for(let i=0; i<canvasArr.length; i++){
canvasArr[i].getContext('webgl').getExtension('WEBGL_lose_context').loseContext()
}


7 参考文档



作者:pauldu
来源:juejin.cn/post/7351712561672798260
收起阅读 »

谁也别拦我们,网页里直接增删改查本地文件!

web
欢迎来到 Jax 的专栏「Web 玩转文件操作」,快来了解 Web 端关于文件操作的方方面面! 转载请联系作者 Jax。 先来玩玩这个 Demo —— 一个网页端的本地文件管理器。 在上面的 Demo 中,点击「打开文件夹」按钮后,你可以选择电脑本地的某个...
继续阅读 »

欢迎来到 Jax 的专栏「Web 玩转文件操作」,快来了解 Web 端关于文件操作的方方面面!


转载请联系作者 Jax。



先来玩玩这个 Demo —— 一个网页端的本地文件管理器


demo.gif


在上面的 Demo 中,点击「打开文件夹」按钮后,你可以选择电脑本地的某个文件夹来打开。然后就能对其中的文件做各种操作了,包括新建、右键打开、编辑、删除等等。你会发现,在网页上的操作会立刻在本地文件上实际生效。


如果你感觉是:”哟?有点儿意思!“,那么这篇文章就是专门为你而写的,读下去吧。


正所谓「边牧是边牧、狗是狗」,Web 应用早已不是当初的简陋网页了,它进化出了令人眼花缭乱的新能力,同时也在迅速追齐系统原生能力。如 Demo 所示,我们甚至可以把文件管理系统搬进网页,并且简单的 JavaScript 代码就能实现。Web 应用能有这样的威力,得益于浏览器对文件系统 API 的实现。


文件系统(File System Access)API 目前还处于社区草稿状态,但早在 2016 年,WHATWG 社区就已经开始着手设计这一标准,并且八年来一直在持续不断地打磨,迄今已经被各大主流浏览器所实现。


venders.jpeg


这套 API 足够简单易用,你很快就能掌握它,并且让它成为你的得力助手。但由于涉及到文件管理这个课题,它有很多方面都可以挖掘得很深很深,比如读写流、OPFS、安全策略等等。咱们这个专栏本着少吃多餐的原则,不会一次性囊括那么多内容。咱们的这第一铲,就先浅浅地挖一个「憨豆」 —— FileSystemHandle


FileSystemHandle


在文件系统中,文件和文件夹是一个个实体,是所有状态和交互的核心。相对应地,在文件系统 API 中,我们用「FileSystemHandle」这个对象来对实体进行抽象。为了能够简洁地描述,我们在下文中将称之为「憨豆」。我们用对象属性来保存实体的名称、类型,通过执行对象方法来操作实体。


那么 FileSystemHandle 从何而来呢?一般是从用户操作而来。以文件为例,当用户通过文件窗口选择了某个本地文件之后,我们就能从代码层面获取到通向这个文件的入口,从而可以对文件进行管理操作。除此之外,拖拽文件进入也能得到憨豆


属性:name 和 kind


name:无论是文件还是文件夹,必然都有一个名字。


kind:实体的类型,值为 ‘file’ 代表文件;值为 ‘directory’ 代表文件夹。


校验方法 isSameEntry()


用于判断两个憨豆是否代表相同的实体。例如用户上传图片时,先后两次选择的是同一张图片,那么两个憨豆指向的其实是同一个文件。


const [handle1] = await showOpenFilePicker() // 第一次上传
const [handle2] = await showOpenFilePicker() // 第二次选择了同一个文件

const isSame = await handle1.isSameEntry(handle2)
console.log(isSame) // true

该方法也同样适用于文件夹校验。


我们可以借此来检测重复性。


删除方法 remove()


用于删除实体。比如,执行下列代码会现场抽取一个幸运文件来删除:


const [handle] = await showOpenFilePicker()
handle.remove()

但如果我们想要选中文件夹来删除,像上面那样直接调用是会报错并删除失败的。我们需要额外传入配置参数:


handle.remove({ recursive: true })

传参后的删除,是对文件夹里面的嵌套结构进行递归删除,这个设计理念旨在让开发者和用户在删除前知悉操作后果。


权限方法 queryPermission() 和 requestPermission()


用于向用户查询和获取读写权限,可以传入一个参数对象来细分权限类型。


const permission = await handle.queryPermission({ mode: 'read' }) // mode 的值还可以是 readwrite
console.log(permission)// 若值为 'granted',则代表有足够权限

我们在对实体进行操作前,最好总是先查询权限。因此一个最佳实践是把这两个方法封装进通用操作逻辑中。


其他特性


除此之外,FileSystemHandle 还具有一些其他特性,比如可以转化为 indexDB 实例、可以通过 postMessage 传输到 Web Workers 里进行操作等等。我们会在后续的专栏文章中继续顺藤摸瓜地了解它们。


两个子类


到目前为止,FileSystemHandle 所具有的属性和方法都在上面了。你可能也意识到了,单靠这三招两式是不可能实现像 Demo 里那么丰富完备的文件操作的。


没错,这个憨豆只是一个抽象父类,它还有两个子类 FileSystemFileHandleFileSystemDirectoryHandle,这两位才是文件和文件夹实体的真正代言人。对实体的常规操作,几乎都是通过这两个紫憨豆来执行的。


除了从父类继承而来的基因,两个紫憨豆都各自具有独特的属性和方法,满足了开发者对文件和文件夹两种不同实体的不同操作需求。


FileSystemFileHandle


在本专栏的上一篇文章《绕过中间商,不用 input 标签也能搞定文件选择》中,我们曾与文件憨豆有过一面之缘。那时候,我们通过 showOpenFilePicker 获取了文件憨豆,并调用它的 getFile 方法拿到了 文件 Blob


此外,文件憨豆还具有的方法如下:



  • createSyncAccessHandle():用于同步读写文件,但是仅限于在 Web Workers 中。

  • createWritable:创建一个写入流对象,用于向文件写入数据。


FileSystemDirectoryHandle


文件夹憨豆的特有方法如下:



  • getDirectoryHandle():按名称查找子文件夹。

  • getFileHandle():按名称查找子文件。

  • removeEntry():按名称移除子实体。

  • resovle():返回指向子实体的路径。


经过上述罗列,我们对两种憨豆的能力有了一个大概的印象。下面,我们将回到 Demo 的交互场景,从具体操作上手,看看文件系统 API 是如何在 JavaScript 中落地的。


操作 & 用法


载入文件夹


我们首先需要载入一个文件夹,然后才能对其中的子实体进行操作。


如果你碰巧读过上一篇文章,知道如何用 showOpenFilePicker() 选择文件,那你一定能举一反三推断出,选择文件夹的方法是 showDirectoryPicker()


const dirHandle = await showDirectoryPicker()

showDirectoryPicker 方法也接收一些参数,其中 idstartIn 这两个参数与 showOpenFilePicker 方法 的同名参数完全对应。另外还支持一个参数 mode ,其值可以是 readreadwrite,用于指定所需的权限。


用户选择文件夹后得到的 dirHandle,就是一个 FileSystemDirectoryHandle 格式的对象。我们可以遍历出它的子实体:


for await (const sub of dirHandle.values()) {
const { name, kind } = sub
console.log(name, kind)
}

从子实体中取出名称和类别属性值,就可以对文件夹内容一目了然了。


读取文件内容


在上一步中,我们已经读取到了子实体的名字和类型,那么对于文件类型,我们可以先按名称检索到对应的憨豆:


// if sub.kind === 'file'
const fileHandle = await dirHandle.getFileHandle(sub.name)

再从文件憨豆中掏出文件 Blob,进一步读取到文件的内容:


const file = await fileHandle.getFile()
const content = file.text()

如果你用来调试的文件是文本内容的文件,那么打印 content 的值,你就可以看到内容文本了。


同理,获取子文件夹的方式是 dirHandle.getDirectoryHandle(sub.name)


新建文件、文件夹


除了指定名称参数,getFileHandlegetDirectoryHandle 这两个方法还支持第二个参数,是一个一个配置对象 { create: true/false },用于应对指定名称的实体不存在的情况。


例如,我们对一个文件夹实体执行 dirHandle.getFileHandle('fileA'),但该文件夹中并没有名为 fileA 的文件。此时第二个参数为空,等同于 create 的默认值为 false,那么此时会抛出一个 NotFoundError 错误,提示我们文件不存在。


而如果我们这样执行:dirHandle.getFileHandle('fileA', { create: true }),那么就会在当前文件夹中新建一个名为 fileA 的空文件。


同理,我们也可以用 dirHandle.getDirectoryHandle('dirA', { create: true }) 新建一个名为 dirA 的空文件夹。


在 Demo 中,我们实现了让用户在新建文件时自定义文件名,这其实是使用了 prompt 方法:


const fileName = prompt('请输入文件名')
await dirHandle.getFileHandle(fileName, { create: true })

在当下这个 AI 时代,Prompt 更多是以「LLM 提示词」被大家初识。在更早时候,它是浏览器实现的一个组件。其实这两种形态的含义是一致的,都是在人机交互中,从一端到另一端的输入输出流程。


编辑文件内容


刚刚我们读取了文件的内容,现在我们来对文件内容进行修改,然后再存回去。


我们已经能够通过 getFile() 方法拿到文本内容,那应该把内容放到哪里进行编辑呢?你有很多种选择:富文本编辑器、给 div 设置 contenteditable、唤起 VS Code…… 但本着最(能)小(用)原(就)则(行),我们还有更便捷的选项 —— prompt!


prompt() 方法也支持第二个参数,我们把文本内容传入,弹出弹窗后,你就会看到内容已经填在了输入框中,现在就可以随意编辑里面的字符了。


const file = await fileHandle.getFile()
const fileContent = await file.text()
const newContent = prompt('', fileContent) // newContent 即为修改后的文件内容

但是点击 Prompt 的确认按钮并不会让新内容自动写入文件,这里就需要用到上面提到的 createWritable 了。下面是一个完整的写入流流程:


const writable = await fileHandle.createWritable() // 对待修改文件开启写入流
await writable.write(newContent) // 把新内容写入到文件
await writable.close() // 关闭写入流

至此,新的内容就已经被保存到文件中了。你可以在 Demo 中再次右键、打开文件查看内容,你会发现确实读取到了修改后的内容。


文件重命名


修改文件名也是文件管理中的常规操作,文件系统 API 也提供了对应的方法来实现。可能手快的同学已经在尝试 fileHandle.rename() 方法了。但 API 中还真没有这个方法,我们其实是要用一个 move() 方法。惊不惊喜意不意外?


因为从底层视角看,重命名文件和移动文件的处理过程类型,都是需要先新建一个文件(使用新的命名或者放到新的位置),再把源文件的数据复制过去,最后把源文件删除。目前在 Web 端还没有更高效的处理方式。


我们只需从 Prompt 获取新名称,再传给 move() 方法即可:


const newName = prompt('请输入新的文件名')
await fileHandle.move(newName)

这样,文件重命名就搞定了。


删除文件、文件夹


删除实体就没有那么多幺蛾子了,我们甚至不用对文件和文件夹做逻辑上的区分,简单直接地调用 currentHandle.remove({ recursive: true }) 就行了。


但越是方便,就越要谨慎,因为涉及到删除用户的文件,所以如果要用于生产环境,最好给用户提供二次确认的机会。


写在结尾


恭喜你读完了本文,你真棒!


这次我们通过实现一个 Web 版文件管理器 demo,对文件管理 API 进行了较为深入的理解和实践。我再嘱咐两句:



  1. 涉及到操作用户文件,请务必谨慎。

  2. 为了保障安全性,文件系统 API 仅支持 https。



我是 Jax,在畅游 Web 技术领域的第 7 年,我仍然是坚定不移的 JavaScript 迷弟,Web 开发带给我太多乐趣。如果你也喜欢 Web 技术,或者想讨论本文内容,欢迎来聊!你可以通过下列方式找到我:


掘金:juejin.cn/user/113435…


GitHub:github.com/JaxNext


微信:JaxNext



作者:JaxNext
来源:juejin.cn/post/7416933490136252452
收起阅读 »

微信小程序避坑scroll-view,用tween.js实现吸附动画

web
背景 在开发一个小程序项目时,遇到一个需要列表滚动,松手时自动吸附的动画需求,如下(最终效果): 很自然用了scroll-view组件,但使用过程中发现scroll-view 里包含'position: fixed;top:0;'的元素时,应用scroll-...
继续阅读 »

背景


在开发一个小程序项目时,遇到一个需要列表滚动,松手时自动吸附的动画需求,如下(最终效果):


吸附动画.gif


很自然用了scroll-view组件,但使用过程中发现scroll-view 里包含'position: fixed;top:0;'的元素时,应用scroll-with-animation=true,搭配更改scroll-top时,松手后fixed的元素会抖一下......


问题.gif


于是决定不用组件内置的scroll-with-animation,改用手动控制scroll-top实现吸附的效果。


思路


通常,要做动画,我们就得确定以下信息,然后用代码实现:



  • 初始状态

  • 结束状态

  • 动画时长

  • 动画过程状态如何变化(匀速/先加速后减速/...)


这四个信息一般从UI/交互那里确认,前三个代码实现很简单,第四个在css动画里(如transition/animation)用 timing-function 指定:


image.png


在js动画里,改变css的属性可通过 Web Animations API 里的 easing 属性指定:


image.png


而如果需要动画的状态不是css的属性呢(例如上面的scrollTop)?这就要用到补间/插值工具了,tween.js登场!


关于 tween.js


tween翻译有‘补间‘的意思



补间(动画)(来自 in-between)是一个概念,允许你以平滑的方式更改对象的属性。你只需告诉它哪些属性要更改,当补间结束运行时它们应该具有哪些最终值,以及这需要多长时间,补间引擎将负责计算从起始点到结束点的值。



简单点就是tweenjs可以指定状态从初始值到结束值该怎么变化,下面是简单的使用例子:


const box = document.getElementById('box') // 获取我们想要设置动画的元素。
const coords = {x: 0, y: 0} // 从 (0, 0) 开始

const tween = new TWEEN.Tween(coords, false) // 创建一个修改“坐标”的新 tween。
.to({x: 300, y: 200}, 1000) // 在 1 秒内移动到 (300, 200)。
.easing(TWEEN.Easing.Quadratic.InOut) // 使用缓动函数使动画流畅。
.onUpdate(() => {
// 在 tween.js 更新“坐标”后调用。
// 使用 CSS transform 将 'box' 移动到 'coords' 描述的位置。
box.style.setProperty('transform', 'translate(' + coords.x + 'px, ' + coords.y + 'px)')
})
.start() // 立即开始 tween。

// 设置动画循环。
function animate(time) {
tween.update(time)
requestAnimationFrame(animate)
}
requestAnimationFrame(animate)


在微信小程序里使用tween.js


导入适配


下载 github.com/tweenjs/twe… 文件,修改下‘now’的实现,把performance.now()改成Date.now() 即可在小程序里使用:


image.png


动画循环


小程序里没有直接支持requestAnimationFrame,这个可以用canvas组件的requestAnimationFrame方法代替:


    // wxml
// ...
<canvas type="2d" id="canvas" style="width: 0; height: 0; pointer-events: none; position: fixed"></canvas>
// ...


// js
wx.createSelectorQuery()
.select("#canvas")
.fields({
node: true,
})
.exec((res) => {
this.canvas = res[0].node;
});
// ...

// ...
const renderLoop = () => {
TWEEN.update();
this.canvas.requestAnimationFrame(renderLoop);
};
renderLoop();

其他


锁帧


手动改scrolltop还是得通过setData方法,频繁调用可能会导致动画卡顿,requestAnimationFrame一般1s跑60次,也就是60fps,根据需要可以增加锁帧逻辑:


const fps = 30; // 锁30fps
const interval = 1000 / fps;
let lastTime = Date.now();
const renderLoop = () => {
this.canvas.requestAnimationFrame(renderLoop);

const now = Date.now();
if(now - lastTime > interval){
// 真正的动作在这里运行
TWEEN.update();
lastTime = now;
}
};
renderLoop();

官方支持?


要是 scrollView 组件支持 wxs 更改scrollTop就好了
developers.weixin.qq.com/community/d…


作者:思路为王
来源:juejin.cn/post/7300771357523820594
收起阅读 »

前端滑块旋转验证登录

web
效果图如下 实现: 封装VerifyImg组件 <template> <el-dialog v-model="dialogShow" width="380px" top="24vh" class="verifyDialog"> ...
继续阅读 »
效果图如下

效果.gif


实现: 封装VerifyImg组件

<template>
<el-dialog v-model="dialogShow" width="380px" top="24vh" class="verifyDialog">
<div class="verify-v">
<div
class="check"
@mousedown="onMouseDown"
@mouseup="onMouseUp"
@mousemove="onMouseMove"
@touchstart="onTouchStart"
@touchmove="onTouchMove"
@touchend="onTouchEnd"
>

<p>拖动滑块使图片角度为正</p>
<div class="img-con">
<img :src="imgUrl" :style="{ transform: imgAngle }" />
<div v-if="showError" class="check-state">验证失败</div>
<div v-else-if="showSuccess" class="check-state">验证成功</div>
<div v-else-if="checking" class="check-state">验证中</div>
</div>
<div
ref="sliderCon"
class="slider-con"
:class="{ 'err-anim': showError }"
:style="{ '--percent': percent, '--bgColor': showError ? bgError : bgColor }"
>

<div ref="slider" class="slider" id="slider" :class="{ sliding }" :style="{ '--move': `${slidMove}px` }">
<el-icon size="22"><Right id="slider" /></el-icon>
</div>
</div>
</div>
</div>
</el-dialog>
</template>
<script>
export default {
data() {
return {
imgUrl: '',
dialogShow: false,
showError: false,
showSuccess: false,
checking: false,
sliding: false,
slidMove: 0,
percent: 0,
sliderConWidth: 0,
bgColor: 'rgba(25, 145, 250, 0.2)',
bgError: 'rgba(255,78,78,0.2)',
imgList: [
new URL(`../../assets/images/verify/fn1.png`, import.meta.url).href,
new URL(`../../assets/images/verify/fn2.png`, import.meta.url).href,
new URL(`../../assets/images/verify/fn3.png`, import.meta.url).href
]
}
},

computed: {
angle() {
let sliderConWidth = this.sliderConWidth ?? 0
let sliderWidth = this.sliderWidth ?? 0
let ratio = this.slidMove / (sliderConWidth - sliderWidth)
return 360 * ratio
},
imgAngle() {
return `rotate(${this.angle}deg)`
}
},
mounted() {
this.imgUrl = this.imgList[this.rand(0, 2)]
},

methods: {
onTouchMove(event) {
console.log('onTouchMove')
if (this.sliding && this.checking === false) {
// 滑块向右的平移距离等于鼠标移动事件的X坐标减去鼠标按下时的初始坐标。
let m = event.touches[0].clientX - this.sliderLeft
if (m < 0) {
// 如果m小于0表示用户鼠标向左移动超出了初始位置,也就是0
// 所以直接等于 0,以防止越界
m = 0
} else if (m > this.sliderConWidth - this.sliderWidth) {
// 滑块向右移动的最大距离是滑槽的宽度减去滑块的宽度。
// 因为css的 translateX 函数是以元素的左上角坐标计算的
// 所以要减去滑块的宽度,这样滑块在最右边时,才不会左上角和滑槽右上角重合。
m = this.sliderConWidth - this.sliderWidth
}
this.slidMove = m
console.log(this.slidMove)

this.percent = ((this.slidMove / document.querySelector('.slider-con').offsetWidth) * 100).toFixed(2) + '%'
}
},
onTouchEnd() {
console.log('onTouchEnd')
if (this.sliding && this.checking === false) {
this.checking = true
this.validApi(this.angle)
.then(isok => {
if (isok) {
this.showSuccess = true
} else {
this.showError = true
}
return new Promise((resolve, reject) => {
// setTimeout(() => {
if (isok) {
resolve(Math.round(this.angle))
} else {
reject()
}
setTimeout(() => {
this.resetSlider()
}, 1000)
// }, 1500)
})
})
.then(angle => {
// 处理业务,或者通知调用者验证成功
console.log(angle)
this.$emit('toLogin')
})
.catch(e => {
console.log(e)

// alert('旋转错误')
})
}
},
onTouchStart(event) {
console.log('onTouchStart', event)
// 设置状态为滑动中
this.sliding = true
// 下面三个变量不需要监听变化,因此不放到 data 中
this.sliderLeft = event.touches[0].clientX // 记录鼠标按下时的x位置
this.sliderConWidth = this.$refs.sliderCon.clientWidth // 记录滑槽的宽度
this.sliderWidth = this.$refs.slider.clientWidth // 记录滑块的宽度
console.log(this.sliderLeft, this.sliderConWidth, this.sliderWidth)
},
rand(m, n) {
return Math.ceil(Math.random() * (n - m + 1) + m - 1)
},

showVerify() {
this.imgUrl = this.imgList[this.rand(0, 2)]
this.dialogShow = true
},
closeVerify() {
//1.5s后关闭弹框
setTimeout(() => {
this.dialogShow = false
}, 1500)
},
// 重置滑块
resetSlider() {
this.sliding = false
this.slidMove = 0
this.checking = false
this.showSuccess = false
this.showError = false
this.percent = 0
},
//拖拽开始
onMouseDown(event) {
console.log(event.target.id, this.checking)
if (event.target.id !== 'slider') {
return
}

if (this.checking) return
// 设置状态为滑动中
this.sliding = true
// 下面三个变量不需要监听变化,因此不放到 data 中
this.sliderLeft = event.clientX // 记录鼠标按下时的x位置
this.sliderConWidth = this.$refs.sliderCon.clientWidth // 记录滑槽的宽度
this.sliderWidth = this.$refs.slider.clientWidth // 记录滑块的宽度
},
//拖拽停止
onMouseUp(event) {
if (this.sliding && this.checking === false) {
this.checking = true
this.validApi(this.angle)
.then(isok => {
if (isok) {
this.showSuccess = true
} else {
this.showError = true
}
return new Promise((resolve, reject) => {
// setTimeout(() => {
if (isok) {
resolve(Math.round(this.angle))
} else {
reject()
}
setTimeout(() => {
this.resetSlider()
}, 1000)
// }, 1500)
})
})
.then(angle => {
// 处理业务,或者通知调用者验证成功
console.log(angle)
this.$emit('toLogin')
})
.catch(e => {
console.log(e)

// alert('旋转错误')
})
}
},
//拖拽进行中
onMouseMove(event) {
if (this.sliding && this.checking === false) {
// 滑块向右的平移距离等于鼠标移动事件的X坐标减去鼠标按下时的初始坐标。
let m = event.clientX - this.sliderLeft
if (m < 0) {
// 如果m小于0表示用户鼠标向左移动超出了初始位置,也就是0
// 所以直接等于 0,以防止越界
m = 0
} else if (m > this.sliderConWidth - this.sliderWidth) {
// 滑块向右移动的最大距离是滑槽的宽度减去滑块的宽度。
// 因为css的 translateX 函数是以元素的左上角坐标计算的
// 所以要减去滑块的宽度,这样滑块在最右边时,才不会左上角和滑槽右上角重合。
m = this.sliderConWidth - this.sliderWidth
}
this.slidMove = m
this.percent = ((this.slidMove / document.querySelector('.slider-con').offsetWidth) * 100).toFixed(2) + '%'
}
},
// 验证角度是否正确
validApi(angle) {
return new Promise((resolve, reject) => {
// 模拟网络请求
setTimeout(() => {
// 图片已旋转的角度
const imgAngle = 90
// 图片已旋转角度和用户旋转角度之和
let sum = imgAngle + angle
// 误差范围
const errorRang = 20
// 当用户旋转角度和已旋转角度之和为360度时,表示旋转了一整圈,也就是转正了
// 但是不能指望用户刚好转到那么多,所以需要留有一定的误差
let isOk = Math.abs(360 - sum) <= errorRang

resolve(isOk)
}, 1000)
})
}
}
}
</script>

<style lang="scss">
.verifyDialog {
.el-dialog__body {
padding: 15px !important;
}
}
</style>
<style lang="scss" scoped>
.verify-v {
display: flex;
justify-content: center;
align-items: center;
}
.check {
--slider-size: 40px;
width: 300px;
background: white;
box-shadow: 0px 0px 12px rgb(0 0 0 / 8%);
border-radius: 5px;
padding: 10px 0;
display: flex;
flex-direction: column;
align-items: center;
.img-con {
position: relative;
overflow: hidden;
width: 120px;
height: 120px;
border-radius: 50%;
margin-top: 20px;
img {
width: 100%;
height: 100%;
user-select: none;
}
.check-state {
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
color: white;
position: absolute;
top: 0;
left: 0;
display: flex;
justify-content: center;
align-items: center;
}
}
}
.check .slider-con {
width: 80%;
height: var(--slider-size);
border-radius: 3px;
margin-top: 1rem;
position: relative;
background: #f5f5f5;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1) inset;
background: linear-gradient(to right, var(--bgColor) 0%, var(--bgColor) var(--percent), #fff var(--percent), #fff 100%);
.slider {
&:hover {
background: #1991fa;
color: #fff;
}
background: #fff;
width: var(--slider-size);
height: var(--slider-size);
border-radius: 3px;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.2);
cursor: move;
display: flex;
justify-content: center;
align-items: center;
--move: 0px;
transform: translateX(var(--move));
.sliding {
background: #4ed3ff;
}
}
}
.slider-con.err-anim {
animation: jitter 0.5s;
.slider {
background: #ff4e4e;
}
}
body {
padding: 0;
margin: 0;
background: #fef5e0;
}

@keyframes jitter {
20% {
transform: translateX(-5px);
}
40% {
transform: translateX(10px);
}
60% {
transform: translateX(-5px);
}
80% {
transform: translateX(10px);
}
100% {
transform: translateX(0);
}
}
</style>


使用

<VerifyImg ref="verifyRef" @to-login="handleLogin"></VerifyImg>


handleLogin(){
...
}

作者:点赞侠01
来源:juejin.cn/post/7358004857889275958
收起阅读 »

API接口超时,网络波动,不要一直弹Alert了!

web
前言前段时间,我司的IoT平台的客户大量上新了一堆设备后,在使用过程中,出现了网络连接超时,服务器错误等问题,客户小题大做,抓着这个尾巴不放手,喷我司系统不健壮,要求我们杜绝API错误。由于IoT多是日志、设备、测试、Action、图表、通信类的功能,API请...
继续阅读 »

前言

前段时间,我司的IoT平台的客户大量上新了一堆设备后,在使用过程中,出现了网络连接超时服务器错误等问题,客户小题大做,抓着这个尾巴不放手,喷我司系统不健壮,要求我们杜绝API错误

由于IoT多是日志、设备、测试、Action、图表、通信类的功能,API请求&返回数据量大、时间长。在后端添加索引的情况下,接口仍然达不到秒级,接口普遍都是1~3s,部分接口甚至达到小10s。我司把服务器的硬盘和CPU都进行了一波升级,效果都不是很理想。

这个烫手的山芋,落在了前端的头上。我们被迫进行了一波系统升级优化

解决方案

我们结合这个需求,制定了以下几条标准:

  1. 不能入侵其他的功能
  2. 对系统的破坏尽可能的小
  3. 杜绝或者尽可能的减少弹框问题
  4. 保证数据的正确展示,对于错误要正确的暴露出来

根据以上几条标准,于是方案就自然的确定了:

API请求时间

拉长API的请求时间,将超时时间由30s,更新为60s

const service = axios.create({
baseURL: '/xxx',
timeout: 60 * 1000 // 请求超时时间
})

重发机制

  1. API请求超时: 当新页面大量接口请求,某个接口请求时间过长,总时间>60s时,我们会对这个接口进行至多重发3次,用180s的时间去处理这个接口,当请求成功后,关闭请求

重发的接口不能超过timeout时长,否则一直会重发失败。也可以自定义重发的时间

  1. 偶发的服务器异常: 当接口出现50X时,重发一次

可以使用axois自带的方法,也可以使用axios-retry插件,axios-retry插件更简单,底层也是axois方法实现的。相比较而言,axios-retry更简单,但不知为什么,我没有实现

// request.js
// 默认重发3次,每次的间隔时间为3s
service.defaults.retry = 3;
service.defaults.retryDelay = 3000;

export function againRequest(
error,
axios,
time = error.config.retry
) {
const config = error.config;

if (!config || !config.retry) return Promise.reject(error);

// 设置用于记录重试计数的变量 默认为0
config.__retryCount = config.__retryCount || 0;

// 判断是否超过了重试次数
if (config.__retryCount >= time) {
// alert

return Promise.reject(error);
}

config.__retryCount += 1;

const backoff = new Promise(resolve => {
setTimeout(() => {
resolve();
}, config.retryDelay || 1);
});

return backoff.then(() => {
/*
以下三行,根据项目实际调整
这三行是处理重发请求接口变成string,并且重定向的问题
*/

if (config.data && isJsonStr(config.data)) {
config.data = JSON.parse(config.data);
}
config.baseURL = "/";

return axios(config);
});
}

export let isLoop = config => {
if (
config.isLoop &&
config.isLoop.count >= 0 &&
config.url == config.isLoop.url
) {
return true;
}
return false;
};

export let isJsonStr = str => {
if (typeof str == "string") {
try {
var obj = JSON.parse(str);
if (typeof obj == "object" && obj) {
return true;
} else {
return false;
}
} catch (e) {
console.log("error: " + str + "!!!" + e);
return false;
}
}
};

注意到是: axois不能是0.19.x

issue中提到使用0.18.0。经过实践,0.20.x也是可以的,但需要进行不同的配置,具体请看issue。参考资料中有提到 axios issues github

也可以使用axios-retry

axios-retry

npm install axios-retry

// ES6
import axiosRetry from 'axios-retry';

axiosRetry(axios, { retries: 3 });

取消机制

当路由发生变化时,取消上一个路由正在请求的API接口

监控路由页面: 调用cancelAllRequest方法

// request.js
const pendingRequests = new Set();

service.cancelAllRequest = () => {
pendingRequests.forEach(cancel => cancel());
pendingRequests.clear();
};

轮询

轮询有2种情况,一种是定时器不停的请求,一种是监听请求N次后停止。

比如: 监听高低电平的变化 - 如快递柜的打开&关闭。

  1. 一直轮询的请求:

    • 使用WebSocket
    • 连续失败N次后,谈框。
  2. 轮询N次的请求:

    • 连续失败N次后,谈框。
export function api(data, retryCount) {
return request({
url: `/xxx`,
method: "post",
isLoop: {
url: "/xxx",
count: retryCount
},
data: { body: { ...data } }
});
}

自定义api url的原因是:

同一个页面中,有正常的接口和轮询的接口,url是区分是否当前的接口是否是轮询的接口

监听滚动

对于图表类的功能,监听滚动事件,根据不同的高度请求对应的API

节流机制

  1. 用户连续多次请求同一个API
    • 按钮loading。最简单有效
    • 保留最新的API请求,取消相同的请求

错误码解析

网络错误 & 断网

if (error.toString().indexOf("Network Error") !== -1) {
if (configData.isLoop && configData.isLoop.count >= 0) {
networkTimeout();
// 关闭所有的定时器
let t = setInterval(function() {}, 100);
for (let i = 1; i <= t; i++) {
clearInterval(i);
}
return;
}
networkTimeout();
}

404

else if (error.toString().indexOf("404") !== -1) {
// 404
}

401

else if (error.toString().indexOf("401") !== -1) {
// 清除token,及相应的数据,返回到登录页面
}

超时

else if (error.toString().indexOf("Error: timeout") !== -1) {
if (!axios.isCancel(error) && !isLoop(configData)) {
return againRequest(error, service);
}
if (configData.isLoop && configData.isLoop.count === 3) {
requestTimeout();
}
}

50X

else if (error.toString().indexOf("50") !== -1) {
if (!axios.isCancel(error) && !isLoop(configData)) {
return againRequest(error, service, 1, 2);
}
if (configData.isLoop && configData.isLoop.count === 1) {
requestTimeout();
}
}

未知错误

else {
// 未知错误,等待以后解析
}

总结

结果将状态码梳理后,客户基本看不到API错误了,对服务的稳定性和可靠性非常满意,给我们提出了表扬和感谢。我们也期待老板升职加薪!

参考资料


作者:高志小鹏鹏
来源:juejin.cn/post/7413187186131533861

收起阅读 »

Video.js:视频播放的全能解决方案

web
大家好,我是徐徐。今天跟大家分享一款多媒体类前端工具库:Video.js。前言在现代网页开发中,视频播放功能已经成为用户体验的一个重要组成部分。无论你是开发一个视频分享平台还是一个简单的博客,选择合适的视频播放器都至关重要。今天,我们要介绍的 Vid...
继续阅读 »

大家好,我是徐徐。今天跟大家分享一款多媒体类前端工具库:Video.js。

前言

在现代网页开发中,视频播放功能已经成为用户体验的一个重要组成部分。无论你是开发一个视频分享平台还是一个简单的博客,选择合适的视频播放器都至关重要。今天,我们要介绍的 Video.js 是一个强大且灵活的 HTML5 视频播放器,它能够满足你对视频播放的所有需求。

基本信息

什么是 Video.js?

Video.js 是一个从零开始为 HTML5 世界打造的网页视频播放器。它不仅支持 HTML5 视频和现代流媒体格式,还支持 YouTube 和 Vimeo。自2010年中期项目启动以来,Video.js 已经发展成为一个拥有数百名贡献者并广泛应用于超过** 80 **万个网站的播放器。

主要特点

  • 全能播放Video.js 支持传统的视频格式,如 MP4 和 WebM,同时也支持自适应流媒体格式,如 HLS 和 DASH。对于直播流,Video.js 还提供了专门的用户界面,使直播体验更加流畅。
  • 易于定制:虽然 Video.js 自带的播放器界面已经非常美观,但它的设计也考虑到了可定制性。通过简单的 CSS 你可以轻松地为播放器增添个人风格,使其更符合你的网页设计需求。
  • 丰富的插件生态:当你需要额外功能时,Video.js 的插件架构能够满足你的需求。社区已经开发了大量的插件和皮肤,包括 Chromecast、IMA 以及 VR 插件,帮助你快速扩展播放器的功能。

使用场景

Video.js 适用于各种视频播放场景:

  • 视频分享平台:无论是播放本地视频还是流媒体内容,Video.js 都能提供稳定的播放体验。
  • 直播应用:通过专用的直播流 UI,Video.js 能够实现高质量的实时视频播放。
  • 教育和培训平台:支持多种格式和流媒体,确保你的教学视频能够在不同设备上顺畅播放。

快速上手

要在你的网页中使用 Video.js,只需以下简单步骤:

  1. 引入 Video.js 的库

<link href="https://unpkg.com/video.js/dist/video-js.min.css" rel="stylesheet">
<script src="https://unpkg.com/video.js/dist/video.min.js">script>


<link href="https://unpkg.com/video.js@8.17.3/dist/video-js.min.css" rel="stylesheet">
<script src="https://unpkg.com/video.js@8.17.3/dist/video.min.js">script>


<link href="https://cdnjs.cloudflare.com/ajax/libs/video.js/8.17.3/video-js.min.css" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/video.js/8.17.3/video.min.js">script>

  1. 添加视频播放器元素
<video
id="my-player"
class="video-js"
controls
preload="auto"
poster="//vjs.zencdn.net/v/oceans.png"
data-setup='{}'>

<source src="//vjs.zencdn.net/v/oceans.mp4" type="video/mp4">source>
<source src="//vjs.zencdn.net/v/oceans.webm" type="video/webm">source>
<source src="//vjs.zencdn.net/v/oceans.ogv" type="video/ogg">source>
<p class="vjs-no-js">
To view this video please enable JavaScript, and consider upgrading to a
web browser that
<a href="https://videojs.com/html5-video-support/" target="_blank">
supports HTML5 video
a>
p>
video>
  1. 初始化播放器
var player = videojs('my-video');

就这样,你就可以在网页上嵌入一个功能丰富的视频播放器了。

videojs函数还接受一个options对象和一个回调:

var options = {};

var player = videojs('my-player', options, function onPlayerReady() {
videojs.log('Your player is ready!');

// In this context, `this` is the player that was created by Video.js.
this.play();

// How about an event listener?
this.on('ended', function() {
videojs.log('Awww...over so soon?!');
});
});

结语

Video.js 是一个功能强大且灵活的视频播放器,它支持多种视频格式和流媒体协议,并且具有丰富的插件生态和良好的定制性。无论你是构建一个视频分享平台还是需要实现高质量的直播播放,Video.js 都能为你提供稳定且可扩展的解决方案。

希望这篇文章能帮助你了解 Video.js 的强大功能,并激发你在项目中使用它的灵感,这么好的东西,赶快分享给你的朋友们吧!


作者:前端徐徐
来源:juejin.cn/post/7411046020840964131
收起阅读 »

文档协同软件是如何解决编辑冲突的?

web
前言 本文将介绍在线协同文档编辑器是如何解决冲突的,大部分公司在解决冲突上目前用的都是 OT 算法,与之对应,也有一个 CRDT 算法实现。接下来,我们将深入这两种算法的实现及原理。 解决冲突的方案 在线协同文档编辑器通常使用不同的算法来解决冲突,具体算法取决...
继续阅读 »

前言


本文将介绍在线协同文档编辑器是如何解决冲突的,大部分公司在解决冲突上目前用的都是 OT 算法,与之对应,也有一个 CRDT 算法实现。接下来,我们将深入这两种算法的实现及原理。


解决冲突的方案


在线协同文档编辑器通常使用不同的算法来解决冲突,具体算法取决于编辑器的实现和设计。以下是一些常见的解决冲突的算法:



  1. OT(Operational Transformation,操作转换):这是一种常见的解决冲突的算法,用于实现实时协同编辑。OT算法通过将用户的编辑操作转换为操作序列,并在多个用户同时编辑时进行操作转换,以确保最终的文档状态一致。

  2. CRDT(Conflict-free Replicated Data Type,无冲突复制数据类型):这是一种基于数据结构的解决冲突的算法,它允许多个用户在不同的副本上进行并发编辑,并最终将编辑结果合并为一致的文档状态。CRDT算法通过设计特定的数据结构和操作规则来实现冲突自由的复制。


这些算法都有各自的优缺点,并且在不同的场景和需求下可能更适合不同的编辑器实现。


接下来,我们先聊聊 OT 算法。


OT 算法


image.png


当多个用户同时编辑同一文档时,OT算法通过操作转换来保持文档状态的一致性。下面是一个简单的示例,帮助你理解OT算法的工作原理。


假设有两个用户 A 和 B 同时编辑一段文本,初始文本为 "Hello, world!"。


用户 A 在文本末尾添加了字符 " How are you?"。


用户 B 在文本末尾添加了字符 " I'm fine."。


在 OT 算法中,每个操作都由一个操作类型和操作内容组成。在这个例子中,添加字符操作的操作类型为 "insert",操作内容为被插入的字符。


用户 A 的操作序列为:[insert(" How are you?")]
用户 B 的操作序列为:[insert(" I'm fine.")]


首先,服务器收到用户 A 的操作序列,将其应用到初始文本上,得到 "Hello, world! How are you?"。然后,服务器收到用户 B 的操作序列,将其应用到初始文本上,得到 "Hello, world! I'm fine."。


接下来,服务器需要进行操作转换,将用户 B 的操作序列转换为适应用户 A 的文本状态。在这个例子中,用户 B 的操作 "insert(" I'm fine.")" 需要转换为适应 "Hello, world! How are you?" 的操作。


操作转换的过程如下:



  1. 用户 A 的操作 "insert(" How are you?")" 在用户 B 的操作 "insert(" I'm fine.")" 之前发生,因此用户 B 的操作不会受到影响。

  2. 用户 B 的操作 "insert(" I'm fine.")" 在用户 A 的操作 "insert(" How are you?")" 之后发生,因此用户 B 的操作需要向后移动。

  3. 用户 B 的操作 "insert(" I'm fine.")" 向后移动到 "Hello, world! How are you? I'm fine."。


最终,服务器将转换后的操作序列发送给用户 A 和用户 B,他们将其应用到本地文本上,最终得到相同的文本状态 "Hello, world! How are you? I'm fine."。


这个例子展示了 OT 算法如何通过操作转换来保持多个用户同时编辑时文档状态的一致性。在实际应用中,OT 算法需要处理更复杂的操作类型和情况,并进行更详细的操作转换。


接下来,我们聊聊 CRDT 算法:


CRDT 算法


image.png


当多个用户同时编辑同一文档时,CRDT(Conflict-free Replicated Data Type,无冲突复制数据类型)算法通过设计特定的数据结构和操作规则来实现冲突自由的复制。下面是一个简单的示例,帮助你理解CRDT算法的工作原理。


在CRDT算法中,文档被表示为一个数据结构,常见的数据结构之一是有序列表(Ordered List)。每个用户的编辑操作会被转换为操作序列,并应用到本地的有序列表上。


假设有两个用户 A 和 B 同时编辑一段文本,初始文本为空。用户 A 在文本末尾添加了字符 "Hello",用户 B 在文本末尾添加了字符 "World"。


在CRDT算法中,每个字符都被赋予一个唯一的标识符,称为标记(Marker)。在这个例子中,我们使用递增的整数作为标记。


用户 A 的操作序列为:[insert("H", 1), insert("e", 2), insert("l", 3), insert("l", 4), insert("o", 5)]
用户 B 的操作序列为:[insert("W", 6), insert("o", 7), insert("r", 8), insert("l", 9), insert("d", 10)]


每个操作都包含要插入的字符以及对应的标记。


当用户 A 应用自己的操作序列时,有序列表变为 "Hello"。同样地,当用户 B 应用自己的操作序列时,有序列表变为 "World"。


接下来,服务器需要将两个用户的操作序列合并为一致的文本状态。在CRDT算法中,合并的过程是通过比较标记的大小来确定字符的顺序,并确保最终的文本状态一致。


合并的过程如下:



  1. 用户 A 的操作序列中的标记小于用户 B 的操作序列中的标记,因此 "H" 在 "W" 之前。

  2. 用户 A 的操作序列中的标记小于用户 B 的操作序列中的标记,因此 "e" 在 "o" 之前。

  3. 用户 A 的操作序列中的标记小于用户 B 的操作序列中的标记,因此 "l" 在 "r" 之前。

  4. 用户 A 的操作序列中的标记小于用户 B 的操作序列中的标记,因此 "l" 在 "l" 之前。

  5. 用户 A 的操作序列中的标记小于用户 B 的操作序列中的标记,因此 "o" 在 "d" 之前。


最终,合并后的有序列表为 "HelloWorld"。


这个例子展示了CRDT算法如何通过设计特定的数据结构和操作规则,以及比较标记的大小来实现冲突自由的复制。在实际应用中,CRDT算法可以应用于各种数据结构和操作类型,并进行更复杂的合并过程。


CRDT 的标记实现方案



  1. 递增整数标记:最简单的标记实现方式是使用递增的整数作为标记。每个操作都会分配一个唯一的整数标记,标记的大小可以用来比较操作的发生顺序。

  2. 时间戳标记:另一种常见的标记实现方式是使用时间戳作为标记。每个操作都会被分配一个时间戳,可以使用系统时间或逻辑时钟来生成时间戳。时间戳可以用来比较操作的发生顺序。

  3. 向量时钟标记:向量时钟是一种用于标记并发事件的数据结构。每个操作都会被分配一个向量时钟标记,向量时钟由多个节点的时钟组成,每个节点维护自己的逻辑时钟。向量时钟可以用来比较操作的发生顺序,并检测并发操作之间的关系。

  4. 哈希值标记:有些情况下,可以使用操作内容的哈希值作为标记。每个操作的内容都会被哈希为一个唯一的标记,这样可以通过比较哈希值来比较操作的发生顺序。


方案选型


OT算法和CRDT算法都是用于实现实时协同编辑的算法,但它们有不同的优点、缺点和工作原理。


OT算法的优点:



  1. 简单性:OT算法相对较简单,易于理解和实现。

  2. 实时协同编辑:OT算法可以实现实时协同编辑,多个用户可以同时编辑文档,并通过操作转换保持文档状态的一致性。


OT算法的缺点:



  1. 操作转换复杂性:OT算法的操作转换过程相对复杂,需要考虑多种情况和操作类型,实现和测试上有一定的挑战。

  2. 需要服务器参与:OT算法需要中央服务器来处理操作转换,这可能会导致一定的延迟和依赖性。


CRDT算法的优点:



  1. 冲突自由:CRDT算法设计了特定的数据结构和操作规则,可以实现冲突自由的复制,多个用户同时编辑时不会发生冲突。

  2. 去中心化:CRDT算法可以在去中心化的环境中工作,不依赖于中央服务器进行操作转换,每个用户可以独立地进行编辑和合并。


CRDT算法的缺点:



  1. 数据结构复杂性:CRDT算法的数据结构和操作规则相对复杂,需要更多的设计和实现工作。

  2. 数据膨胀:CRDT算法可能会导致数据膨胀,特别是在复杂的编辑操作和大规模的协同编辑场景下。


OT算法和CRDT算法的区别:



  1. 算法原理:OT算法通过操作转换来保持文档状态的一致性,而CRDT算法通过设计特定的数据结构和操作规则来实现冲突自由的复制。

  2. 中心化 vs. 去中心化:OT算法需要中央服务器进行操作转换,而CRDT算法可以在去中心化的环境中工作,每个用户都有完整的本地数据,可以将操作发送给服务器,然后网络分发到其他客户端。

  3. 复杂性:OT算法相对较简单,但在处理复杂操作和转换时可能会变得复杂;CRDT算法相对复杂,需要更多的设计和实现工作。


选择使用哪种算法取决于具体的应用场景和需求。OT算法适用于需要实时协同编辑的场景,而CRDT算法适用于去中心化和冲突自由的场景。


总结


本文介绍了在线协同文档编辑器中解决冲突的算法,主要包括OT算法和CRDT算法。OT算法通过操作转换来保持文档状态的一致性,而CRDT算法通过设计特定的数据结构和操作规则实现冲突自由的复制。OT算法需要中央服务器进行操作转换,适用于需要实时协同编辑的场景。CRDT算法可以在去中心化环境中工作,每个用户都有完整的本地数据,适用于去中心化和冲突自由的场景。选择使用哪种算法取决于具体的应用场景和需求。


作者:谦宇
来源:juejin.cn/post/7283018190593785896
收起阅读 »

audio自动播放为什么会失败

web
背景 某天客户报了一个问题,说是大屏的声音不能自动播放了,我们的大屏应用是有个报警的,当有报警的时候,自动会播放警报的声音 复线步骤 测试后发现如下结论 当刷新页面后,audio不会自动播放 当从另外的一个页面进入到当前页面,可以直接播放声音 如果你想测试...
继续阅读 »

背景


某天客户报了一个问题,说是大屏的声音不能自动播放了,我们的大屏应用是有个报警的,当有报警的时候,自动会播放警报的声音


复线步骤


测试后发现如下结论



  1. 当刷新页面后,audio不会自动播放

  2. 当从另外的一个页面进入到当前页面,可以直接播放声音


如果你想测试,可以点我进行测试


你可以先点击上方链接的 尝试一下 ,下方为截图


image.png


这个时候你会听到一声马叫声


然后,你刷新下马叫声的页面,这个时候声音的自动播放将不会生效


image.png


报错问题排查


打开控制台,不出意外看到了一个报错信息。


image.png


翻译为中文的意思为
不允许的错误。播放失败,因为用户没有先与文档交互。goo.gl/xX8pDD


尝试解决


那我给通过给body添加点击事件,自动触发点击事件,在点击的事件后自动播放声音。
(当是我的想法是,这个大概率是不行的,chrome应该不会忽略这一点,不然这个功能就相当于不存在)


经过测试后,发现确实还不行,在意料中。


参考别人的网站,用抖音测试


点击我跳转抖音


想到了我们可以参考抖音,我用抖音的进行测试,当你不做任何的操作,页面应该如下
image.png


我们从这里得出结论,这个应该是浏览器的限制,需要查看官方文档,看看原因


查阅官方文档


点我查看chrome的官方文档


我截取了一些关键的信息


image.png


注意浏览器会有一个媒体互动指数,这是浏览器自动计算的,该分越高,才会触发自动播放


查看电脑的媒体互动指数


在url上输入 about://media-engagement,你会看到如下的截图,


image.png


经过测试后 当网站变成了is High,音频会自动播放,不会播放失败。


这就解释了为什么有的网站可以自动播放声音,有的网站不可以自动播放声音


ok,我们继续往下看,这个时候看到了一些关键的信息。


作为开发者,我们不应该相信音频/视频会播放成功,要始终在播放的回掉中来进行判断


image.png


看到这些,我们来模仿抖音的实现.在播放声音的catch的时候,显示一个错误的弹窗,提示用户,当用户点击的时候,自动播放声音


   this.alarmAudio = new Audio(require("@/assets/sound/alarm.mp3"));
this.alarmAudio
.play()
.then(() => {
this.notifyId && this.notifyId.close();
})
.catch((error) => {
if (error instanceof DOMException) {
// 这里可以根据异常类型进行相应的错误处理
if (error.name === "NotAllowedError") {
if (this.notifyId) return;
this.notifyId = Notification({
title: "",
duration: 0,
position: "bottom-left",
dangerouslyUseHTMLString: true,
onClick: this.onAudioNotifyConfirm,
showClose: false,
customClass: "audio-notify-confirm",
message:
"<div style='color:#fff;font-size:18px;cursor:pointer'>因浏览器限制,需<span style='color:#ff2c55'>点击打开声音</span></div>",
});
}
}
});

实现效果如下


image.png


总结



  1. 在用到video或者audio的时候,要始终不相信他会播放声音成功,并且添加catch处理异常场景,给用户友好的提示

  2. video或者audio的自动播放跟媒体互动指数有关(MEI),当媒体指数高,会自动播放,否则需要用户先交互后,audio才可以自动播放。

  3. 从一个页面window.open另外一个页面可以自动播放声音,当刷新页面后,需要有高的MEI,audio才会自动播放,如果你需要在后台打开一个大屏的页面,刚好可以这样设计,不要用页面跳转


作者:pauldu
来源:juejin.cn/post/7412505754383007744
收起阅读 »

Vue3真的不需要用pinia!!!

web
前言 之前使用vue3都是在公司的基建项目中,为了快速达到目的,把以前vue2的模板拿来简单改改就直接用了,所以项目中用法特别乱,比如:状态管理依旧用的vuex,各种类型定义全是any,有些代码是选项式API,有些代码是组合式API... 最近终于有时间推动一...
继续阅读 »

前言


之前使用vue3都是在公司的基建项目中,为了快速达到目的,把以前vue2的模板拿来简单改改就直接用了,所以项目中用法特别乱,比如:状态管理依旧用的vuex,各种类型定义全是any,有些代码是选项式API,有些代码是组合式API...


最近终于有时间推动一下业务项目使用vue3了。作为极简主义的我,始终奉行少即是多,既然是新场景,一切从新,从头开始写模版:



  • 使用最新的vue3版本v3.5.x

  • 所有使用的内部库全部生成ts类型并引入到环境中。

  • 将所有的mixins重写,包装成组合式函数。

  • 将以前的vue上的全局变量挂载到app.config.globalProperties

  • 全局变量申明类型到vue-runtime-core.d.ts中,方便使用。

  • 全部使用setup语法,使用标签<script setup lang="ts">

  • 使用pinia作为状态管理。


pinia使用


等等,pinia?好用吗?打开官方文档研究了下,官方优先推荐的是选项式API的写法。


调用defineStore方法,添加属性state, getters, actions等。


export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0, name: 'Eduardo' }),
getters: {
doubleCount: (state) => state.count * 2,
},
actions: {
increment () {
this.count++
},
},
})

使用的时候,调用useCounterStore即可。


import { useCounterStore } from '@/stores/counter'
import { computed } from 'vue'

const store = useCounterStore()
setTimeout(() => {
store.increment()
}, 1000)
const doubleValue = computed(() => store.doubleCount)


看上去还不错,但是我模版中全部用的是组合式写法,肯定要用组合式API,试着写了个demoref就是选项式写法中的statecomputed就是选项式中的gettersfunction就是actions


// useTime.ts
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
import vueConfig from '../../../common/config/vueConfig'
import * as dayjs from 'dayjs'

export default defineStore('time', () => {
const $this = vueConfig()
const time = ref<number>()
const timeFormat = computed(() => dayjs(time.value).format('YYYY-MM-DD HH:mm:ss'))
const getSystemTime = async () => {
const res = await $this?.$request.post('/system/time')
time.value = Number(res.timestamp)
}
return { timeFormat, getSystemTime }
})

调用时解构赋值,就可以直接用了。


// index.vue
<script setup lang="ts">
import { onMounted } from 'vue'
import useTime from './use/useTime'

const { timeFormat, getSystemTime } = useTime()
onMounted(async () => {
// 请求
await getSystemTime()
console.log('当前时间:', timeFormat)
})
</script>

优雅了很多,之前用vuex时还有个问题,storeA中的state、actions等,会在storeB中使用,这一点pinia文档也有说明,直接在storeB调用就好了,比如我想在另一个组件中调用上文中提到的timeFormat


defineStore('count', () => {
const count = ref<number>(0)
const { timeFormat } = useTime()
return {
count,
timeFormat,
}
})

怎么看着这么眼熟呢,这不就是组合式函数吗?为什么我要用defineStore再包一层呢?试一试不用pinia,看能不能完成状态管理。


组合式函数


直接添加一个useCount.ts文件,申明一个组合式函数。


// useCount.ts
import { computed, ref } from 'vue'

const useCount = () => {
const count = ref<number>(0)
const doubleCount = computed(() => {
return count.value * 2
})
const setCount = (v: number) => {
count.value = v
}
return {
count,
doubleCount,
setCount,
}
}
export default useCount

使用时直接解构申明,并使用。


import useCount from './use/useCount'

const { count, setCount } = useCount()
onMounted(async () => {
console.log('count', count.value) // 0
setCount(10)
console.log('count', count.value) // 10

})

最大的问题来了,如何在多个地方共用count的值呢,这也是store最大的好处,了解javascript函数机制的我们知道useCount本身是一个闭包,每次调用,里面的ref就会重新生成。count就会重置。


import useCount from './use/useCount'

const { count, setCount } = useCount()
const { doubleCount } = useCount()

onMounted(async () => {
console.log('count', count.value, doubleCount.value) // 0 0
setCount(10)
console.log('count', count.value, doubleCount.value) // 10 0

})

这个时候doubleCount用的并不是第一个useCount中的count,而是第二个重新生成的,所以setCount并不会引起doubleCount的变化。


怎么办呢?简单,我们只需要把count的声明暴露在全局环境中,这样在import时就会申明了,调用函数时不会被重置。


import { computed, ref } from 'vue'

const count = ref<number>(0)
const useCount = () => {
const doubleCount = computed(() => {
return count.value * 2
})
const setCount = (v: number) => {
count.value = v
}
return {
count,
doubleCount,
setCount,
}
}
export default useCount

当我们多次调用时,发现可以共享了。


import useCount from './use/useCount'

const { count, setCount } = useCount()
const { doubleCount } = useCount()

onMounted(async () => {
console.log('count', count.value, doubleCount.value) // 0 0
setCount(10)
console.log('count', count.value, doubleCount.value) // 10 20

})

但是这个时候count是比较危险的,store应该可以保护state不被外部所修改,很简单,我们只需要用readonly包裹一下返回的值即可。


import { computed, readonly, ref } from 'vue'

const count = ref<number>(0)
const useCount = () => {
const doubleCount = computed(() => {
return count.value * 2
})
const setCount = (v: number) => {
count.value = v
}
return {
// readonly可以确保引用对象不会被修改
count: readonly(count),
doubleCount,
setCount,
}
}
export default useCount


总结


经过我的努力,vue3又减少了一个库的使用,我就说不需要用pinia,不过放弃pinia也就意味着放弃了它自带的一些方法store.$statestore.$patch等等,这些方法实现很简单,很轻松就可以手写出来,如果你是这些方法的重度用户,保留pinia也没问题,如果你也想代码更加精简,赶紧尝试下组合式函数吧。


作者:自在的小李子
来源:juejin.cn/post/7411328136740847654
收起阅读 »

拖拽神器:Pragmatic-drag-and-drop!

web
哈喽,大家好 我是 xy👨🏻‍💻。今天给大家分享一个开源的前端最强拖拽组件 — pragmatic-drag-and-drop! 前言 在前端开发中,拖拽功能是一种常见的交互方式,它能够极大提升用户体验。 今天,我们要介绍的是一个开源的前端拖拽组件 — p...
继续阅读 »

哈喽,大家好 我是 xy👨🏻‍💻。今天给大家分享一个开源的前端最强拖拽组件 — pragmatic-drag-and-drop



前言


在前端开发中,拖拽功能是一种常见的交互方式,它能够极大提升用户体验。



今天,我们要介绍的是一个开源的前端拖拽组件 — pragmatic-drag-and-drop,它以其轻量级高性能强大的兼容性,成为了前端开发者的新宠。


什么是 pragmatic-drag-and-drop?


pragmatic-drag-and-drop 是由 Atlassian 开源的一款前端拖拽组件。



Atlassian,作为全球知名的软件开发公司,其核心产品 TrelloJiraConfluence 都采用了 pragmatic-drag-and-drop 组件。


这不仅证明了该组件的实用性和可靠性,也反映了 Atlassian 对前端交互体验的极致追求。


组件的作者:Alex Reardon,也是流行 React 开源拖拽组件 react-beautiful-dnd 的开发者。


pragmatic-drag-and-drop 继承了作者对拖拽交互的深刻理解,支持多种拖拽场景,包括列表面板表格网格绘图调整大小等。


为什么选择 pragmatic-drag-and-drop?



  • 轻量化:核心包大小仅为 4.7KB,轻量级的体积使得它在加载速度上具有优势。

  • 灵活性:提供无头(headless)解决方案,开发者可以完全自定义视觉效果和辅助技术控制。

  • 框架无关性:适用于所有主流前端框架,如 React、Svelte、Vue 和 Angular。

  • 高性能:支持虚拟化,适应各种复杂的用户体验,确保拖拽操作流畅。

  • 全平台覆盖:在所有主流浏览器移动设备上运行良好,包括 Firefox、Safari、Chrome 以及 iOS 和 Android 设备。

  • 无障碍支持:为非鼠标操作用户提供友好体验,确保所有用户都能享受拖拽体验。


应用场景


pragmatic-drag-and-drop 功能适用于多种场景,包括但不限于:



  • 任务管理应用:通过拖放操作,轻松实现卡片式任务列表的排序与整理。

  • 文档管理系统:简化文件夹和文件的移动与组织过程,提高工作效率。

  • 在线编辑器:提供直观的内容布局调整体验,增强用户自定义能力。

  • 数据可视化工具:允许用户动态调整图表元素位置,实现更丰富的信息展示。

  • 设计工具:在组件库中轻松排列组合元素,激发创意无限可能。


案例演示


列表拖拽排序:



面板拖拽:



表格拖拽排序:



树形节点拖拽:



绘图功能鼠标拖动:



可拖动棋子的棋盘:



在线演示地址:https://atlassian.design/components/pragmatic-drag-and-drop/examples


最后



如果觉得本文对你有帮助,希望能够给我点赞支持一下哦 💪 也可以关注wx公众号:前端开发爱好者 回复加群,一起学习前端技能 公众号内包含很多实战精选资源教程,欢迎关注



作者:前端开发爱好者
来源:juejin.cn/post/7406139000265752639
收起阅读 »

「滚动绽放」页面滚动时逐渐展示/隐藏元素

web
本文将介绍如何使用HTML、CSS和JavaScript代码实现页面在滚动时元素逐渐出现/隐藏。这个动画效果会在用户滚动/隐藏页面时从不同方向逐渐显示出一组彩色方块🏳️‍🌈 HTML结构 首先,HTML部分包含了一个<section>元素和一个名...
继续阅读 »

本文将介绍如何使用HTMLCSSJavaScript代码实现页面在滚动时元素逐渐出现/隐藏。这个动画效果会在用户滚动/隐藏页面时从不同方向逐渐显示出一组彩色方块🏳️‍🌈



HTML结构


首先,HTML部分包含了一个<section>元素和一个名为container的容器,其中包含了多个box元素。别忘了引入外部CSS和JS文件;


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="./style.css">

<title>Scroll To Reveal Animation</title>
</head>
<body>
<section>
<h2>Scroll To Reveal</h2>
</section>

<div class="container">
<!-- 调试CSS样式阶段 -->
<!-- <div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div> -->

</div>

<script src="./index.js"></script>
</body>
</html>

CSS样式


接着,设置一些基本的全局样式和居中布局、背景颜色和文字颜色;



  • 关于container容器,使用grid布局三列

  • 对于box容器,这部分CSS伪类代码定义了元素在动画中的位置和缩放变换。解释一下每个选择器的作用:

    • .box:nth-child(3n + 1):选择容器中每隔3个元素的第一个方块元素(这里表示第一列)。沿X轴向左平移400像素,缩放为0,即隐藏起来。

    • .box:nth-child(3n + 2):选择容器中每隔3个元素的第二个方块元素(这里表示第二列)。沿Y轴向下平移400像素,缩放为0。

    • .box:nth-child(3n + 3):选择容器中每隔3个元素第三个方块元素(这里表示第三列)。沿X轴向右平移400像素,缩放为0。




这些选择器定义了方块元素的初始状态,使它们在页面加载时处于隐藏状态。并且预设了.box.active激活状态的样式。



  • 将其平移到原始位置并恢复为原始尺寸,即显示出来。当滚动触发相应的事件时,方块元素将根据添加或移除active类来决定是逐渐显示或隐藏。


* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: Arial, Helvetica, sans-serif;
}

body {
display: flex;
flex-direction: column;
justify-content:center;
align-items: center;

background-color: #111;
color: #fff;
overflow-x: hidden;
}

section {
min-height: 100vh;
display: flex;
justify-content:center;
align-items: center;
}
section h2 {
font-size: 8vw;
font-weight: 500;
}

.container {
width: 700px;
position: relative;
top: -200px;

display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 30px;
}
.container .box {
width: 200px;
height: 200px;
background-color: #fff;
border-radius: 10px;

position: relative;
top: 50vh;
transition: .5s;
}

.container .box:nth-child(3n + 1) {
transform: translate(-400px, 0) scale(0);
}
.container .box:nth-child(3n + 2) {
transform: translate(0, 400px) scale(0);
}
.container .box:nth-child(3n + 3) {
transform: translate(400px, 0) scale(0);
}

.container .box.active {
transform: translate(0, 0) scale(1);
}

表现


scroll-reveal-rendering

JavaScript实现


最后,使用JavaScript生成每个方块并设置了随机的背景颜色,随后将它们添加到container容器中,通过监听滚动事件,使方块在用户滚动页面时根据位置添加类名否,应用CSS样式实现逐渐显示或隐藏;



  • 定义randomColor函数,用于生成随机的颜色值。这个函数会从一组字符中随机选择6个字符(每次循环随机取一个)作为颜色代码,并将其拼接成一个十六进制颜色值返回。

  • 获取container容器元素,并创建一个文档片段fragment用于存储循环创建出来带有背景色的.box方块元素,最后将文档片段附加到container中。

  • 定义scrollTrigger函数,绑定到窗口的滚动事件上。在这个函数中,遍历每个方块,检查相对于窗口顶部的偏移量,如果小于等于当前滚动的距离,则添加active类,显示方块。反之,则移除active类,隐藏方块。


/**创建随机色 */
const randomColor = () => {
const chars = "1234567890abcdef",
colorLegh = 6;

let color = '#';
for (let i = 0; i < colorLegh; i++) {
const p = Math.floor(Math.random() * chars.length);
color += chars.substring(p, p + 1);
};

return color;
};

/**创建DOM */
const container = document.querySelector('.container'),
fragment = document.createDocumentFragment();

for (let i = 0; i < 60; i++) {
const box = document.createElement('div');
box.style.backgroundColor = randomColor();
box.classList.add('box');

fragment.appendChild(box);
};
container.appendChild(fragment);


/**创建动画 */
const randomColorBlock = document.querySelectorAll('.box');

const scrollTrigger = () => {
randomColorBlock.forEach((box) => {
if (box.offsetTop <= window.scrollY) {
box.classList.add('active')
} else {
box.classList.remove('active')
}
});
};

window.addEventListener('scroll', scrollTrigger);

总结


通过本篇文章的详细介绍,相信能够帮助你更好地使用CSSJavaScript来创建一个滚动显示元素动画,从而理解掌握和应用这个效果。通过设置合适的样式和脚本来控制元素的显示和隐藏为网页提供了生动和吸引力。


希望这篇文章对你在开发类似交互动画效果时有所帮助!如果你对这个案列还有任何问题,欢迎在评论区留言或联系(私信)我。码字不易🥲,不要忘了三连鼓励🤟,谢谢阅读,Happy Coding🎉!


源码我放在了GitHub,里面还有一些酷炫的效果、动画案列,喜欢的话不要忘了 starred 不迷路!


作者:掘一
来源:juejin.cn/post/7280926568854781987
收起阅读 »

前端中的 File 和 Blob两个对象到底有什么不同❓❓❓

web
JavaScript 在处理文件、二进制数据和数据转换时,提供了一系列的 API 和对象,比如 File、Blob、FileReader、ArrayBuffer、Base64、Object URL 和 DataURL。每个概念在不同场景中都有重要作用。下面的内...
继续阅读 »

JavaScript 在处理文件、二进制数据和数据转换时,提供了一系列的 API 和对象,比如 File、Blob、FileReader、ArrayBuffer、Base64、Object URL 和 DataURL。每个概念在不同场景中都有重要作用。下面的内容我们将会详细学习每个概念及其在实际应用中的用法。


接下来的内容中我们将来了解 File和 Blob 这两个对象。


blob


在 JavaScript 中,Blob(Binary Large Object)对象用于表示不可变的、原始的二进制数据。它可以用来存储文件、图片、音频、视频、甚至是纯文本等各种类型的数据。Blob 提供了一种高效的方式来操作数据文件,而不需要将数据全部加载到内存中,这在处理大型文件或二进制数据时非常有用。


我们可以使用 new Blob() 构造函数来创建一个 Blob 对象,语法如下:


const blob = new Blob(blobParts, options);


  1. blobParts: 一个数组,包含将被放入 Blob 对象中的数据,可以是字符串、数组缓冲区(ArrayBuffer)、TypedArray、Blob 对象等。

  2. options: 一个可选的对象,可以设置 type(MIME 类型)和 endings(用于表示换行符)。


例如:


const blob = new Blob(["Hello, world!"], { type: "text/plain" });

20240913142627


Blob 对象主要有以下几个属性:



  1. size: 返回 Blob 对象的大小(以字节为单位)。


console.log(blob.size); // 输出 Blob 的大小


  1. type: 返回 Blob 对象的 MIME 类型。


console.log(blob.type); // 输出 Blob 的 MIME 类型

Blob 对象提供了一些常用的方法来操作二进制数据。



  1. slice([start], [end], [contentType])


该方法用于从 Blob 中提取一部分数据,并返回一个新的 Blob 对象。参数 start 和 end 表示提取的字节范围,contentType 设置提取部分的 MIME 类型。


const blob = new Blob(["Hello, world!"], { type: "text/plain" });

const partialBlob = blob.slice(0, 5);


  1. text()


该方法将 Blob 的内容读取为文本字符串。它返回一个 Promise,解析为文本数据。


blob.text().then((text) => {
console.log(text); // 输出 "Hello, world!"
});

20240913143250



  1. arrayBuffer()


该方法将 Blob 的内容读取为 ArrayBuffer 对象,适合处理二进制数据。它返回一个 Promise,解析为 ArrayBuffer 数据。


const blob = new Blob(["Hello, world!"], { type: "text/plain" });

blob.arrayBuffer().then((buffer) => {
console.log(buffer);
});

20240913143451



  1. stream()


该方法将 Blob 的数据作为一个 ReadableStream 返回,允许你以流的方式处理数据,适合处理大文件。


const stream = blob.stream();

Blob 的使用场景


Blob 对象在很多场景中非常有用,尤其是在 Web 应用中处理文件、图片或视频等二进制数据时。以下是一些常见的使用场景:



  1. 生成文件下载


你可以通过 Blob 创建文件并生成下载链接供用户下载文件。


const blob = new Blob(["This is a test file."], { type: "text/plain" });
const url = URL.createObjectURL(blob); // 创建一个 Blob URL
const a = document.createElement("a");
a.href = url;
a.download = "test.txt";
a.click();
URL.revokeObjectURL(url); // 释放 URL 对象

当我们刷新浏览器的时候发现是可以自动给我们下载图片了:


20240913144132



  1. 上传文件


你可以通过 FormData 对象将 Blob 作为文件上传到服务器:


const formData = new FormData();
formData.append("file", blob, "example.txt");

fetch("/upload", {
method: "POST",
body: formData,
}).then((response) => {
console.log("File uploaded successfully");
});


  1. 读取图片或其他文件


通过 FileReader API 可以将 Blob 对象读取为不同的数据格式。举例来说,你可以将 Blob 读取为图片并显示在页面上:


html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Documenttitle>
head>
<body>
<input type="file" id="fileInput" accept="image/*" />

<div id="imageContainer">div>
<script>
const fileInput = document.getElementById("fileInput");

const imageContainer = document.getElementById("imageContainer");

fileInput.
addEventListener("change", function (event) {
const file = event.target.files[0];

if (file && file.type.startsWith("image/")) {
const reader = new FileReader();

reader.
onload = function (e) {
const img = document.createElement("img");
img.
src = e.target.result;
img.
style.maxWidth = "500px";
img.
style.margin = "10px";
imageContainer.
innerHTML = "";
imageContainer.
appendChild(img);
};

reader.
readAsDataURL(file);
}
else {
alert("请选择一个有效的图片文件。");
}
});
script>
body>
html>

20240913145303



  1. Blob 和 Base64


有时你可能需要将 Blob 转换为 Base64 编码的数据(例如用于图像的内联显示或传输)。可以通过 FileReader 来实现:


const reader = new FileReader();
reader.onloadend = function () {
const base64data = reader.result;
console.log(base64data); // 输出 base64 编码的数据
};

reader.readAsDataURL(blob); // 将 Blob 读取为 base64

20240913145547


File


File 是 JavaScript 中代表文件的数据结构,它继承自 Blob 对象,包含文件的元数据(如文件名、文件大小、类型等)。File 对象通常由用户通过 选择文件时创建,也可以使用 JavaScript 构造函数手动创建。


<input type="file" id="fileInput" />
<script>
document.getElementById("fileInput").addEventListener("change", (event) => {
const file = event.target.files[0];
console.log("文件名:", file.name);
console.log("文件类型:", file.type);
console.log("文件大小:", file.size);
});
script>

最终输出结果如下图所示:


20240913141055


我们可以使用 File 的方式来访问用户上传的文件,我们也可以手动创建 File 对象:


const file = new File(["Hello, world!"], "hello-world.txt", {
type: "text/plain",
});

console.log(file);

20240913141356


File 对象继承了 Blob 对象的方法,因此可以使用一些 Blob 对象的方法来处理文件数据。



  1. slice(): 从文件中获取一个子部分数据,返回一个新的 Blob 对象。


const blob = file.slice(0, 1024); // 获取文件的前 1024 个字节


  1. text(): 读取文件内容,并将其作为文本返回(这是 Blob 的方法,但可以用于 File 对象)。


file.text().then((text) => {
console.log(text); // 输出文件的文本内容
});


  1. arrayBuffer(): 将文件内容读取为 ArrayBuffer(用于处理二进制数据)。


file.arrayBuffer().then((buffer) => {
console.log(buffer); // 输出文件的 ArrayBuffer
});


  1. stream(): 返回一个 ReadableStream 对象,可以通过流式读取文件内容。


const stream = file.stream();

20240913141746


总结


Blob 是纯粹的二进制数据,它可以存储任何类型的数据,但不具有文件的元数据(如文件名、最后修改时间等)。


File 是 Blob 的子类,File 对象除了具有 Blob 的所有属性和方法之外,还包含文件的元数据,如文件名和修改日期。


你可以将 File 对象看作是带有文件信息的 Blob。


const file = new File(["Hello, world!"], "hello.txt", { type: "text/plain" });

console.log(file instanceof Blob); // true

二者在文件上传和二进制数据处理的场景中被广泛使用。Blob 更加通用,而 File 更专注于与文件系统的交互。




作者:Moment
来源:juejin.cn/post/7413921824066551842
收起阅读 »

uni-app小程序超过2M怎么办?

web
一、开发版 开发版可以调整上限为4M 开发者工具 -> 详情 -> 本地设置 -> 预览及真机调试时主包、分包体积上限调整为4M -> 勾选 二、体验版、正式版 上传代码时,主包必须在2M以内。 小程序tabbar页面必须放在主包。 推...
继续阅读 »

一、开发版


开发版可以调整上限为4M


开发者工具 -> 详情 -> 本地设置 -> 预览及真机调试时主包、分包体积上限调整为4M -> 勾选


二、体验版、正式版


上传代码时,主包必须在2M以内。


小程序tabbar页面必须放在主包。


推荐除了tabbar页面以外,其余的都放在分包。其实只要这样做了,再复杂的小程序,主包代码都很难超过2M,但如果是uni-app开发的,那就不一定了。


uni-app优化


开发环境压缩代码


使用cli创建的项目


package.jsonscript中设置压缩:在命令中加入--minimize


"dev:mp-weixin": "cross-env NODE_ENV=development UNI_PLATFORM=mp-weixin vue-cli-service uni-build --watch --minimize",

使用hbuilderx创建的项目


顶部菜单栏点击运行 -> 运行到小程序模拟器 -> 运行时是否压缩代码 -> 勾选


开启压缩后,开发环境的小程序代码体积会大大降低


uni.scss优化


uni-app项目创建后会自带一个uni.scss文件,这个文件无需手动引入,会自动引入到每一个页面文件,所以尽量不要在这个文件内写公共css代码。


我接手的一个uni-app小程序项目,随着功能迭代,打包代码主包体积越来越接近2M,终于有一天写完一个功能,突然就达到了2.2M,无法上传了。参考小程序提供的代码依赖分析,发现wxss文件占用了大部分体积,于是我就去一个个搜,看某个class有没有被用到,没用到的就删掉,可是再怎么优化冗余代码,也无法降到2M以下。


直到我看到了uni.scss文件,除了里面自带的一些颜色变量代码,另外还加了700行的公共class,然后我在根目录新建一个assets/common.scss文件,把那700行代码移出去,在App.vue内引入


@import './assets/common.scss'

主包体积瞬间降到了1.41M


image.png


总结


重要的事情说三遍



  • 不要在uni.scss文件内写公共css代码

  • 不要在uni.scss文件内写公共css代码

  • 不要在uni.scss文件内写公共css代码


作者:xintianyou
来源:juejin.cn/post/7411334549739733018
收起阅读 »

2024 前端趋势:全栈也许已经是必选项

web
《2023 的 Javascript 星趋势》在 1 月就出来了,当时略略看了一下,并没有想太多。 过年期间,回想起来,似乎感觉到这次有一点不一样:也许,全栈的时代已经到来了。 React 与 Vue 生态对比 首先,我们来看看 React 与 Vue 生态的...
继续阅读 »

《2023 的 Javascript 星趋势》在 1 月就出来了,当时略略看了一下,并没有想太多。


过年期间,回想起来,似乎感觉到这次有一点不一样:也许,全栈的时代已经到来了。


React 与 Vue 生态对比


首先,我们来看看 React 与 Vue 生态的星趋势对比:


截屏2024-02-29 10.05.39转存失败,建议直接上传图片文件


上图中,React 整个生态的星星数远超于 Vue,第十名都要比 Vue 第一名的多。我们将其做一个分类:


排名ReactVue
1UI全栈
2白板演示文稿
3全栈后台管理系统
4状态管理hook
5后台管理系统UI
6文档文档
7全栈框架集成UI
8全栈框架UI框架
9后台管理系统UI
10无服务栈状态管理

可以看到 React 这边的生态链基本成熟,几乎每一个分类都有一个上榜的库,不再像 Vue 那样还在卷 UI 框架。


在全栈方面,Vue 的首位就是全栈 Nuxt。


React 的 Next.js 虽然不在首位,但是服务端/全栈相关的内容就占了 4 个,其中包含第 10 名的无服务栈。另外值得注意的是,React 这边还有服务端组件的概念。Shadcn/ui 能占到第一位,因为它基于无头 UI Radix 实现的,在服务端组件也能运用。所以,服务端/全栈在 React 中占的比重相当大的。


这样看来,前端往服务端进发已经成为一个必然趋势。


htmx 框架的倒退


再看看框架这边,htmx 在星趋势里,排行第二位,2023增长的星星数为 15.6K,与第一位的 React 颇为相近。


而 htmx 也是今年讨论度最高的。


在我经历过前后端不分离的阶段中,使用 jsp 生成前端页面,js 更多是页面炫技的工具。然后在 jQuery + Ajax 得到广泛应用之后,才真正有前后端分离的项目。


htmx 的出现,不了解的人,可能觉得是倒退到 Java + jQuery + Ajax 的前后端分离状态。但是,写过例子之后,我发现,它其实是倒退到了前后端不分离的阶段。


用 java 也好,世界上最好的 php 也好,或者用现在的 nodejs 服务,都能接入 htmx。你只要在服务端返回 html 即可。


/** nodejs fastity 写的一个例子 **/
import fastify from 'fastify'
import fastifyHtml from 'fastify-html'
import formbody from '@fastify/formbody';

const app = fastify()
await app.register(fastifyHtml)
await app.register(formbody);
// 省略首页引入 htmx

// 首页的模板,提供一个按钮,点击后请求 html,然后将请求返回的内容渲染到 parent-div 中
app.get('/', async (req, reply) => {
const name = req.query.name || 'World'
return reply.html`

Hello ${name}


`
, reply
})

// 请求返回 html
app.post('/clicked', (req, reply) => {
reply.html`

Clicked!

`
;
})

await app.listen({ port: 3000 })

也许大家会觉得离谱,但是很显然,事情已经开始发生了变化,后端也来抢前端饭碗了。


截屏2024-02-29 10.32.24.png


htmx 在 github 上已经有不少跟随者,能搜出前端代码已有不少,前三就有基于 Python 语言的 Django 服务端框架。


jQuery 见势头不错,今年也更新了 4.0 的 beta 版本,对现代浏览器提供了更好的支持。这一切似乎为旧架构重回大众视野做好了准备。


企业角度


站在企业角度来看,一个人把前后端都干了不是更好吗?


的确如此。前后端一把撸更符合企业的利益。国外的小公司更以全栈作为首选项。


也许有人觉得国情不同,但是在我接触的前端群里,这两年都有人在群里说他们公司前后端分离的情况。


还有的人还喜欢大厂那一套,注意分工合作,但是其实大厂里遗留项目也不少,有的甚至是 php;还有新的实验项目,如果能投入最少人力,快速试错,这种全栈的框架自然也是最优选择。


我并不是说,前后端分离不值得。但是目前已经进入 AI 赛道,企业对后台系统的开发,并不愿意投入更多了。能用就行已经成为当前企业的目标,自然我们也应该跟着变化。


全栈破局


再说说前端已死的论调。我恰恰觉得这是最好做改变的时机。


在浏览器对新技术支持稳定,UI 框架趋同,UI 组件库稳定之后,前端不再需要为浏览器不兼容素手无策了,不再需要苦哈哈地为1个像素争辩不停了,也不再需要为产品莫名其妙的交互焦头烂额了。


这并不意味着前端已死,反而可能我们某个阶段的任务完成了,后面有更重要的任务交给我们。也许,全栈就是一个破局。


在云服务/云原生如此普遍的情况下,语言不再是企业开发考虑的主要因素,这也为 nodejs 全栈铺平了道路。


前端一直拣最苦最脏的话来做,从 UI 中拿到了切图的工作,然后接手了浏览器兼容的活,后来又从后端拿到了渲染页面的工作。


那我们为何不再进一步,主动把 API 开发的工作也拿过来?


作者:陈佬昔没带相机
来源:juejin.cn/post/7340603873604599843
收起阅读 »

8个小而美的前端库

web
前端有很多小而美的库,接入成本很低又能满足日常开发需求,同时无论是 npm 方式引入还是直接复制到本地使用都可以。 2024 年推荐以下小而美的库。 radash 实用的工具库,相比与 lodash,更加面向现代,提供更多新功能(tryit,retry 等函数...
继续阅读 »

前端有很多小而美的库,接入成本很低又能满足日常开发需求,同时无论是 npm 方式引入还是直接复制到本地使用都可以。


2024 年推荐以下小而美的库。


radash


实用的工具库,相比与 lodash,更加面向现代,提供更多新功能(tryit,retry 等函数),源码可读性高,如果不想安装它,大部分函数可以直接复制到本地使用。



use-debounce


React Hook Debouce 库,让你不再为使用防抖烦恼。库的特点:体积小 < 1 Kb、与 underscore / lodash impl 兼容 - 一次学习,随处使用、服务器渲染友好。



timeago.js


格式化日期时间库,比如:“3 hours ago”,支持多语言,仅 2Kb 大小。同时提供了 React 版本 timeago-react。


timeage.format(1544666010224, 'zh_CN') // 输出 “5 年前”
timeage.format(Date.now() - 1000, 'zh_CN') // 输出 “刚刚”
timeage.format(Date.now() - 1000 * 60 * 5, 'zh_CN') // 输出 “5 分钟前”

react-use


实用 Hook 大合集 - 内容丰富,从跟踪电池状态和地理位置,到设置收藏夹、防抖和播放视频,无所不包。



dayjs


Day.js 是一个简约的 JavaScript 库,仅 2 Kb 大小。它可以使用基本兼容 Moment.js,为你提供日期的解析、处理和显示,支持多语言能力。



filesize


filesize.js 提供了一种简单方法,便于从数字(浮点数或整数)或字符串转换成可读性高的文件大小,filesize.min.js 大小为 2.94 kb。


import {filesize} from "filesize";
filesize(265318, {standard: "jedec"}); // "259.1 KB"

driver.js


driver.js 是一款用原生 js 实现的页面引导库,上手非常简单,体积在 gzip 压缩下仅仅 5kb。



@formkit/drag-and-drop


FormKit DnD 是一个小型拖拽库,它简单、灵活、与框架无关,压缩后只有 4Kb 左右,设计理念为数据优先。



小结


前端小而美的库使用起来一般都比较顺手,欢迎在评论区推荐你们开发中的使用小而美的库。


作者:晓得迷路了
来源:juejin.cn/post/7350140676615798824
收起阅读 »

登录页面一些有趣的css效果

web
前言 今天无意看到一个登录页,input框focus时placeholder上移变成label的效果,无聊没事干就想着自己来实现一下,登录页面能做文章的,普遍的就是按钮动画,title的动画,以及input的动画,这是最终的效果图(如下), 同时附上预览页以及...
继续阅读 »

前言


今天无意看到一个登录页,inputfocusplaceholder上移变成label的效果,无聊没事干就想着自己来实现一下,登录页面能做文章的,普遍的就是按钮动画,title的动画,以及input的动画,这是最终的效果图(如下), 同时附上预览页以及实现源码


919c40a2a264f683ab5e74e8a649ac5.png


title 的动画实现


首先描述一下大概的实现效果, 我们需要一个镂空的一段白底文字,在鼠标移入时给一个逐步点亮的效果。
文字镂空我们可以使用text-stroke, 逐步点亮只需要使用filter即可


text-stroke


text-stroke属性用于在文本的边缘周围添加描边效果,即文本字符的外部轮廓。这可以用于创建具有描边的文本效果。text-stroke属性通常与-webkit-text-stroke前缀一起使用,因为它目前主要在WebKit浏览器(如Chrome和Safari)中支持


text-stroke属性有两个主要值:



  1. 宽度(width) :指定描边的宽度,可以是像素值、百分比值或其他长度单位。

  2. 颜色(color) :指定描边的颜色,可以使用颜色名称、十六进制值、RGB值等。


filter


filter是CSS属性,用于将图像或元素的视觉效果进行处理,例如模糊、对比度调整、饱和度调整等。它可以应用于元素的背景图像、文本或任何具有视觉内容的元素。


filter属性的值是一个或多个滤镜函数,这些函数以空格分隔。以下是一些常见的滤镜函数和示例:



  1. 模糊(blur) : 通过blur函数可以实现模糊效果。模糊的值可以是像素值或其他长度单位。


    .blurred-image {
    filter: blur(5px);
    }


  2. 对比度(contrast) : 通过contrast函数可以调整对比度。值为百分比,1表示原始对比度。


    .high-contrast-text {
    filter: contrast(150%);
    }


  3. 饱和度(saturate) : 通过saturate函数可以调整饱和度。值为百分比,1表示原始饱和度。


    .desaturated-image {
    filter: saturate(50%);
    }


  4. 反色(invert) : 通过invert函数可以实现反色效果。值为百分比,1表示完全反色。


    .inverted-text {
    filter: invert(100%);
    }


  5. 灰度(grayscale) : 通过grayscale函数可以将图像或元素转换为灰度图像。值为百分比,1表示完全灰度。


    .gray-text {
    filter: grayscale(70%);
    }


  6. 透明度(opacity) : 通过opacity函数可以调整元素的透明度。值为0到1之间的数字,0表示完全透明,1表示完全不透明。


    .semi-transparent-box {
    filter: opacity(0.7);
    }


  7. 阴影(drop-shadow) :用于在图像、文本或其他元素周围添加阴影效果。这个属性在 CSS3 中引入,通常用于创建阴影效果,使元素看起来浮在页面上或增加深度感


    drop-shadow(<offset-x> <offset-y> <blur-radius>? <spread-radius>? <color>?)

    各个值的含义如下:



    • <offset-x>: 阴影在 X 轴上的偏移距离。

    • <offset-y>: 阴影在 Y 轴上的偏移距离。

    • <blur-radius> (可选): 阴影的模糊半径。默认值为 0。

    • <spread-radius> (可选): 阴影的扩散半径。默认值为 0。

    • <color> (可选): 阴影的颜色。默认值为当前文本颜色。




filter属性的支持程度因浏览器而异,因此在使用时应谨慎考虑浏览器兼容性。


实现移入标题点亮的效果


想实现移入标题点亮的效果我们首先需要两个通过定位重叠的span元素,一个做镂空用于展示,另一个作为
hover时覆盖掉镂空元素,并通过filter: drop-shadow实现光影效果,需要注意的是这里需要使用inline元素实现效果。


title-animation.gif


input 的动画实现


input的效果比较简单,只需要在focusspan(placeholder)上移变成span(label)同时给inputborder-bottom做一个底色的延伸,效果确定了接着就看看实现思路。


input placeholder 作为 label


使用div作为容器包裹inputspanspan首先绝对定位到框内,伪装为placeholder, 当input状态为focus提高spantop值,即可伪装成label, 这里有两个问题是:



  1. 当用户输入了值的时候,span并不需要恢复为之前的top, 这里我们使用css或者js 去判断都可以, js就是拿到输入框的值,这里不多做赘述,css 有个比较巧妙的做法, 给input required属性值设置为required, 这样可以使用css:valid伪类去判断input是否有值。

  2. 由于span层级高于input,当点击span时无法触发input的聚焦,这个问题我们可以使用pointer-events: none; 来解决。pointer-events 是一个CSS属性,用于控制元素是否响应用户的指针事件(例如鼠标点击、悬停、触摸等)。这个属性对于控制元素的可交互性和可点击性非常有用。


    pointer-events 具有以下几个可能的值:



    1. auto(默认值):元素会按照其正常行为响应用户指针事件。这是默认行为。

    2. none:元素不会响应用户的指针事件,就好像它不存在一样。用户无法与它交互。

    3. visiblePainted:元素在绘制区域上响应指针事件,但不在其透明区域上响应。这使得元素的透明部分不会响应事件,而其他部分会。

    4. visibleFill:元素在其填充区域上响应指针事件,但不在边框区域上响应。

    5. visibleStroke:元素在其边框区域上响应指针事件,但不在填充区域上响应。

    6. painted:元素会在其绘制区域上响应指针事件,包括填充、边框和透明区域。

    7. fill:元素在其填充区域上响应指针事件,但不在边框区域上响应。

    8. stroke:元素在其边框区域上响应指针事件,但不在填充区域上响应。




pointer-events 属性非常有用,特别是在创建交互性复杂的用户界面时,可以通过它来控制元素的响应区域。例如,你可以使用它来创建自定义的点击区域,而不仅仅是元素的边界。它还可以与其他CSS属性和JavaScript事件处理程序结合使用,以创建特定的交互效果。


input border bottom 延伸展开效果


效果比较简单,input被聚焦的时候,一个紫色的边从中间延伸覆盖白色的底边即可。 在使用一个span作为底部的边, 初始不可见, focus时从中间向两边延伸直至充满, 唯一头痛的就是怎么从中间向两边延伸,这里可以使用transform变形,首先使用transform: scaleX(0);达到不可见的效果, 然后设置变形原点为中间transform-origin: center;,这样效果就可以实现了


input 的动画实现效果


input-animation.gif


按钮的动画实现


关于按钮的动画很多,我们这里就实现一个移入的散花效果,移入时发散出一些星星,这里需要使用到动画去实现了,首先通过伪类创建一些周边元素,这里需要用到 background-image(radial-gradient)


background-image(radial-gradient)


background-image 属性用于设置元素的背景图像,而 radial-gradient 是一种 CSS 渐变类型,可用于创建径向渐变背景。这种径向渐变背景通常以一个中心点为基础,然后颜色渐变向外扩展,形成一种放射状的效果。


radial-gradient 的语法如下:


background-image: radial-gradient([shape] [size] at [position], color-stop1, color-stop2, ...);


  • [shape]: 可选,指定渐变的形状。常用的值包括 "ellipse"(椭圆)和 "circle"(圆形)。

  • [size]: 可选,指定渐变的大小。可以是长度值或百分比值。

  • at [position]: 可选,指定渐变的中心点位置。

  • color-stopX: 渐变的颜色停止点,可以是颜色值、百分比值或长度值。


按钮移入动画效果实现


btn-animation.gif


结尾


css 能实现的效果越来越多了,遇到有趣的效果,可以自己想想实现方式以及动手实现一下,思路毕竟是思路,具体实现起来说不定会遇到什么坑,逐步解决问题带来的成就感满足感还是很强的。


作者:刘圣凯
来源:juejin.cn/post/7294908459002331171
收起阅读 »

日历表格的制作,我竟然选择了这样子来实现...

web
前言 最近有个日历表格的需求,具体效果如下所示,鼠标经过时表格还有一个十字高亮的效果,在拿到这个设计图的时候,就在想应该用什么来实现,由于我所在的项目用的是vue3 + element,所以我第一时间想到的就是饿了么里面的表格组件,但是经过一番调式之后,发现在...
继续阅读 »

前言


最近有个日历表格的需求,具体效果如下所示,鼠标经过时表格还有一个十字高亮的效果,在拿到这个设计图的时候,就在想应该用什么来实现,由于我所在的项目用的是vue3 + element,所以我第一时间想到的就是饿了么里面的表格组件,但是经过一番调式之后,发现在饿了么表格的基础上想要调整我要的样式效果太复杂太麻烦了,所以我决定用原生的div循环来实现!


soogif.gif


第一步 初步渲染表格


由于表格的表头是固定的,我们可以先渲染出来


<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;
}
}
}
}
}

看一下页面效果:


image.png
表格的表头初步完成!


第二步 确认接口返回的数据格式


这是接口返回的格式数据 就例如第一个对象代表着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的数据格式是怎么样的


image.png
接下来就可以开始渲染表格的内容了,给有数据的单元格做个高亮,同时固定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>

image.png


到这里基本就完成了,还差一个鼠标经过表格十字高亮的需求


我们可以给每个单元格加上鼠标的移入移出事件,移入事件函数传两个参数,一个就是行一个就是列,行可以从一开始的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>

最终的效果就是:


soogif.gif


以下就是完整的代码:


<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>



如果对你有帮助的话,欢迎点赞留言收藏🌹


作者:coder_zsz
来源:juejin.cn/post/7413311432971141160
收起阅读 »

贼好用!五分钟搭建一个美观且易用的导航页面!

web
大家好,我是 Java陈序员。 今天,给大家介绍一个贼好用的导航网站搭建工具,只需通过几步操作,就能搭建出个性化导航网站! 关注微信公众号:【Java陈序员】,获取开源项目分享、AI副业分享、超200本经典计算机电子书籍等。 项目简介 Pintree 是一...
继续阅读 »

大家好,我是 Java陈序员


今天,给大家介绍一个贼好用的导航网站搭建工具,只需通过几步操作,就能搭建出个性化导航网站!



关注微信公众号:【Java陈序员】,获取开源项目分享、AI副业分享、超200本经典计算机电子书籍等。



项目简介


Pintree 是一个开源项目,旨在将浏览器书签导出成导航网站。通过简单的几步操作,就可以将书签转换成一个美观且易用的导航页面。



Pintree 支持使用 GitHub Pages 进行部署,无需购买服务器、域名等资源!


因此,只要有一个 Github 账号,就能快速搭建一个导航网站。接下来我们就来部署实现下!


项目部署


步骤一:Fork 项目


1、访问 pintree 项目地址


https://github.com/Pintree-io/pintree

2、Fork 项目到自己的仓库中


步骤二:启用 Github Pages


1、打开 GitHub 账号中 Forkpintree 项目


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/#/



大家的点赞、收藏和评论都是对作者的支持,如文章对你有帮助还请点赞转发支持下,谢谢!





作者:Java陈序员
来源:juejin.cn/post/7413187186132631589
收起阅读 »

flex 布局中更巧妙的布局方案!比 justify-content 和 align-items 好用多了!

web
在前端开发中,实现水平垂直居中一直是个热门话题。随着 CSS Flexbox 布局的普及,开发者们开始更多地使用 justify-content 和 align-items 这两个属性来解决这个问题。 然而,还有一种更加简洁、灵活的方式——使用 margi...
继续阅读 »

在前端开发中,实现水平垂直居中一直是个热门话题。随着 CSS Flexbox 布局的普及,开发者们开始更多地使用 justify-contentalign-items 这两个属性来解决这个问题。




然而,还有一种更加简洁、灵活的方式——使用 margin: auto; 来实现居中以及更多实际场景下的特定效果。让我们一起回顾一下常见方式:justify-contentalign-items,然后再来探讨一下使用:margin 的优势,以及如何在实际项目中使用它。





一、常见方式:justify-contentalign-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;
}

image.png



如上图所示,元素在水平和垂直方向都居中了。



示例 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;
}

image.png



如上图所示,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;
}

image.png



如上图所示,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;
}

image.png



如上图所示,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;
}

image.png



如上图所示,justify-content: space-evenly; 会使元素会在水平方向等间距;如果不设置元素的高度,使其自适应,align-items: stretch; 会使其垂直方向拉伸铺满。



1.4 思考与延伸


但你有没有想过,这些写法是否是最简洁的?能否实现我们日常开发的需求呢?有没有更优雅、更轻量的方案呢?


实际上在很多情况下这两个属性并不能够满足我们的开发需求。


比如我需要实现子元素部分集中的布局:



单纯依靠 justify-contentalign-items,很难让几个子元素集中在一起。比如我们希望某些元素靠近并且与其他元素保持一定的间距就会比较麻烦了。


此时为了实现这种布局,通常需要结合 flex-growmargin 或者 space-between,甚至需要使用嵌套的 flex 布局,增加了复杂性。



image.png


又或者是等宽子项的平均分布问题:



比如在导航菜单或展示商品卡片时,可能要求子项无论数量多少,都要从左向右均匀分布,并且保持等宽。


通过 justify-content: space-betweenspace-around 可以部分解决这个问题,但是往往会出现无法保证元素从左向右,或者是无法等分的问题。



image.png


以及一些其他的情况,如垂直排列的固定间距复杂的网格布局混合布局等,justify-contentalign-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; /* 自动分配外边距 */
}

image.png


在这个例子中,我们没有使用 justify-contentalign-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 是一种常见的办法,但这种方法也有一定的局限性:每个元素之间平等分配剩余空间,无法实现特定元素之间紧密靠拢。



image.png


代码实现:


<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-contentalign-items 可能会出现以下问题:



  1. 使用 space-between 时如果最后一行的元素数量不足以填满整行,剩余的元素会分散到两侧,留出较大的空白区域,导致布局不整齐。
    image.png

  2. 使用 space-around 时如果最后一行的元素数量不满,元素会在行中均匀分布,导致它们集中在中间,而不是靠左或对齐其他行。
    image.png

    大家在遇到这些情况时是不是就在考虑换用 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,我们能够简单而有效地实现等宽子项的平均分布,使布局更加简洁明了。



image.png




三、总结


在前端开发中,实现各种页面布局一直是一个常见的需求。


传统的做法如使用 justify-contentalign-items 属性已经被广泛采用,但这种方法有时可能显得不够简洁或灵活。


在适当的情况下直接使用 margin 进行布局是一种更优雅、简洁的替代方案,可以在 Flexbox 布局中有效地实现居中对齐和一些复杂的布局需求。掌握并运用这种方法,可以提高开发效率,并使布局更加优雅。快来玩起来吧!




作者:空白诗
来源:juejin.cn/post/7413222778855964706
收起阅读 »

告别繁琐的 try-catch:JavaScript 安全赋值运算符 (?= ) 来了!

web
你是否厌倦了代码中难以阅读和维护的冗长 try-catch 代码块?全新的 ECMAScript 安全赋值运算符 (?= ) 将彻底改变游戏规则!这一突破性的特性简化了错误处理,让你的代码更简洁、更高效。让我们深入了解 ?= 运算符如何彻底改变你的编码体验! ...
继续阅读 »

你是否厌倦了代码中难以阅读和维护的冗长 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踩坑记录

web
人员搭配和两个舍友,一前两后,从开发到最终上线,耗时两个月,此篇文章记录一下整个开发流程中踩的坑......前置准备:资质核验。需要企业认证才能发布社交类的小程序。需要上传营业执照、法人信息等类目选择。小程序中有个类目选择,选择自己小程序涉及的类目,但这里就比...
继续阅读 »

人员搭配和两个舍友,一前两后,从开发到最终上线,耗时两个月,此篇文章记录一下整个开发流程中踩的坑......

前置准备:

  1. 资质核验。需要企业认证才能发布社交类的小程序。需要上传营业执照、法人信息等
  2. 类目选择。小程序中有个类目选择,选择自己小程序涉及的类目,但这里就比较抽象了(下文讲)
  3. 微信认证。300元,腾子的吃相很难看,但奈何寄人篱下
  4. 小程序备案。在前面流程完成之后才能进行小程序的备案

image.png

审核流程

整个审核流程给我的感觉就是跟便秘一样,交一次吐一句,交一次吐一句,然后打回来重改 这里记录一下几个需要注意的点,如果你和我一样也是做UGC类的小程序的话

  1. 微信审核。所有但凡涉及言论的,能展示出你的思想的、个性的,对不起,请通通接入微信审核。包括但不限于用户昵称、头像;发布的帖子内容、图片

文字审核算比较快,图片审核就有点慢,且图片审核不是很精准。为了避免等待检测而让用户在界面等待过久的问题,后面无奈自己搭了个后台管理。以致于流程就变成了:用户发点什么东西 -> 后端调用微信检测接口 -> 检测完甩到后台 -> 后台管理员做个二次审核 -> 通过后在小程序获取

  1. 不能使用微信icon图标。之前做微信快捷登录,想着搞个微信的icon图标好看点,结果给我审核失败的原因就是不能使用微信图标 image.png
  2. 请预留足够的时间和审核慢慢耗。以上讲到的还只是真正发布之前踩的坑,等所有条件都符合后,算可以真正的发布,然后提示你首次提交一般需要7个自然日,然后就真的是7*24小时

image.png

image.png 5. 第一次代码真正预上线之后,此后更新代码再发布新的包,一半就只需要1~2天

开发过程

  1. 文件上传。以往网页开发中涉及文件上传的业务都是new FormData,然后再append必要的字段。但是,小程序中使用FormData会报错,所以,得使用uniapp自带的uni.uoloadFile
  2. 消息提示。在帖子发布成功之后等场景都会涉及到消息提示,一般涉及消息提示、页面回退,执行顺序请按navigateBackuni.showToast,如果二者调用顺序反了的话,只会页面回退,不会显示提示内容
  3. 分享功能。小程序的分享功能需要在onShareAppMessage(分享至好友)或者onShareTimeline(分享至朋友圈)调用。这两个是和onLoad同级的,如果你的技术选型也是Vue3的话,可以从@dcloudio/uni-app中导入
  4. 消息订阅。小程序涉及到一个需求:当用户发布帖子,微信审核+后台审核都通过之后会通知用户,此时就需要进行消息订阅

先在微信公众号平台开通消息订阅模板,拿到模板ID,前端中再调用 uni.requestSubscribeMessage 传入拿到的模板ID就可以实现消息订阅

  1. webSocket。小程序中的树洞评论功能我们选用的是webSocket,小程序中我们没有使用三方库,调用的是uniapp的uni.connectSocket,创建一个webSocket实例,然后处理对应的回调。由于用户一段时间内如果不发送消息,服务端也就没东西推送过来,webSocket自己会断掉。所以我们引入了心跳机制和断线重连机制。但是在做短信重连的时候就发现一个问题:断线重连之后确实是会创建新的实例,心跳包也正常推送给服务端,但是就是接收不到服务端反推回来的东西,后面经过排查,是webSocket实例的onMessage事件应当写在onOpen中,而不是独立写到外面

独立写到外面进行处理就会出现:断线重连之后死活接不到最新的实例返回的消息

这里再次吐槽微信的内容审核机制。原先选用webSocket的原因就是看中了它的实时推送,但是接入了内容审核就变得很抽象,时而秒通过,时而得等一下,这也搞得失去了选用webSocket的意义

  1. 请求池。小程序中使用到了tabs组件,当tab在切换过程中,比如tabA切换至tabB,由于需要请求数据,所以页面会有短暂的白屏时间,这里采用的是请求池,在获取第一个tab的列表数据的时候,由请求池顺便把之后的tab的内容也请求回来,此后在进行tab切换时就可以避免白屏,优化用户体验

image.png

image.png

  1. 响应式布局。小程序在个人页模仿了小红书个人页的实现形式,也就是随着页面的滚动,页面的布局(主要是用户头像缩小并由透明度为0逐渐变为1)发生变化。一开始采用的是监听页面scroll事件。但是,scroll涉及大量的计算;后面采用Intersection Observer。但是注意,uniapp不能直接使用这个API,得调用uni.createIntersectionObserver,二者语法差不多
  2. 防抖节流的使用。页面滚动加载下一页使用防抖,按钮点击进行节流,常规操作。

大概暂时先能想到这么多,后面有想到再接着补充......

后记

其实校园小程序这个题材市面上早已烂大街,说不上有任何的创新。此前决定搞这个的原因有以下几点:

  1. 很多东西只停留于理论,没有实操。就像webSocket的心跳和断线重连,博客教你怎样怎样,始终不如你自己去亲手实现一下,这个算主要驱动原因
  2. 这个程序刚好拿去参加学校的比赛,拿奖有钱doge
  3. ......

然后整一个项目跟下来吧,给我的感觉就是:技术重要吗?重要;但也不太重要,技术的重要性就跟我这句废话一样。因为一个东西,推广不起来,没人用,你就没动力去更新、去维护。当然,有没有人用对我们此次的开发来说还不算是必选项。

大家觉得校园社交类的程序还能集成什么功能,也欢迎提出您的宝贵意见


作者:吃肉不吃皮
来源:juejin.cn/post/7412665439501844490
收起阅读 »

利用CSS延迟动画,打造令人惊艳的复杂动画效果!

web
动画在前端开发中是经常遇到的场景之一,加入动画后页面可以极大的提升用户体验。 绝大多数简单的动画场景可以直接通过CSS实现,对于一些特殊场景的动画可能会使用到JS计算实现,通过本文的学习,可以让你在一些看似需要使用JS实现的动画场景,使用纯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
收起阅读 »