注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

前端学哪些技能饭碗越铁收入还高

web
随着经济的下行以及移动互联网发展趋于成熟,对软件开发人员的需求大大减少,互联网行业所有的公司都在降本增效,合并通道,降薪裁员的新闻层出不穷。 但相比其他行业,互联网行业的从业者薪资还是比较可观的,但要求也比之前高了很多,需要大家掌握更多的技能和在某些技术领域深...
继续阅读 »

随着经济的下行以及移动互联网发展趋于成熟,对软件开发人员的需求大大减少,互联网行业所有的公司都在降本增效,合并通道,降薪裁员的新闻层出不穷。


但相比其他行业,互联网行业的从业者薪资还是比较可观的,但要求也比之前高了很多,需要大家掌握更多的技能和在某些技术领域深耕。


本文,我们就聊聊,掌握了哪些技能,能让前端同学,收入高且稳定。


端智能


首推的是端智能,很多行业大咖都认为,随着ChatGPT的横空出世,开启了第四次工业革命,很多产品都可以用大模型重做一遍。当前,我创业的方向,也和大模型有关。


当前的大模型主要还跑在云端,但云端的成本高,大模型的未来在端智能,这也是小米创始人雷军在今年一次发布会上提出的观点。


在2023年8月14日的雷军年度演讲中,雷军宣布小米已经在手机端跑通13亿参数的大模型,部分场景效果媲美云端。


目前,端上大模型的可行性和前景已经得到了业内的普遍认可,国内外各个科技大厂在大模型的端侧部署领域均开始布局,目前大量工程已在PC端、手机端实现大模型的离线部署,更有部分App登陆应用商店,只需下载即可畅通无阻地对话。


我们相信,在不久的将来,端上大模型推理将会成为智能应用的重要组成部分,为用户带来更加便捷、智能的体验。


我在美团从零研发了web端智能推理引擎,当时立项时,就给老板画饼,美团每天的几百亿推理调用,如果有一半用端智能替代的话,每年能为公司节省上亿元。


要想掌握端智能,需要学习深度学习的基本知识,还要掌握图形学和C++编程,通过webgl或webassembly 技术实现在Web端执行深度学习算法。


图形学


前面提到的端智能,只是涉及到了图形学中的webgl计算,但图形学的未来在元宇宙,通过3D渲染,实现VR、AR、XR等各种R。


计算机图形学是一门快速发展的领域,涵盖了三维建模、渲染、动画、虚拟现实等众多技术和应用。在电影、广告、游戏等领域中,计算机图形学的应用已经非常广泛。


熟练使用threejs开发各种3D应用,只能算是入门。真正的图形学高手,不仅可以架构类似3D家装软件的大型应用,而且能掌握渲染管线的底层原理,熟练掌握各种模型格式和解决各种软件,进行模型转换遇到的各种兼容问题。


随着计算机硬件和算法的不断进步,计算机图形学正迎来新的发展趋势。


首先是实时渲染与逼真度提升



  • 实时渲染技术:随着游戏和虚拟现实的兴起,对实时渲染的需求越来越高。计算机图形学将继续致力于研发更高效的实时渲染算法和硬件加速技术,以实现更逼真、流畅的视觉效果。

  • 光线追踪与全局照明:传统的实时渲染技术在光照模拟方面存在挑战。计算机图形学将借助光线追踪等技术,实现更精确的全局照明效果,提升场景的真实感和细节表现。


其次是虚拟与增强现实的融合



  • 混合现实技术:计算机图形学将与传感器技术、机器视觉等相结合,推动虚拟现实与增强现实的融合发展。通过实时感知和交互,用户可以在真实世界中与虚拟对象进行互动,创造更沉浸式的体验。

  • 空间感知与虚拟对象定位:计算机图形学将致力于解决空间感知和虚拟对象定位的挑战。利用深度学习、摄像头阵列等技术,实现高精度的空间感知和虚实融合,为虚拟与增强现实应用带来更自然、精确的交互方式。


再次是计算机图形学与人工智能的融合



  • 生成对抗网络(GAN)在图形生成中的应用:GAN等人工智能技术为计算机图形学带来了新的创作手段。通过训练模型生成逼真的图像和场景,计算机图形学能够更便捷地创建大量内容,并提供个性化的用户体验。

  • 计算机图形学驱动的虚拟人物与角色生成:结合计算机图形学和人工智能技术,研究人员正在努力开发高度逼真的虚拟人物和角色生成方法。这将应用于游戏、影视等领域,带来更具情感表达和交互性的虚拟角色。


最后是可视化分析与科学研究。



  • 一是大数据可视化:随着大数据时代的到来,计算机图形学在可视化分析方面扮演着关键角色。通过创新的可视化方法和交互技术,研究人员能够更深入地理解和分析庞大而复杂的数据集,揭示潜在的模式和趋势。

  • 二是科学数据可视化:计算机图形学在科学研究中的应用也日益重要。通过将科学数据转化为可视化形式,研究人员能够更直观地理解复杂的数据模式和关系,加快对科学问题的洞察和发现。这种可视化分析有助于领域如天文学、生物学、气象学等的研究进展。


工程提效


其实,过去三年我在美团的工作,至少有一半的精力是做和工程提效相关的事情。当然,也做了降本的事情,从零搭建外包团队。


就像我之前总结的文章:我在美团三年的前端工程化实践回顾 中提到那样,前端工程提效,一般会按照工具化、标准化、平台化和体系化进行演进。


相比前面的端智能和图形学,除了建设低代码平台和WebIDE有点技术难度,其他更多需要的是沟通、整合资源的能力,既要有很强的项目管理能力,又要人产品思维。


前两个方向做好了我们一般称为技术专家,而工程提效则更偏管理者,未来可以成为高管或自己创业。


总结


端智能和图形学,我在美团都尝试过进行深耕,但自己性格外向,很难坐得住。工程提效做得也一般,主要是因为需要换城市而换了部门,没有机缘继续推进,在美团很难往上走,所以只能尝试自己创业。


作者:三一习惯
来源:juejin.cn/post/7310143510103064585
收起阅读 »

树形列表翻页,后端: 搞不了搞不了~~

web
背景 记得几年前做了一个报告,报告里面加载的是用户的历年作品还有会员信息,然后按照年月倒序展示出来,其中历年作品都要将作品的封面展示出来。一开始这个报告到没啥问题,而且一个时间轴下来感觉挺好,有用户的作品、会员记录、关注以及粉丝记录很全面。直到最近忽然有一批用...
继续阅读 »

背景


记得几年前做了一个报告,报告里面加载的是用户的历年作品还有会员信息,然后按照年月倒序展示出来,其中历年作品都要将作品的封面展示出来。一开始这个报告到没啥问题,而且一个时间轴下来感觉挺好,有用户的作品、会员记录、关注以及粉丝记录很全面。直到最近忽然有一批用户说一进到这个报告页面就卡住不动了,上去一查发现不得了,都是铁杆用户,每年作品都几百个,导致几年下来,这个报告返回了几千个作品,包含上千的图片。


问题分析


上千的图片,肯定会卡,首先想到的是做图片懒加载。这个很简单,使用一个vue的全局指令就可以了。但是上线发现,没啥用,dom节点多的时候,懒加载也卡。


然后就问服务端能不能支持分页,服务端说数据太散,连表太多,树形结构很难做分页。光查询出来就已经很费劲了。


没办法于是想了一下如何前端来处理掉。


思路



  1. 由于是app中的嵌入页面,首先考虑通过滚动进行分页加载。

  2. 一次性拿了全部的数据,肯定不能直接全部渲染,我们可以只渲染一部分,比如第一个节点,或者前几个节点。

  3. 随着滚动一个节点一个节点或者一批一批的渲染到dom中。


实现


本文仅展示一种基于vue的实现


1. 容器

设计一个可以进行滚动翻页的容器 然后绑定滚动方法OnPageScrolling



<style lang="less" scoped>

.study-backup {

overflow-x: hidden;

overflow-y: auto;

-webkit-overflow-scrolling: touch;

width: 100%;

height: 100%;

position: relative;

min-height: 100vh;

background: #f5f8fb;

box-sizing: border-box;

}

</style>

<template>

<section class="report" @scroll="OnPageScrolling($event)">

</section>

</template>



2.初始化数据

这里定义一下树形列表的数据结构,实现初始化渲染,可以渲染一个树节点,或者一个树节点的部分子节点



GetTreeData() {

treeapi

.GetTreeData({ ... })

.then((result) => {

// 处理结果

const data = Handle(result)

// 这里备份一份数据 不参与展示

this.backTreeList = data.map((item) => {

return {

id: item.id,

children: item.children

}

})

// 这里可以初始化为第一个树节点

const nextTree = this.backTreeList[0]

const nextTansformTree = nextTree.children.splice(0)

this.treeList = [{

id: nextTree.id,

children: nextTansformTree

}]

// 这里可以初始化为第一树节点 但是只渲染第一个子节点

const nextTree = this.backTreeList[0]

const nextTansformTree = nextTree.children.splice(0, 1)

this.treeList = [{

id: nextTree.id,

children: nextTansformTree

}]

})

},


3.滚动加载

这里通过不断的把 backTreeList 的子节点转存入 treeList来实现分页加载。



OnPageScrolling(event) {

const container = event.target

const scrollTop = container.scrollTop

const scrollHeight = container.scrollHeight

const clientHeight = container.clientHeight

// console.log(scrollTop, clientHeight, scrollHeight)

// 判断是否接近底部

if (scrollTop + clientHeight >= scrollHeight - 10) {

// 执行滚动到底部的操作

const currentReport = this.backTreeList[this.treeList.length - 1]

// 检测匹配的当前树节点 treeList的长度作为游标定位

if (currentReport) {

// 判断当前节点的子节点是否还存在 如果存在则转移到渲染树中

if (currentReport.children.length > 0) {

const transformMonth = currentReport.children.splice(0, 1)

this.treeList[this.treeList.length - 1].children.push(

transformMonth[0]

)

// 如果不存在 则寻找下一树节点进行复制 同时复制下一节点的第一个子节点 当然如果寻找不到下一树节点则终止翻页

} else if (this.treeList.length < this.backTreeList.length) {

const nextTree = this.backTreeList[this.treeList.length]

const nextTansformTree = nextTree.children.splice(0, 1)

this.treeList.push({

id: nextTree.id,

children: nextTansformTree

})

}

}

}

}


4. 逻辑细节

从上面代码可以看到,翻页的操作是树copy的操作,将备份树的子节点转移到渲染树中



  1. copy备份树的第一个节点到渲染树,同时将备份树的第一个节点的子节点的第一个节点转移到渲染树的第一个节点的子节点中

  2. 所谓转移操作,就是数组splice操作,从一颗树中删除,然后把删除的内容插入到另一颗树中

  3. 由于渲染树是从长度1开始的,所以我们可以根据渲染树的长度作为游标和备份树进行匹配,设渲染树的长度为当前游标

  4. 根据当前游标查询备份树,如果备份树的当前游标节点的子节点不为空,则进行转移

  5. 如果备份树的当前游标节点的子节点为空,则查找备份树的当前游标节点的下一节点,设为下一树节点

  6. 如果找到了备份树的当前游标节点的下一节点,扩展渲染树,将下一树节点复制到渲染树,同时将下一树节点的子节点的第一节点复制到渲染树

  7. 循环4-6,将备份树完全转移到渲染树,完成所有翻页


扩展思路


这个方法可以进行封装,将每次复制的节点数目和每次复制的子节点数目作为传参,一次可复制多个节点,这里就不做展开


作者:CodePlayer
来源:juejin.cn/post/7270503053358612520
收起阅读 »

你不会还在useEffect中请求数据吧

web
使用React Query代替useEffect获取数据的优势与对比 在构建现代React应用时,我们经常需要从后端API获取数据来渲染界面。传统的方式是使用React的useEffect钩子结合fetch或axios等HTTP请求库来完成数据获取和状态管理。...
继续阅读 »

使用React Query代替useEffect获取数据的优势与对比


在构建现代React应用时,我们经常需要从后端API获取数据来渲染界面。传统的方式是使用React的useEffect钩子结合fetchaxios等HTTP请求库来完成数据获取和状态管理。然而,随着React Query的出现,获取和同步服务器状态的方式得到了显著的改进。本文将详细介绍使用React Query代替useEffect获取数据的原因,并通过示例对比两种方式在代码层面的不同,在最后总结React Query的优势。


传统方式:使用useEffect获取数据


在没有使用React Query之前,我们通常会这样获取数据:


import React, { useState, useEffect } from 'react';

function MyComponent() {
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);

useEffect(() => {
const fetchData = async () => {
setIsLoading(true);
setError(null);
try {
const response = await fetch('https://my-api/data');
const result = await response.json();
setData(result);
} catch (error) {
setError(error);
}
setIsLoading(false);
};

fetchData();
}, []);

if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return <div>{JSON.stringify(data)}</div>;
}

这段代码虽然能工作,但存在几个问题:缺乏缓存策略、复杂的错误处理、不自动的数据更新、重复的数据请求等。


使用React Query改进数据获取


接下来,看看React Query如何为我们解决上述问题和简化代码:


import React from 'react';
import { useQuery } from 'react-query';

async function fetchData() {
const response = await fetch('https://my-api/data');
return response.json();
}

function MyComponent() {
const { data, isLoading, error } = useQuery('data', fetchData);

if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return <div>{JSON.stringify(data)}</div>;
}

在这个改进后的版本中,我们用useQuery钩子来代理数据加载。这行代码做了很多工作:它自动进行数据请求,处理加载状态和错误状态,还负责缓存和更新数据。


使用React Query的原因


简化的状态管理


React Query内部处理了数据的加载(isLoading)、数据更新(isFetching)、错误(error)状态管理,这使得开发者无需手动设置这些状态。


自动化的数据缓存和无效化


React Query还提供了出色的数据缓存策略。默认情况下,当组件卸载再重新挂载时,React Query会使用旧的缓存数据,同时在后台静默地为你刷新数据,保证数据的新鲜度。


更好的错误和重试处理


通过对错误状态的内部管理,React Query提供了错误捕获的机制并允许自动重试功能。这比手动实现要简单得多。


优化请求节省带宽


React Query会自动去重和合并并发的查询请求,减少不必要的网络请求,节省宽带。


React Query的优势


总结来说,React Query的主要优势包括:



  • 自动化:管理请求生命周期(查询、缓存、更新、重试)无需手动编写代码。

  • 减少样板代码:少写很多状态处理的逻辑,让代码简洁易维护。

  • 性能提升:智能缓存和数据更新策略,更少的重新渲染。

  • 鲁棒性:更健壮的错误处理和重试逻辑。

  • 开箱即用:丰富的功能如后台获取、分页、无限加载等。


在创建现代化的React应用程序时,React Query提供了一种更智能、更高效和简单的方法来处理数据获取和同步,这也是越来越多的React开发者选择它的原因。


React Query


下面将详细介绍React Query的功能,以及它如何在一个实际的场景中被使用。我们将构建一个用户列表的应用,这个应用将展示用户数据、支持数据刷新、加载更多用户以及处理错误重试。


项目准备


首先,确保已经在React项目中安装了React Query:


npm install react-query

或者


yarn add react-query

功能概览



  • 数据获取 (useQuery): 用于获取数据并提供状态管理,比如loading, error, data。

  • 缓存与背景更新 (staleTimecacheTime): 确定数据保持新鲜的时间,以及未被使用时保持在缓存中的时间。

  • 自动重试 (retry): 当请求失败时,自动进行重试。

  • 分页和加载更多 (页码或游标): 当我们需要分页或者无限加载数据时使用。

  • 数据预加载 (queryClient.prefetchQuery): 加载关键数据以提升用户体验。

  • 数据变异 (useMutation): 提交数据至服务器,并更新本地缓存。


示例应用


获取用户列表


我们使用useQuery钩子来获取用户数据。这个钩子会自动发起请求并监听数据状态。


import { useQuery } from 'react-query';

const fetchUsers = async (page = 0) => {
const response = await fetch(`https://my-api/users?page=${page}`);
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
};

function Users() {
const { data, error, isLoading, isFetching } = useQuery('users', fetchUsers);

if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;

return (
<>
{data.users.map(user => (
<div key={user.id}>{user.name}</div>
))}
{isFetching ? <span>Updating...</span> : null}
</>

);
}

自动刷新和背景更新


React Query可以配置数据自动刷新的时间,我们可以设置staleTime来避免不必要的后台更新,同时让我们的数据保持最新。


const { data } = useQuery('users', fetchUsers, {
staleTime: 5 * 60 * 1000 // 每5分钟更新一次数据
});

自动重试


如果请求失败,React Query可以自动尝试重新获取数据:


const { data } = useQuery('users', fetchUsers, {
retry: 2 // 请求失败会尝试2次重试
});

分页和加载更多


对于需要加载更多数据的情况,我们可以使用React Query的页码或游标方法来实现:


const { data, fetchNextPage, hasNextPage } = useInfiniteQuery(
'users',
({ pageParam = 0 }) => fetchUsers(pageParam),
{
getNextPageParam: (lastPage, allPages) => lastPage.nextPage,
}
);

// ...

<button onClick={() => fetchNextPage()} disabled={!hasNextPage}>
Load More
</button>


加载更多的按钮会根据hasNextPage来判断是否还有更多数据可以加载。


数据预加载


我们可以在用户的鼠标悬浮到某个按钮上时提前获取数据:


const queryClient = useQueryClient();

// ...

<button
onMouseEnter={() =>
queryClient.prefetchQuery('more-users-data', fetchAdditionalUsers)}
>
Show More Users
</button>


数据变异


当需要提交数据到服务端时,我们可以使用useMutation来处理:


import { useMutation, useQueryClient } from 'react-query';

const addUser = async (newUser) => {
const response = await fetch(`https://my-api/users`, {
method: 'POST',
body: JSON.stringify(newUser)
});
if (!response.ok) {
throw new Error('Could not add user');
}
return response.json();
};

function AddUser() {
const queryClient = useQueryClient();
const mutation = useMutation(addUser, {
onSuccess: () => {
// Invalidate and refetch
queryClient.invalidateQueries('users');
}
});

return (
<button
onClick={() =>
{
const newUser = { name: 'New User' };
mutation.mutate(newUser);
}}
>
Add User
</button>

);
}

当我们向服务端增加一个新用户时,使用useMutation并提供一个成功的回调,该回调通过调用queryClient.invalidateQueries来标记用户列表的缓存为无效,以便它可以自动重新获取最新的用户列表。


总结React Query的优势


通过上述示例,我们可以看到React Query提供了强大且灵活的功能来处理数据的获取、缓存、更新、预加载、变异等操作。它大大简化了数据同步和状态管理的复杂性,使开发者可以专注于构建交互式的用户界面,而不必担心数据操作的底层细节。此外,React Query的自动重试和智能缓存策略可以提高应用的健壮性和用户的体验。


最后,简要地复习一下React Query的优势:



  1. 内置缓存功能:React Query 为获取的数据提供缓存机制,这意味着当组件重新渲染或者同用户交互时,相同的数据正在加载,不需要再次发起网络请求,可以直接从缓存中获取数据。这减少了不必要的网络请求,提高了应用的效率。

  2. 错误处理和错误重试:在处理异常数据时,错误处理和错误和错误重试在其他较繁琐。React Query 提供了强化的方式来处理这些状态,简化了开发者的工作。

  3. 优化数据获取:React Query 会自动合并重复的查询请求,并将它们批量处理。这意味着如果多个组件请求相同的数据,React Query 只会发送一次网络请求,并且将数据分发给所有请求的组件。

  4. 简洁高效和提高内存性能:通过减少不必要的网络请求和优化数据处理,React Query 可以帮助节省带宽并提高应用的响应性能。

  5. 数据同步:在复杂的应用中,保持组件间数据的同步是一个挑战。React Query 通过其高层机制,帮助保持不同组件间数据的一致性。


作者:慕仲卿
来源:juejin.cn/post/7313242113436827686
收起阅读 »

深入探究npm run的底层原理

web
起因 某一天,我正在很悠闲的喝着小茶,无所事事浏览着技术文章,忽然看到一篇文章成功的吸引了我的眼球——你真的了解npm run吗,看完之后打开了一扇新世界的大门。 我陷入了沉思之后,有那么一瞬间感觉到了不可思议。惯性思维下,我们已经习惯性的认为命令就是要通过n...
继续阅读 »

起因


某一天,我正在很悠闲的喝着小茶,无所事事浏览着技术文章,忽然看到一篇文章成功的吸引了我的眼球——你真的了解npm run吗,看完之后打开了一扇新世界的大门。


我陷入了沉思之后,有那么一瞬间感觉到了不可思议。惯性思维下,我们已经习惯性的认为命令就是要通过npm run的方式进行执行,而没有进一步思考为什么是这样的。大多数可能跟我一样,第一反应是:难道不是因为在 package.json 中定义了各种的 script 命令,然后我们通过npm run xxx的方式进行执行吗?


揭开事情的迷雾,我们抱着打破沙锅问到底的心态来思考一下:


我们为什么要执行 npm run start 命令,而不是直接执行 react-scripts start 呢?


难道不是因为使用方便吗


这是我的第一反应,但是,实际上可能不是的。为了验证一下两者的区别,我立马跑去打开我的测试项目,分别使用了两种方式。意外的发现直接执行命令的方式竟然报错了,而通过npm run 的方式执行的时候一如既往的Ok。


image.png


为什么命令会不存在呢


报错信息很明显,直接执行命令的时候,系统报错:react-scripts 命令不存在。


这个时候我就感到有点不可思议了。


既然命令不存在,凭什么npm run就可以执行呢?


不知道大家在windows上安装node的时候,需要将node配置到系统环境变量里面去了,然后我们可以全局通过 node -v 来验证node是否安装成功和查询当前node版本信息。难道说 npm run 的玄机跟node配置过环境变量有关系吗?


抱着怀疑的态度,我在node文件夹中一通翻找(我是基于nvm进行node管理的,可能跟直接使用node的目录结构有所出入),终于找到了问题的关键信息:安装依赖的时候,会在这里创建几个命令文件。


image.png


经过几次反复的安装、删除依赖操作之后,终于确认了我的想法。每次我们通过 npm i xxx -g 安装某个依赖的时候,除了在node下的node_modules文件夹中安装对应的依赖包之外,还会在node下创建这个依赖的可执行文件(对应不同的环境,会有好几个不同的命令文件)。


这个时候,我忽然想起来了linux上的操作,在linux上安装全局依赖的时候,我们安装完依赖之后,还需要手动创建软连接。两相印证,事实的真相已经很明显了。


    ln -s /usr/local/src/nodejs/bin/node /usr/local/bin/node

ln -s /usr/local/src/nodejs/bin/npm /usr/local/bin/npm

安装依赖的时候,会在bin目录下创建一个对应的可行性文件,这个其实就跟我们node文件夹下创建的这个npm文件夹的性质是一样的。


npm run 命令可执行总结


我们经过一番摸索终于弄清楚了,这里我们再一起来总结一下:



  1. 我们安装依赖的时候,在对应的文件夹下创建了对应的可行性命令;

  2. 我们执行npm run命令的时候会在当前目录中查找相关命令,如果找到的话,直接运行对应的命令;

  3. 如果没有找到的话,会到全局的node文件夹下查找相关的命令,如果找到的话,直接运行对应的命令;

  4. 如果依然没有找到的话,就会报错误信息了


为什么会创建多个可执行文件呢


前面我们说到了,创建的可执行文件是有多个。细心的你可能已经注意到其中的一个可执行文件xxx.cmd了,它的类型很明显已经告诉我们它是什么了 —— Windows 命令脚本。大胆的猜测一下:另外几个分别对应的是不同环境的可执行命令,比方说:没有文件后缀的可执行文件,其实就是我们前面说到的在linux中安装的软链接的方式。


我们大致看一下其中一个cross-env.cmd的可执行命令的内容(假装可以看得懂)。


    @ECHO off
SETLOCAL
CALL :find_dp0

IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)

"%_prog%" "%dp0%\node_modules\cross-env\src\bin\cross-env.js" %*
ENDLOCAL
EXIT /b %errorlevel%
:find_dp0
SET dp0=%~dp0
EXIT /b

虽然看不懂,但是其中很重要的一个点我们其实还是可以猜出来的 "%_prog%" "%dp0%\node_modules\cross-env\src\bin\cross-env.js" %*将可执行命令 cross-env 指向对应的依赖的bin文件 bin\cross-env.js。


这里其实变相的给我们解释了另外一个问题。


为什么安装依赖可以创建可执行命令呢


在依赖的package.json中配置了bin属性,定义了可执行命令的名字和可执行命令的文件,当我们通过npm安装依赖的时候,npm就会根据声明的bin属性来创建对应的可执行文件。


    "bin": {
"cross-env": "src/bin/cross-env.js",
"cross-env-shell": "src/bin/cross-env-shell.js"
},

相信看到这里之后,大家应该心里已经很清楚npm run的底层原理了。


打完收功


好了,有关npm run的内容暂时就这么多了,希望对大家有所帮助。


欢迎大家在下方进行留言交流。


作者:花开花落花中妖
来源:juejin.cn/post/7313203461705580580
收起阅读 »

董老师的话充满力量——手写call、apply、bind

web
前言: 大家好,我是小瑜。最近在网上看到了东方甄选和董宇辉的小作文事件,我也一直是众多吃瓜群众的一员,看完后董宇辉俞敏洪的联合直播,心中也有很多感触。 董宇辉老师说,一放假,就喜欢往家里跑,因为接近土地可以感到踏实。一个千万网红这种纯粹质朴的精神着实让人很贴切...
继续阅读 »

前言:


大家好,我是小瑜。最近在网上看到了东方甄选和董宇辉的小作文事件,我也一直是众多吃瓜群众的一员,看完后董宇辉俞敏洪的联合直播,心中也有很多感触。


董宇辉老师说,一放假,就喜欢往家里跑,因为接近土地可以感到踏实。一个千万网红这种纯粹质朴的精神着实让人很贴切。


特别是董老师的另外一番话:你必须人生中有一段经历是自己走过去的。你充满了痛苦,然后充满了孤独,但这个东西叫做成长,好的生活和幸福的经历是不能带来成长的,所以能见到很多人,四五十岁看着还很幼稚,说明他从来没有受到苦,能让你成长的东西,就是让你反思的东西,因为在历史的长河中进化是痛苦的,逼不得已,人才会进步很成长,所以成长都不快乐。但同时也恭喜你一直在成长。


在学习以及编写这篇文章的时候,我也是痛苦的,同时也有所收获。接下来给大家分享this指向以及手写call、apply、bind。


在这之间给大家简单举几个例子说明下this指向的不同


普通函数调用


// 谁调用就是谁, 直接调用window
function sayHi() {
console.log(this); // window
}
sayHi() // === window.sayHi()

对象中的方法调用


const obj = {
name: 'zs',
objSayHi() {
console.log(this) // obj
setTimeout(() => {
console.log(this, 'setTimeout'); // obj
}, 1000),
function inner() {
console.log(this); // window
}
inner()
},
qwe: () => console.log(this) // window
}
obj.objSayHi()
obj.qwe()

obj.objSayHi() => obj



  • 因为是 **obj **对象调用,所以 **this **指向 **obj **这个对象


obj.qwe() => window



  • 对于箭头函数 qwe,它捕获的是定义时外部的 this 上下文。在浏览器中全局范围内的箭头函数 qwethis 指向的是全局对象 window(或者是全局的 this,具体取决于执行上下文)。


inner() => window



  • inner() 函数是通过常规函数声明方式定义的。在 JavaScript 中,常规函数声明方式中的 this 在严格模式下指向 undefined,而在非严格模式下(例如浏览器环境中),this 指向全局对象(在浏览器中通常是 window 对象)。因此,当 inner() 函数在 objSayHi() 方法内部被调用时,其 this 指向全局对象 window


setTimeout => obj



  • objSayHi 方法中,setTimeout 中的回调函数使用了箭头函数。箭头函数内部的 this 会捕获最近的普通函数(非箭头函数)的 this 值,也就是 objSayHi 被调用时的 this。因此,setTimeout 中的箭头函数捕获到的 this 值指向的是 obj 对象。


总结:浏览器环境中, 谁调用this指向谁,但是箭头函数的this义是外部的 this 上下文。通过常规函数声明方式定义this指向window。其他关于this指向可以参考这张图


image.png


修改this指向


call


第1个参数为this,第2-n为传入该函数的参数


function myThis1(name, age) {
console.log(this);
console.log(name);
console.log(age);
}
const obj = {
name: 'zs',
age: 18
}
myThis1.call(obj, 'ls', 20) // {name:"zs",age:18} ls 18

apply


第1个参数为this,第2-n已数组的方式传递


function myThis1(name, age) {
console.log(this);
console.log(name);
console.log(age);
}
const obj = {
name: 'zs',
age: 18
}
myThis1.apply(obj, ['王五', 18]) // {name:"zs",age:18} 王五 18

bind


bind() 方法创建一个新函数,当调用该新函数时,它会调用原始函数并将其 this 关键字设置为给定的值,同时,还可以传入一系列指定的参数


function myThis1(name, age) {
console.log(this);
console.log(name);
console.log(age);
}
const obj = {
name: 'zs',
age: 18
}

![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/92cfe20fc6374374bacf97bcc3d31ac6~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=814&h=308&s=145955&e=png&b=fafafa)
const fn = myThis1.bind(obj, '赵六',30)
fn() // {name:"zs",age:18} 赵六 30

手写call函数


要求实现


const obj = {
name: 'zs',
age: 20
}
function myFn(a, b, c, d, e) {
console.log(`大家好,我的名字叫${this.name} 我今年${this.age}岁了`, a, b, c, d, e);
}
myFn.myCall(obj, 1, 2, 3, 4, 5)

简单思路:



  1. ** **原本并不存在 **myCall **方法,那么如何去创建这个方法?

  2. 如何让函数内部的 **this **为 某个对象?

  3. 如何将调用时传入的参数传入到 **myFn **函数中?


实现思路1:通过函数原型的方式,给原型添加 myCall 方法,这样通过原型链就可以使用


Function.prototype.myCall = function () {
console.log('myCall被调用了');
}
myFn.myCall()


实现思路2:在myCall调用的时候将obj传入到函数中,并根据谁调用this就指向谁的原则给对象添加this方法并执行


首先可以打印看一下thisArg,this 分别是什么


const obj = {
name: 'zs',
age: 20
}
Function.prototype.myCall = function (thisArg) {
console.log('myCall被调用了',thisArg,this);
}
function myFn(a, b, c, d, e) {
console.log(`大家好,我的名字叫${this.name} 我今年${this.age}岁了`, a, b, c, d, e);
}
myFn.myCall(obj)

image.png
很明显 **thisArg 就是 obj **对象 而 this就是 myFn 这个函数,那么就可以根据谁调用this就指向谁的原则,将obj这个对象也就是 **thisArg **添加 myCall 方法 = this


  Function.prototype.myCall = function (thisArg) {
console.log('myCall被调用了', thisArg, this);
thisArg.myCall = this
thisArg.myCall()
}
function myFn(a, b, c, d, e) {
console.log(`大家好,我的名字叫${this.name} 我今年${this.age}岁了`, a, b, c, d, e);
}
const obj = {
name: 'zs',
age: 20
}
myFn.myCall(obj, 1, 2, 3, 4, 5)

此时就发现,this已经成功指向了这个obj对象,但是还差参数没有传递,接下去就去实现


image.png


实现思路3:利用剩余参数加展开运算符传入参数


  Function.prototype.myCall = function (thisArg,...args) {
console.log('myCall被调用了', thisArg, this);
thisArg.myCall = this
thisArg.myCall(...args)
}
function myFn(a, b, c, d, e) {
console.log(`大家好,我的名字叫${this.name} 我今年${this.age}岁了`, a, b, c, d, e);
}
const obj = {
name: 'zs',
age: 20
}
myFn.myCall(obj, 1, 2, 3, 4, 5)

此时就基本上可以完成了,还有一点优化,就是查看** obj 发现 myCall **是一直存在的,因为之前通过给原型添加方法,希望的是使用完成后将myCall方法删除,这里只需要 在 **myCall 最后再添加一句 delete thisArg.myCall **即可


优化: 增加返回值并 利用 Symbol 动态生成唯一的属性名


Function.prototype.myCall = function (thisArg, ...args) {
const key = Symbol()
thisArg[key] = this
const res = thisArg[key](...args)
delete thisArg[key]
return res
}

手写apply


apply 方法同理 call 只是第二个参数需要改为数组


Function.prototype.myApply = function (thisArg, args) {
console.log(args);
const key = Symbol()
thisArg[key] = this
const res = thisArg[key](args)
delete thisArg[key]
return res
}
const obj = {
name: 'zs',
age: 20
}
function myFn(args) {
const div = `大家好,我的名字叫${this.name} 我今年${this.age}岁了,${args.toString()}`
return div
}
const res = myFn.myApply(obj, [1, 2, 3, 4, 5])
console.log(res);

手写bind


Function.prototype.myBind = function (thisArg, ...args) {
const fn = this
return function (...args1) {
const allArgs = [...args, ...args1]
// 判断是否为new的构造函数
if (new.target) {
return new fn(...allArgs)

} else {
return fn.call(thisArg, allArgs)
}
}
}
const obj = {
name: 'zs',
age: 20
}
function myFn(...arg) {
console.log(`大家好,我的名字叫${this.name} 我今年${this.age}岁了,${arg}`);
const div = `大家好,我的名字叫${this.name} 我今年${this.age}岁了,${arg}`
return div
}
const res = myFn.myBind(obj, '1')
console.log(res('122'));

作者:不知名小瑜
来源:juejin.cn/post/7313135267572121612
收起阅读 »

你真的需要Pinia🍍吗?

web
尤大大:理论上来说,每一个 Vue 组件实例都已经在“管理”它自己的响应式状态了。 🤦‍♂️:不会吧🤡!既然Vue本身具备状态管理的能力,我们还有必要引入Pinia🍍或者Vuex等状态管理工具吗? Vue实例作为状态管理器应该怎么实现?按照vue官网我们来实践...
继续阅读 »

尤大大:理论上来说,每一个 Vue 组件实例都已经在“管理”它自己的响应式状态了


🤦‍♂️:不会吧🤡!既然Vue本身具备状态管理的能力,我们还有必要引入Pinia🍍或者Vuex等状态管理工具吗?


Vue实例作为状态管理器应该怎么实现?按照vue官网我们来实践一次。


简单状态管理 😎


状态管理器


我们以Vue3为例,实现一个状态管理。首先创建一个名为auth.ts的ts文件,这文件将用来定义状态管理器。


import { reactive, readonly } from 'vue';

export interface Account {
name: string;
}

export interface AuthStore {
account: Account | null;
isAuthed: boolean;
}

const auth = reactive<AuthStore>({
isAuthed: false,
account: null,
});

export const useAuthStore = () => {
return {
state: readonly(auth),
actions: {
login(account: Account) {
auth.isAuthed = true;
auth.account = account;
},
logout() {
auth.isAuthed = false;
auth.account = null;
},
},
};
};

export default useAuthStore;

接下来创建两个组件Info.vueLogin.vue,在这两个组件中使用我们自定义的useAuthStore状态管理器。


Login.vue


使用import { useAuthStore } from '../auth';来引入这个store,通过useAuthStore()获取store实例。


<script setup lang="ts">
import { ref } from 'vue';

import { useAuthStore } from '../auth';

const username = ref('');
const { state, actions } = useAuthStore();
</script>

<template>
<div>
<div v-if="!state.isAuthed">
<div>
<span>用户名:</span>
<input v-model="username" />
</div>
<button @click="actions.login({ name: username })">登录</button>
</div>
<button v-if="state.isAuthed" @click="actions.logout">退出</button>
</div>
</template>

Info.vue


<script setup lang="ts">
import { useAuthStore } from '../auth';

const { state } = useAuthStore();
</script>

<template>
<div>
<div v-if="!state.isAuthed">
<h1>请登录</h1>
</div>
<div v-if="state.isAuthed">
<h1>欢迎:{{ state.account?.name }}</h1>
</div>
</div>
</template>

使用效果


Kapture 2023-06-25 at 21.35.52.gif


解读


使用reactive()是因为State是一个对象,当然也可以使用ref()。但是,就必须使用.value来访问数据,这并不是想要的效果。


为了实现单向数据流useAuthStore中的State采用Vue3的readonly API将状态对象置为只读的对象,这样避免了在使用该状态对象时直接操作State的情况。因此想要修改State就只能通过Actions,就像下图这样:


image.png


Vue2也可以么?😲


虽然 Vue2 中没有reactive()ref()API,但是事实是 Vue2 也实现简单的状态管理。利用 Vue2 中的Vue.observable()可以将一个普通对象转换为响应式对象,从而实现当State变更时驱动View更新。


🤔需要注意的是 Vue2 中没有 readonly() API,因此在这个例子中,我们直接使用 auth 作为状态。要确保状态不被意外修改,你需要确保只在 actions 对象中的方法内修改状态。


import Vue from 'vue';

export interface Account {
name: string;
}

export interface AuthStore {
account: Account | null;
isAuthed: boolean;
}

const auth = Vue.observable<AuthStore>({
isAuthed: false,
account: null,
});

export const useAuthStore = () => {
return {
state: auth,
actions: {
login(account: Account) {
auth.isAuthed = true;
auth.account = account;
},
logout() {
auth.isAuthed = false;
auth.account = null;
},
},
};
};

export default useAuthStore;

Login.vue


在vue2中将useAuthStore()解构进组件的data中即可。


<template>
...
</template>

<script>
import { useAuthStore } from '../auth';

export default {
data() {
const { state, actions } = useAuthStore();
return {
authState: state,
login: actions.login,
logout: actions.logout,
};
},
};
</script>

首先从 useAuthStore 文件中导入 useAuthStore 函数。然后,在组件的 data 选项中,我们调用 useAuthStore() 并将返回的 state 和 actions 解构。接下来,我们将 statelogin 和 logout 添加到组件的响应式数据中,以便在模板中使用。最后,在模板中,我们根据 authState.isAuthed 的值显示不同的内容,并使用 login 和 logout 方法处理按钮点击事件。


关于服务器端渲染 🧐


在 SSR 环境下,应用模块通常只在服务器启动时初始化一次。同一个应用模块会在多个服务器请求之间被复用,而我们的单例状态对象也一样。如果我们用单个用户特定的数据对共享的单例状态进行修改,那么这个状态可能会意外地泄露给另一个用户的请求。我们把这种情况称为跨请求状态污染


如果使用 SSR,则需要避免所有请求共享同一存储。在这种情况下,需要为每个请求创建一个单独的存储并提供/注入它。


// app.js (在服务端和客户端间共享)
import { createSSRApp } from 'vue'
import { createStore } from './store.js'

// 每次请求时调用
export function createApp() {
const app = createSSRApp(/* ... */)
// 对每个请求都创建新的 store 实例
const store = createStore(/* ... */)
// 提供应用级别的 store
app.provide('store', store)
// 也为激活过程暴露出 store
return { app, store }
}

优势与不足


优势



  • 简单易学:对于初学者和小型项目来说,这种方法更容易理解和实现。它不需要引入额外的库或学习新的概念。

  • 轻量级:由于不需要引入额外的库,这种方法在体积上更轻量级,对于那些对性能有严格要求的项目来说,这可能是一个优势。

  • 灵活性:这种方法允许开发人员根据项目需求自由地调整状态管理结构。这种灵活性可能适用于一些具有特殊需求的项目。


不足



  • 缺乏结构和约束:这种方法没有强制执行任何特定的结构或约束,这可能导致不一致的代码和难以维护的项目。当多个开发人员协同工作时,这可能会导致问题。

  • 缺乏调试工具:与像 Vuex 或 Pinia 这样的专门的状态管理库相比,这种方法没有提供调试工具,这可能会使调试和追踪状态变更更加困难。

  • 可扩展性:对于大型应用程序,这种简单的状态管理可能不够强大,因为它可能无法很好地处理复杂的状态逻辑和多个状态模块。

  • 性能优化:这种方法可能无法提供像 Vuex 或 Pinia 这样的库所提供的性能优化,例如,缓存计算属性。


🚀简单状态管理 vs Pinia🚀


开发 Vue 应用时,状态管理是一个重要的考虑因素。Vue 自身提供了一些状态管理工具,如 ref 和 reactive,但在某些情况下,引入专门的状态管理库(如 Pinia 或 Vuex)可能会带来更多的便利和优势。那么,在什么情况下你真的需要 Pinia?让我们来总结一下。


使用 Vue 自身的状态管理


在以下场景下,使用 Vue 自身的状态管理就可以完美解决问题:



  1. 当应用的规模较小,组件层级较浅时,Vue 自身的状态管理可以很好地处理状态。

  2. 当组件之间的状态共享较少,且状态变化较简单时,Vue 的响应式系统足以应对这些需求。

  3. 当应用的状态变化逻辑较为简单,易于维护时,Vue 的状态管理可以很好地解决问题。


在这些场景下,使用 Vue 自身的状态管理,如 ref 和 reactive,可以满足应用的需求,而无需引入额外的状态管理库。


这样的小型项目存在吗?


小型项目通常具有以下特点:



  1. 功能有限:项目的功能和需求相对较少,不需要复杂的状态管理。

  2. 规模较小:项目的代码量和组件数量较少,易于维护。

  3. 开发周期短:项目的开发和发布周期相对较短。

  4. 团队规模较小:负责项目的开发人员数量较少。


这些小型项目可能包括个人博客、简历网站、小型企业网站、原型和概念验证等。


何时考虑使用 Pinia


选择是否一开始就使用 Pinia 取决于项目的需求和预期的复杂性。以下是一些建议:



  1. 如果您预计项目将迅速增长并变得复杂,那么从一开始就使用 Pinia 可能是一个明智的选择。这样,您可以从一开始就利用 Pinia 提供的强大功能、更好的开发体验和更强的约定。

  2. 如果项目是一个小型项目,且预计不会变得很复杂,那么可以从简单的状态管理方法开始。这样,您可以减少项目的依赖和包大小,同时保持灵活性。然后,根据项目的发展情况,您可以在需要时迁移到 Pinia。

  3. 如果您的团队已经熟悉 Pinia 或类似的状态管理库,那么从一开始就使用 Pinia 可能会使团队更加高效。


总之,在决定是否从一开始就使用 Pinia 时,您应该权衡项目的需求、预期的复杂性和团队的经验。如果您认为 Pinia 可以为您的项目带来长期的好处,那么从一开始就使用它是合理的。


最后


没有最好的架构,只有最合适的选择。对于小型项目和初学者,简单的状态管理方法可能是一个合适的选择。然而,在大型、复杂的应用程序中,使用像 Pinia 这样的专门的状态管理库可能更加合适,因为它们提供了更强大的功能、更好的开发体验和更强的约定。


关于项目是否应该使用第三方的状态管理库,完全取决于项目自身和开发团队的选择!


如果您有不同的看法,以自身看法为准


作者:youth君
来源:juejin.cn/post/7248606372954456120
收起阅读 »

第一次使用canvas,实现环状类地铁时刻图

web
前情提要 今天,产品找到我,说能不能实现这个图呢 众所周知,产品说啥就是啥,于是就直接开干。 小波折 为了实现我的需求做了下调研,看了d3还有x6之类的库,感觉都太重了,也不一定能实现我的需求。于是在沸点上寻求了下建议 掘友们建议canvas直接画 于是...
继续阅读 »

前情提要


今天,产品找到我,说能不能实现这个图呢


image.png


众所周知,产品说啥就是啥,于是就直接开干。


小波折


为了实现我的需求做了下调研,看了d3还有x6之类的库,感觉都太重了,也不一定能实现我的需求。于是在沸点上寻求了下建议



掘友们建议canvas直接画



于是决定手撸


结果


之前没有使用canvas画过东西,于是花了一天边看文档,边画,最终画完了,效果如下:


image.png


代码及思路


首先构造数据集在画布上的节点位置


 let stations = new Array(13).fill(null);

/** 拐角的节点 */
const cornerP = [
{ x: 20, y: 67.5, type: 'corner', showP: true },
{ x: 55, y: 47.5, type: 'corner', showP: false },
{ x: 337.5, y: 47.5, type: 'corner', showP: false },
{ x: 337.5, y: 112.5, type: 'corner', showP: false },
{ x: 55, y: 112.5, type: 'corner', showP: false },
{ x: 20, y: 92.5, type: 'corner', showP: true },
];
/** 生成站点笔触位置 */
function getStationsPosition(): {
num: string;
status: number;
x: number;
y: number;
type?: string;
}[] {
const middleIndex = Math.floor(stations.length / 2);
const { width, height } = canvasRef.current as HTMLCanvasElement;
let centerPoint = { x: width - 20, y: height / 2 + 20 };
let leftArr = stations.filter((v, _i) => _i < middleIndex);
const leftWidth = (width - 40 - 35 - 32.5) / (leftArr.length - 1);
const leftP = leftArr.map((v, i) => ({
x: leftWidth * i + 55,
y: height / 2 + 20 - 32.5,
}));

const rightArr = stations.filter((v, _i) => _i > middleIndex);
const rightWidth = (width - 40 - 35 - 32.5) / (rightArr.length - 1);
const rightP = rightArr.map((v, i) => ({
x: 370 - 32.5 - rightWidth * i,
y: height / 2 + 20 + 32.5,
}));

return [
cornerP[0],
cornerP[1],
...leftP,
cornerP[2],
centerPoint,
cornerP[3],
...rightP,
cornerP[4],
cornerP[5],
].map((v, i) => ({
...v,
num: String(2),
status: i > 3 ? 0 : i > 2 ? 1 : 2,
}));
}

为了避免实际使用过程中,数据点位不够,上面的点位生成主动加入了拐角的点位。


然后画出背景路径


   function drawBgLine(
points: ReturnType<typeof getStationsPosition>,
color?: string,
) {
const ctx = canvasRef.current?.getContext(
'2d',
) as unknown as CanvasRenderingContext2D;

points.forEach((item, index) => {
const next = points[index + 1];
if (next) {
if (next.y === item.y) {
ctx.beginPath();
ctx.moveTo(item.x, item.y);
ctx.lineWidth = 4;
ctx.strokeStyle = color ?? 'rgba(55, 59, 62, )';
ctx.lineTo(next.x, next.y);
ctx.stroke();
} else if (Math.abs(next.y - item.y) === 32.5) {
ctx.beginPath();
if (next.x < item.x) {
ctx.arc(
next.x,
item.y,
32.5,
(Math.PI / 180) * 0,
(Math.PI / 180) * 90,
);
} else {
ctx.arc(
item.x,
next.y,
32.5,
(Math.PI / 180) * 270,
(Math.PI / 180) * 0,
);
}
ctx.lineWidth = 4;
ctx.strokeStyle = color ?? 'rgba(55, 59, 62, 0.5)';
ctx.stroke();
} else if (Math.abs(next.y - item.y) < 32.5) {
ctx.beginPath();
if (next.x < item.x) {
ctx.moveTo(item.x, item.y);
ctx.quadraticCurveTo(next.x, item.y, next.x, next.y);
ctx.lineWidth = 4;
ctx.strokeStyle = color ?? 'rgba(55, 59, 62, 0.5)';
ctx.stroke();
ctx.beginPath();
ctx.fillStyle = color ?? 'rgba(55, 59, 62, 0.5)';

ctx.arc(next.x, next.y, 4, 0, Math.PI * 2);

ctx.fill();
} else {
ctx.moveTo(item.x, item.y);
ctx.quadraticCurveTo(item.x, next.y, next.x, next.y);
ctx.lineWidth = 4;
ctx.strokeStyle = color ?? 'rgba(55, 59, 62, 0.5)';
ctx.stroke();
ctx.beginPath();
ctx.fillStyle = color ?? 'rgba(55, 59, 62, 0.5)';

ctx.arc(item.x, item.y, 4, 0, Math.PI * 2);
ctx.fill();
}
}
}
});
}

此处主要的思路是根据相领点位的高低差,来画不同的路径


然后画进度图层


  function drawProgressBgLine(points: ReturnType<typeof getStationsPosition>) {
const ctx = canvasRef.current?.getContext(
'2d',
) as unknown as CanvasRenderingContext2D;

const index = points.findIndex((v) => v.status === 0);

const newArr = points.slice(0, index);
const lastEl = points[index];
const curEl = points[index - 1];
console.log(lastEl, curEl);

if (lastEl) {
/**处于顶部的时候画出箭头 */
if (lastEl.y === curEl.y) {
if (lastEl.x > curEl.x) {
const centerP = {
x: (lastEl.x - curEl.x) / 2 + curEl.x,
y: curEl.y,
};
const img = new Image();
img.src = carIcon;
img.onload = function () {
ctx.drawImage(img, centerP.x - 12, centerP.y - 32, 19, 24);
};

ctx.beginPath();
ctx.moveTo(curEl.x, curEl.y);
ctx.lineTo(centerP.x, centerP.y);
/**生成三角形标记 */
ctx.lineTo(centerP.x, centerP.y - 2);
ctx.lineTo(centerP.x + 3, centerP.y);
ctx.lineTo(centerP.x, centerP.y + 2);
ctx.lineTo(centerP.x, centerP.y);
ctx.fillStyle = 'rgba(107, 255, 236, 1)';
ctx.fill();

ctx.lineWidth = 4;
ctx.strokeStyle = 'rgba(107, 255, 236, 1)';
ctx.stroke();
}
/** 其他条件暂时留空 */
}
}

/** 生成带进度颜色背景 */
drawBgLine(newArr, 'rgba(107, 255, 236, 1)');
}

主要是已经走过的路径线路变蓝,未走过的,获取两点中间位置,添加图标,箭头。这里箭头判断我未补全,等待实际使用补全


最后画出节点就可以了


  function draw() {
if (!canvasRef.current) {
return;
}
const ctx = canvasRef.current?.getContext(
'2d',
) as unknown as CanvasRenderingContext2D;

ctx.clearRect(0, 0, canvasRef.current?.width, canvasRef.current?.height);
if (ctx) {
/** 绘制当前遍数的文字 */
ctx.font = '12px serif';

ctx.fillStyle = '#fff';
ctx.fillText(text, 10, canvasRef.current?.height / 2 + 24);

const points = getStationsPosition();
/** 画出背景线 */
drawBgLine(points);

/** 画出当前进度 */
drawProgressBgLine(points);

points.forEach((item) => {
if (item.type !== 'corner') {
ctx.clearRect(item.x - 6, item.y - 6, 12, 12);
ctx.beginPath();
/** 生成标记点 */
ctx.moveTo(item.x, item.y);

ctx.fillStyle =
item.status === 2
? 'rgba(255, 157, 31, 1)'
: item.status === 1
? 'rgba(107, 255, 236, 1)'
: 'rgba(55, 59, 62, 1)';
ctx.arc(item.x, item.y, 4, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.lineWidth = 1;
ctx.strokeStyle = 'rgba(55, 59, 62, 1)';
ctx.arc(item.x, item.y, 6, 0, Math.PI * 2);
ctx.stroke();
ctx.closePath();

ctx.fillStyle = '#fff';
ctx.fillText(item.num, item.x - 4, item.y - 12);
}
});
}
}

最后贴一下全部代码


import carIcon from '@/assets/images/map/map_car1.png';
import { useEffect, useRef } from 'react';
const LineCanvas = () => {
const canvasRef = useRef<HTMLCanvasElement>(null);
let text = '第3遍(15:00-18:00)';

let stations = new Array(13).fill(null);

/** 拐角的节点 */
const cornerP = [
{ x: 20, y: 67.5, type: 'corner', showP: true },
{ x: 55, y: 47.5, type: 'corner', showP: false },
{ x: 337.5, y: 47.5, type: 'corner', showP: false },
{ x: 337.5, y: 112.5, type: 'corner', showP: false },
{ x: 55, y: 112.5, type: 'corner', showP: false },
{ x: 20, y: 92.5, type: 'corner', showP: true },
];
/** 生成站点笔触位置 */
function getStationsPosition(): {
num: string;
status: number;
x: number;
y: number;
type?: string;
}[] {
const middleIndex = Math.floor(stations.length / 2);
const { width, height } = canvasRef.current as HTMLCanvasElement;
let centerPoint = { x: width - 20, y: height / 2 + 20 };
let leftArr = stations.filter((v, _i) => _i < middleIndex);
const leftWidth = (width - 40 - 35 - 32.5) / (leftArr.length - 1);
const leftP = leftArr.map((v, i) => ({
x: leftWidth * i + 55,
y: height / 2 + 20 - 32.5,
}));

const rightArr = stations.filter((v, _i) => _i > middleIndex);
const rightWidth = (width - 40 - 35 - 32.5) / (rightArr.length - 1);
const rightP = rightArr.map((v, i) => ({
x: 370 - 32.5 - rightWidth * i,
y: height / 2 + 20 + 32.5,
}));

return [
cornerP[0],
cornerP[1],
...leftP,
cornerP[2],
centerPoint,
cornerP[3],
...rightP,
cornerP[4],
cornerP[5],
].map((v, i) => ({
...v,
num: String(2),
status: i > 3 ? 0 : i > 2 ? 1 : 2,
}));
}

function drawBgLine(
points: ReturnType<typeof getStationsPosition>,
color?: string,
) {
const ctx = canvasRef.current?.getContext(
'2d',
) as unknown as CanvasRenderingContext2D;

points.forEach((item, index) => {
const next = points[index + 1];
if (next) {
if (next.y === item.y) {
ctx.beginPath();
ctx.moveTo(item.x, item.y);
ctx.lineWidth = 4;
ctx.strokeStyle = color ?? 'rgba(55, 59, 62, 0.5)';
ctx.lineTo(next.x, next.y);
ctx.stroke();
} else if (Math.abs(next.y - item.y) === 32.5) {
ctx.beginPath();
if (next.x < item.x) {
ctx.arc(
next.x,
item.y,
32.5,
(Math.PI / 180) * 0,
(Math.PI / 180) * 90,
);
} else {
ctx.arc(
item.x,
next.y,
32.5,
(Math.PI / 180) * 270,
(Math.PI / 180) * 0,
);
}
ctx.lineWidth = 4;
ctx.strokeStyle = color ?? 'rgba(55, 59, 62, 0.5)';
ctx.stroke();
} else if (Math.abs(next.y - item.y) < 32.5) {
ctx.beginPath();
if (next.x < item.x) {
ctx.moveTo(item.x, item.y);
ctx.quadraticCurveTo(next.x, item.y, next.x, next.y);
ctx.lineWidth = 4;
ctx.strokeStyle = color ?? 'rgba(55, 59, 62, 0.5)';
ctx.stroke();
ctx.beginPath();
ctx.fillStyle = color ?? 'rgba(55, 59, 62, 0.5)';

ctx.arc(next.x, next.y, 4, 0, Math.PI * 2);

ctx.fill();
} else {
ctx.moveTo(item.x, item.y);
ctx.quadraticCurveTo(item.x, next.y, next.x, next.y);
ctx.lineWidth = 4;
ctx.strokeStyle = color ?? 'rgba(55, 59, 62, 0.5)';
ctx.stroke();
ctx.beginPath();
ctx.fillStyle = color ?? 'rgba(55, 59, 62, 0.5)';

ctx.arc(item.x, item.y, 4, 0, Math.PI * 2);
ctx.fill();
}
}
}
});
}

function drawProgressBgLine(points: ReturnType<typeof getStationsPosition>) {
const ctx = canvasRef.current?.getContext(
'2d',
) as unknown as CanvasRenderingContext2D;

const index = points.findIndex((v) => v.status === 0);

const newArr = points.slice(0, index);
const lastEl = points[index];
const curEl = points[index - 1];
console.log(lastEl, curEl);

if (lastEl) {
/**处于顶部的时候画出箭头 */
if (lastEl.y === curEl.y) {
if (lastEl.x > curEl.x) {
const centerP = {
x: (lastEl.x - curEl.x) / 2 + curEl.x,
y: curEl.y,
};
const img = new Image();
img.src = carIcon;
img.onload = function () {
ctx.drawImage(img, centerP.x - 12, centerP.y - 32, 19, 24);
};

ctx.beginPath();
ctx.moveTo(curEl.x, curEl.y);
ctx.lineTo(centerP.x, centerP.y);
/**生成三角形标记 */
ctx.lineTo(centerP.x, centerP.y - 2);
ctx.lineTo(centerP.x + 3, centerP.y);
ctx.lineTo(centerP.x, centerP.y + 2);
ctx.lineTo(centerP.x, centerP.y);
ctx.fillStyle = 'rgba(107, 255, 236, 1)';
ctx.fill();

ctx.lineWidth = 4;
ctx.strokeStyle = 'rgba(107, 255, 236, 1)';
ctx.stroke();
}
/** 其他条件暂时留空 */
}
}

/** 生成带进度颜色背景 */
drawBgLine(newArr, 'rgba(107, 255, 236, 1)');
}
function draw() {
if (!canvasRef.current) {
return;
}
const ctx = canvasRef.current?.getContext(
'2d',
) as unknown as CanvasRenderingContext2D;

ctx.clearRect(0, 0, canvasRef.current?.width, canvasRef.current?.height);
if (ctx) {
/** 绘制当前遍数的文字 */
ctx.font = '12px serif';

ctx.fillStyle = '#fff';
ctx.fillText(text, 10, canvasRef.current?.height / 2 + 24);

const points = getStationsPosition();
/** 画出背景线 */
drawBgLine(points);

/** 画出当前进度 */
drawProgressBgLine(points);

points.forEach((item) => {
if (item.type !== 'corner') {
ctx.clearRect(item.x - 6, item.y - 6, 12, 12);
ctx.beginPath();
/** 生成标记点 */
ctx.moveTo(item.x, item.y);

ctx.fillStyle =
item.status === 2
? 'rgba(255, 157, 31, 1)'
: item.status === 1
? 'rgba(107, 255, 236, 1)'
: 'rgba(55, 59, 62, 1)';
ctx.arc(item.x, item.y, 4, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.lineWidth = 1;
ctx.strokeStyle = 'rgba(55, 59, 62, 1)';
ctx.arc(item.x, item.y, 6, 0, Math.PI * 2);
ctx.stroke();
ctx.closePath();

ctx.fillStyle = '#fff';
ctx.fillText(item.num, item.x - 4, item.y - 12);
}
});
}
}

useEffect(() => {
draw();
}, []);

return <canvas ref={canvasRef} width="390" height="120"></canvas>;
};

export default LineCanvas;


转载请注明出处!


作者:MshengYang_lazy
来源:juejin.cn/post/7312723512724439094
收起阅读 »

Echarts高级配色

web
Echarts高级配色 Echarts是一款功能强大的JavaScript图表库,能够为用户提供丰富多样的数据可视化效果。其中,配色是图表呈现中非常重要的一部分,合适的配色方案能够使图表更加美观、易于辨识,并提升数据可视化的表达力。Echarts提供了多种高级...
继续阅读 »

Echarts高级配色


Echarts是一款功能强大的JavaScript图表库,能够为用户提供丰富多样的数据可视化效果。其中,配色是图表呈现中非常重要的一部分,合适的配色方案能够使图表更加美观、易于辨识,并提升数据可视化的表达力。Echarts提供了多种高级配色设置的方式,让用户可以根据自己的需求,轻松地定制图表的配色方案。


Echarts配色配置概述


Echarts提供了两种方式来配置配色方案:使用预定义的配色方案和自定义配色方案。预定义的配色方案包括一系列经过精心设计的颜色配置,而自定义配色方案则允许用户根据自己的需求,自由地调整配色方案。


以下将对Echarts的高级配色进行详细介绍,并提供相应的代码示例。


使用预定义的配色方案


Echarts提供了一些预定义的配色方案,以供用户选择使用。这些预定义的配色方案是经过深思熟虑和优化的,能够使图表在不同场景下保持一致和美观。


以下是一些常见的预定义配色方案及其名称:



  • colorBlind:适用于色盲人群的配色方案,通过优化颜色对比度,使得色盲人群更容易分辨。

  • light:明亮配色方案,适用于明亮的背景或需要突出显示的图表。

  • dark:低亮度配色方案,适用于暗色背景或需要弱化图表的亮度。


使用预定义的配色方案非常简单,只需在图表的配置项中设置配色方案的名称即可。


option = {
// 其他配置项...
color: 'light', // 使用预定义的明亮配色方案
};

在上面的示例代码中,通过设置配色方案的名称为light,来应用明亮的预定义配色方案。


自定义配色方案


Echarts也支持用户根据自己的需求,定制个性化的配色方案。自定义配色方案使用户可以根据自己的品牌风格、场景需求等,灵活地设置图表的颜色。


以下是一个自定义配色方案的示例:


option = {
// 其他配置项...
color: ['#FF0000', '#00FF00', '#0000FF'], // 使用自定义配色方案
};

在上述示例中,通过设置color字段为一个颜色数组,来使用自定义的配色方案。在这个例子中,我们使用红色、绿色和蓝色来自定义配色方案。


配色方案详解


Echarts提供了丰富的配置选项,用户可以通过调整配置项来实现个性化的配色方案。以下是一些常用的配色方案的配置项:



  • color:图表的系列颜色配置,可以设置为预定义的配色方案名称或自定义的颜色数组。

  • backgroundColor:图表背景色配置,可以设置为颜色值或渐变色。

  • textStyle:图表中文字的样式配置,包括字体、字号和颜色等。

  • axisLineaxisLabelaxisTick:坐标轴线、刻度线、刻度标签的样式配置。


通过修改这些配置项的值,我们可以轻松地调整图表的配色方案。


完整示例


下面是一个使用Echarts配色功能的示例代码,包括预定义配色方案和自定义配色方案的应用。


<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Echarts高级配色示例</title>
<script src="https://cdn.jsdelivr.net/npm/echarts/dist/echarts.min.js"></script>
</head>
<body>
<div id="chart" style="width: 600px; height: 400px;"></div>
<script>
// 初始化Echarts实例
var myChart = echarts.init(document.getElementById('chart'));

// 配置项
var option = {
title: {
text: 'Echarts高级配色示例'
},
// 其他配置项...
textStyle: {
fontFamily: 'Arial, sans-serif',
fontSize: 12,
fontWeight: 'normal'
},
tooltip: {
// 配置提示框样式
},
xAxis: {
// 配置X轴样式
},
yAxis: {
// 配置Y轴样式
},
series: [{
type: 'bar',
// 配置系列样式
}],
// 使用预定义配色方案
color: 'colorBlind',
};

// 使用配置项显示图表
myChart.setOption(option);
</script>
</body>
</html>

在上述示例中,我们首先引入了Echarts库,并创建一个容器元素来显示图表。然后,我们初始化Echarts实例,并设置图表的配置项,包括标题、文字样式、提示框样式、坐标轴样式和系列样式等。最后,调用setOption方法将配置项应用于图表。


通过配色方案的选择和自定义,我们可以灵活定制图表的配色方案,使图表更加美观和易于辨识。


总结


Echarts的高级配色功能使用户可以根据自己的需求,定制图表的颜色配色方案。预定义配色方案提供了一系列经过优化的配色方案,能够满足常见的图表需求,而自定义配色方案则允许用户根据自己的品牌风格和场景需求,灵活地设置图表的颜色。


通过使用配色功能,我们可以轻松定制个性化的图表样式,使数据可视化更加美观和易于理解。在实际应用中,根据需要选择合适的预定义配色方案,或者自定义配色方案,都能为数据可视化带来不同的风格和效果。


通过本文的全面介绍和示例代码的演示,相信您已经掌握了Echarts的高级配色功能,并可以灵活应用于实际的数据可视化项目中。继续探索和研究Echarts的配色功能,将为您的数据可视化项目增添更多的创意和魅力!


作者:程序员也要学好英语
来源:juejin.cn/post/7313027887885123599
收起阅读 »

让你的PDF合成后不再失真

web
前言 现在的前端要处理越来越多的业务需求,如果你的业务中有涉及PDF的处理,你肯定会遇到这么一种情况, 在一个原始的pdf文件上合成进一张图片,或者一段文字。 之前的解决方案基本上是把图片扔给后端,让后端处理,处理好之后,前端再调用接口拿到。 如果非要前端来做...
继续阅读 »

前言


现在的前端要处理越来越多的业务需求,如果你的业务中有涉及PDF的处理,你肯定会遇到这么一种情况,
在一个原始的pdf文件上合成进一张图片,或者一段文字。


之前的解决方案基本上是把图片扔给后端,让后端处理,处理好之后,前端再调用接口拿到。


如果非要前端来做,也不是不可以。


一脸无奈的小


canvas


网上搜了一圈,主流的方案是,用canvas画布将pdf画出来,再将图片合成进canvans
这里也提供一下这个方案的代码


const renderPDF = async (pdfData, index, pages, newPdf, images = []) => {
await pdfData.getPage(index).then(async (pdfPage) => {
const viewport = pdfPage.getViewport({ scale: 3, rotation: 0 })
const canvas = document.createElement("canvas")
const context = canvas.getContext("2d")
canvas.width = 600 * 3
canvas.height = 800 * 3
// PDF渲染到canvas
const renderTask = pdfPage.render({
canvasContext: context,
viewport: viewport,
})

await renderTask.promise.then(() => {
if (index > 1) {
newPdf.addPage()
}
newPdf.addImage(canvas, "JPEG", 0, 0, 600, 800, undefined, "FAST")
images.forEach((item) => {
let width = item.width
let height = item.height
if (index == pages) {
item.src !== "" &&
newPdf.addImage(
item.src,
"PNG",
item.x,
item.y,
width,
height,
undefined,
"FAST"
)
}
})
})
})
}

但是!


这样会有一个很严重的问题,那就是pdf失真,显得很模糊,当然也有解决方案,那就是canvas的缩放比例增加,


image.png
但是,缩放比例的增加却带来了pdf文件大小的倍数及增加,前端渲染的压力很大,只有7张的pdf,已经渲染出了8M大小常常见到loading等待。


所以有没有更好的方法解决呢?


暴漫g


有的。


pdf-lib


那就是今天所推荐的库 pdf-lib
github地址
他在github上的star 有5.6k,算的上是成熟,顶级的开源项目


image.png


在任何JavaScript环境中创建和修改PDF文档。


好,今天就只介绍如何将图片合成进pdf的功能 ,抛砖引玉。


熊猫头抛砖头 .gif


其余的功能由您自己探索。


合成的思路是这样的:



  • 1、我们的原始pdf,转换成pdf-lib 可识别的格式

  • 2、同时将我们的图片合成进 pdf-lib里

  • 3、pdf-lib 导出合成后的pdf



由于他只是一个工具,没有办法展示pdf
最后找一个pdf预览工具显示在页面即可
我找的是 vue-pdf-embed



这样,使用pdf-lib 方案,就不再是canvas画布画出来的。
我们可以看到,生成后的pdf文件体积增加不大,


image.png


而且能够保留原始pdf的文字选择,不再是图片了


image.png


同样,页面的缩放不会出现模糊失真的情况(因为不是图片,还是保持文字的矢量)。


代码


以下是代码,请查收


感谢 给你磕头 GIF .gif


import { PDFDocument } from "pdf-lib"

const getNewPdf = async (pdfBase64, imagesList = []) => {
// 创建新的pdf
const pdfDoc = await PDFDocument.create()

let page = ""
// 传入的pdf进行格式转换
const usConstitutionPdf = await PDFDocument.load(pdfBase64)
// 获取转换后的每一页数据
const userPdf = usConstitutionPdf.getPages()
// 将每一个数据 导入到我们新建的pdf每一页上
for (let index = 0; index < userPdf.length; index++) {
page = pdfDoc.addPage()
const element = userPdf[index]
const firstPageOfConstitution = await pdfDoc.embedPage(element)
page.drawPage(firstPageOfConstitution)
// 如果有传入图片,则遍历信息,并将他合成到对应的页码上
const imageSel = imagesList.filter((i) => i.pageIndex === index)
if (imageSel.length > 0) {
for (let idx = 0; idx < imageSel.length; idx++) {
const el = imageSel[idx]
const pngImage = await pdfDoc.embedPng(el.src)
page.drawImage(pngImage, {
x: +el.x,
y: +el.y,
width: +el.width,
height: +el.height,
})
}
}
}
// 保存pdf
const pdfBytes = await pdfDoc.save()
// 将arrayButter 转换成 base64 格式
function ArrayBufferToBase64(buffer) {
//第一步,将ArrayBuffer转为二进制字符串
var binary = ""
var bytes = new Uint8Array(buffer)
for (var len = bytes.byteLength, i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i])
}
//将二进制字符串转为base64字符串
return window.btoa(binary)
}

// console.log("data:application/pdf;base64," + ArrayBufferToBase64(pdfBytes))
// 最后将合成的pdf返回
return "data:application/pdf;base64," + ArrayBufferToBase64(pdfBytes)
}
export default getNewPdf


这里的传参要注意,


可爱小男生拿喇叭注意啦_爱给网_aigei_com.gif



  • pdfBase64 是base64位的格式

  • imagesList 数组对象格式为
    [
    {
    src:'base64',
    x:'',
    yL'',
    width:'',
    height:'',
    pageIndex:''
    }
    ]



最后也附上vue文件中如何使用的代码


<template>
<div>
<el-button @click="pdfComposite">生成新的pdf</el-button>
<div class="pdf-content">
<vue-pdf-embed
:source="url"
/>
</div>
</div>

</template>

<script>
import VuePdfEmbed from "vue-pdf-embed/dist/vue2-pdf-embed"
import getNewPdf from "./utils"
import { pngText, pdfbase64 } from "../data"
export default {
name: "PdfPreview",
components: {
VuePdfEmbed,
},

data() {
return {
url: pdfbase64,// 原始的base64位 pdf
}
},
methods: {
pdfComposite() {
// getNewPdf 返回的是promise 对象
getNewPdf(this.url, pngText).then(res =>{
this.url = res
})
},
},
}
</script>

<style >
.pdf-content {
width: 400px;
min-height: 600px;
}
</style>


作者:前端代码王
来源:juejin.cn/post/7293175592163049506
收起阅读 »

阿里妈妈刀隶体使用

web
最近在做的一个前端小项目,需要一种比较炫酷的字体,在网上找到了iconfont中的刀隶体,感觉还不错,本文记录了如何在前端项目中使用这种字体的步骤。 1. 找到刀隶体生成的网站 访问下面这个网站就可以了: 阿里妈妈刀隶体字体 http://www.iconfo...
继续阅读 »

最近在做的一个前端小项目,需要一种比较炫酷的字体,在网上找到了iconfont中的刀隶体,感觉还不错,本文记录了如何在前端项目中使用这种字体的步骤。


1. 找到刀隶体生成的网站


访问下面这个网站就可以了:


阿里妈妈刀隶体字体


http://www.iconfont.cn/fonts/detai…


2. 生成自己想要的字并下载到本地


找到文本输入框
image.png


然后输入自己想要展示的字体:我是一只小青蛙,最爱说笑话
image.png


最后点击下载子集按钮


image.png


下载好的压缩包:


image.png


将压缩包中的内容复制到剪切板:


image.png


3. 项目中引入


在项目中创建管理字体的目录


mkdir -p src/assets/font

然后到font目录下粘贴复制的字体文件夹


最后在项目的根样式文件中(一般来说是src/index.css)引入新字体:


@font-face {
font-family: "DaoLiTi";
font-weight: 400;
src: url("./assets/font/jHsMvOZ7UDEO/XBddIp4y0BmM.woff2") format("woff2"),
url("./assets/font/jHsMvOZ7UDEO/XBddIp4y0BmM.woff") format("woff");
font-display: swap;
}

body {
font-family: 'DaoLiTi', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
}

4. 新字体使用及效果


<span style={{fontFamily: 'DaoLiTi'}}>我是一只小青蛙,最爱说笑话</span>


5. 注意点


DaoLiTi这个名字是可以自定义的,但是在样式文件中的无论什么地方使用的时候都不能少了引号。


此外就是除了“我是一只小青蛙,最爱说笑话”,其他字是没有刀隶体效果的!


作者:慕仲卿
来源:juejin.cn/post/7305359585107738661
收起阅读 »

你真的懂 Base64 吗?短链服务常用的 Base62 呢?

web
黄山的冬天,中国 (© Hung Chung Chih/Shutterstock) Base64 前端的日常开发中可能会接触到 Base64 ,比如页面上的小图片,在为了节省网络资源的情况下,通常会将图片转为 Base64 直接嵌入到 html 或者 css ...
继续阅读 »

黄山的冬天,中国 (© Hung Chung Chih/Shutterstock)

Base64


前端的日常开发中可能会接触到 Base64 ,比如页面上的小图片,在为了节省网络资源的情况下,通常会将图片转为 Base64 直接嵌入到 html 或者 css 里。这里,我们先来看看 Base64 是什么,以及 Base64 编码做了什么。


什么是 Base64


Base64 是一种基于64个可打印字符来表示二进制数据的表示方法。一般来说,64个字符包括 A-Z,a-z,0-9 以及 +/ 两个字符(via)。换句话说, Base64 可以将二进制数据转换为这 64 个字符来表示,数据来源可以是图片,也可以是任意的字符串。


Base64 做了些什么


上面提到了 Base64 做的其实就是将二进制数据按照对应规则进行转化,转化的流程其实也很简单



  1. 得到一份二进制数据

  2. 将二进制数据 6位 一组进行划分,并进行适当的补位

  3. 按照 Base64 索引表,将每组数据转换为 Base64 索引表对应的字符,补位位置用 =


下面我们来尝试一下将 aa 这个字符串进行一下 Base64 编码:


第一步:通过 ASCII 表将 aa 转换为对应的二进制表示

可知 a 在 ASCII 表对应的二进制表示为 0110 0001,则 aa 对应为 0110 0001 0110 0001

注:



  1. ascii 参考

  2. 中文等其他字符参考其他表(UTF-8)进行转换即可


第二步:数据分组和补位

按 6 位一组划分后 011000 010110 0001,我们发现数据还少两位,所以我们需要按规则对数据进行补位,补一字节(8位),二进制数据变为 0110 0001 0110 0001 0000 0000,划分为 011000 010110 000100 000000


第三步:查 Base64 索引表 进行转换

参考一张常用的索引表
image.png
可知 aa = 011000 010110 000100 000000 对应为 011000(Y) 010110(W) 000100(E) 000000(补位=) 即 YWE=,我们就得到了 aa 的 Base64 编码为 YWE= ,是不是挺简单。


Base62


说完了 Base64,我们再来聊聊 Base62。在通过 url 传递数据的场景下,通过 Base64 进行编码的数据会带来问题(Base64 中的 / 等可能会带来路径的解析异常),所以在 Base62 里,去掉了 +/= 字符。
说到这里,大家可能觉得就讲完了,Base62 就是丢掉了几个不安全的字符而已,其余转换方法和 Base64 一样,我起初也是这么认为的。


不一样的 Base62 结果


当我尝试对 aa 进行 Base62 编码时,按推算好像也不太对? = 补位已经被去掉了,怎么来做实现呢?
在我找了几个 online 转换进行测试后,发现 aa 对应的 Base62 编码为 6U5 看着跟 Base64 毫无关系对吧,实际上也是的。


揭开面纱看看


查了几份资料以及现有的仓库实现后,我发现 Base62 编码的流程是这样的:



  1. 获得一份二进制数据

  2. 二进制数据 转 10进制

  3. 10进制 转 62进制(按索引表)


我们再来试试将 aa 转 Base62:


第一步:转二进制

aa 对应 0110 0001 0110 0001


第二步:转10进制

0110000101100001 对应十进制为 24929


第三步:转62进制(参考索引表)

image.png
24929 = 6622+3062+5

按表可知,6=6 30=U 5=5,即 6U5


注:



  1. 索引表可以自行更换,并不一定是上图顺序

  2. 现有的仓库实现里,部分只实现了 10进制 转 62进制(base62/base62.js),有的实现了更完整的转换 (tuupola/base62


分析一下原因


说实话我查到的资料不多,但是根据 en.wikipedia.org/wiki/Talk:B… 猜测,文里提到 Base64 后的数据会膨胀到 133% 。Base62 还存在对数据进行压缩的改进,所以采用了这样与 Base64 差别有点大的方式来设计。


总结一下


文章简单的谈了谈 Base64 是什么,怎么实现以及 Base62 的实现,并分析了一下 Base62 设计的初衷,整体来说还是挺简单,希望对你有所帮助 :)


作者:破竹
来源:juejin.cn/post/7311596852264878115
收起阅读 »

关于解构赋值的一些意想不到的坑

web
今天群里有个人问了一个问题,问我们为什么报错,代码如下 var arr = [1,2,3,4,5,6,7,8,9,10] for(let i = arr.length; i > 0; i--) { let index = Math.floor(M...
继续阅读 »

今天群里有个人问了一个问题,问我们为什么报错,代码如下


var arr = [1,2,3,4,5,6,7,8,9,10]
for(let i = arr.length; i > 0; i--) {
let index = Math.floor(Math.random() * i)
// console.log(i, index);
[arr[i-1], arr[index]] = [arr[index], arr[i-1]]
// ReferenceError: Cannot access 'index' before initialization
}
console.log(arr)

我乍一看,这能报错?不应该啊,怎么能呢,于是我特意复制下来跑了一下,嘿,还真是


关于ReferenceError: Cannot access 'xxx' before initialization的报错,往往和暂时性死区有关,但我看了看顺序,是先定义的index啊,没有错。


抱着求知的心态,上网查了一些文章,都没有提到这种问题,于是只能去看ecma规范,但对不起,我英语太差了,我就连在哪都没找到。


后来我想起自己曾经遇到过类似的问题,只不过是在解构对象的时候遇到的


大概是这样的操作


let a = xxx

({
a: this.options.a,
b: this.options.b,
......
} = /*一个对象*/ ?? {})

当时也报了错,我就想起来了



js中是允许语句不使用;结尾的,许多小伙伴可能养成了这个习惯,虽然不写分号有时候确实很爽很轻松,也是一些企业的规范,但是等到流泪的时候可就知道惨了。



只需要将上述结构赋值的代码的前面一个语句加上分号,就可以解决这个问题


相当于把一个语句拆开了


什么?你问我怎么就成同一个语句了?


我没记错的话,js在执行的时候是会忽略换行符的吧,或者说这个换行符没那么重要,所以我们平时看到的很多库打包出来的min.js文件都是只有一行的然后通过分号分割语句。


如果把上述代码换行内容忽视掉,就变成了这个样子,只放了部分代码


    let index = Math.floor(Math.random() * i)[arr[i-1], arr[index]] = [arr[index], arr[i-1]]

这不报错谁报错啊,根据等号从右到左的运算顺序,不就是访问了暂时性死区嘛


所以加上分号,问题就引刃而解了。


想当年因为先学c++和java的缘故,总是养成写分号的习惯,在切图仔里面似乎成为了一个异类,现在知道了吧,养成写分号的好习惯啊,呜呜呜呜


最后附上修改后的代码


var arr = [1,2,3,4,5,6,7,8,9,10]
for(let i = arr.length; i > 0; i--) {
let index = Math.floor(Math.random() * i);
// console.log(i, index);
[arr[i-1], arr[index]] = [arr[index], arr[i-1]]
// ReferenceError: Cannot access 'index' before initialization
}
console.log(arr)

更好笑的是,这哥们明显是想通过console测试一下的,结果他发现console就不报错,不console就报错,这是真的折磨哈哈哈哈哈哈


养成语句加分号的好习惯!!
从你我做起!


作者:笑心
来源:juejin.cn/post/7311681326712995903
收起阅读 »

如何实现一个可视化数据转换的小工具

web
前端开发过程中,经常有两个不同的组件、或者两个不同的业务模块需要对接,但是双方提供的数据结构又不一致的场景。这个时候就需要一个转换器来实现两个模块无缝对接,通过这个转换器的处理达到数据结构一致的目的。 基本需求梳理 场景中两个相同或者不同数据结构对象,对象是...
继续阅读 »

前端开发过程中,经常有两个不同的组件、或者两个不同的业务模块需要对接,但是双方提供的数据结构又不一致的场景。这个时候就需要一个转换器来实现两个模块无缝对接,通过这个转换器的处理达到数据结构一致的目的。


基本需求梳理



  • 场景中两个相同或者不同数据结构对象,对象是任意数据结构的,对接场景下数据结构是固定的

  • 针对当前场景下转换器处理是相同的

  • 尽量图形化操作就可以换成配置以及预览效果


设计基本思路


实现思路



  • 两个不同的数据结构可以通过字段映射的方式来取值和设置值,实现数据的对接

  • 取值路径和设置值的路径规则最好一条一条保存下来,作为转换器的规则描述

  • 取值路径和设置值路径可以通过lodash的get和set实现

  • 可视化操作可以通过json树的渲染及操作来实现


设计实现思路草图


我根据自己的想法大致设计一下交互方式如下:


数据转换器 (3).png


实现步骤


json树操作


我找了一下josn树操作的组件,发现react-json-view挺不错的;可以实现值的复制、选择;但是选择是针对叶子节点的,所以这里我使用复制功能来实现,无论是叶子节点还是非叶子节点都可以复制到,(enableClipboard)复制的时候获取当前的path信息即可。另外path多了一层根路径默认是'root',如果不想要操作保存的时候去掉即可。


如下图所示,鼠标悬浮点击这个icon图标来选中key,取其路径值保存起来
image.png


// 复制的操作
const enableClipboard = (copy) => {
const { namespace } = copy;
if (namespace?.length === 1) { // 复制的根元素
setSourceKeyPath([])
} else {
const curNamespace = namespace.splice(1, namespace.length - 1);
setSourceKeyPath(curNamespace)
}
}

路径保存


路径映射保存在数组里。


转换器处理


转换器只需要根据规则数组map处理一下每条规则进行对原数据和目标数据进行取值设置值操作就可以了


// dataSource 是规则数组
// targetData 是目标数据
// sourceData 是源数据
dataSource.map((item) => {
set(targetData, item.targetKeyPath, get(sourceData, item.sourceKeyPath))
return item
});

效果预览


image.png
另外数据随机生成我使用的是@faker-js/faker


部署到网站


已经部署到我的个人网站timesky.top/data-conver…


作者:TimeSky
来源:juejin.cn/post/7313242069099954226
收起阅读 »

Antd Upload上传后还想要拖拽?行~开干!玩的就是真实

web
原创 陈夏杨 / 叫叫技术团队 基于 Antd Upload 实现拖拽(兼容低版本) 背景 哎呀!我想要的是边上传边调整位置可以拖拽的那种效果!哪种?就那种。(这句话是不是似曾相识,没错,到这里作为开发还没领悟那就要面壁思过了~哈哈。)话不多说,目前 Ant...
继续阅读 »

原创 陈夏杨 / 叫叫技术团队



基于 Antd Upload 实现拖拽(兼容低版本)


背景


哎呀!我想要的是边上传边调整位置可以拖拽的那种效果!哪种?就那种。(这句话是不是似曾相识,没错,到这里作为开发还没领悟那就要面壁思过了~哈哈。)话不多说,目前 Antd 的 Upload 组件并未支持拖拽排序功能,社区也没有发现可以借鉴的 demo,于是我们调研后采用 react-dnd 和 react-sortable-hoc 实现“就那种”效果。


技术分析


其实这个需求之前我们已经有一些基于 react-dnd 技术的沉淀,但是都是基于 html 的 dom 元素进行 ref 绑定操作,并没有搭配 Upload 组件。如下 demo


拖拽2.gif
以上是基于 react-dnd 实现的场景拖拽,直接上核心代码


const [, dragPreview] = useDrag({
collect: (monitor: DragSourceMonitor) => ({
isDragging: monitor.isDragging()
}),
// item 中包含 index 属性,则在 drop 组件 hover 和 drop 是可以根据第一个参数获取到 index 值
item: { type: 'page', index }
});
const [, drop] = useDrop({
accept: 'page',
hover(item: { type: string; index: number }, monitor: any) {
if (!ref.current) {
return;
}
const dragIndex = item.index;
const hoverIndex = index;

// 拖拽元素下标与鼠标悬浮元素下标一致时,不进行操作
if (dragIndex === hoverIndex) {
return;
}
// 确定屏幕上矩形范围
const hoverBoundingRect = ref.current!.getBoundingClientRect();
// 获取中点垂直坐标
const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
// 确定鼠标位置
const clientOffset = monitor.getClientOffset();
// 获取距顶部距离
const hoverClientY = (clientOffset as any).y - hoverBoundingRect.top;
/**
* 只在鼠标越过一半物品高度时执行移动。
* 当向下拖动时,仅当光标低于50%时才移动。
* 当向上拖动时,仅当光标在50%以上时才移动。
* 可以防止鼠标位于元素一半高度时元素抖动的状况
*/

// 向下拖动
if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
return;
}
// 向上拖动
if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
return;
}

// 执行 move 回调函数
moveCard(dragIndex, hoverIndex);

/**
* 如果拖拽的组件为 Box,则 dragIndex 为 undefined,此时不对 item 的 index 进行修改
* 如果拖拽的组件为 Card,则将 hoverIndex 赋值给 item 的 index 属性
*/

if (item.index !== undefined) {
item.index = hoverIndex;
}
}
});
dragPreview(drop(ref));

因为 Upload 组件上传文件是通过自身 fileList api 底层消化处理的,所以处理起来比较麻烦,还好 Antd 4.16.0 版本Upload 提供的 itemRender 解决了这个问题。但是对于低于这个版本的后续也有解决方案。


注意:如果 Antd 用的是最新的版本 5.x.x,其实官网也提供了 集成 dnd-kit 来实现对上传列表拖拽排序


技术选型


市面上可以实现拖拽排序的库有很多,比如 SortableJS、react-dnd、react-beautiful-dnd、react-sortable-hoc 等。
我列了一个表格:


优点缺点
SortableJS足够轻量级,而且功能齐全React 中使用起来并不是太方便,而且它的配置项写起来实在不太符合 React 的思维
react-dnd库小,贴合 react 拖拽场景多行拖拽不理想,react-beautiful-dnd 库比较大赖于HTML5 拖放 API,这有一些严重的限制
react-beautiful-dnd动画效果和细节非常完美同上
react-sortable-hoc多行拖拽优势很明显(相比其他库大多依赖于HTML5拖放API ,这有一些严重的限制。例如,如果你需要支持触摸设备,如果你需要锁定拖动到一个轴上,或者想在节点排序时设置动画,事情就会变得很棘手。React-sortablehoc 旨在提供一组简单的 higher-order 组件来填补这些空白。如果您正在寻找一种 dead-simple ,mobile-friendly 的方式来向列表中添加可排序功能,那么您就在正确的位置了。)列表过长,需要滑动的列表中拖拽时,滑动后位置不匹配会发生偏移

实践中还会有一些踩坑:



  • reat-dnd 在项目中快速拖拽时一直报错,"Invariant Violation: Expected targetIds to be registered. "在他的 issue 中也有很多人反应这个问题,虽然有修复过但是并没有完全修复,在 overStack 中也并没有找到好的解决方案。

  • 这里以我们实践结论,从 react-dnd、react-sortable-hoc 两个库进行讲解


react-dnd


概念

react dnd 是一组 react 高阶组件,使用的时候只需要使用对应的 API 将目标组件进行包裹,即可实现拖动或接受拖动元素的功能。
在拖动的过程中,不需要开发者自己判断拖动状态,只需要在传入的配置对象中各个状态属性中做对应处理即可,因为react-dnd 使用了 redux 管理自身内部的状态。
值得注意的是,react-dnd 并不会改变页面的视图,它只会改变页面元素的数据流向,因此它所提供的拖拽效果并不是很炫酷的,我们可能需要写额外的视图层来完成想要的效果,但是这种拖拽管理方式非常的通用,可以在任何场景下使用,非常适合用来定制。


安装

npm i react-dnd

核心 API

介绍实现拖拽和数据流转的核心 API ,这里以 hook 为例。


DndProvider

使用 react-dnd 需要最外层元素加 DndProvider ,DndProvider 的本质是一个由 React.createContext 创建一个上下文的容器(组件),用于控制拖拽的行为,数据的共享,类似于 react-redux 的 Provider。


import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';

<DndProvider backend={HTML5Backend}>组建模块</DndProvider>;

Backend

react dnd 将 DOM 事件相关的代码独立出来,将拖拽事件转换为 react dnd 内部的 redux action。由于拖拽发生在 H5 的时候是 ondrag,发生在移动设备的时候是由 touch 模拟,react dnd 将这部分单独抽出来,方便后续的扩展,这部分就叫做 backend。它是 dnd 在 DOM 层的实现。



  • react-dnd-html5-backend : 用于控制 html5 事件的 backend

  • react-dnd-touch-backend : 用于控制移动端 touch 事件的 backend

  • react-dnd-test-backend : 用户可以参考自定义 backend


useDrag

让 DOM 实现拖拽能力的构子


import React from 'react';
import { useDrag } from 'react-dnd';

export default function Player() {
// 第一个返回值是一个对象,主要放一些拖拽物的状态。后面会介绍,先不管
// 第二个返回值:顾名思义就是一个Ref,只要将它注入到DOM中,该DOM就会变成一个可拖拽的DOM
const [_, dragRef] = useDrag(
{
type: 'Player', // 给拖拽物命名,后面用于分辨该拖拽物是谁,支持string和symbol
item: { id: 1 } // 拖拽物所携带的数据,让后面一些事件可以拿到数据,已达到交互的目的
},
[]
);
// 注入Ref,现在这个DOM就可以拖拽了
return <div ref={dragRef} />;
}

返回三个参数

第一个返回值是一个对象 表示关联在拖拽过程中的变量,需要在传入 useDrag 的规范方法的 collect 属性中进行映射绑定,比如:isDraging, canDrag 等

第二个返回值 代表拖拽元素的 ref

第三个返回值 代表拖拽元素拖拽后实际操作到的 dom

传入两个参数



  • 第一个参数,是一个对象,是用于描述了drag 的配置信息,常用属性


type指定元素的类型,只有类型相同的元素才能进行 drop 操作
item元素在拖拽过程中,描述该对象的数据,如果指定的是一个方法,则方法会在开始拖拽时调用,并且需要返回一个对象来描述该元素。
end(item, monitor)拖拽结束的回调函数,item 表示拖拽物的描述数据,monitor 表示一个 DragTargetMonitor 实例
isDragging(monitor)判断元素是否在拖拽过程中,可以覆盖Monitor对象中的 isDragging方法,monitor 表示一个 DragTargetMonitor 实例
canDrag(monitor)判断是否可以拖拽的方法,需要返回一个 bool 值,可以覆盖 Monitor 对象中的 canDrag 方法,与 isDragging 同理,monitor 表示一个 DragTargetMonitor 实例
collect它应该返回一个描述状态的普通对象,然后返回以注入到组件中。它接收两个参数,一个 DragTargetMonitor 实例和拖拽元素描述信息item


  • 第二个参数是一个数组,表示对方法更新的约束,只有当数组中的参数发生改变,才会重新生成方法,基于react 的 useMemo 实现


useDrop

实现拖拽物放置的钩子


import { useDrop } from 'react-dnd';

export const Dustbin = () => {
const [_, dropRef] = useDrop({
accept: ['Player'], // 指明该区域允许接收的拖放物。可以是单个,也可以是数组
// 里面的值就是useDrag所定义的type

// 当拖拽物在这个拖放区域放下时触发,这个item就是拖拽物的item(拖拽物携带的数据)
drop: (item) => {}
});

// 将ref注入进去,这个DOM就可以处理拖拽物了
return <div ref={dropRef}></div>;
};

返回两个参数

第一个返回值是一个对象 表示关联在拖拽过程中的变量,需要在传入 useDrag 的规范方法的 collect 属性中进行映射绑定。

第二个返回值 代表拖拽元素的 ref
传入一个参数
用于描述drop的配置信息,常用属性


accept指定接收元素的类型,只有类型相同的元素才能进行 drop 操作
drop(item, monitor)有拖拽物放置到元素上触发的回调方法,item 表示拖拽物的描述数据,monitor 表示 DropTargetMonitor 实例,该方法返回一个对象,对象的数据可以由拖拽物的 monitor.getDropResult 方法获得
hover(item, monitor)当拖住物在上方 hover 时触发,item 表示拖拽物的描述数据,monitor表示 DropTargetMonitor 实例,返回一个 bool 值
canDrop(item, monitor)判断拖拽物是否可以放置,item 表示拖拽物的描述数据,monitor 表示 DropTargetMonitor 实例,返回一个 bool 值

API 数据流转

image.png


react-sortable-hoc


概念

react-sortable-hoc 是一个基于 React 的拖拽排序组件,它可以让你轻松地实现拖拽排序功能。它提供了一系列的 API,可以让你自定义拖拽排序的行为。它支持拖拽排序的单个列表和多个列表,以及拖拽排序的可视化。


安装

npm install react-sortable-hoc

引入

import { SortableContainer, SortableElement, arrayMove, SortableHandle } from 'react-sortable-hoc';

核心 API


  • sortableContainer 是所有可排序元素的容器

  • sortableElement 是每个可渲染元素的容器

  • sortableHandle 是定义拖拽手柄的容器

  • arrayMove 主要用于将移动后的数据排列好后返回


SortableContainer HOC

PropertyTypeDefaultDescription
axisStringy项目可以水平、垂直或网格排序。可能值:x、y 或 xy
lockAxisString如果您愿意,可以在排序时将移动锁定在轴上。这不是 HTML5 拖放所能做到的。可能值:x 或 y。
helperClassString您可以提供一个要添加到 sortable helper 的类,以向其添加一些样式
transitionDurationNumber300元素移动位置时转换的持续时间。{ 39d 要禁用 @661 }
keyboardSortingTransitionDurationNumbertransitionDuration在键盘排序期间移动辅助对象时转换的持续时间。如果要禁用键盘排序助手的转换,请将其设置为 0。如果未定义,则默认为 transitionDuration 设置的值
keyCodesArray{lift: [32],drop: [32],cancel: [27],up: [38, 37],down: [40, 39]}一个包含每个 keyboard-accessible 操作的键码数组的对象。
pressDelayNumber0如果您希望元素只在按下一段时间后才可排序,请更改此属性。mobile 的一个合理的默认值是 200。不能与 distance 属性一起使用。
pressThresholdNumber5忽略冲压事件之前要容忍的移动像素数。
distanceNumber0如果您希望元素只在被拖动一定数量的像素之后才变得可排序。不能与 pressDelay 属性一起使用。
shouldCancelStartFunctionFunction此函数在排序开始前调用,可用于在排序开始前以编程方式取消排序。默认情况下,如果事件目标是 input、textarea、select 或 option,它将取消排序。
updateBeforeSortStartFunction在排序开始之前调用此函数。它可以返回一个 promise,允许您在排序开始之前运行异步更新(比如 setState )。function ({ node, index, collection, isKeySorting }, event )
onSortStartFunction开始排序时调用的回调。function({ node, index, collection, isKeySorting }, event ) |
onSortMoveFunction当光标移动时在排序期间调用的回调。function ( event )|
onSortOverFunction在向上移动时调用的回调。function ({ index, oldIndex, newIndex, collection, isKeySorting }, e )
onSortEndFunction排序结束时调用的回调。function ({ oldIndex, newIndex, collection, isKeySorting }, e )
useDragHandleBooleanfalse如果您使用的是SortableHandleHOC,请将其设置为true
useWindowAsScrollContainerBooleanfalse如果需要,可以将window设置为滚动容器
hideSortableGhostBooleantrue是否 auto-hide 重影元素。默认情况下,为了方便起见,React Sortable List 将自动隐藏当前正在排序的元素。如果要应用自己的样式,请将此设置为 false。
lockToContainerEdgesBooleanfalse您可以将可排序元素的移动锁定到其父元素 SortableContainer
lockOffsetOffsetValue*|[OffsetValue*,OffsetValue*]"50%"当 lockToContainerEdges 设置为 true 时,这将控制可排序辅助对象与其父对象 SortableContainer 的上/下边缘之间的偏移距离。百分比值相对于当前正在排序的项的高度。如果您希望指定不同的行为来锁定容器的顶部和底部,您还可以传入 array(例如:["0%", "100%"])。
getContainerFunction返回可滚动容器元素的可选函数。此属性默认为 SortableContainer 元素本身或(如果 useWindowAsScrollContainer 为真)窗口。使用此函数指定一个自定义容器对象(例如,这对于与某些第三方组件(如 FlexTable )集成非常有用)。这个函数被传递给一个参数(即 wrappedInstanceReact 元素),它应该返回一个 DOM 元素。
getHelperDimensionsFunctionFunction可选的function ({ node, index, collection }),它应该返回 SortableHelper 的计算维度。有关详细信息,请参见默认实现 |
helperContainerHTMLElement | 函数document.body默认情况下,克隆的可排序帮助程序将附加到文档正文。使用此属性可指定要附加到可排序克隆的其他容器。接受 HTMLElement 或返回 HTMLElement 的函数,该函数将在排序开始之前调用
disableAutoscrollBooleanfalse拖动时禁用自动滚动

如何使用

直接上demo


import React from 'react';
import { arrayMove, SortableContainer, SortableElement } from 'react-sortable-hoc';

// 需要拖动的元素的容器
const SortableItem = SortableElement((value) => <div>{value}</div>);
// 整个元素排序的容器
const SortableList = SortableContainer((items) => {
return items.map((value, index) => {
return <SortableItem key={`item-${index}`} index={index} value={value} />;
});
});

// 拖动排序组件
class SortableComponnet extends React.Component {
state = {
items: ['1', '2', '3']
};
onSortEnd = ({ oldIndex, newIndex }) => {
this.setState(({ items }) => {
arrayMove(items, oldIndex, newIndex);
});
};
render() {
return (
<div>
<SortableList
distance={5}
axis={'xy'}
items={this.state.items}
helperClass={style.helperClass}
onSortEnd={this.onSortEnd}
/>

</div>

);
}
}

export default SortableComponnet;

在上面的示例中,我们使用 SortableContainer 组件容纳了一组可拖拽排序的元素,使用 SortableElement 组件包裹了每个元素,并且实现了 onSortEnd 回调函数,以便在拖拽排序完成后更新状态。


效果展示

拖拽3.gif


踩坑

image.png



解决:这种报错的解决方法都是 SortableElement 和 SortableContainer 返回组件时外面都要单独在包一个 html 容器标签, 例子是包了个 <div>



结果导向


Antd 版本 4.16.0及以上


使用 react-dnd 搭配 Upload 上传组件 itemRender api实现

注:这里主要基于 react-dnd 实现,Antd 5.x.x 官网有基于 dnd-kit 来实现对上传列表拖拽排序。


const Box: React.FC<BoxProps> = ({ children, index, className, onClick, moveCard }) => {
const ref = useRef<HTMLDivElement>(null);

const [, dragPreview] = useDrag({
collect: (monitor: DragSourceMonitor) => ({
isDragging: monitor.isDragging()
}),
// item 中包含 index 属性,则在 drop 组件 hover 和 drop 是可以根据第一个参数获取到 index 值
item: { type: 'page', index }
});
const [, drop] = useDrop({
accept: 'page',
hover(item: { type: string; index: number }) {
if (!ref.current) {
return;
}
const dragIndex = item.index;
const hoverIndex = index;
// 自定义逻辑处理

// 执行 move 回调函数
moveCard(dragIndex, hoverIndex);
}
});
dragPreview(drop(ref));

return (
<div ref={ref} className={className} onClick={onClick}>
{children}
</div>

);
};

使用

使用 Antd Upload itemRender


image.png


Antd 版本低于 4.16.0


使用 react-sortable-hoc 库实现


  • SortableItem


const SortableItem = SortableElement((params: SortableItemParams) => (
<div>
<UploadList
locale={{ previewFile: '预览图片', removeFile: '删除图片' }}
showDownloadIcon={false}
showRemoveIcon={params?.props?.disabled}
listType={params?.props?.listType}
onPreview={params.onPreview}
onRemove={params.onRemove}
items={[params.item]}
/>

</div>

));


  • SortableList


const SortableList = SortableContainer((params: SortableListParams) => {
return (
<div className='sortableList'>
{params.items.map((item, index) => (
<SortableItem
key={`${item.uid}`}
index={index}
item={item}
props={params.props}
onPreview={params.onPreview}
onRemove={params.onRemove}
/>

))}
{/* 这里是上传组件,设置最大限制后超出隐藏 */}
<Upload {...params.props} showUploadList={false} onChange={params.onChange}>
{params.props.children}
</Upload>
</div>

);
});


  • DragHoc


const DragHoc: React.FC<Props> = memo(
({ onChange: onFileChange, axis, onPreview, onRemove, ...props }) => {
const fileList = props.fileList || [];
const onSortEnd = ({ oldIndex, newIndex }: SortEnd) => {
onFileChange({ fileList: arrayMove(fileList, oldIndex, newIndex) });
};

const onChange = ({ fileList: newFileList }: UploadChangeParam) => {
onFileChange({ fileList: newFileList });
};

return (
<>
<SortableList
// 当移动 1 之后再触发排序事件默认是0会导致无法触发图片的预览和删除事件
distance={1}
items={fileList}
onSortEnd={onSortEnd}
axis={axis || 'xy'}
helperClass='SortableHelper'
props={props}
onChange={onChange}
onRemove={onRemove}
onPreview={onPreview}
/>

</>

);
}
);

使用方式

可直接替换掉 Upload 组件,props 不变。
如果项目中有已经封装好的 Upload 上传组件,尽量不改变原有逻辑代码前提下,更希望以插件的形式按需加载?方案:



可以剔除掉 SortableList 中 SortableContainer 包裹的 Upload 组件(这一步经过实践是可行的,说 Upload UploadList 都要被 SortableContainer 包裹,否走会重复上传和拖拽失败?目前我是没遇到,重复上传是因为 Upload 组件 showUploadList 拖拽场景下必须是 false )



使用案例:( isDrag 表示需要拖拽场景,继而加载)



{isDrag && (
<DragHoc
accept={accept}
axis={axis}
showUploadList={{ showRemoveIcon }}
fileList={fileList}
onChange={(e) =>
{
if (onChange) {
onChange(e);
}
}}
onPreview={preview}
onRemove={remove}
listType={listType}
/>

)}

注:当然也可以用 react-dnd 来实现,只是多行拖拽流畅性较差。感兴趣也可以试试


踩坑

图片按钮点击无效

在 Antd 的 Upload 组件中,图片墙上会有「预览」、「删除」等按钮,但是在 react-sortable-hoc 的逻辑中,只要我点击了图片,就会触发图片的拖拽函数,无法触发图片上的各种按钮,所以需要在 SortableList 上重新设置一下 distance 属性,设置成 1 即可。

官网:



If you'd like elements to only become sortable after being dragged a certain number of pixels. Cannot be used in conjunction with the pressDelay prop.

(如果您希望元素仅在拖动一定数量的像素后才可排序。不能与 pressDelay 道具一起使用。默认为 0)



上传图片一直 uploading

image.png

原因:当图片列表发生变化,整个 sortable 容器被删除并重新渲染,导致请求失效。


解决方案:



需要将 SortableItem,SortableList 写在 React.FC 外面,每次组件内部 state 发生变化,不会重新执行 SortableContainer 和 SortableElement 方法,就可以让可排序容器里面的元素自动只更新需要改变的 DOM 元素,而不会整个删除并重新渲染了。



图片的 disabled 状态失效

原因:SortableContainer 包裹的组件对 Upload 图片和上传进行了拆分处理,所以需要单独去控制预览和删除按钮


const SortableItem = SortableElement((params: SortableItemParams) => (
<div>
<UploadList
locale={{ previewFile: '预览图片', removeFile: '删除图片' }}
showDownloadIcon={false}
showRemoveIcon={params?.props?.disabled} //这里需要单独控制
listType={params?.props?.listType}
onPreview={params.onPreview}
onRemove={params.onRemove}
items={[params.item]}
/>

</div>

));

效果展示

拖拽1.gif


参考文献



作者:叫叫技术团队
来源:juejin.cn/post/7312634879987122186
收起阅读 »

16进制竟然可以减小代码体积

web
随着前端项目的复杂度越来越高,打包后的文件体积也越来越大,这直接影响到页面的加载速度。为了优化加载速度,我们需要采取各种方式来减小包的体积。使用 16 进制存储就是一种非常有效的方法。 以 @tenado/lunarjs 库为例,它提供了阴阳历转换以及获取节日...
继续阅读 »

随着前端项目的复杂度越来越高,打包后的文件体积也越来越大,这直接影响到页面的加载速度。为了优化加载速度,我们需要采取各种方式来减小包的体积。使用 16 进制存储就是一种非常有效的方法。


@tenado/lunarjs 库为例,它提供了阴阳历转换以及获取节日信息等功能。如果我们查看它的源码,可以看到使用了 16 进制存储。例如src/lunar.js


# src/lunar.js
export default [ 0x4bd8, 0x4ae0, 0xa570, 0x54d5, 0xd260, ... ];

这些信息如果用字符串存储,会占用很多字节。但使用 16 进制后,每个阴历信息只需要 6 个字符串。这极大地减少了库的大小。在实际生产环境中,使用 16 进制存储甚至可以节省几十 KB 的大小。


此外,16 进制还可以用于压缩其他数据,比如图片等资源。使用 16 进制编码,可以达到无损压缩的效果,相比传统压缩算法可以减小体积而不影响质量。


总之,16 进制编码是一种非常高效的存储方式,可以大幅减小项目的打包体积,提升页面加载速度。在前端优化中,合理使用 16 进制编码是一个非常重要和有效的手段。


16 进制的基本概念


16 进制在数学中是一种逢16进1的进位制。一般用数字0到9和字母A到F表示,其中:A~F相当于十进制的10~15,这些称作十六进制数字,在 js 中,16 进制使用 0x 前缀表示。


它的优点是可以使用更少的位数来表示一个数值,每个 16 进制位数代表 4 个二进制位,例如0x10,表示16进制的10,十进制的16,二进制表示为00010000,占用 4 个二进制位。


16 进制在代码中的表示


在库@tenado/lunarjs 中,保存了 1900-2100 年的阴历信息,数据格式如下:


{
1900: {
year: 1900,
firstMonth: 1,
firstDay: 31,
isRun: false,
runMonth: 8,
runMonthDays: 29,
monthsDays: [29, 30, 29, 29, 30, 29, 30, 30, 30, 30, 29, 30],
},
1901: {
year: 1901,
firstMonth: 2,
firstDay: 19,
isRun: false,
runMonth: 0,
runMonthDays: 0,
monthsDays: [29, 30, 29, 29, 30, 29, 30, 29, 30, 30, 30, 29],
},
}

将 1900-2100 年的数据存起来,占用的体积大概是 41k,作为包来说这个体积很大,这里存为十六进制数据,将数据压缩到 4k。


如何压缩数据呢?查看数据规律,我们发现:



闰月天数:只有三种值,即 0、29、30,在计算的时候先判断是否为闰月,再计算天数,0 代表 29, 1 代表 30




1-12 月天数,天数可以为 29 和 30,分别用 0 和 1 表示




闰月月份,闰月可能为 1-12,因此我们使用4个二进制数表示,最大可以表示 16



用 17 个二进制数表示阴历数据信息,从右到左:


1716-54-1
闰月天数1-12 月天数闰月月份

这里transform/index.js实现了一个简单的转换处理,将数据转换为 1900-2100 年依次按 index 排序的数组,数组的每一项里面存储了该年的阴历信息。


使用位运算从 16 进制中还原数据


位运算是将参与运算的数字转换为二进制,然后逐位对应进行运算。



按位与& 按位与运算为:两位全为1,结果为1,即1&1=1,1&0=0,0&1=0,0&0=0




按位或| 按位或运算为:两位只要有一位为1,结果则为1,即1|1=1,1|0=1,0|1=1,0|0=0




异或运算^ 两位为异,即一位为1一位为0,则结果为1,否则为0。即1 ^ 1=0,1 ^ 0=1,0 ^ 1=1,0 ^ 0=0




取反~ 将一个数按位取反,即~ 0 = 1,~ 1 = 0




右移>> 将一个数右移若干位,右边舍弃,正数左边补0,负数左边补1。每右移一位,相当于除以一次2 例如8 >> 2表示将8的二进制数1000右移两位变成0010 例如i >>= 2表示将变量i的二进制右移两位,并将结果赋值给i




设置二进制指定位置的值为1 value | (1 << position),例如设置十进制数8(1000)的第2位二进制数为1,注意这里index从0开始,且是从右向左计算,可以这样做8 | (1 << 2),结果为1100




设置二进制指定位置的值为0 value & ~(1 << position),例如设置十进制数8(1000)的第3位二进制数为0,注意这里index从0开始,可以这样做8 & ~(1 << 3),结果为0000



1、获取 1-4 位存储的闰月月份信息


取出16进制数据中存储的,从右边数1-4位数据,使用二进制数1111和十六进制数据按位与运算,可以获取到月份信息。通过转换1111可以得到对应的十六进制为0xf


例如,从src/lunar.js的数据里面获取1900年,即index为0的数据,进行位运算,0x4bd8 & 0xf可以得到结果为8,即1900年的8月为闰月。


2、获取 5-16 位存储的月天数信息


取出16进制数据中存储的,从右边数5-16位数据,仍旧可以使用按位与运算,获取到月天数信息。


从16开始的二进制数为1000000000000000,对应的十六进制为0x8000,到4结束的二进制数为1000,对应的十六进制为0x8,每次向右移一位进行按位与计算,可以获取到1-12月的天数数据,可以这样计算:


let sum = 0;
const lunar = 0x4bd8;
for (let i = 0x8000; i > 0x8; i >>= 1) {
sum += lunar & i ? 1 : 0;
}

3、获取 17 位存储的闰月天数信息


取出16进制数据中存储的,从右边数17位数据,使用二进制数10000000000000000和十六进制数据按位与运算,可以获取到闰月天数信息,10000000000000000对应的十六进制为0x100000x4bd8 & 0x10000可以得到结果为0,即1900年的闰月天数为29。


总结


总结起来,使用16进制保存数据可以有效减小包体积,提高前端项目的加载速度。在具体实现上,可以利用位运算来从16进制中还原数据。以下是一些关键点的总结:


1、数据格式


数据以16进制形式存储,可以有效减小体积。在JavaScript中,16进制使用0x前缀表示,例如0x4bd8。


2、数据规律


观察数据规律,了解存储的信息是如何组织的,包括每部分数据的含义和位数


3、使用位运算还原数据


使用位运算可以从16进制中还原具体的数据。以下是一些常用的位运算操作:


按位与&: 用于提取指定位的信息。
右移>>: 用于将二进制数向右移动,类似于除以2的操作。
设置二进制指定位置的值为1: 使用value | (1 << position)操作。
设置二进制指定位置的值为0: 使用value & ~(1 << position)操作。

4、注意事项


使用16进制存储需要在代码中添加注释,以便他人理解和维护。


数据存储格式的选择要根据具体场景和需求,权衡可读性和体积优化。


综合以上总结,合理使用16进制存储数据是一种有效的前端优化手段,特别适用于需要大量静态数据的情况。


作者:是阿派啊
来源:juejin.cn/post/7312611470733836340
收起阅读 »

一个看起来只有2个字长度却有8的字符串引起的bug

web
前言 我们有一个需求,用户的昵称如果长度超过6就截取前6个字符并显示...。今天,测试突然提了一个bug,某个用户的昵称只显示了...,鼠标hover的时候又显示2个字的昵称。刚看到这个问题的时候我也是一头雾水。 找出原因 在看到这个现象后,我发现其他昵称都...
继续阅读 »

前言


我们有一个需求,用户的昵称如果长度超过6就截取前6个字符并显示...。今天,测试突然提了一个bug,某个用户的昵称只显示了...,鼠标hover的时候又显示2个字的昵称。刚看到这个问题的时候我也是一头雾水。


找出原因


image.png

在看到这个现象后,我发现其他昵称都显示正常,但实在摸不着头脑这到底是怎么回事。然后查看了一下其他2个字的昵称是没问题的,然后通过console.log发现这个昵称居然长度有8,走了截取的分支。然后通过google发现这里面应该包含了零宽字符。

其实,第一时间就应该想到这个字符串不对劲的,但完全忘记了零宽字符的存在,走了不少弯路。


在查找的过程中发现,Array.from可以查看字符串的真实长度,除了emoji


image.png

不过Array.from并不能解决我的问题。


使用正则匹配unicode码点过滤零宽字符


在网上找了个方法来过滤掉这些看不见的字符,最常见的解决方案就是下面这行代码。


str.replace(/[\u200b-\u200f\uFEFF\u202a-\u202e]/g, "");

然而并没有用,我开始怀疑是不是这个方法有问题,然后遍历了这个昵称,把它的每个字符都转换成码点,发现这个昵称里的零宽字符并不是常见的这几种。


后来,又找到了一个比较完善的码点正则,但它太完善了,很长很长,也会过滤掉emoji,这可不行,用户昵称可能会包含emoji的。(这里就不贴出来代码了,太长了而且不适合我的情况。)


使用正则匹配unicode类别


一个字符有多种unicode属性,而正则支持按unicode属性匹配。


function stripNonPrintableAndNormalize(text, stripSurrogatesAndFormats) {
// strip control chars. optionally, keep surrogates and formats
if(stripSurrogatesAndFormats) {
text = text.replace(/\p{C}/gu, '');
} else {
text = text.replace(/\p{Cc}/gu, '');
text = text.replace(/\p{Co}/gu, '');
text = text.replace(/\p{Cn}/gu, '');
}

// other common tasks are to normalize newlines and other whitespace

// normalize newline
text = text.replace(/\n\r/g, '\n');
text = text.replace(/\p{Zl}/gu, '\n');
text = text.replace(/\p{Zp}/gu, '\n');

// normalize space
text = text.replace(/\p{Zs}/gu, ' ');

return text;
}
console.log("⁡⁡⁠河豚".length);
console.log(stripNonPrintableAndNormalize("⁡⁡⁠河豚", true).length);

image.png


总结


这个昵称其实就是包含了&nobreak;,通过unicode类别匹配可以过滤掉它。


我之前有在原贴用户主页的控制台中看见了&nobreak;,但当时居然没当回事,以为是别人对昵称做的处理。如果直接搜它马上就能解决问题了,有不少人遇到non-break-space引发的bug。谨以此记,吸取教训。


参考链接:stackoverflow中的解决办法unicode属性


作者:河豚学前端
来源:juejin.cn/post/7312241785542541327
收起阅读 »

告别繁琐操作!Maven常用命令一网打尽,让你的项目开发事半功倍!

Maven作为一款强大的项目管理工具,已经成为了Java开发者的必备技能。那么,如何才能更好地利用Maven来管理我们的项目呢?本文将为你介绍Maven的常用命令,让你的项目构建更轻松!一、maven 的概念模型Maven 包含了一个项目对象模型 ,一组标准集...
继续阅读 »

Maven作为一款强大的项目管理工具,已经成为了Java开发者的必备技能。那么,如何才能更好地利用Maven来管理我们的项目呢?本文将为你介绍Maven的常用命令,让你的项目构建更轻松!

一、maven 的概念模型

Maven 包含了一个项目对象模型 ,一组标准集合,一个项目生命周期,一个依赖管理系统,和用来运行定义在生命周期阶段(phase)中插件(plugin)目标(goal)的逻辑。

Description

项目对象模型 (Project Object Model)

一个 maven 工程都有一个 pom.xml 文件,通过 pom.xml 文件定义项目的坐标、项目依赖、项目信息、插件目标等。

依赖管理系统(Dependency Management System)

通过 maven 的依赖管理对项目所依赖的 jar 包进行统一管理。

比如:项目依赖 junit4.9,通过在 pom.xml 中定义 junit4.9 的依赖即使用 junit4.9,如下所示是 junit4.9的依赖定义:

<dependencies>

<dependency>

<groupId>junit</groupId>

<artifactId>junit</artifactId>

<version>4.9</version>

<scope>test</scope>
</dependency>
<dependencies>

一个项目生命周期(Project Lifecycle)

使用 maven 完成项目的构建,项目构建包括:清理、编译、测试、部署等过程,maven 将这些过程规范为一个生命周期,如下所示是生命周期的各个阶段:
Description
maven 通过执行一些简单命令即可实现上边生命周期的各个过程,比如执行 mvn compile 执行编译、执行 mvn clean 执行清理。

一组标准集合

maven将整个项目管理过程定义一组标准,比如:通过 maven 构建工程有标准的目录结构,有标准的生命周期阶段、依赖管理有标准的坐标定义等。

插件(plugin)目标(goal)

maven 管理项目生命周期过程都是基于插件完成的。

二、Maven的常用命令

我们可以在cmd 中通过maven命令来对我们的maven工程进行编译、测试、运行、打包、安装、部署。下面将简单介绍一些我们日常开发中经常会用到的一些maven 命令。
Description

1、mvn compile 编译命令

compile 是 maven 工程的编译命令,作用是将 src/main/java 下的文件编译为 class 文件输出到 target目录下。

cmd 进入命令状态,执行mvn compile,如下图提示成功:
Description

查看 target 目录,class 文件已生成,编译完成。
Description

2、mvn test 测试命令

test 是 maven 工程的测试命令 mvn test,会执行src/test/java下的单元测试类。

cmd 执行 mvn test 执行 src/test/java 下单元测试类,下图为测试结果,运行 1 个测试用例,全部成功。

Description

3 、mvn clean 清理命令

clean 是 maven 工程的清理命令,执行 clean 会删除 target 目录及内容。

4、mvn package打包命令

package 是 maven 工程的打包命令,对于 java 工程执行 package 打成 jar 包,对于web 工程打成war包。

只打包不测试(跳过测试):

mvn install -Dmaven.test.skip=true

5、 mvn install安装命令

install 是 maven 工程的安装命令,执行 install 将 maven 打成 jar 包或 war 包发布到本地仓库。

从运行结果中,可以看出:当后面的命令执行时,前面的操作过程也都会自动执行。

6、 mvn deploy 部署命令

这个命令用于将项目部署到远程仓库,以便其他项目可以引用。在执行这个命令之前,需要先执行mvn install命令。

7、mvn help:system

这个命令用于查看系统中可用的Maven版本。

8、mvn -v

这个命令用于查看当前环境中Maven的版本信息。

9、源码打包

#源码打包
mvn source:jar

mvn source:jar-no-fork

10、Maven 指令的生命周期

关于Maven的三套生命周期,前面我们已经详细的讲过了,这里再简单地回顾一下。

maven 对项目构建过程分为三套相互独立的生命周期,请注意这里说的是“三套”,而且“相互独立”。

Description

这三套生命周期分别是:

  • Clean Lifecycle 在进行真正的构建之前进行一些清理工作。

  • Default Lifecycle 构建的核心部分,编译,测试,打包,部署等等。

  • Site Lifecycle 生成项目报告,站点,发布站点。

在这里给大家分享一下【云端源想】学习平台,无论你是初学者还是有经验的开发者,这里都有你需要的一切。包含课程视频、在线书籍、在线编程、一对一咨询等等,现在功能全部是免费的,点击这里,立即开始你的学习之旅!

三、Maven常用技巧

1、使用镜像仓库加速构建

由于Maven默认从中央仓库下载依赖,速度较慢。我们可以配置镜像仓库来加速构建。在settings.xml文件中添加以下内容:


<mirrors>
<mirror>
<id>aliyunmaven</id>
<mirrorOf>*</mirrorOf>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
</mirror>
</mirrors>

2、使用插件自定义构建过程

Maven提供了丰富的插件来帮助我们完成各种任务,如代码检查、静态代码分析、单元测试等。我们可以在pom.xml文件中添加相应的插件来自定义构建过程。例如,添加SonarQube插件进行代码质量检查:


<build>
<plugins>
<plugin>
<groupId>org.sonarsource.scanner.maven</groupId>
<artifactId>sonar-maven-plugin</artifactId>
<version>3.9.1.2184</version>
</plugin>
</plugins>
</build>

四、第一个Maven程序

IDEA创建 Maven项目

打开我们的IDEA,选择 Create New Project

Description
然后,选择 Maven 项目
Description
填写项目信息,然后点击next
Description
选择工作空间,然后第一个Maven程序就创建完成了
Description
以上就是IDEA创建创建第一个 Maven项目的步骤啦,都看到这里了,不要偷懒记得打开电脑和我一起试一试哦。

目录结构

Java Web 的 Maven 基本结构如下:

├─src
│ ├─main
│ │ ├─java
│ │ ├─resources
│ │ └─webapp
│ │ └─WEB-INF
│ └─test
│ └─java

结构说明:

  • src:源码目录

  • src/main/java:Java 源码目录

  • src/main/resources:资源文件目录

  • src/main/webapp:Web 相关目录

  • src/test:单元测试

小结:

通过掌握Maven的常用命令和技巧,我们可以更高效地管理Java项目,提高开发效率。希望这篇文章能帮助大家更好地使用Maven。

收起阅读 »

如何告别502、友好的告知用户网站/app正在升级维护

web
封面只是想分享一张喜欢的图片,嘻嘻嘻 一个产品,用户除了会在意流程简不简单、好不好用、页面好不好看以外,各种小细节也很重要。 今天讲讲我接到的一个新需求——整改版本更新功能。 一、以前是这样的 1. 发版前:微信群通知用户 如果计划今晚发版,研发部就通知运营...
继续阅读 »

封面只是想分享一张喜欢的图片,嘻嘻嘻



一个产品,用户除了会在意流程简不简单、好不好用、页面好不好看以外,各种小细节也很重要。


今天讲讲我接到的一个新需求——整改版本更新功能。


一、以前是这样的


1. 发版前:微信群通知用户


如果计划今晚发版,研发部就通知运营部,运营部再在群里通知各用户"我们将于YYYY年MM月DD日 hh:mm:ss发版,请大家......"


image.png


2. 发版时:接口502+页面无响应


在这之前,发版时用户仍停留在网站,但接口返回502,但502未告知给用户,所以用户不知道这时到底发生什么了,只知道页面没数据了、卡着动不了了、怎么刷新都没办法......


image.png


3. 发版后:app粗暴的强制更新


在这之前我们的项目还处于快速开发的过程,迭代间隔短频率高,有时我们会更新影响流程的重要功能,担心用户未及时更新,所以我们只提供了强制更新方案。


二、现在是这样的


这都什么时代了,还需要口口相传......


1. 发版前:升级预告


大家想想618、双十一、双十二,各大平台是不是很早就开始宣传“走过路过别错过,满三百减五十,快来加购吧~”


我们产品的用户群体有一线工人、办公室文员、喝茶的经理、车上的老总,提前告知用户,可以让经理及时换班、让工人规避做工单的风险等等。总之,有事早通知,准是没错的。


怎么通知呢?



  1. 在内部管理平台新增一条升级预告消息,可以包含版本号version、预告内容content、预告状态status(是否有效)、平台(区分网站和APP,因为可能不是同时都需要发版)

  2. 网站:

    1)用户登录或主动刷新页面时,页面顶部显示升级预告,实现逻辑和app一样

  3. APP:

    1)充分利用消息推送、短信推送,可以根据自身业务来定,看这次更新是否需要紧急通知用户,我们仅使用消息推送。平台新增一条升级预告后,就主动推送一条app消息给用户


    2)打开app后,弹框弹出升级提醒(包含“我知道了”和“不再提醒”按钮),在store记录预告消息的id。




  • 点击“不再提醒”,则在store缓存中将isRemind置为false

  • 点击“我知道了”,则下次打开首页还会显示弹框。

  • 每次打开首页时,从接口获取到数据,如果消息状态有效status: true,则判断消息id同缓存中消息id是否一致:一致则表示是同一条消息,则根据缓存中的isRemind来判断是否显示消息;不一致则表示是新消息,直接显示。


1702434850828_56C69DC0-7552-4e81-AA8D-0CB2B275BB09.png


2. 发版时:


场景:我们产品是全量发布,发布完成后网页能正常访问,但这时产品会进行验收,还不希望用户进行访问。


页面:pc和app的升级中页面单独写在项目中,这样可以在页面中写监听:监听到版本正常后就返回首页。


方案:发版中和验收中,运维将别人的IP设置为不能访问,将公司特定IP设置为可访问。


网站:不能访问的,nginx就设置跳转到升级中页面;能访问的就不做处理,发版时页面会接口报502(只能内部人员能看见),发版后验收时页面正常使用。


APP:app中不好重定向,所以通过配置文件来告诉前端该用户是否应该访问页面。远程存在2个json配置文件,内容就是一个对象之类的{isEntry: 1}{isEntry: 0},分别表示可以访问和不可访问;app打开时,前端请求json,根据是否可以访问做处理,不能访问的,前端让重定向到升级中页面,能访问的不做处理。


3. 发版后:


内部管理平台:上传新的安装包、配置升级文案等


网站:所有人正常使用


APP:所有人正常使用,打开App如果版本较低则会主动打开"版本更新"弹框。


APP可以提升用户体验的地方:



  • 版本判断放在登录之前,先检查是否有更新,也就是这个接口不需要用户权限控制

  • 升级页面判断是否连接了wifi,连接wifi则更新不耗流量,没连接则显示此安装包只有多大

  • 提供“暂不更新”和“检查更新”的入口


image.png


总结


思考方案时,自己感觉做了一件大事;但多思考几次后,特别是写完文章后,又觉得新的升级方案其实很简单,上面说了一堆废话。



没关系,把每一件小事做好,就很好啦~~



作者:LJINGER
来源:juejin.cn/post/7311695633563795506
收起阅读 »

⭐️天啦噜~实习生被当作正式员工直接上手toc端项目啦

web
背景 本需求的提出基于谷歌应用商店的硬性要求:需要在xxx日前于谷歌商店上架账号注销的网页,保证玩家能够通过网页进行账号注销,从而满足谷歌商店的硬性需求。(问了产品和运营在谷歌商店哪,结果都找不到在哪,离谱)账号注销 配置解析 package.json 我个人...
继续阅读 »

背景


本需求的提出基于谷歌应用商店的硬性要求:需要在xxx日前于谷歌商店上架账号注销的网页,保证玩家能够通过网页进行账号注销,从而满足谷歌商店的硬性需求。(问了产品和运营在谷歌商店哪,结果都找不到在哪,离谱)账号注销


配置解析


package.json


我个人拉项目的时候比较喜欢从package.json中开始了解项目,比如项目中用了哪些第三方依赖,项目使用的是vue-cli启动还是webpack启动等等......


"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"postinstall": "patch-package"
},

比如上述中使用的是vue-cli(vue官方脚手架)启动的项目


serve:不用说,使用vue-cli启动项目


build:使用vue-cli打包项目,打包成js,css,html文件,具体看下图(这里是vue-cli打包为样例,vite打包的话未说明)


image.png


这里统一说明,下列所有文件,


前面的那串类似190.xxx.xxx,是由Webpack 为每个模块分配一个唯一的数字标识,这个标识通常代表了模块在整个打包中的位置。


中间的那串类似xxx.a768c482.xxx,都是由webpack或者vue-cli在构建(build)时,通过计算文件内容生成的哈希值,这样可以确保文件内容的唯一性和变化时生成不同的哈希值。所以在文件内容发生变化时,生成的文件名也会相应变化,从而避免浏览器缓存旧的文件。



注:每个css和js的前缀都基本对应,并且由于是webacpk生成的,所以可以自己额外的对其命名进行配置。



css(压缩过)


image.png



  • chunk-vendors:以chunk-vendors开头的,主要是对于引入的第三方依赖的样式,比如项目中使用的ant-design-vue,这里面就包含了ant-design-vue的样式


image.png

  • app:项目自身的样式代码,除了路由router里配置的组件

  • 其他:路由router中配置的组件里的样式(删掉路由配置的组件后,相应的打包样式文件消失了)


js(压缩过)


与css类似,多了map(映射文件)和-legacy后缀


source map文件包含了源代码与生成代码之间的映射关系,用于在浏览器中调试时将生成代码映射回源代码。


-legacy 的后缀通常表示这部分代码是针对不支持现代 JavaScript 特性的旧版浏览器生成的。


image.png


img 项目中使用过的图片,没使用的不会进行打包


index.html 原项目中public/index.html压缩后的


favicon.icon 原项目中public/favicon.ico图标


lint: 检查代码风格和潜在错误的方法。


也可以在项目根目录下的 .eslintrc.js 文件中进行自定义的规则定制


module.exports = {
root: true, // 表示 ESLint 应该停止在父级目录中查找配置文件。
env: { // 将 Node.js 的全局对象和一些特定于 Node.js 环境的变量(例如 `process`、
node: true, // `require` 等) 考虑在内,以避免对这些变量的使用产生未定义的警告或错误。
},
extends: [ // 包含了所使用的 ESLint 规则集,包含几个扩展
"plugin:vue/essential",
"eslint:recommended",
"@vue/typescript/recommended",
"plugin:prettier/recommended",
],
parserOptions: { //指定解析器版本,确保 ESLint 解析器能够正确理解代码中使用的 JavaScript 特性
ecmaVersion: 2020,
},
rules: {
// 在生产环境中允许控制台输出,但在开发环境中关闭。
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
"vue/multi-word-component-names": "off", // 关闭 Vue 组件名使用多个单词的规则。
},
};


检查警告效果图:
image.png


postinstall:会检测 node_modules 中的包是否有需要修复的问题,并自动打补丁。


gitHooks


"gitHooks": {
"pre-commit": "lint-staged"
}

指定了在执行 Git 提交前(pre-commit 钩子)运行 lint-staged。这是一种通过 git 钩子(git hooks)来自动化代码检查和格式化的方法。(即当你执行git commit 后会进行检查)可以在lint-staged.config.js中配置,也可以在package.json中。


// lint-staged.config.js
module.exports = {
"*.{js,jsx,vue,ts,tsx}": "vue-cli-service lint", // js,jsx,vue,ts,tsx文件都会检查
};


env环境变量


环境变量在不同的环境下是不同的,比如现在下面的环境变量是开发环境的,当到正式环境时,baseUrl会换成类似https://juejin.cn/,也就是把原本32进制的ip地址换成了这种形式。


后端是对打包(build)后项目进行部署的,而env文件后端需要看到并且对你的环境变量相应的替换,才能正式上线部署。


window.$$env = {
baseUrl: "/test/apis",
appId: "test",
publicPath: "/test",
};

export interface Env {
baseUrl: string;
appId: string;
publicPath: string;
}

const env = (window as any).$$env as Env;
export default env;


封装网络拦截


先使用枚举定义状态码


export enum HttpCode {
Ok = 0,
ServerError = 500,
COOKIE_INVALID = 204,
INFO_INVALID = 205,
ERR_PRODUCT_CHANGE = 402,
SUSPENSION = 503,
}


封装一个网路拦截


export class apiService {
static instance: AxiosInstance | null = null;

// 重置网络拦截
static resetConfig(config?: AxiosRequestConfig, appId?: string) {
this.instance = this.createAxiosInstance(config, appId);
}

static getInstance() {
return this.instance || this.createAxiosInstance();
}

static createAxiosInstance(config?: AxiosRequestConfig, appId?: string) {
// 创建axios实例
xxx
// 请求拦截
xxx
// 响应拦截
xxx
return instance;
}
}

创建axios实例


const instance = Axios.create({
withCredentials: true, // 允许发送跨域请求的时候携带认证信息,通常是 Cookie
baseURL: env.baseUrl, // 网路请求前缀
timeout: 30 * 1000, // 超时请求时间限制
...config,
});

请求拦截


根据项目需求传请求头,比如用户信息,Authorization等


// 请求拦截
instance.interceptors.request.use((config) => {
config.headers = {
...config.headers,
"x-yh-appid": env.appId,
};
return config;
});

响应拦截


根据后端传回来的状态码处理相应的状态


// 响应拦截
instance.interceptors.response.use(
(response: AxiosResponse<any>) => {
const { code = -1, data = {}, msg = "" } = response.data;
if (handleUnlogin(code)) {
return Promise.reject(msg);
}
if (code === HttpCode.SUSPENSION) {
redirectSuspension();
return Promise.reject(msg);
}
if (code === HttpCode.Ok) {
return Promise.resolve(data);
}
return Promise.reject(msg);
},
(error) => {
return handleHttpError(error);
}
);

根据后端发送的状态码,判断用户是否登录


export function handleUnlogin(code: number) {
if ([HttpCode.COOKIE_INVALID, HttpCode.INFO_INVALID].includes(code)) {
localStorage.removeItem(LOCALSTORAGE_CURRENCY_CODE);
redirectLogin();
return true;
}
return false;
}

处理后端返回的错误信息


export function handleHttpError(error: any) {
const { status = 500, data = {} } = error.response || {};
let msg = data.msg || error.message;
switch (status) {
case HttpCode.ServerError:
msg = "Server internal error";
break;
}
return Promise.reject(msg);
}

功能设计


国际化设计


没配置翻译前但使用了vue-i18n


// 
export enum Direction {
UP = "上",
DOWN = "下",
LEFT = "左",
RIGHT = "右",
}

使用方法,vue中通过$t()来注入翻译文本


<script lang="ts">
import { Translate } from "@/constants";
import { defineComponent } from "vue";
export default defineComponent({
setup() {
return { Translate };
},
});
</script>

<span>{{ $t(Translate.UP) }}</span>
<input :placeholder="$t(Translate.UP)" />

实际展示


<span></span>
<input placeholder="上" />

配置翻译后


// main.ts
import i18n from "./locales";

new Vue({
router,
i18n,
render: (h) => h(App),
}).$mount("#app");

src/locales/index


// src/locales/index
import VueI18n from "vue-i18n";
import en from "./language/en";

const i18n = new VueI18n({
locale: "en",
messages: {
en,
},
});

src/locales/language/en


// src/locales/language/en
import { Translate } from "@/translate";

export default {
[Translate.UP]: "Up",
[Translate.DOWN]: "Downe",
[Translate.LEFT]: "Left",
[Translate.RIGHT]: "Right",
}

实际展示


<span>Up</span>
<input placeholder="Up" />

main.ts中引入了配置好后的i18n,就会对每个组件中$t(Translate.xx)进行翻译,然后如果想翻译成其他语言,只需要修改在src/locales/index并且在src/locales/language中新增一个其他语言的文件


比如日文(看看就行,翻译别当真)


// src/locales/index
const i18n = new VueI18n({
locale: "ja",
messages: {
ja,
},
});

// src/locales/language/ja
import { Translate } from "@/translate";

export default {
[Translate.UP]: "じょうげ",
[Translate.DOWN]: "さゆう"
}

pc端和移动端适配设计(适用于结构类似,各自两套样式)


适配原理


export class SettingService {
// 表示该属性是只读的,即一旦被赋值,就不能再被修改。
// 确保 `mode` 属性在运行时保持不变,避免了一些意外的修改。
readonly mode: "pc" | "mobile";

constructor() {
// 判断设备是什么类型的,进行初始化
this.mode = isAndriod() || isIos(false) ? "mobile" : "pc";
// 将 `mode` 作为全局变量挂载到 Vue 的原型上,以便在整个应用程序中访问
Vue.prototype.$global = { mode: this.mode };
}
// 引入该方法判断是否是pc端,便于读取mode状态
isPc() {
return this.mode === "pc";
}
}

export const settingService = new SettingService();

整个项目适配


pc端和移动端各自展示的窗口样式是不同的,所以需要在容器中设置不同的样式


// router.js
{
path: "/",
component: settingService.isPc() ? PcLayout : MobileLayout,
name: "layout",
redirect: "/notice",
}

// pc端
<template>
<div class="layout">
<layout-header />
<div class="layout-kv"></div>
<router-view></router-view>
<layout-footer />
</div>
</template>

// 移动端
<template>
<div class="layout">
<layout-header />
<router-view class="layout-body"></router-view>
<layout-footer />
</div>
</template>

在App.vue中设置了 <body> 元素的 screen-mode 属性,属性值为 settingService.mode。这样,通过在 <body> 元素上设置这个属性,可以影响到整个页面中使用了相应选择器的样式。


<template>
<loading v-if="initing" />
<router-view v-else />
</template>
// App.vue
export default {
name: "App",
mounted() {
document.body.setAttribute("screen-mode", settingService.mode);
}
}

适配案例(即使用方法)


<div class="myClass1">不会覆盖</div>
<div class="myClass2">会覆盖</div>

默认为[screen-mode="mobile"]上面的样式,当切换到移动端时,下面的会覆盖上面同一类名的样式。


.myClass1 {
color: red
}
.myClass2 {
color: red;
font-size: 16px;
}

[screen-mode="mobile"] {
.myClass2 {
color: blue;
font-size: 32dpx;
}
}

解决页面显示的是缓存的内容而不是最新的内容


// 浏览器回退强制刷逻辑
window.addEventListener("pageshow", (event) => {
if (event.persisted) {
window.location.reload();
}
});

监听了 pageshow 事件,该事件在页面显示时触发,包括页面加载和页面回退(从缓存中重新显示页面)。



  • window.addEventListener("pageshow", (event) => {...});: 给 window 对象添加了一个 pageshow 事件监听器。当页面被显示时,这个监听器中的回调函数将被执行。

  • if (event.persisted) {...}: event.persisted 是一个布尔值,表示页面是否是从缓存中恢复显示的。如果为 true,表示页面是通过浏览器的后退/前进按钮从缓存中加载的。

  • window.location.reload(): 如果页面是从缓存中加载的,就调用 window.location.reload() 强制刷新页面,以确保页面的状态和内容是最新的。


这种逻辑通常用于解决缓存导致的页面状态不一致的问题。在有些情况下,浏览器为了提高性能会缓存页面,但有时这可能导致页面显示的是缓存的内容而不是最新的内容。通过在 pageshow 事件中检测 event.persisted,可以判断页面是否是从缓存中加载的,如果是,则强制刷新页面,确保它是最新的状态。


实时监听登录状态设计(操作浏览器前进回退刷新)


popstate 事件监听器,它会在浏览器的历史记录发生变化(比如用户点击浏览器的后退或前进或刷新按钮,或者执行了类似 history.back()history.forward()history.go(-1) 等 JavaScript 操作导致页面的 URL 发生了变化)。但使用router.push之类的操作不会触发。


 window.addEventListener("popstate", () => {
if (!settingService.hasUser() && !isLogin()) {
router.push("/login");
return;
}
});

对用户进行埋点(埋点时机)


埋点是对用户的一些信息进行收集,比如用户登录网站的时间,用户的昵称等等。


业务功能


1. 阅读须知,滑到底部并且勾选了同意按钮才能执行下一步


image.png
<div
ref="scrollContainer"
style="height: 340px;overflow-y: auto;"
@scroll="handleScroll"
>

文本内容
</div>
<div>
<input @change="handleScroll" type="checkbox" v-model="isChecked" />
<label for="customCheckbox">I know and satisfy all the conditions</label>
</div>
<!-- 执行下一步按钮 -->
// 如果阅读完了并且勾选了同意按钮,则可以执行下一步,否则不能
<button
v-if="isReaded && isChecked"
@submit="gotoPage"
/>

<button v-else disabled/>

// 创建响应式 ref
const scrollContainer: any = ref(null);
// 是否阅读须知到底部
let isReaded = ref<boolean>(false);
// 是否勾选同意
const isChecked = ref<boolean>(false);

// 滚动事件处理逻辑
const handleScroll = () => {
if (scrollContainer.value) {
// 判断是否滚动到底部
1const height = scrollContainer.value.scrollHeight - scrollContainer.value.scrollTop;
const isAtBottom = height <= scrollContainer.value.clientHeight + 20;
if (isAtBottom && isChecked.value) {
// 表示已经阅读完了并且勾选了
isReaded.value = true;
}
}
};


【1】// 滑动框的总高度 scrollContainer.value.scrollHeight = 974


// scrollContainer.value.scrollTop = 滑动条距离顶部的距离


// 滑动框的可见高度 scrollContainer.value.clientHeight = 340


// 当scrollHeight-scrollTop 达到340时,即滚动到底部了


// 在上述基础上增加一个区域20,即360,防止不同设备的滚动条滚动高度不一致



2. 勾选原因才能执行下一步


image.png

该部分主要是勾选了“other"才会弹出文本框,并且后端传的数据是数组,因此需要对其进行处理。


<div v-for="option in options" :key="option.id">
<input
type="radio"
:id="option.id"
:value="option.id"
name="group"
v-model="selectedOption"
/>

<label :for="option.id">{{ option.label }}</label>
</div>
// 只有勾选了btn4才会展示
<textarea
v-if="selectedOption === 'btn4'"
v-model="textareaValue"
placeholder="If you do have any comments or suggestions please fill in here"
>
</textarea>

// 如果勾选了按钮,或者选择勾选了btn4并且输入了值
<button
v-if="(selectedOption && selectedOption !== 'btn4') || textareaValue"
@submit="gotoPage"
/>

<button v-else disabled />

const textareaValue = ref("");
const selectedOption = ref(null);
// 初始化原因列表
let options = ref([
{ id: "btn1", label: "1" },
{ id: "btn2", label: "2" },
{ id: "btn3", label: "3" },
{ id: "btn4", label: "4" },
]);

// 后端传的数据为["原因1","原因2","原因3","原因4"],需要进行处理
options.value.forEach((option, index) => {
option.label = resp.reason[index];
});

const gotoPage = () => {
// 把数组对象变为纯数组,并且由于other的值为文本输入值,需要进行判断
const reasonList = options.value.map((item) => {
if (selectedOption.value === item.id) {
if (selectedOption.value === "btn4") {
return textareaValue.value;
} else {
return item.label;
}
}
// 如果没有匹配的项,返回 undefined
});
// 最后数值为[undefined, "don't like this game", undefined, undefined]

// 再从reasonList中[undefined, "don't like this game", undefined, undefined]筛选出来原因即可
let reason = reasonList.filter((item) => item !== undefined)[0];

router.push({
path: "/reconfirm",
query: { reason: reason }
});
};


3. 文本框右下角显示输入值和限制


主要是样式,通过定位来进行布局。


image.png
<div v-if="selectedOption === 'bnt4'" style="position: relative">
<textarea
v-model="textareaValue"
:maxlength="maxCharacters"
>
</textarea>
<div
:class="{
'character-count': true,
'red-text': textareaValue.length === maxCharacters,
}"

>

{{ textareaValue.length }} / {{ maxCharacters }}
</div>
</div>

// 限制最大输入字数
const maxCharacters = 140;
const textareaValue = ref("");

.character-count {
position: absolute;
right: 10px;
bottom: 10px;
color: #888;
font-size: 12px;
}
.red-text {
color: red;
}

4. 输入指定的字才能执行下一步


image.png

主要是对@input的使用,然后进行判断


<div>
<textarea
type="text"
v-model="textareaValue"
:placeholder="reConfirmText"
@input="inputChange"
/>

</div>
<button v-if="isEqual" @submit="gotoPage" />
<button v-else disabled />

const reConfirmText = "I confirm to delete Ninja Must Die account";
const textareaValue = ref("");
const isEqual = ref(false);

const gotoPage = () => {
router.push("/hesitation");
};

const inputChange = () => {
if (textareaValue.value === reConfirmText) {
isEqual.value = true;
} else {
isEqual.value = false;
}
}

5. 登录界面需要使用iframe全屏引入


<iframe src="example.com" class="iframe"></iframe>

.iframe {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: none;
margin: 0;
padding: 0;
overflow: hidden;
z-index: 9999;
}

其他


代码优化(导师指点)



  1. 关于跳转路径的变量,用env传递,不要写死

  2. 常量尽量抽离出来,可以做成枚举的做成枚举

  3. 关于find,map之类的函数,能抽离出来的抽离出来,不要直接用,太抽象了


使用到的git操作(非常规)


1. 从一个仓库的代码放到另一个仓库上



场景:从第一个仓库中拉取代码到本地(比如团队中的模板仓库),但你需要把本地开发的代码(处于第一个仓库)推到第二个仓库中(真正开发仓库)



但你首先得在仓库上加ssh地址,打开powershell粘贴下述命令


ssh-keygen -t rsa -C "xxx@xxx.com"

回车到底


image.png


cat ~/.ssh/id_rsa.pub

复制所有


image.png

打开仓库,找到SSH Keys复制上去点击Add key即可


image.png
image.png
image.png

image.png


接下来就是正式操作了


git remote remove origin
git remote add origin xxx(目标的仓库ssh地址)
git checkout -b 'feature/zyj20231114'(在目标仓库新建一个开发分支)
git push --set-upstream origin feature/zyj20231114
git add
git commit
git push

2. 提交一个空白内容的提交



场景:由于是新项目,创建完主分支后,后端才会其打镜像,但需要前端再提交一次来触发dockek里镜像更新的脚本。(应该是这样,我个臭前端怎么可能太清楚后端弄镜像的啊,)



git commit --allow-empty -m “Message”

作者:吃腻的奶油
来源:juejin.cn/post/7311368716804603944
收起阅读 »

7个Js async/await高级用法

web
7个Js async/await高级用法 JavaScript的异步编程已经从回调(Callback)演进到Promise,再到如今广泛使用的async/await语法。后者不仅让异步代码更加简洁,而且更贴近同步代码的逻辑与结构,大大增强了代码的可读性与可维护...
继续阅读 »

7个Js async/await高级用法


JavaScript的异步编程已经从回调(Callback)演进到Promise,再到如今广泛使用的async/await语法。后者不仅让异步代码更加简洁,而且更贴近同步代码的逻辑与结构,大大增强了代码的可读性与可维护性。在掌握了基础用法之后,下面将介绍一些高级用法,以便充分利用async/await实现更复杂的异步流程控制。


1. async/await与高阶函数


当需要对数组中的元素执行异步操作时,可结合async/await与数组的高阶函数(如mapfilter等)。


// 异步过滤函数
async function asyncFilter(array, predicate) {
const results = await Promise.all(array.map(predicate));

return array.filter((_value, index) => results[index]);
}

// 示例
async function isOddNumber(n) {
await delay(100); // 模拟异步操作
return n % 2 !== 0;
}

async function filterOddNumbers(numbers) {
return asyncFilter(numbers, isOddNumber);
}

filterOddNumbers([1, 2, 3, 4, 5]).then(console.log); // 输出: [1, 3, 5]

2. 控制并发数


在处理诸如文件上传等场景时,可能需要限制同时进行的异步操作数量以避免系统资源耗尽。


async function asyncPool(poolLimit, array, iteratorFn) {
const result = [];
const executing = [];

for (const item of array) {
const p = Promise.resolve().then(() => iteratorFn(item, array));
result.push(p);

if (poolLimit <= array.length) {
const e = p.then(() => executing.splice(executing.indexOf(e), 1));
executing.push(e);
if (executing.length >= poolLimit) {
await Promise.race(executing);
}
}
}

return Promise.all(result);
}

// 示例
async function uploadFile(file) {
// 文件上传逻辑
}

async function limitedFileUpload(files) {
return asyncPool(3, files, uploadFile);
}

3. 使用async/await优化递归


递归函数是编程中的一种常用技术,async/await可以很容易地使递归函数进行异步操作。


// 异步递归函数
async function asyncRecursiveSearch(nodes) {
for (const node of nodes) {
await asyncProcess(node);
if (node.children) {
await asyncRecursiveSearch(node.children);
}
}
}

// 示例
async function asyncProcess(node) {
// 对节点进行异步处理逻辑
}

4. 异步初始化类实例


在JavaScript中,类的构造器(constructor)不能是异步的。但可以通过工厂函数模式来实现类实例的异步初始化。


class Example {
constructor(data) {
this.data = data;
}

static async create() {
const data = await fetchData(); // 异步获取数据
return new Example(data);
}
}

// 使用方式
Example.create().then((exampleInstance) => {
// 使用异步初始化的类实例
});

5. 在async函数中使用await链式调用


使用await可以直观地按顺序执行链式调用中的异步操作。


class ApiClient {
constructor() {
this.value = null;
}

async firstMethod() {
this.value = await fetch('/first-url').then(r => r.json());
return this;
}

async secondMethod() {
this.value = await fetch('/second-url').then(r => r.json());
return this;
}
}

// 使用方式
const client = new ApiClient();
const result = await client.firstMethod().then(c => c.secondMethod());

6. 结合async/await和事件循环


使用async/await可以更好地控制事件循环,像处理DOM事件或定时器等场合。


// 异步定时器函数
async function asyncSetTimeout(fn, ms) {
await new Promise(resolve => setTimeout(resolve, ms));
fn();
}

// 示例
asyncSetTimeout(() => console.log('Timeout after 2 seconds'), 2000);

7. 使用async/await简化错误处理


错误处理是异步编程中的重要部分。通过async/await,可以将错误处理的逻辑更自然地集成到同步代码中。


async function asyncOperation() {
try {
const result = await mightFailOperation();
return result;
} catch (error) {
handleAsyncError(error);
}
}

async function mightFailOperation() {
// 有可能失败的异步操作
}

function handleAsyncError(error) {
// 错误处理逻辑
}

通过以上七个async/await的高级用法,开发者可以在JavaScript中以更加声明式和直观的方式处理复杂的异步逻辑,同时保持代码整洁和可维护性。在实践中不断应用和掌握这些用法,能够有效地提升编程效率和项目的质量。


作者:慕仲卿
来源:juejin.cn/post/7311603994928513076
收起阅读 »

推送数据?也许你不需要 WebSocket

web
提到推送数据,大家可能会首先想到 WebSocket。 确实,WebSocket 能双向通信,自然也能做服务器到浏览器的消息推送。 但如果只是单向推送消息的话,HTTP 就有这种功能,它就是 Server Send Event。 WebSocket 的通信过程...
继续阅读 »

提到推送数据,大家可能会首先想到 WebSocket。


确实,WebSocket 能双向通信,自然也能做服务器到浏览器的消息推送。


但如果只是单向推送消息的话,HTTP 就有这种功能,它就是 Server Send Event。


WebSocket 的通信过程是这样的:



首先通过 http 切换协议,服务端返回 101 的状态码后,就代表协议切换成功。


之后就是 WebSocket 格式数据的通信了,一方可以随时向另一方推送消息。


而 HTTP 的 Server Send Event 是这样的:



服务端返回的 Content-Type 是 text/event-stream,这是一个流,可以多次返回内容。


Sever Send Event 就是通过这种消息来随时推送数据。


可能你是第一次听说 SSE,但你肯定用过基于它的应用。


比如你用的 CICD 平台,它的日志是实时打印的。


那它是如何实时传输构建日志的呢?


明显需要一段一段的传输,这种一般就是用 SSE 来推送数据。


再比如说 ChatGPT,它回答一个问题不是一次性给你全部的,而是一部分一部分的加载回答。


这也是基于 SSE。




知道了什么是 SSE 以及它的应用,我们来自己实现一下吧:


创建 nest 项目:


npx nest new sse-test


把它跑起来:


npm run start:dev


访问 http://localhost:3000 可以看到 hello world,代表服务器跑成功了:



然后在 AppController 添加一个 stream 接口:



这里不是通过 @Get、@Post 等装饰器标识,而是通过 @Sse 标识这是一个 event stream 类型的接口。


@Sse('stream')
stream() {
return new Observable((observer) => {
observer.next({ data: { msg: 'aaa'} });

setTimeout(() => {
observer.next({ data: { msg: 'bbb'} });
}, 2000);

setTimeout(() => {
observer.next({ data: { msg: 'ccc'} });
}, 5000);
});
}

返回的是一个 Observable 对象,然后内部用 observer.next 返回消息。


可以返回任意的 json 数据。


我们先返回了一个 aaa、过了 2s 返回了 bbb,过了 5s 返回了 ccc。


然后写个前端页面:


创建一个 react 项目:


npx create-react-app --template=typescript sse-test-frontend


在 App.tsx 里写如下代码:


import { useEffect } from 'react';

function App() {

useEffect(() => {
const eventSource = new EventSource('http://localhost:3000/stream');
eventSource.onmessage = ({ data }) => {
console.log('New message', JSON.parse(data));
};
}, []);

return (
<div>hello</div>
);
}

export default App;

这个 EventSource 是浏览器原生 api,就是用来获取 sse 接口的响应的,它会把每次消息传入 onmessage 的回调函数。


我们在 nest 服务开启跨域支持:



然后把 react 项目 index.tsx 里这几行代码删掉,它会导致额外的渲染:



执行 npm run start


因为 3000 端口被占用了,它会跑在 3001:



浏览器访问下:



看到一段段的响应了没?


这就是 Server Send Event。


在 devtools 里可以看到,响应的 Content-Type 是 text/event-stream:



然后在 EventStream 里可以看到每一次收到的消息:



这样,服务端就可以随时向网页推送消息了。


那它兼容性怎么样呢?


可以在 MDN 看到:



除了 ie、edge 外,其他浏览器都没任何兼容问题。


基本是可以放心用的。


那用在哪呢?


一些只需要服务端推送的场景就特别适合 Server Send Event。


比如这个站内信:



这种推送用 WebSocket 就没必要了,可以用 SSE 来做。


那连接断了怎么办呢?


不用担心,浏览器会自动重连。


这点和 WebSocket 不同,WebSocket 如果断开之后是需要手动重连的,而 SSE 不用。


再比如说日志的实时推送。


我们来测试下:


tail -f 命令可以实时看到文件的最新内容:



我们通过 child_process 模块的 exec 来执行这个命令,然后监听它的 stdout 输出:


const { exec } = require("child_process");

const childProcess = exec('tail -f ./log');

childProcess.stdout.on('data', (msg) => {
console.log(msg);
});

用 node 执行它:



然后添加一个 sse 的接口:


@Sse('stream2')
stream2() {
const childProcess = exec('tail -f ./log');

return new Observable((observer) => {
childProcess.stdout.on('data', (msg) => {
observer.next({ data: { msg: msg.toString() }});
})
});

监听到新的数据之后,把它返回给浏览器。


浏览器连接这个新接口:



测试下:



可以看到,浏览器收到了实时的日志。


很多构建日志都是通过 SSE 的方式实时推送的。


日志之类的只是文本,那如果是二进制数据呢?


二进制数据在 node 里是通过 Buffer 存储的。


const { readFileSync } = require("fs");

const buffer = readFileSync('./package.json');

console.log(buffer);


而 Buffer 有个 toJSON 方法:



这样不就可以通过 sse 的接口返回了么?


试一下:


@Sse('stream3')
stream3() {
return new Observable((observer) => {
const json = readFileSync('./package.json').toJSON();
observer.next({ data: { msg: json }});
});
}



确实可以。


也就是说,基于 sse,除了可以推送文本外,还可以推送任意二进制数据。


总结


服务端实时推送数据,除了用 WebSocket 外,还可以用 HTTP 的 Server Send Event。


只要 http 返回 Content-Type 为 text/event-stream 的 header,就可以通过 stream 的方式多次返回消息了。


它传输的是 json 格式的内容,可以用来传输文本或者二进制内容。


我们通过 Nest 实现了 sse 的接口,用 @Sse 装饰器标识方法,然后返回 Observe 对象就可以了。内部可以通过 observer.next 随时返回数据。


前端使用 EventSource 的 onmessage 来接收消息。


这个 api 的兼容性很好,除了 ie 外可以放心的用。


它的应用场景有很多,比如站内信、构建日志实时展示、chatgpt 的消息返回等。


再遇到需要消息推送的场景,不要直接 WebSocket 了,也许 Server Send Event 更合适呢?


作者:zxg_神说要有光
来源:juejin.cn/post/7272564663116759074
收起阅读 »

只会Vue的我,用两天学会了react,这个方法您也可以

web
背景 由于工作需要学习react框架;最开始看文档的时候感觉还挺难的。但当我看了半天文档以后才发现,原来react这样学才是最快的;前提是同学们会vue一类的框架哈。 该方法适用于会vue的同学们食用 我们在学习以前先去想一想,在vue中我们常用的方法是什么,...
继续阅读 »

背景


由于工作需要学习react框架;最开始看文档的时候感觉还挺难的。但当我看了半天文档以后才发现,原来react这样学才是最快的;前提是同学们会vue一类的框架哈。


该方法适用于会vue的同学们食用


我们在学习以前先去想一想,在vue中我们常用的方法是什么,我们遇到一些场景时在vue中是怎么做的。


当我们想到这儿的时候就会发现,对啊;既然vue是这样做的,那么react中是怎么做的呢?别急,我们一步一步对比着来。


这样岂不是更能理解哦!下面就让我们开始吧!


冲冲冲。。。


Vue梳理


在开始之前,我们先来梳理一下我们在vue中常用的API或者场景有哪些。


以下这几种就是我们常见的一些功能,主要是列表渲染、表单输入和一些计算属性等等;我们只需要根据原有的需要的功能去学习即可。



  • 组件传值

  • 获取DOM

  • 列表渲染

  • 条件渲染

  • class

  • 计算属性

  • 监听器

  • 表单输入

  • 模板


vue/react对比学习


组件传值


vue


// 父组件
<GoodsList v-if="!isGoodsIdShow" :goodsList="goodsList"/>
// 子组件 -- 通过props获取即可
props: {
goodsList:{
type:Array,
default:function(){
return []
}
}
}

react


// 父组件
export default function tab(props:any) {
const [serverUrl, setServerUrl] = useState<string | undefined>('https://');
console.log(props);
// 父组件接收子组件的值并修改
const changeMsg = (msg?:string) => {
setServerUrl(msg);
};

return(
<View className='tab'>
<View className='box'>
<TabName msg={serverUrl} changeMsg={changeMsg} />
</View>
</View>

)
}

// 子组件
function TabName(props){
console.log('props',props);
// 子传父
const handleClick = (msg:string) => {
props.changeMsg(msg);
};
return (
<View>
<Text>{props.msg}</Text>
<Button onClick={()=>{handleClick('77777')}}>测试</Button>
</View>

);
};

获取DOM


vue


this.$refs['ref']

react


// 声明ref    
const domRef = useRef<HTMLInputElement>(null);
// 通过点击事件选择input框
const handleBtnClick = ()=> {
domRef.current?.focus();
console.log(domRef,'domRef')
}

return(
<View className='home'>
<View className='box'>
<Input ref={domRef} type="text" />
<button onClick={handleBtnClick}>增加</button>
</View>
</View>

)

列表渲染


vue


<div v-for="(item, index) in mealList" :key="index">
{{item}}
</div>

react


//声明对象类型
type Coordinates = {
name:string,
age:number
};
// 对象
let [userState, setUserState] = useState<Coordinates>({ name: 'John', age: 30 });
// 数组
let [list, setList] = useState<Coordinates[]>([{ name: '李四', age: 30 }]);

// 如果你的 => 后面跟了一对花括号 { ,那你必须使用 return 来指定返回值!
const listItem = list.map((oi)=>{
return <View key={oi.age}>{oi.name}</View>
});

return (
{
list.map((oi)=>{
return <Text className='main-list-title' key={oi.age}>{oi.name}</Text>
})
}
<View>{ listItem }</View>
</View>
)

条件渲染


计算属性


vue


computed: {
userinfo() {
return this.$store.state.userinfo;
},
},

react


const [serverUrl, setServerUrl] = useState('https://localhost:1234');
let [age, setAge] = useState(2);

const name = useMemo(() => {
return serverUrl + " " + age;
}, [serverUrl]);
console.log(name) // https://localhost:1234 2

监听器


vue


watch: {
// 保证自定义菜单始终显示在页面中
customContextmenuTop(top) {
...相关操作
}
},

react


import { useEffect, useState } from 'react';

export default function home() {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
const [age, setAge] = useState(2);

/**
* useEffect第二个参数中所传递的值才会进行根据值的变化而出发;
* 如果没有穿值的话,就不会监听数据变化
*/

useEffect(()=>{
if (age !== 5) {
setAge(++age)
}
},[age])

useEffect(()=>{
if(serverUrl !== 'w3c') {
setServerUrl('w3c');
}
},[serverUrl])

return(78)
}

总结


从上面的方法示例我们可以得出一个结论:在其他框架(自己会的)中常用到的方法或者场景进行针对性的学习即可。


这样的好处是你能快速的上手开发,然后在实际开发场景中遇到解决不了的问题再去查文档或者百度。


这只是我的一点小小的发现,哈哈哈。。。


如果对你有感触的话,可以尝试一下这个方法;我觉得还是很不错的


注意:react推荐函数式组件开发,不推荐类组件开发,我在上面没有说明,大家也可以去文档看看,类组件和函数组件还是有很大差别的,如:函数组件没有生命周期,一般使用监听来完成的,监听的使用方法还是有所不同,大家可以具体的去试试,我在这儿也是告诉大家一些方法;具体去学了才是你的。


为了方便自己学习记录,以及给大家提供思路,我下期给大家带来 vite + ts + react的搭建


作者:雾恋
来源:juejin.cn/post/7268844150233219107
收起阅读 »

看不了电视直播了?那就自己做一个(一)

web
事情的起因是这样的,前两天打开电视看直播,突然变成了下面这个画面。 开始以为是太久没更新了,想着重新安装一下就好了。结果上网一查才知道,电视直播软件都下架了。 电视直播只能安装有线,或者通过央视频手机收看了。有线暂时安装不了,用手机看又总是感觉很别扭。 虽...
继续阅读 »

事情的起因是这样的,前两天打开电视看直播,突然变成了下面这个画面。


j29zRYWy.jpeg


开始以为是太久没更新了,想着重新安装一下就好了。结果上网一查才知道,电视直播软件都下架了。


电视直播只能安装有线,或者通过央视频手机收看了。有线暂时安装不了,用手机看又总是感觉很别扭。


a71ea8d3fd1f4134205611b2199935ccd0c85e21.png


虽然也可以通过投屏的方式用电视播放,但切换频道时还得使用手机操作,非常的麻烦。


广电的这波操作出发点可能是好的,后续应该会提供其他收看方式,但是目前这个真空期着实有点尴尬。思来想去,干脆自己动手做一个吧


就是干.jpg


经过一个周末的折腾,过程虽然有一点点曲折,但总算是完成了第一个电视版本,感觉和以前相比,清晰度还不错,切换也更流畅。


111.gif


再来看一下卫视。


也是OK的


22 (1).gif


电视端有了,又突然想着把它放到手机上,虽然手机上可以直接使用央视频播放,但还是有点繁琐,于是又稍微做了一些调整,推出了一个手机端的版本,切换还是相当的丝滑。


手机.gif


最后我其实还改了一版在电脑上使用的,但是这个除了摸鱼好像也没别的用处,所以对于我来说意义不大。


实现篇


接下来说一下具体的实现,会涉及到一些编程相关的内容,如果不感兴趣可以直接跳到结尾。


客户端应用开发


这一篇先介绍客户端的应用的开发,主要就是安卓应用的开发。虽然以前没有这方面经验,但是想法有了,剩下的交给chatGpt就好了。


1. 播放器


首先是播放器的选择,一开始我采用了原生MediaPlayer,主要是考虑到跟各版本安卓系统的兼容性会好一点,而且它使用起来非常的简单,十几行代码就搞定了。


public class PlayActivity extends Activity {
ChannelService channelService;
VideoView videoView;

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
channelService = new HttpChannelService();
setContentView(R.layout.activity_play);
videoView = findViewById(R.id.video_view);
channelService.loadChannels((success, message) -> runOnUiThread(() -> {
Channel channel = channelService.getDefaultChannel();
videoView.setVideoURI(Uri.parse(channel.getSource()));
videoView.start();
}));
}
}

后来替换成了谷歌开源的ExoPlayer,因为只是简单使用,所以代码基本上也没什么差别。


public class PlayActivity extends Activity {
ChannelService channelService;
private StyledPlayerView videoView;
private ExoPlayer player;

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
initView();
initChannels();
}

private void initView() {
setContentView(R.layout.activity_play);
videoView = findViewById(R.id.video_view);
player = new ExoPlayer.Builder(this).build();
videoView.setPlayer(player);
}

private void initChannels() {
channelService = new HttpChannelService();
channelService.loadChannels((boolean success, String message) -> runOnUiThread(() -> {
ChannelService.Channel channel = channelService.getDefaultChannel();
play(channel);
}));
}

private void play(Channel channel){
player.setMediaItem(MediaItem.fromUri(channel.getSource()));
player.prepare();
player.setPlayWhenReady(true);
}
}

2. 监听器


接下来就是考虑对遥控器按键的监听处理,对于这个应用而言,只需要监控方向键以及退出键就好了,当然也可以根据需要对菜单键或者确定键进行响应。


videoView.setOnKeyListener((view, keyCode, event) -> {
switch (keyCode) {
// 向下操作处理 切换下一个频道
case KeyEvent.KEYCODE_DPAD_DOWN:
if (event.getAction() == KeyEvent.ACTION_DOWN) {
Channel channel = channelService.getNextChannel();
play(channel);
return true;
}
break;
}
return false;
});

3. 视频源管理


把ChannelService留到最后讲,是因为它的作用通过下面的接口定义就一目了然了。


这里之所以要定义成接口,比如常见的通过m3u获取,是因为考虑到视频源可能有不同实现,关于实现部分会在下一篇详细讲解。


public interface ChannelService {

/**
* 加载频道
*/

void loadChannels(LoadCallBack callBack);

/**
* 获取默认频道
*/

Channel getDefaultChannel();

/**
* 获取下一个频道
*/

Channel getNextChannel();

/**
* 获取前一个频道
*/

Channel getPrevChannel();

}

4. 手机版


手机版因为没有了遥控器,所以需要对触屏动作进行监听来对视频进行操控,主要就是左右滑动的切换,以及上下滑动的音量调节等。


结语


因为是即兴的创作,也没有打算能长久使用,所以很多细节我并没有考虑,比如内容的缓存,节目回看,网络监控等等这些。但是这几天使用下来体验还是挺不错的。后续我可以把整个源码开放出来,大家有兴趣可以自行去补充。


到这里客户端的实现就讲完了,下一篇再讲一下其他部分的实现。


作者:双子小匠
来源:juejin.cn/post/7311961893610995748
收起阅读 »

带圆角的虚线边框?CSS 不在话下

今天,我们来看这么一个非常常见的切图场景,我们需要一个带圆角的虚线边框,像是这样: 这个我们使用 CSS 还是可以轻松解决的,代码也很简单,核心代码: div { border-radius: 25px; border: 2px dashed...
继续阅读 »

今天,我们来看这么一个非常常见的切图场景,我们需要一个带圆角的虚线边框,像是这样:



这个我们使用 CSS 还是可以轻松解决的,代码也很简单,核心代码:


div {
border-radius: 25px;
border: 2px dashed #aaa;
}

但是,原生的 dashed 有一个问题,就是我们无法控制虚线的单段长度与间隙


假设,我们要这么一个效果呢虚线效果呢:



此时,由于无法控制 border: 2px dashed #aaa 产生的虚线的单段长度与线段之间的间隙,border 方案就不再适用了。


那么,在 CSS 中,我们还有其它方式能够实现带圆角,且虚线的单段长度与线段之间间隙可控的方式吗?


本文,我们就一起探讨探讨。


实现不带圆角的虚线效果


上面的场景,使用 CSS 实现起来比较麻烦的地方在于,图形有一个 border-radius


如果不带圆角,我们可以使用渐变,很容易的模拟虚线效果。


我们可以使用线性渐变,轻松的模拟虚线的效果:


div {
width: 150px;
height: 100px;
background: linear-gradient(90deg, #333 50%, transparent 0) repeat-x;
background-size: 4px 1px;
background-position: 0 0;
}

看看,使用渐变模拟的虚线如下:



解释一下上面的代码:



  1. linear-gradient(90deg, #333 50%, transparent 0),实现一段渐变内容,100% - 50% 的内容是 #333 颜色,剩下的一半 50% - 0 的颜色是透明色 transprent

  2. repeat-x 表示只在 x 方向重复

  3. background-size: 4px 1px 表示上述渐变内容的长宽分别是 4px\ 1px,这样配合 repeat-x就能实现只有 X 方向的重复

  4. 最后的 background-position: 0 0 控制渐变的定位


因此,我们只需要修改 background 的参数,就可以得到各种不一样的虚线效果:



完整的代码,你可以戳这里:CodePen Demo -- Linear-gradient Dashed Effect


并且,渐变是支持多重渐变的,因此,我们把容器的 4 个边都用渐变表示即可:


div {
background:
linear-gradient(90deg, #333 50%, transparent 0) repeat-x,
linear-gradient(90deg, #333 50%, transparent 0) repeat-x,
linear-gradient(0deg, #333 50%, transparent 0) repeat-y,
linear-gradient(0deg, #333 50%, transparent 0) repeat-y;
background-size: 4px 1px, 4px 1px, 1px 4px, 1px 4px;
background-position: 0 0, 0 100%, 0 0, 100% 0;
}

效果如下:



但是,如果要求的元素带 border-radius 圆角,这个方法就不好使了,整个效果就会穿帮。


因此,在有圆角的情况下,我们就需要另辟蹊径。


利用渐变实现带圆角的虚线效果


当然,本质上我们还是需要借助渐变效果,只是,我们需要转换一下思路。


譬如,我们可以使用角向渐变。


假设,我们有这么一个带圆角的元素:


<div>div>

div {
width: 300px;
height: 200px;
background: #eee;
border-radius: 20px;
}

效果如下:



如果我们修改内部的 background: #eee,把它替换成重复角向渐变的这么一个图形:


div {
//...
- background: #eee;
+ background: repeating-conic-gradient(#000, #000 3deg, transparent 3deg, transparent 6deg);
}

解释一下,这段代码创建了一个重复的角向渐变背景,从黑色(#000)开始,每 3deg 变为透明,然后再从透明到黑色,以此循环重复。


此时,这样的背景效果可用于创建一种渐变黑色到透明的重复纹理效果:



在这个基础上,我们只需要给这个图形上层,再利用伪元素,叠加一层颜色,就得到了我们想要的边框效果,并且,边框间隙和大小可以简单调整。


完整的代码:


div {
position: relative;
width: 300px;
height: 200px;
border-radius: 20px;
background: repeating-conic-gradient(#000, #000 3deg, transparent 3deg, transparent 6deg);

&::before {
content: "";
position: absolute;
inset: 1px;
background: #eee;
border-radius: 20px;
}
}

效果如下:



乍一看,效果还不错。但是如果仔细观察,会发现有一个致命问题:虚线线段的每一截长度不一致


只有当图形的高宽一致时,线段长度才会一致。高宽比越远离 1,差异则越大:



完整的代码,你可以戳这里:CodePen Demo -- BorderRadius Dashed Border


那有没有办法让虚线长度能够保持一样呢?


可以!我们再换一种渐变,我们改造一下底下的角向渐变,重新利用重复线性渐变:


div {
border-radius: 20px;
background:
repeating-linear-gradient(
-45deg,
#000 0,
#000 7px,
transparent 7px,
transparent 10px
);
}

此时,我们能得到这样一个斜 45° 的重复线性渐变图形:



与上面方法一类似,再通过在这个图形的基础上,在元素中心,叠加多一层纯色遮罩图形,只漏出最外围一圈的图形,带圆角的虚线边框就实现了:



此方法比上面第一种渐变方法更好之处在于,虚线每一条线段的长度是固定的!是不是非常的巧妙?


完整的代码,你可以戳这里:CodePen Demo -- BorderRadius Dashed Border


最佳解决方案:SVG


当然,上面使用 CSS 实现带圆角的虚线边框,还是需要一定的 CSS 功底。


并且,不管是哪个方法,都存在一定的瑕疵。譬如如果希望边框中间不是背景色,而是镂空的,上述两种 CSS 方式都将不再使用。


因此,对于带圆角的虚线边框场景,最佳方式一定是 SVG。(切图也算是吧,但是灵活度太低)


只是很多人看到 SVG 会天然的感到抗拒,或者认为 SVG 不太好掌握。


所以,本文再介绍一个非常有用的开源工具 -- Customize your CSS Border



通过这个开源工具,我们可以快速生成我们想要的虚线边框效果,并且一键复制可以嵌入到 CSS background 中的 SVG 代码图片格式。


图形的大小、边框的粗细、虚线的线宽与间距,圆角大小统统是可以可视化调整的。


通过一个动图,简单感受一下:



总结一下


本文介绍了 2 种在 CSS 中,不借助切图和 SVG 实现带圆角的虚线边框的方式:



  1. 重复角向渐变叠加遮罩层

  2. 重复线性渐变叠加遮罩层


当然,两种 CSS 方式都存在一定瑕疵,但是对于一些简单场景是能够 Cover 住的。


最后,介绍了借助 SVG 工具 Customize your CSS Border 快速生成带圆角的虚线边框的方式。将 SVG 生成的矢量图像数据直接嵌入到 background URL 中,能够应付几乎所有场景,相对而言是更好的选择。


最后


好了,本文到此结束,希望本文对你有所帮助 :)


作者:Chokcoco
来源:juejin.cn/post/7311681326712487999
收起阅读 »

前端打包后,静态文件的名字为什么是一串Hash值?

web
引言 前段时间公司需要招聘几个初级前端,面试过程中,问了这么一个问题:“项目打包后的dist文件夹中,比如js、css这些文件的名称为什么是hash值的,就是一串无规则的字符串”。基本上都不知道静态文件为什么需要这种无规则的hash值,今天就稍微说一下哈。 静...
继续阅读 »

引言


前段时间公司需要招聘几个初级前端,面试过程中,问了这么一个问题:“项目打包后的dist文件夹中,比如js、css这些文件的名称为什么是hash值的,就是一串无规则的字符串”。基本上都不知道静态文件为什么需要这种无规则的hash值,今天就稍微说一下哈。


静态文件何时被加载


拿常用的单页面应用举例,当我们访问一个网站的时候,最终会指向 index.html 这个文件,也就是打包后的 dist 文件夹中的 index.html


比如说 https://some-domain.com/home, 并点击回车键,我们的服务器中实际上没有这个路由,但我们不是返回 404,而是返回我们的 index.html。为什么地址中我们没有输入index.html这个路径,但还是指向到 index.html文件并加载它?因为现在大多数都用nginx去部署,一般在url中输入地址的时候末尾都会加个 “/” ,nginx中已经把“/”重定向到 index.html文件了


image.png


此时按下回车,这个 index.html 文件就被加载获取到了,然后开始自上而下的去加载里面的引用和代码,比如在html中引入的css、js、图片文件。


浏览器默认缓存


当用户按下回车键就向目标服务器去请求index.html文件,加载解析index.html文件的同时就会连带着加载里面的js、css文件。有没有想过,用户第一次已经从服务器请求下载静态文件到客户端了,第二次去浏览该网站该不会还让我去向服务器请求吧,不会吧不会吧,如果每次都请求下载,那用户的体验多不好,每次请求都需要时间,不说服务器的压力会增加,最重要的是用户的体验感,展现到用户眼前的时间会增加!


所以说浏览器已经想到这个了,当请求静态资源的时候,就会默认缓存请求过来的静态文件,这种浏览器自带的默认缓存就叫做 启发式缓存 。 除非手动设置不需要缓存no-store,此时请求静态文件的时候文件才不会缓存到本地!


浏览器默认缓存详情可见 MDN 启发式缓存。不管什么缓存都有缓存的时效性吧,如果想知道 启发式缓存 到底把静态文件缓存了多久,可以阅读笔者的这篇文章 浏览器的启发式缓存到底缓存了多久?


vue-cli里的默认配置,css和js的名字都加了哈希值,所以新版本css、js和就旧版本的名字是不同的,不会有缓存问题。


Hash值的作用


那既然知道了浏览器会有默认的缓存,当加载静态资源的时候会命中启发式缓存并缓存到本地。那如果我们重新部署前端包的时候,如何去请求新的静态资源呢,而不是缓存的静态资源?这时候就得用到hash值了


下面模拟了掘金网站的静态资源获取,当请求静态资源的时候,实际访问的是服务器中静态资源存放的位置
image.png


返回即是当前请求静态资源的具体内容
image.png


第一次进来的时候会去请求服务器的资源缓存到本地,比如 0dbcf63.js 这个文件就被缓存到本地了,后面再正常刷新就直接获取的是缓存中的资源了(disk cache 内存缓存)。


如果前端包重新部署后,试想一下如果 0dbcf63.js这个js文件不叫这个无规则的名字,而是固定的名字,那浏览器怎么知道去请求缓存中的还是服务器中的,所以浏览器的机制就是请求缓存中的,除非缓存中的过期了,才会去请求服务器中的静态资源。如果没有请求到最新的资源,页面也不会更新到最新的内容,影响用户的体验。


那浏览器这个机制是怎么判断的,那就是根据资源的名称,该资源的资源名称如果没变 并且 没有设置不缓存 并且 资源没过期,那就会请求缓存中的资源,否则就会请求服务器中的资源


image.png



当静态资源的名称发生变化的时候,请求路径就会发生变化,所以就会重新命中服务器新部署的静态资源!这就是为什么需要hash值的原因,为了区分之前部署的文件和这次部署文件的区别,好让浏览器去重新获取最新的资源。



第三方库


由于像 lodash 或 react 这样的第三方库很少像本地源代码一样频繁修改,因此通常推荐将第三方库提取到单独的 chunk-vendor 中


 const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
entry: './src/index.js',
plugins: [
new HtmlWebpackPlugin({
title: 'Caching',
}),
],
output: {
filename: '[name].[contenthash].js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
optimization: {
moduleIds: 'deterministic',
runtimeChunk: 'single',
splitChunks: {
cacheGr0ups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
},
},
},
},
};

这样依赖的静态文件就会打包到chunk-vendor中,并且多次打包不会改变文件的hash值,以上是webpack原生的配置,如果使用的vue脚手架,那么脚手架已经都配置好了。


image.png


作者:Lakeiedward
来源:juejin.cn/post/7311219067199881254
收起阅读 »

三行代码实现完美瀑布流

web
需求 最近准备做一个瀑布流的需求,这里每个卡片的高度是不固定的,有点类似下图这样的。 难点 如果绝对定位,如何定位每个卡片的位置。 因为可以发现需求里边的每个卡片的高度都是不固定的,所以如果想使用绝对定位的话,需要实时动态的计算每个卡片的left、top,...
继续阅读 »

需求


最近准备做一个瀑布流的需求,这里每个卡片的高度是不固定的,有点类似下图这样的。



难点



  1. 如果绝对定位,如何定位每个卡片的位置。
    因为可以发现需求里边的每个卡片的高度都是不固定的,所以如果想使用绝对定位的话,需要实时动态的计算每个卡片的left、top,这里会涉及大量的计算。


因为每个卡片的高度是不固定,所以如果想要计算left、top,必须首先获取卡片的高度,但是卡片里边不仅包含图片,还有文字,这个时候计算高度是比较困难的。




  1. 如何结合虚拟列表实现瀑布流


这个时候必须要根据scrollTop的位置,判断什么时候需要加载哪些数据,判断可视区域里边数据的起始索引以及结束索引,这里同样会涉及大量的计算,同时还因为每个卡片的高度不固定,甚至只有图片和文字加载到浏览器以后,才能得到真实的高度,这样会更困难。


解决方案


解决方案1



  1. 如果绝对定位,如何定位每个卡片的位置。


1.1 后端计算
后端可以先把每个图片的高度和宽度提前计算好,直接返回给前端进行处理,然后前端根据后端返回的图片高度和宽度,然后再动态的计算出每个卡片的高度(文字部分也可以固定高度,使用省略号实现)。


1.2 前端计算


前端计算还是比较麻烦的,需要先等卡片组件加载完成,才能得到宽度和高度,而且因为数据量比较大,每个卡片计算出来以后,还需要去根据计算出来的结果去更新left、top,会非常麻烦。
这里可以采用node作为中间层进行计算,还是使用类似后端计算的思路。


还有一种方法是使用observe api 动态观察每个卡片,当观察到卡片加载完成后,再动态根据卡片的宽度和高度计算,不过这样同样很麻烦。



  1. 如何结合虚拟列表实现瀑布流


这里因为卡片的高度是不固定的,同时也是瀑布流,所以不能使用react-window 来解决,不过可以使用react-window的类似思路,自己封装一个npm 包,根据scroll事件判断需要加载那些数据。


解决方案2


使用css3的columns来实现,该技术解决方案不需要计算高度,也不需要去定位,但是columns这个属性会把卡片高度给切开
如下图:



不过可以使用下面代码来解决,


js
复制代码

.test {
// color: red;
// height: 2000px;
background-color: red;
gap: 1rem;
columns: 5;
.no-break {
break-inside: avoid;
}
}

效果如下:



作者:邹小邹
链接:https://juejin.cn/post/7273890921506553890
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

被中文输入法坑死了

web
PM:在PC端做一个@功能吧,就是那种...。 我:你不用解释🤔我知道那个功能,监听keydown事件,然后e.keycode === 50,那可太简单了。 那可太简单了,可太简单了,太简单了,简单了,单了,了......(掉进坑里的回声) 坑1:KeyB...
继续阅读 »

PM:在PC端做一个@功能吧,就是那种...。



我:你不用解释🤔我知道那个功能,监听keydown事件,然后e.keycode === 50,那可太简单了。



那可太简单了,可太简单了,太简单了,简单了,单了,了......(掉进坑里的回声)


坑1:KeyBoardEvent.keycode



废弃的属性你就坚持用吧,一用一个不吱声。以后线上跑得好好的代码突然报错了,你都不知道bug在哪儿。


现在的web标准里,要确定一个键盘事件是靠e.keye.codecode代表触发事件的物理按键,比如2的位置code='Digit2'key返回用户按下的物理按键的值,它还与 shiftKey 等调节性按键的状态、键盘的区域和布局有关。


所以对于@来说,直接判断e.key === "@"来做后续的操作就行了。


addEventListner('keydown', (e) => {
if (e.key === "@") {
e.preventDefalut(); // 这里去掉@字符是为了后续插入和监听方便处理
// 插入有@字符的,可监听输入的元素
// 唤起小窗....
}
});


仔细看上面的这几行代码和注释,要开始考(坑)了。


坑2:输入法的坑


起因


在我美滋滋地以为避过了坑1就没事了的时候,一个夜晚我的测试同学告诉我,在测试环境突然就体验不到这个功能了,无论输入多少个@都不行,白天还好好的🤯。


好一个「白天还好好的」。


我自己测试的时候又百分百能体验到🤔,所以最开始我还在怀疑他没有配上测试系统......


于是,让测试同学的windows电脑连到我的开发环境debug一看:


好家伙,真是好家伙😅他的电脑的e.key === "Process"????!!!


什么意思呢,就是正常我们理想中的@字符产生是shift+2按键的组合,监听keydown之后我们会按顺序收到两个回调:



  1. e.key === "Shift"e.code === "ShiftLeft"或者shiftRight

  2. e.key === "@"e.code === "Digit2"


但是实际在测试同学的电脑里,1是一样的,但是2变了,2变成了e.key === "Process"


虽然键盘事件有变化,但是在前端页面上的@字符是没有任何变化的。难怪他说他会突然失效了。我问他做了什么怎么会突然变了,他想了想说晚上从系统输入法换成了微信输入法.....


上网检索(chatGPT)了一番,明白了一个新的知识点:


输入法的全称叫Input Method Editor输入法编辑器(IME)。本质上它也是个编辑器。为了能输入各类字符(比如汉字和阿拉伯字等),IME会先处理用户的英文字母输入然后通过系统的底层调用传递给浏览器,浏览器再显示到用户的界面。这里的Process很大概率就是当时输入法给出的某个值表示那个时刻它还在处理中。


解决办法


既然KeyBoardEvent靠不住,那我们换一种监听方式。


我找到了一个非常适用于输入法的监听事件叫做CompositionEvent,它表示用户间接输入文本(如使用输入法)时发生的事件。此接口的常用事件有compositionstart, compositionupdatecompositionend。它们三个事件分别对应的动作,通俗一点说就是你用输入法开始打字、正在打字和结束打字。


于是乎,我监听compositionend不就行了!在输入法end的时候我再去看你end的字符是不是@不就行了!


// addEventListner('keydown', (e) => {
addEventListner('compositionend', (e) => {
// if (e.key === "@") {
if (e.data === "@") {
e.preventDefalut(); // 这里去掉@字符是为了后续插入和监听方便处理
// 插入有@字符并且可监听输入的元素
// 唤起小窗....
}
});

对于输入法来说,按键的up和down的key值就算不尽人意也没什么损失,毕竟用户毫无感知。但是,compositionend永远是不会错的,如果compositionende.data都不是@字符了,那么在用户的编辑器界面上的显示肯定也会跟着出错。


所以监听这个肯定就是万无一失的方法了,哈哈哈我真是个“天才”(蠢材)。
修改之后让测试同学尝试之后果然就可以了。


坑3:输入法继续坑


起因


时间过去了没一会,本天才就收到了另一个测试同学反馈的问题说为什么输入了一个@字符之后,会出现两个@在界面上?


我第一反应就是难道没有执行到e.preventDefalut()?既然后续功能能正常使用,没执行到也不应该啊🤔。然后在我电脑一通尝试,发现safari浏览器在输入法为中文的情况下也会触发这个问题。


于是,让测试同学的windows电脑连到我的开发环境debug一看(梅开二度):


执行到了,也没有报错什么的,但是@字符并没有被prevent掉🤯。


再加上我自己传入的@,所以界面上就出现了两个@字符。啊这这这,这很难评......


ceeb653ely8gzozgvjq1cg20j20hhgvb.gif


我是左思右想,百思不得其解,于是只能:



stack overflow上也有这个问题


上面大概的意思就是compositionend事件里使用 e.preventDefault() 在技术上可行的,但它可能不会产生你期望的效果。可能是因为 compositionend 事件标志着一次输入构成(composition)会话的结束,而在这个点上阻止默认行为可能没有意义,因为输入的流程已经完成了。


更推荐用keydown,compositionstartinput来处理这种情况。


keydown是不可能keydown了,已经被坑了。compositionstart也不行,因为刚开始输入那会才按下了shift键,@字符还没出来呢。那就只能input了。


解决办法


最开始我没有选择input就是因为它不能使用e.preventDefault()。我必须要对输入的字符串进行单独处理,去掉@,当时觉得很麻烦就没有选择这个方法。


额....好好好,行行行,现在还是必须得处理一下了。


// addEventListner('keydown', (e) => {
// addEventListner('compositionend', (e) => {
addEventListner('input', (e) => {
// if (e.key === "@") {
if (e.data === "@") {
?怎么去处理字符呢 // 这里去掉@字符是为了后续插入和监听方便处理
// 插入有@字符并且可监听输入的元素
// 唤起小窗....
}
});

对于这个处理字符的方法,也是一个新知识点了。起初我还想的是去处理编辑器里的content,然后再给它插入回去,这样子复杂度很高并且出错的概率极大。


这里的解决办法主要是使用CharacterData接口。CharacterData 接口是操作那些包含字符数据的节点的核心,特别是在需要动态地更改文本内容时。


例如,在一个文本节点上使用 deleteData() 方法可以从文本中移除一部分内容,而不必完全替换或重写整个节点


const selection = window.getSelection();
const { anchorNode, anchorOffset } = selection;
if (anchorNode.substringData(anchorOffset - 1, 1) === '@') {
selection.anchorNode.deleteData(anchorOffset - 1, 1);
}

写完这个之后,我用自己的safari浏览器测试发现果然没有问题了。


哈哈哈我真是个“天才”(蠢材)。


坑4:输入法深坑🕳️


我自信满满地让测试同学再重试一下😎,然后测试同学说:和之前一样啊,还是有两个@字符。


我:啊?啊啊??啊啊啊???


IMG_6547.jpg


于是,让测试同学的windows电脑连到我的开发环境debug一看(梅开三度):


发现测试同学电脑上的anchorOffset和正常的情况下是不一样的,会小一位,所以导致anchorOffset - 1拿到的前一个字符并不等于@,所以后续也没有把它处理掉🤯。


我是左思右想,百思不得其解,stack overflow上也没有相关的问题。不过,结合IME的概念,肯定还是输入法的问题。


结合之前keydown的e.key==="Processing",可能在input触发时输入法的编辑器其实还是没有完成工作(composition),导致在那个时候SelectionanchorOffset不一致。其实浏览器的Selection肯定不会错,那anchorOffset看起来像是错了,我觉得应该是输入法在转换的过程对我们的前端页面做了一些用户看不到的东西,而anchorOffset把它显化出来罢了。


解决办法


于是乎,我尝试性的对处理字符串的那串代码进行延时,目的是为了等待输入法彻底工作完毕。


// addEventListner('keydown', (e) => {
// addEventListner('compositionend', (e) => {
addEventListner('input', (e) => {
// if (e.key === "@") {
if (e.data === "@") {
setTimeout(() => {
const selection = window.getSelection();
const { anchorNode, anchorOffset } = selection;
if (anchorNode.substringData(anchorOffset - 1, 1) === '@') {
selection.anchorNode.deleteData(anchorOffset - 1, 1);
} // 这里去掉@字符是为了后续插入和监听方便处理
});

// 插入有@字符并且可监听输入的元素
// 唤起小窗....
}
});

然后,问题真的就彻底解决了。


这个功能做起来可太简单了......😅


作者:Liqiuyue
来源:juejin.cn/post/7307041255740981286
收起阅读 »

使用flex实现瀑布流

web
什么是瀑布流 瀑布流,又称瀑布流式布局。是比较流行的一种网站页面布局,视觉表现为参差不齐的多栏布局,随着页面滚动条向下滚动,这种布局还会不断加载数据块并附加至当前尾部。 特点: 固定宽度,高度不一 参差不齐的布局 使用flex实现瀑布流 实现的效果是分成两...
继续阅读 »

什么是瀑布流


瀑布流,又称瀑布流式布局。是比较流行的一种网站页面布局,视觉表现为参差不齐的多栏布局,随着页面滚动条向下滚动,这种布局还会不断加载数据块并附加至当前尾部。


特点:



  • 固定宽度,高度不一

  • 参差不齐的布局


使用flex实现瀑布流


实现的效果是分成两列的瀑布流,往下滑动会加载下一页的数据,并渲染到页面中!


微信图片_20230731231617.jpg


样式的实现


<view class="blessing-con">
<view class='blessing-con-half'>
<view id="leftHalf">
<view class="blessing-con-half-item" :class="bgColor[index % 3]"
v-for="(item, index) in newBlessingWordsList1" :key="index">
<view class="item-con">
</view>
</view>
</view>
</view>
<view class='blessing-con-half'>
<view id="rightHalf">
<view class="blessing-con-half-item" :class="bgColor[(index + 1) % 3]"
v-for="(item, index) in newBlessingWordsList2" :key="index">
<view class="item-con"></view>
</view>
</view>
</view>
</view>
<view class="blessing-more" @click="handlerMore">
<image v-if="hasWallNext" class="more-icon"
src="xx/blessingGame/arr-down.png">
</image>
<view class="blessing-more-text">{{ blessingStatus }}</view>
</view>

.blessing-con 定义外层容器为flex布局,同时设置主轴对齐方式为space-between


.blessing-con-half定义左右两侧的容器的样式


.blessing-con-half-item定义每一个小盒子的样式


.blessing-con {
padding: 32rpx 20rpx;
display: flex;
justify-content: space-between;
height: 1100rpx;
overflow-y: auto;
.blessing-con-half {
width: 320rpx;
height: 100%;
box-sizing: border-box;
.blessing-con-half-item {
width: 100%;
height: auto;
display: flex;
flex-direction: column;
box-sizing: border-box;
margin: 0 0 24rpx;
position: relative;
}
}
}

这里每个小盒子的背景色按蓝-黄-红的顺序,同时通过伪类给盒子顶部添加锯齿图片,实现锯齿效果


bgColor: ['blueCol', 'yellowCol', 'pinkCol'], //祝福墙背景

// 不同颜色
.blessing-con-half-item {
&.pinkCol {
&::before {
.setbg(320rpx, 16rpx, 'blessingGame/pink-bg.png');
}
.item-con {
background: #FFE7DF;
}
}

&.yellowCol {
&::before {
.setbg(320rpx, 16rpx, 'blessingGame/orange-bg.png');
}
.item-con {
background: #fff0e0;
}
}

&.blueCol {
&::before {
.setbg(320rpx, 16rpx, 'blessingGame/blue-bg.png');
}
.item-con {
background: #e0f7ff;
}
}
}
}

功能实现


在data中定义两个数组存储左右列表的数据


data(){
return{
blessingWordsList: [],// 祝福墙数据
newBlessingWordsList: [],//已添加的数据
newBlessingWordsList1: [],//左列表
newBlessingWordsList2: [],//右列表
isloading:false,//是否正在加载
hasWallNext:false,//是否有下一页
leftHeight: 0,//左高度
rightHeight: 0,//右高度
blessingWordsCount: 0,//计数器
isActive: 0, //tab初始化索引
timer:null,//定时器
}
}

调取接口请求列表数据



  • 第一次请求数据需要初始化列表数据和计数器

  • 每一次请求完都需要开启定时器


// 获取祝福墙列表(type=1则请求下一页)
async getBlessingWall(type = 0) {
try {
let res = await api.blessingWall({
activityId: this.activityId,
pageNum: this.pageWallNum,
pageSize: this.pageWallSize
})
this.isloading = false
if (res.code == 1 && res.rows) {
let list = res.rows
this.blessingWordsList = (type==0 ? list : [...this.blessingWordsList, ...list])
if (this.blessingWordsList.length > 0 && !this.timer && this.isActive == 1) {
if(this.pageWallNum == 1){
this.newBlessingWordsList = []
this.newBlessingWordsList1 = []
this.newBlessingWordsList2 = []
this.blessingWordsCount = 0
}
this.start()
}
// 处理请求下一页的情况
if (type == 1) {
this.start()
}
this.hasWallNext = res.hasNext
if (!this.hasWallNext) {
this.blessingStatus = "没有更多了哟"
} else {
this.blessingStatus = "点击加载更多"
}
}
} catch (error) {
console.log(error)
}
},
// 加载更多
async handlerMore() {
if (this.hasWallNext && !this.isloading) {
this.isloading = true
this.pageWallNum++
await this.getBlessingWall(1)
}
},

开启一个定时器,用于动态添加左右列表的数据


start() {
// 清除定时器
clearInterval(this.timer)
this.timer = null;

this.timer = setInterval(() => {
let len = this.blessingWordsList.length
if (this.blessingWordsCount < len) {
let isHave = false
// 在列表中获取一个元素
let item =this.blessingWordsList[this.blessingWordsCount]
// 判断新列表中是否已经存在相同元素,防止重复添加
this.newBlessingWordsList.forEach((tmp)=>{
if(tmp.id == item.id){
isHave = true
}
})
// 如果不存在
if (!isHave) {
this.newBlessingWordsList.push(item)//添加该元素
this.$nextTick(() => {
this.getHei(item)//添加元素到左右列表
})
}
} else {
// 遍历完列表中的数据,则清除定时器
clearInterval(this.timer)
this.timer = null;
}
}, 10)
}

计算当前左右容器的高度,判断数据要添加到哪一边



  • 使用uni-app的方法获取左右容器的dom对象,再获取他们当前的高度

  • 比较左右高度,向两个数组动态插入数据

  • 每插入一条数据,计数器+1


getHei(item) {
const query = uni.createSelectorQuery().in(this)
// 左边
query.select('#leftHalf').boundingClientRect(res => {
if (res) {
this.leftHeight = res.height
}
// 右边
const query1 = uni.createSelectorQuery().in(this)
query1.select('#rightHalf').boundingClientRect(dataRight => {
if (dataRight) {
this.rightHeight = dataRight.height != 0 ? dataRight.height : 0
if (this.leftHeight == this.rightHeight || this.leftHeight < this.rightHeight) {
// 相等 || 左边小
this.newBlessingWordsList1.push(item)
} else {
// 右边小
this.newBlessingWordsList2.push(item)
}
}
this.blessingWordsCount++
}).exec()
}).exec()
},

这里有一个注意点,调用start方法的时候,必须确保页面渲染了左右容器的元素,否则会拿不到容器的高度


比如我这个项目是有tab切换的!


微信图片_20230731231616.jpg
进入页面的时候会请求一次数据,这时候因为tab初始状态在0,所以并不会调用start方法,要到切换tab到1时,才会调用start方法开始计算高度。


data(){
return{
isActive: 0, //tab初始化索引
timer:null,//定时器
}
}
async onLoad(options) {
this.getBlessingWall()
}
// tab选项卡切换
tabClick(index) {
this.isActive = index
this.isLoaded = false;
if (this.blessingWordsList.length > 0 && !this.timer && this.isActive == 1) {
if(this.pageWallNum == 1){
this.newBlessingWordsList = []
this.newBlessingWordsList1 = []
this.newBlessingWordsList2 = []
this.blessingWordsCount = 0
}
this.start()
}
},

最后


这次选用了flex实现瀑布流,实现瀑布流的方式还有其他几种方法,后续有机会的话,我会补充其他几种方式,如果感兴趣的话,可以点点关注哦!


作者:藤原豆腐店
来源:juejin.cn/post/7260713996165021754
收起阅读 »

前端同事最讨厌的后端行为,看看你中了没有

web
前端同事最讨厌的后端行为,看看你中了没有 听说这是前端程序员最讨厌的后端行为,不知道你有没有碰到过,或者你的前端同事虽然没跟你说过,但是你已经被偷偷吐槽了。 前端吐槽:后端从不自测接口,等到前后端联调时,这个接口获取不到,那个接口提交不了,把前端当成自己...
继续阅读 »

前端同事最讨厌的后端行为,看看你中了没有



听说这是前端程序员最讨厌的后端行为,不知道你有没有碰到过,或者你的前端同事虽然没跟你说过,但是你已经被偷偷吐槽了。




前端吐槽:后端从不自测接口,等到前后端联调时,这个接口获取不到,那个接口提交不了,把前端当成自己的接口测试员,耽误前端的开发进度。



听到这个吐槽,仿佛看到曾经羞愧的自己。这个毛病以前我也有啊,有些接口,尤其是大表单提交接口,表单特别大,字段很多,有时候就偷懒了,直接编译过了,就发到测试环境了。前端同时联调的时候一调接口,异常了。


好在后来改了,毕竟让人发现自己接口写的有问题,也是一件丢脸的事儿。


但是我还真见过后端的同学,写完接口一个都不测,直接发测试环境的。


我就碰到过厉害的,编译都不过,就直接提代码。以前,有个新来的同事,分了任务就默默的干着,啥也不问,然后他做的功能测试就各种发现问题。说过之后,就改一下,但是基本上还是不测试,本想再给他机会的,所以后来他每次提代码,我都review一下。直到有一天,我发现忍不了了,他把一段全局配置给注释了,然后把代码提了,我过去问他是不是本地调试,忘了取消注释了。他的回答直接让我震惊了,他说:不是的,是因为不注释那段代码,我本地跑步起来,所以肯定是那段代码有问题,所以就注释了。


然后,当晚,他就离职了。


解决方式


对于这种大表单类似的问题,应该怎么处理呢?


好像没有别的方法,只能克服自己的懒惰,为自己写的代码负责。就想着,万一接口有问题,别人可能会怀疑你水平不行,你水平不行,就是你不行啊,程序员怎么能不行呢。


你可以找那么在线 Java BeanJSON的功能,直接帮你生成请求参数,或者现在更可以借助 ChatGPT ,帮你生成请求参数,而且生成的参数可能比你自己瞎填的看上去更合理。


或者,如果是小团队,不拘一格的话,可以让前端的同事把代码提了,你本地跑着自测一下,让前端同事先做别的功能,穿插进行也可以。



前端吐槽:后端修改了字段或返回结构不通知前端



这个就有点不讲武德了。


正常情况下,返回结构和字段都是事先约定好的,一般都是先写接口,做一些 Mock 数据,然后再实现真实的逻辑。


除了约定好返回字段和结构外,还包括接口地址、请求方法、头信息等等,而且一个项目都会有项目接口规范,同一类接口的返回字段可能有很多相同的部分。


后端如果改接口,必须要及时通知前端,这其实应该是正常的开发流程。后端改了接口,不告诉前端,到时候测试出问题了,一般都会先找前端,这不相当于让前端背锅了吗,确实不地道啊。


后端的同学们,谨记啊。



前端吐槽:为了获取一个信息,要先调用好几个接口,可能参数还是相同的



假设在一个详情页面,以前端的角度就是,我获取详情信息,就调用详情接口好了,为什么调用详情接口之前,要调用3、4个其他的接口,你详情里需要啥参数,我直接给你传过去不就好了吗。


在后端看来可能是这样的,我这几个接口之前就写好了,前端拿过去就能用,只不过就是多调几次罢了,没什么大不了的吧。


有些时候,可能确实是必须这么做的,比如页面内容太多,有的部分查询逻辑复杂,比较耗时,这时候需要异步加载,这样搞确实比较好。


但是更多时候其实就是后端犯懒了,不想再写个新接口。除了涉及到性能的问题,大多数逻辑都应该在后端处理,能用一个接口处理完,就不应该让前端多调用第二个接口。


有前端的朋友曾经问过我,他说,他们现在做的系统中有些接口是根据用户身份来展示数据的,但是前端调用登录接口登录系统后,在调用其他接口的时候,除了在 Header 中加入 token 外,还有传很多关于用户信息的很多参数,这样做是不是不合理的。


这肯定不合理,token 本来就是根据用户身份产生的,后端拿到 token 就能获取用户信息,这是常识问题,让前端在接口中再传一遍,既不合理也不安全。


类似的问题还有,比如后端接口返回一堆数据,然后有的部分有用、有的部分没有,有的部分还涉及到逻辑,不借助文档根本就看不明白怎么用,这其实并不合理。


接口应该尽量只包含有用的部分,并且尽可能结构清晰,配合简单的字段说明就能让人明白是怎么回事,是最好的效果。


如果前后端都感觉形势不对了,后端一个接口处理性能跟不上了,前端处理又太麻烦了。这时候就要向上看了,产品设计上可能需要改一改了。


后端的同学可以学一点前端,前端的同学也可以学一点后端,当你都懂一些的时候,就能两方面考虑了,这样做出来的东西可能会更好用一点。总之,前后端相互理解,毕竟都是为了生活嘛。


作者:古时的风筝
来源:juejin.cn/post/7254927062425829413
收起阅读 »

面试官:你能说说常见的前端加密方法吗?

web
前言 本篇文章略微介绍一下前端中常见的加密算法。前端中常见的加密算法主要形式包括——哈希函数,对称加密和非对称加密算法。 一、哈希函数 定义:哈希也叫散列,是指将任意长度的消息映射为固定长度的输出的算法,该输出一般叫做散列值或者哈希值,也叫做摘要(Dige...
继续阅读 »

前言


本篇文章略微介绍一下前端中常见的加密算法。前端中常见的加密算法主要形式包括——哈希函数,对称加密和非对称加密算法。


一、哈希函数


image.png



  • 定义:哈希也叫散列,是指将任意长度的消息映射为固定长度的输出的算法,该输出一般叫做散列值或者哈希值,也叫做摘要(Digest)。简单来说,这种映射就是一种数据压缩,而且散列是不可逆的,也就是无法通过输出还原输入。

  • 特点:不可逆性(单向性)、抗碰撞性(消息不同其散列值也不同)、长度固定

  • 常见应用场景:由于不可逆性,常用于密码存储、数字签名、电子邮件验证、验证下载等方面,更多的是用用在验证数据的完整性方面。



    • 密码存储:明文保存密码是危险的。通常我们把密码哈希加密之后保存,这样即使泄漏了密码,因为是散列后的值,也没有办法推导出密码明文(字典攻击难以破解)。验证的时候,只需要对密码(明文)做同样的散列,对比散列后的输出和保存的密码散列值,就可以验证同一性。

    • 可用于验证下载文件的完整性以及防篡改:比如网站提供安装包的时候,通常也同时提供md5值,这样用户下载之后,可以重算安装包的md5值,如果一致,则证明下载到本地的安装包跟网站提供的安装包是一致的,网络传输过程中没有出错。



  • 优势:不可逆,速度快、存储体积小,可以帮助保护数据的完整性和减轻篡改风险。

  • 缺点:安全性不高、容易受到暴力破解


image.png


常见类型:SHA-512、SHA-256、MD5(MD5生成的散列码是128位)等。



  • MD5(Message Digest Algorithm 5) :是RSA数据安全公司开发的一种单向散列算法,非可逆,相同的明文产生相同的密文。

  • SHA(Secure Hash Algorithm) :可以对任意长度的数据运算生成一个固定位数的数值。

  • SHA/MD5对比:SHA在安全性方面优于MD5,并且可以选择多种不同的密钥长度。 但是,由于内存需求更高,运行速度可能会更慢。 不过,MD5因其速度而得到广泛使用,但是由于存在碰撞攻击风险,因此不再推荐使用。


二、对称加密



  • 定义:指加密和解密使用同一种密钥的算法。


image.png



  • 特点:优点是速度快,通信效率高;缺点是安全性相对较低。信息传输使用一对一,需要共享相同的密码,密码的安全是保证信息安全的基础,服务器和N个客户端通信,需要维持N个密码记录且不能修改密码。

  • 优势:效率高,算法简单,系统开销小,速度快,适合大数量级的加解密,安全性中等

  • 缺点:秘钥管理比较难,密钥存在泄漏风险。

  • 常见应用场景:适用于需要高速加密/解密的场景,例如 HTTP 传输的 SSL/TLS 部分,适用于加密大量数据,如文件加密、网络通信加密、数据加密、电子邮件、Web 聊天等。



    • 文件加密:将文件用相同的密钥加密后传输或存储,只有拥有密钥的用户才能解密文件。

    • 数据库加密:对数据库中的敏感信息进行加密保护,防止未经授权的人员访问。

    • 通信加密:将网络数据通过对称加密算法进行加密,确保数据传输的机密性,比较适合大量短消息的加密和解密。

    • 个人硬盘加密:对称加密可以为硬盘加密提供较好的安全性和高处理速度,这对个人电脑而言可能是一个不错的选择。



  • 常见类型DES,3DES,AES 等:



    • DES(Data Encryption Standard):分组式加密算法,以64位为分组对数据加密,加解密使用同一个算法,速度较快,适用于加密大量数据的场合。

    • 3DES(Triple DES):三重数据加密算法,是基于DES,对每个数据块应用三次DES加密算法,强度更高。

    • AES(Advanced Encryption Standard):高级加密标准算法,速度快,安全级别高,目前已被广泛应用,适用于加密大量数据,如文件加密、网络通信加密等。




AES与DES区别

AES与DES之间的主要区别在于加密过程。在DES中,将明文分为两半,然后再进行进一步处理;而在AES中,整个块不进行除法,整个块一起处理以生成密文。相对而言,AES比DES快得多,与DES相比,AES能够在几秒钟内加密大型文件。



  • DES



    • 优点:DES算法具有极高安全性,到目前为止,除了用穷举搜索法对DES算法进行攻击外,还没有发现更有效的办法。

    • 缺点:分组比较短、密钥太短、密码生命周期短、运算速度较慢。



  • AES



    • 优点:运算速度快,对内存的需求非常低,适合于受限环境。分组长度和密钥长度设计灵活, AES标准支持可变分组长度;具有很好的抵抗差分密码分析及线性密码分析的能力。

    • 缺点:目前尚未存在对AES 算法完整版的成功攻击,但已经提出对其简化算法的攻击。




三、非对称加密


-定义:指加密和解密使用不同密钥的算法,通常情况下使用公共密钥进行加密,而私有密钥用于解密数据。公钥和私钥是成对存在,公钥是从私钥中提取产生公开给所有人的,如果使用公钥对数据进行加密,那么只有对应的私钥(不能公开)才能解密,反之亦然。


image.png



  • 特点:缺点是加密解密速度较慢,通信效率较低,优点是安全性高,需要两个不同密钥,信息一对多。因为它使用的是不同的密钥,所以需要耗费更多的计算资源。服务器只需要维持一个私钥就可以和多个客户端进行通信,但服务器发出的信息能够被所有的客户端解密,且该算法的计算复杂,加密的速度慢。

  • 优势:秘钥容易管理,不存在密钥的交换问题,安全性好,主要用在数字签名,更适用于区块链技术的点对点之间交易的安全性与可信性。

  • 缺点:加解密的计算量大,比对称加密算法计算复杂,性能消耗高,速度慢,适合小数据量或数据签名

  • 常见应用场景:在实际应用中,非对称加密通常用于需要确保数据完整性和安全性的场合,例如数字证书的颁发、SSL/TLS 协议的加密、数字签名、加密小文件、密钥交换、实现安全的远程通信等。



    • 数字签名:数字签名是为了保证数据的真实性和完整性,通常使用非对称加密实现。发送方使用自己的私钥对数据进行签名,接收方使用发送方的公钥对签名进行验证,如果验证通过,则可以确认数据的来源和完整性。常见的数字签名算法都基于非对称加密,如RSA、DSA等。

    • ** 身份认证**:Web浏览器和服务器使用SSL/TLS技术来进行安全通信,其中就使用了非对称加密技术。Web浏览器在与服务器建立连接时,会对服务器进行身份验证并请求其证书。服务器将其证书发送给浏览器,证书包含服务器的公钥。浏览器使用该公钥来加密随机生成的“对话密钥”,然后将其发送回服务器。服务器使用自己的私钥解密此“对话密钥”,以确保双方之间的会话是安全的。

    • 安全电子邮件:非对称加密可用于电子邮件中,确保邮件内容只能由预期的收件人看到。发件人使用收件人的公钥对邮件进行加密,收件人使用自己的私钥对其进行解密。这确保了只有目标收件人才能读取邮件。



  • 常见类型RSA,DSA,DSS,ECC 等



    • RSA:由 RSA 公司发明,是一个支持变长密钥的公共密钥算法,需要加密的文件块的长度也是可变的。RSA 是一种非对称加密算法,即加密和解密使用一对不同的密钥,分别称为公钥和私钥。公钥用于加密数据,私钥用于解密数据。RSA 算法的安全性基于大数分解问题,密钥长度通常选择 1024 位、2048 位或更长。RSA 算法用于保护数据的机密性、确保数据的完整性和实现数字签名等功能。

    • DSA(Digital Signature Algorithm) :数字签名算法,仅能用于签名,不能用于加解密。

    • ECC(Elliptic Curves Cryptography) :椭圆曲线密码编码学。

    • DSS:数字签名标准,可用于签名,也可以用于加解密。




总结


前端使用非对称加密原理很简单,平时用的比较多的也是非对称加密,前后端共用一套加密解密算法,前端使用公钥对数据加密,后端使用私钥将数据解密为明文。中间攻击人拿到密文,如果没有私钥的话是没办法破解的。


欢迎大佬继续评论区补充


作者:优秀稳妥的Zn
来源:juejin.cn/post/7280057907055919144
收起阅读 »

你的网站如何接入QQ,微信登录

web
主要实现步骤 对接第三方平台,获取第三方平台的用户信息。 利用该用户信息,完成本应用的注册。 qq登录接入 接入前的配置 qq互联 登录后,点击头像,进行开发者信息填写,等待审核。 邮箱验证后,等待审核。 审核通过后,然后就可以创建应用了。 然后填写...
继续阅读 »

主要实现步骤



  • 对接第三方平台,获取第三方平台的用户信息。

  • 利用该用户信息,完成本应用的注册。


qq登录接入


接入前的配置


qq互联


登录后,点击头像,进行开发者信息填写,等待审核。


image.png


邮箱验证后,等待审核。


image.png


审核通过后,然后就可以创建应用了。


image.png


然后填写一些网站信息,等待审核。审核通过后,即可使用。


开始接入



  1. 导入qq登录的sdk



<script type="text/javascript" charset="utf-8" src="https://connect.qq.com/qc_jssdk.js" data-appid="您应用的appid"
data-redirecturi="qq扫码后的回调地址(上面配置中可以查到)">
script>


  1. 点击qq登录,弹出扫码窗口。


// QQ 登录的 URL
const QQ_LOGIN_URL =
'https://graph.qq.com/oauth2.0/authorize?client_id=您的appid&response_type=token&scope=all&redirect_uri=您的扫码后的回调地址'
window.open(
QQ_LOGIN_URL,
'oauth2Login_10609',
'height=525,width=585, toolbar=no, menubar=no, scrollbars=no, status=no, location=yes, resizable=yes'
)


  1. 挂起qq登录。需要注意的是,扫码登录成功后,调试代码需要在线上环境。


"qqLoginBtn" v-show="false">

// QQ 登录挂起
onMounted(() => {
QC.Login(
{
btnId: 'qqLoginBtn' //插入按钮的节点id
},
// 登录成功之后的回调,但是需要注意,这个回调只会在《登录回调页面中被执行》
// 登录存在缓存,登录成功一次之后,下次进入会自动重新登录(即:触发该方法,所以我们应该在离开登录页面时,注销登录)
// data就是当前qq的详细信息
(data, opts) => {
console.log('QQ登录成功')
// 1. 注销登录,否则在后续登录中会直接触发该回调
QC.Login.signOut()
// 2. 获取当前用户唯一标识,作为判断用户是否已注册的依据。(来决定是否跳转到注册页面)
const accessToken = /access_token=((.*))&expires_in/.exec(
window.location.hash
)[1]
// 3. 拼接请求对象
const oauthObj = {
nickname: data.nickname,
figureurl_qq_2: data.figureurl_qq_2,
accessToken
}
// 4. 完成跨页面传输 (需要将数据传递到项目页面,而非qq登录弹框页面中进行操作)
brodacast.send(oauthObj)

// 针对于 移动端而言:通过移动端触发 QQ 登录会展示三个页面,原页面、QQ 吊起页面、回调页面。并且移动端一个页面展示整屏内容,且无法直接通过 window.close() 关闭,所以在移动端中,我们需要在当前页面继续进行后续操作。
oauthLogin(LOGIN_TYPE_QQ, oauthObj)
// 5. 在 PC 端下,关闭第三方窗口
window.close()
}
)
})


  1. 跨页面窗口通信


想要实现跨页面信息传输,通常有两种方式:



  • BroadcastChannel:允许 同源 的不同浏览器窗口,Tab页,frame或者 iframe 下的不同文档之间相互通信。但是会存在兼容性问题。

  • localStorage + window.onstorage:通过localStorage 进行 同源 的数据传输。用来处理 BroadcastChannel 不兼容的浏览器。以前写过一篇文章


// brodacast.js
// 频道名
const LOGIN_SUCCESS_CHANNEL = 'LOGIN_SUCCESS_CHANNEL'

// safari@15.3 不支持 BroadcastChannel,所以我们需要对其进行判定使用,在不支持 BroadcastChannel 的浏览器中,使用 localstorage
let broadcastChannel = null
if (window.BroadcastChannel) {
broadcastChannel = new BroadcastChannel(LOGIN_SUCCESS_CHANNEL)
}

/**
* 等待 QQ 登录成功
* 因为 QQ 登录会在一个新的窗口中进行,用户扫码登录成功之后会回调《新窗口的 QC.Login 第二参数 cb》,而不会回调到原页面。
* 所以我们需要在《新窗口中通知到原页面》,所以就需要涉及到 JS 的跨页面通讯,而跨页面通讯指的主要就是《同源页面的通讯》
* 同源页面的通讯方式有很多,我们这里主要介绍:
* 1. BroadcastChannel ->
https://developer.mozilla.org/zh-CN/docs/Web/API/BroadcastChannel

* 2. window.onstorage:注意:该事件不在导致数据变化的当前页面触发
*/

/**
* 等待回调,它将返回一个 promise,并携带对应的数据
*/

const wait = () => {
return new Promise((resolve, reject) => {
if (broadcastChannel) {
// 触发 message 事件时的回调函数
broadcastChannel.onmessage = async (event) => {
// 改变 promise 状态
resolve(event.data)
}
} else {
// 触发 localStorage 的 setItem 事件时回调函数
window.onstorage = (e) => {
// 判断当前的事件名
if (e.key === LOGIN_SUCCESS_CHANNEL) {
// 改变 promise 状态
resolve(JSON.parse(e.newValue))
}
}
}
})
}

/**
* 发送消息。
* broadcastChannel:触发 message
* localStorage:触发 setItem
*/

const send = (data) => {
if (broadcastChannel) {
broadcastChannel.postMessage(data)
} else {
localStorage.setItem(LOGIN_SUCCESS_CHANNEL, JSON.stringify(data))
}
}

/**
* 清除
*/

const clear = () => {
if (broadcastChannel) {
broadcastChannel.close()
broadcastChannel = null
}
localStorage.removeItem(LOGIN_SUCCESS_CHANNEL)
}

export default {
wait,
send,
clear
}


  1. 拿到数据后,进行登录(自己服务器登录接口)操作。



  • 传入对应参数(loginType, accessToken)等参数进行用户注册判断。

  • 通过accessToken判断用户已经注册,那么我们就直接在后台查出用户名和密码直接登录了。

  • 通过accessToken判断用户未注册,那么我们将跳转到注册页面,让其注册。


 // 打开视窗之后开始等待
brodacast.wait().then(async (oauthObj) => {
// 登录成功,关闭通知
brodacast.clear()
// TODO: 执行登录操作
oauthLogin("QQ", oauthObj)
})

// oauthLogin.js
import store from '@/store'
import router from '@/router'
import { message } from '@/libs'
import { LOGIN_TYPE_OAUTH_NO_REGISTER_CODE } from '@/constants'

/**
* 第三方登录统一处理方法
*
@param {*} oauthType 登录方式
*
@param {*} oauthData 第三方数据
*/

export const oauthLogin = async (oauthType, oauthData) => {
const code = await store.dispatch('user/login', {
loginType: oauthType,
...oauthData
})
// 返回 204 表示当前用户未注册,此时给用户一个提示,走注册页面
if (code === LOGIN_TYPE_OAUTH_NO_REGISTER_CODE) {
message('success', `欢迎您 ${oauthData.nickname},请创建您的账号`, 6000)
// 进入注册页面,同时携带当前的第三方数据和注册标记
router.push({
path: '/register',
query: {
reqType: oauthType,
...oauthData
}
})
return
}

// 否则表示用户已注册,直接进入首页
router.push('/')
}

微信扫码登录接入


微信开放平台


登录后,进行对应的应用注册,填写一大堆详细信息,然后进行交钱,就可以使用微信登录了。


image.png


开始接入


整个微信登录流程与QQ登录流程略有不同,分为以下几步:


1.通过 微信登录前置数据获取 接口,获取登录数据(比如 APP ID)。就是后台将一些敏感数据通过接口返回。


2.根据获取到的数据,拼接得到 open url 地址。打开该地址,展示微信登录二维码。移动端微信扫码确定登录。


// 2. 根据获取到的数据,拼接得到 `open url` 地址
window.open(
`https://open.weixin.qq.com/connect/qrconnect?appid=${appId}&redirect_uri=${redirectUri}&response_type=code&scope=${scope}&state=${state}#wechat_redirect`,
'',
'height=525,width=585, toolbar=no, menubar=no, scrollbars=no, status=no, location=yes, resizable=yes'
)

3.等待用户扫码后,从当前窗口中解析 window.location.search 得到用户的 code数据。 微信扫码后,会重定向到登录页面。


/**
* 微信登录成功之后的窗口数据解析
*/

if (window.location.search) {
const code = /code=((.*))&state/.exec(window.location.search)[1]
if (code) {
brodacast.send({
code
})
// 关闭回调网页
window.close()
}
}

4.根据 appId、appSecret、code 通过接口获取用户的 access_token


5.根据 access_token 获取用户信息


6.通过用户信息触发 oauthLogin 方法。


调用的接口,都是后端通过微信提供的api来获取到对应的数据,然后再通过接口返回给开发者。  以前也写过微信登录文章


// 等待扫码登录成功通知
brodacast.wait().then(async ({ code }) => {
console.log('微信扫码登录成功')
console.log(code)
// 微信登录成功,关闭通知
brodacast.clear()
// 获取 AccessToken 和 openid
const { access_token, openid } = await getWXLoginToken(
appId,
appSecret,
code
)
// 获取登录用户信息
const { nickname, headimgurl } = await getWXLoginUserInfo(
access_token,
openid
)
console.log(nickname, headimgurl)
// 执行登录操作
oauthLogin(LOGIN_TYPE_WX, {
openid,
nickname,
headimgurl
})
})

需要注意的是,在手机端,普通h5页面是不能使用微信扫码登录的。


总结


相同点



  • 接入前需要配置一些内容信息。

  • 都需要在线上环境进行调试。

  • 都是扫码后在三方窗口中获取对应的信息,发送到当前项目页面进行请求,判断用户是否已经注册,还是未注册。已经注册时,调用login接口时,password直接传递空字符串即可,后端可以通过唯一标识,获取到对应的用户名和密码,直接返回token进行登录。未注册,就跳转到注册页面,让其注册。


不同点



  • qq接入需要导入qc_sdk。

  • qq直接扫码后即可获取到用户信息,就可以直接调用login接口进行判断用户是否注册了。

  • 微信扫码后,获取code来换取access_token, openid,然后再通过access_token, openid来换取用户信息。然后再调用login接口进行判断用户是否注册了。


作者:Spirited_Away
来源:juejin.cn/post/7311343161363234866
收起阅读 »

HTML问题:如何实现分享URL预览?

web
前端功能问题系列文章,点击上方合集↑ 序言 大家好,我是大澈! 本文约2100+字,整篇阅读大约需要3分钟。 本文主要内容分三部分,如果您只需要解决问题,请阅读第一、二部分即可。如果您有更多时间,进一步学习问题相关知识点,请阅读至第三部分。 感谢关注微信公众号...
继续阅读 »

前端功能问题系列文章,点击上方合集↑


序言


大家好,我是大澈!


本文约2100+字,整篇阅读大约需要3分钟。


本文主要内容分三部分,如果您只需要解决问题,请阅读第一、二部分即可。如果您有更多时间,进一步学习问题相关知识点,请阅读至第三部分。


感谢关注微信公众号:“程序员大澈”,然后加入问答群,从此让解决问题的你不再孤单!


1. 需求分析


为了提高用户对页面链接分享的体验,需要对分享链接做一些处理。


以 Telegram(国外某一通讯软件) 为例,当在 Telegram 上分享已做过处理的链接时,它会自动尝试获取链接的预览信息,包括标题、描述和图片。


如此当接收者看到时,可以立即获取到分享链接的一些重要信息。这有助于接收者更好地了解链接的内容,决定是否点击查看详细内容。


图片


2. 实现步骤


2.1 实现前的说明


对于URL分享预览这个功能问题,在项目中挺常用的,只不过今天我们是以一些框架分享API的底层原理角度来讲的。


实现这种功能的关键,是在分享的链接中嵌入适当的元数据信息,应用软件会自动解析,请求分享链接的预览信息,并根据返回的元数据生成预览卡片。


对于国内的应用软件,目前我试过抖音,它可以实现分享和复制粘贴都自动解析,而微信、QQ等只能实现分享的自动解析。


对于国外的应用软件,我只实验过Telegram,它可以实现分享和复制粘贴都自动解析,但我想FacebookTwitterInstagram这些应用应该也都是可以的。


2.2 实现代码


实现URL链接的分享预览,你可以使用 Open Graph协议或 Twitter Cards,然后在 HTML 的 标签中,添加以下 meta 标签来定义链接预览的信息。


使用时,将所有meta全部复制过去,然后根据需求进行自定义即可。


还要注意两点,确保你页面的服务器正确配置了 SSL 证书,以及确保链接的URL有效(即:服务器没有做白名单限制)。


<head>
  
  <meta property="og:title" content="预览标题">
  <meta property="og:description" content="预览描述">
  <meta property="og:image:width" content="图片宽度">
  <meta property="og:image:height" content="图片高度">
  <meta property="og:image" content="预览图片的URL">
  <meta property="og:url" content="链接的URL">
  
  
  <meta name="twitter:card" content="summary">
  <meta name="twitter:title" content="预览标题">
  <meta name="twitter:description" content="预览描述">
  <meta property="twitter:image:width" content="图片宽度">
  <meta property="twitter:image:height" content="图片高度">
  <meta name="twitter:image" content="预览图片的URL">
  <meta name="twitter:url" content="链接的URL">
head>

下面我们做一些概念的整理、总结和学习。


3. 问题详解


3.1 什么是Open Graph协议?


Open Graph协议是一种用于在社交媒体平台上定义和传递网页元数据的协议。它由 Facebook 提出,并得到了其他社交媒体平台的支持和采纳。Open Graph 协议旨在标准化网页上的元数据,使网页在社交媒体上的分享和预览更加一致和可控。


通过在网页的 HTML  标签中添加特定的 meta 标签,使用 Open Graph 协议可以定义和传递与网页相关的元数据信息,如标题、描述、图片等。这些元数据信息可以被社交媒体平台解析和使用,用于生成链接预览、分享内容和提供更丰富的社交图谱。


使用 Open Graph 协议,网页的所有者可以控制链接在社交媒体上的预览内容,确保链接在分享时显示的标题、描述和图片等信息准确、有吸引力,并能够准确传达链接的主题和内容。这有助于提高链接的点击率、转化率和用户体验。


Open Graph 协议定义了一组标准的 meta 标签属性,如 og:titleog:descriptionog:image 等,用于提供链接预览所需的元数据信息。通过在网页中添加这些 meta 标签并设置相应的属性值,可以实现链接预览在社交媒体平台上的一致展示。


需要注意的是,Open Graph 协议是一种开放的标准,并不限于 Facebook 平台。其他社交媒体平台,如 Twitter、LinkedIn 等,也支持使用 Open Graph 协议定义和传递网页元数据,以实现链接预览的一致性。


图片


3.2 什么是Twitter Cards?


Twitter Cards 是一种由 Twitter 推出的功能,它允许网站所有者在他们的网页上定义和传递特定的元数据,以便在 Twitter 上分享链接时生成更丰富和吸引人的预览卡片。通过使用 Twitter Cards,网页链接在 Twitter 上的分享可以展示标题、描述、图片、链接和其他相关信息,以提供更具吸引力和信息丰富的链接预览。


Twitter Cards 提供了多种类型的卡片,以适应不同类型的内容和需求。以下是 Twitter Cards 的一些常见类型:



  • Summary CardSummary Card 类型的卡片包含一个标题、描述和可选的图片。它适用于分享文章、博客帖子等内容。

  • Summary Card with Large ImageSummary Card with Large Image 类型的卡片与 Summary Card 类型类似,但图片尺寸更大,更突出地展示在卡片上。

  • App CardApp Card 类型的卡片用于分享移动应用程序的信息。它包含应用的名称、图标、描述和下载按钮,以便用户可以直接从预览卡片中下载应用。

  • Player CardPlayer Card 类型的卡片用于分享包含媒体播放器的内容,如音频文件、视频等。它允许在预览卡片上直接播放媒体内容。


通过在网页的 HTML  标签中添加特定的 meta 标签,使用 Twitter Cards 可以定义和传递与链接预览相关的元数据信息,如标题、描述、图片、链接等。这些元数据信息将被 Twitter 解析和使用,用于生成链接预览卡片。


使用 Twitter Cards 可以使链接在 Twitter 上的分享更加吸引人和信息丰富,提高链接的点击率和用户参与度。它为网站所有者提供了更多控制链接在 Twitter 上展示的能力,并提供了一种更好的方式来呈现他们的内容。


图片


图片


作者:程序员大澈
来源:juejin.cn/post/7310112330663231515
收起阅读 »

大厂是怎么封装api层的?ts,axios 基于网易公开课

web
先看一下使用方法 先声明,这套写法是我在一次网易公开课学来的,说是大厂内部是怎么封装请求的,本人只是给它改成ts版本。 挺香的。 上核心代码 代码一:utils/request/getrequest.ts import axios, { type Axi...
继续阅读 »

先看一下使用方法
请求封装2.png


先声明,这套写法是我在一次网易公开课学来的,说是大厂内部是怎么封装请求的,本人只是给它改成ts版本。
挺香的。


上核心代码


WX20231124-143933@2x.png


代码一:utils/request/getrequest.ts



import axios, { type AxiosRequestConfig, type CancelTokenSource } from "axios";
import { manualStopProgram } from '@/utils/index';

import server from "./server";
import type { RequestConfig, ApiRouter, ServerRes } from './server.types'

class Requestextends keyof ApiRouter = keyof ApiRouter> {
requestRouter: ApiRouter = {} as ApiRouter
requestTimes = 0;
requestMap: Record<string, CancelTokenSource> = {};
toLogined = false

/**
*
@feat <注册请求路由>
*/

parseRouter(routerName: T, defaultAxiosConfigMap: Record) {
const apiModule = this.requestRouter[routerName] = {} as ApiRouter[T]

Object.entries(defaultAxiosConfigMap).forEach((item) => {
type ApiName = keyof ApiRouter[T]

const [apiName, defaultRequestConfig] = item as [ApiName, RequestConfig];

const request = this.sendMes.bind(
this,
routerName,
apiName,
defaultRequestConfig,
);

apiModule[apiName] = request as ApiRouter[T][ApiName]
apiModule[apiName].state = "ready";
});
}
async sendMes<ApiName extends keyof ApiRouter[T] = keyof ApiRouter[T]>(
routerName: T,
apiName: ApiName,
defaultRequestConfig: RequestConfig,
requestParams: Record<string, any>,
otherAxiosConfig?: RequestConfig
): Promise<ServerRes> {
this.requestTimes += 1;

return new Promise(async (resolve, reject) => {
try {
const selfMe = this.requestRouter[routerName][apiName];
const requstConfig: RequestConfig = {
...defaultRequestConfig,
...otherAxiosConfig,
data: requestParams,
};

/**
*
@feat <取消上一个同url请求>
*
@remarks [注:
* 个别页面需要同个api地址,多次请求,请传uniKey 作为区分,
* 不然有概率出现上一个请求被干掉
* ]
*/

if (selfMe.state === 'pending') this.cancelLastSameUrlRequest(requstConfig);

// 保险方案,传了 uniKey 才取消请求
// if (selfMe.state === 'pending' && requstConfig.uniKey) this.cancelLastSameUrlRequest(requstConfig);

const successCb = (res: ServerRes) => {
const ret = this.responseHandle(res, requstConfig)
resolve(ret);
};
const failCb = (error: unknown) => {
console.error("接口报错: " + requstConfig.url, error);
// 处理错误逻辑
throw error;
};
const complete = () => {
selfMe.state = "ready";
this.requestTimes -= 1;

if (this.requestTimes === 0) {
this.toLogined = false;
}
};

selfMe.state = "pending";
requstConfig.cancelToken = this.axiosSourceHandle(requstConfig).token;

await server(requstConfig).then(successCb).catch(failCb).finally(complete);

} catch (error) {
reject(error);
}
})
}

responseHandle(res: ServerRes, config: RequestConfig) {
const { code } = res;
console.warn(`请求返回: ${config.url}`, res);

if (code === 405) throw String("405 检查请求方式");
if (code === 401) this.toLogin();
if (code !== 200) throw String(res.message);

return res;
}

toLogin() {
if (this.toLogined) return;
throw String("请先登录");
}

generateReqKey(requestConfig: RequestConfig) {
return `${requestConfig.url}__${requestConfig.uniKey || ''}`
}
axiosSourceHandle(requestConfig: RequestConfig) {
const cancelToken = axios.CancelToken;
const source = cancelToken.source();

const reqKey = this.generateReqKey(requestConfig);
this.requestMap[reqKey] = source;

return source;
}
// 处理取消上一个请求
cancelLastSameUrlRequest(requestConfig: RequestConfig) {
const reqKey = this.generateReqKey(requestConfig);
const currentReqKey = this.requestMap[reqKey];

currentReqKey.cancel(`${manualStopProgram} reqKey: ${reqKey}`); // manualStopProgram 是一个标识,让外面的提示框忽略报错
}
}


export default new Request();



代码二:utils/request/server.ts


import axios from "axios";
import { UserInfo } from "@/utils/index";
import type { RequestConfig, ServerRes } from "./server.types";

export default async function server(
axiosRequestConfig: RequestConfig
): Promise<ServerRes> {
const token = UserInfo.getToken() || "";
const reqData = (() => {
const data = axiosRequestConfig.data;
const isFormData = data instanceof FormData;

if (isFormData) {
data.
append("token", token);
return data;
}
return {
...data,
token
};
}
)();

const { data: resBody, status } = await axios({
...axiosRequestConfig,
withCredentials: true,
data: reqData
}
).
catch((err) => {
const errMsg = err && typeof err === "object" && err !== null && "message" in err
if (errMsg) throw err.message;
throw err;
}
);

return resBody;
}

export {
server
}

import type { AxiosRequestConfig } from "axios";
import type { Api } from "@/apis/index";

export type RequestConfigany> = AxiosRequestConfig & {
uniKey?: string | number;
};

export type ApiConfig = {
params: T;
return: K;
};

export type List_pagiantion = {
page: number;
page_size: number;
};

// 这里有点绕,把各个api的参数和返回值 合成一个个特定的函数
export type ApiRouter = {
[K in keyof Api]: {
[T in keyof Api[K]]: Api[K][T] extends ApiConfig<any, any>
? {
(params: Api[K][T]["params"], otherRequestConfig?: RequestConfig): Promise<{
message: string;
code: number;
data: Api[K][T]["return"];
}>;
state?: 'pending' | 'ready'
}
: never;
};
}

export type ApiRouter__requestConfig = {
[K in keyof Api]: {
[T in keyof Api[K]]: RequestConfig;
};
}
export type ServerRes = {
code: number,
message: string,
data: any
};

接下来是apis文件夹(即开头的那个图片),在这里配置接口信息,日常业务代码在这里写


接口.png


代码一 写api配置 :src/apis/modules/admin-admin/index.ts


import type { ApiRouter__requestConfig } from "@/utils/modules/request/server.types.d";

const indexAdmin: ApiRouter__requestConfig["adminAdmin"] = {
getList: {
method: "post",
url: "/admin/admin/getList"
},
};

export default indexAdmin;

代码二 写接口类型声明 :src/apis/modules/admin-admin/index.types.d.ts


import type { ApiConfig } from "@/utils/modules/request/server.types.d";

export type AdminAdmin = {
getList: ApiConfig<
{
page: number,
page_size: number,
phone?: string,
status?: ManagerStatus,
groups_id?: number
},
{
count: number,
list: {
id: number,
phone: string,
groups_id: number,
create_at: string,
status: number,
status_txt: string,
groups_txt: string
}[]
}
>,
};


代码三 注册路由 :src/apis/index.ts


import { request } from "@/utils/index";
import indexAdmin from "./modules/index-admin/index";

request.parseRouter("indexAdmin", indexAdmin);

export type Api = {
indexAdmin: IndexAdmin;
}

// 这个是另一个作用,到处配置项,配合接口做权限控制,下面在说
export function getApiConfigMap() {
return {
indexAdmin,
};
}

下面说说这个封装方式好在哪里:


1. 解决的痛点:


以往我们看到最常见的封装方式,就这种

export function Api1() {
return axios.get('xx')
}

export function Api2() {
return axios.get('xx')
}

export function Api3() {
return axios.get('xx')
}

export function Api4() {
return axios.get('xx')
}

这种就非常麻木,一直写函数,每一个都要写配置项,没有数据结构结构=(无法复用)。
如果换成上面的 const indexAdmin: ApiRouter__requestConfig["adminAdmin"] = {},这种写法,就有数据结构了,有了结构之后就可以进行组合复用
比如上面提到的 getApiConfigMap 可以把数据结构直接导出,配合接口做按钮级权限控制,
接口会返回一份配置项{authen1: '/admin/admin/getList'}。
我们二者一比对,就能判断出是否有权限了。


比如看下面代码
PermissionWrapper 是一个权限容器组件 hasPermission=true就显示按钮
store.state.myPermission?.enterpriseAlarm?.edit 是用 getApiConfigMap 和结构权限表配合生成的


        <PermissionWrapper
hasPermission={store.
state.myPermission?.enterpriseAlarm?.edit}
>

<el-button type="primary" size="small" text onClick={openEditDialog}>
编辑
el-button>

PermissionWrapper>

ts直接提示,写起来很舒服,快准狠
ts提示.png


2. 请求函数封闭又开放


经过上面的 parseRouter 注册路由之后,sendMes 生成了N个请求函数,独一无二的函数,里面的fail success 可以做的事情很多,不如限制登录,取消上一个请求等等。大家有啥想法欢迎评论区写出来,我们一起优化它。


sendMes 最后一个参数,保持了开放性,在调用的时候我们传入uniKey就可以取消上一个请求了,还有一些特殊的参数,随便造。


3. 方便提取Api类型的参数和返回值类型(这是我额外拓展的)


我们经常会需要把参数和返回值的类型拿出来到页面上使用,这时候,就可以通过下面这个XX全局声明拿到。


declare namespace XX {
export type PromiseParams = T extends Promise ? T : never;

/**
*
@feat < 提取 api 参数 >
*
@describe <
type GetListParams = XX.ExtractApiParams

* >
*/

export type ExtractApiParams<Function_type> = Function_type extends (e: infer U) => any
? Excludeundefined>
: never;

/**
*
@feat < 提取 api 返回值 >
*
@describe <
type GetListReturn = XX.ExtractApiReturnType

* >
*/

export type ExtractApiReturnType<Function_type> = PromiseParams<
ReturnType<Function_type>
>["data"];

/**
*
@feat < 提取 api 为分页的 list item>
*
@describe <
type TableRow = XX.ExtractPromiseListItem

* >
*/

export type ExtractPromiseListItem<Function_type> =
ExtractApiReturnType<Function_type>["list"][number];
}

下面是一些使用方法举例
image.png


image.png


image.png


image.png


用上面的写法很方便就能在页面中把具体的类型拿出来。做到一次类型声明,到处使用。


api封装是一个长期的话题,axios很好用,但其实它就是一个请求方法而已。相信大家也见过很多乱七八糟的写法。特别是一些老项目,想新增api都不知道放在哪个文件夹。


很幸运无意中看到网易公开课的老师们讲解,那时候他们写的是js版本,看到这种由配置对象直接生成api函数的做法瞬间眼前一亮,这不就是我一直在找的封装方式,满足了我所有的想象。感谢感谢


后来我花了点时间,让它变成ts版本,还封装XX这个全局声明,让它彻底好用起来。希望这个封装能让大家受益。


细心的读者可能会发现上面的代码,一直抛错误,但是却没有拦截提示。 这是笔者推崇的报错终止程序,而不是用return的方式。(js终止程序,我常用throw 替代 return


如果您有什么好的建议或想法,欢迎评论区留言。有用请点点赞,还有更多经验总结在路上。
嘴下留情,骂我倒无所谓,重要的是别把评论区搞得乌烟瘴气


原课程链接:js es5版本,有兴趣的可以看看。但我觉得它那个取消请求,得再升级一下不然万一同个页面调用同个接口2次,就会取消第一个请求了。所以我加装了uniKey作为标识。
live.study.163.com/live/index.…


作者:闸蟹
来源:juejin.cn/post/7304594468157849640
收起阅读 »

喊话各大流行UI库,你们的Select组件到底行不行啊?

web
各种 UI 库的 Select,你们能不能人性化一点! 最近在云效上合并代码,本想着懒的目的输入了非连续的关键字搜索分支,结果... 大概逻辑就是,搜索时必须输入连续的字母,比如,要找 “master-alpha”分支,非要输入 master-al 才能搜到...
继续阅读 »

各种 UI 库的 Select,你们能不能人性化一点!


最近在云效上合并代码,本想着懒的目的输入了非连续的关键字搜索分支,结果...


1.gif


大概逻辑就是,搜索时必须输入连续的字母,比如,要找 “master-alpha”分支,非要输入 master-al 才能搜到,像图中输入 “masal” 就完全搜索不到。这导致了很多场景下使用起来很不方便,例如我们只记得几个非连续的关键字,或者懒得打那么多连续的关键字来搜索,用户体验较差。


然后我又看了几个流行组件库的 Select。


Element-ui


2.gif


Antd


3.gif


Naive-ui


4.gif


全军覆没!


那我们来自己实现一个吧!先来两个实战图。


不带高亮的非连续搜索


6.gif


带高亮的非连续搜索


5.gif


实现不带高亮的非连续搜索


以vue3+ElementUI为例,在这里将会用到一个小小的js库叫sdm2来实现非连续的字符串匹配。


视图部分


<el-select
v-model="value"
size="large"
placeholder="Filter options"
filterable
:filter-method="q => (query = q)">

<el-option
v-for="item in optionsComputed"
:key="item.value"
:value="item.value"
:label="item.label">

el-option>
el-select>

没有什么特别的,就是加了个filterMethod函数将关键词赋值给query状态,然后optionsComputedquery值根据关键词进行筛选


import { match } from 'sdm2';

const options = [/* 选项列表 */];
const query = ref('');
const optionsComputed = computed(() => options.filter(({ label }) =>
// 使用sdm2的match函数筛选
match(label, query.value, {
// 忽略大小写匹配
ignoreCase: true,
}
)));

就这么简单完成了。


实现带高亮的非连续搜索


视图部分


高亮部分使用v-html动态解析html字符串。


<el-select
v-model="value"
size="large"
placeholder="Filter options"
filterable
:filter-method="q => (query = q)">

<el-option
v-for="item in optionsComputed"
:key="item.value"
:value="item.value"
:label="item.label">

<div v-html="item.highlight">div>
el-option>
el-select>

为了让匹配到的关键字高亮,我们需要将匹配到的关键字转换为html字符串,并将高亮部分用包裹,最后用v-html动态解析。


import { filterMap } from 'sdm2';

const options = [/* 选项列表 */];
const query = ref('');
const optionsComputed = computed(() =>
// 为了过滤出选项,并将到的转换为html字符串,此时我们要用sdm2的filterMap函数
filterMap(options, query.value, {
ignoreCase: true,

// 把matchStr返回的字符串作为被匹配项
matchStr: ({ label }) => label,

// 匹配到后转换为html高亮字符串
onMatched: (matchedStr) => `${matchedStr}`,

// 将匹配到的项转换转换为需要的格式,str为onMatched转换后的字符串,origin为数组的每项原始值
onMap: ({ str, origin }) => {
return {
highlight: str,
...origin
}
}
})
);

然后一个带高亮的非连续搜索就完成了。


总结


这样你的搜索库就又更智能点了吧,然后各位 UI 库作者,你们也可以考虑考虑这个方案,或者有哪位朋友愿意的话也可以去为他们提一个issue或PR。

作者:古韵
来源:juejin.cn/post/7310104657212178459
收起阅读 »

Ant Design Mini 支持微信小程序啦!

web
Ant Design Mini 经过一段时间的开发与不断的努力,我们兴奋地宣布,Ant Desigin Mini 组件库中已有 16 个核心组件完成了微信小程序的适配工作!现在你不仅可以在支付宝小程序中使用 Ant Desigin Mini 组件库,也可以在微...
继续阅读 »

Ant Design Mini


经过一段时间的开发与不断的努力,我们兴奋地宣布,Ant Desigin Mini 组件库中已有 16 个核心组件完成了微信小程序的适配工作!现在你不仅可以在支付宝小程序中使用 Ant Desigin Mini 组件库,也可以在微信小程序中使用了!


目前这项适配正处于 Beta 阶段,我们诚挚地邀请大家前来体验。首批适配的组件包括:ButtonSliderContainerIconLoadingSwitchTagInputCalendarListResultPopoverMaskStepperPopupCheckbox


先来看看 Demo 效果:
image.png


我们的官网文档、Demo 都已同步更新双端切换能力:
2023-11-28 14.44.51.gif


你可以参考以下文档进行接入:



双平台无法对齐的特性


受制于各平台框架设计,以下这些特性存在差异:



  • 两个平台的事件 API 不同。支付宝小程序可以把实例上通过 props 传递给子组件,而微信需要在 data 里传递函数。视图层的写法也所有不同。

    • 下面是 Calendar 在两种平台的使用方式。

      • 微信小程序






Page({
data:{
handleFormat() {
}
}
})

<calendar onFormat="{{handleFormat}}" />

  - 支付宝小程序

Page({
handleFormat() {

}
})
<calendar onFormat="handleFormat" />


  • 微信小程序不支持在 slot 为空时显示默认值, 所以我们在微信平台取消了部分 slot,对应会损失一些定制能力。 比如说 Calendar 组件, 在支付宝可以通过 calendarTitle 这个 slot 定制标题,在微信端只能通过 css 来控制样式。


<View class="ant-calendar-title-container">
{/* #if ALIPAY */}
<Slot name="calendarTitle">
{/* #endif */}
<View class="ant-calendar-title">{currentMonth.title}</View>
{/* #if ALIPAY */}
</Slot>
{/* #endif */}
</View>


  • 微信小程序不支持循环渲染 slot , 所以部分组件无法迁移到微信, 比如说 IndexBar 组件, 使用了 Slot 递归渲染整个组件的内容。这种写法无法迁移到微信。


<view a:for="{{items}}">
<slot
value="{{item}}"
index="{{index}}"
name="labelPreview" />

</view>

双平台适配背后的工程技术


下面我们为大家介绍一下 Antd Mini 支持多平台背后的一些工程方案。


中立的模板语法: 使用 tsx 开发小程序


由于支付宝和微信的小程序的语法各有差异,为了解决让 Antd Mini 同时支持两个端,我们团队选择的 tsx 的一个子集作为小程序的模板语言。
使用 tsx 具有以下优势:



  • 可以直接使用 babel 解析代码,无需自己开发编译器。

  • 各个 IDE 原生支持 TSX 的类型推导与代码提示。

  • 视图层和逻辑层可以复用同一份 props 类型。

  • 可以直接通过 import 导入其他的小程序组件,使用 typescript 进行类型检查。

  • 视图层脚本也可以享受类型校验,无需依赖平台 IDE


由于小程序的视图语法比较受限,从 tsx 向跨平台视图语法转换是比较容易的。我们基于 babel 开发了一个简单的编译器,解析原先 tsx 的语法树以后,将 React 的语法平行转换为可读性比较强的小程序视图语法。
具体举例来看:



  • 条件判断 : 我们使用了 &&以及 ?: 三元表达式替代了之前的 :if 标签。

    • tsx: !!a && <Text>a</Text>

    • 微信小程序:<text wx:if="{{ !!a }}" />

    • 支付宝小程序:<text wx:if="{{ !!a }}" />



  • 循环: 我们使用了 map 代替之前的 :for 标签,从源码里自动分析出 :for-item :for-index :key 等标签。

    • tsx:




{todoList.map((task, taskIndex) => (
<Text
hidden={!mixin.value}
key={task.id}
data-item-id={taskIndex}
data-num={20}
>

{taskIndex} {task}
</Text>

))}


  • 微信小程序:


<block
wx:for="{{ todoList }}"
wx:for-index="taskIndex"
wx:for-item="task"
wx:key="{{ task.id }}">
<!-- display: inline -->
<text
hidden="{{ !mixin.value }}"
data-item-id="{{ taskIndex }}"
data-num="{{ 20 }}"
>
{{ taskIndex }}{{ task }}</text
>
</block>



  • 支付宝小程序


  <block
a:for="{{ todoList }}"
a:for-index="taskIndex"
a:for-item="task"
a:key="{{ task.id }}">
<!-- display: inline -->
<text
hidden="{{ !mixin.value }}"
data-item-id="{{ taskIndex }}"
data-num="{{ 20 }}"
>
{{ taskIndex }}{{ task }}</text
>
</block>



  • 事件绑定: 我们会按照配置,自动将直接转换为两个平台的格式。

    • tsx:<Text onClick="handleClick" />

    • 微信小程序: <text bind:click="handleClick" />

    • 支付宝小程序: <text onClick="handleClick" />



  • 视图层脚本

    • 我们还规定了以 sjs.ts 作为视图层脚本的文件格式。 在编译时候转换为对应平台的文件格式。

      • tsx: import helper from './helper.sjs.ts'

      • 微信小程序: <wxs src="./helper.wxs" module="helper" />

      • 支付宝小程序: <import-sjs src="./helper.sjs" module="helper" />





  • 类型方案

    • 为了让逻辑层类型与视图层关联,我们设计了一些工具类型。 比如说下面使用的 TSXMLProps,将 IProps 的 onClick 转换成了字符串。




// Calendar.axml.tsx
import { TSXMLProps, View } from 'tsxml';

interface IProps {
className?: string;
style?: string;
onClick: (e) => void;
}

interface InternalData {
size: number;
}

export default (
{ className, style }: TSXMLProps<IProps>,
{ size }: InternalData
) => (
<View class={`ant-calendar ${className ? className : ''}`} style={style}>
{size}
</View>

);

// Page.axml.tsx

import Calendar from './Calendar.axml.tsx'

export default () => (<Calendar onClick="handleClick" />)

目前使用 tsx 的这套方案还存在一些限制:



  • 和小程序相同,一个文件内只能定义一个组件。

  • 如果使用自定义组件,需要配置组件事件在各个平台的写法。


老组件语法转换?用 AI 就行了


在决定使用 tsx 语法之后,我们还面临一个很棘手的工作量问题:如何把历史组件库 axml 代码全量转换为最新的 tsx 语法?
这时候就该 ChatGPT 出场了,我们请 AI 来帮助我们完成这个一次性转换工作。
为了让转换结果更靠谱,我们使用了一些技巧:



  • 使用了 tsx 编译器等测试用例作为 prompt ,让 AI 可以更好的了解 tsx 的写法。

  • 除了 tsx 文件以外,我们还将组件的 props.ts 与 config.json 加到了 propmt 里,可以帮助 AI 生成更好的 import 导入。


在这里,你可以看到这份转换的完整 prompt。


确保 AI 产出的正确性?再用我们的编译器转回来


为了确保 AI 产出的代码是正确的,我们使用编译器将 AI 编写的 tsx 重新编译回 axml ,再用 git diff 对原始代码做比对,由此即可核查 AI 转换的正确性。


当然,这两次转换的过程不会完全等价,比如转换 map 的过程中会出现一层嵌套的 <block/>。好在这样的差异不多,一般肉眼看一遍就能确认正确性了。


跨平台通用的组件逻辑:小程序函数式组件(functional-mini)


除了视图,我们还需要确保组件逻辑适配到双端。这里我们使用了小程序函数式组件( functional-mini )的形式来编写,functional-mini 的源码及文档放置均在 ant-design/functional-mini


使用了函数式组件后,Antd Mini 用上了计算属性、useEffect 等特性,也能通过 hooks 来替换原有的大量 mixin 实现,让代码的可维护性提升了一个台阶。


以典型的 Popover 组件为例,逻辑部分适配完成后,它的代码完全变成了 React 风格,数据变更流程一目了然:


const Popover = (props: IPopoverProps) => {
const [value] = useMergedState(props.defaultVisible, {
value: props.visible,
});
const [popoverStyle, setPopoverStyle] = useState({
popoverContentStyle: '',
adjustedPlacement: '',
});

useEffect(() => {
setPopoverStyle({
popoverContentStyle: '',
adjustedPlacement: '',
});
}, [value, props.autoAdjustOverflow, props.placement]);

return {
adjustedPlacement: popoverStyle.adjustedPlacement,
popoverContentStyle: popoverStyle.popoverContentStyle,
};
};

关于小程序函数式组件的原理、特性介绍,我们将在后续的分享中另行展开。


写在最后


欢迎大家一起来尝试 Ant Design Mini 的跨平台能力,你可以在 issue 区提出宝贵的建议与 Bug 反馈。


官网: mini.ant.design/


国内镜像:ant-design-mini.antgroup.com/


作者:支付宝体验科技
来源:juejin.cn/post/7311603519570952246
收起阅读 »

大厂前端开发规定,你也能写成诗一样的代码(保姆级教程)

web
BEM 使用起来很多人不晓得BEM是什么东西 我来解释给你们听  BEM是一种前端开发的命名方法论,全称为Block-Element-Modifier,中文翻译为块、元素、修饰符。BEM的主要思想是将页面分解成多个可重用、独立的模块,每个模块由一个主块(Blo...
继续阅读 »

BEM 使用起来

很多人不晓得BEM是什么东西 我来解释给你们听

  BEM是一种前端开发的命名方法论,全称为Block-Element-Modifier,中文翻译为块、元素、修饰符。BEM的主要思想是将页面分解成多个可重用、独立的模块,每个模块由一个主块(Block)和零个或多个元素(Element)组成,可以使用修饰符(Modifier)来描述模块的不同状态和变化。

BEM的命名规则如下:

  • 块(Block):一个独立的、可重用的组件,通常由一个或多个元素组成,并且有一个命名空间作为标识符。通常以单个单词命名,使用连字符分割单词,例如:menu、button、header等。
  • 元素(Element):块的组成部分,不能独立存在,必须隶属于某个块。元素的命名使用两个连字符“__”与块名分隔,例如:menu__item、button__text、header__logo等。
  • 修饰符(Modifier):描述块或元素的某种状态或变化,以单个单词或多个单词组成,使用两个连字符“--”与块或元素名分隔。例如:menu--horizontal、button--disabled、header__logo--small等。

  通过使用BEM命名方法论,可以实现更好的代码复用性、可维护性和可扩展性。BEM的命名规则清晰明了,易于理解和使用,可以有效地提高团队开发效率和代码质量。

page+ hd/bd/ft 用起来

"page+ hd/bd/ft" 是一种简化的命名约定,常用于网页布局中。下面是对这些缩写的解释:

  • page:页面的整体容器,表示整个页面的最外层包裹元素。
  • hd:代表页头(header),用于放置页面的标题、导航栏等顶部内容。
  • bd:代表页体(body),用于放置页面的主要内容,如文章、图片、表格等。
  • ft:代表页脚(footer),用于放置页面的底部内容,如版权信息、联系方式等。

  这种命名约定的好处是简洁明了,可以快速理解页面的结构和布局。通过将页面划分为页头、页体和页脚,可以更好地组织和管理页面的各个部分,提高代码的可读性和可维护性。

更好的使用工具(stylus插件)

,Stylus 是一种 CSS 预处理器,它允许你使用更加简洁、优雅的语法编写 CSS。通过在命令行中运行 npm i -g stylus 命令,你可以在全局范围内安装 Stylus,并开始使用它来编写样式文件。 .styl 是 Stylus 文件的扩展名,你可以使用 Stylus 编写样式规则。然后,你可以将这些编写好的 Stylus 文件转换为普通的 CSS 文件,以便在网页中使用。

  具体地说,你可以创建一个名为 common.styl 的文件,并在其中编写 Stylus 样式规则。然后,通过运行 stylus -w common.styl -o common.css 命令,你可以让 Stylus 监听 common.styl 文件的变化,并自动将其编译为 common.css 文件。

  以下是一份示例代码来说明这个过程:

  1. 创建 common.styl 文件,并在其中编写样式规则:
// common.styl
$primary-color = #ff0000

body
font-family Arial, sans-serif
background-color $primary-color

h1
color white
  1. 打开终端,进入 common.styl 文件所在的目录,运行以下命令:
Copy Code
stylus -w common.styl -o common.css

  这将启动 Stylus 监听模式,并将 common.styl 文件编译为 common.css 文件。每当你在 common.styl 文件中进行更改时,Stylus 将自动重新编译 common.css 文件,以反映出最新的样式更改。 请注意,为了运行上述命令,你需要先在全局范围内安装 Stylus,可以使用 npm i -g stylus 命令进行安装。

stylus的优点

  Stylus 作为一种 CSS 预处理器,在实际开发中有以下几个优点:

  1. 更加简洁、优雅的语法:Stylus 的语法比原生 CSS 更加简洁,可以让我们更快地编写样式规则,同时保持代码的可读性和可维护性。
  2. 变量和函数支持:Stylus 支持变量和函数,可以提高样式表的重用性和可维护性。通过使用变量和函数,我们可以在整个样式表中轻松更改颜色、字体等属性,而无需手动修改每个样式规则。
  3. 混合(Mixins)支持:Stylus 的混合功能允许我们将一个样式规则集合包装在一个可重用的块中,并将其应用于多个元素。这可以大大简化样式表的编写,并减少重复代码。
  4. 自动前缀处理:Stylus 可以自动添加适当的浏览器前缀,以确保样式规则在不同的浏览器中得到正确的渲染。
  5. 非常灵活的配置:Stylus 提供了非常灵活的配置选项,可以根据项目的需要启用或禁用不同的功能,例如自动压缩、源映射等。

  总之,Stylus 通过提供更加简洁、灵活的语法和功能,可以使我们更加高效地编写 CSS 样式表,并提高代码的可重用性和可维护性。

最后一个大招阿里的适配神器 flexible.js

  flexible.js 是一款由阿里巴巴的前端团队开发的移动端适配解决方案。它通过对 Viewport 的缩放和 rem 单位的使用,实现了在不同设备上的自适应布局。

具体来说,flexible.js 主要包括以下几个步骤:

  1. 根据屏幕的宽度计算出一个缩放比例,并将该值设置到 Viewport 的 meta 标签中。
  2. 计算出 1rem 对应的像素值,并将其动态设置到 HTML 元素的 font-size 属性中。
  3. 在 CSS 中使用 rem 单位来定义样式规则。这些规则会自动根据 HTML 元素的 font-size 属性进行适配。

  通过这种方式,我们可以实现在不同设备上的自适应布局。具体来说,我们只需要在 CSS 中使用 rem 单位来定义样式规则,而不需要关注具体的像素值。当页面在不同设备上打开时,flexible.js 会自动根据屏幕宽度和像素密度等信息进行适配,从而保证页面的布局和样式在不同设备上都可以得到正确的显示。

  需要注意的是,flexible.js 并不能完全解决移动端适配的所有问题,还有一些特殊情况需要我们手动处理。例如,一些图片或者 Canvas 等元素可能需要根据不同设备的像素密度进行缩放,而这些操作需要我们手动实现。不过,flexible.js 可以帮助我们简化移动端适配的工作,提高开发效率。

  下期我来教大家手动写适配器 喜欢的来个关注 点赞 这个也是以后写文章的动力所在 谢谢大家能观看我的文章 咱下期在见 拜拜


作者:扯蛋438
来源:juejin.cn/post/7303126570323443722

收起阅读 »

JS问题:简单的console.log不要再用了!试试这个

web
前端功能问题系列文章,点击上方合集↑ 序言 大家好,我是大澈! 本文约1500+字,整篇阅读大约需要3分钟。 本文主要内容分三部分,如果您只需要解决问题,请阅读第一、二部分即可。如果您有更多时间,进一步学习问题相关知识点,请阅读至第三部分。 1. 需求分析 一...
继续阅读 »

前端功能问题系列文章,点击上方合集↑


序言


大家好,我是大澈!


本文约1500+字,整篇阅读大约需要3分钟。


本文主要内容分三部分,如果您只需要解决问题,请阅读第一、二部分即可。如果您有更多时间,进一步学习问题相关知识点,请阅读至第三部分。


1. 需求分析


一般情况下,我们在项目中进行代码调试时,往往只会在逻辑中使用console.log进行控制台打印调试。


这种方式虽然比较常规直接,但是如果打印数据多了,就会导致你的控制台消息变得异常混乱。


所以,我们有了更好的选择,那就是console对象提供的其它API,来让我们能够更清晰的区分打印信息。


图片


2. 实现步骤


2.1 console.warn


当我们需要区分一些比较重要的打印信息时,可以使用warn进行警告提示。


图片



2.2 console.error


当我们需要区分一些异常错误的打印信息时,可以使用error进行错误提示。


图片


2.3 console.time/timeEnd


想看看一段代码运行需要多长时间,可以使用time


这对于需要一些时间的CPU密集型应用程序非常有用,例如神经网络或 HTML Canvas读取。


下面执行这段代码:


console.time("Loop timer")
for(let i = 0; i < 10000; i++){
    // Some code here
}
console.timeEnd("Loop timer")


结果如下:图片


2.4 console.trace


想看看函数的调用顺序是怎样的吗?可以使用trace


下面执行这段代码:



  function trace(){
    console.trace()
  }
  function randomFunction(){
      trace();
  }
  randomFunction()


setup中,randomFunction 调用trace,然后又调用console.trace


因此,当您调用 randomFunction 时,您将得到类似的输出,结果如下:


图片


2.5 console.group/groupEnd


当我们需要将一类打印信息进行分组时,可以使用group


下面执行这段代码:


console.group("My message group");

console.log("Test2!");
console.log("Test2!");
console.log("Test2!");

console.groupEnd()

结果如下:


图片



2.6 console.table


在控制台中打印表格信息,可以使用table


对!你没听错,就是让我们以表格形式展示打印信息。


如果使用log打印:


var person1 = {name: "Weirdo", age : "-23", hobby: "singing"}
var person2 = {name: "SomeName", age : "Infinity", hobby: "programming"}

console.log(person1, person2);

结果如下:


这样做是不是让数据看起来很混乱。


图片


反之,如果我们使用table输出:


var person1 = {name: "Weirdo", age : "-23", hobby: "singing"}
var person2 = {name: "SomeName", age : "Infinity", hobby: "programming"}

console.table({person1, person2})

结果如下:


怎么样!从来不知道控制台可以看起来如此干净,对吧!


图片


2.7 console.clear


最后,使用clear把控制台清空吧!


图片


3. 问题详解


3.1 可以自定义log的样式吗?


答案当然是可以的,只需要借助%c这个占位符。


%c 是console的占位符,用于指定输出样式或应用 CSS 样式到特定的输出文本。


但请注意,%c 占位符只在部分浏览器中支持,如 Chrome、Firefox 等。


通过使用 %c 占位符,可以在 console.log 中为特定的文本应用自定义的 CSS 样式。这样可以改变输出文本的颜色、字体、背景等样式属性,以便在控制台中以不同的样式突出显示特定的信息。


以下是使用%c 占位符应用样式的示例:


console.log("%c Hello, World!", 
  "color: red; font-weight: bold;border1px solid red;");

结果如下:


图片


通过使用 %c 占位符和自定义的样式规则,可以在控制台输出中以不同的样式突出显示特定的文本,使得输出更加清晰和易于识别。


这在调试和日志记录过程中非常有用,特别是当需要突出显示特定类型的信息或错误时。


结语


建立这个平台的初衷:



  • 打造一个仅包含前端问题的问答平台,让大家高效搜索处理同样问题。

  • 通过不断积累问题,一起练习逻辑思维,并顺便学习相关的知识点。

  • 遇到难题,遇到有共鸣的问题,一起讨论,一起沉淀,一起成长。

作者:程序员大澈
来源:juejin.cn/post/7310102466570321958
收起阅读 »

TS中,到底用`type`还是`interface`呢?

web
结论 直接说结论,用type一把梭即可,除非你要发布到npm,接下来我一一解释 为什么定义对象都要使用type呢? 如图所示,我鼠标悬浮后,并不知道里面是什么东西 只能获取结果时调出代码提示,或者ctrl + 鼠标左键进入,查看类型定义 那么我用type呢? ...
继续阅读 »

结论


直接说结论,用type一把梭即可,除非你要发布到npm,接下来我一一解释


为什么定义对象都要使用type呢?


如图所示,我鼠标悬浮后,并不知道里面是什么东西


只能获取结果时调出代码提示,或者ctrl + 鼠标左键进入,查看类型定义


那么我用type呢?


image.png


可以看到,现在鼠标悬浮能直接查看类型定义了


这一点是让我最受不了的,所以直接选择type即可


image.png


区别


1. 如何继承



先看看interface,通过extends关键字



image.png



type,则通过交叉类型。不过我认为interface好看点



image.png


2. 其他特性



interface重写时



  • 如果有不同的属性,则会添加;

  • 如果是相同的属性但是类型不同,则会报错;



这点有好有坏,当你不小心名字重复了,那你就容易出问题


但同时利于扩展,不过没有人会这么写吧?


直接去原来的接口添加属性不行吗?


唯一的场景,就是开发工具库后。别人使用你的工具时,可以为你扩展类型


image.png


3. type独有的优势


除了上面的悬浮能查看具体类型外,type还提供了很多的关键字使用,这是interface不具备的


比如in关键字,用来枚举类型


这里我写个删除属性的泛型,和Omit一样的,但是interface不支持


此外还有很多TS特有的关键字,都只能通过type使用,比如infer


不过这也符合直觉,因为interface就是定义一个类型而已


image.png


经过以上探讨,可以得出一个结论


平时开发可以都用type


发布工具库给别人用时,用interface


作者:寅时码
来源:juejin.cn/post/7304867327752912906
收起阅读 »

个人代码优化技巧

web
背景 贴近目前公司的业务,做的增删改查比较多。基本上都是做一些表格的业务系统比较多,因此在写的过程中,都会遇到一些优化的细点,仅供参考,觉得好的可以采纳,不好的欢迎提出来。 一、import按需加载 很多小伙伴肯定不少看到过,性能优化路由要用import(...
继续阅读 »

背景



贴近目前公司的业务,做的增删改查比较多。基本上都是做一些表格的业务系统比较多,因此在写的过程中,都会遇到一些优化的细点,仅供参考,觉得好的可以采纳,不好的欢迎提出来。



一、import按需加载


很多小伙伴肯定不少看到过,性能优化路由要用import('@/views/xxxx.vue')这样就可以按需加载了。
本身的vue-cli自动创建出来的时候也会有这一条语句。除了给路由优化之外呢,还有别的场景优化空间呢?那肯定有的啦。那就是结合<component/>自带的组件去一起实现。


场景呈现


正常情况下,做一个业务模块,都会分为【基础表】、【业务表】,一般情况下,用户维护好了基础表信息了之后,剩下的就是信息交叉复用,有可能在某个业务页面,我需要点击某个按钮后根据某个值到某个基础表的页面进行搜索信息,并勾选行信息。


<template>
<div>
<div class="count" @click="showDynamicComponent">按需加载页面</div>
<Modal title="动态数据" :visible="visible" @ok="()=>dynamicComponent=null">
<component :is="dynamicComponent" ref="dynamicRef"/>
</Modal>
</div>

</template>

<script>
import { Modal } from 'ant-design-vue'
export default {
components: {
Modal
},
data() {
return {
dynamicComponent: null,
visible: false
};
},
methods: {
showDynamicComponent() {
this.visible = true
import('@/views/baseInfo/a.vue').then(res=>{
this.dynamicComponent = res.module
})
},
},
};
</script>


最后通过this.$refs.dynamicRef这个方式来拿到组件的信息和方法。




二、表格维护


因为公司的做的系统报表比较多,这时候表头的数量和表单都是比较多的,恰好公司使用的UI框架是ant-design-vue,表头的数量达到40-50的时候,那么代码的占用函数就很大,而且在产品经常在开发阶段,定义的表头位置顺序变来变去,于是为了方便维护和开发,我封装成一个函数,我还没考虑过这个性能损耗问题,但是维护起来确实方便很多。


业务场景


举个例子,一个表头有用户姓名年龄,正常情况下,ant-design-vue表头是这么写的。


const columns = [{
dataIndex: 'username',
title: '用户'
}, {
dataIndex: 'realname',
title: '姓名'
}, {
dataIndex: 'age',
title: '年龄'
}]

数据少的时候,维护没有什么问题,倒是表头数量很多的时候,可能40-50个,一百个?大概是这个数,看起来就很费劲。因为自己业务确实遇到过这个问题,维护起来要么单独创建一个文件大概一百多行一点点找,要么就放在业务代码里,但是无论如何阅读性都很差。所以我想了个办法,把它平铺变成数组形式。


import { genTableColumnsUtil } from '@/utils/tableJs'
const columns = genTableColumnsUtil([
['username', '用户'],
['realname', '姓名'],
['age', '年龄'],
])

这时候是不是就好看多了?甚至这个可以做成二级表头,递归做嵌套。那额外的配置项拓展项怎么搞?


const columns = genTableColumnsUtil([
['username', '用户'],
['realname', '姓名'],
['age', '年龄'],
],
{username: { width: '20%' }})

我的做法就是在函数里面在穿多一个对象,这样就可以填充上去了。毕竟大多数字段只是展示而已,没有做太多的单元格定制化,如果要定制化,搜索对应的dataIndex就好了。


image.png


image.png


这时候调整顺序的时候,还有定制化的时候就阅读性就好很多。




三、依赖包单独抽离


性能优化不只是代码层面的优化,除了nginx配置http2,gzip...
单独抽离chunk包也可以达到加快访问速度的目的。


业务场景


// 在vue.config.js加入这段代码
module.exports = {
configureWebpack: config => {
// 分包,打包时将node_modules中的代码单独打包成一个chunk
config.optimization.splitChunks = {
maxInitialRequests: Infinity, // 一个入口最大的并行请求数,默认为3
minSize: 0, // 一个入口最小的请求数,默认为0
chunks: 'all', // async只针对异步chunk生效,all针对所有chunk生效,initial只针对初始chunk生效
cacheGr0ups: { // 这里开始设置缓存的 chunks
packVendor: { // key 为entry中定义的 入口名称
test: /[\\/]node_modules[\\/]/, // 正则规则验证,如果符合就提取 chunk
name(module) {
const packageName = module.context.match(
/[\\/]node_modules[\\/](.*?)([\\/]|$)/
)[1]
return `${packageName.replace('@', '')}`
}
}
}
}
}
}
}

最后在打包完了之后。可以查看一下。


image.png




四、thread-loader打包


业务场景


充分利用cpu核心数,进行快速打包、其实我也没感觉有多快。


 // 开启多线程打包
config.module
.rule('js')
.test(/\.js$/)
.use('thread-loader')
.loader('thread-loader')
.options({
// worker使用cpu核心数减1
workers: require('os').cpus().length - 1,
// 设置cacheDirectory
cacheDirectory: '.cache/thread-loader'
})
.end()



五、ECharts按需使用


业务场景


数字化是趋势,图形可视化在所难免,但往往我们有时候没做那么复杂的图形,可能只用到了饼图和柱状图,或者别的,怎么样都用不完ECharts更多的图形,ECharts是大家常用的图形化之一,ECharts第一步教程都是告诉我们,在
vue文件里


import * as echarts from 'echarts'

殊不知,我们用不到的图形都加载进来,打包的时候就可以看到,这玩意,3M多。
所以,看情况来加载图形配置


import * as echarts from 'echarts/core'

import { BarChart, LineChart, PieChart } from 'echarts/charts'

import {
TitleComponent,
TooltipComponent,
GridComponent,
LegendComponent,
ToolboxComponent,
} from 'echarts/components'

import { CanvasRenderer } from 'echarts/renderers'

echarts.use([
TitleComponent,
TooltipComponent,
GridComponent,
BarChart,
LineChart,
PieChart,
CanvasRenderer,
LegendComponent,
ToolboxComponent
])

export default echarts

通过vscode的包插件,可以看到引入的模块大小


image.png


作者:hhope
来源:juejin.cn/post/7309791510873784372
收起阅读 »

学会Grid之后,我觉得再也没有我搞不定的布局了

web
说到布局很多人的感觉应该都是恐惧,为此很多人都背过一些很经典的布局方案,例如:圣杯布局、双飞翼布局等非常耳熟的名词; 为了实现这些布局我们有很多种实现方案,例如:table布局、float布局、定位布局等,当然现在比较流行的肯定是flex布局; flex布局属...
继续阅读 »

说到布局很多人的感觉应该都是恐惧,为此很多人都背过一些很经典的布局方案,例如:圣杯布局双飞翼布局等非常耳熟的名词;


为了实现这些布局我们有很多种实现方案,例如:table布局float布局定位布局等,当然现在比较流行的肯定是flex布局


flex布局属于弹性布局,所谓弹性也可以理解为响应式布局,而同为响应式布局的还有Grid布局


Grid布局是一种二维布局,可以理解为flex布局的升级版,它的出现让我们在布局方面有了更多的选择,废话不多说,下面开始全程高能;



本篇不会过多介绍grid的基础内容,更多的是一些布局的实现方案和一些小技巧;



常见布局


所谓的常见布局只是我们在日常开发中经常会遇到的布局,例如:圣杯布局双飞翼布局这种名词我个人觉得不用太过于去在意;


因为这类布局最后的解释都会变成几行几列,内容在哪一行哪一列,而这些就非常直观的对标了grid的特性;


接下来我们来一起看看一些非常常见的布局,并且用grid来实现;


1. 顶部 + 内容


image.png


html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Titletitle>
<style>
html, body {
margin: 0;
padding: 0;
}

body {
display: grid;
grid-template-rows: 60px 1fr;
height: 100vh;
}

.header {
background-color: #039BE5;
}

.content {
background-color: #4FC3F7;
}

.header,
.content {
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
style>
head>
<body>
<div class="header">Headerdiv>
<div class="content">Contentdiv>
body>
html>


2. 顶部 + 内容 + 底部


image.png


html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Titletitle>
<style>
html, body {
margin: 0;
padding: 0;
}

body {
display: grid;
grid-template-rows: 60px 1fr 60px;
height: 100vh;
}

.header {
background-color: #039BE5;
}

.content {
background-color: #4FC3F7;
}

.footer {
background-color: #039BE5;
}

.header,
.content,
.footer {
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
style>
head>
<body>
<div class="header">Headerdiv>
<div class="content">Contentdiv>
<div class="footer">Footerdiv>
body>
html>


这里示例和上面的示例唯一的区别就是多了一个footer,但是我们可以看到代码并没有多少变化,这就是grid的强大之处;


可以看码上掘金的效果,这里的内容区域是单独滚动的,从而实现了headerfooter固定,内容区域滚动的效果;


实现这个效果也非常简单,只需要在content上加上overflow: auto即可;




3. 左侧 + 内容


image.png


html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Titletitle>
<style>
html, body {
margin: 0;
padding: 0;
}

body {
display: grid;
grid-template-columns: 240px 1fr;
height: 100vh;
}

.left {
background-color: #039BE5;
}

.content {
background-color: #4FC3F7;
}

.left,
.content {
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}


style>
head>
<body>
<div class="left">Leftdiv>
<div class="content">Contentdiv>
body>
html>


这个示例效果其实和第一个是类似的,只不过是把grid-template-rows换成了grid-template-columns,这里就不提供码上掘金的示例了;



4. 顶部 + 左侧 + 内容


image.png


html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Titletitle>
<style>
html, body {
margin: 0;
padding: 0;
}

body {
display: grid;
grid-template-rows: 60px 1fr;
grid-template-columns: 240px 1fr;
height: 100vh;
}

.header {
grid-column: 1 / 3;
background-color: #039BE5;
}

.left {
background-color: #4FC3F7;
}

.content {
background-color: #99CCFF;
}

.header,
.left,
.content {
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}

style>
head>
<body>
<div class="header">Headerdiv>
<div class="left">Leftdiv>
<div class="content">Contentdiv>
body>
html>


这个示例不同点在于header占据了两列,这里我们可以使用grid-column来实现,grid-column的值是start / end,例如:1 / 3表示从第一列到第三列;


如果确定这一列是占满整行的,那么我们可以使用1 / -1来表示,这样如果后续变成顶部 + 左侧 + 内容 + 右侧的布局,那么header就不需要修改了;



5. 顶部 + 左侧 + 内容 + 底部


image.png


html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Titletitle>
<style>
html, body {
margin: 0;
padding: 0;
}

body {
display: grid;
grid-template-areas:
"header header"
"left content"
"left footer";
grid-template-rows: 60px 1fr 60px;
grid-template-columns: 240px 1fr;
height: 100vh;
}

.header {
grid-area: header;
background-color: #039BE5;
}

.left {
grid-area: left;
background-color: #4FC3F7;
}

.content {
grid-area: content;
background-color: #99CCFF;
}

.footer {
grid-area: footer;
background-color: #6699CC;
}

.header,
.left,
.content,
.footer {
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
style>
head>
<body>
<div class="header">Headerdiv>
<div class="left">Leftdiv>
<div class="content">Contentdiv>
<div class="footer">Footerdiv>
body>
html>


这个示例的小技巧是使用了grid-template-areas,使用这个属性可以让我们通过代码来直观的看到布局的样式;


这里的值是一个字符串,每一行代表一行,每个字符代表一列,例如:"header header"表示第一行的两列都是header,这里的header是我们自己定义的,可以是任意值;


定义好了之后就可以在对应的元素上使用grid-area来指定对应的区域,这里的值就是我们在grid-template-areas中定义的值;





码上掘金中的效果可以看到,左侧的菜单和内容区域都是单独滚动的,这里的实现方式和第二个示例是一样的,只需要需要滚动的元素上加上overflow: auto即可;



响应式布局


响应式布局指的是页面的布局会随着屏幕的大小而变化,这里的变化可以是内容区域大小可以自动调整,也可以是页面布局随着屏幕大小进行自动调整;


这里我就用掘金的页面来举例,这里只提供一个思路,所以不会像上面那样提供那么多示例;


1. 基础布局实现


移动端布局


image.png



以移动端的效果开始,掘金的移动端的布局就是上面的效果,这里我简单的将页面分为了三个部分,分别是headernavigationcontent


注:这里不是要100%还原掘金的页面,只是为了演示grid布局,具体页面结构和最后实现的效果会有非常大的差异,这里只会实现一些基础的布局;



html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Titletitle>
<style>
html, body {
margin: 0;
padding: 0;
}

body {
display: grid;
grid-template-areas:
"header"
"navigation"
"content";
grid-template-rows: 60px 48px 1fr;
height: 100vh;
}

.header {
grid-area: header;
background-color: #039BE5;
}

.navigation {
grid-area: navigation;
background-color: #4FC3F7;
}

.content {
grid-area: content;
background-color: #99CCFF;
}


.header,
.navigation,
.content {
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}

style>
head>
<body>
<div class="header">Headerdiv>
<div class="navigation">Navigationdiv>
<div class="content">Contentdiv>
body>
html>

iPad布局


image.png



这里是需要借助媒体查询来实现的,在媒体查询中只需要调整一下grid-template-rowsgrid-template-columns的值即可;


由于这里的效果是上面一个的延伸,为了阅读体验会移除上面相关的css代码,只保留需要修改的代码;



html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Titletitle>
<style>

.right {
display: none;
background-color: #6699CC;
}

@media (min-width: 1000px) {
body {
grid-template-areas:
"header header"
"navigation navigation"
"content right";
grid-template-columns: 1fr 260px;
}

.right {
grid-area: right;

display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
}
style>
head>
<body>
<div class="header">Headerdiv>
<div class="navigation">Navigationdiv>
<div class="content">Contentdiv>
<div class="right">Rightdiv>
body>
html>

PC端布局


image.png



和上面处理方式相同,由于Navigation移动到了左侧,所以还要额外的修改一下grid-template-areas的值;


这里就可以体现grid的强大之处了,我们可以简单的修改grid-template-areas就可以实现一个完全不同的布局,而且代码量非常少;


为了居中显示内容,我们需要在左右两侧加上一些空白区域,可以简单的使用.来实现,这里的.表示一个空白区域;


由于内容的宽度基本上是固定的,所以留白区域简单的使用1fr进行占位即可,这样就可以平均的分配剩余的空间;



html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Titletitle>
<style>
@media (min-width: 1220px) {
body {
grid-template-areas:
"header header header header header"
". navigation content right .";
grid-template-columns: 1fr 180px minmax(0, 720px) 260px 1fr;
grid-template-rows: 60px 1fr;
}
}
style>
head>
<body>
<div class="header">Headerdiv>
<div class="navigation">Navigationdiv>
<div class="content">Contentdiv>
<div class="right">Rightdiv>
body>
html>

完善一些细节


QQ录屏20231210000552.gif



最终的布局大概就是上图这样,这里主要处理的各个版块的间距和响应式内容区域的大小,这里的处理方式主要是使用column-gap和一个空的区域进行占位来实现的;


这里的column-gap表示列与列之间的间距,值可以是pxemrem等基本的长度属性值,也可以使用计算函数,但是不能使用弹性值fr


空区域进行占位留间距其实我并不推荐,这里只是演示grid布局可以实现的一些功能,具体的实现方式还是要根据实际情况来定,这里我更推荐使用margin来实现;


完整代码如下:



html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Titletitle>
<style>
html, body {
margin: 0;
padding: 0;
}

body {
display: grid;
grid-template-areas:
"header header header"
"navigation navigation navigation"
". . ."
". content .";
grid-template-columns: 1fr minmax(0, 720px) 1fr;
grid-template-rows: 60px 48px 10px 1fr;
column-gap: 10px;
height: 100vh;
}

.header {
grid-area: header;
background-color: #039BE5;
}

.navigation {
grid-area: navigation;
background-color: #4FC3F7;
}

.content {
grid-area: content;
background-color: #99CCFF;
}

.right {
display: none;
background-color: #6699CC;
}

@media (min-width: 1000px) {
body {
grid-template-areas:
"header header header header"
"navigation navigation navigation navigation"
". . . ."
". content right .";
grid-template-columns: 1fr minmax(0, 720px) 260px 1fr;
}

.right {
grid-area: right;

display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
}

@media (min-width: 1220px) {
body {
grid-template-areas:
"header header header header header"
". . . . ."
". navigation content right .";
grid-template-columns: 1fr 180px minmax(0, 720px) 260px 1fr;
grid-template-rows: 60px 10px 1fr;
}
}

.header,
.navigation,
.content {
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}

style>
head>
<body>
<div class="header">Headerdiv>
<div class="navigation">Navigationdiv>
<div class="content">Contentdiv>
<div class="right">Rightdiv>
body>
html>

简单复刻版




码上掘金上的效果来说已经完成了大部分的布局和一些效果,目前来说就是还差一些交互,还有一些细节上的处理,感兴趣的同学可以自行完善;



异型布局


异性布局指的是页面中的元素不是按照常规的流式布局进行排版,又或者说不规则的布局,这里我简单的列出几个布局,来看看grid是如何实现的;


1. 照片墙


image.png


html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Titletitle>
<style>
html, body {
margin: 0;
padding: 0;
background: #f2f3f5;
overflow: auto;
}

body {
display: grid;
grid-template-columns: repeat(12, 100px);
grid-auto-rows: 100px;
place-content: center;
gap: 6px;
height: 100vh;
}

.photo-item {
width: 200px;
height: 200px;
clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%);
}

style>
head>
<body>

body>
<script>
function randomColor() {
return '#' + Math.random().toString(16).substr(-6);
}

let row = 1;
let col = 1;
for (let i = 0; i < 28; i++) {
const div = document.createElement('div');
div.className = 'photo-item';
div.style.backgroundColor = randomColor();
div.style.gridRow = `${row} / ${row + 2}`;
div.style.gridColumn = `${col} / ${col + 2}`;

document.body.appendChild(div);
col += 2;
if (col > 11) {
row += 1;
col = row % 2 === 0 ? 2 : 1;
}
}
script>
html>


这是一个非常简单的照片墙效果,如果不使用grid的话,我们大概率是会使用定位去实现这个效果,但是换成grid的话就非常简单了;


而且代码量是非常少的,这里就不提供码上掘金的 demo 了,感兴趣的同学可以将代码复制下来自行查看效果;



2. 漫画效果


image.png




在漫画中有很多类似这种不规则的漫画框,如果使用定位的话,那么代码量会非常大,而且还需要计算一些位置,使用grid的话就非常简单了;


可以看到这里还有一个气泡文字显示的效果,按照页面书写顺序,气泡是不会显示的,这里我们可以使用z-index来实现,这里的z-index值越大,元素就越靠前;


而且气泡文字效果也是通过grid来进行排版的,并没有使用其他的布局来实现,代码量也并不多,感兴趣的同学可以自行查看;



3. 画报效果


image.png




在一个画报中,我们经常会看到文字和图片混合排版的效果,由于这里直接使用的是渐变的背景,而且我的文字都是随意进行排列的,没有什么规律,所以看起来会比较混乱;


在画报效果中看似文字排版非常混乱不规则,但是实际上设计师在设计的时候也是会划分区域的,当然用定位也是没问题的,但是使用grid的话就会简单很多;


我这里将页面划分为12 * 12区域的网格,然后依次对不同的元素进行单独排列和样式的设置;



流式布局


流式布局指的是页面的内容会随着屏幕的大小而变化,流式布局也可以理解为响应式布局;


但是不同于响应式布局的是,流式布局的布局不会像响应式布局那样发生变化,只是内容会随着轴进行流动;


通常这种指的是grid-template-columns: repeat(auto-fit, minmax(0, 1fr))这种;


直接看效果:


QQ录屏20231210222012.gif




这里有两个关键字,一个是auto-fit,还有一个是auto-fill,在行为上它们是相同的,不同的是它们在网格创建的不同,



image.png



就像上面图中看到的一样,使用auto-fit会将空的网格进行折叠,可以看到他们的结束colum的数字都是6;


像我们上面的实例中不会出现这个问题,因为我们使用了响应式单位fr,只有使用固定单位才会出现这个现象;


感兴趣的同学可以将minmax(200px, 1fr)换成200px尝试;



对比 Flex 布局


在我上面介绍了这么多的布局场景和案例,其实可以很明显的发现一件事,那就是我使用grid进行的布局基本上都是大框架;


当然上面也有一些布局使用flex也是可以实现的,但是我们再换个思路,除了flex可以做到上面的一些布局,float布局、table布局、定位布局其实也都能实现;


不同的是float布局、table布局、定位布局基本上都是一些hack的方案,就拿table布局来说,table本身就是一个html标签,作用就是用来绘制表格,被拿来当做布局的一种方案也是迫不得已;


web布局发展到现在的我们有了正儿八经可以布局的方案flex,为什么又要出一个grid呢?


grid的出现绝对不是用来替代flex的,在我上面的实现的一些布局案例中,也可以看到我还会使用flex


我个人理解的是使用grid进行主体的大框架的搭建,flex作为一些小组件的布局控制,两者搭配使用;


flex能实现一些grid不好实现的布局,同样grid也可以实现flex实现困难的布局;


本身它们的定位就不痛,flex作为一维布局的首选,grid定位就是比flex高一个维度,它的定位是二维布局,所以他们之间没有必要进行对比,合理使用就好;


总结


上面介绍的这么多基于grid布局实现的布局方案,足以看出grid布局的强大;


grid布局的体系非常庞大,本文只是梳理出一些常见的布局场景,通过grid布局去实现这些布局,来体会grid带来的便利;


可能需要完全理解我上面的全部示例需要对grid有一定的了解才可以,但是都看到这里了,不妨去深挖一下;


grid布局作为一项强大的布局技术,有望在未来继续发展,除了我上面说到的布局,grid还有很多小技巧来实现非常多的布局场景;


碍于我的见识和文笔的限制,我这次介绍grid肯定是有很多不足的,但是还是希望这篇文章能为你对于布局相关能有新的认识;


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

今天还要用 React 吗:利弊权衡

web
免责声明 本文属于是语冰的直男翻译了属于是,略有删改,仅供粉丝参考,英文原味版请临幸 The Pros and Cons of Using React Today。 在过去的十年中,React 因辅助整个行业的开发者构建顶尖 UI 的超能力,为大家所熟知。...
继续阅读 »




免责声明


本文属于是语冰的直男翻译了属于是,略有删改,仅供粉丝参考,英文原味版请临幸 The Pros and Cons of Using React Today



00-wall.jpg


在过去的十年中,React 因辅助整个行业的开发者构建顶尖 UI 的超能力,为大家所熟知。


本文在 2023 年底和 2024 年对 React 进行了深入而平衡的展望。我们将看看它值得称道的优势、明显的短板,以及对当今开发者的可靠性。


让我们先从 React 与众不同的创新功能开始,然后再将注意力转向它给开发者带来的挑战。


React JS 是什么鬼物?


ReactJS 是一个组件筑基的 JS 库,最初由 Facebook 创建,并在十年前发布。该库简化了开发者创建交互式 UI,同时有效管理组件状态。它能够为复杂 App 编写多个组件,而不会丢失它们在浏览器的 DOM(文档对象模型)中的状态,这对一大坨开发者而言是一个明显的福利。


虽然 React 主要是一个用于 Web App 的工具,但其多功能性通过 React Native 扩展到移动 App 开发。这个强大的开源库允许开发 Android、iOS 和 Windows App,展示了 React 跨平台开发的灵活性。


React 生态系统


React 最大的资源之一是其庞大的生态系统,其中充满了第三方库和工具,极大地扩展了其功能。这对于路线规划 App 等复杂项目尤其有利,这些项目通常依赖集成大量外部服务,比如地图 API 和路径算法。


React 的灵活性和与各种第三方服务的兼容性简化了集成过程,允许开发者使用高级功能增强其 App,而不会产生过多的开销。


其核心是基本的库和工具,比如 React Router,用于 SPA(单页应用程序)中的动态路由,确保无缝的 UX(用户体验)过渡。Redux 是一个关键的状态管理工具,它为状态创建了一个中心化 store,使不同的组件能够一致地访问和更新它,这在大型 App 中尤为重要。


React.js:不仅仅是复杂性


虽然 React 在 UI 创建方面表现出色,但在状态管理和 SEO 优化等领域存在不足。幸运的是,更广泛的 JS 生态系统提供了许多工具,这些工具好处多多,比如更简化的状态管理方案、通过 SSR(服务器端渲染)增强的 SEO 和数据库管理。让我们瞄一下 React 若干更突出的集成选项。


对于那些寻求更简单替代方案的人而言,MobX 提供了一种直观的状态管理方案,并且样板更少。此外,Next.js 通过提供 SSR 和 SSG(静态站点生成)解决了客户端渲染 App 的当前 SEO 限制。在开发和测试方面,CRA(Create React App)简化了设置新前端构建管道的过程,使开发者能够立即开始运行,而不会受到配置的困扰。


同时,Storybook 作为一个 UI 开发环境,开发者可以在其中独立可视化其 UI 组件的不同状态。Jest 在单元和快照测试中很受欢迎,它与 React 无缝集成。由 Airbnb 开发的 Enzyme 是一个测试工具,它简化了断言、操作和遍历 React 组件输出的过程。


额外的库和工具进一步丰富了 React 生态系统;Material-UI 和 Ant Design 提供了全面的 UI 框架,可以满足美学和功能要求,而 Axios 则提供了一个 Promise 筑基的 HTTP 客户端来发送 HTTP 请求。React Query 简化了获取、缓存和更新异步数据的过程,React Helmet 有助于管理对文档头的更改,这对于 SPA 中的 SEO 至关重要。


React 与其他技术的集成 —— 比如后端框架,包括 Node.js 和 Django;状态管理库,比如 Apollo for GraphQL,增强了其灵活性。如今,开发者甚至可以将 PDF 查看器嵌入到网站中,并大大优化 UX。


然而,React 的不断发展要求开发者跟上最新的变化和进步,React 为试图制作高质量、可扩展和可维护的 Web App 的开发者提供的无数解决方案抵消了这一挑战。


React 之利


React 已经将自己确立为构建动态和响应式 Web App 的关键库,原因如下:


组件筑基架构


传统的 JS App 在扩展时经常会遇到状态管理问题。虽然但是,React 提供了复杂的、独立维护的可复用组件,允许开发者在不影响其他页面的情况下更新网页的局部 —— 确保松耦合和协作功能。


当然,这个概念并不是 React 独有的;举个栗子,Angular 也使用组件作为基本构建块。尽管如此,React 庞大的社区、Meta 的支持和相对丝滑的学习曲线使其成为开发者的最爱。


开发中的增强定制


React 的多功能性在构建针对特定业务需求量身定制的 App 时大放异彩。尤其是其组件筑基架构允许在 App 中无缝组装复杂结构。


举个栗子,在构建集成仪表板时,React 的生态系统有助于将各种模块(比如图表、小部件和实时数据源)集成到一个有凝聚力的 UI 中,使开发者能够打造不仅功能强大,而且直观且具有视觉吸引力的 UX。


这种强大的适应性恰恰凸显了为什么 React 仍然是旨在创建多功能和健壮的 Web App 的开发者的首选。


面向未来的开发者选项


React 面向未来的特性是它为开发者提供的最引人注目的优势之一。React 灵活的架构迎合了当前的 Web 开发需求,同时也无缝地适应了将塑造行业近期的新兴技术。


值得注意的是,机器学习正在向 Web 开发领域取得重大进展,2022 年全球 ML 市场价值已经达到 210 亿美元,这凸显了 React 面向未来的特性以及与此类进步相协调的能力的重要性。


其中一个比较突出的例子是 TensorFlow.js,一个用于图像和模式识别的 ML 库。同样,React 允许集成 ML 驱动的聊天机器人甚至推荐功能。此外,WebAssembly 可以帮助允许用 Rust、Python 或 C++ 编码的 ML 应用程序存在于原生 App 中。


用于状态管理的 Redux


在 SPA 中,多个组件驻留在单个页面上,管理状态和组件间通信很快就会变得具有挑战性 —— 这正是 Redux for React 的亮点。


作为 React 不可或缺的一部分,它充当“管理器”,确保组件之间的数据流一致且准确,集中状态管理并促进组件自治性,显着提高数据稳定性和 UX。


React 之弊


虽然 React 为不同技能水平的开发者提供了许多优势,但它并非没有各自的缺点,包括以下内容:



  • 复杂的概念和高级模式:React 引入了若干高级概念和模式,这些概念和模式一开始可能会让初学者不知所措。要了解 JSX、组件、props、状态管理、生命周期方法和钩子,需要扎实掌握 JS 基础知识。

  • 与其他技术的集成复杂性:React 经常与其他工具和技术结合使用——如 Redux、React Router 和各种中间件 —— 对于新手来说,了解如何将这些技术与 React 集成可能极具挑战。

  • 非 JS 开发者的障碍:React 对 JS 的严重依赖对于不精通 JS 的开发者而言可能是一个障碍。虽然 JS 是一种通用且广泛使用的语言,但来自不同编程背景的开发者可能会发现适应 JS 的范式和 React 的使用方式极具挑战。

  • 不是一个成熟的框架:React 主要处理 MVC 的“视图”部分,也称为模型视图控制器架构。对于“模型”和“控制器”,需要额外的库,与 Angular 等功能齐全的框架相比,这最终会导致结构化程度较低且可能更加混乱的代码。

  • 代码膨胀:React.js 的特点是其大量的库和依赖需求,因其臃肿的 App 而臭名昭著。这种膨胀通常表现为较长的加载时间,尤其是在复杂的项目中。该框架的结构严重依赖其虚拟 DOM,即使是次要功能也需要加载整个库,这大大增加了 App 的数字足迹并降低了其效率。

  • 在传统设备和弱网络上的性能下降:React.js App 的性能在较旧的硬件和互联网连接较差的地区往往会下降。这主要是由于框架的客户端渲染模型和密集的 JS 处理。这些因素可能会导致渲染交互式元素的延迟,这在计算能力有限的设备或带宽有限的环境中尤为明显,这会对 UX 产生不利影响。


最终裁决


随着 Web 开发领域的不断发展,React 的灵活性和强大的生态系统使其处于有利地位。它将继续使开发者能够将尖端功能无缝地整合到其 App 中。虽然但是,虽然 React 为开发者提供了很多好处,但它仍然有其缺点。


React 的复杂性和对高级 JS 概念的依赖带来了曲折的学习曲线,尤其是对于新手或尚未精通 JS 的人。它还主要解决了 MVC 架构的“视图”方面,需要额外的工具来进行完整的 App 开发,这可能会导致更复杂和结构化更少的代码库。


尽管存在这些挑战,但庞大而活跃的 React 社区在其持续发展中发挥着至关重要的作用。在可预见的未来,它将继续成为 Web 和移动 App 开发的关键库。


作者:人猫神话
来源:juejin.cn/post/7310033153905164303
收起阅读 »

一个30岁老前端的人生经历(学习+工作+婚姻+孩子),给迷茫的朋友一点激励。

web
前言 我93年的,还差几天就到30周岁生日。做前端开发大概也有6、7年,算是老前端了。 2023 写作 在掘金看文章已经很多年了,学到了很多东西。今年三月份在掘金写下了第一篇文章,开始的想法是把自己的开发笔记整理一下写成文章,来巩固一下自己的知识,没想到在写文...
继续阅读 »

前言


我93年的,还差几天就到30周岁生日。做前端开发大概也有6、7年,算是老前端了。


2023


写作


在掘金看文章已经很多年了,学到了很多东西。今年三月份在掘金写下了第一篇文章,开始的想法是把自己的开发笔记整理一下写成文章,来巩固一下自己的知识,没想到在写文章分享的过程中帮助了很多人,也得到了很多人认可,让我写文章分享的动力也越来越强,基本每周都会写一篇,闲在没事的时候基本都是在构思下一篇文章写什么。希望明年能写更多的文章,帮助更多的人。


写作给我带来了什么



  1. 巩固知识。把自己学到的东西,分享出去,印象更深刻了。

  2. 更多的机会。写作过程中,有大厂私信我,给我一些面试机会,这个时候的面试机会还是很宝贵的,不过因为一些原因都拒绝了。

  3. 快乐。很多人私信我说很感谢我分享的东西,让他们学到了很多东西。看到这些感谢的话,自己得到认可,还是很开心的。

  4. 钱。写文章参与掘金的金石计划活动,陆陆续续差不多获得了接近1000的收入,每次拿到钱,带着老婆孩子去吃顿好的,还是不错的。


降薪


今年公司受大环境影响,裁了一部分人,留下来的人也都降薪了。开始有点接受不了,想跑,但是因为是在上海嘉定郊区,附近找不到好的工作,最近的也要1个小时的地铁,并且小孩刚上一年级,上海基本不可能跨区转校,所以打消了换工作的念头,只能在公司了干下去,相信公司会好起来的。


家庭


看了上面肯定有倔友怀疑,30岁小孩怎么能上一年级?不错我大四结的婚,还是奉子成婚,所以早早的有了孩子。


因为自己小时候是留守儿童,不想让自己孩子过留守儿童的生活,所以孩子一直是和我们在一起,记得我刚出来工作的时候,一个月才3500,我老婆全职带孩子,这些薪资刚够花销,生活过的比较拮据,有时候还要靠我父母接济。


现在收入稍微好了一些,但是我们还没有买房子,存款也不多,在别人看来压力可能会有点大,但是我心比较大,平时消费欲望也比较低,对钱不是那么渴望,一家人在一起也是开开心心的,不过还是想给老婆孩子一个自己的家,努力奋斗吧。


孩子今年上一年级了,再上一年级后,作业明显变多了,每天都要写到很晚,看着孩子很累,也没办法,不写好作业,第二天老师就会在群里点名说。


孩子比较调皮,经常在学校里和同学打架,最多的时候,一周被班主任叫了三次家长,有时候是他的错,有时候是别人的错。因为老婆全职在家带孩子,这些都是我老婆处理的,她最近有点焦虑,每天都担惊受怕的,害怕孩子在学校又闯祸,整的我也有点焦虑,工作状态有点差。


关于孩子打架的事,我和老婆猜测可能是学习压力太大了,每天放学回来就开始写作业,一直写到睡觉,平时还有一些兴趣班要上,玩的时间太少了,积累了很多怨气没地方发泄,所以比较暴躁。现在每天放学后先让他玩半个小时再写作业,并且和他多次沟通,告诉他暴力解决问题是不对的,目前稍微好了一些。有这方面经验的兄弟,可以在评论区指点一下。


健康


今年五月份的时候,身体有点不舒服,平时熬夜比较多,人也比较胖,就想着去体检一下,体检结果肝功能有一项转氨酶比正常高三倍,然后到医院做了一次全身体检,抽了9管血,结果还好没啥大问题,可能是脂肪肝导致的转氨酶很高,医生建议要减肥。


因为平时比较忙,没有时间健身,就搞了个自行车上下班骑,公司离家大概5公里左右,上下班每天骑10公里左右,从6月份买车到现在基本没断过,虽然体重没有降下来,但是精神状态和体力好了不少,以前稍微有点运动量,就气喘吁吁全身冒汗,现在好多了。


希望倔友们多注意健康,少熬夜,身体才是最重要的。


2024展望


关于2024,立几个flag吧



  1. 最少分享40篇文章

  2. 完善fluxy-admin平台,把前后端低代码平台集成进来,做出一个企业级低代码平台开源出去。

  3. 看react源码,并做个专栏分享。每次面试,被面试官问react底层一些东西的时候,回答的都不是很好,就是因为没有彻底了解底层,所以回答的都很片面,明年一定要把react吃透。今年年初买了卡颂大佬的react设计原理书籍,现在在床头吃灰呢。

  4. 减肥


个人真实经历分享


给大家分享一下我的个人真实经历,与君共勉。


我出生在一个很普通的农村家庭,有点小聪明,但是贪玩,高中三年基本都是看电子书度过,天天上课把手机放在书下面,装作看书,实际上都是在看小说,现在回想起来,想不通老师为啥重来没有发现过。


开始决定好好学习是高三上学期,有次上课和同桌说话,被老师说你自己不学,不要影响别人学习,还说了一些很难听的话(当时我在班里大概倒数10几名的样子,同桌10几名左右。),虽然我贪玩,但是我自尊心比较强,我就不服气,然后上课开始好好听课,后面一次月考竟然考到了10几名,和同桌成绩差不多,然后就开始飘了,上课又开始看小说,下次月考又考的很差,然后难受,又开始好好听课,就这样成绩一会好一会坏,不过拿了几次进步奖,同学笑话我是不是为了拿进去奖,故意退步的。


真正让我决定好好学习的是高三下学期开学的前一天晚上,我家庭条件不是特别好,而我当时因为中考考的很差,只能上一个学费比较贵的私立高中,高三下学期开学前一天晚上我爸还在为我筹学费(家里没有穷到付不起学费的地步,只是当前家里钱被其他地方占用了,拿不出来。),最终从亲戚那里借了点钱,然后我爸把钱交到我手里,让我明天交学费,看着我爸粗糙的手(我爸是干工地的),这一刻我决定好好学习,不然都对不起这学费。高三下学期上课就没看过手机了,由于底子太差,高考离二本线差了几分,最终上了个三本。


高考结束,暑假期间迷上了英雄联盟。大学的时候,室友也玩,经常和室友一块包夜,第二天要么旷课在宿舍睡觉,要么在教室最后一排睡觉,导致第一学期就挂了三科,不过后面补考都过了。后面还是继续玩,大二下学期突然觉得不能这样浑浑噩噩了,还不如出去打工,给家里省点学费还能挣点钱(不知道当时为啥有这想法),然后就和父母说了一下,不上学了出去打工,当时是想退学的,还好我好朋友和我说先休学吧,以后后悔还有机会。


在苏州找了一个工厂,干了一个星期干不下去了,身体上的劳累倒是其次,主要是看不到生活的希望,每天就像一个机器一样,后面就回去上学了,然后学习非常努力,后面还得了奖学金,毕业论文也被评上了优秀论文,也是优秀毕业生,但是毕业学校没有给学位证,只给了毕-业-证,因为挂科超过5门(补考过了也没用,只要挂科超过5门,就完了,我们那一届有不少没有学位证的。),这个政策最开始都不知道,没有学位证后问学校,学校才说的,也不能怨学校,算是自食其果吧。没有学位证对找工作还是有很大影响的,后面有几次面试通过大厂了,因为没有学位证而被拒。


实习的时候,实习单位和学校是有合作的,学校知道我的事迹也知道我在实习单位表现的不错,所以就邀请我回去给学弟学妹们分享我的经历。当时分享完后,有几个学弟加我微信说,他们现在也是这个状态,我的经历让他们有了重新开始的信心。


后面的工作之旅也是一路坎坷,不过最后的结果是好的,目前在公司里做前端负责人,收入还不错。工作之旅明年年终总结再和大家分享吧。


和大家分享我的经历,就是想告诉大家永远不要放弃,只要坚持,就会有希望,同时也想告诉大家每个人都要为自己做过的事负责,因为贪玩我没考上好一点的大学,因为贪玩我没有学位证,但是我后面迷途知返,通过自己的努力,还是得到了一份不错的工作,一个美满的家庭。


最后


很喜欢deft在夺冠时说的一句话:我唯一会的仅剩英雄联盟,如果在这条路上我不能成功,那我的人生将没有任何意义。


而我唯一会的就是写代码,我不一定能成功,但是我想努力做到更好。


作者:前端小付
来源:juejin.cn/post/7310549035965890614
收起阅读 »

客户要一次加载上千张轮播图如何丝滑地实现?不用第三方(没找到),十来行核心代码实现

web
引言 最近再做3D的大屏,然后客户发来1G的图片压缩包,让我全放上去当轮播图 这不得卡死啊,现成且现代化好用的第三方库没找到 于是又到了我最爱的实现源码环节,核心代码十多行即可 底部有源码 思路 压缩图片 轮播只需要两张,来回交换,用点障眼法就是无缝了 批...
继续阅读 »

引言


最近再做3D的大屏,然后客户发来1G的图片压缩包,让我全放上去当轮播图


这不得卡死啊,现成且现代化好用的第三方库没找到


于是又到了我最爱的实现源码环节,核心代码十多行即可


底部有源码


思路



  • 压缩图片

  • 轮播只需要两张,来回交换,用点障眼法就是无缝了


批量压缩


这个用canvas就能实现,于是我写了个HTML来批量压缩


canvas转存图片时,使用jpeg | webp的格式即可压缩,MDN上有


使用canvas.toBlob可以压缩更多空间,这个不是重点,提一嘴而已


image.png


虚拟无缝轮播实现


直接一张动图,清晰明了的解决问题


t.gif


是不是看起来很简单,加载前两张图,当动画结束时,改变移动那张图的src,同时位移


再加个overflow: hidden不就行了吗


e.gif


编码实现


HTMLCSS我就不贴了,这个没什么难度,容器固定大小,子元素排列一行


然后给包装的容器添加个transform即可


下面是用vue3写的,定义了一个imgIndexArr数组装当前要显示的索引


_data为计算属性,根据imgIndexArr自动变化,里面放的就是图片


我们只需要修改imgIndexArr即可实现数据切换


image.png


我们需要在动画完成时改变,即添加ontransitionend事件


当触发next方法,图片滚动停止后,就要执行onTransitionEnd


定义俩变量,一个代表最左边的图,一个为右边的图


这里根据变量,决定谁会更新src,并且改变left值实现位移,不好描述啊


transform会一直向右位移,left值也是,所以他们会形成永动机


image.png


HTML里写上他们位移的样式即可自动更新


image.png


Bug


至此,看着已完成,似乎没有任何问题


但是你把页面隐藏了过后,过一会图片全都不见了,我们打开控制台看看为什么


可以看到,left停止更新了,也就是说,onTransitionEnd没有执行


image.png


transitionend在你浏览器隐藏页面时,就会停止执行


这时需要在页面隐藏时,停止执行,执行如下代码即可


/** 离开浏览器时 不会执行`transitionend` 所以要停止 */
function bindEvent() {
window.addEventListener('visibilitychange', () => {
document.hidden
? stop()
: play()
})
}

这时一定有人会说,你这不能往左啊,没有控制组件啊


如果要往左的话,只需要把两张图轮流交换改成4张图即可


具体逻辑都是差不多的



源码: gitee.com/cjl2385/dig…



作者:寅时码
来源:juejin.cn/post/7310111620368597011
收起阅读 »

吐槽大会,来瞧瞧资深老前端写的代码

web
忍无可忍,不吐不快。 本期不写技术文章,单纯来吐槽下公司项目里的奇葩代码,还都是一些资深老前端写的,希望各位对号入座。 知道了什么是烂代码,才能写出好代码。 别说什么代码和人有一个能跑就行的话,玩笑归玩笑。 人都有菜的时候,写出垃圾代码无可厚非,但是工作几年...
继续阅读 »

忍无可忍,不吐不快。



本期不写技术文章,单纯来吐槽下公司项目里的奇葩代码,还都是一些资深老前端写的,希望各位对号入座。


知道了什么是烂代码,才能写出好代码。


别说什么代码和人有一个能跑就行的话,玩笑归玩笑。


人都有菜的时候,写出垃圾代码无可厚非,但是工作几年了还是写垃圾代码,有点说不过去。


我认为的垃圾代码,不是你写不出深度优先、广度优先的算法,也不是你写不出发布订阅、策略模式的 if else


我认为垃圾代码都有一个共性:看了反胃。就是你看了天然的不舒服,看了就头疼的代码,比如排版极丑,没有空格、没有换行、没有缩进。


优秀的代码就是简洁清晰,不能用策略模式优化 if else 没关系,多几行代码影响不大,你只要清晰,就是好代码。


有了差代码、好代码的基本标准,那我们也废话不多说,来盘盘这些资深前端的代码,到底差在哪里。


-------------更新------------


集中回答一下评论区的问题:


1、项目是原来某大厂的项目,是当时的外包团队写的,整体的代码是还行的。eslintcommitlint 之类的也是有的,只不过这些都是可以禁用的,作用全靠个人自觉。


2、后来项目被我司收购,原来的人也转为我司员工,去支撑其他项目了。这个项目代码也就换成我们这批新来的维护了。很多代码是三几年前留下的确实没错,谁年轻的时候没写过烂代码,这是可以理解的。只不过同为三年前的代码,为啥有人写的简单清晰,拆分清楚,有的就这么脏乱差呢?


3、文中提到的很多代码其实是最近一年写的,写这些功能的前端都是六七年经验的,当时因为项目团队刚组建,没有前端,抽调这些前端过来支援,写的东西真的非常非常不规范,这是让人难以理解的,他们在原来的项目组(核心产品,前端基建和规范都很厉害)也是这么写代码的吗?


4、关于团队规范,现在的团队是刚成立没多久的,都是些年轻人,领导也不是专业前端,基本属于无前端 leader 的状态,我倒是很想推规范,但是我不在其位,不好去推行这些东西,提了建议,领导是希望你简单写写业务就行(说白了我不是 leader 说啥都没用)。而且我们这些年轻前端大家都默认打开 eslint,自觉性都很高,所以暂时都安于现状,代码 review 做过几次,都没太大的毛病,也就没继续做了。


5、评论区还有人说想看看我写的代码,我前面文章有,我有几个开源项目,感兴趣可以自己去看。项目上的代码因为涉及公司机密,没法展示。


6、本文就是篇吐槽,也不针对任何人,要是有人看了不舒服,没错,说的就是你。要是没事,大家看了乐呵乐呵就行。轻喷~


文件命名千奇百怪


同一个功能,都是他写的,三个文件夹三种奇葩命名法,最重要的是第三个,有人能知道它这个文件夹是什么功能吗?这完全不知道写的什么玩意儿,每次我来看代码都得重新理一遍逻辑。


image.png


组件职责不清


还是刚才那个组件,这个 components 文件夹应该是放各个组件的,但是他又在底下放一个 RecordDialog.jsx 作为 components 的根组件,那 components 作为文件名有什么意义呢?


image.png


条件渲染逻辑置于底层


这里其实他写了几个渲染函数,根据客户和需求的不同条件性地渲染不同内容,但是判断条件应该放在函数外,和函数调用放在一起,根据不同条件调用不同渲染函数,而不是函数内就写条件,这里就导致逻辑内置,过于分散,不利于维护。违反了我们常说的高内聚、低耦合的编程理念。


image.png


滥用、乱用 TS


项目是三四年前的项目了,主要是类组件,资深前端们是属于内部借调来支援维护的,发现项目连个 TS 都没有,不像话,赶紧把 TS 配上。配上了,写了个 tsx 后缀,然后全是无类型或者 anyscript。甚至于完全忽视 TSESlint 的代码检查,代码里留下一堆红色报错。忍无可忍,我自己加了类型。


image.png


留下大量无用注释代码和报错代码


感受下这个注释代码量,我每次做需求都删,但是几十个工程,真的删不过来。


image.png


image.png


丑陋的、隐患的、无效的、混乱的 css


丑陋的:没有空格,没有换行,没有缩进


隐患的:大量覆盖组件库原有样式的 css 代码,不写类名实现 namespace


无效的:大量注释的 css 代码,各种写了类名不写样式的垃圾类名


混乱的:从全局、局部、其他组件各种引入 css 文件,导致样式代码过于耦合


image.png


一个文件 6 个槽点


槽点1:代码的空行格式混乱,影响代码阅读


槽点2:空函数,写了函数名不写函数体,但是还调用了!


槽点3:函数参数过多不优化


槽点4:链式调用过长,属性可能存在 undefined 或者 null 的情况,代码容易崩,导致线上白屏


槽点5:双循环嵌套,影响性能,可以替换为 reduce 处理


槽点6:参数、变量、函数的命名随意而不语义化,完全不知道有何作用,且不使用小驼峰命名


image.png


变态的链式取值和赋值


都懒得说了,各位观众自己看吧。


image.png


代码拆分不合理或者不拆分导致代码行数超标


能写出这么多行数的代码的绝对是人才。


尽管说后来维护的人加了一些代码,但是其初始代码最起码也有近 2000 行。


image.png


这是某个功能的数据流文件,使用 Dva 维护本身代码量就比 Redux 少很多了,还是能一个文件写出这么多。导致到现在根本不敢拆出去,没人敢动。


image.png


杂七杂八的无用 js、md、txt 文件


在我治理之前,充斥着非常多的无用的、散乱的 md 文档,只有一个函数却没有调用的 js 文件,还有一堆测试用的 html 文件。


实在受不了干脆建个文件夹放一块,看起来也要舒服多了。


image.png


less、scss 混用


这是最奇葩的。


image.png


特殊变量重命名


这是真大佬,整个项目基建都是他搭的。写的东西也非常牛,bug 很少。但是大佬有一点个人癖好,例如喜欢给 window 重命名为 G。这虽然算不上大缺点,但真心不建议大家这么做,window 是特殊意义变量,还请别重命名。


const G = window;
const doc = G.document;

混乱的 import


规范的 import 顺序,应该是框架、组件等第三方库优先,其次是内部的组件库、包等,然后是一些工具函数,辅助函数等文件,其次再是样式文件。乱七八糟的引入顺序看着都烦,还有这个奇葩的引入方式,直接去 lib 文件夹下引入组件,也是够奇葩了。


image.png


写在最后


就先吐槽这么多吧,这些都是平时开发过程中容易犯的错误。


要想保持一个好的编码习惯,写代码的时候就得时刻告诉自己,你的代码后面是会有人来看的,不想被骂就写干净点。


我觉得什么算法、设计模式都是次要的,代码清晰,数据流向清晰,变量名起好,基本 80% 不会太差。


写代码就像做人,现实总是千难万苦,各种妥协和无奈,但是这不意味着我们可以无底线的做事。给自己设个底线,不论做人还是做事。


共勉。


作者:北岛贰
来源:juejin.cn/post/7265505732158472249
收起阅读 »

【Java集合】双列集合HashMap的概念、特点及使用

HashMap是Java中的一个集合类,它实现了Map接口,提供了一种存储键值对的方式。你可以把它想象成一个没有固定大小和形状的储物柜,你可以随时往里面放东西,也可以随时取出东西。而且,这个储物柜还有一个神奇的功能,那就是无论你放进去的是什么,取出来的总是你放...
继续阅读 »

HashMap是Java中的一个集合类,它实现了Map接口,提供了一种存储键值对的方式。你可以把它想象成一个没有固定大小和形状的储物柜,你可以随时往里面放东西,也可以随时取出东西。而且,这个储物柜还有一个神奇的功能,那就是无论你放进去的是什么,取出来的总是你放进去的那个。

上篇文章讲了Map接口的概念,以及Map接口中的常用方法和对Map集合的遍历,本篇文章我们将继续介绍另一个十分重要的双列集合—HashMap。


HashMap 概念

HashMap集合是Map接口的一个实现类,它用于存储键值映射关系,该集合的键和值允许为空,但键不能重复,且集合中的元素是无序的。

特点

HashMap底层是由哈希表结构组成的,其实就是“数组+链表”的组合体,数组是HashMap的主体结构,链表则主要是为了解决哈希值冲突而存在的分支结构。正因为这样特殊的存储结构,HashMap集合对于元素的增、删、改、查操作表现出的效率都比较高。

结构

在java1.8以后采用数组+链表+红黑树的形势来进行存储,通过散列映射来存储键值对,如下图:

Description

  • 在初始化时将会给定默认容量为16

  • 对key的hashcode进行一个取模操作得到数组下标

  • 数组存储的是一个单链表

  • 数组下标相同将会被放在同一个链表中进行存储

  • 元素是无序排列的

  • 链表超过一定长度(TREEIFY_THRESHOLD=8)会转化为红黑树

  • 红黑树在满足一定条件会再次退回链表

看到这个图,是不是挺熟悉!没错,这个就是我们在讲Set时,它的内存结构图,当时我们说 HashSet的底层就是 Map集合,只不过Set只使用了Map集合中的Key,没有使用Value而已。

小练习

在之前我们已经讲了不少Map的使用方法,本篇中就不做过多解释了,来上了个小练习,在体会下它的使用。

每位学生(姓名,年龄)都有自己的家庭住址。那么,既然有对应关系,则将学生对象和家庭住址存储到map集合中。学生作为键, 家庭住址作为值。

注意,学生姓名相同并且年龄相同视为同一名学生。

编写学生类:

    public class Student {
private String name;
private int age;

public Student() {
}

public Student(String name, int age) {
this.name = name;
this.age = age;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
Student student = (Student) o;
return age == student.age && Objects.equals(name, student.name);
}

@Override
public int hashCode() {
return Objects.hash(name, age);
}
}

编写测试类:

    public class HashMapTest {
public static void main(String[] args) {
//1,创建Hashmap集合对象。
Map<Student,String>map = new HashMap<Student,String>();
//2,添加元素。
map.put(newStudent("lisi",28), "上海");
map.put(newStudent("wangwu",22), "北京");
map.put(newStudent("zhaoliu",24), "成都");
map.put(newStudent("zhouqi",25), "广州");
map.put(newStudent("wangwu",22), "南京");

//3,取出元素。键找值方式
Set<Student>keySet = map.keySet();
for(Student key: keySet){
Stringvalue = map.get(key);
System.out.println(key.toString()+"....."+value);
}
}
}
  • 当给HashMap中存放自定义对象时,如果自定义对象作为key存在,这时要保证对象唯一,必须复写对象的hashCode和equals方法(如果忘记,请回顾HashSet存放自定义对象)。

  • 如果要保证map中存放的key和取出的顺序一致,可以使用java.util.LinkedHashMap集合来存放。

你还在苦恼找不到真正免费的编程学习平台吗?可以试试云端源想!课程视频、在线书籍、在线编程、实验场景模拟、一对一咨询……你想要的全部学习资源这里都有,重点是统统免费!点这里即可查看

LinkedHashMap

我们知道HashMap保证成对元素唯一,并且查询速度很快,可是成对元素存放进去是没有顺序的,那么我们要保证有序,还要速度快怎么办呢?

在HashMap下面有一个子类LinkedHashMap,它继承自HashMap。特别的是,LinkedHashMap在HashMap的基础上维护了一个双向链表,可以按照插入顺序或者访问顺序来迭代元素。此外,LinkedHashMap结合了HashMap的数据操作和LinkedList的插入顺序维护的特性,因此也可以被看做是HashMap与LinkedList的结合。它是链表和哈希表组合的一个数据存储结构。把上个练习使用LinkedHashMap的使用一下

    publicclass LinkedHashMapDemo {
publicstaticvoid main(String[] args) {

//Map<String, String> map = new HashMap<String, String>();

LinkedHashMap<String, String> map = new LinkedHashMap<String, String>();
map.put("马云", "阿里巴巴");
map.put("马化腾", "腾讯");
map.put("李彦宏", "百度");
Set<Entry<String, String>> entrySet = map.entrySet();
for (Entry<String, String> entry : entrySet) {
System.out.println(entry.getKey() + " " + entry.getValue());
}
}
}

总结

总的来说,HashMap是Java中的一个强大工具,它可以帮助我们高效地处理大量的数据。但是,我们也需要注意,虽然HashMap的性能很高,但如果不正确地使用它,可能会导致内存泄漏或者数据丢失的问题。因此,我们需要正确地理解和使用HashMap,才能充分发挥它的强大功能。

本系列文章写到这里,为大家介绍集合家族的知识,基本上就可以告一段落了。

在这个系列文章中,我们讲述了单列和双列集合的家族体系以及简单的使用。集合中不少的实现类,我们并未讲述,大家下来可以通过java的API文档,去学习使用。还是那句话,熟能生巧!只看不练,假把式!

本系列以上内容,都是在实际项目中,会经常碰到这些概念的使用,当然了,文中的内容可能也不是尽善尽美的,如有错误,可以私信,探讨!
happy ending!

收起阅读 »

教你如何实现一个页面自动打字效果

web
前言: 最近在写一个类似于 windows 启动页的项目,不知道大家是否还记的 windows 很经典的小光标加载页面,我又稍微改造了一下效果如下: 一. 光标闪烁效果的实现 tips: 在这里我们使用了 UnoCSS,如果你不清楚 UnoCSS 的使用方...
继续阅读 »

前言: 最近在写一个类似于 windows 启动页的项目,不知道大家是否还记的 windows 很经典的小光标加载页面,我又稍微改造了一下效果如下:


loading.gif




一. 光标闪烁效果的实现


tips: 在这里我们使用了 UnoCSS,如果你不清楚 UnoCSS 的使用方法,那你可以点击下面这篇文章。

🫱 🎁手把手教你创建自己的代码仓库



  1. 首先准备一块黑色的背景。

    image.png

  2. 其实光标的样式非常非常简单,仅仅只需要你创建一个宽高合适的 div,然后创建一个底部的 border 效果即可。

    image.png
    下面应该是你目前的效果。

    image.png

  3. 现在需要清楚的知道,这个白块的展示其实就是我们控制展示这个 divborder 的显示还是隐藏。那么现在我们的思路就很清晰了,所以这里我们只需要写一个变量来动态的切换这个 border 值即可。

    image.png

  4. 现在你的页面效果应该是漆黑一片,那交给谁来动态的切换这个状态呢?这里其实很简单,当页面挂载的时候,我们只需要开启一个定时器来动态切换即可。

    image.png

    这时候我们其实就能看到一丢丢效果了:

    flash.gif


二. 自动打字效果的实现



  1. 首先我们应该明确一个概念,我们目前要做的事很简单,只需要在百块 div 的前面插入文字其实就是在向后推白块

    image.png

    image.png

    所以白块的移动是我们无需关心的,我们仅仅只需要去处理如何插入字体的问题。

  2. 这里我们先准备一个常量来书写一段字符串文字,然后还需要给准备放文字的 div 打上 ref 为后面的工作做准备,之后我们需要用到它身上相关的属性。

    image.png

  3. 接下来我们要编写一个函数去处理这个问题,名字起的就随意点吧,就叫做 autoPrint

    image.png

  4. 这里我们仍需要开启一个循环定时器去控制,因为我们无法得知文字具体有多少,不考虑使用 setTimeout

    image.png

  5. 还需要准备两个变量,来存放接下来我们要处理的文字信息。

    image.png

  6. 下面代码的思路就比较简单了,其实就是调用了 substring 方法来一直切割获取下一个字符串的值。substring本身也是不改变原字符串的,所以我们只需要控制 index 就可以很轻松的获取到最后的值。

    image.png

    效果如下:

    3.gif

  7. 最后别忘了在合适的时机清除这个定时器。

    image.png


三. 更优雅的实现小方块闪烁


更新于 2023/02/22



  1. 在写上面的代码之前我没有考虑文字过长的问题,导致小光标不会换行的问题。

  2. 今天更新一下,修复这个 bug

    自动.gif

  3. 我们删除上面之前控制 border 的显示与否而展示的小光标样式。

    image.png

  4. 在放置文字的 div 添加一个伪元素来实现这个效果,更加简洁一点。

    image.png

  5. 并且使用动画来替换之前的 flicker
    image.png


四. 源码



<script>

//tips: automatic printing welcome words.
function autoPrintText(text: string) {
let _str = ""
let _index = 0
const _timerID = window.setInterval(() => {
if (!textAreas.value) return
if (_index > text.length - 1) {
clearInterval(_timerID)
return
}
_str = _str + text.substring(_index, _index + 1)
textAreas.value!.innerText = _str
_index++
}, printSpeed)
}

</script>

<template>

<div v-if="isFlicker" class="w-full h-full">
<div class="text-box w-fit">
<span ref="textAreas" class="text-1.8rem font-600"></span>
</div>
</template>

<style scoped>
.text-box::after {
display: inline-block;
content: "";
width: 2rem;
vertical-align: text-bottom;
border-bottom: 3px solid white;
margin-left: 8px;
animation: flicker 0.5s linear infinite;
}

@keyframes flicker {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
</style>

预告


最近在实现一个 window 的全套 UI ,PC 和移动端的效果是完全自适应的,两者有两套 UI

4.gif

我会在本周更新拖拽这个经典面试题的实现,仍会使用费曼学习法通俗易懂的讲解。如果你有兴趣,不妨保持关注。🎁


作者:韩振方
来源:juejin.cn/post/7200773486796914725
收起阅读 »

前端如何使用websocket发送消息

web
1 基础介绍 1.1 什么是WebSocket WebSocket 是一种在单个 TCP 连接上进行 全双工 通信的协议,它可以让客户端和服务器之间进行实时的双向通信。与传统的 HTTP 请求不同,WebSocket 使用了一个长连接,在客户端和服务器之间保...
继续阅读 »

1 基础介绍


1.1 什么是WebSocket



WebSocket 是一种在单个 TCP 连接上进行 全双工 通信的协议,它可以让客户端和服务器之间进行实时的双向通信。与传统的 HTTP 请求不同,WebSocket 使用了一个长连接,在客户端和服务器之间保持持久的连接,从而可以实时地发送和接收数据。




在 WebSocket 中,客户端和服务器之间可以互相发送消息。
客户端可以使用 JavaScript 中的 WebSocket API 发送消息到服务器,也可以接收服务器发送的消息。



1.2 WebSocket与HTTP的区别



WebSocket与HTTP的区别在于连接的性质和通信方式。WebSocket是一种双向通信的协议,通过一次握手即可建立持久性的连接,服务器和客户端可以随时发送和接收数据。而HTTP协议是一种请求-响应模式的协议,每次通信都需要发送一条请求并等待服务器的响应。WebSocket的实时性更好,延迟更低,并且在服务器和客户端之间提供双向的即时通信能力,适用于需要实时数据传输的场景。



1.3 代码示例



下面是一个使用 WebSocket API 发送消息的代码示例:



var socket = new WebSocket("ws://example.com/socketserver");

socket.onopen = function(event) {
socket.send("Hello server!");
};

socket.onmessage = function(event) {
console.log("Received message from server: " + event.data);
};

socket.onerror = function(event) {
console.log("WebSocket error: " + event.error);
};

socket.onclose = function(event) {
console.log("WebSocket connection closed with code " + event.code);
};


在上面的代码中,首先创建了一个 WebSocket 对象,指定了服务器的地址。然后在 onopen 回调函数中,发送了一个消息到服务器。当服务器发送消息到客户端时,onmessage 回调函数会被触发,从而可以处理服务器发送的消息。如果出现错误或者连接被关闭,onerror 和 onclose 回调函数会被触发,从而可以处理这些事件。


需要注意的是,在使用 WebSocket 发送消息之前,必须先建立 WebSocket 连接。在上面的代码中,通过创建一个 WebSocket 对象来建立连接,然后在 onopen 回调函数中发送消息到服务器。如果在连接建立之前就尝试发送消息,那么这些消息将无法发送成功。



2 前端使用WebSocket的流程


2.1 创建WebSocket对象


通过JavaScript中的new WebSocket(URL)方法创建WebSocket对象,其中URL是WebSocket服务器的地址。根据实际情况修改URL以与特定的WebSocket服务器进行连接。例如:


const socket = new WebSocket('ws://localhost:8000');

2.2 监听WebSocket事件


WebSocket对象提供多种事件用于监听连接状态和接收消息,例如:open、message、close、error等。



  • open:当与服务器建立连接时触发。

  • message:当收到服务器发送的消息时触发。

  • close:当与服务器断开连接时触发。

  • error:当连接或通信过程中发生错误时触发。


通过添加事件监听器,可以在相应事件发生时执行特定的逻辑。例如:


socket.addEventListener('open', () => {
console.log('WebSocket连接已建立');
});

socket.addEventListener('message', (event) => {
const message = event.data;
console.log('收到消息:', message);
});

socket.addEventListener('close', () => {
console.log('WebSocket连接已断开');
});

socket.addEventListener('error', (error) => {
console.error('发生错误:', error);
});

2.3 发送消息


通过WebSocket对象的send(data)方法发送消息,其中data是要发送的数据,可以是字符串、JSON对象等。可以根据实际需求将数据格式化成特定的类型进行发送。例如:


const message = 'Hello, server!';
socket.send(message);

2.4 关闭WebSocket连接


当通信结束或不再需要与服务器通信时,需要关闭WebSocket连接以释放资源。通过调用WebSocket对象的close()方法可以主动关闭连接,也可以根据业务需求设置自动关闭连接的条件。例如:


socket.close();

3 前端发送消息的应用实例


一个常见的前端发送消息的应用实例是在线聊天应用。在这种应用中,前端通过WebSocket与后端服务器建立连接,并实时发送和接收聊天消息。


以下是一个简单的前端发送消息的示例代码:


const socket = new WebSocket('ws://localhost:8000');

// 连接建立事件
socket.addEventListener('open', () => {
console.log('WebSocket连接已建立');
});

// 消息接收事件
socket.addEventListener('message', (event) => {
const message = event.data;
console.log('收到消息:', message);
// 处理接收到的消息,将其显示在前端界面上
});

// 发送消息
function sendMessage(message) {
socket.send(message);
}

// 调用发送消息的函数,例如在点击按钮后发送消息
const sendButton = document.getElementById('sendBtn');
sendButton.addEventListener('click', () => {
const messageInput = document.getElementById('messageInput');
const message = messageInput.value;
sendMessage(message);
messageInput.value = ''; // 清空输入框
});

// 连接关闭事件
socket.addEventListener('close', () => {
console.log('WebSocket连接已断开');
});

// 连接错误事件
socket.addEventListener('error', (error) => {
console.error('发生错误:', error);
});


该示例中,通过创建WebSocket对象,监听连接建立事件、消息接收事件、连接关闭事件和错误事件,从而实现与服务器的实时通信。通过构建界面和处理消息的逻辑,可以实现实时聊天功能。


这只是一个简单的示例,实际上,前端发送消息的应用可以更广泛,如实时数据更新、多人协作编辑、实时游戏等。具体的实现方式和功能根据实际需求而定,可以灵活调整和扩展。



4 WebSocket的应用场景


WebSocket的应用场景包括但不限于以下几个方面:



  1. 实时聊天应用:WebSocket能够提供双向、实时的通信机制,使得实时聊天应用能够快速、高效地发送和接收消息,实现即时通信。

  2. 实时协作应用:WebSocket可以用于实时协作工具,如协同编辑文档、白板绘画、团队任务管理等,团队成员可以实时地在同一页面上进行互动和实时更新。

  3. 实时数据推送:WebSocket可以用于实时数据推送场景,如股票行情、新闻快讯、实时天气信息等,服务器可以实时将数据推送给客户端,确保数据的及时性和准确性。

  4. 多人在线游戏:WebSocket提供了实时的双向通信机制,适用于多人在线游戏应用,使得游戏服务器能够实时地将游戏状态和玩家行为传输给客户端,实现游戏的实时互动。

  5. 在线客服和客户支持:WebSocket可以用于在线客服和客户支持系统,实现实时的客户沟通和问题解决,提供更好的用户体验,减少等待时间。



WebSocket适用于需要实时双向通信的场景,在这些场景中,它能够提供更好的实时性、低延迟和高效性能,为Web应用程序带来更好的交互性和用户体验。



作者:李泽南
来源:juejin.cn/post/7277835425959886882
收起阅读 »

面试官:你知道websocket的心跳机制吗?

web
前言 哈喽,大家好,我是泽南Zn👨‍🎓。在之前的一篇文章写到, 前端如何使用websocket发送消息,websocket是怎么建立连接的呢?如果断开了会怎样?如何一直保持长连接呢?接下来,本篇文章将会带你了解--- WebSocket心跳机制 一、...
继续阅读 »

前言


哈喽,大家好,我是泽南Zn👨‍🎓。在之前的一篇文章写到, 前端如何使用websocket发送消息,websocket是怎么建立连接的呢?如果断开了会怎样?如何一直保持长连接呢?接下来,本篇文章将会带你了解--- WebSocket心跳机制


一、WebSocket心跳机制


前端实现WebSocket心跳机制的方式主要有两种:




  1. 使用setInterval定时发送心跳包。

  2. 在前端监听到WebSocket的onclose()事件时,重新创建WebSocket连接。



第一种方式会对服务器造成很大的压力,因为即使WebSocket连接正常,也要定时发送心跳包,从而消耗服务器资源。第二种方式虽然减轻了服务器的负担,但是在重连时可能会丢失一些数据。


二、WebSocket心跳包机制


WebSocket心跳包是WebSocket协议的保活机制,用于维持长连接。有效的心跳包可以防止长时间不通讯时,WebSocket自动断开连接。


心跳包是指在一定时间间隔内,WebSocket发送的空数据包。常见的WebSocket心跳包机制如下:




  1. 客户端定时向服务器发送心跳数据包,以保持长连接。

  2. 服务器定时向客户端发送心跳数据包,以检测客户端连接是否正常。

  3. 双向发送心跳数据包。



三、WebSocket心跳机制原理


WebSocket心跳机制的原理是利用心跳包及时发送和接收数据,保证WebSocket长连接不被断开。WebSocket心跳机制的原理可以用下面的流程来说明:




  1. 客户端建立WebSocket连接。

  2. 客户端向服务器发送心跳数据包,服务器接收并返回一个表示接收到心跳数据包的响应。

  3. 当服务器没有及时接收到客户端发送的心跳数据包时,服务器会发送一个关闭连接的请求。

  4. 服务器定时向客户端发送心跳数据包,客户端接收并返回一个表示接收到心跳数据包的响应。

  5. 当客户端没有及时接收到服务器发送的心跳数据包时,客户端会重新连接WebSocket



四、WebSocket心跳机制必要吗


WebSocket心跳机制是必要的,它可以使 WebSocket 连接保持长连接,避免断开连接的情况发生。同时,心跳机制也可以检查WebSocket连接的状态,及时处理异常情况。


五、WebSocket心跳机制作用


WebSocket心跳机制的作用主要有以下几点:



  1. 保持WebSocket连接不被断开。

  2. 检测WebSocket连接状态,及时处理异常情况。

  3. 减少WebSocket连接及服务器资源的消耗。


六、WebSocket重连机制


WebSocket在发送和接收数据时,可能会因为网络原因、服务器宕机等因素而断开连接,此时需要使用WebSocket重连机制进行重新连接。


WebSocket重连机制可以通过以下几种方式实现:




  1. 前端监听WebSocket的onclose()事件,重新创建WebSocket连接。

  2. 使用WebSocket插件或库,例如Sockjs、Stompjs等。

  3. 使用心跳机制检测WebSocket连接状态,自动重连。

  4. 使用断线重连插件或库,例如ReconnectingWebSocket等。



七、WebSocket的缺点和不足


WebSocket的缺点和不足主要有以下几点:




  1. WebSocket需要浏览器和服务器端都支持该协议。

  2. WebSocket会增加服务器的负担,不适合大规模连接的应用场景。



八、关键代码


  // 开启心跳
const start = () => {
clearTimeout(timeoutObj);
// serverTimeoutObj && clearTimeout(serverTimeoutObj);
timeoutObj = setTimeout(function () {
if (websocketRef.current?.readyState === 1) {
//连接正常
sendMessage('hello');
}
}, timeout);
};
const reset = () => {
// 重置心跳 清除时间
clearTimeout(timeoutObj);
// 重启心跳
start();
};

ws.onopen = (event) => {
onOpenRef.current?.(event, ws);
reconnectTimesRef.current = 0;
start(); // 开启心跳
setReadyState(ws.readyState || ReadyState.Open);
};
ws.onmessage = (message: WebSocketEventMap['message']) => {
const { data } = message;

if (data === '收到,hello') {
reset();
return;
}
if (JSON.parse(data).status === 408) {
reconnect();
return;
}
onMessageRef.current?.(message, ws);
setLatestMessage(message);
};
const connect = () => {
reconnectTimesRef.current = 0;
connectWs();
};

主要思路:在建立长连接的时候开启心跳,通过和服务端发送信息,得到服务端给返回的信息,然后重置心跳,清楚时间,再重新开启心跳。如果网络断开的话,会执行方法,重新连接。


作者:泽南Zn
来源:juejin.cn/post/7290005438153867283
收起阅读 »

qq农场私信我,您菜死了🥬

web
最近在写代码的时候发现自己总是有这样几种症状: 脸红心跳,像发烧一样😳; 口干舌燥、咳嗽不停😮‍💨; 脑袋放空,像刚通宵了一般👀; ...... 我逐渐怀疑,自己有没有可能是🐑了,甚至时不时就拿起体温计量一量,拿起自测试纸测一测,这样的情况一直没有得到好转...
继续阅读 »

最近在写代码的时候发现自己总是有这样几种症状



  1. 脸红心跳,像发烧一样😳;

  2. 口干舌燥、咳嗽不停😮‍💨;

  3. 脑袋放空,像刚通宵了一般👀;

  4. ......


我逐渐怀疑,自己有没有可能是🐑了,甚至时不时就拿起体温计量一量,拿起自测试纸测一测,这样的情况一直没有得到好转,直到收到QQ农场给我发来这样的一条信息:


尊敬的QQ农场主,您去年和今年菜死了!🥬🥬🥬


🤔于是,我开始分析我症状根因是什么:



  1. 脸红心跳:是因为自己脑海中想象好的实现方案,但实际却写不出一行代码,或者各种Error,导致我心里落差很大,自我怀疑,或者是被人看穿菜的窘迫、害羞?

  2. 口干舌燥:是因为自己陷入了 写不出代码 => 憋着气接着写,不休息喝水 => 写不出代码 这样的闭环🐶里面;

  3. 脑袋放空:摆脱了内耗,很容易得出结论,就是看的技术不够多,写的代码不够多


痛定思痛,决定在这里立下FLAG,要多看多实践,学习和思考好的代码写法,看得多,写得多。


今天分享的主要是:用好发布订阅、偏函数的一对多 & 多对一关系工厂函数


发布订阅 & 偏函数(一对多/多对一关系)


是一种一对多的模式,或者说多对多的模式;一个事件对应多个处理函数,多个事件对应各自对应的处理函数



那假如我们想实现一个多对一的关系呢?我们可以使用偏函数


偏函数个人理解类似工厂函数,利用了闭包的特性


// 偏函数
function after(times, cb) {
let count = 0;
const result = {};
return inner(key, value) => {
result[key] = value;
count++;
if (count === times) {
cb(result);
}
};
}

结合代码看此处相当于多个inner函数对应一个callback函数,由count来控制是否触发callback,这种模式常常用于异步编程,比如Promise.all



综合一对多和多对一模式:


// 偏函数
function after(times, cb) {
let count = 0;
const result = {};
return (key, value) => {
result[key] = value;
count++;
if (count === times) {
cb(result);
}
};
}

// 发布订阅
const emitter = new (require("events").EventEmitter)();
const done = after(3, render);

emitter.on("done", done);
emitter.on("done", other);

fs.readFile(file, (err, template) => {
emitter.emit("done", "template", template);
});

fs.readFile(file, (err, data) => {
emitter.emit("done", "data", data);
});

fs.readFile(file, (err, str) => {
emitter.emit("done", "str", str);
});



工厂函数


类似现实工厂,在代码中用来生产特定结构函数/对象等的函数


比如想实现一个生成校验函数的工厂函数:


/**
* config里可以包含一般的描述性属性,钩子函数等
**/
export function factory(config) {
config.before = config.before || ((d) => d);
// pre钩子
handlersMap[config.type]?.pre(config);

return function (data) {
// before钩子函数
data = config.before(data);
return handlersMap[config.type].check(data);
};
}

// 通过该方法注册不同的校验函数
const handlersMap = {};
factory.registerHandler = function (type, handler) {
handlersMap[type] = handler;
};

在项目中的实现可如图:



🌊总结:


阅读好的代码,并学习一些好的写法,才是比较实际提高代码能力的方式,我也将💪持续阅读好的代码库,思考学习好的代码,把自己的成长分享出来。


作者:Kuroo
来源:juejin.cn/post/7182545613282623549
收起阅读 »

WebSocket 从入门到入土

web
前言因新部门需求有一个后台管理需要一个右上角的实时的消息提醒功能,第一时间想到的就是使用WebSocket建立实时通信了,之前没整过,于是只能学习了。和原部门相比现在太忙了,快乐的日子一去不复返了。经典的加量不加薪啊!!!一.WebSocket 基本概念1.W...
继续阅读 »

前言

因新部门需求有一个后台管理需要一个右上角的实时的消息提醒功能,第一时间想到的就是使用WebSocket建立实时通信了,之前没整过,于是只能学习了。和原部门相比现在太忙了,快乐的日子一去不复返了。经典的加量不加薪啊!!!

一.WebSocket 基本概念

1.WebSocket是什么?

WebSocket 是基于 TCP 的一种新的应用层网络协议。它提供了一个全双工的通道,允许服务器和客户端之间实时双向通信。因此,在 WebSocket 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输,客户端和服务器之间的数据交换变得更加简单。 WebSocket

2.与 HTTP 协议的区别

与 HTTP 协议相比,WebSocket 具有以下优点:

  1. 更高的实时性能:WebSocket 允许服务器和客户端之间实时双向通信,从而提高了实时通信场景中的性能。
  2. 更少的网络开销:HTTP 请求和响应之间需要额外的数据传输,而 WebSocket 通过在同一个连接上双向通信,减少了网络开销。
  3. 更灵活的通信方式:HTTP 请求和响应通常是一一对应的,而 WebSocket 允许服务器和客户端之间以多种方式进行通信,例如消息 Push、事件推送等。
  4. 更简洁的 API:WebSocket 提供了简洁的 API,使得客户端开发人员可以更轻松地进行实时通信。

当然肯定有缺点的:

  1. 不支持无连接: WebSocket 是一种持久化的协议,这意味着连接不会在一次请求之后立即断开。这是有利的,因为它消除了建立连接的开销,但是也可能导致一些资源泄漏的问题。
  2. 不支持广泛: WebSocket 是 HTML5 中的一种标准协议,虽然现代浏览器都支持,但是一些旧的浏览器可能不支持 WebSocket。
  3. 需要特殊的服务器支持: WebSocket 需要服务端支持,只有特定的服务器才能够实现 WebSocket 协议。这可能会增加系统的复杂性和部署的难度。
  4. 数据流不兼容: WebSocket 的数据流格式与 HTTP 不同,这意味着在不同的网络环境下,WebSocket 的表现可能会有所不同。

3.WebSocket工作原理

1. 握手阶段

WebSocket在建立连接时需要进行握手阶段。握手阶段包括以下几个步骤:

  • 客户端向服务端发送请求,请求建立WebSocket连接。请求中包含一个Sec-WebSocket-Key参数,用于生成WebSocket的随机密钥。
  • 服务端接收到请求后,生成一个随机密钥,并使用随机密钥生成一个新的Sec-WebSocket-Accept参数。
  • 客户端接收到服务端发送的新的Sec-WebSocket-Accept参数后,使用原来的随机密钥和新的Sec-WebSocket-Accept参数共同生成一个新的Sec-WebSocket-Key参数,用于加密数据传输。
  • 客户端将新的Sec-WebSocket-Key参数发送给服务端,服务端接收到后,使用该参数加密数据传输。

2. 数据传输阶段

建立连接后,客户端和服务端就可以通过WebSocket进行实时双向通信。数据传输阶段包括以下几个步骤:

  • 客户端向服务端发送数据,服务端收到数据后将其转发给其他客户端。
  • 服务端向客户端发送数据,客户端收到数据后进行处理。

双方如何进行相互传输数据的 具体的数据格式是怎么样的呢?WebSocket 的每条消息可能会被切分成多个数据帧(最小单位)。发送端会将消息切割成多个帧发送给接收端,接收端接收消息帧,并将关联的帧重新组装成完整的消息。

发送方 -> 接收方:ping。

接收方 -> 发送方:pong。

ping 、pong 的操作,对应的是 WebSocket 的两个控制帧

3. 关闭阶段

当不再需要WebSocket连接时,需要进行关闭阶段。关闭阶段包括以下几个步骤:

  • 客户端向服务端发送关闭请求,请求中包含一个WebSocket的随机密钥。
  • 服务端接收到关闭请求后,向客户端发送关闭响应,关闭响应中包含服务端生成的随机密钥。
  • 客户端收到关闭响应后,关闭WebSocket连接。

总的来说,WebSocket通过握手阶段、数据传输阶段和关闭阶段实现了服务器和客户端之间的实时双向通信。

二.WebSocket 数据帧结构和控制帧结构。

1. 数据帧结构

WebSocket 数据帧主要包括两个部分:帧头和有效载荷。以下是 WebSocket 数据帧结构的简要介绍:

  • 帧头:帧头包括四个部分:fin、rsv1、rsv2、rsv3、opcode、masked 和 payload_length。其中,fin 表示数据帧的结束标志,rsv1、rsv2、rsv3 表示保留字段,opcode 表示数据帧的类型,masked 表示是否进行掩码处理,payload_length 表示有效载荷的长度。
  • 有效载荷:有效载荷是数据帧中实际的数据部分,它由客户端和服务端进行数据传输。

2. 控制帧结构

除了数据帧之外,WebSocket 协议还包括一些控制帧,主要包括 Ping、Pong 和 Close 帧。以下是 WebSocket 控制帧结构的简要介绍:

  • Ping 帧:Ping 帧用于测试客户端和服务端之间的连接状态,客户端向服务端发送 Ping 帧,服务端收到后需要向客户端发送 Pong 帧进行响应。
  • Pong 帧:Pong 帧用于响应客户端的 Ping 帧,它用于测试客户端和服务端之间的连接状态。
  • Close 帧:Close 帧用于关闭客户端和服务端之间的连接,它包括四个部分:fin、rsv1、rsv2、rsv3、opcode、masked 和 payload_length。其中,opcode 的值为 8,表示 Close 帧。

三. JavaScript 中 WebSocket 对象的属性和方法,以及如何创建和连接 WebSocket。

WebSocket 对象的属性和方法:

  1. WebSocket 对象:WebSocket 对象表示一个新的 WebSocket 连接。
  2. WebSocket.onopen 事件处理程序:当 WebSocket 连接打开时触发。
  3. WebSocket.onmessage 事件处理程序:当接收到来自 WebSocket 的消息时触发。
  4. WebSocket.onerror 事件处理程序:当 WebSocket 发生错误时触发。
  5. WebSocket.onclose 事件处理程序:当 WebSocket 连接关闭时触发。
  6. WebSocket.send 方法:向 WebSocket 发送数据。
  7. WebSocket.close 方法:关闭 WebSocket 连接。

创建和连接 WebSocket:

  1. 创建 WebSocket 对象:
var socket = new WebSocket('ws://example.com');

其中,ws://example.com 是 WebSocket 的 URL,表示要连接的服务器。

  1. 连接 WebSocket:

使用 WebSocket.onopen 事件处理程序检查 WebSocket 是否成功连接。

socket.onopen = function() {
console.log('WebSocket connected');
};
  1. 接收来自 WebSocket 的消息:

使用 WebSocket.onmessage 事件处理程序接收来自 WebSocket 的消息。

socket.onmessage = function(event) {
console.log('WebSocket message:', event.data);
};
  1. 向 WebSocket 发送消息:

使用 WebSocket.send 方法向 WebSocket 发送消息。

socket.send('Hello, WebSocket!');
  1. 关闭 WebSocket:

当需要关闭 WebSocket 时,使用 WebSocket.close 方法。

socket.close();

注意:在 WebSocket 连接成功打开和关闭时,会分别触发 WebSocket.onopen 和 WebSocket.onclose 事件。在接收到来自 WebSocket 的消息时,会触发 WebSocket.onmessage 事件。当 WebSocket 发生错误时,会触发 WebSocket.onerror 事件。

四.webSocket简单示例

以下是一个简单的 WebSocket 编程示例,通过 WebSocket 向服务器发送数据,并接收服务器返回的数据:

  1. 首先,创建一个 HTML 文件,添加一个按钮和一个用于显示消息的文本框:
html>
<html>
<head>
<meta charset="UTF-8">
<title>WebSocket 示例title>
head>
<body>
<button id="sendBtn">发送消息button>
<textarea id="messageBox" readonly>textarea>
<script src="main.js">script>
body>
html>
  1. 接下来,创建一个 JavaScript 文件(例如 main.js),并在其中编写以下代码:
// 获取按钮和文本框元素
const sendBtn = document.getElementById('sendBtn');
const messageBox = document.getElementById('messageBox');

// 创建 WebSocket 对象
const socket = new WebSocket('ws://echo.websocket.org'); // 使用一个 WebSocket 服务器进行测试

// 设置 WebSocket 连接打开时的回调函数
socket.onopen = function() {
console.log('WebSocket 连接已打开');
};

// 设置 WebSocket 接收到消息时的回调函数
socket.onmessage = function(event) {
console.log('WebSocket 接收到消息:', event.data);
messageBox.value += event.data + '\n';
};

// 设置 WebSocket 发生错误时的回调函数
socket.onerror = function() {
console.log('WebSocket 发生错误');
};

// 设置 WebSocket 连接关闭时的回调函数
socket.onclose = function() {
console.log('WebSocket 连接已关闭');
};

// 点击按钮时发送消息
sendBtn.onclick = function() {
const message = 'Hello, WebSocket!';
socket.send(message);
messageBox.value += '发送消息: ' + message + '\n';
};

五.webSocket应用场景

  1. 实时通信:WebSocket 非常适合实时通信场景,例如聊天室、在线游戏、实时数据传输等。通过 WebSocket,客户端和服务器之间可以实时通信,无需依赖轮询,从而提高通信效率和减少网络延迟。
  2. 监控数据传输:WebSocket 可以在监控系统中实现实时数据传输,例如通过 WebSocket,客户端可以实时接收和处理监控数据,而无需等待轮询数据。
  3. 自动化控制:WebSocket 可以在自动化系统中实现远程控制,例如通过 WebSocket,客户端可以远程控制设备或系统,而无需直接操作。
  4. 数据分析:WebSocket 可以在数据分析场景中实现实时数据传输和处理,例如通过 WebSocket,客户端可以实时接收和处理数据,而无需等待数据存储和分析。
  5. 人工智能:WebSocket 可以在人工智能场景中实现实时数据传输和处理,例如通过 WebSocket,客户端可以实时接收和处理数据,而无需等待数据处理和分析。

六.WebSocket 错误处理

WebSocket 的错误处理

  1. WebSocket is not supported:当浏览器不支持 WebSocket 时,会出现此错误。解决方法是在浏览器兼容性列表中检查是否支持 WebSocket。
  2. WebSocket connection closed:当 WebSocket 连接被关闭时,会出现此错误。解决方法是在 WebSocket.onclose 事件处理程序中进行错误处理。
  3. WebSocket error:当 WebSocket 发生错误时,会出现此错误。解决方法是在 WebSocket.onerror 事件处理程序中进行错误处理。
  4. WebSocket timeout:当 WebSocket 连接超时时,会出现此错误。解决方法是在 WebSocket.ontimeout 事件处理程序中进行错误处理。
  5. WebSocket handshake error:当 WebSocket 握手失败时,会出现此错误。解决方法是在 WebSocket.onerror 事件处理程序中进行错误处理。
  6. WebSocket closed by server:当 WebSocket 连接被服务器关闭时,会出现此错误。解决方法是在 WebSocket.onclose 事件处理程序中进行错误处理。
  7. WebSocket closed by protocol:当 WebSocket 连接被协议错误关闭时,会出现此错误。解决方法是在 WebSocket.onclose 事件处理程序中进行错误处理。
  8. WebSocket closed by network:当 WebSocket 连接被网络错误关闭时,会出现此错误。解决方法是在 WebSocket.onclose 事件处理程序中进行错误处理。
  9. WebSocket closed by server:当 WebSocket 连接被服务器错误关闭时,会出现此错误。解决方法是在 WebSocket.onclose 事件处理程序中进行错误处理。

通过为 WebSocket 对象的 oncloseonerror 和 ontimeout 事件添加处理程序,可以及时捕获和处理 WebSocket 错误,从而确保程序的稳定性和可靠性。

七.利用单例模式创建完整的wesocket连接

class webSocketClass {
constructor(thatVue) {
this.lockReconnect = false;
this.localUrl = process.env.NODE_ENV === 'production' ? 你的websocket生产地址' : '测试地址';
this.globalCallback = null;
this.userClose = false;
this.createWebSocket();
this.webSocketState = false
this.thatVue = thatVue
}

createWebSocket() {
let that = this;
// console.log('
开始创建websocket新的实例', new Date().toLocaleString())
if( typeof(WebSocket) != "function" ) {
alert("您的浏览器不支持Websocket通信协议,请更换浏览器为Chrome或者Firefox再次使用!")
}
try {
that.ws = new WebSocket(that.localUrl);
that.initEventHandle();
that.startHeartBeat()
} catch (e) {
that.reconnect();
}
}

//初始化
initEventHandle() {
let that = this;
// //连接成功建立后响应
that.ws.onopen = function() {
console.log("连接成功");
};
//连接关闭后响应
that.ws.onclose = function() {
// console.log('
websocket连接断开', new Date().toLocaleString())
if (!that.userClose) {
that.reconnect(); //重连
}
};
that.ws.onerror = function() {
// console.log('
websocket连接发生错误', new Date().toLocaleString())
if (!that.userClose) {
that.reconnect(); //重连
}
};
that.ws.onmessage = function(event) {
that.getWebSocketMsg(that.globalCallback);
// console.log('
socket server return '+ event.data);
};
}
startHeartBeat () {
// console.log('
心跳开始建立', new Date().toLocaleString())
setTimeout(() => {
let params = {
request: '
ping',
}
this.webSocketSendMsg(JSON.stringify(params))
this.waitingServer()
}, 30000)
}
//延时等待服务端响应,通过webSocketState判断是否连线成功
waitingServer () {
this.webSocketState = false//在线状态
setTimeout(() => {
if(this.webSocketState) {
this.startHeartBeat()
return
}
// console.log('
心跳无响应,已断线', new Date().toLocaleString())
try {
this.closeSocket()
} catch(e) {
console.log('
连接已关闭,无需关闭', new Date().toLocaleString())
}
this.reconnect()
//重连操作
}, 5000)
}
reconnect() {
let that = this;
if (that.lockReconnect) return;
that.lockReconnect = true; //没连接上会一直重连,设置延迟避免请求过多
setTimeout(function() {
that.createWebSocket();
that.thatVue.openSuccess(that) //重连之后做一些事情
that.thatVue.getSocketMsg(that)
that.lockReconnect = false;
}, 15000);
}

webSocketSendMsg(msg) {
this.ws.send(msg);
}

getWebSocketMsg(callback) {
this.ws.onmessage = ev => {
callback && callback(ev);
};
}
onopenSuccess(callback) {
this.ws.onopen = () => {
// console.log("连接成功", new Date().toLocaleString())
callback && callback()
}
}
closeSocket() {
let that = this;
if (that.ws) {
that.userClose = true;
that.ws.close();
}
}
}
export default webSocketClass;

作者:耀耀切克闹灬
来源:juejin.cn/post/7309687967063818292

收起阅读 »