注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

环信FAQ

环信FAQ

集成常见问题及答案
RTE开发者社区

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

新年 10 个面试题,我曾 10 次拷问我的灵魂

web
大家好,这里是大家的林语冰。坚持阅读,自律打卡,每天一次,进步一点。 免责声明 本文属于是语冰的直男翻译了属于是,略有删改,仅供粉丝参考。英文原味版请传送 10 Interview Questions Every JavaScript Developer S...
继续阅读 »

大家好,这里是大家的林语冰。坚持阅读,自律打卡,每天一次,进步一点



免责声明


本文属于是语冰的直男翻译了属于是,略有删改,仅供粉丝参考。英文原味版请传送 10 Interview Questions Every JavaScript Developer Should Know in 2024



本期共享的是 —— 新年里前端面试需要掌握的十大面试题,知识面虽小,但思路清晰。


JS 的世界日新月异,多年来面试趋势也与时俱进。本文科普了新年每个 JS 开发者必知必会十大基本问题,涵盖了从闭包到 TDD(测试驱动开发)的一系列主题,为大家提供应对现代 JS 挑战的知识和信心。


1. 闭包到底是什么鬼物?


闭包让我们有权从内部函数访问外部函数的作用域。当函数嵌套时,内部函数可以访问外部函数作用域中声明的变量,即使外部函数返回后也是如此:


const createCat = cat => {
return {
getCat: () => cat,
setCat: newCat => {
cat = newCat
}
}
}

const myCat = createCat('薛定谔')
console.log(myCat.getCat()) // 薛定谔

myCat.setCat('龙猫')
console.log(myCat.getCat()) // 龙猫

闭包变量是对外部作用域变量的实时引用,而不是拷贝。这意味着,如果变更外部作用域变量,那么变更会反映在闭包变量中,反之亦然,这意味着,在同一外部函数中声明的其他函数将可以访问这些变更。


闭包的常见用例包括但不限于:



  • 数据隐藏

  • 柯里化和偏函数(经常用于改进函数组合,比如形参化 Express 中间件或 React 高阶组件)

  • 与事件处理程序和回调共享数据


数据隐藏


封装是面向对象编程的一个重要特征。封装允许我们向外界隐藏类的实现细节。JS 中的闭包允许我们声明对象的私有变量:


// 数据隐藏
const createGirlFans = () => {
let fans = 0
return {
increment: () => ++fans,
decrement: () => --fans,
getFans: () => fans
}
}

柯里化函数和偏函数:


// 一个柯里化函数一次接受多个参数。
const add = a => b => a + b

// 偏函数是已经应用了某些参数的函数,
// 但没有完全应用所有参数。
const increment = add(1) // 偏函数

increment(2) // 3

2. 纯函数是什么鬼物?


纯函数在函数式编程中兹事体大。纯函数是可预测的,这使得它们比非纯函数更易理解、调试和测试。纯函数遵循两个规则:



  1. 确定性 —— 给定相同的输入,纯函数会始终返回相同的输出。

  2. 无副作用 —— 副作用是在被调用函数外部可观察到的、不是其返回值的任何 App 状态更改。


非确定性函数依赖于以下各项的函数,包括但不限于:



  • 随机数生成器

  • 可以改变状态的全局变量

  • 可以改变状态的参数

  • 当前系统时间


副作用包括但不限于:



  • 修改任何外部变量或对象属性(比如全局变量或父函数作用域链中的变量)

  • 打印到控制台

  • 写入屏幕、文件或网络

  • 报错。相反,该函数应该返回表明错误的结果

  • 触发任何外部进程


在 Redux 中,所有 reducer 都必须是纯函数。如果不是,App 的状态不可预测,且时间旅行调试等功能无法奏效。reducer 函数中的杂质还可能导致难以追踪的错误,包括过时的 React 组件状态。


3. 函数组合是什么鬼物?


函数组合是组合两个或多个函数,产生新函数或执行某些计算的过程:(f ∘ g)(x) = f(g(x))


const compose = (f, g) => x => f(g(x))

const g = num => num + 1
const f = num => num * 2

const h = compose(f, g)

h(20) // 42

React 开发者可通过函数组合来清理大型组件树。我们可以将它们组合,创建一个新的高阶组件,而不是嵌套组件,该组件可以通过附加功能强化传递给它的任何组件。


4. 函数式编程是什么鬼物?


函数式编程是一种使用纯函数作为主要组合单元的编程范式。组合在软件开发中兹事体大,几乎所有编程范式都是根据它们使用的组合单元来命名的:



  • 面向对象编程使用对象作为组合单元

  • 过程式编程使用过程作为组合单元

  • 函数式编程使用函数作为组合单元


函数式编程是一种声明式编程范式,这意味着,程序是根据它们做什么,而不是如何做来编写的。这使得函数式程序比命令式程序更容理解、调试和测试。它们往往更加简洁,这降低了代码复杂性,并使其更易维护。


函数式编程的其他关键方面包括但不限于:



  • 不变性 —— 不可变数据结构比可变数据结构更易推理

  • 高阶函数 —— 将其他函数作为参数或返回函数作为结果的函数

  • 避免共享可变状态 —— 共享可变状态使程序难以理解、调试和测试。这也使得推断程序的正确性更加头大


5. Promise 是什么鬼物?


JS 中的 Promise 是一个表示异步操作最终完成或失败的对象,它充当最初未知值的占位符,通常是因为该值的计算尚未完成。


Promise 的主要特征包括但不限于:



  • 有状态Promise 处于以下三种状态之一:

    • 待定:初始状态,既未成功也未失败

    • 已完成:操作成功完成

    • 拒绝:操作失败



  • 不可变:一旦 Promise 被完成或拒绝,其状态就无法改变。它变得不可变,永久保留其结果。这使得 Promise 在异步流控制中变得可靠。

  • 链接Promise 可以链接起来,这意味着,一个 Promise 的输出可以用作另一个 Promise 的输入。这通过使用 .then() 表示成功或使用 .catch() 处理失败来链接,从而允许优雅且可读的顺序异步操作。链接是函数组合的异步等价物。


const promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('成功!')
// 我们也可以在失败时 reject 新错误。
}, 1000)
})

promise
.then(value => {
console.log(value) // 成功
})
.catch(error => {
console.log(error)
})

6. TS 是什么鬼物?


TS 是 JS 的超集,由微软开发和维护。近年来,TS 的人气与日俱增,如果您是一名 JS 工程师,您最终很可能需要使用 TS。它为 JS 添加了静态类型,JS 是一种动态类型语言。静态类型可以辅助开发者在开发过程的早期发现错误,提高代码质量和可维护性。


TS 的主要特点包括但不限于:



  • 静态类型:定义变量和函数参数的类型,确保整个代码一致性。

  • 给力的 IDE 支持:IDE(集成开发环境)可以提供更好的自动补全、导航和重构,使开发过程更加高效。

  • 编译:TS 代码被转译为 JS,使其与任何浏览器或 JS 环境兼容。在此过程中,类型错误会被捕获,使代码更鲁棒。

  • 接口:接口允许我们指定对象和函数必须满足的抽象契约。

  • 与 JS 的兼容性:Ts 与现有 JS 代码高度兼容。JS 代码可以逐步迁移到 JS,使现有项目能够顺利过渡。


interface User {
id: number
name: string
}

type GetUser = (userId: number) => User

const getUser: GetUser = userId => {
// 从数据库或 API 请求用户数据
return {
id: userId,
name: '人猫神话'
}
}

防范 bug 的最佳方案是代码审查、TDD 和 lint 工具(比如 ESLint)。TS 并不能替代这些做法,因为类型正确性并无法保证程序的正确性。即使应用了所有其他质量措施后,TS 偶尔也会发现错误。但它的主要好处是通过 IDE 支持,提供改进的开发体验。


7. Web Components 是什么鬼物?


WC(Web 组件)是一组 Web 平台 API,允许我们创建新的自定义、可重用、封装的 HTML 标签,在网页和 Web App 中使用。WC 是使用 HTML、CSS 和 JS 等开放 Web 技术构建的。它们是浏览器的一部分,不需要外部库或框架。


WC 对于拥有一大坨可能使用不同框架的工程师的大型团队特别有用。WC 允许我们创建可在任何框架或根本没有框架中使用的可重用组件。举个栗子,Adobe(PS 那个公司)的某个设计系统是使用 WC 构建的,并与 React 等流行框架顺利集成。


WC 由来已久,但最近人气爆棚,尤其是在大型组织中。它们被所有主要浏览器支持,并且是 W3C 标准。


8. React Hook 是什么鬼物?


Hook 是让我们无需编写类即可使用状态和其他 React 功能的函数。Hook 允许我们通过调用函数而不是编写类方法,来使用状态、上下文、引用和组件生命周期事件。函数的额外灵活性使我们更好地组织代码,将相关功能分组到单个钩子调用中,并通过在单独的函数调用中实现不相关功能,分离不相关的功能。Hook 提供了一种给力且富有表现力的方式来在组件内编写逻辑。


重要的 React Hook 包括但不限于:



  • useState —— 允许我们向函数式组件添加状态。状态变量在重新渲染之间保留。

  • useEffect —— 允许我们在函数式组件中执行副作用。它将 componentDidMount/componentDidUpdate/componentWillUnmount 的功能组合到单个函数调用中,减少了代码,并创建了比类组件更好的代码组织。

  • useContext —— 允许我们使用函数式组件中的上下文。

  • useRef —— 允许我们创建在组件的生命周期内持续存在的可变引用。

  • 自定义 Hook —— 封装可重用逻辑。这使得在不同组件之间共享逻辑变得容易。


Hook 的规则:Hook 必须在 React 函数的顶层使用(不能在循环、条件或嵌套函数内),且能且只能在 React 函数式组件或自定义 Hook 中使用。


Hook 解决了类组件的若干常见痛点,比如需要在构造函数中绑定方法,以及需要将功能拆分为多个生命周期方法。它们还使得在组件之间共享逻辑以及重用有状态逻辑,而无需更改组件层次结构更容易。


9. 如何在 React 中创建点击计数器?


我们可以使用 useState 钩子在 React 中创建点击计数器,如下所示:


import React, { useState } from 'react'

const ClickCounter = () => {
const [count, setCount] = useState(0) // 初始化为 0

return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count => count + 1)}>Click me</button>
</div>

)
}

export default ClickCounter

粉丝请注意,当我们从现有状态派生新值时,将函数传递给 setCount 是最佳实践,确保我们始终使用最新状态。


10. TDD 是什么鬼物?


TDD(测试驱动开发)是一种软件开发方法,其中测试是在实际代码之前编写的。它围绕一个简短的重复开发周期,旨在确保代码满足指定的要求且没有错误。TDD 在提高代码质量、减少错误和提高开发者生产力方面,可以发挥至关重要的作用。


开发团队生产力最重要的衡量标准之一是部署频率。持续交付的主要障碍之一是对变化的恐惧。TDD 通过确保代码始终处于可部署状态,辅助减少这种恐惧。这使得部署新功能和错误修复更容易,提高了部署频率。


测试先行多了一大坨福利,包括但不限于:



  • 更好的代码覆盖率:测试先行更有可能覆盖所有极端情况。

  • 改进的 API 设计:测试迫使我们在编写代码之前考虑 API 设计,这有助于避免将实现细节泄漏到 API 中。

  • 更少的 bug:测试先行可以辅助在开发过程中尽早发现错误,这样更容易修复。

  • 更好的代码质量:测试先行迫使我们编写模块化、松耦合的代码,这样更容易维护和重用。


TDD 的关键步骤包括但不限于:



  1. 编写测试:此测试最初会失败,因为相应的功能尚不存在。

  2. 编写实现:足以通过测试。

  3. 自信重构:一旦测试通过,就可以自信重构代码。重构是在不改变其外部行为的情况下,重构现有代码的过程。其目的是清理代码、提高可读性并降低复杂性。测试到位后,如果我们犯错了,我们会立即因测试失败而收到警报。


重复:针对每个功能需求重复该循环,逐步构建软件,同时确保所有测试继续通过。


学习曲线:TDD 是一项需要相当长的时间才能培养的技能和纪律。经过大半年的 TDD 体验后,我们可能仍觉得 TDD 难如脱单,且妨碍了生产力。虽然但是,使用 TDD 两年后,我们可能会发现它已经成为第二天性,并且比以前更有效率。


耗时:为每个小功能编写测试一开始可能会感觉很耗时,但长远来看,这通常会带来回报,减少错误并简化维护。我常常告诫大家,“如果你认为自己没有时间进行 TDD,那么你真的没有时间跳过 TDD。”


本期话题是 —— 你遭遇灵魂拷问的回头率最高的面试题是哪一道?


欢迎在本文下方群聊自由言论,文明共享。谢谢大家的点赞,掰掰~


《前端 9 点半》每日更新,坚持阅读,自律打卡,每天一次,进步一点


26-cat.gif


作者:人猫神话
来源:juejin.cn/post/7334653735359348777
收起阅读 »

集帅(美)们,别再写 :key = "index" 啦!

web
浅聊一下 灵魂拷问:你有没有在v-for里使用过:key = "index",如果有,我希望你马上改正过来并且给我点个赞,如果没有,来都来了,顺手给我点个赞... 开始 在向掘友们解释为什么不能使用 :key = "index" 之前,我想我还得向你们铺垫一点...
继续阅读 »

浅聊一下


灵魂拷问:你有没有在v-for里使用过:key = "index",如果有,我希望你马上改正过来并且给我点个赞,如果没有,来都来了,顺手给我点个赞...


开始


在向掘友们解释为什么不能使用 :key = "index" 之前,我想我还得向你们铺垫一点东西


虚拟DOM


什么是虚拟DOM呢?虚拟DOM是一个对象,没想到吧...我们来看看Vue是如何将template模板里面的东西交给浏览器来渲染的


image.png


首先通过 compiler 将 template模板变成一个虚拟DOM,再将虚拟DOM转换成HTML,最后再交给浏览器V8引擎渲染,那么虚拟DOM是什么样的呢?


html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Documenttitle>
<script src="https://unpkg.com/vue@3/dist/vue.global.js">script>
head>

<body>
<div id="app">
<ul id="item">
<li v-for="item in list" class="item">{{item}}li>
ul>
div>
<script>
const { createApp, ref } = Vue
createApp({
setup() {
const list = ref(['vue','js','html'])
return {
list
}
}
}).
mount('#app')
script>
body>

html>

在这里,template模板实际上是


 <ul>
<li v-for="item in list">{{item}}li>
ul>

通过v-for循环,渲染出来了3个li


<ul>
<li>vue<li>
<li>js<li>
<li>html<li>
ul>

我们的compiler会将这个模板转化成虚拟DOM


let oldDom = {
tagName = 'ul',
props:{
//存放id 和 class 等
id:'item'
},
children[
{
tagName = 'li',
props:{
class:'item'
},
children:['vue']
},
{
tagName = 'li',
props:{
class:'item'
},
children:['js']
},
{
tagName = 'li',
props:{
class:'item'
},
children:['html']
},
]
}

diff算法


给前面的例子来点刺激的,加上一个按钮和反转函数,点击按钮,list反转


html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Documenttitle>
<script src="https://unpkg.com/vue@3/dist/vue.global.js">script>
head>

<body>
<div id="app">
<ul>
<li v-for="item in list">{{item}}li>
ul>
<button @click="change">changebutton>
div>
<script>
const { createApp, ref } = Vue
createApp({
setup() {
const list = ref(['唱','跳','rap','篮球'])
const change = ()=>{
list.
value.reverse()
}
const add = ()=>{
list.
value.unshift('6')
}
return {
list,
change,
}
}
}).
mount('#app')
script>
body>

html>

点击change按钮,此时我们的DOM更改vue又是如何来更新DOM的呢?


image.png


众所周知,回流和重绘会消耗极大的性能,而当DOM发生变更的时候会触发回流重绘(可以去看我的文章(从输入4399.com到页面渲染之间的回流和重绘),那么vue3就有一个diff算法,用来优化性能


image.png


当DOM更改,compiler会生成一个新的虚拟DOM,然后通过diff算法来生成一个补丁包,用来记录旧DOM和新DOM的差异,然后再拿到html里面进行修改,最后再交给浏览器V8进行渲染


简单介绍一下diff算法的比较规则



  1. 同层比较,是不是相同的结点,不相同直接废弃老DOM

  2. 是相同结点,比较结点上的属性,产生一个补丁包

  3. 继续比较下一层的子节点,采用双端对列的方式,尽量复用,产生一个补丁包

  4. 同上


image.png


别再写 :key = "index"


要说别写 :key = "index" ,我们得先明白key是用来干什么的...如果没有key,那么在diff算法对新旧虚拟DOM进行比较的时候就没法比较了,你看这里有两个一样的vue,当反转顺序以后diff算法不知道哪个vue该对应哪个vue了


image.png


如果我们用index来充当key的话来看,当我们在头部再插入一个结点的时候,后面的index其实是改变了的,导致diff算法在比较的时候认为他们与原虚拟DM都不相同,那么diff算法就等于没有用...


image.png


可以用随机数吗?


  • for="item in list" :key="Math.random()">

  • 想出这种办法的,也是一个狠人...当然是不行的,因为在template模板更新时,会产生一个新的虚拟DOM,而这个虚拟DOM里面的key也是随机值,和原虚拟DOM里的key99.99999%是不一样的...


    结尾


    希望你以后再也不会写 :key = "index" 了


    作者:滚去睡觉
    来源:juejin.cn/post/7337513012394115111
    收起阅读 »

    图片转base64,实现图片上传?你学会了吗?

    web
    前言 前段时间在写我的VUE全栈项目的时候,遇到要把前端的照片上传到后端,再由后端存到数据库的问题,通过网上查找资料,看了一些其他写者的解决方法,最终采用转BASE64的方法。本人觉得把上传的图片转为BASE64格式相比其他是比较简单的。 什么是BASE64 ...
    继续阅读 »

    前言


    前段时间在写我的VUE全栈项目的时候,遇到要把前端的照片上传到后端,再由后端存到数据库的问题,通过网上查找资料,看了一些其他写者的解决方法,最终采用转BASE64的方法。本人觉得把上传的图片转为BASE64格式相比其他是比较简单的。


    什么是BASE64


    Base64是一种用64个字符来表示任意二进制数据的方法。它是一种编码方式,而非加密方式,即可以将一张图片数据编码成一串字符串,使用该字符串代替图像地址


    BASE64的优缺点


    优点: 减少一张图片的http请求


    缺点: 导致转换后的css文件体积增大,而CSS 文件的体积直接影响渲染,导致用户会长时间注视空白屏幕,而且转换后的数据是一大串字符串。



    注意:图片转BASE64格式的适合小图片或者极简单图片,大图片不划算。它的格式为:data:image/type;base64,xxxx...



    虽然说这种方式不适用于体积大的图片,但不得不说有时候还挺方便的。由于在我的vue项目中上传的图片都比较小,单一,为了方便我采用了这种方式来实现将前端上传的图片存到数据库中。


    话不多说,进入正题!下面以Vue+Koa框架、数据库为MYSQL为例。


    案例


    前端: 首先在把图片传给后端之前,前端对图片进行格式转换,转换成功后就可以照常调用后端给的接口,传进去就行。



    我这里就是点了提交按钮后,触发编写的点击事件函数,在函数里先对图片转base64,转成功后再调用后端给的接口,把此数据以及其他数据传进去就行。



    在前端编写转base64的函数(很重要)


    export function uploadImgToBase64 (file) {
    return new Promise((resolve, reject) => {
    const reader = new FileReader()//html5提供的一种异步文件读取机制
    reader.readAsDataURL(file)//将文件读取为Base64编码的数据URL
    reader.onload = function () { // 图片转base64完成后返回reader对象
    resolve(reader)
    }
    reader.onerror = reject
    })
    }

    前端要为转为base64的图片的相应操作


    const state = reactive({
    picture: [] //这个是用来装表单里选中的照片
    })


    const imgBroadcastListBase64 = [] //用来存放转base64后的照片,用数组是因为上传的图片可能不止一张
    console.log('图片转base64开始...')
    // 遍历每张图片picture 异步
    const filePromises = state.picture.map(async file => {
    //console.log(file);
    const response = await uploadImgToBase64(file.file) //调用函数 将图片转为base64
    //console.log(response,111);
    return response.result.replace(/.*;base64,/, '') // 去掉data:image/jpeg;base64,
    })
    // 按次序输出 base64图片
    for (const textPromise of filePromises) {
    imgBroadcastListBase64.push(await textPromise)
    }
    console.log('图片转base64结束..., ', imgBroadcastListBase64)


    //判断imgBroadcastListBase64是否<=1,是的话就是上传一张图片,否则上传的是多张图片
    if(imgBroadcastListBase64.length<=1){
    state.imgsStr = imgBroadcastListBase64.join()//转字符串
    }else{
    state.imgsStr = imgBroadcastListBase64.join(',')//转字符串并且每个值用','拼接,这样是为了方便后面从数据库拿到数据,将图片又转为之前的base64格式
    }
    //调用后端提供的接口,传数据到数据库里(这个只是自己编写的后端接口,主要是为了展示传数据)
    const res = await secondGoodsAdd({
    create_time: ti,
    content_goods: state.content,
    color: state.title2,
    price: state.title3,
    tel: state.title4,
    img: state.imgsStr,//转base64后的图片
    concat_num: 0,
    like_num: 0,
    name_goods: state.title1
    })
    if (res.code === '80000') {
    showSuccessToast('发布成功!')
    }
    router.push('/cicle')


    存到数据库中的图片路径是转为base64后的且删除前面data:image/jpeg;base64的字符串。



    这样数据就存到数据库中啦!存进去的是字符串。


    那么问题来了,数据是存进去了,但是我又想拿到这个数据到前端显示出来,好,那我就先直接拿到前端用,结果发现报错了!说是请求头太长?想办法解决下!后面我的解决办法是拿到这个转为了base64且去掉前面data...字段的的图片数据再转为正常的base64的格式。好,来转换吧!


    //我这里是在后端编写的接口,用于展示被添加到数据库中的所有数据
    router.get('/cirleLifeLook',async(ctx,next)=>{
    try {
    const result=await userService.cirleLifeLook()
    for(let i=0;i<result.length;i++){
    var imgData=result[i].img //获取每条数据中的照片字段
    if(imgData){
    if(imgData.indexOf(',')>-1){//存在','的话代表是多张图片的混合的字符串
    let ans=imgData.split(',') //切割 获得多张之前切掉data...后的base64字符串
    let s=[]
    for(let j=0;j<ans.length;j++){
    s.push("data:image/png;base64,"+ans[j])//还原每张图片初始的base64数据
    }
    result[i].img=s
    }else{
    result[i].img="data:image/png;base64,"+imgData //就一张图片直接在前面拼接"data:image/png;base64,"
    }
    }
    }//到此为止,在给前端传数据前就修改了其中每条数据里的照片地址,这样就可以正常显示啦
    if(result.length){
    ctx.body={
    code:'80000',
    data:result,
    msg:'获取成功'
    }
    }else{
    ctx.body={
    code:'80005',
    data:'null',
    msg:'还没有信息'
    }
    }
    } catch (error) {
    ctx.body={
    code:'80002',
    data:error,
    msg:'服务器异常'
    }
    }
    })

    OK,到此就结束啦~现在是不是觉得把图片转base64还是挺简单的?还挺有用的?快去实践下吧。记住此方法只适合小图片类型的,大点的文件可能会崩掉哈!


    结束语


    本篇文章就到此为止啦,由于本人经验水平有限,难免会有纰漏,对此欢迎指正。如觉得本文对你有帮助的话,欢迎点赞收藏❤❤❤,您的点赞是持续写作的动力,感谢支持。要是您觉得有更好的方法,欢迎评论,提出建议!


    作者:嗯嗯呢
    来源:juejin.cn/post/7255785481119727672
    收起阅读 »

    将一个图片地址转成文件流(File)再上传

    web
    写在开头 最近,小编在业务中遇到一个图片转存的场景。 领导🤠:大概过程就是,接口会给我返回一个图片列表数据,图片路径是全路径,但是路径中的域名是其他系统的,必须要在用户选择图片的时候将图片重新转存到自个的系统上,防止其他系统删除图片对此有影响。 我😃:Em....
    继续阅读 »

    写在开头


    最近,小编在业务中遇到一个图片转存的场景。


    领导🤠:大概过程就是,接口会给我返回一个图片列表数据,图片路径是全路径,但是路径中的域名是其他系统的,必须要在用户选择图片的时候将图片重新转存到自个的系统上,防止其他系统删除图片对此有影响。


    14DBA96E.gif


    我😃:Em...很合理的需求。


    (但是,和有什么关系?我只是一个前端小菜鸡呀,不祥的预感.......)


    我😃:(卑微提问)这个过程不是放后端做比较合理一点?


    后端大哥😡:前端不能做?


    我😣:可以可以,只是...这个好像会跨域?


    后端大哥😠:已经配置了请求头('Access-Control-Allow-Origin': '*')。


    我😖:哦,好的,我去弄一下。(*******此处省略几万字心理活动内容)


    14F03F73.jpg

    第一种(推荐)


    那么,迫于......不,我自愿的,我们来看看前端要如何完成这个转成过程,代码比较简单,直接贴上来瞧瞧:


    async function imageToStorage(path) {
    // 获取文件名
    const startIndex = path.lastIndexOf('/');
    const endIndex = path.indexOf('?');
    const imgName = path.substring(startIndex + 1, endIndex);
    // 获取图片的文件流对象
    const file = await getImgToFile(path, imgName);
    // TODO: 将File对象上传到其他接口中
    }

    /**
    * @name 通过fetch请求文件,将文件转成文件流对象
    * @param { string } path 文件路径全路径
    * @param { string } fileName 文件名
    * @returns { File | undefined }
    */

    function getImgToFile(path, fileName) {
    const response = await fetch(path);
    if (response) {
    const blob = await response.blob();
    const file = new File([blob], fileName, { type: blob.type });
    return file;
    }
    }

    上述方式,在后端配置了允许跨域后,正常是没有什么问题的,也是比较好的一种方式了。😃


    但是,在小编实际第一次编码测试后,却还是遇上了跨域。😓


    image.png


    一猜应该就是后端实际还没配置好,问了一下。


    后端大哥😑:还没部署,一会再自己试试。


    我😤:嗯嗯。


    第二种


    等待的过程,小编又在网上找了找了,找到了第二种方式,各位看官可以瞧瞧:


    /** @name 将图片的网络链接转成base64 **/
    function imageUrlToBase64(imageUrl: string, fileName: string): Promise<File> {
    return new Promise(resolve => {
    const image = new Image();
    // 让Image元素启用cors来处理跨源请求
    image.setAttribute('crossOrigin', 'anonymous');
    image.src = imageUrl + '&v=' + Math.random();
    image.onload = () => {
    const canvas = document.createElement('canvas');
    canvas.width = image.width;
    canvas.height = image.height;
    const context = canvas.getContext('2d')!;
    context.drawImage(image, 0, 0, image.width, image.height);
    // canvas.toDataURL
    const imageBase64 = canvas.toDataURL('image/jpeg', 1); // 第二个参数是压缩质量
    // 将图片的base64转成文件流
    const file = base64ToFile(imageBase64, fileName);
    resolve(file);
    };
    });
    }
    /** @name 将图片的base64转成文件流 **/
    function base64ToFile(base64: string, fileName: string) {
    const baseArray = base64.split(',');
    // 获取类型与后缀名
    const mime = baseArray[0].match(/:(.*?);/)![1];
    const suffix = mime.split('/')[1];
    // 转换数据
    const bstr = atob(baseArray[1]);
    let n = bstr.length;
    const u8arr = new Uint8Array(n);
    while (n--) {
    u8arr[n] = bstr.charCodeAt(n);
    }
    // 生成文件流
    const file = new File([u8arr], `${fileName}.${suffix}`, {
    type: mime,
    });
    return file;
    }

    这第二种方式由于要先把图片绘制到 canvas 再去转成 base64 再去转成文件流,小编用 console.time 稍微测了一下,每次转化过程都要几百毫秒,图片越大时间越长,挺影响性能的。


    所以,小编还是推荐使用第一种方式,当然,最稳妥的方案是后端去搞最好了。😉



    网上很多都说第二种方式可以直接绕过跨域,各种谈论。😪


    主要就是这个 crossOrigin 属性。MDN解释


    它原理是通过了 CORS


    image.png


    或者可以再看看这个解释:传送门










    至此,本篇文章就写完啦,撒花撒花。


    image.png


    希望本文对你有所帮助,如有任何疑问,期待你的留言哦。

    老样子,点赞+评论=你会了,收藏=你精通了。


    作者:橙某人
    来源:juejin.cn/post/7336756027385872424
    收起阅读 »

    NestJS 依赖注入DI与控制反转IOC

    web
    1. 前言 在《NestJS 快速入门》和《NestJS HTTP 请求与响应 》两篇文章中,主要使用了 NestJS 控制器处理简单的逻辑,对于复杂的业务逻辑,通常使用单独的 Service 服务进行处理,然后可以把 Service 作为依赖注入到 Cont...
    继续阅读 »

    1. 前言


    在《NestJS 快速入门》和《NestJS HTTP 请求与响应 》两篇文章中,主要使用了 NestJS 控制器处理简单的逻辑,对于复杂的业务逻辑,通常使用单独的 Service 服务进行处理,然后可以把 Service 作为依赖注入到 Controller 中。


    2. 概念


    2.1 依赖注入、控制反转、容器


    何为容器?容器是可以装一些资源的。举个例子,普通的容器,生活中的容器比如保温杯,只能用来存储东西,没有更多的功能。而程序中的容器则包括数组、集合Set、Map 等。


    而复杂的容器,生活中比如政府,政府管理我们的一生,生老病死和政府息息相关。


    AB7410FF-F52F-4C58-9171-DFB6303157DD.png


    程序中复杂的容器比如NestJS 容器,它能够管理 Controller、Service 等组件,负责创建组件的对象、存储 组件的对象,还要负责 调用 组件的方法让其工作,并在一定的情况下 销毁 组件。


    依赖注入(Dependency Injection)是实现控制反转的一种方式。控制反转又是什么呢?控制反转(Inversion of Control)是指从容器中获取资源的方式跟以往有所不同。


    2.2 为什么需要控制反转


    2.2.1 依赖关系复杂、依赖顺序约束


    后端系统中有多个对象:



    • Controller 对象: 处理 HTTP 请求,调用 Service,返回响应。

    • Service 对象: 实现业务逻辑。

    • Repository 对象: 实现对数据库的增删改查。


    此外,还包括数据库链接对象 DataSource、配置对象 Config 等等。这些对象之间存在复杂的关系:



    • Controller 依赖 Service 实现业务逻辑。

    • Service 依赖 Repository 进行数据库操作。

    • Repository 依赖 DataSource 建立连接,而 DataSource 则需要从 Config 对象获取用户名密码等信息。


    这导致对象的创建变得复杂,需要理清它们之间的依赖关系,确保正确的创建顺序。例如:


    const config = new Config({ username: 'xxx', password: 'xxx'});
    const dataSource = new DataSource(config);
    const repository = new Repository(dataSource);
    const service = new Service(repository);
    const controller = new Controller(service);

    这些对象需要一系列初始化步骤后才能使用。此外,像 config、dataSource、repository、service、controller 这些对象不需要每次都新建一个,可以保持单例。在应用初始化时,需要明确依赖关系,创建对象组合,并确保单例模式,这是后端系统常见的挑战。


    2.3.2 高层逻辑直接依赖低层逻辑,违反依赖倒置规范



    依赖倒置: 什么是依赖倒置原则(Dependency Inversion Principle)高层模块不应该依赖底层模块,二者都应该依赖抽象(例如接口)。  抽象不应该依赖细节,细节(具体实现)应该依赖抽象。 



    1.举一个工厂例子,初始化时有工人、车间、工厂。


    2FE7E55F-42A4-48FF-9A6B-8E987E386A7F.png


    1.工厂是容器,车间是消费者,依赖工人和工人的服务,工人是依赖,是生产者。


    // 工人
    class Worker {
      manualProduceScrew(){
        console.log('A screw is built')
      }
    }

    // 螺丝生产车间
    class ScrewWorkshop {
      private worker: Worker = new Worker()
     
      produce(){
        this.worker.manualProduceScrew() // 调用工人的方法
      }
    }

    // 工厂
    class Factory {
      start(){
        const screwWorkshop = new ScrewWorkshop()
        screwWorkshop.produce()
      }
    }

    const factory = new Factory()
    // 工厂开工啦!!!
    factory.start()



    2.现在要把工人制造改为机器制造,需要直接在车间把工人制造改为机器制造,麻烦。


    // 机器
    class Machine {
      autoProduceScrew(){
        console.log('A screw is built')
      }
    }

    class ScrewWorkshop {
      // 改为一个机器实例
      private machine: Machine = new Machine()
     
      produce(){
        this.machine.autoProduceScrew() // 调用机器的方法
      }
    }

    class Factory {
      start(){
        const screwWorkshop = new ScrewWorkshop()
        screwWorkshop.produce()
      }
    }

    const factory = new Factory()
    // 工厂开工啦!!!
    factory.start()


    3.此时考虑依赖倒置原则,通过实现生产者接口来处理(PS:这也是为什么像 Java 语言中,实现业务服务时需要定义接口类和实现类,遵循依赖倒置,方便切换不同的业务逻辑)


    // 定义一个生产者接口
    interface Producer {
      produceScrew: () => void
    }

    // 实现了接口的机器
    class Machine implements Producer {
      autoProduceScrew(){
        console.log('A screw is built')
      }
     
      produceScrew(){
        this.autoProduceScrew()
      }
    }

    // 实现了接口的工人
    class Worker implements Producer {
      manualProduceScrew(){
        console.log('A screw is built')
      }
     
      produceScrew(){
        this.manualProduceScrew()
      }
    }

    class ScrewWorkshop {
      // 依赖生产者接口,可以随意切换啦!!!
      // private producer: Producer = new Machine()
      private producer: Producer = new Worker()
     
      produce(){
        this.producer.produceScrew() // 工人和机器都提供了相同的接口
      }
    }

    class Factory {
      start(){
        const screwWorkshop = new ScrewWorkshop()
        screwWorkshop.produce()
      }
    }

    const factory = new Factory()
    // 工厂开工啦!!!
    factory.start()



    4.工厂改造后,螺丝生产车间的改造变得更容易了,只需要改变其属性中所新建的遵循Producer接口的实例即可。然而,这并没有完全改善我们与车间主任之间的关系,每次厂里在改造生产机器时我们还是需要麻烦车间主任。这是因为我们还是没有完全遵守依赖倒置原则,ScrewWorkshop 仍然依赖了 Worker/Machine的实例,只不过这种依赖相较之前少了一点罢了。


    要完全遵守依赖倒置原则,需要使用控制反转依赖注入


    2.3 控制反转思想


    2.3.1 获取资源的传统方式



    • 自己做饭:买菜、洗菜、择菜、切菜、炒菜,全过程参与,费时费力,必须了解资源创建整个过程中的全部细节并熟练掌握。

    • 在应用程序中,组件需要获取资源,传统的方式是主动从容器中获取所需资源,这样开发人员需要知道具体容器中特定资源的获取方式,增加了学习成本,也降低了开发效率。


    2.3.2 获取资源的控制反转方式



    • 点外卖:下单、等待、吃外卖,省时省力,不必关心资源创建过程的全部细节。

    • 控制反转的思想改变了应用程序组件获取资源的方式,容器会主动将资源推送给需要的组件,开发人员只需要提供接收资源的方式即可,这样可以降低学习成本,提高开发效率。这种方式被称为查找的被动方式。


    2.4 如何实现控制反转


    起源:许多应用程序的业务逻辑实现需要两个或多个类之间的协作,这种协作使得每个对象都需要获取与其合作的对象(即其所依赖的对象的引用)。如果这种获取过程由对象自身实现,那么将导致代码高度耦合,难以维护和调试。


    技术描述


    在 Class A 中,我们使用了 Class B 的对象 b。通常情况下,我们需要在 A 的代码中显式地使用 new 来创建 B 的对象。但是,如果采用依赖注入技术,A 的代码只需要定义一个 private 的 B 对象,而不需要直接 new 来获取这个对象。相反,我们可以通过相关的容器控制程序来在外部创建 B 对象,并将其注入到 A 类中的引用中。具体获取的方法以及对象被获取时的状态由配置文件(如 XML)来指定。这种方法可以使代码更加清晰和正式。


    loc 也可以理解为把流程的控制从应用程序转移到框架之中。以前,应用程序掌握整个处理流程;现在,控制权转移到了框架,框架利用一个引擎驱动整个流程的执行,框架会以相应的形式提供一系列的扩展点,应用程序则通过定义扩展的方式实现对流程某个环节的定制。“框架Call 应用”。基于 MVC 的 web 应用程序就是如此。


    实现方法


    实现控制反转主要有两种方式:依赖注入和依赖查找。两者的区别在于,前者是被动的接收对象,在类 A 的实例创建过程中即创建了依赖的 B对象,通过类型或名称来判断将不同的对象注入到不同的属性中,而后者是主动索取相应类型的对象,获得依赖对象的事件也可以在代码中自由控制。


    细说


    1.依赖注入:



    • 基于接口。实现特定接口以供外部容器注入所依赖类型的对象

    • 基于set方法。实现特定属性的publicSet方法,来让外部容器调用传入所依赖类型的对象

    • 基于构造函数。实现特定参数的构造函数,在新建对象时传入所依赖类型的对象

    • 基于注解。基于Java的注解功能,在私有变量前加“@Autowired”等注解,不需要显式的定义以上三种代码,便可以让外部容器传入对应的对象。该方案相当于定义了public的set方法,但是因为没有真正的set方法,从而不会为了实现依赖注入导致暴露了不该暴露的接口(因为set方法只想让容器访问来注入而并不希望其他依赖此类的对象访问)。


    2.依赖查找


    依赖查找更加主动,在需要的时候通过调用框架提供的方法来获取对象,获取时需要提供相关的配置文件路径、key等信息来确定获取对象的状态。


    2.4.1 工厂例子依赖注入改造


    通过以上学习,现在把工厂例子代码进一步改造,将底层类的依赖,由从中间类直接引用,变为高层类在构造时的依赖注入


    // ......Worker/Machine及其所遵循的接口Producer的实现与此前一致,此处省略

    class ScrewWorkshop
      private producer: Producer
     
      // 通过构造函数注入
      constructor(producer: Producer){
        this.producer = producer
      }
     
      produce(){
        this.producer.produceScrew()
      }
    }

    class Factory {
      start(){
        // 在Factory类中控制producer的实现,控制反转啦!!!
        // const producer: Producer = new Worker()
        const producer: Producer = new Machine()
        // 通过构造函数注入
        const screwWorkshop = new ScrewWorkshop(producer)
        screwWorkshop.produce()
      }
    }

    const factory = new Factory()
    // 工厂开工啦!!!
    factory.start()



    至此,回顾对这个车间的改造三步



    1. 依赖倒置: 解除ScrewWorkshop与Worker/Machine具体类之间的依赖关系,转为全部依赖Producer接口;

    2. 控制反转: 在Factory类中实例化ScrewWorkshop中需要使用的producer,ScrewWorkshop的对依赖项Worker/Machine的控制被反转了;

    3. 依赖注入: ScrewWorkshop中不关注具体producer实例的创建,而是通过构造函数constructor注入;


    3. NestJS 依赖注入


    在Nest的设计中遵守了控制反转的思想,使用依赖注入(包括构造函数注入、参数注入、Setter方法注入)解藕了Controller 与 Provider之间的依赖。


    我们将Nest中的元素与我们自己编写的工厂进行一个类比:



    1. Provider & Worker/Machine:真正提供具体功能实现的低层类。

    2. Controller & ScrewWorkshop:调用低层类来为用户提供服务的高层类。

    3. Nest框架本身 & Factory:控制反转容器,对高层类和低层类统一管理,控制相关类的新建与注入,解藕了类之间的依赖。


    IOC 机制是在 class 上标识哪些是可以被注入的,它的依赖是什么,然后从入口开始扫描这些对象和依赖,自动创建和组装对象。


    Nest 实现了 IOC 容器,会从入口模块开始扫描,分析 Module 之间的引用关系,对象之间的依赖关系,自动把 provider 注入到目标对象。


    Nest 里通过 @Controller 声明可以被注入的 controller,通过 @Injectable 声明可以被注入也可以注入别的对象的 provider,然后在 @Module 声明的模块里引入。并且Nest 还提供了 Module 和 Module 之间的 import,可以引入别的模块的 provider 来注入。


    provider 一般都是用 @Injectable 修饰的 class:


    2864AB0B-6A5F-4C29-AE1C-87AD610BA635.png


    在 Module 的 providers 里声明:


    27933B1A-306E-43E2-8203-598E1998B6B0.png


    上面是一种简写,完整的写法是这样的


    4CF33A34-F899-4AA0-9DA0-B93D177C9760.png


    构造函数或者属性注入


    E9561C4C-D02B-47DD-8B66-D0076666E2A8.png


    异步的注入对象


    24974844-A158-4FC1-8A14-33A1AF8033FB.png


    通常情况下,提供者通过使用 @Injectable 声明,然后在 @Moduleproviders 数组中注册类来实现。默认的 token 是类本身,因此不需要使用 @Inject 来指定注入的 token。


    但是,也可以使用字符串类型的 token,但在注入时需要单独指定 @Inject。除了可以使用 useClass 指定注入的类,还可以使用 useValue 直接指定注入的对象。如果想要动态生成对象,则可以使用 useFactory,它的参数也注入到 IOC 容器中的对象中,然后动态返回提供者的对象。如果想要为已有的 token 指定一个新的 token,可以使用 useExisting 来创建别名。通过灵活运用这些提供者类型,可以在 Nest 的 IOC 容器中注入任何对象。


    4.实践


    之前部门逻辑都是放在 controller 中的,现在可以把逻辑放 dept.service.ts  上来,添加 @Injectable 装饰器。


    9102DFFB-B32C-4ECE-8A5E-4D34135A6391.png


    在 DeptModule  模块中的 propviders 中引入 DeptService


    1397E621-39A4-4EF7-829A-8500C7B7B0B6.png


    最后在 dep.controller  使用部门服务,通过 @Inject() 装饰器注入。


    C63CF5D6-F08C-440C-9255-5688F35ADA41.png


    小结


    本文我们学习了什么是容器、依赖注入与控制反转,掌握了 Nest 依赖注入使用,并通过部门服务例子进行演示。


    参考资料



    作者:jecyu
    来源:juejin.cn/post/7336055070508843048
    收起阅读 »

    前端最全的5种换肤方案总结

    web
    最近一年很多客户要求更换主题色,而团队的项目基础框架不一,因此几乎使用了所有的更换主题的方案。以下总结了各方案的实现以及优缺点,希望能帮助有需要更换主题色的伙伴少踩坑。 方案一:硬编码 对一些老的前端基础库和业务库样式没有提取公共样式,当有更换主题的需求,实现...
    继续阅读 »

    最近一年很多客户要求更换主题色,而团队的项目基础框架不一,因此几乎使用了所有的更换主题的方案。以下总结了各方案的实现以及优缺点,希望能帮助有需要更换主题色的伙伴少踩坑。


    方案一:硬编码


    对一些老的前端基础库和业务库样式没有提取公共样式,当有更换主题的需求,实现的方法只能是全局样式替换,工作量比较大,需要更改form表单、按钮、表格、tab、容器等所有组件的各种状态,此外还需更换icon图标。


    以下是我们的一个老项目实现主题色更换,全局样式替换接近500行,如下图所示:


    image.png


    image.png


    image.png


    总结: 对于这种老项目只能通过硬编码的方式去更改,工作量较大,好在老项目依赖同一个基础库和业务库,所以在一个项目上实现了也可以快速推广到其它项目。


    方案二:sass变量配置


    团队的基础组件库Link-ui是基于Eelement-ui二次开发,因此可以采取类似于Element-ui的方式进行主题更改,只需要设计师提供6个主题色即可完成主题色的更改,如下所示。


    image.png



    • 配置基础色
      基础色一般需要设计师提供,也可以通过配置化的方式实现,


    $--color-primary-bold: #1846D1 !default;
    $--color-primary: #2664FD !default;
    $--color-primary-light: #4D85FD !default;
    $--color-primary-light-1: #9AC0FE !default;
    $--color-primary-light-2: #C1DBFF !default;
    $--color-primary-lighter: #E8F2FF !default;



    • 从基础库安装包引入基础色和库的样式源文件
      image.png


    @import "./common/base_var.scss";

    /* 改变 icon 字体路径变量,必需 */
    $--font-path: '~link-ui-web/lib/theme-chalk/fonts';

    @import "~link-ui-web/packages/theme-chalk/src/index";


    • 全局引入


    import '@/styles/link-variables.scss';


    • 更换主题色
      只需要更改上面的6个变量即可实现主题色的更改,比如想改成红色,代码如下


    $--color-primary-bold: #D11824 !default;
    $--color-primary: #FD268E !default;
    $--color-primary-light: #D44DFD !default;
    // $--color-primary-light-1: #9AC0FE !default;
    $--color-primary-light-2: #DCC1FF !default;
    $--color-primary-lighter: #F1E8FF !default;

    image.png


    总结: 对于基础库和样式架构设计合理的项目更改主题色非常的简单,只要在配置文件更换变量的值即可。它的缺点是sass变量的更改每次都需要编译,很难实现配置化。


    方案三、css变量+sass变量+data-theme


    代码结构如下:


    image.png



    • 设计三套主题分别定义不同的变量(包含颜色、图标和图片)


      // theme-default.scss
    /* 默认主题色-合作蓝色 */
    [data-theme=default] {
    --color-primary: #516BD9;
    --color-primary-bold: #3347B6;

    --color-primary-light: #6C85E1;
    --color-primary-light-1: #C7D6F7;
    --color-primary-light-2: #c2d6ff;
    --color-primary-lighter: #EFF4FE;

    --main-background: linear-gradient(90deg,#4e68d7, #768ff3);

    --user-info-content-background-image: url('../../assets/main/top-user-info-bg.png');
    --msg-tip-content-background-image: url('../../assets/main/top-user-info-bg.png');
    ...
    }


      // theme-orange.scss
    // 阳光黄
    [data-theme=orange] {
    --color-primary: #FF7335;
    --color-primary-bold: #fe9d2e;

    --color-primary-light: #FECB5D;
    --color-primary-light-1: #FFDE8B;
    --color-primary-light-2: #fcdaba;
    --color-primary-lighter: #FFF3E8;

    --main-background: linear-gradient(90deg,#ff7335 2%, #ffa148 100%);


    --user-info-content-background-image: url('../../assets/main/top-user-info-bg-1.png');
    --msg-tip-content-background-image: url('../../assets/main/top-user-info-bg-1.png');
    ...
    }


      // theme-red.scss
    /* 财富红 */
    [data-theme=red] {
    --color-primary: #DF291E;
    --color-primary-bold: #F84323;

    --color-primary-light: #FB8E71;
    --color-primary-light-1: #FCB198;
    --color-primary-light-2: #ffd1d1;
    --color-primary-lighter: #FFEEEE;


    --main-background: linear-gradient(90deg,#df291e 2%, #ff614c 100%);

    --user-info-content-background-image: url('../../assets/main/top-user-info-bg-2.png');
    --msg-tip-content-background-image: url('../../assets/main/top-user-info-bg-2.png');
    ...
    }



    • 把主题色的变量作为基础库的变量


    $--color-primary-bold: var(--color-primary-bold) !default;
    $--color-primary: var(--color-primary) !default;
    $--color-primary-light: var(--color-primary-light) !default;
    $--color-primary-light-1: var(--color-primary-light-1) !default;
    $--color-primary-light-2: var(--color-primary-light-2) !default;
    $--color-primary-lighter: var(--color-primary-lighter) !default;


    • App.vue指定默认主题色


    window.document.documentElement.setAttribute('data-theme', 'default')

    data-theme会注入到全局的变量上,所以我们可以在任何地方获取定义的css变量


    image.png


    实现效果如下:


    image.png


    image.png


    image.png


    总结: 该方案是最完美的方案,但是需对颜色、背景图、icon等做配置,需设计师设计多套方案,工作量相对较大,适合要求较高的项目或者标准产品上面,目前我们的标准产品选择的是该方案。


    方案四:滤镜filter


    filter CSS属性将模糊或颜色偏移等图形效果应用于元素。滤镜通常用于调整图像,背景和边框的渲染。


    它有个属性hue-rotate() 用于改变图整体色调,设定图像会被调整的色环角度值。值为0deg展示原图,大于360deg相当于又绕一圈。
    用法如下:


    body {
    filter: hue-rotate(45deg);
    }


    产品新建UI单元测试运行录制.gif


    总结: 成本几乎为0,实现简单。缺点是对于某些图片或者不想改的颜色需要特殊处理。


    方案五:特殊时期变灰



    • filter还有个属性 grayscale() 改变图像灰度,值在 0% 到 100% 之间,值为0%展示原图,值为100% 则完全转为灰度图像。


    body {
    filter: grayscale(1);
    }

    image.png


    总结: 成本小,可以将该功能做成配置项,比如配置它的生效开始时间和生效结束时间,便于运营维护也不用频繁发布代码。


    总结


    以上就是实现换肤的全部方案,我们团队在实际项目都有使用,比较好推荐的方案是方案一、方案三、方案五,对于要求不高的切换主题推荐方案四,它的技术零成本,对于标准产品推荐方案三。如有更好的方案欢迎评论区交流。


    作者:_无名_
    来源:juejin.cn/post/7329573754987462693
    收起阅读 »

    vue3的宏到底是什么东西?

    web
    前言 从vue3开始vue引入了宏,比如defineProps、defineEmits等。我们每天写vue代码时都会使用到这些宏,但是你有没有思考过vue中的宏到底是什么?为什么这些宏不需要手动从vue中import?为什么只能在setup顶层中使用这些宏? ...
    继续阅读 »

    前言


    vue3开始vue引入了宏,比如definePropsdefineEmits等。我们每天写vue代码时都会使用到这些宏,但是你有没有思考过vue中的宏到底是什么?为什么这些宏不需要手动从vueimport?为什么只能在setup顶层中使用这些宏?


    vue 文件如何渲染到浏览器上


    要回答上面的问题,我们先来了解一下从一个vue文件到渲染到浏览器这一过程经历了什么?


    我们的vue代码一般都是写在后缀名为vue的文件上,显然浏览器是不认识vue文件的,浏览器只认识html、css、jss等文件。所以第一步就是通过webpack或者vite将一个vue文件编译为一个包含render函数的js文件。然后执行render函数生成虚拟DOM,再调用浏览器的DOM API根据虚拟DOM生成真实DOM挂载到浏览器上。


    progress.png


    vue3的宏是什么?


    我们先来看看vue官方的解释:



    宏是一种特殊的代码,由编译器处理并转换为其他东西。它们实际上是一种更巧妙的字符串替换形式。



    宏是在哪个阶段运行?


    通过前面我们知道了vue 文件渲染到浏览器上主要经历了两个阶段。


    第一阶段是编译时,也就是从一个vue文件经过webpack或者vite编译变成包含render函数的js文件。此时的运行环境是nodejs环境,所以这个阶段可以调用nodejs相关的api,但是没有在浏览器环境内执行,所以不能调用浏览器的API


    第二阶段是运行时,此时浏览器会执行js文件中的render函数,然后依次生成虚拟DOM和真实DOM。此时的运行环境是浏览器环境内,所以可以调用浏览器的API,但是在这一阶段中是不能调用nodejs相关的api


    而宏就是作用于编译时,也就是从vue文件编译为js文件这一过程。


    举个defineProps的例子:在编译时defineProps宏就会被转换为定义props相关的代码,当在浏览器运行时自然也就没有了defineProps宏相关的代码了。所以才说宏是在编译时执行的代码,而不是运行时执行的代码。


    一个defineProps宏的例子


    我们来看一个实际的例子,下面这个是我们的源代码:


    <template>
    <div>content is {{ content }}div>
    <div>title is {{ title }}div>
    template>

    <script setup lang="ts">
    import {ref} from "vue"
    const props = defineProps({
    content: String,
    });
    const title = ref("title")
    script>

    在这个例子中我们使用defineProps宏定义了一个类型为String,属性名为contentprops,并且在template中渲染content的内容。


    我们接下来再看看编译成js文件后的代码,代码我已经进行过简化:


    import { defineComponent as _defineComponent } from "vue";
    import { ref } from "vue";

    const __sfc__ = _defineComponent({
    props: {
    content: String,
    },
    setup(__props) {
    const props = __props;
    const title = ref("title");
    const __returned__ = { props, title };
    return __returned__;
    },
    });

    import {
    toDisplayString as _toDisplayString,
    createElementVNode as _createElementVNode,
    Fragment as _Fragment,
    openBlock as _openBlock,
    createElementBlock as _createElementBlock,
    } from "vue";

    function render(_ctx, _cache, $props, $setup) {
    return (
    _openBlock(),
    _createElementBlock(
    _Fragment,
    null,
    [
    _createElementVNode(
    "div",
    null,
    "content is " + _toDisplayString($props.content),
    1 /* TEXT */
    ),
    _createElementVNode(
    "div",
    null,
    "title is " + _toDisplayString($setup.title),
    1 /* TEXT */
    ),
    ],
    64 /* STABLE_FRAGMENT */
    )
    );
    }
    __sfc__.render = render;
    export default __sfc__;

    我们可以看到编译后的js文件主要由两部分组成,第一部分为执行defineComponent函数生成一个 __sfc__ 对象,第二部分为一个render函数。render函数不是我们这篇文章要讲的,我们主要来看看这个__sfc__对象。


    看到defineComponent是不是觉得很眼熟,没错这个就是vue提供的API中的 definecomponent函数。这个函数在运行时没有任何操作,仅用于提供类型推导。这个函数接收的第一个参数就是组件选项对象,返回值就是该组件本身。所以这个__sfc__对象就是我们的vue文件中的script代码经过编译后生成的对象,后面再通过__sfc__.render = renderrender函数赋值到组件对象的render方法上面。


    我们这里的组件选项对象经过编译后只有两个了,分别是props属性和setup方法。明显可以发现我们原本在setup里面使用的defineProps宏相关的代码不在了,并且多了一个props属性。没错这个props属性就是我们的defineProps宏生成的。


    convert.png


    我们再来看一个不在setup顶层调用defineProps的例子:


    <script setup lang="ts">
    import {ref} from "vue"
    const title = ref("title")

    if (title.value) {
    const props = defineProps({
    content: String,
    });
    }
    script>

    运行这个例子会报错:defineProps is not defined


    我们来看看编译后的js代码:


    import { defineComponent as _defineComponent } from "vue";
    import { ref } from "vue";

    const __sfc__ = _defineComponent({
    setup(__props) {
    const title = ref("title");
    if (title.value) {
    const props = defineProps({
    content: String,
    });
    }
    const __returned__ = { title };
    return __returned__;
    },
    });

    明显可以看到由于我们没有在setup的顶层调用defineProps宏,在编译时就不会将defineProps宏替换为定义props相关的代码,而是原封不动的输出回来。在运行时执行到这行代码后,由于我们没有任何地方定义了defineProps函数,所以就会报错defineProps is not defined


    总结


    现在我们能够回答前面提的三个问题了。



    • vue中的宏到底是什么?


      vue3的宏是一种特殊的代码,在编译时会将这些特殊的代码转换为浏览器能够直接运行的指定代码,根据宏的功能不同,转换后的代码也不同。


    • 为什么这些宏不需要手动从vueimport


      因为在编译时已经将这些宏替换为指定的浏览器能够直接运行的代码,在运行时已经不存在这些宏相关的代码,自然不需要从vueimport


    • 为什么只能在setup顶层中使用这些宏?


      因为在编译时只会去处理setup顶层的宏,其他地方的宏会原封不动的输出回来。在运行时由于我们没有在任何地方定义这些宏,当代码执行到宏的时候当然就会报错。



    如果想要在vue中使用更多的宏,可以使用 vue macros。这个库是用于在vue中探索更多的宏和语法糖,作者是vue的团队成员 三咲智子


    作者:欧阳码农
    来源:juejin.cn/post/7335721246931189795
    收起阅读 »

    【如诗般写代码】你甚至连注释都没玩明白

    web
    引言要问我认为最难的事是什么,那只有维护前人的代码。我经常能看见一个文件里,写上百行,没有几个函数,没有注释,没有换行,一头扎进去,闷头写到天昏地暗的什么修复bug,提升性能之类的都不是事,会用调试工具和分时渲染就够了但是看屎山是真的难受注释篇利用注释在编辑器...
    继续阅读 »

    引言

    要问我认为最难的事是什么,那只有维护前人的代码。
    我经常能看见一个文件里,写上百行,没有几个函数,没有注释,没有换行,
    一头扎进去,闷头写到天昏地暗的

    什么修复bug,提升性能之类的都不是事,会用调试工具和分时渲染就够了
    但是看屎山是真的难受


    注释篇

    1. 利用注释在编辑器开启代码提示

      image.png

      看到区别了吗,左边用的是文档注释,鼠标悬浮能看到变量描述
      右边用的是行内注释,没有任何作用

      初步认识了文档注释的好处后,很多人可能还是不用,因为嫌麻烦。
      所以编辑器也为你着想了,以 VSCode为例,输入 /**,就会自动生成文档注释
      如果在函数上面,再按下回车,还能补齐函数参数文档,如下图所示

      comment.gif


    1. 利用文档注释描述函数功能

      当我鼠标悬浮在函数上时,就能看到他的各种描述,从其他文件导入也有效
      image.png


    1. 智能提示当前参数描述,以及类型等

      这里也能用快捷键呼出,详见我上篇文章:# 【最高效编码指南】也许你不会用VSCode | IDEA

      image.png


    1. 添加 JS 类型,实现类似 TS 的效果

      这里我呼出代码提示,但是他并没有给我补全任何方法,因为他不知道你的类型是什么

      image.png

      如果是强类型语言的话,那就会给你补全代码
      那么动态类型如何实现呢?以 JS 为例,使用文档注释即可,也就是前阵子沸沸扬扬的利用 JSDoc 代替 TS

      image.png

      不仅于此,连枚举都能实现,反正 TS 有的,他应该都有,我没有详细研究

      image.png

    2. 文档注释指令

      如下图所示,我想在文档注释里写上用法,但是他的格式十分丑陋,而且没有语法高亮

      image.png

      于是我使用 example 指令,告诉他这是一个示例,这时就有语法高亮了

      image.png

      指令还有很多,你们输入 @ 就会有提示了,比如 deprecated,标记已弃用
      这时你使用它就会有个提示,并且划上一根线

      image.png

    3. MarkDown 文档注释

      有时候,指令可能不够用,这时就可以使用 MarkDown 语法了

      image.png

    4. 结合 TS

      定义类型时,写上文档注释,当你鼠标悬浮时,就能查看对应注释

      image.png

      函数重载情况下,文档注释要写在类型上才行,下面这种无效

      image.png

      要写在类型定义的地方才行

      image.png

    5. 总结

      如果你用的是变量、函数或是 TS 定义类型,你要写注释,那就一定要写 文档注释,我跪下来求求你了 😭

    减少条件分支语句

    1. 策略模式,写个映射表即可。这个有一点开发经验的应该都知道吧

      如果遇到复杂情况,映射表里也可以写函数,执行后返回逻辑

      image.png

    2. 提前返回

      这里第 2 种提前返回就减少了一层嵌套,实际开发中,能减少更多嵌套语句

      image.png

    3. 多个相等判断,使用数组代替

      image.png

    代码七宗罪

    让我来细数一下这坨代码的罪行,然后引出另一个主题,美化代码
    下面这段,这简直是"甲级战犯",

    1. 一堆变量写了或者导入了不用,放那恶心谁呢
    2. 注释了的代码不删 (虽然可能有用,但是真丑)
    3. 都什么年代了,还在用var (坏处下面说)
    4. 用行内注释和没写区别不大,要写就写文档注释 (文档注释的优点上面解释了,不再赘述)
    5. 小学生流水账一般的代码,连个函数入口都没提供,想一句写一句
    6. 连个代码格式化都不会,多按几个回车,你的键盘不会烂掉;每个分段加个注释,你的速度慢不了多少
    7. 硬编码,所有类型用字符串直接区分,你万一要改怎么办?

    image.png

    语义化

    我经常能看见一个文件里,写上百行,没有几个函数,没有注释,没有换行
    一头扎进去,闷头写到天昏地暗的,比如下面这种

    image.png

    这玩意要我一行一行看?我是真的被恶心坏了
    写代码要突出一个重点,看个大概,然后才能快速排查,看第三方库源码也是如此

    我的习惯是写一个主入口,你叫 main | init | start 什么的都行,我只希望你能写上
    然后主入口按照逻辑,给每个函数命名,这样一眼就能看出来你在干什么
    如下图所示,这是我的偏好

    image.png

    我喜欢利用函数全局提升,把初始化函数放在文件顶部。这样每次打开一个文件,就能立刻看到大概逻辑
    所以我很少用匿名函数,像上面那种全部写一坨,还都是匿名函数,我真的很难看出来谁是函数,谁是变量

    这就引出一个新问题,函数的二义性

    函数二义性

    众所周知, JS 的类就是函数,里面有自己的 this,可以 new 一个函数

    image.png

    你要知道他是函数还是类,一般是通过首字母是否大写区分
    但是这仅仅是弱规范,人家爱咋写咋写,所以后来出现了匿名函数(主要还是为了解决 this)

    匿名函数没有自己的 this 指向,没有 arguments,如下图

    image.png

    而且用 const 定义,所以也就没了函数提升,严格来说,匿名函数才是真函数

    不过我觉得直接写匿名函数有点丑,而且写起来似乎繁琐一点,虽然我都是用代码片段生成的
    如果用了匿名函数,那么我就没了函数提升了

    所以我仅仅在以下情况使用匿名函数

    1. 作为回调函数
    2. 不需要 this
    3. 函数重载

    函数重载我来说说吧,应该挺多人不知道。
    比如下图,针对每一种情况,写一遍类型,这样就能更加清楚描述函数的所有参数情况

    image.png

    不过这样好麻烦,而且好丑啊,于是可以用接口,这时你用 function 就实现不了了

    image.png

    var 的坏处

    1. var 会变量提升,你可能拿到 undefined

      image.png

    2. var 没有块级作用域,会导致变量共享

      按照常识,下面代码应该输出 0,1,2,3,4

      image.png

      但是你里面是异步打印,于是等你打印时,i 以及加了5次了,又没有块级作用域,所以你拿到的是同一个东西

      在古时候,是用立即执行函数解决的,如下图。因为函数会把变量存起来传给内部

      image.png

      现在用 let 就行了

      image.png

      所以我求求你别用 var 了

    格式化

    这里可能有争议性,仅仅是我个人喜欢,看着舒服

    大多数写前端的,基本人手一个 Prettier 插件自动格式化,再来个 EsLint
    然后也懒得看配置,默认就是 2 格缩进,回车多了会被删掉什么的

    这样下来,整个文件就相当臃肿,密密麻麻的,我看着很难受

    我的风格如下

    • 用 4 格缩进
    • 代码按照语义类型分块,写上块级文档注释
    • import 语句下面空两行,这样更加直观
    • 每一段,用独特醒目的文档注释划分
    • 定义变量优先使用 const,并且只写一个 const
    • 函数参数过长,则一行放一个参数
    • 写行内样式以及较长字符串时( 比如函数作为字符串 ),用特殊的宽松格式书写,保持类似代码的格式化
    • if 分支语句,要多空一行,看着清爽
    • 三目运算,用三行来写
    • 条件判断尽量提前 return,减少分支缩进

    下面来用图演示一下,不然看着上面的描述抽象

    代码按照语义类型分块,写上块级文档注释

    每一段逻辑写完,用个醒目的、大块的文档注释分开。
    全部执行的逻辑,放在一个 init 函数中

    image.png

    定义变量优先使用 const,并且只写一个 const

    比如声明变量,我喜欢这么写
    按照分类,类型不同则换行,并且写上注释,仅用一个 const

    image.png

    来看看大众写法,可以说 99.9878987%的人都这么写,这种我一看就难受

    image.png

    如果你用 let,并且用 4 格缩进,那么你就刚好对齐了,能少写一个回车
    不过尽量使用 const

    image.png

    函数参数过长,则一行放一个参数

    如果你这么写,那我看完会头晕眼花,属实是又臭又长的参数列表

    image.png

    如果你这么写,我会夸你代码和人一样好看

    image.png

    三目运算格式化

    这俩,你说谁的可读性高,肯定是分三行写的好看啊

    image.png

    字符串以及对象格式化

    这俩,你说谁看得舒服,那肯定是 2 啊
    我看了身边的人和网上的很多代码,大多数都是 1 这种

    image.png

    不管你是用字符串,还是对象等方式表达,你都应该写 2 这种样式

    image.png

    分支语句

    这俩哪种好看还用说吗,肯定是左边的好啊。但是你用 Prettier 格式化的话,应该就变成右边的了
    同理 try catch 之类的也是一样

    image.png

    最后,多用换行,我跪下来求求你了 😭


    作者:寅时码
    来源:juejin.cn/post/7335277377621639219

    收起阅读 »

    前端实现excel_xlsx文件预览

    web
    使用的框架: React 要使用的库: exceljs、handsontable 1. 概述 接到一个任务,是要前端实现文件预览效果,百度了一圈,发现也没有什么好的方法可以完美的将表格渲染出来。在前端中有sheetjs和exceljs可以对xlsx文件进行解...
    继续阅读 »

    使用的框架: React


    要使用的库: exceljs、handsontable



    1. 概述


    接到一个任务,是要前端实现文件预览效果,百度了一圈,发现也没有什么好的方法可以完美的将表格渲染出来。在前端中有sheetjsexceljs可以对xlsx文件进行解析,本来一开始我用的是sheetjs,但是在样式获取上遇到了麻烦,所以我改用了exceljs,不过很难受,在样式获取时同样遇到了不小的麻烦,但是我懒得换回sheetjs了,那就直接使用exceljs吧。


    要实现xlsx文件预览效果,我的想法是使用一个库对xlsx文件进行解析,然后使用另一个库对解析出来的数据在页面上进行绘制,综上,我采用的方案是:exceljs+handsontable


    2. 实现步骤


    2.1 安装库


    使用命令: npm i exceljs handsontable @handsontable/react


    2.2 使用exceljs解析数据并使用handsontable进行渲染


    直接贴代码了:


    import Excel from 'exceljs'
    import { useState } from 'react';

    import { HotTable } from '@handsontable/react';
    import { registerAllModules } from 'handsontable/registry';
    import 'handsontable/dist/handsontable.full.min.css';
    import { textRenderer, registerRenderer } from 'handsontable/renderers';

    // 注册模块
    registerAllModules();

    export default function XLSXPreView() {
    const [data, setData] = useState([]);

    const handleFile = async (e) => {
    const file = e.target.files[0];

    const workbook = new Excel.Workbook();
    await workbook.xlsx.load(file)

    // 第一个工作表
    const worksheet = workbook.getWorksheet(1);

    // 遍历工作表中的所有行(包括空行)
    const sheetData = [];
    worksheet.eachRow({ includeEmpty: true }, function(row, rowNumber) {
    // console.log('Row ' + rowNumber + ' = ' + JSON.stringify(row.values));
    // 使用row.values获取每一行的值时总会多出一条空数据(第一条),这里我把它删除
    const row_values = row.values.slice(1);
    sheetData.push(row_values)
    });
    setData(sheetData);
    }

    return (
    <>
    <input type="file" onChange={handleFile}/>
    <div id='table_view'>
    <HotTable
    data={data}
    readOnly={true}
    rowHeaders={true}
    colHeaders={true}
    width="100vw"
    height="auto"
    licenseKey='non-commercial-and-evaluation'// 一定得加这个handsontable是收费的加了这个才能免费用
    />


    </div>
    </>

    )
    }

    到这里,已经实现了从xlsx文件中获取数据,并使用handsontable将表格中的数据渲染出来,示例结果如下,如果只需要将数据显示出来,并不需要将样式什么的一起复现了,那到这里就已经结束了!


    image.png


    但事实上,这并不是我要做到效果,我的xlsx里面还有样式什么的,也需要复现,头疼😔


    3. 其它的杂七杂八


    3.1 单元格样式


    事实上,在exceljs解析xlsx文件时,它顺带一起把样式获取到了,通过worksheet.getCell(1, 1).style可以获取对应单元格的样式,如下,背景色存放在fill.fgColor中,字体颜色存放在font.color中,这样的话只需要将这些样式一一赋值给handsontable组件再添加样式就好了。


    image.png


    但是实际操作的时候却遇到了问题,先说excel中的颜色,在选择颜色时,应该都会打开下面这个选项框吧,如果你选择的是标准色,它获取到的颜色就是十六进制,但是如果你选择主题中的颜色,那就是另一种结果了,并且还会有不同的深暗程度tint,这就很难受了!


    image.png


    随后在控制台中打印了workbook,发现它把主题返回了,可以通过work._themes.theme1获取,不过获取到的是xml格式的字符串,由于xml我没学,我不会,所以我就把它转换成json来进行处理了。


    第一步


    安装xml转json的库: npm i fast-xml-parser


    import {XMLParser} from 'fast-xml-parser'

    // 将主题xml转换成json
    const themeXml = workbook._themes.theme1;
    const options = {
    ignoreAttributes: false,
    attributeNamePrefix: '_'
    }
    const parser = new XMLParser(options);
    const json = parser.parse(themeXml)
    setThemeJson(json);


    其实它的theme好像是固定的,也可以在一些格式转换的网站中直接转换成json然后放到一个json文件中,读取就行,我这里就直接放到一个state中了!



    第二步


    接下来就是重头戏了!设置单元格样式...


    首先安装一个处理颜色的库color,用来根据tint获得不同明暗程度的颜色: npm i color


    下面是获取颜色的函数:


    // 根据主题和明暗度获取颜色
    const getThemeColor = (themeJson, themeId, tint) => {
    let color = '';
    const themeColorScheme = themeJson['a:theme']['a:themeElements']['a:clrScheme'];
    switch (themeId) {
    case 0:
    color = themeColorScheme['a:lt1']['a:sysClr']['_lastClr'];
    break;
    case 1:
    color = themeColorScheme['a:dk1']['a:sysClr']['_lastClr'];
    break;
    case 2:
    color = themeColorScheme['a:lt2']['a:srgbClr']['_val'];
    break;
    case 3:
    color = themeColorScheme['a:dk2']['a:srgbClr']['_val'];
    break;
    default:
    color = themeColorScheme[`a:accent${themeId-3}`]['a:srgbClr']['_val'];
    break;
    }
    // 根据tint修改颜色深浅
    color = '#' + color;
    const colorObj = Color(color);
    if(tint){
    if(tint>0){// 淡色
    color = colorObj.lighten(tint).hex();
    }else{ // 深色
    color = colorObj.darken(Math.abs(tint)).hex();
    }
    }
    return color;
    }
    // 获取颜色
    const getColor = (obj, themeJson) => {
    if('argb' in obj){ // 标准色
    // rgba格式去掉前两位: FFFF0000 -> FF0000
    return '#' + obj.argb.substring(2);
    }else if('theme' in obj){ // 主题颜色
    if('tint' in obj){
    return getThemeColor(themeJson, obj.theme, obj.tint);
    }else{
    return getThemeColor(themeJson, obj.theme, null);
    }
    }
    }

    然后设置handonsontable的单元格的一些样式:颜色、加粗、下划线、边框balabala...的


    顺带把行高和列宽一起设置了,这个还比较简单,就一笔带过了...


    3.2 合并单元格


    从获取到的sheet中有一个_meages属性,该属性中存放了表格中所有的合并单元格区域,所以只需要将它们重新渲染在handsontable中就好。


    image.png


    然后就实现了表格的一些基本功能的预览,结果如下图:


    image.png


    3. 总结(附全代码)


    其实这个的本质主要就是通过ecxeljs解析表格文件的数据,然后通过handsontable将它们重新绘制在页面上,个人觉得这种方法并不好,因为表格里的操作太多了要把它们一一绘制工作量实在是太大了,而且很麻烦,如有有更好的方案希望大佬们告诉我一下,我这里把表格的一些常用到的功能实现了预览,还有想表格里放图片什么的都没有实现,如果有需要,可以根据需求再进行进行写。



    我写的其实还有一点bug,单元格的边框样式我只设置了solid和dashed,但事实上excel中单元格的边框有12种样式,而且还有对角线边框,设置起来好麻烦,我就不弄了,大家用的时候注意一下哈,有需要的话可以自己修改一下!



    附上全部代码:


    /**
    * exceljs + handsontable
    */

    import Excel from 'exceljs'
    import { useState } from 'react';

    import { HotTable } from '@handsontable/react';
    import { registerAllModules } from 'handsontable/registry';
    import 'handsontable/dist/handsontable.full.min.css';
    import { textRenderer, registerRenderer } from 'handsontable/renderers';

    import {XMLParser} from 'fast-xml-parser'
    import Color from 'color';

    // 注册模块
    registerAllModules();

    // 根据主题和明暗度获取颜色
    const getThemeColor = (themeJson, themeId, tint) => {
    let color = '';
    const themeColorScheme = themeJson['a:theme']['a:themeElements']['a:clrScheme'];
    switch (themeId) {
    case 0:
    color = themeColorScheme['a:lt1']['a:sysClr']['_lastClr'];
    break;
    case 1:
    color = themeColorScheme['a:dk1']['a:sysClr']['_lastClr'];
    break;
    case 2:
    color = themeColorScheme['a:lt2']['a:srgbClr']['_val'];
    break;
    case 3:
    color = themeColorScheme['a:dk2']['a:srgbClr']['_val'];
    break;
    default:
    color = themeColorScheme[`a:accent${themeId-3}`]['a:srgbClr']['_val'];
    break;
    }
    // 根据tint修改颜色深浅
    color = '#' + color;
    const colorObj = Color(color);
    if(tint){
    if(tint>0){// 淡色
    color = colorObj.lighten(tint).hex();
    }else{ // 深色
    color = colorObj.darken(Math.abs(tint)).hex();
    }
    }
    return color;
    }
    // 获取颜色
    const getColor = (obj, themeJson) => {
    if('argb' in obj){ // 标准色
    // rgba格式去掉前两位: FFFF0000 -> FF0000
    return '#' + obj.argb.substring(2);
    }else if('theme' in obj){ // 主题颜色
    if('tint' in obj){
    return getThemeColor(themeJson, obj.theme, obj.tint);
    }else{
    return getThemeColor(themeJson, obj.theme, null);
    }
    }
    }
    // 设置边框
    const setBorder = (style) =>{
    let borderStyle = 'solid';
    let borderWidth = '1px';
    switch (style) {
    case 'thin':
    borderWidth = 'thin';
    break;
    case 'dotted':
    borderStyle = 'dotted';
    break;
    case 'dashDot':
    borderStyle = 'dashed';
    break;
    case 'hair':
    borderStyle = 'solid';
    break;
    case 'dashDotDot':
    borderStyle = 'dashed';
    break;
    case 'slantDashDot':
    borderStyle = 'dashed';
    break;
    case 'medium':
    borderWidth = '2px';
    break;
    case 'mediumDashed':
    borderStyle = 'dashed';
    borderWidth = '2px';
    break;
    case 'mediumDashDotDot':
    borderStyle = 'dashed';
    borderWidth = '2px';
    break;
    case 'mdeiumDashDot':
    borderStyle = 'dashed';
    borderWidth = '2px';
    break;
    case 'double':
    borderStyle = 'double';
    break;
    case 'thick':
    borderWidth = '3px';
    break;
    default:
    break;
    }
    // console.log(borderStyle, borderWidth);
    return [borderStyle, borderWidth];
    }

    export default function XLSXPreView() {
    // 表格数据
    const [data, setData] = useState([]);
    // 表格
    const [sheet, setSheet] = useState([]);
    // 主题
    const [themeJson, setThemeJson] = useState([]);
    // 合并的单元格
    const [mergeRanges, setMergeRanges] = useState([]);

    registerRenderer('customStylesRenderer', (hotInstance, td, row, column, prop, value, cellProperties) => {
    textRenderer(hotInstance, td, row, column, prop, value, cellProperties);
    // console.log(cellProperties);
    // 填充样式
    if('fill' in cellProperties){
    // 背景颜色
    if('fgColor' in cellProperties.fill && cellProperties.fill.fgColor){
    td.style.background = getColor(cellProperties.fill.fgColor, themeJson);
    }
    }
    // 字体样式
    if('font' in cellProperties){
    // 加粗
    if('bold' in cellProperties.font && cellProperties.font.bold){
    td.style.fontWeight = '700';
    }
    // 字体颜色
    if('color' in cellProperties.font && cellProperties.font.color){
    td.style.color = getColor(cellProperties.font.color, themeJson);
    }
    // 字体大小
    if('size' in cellProperties.font && cellProperties.font.size){
    td.style.fontSize = cellProperties.font.size + 'px';
    }
    // 字体类型
    if('name' in cellProperties.font && cellProperties.font.name){
    td.style.fontFamily = cellProperties.font.name;
    }
    // 字体倾斜
    if('italic' in cellProperties.font && cellProperties.font.italic){
    td.style.fontStyle = 'italic';
    }
    // 下划线
    if('underline' in cellProperties.font && cellProperties.font.underline){
    // 其实还有双下划线,但是双下划綫css中没有提供直接的设置方式,需要使用额外的css设置,所以我也就先懒得弄了
    td.style.textDecoration = 'underline';
    // 删除线
    if('strike' in cellProperties.font && cellProperties.font.strike){
    td.style.textDecoration = 'underline line-through';
    }
    }else{
    // 删除线
    if('strike' in cellProperties.font && cellProperties.font.strike){
    td.style.textDecoration = 'line-through';
    }
    }

    }
    // 对齐
    if('alignment' in cellProperties){
    if('horizontal' in cellProperties.alignment){ // 水平
    // 这里我直接用handsontable内置类做了,设置成类似htLeft的样子。
    //(handsontable)其实至支持htLeft, htCenter, htRight, htJustify四种,但是其是它还有centerContinuous、distributed、fill,遇到这几种就会没有效果,也可以自己设置,但是我还是懒的弄了,用到的时候再说吧
    const name = cellProperties.alignment.horizontal.charAt(0).toUpperCase() + cellProperties.alignment.horizontal.slice(1);
    td.classList.add(`ht${name}`);
    }
    if('vertical' in cellProperties.alignment){ // 垂直
    // 这里我直接用handsontable内置类做了,设置成类似htTop的样子。
    const name = cellProperties.alignment.vertical.charAt(0).toUpperCase() + cellProperties.alignment.vertical.slice(1);
    td.classList.add(`ht${name}`);
    }
    }
    // 边框
    if('border' in cellProperties){
    if('left' in cellProperties.border && cellProperties.border.left){// 左边框
    const [borderWidth, borderStyle] = setBorder(cellProperties.border.left.style);
    let color = '';
    // console.log(row, column, borderWidth, borderStyle);
    if(cellProperties.border.left.color){
    color = getColor(cellProperties.border.left.color, themeJson);
    }
    td.style.borderLeft = `${borderStyle} ${borderWidth} ${color}`;
    }
    if('right' in cellProperties.border && cellProperties.border.right){// 左边框
    const [borderWidth, borderStyle] = setBorder(cellProperties.border.right.style);
    // console.log(row, column, borderWidth, borderStyle);
    let color = '';
    if(cellProperties.border.right.color){
    color = getColor(cellProperties.border.right.color, themeJson);
    }
    td.style.borderRight = `${borderStyle} ${borderWidth} ${color}`;
    }
    if('top' in cellProperties.border && cellProperties.border.top){// 左边框
    const [borderWidth, borderStyle] = setBorder(cellProperties.border.top.style);
    let color = '';
    // console.log(row, column, borderWidth, borderStyle);
    if(cellProperties.border.top.color){
    color = getColor(cellProperties.border.top.color, themeJson);
    }
    td.style.borderTop = `${borderStyle} ${borderWidth} ${color}`;
    }
    if('bottom' in cellProperties.border && cellProperties.border.bottom){// 左边框
    const [borderWidth, borderStyle] = setBorder(cellProperties.border.bottom.style);
    let color = '';
    // console.log(row, column, borderWidth, borderStyle);
    if(cellProperties.border.bottom.color){
    color = getColor(cellProperties.border.bottom.color, themeJson);
    }
    td.style.borderBottom = `${borderStyle} ${borderWidth} ${color}`;
    }
    }

    });

    const handleFile = async (e) => {
    const file = e.target.files[0];

    const workbook = new Excel.Workbook();
    await workbook.xlsx.load(file)

    const worksheet = workbook.getWorksheet(1);

    // const sheetRows = worksheet.getRows(1, worksheet.rowCount);
    setSheet(worksheet)

    // console.log(worksheet.getCell(1, 1).style);

    // 遍历工作表中的所有行(包括空行)
    const sheetData = [];
    worksheet.eachRow({ includeEmpty: true }, function(row, rowNumber) {
    // console.log('Row ' + rowNumber + ' = ' + JSON.stringify(row.values));
    // 使用row.values获取每一行的值时总会多出一条空数据(第一条),这里我把它删除
    const row_values = row.values.slice(1);
    sheetData.push(row_values)
    });
    setData(sheetData);

    // 将主题xml转换成json
    const themeXml = workbook._themes.theme1;
    const options = {
    ignoreAttributes: false,
    attributeNamePrefix: '_'
    }
    const parser = new XMLParser(options);
    const json = parser.parse(themeXml)
    setThemeJson(json);

    // 获取合并的单元格
    const mergeCells = [];

    for(let i in worksheet._merges){
    const {top, left, bottom, right} = worksheet._merges[i].model;
    mergeCells.push({ row: top-1, col: left-1, rowspan: bottom-top+1 , colspan: right-left+1})
    }
    setMergeRanges(mergeCells)
    console.log(worksheet);
    }

    return (
    <>
    <input type="file" onChange={handleFile}/>
    <div id='table_view'>
    <HotTable
    data={data}
    readOnly={true}
    rowHeaders={true}
    colHeaders={true}
    width="100vw"
    height="auto"
    licenseKey='non-commercial-and-evaluation'
    rowHeights={function(index) {
    if(sheet.getRow(index+1).height){
    // exceljs获取的行高不是像素值事实上它是23px - 13.8 的一个映射所以需要将它转化为像素值
    return sheet.getRow(index+1).height * (23 / 13.8);
    }
    return 23;// 默认
    }}
    colWidths={function(index){
    if(sheet.getColumn(index+1).width){
    // exceljs获取的列宽不是像素值事实上它是81px - 8.22 的一个映射所以需要将它转化为像素值
    return sheet.getColumn(index+1).width * (81 / 8.22);
    }
    return 81;// 默认
    }}
    cells={(row, col, prop) =>
    {
    const cellProperties = {};
    const cellStyle = sheet.getCell(row+1, col+1).style

    if(JSON.stringify(cellStyle) !== '{}'){
    // console.log(row+1, col+1, cellStyle);
    for(let key in cellStyle){
    cellProperties[key] = cellStyle[key];
    }
    }
    return {...cellProperties, renderer: 'customStylesRenderer'};
    }}
    mergeCells={mergeRanges}
    />

    </div>
    </>

    )
    }

    作者:汤圆要吃咸的
    来源:juejin.cn/post/7264461721279774780
    收起阅读 »

    告别axios,这个库让你爱上前端分页!

    web
    嗨,我们又见面了! 今天咱们聊聊前端分页加载那些事儿。你有没有遇到过这样的烦恼:在做分页的时候,要手动维护各种状态,比如页码、每页显示数量、总数据量等等,还要处理各种边界情况,哎呀妈呀,真是太麻烦了! 那么,有没有什么好办法能让我们从这些繁琐的工作中解脱出来呢...
    继续阅读 »

    嗨,我们又见面了!


    今天咱们聊聊前端分页加载那些事儿。你有没有遇到过这样的烦恼:在做分页的时候,要手动维护各种状态,比如页码、每页显示数量、总数据量等等,还要处理各种边界情况,哎呀妈呀,真是太麻烦了!


    那么,有没有什么好办法能让我们从这些繁琐的工作中解脱出来呢?这时候,alovajs就派上用场了!


    alovajs:轻量级请求策略库


    alovajs是一个轻量级的请求策略库,它可以帮助我们轻松处理分页请求。它支持开发者使用声明式实现各种复杂的请求,比如请求共享、分页请求、表单提交、断点续传等等。使用alovajs,我们可以用很少的代码就实现高效、流畅的请求功能。比如,在Vue中,你可以这样使用alovajs进行分页请求:


    const alovaInstance = createAlova({
    // VueHook用于创建ref状态,包括请求状态loading、响应数据data、请求错误对象error等
    statesHook: VueHook,
    requestAdapter: GlobalFetch(),
    responded: response => response.json()
    });

    const { loading, data, error } = useRequest(
    alovaInstance.Get('https://api.alovajs.org/profile', {
    params: {
    id: 1
    }
    })
    );

    看到了吗?只需要几行代码,alovajs就帮我们处理了分页请求的各种细节,我们再也不用手动维护那些繁琐的状态了!


    对比axios,alovajs的优势


    和axios相比,alovajs有哪些优势呢?首先,alovajs与React、Vue等现代前端框架深度融合,可以自动管理请求相关数据,大大提高了开发效率。其次,alovajs在性能方面做了很多优化,比如默认开启了内存缓存和请求共享,这些都能显著提高请求性能,提升用户体验的同时还能降低服务端的压力。最后,alovajs的体积更小,压缩后只有4kb+,相比之下,axios则有11+kb。


    总之,如果你想在分页加载方面做得更轻松、更高效,alovajs绝对值得一试!



    作者:古韵
    来源:juejin.cn/post/7331924057925533746
    收起阅读 »

    indexOf的第二个参数你用过嘛🤔

    web
    大家好,我是哈默。indexOf 是我们非常熟悉的一个方法,它可以用来获取某一个元素在一个数组里的位置,我们一般就会使用 array.indexOf(element) 的方法来进行使用。 但是,大家有没有使用过 indexOf 的第二个参数呢?第二个参数的使用...
    继续阅读 »

    大家好,我是哈默。indexOf 是我们非常熟悉的一个方法,它可以用来获取某一个元素在一个数组里的位置,我们一般就会使用 array.indexOf(element) 的方法来进行使用。


    但是,大家有没有使用过 indexOf 的第二个参数呢?第二个参数的使用会经常出现在一些优秀的库的源码当中,用于依次分析(或者说扫描)某一个字符串。


    比如命令行美化输出的 chalk 库中就有此应用,因为 chalk 库的原理就是对于我们输出在终端的内容进行处理,然后将处理后的字符串显示在终端上。


    indexOf 基本用法


    首先,我们还是先来回顾一下 indexOf 的最基本用法。


    给定一个数组:[10, 20, 30],寻找这个数组中 30 的位置,是 2


    const arr = [10, 20, 30];
    const element = 30;
    const index = arr.indexOf(element);

    console.log(index); // 2

    indexOf 的第二个参数


    明确了 indexOf 的基本用法以后,它的第 2 个参数有什么用呢?


    其实是起到了一个调整从哪里开始查找的作用。


    我们来看一个例子:


    const arr = [10, 20, 30];
    const element = 10;
    const index = arr.indexOf(element);

    console.log(index); // 0

    const arr2 = [10, 20, 30, 10];
    const element2 = 10;
    const index2 = arr2.indexOf(element2, 1);

    console.log(index2); // 3

    可以看到,同样是查找 [10, 20, 30, 10] 当中 10 的位置,但是因为第一次是从数组第 1 个元素开始查找的,所以得到的结果是 0。


    而第二次是从数组的第 2 个元素开始查找的,所以得到的结果是 3。


    优秀库源码里的使用


    明确了 indexOf 第二个参数的使用之后,我们再来看一下在一些优秀的库的源码里面,它们是如何利用起这个第二个参数的作用的。



    ⚠️注意:我下面会以 String.prototype.indexOf 举例,而上面举的例子是以 Array.prototype.indexOf 为例,但是这两个 API 的第二个参数都是起到一个搜索位置的作用,所以在这里可以一起学习一下



    这里,我们只会分析它的思想,具体的实现在具体的源码里会存在差异,但思想是相同的。


    我们首先定义一个方法,addEmoji,接受三个参数:


    /**
    * 在一个 string 的 targetString 后面,加上一个 emoji
    * @param string 原始 string
    * @param targetString 加 emoji 的那个 string
    * @param emoji 加入的 emoji
    * @returns 处理后的最终结果
    */

    function addEmoji(string, targetString, emoji) {
    let result = "";

    // 一系列处理
    // ...

    return result;
    }

    我们最终会这样调用,在 大家好,我是哈默,今天是一个好天气。 这个字的后面,加上 👍 的 emoji:


    const res = addEmoji("大家好,我是哈默,今天是一个好天气。", "好", "👍");
    console.log(res);

    那么首先我们就可以使用 indexOf 方法来从输入的字符串里找到 的位置:


    function addEmoji(string, targetString, emoji) {
    // 找到 targetString 的位置
    let index = string.indexOf(targetString);

    let result = "";

    // 记录当前扫描到的位置,现在是在参数 string 的开头位置
    // 因为 string 当中,可能会存在多个 targetString,所以我们会跳着进行扫描,也就是会使用 indexOf 的第二个参数
    let currentScanIndex = 0;

    return result;
    }

    如果我们找到了 targetString,即 index !== -1,那么我们就在 targetString 后,加上一个 emoji:


    function addEmoji(string, targetString, emoji) {
    // 找到 targetString 的位置
    let index = string.indexOf(targetString);

    let result = "";

    // 记录当前扫描到的位置,现在是在参数 string 的开头位置
    // 因为 string 当中,可能会存在多个 targetString,所以我们会跳着进行扫描,也就是会使用 indexOf 的第二个参数
    let currentScanIndex = 0;

    // 如果找到了 targetString
    if (index !== -1) {
    // 在 targetString 后面增加 emoji
    result += string.slice(currentScanIndex, index) + targetString + emoji;
    // 将当前扫描位置,移动到 targetString 之后的那个位置上
    currentScanIndex = index + targetString.length;
    }

    // 将 targetString 之后的内容追加到 result 里
    result += string.slice(currentScanIndex);

    return result;
    }

    此时,我们在第一个 字后面,加上了 👍,得到的结果:


    res1.png


    但是,我们这个字符串中,还有一个 好天气,也就是存在多个 targetString,所以我们这里不能是 if 只执行一次,而是要做一个循环。


    我们可以使用一个 while 循环:


    function addEmoji(string, targetString, emoji) {
    // 找到 targetString 的位置
    let index = string.indexOf(targetString);

    let result = "";

    // 记录当前扫描到的位置,现在是在参数 string 的开头位置
    // 因为 string 当中,可能会存在多个 targetString,所以我们会跳着进行扫描,也就是会使用 indexOf 的第二个参数
    let currentScanIndex = 0;

    // 如果找到了 targetString
    while (index !== -1) {
    // 在 targetString 后面增加 emoji
    result += string.slice(currentScanIndex, index) + targetString + emoji;
    // 将当前扫描位置,移动到 targetString 之后的那个位置上
    currentScanIndex = index + targetString.length;
    + // 重点来了!!!我们要从当前扫描的位置开始,去寻找 targetString
    + index = string.indexOf(targetString, currentScanIndex);
    }

    // 将 targetString 之后的内容追加到 result 里
    result += string.slice(currentScanIndex);

    return result;
    }

    此时,我们便成功的给第二个 ,也加上了 emoji:


    res2.png


    这个地方我们就使用到了之前提到的 indexOf 的第二个参数:


    // 重点来了!!!我们要从当前扫描的位置开始,去寻找 targetString
    index = string.indexOf(targetString, currentScanIndex);

    我们是从当前扫描到的位置 currentScanIndex 开始,查找 targetString 的,这样我们就可以找到下一个 targetString 了。


    所以,这里的思想就是通过 indexOf 的第二个参数,帮助我们能够依次扫描一个字符串,依次找到我们想要找的那个元素的位置,然后做相应的处理。


    总结


    indexOf 的第二个参数,叫 fromIndex,看到这里,大家应该也能很好的理解这个 fromIndex 的作用了,就是从哪里开始找嘛!


    作者:我是哈默
    来源:juejin.cn/post/7332858431571230747
    收起阅读 »

    春晚刘谦魔术的模拟程序

    web
    昨晚春晚上刘谦的两个魔术表演都非常精彩,尤其是第二个魔术,他演绎了经典的约瑟夫环问题! 什么是约瑟夫环问题? 约瑟夫环(Josephus problem)是一个经典的数学问题,最早由古罗马历史学家弗拉维奥·约瑟夫斯提出,但它的名字是在19世纪由德国数学家约瑟夫...
    继续阅读 »

    昨晚春晚上刘谦的两个魔术表演都非常精彩,尤其是第二个魔术,他演绎了经典的约瑟夫环问题!


    什么是约瑟夫环问题?


    约瑟夫环(Josephus problem)是一个经典的数学问题,最早由古罗马历史学家弗拉维奥·约瑟夫斯提出,但它的名字是在19世纪由德国数学家约瑟夫·乔瑟夫斯(Josef Stein)命名的。


    问题的描述是这样的:假设有n个人(编号从1到n)站成一个圆圈,从第一个人开始报数,报到某个数字(例如k)的人就被杀死,然后从下一个人开始重新报数并继续这个过程,直到只剩下一个人留下来。


    问题的关键是找出存活下来的那个人的编号。


    结合扑克牌解释约瑟夫环问题


    1、考虑最简单的情况


    假设有2张牌,编号分别是1和2。


    首先将1放到后面,扔掉2。剩下的就是最开始放在最上边的那张1。


    2、稍微复杂一点的情况,牌的张数是2的n次方


    比如有8张牌,编号分别是1、2、3、4、5、6、7、8。


    第一轮会把2、4、6、8扔掉,剩下1、3、5、7按顺序放在后面,又退化成了4张牌的情况。


    第二轮会把3、7扔掉,剩下1、5按顺序放在后面,又退化成了2张牌的情况。


    第三轮把5扔掉,剩下1,就是最初在最前面的那张。


    结论:如果牌的张数是2^n,最后剩下的一定是最开始放在牌堆顶的那张。


    3、考虑任意的情况,牌的张数是2^n+m


    比如牌的张数是11,等于8+3。把1放到后面,把2扔掉,把3放到后面,把4扔掉,把5放到后面,把6扔掉,现在剩下的编号序列是7、8、9、10、11、1、3、5,这又是8张牌的情况!最后一定剩下的是现在牌堆顶的7!


    因此,只要提前知道牌的张数,就一定能马上推导出最终是剩下哪一张牌。一切的魔法都是数学!!都是算法!!


    见证奇迹的时刻!魔术的流程



    1. 4张牌对折后撕开,就是8张,叠放在一起就是ABCDABCD。注意,ABCD四个数字是完全等价的。

    2. 根据名字字数,把顶上的牌放到下面,但怎么放都不会改变循环序列的相对位置。譬如2次,最后变成CDABCDAB;譬如3次,最后换成DABCDABC。但无论怎么操作,第4张和第8张牌都是一样的。

    3. 把顶上3张插到中间任意位置。这一步非常重要!因为操作完之后必然出现第1张和第8张牌是一样的!以名字两个字为例,可以写成BxxxxxxB(这里的x是其他和B不同的牌)。

    4. 拿掉顶上的牌放到一边,记为B。剩下的序列是xxxxxxB,一共7张牌。

    5. 南方人/北方人/不确定,分别拿顶上的1/2/3张牌插到中间,但是不会改变剩下7张牌是xxxxxxB的结果。

    6. 男生拿掉1张,女生拿掉2张。也就是男生剩下6张,女生剩下5张。分别是xxxxxB和xxxxB。

    7. 循环7次,把最顶上的放到最底下,男生和女生分别会是xxxxBx和xxBxx。

    8. 最后执行约瑟夫环过程!操作到最后只剩下1张。当牌数为6时(男生),剩下的就是第5张牌;当牌数为5时(女生),剩下的就是第3张牌。Bingo!就是第4步拿掉的那张牌!


    下面是完整的 JavaScript 代码实现:


    // 定义一个函数,用于把牌堆顶n张牌移动到末尾
    function moveCardBack(n, arr) {
    // 循环n次,把队列第一张牌放到队列末尾
    for (let i = 0; i < n; i++) {
    const moveCard = arr.shift(); // 弹出队头元素,即第一张牌
    arr.push(moveCard); // 把原队头元素插入到序列末尾
    }
    return arr;
    }

    // 定义一个函数,用于把牌堆顶n张牌移动到中间的任意位置
    function moveCardMiddleRandom(n, arr) {
    // 插入在arr中的的位置,随机生成一个idx
    // 这个位置必须是在n+1到arr.length-1之间
    const idx = Math.floor(Math.random() * (arr.length - n - 1)) + n + 1;
    // 执行插入操作
    const newArr = arr.slice(n, idx).concat(arr.slice(0, n)).concat(arr.slice(idx));
    return newArr;
    }

    // 步骤1:初始化8张牌,假设为"ABCDABCD"
    let arr = ["A", "B", "C", "D", "A", "B", "C", "D"];
    console.log("步骤1:拿出4张牌,对折撕成8张,按顺序叠放。");
    console.log("此时序列为:" + arr.join('') + "\n---");

    // 步骤2(无关步骤):名字长度随机选取,这里取2到5(其实任意整数都行)
    const nameLen = Math.floor(Math.random() * 4) + 2;
    // 把nameLen张牌移动到序列末尾
    arr = moveCardBack(nameLen, arr);
    console.log(`步骤2:随机选取名字长度为${nameLen},把第1张牌放到末尾,操作${nameLen}次。`);
    console.log(`此时序列为:${arr.join('')}\n---`);

    // 步骤3(关键步骤):把牌堆顶三张放到中间任意位置
    arr = moveCardMiddleRandom(3, arr);
    console.log(`步骤3:把牌堆顶3张放到中间的随机位置。`);
    console.log(`此时序列为:${arr.join('')}\n---`);

    // 步骤4(关键步骤):把最顶上的牌拿走
    const restCard = arr.shift(); // 弹出队头元素
    console.log(`步骤4:把最顶上的牌拿走,放在一边。`);
    console.log(`拿走的牌为:${restCard}`);
    console.log(`此时序列为:${arr.join('')}\n---`);

    // 步骤5(无关步骤):根据南方人/北方人/不确定,把顶上的1/2/3张牌插入到中间任意位置
    // 随机选择1、2、3中的任意一个数字
    const moveNum = Math.floor(Math.random() * 3) + 1;
    arr = moveCardMiddleRandom(moveNum, arr);
    console.log(`步骤5:我${moveNum === 1 ? '是南方人' : moveNum === 2 ? '是北方人' : '不确定自己是哪里人'},\
    ${moveNum}张牌插入到中间的随机位置。`
    );
    console.log(`此时序列为:${arr.join('')}\n---`);

    // 步骤6(关键步骤):根据性别男或女,移除牌堆顶的1或2张牌
    const maleNum = Math.floor(Math.random() * 2) + 1; // 随机选择1或2
    for (let i = 0; i < maleNum; i++) { // 循环maleNum次,移除牌堆顶的牌
    arr.shift();
    }
    console.log(`步骤6:我是${maleNum === 1 ? '男' : '女'}生,移除牌堆顶的${maleNum}张牌。`);
    console.log(`此时序列为:${arr.join('')}\n---`);

    // 步骤7(关键步骤):把顶部的牌移动到末尾,执行7次
    arr = moveCardBack(7, arr);
    console.log(`步骤7:把顶部的牌移动到末尾,执行7次`);
    console.log(`此时序列为:${arr.join('')}\n---`);

    // 步骤8(关键步骤):执行约瑟夫环过程。把牌堆顶一张牌放到末尾,再移除一张牌,直到只剩下一张牌。
    console.log(`步骤8:把牌堆顶一张牌放到末尾,再移除一张牌,直到只剩下一张牌。`);
    while (arr.length > 1) {
    const luck = arr.shift(); // 好运留下来
    arr.push(luck);
    console.log(`好运留下来:${luck}\t\t此时序列为:${arr.join('')}`);
    const sadness = arr.shift(); // 烦恼都丢掉
    console.log(`烦恼都丢掉:${sadness}\t\t此时序列为:${arr.join('')}`);
    }
    console.log(`---\n最终结果:剩下的牌为${arr[0]},步骤4中留下来的牌也是${restCard}`);


    这段代码实现了昨晚春晚上刘谦的第二个魔术表演的过程,并提供了详细的解释。享受魔术的魅力吧!


    image-20240210161329783


    image-20240210161339317


    看到观看的人这么多,除了JavaScript,下面我补充了一些其他语言的实现


    import random

    # 定义一个函数,用于把牌堆顶n张牌移动到末尾
    def move_card_back(n, arr):
       for i in range(n):
           move_card = arr.pop(0)
           arr.append(move_card)
       return arr

    # 定义一个函数,用于把牌堆顶n张牌移动到中间的任意位置
    def move_card_middle_random(n, arr):
       idx = random.randint(n + 1, len(arr) - 1)
       new_arr = arr[n:idx] + arr[0:n] + arr[idx:]
       return new_arr

    # 步骤1:初始化8张牌,假设为"ABCDABCD"
    arr = ["A", "B", "C", "D", "A", "B", "C", "D"]
    print("步骤1:拿出4张牌,对折撕成8张,按顺序叠放。")
    print("此时序列为:" + ''.join(arr) + "\n---")

    # 步骤2(无关步骤):名字长度随机选取,这里取2到5(其实任意整数都行)
    name_len = random.randint(2, 5)
    move_card_back(name_len, arr)
    print("步骤2:随机选取名字长度为" + str(name_len) + ",把第1张牌放到末尾,操作" + str(name_len) + "次。")
    print("此时序列为:" + ''.join(arr) + "\n---")

    # 步骤3(关键步骤):把牌堆顶三张放到中间任意位置
    arr = move_card_middle_random(3, arr)
    print("步骤3:把牌堆顶3张放到中间的随机位置。")
    print("此时序列为:" + ''.join(arr) + "\n---")

    # 步骤4(关键步骤):把最顶上的牌拿走
    rest_card = arr.pop(0)
    print("步骤4:把最顶上的牌拿走,放在一边。")
    print("拿走的牌为:" + rest_card)
    print("此时序列为:" + ''.join(arr) + "\n---")

    # 步骤5(无关步骤):根据南方人/北方人/不确定,把顶上的1/2/3张牌插入到中间任意位置
    # 随机选择1、2、3中的任意一个数字
    move_num = random.randint(1, 3)
    arr = move_card_middle_random(move_num, arr)
    print("步骤5:我" + ("是南方人" if move_num == 1 else "是北方人" if move_num == 2 else "不确定自己是哪里人") + ",把" + str(move_num) + "张牌插入到中间的随机位置。")
    print("此时序列为:" + ''.join(arr) + "\n---")

    # 步骤6(关键步骤):根据性别男或女,移除牌堆顶的1或2张牌
    male_num = random.randint(1, 2)
    for i in range(male_num):
       arr.pop(0)
    print("步骤6:我是" + ("男" if male_num == 1 else "女") + "生,移除牌堆顶的" + str(male_num) + "张牌。")
    print("此时序列为:" + ''.join(arr) + "\n---")

    # 步骤7(关键步骤):把顶部的牌移动到末尾,执行7次
    for i in range(7):
       move_card = arr.pop(0)
       arr.append(move_card)
    print("步骤7:把顶部的牌移动到末尾,执行7次")
    print("此时序列为:" + ''.join(arr) + "\n---")

    # 步骤8(关键步骤):执行约瑟夫环过程。把牌堆顶一张牌放到末尾,再移除一张牌,直到只剩下一张牌。
    print("步骤8:把牌堆顶一张牌放到末尾,再移除一张牌,直到只剩下一张牌。")
    while len(arr) > 1:
       luck = arr.pop(0)
       arr.append(luck)
       print("好运留下来:" + luck + "\t\t此时序列为:" + ''.join(arr))
       sadness = arr.pop(0)
       print("烦恼都丢掉:" + sadness + "\t\t此时序列为:" + ''.join(arr))
    print("---\n最终结果:剩下的牌为" + arr[0] + ",步骤4中留下来的牌也是" + rest_card)


    java


    import java.util.ArrayList;
    import java.util.List;
    import java.util.Random;

    public class Main {
       public static void main(String[] args) {
           List<String> arr = new ArrayList<>();
           arr.add("A");
           arr.add("B");
           arr.add("C");
           arr.add("D");
           arr.add("A");
           arr.add("B");
           arr.add("C");
           arr.add("D");

           System.out.println("步骤1:拿出4张牌,对折撕成8张,按顺序叠放。");
           System.out.println("此时序列为:" + String.join("", arr));
           System.out.println("---");

           Random rand = new Random();

           int nameLen = rand.nextInt(4) + 2;
           moveCardBack(nameLen, arr);
           System.out.println("步骤2:随机选取名字长度为" + nameLen + ",把第1张牌放到末尾,操作" + nameLen + "次。");
           System.out.println("此时序列为:" + String.join("", arr));
           System.out.println("---");

           moveCardMiddleRandom(3, arr);
           System.out.println("步骤3:把牌堆顶3张放到中间的随机位置。");
           System.out.println("此时序列为:" + String.join("", arr));
           System.out.println("---");

           String restCard = arr.remove(0);
           System.out.println("步骤4:把最顶上的牌拿走,放在一边。");
           System.out.println("拿走的牌为:" + restCard);
           System.out.println("此时序列为:" + String.join("", arr));
           System.out.println("---");

           int moveNum = rand.nextInt(3) + 1;
           moveCardMiddleRandom(moveNum, arr);
           System.out.println("步骤5:我" + (moveNum == 1 ? "是南方人" : moveNum == 2 ? "是北方人" : "不确定自己是哪里人") + ",把" + moveNum + "张牌插入到中间的随机位置。");
           System.out.println("此时序列为:" + String.join("", arr));
           System.out.println("---");

           int maleNum = rand.nextInt(2) + 1;
           for (int i = 0; i < maleNum; i++) {
               arr.remove(0);
          }
           System.out.println("步骤6:我是" + (maleNum == 1 ? "男" : "女") + "生,移除牌堆顶的" + maleNum + "张牌。");
           System.out.println("此时序列为:" + String.join("", arr));
           System.out.println("---");

           for (int i = 0; i < 7; i++) {
               String moveCard = arr.remove(0);
               arr.add(moveCard);
          }
           System.out.println("步骤7:把顶部的牌移动到末尾,执行7次");
           System.out.println("此时序列为:" + String.join("", arr));
           System.out.println("---");

           System.out.println("步骤8:把牌堆顶一张牌放到末尾,再移除一张牌,直到只剩下一张牌。");
           while (arr.size() > 1) {
               String luck = arr.remove(0);
               arr.add(luck);
               System.out.println("好运留下来:" + luck + "\t\t此时序列为:" + String.join("", arr));
               String sadness = arr.remove(0);
               System.out.println("烦恼都丢掉:" + sadness + "\t\t此时序列为:" + String.join("", arr));
          }
           System.out.println("---\n最终结果:剩下的牌为" + arr.get(0) + ",步骤4中留下来的牌也是" + restCard);
      }

       private static void moveCardBack(int n, List<String> arr) {
           for (int i = 0; i < n; i++) {
               String moveCard = arr.remove(0);
               arr.add(moveCard);
          }
      }

       private static void moveCardMiddleRandom(int n, List<String> arr) {
           Random rand = new Random();
           int idx = rand.nextInt(arr.size() - n - 1) + n + 1;
           List<String> newArr = new ArrayList<>(arr.subList(n, idx));
           newArr.addAll(arr.subList(0, n));
           newArr.addAll(arr.subList(idx, arr.size()));
           arr.clear();
           arr.addAll(newArr);
      }
    }


    以及c++代码


    #include <iostream>
    #include <vector>
    #include <algorithm>
    #include <cstdlib>
    #include <ctime>

    void moveCardBack(int n, std::vector<std::string>& arr) {
       for (int i = 0; i < n; i++) {
           std::string moveCard = arr[0];
           arr.erase(arr.begin());
           arr.push_back(moveCard);
      }
    }

    void moveCardMiddleRandom(int n, std::vector<std::string>& arr) {
       int idx = rand() % (arr.size() - n - 1) + n + 1;
       std::vector<std::string> newArr;
       newArr.insert(newArr.end(), arr.begin() + n, arr.begin() + idx);
       newArr.insert(newArr.end(), arr.begin(), arr.begin() + n);
       newArr.insert(newArr.end(), arr.begin() + idx, arr.end());
       arr = newArr;
    }

    int main() {
       srand(time(0));

       std::vector<std::string> arr = {"A", "B", "C", "D", "A", "B", "C", "D"};
       std::cout << "步骤1:拿出4张牌,对折撕成8张,按顺序叠放。" << std::endl;
       std::cout << "此时序列为: ";
       for (const std::string& card : arr) {
           std::cout << card;
      }
       std::cout << std::endl;
       std::cout << "---" << std::endl;

       int nameLen = rand() % 4 + 2;
       moveCardBack(nameLen, arr);
       std::cout << "步骤2:随机选取名字长度为" << nameLen << ",把第1张牌放到末尾,操作" << nameLen << "次。" << std::endl;
       std::cout << "此时序列为: ";
       for (const std::string& card : arr) {
           std::cout << card;
      }
       std::cout << std::endl;
       std::cout << "---" << std::endl;

       moveCardMiddleRandom(3, arr);
       std::cout << "步骤3:把牌堆顶3张放到中间的随机位置。" << std::endl;
       std::cout << "此时序列为: ";
       for (const std::string& card : arr) {
           std::cout << card;
      }
       std::cout << std::endl;
       std::cout << "---" << std::endl;

       std::string restCard = arr[0];
       arr.erase(arr.begin());
       std::cout << "步骤4:把最顶上的牌拿走,放在一边。" << std::endl;
       std::cout << "拿走的牌为: " << restCard << std::endl;
       std::cout << "此时序列为: ";
       for (const std::string& card : arr) {
           std::cout << card;
      }
       std::cout << std::endl;
       std::cout << "---" << std::endl;

       moveCardMiddleRandom(rand() % 3 + 1, arr);
       std::cout << "步骤5:我" << (rand() % 2 == 0 ? "是南方人" : "是北方人") << ",把" << rand() % 3 + 1 << "张牌插入到中间的随机位置。" << std::endl;
       std::cout << "此时序列为: ";
       for (const std::string& card : arr) {
           std::cout << card;
      }
       std::cout << std::endl;
       std::cout << "---" << std::endl;

       int maleNum = rand() % 2 + 1;
       for (int i = 0; i < maleNum; i++) {
           arr.erase(arr.begin());
      }
       std::cout << "步骤6:我" << (maleNum == 1 ? "男" : "女") << "生,移除牌堆顶的" << maleNum << "张牌。" << std::endl;
       std::cout << "此时序列为: ";
       for (const std::string& card : arr) {
           std::cout << card;
      }
       std::cout << std::endl;
       std::cout << "---" << std::endl;

       for (int i = 0; i < 7; i++) {
           std::string moveCard = arr[0];
           arr.erase(arr.begin());
           arr.push_back(moveCard);
      }
       std::cout << "步骤7:把顶部的牌移动到末尾,执行7次" << std::endl;
       std::cout << "此时序列为: ";
       for (const std::string& card : arr) {
           std::cout << card;
      }
       std::cout << std::endl;
       std::cout << "---" << std::endl;

       std::cout << "步骤8:把牌堆顶一张牌放到末尾,再移除一张牌,直到只剩下一张牌。" << std::endl;
       while (arr.size() > 1) {
           std::string luck = arr[0];
           arr.erase(arr.begin());
           arr.push_back(luck);
           std::cout << "好运留下来: " << luck << "\t\t此时序列为: ";
           for (const std::string& card : arr) {
               std::cout << card;
          }
           std::cout << std::endl;

           std::string sadness = arr[0];
           arr.erase(arr.begin());
           std::cout << "烦恼都丢掉: " << sadness << "\t\t此时序列为: ";
           for (const std::string& card : arr) {
               std::cout << card;
          }
           std::cout << std::endl;
      }
       std::cout << "---\n最终结果: " << arr[0] << ", 步骤4中留下来的牌也是" << restCard << std::endl;

       return 0;
    }

    作者:小u
    来源:juejin.cn/post/7332865125640044556
    收起阅读 »

    async/await 你可能正在将异步写成同步

    web
    前言 你是否察觉到自己随手写的异步函数,实际却是“同步”的效果! 正文 以一个需求为例:获取给定目录下的全部文件,返回所有文件的路径数组。 第一版 思路很简单:读取目录内容,如果是文件,添加进结果数组,如果还是目录,我们递归执行。 import path fr...
    继续阅读 »

    前言


    你是否察觉到自己随手写的异步函数,实际却是“同步”的效果!


    正文


    以一个需求为例:获取给定目录下的全部文件,返回所有文件的路径数组。


    第一版


    思路很简单:读取目录内容,如果是文件,添加进结果数组,如果还是目录,我们递归执行。


    import path from 'node:path'
    import fs from 'node:fs/promises'
    import { existsSync } from 'node:fs'

    async function findFiles(root) {
    if (!existsSync(root)) return

    const rootStat = await fs.stat(root)
    if (rootStat.isFile()) return [root]

    const result = []
    const find = async (dir) => {
    const files = await fs.readdir(dir)
    for (let file of files) {
    file = path.resolve(dir, file)
    const stat = await fs.stat(file)
    if (stat.isFile()) {
    result.push(file)
    } else if (stat.isDirectory()) {
    await find(file)
    }
    }
    }
    await find(root)
    return result
    }

    机智的你是否已经发现了问题?


    我们递归查询子目录的过程是不需要等待上一个结果的,但是第 20 行代码,只有查询完一个子目录之后才会查询下一个,显然让并发的异步,变成了顺序的“同步”执行。


    那我们去掉 20 行的 await 是不是就可以了,当然不行,这样的话 await find(root) 在没有完全遍历目录之前就会立刻返回,我们无法拿到正确的结果。


    思考一下,怎么修改它呢?......让我们看第二版代码。


    第二版


    import path from 'node:path'
    import fs from 'node:fs/promises'
    import { existsSync } from 'node:fs'

    async function findFiles(root) {
    if (!existsSync(root)) return

    const rootStat = await fs.stat(root)
    if (rootStat.isFile()) return [root]

    const result = []
    const find = async (dir) => {
    const task = (await fs.readdir(dir)).map(async (file) => {
    file = path.resolve(dir, file)
    const stat = await fs.stat(file)
    if (stat.isFile()) {
    result.push(file)
    } else if (stat.isDirectory()) {
    await find(file)
    }
    })
    return Promise.all(task)
    }
    await find(root)
    return result
    }

    我们把每个子目录内容的查询作为独立的任务,扔给 Promise.all 执行,就是这个简单的改动,性能得到了质的提升,让我们看看测试,究竟能差多少。


    对比测试


    console.time('v1')
    const files1 = await findFiles1('D:\\Videos')
    console.timeEnd('v1')

    console.time('v2')
    const files2 = await findFiles2('D:\\Videos')
    console.timeEnd('v2')

    console.log(files1?.length, files2?.length)

    result


    版本二快了三倍不止,如果是并发的接口请求被不小心搞成了顺序执行,差距比这还要夸张。


    作者:justorez
    来源:juejin.cn/post/7332031293877485578
    收起阅读 »

    为什么大家都不想回家过年了

    大家好,我是林家少爷,是一位专注于前端技术,立志成为前端技术专家的热血青年。 2023年,看到身边的朋友多多少少都经历了裁员,降薪,甚至被迫自离。 我还好,2023年唯一值得庆幸的事情就是我工资涨薪2k,不过年终奖依旧无望。 今年年会都取消了,过年礼品也没见影...
    继续阅读 »

    大家好,我是林家少爷,是一位专注于前端技术,立志成为前端技术专家的热血青年。


    2023年,看到身边的朋友多多少少都经历了裁员,降薪,甚至被迫自离。


    我还好,2023年唯一值得庆幸的事情就是我工资涨薪2k,不过年终奖依旧无望。
    今年年会都取消了,过年礼品也没见影。


    坦白讲,自从放开疫情之后,行情不仅没有好转,反而更差了,预计2024年要比之前更难,所以,2024年到2025年,这两年还是多攒钱,多搞钱,然后把钱存下来,尽量多存钱,比什么都强。


    好不容易熬到了过年,本来想回到温暖的港湾,把2023年的事情好的不好的都说出来跟家里人倾诉一下。


    没想到面对的,是家里人的催婚,在一线城市工资那么高,钱都到哪里去了,人家谁谁才比你大三个月都已经二胎了......


    甚至过年还要走亲戚,甚至拉着亲戚说介绍对象。


    这些我都理解,因为我正在经历这一切。


    我的观点是,2023年赚钱是真不容易,打工的面对工作量增加,平时无偿加班,领导PUA(可能领导也被更高级的领导PUA),还有被迫降薪,薪资被压年底年终奖(然后这中间可操作空间很大),而且大城市本来消费就高,有好些同事被裁员还拿不到裁员费,大过年还要跳楼拉横幅争取,以上总总,我都亲眼看过,赚钱太难了。


    我从2020年开始,一直都在做副业,中间经历了一次失败的创业,2021年回归职场,也是行情最差的时候,没想到后面两年越来越差,也明显感觉2023年副业赚钱越来越卷,任何一点赚钱的项目也会被互联网给公开,摊平了信息差,然后大家一窝蜂地涌进来,自己流量也被抢占了,很快这个项目又得放弃了,我还没统计2023年我的副业赚了多少,但肯定不超过10万,离我定的目标(超过主业工资持续3个月以上)还差很远,而且总是被加班,老板周末问话给打断,有时候自己也被气炸了,好几次都想不干了,但是看着自己还没有做起来的副业盘子,贸贸然走人收入立马断了,也是要重新找工作的,就忍了下来。


    大家看到这里就知道我当时内心有多矛盾,但是我都坚持了下来,我相信总有一天我可以真正把副业做起来,真正拥有属于自己的事业,手上有许多现钱,不需要看任何人眼色,我能活成我自己。


    回到过年这个话题,很多老一辈就觉得,大过年的就应该走走亲戚,见见七八姑八大姨,互相聊聊家常,好不热闹。


    但是我身边很多同事,不包括程序员,其实都是偏内向的人(包括我也是),就想着在家里跟家里人倾诉一下,哪怕不倾诉,也是关在家里面,把房间打扫的干净整洁,安静的看书,或者玩玩游戏,或者搞钱,就是不想去见一些八竿子打不着的亲戚,这是一种精神内耗。


    说实话,他们真的只想回家好好休息,啥事都不做,饭店有家人做好的满满一桌饭菜,上班那点屁事就不管他了~


    催婚,算了吧,想当年我们的目标都是考清华北大,985,211,为了高考付出了多少个不眠之夜,但是人人都考得上吗?


    尤其是奔三的女孩子,我能体会她们被家里人各种催婚的痛苦,真的会把人逼疯。


    婚姻大事,岂非儿戏,还是要找到同频人,先谈恋爱,再结婚,感情基础决定上层建筑,只有这样才能长长久久。


    搭伙过日子,那只适合70年代,不适合我们,随意搭伙,闪婚,没有前期磨合阶段,大概率会闪离,现在离婚率高不是没有原因的,所以还需慎重。


    至于面对家里走亲戚,出到社会都知道不过是演戏而已,跟着演,演到底即可。


    不是个好演员做不了一个好销售,不是好销售做不了一个会赚钱的程序员。


    这句话我自己总结出来的,会自我营销就是要会演戏,把自己都骗过了才能骗别人。


    前期会比较累,慢慢就习惯了。


    实在忍受不了,下一年大不了不回家了,图个清静。


    点到为止,祝大家新年快乐。


    作者:林家少爷
    来源:juejin.cn/post/7332593229337100303
    收起阅读 »

    小镇做题家必须要跨过的三道坎

    其实我们大多数人都还称不上小镇做题家,因为我们大多都是来自乡村,只不过在乡镇做了上了几年的学而已。 大多数人的人生轨迹基本上都是从乡村到乡镇再到县城,最终去到了市级以上的城市读大学,没有资源,没人指路,毕业后也是一脸茫然,最终回到原点! 所以大多数小镇做题家的...
    继续阅读 »

    其实我们大多数人都还称不上小镇做题家,因为我们大多都是来自乡村,只不过在乡镇做了上了几年的学而已。


    大多数人的人生轨迹基本上都是从乡村到乡镇再到县城,最终去到了市级以上的城市读大学,没有资源,没人指路,毕业后也是一脸茫然,最终回到原点!


    所以大多数小镇做题家的生活永远都是那么艰难,很难成为出题家,是因为多数人身上都出现了致命的弱点,而这些弱点多数是由原生家庭带来的。


    一.自卑


    自卑可以说是原生家庭带来的第一原罪,也是很难突破的一关,而导致自卑的导火索一定是贫穷。


    因为我们多数人从小都被父母灌输“我们家穷,所以你不应该这样做”。


    所以在进入大学后,多数人的心中都有一个魔咒,我不配拥有,不配出去玩,不配谈恋爱,不配穿好看的衣服,即使是自己打工赚的钱,多花一分都觉得有负罪感。


    因为心里会想着,父母那么辛苦,我就不应该去花钱,而是要节省,当然节省并没有不好。


    但是从逻辑上就已经搞错了,首先如果自己舍不得钱去学习,去投资自己,一个月挣3000能够省下2000,那么这个省钱毫无意义,因为省下的这点钱根本做不了什么,但是会把自己置入一个更深的漩涡,收入无法提升,眼界无法开阔,而是一直盯着一个月省2000这个感人的骗局。


    除了用钱有负罪感,还有不敢向上社交,觉得自己是小山旮旯出来的,不配和优秀的人结伴,因为父母从小就说: 我们家条件没人家好,人家有钱,人家会瞧不起你的。所以很多人内心就已经打了退堂鼓,从而错过了和优秀的人链接的机会。


    但是事实上真正优秀的人,人家从来不会有瞧不起人的心态,只要你身上有优点,你们之间能够进行价值交换,别人比你还谦卑。


    我这几年从互联网行业链接到的人有1000个以上,优秀的人不在少数,我发现越优秀的人越谦虚。


    自卑导致的问题还很多,比如做事扭扭咧咧,不敢上台,不敢发表意见,总是躲在角落里。


    但是这个社会没有人喜欢扭扭咧咧的人,都喜欢落落大方的人,即使你的知识不专业,你普通话不好,这些都不重要,但是只要你表现出自信,大方的态度,那么大家都会欣赏你,因为这就是勇敢。


    二.面子


    有一个事实,越脆弱的人越爱面子,越无能的人越爱面子。


    这句话虽然说起来有点不好听,但是这是事实,我们这种小镇做题家最要先放下面子。


    比如自己不懂很多东西,但是不愿意去问别人,怕被别人说,在学校的时候,有问题不敢问,于是花费很多时间去弄,到最后还是搞不懂。


    进入工作后,不懂的也不好意思问同事和领导,怕被别人说菜,怀疑自己。


    其实真的没几个人有那个闲心去关注你的过往,你的家庭,相反越是伪装,越容易暴露。


    我很喜欢的一段话: 我穷,但是我正在拼命改变、我菜,但是我可以拼命学!


    面子的背后是自负,是错失,是沦陷。


    三.认知


    认知是一个人的天花板,它把人划分了层级。


    有一段话说得很好:你永远无法赚到认知之外的钱,你也无法了解认知之外的世界。


    我们多数小镇做题家的思维永远停留在老师和父母的教导:好好读书,将来就能考一个好工作,就能找一个铁饭碗。

    然后在职场中听老板的:好好工作,任劳任怨,以后给你升职加薪。


    然后多数人就深信不疑,就觉得人生的目标就是铁饭碗,其他的都不行,工作努力就一定能得到升职加薪,实际上这一切是在PUA自己。


    当被这个社会毒打后,才发现自己是那么无知,那么天真。


    而这一切的罪魁祸首就是自己认知不够,总觉得按部就班就一定能顺利过好一生。


    ————


    自卑,面子,认知这三点是我们多数小镇做题家难以跨过的三道坎。


    而这三道坎基本上都是原生家庭和教育造成的。


    跨过这三道坎的方法就是逃离和向上链接。


    施耐庵在几百年前就总结出的一句话:母弱出商贾,父强做侍郎,族望留原籍,家贫走他乡。


    显然我们大多数都是属于第四类,所以远走他乡是唯一的选择,只有脱离原生的环境,才能打开视野。


    事实也是如此,我们观察身边没有背景,没有经济条件的人,他们的谷底反弹一定是逃离。


    绝非留恋原地!


    作者:苏格拉的底牌
    来源:juejin.cn/post/7330295661784875043
    收起阅读 »

    压缩炸弹,Java怎么防止

    一、什么是压缩炸弹,会有什么危害 1.1 什么是压缩炸弹 压缩炸弹(ZIP):一个压缩包只有几十KB,但是解压缩后有几十GB,甚至可以去到几百TB,直接撑爆硬盘,或者是在解压过程中CPU飙到100%造成服务器宕机。虽然系统功能没有自动解压,但是假如开发人员在不...
    继续阅读 »

    一、什么是压缩炸弹,会有什么危害


    1.1 什么是压缩炸弹


    压缩炸弹(ZIP):一个压缩包只有几十KB,但是解压缩后有几十GB,甚至可以去到几百TB,直接撑爆硬盘,或者是在解压过程中CPU飙到100%造成服务器宕机。虽然系统功能没有自动解压,但是假如开发人员在不细心观察的情况下进行一键解压(不看压缩包里面的文件大小),可导致压缩炸弹爆炸。又或者压缩炸弹藏在比较深的目录下,不经意的解压缩,也可导致压缩炸弹爆炸。


    以下是安全测试几种经典的压缩炸弹


    graph LR
    A(安全测试的经典压缩炸弹)
    B(zip文件42KB)
    C(zip文件10MB)
    D(zip文件46MB)
    E(解压后5.5G)
    F(解压后281TB)
    G(解压后4.5PB)

    A ---> B --解压--> E
    A ---> C --解压--> F
    A ---> D --解压--> G

    style B fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
    style C fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
    style D fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px
    style E fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
    style F fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
    style G fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px


    压缩炸弹(也称为压缩文件炸弹、炸弹文件)是一种特殊的文件,它在解压缩时会迅速膨胀成极其庞大的文件,可能导致系统资源耗尽、崩溃或磁盘空间耗尽。


    压缩炸弹的原理是利用文件压缩算法中的重复模式和递归压缩的特性。它通常是一个非常小的压缩文件,但在解压缩时会生成大量的重复数据,导致文件大小迅速增长。这种文件的设计意图是迫使系统进行大量的解压缩操作,以消耗系统资源或填满磁盘空间。


    压缩炸弹可能对系统造成严重的影响,包括系统崩溃、资源耗尽、拒绝服务攻击等。因此,它被视为一种恶意的计算机攻击工具,常常被用于恶意目的或作为安全测试中的一种工具。



    1.2 压缩炸弹会有什么危害


    graph LR
    A(压缩炸弹的危害)
    B(资源耗尽)
    C(磁盘空间耗尽)
    D(系统崩溃)
    E(拒绝服务攻击)
    F(数据丢失)

    A ---> B
    A ---> C
    A ---> D
    A ---> E
    A ---> F

    style B fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
    style C fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
    style D fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px
    style E fill:#98FB98,stroke:#98FB98,stroke-width:2px
    style F fill:#00FFFF,stroke:#00FFFF,stroke-width:2px

    压缩炸弹可能对计算机系统造成以下具体的破坏:



    1. 资源耗尽:压缩炸弹在解压缩时会生成大量的重复数据,导致系统的CPU、内存和磁盘资源被迅速占用。这可能导致系统反应迟缓、无法响应用户的请求,甚至系统崩溃。

    2. 磁盘空间耗尽:由于压缩炸弹的膨胀特性,它可能在解压缩过程中填满磁盘空间。这会导致系统无法写入新的数据,造成磁盘空间耗尽,影响系统的正常运行。

    3. 系统崩溃:当一个压缩炸弹被解压缩时,系统可能由于资源耗尽或磁盘空间耗尽而崩溃。这可能导致系统重启或需要进行紧急修复,造成数据丢失或系统不可用的情况。

    4. 拒绝服务攻击:大规模的解压缩操作可能消耗大量系统资源,导致系统无法正常提供服务。这被恶意攻击者利用,用于进行拒绝服务攻击,使目标系统无法响应合法用户的请求。

    5. 数据丢失:在某些情况下,压缩炸弹可能会导致系统文件或数据的损坏或丢失。这可能发生在磁盘空间被耗尽时,写入操作无法成功完成的情况下。



    重要提示:压缩炸弹可能对计算机系统造成不可逆的损害,请不要尝试创建、传播或使用压缩炸弹,以保护计算机和网络的安全。



    二、怎么检测和处理压缩炸弹,Java怎么防止压缩炸弹


    2.1 个人有没有方法可以检测压缩炸弹?


    有一些方法可以识别和处理潜在的压缩炸弹,以防止对系统造成破坏。以下是一些常见的方法:


    graph LR
    A(个人检测压缩炸弹)
    B(安全软件和防病毒工具)
    C(文件大小限制)
    D(文件类型过滤)

    A ---> B --> E(推荐)
    A ---> C --> F(太大的放个心眼)
    A ---> D --> G(注意不认识的文件类型)

    style B fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
    style C fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
    style D fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px
    style E fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
    style F fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
    style G fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px


    1. 安全软件和防病毒工具(推荐):使用最新的安全软件和防病毒工具可以帮助检测和阻止已知的压缩炸弹。这些工具通常具备压缩文件扫描功能,可以检查文件是否包含恶意的压缩炸弹。

    2. 文件大小限制:设置对文件大小的限制可以帮助防止解压缩过程中出现过大的文件。通过限制解压缩操作的最大文件大小,可以减少对系统资源和磁盘空间的过度消耗。

    3. 文件类型过滤:识别和过滤已知的压缩炸弹文件类型可以帮助阻止这些文件的传输和存储。通过检查文件扩展名或文件头信息,可以识别潜在的压缩炸弹,并阻止其传输或处理。


    2.2 Java怎么防止压缩炸弹


    在java中实际防止压缩炸弹的方法挺多的,可以采取以下措施来防止压缩炸弹:


    graph LR
    A(Java防止压缩炸弹)
    B(解压缩算法的限制)
    C(设置解压缩操作的资源限制)
    D(使用安全的解压缩库)
    E(文件类型验证和过滤)
    F(异步解压缩操作)
    G(安全策略和权限控制)

    A ---> B
    A ---> C
    A ---> D
    A ---> E
    A ---> F
    A ---> G

    style B fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
    style C fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
    style D fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px
    style E fill:#98FB98,stroke:#98FB98,stroke-width:2px
    style F fill:#ADD8E6,stroke:#ADD8E6,stroke-width:2px
    style G fill:#00FFFF,stroke:#00FFFF,stroke-width:2px


    1. 解压缩算法的限制:限制解压缩算法的递归层数和重复模式的检测可以帮助防止解压缩过程无限膨胀。通过限制递归的深度和检测重复模式,可以及时中断解压缩操作并避免过度消耗资源。

    2. 设置解压缩操作的资源限制:使用Java的java.util.zipjava.util.jar等类进行解压缩时,可以设置解压缩操作的资源限制,例如限制解压缩的最大文件大小、最大递归深度等。通过限制资源的使用,可以减少对系统资源的过度消耗。

    3. 使用安全的解压缩库:确保使用的解压缩库是经过安全验证的,以避免存在已知的压缩炸弹漏洞。使用官方或经过广泛验证的库可以减少受到压缩炸弹攻击的风险。

    4. 文件类型验证和过滤:在解压缩之前,可以对文件进行类型验证和过滤,排除潜在的压缩炸弹文件。通过验证文件的类型、扩展名和文件头信息,可以识别并排除不安全的压缩文件。

    5. 异步解压缩操作:将解压缩操作放在异步线程中进行,以防止阻塞主线程和耗尽系统资源。这样可以保持应用程序的响应性,并减少对系统的影响。

    6. 安全策略和权限控制:实施严格的安全策略和权限控制,限制用户对系统资源和文件的访问和操作。确保只有受信任的用户或应用程序能够进行解压缩操作,以减少恶意使用压缩炸弹的可能性。


    2.2.1 使用解压算法的限制来实现防止压缩炸弹


    在前面我们说了Java防止压缩炸弹的一些策略,下面我将代码实现通过解压缩算法的限制来实现防止压缩炸弹。


    先来看看我们实现的思路


    graph TD
    A(开始) --> B[创建 ZipFile 对象]
    B --> C[打开要解压缩的 ZIP 文件]
    C --> D[初始化 zipFileSize 变量为 0]
    D --> E{是否有更多的条目}
    E -- 是 --> F[获取 ZIP 文件的下一个条目]
    F --> G[获取当前条目的未压缩大小]
    G --> H[将解压大小累加到 zipFileSize 变量]
    H --> I{zipFileSize 是否超过指定的大小}
    I -- 是 --> J[调用 deleteDir方法删除已解压的文件夹]
    J --> K[抛出 IllegalArgumentException 异常]
    K --> L(结束)
    I -- 否 --> M(保存解压文件) --> E
    E -- 否 --> L

    style A fill:#00FFFF,stroke:#00FFFF,stroke-width:2px
    style B fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
    style C fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
    style D fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px
    style E fill:#98FB98,stroke:#98FB98,stroke-width:2px
    style F fill:#ADD8E6,stroke:#ADD8E6,stroke-width:2px
    style G fill:#00FFFF,stroke:#00FFFF,stroke-width:2px
    style H fill:#E6E6FA,stroke:#E6E6FA,stroke-width:2px
    style I fill:#ADD8E6,stroke:#ADD8E6,stroke-width:2px
    style J fill:#00FFFF,stroke:#00FFFF,stroke-width:2px
    style K fill:#E6E6FA,stroke:#E6E6FA,stroke-width:2px
    style L fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
    style M fill:#ADD8E6,stroke:#ADD8E6,stroke-width:2px

    实现流程说明如下:



    1. 首先,通过给定的 file 参数创建一个 ZipFile 对象,用于打开要解压缩的 ZIP 文件。

    2. zipFileSize 变量用于计算解压缩后的文件总大小。

    3. 使用 zipFile.entries() 方法获取 ZIP 文件中的所有条目,并通过 while 循环逐个处理每个条目。

    4. 对于每个条目,使用 entry.getSize() 获取条目的未压缩大小,并将其累加到 zipFileSize 变量中。

    5. 如果 zipFileSize 超过了给定的 size 参数,说明解压后的文件大小超过了限制,此时会调用 deleteDir() 方法删除已解压的文件夹,并抛出 IllegalArgumentException 异常,以防止压缩炸弹攻击。

    6. 创建一个 File 对象 unzipped,表示解压后的文件或目录在输出文件夹中的路径。

    7. 如果当前条目是一个目录,且 unzipped 不存在,则创建该目录。

    8. 如果当前条目不是一个目录,确保 unzipped 的父文件夹存在。

    9. 创建一个 FileOutputStream 对象 fos,用于将解压后的数据写入到 unzipped 文件中。

    10. 通过 zipFile.getInputStream(entry) 获取当前条目的输入流。

    11. 创建一个缓冲区 buffer,并使用循环从输入流中读取数据,并将其写入到 fos 中,直到读取完整个条目的数据。

    12. 最后,在 finally 块中关闭 fos 和 zipFile 对象,确保资源的释放。


    实现代码工具类


    import java.io.File;
    import java.io.FileOutputStream;
    import java.io.InputStream;
    import java.util.Enumeration;
    import java.util.zip.ZipEntry;
    import java.util.zip.ZipFile;

    /**
    * 文件炸弹工具类
    *
    * @author bamboo panda
    * @version 1.0
    * @date 2023/10
    */

    public class FileBombUtil {

    /**
    * 限制文件大小 1M(限制单位:B)[1M=1024KB 1KB=1024B]
    */

    public static final Long FILE_LIMIT_SIZE = 1024 * 1024 * 1L;

    /**
    * 文件超限提示
    */

    public static final String FILE_LIMIT_SIZE_MSG = "The file size exceeds the limit";

    /**
    * 解压文件(带限制解压文件大小策略)
    *
    * @param file 压缩文件
    * @param outputfolder 解压后的文件目录
    * @param size 限制解压之后的文件大小(单位:B),示例 3M:1024 * 1024 * 3L (FileBombUtil.FILE_LIMIT_SIZE * 3)
    * @throws Exception IllegalArgumentException 超限抛出的异常
    * 注意:业务层必须抓取IllegalArgumentException异常,如果msg等于FILE_LIMIT_SIZE_MSG
    * 要考虑后面的逻辑,比如告警
    */

    public static void unzip(File file, File outputfolder, Long size) throws Exception {
    ZipFile zipFile = new ZipFile(file);
    FileOutputStream fos = null;
    try {
    Enumerationextends ZipEntry> zipEntries = zipFile.entries();
    long zipFileSize = 0L;
    ZipEntry entry;
    while (zipEntries.hasMoreElements()) {
    // 获取 ZIP 文件的下一个条目
    entry = zipEntries.nextElement();
    // 将解缩大小累加到 zipFileSize 变量
    zipFileSize += entry.getSize();
    // 判断解压文件累计大小是否超过指定的大小
    if (zipFileSize > size) {
    deleteDir(outputfolder);
    throw new IllegalArgumentException(FILE_LIMIT_SIZE_MSG);
    }
    File unzipped = new File(outputfolder, entry.getName());
    if (entry.isDirectory() && !unzipped.exists()) {
    unzipped.mkdirs();
    continue;
    } else if (!unzipped.getParentFile().exists()) {
    unzipped.getParentFile().mkdirs();
    }

    fos = new FileOutputStream(unzipped);
    InputStream in = zipFile.getInputStream(entry);

    byte[] buffer = new byte[4096];
    int count;
    while ((count = in.read(buffer, 0, buffer.length)) != -1) {
    fos.write(buffer, 0, count);
    }
    }
    } finally {
    if (null != fos) {
    fos.close();
    }
    if (null != zipFile) {
    zipFile.close();
    }
    }

    }

    /**
    * 递归删除目录文件
    *
    * @param dir 目录
    */

    private static boolean deleteDir(File dir) {
    if (dir.isDirectory()) {
    String[] children = dir.list();
    //递归删除目录中的子目录下
    for (int i = 0; i < children.length; i++) {
    boolean success = deleteDir(new File(dir, children[i]));
    if (!success) {
    return false;
    }
    }
    }
    // 目录此时为空,可以删除
    return dir.delete();
    }

    }

    测试类


    import java.io.File;

    /**
    * 文件炸弹测试类
    *
    * @author bamboo panda
    * @version 1.0
    * @date 2023/10
    */

    public class Test {

    public static void main(String[] args) {
    File bomb = new File("D:\temp\3\zbsm.zip");
    File tempFile = new File("D:\temp\3\4");
    try {
    FileBombUtil.unzip(bomb, tempFile, FileBombUtil.FILE_LIMIT_SIZE * 60);
    } catch (IllegalArgumentException e) {
    if (FileBombUtil.FILE_LIMIT_SIZE_MSG.equalsIgnoreCase(e.getMessage())) {
    FileBombUtil.deleteDir(tempFile);
    System.out.println("原始文件太大");
    } else {
    System.out.println("错误的压缩文件格式");
    }
    } catch (Exception e) {
    e.printStackTrace();
    }
    }

    }

    三、总结


    文件炸弹是一种恶意的计算机程序或文件,旨在利用压缩算法和递归结构来创建一个巨大且无限增长的文件或文件集
    合。它的目的是消耗目标系统的资源,如磁盘空间、内存和处理能力,导致系统崩溃或无法正常运行。文件炸弹可能是有意制造的攻击工具,用于拒绝服务(DoS)攻击或滥用资源的目的。


    文件炸弹带来的危害极大,作为开发人员,我们必须深刻认识到文件炸弹的危害性,并始终保持高度警惕,以防止这种潜在漏洞给恐怖分子以可乘之机。


    总而言之,我们作为开发人员,要深刻认识到文件炸弹的危害性,严防死守,不给恐怖分子任何可乘之机。通过使用安全工具、限制文件大小、及时更新软件、定期备份数据以及加强安全意识,我们可以有效地防止文件炸弹和其他恶意活动对系统造成损害。


    在中国,网络安全和计算机犯罪问题受到相关法律法规的管理和监管。以下是一些中国关于网络安全和计算机犯罪方面的法律文献,其中也涉及到文件炸弹的相关规定:




    1. 《中华人民共和国刑法》- 该法律规定了各种计算机犯罪行为的法律责任,包括非法控制计算机信息系统、破坏计算机信息系统功能、非法获取计算机信息系统数据等行为,这些行为可能涉及到使用文件炸弹进行攻击。

    2. 《中华人民共和国网络安全法》- 该法律是中国的基本法律,旨在保障网络安全和维护国家网络空间主权。它规定了网络安全的基本要求和责任,包括禁止制作、传播软件病毒、恶意程序和文件炸弹等危害网络安全的行为。

    3. 《中华人民共和国计算机信息系统安全保护条例》- 这是一项行政法规,详细规定了计算机信息系统安全的保护措施和管理要求。其中包含了对恶意程序、计算机病毒和文件炸弹等威胁的防范要求。



    作者:独爱竹子的功夫熊猫
    来源:juejin.cn/post/7289667869557178404
    收起阅读 »

    突发奇想(吃饱了撑的) vue3能实现react中的hooks吗?

    web
    前言在前端领域,Vue 和 React 是两个备受欢迎的 JavaScript 框架,它们都为开发者提供了灵活而强大的工具。React 引入了 Hooks 的概念,使得函数组件可以拥有状态和其他 React 特性,而不再需要使用类组件。那么,对于那些突发奇想、...
    继续阅读 »

    前言

    在前端领域,Vue 和 React 是两个备受欢迎的 JavaScript 框架,它们都为开发者提供了灵活而强大的工具。React 引入了 Hooks 的概念,使得函数组件可以拥有状态和其他 React 特性,而不再需要使用类组件。那么,对于那些突发奇想、吃饱了撑的时候,我们是否可以在 Vue 3 中实现类似 React Hooks 的功能呢?

    首先,我们需要了解 React Hooks 的核心思想。React Hooks 允许函数组件拥有状态,副作用和其他 React 特性,而无需使用类组件。这是通过使用 useStateuseEffect 等特殊的函数来实现的。

    Vue 3 也引入了 Composition API,它在一定程度上类似于 React Hooks。Composition API 允许我们在函数组件中组织和重用逻辑。虽然它不是完全相同的实现,但能够达到类似的效果。

    useState

    React 中的 useState:

    useState 是 React 中的一个 Hook,用于在函数组件中引入状态。通过 useState,我们可以在函数组件中保存和更新状态,而不必使用类组件。

    基本语法如下:

    import React, { useState } from 'react';

    function ExampleComponent() {
    // 使用 useState 定义状态变量 count 和更新函数 setCount,并初始化为 0
    const [count, setCount] = useState(0);

    return (
    <div>
    <p>Count: {count}p>
    <button onClick={() => setCount(count + 1)}>
    加一
    button>
    div>
    );
    }

    下面是一个使用 vue3实现类似于 useState 的例子:

    import { ref, UnwrapRef } from "vue";

    type UpdateFunction = (nextState: UnwrapRef) => UnwrapRef;
    function isUpdateFc(
    nextState: UnwrapRef | UpdateFunction
    ): nextState is UpdateFunction {
    return typeof nextState === "function";
    }

    export default function useState(initialState: T) {
    const state = ref(initialState);
    const useState = (nextState: UnwrapRef | UpdateFunction) => {
    // 检测传入的是不是函数,如果是函数就把state传给函数,把函数执行返回值赋给重新state
    if (isUpdateFc(nextState)) {
    state.value = nextState(state.value);
    } else {
    state.value = nextState;
    }
    };
    return [state, useState] as const;
    }

    <template>
    <div>
    <button>{{ count }}button>
    <button @click="() => handerCount()">+button>
    div>
    template>

    useEffect

    React 中的 useEffect:

    useEffect 是 React 中的一个重要 Hook,用于处理副作用操作,比如数据获取、订阅、手动操作 DOM 等。它在函数组件渲染完成后执行,可以用于管理组件的生命周期。

    基本语法如下:

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

    function ExampleComponent() {
    const [data, setData] = useState(null);

    useEffect(() => {
    // 在组件渲染完成后执行的副作用操作
    fetchData(); // 例如,发起数据请求
    }, []); // 第二个参数是依赖数组,为空数组表示只在组件挂载和卸载时执行

    return (
    <div>
    {/* 组件渲染的内容 */}
    div>
    );
    }

    在 Vue 3 中使用 onMounted, onUpdated, onUnmounted,watch 实现类似功能:

    在 Vue 3 中,可以使用一系列的生命周期钩子和 watch 函数来实现与 useEffect 类似的效果。

    1. onMounted: 在组件挂载后执行。
    2. onUpdated: 在组件更新后执行。
    3. onUnmounted: 在组件卸载前执行。
    4. watch: 监听特定数据的变化。

    下面是一个使用 vue3实现类似于 useEffect 的例子:

    import { ref, onMounted, watch, onUnmounted, onUpdated } from "vue";

    type EffectCleanup = void | (() => void);
    export default function useEffect(
    setup: () => EffectCleanup,
    dependencies?: readonly unknown[]
    ): void {
    const cleanupRef = ref<EffectCleanup | null>(null);
    const runEffect = () => {
    // 判断下一次执行副作用前还有没有清理函数没有执行
    if (cleanupRef.value) {
    cleanupRef.value();
    }
    // 执行副作用,并赋值清理函数
    cleanupRef.value = setup();
    };
    // 组件挂载的时候执行一次副作用
    onMounted(runEffect);
    // 判断有没有传依赖项,有的话就watch监听
    if (dependencies && dependencies.length > 0) {
    watch(dependencies, runEffect);
    } else if(dependencies === undefined) {
    // 没有传依赖项就组件每次渲染都要执行副作用
    onUpdated(runEffect)
    }
    // 组件销毁的使用如果有清理函数就执行清理函数
    onUnmounted(() => {
    if (cleanupRef.value) {
    cleanupRef.value();
    }
    });
    }

    useReducer

    React 中的 useReducer:

    useReducer 是 React 中的另一个 Hook,用于处理具有复杂状态逻辑的组件。它接受一个包含当前状态和触发状态更新的函数的 reducer,以及初始状态。通过 useReducer,我们可以更好地管理和处理复杂的状态变更逻辑。

    基本语法如下:

    import React, { useReducer } from 'react';

    // 定义 reducer 函数
    const reducer = (state, action) => {
    switch (action.type) {
    case 'increment':
    return { count: state.count + 1 };
    case 'decrement':
    return { count: state.count - 1 };
    default:
    return state;
    }
    };

    function ExampleComponent() {
    // 使用 useReducer,传入 reducer 函数和初始状态
    const [state, dispatch] = useReducer(reducer, { count: 0 });

    return (
    <div>
    <p>Count: {state.count}p>
    <button onClick={() => dispatch({ type: 'increment' })}>加一button>
    <button onClick={() => dispatch({ type: 'decrement' })}>减一button>
    div>
    );
    }

    通过刚刚实现的 useState 来实现类似 useReducer 的功能:

    import { UnwrapRef } from "vue";
    import useState from "./useState";

    type ReducerType = (state: T, action: A) => any;
    export default function useReducer(
    reducer: ReducerType<UnwrapRef, A>,
    initialArg: T,
    init?:
    (value: T) => T
    ) {
    // 根据传没传init函数来初始化state
    const [state, setState] = useState(init ? init(initialArg) : initialArg);
    const dispatch = (action: A) => {
    // 通过reducer函数的返回结果来修改state的值
    setState((state) => reducer(state, action));
    };
    return [state, dispatch] as const;
    }

    <template>
    <div>
    <div>
    <p>Count: {{ state.count }}p>
    <button @click="() => dispatch({ type: 'increment' })">
    加一
    button>
    <button @click="() => dispatch({ type: 'decrement' })">
    减一
    button>
    div>
    div>
    template>

    useCallback

    React 中的 useCallback:

    useCallback 是 React 中的一个 Hook,用于返回一个 memoized 版本的回调函数,避免在每次渲染时都创建新的回调函数。这在防止不必要的渲染和优化性能方面非常有用。

    基本语法如下:

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

    function ExampleComponent() {
    const [count, setCount] = useState(0);

    // 使用 useCallback 返回 memoized 版本的回调函数
    const handleClick = useCallback(() => {
    setCount(count + 1);
    }, [count]); // 依赖数组中的值发生变化时,重新创建回调函数

    return (
    <div>
    <p>Count: {count}p>
    <button onClick={handleClick}>加一button>
    div>
    );
    }

    下面是一个使用 useState 和 vue3 中的 watch 模拟实现类似 useCallback 的例子:

    import { watch } from "vue";
    import useState from "./useState";

    type FnType = (...args: T[]) => any;
    export default function useCallback(fn: FnType, dependencies: D[]) {
    const [callback, setCallback] = useState(fn);
    // 如果依赖项有变更就把fn重新赋值没有就直接返回callback
    watch(
    dependencies,
    () => {
    setCallback((cb: FnType) => cb = fn);
    },
    { immediate: false }
    );
    return callback;
    }

    <template>
    <div>
    <button>{{ count }}button>
    <button @click="() => handerCount()">+button>
    div>
    template>

    useMemo

    React 中的 useMemo:

    useMemo 是 React 中的一个 Hook,用于记忆(memoize)计算结果,避免在每次渲染时都重新计算。它对于在渲染期间执行昂贵的计算并确保只在依赖项更改时重新计算结果非常有用。

    基本语法如下:

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

    function ExampleComponent() {
    const [count, setCount] = useState(0);

    // 使用 useMemo 记忆计算结果
    const expensiveCalculation = useMemo(() => {
    console.log('计算了一次...');
    return count * 2;
    }, [count]); // 依赖数组中的值发生变化时,重新计算结果

    return (
    <div>
    <p>Count1: {count}p>
    <p>Count2: {expensiveCalculation}p>
    <button onClick={() => setCount(count + 1)}>加一button>
    div>
    );
    }

    下面是一个使用 useStateuseEffect 和 vue3 的 computed 模拟实现类似 useMemo 的例子:

    import { UnwrapRef, computed } from "vue";
    import useEffect from "./useEffect";
    import useState from "./useState";

    export default function useMemo(
    calculateValue: () => R,
    dependencies: T[]
    ) {
    const [cache, setCache] = useStatenull>(null);
    // 判断依赖项有没有变更,没有就直接返回缓存,有的话就重新计算
    useEffect(() => {
    setCache((cache) => {
    return (cache = computed(calculateValue) as UnwrapRef);
    });
    }, dependencies);
    return cache as UnwrapRef;
    }

    <template>
    <div>
    <div>平方: {{ squareSum }}div>
    <div>平方: {{ squareSum }}div>
    <button @click="handelNumbers">更改numbersbutton>
    div>
    template>

    useRef

    React 中的 useRef:

    useRef 是 React 中的一个 Hook,主要用于在函数组件中创建一个可变的对象,该对象的 current 属性被初始化为传入的参数。通常用于获取或存储组件中的引用(reference),并且不会触发组件重新渲染。

    基本语法如下:

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

    function ExampleComponent() {
    const myRef = useRef(null);

    useEffect(() => {
    // 使用 myRef.current 访问引用的 DOM 元素
    console.log(myRef.current);
    }, []);

    return <div ref={myRef}>获取DOMdiv>;
    }

    下面是一个使用 Vue 3 的 ref 模拟实现 useRef 的例子:

    import { ref, Ref } from "vue";

    function isHTMLElement(obj: unknown): obj is HTMLElement {
    return obj instanceof HTMLElement;
    }

    function useRefextends HTMLElement>(initialValue: T | null): Refnull>;
    function useRefextends unknown>(
    initialValue: T extends HTMLElement ? never : T
    ): { current: T };

    function useRef(
    initialValue: unknown
    ): Ref<HTMLElement | null> | { current: unknown } {
    // 判断传入的是不是一个HTML节点
    // 这里可能有点问题就是,或者传入null也会被判定为HTML节点,我没想到怎么解决这个问题
    if (isHTMLElement(initialValue) || initialValue === null) {
    return ref(initialValue);
    } else {
    // 不是就返回一个普通对象
    return {
    current: initialValue,
    };
    }
    }

    export default useRef;

    <template>
    <div>
    <input ref="myInputRef" type="text" />
    <p>Counter: {{ counterRef.current }}p>
    <button @click="incrementCounter">加一button>
    div>
    template>

    补充

    对于react中的createContext,useContext和vue3中的provide,inject很像。

    React 中的 createContext 和 useContext:

    1. createContext: 用于创建一个上下文对象,它包含一个 Provider 组件和一个 Consumer 组件。createContext 接受一个默认值,这个默认值在组件树中找不到对应的 Provider 时被使用。
    const MyContext = React.createContext(defaultValue);
    1. useContext: 用于在函数组件中订阅上下文的变化,获取当前 Provider 提供的值。
    const contextValue = useContext(MyContext);

    Vue3 中的 provide 和 inject:

    1. provide: 用于在父组件中提供数据,被提供的数据可以被子组件通过 inject 访问到。provide 接受一个对象,对象的属性即为提供的数据。

    1. inject: 用于在子组件中注入父组件提供的数据。可以是一个数组,也可以是一个对象,对象的属性为子组件中的变量名,值为从父组件中注入的数据。

    相似之处:

    • 目的相同: 无论是 React 中的上下文和钩子,还是 Vue 3 中的 provide 和 inject,它们都旨在实现组件之间的状态共享,提供一种在组件树中传递数据的方式。
    • 使用方式: 在使用上,它们都在父组件中提供数据,并在子组件中获取数据。
    • 避免了 props 层层传递: 这些机制都避免了将数据通过 props 层层传递的麻烦,特别在深层嵌套的组件树中,可以更方便地进行状态管理。

    总体而言,虽然具体的实现和语法有所不同,但这些机制在概念上非常相似,都是为了解决在组件树中共享数据的问题。

    总结

    本文源于作者的一时灵感,尝试探讨在 Vue 3 中是否能实现类似 React Hooks 的功能。虽然这个想法是出于好奇和娱乐,但在实际的开发中或许并没有太多实际用途。

    通过对比 React Hooks 和 Vue 3 Composition API,我们发现两者在语法和实现上存在一些差异,但本质上都为开发者提供了在函数组件中组织和重用逻辑的方式。这种灵活性是前端技术不断演进的体现,而每一种方式都有其适用的场景。

    在实际项目中,选择使用 React Hooks 还是 Vue3 Composition API 取决于团队和个人的偏好,以及项目的具体需求。技术的发展是不断前行的过程,而我们在其中的探索和实践都是宝贵的经验。

    愿读者在技术的海洋中,既能保持对新鲜事物的好奇心,又能在实际项目中选择合适的工具,取得更好的开发效果。无论是整活还是严肃的技术探讨,都让我们在编码的世界里保持一份热爱和乐趣。


    作者:辛克莱
    来源:juejin.cn/post/7328229830134972425
    收起阅读 »

    换个角度学TS,也许你能熟悉它

    web
    前言 TS绝非为了炫技的存在, 一切为了类型安全, 牢记这句话, 牢记这句话, 牢记这句话!!!感谢林不渡和光神大佬, 本文基本上都是从大佬们那里学来的。 一道开胃菜 function memoize

    前言


    TS绝非为了炫技的存在, 一切为了类型安全, 牢记这句话, 牢记这句话, 牢记这句话!!!感谢林不渡和光神大佬, 本文基本上都是从大佬们那里学来的。


    一道开胃菜


    function memoizeextends (...args: any[]) => any>(fn: T) {
    const cache = new Map()
    return (...args: Parameters<typeof fn>) => {
    const key = JSON.stringify(fn)
    if (cache.has(key)) {
    return cache.get(key)
    }
    const result = fn(...args)
    cache.set(key, result)
    return result
    }
    }

    const add = (a: number, b: number) => a + b
    const memoAdd = memoize(add)
    console.log(memoAdd(1, 2)) // 3
    console.log(memoAdd(1, 2)) // 3

    上面这个缓存函数的有个泛型T, 泛型T被约束为是一个可以是任意类型参数和返回任意类型的函数, 函数被调用时才明确知道T的类型, 比如下面的add函数, 这个时候TS类型就自动推导T是一个a、b参数类型为number返回一个number的函数, 我们看缓存函数内部返回一个函数接收的参数类型应该和T的参数类型一致, 这个时候可以使用TS内置的工具类型Parameters, 它可以返回一个函数类型的参数数组类型, 所以我们typeof fn, 就可以拿到fn的类型, 或者直接使用Parameters


    我们来看看Parameters是怎么实现的:


    type Parametersextends (...args: any) => any> =
    T extends (...args: infer P) => any ? P : never;

    Parameters工具类型接收一个泛型T被约束为可以是任意的函数类型, 后面用了infer类型推导, 可以在类型被使用时推导出具体的类型, T如果是(...args: infer P) => any的子类型, 就返回P, 否则返回never(表示永不可达)。


    不熟悉这种语法没关系, 我们把TS的内置类型都实现一遍,熟能生巧。


    TS内置类型工具


    Awaited


    // 基础用法
    type promise = Promise<string>
    type p = Awaited // string

    // 定义一个返回 Promise 的函数
    function fetchData(): Promise<string> {
    return new Promise((resolve) => {
    setTimeout(() => {
    resolve('成功啦啦啦');
    }, 1000);
    });
    }
    // 使用 Awaited 获取 Promise 结果的类型
    type ResultType = Awaited<ReturnType<typeof fetchData>>;

    const result: ResultType = 'Awaited'; // 此处类型会被推断为 string

    async function useResult() {
    const data = await fetchData();
    console.log(data); // 此处 data 的类型已经被推断为 string
    }
    useResult();

    这里的ReturnType和Parameters是配对的, 一个是获取函数类型的参数类型, 一个是获取函数类型的返回值类型, 我们看看ReturnType是怎么实现的


    type ReturnTypeextends (...args: any) => any> =
    T extends (...args: any) => infer R ? R : any;

    我们发现和上面Parameters的实现如出一辙, 只是把infer推断从参数的位置移动到了函数返回值的位置。不过ReturnType如果不满足条件返回的是any而不是never了, 这并没有什么影响。所以ReturnType拿到的类型是定义promise函数的返回类型Promise, 而我们的Awaited就是要拿到Promise里面的类型string


    这里有个思路


    type MyAwait = T extends Promise ? P : never
    type p = MyAwait<Promise<string>> // string

    利用infer推断Promise里面的类型, 好的, 恭喜你, 你已经了解TS了, 可是这并不全面, 如果Promise返回的还是一个Promise呢


    type MyAwait = T extends Promise ? P : never
    type p = MyAwait<Promise<Promise<string>>> // Promise

    递归?如果P还是一个Promise就递归调用MyAwait直到拿到最里面的类型, 没错就是你想的这样, 非常完美


    type MyAwait = T extends Promise // T如果是Promise的子类型
    ? P extends Promise<unknown> // 如果推断出来的P还是一个Promise
    ? MyAwait

    // 递归MyAwait


    : P // 不是Promise就直接返回P
    : T; // 如果泛型传的都不是一个promise直接返回T
    type p = MyAwait<Promise<Promise<string>>>; // string


    我们来看看TS内部是如何实现的


    type Awaited = T extends null | undefined
    ? T // 对于 `null | undefined` 这种特殊情况,当未启用 `--strictNullChecks` 时,直接返回 T
    : T extends object & { then(onfulfilled: infer F, ...args: infer _): any }
    // 仅当类型 T 是一个对象并且具有可调用 `then` 方法时,才会进行下一步处理
    ? F extends (value: infer V, ...args: infer _) => any
    // 如果 `then` 方法的参数是可调用的,提取其第一个参数的类型
    ? Awaited // 递归地解开该值的嵌套异步类型
    : never // `then` 方法的参数不可调用
    : T; // 非对象或不具有 `then` 方法的类型

    Partial


    // 基础用法
    type obj = {
    a: 1,
    b: 2
    }
    type obj2 = Partial
    /**
    * type obj2 = {
    a?: 1 | undefined;
    b?: 2 | undefined;
    }
    */


    我们来实现实现,上面我们讲的是infer提取类型如果有深层的话就是extends配合三元递归,这个Partial就可以用映射成新类型?:表示的就是可选, 可选后除了原来的类型还联合了undefined类型, 这是为了类型安全考虑可选后就可能没有值。


    // 基础用法
    type obj = {
    a: 1,
    b: 2
    }
    type MyPartial = {
    [K in keyof T]?: T[K]
    }
    type obj2 = MyPartial
    /**
    * type obj2 = {
    a?: 1 | undefined;
    b?: 2 | undefined;
    }
    */


    原来的obj类型被构造成了一个新的类型obj2,还是一个对象, 对象里面的每一个键(K)是(in)对象里面所有的键(keyof T)?:(可选) T[K](T对象里面的K键的值),反复想想是不是这回事。


    如果有多个对象嵌套,就递归


    type obj = {
    a: 1,
    b: {
    c: 2
    }
    }
    type DeepPartial = {
    [K in keyof T]?: T[K] extends object ? DeepPartial : T[K]
    }
    type obj2 = DeepPartial
    /**
    * type obj2 = {
    a?: 1 | undefined;
    b?: DeepPartial<{
    c: 2;
    }> | undefined;
    }
    */


    Required


    // 基础用法
    type obj = {
    a: 1,
    b: {
    c: 2
    }
    }
    type MyPartial = {
    [K in keyof T]?: T[K]
    }
    type obj2 = Required<MyPartial>
    /**
    *type obj2 = {
    a: 1;
    b: {
    c: 2;
    };
    }
    */


    Required就是把可选的变成必传的,非常简单,只需要把?去掉


    // 基础用法
    type obj = {
    a: 1,
    b: {
    c: 2
    }
    }
    type MyPartial = {
    [K in keyof T]?: T[K]
    }
    type MyRequired = {
    [K in keyof T]-?: T[K]
    }
    type obj2 = MyRequired<MyPartial>
    /**
    *type obj2 = {
    a: 1;
    b: {
    c: 2;
    };
    }
    */


    直接-?就可以了,神奇吧,那如果对象嵌套了,我想你会举一反三了吧,没错还是它递归


    type DeepRequired = {
    [K in keyof T]-?: T[K] extends object ? DeepRequired : T[K]
    }

    Readonly


    type obj = {
    a: 1,
    b: {
    c: 2
    }
    }
    type obj2 = Readonly
    /**
    * type obj2 = {
    readonly a: 1;
    readonly b: {
    c: 2;
    };
    }
    */


    Readonly就是把对象里面的值变成只读的,看这个obj2的类型,我想你知道怎么做了吧


    type MyReadonly = {
    readonly [K in keyof T]: T[K]
    }

    type DeepReadonly = {
    readonly [K in keyof T]: T[K] extends object ? DeepReadonly : T[K]
    }

    Record


    type obj = Record<string, any>
    /**
    * type obj = {
    [x: string]: any;
    }
    */


    其实根据上面学的,你已经会实现它了


    type MyRecordextends keyof any, T> = {
    [P in K]: T
    }

    type obj = MyRecord<string, any>
    /**
    * type obj = {
    [x: string]: any;
    }
    */


    K extends keyof any: 这里使用了泛型约束,确保 K 是任何类型的键集合的子集。keyof any 表示任何类型的所有键的联合。[P in K]: T: 这是一个映射类型。它表示对于 K 中的每个键 P,都创建一个属性,其值类型是 T


    Pick


    type MyPickextends object, K extends keyof T> = {
    [P in K]: T[K]
    }

    type obj = MyPick<{a: 1, b: 2}, 'a'>
    /***
    * type obj = {
    a: 1;
    }
    */


    Omit


    type MyOmitextends object, K extends keyof T> =
    PickExclude>

    type obj = MyOmit<{ a: 1, b: 2 }, 'a'>
    /***
    * type obj = {
    b: 2;
    }
    */



    • Exclude: 使用 keyof T 获取对象 T 的所有键,然后使用 Exclude 排除其中与 K 相同的键。这样得到的是 T 中除去 K 对应键的键集合。

    • Pick: 使用 Pick 从对象 T 中选择具有特定键集合的属性。在这里,我们选择了 T 中除去 K 对应键的其他所有属性。


    我们来看看Exclude的实现


    Exclude


    type MyExclude = T extends U ? never : T
    type T0 = MyExclude<"a" | "b" | "c", "a">;
    // type T0 = "b" | "c"

    如果T中存在U就剔除(never)否则保留


    Extract


    很明显就是Exclude的反向操作


    type MyExtract = T extends U ? T : never
    type T0 = MyExtract<"a" | "b" | "c", "a">;
    // type T0 = "a"

    NonNullable


    type T0 = NonNullable<string | number | undefined>;
    type T1 = NonNullable<string[] | null | undefined>;
    type NonNullable = T & {};

    T0 的类型是 string | number。这是因为 NonNullable 类型别名被定义为 T & {},这意味着它接受一个类型 T,并返回一个新的类型,该新类型是 T 和空对象类型的交叉类型。由于 TypeScript 中的交叉类型(T & {})会过滤掉 nullundefined,因此 T0 的类型就是排除了 undefinedstring | number


    也可以这样实现


    type MyNonNullable = T extends null | undefined ? never : T;

    ConstructorParameters


    type MyConstructorParametersextends abstract new (...args: any) => any> =
    T extends abstract new (...args: infer P) => any ? P : never;
    class C {
    constructor(a: number, b: string) {}
    }
    type T3 = MyConstructorParameters<typeof C>;
    // type T3 = [a: number, b: string]

    还是老套路infer推断,这个和我们上面那个获取函数参数类型差不多的,换了个形式。



    • T extends abstract new (...args: any) => any: 这是对输入类型 T 进行约束,要求它是一个抽象类的构造函数类型。

    • T extends abstract new (...args: infer P) => any ? P : never: 这是一个条件类型,检查 T 是否符合指定的构造函数模式。如果符合,就返回构造函数的参数类型 P,否则返回 never


    InstanceType


    class C {
    x = 0;
    y = 0;
    }
    type MyInstanceTypeextends abstract new (...args: any) => any> =
    T extends abstract new (...args: any) => infer R ? R : never;
    type T0 = MyInstanceType<typeof C>;
    // type T0 = C

    和我们上面那个实现的获取函数类型返回值类型如出一辙和获取类的构造器类型是配对的。



    • T extends abstract new (...args: any) => any: 这是对输入类型 T 进行约束,要求它是一个抽象类的构造函数类型。

    • T extends abstract new (...args: any) => infer R ? R : any: 这是一个条件类型,检查 T 是否符合指定的构造函数模式。如果符合,就返回构造函数的实例类型 R,否则返回 never


    ThisParameterType


    function toHex(this: Number) {
    return this.toString(16);
    }
    function numberToString(n: ThisParameterType<typeof toHex>) {
    return toHex.apply(n);
    }

    像这种提取类型的都用infer,先猜一猜,内部肯定是判断T是不是一个函数, 第一个参数this infer R是就返回R不是就返回never。
    我们看看答案


    type ThisParameterType =
    T extends (this: infer U, ...args: never) => any
    ? U
    : unknown;

    和我们猜想的差不多,我想你现在应该可以类型编程了吧。


    TS内部还有四个内置类型是通过JS来实现的,我们就不研究了


    `Uppercase`
    `Lowercase`
    `Capitalize`
    `Uncapitalize`

    可以看看我的这篇文章vue里面对于TS的使用 # 突发奇想(吃饱了撑的) vue3能实现react中的hooks吗?


    祝大家TS的编程技术越来越好吧,本人非常菜,大佬轻喷。


    作者:辛克莱
    来源:juejin.cn/post/7332435905926070322

    JS 不写分号踩了坑,但也可以不踩坑

    web
    前言 “所有直觉性的 “当然应该加分号” 都是保守的、未经深入思考的草率结论。” —— 尤雨溪。 重新认识分号在代码中的作用,为什么要写分号?又为什么可以没有分号? 踩的坑 写一个方法将秒数转为“xx天xx时xx分xx秒”的形式 const ONEDAYSEC...
    继续阅读 »

    前言


    “所有直觉性的 “当然应该加分号” 都是保守的、未经深入思考的草率结论。” —— 尤雨溪。

    重新认识分号在代码中的作用,为什么要写分号?又为什么可以没有分号?


    踩的坑


    写一个方法将秒数转为“xx天xx时xx分xx秒”的形式


    const ONEDAYSECOND = 24 * 60 * 60
    const ONEHOURSECOND = 60 * 60
    const ONEMINUTESECOND = 60

    function getQuotientandRemainder(dividend,divisor){
    const remainder = dividend % divisor
    const quotient = (dividend - remainder) / divisor
    return [quotient,remainder]
    }

    function formatSeconds(time){
    let restTime,day,hour,minute
    restTime = time
    [day,restTime] = getQuotientandRemainder(restTime,ONEDAYSECOND)
    [hour,restTime] = getQuotientandRemainder(restTime,ONEHOURSECOND)
    [minute,restTime] = getQuotientandRemainder(restTime,ONEMINUTESECOND)
    return day + '天' + hour + '时' + minute + '分' + restTime + '秒'
    }
    console.log(formatSeconds(time)) // undefined天undefined时undefined分NaN,NaN秒

    按照这段代码执行完后,day、hour、minute这些变量得到的都是 undefined,而 restTime 则好像得到一个数组。

    问题就在于 13、14、15、16 行之间没有添加分号,导致解析时,没有将这三行解析成三条语句,而是解析成一条语句。最终的表达式就是这样的:


    restTime = time[day,restTime] = getQuotientandRemainder(restTime,ONEDAYSECOND)[hour,restTime] = getQuotientandRemainder(restTime,ONEHOURSECOND)[minute,restTime] = getQuotientandRemainder(restTime,ONEMINUTESECOND)

    那执行的过程相当于给 restTime 进行赋值,表达式从左往右执行,最终表达式的值为右值。最右边的值就是 getQuotientandRemainder(restTime,ONEMINUTESECOND),由于在计算过程中 restTime 还没有被赋值,一直是 undefined,所以经过 getQuotientandRemainder 计算后得到的数组对象每个成员都是 NaN,最终赋值给 restTime 就是这样一个数组。


    分号什么时候会“自动”出现


    有时候好像不写分号也不会出问题,比如这种情况:


    let a,b,c
    a = 1
    b = 2
    c = 3
    console.log(a,b,c) // 1 2 3

    这是因为,JS 进行代码解析的时候,能够识别出语句的结束位置并“自动添加分号“,从而能够解析出“正确”的抽象语法树,最终执行的结果也就是我们所期待的。

    JS 有一个语法特性叫做 ASI (Automatic Semicolon Insertion),就是上面说到的”自动添加分号”的东西,它有一定的插入规则,在满足时会为代码自动添加分号进行断句,在我们不写分号的时候,需要了解这个规则,才能不踩坑。(当然这里说的加分号并不是真正的加分号,只是一种解析规则,用分号来代表语句间的界限)


    ASI 规则


    JS 只有在出现换行符的时候才会考虑是否添加分号,并且会尽量“少”添加分号,也就是尽量将多行语句合成一行,仅在必要时添加分号。


    1. 行与行之间合并不符合语法时,插入分号


    比如上面那个自动添加分号的例子,就是合并多行时会出现语法错误。

    a = 1b = 2 这里 1b 是不合法的,因此会加入分号使其合法,变为 a = 1; b = 2


    2. 在规定[no LineTerminator here]处,插入分号


    这种情况很有针对性,针对一些特定的关键字,如 return continue break throw async yield,规定在这些关键字后不能有换行符,如果在这些关键字后有了换行符,JS 会自动在这些关键字后加上分号。
    看下面这个例子🌰:


    function a(){
    return
    123
    }
    console.log(a()) // undefined

    function b(){
    return 123
    }
    console.log(b()) // 123

    在函数a中,return 后直接换行了,那么 return 和 123 就会被分成两条语句,所以其实 123 根本不会被执行到,而 return 也是啥也没返回。


    3. ++、--这类运算符,若在一行开头,则在行首插入分号


    ++ 和 -- 既可以在变量前,也可以在变量后,如果它们在行首,当多行进行合并时,会产生歧义,到底是上一行变量的运算,还是下一行变量的运算,因此需要加入分号,处理为下一行变量的运算。


    a
    ++
    b
    // 添加分号后
    a
    ++b

    如果你的预期是:


    a++ 
    b

    那么就会踩坑了。


    4. 在文件末尾发现语法无法构成合法语句时,会插入分号


    这条和 1 有些类似


    不写分号时需要注意⚠️


    上面的 ASI 规则中,JS 都是为了正确运行代码,必须按照这些规则来分析代码。而它不会做多余的事,并且在遵循“尽量合并多行语句”的原则下,它会将没有语法问题的多行语句都合并起来。这可能违背了你的逻辑,你想让每行独立执行,而不是合成一句。开头贴出的例子,就是这样踩坑的,我并不想一次次连续的对数组进行取值🌚。

    因此我们要写出明确的语句,可以被合并的语句,明确是多条语句时需要加上分号。


    (如果你的项目中使用了某些规范,它不想让你用分号,别担心,它只是不想让你在行尾用分号,格式化时它会帮你把分号移到行首)像这样:


    // before lint
    restTime = time;
    [day, restTime] = getQuotientandRemainder(restTime, ONEDAYSECOND);
    [hour, restTime] = getQuotientandRemainder(restTime, ONEHOURSECOND);
    [minute, restTime] = getQuotientandRemainder(restTime, ONEMINUTESECOND);

    // after lint
    restTime = time
    ;[day, restTime] = getQuotientandRemainder(restTime, ONEDAYSECOND)
    ;[hour, restTime] = getQuotientandRemainder(restTime, ONEHOURSECOND)
    ;[minute, restTime] = getQuotientandRemainder(restTime, ONEMINUTESECOND)

    参考



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

    基于 localStorage 实现有过期时间的存储方式

    web
    我们知道 localStorage 中的数据是长期保存的,除非手动删除,否则他会一直存在。如果我们想实现一种数据有过期时间的存储方式,该怎么实现呢? 首先应该想到的是 cookie,cookie 本身就有有效期的配置,当某个 cookie 时,浏览器自动清理该...
    继续阅读 »

    我们知道 localStorage 中的数据是长期保存的,除非手动删除,否则他会一直存在。如果我们想实现一种数据有过期时间的存储方式,该怎么实现呢?


    首先应该想到的是 cookie,cookie 本身就有有效期的配置,当某个 cookie 时,浏览器自动清理该 cookie。可是使用 cookie 存储数据,有个不好的地方,很多我们存储的数据,本就是我们前端自己用到的,后端根本用不到。可是存储到 cookie 中后,页面中所有的 cookie 都会随着请求发送给后端,造成传输的 cookie 比较长,而且没有必要。


    低调低调


    因此,我们可以基于 localStorage 来实现一套这样的有过期时间的存储方式。我们在之前的文章 如何重写 localStorage 中的方法 中,也了解了一些重写 localStorage 的方法。这里我们是自己在外层封装一层的方式,来调用 localStorage。


    我这里封装的类名叫: LocalExpiredStorage,即有过期时间的 localStorage。


    1. 实现与 localStorage 基本一致的 api


    我们为了实现跟 localStorage 使用上的一致性体验,这里我们自己的 api 名称和实现方式跟 localStorage 基本一致。


    interface SetItemOptions {
    maxAge?: number; // 从当前时间往后多长时间过期
    expired?: number; // 过期的准确时间点,优先级比maxAge高
    }

    class LocalExpiredStorage {
    private prefix = "local-"; // 用于跟没有过期时间的key进行区分

    constructor(prefix?: string) {
    if (prefix) {
    this.prefix = prefix;
    }
    }

    setItem(key: string, value: any, options?: SetItemOptions) {}
    getItem(key: string): any {}
    removeItem(key: string) {}
    clearAllExpired() {}
    }
    const localExpiredStorage = new LocalExpiredStorage();
    export default localExpiredStorage;

    可以看到我们实现的类里,有三个变化:



    1. setItem()方法新增了一个 options 参数,这里主要是为了配置过期时间,这里有两种配置方式,一种是可以设置多长时间后过期,比如 2 个小时后过期(开发者不用特殊计算 2 个小时后的时间节点);再一种是设置过期的时间节点,该值可以是格式化的时间,也可以是时间戳;

    2. 有一个 prefix 属性,在具体实现中,我们会将 prefix 属性与操作的 key 进行拼接,标识该 key 是具有过期时间特性的,方便我们自己的类进行处理;

    3. 新增了一个 clearAllExpired() 方法,这是为了清理所有已经过期的 key,避免占用缓存;该方法在应用的入口处就应当调用,便于及时清理;


    上面是我们的大致框架,接下来我们来具体实现下这些方法。


    干饭


    2. 具体实现


    接下来我们来一一实现这些方法。


    2.1 setItem


    这里我们新增了一个 options 参数,用来配置过期时间:



    • expired: 固定的过期时间点,比如点击关闭按钮,当天不再展示,那过期时间就是今天晚上的 23:59:59,可以使用该属性;

    • maxAge: 从当前时间起,设置多长时间后过期;比如点击某个提示,3 天内不再展示,使用该属性就比较方便;


    假如两个属性都设置了,我这里约定 expired 属性的优先级更高一些。


    class LocalExpiredStorage {
    private prefix = "local-"; // 用于跟没有过期时间的key进行区分

    constructor(prefix?: string) {
    if (prefix) {
    this.prefix = prefix;
    }
    }

    setItem(key: string, value: any, options?: SetItemOptions) {
    const now = Date.now();
    let expired = now + 1000 * 60 * 60 * 3; // 默认过期时间为3个小时

    // 这里我们限定了 expired 和 maxAge 都是 number 类型,
    // 您也可以扩展支持到 string 类型或者如 { d:2, h:3 } 这种格式
    if (options?.expired) {
    expired = options?.expired;
    } else if (options?.maxAge) {
    expired = now + options.maxAge;
    }

    // 我们这里用了 dayjs 对时间戳进行格式化,方便快速识别
    // 若没这个需要,也可以直接存储时间戳,减少第三方类库的依赖
    localStorage.setItem(
    `${this.prefix}${key}`,
    JSON.stringify({
    value,
    start: dayjs().format("YYYY/MM/DD hh:mm:ss"), // 存储的起始时间
    expired: dayjs(expired).format("YYYY/MM/DD hh:mm:ss"), // 存储的过期时间
    })
    );
    }
    }

    我们在过期时间的实现过程中,目前只支持了 number 类型,即需要传入一个时间戳,参与运算。您也可以扩展到 string 类型(比如'2024/11/23 14:45:34')或者其他格式{ d:2, h:3 } 这种格式。


    设置好过期时间后,我们将 value,存储的起始时间和过期时间,转义成 json string 存储起来。我们这里用了 dayjs 对时间戳进行格式化,方便开发者可以快速地识别。若没有这个需要,也可以直接存储时间戳,减少第三方类库的依赖。


    该方法并没有支持永久存储的设定,若您需要永久存储,可以直接使用 localStorage 来存储。


    2.2 getItem


    获取某 key 存储的值,主要是对过期时间的判断。


    class LocalExpiredStorage {
    private prefix = "local-"; // 用于跟没有过期时间的key进行区分

    constructor(prefix?: string) {
    if (prefix) {
    this.prefix = prefix;
    }
    }

    getItem(key: string): any {
    const result = localStorage.getItem(`${this.prefix}${key}`);
    if (!result) {
    // 若key本就不存在,直接返回null
    return result;
    }
    const { value, expired } = JSON.parse(result);
    if (Date.now() <= dayjs(expired).valueOf()) {
    // 还没过期,返回存储的值
    return value;
    }
    // 已过期,删除该key,然后返回null
    this.removeItem(key);
    return null;
    }
    removeItem(key: string) {
    localStorage.removeItem(`${this.prefix}${key}`);
    }
    }

    在获取 key 时,主要经过 3 个过程:



    1. 若本身就没存储这个 key,直接返回 null;

    2. 已存储了该 key 的数据,解析出数据和过期时间,若还在有效期,则返回存储大数据;

    3. 若已过期,则删除该 key,然后返回 null;


    这里我们在删除数据时,使用了this.removeItem(),即自己实现的删除方法。本来我们也是要实现这个方法的,那就直接使用了吧。


    2.3 clearAllExpired


    localStorage 中的数据并不会自动清理,我们需要一个方法用来手动批量清理已过期的数据。


    class LocalExpiredStorage {
    private prefix = "local-"; // 用于跟没有过期时间的key进行区分

    clearAllExpired() {
    let num = 0;

    // 判断 key 是否过期,然后删除
    const delExpiredKey = (key: string, value: string | null) => {
    if (value) {
    // 若value有值,则判断是否过期
    const { expired } = JSON.parse(value);
    if (Date.now() > dayjs(expired).valueOf()) {
    // 已过期
    localStorage.removeItem(key);
    return 1;
    }
    } else {
    // 若 value 无值,则直接删除
    localStorage.removeItem(key);
    return 1;
    }
    return 0;
    };

    const { length } = window.localStorage;
    const now = Date.now();

    for (let i = 0; i < length; i++) {
    const key = window.localStorage.key(i);

    if (key?.startsWith(this.prefix)) {
    // 只处理我们自己的类创建的key
    const value = window.localStorage.getItem(key);
    num += delExpiredKey(key, value);
    }
    }
    return num;
    }
    }

    在项目的入口处添加上该方法,用户每次进入项目时,都会自动清理一次已过期的 key。


    醒一醒


    3. 完整的代码


    上面我们是分步讲解的,这里我们放下完整的代码。同时,我也在 GitHub 上放了一份:wenzi0github/local-expired-storage


    interface SetItemOptions {
    maxAge?: number; // 从当前时间往后多长时间过期
    expired?: number; // 过期的准确时间点,优先级比maxAge高
    }

    class LocalExpiredStorage {
    private prefix = "local-"; // 用于跟没有过期时间的key进行区分

    constructor(prefix?: string) {
    if (prefix) {
    this.prefix = prefix;
    }
    }

    // 设置数据
    setItem(key: string, value: any, options?: SetItemOptions) {
    const now = Date.now();
    let expired = now + 1000 * 60 * 60 * 3; // 默认过期时间为3个小时

    // 这里我们限定了 expired 和 maxAge 都是 number 类型,
    // 您也可以扩展支持到 string 类型或者如 { d:2, h:3 } 这种格式
    if (options?.expired) {
    expired = options?.expired;
    } else if (options?.maxAge) {
    expired = now + options.maxAge;
    }

    // 我们这里用了 dayjs 对时间戳进行格式化,方便快速识别
    // 若没这个需要,也可以直接存储时间戳,减少第三方类库的依赖
    localStorage.setItem(
    `${this.prefix}${key}`,
    JSON.stringify({
    value,
    start: dayjs().format("YYYY/MM/DD hh:mm:ss"), // 存储的起始时间
    expired: dayjs(expired).format("YYYY/MM/DD hh:mm:ss"), // 存储的过期时间
    })
    );
    }

    getItem(key: string): any {
    const result = localStorage.getItem(`${this.prefix}${key}`);
    if (!result) {
    // 若key本就不存在,直接返回null
    return result;
    }
    const { value, expired } = JSON.parse(result);
    if (Date.now() <= dayjs(expired).valueOf()) {
    // 还没过期,返回存储的值
    return value;
    }
    // 已过期,删除该key,然后返回null
    this.removeItem(key);
    return null;
    }

    // 删除key
    removeItem(key: string) {
    localStorage.removeItem(`${this.prefix}${key}`);
    }

    // 清除所有过期的key
    clearAllExpired() {
    let num = 0;

    // 判断 key 是否过期,然后删除
    const delExpiredKey = (key: string, value: string | null) => {
    if (value) {
    // 若value有值,则判断是否过期
    const { expired } = JSON.parse(value);
    if (Date.now() > dayjs(expired).valueOf()) {
    // 已过期
    localStorage.removeItem(key);
    return 1;
    }
    } else {
    // 若 value 无值,则直接删除
    localStorage.removeItem(key);
    return 1;
    }
    return 0;
    };

    const { length } = window.localStorage;
    const now = Date.now();

    for (let i = 0; i < length; i++) {
    const key = window.localStorage.key(i);

    if (key?.startsWith(this.prefix)) {
    // 只处理我们自己的类创建的key
    const value = window.localStorage.getItem(key);
    num += delExpiredKey(key, value);
    }
    }
    return num;
    }
    }
    const localExpiredStorage = new LocalExpiredStorage();
    export default localExpiredStorage;

    使用:


    localExpiredStorage.setItem("key", "value", { maxAge: 5000 }); // 有效期为5000毫秒
    localExpiredStorage.setItem("key", "value", {
    expired: Date.now() + 1000 * 60 * 60 * 12,
    }); // 有效期为 12 个小时,自己计算到期的时间戳

    // 获取数据
    localExpiredStorage.getItem("key");

    // 删除数据
    localExpiredStorage.removeItem("key");

    // 清理所有过期的key
    localExpiredStorage.clearAllExpired();

    4. 总结


    这个功能本身不难,也有很多开发者自己实现过。这里我也是总结下之前实现的过程。



    作者:小蚊酱
    来源:juejin.cn/post/7215775714417655867
    收起阅读 »

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

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

    是的,最近又踩坑了!


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


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


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


    正常的传参:


    image.png


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


    image.png


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


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


    方案一:encodeURIComponent/decodeURIComponent


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


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

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

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


    image.png


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



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


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

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


    方案二: qs.stringify()


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


    const qs = require('qs');

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


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


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

    别再只用axios了,试试这个更轻量的网络请求库!

    web
    嗨,我们又见面了!今天我要和大家分享一个前端工程师都会遇到的问题:如何优化移动端网络请求。相信很多人都用过 axios 或 fetch API,但你是否觉得处理分页逻辑、缓存、请求防抖等很麻烦呢?别担心,我找到了一个解决方案——Alova.js。 Alova...
    继续阅读 »

    嗨,我们又见面了!今天我要和大家分享一个前端工程师都会遇到的问题:如何优化移动端网络请求。相信很多人都用过 axios 或 fetch API,但你是否觉得处理分页逻辑、缓存、请求防抖等很麻烦呢?别担心,我找到了一个解决方案——Alova.js。



    Alova.js 是一个轻量级的请求策略库,它可以帮助我们简化网络请求的编写,让我们更专注于业务逻辑。它提供了多种服务端数据缓存模式,比如内存模式和持久化模式,这些都能提升用户体验,同时降低服务端的压力。而且,Alova.js 只有 4kb+,体积是 axios 的 30%,非常适合移动端使用。


    Alova.js 的基础请求功能非常简单,比如你可以这样请求数据:


    const todoDetail = alova.Get('/todo', { params: { id: 1 } });
    const { loading, data, error } = useRequest(todoDetail);

    它还提供了分页请求、表单提交、验证码发送、文件上传等多种请求策略,大大减少了我们的工作量。比如,使用分页请求策略,你只需要这样:


    const {
    loading,
    data,
    isLastPage,
    page,
    pageSize,
    pageCount,
    total,
    } = usePagination((page, pageSize) => queryStudents(page, pageSize));

    怎么样,是不是很简单?Alova.js 还支持 Vue、React、React Native、Svelte 等多种前端框架,以及 Next、Nuxt、SvelteKit 等服务端渲染框架,非常适合现代前端开发。


    感兴趣的话,可以去 Alova.js 的官网看看:Alova.js 官网。也可以在评论区分享你对 Alova.js 的看法哦!嘿嘿,今天就聊到这里,下次见!👋
    有任何问题,你可以加入以下群聊咨询,也可以在github 仓库中发布 Discussions,如果遇到问题,也请在github 的 issues中提交,我们会在最快的时间解决。


    作者:古韵
    来源:juejin.cn/post/7332388389944819748
    收起阅读 »

    记录一次类似页面抽出经历

    web
    一、背景 刚入职了一家新公司,摸鱼式的把每个项目都打开看了下。不够五分钟便惊出了一身冷汗,午饭时分,便和领导聊了下这些项目中的通病。其中就有一个问题就是代码复用的问题,一个Vue文件6000多行代码,多个文件中还有很多重复的代码。领导听了休息了片刻便拍案而起,...
    继续阅读 »

    一、背景


    刚入职了一家新公司,摸鱼式的把每个项目都打开看了下。不够五分钟便惊出了一身冷汗,午饭时分,便和领导聊了下这些项目中的通病。其中就有一个问题就是代码复用的问题,一个Vue文件6000多行代码,多个文件中还有很多重复的代码。领导听了休息了片刻便拍案而起,好的小陈你就把这几个类似的页面抽出来吧!我:。。。。默默扒饭。


    二、问题和方案


    类似登录页这种几百年不变的页面,多个项目不管是逻辑还是UI基本上都是一样的,多个项目要用。虽然CV也挺快,但是如果逻辑一改的话,yi那其实还是挺麻烦的。(领导视角)


    方案一:Iframe嵌入主项目❌


    一开始是想把要引入的页面打包然后通过Iframe引入,但是这样的话会存在域不同的问题,而无法随心所欲的操作本地存储之类的东西。虽然可以用postMessage的方法进行通信传输数据,但是要传输到主页面的信息一多的话,很难分清楚哪个数据是所需要的。在尝试了半天之后,PASS了这个方案。


    方案二:将页面打包成组件,然后在主项目中注册且使用✔


    通过采用lib库模式打包Vue页面为组件,然后在主项目中引入,便可以实现页面的复用。然后引入组件也可以自由的访问本地存储等东西。
    打包命令:


    vue-cli-service build --target lib --name main --dest lib src/components/index.js

    详情可以参考官网的指南
    构建目标 | Vue CLI (vuejs.org)


    接下来便是痛苦且折磨的试错之路😖


    初步实现



    1. 要引入页面的项目结构(components中的About和Main中的文件即为要打包的文件)
      image.png

    2. 配置库打包的文件

      简单说明下这两个文件的作用:

      Main文件夹下面的index.js作用:包含Main的Vue文件注册成全局组件的方法;

      components文件夹下index.js作用:暴露出一个方法可以批量注册components下的组件。

      image.png


    接下来看下这两个文件的具体内容

    Main下面的index.js
    image.png


    components下面的index.js
    image.png


    看了下这两个文件的内容,写过Vue插件的铁铁们应该都很熟悉,对其实就是把页面当成组件了。有的铁铁举手问,小陈那个initRouter是啥呀,小陈后面为铁铁们解答,我们一步一步慢慢实现。

    Main页面如下
    image.png


    接下来便是通过命令行打包成组件的步骤了
    image.png


    现在打包的项目这边的任务就告一段落,后面我们看下主项目要如何引用这个被打包的组件。
    image.png
    只需要在主项目的main中注册我们打包的组件就可以使用了,然后结构出来的Main和About正是刚才我们在components下index.js暴露的两个组件,componentPage则是components下index.js暴露的默认的install方法用于注册。启动下主项目试试就发现我们引入的两个组件都加到主项目路由里面去了。心头一甜但是隐约觉得事情没这么简单。😱


    image.png
    image.png


    三、遇到的问题及解决策略


    问题、组件需要使用主项目的路由


    以登录页为例子,在用户验证完身份之后,需要跳转到主项目中的其他页面。例如跳转home需要跳转到主项目的home页面,在主项目中点击会报错,因为组件路由根本没有这个路由配置,所以需要把主项目的路由引入到组件中,那要怎么做捏?容小陈慢慢解释。


    image.png


    解决:在注册的时候引入主项目的路由


    通过initRouter在注册组件的时候,把主项目的路由引入到组件中,然后在需要使用主项目路由的时候,使用getCurRouter给组件路由赋值成主项目的路由即可。只不过在使用router的js文件都需要使用getCurRouter。Vue文件中则不需要做任何配置,因为this.router/this.route访问的均是主项目的路由。

    组件的路由文件配置
    image.png
    组件下components的index.js配置
    image.png
    Main中跳转的方法
    image.png
    顺便一提:判断生产还是开发环境都是为了开发的时候,不用做额外的配置,只是方法比较笨。如果大佬们有更好的方法麻烦踢一下小陈。


    总结


    这是小陈第一次在掘金上写文章,可能这篇文章的作用不是很大,但也是记录小陈解决问题的载体。文章有啥不清楚的或者不合理的地方还麻烦铁铁们和小陈促膝长谈。但是此次的实践还是让小陈对Vue的一些知识这块有了新的理解。然后日后还请大佬们多多指教。

    Demo的地址: only-for-test: 仅用来测试的仓库 (gitee.com)


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

    曹贼,莫要动‘我’网站 —— MutationObserver

    web
    前言 本篇主要讲述了如何禁止阻止控制台调试以及使用了MutationObserver构造方法禁止修改DOM样式。 正文 话说在三国时期,大都督周瑜为了向世人宣布自己盛世容颜的妻子小乔,并专门创建了一个网站来展示自己的妻子 这么好看的看的小乔,谁看谁不糊,更何...
    继续阅读 »

    前言


    本篇主要讲述了如何禁止阻止控制台调试以及使用了MutationObserver构造方法禁止修改DOM样式。


    正文


    话说在三国时期,大都督周瑜为了向世人宣布自己盛世容颜的妻子小乔,并专门创建了一个网站来展示自己的妻子


    image.png
    这么好看的看的小乔,谁看谁不糊,更何况曹老板。这天,曹操在浏览网页的时候,无意间发现了周瑜的这个网站,看着美若天仙的小乔,曹操的眼泪止不住的从嘴角流了下来。赶紧将网站上的照片保存了下来。

    这个消息最后传到了周瑜的耳朵里,他只是想展示小乔,可不是为了让别人下载的。于是在自己的网站上做了一些预防措施。

    为了防止他人直接在网站上直接下载图片,周瑜将右键的默认事件给关闭了,并且为了防止有人打开控制台,并对图片保存,采取了以下方法:


    禁用右键和F12键


    //给整个document添加右击事件,并阻止默认行为
    document.addEventListener("contextmenu", function (e) {
    e.preventDefault();
    return false;
    });

    //给整个页面禁用f12按键 keyCode即将被禁用 不再推荐使用 但仍可以使用
    document.addEventListener("keydown", function (e) {
    //当点了f3\f6\f10之后,即使禁用了f12键依旧可以打开控制台,所以一并禁用
    if (
    [115, 118, 121, 123].includes(e.keyCode) ||
    ["F3", "F6", "F10", "F12"].includes(e.key) ||
    ["F3", "F6", "F10", "F12"].includes(e.code) ||
    //ctrl+f 效果和f3效果一样 点开搜索之后依旧可以点击f12 打开控制台 所以一并禁用
    //缺点是此网站不再能够 **全局搜索**
    (e.ctrlKey && (e.key == "f" || e.code == "KeyF" || e.keyCode == 70))||
    //禁用专门用于打开控制台的组合键
    (e.ctrlKey && e.shiftKey && (e.key == "i" || e.code == "KeyI" || e.keyCode == 73))
    ) {
    e.preventDefault();
    return false;
    }
    });

    当曹操再次想保存小乔照片的时候,发现使用网页的另存了已经没用了。这能难倒曹老板吗,破解方法,在浏览器的右上角进行操作就可打开控制台,这个地方是浏览器自带的,没办法禁用


    image.png
    这番操作之后,曹操可以选择元素保存那个图片了。周瑜的得知了自己的禁用措施被破解后,赶忙连夜加班打补丁,于是又加了一些操作,禁止打开控制台后进行操作


    禁用控制台


    如何判定控制台被打开了,可以使用窗口大小来判定


    function resize() {
    var threshold = 100;
    //窗口的外部减窗口内超过100就判定窗口被打开了
    var widthThreshold = window.outerWidth - window.innerWidth > threshold;
    var heightThreshold = window.outerHeight - window.innerHeight > threshold;
    if (widthThreshold || heightThreshold) {
    console.log("控制台打开了");
    }
    }
    window.addEventListener("resize", resize);

    但是也容易被破解,只要让控制台变成弹窗窗口就可以了


    也可以使用定时器进行无限debugger,因为只有在控制台打开的时候debugger才会生效。关闭控制台的时候,并不会影响功能。当前网页内存占用比较大的时候,定时器的占用并不明显。在当前网页占用比较小的时候,一直开着定时器才会有较为明显的提升


      setInterval(() => {
    (function () {})["constructor"]("debugger")();
    }, 500);

    破解方法一样有,在debugger的位置右键禁用调试就可以了。这样控制台就可以正常操作了


    image.png
    既然有方法破解,就还要做一层措施,既然是要保存图片,那就把img转成canvas,这样即使打开控制台也没办法进行对图片的保存


    //获取dom
    const img = document.querySelector(".img");
    const canvas = document.querySelector("#canvas");
    //img转成canvas
    canvas.width = img.width;
    canvas.height = img.height;
    ctx = canvas.getContext("2d");
    ctx.drawImage(img, 0, 0, img.width, img.height);
    document.body.removeChild(img);

    经过一夜的努力,该加的措施都加上了。周瑜心想这下就没办法保存我的小乔了吧。

    来到曹操这边,再次打开周瑜的小破站,还想故技重施时,发现已经有了各种显示,最后也没难倒曹操,那些阻碍也都被破解了。但是到保存图片的时候傻眼了,竟然已经不是图片格式了,那就没办法下载了呀。但是小乔真的很养神,曹操心有不甘,于是使用了最后一招,既然没办法下载那就截图,虽然有损画质,但是依旧能看。


    得知如此情况的大都督周瑜不淡定了,从未见过如此厚颜无耻之人,竟然使用截图。


    006APoFYly1g2qcclw1frg308w06ox2t.gif
    话说魔高一尺,道高一丈,周瑜再次熬夜加班进行对网站的优化。于是使用了全屏水印+MutationObserver监听水印dom的方法。即使截图也让他看着不舒服。


    MutationObserver


    MutationObserver是一个构造函数,接口提供了监视对 DOM 树所做更改的能力。它被设计为旧的 Mutation Events 功能的替代品,该功能是 DOM3 Events 规范的一部分。

    它接收一个回调函数,每当监听的dom发生改变时,就会调用这个函数,函数传入一个参数,数组包对象的格式,里面记录着dom的变化以及dom的信息。


    image.png
    返回的实例是一个新的、包含监听 DOM 变化回调函数的 MutationObserver 对象。有三个方法observedisconnecttakeRecords



    • observe接收两个参数,第一个为要监听的dom元素,第二个则是一些配置对象,当调用 observe() 时,childListattributes 和 characterData 中,必须有一个参数为 true。否则会抛出 TypeError 异常。配置对象如下:

      • subtree:当为 true 时,将会监听以 target 为根节点的整个子树。包括子树中所有节点的属性,而不仅仅是针对 target。默认值为 false

      • childList:当为 true 时,监听 target 节点中发生的节点的新增与删除(同时,如果 subtree 为 true,会针对整个子树生效)。默认值为 false

      • attributes:当为 true 时观察所有监听的节点属性值的变化。默认值为 true,当声明了 attributeFilter 或 attributeOldValue,默认值则为 false

      • attributeFilter:一个用于声明哪些属性名会被监听的数组。如果不声明该属性,所有属性的变化都将触发通知。

      • attributeOldValue:当为 true 时,记录上一次被监听的节点的属性变化;可查阅监听属性值了解关于观察属性变化和属性值记录的详情。默认值为 false

      • characterDate:当为 true 时,监听声明的 target 节点上所有字符的变化。默认值为 true,如果声明了 characterDataOldValue,默认值则为 false

      • characterDateOldValue:当为 true 时,记录前一个被监听的节点中发生的文本变化。默认值为 false



    • disconnect方法用来停止观察(当被观察dom节点被删除后,会自动停止对该dom的观察),不接受任何参数

    • takeRecords:方法返回已检测到但尚未由观察者的回调函数处理的所有匹配 DOM 更改的列表,使变更队列保持为空。此方法最常见的使用场景是在断开观察者之前立即获取所有未处理的更改记录,以便在停止观察者时可以处理任何未处理的更改。


    该构造函数监听的dom即使在控制台中被更改属性或值,也会被监听到。


    使用MutationObserver对水印dom进行监听,并限制更改。


    <style>
    //定义水印的样式
    #watermark {
    width: 100vw;
    height: 100vh;
    position: absolute;
    left: 0;
    top: 0;
    font-size: 34px;
    color: #32323238;
    font-weight: 700;
    display: flex;
    flex-wrap: wrap;
    justify-content: space-evenly;
    align-content: space-evenly;
    z-index: 9999999;
    }
    #watermark span {
    transform: rotate(45deg);
    }
    </style>

    <script>
    //获取水印dom
    const watermark = document.querySelector("#watermark");
    //克隆水印dom ,用作后备,永远不要改变
    const _watermark = watermark.cloneNode(true);
    //获取水印dom的父节点
    const d = watermark.parentNode;
    //获取水印dom的后一个节点
    let referenceNode;
    [...d.children].forEach((item, index) => {
    if (item == watermark) referenceNode = d.children[index + 1];
    });
    //定义MutationObserver实例observe方法的配置对象
    const prop = {
    childList: true,//针对整个子树
    attributes: true,//属性变化
    characterData: true,//监听节点上字符变化
    subtree: true,//监听以target为根节点的整个dom树
    };
    //定义MutationObserver
    const observer = new MutationObserver(function (mutations) {
    //在这里每次坚挺的dom发生改变时 都会运行,传入的参数为数组对象格式
    mutations.forEach((item) => {
    //这里可以只针对监听dom的样式来判断
    if (item.attributeName === "style") {
    //获取父节点的所有子节点,因为时伪数组,使用扩展运算符转以下
    [...d.children].forEach((v) => {
    //判断一下,是父节点里的那个节点被改变了,并且删除那个被改变的节点(也就是删除水印节点)
    if (item.target.id && v == document.querySelector(`#${item.target.id}`)) {
    v.remove();
    }
    });
    //原水印节点被删除了,这里使用克隆的水印节点,再次克隆
    const __watermark = _watermark.cloneNode(true);
    //这里的this指向是MutationObserver的实例对象,所以同样可以使用observe监听dom
    //监听第二次克隆的dom
    this.observe(__watermark, prop);
    //因为水印dom被删除了,再将克隆的水印dom添加到原来的位置 就是referenceNode节点的前面
    d.insertBefore(__watermark, referenceNode);
    }
    });
    });
    在初始化的时候监听初始化的水印dom
    observer.observe(watermark, prop);
    </script>



    这样,每当对水印dom进行更改样式的时候,就会删除该节点,并重新添加一个初始的水印dom,即使突破重重困难打开开控制台,用户也是无法对dom 进行操作。


    视频转Gif_爱给网_aigei_com.gif


    隔天曹操再次打开网页,发现网页上的水印,心里不足为惧,心想区区水印能难倒自己?操作到最后却发现,不论如何对水印dom进行操作,都无法改变样式。虽说只是为了保存图片,但是截图有着这样水印,任谁也不舒服呀。曹操大怒,刚吃了两口的饭啪的一下就盖在了桌子上......


    20230508094549_33500.gif
    然而曹操不知道的是,在控制台中,获取dom节点右键是可以只下载获取的那个节点的......


    image.png


    结尾


    文章主要是以鬼畜恶搞的方式讲述了,如何禁止用户打开控制台(还有重写toSring,consloe.log等一些方法,但我并没有没有实现,所以这里并没有写上),并且如何使用MutationObserver构造函数来监听页面中的dom元素。其实大多情况下并没有这方面的项目需求,完全可以当扩展知识看了。


    写的不好的地方可以提出意见,虚心请教!


    作者:iceCode
    来源:juejin.cn/post/7290862554657423396
    收起阅读 »

    前端实现 word 转 png

    web
    在此之前 word 转图片的需求都是在后端实现的,用的是 itext 库,但是 itext 收费的,商用需要付费。近期公司内部安全部门发出侵权警告,要求整改。 所以采用前端实现 word 文档转图片功能。 一、需求 用户在页面上上传 .docx 格式的文件...
    继续阅读 »

    在此之前 word 转图片的需求都是在后端实现的,用的是 itext 库,但是 itext 收费的,商用需要付费。近期公司内部安全部门发出侵权警告,要求整改。


    所以采用前端实现 word 文档转图片功能。



    一、需求



    1. 用户在页面上上传 .docx 格式的文件

    2. 前端拿到文件,解析并生成 .png 图片

    3. 上传该图片到文件服务器,并将图片地址作为缩略图字段


    二、难点


    目前来看,前端暂时无法直接实现将 .docx 文档转成图片格式的需求


    三、解决方案


    既然直接转无法实现,那就采用迂回战术



    1. 先转成 html(用到库 docx-preview

    2. 再将 html 转成 canvas(用到库 html2canvas

    3. 最后将 canvas 转成 png


    四、实现步骤




    1. .docx 文件先转成 html 格式,并插入到目标节点中


      安装 docx-preview 依赖: pnpm add docx-preview --save




    jsx
    复制代码
    import { useEffect } from 'react';
    import * as docx from 'docx-preview';

    export default ({ file }) => {
    useEffect(() => {
    // file 为上传好的 docx 格式文件
    docx2Html(file);
    }, [file]);

    /**
    * @description: docx 文件转 html
    * @param {*} file: docx 格式文件
    * @return {*}
    */
    const docx2Html = file => {
    if (!file) {
    return;
    }
    // 只处理 docx 文件
    const suffix = file.name?.substr(file.name.lastIndexOf('.') + 1).toLowerCase();
    if (suffix !== 'docx') {
    return;
    }
    // 生成 html 后挂载的 dom 节点
    const htmlContentDom = document.querySelector('#htmlContent');
    const docxOptions = Object.assign(docx.defaultOptions, {
    debug: true,
    experimental: true,
    });
    docx.renderAsync(file, htmlContentDom, null, docxOptions).then(() => {
    console.log('docx 转 html 完成');
    });
    };

    return <div id='htmlContent' />;
    };

    此时,在 idhtmlContent 的节点下,就可以看到转换后的 html 内容了( htmlContent 节点的宽高等 css 样式自行添加)




    1. html 转成 canvas


      安装 html2canvas 依赖: pnpm add html2canvas --save




    jsx
    复制代码
    import html2canvas from 'html2canvas';

    /**
    * @description: dom 元素转为图片
    * @return {*}
    */
    const handleDom2Img = async () => {
    // 生成 html 后挂载的 dom 节点
    const htmlContentDom = document.querySelector('#htmlContent');
    // 获取刚刚生成的 dom 元素
    const htmlContent = htmlContentDom.querySelectorAll('.docx-wrapper>section')[0];
    // 创建 canvas 元素
    const canvasDom = document.createElement('canvas');
    // 获取 dom 宽高
    const w = parseInt(window.getComputedStyle(htmlContent).width, 10);
    // const h = parseInt(window.getComputedStyle(htmlContent).height, 10);

    // 设定 canvas 元素属性宽高为 DOM 节点宽高 * 像素比
    const scale = window.devicePixelRatio; // 缩放比例
    canvasDom.width = w * scale; // 取文档宽度
    canvasDom.height = w * scale; // 缩略图是正方形,所以高度跟宽度保持一致

    // 按比例增加分辨率,将绘制内容放大对应比例
    const canvas = await html2canvas(htmlContent, {
    canvas: canvasDom,
    scale,
    useCORS: true,
    });
    return canvas;
    };


    1. 将生成好的 canvas对象转成 .png 文件,并下载


    jsx
    复制代码
    // 将 canvas 转为 base64 图片
    const base64Str = canvas.toDataURL();

    // 下载图片
    const imgName = `图片_${new Date().valueOf()}`;
    const aElement = document.createElement('a');
    aElement.href = base64Str;
    aElement.download = `${imgName}.png`;
    document.body.appendChild(aElement);
    aElement.click();
    document.body.removeChild(aElement);
    window.URL.revokeObjectURL(base64Str);

    五、总结


    前端无法直接实现将 .docx 文档转成图片格式,所以要先将 .docx 文档转换成 html 格式,并插入页面文档节点中,然后根据 html 内容生成canvas对象,最后将 canvas对象转成 .png 文件


    有以下两个缺点:



    1. 只能转 .docx 格式的 word 文档,暂不支持 .doc 格式;

    2. 无法自动获取文档第一页来生成图片内容,需要先将 word 所有页面生成为 html,再通过 canvas 手动裁切,来确定图片宽高。

    作者:Tim_
    链接:https://juejin.cn/post/7331799381896151067
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    低成本创建数字孪生场景-开发篇

    web
    介绍 本文承接《低成本创建数字孪生场景-数据篇》,获取到了数据之后就是愉快的开发环节,我们用到了开源项目CesiumJS做为GIS技术框架。 CesiumJS项目从创建至今已经有12年历史,提供了创建3D地球和2D地图的强大功能,它支持多种地图数据源,可以创建...
    继续阅读 »

    介绍


    本文承接《低成本创建数字孪生场景-数据篇》,获取到了数据之后就是愉快的开发环节,我们用到了开源项目CesiumJS做为GIS技术框架。


    CesiumJS项目从创建至今已经有12年历史,提供了创建3D地球和2D地图的强大功能,它支持多种地图数据源,可以创建复杂的3D地形和城市模型。CesiumJS的功能强大,但入门难度比较高,需要提前了解很多概念和设计理念,为方便理解本案例仅仅使用其提供的一些基础功能。


    Guanlianx_5.gif


    需求说明


    为了搭建1个简易的山区小乡镇场景,我们首先梳理下需求,把任务分解好。



    1. 在底图上叠加各种图层

      • 支持叠加地形图层、3DTiles图层、数据图层

      • 支持多种方式分发图层数据



    2. 鼠标与图层元素的交互

      • 鼠标移动时,使用屏幕事件处理器监听事件,获取当前屏幕坐标

      • 如果已经有高亮的元素,将其恢复为正常状态

      • 以当前屏幕坐标为起点发送射线,获取射线命中的元素,如果有命中的元素就高亮它

      • 鼠标点击时使用屏幕事件处理器获取命中元素,如果命中了,就判断元素是否描边状态,有则取消描边,没有则增加描边轮廓



    3. 加载Gltf等其他模型

      • 模型与其他图层元素一样,可以被光标拾取

      • 模型支持播放自带动画




    准备工作


    数据分发服务


    当前案例涉及的图层数据以文件类为主,为方便多处使用,需要将图层的服务独立部署,这里有两个方案:



    1. 自行搭建静态文件服务器,网上搜一个常用的node.js静态服务脚本即可

    2. 把文件放到cesium ion上,如果你要使用cesium ion资产,需要注意配好defaultAccessToken,具体调用方式看下文的代码实现


    安装依赖


    以下为本案例的前端工程使用的核心框架版本


    依赖版本
    vue^3.2.37
    vite^2.9.14
    Cesium^1.112.0

    代码实现



    1. 地图基本场景,本示例使用vite+vue3开发,html非常简单,只需要一个

      标签即可,在cesiumjs中以Viewer为起点调用其他控件,因此我们实例化一个Cesium.viewer, 这里面有非常多配置参数,详细请看开发文档


      import * as Cesium from 'cesium'
      import 'cesium/Build/Cesium/Widgets/widgets.css'

      Cesium.Ion.defaultAccessToken = '可以把一些GIS资产放到Cesium ION上托管,Tokenw为调用凭证'

      // 地图中心
      const center = [1150, 29]

      // cesium实例
      let viewer = null

      // 容器
      const cesiumContainer = ref(null)

      onMounted(async () => {
      await init()
      })

      async function init() {
      viewer = new Cesium.Viewer(cesiumContainer.value, {
      timeline: true, //显示时间轴
      animation: true, //开启动画
      sceneModePicker: true, //场景内容可点击
      baseLayerPicker: true, //图层可点击
      infoBox: false, // 自动信息弹窗
      shouldAnimate: true // 允许播放动画
      })
      // 初始化镜头视角
      restoreCameraView()

      // 开启地形深度检测
      viewer.scene.globe.depthTestAgainstTerrain = true
      // 开启全局光照
      viewer.scene.globe.enableLighting = true
      // 开启阴影
      viewer.shadows = true

      })

      // 设置初始镜头
      function restoreCameraView(){
      viewer.camera.flyTo({
      destination: Cesium.Cartesian3.fromDegrees(center[0], center[1], 0),
      orientation: {
      heading: Cesium.Math.toRadians(0), // 相机的方向
      pitch: Cesium.Math.toRadians(-90), // 相机的俯仰角度
      roll: 0 // 相机的滚动角度
      }
      })
      }

      // 加载地形图层
      async function initTerrainLayer() {
      const tileset = await Cesium.CesiumTerrainProvider.fromUrl(
      'http://localhost:9003/terrain/c8Wcm59W/',
      {
      requestWaterMask: true,
      requestVertexNormals: false
      }
      )
      viewer.terrainProvider = tileset
      }


    2. 在地图上叠加地形图层,图层数据可以自行部署


      // 方法1: 加载本地地形图层
      async function initTerrainLayer() {
      const tileset = await Cesium.CesiumTerrainProvider.fromUrl(
      'http://localhost:9003/terrain/c8Wcm59W/',
      {
      requestWaterMask: true,
      requestVertexNormals: false
      }
      )
      viewer.terrainProvider = tileset
      }

      // 方法2: 加载Ion地形图层
      async function initTerrainLayer() {
      const tileset = await Cesium.CesiumTerrainProvider.fromIonAssetId(1,{
      requestVertexNormals: true
      }
      )
      viewer.terrainProvider = tileset
      }


    3. 加载3DTiles图层,与地形图层类似,换成了Cesium3DTileset类。需要注意使用url加载需要自行解决跨域问题


      const tileset = await Cesium.Cesium3DTileset.fromUrl(
      'http://localhost:9003/model/tHuVnsJXZ/tileset.json',
      {}
      )
      // 将图层加入到场景
      viewer.scene.primitives.add(tileset)

      // 适当调整图层位置
      const translation = getTransformMatrix(tileset, { x: 0, y: 0, z: 86 })
      tileset.modelMatrix = Cesium.Matrix4.fromTranslation(translation)

      // 获取变化矩阵
      function getTransformMatrix (tileset, { x, y, z }) {
      // 高度偏差,正数为向上偏,负数为向下偏,根据真实的模型位置不断进行调整
      const heightOffset = z
      // 计算tileset的绑定范围
      const boundingSphere = tileset.boundingSphere
      // 计算中心点位置
      const cartographic = Cesium.Cartographic.fromCartesian(boundingSphere.center)
      // 计算中心点位置坐标
      const surface = Cesium.Cartesian3.fromRadians(cartographic.longitude,
      cartographic.latitude, 0)
      // 偏移后的三维坐标
      const offset = Cesium.Cartesian3.fromRadians(cartographic.longitude + x,
      cartographic.latitude + y, heightOffset)

      return Cesium.Cartesian3.subtract(offset, surface, new Cesium.Cartesian3())
      }


    4. 鼠标事件交互,鼠标悬浮,在改变选中元素的状态之前,需要将它的当前状态保存下来以便下次可以恢复。


      // 缓存高亮状态
      const highlighted = {
      feature: undefined,
      originalColor: new Cesium.Color()
      }

      // 鼠标与物体交互事件
      function initMouseInteract () {
      // 事件处理器
      const handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas)

      // 鼠标悬浮选中
      handler.setInputAction((event) => {
      // 将原有高亮对象恢复
      if (Cesium.defined(highlighted.feature)) {
      highlighted.feature.color = highlighted.originalColor
      highlighted.feature = undefined
      }
      // 获取选中对象
      const pickedFeature = viewer.scene.pick(event.endPosition)

      if (Cesium.defined(pickedFeature)) {
      // 高亮选中对象
      if (pickedFeature !== moveSelected.feature) {
      highlighted.feature = pickedFeature
      Cesium.Color.clone(pickedFeature.color, highlighted.originalColor)
      pickedFeature.color = Cesium.Color.YELLOW
      }
      }
      }, Cesium.ScreenSpaceEventType.MOUSE_MOVE)


    5. 鼠标事件,鼠标点击,描边轮廓使用了Cesium自带的后期效果处理器,不需要自行编写着色器等操作,因此实现起来很便捷。只需要将选中的元素放到 效果的selected对象数组内就行了。


      // 缓存后期效果
      let edgeEffect = null

      function initMouseInteract(){
      // 鼠标点击选中
      handler.setInputAction((event) => {

      // 获取选中对象
      const pickedFeature = viewer.scene.pick(event.position)

      if (!Cesium.defined(pickedFeature)) {
      return null
      } else {

      // 描边效果:兼容GLTF和3DTiles
      setEdgeEffect(pickedFeature.primitive || pickedFeature)

      // 如果拾取的要素包含属性信息,则打印出来
      if (Cesium.defined(pickedFeature.getPropertyIds)) {
      const propertyNames = pickedFeature.getPropertyIds()
      const props = propertyNames.map(key => {
      return {
      name: key,
      value: pickedFeature.getProperty(key)
      }
      })
      console.info(props)
      }
      }
      }, Cesium.ScreenSpaceEventType.LEFT_CLICK)
      }

      // 选中描边
      function setEdgeEffect (feature) {
      if (edgeEffect == null) {
      // 后期效果
      const postProcessStages = viewer.scene.postProcessStages

      // 增加轮廓线
      const stage = Cesium.PostProcessStageLibrary.createEdgeDetectionStage()
      stage.uniforms.color = Cesium.Color.LIME //描边颜色
      stage.uniforms.length = 0.05 // 产生描边的阀值
      stage.selected = [] // 用于放置对元素

      // 将描边效果放到场景后期效果中
      const silhouette = Cesium.PostProcessStageLibrary.createSilhouetteStage([stage])
      postProcessStages.add(silhouette)

      edgeEffect = stage
      }

      // 选多个元素进行描边
      const matchIndex = edgeEffect.selected.findIndex(v => v._batchId === feature._batchId)
      if (matchIndex > -1) {
      edgeEffect.selected.splice(matchIndex, 1)
      } else {
      edgeEffect.selected.push(feature)
      }

      }


    6. 加载gltf模型, gltf加载后需要进行一次矩阵变换modelMatrix, 加载后启动指定索引的动画进行播放。


      // 加载模型
      async function loadGLTF () {

      let animations = null

      let modelMatrix = Cesium.Transforms.eastNorthUpToFixedFrame(
      Cesium.Cartesian3.fromDegrees(lng,lat,altitude)
      )

      const model = await Cesium.Model.fromGltfAsync({
      url: './static/gltf/windmill.glb',
      modelMatrix: modelMatrix,
      scale: 30,
      // minimumPixelSize: 128, // 设定模型最小显示尺寸
      gltfCallback: (gltf) => {
      animations = gltf.animations
      }
      })

      model.readyEvent.addEventListener(() => {
      const ani = model.activeAnimations.add({
      index: animations.length - 1, // 播放第几个动画
      loop: Cesium.ModelAnimationLoop.REPEAT, //循环播放
      multiplier: 1.0 //播放速度
      })
      ani.start.addEventListener(function (model, animation) {
      console.log(`动画开始: ${animation.name}`)
      })
      })

      viewer.scene.primitives.add(model)
      }



    部署说明



    1. 场景演示包括前端工程、GIS数据分发服务、服务端接口几个部分

    2. 前端工程使用vue3开发,其中CesiumJs通过NPM依赖包引入

    3. 场景中相关图层均为静态文件,可放入主工程静态目录中,也可以独立部署(需解决跨域访问),或者使用cesiumlab3分发服务便于管理

    4. web端场景对终端设备和浏览器有一定要求,具体配置需要进一步测试


    总结


    在本文中并没有涉及到服务端数据的接入,数据接入进来后,我们可以利用Cesium在GIS开发领域强大功能,与three.js的webGL开发优势,两者相互融合创建更多数据可视化效果。那么关于Cesium和three.js的融合开发还在初步探索阶段,希望下一次有精彩内容分享给大家。


    Hengjiang3.gif


    相关链接


    最新版cesium集成threejs


    Cesium和Three.js结合的5个方案


    Cesium实现更实用的3D描边效果


    作者:gyratesky
    来源:juejin.cn/post/7331626882552872986
    收起阅读 »

    前端将dom转换成图片

    web
    一、问题描述 在工作的过程中会遇到要将dom变成图片并下载的问题,刚开始使用的html2canvas进行转换的,但是我老大告诉我说,咱们做的是面向国外的网页,插件太大会导致页面加载慢的问题(国外部分地区网络没有国内这么发达),就让我用原生dom或一些比较小的插...
    继续阅读 »

    一、问题描述


    在工作的过程中会遇到要将dom变成图片并下载的问题,刚开始使用的html2canvas进行转换的,但是我老大告诉我说,咱们做的是面向国外的网页,插件太大会导致页面加载慢的问题(国外部分地区网络没有国内这么发达),就让我用原生dom或一些比较小的插件,在原生dom下载的时候遇到了context.drawImage(element, 0, 0, width, height)这一方法传入参数要传类型HTMLCanvasElement的问题,所以要将一个HTMLElement转换成HTMLCanvasElement,但是经过一些信息的查找,我发现有个很好用且轻量化的插件,可以完美解决这一问题,所以这里给大家推荐一个轻量级的插件dom-to-image(23kb),这个插件可以不用进行类型转换,直接将dom元素转换成需要的文件格式。


    二、dom-to-image的使用


    2.1 dom-to-image的安装


    在终端输入以下代码进行dom-to-image安装



    npm install dom-to-image



    2.2 dom-to-image引入


    2.2.1 vue项目引入


    在需要使用这个插件的页面使用以下代码进行局部引入


    import domToImage from 'dom-to-image';

    然后就可以通过以下代码进行图片的转换了


    const palGradientGap = document.getElementById('element')
    const canvas = document.createElement('canvas')
    canvas.width = element.offsetWidth
    canvas.height = element.offsetHeight
    this.domtoimage.toPng(element).then(function (canvas) {
    const link = document.createElement('a')
    link.href = canvas
    link.download = 'image.png' // 下载文件的名称
    link.click()
    })

    当然也可以进行全局引入
    创建一个domToImage.js文件写入以下代码


    import Vue from 'vue'; 
    import domToImage from 'dom-to-image';
    const domToImagePlugin = {
    install(Vue) {
    Vue.prototype.$domToImage = domToImage;
    }
    };
    Vue.use(domToImagePlugin);

    然后再入口文件main.js写入以下代码全局引入插件


    import Vue from 'vue'
    import App from './App.vue'
    import './domToImage.js'; // 引入全局插件
    Vue.config.productionTip = false
    new Vue({ render: h => h(App), }).$mount('#app')

    三、dom-to-image相关方法



    1. toSvg(node: Node, options?: Options): Promise<string>:将 DOM 元素转换为 SVG 图片,并返回一个 Promise 对象。


      参数说明:



      • node:要转换为图片的 DOM 元素。

      • options:可选参数对象,用于配置转换选项。



    2. toPng(node: Node, options?: Options): Promise<string>:将 DOM 元素转换为 PNG 图片,并返回一个 Promise 对象。


      参数说明:



      • node:要转换为图片的 DOM 元素。

      • options:可选参数对象,用于配置转换选项。



    3. toJpeg(node: Node, options?: Options): Promise<string>:将 DOM 元素转换为 JPEG 图片,并返回一个 Promise 对象。


      参数说明:



      • node:要转换为图片的 DOM 元素。

      • options:可选参数对象,用于配置转换选项。



    4. toBlob(node: Node, options?: Options): Promise<Blob>:将 DOM 元素转换为 Blob 对象,并返回一个 Promise 对象。


      参数说明:



      • node:要转换为图片的 DOM 元素。

      • options:可选参数对象,用于配置转换选项。



    5. toPixelData(node: Node, options?: Options): Promise<Uint8ClampedArray>:将 DOM 元素转换为像素数据,并返回一个 Promise 对象。


      参数说明:



      • node:要转换为图片的 DOM 元素。

      • options:可选参数对象,用于配置转换选项。



    6. toCanvas(node: Node, options?: Options): Promise<HTMLCanvasElement>:将 DOM 元素转换为 Canvas 对象,并返回一个 Promise 对象。


      参数说明:



      • node:要转换为图片的 DOM 元素。

      • options:可选参数对象,用于配置转换选项。




    其中,Options 参数是一个可选的配置对象,用于设置转换选项。以下是一些常用的选项:



    • width:输出图像的宽度,默认值为元素的实际宽度。

    • height:输出图像的高度,默认值为元素的实际高度。

    • style:要应用于元素的样式对象。

    • filter:要应用于元素的 CSS 滤镜。

    • bgcolor:输出图像的背景颜色,默认值为透明。

    • quality:输出图像的质量,仅适用于 JPEG 格式,默认值为 0.92。


    作者:crazy三笠
    来源:juejin.cn/post/7331626882553937946
    收起阅读 »

    JS 前端框架的新年预言

    web
    免责声明 本文属于是语冰的直男翻译了属于是,略有删改,仅供粉丝参考。英文原味版请传送 2024 Predictions by JavaScript Frontend Framework Maintainers。 本期共享的是 —— 来自 React/Next...
    继续阅读 »

    免责声明


    本文属于是语冰的直男翻译了属于是,略有删改,仅供粉丝参考。英文原味版请传送 2024 Predictions by JavaScript Frontend Framework Maintainers



    本期共享的是 —— 来自 React/Next.js/Angular/Solid 的维护者和创建者科普了它们计划在新年里框架改进的未来规划。


    fe-2024.png


    React:新年预览


    Meta(前脸书)的 React 工程经理 E.W. 表示,React 团队预计在新的一年里会有更多的框架采用 RSC(React 服务服务端组件)。


    “对于大多数人而言,RSC 已经对其所了解的 React 作用域产生了重大变化,从只是一个 UI 层,到对构建 App 的方式产生更大的影响,以享受最佳的用户体验和开发体验,尤其以前对于 SPA(单页应用程序)还不够好,”E.W. 如是说。


    虽然它没有具体爆料来年的任何新进展,但 E.W. 确实表示它们会发布并共享某些去年开始可公开的进展。举个栗子,在 React Advanced 上,该团队向与会者展示了 React Forget,这是 React 的自动记忆编译器。E.W. 表示,React Forget 意味着,开发者不再需要使用 useMemo/useCallback


    “在 React Native EU 上,我们表示,从 0.73 版本开始,我们会把 Web 开发者熟悉的 Chrome 开发工具移植到 React Native 中,”E.W. 补充道。“我们还共享了我们对 Static Hermes 的研究,它是我们的 JS 原生编译器,它不仅有可能加快 React Native App 的速度,还能从根本上改变 JS 的有效用途。”


    Next.js:正在运行的新编译器


    Next.js 推出了一款新的 App 服务器,旨在支持去年的 RSC(React 服务端组件)和 SA(服务端操作)。Vercel 的产品主管 L.R. 表示,它会继续支持旧版的 App 服务器,并且它们的路由系统是可互换的。这种互操作性意味着,开发者可以把时间花在添加新功能上。


    “有些客户已经使用 Next.js 开发了五六年,它们采用这些新功能也需要多年时间,”L.R. 讲道。“我们希望让大家尽可能顺利地度过这段旅程。”


    新的一年里,Next.js 想要解决一大坨问题,但其中一个优先事项可能是简化缓存。它说,就开发体验而言,这可能会更容易。


    “通常情况下,生态系统中的一大坨开发者必须引入一大坨额外软件包,或学习如何使用其他工具来请求、缓存和重新验证,”L.R. 说。“Next.js 现在已经内置了一大坨十分给力的同款功能,但这也意味着,大家需要学习其他东西,目前用户初步的反馈是,‘这很棒棒哒;它十分给力,但如果能更简单一点的话,我们会不吝赞词。’”


    Next.js 团队还会继续关注性能优化,它称之为“我们的持续投资”。


    它补充道,这可能会在新年里以新编译器的形式出现,这将加快在开发者的机器上启动 Next.js 的速度。该编译器已经投入使用了大约一年,Vercel 一直在内部将其用于其产品和 App。它说,由 Rust 提供支持的编译器,在无缓存的情况下比以前有缓存的编译器更快。


    L.R. 说:“我们推出该功能指日可待,大家都可以默认启动它,而且它比现存的 Webpack 编译解决方案更快。” “开发者希望它们的工具更快。它们永远不会抱怨它变得更快。因此,很有趣的是,可以看到工具作者,而不是工具的用户,而是实际的工具开发者转向 Rust 等较低阶的工具,帮助斩获咫尺之间的性能优势。”


    目标三是继续为 Next.js 的未来 10 年奠定基础。


    “你知道的,这个新的路由系统显然让我们十分鸡冻。我们相信这是未来的基础,”它说。“但这也需要时间。大家会尝试,用户会提出功能请求,它们会希望看到事情发生改变。我们认为这是未来五到十年的一项非常长期的投资。”


    它补充说,“有一天”但可能不是今年的目标是,寻求一种更棒的方案来处理 Next.js 内部的内容。


    “今天,它能奏效,我们仍然可以连接到想要的任何内容源,但存在某些方案可以简化开发体验,”它补充道。“与其说这是一项要求,不如说是一种美好的享受,这就是为什么私以为我们无法在新年实现此目标,但我想在未来用它搞点事情。”


    Angular:可选的 Zone.js


    谷歌 Angular DevRel 技术主管兼经理 M.G. 表示,在过去的一年里,Angular 的两大成就是:



    • 引入了 Signal(信号)的细粒度响应性

    • 引入了可延迟视图


    它讲道,明年会在此基础上,进一步关注细粒度响应性,并使 Zone.js 成为可选选项。


    在 Angular 中,Zone 是跨异步任务持续存在的执行上下文。Zone 的 GitHub 仓库对此进行了详细解释,但 Zone 有五大职责,包括但不限于拦截异步任务调度和包装错误处理的回调,以及跨异步操作的 Zone 追踪。Zone.js 可以创建跨异步操作持久存在的上下文,并为异步操作提供生命周期钩子。


    “我们正在探索为现存项目启用可选的 Zone.js,开发者应该可以通过重构现存 App 来利用该功能,”M.G. 如是说。“诉诸可选的 Zone.js,我们期望优化加载时间,并提升初始渲染速度。研究细粒度响应性将其提升到另一个水平,使我们能够只检测组件模板的局部变化。”


    它说,这些功能将带来更快的运行时间。


    在另一个性能游戏中,Angular 正在考虑是否默认启用混合渲染。它补充说,可以选择退出混合渲染,因为它会增加托管要求和成本。


    “我们瞄到了 SSG(静态站点生成)和 SSR(服务端渲染)的巨大价值,凭借我们在 v17 中奠定的坚硬基建,我们正在努力进行最后的润色,以便从一开始就实现这种体验,”M.G. 如是说。


    它补充道,另一个优先事项是落实 Signal 的征求意见。


    开发者还可能会见证 Angular 文档的改进。根据其开发者调查,开发者希望享受进阶的学习体验,其中一部分包括使 Angular.dev 成为 Angular 的全新官网主页。它补充道,开发者还优先考虑了初始加载时间(混合渲染、部分水合和可选的 Zone.js 部分应该解决此问题),以及组件创作(Angular 计划进一步简化组件创作)。


    “我们致力于可持续迭代功能,并与时俱进地渐进增强它们,”M.G. 讲道。“开发者将能够从新年里的所有优化中受益,并将在接下来的几年中享受更好的开发体验和性能。”


    Solid:聚焦原语


    “Solid 之父”R.C. 表示,Solid 开发者可以关注新年的 SolidStart 1.0 和 Solid.js 2.0。SolidStart 是一个元框架,这意味着,它构建于 Solid.js 框架之上。它说,它相相当于 Svelte 的 SvelteKit。


    SolidStart 的官网文档是这样解释的:


    “Web App 通常包含一大坨组件:数据库、服务器、前端、打包器、数据请求/变更、缓存和基建。编排这些组件极具挑战性,并且通常需要跨 App 堆栈大量共享状态和冗余逻辑。进入 SolidStart:一种元框架,它提供了将所有这些组件万法归一的平台。”


    由于 SolidStart 仍处于测试阶段,R.C. 基本上有机会使用生态系统中已有的内容来使其变得更好。


    “其中一个重要的部分是,我们现在不再编写自己的部署适配器,而是使用 Nitro,它也为 Nuxt 框架提供支持,这让我们可以部署到所有不同的平台,”R.C. 讲道。


    另一个例子是,任何 Solid 路由器都可以在 SolidStart 中奏效。


    “这意味着,对路由器的底层部分大量更新,这样它们能够“梦幻联动”,但我非常满意的最终结果是,我们的志愿者小团队需要维护的代码更少了,而且它为开发者提供了很大的灵活性和控制力,”它说。“它们不会被迫采用单一的解决方案,这对我而言兹事体大,因为每个人都有自己的需求。正如我所言,如果您构建正确的基建,并弄清楚这些构建模块是什么,大家可以做更多的事情。”


    它说,最终的结果是一个具有“可交换”部分的元框架,而且不太我行我素。在越来越多的元框架决定开发者技术方案的世界中,Solid 团队一直在思考正确的原语片段的影响。


    “于我而言,它始终是关于构建基元块,这是一个非常工程化的焦点,我认为这是它与众不同的部分原因,”它说。“我一直喜欢提供选择,而且私以为如果我们有正确的原语、正确的片段,我们就可以构建正确的解决方案。”


    它表示,Solid 2.0 应该会在新年中后期的某个时间点发布。它说,目前它们正在设计如何处理异步系统的原型。


    “Solid 2.0 也将是重量级版本,因为我们正在重新审视响应式系统,并研究如何解决异步 Signal 或异步系统,”R.C. 讲道。


    它补充道,Solid 试图平衡控制与性能。


    “我们的社区中有一大坨热心人,它们非常有技术头脑,既关心性能,也关心控制,”它说。“我们确实吸引了一大坨自己真正想要控制构建的方方面面的用户。”


    作者:人猫神话
    来源:juejin.cn/post/7331925629082566707
    收起阅读 »

    浏览器沙盒你知多少😍

    web
    开题话: 😍随着业务环境的快速变化,安全性是开发人员和测试人员在现代 Web 开发周期中面临的最大挑战之一。构建和部署现代 Web 应用程序的复杂性会导致更多的安全漏洞。根据 IBM 和 Ponemon Institute 的数据泄露成本报告,2021 年,数...
    继续阅读 »

    开题话:


    😍随着业务环境的快速变化,安全性是开发人员和测试人员在现代 Web 开发周期中面临的最大挑战之一。构建和部署现代 Web 应用程序的复杂性会导致更多的安全漏洞。根据 IBM 和 Ponemon Institute 的数据泄露成本报告,2021 年,数据泄露成本从 3 万美元(86 年的平均成本)上升到 2019 万美元,啧啧啧,这是该报告 4 年来的最高平均成本


    因此,网络安全在软件开发生命周期中变得越来越重要,以确保用户数据安全和隐私。如果你可以开发和测试网站和 Web 应用程序而不必担心安全漏洞,那不是很好吗?👏沙盒是一种可以帮助你实现此目的的技术。沙盒是一种安全隔离应用程序、Web 浏览器和一段代码的方法。它可以防止恶意或有故障的应用程序攻击或监视你的 Web 资源和本地系统。


    举个栗子➡, 在现实世界中,沙盒是被墙壁包围的儿童游乐区。它允许孩子们玩沙子,而草坪周围没有沙子。同样,沙盒浏览器创建了一个隔离的环境,用户可以在其中从第三方来源下载和安装应用程序,并在安全、隔离的环境中操作它们,即使他们行为可疑。因此,沙盒浏览器可以保护你的计算机免受额外的安全风险。


    下面我们说说什么是浏览器沙盒吧!😘


    本文将探讨什么是浏览器沙盒、不同类型的沙盒的优点和重要性,以及如何实现沙盒。


    一、什么是浏览器沙盒?


    为了防止系统或 Web 应用程序中出现安全漏洞,开发人员需要弄清楚如何处理它们。这是浏览器沙盒派上用场的时候。浏览器沙箱提供了一个安全的虚拟环境来测试有害代码或运行第三方软件,而不会损害系统的数据或本地文件。


    例如,如果你在沙盒中下载恶意附件,它不会损坏系统的现有文件或资源。沙盒具有同源功能,它允许JavaScript在网页上添加或自定义元素,同时限制对外部JSON文件的访问。


    今天,流行的网络浏览器,如Chrome,Firefox和Edge,都带有内置的沙箱。沙盒浏览器的最终目标是保护你的机器免受与浏览相关的风险。因此,如果用户从网站下载恶意软件,该软件将下载到浏览器的沙箱中。关闭沙箱时,其中的所有内容(包括有害代码)都会被清除。


    浏览器沙盒使用两种隔离技术来保护用户的 Web 浏览活动和系统硬件、本地 PC 和网络:



    • 本地浏览器隔离

    • 远程浏览器隔离


    本地浏览器隔离


    本地浏览器隔离是一种传统的浏览器隔离技术,它在沙盒中运行虚拟浏览器或在用户的本地基础结构上运行虚拟机。它有助于将数据与外部安全威胁和不安全浏览隔离开来。例如,如果恶意元素潜入,影响将仅限于沙盒浏览器和虚拟机。


    远程浏览器隔离


    远程浏览器隔离涉及一种虚拟化技术,其中浏览器在基于云的服务器(公共云和私有云)上运行。在远程隔离中,用户的本地系统没有浏览活动,浏览器沙盒、过滤和风险评估在远程服务器上进行。


    远程浏览器隔离涉及两种隔离用户本地基础结构和 Web 内容的方法:



    1. DOM 镜像:在这种技术中,浏览器并不完全与用户的本地系统隔离。但是,DOM 镜像技术会过滤恶意内容,并将其余内容呈现给用户。

    2. 可视化流式处理:此技术提供完全的远程浏览器隔离。可视化流式处理的工作方式类似于 VDI(虚拟桌面基础结构)系统,其中浏览器在基于云的服务器上运行,并将视觉输出显示到用户的本地计算机。


    二、为什么浏览器沙盒很重要?


    现代 Web 技术正在迅速扩展,从而使用户能够顺利开发和发布网站和 Web 应用程序。与此同时,对Web应用程序的需求也在以前所未有的速度增长。根据Imperva的一项调查,Web应用程序是50%数据泄露的来源。因此,拥有一个安全、受控的环境(如沙盒浏览器)至关重要,以便在不危及本地基础设施和系统资源的情况下执行操作。


    例如,用户在沙盒中运行 Web 浏览器。如果恶意代码或文件利用 Web 浏览器漏洞,则沙盒中的影响受到限制。此外,引爆程序可以帮助发现新的漏洞并在 Web 浏览器中缓解它们。但是,如果禁用沙盒浏览器,恶意程序可以利用 Web 浏览器漏洞并损坏用户的本地系统和资源。


    三、沙盒的好处


    将沙盒合并到 Web 开发工作流中有很多优点。下面提到了一些优点😍😍:



    • 沙盒使设备和操作系统免于面临潜在威胁。

    • 与未经授权的一方或供应商合作时,最好使用沙盒环境。在部署内容之前,你可以使用沙盒来测试可疑代码或软件。

    • 沙盒可以帮助防止零日攻击。由于开发人员无法发现漏洞的即时补丁,因此零日攻击本质上是有害的。因此,沙盒通过向系统隐藏恶意软件来减轻损害。

    • 沙盒环境隔离威胁和病毒。这有助于网络专家研究和分析威胁趋势。它可以防止未来的入侵和识别网络漏洞。

    • 沙盒应用程序是一种混合解决方案,这意味着它们可以在本地和远程部署(基于云的服务器)。混合系统比传统解决方案更安全、更可靠、更具成本效益。

    • 沙盒和 RDP(远程桌面协议)设置可帮助企业确保安全的外部网络连接。

    • 沙盒可以与防病毒或其他安全工具和策略结合使用,以增强整个安全生态系统。


    四、哪些应用正在沙盒化😎?


    我们在日常工作流程中使用的大部分资产(如在线浏览器、网页、PDF、移动应用程序和 Windows 应用程序)都是沙盒化的。


    下面列出了正在沙盒化的应用:



    • Web 浏览器:可能易受攻击的浏览器在沙盒环境中运行。

    • 浏览器插件:加载内容时,浏览器插件在沙盒中运行。沙盒浏览器插件(如 Java)更容易受到攻击。

    • 网页:浏览器以沙盒模式加载网页。由于网页是内置的 JavaScript,因此它无法访问本地计算机上的文件。

    • 移动应用:与 Android 和 iOS 一样,移动操作系统在沙盒模式下运行其应用。如果他们希望访问你的位置、联系人或其他信息,他们会弹出权限框。

    • Windows 软件和程序:在对系统文件进行更改之前,Windows 操作系统中的用户帐户控制 (UAC) 会请求你的许可。UAC 的功能类似于沙盒,但它不提供完整的保护。但是,不应禁用它。


    五、不同类型的沙盒


    在浏览器沙盒的这一部分中,我们将讨论不同类型的沙盒。沙盒分为三类:



    1. 应用程序沙盒

    2. 浏览器沙盒

    3. 安全沙盒


    应用程序沙盒


    使用应用程序沙箱,你可以在沙盒中运行不受信任的应用程序,以防止它们损坏本地系统或窃取数据。它有助于创建一个安全的环境,使应用程序可以在其中运行而不会损坏系统。通过将应用与用户的本地计算机隔离,应用程序沙盒增强了应用的完整性。


    浏览器沙盒


    可以在沙盒中执行基于浏览器的潜在恶意应用程序,以防止它们对你的本地基础架构造成损害。它导致建立一个安全的环境,在该环境中,Web 应用程序可以在不影响系统的情况下运行。引爆技术可以帮助发现 Web 浏览器中的新漏洞并缓解其。


    安全沙盒


    安全沙盒允许你探索和检测可疑代码。它扫描附件并识别潜在有害网站的列表,并确定是否下载或安装受感染的文件。


    六、使用内置沙盒浏览器进行沙盒分析


    沙盒预装在流行的浏览器中,如Chromium,Firefox和Edge,以保护你的系统免受浏览漏洞的影响。让我们看看沙盒在不同浏览器中的工作原理:


    Chromium浏览器沙盒


    Google Chrome和Microsoft Edge建立在Chromium浏览器上。代理和目标是构成 Chromium 浏览器沙箱的两个进程。目标进程是子进程,而浏览器进程是代理进程。目标进程的代码在沙盒环境中执行。代理进程在子进程和硬件资源之间操作,为子进程提供资源。


    火狐浏览器沙盒


    为了保护本地系统免受威胁,Firefox 在沙箱中执行不受信任的代码。Firefox 浏览器是使用父进程和子进程沙盒化的。浏览时,潜在的恶意程序会在沙盒中运行。在沙盒期间,父进程是子进程与其余系统资源之间的中介。


    你可以更改 Firefox 浏览器中的沙盒程度,使其限制最少、中等或高度严格:



    • 级别 0:限制最少

    • 1级:中等

    • 第 2 级:高度限制


    要检查 Firefox 沙盒浏览器的级别,请在地址栏中传递以下命令:


    arduino
    复制代码
    about:config

    在页面上,它将加载 Firefox 可配置变量。现在,在配置页面上点击“CTRL + F”,在搜索框中输入以下命令,然后按“Enter”。


    image.png


    image.png


    Edge浏览器沙盒


    当启动 Edge 沙盒浏览器 Windows 10 时,你将看到一个全新的桌面,该桌面仅具有“回收站”和 Edge 快捷方式。它显示“开始菜单”和其他图标,但它们在此沙盒环境中不起作用。你可以在标准Windows 10上访问它们,而不是沙盒Windows 10。


    关闭 Edge 浏览器沙盒后,你的浏览器历史记录将不再可用。你的 ISP 可能会跟踪沙盒中的操作,但此数据不可审核。


    七、禁用谷歌浏览器沙盒


    在执行基于 Chrome 的沙盒测试时,你可能会遇到这样一种情况,即沙盒功能可能会导致 Chrome 浏览器闪烁以下错误:“应用程序初始化失败”。


    在这种情况下,你可能需要停用 Chrome 浏览器沙盒。以下是以下步骤:



    1. 如果你没有 Google Chrome 沙盒快捷方式,请创建一个。

    2. 右键单击快捷方式,然后选择“属性”。选择属性

    3. 在目标中提供的应用路径中输入以下命令:



    css
    复制代码
    --no-sandbox

    属性 2



    1. 单击“应用”,然后单击“确定”。


    八、浏览器沙盒:它是100%安全的吗💢?


    大多数 Web 浏览器都使用沙盒。但是,互联网仍然是病毒和其他恶意软件的来源。沙盒的级别似乎有所不同。不同的 Web 浏览器以不同的方式实现沙盒,因此很难弄清楚它们是如何工作的。但是,这并不意味着所有网络浏览器都是不安全的。另一方面,浏览器沙箱可以使它们更安全。


    但是,如果你问它是否提供100%的安全性,答案是否定的。如果某些浏览器组件使用 Flash 和 ActiveX,则它们可能会延伸到沙箱之外。


    九、写在最后


    企业受到高级持续性威胁 (APT) 的攻击,沙盒可以保护它们。通过查看前方的情况,你可以为未知攻击做好准备。你可以在隔离的环境中测试和开发应用程序,而不会因沙盒而损害本地系统资产。Sandboxie,BitBox和其他沙盒工具在市场上可用。但是,在沙盒中设置和安装不同的浏览器需要时间。


    下回再说说前端——浏览器的安全性吧,本文就说到这里了😘


    感谢jym浏览本文🤞,若有更好的建议,欢迎评论区讨论哈🌹。


    如果jym觉得这篇干货很有用,加个关关🥰,点个赞赞🥰,后面我会继续卷,分享更多干货!感谢支持!😍


    作者:珑墨
    链接:https://juejin.cn/post/7241492186917847077
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    尾递归优化是一场谎言

    web
    TLDR 本文是对蹦床函数的应用案例。 蹦床函数跟尾递归优化都是为了解决递归次数过多导致调用栈溢出的问题。 蹦床函数的原理:令原函数返回一个可执行函数,由蹦床函数来控制执行时机,使执行函数与其父函数无执行调用关系。 处理递归栈溢出还有递归转迭代、异步执行等方...
    继续阅读 »

    TLDR



    • 本文是对蹦床函数的应用案例。

    • 蹦床函数跟尾递归优化都是为了解决递归次数过多导致调用栈溢出的问题。

    • 蹦床函数的原理:令原函数返回一个可执行函数,由蹦床函数来控制执行时机,使执行函数与其父函数无执行调用关系。

    • 处理递归栈溢出还有递归转迭代、异步执行等方案,但蹦床函数对代码的改动量很低,也不影响原代码的阅读和执行逻辑。


    起因


    接到一个需求,需要对 markdown 进行分句。大致如下:


    // 原始 md
    aaa。bbb[^1^]。ccc。

    // 转换后 md
    aaa。x:[bbb[^1^]]。ccc。

    即把以[^*^]结尾的句子给包起来。


    方案思路比较简单:



    • 写一个函数,入参为起始位置,大概这样:walk(startIndex)

    • 起始位置设为最后一个字符位置,往前找,如果找到[^*^]则往前找句子结束符(如。!?-),找到后就把整句话用x:[]包起来。

    • 继续调用这个函数,入参startIndex为刚刚那个句子的起始位置。

    • 直到入参为0(即找到头了),结束执行。


    完美,补充完测试用例,开心光速下班 😃。





    命运所有的馈赠早已暗中标好了价格。





    报错


    一天后:


    image.png


    image.png


    栈溢出了。


    同事的需求需要往这段 markdown 里填充上万字符,我的递归函数爆栈了 🤯🤯🤯。


    一时间脑海里立刻出现 3 条解决方案:



    • 问 gpt

    • 问 gpt

    • 问 gpt


    image.png
    得到了6种解决方案:



    1. 尾递归优化(Tail Call Optimization)

    2. 循环替换递归( Loop) 将递归转换为等效的循环

    3. 使用堆栈来管理状态(Manual Stack Management) 手动使用数组作为堆栈来保存需要的状态,模拟递归的过程。

    4. 使用异步递归( Async Recursion) 通过把递归调用放在setTimeout、setImmediate或Promise中来异步执行,可以避免同步递归调用造成的堆栈溢出。

    5. 节流递归(Throttling Recursion) 通过定期将递归调用事件推迟到下一个事件循环迭代中,你可以避免堆栈溢出。

    6. Trampoline 函数(Trampoline Function) Trampoline 是一种编程技巧,允许你改写递归函数,使其成为迭代的,而不需要占用新的调用栈帧。


    简单评估下这几个方案:



    • 方案2和3对代码的改动量较大,递归变迭代,实在懒得改也懒得验证,哒咩 ❌

    • 方案4和5把同步逻辑改成了异步,对代码逻辑的侵入性太强,哒咩 ❌

    • 方案1的尾递归优化我现在就在用,无效,哒咩 ❌


    方案一让我感觉撞了鬼:为什么我写的尾递归优化没生效?


    搜了下才发现尾递归优化可谓名存实亡,主流浏览器全都不支持尾递归优化:


    compat-table.github.io/compat-tabl…


    image.png


    详见文章:尾递归的后续探究-腾讯云开发者社区-腾讯云


    解决


    5/6的方案都被否掉,看来看去只能使用 Trampoline 函数,即蹦床函数。


    我们看下 gpt给的示例,以解释蹦床函数做了些什么:


    function trampoline(fn) {
    return function(...args) {
    let result = fn(...args);
    while (typeof result === 'function') {
    result = result();
    }
    return result;
    };
    }

    function sum(x, y) {
    if (y > 0) {
    return () => sum(x + 1, y - 1);
    } else {
    return x;
    }
    }

    let safeSum = trampoline(sum);
    safeSum(1, 100000);

    这里对原函数的修改在第13行,正常的递归会直接执行sum函数,而这段优化里则改成返回了一个函数,我们姑且称之为 handler 函数。


    而 trampoline 函数的作用则是执行 sum 函数并判断返回值,如果返回值是函数(即 handler 函数),则继续执行该函数,直到返回值是数字。整个判断&执行过程使用 while循环。


    蹦床函数之所以能够摆脱递归调用栈限制,是因为 handler 函数是由蹦床函数执行的,handler 函数执行前,它的父函数 sum 函数已经执行完毕了,handler 的执行跟 sum 的执行没有堆栈关系。




    完美,



    • 补充测试用例。

    • 加上try catch防止白屏。

    • 加上memo防止每次render都递归计算。


    开心光速下班 😃。


    作者:tumars
    来源:juejin.cn/post/7330521390510440511
    收起阅读 »

    Vite 4.3 为何性能爆表?

    web
    免责声明 本文属于是语冰的直男翻译了属于是,略有删改,仅供粉丝参考。英文原味版请传送 How we made Vite 4.3 faaaaster。 本期共享的是 —— Vite 4.3 性能大幅提升的幕后技术细节。 地球人都知道,Vite 4.3 相比 V...
    继续阅读 »

    免责声明


    本文属于是语冰的直男翻译了属于是,略有删改,仅供粉丝参考。英文原味版请传送 How we made Vite 4.3 faaaaster



    本期共享的是 —— Vite 4.3 性能大幅提升的幕后技术细节。


    地球人都知道,Vite 4.3 相比 Vite 4.2 取得了惊人的性能提升。


    01-vite.png


    fs.realpathSync 的问题


    Nodejs 中有一个有趣的 realpathSync 问题,它指出 fs.realpathSyncfs.realpathSync.native70 倍。


    但由于在 Windows 上的行为不同,Vite 4.2 只在非 Windows 系统上使用 fs.realpathSync.native。为了搞定此问题,Vite 4.3 在 Windows 上调用 fs.realpathSync.native 时添加了网络驱动验证。


    Vite 从未放弃 Windows,它真的......我哭死。


    JS 优化


    不要错过编程语言优化。Vite 4.3 中若干有趣的 JS 优化案例:


    *yield 重构为回调函数


    Vite 使用 tsconfck 来查找和解析 tsconfig 文件。tsconfck 用于通过 *yield 遍历目标目录,生成器的短板之一在于,它需要更多的内存空间来存储其 Generator 对象,且生成器中存在一大坨生成器上下文切换运行。因此,自 v2.1.1 以来,该核心库用回调函数重构了 *yield


    startsWith/endsWith 重构为 ===


    Vite 4.2 使用 startsWith/endsWith 来检查热门 URL 中的前置和后置 '/'。我们测评了 str.startsWith('x') 和 str[0] === 'x' 的执行基准跑分,发现 ===startsWith 快约 20%。同时 endsWith=== 慢约 60%。


    避免重新创建正则表达式


    Vite 需要一大坨正则表达式来匹配字符串,其中大多数都是静态的,所以最好只使用它们的单例。Vite 4.3 优化了正则表达式,这样可以重复使用它们。


    放弃生成自定义错误


    为了更好的开发体验,Vite 4.2 中存在若干自定义错误。这些错误可能会导致额外的计算和垃圾收集,降低 Vite 的速度。在 Vite 4.3 中,我们不得不放弃生成某些热门的自定义错误(比如 package.json NOT_FOUND 错误),并直接抛出原始错误,获取更好的性能。


    更机智的解析策略


    Vite 会解析所有已接收的 URL 和路径,获取目标模块。


    Vite 4.2 中存在一大坨冗余的解析逻辑和非必要的模块搜索。 Vite 4.3 使解析逻辑更精简、更严格、更准确,减少计算量和 fs 调用。


    更简单的解析


    Vite 4.2 重度依赖 resolve 模块来解析依赖的 package.json,当我们偷看 resolve 模块的源码时,发现在解析 package.json 时存在一大坨无用逻辑。Vite 4.3 弃用了 resolve 模块,遵循更精简的解析逻辑:直接检查嵌套父目录中是否存在 package.json


    更严格的解析


    Vite 需要调用 Nodejs 的 fs API 来查找模块。但 IO 成本昂贵。Vite 4.3 缩小了文件搜索范围,并跳过搜索某些特殊路径,尽量减少 fs 调用。举个栗子:



    1. 由于 # 符号不会出现在 URL 中,且用户可以控制源文件路径中不存在 # 符号,因此 Vite 4.3 不再检查用户源文件中带有 # 符号的路径,而只在 node_modules 中搜索它们。

    2. 在 Unix 系统中,Vite 4.2 首先检查根目录内的每个绝对路径,对于大多数路径而言问题不大,但如果绝对路径以 root 开头,那大概率会失败。为了在 /root/root 不存在时,跳过搜索 /root/root/path-to-file,Vite 4.3 会在开头判断 /root/root 是否作为目录存在,并预缓存结果。

    3. 当 Vite 服务器接收到 @fs/xxx@vite/xxx 时,无需再次解析这些 URL。Vite 4.3 直接返回之前缓存的结果,不再重新解析。


    更准确的解析


    当文件路径为目录时,Vite 4.2 会递归解析模块,这会导致不必要的重复计算。Vite 4.3 将递归解析扁平化,针对不同类型的路径对症下药。拍平后缓存某些 fs 调用也更容易。


    package


    Vite 4.3 打破了解析 node_modules 包数据的性能瓶颈。


    Vite 4.2 使用绝对文件路径作为包数据缓存键。这还不够,因为 Vite 必须在 pkg/foo/barpkg/foo/baz 中遍历相同的目录。


    Vite 4.3 不仅使用绝对路径(/root/node_modules/pkg/foo/bar.js/root/node_modules/pkg/foo/baz.js),还使用遍历的目录(/root/node_modules/pkg/foo/root/node_modules/pkg)作为 pkg 缓存的键。


    另一种情况是,Vite 4.2 在单个函数内查找深度导入路径的 package.json,举个栗子,当 Vite 4.2 解析 a/b/c/d 这样的文件路径时,它首先检查根 a/package.json 是否存在,如果不存在,那就按 a/b/c/package.json -> a/b/package.json 的顺序查找最近的 package.json,但事实上,查找根 package.json 和最近的 package.json 应该分而治之,因为它们需要不同的解析上下文。Vite 4.3 将根 package.json 和最接近的 package.json 的解析分而治之,这样它们就不会混合。


    非阻塞任务


    作为一种按需服务,Vite 开发服务器无需备妥所有东东就能启动。


    非阻塞 tsconfig 解析


    Vite 服务器在预打包 ts/tsx 时需要 tsconfig 的数据。


    Vite 4.2 在服务器启动前,会在 configResolved 插件钩子中等待解析 tsconfig 的数据。一旦服务器启动而尚未备妥 tsconfig 的数据,即使该请求稍后可能需要等待 tsconfig 解析,页面请求也可以访问服务器,


    Vite 4.3 在服务器启动前初始化 tsconfig 解析,但服务器不会等待它。解析过程在后台运行。一旦 ts 相关的请求进来,它就必须等待 tsconfig 解析完成。


    非阻塞文件处理


    Vite 中存在一大坨 fs 调用,其中某些是同步的。这些同步 fs 调用可能会阻塞主线程。Vite 4.3 将其更改为异步。此外,异步函数的并行化也更容易。关于异步函数,我们关心的一件事是,解析后可能需要释放一大坨 Promise 对象。得益于更机智的解析策略,释放 fs - Promise 对象的成本要低得多。


    HMR 防抖


    请考虑两个简单的依赖链 C <- B <- AD <- B <- A,当编辑 A 时,HMR 会将两者从 A 传播到 CD。这导致 AB 在 Vite 4.2 中更新了两次。


    Vite 4.3 会缓存这些遍历过的模块,避免多次探索它们。这可能会对那些具有组件集装导入的文件结构产生重大影响。这对于 git checkout 触发的 HMR 也有好处。


    并行化


    并行化始终是获取更好性能的不错选择。在 Vite 4.3 中,我们并行化了若干核心功能,包括但不限于导入分析、提取 deps 的导出、解析模块 url 和运行批量优化器。并行化之后确实有令人印象深刻的改进。


    基准测试生态系统



    • vite-benchmark:Vite 使用此仓库来测评每个提交的跑分,如果您正在使用 Vite 开发大型项目,我们很乐意测试您的仓库,以获得更全面的性能。

    • vite-plugin-inspect:vite-plugin-inspect 从 v0.7.20 开始支持显示插件的钩子时间,并且将来会有更多的跑分图,请向我们反馈您的需求。

    • vite-plugin-warmup:预热您的 Vite 服务器,并提升页面加载速度!


    《前端 9 点半》每日更新,持续关注,坚持阅读,每天一次,进步一点


    谢谢大家的点赞,掰掰~


    26-cat.gif


    作者:人猫神话
    来源:juejin.cn/post/7331361547011801115
    收起阅读 »

    百度输入法在候选词区域植入广告,网友:真nb!

    web
    V2EX 用户发帖称,百度输入法最新版本在候选词区域植入了广告。具体表现为,如果用户要打 “招商银行” 四个字,当输入 “招商” 之后,候选词的首位是 “★热门加盟店排行” 的链接,点击后会进入名为「加盟星榜单」的广告页面。https://www.v2ex.c...
    继续阅读 »

    V2EX 用户发帖称,百度输入法最新版本在候选词区域植入了广告。

    具体表现为,如果用户要打 “招商银行” 四个字,当输入 “招商” 之后,候选词的首位是 “★热门加盟店排行” 的链接,点击后会进入名为「加盟星榜单」的广告页面。

    https://www.v2ex.com/t/1011440

    别的不说,想出这个功能的产品经理真是个人才,因此评论区有用户感叹道:


    不说用户体验怎么样,不得不说这个键盘的候选词广告想法确实超前,不光超前,还实现了。
    根据输入内容,直接用候选词的方式推送广告,从源头出发拿到用户的一手数据,直接甩掉了各种中间商。速度也更快,更精确的投送。
    可以说是真 nb 呀


    知名科技博主阑夕对此评论道:“你都打出招商两个字了,一定是想加盟店铺做生意吧?逻辑极其通顺智能,对不对?这真的是人类能够企及的创新吗,太牛逼了。


    作者:架构师大咖
    来源:mp.weixin.qq.com/s/0KR2F_a9q2_9JSS8nXtodQ
    收起阅读 »

    从uni-app中去掉编译后微信小程序的滚动条

    web
    首先如果你使用的是页面级滚动,即使uni-app中的pages.json中有相关配置,在编译到小程序中也是没有效果的,因为小程序原生不支持,如下: 那么我们去看微信的官方回复: 所以得出一个结论,要想隐藏滚动条,我们必须使用scroll-view视图组件...
    继续阅读 »

    首先如果你使用的是页面级滚动,即使uni-app中的pages.json中有相关配置,在编译到小程序中也是没有效果的,因为小程序原生不支持,如下:



    那么我们去看微信的官方回复:




    所以得出一个结论,要想隐藏滚动条,我们必须使用scroll-view视图组件


    那么在uni-app页面滚动是不是scroll-view,答案是的,但是我们没办法在顶层设置,因为官方没有暴露相关api,那么要想去掉滚动条,我们就只能在自己的页面使用scroll-view视图组件,取代全局的滚动视图。


    下面上简易代码


    <template>
    <scroll-view scroll-y="true" :show-scrollbar="false" :enhanced="true" class="main">
    <view class="list" v-for="iten in 30">列表{{iten}}</view>
    </scroll-view>
    </template>


    <style lang="scss" scoped>
    .main{
    height: 100vh;
    }
    .list{
    border: 1xp solid black;
    margin: 20rpx auto;
    text-align: center;
    line-height: 100rpx;
    }
    </style>

    效果图:


    初版.gif


    如果你的组件不是占满全屏,比如有头部导航


    这时候有两种做法:


    1.将头部标签放到scroll-view内部,然后固定定位


    <template>
    <scroll-view scroll-y="true" :show-scrollbar="false" :enhanced="true" class="main">
    <view class="nav">导航nav</view>
    <view class="list-container">
    <view class="list" v-for="iten in 30">列表{{iten}}</view>
    </view>
    </scroll-view>
    </template>

    <style lang="scss" scoped>
    .main{
    height: 100vh;
    }
    .list-container{
    margin-top: 200rpx;
    }
    .list{
    border: 1xp solid black;
    margin: 20rpx auto;
    text-align: center;
    line-height: 100rpx;
    }
    .nav{
    position: fixed;
    top: 0;
    line-height: 200rpx;
    padding-top: 20rpx;
    width: 100vw;
    text-align: center;
    border: 1px solid black;
    background-color: #fff;
    }
    </style>

    效果图:


    230187154229138168229133168229177143.gif


    2.将scroll-view的高度设置为视口余下高度


    这里注意一下在移动端尽量较少的使用cale()计算高度


    所以这里我们使用flex布局


    <template>
    <view class="content">
    <view class="nav">导航nav</view>
    <scroll-view scroll-y="true" :show-scrollbar="false" :enhanced="true" class="main">
    <view class="list" v-for="iten in 30">列表{{iten}}</view>
    </scroll-view>
    </view>
    </template>

    <style lang="scss" scoped>
    .content{
    height: 100vh;
    width: 100vw;
    overflow: hidden;
    display: flex;
    flex-direction: column;
    }
    .main{
    flex-grow: 1;
    }
    .list{
    border: 1xp solid black;
    margin: 20rpx auto;
    text-align: center;
    line-height: 100rpx;
    }
    .nav{
    height: 200rpx;
    line-height: 200rpx;
    width: 100vw;
    text-align: center;
    border: 1px solid black;
    background-color: #fff;
    }
    </style>

    效果图:


    230187154229138168229133168229177143.gif


    如果有帮助到你的话,记得点个赞哦!


    猫咪.gif


    作者:aways
    来源:juejin.cn/post/7330655456883654667
    收起阅读 »

    解锁 JSON.stringify() 7 个鲜为人知的坑

    web
    在本文中,我们将探讨与JSON.stringify()相关的各种坑。 1. 处理undefined、Function和Symbol值 在前端中 undefined、Function和Symbol值不是有效的JSON值。在转换过程中遇到它们时,它们会被省略(在对...
    继续阅读 »

    在本文中,我们将探讨与JSON.stringify()相关的各种坑。


    1. 处理undefined、Function和Symbol值


    在前端中 undefinedFunctionSymbol值不是有效的JSON值。在转换过程中遇到它们时,它们会被省略(在对象中),或者被更改为null(在数组中)。


    例如:

    const obj = { foo: function() {}, bar: undefined, baz: Symbol('example') };  
    const jsonString = JSON.stringify(obj);
    console.log(jsonString); // 输出: '{}'

    const obj2 = {arr: [function(){}]};
    console.log(JSON.stringify(obj2)); // 输出: {"arr":[null]}

    2. 布尔、数字和字符串对象


    布尔、数字和字符串对象在字符串化过程中会被转换为它们对应的原始值。

    const boolObj = new Boolean(true);  
    const jsonString = JSON.stringify(boolObj);
    console.log(jsonString); // 输出: 'true'

    3. 忽略Symbol键的属性


    Symbol键属性在字符串化过程中完全被忽略,即使使用替换函数也是如此。这意味着与Symbol键关联的任何数据都将在生成的JSON字符串中被排除。

    const obj = { [Symbol('example')]: 'value' };  
    const jsonString = JSON.stringify(obj);
    console.log(jsonString); // 输出: '{}'

    const obj2 = {[Symbol('example')]: [function(){}]};
    console.log(JSON.stringify(obj2)); // 输出 '{}'

    4. 处理无穷大(Infinity)、NaN和Null值


    Infinity、NaN 和 null 值在字符串化过程中都被视为 null。

    const obj = { value: Infinity, error: NaN, nothing: null };  
    const jsonString = JSON.stringify(obj);
    console.log(jsonString); // 输出: '{"value":null,"error":null,"nothing":null}'

    5. Date对象被视为字符串


    Date实例通过实现toJSON()函数来返回一个字符串(与date.toISOString()相同),因此在字符串化过程中被视为字符串。

    const dateObj = new Date();
    const jsonString = JSON.stringify(dateObj);
    console.log(jsonString); // 输出:"2024-01-31T09:42:00.179Z"

    6. 循环引用异常


    如果 JSON.stringify() 遇到具有循环引用的对象,它会抛出一个错误。循环引用发生在一个对象在循环中引用自身的情况下。

    const circularObj = { self: null };
    circularObj.self = circularObj;
    JSON.stringify(circularObj); // Uncaught TypeError: Converting circular structure to JSON

    7. BigInt转换错误


    使用JSON.stringify()转换BigInt类型的值时引发错误。

    const bigIntValue = BigInt(42);  
    JSON.stringify(bigIntValue); // Uncaught TypeError: Do not know how to serialize a BigInt

    各位同学如果在开发中还遇到过不一样的坑,还请评论区补充互相讨论


    作者:StriveToY
    来源:juejin.cn/post/7330289404731047936
    收起阅读 »

    分享:一个超实用的文字截断技巧

    web
    文字截断是 Web 开发中非常普遍的一个需求,原因无他,很多时候我们无法确定展示文本的容器的宽度,如果不使用文字截断,要么文本被生硬的截断隐藏、要么文本破坏了预期中的布局。 Tailwind CSS 提供的文字截断的原子类: .truncate { over...
    继续阅读 »

    文字截断是 Web 开发中非常普遍的一个需求,原因无他,很多时候我们无法确定展示文本的容器的宽度,如果不使用文字截断,要么文本被生硬的截断隐藏、要么文本破坏了预期中的布局。


    Tailwind CSS 提供的文字截断的原子类:


    .truncate {
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
    }

    这 3 组 CSS 规则共同作用才可实现用 ... 截断文字的效果,缺一不可。



    • overflow: hidden 表示容器空间不足时内容应该隐藏,而非默认的 visible 完全展示

    • white-space: nowrap 表示文本容器宽度不足时,不能换行,其默认值为 normal,该行为不太好预测,但大部分情况下,它等同于 wrap 即文本尽可能的换行

    • text-overflow: ellipsis 指定文本省略使用 ... ,该属性默认值为 clip ,表示文本直接截断什么也不显示,这样展示容易对用户造成误解,因此使用 ... 更合适


    接下来介绍一个在 PC Web 上很实用的交互效果:在需要截断的文本后面,紧跟一个鼠标 hover 上去才会展示的按钮, 执行一些和省略文本强相关、轻操作的动作。


    Untitled.gif


    如图所示,鼠标 hover 表示的按钮可以用来快速的编辑「标题」。下面介绍一下它的纯 CSS 实现。


    首先,我们来实现一个基础版的。


    Untitled 1.gif


    代码:


    <div class="container">
    <p class="complex-line truncate">
    <span class="truncate">海和天相连成为海岸线</span>
    <span class="icon">❤️</span>
    </p>
    <p class="truncate">鱼和水相濡以沫的世界</p>
    </div>

    <style>
    .truncate {
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
    }
    .container {
    max-width: 100px;
    }
    .complex-line {
    display: flex;
    }
    .complex-line:hover .icon {
    display: block;
    }
    .icon {
    display: none;
    }
    </style>

    一些重点:



    • 容器 .container 必须是宽度固定、或者最大宽度固定的,以此确保容器不会因为子元素无止境的扩充。比如你可以设置容器的 widthmax-width 。在某些情况下,即使设置了 max-width 但不生效,此时可以尝试添加 overflow: hidden

    • 含有按钮的行 .complex-line 和子元素中展示文字的标签( .complex-line 下的 span.truncate),都要添加文字截断类 .truncate

    • 按钮 .icon 默认不展示,hover 当前行的时候才展示,这个也很常规,主要通过设置其 display 属性实现。

    • 🌟 接下来一步很巧妙,就是为含有按钮的行 .complex-line ,设置其为 Flex 容器,这主要是借助 Flex Item 的 flex-shrink 属性默认值为 1 的特性,允许含有文字的 Flex Item span.truncate 在按钮 span.icon 需要展示的时候进一步缩放,腾出空间,用于完整展示按钮。


    这样便实现了基础版的文字截断 + 可以自动显示隐藏按钮的交互。


    接下来再看看文章开头提到的这个交互:


    Untitled.gif


    它和基础版的样式最大的不同就是它是多列的,这里可以使用 Grid 网格布局实现。


    <div class="container">
    <label>标题:</label>
    <p class="complex-line truncate">
    <span class="truncate">高质量人才生产总基地</span>
    <span class="icon">✏️</span
    </p>
    <label>编号:</label>
    <p class="truncate">No.9781498128959</p>
    </div>

    <style>
    .container {
    display: grid;
    grid-template-columns: auto 1fr;
    /** ... */
    }
    </style>

    其他样式代码和基础版中的一致,不再赘述。


    总结


    为了实现本文介绍的这种交互,应该确保:



    • 容器的宽度或最大宽度应该是固定的

    • 按钮前面的那个展示文字的元素,和它们的父元素,都应该有 .truncate 文字截断效果

    • 按钮的父元素应该为 Flex 容器,使得按钮显示时,文字所在标签可以进一步缩放


    作者:人间观察员
    来源:juejin.cn/post/7330464865315094554
    收起阅读 »

    消息通知文字竖向无缝轮播组件实现历程

    web
    背景 最近有个需求需要做一个无缝轮播的消息通知,并且需要抽离成通用组件,记录下实现这个组件的历程。 先看效果 实现过程 思考(part-1) 因为刚开始给的设计稿是没有动画效果的,我刚开始想的效果是只有红色加粗的文字轮播,其他文字不变;然后想着看下有没...
    继续阅读 »

    背景



    最近有个需求需要做一个无缝轮播的消息通知,并且需要抽离成通用组件,记录下实现这个组件的历程。



    先看效果
    noticeBar.gif


    实现过程


    思考(part-1)



    因为刚开始给的设计稿是没有动画效果的,我刚开始想的效果是只有红色加粗的文字轮播,其他文字不变;然后想着看下有没有已经实现好的轮子,找到一个 js 库:react-text-loop;但是经过我的考虑,如果只是部分文字滚动,红色加粗的文字可能宽度不一样,会导致其他文字换位,所以还是想着整条文字滚动会比较好。



    使用 antd-m Swiper 实现(part-2)



    想到这个滚动效果在移动端应该很常见,antd-m 应该可能会有组件吧,去瞧瞧👀;noticeBar 组件只有横向滚动文字,没有竖直滚动的。既然没有,那就用其他方式实现吧,最简单的就是用 Swiper 组件设置成 竖向滚动,因为我负责的项目基本都用 antd-m,所以就用 antd-m 的 Swiper 来实现了。



    实现过程遇到的问题


    antd-m 的 Swiper 组件竖向滚动必须指定高度

    在使用 antd-m 的 Swiper 组件竖向滚动的方式好像有问题,但是看文档的使用又是正常,结果发现竖向滚动需要指定高度,所以文档还是要仔细看清楚: Swiper 竖向文档


    依赖冲突问题

    在自己仓库使用很正常,一点问题都没有;然后打算抽离到我们项目的组件库中,然后在把项目中使用替换成组件库的包,过程很顺畅;过了段时间另一个项目要使用我们的组件,然后我就把包发给他,结果他说他项目里用不了,会报错。


    然后 clone 他的项目试了下,果然是有问题的,因为他们项目里用的是 antd-m 2.x,2.x 没有 Swiper 组件,而我的组件库依赖的是 antd-m 5.x,看了下他们仓库用的是antd-m 2.x 和 5.x 共存的方式,可以看一下这个 antd-m 迁移指南,如果要两个版本共存且之前用的是组件按需导入,那么组件按需导入的配置也会有问题,因为两个版本的文件差异比较大,所以需要改一下按需导入的配置:


    module.exports = {
    "plugins": [
    // 原配置
    // [
    // 'import',
    // {
    // libraryName: 'antd-mobile',
    // libraryDirectory: 'lib',
    // style: 'css',
    // },
    // 'antd-mobile',
    // ]

    // 修改为
    [
    'import',
    {
    libraryName: 'antd-mobile',
    customName: (name, file) => {
    const { filename } = file.opts;
    if (filename.includes('/es/')) {
    return `antd-mobile/es/components/${name}`;
    }
    return `antd-mobile/lib/${name}`;
    },
    style: (name, file) => {
    const { filename } = file.opts;
    if (filename.includes('/es/')) {
    // 如果项目已经有 global 文件,return false 即可
    // 如果没有,这样配置后就不需要手动引入 global 文件
    return 'antd-mobile/es/global';
    }
    return `${name}/style/css`;
    },
    },
    'antd-mobile',
    ]
    ]
    }

    想彻底解决这个依赖冲突问题


    其实修改完配置之后使用就正常了,但是我考虑到如果之后想使用这个组件,如果 antd-m 版本不是 5.x,那么有一个项目就要改一个配置,很烦人;而且 antd-m Swiper 竖向需要指定高度,如果都需要指定高度了,那么我直接实现一个滚动动画应该也很简单吧,说干就干。



    自己手写一个轮播组件(pard-3)



    手动实现轮播还是比较简单的,只不过无缝轮播那里需要处理下,传统的方式都是通过在轮播 item 首尾复制 item 插入,当轮播到最后一个,用复制的 item 来承接,之后在回归原位正常滚动。



    手写实现思路



    1. 传入轮播容器的高度,使用 transform: translate3d(0px, ${容器高度}, 0px); 每次移动列表 translateY 的距离为容器的高度。

    2. 处理无缝轮播,因为这个组件没有手动滑动轮播,自由自动向下轮播,所以不需要考虑反方向的轮播处理;当轮播到最后一个 item,那么就将第一个 item transform: translateY(${轮播列表高度}),这时候第一个就在最后一个下面,监听轮播列表 onTransitionEnd,判断当前是否轮播到第一个,是的话就将轮播列表的 translateY 的距离归 0。


    最终实现代码



    其实最后是封装成了一个 react 组件,但是掘金上为了大家看到更好的效果,用原生的方式简单写了下。如果需要封装成 react/vue 组件参考下方代码应该就够了。



    容器未 hidden 效果



    组件封装的设计



    这里放一下我封装组件设计的 props



    interface IProps {
    /** 轮播通知 list */
    list: any[];
    /** noticebar 显隐控制 */
    visible?: boolean;
    /** 单个轮播 item 的高度, 传入 750 设计稿的数字,会转成 vw */
    swiperHeight?: number;
    /** 每条通知轮播切换的间隔 */
    interval?: number;
    /** 轮播动画的持续时间 */
    animationDuration?: number;
    /** 是否展示关闭按钮 */
    closeable?: boolean;
    /** 关闭按钮点击的回调 */
    onClose?: () => void;
    /** 自定义轮播 item 的内容 */
    renderItem?: (item: any) => React.ReactNode;
    /** notice 的标题 */
    noticeTitle?: ReactNode;
    /** notice 右边自定义 icon */
    rightIcon?: ReactNode;
    /** 是否展示 notice 左边 icon */
    showLeftIcon?: boolean;
    /** notice 左边自定义 icon */
    leftIcon?: ReactNode;
    /** 自定义类名 */
    className?: string;
    }

    作者:wait
    来源:juejin.cn/post/7330054489079169065
    收起阅读 »

    Vue 依赖注入:一种高效的数据共享方法

    web
    什么是vue依赖注入? Vue是一个用于构建用户界面的渐进式框架。 它提供了一种简单而灵活的方式来管理组件之间的数据流,即依赖注入(Dependency Injection,DI)。 依赖注入是一种设计模式,它允许一个组件从另一个组件获取它所依赖的数据...
    继续阅读 »

    什么是vue依赖注入?



    Vue是一个用于构建用户界面的渐进式框架。




    它提供了一种简单而灵活的方式来管理组件之间的数据流,即依赖注入(Dependency Injection,DI)。



    依赖注入是一种设计模式,它允许一个组件从另一个组件获取它所依赖的数据或服务,而不需要自己创建或管理它们。这样可以降低组件之间的耦合度,提高代码的可维护性和可测试性。


    依赖注入示意图


    在这里插入图片描述

    provide和inject



    在Vue中,依赖注入的方式是通过provide和inject两个选项来实现的。




    provide选项允许一个祖先组件向下提供数据或服务给它的所有后代组件。
    inject选项允许一个后代组件接收来自祖先组件的数据或服务。
    这两个选项都可以是一个对象或一个函数,对象的键是提供或接收的数据或服务的名称,值是对应的数据或服务。函数的返回值是一个对象,具有相同的格式。



    下面是一个简单的例子,演示了如何使用依赖注入的方式共享数据:


    父组件


    <template>
    <div>
    <h1>我是祖先组件</h1>
    <child-component></child-component>
    </div>
    </template>

    <script>
    import ChildComponent from './ChildComponent.vue'

    export default {
    name: 'AncestorComponent',
    components: {
    ChildComponent
    },
    // 提供一个名为message的数据
    provide: {
    message: 'Hello from ancestor'
    }
    }
    </script>

    子组件


    <template>
    <div>
    <h2>我是后代组件</h2>
    <p>{{ message }}</p>
    </div>
    </template>


    // 后代组件
    <script>
    export default {
    name: 'ChildComponent',
    // 接收一个名为message的数据
    inject: ['message']
    }
    </script>

    这样,后代组件就可以直接使用祖先组件提供的数据,而不需要通过props或事件来传递。


    需要注意的是,依赖注入的数据是不可响应的,也就是说,如果祖先组件修改了提供的数据,后代组件不会自动更新。
    如果需要实现响应性,可以使用一个响应式的对象或者一个返回响应式对象的函数作为provide的值。


    实现响应式依赖注入的几种方式


    一、提供响应式数据



    方法是在提供者组件中使用ref或reactive创建响应式数据,然后通过provide提供给后代组件。后代组件通过inject接收后,就可以响应数据的变化。



    提供者:


    <template>
    <div>
    <h1>我是提供者组件</h1>
    <button @click="count++">增加计数</button>
    <child-component></child-component>
    </div>

    </template>

    <script>
    import ChildComponent from './ChildComponent.vue'
    import { ref, provide } from 'vue'

    export default {
    name: 'ProviderComponent',
    components: {
    ChildComponent
    },
    setup() {
    // 使用ref创建一个响应式的计数器
    const count = ref(0)
    // 提供给后代组件
    provide('count', count)
    return {
    count
    }
    }
    }
    </script>


    接收者:


    <template>
    <div>
    <h2>我是接收者组件</h2>
    <p>计数器的值是:{{ count }}</p>
    </div>

    </template>

    <script>
    import { inject } from 'vue'

    export default {
    name: 'ChildComponent',
    setup() {
    // 接收提供者组件提供的响应式对象
    const count = inject('count')
    return {
    count
    }
    }
    }
    </script>


    二、提供修改数据的方法



    提供者组件可以提供修改数据的方法函数,接收者组件调用该方法来更改数据,而不是直接修改注入的数据。



    提供者:


    <template>
    <div>
    <h1>我是提供者组件</h1>
    <p>消息是:{{ message }}</p>
    <child-component></child-component>
    </div>

    </template>

    <script>
    import ChildComponent from './ChildComponent.vue'
    import { ref, provide } from 'vue'

    export default {
    name: 'ProviderComponent',
    components: {
    ChildComponent
    },
    setup() {
    // 使用ref创建一个响应式的消息
    const message = ref('Hello')
    // 定义一个更改消息的方法
    function updateMessage() {
    message.value = 'Bye'
    }
    // 提供给后代组件
    provide('message', { message, updateMessage })
    return {
    message
    }
    }
    }
    </script>


    接收者:


    <template>
    <div>
    <h2>我是接收者组件</h2>
    <p>消息是:{{ message }}</p>
    <button @click="updateMessage">更改消息</button>
    </div>

    </template>

    <script>
    import { inject } from 'vue'

    export default {
    name: 'ChildComponent',
    setup() {
    // 接收提供者组件提供的响应式对象和方法
    const { message, updateMessage } = inject('message')
    return {
    message,
    updateMessage
    }
    }
    }
    </script>


    三、使用readonly包装



    通过readonly包装provide的数据,可以防止接收者组件修改数据,保证数据流的一致性。



    提供者:


    <template>
    <div>
    <h1>我是提供者组件</h1>
    <p>姓名是:{{ name }}</p>
    <child-component></child-component>
    </div>

    </template>

    <script>
    import ChildComponent from './ChildComponent.vue'
    import { ref, provide, readonly } from 'vue'

    export default {
    name: 'ProviderComponent',
    components: {
    ChildComponent
    },
    setup() {
    // 使用ref创建一个响应式的姓名
    const name = ref('Alice')
    // 使用readonly包装提供的值,使其不可修改
    provide('name', readonly(name))
    return {
    name
    }
    }
    }
    </script>


    接收者:


    <template>
    <div>
    <h2>我是接收者组件</h2>
    <p>姓名是:{{ name }}</p>
    <button @click="name = 'Bob'">尝试修改姓名</button>
    </div>

    </template>

    <script>
    import { inject } from 'vue'

    export default {
    name: 'ChildComponent',
    setup() {
    // 接收提供者组件提供的只读对象
    const name = inject('name')
    return {
    name
    }
    }
    }
    </script>


    四、使用<script setup>



    <script setup>组合式写法下,provide和inject默认就是响应式的,无需额外处理。



    总结



    依赖注入的方式共享数据在Vue中是一种高级特性,它主要用于开发插件或库,或者处理一些特殊的场景。



    作者:Yoo前端
    来源:juejin.cn/post/7329830481722294272
    收起阅读 »

    你还在使用websocket实现实时消息推送吗?

    web
    前言 在日常的开发中,我们经常能碰见服务端需要主动推送给客户端数据的业务场景,比如数据大屏的实时数据,比如消息中心的未读消息,比如聊天功能等等。 本文主要介绍SSE的使用场景和如何使用SSE。 服务端向客户端推送数据的实现方案有哪几种? 我们常规实现这些需求...
    继续阅读 »

    前言


    在日常的开发中,我们经常能碰见服务端需要主动推送给客户端数据的业务场景,比如数据大屏的实时数据,比如消息中心的未读消息,比如聊天功能等等。


    本文主要介绍SSE的使用场景和如何使用SSE。


    image.png


    服务端向客户端推送数据的实现方案有哪几种?


    我们常规实现这些需求的方案有以下三种



    1. 轮询

    2. websocket

    3. SSE


    轮询简介


    在很久很久以前,前端一般使用轮询来进行服务端向客户端进行消息的伪推送,为什么说轮询是伪推送?因为轮询本质上还是通过客户端向服务端发起一个单项传输的请求,服务端对这个请求做出响应而已。通过不断的请求来实现服务端向客户端推送数据的错觉。并不是服务端主动向客户端推送数据。显然,轮询一定是上述三个方法里最下策的决定。


    轮询的缺点:



    1. 首先轮询需要不断的发起请求,每一个请求都需要经过http建立连接的流程(比如三次握手,四次挥手),是没有必要的消耗。

    2. 客户端需要从页面被打开的那一刻开始就一直处理请求。虽然每次轮询的消耗不大,但是一直处理请求对于客户端来说一定是不友好的。

    3. 浏览器请求并发是有限制的。比如Chrome 最大并发请求数目为 6,这个限制还有一个前提是针对同一域名的,超过这一限制的后续请求将会被阻塞。而轮询意味着会有一个请求长时间的占用并发名额

    4. 而如果轮询时间较长,可能又没有办法非常及时的获取数据


    websocket简介


    websocket是一个双向通讯的协议,他的优点是,可以同时支持客户端和服务端彼此相互进行通讯。功能上很强大。


    缺点也很明显,websocket是一个新的协议,ws/wss。也就是说,支持http协议的浏览器不一定支持ws协议。


    相较于SSE来说,websocket因为功能更强大。结构更复杂。所以相对比较


    websocket对于各大浏览器的兼容性↓
    image.png


    SSE简介


    sse是一个单向通讯的协议也是一个长链接,它只能支持服务端主动向客户端推送数据,但是无法让客户端向服务端推送消息。


    长链接是一种HTTP/1.1的持久连接技术,它允许客户端和服务器在一次TCP连接上进行多个HTTP请求和响应,而不必为每个请求/响应建立和断开一个新的连接。长连接有助于减少服务器的负载和提高性能。

    SSE的优点是,它是一个轻量级的协议,相对于websockte来说,他的复杂度就没有那么高,相对于客户端的消耗也比较少。而且SSE使用的是http协议(websocket使用的是ws协议),也就是现有的服务端都支持SSE,无需像websocket一样需要服务端提供额外的支持。


    注意:IE大魔王不支持SSE


    SSE对于各大浏览器的兼容性↓
    image.png


    注意哦,上图是SSE对于浏览器的兼容不是对于服务端的兼容。


    websocket和SSE有什么区别?


    轮询


    对于当前计算机的发展来说,几乎很少出现同时不支持websocket和sse的情况,所以轮询是在极端情况下浏览器实在是不支持websocket和see的下策。


    Websocket和SSE


    我们一般的服务端和客户端的通讯基本上使用这两个方案。首先声明:这两个方案没有绝对的好坏,只有在不同的业务场景下更好的选择。


    SSE的官方对于SSE和Websocket的评价是



    1. WebSocket是全双工通道,可以双向通信,功能更强;SSE是单向通道,只能服务器向浏览器端发送。

    2. WebSocket是一个新的协议,需要服务器端支持;SSE则是部署在HTTP协议之上的,现有的服务器软件都支持。

    3. SSE是一个轻量级协议,相对简单;WebSocket是一种较重的协议,相对复杂。

    4. SSE默认支持断线重连,WebSocket则需要额外部署。

    5. SSE支持自定义发送的数据类型。


    Websocket和SSE分别适用于什么业务场景?


    对于SSE来说,它的优点就是轻,而且对于服务端的支持度要更好。换言之,可以使用SSE完成的功能需求,没有必要使用更重更复杂的websocket。


    比如:数据大屏的实时数据,消息中心的消息推送等一系列只需要服务端单方面推送而不需要客户端同时进行反馈的需求,SSE就是不二之选。


    对于Websocket来说,他的优点就是可以同时支持客户端和服务端的双向通讯。所适用的业务场景:最典型的就是聊天功能。这种服务端需要主动向客户端推送信息,并且客户端也有向服务端推送消息的需求时,Websocket就是更好的选择。


    SSE有哪些主要的API?


    建立一个SSE链接 :var source = new EventSource(url);

    SSE连接状态


    source.readyState



    • 0,相当于常量EventSource.CONNECTING,表示连接还未建立,或者连接断线。

    • 1,相当于常量EventSource.OPEN,表示连接已经建立,可以接受数据。

    • 2,相当于常量EventSource.CLOSED,表示连接已断,且不会重连。


    SSE相关事件



    • open事件(连接一旦建立,就会触发open事件,可以定义相应的回调函数)

    • message事件(收到数据就会触发message事件)

    • error事件(如果发生通信错误(比如连接中断),就会触发error事件)


    数据格式


    Content-Type: text/event-stream //文本返回格式
    Cache-Control: no-cache //不要缓存
    Connection: keep-alive //长链接标识

    image.png


    SSE:相关文档,文档入口文档入口文档入口文档入口


    显然,如果直接看api介绍不论是看这里还是看官网,大部分同学都是比较懵圈的状态,那么我们写个demo来看一下?


    image.png


    demo请看下方


    我更建议您先把Demo跑起来,然后在看看上面这个w3cschool的SSE文档。两个配合一起看,会更方便理解些。


    image.png


    如何实操一个SSE链接?Demo↓


    这里Demo前端使用的就是最基本的html静态页面连接,没有使用任何框架。
    后端选用语言是node,框架是Express。


    理论上,把这两段端代码复制过去跑起来就直接可以用了。



    1. 第一步,建立一个index.html文件,然后复制前端代码Demo到index.html文件中,打开文件

    2. 第二步,进入一个新的文件夹,建立一个index.js文件,然后将后端Demo代码复制进去,然后在该文件夹下执行


    npm init          //初始化npm       
    npm i express //下载node express框架
    node index //启动服务

    image.png


    在这一层文件夹下执行命令。


    完成以上操作就可以把项目跑起来了


    前端代码Demo


    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    </head>
    <body>
    <ul id="ul">

    </ul>
    </body>
    <script>

    //生成li元素
    function createLi(data){
    let li = document.createElement("li");
    li.innerHTML = String(data.message);
    return li;
    }

    //判断当前浏览器是否支持SSE
    let source = ''
    if (!!window.EventSource) {
    source = new EventSource('http://localhost:8088/sse/');
    }else{
    throw new Error("当前浏览器不支持SSE")
    }

    //对于建立链接的监听
    source.onopen = function(event) {
    console.log(source.readyState);
    console.log("长连接打开");
    };

    //对服务端消息的监听
    source.onmessage = function(event) {
    console.log(JSON.parse(event.data));
    console.log("收到长连接信息");
    let li = createLi(JSON.parse(event.data));
    document.getElementById("ul").appendChild(li)
    };

    //对断开链接的监听
    source.onerror = function(event) {
    console.log(source.readyState);
    console.log("长连接中断");
    };

    </script>
    </html>

    后端代码Demo(node的express)


    const express = require('express'); //引用框架
    const app = express(); //创建服务
    const port = 8088; //项目启动端口

    //设置跨域访问
    app.all("*", function(req, res, next) {
    //设置允许跨域的域名,*代表允许任意域名跨域
    res.header("Access-Control-Allow-Origin", '*');
    //允许的header类型
    res.header("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With");
    //跨域允许的请求方式
    res.header("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS");
    // 可以带cookies
    res.header("Access-Control-Allow-Credentials", true);
    if (req.method == 'OPTIONS') {
    res.sendStatus(200);
    } else {
    next();
    }
    })

    app.get("/sse",(req,res) => {
    res.set({
    'Content-Type': 'text/event-stream', //设定数据类型
    'Cache-Control': 'no-cache',// 长链接拒绝缓存
    'Connection': 'keep-alive' //设置长链接
    });

    console.log("进入到长连接了")
    //持续返回数据
    setInterval(() => {
    console.log("正在持续返回数据中ing")
    const data = {
    message: `Current time is ${new Date().toLocaleTimeString()}`
    };
    res.write(`data: ${JSON.stringify(data)}\n\n`);
    }, 1000);
    })

    //创建项目
    app.listen(port, () => {
    console.log(`项目启动成功-http://localhost:${port}`)
    })

    效果


    动画3.gif


    总结



    1. SSE比websocket更轻

    2. SSE是基于http/https协议的

    3. websocket是一个新的协议,ws/wss协议

    4. 如果只需要服务端向客户端推送消息,推荐使用SSE

    5. 如果需要服务端和客户端双向推送,请选择websocket

    6. 不论是SSE还是websocket,对于浏览器的兼容性都不错

    7. 轮询是下策,很占用客户端资源,不建议使用。(不过偷懒的时候他确实方便)

    8. IE不支持SSE

    9. 小白同学demo如果跑不明白可以私信我


    对了,小程序不支持SSE哦


    image.png


    最后


    如果文章对您有帮助的话。


    image.png


    作者:工边页字
    来源:juejin.cn/post/7325730345840066612
    收起阅读 »

    实现一个鼠标框选的功能,要怎么实现和设计 api?

    web
    前言 前两年在一家做电商的公司做了一个需求:鼠标框选商品卡片,开始拖拽的时候合成一个然后改变位置,页面上有几千个所以还要结合虚拟列表。当时不知道怎么做,就在 github 上到处找现成的库,最后找到了 react-selectable-fast,并结合 rea...
    继续阅读 »

    285330798-9d463acf-c56b-48d8-b7d5-2dc02b4257e0.gif


    前言


    前两年在一家做电商的公司做了一个需求:鼠标框选商品卡片,开始拖拽的时候合成一个然后改变位置,页面上有几千个所以还要结合虚拟列表。当时不知道怎么做,就在 github 上到处找现成的库,最后找到了 react-selectable-fast,并结合 react-virtualizedreact-sortable-hoc 完成了需求。虽然该库已经很久不维护了,但大致上能满足我的需求了,尽管它是以 dom 的方式,很不 react,但秉承着能用就行的原则。意料之中,开发过程中遇到了 bug,最后只能 fork 一份修改源码后自己发了个 npm 包来使用。


    项目介绍


    前几个月在空闲时间突然来了兴致,自己找点事做,就想自己开发一个框选的库吧,万一也有人有这个需求不知道怎么办呢?写完后发到了 antd 的社区共建群里,有的人觉得不错也 star 了。先献上项目地址 react-selectable-box,文档完整,使用 dumi 编写。api 友好,支持自定义一些功能。


    api 设计


    一个组件在设计时,首先思考的应该是 api 如何去设计,最好符合大家平常的习惯,并具有一定的自定义和拓展能力。再加上了解 react-selectable-fast 这个库的缺点和痛点,对我的设计就更加有帮助了。大家在看下面的文章之前也可以思考一下,如果是你,你会怎么设计?这里只选取几个 api 来进行介绍。


    主组件 Selectable


    选中的值


    defaultValuevalue,类型为 any[],每个方块一般都有一个唯一 id 来标识,2024-1-31 更新后 后支持任意类型,因为考虑到很多情况你可能需要一个对象或数组来标识,文章后面提供了 compareFn 来自定义比较值相等。


    禁用


    disabled,大部分有值的组件应该都会有此属性,能直接禁用框选功能。


    模式


    mode,类型为 "add" | "remove" | "reverse"。模式,表明当前框选是增加、减少还是取反。这个 api 感觉是设计的最好的,用户会框选来选择目标,肯定也会需要删除已经框选的目标,可能是按住 shift 来删除等等之类的操作。用户可以自己编写自定义逻辑来修改 mode 的值来控制不同的行为,反观 react-selectable-fast,则是提供了 deselectOnEscallowAltClickallowCtrlClickallowMetaClickallowShiftClick 等多个 api。


    开始框选的条件


    selectStartRange,类型 "all" | "inside" | "outside",鼠标从方块内部还是外部可以开始框选,或都可以。


    可以进行框选的容器


    dragContainer,类型 () => HTMLElement,例如你只希望某个卡片内才可以进行框选,不希望整个页面都可以进行框选,这个 api 就会起到作用了。


    滚动的容器


    scrollContainer,类型 () => HTMLElement,如果你的这些方块是在某个容器中并且可滚动,就需要传入这个属性,就可以在滚动的容器中进行框选操作了。


    框选矩形的 style 与 className


    boxStyleboxClassName,使用者可以自定义颜色等一些样式。


    自定义 value 比较函数


    compareFn,类型 (a: any, b: any) => boolean,默认使用 === 进行比较(因为 value 支持任意类型,比如你使用了对象或数组类型,所以你可能需要自定义比较)


    框选开始事件


    onStart,框选开始时,使用者可能需要做一些事情。


    框选结束事件


    onEnd,类型为 (selectingValue: (string | number)[], { added: (string | number)[], removed: (string | number)[] }) => voidselectingValue 为本次框选的值,added 为本次增加的值,removed 为本次删除的值。例如你想在每次框选后覆盖之前的操作,直接设置 selectingValue 成 value 即可。如果你想每次框选都是累加,加上 added 的值即可,这里就不再说明了。


    方块可选 - useSelectable


    怎么让方块可以被选择呢?并且一一绑定上对应的值?react-selectable-fast 则是提供 clickableClassName api,传入可以被选择的目标的 class,这种方式太不 react 了。此时我的脑海里想到了 dnd-kit,我认为是 react 最好用的拖拽库,它是怎么让每个方块可以被拖拽的呢?优秀的东西应该借鉴,于是就有了 useSelectable


    const { 
    setNodeRef, // 设置可框选元素
    isSelected, // 是否已经选中
    isAdding, // 当前是否正在添加
    isRemoving, // 当前是否正在删除
    isSelecting, // 当前是否被框选
    isDragging // 是否正在进行框选操作
    } = useSelectable({
    value, // 每个元素的唯一值,支持任意类型
    disabled, // 这个元素是否禁用
    rule, // "collision" | "inclusion" | Function,碰撞规则,碰到就算选中还是全部包裹住才算选中,也可以自定义
    });

    如何使用?


    const Item = ({ value }: { value: number }) => {
    const { setNodeRef, isSelected, isAdding } = useSelectable({
    value,
    });

    return (
    <div
    ref={setNodeRef}
    style={{
    width: 50,
    height: 50,
    borderRadius: 4,
    border: isAdding ? '1px solid #1677ff' : undefined,
    background: isSelected ? '#1677ff' : '#ccc',
    }}
    />

    );
    };

    实现


    这里只简单讲一下思路,有兴趣的同学可以直接前往源码进行阅读。


    主组件 Selectable 相当于一个 context,一些状态在这里进行保存,并掌管每个 useSelectable,将其需要的值通过 context 传递过去。


    在设置的可被框选的容器内监听鼠标 mousedown 事件,记录其坐标,根据 mousemove 画出框选矩形,再根据 setNodeRef 收集的元素和框选矩形根据碰撞检测函数计算出是否被框选了,并将值更新到 Selectable 中去,最后在 mouseup 时触发 onEnd,将值处理完之后并丢出去。


    演示


    这里演示一下文章开头所说的框选拖拽功能,配合 dnd-kit 实现,代码在文档的 example 中。
    录屏2024-01-23 19.27.43.gif


    遇到的坑


    这里分享一下遇到的坑的其中之一:框选的过程中会选中文字,很影响体验,怎么让这些文字不能被框选呢?


    方案1: 用 user-select: none 来控制文本不可被选中,但是这是在用户侧来做,比较麻烦。并且发现在 chrome 下设置此属性后,拖拽框选到浏览器边缘或容器边缘后不会自动滚动,其它浏览器则正常


    方案2: 在 mousedown 时设置 e.preventDefault(),这样选中时文字就不会被选中,但是拖拽框选到浏览器边缘或容器边缘后不会自动滚动,只能自己实现了滚动逻辑。后面又发现在移动端的 touchstart 设置时,会发现页面上的点击事件都失效了,查资料发现没法解决,只能另辟蹊径。


    方案3: 在 mousemovetouchmove 时设置 e.preventDefault() 也是可以的,但也需要自己实现滚动逻辑。


    最终也是采取了方案3。


    后续目标


    目前只能进行矩形的碰撞检测,不支持圆形(2024.1.26 更新支持自定义已经可以实现)及一些不规则图形(2024.1.26 更新提供自定义碰撞检测(dom 下太难,canvas 比较好做碰撞检测),剩下的就是使用者的事了!)。这是一个难点,如果有想法的可以在评论区提出或者 pr 也可。


    2024-1-24 更新


    添加 cancel 方法,试一试。可以调用 ref.current?.cancel() 方法取消操作。这样可以自定义按下某个键来取消当前操作。有想需不需要添加一个属性传入 keyCode 数组内置取消,但是感觉会使 api 太多而臃肿,也欢迎留下你的想法。


    2024-1-26 更新一


    添加 items api 以优化虚拟滚动滚动时框选范围增加或减小时,已经卸载的 Item 的判断框选。(可选)试一试


    优化前:滚动到下面时,加大框选面积,上面已经被卸载的不会被选中


    录屏2024-01-26 16.50.31.gif


    优化后:滚动到下面时,加大框选面积,上面已经被卸载的会被选中


    录屏2024-01-26 16.53.36.gif


    2024-1-26 更新二


    支持自定义碰撞规则检测,试一试自定义圆形碰撞检测
    录屏2024-01-26 17.41.37.gif


    2024-1-31 更新


    value 支持任意类型 any,不再只是 string | number 类型,因为很多情况需要是一个对象或数组来当唯一标识,并提供了 compareFn 来支持自定义值的比较,默认使用 ===,如果你的 value 是对象或数组,需要此属性来比较值。


    总结


    开发一个较为复杂的组件,可以提交自己的 api 设计能力和解决问题的能力,可以将平常所学习、所了解、所使用的东西取其精华运用起来。最后希望这个组件能帮助到有需要的人,欢迎大家提出建议!有 issues 才能维护下去!如果觉得不错,帮忙点个 star 吧,地址 react-selectable-box


    作者:马格纳斯
    来源:juejin.cn/post/7326979670485123110
    收起阅读 »

    01CSS 实现多行文本“展开收起”

    web
    最近在开发移动端的评论内容功能时,我遇到了一个需求,需要实现一个展开收起效果。主要目的是让用户能够方便地查看和隐藏评论内容,现在想将我的成果分享给大家 完成效果: 实现思路: 1.准备一个文本的外部容器( content),并将最大高度设置为65px(根据...
    继续阅读 »

    最近在开发移动端的评论内容功能时,我遇到了一个需求,需要实现一个展开收起效果。主要目的是让用户能够方便地查看和隐藏评论内容,现在想将我的成果分享给大家



    完成效果:


    展开收起.gif


    实现思路:


    1.准备一个文本的外部容器( content),并将最大高度设置为65px(根据实际需求而定),超出内容设置不可见


    image.png


    2.文本容器的高度(text-content)不做样式设置,这个容器是为了获取内容实际高度


    image.png


    3.通过 js 获取文本容器的高度(text-content),判断文本高度是否超过外部容器(content)的最大高度,控制展开收起按钮是否显示


    4.点击按钮时根据条件设置容器(content)的最大高度,css 对通过 transition 对 max-height 设置过渡效果


    完整示例代码如下


    HTML



    <div class="container">
    <div class="content">
    <div class="text-content">
    1月30日上午10时,中国贸促会将召开1月例行新闻发布会,介绍第二届链博会筹备进展情况;
    2025大阪世博会中国馆筹备进展;2023年全国贸促系统商事认证数据;2023年贸法通运行情况;
    2023年11月全球经贸摩擦指数;2023年12月全球知识产权保护指数月度观察报告;助力培育外贸新动能有关工作考虑等。
    </div>
    </div>
    <button class="btn">展开</button>
    </div>


    CSS



    .container {
    width: 260px;
    padding: 20px;
    border: 1px solid #ccc;
    margin: 50px auto;
    }

    .content {
    max-height: 65px;
    overflow: hidden;
    transition: max-height 0.5s;
    }


    .btn {
    display: flex;
    width: 40px;
    color: cornflowerblue;
    outline: none;
    border: none;
    background-color: transparent;
    }



    JS


        const maxHeight=65
    const btn = document.querySelector('.btn')
    const content = document.querySelector('.content')
    const textContent=document.querySelector('.text-content')
    const textHeight=textContent.getBoundingClientRect().height // 文本高度
    const contentHeight=content.getBoundingClientRect().height // 容器高度
    let flag = false
    if (textHeight < maxHeight) {
    btn.style.display = 'none'
    }
    btn.addEventListener('click', () => {
    if (!flag) {
    content.style.maxHeight=textHeight+'px'
    btn.innerHTML = '收起'
    flag = true
    } else {
    content.style.maxHeight=contentHeight+'px'
    btn.innerHTML = '展开'
    flag = false
    }
    })



    实现一个功能的方式往往有多种,你们是怎么解决的呢?


    作者:前端小山
    来源:juejin.cn/post/7329694104118919195
    收起阅读 »

    浏览器关闭实现登出(后端清token)

    web
    实现浏览器关闭后,后端记录用户登出日志的最佳方式是通过前端发送请求来通知后端进行记录。以下是一种常见的实现方式,重点就是如何区分用户行为是页面刷新还是关闭浏览器。 // 写在APP.vue mounted() { window.addEventLi...
    继续阅读 »

    实现浏览器关闭后,后端记录用户登出日志的最佳方式是通过前端发送请求来通知后端进行记录。以下是一种常见的实现方式,重点就是如何区分用户行为是页面刷新还是关闭浏览器。


    // 写在APP.vue
    mounted() {
    window.addEventListener("beforeunload", () => this.beforeunloadHandler())
    window.addEventListener("unload", () => this.unloadHandler())
    },

    destroyed() {
    window.removeEventListener("beforeunload", () => this.beforeunloadHandler())
    window.removeEventListener("unload", () => this.unloadHandler())
    clearInterval(this.timer)
    },

    methods:{
    beforeunloadHandler() {
    this.beforeUnloadTime = new Date().getTime()
    },
    unloadHandler() {
    this.gapTime = new Date().getTime() - this.beforeUnloadTime
    if (this.gapTime <= 5) { //判断是窗口关闭还是刷新,小于5代表关闭,否则就是刷新。
    // 这里是关闭浏览器
    logout()
    }
    },
    }


    但是经测试,发现上面这种浏览器关闭事件并不是一种可靠的方式来捕捉用户的登出操作,后端并非百分百接收到logout请求,经查资料得知,在unload阶段发送的异步请求是不可靠的,有可能被cancel。后面又尝试了fetch,设置了keepalive(即使浏览器关闭,请求照样发送), 但是又发现gapTime<=5的判断条件也存在兼容性问题,不同浏览器的时间差存在差异。此外还存在一些特殊情况:用户可能直接关闭浏览器窗口、断开网络连接或发生其他异常情况,导致浏览器关闭事件无法被触发,因此pass掉上述方案。


    后面也尝试了心跳机制(websocket),也存在局限性,pass。


    最后想到了一种最简单,最朴实的方式:
    开启定时器每秒往localStorage写入当前时间lastRecordTime(new Date().getTime()), 在请求拦截器中给每个接口请求头带上两个时间,最后一次写入时间lastRecordTime和当前时间nowTime, 后端只要把两个时间相减, 超过5s(自定义)就算登出,清掉redis里相应的token。


    // 写在APP.vue
    created (){
    // 每秒写入一次时间
    this.timer = setInterval(() => {
    // 这个判断代表登录成功后才开始写入时间
    if(localStorage.getItem('token')) {
    localStorage.setItem('lastRecordTime', new Date().getTime())
    }
    }, 1000)
    }

    另外需要注意, 在登录成功的地方要立即写入一次时间, 不然有BUG。


      // 写在请求拦截器
    const headers = config.headers;
    /** 用于判断用户是否关闭过浏览器,如果关闭则跳转至登录页面,以及及时清理redis中的token */
    if (localStorage.getItem('lastRecordTime')) {
    headers.lastRecordTime = localStorage.getItem('lastRecordTime');
    }
    headers.nowTime = new Date().getTime();

    总结一下,目前没发现哪种方式可以提供一种可靠的通信方式去通知后端清除token, 通过两个时间差的方式相对靠谱。


    作者:起床搬砖啦
    来源:juejin.cn/post/7328221562817478665
    收起阅读 »

    🌟前端使用Lottie实现炫酷的开关效果🌟

    web
    前言 在平时的开发过程中,前端或多或少都会遇到实现动画效果的场景。手写动画是一件相当麻烦的事情,调来调去不仅费时费力,可能还会被产品/UI吐槽:这动画效果也不难呀,为什么就不能实现呢?/为什么就没有还原成我想要的样子呢。 比如说产品让我们实现这样的一个开关动...
    继续阅读 »

    前言


    在平时的开发过程中,前端或多或少都会遇到实现动画效果的场景。手写动画是一件相当麻烦的事情,调来调去不仅费时费力,可能还会被产品/UI吐槽:这动画效果也不难呀,为什么就不能实现呢?/为什么就没有还原成我想要的样子呢。


    image.png


    比如说产品让我们实现这样的一个开关动效


    Kapture 2024-01-20 at 21.53.34.gif


    今天我们就用动画的实现方式——Lottie,来百分百还原设计师的动画效果,并且可以大大提高我们的工作效率(摸鱼时间)。


    image.png


    Lottie简介


    首先我们先来看一下,平时我们实现动画都有哪些方式,它们分别有什么优缺点:


    动画类型优点缺点
    CSS 动画使用简便,通过@keyframestransition创建动画;浏览器原生支持,性能较好控制有限,不适用于复杂动画;复杂动画可能需要大量 CSS 代码,冗长
    JavaScript 动画提供更高程度的控制和灵活性;适用于复杂和精细动画效果引入库增加页面负担,可能需要学习曲线;使用不当容器对页面性能造成影响,产生卡顿
    GIF 动画制作和使用简单,无需额外代码;几乎所有浏览器原生支持有限颜色深度,不适用于所有场景;清晰度与文件尺寸成正比,无法适应所有分辨率
    Lottie支持矢量动画,保持清晰度和流畅性 ;跨平台使用,适用于 iOS、Android 和 Web在一些较旧或性能较低的设备上,播放较大的 Lottie 动画可能会导致性能问题;对设计师要求较高

    Lottie是由Airbnb开发的一个开源库,用于在移动端和Web上呈现矢量动画。它基于JSON格式的Bodymovin文件,可以将由设计师在AE中创建的动画导出为可在Lottie库中播放的文件。


    相对于手写CSS/JS动画而言,它可以大大减少前端开发的工作量,相对于GIF文件来说,它可以在一个合理的文件体积内保证动画的清晰度以及流畅程度。下面我们就介绍一下如何播放一个Lottie动画,并实现一个炫酷的开关效果。


    Hello Lottie


    假设我们现在已经有一个Lottiejson文件,那么现在安装一些依赖


    npm i react-lottie prop-types

    安装完之后我们就可以这样子来播放一个Lottie动画:


    import animationData from "../../assets/switch-lottie.json";

    const LottieSwitch = () => {
    const playing = useRef(false);
    const options = {
    loop: true,
    autoplay: true,
    animationData: animationData,
    rendererSettings: {
    preserveAspectRatio: "xMidYMid slice",
    },
    };
    return (
    <Lottie
    options={options}
    height={20}
    width={40}
    />

    );
    };


    Kapture 2024-01-20 at 21.41.20.gif


    来解释一下上面的options参数里面各个字段是什么意思:



    • loop:是否循环播放

    • autoplay:是否自动播放

    • animationDataLottie动画json资源

    • rendererSettings.preserveAspectRatio:指定如何在给定容器中渲染Lottie动画

      • xMidYMid: 表示在水平和垂直方向上都在中心对齐

      • 表示保持纵横比,但可能会裁剪超出容器的部分




    正/反向播放


    正常的把Lottie动画播放出来之后,我们就可以开始实现一个开关的功能。其实就是点击的时候更换Lottie的播放方向,这里对应的是direction字段,direction1时正向播放,direction-1时反向播放。


    我们就要实现下面的功能:



    • 点击时切换方向

    • 播放过程中加锁,禁止切换方向

    • 监听播放结束事件,解锁

    • loop改为falseautoplay改为false


    实现代码如下:


    const LottieSwitch = () => {
    const [direction, setDirection] = useState(null);
    const playing = useRef(false);
    const options = {
    loop: false,
    autoplay: false,
    animationData: animationData,
    rendererSettings: {
    preserveAspectRatio: "xMidYMid slice",
    },
    };

    const handleClick = () => {
    if (playing.current) {
    return;
    }
    playing.current = true;
    setDirection((prevState) => (prevState === 1 ? -1 : 1));
    };
    return (
    <div style={{ padding: 40 }}>
    <div onClick={handleClick} className={styles.lottieWrapper}>
    <Lottie
    direction={direction}
    options={options}
    speed={2}
    height={20}
    width={40}
    eventListeners={[
    {
    eventName: "complete",
    callback: () =>
    {
    playing.current = false;
    },
    },
    ]}
    />
    </div>
    </div>

    );
    };

    这样我们就是实现了一个开关的效果


    Kapture 2024-01-20 at 21.53.34.gif


    持续时长


    Lottiejson中,有几个关键的字段跟动画的播放时长有关系:



    • fr:帧率,每一秒的帧数

    • ip:开始帧

    • op:结束帧


    假如说有下面的一个描述:


    {
    "fr": 30,
    "ip": 0,
    "op": 60,
    }

    则表示帧率是30帧,从第0帧开始,60帧结束,那这个动画的持续时长是 (op-ip)/fr,为2s。那如果我们希望整个动画的播放时长是500ms,则只需要把Lottie的倍速调整为4。对应的就是speed字段:


    <Lottie
    direction={direction}
    options={options}
    speed={4}
    height={20}
    width={40}
    eventListeners={[
    {
    eventName: "complete",
    callback: () => {
    playing.current = false;
    },
    },
    ]}
    />

    Kapture 2024-01-20 at 22.06.53.gif


    修改Lottie


    Lottie json中,描述整个动画的过程以及效果其实对应的就是某个值。在实现的过程中,其实开发是可以去修改这些值的。比如说我们可以修改上面开关的边框颜色以及小球的颜色。


    首先在页面中找到小球对应的颜色是rgb(99, 102, 241)


    image.png


    Lottie JSON文件中,颜色信息通常出现在表示图层样式的字段中。常见的字段是 "c"(color)
    "c" 字段表示颜色,通常以RGBA格式(红绿蓝透明度)存储。例如:


    "c": {"a":0,"k":[1,0,0,1]}

    这表示红色,RGBA值为 [1, 0, 0, 1]


    rgb(99, 102, 241)转成上面的写法那就是"c": {"a":0,"k":[99/255,102/255,241/255,1]}。以99/255为例,结果是0.38823529411764707,那么就拿这个结果去json文件中找到对应的节点。


    image.png


    对应有2个结果,就是小球的颜色以及边框的颜色。当我们找到这个值的时候,如果我们想修改这个值,就必须知道这个值的路径,在一个Lottie中,想肉眼找到这个值的路径是一件很难的事情。所以我们写一个辅助函数:


    const updateJsonValue = (json, targetValue, newValue) => {
    const find = (json, targetValue, currentPath = []) => {
    for (const key in json) {
    if (json[key] === targetValue) {
    return [...currentPath, key];
    } else if (typeof json[key] === "object" && json[key] !== null) {
    const path = find(json[key], targetValue, [...currentPath, key]);
    if (path) {
    return path;
    }
    }
    }
    };
    const res = JSON.parse(JSON.stringify(json));
    const path = find(res, targetValue);
    let current = res;

    for (let i = 0; i < path.length - 1; i++) {
    const key = path[i];
    current = current[key];
    }

    const lastKey = path[path.length - 1];
    current[lastKey] = newValue;

    return json;
    };

    上面的辅助函数就帮助我们找到这个值的路径,并修改目标值。比如说我们想把目前的颜色改成绿色(rgb(25, 195, 125)),就可以找到对应的路径,并修改。别忘了替换的时候把rgb对应的值除以255


    let newAnimationData = updateJsonValue(animationData, 0.388235300779, 0.09803921568627451)
    newAnimationData = updateJsonValue(newAnimationData, 0.388235300779, 0.09803921568627451)
    newAnimationData = updateJsonValue(newAnimationData, 0.40000000596, 0.7647058823529411)
    newAnimationData = updateJsonValue(newAnimationData, 0.40000000596, 0.7647058823529411)
    newAnimationData = updateJsonValue(newAnimationData, 0.945098042488, 0.49019607843137253)
    newAnimationData = updateJsonValue(newAnimationData, 0.945098042488, 0.49019607843137253)

    image.png


    掌握了这种方式之后,我们就能修改Lottie里面的大部分内容,包括文案、资源图片、颜色等等。


    最后


    以上就是一些Lottie的使用以及修改的介绍,下次再遇到比较麻烦的动画需求。就可以跟产品说:可以做,让UI给我导出一个Lottie


    image.png


    如果你有一些别的想法,欢迎评论区交流~如果你觉得有意思的话,点点关注点点赞吧~


    作者:可乐鸡翅kele
    来源:juejin.cn/post/7325717778597773348
    收起阅读 »

    从‘相信前端能做一切’到‘连这个都做不了么’

    web
    帮助阅读 此篇文章主要是为了实现仪表盘功能,前后过了4种方案,每篇方案从逻辑、代码、效果、问题四个方面出发。最后个人总结。同时非常非常希望有大佬能够提供一个方案,个人实在想不到实现方案了 需求 h5页面中,做一个环形仪表盘(如下图),需要一个从0%到实际百分比...
    继续阅读 »

    4711705568245_.pic.jpg


    帮助阅读


    此篇文章主要是为了实现仪表盘功能,前后过了4种方案,每篇方案从逻辑、代码、效果、问题四个方面出发。最后个人总结。同时非常非常希望有大佬能够提供一个方案,个人实在想不到实现方案了


    需求


    h5页面中,做一个环形仪表盘(如下图),需要一个从0%到实际百分比的增长过渡动效
    未命名.png


    前提


    使用前端原生Html、css、js语言实现, 不打算借助第三方插件。


    最初Scheme


    将UI图片作为背景,上面放一个白色div作为遮罩,再利用css3将白色div旋转,从而达到过渡效果。


    代码如下:


    <style>
    .light-strip {
    width: 500px;
    height:500px;
    border: 1px solid #efefef;
    background-image: url('Frame 29@3x.png');
    float: right;
    background-size: 100% 100%;
    }
    .light-strip-top {
    margin-top: 0%;
    width: 500px;
    height: 250px;
    background: #fff;
    transform-origin: bottom center;
    /* transform: rotate 5s ; */
    rotate: 0deg;
    transition: all 2s ease-in-out;
    }
    </style>
    <body onload="load()">
    <div class="light-strip">
    <div class="light-strip-top">

    </div>
    </div>
    </body>
    <script>
    function load() {
    setTimeout(() => {
    document.querySelectorAll('.light-strip-top')[0].setAttribute('style', "rotate: 180deg")
    }, 1000)
    }
    </script>

    效果如下:


    屏幕录制2024-01-29 13.50.58.gif


    出现问题:


    由于仪表盘整体大于180度,所以白色div,在最开始遮挡全部仪表盘,那么旋转一定角度后一定会覆盖UI图。


    进化Scheme


    根据上面出现的问题,想到与UI沟通将仪表盘改成180度效果(解决不了问题,就把问题解决掉),该方案由于改变了原型之后会导致UI过于丑,就没有进行深度测试。


    超进化Scheme


    根据上面两个方案的结合,想到将方案1中的白色div换成一张指针图片,利用css3旋转追针,达到过渡效果,但此方案也是改了UI效果。


    代码如下:


    	<style>
    .light-strip {
    width: 500px;
    height:500px;
    border: 1px solid #efefef;
    background-image: url('Frame 29@3x.png');
    /* background-color: #fff; */
    float: right;
    background-size: 100% 100%;
    }
    .light-strip-top {
    margin-top: 50%;
    width: 49%;
    height: 4px;
    background: red;
    transform-origin: center right;
    /* transform: rotate 5s ; */
    rotate: -35deg;
    transition: all 2s ease-in-out;
    }

    </style>
    <body onload="load()">
    <div class="light-strip">
    <div class="light-strip-top">

    </div>
    </div>
    </body>
    <script>
    function load() {
    setTimeout(() => {
    document.querySelectorAll('.light-strip-top')[0].setAttribute('style', "rotate: 90deg")
    }, 1000)
    }
    </script>

    效果如下:


    屏幕录制2024-01-29 15.44.31.gif


    Now:


    此时大脑宕机了,在我的前端知识基础上,想不到能够完美实现UI效果的方案了。于是和同事探讨了一下,了解到element-plus中的进度条有类似的效果,于是打算看一下源码,了解到它是svg实现的。发现新大陆又开始尝试svg实现。


    究极进化Scheme


    利用svg,做一个带白色的背景圆环A,再做一个带有渐变背景色的进度圆环B, 利用进度圆环的偏移值、显示长度、断口长度配合css3过渡实现过渡效果。


    代码如下:


     <style>
    body {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 100vh;
    margin: 0;
    background-color: #f5f5f5;
    }

    .dashboard {
    position: relative;
    width: 200px;
    height: 200px;
    background-size: 100% 100%;
    }

    .circle-background {
    fill: none; /* 不填充 */
    stroke: #fff; /* 圆环的颜色 */
    stroke-width: 10; /* 圆环的宽度 */
    stroke-dasharray: 200, 52; /* 圆环断开部分的长度,总长度为周长 */
    stroke-dashoffset: 163;
    stroke-linecap: round;
    border-radius: 10;
    transition: all 1s; /* 过渡效果时间 */
    }

    .circle-progress {
    fill: none; /* 不填充 */
    stroke: url(#gradient); /* 圆环的颜色 */
    stroke-width: 10; /* 圆环的宽度 */
    stroke-dasharray: 252, 0; /* 圆环断开部分的长度,总长度为周长 */
    stroke-dashoffset: 163;
    stroke-linecap: round; /* 圆滑断点 */
    transition: all 1s; /* 过渡效果时间 */
    }

    .percentage {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    font-size: 24px;
    color: #3498db;
    }
    </style>
    </head>
    <body>

    <svg class="dashboard" viewBox="0 0 100 100">
    <!-- 定义渐变色 -->
    <defs>
    <linearGradient id="gradient" gradientUnits="userSpaceOnUse" x1="50" y1="0" x2="50" y2="100%">
    <stop offset="0%" style="stop-color: rgba(111, 232, 191, 1)" />
    <stop offset="33%" style="stop-color: rgba(255, 175, 19, 1)" />
    <stop offset="70%" style="stop-color: rgba(222, 19, 80, 1)" />
    <stop offset="100%" style="stop-color: rgba(133, 14, 205, 1)" />
    </linearGradient>
    </defs>

    <!-- 背景圆环 -->
    <circle class="circle-background" cx="50" cy="50" r="40"></circle>

    <!-- 进度圆环 -->
    <circle class="circle-progress" cx="50" cy="50" r="40"></circle>

    </svg>

    <!-- 进度百分比显示 -->
    <div class="percentage" id="percentage">0%</div>

    <script>
    function setProgress(percentage) {
    const circleProgress = document.querySelector('.circle-progress');
    const circleBackground = document.querySelector('.circle-background');
    const percentageText = document.getElementById('percentage');

    const circumference = 2 * Math.PI * 40; // 圆的周长
    const circumNewLength = (percentage / 100) * (circumference - 52);
    const dashOffset = 163 - circumNewLength;


    // 设置进度圆环的样式
    circleBackground.style.strokeDashoffset = dashOffset;
    circleBackground.style.strokeDasharray = `${200 - circumNewLength}, ${ 52 + circumNewLength }`
    circleProgress.style.strokeDasharray = `${circumNewLength}, ${ circumference - circumNewLength }`

    // 更新百分比文本
    percentageText.textContent = `${percentage}%`;
    }

    // 设置初始进度为0%
    setProgress(0);

    // 模拟过渡效果,从0%到50%
    setTimeout(() => {
    setProgress(50);
    }, 1000); // 过渡时间为1秒,你可以根据需要调整这个值
    </script>


    效果如下:


    屏幕录制2024-01-29 15.46.35.gif


    问题:


    基本实现,但是还有一个问题是,渐变色是两点之间的线性渐变,无法做到圆环的顺时针渐变。


    总结



    • 单纯前端不是万能的😂😂😂😂

    • 个人认为这个需求还是能够实现的

    • 希望有da lao能出个方案

    • 加油,继续搞


    作者:Otway
    来源:juejin.cn/post/7329310941106356275
    收起阅读 »

    伪指纹浏览器开发的那些事

    web
    什么是伪指纹浏览器开发 就是通过开源的chromium浏览器进行二次简单的封装不涉及到重新编译chromium,配合puppeteer进行轻微的指纹修改开发 一、如何操作 本次操作客户端以前端擅长的electron来举例子,至于electron是什么,打开文心...
    继续阅读 »

    什么是伪指纹浏览器开发


    就是通过开源的chromium浏览器进行二次简单的封装不涉及到重新编译chromium,配合puppeteer进行轻微的指纹修改开发


    一、如何操作


    本次操作客户端以前端擅长的electron来举例子,至于electron是什么,打开文心一言看看...


    第一步下载chromium到本地客户端


    登录官网,看到如下界面


    image.png


    可以发现箭头处指定是浏览器对应的版本buildId和系统,这里可以直接手动点击下载到本地,也可以通过@puppeteer/browsers这个库使用js代码去下载。这里说说如何使用它下载


    const { app } = require('electron')
    const browserApi = require('@puppeteer/browsers')
    const axios = require('axios')

    // browser缓存路径,避免和electron一起打包占用安装包体积和打包时间
    const cacheDir = `${app.getPath('cache')}/myBrowser`

    browserApi.install({
    cacheDir, // 自己想要下载的路径,用来给puppeteer去调用
    browser: browserApi.Browser.CHROMIUM,
    // buildId: '1247373',
    // baseUrl: 'https://commondatastorage.googleapis.com/chromium-browser-snapshots'
    })

    耐心的小伙伴肯定发现了这里buildId版本号和baseUrl下载url我打了注释,是因为@puppeteer/browsers默认下载的chromium版本比较旧,那么我们怎么获取这个最新版本buildId和baseUrl呢,还是官网那个界面打开控制台,可以看到如下请求链接


    image.png
    然后看到请求结果
    image.png
    这就是最新的buildId了,然后封装成函数调用


    // 获取最新的chromium构建ID
    function getLastBuildId(platform) {
    return axios
    .get(
    `https://download-chromium.appspot.com/rev/${browserApi.BrowserPlatform.MAC}?type=snapshots`
    )
    .then((res) => res.data.content)
    }

    baseUrl可以在界面点击下载时候,看到控制台有一个请求,那就是baseUrl了


    image.png
    下载好后,可以去我们定义的下载保存地址,通过终端去打开就可以看到了


    二、第二步启动chromium


    使用puppeteer-core这个库,启动我们下好的chromium


    const puppeteer = require('puppeteer-core')
    const browserApi = require('@puppeteer/browsers')

    // browser缓存路径
    const cacheDir = `${app.getPath('cache')}/myBrowser`

    // 获取安装的浏览器路径
    function getBrowserPath() {
    return browserApi
    .getInstalledBrowsers({ cacheDir })
    .then((list) => list[0]?.executablePath)
    }

    // 浏览器生成
    const createBrowser = async (proxyServer, userAgent) => {
    const browser = await puppeteer.launch({
    args: [
    `--proxy-server=${proxyServer}`,
    `--user-agent="${userAgent}"`,
    '--no-first-run',
    '--no-zygote',
    '--disable-extensions',
    '--disable-infobars',
    '--disable-automation',
    '--no-default-browser-check',
    '--disable-device-orientation',
    '--disable-metrics-reporting',
    '--disable-logging'
    ],
    headless: false,
    defaultViewport: null,
    ignoreHTTPSErrors: true,
    ignoreDefaultArgs: [
    '--enable-infobars',
    '--enable-features=NetworkService,NetworkServiceInProcess',
    '--enable-automation',
    'about:blank'
    ],
    executablePath: await getBrowserPath()
    })

    return browser
    }

    通过puppeteer.launch启动一个浏览器,至于启动参数这里我只说指纹相关的两个参数--proxy-server--user-agent,其他AI一下。


    --proxy-server代理服务,浏览器访问的出口IP,即你用自己启动的浏览器访问google时候,那边服务端获取的ip就是你的代理ip,测试时候可以自己在另外一台机器上装个Squid测试。--user-agent即浏览器的window.navigator.userAgent,简单指纹一般都是依赖于它生成


    三、开发过程中用到的功能点


    看完puppeteer官网,我们知道操作chromium依赖于一套协议chromedevtools.github.io/devtools-pr…


    3.1 更换dock图标


    比如多开浏览器,我如何更换chromium的桌面dock图标,去标识这是我启动的第几个浏览器。我们可以使用Browser.setDockTile去操作浏览器更换dock图标


    const pages = await browser.pages()
    const page = pages[0]
    const session = await pages[0].target().createCDPSession()
    await session.send('Browser.setDockTile', {
    image: new Buffer.from(fs.readFileSync(file)).toString('base64')
    })

    效果如下:


    image.png


    更多的协议操作需要自己摸索了,提示下,AI搜索chrome cdp协议


    3.2 增加默认书签


    这里我没找到协议,直接通过类似爬虫的方式,先进入标签管理页面,直接操作js新增,也算是一个技巧性的骚操作


    await page.goto('chrome://bookmarks/') // 进入标签管理页面
    await page.evaluate(async () => {
    // 类似在控制台直接操作一样,下面的代码控制台一样可以达到效果
    const defaultBookmarks = [
    {
    title: "文心一言",
    url: "https://yiyan.baidu.com/",
    },
    {
    title: "掘金",
    url: "https://juejin.cn/",
    },
    ];

    defaultBookmarks.forEach((item) => {
    chrome.bookmarks.create({
    parentId: "1",
    ...item,
    });
    });
    });
    await page.goto('自己的本来要跳的首页')

    3.3 如何使用已经打开的浏览器


    const browserWSEndpoint = browser.wsEndpoint() // 获取本次打开的浏览器链接,留作下一次使用
    // 保存下来, 比如直接存在一个变量map中,给它定义一个唯一的browserId,下一次好直接获取
    browserMap.set(browserId, browserWSEndpoint)

    ...
    // 再次打开新页面,要用到上一次打开的浏览器
    const browser = puppeteer.connect({
    ...launchOptions, // 和自己首次打开浏览器的配置一样
    browserWSEndpoint: browserMap.set(browserId)
    })

    这样就可以使用之前打开的浏览器打开网页了


    3.4 如何把浏览器的信息显示在网页上


    比如代理、userAgent、地区、浏览器名称等信息,先写个页面,然后轮询从localStorage直到获取信息为止。


    // 浏览器代理信息页
    await page.goto('浏览器信息页')
    // 设置localStorage
    await page.evaluate(
    (values) => {
    window.localStorage.setItem('browserInfo', values)
    },
    JSON.stringify(browserData)
    )

    page在打开页面后,并不会在页面中马上能获取到这里注入的browserInfo,可以通过轮询方式去扫描localStorage中是否存在我们注入的变量,这里举个react中的例子,在页面ready后去轮询处理


    useEffect(() => {
    let loopId = null
    const clearLoop = () => {
    loopId && clearTimeout(loopId)
    }

    // 轮询直到获取browserInfo
    const loop = () => {
    loopId = setTimeout(() => {
    const localData = window.localStorage.getItem('browserInfo')
    if (localData) {
    Promise.resolve()
    .then(() => {
    setInfo(JSON.parse(localData))
    })
    .catch(() => {
    message.error('获取浏览器信息失败')
    })
    } else {
    loop()
    }
    }, 1500)
    }

    loop()

    return () => {
    clearLoop()
    }
    })

    3.5 校验代理


    一般的代理服务为了不让别人也能用都会加上账密校验,所以我们还需要在启动后,调用方法去校验


    // 校验proxy
    if (proxyData.proxyServer) {
    await page.authenticate({
    username: proxyData.proxyUser,
    password: proxyData.proxyPwd
    })
    }

    四、遇到了哪一些问题


    4.1 mac下关闭浏览器关不掉


    当我们点击左上角关闭浏览器按钮或者是关闭所有页面时候,底部的dock中依旧存在着,我们不希望像mac其他软件一样保留在dock中,不然下一次打开浏览器时候,会出现相同标识的浏览器,可以这么解决


    // 每次页面关闭时候,查看浏览器是不是还有页面了,没有就关闭
    browser.on('targetdestroyed', async () => {
    const pages = await browser.pages()
    if (!pages.length) {
    await browser.close()
    }
    })

    4.2 当我们之间关闭电脑屏幕时候,比如盖上电脑,再次打开时候,关闭不了浏览器


    打上log,可以发现熄屏时候,会触发puppeteer定义的browser的disconnected事件,但是再次打开电脑时候浏览器是可以正常使用的,也就是说,puppeteer和我们打开的chromium断连了,所以我们需要在disconnected事件里再此尝试链接下chromium,如果不行才认为是浏览器被关闭了


    browser.on('disconnected', () => {
    const cacheData = browserMap.get(browserId)
    puppeteer
    .connect({
    ...launchOptions,
    browserWSEndpoint: cacheData.browserWSEndpoint
    })
    .then((newBrowser) => {
    browser = newBrowser
    log.info(
    'browser disconnected but browser is exist',
    )
    initEvent()
    })
    .catch((err) => {
    log.info(
    'browser disconnected success',
    )
    })
    })

    结语


    puppeteer很强大,chromium也强大,就是那个官网文档啊,写的真是让人...,所以多问问AI吧


    作者:柠檬阳光
    来源:juejin.cn/post/7327642905245433891
    收起阅读 »

    【干货】一文掌握JavaScript检查对象空值的N种技巧!

    在开发 JavaScript 应用程序时,经常需要检查对象是否为空。这是因为在处理和操作对象数据时,我们需要确保对象包含有效的值或属性。以下是一些常见情况,我们需要检查 JavaScript 对象是否为空:防止空引用错误:当我们尝试访问或使用一个空对象时,可能...
    继续阅读 »

    在开发 JavaScript 应用程序时,经常需要检查对象是否为空。这是因为在处理和操作对象数据时,我们需要确保对象包含有效的值或属性。以下是一些常见情况,我们需要检查 JavaScript 对象是否为空:

    1. 防止空引用错误:当我们尝试访问或使用一个空对象时,可能会导致空引用错误(如 TypeError: Cannot read property ‘x’ of null)。通过检查对象是否为空,我们可以避免这些错误的发生,并采取相应的处理措施。
    2. 数据验证和表单提交:在表单提交之前,通常需要验证用户输入的数据是否有效。如果对象为空,表示用户未提供必要的数据或未填写表单字段,我们可以显示错误消息或阻止表单提交。
    3. 条件逻辑和流程控制:根据对象是否为空,可以根据不同的条件逻辑执行不同的操作或采取不同的分支。例如,如果对象为空,可以执行备用的默认操作或返回默认值。
    4. 数据处理和转换:在处理对象数据之前,可能需要对其进行处理或转换。如果对象为空,可以提前终止或跳过数据处理逻辑,以避免不必要的计算或错误发生。
    5. 用户界面交互和显示:在用户界面中,可能需要根据对象的存在与否来显示或隐藏特定的界面元素、更改样式或呈现不同的内容。

    通过检查 JavaScript 对象是否为空,可以增加应用程序的健壮性、提升用户体验,并避免潜在的错误和异常情况。因此,检查对象是否为空是编写高质量代码的重要部分。

    在本文中,我们将讨论如何检查对象是否为空,其中包括 JavaScript 中检查对象是否为空的不同方法以及如何检查对象是否为空、未定义或为 null。

    使用Object.keys()

    使用Object.keys()方法可以检查对象是否为空。Object.keys(obj)返回一个包含给定对象所有可枚举属性的数组。
    利用这个特性,我们可以通过检查返回的数组长度来确定对象是否为空。如果数组长度为0,则表示对象没有任何属性,即为空。
    以下是一个示例代码:

    javascriptCopy Codefunction isObjectEmpty(obj) {
    return Object.keys(obj).length === 0;
    }

    // 测试对象是否为空
    const obj1 = {};
    console.log(isObjectEmpty(obj1)); // true

    const obj2 = { name: "John", age: 25 };
    console.log(isObjectEmpty(obj2)); // false

    在上面的示例中,我们定义了一个isObjectEmpty()函数,它接受一个对象作为参数。函数内部使用Object.keys(obj)获取对象的所有可枚举属性,并检查返回的数组长度是否为0。根据返回结果,判断对象是否为空。

    使用Object.values()

    使用Object.values()方法来检查对象是否为空,Object.values(obj)方法返回一个包含给定对象所有可枚举属性值的数组。如果返回的数组长度为0,则表示对象没有任何属性值,即为空。

    以下是使用Object.values()方法来检查对象是否为空的示例代码:

    function isObjectEmpty(obj) {
    return Object.values(obj).length === 0;
    }

    // 测试对象是否为空
    const obj1 = {};
    console.log(isObjectEmpty(obj1)); // true

    const obj2 = { name: "John", age: 25 };
    console.log(isObjectEmpty(obj2)); // false

    在上面的示例中,我们定义了一个isObjectEmpty()函数,它接受一个对象作为参数。函数内部使用Object.values(obj)获取对象的所有可枚举属性值,并检查返回的数组长度是否为0。根据返回结果,判断对象是否为空。
    请注意,Object.values()方法是ES2017(ES8)引入的新方法,因此在一些旧版本的JavaScript引擎中可能不被支持。在使用之前,请确保你的环境支持该方法或使用适当的polyfill来提供支持。

    使用 for…in 循环

    使用 for…in 循环方法是通过遍历对象的属性来判断对象是否为空。以下是一个示例代码:

    javascriptCopy Codefunction isObjectEmpty(obj) {
    for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
    return false; // 只要有一个属性存在,就返回false表示不为空
    }
    }
    return true; // 如果遍历完所有属性后仍然没有返回false,表示对象为空
    }

    // 测试对象是否为空
    const obj1 = {};
    console.log(isObjectEmpty(obj1)); // true

    const obj2 = { name: "John", age: 25 };
    console.log(isObjectEmpty(obj2)); // false

    在上面的示例中,isObjectEmpty()函数接受一个对象作为参数。函数内部使用 for…in 循环遍历对象的属性,如果发现任何属性,则返回false表示对象不为空;如果循环结束后仍然没有返回false,则表示对象为空,并返回true。
    虽然使用 for…in 循环可以达到同样的目的,但相比起使用 Object.keys() 或 Object.values() 方法,它的实现稍显繁琐。因此,通常情况下,推荐使用 Object.keys() 或 Object.values() 方法来检查对象是否为空,因为它们提供了更简洁和直观的方式。

    使用 Object.entries()

    Object.entries()方法返回一个给定对象自身可枚举属性的键值对数组。如果返回的数组长度为0,则表示对象没有任何属性,即为空。
    以下是使用Object.entries()方法来检查对象是否为空的示例代码:

    function isObjectEmpty(obj) {
    return Object.entries(obj).length === 0;
    }

    // 测试对象是否为空
    const obj1 = {};
    console.log(isObjectEmpty(obj1)); // true

    const obj2 = { name: "John", age: 25 };
    console.log(isObjectEmpty(obj2)); // false

    在上面的示例中,isObjectEmpty()函数接受一个对象作为参数。函数内部使用Object.entries(obj)获取对象的键值对数组,并检查返回的数组长度是否为0。如果数组长度为0,则表示对象没有任何属性,即为空。
    请注意,Object.entries()方法是ES2017(ES8)引入的新方法,因此在一些旧版本的JavaScript引擎中可能不被支持。在使用之前,请确保你的环境支持该方法或使用适当的polyfill来提供支持。

    使用 JSON.stringify()

    使用 JSON.stringify() 方法来检查对象是否为空的方法是将对象转换为 JSON 字符串,然后检查字符串的长度是否为 2。当对象为空时,转换后的字符串为 “{}”,长度为 2。如果对象不为空,则转换后的字符串长度会大于 2。
    以下是使用 JSON.stringify() 方法来检查对象是否为空的示例代码:

    function isObjectEmpty(obj) {
    return JSON.stringify(obj) === "{}";
    }

    // 测试对象是否为空
    const obj1 = {};
    console.log(isObjectEmpty(obj1)); // true

    const obj2 = { name: "John", age: 25 };
    console.log(isObjectEmpty(obj2)); // false

    在上述示例中,isObjectEmpty() 函数接受一个对象作为参数。函数内部使用 JSON.stringify(obj) 将对象转换为 JSON 字符串,然后将转换后的字符串与 “{}” 进行比较。如果相等,则表示对象为空。
    需要注意的是,这种方式只适用于纯粹的对象,并且不包含任何非原始类型属性(如函数、undefined 等)。如果对象中包含了非原始类型的属性,那么转换后的 JSON 字符串可能不为空,即使对象实际上是空的。

    E6使用Object.getOwnPropertyNames()

    在ES6中,你可以使用Object.getOwnPropertyNames()方法来检查对象是否为空,但需要注意的是,该方法返回一个数组,其包含给定对象中所有自有属性(包括不可枚举属性,但不包括使用 symbol 值作为名称的属性)。
    以下是使用Object.getOwnPropertyNames()方法来检查对象是否为空的示例代码:

    function isObjectEmpty(obj) {
    return Object.getOwnPropertyNames(obj).length === 0;
    }

    // 测试对象是否为空
    const obj1 = {};
    console.log(isObjectEmpty(obj1)); // true

    const obj2 = { name: "John", age: 25 };
    console.log(isObjectEmpty(obj2)); // false

    在上面的示例中,isObjectEmpty()函数接受一个对象作为参数。函数内部使用Object.getOwnPropertyNames(obj)获取对象的所有属性名,并检查返回的数组长度是否为0。如果数组长度为0,则表示对象没有任何属性,即为空。
    请注意,Object.getOwnPropertyNames()方法返回的数组只包含对象自身的属性,不包括继承的属性。如果你需要检查继承的属性,请使用for…in循环或其他方法。同样,Object.getOwnPropertyNames()方法在ES5中引入,因此在一些旧版本的JavaScript引擎中可能不被支持。

    ES6使用Object.getOwnPropertySymbols()方法

    在ES6中,可以使用Object.getOwnPropertySymbols()方法来检查对象是否为空。该方法返回一个数组,其中包含了给定对象自身的所有符号属性。
    以下是使用Object.getOwnPropertySymbols()方法来检查对象是否为空的示例代码:

    function isObjectEmpty(obj) {
    const symbols = Object.getOwnPropertySymbols(obj);
    const hasSymbols = symbols.length > 0;
    return !hasSymbols;
    }

    // 测试对象是否为空
    const obj1 = {};
    console.log(isObjectEmpty(obj1)); // true

    const symbol = Symbol("key");
    const obj2 = { [symbol]: "value" };
    console.log(isObjectEmpty(obj2)); // false

    在上面的示例中,isObjectEmpty()函数接受一个对象作为参数。函数内部使用Object.getOwnPropertySymbols(obj)获取对象的所有符号属性,并将它们存储在symbols数组中。然后,通过检查symbols数组的长度是否大于0来判断对象是否具有符号属性。如果symbols数组的长度为0,则表示对象没有任何符号属性,即为空。

    你还在苦恼找不到真正免费的编程学习平台吗?可以试试【云端源想】!课程视频、知识库、微实战、云实验室、一对一咨询……你想要的全部学习资源这里都有,重点是现在还是免费的!点这里即可免费学习!

    注意,Object.getOwnPropertySymbols()方法只返回对象自身的符号属性,不包括其他类型的属性,例如字符串属性。如果你想同时检查对象的字符串属性和符号属性,可以结合使用Object.getOwnPropertyNames()和Object.getOwnPropertySymbols()方法。

    ES6使用Reflect.ownKeys()

    在ES6中,你可以使用Reflect.ownKeys()方法来检查对象是否为空。Reflect.ownKeys()返回一个包含了指定对象自身所有属性(包括字符串键和符号键)的数组。
    以下是使用Reflect.ownKeys()方法来检查对象是否为空的示例代码:

    function isObjectEmpty(obj) {
    return Reflect.ownKeys(obj).length === 0;
    }

    // 测试对象是否为空
    const obj1 = {};
    console.log(isObjectEmpty(obj1)); // true

    const symbol = Symbol("key");
    const obj2 = { [symbol]: "value" };
    console.log(isObjectEmpty(obj2)); // false

    在上面的示例中,isObjectEmpty()函数接受一个对象作为参数。函数内部使用Reflect.ownKeys(obj)获取对象的所有自身属性名(包括字符串键和符号键),并检查返回的数组长度是否为0。如果数组长度为0,则表示对象没有任何属性,即为空。
    Reflect.ownKeys()方法提供了一种统一的方式来获取对象的所有键,包括字符串键和符号键。因此,使用Reflect.ownKeys()方法可以更全面地检查对象是否为空。

    使用lodash库的isEmpty()函数

    如果您使用了lodash库,可以使用其提供的isEmpty()函数来直接判断对象是否为空。
    以下是使用 Lodash 的 isEmpty() 函数进行对象空检查的示例代码:

    // 导入Lodash库
    const _ = require('lodash');

    // 检查对象是否为空
    const obj1 = {};
    console.log(_.isEmpty(obj1)); // true

    const obj2 = { name: "John", age: 25 };
    console.log(_.isEmpty(obj2)); // false

    在上述示例中,_.isEmpty() 函数接受一个对象作为参数,并返回一个布尔值表示对象是否为空。如果对象为空,则返回 true;否则返回 false。
    使用 Lodash 的 isEmpty() 函数可以更方便地进行对象空检查,同时处理了各种情况,包括非原始类型的属性、数组、字符串等。

    使用jQuery库的$.isEmptyObject()函数

    要使用 jQuery 库中的 $.isEmptyObject() 函数来检查 JavaScript 对象是否为空,首先确保已经安装了 jQuery 库,并将其导入到你的项目中。
    以下是使用 jQuery 的 $.isEmptyObject() 函数进行对象空检查的示例代码:

    // 导入jQuery库
    const $ = require('jquery');

    // 检查对象是否为空
    const obj1 = {};
    console.log($.isEmptyObject(obj1)); // true

    const obj2 = { name: "John", age: 25 };
    console.log($.isEmptyObject(obj2)); // false

    在上述示例中,$.isEmptyObject() 函数接受一个对象作为参数,并返回一个布尔值表示对象是否为空。如果对象为空,则返回 true;否则返回 false。
    使用 jQuery 的 $.isEmptyObject() 函数可以更方便地进行对象空检查,同时处理了各种情况,包括非原始类型的属性、数组、字符串等。

    检查对象是否为空、未定义或为 null

    要同时检查对象是否为空、未定义或为 null,你可以使用以下函数来进行判断:

    function isObjectEmptyOrNull(obj) {
    return obj === undefined || obj === null || Object.getOwnPropertyNames(obj).length === 0;
    }

    在上述代码中,isObjectEmptyOrNull函数接收一个对象作为参数。它首先检查对象是否为 undefined 或者 null,如果是,则直接返回 true 表示对象为空或者未定义。如果对象不是 undefined 或者 null,则使用 Object.getOwnPropertyNames() 方法获取对象的所有自身属性名,然后判断属性名数组的长度是否为 0。如果属性名数组长度为 0,则表示对象没有任何属性,即为空。
    下面是一个示例用法:

    const obj1 = {};
    console.log(isObjectEmptyOrNull(obj1)); // true

    const obj2 = null;
    console.log(isObjectEmptyOrNull(obj2)); // true

    const obj3 = { name: "John", age: 25 };
    console.log(isObjectEmptyOrNull(obj3)); // false

    const obj4 = undefined;
    console.log(isObjectEmptyOrNull(obj4)); // true

    总结和比较

    在本文中,我们介绍了多种方法来检查 JavaScript 对象是否为空。下面是这些方法的优缺点总结:

    • 使用 Object.keys() 方法

    优点:简单易用,不需要依赖第三方库。
    缺点:无法处理非原始类型的属性,如函数、undefined 等。

    • Object.values()

    优点:能够将对象的属性值组成一个数组,可以通过判断该数组的长度来判断对象是否为空。
    缺点:无法直接判断对象是否为空,只提供了属性值的数组。

    • 使用 for…in 循环遍历对象

    优点:可以处理非原始类型的属性。
    缺点:代码较为冗长,需要手动判断每个属性是否为对象自身属性。

    • 使用 JSON.stringify() 方法

    优点:可以处理非原始类型的属性,并且转换后的字符串长度为 2 表示对象为空。
    缺点:当对象包含循环引用时,将抛出异常。

    • Object.entries()

    优点:能够将对象的键值对组成一个数组,可以通过判断该数组的长度来判断对象是否为空。
    缺点:同样无法直接判断对象是否为空,只提供了键值对数组。

    • Object.getOwnPropertyNames()

    优点:能够返回对象自身的所有属性名组成的数组,包括可枚举和不可枚举的属性,可以通过判断该数组的长度来判断对象是否为空。
    缺点:同样无法直接判断对象是否为空,只提供了属性名数组。

    • Object.getOwnPropertySymbols()

    优点:能够返回对象自身的所有 Symbol 类型的属性组成的数组,可以通过判断该数组的长度来判断对象是否为空。
    缺点:仅针对 Symbol 类型的属性,无法判断其他类型的属性是否为空。

    • Reflect.ownKeys()

    优点:能够返回对象自身的所有属性(包括字符串键和 Symbol 键)组成的数组,包括可枚举和不可枚举的属性,可以通过判断该数组的长度来判断对象是否为空。
    缺点:同样无法直接判断对象是否为空,只提供了所有键的数组。

    • 使用 Lodash 库的 isEmpty() 函数

    优点:可以处理各种情况,包括非原始类型的属性、数组、字符串等。
    缺点:需要依赖第三方库。

    • 使用 jQuery 库的 $.isEmptyObject() 函数

    优点:可以处理各种情况,包括非原始类型的属性、数组、字符串等。
    缺点:需要依赖第三方库。

    总体来说, 这些方法都提供了一种间接判断对象是否为空的方式,即通过获取对象的属性、属性值或键值对的数组,并判断该数组的长度。然而,它们并不能直接告诉我们对象是否为空,因为它们只提供了属性、属性值或键值对的信息。因此,在使用这些方法判断对象是否为空时,需要结合其他判断条件来综合考虑。

    收起阅读 »

    JS逐页转pdf文件为图片格式

    web
    背景 年前的时候,开发一个电子杂志项目,功能需求是通过上传pdf文件,将其转为图片格式,所以杂志的内容其实就是一张张图片 不过当时技术要求用后端实现,所以使用的是PHP实现该功能。项目完成后,寻思着在前端是否也能实现pdf转图片的功能。一番研究后,果真可行。以...
    继续阅读 »

    背景


    年前的时候,开发一个电子杂志项目,功能需求是通过上传pdf文件,将其转为图片格式,所以杂志的内容其实就是一张张图片


    不过当时技术要求用后端实现,所以使用的是PHP实现该功能。项目完成后,寻思着在前端是否也能实现pdf转图片的功能。一番研究后,果真可行。以下就分享如何通过前端js将pdf文件转为图片格式,并且支持翻页预览、以及图片打包下载


    效果预览


    图片

    所需工具



    1. pdf.js(负责API解析,可将pdf文件渲染成canvas实现预览)

    2. pdf.worker.js(负责核心解析)

    3. jszip.js(将图片打包成生成.zip文件)

    4. Filesaver.js(保存下载zip文件)


    工具下载


    一、pdf.js及pdf.worker.js下载地址:


    mozilla.github.io/pdf.js/gett…


    1.选择稳定版下载


    图片


    2.解压后将bulid中的pdf.js及pdf.worker.js拷贝到项目中


    图片


    二、jszip.js及Filesaver.js下载地址:

    stuk.github.io/jszip/


    1.点击download.JSZip


    图片


    2.解压后将dist文件夹下的jszip.js文件以及vendor文件夹下的FileSaver.js文件拷贝到项目中


    图片


    至此,所需工具已齐全。以下直接附上项目完整代码(代码可直接复制使用,查看效果。 对应的文件需自行下载引入)


    源代码: 嫌麻烦的小伙伴可以直接在公众号后回复: pdf转图片


    代码实现


    <!DOCTYPE html>
    <html>
    <head>
    <meta charset="utf-8" />
    <title>PDF文件转图片</title>
    <script src="https://cdn.staticfile.org/jquery/1.10.2/jquery.min.js"></script>
    <script type="text/javascript" src="js/pdf.js"></script>
    <script type="text/javascript" src="js/pdf.worker.js"></script>
    <script type="text/javascript" src="js/jszip.js"></script>
    <script type="text/javascript" src="js/FileSaver.js"></script>
    <style type="text/css">

    button {
    width: 120px;
    height: 30px;
    background: none;
    border: 1px solid #b1afaf;
    border-radius: 5px;
    font-size: 12px;
    font-weight: 1000;
    color: #384240;
    cursor: pointer;
    outline: none;
    margin: 0 0.5%
    }

    button:hover {
    background: #ccc;
    }

    #container {
    width: 600px;
    height: 780px;
    margin-top: 1%;
    border-radius: 2px;
    border: 2px solid #a29b9b;
    }

    .pdfInfos {
    margin: 0 2%;
    }
    </style>
    </head>

    <body>

    <div style="margin-top:1%">
    <button id="prevpage">上一页</button>
    <button id="nextpage">下一页</button>
    <button id="exportImg">导出图片</button>
    <button onclick="choosePdf()">选择一个pdf文件</button>
    <input style="display:none" id='chooseFile' type='file' accept="application/pdf">
    </div>

    <div style="margin-top:1%">
    <span class="pdfInfos">页码:<span id="currentPages"></span><span id="totalPages"></span></span>
    <span class="pdfInfos">文件名:<span id="fileName"></span></span>
    <span class="pdfInfos">文件大小:<span id="fileSize"></span></span>
    </div>

    <div style="position: relative;">
    <div id="container"></div>
    <img id="imgloading" style="position: absolute;top: 20%;left: 2%;display:none" src="loading.gif">
    </div>

    </body>


    <script>

    var currentPages,totalPages //声明一个当前页码及总页数变量
    var scale = 2; //设置缩放比例,越大生成图片越清晰

    $('#chooseFile').change(function() {
    var pdfFilePath = $('#chooseFile').val();
    if(pdfFilePath) {

    $("#imgloading").css('display','block');
    $("#container").empty(); //清空上一PDF文件展示图

    currentPages=1; //重置当前页数
    totalPages=0; //重置总页数

    var filesdata = $('#chooseFile')[0].files; //jquery获取到文件 返回属性的值
    var fileSize = filesdata[0].size; //文件大小
    var mb;

    if(fileSize) {
    mb = fileSize / 1048576;
    if(mb > 10) {
    alert("文件大小不能>10M");
    return;
    }
    }

    $("#fileName").text(filesdata[0].name);
    $("#fileSize").text(mb.toFixed(2) + "Mb");

    var reader = new FileReader();
    reader.readAsDataURL(filesdata[0]); //将文件读取为 DataURL
    reader.onload = function(e) { //文件读取成功完成时触发

    pdfjsLib.getDocument(this.result).then(function(pdf) { //调用pdf.js获取文件
    if(pdf) {
    totalPages = pdf.numPages; //获取pdf文件总页数
    $("#currentPages").text("1/");
    $("#totalPages").text(totalPages);

    //遍历动态创建canvas
    for(var i = 1; i <= totalPages; i++) {
    var canvas = document.createElement('canvas');
    canvas.id = "pageNum" + i;
    $("#container").append(canvas);
    var context = canvas.getContext('2d');
    renderImg(pdf,i,context);
    }

    }
    });

    };
    }
    });

    //渲染生成图片
    function renderImg(pdfFile,pageNumber,canvasContext) {
    pdfFile.getPage(pageNumber).then(function(page) { //逐页解析PDF
    var viewport = page.getViewport(scale); // 页面缩放比例
    var newcanvas = canvasContext.canvas;

    //设置canvas真实宽高
    newcanvas.width = viewport.width;
    newcanvas.height = viewport.height;

    //设置canvas在浏览中宽高
    newcanvas.style.width = "100%";
    newcanvas.style.height = "100%";

    //默认显示第一页,其他页隐藏
    if (pageNumber!=1) {
    newcanvas.style.display = "none";
    }

    var renderContext = {
    canvasContext: canvasContext,
    viewport: viewport
    };

    page.render(renderContext); //渲染生成
    });

    $("#imgloading").css('display','none');

    return;
    };

    //上一页
    $("#prevpage").click(function(){

    if (!currentPages||currentPages <= 1) {
    return;
    }

    nowpage=currentPages;
    currentPages--;

    $("#currentPages").text(currentPages+"/");

    var prevcanvas = document.getElementById("pageNum"+currentPages);
    var currentcanvas = document.getElementById("pageNum"+nowpage);
    currentcanvas.style.display = "none";
    prevcanvas.style.display = "block";

    })

    //下一页
    $("#nextpage").click(function(){

    if (!currentPages||currentPages>=totalPages) {
    return;
    }

    nowpage=currentPages;
    currentPages++;

    $("#currentPages").text(currentPages+"/");

    var nextcanvas = document.getElementById("pageNum"+currentPages);
    var currentcanvas = document.getElementById("pageNum"+nowpage);
    currentcanvas.style.display = "none";
    nextcanvas.style.display = "block";

    })

    //导出图片
    $("#exportImg").click(function() {

    if (!$('#chooseFile').val()) {
    alert('请先上传pdf文件')
    return false;
    }

    $("#imgloading").css('display','block');

    var zip = new JSZip(); //创建一个JSZip实例
    var images = zip.folder("images"); //创建一个文件夹用来存放图片

    //遍历canvas,将其生成图片放进文件夹images中
    $("canvas").each(function(index, ele) {
    var canvas = document.getElementById("pageNum" + (index + 1));

    //将图片放进文件夹images中
    //参数1为图片名称,参数2为图片数据(格式为base64,需去除base64前缀 data:image/png;base64)
    images.file("" + (index + 1) + ".png", splitBase64(canvas.toDataURL("image/png", 1.0)), {
    base64: true
    });

    })

    //打包下载
    zip.generateAsync({
    type: "blob"
    }).then(function(content) {
    saveAs(content, "picture.zip"); //saveAs依赖的js文件是FileSaver.js
    $("#imgloading").css('display','none');
    });

    });

    //截取base64前缀
    function splitBase64(dataurl) {
    var arr = dataurl.split(',')
    return arr[1]
    }

    function choosePdf(){
    $("#chooseFile").click()
    }
    </script>
    </html>

    项目实现原理分析



    1. 首先利用pdf.js将上传的pdf文件转化成canvas

    2. 然后使用jszip.js将canvas打包图片生成.zip文件

    3. 最后使用Filesaver.js将zip文件保存下载


    项目注意要点



    1. 由于pdf文件是通过上传的,因此需要通过js的FileReader()对象将其读取为DataURL,pdf.js文件才可读取渲染

    2. JSZip对象的.file()函数中第二个参数传入的是base64格式图片,但是要去掉base64前缀标识


    作者:程序员Winn
    来源:juejin.cn/post/7238442926334918711
    收起阅读 »

    JSON.parse记录一次线上bug排查

    web
    最近项目中有一个匪夷所思的问题,业务在使用的时候,偶发性的会白屏,经常下班的时候骚扰我们,开发苦不堪言,经过长达一周的排查,仍然没有查到bug的存在,最终尝试通过添加埋点日志,记录关键信息。 现状 首先讲述一下现状,首先业务进入后,页面可以认为有两个按钮 跳...
    继续阅读 »

    最近项目中有一个匪夷所思的问题,业务在使用的时候,偶发性的会白屏,经常下班的时候骚扰我们,开发苦不堪言,经过长达一周的排查,仍然没有查到bug的存在,最终尝试通过添加埋点日志,记录关键信息。


    现状


    首先讲述一下现状,首先业务进入后,页面可以认为有两个按钮



    • 跳转共享链接

    • 打开表单弹窗按钮,点击后展示表单。


    image-20240124132744181


    操作顺序是,页面加载后,先点击跳转共享链接,看完链接后再返回点击表单弹窗。



    里面有两个重要的时间节点,一个是跳转链接之前,一个是返回到当前页面。




    • 跳转链接之前



      • 需要存储接口数据,接口数据包含了表单的数据



    • 返回当前页面



      • 请求接口数据



        • 本地缓存无,直接使用接口数据

        • 本地缓存有,缓存和接口数据合并,接口数据优先






    image-20240124132901079


    返回页面的时候,点击表单弹窗


    正常上来说弹窗能够正常显示,但是线上环境再点击 展示弹窗的按钮导致白屏了。整个流程如下


    image-20240124133213958


    初步判断是整合缓存和接口数据问题,于是需要给页面添加两个埋点



    • 页面报错异常时上报

    • 点击打开表单的时,上报缓存数据和聚合之后的数据。



      • 为什么不上报接口数据呢?因为当时修复bug比较紧急,观察代码发现接口直接返回的数据没有在公共变量中存储,如果需要存储改动较大,还有就是接口数据也可以从后端日志去排查




    页面报错异常上报


    异常上报的方法有很多,通常使用一个gif图片,地址为get的请求地址+上报信息,具体的可以自行百度,此处简单叙述下


    使用图片是因为加载资源里面img优先级比较低,不会阻塞其他资源,而且图片请求不会跨域,用gif是因为对比图片类型他是比较小的


    //utils/utils.js
    /**
    * 异常上报方法
    * 希望抽离出来同步异常类和异步异常类
    */

    function uploadError() {
     //上报处理参数
     const upload = errObj =>{
       const logUrl = 'https://xxx.xxx.com/log.gif'; // 上报接口
       //将obj拼接成url
       const queryStr = Object.entries(errObj)
          .map(([key, value]) => `${key}=${value}`)
          .join('&');
         const oImg = new Image();
         oImg.src = logUrl + '?' + encodeURIComponent(queryStr);
    }
     //同步方法
     function handleError(e) {
       try {
         let baseInfo = localStorage.getItem('base_info'); // 域账户
         let masterName = baseInfo ? JSON.parse(baseInfo)?.master_name : ''; // 域账户
         let errObj = {
           masterName: masterName,//域账户
           url: window.location.href,//报错的路由,利于排查
           reason: JSON.stringify({
             message: e?.error?.message, //报错信息
             stack: e?.error?.stack,//调用栈
          }),
           message: e?.message, //报错信息
        };
         upload(errObj)
         console.log('error', errObj);
      } catch (err) {
         console.log('error', err);
      }
    }
     window.addEventListener('error', handleError);//调用监听
    }

    //app.js
    //异常上报方法 开发环境禁止上报
    if(!['dev'].includes(process.env.BUILD_ENV)){
     uploadError()
    }

    点击弹窗的异常上报


    //打开弹窗的操作  
    const open = () => {
       setShow(!show);//控制表单的展示隐藏
       if(!show){
         const logUrl = 'https://xxx.xxx.com/log.gif'; // 上报接口
         const oImg = new Image();
         let initFormVal = localStorage.getItem('initFormVal' + query?.id);
         oImg.src = logUrl + '?' + encodeURIComponent(`initFormVal=${initFormVal}&integratedData=${JSON.stringify(integratedData)}`);
      }
    };
    //initFormVal为缓存中的数据 integratedData为整合后的数据

    发现问题原因


    通过添加以上异常上报,业务员进行操作时,又出现了白屏,此时根据业务员token与上报关键字与时间查到了相关日志,其中日志中记录的是


    https://xxx.xxx.com/log.gif?initFormVal=&integratedData=null

    integratedData是后端接口数据和缓存的融合呀!通过查日志发现当时后端确确实实返回正常的响应了,不可能为null,同时还有一个疑问浮出水面,为什么initFormVal没有值,而不是null


    正常来说如果initFormVal从json中取值时,取不到应该默认就是null,此处为'',只说明一个问题,缓存的时候给他赋值了


    那么问题大致可以定位到以下两个操作节点



    • 缓存时

    • 返回页面后,缓存和接口数据融合时


    //缓存时操作  
    const getFormValues = () => {
       let formVal = childRef?.current?.getFormVal() || '';
    localStorage.setItem('initFormVal' + query.id, JSON.stringify(formVal));
    };

    缓存时,如果子节点获取不到,那么childRef?.current?.getFormVal()就为undefind,又由于使用了或运算符,那么此时存储的是'',那么取这个暂时看也没问题呀,然后也写入了缓存



    更严格来讲,应该先判断formVal是否存在然后再去缓存,没有就不缓存。



    再看一下返回页面,数据融合的代码


    const getDataFn = url => {
       dispatch({
         type: url,
         payload: { id: query.id },
         callback: res => {
           if (res.ret === 1) {
             let initFormVal = localStorage.getItem('initFormVal' + query?.id);
             console.log('initFormVal', JSON.parse(initFormVal));
             let cacheFormVal = {};
             
             if (initFormVal) {
               //initFormVal赋值给cacheFormVal,此处省略
            }
             setPricingInfo({
               ...cacheFormVal,
               ...res.data
            });
          }  

    发现有一个console.log(),JSON.parse('')会是什么?报错,果然,查异常上报日志的时候,也查到这个错误,真是一失足成千古恨,当时只是为了方便查看,打印了一下缓存数据,没想到是这个地方出现的问题 Uncaught SyntaxError: Unexpected end of JSON input


    image-20240124142222982


    JSON.parse


    那问题来了 json.parse什么情况会报错呢?通过查阅MDN


    image-20240124143007732


    那么,什么是规范的JSON格式呢?我们此处再去查阅MDN


    此处只列出了json的结构 很显然,传入null 是合法的,但是传入空字符是不合法的,


    JSON = null
       or true or false
       or JSONNumber
       or JSONString
       or JSONObject
       or JSONArray

    吐槽


    可能有人要吐槽,直接写JSON存储的时候格式不对不就行了吗?干什么这那么多,又是异常上报,又是贴代码?又是贴MDN的。


    我在这里回答一下之所以这么写一是为了记录出错的时候出现的问题,方便下次出现类似问题能够即时复盘。


    二是希望贴出自己的排错方式,新手若有不明白的可以模仿这个方式得到一些启发和思考,高手也可指出我的问题,共同成长


    同样我也希望大家遇到问题的时候要记得查文档,查文档再查文档,自己遇到的问题,先文档,是不是自己理解错了,如果还不行就去stackoverflow,如果再不济就去github issue看看是否有相同的问题是不是作者的bug,如果都没有,那么好了,这个问题几乎解决不了了,此时有两个选择,要么产品接受,要么 那我走???


    作者:傲娇的腾小三
    来源:juejin.cn/post/7327227246618476583
    收起阅读 »

    相见恨晚的前端开发利器-PageSpy

    web
    今天介绍一个非常有用的前端开发工具。 做前端开发的你,一定有过以下经历: 我这里是好的啊,你截个图给我看看 不会吧,你打开f12,控制台截个图给我看看 录个屏给我看看你是怎么操作的 ... 还有,我们在开发h5的时候,一般为了调试方便,可能会在开发环境和测...
    继续阅读 »

    今天介绍一个非常有用的前端开发工具。


    做前端开发的你,一定有过以下经历:



    1. 我这里是好的啊,你截个图给我看看

    2. 不会吧,你打开f12,控制台截个图给我看看

    3. 录个屏给我看看你是怎么操作的

    4. ...


    还有,我们在开发h5的时候,一般为了调试方便,可能会在开发环境和测试环境实例化一个vConsole,遇到问题看一下大概就能定位到错误。


    可是如果测试小姐姐在远程呢?如果是线上环境呢?如果有这么一个工具,能让我坐在工位上就能调(窥)试(探)用户的操作,那岂不是美滋滋。


    你可能会说,这不就是埋点吗,先别急,今天介绍的这个工具和埋点有着本质区别。


    不啰嗦了,有请主角**「PageSpy」**登场。


    PageSpy是什么?




    PageSpy[1] 是由货拉拉大前端开源的一款用于调试 H5 、或者远程 Web 项目的工具。是一个强大的开源前端远程调试平台,它可以显著提高我们在面对前端问题时的效率。




    有什么作用?



    • 一眼查看客户端信息 能识别客户端运行环境,支持Linux/Mac/Window/IOS/Android

    • 实时查看输出 可以实时输出客户端的Element,Console,Network,Storage

    • 网络请求监控 可以捕获和显示页面的网络请求

    • 远程控制台 支持远程调试客户机上的js代码


    如何使用?


    查看官方文档[2]



    1. 安装npm包


    yarn global add @huolala-tech/page-spy-api

    # 如果你使用 npm

    npm install -@huolala-tech/page-spy-api


    1. 启动服务


    直接在命令行执行page-spy-api,部署完成后浏览器访问:6752,页面顶部会出现接入SDK菜单,点击菜单查看如何在业务项目中配置并集成。图片命令行执行后出现这个界面表示服务启动成功了,然后访问我自己的ip+端口,再点击顶部接入SDK图片去创建一个测试项目,建一个最简单的index.html,按照文档接入SDK,然后在浏览器访问这个页面图片图片左下角出现Pagepy的logo说明引入成功了。


    此时点击顶部菜单房间列表图片点击调试,就可以看到这个项目的一些实时调试信息,但是还没加什么代码。图片现在改一下我们的代码,加一些输出信息。图片Console控制台的信息图片直接输出用户端代码变量的实时的值图片加个定时器试试,也是实时输出的图片图片再来看看Storage信息图片图片Element信息图片调个接口试试图片图片图片


    好了,今天的介绍就到这里,这么牛叉的工具,是不是有种相见恨晚的感觉,感兴趣的小伙伴快去试试吧!


    Reference


    [1] PageSpy:huolalatech.github.io/page-spy-we…


    [2] 官方文档:github.com/HuolalaTech…


    作者:丝绒拿铁有点甜
    来源:juejin.cn/post/7327691403844665380
    收起阅读 »

    uniapp云开发--微信登录

    web
    前言 我们主要使用uniapp + uniCloud 实现小程序登录,并将用户数据存入云数据库。 小程序 wx.getUserProfile 调整,接口将被收回 详情,所以需要用户自己填写资料。 注:填写个人资料是一个组件,覆盖在登录之上而已,还是在同一个页面...
    继续阅读 »

    前言


    我们主要使用uniapp + uniCloud 实现小程序登录,并将用户数据存入云数据库。


    小程序 wx.getUserProfile 调整,接口将被收回 详情,所以需要用户自己填写资料。


    注:填写个人资料是一个组件,覆盖在登录之上而已,还是在同一个页面



    uniCloud


    创建 uniapp + uniCloud 项目,创建云数据库 数据表 uniCloud传送门


    开始


    创建项目


    39d23acf47b440e2880f5ccadc1417f9~tplv-k3u1fbpfcp-zoom-in-crop-mark_4536_0_0_0.webp


    关联云服务空间




    创建云数据库 数据表


    不使用模版,输入名称直接创建即可。



    编辑表结构,想了解更多可以去看云数据库 DB Schema 数据结构文档 传送门


    {
    "bsonType": "object",
    "required": [],
    "permission": {
    "read": true,
    "create": true,
    "update": true,
    "delete": false
    },
    "properties": {
    "_id": {
    "description": "ID,系统自动生成"
    },
    "nickName": {
    "bsonType": "string",
    "label": "昵称",
    "description": "用户昵称,登录获取的"
    },
    "avatarUrl": {
    "bsonType": "string",
    "label": "头像",
    "description": "用户头像图片的 URL,登录获取的"
    },
    "gender": {
    "bsonType": "number",
    "label": "性别",
    "description": "用户性别,1: 男;2: 女"
    },
    "personalize": {
    "bsonType": "string",
    "label": "个性签名",
    "description": "个性签名,编辑资料获取"
    },
    "background": {
    "bsonType": "object",
    "label": "个人中心背景图",
    "description": "个人中心背景图,编辑资料获取"
    },
    "mp_wx_openid": {
    "bsonType": "string",
    "description": "微信小程序平台openid"
    },
    "register_date": {
    "bsonType": "timestamp",
    "description": "注册时间",
    "forceDefaultValue": {
    "$env": "now"
    }
    }
    }
    }

    创建云函数




    云函数代码


    云函数 将 uni.login 取得的 code 获取到用户 session, 并对 数据库进行 增加、修改、查询 操作,第一次注册必须用户主动填写用户资料。


    对云数据库的相关操作 传送门


    'use strict';

    //小程序的AppID 和 AppSecret
    const mp_wx_data = {AppID: '************', AppSecret: '***********************'}

    //event为客户端上传的参数
    exports.main = async (event, context) => {

    //使用云数据库
    const db = uniCloud.database();
    // 获取 `users` 集合的引用
    const pro_user = db.collection('users');
    // 通过 action 判断请求对象

    let result = {};
    switch (event.action) {
    // 通过 code 获取用户 session
    case 'code2Session':
    const res_session = await uniCloud.httpclient.request('https://api.weixin.qq.com/sns/jscode2session', {
    method: 'GET', data: {
    appid: mp_wx_data.AppID,
    secret: mp_wx_data.AppSecret,
    js_code: event.js_code,
    grant_type: 'authorization_code'
    }, dataType: 'json'
    }
    )
    const success = res_session.status === 200 && res_session.data && res_session.data.openid
    if (!success) {
    return {
    status: -2, msg: '从微信获取登录信息失败'
    }
    }

    //从数据库查找是否已注册过
    const res_user = await pro_user.where({
    mp_wx_openid: res_session.data.openid
    }).get()
    // 没有用户信息,进入注册
    if (res_user.data && res_user.data.length === 0) {
    //event.user_info 用户信息
    if (event.user_info) {
    //有信息则进入注册,向数据库写入数据
    const register = await uniCloud.callFunction({
    name: 'user',
    data: {
    action: 'register',
    open_id: res_session.data.openid,
    user_info: event.user_info
    }
    }).then(res => {
    result = res
    })
    } else {
    //没有信息返回{register: true}
    result = {
    result: {
    result: {register: true}
    }
    }
    }
    } else {
    result = {
    result: {
    result: res_user.data[0]
    }
    }
    }
    break;
    //注册 向数据库写入数据
    case 'register':
    const res_reg = await pro_user.add({
    nickName: event.user_info.nickName,
    avatarUrl: event.user_info.avatarUrl,
    gender: event.user_info.gender,
    mp_wx_openid: event.open_id,
    register_date: new Date().getTime()
    })
    if (res_reg.id) {
    const res_reg_val = await uniCloud.callFunction({
    name: 'user', data: {
    action: 'getUser', open_id: event.open_id
    }
    }).then(res => {
    result = res
    })
    } else {
    result = {
    status: -1, msg: '微信登录'
    }
    }
    break;
    case 'update':
    if (event._id && event.info) {
    const res_update = await pro_user.doc(event._id).update(event.info)
    if (res_update.updated >= 0) {
    result = {status: 200, msg: '修改成功'}
    } else {
    result = {status: -1, msg: '修改失败'}
    }
    } else {
    result = {status: -1, msg: '修改失败'}
    }
    break;
    case 'getUser':
    const res_val = await pro_user.where({
    mp_wx_openid: event.open_id
    }).get()
    return res_val.data[0]
    break;
    }
    return result;
    };

    微信登录操作


    如上面所说,用户需手动上传资料,对于用户头像我们需要上传至云储存。


    上传用户头像


    上传图片函数参数为微信本地图片路径,我们对路径用/进行分割,取最后的图片名称进行上传


    /**
    * 上传图片至云存储
    */

    export async function uploadImage(url) {
    const fileName = url.split('/')
    return new Promise(resolve => {
    uniCloud.uploadFile({
    filePath: url,
    cloudPath: fileName[fileName.length - 1],
    success(res) {
    resolve(res)
    },
    fail() {
    uni.showToast({
    title: '图片上传失败!',
    icon: 'none'
    })
    resolve(false)
    }
    })
    })
    }

    登录函数


    如果用户第一次上传资料,我们需要先上传头像并取得图片链接,再将用户资料写入数据库。


    async wxLogin() {
    if (this.userInfo && this.userInfo.avatarUrl) {
    uni.showLoading({
    title: '正在上传图片...',
    mask: true
    });
    //上传头像至云储存并返回图片链接
    const imageUrl = await uploadImage(this.userInfo.avatarUrl)
    if (!imageUrl) {
    return
    }
    this.userInfo = {...this.userInfo, avatarUrl: imageUrl.fileID}
    }
    uni.showLoading({
    title: '登陆中...',
    mask: true
    });
    const _this = this
    uni.login({
    provider: 'weixin',
    success: (res) => {
    if (res.code) {
    //取得code并调用云函数
    uniCloud.callFunction({
    name: 'user',
    data: {
    action: 'code2Session',
    js_code: res.code,
    user_info: _this.userInfo
    },
    success: (res) => {
    //如register为true,用户未填写资料
    if (res.result.result.result.register) {
    //_this.showUserInfo 显示填写资料组件
    _this.showUserInfo = true
    uni.hideLoading();
    return
    }
    if (res.result.result.result._id) {
    const data = {
    _id: res.result.result.result._id,
    mp_wx_openid: res.result.result.result.mp_wx_openid,
    register_date: res.result.result.result.register_date
    }
    this.loginSuccess(data)
    }
    },
    fail: () => {
    this.loginFail()
    }
    })
    }
    }
    })
    },

    登录成功与失败


    在用户登录成功后将数据存入 Storage 中,添加登录过期时间,我这里设置的是七天的登录有效期。


    loginSuccess(data) {
    updateTokenStorage(data)
    updateIsLoginStorage(true)
    uni.showToast({
    title: '登陆成功!',
    icon: 'none'
    });
    uni.navigateBack()
    },

    将用户数据存入 Storage,并设置过期时间 expiresTime


    export function updateTokenStorage(data = null) {
    if (data) {
    const expiresTime = new Date().getTime() + 7 * 24 * 60 * 60 * 1000
    data = {...data, expiresTime: expiresTime}
    }
    uni.setStorageSync('user', data)
    }

    isLogin 用于判断是否是否登录


    export function updateIsLoginStorage(data = null) {
    uni.setStorageSync('isLogin', data)
    }

    登录失败


    loginFail() {
    updateTokenStorage()
    updateIsLoginStorage()
    uni.showToast({
    title: '登陆失败!',
    icon: 'none'
    });
    }

    判断是否登录


    除了判断 isLogin 还要判断 expiresTime 是否登录过期


    //判断是否登陆
    export function isLogin() {
    try {
    const user = uni.getStorageSync('user')
    const isLogin = uni.getStorageSync('isLogin')
    const nowTime = new Date().getTime()
    return !!(isLogin && user && user._id && user.expiresTime > nowTime);
    } catch (error) {

    }
    }

    最后


    至此就实现了微信登录并将用户信息存入数据库中,我们也可以通过云函数获取用户数据,做出用户个人主页。



    以上是我做个人小程序时用的登录流程,整个小程序项目已上传至 GitHub。


    GitHub地址


    小程序码



    作者:Biao
    来源:juejin.cn/post/7264592481592705076
    收起阅读 »

    真的不考虑下grid布局?有时候真的很方便!

    web
    前言 flex布局大家应该已经运用的炉火纯青了,相信在日常开发中大家和我一样不管遇到什么都是flex一把搜哈。直到我遇到grid,才发现有些场景下,不是说flex实现不了而是使用grid能够更加轻松的完成任务。下面拿几个场景和大家分享一下。 宫格类的布局 比如...
    继续阅读 »

    前言


    flex布局大家应该已经运用的炉火纯青了,相信在日常开发中大家和我一样不管遇到什么都是flex一把搜哈。直到我遇到grid,才发现有些场景下,不是说flex实现不了而是使用grid能够更加轻松的完成任务。下面拿几个场景和大家分享一下。


    宫格类的布局


    比如我要实现一个布局,最外层元素的宽度是1000px,高度自适应。子元素宽度为300px,一行展示3个,从左到右排列。其中最左边与最右边的元素需要紧挨父元素的左右边框。如下图所示:



    使用flex实现


    这个页面布局在日常开发中非常常见,考虑下使用flex布局如何实现,横向排列元素,固定宽度300,wrap设置换行显示,设置双端对齐。看起来很简单,来实现一下。


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

    <head>
    <style>
    .box{
    width: 1000px;
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
    gap: 10px;
    }
    .item{
    background: pink;
    width: 300px;
    height: 150px;
    }
    </style>
    </head>

    <body>
    <div class="box">
    <div class="item">1</div>
    <div class="item">2</div>
    <div class="item">3</div>
    <div class="item">4</div>
    <div class="item">5</div>
    <div class="item">6</div>
    <div class="item">7</div>
    <div class="item">8</div>
    </div>
    </body>

    </html>


    实现之后发现了问题,由于我们设置了双端对齐导致,当最后一行的个数不足三个时,页面展示的效果和我们预期的效果有出入。使用flex实现这个效果就要对这个问题进行额外的处理。


    处理的方式有很多种,最常见的处理方式是在元素后面添加空元素,使其成为3的倍数即可。其实这里添加空元素的个数没有限制,因为空元素不会展示到页面上,即使添加100个空元素用户也是感知不到的。个人觉得这并不是一个好办法,在实际处理的时候可能还会遇到别的问题。个人觉得还是把flex下的子元素设置成百分比好一点。


    使用grid实现


    面对这种布局使用grid是非常方便的,设置3列,每列300px,剩下的元素让它自己往下排即可。几行代码轻松实现该效果,不需要flex那样额外的处理。


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

    <head>
    <style>
    .box {
    display: grid;
    grid-template-columns: repeat(3, 300px);
    justify-content: space-between;
    gap: 10px;
    width: 1000px;
    }

    .item {
    background: pink;
    height: 100px;
    }
    </style>
    </head>

    <body>
    <div class="box">
    <div class="item">1</div>
    <div class="item">2</div>
    <div class="item">3</div>
    <div class="item">4</div>
    <div class="item">5</div>
    <div class="item">6</div>
    <div class="item">7</div>
    <div class="item">8</div>

    </div>
    </body>

    </html>


    实现后台管理布局



    这种后台管理的布局,使用flex实现当然也没有问题。首先需要纵向排列红色的两个div,然后再横向的排列蓝色的两个div,最后再纵向的排列绿色的两个div实现布局。达到效果是没有问题的,但是实现起来较为繁琐,而且需要很多额外的标签嵌套。



    由于grid是二维的,所以它不需要额外的标签嵌套。html里面结构清晰,如果需要改变页面结构,只需要改变container的样式就可以了,不需要对html进行修改。


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

    <head>
    <style>
    .container {
    display: grid;
    grid-template-columns: 250px 1fr;
    grid-template-rows: 100px 1fr 100px;
    grid-template-areas:
    'header header'
    'aside main'
    'aside footer';
    height: 100vh;
    }

    .header {
    grid-area: header;
    background: #b3c0d1;
    }

    .aside {
    grid-area: aside;
    background: #d3dce6;
    }

    .main {
    grid-area: main;
    background: #e9eef3;
    }

    .footer {
    grid-area: footer;
    background: #b3c0d1;
    }
    </style>
    </head>

    <body>
    <div class="container">
    <div class="header">Header</div>
    <div class="aside">Aside</div>
    <div class="main">Main</div>
    <div class="footer">Footer</div>
    </div>
    </body>

    </html>

    实现响应式布局


    借助grid的auto-fillminmax函数可以实现类似响应式布局的效果,可以应用在后台管理的表单布局等场景。



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

    <head>
    <style>
    .box {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
    justify-content: space-between;
    gap: 10px;
    }

    .item {
    background: pink;
    height: 100px;
    }
    </style>
    </head>

    <body>
    <div class="box">
    <div class="item">1</div>
    <div class="item">2</div>
    <div class="item">3</div>
    <div class="item">4</div>
    <div class="item">5</div>
    <div class="item">6</div>
    <div class="item">7</div>
    <div class="item">8</div>
    </div>
    </body>

    </html>

    兼容性对比


    flex的兼容性


    image.png


    grid的兼容性


    image.png


    可以看到grid在兼容性上还是不如flex,grid虽然强大,但是在使用前还是需要先考虑一下项目的用户群体。


    结尾


    除了上述场景外肯定还有许多场景适合使用grid来完成。gridflex都是强大的布局方式,它们并没有明显的优劣之分。关键在于掌握这两种方法,并在开发中根据实际情况选择最合适的方案。


    希望大家能有所收获!


    作者:欲买炸鸡同载可乐
    来源:juejin.cn/post/7326816030042669110
    收起阅读 »