注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

前端:“这需求是认真的吗?” —— el-select 的动态宽度解决方案

web
Hello~大家好。我是秋天的一阵风 ~ 前言 最近我遇到了一个神奇的需求,客户要求对 el-select 的 宽度 进行动态设置。 简单来说,就是我们公司有一些选择框,展示的内容像“中华人民共和国/广西壮族自治区/南宁市/西乡塘区”这么长,一不小心就会内容超...
继续阅读 »

Hello~大家好。我是秋天的一阵风 ~


前言


最近我遇到了一个神奇的需求,客户要求对 el-select宽度 进行动态设置。


简单来说,就是我们公司有一些选择框,展示的内容像“中华人民共和国/广西壮族自治区/南宁市/西乡塘区”这么长,一不小心就会内容超长,显示不全。详情请看下面动图:


1.gif


一般来说,想解决内容展示不全的问题,有几种方法。


第一种:给选择框加个tooltip效果,在鼠标悬浮时展示完整内容。


第二种:对用户选择label值进行切割,只展示最后一层内容。


但是我们的客户对这两种方案都不接受,要求选择的时候让select选择框的宽度动态增加。


有什么办法呢?客户就是上帝,必须满足,他们说什么就是什么,所以我们只能开动脑筋,动手解决。


思路


我们打开控制台,来侦察一下el-select的结构,发现它是一个el-input--suffixdiv包裹着一个input,如下图所示。


image.png

内层input的宽度是100%,外层div的宽度是由这个内层input决定的。也就是说,内层input的宽度如果动态增加,外层div的宽度也会随之增加。那么问题来了,如何将内层input的宽度动态增加呢?



tips:


如果你对width的100%和auto有什么区别感兴趣,可以点击查看我之前的文章


探究 width:100%与width:auto区别



解决方案


为了让我们的el-select宽度能够跟着内容走,我们可以在内层input同级别增加一个元素,内容就是用户选中的内容。内容越多,它就像一个胃口很大的小朋友,把外层div的宽度撑开。下面来看图示例 :


image.png

借助prefix


幸运的是,el-select本身有一个prefix的插槽选项,我们可以借助这个选项实现:


image.png

我们添加一个prefix的插槽,再把prefix的定位改成relative,并且把input的定位改成绝对定位absolute。最后将prefix的内容改成我们的选项内容。看看现在的效果:



<template>
<div>
<el-select class="autoWidth" v-model="value" placeholder="请选择">
<template slot="prefix">
{{optionLabel}}
</template>
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value"
>

</el-option>
</el-select>
</div>

</template>

<script>
export default {
data() {
return {
options: [
{
value: "选项1",
label: "中华人民共和国/广东省/深圳市/福田区",
},
{
value: "选项2",
label: "中华人民共和国/广西壮族自治区/南宁市/西乡塘区",
},
{
value: "选项3",
label: "中华人民共和国/北京市",
},
{
value: "选项4",
label: "中华人民共和国/台湾省",
},
{
value: "选项5",
label: "中华人民共和国/香港特别行政区",
},
],
value: "",
};
},
computed: {
optionLabel() {
return (this.options.find((item) => item.value === this.value) || {})
.label;
},
},
};
</script>

<style lang="scss" scoped>

::v-deep .autoWidth .el-input__prefix {
position: relative;
}

::v-deep .autoWidth input {
position: absolute;
}
</style>



2.gif

细节调整


现在el-select已经可以根据选项label的内容长短动态增加宽度了,但是我们还需要继续处理一下细节部分,将prefix的内容调整到和select框中的内容位置重叠,并且将它隐藏。看看现在的效果


::v-deep .autoWidth .el-input__prefix {
position: relative;
box-sizing: border-box;
border: 1px solid #fff;
padding: 0 30px;
height: 40px;
line-height: 40px;
left: 0px;
visibility: hidden;
}

3.gif

调整初始化效果(用户未选择内容)


目前已经基本实现了效果了,还有最后一个问题,当用户没有选择内容的时候,select的宽度是“没有”的,如下图所示。


image.png

所以我们还得给他加上一个最小宽度


image.png

我们加上最小宽度以后,发现这个select的图标又没对齐,这是因为我们在重写.el-input__prefix样式的时候设置了padding: 0 30px,当用户没有选择内容的时候,select的图标应该是默认位置,我们需要继续调整代码,最后效果如下图所示:


4.gif

完整代码


最后附上完整代码:



<template>
<div>
<el-select
class="autoWidth"
:class="{ 'has-content': optionLabel }"
v-model="value"
placeholder="请选择"
clearable
>

<template slot="prefix">
{{ optionLabel }}
</template>
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value"
>

</el-option>
</el-select>
</div>

</template>

<script>
export default {
data() {
return {
options: [
{
value: "选项1",
label: "中华人民共和国/广东省/深圳市/福田区",
},
{
value: "选项2",
label: "中华人民共和国/广西壮族自治区/南宁市/西乡塘区",
},
{
value: "选项3",
label: "中华人民共和国/北京市",
},
{
value: "选项4",
label: "中华人民共和国/台湾省",
},
{
value: "选项5",
label: "中华人民共和国/香港特别行政区",
},
],
value: "",
};
},
computed: {
optionLabel() {
return (this.options.find((item) => item.value === this.value) || {})
.label;
},
},
};
</script>

<style lang="scss" scoped>
.autoWidth {
min-width: 180px;
}
::v-deep .autoWidth .el-input__prefix {
position: relative;
box-sizing: border-box;
border: 1px solid #fff;
padding: 0 30px;
height: 40px;
line-height: 40px;
left: 0px;
visibility: hidden;
}

::v-deep .autoWidth input {
position: absolute;
}
.autoWidth {
// 当.has-content存在时设置样式
&.has-content {
::v-deep .el-input__suffix {
right: 5px;
}
}
// 当.has-content不存在时的默认或备选样式
&:not(.has-content) {
::v-deep .el-input__suffix {
right: -55px;
}
}
}
</style>



作者:秋天的一阵风
来源:juejin.cn/post/7385825759118196771
收起阅读 »

告别频繁登录:教你用Axios实现无感知双Token刷新

web
一、引言在现代系统中,Token认证已成为保障用户安全的标准做法。然而,尽管许多系统采用了这种认证方式,却在处理Token刷新方面存在不足,导致用户体验不佳。随着Token有效期的缩短,频繁的重新登录成为常见现象,许多系统未能提供一种无缝的、用户无感知的Tok...
继续阅读 »

一、引言

在现代系统中,Token认证已成为保障用户安全的标准做法。然而,尽管许多系统采用了这种认证方式,却在处理Token刷新方面存在不足,导致用户体验不佳。随着Token有效期的缩短,频繁的重新登录成为常见现象,许多系统未能提供一种无缝的、用户无感知的Token刷新机制。通过结合Vue3和Axios这两大前端技术栈,我们可以借助Promise机制,开发出一种更加完善的自动化Token刷新方案,显著提升系统的稳定性和用户体验。本文将深入探讨这一实现过程,帮助你解决Token刷新难题。

二、示意图

image.png

三、具体实现

了解了基本步骤后,实际的实现过程其实相当简洁。然而,在具体操作中,仍有许多关键细节需要我们仔细考量,以确保Token刷新机制的稳定性和可靠性。

  1. Token 存储与管理:首先,明确如何安全地存储和管理Access Token与Refresh Token。这涉及到浏览器的存储策略,比如使用localStoragesessionStorage,存储策略不在本文中提及,本文采用localStorage 进行存储。
  2. 请求拦截器的设置:在Axios中设置请求拦截器,用于在每次发送请求前检查Token的有效性。如果发现Token过期,则触发刷新流程。这一步骤需注意避免并发请求引发的重复刷新。
  3. 处理Token刷新的响应逻辑:当Token过期时,通过发送Refresh Token请求获取新的Access Token。在这里,需要处理刷新失败的情况,如Refresh Token也失效时,如何引导用户重新登录。
  4. 队列机制的引入:在Token刷新过程中,可能会有多个请求被同时发出。为了避免重复刷新Token,可以引入队列机制,确保在刷新Token期间,其他请求被挂起,直到新的Token可用。
  5. 错误处理与用户体验:最后,要对整个流程中的错误进行处理,比如刷新失败后的重试逻辑、错误提示信息等,确保用户体验不受影响。

通过以上步骤的实现,你可以构建一个用户无感知、稳定可靠的双Token刷新机制,提升应用的安全性与用户体验。接下来,我们将逐一解析这些关键步骤的具体实现。

1. 编写请求拦截器

实现请求拦截器的基本逻辑比较简单,即在每次请求时自动附带上Token以进行认证。

service.interceptors.request.use((config: InternalAxiosRequestConfig) => {  
const userStore = useUserStore()
if (userStore.authInfo.accessToken && userStore.authInfo.accessToken !== "") {
// 设置头部 token
config.headers.Authorization = RequestConstant.Header.AuthorizationPrefix + userStore.authInfo.accessToken;
}
return config;
}, (error: any) => {
return Promise.reject(error);
}
);

目前的实现方案是,在请求存在有效Token时,将其附带到请求头中发送给服务器。但在一些特殊情况下,某些请求可能不需要携带Token。为此,我们可以在请求配置中通过config对象来判断是否需要携带Token。例如:

request: (deptId: number, deptForm: DeptForm): AxiosPromise<void> => {  
return request<void>({
url: DeptAPI.UPDATE.endpoint(deptId),
method: "put",
data: deptForm,
headers: {
// 根据需要添加Token,或者通过自定义逻辑决定是否包含Authorization字段
token: false
}
});
}

那么在请求拦截器中,您需要多加一个判断,就是判断请求头中token是否需要

// 代码省略

2. 深究响应拦截器

对于双token刷新的难点就在于响应拦截器中,因为在这里后端会返回token过期的信息。我们需要先清楚后端接口响应内容

2.1 接口介绍

  • 正常接口响应内容
// Status Code: 200 OK
{
"code":"0000",
"msg":"操作成功",
"data":{}
}
  • accessToken 过期响应内容
// Status Code: 401 Unauthorized
{
"code":"I009",
"msg":"登录令牌过期"
}
  • accessToken 刷新响应内容
// Status Code: 200 OK
{
"code": "0000",
"msg": "操作成功",
"data": {
"accessToken": "",
"refreshToken": "",
"expires": ""
}
}
  • refreshToken 过期响应内容
// Status Code: 200 OK
{
"code": "I009",
"msg": "登录令牌过期"
}

注意 : Status Code不是200时,Axios的响应拦截器会自动进入error方法。在这里,我们可以捕捉到HTTP状态码为401的请求,从而初步判断请求是由于Unauthorized(未授权)引发的。然而,触发401状态码的原因有很多,不一定都代表Token过期。因此,为了准确判断Token是否真的过期,我们需要进一步检查响应体中的code字段。

2.2 响应拦截器编写

有上面的接口介绍,我们编写的就简单,判断error.response?.status === 401、code === I009 即可,如果出现这种情况就直接刷新token。

service.interceptors.response.use(async (response: AxiosResponse) => {
// 正常请求代码忽略
return Promise.reject(new Error(msg || "Error"));
},
async (error: any) => {
const userStore = useUserStore()
if (error.response?.status === 401) {
if (error.response?.data?.code === RequestConstant.Code.AUTH_TOKEN_EXPIRED) {
// token 过期处理
// 1. 刷新 token
const loginResult: LoginResult = await userStore.refreshToken()
if (loginResult) {
// refreshToken 未过期
// 2.1 重构请求头
error.config.headers.Authorization = RequestConstant.Header.AuthorizationPrefix + userStore.authInfo.accessToken;
// 2.2 请求
return await service.request(error.config);
} else {
// refreshToken 过期
// 1. 重置登录 token , 跳转登录页
await userStore.resetToken()
}
} else {
// 如果是系统发出的401 , 重置登录 token , 跳转登录页
await userStore.resetToken()
}
} else if (error.response?.status === 403) {
// 403 结果处理 , 代码省略
} else {
// 其他错误结果处理 , 代码省略
}
return Promise.reject(error.message);
}
);

2.3 解决重复刷新问题

编写完成上面的内容,考虑一下多个请求可能同时遇到 Token 过期,如果没有适当的机制控制,这些请求可能会同时发起刷新 Token 的操作,导致重复请求,甚至可能触发后端的安全机制将这些请求标记为危险操作。

为了解决这个问题,我们实现了一个单例 Promise 的刷新逻辑,通过 singletonRefreshToken 确保在同一时间只有一个请求会发起 Token 刷新操作。其核心思想是让所有需要刷新的请求共享同一个 Promise,这样即使有多个请求同时遇到 Token 过期,它们也只会等待同一个刷新操作的结果,而不会导致多次刷新。

/**
* 刷新 token
*/

refreshToken(): Promise<LoginResult> {
// 如果 singletonRefreshToken 不为 null 说明已经在刷新中,直接返回
if (singletonRefreshToken !== null) {
return singletonRefreshToken
}
// 设置 singletonRefreshToken 为一个 Promise 对象 , 处理刷新 token 请求
singletonRefreshToken = new Promise<LoginResult>(async (resolve) => {
await AuthAPI.REFRESH.request({
accessToken: this.authInfo.accessToken as string,
refreshToken: this.authInfo.refreshToken as string
}
).
then(({data}) => {
// 设置刷新后的Token
this.authInfo = data
// 刷新路由
resolve(data)
}
).
catch(() => {
this.resetToken()
}
)
}
)
// 最终将 singletonRefreshToken 设置为 null, 防止 singletonRefreshToken 一直占用
singletonRefreshToken.
finally(() => {
singletonRefreshToken =
null;
}
)
return singletonRefreshToken
}

重要点解析:

  1. singletonRefreshToken 的使用

    • singletonRefreshToken 是一个全局变量,用于保存当前正在进行的刷新操作。如果某个请求发现 singletonRefreshToken 不为 null,就说明另一个请求已经发起了刷新操作,它只需等待这个操作完成,而不需要自己再发起新的刷新请求。
  2. 共享同一个 Promise

    • 当 singletonRefreshToken 被赋值为一个新的 Promise 时,所有遇到 Token 过期的请求都会返回这个 Promise,并等待它的结果。这样就避免了同时发起多个刷新请求。
  3. 刷新完成后的处理

    • 刷新操作完成后(无论成功与否),都会通过 finally 将 singletonRefreshToken 置为 null,从而确保下一次 Token 过期时能够重新发起刷新请求。

通过这种机制,我们可以有效地避免重复刷新 Token 的问题,同时也防止了由于过多重复请求而引发的后端安全性问题。这种方法不仅提高了系统的稳定性,还优化了资源使用,确保了用户的请求能够正确地处理。

四、测试

  1. 当我们携带过期token访问接口,后端就会返回401状态和I009。

image.png

这时候进入

const loginResult: LoginResult = await userStore.refreshToken()
  1. 携带之前过期的accessToken和未过期的refreshToken进行刷新
  • 携带过期的accessToken的原因 :
    • 防止未过期的 accessToken 进行刷新
    • 防止 accessToken 和 refreshToken 不是同一用户发出的
    • 其他安全性考虑

image.png

  1. 获取到正常结果

image.png


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

收起阅读 »

前端到底该如何安全的实现“记住密码”?

web
在 web 应用里,“记住密码”这个小小的功能,可是咱用户的贴心小棉袄啊,用起来超级方便!但话说回来,咱们得怎样做才能既让用户享受这便利,又能牢牢护住他们的数据安全呢?这可得好好琢磨一番哦!接下来,咱们就来聊聊,有哪些靠谱的方法能实现“记住密码”这个功能,而且...
继续阅读 »

web 应用里,“记住密码”这个小小的功能,可是咱用户的贴心小棉袄啊,用起来超级方便!但话说回来,咱们得怎样做才能既让用户享受这便利,又能牢牢护住他们的数据安全呢?这可得好好琢磨一番哦!接下来,咱们就来聊聊,有哪些靠谱的方法能实现“记住密码”这个功能,而且安全性也是杠杠的!


1. 使用 localStorage


localStorage 是一种持久化存储方式,数据在浏览器关闭后仍然存在。适用于需要长期保存的数据。


示例代码


// 生成对称密钥
async function generateSymmetricKey() {
const key = await crypto.subtle.generateKey(
{
name: "AES-GCM",
length: 256,
},
true,
["encrypt", "decrypt"]
);
return key;
}

// 加密数据
async function encryptData(data, key) {
const iv = crypto.getRandomValues(new Uint8Array(12));
const encryptedData = await crypto.subtle.encrypt(
{
name: "AES-GCM",
iv: iv,
},
key,
new TextEncoder().encode(data)
);
return { iv, encryptedData };
}

// 解密数据
async function decryptData(encryptedData, key, iv) {
const decryptedData = await crypto.subtle.decrypt(
{
name: "AES-GCM",
iv: iv,
},
key,
encryptedData
);
return new TextDecoder().decode(decryptedData);
}

// 保存用户信息
async function saveUserInfo(username, password) {
const key = await generateSymmetricKey();
const { iv, encryptedData } = await encryptData(password, key);
localStorage.setItem('username', username);
localStorage.setItem('password', JSON.stringify({ iv, encryptedData }));
// 密钥可以存储在更安全的地方,如服务器端
}

// 获取用户信息
async function getUserInfo() {
const username = localStorage.getItem('username');
const { iv, encryptedData } = JSON.parse(localStorage.getItem('password'));
const key = await generateSymmetricKey(); // 这里应使用同一个密钥
const password = await decryptData(encryptedData, key, iv);
return { username, password };
}

// 示例:用户登录时调用
async function login(username, password, rememberMe) {
if (rememberMe) {
await saveUserInfo(username, password);
}
// 其他登录逻辑
}

// 示例:页面加载时自动填充
window.onload = async function() {
const userInfo = await getUserInfo();
if (userInfo.username && userInfo.password) {
document.getElementById('username').value = userInfo.username;
document.getElementById('password').value = userInfo.password;
}
};

2. 使用 sessionStorage


sessionStorage 是一种会话级别的存储方式,数据在浏览器关闭后会被清除。适用于需要临时保存的数据。


示例代码


// 保存用户信息
async function saveUserInfoSession(username, password) {
const key = await generateSymmetricKey();
const { iv, encryptedData } = await encryptData(password, key);
sessionStorage.setItem('username', username);
sessionStorage.setItem('password', JSON.stringify({ iv, encryptedData }));
// 密钥可以存储在更安全的地方,如服务器端
}

// 获取用户信息
async function getUserInfoSession() {
const username = sessionStorage.getItem('username');
const { iv, encryptedData } = JSON.parse(sessionStorage.getItem('password'));
const key = await generateSymmetricKey(); // 这里应使用同一个密钥
const password = await decryptData(encryptedData, key, iv);
return { username, password };
}

// 示例:用户登录时调用
async function loginSession(username, password, rememberMe) {
if (rememberMe) {
await saveUserInfoSession(username, password);
}
// 其他登录逻辑
}

// 示例:页面加载时自动填充
window.onload = async function() {
const userInfo = await getUserInfoSession();
if (userInfo.username && userInfo.password) {
document.getElementById('username').value = userInfo.username;
document.getElementById('password').value = userInfo.password;
}
};

3. 使用 IndexedDB


IndexedDB 是一种更为复杂和强大的存储方式,适用于需要存储大量数据的场景。


示例代码


// 打开数据库
function openDatabase() {
return new Promise((resolve, reject) => {
const request = indexedDB.open('UserDatabase', 1);
request.onupgradeneeded = (event) => {
const db = event.target.result;
db.createObjectStore('users', { keyPath: 'username' });
};
request.onsuccess = (event) => {
resolve(event.target.result);
};
request.onerror = (event) => {
reject(event.target.error);
};
});
}

// 保存用户信息
async function saveUserInfoIndexedDB(username, password) {
const key = await generateSymmetricKey();
const { iv, encryptedData } = await encryptData(password, key);
const db = await openDatabase();
const transaction = db.transaction('users', 'readwrite');
const store = transaction.objectStore('users');
store.put({ username, iv, encryptedData });
// 密钥可以存储在更安全的地方,如服务器端
}

// 获取用户信息
async function getUserInfoIndexedDB(username) {
const db = await openDatabase();
return new Promise((resolve, reject) => {
const transaction = db.transaction('users', 'readonly');
const store = transaction.objectStore('users');
const request = store.get(username);
request.onsuccess = async (event) => {
const result = event.target.result;
if (result) {
const key = await generateSymmetricKey(); // 这里应使用同一个密钥
const password = await decryptData(result.encryptedData, key, result.iv);
resolve({ username: result.username, password });
} else {
resolve(null);
}
};
request.onerror = (event) => {
reject(event.target.error);
};
});
}

// 示例:用户登录时调用
async function loginIndexedDB(username, password, rememberMe) {
if (rememberMe) {
await saveUserInfoIndexedDB(username, password);
}
// 其他登录逻辑
}

// 示例:页面加载时自动填充
window.onload = async function() {
const username = 'exampleUsername'; // 从某处获取用户名
const userInfo = await getUserInfoIndexedDB(username);
if (userInfo && userInfo.username && userInfo.password) {
document.getElementById('username').value = userInfo.username;
document.getElementById('password').value = userInfo.password;
}
};

4. 使用 Cookie


Cookie 是一种简单的存储方式,适用于需要在客户端和服务器之间传递少量数据的场景。需要注意的是,Cookie 的安全性较低,建议结合 HTTPS 和 HttpOnly 属性使用。


示例代码


// 设置 Cookie
function setCookie(name, value, days) {
const date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
const expires = "expires=" + date.toUTCString();
document.cookie = name + "=" + value + ";" + expires + ";path=/";
}

// 获取 Cookie
function getCookie(name) {
const nameEQ = name + "=";
const ca = document.cookie.split(';');
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) === ' ') c = c.substring(1, c.length);
if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
}
return null;
}

// 保存用户信息
async function saveUserInfoCookie(username, password) {
const key = await generateSymmetricKey();
const { iv, encryptedData } = await encryptData(password, key);
setCookie('username', username, 7);
setCookie('password', JSON.stringify({ iv, encryptedData }), 7);
// 密钥可以存储在更安全的地方,如服务器端
}

// 获取用户信息
async function getUserInfoCookie() {
const username = getCookie('username');
const { iv, encryptedData } = JSON.parse(getCookie('password'));
const key = await generateSymmetricKey(); // 这里应使用同一个密钥
const password = await decryptData(encryptedData, key, iv);
return { username, password };
}

// 示例:用户登录时调用
async function loginCookie(username, password, rememberMe) {
if (rememberMe) {
await saveUserInfoCookie(username, password);
}
// 其他登录逻辑
}

// 示例:页面加载时自动填充
window.onload = async function() {
const userInfo = await getUserInfoCookie();
if (userInfo.username && userInfo.password) {
document.getElementById('username').value = userInfo.username;
document.getElementById('password').value = userInfo.password;
}
};

5. 使用 JWT(JSON Web Token)


JWT 是一种常用的身份验证机制,特别适合在前后端分离的应用中使用。JWT 可以安全地传递用户身份信息,并且可以在客户端存储以实现“记住密码”功能。


示例代码


服务器端生成 JWT


假设你使用 Node.js 和 Express 作为服务器端框架,并使用 jsonwebtoken 库来生成和验证 JWT。


const express = require('express');
const jwt = require('jsonwebtoken');
const bodyParser = require('body-parser');

const app = express();
app.use(bodyParser.json());

const SECRET_KEY = 'your_secret_key';

// 用户登录接口
app.post('/login', (req, res) => {
const { username, password } = req.body;
// 验证用户名和密码
if (username === 'user' && password === 'password') {
// 生成 JWT
const token = jwt.sign({ username }, SECRET_KEY, { expiresIn: '1h' });
res.json({ token });
} else {
res.status(401).json({ message: 'Invalid credentials' });
}
});

// 受保护的资源
app.get('/protected', (req, res) => {
const token = req.headers['authorization'];
if (!token) {
return res.status(401).json({ message: 'No token provided' });
}
jwt.verify(token, SECRET_KEY, (err, decoded) => {
if (err) {
return res.status(401).json({ message: 'Failed to authenticate token' });
}
res.json({ message: 'Protected resource', user: decoded.username });
});
});

app.listen(3000, () => {
console.log('Server is running on port 3000');
});

客户端存储和使用 JWT


在客户端,可以使用 localStoragesessionStorage 来存储 JWT,并在后续请求中使用。


// 用户登录
async function login(username, password, rememberMe) {
const response = await fetch('/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ username, password })
});
const data = await response.json();
if (response.ok) {
const token = data.token;
if (rememberMe) {
localStorage.setItem('token', token);
} else {
sessionStorage.setItem('token', token);
}
} else {
console.error(data.message);
}
}

// 获取受保护的资源
async function getProtectedResource() {
const token = localStorage.getItem('token') || sessionStorage.getItem('token');
if (!token) {
console.error('No token found');
return;
}
const response = await fetch('/protected', {
method: 'GET',
headers: {
'Authorization': token
}
});
const data = await response.json();
if (response.ok) {
console.log(data);
} else {
console.error(data.message);
}
}

// 示例:用户登录时调用
document.getElementById('loginButton').addEventListener('click', async () => {
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
const rememberMe = document.getElementById('rememberMe').checked;
await login(username, password, rememberMe);
});

// 示例:页面加载时自动填充
window.onload = async function() {
await getProtectedResource();
};

总结


如上示例,展示了如何使用 localStoragesessionStorage、IndexedDB、Cookie 和 JWT 来实现“记住密码”功能。每种方式都有其适用场景和安全考虑,大家可以根据具体需求选择合适的实现方式。


欢迎在评论区留言讨论~
Happy coding! 🚀


作者:我是若尘
来源:juejin.cn/post/7397284874652942363
收起阅读 »

uniapp 地图如何添加?你要的教程来喽!

web
地图在 app 中使用还是很广泛的,常见的应用常见有: 1、获取自己的位置,规划路线。 2、使用标记点进行标记多个位置。 3、绘制多边形,使用围墙标记位置等等。 此篇文章就以高德地图为例,以上述三个常见需求为例,教大家如何在 uniapp 中添加地图。 作为一...
继续阅读 »

地图在 app 中使用还是很广泛的,常见的应用常见有:


1、获取自己的位置,规划路线。


2、使用标记点进行标记多个位置。


3、绘制多边形,使用围墙标记位置等等。


此篇文章就以高德地图为例,以上述三个常见需求为例,教大家如何在 uniapp 中添加地图。


作为一个不管闲事的前端姑娘,我就忽略掉那些繁琐的账号申请,假设需要的信息问项目经理都要来了,如果你没有现成的信息,还需要申请,请查看:


lbs.amap.com/api/javascr…


去高德地图注册账号,根据官网指示获取 key。然后就正式开始前端 uniapp + 高德地图之旅啦!


一、地图配置


在使用地图之前需要配置一下你的地图账号信息,找到项目中的 manifest.json 文件,打开 web 配置,如图:


图片


此处是针对 h5 端,如果我们要打包 安卓和 IOS app 需要配置对应的key信息,如图:


图片


如果这些信息没有人给你提供,就需要自己去官网注册账号实名认证获取。


二、地图使用


2.1、使用标记点进行标记多个位置,具体效果图如下:


图片


<template>
<view class="map-con">
<map style="width: 100%; height: 300px;"
:latitude="latitude"
:longitude="longitude"
:markers="covers"
:scale="12">

</map>
</view>
</template>

<script>
export default {
data() {
return {
longitude: '116.473115',
latitude: '39.993207',
covers: [{
id: 1,
longitude: "116.474595",
latitude: "40.001321",
iconPath: '/static/images/point.png',
},
{
id: 1,
longitude: "116.274595",
latitude: "40.101321",
iconPath: '/static/images/point.png',
},
{
id: 1,
longitude: "116.374595",
latitude: "40.101321",
iconPath: '/static/images/point.png',
},
{
id: 1,
longitude: "116.374595",
latitude: "40.011321",
width: 44,
height: 50,
iconPath:'/static/images/point.png',
}
]
}
}
}
</script>

注意:


看着代码很简单,运行在 h5 之后一切正常,但是运行在安卓模拟器的时候,发现自定义图标没有起作用,显示的是默认标记点。


图片


iconpath 的路径不是相对路径,没有 ../../ 这些,直接根据官网提示写图片路径,虽然模拟器不显示但是真机是正常的。


2.2、绘制多边形,使用围墙标记位置等等。


图片


<template>
<view class="map-con">
<map style="width: 100%; height: 400px;" :latitude="latitude" :longitude="longitude" :scale="11"
:polygons="polygon" :markers="covers">

</map>
</view>
</template>

<script>
export default {
data() {
return {
longitude: '116.304595',
latitude: '40.053207',
polygon: [{
fillColor: '#f00',
strokeColor: '#0f0',
strokeWidth: 3,
points: [{
latitude: '40.001321',
longitude: '116.304595'
},
{
latitude: '40.101321',
longitude: '116.274595'
},
{
latitude: '40.011321',
longitude: '116.374595'
}
]
}],
covers: [{
id: 1,
width: 30,
height: 33,
longitude: "116.314595",
latitude: "40.021321",
iconPath: '/static/images/point.png',
}, ]
}
}
}
</script>

更多样式配置我们去参考官网,官网使用文档写的很细致,地址为:


uniapp 官网:uniapp.dcloud.net.cn/component/m…


三、易错点


1、地图已经显示了,误以为地图未展示


图片


左下角有高德地图标识,就说明地图已经正常显示了,此时可以使用鼠标进行缩放,或设置地图的缩放比例或者修改下地图中心点的经纬度。


2、标记点自定义图标不显示


marker 中的 iconPath 设置标记点的图标路径,可以使用相对路径、base64 等,但是在 h5 查看正常,app 打包之后就不能正常显示了,务必参考官网。


3、uni.getLocation 无法触发


在调试模式中,调用 uni.getLocation 无法触发,其中的 success fail complete 都无法执行,不调用的原因是必须在 https 环境下,所以先保证是在 https 环境下。****


四、有可用插件吗?


uniapp 插件:ext.dcloud.net.cn/search?q=ma…


搜索地图插件的时候,插件挺多的,有免费的也有付费的,即使使用插件也是需要需要注册第三方地图账号的。


我个人认为 uniapp 已经将第三方地图封装过了,使用挺便捷的,具体是否使用插件就根据项目实际情况定。


作者:前端人_倩倩
来源:juejin.cn/post/7271942371637559348
收起阅读 »

告别轮询,SSE 流式传输可太香了!

web
今天想和大家分享的一个技术是 SSE 流式传输 。如标题所言,通过 SSE 流式传输的方式可以让我们不再通过轮询的方式获取服务端返回的结果,进而提升前端页面的性能。 对于需要轮询的业务场景来说,采用 SSE 确实是一个更好的技术方案。 接下来,我将从 SSE ...
继续阅读 »

今天想和大家分享的一个技术是 SSE 流式传输 。如标题所言,通过 SSE 流式传输的方式可以让我们不再通过轮询的方式获取服务端返回的结果,进而提升前端页面的性能。


对于需要轮询的业务场景来说,采用 SSE 确实是一个更好的技术方案。


接下来,我将从 SSE 的概念、与 Websocket 对比、SSE 应用场景多个方面介绍 SSE 流式传输,感兴趣的同学一起来了解下吧!


什么是 SSE 流式传输


SSE 全称为 Server-sent events , 是一种基于 HTTP 协议的通信技术,允许服务器主动向客户端(通常是Web浏览器)发送更新。


它是 HTML5 标准的一部分,设计初衷是用来建立一个单向的服务器到客户端连接,使得服务器可以实时地向客户端发送数据。


这种服务端实时向客户端发送数据的传输方式,其实就是流式传输。


我们在与 ChatGPT 交互时,可以发现 ChatGPT 的响应总是间断完成。细扒 ChatGPT 的网络传输模式,可以发现,用的也是流式传输。


图片


SSE 流式传输的好处


在 SSE 技术出现之前,我们习惯把需要等待服务端返回的过程称为长轮询。


长轮询的实现其实也是借助 http 请求来完成,一个完整的长轮询过程如下图所示:


图片


从图中可以发现,长轮询最大的弊端是当服务端响应请求之前,客户端发送的所有请求都不会被受理。并且服务端发送响应的前提是客户端发起请求。


前后端通信过程中,我们常采用 ajax 、axios 来异步获取结果,这个过程,其实也是长轮询的过程。


而同为采用 http 协议通信方式的 SSE 流式传输,相比于长轮询模式来说,优势在于可以在不需要客户端介入的情况下,多次向客户端发送响应,直至客户端关闭连接。


这对于需要服务端实时推送内容至客户端的场景可方便太多了!


SSE 技术原理


1. 参数设置

前文说到,SSE 本质是一个基于 http 协议的通信技术。


因此想要使用 SSE 技术构建需要服务器实时推送信息到客户端的连接,只需要将传统的 http 响应头的 contentType 设置为 text/event-stream 。


并且为了保证客户端展示的是最新数据,需要将 Cache-Control 设置为 no-cache 。


在此基础上,SSE 本质是一个 TCP 连接,因此为了保证 SSE 的持续开启,需要将 Connection 设置为 keep-alive 。


Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive

完成了上述响应头的设置后,我们可以编写一个基于 SSE 流式传输的简单 Demo 。


2. SSE Demo

服务端代码:


const express = require('express');
const app = express();
const PORT = 3000;

app.use(express.static('public'));

app.get('/events'function(req, res) {
    res.setHeader('Content-Type''text/event-stream');
    res.setHeader('Cache-Control''no-cache');
    res.setHeader('Connection''keep-alive');

    let startTime = Date.now();

    const sendEvent = () => {
        // 检查是否已经发送了10秒
        if (Date.now() - startTime >= 10000) {
            res.write('event: close\ndata: {}\n\n'); // 发送一个特殊事件通知客户端关闭
            res.end(); // 关闭连接
            return;
        }

        const data = { message'Hello World'timestampnew Date() };
        res.write(`data: ${JSON.stringify(data)}\n\n`);

        // 每隔2秒发送一次消息
        setTimeout(sendEvent, 2000);
    };

    sendEvent();
});

app.listen(PORT() => {
    console.log(`Server running on http://localhost:${PORT}`);
});

客户端代码:


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

<head>
    <meta charset="UTF-8">
    <title>SSE Example</title>
</head>

<body>
    <h1>Server-Sent Events Example</h1>
    <div id="messages"></div>

    <script>
        const evtSource = new EventSource('/events');
        const messages = document.getElementById('messages');

        evtSource.onmessage = function(event) {
            const newElement = document.createElement("p");
            const eventObject = JSON.parse(event.data);
            newElement.textContent = "Message: " + eventObject.message + " at " + eventObject.timestamp;
            messages.appendChild(newElement);
        };
    
</script>
</body>
</html>

当我们在浏览器中访问运行在 localhost: 3000 端口的客户端页面时,页面将会以 流式模式 逐步渲染服务端返回的结果:


图片


需要注意的是,为了保证使用 SSE 通信协议传输的数据能被客户端正确的接收,服务端和客户端在发送数据和接收数据应该遵循以下规范:


服务端基本响应格式

SSE 响应主要由一系列以两个换行符分隔的事件组成。每个事件可以包含以下字段:


data:事件的数据。如果数据跨越多行,每行都应该以data:开始。
id:事件的唯一标识符。客户端可以使用这个ID来恢复事件流。
event:自定义事件类型。客户端可以根据不同的事件类型来执行不同的操作。
retry:建议的重新连接时间(毫秒)。如果连接中断,客户端将等待这段时间后尝试重新连接。

字段之间用单个换行符分隔,而事件之间用两个换行符分隔。


客户端处理格式

客户端使用 EventSource 接口监听 SSE 消息:


const evtSource = new EventSource('path/to/sse');
evtSource.onmessage = function(event) {
    console.log(event.data); // 处理收到的数据
};

SSE 应用场景


SSE 作为基于 http 协议由服务端向客户端单向推送消息的通信技术,对于需要服务端主动推送消息的场景来说,是非常适合的:


图片


SSE 兼容性


图片


可以发现,除了 IE 和低版本的主流浏览器,目前市面上绝大多数浏览器都支持 SSE 通信。


SSE 与 WebSocket 对比


看完 SSE 的使用方式后,细心的同学应该发现了:


SSE 的通信方式和 WebSocket 很像啊,而且 WebSocket 还支持双向通信,为什么不直接使用 WebSocket ?


下表展示了两者之间的对比:


特性/因素SSEWebSockets
协议基于HTTP,使用标准HTTP连接单独的协议(ws:// 或 wss://),需要握手升级
通信方式单向通信(服务器到客户端)全双工通信
数据格式文本(UTF-8编码)文本或二进制
重连机制浏览器自动重连需要手动实现重连机制
实时性高(适合频繁更新的场景)非常高(适合高度交互的实时应用)
浏览器支持良好(大多数现代浏览器支持)非常好(几乎所有现代浏览器支持)
适用场景实时通知、新闻feed、股票价格等需要从服务器推送到客户端的场景在线游戏、聊天应用、实时交互应用
复杂性较低,易于实现和维护较高,需要处理连接的建立、维护和断开
兼容性和可用性基于HTTP,更容易通过各种中间件和防火墙可能需要配置服务器和网络设备以支持WebSocket
服务器负载适合较低频率的数据更新适合高频率消息和高度交互的场景

可以发现,SSE 与 WebSocket 各有优缺点,对于需要客户端与服务端高频交互的场景,WebSocket 确实更适合;但对于只需要服务端单向数据传输的场景,SSE 确实能耗更低,且不需要客户端感知


参考文档


developer.mozilla.org/zh-CN/docs/…


作者:veneno
来源:juejin.cn/post/7355666189475954725
收起阅读 »

整理最近的生活

web
Hi,见到你真好 :)写在开始自从去年 8月 搬到 上海 之后,就很少再写文章,很多的思路都是断断续续,导致也不知道该写点什么,所以最后是是草稿攒了一堆,大纲整整齐齐,内容空空如也,甚是尴尬。时间长了后,逐渐就有点开摆的心态。也有点理解为什么很多技术同学突然就...
继续阅读 »

Hi,见到你真好 :)

写在开始

自从去年 8月 搬到 上海 之后,就很少再写文章,很多的思路都是断断续续,导致也不知道该写点什么,所以最后是是草稿攒了一堆,大纲整整齐齐,内容空空如也,甚是尴尬。

时间长了后,逐渐就有点开摆的心态。也有点理解为什么很多技术同学突然就不更新了。🫨

不过最近好在有些念头又开始跃跃欲试,故想着写一些东西,活跃活跃生锈的脑子🫡。

故本篇,其实算是一个随记,想到哪里就写到哪里,不包含任何技术指南。

搬家三两事

背景

因为当时是需要从北京搬到上海,家里还有两只猫以及超级多的行李,故需要考虑的事情有下面几点

  1. 琐碎的行李怎么处理?
  2. 如何将 两只猫 安全的送到上海?
  3. 升降桌、工学椅、冰柜、猫厕所等大件怎么处理?

头脑风暴

  1. 两只猫托运走

    成本过高,一只平均需要 1000+ ,以及需要疫苗齐全,以及需要提前7天以上准备。

  2. 升降桌、工学椅、冰柜出二手

    出二手 = 5折以下,最主要是刚买没半年。😑

  3. 行李快递走?

    货拉拉跨城搬运+快递一部分。

上述总费用: 托运🐱2000 + 二手折损(3000) + 货拉拉(4000+) + 动态费用1000 = 9500 左右。

备注点:

  1. 托运猫的安全性;
  2. 如果喊货拉拉,那就不需要二手回收;

算完上述费用之后,我忍不住拍了一下家里两只,嘴里嚷嚷着:要不是你两,我何至于如此!!!

最终解决

小红书约了一个跨城搬家师傅,车型依维柯(长4.9,宽1.9,高2.2),最后只花了 3500 解决了。

关于爱好的倒腾

世界奇奇怪怪,生活慢慢悠悠。

有时候会想,人的一生难得有几个爱好,那可能就会包含折腾 电子小垃圾:)

下面列一下今年折腾过的一些小垃圾:

  • Pixel 7 绿色
  • 戴尔 U2723QX
  • ikbc 高达97键盘
  • ipadAir M1丐版
  • PS5 国行双手柄Slim
  • Studio Display 带升降
  • 富士 XT-3、35-1.4镜头
  • MacMini M1 16+256 丐版

关于相机

一直以来,其实都比较喜欢富士相机的直出,主要因为自己较懒。所以对于学习修图,实在是提不起感觉,而对于摄像的技巧,也只是草草了解几个构图方式,也谈不上研究,故富士就比较适合我这种[懒人]。

但其实这个事情的背景是,在最开始接触相机时,大概是21年,那时想买富士 xs-10,结果因为疫情缺货,国内溢价到了1w,属实是离谱。所以当时就买了佳能 M6 Mark2

等过了半年左右,觉得佳能差点意思,就又换了索尼的 A7C,对焦嘎嘎猛,主打的就是一个激情拍摄,后期就是套个滤镜就行🫡。

等新手福利期一过,时间线再往后推半年,相机开始吃土,遂出了二手👀。

来上海后,又想起了相机,故在闲鱼上又收了一个富士XT3,属于和xs20同配置(少几个滤镜),主打的就是一个拍到就是赚到(当然现在也是吃土🤡)。

关于PS5

因为21年淘过一个 PS4 Pro ,平时也是处于吃灰,玩的最多的反而是 双人成行(现在也没和老婆打完,80%)😬。

但抵不住冲动的心,没事就会看几眼 PS5 ,为此,老婆专门买了一个,以解我没事的念想。

不过真得知快递信息后,还是心里有点不舍。就以家里还有一个在吃土,买这个没啥用为由退掉🫨。

抵得过暂时,抵不过长久,过了一段时间,念头又上来了,没事又翻起了pdd和二手鱼。

于是,在一个风和日丽的中午,激情下单。

结果卖家的名字居然和我只有一字之差,真的是造化弄人。🤡

到手之后,每周的愿望就是能在周末打一会游戏,结果很难有实现过,唯一一次畅玩 [潜水员戴夫🐟],结果导致身心疲惫。

ps: 截止写完这篇时候,最近在爽玩黑神话悟空,故真正实现了使用。

最近抽空在打大镖客2和黑悟空,遂带上几张图。

IMG_8222

image-20240825173156146

关于小主机

之前因为只有一个笔记本,上下班都需要背笔记本,遇到冬天还好,到了夏天,就非常反感,故最近诞生出了买一个 小主机 的想法。

因为不玩pc游戏,故 winodws 系天然不用选择,当然也就不用考虑同事推荐的 零刻 这种小主机,直接去搞一个 mac mini 即可。

最后综合对比了一下,觉得 Mac Mini(M1-16/256) 即可,闲鱼只需要3000左右即可。

对 M1 及以后的Mac设备而言,RAM>CPU>固态 ,故 cpu 的性能对我而言足矣。

故而以 16g 作为标准,而存储方面,可以考虑外接固态做扩展盘解决,这套方案是性价比最高的。🫡

有了上面的结论之后,就直接去闲鱼收,最后 3100 收了一个 MacMini M1/16g

然后又在京东买了一个 硬盘盒子(海康威视20Gbps)+雷神固态(PR5000/读写4.5k)。

本来想去买一个 40Gbps 的硬盘盒,结果一看价,399起步,有点夸张(我盘才4xx),遂放弃。

Tips:

  1. 不要轻易把系统装到扩展固态(特别是硬盘盒速度不够40Gbps时)上。
  • 开机引导会变慢(如果一直不关机当我没说);
  • 如果硬盘散热跟不上,系统会卡顿,如同ANR了一样;
  1. 因为 MacMini 雷电口 不支持 usb3.2 Gen2x2,在不满足雷电口的情况下,最快读写 只有1k。故导致硬盘速度只能到达 1k 读写,也就是硬盘盒的一半速度

最后全家福如下:

image.png

关于显示器

最近一年连着用过了 3 个显示器,价位一路从 1K -> 1.2W,也算又多了一点点经验分享。

先说背景:因为没有游戏与高刷需求,更多的是追求接近Mac体验(用同事话说就是,被果子惯得),故下面的评测仅限于特定范围。

本次的参照物:MacBook Pro14寸自带的 XDR显示器、峰值亮度 1600nt

Redmi 27/4k (1.5k)

HDR400、65W Typc、95%P3、E<2

全功能typc、支持kvm,色彩模式有forMac。

塑料机身,但设计挺好看的,观感不错,for Mac模式 能接近 70% 体验;

仔细对比,与mac屏幕差距最大的是通透度与色准问题,如果说MacBook是 100% 通透,红米只有 60-70% 之间;

后期偶尔会出现连接不稳定,屏幕闪烁问题,时好时不好,猜测可能与 typc65w 电压不稳有关。

综合来说,这个价位,性价比非常之高

戴尔U2723QX (3k)

HDR400、90W Typc、98%P3

全功能typc、支持kvm、接口大满贯,多到发指。

全金属机身,屏幕是 IPS Black ,也就是 LG 的 NanoIPS 面板,不过也算是一块老屏幕了。

整体体验下来不错,有接近MacBook 80% 的体验,色准什么的都很ok,在使用过程中,也没遇见过任何问题。

综合来说,算是 普通消费级代码显示器的王者 ,不过如果要提性价比,可能不如同款屏幕的 LG。

至于戴尔的售后,因为没体验过,所以难评。

Studio Display (1.2w)

5k分辨率、600nits、A13、六扬声器、4麦克风、自带摄像头

接口方面:雷电3(40G) + 3 x typc(10G)

Mac系列的最佳搭配,最接近 MacBook 的色准,非常通透,95%+ 的接近水平,亮度差点,不过已经够了;

整体来说,如果对屏幕要求,或者眼睛比较敏感,那么这个显示器是比较不错的选择(别忘了开原彩+自动亮度,看习惯了后,还是很舒服)。

至于不足之处,可能就只有价格这一个因素。

家用设备的倒腾

来上海之后,家庭设备倒腾的比较少,少有的几个物件是:

  • 小米除湿机 22L;
  • 追觅洗地机 H30Mix;
  • 小米55寸 MiniLed 电视;

关于除湿机

当时刚来上海,作为一个土生土长的北方人,遇到黄梅天,那感觉,简直浑身难受,一个字 黏,两个字 闷热

故当时紧急下单了一款除湿机,一晚上可以除满满一桶,实测大概4-5L,最高档开1小时左右,家里基本就可以感受到干爽,比较适合40m的小家使用。再说说缺点:

  • 吵!
  • 如果没有接下水管,可能需要隔两天换一次水;

再说说现状,成功实现 100% 吃土状态,几乎毫无悬念,因为空调更省事。。。

最后给一些北方人首次去南方居住的建议:

  • 不要选3层以下,太潮;
  • 注意白天看房,看看光线如何;
  • 注意附近切记不是建筑工地等等;
  • 上海合租比较便宜,自如挺合适;

关于洗地机

刚到上海时,之前的 米家扫拖机器人 也一起带过来了,但因为实在 版本太旧,逐渐不堪大用 ,只能用勉强可用这个词来形容,而且特别容易 积攒头发加手动加水清洁 ,时间久了,就比较烦。

故按照我的性格,没事就在看新的替代物件,最开始锁定的是追觅扫拖机器人,但最后经过深思熟虑,觉得家里太小(60m) ,故扫地机器人根本转不开腿,可能咣咣撞椅子了,故退而求其次,去看洗地机。入手了追觅的 H30mix洗地吸尘都能干

最后经过实际证明:洗地机就老老实实洗地,吸尘还是交给专门的吸尘器,主要是拆卸太麻烦🤡。故家里本来已经半个身子准备退休的德尔玛又被强行续命了一波。

再说优点,真的很好用,拖完地放架子上自动洗烘一体,非常方便。实测拖的比我干净,唯一缺点就是,每天洗完需要手动倒一下脏水(不倒可能会反味)。

关于电视

来上海后,一直想换个电视打游戏用,就没事看了看电视。因为之前的 Tcl 邮给了岳父老房子里,于是按耐不住的心又开始躁动了,故某个夜晚就看了下电视,遂对比后下单了小米的55寸 miniLed。考虑到家里地方不是很大,故也顺带卖了一个可移动的支架。

现在电视的价格是真的便宜,Miniled 都被小米干到了 2k 附近,但一分钱一分货,纸面参数终究是纸面参数,最后看看实际观感,也就那样。

image.png

关于一些想法

人生不过几十载,如果工作要占用掉最宝贵的20年华,那未免太过于糟糕。

不知为何,最近总感觉上班的时间过得尤为快,每周过了周二后,周五的下一步就又要到了,下个月初也越来越近了。

来上海后,几乎每天都会和老婆晚上下楼走走,近的时候绕着小区,远的时候绕着小区外面的路。起初刚来上海时,脑子里依然会有过几年会北京的想法,但在上海有段时间后,这个想法就变得没那么重了,直到现在,我两都变成了留在上海也许更好(仔细算了算)😐。

写在最后

兜兜转转,这篇也是写了近一个月,属于是想起来写一点,接下来会更新的频繁一点。

下次再见,朋友们 👋

关于我

我是 Petterp ,一个 Android 工程师。如果本文,你觉得写的还不错,不妨点个赞或者收藏,你的支持,是我持续创作的最大鼓励!


作者:Petterp
来源:juejin.cn/post/7406258856953790515
收起阅读 »

if-else嵌套太深怎么办?

web
在前端开发中,if-else 嵌套过深往往会导致代码可读性下降、维护难度增加,甚至引发潜在的逻辑错误。本文将从一个典型的深层 if-else 嵌套案例出发,逐步分析并探讨多种优化策略,帮助开发者解决这一问题。 一、深层 if-else 嵌套的案例 假设我们正在...
继续阅读 »

在前端开发中,if-else 嵌套过深往往会导致代码可读性下降、维护难度增加,甚至引发潜在的逻辑错误。本文将从一个典型的深层 if-else 嵌套案例出发,逐步分析并探讨多种优化策略,帮助开发者解决这一问题。


一、深层 if-else 嵌套的案例


假设我们正在开发一个处理订单状态的功能,根据订单的不同状态执行相应的操作。下面是一个典型的 if-else 嵌套过深的代码示例:


function processOrder(order) {
if (order) {
if (order.isPaid) {
if (order.hasStock) {
if (!order.isCanceled) {
// 处理已付款且有库存的订单
return 'Processing paid order with stock';
} else {
// 处理已取消的订单
return 'Order has been canceled';
}
} else {
// 处理库存不足的订单
return 'Out of stock';
}
} else {
// 处理未付款的订单
return 'Order not paid';
}
} else {
// 处理无效订单
return 'Invalid order';
}
}
****

这段代码展示了多个条件的嵌套判断,随着条件的增多,代码的层级不断加深,使得可读性和可维护性大幅降低。


二、解决方案


1. 使用早返回


早返回是一种有效的方式,可以通过尽早退出函数来避免不必要的嵌套。


function processOrder(order) {
if (!order) {
return 'Invalid order';
}
if (!order.isPaid) {
return 'Order not paid';
}
if (!order.hasStock) {
return 'Out of stock';
}
if (order.isCanceled) {
return 'Order has been canceled';
}
return 'Processing paid order with stock';
}


通过早返回,条件判断被简化为一系列独立的判断,减少了嵌套层级,代码更直观。


2. 使用对象字面量或映射表


当条件判断基于某个特定的值时,可以利用对象字面量替代 if-else


const orderStatusActions = {
'INVALID': () => 'Invalid order',
'NOT_PAID': () => 'Order not paid',
'OUT_OF_STOCK': () => 'Out of stock',
'CANCELED': () => 'Order has been canceled',
'DEFAULT': () => 'Processing paid order with stock',
};

function processOrder(order) {
if (!order) {
return orderStatusActions['INVALID']();
}
if (!order.isPaid) {
return orderStatusActions['NOT_PAID']();
}
if (!order.hasStock) {
return orderStatusActions['OUT_OF_STOCK']();
}
if (order.isCanceled) {
return orderStatusActions['CANCELED']();
}
return orderStatusActions['DEFAULT']();
}


使用对象字面量将条件与行为进行映射,使代码更加模块化且易于扩展。


3. 使用策略模式


策略模式可以有效应对复杂的多分支条件,通过定义一系列策略类,将不同的逻辑封装到独立的类中。


class OrderProcessor {
constructor(strategy) {
this.strategy = strategy;
}

process(order) {
return this.strategy.execute(order);
}
}

class PaidOrderStrategy {
execute(order) {
if (!order.hasStock) {
return 'Out of stock';
}
if (order.isCanceled) {
return 'Order has been canceled';
}
return 'Processing paid order with stock';
}
}

class InvalidOrderStrategy {
execute(order) {
return 'Invalid order';
}
}

class NotPaidOrderStrategy {
execute(order) {
return 'Order not paid';
}
}

// 使用策略模式
const strategy = order ? (order.isPaid ? new PaidOrderStrategy() : new NotPaidOrderStrategy()) : new InvalidOrderStrategy();
const processor = new OrderProcessor(strategy);
processor.process(order);


策略模式将不同逻辑分散到独立的类中,避免了大量的 if-else 嵌套,增强了代码的可维护性。


4. 使用多态


通过多态性,可以通过继承和方法重写替代 if-else 条件分支。


优化后的代码:


class Order {
process() {
throw new Error('This method should be overridden');
}
}

class PaidOrder extends Order {
process() {
if (!this.hasStock) {
return 'Out of stock';
}
if (this.isCanceled) {
return 'Order has been canceled';
}
return 'Processing paid order with stock';
}
}

class InvalidOrder extends Order {
process() {
return 'Invalid order';
}
}

class NotPaidOrder extends Order {
process() {
return 'Order not paid';
}
}

// 通过多态处理订单
const orderInstance = new PaidOrder(); // 根据order实例化相应的类
orderInstance.process();


多态性允许我们通过不同的子类实现不同的逻辑,从而避免在同一个函数中使用大量的 if-else


5. 使用函数式编程技巧


函数式编程中的 map, filter, 和 reduce 可以帮助我们避免复杂的条件判断。


优化后的代码:


const orderProcessors = [
{condition: (order) => !order, process: () => 'Invalid order'},
{condition: (order) => !order.isPaid, process: () => 'Order not paid'},
{condition: (order) => !order.hasStock, process: () => 'Out of stock'},
{condition: (order) => order.isCanceled, process: () => 'Order has been canceled'},
{condition: () => true, process: () => 'Processing paid order with stock'},
];

const processOrder = (order) => orderProcessors.find(processor => processor.condition(order)).process();


通过 findfilter 等函数式编程方法,我们可以避免嵌套的 if-else 语句,使代码更加简洁和易于维护。


三、总结


if-else 嵌套过深的问题是前端开发中常见的挑战。通过本文提供的多种解决方案,如早返回、对象字面量、策略模式、多态和函数式编程技巧,开发者可以根据实际需求选择合适的优化方案,从而提高代码的可读性、可维护性和性能。


希望这些方法能对你的开发工作有所帮助,欢迎在评论区分享你的经验与想法!


作者:争取不脱发的程序猿
来源:juejin.cn/post/7406538050228633641
收起阅读 »

Oracle开始严查Java许可!

web
0x01、 前段时间在论坛里就看到一个新闻,说“Oracle又再次对Java下手,开始严查Java许可,有企业连夜删除JDK”,当时就曾在网上引起了一阵关注和讨论。 这不最近在科技圈又看到有媒体报道,Oracle再次严查,对于Java许可和版权的审查越来越严格...
继续阅读 »

0x01、


前段时间在论坛里就看到一个新闻,说“Oracle又再次对Java下手,开始严查Java许可,有企业连夜删除JDK”,当时就曾在网上引起了一阵关注和讨论。


这不最近在科技圈又看到有媒体报道,Oracle再次严查,对于Java许可和版权的审查越来越严格了。


其实很早之前就有看到新闻报道说,甲骨文公司Oracle已经开始将Java纳入其软件许可审查中,并且对一些公司的Java采用情况开启审计,目的是找出那些处于不合规边缘或已经违规的客户。


之前主要还是针对一些小公司发出过审查函件,而现在,甚至包括财富200强在内的一些组织或公司都收到了来自Oracle有关审查方面的信件。



0x02、


还记得去年上半年的时候,Oracle就曾发布过一个PDF格式的新版Java SE收费政策《Oracle Java SE Universal Subscription Global Price List (PDF)》。



打开那个PDF,在里面可以看到Oracle新的Java SE通用订阅全球价目表:



表格底部还举了一个具体计费的例子。


比方说一个公司有28000名总雇员,里面可能包含有23000名全职、兼职、临时雇员,以及5000其他类型员工(比如说代理商、合约商、咨询顾问),那这个总价格是按如下方式进行计算:


28000 * 6.75/12个月=2268000/月 * 12个月 = 2268000/年


合着这个新的收费标准是直接基于公司里总的员工数来进行计算的,而不仅仅是使用Java SE的员工数。


这样一来,可能就会使企业在相同软件的的使用情况下会多出不少费用,从而增加软件成本。


看到这里不得不说,Oracle接手之后把Java的商业化运作这块整得是明明白白的。


0x03、


众所周知,其实Java最初是由Sun公司的詹姆斯·高斯林(James Gosling,后来也被称为Java之父)及其团队所研发的。



并且最开始名字并不叫Java,而是被命名为:Oak,这个名字得自于 Gosling 想名字时看到了窗外的一棵橡树。



就在 Gosling 的团队即将发布成果之前,又出了个小插曲——Oak 竟然是一个注册商标。Oak Technology(OAKT)是一家美国半导体芯片制造商,Oak 是其注册商标。


既然不能叫Oak,那应该怎么命名好呢?


后来 Gosling 看见了同事桌上有一瓶咖啡,包装上写着 Java,于是灵感一现。至此,Java语言正式得名,并使用至今。


1995年5月,Oak语言才更名为Java(印度尼西亚爪哇岛的英文名称,因盛产咖啡而闻名),并于当时的SunWorld大会上发布了JAVA 1.0,而且那句“Write Once,Run Anywhere”的slogan也是那时候推出的。



此后,Java语言一直由Sun公司来进行维护开发,一直到早期的JDK 7。


2009年4月,Oracle以74亿美元现金收购了Sun公司,至此一代巨头基本没落。


与此同时,Java商标也被列入Oracle麾下,成为了Oracle的重要资源。



众所周知,Oracle接手Java之后,就迅速开始了商业化之路的实践,也于后续推出了一系列调整和改革的操作。


其实Oracle早在2017年9月就宣布将改变JDK版本发布周期。新版本发布周期中,一改原先以特性驱动的发布方式,而变成了以时间为驱动的版本迭代。


也即:每6个月会发布一个新的Java版本,而每3年则会推出一个LTS版本。



而直到前段时间,Java 22都已经正式发布了。



0x04、


那针对Oracle这一系列动作,以及新的定价策略和订阅问题,有不少网友讨论道,那就不使用Oralce JDK,切换到OpenJDK,或者使用某些公司开源的第三方JDK。


众所周知,OpenJDK是一个基于GPL v2 许可的开源项目,自Java 7开始就是Java SE的官方参考实现。


既然如此,也有不少企业或者组织基于OpenJDK从而构建了自己的JDK版本,这些往往都是基于OpenJDK源码,然后增加或者说定制一些自己的专属内容。


比如像阿里的Dragonwell,腾讯的Kona,AWS的Amazon Corretto,以及Azul提供的Zulu JDK等等,都是这类典型的代表。




它们都是各自根据自身的业务场景和业务需求并基于OpenJDK来打造推出的开源JDK发行版本,像这些也都是可以按需去选用的。


文章的最后,也做个小调查:


大家目前在用哪款JDK和版本来用于开发环境或生产环境的呢?



注:本文在GitHub开源仓库「编程之路」 github.com/rd2coding/R… 中已经收录,里面有我整理的6大编程方向(岗位)的自学路线+知识点大梳理、面试考点、我的简历、几本硬核pdf笔记,以及程序员生活和感悟,欢迎star。



作者:CodeSheep
来源:juejin.cn/post/7405845617282449462
收起阅读 »

网页也能像 QQ 一样发出右下角消息?轻松实现桌面通知!

web
网页也能像 QQ 一样发出右下角消息?轻松实现桌面通知! 大家好,我是蒜鸭。今天我们来聊聊如何让网页像 QQ 那样在右下角弹出消息通知。这个功能不仅能提升用户体验,还能增加网站的互动性。让我们一起探索如何在网页中实现这个酷炫的功能吧! 1. 为什么需要网页通知...
继续阅读 »

网页也能像 QQ 一样发出右下角消息?轻松实现桌面通知!


大家好,我是蒜鸭。今天我们来聊聊如何让网页像 QQ 那样在右下角弹出消息通知。这个功能不仅能提升用户体验,还能增加网站的互动性。让我们一起探索如何在网页中实现这个酷炫的功能吧!


1. 为什么需要网页通知?


在当今信息爆炸的时代,获取用户注意力变得越来越困难。传统的网页通知方式,如弹窗或页面内提示,往往会打断用户的浏览体验。而类似 QQ 那样的右下角消息通知,既能及时传递信息,又不会过分干扰用户,可以说是一种相当优雅的解决方案。


实现这种通知功能,我们有两种主要方式:使用 Web Notifications API 或自定义 CSS+JavaScript 实现。接下来,我们将详细探讨这两种方法的实现过程、优缺点以及适用场景。


2. 使用 Web Notifications API


2.1 Web Notifications API 简介


Web Notifications API 是现代浏览器提供的一个强大功能,它允许网页向用户发送通知,即使在用户没有打开网页的情况下也能工作。这个 API 的使用非常简单,但功能却十分强大。


2.2 基本实现步骤



  1. 检查浏览器支持

  2. 请求用户授权

  3. 创建并显示通知


让我们来看看具体的代码实现:


// 检查浏览器是否支持通知
if ("Notification" in window) {
// 请求用户授权
Notification.requestPermission().then(function (permission) {
if (permission === "granted") {
// 创建并显示通知
var notification = new Notification("Hello from Web!", {
body: "这是一条来自网页的通知消息",
icon: "path/to/icon.png"
});

// 点击通知时的行为
notification.onclick = function() {
window.open("https://example.com");
};
}
});
}

2.3 优点和注意事项


优点:

– 原生支持,无需额外库

– 可以在用户未浏览网页时发送通知

– 支持富文本和图标


注意事项:

– 需要用户授权,一些用户可能会拒绝

– 不同浏览器的显示样式可能略有不同

– 过度使用可能会引起用户反感


3. 自定义 CSS+JavaScript 实现


如果你想要更多的样式控制,或者希望通知始终显示在网页内,那么使用自定义的 CSS+JavaScript 方案可能更适合你。


3.1 基本思路



  1. 创建一个固定位置的 div 元素作为通知容器

  2. 使用 JavaScript 动态创建通知内容

  3. 添加动画效果使通知平滑显示和消失


3.2 HTML 结构


<div id="notification-container"></div>

3.3 CSS 样式


#notification-container {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 9999;
}

.notification {
background-color: #f8f8f8;
border-left: 4px solid #4CAF50;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
padding: 16px;
margin-bottom: 10px;
width: 300px;
opacity: 0;
transform: translateX(100%);
transition: all 0.3s ease-in-out;
}

.notification.show {
opacity: 1;
transform: translateX(0);
}

.notification-title {
font-weight: bold;
margin-bottom: 5px;
}

.notification-body {
font-size: 14px;
}

3.4 JavaScript 实现


function showNotification(title, message, duration = 5000) {
const container = document.getElementById('notification-container');

const notification = document.createElement('div');
notification.className = 'notification';

const titleElement = document.createElement('div');
titleElement.className = 'notification-title';
titleElement.textContent = title;

const bodyElement = document.createElement('div');
bodyElement.className = 'notification-body';
bodyElement.textContent = message;

notification.appendChild(titleElement);
notification.appendChild(bodyElement);

container.appendChild(notification);

// 触发重绘以应用初始样式
notification.offsetHeight;

// 显示通知
notification.classList.add('show');

// 设置定时器移除通知
setTimeout(() => {
notification.classList.remove('show');
setTimeout(() => {
container.removeChild(notification);
}, 300);
}, duration);
}

// 使用示例
showNotification('Hello', '这是一条自定义通知消息');

3.5 优点和注意事项


优点:

– 完全可定制的外观和行为

– 不需要用户授权

– 可以轻松集成到现有的网页设计中


注意事项:

– 仅在用户浏览网页时有效

– 需要考虑移动设备的适配

– 过多的通知可能会影响页面性能


4. 高级技巧和最佳实践


4.1 通知分级


根据通知的重要性进行分级,可以使用不同的颜色或图标来区分:


function showNotification(title, message, level = 'info') {
// ... 前面的代码相同

let borderColor;
switch(level) {
case 'success':
borderColor = '#4CAF50';
break;
case 'warning':
borderColor = '#FFC107';
break;
case 'error':
borderColor = '#F44336';
break;
default:
borderColor = '#2196F3';
}

notification.style.borderLeftColor = borderColor;

// ... 后面的代码相同
}

// 使用示例
showNotification('成功', '操作已完成', 'success');
showNotification('警告', '请注意...', 'warning');
showNotification('错误', '出现问题', 'error');

4.2 通知队列


为了避免同时显示过多通知,我们可以实现一个简单的通知队列:


const notificationQueue = [];
let isShowingNotification = false;

function queueNotification(title, message, duration = 5000) {
notificationQueue.push({ title, message, duration });
if (!isShowingNotification) {
showNextNotification();
}
}

function showNextNotification() {
if (notificationQueue.length === 0) {
isShowingNotification = false;
return;
}

isShowingNotification = true;
const { title, message, duration } = notificationQueue.shift();
showNotification(title, message, duration);

setTimeout(showNextNotification, duration + 300);
}

// 使用示例
queueNotification('通知1', '这是第一条通知');
queueNotification('通知2', '这是第二条通知');
queueNotification('通知3', '这是第三条通知');

4.3 响应式设计


为了确保通知在各种设备上都能正常显示,我们需要考虑响应式设计:


@media (max-width: 768px) {
#notification-container {
left: 20px;
right: 20px;
bottom: 20px;
}

.notification {
width: auto;
}
}

4.4 无障碍性考虑


为了提高通知的可访问性,我们可以添加 ARIA 属性和键盘操作支持:


function showNotification(title, message, duration = 5000) {
// ... 前面的代码相同

notification.setAttribute('role', 'alert');
notification.setAttribute('aria-live', 'polite');

const closeButton = document.createElement('button');
closeButton.textContent = '×';
closeButton.className = 'notification-close';
closeButton.setAttribute('aria-label', '关闭通知');

closeButton.addEventListener('click', () => {
notification.classList.remove('show');
setTimeout(() => {
container.removeChild(notification);
}, 300);
});

notification.appendChild(closeButton);

// ... 后面的代码相同
}

5. 性能优化与注意事项


在实现网页通知功能时,我们还需要注意以下几点:



  1. 防抖和节流:对于频繁触发的事件(如实时通知),使用防抖或节流技术可以有效减少不必要的通知显示。

  2. 内存管理:确保在移除通知时,同时清理相关的事件监听器和 DOM 元素,避免内存泄漏。

  3. 优雅降级:对于不支持 Web Notifications API 的浏览器,可以降级使用自定义的 CSS+JavaScript 方案。

  4. 用户体验:给用户提供控制通知显示的选项,如允许用户设置通知的类型、频率等。

  5. 安全考虑:在使用 Web Notifications API 时,确保只在 HTTPS 环境下请求权限,并尊重用户的权限设置。


网页通知是一个强大的功能,能够显著提升用户体验和网站的互动性。无论是使用 Web Notifications API 还是自定义的 CSS+JavaScript 方案,都能实现类似 QQ 那样的右下角消息通知。选择哪种方式取决于你的具体需求和目标用户群。通过合理使用通知功能,你可以让你的网站变得更加生动和用户友好。



作者:Syferie
来源:juejin.cn/post/7403283321793314850
收起阅读 »

url请求参数带有特殊字符“%、#、&”时,参数被截断怎么办?

web
是的,最近又踩坑了! 事情是这样的,我们测试小姐姐在一个全局搜索框里输入了一串特殊字符“%%%”,然后进行搜索,结果报错了。而输入常规字符,均可以正常搜索。 一排查,发现特殊字符“%%%”并未成功传给后端。 我们的这个全局搜索功能是需要跳转页面才能查看到搜索结...
继续阅读 »

是的,最近又踩坑了!


事情是这样的,我们测试小姐姐在一个全局搜索框里输入了一串特殊字符“%%%”,然后进行搜索,结果报错了。而输入常规字符,均可以正常搜索。


一排查,发现特殊字符“%%%”并未成功传给后端。


我们的这个全局搜索功能是需要跳转页面才能查看到搜索结果的。所以,搜索条件是作为参数拼接在页面url上的。


正常的传参:


image.png


当输入的是特殊字符“%、#、&”时,参数丢失


image.png


也就是说,当路由请求参数带有浏览器url中的特殊含义字符时,参数会被截断,无法正常获取参数。


那么怎么解决这个问题呢?


方案一:encodeURIComponent/decodeURIComponent


拼接参数时,利用encodeURIComponent()进行编码,接收参数时,利用decodeURIComponent()进行解码。


// 编码
this.$router.push({path: `/crm/global-search/search-result?type=${selectValue}&text=${encodeURIComponent(searchValue)}`});

// 解码
const text = decodeURIComponent(this.$route.query.text)

此方法对绝大多数特殊字符都适用,但是唯独输入“%”进行搜索时不行,报错如下。


image.png


所以在编码之前,还需进行一下如下转换:



this.$router.push({path: `/crm/global-search/search-result?type=${selectValue}&text=${encodeURIComponent(encodeSpecialChar(searchValue))}`});


/**
* @param {*} char 字符串
* @returns
*/

export const encodeSpecialChar = (char) => {
// #、&可以不用参与处理
const encodeArr = [{
code: '%',
encode: '%25'
},{
code: '#',
encode: '%23'
}, {
code: '&',
encode: '%26'
},]
return char.replace(/[%?#&=]/g, ($) => {
for (const k of encodeArr) {
if (k.code === $) {
return k.encode
}
}
})
}


方案二: qs.stringify()


默认情况下,qs.stringify()方法会使用encodeURIComponent方法对特殊字符进行编码,以保证URL的合法性。


const qs = require('qs');

const searchObj = {
type: selectValue,
text: searchValue
};
this.$router.push({path: `/crm/global-search/search-result?${qs.stringify(searchObj)}`});


使用了qs.stringify()方法,就无需使用encodeSpecialChar方法进行转换了。


作者:HED
来源:juejin.cn/post/7332048519156776979
收起阅读 »

前端代码重复度检测

web
在前端开发中,代码的重复度是一个常见的问题。重复的代码不仅增加了代码的维护成本,还可能导致程序的低效运行。为了解决这个问题,有许多工具和技术被用来检测和消除代码重复。其中一个被广泛使用的工具就是jscpd。 jscpd简介 jscpd是一款开源的JavaScr...
继续阅读 »

在前端开发中,代码的重复度是一个常见的问题。重复的代码不仅增加了代码的维护成本,还可能导致程序的低效运行。为了解决这个问题,有许多工具和技术被用来检测和消除代码重复。其中一个被广泛使用的工具就是jscpd


jscpd简介


jscpd是一款开源的JavaScript的工具库,用于检测代码重复的情况,针对复制粘贴的代码检测很有效果。它可以通过扫描源代码文件,分析其中的代码片段,并比较它们之间的相似性来检测代码的重复度。jscpd支持各种前端框架和语言,包括HTML、CSS和JavaScript等150种的源码文件格式。无论是原生的JavaScript、CSS、HTML代码,还是使用typescriptscssvuereact等代码,都能很好的检测出项目中的重复代码。


开源仓库地址:github.com/kucherenko/jscpd/tree/master


如何使用


使用jscpd进行代码重复度检测非常简单。我们需要安装jscpd。可以通过npmyarn来安装jscpd


npm install -g jscpd

yarn global add jscpd

安装完成后,我们可以在终端运行jscpd命令,指定要检测的代码目录或文件。例如,我们可以输入以下命令来检测当前目录下的所有JavaScript文件:


jscpd .

指定目录检测:


jscpd /path/to/code

在命令行执行成功后的效果如下图所示:



简要说明一下对应图中的字段内容:



  • Clone found (javascript):
    显示找到的重复代码块,这里是javascript文件。并且会显示重复代码在文件中具体的行数,便于查找。

  • Format:文件格式,这里是 javascript,还可以是 scss、markup 等。

  • Files analyzed:已分析的文件数量,统计被检测中的文件数量。

  • Total lines:所有文件的总行数。

  • Total tokens:所有的token数量,一行代码一般包含几个到几十个不等的token数量。

  • Clones found:找到的重复块数量。

  • Duplicated lines:重复的代码行数和占比。

  • Duplicated tokens:重复的token数量和占比。

  • Detection time:检测耗时。


工程配置


以上示例是比较简单直接检测单个文件或文件夹。当下主流的前端项目大多都是基于脚手架生成或包含相关前端工程化的文件,由于很多文件是辅助工具如依赖包、构建脚本、文档、配置文件等,这类文件都不需要检测,需要排除。这种情况下的工程一般使用配置文件的方式,通过选项配置规范 jscpd 的使用。


jscpd 的配置选项可以通过以下两种方式创建,增加的内容都一致无需区分对应的前端框架。


在项目根目录下创建配置文件 .jscpd.json,然后在该文件中增加具体的配置选项:


    {
"threshold": 0,
"reporters": ["html", "console", "badge"],
"ignore": ["**/__snapshots__/**"],
"absolute": true
}

也可直接在 package.json 文件中添加jscpd


    {
...
"jscpd": {
"threshold": 0.1,
"reporters": ["html", "console", "badge"],
"ignore": ["**/__snapshots__/**"],
"absolute": true,
"gitignore": true
}
...
}

简要介绍一下上述配置字段含义:



  • threshold:表示重复度的阈值,超过这个值,就会输出错误报警。如阈值设为 10,当重复度为18.1%时,会提示以下错误❌,但代码的检测会正常完成。


ERROR: jscpd found too many duplicates (18.1%) over threshold (10%)


  • reporters:表示生成结果检测报告的方式,一般有以下几种:

    • console:控制台打印输出

    • consoleFull:控制台完整打印重复代码块

    • json:输出 json 格式的报告

    • xml:输出 xml 格式的报告

    • csv:输出 csv 格式的报告

    • markdown:输出带有 markdown 格式的报告

    • html:生成html报告到html文件夹

    • verbose:输出大量调试信息到控制台



  • ignore:检测忽略的文件或文件目录,过滤一些非业务代码,如依赖包、文档或静态文件等

  • format:需要进行重复度检测的源代码格式,目前支持150多种,我们常用的如 javascript、typescript、css 等

  • absolute:在检测报告中使用绝对路径


除此之外还有很多其他的配置,有兴趣的可以看源码文档中有详细的介绍。


检测报告


完成以上jscpd配置后执行以下命令即可输出对应的重复检测报告。运行完毕后,jscpd会生成一个报告,展示每个重复代码片段的信息。报告中包含了重复代码的位置、相似性百分比和代码行数等详细信息。通过这些信息,我们可以有针对性的进行代码重构。


jscpd ./src -o 'report'

项目中的业务代码通常会选择放在 ./src 目录下,所以可以直接检测该目录下的文件,如果是放在其他目录下根据实际情况调整即可。
通过命令行参数-o 'report'输出检测报告到项目根目录下的 report 文件夹中,这里的report也可以自定义其他目录名称,输出的目录结构如下所示:



生成的报告页面如下所示:


项目概览数据:



具体重复代码的位置和行数:



默认检测重复代码的行数(5行)和tokens(50)比较小,所以产生的重复代码块可能比较多,在实际使用中可以针对检测范围进行设置,如下设置参数供参考:



  • 最小tokens:--min-tokens,简写 -k

  • 最小行数:--min-lines,简写 -l

  • 最大行数:--max-lines,简写 -x


jscpd ./src --min-tokens 200 --min-lines 20 -o 'report'

为了更便捷的使用此命令,可将这段命令集成到 package.json 中的 scripts 中,后续只需执行 npm run jscpd 即可执行检测。如下所示:


"scripts": {
...
"jscpd": "jscpd ./src --min-tokens 200 --min-lines 20 -o 'report'",
...
}

忽略代码块


上面所提到的ignore可以忽略某个文件或文件夹,还有一种忽略方式是忽略文件中的某一块代码。由于一些重复代码在实际情况中是必要的,可以使用代码注释标识的方式忽略检测,在代码的首尾位置添加注释,jscpd:ignore-startjscpd:ignore-end 包裹代码即可。


在js代码中使用方式:


/* jscpd:ignore-start */
import lodash from 'lodash';
import React from 'react';
import {User} from './models';
import {UserService} from './services';
/* jscpd:ignore-end */

在CSS和各种预处理中与js中的用法一致:


/* jscpd:ignore-start */
.style {
padding: 40px 0;
font-size: 26px;
font-weight: 400;
color: #464646;
line-height: 26px;
}
/* jscpd:ignore-end */

在html代码中使用方式:


<!--
// jscpd:ignore-start
-->

<meta data-react-helmet="true" name="theme-color" content="#cb3837"/>
<link data-react-helmet="true" rel="stylesheet" href="https://static.npmjs.com/103af5b8a2b3c971cba419755f3a67bc.css"/>
<link data-react-helmet="true" rel="apple-touch-icon" sizes="120x120" href="https://static.npmjs.com/58a19602036db1daee0d7863c94673a4.png"/>
<link data-react-helmet="true" rel="icon" type="image/png" href="https://static.npmjs.com/b0f1a8318363185cc2ea6a40ac23eeb2.png" sizes="32x32"/>
<!--
// jscpd:ignore-end
-->


总结


jscpd是一款强大的前端本地代码重复度检测工具。它可以帮助开发者快速发现代码重复问题,简单的配置即可输出直观的代码重复数据,通过解决重复的代码提高代码的质量和可维护性。


使用jscpd我们可以有效地优化前端开发过程,提高代码的效率和性能。希望本文能够对你了解基于jscpd的前端本地代码重复度检测有所帮助。




看完本文如果觉得有用,记得点个赞支持,收藏起来说不定哪天就用上啦~


专注前端开发,分享前端相关技术干货,公众号:南城大前端(ID: nanchengfe)


作者:南城FE
来源:juejin.cn/post/7288699185981095988
收起阅读 »

2种纯前端换肤方案

web
前言 换肤功能是一项普遍的需求,尤其是在夜晚,用户更倾向于使用暗黑模式。在我负责的公司项目中,每个项目都有换肤功能的需求。 过去,我主要使用 SCSS 变量,并利用其提供的函数,如 @each、map-get来实现换肤功能。但因其使用成本高,只能适用于SCSS...
继续阅读 »

前言


换肤功能是一项普遍的需求,尤其是在夜晚,用户更倾向于使用暗黑模式。在我负责的公司项目中,每个项目都有换肤功能的需求。


过去,我主要使用 SCSS 变量,并利用其提供的函数,如 @eachmap-get来实现换肤功能。但因其使用成本高,只能适用于SCSS项目,于是后来我改用 CSS 变量来实现换肤。这样无论是基于 LESS 的 React 项目,还是基于 SCSS 的 Vue 项目,都能应用换肤功能。并且使用时只需调用var函数,降低了使用成本。


Demo地址:github.com/cwjbjy/vite…


1. 一键换肤


1. 前置知识


CSS变量:声明自定义CSS属性,它包含的值可以在整个文档中重复使用。属性名需要以两个减号(--)开始,属性值则可以是任何有效的 CSS 值


--fontColor:'#fff'

Var函数:用于使用CSS变量。第一个参数为CSS变量名称,第二个可选参数作为默认值


color: var(--fontColor);

CSS属性选择器:匹配具有特定属性或属性值的元素。例如[data-theme='black'],将选择所有 data-theme 属性值为 'black' 的元素


2. 定义主题色


1. 新建src/assets/theme/theme-default.css


这里定义字体颜色与布局的背景色,更多CSS变量可根据项目的需求来定义


[data-theme='default'] {
/* 字体 */
--font-primary: #fff;
--font-highlight: #434a50;
/* 布局 */
--background-header: #2f3542;
--background-aside: #545c64;
--background-main: #0678be;
}

2. 新建src/assets/theme/theme-black.css


再定义一套暗黑主题色


[data-theme='black'] {
/* 字体 */
--font-primary: #fff;
--font-highlight: #434a50;
/* 布局 */
--background-header: #303030;
--background-aside: #303030;
--background-main: #393939;
}

3. 新建src/assets/theme/index.css


在index.css文件中导出全部主题色


@import './theme-default.css'; 
@import './theme-black.css';

4. 引入全局样式


在入口文件引入样式,比如我这里是main.tsx


import '@/assets/styles/theme/index.css';

3. 在html标签上增加自定义属性


修改index.html,在html标签上增加自定义属性data-theme


<html lang="en" data-theme="default"></html>

这里使用data-theme是为了被CSS属性选择器[data-theme='default']选中,也可更换为其他自定义属性,只需与CSS属性选择器对应上即可。


4. 修改CSS主题色


关键点:监听change事件,使用document.documentElement.setAttribute动态修改data-theme属性,然后CSS属性选择器将自动选择对应的css变量


<template>
<div>
<select name="pets" @change="handleChange">
<option value="default">默认色</option>
<option value="black">黑色</option>
</select>
<div>登录页面</div>
</div>

</template>

<script setup lang="ts">
const handleChange = (e: Event) => {
window.document.documentElement.setAttribute('data-theme', (e.target as HTMLSelectElement).value);
};
</script>


<style lang="scss">
body {
color: var(--font-primary);
background-color: var(--background-main);
}
</style>


效果图,默认色:


1708935487468.png


效果图,暗黑色:


1708935536950.png


5. 修改JS主题色


切换主题色,除了需要修改css样式,有时也需在js文件中修改样式,例如修改echarts的配置文件,来改变柱状图、饼图等的颜色。


1. 新建src/config/theme.js


定义图像的颜色,这里定义字体的颜色,默认情况下字体为黑色,暗黑模式下,字体为白色


const themeColor = {
default: {
font: '#333',
},
black: {
font: '#fff',
},
};

export default themeColor;

2. 修改vue文件


关键点:



  1. 定义主题色TS类型,规定默认和暗黑两种:type ThemeTypes = 'default' | 'black';

  2. 定义theme响应式变量,用来记录当前主题色:const theme = ref<ThemeTypes>('default');

  3. 监听change事件,将选中的值赋给theme:theme.value = selectTheme;

  4. 使用watch进行监听,如果theme改变,则重新绘制echarts图形


完整的vue文件:


<template>
<div>
<select name="pets" @change="handleChange">
<option value="default">默认色</option>
<option value="black">黑色</option>
</select>
<div>登录页面</div>
<div ref="echartRef" class="myChart"></div>
</div>

</template>

<script setup lang="ts">
import { onMounted, ref, watch } from 'vue';
import themeColor from '@/config/theme';
import * as echarts from 'echarts';

type ThemeTypes = 'default' | 'black';

const echartRef = ref<HTMLDivElement | null>(null);
const theme = ref<ThemeTypes>('default');
const handleChange = (e: Event) => {
const selectTheme = (e.target as HTMLSelectElement).value as ThemeTypes;
theme.value = selectTheme;
window.document.documentElement.setAttribute('data-theme', selectTheme);
};

const drawGraph = () => {
let echartsInstance = echarts.getInstanceByDom(echartRef.value!);
if (!echartsInstance) {
echartsInstance = echarts.init(echartRef.value);
}
echartsInstance.clear();
var option = {
color: ['#3398DB'],
title: {
text: '柱状图',
left: 'center',
textStyle: {
color: themeColor[theme.value].font,
},
},
xAxis: [
{
type: 'category',
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
axisLabel: {
show: true,
color: themeColor[theme.value].font,
},
nameTextStyle: {
color: themeColor[theme.value].font,
},
},
],
yAxis: [
{
type: 'value',
axisLabel: {
show: true,
color: themeColor[theme.value].font,
},
nameTextStyle: {
color: themeColor[theme.value].font,
},
},
],
series: [
{
name: '直接访问',
type: 'bar',
barWidth: '60%',
data: [10, 52, 200, 334, 390, 330, 220],
},
],
};

echartsInstance.setOption(option);
};
onMounted(() => {
drawGraph();
});
watch(theme, () => {
drawGraph();
});
</script>

<style lang="scss">
body {
color: var(--font-primary);
background-color: var(--background-main);
}
.myChart {
width: 300px;
height: 300px;
}
</style>

2. 一键变灰


在特殊的日子里,网页有整体变灰色的需求。可以使用filter 的 grayscale() 改变图像灰度,值在 0% 到 100% 之间,值为0%展示原图,值为100% 则完全转为灰度图像


body {
filter: grayscale(1); //1相当于100%
}

结尾


本文只是介绍大概的思路,更多的功能可根据业务增加。例如将主题色theme存储到pinia上,应用到全局上;将主题色存储到localStorage上,在页面刷新时,防止主题色恢复默认。


本文可结合以下文章阅读:



如果有更多的换肤方案,欢迎在留言区留言讨论。我会根据留言区内容实时更新。


作者:敲代码的彭于晏
来源:juejin.cn/post/7342527074526019620
收起阅读 »

实现一个支持@的输入框

web
近期产品期望在后台发布帖子或视频时,需要添加 @用户 的功能,以便用户收到通知,例如“xxx在xxx提及了您!”。然而,现有的开源库未能满足我们的需求,例如 ant-design 的 Mentions 组件: 但是不难发现跟微信飞书对比下,有两个细节没有处...
继续阅读 »

近期产品期望在后台发布帖子或视频时,需要添加 @用户 的功能,以便用户收到通知,例如“xxx在xxx提及了您!”。然而,现有的开源库未能满足我们的需求,例如 ant-design 的 Mentions 组件:



20240415161851.gif


但是不难发现跟微信飞书对比下,有两个细节没有处理。



  1. @用户没有高亮

  2. 在删除时没有当做一个整体去删除,而是单个字母删除,首先不谈用户是否想要整体删除,在这块有个模糊查询的功能,如果每删一个字母之后去调接口查询数据库造成一些不必要的性能开销,哪怕加上防抖。


然后也是找了其他的库都没达到产品的期望效果,那么好,自己实现一个,先看看最终实现的效果


6ND88RssMr.gif


封装之后使用:


<AtInput
height={150}
onRequest={async (searchStr) => {
const { data } = await UserFindAll({ nickname: searchStr });
return data?.list?.map((v) => ({
id: v.uid,
name: v.nickname,
wechatAvatarUrl: v.wechatAvatarUrl,
}));
}}
onChange={(content, selected) => {
setAtUsers(selected);
}}
/>

那么实现这么一个输入框大概有以下几个点:



  1. 高亮效果

  2. 删除/选中用户时需要整体删除

  3. 监听@的位置,复制给弹框的坐标,联动效果

  4. 最后我需要拿到文本内容,并且需要拿到@那些用户,去做表单提交


大多数文本输入框我们会使用input,或者textarea,很明显以上1,2两点实现不了,antd也是使用的textarea,所以也是没有实现这两个效果。所以这块使用富文本编辑,设置contentEditable,将其变为可编辑去做。输入框以及选择器的dom就如下:


 <div style={{ height, position: 'relative' }}>
{/* 编辑器 */}
<div
id="atInput"
ref={atRef}
className={'editorDiv'}
contentEditable
onInput={editorChange}
onClick={editorClick}
/>

{/* 选择用户框 */}
<SelectUser
options={options}
visible={visible}
cursorPosition={cursorPosition}
onSelect={onSelect}
/>

</div>

实现思路:



  1. 监听输入@,唤起选择框。

  2. 截取@xxx的xxx作为搜素的关键字去查询接口

  3. 选择用户后需要将原先输入的 @xxx 替换成 @姓名,并且将@的用户缓存起来

  4. 选择文本框中的姓名时需要变为整体选中状态,这块依然可以给标签设置为不可编辑状态就可实现,contentEditable=false,即可实现整体删除,在删除的同时需要将当前用户从之前缓存的@过的用户数组删除

  5. 那么可以拿到输入框的文本,@的用户, 最后将数据抛给父组件就完成了


以上提到了监听@文本变化,通常绑定onChange事件就行,但是还有一种用户通过点击移动光标,这块需要绑定change,click两个时间,他们里边的逻辑基本一样,只需要额外处理点击选中输入框中用户时,整体选中g功能,那么代码如下:


    const onObserveInput = () => {
let cursorBeforeStr = '';
const selection: any = window.getSelection();
if (selection?.focusNode?.data) {
cursorBeforeStr = selection.focusNode?.data.slice(0, selection.focusOffset);
}
setFocusNode(selection.focusNode);
const lastAtIndex = cursorBeforeStr?.lastIndexOf('@');
setCurrentAtIdx(lastAtIndex);
if (lastAtIndex !== -1) {
getCursorPosition();
const searchStr = cursorBeforeStr.slice(lastAtIndex + 1);
if (!StringTools.isIncludeSpacesOrLineBreak(searchStr)) {
setSearchStr(searchStr);
fetchOptions(searchStr);
setVisible(true);
} else {
setVisible(false);
setSearchStr('');
}
} else {
setVisible(false);
}
};

const selectAtSpanTag = (target: Node) => {
window.getSelection()?.getRangeAt(0).selectNode(target);
};

const editorClick = async (event) => {
onObserveInput();
// 判断当前标签名是否为span 是的话选中当做一个整体
if (e.target.localName === 'span') {
selectAtSpanTag(e.target);
}
};

const editorChange = (event) => {
const { innerText } = event.target;
setContent(innerText);
onObserveInput();
};

每次点击或者文本改变时都会去调用onObserveInput,以上onObserveInput该方法中主要做了以下逻辑:




  1. 通过getSelection方法可以获取光标的偏移位置,那么可以截取光标之前的字符串,并且使用lastIndexOf从后向前查找最后一个“@”符号,并记录他的下标,那么有了【光标之前的字符串】,【@的下标】就可以拿到到@之后用于过滤用户的关键字,并将其缓存起来。

  2. 唤起选择器,并通过关键字去过滤用户。这块涉及到一个选择器的位置,直接使用window.getSelection()?.getRangeAt(0).getBoundingClientRect()去获取光标的位置拿到的是光标相对于窗口的坐标,直接用这个坐标会有问题,比如滚动条滚动时,这个选择器发生位置错乱,所以这块同时去拿输入框的坐标,去做一个相减,这样就可以实现选择器跟着@符号联动的效果。


 const getCursorPosition = () => {
// 坐标相对浏览器的坐标
const { x, y } = window.getSelection()?.getRangeAt(0).getBoundingClientRect() as any;
// 获取编辑器的坐标
const editorDom = window.document.querySelector('#atInput');
const { x: eX, y: eY } = editorDom?.getBoundingClientRect() as any;
// 光标所在位置
setCursorPosition({ x: x - eX, y: y - eY });
};

选择器弹出后,那么下面就到了选择用户之后的流程了,


 /**
* @param id 唯一的id 可以uid
* @param name 用户姓名
* @param color 回显颜色
* @returns
*/

const createAtSpanTag = (id: number | string, name: string, color = 'blue') => {
const ele = document.createElement('span');
ele.className = 'at-span';
ele.style.color = color;
ele.id = id.toString();
ele.contentEditable = 'false';
ele.innerText = `@${name}`;
return ele;
};

/**
* 选择用户时回调
*/

const onSelect = (item: Options) => {
const selection = window.getSelection();
const range = selection?.getRangeAt(0) as Range;
// 选中输入的 @关键字 -> @郑
range.setStart(focusNode as Node, currentAtIdx!);
range.setEnd(focusNode as Node, currentAtIdx! + 1 + searchStr.length);
// 删除输入的 @关键字
range.deleteContents();
// 创建元素节点
const atEle = createAtSpanTag(item.id, item.name);
// 插入元素节点
range.insertNode(atEle);
// 光标移动到末尾
range.collapse();
// 缓存已选中的用户
setSelected([...selected, item]);
// 选择用户后重新计算content
setContent(document.getElementById('atInput')?.innerText as string);
// 关闭弹框
setVisible(false);
// 输入框聚焦
atRef.current.focus();
};

选择用户的时候需要做的以下以下几点:



  1. 删除之前的@xxx字符

  2. 插入不可编辑的span标签

  3. 将当前选择的用户缓存起来

  4. 重新获取输入框的内容

  5. 关闭选择器

  6. 将输入框重新聚焦


最后


在选择的用户或者内容发生改变时将数据抛给父组件


 const getAttrIds = () => {
const spans = document.querySelectorAll('.at-span');
let ids = new Set();
spans.forEach((span) => ids.add(span.id));
return selected.filter((s) => ids.has(s.id));
};

/** @的用户列表发生改变时,将最新值暴露给父组件 */
useEffect(() => {
const selectUsers = getAttrIds();
onChange(content, selectUsers);
}, [selected, content]);

完整组件代码


输入框主要逻辑代码:


let timer: NodeJS.Timeout | null = null;

const AtInput = (props: AtInputProps) => {
const { height = 300, onRequest, onChange, value, onBlur } = props;
// 输入框的内容=innerText
const [content, setContent] = useState<string>('');
// 选择用户弹框
const [visible, setVisible] = useState<boolean>(false);
// 用户数据
const [options, setOptions] = useState<Options[]>([]);
// @的索引
const [currentAtIdx, setCurrentAtIdx] = useState<number>();
// 输入@之前的字符串
const [focusNode, setFocusNode] = useState<Node | string>();
// @后关键字 @郑 = 郑
const [searchStr, setSearchStr] = useState<string>('');
// 弹框的x,y轴的坐标
const [cursorPosition, setCursorPosition] = useState<Position>({
x: 0,
y: 0,
});
// 选择的用户
const [selected, setSelected] = useState<Options[]>([]);
const atRef = useRef<any>();

/** 获取选择器弹框坐标 */
const getCursorPosition = () => {
// 坐标相对浏览器的坐标
const { x, y } = window.getSelection()?.getRangeAt(0).getBoundingClientRect() as any;
// 获取编辑器的坐标
const editorDom = window.document.querySelector('#atInput');
const { x: eX, y: eY } = editorDom?.getBoundingClientRect() as any;
// 光标所在位置
setCursorPosition({ x: x - eX, y: y - eY });
};

/**获取用户下拉列表 */
const fetchOptions = (key?: string) => {
if (timer) {
clearTimeout(timer);
timer = null;
}
timer = setTimeout(async () => {
const _options = await onRequest(key);
setOptions(_options);
}, 500);
};

useEffect(() => {
fetchOptions();
// if (value) {
// /** 判断value中是否有at用户 */
// const atUsers: any = StringTools.filterUsers(value);
// setSelected(atUsers);
// atRef.current.innerHTML = value;
// setContent(value.replace(/<\/?.+?\/?>/g, '')); //全局匹配内html标签)
// }
}, []);

const onObserveInput = () => {
let cursorBeforeStr = '';
const selection: any = window.getSelection();
if (selection?.focusNode?.data) {
cursorBeforeStr = selection.focusNode?.data.slice(0, selection.focusOffset);
}
setFocusNode(selection.focusNode);
const lastAtIndex = cursorBeforeStr?.lastIndexOf('@');
setCurrentAtIdx(lastAtIndex);
if (lastAtIndex !== -1) {
getCursorPosition();
const searchStr = cursorBeforeStr.slice(lastAtIndex + 1);
if (!StringTools.isIncludeSpacesOrLineBreak(searchStr)) {
setSearchStr(searchStr);
fetchOptions(searchStr);
setVisible(true);
} else {
setVisible(false);
setSearchStr('');
}
} else {
setVisible(false);
}
};

const selectAtSpanTag = (target: Node) => {
window.getSelection()?.getRangeAt(0).selectNode(target);
};

const editorClick = async (e?: any) => {
onObserveInput();
// 判断当前标签名是否为span 是的话选中当做一个整体
if (e.target.localName === 'span') {
selectAtSpanTag(e.target);
}
};

const editorChange = (event: any) => {
const { innerText } = event.target;
setContent(innerText);
onObserveInput();
};

/**
* @param id 唯一的id 可以uid
* @param name 用户姓名
* @param color 回显颜色
* @returns
*/

const createAtSpanTag = (id: number | string, name: string, color = 'blue') => {
const ele = document.createElement('span');
ele.className = 'at-span';
ele.style.color = color;
ele.id = id.toString();
ele.contentEditable = 'false';
ele.innerText = `@${name}`;
return ele;
};

/**
* 选择用户时回调
*/

const onSelect = (item: Options) => {
const selection = window.getSelection();
const range = selection?.getRangeAt(0) as Range;
// 选中输入的 @关键字 -> @郑
range.setStart(focusNode as Node, currentAtIdx!);
range.setEnd(focusNode as Node, currentAtIdx! + 1 + searchStr.length);
// 删除输入的 @关键字
range.deleteContents();
// 创建元素节点
const atEle = createAtSpanTag(item.id, item.name);
// 插入元素节点
range.insertNode(atEle);
// 光标移动到末尾
range.collapse();
// 缓存已选中的用户
setSelected([...selected, item]);
// 选择用户后重新计算content
setContent(document.getElementById('atInput')?.innerText as string);
// 关闭弹框
setVisible(false);
// 输入框聚焦
atRef.current.focus();
};

const getAttrIds = () => {
const spans = document.querySelectorAll('.at-span');
let ids = new Set();
spans.forEach((span) => ids.add(span.id));
return selected.filter((s) => ids.has(s.id));
};

/** @的用户列表发生改变时,将最新值暴露给父组件 */
useEffect(() => {
const selectUsers = getAttrIds();
onChange(content, selectUsers);
}, [selected, content]);

return (
<div style={{ height, position: 'relative' }}>
{/* 编辑器 */}
<div id="atInput" ref={atRef} className={'editorDiv'} contentEditable onInput={editorChange} onClick={editorClick} />
{/* 选择用户框 */}
<SelectUser options={options} visible={visible} cursorPosition={cursorPosition} onSelect={onSelect} />
</div>

);
};

选择器代码


const SelectUser = React.memo((props: SelectComProps) => {
const { options, visible, cursorPosition, onSelect } = props;

const { x, y } = cursorPosition;

return (
<div
className={'selectWrap'}
style={{
display: `${visible ? 'block' : 'none'}`,
position: 'absolute',
left: x,
top: y + 20,
}}
>

<ul>
{options.map((item) => {
return (
<li
key={item.id}
onClick={() =>
{
onSelect(item);
}}
>
<img src={item.wechatAvatarUrl} alt="" />
<span>{item.name}</span>
</li>
);
})}
</ul>
</div>

);
});
export default SelectUser;

以上就是实现一个支持@用户的输入框功能,就目前而言,比较死板,不支持自定义颜色,自定义选择器等等,未来,可以进一步扩展功能,例如添加@用户的高亮样式定制、支持键盘快捷键操作等,从而提升用户体验和功能性。


作者:tech_zjf
来源:juejin.cn/post/7357917741909819407
收起阅读 »

Vue.js 自动路由:告别手动配置,让开发更轻松!

web
在使用 Vue.js 开发项目的时候,我最头疼的就是创建路由,尤其是项目越来越大的时候,管理 route.ts 或 route.js 文件简直是一场噩梦! 我曾经做过一个页面超级多的项目,每新增一个页面就要更新路由,删除页面也要更新路由文件,不然就会报错,真是...
继续阅读 »

在使用 Vue.js 开发项目的时候,我最头疼的就是创建路由,尤其是项目越来越大的时候,管理 route.tsroute.js 文件简直是一场噩梦!


我曾经做过一个页面超级多的项目,每新增一个页面就要更新路由,删除页面也要更新路由文件,不然就会报错,真是烦死人了!


所以,我开始寻找自动生成路由的方法。我在网上搜了很久,但大部分结果都是针对 Webpack 和 Vue 2 的,很难找到适合我的方案。最后,我在 Vue 的 GitHub 仓库的讨论区里提问,终于找到了答案!


那就是 Unplugin Vue Router! 它可以为 Vue 3 实现基于文件的自动路由,而且支持 TypeScript,设置起来也超级简单! 虽然官方说它还在实验阶段,但用起来已经很方便了。


创建项目,安装插件


首先,我们创建一个新的 Vue 项目。 相信大家都很熟悉用 Vue CLI 创建项目了,这里就不赘述了,不熟悉的小伙伴可以去看看 Vue.js 官网的快速入门指南。


pnpm create vue@latest 

我创建项目的时候选择了 TypeScript 和 Vue Router,这样它就会自动生成一些页面和路由。


然后,进入项目目录,安装依赖。我最近开始用 pnpm 来管理依赖,感觉还不错。


pnpm add -D unplugin-vue-router 

接下来,更新 vite.config.ts 文件, 注意要把插件放在第 0 个位置


import { fileURLToPath, URL } from "node:url";
import VueRouter from "unplugin-vue-router/vite";
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";

// https://vitejs.dev/config/
export default defineConfig({
plugins: [
VueRouter({
/* options */
}),
// ⚠️ Vue must be placed after VueRouter()
vue(),
],
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)),
},
},

});

然后,更新 env.d.ts 文件,让编辑器能够识别插件的类型。


/// <reference types="vite/client" />
/// <reference types="unplugin-vue-router/client" />

最后,更新路由文件 src/router/index.ts


import { createRouter, createWebHistory } from "vue-router";
import { routes, handleHotUpdate } from "vue-router/auto-routes";

const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes,
});

if (import.meta.hot) {
handleHotUpdate(router);
}

export default router;

创建页面,自动生成路由


现在,我们可以创建 src/pages 目录了,在这个目录下创建的 Vue 组件会自动变成路由和页面,就像 Nuxt 一样方便!


我们先在 src\pages\about.vue 创建一个关于页面:


<template>
    <div>This is the about page</div>
</template>

然后在 src\pages\index.vue 创建首页:


<template>
    <div>This is Home Page</div>
</template>

运行 pnpm dev 启动开发服务器,点击 “Home” 链接就会跳转到首页,点击 “About” 链接就会跳转到关于页面。


怎么样,是不是很方便? 如果你不熟悉路由文件夹结构,可以看看这个文档: uvr.esm.is/guide/file-…


动态路由


我们再来试试创建带参数的动态路由。在 src/pages/blog/[id].vue 创建一个组件,内容如下:


<script setup>
const { id } = useRoute().params;
</script>
<template>
    <div>This is the blog post with id: {{ id }}</div>
</template>

再次运行 pnpm dev ,然后访问 http://localhost:5173/blog/6 ,你就会看到以下内容:


vuejs 自动路由


是不是很神奇? 希望这篇简短的博客能帮助你在 Vue.js 的旅程中更轻松地创建路由!


作者:前端宝哥
来源:juejin.cn/post/7401354593588199465
收起阅读 »

JS类型判断的四种方法,你掌握了吗?

web
引言 JavaScript中有七种原始数据类型和几种引用数据类型,本文将清楚地介绍四种用于类型判断的方法,分别是typeOf、instanceOf、Object.prototype.toString.call()、Array.isArray(),并介绍其使用方...
继续阅读 »

引言


JavaScript中有七种原始数据类型和几种引用数据类型,本文将清楚地介绍四种用于类型判断的方法,分别是typeOfinstanceOfObject.prototype.toString.call()Array.isArray(),并介绍其使用方法和判定原理。


typeof



  1. 可以准确判断除null之外的所有原始类型,null会被判定成object

  2. function类型可以被准确判断为function,而其他所有引用类型都会被判定为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

let obj = {}
let arr = []
let fn = function() {}
let date = new Date()

console.log(typeof s); // string typeof后面有无括号都行
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

console.log(typeof(obj)); // object
console.log(typeof(arr)); // object
console.log(typeof(date)); // object

console.log(typeof(fn)); // function

判定原理


typeof是通过将值转换为二进制之后,判断其前三位是否为0:都是0则为object,反之则为原始类型。因为原始类型转二进制,前三位一定不都是0;反之引用类型被转换成二进制前三位一定都是0。


null是原始类型却被判定为object就是因为它在机器中是用一长串0来表示的,可以把这看作是一个史诗级的bug。


所以用typeof判断接收到的值是否为一个对象时,还要注意排除null的情况:


function isObject() {
if(typeof(o) === 'object' && o !== null){
return true
}
return false
}

你丢一个值给typeof,它会告诉你这个字值是什么类型,但是它无法准确告诉你这是一个Array或是Date,若想要如此精确地知道一个对象类型,可以用instanceof告诉你是否为某种特定的类型


instanceof


只能精确地判断引用类型,不能判断原始类型


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(s instanceof String);// false
console.log(n instanceof Number);// false
console.log(arr instanceof Object);// true

判定原理


instanceof既能把数组判定成Array,又能把数组判定成Object,究其原因是原型链的作用————顺着数组实例 arr 的隐式原型一直找到了 Object 的构造函数,看下面的代码:


arr.__proto__ = Array.prototype
Array.prototype.__proto__ = Object.prototype

所以我们就知道了,instanceof能准确判断出一个对象是否为某种类型,就是依靠对象的原型链来查找的,一层又一层地判断直到找到null为止。


手写instanceOf


根据这个原理,我们可以手写出一个instanceof:


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


对象的隐式原型 等于 构造函数的显式原型!可看文章 给我三分钟,带你完全理解JS原型和原型链前言



Object.prototype.toString.call()


可以判断任何数据类型


在浏览器上执行这三段代码,会得到'[object Object]''[object Array]''[object Number]'


var a = {}
Object.prototype.toString.call(a)

var a = {}
Object.prototype.toString.call(a)

var a = 123
Object.prototype.toString.call(a)

原型上的toString的内部逻辑


调用Object.prototype.toString的时候执行会以下步骤: 参考官方文档:带注释的 ES5



  1. 如果此值是undefined类型,则返回 ‘[object Undefined]’

  2. 如果此值是null类型,则返回 ‘[object Null]’

  3. 将 O 作为 ToObject(this) 的执行结果。toString执行过程中会调用一个ToObject方法,执行一个类似包装类的过程,我们访问不了这个方法,是JS自己用的

  4. 定义一个class作为内部属性[[class]]的值。toString可以读取到这个值并把这个值暴露出来让我们看得见

  5. 返回由 "[object"class"]" 组成的字符串


为什么结合call就能准确判断值类型了呢?


① 首先我们要知道Object.prototype.toString(xxx)往括号中不管传递什么返回结果都是'[object Object]',因为根据上面五个步骤来看,它内部会自动执行ToObject()方法,xxx会被执行一个类似包装类的过程然后转变成一个对象。所以单独一个Object.prototype.toString(xxx)不能用来判定值的类型


② 其次了解call方法的核心原理就是:比如foo.call(obj),利用隐式绑定的规则,让obj对象拥有foo这个函数的引用,从而让foo函数的this指向obj,执行完foo函数内部逻辑后,再将foo函数的引用从obj上删除掉。手搓一个call的源码就是这样的:


// call方法只允许被函数调用,所以它应该是放在Function构造函数的显式原型上的
Function.prototype.mycall = function(context) {
// 判断调用我的那个哥们是不是函数体
if (typeof this !== 'function') {
return new TypeError(this+ 'is not a function')
}

// this(函数)里面的this => context对象
const fn = Symbol('key') // 定义一个独一无二的fn,防止使用该源码时与其他fn产生冲突
context[fn] = this // 让对象拥有该函数 context={Symbol('key'): foo}
context[fn]() // 触发隐式绑定
delete context[fn]
}

③ 所以Object.prototype.toString.call(xxx)就相当于 xxx.toString(),把toString()方法放在了xxx对象上调用,这样就能精准给出xxx的对象类型



toString方法有几个版本:



  1. {}.toString() 得到由"[object" 和 class 和 "]" 组成的字符串

  2. [].toString() 数组的toString方法重写了对象上的toString方法,返回由数组内部元素以逗号拼接的字符串

  3. xx.toString() 返回字符串字面量,比如


let fn = function(){}; 
console.log( fn.toString() ) // "function () {}"


Array.isArray(x)


只能判断是否是数组,若传进去的x是数组,返回true,否则返回false


总结


typeOf:原始类型除了null都能准确判断,引用类型除了function能准确判断其他都不能。依靠值转为二进制后前三位是否为0来判断


instanceOf:只能把引用类型丢给它准确判断。顺着对象的隐式原型链向上比对,与构造函数的显式原型相等返回true,否则false


Object.prototype.toString.call():可以准确判断任何类型。要了解对象原型的toString()内部逻辑和call()的核心原理,二者结合才有精准判定的效果


Array.isArray():是数组则返回true,不是则返回false。判定范围最狭窄


作者:今天一定晴q
来源:juejin.cn/post/7403288145196580904
收起阅读 »

学TypeScript必然要了解declare

web
背景 declare关键字是为了服务TypeScript的。TypeScript是什么在这里就不多介绍了,但是我们要知道ts文件是需要TypeScript编译器转换为js文件才可以执行,并且在编译阶段就会进行类型检查。但是在TypeScript中并不支持js可...
继续阅读 »

背景


declare关键字是为了服务TypeScript的。TypeScript是什么在这里就不多介绍了,但是我们要知道ts文件是需要TypeScript编译器转换为js文件才可以执行,并且在编译阶段就会进行类型检查。但是在TypeScript中并不支持js可识别的所有类型,例如我们使用第三方库JQuery,我们通过一下方法获取一个id为‘foo’的标签元素。


$('#foo');
// or
jQuery('#foo');

然而在ts文件中,使用语法,语法,底下就会爆出一条红线提示到:Cannot find name '$'



因此,需要declare来声明,告诉TypeScript编译器该标识符已存在,通过编译时的检查并在开发时提供类型提示。


定义


在 TypeScript 中,declare关键字告诉编译器存在一个对象(并且可以在代码中引用)。它向 TypeScript 编译器声明该对象。简而言之,它允许开发人员使用在其他地方声明的对象。

注:编译器不会将declare语句编译为 JavaScript。对比下面两段代码:


// declare声明了一个名为 myGlobal 的全局变量,并指定其类型为 any。
// 该声明并不会生成真正的 JavaScript 代码,而只是告诉 TypeScript 编译器该变量存在。
declare var myGlobal: any;

// 给 myGlobal 赋值为 42。
myGlobal = 42;
console.log(myGlobal); // 42

// 直接声明了一个名为 myGlobal 的全局变量,并指定其类型为 any。这会生成真正的 JavaScript 代码。
var myGlobal: any;

// 给 myGlobal 赋值为 42。
myGlobal = 42;
console.log(myGlobal); // 42

使用



  • declare var 声明全局变量

  • declare function 声明全局方法

  • declare class 声明全局类

  • declare enum 声明全局枚举类型

  • declare namespace 声明(含有子属性的)全局对象

  • declare global 扩展全局变量

  • declare module 扩展模块


声明文件


通常,在使用第三方库或模块时,有两种方式引入声明文件:



  • 全局声明:如果第三方库或模块是全局可访问的,你可以在整个项目的任何地方直接使用它们,而无需显式导入。此时,你只需要确保在 TypeScript 项目中正确引入了相应的声明文件。一般情况下,TypeScript 会自动查找并加载全局声明文件。如果没有自动加载,你可以使用 /// 的方式在具体的源文件中将声明文件引入。

  • 模块导入:如果第三方库或模块是通过模块化方式提供的,你需要使用 import 语句将其导入到你的代码中,同时也需要确保相应的声明文件被正确引入。在这种情况下,你可以使用 import 或 require 来引入库,并且不需要显式地引入声明文件,因为 TypeScript 编译器会根据模块的导入语句自动查找和加载相应的声明文件。


有很多第三方库提供了声明文件,可以在packages.json文件中查看。types表示类型声明文件是哪一个。


image.png


可以使用 @types 统一管理第三方库的声明文件。@types 的使用方式很简单,直接用 npm 安装对应的声明模块即可,以 jQuery 举例:


npm install @types/jquery --save-dev

作者:用户483146118862
来源:juejin.cn/post/7402811318816702515
收起阅读 »

厉害了,不用js就能实现文字中间省略号

web
今天发现一个特别有意思的效果,并进行解析,就是标题的效果 参考链接 实现地址 CodePen 如果要实现这个功能,我想很多人第一时间想到的都是用js去计算dom容器和文字之间是否溢出吧?但今天带来一个用css实现的效果,不用自己计算,只需要寥寥几行(bu...
继续阅读 »

今天发现一个特别有意思的效果,并进行解析,就是标题的效果



参考链接



如果要实现这个功能,我想很多人第一时间想到的都是用js去计算dom容器和文字之间是否溢出吧?但今天带来一个用css实现的效果,不用自己计算,只需要寥寥几行(bushi)就可以实现让人头疼的文字中间省略号功能。




实现思路


1. 简单实现


在用css实现的时候我们不妨用这个思路想想,设置一个当前显示文字span伪元素的width为50%,浮动到当前span上面,并且设置direction: rtl; 显示右边文字,不就可以很简单的实现这个功能了?让我们试试:


<style>
.wrap {
width: 200px;
border: 1px solid white;
}
.test-title {
display: block;
color: white;
overflow: hidden;
height: 20px;
}
.test-title::before {
content: attr(title);
width: 50%;
float: right;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
direction: rtl;
}
</style>
<body>
<div class="wrap">
<span class="test-title" title="这是一行文字文字文字文字文字文字文字文字文字文字文字文字文字">
这是一行文字文字文字文字文字文字文字文字文字文字文字文字文字
</span>
</div>
</body>

Untitled


💡 此处应有图

2. 优化效果


在上面我们已经看到,其实效果我们已经实现了,现在文字中间已经有了省略号了!但是这其中其实有一个弊端不知道大家有没有发现,那就是文本不溢出的情况呢?伪元素是不是会一直显示在上面?这该怎么办?难道我们需要用js监听文本不溢出的情况然后手动隐藏吗?


Untitled


既然是用css来进行实现,那么我们当然不能用这种方式了。这里原作者用了一种很取巧,但也很好玩的一种方法,让我们来看看吧!


既然我们上面实现的是文本溢出的情况,那么当文本不溢出的时候我们直接显示文字不就行了?你可能想说:“这不是废话吗?但我现在不就是不知道怎么判断吗? ”。hhhhh对,那我们就要用css来想想,css该怎么判断呢?我就不卖关子了,让我们想想,我们给文本的容器添加一个固定宽度,那么当文本溢出的时候会发生什么呢?是不是会换行,高度变大呢,那么当我们设置两个文本元素,一个是正常样式,一个是我们上方的溢出样式。等文本不溢出没换行的时候,显示正常样式,当文本溢出高度变大的时候显示溢出样式可以吗?让我们试试吧


<!DOCTYPE html>
<html lang="en">
<head>
<style>
body {
background: #333;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
margin: 0;
font-size: 14px;
}
.wrap {
width: 300px;
background: #333;

/* 设置正常行高,并隐藏溢出画面 */
height: 2em;
overflow: hidden;
line-height: 2;
position: relative;
text-align: -webkit-match-parent;
resize: horizontal;
}

.normal-title {
/* 设置最大高度为双倍行高使其可以换行 */
display: block;
max-height: 4em;
}
.test-title {
position: relative;
top: -4em;
display: block;
color: white;
overflow: hidden;
height: 2em;
text-align: justify;
background: inherit;
overflow: hidden;
}
.test-title::before {
content: attr(title);
width: 50%;
float: right;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
direction: rtl;
}
</style>
</head>

<body>
<div class="wrap">
<span class="normal-title">这是一行文字文字文字文字文字文字文字文字文字文字文字文字文字</span>
<span class="test-title" title="这是一行文字文字文字文字文字文字文字文字文字文字文字文字文字">
这是一行文字文字文字文字文字文字文字文字文字文字文字文字文字
</span>
</div>
</body>

</html>

大家都试过了吧?那让我们来讲一下这段代码的实现:


实现方式:简单来说这段代码实现的就是一个覆盖的效果,normal-title元素平常是普通高度(1em),等到换行之后就会变成2em,那么我们的溢出样式test-title怎么实现的覆盖呢?这主要依赖于test-title的top属性,让我们这样子想,当normal-title高度为1em的时候,test-title的top为-2em,那么这时候因为wrap的hidden效果,所以test-title是看不到的。那么当normal-title的高度为2em的时候呢?test-title刚好就会覆盖到normal-title上面,所以我们刚好可以看到test-title的省略号效果。


这就是完整的实现过程和方式,css一些取巧的判断方式总会让我们大开眼界,不断学习,方得始终。


作者:一_个前端
来源:juejin.cn/post/7401812292211081226
收起阅读 »

Vue.js 自动路由:告别手动配置,让开发更轻松!

web
在使用 Vue.js 开发项目的时候,我最头疼的就是创建路由,尤其是项目越来越大的时候,管理 route.ts 或 route.js 文件简直是一场噩梦! 我曾经做过一个页面超级多的项目,每新增一个页面就要更新路由,删除页面也要更新路由文件,不然就会报错,真是...
继续阅读 »

在使用 Vue.js 开发项目的时候,我最头疼的就是创建路由,尤其是项目越来越大的时候,管理 route.tsroute.js 文件简直是一场噩梦!


我曾经做过一个页面超级多的项目,每新增一个页面就要更新路由,删除页面也要更新路由文件,不然就会报错,真是烦死人了!


所以,我开始寻找自动生成路由的方法。我在网上搜了很久,但大部分结果都是针对 Webpack 和 Vue 2 的,很难找到适合我的方案。最后,我在 Vue 的 GitHub 仓库的讨论区里提问,终于找到了答案!


那就是 Unplugin Vue Router! 它可以为 Vue 3 实现基于文件的自动路由,而且支持 TypeScript,设置起来也超级简单! 虽然官方说它还在实验阶段,但用起来已经很方便了。


创建项目,安装插件


首先,我们创建一个新的 Vue 项目。 相信大家都很熟悉用 Vue CLI 创建项目了,这里就不赘述了,不熟悉的小伙伴可以去看看 Vue.js 官网的快速入门指南。


pnpm create vue@latest 

我创建项目的时候选择了 TypeScript 和 Vue Router,这样它就会自动生成一些页面和路由。


然后,进入项目目录,安装依赖。我最近开始用 pnpm 来管理依赖,感觉还不错。


pnpm add -D unplugin-vue-router 

接下来,更新 vite.config.ts 文件, 注意要把插件放在第 0 个位置


import { fileURLToPath, URL } from "node:url";
import VueRouter from "unplugin-vue-router/vite";
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";

// https://vitejs.dev/config/
export default defineConfig({
plugins: [
VueRouter({
/* options */
}),
// ⚠️ Vue must be placed after VueRouter()
vue(),
],
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)),
},
},

});

然后,更新 env.d.ts 文件,让编辑器能够识别插件的类型。


/// <reference types="vite/client" />
/// <reference types="unplugin-vue-router/client" />

最后,更新路由文件 src/router/index.ts


import { createRouter, createWebHistory } from "vue-router";
import { routes, handleHotUpdate } from "vue-router/auto-routes";

const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes,
});

if (import.meta.hot) {
handleHotUpdate(router);
}

export default router;

创建页面,自动生成路由


现在,我们可以创建 src/pages 目录了,在这个目录下创建的 Vue 组件会自动变成路由和页面,就像 Nuxt 一样方便!


我们先在 src\pages\about.vue 创建一个关于页面:


<template>
    <div>This is the about page</div>
</template>

然后在 src\pages\index.vue 创建首页:


<template>
    <div>This is Home Page</div>
</template>

运行 pnpm dev 启动开发服务器,点击 “Home” 链接就会跳转到首页,点击 “About” 链接就会跳转到关于页面。


怎么样,是不是很方便? 如果你不熟悉路由文件夹结构,可以看看这个文档: uvr.esm.is/guide/file-…


动态路由


我们再来试试创建带参数的动态路由。在 src/pages/blog/[id].vue 创建一个组件,内容如下:


<script setup>
const { id } = useRoute().params;
</script>
<template>
    <div>This is the blog post with id: {{ id }}</div>
</template>

再次运行 pnpm dev ,然后访问 http://localhost:5173/blog/6 ,你就会看到以下内容:


vuejs 自动路由


是不是很神奇? 希望这篇简短的博客能帮助你在 Vue.js 的旅程中更轻松地创建路由!


作者:前端宝哥
来源:juejin.cn/post/7401354593588199465
收起阅读 »

学TypeScript必然要了解declare

web
背景 declare关键字是为了服务TypeScript的。TypeScript是什么在这里就不多介绍了,但是我们要知道ts文件是需要TypeScript编译器转换为js文件才可以执行,并且在编译阶段就会进行类型检查。但是在TypeScript中并不支持js可...
继续阅读 »

背景


declare关键字是为了服务TypeScript的。TypeScript是什么在这里就不多介绍了,但是我们要知道ts文件是需要TypeScript编译器转换为js文件才可以执行,并且在编译阶段就会进行类型检查。但是在TypeScript中并不支持js可识别的所有类型,例如我们使用第三方库JQuery,我们通过一下方法获取一个id为‘foo’的标签元素。


$('#foo');
// or
jQuery('#foo');

然而在ts文件中,使用语法,语法,底下就会爆出一条红线提示到:Cannot find name '$'



因此,需要declare来声明,告诉TypeScript编译器该标识符已存在,通过编译时的检查并在开发时提供类型提示。


定义


在 TypeScript 中,declare关键字告诉编译器存在一个对象(并且可以在代码中引用)。它向 TypeScript 编译器声明该对象。简而言之,它允许开发人员使用在其他地方声明的对象。

注:编译器不会将declare语句编译为 JavaScript。对比下面两段代码:


// declare声明了一个名为 myGlobal 的全局变量,并指定其类型为 any。
// 该声明并不会生成真正的 JavaScript 代码,而只是告诉 TypeScript 编译器该变量存在。
declare var myGlobal: any;

// 给 myGlobal 赋值为 42。
myGlobal = 42;
console.log(myGlobal); // 42

// 直接声明了一个名为 myGlobal 的全局变量,并指定其类型为 any。这会生成真正的 JavaScript 代码。
var myGlobal: any;

// 给 myGlobal 赋值为 42。
myGlobal = 42;
console.log(myGlobal); // 42

使用



  • declare var 声明全局变量

  • declare function 声明全局方法

  • declare class 声明全局类

  • declare enum 声明全局枚举类型

  • declare namespace 声明(含有子属性的)全局对象

  • declare global 扩展全局变量

  • declare module 扩展模块


声明文件


通常,在使用第三方库或模块时,有两种方式引入声明文件:



  • 全局声明:如果第三方库或模块是全局可访问的,你可以在整个项目的任何地方直接使用它们,而无需显式导入。此时,你只需要确保在 TypeScript 项目中正确引入了相应的声明文件。一般情况下,TypeScript 会自动查找并加载全局声明文件。如果没有自动加载,你可以使用 /// 的方式在具体的源文件中将声明文件引入。

  • 模块导入:如果第三方库或模块是通过模块化方式提供的,你需要使用 import 语句将其导入到你的代码中,同时也需要确保相应的声明文件被正确引入。在这种情况下,你可以使用 import 或 require 来引入库,并且不需要显式地引入声明文件,因为 TypeScript 编译器会根据模块的导入语句自动查找和加载相应的声明文件。


有很多第三方库提供了声明文件,可以在packages.json文件中查看。types表示类型声明文件是哪一个。


image.png


可以使用 @types 统一管理第三方库的声明文件。@types 的使用方式很简单,直接用 npm 安装对应的声明模块即可,以 jQuery 举例:


npm install @types/jquery --save-dev

作者:用户483146118862
来源:juejin.cn/post/7402811318816702515
收起阅读 »

还在用&nbsp;来当作空格?别忽视他对样式的影响!

web
许久没有更新博客了,今天就抽空来分享下之前遇到个有意思的现象~ 奇怪的现象,被换行的单词 在一次新需求完工之后,进行国际化样式优化时,我发现了一个奇怪的现象:即使页面元素有word-wrap:break-word; 样式属性,单词也照样会被直接裁断换行。 这...
继续阅读 »

许久没有更新博客了,今天就抽空来分享下之前遇到个有意思的现象~


奇怪的现象,被换行的单词


在一次新需求完工之后,进行国际化样式优化时,我发现了一个奇怪的现象:即使页面元素有word-wrap:break-word; 样式属性,单词也照样会被直接裁断换行。


image.png


这又是为什么嘞?细细分析页面元素,突然发现或许这与之前的踩过的坑:特殊的不换行空格有关?!


来复现吧!


那我们马上就来试一试!


  <style>
.normal_style{
width:70px;
height:200px;
margin-right:150px;
border:1px solid red;
/* 👇表示 如果一个单词超出行长度,要截取换行,其他默认;👇 */
word-wrap:break-word;
}
</style>

<div style="display:flex;">
<div class='normal_style'>This is a long a long sentence</div>
<div class='normal_style'>This&nbsp;is&nbsp;a&nbsp;long&nbsp;a&nbsp;long&nbsp;sentence</div>
</div>

image.png


很明显,单词直接被强行换行拆分了!


那会不会是页面解析的时候,把 &nbsp; 连同其他单词一起,当作一长串单词来处理了,所以才不换行的嘞?


你知道空格转义符有几种写法吗?


那我们就再来试试!不使用 &nbsp; 转而使用其他空格转义符呢?


其实除了 &nbsp; ,还有其他很多种空格转义符。


1. 半角空格


&ensp;

它才是典型的“半角空格”,全称是En Space,en是字体排印学的计量单位,为em宽度的一半。根据定义,它等同于字体度的一半(如16px字体中就是8px)。名义上是小写字母n的宽度。此空格传承空格家族一贯的特性:透明的,此空格有个相当稳健的特性,就是其占据的宽度正好是1/2个中文宽度,而且基本上不受字体影响。


2. 全角空格


&emsp;

从这个符号到下面, 我们就很少见到了, 它叫“全角空格”,全称是Em Space,em是字体排印学的计量单位,相当于当前指定的点数。例如,1 em在16px的字体中就是16px。此空格也传承空格家族一贯的特性:透明的,此空格也有个相当稳健的特性,就是其占据的宽度正好是1个中文宽度,而且基本上不受字体影响。


3. 窄空格


&thinsp;

窄空格,全称是Thin Space。我们不妨称之为“瘦弱空格”,就是该空格长得比较瘦弱,身体单薄,占据的宽度比较小。它是em之六分之一宽。


4. 零宽不连字


&zwnj;

它叫零宽不连字,全称是Zero Width Non Joiner,简称“ZWNJ”,是一个不打印字符,放在电子文本的两个字符之间,抑制本来会发生的连字,而是以这两个字符原本的字形来绘制。Unicode中的零宽不连字字符映射为“”(zero width non-joiner,U+200C),HTML字符值引用为: ‌


5. 零宽连字


&zwj;

它叫零宽连字,全称是Zero Width Joiner,简称“ZWJ”,是一个不打印字符,放在某些需要复杂排版语言(如阿拉伯语、印地语)的两个字符之间,使得这两个本不会发生连字的字符产生了连字效果。零宽连字符的Unicode码位是U+200D (HTML: ‍ ‍)。


再次尝试复现-&thinsp;


  <style>
.normal_style{
width:70px;
height:200px;
margin-right:150px;
border:1px solid red;
/* 👇表示 如果一个单词超出行长度,要截取换行,其他默认;👇 */
word-wrap:break-word;
}
</style>

<div style="display:flex;">
<div class='normal_style'>This is a long a long sentence</div>
<div class='normal_style'>This&nbsp;is&nbsp;a&nbsp;long&nbsp;a&nbsp;long&nbsp;sentence</div>
<div class='normal_style'>This&thinsp;is&thinsp;a&thinsp;long&thinsp;a&thinsp;long&thinsp;sentence</div>
</div>

image.png


我们可以看到 &thinsp; 进行转义的话,单词截取换行是正常的!所以,真凶就是 &nbsp; 特殊的不换行空格!


如何修订?


因为这个提示框是使用公司自制的 UI 组件实现的,而之所以使用 &nbsp; 进行转义是为了修订XSS注入。(对,这个老东西现在没人维护,还是我去啃源码加上的,使用了公共的转义方法)。最后就简单去修改这个公共方法吧!使用了最贴近 &nbsp; 宽度的空格转义符:&thinsp;


作者:bachelor98
来源:juejin.cn/post/7403953367766859828
收起阅读 »

数据大屏的解决方案

web
1. 使用缩放比例适配各种设备(适用16*9比例的屏幕分辨率) 封装一个获取缩放比例的工具函数 /** * 大屏效果需要满足16:9的屏幕比例,才能达到完美的大屏适配效果 * 其他比例的大屏效果,不能铺满整个屏幕 * @param {*} w 设备宽度...
继续阅读 »

1. 使用缩放比例适配各种设备(适用16*9比例的屏幕分辨率)



  1. 封装一个获取缩放比例的工具函数


    /**
    * 大屏效果需要满足16:9的屏幕比例,才能达到完美的大屏适配效果
    * 其他比例的大屏效果,不能铺满整个屏幕
    * @param {*} w 设备宽度 默认 1920
    * @param {*} h 设备高度 默认 1080
    * @returns 返回值是缩放比例
    */

    export function getScale(w = 1920, h = 1080) {
    const ww = window.innerWidth / w
    const wh = window.innerHeight / h
    return ww < wh ? ww : wh
    }


  2. vue中使用方案如下


    <template>
    <div class="full-screen-container">
    <div id="screen">
    大屏展示的内容
    </div>
    </div>
    </template>
    <script>
    import { getScale } from "@/utils/tool";
    import screenfull from "screenfull";
    export default {
    name: "cockpit",
    mounted() {
    if (screenfull && screenfull.enabled && !screenfull.isFullscreen) {
    screenfull.request();
    }
    this.setFullScreen();
    },
    methods: {
    setFullScreen() {
    const screenNode = document.getElementById("screen");
    // 非标准设备(笔记本小于1920,如:1366*768、mac 1432*896)
    if (window.innerWidth < 1920) {
    screenNode.style.left = "50%";
    screenNode.style.transform = `scale(${getScale()}) translate(-50%, 0)`;
    } else if (window.innerWidth === 1920) {
    // 标准设备 1920 * 1080
    screenNode.style.left = 0;
    screenNode.style.transform = `scale(1) translate(0, 0)`;
    } else {
    // 大屏设备(4K 2560*1600)
    screenNode.style.left = "50%";
    screenNode.style.transform = `scale(${getScale()}) translate(-50%, 0)`;
    }
    // 监听视口变化
    window.addEventListener("resize", () => {
    if (window.innerWidth === 1920) {
    screenNode.style.left = 0;
    screenNode.style.transform = `scale(1) translate(0, 0)`;
    } else {
    screenNode.style.left = "50%";
    screenNode.style.transform = `scale(${getScale()}) translate(-50%, 0)`;
    }
    });
    },
    },
    };
    </script>
    <style lang="scss">
    .full-screen-container {
    width: 100vw;
    height: 100vh;
    display: flex;
    flex-direction: column;
    background-color: #131a2b;
    #screen {
    position: fixed;
    width: 1920px;
    height: 1080px;
    top: 0;
    transform-origin: left top;
    color: #fff;
    }
    }
    </style>


  3. mac设备上的屏幕分辨率,在适配的时候,可能不是那么完美,以短边缩放为准,所以宽度到达百分之百后,高度不会铺满



    1. 1432*896 13寸mac本

    2. 2560*1600 4k屏幕




2. 使用第三方插件来实现数据大屏(mac设备会产生布局错落)



  1. 建议在全屏容器内使用百分比搭配flex进行布局,以便于在不同的分辨率下得到较为一致的展示效果。

  2. 使用前请注意将bodymargin设为0,否则会引起计算误差,全屏后不能完全充满屏幕。

  3. 使用方式


    1. npm install @jiaminghi/data-view
    2. yarn add @jiaminghi/data-view

    // 在vue项目中的main.js入口文件,将自动注册所有组件为全局组件
    import {fullScreenContainer} from '@jiaminghi/data-view'
    Vue.use(fullScreenContainer)

    <template>
    <dv-full-screen-container>
    要展示的数据大屏内容
    这里建议高度使用百分比来布局,而且要考虑mac设备适配问题,防止百分比发生布局错乱
    需要注意的点是,一个是宽度,一个是字体大小,不产生换行
    </dv-full-screen-container>
    </template>
    <script>
    import screenfull from "screenfull";
    export default {
    name: "cockpit",
    mounted() {
    if (screenfull && screenfull.enabled && !screenfull.isFullscreen) {
    screenfull.request();
    }
    }
    };
    </script>
    <style lang="scss">
    #dv-full-screen-container {
    width: 100vw;
    height: 100vh;
    display: flex;
    flex-direction: column;
    background-color: #131a2b;
    }
    </style>


  4. 插件地址


3. 效果图


image.png


作者:狗尾巴花的尖
来源:juejin.cn/post/7372105071573663763
收起阅读 »

从编程语言的角度,JS 是不是一坨翔?一个来自社区的暴力观点

web
给前端以福利,给编程以复利。大家好,我是大家的林语冰。 00. 写在前面 毋庸置疑,JS 的历史包袱确实罄竹难书。用朱熹的话说,天不生 ES6,JS 万古如长夜。 就技术细节而言,ES6 之前,JS 保留了若干臭名昭著的反人类设计,包括但不限于: typeo...
继续阅读 »

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


00. 写在前面


毋庸置疑,JS 的历史包袱确实罄竹难书。用朱熹的话说,天不生 ES6,JS 万古如长夜。


就技术细节而言,ES6 之前,JS 保留了若干臭名昭著的反人类设计,包括但不限于:



  • typeof null 的抽象泄露

  • == 的无理要求

  • undefined 不是关键词

  • 其他雷区......


幸运的是,ES5 的“阉割模式”(strict mode)把一大坨 JS 的反人类设计都屏蔽了,后 ES6 时代的 JS 焕然一新。


所以,本期我们就从编程语言的宏观设计来思考,先不纠结 JS 极端情况的技术瑕疵,探讨一下 JS 作为地球上人气最高的编程语言,设计哲学上到底有何魅力?


00-js.png



免责声明:上述统计数据来源于 GitHub 社区,可能存在统计学偏差,仅供粉丝参考。



01. 标准化


编程语言人气排行榜屈居亚军的是 Python,那我们就用 JS 来打败 Python。


根据 MDN 电子书,JS 的核心语言是 ECMAScript,是一门由 ECMA TC39 委员会标准化的编程语言。并不是所有的编程语言都有标准化流程,JS 恰好是“天选之子”,后 ES6 的提案流程也相对稳定,虽然最近突然增加了 stage 2.7,但整体标准化无伤大雅。


值得一提的是,JS 的标准是向下兼容的,用户友好。JS 的大多数技术债务已经诉诸“阉割模式”禁用,而向下兼容则避免了近未来的 ESNext 出现主版本级别的破坏性更新。


举个栗子,ES2024 最新支持的数组分组提案,出于兼容性考虑,提案一度从 Array.prototype.group() 修改为 Object.groupBy()


02-group.png


可以看到,JS 的向下兼容设计正是为了防止和某些遗留代码库产生命名冲突,降低迁移成本。


换而言之,JS 不会像 Python 2 升级 Python 3 那样,让用户承担语言迭代伴生的兼容性税,学习成本和心智负担相对较小。


02. 动态类型


编程语言人气排行榜屈居季军的是 TS,那我们就用 JS 来打败 TS。


JS 和 TS 都是 ECMAScript 的超集,TS 则是 JS 的超集。简而言之,TS ≈ JS + 静态类型系统。


JS 和 TS 区别在于动态类型 vs 静态类型,不能说孰优孰劣,只能说各有千秋。我的个人心证是,静态类型对于工程化生态而言不可或缺,比如 IDE 或 Linting,但对于编程应用则不一定,因为没有静态类型,JS 也能开发 Web App。


03-ts.png


可以看到,因为 TS 是 JS 的超集,所以虽然没有显式的类型注解,上述代码也可作为 TS 代码,只不过 TS 和 JS 类型检查的粒度和时机并不一致。


一个争论点在于,静态类型的编译时检查有利于服务大型项目,但其实在大型项目中,一般会诉诸单元测试保障代码质量和回归测试。严格而言,静态类型之于大型项目的充要条件并不成立。


事实上,剥离静态类型注解的源码,恰恰体现了编程的本质和逻辑,这也是后来的静态类型语言偏爱半自动化的智能类型系统,因为有的类型注解可能是画蛇添足,而诉诸智能的类型推论可以解放程序猿的生产力。


03. 面向对象


编程语言人气排行榜第四名是 Java,那我们就用 JS 来打败 Java。


作为被误解最深的语言,前 ES6 的 JS 有一个普遍的误区:JS 不是面向对象语言,原因在于 ES5 没有 class。


这种“思想钢印”哪里不科学呢?不科学的地方在于,面向对象编程是面向对象,而不是 面向类。换而言之,类不是面向对象编程的充要条件


作为经典的面向对象语言,Java 不同于 C艹,不支持多继承。如果说多继承的 C艹 是“面向对象完备”的,那么能且仅能支持单继承的 Java,其面向对象也一定有不完备的地方,需要诉诸其他机制来弥补。


JS 的面向对象是不同于经典类式继承的原型机制,为什么无类、原型筑基的 JS 也能实现多继承的 C艹 的“面向对象完备”呢?搞懂这个问题,才能深入理解“对象”的本质。


04-oop.png


可以看到,JS 虽然没有类,但也通过神秘机制具备面向对象的三大特征。如果说 JS 不懂类,那么经典面向对象语言可能不懂面向对象编程,只懂“面向类编程”。


JS 虽然全称 JavaScript,但其实和 Java 一龙一猪,原型筑基的 JS 没有类也问题不大。某种意义上,类可以视为 JS 实现面向对象的语法糖。很多人知道封装、继承和多态的面向对象特性,但不知道面向对象的定义和思想,所以才会把类和“面向对象完备”等同起来,认为 JS 不是面向对象的语言。


04. 异步编程


编程语言人气排行榜第五名是 C#,那我们就用 JS 来打败 C#。


一般认为,JS 是一门单线程语言,有且仅有一个主线程。JS 有一个基于事件循环的并发模型,这个模型与 C# 语言等模型一龙一猪。


在 JS 中,当一个函数执行时,只有在它执行完毕后,JS 才会去执行任何其他的代码。换而言之,函数执行不会被抢占,而是“运行至完成”(除了 ES6 的生成器函数)。这与 C 语言不同,在 C 语言中,如果函数在线程中运行,它可能在任何位置被终止,然后在另一个线程中运行其他代码。


单线程的优势在于,大部分情况下,JS 不需要考虑多线程某些让人头大的复杂处理。因为我只会 JS,所以无法详细说明多线程的痛点,比如竞态、死锁、线程间通信(消息、信道、队列)、Actor 模型等。


但 JS 的单线程确实启发了其他支持多线程的语言,比如阮一峰大大的博客提到,Python 的 asyncio 模块只存在一个线程,跟 JS 一样。


05-async.jpg


在异步编程方面,JS 的并发模型其实算是奇葩,因为主流的编程语言都支持多线程模型,所以学习资料可以跨语言互相借鉴,C# 关于多线程的文档就有 10 页,而单线程的 JS 就像非主流的孤勇者,很多异步的理论都用不上,所以使用起来较为简单。


05. 高潮总结


JS 自诞生以来就是一种混合范式的“多面手语言”,这是 JS 二十年来依然元气满满的根本原因。


举个栗子,你可能已经见识过“三位一体”的神奇函数了:


06-fn.png


可以看到,在 ES6 之前,JS 中的函数其实身兼数职,正式由于 JS 是天生支持混合范式导致的。


前 JS 时代的先驱语言部分是单范式语言,比如纯粹的命令式过程语言 C 语言无法直接支持面向对象编程,而 C艹 11、Java 8 等经典面向对象语言则渐进支持函数式编程等。


后 JS 时代的现代编程语言大都直接拥抱混合范式设计,比如 TypeScript 和 Rust 等。作为混合范式语言,JS 是一种原型筑基、动态弱类型的脚本语言,同时支持面向对象编程、函数式编程、异步编程等编程范式或编程风格。


粉丝请注意,你可能偶尔会看到“JS 是一门解释型语言”的说法,其实后 ES6 时代的 JS 已经不再是纯粹的解释型语言了,也可能是 JIT(即时编译)语言,这取决于宿主环境中 JS 引擎或运行时的具体实现。


值得一提的是,混合范式语言的优势在于博采众家之长,有助于塑造攻城狮的开放式编程思维和心智模型;缺陷在于不同于单范式语言,混合范式语言可能支持了某种编程范式,但没有完全支持。(PS:编程范式完备性的判定边界是模糊的,所以偶尔也解读为风格或思维。)


因此,虽然 JS 不像 PHP 一样是地球上最好的语言,但 JS 作为地表人气最高的语言,背后是有深层原因的。


参考文献



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

你真的了解圣杯和双飞翼布局吗?

web
前言 圣杯和双飞翼布局作为面试常考的题目之一,相信大家肯定都会有着自己的一套知识储备,但是,你真的了解这两种经典布局吗?本文将介绍这两种经典布局产生的来龙去脉,以及实现这两种布局的一些方式,希望大家能够喜欢。 为什么需要圣杯和双飞翼布局 大家思考一个问题,这样...
继续阅读 »

前言


圣杯和双飞翼布局作为面试常考的题目之一,相信大家肯定都会有着自己的一套知识储备,但是,你真的了解这两种经典布局吗?本文将介绍这两种经典布局产生的来龙去脉,以及实现这两种布局的一些方式,希望大家能够喜欢。


为什么需要圣杯和双飞翼布局


大家思考一个问题,这样一种布局,你该怎么处理呢?


image.png

常规情况下,我们的布局思路应该这样写,从上到下,从左到右


<div>header</div>
<div>
<div>left</div>
<div>main</div>
<div>right</div>
</div>
<div>footer</div>

这样的三栏布局也没有什么问题,但是我们要知道一个网站的主要内容就是中间的部分,比如像掘金:


image.png

那么对于用户来说,他们当然是希望最中间的部分首先加载出来的,能看到最重要的内容,但是因为浏览器加载dom的机制是按顺序加载的,浏览器从HTML文档的开头开始,逐步解析并构建文档对象模型,所以,我们想让main首先加载出来的话,那就将它前置,然后通过一些CSS的样式,将其继续展示出上面的三栏布局的样式:


<div>header</div>
<div>
<div>main</div>
<div>left</div>
<div>right</div>
</div>
<div>footer</div>

这就是所谓的圣杯布局,最早是Matthew Levine 在2006年1月30日在In Search of the Holy Grail 这篇文章中提出来的;


那么完整效果、实现代码如下:


image.png


<style>
.header,
.footer {
width: 100%;
height: 200px;
background-color: pink;
}
.container {
padding: 0 100px 0 100px;
overflow: hidden;
}
.col {
position: relative;
float: left;
height: 400px;
}
.left {
background-color: green;
width: 100px;
margin-left: -100%;
left: -100px;
}
.main {
width: 100%;
background-color: blue;
}
.right {
width: 100px;
background-color: green;
margin-left: -100px;
right: -100px;
}
</style>

<div class="header">header</div>
<div class="container">
<div class="main col">main</div>
<div class="left col">left</div>
<div class="right col">right</div>
</div>
<div class="footer">footer</div>

如上述代码所示,,使用了相对定位浮动负值margin,将left和right装到main的两侧,所以顾名:圣杯


但是呢,圣杯是有问题的,在某些特殊的场景下,比如说,left和right盒子的宽度过宽的情况下,圣杯就碎掉了,比如将上述代码的left和right盒子的宽度改为以500px为基准:


<style>
.header,
.footer {
width: 100%;
height: 200px;
background-color: pink;
}
.container {
padding: 0 500px 0 500px;
overflow: hidden;
}
.col {
position: relative;
float: left;
height: 400px;
}
.left {
background-color: green;
width: 500px;
margin-left: -100%;
left: -500px;
}
.main {
width: 100%;
background-color: blue;
}
.right {
width: 500px;
background-color: green;
margin-left: -500px;
right: -500px;
}
</style>

<div class="header">header</div>
<div class="container">
<div class="main col">main</div>
<div class="left col">left</div>
<div class="right col">right</div>
</div>
<div class="footer">footer</div>

正常情况下,布局还是依然正常,只是两侧宽了而已:


image.png


但是我们将整个窗口缩小,圣杯就碎掉了:


image.png


原因是因为 padding: 0 500px 0 500px;,当整个窗口的最大宽度已经小于左右两边的padding共1000px,left和right就被挤下去了;


于是针对这种情况,淘宝UED的玉伯大大提出来了双飞翼布局,效果和圣杯布局一样,只是他将其比作一只鸟,左翅膀、中间、右翅膀;


相比于圣杯布局,双飞翼布局在原有的main盒子再加了一层div:


<div>header</div>
<div>
<div><div>main</div></div>
<div>left</div>
<div>right</div>
</div>
<div>footer</div>

实际的效果代码如下,哪怕再怎么缩,都不会被挤下去:


image.png


<style>
.header,
.footer {
width: 100%;
height: 200px;
background-color: pink;
}
.container {
padding: 0;
overflow: hidden;
}
.col {
float: left;
height: 400px;
}
.left {
background-color: green;
width: 500px;
margin-left: -100%;
}
.main {
width: 100%;
background-color: blue;
}
.main-in {
margin: 0 500px 0 500px;
}
.right {
width: 500px;
background-color: green;
margin-left: -500px;
}
</style>
<div class="header">header</div>
<div class="container">
<div class="main col">
<div class="main-in">main</div>
</div>
<div class="left col">left</div>
<div class="right col">right</div>
</div>
<div class="footer">footer</div>

圣杯布局实现方式补充


上面介绍了一种圣杯布局的实现方式,这里再介绍一种用绝对定位的,这种方法其实也能避免上述说的当左右两侧的盒子过于宽时,圣杯被挤破的情况:


<style>
.header,
.footer {
width: 100%;
height: 200px;
background-color: pink;
}
.container {
position: relative;
padding: 0 100px;
}
.col {
height: 400px;
}
.left {
background-color: green;
width: 100px;
position: absolute;
left: 0;
top: 0;
}
.main {
width: 100%;
background-color: blue;
}
.right {
width: 100px;
background-color: green;
position: absolute;
right: 0;
top: 0;
}
</style>

<div class="header">header</div>
<div class="container">
<div class="main col">main</div>
<div class="left col">left</div>
<div class="right col">right</div>
</div>
<div class="footer">footer</div>

双飞翼布局实现方式补充


也是使用绝对定位的:


<style>
.header,
.footer {
width: 100%;
height: 200px;
background-color: pink;
}
.container {
position: relative;
}
.col {
height: 400px;
}
.left {
background-color: green;
width: 500px;
position: absolute;
top: 0;
left: 0;
}
.main {
width: calc(100% - 1000px);
background-color: blue;
margin-left: 500px;
}
.main-in {
/* margin: 0 500px 0 500px; */
}
.right {
width: 500px;
background-color: green;
position: absolute;
top: 0;
right: 0;
}
</style>
<div class="header">header</div>
<div class="container">
<div class="main col">
<div class="main-in">main</div>
</div>
<div class="left col">left</div>
<div class="right col">right</div>
</div>
<div class="footer">footer</div>


其它普通的三列布局的实现


flex布局实现


<style>
* {
padding: 0;
margin: 0;
}
.header,
.footer {
width: 100%;
height: 200px;
background-color: blue;
}
.container {
display: flex;
}
.left {
width: 100px;
height: 300px;
background-color: pink;
}
.main {
flex: 1;
width: 100%;
height: 300px;
background-color: green;
}

.right {
width: 100px;
height: 300px;
background-color: pink;
}
</style>

<div class="header">header</div>
<div class="container">
<div class="left col">left</div>
<div class="main col">main</div>
<div class="right col">right</div>
</div>
<div class="footer">footer</div>

绝对定位实现


<style>
* {
padding: 0;
margin: 0;
}
.header,
.footer {
width: 100%;
height: 200px;
background-color: blue;
}
.container {
position: relative;
padding: 0 100px;
}
.left {
width: 100px;
height: 300px;
background-color: pink;
position: absolute;
top: 0;
left: 0;
}
.main {
width: 100%;
height: 300px;
background-color: green;
}

.right {
width: 100px;
height: 300px;
background-color: pink;
position: absolute;
top: 0;
right: 0;
}
</style>

<div class="header">header</div>
<div class="container">
<div class="left col">left</div>
<div class="main col">main</div>
<div class="right col">right</div>
</div>
<div class="footer">footer</div>


总结


现在真正了解到圣杯布局、双飞翼布局和普通三列布局的思想了吗?虽然它们三者最终的效果可能一样,但是实际的思路,优化也都不一样,希望能对你有所帮助!!!!感谢支持


作者:进阶的鱼
来源:juejin.cn/post/7405467437564428299
收起阅读 »

前端中的“+”连接符,居然有鲜为人知的强大功能!

web
故事背景:"0"和"1"布尔判断 这几天开发,遇到一个问题:根据后端返回的isCritical判断页面是否展示【关键标签】 很难受的是,后端的isCritical的枚举值是字符串 ”0“: 非关键 ”1“ :关键 这意味着前端得自己转换一下,于是我写出了这...
继续阅读 »

故事背景:"0"和"1"布尔判断


这几天开发,遇到一个问题:根据后端返回的isCritical判断页面是否展示【关键标签】



很难受的是,后端的isCritical的枚举值是字符串


0“: 非关键
1“ :关键

这意味着前端得自己转换一下,于是我写出了这样的代码


// html
<van-icon v-if="getCriticalStatus(it.isCritical)" color="#E68600"/>

// js
const getCriticalStatus = (critical: string) => {
if (critical.value === "1") return true;
return false;
}

我以为我这样写很简单了,没想到同事看到后,说我这样写麻烦了,于是给我改了一下代码


// html
<van-icon v-if="+it.isCritical" color="#E68600"/>

我大惊失色脱水缩合,这就行了?看来,我还是小看"+"运算符了!


"+"的常见使用场景


前端对"+"连字符一定不陌生,它的算术运算符功能和字符串连接功能,我们用脚趾头也能敲出来。


算术运算符


在 JavaScript 中,+ 是最常见的算术运算符之一,可以用来执行加法运算。


let a = 5;
let b = 10;
let sum = a + b;
// sum的值为15

字符串连接符


+ 还可以用来连接字符串。


let firstName = "石";
let lastName = "小石";
let fullName = firstName + " " + lastName; // fullName的值为"石小石"

如果是数字和字符连接,它会把数字转成字符


const a = 1
const b = "2"
const c = a + b; // c的值为字符串"12"

"+"的高级使用场景


除了上述的基本使用场景,其实它还有一些冷门但十分使用的高级使用场景。


URL编码中的空格


在 URL 编码中,+ 字符可以表示空格,尤其是在查询字符串中。


http://shixiaoshi.com/search?query=hello+world

上面的代码中,hello+world 表示查询 hello world,其中的 + 会被解码为一个空格。


但要注意的是,现代 URL 编码规范中推荐使用 %20 表示空格,而不是 +


一元正号运算符


+ 的高级用法,再下觉得最牛逼的地方就是可以作为一元运算符使用!


+ 作为一元运算符时,可以将一个值转换为数字(如果可能的话)。


let str = "123";
let num = +str;
// num的值为123,类型为number

这一用法在处理表单输入时特别有用,因为表单输入通常是字符串类型。


let inputValue = "42";
let numericValue = +inputValue; // 将字符串转换为数字42

那么回到文章开头的问题,我们看看下面的代码为什么可以生效


// html
<van-icon v-if="getCriticalStatus(it.isCritical)" color="#E68600"/>

// js
const getCriticalStatus = (critical: string) => {
if (critical.value === "1") return true;
return false;
}

// html   优化后的代码
<van-icon v-if="+it.isCritical" color="#E68600"/>

由于it.isCritical的值是字符"0"或"1",通过"+it.isCritical"转换后,其值是数字0或1,而恰好0可以当false使用,1可以当true使用!因此,上述代码可以生效!



JavaScript 中的类型转换规则会将某些值隐式转换为布尔值:



  • 假值 :在转换为布尔值时被视为 false 的值,包括:false0 (数字零)、-0 (负零)、"" (空字符串)、nullundefinedNaN (非数字)

  • 真值 :除了上述假值外,所有其他值在转换为布尔值时都被视为 true



一元正号运算符的原理


通过上文,我们知道:当使用 + 操作符时,JavaScript 会尝试把目标值转换为数字,它遵循以下规则:。


转换规则


数字类型


如果操作数是数字类型,一元正号运算符不会改变其值。
例如:+5 还是 5


// 数字类型
console.log(+5); // 5(数字)

字符串类型


如果字符串能够被解析为有效的数字,则返回相应的数字。
如果字符串不能被解析为有效的数字(如含有非数字字符),则返回 NaN(Not-a-Number)。
例如:+"123" 返回 123+"abc" 返回 NaN


// 字符串类型
console.log(+"42"); // 42
console.log(+"42abc"); // NaN

布尔类型


true 会被转换为 1
false 会被转换为 0


// 布尔类型
console.log(+true); // 1
console.log(+false); // 0

null


null 会被转换为 0


// null 
console.log(+null); // 0

undefined


undefined 会被转换为 NaN


//  undefined
console.log(+undefined); // NaN

对象类型


对象首先会通过内部的 ToPrimitive 方法被转换为一个原始值,然后再进行数字转换。通常通过调用对象的 valueOftoString 方法来实现,优先调用 valueOf


// 对象类型
console.log(+{}); // NaN
console.log(+[]); // 0
console.log(+[10]); // 10
console.log(+["10", "20"]); // NaN

底层原理


不重要,简单说说:


在 JS引擎内部,执行一元正号运算符时,实际调用了 ToNumber 抽象操作,这个操作试图将任意类型的值转换为数字。ToNumber 操作依据 ECMAScript 规范中的规则,将不同类型的值转换为数字。


总结


一元正号运算符 + 是一个简便的方法,用于将非数字类型转换为数字。


如果你们后端返回字符串0和1,你需要转换成布尔值,使用"+"简直不要太爽


// isCritical 是字符串"0"或"1"
<van-icon v-if="+isCritical" color="#E68600"/>

或者处理表单输入时用


let inputValue = "42";
let value = +inputValue; // 将字符串转换为数字42

作者:石小石Orz
来源:juejin.cn/post/7402076531294863360
收起阅读 »

还在封装 xxxForm,xxxTable 残害你的同事?试试这个工具

web
之前写过一篇文章 我理想中的低代码开发工具的形态,已经吐槽了各种封装 xxxForm,xxxTable 的行为,这里就不啰嗦了。今天再来看看我的工具达到了什么程度。 多图预警。。。 以管理后台一个列表页为例 选择对应的模板 截图查询区域,使用 OCR 初始...
继续阅读 »

之前写过一篇文章 我理想中的低代码开发工具的形态,已经吐槽了各种封装 xxxForm,xxxTable 的行为,这里就不啰嗦了。今天再来看看我的工具达到了什么程度。


多图预警。。。


以管理后台一个列表页为例



选择对应的模板



截图查询区域,使用 OCR 初始化查询表单的配置



截图表头,使用 OCR 初始化 table 的配置



使用 ChatGPT 翻译中文字段



生成代码



效果


目前我们没有写一行代码,就已经达到了如下的效果



下面是一部分生成的代码


import { reactive, ref } from 'vue'

import { IFetchTableListResult } from './api'

interface ITableListItem {
/**
* 决算单状态
*/

settlementStatus: string
/**
* 主合同编号
*/

mainContractNumber: string
/**
* 客户名称
*/

customerName: string
/**
* 客户手机号
*/

customerPhone: string
/**
* 房屋地址
*/

houseAddress: string
/**
* 工程管理
*/

projectManagement: string
/**
* 接口返回的数据,新增字段不需要改 ITableListItem 直接从这里取
*/

apiResult: IFetchTableListResult['result']['records'][0]
}

interface IFormData {
/**
* 决算单状态
*/

settlementStatus?: string
/**
* 主合同编号
*/

mainContractNumber?: string
/**
* 客户名称
*/

customerName?: string
/**
* 客户手机号
*/

customerPhone?: string
/**
* 工程管理
*/

projectManagement?: string
}

interface IOptionItem {
label: string
value: string
}

interface IOptions {
settlementStatus: IOptionItem[]
}

const defaultOptions: IOptions = {
settlementStatus: [],
}

export const defaultFormData: IFormData = {
settlementStatus: undefined,
mainContractNumber: undefined,
customerName: undefined,
customerPhone: undefined,
projectManagement: undefined,
}

export const useModel = () => {
const filterForm = reactive<IFormData>({ ...defaultFormData })

const options = reactive<IOptions>({ ...defaultOptions })

const tableList = ref<(ITableListItem & { _?: unknown })[]>([])

const pagination = reactive<{
page: number
pageSize: number
total: number
}>({
page: 1,
pageSize: 10,
total: 0,
})

const loading = reactive<{ list: boolean }>({
list: false,
})

return {
filterForm,
options,
tableList,
pagination,
loading,
}
}

export type Model = ReturnType<typeof useModel>


这就是用模板生成的好处,有规范,随时可以改,而封装 xxxForm,xxxTable 就是一个黑盒。


原理


下面大致说一下原理



首先是写好一个个模版,vscode 插件读取指定目录下模版显示到界面上



每个模版下可能包含如下内容:



选择模版后,进入动态表单配置界面



动态表单是读取 config/schema.json 里的内容进行动态渲染的,目前支持 amis、form-render、formily



配置表单是为了生成 JSON 数据,然后根据 JSON 数据生成代码。所以最终还是无法避免的使用私有的 DSL ,但是生成后的代码是没有私有 DSL 的痕迹的。生成代码本质是 JSON + EJS 模版引擎编译 src 目录下的 ejs 文件。


为了加快表单的配置,可以自定义脚本进行操作



这部分内容是读取 config/preview.json 内容进行显示的



选择对应的脚本方法后,插件会动态加载 script/index.js 脚本,并执行里面对应的方法



以 initColumnsFromImage 方法为例,这个方法是读取剪贴板里的图片,然后使用百度 OCR 解析出文本,再使用文本初始化表单


initColumnsFromImage: async (lowcodeContext) => {
context.lowcodeContext = lowcodeContext;
const res = await main.handleInitColumnsFromImage();
return res;
},

export async function handleInitColumnsFromImage() {
const { lowcodeContext } = context;
if (!lowcodeContext?.clipboardImage) {
window.showInformationMessage('剪贴板里没有截图');
return lowcodeContext?.model;
}
const ocrRes = await generalBasic({ image: lowcodeContext!.clipboardImage! });
env.clipboard.writeText(ocrRes.words_result.map((s) => s.words).join('\r\n'));
window.showInformationMessage('内容已经复制到剪贴板');
const columns = ocrRes.words_result.map((s) => ({
slot: false,
title: s.words,
dataIndex: s.words,
key: s.words,
}));
return { ...lowcodeContext.model, columns };
}

反正就是可以根据自己的需求定义各种各样的脚本。比如使用 ChatGPT 翻译 JSON 里的指定字段,可以看我的上一篇文章 TypeChat、JSONSchemaChat实战 - 让ChatGPT更听你的话


再比如要实现把中文翻译成英文,然后英文使用驼峰语法,这样就可以将中文转成英文代码变量,下面是实现的效果



选择对应的命令菜单后 vscode 插件会加载对应模版里的脚本,然后执行里面的 onSelect 方法。



main.ts 代码如下


import { env, window, Range } from 'vscode';
import { context } from './context';

export async function bootstrap() {
const clipboardText = await env.clipboard.readText();
const { selection, document } = window.activeTextEditor!;
const selectText = document.getText(selection).trim();
let content = await context.lowcodeContext!.createChatCompletion({
messages: [
{
role: 'system',
content: `你是一个翻译家,你的目标是把中文翻译成英文单词,请翻译时使用驼峰格式,小写字母开头,不要带翻译腔,而是要翻译得自然、流畅和地道,使用优美和高雅的表达方式。请翻译下面用户输入的内容`,
},
{
role: 'user',
content: selectText || clipboardText,
},
],
});
content = content.charAt(0).toLowerCase() + content.slice(1);
window.activeTextEditor?.edit((editBuilder) => {
if (window.activeTextEditor?.selection.isEmpty) {
editBuilder.insert(window.activeTextEditor.selection.start, content);
} else {
editBuilder.replace(
new Range(
window.activeTextEditor!.selection.start,
window.activeTextEditor!.selection.end,
),
content,
);
}
});
}


使用了 ChatGPT。


再来看看,之前生成管理后台 CURD 页面的时候,连 mock 也一起生成了,主要逻辑放在了 complete 方法里,这是插件的一个生命周期函数。



因为 mock 服务在另一个项目里,所以需要跨目录去生成代码,这里我在 mock 服务里加了个接口返回 mock 项目所在的目录


.get(`/mockProjectPath`, async (ctx, next) => {
ctx.body = {
status: 200,
msg: '',
result: __dirname,
};
})

生成代码的时候请求这个接口,就知道往哪个目录生成代码了


const mockProjectPathRes = await axios
.get('http://localhost:3001/mockProjectPath', { timeout: 1000 })
.catch(() => {
window.showInformationMessage(
'获取 mock 项目路径失败,跳过更新 mock 服务',
);
});
if (mockProjectPathRes?.data.result) {
const projectName = workspace.rootPath
?.replace(/\\/g, '/')
.split('/')
.pop();
const mockRouteFile = path.join(
mockProjectPathRes.data.result,
`${projectName}.js`,
);
let mockFileContent = `
import KoaRouter from 'koa-router';
import proxy from '../middleware/Proxy';
import { delay } from '../lib/util';

const Mock = require('mockjs');

const { Random } = Mock;

const router = new KoaRouter();
router{{mockScript}}
module.exports = router;
`
;

if (fs.existsSync(mockRouteFile)) {
mockFileContent = fs.readFileSync(mockRouteFile).toString().toString();
const index = mockFileContent.lastIndexOf(')') + 1;
mockFileContent = `${mockFileContent.substring(
0,
index,
)}
{{mockScript}}\n${mockFileContent.substring(index)}`
;
}
mockFileContent = mockFileContent.replace(/{{mockScript}}/g, mockScript);
fs.writeFileSync(mockRouteFile, mockFileContent);
try {
execa.sync('node', [
path.join(
mockProjectPathRes.data.result
.replace(/\\/g, '/')
.replace('/src/routes', ''),
'/node_modules/eslint/bin/eslint.js',
),
mockRouteFile,
'--resolve-plugins-relative-to',
mockProjectPathRes.data.result
.replace(/\\/g, '/')
.replace('/src/routes', ''),
'--fix',
]);
} catch (err) {
console.log(err);
}

mock 项目也可以通过 vscode 插件快速创建和使用



上面展示的模版都放在了 github.com/lowcode-sca… 仓库里,照着 README 步骤做就可以使用了。


作者:若邪
来源:juejin.cn/post/7315242945454735414
收起阅读 »

领导:你加的水印怎么还能被删掉的,扣工资!

web
故事是这样的 领导:小李,你加的水印怎么还能被删掉的?这可是关乎公司信息安全的大事!这种疏忽怎么能不扣工资呢? 小李:领导,请您听我解释一下!我确实按照常规的方法加了水印,可是…… 领导:(打断)但是什么?难道这就是你对公司资料的保护吗? 小李:我也不明白,...
继续阅读 »

故事是这样的



领导:小李,你加的水印怎么还能被删掉的?这可是关乎公司信息安全的大事!这种疏忽怎么能不扣工资呢?


小李:领导,请您听我解释一下!我确实按照常规的方法加了水印,可是……


领导:(打断)但是什么?难道这就是你对公司资料的保护吗?


小李:我也不明白,按理说水印是无法删除的,我会再仔细检查一下……


领导:我不能容忍这样的失误。这种安全隐患严重影响了我们的机密性。


小李焦虑地试图解释,但领导的目光如同刀剑一般锐利。他决定,这次一定要找到解决方法,否则,这将是一场职场危机……



2 (2).gif


水印组件



小李想到antd中有现成的水印组件,便去研究了一下。即使删掉了水印div,水印依然存在,因为瞬间又生成了一个相同的水印div。他一瞬间想到了解决方案,并开始了重构水印组件。



3.gif


原始代码


//app.vue
<template>
<div>
<Watermark text="前端百事通">
<div class="content"></div>
</Watermark>
</div>

</template>

<script setup>
import Watermark from './components/Watermark.vue';
</script>



<style scoped>
.content{
width: 400px;
height: 400px;
background-color: aquamarine;
}
</style>

//watermark.vue
<template>
<div ref="watermarkRef" class="watermark-container">
<slot>

</slot>
</div>

</template>


<script setup>
import { onMounted, ref } from 'vue';
const watermarkRef=ref(null)
const props = defineProps({
text: {
type: String,
default: '前端百事通'
},
fontSize: {
type: Number,
default: 14
},
gap: {
type: Number,
default: 50
},
rotate: {
type: Number,
default: 45
}
})
onMounted(() => {
addWatermark()
})
const addWatermark = () => {
const { rotate, gap, text, fontSize } = props
const color = 'rgba(0, 0, 0, 0.3)'; // 可以从props中传入
const watermarkContainer = watermarkRef.value;

const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
const font=fontSize+'px DejaVu Sans Mono'
// 设置水印文字的宽度和高度
const metrics = context.measureText(text);
const canvasWidth=metrics.width+gap
canvas.width=canvasWidth
canvas.height=canvasWidth
// 绘制水印文字
context.translate(canvas.width/2,canvas.height/2)
context.rotate((-1 * rotate * Math.PI / 180));
context.fillStyle = color;
context.font=font
context.textAlign='center'
context.textBaseline='middle'
context.fillText(text,0,0)
// 将canvas转为图片
const url = canvas.toDataURL('image/png');
// 创建水印元素并添加到容器中
const watermarkLayer = document.createElement('div');
watermarkLayer.style.position = 'absolute';
watermarkLayer.style.top = '0';
watermarkLayer.style.left = '0';
watermarkLayer.style.width = '100%';
watermarkLayer.style.height = '100%';
watermarkLayer.style.pointerEvents = 'none';
watermarkLayer.style.backgroundImage = `url(${url})`;
watermarkLayer.style.backgroundRepeat = 'repeat';
watermarkLayer.style.zIndex = '9999';
watermarkContainer.appendChild(watermarkLayer);
}
</script>


<style>
.watermark-container {
position: relative;
width: 100%;
height: 100%;
overflow: auto;
}
</style>



防篡改思路



  • 监听删除dom操作,在删除dom操作的瞬间重新生成一个相同的dom元素

  • 监听修改dom样式操作

  • 不能使用onMounted,改为watchEffect进行监听操作


使用MutationObserver监听整个区域


let ob
onMounted(() => {
ob=new MutationObserver((records)=>{
console.log(records)
})
ob.observe(watermarkRef.value,{
childList:true,
attributes:true,
subtree:true
})
})
onUnmounted(()=>{
ob.disconnect()
})

在删除水印div之后,打印一下看看records是什么。


image.png


在修改div样式之后,打印一下records


image.png


很明显,如果是删除,我们就关注removedNodes字段,如果是修改,我们就关注attributeName字段。


onMounted(() => {
ob=new MutationObserver((records)=>{
for(let item of records){
//监听删除
for(let ele of item.removedNodes){
if(ele===watermarkDiv){
generateFlag.value=!generateFlag.value
return
}
}
//监听修改
if(item.attributeName==='style'){
generateFlag.value=!generateFlag.value
return
}
}
})
ob.observe(watermarkRef.value,{
childList:true,
attributes:true,
subtree:true
})
})

watchEffect(() => {
//generateFlag的用处是让watchEffect收集这个依赖
//通过改变generateFlag的值来重新调用生成水印的函数
generateFlag.value
if(watermarkRef.value){
addWatermark()
}
})


4.gif



最终,小李向领导展示了新的水印组件,取得了领导的认可和赞许,保住了工资。


全剧终。
文章同步发表于前端百事通公众号,欢迎关注!



作者:LonelyCorner
来源:juejin.cn/post/7362309246556356647
收起阅读 »

终于搞懂类型声明文件.d.ts和declare了,原来用处如此大

web
项目中的.d.ts和declare 最近开发项目,发现公司代码里都有一些.d.ts后缀的文件 还有一些奇奇怪怪的declare代码 秉持着虚心学习的态度,我向同事请教了这些知识点,发现这些东西居然蛮重要的。于是,我根据自己的理解,把这些知识简单总结一下。 ...
继续阅读 »



项目中的.d.ts和declare


最近开发项目,发现公司代码里都有一些.d.ts后缀的文件



还有一些奇奇怪怪的declare代码



秉持着虚心学习的态度,我向同事请教了这些知识点,发现这些东西居然蛮重要的。于是,我根据自己的理解,把这些知识简单总结一下。


类型声明文件.d.ts


为什么需要 .d.ts 文件?


如果我们在ts项目中使用第三方库时,如果这个库内置类型声明文件.d.ts,我们在写代码时可以获得对应的代码补全、接口提示等功能。


比如,我们在index.ts中使用aixos时:



当我们引入axios时,ts会检索aixos的package.json文件,并通过其types属性查找类型声明文件,查找到index.d.ts这个文件后,就会根据其内部配置进行语法提示。



但是如果某个库没有内置类型声明文件时,我们使用这个库,不会获得Ts的语法提示,甚至会有类型报错警告



像这种没有内置类型声明文件的,我们就可以自己创建一个xx.d.ts的文件来自己声明,ts会自动读取到这个文件里面的内容的。比如,我们在index.ts中使用"vue-drag",会提示缺少声明文件。


由于这个库没有@types/xxxx声明包,因此,我们可以在项目内自定义一个vueDrag.d.ts声明文件。


// vueDrag.d.ts
declare module 'vue-drag'

这个时候,就不会报错了,没什么警告了。


第三方库的默认类型声明文件


当我们引入第三方库时,ts会自动检索aixos的package.json文件,并通过其types属性查找类型声明文件,查找到index.d.ts这个文件后,就会根据其内部配置进行语法提示。比如,我们刚才说的axios




  • "typings"与"types"具有相同的意义,也可以使用它。

  • 主声明文件名是index.d.ts并且位置在包的根目录里(与index.js并列),你就不需要使用"types"属性指定了。


第三方库的@types/xxxx类型声明文件


如express这类框架,它们的开发时Ts还没有流行,自然没有使用Ts进行开发,也自然不会有ts的类型声明文件。如果你想引入它们时也获得Ts的语法提示,就需要引入它们对应的声明文件npm包了。


使用声明文件包,不用重构原来的代码就可以在引入这些库时获得Ts的语法提示


比如,我们安装express对应的声明文件包后,就可以获得相应的语法提示了。


npm i --save-dev @types/express


@types/express包内的声明文件



.d.ts声明文件


通过上述的几个示例,我们可以知道.d.ts文件的作用和@types/xxxx包一致,@type/xxx需要下载使用,而.d.ts是我们自己创建在项目内的。


.d.ts文件除了可以声明模块,也可以用来声明变量。


例如,我们有一个简单的 JavaScript 函数,用于计算两个数字的总和:


// math.js
const sum = (a, b) => a + b
export { sum }

TypeScript 没有关于函数的任何信息,包括名称、参数类型。为了在 TypeScript 文件中使用该函数,我们在 d.ts 文件中提供其定义:


// math.d.ts
declare function sum(a: number, b: number): number

现在,我们可以在 TypeScript 中使用该函数,而不会出现任何编译错误。


.ts 是标准的 TypeScript 文件。其内容将被编译为 JavaScript。


*.d.ts 是允许在 TypeScript 中使用现有 JavaScript 代码的类型定义文件,其不会编译为 JavaScript。


shims-vue.d.ts


shims-vue.d.ts 文件的主要作用是声明 Vue 文件的模块类型,使得 TypeScript 能够正确地处理 .vue 文件,并且不再报错。通常这个文件会放在项目的根目录或 src 目录中。


shims-vue.d.ts 文件的内容一般长这样:


// shims-vue.d.ts

declare module '*.vue' {
import { DefineComponent } from 'vue';
const component: DefineComponent<{}, {}, any>;
export default component;
}


  • declare module '*.vue' : 这行代码声明了一个模块,匹配所有以 .vue 结尾的文件。* 是通配符,表示任意文件名。

  • import { DefineComponent } from 'vue'; : 引入 Vue 的 DefineComponent 类型。这是 Vue 3 中定义组件的类型,它具有良好的类型推断和检查功能。

  • const component: DefineComponent<{}, {}, any>; : 定义一个常量 component,它的类型是 DefineComponent,并且泛型参数设置为 {} 表示没有 props 和 methods 的基本 Vue 组件类型。any 用来宽泛地表示组件的任意状态。

  • export default component; : 将这个组件类型默认导出。这样,当你在 TypeScript 文件中导入 .vue 文件时,TypeScript 就知道导入的内容是一个 Vue 组件。


declare


.d.ts 文件中的顶级声明必须以 “declare” 或 “export” 修饰符开头。


通过declare声明的类型或者变量或者模块,在include包含的文件范围内,都可以直接引用而不用去import或者import type相应的变量或者类型。



  • declare声明一个类型


declare type Asd {
name: string;
}


  • declare声明一个模块


declare module '*.css';
declare module '*.less';
declare module '*.png';

.d.ts文件顶级声明declare最好不要跟export同级使用,不然在其他ts引用这个.d.ts的内容的时候,就需要手动import导入了


在.d.ts文件里如果顶级声明不用export的话,declare和直接写type、interface效果是一样的,在其他地方都可以直接引用


declare type Ass = {
a: string;
}
type Bss = {
b: string;
};

作者:快乐就是哈哈哈
来源:juejin.cn/post/7402891257196691468
收起阅读 »

前端:金额高精度处理

web
Decimal 是什么 想必大家在用js 处理 数字的 加减乘除的时候,或许都有遇到过 精度不够 的问题,还有那些经典的面试题 0.2+0.1 !== 0.3, 至于原因,那就是 js 计算底层用的是 IEEE 754 ,精度上有限制, 那么Decimal.j...
继续阅读 »

Decimal 是什么


想必大家在用js 处理 数字的 加减乘除的时候,或许都有遇到过 精度不够 的问题,还有那些经典的面试题 0.2+0.1 !== 0.3,
至于原因,那就是 js 计算底层用的是 IEEE 754 ,精度上有限制,
那么Decimal.js 就是帮助我们解决 js中的精度失准的问题。


原理



  • 它的原理就是将数字用字符串表示,字符串在计算机中可以说是无限的。

  • 并使用基于字符串的算术运算,以避免浮点数运算中的精度丢失。它使用了一种叫做十进制浮点数算术(Decimal Floating Point Arithmetic)的算法来进行精确计算。

  • 具体来说,decimal.js库将数字表示为一个字符串,其中包含整数部分、小数部分和一些其他的元数据。它提供了一系列的方法和运算符,用于执行精确的加减乘除、取模、幂运算等操作。


精度丢失用例


const a = 31181.82
const b = 50090.91
console.log(a+b) //81272.73000000001

Decimal 的引入 与 加减乘除



  • 如何引入


npm install --save decimal.js  // 安装
import Decimal from "decimal.js" // 具体文件中引入




let a = 1
let b = 6
// a 与 b 可以是 任何类型,Decimal 内部会自己处理兼容
// 下面两种都可以 可以带 new 也不可以不带 new(推荐带new)
let res = new Decimal(a).add(new Decimal(b))
let res = Decimal(a).add(Decimal(b))




let a = "4"
let b = "8"
// a 与 b 可以是 任何类型,Decimal 内部会自己处理兼容
// 下面两种都可以 可以带 new 也不可以不带 new
let res = new Decimal(a).sub(new Decimal(b))
let res = Decimal(a).sub(Decimal(b))




let a = 1
let b = 6
// a 与 b 可以是 任何类型,Decimal 内部会自己处理兼容
// 下面两种都可以 可以带 new 也不可以不带 new
let res = new Decimal(a).mul(new Decimal(b))
let res = Decimal(a).mul(Decimal(b))




let a = 1
let b = 6
// a 与 b 可以是 任何类型,Decimal 内部会自己处理兼容
// 下面两种都可以 可以带 new 也不可以不带 new
let res = new Decimal(a).div(new Decimal(b))
let res = Decimal(a).div(Decimal(b))

注意


上面的结果是 一个 Decimal 对象,你可以转换成 Number 或则 String


let res = Decimal(a).div(Decimal(b)).toNumber()  // 结果转换成 Number
let res = Decimal(a).div(Decimal(b)).toString() // 结果转换成 String

关于保存几位小数相关


//查看有几位小数 (注意不计算 小数点 最后 末尾 的 0)
y = new Decimal(987000.000)
y.sd() // '3' 有效位数
y.sd(true) // '6' 总共位数

// 保留 多少个位数 (小数位 会补0)
x = 45.6
x.toPrecision(5) // '45.600'

// 保留 多少位有效位数(小数位 不会补0,是计算的有效位数)
x = new Decimal(9876.5)
x.toSignificantDigits(6) // '9876.5' 不会补0 只是针对有效位数

// 保留几位小数 , 跟 js 中的 number 一样
toFixed
x = 3.456
// 向下取整
x.toFixed(2, Decimal.ROUND_DOWN) // '3.45' (舍入模式 向上0 向下1 四舍五入 4,7)
// 向上取整
Decimal.ROUND_UP

//四舍五入
ROUND_HALF_UP //(主要)

// 使用例子
let num2 = 0.2
let num3 = 0.1
let res = new Decimal(num2).add(new Decimal(num3)).toFixed(2, Decimal.ROUND_HALF_UP)
console.log(res); //返回值是字符串类型

超过 javascript 允许的数字


如果使用超过 javascript 允许的数字的值,建议传递字符串而不是数字,以避免潜在的精度损失。


new Decimal(1.0000000000000001); // '1'
new Decimal(88259496234518.57); // '88259496234518.56'
new Decimal(99999999999999999999); // '100000000000000000000'

new Decimal(2e308); // 'Infinity'
new Decimal(1e-324); // '0'

new Decimal(0.7 + 0.1); // '0.7999999999999999'

可读性


与 JavaScript 数字一样,字符串可以包含下划线作为分隔符以提高可读性。


x = new Decimal("2_147_483_647");


其它进制的数字


如果包含适当的前缀,则也接受二进制、十六进制或八进制表示法的字符串值。


x = new Decimal("0xff.f"); // '255.9375'
y = new Decimal("0b10101100"); // '172'
z = x.plus(y); // '427.9375'

z.toBinary(); // '0b110101011.1111'
z.toBinary(13); // '0b1.101010111111p+8'

x = new Decimal(
"0b1.1111111111111111111111111111111111111111111111111111p+1023"
);
// '1.7976931348623157081e+308'

最后:希望本篇文章能帮到您!


作者:分母等于零
来源:juejin.cn/post/7405153695507234867
收起阅读 »

视差滚动效果实现

web
视差滚动是一种在网页设计和视频游戏中常见的视觉效果技术,它通过在不同速度上移动页面或屏幕上的多层图像,创造出深度感和动感。 这种效果通过前景、中景和背景以不同的速度移动来实现,使得近处的对象看起来移动得更快,而远处的对象移动得较慢。 在官网中适当的使用视差...
继续阅读 »

视差滚动是一种在网页设计和视频游戏中常见的视觉效果技术,它通过在不同速度上移动页面或屏幕上的多层图像,创造出深度感和动感。
这种效果通过前景、中景和背景以不同的速度移动来实现,使得近处的对象看起来移动得更快,而远处的对象移动得较慢。



example.gif


在官网中适当的使用视差效果,可以增加视觉吸引力,提高用户的参与度,从而提升网站和品牌的形象。本文通过JavaScript、CSS多种方式并在React框架下进行了视差效果的实现,供你参考指正。


实现方式


1、background-attachment


通过配置该 CSS 属性值为fixed可以达到背景图像的位置相对于视口固定,其他元素正常滚动的效果。但该方法的视觉表现单一,没有纵深,缺少动感。


.parallax-box {
width: 100%;
height: 100vh;
background-image: url("https://picsum.photos/800");
background-size: cover;
background-attachment: fixed;

display: flex;
justify-content: center;
align-items: center;
}

iShot_2024-08-24_10.16.55.gif
点击访问完整在线代码


2、Transform 3D


在 CSS 中使用 3D 变换效果,通过将元素划分至不同的纵深层级,在滚动时相对视口不同距离的元素,滚动所产生的位移在视觉上就会呈现越近的元素滚动速度越快,相反越远的元素滚动速度就越慢。


为方便理解,你可以想象正开车行驶在公路上,汽车向前移动,你转头看向窗外,近处的树木一闪而过,远方的群山和风景慢慢的渐行渐远,逐渐的在视野中消失,而天边的太阳却只会在很长的一段距离细微的移动。


iShot_2024-08-24_10.20.17.gif
点击访问完整在线代码


.parallax {
perspective: 1px; /* 设置透视效果,为3D变换创造深度感 */
overflow-x: hidden;
overflow-y: auto;
height: 100vh;
}

.parallax__group {
transform-style: preserve-3d; /* 保留子元素3D变换效果 */
position: relative;
height: 100vh;
}

.parallax__layer {
position: absolute;
inset: 0;
display: flex;
justify-content: center;
align-items: center;
}

/* 背景层样式,设置为最远的层 */
.parallax__layer--back {
transform: translateZ(-2px) scale(3);
z-index: 1;
}

/* 中间层样式,设置为中等距离的层 */
.parallax__layer--base {
transform: translateZ(-1px) scale(2);
z-index: 2;
}

/* 前景层样式,设置为最近的层 */
.parallax__layer--front {
transform: translateZ(0px);
z-index: 3;
}

实现原理


principle.png


通过设置 perspective 属性,为整个容器创建一个 3D 空间。


使用 transform-style: preserve-3d 保持子元素的 3D 变换效果。


将内容分为多个层(背景、中间、前景),使用 translateZ() 将它们放置在 3D 空间的不同深度。


对于较远的层(如背景层),使用 scale() 进行放大,以补偿由于距离产生的视觉缩小效果。


当用户滚动页面时,由于各层位于不同的 Z 轴位置,它们会以不同的速度移动,从而产生视差效果。


3、ReactScrollParallax


想得到更炫酷的滚动视差效果,纯 CSS 的实现方式就会有些吃力。


如下是在 React 中实现示例,通过监听滚动事件,封装统一的视差组件,来达到多样的动画效果。


iShot_2024-08-24_10.26.20.gif
点击访问完整在线代码


const Parallax = ({ children, effects = [], speed = 1, style = {} }) => {
// 状态hooks:用于存储动画效果的当前值
const [transform, setTransform] = useState("");

useEffect(() => {
if (!Array.isArray(effects) || effects.length === 0) {
console.warn("ParallaxElement: effects should be a non-empty array");
return;
}

const handleScroll = () => {
// 计算滚动进度
const scrollProgress =
(window.scrollY /
(document.documentElement.scrollHeight - window.innerHeight)) *
speed;

let transformString = "";

// 处理每个效果
effects.forEach((effect) => {
const { property, startValue, endValue, unit = "" } = effect;
const value =
startValue +
(endValue - startValue) * Math.min(Math.max(scrollProgress, 0), 1);

switch (property) {
case "translateX":
case "translateY":
transformString += `${property}(${value}${unit}) `;
break;
case "scale":
transformString += `scale(${value}) `;
break;
case "rotate":
transformString += `rotate(${value}${unit}) `;
break;
// 更多的动画效果...
default:
console.warn(`Unsupported effect property: ${property}`);
}
});

// 更新状态
setTransform(transformString);
};

window.addEventListener("scroll", handleScroll);
// 初始化位置
handleScroll();

return () => {
window.removeEventListener("scroll", handleScroll);
};
}, [effects, speed]);

// 渲染带有计算样式的子元素
return <div style={{ ...style, transform }}>{children}</div>;
};

在此基础上你可以添加缓动函数使动画效果更加平滑;以及使用requestAnimationFrame获得更高的动画性能。


requestAnimationFrame 带来的性能提升



同步浏览器渲染周期:requestAnimationFrame 会在浏览器下一次重绘之前调用指定的回调函数。这确保了动画更新与浏览器的渲染周期同步,从而产生更流畑的动画效果。


提高性能:与使用 setInterval 或 setTimeout 相比,requestAnimationFrame 可以更高效地管理动画。它只在浏览器准备好进行下一次重绘时才会执行,避免了不必要的计算和重绘。


优化电池使用:在不可见的标签页或最小化的窗口中,requestAnimationFrame 会自动暂停,这可以节省 CPU 周期和电池寿命。


适应显示器刷新率:requestAnimationFrame 会自动适应显示器的刷新率。这意味着在 60Hz、120Hz 或其他刷新率的显示器上,动画都能保持流畑。


避免丢帧:由于与浏览器的渲染周期同步,使用 requestAnimationFrame 可以减少丢帧现象,特别是在高负荷情况下。


更精确的时间控制:requestAnimationFrame 提供了一个时间戳参数,允许更精确地控制动画的时间。



4、组件库方案


在当前成熟的前端生态中,想要获得精彩的视差动画效果,你可以通过现有的开源组件库来高效的完成开发。


以下是一些你可以尝试的主流组件库:



引用参考


MDN - background-attachment


MDN - transform-style


Pure CSS Parallax Websites


How to create parallax scrolling with CSS


视差滚动实践


原文地址


作者:小学后生
来源:juejin.cn/post/7406161967617163301
收起阅读 »

为什么vue:deep、/deep/、>>>样式能穿透到子组件

web
为什么vue:deep、/deep/、>>>样式能穿透到子组件 在scoped标记的style中,只要涉及三方组件,那deep符号会经常被使用,用来修改外部组件的样式。 小试牛刀 不使用deep 要想修改三方组件样式,只能添加到scoped之...
继续阅读 »

为什么vue:deep、/deep/、>>>样式能穿透到子组件


在scoped标记的style中,只要涉及三方组件,那deep符号会经常被使用,用来修改外部组件的样式。


小试牛刀


不使用deep


要想修改三方组件样式,只能添加到scoped之外,弊端是污染了全局样式,后续可能出现样式冲突。


<style lang="less">
.container {
.el-button {
background: #777;
}
}

使用 /deep/ deprecated


 .container1 {
/deep/ .el-button {
background: #000;
}
}

使用 >>> deprecated


.container2 >>> .el-button {
background: #222;
}

当在vue3使用/deep/或者>>>::v-deep,console面板会打印警告信息:


the >>> and /deep/ combinators have been deprecated. Use :deep() instead.

由于/deep/或者>>>在less或者scss中存在兼容问题,所以不推荐使用了。


使用:deep


.container3 {
:deep(.el-button) {
background: #444;
}
}

那么问题来了,如果我按以下的方式嵌套deep,能生效吗?


.container4 {
:deep(.el-button) {
:deep(.el-icon) {
color: #f00;
}
}
}

源码解析


/deep/或>>>会被编译为什么


编译后的代码为:


.no-deep .container1[data-v-f5dea59b] .el-button { background: #000; } 

源代码片段:


if (
n.type === 'combinator' &&
(n.value === '>>>' || n.value === '/deep/')
) {
n.value = ' '
n.spaces.before = n.spaces.after = ''
warn(
`the >>> and /deep/ combinators have been deprecated. ` +
`Use :deep() instead.`,
)
return false
}

当vue编译样式时,先将样式解析为AST对象,例如deep/ .el-button会被解析为Selector对象,/deep/ .el-button解析后生成的Selector包含的字段:


{ type: 'combinator', value: '/deep/' } 

然后将n.value由/deep/替换为空 。所以转换出来的结果,.el-button直接变为.container下的子样式。


:deep会被编译为什么?


编译后的代码:


.no-deep .container3[data-v-f5dea59b] .el-button { background: #444; }

源代码片段:


// .foo :v-deep(.bar) -> .foo[xxxxxxx] .bar
let last: selectorParser.Selector['nodes'][0] = n
n.nodes[0].each(ss => {
selector.insertAfter(last, ss)
last = ss
})
// insert a space combinator before if it doesn't already have one
const prev = selector.at(selector.index(n) - 1)
if (!prev || !isSpaceCombinator(prev)) {
selector.insertAfter(
n,
selectorParser.combinator({
value: ' ',
}),
)
}
selector.removeChild(n)

还是以.container4 :deep(.el-button)为例,当解析到:deep符号式,selector快照为


image.png


parent为.container4 :deep(.el-button),当前selector的type正好为伪类标识pseudo,nodes节点包含一个.el-button


经过递归遍历,生成的selector结构为.container4 :deep(.el-button).el-button


最后一行代码selector.removeChild(n)会将:deep(.el-button)移出,所以输出的最终样式为.container4 .el-button


如果样式为:deep(.el-button) { :deep(.el-icon) { color: #f00 } },当遍历.el-icon时找不到ancestor,所以直接将:deep(.el-icon)作为其icon时找不到ancestor,其结果为:


.no-deep .container4[data-v-f5dea59b] .el-button :deep(.el-icon) { color: #f00; }

因此,deep是不支持嵌套的。


结尾



插个广告,麻烦各位大佬为小弟开源项目标个⭐️,点点关注:




  1. react-native-mapa, react native地图组件库

  2. mapboxgl-syncto-any,三维地图双屏联动


作者:前端下饭菜
来源:juejin.cn/post/7397285315822632997
收起阅读 »

8年前端总结和感想

web
8年前端总结和感想 本文是我前端工作 8 年的一些总结和感想 主要记录下个人点滴、前端知识点、场景应用、未来的憧憬以及个人规划,供自己以后查漏补缺,也欢迎同道朋友交流学习。 自我介绍 我是一名工作在非知名公司的 8 年前端,双非普通本科,自动化专业(非...
继续阅读 »

8年前端总结和感想



本文是我前端工作 8 年的一些总结和感想



主要记录下个人点滴、前端知识点、场景应用、未来的憧憬以及个人规划,供自己以后查漏补缺,也欢迎同道朋友交流学习。




自我介绍


我是一名工作在非知名公司的 8 年前端,双非普通本科,自动化专业(非计算机)。目前也在努力的提升自己,积极找工作状态。虽然工作已经 8 年了,但没待过超 10 人的前端团队,更没有开发过千万级、亿级流量的应用的经历,也是一种遗憾。


16 年才工作那会儿使用原生 JS 和 JQ 比较多,经常写 CSS 动画,主要做企业建站,自学了 PHPReactNative,还学过 krpano 的使用;17 年到 19 年,主要使用 reactReact Native做 H5 和 跨端开发,也维护过老 NG2.x 项目;19年到22年主要使用 vue、react 做hybrid APP及小程序,自学了electronnodeMongoDB;22 年至今主要从事 B 端的开发,C端也有部分,也主要是 reactvue 相关技术栈;


前端应用场景


前端是直面浏览器的,也可以说是直面用户的,我们的应用场景远广泛于后端,用到的 UI 组件库、插件、基础框架、基础语言也非常繁杂,所以在面试和自我学习提升的时候需要准备的东西非常多,有种学不动的感觉。


常见的应用场景就是 PC 浏览器,需要我们掌握一些兼容不同版本和不同浏览器的知识点,一般采取渐进增强或者优雅降级去处理;当然现在很多公司已经不做IE的兼容了,复杂度降低很多;同时大部分新项目都会使用 postcssautoprefixer,给 css 加兼容的前缀。其他的就是做后台的表单为主了,用的基本上都是 antd designelement ui。当然复杂点的要涉及到网页编辑器、Low Code、No Code等。


另一个主要场景就是手机浏览器APP 了,H5 WebAPP 会遇到 AndroidIOS的一些样式和行为需要兼容、列表和图片懒加载问题,还有调用原生的 SDK 进行地图、OCR、拍照、扫一扫等功能进行开发;为了更好的体验,还会选择 RN、flutter 进行跨端开发;复杂的处理场景一般涉及原生端,例如聊天室、直播等。


另一个场景就是小程序,其实还是写H5,主要是使用微信或者支付宝等相关 SDK 去实现,看官网就行了,文档也比较全。


还有一些是做 H5 小游戏,要求数学逻辑思维和算法更好点,初级一点的用 canvas+css3 去做,好一点的用游戏引擎去做,一般是 egretLayacreatejsthreejscocos


还有一些场景是做TV端的,有的是基于PC浏览器的,有些是套壳APP,一般使用 AntVecharts做图表数据展示,3D一般用 threejs去做。


还有一些做桌面应用的,一般来说桌面应用大多基于 C,但一些简单应用前端可以使用 electron 去进行桌面端开发,一般也用于大屏可视化,做数据展示的,当然我们熟悉的 vscode 也基于 electron 开发。


还有一些是做 ARVR3D全景图的,一般使用 WebGL3D引擎threejsbabylon.jsplaycanvas等,还可以用 css3d-enginekrpanopano2vr去做。


还有一些场景是做 web3DAPP(去中心化应用程序),大部分是做区块链和数字藏品的,推荐的技术是 Solidityweb3.jsEthers.js


前端网络


我们前端不管是开发 PC、 H5、小程序、 HyBrid App 还是其他应用,始终离不开是浏览器和 web-view,与浏览器交互就要了解基础的 HTTP、网络安全、 nginx方面的知识。


浏览器


浏览器的发展简史和市场份额竞争有空可以自行了解,首先我们要了解计算机架构:



  • 底层是机器硬件结构:简单的来说就是电脑主机+各种 IO 设备;复杂的来说有用于输入的鼠标键盘,用于输出的显示器、打印等设备,用于控制计算机的控制器和 CPU(核心大脑),用于存储的硬盘、内存,用于计算的CPU和GPU;

  • 中层是操作系统:常见的就是 WindowsMAC OSLinuxCentOS,手机就是安卓和 IOS(当然还有华为的鸿蒙);可以了解内存分配、进程和线程管理等方面知识。

  • 上层就是我们最熟悉的应用程序:有系统自带的核心应用程序、浏览器、各个公司和开发者开发的各种应用。


前端开发必要了解的就是chrome浏览器,可以说大部分开发基于此浏览器去做的。需要了解 进程和线程 概念、了解 chrome 多进程架构(浏览器主进程GPU进程网络进程渲染进程插件进程)。


其中最主要的是要了解主进程,包含多个线程:GUI渲染线程JS引擎线程(V8引擎)定时触发器线程事件线程异步HTTP请求线程。其中 V8 引擎又是核心,需要了解其现有架构:


V8引擎架构


了解 JS 编译成机器可以识别的机器码的过程:简单的说就是把 JS 通过 Lexer 词法分析器分解成一系列词法 tokens,再通过 Parser 语法分析为语法树 AST,再通过 Bytecode Generator 把语法树转成二进制代码 Bytecode,二进制代码再通过实时编译 JST 编译成机器能识别的汇编代码 MachineCode 去执行。


JS编码过程


代码的执行必然会占用大量的内存,那如何自动的把不需要使用的变量回收就叫作 GC 垃圾回收,有空可以了解其算法和回收机制。


HTTP


对于Http,我们前端首先需要了解其网络协议分层: OSI七层协议TCP/IP四层协议五层协议,这有助于我们了解应用层和传输层及网络层的工作流程;同时我们也要了解应用层的核心 http1.0http1.1http2.0https 的区别;还要了解传输层的 TCPUDPwebSocket



  • 在前后端交互方面必须了解 GETPOST 的请求方式,以及浏览器返回状态 2003xx4xx5xx的区别;还有前后端通信传输的 request header 头、响应报文体 response body,通信的 sessioncookie

  • 网络安全方面需要了解 https,了解非对称算法 rsa 和对称算法 des,登录认证的 JWT(JSON Web Token);同时也需要了解怎么防范 XSSCSRFSQL注入URL跳转漏洞点击劫持OS命令注入攻击


Nginx


我们的网页都是存储在 web 服务器上的,公司一般都会进行 nginx 的配置,可以对资源进行 gzip 压缩,redirect 重定向,解决 CROS 跨域问题,配置 history 路由拦截。技术方面,我们还要了解其安装、常用命令、反向代理、正向代理和负载均衡。


前端三剑客


前端简单的说就是在写 htmlcssjs 的,一般来说 js 我们会更多关注,其实 html 和 css 也大有用处。


HTML


html 的历史可以自行了解,我们需要更关注 文档声明、各种 标签元素块级元素及非块级元素语义化src与href的区别WebStorageHTML5的新特性。复杂的页面和功能会更依赖于我们的 canvas


css


css 方面主要了解布局相关 盒子模型position伪类和伪元素css选择器优先级、 各种 水平垂直居中方法、 清除浮动CSS3新特性CSS动画响应式布局相关的 remflex@media。当然也有部分公司非常重视用户的交互体验和 UI 效果,那会更依赖我们 CSS3 的使用。


JS


js 在现代开发过程中确实是最重要的,我们更关心其底层原理、使用的方法、异步的处理及 ES6 的使用。



  • 在底层方面我们需要了解其 作用域及作用域链闭包this绑定原型和原型链继承和类、属性描述符 defineProperty 和事件循环 Event Loop



    可以详看我写的javascript随笔




  • 在使用方面我们需要了解 值和类型 的判断、内置类型的 nullundefinedbooleannumberstringobjectsymbol,其中对象类型是个复杂类型,数组函数DateRegExp等都是一个对象;数组的各种 API 是我们开发中最常用的,了解 Dom操作 的API也是必要的。

  • ES6 方面要了解 let、const声明块作用域解构赋值箭头函数classpromiseasync awaitSetWeakSetMapWeakMapproxyReflect



    可以详看我写的(ES6+)随笔




  • TypeScript 在前端的使用越来越广泛,如果要搞 NodeJS 基本上是标配了,而且也是大厂的标配,还是有必要学习下的。要了解 TypeScript 的安装配置、基本语法、Type泛型<T>ClassInterfaceEnum命名空间模块



    可以详看我写的typescript随笔





前端框架


我们在开发过程中直接操作 dom 已经不多了,有的公司可能还要部分维护 JQ,但大多都在使用 ReactVueAngular这三个基础前端框架,很多其他跨平台框架及 UI 组件库都基于此,目前来说国内 React 和 Vue 是绝对的主流,我本人就更擅长React。



可以详看我写的react随笔vue随笔



React


开发 react,也就是在写 all in js,或者说是 JSX,那就必须了解其底层 JSX 是如何转化成虚拟节点 VDom 的。在转换 jsx 转换成 VDom,VDom在转换成真实 Dom,react 的底层做了很多优化,其中大家熟悉的就是 Fiberdiff生命周期 以及 事件绑定


那我们写 react 都是在写组件化的东西, 组件通信的各种方式也是需要了解的;还要了解 PureComponentmemoforwardRef等组件类的方法;了解 createElementcloneElementcreateContext等工具类的方法;了解 useStateuseEffectuseMemouseCallbackuseRef等hooks的使用;还有了解 高阶组件HOC 及自定义 hooks


了解 react16react17react18做了哪些优化。


Vue


vue 方面,我们需要了解 MVVM 原理、 template的解析、数据的 双向绑定、vue2 和 vue3 的响应式原理、其数据更新的 diff 算法;使用方面要了解其生命周期组件通信的各种方式和 vue3 的新特性。


前端工程化


上面写到了前端框架,在使用框架开发的过程中,我们必不可少的在整个开发过程向后端看齐,工程化的思想也深入前端。代码提交时可以使用git的钩子hooks进行流水线的自动化拉取,然后使用 webpackrollupgulp以及 vite 进行代码编译打包,最后使用 jenkinsAWS阿里云效等平台进行自动化部署,完成整个不同环境的打包部署流程。



可以详看我写的webpack随笔使用rollup搭建工具库并上传npm



webpack


在代码编译打包这块儿, webpack是最重要的,也是更复杂的,所以我们有必要多了解它。


在基础配置方面,我们要了解 modeentryoutputloaderplugin,其中 loader 和 plugin 是比较复杂的,webpack 默认只支持 js,那意味着要使用 es6 就要用 babel-loader,css 方面要配置 css-loaderstyle-loaderless-loadersass-loader等,图片文件等资源还要配置 file-loader


plugin 方面要配置 antd 的相关配置、清空打包目录的 clean-webpack-plugin、多线程打包的 HappyPack、分包的 splitChunks 等等配置。


在不同环境配置方面要基于 cross-env 配置 devServersourcemap


在构建优化方面要配置按需加载hashcachenoParsegzip压缩tree-shakingsplitChunks等。


幸运的是,现在很多脚手架都自动的帮你配置了很多,并且支持你选择什么模版去配置。


环境部署


环境部署方面,第一家公司我用的软件 FileZilla 进行手动上传 FTP 服务器,虽然也没出过啥错,但不智能,纯依靠人工,如果项目多,时间匆忙,很容易部署错环境,而且还要手动备份数据。后面学了点终端命令,使用 SSH 远程上传文件,其实还没有软件上传来的直接,也容易出错。后面换了公司,也用上了 CI/CD 持续集成,其本质就是平台帮你自动的执行配置好的命令,有 git 拉取代码的命令、npm run build的打包命令,最后 SSH 远程存到服务器的目录文件,并重启 nginx 的 web 服务器。


CI/CD 可让持续自动化和持续监控贯穿于应用的整个生命周期(从集成和测试阶段,到交付和部署)。


后端服务


为了更好的完成整个应用,了解后端技术也是必要的,我们可以从 nodejsMongoDBMySQL 等入手。如果有余力,了解 javac#c++ 也可以帮助我们更好的开发安卓和 IOS 应用。前后端都通了话,不管对于我们工作、面试、接活儿或者做独立开发者都是很必要的。


node


node 这方面,我们了解常用模块Event Loop 是必要的,框架可以选择 expresskoaegg,还有我最近刚学的NestJS也非常不错。


形而上学


了解完上面的文章,基本上你就了解了整个前端大体的开发流程、所需的知识点、环境的部署、线上网络安全。但如果需要进阶且不局限于前端和后端,我们需要了解数据结构设计模式算法英语


数据结构


常见的数据结构有8种: 数组队列链表散列表



可以详看我写的算法随笔-数据结构(栈)


可以详看我写的算法随笔-数据结构(队列)



设计模式


设计模式方面我们需要了解:



  • 六大原则: 单一职责原则开放封闭原则里氏替换原则依赖倒置原则接口隔离原则迪米特原则(最少知道原则)

  • 创建型设计模式: 单例模式原型模式工厂模式抽象工厂模式建造者模式

  • 结构型设计模式: 适配器模式装饰器模式代理模式外观模式桥接模式组合模式享元模式

  • 行为型设计模式: 观察者模式迭代器模式策略模式模板方法模式职责链模式命令模式备忘录模式状态模式访问者模式中介者模式解释器模式


算法


算法方面我们需要了解:



  • 基础概念: 时间复杂度空间复杂度

  • 排序方法:初级排序的 选择排序插入排序冒泡排序,高级排序的 快速排序归并排序堆排序

  • 搜索: 深度优先搜索广度优先搜索

  • 其他: 递归分治回溯动态规划贪心算法



可以详看我写的算法随笔-基础知识



英语


学生时代,觉得英语离我们挺远,进社会就用不到了。现在发现学好英语非常有用,我们可以入职福利待遇比较好的外企、可以更好的看懂文档、甚至起个文件名和变量名都好的多。最近我也在用多邻国学英语,目标是能进行简单的商务交流和国外旅游,还能在未来辅导下孩子英语作业。


前端未来


目前,初级前端确实饱和了,各个公司对前端已经不像我入职第一家公司那样简单就可以找到工作的了,尤其是在这个各种卷的环境里,我们不得不多学习更多前端方面的知识。对于初学者,我建议更多的了解计算机基础、js原理、框架的底层;对于已经工作一俩年想提升的,不妨多学点跨端、跨平台技术,还有后端的一些技术;对于工作多年想让未来路子更宽的,不得不内卷的学习更多的应用场景所需要的知识。


关于AI,我觉得并不是会代替我们的工具,反而缩小了我们和资深前端的距离。我们可以借助AI翻译国外的一些技术文档,学习更新的技术;可以帮我们进行代码纠错;还可以帮助我们实现复杂的算法和逻辑;善用 AI,让它成为我们的利器;


感想和个人规划


前端很复杂,并不是像很多后端所说的那么简单,处理的复杂度和应对多样的客户群都是比较大的挑战。资深的前端能很快的完成任务需求开发、并保证代码质量,并搭建更好的基础架构,但就业行情的不景气让我一度很迷茫,我们大龄程序员的出路在哪里,经验就不值钱了嘛?


对于未来,我会更多的学习英语、学习后端,向独立开发者转型。


谨以此文,献给未来的自己和同道中人!


作者:牛奶
来源:juejin.cn/post/7387420922809942035
收起阅读 »

同事一行代码,差点给我整破防了!

web
大家好,我是多喝热水。 最近开发公司项目的时候遇到一个哭笑不得的问题,知道真相的我差点破防! 还原现场 周一开周会的时候正常评审需求,在演示的过程中发生了一点小插曲,我们的聚合搜索功能它不能正常使用了,搜到的内容还是首次加载的数据,如下: 看到这种情况,我下...
继续阅读 »

大家好,我是多喝热水。


最近开发公司项目的时候遇到一个哭笑不得的问题,知道真相的我差点破防!


还原现场


周一开周会的时候正常评审需求,在演示的过程中发生了一点小插曲,我们的聚合搜索功能它不能正常使用了,搜到的内容还是首次加载的数据,如下:


Kapture 2024-07-15 at 18.50.39.gif


看到这种情况,我下意识的以为是后端返回的数据的问题,所以结束会议后我就着手排查了,如下:



结果发现后端的数据是没问题的,这我就很奇怪了,其他的 tab 都能正常展示数据,为什么就只有综合出现了问题?



开始排查


因为这个无限滚动组件是我封装的,所以我猜测会不会是这个组件出了什么问题?


但经过排查我发现,这个组件接收到的数据是没问题的。


那就很奇怪了,我传递的参数是正确的,后端返回的数据也是没问题的,凭什么你不能正常渲染?


直到我看到了这一行代码,我沉默了:



woc,你小子在代码里下毒!



看到这里我基本上可以确定就是这个 index 搞的鬼,在我尝试把它修改成 item.id 后,搜索功能就能正常使用了,如下:



问题复盘


为什么用 id 就正常了?


这里涉及到 React 底层 diff 算法的优化,有经验的小伙伴应该知道,React 源码中判断两个节点是否是同一个节点就是通过这个 key 属性来判断的,key 相同的话会直接复用旧的节点,如下:



这也就解释了为什么切换 tab 后列表中始终都是旧数据,因为我们使用了 index 作为 key,而 index 它是会重复的,新 index 和旧 index 对比,两者相等,React 就直接复用了旧的节点!


但 id 就不一样了,id 我们可以确保它就是唯一的,不会发生重复!


哎,排查问题半小时,解决问题只花 3 秒钟,我 tm.....



这个故事告诉我们:


一定不要在循环节点的时候使用 index 作为 key!


一定不要在循环节点的时候使用 index 作为 key!


一定不要在循环节点的时候使用 index 作为 key!


养成好习惯,特别是这种数据会动态变化的场景!!!


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

fabric.js 实现服装/商品定制预览效果

web
大家好,我是秦少卫,vue-fabric-editor 开源图片编辑项目的作者,很多开发者有问过我如何使用 fabric.js 实现商品定制的预览效果,今天跟大家分享一下实现思路。 预览图: 简单介绍大部分开发这类产品的开发者,都会提到一个关键词叫做 P...
继续阅读 »

大家好,我是秦少卫,vue-fabric-editor 开源图片编辑项目的作者,很多开发者有问过我如何使用 fabric.js 实现商品定制的预览效果,今天跟大家分享一下实现思路。


预览图:


20240816-085407.gif


20240816-085504.gif


20240816-085334.gif


简单介绍大部分开发这类产品的开发者,都会提到一个关键词叫做 POD ,按需定制,会通过设计工具简单的对产品进行颜色、图片的修改后,直接下单,获得自己独一无二的商品。



POD是什么?


按需定制(Print On Demand,简称POD),是一种订单履约方式,卖家提前设计好商品模板上架到销售平台,出单后,同步订到给供应商进行生产发货。



使用 fabric.js 实现商品定制预览,有 4 种实现方式


方式一:镂空 PNG 素材


这种方式最简单方便,只需要准备镂空的png素材,将图层放置在顶部不可操作即可,定制的图案在图层底部,进行拖拽修改即可,优点是简单方便,缺点是只能针对一个部位操作


image.png


方式二:png阴影 + 色块 + 图案叠加


如果要进一步实现多个部位的定制设计,不同部位使用不同的定制图,第一种方案就无法满足了,那么可以采用透明阴影 + 色块叠加图案的方式来实现多个位置的定制。


例如这样的商品,上下需要 2 张不同的定制图案。


Foxmail20240816092343.png


我们需要准备透明的阴影素材在最上方,下方添加色块区域并叠加图案:


阴影层


上身色块


下身色块


最底部放上原始的图片即可。


Foxmail20240816091617.png


方式三:SVG + 图案/颜色填充


fabric.js 支持导入 svg图片,如果是SVG形式的设计文件,只需要导入到编辑器中,对不同区域修改颜色或者叠加图案就可以。


20240816-091831.gif


方式四:平面图 + 3D 贴图


最后一种是平面图设计后,将平面图贴图到 3D 模型,为了效果更逼真,需要增加光源、法线等贴图,从实现上并不会太复杂,只是运营成本比较高,每一个 SKU 都需要做一个 3D模型
20240816-092903.gif


参考 Demo:


20240816-093350.gif
codepen.io/ricardcreag…


结束


以上就是fabric.js 实现服装/商品定制预览效果的 4 种思路,如果你正在开发类似产品,也可以使用开源项目快速构建你的在线商品定制工具。



Foxmail20240816093646.png


作者:愚坤秦少卫
来源:juejin.cn/post/7403245452215386150
收起阅读 »

前端如何做截图?

web
一、 背景 页面截图功能在前端开发中,特别是营销场景相关的需求中, 是比较常见的。比如截屏分享,相对于普通的链接分享,截屏分享具有更丰富的展示、更多的信息承载等优势。最近在需求开发中遇到了相关的功能,所以调研了相关的实现和原理。 二、相关技术 前端要实现页面截...
继续阅读 »

一、 背景


页面截图功能在前端开发中,特别是营销场景相关的需求中, 是比较常见的。比如截屏分享,相对于普通的链接分享,截屏分享具有更丰富的展示、更多的信息承载等优势。最近在需求开发中遇到了相关的功能,所以调研了相关的实现和原理。


二、相关技术


前端要实现页面截图的功能,现在比较常见的方式是使用开源的截图npm库,一般使用比较多的npm库有以下两个:



以上两种常见的npm库,对应着两种常见的实现原理。实现前端截图,一般是使用图形API重新绘制页面生成图片,基本就是SVG(dom-to-image)和Canvas(html2canvas)两种实现方案,两种方案目标相同,即把DOM转为图片,下面我们来分别看看这两类方案。


三、 dom-to-image


dom-to-image库主要使用的是SVG实现方式,简单来说就是先把DOM转换为SVG然后再把SVG转换为图片。


(一)使用方式


首先,我们先来简单了解一下dom-to-image提供的核心api,有如下一些方法:



  • toSvg (dom转svg)

  • toPng (dom转png)

  • toJpeg (dom转jpg)

  • toBlob (dom转二进制格式)

  • toPixelData (dom转原始像素值)


如需要生成一张png的页面截图,实现代码如下:


import domtoimage from "domtoimage"

const node = document.getElementById('node');
domtoimage.toPng(node,options).then((dataUrl) => {
const img = new Image();
img.src = dataUrl;
document.body.appendChild(img);
})

toPng方法可传入两个参数node和options。


node为要生成截图的dom节点;options为支持的属性配置,具体如下:filter,backgroundColor,width,height,style,quality,imagePlaceholder,cacheBust。


(二)原理分析


dom to image的源码代码不是很多,总共不到千行,下面就拿toPng方法做一下简单的源码解析,分析一下其实现原理,简单流程如下:


image.png


整体实现过程用到了几个函数:



  • toPng(调用draw,实现canvas=>png )

  • Draw(调用toSvg,实现dom=>canvas)

  • toSvg(调用cloneNode和makeSvgDataUri,实现dom=>svg)

  • cloneNode(克隆处理dom和css)

  • makeSvgDataUri(实现dom=>svg data:url)

  • toPng


toPng函数比较简单,通过调用draw方法获取转换后的canvas,利用toDataURL转化为图片并返回。


function toPng(node, options) {
return draw(node, options || {})
.then((canvas) => canvas.toDataURL());
}


  • draw


draw函数首先调用toSvg方法获得dom转化后的svg,然后将获取的url形式的svg处理成图片,并新建canvas节点,然后借助drawImage()方法将生成的图片放在canvas画布上。


function draw(domNode, options) {
return toSvg(domNode, options)
// 拿到的svg是image data URL, 进一步创建svg图片
.then(util.makeImage)
.then(util.delay(100))
.then((image) => {
// 创建canvas,在画布上绘制图像并返回
const canvas = newCanvas(domNode);
canvas.getContext("2d").drawImage(image, 0, 0);
return canvas;
});
// 新建canvas节点,设置一些样式的options参数
function newCanvas(domNode) {
const canvas = document.createElement("canvas");
canvas.width = options.width || util.width(domNode);
canvas.height = options.height || util.height(domNode);
if (options.bgcolor) {
const ctx = canvas.getContext("2d");
ctx.fillStyle = options.bgcolor;
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
return canvas;
}
}


  • toSvg

  • toSvg函数实现从dom到svg的处理,大概步骤如下:

  • 递归去克隆dom节点(调用cloneNode函数)

  • 处理字体,获取所有样式,找到所有的@font-face和内联资源,解析并下载对应的资源,将资源转为dataUrl给src使用。把上面处理完的css rules放入中,并把标签加入到clone的节点中去。
  • 处理图片,将img标签的src的url和css中backbround中的url,转为dataUrl使用。

  • 获取dom节点转化的dataUrl数据(调用makeSvgDataUri函数)


function toSvg(node, options) {
options = options || {};
// 处理imagePlaceholder、cacheBust值
copyOptions(options);
return Promise.resolve(node)
.then((node) =>
// 递归克隆dom节点
cloneNode(node, options.filter, true))
// 把字体相关的csstext放入style
.then(embedFonts)
// clone处理图片,将图片链接转换为dataUrl
.then(inlineImages)
// 添加options里的style放入style
.then(applyOptions)
.then((clone) =>
// node节点转化成svg
makeSvgDataUri(clone,
options.width || util.width(node),
options.height || util.height(node)));
// 处理一些options的样式
function applyOptions(clone) {
...
return clone;
}
}


  • cloneNode


cloneNode函数主要处理dom节点,内容比较多,简单总结实现如下:



  • 递归clone原始的dom节点,其中, 其中如果有canvas将转为image对象。

  • 处理节点的样式,通过getComputedStyle方法获取节点元素的所有CSS属性的值,并将这些样式属性插入新建的style标签上面, 同时要处理“:before,:after”这些伪元素的样式, 最后处理输入内容和svg。


function cloneNode(node, filter, root) {
if (!root && filter && !filter(node)) return Promise.resolve();
return Promise.resolve(node)
.then(makeNodeCopy)
.then((clone) => cloneChildren(node, clone, filter))
.then((clone) => processClone(node, clone));
function makeNodeCopy(node) {
// 将canvas转为image对象
if (node instanceof HTMLCanvasElement) return util.makeImage(node.toDataURL());
return node.cloneNode(false);
}
// 递归clone子节点
function cloneChildren(original, clone, filter) {
const children = original.childNodes;
if (children.length === 0) return Promise.resolve(clone);
return cloneChildrenInOrder(clone, util.asArray(children), filter)
.then(() => clone);
function cloneChildrenInOrder(parent, children, filter) {
let done = Promise.resolve();
children.forEach((child) => {
done = done
.then(() => cloneNode(child, filter))
.then((childClone) => {
if (childClone) parent.appendChild(childClone);
});
});
return done;
}
}
function processClone(original, clone) {
if (!(clone instanceof Element)) return clone;
return Promise.resolve()
.then(cloneStyle)
.then(clonePseudoElements)
.then(copyUserInput)
.then(fixSvg)
.then(() => clone);
// 克隆节点上的样式。
function cloneStyle() {
...
}
// 提取伪类样式,放到css
function clonePseudoElements() {
...
}
// 处理Input、TextArea标签
function copyUserInput() {
...
}
// 处理svg
function fixSvg() {
...
}
}
}


  • makeSvgDataUri


首先,我们需要了解两个特性:



  • SVG有一个元素,这个元素的作用是可以在其中使用具有其它XML命名空间的XML元素,换句话说借助标签,我们可以直接在SVG内部嵌入XHTML元素,举个例子:


<svg xmlns="http://www.w3.org/2000/svg">
<foreignObject width="120" height="50">
<body xmlns="http://www.w3.org/1999/xhtml">
<p>文字。</p>
</body>
</foreignObject>
</svg>

可以看到标签里面有一个设置了xmlns=“http://www.w3.org/1999/xhtml”…标签,此时标签及其子标签都会按照XHTML标准渲染,实现了SVG和XHTML的混合使用。



  • XMLSerializer对象能够把一个XML文档或Node对象转化或“序列化”为未解析的XML标记的一个字符串。


基于以上特性,我们再来看一下makeSvgDataUri函数,该方法实现node节点转化为svg,就用到刚刚提到的两个重要特性。


首先将dom节点通过


XMLSerializer().serializeToString() 序列化为字符串,然后在


标签 中嵌入转换好的字符串,foreignObject 能够在 svg


内部嵌入XHTML,再将svg处理为dataUrl数据返回,具体实现如下:


function makeSvgDataUri(node, width, height) {
return Promise.resolve(node)
.then((node) => {
// 将dom转换为字符串
node.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
return new XMLSerializer().serializeToString(node);
})
.then(util.escapeXhtml)
.then((xhtml) => `<foreignObject x="0" y="0" width="100%" height="100%">${xhtml}</foreignObject>`)
// 转化为svg
.then((foreignObject) =>
// 不指定xmlns命名空间是不会渲染的
`<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">${
foreignObject}
</svg>`
)
// 转化为data:url
.then((svg) => `data:image/svg+xml;charset=utf-8,${svg}`);
}

四、 html2canvas


html2canvas库主要使用的是Canvas实现方式,主要过程是手动将dom重新绘制成canvas,因此,它只能正确渲染可以理解的属性,有许多CSS属性无法正确渲染。


支持的CSS属性的完整列表:


html2canvas.hertzen.com/features/


浏览器兼容性:


Firefox 3.5+ Google Chrome Opera 12+ IE9+ Edge Safari 6+


官方文档地址:


html2canvas.hertzen.com/documentati…


(一)使用方式


// dom即是需要绘制的节点, option为一些可配置的选项
import html2canvas from 'html2canvas'
html2canvas(dom, option).then(canvas=>{
canvas.toDataURL()
})

常用的option配置:


image.png


全部配置文档:


html2canvas.hertzen.com/configurati…


(二)原理分析


html2canvas的内部实现相对dom-to-image来说要复杂一些, 基本原理是读取DOM元素的信息,基于这些信息去构建截图,并呈现在canvas画布中。


其中重点就在于将dom重新绘制成canvas的过程,该过程整体的思路是:遍历目标节点和目标节点的子节点,遍历过程中记录所有节点的结构、内容和样式,然后计算节点本身的层级关系,最后根据不同的优先级绘制到canvas画布中。


由于html2canvas的源码量比较大,可能无法像dom-to-image一样详细的分析,但还是可以大致了解一下整体的流程,首先可以看一下源码中src文件夹中的代码结构,如下图:


image.png


简单解析一下:



  • index:入口文件,将dom节点渲染到一个canvas中,并返回。

  • core:工具函数的封装,包括对缓存的处理函数、Context方法封装、日志模块等。

  • css:对节点样式的处理,解析各种css属性和特性,进行处理。

  • dom:遍历dom节点的方法,以及对各种类型dom的处理。

  • render:基于clone的节点生成canvas的处理方法。


基于以上这些核心文件,我们来简单了解一下html2canvas的解析过程, 大致的流程如下:


image.png



  • 构建配置项


在这一步会结合传入的options和一些defaultOptions,生成用于渲染的配置数据renderOptions。在过程中会对配置项进行分类,比如resourceOptions(资源跨域相关)、contextOptions(缓存、日志相关)、windowOptions(窗口宽高、滚动配置)、cloneOptions(对指定dom的配置)、renderOptions(render结果的相关配置,包括生成图片的各种属性)等,然后分别将各类配置项传到下接下来的步骤中。



  • clone目标节点并获取样式和内容


在这一步中,会将目标节点到指定的dom解析方法中,这个过程会clone目标节点和其子节点,获取到节点的内容信息和样式信息,其中clone dom的解析方法也是比较复杂的,这里不做详细展开。获取到目标节点后,需要把克隆出来的目标节点的dom装载到一个iframe里,进行一次渲染,然后就可以获取到经过浏览器视图真实呈现的节点样式。



  • 解析目标节点


目标节点的样式和内容都获取到了之后,就需要把它所承载的数据信息转化为Canvas可以使用的数据类型。在对目标节点的解析方法中,递归整个DOM树,并取得每一层节点的数据,对于每一个节点而言需要绘制的部分包括边框、背景、阴影、内容,而对于内容就包含图片、文字、视频等。在整个解析过程中,对目标节点的所有属性进行解析构造,转化成为指定的数据格式,基础数据格式可见以下代码:


class ElementContainer {
// 所有节点上的样式经过转换计算之后的信息
readonly styles: CSSParsedDeclaration;
// 节点的文本节点信息, 包括文本内容和其他属性
readonly textNodes: TextContainer[] = [];
// 当前节点的子节点
readonly elements: ElementContainer[] = [];
// 当前节点的位置信息(宽/高、横/纵坐标)
bounds: Bounds;
flags = 0;
...
}

具体到不同类型的元素如图片、IFrame、SVG、input等还会extends ElementContainer拥有自己的特定数据结构,在此不详细贴出。



  • 构建内部渲染器


把目标节点处理成特定的数据结构之后,就需要结合Canvas调用渲染方法了,Canvas绘图需要根据样式计算哪些元素应该绘制在上层,哪些在下层,那么这个规则是什么样的呢?这里就涉及到CSS布局相关的一些知识。


默认情况下,CSS是流式布局的,元素与元素之间不会重叠。不过有些情况下,这种流式布局会被打破,比如使用了浮动(float)和定位(position)。因此需要需要识别出哪些脱离了正常文档流的元素,并记住它们的层叠信息,以便正确地渲染它们。


那些脱离正常文档流的元素会形成一个层叠上下文。元素在浏览器中渲染时,根据W3C的标准,所有的节点层级布局,需要遵循层叠上下文和层叠顺序的规则,具体规则如下:


image.png


在了解了元素的渲染需要遵循这个标准后,Canvas绘制节点的时候,需要生成指定的层叠数据,就需要先计算出整个目标节点里子节点渲染时所展现的不同层级,构造出所有节点对应的层叠上下文在内部所表现出来的数据结构,具体数据结构如下:


// 当前元素
element: ElementPaint;
// z-index为负, 形成层叠上下文
negativeZIndex: StackingContext[];
// z-index为0、auto、transform或opacity, 形成层叠上下文
zeroOrAutoZIndexOrTransformedOrOpacity: StackingContext[];
// 定位和z-index形成的层叠上下文
positiveZIndex: StackingContext[];
// 没有定位和float形成的层叠上下文
nonPositionedFloats: StackingContext[];
// 没有定位和内联形成的层叠上下文
nonPositionedInlineLevel: StackingContext[];
// 内联节点
inlineLevel: ElementPaint[];
// 不是内联的节点
nonInlineLevel: ElementPaint[];

基于以上数据结构,将元素子节点分类,添加到指定的数组中,解析层叠信息的方式和解析节点信息的方式类似,都是递归整棵树,收集树的每一层的信息,形成一颗包含层叠信息的层叠树。



  • 绘制数据


基于上面两步构造出的数据,就可以开始调用内部的绘制方法,进行数据处理和绘制了。使用节点的层叠数据,依据浏览器渲染层叠数据的规则,将DOM元素一层一层渲染到canvas中,其中核心具体源码如下:


async renderStackContent(stack: StackingContext): Promise<void> {
if (contains(stack.element.container.flags, FLAGS.DEBUG_RENDER)) {
debugger;
}
// 1. the background and borders of the element forming the stacking context.
await this.renderNodeBackgroundAndBorders(stack.element);
// 2. the child stacking contexts with negative stack levels (most negative first).
for (const child of stack.negativeZIndex) {
await this.renderStack(child);
}
// 3. For all its in-flow, non-positioned, block-level descendants in tree order:
await this.renderNodeContent(stack.element);
for (const child of stack.nonInlineLevel) {
await this.renderNode(child);
}
// 4. All non-positioned floating descendants, in tree order. For each one of these,
// treat the element as if it created a new stacking context, but any positioned descendants and descendants
// which actually create a new stacking context should be considered part of the parent stacking context,
// not this new one.
for (const child of stack.nonPositionedFloats) {
await this.renderStack(child);
}
// 5. the in-flow, inline-level, non-positioned descendants, including inline tables and inline blocks.
for (const child of stack.nonPositionedInlineLevel) {
await this.renderStack(child);
}
for (const child of stack.inlineLevel) {
await this.renderNode(child);
}
// 6. All positioned, opacity or transform descendants, in tree order that fall int0 the following categories:
// All positioned descendants with 'z-index: auto' or 'z-index: 0', in tree order.
// For those with 'z-index: auto', treat the element as if it created a new stacking context,
// but any positioned descendants and descendants which actually create a new stacking context should be
// considered part of the parent stacking context, not this new one. For those with 'z-index: 0',
// treat the stacking context generated atomically.
//
// All opacity descendants with opacity less than 1
//
// All transform descendants with transform other than none
for (const child of stack.zeroOrAutoZIndexOrTransformedOrOpacity) {
await this.renderStack(child);
}
// 7. Stacking contexts formed by positioned descendants with z-indices greater than or equal to 1 in z-index
// order (smallest first) then tree order.
for (const child of stack.positiveZIndex) {
await this.renderStack(child);
}
}

在renderStackContent方法中,首先对元素本身调用renderNodeContent和renderNodeBackgroundAndBorders进行渲染处理。


然后处理各个分类的子元素,如果子元素形成了层叠上下文,就调用renderStack方法,这个方法内部继续调用了renderStackContent,这就形成了对于层叠上下文整个树的递归。


如果子元素是正常元素没有形成层叠上下文,就直接调用renderNode,renderNode包括两部分内容,渲染节点内容和渲染节点边框背景色。


async renderNode(paint: ElementPaint): Promise<void> {
if (paint.container.styles.isVisible()) {
// 渲染节点的边框和背景色
await this.renderNodeBackgroundAndBorders(paint);
// 渲染节点内容
await this.renderNodeContent(paint);
}
}

其中renderNodeContent方法是渲染一个元素节点里面的内容,其可能是正常元素、文字、图片、SVG、Canvas、input、iframe,对于不同的内容也会有不同的处理。


以上过程,就是html2canvas的整体内部流程,在了解了大致原理之后,我们再来看一个更为详细的源码流程图,对上述流程进行一个简单的总结。


五、 常见问题总结


在使用html2canvas的过程中,会有一些常见的问题和坑,总结如下:


(一)截图不全


要解决这个问题,只需要在截图之前将页面滚动到顶部即可:


document.documentElement.scrollTop = 0;
document.body.scrollTop = 0;

(二)图片跨域


插件在请求图片的时候会有图片跨域的情况,这是因为,如果使用跨域的资源画到canvas中,并且资源没有使用CORS去请求,canvas会被认为是被污染了,canvas可以正常展示,但是没办法使用toDataURL()或者toBlob()导出数据,详情可参考:developer.mozilla.org/en-US/docs/…


解决方案:在img标签上设置crossorigin,属性值为anonymous,可以开启CROS请求。当然,这种方式的前提还是服务端的响应头Access-Control-Allow-Origin已经被设置过允许跨域。如果图片本身服务端不支持跨域,可以使用canvas统一转成base64格式,方法如下。


function getUrlBase64_pro( len,url ) {
//图片转成base64
var canvas = document.createElement("canvas"); //创建canvas DOM元素
var ctx = canvas.getContext("2d");
return new Promise((reslove, reject) => {
var img = new Image();
img.crossOrigin = "Anonymous";
img.onload = function() {
canvas.height = len;
canvas.width = len;
ctx.drawImage(img, 0, 0, len, len);
var dataURL = canvas.toDataURL("image/");
canvas = null;
reslove(dataURL);
};
img.onerror = function(err){
reject(err)
}
img.src = url;
});
}

(三)截图与当前页面有区别


方式一:如果要从渲染中排除某些elements,可以向这些元素添加data-html2canvas-ignore属性,html2cnavas会将它们从渲染中排除,例如,如果不想截图iframe的部分,可以如下:


html2canvas(ele,{
useCORS: true,
ignoreElements: (element: any) => {
if (element.tagName.toLowerCase() === 'iframe') {
return element;
}
return false;
},
})

方式二:可以将需要转化成图片的部分放在一个节点内,再把整个节点,透明度设置为0, 其他部分层级设置高一些,即可实现截图指定区域。


六、 小结


本文针对前端截图实现的方式,对两个开源库dom-to-image和html2canvas的使用和原理进行了简单的使用方式、实现原理方面,进行介绍和分析。


参考资料:


1.dom-to-image原理


2.html2image原理简述


3.浏览器端网页截图方案详解


4.html2canvas


5.html2canvas实现浏览器截图的原理(包含源码分析的通用方法)


作者:庚云
来源:juejin.cn/post/7400319811358818340
收起阅读 »

折腾我2周的分页打印和下载pdf

web
1.背景 一开始接到任务需要打印html,之前用到了vue-print-nb-jeecg来处理Vue2一个打印的问题,现在是遇到需求要在Vue3项目里面去打印十几页的打印和下载为pdf,难点和坑就在于我用的库vue3-print-nb来做分页打印预览,下载pd...
继续阅读 »

1722391577748.jpg


1.背景


一开始接到任务需要打印html,之前用到了vue-print-nb-jeecg来处理Vue2一个打印的问题,现在是遇到需求要在Vue3项目里面去打印十几页的打印和下载为pdf,难点和坑就在于我用的库vue3-print-nb来做分页打印预览,下载pdf后面介绍


2.预览打印实现


    <div id="printMe" style="background:red;">
<p>葫芦娃,葫芦娃</p>
<p>一根藤上七朵花 </p>
<p>小小树藤是我家 啦啦啦啦 </p>
<p>叮当当咚咚当当 浇不大</p>
<p> 叮当当咚咚当当 是我家</p>
<p> 啦啦啦啦</p>
<p>...</p>
</div>

<button v-print="'#printMe'">Print local range</button>

因为官方提供的方案都是DOM加载完成后然后直接打印,但是我的需求是需要点击打印的时候根据id渲染不同的组件然后渲染DOM,后面仔细看官方文档,有个beforeOpenCallback方法在打印预览之前有个钩子,但是这个钩子没办法确定我接口加载完毕,所以我的思路就是用户先点击我写的点击按钮事件,等异步渲染完毕之后,我再同步触发真正的打印预览按钮,这样就变相解决了我的需求。




  1. 没办法处理接口异步渲染数据展示DOM进行打印操作

  2. 在布局相对定位的时候在谷歌浏览器会发现有布局整体变小的问题(后续用zoom处理的)


3.掉头发之下载pdf


下载pdf这种需求才是我每次去理发店不敢让tony把我头发打薄的原因,我看了很多技术文章,结合个人业务情况,采取的方案是html2canvas把html转成canvas然后转成图片然后通过jsPDF截取图片分页最后下载到本地。本人秉承着不生产水,只做大自然的搬运工的匠人精神,迅速而又果断的从社区来到社区去,然后找到了适配当前业务的逻辑代码(实践出真知)。


import html2canvas from 'html2canvas'
import jsPDF, { RGBAData } from 'jspdf'

/** a4纸的尺寸[595.28,841.89], 单位毫米 */
const [PAGE_WIDTH, PAGE_HEIGHT] = [595.28, 841.89]

const PAPER_CONFIG = {
/** 竖向 */
portrait: {
height: PAGE_HEIGHT,
width: PAGE_WIDTH,
contentWidth: 560
},
/** 横向 */
landscape: {
height: PAGE_WIDTH,
width: PAGE_HEIGHT,
contentWidth: 800
}
}

// 将元素转化为canvas元素
// 通过 放大 提高清晰度
// width为内容宽度
async function toCanvas(element: HTMLElement, width: number) {
if (!element) return { width, height: 0 }

// canvas元素
const canvas = await html2canvas(element, {
// allowTaint: true, // 允许渲染跨域图片
scale: window.devicePixelRatio * 2, // 增加清晰度
useCORS: true // 允许跨域
})

// 获取canvas转化后的宽高
const { width: canvasWidth, height: canvasHeight } = canvas

// html页面生成的canvas在pdf中的高度
const height = (width / canvasWidth) * canvasHeight

// 转化成图片Data
const canvasData = canvas.toDataURL('image/jpeg', 1.0)

return { width, height, data: canvasData }
}

/**
* 生成pdf(A4多页pdf截断问题, 包括页眉、页脚 和 上下左右留空的护理)
* @param param0
* @returns
*/

export async function outputPDF({
/** pdf内容的dom元素 */
element,

/** 页脚dom元素 */
footer,

/** 页眉dom元素 */
header,

/** pdf文件名 */
filename,

/** a4值的方向: portrait or landscape */
orientation = 'portrait' as 'portrait' | 'landscape'
}
) {
if (!(element instanceof HTMLElement)) {
return
}

if (!['portrait', 'landscape'].includes(orientation)) {
return Promise.reject(
new Error(`Invalid Parameters: the parameter {orientation} is assigned wrong value, you can only assign it with {portrait} or {landscape}`)
)
}
const [A4_WIDTH, A4_HEIGHT] = [PAPER_CONFIG[orientation].width, PAPER_CONFIG[orientation].height]

/** 一页pdf的内容宽度, 左右预设留白 */
const { contentWidth } = PAPER_CONFIG[orientation]

// eslint-disable-next-line new-cap
const pdf = new jsPDF({
unit: 'pt',
format: 'a4',
orientation
})

// 一页的高度, 转换宽度为一页元素的宽度
const { width, height, data } = await toCanvas(element, contentWidth)

// 添加
function addImage(
_x: number,
_y: number,
pdfInstance: jsPDF,
base_data: string | HTMLImageElement | HTMLCanvasElement | Uint8Array | RGBAData,
_width: number,
_height: number
) {
pdfInstance.addImage(base_data, 'JPEG', _x, _y, _width, _height)
}

// 增加空白遮挡
function addBlank(x: number, y: number, _width: number, _height: number) {
pdf.setFillColor(255, 255, 255)
pdf.rect(x, y, Math.ceil(_width), Math.ceil(_height), 'F')
}

// 页脚元素 经过转换后在PDF页面的高度
const { height: tFooterHeight, data: headerData } = footer ? await toCanvas(footer, contentWidth) : { height: 0, data: undefined }

// 页眉元素 经过转换后在PDF的高度
const { height: tHeaderHeight, data: footerData } = header ? await toCanvas(header, contentWidth) : { height: 0, data: undefined }

// 添加页脚
async function addHeader(headerElement: HTMLElement) {
headerData && pdf.addImage(headerData, 'JPEG', 0, 0, contentWidth, tHeaderHeight)
}

// 添加页眉
async function addFooter(pageNum: number, now: number, footerElement: HTMLElement) {
if (footerData) {
pdf.addImage(footerData, 'JPEG', 0, A4_HEIGHT - tFooterHeight, contentWidth, tFooterHeight)
}
}

// 距离PDF左边的距离,/ 2 表示居中
const baseX = (A4_WIDTH - contentWidth) / 2 // 预留空间给左边
// 距离PDF 页眉和页脚的间距, 留白留空
const baseY = 15

// 除去页头、页眉、还有内容与两者之间的间距后 每页内容的实际高度
const originalPageHeight = A4_HEIGHT - tFooterHeight - tHeaderHeight - 2 * baseY

// 元素在网页页面的宽度
const elementWidth = element.offsetWidth

// PDF内容宽度 和 在HTML中宽度 的比, 用于将 元素在网页的高度 转化为 PDF内容内的高度, 将 元素距离网页顶部的高度 转化为 距离Canvas顶部的高度
const rate = contentWidth / elementWidth

// 每一页的分页坐标, PDF高度, 初始值为根元素距离顶部的距离
const pages = [rate * getElementTop(element)]

// 获取该元素到页面顶部的高度(注意滑动scroll会影响高度)
function getElementTop(contentElement) {
if (contentElement.getBoundingClientRect) {
const rect = contentElement.getBoundingClientRect() || {}
const topDistance = rect.top

return topDistance
}
}

// 遍历正常的元素节点
function traversingNodes(nodes) {
for (const element of nodes) {
const one = element

/** */
/** 注意: 可以根据业务需求,判断其他场景的分页,本代码只判断表格的分页场景 */
/** */

// table的每一行元素也是深度终点
const isTableRow = one.classList && one.classList.contains('ant4-table-row')

// 对需要处理分页的元素,计算是否跨界,若跨界,则直接将顶部位置作为分页位置,进行分页,且子元素不需要再进行判断
const { offsetHeight } = one
// 计算出最终高度
const offsetTop = getElementTop(one)

// dom转换后距离顶部的高度
// 转换成canvas高度
const top = rate * offsetTop
const rateOffsetHeight = rate * offsetHeight

// 对于深度终点元素进行处理
if (isTableRow) {
// dom高度转换成生成pdf的实际高度
// 代码不考虑dom定位、边距、边框等因素,需在dom里自行考虑,如将box-sizing设置为border-box
updateTablePos(rateOffsetHeight, top)
}
// 对于普通元素,则判断是否高度超过分页值,并且深入
else {
// 执行位置更新操作
updateNormalElPos(top)
// 遍历子节点
traversingNodes(one.childNodes)
}
updatePos()
}
}

// 普通元素更新位置的方法
// 普通元素只需要考虑到是否到达了分页点,即当前距离顶部高度 - 上一个分页点的高度 大于 正常一页的高度,则需要载入分页点
function updateNormalElPos(top) {
if (top - (pages.length > 0 ? pages[pages.length - 1] : 0) >= originalPageHeight) {
pages.push((pages.length > 0 ? pages[pages.length - 1] : 0) + originalPageHeight)
}
}

// 可能跨页元素位置更新的方法
// 需要考虑分页元素,则需要考虑两种情况
// 1. 普通达顶情况,如上
// 2. 当前距离顶部高度加上元素自身高度 大于 整页高度,则需要载入一个分页点
function updateTablePos(eHeight: number, top: number) {
// 如果高度已经超过当前页,则证明可以分页了
if (top - (pages.length > 0 ? pages[pages.length - 1] : 0) >= originalPageHeight) {
pages.push((pages.length > 0 ? pages[pages.length - 1] : 0) + originalPageHeight)
}
// 若 距离当前页顶部的高度 加上元素自身的高度 大于 一页内容的高度, 则证明元素跨页,将当前高度作为分页位置
else if (
top + eHeight - (pages.length > 0 ? pages[pages.length - 1] : 0) > originalPageHeight &&
top !== (pages.length > 0 ? pages[pages.length - 1] : 0)
) {
pages.push(top)
}
}

// 深度遍历节点的方法
traversingNodes(element.childNodes)

function updatePos() {
while (pages[pages.length - 1] + originalPageHeight < height) {
pages.push(pages[pages.length - 1] + originalPageHeight)
}
}

// 对pages进行一个值的修正,因为pages生成是根据根元素来的,根元素并不是我们实际要打印的元素,而是element,
// 所��要把它修正,让其值是以真实的打印元素顶部节点为准
const newPages = pages.map(item => item - pages[0])

// 根据分页位置 开始分页
for (let i = 0; i < newPages.length; ++i) {
// 根据分页位置新增图片
addImage(baseX, baseY + tHeaderHeight - newPages[i], pdf, data!, width, height)
// 将 内容 与 页眉之间留空留白的部分进行遮白处理
addBlank(0, tHeaderHeight, A4_WIDTH, baseY)
// 将 内容 与 页脚之间留空留白的部分进行遮白处理
addBlank(0, A4_HEIGHT - baseY - tFooterHeight, A4_WIDTH, baseY)
// 对于除最后一页外,对 内容 的多余部分进行遮白处理
if (i < newPages.length - 1) {
// 获取当前页面需要的内容部分高度
const imageHeight = newPages[i + 1] - newPages[i]
// 对多余的内容部分进行遮白
addBlank(0, baseY + imageHeight + tHeaderHeight, A4_WIDTH, A4_HEIGHT - imageHeight)
}

// 添加页眉
if (header) {
await addHeader(header)
}

// 添加页脚
if (footer) {
await addFooter(newPages.length, i + 1, footer)
}

// 若不是最后一页,则分页
if (i !== newPages.length - 1) {
// 增加分页
pdf.addPage()
}
}
return pdf.save(filename)
}


4.分页的小姿势


如果有需求把打印预览的时候的页眉页脚默认取消不展示,然后自定义页面的边距可以这么设置样式


@page {
size: auto A4 landscape;
margin: 3mm;
}

@media print {
body,
html {
height: initial;
padding: 0px;
margin: 0px;
}
}

5.关于页眉页脚


由于业务是属于比较自定义化的展示,所以我封装成组件,然后根据返回的数据进行渲染到每个界面,然后利用绝对定位放在相同的位置,最后一点小优化就是,公共化提取界面的样式,然后整合为pub.scss然后引入到界面里面,这样即使产品有一定的样式调整,我也可以在公共样式里面去配置和修改,大大的减少本人的工作量。在日常的开发中也是这样,不要去抱怨需求的变动频繁,而是力争在写组件的过程中考虑到组件的健壮性和灵活度,给自己的工作减负,到点下班。


参考文章


juejin.cn/post/732343…


作者:endlesskiller
来源:juejin.cn/post/7397319113796780042
收起阅读 »

借助 LocatorJS ,快速定位本地代码

web
引言 前端coder在刚接触一个新项目时是十分迷茫的,修改一个小 bug 可能要从路由结构入手逐级查找。 LocatorJS 提供了一种更便捷的方式,你只需要按住预设的快捷键选中元素点击,就可以快速打开本地编辑器中的代码,是不是非常神奇? 安装 访问 goo...
继续阅读 »

引言


前端coder在刚接触一个新项目时是十分迷茫的,修改一个小 bug 可能要从路由结构入手逐级查找。 LocatorJS 提供了一种更便捷的方式,你只需要按住预设的快捷键选中元素点击,就可以快速打开本地编辑器中的代码,是不是非常神奇?


安装


访问 google 商店进行插件安装 地址


用法


本文以 MacOS 系统为例, Win系统可以用 Control 键替代 options键使用


LocatorJS 是 Chrome 浏览器的一个扩展程序,使用很便捷,只需要进行下面的三个步骤:



  1. 运行一个本地项目(本文以 LocatorJS源码 的 React 项目为例)

  2. 打开项目访问本地链接(例如:http://localhost:3348 )

  3. 按住键盘的 option 键(win系统是 control)后选中某一个元素并点击


0.jpeg


这时候,就会跳出一个是否打开的提示,点击 “打开Visual Studio Code” 后 元素所在的本地代码就会通过你的 VsCode (或者其他编辑器) 打开。是不是很神奇,那么它是怎么实现的呢?


原理解读


解读 Chrome 扩展程序,我们先打开 apps/extension/src/pages 路径,可以看到如下几个文件夹:


1.png


● Background 是放置后台代码的文件夹,本插件不涉及


● ClientUI 这里只有一行,引入了 @locator/runtime(本插件的核心代码)


● Content 放着插件与浏览器内容页面的代码,与页面代码一起执行


● Popup 文件夹下是点击浏览器插件图标弹出层的代码


4.1 解读  Content/index.ts


  Content/index.ts 中最重要的代码是 injectScript 方法,主要做了两件事情,一个是创建了 Script 标签执行了 hook.bundle.js,另一个是将 client.bundle.js 赋值给了 document.documentElement.dataset.locatorClientUrl(通过 Dom 传值),其余代码是一些监听事件


function injectScript() {
const script = document.createElement('script');
// script.textContent = code.default;
script.src = browser.runtime.getURL('/hook.bundle.js');

document.documentElement.dataset.locatorClientUrl =
browser.runtime.getURL('/client.bundle.js');

// This script runs before the <head> element is created,
// so we add the script to <html> instead.
if (document.documentElement) {
document.documentElement.appendChild(script);
if (script.parentNode) {
script.parentNode.removeChild(script);
}
}
}

4.2 解读 hook.bundle.js


hook.bundle.js 是 hook 文件夹下的 index文件打包后的产物,因此我们去·看 apps/extension/src/pages/hook/index.ts 即可


import { installReactDevtoolsHook } from '@locator/react-devtools-hook';
import { insertRuntimeScript } from './insertRuntimeScript';

installReactDevtoolsHook();
insertRuntimeScript();

● installReactDevtoolsHook 会确保你的 react devtools扩展已安装 (没安装就install一个,猜测是仅涉及使用 API 的轻量版(笔者未深究))


● insertRuntimeScript 会对页面生命周期做一个监听,尝试加载 LocatorJS 的 runtime 组件, 在 insertRuntimeScript() 中,看到了这两行:


  const locatorClientUrl = document.documentElement.dataset.locatorClientUrl;
delete document.documentElement.dataset.locatorClientUrl;

这个 locatorClientUrl 就是之前在 Content/index.ts 里传值的那个 client.bundle.js,这里笔者简单说下,在尝试加载插件的方法 tryToInsertScript() 第一行判断如下:


   if (!locatorClientUrl) {
return 'Locator client url not found';
}

这行判断其实已经可以推测出 client.bundle.js 的重要性了,它加载失败,整个插件直接返回错误信息了。
回过头来看向 ClientUI 文件夹下的 index.tsx 文件:


import '@locator/runtime';

至此,我们已经完成了 locatorJs 的加载逻辑推导,下一步我们讲揭开“定位器”的神秘面纱...


4.3 解读核心代码 runtime 模块


打开 packages/runtime/src/index.ts 文件


3.png
在这里我们看到不论是本地加载 runtime,还是浏览器加载扩展的方式都会去执行 initRuntime


initRuntime.ts


packages/runtime/src/initRuntime.ts 的initRuntime 


4.png
这个文件中声明了一些全局样式,并用 shadow dom 的方式进行了全局的样式隔离,我们关注下底部的这几行代码即可:


  // This weird import is needed because:
// SSR React (Next.js) breaks when importing any SolidJS compiled file, so the import has to be conditional
// Browser Extension breaks when importing with "import()"
// Vite breaks when importing with "require()"
if (typeof require !== "undefined") {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { initRender } = require("./components/Runtime");
initRender(layer, adapter, targets || allTargets);
} else {
import("./components/Runtime").then(({ initRender }) => {
initRender(layer, adapter, targets || allTargets);
});
}

兼容了一下服务端渲染和 SolidJs 的引入方式,引入相对路径下的 ./components/Runtime


核心组件 Runtime.tsx


packages/runtime/src/components/Runtime.tsx
抽丝剥茧,我们终于找到了它的核心组件 Runtime,这是一个使用 SolidJs框架编写的组件,包含了我们选中元素时出现的红框样式,以及所有的事件:


5.png


我们重点关注点击事件 clickListener ,最后点击跳转的方法是 goToLinkProps


export function goToLinkProps(
linkProps: LinkProps,
targets: Targets,
options: OptionsStore
) {
const link = buildLink(linkProps, targets, options);
window.open(link, options.getOptions().hrefTarget || HREF_TARGET);
}

采用逆推的方式,看 clickListener 事件里的 LinkProps 是怎样生成的:


  function clickListener(e: MouseEvent) {
...
const elInfo = getElementInfo(target, props.adapterId);

if (elInfo) {
const linkProps = elInfo.thisElement.link;
...
}
...
}

同样的方式,我们去看看 getElementInfo 怎么返回的(过程略过),我们以 react 的实现为例,打开
packages/runtime/src/adapters/react/reactAdapter.ts, 查看 getElementInfo 方法


export function getElementInfo(found: HTMLElement): FullElementInfo | null {
const labels: LabelData[] = [];

const fiber = findFiberByHtmlElement(found, false);
if (fiber) {
...
const thisLabel = getFiberLabel(fiber, findDebugSource(fiber)?.source);
...
return {
thisElement: {
box: getFiberOwnBoundingBox(fiber) || found.getBoundingClientRect(),
...thisLabel,
},
...
};
}
return null;
}

前面 goToLinkProps 使用的是 thisElement.link 字段, thisLabel 又依赖于 fiber 字段,等等! 这不是我们 react 玩家的老朋友 fiber 吗,我们查看一下生成它的 findFiberByHtmlElement 方法


export function findFiberByHtmlElement(
target: HTMLElement,
shouldHaveDebugSource: boolean
): Fiber | null {
const renderers = window.__REACT_DEVTOOLS_GLOBAL_HOOK__?.renderers;
const renderersValues = renderers?.values();
if (renderersValues) {
for (const renderer of Array.from(renderersValues) as Renderer[]) {
if (renderer.findFiberByHostInstance) {
const found = renderer.findFiberByHostInstance(target as any);
console.log('found', found)
if (found) {
if (shouldHaveDebugSource) {
return findDebugSource(found)?.fiber || null;
} else {
return found;
}
}
}
}
}
return null;
}

可以看到,这里是直接使用的 window 对象下的 __REACT_DEVTOOLS_GLOBAL_HOOK__ 属性做的处理,我们先打印一下 fiber 查看下生成的结构


image.png


惊奇的发现 _debugSource 字段里竟然包含了点击元素所对应本地文件的路径


我们到 goToLinkProps 方法里打印一下跳转的路径发现果然一致,只是实际跳转的路径加上了 vscode:// 开头,进行了协议跳转。


真相解读,_debugOwner 是怎么来的


一路砍瓜切菜终于要接近真相了,回顾代码我们其实只需要搞懂 window.REACT_DEVTOOLS_GLOBAL_HOOK 是怎么来的以及它做了什么,就可以收工了。



  1. _debugOwner 怎么来的?


    _debugOwner 是通过 window.REACT_DEVTOOLS_GLOBAL_HOOK 根据 HtmlElement 生成的 fiber 得来的, 它是 React Devtools 插件的全局变量 HOOK,这就是为什么 hook.bundle.js 要确保安装了 React Devtools


  2. REACT_DEVTOOLS_GLOBAL_HOOK 做了什么


    它是通过 @babel/plugin-transform-react-jsx-source 实现的,这个 plugin 可以在创建 fiber 的时候,将元素本地代码的位置信息保存下来,以 _debugSource 字段进行抛出



总结


LocatorJs 的 React 方案使用 React Devtools 扩展的全局 Hook,由 @babel/plugin-transform-react-jsx-source plugin 将元素所在代码路径写入 fiber 对象当中,通过 HtmlElement 查找到相对应的 fiber,取得本地代码的路径,随即可实现定位代码并跳转的功能。


结语


本文粗略的讲解了 LocatorJs 在 React 框架的原理实现,算是抛砖引玉,供大家参考。



篇幅原因,略过很多细节,感兴趣的朋友建议看看源码,结合调试学习



我是饮东,欢迎点赞关注,江湖再见


作者:饮东
来源:juejin.cn/post/7358274599883653120
收起阅读 »

这个字符串”2*(1+3-4)“的结果是多少

web
大家好,我是火焱。 前两天,在抖音上刷到一个计算器魔术,很有意思。 于是拿出手机尝试,发现不太对,为什么我的计算器直接把输入的内容都展示出来了?看评论区发现很多人都有类似的问题。 既然自带的计算器不好使,那就用小程序写一个。 产品描述 计算器的显示区只展示...
继续阅读 »

大家好,我是火焱


前两天,在抖音上刷到一个计算器魔术,很有意思。



于是拿出手机尝试,发现不太对,为什么我的计算器直接把输入的内容都展示出来了?看评论区发现很多人都有类似的问题。



既然自带的计算器不好使,那就用小程序写一个。


产品描述


计算器的显示区只展示当前的数字,如果按了运算符(+ - * /),再输入数字时,展示当前的新数字,不展示之前输入的内容,按等于(=)号后,展示计算结果。


从程序员视角看,按等于(=) 时,我们拿到的是四则运算的字符串,比如:"1 + 2 * 3 - 4",然后通过代码计算这个字符串的结果,那么如何计算呢?


初步尝试


对于 javascript,很容易想到通过 eval 或者 new Function 实现,可是小程序...


image.png


既然捷径走不通,那就用逆波兰表达式来解决,我们来看下表达式的三种表示方法。


三种表示


中缀表达式,就是我们常用的表示方式:1 + 2 * 3 - 4


前缀表达式,也叫波兰表达式,是把操作符放到操作数前边,表示成:- + 1 * 2 3 4,由于后缀表达式操作起来比较方便,我们重点看下后缀表达式;


后缀表达式,也叫逆波兰表达式,它是把操作符放到操作数后边,表示成:1 2 3 * + 4 -,有了后缀表达式,我们就可以很容易计算结果了,那如何将中缀表达式转化成后序表达式呢?语言表述比较乏力,直接看代码吧,逻辑比较清晰:


/** 中缀表达式 转 后缀表达式 */
function infixToPostfix(infixExpression) {
let output = [];
// 存放运算符
let stack = [];

for (let i = 0; i < infixExpression.length; i++) {
let char = infixExpression[i];

if (!isOperator(char)) { // char 是数字
output.push(char);
} else { // char 是运算符
while (
// 栈不为空
stack.length > 0 &&
// 栈顶操作符的优先级不小于 char 的优先级
getPrecedence(stack[stack.length - 1]) >= getPrecedence(char)
) {
output.push(stack.pop());
}
stack.push(char);
}
}

// 将剩余的运算符弹出并追加到 output 后边
while (stack.length > 0) {
output.push(stack.pop());
}

return output.join('');
}


结合下图理解一下:


表达式1 + 2 * 3 - 4


image.png


处理括号


带括号的表达式,处理逻辑和不带括号是一样的,只是多了对括号的处理。当遇到右括号时,需要把栈中左括号后面的所有运算符弹出,并追加到 output,举个例子:


计算:2 * ( 1 + 3 - 4)


image.png


通过这个例子,我们可以看出,后缀表示法居然不需要括号,更简洁。


好了,现在已经有了后序表达式,我们如何的到计算结果呢?


计算结果


计算这一步其实比较简单,直接上代码吧:


const operators = {
'+': function (a, b) { return a + b; },
'-': function (a, b) { return a - b; },
'*': function (a, b) { return a * b; },
'/': function (a, b) { return a / b; }
};

const stack = [];
postfixTokens.forEach(function (token) {
if (!isNaN(token)) {
stack.push(token);
} else if (isOperator(token)) {
var b = stack.pop();
var a = stack.pop();
stack.push(operators[token](a, b));
}
});

总结


中缀表达式对于人比较友好,而后缀表达式对计算机友好,通过对数字和运算符的编排即可实现带优先级的运算。如果本文对你有帮助,欢迎点赞、评论。


参考代码:github.com/laohuoyan/m…




作者:程序员火焱
来源:juejin.cn/post/7294441582983528484
收起阅读 »

关于我在uni-app中踩的坑

web
前言 这段时间刚入坑uni-app小程序,本人使用的编辑器是VScode(不是HbuliderX!!!),在此记录本人所踩的坑 关于官方模板 我采用的是官方提供的Vue3+Vite+ts模板,使用的包管理工具是pnpm。大家可以使用npx下载模板 $ npx ...
继续阅读 »

前言


这段时间刚入坑uni-app小程序,本人使用的编辑器是VScode(不是HbuliderX!!!),在此记录本人所踩的坑


关于官方模板


我采用的是官方提供的Vue3+Vite+ts模板,使用的包管理工具是pnpm。大家可以使用npx下载模板


$ npx degit dcloudio/uni-preset-vue#vite-ts my-vue3-project

当然不出意外,大家下载都是失败的
So这里附上官方gitee下载地址 点击前去下载

下载解压后运行pnpm i,如果有报错可以尝试切换node版本。


微信小程序开发


第一步注册账号 小程序 (qq.com),按官方所需填写即可。


第二步,登录你的小程序账号,在开发->开发管理->开发设置,获取你的AppID(小程序ID)


第三步,在你的项目工程文件里找到manifest.json中的小程序相关填写你上一步获取的AppID


 "mp-weixin": {
"appid": "替换你的小程序ID",
"setting": {
"urlCheck": false
},
"usingComponents": true
},

然后终端运行pnpm run dev:mp-weixin
然后会生成一个dist目录,这里存放的是编译成微信小程序的源码


第四步,下载安装微信小程序开发工具 微信开发者工具下载地址


第五步,打开并登录微信小程序开发工具,选择导入项目,选择刚刚生成的dist目录下的mp-weixin即可


image.png
成功界面如图
image.png


关于node版本


让我们来看看人家官方是怎么说的



注意



  • Vue3/Vite版要求 node 版本^14.18.0 || >=16.0.0

  • 如果使用 HBuilderX(3.6.7以下版本)运行 Vue3/Vite 创建的最新的 cli 工程,需要在 HBuilderX 运行配置最底部设置 node路径 为自己本机高版本 node 路径(注意需要重启 HBuilderX 才可以生效)



    • HBuilderX Mac 版本菜单栏左上角 HBuilderX->偏好设置->运行配置->node路径

    • HBuilderX Windows 版本菜单栏 工具->设置->运行配置->node路径





当然想要把这个官方模板跑起来还真是不容易(T-T),为什么这么说呢,本人使用node18居然跑不起来,按理说应该是可以的,but我最后选择将node版本降到node16,在前端中我们会经常切换node,小编在这里要强推nvm(一款node版本管理工具),本文不在这里着重介绍,贴心的小编已经为大家附上了nvm的下载地址 点击前去下载


关于easycome配置


对于熟悉前端的小伙伴来说,自定义组件是家常便饭啦,uniapp内置easycom,用于自动导入自己和第三方的组件
首先我们找到pages.json文件,输入(cv)以下代码


 "easycom": {
"autoscan": true,
"custom": {
// uni-ui 规则如下配置
"^uni-(.*)": "@dcloudio/uni-ui/lib/uni-$1/uni-$1.vue",
//自定义规则
"^Xtx(.*)":"@/components/Xtx$1.vue"
}
},

自动查找以uni、Xtx开头的Vue文件,一定要注意规则,否则可能导致导入失败,写完后可以在导入的组件中log一下,判断是否导入成功,配置easycom后无需手动导入组件


关于uni-helper插件


如果你想增加在uni-app中开发体验,你可以选择uni-helper插件,首先确保你在vscode中安装了Vue Language Features (Volar)以及TypeScript Vue Plugin (Volar)插件,这俩插件提供Vue高亮显示和ts语法支持。
安装vscode uni-helper相关插件


image.png


image.png
然后安装3个包


$ pnpm i -D @uni-helper/uni-app-types
$ pnpm i -D @uni-helper/uni-cloud-types
$ pnpm i -D @uni-helper/uni-ui-types

接着在tsconfig.json中将3种类型应用。在compilerOptions的types中添加。配置如下:


{
...
"compilerOptions": {
...
"types": [
"@dcloudio/types",
"@uni-helper/uni-app-types",
"@uni-helper/uni-ui-types",
...
],
}
}


诶,这怎么原生标签报错了呢?别急,出现这个错误是因为unihelp的类型与原生发生了冲突,我们只需要在compilerOptions同级增加以下代码即可解决此问题


{
...
"compilerOptions": {
...
"types": [
"@dcloudio/types",
"@uni-helper/uni-app-types",
"@uni-helper/uni-ui-types",
...
],
}
//增加vueCompilerOptions配置项
"vueCompilerOptions": {
"nativeTags": ["block", "component", "template", "slot"]
},
}


避坑热重载


经过小编的测试发现,把微信开发者工具的自动保存和热重载关闭后,居然可以自动同步代码,起因是一天小编正苦于添加了请求拦截器却无法响应,偶然重新编译后发现可以拦截,于是考虑是否代码没更新,一看源码,果然如此,这里不知道是工具的bug还是vscode编译的bug。有了解的小伙伴可以在评论区留一下言。总之就是踩了很多坑(QWQ)


作者:彼日花
来源:juejin.cn/post/7286762580876902441
收起阅读 »

微信小程序:轻松实现时间轴组件

web
效果图 引言 老板: “我们公司是做基金的,用户都在买买买,可他们的钱去了哪里?没有时间轴,用户会不会觉得自己的钱瞬移了?” 你: “哈哈,确实!时间轴就像用户的投资地图,不然他们可能觉得钱被外星人劫走了。” 老板: “没错!我们得在时间轴上标清‘资金到账...
继续阅读 »

效果图


企业微信截图_17230101989794-imageonline.co-merged.png


引言



老板: “我们公司是做基金的,用户都在买买买,可他们的钱去了哪里?没有时间轴,用户会不会觉得自己的钱瞬移了?”


你: “哈哈,确实!时间轴就像用户的投资地图,不然他们可能觉得钱被外星人劫走了。”


老板: “没错!我们得在时间轴上标清‘资金到账’、‘收益结算’这些节点,这样用户就不会担心他们的钱去买彩-票了。”


你: “放心吧,老板,我马上设计一个时间轴,让用户一看就明白他们的钱在干什么,还能时不时地笑一笑!”


老板: “好,赶紧行动,不然用户要开始给我们寄失踪报告了!”



废话不多说,我们直接开始吧!!!


组件定义


以下代码为时间轴组件的实现,详细注释在代码中。如果有任何疑问,欢迎在评论区留言讨论,或者联系我获取完整案例。


组件的 .js 文件:


/*可视化地呈现时间流信息*/
Component({
 options: {
   multipleSlots: true // 在组件定义时的选项中启用多slot支持
},
 properties: {
   activities: { // 时间轴列表
     type: Array,
     value: []
  },
   shape: { // 时间轴形状
     type: String,
     value: 'circle' // circle | square
  },
   ordinal: { // 是否显示序号
     type: Boolean,
     value: true
  },
   reverse: { // 是否倒序排列
     type: Boolean,
     value: false
  }
},
 lifetimes: {
   attached() {
     // 是否倒序排列操作数据
     const {reverse, activities} = this.data
     if (!reverse) return
     this.setData({
       activities: activities.reverse()
    })
  }
}
})

组件的.wxml文件:


<view class="container">
 <view class="item" wx:for="{{activities}}" wx:key="item">
   <view class="item-tail"></view>
   <view class="item-node {{shape}} {{item.status}}">
     <block wx:if="{{ordinal}}">{{index + 1}}</block>
   </view>
   <view class="item-wrapper">
     <view class="item-news">
       <view class="item-timestamp">{{item.date}}</view>
       <view class="item-mark">收益结算</view>
     </view>
     <view class="item-content">
       <view>{{item.content}}</view>
       <!--动态slot的实现方式-->
       <slot name="operate{{index}}"></slot>
     </view>
   </view>
 </view>
</view>

组件使用


要使用该组件,首先需要在 app.jsonindex.json 中引用组件:


"usingComponents": {
"eod-timeline": "/components/Timeline/Timeline"
}

然后你可以通过以下方式进行基本使用:


<eod-timeline activities="{{dataList}}" ordinal="{{true}}"></eod-timeline>

如果需要结合插槽动态显示操作记录,可以这样实现:


<eod-timeline activities="{{dataList}}" ordinal="{{true}}">
   <!--动态slot的实现方式-->
   <view wx:for="{{dataList}}" wx:for-index="idx" wx:key="idx" slot="operate{{idx}}">
     <view class="row-operate">
       <view>操作记录</view>
       <view>收益记录</view>
       <view>动账记录</view>
     </view>
   </view>
</eod-timeline>

数据结构与属性说明


dataList 数据结构示例如下:


dataList:[
{date: '2023-05-26 12:04:14', status: 'info', content: '内容一'},
{date: '2023-05-25 12:04:14', status: 'success', content: '内容二'},
{date: '2023-05-24 12:04:14', status: 'success', content: '内容三'},
{date: '2023-05-23 12:04:14', status: 'error', content: '内容四'},
{date: '2023-05-22 12:04:14', status: 'warning', content: '内容五'}
]

组件的属性配置如下表所示:


参数说明可选值类型默认值
activities显示的数据array
shape时间轴点形状circle / squarestringcircle
ordinal是否显示序号booleantrue
reverse是否倒序排列booleanfalse

总结


这个时间轴组件提供了一个简单易用的方式来展示事件的时间顺序。组件支持定制形状、序号显示以及正序或倒序排列,同时允许通过插槽自定义内容,增强了组件的灵活性。代码中有详细注释,方便理解和修改。如果需要更详细的案例或有任何疑问,请在评论区留言。希望这篇文章对你有所帮助!


拓展阅读


关于动态 Slot 实现:


由于动态 slot 目前仅可用于 glass-easel 组件框架,而该框架仅可用于 Skyline 渲染引擎,因此这些特性也同样受此限制。如果需要在非 glass-easel 组件框架中实现动态 slot,请参考上文标记了 <!--动态slot的实现方式--> 的代码段。


如需了解更多关于 glass-easel 组件框架的信息,请参阅微信小程序官方开发指南


作者:一点一木
来源:juejin.cn/post/7399983901812604980
收起阅读 »

这些天,我们前端组一起处理的网站换肤功能

web
前言  大家好,我是沐浴在曙光下的贰货道士,好久不见,别来无恙!本文主要根据UI设计需求,讲述一套基于scss封装方法的网页响应式布局,以及不同于传统引入element UI主题配色文件的换肤思路。大家仔细看完文章,相信一定会有所收获。倘若本文提供的响应式布局...
继续阅读 »

前言

  大家好,我是沐浴在曙光下的贰货道士,好久不见,别来无恙!本文主要根据UI设计需求,讲述一套基于scss封装方法的网页响应式布局,以及不同于传统引入element UI主题配色文件的换肤思路。大家仔细看完文章,相信一定会有所收获。倘若本文提供的响应式布局思路对您有所帮助,烦请大家一键三连哦。同时如果您有其他响应式布局解决方案或网站换肤思路,欢迎您不吝赐教,在评论区留言分享。感谢大家!

需求分析

  • 早期我们前端项目组开发了一个国外业务网站。这周为了迎合其他国家的喜好,需要在国外业务项目的基础上,新建多个项目,对之前的主题配色和部分布局进行修改,并需要适配不同分辨率下的屏幕。UI提供了包括主题配色页面布局修改在内的一系列项目稿件,这些稿件基于1920px分辨率的屏幕进行处理。前端需要根据UI提供的主题色,修改项目中的颜色变量。接口暂时使用国外业务的那一套接口,后期需要对接这些项目的接口,而我们目前的主要任务就是处理这些项目的静态页面改版。
  • 主题色修改:

    1. 首先,我们前端团队需要根据UI提供的主题色,更新项目中的颜色变量,确保页面上的所有元素都符合新的配色方案。
    2. 页面布局提供的修改稿件,如果有不在主题色内的颜色,需要和UI确认是否需要更换为其他颜色相近的主题配色或者双方都新增主题配色
    3. 检查项目中包括CSSHTML在内的所有带#颜色值的信息。与UI确认后,将其更换为其他颜色接近的主题配色,或者双方共同新增主题配色,以确保配色方案的一致性和协调性。
  • 响应式布局:

    1. 前端需要根据UI提供的稿件和意见,适配项目在不同屏幕下的样式。对于页面上的不同元素,在小于等于1920px的屏幕上进行缩放时,需要保持横纵比,并根据页面大小进行等比例缩放,包括容器宽高、间距等在内的页面布局是否合适都需要与UI确认;在高于1920px屏幕的设备上,需要保持和1920px屏幕的布局风格,即元素的宽高不变。
    2. 然而,字体元素在页面缩放时,需要保持一定的风格。比如:16px的文字最小不能低于14px18px20px以及24px的文字最小不能低于16px32px的文字最小不能低于18px36px的文字最小不能低于20px44px的文字最小不能低于28px48px的文字最小不能低于32px
    3. 在移动设备上,需要保持和800px网页相同的布局。

项目现状

  • 主题色: 早期在与UI团队合作时,我们为国外业务系统确定了一套配色方案,并将其定义在项目的颜色变量中。然而,后续设计稿中出现了一些不在这套配色方案中的色值。 由于种种原因,我们在开发时没有与UI确认这些颜色是否需要更换,也没有将新增的颜色定义到颜色变量中,而是直接在代码中使用了这些颜色值。这导致在此次换肤过程中,仅通过修改颜色变量无法实现统一换肤的效果。我们需要逐一检查代码中硬编码的颜色值,并将其替换为新的颜色变量,以确保换肤的统一性和一致性。
  • 布局: 以前我们使用flex、百分比、最小最大宽度/高度以及element UI的栅格布局做了一些简单的适配,但这些方法不够灵活。为了更好地适应不同分辨率的屏幕,我们需要采用更为灵活和动态的布局方案,以确保在各种设备上的显示效果都能达到预期。

思路分析

主题色

传统的解决方案

  1. 以前在官网上,我们可以直接编辑并修改一套主题色。点击下载后,会生成一个css文件。

    image.png
  2. 将下载后的css文件引入到我们项目中,可以看到编译后的css文件

    image.png
  3. 最后在项目中的入口文件,引入我们下载的css文件(这种方式会增加app.css的体积)。
`main.js`

import '@/styles/theme/index.css'
  1. 后续处理的优化
`将编译后的element样式,从main.js指向到index.html中,减小了main.css体积`
`main.js中的css文件,最终还是会link到index.html中。那为什么还要把它拆开呢?`
`这涉及到css的拆分:浏览器会并行请求加载多个css文件,比单独请求并加载一个css文件要快`
`这样处理的目的是:将main.js中的css文件,抽出一部分放到index.html中`

<link rel="stylesheet" href="<%= BASE_URL %>theme/index.css">
  1. webpack小知识:loader
  • webpack 只识别js文件:当遇到其他非js文件时,因为不识别js文件,所以需要使用loader插件(或引入第三方插件,或自己编写一个loader方法),将其他文件转换为webpack能够识别的js文件。
  • 因此,loader的作用相当于对传入的非js文件做处理,将它转换为 webpack 可识别的js字符串。
  1. 在字体商用不侵权的前提下,严格遵循设计稿的字体样式
`如果用户电脑不存在设计稿上提供的字体样式,则会展示用户电脑的默认字体样式。`
`为此,我们需要下载并引入字体,将字体集成到网站中,确保用户电脑呈现效果与我们开发一致`
`(1) 引入: 在public文件夹下新建fonts文件夹,在fonts文件夹下引入我们下载好的字体样式`
`(2) 在index.html中, 为document增加字体`
`(3) 引入并挂载字体后,我们就可以使用下载的字体了,也可以在body上全局挂载字体`

`类似element字体的引入和挂载`
`FontFace: https://developer.mozilla.org/zh-CN/docs/Web/API/CSS_Font_Loading_API`

const font1 = new FontFace(
'iconfont',
'url(/iconfont/iconfont.woff2?t=1688345853791),
url(/iconfont/iconfont.woff?t=1688345853791),
url(/iconfont/iconfont.ttf?t=1688345853791)'
)

const font2 = new FontFace(
'element-icons',
'url(/theme/fonts/element-icons.woff),
url(/theme/fonts/element-icons.ttf)'
)

font1.load().then(function() {
document.fonts.add(font1)
})

font2.load().then(function() {
document.fonts.add(font2)
})

现在的解决方案

  由于element UI官方已不再维护传统的主题配色下载,我们项目采取官方提供的第二种方式:

  1. 原理: 我们项目使用scss编写csselement UItheme-chalk又恰好使用scss进行编写。在官方定义的scss变量中,使用了!default语法,用于提供默认值。这也就意味着,我们不用考虑css的加载顺序,直接新建scss文件,覆盖定义在theme-chalk文件且在我们系统中常用的scss变量,达到在css编译阶段自定义主题scss变量的效果。
image.png
  1. 引入变量: 新建element-variable.scss文件,在这个文件中引入theme-chalk定义的主题scss变量,同时需要改变icon字体路径变量(使用传统方法不需要改变路径变量,是因为我们直接引入了编译后的css文件,里面已经帮我们做过处理了;而使用现在的解决方案,如果不改变字体路径变量,项目会提示找不到icon字体路径,所以这个配置必填)。此时,将这个文件引入到我们的入口文件,那么系统中已经存在theme-chalk定义好的scss变量了
d227267f21a77a05a67159b1d71ae43a.png
  1. 修改变量: 新建element.scss文件,在里面覆盖我们需要修改的主题变量,最后在vue.config.jssass配置下的additionalData里全局引入到项目中的每个vue文件中(因为是挂载到每个vue文件中,所以这个配置下的scss文件不宜过多),方便在vue文件中直接使用变量。
image.png image.png

优势

1. 定制化和灵活性

  • 更改主题色和变量: 轻松改变Element UI的主题色、字体、间距等变量,而无需过多地覆盖现有的element CSS样式。
  • 精细控制: 原先的配置方式只能配置主题色,无法控制更细粒度的配置,比如边框颜色之类。

2. 避免样式冲突

  • 避免样式覆盖的冲突: 通过直接修改SCSS变量来定制样式,可以避免在使用编译后的 CSS 文件时可能出现的样式覆盖冲突问题。这样可以保证样式的独立性和一致性。

3. 便于维护

  • 集中管理: 所有的样式修改都集中在一个地方(变量文件),这使得维护样式变得更加方便和清晰。只需要修改文件中定义的变量,就可以影响整个项目中的样式,无需逐一查找以及修改每个组件的样式。

缺陷

  • sass loaderadditionalData中配置了过多的全局css变量,添加到每个vue文件中
  • 相比之前的处理方式,在main.js中引入element自定义的主题scss变量,首页加载的css文件更多,

响应式布局

思路分析

  1. UI提供的稿件是1920px,前端需要对UI提供的稿件进行一比一还原;
  2. 网页在小屏缩放时,需要保持元素的横纵比。针对这个问题,我们可以用百分比作为布局单位。 以设计稿宽度1920px为基准,建立pxvw之间的关系。如果把1920px视为100vw,那么1vw = 19.2px。 如果设计稿上某个元素的宽度为192px, 那么将它换算得到的结果将会是192px / 19.2px * 1vw = 10vw。因此我们在布局时,需要严格遵循UI提供的设计稿件,并借助下文封装的方法,将设计稿元素的像素作为第一个形参,传递到下文封装的方法中; 实现思路:为等比例缩放网页元素,先去掉传入的像素单位。最后使用前文提到的换算公式,不论宽高,都将其转换为vw单位,等比缩放
  3. 字体页面元素在放大时,需要限制字体元素展现的最大阈值。 那么我们封装的方法,第二个形参需要控制字体元素的最大阈值; 实现思路:借助scss中的max方法实现。
  4. 字体页面元素在缩小时,需要限制字体元素展现的最小阈值。 那么我们封装的方法,第三个形参需要控制字体元素的最小阈值; 实现思路:借助scss中的min方法实现。
  5. 在高于1920px屏幕的设备上,需要保持和1920px屏幕的布局风格,即元素的宽高不变。 针对这个问题,我们只需要保证方法中的max形参和1920px下的像素值一致,即保证方法中的第一个形参和第二个形参相同。
  6. 在移动设备上,需要使用800px的网页布局。针对这个问题,我们可以使用meta标签进行适配:  
  7. 不同屏幕下的元素显示势必不会那么完美。我们可以通过媒体查询,在不同分辨率的屏幕下,按照UI给定的反馈意见,对网页进行适配,这样就可以解决问题。但是在项目中大量使用媒体查询语法,会导致整个项目看上去很乱。为此,我们可以基于scss语法,对媒体查询语法进行二次封装。
  8. 如何测试我们编写的scss代码? 移步sass在线调试
image.png

自适应scss方法封装

// 自定义scss函数, 作用是去掉传入变量的单位
// 之所以要去掉单位,是为了将传入的px转换为vw单位,自适应布局`

@function stripUnits($value) {

// 对带有单位的变量进行特殊处理,返回去掉单位后的结果`
// 对于scss来说, 90px和90都是number`
// 在scss中,unitless是一个术语,指的是没有单位的数值,not unitless就是变量带单位`

@if type-of($value) == 'number' and not unitless($value) {
// 90px / 1 得到的结果是90px, 90px / 1px得到的结果是90
// 这也是这里为什么要用($value * 0 + 1),而不是直接写1的原因`

@return $value / ($value * 0 + 1);
}
@return $value;
}

/*
自定义scss函数,提供三个参数:
第一个参数是设计稿提供的元素大小,传入会自动转换为vw单位,达到自适应的效果
第二个参数是用来约束这个元素的大小最大不能超过第一个参数和第二个参数的最大值, 必须带单位
第三个参数是用来约束这个元素的大小最小不能小于第一个参数和第三个参数的最小值,必须带单位
如果不传入第二个和第三个参数,则表示元素完全随屏幕响应式缩放

应用场景:
1. 1920px下标题的字体是48px,当屏幕分辨率为960px时,标题字号缩放为24px,起不到突出的作用。
于是我们可以给它设置一个最小阈值,比如最小不能小于32px;
2. 同理,当屏幕分辨率为3840px时,标题字号放大为96px,我们不希望字号这么大。
于是可以给它设置一个最大阈值,比如最大不能超过60px。
*/


@function auto($raw, $max:null, $min:null) {
$raw: stripUnits($raw);
$str: #{$raw / $proportion}vw;
@if $max {
$str: min(#{$str}, #{$max});
}
@if $min {
$str: max(#{$str}, #{$min});
}
@return $str;
}

/*
自定义scss函数,auto方法的二次封装, 提供两个参数
第一个参数用于设置1920px下的元素大小
第二个参数用于设置这个元素的最小值

应用场景:
1. 1920px下标题的字体是48px,当屏幕分辨率为3840px时,标题字号放大为96px,我们希望它保持48px大小,
于是我们可以给它设置一个最大阈值48px。同时,我们可以传入一个最小阈值,让它最小不能小于这个参数。
*/


@function autoMax($raw, $min:null) {
@return auto($raw, $raw, $min)
}

// 和上面相反

@function autoMin($raw, $max:null) {
@return auto($raw, $max, $raw)
}

//1vw = 1920 / 100 ;
$proportion: 19.2;

// 根据UI需求,对不同字体大小进行封装
$wb-font-size-mini: 16px; // $text-mini-1
$wb-font-size-extra-small: 18px; // $text-small-1
$wb-font-size-small: 20px; //$text-sm-md-1
$wb-font-size-base: 24px; //$text-medium-1
$wb-font-size-lesser-medium: 32px;
$wb-font-size-medium: 36px; //$text-large-1
$wb-font-size-extra-medium: 44px;
$wb-font-size-large: 48px; //$text-title-1

// 根据UI需求,在屏幕分辨率缩小时,字体响应式变化,并设定最小阈值
// 并在1920px以上的屏幕,保持和1920px一样的字体大小

$wb-auto-font-size-mini: autoMax($wb-font-size-mini, 14px);
$wb-auto-font-size-extra-small: autoMax($wb-font-size-extra-small, 16px);
$wb-auto-font-size-small: autoMax($wb-font-size-small, 16px);
$wb-auto-font-size-base: autoMax($wb-font-size-base, 16px);
$wb-auto-font-size-lesser-medium: autoMax($wb-font-size-lesser-medium, 18px);
$wb-auto-font-size-medium: autoMax($wb-font-size-medium, 20px);
$wb-auto-font-size-extra-medium: autoMax($wb-font-size-extra-medium, 28px);
$wb-auto-font-size-large: autoMax($wb-font-size-large, 32px);
// 严格按照UI稿件提供的元素大小、间距编写代码,以下是示例代码

.title {
padding: 0 autoMax(180px);
font-size: $wb-auto-font-size-large;
font-weight: 600;
text-align: center;
}
image.png

媒体查询语法封装及使用规范

// 导入scss的list和map模块,用于处理相关操作。

@use 'sass:list';
@use "sass:map";

/*
媒体查询映射表,定义各种设备类型的媒体查询范围
key为定义的媒体类型,value为对应的分辨率范围
*/


$media-list: (
mobile-begin: (0, null),
mobile: (0, 800),
mobile-end:(null, 800),
tablet-begin: (801, null),
tablet: (801, 1023),
tablet-end:(null, 1023),
mini-desktop-begin: (1024, null),
mini-desktop: (1024, 1279),
mini-desktop-end: (null, 1279),
small-desktop-begin: (1280, null),
small-desktop: (1280, 1439),
small-desktop-end: (null, 1439),
medium-desktop-begin: (1440, 1919),
medium-desktop: (1440, 1919),
medium-desktop-end: (null, 1919),

large-desktop-begin: (1920, null),
large-desktop: (1920, 2559),
large-desktop-end: (null, 2559),

super-desktop-begin: (2560, null),
super-desktop: (2560, null),
super-desktop-end: (2560, null)
);

/*
创建响应式媒体查询的函数,传参是媒体查询映射表中的媒体类型
从$media-list中获取对应的最小和最大宽度,并返回相应的媒体查询字符串。
*/


@function createResponsive($media) {
$size-list: map.get($media-list, $media);
$min-size: list.nth($size-list, 1);
$max-size: list.nth($size-list, 2);
@if ($min-size and $max-size) {
@return "screen and (min-width:#{$min-size}px) and (max-width: #{$max-size}px)";
} @else if ($max-size) {
@return "screen and (max-width: #{$max-size}px)";
} @else {
@return "screen and (min-width:#{$min-size}px)";
}
}

/*
这个混入接受一个或多个媒体类型参数,调用createResponsive函数生成媒体查询
@content是Scss中的一个占位符,用于在混入中定义块级内容。
它允许你在调用混入时,将实际的样式代码插入到混入定义的样式规则中。
*/


@mixin responsive-to($media...) {
@each $item in $media {
$media-content: createResponsive($item);
@media #{$media-content} {
@content;
}
}
}

// 以下是针对各种媒体类型定义的混入:

@mixin mobile() {
@include responsive-to(mobile) {
@content;
}
}

@mixin tablet() {
@include responsive-to(tablet) {
@content;
}
}

@mixin mini-desktop() {
@include responsive-to(mini-desktop) {
@content;
}
}

@mixin small-desktop() {
@include responsive-to(small-desktop) {
@content;
}
}

@mixin medium-desktop() {
@include responsive-to(medium-desktop) {
@content;
}
}

@mixin large-desktop() {
@include responsive-to(large-desktop) {
@content;
}
}

@mixin super-desktop() {
@include responsive-to(super-desktop) {
@content;
}
}

@mixin mobile-begin() {
@include responsive-to(mobile-begin) {
@content;
}
}

@mixin tablet-begin() {
@include responsive-to(tablet-begin) {
@content;
}
}

@mixin mini-desktop-begin() {
@include responsive-to(mini-desktop-begin) {
@content;
}
}

@mixin small-desktop-begin() {
@include responsive-to(small-desktop-begin) {
@content;
}
}

@mixin medium-desktop-begin() {
@include responsive-to(medium-desktop-begin) {
@content;
}
}

@mixin large-desktop-begin() {
@include responsive-to(large-desktop-begin) {
@content;
}
}

@mixin super-desktop-begin() {
@include responsive-to(super-desktop-begin) {
@content;
}
}


@mixin mobile-end() {
@include responsive-to(mobile-end) {
@content;
}
}

@mixin tablet-end() {
@include responsive-to(tablet-end) {
@content;
}
}

@mixin mini-desktop-end() {
@include responsive-to(mini-desktop-end) {
@content;
}
}

@mixin small-desktop-end() {
@include responsive-to(small-desktop-end) {
@content;
}
}

@mixin medium-desktop-end() {
@include responsive-to(medium-desktop-end) {
@content;
}
}

@mixin large-desktop-end() {
@include responsive-to(large-desktop-end) {
@content;
}
}

@mixin super-desktop-end() {
@include responsive-to(super-desktop-begin) {
@content;
}
}
image.png

需求解决思路:

  • 根据提供的设计稿,使用autoMax系列方法,对页面做初步的响应式布局适配
  • 针对不同屏幕下部分元素布局需要调整的问题,使用封装的媒体查询方法进行处理

书写规范:

  为避免项目中的scss文件过多,搞得整个项目看上去很臃肿,现提供一套书写规范:

  • 在每个路由下的主index.vue文件中,引入同级文件夹scss下的media.scss文件


// 小屏状态下,覆盖前面定义的css样式


  • media.css文件

  写法:以vue文件最外层的类进行包裹,使用deep穿透,以屏幕分辨率大小作为排序依据,从大到小书写媒体查询样式

.about-wrapper::v-deep {
@include small-desktop {
.a {
.b {

}
}
}

@include mini-desktop {
.a {
.b {

}
}
}

@include tablet-end {
.a {
.b {

}
}
}
}

结语

  感谢掘友们耐心看到文末,希望你们不是一路跳转至评论区,我们江湖再见!


作者:沐浴在曙光下的贰货道士
来源:juejin.cn/post/7388753413309775887

收起阅读 »

淘宝、京东复制好友链接弹出商品详情是如何实现的

web
前言: 最近接到了一个需求很有意思,类似于我们经常在逛购物平台中,选择一个物品分享给好友,然后好友复制这段文本打开相对应的平台以后,就可以弹出链接上的物品。实现过程也比较有意思,特来分享一下实现思路🎁。 一. 效果预览 当我在别的界面复制了内容以后,回到主应用...
继续阅读 »

前言: 最近接到了一个需求很有意思,类似于我们经常在逛购物平台中,选择一个物品分享给好友,然后好友复制这段文本打开相对应的平台以后,就可以弹出链接上的物品。实现过程也比较有意思,特来分享一下实现思路🎁。


一. 效果预览


当我在别的界面复制了内容以后,回到主应用,要求可以检测到当前剪切板是什么内容。

1.gif


二. 监听页面跳转动作



  1. 要完成这个需求,整体思路并不复杂。首先我们要解决的就是如何检测到用户从别的应用切回到我们自己的应用。

  2. 这个听起来很复杂,但其实浏览器已经提供了相对应的 api 来帮我们检测用户这个操作----document.visibilitychange
    image.png

  3. 那么我们就可以写下如下代码


    document.addEventListener("visibilitychange", () => {
    console.log("用户切换了");
    });

    相对应的效果如下图所示,你可能会好奇,我明明只切换了一次,但是为什么控制台却执行了两次打印?

    2.gif

    这也不难理解,首先你要理解这个 change 这个动作,你从 tab1 切换到 Tab2 的时候,触发了当前 Tab1可见 变为=> 不可见

    而当你从 tab2 切回 tab1 的时候,触发了当前 Tab1不可见变为了可见。完整动作引起了状态两次变化,所以才有了两次打印。


  4. 而我们的场景只是希望 app 从不可见转变为可见的时候才触发。那么我们就需要用到��外一个变量来配合使用-------document.visibilityState
    image.png
    这个值是一个 document 对象上的一个只读属性,它有三个 string 类型的值 visiblehiddenprerender 。从它的使用说明中不难看出,我们要使用的值是 visible



    tips:hidden 可以用来配合做一些流量控制优化,当用户切换网页到后台的时候,我们可以停止一些不必要的轮询任务,待用户切回后再开启。




  5. 那么我们现在的代码应该是这样的:


    document.addEventListener("visibilitychange", () => {
    if (document.visibilityState === "visible") {
    console.log("页面变得可见了!");
    }
    });

    可以看到,现在控制台正确的只执行了一次。

    3.gif



三. 完成读取剪切板内容



  1. 要完成读取剪切板内容需要用到浏览器提供的另外一个 api-----navigator.clipboard。这里穿插一个英语记忆的小技巧,我们要把这个单词分成两部分记忆:clipboardclip 本身就有修剪的意思,board 常作为木板相近的含义和别的单词组合,如:黑板 blackboard棋盘 chessboard。所以这两个单词组合起来的含义就是剪切板。

    image.png

  2. 这里需要注意一句话,这个功能只能用在安全上下文中。这个概念很抽象,如果想深入了解的话,还需自行查阅资料。这里指简单说明这句话的限制:要想使用这个 api 你只能在 localhost、127.0.0.1 这样的本地回环地址或者使用 https 协议的网站中使用。

    image.png

  3. 要快速检测当前浏览器或者网站是否是安全上下文 ,可以使用 Window:isSecureContext 属性来判断。

  4. 你可以动手访问一个 http 的网站,然后在控制台打印一下该属性,你大概率会看到一个 false,则说明该环境不是一个安全上下文,所以 clipboard 在这个环境下大概率不会生效。因为本文章代码都为本地开发(localhost),所以自然为安全上下文
    image.png

  5. 经过上面的知识,那么我们就可以写出下面的兼容性代码。

    image.png

  6. 前置步骤都已经完成,接下来就是具体读取剪切板内容了。关于读取操作,clipboard 提供了两个 api-----readreadText。这里由于我们的需求很明确,我读取的链接本身就是一个字符串类型的数据,所以我们就直接选用 readText 方法即可。稍后在第四章节我会介绍 read 方法。

  7. clipboard 所有操作都是异步会返回一个 Promise 类型的数据的,所以这里我们的代码应该是这样的:


    document.addEventListener("visibilitychange", () => {
    if (document.visibilityState === "visible") {
    if (window.isSecureContext && navigator.clipboard) {
    const clipboardAPI = navigator.clipboard; //获取 clipboard 对象
    setTimeout(() => {
    clipboardAPI.readText().then((text) => {
    console.log("text", text);
    });
    }, 1000);
    } else {
    console.log("不支持 clipboard");
    }
    }
    });



    注意⚠️:这里你会看到我使用了 setTimeout 来解决演示的问题,如果你正在跟着练习但是不明白原因,请查看下面链接:

    关于 DOM exception: document is not focused 请查阅stackoverflow 文档未聚焦的解决方案



    相应的效果如下图所示,可以看到我们已经可以正确读取刚刚剪切板的内容了。

    QQ20240629-144157.gif


  8. 此时,当拿到用户剪切板的内容以后,我们就可以根据某些特点来判断弹窗了。这里我随便使用了一个弹出组件来展示效果:

    1.gif

  9. 什么?到这里你还是没看懂和网购平台链接之间有什么关系?ok,让我们仔细看一下我分别从两家平台随手复制的两个链接,看出区别了吗?开头的字符串可以很明显看出是各家的名字。

    image.png

  10. 那么我只需判断用户剪切板上的字符串是否符合站内的某项规则不就行了吗?让我来举个更具体的栗子,下面链接是我掘金的个人首页,假如用户此时复制了这段文本,然后跳转回我们自己的应用后,刚刚的代码就可以加一个逻辑判断,检测用户剪切板上的链接是否是以 juejin.cn 开头的,如果则跳转首页;如果不是,那么什么事情也不做。

    image.png

    对应的代码如下:

    image.png

  11. 那么相对应的效果如下,这也就是为什么复制某宝的链接到某东后没任何反应的原因。某东并不是没读取,而是读取后发现不是自家的就不处理罢了。

    6.gif


四*. 思维拓展:粘贴图片自动转链接的实现



  1. 用过相关写作平台的小伙伴大概对在编辑器中直接接粘贴图片的功能不陌生。如掘金的编辑器,当我复制一个图片以后,直接在编辑器中粘贴即可。掘金会自动将图片转换为一个链接,这样会极大的提高创作者的写作体验。

    7.gif

  2. 那么现在让我们继续发散思维来思考这个需求如何实现,这里我们先随便创建一个富文本框。

    image.png

  3. 既然是粘贴图片,那么最起码我得知道用户什么时候进行粘贴操作吧?这还不简单,直接监听 paste 事件即可。


    document.addEventListener("paste",()=>{
    console.log("用户粘贴了")
    })

    实现的效果如下:

    8.gif


  4. 把之前的 clipboard.readText 替换为 clipboard.read 以后你的代码应该是下面这样的:


    document.addEventListener("paste", () => {
    if (window.isSecureContext && navigator.clipboard) {
    const clipboardAPI = navigator.clipboard; //获取 clipboard 对象
    setTimeout(() => {
    clipboardAPI.read().then((result) => {
    console.log("result", result);
    });
    }, 1000);
    } else {
    console.log("不支持 clipboard");
    }
    });

    让我们复制一张图片到富文本区域执行粘贴操作后,控制台会打印以下信息:

    image.png


  5. clipboardItem 是一个数组,里面有很多子 clipboardItem,是数组的原因是因为你可以一下子复制多张图片,不过在这里我们只考虑一张图片的场景。

  6. 这里我们取第一项,然后调用 clipboardItem.getType 方法,这个方法需要传递一个文件类型的参数,这里我们传入粘贴内容对应的类型即可,这里传入 image/png
    image.png
    在控制台这里可以看到一下输出,就表示我们已经正确拿到图片的 blob 的格式数据了。

    image.png

  7. 此时我们就只需要把相对应的图片数据传递给后端或者 CDN 服务器,让它们返回一个与之对应的链接即可。在掘金的编辑器中,对应的请求就是 get-image-url 这个请求。

    9.gif

  8. 然后调用 textarea.value + link 把链接补充到文章最后位置即可。


五. 源码


<script lang="ts" setup>
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "visible") {
if (window.isSecureContext && navigator.clipboard) {
const clipboardAPI = navigator.clipboard; //获取 clipboard 对象
setTimeout(() => {
clipboardAPI.read().then((result) => {});
}, 1000);
} else {
console.log("不支持 clipboard");
}
}
});

document.addEventListener("paste", () => {
if (window.isSecureContext && navigator.clipboard) {
const clipboardAPI = navigator.clipboard; //获取 clipboard 对象
setTimeout(() => {
clipboardAPI.read().then((result) => {
result[0].getType("image/png").then((blob) => {
console.log("blob", blob);
const reader = new FileReader();
reader.readAsDataURL(blob);
reader.onload = (e) => {
const img = document.createElement("img");
img.src = e.target?.result;
const wrapper = document.getElementById("han");
wrapper.appendChild(img);
};
});
});
}, 1000);
} else {
console.log("不支持 clipboard");
}
});
</script>
<template>
<div id="han" class="w-full h-full bg-blue">
<textarea class="w-300px h-300px"></textarea>
</div>
</template>

六. 思考 writeText 的用法


有了上面的经验,我相信你已经可以自己理解 clipboard 剩下的两个方法 writewriteText 了。你可以思考下面的问题:


为什么在掘金复制文章内容后,剪切板会自动加版权信息呢?

11.gif


如果你实现了,不妨在评论区写下你的思路~🌹


作者:韩振方
来源:juejin.cn/post/7385776238789181449
收起阅读 »

Dart中令人惊艳的8个用法(深入探索)

web
Dart是谷歌开发的现代化编程语言,凭借其简洁的语法和强大的功能,在开发者当中赢得了极高的声誉,尤其是在Flutter框架中发挥了巨大的作用。本文将介绍Dart中的8个令人惊艳的用法,这些用法不仅技术深度足够,充满启发性,而且能够让您的Dart编程效率飞速提升...
继续阅读 »

Dart是谷歌开发的现代化编程语言,凭借其简洁的语法和强大的功能,在开发者当中赢得了极高的声誉,尤其是在Flutter框架中发挥了巨大的作用。本文将介绍Dart中的8个令人惊艳的用法,这些用法不仅技术深度足够,充满启发性,而且能够让您的Dart编程效率飞速提升。


1. 泛型类型别名的高级应用


类型别名可以让你用简单的名称定义更复杂的类型,尤其是在处理大量嵌套的泛型时特别有用。


typedef ComplexList<T> = List<Map<T, T>>;

void main() {
// 适用于需要设置特定键值对类型的列表
ComplexList<String> complexList = [
{'key1': 'value1'},
{'key2': 'value2'},
];

// 复杂集合的操作
complexList.add({'key3': 'value3'});
print(complexList);
}

泛型类型别名可以更好地组织代码,增强代码的可读性。


2. Stream的高级处理技巧


利用Stream提供的各种操作符和转换器,能够更好地处理事件流和异步数据。


Stream<int> timedCounter(Duration interval, int maxCount) async* {
int count = 0;
while (count < maxCount) {
await Future.delayed(interval);
yield ++count;
}
}

void main() async {
// 监听Stream,执行特定逻辑
await for (final count in timedCounter(Duration(seconds: 1), 5)) {
print(count);
}
}

通过async*yield,你可以构建出能够发射数据序列的Stream,为异步编程提供强大支持。


3. Isolate的轻量级并行计算


Isolate可以在不同的执行线程中运是执行并发操作的强大工具。


import 'dart:isolate';

Future<void> computeOnIsolate() async {
final receivePort = ReceivePort();

Isolate.spawn(_heavyComputation, receivePort.sendPort);

final message = await receivePort.first as String;
print(message);
}

void _heavyComputation(SendPort sendPort) {
// 很重的计算
// 假设这是一个令CPU满负荷的操作
sendPort.send('计算完成');
}

void main() {
computeOnIsolate();
}

通过Isolate,你可以在Flutter应用中执行耗时操作而不影响应用的响应性。


4. 使用枚举的高级技巧


枚举类型不仅仅可以代表一组命名常量,通过扩展方法,可以大幅提升它们的功能。


enum ConnectionState {
none,
waiting,
active,
done,
}

extension ConnectionStateX on ConnectionState {
bool get isTerminal => this == ConnectionState.done;
}

void main() {
final state = ConnectionState.active;

print('Is the connection terminal? ${state.isTerminal}');
}

枚举类型的扩展性提供了类似面向对象的模式,从而可以在保证类型安全的前提下,增加额外的功能。


5. 使用高级const构造函数


const构造函数允许在编译时创建不可变实例,有利于性能优化。


class ImmutableWidget {
final int id;
final String name;

const ImmutableWidget({this.id, this.name});

@override
String toString() => 'ImmutableWidget(id: $id, name: $name)';
}

void main() {
const widget1 = ImmutableWidget(id: 1, name: 'Widget 1');
const widget2 = ImmutableWidget(id: 1, name: 'Widget 1');

// 标识符相同,它们是同一个实例
print(identical(widget1, widget2)); // 输出: true
}

使用const构造函数创建的实例,由于它们是不可变的,可以被Dart VM在多个地方重用。


6. 元数据注解与反射


虽然dart:mirrors库在Flutter中不可用,但理解元数据的使用可以为你提供设计灵感。


import 'dart:mirrors'; // 注意在非Web平台上不可用

class Route {
final String path;
const Route(this.path);
}

@Route('/login')
class LoginPage {}

void main() {
final mirror = reflectClass(LoginPage);
for (final instanceMirror in mirror.metadata) {
final annotation = instanceMirror.reflectee;
if (annotation is Route) {
print('LoginPage的路由是: ${annotation.path}');
}
}
}

通过注解,你可以给代码添加可读的元数据,并通过反射在运行时获取它们,为动态功能提供支持,虽然在Flutter中可能会借助其他方式如代码生成来实现。


7. 匿名mixin


创建匿名mixin能够在不暴露mixin到全局作用域的情况下复用代码。


class Bird {
void fly() {
print('飞翔');
}
}

class Swimmer {
void swim() {
print('游泳');
}
}

class Duck extends Bird with Swimmer {}

void main() {
final duck = Duck();
duck.fly();
duck.swim();
}

利用匿名mixin可以在不同的类中混入相同的功能而不需要创建明显的类层次结构,实现了代码的复用。


8. 高级异步编程技巧


在异步编程中,Dart提供了Future、Stream、async和await等强大的工具。


Future<String> fetchUserData() {
// 假设这是一个网络请求
return Future.delayed(Duration(seconds: 2), () => '用户数据');
}

Future<void> logInUser(String userId) async {
print('尝试登录用户...');
try {
final data = await fetchUserData();
print('登录成功: $data');
} catch (e) {
print('登录失败: $e');
}
}

void main() {
logInUser('123');
}

通过使用asyncawait,可以编写出看起来像同步代码的异步操作,使得异步代码更加简洁和易于理解。


作者:慕仲卿
来源:juejin.cn/post/7321526403434315811
收起阅读 »

用了这么多年的字体,你知道它是怎么解析的吗?

web
大家好呀。 因为之前有过字体解析的相关代码开发,一直想把这个过程记录下来,总觉得这个并不是很难,自认为了解得不算全面,就一拖再拖了。今天仅做简单的抛砖引玉,通过本篇文章,可以知道Opentype.js是如何解析字体的。在遇到对应问题的时候,可以有其它的思路去解...
继续阅读 »

大家好呀。


因为之前有过字体解析的相关代码开发,一直想把这个过程记录下来,总觉得这个并不是很难,自认为了解得不算全面,就一拖再拖了。今天仅做简单的抛砖引玉,通过本篇文章,可以知道Opentype.js是如何解析字体的。在遇到对应问题的时候,可以有其它的思路去解决。比如:.ttc的解析。又或者好奇我们开发软件过程中字体是如何解析的。


Opentype.js 使用


看官方readme也可以,这里直接将github代码下载,使用自动化测试目录里的字体文件。


需要注意的是load方法已经被废弃。


function load() {
console.error('DEPRECATED! migrate to: opentype.parse(buffer, opt) See: https://github.com/opentypejs/opentype.js/issues/675');
}

package.json设置为type: module,然后就可以直接使用import了。


import { parse } from './src/opentype.mjs';
import fs from 'fs';
// test/fonts/AbrilFatface-Regular.otf
const buffer = fs.promises.readFile('./test/fonts/AbrilFatface-Regular.otf');
// if not running in async context:
buffer.then(data => {
const font = parse(data);
console.log(font.tables);
})

这样就能得到解析的结果了。


Opentype源码阅读


parseBuffer:解析的入口


通过简单的调用入口,我们可以反查源码。传入文件的ArrayBuffer并返回Font结构的对象,在不清楚会有什么结构的时候,可以通过Font查看,当然了,直接console.log查看更方便。


// Public API ///////////////////////////////////////////////////////////

/**
* Parse the OpenType file data (as an ArrayBuffer) and return a Font object.
* Throws an error if the font could not be parsed.
* @param {ArrayBuffer}
* @param {Object} opt - options for parsing
* @return {opentype.Font}
*/

function parseBuffer(buffer, opt={}) {
// ...
// should be an empty font that we'll fill with our own data.
const font = new Font({empty: true});
}
export {
// ...
parseBuffer as parse,
// ...
};

字体类型判断


接着往下阅读。

根据signature的值,去确认字体类型。粗略看来,这里仅支持了TrueType(.ttf)、CFF(.otf)、WOFFWOFF2


    const signature = parse.getTag(data, 0);
if (signature === String.fromCharCode(0, 1, 0, 0) || signature === 'true' || signature === 'typ1') {
} else if (signature === 'OTTO') {
} else if (signature === 'wOFF') {
} else if (signature === 'wOF2') {
} else {
throw new Error('Unsupported OpenType signature ' + signature);
}

还需要注意的是,signature的值是的获取(后续基本都是这样婶儿获取的信息)。从指定偏移位置开始,读取4个字节的数据,并将每个字节转换为字符,最终返回一个4字符的字符串标签。


// Retrieve a 4-character tag from the DataView.
// Tags are used to identify tables.
function getTag(dataView, offset) {
let tag = '';
for (let i = offset; i < offset + 4; i += 1) {
tag += String.fromCharCode(dataView.getInt8(i));
}

return tag;
}

表入口信息获取


再看TrueTypeCFF字体的处理,除了对font.outlinesFormat属性的设置之外。剩余的处理方式都是:获取表的个数numTables,再获取表的入口偏移信息。


numTables = parse.getUShort(data, 4);
tableEntries = parseOpenTypeTableEntries(data, numTables);

// Table Directory Entries //////////////////////////////////////////////
/**
* Parses OpenType table entries.
* @param {DataView}
* @param {Number}
* @return {Object[]}
*/

function parseOpenTypeTableEntries(data, numTables) {
const tableEntries = [];
let p = 12;
for (let i = 0; i < numTables; i += 1) {
const tag = parse.getTag(data, p);
const checksum = parse.getULong(data, p + 4);
const offset = parse.getULong(data, p + 8);
const length = parse.getULong(data, p + 12);
tableEntries.push({tag: tag, checksum: checksum, offset: offset, length: length, compression: false});
p += 16;
}

return tableEntries;
}

function getUShort(dataView, offset) {
return dataView.getUint16(offset, false);
}
// Retrieve an unsigned 32-bit long from the DataView.
// The value is stored in big endian.
function getULong(dataView, offset) {
return dataView.getUint32(offset, false);
}

留意到tableEntries获取的offset是从12开始的,而获取numTables是从4开始的,也仅仅是getUnit16,也就是说4-12中间还会有别的信息。


表信息标准描述


这时候只能通过查看微软排版文档描述,Microsoft Typography documentation: Organization of an OpenType Font
Organization of an Opentype Font.png
按照8bit计算,这些信息之后,刚好是在12个字节开始。


后续的描述就是parseOpenTypeTableEntries的结构信息了。


表入口数据


以选择的AbrilFatface-Regular.otf 为例。我们可以打断点看看,这两步骤得到的结果,这里Opentype提供了网址,就直接在上面断点了。
parse opentype.png
这里有11个表,在入口分别有对应的名称、偏移量、长度、校验和。


表数据解析


有了表入口信息,就可以通过tableEntries获取表的数据了。接下来的代码就是通过对应的tag(name)去选择对应的解析方式。有些表的信息需要依赖于别的表,则先暂时存起来。比如: name表需要依赖language表。


    case 'ltag':
table = uncompressTable(data, tableEntry);
ltagTable = ltag.parse(table.data, table.offset);
break;
// ...
case 'name':
nameTableEntry = tableEntry;
break;
// ...
const nameTable = uncompressTable(data, nameTableEntry);
font.tables.name = _name.parse(nameTable.data, nameTable.offset, ltagTable);
font.names = font.tables.name;

这里就简单看下ltag表的解析,table = uncompressTable(data, tableEntry);判断是否有压缩,比如WOFF压缩字体,这里没有entry数据就还是原来的。


ltag表的解析


function parseLtagTable(data, start) {
const p = new parse.Parser(data, start);
const tableVersion = p.parseULong();
check.argument(tableVersion === 1, 'Unsupported ltag table version.');
// The 'ltag' specification does not define any flags; skip the field.
p.skip('uLong', 1);
const numTags = p.parseULong();

const tags = [];
for (let i = 0; i < numTags; i++) {
let tag = '';
const offset = start + p.parseUShort();
const length = p.parseUShort();
for (let j = offset; j < offset + length; ++j) {
tag += String.fromCharCode(data.getInt8(j));
}

tags.push(tag);
}

return tags;
}

创了p这个Parser实例,包含各种长度parseShortparseULong等。自动移动offset,避免每次手动传入位置。获取了table的version信息,而后就是循环的获取表内容了。找了好些个字体,都没有ltag表🤦🏻‍♀️


解析小结


这里我们可以初步的了解到整个字体的解析过程,就是按照约定的顺序,有个线头般一点儿一点儿的找到所需,只储存了数据。


如需获取最终字形信息,可能需要经过多个表联合查询,比如loca获取字形数据的偏移量,glyf获取字形数据,又或者camp获取字符代码对应的字形索引。


TTC字体集合的解析


回到前面提出的,ttc字体集合,应该怎么解析呢?参照文档对字体集合的处理 Font Collections,相信大家已经有办法解析了。
TTC header.png
注意:这里截图给出的是1.0的结构,更多的查看文档。


最后


这次的分享就到这里了,对一些有按需解析,自定义解析的场景下,希望对大家有帮助。


作者:斯文的烟鬼去shi吧
来源:juejin.cn/post/7400072326199640100
收起阅读 »

前端身份验证终极指南:Session、JWT、SSO 和 OAuth 2.0

web
Hello,大家好,我是 Sunday 在前端项目开发中,验证用户身份主要有 4 种方式:Session、JWT、SSO 和 OAuth 2.0。 那么这四种方式各有什么优缺点呢?今天,咱们就来对比下! 01:基于 Session 的经典身份验证方案 什么是基...
继续阅读 »

Hello,大家好,我是 Sunday


在前端项目开发中,验证用户身份主要有 4 种方式:Session、JWT、SSO 和 OAuth 2.0


那么这四种方式各有什么优缺点呢?今天,咱们就来对比下!


01:基于 Session 的经典身份验证方案


什么是基于Session的身份验证?


基于 Session 的身份验证是一种在前端和后端系统中常用的用户认证方法。


它主要依赖于服务器端创建和管理用户会话。


Session 运行的基本原理


Session 的运行流程分为 6 步:



  1. 用户登录:用户在登录页面输入凭据(如用户名和密码)。这些凭据通过前端发送到后端服务器进行验证。

  2. 创建会话:后端服务器验证凭据后,创建一个会话(session)。这个会话通常包括一个唯一的会话 ID,该 ID 被存储在服务器端的会话存储中。

  3. 返回会话 ID:服务器将会话 ID 返回给前端,通常是通过设置一个 cookie。这个 cookie 被发送到用户的浏览器,并在后续的请求中自动发送回服务器。

  4. 保存会话 ID:浏览器保存这个 cookie,并在用户每次向服务器发起请求时都会自动包含这个 cookie。这样,服务器就能识别出该用户的会话,从而实现身份验证。

  5. 会话验证:服务器根据会话 ID 查找和验证该用户的会话信息,并确定用户的身份。服务器可以使用会话信息来确定用户的权限和访问控制。

  6. 会话过期与管理:服务器可以设置会话过期时间,定期清除过期的会话。用户注销或会话超时后,服务器会删除或使会话失效。


通过以上流程,我们可以发现:基于 Session 的身份验证,前端是不需要主动参与的。核心是 浏览器 和 服务器 进行处理


优缺点


优点



  • 简单易用:对开发者而言,管理会话和验证用户身份相对简单。

  • 兼容性好:大多数浏览器支持 cookie,能够自动发送和接收 cookie。


缺点



  • 扩展性差:在分布式系统中,多个服务器可能需要共享会话存储,这可能会增加复杂性。

  • 必须配合 HTTPS:如果 cookie 被窃取,可能会导致会话劫持。因此需要使用 HTTPS 来保护传输过程中的安全性,并实施其他安全措施(如设置 cookie 的 HttpOnlySecure 属性)。


示例代码


接下来,我们通过 Express 实现一个基本的 Session 验证示例


const express = require('express'); 
const session = require('express-session');
const app = express();

// 配置和使用 express-session 中间件
app.use(session({
secret: 'your-secret-key', // 用于签名 Session ID cookie 的密钥,确保会话的安全
resave: false, // 是否每次请求都重新保存 Session,即使 Session 没有被修改
saveUninitialized: true, // 是否保存未初始化的 Session
cookie: {
secure: true, // 是否只通过 HTTPS 发送 cookie,设置为 true 需要 HTTPS 支持
maxAge: 24 * 60 * 60 * 1000 // 设置 cookie 的有效期,这里设置为 24 小时
}
}));

// 登录路由处理
app.post('/login', (req, res) => {
// 进行用户身份验证(这里假设用户已经通过验证)
// 用户 ID 应该从数据库或其他存储中获取
const user = { id: 123 }; // 示例用户 ID
req.session.userId = user.id; // 将用户 ID 存储到 Session 中
res.send('登录成功');
});


app.get('/dashboard', (req, res) => {
if (req.session.userId) {
// 如果 Session 中存在用户 ID,说明用户已登录
res.send('返回内容...');
} else {
// 如果 Session 中没有用户 ID,说明用户未登录
res.send('请登录...'); // 提示用户登录
}
});

app.listen(3000, () => {
console.log('服务器正在监听 3000 端口...');
});


02:基于 JWT(JSON Web Token) 的身份验证方案


什么是基于 JWT 的身份验证?


这应该是我们目前 最常用 的身份验证方式。


服务端返回 Token 表示用户身份令牌。在请求中,把 token 添加到请求头中,以验证用户信息。


因为 HTTP 请求本身是无状态的,所以这种方式也被成为是 无状态身份验证方案


JWT 运行的基本原理



  1. 用户登录:用户在登录页面输入凭据(如用户名和密码),这些凭据通过前端发送到后端服务器进行验证。

  2. 生成 JWT:后端服务器验证用户凭据后,生成一个 JWT。这个 JWT 通常包含用户的基本信息(如用户 ID)和一些元数据(如过期时间)。

  3. 返回 JWT:服务器将生成的 JWT 发送回前端,通常通过响应的 JSON 数据返回。

  4. 存储 JWT:前端将 JWT 存储在客户端(Token),通常是 localStorage 。极少数的情况下会保存在 cookie 中(但是需要注意安全风险,如:跨站脚本攻击(XSS)和跨站请求伪造(CSRF))

  5. 使用 JWT 进行请求:在用户进行 API 调用时,前端将 JWT(Token) 附加到请求的 Authorization 头部(格式为 Bearer )发送到服务器。

  6. 验证 JWT:服务器接收到请求后,提取 JWT(Token) 并验证其有效性。验证过程包括检查签名、过期时间等。如果 JWT 合法,服务器会处理请求并返回相应的资源或数据。

  7. 响应请求:服务器处理请求并返回结果,前端根据需要展示或处理这些结果。


优缺点


优点



  • 无状态:JWT 是自包含的,不需要在服务器端存储会话信息,简化了扩展性和负载均衡。

  • 跨域支持:JWT 可以在跨域请求中使用(例如,API 与前端分离的场景)。


缺点



  • 安全性:JWT 的安全性取决于密钥的保护和有效期的管理。JWT 一旦被盗用,可能会带来安全风险。


示例代码


接下来,我们通过 Express 实现一个基本的 JWT 验证示例


const express = require('express');
const jwt = require('jsonwebtoken');
const bodyParser = require('body-parser');
const app = express();

app.use(bodyParser.json());

const secretKey = 'your-secret-key'; // JWT 的密钥,用于签名和验证

// 登录路由,生成 JWT
app.post('/login', (req, res) => {
const { username, password } = req.body;
// 用户身份验证(假设验证通过)
const user = { id: 1, username: 'user' }; // 示例用户信息
const token = jwt.sign(user, secretKey, { expiresIn: '24h' }); // 生成 JWT
res.json({ token }); // 返回 JWT
});

// 受保护的路由
app.get('/dashboard', (req, res) => {
const token = req.headers['authorization']?.split(' ')[1];
if (!token) {
return res.status(401).send('没有提供令牌');
}
jwt.verify(token, secretKey, (err, decoded) => {
if (err) {
return res.status(401).send('无效的令牌');
}
res.send('返回仪表板内容');
});
});

app.listen(3000, () => {
console.log('服务器正在监听 3000 端口...');
});

03:基于 SSO 的身份验证方案


什么是基于 SSO(Single Sign-On,单点登录) 的身份验证?


SSO 身份验证多用在 “成套” 的应用程序中,通过 登录中心 的方式,可以实现 一次登录,在多个应用中均可以获取身份


SSO 运行的基本原理



  1. 用户访问应用:用户访问一个需要登录的应用(称为服务提供者或 SP)。

  2. 重定向到身份提供者:由于用户尚未登录,应用会将用户重定向到 SSO 身份提供者(Identity Provider,简称 IdP)(一般称为 登录中心)。登录中心 是负责处理用户登录和身份验证的系统。

  3. 用户登录:用户在 登录中心 输入凭据进行登录。如果用户已经在 IdP 处登录过(例如,已登录到公司内部的 SSO 系统),则可能直接跳过登录步骤。

  4. 生成 SSO 令牌:SSO 身份提供者验证用户身份后,生成一个 SSO 令牌(如 OAuth 令牌或 SAML 断言),并将用户重定向回原应用,同时附带令牌。

  5. 令牌验证:原应用(服务提供者)接收到令牌后,会将其发送到 SSO 身份提供者进行验证。SSO 身份提供者返回用户的身份信息。

  6. 用户访问应用:一旦身份验证成功,原应用会根据用户的身份信息提供访问权限。用户现在可以访问应用中的受保护资源,而无需再次登录。

  7. 访问其他应用:如果用户访问其他应用,这些应用会重定向用户到相同的 登录中心 进行身份验证。由于用户已经登录,登录中心 会自动验证并将用户重定向回目标应用,从而实现无缝登录。


优缺点


优点



  • 简化用户体验:用户只需登录一次,即可访问多个应用或系统,减少了重复登录的麻烦。

  • 集中管理:管理员可以集中管理用户的身份和访问权限,提高了管理效率和安全性。

  • 提高安全性:减少了密码泄露的风险,因为用户只需记住一个密码,并且可以使用更强的认证机制(如多因素认证)。


缺点



  • 单点故障:如果 登录中心 出现问题,可能会影响所有依赖该 SSO 服务的应用。

  • 复杂性:SSO 解决方案的部署和维护可能较为复杂,需要确保安全配置和互操作性。


常见的 SSO 实现技术



  • SAML(Security Assertion Markup Language)



    • 一个 XML-based 标准,用于在身份提供者和服务提供者之间传递认证和授权数据。

    • 常用于企业环境中的 SSO 实现。



  • OAuth 2.0 和 OpenID Connect



    • OAuth 2.0 是一种授权框架,用于授权第三方访问用户资源。

    • OpenID Connect 是建立在 OAuth 2.0 之上的身份层,提供用户身份认证功能。

    • 常用于 Web 和移动应用中的 SSO 实现。



  • CAS(Central Authentication Service)



    • 一个用于 Web 应用的开源 SSO 解决方案,允许用户通过一次登录访问多个 Web 应用。




04:基于 OAuth 2.0 的身份验证方案


什么是基于 OAuth 2.0 的身份验证?


基于 OAuth 2.0 的身份验证是一种用于授权第三方应用访问用户资源的标准协议。常见的有:微信登录、QQ 登录、APP 扫码登录等


OAuth 2.0 主要用于授权,而不是身份验证,但通常与身份验证结合使用来实现用户登录功能。


OAuth 2.0 运行的基本原理


OAuth 2.0 比较复杂,在了解它的原理之前,我们需要先明确一些基本概念。


OAuth 2.0 的基本概念



  1. 资源拥有者(Resource Owner):通常是用户,拥有需要保护的资源(如个人信息、文件等)。

  2. 资源服务器(Resource Server):提供资源的服务器,需要保护这些资源免受未经授权的访问。

  3. 客户端(Client):需要访问资源的应用程序或服务。客户端需要获得资源拥有者的授权才能访问资源。

  4. 授权服务器(Authorization Server):责认证资源拥有者并授权客户端访问资源。它颁发访问令牌(Access Token)给客户端,允许客户端访问资源服务器上的受保护资源。


运行原理



  1. 用户授权:用户使用客户端应用进行操作时,客户端会请求授权访问用户的资源。用户会被重定向到授权服务器进行授权。

  2. 获取授权码(Authorization Code):如果用户同意授权,授权服务器会生成一个授权码,并将其发送回客户端(通过重定向 URL)。

  3. 获取访问令牌(Access Token):客户端使用授权码向授权服务器请求访问令牌。授权服务器验证授权码,并返回访问令牌。

  4. 访问资源:客户端使用访问令牌向资源服务器请求访问受保护的资源。资源服务器验证访问令牌,并返回请求的资源。


常见的授权流程



  1. 授权码流程(Authorization Code Flow):最常用的授权流程,适用于需要与用户交互的客户端(如 Web 应用)。用户在授权服务器上登录并授权,客户端获取授权码后再交换访问令牌。

  2. 隐式流程(Implicit Flow):适用于公共客户端(如单页应用)。用户直接获得访问令牌,适用于不需要安全存储的情况,但不推荐用于高度安全的应用。

  3. 资源所有者密码凭据流程(Resource Owner Password Credentials Flow):适用于信任客户端的情况。用户直接将用户名和密码提供给客户端,客户端直接获得访问令牌。这种流程不推荐用于公开的客户端。

  4. 客户端凭据流程(Client Credentials Flow):适用于机器对机器的情况。客户端直接向授权服务器请求访问令牌,用于访问与客户端本身相关的资源。


优缺点


优点



  • 灵活性:OAuth 2.0 支持多种授权流程,适应不同类型的客户端和应用场景。

  • 安全性:通过分离授权和认证,增强了系统的安全性。使用令牌而不是用户名密码来访问资源。


缺点



  • 复杂性:OAuth 2.0 的实现和配置可能较复杂,需要正确管理访问令牌和刷新令牌。

  • 安全风险:如果令牌泄露,可能会导致安全风险。因此需要采取适当的安全措施(如使用 HTTPS 和适当的令牌管理策略)。


示例代码


接下来,我们通过 Express 实现一个基本的 OAuth 2.0 验证示例


const express = require('express');
const axios = require('axios');
const app = express();

// OAuth 2.0 配置
const clientId = 'your-client-id';
const clientSecret = 'your-client-secret';
const redirectUri = 'http://localhost:3000/callback';
const authorizationServerUrl = 'https://authorization-server.com';
const resourceServerUrl = 'https://resource-server.com';

// 登录路由,重定向到授权服务器
app.get('/login', (req, res) => {
const authUrl = `${authorizationServerUrl}/authorize?response_type=code&client_id=${clientId}&redirect_uri=${redirectUri}&scope=read`;
res.redirect(authUrl);
});

// 授权回调路由,处理授权码
app.get('/callback', async (req, res) => {
const { code } = req.query;
if (!code) {
return res.status(400).send('Authorization code is missing');
}

try {
// 请求访问令牌
const response = await axios.post(`${authorizationServerUrl}/token`, {
grant_type: 'authorization_code',
code,
redirect_uri: redirectUri,
client_id: clientId,
client_secret: clientSecret
});

const { access_token } = response.data;

// 使用访问令牌访问资源
const resourceResponse = await axios.get(`${resourceServerUrl}/user-info`, {
headers: { Authorization: `Bearer ${access_token}` }
});

res.json(resourceResponse.data);
} catch (error) {
res.status(500).send('Error during token exchange or resource access');
}
});

app.listen(3000, () => {
console.log('服务器正在监听 3000 端口...');
});

总结一下


目前这四种验证方案均有对应的 优缺点、应用场景:



  • Session:非常适合简单的服务器呈现的应用程序

  • JWT:适用于现代无状态架构和移动应用

  • SSO:非常适合具有多种相关服务的企业环境

  • OAuth 2.0:第三方集成和 API 访问的首选

作者:程序员Sunday
来源:juejin.cn/post/7399986979736322063
收起阅读 »

💥图片碎片化展示-Javascript

web
写在开头 哈喽吖!各位好!😁 今天刚好是周四呢,疯狂星期四快整起来。🍔🍟🍗 最近,小编从玩了两年多的游戏中退游了😔,本来以为会一直就这么玩下去,和队友们相处很融洽,收获了很多开心快乐的时光😭。可惜,游戏的一波更新......准备要开始收割韭菜了,只能无奈选择弃...
继续阅读 »

写在开头


哈喽吖!各位好!😁


今天刚好是周四呢,疯狂星期四快整起来。🍔🍟🍗


最近,小编从玩了两年多的游戏中退游了😔,本来以为会一直就这么玩下去,和队友们相处很融洽,收获了很多开心快乐的时光😭。可惜,游戏的一波更新......准备要开始收割韭菜了,只能无奈选择弃坑了。


02D754C7.jpg

小编属于贫民玩家,靠着硬肝与白嫖也将游戏号整得还不错,这两天把号给卖了💰。玩了两年多,竟然还能赚一点小钱,很开心😛。只是...多少有点舍不得的一起组队的队友们,唉。😔


记录一下,希望未来还有重逢一日吧,也希望各位一切安好!😆


好,回到正题,本文将分享一个图片碎片化展示的效果,具体效果如下,请诸君按需食用。


06132.gif

原理


这种特效早在几年前就已经出现,属于老演员了😪,它最早是经常在轮播图(banner)上应用的,那会追求各种花里胡哨的特效,而现在感觉有点返璞归真了,简洁实用就行。


今天咱们来看看它的具体实现原理是如何的,且看图:


image.png

一图胜千言,不知道聪明的你是否看明白了?😉


大概原理是:通过容器/图片大小生成一定数量的小块,然后每个小块背景也使用相同图片,再使用 background-sizebackground-position 属性调整背景图片的大小与位置,使小块又合成一整张大图片,这操作和使用"精灵图"的操作是一样的,最后,我们再给每个小块增加动画效果,就大功告成。


简单朴实😁,你可以根据这个原理自个尝试一下,应该能整出来吧。👻


具体实现


布局与样式:


<!DOCTYPE html>
<html>
<head>
<style>
body{
width: 100%;
height: 100vh;
padding: 0;
margin: 0;
display: flex;
justify-content: center;
align-items: center;
}
.box {
width: var(--width);
height: var(--height);
display: flex;
/* 小块自动换行排列 */
flex-wrap: wrap;
justify-content: center;
}
.small-box {
background-image: url('https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/99b070fcb1de471d9af4f4d5d3f71909~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=1120&h=1680&s=2088096&e=png&b=d098d0');
box-sizing: border-box;
background-repeat: no-repeat;
}
</style>
</head>
<body>
<div id="box" class="box"></div>
</body>
</html>

生成无数小块填充:


<script>
document.addEventListener('DOMContentLoaded', () => {
const box = document.getElementById('box');
const { width, height } = box.getBoundingClientRect();
// 定义多少个小块,由多少行和列决定
const row = 14;
const col = 10;
// 计算小块的宽高
const smallBoxWidth = width / col;
const smallBoxHeight = height / row;
/** @name 创建小块 **/
function createSmallBox() {
for (let i = 0; i < row; i++) {
for (let j = 0; j < col; j++) {
const smallBox = document.createElement('div');
smallBox.classList.add('small-box');
smallBox.style.width = smallBoxWidth + 'px';
smallBox.style.height = smallBoxHeight + 'px';
smallBox.style.border = '1px solid red';
// 插入小块
box.appendChild(smallBox);
}
}
}
createSmallBox();
});
</script>

上面,生成多少个小块是由人为规定行(row)与列(col)来决定。可能有的场景想用小块固定的宽高来决定个数,这也是可以的,只是需要注意处理一下"边界"的情况。😶


image.png

调整小块背景图片的大小与位置:


<script>
document.addEventListener('DOMContentLoaded', () => {
// ...
function createSmallBox() {
for (let i = 0; i < row; i++) {
for (let j = 0; j < col; j++) {
// ...
smallBox.style.border = '1px solid red';

// 设置背景偏移量,让小块的背景显示对应图片的位置,和以前那种精灵图一样
const offsetX = j * smallBoxWidth * -1;
const offsetY = i * smallBoxHeight * -1;
smallBox.style.backgroundPosition = `${offsetX}px ${offsetY}px`;
smallBox.style.backgroundSize = `${width}px ${height}px`;

box.appendChild(smallBox);
}
}
}
createSmallBox();
});
</script>

女神拼接成功,到这里就已经完成一大步了,是不是没什么难度!😋


image.png

小块样式整好后,接下来,我们需要来给小块增加动画,让它们动起来,并且是有规律的动起来。


先来整个简单的透明度动画,且看:


<!DOCTYPE html>
<html>
<head>
<style>
/* ... */
.small-box {
/* ... */
opacity: 0;
animation: smallBoxAnimate 2000ms linear forwards;
}
@keyframes smallBoxAnimate {
0% {
opacity: 0;
}
40% {
opacity: 0;
}
70% {
opacity: 1;
}
100% {
opacity: 1;
}
}
</style>
</head>
<body>
<script>
document.addEventListener('DOMContentLoaded', () => {
// ...
function createSmallBox() {
for (let i = 0; i < row; i++) {
for (let j = 0; j < col; j++) {
// ...
// smallBox.style.border = '1px solid red';

// 给每个小块增加不同的延时,让动画不同时间执行
const delay = i * 100; // 延迟时间为毫秒(ms),注意不要太小了
smallBox.style.animationDelay = `${delay}ms`;

box.appendChild(smallBox);
}
}
}
createSmallBox();
});
</script>
</body>
</html>

嘿嘿😃,稍微有点意思了吧?


06133.gif
image.png

Em...等等,你发现没有?怎么有一些小白条?这可不是小编添加的,小块的边框(border)已经是注释了的。😓


一开始小编以为是常见的"图片底部白边"问题,直接设置一下 display: block 或者 vertical-align : middle 就能解决,结果还不是,折腾了很久都没有搞掉这个小白条。😤


最后,竟然通过设置 will-change 属性能解决这个问题❗我所知道的 will-change 应该是应用在性能优化上,解决动画流畅度问题上的,想不到这里竟然也能用。



❗不对不对,当初以为是 will-change 能直接完美解决白边的问题,但是感觉还是不对,但又确实能解决。。。(部分电脑屏幕)


但其实,应该是 smallBoxWidthsmallBoxHeight 变量不是整数的问题,只要小块的宽度与高度保持一个整数,自然就没有这些白边了❗这是比较靠谱的事实,对于当前的高清屏幕来说。


但是,也是很奇怪,在小编另一台电脑(旧电脑)上即使是保持了整数,也会在横向存在一些小白边,太难受了。。。没办法彻底搞定这个问题。


猜测应该是和屏幕分辨率有关,毕竟那才是根源所在。


2024年07月01日



看来得去深度学习一下💪 will-change 属性的原理过程才行,这里也推荐倔友写得一篇文章:传送门


解决相邻背景图片白条/白边间隙问题:


<script>
document.addEventListener('DOMContentLoaded', () => {
// ...
function createSmallBox() {
for (let i = 0; i < row; i++) {
for (let j = 0; j < col; j++) {
// ...

smallBox.style.willChange = 'transform';
// 在动画执行后,需要重置will-change
const timer = setTimeout(() => {
smallBox.style.willChange = 'initial';
clearTimeout(timer);
}, 2000);

box.appendChild(smallBox);
}
}
}
createSmallBox();
});
</script>

一定要注意 will-change 不可能被滥用,注意重置回来❗


这下女神在动画执行后,也清晰可见了,这是全部小块拼接组成的图片。


06134.gif

在上述代码中,咱们看到,通过 animation-delay 去延迟动画的执行,就能制造一个从上到下的渐变效果。


那么,咱们再改改延迟时间,如:


// const delay = i * 100; 
// 改成 ⤵
const delay = j * 100;

效果:


06135.gif

这...好像有那么点意思吧。。。


0363D0F3.png

但是,这渐变...好像还达不到我们开头 gif 的碎片化效果吧?


那么,碎片化安排上:


.small-box {
/* ... */
--rotateX: rotateX(0);
--rotateY: rotateY(0);
transform: var(--rotateX) var(--rotateY) scale(0.8);
}
@keyframes smallBoxAnimate {
0% {
opacity: 0;
transform: var(--rotateX) var(--rotateY) scale(0.8);
}
40% {
opacity: 0;
transform: var(--rotateX) var(--rotateY) scale(0.8);
}
70% {
opacity: 1;
transform: rotateX(0) rotateY(0) scale(0.8);
}
100% {
opacity: 1;
transform: rotateX(0) rotateY(0) scale(1);
}
}

其实就是增加小块的样式动画而已,再加点旋转,再加点缩放,都整上,整上。😆


效果:


06136.gif

是不是稍微高级一点?有那味了?😁


看到上面旋转所用的"样式变量"没有?


--rotateX: rotateX(0);
--rotateY: rotateY(0);

不可能无缘无故突然使用,必然是有深意啦。😁


现在效果还不够炫,咱们将样式变量利用起来,让"相邻两个小块旋转相反":


<script>
document.addEventListener('DOMContentLoaded', () => {
// ...
function createSmallBox() {
for (let i = 0; i < row; i++) {
for (let j = 0; j < col; j++) {
// ...

// 相邻两个小块旋转相反
const contrary = (i + j) % 2 === 0;
smallBox.style.setProperty('--rotateX', `rotateX(${contrary ? -180 : 0}deg)`);
smallBox.style.setProperty('--rotateY', `rotateY(${contrary ? 0 : -180}deg)`);

box.appendChild(smallBox);
}
}
}
createSmallBox();
});
</script>

效果:


06137.gif

这下对味了。😃


总的来说,我们可以通过"延迟"执行动画与改变"旋转"行为,让小块们呈现不同的动画效果,或者你只要有足够多的设想,你可以给小块添加不同的动画效果,相信也能制造出不错的整体效果。


更多效果


下面列举一些通过"延迟"执行动画产生的效果,可以瞧瞧哈。


随机:


const getRandom = (min, max) => Math.floor(Math.random() * (max - min + 1) + min);
const delay = getRandom(0, col + row) * 100;

06139.gif

从左上角到右下角:


const delay = (i + j) * 100;

06140.gif

其他的从"右上角到左下角"或者"左下角到右上角"等等的,只要反向调整一下变量就行了,就靠你自己悟啦,Come On!👻


从中心向四周扩散:


const delay = ((Math.abs(col / 2 - j) + Math.abs(row / 2 - i))) * 100;

06141.gif

从四周向中心聚齐:


const delay = (col / 2 - Math.abs(col / 2 - j) + (col / 2 - Math.abs(row / 2 - i))) * 100;

06142.gif

那么,到这里就差不多了❗


但还有最后一个问题,那就是图片的大量使用与加载时长的情况可能会导致效果展示不佳,这里你最好进行一些防范措施,如:



  • 图片链接设置缓存,让浏览器缓存到内存或硬盘中。

  • 通过 JS 手动将图片缓存到内存,主要就是创建 Image 对象。

  • 将图片转成 base64 使用。

  • 直接将图片放到代码本地使用。

  • ...


以上等等吧,反正最好就是要等图片完整加载后再进行效果展示。









至此,本篇文章就写完啦,撒花撒花。


image.png


希望本文对你有所帮助,如有任何疑问,期待你的留言哦。

老样子,点赞+评论=你会了,收藏=你精通了。


作者:橙某人
来源:juejin.cn/post/7379856289487831074
收起阅读 »

Canvas星空类特效

web
思路绘制单个星星在画布批量随机绘制星星添加星星移动动画页面resize处理Vanilla JavaScript实现初始化一个工程pnpm create vite@latest # 输入工程名后,类型选择Vanilla cd <工程目录> pnp...
继续阅读 »

star-sky.gif

思路

  1. 绘制单个星星
  2. 在画布批量随机绘制星星
  3. 添加星星移动动画
  4. 页面resize处理

Vanilla JavaScript实现

  1. 初始化一个工程
pnpm create vite@latest

# 输入工程名后,类型选择Vanilla

cd <工程目录> pnpm install pnpm dev # 运行本地服务
body {
background-color: black;
overflow: hidden;
}
"use strict";
import './style.css';

document.querySelector('#app').innerHTML = `

`
;
// 后续代码全部在main.js下添加即可
  1. 绘制单个星星

image.png

const hue = 220; // 颜色hue值可以根据自己喜好调整
// 离屏canvas不需要写入到静态html里,所以用createElement
const offscreenCanvas = document.createElement("canvas");
const offscreenCtx = offscreenCanvas.getContext("2d");
offscreenCanvas.width = 100;
offscreenCanvas.height = 100;
const half = offscreenCanvas.width / 2;
const middle = half;
// 设定径向渐变的范围,从画布中心到画布边缘
const gradient = offscreenCtx.createRadialGradient(
middle,
middle,
0,
middle,
middle,
half
);
// 添加多级颜色过渡,可以根据自己喜好调整
gradient.addColorStop(0.01, "#fff");
gradient.addColorStop(0.1, `hsl(${hue}, 61%, 33%)`);
gradient.addColorStop(0.5, `hsl(${hue}, 64%, 6%)`);
gradient.addColorStop(1, "transparent");

// 基于渐变填充色,在画布中心为原点绘制一个圆形
offscreenCtx.fillStyle = gradient;
offscreenCtx.beginPath();
offscreenCtx.arc(middle, middle, half, 0, Math.PI * 2);
offscreenCtx.fill();

参考链接:

hsl() - CSS:层叠样式表 | MDN

  1. 在画布批量绘制星星

其实要绘制星星,我们只需要在画布上基于离屏画布来在指定位置将离屏画布渲染成图片即可,但是批量绘制以及后续的动画需要我们能记录每颗星星的位置、状态和行驶轨迹,所以可以考虑创建一个星星的类。

// 声明存放星星数据的数组,以及最大星星数量
const stars = [];
const maxStars = 1000;

// 用于提供随机值,不用每次都Math.random()
const random = (min, max) => {
if (!max) {
max = min;
min = 0;
}
if (min > max) {
[min, max] = [max, min];
}
return Math.floor(Math.random() * (max - min + 1)) + min;
};
// 用于计算当前以画布为中心的环绕半径
const maxOrbit = (_w, _h) => {
const max = Math.max(_w, _h);
const diameter = Math.round(Math.sqrt(max * max + max * max));
return diameter / 2;
};

class Star {
constructor(_ctx, _w, _h) {
this.ctx = _ctx;
// 最大轨道半径
this.maxOrbitRadius = maxOrbit(_w, _h);
// 轨道半径
this.orbitRadius = random(this.maxOrbitRadius);
// 星星大小(半径)
this.radius = random(60, this.orbitRadius) / 12;
// 环绕轨道中心,即画布中心点
this.orbitX = _w / 2;
this.orbitY = _h / 2;
// 随机时间,用于动画
this.elapsedTime = random(0, maxStars);
// 移动速度
this.speed = random(this.orbitRadius) / 500000;
// 透明度
this.alpha = random(2, 10) / 10;
}
// 星星的绘制方法
draw() {
// 计算星星坐标[x, y],使用sin和cos函数使星星围绕轨道中心做圆周运动
const x = Math.sin(this.elapsedTime) * this.orbitRadius + this.orbitX;
const y = Math.cos(this.elapsedTime) * this.orbitRadius + this.orbitY;

// 基于随机数调整星星的透明度
const spark = Math.random();
if (spark < 0.5 && this.alpha > 0) {
this.alpha -= 0.05;
} else if (spark > 0.5 && this.alpha < 1) {
this.alpha += 0.05;
}

// 调整全局绘制透明度,使后续绘制都基于这个透明度绘制,也就是绘制当前星星
// 因为动画里会遍历每一个星星进行绘制,所以透明度会来回改变
this.ctx.globalAlpha = this.alpha;
// 在星星所在的位置基于离屏canvas绘制一张星星的图片
this.ctx.drawImage(offscreenCanvas, x - this.radius / 2, y - this.radius / 2, this.radius, this.radius);
// 时间基于星星的移动速度递增,为下一帧绘制做准备
this.elapsedTime += this.speed;
}
}

获取当前画布,批量添加星星

const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
let w = canvas.width = window.innerWidth;
let h = canvas.height = window.innerHeight;

for (let i = 0; i < maxStars; i++) {
stars.push(new Star(ctx, w, h));
}
  1. 添加星星的移动动画
function animation() {
// 绘制一个矩形作为背景覆盖整个画布,'source-over'是用绘制的新图案覆盖原有图像
ctx.globalCompositeOperation = 'source-over';
ctx.globalAlpha = 0.8;
ctx.fillStyle = `hsla(${hue} , 64%, 6%, 1)`;
ctx.fillRect(0, 0, w, h);
// 绘制星星,'lighter'可以使动画过程中重叠的星星有叠加效果
ctx.globalCompositeOperation = 'lighter';
stars.forEach(star => {
star.draw();
});
window.requestAnimationFrame(animation);
}
// 调用动画
animation();

这样星星就动起来了。

  1. 页面resize处理

其实只需要在resize事件触发时重新设定画布的大小即可

window.addEventListener('resize', () => {
w = canvas.width = window.innerWidth;
h = canvas.height = window.innerHeight;
});

但是有一个问题,就是星星的运行轨迹并没有按比例变化,所以需要添加两处变化

// 在Star类里添加一个update方法
class Star {
constructor(_ctx, _w, _h) {//...//}
//添加部分
update(_w, _h) {
// 计算当前的最大轨道半径和类之前保存的最大轨道半径的比例
const ratio = maxOrbit(_w, _h) / this.maxOrbitRadius;
// 因为每帧动画都会调用这个方法,但比例没变化时不需要按比例改变移动轨道,所以加个判断
if (ratio !== 1) {
// 重新计算最大轨道半径
this.maxOrbitRadius = maxOrbit(_w, _h);
// 按比例缩放轨道半径和星星的半径
this.orbitRadius = this.orbitRadius * ratio;
this.radius = this.radius * ratio;
// 重新设置轨道中心点
this.orbitX = _w / 2;
this.orbitY = _h / 2;
}
}

draw() {//...//}
}

// 在animation函数里调用update
function animation() {
// ...
stars.forEach(star => {
star.update(w, h);
star.draw();
});
// ...
}

React实现

react实现主要需要注意resize事件的处理,怎样避免重绘时对星星数据初始化,当前思路是使用多个useEffect

import React, { useEffect, useRef, useState } from 'react';

const HUE = 217;
const MAX_STARS = 1000;

const random = (min: number, max?: number) => {
if (!max) {
max = min;
min = 0;
}
if (min > max) {
[min, max] = [max, min];
}
return Math.floor(Math.random() * (max - min + 1)) + min;
};

const maxOrbit = (_w: number, _h: number) => {
const max = Math.max(_w, _h);
const diameter = Math.round(Math.sqrt(max * max + max * max));
return diameter / 2;
};

// 离屏canvas只需要执行一次,但是直接在函数外部使用document.createElement会出问题
const getOffscreenCanvas = () => {
const offscreenCanvas = document.createElement('canvas');
const offscreenCtx = offscreenCanvas.getContext('2d')!;
offscreenCanvas.width = 100;
offscreenCanvas.height = 100;
const half = offscreenCanvas.width / 2;
const middle = half;
const gradient = offscreenCtx.createRadialGradient(middle, middle, 0, middle, middle, half);
gradient.addColorStop(0.01, '#fff');
gradient.addColorStop(0.1, `hsl(${HUE}, 61%, 33%)`);
gradient.addColorStop(0.5, `hsl(${HUE}, 64%, 6%)`);
gradient.addColorStop(1, 'transparent');

offscreenCtx.fillStyle = gradient;
offscreenCtx.beginPath();
offscreenCtx.arc(middle, middle, half, 0, Math.PI * 2);
offscreenCtx.fill();
return offscreenCanvas;
};

class OffscreenCanvas {
static instance: HTMLCanvasElement = getOffscreenCanvas();
}

class Star {
orbitRadius!: number;
maxOrbitRadius!: number;
radius!: number;
orbitX!: number;
orbitY!: number;
elapsedTime!: number;
speed!: number;
alpha!: number;
ratio = 1;
offscreenCanvas = OffscreenCanvas.instance;
constructor(
private ctx: CanvasRenderingContext2D,
private canvasSize: { w: number, h: number; },
) {
this.maxOrbitRadius = maxOrbit(this.canvasSize.w, this.canvasSize.h);
this.orbitRadius = random(this.maxOrbitRadius);
this.radius = random(60, this.orbitRadius) / 12;
this.orbitX = this.canvasSize.w / 2;
this.orbitY = this.canvasSize.h / 2;
this.elapsedTime = random(0, MAX_STARS);
this.speed = random(this.orbitRadius) / 500000;
this.alpha = random(2, 10) / 10;
}

update(size: { w: number, h: number; }) {
this.canvasSize = size;
this.ratio = maxOrbit(this.canvasSize.w, this.canvasSize.h) / this.maxOrbitRadius;
if (this.ratio !== 1) {
this.maxOrbitRadius = maxOrbit(this.canvasSize.w, this.canvasSize.h);
this.orbitRadius = this.orbitRadius * this.ratio;
this.radius = this.radius * this.ratio;
this.orbitX = this.canvasSize.w / 2;
this.orbitY = this.canvasSize.h / 2;
}
}

draw() {
const x = (Math.sin(this.elapsedTime) * this.orbitRadius + this.orbitX);
const y = (Math.cos(this.elapsedTime) * this.orbitRadius + this.orbitY);
const spark = Math.random();

if (spark < 0.5 && this.alpha > 0) {
this.alpha -= 0.05;
} else if (spark > 0.5 && this.alpha < 1) {
this.alpha += 0.05;
}

this.ctx.globalAlpha = this.alpha;
this.ctx.drawImage(this.offscreenCanvas, x - this.radius / 2, y - this.radius / 2, this.radius, this.radius);
this.elapsedTime += this.speed;
}
}

const StarField = () => {
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const animationRef = useRefnull>(null);
const [canvasSize, setCanvasSize] = useState({ w: 0, h: 0 });
const [initiated, setInitiated] = useState(false);
const [stars, setStars] = useState<Star[]>([]);

// 这里会在画布准备好之后初始化星星,理论上只会执行一次
useEffect(() => {
if (canvasRef.current && canvasSize.w !== 0 && canvasSize.h !== 0 && !initiated) {
const ctx = canvasRef.current!.getContext('2d')!;
const _stars = Array.from({ length: MAX_STARS }, () => new Star(ctx, canvasSize));
setStars(_stars);
setInitiated(true);
}
}, [canvasSize.w, canvasSize.h]);
// 这里用于处理resize事件,并重新设置画布的宽高
useEffect(() => {
if (canvasRef.current) {
const resizeHandler = () => {
const { clientWidth, clientHeight } = canvasRef.current!.parentElement!;
setCanvasSize({ w: clientWidth, h: clientHeight });
};
resizeHandler();
addEventListener('resize', resizeHandler);
return () => {
removeEventListener('resize', resizeHandler);
};
}
}, []);
// 这里用于渲染动画,每次画布有变化时都会触发,星星初始化完成时也会触发一次
useEffect(() => {
if (canvasRef.current) {
const ctx = canvasRef.current.getContext('2d')!;
canvasRef.current!.width = canvasSize.w;
canvasRef.current!.height = canvasSize.h;
const animation = () => {
ctx.globalCompositeOperation = 'source-over';
ctx.globalAlpha = 0.8;
ctx.fillStyle = `hsla(${HUE} , 64%, 6%, 1)`;
ctx.fillRect(0, 0, canvasSize.w, canvasSize.h);

ctx.globalCompositeOperation = 'lighter';
stars.forEach((star) => {
if (star) {
star.update(canvasSize);
star.draw();
}
});

animationRef.current = requestAnimationFrame(animation);
};

animation();
return () => {
cancelAnimationFrame(animationRef.current!);
};
}
}, [canvasSize.w, canvasSize.h, stars]);
return (
<canvas ref={canvasRef}>canvas>
);
};

export default StarField;


作者:oooooold2
来源:juejin.cn/post/7399983901811474484
收起阅读 »

1个demo带你入门canvas

web
canvas画布是前端一个比较重要的能力,在MDN上看到有关canvas的API时,是否会感到枯燥?今天老弟就给各位带来了1个还说得过去的demo,话不多说,咱们一起来尝尝咸淡。 一、小球跟随鼠标移动 先来欣赏一下这段视频: 从上图我们发现,小球跟着我们的鼠...
继续阅读 »

canvas画布是前端一个比较重要的能力,在MDN上看到有关canvas的API时,是否会感到枯燥?今天老弟就给各位带来了1个还说得过去的demo,话不多说,咱们一起来尝尝咸淡。


一、小球跟随鼠标移动


先来欣赏一下这段视频:



从上图我们发现,小球跟着我们的鼠标移动,并且鼠标点到的位置就是小球的中心点。想要实现这样的功能,我们可以将它抽象为下面图里的样子:


canvas2.png


是的,一个是画布(canvas)类,一个是小球(Ball)类。


canvas主要负责尺寸、执行绘画、事件监听。


Ball主要负责圆心坐标、半径、以及更新自己的位置。


接下来就是代码部分,我们先来完成canvas类的实现。


1.1、初始化canvas类属性


从上面的视频以及拆解的图里,我们会发现这个画布至少拥有以下几个属性:



  • width。画布的宽度

  • height。画布的高度

  • element。画布的标签元素

  • context。画布的渲染上下文

  • events。这个画布上的事件集合

  • setWidthAndHeight()。设置画布的大小

  • draw()。用于执行绘画的函数

  • addEventListener()。用于给canvas添加相应的监听函数


因此我们可以得出下面这段代码:


class Canvas {
constructor(props){
this.element = props.element;
this.canvasContext = props.element.getContext('2d');
this.events = [];
this.width = props.width;
this.height = props.height;
}
}

1.2、设置canvas的大小


class Canvas {
// 构造器
constructor(props){
this.element = props.element;
this.canvasContext = props.element.getContext('2d');
this.events = [];
this.width = props.width;
this.height = props.height;
this.setWidthAndHeight(props.width, props.height);
}
// 设置canvas的宽度与高度
setWidthAndHeight(width, height){
this.element.setAttribute('width', width);
this.element.setAttribute('height', height);
}
}

在上面的代码里,我们手动实现了一个setWidthAndHeight这样的函数,并且当执行new操作的时候,自动这个函数,从而达到设置几何元素大小的作用。


1.3、绘画小球


我们的这个绘画功能应该只负责绘画,也就是说这个小球的位置坐标等信息应该通过传参的形式传入到我们的draw函数里


class Canvas {
// 构造器
constructor(props){
this.element = props.element;
this.canvasContext = props.element.getContext('2d');
this.events = [];
this.width = props.width;
this.height = props.height;
this.setWidthAndHeight(props.width, props.height);
}
// 设置canvas的宽度与高度
setWidthAndHeight(width, height){
this.element.setAttribute('width', width);
this.element.setAttribute('height', height);
}
// 绘画小球
draw(ball){
this.canvasContext.beginPath();
this.canvasContext.arc(ball.centerX, ball.centerY, ball.radius, 0, 2 * Math.PI, true);
this.canvasContext.fillStyle = ball.background;
this.canvasContext.fill();
this.canvasContext.closePath();
}
}

1.4、给canvas添加事件监听


根据我们的需求,小球随着鼠标移动,说明我们应该是在canvas标签上监听mouseover事件。我们再来思考一下,其实这个事件监听函数也应该保持纯粹。纯粹的意思就是,你不要在这个方法里去写业务逻辑。这个函数只负责添加相应的事件监听。


接下来我们实现一下这个addEventListener函数。


class Canvas {
// 构造器
constructor(props){
this.element = props.element;
this.canvasContext = props.element.getContext('2d');
this.events = [];
this.width = props.width;
this.height = props.height;
this.setWidthAndHeight(props.width, props.height);
}
// 设置canvas的宽度与高度
setWidthAndHeight(width, height){
this.element.setAttribute('width', width);
this.element.setAttribute('height', height);
}
// 画物体
draw(ball){
this.canvasContext.beginPath();
this.canvasContext.arc(ball.centerX, ball.centerY, ball.radius, 0, 2 * Math.PI, true);
this.canvasContext.fillStyle = ball.background;
this.canvasContext.fill();
this.canvasContext.closePath();
}
// 添加监听器(eventName:要监听的事件,eventCallback:事件对应的处理函数)
addEventListener(eventName, eventCallback){
let finalEventName = eventName.toLocaleLowerCase();
if (!this.events.filter(item => item === finalEventName)?.length > 0){
this.events.push(finalEventName);
}
this.element['on' + finalEventName] = (target) => {
eventCallback && eventCallback(target);
}
}
}

好啦,Canvas类的实现到这里先告一段落,我们来看看小球(Ball)类的实现。


1.5、Ball类的初始化


Ball这个类比较简单,4个属性+一个方法。


属性分别是:



  • centerX。小球的圆心横坐标X

  • centerY。小球的圆心纵坐标Y

  • background。小球的背景色

  • radius。小球的半径


方法是updateLocation函数。这个函数同样也是一个纯函数,只负责更新圆心坐标,更新的值也是由参数传递。


class Ball {
constructor(props){
this.centerX = props.centerX;
this.centerY = props.centerY;
this.radius = props.radius;
this.background = props.background || 'orange';
}

// 更新小球的地理位置
updateLocation(x, y){
this.centerX = x;
this.centerY = y;
}
}

1.6、添加推动器


说的直白点,就是我们现在只有2个class类,但是无法实现想要的效果,现在来想想什么时机去触发draw方法。


根据上面的视频,我们知道需要在canvas标签上添加鼠标over事件,然后在over事件里来实时获取小球的位置信息,最后再触发draw方法。


当鼠标离开canvas画布后,需要将画布上的内容清除,不留痕迹。


这样一来,我们不仅要实现类似桥梁(bridge)的功能,还需要在canvas类上实现“画布清空”的功能。



class Canvas {
// ...其他代码不动

// 清空画布的功能
clearCanvas(){
this.canvasContext.clearRect(0, 0, this.width, this.height);
}
}

// 画布对象
let canvas = new Canvas({
element: document.querySelector('canvas'),
width: 300,
height: 300
});

// 小球对象
let ball = new Ball({
centerX: 0,
centerY: 0,
radius: 30,
background: 'orange'
});

// 给canvas标签添加mouseover事件监听
canvas.addEventListener(
'mousemove',
(target) => {
canvasMouseOverEvent(target, canvas.element, ball);
}
)

canvas.addEventListener(
'mouseleave',
(target) => {
canvasMouseLeave(target, canvas);
}
)

// 鼠标滑动事件
function canvasMouseOverEvent(target, canvasElement, ball){
let pointX = target.offsetX;
let pointY = target.offsetY;
ball.updateLocation(pointX, pointY);
canvas.draw(ball);
}

// 鼠标离开事件
function canvasMouseLeave(target, canvasInstance){
canvasInstance.clearCanvas();
}

这样一来,我们便实现了大致的效果,如下:



我们似乎实现了当初的需求,只是为啥目前的表现跟“刮刮乐”差不多?因为下一次绘画的时候,没有将上次的绘画清空,所以才导致了现在的这个样子。


想要解决这个bug,只需在canvas类的draw方法里,加个清空功能就可以了。


class Canvas {
// 画物体
draw(ball){
this.clearCanvas();
this.canvasContext.beginPath();
this.canvasContext.arc(ball.centerX, ball.centerY, ball.radius, 0, 2 * Math.PI, true);
this.canvasContext.fillStyle = ball.background;
this.canvasContext.fill();
this.canvasContext.closePath();
}
}

如此一来,这个bug就算解决了。


二、最后


好啦,本期内容到这里就告一段落了,希望这个demo能够帮助你了解一下canvas的相关API,如果能够帮到你,属实荣幸,那么我们下期再见啦,拜拜~~


作者:小九九的爸爸
来源:juejin.cn/post/7388056383642206262
收起阅读 »

全面横评 6 大前端视频播放器 - Vue3 项目应该怎么选?

web
前言 最近,我在负责公司的音视频开发,使用的技术栈是 Vue3,技术方案采用的是基于第三方库二次封装播放器组件,来实现公司的业务定制需求。 市面上有很多第三方视频库,在最开始进行技术选型的时候,我也花了很多时间。 现在初步把我们公司的音视频组件开发完成了,我对...
继续阅读 »

前言


最近,我在负责公司的音视频开发,使用的技术栈是 Vue3,技术方案采用的是基于第三方库二次封装播放器组件,来实现公司的业务定制需求。


市面上有很多第三方视频库,在最开始进行技术选型的时候,我也花了很多时间。


现在初步把我们公司的音视频组件开发完成了,我对视频第三方库的技术选型进行一个总结分享,希望能帮助到正在进行音视频开发的你。


这次技术选型一共会对比 6 个第三方库:xgplayervideo.jsckplayer x3aliplayertcplayerflv.js


在对比每个第三库时,我都会给出文档地址、示例、Git 地址,便于大家自行对比。


我会给出收集到的每个第三方库的优缺点分析。对于部分重要的第三方库,将进行详细的优缺点分析;而对于涉及闭源等不可接受情况的库,优缺点分析将较为简略,不再深入对比。


因为我们技术团队的英文水平还达不到无障碍阅读英文文档的地步,所以第三库的文档是否支持中文,也是我们考虑的一个因素。


我们技术团队详细对比这些第三方库之后,最后选择的是 xgplayer


好了,接下来开始分析以上提到的 6 个音视频第三方库~


1. xgplayer(推荐)


这个库也是我们现在所采用的库,整体使用下来感觉很不错,文档很详细,支持自定义插件,社区活跃~

文档地址: h5player.bytedance.com/

示例地址: h5player.bytedance.com/examples/

Git 地址: github.com/bytedance/x…


优点



  1. 基本满足现有功能,自带截图 等功能

  2. 中文文档,有清晰详细的功能释义

  3. 可通过在线配置和生成对应功能代码参考,预览配置后的视频效果,开发体验好

  4. 项目积极维护

  5. 近期从 v2 版本升级到了 v3版本,优化了很多功能,开发体验更好

  6. 支持自定义插件,对开发业务定制需求很有用


缺点



  1. 直播需要浏览器支持Media Source Extensions

  2. PC Web端支持直接播放mp4视频,播放HLS、FLV、MPEG-DASH需要浏览器支持Media Source Extensions

  3. iOS Web端支持直接播放mp4和HLS,不支持播放FLV、MPEG-DASH(iOS webkitwebview 均不支持MediaSource,因此无法支持flv文件转封装播放)

  4. Android Web端支持直接播放mp4和HLS,播放FLV、MPEG-DASH需要浏览器支持Media Source Extensions

  5. 进度条拖动:拖动时,视频会一直播放当前拖动视频帧的声音,导致挺起来声音一卡一卡的,而且拖动一停止就立马开始播放视频

  6. 自动播放限制:对于大多数移动 webview 和浏览器,默认情况下会阻止有声自动播放。可以设置静音起播,达到自动播放的目的,不能保证一定能够自动播放,不同的app和浏览器配置不一样,表现不一样

  7. 打点功能没有提供图片的配置,可能需要二次开发或者用预览功能

  8. hls和flv不能同时添加,但是可以自己通过逻辑判断,去允许 hls 和 flv 同时播放

  9. Android 在网页端打开后截图很模糊


2. video.js(候选)


文档地址: videojs.com/

示例地址: videojs.com/advanced/?v…

Git 地址: github.com/videojs/vid…


优点



  1. 功能全面:提供暂停、播放等功能,基本满足项目所有功能需求

  2. 社区情况:社区活跃,项目持续维护

  3. 插件和主题丰富:可以根据需求进行定制和扩展

  4. 跨平台和浏览器兼容性:支持跨平台播放,适用于各种设备和操作系统

  5. 进度条拖动时,视频暂停,且能预览当前拖动所处位置,在放开拖动时,才开始播放视频,体验比较好


缺点



  1. 英文文档:上手学习播放器难度大,且后期维护成本高(搭建 demo 时,发现英文文档对开发有影响)

  2. 学习曲线:提供广泛功能,可能需要一定时间来理解其概念、API 等

  3. 不支持 flv 格式,但是可以通过安装 videojs-flvjs-es6 插件,同时安装 flv.js 库,来提供 flv 格式支持(但是 videojs-flvjs-es6 库的 star 太少,可能会出现其他问题)

  4. 没有自带截图功能,需要自己开发


3. ckplayer x3(候选)


文档地址: http://www.ckplayer.com/

示例地址: http://www.ckplayer.com/demo.html

Git 地址: gitee.com/niandeng/ck…


优点



  1. 功能丰富,且提供良好的示例

  2. 中文文档,文档相对比较丰富和专业

  3. 格式支持度较高,通过插件还可以播放 ts、mpd 等视频


缺点



  1. 社区支持不够丰富,如果以后有扩展功能需求,不便开发

  2. git 仓库 issue 响应慢,后续出问题,可能不便解决

  3. 文档的左侧菜单的交互不太友好,功能模块分级不够清晰,导致查找 API 不方便

  4. 没有直接提供视频列表(通道切换)的功能或插件

  5. 进度条拖动:拖动时,视频一直在播放,且没有显示当前所处拖动位置的预览画面,用户不知道当前拖动所处的具体位置,体验不佳


4. aliplayer(候选)


文档地址: player.alicdn.com/aliplayer/i…

示例地址: player.alicdn.com/aliplayer/p…

Git 地址: github.com/aliyunvideo…


优点



  1. 基本满足现有功能需求,自带截图、视频列表等功能

  2. 提供部分功能演示和在线配置

  3. 中文文档

  4. 支持4K视频播放,并且具备高分辨率和高比特率视频的优化能力

  5. 刷新和切换页面的 loading 时间比 xgplayer 短

  6. 播放器内部集成 flv 和 hls 格式,可以直接播放


缺点



  1. Web播放器H5模式在移动端不支持播放FLV视频,但可播 HLS(m3u8)

  2. Web播放器提供的音量调节方法iOS系统和部分Android系统会失效

  3. 自动播放限制:由于浏览器自身的限制,在Web播放器SDK中无法通过设置autoplay属性或者调用play()方法实现自动播放。只有视频静音才可以实现自动播放或者通过用户行为手动触发播放

  4. 截图功能限制:fiv 视频在Safari浏览器下不支持截图功能。即使启用截图按钮也不会出现

  5. 回放时,必须点击播放按钮那个图标才能播放,体验不佳。且鼠标悬停时,会显示视频控制栏,但点击控制栏,视频无对应功能响应,体验不佳

  6. 回放播放效果不统一:同样的设置,刷新页面时视频不会自动播放,切换页面再回来,视频会自动播放

  7. 有些高级功能需要商业授权:Web播放器SDK从2.14.0版本开始支持播放H.265编码协议的视频流,如需使用此功能,您需要先填写表单申请License授权

  8. 文档:文档目录混乱且杂糅其他播放器不需要的文档

  9. 进度条拖动:拖动时,视频会一直播放当前拖动视频帧的声音,导致听起来声音一卡一卡的,而且拖动一停止就立马开始播放视频


5. tcplayer(不推荐)


文档地址: cloud.tencent.com/document/pr…

示例地址: tcplayer.vcube.tencent.com/

Git 地址: 闭源,无 git 仓库


优点



  1. 断点续播:播放失败时自动重试,支持直播的自动重连功能


缺点



  1. 文档不丰富,示例功能不多

  2. 闭源项目,出现问题不好解决

  3. 内置的功能和插件相对阿里云和 CK 较少

  4. web 端截图功能不支持


6. flv.js(不推荐)


文档地址: github.com/bilibili/fl…

示例地址: bilibili.github.io/flv.js/demo…

Git 地址: github.com/bilibili/fl…


优点



  1. 由于浏览器对原生Video标签采用了硬件加速,性能很好,支持高清


缺点



  1. 文档:缺乏详细功能说明文档,只有 md 英文文档,文档阅读不方便

  2. 项目很久未更新,原作已离开哔站,虽已开源,但后期应该不会有啥版本升级和优化

  3. 播放 flv 格式需要依赖 Media Source Extensions,但目前所有 iOS 和 Android4.4.4 以下的浏览器都不支持


结语


以上是我对音视频第三方库进行技术选型对比的一个总结。如果有更好的见解或者其他补充,欢迎在评论区留言或者私聊我进行沟通。


作者:爱听书的程序员阿超
来源:juejin.cn/post/7359083412386807818
收起阅读 »

受够了useState的逻辑分散?来,试试用reducer聚合逻辑

web
useState的缺点 经常写react 的同学都知道,useState 是 React 中的一个 Hook,可以在函数组件中管理组件的状态,并在状态更新时重新渲染组件。 这东西虽然简单好用,但有一个致命缺点:当组件有非常多的状态更新逻辑时,事件处理会非常分散...
继续阅读 »

useState的缺点


经常写react 的同学都知道,useState 是 React 中的一个 Hook,可以在函数组件中管理组件的状态,并在状态更新时重新渲染组件。


这东西虽然简单好用,但有一个致命缺点:当组件有非常多的状态更新逻辑时,事件处理会非常分散,维护起来很头疼!


比如,一个简单的记事本功能



我需要通过三个不同的事件处理程序来实现任务的添加、删除和修改:


const [tasks, setTasks] = useState([  
{ id: 1, title: '去贝加尔湖旅行', completed: false },
{ id: 2, title: '去烟台海边度假', completed: false },
{ id: 3, title: '再去一次厦门看海', completed: false },
]);

// 添加
const addTask = (taskTitle) => {
const newId = tasks.length + 1;
const newTask = { id: newId, title: taskTitle, completed: false };
setTasks([...tasks, newTask]);
};

// 删除
const deleteTask = (taskId) => {
setTasks(tasks.filter(task => task.id !== taskId));
};

// 编辑
const toggleTaskCompletion = (taskId) => {
setTasks(tasks.map(task =>
task.id === taskId ? { ...task, completed: !task.completed } : task
));
};

上面的代码中,每个事件处理程序都通过 setTasks 来更新状态。随着这个组件功能的,其状态逻辑也会越来越多。为了降低这种复杂度,并让所有逻辑都可以存放在一个易于理解的地方,我们可以将这些状态逻辑移到组件之外的一个称为 reducer 的函数中。


使用 reducer 整合状态逻辑


在学习reducer之前,我们先看看使用reducer整合后的代码


import React, { useReducer, useState } from 'react';

// 定义 reducer 函数
const taskReducer = (state, action) => {
switch (action.type) {
case 'ADD_TASK':
const newId = state.length + 1;
return [...state, { id: newId, title: action.payload, completed: false }];
case 'DELETE_TASK':
return state.filter(task => task.id !== action.payload);
case 'TOGGLE_TASK':
return state.map(task =>
task.id === action.payload ? { ...task, completed: !task.completed } : task
);
default:
return state;
}
};

function TaskList() {
const [tasks, dispatch] = useReducer(taskReducer, [
{ id: 1, title: '去贝加尔湖旅行', completed: false },
{ id: 2, title: '去烟台海边度假', completed: false },
{ id: 3, title: '再去一次厦门看海', completed: false },
]);

const [newTaskTitle, setNewTaskTitle] = useState('');

const handleAddTask = () => {
if (newTaskTitle.trim()) {
dispatch({ type: 'ADD_TASK', payload: newTaskTitle });
setNewTaskTitle('');
}
};

return (
<div>
<h2>快乐就是哈哈哈的记事本</h2>
<div>
<input
type="text"
placeholder="添加新任务"
value={newTaskTitle}
onChange={(e) =>
setNewTaskTitle(e.target.value)}
/>
<button onClick={handleAddTask} style={{ marginLeft: '10px' }}>添加</button>
</div>
<div>
{tasks.map(task => (
<div key={task.id} style={{ marginTop: '10px' }}>
<input
type="checkbox"
checked={task.completed}
onChange={() =>
dispatch({ type: 'TOGGLE_TASK', payload: task.id })}
/>
{task.title}
<button onClick={() => { /* Add edit functionality here */ }} style={{ marginLeft: '10px' }}>编辑</button>
<button onClick={() => dispatch({ type: 'DELETE_TASK', payload: task.id })} style={{ marginLeft: '10px' }}>删除</button>
</div>
))}
</div>
</div>

);
}

export default TaskList;

能够看出,所有的逻辑被整合到taskReducer这个函数中了,我们的逻辑聚合度很高,非常好维护!


useReducer的基本语法


const [state, dispatch] = useReducer(reducer, initialArg, init?)

在组件的顶层作用域调用 useReducer 以创建一个用于管理状态的 reducer。


import { useReducer } from 'react';

function reducer(state, action) {
// ...
}

function MyComponent() {
const [state, dispatch] = useReducer(reducer, { age: 42 });
// ...

参数



  • reducer:用于更新 state 的纯函数。参数为 state 和 action,返回值是更新后的 state。state 与 action 可以是任意合法值。

  • initialArg:用于初始化 state 的任意值。初始值的计算逻辑取决于接下来的 init 参数。

  • 可选参数 init:用于计算初始值的函数。如果存在,使用 init(initialArg) 的执行结果作为初始值,否则使用 initialArg


返回值


useReducer 返回一个由两个值组成的数组:



  1. 当前的 state。初次渲染时,它是 init(initialArg)initialArg (如果没有 init 函数)。

  2. dispatch函数。用于更新 state 并触发组件的重新渲染。


dispatch 函数


dispatch 函数可以用来更新 state的值 并触发组件的重新渲染,它的用法其实和vue的store,react的状态管理库非常相似!



dispacth可以有很多,通过dispacth可以发送数据给reducer函数,函数内部,我们通过action可以拿到所有dispatch发送的数据,然后进行逻辑判断,更改state的值。


通常来说 action 是一个对象,其中 type 属性标识类型,其它属性携带额外信息。


代码解读


熟悉了它的语法后,我们的整合逻辑就非常好理解了。我们简化下逻辑:


import React, { useReducer, useState } from 'react';

// 定义 reducer 函数
const taskReducer = (state, action) => {
switch (action.type) {
// 根据不同逻辑,返回一个新的state的值
default:
return state;
}
};

function TaskList() {
const [tasks, dispatch] = useReducer(taskReducer, [
{ id: 1, title: '去贝加尔湖旅行', completed: false },
{ id: 2, title: '去烟台海边度假', completed: false },
{ id: 3, title: '再去一次厦门看海', completed: false },
]);
// 通过dispatch发送数据给taskReducer
return (
<div>
<h2>快乐就是哈哈哈的记事本</h2>
<div key={task.id} style={{ marginTop: '10px' }}>
<input
type="checkbox"
checked={task.completed}
onChange={() =>
dispatch({ type: 'TOGGLE_TASK', payload: task.id })}
/>
<button onClick={() => dispatch({ type: 'DELETE_TASK', payload: task.id })} style={{ marginLeft: '10px' }}>删除</button>
</div>
</div>

);
}

export default TaskList;

useReducer的性能优化


我们先看看下面的代码


function createInitialState(username) {
// ...
// 生成初始值的一些逻辑
}

function TodoList({ username }) {
const [state, dispatch] = useReducer(reducer, createInitialState(username));
// ...

createInitialState方法用于生成初始值,但是在每一次渲染的时候都会被调用,如果创建了比较大的数组或计算是比较浪费性能的!


我们可以通过给 useReducer 的第三个参数传入 初始化函数 来解决这个问题:


function createInitialState(username) {
// ...
}

function TodoList({ username }) {
const [state, dispatch] = useReducer(reducer, username, createInitialState);
// ...

需要注意的是你传入的参数是 createInitialState 这个 函数自身,而不是执行 createInitialState() 后的返回值


如果createInitialState可以直接计算出初始值,不需要默认的username,上面的代码可以进一步优化


function createInitialState() {
// ...
}

function TodoList() {
const [state, dispatch] = useReducer(reducer, null, createInitialState);
// ...

作者:快乐就是哈哈哈
来源:juejin.cn/post/7399496845277151242
收起阅读 »

才4W条数据页面就崩溃了

web
写过地图需求的同学应该都遇到过地图加载大量点(4W多个点)导致页面十分卡顿的问题吧。 最近项目上线验收,现场直接卡崩溃了,其实在公司还好,因为公司的电脑配置还算可以,没有出现过崩溃的现象(但是也很卡,本来也想偷下懒)。崩溃了怎么办啊(我感觉我更崩溃呢,天天加班...
继续阅读 »

写过地图需求的同学应该都遇到过地图加载大量点(4W多个点)导致页面十分卡顿的问题吧。


最近项目上线验收,现场直接卡崩溃了,其实在公司还好,因为公司的电脑配置还算可以,没有出现过崩溃的现象(但是也很卡,本来也想偷下懒)。崩溃了怎么办啊(我感觉我更崩溃呢,天天加班赶需求哪有时间做优化的啊)。


4F4D1E5D.png


原因:用户想要加载所有的点还不做聚合,而且每个点都要做动态扩散效果,还要实时刷新地图数据。


哎,先说我的解决方案吧。



  1. 取消点的动态扩散效果

  2. 图层层级显示图标点才会更新

  3. 只显示可视范围内的点

  4. 用户操作过程中不更新图层


第一点,我必须拿着我的数据让产品经理去给客户说有动效内存占用在600M到1200M跳动,动效是1s一个循环。那么每次执行动效就会让内存飚到1200M,然后接下来浏览器会回收之前渲染的内存,内存又降至600M。我就说这个实在没法优化只能去掉(我没有试加载动态图片会不会好点,但想来也差不多)。


第二点,我先说明图层的加载方案,当图层层级小于14时就加载全部点否则加载聚合点(what fuck)。客户就是这么牛,一般不应该是反着来吗。。那必须滴,这么多点客户也看不出点的位置更新,看到了也不知道是那个更新了。所以,出于性能考虑给出的方案是:当图层层级小于14的时候就不更新点。那么多点一起更新


75c5e511cde86257cb8afdde8bfef95e_u=4144058492,2792071934&fm=253&fmt=auto&app=138&f=JPEG_w=440&h=390.webp


第三点,前端是首次加载的时候把所有的数据都缓存起来的,由服务端推送更新消息,前端收到消息就维护缓存的数据并做相应的更新逻辑(在线离线/GIS等),会维护一个更新队列,如果数据太大的时候就分次更新。好的,说了那么多废话是不是想水文啊


739fa618a8025ab12c7ead350271d4f0_u=3416052748,1393281168&fm=253&fmt=auto&app=138&f=JPEG_w=440&h=350.webp


首先,每次更新(用户缩放和拖拽地图与推送)之前需要先拿到当前地图的四个角经纬度,然后调用Turf.js库提供的# pointsWithinPolygon方法:


const searchWithin = Turf.multiPolygon([[东],[南],[西],[北]]);
const ptsWithin = Turf.pointsWithinPolygon(Turf.points([...points]), searchWithin);

拿到当前可视范围内的点,再将可视范围内的点渲染到地图上。


第四点,当我开开心心把代码提交上去后,过了一会,我的前端同事给我说感觉页面还是很卡啊(0.0)。我表示不信,然后实际操作了一下,虽然上面的减少点的操作和减少点的数量让浏览器内存占用降了下来页面也确实不卡了,但是当我去拖动地图的时候发现问题了,怎么感觉拖着拖着的地图有规律的卡。怎么回事呢,再梳理下我明白了,之前的地图刷新时间是10s由于客户觉得刷新太慢了,索性就改成了3s,这一改一个不吱声,3s那不是很大概率当用户正在操作地图的时候地图重新渲染了所以感觉卡。知道问题就好办,判断用户当前是否在操作地图,movestart事件时表示用户开始操作地图,moveend事件表示用户结束操作。那就等用户操作地图结束后再更新地图,上手感受了一下一点也不卡了,搞定。


创作不易求,如果你看到这里还请您点赞收藏


02db46e7aba740fde614ed12ca2d902d_u=4047343355,1704049767&fm=253&fmt=auto&app=138&f=JPEG_w=480&h=360.webp


作者:嗨皮儿
来源:juejin.cn/post/7361973121790656562
收起阅读 »