注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

一个排查了一天的BUG,你在摸鱼🐟吧!

web
站会 在一次日常站会上,组员们轮流分享昨天的工作进展。一个组员提到:“昨天我整天都在排查一个BUG,今天还得继续。” 出于好奇,我问:“是什么BUG让你排查了这么久还没解决呢?” 他解释说:“是关于一个数据选择弹窗的问题。这个弹窗用表格展示数据,并且表格具有选...
继续阅读 »

站会


在一次日常站会上,组员们轮流分享昨天的工作进展。一个组员提到:“昨天我整天都在排查一个BUG,今天还得继续。”


出于好奇,我问:“是什么BUG让你排查了这么久还没解决呢?”


他解释说:“是关于一个数据选择弹窗的问题。这个弹窗用表格展示数据,并且表格具有选择功能。问题在于,编辑这个弹窗时,表格中原本应该显示为已选状态的数据并没有正确显示已选状态。”


我猜测道:“是不是因为表格中数据的主键ID是大数值导致的?”


他回答说:“大数值?我不太确定。”


我有些质疑地问:“那你昨天都是怎么排查的?需要花一整天的时间,难道是在摸鱼吗?”


“没有摸鱼,只是这个BUG真得有点难搞,那个什么是大数值?”


“行吧,姑且信你,我待会给你看看。”


排查


表格使用的是 Ant Design 4.0 提供的 Table 组件。我检查了组件的 rowKey 属性配置,如下所示:


<Table rowKey={record => record.obj_id}></Table>

这表明表格行的 key 是通过数据中的 obj_id 字段来指定的。随后,我进一步查看了服务端返回的数据。


image.png

可以看到一条数据中的 obj_id 字段值为 "898186844400803840",这是一个18位的数值。



在ES6(ECMAScript 2015)之前,JavaScript没有专门的整数类型,所有的数字都被表示为双精度64位浮点数(遵循IEEE 754标准)。这意味着在这种情况下,JavaScript能够安全地表示的整数范围是从253+1-2^{53} + 125312^{53} - 1(即-9,007,199,254,740,991到9,007,199,254,740,991)。可以简单地认为超过16位的数值就是大数值。



JavaScript中很多操作处理大数值时会导致大数值失去精度。比如 Number("898186844400803840")


image.png


可以看到 "898186844400803840""898186844400803800" 的区别在第16位后,从 40 变成 00 这就是大数值失去精度的表现。


在看一下表格的数据展示,如下图所示:


image.png


可以确定的是,从服务端返回的数据到在表格中的渲染过程是没有问题的。那么,可能出现问题的地方还有两个:一是在选择数据后,数据被传递到父组件的过程中;二是父组件将已选数据发送回选择数据组件的过程中。


定位


我检查了他将数据传递给父组件的逻辑代码,发现了一个可疑点。


image.png

在上述代码中,JSON.parse 被用来转换数据中的每个值。在这个转换过程中,如果 item[key] 是以字符串形式出现的数值,并且这个字符串能够被 JSON.parse() 解析为 JSON 中的数值类型,那么 JSON.parse() 将会把它转换为 JavaScript 的 Number 类型。


这种转换过程中可能会出现精度丢失的问题。因为一旦字符串表示的数值的位数超过16位后,在转换为 Number 类型时就无法保证其精度完整无损。


解决


我们通过正则表达式排除了这种情况,如下所示:


newItem[key] = typeof item[key] === 'string' && /^\d{16,}$/.test(item[key]) ? 
item[key] :
JSON.parse(item[key]);

经过修改并重新验证,问题得到了解决,数据选择弹窗现在可以正确展示已选择状态。


image.png


反思


这个表面上不起眼的BUG为何花费了如此长的时间来排查?除了对大数值的概念不甚了解外,还有一个关键原因是对JavaScript中可能导致大数值失去精度的操作缺乏深入理解。


大数值通常由两种表示方式,一个是用数值类型表示,一个是字符串类型表示。


如果用数值类型表示一个大数值,而且你不能直接修改源代码或源数据,这种情况比较棘手,因为一旦 JavaScript 解析器处理这个数值,它可能已经失去了精度。


这种情况通常发生在你从某个源(比如一个API或者外部数据文件)接收到一个数值类型的大数值,如果数据源头不能修改,只能使用第三方库lossless-json、json-bigint来解决。


如果用字符串类型表示一个大数值,在JS中只要有把其转成Number类型的值就会失去精度,不管是显式转换还是隐式转换。


显式转换,比如 Number()parseInt()parseFloat()Math.floorMath.ceilMath.round等等。


隐式转换,比如除了加法外的算术运算符、JSON.parseswitch 语句、sort的回调函数等等。


作者:前端大骆
来源:juejin.cn/post/7348712837849284644
收起阅读 »

Node拒绝当咸鱼,Node 22大进步

web
这几年,deno和bun风头正盛,大有你方唱罢我登场的态势,deno和bun的每一次更新版本,Node都会被拿来比较,比较结果总是Node落后了。 这种比较是不是非常熟悉,就像卖手机的跟iPhone比,卖汽车的跟特斯拉比,比较的时候有时候还得来个「比一分钱硬币...
继续阅读 »

这几年,deno和bun风头正盛,大有你方唱罢我登场的态势,deno和bun的每一次更新版本,Node都会被拿来比较,比较结果总是Node落后了。


这种比较是不是非常熟悉,就像卖手机的跟iPhone比,卖汽车的跟特斯拉比,比较的时候有时候还得来个「比一分钱硬币还薄」的套路。


1.png


Node虽然没有落后了,但是确实有点压力了,所以20和22版本都大跨步前进,拒绝当咸鱼了。


因为Node官网对22版本特性的介绍太过简单,所以我决定来一篇详细介绍新特性的文章,让学习Node的朋友们知道,Node现在在第几层。


首先我把新特性分为两类,分别是:开发者可能直接用到的特性、开发者相对无感知的底层更新。本文重点介绍前者,简单介绍后者。先来一个概览:


开发者可能直接用到的特性:



  1. 支持通过 require() 引入ESM

  2. 运行 package.json 中的脚本

  3. 监视模式(--watch)稳定化

  4. 内置 WebSocket 客户端

  5. 增加流的默认高水位线

  6. 文件模式匹配功能


开发者相对无感知的底层更新:



  1. V8 引擎升级至 12.4 版本

  2. Maglev 编译器默认启用

  3. 改进 AbortSignal 的创建性能


接下来开始介绍。


支持通过 require() 导入 ESM


以前,我们认为 CommonJS 与 ESM 是分离的。


例如,在 CommonJS里,我们用并使用 module.exports 导出模块,用 require() 导入模块:


// CommonJS

// math.js
function add(a, b) {
return a + b;
}
module.exports.add = add;

// useMath.js
const math = require('./math');
console.log(math.add(2, 3));

在 ECMAScript Modules (ESM) **** 里,我们使用 export 导出模块,用 import 导入模块:


// ESM

// math.mjs
export function add(a, b) {
return a + b;
}

// useMath.js
import { add } from './math.mjs';
console.log(add(2, 3));

Node 22 支持新的方式——用 require() 导入 ESM:


// Node 22

// math.mjs
export function add(a, b) {
return a + b;
}

// useMath.js
const { add } = require('./mathModule.mjs');
console.log(add(2, 3));

这么设计的原因是为了给大型项目和遗留系统提供一个平滑过渡的方案,因为这类项目难以快速全部迁移到 ESM,通过允许 require() 导入 ESM,开发者就可以逐个模块迁移,而不是一次性对整个项目进行修改。


目前这种写法还是实验性功能,所以使用是有“门槛”的:



  • 启动命令需要添加 -experimental-require-module 参数,如:node --experimental-require-module app.js

  • 模块标记:确保 ESM 模块通过 package.json 中的 "type": "module" 或文件扩展名是 .mjs

  • 完全同步:只有完全同步的ESM才能被 require() 导入,任何含有顶级 await 的ESM都不能使用这种方式加载。


运行package.json中的脚本


假设我们的 package.json 里有一个脚本:


"scripts": {
"test": "jest"
}

在此之前,我们必须依赖 npm 或者 yanr 这样的包管理器来执行命令,比如:npm run test


Node 22 添加了一个新命令行标志 --run,允许直接从命令行执行 package.json 中定义的脚本,可以直接使用 node --run test 这样的命令来运行脚本。


刚开始我还疑惑这是不是脱裤子放屁的行为,因为有 node 的地方一般都有 npm,我要这 node —run 有何用?


后来思考了一下,主要原因应该还是统一运行环境和提升性能。不同的包管理器在处理脚本时可能会有微小的差异,Node 提供一个标准化的方式执行脚本,有助于统一这些行为;而且直接使用 node 执行脚本要比通过 npm 执行脚本更快,因为绕过了 npm 这个中间层。


监视模式(--watch)稳定化


在 19 版本里,Node 引入了 —watch 指令,用于监视文件系统的变动,并自动重启。22 版本开始,这个指令成为稳定功能了。


要启用监视模式,只需要在启动 Node 应用时加上 --watch ****参数。例如:


node --watch app.js

正在用 nodemon 做自动重启的朋友们可以正式转战 --watch 了~


内置 WebSocket 客户端


以前,要用 Node 开发一个 socket 服务,必须使用 ws、socket.io 这样的第三方库来实现。第三方库虽然稳如老狗帮助开发者许多年,但是终究是有点不方便。


Node 22 正式内置了 WebSocket,并且属于稳定功能,不再需要 -experimental-websocket 来启用了。


除此之外,WebScoket 的实现还遵循了浏览器中 WebSocket API 的标准,这意味着在 Node 中使用 WebSocket 的方式将与在 JavaScript 中使用 WebSocket 的方式非常相似,有助于减少学习成本并提高代码的一致性。


用法示例:


const socket = new WebSocket("ws://localhost:8080");

socket.addEventListener("open", (event) => {
socket.send("Hello Server!");
});

增加流(streams)的默认高水位线(High Water Mark)


streams 在 Node 中有举足轻重的作用,读写数据都得要 streams 来完成。而 streams 可以设置 highWaterMark 参数,用于表示缓冲区的大小。highWaterMark 越大,缓冲区越大,占用内存越多,I/O 操作就减少,highWaterMark 越小,其他信息也对应相反。


用法如下:


const fs = require('fs');

const readStream = fs.createReadStream('example-large-file.txt', {
highWaterMark: 1024 * 1024 // 设置高水位线为1MB
});

readStream.on('data', (chunk) => {
console.log(`Received chunk of size: ${chunk.length}`);
});

readStream.on('end', () => {
console.log('End of file has been reached.');
});

虽然 highWaterMark 是可配置的,但通常情况下,我们是使用默认值。在以前的版本里,highWaterMark 的默认值是 16k,Node 22 版本开始,默认值被提升到 64k 了。


文件模式匹配——glob 和 globSync


Node 22 版本在 fs 模块中新增了 globglobSync 函数,它们用于根据指定模式匹配文件路径。


文件模式匹配允许开发者定义一个匹配模式,以找出符合特定规则的文件路径集合。模式定义通常包括通配符,如 *(匹配任何字符)和 ?(匹配单个字符),以及其他特定的模式字符。


glob 函数(异步)


glob 函数是一个异步的函数,它不会阻塞 Node.js 的事件循环。这意味着它在搜索文件时不会停止其他代码的执行。glob 函数的基本用法如下:


const { glob } = require('fs');

glob('**/*.js', (err, files) => {
if (err) {
throw err;
}
console.log(files); // 输出所有匹配的.js文件路径
});

在这个示例中,glob 函数用来查找所有子目录中以 .js 结尾的文件。它接受两个参数:



  • 第一个参数是一个字符串,表示文件匹配模式。

  • 第二个参数是一个回调函数,当文件搜索完成后,这个函数会被调用。如果搜索成功,err 将为 null,而 files 将包含一个包含所有匹配文件路径的数组。


globSync 函数(同步)


globSyncglob 的同步版本,它会阻塞事件循环,直到所有匹配的文件都被找到。这使得代码更简单,但在处理大量文件或在需要高响应性的应用中可能会导致性能问题。其基本用法如下:


const { globSync } = require('fs');

const files = globSync('**/*.js');
console.log(files); // 同样输出所有匹配的.js文件路径

这个函数直接返回匹配的文件数组,适用于脚本和简单的应用,其中执行速度不是主要关注点。


使用场景


这两个函数适用于:



  • 自动化构建过程,如自动寻找和处理项目中的 JavaScript 文件。

  • 开发工具和脚本,需要对项目目录中的文件进行批量操作。

  • 任何需要从大量文件中快速筛选出符合特定模式的文件集的应用。


V8 引擎升级至 12.4 版本


从这一节开始,我们了解一下开发者相对无感知的底层更新,第一个就是 V8 引擎升级到 12.4 版本了,有了以下特性升级:



  • WebAssembly 垃圾回收:这一特性将改善 WebAssembly 在内存管理方面的能力。

  • Array.fromAsync:这个新方法允许从异步迭代器创建数组。

  • Set 方法和迭代器帮助程序:提供了更多内建的Set操作和迭代器操作的方法,增强了数据结构的操作性和灵活性。


Maglev 编译器默认启用


Maglev 是 V8 的新编译器,现在在支持的架构上默认启用。它主要针对短生命周期的命令行程序(CLI程序)性能进行优化,通过改进JIT(即时编译)的效率来提升性能。这对开发者编写的工具和脚本将带来明显的速度提升。


改进AbortSignal的创建性能


在这次更新中,Node 提高了 AbortSignal 实例的创建效率。AbortSignal 是用于中断正在进行的操作(如网络请求或任何长时间运行的异步任务)的一种机制。通过提升这一过程的效率,可以加快任何依赖这一功能的应用,如使用 fetch 进行HTTP请求或在测试运行器中处理中断的场景。


AbortSignal 的工作方式是通过 AbortController 实例来管理。AbortController 提供一个 signal 属性和一个 abort() 方法。signal 属性返回一个 AbortSignal 对象,可以传递给任何接受 AbortSignal 的API(如fetch)来监听取消事件。当调用abort()方法时,与该控制器关联的所有操作将被取消。


const controller = new AbortController();
const signal = controller.signal;

fetch(url, { signal })
.then(response => response.json())
.catch(err => {
if (err.name === 'AbortError') {
console.log('Fetch aborted');
} else {
console.error('Fetch error:', err);
}
});

// 取消请求
controller.abort();

总结


最后,我只替 Node 说一句:Node 没有这么容易被 deno 和 bun 打败~


3.jpeg


关于我


全栈工程师,Next.js 开源手艺人,AI降临派。


今年致力于 Next.js 和 Node.js 领域的开源项目开发和知识分享。




作者:程普
来源:juejin.cn/post/7366185272768036883
收起阅读 »

Vue3 新项目,没必要再用 Pinia 了!

web
最近弄了一个新的 Vue3 项目,页面不多,其中有三四个页面需要共享状态,我几乎条件反射般地安装了 Pinia 来做状态管理。后来一想,我只需要一个仓库,存放几个状态而已,有必要单独接一套 Pinia 吗?其实不需要,我差点忘记了 Vue3...
继续阅读 »

最近弄了一个新的 Vue3 项目,页面不多,其中有三四个页面需要共享状态,我几乎条件反射般地安装了 Pinia 来做状态管理。

后来一想,我只需要一个仓库,存放几个状态而已,有必要单独接一套 Pinia 吗?

其实不需要,我差点忘记了 Vue3 的一个重要特性,那就是 组合式函数

组合式 API 大家都知道,组合式函数可能大家没有特别留意。但是它功能强大,足矣实现全局状态管理。

组合式函数

什么是组合式函数?以下是官网介绍:

在 Vue 应用的概念中,“组合式函数”(Composables) 是一个利用 Vue 的组合式 API 来封装和复用有状态逻辑的函数。

从这段介绍中可以看出,组合式函数要满足两个关键点:

  1. 组合式 API。
  2. 有状态逻辑的函数。

在 Vue 组件中,状态通常定义在组件内部。比如典型的选项式 API,状态定义在组件的 data() 方法下,因此这个状态只能在组件内使用。

Vue3 出现之后,有了组合式 API。但对于大部分人来说,只是定义状态的方式从 data()变成了 ref(),貌似没有多大的区别。

实际上,区别大了去了。

组合式 API 提供的 ref() 等方法,不是只可以在 Vue 组件内使用,而是在任意 JS 文件中都可以使用。

这就意味着,组合式 API 可以将 组件与状态分离,状态可以定义在组件之外,并在组件中使用。当我们使用组合式 API 定义了一个有状态的函数,这就是组合式函数。

因此,组合式函数,完全可以实现全局状态管理。

举个例子:假设将用户信息状态定义在一个组合式函数中,方法如下:

// user.js
import { ref } from 'vue'

export function useUinfo() {
// 用户信息
const user_info = ref(null)
// 修改信息
const setUserInfo = (data) => {
user_info.value = data
}
return { user_info, setUserInfo }
}

代码中的 useUinfo() 就是一个组合式函数,里面使用 ref() 定义了状态,并将状态和方法抛出。

在 Vue3 组件之中,我们就可以导入并使用这个状态:


仔细看组合式函数的使用方法,像不像 React 中的 Hook?完全可以将它看作一个 Hook。

在多个组件中使用上述方法导入状态,跨组件的状态管理方式也就实现了。

模块化的使用方法

组合式函数在多个组件中调用,可能会出现重复创建状态的问题。其实我们可以用模块化的方法,更简单。

将上方 user.js 文件中的组合式函数去掉,改造如下:

import { ref } from 'vue'

// 用户信息
export const user_info = ref(null)
// 修改信息
export const setUserInfo = (data) => {
user_info.value = data
}

这样在组件中使用时,直接导入即可:


经过测试,这种方式是可以的。

使用模块化的方法,也就是一个文件定义一组状态,可以看作是 Pinia 的仓库。这样状态模块化的问题也解决了。

Pinia 中最常用的功能还有 getters,基于某个状态动态计算的另一个状态。在组合式函数中用计算属性完全可以实现。

import { ref, computed } from 'vue'

export const num1 = ref(3)

export const num2 = computed(()=> {
return num1 * num1
}

所以思考一下,对于使用 Vue3 组合式 API 开发的项目,是不是完全可以用组合式函数来替代状态管理(Pinia,Vuex)呢?

当然,以上方案仅适用于组合式 API 开发的普通项目。对于选项式 API 开发的项目,或者需要 SSR,还是乖乖用 Pinia 吧 ~

最重要的是!如果面试官问你:除了 Pinia 和 Vuex 还有没有别的状态管理方案?

你可别说不知道,记住这几个字:组合式函数!


作者:杨成功
来源:juejin.cn/post/7348680291937435682
收起阅读 »

不容错过的秘籍:JavaScript数组的创建和使用详解

在编程的世界里,数据是构建一切的基础。而在JavaScript中,有一种特殊且强大的数据结构,它就是——数组。今天,我们就来一起探索数组的奥秘,从创建到使用,一步步掌握这个重要的工具。一、什么是数组数组(Array)是一种按顺序存储多个值的数据结构。你可以把它...
继续阅读 »

在编程的世界里,数据是构建一切的基础。而在JavaScript中,有一种特殊且强大的数据结构,它就是——数组。

今天,我们就来一起探索数组的奥秘,从创建到使用,一步步掌握这个重要的工具。

一、什么是数组

数组(Array)是一种按顺序存储多个值的数据结构。你可以把它想象成一个盒子,这个盒子可以存放多个物品,而且每个物品都有一个编号,我们可以通过这个编号来找到或者修改这个物品。

在JavaScript中,数组是一种特殊的对象,用于存储和操作多个值。与其他编程语言不同,JavaScript的数组可以同时存储不同类型的值,并且长度是动态的,可以根据需要随时添加或删除元素。

Description

JavaScript数组使用方括号([])来表示,其中的每个元素用逗号分隔。例如,以下是一个包含不同类型元素的数组的示例:

var myArray = [1, "two", true, [3, 4, 5]];

数组中的元素可以通过索引来访问和修改,索引从0开始。例如,要访问数组中的第一个元素,可以使用以下代码:

var firstElement = myArray[0];

JavaScript也提供了一些内置方法来操作数组,如push()、pop()、shift()、unshift()等,用于添加、删除和修改数组中的元素。

二、数组的作用

数组在编程中扮演着非常重要的角色。它可以帮助我们:

  • 存储多个值:我们可以在一个变量中存储多个值,而不需要为每个值创建单独的变量。

  • 操作数据:我们可以对数组中的元素进行添加、删除、修改和查找等操作。

  • 实现各种算法:通过数组,我们可以实现排序、搜索等常见算法。

  • 循环遍历:数组的元素是有序的,可以使用循环结构遍历数组的每个元素,从而对每个元素进行相同或类似的操作。这在处理大量数据时非常有用。

三、创建数组的方法

在JavaScript中,有多种方法可以创建数组,下面列出常见的三种:

1)字面量方式:

这是最常见的创建数组的方式,只需要在一对方括号[]中放入元素即可,如

var arr = [];

2)使用Array构造函数:

通过new Array()也可以创建数组,如

var arr = new Array();

3)使用Array.of()方法:

这个方法可以创建一个具有相同元素的新数组实例,如

var arr = Array.of(1, 2, 3);

四、使用数组的方法

创建了数组后,我们就可以对它进行各种操作了:

1、访问和修改数组元素

要访问和修改数组元素,需要使用数组的索引。数组的索引从0开始,依次递增。要访问数组元素,可以使用以下语法:

console.log(arr[0]); // 输出第一个元素
arr[1] = 4; // 修改第二个元素的值

2、向数组末尾添加元素

要向数组的末尾添加一个元素,可以使用数组的push()方法。该方法 会在数组的末尾添加指定的元素。以下是使用push()方法向数组末尾添加元素的示例:

arr.push(5);

3、从数组末尾移除元素

要从数组的末尾移除一个元素,可以使用数组的pop()方法。该方法 会移除并返回数组中的最后一个元素。以下是使用pop()方法从数组末尾移除元素的示例:

arr.pop();

4、从数组末尾移除元素
要从数组的末尾移除一个元素,可以使用数组的unshift()方法。该方法 会移除并返回数组中的最后一个元素。以下是使用unshift()方法从数组末尾移除元素的示例:

arr.unshift(0);

5、从数组开头移除元素
要从数组的开头移除一个元素,可以使用数组的shift()方法,并将索引值设置为0。该方法 会移除并返回数组中的第一个元素。以下是使用shift()方法从数组开头移除元素的示例:

arr.shift();

6、获取数组的长度
要获取数组的长度,可以使用内置函数length()。length()函数返回数组中元素的个数。以下是获取数组长度的示例:

console.log(arr.length);

7、遍历数组

要遍历数组的所有元素,可以使用for循环。下面是遍历数组的示例:

for (var i = 0; i < arr.length; i++) {
console.log(arr[i]);
}

8、数组排序

要对数组进行排序,可以使用JavaScript内置的sort()方法。下面是对数组进行排序的示例:

arr.sort();

9、数组反转

要对数组进行反转,可以使用JavaScript内置的reverse()方法。下面是对数组进行反转的示例:

arr.reverse();

10、数组搜索
要在数组中搜索特定的元素,可以使用循环遍历数组,逐个比较每个元素与目标值,找到目标值后返回其索引。下面是一个示例代码:

console.log(arr.indexOf(3)); // 返回3在数组中的索引位置
console.log(arr.includes(4)); // 检查数组中是否包含4

以上就是一些常见的数组操作方法,可以根据需要使用适当的方法来操作数组中的元素。

想要快速入门前端开发吗?推荐一个免费前端开发基础课程,这个老师讲的特别好,零基础学习无压力,知识点结合代码,边学边练,可以免费试看试学,还有各种辅助工具和资料,非常适合新手!点这里前往学习哦!

五、使用数组方法的注意事项

  • 数组方法是JavaScript中针对数组对象的内置方法,可以方便地对数组进行操作和处理。

  • 使用数组方法之前,需要先创建一个数组对象。可以使用数组字面量创建一个数组,也可以使用Array()构造函数来创建一个数组。

  • 数组方法可以改变原始数组,也可以返回一个新的数组。需要根据实际需求来选择使用具体的方法。

  • 改变原始数组的方法包括:push()、pop()、shift()、unshift()、splice()、sort()、reverse()等。

  • 不改变原始数组的方法包括:slice()、concat()、join()、map()、filter()、reduce()、forEach()等。

  • 使用数组方法时需要注意方法的参数和返回值,不同的方法可能需要不同的参数,并且返回值类型也可能不同。

  • 数组方法的具体用法可以参考JavaScript官方文档或者其他相关教程和资源。熟练掌握数组方法可以提高代码的效率和可读性。

以上就是JavaScript数组的创建和使用方法,希望对你有所帮助。记住,数组是JavaScript中非常重要的一部分,掌握好它可以让我们的编程工作更加高效。

收起阅读 »

你还以为前端无法操作文件吗

web
这里面有个值得说明一点的问题是,我一直以为(可能有人跟我一样)前端是无法操作文件的,可实际上自从HTML5标准出现之后,前端虽然无法像后端那样能灵活的进行文件处理,但因为有了File System Api这套接口,前端也能够进行简单的文件处理操作(不只是读,还...
继续阅读 »

这里面有个值得说明一点的问题是,我一直以为(可能有人跟我一样)前端是无法操作文件的,可实际上自从HTML5标准出现之后,前端虽然无法像后端那样能灵活的进行文件处理,但因为有了File System Api这套接口,前端也能够进行简单的文件处理操作(不只是读,还有写)。当然,网络环境鱼龙混杂,为防止不法网站任意获取和修改用户数据,所有本地文件操作都需要用户手动操作,不能自动保存或打开。

  1. 使用场景

    File System Api为浏览器应用增加了无限可能,比如我们经常用到的一些流程图工具,上面的保存到本地的功能,就不用再依赖后端,可以直接将数据保存到本地的文件系统中,下次打开时选中本地的指定文件,可以直接加载到浏览器中,大大提高的前端的能力边界。

  2. 功能描述

    我们就利用File Access Api搞一个简单的在线编辑器,能实现的功能如下:

    第一步,新建一个文件,命名为hello.txt,并填写初始信息 "hello world"

    第二步,打开文件,修改文件内容为“hello world,hello you!”

    第三步,保存文件

editfile.gif

  1. 实现方式概述

    直接看代码:

    <template>
     <div>
       <el-button type="primary" @click="editFile">编辑文件el-button>
       <el-button type="primary" @click="saveFile">保存文件el-button>
       <el-input
           type="textarea"
           :rows="20"
           placeholder="请输入内容"
           v-model="textarea">
    el-input>
     
     div>
    template>

    <script>
    export default {
       data() {
           return {
               textarea: ''
          }
      },
       methods: {
           editFile: async function() {
               // 选择文件
               let [fileHandle] = await window.showOpenFilePicker()
               // 复显文件内容
               fileHandle.getFile().then(blob => {
                   blob.text().then(val => {
                       this.textarea = val
                  })
              })
          },
           saveFile: async function() {
               // 新建一个文件
               const fileHandle = await window.showSaveFilePicker({
                   types: [
                      {
                           description: 'hello',
                           accept: {
                               'text/plain': ['.txt']
      // 对于一些非常用的后缀,均使用这种方式进行定义
                               // 参考:https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
                               // 'application/octet-stream': ['.a','.b']
                          }
                      }
                  ]
              })
               // 在文件内写入内容,写入内容用的是Stream Api,流式写入
               const writable = await fileHandle.createWritable();
               await writable.write(this.textarea);
               await writable.close();
          }
      }
    }
    script>

    可以看到,只需要短短的几行代码就可以完成本地文件的修改,需要注意的是,文件的保存不是实际意义上的修改,而是新建一个文件,进行替换,然后在新的文件里写入最新信息进行的修改。

    另:File System Api目前支持程度还不够普遍,从mdn上来看,大多数api上还有Experimental: This is an experimental technology Check the Browser compatibility table carefully before using this in production.的描述,使用前需要确认好是否满足浏览器要求。


作者:DaEar图图
来源:juejin.cn/post/7365679089811947561
收起阅读 »

这么炫酷的换肤动画,看一眼你就会爱上

web
实现过程 我们先创建下 vue 项目 npm init vite-app vue3-vite-animation 进入文件夹中 cd vue3-vite-animation 安装下依赖 npm install 启动 npm run dev 重新修改 ...
继续阅读 »

动画.gif


实现过程


我们先创建下 vue 项目


npm init vite-app vue3-vite-animation

进入文件夹中


cd vue3-vite-animation

安装下依赖


npm install

启动


npm run dev

image-20240503171537954.png


重新修改 App.vue


<template>
<div class="info-box">
<div class="change-theme-btn">改变主题</div>
<h1>Element Plus</h1>
<p>基于 Vue 3,面向设计师和开发者的组件库</p>
</div>

</template>

<script setup lang="ts">

</script>



<style>

.change-theme-btn {
width: 80px;
height: 40px;
background-color: #fff;
text-align: center;
line-height: 40px;
color: #282c34;
cursor: pointer;
border-radius: 8px;
border: 2px solid #282c34;
}

.info-box {
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
</style>


基本样式出来了,但是页面出现了滚动条,我们需要去掉原有样式


image-20240503175456039.png


src/index.css,里的所有样式都删除了,再到 index.html 中将 bodymargin 属性去掉


<body style="margin: 0;">
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>

接下来,我们来实现下换肤功能


使用 css 变量,先定义下一套黑暗主题、一套白色主题


:root {
--background-color: #fff;
--color: #282c34;
background-color: var(--background-color);
color: var(--color);
}

:root.dark {
--background-color: #282c34;
--color: #fff;
}

再定义点击事件 changeColor,点击 "改变主题" 就会改变主题颜色


classList.toggle 这个方法的第一个参数是类名,第二个参数是布尔值,表示是否添加类


如果第二个参数为 true,则添加类;如果第二个参数为 false,则移除类


<div class="change-theme-btn" @click="changeColor">改变主题</div>

/* 改变颜色 */
const changeColor = () => {
document.documentElement.classList.toggle('dark')
}

image-20240503180914393.png


按钮背景颜色、边框、字体颜色都没有改变


调整下按钮样式,把背景颜色、边框、字体颜色这些都用 css 变量代替


.change-theme-btn {
width: 80px;
height: 40px;
background-color: var(--background-color);
text-align: center;
line-height: 40px;
color: var(--color);
cursor: pointer;
border-radius: 8px;
border: 2px solid var(--color);
}

image-20240503181138545.png


这个效果不是我们想要的,需要一个过渡动画对不对


使用 startViewTransition,这个 API 会生成一个屏幕截图,将新旧屏幕截图进行替换


截图分别对应两个伪元素 ::view-transition-new(root)::view-transition-old(root)


 // 创建一个过渡对象
document.startViewTransition(() => {
document.documentElement.classList.toggle('dark')
})

可以看到,一个淡入淡出的效果,但是我们需要的是一个圆向外扩散的效果


用剪切效果就可以实现,其中 circle(动画进度 at 动画初始x坐标 动画初始y坐标)


设置动画时间为 1秒,作用在新的伪元素上,也即是作用在新的截图上


const transition = document.startViewTransition(() => {
document.documentElement.classList.toggle('dark')
})

transition.ready.then(() => {
document.documentElement.animate({
clipPath: ['circle(0% at 50% 50%)', 'circle(100% at 100% 100%)']
}, {
duration: 1000,
pseudoElement: '::view-transition-new(root)'
})
})

动画-1714752074132-6.gif


为什么动画效果和预期的不一样


因为,默认的动画效果,把当前动画覆盖了,我们把默认动画效果去掉


/* 隐藏默认的过渡效果 */
::view-transition-new(root),
::view-transition-old(root) {
animation: none;
}

动画-1714752309164-8.gif


效果出来了,但是圆的扩散不是从按钮中心扩散的


那么,通过 ref="btn" 来获取 “改变主题” 按钮的坐标位置


再获取按钮坐标减去宽高,就能得到按钮的中心坐标了


<div ref="btn" class="change-theme-btn" @click="changeColor">改变主题</div>

<script setup>
import { ref } from 'vue';
const btn = ref<any>(null)

/* 改变颜色 */
const changeColor = () => {
// 创建一个过渡对象
const transition = document.startViewTransition(() => {
document.documentElement.classList.toggle('dark')
})

const width = btn.value.getBoundingClientRect().width // 按钮的宽度
const height = btn.value.getBoundingClientRect().height // 按钮的高度
const x = btn.value.getBoundingClientRect().x + width / 2 // 按钮的中心x坐标
const y = btn.value.getBoundingClientRect().y + height / 2 // 按钮的中心y坐标

transition.ready.then(() => {
document.documentElement.animate({
clipPath: [`circle(0% at ${x}px ${y}px)`, `circle(100% at ${x}px ${y}px)`]
}, {
duration: 1000,
pseudoElement: '::view-transition-new(root)',
})
})
}
</script>

扩展,如果,我不要从中心扩展,要从左上角开始动画呢,右上角呢...


我们把按钮放在左上角,看看效果


修改下样式、与模板


<template>
<div ref="btn" class="change-theme-btn" @click="changeColor">改变主题</div>
<div class="info-box">
<h1>Element Plus</h1>
<p>基于 Vue 3,面向设计师和开发者的组件库</p>
</div>

</template>

.info-box {
width: 100vw;
height: calc(100vh - 44px);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}

动画这个圆的半径不对,导致动画到快末尾的时候,直接就结束了


动画-1714753474905-10.gif


动画的圆的半径 = 按钮中心坐标 到 对角点的坐标


可以使用三角函数计算,两短边平方 = 斜边平方


image-20240504002759638.png


// 计算展开圆的半径
const tragetRadius = Math.hypot(
window.innerWidth - x,
innerHeight - y
)

// 设置过渡的动画效果
transition.ready.then(() => {
document.documentElement.animate({
clipPath: [`circle(0% at ${x}px ${y}px)`, `circle(${tragetRadius}px at ${x}px ${y}px)`]
}, {
duration: 1000,
// pseudoElement
// 设置过渡效果的伪元素,这里设置为根元素的伪元素
// 这样过渡效果就会作用在根元素上
pseudoElement: '::view-transition-new(root)',
})
})

动画-1714754131456-15.gif


如果是右上角呢


.change-theme-btn {
float: right;
width: 80px;
height: 40px;
background-color: var(--background-color);
text-align: center;
line-height: 40px;
color: var(--color);
cursor: pointer;
border-radius: 8px;
border: 2px solid var(--color);
}

动画-1714754468881-23.gif


在右边的话,使用三角函数计算,其中一个短边就不能是 屏幕宽度 - 按钮x坐标,直接是 x 坐标就对了


那要怎么实现呢,直接取 屏幕宽度 - 按钮x坐标 与 按钮x坐标 的最大值就可以了


y 也是同理


const tragetRadius = Math.hypot(
Math.max(x, window.innerWidth - x),
Math.max(y, window.innerHeight - y)
)

动画-1714754788538-25.gif


你可以试试其他位置,是否也是可行的


完整代码


<template>
<div ref="btn" class="change-theme-btn" @click="changeColor">改变主题</div>
<div class="info-box">
<h1>Element Plus</h1>
<p>基于 Vue 3,面向设计师和开发者的组件库</p>
</div>

</template>

<script setup lang="ts">
import { ref } from 'vue';
const btn = ref<any>(null)

/* 改变颜色 */
const changeColor = () => {
// 创建一个过渡对象
const transition = document.startViewTransition(() => {
document.documentElement.classList.toggle('dark')
})

const width = btn.value.getBoundingClientRect().width // 按钮的宽度
const height = btn.value.getBoundingClientRect().height // 按钮的高度
const x = btn.value.getBoundingClientRect().x + width / 2 // 按钮的中心x坐标
const y = btn.value.getBoundingClientRect().y + height / 2 // 按钮的中心y坐标

// 计算展开圆的半径
const tragetRadius = Math.hypot(
Math.max(x, window.innerWidth - x),
Math.max(y, window.innerHeight - y)
)

// 设置过渡的动画效果
transition.ready.then(() => {
document.documentElement.animate({
clipPath: [`circle(0% at ${x}px ${y}px)`, `circle(${tragetRadius}px at ${x}px ${y}px)`]
}, {
duration: 1000,
// pseudoElement
// 设置过渡效果的伪元素,这里设置为根元素的伪元素
// 这样过渡效果就会作用在根元素上
pseudoElement: '::view-transition-new(root)',
})
})
}
</script>


<style>

:root {
--background-color: #fff;
--color: #282c34;
background-color: var(--background-color);
color: var(--color);
}

:root.dark {
--background-color: #282c34;
--color: #fff;
}

/* 隐藏默认的过渡效果 */
::view-transition-new(root),
::view-transition-old(root) {
animation: none;
}

.change-theme-btn {
float: right;
width: 80px;
height: 40px;
background-color: var(--background-color);
text-align: center;
line-height: 40px;
color: var(--color);
cursor: pointer;
border-radius: 8px;
border: 2px solid var(--color);
}

.info-box {
width: 100vw;
height: calc(100vh - 44px);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
</style>

换肤动画源码


小结


换肤功能,主要靠 css 变量 与 classList.toggle


startViewTransition 这个 API 来实现过渡动画效果,注意需要清除默认动画


圆点扩散效果,主要运用剪切的方式进行实现,计算过程运用了三角函数运算


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

background简写,真细啊!

web
背景原因 今天写需求,需要使用background简写属性,心想这还不简单吗,真男人写样式只需要两秒: background: url('./bg.png') no-repeat center contain ; 搞定! 上面设置的依次是 背景图片 背...
继续阅读 »

背景原因


今天写需求,需要使用background简写属性,心想这还不简单吗,真男人写样式只需要两秒:


background:  url('./bg.png') no-repeat center contain ;

搞定!


上面设置的依次是 背景图片 背景平铺模式 背景位置 背景图片是保有其原有的尺寸还是拉伸到新的尺寸。


so easy~


看我ctrl + s 保存代码,编译。


嗯? 怎么不生效? 俺的背景呢?
打开控制台一看,好家伙,压根没生效:


image.png


问题排查


第一反应是这些属性有固定顺序,但是凭我练习两年半的经验,不应该啊,之前也是这样用的啊,遂打开MDN,仔细翻阅....


发现了下面这段话:


image.png


这让我更加确信 写的没毛病啊!!


background-attachment、background-color、background-image、background-position、background-repeat、background-size
这些属性可以以任意顺序书写。


见了鬼了,待我排查两小时(摸鱼...)


原因浮现


在仔细阅读文档后发现,其实在文档的上面,还有另外一段话:


image.png


我恍然大悟,索嘎,以后看文档不能马虎了,得仔细查阅,过于经验主义了,这都是细节啊!


background使用注意事项和总结


其实,使用background时,大部分时候 属性的顺序是可以任意位置书写的,
但是有两个属性有点特殊,那就是background-size和background-position,


当background简写同时有这两个属性时,那么必须background-position在前,background-size在后,且两者只能紧挨着书写并且以 "/"分隔。
例如:


错误: background: url('./bg.png') no-repeat center  contain ; // 没有以 "/"分隔
错误: background: url('./bg.png') center no-repeat contain ; // 没有紧挨着书写
错误: background: url('./bg.png') no-repeat contain / center; //background-size写在了 background-position的前面

正确: background: url('./bg.png') no-repeat center / contain ;


写在最后


其实MDN在关于background的文档最开头的例子中就有写:


image.png


只不过没有用语言描述出来,一般没有认真看很难发现,所以有时候能够静下心来认真查阅文档,真的会发现很多细节(甩锅:这tm是谁写的文档,出来挨打).


作者:可狗可乐
来源:juejin.cn/post/7234825495333158949
收起阅读 »

JavaScript 流程控制语句详解:if语句、switch语句、while循环、for循环等

JavaScript,作为一种广泛使用的编程语言,它的流程控制语句是构建逻辑和实现功能的基础。流程控制语句包括条件语句、循环语句和转向语句,它们是编程中不可或缺的部分。接下来,我们将一一解析这些语句,带你走进JavaScript的世界。一、什么是流程控制语句流...
继续阅读 »

JavaScript,作为一种广泛使用的编程语言,它的流程控制语句是构建逻辑和实现功能的基础。流程控制语句包括条件语句、循环语句和转向语句,它们是编程中不可或缺的部分。

接下来,我们将一一解析这些语句,带你走进JavaScript的世界。

一、什么是流程控制语句

流程控制语句是用来控制程序中语句执行顺序的语句,它们可以影响程序的流程,从而实现不同的逻辑。流程控制语句主要分为以下三类:
Description

顺序结构: 这是最基本的流程控制,代表代码按照书写的顺序从上到下依次执行。通常程序都是从第一行代码开始顺序执行到结束的。

选择结构: 用于根据特定条件来控制代码的执行路径。常见的选择结构包括if、else、if-else if和switch等。这些语句允许程序在满足某些条件时执行特定的代码块,而在其他条件下执行另外的代码块或跳过某些代码。

循环结构: 用于重复执行某段代码直到满足退出条件为止。循环语句包括for、foreach、while和do-while等。通过这些语句,可以实现固定次数的循环或者当某个条件成立时的持续循环。

此外,还有跳转语句如break、continue和return等,它们可以改变正常的控制流程,例如跳出当前循环或者返回函数的结果。

二、条件判断语句

使用条件判断语句可以在执行某个语句之前进行判断,如果条件成立才会执行语句,条件不成立,则语句不执行。

语法一:if(条件表达式){语句…};

执行流程:
if语句在执行时,会先对条件表达式进行求值判断,

  • 如果条件表达式的值为true,则执行if后的语句,

  • 如果条件表达式的值为false,则不会执行if后的语句if语句只能控制紧随其后的那个语句。

如果希望if语句可以控制多条语句,可以将这些语句统一放在代码块中,如果就一条if语句,代码块不是必须的,但在开发中尽量写清楚。

代码演示:

<script>
if(true) console.log('好好学习,天天向上');
// 加上条件运算符 && ||
var a=20;
if(a>10&&a<=20){
alert('a在10-20之间');
alert("4567")
}
</script>

语法二:if…else…语句

语法:

if(条件表达式){
语句....
}else{
语句....
}

执行流程:
当该语句执行时,会先对if后的条件进行判断,

  • 如果该值为true,则执行if后的语句,

  • 如果该值为false,则执行else后的语句,两者选其一执行。

语法三:if…else if…else

语法:

if(条件表达式){
语句....
}else if(条件表达式){

语句....
}else{
语句....
}

执行流程:
当该语句执行时,会从上到下依次对条件表达式进行求值,

  • 如果值为true,则执行当前语句。

  • 如果值为false,则继续向下判断,如果所有的条件都不满意,就执行最后一个else或者不执行,该语句中,只会有一个代码块被执行,一旦代码块执行了, 则直接结束语句。

<script>
var age=16;
/* if(age>=60){
alert("你已经退休了~~~")
}else{
alert("你还没退休~~~")
} */


if(age>=100){
alert("您老高寿呀~~~");
}else if(age>=80){
alert("你也不小了");
} else if(age>=60){
alert("你刚退休呀~~~");
}else if(age>=30){
alert("你已经中年了");
}else if(age>=17){
alert("你刚成年呀~~~");
}else{
alert("你还是个小孩子~~")
};
</script>

三、条件分支语句

switch语句是一种多分支选择结构,它可以根据表达式的值,来选择执行不同的代码块。

语法:switch…case…

switch(条件表达式){
case 表达式:
语句....
break;
case 表达式:
语句....
break;
default:
语句...
break;
}

执行流程:

在执行时,会依次将case后的表达式的值和switch后的条件表达式的值进行全等比较。

  • 如果比较结果为true,则从当前case处开始执行代码,当前case后的所有代码都会执行;

  • 在case的后边跟着一个break关键字,这样可以确保只会执行当前case后的语句,而不会执行其他的case;

  • 如果比较结果为false,则继续向下比较;

  • 如果所有的比较结果都为false,则只执行default后的语句;

注意: switch语句和if语句的功能实际上有重复的,使用switch可以实现if的功能,同样使用if也可以实现switch的功能,所以我们使用时,可以根据自己的习惯选择。
代码演示:

<script>
var num=2;
switch(num){
case 1:
console.log("壹");
//使用break可以退出switch语句
break;
case 2:
console.log("贰");
break;
case 3:
console.log("叁")
break;

default:
console.log("非法数字~~~");
break;
}
</script>

四、循环语句

循环语句,就是让某段代码反复执行。在JavaScript中,主要有for循环、while循环、do…while循环等。

1) while循环

语法:

while(条件表达式){
语句
}

while语句执行流程:

先对条件表示式进行求值判断,如果值为true,则执行循环体,循环体执行完毕以后,继续对表达式进行判断,如果值为false,则终止循环。

2) do…while循环

语法:

do{

语句....

}while(条件表达式)

执行流程:

do…while 语句在执行时,会先执行循环体,循环体执行完毕后,再对while后的条件表示式进行判断,如果结果为true,则继续执行,执行完毕继续判断,如果结果为false,则停止执行。

注意: 实际上以上两个语句功能类似,不同的是while 是先判断后执行,而do…while会先执行后判断,do…while可以保证循环体至少执行一次,而while不行。


想要快速入门前端开发吗?推荐一个前端开发基础课程,这个老师讲的特别好,零基础学习无压力,知识点结合代码,边学边练,可以免费试看试学,还有各种辅助工具和资料,非常适合新手!点这里前往学习哦!

3)for语句(for循环)

在for循环中,为我们提供了专门的位置,用来放三个表达式。

  • 初始化表达式
  • 条件表达式
  • 更新表达式

for循环的语法:

for(初始化表达式;条件表达式;更新表达式){

语句....

}

for循环的执行流程:

  • 初始化表达式,初始化变量(初始化表达式,只会执行一次);

  • 条件表达式,判断是否执行循环;

  • 如果为true,则执行循环,如果为false,终止循环;

  • 执行更新表达式,更新表达式执行完毕继续重复。

<script>
//第一种写法
for(var i=0;i<10;i++){
alert(i);
}
//第二种写法 for循环中的三个部分都可以省略,也都可以写在外部
var i=0;
for(;i<10;){
alert(i++);
}

//如果在for循环中,不写任何的表达式,只写两个;
//此时循环是一个死循环,会一直执行下去,慎用
for(;;){
alert("hello");
}
</script>

五、break和continue语句

break关键字

可以用来退出switch或循环语句,不能在if语句中使用break和continue,break关键字,会立即终止离它最近的那个循环语句。

continue关键字

可以用来跳过当次循环,同样continue也是默认只会对离它最近的循环起作用。

终止指定循环

可以为循环语句创建一个label(标签),来标识当前的循环。

语法:

label(给起的标签名字):循环语句

使用break语句时,可以在break后跟着一个label,这样break可以结束指定的循环,而不是最近的。

代码演示

<script>

/* for(var i=0;i<5;i++){
console.log(i);
//break;//用来结束for的循环语句,for只会循环一次
if(i==2){
break;//这个break是对整个for循环起作用的
}
} */


/* for (var i = 0; i < 5; i++) {
console.log("@外层循环" + i);
for (var j = 0; j < 5; j++) {
break;//只会结束离他最近的内层循环
console.log("内层循环" + j);
}
} */



/* outer: for (var i = 0; i < 5; i++) {
console.log("@外层循环" + i);
for (var j = 0; j < 5; j++) {
break outer; //指定结束外层的for循环
console.log("内层循环" + j);
}
} */



for (var i = 0; i < 5; i++) {
if (i == 2) {
continue;
}
console.log(i);
}
</script>

JavaScript的流程控制语句,就像是一把魔法棒,它能让我们的代码按照我们的意愿去运行。掌握了这些语句,我们就可以在编程的世界里自由翱翔。

希望这篇文章能帮助你更好地理解和使用JavaScript的流程控制语句,让我们一起在编程的道路上,探索更多的可能性。

收起阅读 »

前端使用a链接下载内容增加loading效果

web
问题描述:最近工作中出现一个需求,纯前端下载 Excel 数据,并且有的下载内容很多,这时需要给下载增加一个 loading 效果。代码如下:// utils.jsconst XLSX = require('xlsx')// 将一个sheet转成最终的exce...
继续阅读 »
  1. 问题描述:最近工作中出现一个需求,纯前端下载 Excel 数据,并且有的下载内容很多,这时需要给下载增加一个 loading 效果。
  2. 代码如下:
// utils.js
const XLSX = require('xlsx')
// 将一个sheet转成最终的excel文件的blob对象,然后利用URL.createObjectURL下载
export const sheet2blob = (sheet, sheetName) => {
sheetName = sheetName || 'sheet1'
var workbook = {
SheetNames: [sheetName],
Sheets: {}
}
workbook.Sheets[sheetName] = sheet
// 生成excel的配置项
var wopts = {
bookType: 'xlsx', // 要生成的文件类型
bookSST: false, // 是否生成Shared String Table,官方解释是,如果开启生成速度会下降,但在低版本IOS设备上有更好的兼容性
type: 'binary'
}
var wbout = XLSX.write(workbook, wopts)
var blob = new Blob([s2ab(wbout)], { type: 'application/octet-stream' })
// 字符串转ArrayBuffer
function s2ab(s) {
var buf = new ArrayBuffer(s.length)
var view = new Uint8Array(buf)
for (var i = 0; i !== s.length; ++i) view[i] = s.charCodeAt(i) & 0xff
return buf
}
return blob
}

/**
* 通用的打开下载对话框方法,没有测试过具体兼容性
* @param url 下载地址,也可以是一个blob对象,必选
* @param saveName 保存文件名,可选
*/
export const openDownloadDialog = (url, saveName) => {
if (typeof url === 'object' && url instanceof Blob) {
url = URL.createObjectURL(url) // 创建blob地址
}
var aLink = document.createElement('a')
aLink.href = url
aLink.download = saveName + '.xlsx' || '1.xlsx' // HTML5新增的属性,指定保存文件名,可以不要后缀,注意,file:///模式下不会生效
var event
if (window.MouseEvent) event = new MouseEvent('click')
else {
event = document.createEvent('MouseEvents')
event.initMouseEvent(
'click',
true,
false,
window,
0,
0,
0,
0,
0,
false,
false,
false,
false,
0,
null
)
}
aLink.dispatchEvent(event)
}

<el-button
@click="clickExportBtn"
>
<i class="el-icon-download"></i>下载数据
</el-button>
<div class="mongolia" v-if="loadingSummaryData">
<el-icon class="el-icon-loading loading-icon">
<Loading />
</el-icon>
<p>loading...</p>
</div>

clickExportBtn: _.throttle(async function() {
const downloadDatas = []
const summaryDataForDownloads = this.optimizeHPPCDownload(this.summaryDataForDownloads)
summaryDataForDownloads.map(summaryItem =>
downloadDatas.push(this.parseSummaryDataToBlobData(summaryItem))
)
// donwloadDatas 数组是一个三维数组,而 json2sheet 需要的数据是一个二维数组
this.loadingSummaryData = true
const downloadBlob = aoa2sheet(downloadDatas.flat(1))
openDownloadDialog(downloadBlob, `${this.testItem}报告数据`)
this.loadingSummaryData = false
}, 2000),

// css
.mongolia {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.9);
display: flex;
justify-content: center;
align-items: center;
font-size: 1.5rem;
color: #409eff;
z-index: 9999;
}
.loading-icon {
color: #409eff;
font-size: 32px;
}
  1. 解决方案探究:
  • 在尝试了使用 $nextTick、将 openDownloadDialog 改写成 Promise 异步函数,或者使用 async/await、在 openDownloadDialog 中添加 loadingSummaryData 逻辑,发现依旧无法解决问题,因此怀疑是 document 添加新元素与 vue 的 v-if 渲染产生冲突,即 document 添加新元素会阻塞 v-if 的执性。查阅资料发现,问题可能有以下几种:

    • openDownloadDialog 在执行过程中执行了较为耗时的同步操作,阻塞了主线程,导致了页面渲染的停滞。
    • openDownloadDialog 的 click 事件出发逻辑存在问题,阻塞了事件循环(Event Loop)。
    • 浏览器在执行 openDownloadDialog 时,将其脚本任务的优先级设置得较高,导致占用主线程时间片,推迟了其他渲染任务。
    • Vue 的批量更新策略导致了 v-if 内容的显示被延迟。
  • 查阅资料后找到了如下几种方案:

      1. 使用 setTimeout 使 openDownloadDialog 异步执行
      clickExport() {
      this.loadingSummaryData = true;

      setTimeout(() => {
      openDownloadDialog(downloadBlob, `${this.testItem}报告数据`);

      this.loadingSummaryData = false;
      });
      }


      1. 对 openDownloadDialog 内部进行优化
      • 避免大循环或递归逻辑
      • 将计算工作分批进行
      • 使用 Web Worker 隔离耗时任务
        • 在编写 downloadWorker.js 中的代码时,要明确这部分代码是运行在一个独立的 Worker 线程内部,而不是主线程中。

            1. 不要直接依赖或者访问主线程的全局对象,比如 window、document 等。这些在 Worker 内都无法直接使用。
            1. 不要依赖 DOM 操作,比如获取某个 DOM 元素。Worker 线程无法访问页面的 DOM。
            1. 代码执行的入口是 onmessage 回调函数,在其中编写业务逻辑。
            1. 和主线程的通信只能通过 postMessage 和 onmessage 发送消息事件。
            1. 代码应该是自包含的,不依赖外部变量或状态。
            1. 可以导入其他脚本依赖,比如用 import 引入工具函数等。
            1. 避免修改或依赖全局作用域,比如定义全局变量等。
        • 总之,编写 downloadWorker.js 时要注意它执行在一个独立环境中,只能通过消息事件与主线程通信。
    // 创建 Worker
    const worker = new Worker('downloadWorker.js');

    // 点击下载时向 Worker 发送消息
    function clickDownload() {

    showLoading();

    worker.postMessage({
    url: fileURL,
    filename: 'report.xlsx'
    });

    worker.onmessage = function(e) {
    // 收到下载完成的消息
    hideLoading();
    }

    }

    // 显示 loading
    function showLoading() {
    loading.style.display = 'block';
    }

    // 隐藏 loading
    function hideLoading() {
    loading.style.display = 'none';
    }

    // downloadWorker.js

    onmessage = function(e) {
    const url = e.data.url;
    const filename = e.data.filename;

    // 创建并点击链接触发下载
    const a = document.createElement('a');
    a.href = url;
    a.download = filename;
    a.click();

    postMessage('下载完成');
    }

    <div id="loading" style="display:none;">
    Downloading...
    </div>

    <button onclick="clickDownload()">
    Download
    </button>

    <script src="downloadWorker.js"></script>


      1. 使用 requestIdleCallback 进行调度
      clickExport() {
      this.loadingSummaryData = true;

      requestIdleCallback(() => {
      openDownloadDialog(downloadBlob, `${this.testItem}报告数据`);

      this.loadingSummaryData = false;
      });
      }
    • 当使用上述方案时发现里面的内容变得异常的慢,于是去问GPT后,得到以下答案:

      使用 requestIdleCallback 来执行 openDownloadDialog 确实可能会需要更多时间。

      因为 requestIdleCallback 会在主线程空闲时才执行回调函数,所以不像 setTimeout 可以直接按指定时间异步执行。

      requestIdleCallback 的优先级较低,只在主线程空闲时才会执行,所以 download 的速度会比 setTimeout 慢一些。

      但使用 requestIdleCallback 的优点是它不会占用主线程时间,可以避免阻塞页面渲染。

      所以需要权衡执行速度和避免阻塞之间的关系:

      • 如果下载速度是优先考虑的因素,可以继续使用 setTimeout 异步执行。
      • 如果避免阻塞渲染更重要,则 requestIdleCallback 是更好的选择。

      偶尔下载任务被延迟不是大问题,但频繁的下载任务阻塞会严重影响页面体验。

      此外,可以结合两者试试:

      setTimeout 先异步调度,然后在回调中使用 requestIdleCallback 避免阻塞。这样就可以兼顾下载速度和避免渲染阻塞了。

    clickExport() {

    this.loadingSummaryData = true;

    setTimeout(() => {

    requestIdleCallback(() => {
    openDownloadDialog(downloadBlob);
    });

    this.loadingSummaryData = false;

    }, 200);

    }


      1. 分析线程模型,找到具体的阻塞点
      • 使用 Performance 工具分析线程
      • debugger 及 console.log 打印关键函数的执行时间
      • 检查是否有非主线程的任务阻塞了主线程
      1. 调整 vue 的批量更新策略
    new Vue({
    config: {
    // 修改批量更新的队列长度
    batchUpdateDuration: 100
    }
    })



作者:李卫泽
来源:juejin.cn/post/7268050036474609683
收起阅读 »

我改进了数据滚动方式!老板直接加薪

web
需求背景 前几天,甲方提了个需求,想让下面的数据循环展示,准备放在他们集团首页给他们领导演示用。 我们领导很重视这个事儿,拍了拍我,语重心长的说,小伙子,好好做。 我啪的一下就兴奋了,老板居然如此器重我,我必当鞠躬尽瘁,减少摸鱼,我要让老板拜倒在精湛的技术下...
继续阅读 »

需求背景


前几天,甲方提了个需求,想让下面的数据循环展示,准备放在他们集团首页给他们领导演示用。



我们领导很重视这个事儿,拍了拍我,语重心长的说,小伙子,好好做。


我啪的一下就兴奋了,老板居然如此器重我,我必当鞠躬尽瘁,减少摸鱼,我要让老板拜倒在精湛的技术下!


于是,我搬出自己的库存代码,仅2min就实现了数据的滚动:


没错,我直接照搬了自己以前写过的文章:JS实现可滚动区域自动滚动展示 - 掘金


就在我准备告诉老板我做完了的时候,我突然想了想,这么快做完,老板一定觉得我没好好做,我以后还怎么升职加薪,赢取白富美?


于是,我连夜研究,终于改进了数据滚动方式,赢得了老板的大饼(以后涨500)。最终效果:



技术方案


技术选型


观察最终效果图,可以发现这其实就是一个数据循环滚动的效果,每条内容之间间隔1000ms,每条出现动的时间为500ms。用术语来说,这就是一个单步停顿滚动效果。


我百度了一下,社区还是有这个实现的现成方案的:vue-seamless-scroll,周下载也还行。



于是,我果断试了试,结果不知道什么原因,并不生效...


既然如此,直接手写一个吧!


实现思路


要实现上述效果其实很简单,如图



我们创造一个含有六个值的数组,每隔一段时间循环更改黄色区域的数据,当黄色区域数据变成最新的时候,红色区域整体向下移动,当有数值超出滚动区域后,在删除这个数据即可。


数据更新


如果不考虑动画,我们的代码应该这么写


<template>
<div class="item-wrap" v-for="(item, index) in animationData">
<!-- 模块内容 -->
</div>

</template>
<script setup lang="ts">
// #假设这是接口请求的10条最新数据
const allCarouseData = ref([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
// #需要轮播的数据
const animationData = ref<any>([])
// *定时器
const animationTimerMeta: any = {
timer: null,
// 这个函数负责设置轮播数据的更新逻辑。
timeFuc() {
let setTimeoutId: any = null
if (this.timer) return
this.timer = setInterval(() => {
// 取轮播数据的第一条id
let firstId = animationData.value[0].id
// 为轮播数据添加最新的第一项数据
let index = allCarouseData.value.findIndex((res: any) => res.id === firstId)
let addIndex = index - 1 < 0 ? allCarouseData.value.length - 1 : index - 1
animationData.value.unshift(allCarouseData.value[addIndex])
setTimeout(() => {
// 删除数组的最后一项
animationData.value.pop()
}, 1000)

}, 1500)
}
}
animationData.value = allCarouseData.value.slice(-5)
animationTimerMeta.timeFuc()
</script>

上述代码的主要功能是:



  1. 从 allCarouseData 中取出最后5个元素作为初始的轮播数据。

  2. 每1.5秒更新一次轮播数据,具体逻辑是:移除当前 animationData 的第一个元素,并从 allCarouseData 中取出前一个元素(如果已经是第一个元素,则取最后一个)添加到 animationData 的开头。

  3. 每1秒从 animationData 的末尾移除一个元素。


上述代码没有实现动画,他的效果是这样的:



动画添加


<template>
<div class="item-wrap" v-for="(item, index) in animationData"
:class="[{ moveToBottom: animationActive }, { show: animationActive && index === 0 }]"
>

<!-- 模块内容 -->
</div>

</template>
<script setup lang="ts">
// #假设这是接口请求的10条最新数据
const allCarouseData = ref([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
// #需要轮播的数据
const animationData = ref<any>([])
// #是否开启动画
const animationActive = ref(false)
// *定时器
const animationTimerMeta: any = {
timer: null,
// 这个函数负责设置轮播数据的更新逻辑。
timeFuc() {
let setTimeoutId: any = null
if (this.timer) return
this.timer = setInterval(() => {
// 取轮播数据的第一条id
let firstId = animationData.value[0].id
// 为轮播数据添加最新的第一项数据
let index = allCarouseData.value.findIndex((res: any) => res.id === firstId)
let addIndex = index - 1 < 0 ? allCarouseData.value.length - 1 : index - 1
animationData.value.unshift(allCarouseData.value[addIndex])
setTimeout(() => {
// 删除数组的最后一项
animationData.value.pop()
}, 1000)

}, 1500)
}
}
animationData.value = allCarouseData.value.slice(-5)
animationTimerMeta.timeFuc()
</script>


@keyframes moveToBottom {
0% {
transform: translateY(-47px);
}

100% {
transform: translateY(0);
}
}

.moveToBottom {
animation: moveToBottom 500ms ease-in-out forwards;
}

@keyframes fadeInFromTop {
0% {
opacity: 0;
transform: translateY(-47px);
}

100% {
opacity: 1;
transform: translateY(0);
color: #683BD6;
}
}

.show {
animation: fadeInFromTop 500ms ease-in-out forwards;
}

上述代码中,为了实现动画效果,采用了动态添加类名的技术方案。


animationData 数组中的元素会按照一定顺序进行显示和隐藏,同时伴随有动画效果。当第一个元素进入视图时,它会应用 fadeInFromTop 动画;其他元素会应用 moveToBottom 动画。通过定时器,元素会定期从 allCarouseData 中获取新的数据并更新 animationData。


代码释义:



  • moveToBottom: 当 animationActive 为真值时,此类名会被添加到 div 上。

  • show: 当 animationActive 为真值且当前元素是数组的第一个元素时,此类名会被添加到 div 上。


CSS 释义:



  • moveToBottom 动画:


定义一个名为 moveToBottom 的关键帧动画,使元素从上方移动到其原始位置。


moveToBottom 类将此动画应用到元素上。



  • fadeInFromTop 动画:


定义一个名为 fadeInFromTop 的关键帧动画,使元素从上方淡入并改变颜色。


show 类将此动画应用到元素上。


通过上述简单的实现方式,就能最终实现我们想要的效果



相比于普通滚动,这种方式看起来要好很多!


结语


要想实现这种单步停帧的效果,其实有很多实现方式,这只是笔者实现的一种,核心逻辑就是动态改变数据、增添类名。如果大家还有更好的方式,也欢迎大家指点。


作者:石小石Orz
来源:juejin.cn/post/7348433631944556555
收起阅读 »

只写后台管理的前端要怎么提升自己

web
本人写了五年的后台管理。每次面试前就会头疼,因为写的页面除了表单就是表格。抱怨过苦恼过也后悔过(虽然我现在已经心安理得的摆烂),但是站在现在的时间点回想以前,发现有很多事情可以做的更好,于是有了这篇文章。 写优雅的代码 一道面试题 大概两年以前,面试美团的时候...
继续阅读 »

本人写了五年的后台管理。每次面试前就会头疼,因为写的页面除了表单就是表格。抱怨过苦恼过也后悔过(虽然我现在已经心安理得的摆烂),但是站在现在的时间点回想以前,发现有很多事情可以做的更好,于是有了这篇文章。


写优雅的代码


一道面试题


大概两年以前,面试美团的时候,面试官让我写一道代码题,时间单位转换。具体的题目我忘记了。


原题目我没做过,但是我写的业务代码代码里有类似的单位转换,后端返回一个数字,单位是kb,而我要展示成 KBMB 等形式。大概写一个工具函数(具体怎么写的忘记了,不过功能比这个复杂点):


function formatSizeUnits(kb) {
let units = ['KB', 'MB', 'GB', 'TB', 'PB'];
let unitIndex = 0;

while (kb >= 1024 && unitIndex < units.length - 1) {
kb /= 1024;
unitIndex++;
}

return `${kb.toFixed(2)} ${units[unitIndex]}`;
}

而在此之前,是一个后端同学写的代码(说起来,后台管理系统前端人力不够后端帮忙写好像是常规操作,后端总觉得写前端简单,而我只能说你们那代码写的,一言难尽……),如下:


function formatSizeUnits(kb) {
var result = '';

if (kb < 1024) {
result = kb + ' KB';
} else if (kb < 1024 * 1024) {
result = (kb / 1024).toFixed(2) + ' MB';
} else if (kb < 1024 * 1024 * 1024) {
result = (kb / 1024 / 1024).toFixed(2) + ' GB';
} else {
result = (kb / 1024 / 1024 / 1024).toFixed(2) + ' TB';
}

return result;
}

虽然功能都一样,但是我写在面试的时候写了上面的代码得到了面试官的赞扬,也因此非常顺利的通过了面试。而如果我写成了下面的样子,估计不会有这种效果。


如何提升代码质量


想提升代码质量,最理想的是靠 code review ,但实际上这玩意在大多数公司根本就不存在。其次就只能多看看大佬们写的代码,但是一来后台管理本来就没太大难度很少有大佬,二来很多时候一个后台管理系统就一两个人负责。


还好现在有了各种 AI 工具,可以非常方便的优化自己的代码了。


还是上面的问题,看看 GPT 给的答案


// 提问:写一个工具函数,对文件大小的单位进行转换,加注释
// 回答:下面是一个JavaScript工具函数,它接受文件大小、初始单位、目标单位和小数位数,然后返回目标单位下的文件大小。

/**
* 将文件大小从一个单位转换为另一个单位。
*
* @param {number} size 文件大小。
* @param {string} fromUnit 初始单位('B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB')。
* @param {string} toUnit 目标单位('B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB')。
* @param {number} [decimalPoint=2] 结果保留的小数位数,默认为2。
* @return {string} 转换后的文件大小,带单位。
*/

function convertFileSize(size, fromUnit, toUnit, decimalPoint = 2) {
// 定义单位与字节之间的转换关系
const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
// 获取初始单位和目标单位的索引
const fromIndex = units.indexOf(fromUnit);
const toIndex = units.indexOf(toUnit);

// 如果单位不在列表中,抛出错误
if (fromIndex === -1 || toIndex === -1) {
throw new Error('Invalid units');
}

// 计算初始单位与目标单位之间的转换系数
const exponent = toIndex - fromIndex;
// 计算结果大小
const resultSize = size / Math.pow(1024, exponent);

// 返回格式化后的结果
return parseFloat(resultSize.toFixed(decimalPoint)) + ' ' + toUnit;
}

// 示例使用
console.log(convertFileSize(1, 'GB', 'MB')); // 输出: 1024.00 MB
console.log(convertFileSize(1, 'MB', 'KB')); // 输出: 1024.00 KB
console.log(convertFileSize(1, 'KB', 'B')); // 输出: 1024.00 B
console.log(convertFileSize(1, 'MB', 'GB', 5)); // 输出: 0.00098 GB

还能说什么,我只能说 NB!(经评论区提示,此段代码可能有问题,建议大家在使用AI工具时,重点是借鉴思路,直接使用代码需要谨慎~)


我现在每次写大段逻辑之后都习惯拿给 AI 看看,有什么更好的实现方式,或者用什么设计模式。AI 是非常低成本且高效提升代码质量的工具。


学会封装


一个功能用到了好多次,为什么不封装成组件?一个组件用到了好几个项目,为什么不单独写个npm包?差不多的项目创建了好几个,为什么不封装成脚手架?


你说,没时间,没必要,复制粘贴反而更快。


那你就完全没理解,这么做不一定是为了让工作更快完成,而是可以让你在年年终述职时更有话说(你就算写了一百个表单表格没有写一个脚手架更值得炫耀),如果不会写可以问问 AI。


而当你真正开始封装组件,开始写工具库了,你会发现你需要思考的确实比之前多了。


关注业务


对于前端业务重要吗?


相比于后端来说,前端一般不会太关注业务。就算出了问题大部分也是后端的问题。


但是就我找工作的经验,业务非常重要!


如果你做的工作很有技术含量,比如你在做低代码,你可以面试时讲一个小时的技术难点。但是你只是一个破写后台管理,你什么都没有的说。这个时候,了解业务就成为了你的亮点。


一场面试


还是拿真实的面试场景举例,当时前同事推我字节,也是我面试过N次的梦中情厂了,刚好那个组做的业务和我之前呆的组做的一模一样。



  • 同事:“做的东西和咱们之前都是一样的,你随便走个过场就能过,我在前端组长面前都夸过你了!”

  • 我:“好嘞!”


等到面试的时候:



  • 前端ld:“你知道xxx吗?(业务名词)”

  • 我:“我……”

  • 前端ld:“那xxxx呢?(业务名词)”

  • 我:“不……”

  • 前端ld:“那xxxxx呢??(业务名词)”

  • 我:“造……”


然后我就挂了………………


如何了解业务



  1. 每次接需求的时候,都要了解需求背景,并主动去理解


    我们写一个表格简简单单,把数据展示出来就好,但是表格中的数据是什么意思呢?比如我之前写一个 kafka 管理平台,里面有表格表单,涉及什么 cluster controller topic broker partition…… 我真的完全不了解,很后悔我几年时间也没有耐下心来去了解。


  2. 每次做完一个需求,都需要了解结果


    有些时候,后台管理的团队可能根本没有PM,那你也要和业务方了解,这个功能做了之后,多少人使用,效率提高了吗?数据是怎样的?


  3. 理解需求,并主动去优化


    产品要展示一千条数据,你要考虑要不要分页,不分页会不会卡,要不要上虚拟表格?


    产品要做一个可拖拽表单,你要考虑是否需要拖动,是否需要配置。


    其实很多时候,产品的思维可能会被局限在竞品的实现方式,而前端可以给TA更多选项。在和产品沟通的时候,你不仅是沟通页面的实现,也更能理解业务。



关注源码


说到源码, Vue,React 这些基本是每次必问,也有太多大佬们的总结我就不多说了。


除此之外,我想大家每次面试应该都会被问,你写了什么亮点的项目,困难的项目,你会说什么?哦,都是表单表格,都用 Element UI 实现的,没什么难度。那估计你面试也就到这里了。


那说什么,后台管理就这些啊?!


如果你用了 Element UI 你可以说写了什么自定义 Element UI 表单组件,Element UI 表单源码是什么原理,怎么通信的?用了 Axios 可以说 Axios 的原理,它的拦截器怎么实现的?用了编辑器 diff 那你知道 diff 算法是什么吗?用了 ECharts 那你知道他底层是用什么绘制的吗?用了 husky ?husky的原理是什么?用了Vite,它为什么快?是怎么打包的?写过插件吗?


可能有人觉得我离谱,事实上上面每一个问题都是我在面试中真实被问到的。当然,如果全部去了解,肯定不现实,每个人的精力有限,不可能每个用到的每个工具每个库都去了解原理,不过只要有一两个你去了解过,就可以在面试中引导面试官来问,要学会表现出你擅长的点


至于怎么读源码?以前我不知道,现在嘛…… 为什么不问问神奇的 ChatGPT 呢?



讲一下 Axios 源码中,拦截器是怎么实现的?


Axios 的拦截器实现基于 Axios 的核心原理,即 Axios 实例是一个包含请求和响应拦截器堆栈的对象。当发出请求或接收响应时,Axios 会遍历这些拦截器,并按照添加的顺序执行请求拦截器,以及按照相反的顺序执行响应拦截器。


在 Axios 的源码中,拦截器是通过一个 AxiosInterceptorManager 实例来管理的,它维护了一个拦截器数组。每个拦截器都是一个包含 fulfilledrejected 函数的对象。这两个函数分别对应于拦截器成功处理和拦截器处理出错的情况。


以下是 Axios 拦截器管理器的一个简化版本,展示了其核心实现思路:


class InterceptorManager {
constructor() {
this.handlers = []; // 存储拦截器的数组
}

use(fulfilled, rejected) {
this.handlers.push({
fulfilled: fulfilled,
rejected: rejected
});
return this.handlers.length - 1; // 返回拦截器的ID
}

eject(id) {
if (this.handlers[id]) {
this.handlers[id] = null; // 移除拦截器
}
}

forEach(fn) {
this.handlers.forEach((h) => {
if (h !== null) {
fn(h);
}
});
}
}

在发送请求或接收响应时,Axios 会创建一个 promise 链,并通过 forEach 方法将拦截器中的 fulfilledrejected 函数添加到这个链中。这样,每个拦截器都可以对请求或响应进行处理,然后将结果传递到链的下一个拦截器,或者在出错时结束链的执行。


axios.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
chain.unshift(interceptor.fulfilled, interceptor.rejected);
});

axios.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
chain.push(interceptor.fulfilled, interceptor.rejected);
});

在 Axios 的完整实现中,这个拦截器机制被集成到了 Axios 的请求发送和响应处理流程中。通过这种方式,Axios 可以在发送请求之前和接收响应之后,但在用户定义的 .then.catch 执行之前,插入自定义的逻辑。


请注意,这里提供的代码只是为了说明 Axios 拦截器的实现原理,并不是 Axios 源码的完整复制。如果你对 Axios 的拦截器实现细节感兴趣,建议查看 Axios 的官方 GitHub 仓库中的源码。



前端基建


当我们工作时间久了面试难免会遇到这些问题,前端工程化,前端监控,工作流,部署,性能等等。其实我们在工作中绝大部分时间都在写代码,对于这些不是所有人都有机会接触到,不过这些和所做的业务无关,是我们提升自己很好的一个思路。


技术选型


技术栈选 Vue 还是 React?Vue 选 Vue2 还是 Vue3?组件库选 ElementUI 还是 Ant Design?微前端有没有使用过?打包工具用 Vite 还是 Webpack?有那么多表单怎么实现的,有没有什么表达配置化方案,比如Formily?


对于我这种菜鸡,我这种只写简单的表单表格的人,这些都……无所谓……


image.png

不过为了应对面试我们还是需要了解下未选择技术栈的缺点,和已选择技术栈的优点(有点本末倒置…但是常规操作啦)


Vue 你可以说简单高效轻量级,面试必会问你为什么,你就开始说 Vue 的响应式系统,依赖收集等。


React 你可以说 JSX、Hooks 很灵活,那你必然要考虑 JSX 怎么编译, Hooks 实现方式等。


总体而言,对于技术选型,依赖于我们对所有可选项的理解,做选择可能很容易,给出合理的理由还是需要花费一些精力的。


开发规范


这个方面,在面试的时候我被问到的不多,我们可以在创建项目的时候,配置下 ESlintstylelintprettiercommitlint 等。


前端监控


干了这么多年前端,前端监控我是……一点没做过。


image.png

前端监控,简单来说就是我们在前端程序中记录一些信息并上报,一般是错误信息,来方便我们及时发现问题并解决问题。除此之外也会有性能监控,用户行为的监控(埋点)等。之前也听过有些团队分享前端监控,为了出现问题明确责任(方便甩锅)。


对于实现方案,无论使用第三方库还是自己实现,重要的都是理解实现原理。


对于错误监控,可以了解一下 Sentry,原理简单来说就是通过 window.onerrorwindow.addEventListener('unhandledrejection', ...) 去分别捕获同步和异步错误,然后通过错误信息和 sourceMap 来定位到源码。


对于性能监控,我们可以通过 window.performancePerformanceObserver 等 API 收集页面性能相关的指标,除此之外,还需要关注接口的响应时间。


最后,收集到信息之后,还要考虑数据上报的方案,比如使用 navigator.sendBeacon 还是 Fetch、AJAX?是批量上报,实时上报,还是延迟上报?上报的数据格式等等。


CI/CD


持续集成(Continuous Integration, CI)和 持续部署(Continuous Deployment, CD),主要包括版本控制,代码合并,构建,单测,部署等一系列前端工作流。


场景的工作流有 Jenkins、 Gitlab CI 等。我们可以配置在合并代码时自动打包部署,在提交代码时自动构建并发布包等。


这块我了解不多,但感觉这些工具层面的东西,不太会涉及到原理,基本上就是使用的问题。还是需要自己亲自动手试一下,才能知道细节。比如在 Gitlab CI 中, Pipeline 、 Stage 和 Job 分别是什么,怎么配置,如何在不同环境配置不同工作流等。


了解技术动态


这个可能还是比较依赖信息收集能力,虽然我个人觉得很烦,但好像很多领导级别的面试很愿意问。


比如近几年很火的低代码,很多面试官都会问,你用过就问你细节,你没用过也会问你有什么设计思路。


还有最近的两年爆火的 AI,又或者 Vue React的最新功能,WebAssembly,还有一些新的打包工具 Vite Bun 什么的,还有鸿蒙开发……


虽然不可能学完每一项新技术,但是可以多去了解下。


总结


写了这么多,可能有人会问,如果能回到过去,你会怎么做。


啊,我只能说,说是一回事,做又是另一回事,事实上我并不希望回到过去去卷一遍,菜点没关系,快乐就好,一切都是最好的安排。


image.png

作者:我不吃饼干
来源:juejin.cn/post/7360528073631318027
收起阅读 »

JavaScript运算符及优先级全攻略,点击立刻升级你的编程水平!

在编程的世界里,运算符是构建逻辑、实现功能的重要工具。它能帮助我们完成各种复杂的计算和操作。今天,我们就来深入探索JavaScript中运算符的奥秘,掌握它们的种类和优先级,让你的代码更加高效、简洁!一、什么是运算符运算符,顾名思义,就是用于执行特定操作的符号...
继续阅读 »

在编程的世界里,运算符是构建逻辑、实现功能的重要工具。它能帮助我们完成各种复杂的计算和操作。

今天,我们就来深入探索JavaScript中运算符的奥秘,掌握它们的种类和优先级,让你的代码更加高效、简洁!

一、什么是运算符

运算符,顾名思义,就是用于执行特定操作的符号。

Description

在JavaScript中,运算符用于对一个或多个值进行操作,并返回一个新的值。它们是编程语言中的基础构件,帮助我们完成各种复杂的计算和逻辑判断。

运算符可以分为多种类型,如算术运算符、关系运算符、逻辑运算符等。通过使用不同的运算符,我们可以实现各种复杂的计算和逻辑判断,让程序更加灵活、强大。


二、运算符的分类

1、算术运算符

用于执行数学计算,如加法、减法、乘法、除法等。常见的算术运算符有:+、-、*、/、%、++、–等。

Description

+ 加法运算

  • 两个字符串进行加法运算,则作用是连接字符串,并返回;

  • 任何字符串 + “ ”空串做运算,都将转换为字符串,由浏览器自动完成,相当于调用了String ( )。

-减法运算 *乘法运算 /除法运算

  • 先转换为 Number 再进行正常的运算。

注意: 可以通过为一个值 -0 *1 /1 来将其转换为Number数据类型,原理和Number ( )函数一样。

%求余运算

对一个数进行求余运算

代码示例:

var num1 = 1;
var num2 = 2;
var res = num1-num2; //返回值为 -1
var res = num1*num2; //返回值为 2
var res = num1/num2; //返回值为 0.5——js中的除法为真除法
var res = num1%num2; //返回值为 1
console.log(res);


2、关系运算符

通过关系运算符可以比较两个值之间的大小关系,如果关系成立它会返回true,如果关系不成立则返回false。常见的比较运算符有:==、!=、>、<、>=、<=等。

> 大于号

  • 判断符号左侧的值是否大于右侧的值;

  • 如果关系成立,返回true,如果关系不成立则返回false。

>= 大于等于

  • 判断符号左侧的值是否大于或等于右侧的值。

< 小于号

  • 判断符号左侧的值是否小于右侧的值;

  • 如果关系成立,返回true,如果关系不成立则返回false。

<= 小于等于

  • 判断符号左侧的值是否小于或等于右侧的值。

非数值的情况

  • 对于非数值进行比较时,会将其转换为数字然后再比较。

  • 如果符号两侧的值都是字符串时,不会将其转换为数字进行比较,而会分别比较字符串中字符的Unicode编码。

== 相等运算符

  • 两者的值相等即可。

  • 比较两个值是否相等,相等返回 true,否则返回 flase。

  • 使用==来做相等运算

特殊:

console.log(null==0);  //返回 false
console.log(undefined == null); //返回true 因为 undefined衍生自null
console.log(NaN == NaN); //返回 false NaN不和任何值相等

isNan() 函数来判断一个值是否是NaN,是返回 true ,否则返回 false。

Description

=== 全等

  • 两者的值不仅要相等,而且数据类型也要相等。

  • 判断两个值是否全等, 全等返回 true 否则返回 false 。

!= 不相等运算符

  • 只考量两者的数据是否不等。

  • 比较两个值是否不相等,不相等返回 true,否则返回 flas。

  • 使用==来做相等运算。

!== 不全等运算符

  • 两者的值不仅要不等,而且数据类型也要不等,才会返回true,否则返回false;

  • 判断两个值是否不全等,不全等返回true,如果两个值的类型不同,不做类型转换直接返回true。

var num1 = 1;
var num2 = '2';
var res =(num1 !== num2); //返回值 true
console.log(res);


3、逻辑运算符

用于连接多个条件判断,如与、或、非等。常见的逻辑运算符有:&&、||、!等。

Description

&& 与

&&可以对符号两侧的值进行与运算并返回结果。

运算规则:

  • 两个值中只要有一个值为false就返回false,只有两个值都为true时,才会返回true;

  • JS中的“与”属于短路的与,如果第一个值为false,则不会看第二个值。

|| 或

  • ||可以对符号两侧的值进行或运算并返回结果

  • 两个值中只要有一个true,就返回true;

  • 如果两个值都为false,才返回false。

JS中的“或”属于短路的或,如果第一个值为true,则不会检查第二个值。

! 非

!可以用来对一个值进行非运算,所谓非运算就是值对一个布尔值进行取反操作,true变false,false变true。

  • 如果对一个值进行两次取反,它不会变化;

  • 如果对非布尔值进行元素,则会将其转换为布尔值,然后再取反;

  • 所以我们可以利用该特点,来将一个其他的数据类型转换为布尔值;

  • 可以为一个任意数据类型取两次反,来将其转换为布尔值;原理和Boolean()函数一样;

非布尔值的与 或 非

非布尔值的与 或 非( 会将其先转换为布尔值, 再进行运算 )

代码示例如下:

var b1 = true;
var b2 = false;
var res = b1 && b2; //返回值为 false
var res = b1 || b2; //返回值为true
console.log(res);


4、赋值运算符

用于给变量赋值,如等于、加等于、减等于等。常见的赋值运算符有:=、+=、-=等。

将右侧的值赋值给符号左侧的变量。

=   右赋给左
+= a+=5 等价于 a=a +5;
-= a-=5 等价于 a=a-5;
*= a*=5 等价于 a=a*5;
/= a/=5 等价于 a=a/5;
%= a%=5 等价于 a=%+5;


5、其他运算符

还有一些特殊的运算符,如类型转换运算符、位运算符等。这些运算符虽然不常用,但在特定场景下会发挥重要作用。

想要快速入门前端开发吗?推荐一个前端开发基础课程,这个老师讲的特别好,零基础学习无压力,知识点结合代码,边学边练,可以免费试看试学,还有各种辅助工具和资料,非常适合新手!点这里前往学习哦!


三、运算符的优先级

在JavaScript中,不同类型的运算符具有不同的优先级。优先级高的运算符会先于优先级低的运算符进行计算。了解运算符的优先级,有助于我们编写出正确、高效的代码。

以下是一些常见运算符的优先级(从高到低):

  • 括号:( )
  • 单目运算符:++、–、!、+、-、~、typeof等
  • 算术运算符:*、/、%、+、-等
  • 比较运算符:<、>、<=、>=、in、instanceof等
  • 相等运算符:==、!=、===、!==等
  • 逻辑运算符:&&、||等
  • 赋值运算符:=、+=、-=等

掌握了这些运算符及其优先级,我们就可以根据实际需求灵活运用,编写出更加高效、简洁的代码。

通过了解JavaScript中的运算符及其优先级,我们可以更好地编写和理解代码。掌握这些知识,你将能更加自如地操纵数据,实现你想要的功能。

收起阅读 »

揭秘JavaScript数据世界:一文通晓基本类型和引用类型的精髓!

在编程的世界里,数据是构建一切的基础。就像建筑师需要了解不同材料的强度和特性一样,程序员也必须熟悉各种数据类型。今天,我们就来深入探讨JavaScript中的数据类型,看看它们如何塑造我们的代码世界。一、JavaScript数据类型简介数据类型是计算机语言的基...
继续阅读 »

在编程的世界里,数据是构建一切的基础。就像建筑师需要了解不同材料的强度和特性一样,程序员也必须熟悉各种数据类型。

今天,我们就来深入探讨JavaScript中的数据类型,看看它们如何塑造我们的代码世界。

一、JavaScript数据类型简介

数据类型是计算机语言的基础知识,数据类型广泛用于变量、函数参数、表达式、函数返回值等场合。JavaScript语言的每一个值,都属于某一种数据类型。

Description

JavaScript的数据类型主要分为两大类:基本数据类型引用数据类型。下面就来详细介绍这两类数据类型中都包含哪些及如何使用它们。

二、基本(值类型)数据类型

首先,让我们从最基本的数据类型开始。JavaScript的基本数据类型包括:字符串(String)、数字(Number)、布尔(Boolean)、空(Null)、未定义(Undefined)、符号(Symbol)。

1、字符串(String)

tring类型用于表示由零或多个16位的Unicode字符组成的字符序列,即字符串。至于用单引号,还是双引号,在js中还是没有差别的。记得成对出现。

let name1 = '张三'
let name2 = "李四"
let name3 = `王五`

1.转换为字符串有2个方法:toString()、String()
let n = 100
n.toString() // '100' 数值类型转换为字符串类型
String(200) // '200' 数值类型转换为字符串类型

2.模板字符串相当于加强版的字符串,可以定义多行字符串。还可以利用${}在字符串中插入变量和表达式
let name = '张三丰'
let age = 180
`我叫${name},今年${age}岁啦!` // 我叫张三丰,今年180岁啦!

2、数字(Number)

该类型的表示方法有两种形式,第一种是整数,第二种为浮点数。整数:可以通过十进制,八进制,十六进制的字面值来表示。

浮点数:就是该数值中必须包含一个小数点,且小数点后必须有一位数字。

let num = 100  // 整数
let floatNum = 3.14 // 浮点数
// toFixed() 方法可以对计算结果进行四舍五入
let pi = Math.PI // 3.141592653589793
pi.toFixed(2) // 3.14 保留2位小数

// 八进制的值第一位必须是零0,后面每一位数的范围在0~7。如果某一位数超出范围,首位的0会被忽略,后面的数值会按照十进制来解析
let octalNum1 = 076 // 八进制的 63
let octalNum2 = 083 // 八进制 83
let octalNum3 = 06 // 八进制 6

// 十六进制的值前两位必须是0x,后面每一位十六进制数字的范围在0~9及A~F,字母A~F可以大写也可以小写。
let hexNum1 = 0xA // 十六进制 10
let hexNum2 = 0x3f // 十六进制 63

// 数值转换的三个方法 Number()、parseInt()、parseFloat()

1.Number() // 可以将字符串、布尔值、null、undefined 等转换为对应的数值,如果无法转换返回NaN
Number("123") // 输出123
Number("hello") // 输出NaN


2.parseInt() // 可以将字符串转换为整数,如果无法转换返回NaN
parseInt("123") // 输出123
parseInt("123.45") // 输出:123
parseInt("hello") // 输出NaN


3.parseFloat() // 可以将字符串转换为浮点数,如果无法转换返回NaN
parseFloat("123.45") // 输出123.45
parseFloat("hello") // 输出NaN

3、布尔(Boolean)

Boolean 数据类型只有两个值:true 和 false,分别代表真和假。很多时候我们需要将各种表达式和变量转换成 Boolean 数据类型来当作判断条件。


1.数值运算判断

1 + 2 === 3 // true
1 + 1 > 3 // false


2.数值类型转换
let bool1 = Boolean(0); // 数值转换为布尔值
let bool2 = Boolean(""); // 字符串转换为布尔值
let bool3 = Boolean(null); // null 转换为布尔值
let bool4 = Boolean(undefined); // undefined 转换为布尔值
let bool5 = Boolean(NaN); // NaN 转换为布尔值
let bool6 = Boolean([]); // 空数组转换为布尔值
let bool7 = Boolean({}); // 空对象转换为布尔值

ECMAScript 类型的值都有与布尔值等价的形式。可以调用 Boolean() 函数来将其他类型转换为布尔值。不同类型转换为布尔值的规则如下表

Description

4、未定义(Undefined)

在 JavaScript 中,undefined 是一个特殊的值和数据类型。当一个变量声明但未赋值时,该变量的值就是 undefined。它表示一个未定义或未初始化的值。

1.声明但未赋值的变量

// 当使用 var、let 或 const 声明一个变量但未对其赋值时,该变量的初始值为 undefined。
let n;
console.log(n) // 输出 undefined


2.未定义的属性

// 当访问一个不存在的属性时,该属性的值为undefined
let obj = { name: '张三丰' }
console.log(obj.age) // 输出 undefined


3.函数没有返回值

// 如果函数没有明确返回值或者使用 return 语句返回一个未定义的值,函数的返回值将是 undefined
function getName() {
// 没有返回值
}
console.log(foo()) // 输出 undefined


4.函数参数未传递

// 如果函数定义了参数但未传递相应的值,那么该参数的值将是 undefined
function getName(name) {
console.log("Hello, " + name)
}
getName() // 输出:Hello, undefined

5、空(Null)

在 JavaScript 中,null 是一个特殊的值和数据类型。它表示一个空值或者不存在的对象。

与undefined不同,null是JavaScript 保留关键字,而 undefined 只是一个常量。也就是说可以声明名称为 undefined 的变量,但将 null 作为变量使用时则会报错。

1.空值

// null 表示一个空值,用于表示变量的值为空
let name = null
console.log(name) // 输出 null


2.不存在的对象

// 当使用 typeof 运算符检测一个值为 null 的对象时,会返回 "object"
let obj = null
console.log(typeof obj) // 输出:object

null 与 undefined 区别

  • undefined 是表示一个未定义或未初始化的值,常用于声明但未赋值的变量,或者访问不存在的属性。

  • null 是一个被赋予的值,用于表示变量被故意赋值为空。

  • 在判断变量是否为空时,使用严格相等运算符(===),因为 undefined 和 null 在非严格相等运算符(==)下会相等。

let x;
let y = null;
console.log(x === undefined) // 输出:true
console.log(x === null) // 输出:false
console.log(y === null) // 输出:true
console.log(y === undefined) // 输出:false

6、符号(Symbol)

符号 (Symbols) 是 ECMAScript 第 6 版新定义的。符号类型是唯一的并且是不可修改的。

1.创建Symbol

// 使用全局函数 Symbol() 可以创建一个唯一的 Symbol 值
let s = Symbol()
console.log(typeof s) // 输出 symbol


2.唯一性

// 每个通过 Symbol() 创建的 Symbol 值都是唯一的,不会与其他 Symbol 值相等,即使它们的描述相同
let s1 = Symbol()
let s2 = Symbol()
console.log(s1 == s2) // 输出 false
let s3 = Symbol('hello')
let s4 = Symbol('hello')
console.log(s3 == s4) // 输出 false


3.Symbol 常量

// 通过 Symbol.for() 方法可以创建全局共享的 Symbol 值,称为 Symbol 常量
let s5 = Symbol.for('key')
let s6 = Symbol.for('key')
console.log(s5 === s6) // 输出 true

Symbol 的主要作用是创建独一无二的标识符,用于定义对象的属性名或者作为一些特殊的标记。它在一些特定的应用场景中非常有用,如在迭代器和生成器中使用 Symbol.iterator 标识可迭代对象。

三、引用数据类型

除了基本数据类型,JavaScript还有引用数据类型:对象(Object)、数组(Array)和函数(Function)

1、对象(Object)

Object 是一个内置的基本数据类型和构造函数。是一组由键、值组成的无序集合,定义对象类型需要使用花括号{ },它是 JavaScript 中最基本的对象类型,也是其他对象类型的基础。

1.创建对象

// Object 类型可以用于创建新的对象。可以使用对象字面量 {} 或者通过调用 Object() 构造函数来创建对象
let obj1 = {} // 使用对象字面量创建空对象
let obj2 = new Object() // 使用 Object() 构造函数创建空对象


2.添加、修改、删除属性

let obj = {}
obj.name = '张三丰' // 添加属性
obj.age = 30 // 添加属性
obj.name = '张无忌' // 修改属性
delete obj.age // 删除属性

2、数组(Array)

JavaScript 中,数组(Array)是一组按顺序排列的数据的集合,数组中的每个值都称为元素,而且数组中可以包含任意类型的数据。

在 JavaScript 中定义数组需要使用方括号[ ],数组中的每个元素使用逗号进行分隔。

数组的特点有哪些?

  • 有序集合: 数组是一种有序的数据集合,每个元素在数组中都有一个对应的索引,通过索引可以访问和操作数组中的元素。

  • 可变长度: 数组的长度是可变的,可以根据需要动态添加或删除元素,或者修改数组的长度。可以使用 push()、pop()、shift()、unshift() 等方法来添加或删除元素,也可以直接修改数组的 length 属性来改变数组的长度。

  • 存储不同类型的值: 数组可以存储任意类型的值,包括基本类型和对象类型。同一个数组中可以混合存储不同类型的值。

  • 索引访问: 通过索引来访问数组中的元素,索引从 0 开始。可以使用方括号语法 [] 或者点号语法 . 来访问数组的元素。

  • 内置方法: 数组提供了许多内置的方法,用于对数组进行常见的操作和处理,如添加、删除、查找、排序、遍历等。常用的数组方法包括 push()、pop()、shift()、unshift()、concat()、slice()、splice()、indexOf()、forEach()、map()、filter()、reduce() 等。

  • 可迭代性: 数组是可迭代的,可以使用 for…of 循环或者 forEach() 方法遍历数组中的元素。

1.创建数组

// 可以使用数组字面量 [] 或者通过调用 Array() 构造函数来创建数组。
let arr1 = [] // 使用数组字面量创建空数组
let arr2 = new Array() // 使用 Array() 构造函数创建空数组
let arr3 = [1, 2, 3] // 使用数组字面量创建包含初始值的数组


2.访问和修改数组元素

// 数组的元素通过索引访问,索引从 0 开始。可以使用索引来读取或修改数组的元素。
let arr = [1, 2, 3]
console.log(arr[0]) // 访问数组的第一个元素,输出:1
arr[1] = 5 // 修改数组的第二个元素
arr.length // 获取数组长度,输出:3

3、函数(Function)

ECMAScript中的函数是对象,与其他引用类型一样具有属性和方法。因此,函数名实际是一个指向函数对象的指针。

1.创建函数

// 可以使用函数声明或函数表达式来创建函数。函数声明使用 function 关键字,后面跟着函数名称和函数体,而函数表达式将函数赋值给一个变量。
// 函数声明
function add(a, b) {
return a + b
}

// 函数表达式
let multiply = function(a, b) {
return a * b
}


2.函数调用

// 函数可以通过函数名后面加括号 () 进行调用。调用函数时,可以传递参数给函数,函数可以接收参数并进行相应的处理。
let result = add(3, 5) // 调用 add 函数并传递参数
console.log(result) // 输出:8


3.函数返回值

// 函数可以使用 return 语句返回一个值,也可以不返回任何值。当函数执行到 return 语句时,会立即停止执行,并将返回值传递给函数调用者。
function calculateSum(a, b) {
return a + b
}
let result = calculateSum(2, 3)
console.log(result) // 输出:5


4.函数作用域

// 函数作用域是指函数内部声明的变量在函数内部有效,外部无法访问。函数内部定义的变量只能在函数内部被访问和使用,在函数外部是不可见的。

function myFunction() {
var x = 10 // 局部变量
console.log(x) // 在函数内部可见
}
myFunction() // 输出:10
console.log(x) // 报错:x is not defined

此外,JavaScript还有一些特殊的数据类型,如Date(表示日期和时间)、RegExp(表示正则表达式),以及ES6新增的Map、Set、WeakMap和WeakSet,用于存储特定类型的数据。


想要快速入门前端开发吗?推荐一个前端开发基础课程,这个老师讲的特别好,零基础学习无压力,知识点结合代码,边学边练,可以免费试看试学,还有各种辅助工具和资料,非常适合新手!点这里前往学习哦!


四、数据类型检测

检测数据类型可以使用typeof操作符,它可以检测基本数据类型和function,但无法区分不同的引用数据类型。

var arr = [
null, // object
undefined, // undefined
true, // boolean
12, // number
'haha', // string
Symbol(), // symbol
20n, // bigint
function(){}, // function
{}, // object
[], // object
]
for (let i = 0; i < arr.length; i++) {
console.log(typeof arr[i])
}

掌握JavaScript数据类型是成为一名高效开发者的关键。它们是构建程序的砖石,理解它们的用法和限制将使你能够构建更稳健、更可维护的代码。

现在,你已经了解了JavaScript的数据类型,是时候在你的代码中运用这些知识了。记住,实践是学习的最佳方式,所以动手尝试吧!

如果觉得本文对你有所帮助,别忘了点赞和分享哦!

收起阅读 »

商品 sku 在库存影响下的选中与禁用

web
分享一下,最近使用 React 封装的一个 Skus 组件,主要用于处理商品的sku在受到库存的影响下,sku项的选中和禁用问题; 需求分析 需要展示商品各规格下的sku信息,以及根据该sku的库存是否为空,判断是否禁用该sku的选择。 以下讲解将按照我的 ...
继续阅读 »

分享一下,最近使用 React 封装的一个 Skus 组件,主要用于处理商品的sku在受到库存的影响下,sku项的选中和禁用问题;


需求分析


需要展示商品各规格下的sku信息,以及根据该sku的库存是否为空,判断是否禁用该sku的选择。


sku-2.gif

以下讲解将按照我的 Skus组件 来,我这里放上我组件库中的线上 demo 和码上掘金的一个 demo 供大家体验;由于码上掘金导入不了组件库,我就上传了一份开发组件前的一份类似的代码,功能和代码思路是差不多的,大家也可以自己尝试写一下,可能你的思路会更优;


线上 Demo 地址


码上掘金



传入的sku数据结构


需要传入的商品的sku数据类型大致如下:


type SkusProps = { 
/** 传入的skus数据列表 */
data: SkusItem[]
// ... 其他的props
}

type SkusItem = {
/** 库存 */
stock?: number;
/** 该sku下的所有参数 */
params: SkusItemParam[];
};

type SkusItemParam = {
name: string;
value: string;
}

转化成需要的数据类型:


type SkuStateItem = {
value: string;
/** 与该sku搭配时,该禁用的sku组合 */
disabledSkus: string[][];
}[];

生成数据


定义 sku 分类


首先假装请求接口,造一些假数据出来,我这里自定义了最多 6^6 = 46656 种 sku。


sku-66.gif

下面的是自定义的一些数据:


const skuData: Record<string, string[]> = {
'颜色': ['红','绿','蓝','黑','白','黄'],
'大小': ['S','M','L','XL','XXL','MAX'],
'款式': ['圆领','V领','条纹','渐变','轻薄','休闲'],
'面料': ['纯棉','涤纶','丝绸','蚕丝','麻','鹅绒'],
'群体': ['男','女','中性','童装','老年','青少年'],
'价位': ['<30','<50','<100','<300','<800','<1500'],
}
const skuNames = Object.keys(skuData)

页面初始化



  • checkValArr: 需要展示的sku分类是哪些;

  • skusList: 接口获取的skus数据;

  • noStockSkus: 库存为零对应的skus(方便查看)。


export default () => {
// 这个是选中项对应的sku类型分别是哪几个。
const [checkValArr, setCheckValArr] = useState<number[]>([4, 5, 2, 3, 0, 0]);
// 接口请求到的skus数据
const [skusList, setSkusList] = useState<SkusItem[]>([]);
// 库存为零对应的sku数组
const [noStockSkus, setNoStockSkus] = useState<string[][]>([])

useEffect(() => {
const checkValTrueArr = checkValArr.filter(Boolean)
const _noStockSkus: string[][] = [[]]
const list = getSkusData(checkValTrueArr, _noStockSkus)
setSkusList(list)
setNoStockSkus([..._noStockSkus])
}, [checkValArr])

// ....

return <>...</>
}

根据上方的初始化sku数据,生成一一对应的sku,并随机生成对应sku的库存。


getSkusData 函数讲解


先看总数(total)为当前需要的各sku分类的乘积;比如这里就是上面传入的 checkValArr 数组 [4,5,2,3]120种sku选择。对应的就是 skuData 中的 [颜色前四项,大小前五项,款式前两项,面料前三项] 即下图的展示。


image.png

遍历 120 次,每次生成一个sku,并随机生成库存数量,40%的概率库存为0;然后遍历 skuNames 然后找到当前对应的sku分类即 [颜色,大小,款式,面料] 4项;


接下来就是较为关键的如何根据 sku的分类顺序 生成对应的 120个相应的sku。


请看下面代码中注释为 LHH-1 的地方,该 value 的获取是通过 indexArr 数组取出来的。可以看到上面 indexArr 数组的初始值为 [0,0,0,0] 4个零的索引,分别对应 4 个sku的分类;



  • 第一次遍历:


indexArr: [0,0,0,0] -> skuName.forEach -> 红,S,圆领,纯棉


看LHH-2标记处: 索引+1 -> indexArr: [0,0,0,1];



  • 第二次遍历:


indexArr: [0,0,0,1] -> skuName.forEach -> 红,S,圆领,涤纶


看LHH-2标记处: 索引+1 -> indexArr: [0,0,0,2];



  • 第三次遍历:


indexArr: [0,0,0,2] -> skuName.forEach -> 红,S,圆领,丝绸


看LHH-2标记处: 由于已经到达该分类下的最后一个,所以前一个索引加一,后一个重新置为0 -> indexArr: [0,0,1,0];



  • 第四次遍历:


indexArr: [0,0,1,0] -> skuName.forEach -> 红,S,V领,纯棉


看LHH-2标记处: 索引+1 -> indexArr: [0,0,1,1];



  • 接下来的一百多次遍历跟上面的遍历同理


image.png
function getSkusData(skuCategorys: number[], noStockSkus?: string[][]) {
// 最终生成的skus数据;
const skusList: SkusItem[] = []
// 对应 skuState 中各 sku ,主要用于下面遍历时,对 product 中 skus 的索引操作
const indexArr = Array.from({length: skuCategorys.length}, () => 0);
// 需要遍历的总次数
const total = skuCategorys.reduce((pre, cur) => pre * (cur || 1), 1)
for(let i = 1; i <= total; i++) {
const sku: SkusItem = {
// 库存:60%的几率为0-50,40%几率为0
stock: Math.floor(Math.random() * 10) >= 4 ? Math.floor(Math.random() * 50) : 0,
params: [],
}
// 生成每个 sku 对应的 params
let skuI = 0;
skuNames.forEach((name, j) => {
if(skuCategorys[j]) {
// 注意:LHH-1
const value = skuData[name][indexArr[skuI]]
sku.params.push({
name,
value,
})
skuI++;
}
})
skusList.push(sku)

// 注意: LHH-2
indexArr[indexArr.length - 1]++;
for(let j = indexArr.length - 1; j >= 0; j--) {
if(indexArr[j] >= skuCategorys[j] && j !== 0) {
indexArr[j - 1]++
indexArr[j] = 0
}
}

if(noStockSkus) {
if(!sku.stock) {
noStockSkus.at(-1)?.push(sku.params.map(p => p.value).join(' / '))
}
if(indexArr[0] === noStockSkus.length && noStockSkus.length < skuCategorys[0]) {
noStockSkus.push([])
}
}
}
return skusList
}

Skus 组件的核心部分的实现


初始化数据


需要将上面生成的数据转化为以下结构:


type SkuStateItem = {
value: string;
/** 与该sku搭配时,该禁用的sku组合 */
disabledSkus: string[][];
}[];

export default function Skus() {
// 转化成遍历判断用的数据类型
const [skuState, setSkuState] = useState<Record<string, SkuStateItem>>({});
// 当前选中的sku值
const [checkSkus, setCheckSkus] = useState<Record<string, string>>({});

// ...
}

将初始sku数据生成目标结构


根据 data (即上面的假数据)生成该数据结构。


第一次遍历是对skus第一项进行的,会生成如下结构:


const _skuState = {
'颜色': [{value: '红', disabledSkus: []}],
'大小': [{value: 'S', disabledSkus: []}],
'款式': [{value: '圆领', disabledSkus: []}],
'面料': [{value: '纯棉', disabledSkus: []}],
}

第二次遍历则会完整遍历剩下的skus数据,并往该对象中填充完整。


export default function Skus() {
// ...
useEffect(() => {
if(!data?.length) return
// 第一次对skus第一项的遍历
const _checkSkus: Record<string, string> = {}
const _skuState = data[0].params.reduce((pre, cur) => {
pre[cur.name] = [{value: cur.value, disabledSkus: []}]
_checkSkus[cur.name] = ''
return pre
}, {} as Record<string, SkuStateItem>)
setCheckSkus(_checkSkus)

// 第二次遍历
data.slice(1).forEach(item => {
const skuParams = item.params
skuParams.forEach((p, i) => {
// 当前 params 不在 _skuState 中
if(!_skuState[p.name]?.find(params => params.value === p.value)) {
_skuState[p.name].push({value: p.value, disabledSkus: []})
}
})
})

// ...接下面
}, [data])
}

第三次遍历主要用于为每个 sku的可点击项 生成一个对应的禁用sku数组 disabledSkus ,只要当前选择的sku项,满足该数组中的任一项,该sku选项就会被禁用。之所以保存这样的一个二维数组,是为了方便后面点击时的条件判断(有点空间换时间的概念)。


遍历 data 当库存小于等于0时,将当前的sku的所有参数传入 disabledSkus 中。


例:第一项 sku(红,S,圆领,纯棉)库存假设为0,则该选项会被添加到 disabledSkus 数组中,那么该sku选择时,勾选前三个后,第四个 纯棉 的勾选会被禁用。


image.png
export default function Skus() {
// ...
useEffect(() => {
// ... 接上面
// 第三次遍历
data.forEach(sku => {
// 遍历获取库存需要禁用的sku
const stock = sku.stock!
// stockLimitValue 是一个传参 代表库存的限制值,默认为0
// isStockGreaterThan 是一个传参,用来判断限制值是大于还是小于,默认为false
if(
typeof stock === 'number' &&
isStockGreaterThan ? stock >= stockLimitValue : stock <= stockLimitValue
) {
const curSkuArr = sku.params.map(p => p.value)
for(const name in _skuState) {
const curSkuItem = _skuState[name].find(v => curSkuArr.includes(v.value))
curSkuItem?.disabledSkus?.push(
sku.params.reduce((pre, p) => {
if(p.name !== name) {
pre.push(p.value)
}
return pre
}, [] as string[])
)
}
}
})

setSkuState(_skuState)
}, [data])
}

遍历渲染 skus 列表


根据上面的 skuState,生成用于渲染的列表,渲染列表的类型如下:


type RenderSkuItem = {
name: string;
values: RenderSkuItemValue[];
}
type RenderSkuItemValue = {
/** sku的值 */
value: string;
/** 选中状态 */
isChecked: boolean
/** 禁用状态 */
disabled: boolean;
}

export default function Skus() {
// ...
/** 用于渲染的列表 */
const list: RenderSkuItem[] = []
for(const name in skuState) {
list.push({
name,
values: skuState[name].map(sku => {
const isChecked = sku.value === checkSkus[name]
const disabled = isChecked ? false : isSkuDisable(name, sku)
return { value: sku.value, disabled, isChecked }
})
})
}
// ...
}

html css 大家都会,以下就简单展示了。最外层遍历sku的分类,第二次遍历遍历每个sku分类下的名称,第二次遍历的 item(类型为:RenderSkuItemValue),里面会有sku的值,选中状态和禁用状态的属性。


export default function Skus() {
// ...
return list?.map((p) => (
<div key={p.name}>
{/* 例:颜色、大小、款式、面料 */}
<div>{p.name}</div>
<div>
{p.values.map((sku) => (
<div
key={p.name + sku.value}
onClick={() =>
selectSkus(p.name, sku)}
>
{/* classBem 是用来判断当前状态,增加类名的一个方法而已 */}
<span className={classBem(`sku`, {active: sku.isChecked, disabled: sku.disabled})}>
{/* 例:红、绿、蓝、黑 */}
{sku.value}
</span>
</div>
))}
</div>
</div>

))
}

selectSkus 点击选择 sku


通过 checkSkus 设置 sku 对应分类下的 sku 选中项,同时触发 onChange 给父组件传递一些信息出去。


const selectSkus = (skuName: string, {value, disabled, isChecked}: RenderSkuItemValue) => {
const _checkSkus = {...checkSkus}
_checkSkus[skuName] = isChecked ? '' : value;
const curSkuItem = getCurSkuItem(_checkSkus)
// 该方法主要是 sku 组件点击后触发的回调,用于给父组件获取到一些信息。
onChange?.(_checkSkus, {
skuName,
value,
disabled,
isChecked: disabled ? false : !isChecked,
dataItem: curSkuItem,
stock: curSkuItem?.stock
})
if(!disabled) {
setCheckSkus(_checkSkus)
}
}

getCurSkuItem 获取当前选中的是哪个sku



  • isInOrder.current 是用来判断当前的 skus 数据是否是整齐排列的,这里当成 true 就好,判断该值的过程就不放到本文了,感兴趣可以看 源码


由于sku是按顺序排列的,所以只需按顺序遍历上面生成的 skuState,找出当前sku选中项对应的索引位置,然后通过 就可以直接得出对应的索引位置。这样的好处是能减少很多次遍历。


如果直接遍历原来那份填充所有 sku 的 data 数据,则需要很多次的遍历,当sku是 6^6 时, 则每次变换选中的sku时最多需要 46656 * 6 (data总长度 * 里面 sku 的 params) 次。


const getCurSkuItem = (_checkSkus: Record<string, string>) => {
const length = Object.keys(skuState).length
if(!length || Object.values(_checkSkus).filter(Boolean).length < length) return void 0
if(isInOrder.current) {
let skuI = 0;
// 由于sku是按顺序排列的,所以索引可以通过计算得出
Object.keys(_checkSkus).forEach((name, i) => {
const index = skuState[name].findIndex(v => v.value === _checkSkus[name])
const othTotal = Object.values(skuState).slice(i + 1).reduce((pre, cur) => (pre *= cur.length), 1)
skuI += index * othTotal;
})
return data?.[skuI]
}
// 这样需要遍历太多次
return data.find(s => (
s.params.every(p => _checkSkus[p.name] === getSkuParamValue(p))
))
}

isSkuDisable 判断该 sku 是否是禁用的


该方法是在上面 遍历渲染 skus 列表 时使用的。



  1. 开始还未有选中值时,需要校验 disabledSkus 的数组长度,是否等于该sku参数可以组合的sku总数,如果相等则表示禁用。

  2. 判断当前选中的 sku 还能组成多少种组合。例:当前选中 红,S ,而 isSkuDisable 方法当前判断的 sku 为 款式 中的 圆领,则还有三种组合 红\S\圆领\纯棉红\S\圆领\涤纶红\S\圆领\丝绸

  3. 如果当前判断的 sku 的 disabledSkus 数组中存在这三项,则表示该 sku 选项会被禁用,无法点击。


const isCheckValue = !!Object.keys(checkSkus).length

const isSkuDisable = (skuName: string, sku: SkuStateItem[number]) => {
if(!sku.disabledSkus.length) return false
// 1.当一开始没有选中值时,判断某个sku是否为禁用
if(!isCheckValue) {
let checkTotal = 1;
for(const name in skuState) {
if(name !== skuName) {
checkTotal *= skuState[name].length
}
}
return sku.disabledSkus.length === checkTotal
}

// 排除当前的传入的 sku 那一行
const newCheckSkus: Record<string, string> = {...checkSkus}
delete newCheckSkus[skuName]

// 2.当前选中的 sku 一共能有多少种组合
let total = 1;
for(const name in newCheckSkus) {
if(!newCheckSkus[name]) {
total *= skuState[name].length
}
}

// 3.选中的 sku 在禁用数组中有多少组
let num = 0;
for(const strArr of sku.disabledSkus) {
if(Object.values(newCheckSkus).every(str => !str ? true : strArr.includes(str))) {
num++;
}
}

return num === total
}

至此整个商品sku从生成假数据到sku的选中和禁用的处理的核心代码就完毕了。还有更多的细节问题可以直接查看 源码 会更清晰。


作者:滑动变滚动的蜗牛
来源:juejin.cn/post/7313979106890842139
收起阅读 »

实现抖音 “视频无限滑动“效果

web
前言 在家没事的时候刷抖音玩,抖音首页的视频怎么刷也刷不完,经常不知不觉的一刷就到半夜了😅 不禁感叹道 "垃圾抖音,费我时间,毁我青春😅" 这是我的 模仿抖音 系列文章的第二篇,本文将一步步实现抖音首页 视频无限滑动 的效果,干货满满。 如果您对滑动原理不太熟...
继续阅读 »

前言


在家没事的时候刷抖音玩,抖音首页的视频怎么刷也刷不完,经常不知不觉的一刷就到半夜了😅

不禁感叹道 "垃圾抖音,费我时间,毁我青春😅"


这是我的 模仿抖音 系列文章的第二篇,本文将一步步实现抖音首页 视频无限滑动 的效果,干货满满。


如果您对滑动原理不太熟悉,推荐先看我的这篇文章:200行代码实现类似Swiper.js的轮播组件


最终效果


在线预览:zyronon.gitee.io/douyin/


Github地址:github.com/zyronon/dou…


源码:SlideVerticalInfinite.vue


实现原理


无限滑动的原理和虚拟滚动的原理差不多,要保持 SlideList 里面永远只有 NSlideItem,就要在滑动时不断的删除和增加 SlideItem

滑动时调整 SlideList 的偏移量 translateY 的值,以及列表里那几个 SlideItemtop 值,就可以了


为什么要调整 SlideList 的偏移量 translateY 的值同时还要调整 SlideItemtop 值呢?

因为 translateY 只是将整个列表移动,如果我们列表里面的元素是固定的,不会变多和减少,那么没关系,只调整 translateY 值就可以了,上滑了几页就减几页的高度,下滑同理


但是如果整个列表向前移动了一页,同时前面的 SlideItem 也少了一个,,那么最终效果就是移动了两页...因为 塌陷 了一页

这显然不是我们想要的,所以我们还需要同时调整 SlideItemtop 值,加上前面少的 SlideItem 的高度,这样才能显示出正常的内容


步骤


定义




virtualTotal:页面中同时存在多少个 SlideItem,默认为 5


//页面中同时存在多少个SlideItem
virtualTotal: {
type: Number,
default: () => 5
},

设置这个值可以让外部组件使用时传入,毕竟每个人的需求不同,有的要求同时存在 10 条,有的要求同时存在 5 条即可。

不过同时存在的数量越大,使用体验就越好,即使用户快速滑动,我们依然有时间处理。

如果只同时存在 5 条,用户只需要快速滑动两次就到底了(因为屏幕中显示第 3 条,刚开始除外),我们可能来不及添加新的视频到最后




render:渲染函数,SlideItem内显示什么由render返回值决定


render: {
type: Function,
default: () => {
return null
}
},

之所以要设定这个值,是因为抖音首页可不只有视频,还有图集、推荐用户、广告等内容,所以我们不能写死显示视频。

最好是定义一个方法,外部去实现,我们内部去调用,拿到返回值,添加到 SlideList




list:数据列表,外部传入


list: {
type: Array,
default: () => {
return []
}
},

我们从 list 中取出数据,然后调用并传给 render 函数,将其返回值插入到 SlideList中


初始化



watch(
() => props.list,
(newVal, oldVal) => {
//新数据长度比老数据长度小,说明是刷新
if (newVal.length < oldVal.length) {
//从list中取出数据,然后调用并传给render函数,将其返回值插入到SlideList中
insertContent()
} else {
//没数据就直接插入
if (oldVal.length === 0) {
insertContent()
} else {
// 走到这里,说明是通过接口加载了下一页的数据,
// 为了在用户快速滑动时,无需频繁等待请求接口加载数据,给用户更好的使用体验
// 这里额外加载3条数据。所以此刻,html里面有原本的5个加新增的3个,一共8个dom
// 用户往下滑动时只删除前面多余的dom,等滑动到临界值(virtualTotal/2+1)时,再去执行新增逻辑
}
}
}
)

watch 监听 list 是因为它一开始不一定有值,通过接口请求之后才有值

同时当我们下滑 加载更多 时,也会触发接口请求新的数据,用 watch 可以在有新数据时,多添加几条到 SlideList 的最后面,这样用户快速滑动也不怕了


如何滑动


这里就不再赘述,参考我的这篇文章:200行代码实现类似Swiper.js的轮播组件


滑动结束


判断滑动的方向


当我们向上滑动时,需要删除最前面的 dom ,然后在最后面添加一个 dom

下滑时反之


slideTouchEnd(e, state, canNext, (isNext) => {
if (props.list.length > props.virtualTotal) {
//手指往上滑(即列表展示下一条视频)
if (isNext) {
//删除最前面的 `dom` ,然后在最后面添加一个 `dom`
} else {
//删除最后面的 `dom` ,然后在最前面添加一个 `dom`
}
}
})

手指往上滑(即列表展示下一条视频)



  • 首先判断是否要加载更多,快到列表末尾时就要加载更多数据了

  • 再判断是否符合 腾挪 的条件,即当前位置要大于 half,且小于列表长度减 half

  • 在最后面添加一个 dom

  • 删除最前面的 dom

  • 将所有 dom 设置为最新的 top 值(原因前面有讲,因为删除了最前面的 dom,导致塌陷一页,所以要加上删除 dom 的高度)


let half = (props.virtualTotal - 1) / 2

//删除最前面的 `dom` ,然后在最后面添加一个 `dom`
if (state.localIndex > props.list.length - props.virtualTotal && state.localIndex > half) {
emit('loadMore')
}

//是否符合 `腾挪` 的条件
if (state.localIndex > half && state.localIndex < props.list.length - half) {
//在最后面添加一个 `dom`
let addItemIndex = state.localIndex + half
let res = slideListEl.value.querySelector(`.${itemClassName}[data-index='${addItemIndex}']`)
if (!res) {
slideListEl.value.appendChild(getInsEl(props.list[addItemIndex], addItemIndex))
}

//删除最前面的 `dom`
let index = slideListEl.value
.querySelector(`.${itemClassName}:first-child`)
.getAttribute('data-index')
appInsMap.get(Number(index)).unmount()

slideListEl.value.querySelectorAll(`.${itemClassName}`).forEach((item) => {
_css(item, 'top', (state.localIndex - half) * state.wrapper.height)
})
}

手指往下滑(即列表展示上一条视频)


逻辑和上滑都差不多,不过是反着来而已



  • 再判断是否符合 腾挪 的条件,和上面反着

  • 在最前面添加一个 dom

  • 删除最后面的 dom

  • 将所有 dom 设置为最新的 top


//删除最后面的 `dom` ,然后在最前面添加一个 `dom`
if (state.localIndex >= half && state.localIndex < props.list.length - (half + 1)) {
let addIndex = state.localIndex - half
if (addIndex >= 0) {
let res = slideListEl.value.querySelector(`.${itemClassName}[data-index='${addIndex}']`)
if (!res) {
slideListEl.value.prepend(getInsEl(props.list[addIndex], addIndex))
}
}
let index = slideListEl.value
.querySelector(`.${itemClassName}:last-child`)
.getAttribute('data-index')
appInsMap.get(Number(index)).unmount()

slideListEl.value.querySelectorAll(`.${itemClassName}`).forEach((item) => {
_css(item, 'top', (state.localIndex - half) * state.wrapper.height)
})
}

其他问题


为什么不直接用 v-for直接生成 SlideItem 呢?


如果内容不是视频就可以。要删除或者新增时,直接操作 list 数据源,这样省事多了


如果内容是视频,修改 list 时,Vue 会快速的替换 dom,正在播放的视频,突然一下从头开始播放了😅😅😅


如何获取 Vue 组件的最终 dom


有两种方式,各有利弊



  • Vuerender 方法

    • 优点:只是渲染一个 VNode 而已,理论上讲内存消耗更少。

    • 缺点:但我在开发中,用了这个方法,任何修改都会刷新页面,有点难蚌😅



  • VuecreateApp 方法再创建一个 Vue 的实例

    • 和上面相反😅




import { createApp, onMounted, reactive, ref, render as vueRender, watch } from 'vue'

/**
* 获取Vue组件渲染之后的dom元素
* @param item
* @param index
* @param play
*/

function getInsEl(item, index, play = false) {
// console.log('index', cloneDeep(item), index, play)
let slideVNode = props.render(item, index, play, props.uniqueId)
const parent = document.createElement('div')
//TODO 打包到线上时用这个,这个在开发时任何修改都会刷新页面
if (import.meta.env.PROD) {
parent.classList.add('slide-item')
parent.setAttribute('data-index', index)
//将Vue组件渲染到一个div上
vueRender(slideVNode, parent)
appInsMap.set(index, {
unmount: () => {
vueRender(null, parent)
parent.remove()
}
})
return parent
} else {
//创建一个新的Vue实例,并挂载到一个div上
const app = createApp({
render() {
return <SlideItem data-index={index}>{slideVNode}</SlideItem>
}
})
const ins = app.mount(parent)
appInsMap.set(index, app)
return ins.$el
}
}

总结


原理其实并不难。主要是一开始可能会用 v-for 去弄,折腾半天发现不行。v-for 不行,就只能想想怎么把 Vue 组件搞到 html 里面去,又去研究如何获取 Vue 组件的最终 dom,又查了半天资料,Vue 官方文档也不写,还得去翻 api ,麻了


结束



以上就是文章的全部内容,感谢看到这里,希望对你有所帮助或启发!创作不易,如果觉得文章写得不错,可以点赞收藏支持一下,也欢迎关注我的公众号 前端张余让,我会更新更多实用的前端知识与技巧,期待与你共同成长~



作者:前端张余让
来源:juejin.cn/post/7361614921519054883
收起阅读 »

微信小程序用户授权获取手机号流程

web
在做小程序开发的过程中,经常会涉及到用户身份的问题,最普遍的就是要获取用户的手机号码,通过微信获取手机号码后可以减少很多操作,比如用户手机号码验证等,以及给用户发送提示短信等等。 ※ 正常情况下,小程序可获取的公开信息有:昵称,城市,ip等公开信息,如果想要手...
继续阅读 »

在做小程序开发的过程中,经常会涉及到用户身份的问题,最普遍的就是要获取用户的手机号码,通过微信获取手机号码后可以减少很多操作,比如用户手机号码验证等,以及给用户发送提示短信等等。


※ 正常情况下,小程序可获取的公开信息有:昵称,城市,ip等公开信息,如果想要手机号等非公开信息,前提是需要已认证的非个人小程序账号。


小程序具有非常简洁的api,通过小程序内部封装,只要通过一个类型 调取他们的api方法,便可直接拉起授权。


接下来和大家聊聊,获取用户手机号码的api:getPhoneNumber


官方文档:developers.weixin.qq.com/miniprogram…


大致实现思路:


无标题.png


1687328383569.png


获取用户手机号码 分为以下几步:


第一步,点击页面获取授权按钮


第二步,获取用户授权参数


第三步,根据加解密算法解密手机号码


接下来我们来实现以上三步(包含前后端)


前端


代码:


<button open-type="getPhoneNumber" bindgetphonenumber="getPhoneNumber"></button>


只要在你的view里将此button放上,便可拉起授权。


分析:
open-type=“getPhoneNumber” 这个是官方给出的api。


bindgetphonenumber=“getPhoneNumber”,是调起授权框之后用户操作的回调 用户点击了拒绝还是接受,由此方法接收通知。


bindgetphonenumber:官方指定点击事件。 如果使用uniapp则需要改为@getphonenumber,并遵循uniapp开发规范。


至于getPhoneNumber


下面看代码


getPhoneNumber (e) {
var that = this;
if (e.detail.errMsg == 'getPhoneNumber:fail user deny') { //用户点击拒绝
wx.showToast({
title: '请绑定手机号',
duration: 5000,
icon: 'none',
});
} else {
}
}

getPhoneNumber:fail user deny 拒绝与否,这个是依据
※ 很多博客上写着 getPhoneNumber:user deny 缺少了fail 。


以下为获取手机号封装方法:


getPhoneNumber(e) {
var that = this;
wx.login({
success (res) {
if (res.code) {
console.log('步骤2获检查用户登录状态,获取用户电话号码!', res)
wx.request({
url: '这里写自己的获取授权的服务器地址',
data: {code: res.code},
header: {'content-type': 'application/json'},
success: function(res) {
console.log("步骤三获取授权码,获取授权openid,session_key",res);
var userphone=res.data.data;
wx.setStorageSync('userphoneKey',userphone);
//解密手机号
var msg = e.detail.errMsg;
var sessionID=wx.getStorageSync("userphoneKey").session_key;
var encryptedData=e.detail.encryptedData;
var iv=e.detail.iv;
if (msg == 'getPhoneNumber:ok') {//这里表示获取授权成功
wx.checkSession({
success:function(){
//这里进行请求服务端解密手机号
that.deciyption(sessionID,encryptedData,iv);
},
fail:function(){
// that.userlogin()
}
})
}

},fail:function(res){
console.log("fail",res);
}
})
} else {
console.log('登录失败!' + res.errMsg)
}
}
})

以上


e.detail.iv
e.detail.encryptedData


两个必传 传到后台 换取mobile


后端


不管是spring还是servlet只要请求能进到该方法即可,所以重点关注中间部分,把参数值传正确即可
1687329461422.png


工具类方法 WxUtil.doGetStr(url)


1687329536242.png


以上值可以返回给前端,前端可以收到三个参数:openid,session_key,expires_in。


接着我们通过授权之后,获取第三个参数iv,调用下面方法进行服务端解密


1687329672055.png


服务端解密代码参考:


1687329836867.png


deciphering解密方法参考:


1687329894328.png


以上


作者:SC前端开发
来源:juejin.cn/post/7246997498571554871
收起阅读 »

如何快速实现一个无缝轮播效果

web
需求简介 轮播图是我们前端开发中的一个常见需求,在项目开发中,我们可以使用element、ant等UI库实现。某些场景,为了一个简单的功能安装一个库是没必要的,我们最好的选择就是手搓。 我们来看一个需求 上述需求核心就是实现一个无缝轮播的切换效果。以这个需求...
继续阅读 »

需求简介


轮播图是我们前端开发中的一个常见需求,在项目开发中,我们可以使用element、ant等UI库实现。某些场景,为了一个简单的功能安装一个库是没必要的,我们最好的选择就是手搓。


我们来看一个需求



上述需求核心就是实现一个无缝轮播的切换效果。以这个需求为例,我们看看最终实现效果:



实现思路


要想实现一个无缝的轮播效果,其实非常简单,核心思想就是动态改变显示的列表而已。比如我们有这样一个数组


const list = ref([
{ name: 1, id: 1 },
{ name: 2, id: 2 },
{ name: 3, id: 3 }
])

如果我们想无缝切换的展示这个数据,最简单的代码就是动态的改变下面的代码的index


<template>
<div>
{{ list[index] }}
</div>

</template>
<script setup>
const index = ref(0)
const list = ref([{ name: 1, id: 1 }, { name: 2, id: 2 }, { name: 2, id: 2 }])
<scriptp>

那如何实现切换的样式呢?也非常简单,我们只要给元素添加一个出现样式和离开样式即可。现在,我们来具体实现这样一个需求。


技术方案


数据的动态切换


要想实现一个数据的动态循环切换效果,是非常容易的:


<template>
<div v-for="(build, index) in list" :key="index">
<div v-show="index === selectIndex">
卡片自定义内容
</div>
</div>

</template>
<script setup>
const selectIndex = ref(0)
const list = ref(
[{ name: "卡片1", id: 1 }, { name: "卡片1", id: 2 }, { name: "卡片1", id: 2 }]
)

// #计时器实例
let timer: any = null

// >计时器逻辑
const timeFuc = () => {
timer = setInterval(() => {
// 更改选中的index
if (selectIndex.value >= list.value.length - 1) {
selectIndex.value = 0
} else {
selectIndex.value++
}
}, 5000)
}

timeFuc()
<scriptp>

上述代码中,我们设置了一个定时器,定时器每5s执行一次,每次执行都会动态更改当前要显示的数据索引值,当索引值达到最大实,在将其重置。通过上述的简单代码,我们就实现了一个可以自动切换的循环渲染的卡片。


动画添加


要想实现最终效果的动态效果也非常容易,我们只需要给每个元素出现时设置一些样式,离开时设置一些样式即可。借助vue的Transition组件,我们能很容易实现这样一个效果。



如果你不了解vue的Transition组件,请去官网补充下知识:cn.vuejs.org/guide/built…



<template>
<div class="main-content">
<Transition v-for="(build, index) in list" :key="selectIndex">
<div class="banner-scroll-wrap" v-show="index === selectIndex">
卡片自定义内容
</div>
</Transition>
</div>

</template>
<script setup>
const selectIndex = ref(0)
const list = ref(
[{ name: "卡片1", id: 1 }, { name: "卡片1", id: 2 }, { name: "卡片1", id: 2 }]
)

// #计时器实例
let timer: any = null

// >计时器逻辑
const timeFuc = () => {
timer = setInterval(() => {
// 更改选中的index
if (selectIndex.value >= list.value.length - 1) {
selectIndex.value = 0
} else {
selectIndex.value++
}
}, 5000)
}

timeFuc()
<scriptp>
<style lang="less" scoped>
.main-content {
position: relative;
height: 100%;
.banner-scroll-wrap {
position: absolute;
top: 0;
bottom: 0;
right: 0;
left: 0;
}
}

.v-enter-from {
transform: translateX(100%);
opacity: 0;
}

.v-enter-active,
.v-leave-active {
transition: transform 600ms ease-in-out, opacity 600ms ease-in-out;
}

.v-enter-to {
transform: translateX(0);
opacity: 1;
}

.v-leave-from {
transform: translateX(0);
opacity: 1;
}

.v-leave-to {
transform: translateX(-100%);
opacity: 0;
}
<style/>

上述代码中,由于 selectIndex是动态的,元素不断地在显示与隐藏。因此,Transition标签的进入样式和离开样式会动态触发,从而形成切换效果。



v-enter是元素的进入样式,进入时,我们从最右边偏移100%的距离到正常位置,透明度从0到1,这个过程持续0.6s,实现了元素左移淡入的效果。




v-leave是元素的离开样式,离开时,我们从正常位置偏移到100%的左侧位置,透明度从1到0,这个过程持续0.6s,实现了元素左移淡出的效果。



在这些类的共同作用下,我们实现了元素的动态切换。


你可能注意到了我给元素设置了一个banner-scroll-wrap类名,并使用了position: absolute,这样设置的注意目的是保证切换离开元素的淡出效果和进入元素的淡入效果是连贯的。如果你不这样写,可能会出现样式问题。


此外,注意我给Transition设置了key="Transition",这样些会保证每次数据在切换时,transition能够重新渲染,触发元素离开和进入的样式。


至此,我们就完成了基本功能样式



轮播的停止与恢复


很常见的一种情况就是我们需要鼠标放在卡片上时停止轮播,离开卡片的时候恢复轮播,这非常容易。


<template>
<div class="main-content" @mouseenter="stop()" @mouseleave="start()">
<Transition v-for="(build, index) in list" :key="selectIndex">
<div class="banner-scroll-wrap" v-show="index === selectIndex">
卡片自定义内容
</div>
</Transition>
</div>

</template>
<script setup>
const selectIndex = ref(0)
const list = ref(
[{ name: "卡片1", id: 1 }, { name: "卡片1", id: 2 }, { name: "卡片1", id: 2 }]
)

// #计时器实例
let timer: any = null

// >计时器逻辑
const timeFuc = () => {
timer = setInterval(() => {
// 更改选中的index
if (selectIndex.value >= list.value.length - 1) {
selectIndex.value = 0
} else {
selectIndex.value++
}
}, 5000)
}

// >开启轮播
const start = () => {
if (timer) return
timeFuc()
}

// >关闭轮播
const stop = () => {
clearInterval(timer)
timer = null
}

timeFuc()
<scriptp>
<style lang="less" scoped>
<style/>

解决重影问题


在某些情况下,我们离开这个页面很久后(浏览器切换到其他选项卡),然后在切回来的时候,可能会出现短暂的画面重影问题,这个问题也很好解决,加上下面的代码即可


<script setup>

//...

// 解决切屏后重影的问题
onMounted(() => {
document.addEventListener('visibilitychange', () => {
// 用户息屏、或者切到后台运行 (离开页面)
if (document.visibilityState === 'hidden') {
stop()
}
// 用户打开或回到页面
if (document.visibilityState === 'visible') {
start()
}
})
})

onBeforeUnmount(() => stop())

<scriptp>

visibilitychange 事件:当其选项卡的内容变得可见或被隐藏时,会在 document 上触发 visibilitychange 事件。该事件不可取消。


总结


在本教程中,我们通过简单代码实现了无缝轮播效果,样式是左右切换,我们也可以通过样式控制实现上下切换的效果,比如将translateX设置为translateY即可。


 .v-enter-from {
transform: translateY(100%);
opacity: 0;
}

时间原因,本教程也没有对技术上做深究,也希望各位大佬能提供自己的思路与建议,感谢大家分享!


作者:石小石Orz
来源:juejin.cn/post/7351790785743978537
收起阅读 »

打造聊天框丝滑滚动体验:AI 聊天框的翻转之道 ——— 聊天框发送消息后自动滚动到底部(前端框架通用)

web
逐字渲染的挑战 最近在开发AI聊天助手的时候,遇到了一个很有趣的滚动问题。我们需要开发一个类似微信聊天框的交互体验: 每当聊天框中展示新消息时,需要将聊天框滚动到底部,展示最新消息。 如果在 web 什么也不做,聊天体验可能是这样的,需要用户手动滚动到最新消...
继续阅读 »

逐字渲染的挑战


最近在开发AI聊天助手的时候,遇到了一个很有趣的滚动问题。我们需要开发一个类似微信聊天框的交互体验:


每当聊天框中展示新消息时,需要将聊天框滚动到底部,展示最新消息。


请在此添加图片描述


如果在 web 什么也不做,聊天体验可能是这样的,需要用户手动滚动到最新消息:


请在此添加图片描述


试想一下如何在 web 中实现微信的效果。每当聊天框中接收到新消息时,都需要调用滚动方法滚动到消息底部。


element.scrollIntoView({ behavior: "smooth", block: "end");

对于普通的聊天工具来说,这样实现没有什么大问题,因为聊天框接收到每条消息的长度都是确定的。但是 AI 大模型一般都是逐字渲染的,AI 助手聊天框接受的消息体大小不是固定的,而是会随着 AI 大模型的输出不断变大。如果仍使用 scrollIntoView 来滚动到底部,就需要监听消息体的变化,每次消息更新时都要通过 JavaScript 调用一次滚动方法,会造成一些问题:



  1. 频繁的 JavaScript 滚动调用。每输出一个文字要滚动一次,听起来就会性能焦虑。

  2. AI 正在输出内容时,用户无法滚动查看历史消息。用户向上滚动查看历史消息,会被 Javascript 不断执行的 scrollIntoView 打断。需要写特殊逻辑才能避免这个情况。

  3. 通过监听数据变化频繁的执行滚动,基于浏览器单线程的设计,不可避免的会造成滚动行为的滞后,导致聊天体验不够丝滑。


自然列表:灵感来源


聊天框接收到新消息时滚动到最新位置,总感觉这应该是一个很自然的行为,不需要这么多 Javascript 代码去实现滚动行为。


于是联想到了 Excel 表格,当我们在表格中第一行插入一行,这一行后边的内容会被很自然的挤下去。并不需要做什么滚动,这一行就会出现在最顶部的位置。


请在此添加图片描述


想到这里惊讶的发现,聊天框实际上不就是一个倒过来的列表吗? 列表最上边新增的行会把后边的行往下挤,而聊天框最下边新增消息需要把上边的消息往上挤。那假如我们将聊天框旋转 180° 呢...?


聊天框的翻转实现


翻转聊天框


请在此添加图片描述


利用 CSS transform: rotate(180deg) 将整个聊天框倒转,并且把接收到最新的消息插入到消息列表的头部。发现我们的设想确实是行得通的,新增的消息很自然的把历史消息顶了上去,消息卡片内容增加也能很自然的撑开。并且在消息输出时,也可以随意滚动查看历史记录。


滚动条调整与滚动行为反转


最核心的问题已经解决了,但总觉得哪里看起来怪怪的。滚动条怎么跑到左边,并且滚动行为和鼠标滚轮的方向反了,滚轮向上滚,聊天框却向下滚。(让人想起了 MacOS 连鼠标滚轮的反人类体验)


查阅文档发现 CSS 有个 direction: rtl; 属性可以改变内容的排布的方向。这样我们就可以把滚动条放回右边了。然后在通过监听滚动事件,改变滚动方向就可以恢复鼠标滚轮的滚动行为。


element.addEventListener('wheel', event => {
event.preventDefault(); // 阻止默认滚动行为
const { deltaY } = event; // 获取滚动方向和速度
chatContent.current.scrollTop -= deltaY; // 反转方向
});

请在此添加图片描述


消息卡片翻转恢复


可以看到目前就只剩下聊天框中的消息卡片是反的,接下来把聊天框中的消息卡片转正就大功告成了。我们在聊天框中,给每个消息卡片都添加 transform: rotate(180deg);direction: ltr; 样式,把消息重新转正。


这样就把翻转的行为全部隔离在了聊天框组件中。消息卡片组件完全感知不到自己其实已经被旋转了 180° 后又旋转了 180° 了。聊天框的父组件也完全不知道自己的子节点被转了又转。


742ea972f92d4e7abc7344e75c331467.avif


总结


最后总结一下,我们通过两行 CSS 代码 + 反转滚动行为,利用浏览器的默认行为完美的实现了 AI 聊天框中的滚动体验。


transform: rotate(180deg);
direction: rtl;

element.addEventListener('wheel', event => {
event.preventDefault(); // 阻止默认滚动行为
const { deltaY } = event; // 获取滚动方向和速度
chatContent.current.scrollTop -= deltaY; // 反转方向
});

DEMO 仓库:github.com/lrwlf/messa…




更新:


想到一个更简洁的办法可以达到相同的效果,只用把聊天框 CSS 设置为:


display: flex;
flex-direction: column-reverse;

让列表倒序渲染,并且像原来的方法一样,在消息列表的头部插入消息,就可以实现一样的效果。不需要对聊天框和消息体再进行旋转操作,也不需要反转滚动条的行为。


以上两种方法都存在一个相同的问题,当一开始聊天消息还很少时,聊天消息也会紧贴着底部,顶部会留出一片空白。


请在此添加图片描述


这时只需要在聊天列表的最开始设置一个空白的占位元素,把它的 CSS 设置为:


flex-grow: 1;
flex-shrink: 1;

就可以实现消息少的时候自动撑开,把消息撑到顶部。消息列表开始滚动时,占位元素又会被挤压消失,不影响列表滚动效果。


(为了演示,把占位元素设置为了黑色)


请在此添加图片描述


更新部分代码见: github.com/lrwlf/messa…


将 App.js 的 chat 组件,替换为 src/components/chat-flex


作者:lrwlf
来源:juejin.cn/post/7306693980959588379
收起阅读 »

实现一个支持@的输入框

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

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



20240415161851.gif


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



  1. @用户没有高亮

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


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


6ND88RssMr.gif


封装之后使用:


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

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



  1. 高亮效果

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

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

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


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


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

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

</div>

实现思路:



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

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

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

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

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


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


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

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

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

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

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




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

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


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

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


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

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

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

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

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



  1. 删除之前的@xxx字符

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

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

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

  5. 关闭选择器

  6. 将输入框重新聚焦


最后


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


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

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

完整组件代码


输入框主要逻辑代码:


let timer: NodeJS.Timeout | null = null;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

);
};

选择器代码


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

const { x, y } = cursorPosition;

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

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

);
});
export default SelectUser;

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


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

和后端吵架后,我写了个库,让整个前端团队更加规范!

web
前言 大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心~ 本文源码地址:github.com/sanxin-lin/… 背景 在平时的开发中,表格数据->(增加/编辑/查看)行->(增加/编辑)提交,这...
继续阅读 »

前言


大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心~



本文源码地址:github.com/sanxin-lin/…



背景


在平时的开发中,表格数据->(增加/编辑/查看)行->(增加/编辑)提交,这是很常见且简单的业务,但是就是这些业务,我也发现一些问题



首先我们来理性一下这些业务的逻辑



  • 第一步:请求回表格的数据

  • 第二步:点开(增加/编辑/查看)弹窗,如果是(编辑/查看),则需要将表格行的数据传到弹窗中回显

  • 第三部:如果是(编辑)弹窗,则需要把表单数据提交请求接口


我用一个图来概括大概就是:



问题所在


我不知道其他公司怎么样,但是就拿我自身来举例子,公司的后端跟前端的命名规则是不同的



  • 后端命名: 请求方法+字段类型+字段含义+下划线命名(比如 in_name、os_user_id)

  • 前端命名: 字段含义+驼峰命名(比如 name、userId)


回到刚刚的业务逻辑,还是那张图,假如我们前端不去管命名的话,那么数据的传输是这样的,发现了很多人都懒得去转换后端返回的字段名,直接拿着后端的字段名去当做前端的表单字段名,但这是不符合前端规范的



理想应该是表单要用前端的命名,比如这样



但是很多前端就是懒得去转换,原因有多个:



  • 开发者自身比较懒,或者没有规范意识

  • 回显时要转一次,提交时还要再转一次,每次总是得写一遍


解决方案


所以能不能写一个工具,解放开发者的压力又能达到期望的效果呢?比如我开发一个工具,然后像下面这样在弹窗里用



  • state: 响应式表单数据,可以用在弹窗表单中

  • resetState: 重置表单

  • inputState: 将表格行数据转成表单数据

  • outputState: 将表单数据转成提交请求的数据


配置的含义如下:



  • default: 表单字段默认值

  • input: 转入的字段名

  • output: 转出的字段名

  • inputStrategy: 转入的转换策略,可以选择内置的,也可以自定义策略函数

  • outputStrategy: 转出的转换策略,可以选择内置的,也可以自定义策略函数



转入和转出策略,内置了一些,你也可以自定义,内置的有如下



下面是自定义策略函数的例子,必须要在策略函数中返回一个转换值



这样的话,当我们执行对应的转换函数之后,会得到我们想要的结果



use-dsp


所以我开发了一个工具



源码地址:github.com/sanxin-lin/…



其实 dsp 意思就是



  • data

  • state

  • parameter


npm i use-dsp
yarn i use-dsp
pnpm i use-dsp

import useDSP from 'use-dsp'

为啥不从一开始就转?


有人会问,为啥不从一开始请求表格数据回来的时候,就把数据转成前端的命名规范?


其实这个问题我也想过,但是设想一下,有一些表格如果只是单纯做展示作用,那么就没必要去转字段名了,毕竟不涉及任何的数据传递。


但是需要编辑或者查看弹窗的表格,就涉及到了行数据的传递,那么就需要转字段名




作者:Sunshine_Lin
来源:juejin.cn/post/7360892717545799689
收起阅读 »

JavaScript精粹:26个关键字深度解析,编写高质量代码的秘诀!

JavaScript关键字是一种特殊的标识符,它们在语言中有固定的含义,不能用作变量名或函数名。这些关键字是JavaScript的基础,理解它们是掌握JavaScript的关键。今天,我们将一起探索JavaScript中的26个关键字,了解这些关键字各自独特的...
继续阅读 »

JavaScript关键字是一种特殊的标识符,它们在语言中有固定的含义,不能用作变量名或函数名。这些关键字是JavaScript的基础,理解它们是掌握JavaScript的关键。

今天,我们将一起探索JavaScript中的26个关键字,了解这些关键字各自独特的含义、特性和使用方法。

一、JavaScript关键字是什么

Javascript关键字(Keyword)是指在Javascript语言中有特定含义,成为Javascript语法中一部分的那些字,是 JavaScript 语言内部使用的一组名字(或称为命令)。

Description


Javascript关键字是不能作为变量名和函数名使用的。使用Javascript关键字作为变量名或函数名,会使Javascript在载入过程中出现编译错误。

Java中的关键字可用于表示控制语句的开始或结束,或者用于执行特定操作等。按照规则,关键字也是语言保留的,不能用作标识符。

下面我们来详细介绍一下JavaScript关键字的作用和使用方法。

二、JavaScript的26个关键字

JavaScript是一种广泛使用的编程语言,它具有丰富的关键字,这些关键字在JavaScript语言中发挥着重要的作用,JavaScript一共提供了26个关键字:


break, case, catch, continue, debugger, default, delete, do, else, finally, for, function, if, in, instanceof, new, return, switch, this, throw, try, typeof, var, void, while, with
其中,debugger在ECMAScript 5 新增的。

1、break:跳出 循环

break用于跳出循环结构。循环结构是一种重复执行某个代码块的结构,break关键字可以用于循环结构中的条件语句中,用于跳出循环。例如:

for (var i = 0; i < 10; i++) {
if (i == 5) {
break; // 当i等于5时跳出循环
}
console.log(i);
}

2、case:捕捉

它用于在switch语句中定义一个分支。switch语句是一种根据表达式的值执行不同代码块的结构,case关键字可以用于switch语句中,用于定义不同的分支。例如:

switch (n) {

case 1:
console.log('n等于1');
break;
case 2:
console.log('n等于2');
break;
default:
console.log('n不等于1或2');
break;
}

3、catch:配合try进行错误判断

catch用于捕获异常。异常是一种程序运行时出现的错误,catch关键字可以用于try-catch语句中,用于捕获并处理异常。例如:

try {
// 代码
} catch (e) {
console.log('发生异常:' + e.message);
}

4、continue:继续

continue用于跳过当前循环中的某个迭代。循环结构是一种重复执行某个代码块的结构,continue关键字可以用于循环结构中的条件语句中,用于跳过当前迭代。例如:

for (var i = 0; i < 10; i++) {
if (i == 5) {
continue; // 当i等于5时跳过当前迭代
}
console.log(i);
}

5、debugger:设置断点

它用于在代码中设置断点,方便调试代码。调试是一种在代码运行时发现和解决问题的过程,debugger关键字可以用于代码中,用于设置断点。例如:

function foo() {

var x = 10;

debugger; // 在这里设置断点
console.log(x);

}

6、default:配合switch,当条件不存在时使用该项

default用于在switch语句中定义一个默认分支。switch语句是一种根据表达式的值执行不同代码块的结构,default关键字可以用于switch语句中,用于定义默认分支。例如:


switch (n) {
case 1:
console.log('n等于1');
break;
case 2:
console.log('n等于2');
break;
default:
console.log('n不等于1或2');
break;
}

7、delete:删除了一个属性

delete用于删除对象的属性或数组中的元素。对象是JavaScript中的一种数据类型,它由一组属性组成,delete关键字可以用于对象的属性中,用于删除属性。例如:

var obj = {a: 1, b: 2, c: 3};
delete obj.b; // 删除对象obj的属性b
console.log(obj); // 输出{a: 1, c: 3}

8、do:声明一个循环

do用于定义一个do-while循环结构。循环结构是一种重复执行某个代码块的结构,do关键字可以用于do-while循环中,用于定义循环体。例如:

var i = 0;
do {
console.log(i);
i++;
} while (i < 10);

9、else:否则//配合if条件判断,用于条件选择的跳转

else用于在if语句中定义一个分支。if语句是一种根据条件执行不同代码块的结构,else关键字可以用于if语句中,用于定义另一个分支。例如:

if (n == 1) {
console.log('n等于1');
} else {
console.log('n不等于1');
}

10、finally:预防出现异常时用的

finally用于定义一个try-catch-finally语句中的finally块。try-catch-finally语句是一种用于处理异常的结构,finally关键字可以用于finally块中,用于定义一些必须执行的代码。例如:

try {
// 可能会抛出异常的代码
} catch (e) {
// 处理异常的代码
} finally {
// 必须执行的代码
}

11、for:循环语句

for用于定义一个for循环结构。循环结构是一种重复执行某个代码块的结构,for关键字可以用于for循环中,用于定义循环条件。例如:

for (var i = 0; i < 10; i++) {
console.log(i);
}

12、function:定义函数的关键字

function用于定义一个函数。函数是一种封装了一段代码的结构,它可以接受参数并返回结果。function关键字可以用于函数定义中,用于定义函数名和参数列表。例如:

function add(a, b) {
return a + b;
}
console.log(add(1, 2)); // 输出3

13、if:定义一个if语句

if用于定义一个if语句。if语句是一种根据条件执行不同代码块的结构,if关键字可以用于if语句中,用于定义条件。例如:


if (n == 1) {
console.log('n等于1');
} else {
console.log('n不等于1');
}

14、in:判断某个属性属于某个对象

in用于判断一个对象是否包含某个属性。对象是JavaScript中的一种数据类型,它由一组属性组成,in关键字可以用于对象中,用于判断对象是否包含某个属性。例如:


var obj = {a: 1, b: 2, c: 3};
if ('a' in obj) {
console.log('obj包含属性a');
} else {
console.log('obj不包含属性a');
}

15、instanceof:某个对象是不是另一个对象的实例

instanceof用于判断一个对象是否是某个类的实例。类是JavaScript中的一种数据类型,它由一组属性和方法组成,instanceof关键字可以用于类中,用于判断对象是否是该类的实例。例如:

function Person(name) {
this.name = name;
}
var p = new Person('张三');
if (p instanceof Person) {
console.log('p是Person类的实例');
} else {
console.log('p不是Person类的实例');
}

16、new:创建一个新对象

new用于创建一个对象。对象是JavaScript中的一种数据类型,它由一组属性和方法组成,new关键字可以用于类中,用于创建该类的实例。例如:

function Person(name) {
this.name = name;
}
var p = new Person('张三');
console.log(p.name); // 输出张三

17、return:返回

return用于从函数中返回一个值。函数是JavaScript中的一种数据类型,它由一段代码块组成,return关键字可以用于函数中,用于返回函数的执行结果。例如:

function add(a, b) {
return a + b;
}

console.log(add(1, 2)); // 输出3

18、switch:弥补if的多重判断语句

switch用于根据不同的条件执行不同的代码块。switch语句是一种根据条件执行不同代码块的结构,switch关键字可以用于switch语句中,用于定义条件。例如:

var day = 3;
switch (day) {
case 1:
console.log('星期一');
break;
case 2:
console.log('星期二');
break;
case 3:
console.log('星期三');
break;
default:
console.log('不是星期一、二、三');
}

19、this:总是指向调用该方法的对象

this用于引用当前对象。对象是JavaScript中的一种数据类型,它由一组属性和方法组成,this关键字可以用于对象中,用于引用当前对象的属性和方法。例如:

var obj = {
name: '张三',
sayName: function() {
console.log(this.name);
}
};
obj.sayName(); // 输出张三

20、throw:抛出异常

throw用于抛出一个异常。异常是JavaScript中的一种错误类型,它可以用于在程序运行过程中发现错误并停止程序的执行。throw关键字可以用于函数中,用于抛出异常。例如:

function divide(a, b) {
if (b === 0) {
throw new Error('除数不能为0');

}
return a / b;
}
console.log(divide(10, 0)); // 抛出异常

21、try:接受异常并做出判断

try用于捕获异常。异常是JavaScript中的一种错误类型,它可以用于在程序运行过程中发现错误并停止程序的执行。try语句是一种捕获异常的结构,try关键字可以用于try语句中,用于捕获异常。例如:

function divide(a, b) {
if (b === 0) {
throw new Error('除数不能为0');
}
return a / b;
}
try {
console.log(divide(10, 0)); // 抛出异常
} catch (e) {
console.log(e.message); // 输出除数不能为0
}

22、typeof:检测变量的数据类型

typeof用于获取变量的类型。变量是JavaScript中的一种数据类型,它可以是数字、字符串、布尔值等。typeof关键字可以用于变量中,用于获取变量的类型。例如:

var a = 10;
console.log(typeof a); // 输出number

23、var:声明变量

var用于声明变量。变量是JavaScript中的一种数据类型,它可以用于存储数据。var关键字可以用于变量中,用于声明变量。例如:

var a = 10;
console.log(a); // 输出10

24、void:空/ 声明没有返回值

void它用于执行一个表达式并返回undefined。undefined是JavaScript中的一种特殊值,它表示一个未定义的值。void关键字可以用于表达式中,用于执行表达式并返回undefined。例如:

function doSomething() {
console.log('执行了doSomething函数');
}
var result = void doSomething();
console.log(result); // 输出undefined

25、while

while用于创建一个循环结构。循环是JavaScript中的一种控制结构,它可以用于重复执行一段代码。while关键字可以用于循环中,用于创建一个基于条件的循环。例如:

var i = 0;
while (i < 10) {
console.log(i);
i++;
}

26、with

with用于创建一个作用域。作用域是JavaScript中的一种机制,它可以用于控制变量的作用范围。with关键字可以用于代码块中,用于创建一个作用域。例如:


var obj = {
name: '张三',
age: 20
};
with (obj) {
console.log(name); // 输出张三
console.log(age); // 输出20
}


三、JS关键字注意事项

在开发过程中使用关键字我们需要注意以下几点:

  • 区分大小写: JavaScript是区分大小写的,因此关键字的大小写必须正确。

  • 不能用作变量名: 关键字不能被用作变量名,函数名等等,会出现问题

  • 不需要硬记关键字: 关键字不用去硬记,我们在编写代码时根据系统的提示去规避就可以了

  • 保留字: JavaScript有一些保留字,不能用作变量名、函数名或属性名。

  • 不要使用全局变量: 尽量避免使用全局变量,以免与其他脚本或库发生冲突。可以使用立即执行函数表达式(IIFE)或模块模式来避免全局变量污染。

  • 使用严格模式: 在代码中添加"use strict"指令,以启用严格模式。这将有助于避免一些常见的错误,例如未声明的变量、隐式类型转换等。

  • 避免使用eval()函数: eval()函数用于动态执行字符串中的JavaScript代码,但可能导致性能问题和安全风险。尽量避免使用eval(),寻找其他替代方案。

  • 不要使用with语句: with语句会改变代码的作用域链,可能导致意外的错误。尽量避免使用with语句,改用局部变量或对象属性访问。

  • 避免使用重复的标识符: 确保变量名、函数名和属性名在同一作用域内是唯一的,以避免命名冲突。

  • 遵循编码规范: 遵循一致的命名约定、缩进风格和代码结构,以提高代码的可读性和可维护性。

想要快速入门前端开发吗?推荐一个前端开发基础课程,这个老师讲的特别好,零基础学习无压力,知识点结合代码,边学边练,可以免费试看试学,还有各种辅助工具和资料,非常适合新手!

点这里前往学习哦!


四、关于保留字的了解

除了关键字还有个“保留字”的概念,所谓保留字,实际上就是预留的“关键字”。意思是现在虽然还不是关键字,但是未来可能会成为关键字,同样不能使用它们当充当变量名、函数名等标识符。

下面是JavaScript中保留字的含义,大家同样不用记,了解一下就行了。

Description

以上就是关于JavaScript关键字的相关内容了,通过了解这26个JavaScript关键字的含义、特性和使用方法,你已经迈出了成为编程高手的重要一步。

记住,实践是学习的关键,不断地编写代码并应用这些关键字,你将逐渐掌握JavaScript的精髓。

如果觉得本文对你有所帮助,别忘了点赞和分享哦!

收起阅读 »

文本美学:text-image打造视觉吸引力

web
当我最近浏览 GitHub 时,偶然发现了一个项目,它能够将文字、图片和视频转化为文本,我觉得非常有趣。于是我就花了一些时间了解了一下,发现它的使用也非常简单方便。今天我打算和家人们分享这个发现。 项目介绍 话不多说,我们先看下作者的demo效果: _202...
继续阅读 »

当我最近浏览 GitHub 时,偶然发现了一个项目,它能够将文字、图片和视频转化为文本,我觉得非常有趣。于是我就花了一些时间了解了一下,发现它的使用也非常简单方便。今天我打算和家人们分享这个发现。


项目介绍


话不多说,我们先看下作者的demo效果:


微信截图_20240420194201.png


_20240420194201.jpg


text-image可以将文字、图片、视频进行「文本化」


只需要通过简单的配置即可使用。


虽然这个项目star数很少,但确实是一个很有意思的项目,使用起来很简单的项目。


_20240420194537.jpg


_20240420194537.jpg


github地址:https://github.com/Sunny-117/text-image


我也是使用这个项目做了一个简单的web页面,感兴趣的家人可以使用看下效果:


web地址:http://h5.xiuji.mynatapp.cc/text-image/


_20240420211509.jpg


_20240420211509.jpg


项目使用


这个项目使用起来相对简单,只需按作者的文档使用即可,虽然我前端属于小白的水平,但还是在ai的帮助下做了一个简单的html页面,如果有家人需要的话可以私信我,我发下文件。下边我们就介绍下:



  • 文字「文本化」


先看效果:


_20240420195701.jpg


_20240420195701.jpg


我们在这儿是将配置的一些参数在页面上做了一个可配置的表单,方便我们配置。


家人们想自己尝试的话可以试下以下这个demo。


demo.html


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Document</title>
</head>

<body>
<canvas id="demo"></canvas>
<script src="http://h5.xiuji.mynatapp.cc/text-image/text-image.iife.js"></script>
<script>
textImage.createTextImage({
canvas: document.getElementById('demo'),
replaceText: '123',
source: {
text: '修己xj',
},
});
</script>
</body>
</html>


  • 图片「文本化」


_20240420200651.jpg


_20240420200651.jpg


demo.html


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

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

<body>
<canvas id="demo"></canvas>
<script src="http://h5.xiuji.mynatapp.cc/text-image/text-image.iife.js"></script>
<script>
textImage.createTextImage({
canvas: document.getElementById('demo'),
raduis: 7,
isGray: true,
source: {
img: './assets/1.png',
},
});
</script>
</body>

</html>


  • 视频「文本化」


动画1.gif


1.gif


demo.html


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

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

<body>
<canvas id="demo"></canvas>
<script src="http://h5.xiuji.mynatapp.cc/text-image/text-image.iife.js"></script>

<script>
textImage.createTextImage({
canvas: document.getElementById('demo'),
raduis: 8,
isGray: true,
source: {
video: './assets/1.mp4',
height: 700,
},
});
</script>
</body>

</html>

需要注意的是:作者在项目中提供的视频的demo这个属性值有错误,我们需要改正后方可正常显示:


_20240420211124.jpg


_20240420211124.jpg


总结


text-image 是一个强大的前端工具,可以帮助用户快速、轻松地将文本、图片、视频转换成文本化的图片,增强文本内容的表现力和吸引力。


作者:修己xj
来源:juejin.cn/post/7359510120248786971
收起阅读 »

vue反编译dist包到源码

web
最近由于公司老项目上的问题,由于项目很老,之前交接的源码包中缺少了很大一部分模块,但是现在线上的环境和dist包是正常运行的,领导希望能够手动将这部分补全,由于前期项目的不规范,缺少接口文档以及原型图,因此无法知道到底该如何补全,因此,我想着能不能通过dist...
继续阅读 »

最近由于公司老项目上的问题,由于项目很老,之前交接的源码包中缺少了很大一部分模块,但是现在线上的环境和dist包是正常运行的,领导希望能够手动将这部分补全,由于前期项目的不规范,缺少接口文档以及原型图,因此无法知道到底该如何补全,因此,我想着能不能通过dist包去反编译源码包呢,经过多方面探索发现是可行的,但是只能编译出vue文件,但是也满足基本需要了。


1.如何反编译


1.首先需要在管理员模式下打开cmd


2.找到需要编译的dist/static/js的目录下
执行完成后在该目录会看到目录下存在下面的文件名:0.7ab7d1434ffcc747c1ca.js.map,这里以0.7ab7d1434ffcc747c1ca.js.map为例,如下图:


image.png


3.全局安装reverse-sourcemap资源



npm install --global reverse-sourcemap



4.反编译
执行:reverse-sourcemap --output-dir source 0.7ab7d1434ffcc747c1ca.js.map


2.脚本反编译


上面的方式执行完毕,确实在source中会出现源码,那么有没有可能用脚本去执行呢,通过node的child_process模块中的exec方式便可以执行reverse-sourcemap --output-dir source这个命令,那么只需要拿到当前文件夹中包含.map文件即可,那么可以借助node中fs模块,递归读取文件名,并使用正则将所有.map的文件提取出来放在一个集合或数组中,在对数组进行递归循环执行reverse-sourcemap --output-dir source这个命令


2.1根据child_process模块编写执行函数



function executeReverseSourceMap(outputDir) {
// 构建 reverse-sourcemap 命令
const command = `reverse-sourcemap --output-dir source ${outputDir}`;

// 执行命令
exec(command, (error, stdout, stderr) => {
if (error) {
console.error(`执行命令时出错:${error.message}`);
return;
}
if (stderr) {
console.error(`命令输出错误:${stderr}`);
return;
}
console.log(`命令输出结果:${stdout}`);
});
}

2.2读取文件并匹配文件


// // 读取文件夹中的文件
fs.readdir(folderPath, (err, files) => {
if (err) {
console.error('读取文件夹时出错:', err);
return;
}
// 遍历文件
files.forEach(file => {
// 使用正则表达式匹配特定格式的文件名
const match = /^(\d+)\..+\.js\.map$/.exec(file);
if (match) {
// 如果匹配成功,将文件名存入数组
targetFiles.push(match[0]);
}
});

// 输出目标文件名数组
targetFiles.forEach(file=>{
executeReverseSourceMap(file)
})
});

2.3完整的执行代码


const fs = require('fs');
const path = require('path');
const { exec } = require('child_process');
// 文件夹路径
const folderPath = '../js';

// 存放目标文件名的数组
const targetFiles = [];
function executeReverseSourceMap(outputDir) {
// 构建 reverse-sourcemap 命令
const command = `reverse-sourcemap --output-dir source ${outputDir}`;

// 执行命令
exec(command, (error, stdout, stderr) => {
if (error) {
console.error(`执行命令时出错:${error.message}`);
return;
}
if (stderr) {
console.error(`命令输出错误:${stderr}`);
return;
}
console.log(`命令输出结果:${stdout}`);
});
}
// // 读取文件夹中的文件
fs.readdir(folderPath, (err, files) => {
if (err) {
console.error('读取文件夹时出错:', err);
return;
}
// 遍历文件
files.forEach(file => {
// 使用正则表达式匹配特定格式的文件名
const match = /^(\d+)\..+\.js\.map$/.exec(file);
if (match) {
// 如果匹配成功,将文件名存入数组
targetFiles.push(match[0]);
}
});

// 输出目标文件名数组
targetFiles.forEach(file=>{
executeReverseSourceMap(file)
})
});

image.png


3最终结果展示图


image.png


作者:ws_qy
来源:juejin.cn/post/7359893196439207972
收起阅读 »

为什么不建议在 Vue <style> 中使用 scoped?

web
前言 标签下编写样式。不知你是否留意,在 标签下有一个属性经常出现 - scoped。你知道它起到什么作用吗?原理是怎样的?有没有什么弊端呢?今天我们就来聊聊它。 1. 什么是 scoped? scoped 顾名思义,与作用域有关,因为是设计组件样式的,所以...
继续阅读 »

前言

亲爱的小伙伴,你好!我是 嘟老板。我们使用 Vue 开发页面时,经常需要在 

收起阅读 »

JavaScript注释:单行注释和多行注释详解

为了提高代码的可读性,JS与CSS一样,也提供了注释功能。JS中的注释主要有两种,分别是单行注释和多行注释。在编程的世界里,注释是那些默默无闻的英雄,它们静静地站在代码的背后,为后来的维护者、为未来的自己,甚至是为那些偶然间翻阅你代码的开发者提供着不可或缺的信...
继续阅读 »

为了提高代码的可读性,JS与CSS一样,也提供了注释功能。JS中的注释主要有两种,分别是单行注释和多行注释。

在编程的世界里,注释是那些默默无闻的英雄,它们静静地站在代码的背后,为后来的维护者、为未来的自己,甚至是为那些偶然间翻阅你代码的开发者提供着不可或缺的信息。

Description


今天,我们就来深入探讨JavaScript中的注释,让我们的代码不仅能够运行,还能够“说话”。

一、什么是JavaScript注释

JavaScript注释是用来解释代码的,不会被浏览器执行。它们可以帮助其他开发者理解代码的功能和目的。

注释就像是给代码穿上了一件华丽的外衣,让我们的代码更加优雅、易读。如下图中的例子所示:


Description


在JavaScript中,有两种类型的注释:单行注释和多行注释。下面分别讲解这两种注释的含义和使用。


二、JavaScript注释的种类

1、单行注释

单行注释: 使用两个斜杠(//)开头,后面的内容直到该行结束都被视为注释。例如:

// 这是一个单行注释
console.log("Hello, World!"); // 这也是一个单行注释

它适用于简短的注释,比如对某一行代码的快速说明。

2、多行注释

多行注释: 使用斜杠星号(/)开头,星号斜杠(/)结尾,中间的内容都被视为注释。

例如:

/*
这是一个多行注释
可以跨越多行
*/

console.log("Hello, World!");

这种注释可以跨越多行,适合用于函数描述、复杂的算法解释或者临时屏蔽代码块。

注意: 在HTML文件中,如果需要将JavaScript代码嵌入到<script>标签中,可以使用以下方法来添加多行注释:

<script>
<!--
这是一个多行注释
可以跨越多行
-->

console.log("Hello, World!");
</script>


三、JavaScript注释的作用

1、解释代码功能:

通过注释,我们可以解释代码的功能和作用,让其他程序员更容易理解我们的代码。

// 这是一个求和函数
function sum(a, b) {
return a + b;
}

2、 标记代码状态:

我们可以使用注释来标记代码的状态,例如TODO、FIXME等,提醒自己或其他程序员注意这些问题。

// TODO: 优化这个函数的性能
function slowFunction() {
// ...
}

3、临时禁用代码:

当我们需要暂时禁用某段代码时,可以使用注释将其包裹起来,而不是直接删除。

// function oldFunction() {
// // ...
// }


想要快速入门前端开发吗?推荐一个前端开发基础课程,这个老师讲的特别好,零基础学习无压力,知识点结合代码,边学边练,可以免费试看试学,还有各种辅助工具和资料,非常适合新手!

点这里前往学习哦!


四、如何写好注释

注释虽好,但过多或不当的注释反而会成为阅读代码的障碍。我们在写注释时也要注意以下几点:

  • 简洁明了: 注释应该简单明了,能够快速传达信息。

  • 适当使用: 不要过度使用注释,只有在必要的时候才添加。

  • 保持一致: 在团队开发中,要遵循统一的注释规范,以便于团队成员之间的沟通。

  • 适时更新: 随着代码的变更,记得更新相关的注释。

JavaScript注释是我们编程过程中的得力助手,它们不仅能够帮助我们更好地理解代码,还能提高代码的可读性和可维护性。让我们一起学会使用注释,让我们的代码更加精彩!

收起阅读 »

记一次划线需求的实现方式

web
1 背景 1.1 需求背景 前半年接了一个划线需求,一直没空总结下。微信公众号和微信读书等读书类应用都有此功能,但前者只对部分用户开放了,并且没有长期运营。 这次只谈下划线技术实现本身。 1.2 功能详叙 用户可以对文章句子进行长按选区,过程中弹出面板,且面...
继续阅读 »

1 背景


1.1 需求背景


前半年接了一个划线需求,一直没空总结下。微信公众号和微信读书等读书类应用都有此功能,但前者只对部分用户开放了,并且没有长期运营。


这次只谈下划线技术实现本身。


1.2 功能详叙



  1. 用户可以对文章句子进行长按选区,过程中弹出面板,且面板位置动态变化,点击点赞按钮后生成划线;

  2. 点击划线句子默认选中,并弹出面板,显示所点句子点赞量;

  3. 划线句子可以合并,规则是选取句子和已赞过的句子有交叉时合并为一条新的首尾更长的句子,选取的句子被包含在已赞过的句子中时显示点赞量,选取句子包含了已赞句子则删掉已赞句子并对新句子点赞量加一;

  4. 点赞量超过3的句子才外显;

  5. 他人的划线句子用虚线展示,自己的划线用实线展示;

  6. 小流量,用户量由小至大,过程中可以对外显策略微调;


1.3 竞品


可以看到,微信公众号划线过程会弹出一个灰色面板,面板上有划线(这次需求改为了点赞)按钮:


image.png
image.png


2 关键逻辑


这个需求乍看可能会觉得没那么复杂,但细细分析后会发现有较长的交互流程和逻辑链:


image.png


其中有几个会影响整体逻辑的关键点需要关注:



  1. 渲染划线的方式:插入 dom 标签还是绝对定位或其他方式;

  2. 监听划线选取的交互事件选择 selectionchange 还是 touchend;

  3. 整个交互过程分为哪些部分;

  4. 怎么判断新划线和其他划线的位置关系,怎么合并或删除;

  5. 数据结构怎么设计;

  6. 怎么将划线序列化;

  7. 怎么将数据反序列化成划线;


3 详细设计


3.1获取划线


window 上提供了 Selection 对象,它代表页面中的文本选区,可能横跨多个元素。文本选区由用户拖拽鼠标经过文字而产生。调用 Selection.toString() 方法会返回被选中区域中的纯文本。


var selObj = window.getSelection();
var range = selObj.getRangeAt(0);

Selection 对象所对应的是用户所选择的 ranges(区域)


var selObj = window.getSelection();
var range = selObj.getRangeAt(0);

3.2 划线渲染方式


渲染划线有两种方式:


1 在划线range对象的首尾dom的位置,插入线段的dom标签;
优点:划线的点击不需要计算点击位置,直接在插入dom上绑定事件即可;
缺点:对原页面结构有入侵,改变了dom结构,可能引发其他问题;


2 绝对定位,相对于整篇文章;
优点:完全增量,对原页面没有入侵;
缺点:需要计算点击位置;


我选择的第二种,原因是为了不影响原有页面逻辑,这样项目风险也是最小的。那么具体怎么实现呢?


range对象提供了一个 getClientRects 方法,表示 range 在屏幕上所占的区域。这个列表相当于汇集了范围中所有元素调用 Element.getClientRects() 方法所得的结果。用拿到的位置信息进行绝对定位即可。


rectList = range.getClientRects()

我们把用户所有划线range对象和其产生的位置信息都存入到一个list中。


pageRangeList.push({
range,
rectInfo
})

3.3 交互过程


我们分析下整个交互过程:
有两个主要的交互事件,一是点击划线,二是滑动选区。


3.3.1 点击事件


处理点击事件,我们拿到点击事件的位置,和存放的 pageRangeList 进行位置比较,得出用户点击的是哪个range对象。


// 点击事件
const {pageX, pageY} = event;
const lineHeight = 23;

const {range} = rectInfo.some(rect => {
const {left, right, realY} = rect;
return pageX < left && pageX > left && pageY > realY
})

this.selection.removeAllRanges();
this.selection.addRange();

3.3.2 选区事件


选区事件我选择的是 selectionchange,需要加防抖和节流处理。


如果你选的是 touchend 安卓系统会点问题。


3.3.3 比较位置关系


如第2点核心逻辑中所说,在滑词过程中,需要比较位置关系,我们直接使用Range.compareBoundaryPoints方法即可。返回值 0 、-1 、1 分别代表不同的位置关系。


const compare = range.compareBoundaryPoints(Range.START_TO_END, sourceRange);

3.4 序列化与反序列化


序列化是整个需求的重点,序列化是指将交互产生的划线转化成某种数据结构能存储在服务器上,反序列化是指如何将server下发的序列化数据转化成非序列化的划线。


两者是两个相反的过程,当我们确定了序列化方案,其实也就知道了反序列化了。


3.4.1 序列化


方案一,识别段落


刚开始我观察文章都会拆分段落,如按P标签或某一个class类名来划分段落,于是计划用段落信息,告诉 server 划线在第几段的第几个字。


interface data {
startParagraph: 1,
startIndex: 22,
endParagraph: 2,
endIndex: 15
}

但后来发现有一些抓取的文章根本内容很混乱,且没有特定的段落,强行识别复杂度极高。(如下图)所以此方案不可行。


image.png


方案二,全文第几个字


前面的方案不可能的原因是,识别段落信息复杂度不可控,那么我们可以绕过段落信息,去识别全文第几个字。


interface data {
startCharacters: 122,
endCharacters: 166
}

具体方式是用Range,圈选文章开头到当前dom,形成一个新Range,再调用range.toString查看字数即可。


const range = new Range();
range.setStart(pageContainer, 0);
range.setEnd(curEndContainer, endOffset);
const str = range.toString();

3.4.2 反序列化


这里注意,由于 Javascript 在大多宿主环境下没有递归的尾调用优化,所以我采用了手动创建栈来进行 dfs:


    dfs({
node = this.content,
}) {
const stack = [];
if (!node) {
return;
}

stack.push(node);

while (stack.length) {
const item = stack.pop();

const children = item.childNodes;
for (let i = children.length - 1; i >= 0; i--) {
stack.push( [i]);
}
}
}

作者:雨默默下了一整夜
来源:juejin.cn/post/7344993022075813938
收起阅读 »

⚡聊天框 - 微信加载历史数据的效果原来这样实现的

web
前言 我记得2021年的时候做过聊天功能,那时业务也只限微信小程序 那时候的心路历程是: 卧槽,让我写一个聊天功能这么高大上?? 嗯?这么简单,不就画画页面来个轮询吗,加个websocket也还行吧 然后,卧槽?这查看历史聊天记录什么鬼,页面闪一下不太好啊,...
继续阅读 »

前言


我记得2021年的时候做过聊天功能,那时业务也只限微信小程序


那时候的心路历程是:



卧槽,让我写一个聊天功能这么高大上??


嗯?这么简单,不就画画页面来个轮询吗,加个websocket也还行吧


然后,卧槽?这查看历史聊天记录什么鬼,页面闪一下不太好啊,真的能做到微信的那种效果吗



然后一堆调研加测试,总算在小程序中查看历史记录没那么鬼畜了,但是总是感觉不是最佳解决方案。



那时打出的子弹,一直等到现在击中了我



最近又回想到了这个痛点,于是网上想看看有没有大佬发解决方案,结果还真被我找到了。


image.png


正文开始


1,效果展示


上才艺~~~


222.gif


2,聊天页面


2.1,查看历史聊天记录的坑


常规写法加载历史记录拼接到聊天主体的顶部后,滚动条会回到顶部、不在原聊天页面


直接上图


111.gif


而我们以往的解决方案也只是各种利用缓存scroll的滚动定位把回到顶部的滚动条重新拉回加载历史记录前的位置,好让我们可以继续在原聊天页面。


但即使我们做了很多优化,也会有安卓和苹果部分机型适配问题,还是不自然,可能会出现页面闪动


其实吧,解决方案只有两行css代码~~~


2.2,解决方案:flex神功


想优雅顺滑的在聊天框里查看历史记录,这两行css代码就是flex的这个翻转属性


dispaly:flex;
flex-direction: column-reverse

灵感来源~~~


333.gif


小伙伴可以看到,在加载更多数据时



滚动条位置没变、加载数据后还是原聊天页面的位置



这不就是我们之前的痛点吗~~~


所以,我们只需要翻转位置,用这个就可以优雅流畅的实现微信的加载历史记录啦


flex-direction: column-reverse


官方的意思:指定Flex容器中子元素的排列方向为列(从上到下),并且将其顺序反转(从底部到顶部)


如果感觉还是抽象,不好理解的话,那就直接上图,不加column-reverse的样子


image.png


加了column-reverse的样子


image.png


至此,我们用column-reverse再搭配data数据的位置处理就完美解决加载历史记录的历史性问题啦


代码放最后啦~~~


2.3,其他问题


2.3.1,数据过少时第一屏展示


因为用了翻转,数据少的时候会出现上图的问题


只需要.mainArea加上height:100%


然后额外写个适配盒子就行


flex-grow: 1; 
flex-shrink: 1;

image.png


2.3.2,用了scroll-view导致的问题


这一part是因为我用了uniappscroll-view组件导致的坑以及解决方案,小伙伴们没用这个组件的可忽略~~~


如下图,.mainArea使用了height:100%后,继承了父级高度后scroll-view滚动条消失了。


image.png


.mainArea去掉height:100%后scroll-view滚动条出现,但是第一屏数据过多时不会滚动到底部展示最新信息


image.png


解决方案:第一屏手动进行滚动条置顶


scrollBottom() {
if (this.firstLoad) return;
// 第一屏后不触发
this.$nextTick(() => {
const query = uni.createSelectorQuery().in(this);
query
.select("#mainArea")
.boundingClientRect((data) => {
console.log(data);
if (data.height > +this.chatHeight) {
this.scrollTop = data.height; // 填写个较大的数
this.firstLoad = true;
}
})
.exec();
});
},

3,服务端


使用koa自己搭一个websocket服务端


3.1 服务端项目目录


image.png


package.json


{
"name": "websocketapi",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"koa": "^2.14.2",
"koa-router": "^12.0.1",
"koa-websocket": "^7.0.0"
}
}


koa-tcp.js


const koa = require('koa')
const Router = require('koa-router')
const ws = require('koa-websocket')

const app = ws(new koa())
const router = new Router()

/**
* 服务端给客户端的聊天信息格式
* {
id: lastid,
showTime: 是否展示时间,
time: nowDate,
type: type,
userinfo: {
uid: this.myuid,
username: this.username,
face: this.avatar,
},
content: {
url:'',
text:'',
w:'',
h:''
},
}
消息数据队列的队头为最新消息,以次往下为老消息
客户端展示需要reverse(): 客户端聊天窗口最下面需要为最新消息,所以队列尾部为最新消息,以此往上为老消息
*/



router.all('/websocket/:id', async (ctx) => {
// const query = ctx.query
console.log(JSON.stringify(ctx.params))
ctx.websocket.send('我是小服,告诉你连接成功啦')
ctx.websocket.on('message', (res) => {
console.log(`服务端收到消息, ${res}`)
let data = JSON.parse(res)
if (data.type === 'chat') {
ctx.websocket.send(`我也会说${data.text}`)
}
})
ctx.websocket.on('close', () => {
console.log('服务端关闭')
})
})

// 将路由中间件添加到Koa应用中
app.ws.use(router.routes()).use(router.allowedMethods())

app.listen(9001, () => {
console.log('socket is connect')
})



切到server目录yarn


然后执行nodemon koa-tcp.js


没有nodemon的小伙伴要装一下


image.png


代码区


完整项目Github传送门


聊天页面的核心代码如下(包含data数据的位置处理和与服务端联动)



完结


这篇文章我尽力把我的笔记和想法放到这了,希望对小伙伴有帮助。


到这里,想给小伙伴分享两句话



现在搞不清楚的事,不妨可以先慢下来,不要让自己钻到牛角尖了


一些你现在觉得解决不了的事,可能需要换个角度



欢迎转载,但请注明来源。


最后,希望小伙伴们给我个免费的点赞,祝大家心想事成,平安喜乐。


image.png


作者:尘落笔记
来源:juejin.cn/post/7337114587123335180
收起阅读 »

基于装饰器——我劝你不要在业务代码上装逼!!!

web
基于装饰器——我劝你不要在业务代码上装逼!!! 装饰器模式的定义 在传统的面向对象语言中,给对象添加功能常使用继承的方式,但继承的方式并不灵活,会带来一些许多问题,如:超类和子类存在强耦合性,也就是说当改变超类时,子类也需要改变。 而装饰器模式的出现改变的这...
继续阅读 »

基于装饰器——我劝你不要在业务代码上装逼!!!


装饰器模式的定义



  • 在传统的面向对象语言中,给对象添加功能常使用继承的方式,但继承的方式并不灵活,会带来一些许多问题,如:超类和子类存在强耦合性,也就是说当改变超类时,子类也需要改变。

  • 而装饰器模式的出现改变的这种方式,装饰器模式可在不改变现有对象解构的基础上,动态地为对象添加功能


传统的 JavaScript 装饰器


var plane = {
fire: function () {
console.log("普通子弹");
},
};

var missleDecorator = function () {
console.log("发射导弹");
};

var atomDecorator = function () {
console.log("发射原子弹");
};

var fire1 = plane.fire;
plane.fire = function () {
fire1();
missleDecorator();
};

var fire2 = plane.fire;
plane.fire = function () {
fire2();
atomDecorator();
};

plane.fire();
/**
普通子弹
发射导弹
发射原子弹
*/



装饰函数



  • 在 JavaScript 中,几乎一切都是对象,其中函数也被成为对象,在平时的开发中,我们都在和函数打交道。在给对象扩展属性和方法时,很难在不改动原功能函数的情况下,给函数添加一些额外的功能,最直接的粗暴方式就是直接改写函数,但这是最差的方式,这违反了开放——封闭原则。

  • 如下:


function a(){
console.log(1);
}

// 改写:
function a(){
console.log(1);

// 新功能
console.log(2);
}



  • 很多时候,我们都不想去触碰之前的一些代码,但需要添加功能,所以如果需要在不改变原功能函数的情况下,给函数添加功能。可使用以下方式:

  • 要想完美的给函数添加功能,可使用 AOP 来装饰函数

    • AOP:一种编程规范,通过将关注点从主业务逻辑中剥离出来并单独处理,以此来提高代码的可读性和重用性。



  • 如下:


Function.prototype.before = function (beforeFn) {
var _self = this;
return function () {
beforeFn.apply(this, arguments);
return _self.apply(this, arguments);
};
};

Function.prototype.after = function (afterFn) {
var _self = this;
return function () {
var ret = _self.apply(this, arguments);
afterFn.apply(this, arguments);
return ret;
}
}

// before 和 after 函数都接收一个函数作为参数,这个函数也就是新添加的函数(里面也就是要添加的新功能逻辑)。
// 而before 和 after 函数区别在于在是原函数之前执行还是之后执行。



  • AOP 函数的使用


Function.prototype.before = function (beforeFn) {
var _self = this;
return function () {
beforeFn.apply(this, arguments);
return _self.apply(this, arguments);
};
};

Function.prototype.after = function (afterFn) {
var _self = this;
return function () {
var ret = _self.apply(this, arguments);
afterFn.apply(this, arguments);
return ret;
}
}

var o1 = function(){
console.log('1');
}
var o2 = function(){
console.log('2');
}
var o3 = function(){
console.log('3');
}

var desctor = o1.after(o2);
desctor = desctor.after(o3);
desctor(); // 1 2 3
/**
var desctor = o1.after(o2);
desctor = desctor.after(o3);
desctor();
1
2
3

var desctor = o1.before(o2);
desctor = desctor.before(o3);
desctor();
3
2
1

var desctor = o1.after(o2);
desctor = desctor.before(o3);
desctor();
3
1
2


var desctor = o1.before(o2);
desctor = desctor.after(o3);
desctor();
2
1
3
*/



AOP的应用


1.数据上报



  • 在程序开发中,当业务代码开发完后,在结尾时需要加很多的日志上报的代码,普遍我们会去改已经之前封装好的功能函数。其实这并不是一个好的方式,那如何在不直接修改之前函数的基础上添加日志上报功能呢?

  • 如下:


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>AOP日志上报</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="https://unpkg.com/vue@3.2.20/dist/vue.global.js"></script>
</head>
<body>
<div id="app">
<button class="btn" @click="handler">Button</button>
<p id="tt">{{message}}</p>
</div>
</body>
</html>
<script type="text/javascript">
// log report
const { reactive, ref, createApp } = Vue;
const app = createApp({
setup() {
const message = ref("未点击");
const count = ref(0);

Function.prototype.before = function (beforeFn) {
var _self = this;
return function () {
beforeFn.apply(this, arguments);
return _self.apply(this, arguments);
};
};

Function.prototype.after = function (afterFn) {
var _self = this;
return function () {
var ret = _self.apply(this, arguments);
afterFn.apply(this, arguments);
return ret;
};
};

function handler() {
message.value = `已点击${++count.value}`;
}

handler = handler.after(log);

function log() {
message.value = message.value + "-----> log reported";
console.log("log report");
}

return {
message,
handler,
};
},
});
app.mount("#app");
</script>


2.动态参数



  • 在日常开发中,我们需要向后台接口发送请求来获取信息,例如传参如下。业务在后续时需要添加新参数,每个接口需要把 token 值也一并传过去, 普遍我们会去改封装的请求方法,把 token 参数添加进去。但我们直接修改封装好的请求方法不是好的行为,那我们可使用上面说过的 AOP 方式来改进。


{
name: 'xxxx',
password: 'xxxx',
}



  • 如下:


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>AOP动态参数</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="https://unpkg.com/vue@3.2.20/dist/vue.global.js"></script>
</head>
<body>
<div id="app">{{message}}</div>
</body>
</html>

<script type="text/javascript">
const { reactive, ref, createApp } = Vue;
const app = createApp({
setup() {
const message = ref("empty params");
Function.prototype.before = function (beforeFn) {
var _self = this;
return function () {
beforeFn.apply(this, arguments);
return _self.apply(this, arguments);
};
};

Function.prototype.after = function (afterFn) {
var _self = this;
return function () {
var ret = _self.apply(this, arguments);
afterFn.apply(this, arguments);
return ret;
};
};

function ajax(type, url, params){
message.value = `${type} ----> ${url} -----> ${JSON.stringify(params)}`;
}

function getToken(){
// do something
return 'token';
}

ajax = ajax.before(function(type, url, params){
params.token = getToken();
})

ajax('get', 'https://www.baidu.com/userinfo', {name: 'se', password: 'xsdsd'});
return {
message,
};
},
});
app.mount("#app");
</script>


3.表单校验



  • 在日常开发中,我们经常要去做校验表单数据,通常的方式是在功能函数中进行判断处理或将判断逻辑提取为一个函数的方式。但这种方式其实是与功能性函数相混合,且校验逻辑与功能性函数有耦合关系。那我们可使用 AOP 方式来改进。

  • 如下:


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>AOP表单验证</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="https://unpkg.com/vue@3.2.20/dist/vue.global.js"></script>
</head>
<body>
<div id="app">
<label>
姓名:
<input
type="text"
v-model="data.name"
placeholder="请输入姓名"
/>

</label>
<label>
密码:
<input
type="text"
v-model="data.pass"
placeholder="请输入密码"
/>

</label>
<p v-if="data.name || data.pass">{{data.name + '/' + data.pass}} ----after-----> {{data.message}}</p>
<hr>
<button @click="submitBtn">submit</button>
</div>
</body>
</html>

<script type="text/javascript">
const { reactive, ref, createApp, watchEffect } = Vue;
const app = createApp({
setup() {
const data = reactive({
name: "",
pass: "",
message: "",
});

Function.prototype.before = function (beforeFn) {
var _self = this;
return function () {
if (beforeFn.apply(this, arguments) === false) return;
return _self.apply(this, arguments);
};
};

function valid() {
if (!data.name || !data.pass) {
alert("用户名或密码不能为空");
return false;
}
}

function formSubmit() {
console.log("data ------>", data);
data.message = `${data.name} ------- ${data.pass}`;
}

formSubmit = formSubmit.before(valid);

function submitBtn() {
formSubmit();
}
return {
data,
submitBtn,
};
},
});
app.mount("#app");
</script>


装饰器模式的优缺点



  • 优点:

    1. 扩展性强:装饰器模式允许在不修改现有代码的情况下,动态地添加新功能或修改现有功能。通过使用装饰器,可以在运行时按需组合和堆叠装饰器对象,实现各种组合方式,从而实现更多的功能扩展。

    2. 遵循开闭原则:装饰器模式通过添加装饰器类来扩展功能,而不是修改现有的代码。这样可以保持原有代码的稳定性,符合开闭原则,即对扩展开放,对修改关闭。

    3. 分离关注点:装饰器模式将功能的扩展和核心功能分离开来,每个装饰器类只关注单一的额外功能。这样可以使代码更加清晰、可读性更高,并且容易维护和测试。



  • 缺点:

    1. 增加复杂性:使用装饰器模式会增加额外的类和对象,引入了更多的复杂性和层次结构。这可能使代码变得更加复杂,理解和调试起来可能更加困难。

    2. 潜在的性能影响:由于装饰器模式涉及多个对象的组合和堆叠,可能会引入额外的运行时开销,对性能产生一定的影响。尤其是当装饰器链较长时,可能会导致性能下降。




装饰器模式的适用场景



  1. 动态地扩展对象功能:当需要在运行时动态地为对象添加额外的功能或责任时,装饰器模式是一个很好的选择

  2. 遵循开闭原则:如果你希望在不修改现有代码的情况下扩展功能,而且要保持代码的稳定性,装饰器模式是一个合适的解决方案。

  3. 分离关注点:当你希望将不同的功能分离开来,使每个功能都有自己独立的装饰器类时,装饰器模式是有用的。每个装饰器只关注单一的额外功能,这样可以使代码更加清晰、可读性更高,并且容易维护和测试。

  4. 多层次的功能组合:如果你需要实现多个功能的组合,而且每个功能都可以灵活选择是否添加,装饰器模式可以很好地满足这个需求。通过堆叠多个装饰器对象,可以按照特定的顺序组合功能,实现各种组合方式。

  5. 继承关系的替代方案:当你面临类似于创建大量子类的情况时,装饰器模式可以作为继承关系的替代方案。通过使用装饰器模式,可以避免创建过多的子类,而是通过组合不同的装饰器来实现不同的功能组合。


Tip: 文章部分内容参考于曾探大佬的《JavaScript 设计模式与开发实践》。文章仅做个人学习总结和知识汇总

作者:南囝coding
来源:juejin.cn/post/7272869799960559679
收起阅读 »

还在封装 xxxForm,xxxTable 残害你的同事?试试这个工具

web
之前写过一篇文章 我理想中的低代码开发工具的形态,已经吐槽了各种封装 xxxForm,xxxTable 的行为,这里就不啰嗦了。今天再来看看我的工具达到了什么程度。 多图预警。。。 以管理后台一个列表页为例 选择对应的模板 截图查询区域,使用 OCR 初始...
继续阅读 »

之前写过一篇文章 我理想中的低代码开发工具的形态,已经吐槽了各种封装 xxxForm,xxxTable 的行为,这里就不啰嗦了。今天再来看看我的工具达到了什么程度。


多图预警。。。


以管理后台一个列表页为例



选择对应的模板



截图查询区域,使用 OCR 初始化查询表单的配置



截图表头,使用 OCR 初始化 table 的配置



使用 ChatGPT 翻译中文字段



生成代码



效果


目前我们没有写一行代码,就已经达到了如下的效果



下面是一部分生成的代码


import { reactive, ref } from 'vue'

import { IFetchTableListResult } from './api'

interface ITableListItem {
/**
* 决算单状态
*/

settlementStatus: string
/**
* 主合同编号
*/

mainContractNumber: string
/**
* 客户名称
*/

customerName: string
/**
* 客户手机号
*/

customerPhone: string
/**
* 房屋地址
*/

houseAddress: string
/**
* 工程管理
*/

projectManagement: string
/**
* 接口返回的数据,新增字段不需要改 ITableListItem 直接从这里取
*/

apiResult: IFetchTableListResult['result']['records'][0]
}

interface IFormData {
/**
* 决算单状态
*/

settlementStatus?: string
/**
* 主合同编号
*/

mainContractNumber?: string
/**
* 客户名称
*/

customerName?: string
/**
* 客户手机号
*/

customerPhone?: string
/**
* 工程管理
*/

projectManagement?: string
}

interface IOptionItem {
label: string
value: string
}

interface IOptions {
settlementStatus: IOptionItem[]
}

const defaultOptions: IOptions = {
settlementStatus: [],
}

export const defaultFormData: IFormData = {
settlementStatus: undefined,
mainContractNumber: undefined,
customerName: undefined,
customerPhone: undefined,
projectManagement: undefined,
}

export const useModel = () => {
const filterForm = reactive<IFormData>({ ...defaultFormData })

const options = reactive<IOptions>({ ...defaultOptions })

const tableList = ref<(ITableListItem & { _?: unknown })[]>([])

const pagination = reactive<{
page: number
pageSize: number
total: number
}>({
page: 1,
pageSize: 10,
total: 0,
})

const loading = reactive<{ list: boolean }>({
list: false,
})

return {
filterForm,
options,
tableList,
pagination,
loading,
}
}

export type Model = ReturnType<typeof useModel>


这就是用模板生成的好处,有规范,随时可以改,而封装 xxxForm,xxxTable 就是一个黑盒。


原理


下面大致说一下原理



首先是写好一个个模版,vscode 插件读取指定目录下模版显示到界面上



每个模版下可能包含如下内容:



选择模版后,进入动态表单配置界面



动态表单是读取 config/schema.json 里的内容进行动态渲染的,目前支持 amis、form-render、formily



配置表单是为了生成 JSON 数据,然后根据 JSON 数据生成代码。所以最终还是无法避免的使用私有的 DSL ,但是生成后的代码是没有私有 DSL 的痕迹的。生成代码本质是 JSON + EJS 模版引擎编译 src 目录下的 ejs 文件。


为了加快表单的配置,可以自定义脚本进行操作



这部分内容是读取 config/preview.json 内容进行显示的



选择对应的脚本方法后,插件会动态加载 script/index.js 脚本,并执行里面对应的方法



以 initColumnsFromImage 方法为例,这个方法是读取剪贴板里的图片,然后使用百度 OCR 解析出文本,再使用文本初始化表单


initColumnsFromImage: async (lowcodeContext) => {
context.lowcodeContext = lowcodeContext;
const res = await main.handleInitColumnsFromImage();
return res;
},

export async function handleInitColumnsFromImage() {
const { lowcodeContext } = context;
if (!lowcodeContext?.clipboardImage) {
window.showInformationMessage('剪贴板里没有截图');
return lowcodeContext?.model;
}
const ocrRes = await generalBasic({ image: lowcodeContext!.clipboardImage! });
env.clipboard.writeText(ocrRes.words_result.map((s) => s.words).join('\r\n'));
window.showInformationMessage('内容已经复制到剪贴板');
const columns = ocrRes.words_result.map((s) => ({
slot: false,
title: s.words,
dataIndex: s.words,
key: s.words,
}));
return { ...lowcodeContext.model, columns };
}

反正就是可以根据自己的需求定义各种各样的脚本。比如使用 ChatGPT 翻译 JSON 里的指定字段,可以看我的上一篇文章 TypeChat、JSONSchemaChat实战 - 让ChatGPT更听你的话


再比如要实现把中文翻译成英文,然后英文使用驼峰语法,这样就可以将中文转成英文代码变量,下面是实现的效果



选择对应的命令菜单后 vscode 插件会加载对应模版里的脚本,然后执行里面的 onSelect 方法。



main.ts 代码如下


import { env, window, Range } from 'vscode';
import { context } from './context';

export async function bootstrap() {
const clipboardText = await env.clipboard.readText();
const { selection, document } = window.activeTextEditor!;
const selectText = document.getText(selection).trim();
let content = await context.lowcodeContext!.createChatCompletion({
messages: [
{
role: 'system',
content: `你是一个翻译家,你的目标是把中文翻译成英文单词,请翻译时使用驼峰格式,小写字母开头,不要带翻译腔,而是要翻译得自然、流畅和地道,使用优美和高雅的表达方式。请翻译下面用户输入的内容`,
},
{
role: 'user',
content: selectText || clipboardText,
},
],
});
content = content.charAt(0).toLowerCase() + content.slice(1);
window.activeTextEditor?.edit((editBuilder) => {
if (window.activeTextEditor?.selection.isEmpty) {
editBuilder.insert(window.activeTextEditor.selection.start, content);
} else {
editBuilder.replace(
new Range(
window.activeTextEditor!.selection.start,
window.activeTextEditor!.selection.end,
),
content,
);
}
});
}


使用了 ChatGPT。


再来看看,之前生成管理后台 CURD 页面的时候,连 mock 也一起生成了,主要逻辑放在了 complete 方法里,这是插件的一个生命周期函数。



因为 mock 服务在另一个项目里,所以需要跨目录去生成代码,这里我在 mock 服务里加了个接口返回 mock 项目所在的目录


.get(`/mockProjectPath`, async (ctx, next) => {
ctx.body = {
status: 200,
msg: '',
result: __dirname,
};
})

生成代码的时候请求这个接口,就知道往哪个目录生成代码了


const mockProjectPathRes = await axios
.get('http://localhost:3001/mockProjectPath', { timeout: 1000 })
.catch(() => {
window.showInformationMessage(
'获取 mock 项目路径失败,跳过更新 mock 服务',
);
});
if (mockProjectPathRes?.data.result) {
const projectName = workspace.rootPath
?.replace(/\\/g, '/')
.split('/')
.pop();
const mockRouteFile = path.join(
mockProjectPathRes.data.result,
`${projectName}.js`,
);
let mockFileContent = `
import KoaRouter from 'koa-router';
import proxy from '../middleware/Proxy';
import { delay } from '../lib/util';

const Mock = require('mockjs');

const { Random } = Mock;

const router = new KoaRouter();
router{{mockScript}}
module.exports = router;
`
;

if (fs.existsSync(mockRouteFile)) {
mockFileContent = fs.readFileSync(mockRouteFile).toString().toString();
const index = mockFileContent.lastIndexOf(')') + 1;
mockFileContent = `${mockFileContent.substring(
0,
index,
)}
{{mockScript}}\n${mockFileContent.substring(index)}`
;
}
mockFileContent = mockFileContent.replace(/{{mockScript}}/g, mockScript);
fs.writeFileSync(mockRouteFile, mockFileContent);
try {
execa.sync('node', [
path.join(
mockProjectPathRes.data.result
.replace(/\\/g, '/')
.replace('/src/routes', ''),
'/node_modules/eslint/bin/eslint.js',
),
mockRouteFile,
'--resolve-plugins-relative-to',
mockProjectPathRes.data.result
.replace(/\\/g, '/')
.replace('/src/routes', ''),
'--fix',
]);
} catch (err) {
console.log(err);
}

mock 项目也可以通过 vscode 插件快速创建和使用



上面展示的模版都放在了 github.com/lowcode-sca… 仓库里,照着 README 步骤做就可以使用了。


作者:若邪
来源:juejin.cn/post/7315242945454735414
收起阅读 »

精美绝伦:小程序日历组件推荐

web
前言众所周知,小程序的渲染性能一直被广大开发者诟病,2023年中旬,小程序团队正式发布了 skyline 渲染引擎,Skyline,旨在替代 WebView 作为小程序的渲染层,以提供更优秀的渲染性能和诸多增强特性,让小程序能达到原生的体验...
继续阅读 »

前言

众所周知,小程序的渲染性能一直被广大开发者诟病,2023年中旬,小程序团队正式发布了 skyline 渲染引擎,Skyline,旨在替代 WebView 作为小程序的渲染层,以提供更优秀的渲染性能和诸多增强特性,让小程序能达到原生的体验。

非常好,那么就是说我们可以在小程序上体验类原生的特性啦!这下谁敢再说小程序是屎?

尝试

在用了一段时间,主要尝试了canvas、手势组件动画等功能,惊奇的发现,小程序做的这个 skyline 渲染引擎,是一陀超大的屎。

噢!腾讯,你小子好啊,研究了这么长时间,跑我这排宿便了是吧?

image.png

image.png

自己写的样式和iconfont样式给我报了很多坨警告,能用吗?能用,但是我是屎我需要恶心你,我必须得给你点警告。

除了控制台脏了之外,还有各种各样数不清的 bug。比如,地图的bindregionchange失效,而你去论坛发,他们只会说:"未复现"、"写片段"、"你试试",发文时实测依然没有修复😅。

爱莲说

铺垫了这么多,实属无奈,我也不想说这么多,只是这口屎憋在嘴里,臭的难受。我本以为出淤泥而不染已经很难得了,没想到在这屎坑里还有大佬栽培了一朵精美绝伦的白莲花,它就是 lspriv/wx-calendar ,github链接:github.com/lspriv/wx-c…

看到这么牛逼的组件,只有区区一百来个 star。

牛逼不牛逼,直接看效果:

QQ2024422-123019.webp

它还同时支持 skyline 和 webview 下渲染。

image.png

每个场景都是丝滑过渡的,元素到元素的联合动画。看的出来,这个日历是有很重的 MIUI 风格的,如果不是右上角的小程序胶囊,我甚至以为是某手机的自带日历。

QQ2024422-14944.webp

依赖 skyline 的 worklet 动画,组件做到了跟手、丝滑,且符合直觉的动画。

lspriv/wx-calendar 使用

lspriv/wx-calendar 需要使用npm下载并构建,然后引入组件使用。

npm i @lspriv/wx-calendar

然后需要使用微信开发者工具构建 npm

{
   "usingComponents": {
       "calendar": "@lspriv/wx-calendar"
  }
}
id="calendar" bindload="handleLoad" />

生态

作者十分聪明,给 lspriv/wx-calendar 预留了插件接口,开发者可以根据自身需求,写扩展功能。

源码中 src>plugins>lunar.ts 是一个内置插件,实现了农历、节气、闰年等功能。

目前为止,还没有看到有第二个人为作者贡献插件。

展望

目前还有很多基础功能还没有开发,比如

  1. 日期标注,日期标注有是有,但是作者将几种标记方式写死了,只能用内置的日程、角标和节假日标记,开发中肯定是期望可以传入组件或自定义样式的。
  2. 选择区间,一个很常见的场景,需要选择日期区间,跨月、跨年选择,这些在不了解源码的情况下,去手写插件也是比较困难的。
  3. 自定义样式,作者将自己的样式隔离了,开发者只能通过传入指定的style字符串修改样式,这个用起来不是很方便。

结语

总的来说,这是一款不可多得的组件,即使在PC端,也是不常见的。在小程序的层层阻挠下能开发出如此的组件,实属不易。

ce2898a24a9846c59a058e07eaeea24c_tplv-k3u1fbpfcp-jj-mark_3024_0_0_0_q75.gif


作者:德莱厄斯
来源:juejin.cn/post/7360237771637489679

收起阅读 »

领导让我加水印

web
tips: 文末有完整示例代码。 领导:『小S啊,我们有个新需求🥸,需要在预览的资源上添加水印,让服务端来加水印成本太高了,在前端渲染的时候把水印加上可以吗🤨?』 小S:『加水印啊,简简单单🤏。我们项目使用的是 Vue3,使用自定义指令一下就可以加好了。领导...
继续阅读 »

tips: 文末有完整示例代码。



领导:『小S啊,我们有个新需求🥸,需要在预览的资源上添加水印,让服务端来加水印成本太高了,在前端渲染的时候把水印加上可以吗🤨?』


小S:『加水印啊,简简单单🤏。我们项目使用的是 Vue3,使用自定义指令一下就可以加好了。领导你看我操作!』


小S说着,就把生产力工具打开了。手速熟练🤠的启动了项目。


小S:『领导你看😈,我先在项目自定义指令的文件夹下新建一个自定义水印指令文件 - watermark.ts。在需要添加水印的目标 Dom 挂载时,创建一个 canvas 节点,canvas 的宽高自然要跟 Dom 的大小一样啦,层级也必须是最高的。然后我再给 canvas 里画上水印内容,最后再给 canvas 挂载到目标节点。当然啦,目标节点销毁时也要把 canvas 销毁掉。』


小S一边讲,一边就在生产力工具中敲🫳出了代码。


import type { Directive, App } from 'vue';
import { nextTick } from 'vue';

const watermarkDirective: Directive = {
// 在绑定元素的父组件
// 及他自己的所有子节点都挂载完成后调用
async mounted(el, binding) {
await createWatermark(el, binding.value.text);
},
// 绑定元素的父组件卸载后调用
unmounted(el) {
removeWatermark(el);
},
};

/** 创建水印 */
async function createWatermark(el, text: string) {
const canvasEl = document.createElement('canvas');
const newCanvas = !el.querySelector('canvas');

canvasEl.id = 'watermark-canvas';
canvasEl.style.position = 'absolute';
canvasEl.style.top = '0';
canvasEl.style.left = '0';
canvasEl.style.zIndex = '99';
canvasEl.style.pointerEvents = 'none';
el.appendChild(canvasEl);
canvasEl.width = window.screen.width;
canvasEl.height = window.screen.height;
const ctx = canvasEl.getContext('2d');
ctx.rotate((-20 * Math.PI) / 180); //旋转角度
ctx.font = '24px serif';
ctx.fillStyle = 'rgba(180, 180, 180, 0.3)';
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
for (let i = -canvasEl.width / 100; i < canvasEl.width / 100; i++) {
for (let j = -canvasEl.height / 200; j < canvasEl.height / 200; j++) {
ctx.fillText(text, i * 300, j * 300);
}
}
}

async function removeWatermark(el) {
await nextTick();
const canvasEl = el.querySelector('#watermark-canvas');
if (canvasEl) {
canvasEl.remove();
}
}

export default watermarkDirective;


小S得意😏的抖着腿,侧身向领导讲到:『这样就可以生成水印啦! 撒花🥳🥳🥳』。


领导🫲🫱:『你这样是可以实现了,但是也仅仅可以防一下小白,稍微懂点前端知识的人,都可以 F12 把控制台打开,选中水印节点,给它哐哐哐删掉。』


小S听了,一拍脑门:『是哦,我怎么没想到呢!嗯……』小S陷入了沉思,如何防止被删掉呢?小S脑子转了3圈后:『领导,我知道怎么做了!DOM3 Event 规范中有一个 MutationObserver,这个接口可以监视 DOM 进行监视,只要我的水印被删掉了,我就赶紧再生成一个水印!』


小S立刻转身,一边思索🤔着逻辑,一边在生产力工具中继续完善:


小S心里想到:『在目标节点挂载,首次添加 canvas 时,我给目标节点添加 MutationObserver 监听,并把实例化的监视器放在目标节点的自定义属性上,监听它的子节点,如果监听到子节点水印被删除,我就再新建一个水印 canvas,插入到目标节点中,对了,还要考虑到我主动删除水印的操作。水印节点也要加监视,不然手动改一下水印的CSS样式,就可以把水印给隐藏掉了。emmm……最后在 目标节点卸载时把监听移除掉。』


小S搞好了,转身给领导讲道:『领导,搞定了!使用的时候只需要引入自定义指令,在需要加水印的节点添加参数就可以啦』


<template>
<div v-watermark="watermarkOption">
<img src="xxxx">
</div>
</template>

<script setup lang="ts">
// @ts-ignore
import vWatermark from '/@/directives/watermark';

const watermarkOption = {
text: '小S水印'
}
</script>

领导看着小S加好水印,笑😼着说:『针不错,这就去给你涨工资!』


小S听了,连忙摇头🙀道:『领导,不用,不用,这都是前端切图仔的基本功!』




END




完整示例代码


import type { Directive, App } from 'vue';
import { nextTick } from 'vue';

const watermarkDirective: Directive = {
// 在绑定元素的父组件
// 及他自己的所有子节点都挂载完成后调用
async mounted(el, binding) {
await createWatermark(el, binding.value.text);
},
// 绑定元素的父组件卸载后调用
unmounted(el) {
removeWatermark(el);
},
};

async function createWatermark(el, text: string) {
const canvasEl = el.querySelector('canvas') || document.createElement('canvas');
const newCanvas = !el.querySelector('canvas');

if (!el.dataset.mutationObserverParent) {
const mutationObserver = new MutationObserver((records) =>
parentCheckWatermark(records, el, text),
);
mutationObserver.observe(el, {
childList: true,
});
el.dataset.mutationObserverParent = mutationObserver;
}
canvasEl.id = 'watermark-canvas';
canvasEl.style.position = 'absolute';
canvasEl.style.top = '0';
canvasEl.style.left = '0';
canvasEl.style.zIndex = '99';
canvasEl.style.pointerEvents = 'none';
newCanvas && el.appendChild(canvasEl);
canvasEl.width = window.screen.width * 3;
canvasEl.height = window.screen.height * 3;
const ctx = canvasEl.getContext('2d');
if (!ctx) return;
ctx.rotate((-20 * Math.PI) / 180); //旋转角度
ctx.font = '24px serif';
ctx.fillStyle = 'rgba(180, 180, 180, 0.3)';
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
for (let i = -canvasEl.width / 100; i < canvasEl.width / 100; i++) {
for (let j = -canvasEl.height / 200; j < canvasEl.height / 200; j++) {
ctx.fillText(text, i * 300, j * 300);
}
}

if (newCanvas) {
// 水印属性监听
const mutationObserver = new MutationObserver(() => canvasCheckWatermark(el, text));
mutationObserver.observe(canvasEl, {
attributes: true,
});
el.dataset.mutationObserverCanvas = mutationObserver;
}
}

/** 检查水印是否被删除 */
async function parentCheckWatermark(records, el, text) {
// 主动删除水印不处理
if (el.dataset.focusRemove) return;
const removedNodes = records[0].removedNodes;
let hasDelWatermark = false;
removedNodes.forEach((el) => {
if (el.id === 'watermark-canvas') {
hasDelWatermark = true;
}
});
// 水印被删除了
hasDelWatermark && createWatermark(el, text);
}

/** 检查水印属性是否变化了 */
async function canvasCheckWatermark(el, text) {
// 防止多次触发
if (el.dataset.canvasRending) return;
el.dataset.canvasRending = 'rending';

// 水印canvas属性变化了,重新创建
await createWatermark(el, text);
el.dataset.canvasRending = '';
}

async function removeWatermark(el) {
el.dataset.focusRemove = true;
el.dataset.mutationObserverParent?.disconnect?.();
await nextTick();
const canvasEl = el.querySelector('#watermark-canvas');
if (canvasEl) {
canvasEl.dataset.mutationObserverCanvas?.disconnect?.();
canvasEl.remove();
}
}
export default watermarkDirective;

作者:sofor
来源:juejin.cn/post/7360269869399392310
收起阅读 »

奇技淫巧:如何修改第三方npm包?

web
如何修改第三方npm包? 有这样一种场景:如果你在开发过程中,发现某个npm包有Bug,应该怎么办? 第一选择当然是向原作者提issue,或者Fork该仓库代码,修改以后,提交合并请求。 但这种情况有个严重的问题,就是耗时,如果遇到严格的作者,或者不活跃的作...
继续阅读 »

如何修改第三方npm包?



有这样一种场景:如果你在开发过程中,发现某个npm包有Bug,应该怎么办?


第一选择当然是向原作者提issue,或者Fork该仓库代码,修改以后,提交合并请求。


但这种情况有个严重的问题,就是耗时,如果遇到严格的作者,或者不活跃的作者,时间线可能会拉得很长。你的项目未必能等这么长时间。


还有一种可能是,你修改的代码并不具备普适性,只有你的业务场景能用到,合并被拒的概率会大大增加。


总而言之,如果能修改npm包的源包,再好不过,如果不行,则需要有个临时方案,或者替代方案。


这时,又有下面两种情况:



  1. 代码量少,可以直接修改npm包代码的,考虑补丁方案。

  2. 代码量多,或者npm包代码是压缩混淆过的,不具备修改条件。修改源码后,再修改包名,重新发布,在应用代码中更换引用。为叙文方便,我将这种方案命名为换日方案(偷天换日,李代桃僵)。


下面,详细介绍下这两种不同方案。


补丁方案


patch-package


patch-package是一个用于修复第三方依赖包的工具,使用方式非常简单。



它支持npm和yarn v1,如果是yarn v2+或者pnpm,则使用自带的patch方案(下文会介绍pnpm方案)。



安装:


$ npm i patch-package
$ yarn add patch-package postinstall-postinstall


如果只是前端使用,可以添加--dev或-D参数。如果是后端使用,为保障生产模式(会去除devDendencies依赖)也能正常使用,就不要加了。



在node_modules中找到你要修改的npm包,修改内容后,就可以运行patch-package创建patch文件了。


$ npx patch-package package-name   # 使用npm
$ yarn patch-package package-name # 使用yarn

运行后会在项目根目录下创建一个patches文件夹,并生成一个名为package-name+version.patch的文件。将该patch文件提交至版本控制中,即可在之后应用该补丁了。


以我修改的verdaccio为例,会生成一个verdaccio+4.4.0.patch的文件,内容大致如下:


diff --git a/node_modules/verdaccio/build/index.js b/node_modules/verdaccio/build/index.js
index 3a79eaa..d00974b 100644
--- a/node_modules/verdaccio/build/index.js
+++ b/node_modules/verdaccio/build/index.js
@@ -5,6 +5,8 @@ Object.defineProperty(exports, "__esModule", {
});
exports.default = void 0;

+console.log('---------------')
+
var _bootstrap = require("./lib/bootstrap");

完成上述操作后,最后在package.jsonscripts中加入"postinstall": "patch-package"


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

这样当其他同事拉下代码,运行npm install或是yarn install命令时,便会自动为依赖包打上我们的补丁了。



简单来说,这个方案的原理就是记录补丁的代码与位置,利用npm的hook(postinstall会在npm install后触发),在安装完依赖以后,触发相应的脚本,将补丁覆盖到node_modules对应的包里。


当然,补丁是对应具体版本的,需要锁定版本号。这样的缺点是如果要升级的话,还得重新来一遍,不过不是有Bug或性能问题,通常不必追求新的版本。



pnpm patch


pnpm的patch自称灵感来自yarn的类似命令。由于yarn v2可能走了邪路,我们就不介绍了。


首先,执行pnpm patch @。该命令会将指定的软件包提取到一个可以随意编辑的临时目录中。


完成修改后, 运行pnpm patch-commit ( 是之前提取的临时目录,这个临时目录会长到你根本记不住,不过不用担心,命令行里会有完备的提示) 以生成一个补丁文件,并提供patchedDependencies 字段注册到你的项目中。


比如,我想修改一个is-even的包:


pnpm patch is-even  
You can now edit the following folder: /private/var/folders/sq/0jfgh1js6cs8_31df82hx3jw0000gn/T/29ba74c7c7ffd7aa157831c6436d3738

Once you're done with your changes, run "pnpm patch-commit /private/var/folders/sq/0jfgh1js6cs8_31df82hx3jw0000gn/T/29ba74c7c7ffd7aa157831c6436d3738"

按照提示,打开这个文件夹,加一行代码:
image.png


执行上面控制台的提示:


pnpm patch-commit /private/var/folders/sq/0jfgh1js6cs8_31df82hx3jw0000gn/T/e103de90617a18eee7942d1df35a2c48
Packages: -1
-
Progress: resolved 5, reused 6, downloaded 0, added 1, done

这时你会发现package.json中多了一段内容:


"pnpm": {
"patchedDependencies": {
"is-even@1.0.0": "patches/is-even@1.0.0.patch"
}
}

根目录下,也多了个文件夹patches,打开以后,你就能找到添加的代码:
image.png


打开node_modules/is-even/index.js,可以看到已经多了我们添加的代码:
image.png


删除node_modules,重新pnpm i安装依赖,仍然与现在一样,这就代表成功了。


整个流程下来,我们看得出来相比于patch-package,要稍微复杂点儿,但也是可以接受的。



注意:patches目录是一定得提交到git的。



换日方案


上面说过,如果要修改的代码较多,或者不具备修改条件,这时就需要修改源码。
到GitHub上找到npm包的源码,Fork该项目,修改代码后,再修改包名,重新发布,比如你要修改的包是lodash,可以修改为awesome-lodash,在应用代码中更换引用。


本来这个方案没什么好说的,但有一种情况,如果你修改的是个底层包,也就是说并不是你的应用代码中直接引用的,而是你引用的npm包A所依赖的,甚至可能同时被包B依赖的,这时就比较尴尬了,你不可能再去修改A和B的源码,那就太不值当了。


pnpm提供了一种别名(Aliases)的能力。


假设你发布了一个名为awesome-lodash的新包,并使用lodash作为别名来安装它:


$ pnpm add lodash@npm:awesome-lodash

不需要更改代码,所有的lodash引用都被解析到了awesome-lodash
就这么简单,上面说的问题就解决了。


再说点儿题外话,有时你会想要在项目中使用一个包的两个不同版本,很简单:


$ pnpm add lodash1@npm:lodash@1
$ pnpm add lodash2@npm:lodash@2

现在,您可以通过 require('lodash1') 引入第一个版本的 lodash 并通过 require('lodash2') 引入第二个。


与pnpm的钩子结合使用功能会更加强大,比如你想将node_modules里所有的lodash引用也替换为awesome-lodash,你可以用下面的.pnpmfile.cjs 轻松实现:


function readPackage(pkg) {
if (pkg.dependencies && pkg.dependencies.lodash) {
pkg.dependencies.lodash = 'npm:awesome-lodash@^1.0.0'
}
return pkg
}

module.exports = {
hooks: {
readPackage
}
}

pnpm功能非常强大,后面我会再详细写篇文章介绍下。


总结


在开发过程中发现npm包的Bug,首先向原作者提交issue或Fork代码修改后提交合并请求。但遇到不活跃或拒绝修改的情况,项目等待时间会很长。这时可以使用补丁方案或换日方案进行解决。


补丁方案中,如果是npm或yarn v1,可以使用patch-package工具包处理;如果是yarn v2或pnpm,可以使用各自的patch命令。


换日方案,则是修改源码,发布新的npm包后,利用pnpm的别名功能,将所有依赖原npm包的地方,全部替换为新的包。


这种场景在日常开发中还是比较常见的,这里为大家提供一种思路。当然,如果真是个Bug,别忘了提issue或PR,为开源贡献自己的一份力量,在与作者的沟通交流中,相信你也能受益匪浅。


作者:纪轻昀
来源:juejin.cn/post/7356534347509497919
收起阅读 »

【前端缓存】localStorage是同步还是异步的?为什么?

web
🧑‍💻 写在开头 点赞 + 收藏 === 学会🤣🤣🤣 首先明确一点,localStorage是同步的 🥝 一、首先为什么会有这样的问题 localStorage 是 Web Storage API 的一部分,它提供了一种存储键值对的机制。loca...
继续阅读 »

🧑‍💻 写在开头


点赞 + 收藏 === 学会🤣🤣🤣



首先明确一点,localStorage是同步的



🥝 一、首先为什么会有这样的问题


localStorage 是 Web Storage API 的一部分,它提供了一种存储键值对的机制。localStorage 的数据是持久存储在用户的硬盘上的,而不是内存。这意味着即使用户关闭浏览器或电脑,localStorage 中的数据也不会丢失,除非主动清除浏览器缓存或者使用代码删除。


当你通过 JavaScript 访问 localStorage 时,浏览器会从硬盘中读取数据或向硬盘写入数据。然而,在读写操作期间,数据可能会被暂时存放在内存中,以提高处理速度。但主要的特点是它的持久性,以及它不依赖于会话的持续性。


🍉 二、硬盘不是io设备吗?io读取不都是异步的吗?


是的,硬盘确实是一个 IO 设备,而大部分与硬盘相关的操作系统级IO操作确实是异步进行的,以避免阻塞进程。不过,在 Web 浏览器环境中,localStorage 的API是设计为同步的,即使底层的硬盘读写操作有着IO的特性。


js代码在访问 localStorage 时,浏览器提供的API接口通常会处于js执行线程上下文中直接调用。这意味着尽管硬盘是IO设备,当一个js执行流程访问 localStorage 时,它将同步地等待数据读取或写入完成,该过程中js执行线程会阻塞。


这种同步API设计意味着开发者在操作 localStorage 时不需要考虑回调函数或者Promise等异步处理模式,可以按照同步代码的方式来编写。不过,这也意味着如果涉及较多数据的读写操作时,可能对性能产生负面影响,特别是在主线程上,因为它会阻塞UI的更新和其他js的执行。


🍑 三、完整操作流程


localStorage 实现同步存储的方式就是阻塞 JavaScript 的执行,直到数据的读取或者写入操作完成。这种同步操作的实现可以简单概述如下:



  1. js线程调用: 当 JavaScript 代码执行一个 localStorage 的操作,比如 localStorage.getItem('key') 或 localStorage.setItem('key', 'value'),这个调用发生在 js 的单个线程上。

  2. 浏览器引擎处理: 浏览器的 js 引擎接收到调用请求后,会向浏览器的存储子系统发出同步IO请求。此时 js 引擎等待IO操作的完成。

  3. 文件系统的同步IO: 浏览器存储子系统对硬盘执行实际的存储或检索操作。尽管操作系统层面可能对文件访问进行缓存或优化,但从浏览器的角度看,它会进行一个同步的文件系统操作,直到这个操作返回结果。

  4. 操作完成返回: 一旦IO操作完成,数据要么被写入硬盘,要么被从硬盘读取出来,浏览器存储子系统会将结果返回给 js 引擎。

  5. JavaScript线程继续执行: js 引擎在接收到操作完成的信号后,才会继续执行下一条 js 代码。


在同步的 localStorage 操作期间,由于 js 的单线程性质,整个线程会阻塞,即不会执行其他任何js代码,也不会进行任何渲染操作,直到 localStorage 调用返回。


🍒 四、localStorage限制容量都是因为同步会阻塞的原因吗?



  1. 资源公平分享:同一用户可能会访问大量不同的网站,如果没有限制,随着时间的积累,每个网站可能会消耗大量的本地存储资源。这样会导致本地存储空间被少数几个站点占用,影响到用户访问其他网页的体验。限制大小可以确保所有网站都有公平的存储机会。

  2. 防止滥用:如果没有存储限制,网站可能会滥用 localStorage,存储大量数据在用户的设备上,这可能导致设备存储空间迅速耗尽,也可能侵犯用户的隐私。

  3. 性能限制:如之前提到的,localStorage 的操作是阻塞的。如果网站能够存储大量数据,就会加剧读写操作对页面性能的影响。

  4. 存储效率localStorage 存储的是字符串形式的数据,不是为存储大量或结构化数据设计的。当尝试存储过多数据时,效率会降低。

  5. 历史和兼容性:5MB 的限制很早就已经被大多数浏览器实现,并被作为一个非正式的标准被采纳。尽管现在有些浏览器支持更大的 localStorage,但出于跨浏览器兼容性的考虑,开发者通常会假设这个限制。

  6. 浏览器政策:浏览器厂商可能会依据自己的政策来设定限制,可能是出于提供用户更一致体验的角度,或者是出于管理用户数据的方便。


🍐 五、那indexDB会造成滥用吗?


虽然它们提供了更大的存储空间和更丰富的功能,但确实潜在地也可能被滥用。但是与相比 localStorage 增加了一些特性用来降低被滥用的风险:



  1. 异步操作IndexedDB 是一个异步API,即使它被用来处理更大量的数据,也不会像 localStorage 那样阻塞主线程,从而避免了对页面响应性的直接影响。

  2. 用户提示和权限:对于某些浏览器,当网站尝试存储大量数据时,浏览器可能会弹出提示,要求用户授权。这意味着用户有机会拒绝超出合理范围的存储请求。

  3. 存储配额和限制:尽管 IndexedDB 提供的存储容量比 localStorage 大得多,但它也不是无限的。浏览器会为 IndexedDB 设定一定的存储配额,这个配额可能基于可用磁盘空间的一个百分比或者是一个事先设定的限额。配额超出时,浏览器会拒绝更多的存储请求。

  4. 更清晰的存储管理IndexedDB 的数据库形式允许有组织的存储和更容易的数据管理。用户或开发者可以更容易地查看和清理占用的数据。

  5. 逐渐增加的存储:某些浏览器实现 IndexedDB 存储时,可能会在数据库大小增长到一定阈值时,提示用户是否允许继续存储,而不是一开始就分配一个很大的空间。




作者:ObjectX不知名程序员
来源:juejin.cn/post/7359405716090011659
收起阅读 »

微信小程序dom节点最高限制16000?能忍?盘它!!

web
起因 最近参与了个微信小程序的项目,有个长列表模块 我在自测的时候(测试人员居然没发现,可能懒得加载那么多数据去验证),发现当列表加载加载500条以上的时候,会有一个 Dom limit exceeded, please check if there's an...
继续阅读 »

起因


最近参与了个微信小程序的项目,有个长列表模块 我在自测的时候(测试人员居然没发现,可能懒得加载那么多数据去验证),发现当列表加载加载500条以上的时候,会有一个 Dom limit exceeded, please check if there's any mistake you've made.(超过了Dom限制,请检查你是否犯了任何错误) 渲染报错,页面会白屏,所以猜测小程序的Dom节点应该是有最高限制的。


分析问题原因


网上查了很多资料,查到了Dom节点确实有渲染限制,有位大神还晒出了源码 如下图,限制最高16000个节点


image.png


之前只知道 包大小 限制、域名需要配置不然限制、各种授权api限制,没想到 dom节点数量还要限制 居然还有这种骚操作


图片.png


不过想想也算合理,因为小程序 小程序,小才行,哈哈,资源也不可能无限大,为的就是让你在有限的资源内完成强大的功能。


实验是检验真理的唯一标准


上文图中 代码 左上角显示版本是2019年的,那现在都2023年了,会不会有所改进呢,为了搞清楚现在到底限制多少节点,实验一波。


c5d0486839eb2ee8d6711d4985df6b4.png


136f39b2c09b1ff611f4bcb15157f65.png


测试结果如上面两张图 Dom限制数量没变还是 16000个节点,但实际可新增的 渲染节点为 15999个 ,你要是问为啥,那我告诉你,因为小程序默认根标签 page 也占用一个,一个字 牛!!


图片.png


解决方案


既然找了原因,就要想办法解决,经过一番深思熟虑(面向百度思考),总结了以下几个解决方案。


方案一 人在屋檐下不得不低头


毕竟平台是人家的,规则都是人家定的,那么就得按照规则去开发,下图是官方推荐说明(也是评分标准),单页面节点尽量不超过1000个节点,嵌套不超过30层,子节点不超过60个


image.png
动动你灵活嘴皮子,看看能不能说服 产品 说服老板,就这标准 爱做不做,爱谁谁


方案二 就是不服 就是干


长列表 数据过多、dom节点过多 确实有性能问题,但谁让需求是刚需呢,看了下网上的方案 五花八门,有利用swiper 始终只展示固定数量的 swiper-item的 根据当前的 index 显示第几页数据。还有有自己写算法动态显示的。也有很多现成的组件


核心原理 我总结了下就是,只渲染显示在屏幕的数据(为了减少白页面和滑动更丝滑,要再当前屏幕的前后再分别多渲染一屏幕),实现就是监听 scroll 事件,并且重新计算需要渲染的数据,不需要渲染的数据留一个空的 view 空白占位元素。


具体实现方案就不在这里展开了,留下几个已经实现的链接地址


可以扫码体验下腾讯官方新闻小程序的 渲染效率 感觉很哇塞




小程序长列表组件


文章1


文章2


懒人方案 官方 [Skyline 渲染引擎] list-view


注意点: 列表布局容器,仅支持作为 scroll-view 自定义模式下的直接子节点或组件直接子节点


解释下,就是要注意2点


1 scroll-view要设置自定义模式 type="custom"


2 list-view要作为 scroll-view 直接子节点(不然会失去效果)


示例代码如下


    <scroll-view type="custom">
<list-view>
...
循环列表逻辑
...
</list-view>
</scroll-view>

作者:iwhao
来源:juejin.cn/post/7245223225575374905
收起阅读 »

JavaScript变量的奥秘:从声明到使用,一文掌握!

在编程的世界里,数据是构建一切的基础。而在JavaScript中,变量就是存储数据的容器。它们就像是我们生活中的盒子,可以装下各种物品,让我们在需要的时候随时取用。今天,就让我们一起揭开变量的神秘面纱,探索它们的概念、使用规则,以及那些令人头疼的错误。一、变量...
继续阅读 »

在编程的世界里,数据是构建一切的基础。而在JavaScript中,变量就是存储数据的容器。它们就像是我们生活中的盒子,可以装下各种物品,让我们在需要的时候随时取用。

今天,就让我们一起揭开变量的神秘面纱,探索它们的概念、使用规则,以及那些令人头疼的错误。


一、变量的概念和作用

变量,顾名思义,是可以变化的量。在JavaScript中,变量是用来存储数据的,这些数据可以是数字、字符串、对象等等。想象一下,如果没有变量,我们的程序就会变得非常死板,无法灵活地处理和交换信息。

Description

注意: 变量不是数据本身,它们仅仅是一个用于存储数值的容器。可以理解为是一个个用来装东西的纸箱子。


二、变量的基本使用

1)声明变量

要想使用变量,首先需要创建变量(也称为声明变量或者定义变量),JavaScript中通常使用var关键字或者let关键字进行变量的声明操作。

语法:

var age;       //声明一个名为age的变量
let name; //声明一个名为name的变量
  • 声明变量有两部分构成:声明关键字、变量名(标识符)
  • let 即声明关键字,所谓关键字是在JavaScript中有特殊意义的词汇,比如let、var、function、if、else、switch、case、break等。

举例:

let age
  • 我们声明了一个age变量
  • age 即变量的名称,也叫标识符

2) 变量赋值

声明出来后的变量是没有值的,我们需要对声明出来的变量进行赋值操作。

变量赋值的语法为:

var age;       //声明一个名为age的变量
age = 18; //为该个age变量赋值为18

定义了一个变量后,你就能够初始化它(赋值)。在变量名之后跟上一个“=”,然后是数值。

Description

注意: 是通过变量名来获得变量里面的数据。

3)变量初始化

变量初始化就相当于声明变量和变量赋值操作的结合,声明变量并为其初始化。

变量初始化语法为:

var age = 18;   //声明变量age并赋值为18

案例如下:


<!DOCTYPE html>
<html>

<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>变量的使用</title>
</head>

<body>
<script>
// 1. 声明一个年龄变量
let age
// 2. 赋值
age = 18
console.log(age)
// 3. 声明的同时直接赋值 变量的初始化
let age2 = 18
// 小案例
let num = 20
let uname = 'pink老师'
console.log(num)
console.log(uname)
</script>
</body>

</html>

4)更新变量

变量赋值后,还可以通过简单地给它一个不同的值来更新它。

Description

注意: let 不允许多次声明一个变量。
案例如下:

<!DOCTYPE html>
<html>

<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>变量的使用更新</title>
</head>

<body>
<script>
// 1 声明的同时直接赋值 变量的初始化
// let age = 18
// age = 19
// // let age = 19
// console.log(age)
// 2. 声明多个变量
// let age = 18, uname = '迪丽热巴'
// console.log(age, uname)
</script>
</body>

</html>

5)声明多个变量

语法:多个变量中间用逗号隔开

let age=18,uname='pink'

**说明:**看上去代码长度更短,但并不推荐这样。为了更好的可读性,请一行只声明一个变量。

Description

输入用户名案例:

<!DOCTYPE html>
<html>

<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>输入用户名案例</title>
</head>

<body>
<script>
// 输出用户名案例
// 1. 用户输入
// prompt('请输入姓名')
// 2. 内部处理保存数据
let uname = prompt('请输入姓名')
// 3. 打印输出
document.write(uname)
</script>
</body>

</html>

Description


三、let 和var区别

1、var声明的特点:

  • 变量可以先使用再声明(不合理)。

  • var声明过的变量可以重复声明(不合理)。

  • 比如变量提升、全局变量、没有块级作用域等等

2、let 声明的特点:

  • let声明的变量不会被提升,即在声明之前引用let声明的变量系统会直接报错,直接阻断程序的运行。

  • let不可以在同一个作用域下重复声明同一个变量,如果用let重复声明同一个变量,那么这时候就会报错。

  • 用let声明的变量支持块级作用域,在es6提出块级作用域的概念之前,作用域只存在函数里面,或者全局。而es6提出的块级作用域则是一个大括号就是一个块级作用域,该变量只能在块级作用域里使用,否则就会报错。

注意:

var 在现代开发中一般不再使用它,只是我们可能在老版程序中看到它。

let 是为了解决 var 的一些问题而出现的,以后声明变量我们统一使用 let。
案例如下:


<!DOCTYPE html>
<html>

<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>let和var的区别</title>
</head>

<body>
<script>
// var可以重复声明,后面声明的覆盖前面的
var num1
var num1 = 10
var num1= 20
console.log(num1)

// let不能重复声明,直接编译不通过
// let num
// let num = 20
// let num = 10
// console.log(num)
</script>
</body>

</html>


四、变量命名规则与规范

规则: 必须遵守,不遵守报错 (法律层面)

  • 不能用关键字(有特殊含义的字符,JavaScript 内置的一些英语词汇,例如:let、var、if、for等)

  • 只能用下划线、字母、数字、$组成,且数字不能开头

  • 字母严格区分大小写,如 Age 和 age 是不同的变量
    **规范:**建议,不遵守不会报错,但不符合业内通识 (道德层面)

  • 起名要有意义

  • 遵守小驼峰命名法:第一个单词首字母小写,后面每个单词首字母大写。例:userName。

Description

案例如下:

<!DOCTYPE html>
<html>

<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>变量的命名规范</title>
</head>

<body>
<script>
// let if = 10
let num1$_ = 11
// let nav-bar = 11
// let 1num = 10
//严格区分大小写
let pink = '老师'
let Pink = '演员'
console.log(pink, Pink)
</script>
</body>

</html>


五、Strict(严格)模式

严格模式是一种限制性更强的JavaScript运行环境。在严格模式下,一些不安全或容易出错的行为会被禁止。

  • JavaScript在设计之初,并不强制要求申明变量,如果一个变量没有申明就被使用,那么该变量就自动被声明为全局变量。

  • 在同一个页面的不同的JavaScript文件中,如果都不声明,将造成变量污染。

  • ECMA在后续规范中推出了strict模式,在strict模式下运行的JavaScript代码,强制要求申明变量,否则报错。启用strict模式的方法是在JavaScript代码的第一行写上:

'use strict';

这是一个字符串,不支持strict模式的浏览器会把它当做一个字符串语句执行,支持strict模式的浏览器将开启strict模式运行JavaScript。

想要快速入门前端开发吗?推荐一个前端开发基础课程,这个老师讲的特别好,零基础学习无压力,知识点结合代码,边学边练,可以免费试看试学,还有各种辅助工具和资料,非常适合新手!点这里前往学习哦!

如果浏览器不报错,说明你的浏览器太古老了,需要尽快升级。

'use strict';
// 如果浏览器支持strict模式,下面的代码将报ReferenceError错误:
abc = 'Hello, world';
console.log(abc);


六、常量const的概念和使用

有时候,我们希望某些变量的值在程序运行过程中保持不变。这时,可以使用const关键字来声明一个常量。

const是ES6引入的一个新特性,用于声明常量。常量一旦被声明并赋值后,其值就不能被改变。这为我们提供了一种保护机制,确保某些值不会被意外修改。

  • 使用场景:当某个变量永远不会改变的时候,就可以使用 const 来声明,而不是let。

  • 命名规范:和变量一致

  • 注意: 常量不允许重新赋值,声明的时候必须赋值(初始化)

案例如下:

<!DOCTYPE html>
<html>

<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>常量</title>
</head>

<body>
<script>
// 1.声明常量,使用常量
const PI = 3.14
console.log(PI)
//不允许更改值
//PI = 3.15
// 2. 常量声明的时候必须赋值
//const G
</script>
</body>

</html>


七、常见错误

1、常量必须要赋值

Description

2、常量被重新赋值

Description

3、变量未定义

Description

分析:

  • 提示 age变量没有定义过。

  • 很可能 age 变量没有声明和赋值。

  • 或者我们输出变量名和声明的变量不一致引起的(简单说写错变量名了)。

4、重复声明变量

Description

分析:

  • 提示 “age”已经声明。

  • 很大概率是因为重复声明了一个变量。

  • 注意let 或者const 不允许多次声明同一个变量。

变量是JavaScript编程的基础,掌握了变量的声明和使用,就能更好地理解和编写代码。希望这篇文章能帮助你更好地理解和使用变量,让你的编程之路更加顺畅。

记住,实践是最好的老师,多写代码,多尝试,你会发现,原来变量的世界,也可以如此精彩!

如果觉得本文对你有所帮助,别忘了点赞和分享哦!

收起阅读 »

一行代码引发的离奇问题,众多大佬纷纷参与点评,最终Typescript之父出手解决

web
故事的起因是这样的, 一个前端开发人员(也算是挺有名的,ariakit.org的作者, wrodPress的前维护者)在社交媒体上发了这么一条帖子。 短短几天就有了51.8万次的view。 简单的文案:又是使用 TypeScript 的一天.。表达了对Typ...
继续阅读 »

故事的起因是这样的, 一个前端开发人员(也算是挺有名的,ariakit.org的作者, wrodPress的前维护者)在社交媒体上发了这么一条帖子。


image-20240317131231595.png


短短几天就有了51.8万次的view。 简单的文案:又是使用 TypeScript 的一天.。表达了对Typescript的又爱又恨😂。在目前的前端市场上,Typescript已经成为标配,ts强大的类型检查机制给我们带来了非常多的好处(代码质量,强大的可维护性,代码即注释),但是其槽点也很多, 很多奇奇怪怪的问题(相信不仅是我一个人这么觉得),繁多的配置项组合,稍不注意就会引起页面爆红,代码量增多和代码组织也会引起一定的负担。但在这些并不能撼动Typescript 在目前前端社区中的地位,在开发项目中一般还是会选择typescript。


反应


话说回到这个帖子上,这个帖子发出来之后迅速引起发酵,被很多大佬转发和引用,下面的评论很多都是wait, what, why happen?类似的语气😂,有很多给出建议,比如换种写法, 重启下Typescript server试试,也有很多开发爱好者希望作者能提供一个例子来复现,他们也想看看是什么问题,看能不能尝试解决这个有趣的例子。


(ps: 在ts中有很多奇怪的东西,特别是在和编辑器配合的时候,有些时候不能判断出来是否是个bug?还是我们代码写的有问题?还是设计如此?还是编辑器的问题?还是版本兼容问题?仅代表个人看法)


复现例子


后来有大佬根据作者提供的信息复现出来了样板例子


declare function hasOwnPropertyextends AnyObject>(
object: T,
prop: keyof any,
): prop is keyof T
;

type EffectCallback = () => void;

declare const useSafeLayoutEffect: (effect: EffectCallback) => void;

type AnyObject = Record<string, any>;
export type State = AnyObject;

type AnyFunction = (...args: any) => any;
type BivariantCallbackextends AnyFunction> = {
bivarianceHack(...args: Parameters): ReturnType;
}["bivarianceHack"];
type SetStateAction = T | BivariantCallback<(prevState: T) => T>;

interface StoreState> {
getState(): S;
setStateextends keyof S>(key: K, value: SetStateAction): void;
}

export function useStoreProps<
S
extends State,
P
extends Partial,
K
extends keyof S,
>(
store: Store, props: P, key: K) {
const value = hasOwnProperty(props, key) ? props[key] : undefined;

useSafeLayoutEffect(() => {
if (value === undefined) return;
value;
// ^?
if (value === undefined) return; // toggle this to see the magic
value;
// ^?
store.setState(key, value);
});
}

将鼠标放到倒数第八行上显示value的类型:


const value: P[K] & ({} | null)

但是将鼠标放到倒数第五行时显示的value类型:


const value: P[K] & {}

真是见了鬼了。同样的操作复制了一遍,显示的类型却不一样?是的,这很Typescript😏。


提出issue


issue的地址在这


image-20240317141601821.png


这个提出issue的哥们就是复现样板例子的人,看的出来他应该是个狂热的技术爱好者,执行力也很强,从问作者要出现这种情况的代码仓库可以是否可以公开 ==> 复现样板例子 ==> 给Typescript提出issue(还尝试了自己能不能解决),执行力power👍。


Typescript之父出手解决


在提出issue之后立即就被官方定位是一个bug, 而且Typescript之父还给出了一个简化版可复现的例子:


function f1extends Record<string, any>, K extends keyof T>(x: T[K] | undefined) {
if (x === undefined) return;
x; // T[K] & ({} | null)
if (x === undefined) return;
x; // T[K] & {}
}

通过上面的例子发现null被意外的消除了。


ahejlsberg(ts之父) 写了一个规范化nullundefined在类型系统中的表现的函数解决了这个问题。


image-20240317152740323.png


至此issue被关闭。


我们打开palyground的nightly版本,可以发现这个问题被解决, 错误不在显示了。


总结


这是无意间从网上看到,然后从问题追溯到问题被一步步的解决。从帖子中可以看出来现在大部分用Typescript写项目的人又爱又恨的普遍状态。不管你是多菜的菜鸟也能感受到ts给日益庞大的前端项目带来的好处,不管你是多厉害的大牛也是会遇到一些奇怪的错误。随着Typescript的普及,社区中有很多不同的声音,有热爱者,有反对者,也有随波逐流者,但这也代表Typescript在社区中展现的旺盛生命力。质疑也好,热爱也罢,我觉得ts会越来越好。


作者:xinling_any
来源:juejin.cn/post/7347210988260147210
收起阅读 »

一行代码搞定禁用web开发者工具

web
在如今的互联网时代,网页源码的保护显得尤为重要,特别是前端代码,几乎就是明文展示,很容易造成源码泄露,黑客和恶意用户往往会利用浏览器的开发者工具来窃取网站的敏感信息。为了有效防止用户打开浏览器的Web开发者工具面板,今天推荐一个不错的npm库,可以帮助开发者更...
继续阅读 »

在如今的互联网时代,网页源码的保护显得尤为重要,特别是前端代码,几乎就是明文展示,很容易造成源码泄露,黑客和恶意用户往往会利用浏览器的开发者工具来窃取网站的敏感信息。为了有效防止用户打开浏览器的Web开发者工具面板,今天推荐一个不错的npm库,可以帮助开发者更好地保护自己的网站源码,本文将介绍该库的功能和使用方法。


功能介绍


npm库名称:disable-devtool,github地址:github.com/theajack/disable-devtool。从f12按钮,右键单击和浏览器菜单都可以禁用Web开发工具。



🚀 一行代码搞定禁用web开发者工具



该库有以下特性:



  • 支持可配置是否禁用右键菜单

  • 禁用 f12 和 ctrl+shift+i 等快捷键

  • 支持识别从浏览器菜单栏打开开发者工具并关闭当前页面

  • 开发者可以绕过禁用 (url参数使用tk配合md5加密)

  • 多种监测模式,支持几乎所有浏览器(IE,360,qq浏览器,FireFox,Chrome,Edge...)

  • 高度可配置、使用极简、体积小巧

  • 支持npm引用和script标签引用(属性配置)

  • 识别真移动端与浏览器开发者工具设置插件伪造的移动端,为移动端节省性能

  • 支持识别开发者工具关闭事件

  • 支持可配置是否禁用选择、复制、剪切、粘贴功能

  • 支持识别 eruda 和 vconsole 调试工具

  • 支持挂起和恢复探测器工作

  • 支持配置ignore属性,用以自定义控制是否启用探测器

  • 支持配置iframe中所有父页面的开发者工具禁用


使用方法


使用该库非常简单,只需按照以下步骤进行操作:


1.1 npm 引用


推荐使用这种方式安装使用,使用script脚本可以被代理单独拦截掉从而无法执行。


npm i disable-devtool

import DisableDevtool from 'disable-devtool';

DisableDevtool(options);

1.2 script方式使用


<script disable-devtool-auto src='https://cdn.jsdelivr.net/npm/disable-devtool'>script>

或者通过版本引用:



<script disable-devtool-auto src='https://cdn.jsdelivr.net/npm/disable-devtool@x.x.x'>script>

<script disable-devtool-auto src='https://cdn.jsdelivr.net/npm/disable-devtool@latest'>script>

1.3 npm 方式 options参数说明


options中的参数与说明如下,各方面的配置相当完善。


interface IConfig {
md5?: string; // 绕过禁用的md5值,默认不启用绕过禁用
url?: string; // 关闭页面失败时的跳转页面,默认值为localhost
tkName?: string; // 绕过禁用时的url参数名称,默认为 ddtk
ondevtoolopen?(type: DetectorType, next: Function): void; // 开发者面板打开的回调,启用时url参数无效,type 为监测模式, next函数是关闭当前窗口
ondevtoolclose?(): void; // 开发者面板关闭的回调
interval?: number; // 定时器的时间间隔 默认200ms
disableMenu?: boolean; // 是否禁用右键菜单 默认为true
stopIntervalTime?: number; // 在移动端时取消监视的等待时长
clearIntervalWhenDevOpenTrigger?: boolean; // 是否在触发之后停止监控 默认为false, 在使用ondevtoolclose时该参数无效
detectors?: Array<DetectorType>; // 启用的检测器 检测器详情
clearLog?: boolean; // 是否每次都清除log
disableSelect?: boolean; // 是否禁用选择文本 默认为false
disableCopy?: boolean; // 是否禁用复制 默认为false
disableCut?: boolean; // 是否禁用剪切 默认为false
disablePaste: boolean; // 是否禁用粘贴 默认为false
ignore?: (string|RegExp)[] | null | (()=>boolean); // 某些情况忽略禁用
disableIframeParents?:
boolean; // iframe中是否禁用所有父窗口
timeOutUrl?:
// 关闭页面超时跳转的url;
}

enum DetectorType {
Unknown = -1,
RegToString = 0, // 根据正则检测
DefineId, // 根据dom id检测
Size, // 根据窗口尺寸检测
DateToString, // 根据Date.toString 检测
FuncToString, // 根据Function.toString 检测
Debugger, // 根据断点检测,仅在ios chrome 真机情况下有效
Performance, // 根据log大数据性能检测
DebugLib, // 检测第三方调试工具 erudavconsole
};

1.4 script 方式使用属性配置


<script 
disable-devtool-auto
src='https://cdn.jsdelivr.net/npm/disable-devtool'
md5='xxx'
url='xxx'
tk-name='xxx'
interval='xxx'
disable-menu='xxx'
detectors='xxx'
clear-log='true'
disable-select='true'
disable-copy='true'
disable-cut='true'
disable-paste='true'
>
script>

1.5 事件监听


ondevtoolopen 事件的回调参数就是被触发的监测模式。可以在 ondevtoolopen 里执行业务逻辑,比如做数据上报、用户行为分析等。


DisableDevtool({
ondevtoolopen(type, next){
alert('Devtool opened with type:' + type);
next();
}
});

1.6 md5 与 tk 绕过禁用


该库中使用 key 与 md5 配合的方式使得开发者可以在线上绕过禁用。


流程如下:


先指定一个 key a(该值不要记录在代码中),使用 md5 加密得到一个值 b,将b作为 md5 参数传入,开发者在访问 url 的时候只需要带上url参数 ddtk=a,便可以绕过禁用。


disableDevtool对象暴露了 md5 方法,可供开发者加密时使用:


DisableDevtool.md5('xxx');

更多细节可查阅官方文档,中文文档地址:https://github.com/theajack/disable-devtool/blob/master/README.cn.md


最后


尽管该库可以有效地禁用浏览器的开发者工具面板,但仍然需要注意以下几点:



  • 该库只能禁用开发者工具的面板,无法阻止用户通过其他途径访问网页源码。因此,建议结合其他安全措施来保护网站。

  • 禁用开发者工具可能会对网站的调试和维护造成一定的困扰。需要调试线上代码的时候可以使用上述1.6绕过禁用进行调试。

  • 该库仅适用于现代浏览器,对于一些较旧的浏览器可能存在兼容性问题。在使用前请确保测试过兼容性。


为了进一步加强网页源码的安全性,我们可以采取以下额外措施:



  • 加密敏感代码,使用加密算法对关键代码进行加密,以防止非授权访问和修改。

  • 使用服务器端渲染,将网页的渲染过程放在服务器端,只返回最终渲染结果给客户端,隐藏源代码和逻辑。

  • 定期更新代码,定期更新代码库以充分利用新的安全特性和修复已知漏洞。


保护网页源码的安全性对于Web开发至关重要。通过使用npm库disable-devtool,并结合其他安全措施,我们可以有效地降低用户访问和修改源代码的风险。但是绝对的安全是不存在的,因此定期更新和加强安全性措施也是必要的。




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

一款好用到爆的可视化拖拽库

web
嗨,大家好,我是徐小夕,之前一直在研究可视化零代码相关的技术实践,也做了很多可视化搭建的产品,比如: H5-Dooring(页面可视化搭建平台) V6.Dooring(数据大屏可视化平台) formManager(表单搭建引擎) Next-Admin(基于n...
继续阅读 »

嗨,大家好,我是徐小夕,之前一直在研究可视化零代码相关的技术实践,也做了很多可视化搭建的产品,比如:



  • H5-Dooring(页面可视化搭建平台)

  • V6.Dooring(数据大屏可视化平台)

  • formManager(表单搭建引擎)

  • Next-Admin(基于nextjs和antd5.0的中后台管理系统)


最近在研发智能搭建系统(WEP)的时候发现一款非常好用的可视化拖拽插件——draggable。它在 github 上有17.4k star,提供了很多非常精美的拖拽案例, 我们使用它可以轻松实现可视化拖拽,组件排序,网格拖拽等效果,而且浏览器兼容性也非常不错,原生 javascript 开发, 可以轻松集成到 reactvue 等主流框架中。


接下来我就和大家一起介绍一下这款开源插件。



安装与使用


我们可以使用如下方式安装:


# yarn add shopify/draggable
pnpm add shopify/draggable

在项目里使用:


import {
Draggable,
Sortable,
Droppable,
Swappable,
} from 'shopify/draggable'

github地址: https://github.com/Shopify/draggable


接下来我就来和大家分享几个非常有价值的使用案例。


1. 3D效果拖拽



代码实现:


// eslint-disable-next-line import/no-unresolved
import {Draggable} from '@shopify/draggable';

// eslint-disable-next-line shopify/strict-component-boundaries
import Plate from '../../components/Plate';

export default function Home() {
const containerSelector = '#Home .PlateWrapper';
const container = document.querySelector(containerSelector);

if (!container) {
return false;
}

const draggable = new Draggable(container, {
draggable: '.Plate',
});
const plates = new Plate(container);

// --- Draggable events --- //
draggable.on('drag:start', (evt) => {
plates.setThreshold();
plates.setInitialMousePosition(evt.sensorEvent);
});

draggable.on('drag:move', (evt) => {
// rAF seems to cause the animation to get stuck?
// requestAnimationFrame(() => {});
plates.dragWarp(evt.source, evt.sensorEvent);
});

draggable.on('drag:stop', () => {
plates.resetWarp();
});

return draggable;
}

2. 可拖拽的开关效果


2.gif


代码如下:


// eslint-disable-next-line import/no-unresolved
import {Draggable} from '@shopify/draggable';

function translateMirror(mirror, mirrorCoords, containerRect) {
if (mirrorCoords.top < containerRect.top || mirrorCoords.left < containerRect.left) {
return;
}

requestAnimationFrame(() => {
mirror.style.transform = `translate3d(${mirrorCoords.left}px, ${mirrorCoords.top}px, 0)`;
});
}

function calcOffset(offset) {
return offset * 2 * 0.5;
}

export default function DragEvents() {
const toggleClass = 'PillSwitch--isOn';
const containers = document.querySelectorAll('#DragEvents .PillSwitch');

if (containers.length === 0) {
return false;
}

const draggable = new Draggable(containers, {
draggable: '.PillSwitchControl',
delay: 0,
});

let isToggled = false;
let initialMousePosition;
let containerRect;
let dragRect;
let dragThreshold;
let headings;
let headingText;

// --- Draggable events --- //
draggable.on('drag:start', (evt) => {
initialMousePosition = {
x: evt.sensorEvent.clientX,
y: evt.sensorEvent.clientY,
};
});

draggable.on('mirror:created', (evt) => {
containerRect = evt.sourceContainer.getBoundingClientRect();
dragRect = evt.source.getBoundingClientRect();

const containerRectQuarter = containerRect.width / 4;
dragThreshold = isToggled ? containerRectQuarter * -1 : containerRectQuarter;
headings = {
source: evt.originalSource.querySelector('[data-switch-on]'),
mirror: evt.mirror.querySelector('[data-switch-on]'),
};
headingText = {
on: headings.source.dataset.switchOn,
off: headings.source.dataset.switchOff,
};
});

draggable.on('mirror:move', (evt) => {
evt.cancel();
const offsetX = calcOffset(evt.sensorEvent.clientX - initialMousePosition.x);
const offsetY = calcOffset(initialMousePosition.y - evt.sensorEvent.clientY);
const offsetValue = offsetX > offsetY ? offsetX : offsetY;
const mirrorCoords = {
top: dragRect.top - offsetValue,
left: dragRect.left + offsetValue,
};

translateMirror(evt.mirror, mirrorCoords, containerRect);

if (isToggled && offsetValue < dragThreshold) {
evt.sourceContainer.classList.remove(toggleClass);
headings.source.textContent = headingText.off;
headings.mirror.textContent = headingText.off;
isToggled = false;
} else if (!isToggled && offsetValue > dragThreshold) {
evt.sourceContainer.classList.add(toggleClass);
headings.source.textContent = headingText.on;
headings.mirror.textContent = headingText.on;
isToggled = true;
}
});

const triggerMouseUpOnESC = (evt) => {
if (evt.key === 'Escape') {
draggable.cancel();
}
};

draggable.on('drag:start', () => {
document.addEventListener('keyup', triggerMouseUpOnESC);
});

return draggable;
}

3.可拖拽的网格元素


3.gif


源码地址: https://github.com/Shopify/draggable/tree/master/examples/src/content/Droppable/UniqueDropzone


4. 可拖拽的列表


4.gif


源码地址: https://github.com/Shopify/draggable/tree/master/examples/src/content/Sortable/SimpleList


5. 卡牌拖拽效果


5.gif


源码地址: https://github.com/Shopify/draggable/tree/master/examples/src/content/Sortable/Transformed


6. 多容器拖拽效果


6.gif


源码地址: https://github.com/Shopify/draggable/tree/master/examples/src/content/Sortable/MultipleContainers


7. 不规则网格拖拽


7.gif


源码地址:https://github.com/Shopify/draggable/tree/master/examples/src/content/Swappable/Floated


8. 拖拽排序动画


8.gif


源码地址: https://github.com/Shopify/draggable/tree/master/examples/src/content/Plugins/SortAnimation


当然还有很多有意思的拖拽案例, 大家也可以去体验一下。


今天就分享到这啦,祝大家节日快乐, 博学!


如果有收获,记得点赞 + 再看哦, 欢迎在评论区评论, 分享你的收藏干货~




作者:徐小夕
来源:juejin.cn/post/7353877562303021093
收起阅读 »

关于页面适配的一些方案

web
早期的页面使用了左右布局。左侧宽度固定,右侧宽度自适应。未使用vm、em、百分比等进行屏幕适配。所有的尺寸(宽度、高度、边框宽度、字体大小等)全部使用的px进行开发。导致只有常用的显示屏尺寸显示较为正常,但是小屏幕显示不正常。 媒体查询屏幕适配 正常显示屏的...
继续阅读 »

早期的页面使用了左右布局。左侧宽度固定,右侧宽度自适应。未使用vm、em、百分比等进行屏幕适配。所有的尺寸(宽度、高度、边框宽度、字体大小等)全部使用的px进行开发。导致只有常用的显示屏尺寸显示较为正常,但是小屏幕显示不正常。



媒体查询屏幕适配


正常显示屏的分辨率是1920 * 1080【假如缩放比例为100%】。在此尺寸下显示正常的布局和展示,如果修改分辨率为1360 * 768。则正常显示的字体等有一种放大的效果。


image.png
如果想要同1920的显示屏同样的显示效果,则需要在index.html中设置:



@media(max-width: 1440px) {
html {
zoom: 90%;
}
}

image.png


但是有一个弊端,字体会变模糊。


根据dpr适配


很多小屏幕推荐的缩放比例是150%。


此时根据dpr进行适配


    @media (-webkit-min-device-pixel-ratio: 1.5) {
html {
zoom: 0.67
}
}

注意,在此设置下,如果系统中有根据pageX, pageY进行定位时,需要额外处理。


        if (window.devicePixelRatio == 1.5) {
x = x/0.67;
y = y/0.67;
}

作者:一涯
来源:juejin.cn/post/7306749023473451045
收起阅读 »

老板让我用JavaScript实现网页复制成图片到剪贴板

web
李经理在使用飞书时无意中发现,飞书竟然支持一键复制网页内容到剪贴板的功能。 他立即叫来了公司的前端开发小王,兴致勃勃地说: "小王啊,你看,飞书的这个功能多方便!我们公司的协同办公系统是不是也可以实现类似的功能?这样用户体验一定能得到很大提升!" 小王看着李经...
继续阅读 »

李经理在使用飞书时无意中发现,飞书竟然支持一键复制网页内容到剪贴板的功能。


他立即叫来了公司的前端开发小王,兴致勃勃地说:


"小王啊,你看,飞书的这个功能多方便!我们公司的协同办公系统是不是也可以实现类似的功能?这样用户体验一定能得到很大提升!"


小王看着李经理充满expectant的眼神, 虽然内心已经吐槽"就这点功能至于吗", 但表面上还是恭恭敬敬地回答:


"老板英明,这个功能确实很实用。技术上应该不难实现,主要就是用Clipboard API写几行代码的事。我这就去安排!"


Xnip2024-03-21_11-42-26.jpg


回到工位后,小王苦笑着摇摇头,找来相关文档开始翻阅,暗暗发誓一定要把这个"划时代"的功能做好.


小王找来了领导说的飞书文档复制网页内容的功能, 如下:


Untitled.png


小王思考了片刻…


功能拆解:


要实现这个功能, 要拆分为4个步骤:



  1. 获得选中内容所属的 div

  2. 把选中内容的div 转换成canvas

  3. 转换canvas到二进制图像

  4. 复制二进制图像到剪贴板


由于小王的业务只需要复制固定区域的div, 所以第一步可以忽略, 简化成:


  const element = document.getElementById("target");

转换div成 canvas:


时间已经很晚了, 小王咳了一杯咖啡, 继续奋战. 小王苦思冥想, 要怎么把div转换成 canvas. 他琢磨:



  1. 递归遍历 DOM 树:

    • 会从指定的根元素开始,递归遍历整个 DOM 树。

    • 对于每个遇到的元素, 分析其样式、位置、大小等属性。



  2. 处理样式和布局:

    • 通过读取元素的 CSS 样式,如颜色、背景、边框等, 复制元素的视觉表现。

    • 它会计算元素的盒模型、定位、层叠等布局信息,以确定元素在最终图片中的位置。





小王这时候已经觉得很累了, 于是索性打开浏览器搜索, 结果第一页就看到了: html2canvas. 他看了一眼, github 29K stars. 他查看了一下调用api:


html2canvas(document.body).then(function(canvas) {
document.body.appendChild(canvas);
});

它正是小王需要的!


于是小王在项目中命令行输入:


npm install --save html2canvas

然后小王在业务代码中敲下了:


function copyDivToImage() {
const element = document.getElementById("target");
html2canvas(element).then(canvas => {
// canvas 拿到了, 然后呢
}
}

转换canvas到二进制图像


小王犹豫, 为什么要转成二进制图像呢, 我直接复制 base64 字符不行吗. 不过很快, 小王就意识到了, 剪贴版API 不支持base64字符串的类型. 于是他翻开 mdn 文档:


HTMLCanvasElement: toBlob() method - Web APIs | MDN (mozilla.org)



function copyDivToImage() {
const element = document.getElementById("target");
html2canvas(element).then(canvas => {
canvas.toBlob(
(blob) => {
// 复制文件到剪贴板
},
"image/jpeg", // 文件的格式
1 // 图像压缩质量 0-1
);
});
}

复制二进制图像到剪贴板


这一步小王已经先前看过 MDN 文档了, ClipboardItem - Web APIs | MDN (mozilla.org) 可以直接调用浏览器的 navigator api :



function copyDivToImage() {
const element = document.getElementById("target");
html2canvas(element).then(canvas => {
canvas.toBlob(
(blob) => {
// 复制文件到剪贴板
try {
await navigator.clipboard.write([
// eslint-disable-next-line no-undef
new ClipboardItem({
[blob.type]: blob
})
]);
console.log("图像已成功复制到剪贴板");
} catch (err) {
console.error("无法复制图像到剪贴板", err);
}
},
"image/jpeg", // 文件的格式
1 // 图像压缩质量 0-1
);
});
}

小王遇到挫折


所有代码已经就绪, 小王随即启动项目, 运行他刚刚编写好的完美的代码. 不出所料, 他遇到了挫折:


Untitled 1.png


小王看到这个报错, 完全没有头绪, 幸好有多年的开发经验, 他遇到这种问题的时候并没有慌张, 内心想, “第一次跑通常这样!”. 随即他打开百度搜索, 有一个回答引起了小王的注意:


Untitled 2.png


原来, 小王是在 http 环境调试的, 他修改了代理的配置, 换成了 https 环境下调试本地代码.


然而让小王没有想到的是, 程序还是没有如期运行, 小王遇到了第二个挫折:


Untitled 3.png


小王崩溃了 “这是什么鬼. 明明都是按照API文档写的!”


Untitled 4.png


原来, 浏览器剪贴板对 jpeg的支持不大好, 于是小王把 canvas.toBlob() 的参数改成了 "image/png”.


他再次运行代码, 他成功了:


Untitled 5.png


小王欣喜地把这个消息告诉了李经理.


功夫不负有心人,凭借扎实的JavaScript功底,小王很快就实现了一个简洁优雅的"一键复制"功能,并成功集成到公司的协同办公系统中。


李经理在看到小王的杰作后非常满意,当即表扬了小王的能力和效率,并承诺会在年终绩效考核中给予小王优秀评级,同时还暗示未来会给小王升职加薪的机会。小王听后喜上眉梢,他明白自己的努力和才能得到了老板的认可。


这次经历不仅巩固了小王在公司中的地位,更坚定了他在前端开发领域继续钻研的决心。他暗自庆幸,幸亏当初学习JavaScript时没有偷懒,才能在关键时刻派上用场,赢得了老板的青睐。


从此以后,小王在技术方面更加勤奋刻苦,也更加善于捕捉用户需求和痛点,设计出更多优秀的功能和体验。他逐渐成长为团队中不可或缺的核心成员,并最终如愿晋升为高级前端开发工程师,走上了实现自我价值和理想的康庄大道。


作者:ziolau
来源:juejin.cn/post/7348634049681293312
收起阅读 »

如何在HTML中使用JavaScript:从基础到高级的全面指南!

JavaScript是一种轻量级的编程语言,通常用于网页开发,以增强用户界面的交互性和动态性。然而在HTML中,有多种方法可以嵌入和使用JavaScript代码。本文就带大家深入了解如何在HTML中使用JavaScript。一、使用 script 标签要在HT...
继续阅读 »

JavaScript是一种轻量级的编程语言,通常用于网页开发,以增强用户界面的交互性和动态性。然而在HTML中,有多种方法可以嵌入和使用JavaScript代码。本文就带大家深入了解如何在HTML中使用JavaScript。

一、使用 script 标签

要在HTML中使用JavaScript,我们需要使用<script>标签。这个标签可以放在<head>或<body>部分,但通常我们会将其放在<body>部分的底部,以确保在执行JavaScript代码时,HTML文档已经完全加载。

Description

使用 <script> 标签有两种方式:

  • 直接在页面中嵌入 JavaScript 代码和包含外部 JavaScript 文件。

  • 包含在 <script> 标签内的 JavaScript 代码在浏览器总按照从上至下的顺序依次解释。

所有 <script> 标签都会按照他们在 HTML 中出现的先后顺序依次被解析。


HTML为 <script> 定义了几个属性:

1)async: 可选。表示应该立即下载脚本,但不妨碍页面中其他操作。该功能只对外部 JavaScript 文件有效。

如果给一个外部引入的js文件设置了这个属性,那页面在解析代码的时候遇到这个<script>的时候,一边下载该脚本文件,一边异步加载页面其他内容。

2)defer: 可选。表示脚本可以延迟到整个页面完全被解析和显示之后再执行。该属性只对外部 JavaScript 文件有效。

3)src: 可选。表示包含要执行代码的外部文件。

4)type: 可选。表示编写代码使用的脚本语言的内容类型,目前在客户端,type属性值一般使用 text/javascript。

不过这个属性并不是必需的,如果没有指定这个属性,则其默认值仍为text/javascript。


1.1 直接在页面中嵌入JavaScript代码

内部JavaScript是将JavaScript代码放在HTML文档的<script>标签中。这样可以将JavaScript代码与HTML代码分离,使结构更清晰,易于维护。


在使用<script>元素嵌入JavaScript代码时,只须为<script>指定type属性。然后,像下面这样把JavaScript代码直接放在元素内部即可:

<script type="text/javascript">
function sayHi(){
alert("Hi!");
}
</script>

如果没有指定script属性,则其默认值为text/javascript。


包含在<script>元素内部的JavaScript代码将被从上至下依次解释。在解释器对<script>元素内部的所有代码求值完毕以前,页面中的其余内容都不会被浏览器加载或显示。

在使用<script>嵌入JavaScript代码的过程中,当代码中出现"</script>"字符串时,由于解析嵌入式代码的规则,浏览器会认为这是结束的</script>标签。可以通过转义字符“\”写成</script>来解决这个问题。

1.2包含外部JavaScript文件

外部JavaScript是将JavaScript代码放在单独的.js文件中,然后在HTML文档中通过<script>标签的src属性引用这个文件。这种方法可以使代码更加模块化,便于重用和共享。

如果要通过<script>元素来包含外部JavaScript文件,那么src属性就是必需的。

这个属性的值是一个指向外部JavaScript文件的链接。

<script type="text/javascript" src="example.js"></script>
  • 外部文件example.js将被加载到当前页面中。

  • 外部文件只须包含通常要放在开始的<script>和结束的</script>之间的那些JavaScript代码即可。

与解析嵌入式JavaScript代码一样,在解析外部JavaScript文件(包括下载该文件)时,页面的处理也会暂时停止。

注意: 带有src属性的<script>元素不应该在其<script>和</script>标签之间再包含额外的JavaScript代码。如果包含了嵌入的代码,则只会下载并执行外部脚本文件,嵌入的代码会被忽略。

通过<script>元素的src属性还可以包含来自外部域的JavaScript文件。它的src属性可以是指向当前HTML页面所在域之外的某个域中的完整URL。

<script type="text/javascript" src="http://www.somewhere.com/afile.js"></script>

于是,位于外部域中的代码也会被加载和解析。


1.3 标签的位置

在HTML中,所有的<script>标签会按照它们出现的先后顺序被解析。在不使用defer和async属性的情况下,只有当前面的<script>标签中的代码解析完成后,才会开始解析后面的<script>标签中的代码。

通常,所有的<script>标签应该放在页面的<head>标签中,这样可以将外部文件(包括CSS和JavaScript文件)的引用集中放置。

然而,如果将所有的JavaScript文件都放在<head>标签中,会导致浏览器在呈现页面内容之前必须下载、解析并执行所有JavaScript代码,这可能会造成明显的延迟,导致浏览器窗口在加载过程中出现空白。

为了避免这种延迟问题,现代Web应用程序通常会将所有的JavaScript引用放置在<body>标签中的页面内容的后面。这样做可以确保在解析JavaScript代码之前,页面的内容已经完全呈现在浏览器中,从而加快了打开网页的速度。


二、执行JavaScript 程序

JavaScript 解析过程包括两个阶段:预处理(也称预编译)和执行。

Description

  • 在编译期,JavaScript 解析器将完成对 JavaScript 代码的预处理操作,把 JavaScript 代码转换成字节码;

  • 在执行期,JavaScript 解析器把字节码生成二进制机械码,并按顺序执行,完成程序设计的任务。

1、执行过程

HTML 文档在浏览器中的解析过程是:按照文档流从上到下逐步解析页面结构和信息。

JavaScript 代码作为嵌入的脚本应该也算做 HTML 文档的组成部分,所以 JavaScript 代码在装载时的执行顺序也是根据 <script> 标签出现的顺序来确定。

想要快速入门前端开发吗?推荐一个前端开发基础课程,这个老师讲的特别好,零基础学习无压力,知识点结合代码,边学边练,可以免费试看试学,还有各种辅助工具和资料,非常适合新手!点这里前往学习哦!

2、预编译

当 JavaScript 引擎解析脚本时候,他会在与编译期对所有声明的变量和函数预先进行处理。当 JavaScript 解析器执行下面脚本时不会报错。

alert(a);    //返回值 undefined
var a = 1;
alert(a); //返回值 1

由于变量声明是在预编译期被处理的,在执行期间对于所有的代码来说,都是可见的,但是执行上面代码,提示的值是 undefined 而不是 1。

因为变量初始化过程发生在执行期,而不是预编译期。在执行期,JavaScript 解析器是按照代码先后顺序进行解析的,如果在前面代码行中没有为变量赋值,则 JavaScript 解析器会使用默认值 undefined 。

由于第二行中为变量 a 赋值了,所以在第三行代码中会提示变量 a 的值为 1,而不是 undefined。

fun();    //调用函数,返回值1
function fun(){
alert(1);
}

函数声明前调用函数也是合法的,并能够正确解析,所以返回值是 1。但如果是下面这种方式则 JavaScript 解释器会报错。

fun();    //调用函数,返回语法错误
var fun = function(){
alert(1);
}

上面的这个例子中定义的函数仅作为值赋值给变量 fun 。在预编译期,JavaScript 解释器只能够为声明变量 fun 进行处理,而对于变量 fun 的值,只能等到执行期时按照顺序进行赋值,自然就会出现语法错误,提示找不到对象 fun。

总结: 声明变量和函数可以在文档的任意位置,但是良好的习惯应该是在所有 JavaScript 代码之前声明全局变量和函数,并对变量进行初始化赋值。在函数内部也是先声明变量,后引用。

通过今天的分享,相信大家已经对JavaScript在HTML中的应用有了一定的了解。这只是冰山一角,JavaScript的潜力远不止于此。

希望这篇文章能激发大家对编程的热情,让我们一起在编程的世界里探索更多的可能性!

收起阅读 »

CSS如何优雅的实现卡片多行排列布局?

web
欢迎关注本专栏,会经常分享一些简单实用的技巧! 感谢各位大佬点赞!关注我,学习实用前端知识! 需求简介 在前端开发中,我们经常遇见这样的开发需求,实现下列以此排布的卡片,这些卡片宽度一般是固定的, 并且在不同大小的屏幕宽度下自动换行。 实际开发中遇到的问...
继续阅读 »

欢迎关注本专栏,会经常分享一些简单实用的技巧!



感谢各位大佬点赞!关注我,学习实用前端知识!


需求简介


在前端开发中,我们经常遇见这样的开发需求,实现下列以此排布的卡片,这些卡片宽度一般是固定的,



并且在不同大小的屏幕宽度下自动换行。



实际开发中遇到的问题


实现这样的一个需求其实不难,我们很容易想到设置一个安全宽度(如下图绿色),然后进行弹性布局。



一个很容易写出的代码是这样的:





使用flex弹性布局,我们很看似轻松的实现了需求。但是,当我们将卡片数量减少一个,问题就出现了




由于我们使用了justify-content: space-between;的布局方式,4,5卡片左右对称布局,这显然不符合我们的要求!



聪明的人,可能会把justify-content: space-between改成align-content: space-between





这样的确会让卡片以此排列,但是没了右边距!因此,你可能会手动加上右边距




你会尴尬的发现换行了,因为两个卡片的宽度加元素的右边距之和大于你设置的安全宽度了!



当然,你可以让每个卡片的右边距小一点,这样不会换行,但是,右边的元素永远无法贴边了!



如何解决这个问题


想解决上的问题,也有很多方法。


如果永远是第3n的元素是最后一列,这个问题非常容易解决:


.container{
display: flex;
width:630px;
align-content: space-between;
flex-flow: wrap;
.crad{
height:100px;
background: blueviolet;
width:200px;
margin-bottom: 16px;
margin-right: 16px;
&:nth-child(3n) {
margin-right: 0;
}
}
}

4n,5n,6n我们都可以用这样的方式解决!


但如果安全宽度是变化的(630px不固定),比如随着浏览器尺寸的变化,每行的卡片数量也变化,上述方式就无法解决了。



此时,我们可以用下面的方法:


我们可以在绿色盒子外在套一个红色盒子,超出红色盒子的部分隐藏即可


代码如下






上述代码中,我们的container元素设置了width: calc(100% + 16px)保证其比父元素多出16px的容错边距,然后我们给红色盒子设置了overflow: hidden,就避免了滚动条出现。


完美解决了这个布局问题!


作者:石小石Orz
来源:juejin.cn/post/7358295139457400869
收起阅读 »

autolog.js:一个小而美的toast插件。

web
前言 最近需要做一个关于自动解析矢量瓦片链接地址的内部Demo,这个demo比较简单,所以没有准备引入任何的第三方UI库,所以遇到了一个小问题,toast提示怎么做? 如果像往常一样,我肯定直接用 alert 了,但是一是 alert 会中断体验,不够友好,二...
继续阅读 »

前言


最近需要做一个关于自动解析矢量瓦片链接地址的内部Demo,这个demo比较简单,所以没有准备引入任何的第三方UI库,所以遇到了一个小问题,toast提示怎么做?


如果像往常一样,我肯定直接用 alert 了,但是一是 alert 会中断体验,不够友好,二是不适用于多个提示共同出现,三是无法区分提示类型,所以我就想着找一个体积小的三方库来实现,但是找来找去,发现没有一个库能入我法眼。


在网上搜索,好像独立的 toast 插件停留在了 jq 时代,靠前的 toast 库居然是 bootstrap 的。所以我决定自己写一个,又小巧,又易用的 toast 插件。


纯 JS 实现


延续 autofit.js 的传统,我依然准备用纯 js 实现,以达到极致的体积、极致的兼容性。此外,还编写了d.ts,支持TS。


autolog.js 诞生了。


image.png


它由两部分构成,一个极简单的js,和一个极简单的css。gzip后体积是1.40kb。


在线体验:larryzhu-dev.github.io/autoLarryPa…


js部分(共37行)


const autolog = {
log(text, type = "log", time = 2500) {
if (typeof type === "number") {
time = type;
type = "log";
}
let mainEl = getMainElement();
let el = document.createElement("span");
el.className = `autolog-${type}`;
el.innerHTML = text;
mainEl.appendChild(el);
setTimeout(() => {
el.classList.add("hide");
}, time - 500);
setTimeout(() => {
mainEl.removeChild(el);
el = null;
}, time);
},
};
function getMainElement() {
let mainEl = document.querySelector("#autolog");
if (!mainEl) {
mainEl = document.createElement("div");
mainEl.id = "autolog";
document.body.appendChild(mainEl);
}
return mainEl;
}
export default autolog;


以上是 autolog.js的全部 js 代码。可以看到只导出了一个 log 方法,而调用此方法,也只需要必填一个参数。


我来讲一下这段代码干了一件什么事



  1. 因为有两个可选参数,所以第一步判断一下传了哪个可选参数,这可以在使用时,只传time或者type。

  2. 获取主容器,getMainElement 方法返回一个主容器,若主容器不存在,就创建它,这省去了用户手动创建主容器的过程,一般的插件会导出一个 init 方法初始化,这一步可以省去 init 操作。

  3. 创建一个 span 标签用于展示 log 内容。

  4. 两个定时器,第一个在清除元素的前 0.5 秒为其添加退场动画,第二个清除元素,el = null 可以保证断开引用,防止产生游离dom,防止内存泄漏。


最重要的在于css部分,css承载了最重要的显示逻辑。


css部分(共100行)


@font-face {
font-family: "iconfont"; /* Project id 4507845 */
src: url("//at.alicdn.com/t/c/font_4507845_4ys40xqhy9u.woff2?t=1713154951707")
format("woff2"),
url("//at.alicdn.com/t/c/font_4507845_4ys40xqhy9u.woff?t=1713154951707")
format("woff"),
url("//at.alicdn.com/t/c/font_4507845_4ys40xqhy9u.ttf?t=1713154951707")
format("truetype");
}
#autolog {
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
pointer-events: none;
width: 100vw;
height: 100vh;
position: fixed;
left: 0;
top: 0;
z-index: 9999999;
cursor: pointer;
transition: 0.2s;
}
#autolog span {
pointer-events: auto;
width: max-content;
animation: fadein 0.4s;
animation-delay: 0s;
border-radius: 6px;
padding: 10px 20px;
box-shadow: 0 0 10px 6px rgba(0, 0, 0, 0.1);
margin: 4px;
transition: 0.2s;
z-index: 9999999;
font-size: 14px;
height: max-content;
background-color: #fafafa;
color: #333;
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
#autolog span::before {
padding-right: 4px;
}
#autolog span.autolog-warn,
#autolog span.autolog-warning {
background-color: #fffaec;
color: #e29505;
}
#autolog span.autolog-warn::before,
#autolog span.autolog-warning::before {
content: "\e682";
}
#autolog span.autolog-error {
background-color: #fde7e7;
color: #d93025;
}
#autolog span.autolog-error::before {
content: "\e66f";
}
#autolog span.autolog-info {
background-color: #e6f7ff;
color: #0e6eb8;
}
#autolog span.autolog-info::before {
content: "\e668";
}
#autolog span.autolog-success,
#autolog span.autolog-ok,
#autolog span.autolog-done {
background-color: #e9f7e7;
color: #1a9e2c;
}
#autolog span.autolog-success::before,
#autolog span.autolog-ok::before,
#autolog span.autolog-done::before {
content: "\e67f";
}
#autolog span.hide {
opacity: 0;
pointer-events: none;
transform: translateY(-10px);
height: 0;
padding: 0;
margin: 0;
}
@keyframes fadein {
0% {
opacity: 0;
transform: translateY(-10px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}

css部分正正好好 100 行代码,从上到下分别是:iconfont 字体图标链接、主容器样式、各类型提示框的样式、退场类,入场动画。


由此可见,你也可以重写这些css,为他们添加不同的 icon、颜色。


没有什么巧妙的设计,也没有什么精致的构思,朴实无华的一百多行代码而已,希望这些代码可以帮到各位。


安装和使用


使用也非常简单,只需引入两个文件。


安装


npm i autolog.js

引入css(引入一次即可)


在js中引入


import 'autolog.js/autolog.css'

在css中引入


@import url('autolog.js/autolog.css');

使用


import aotolog from "autolog.js";

autolog.log("Hi,this is a normal tip");
autolog.log("Hello World", "success", 2500);
// 其中 "success" 和 2500 都是可选项

Github Link:github.com/LarryZhu-de…


NPM Link:http://www.npmjs.com/package/aut…


效果图


QQ2024417-122454.webp


在线体验:larryzhu-dev.github.io/autoLarryPa…


作者:德莱厄斯
来源:juejin.cn/post/7358598695267008527
收起阅读 »

这个交互式个人博客能让你眼前一亮✨👀 ?

web
从构思到上线的全过程,开发中遇到一些未知问题,也都通过查阅资料和源码一一解决,小记一下望对正在使用或即将使用Nextjs开发的你们有所帮助。 那些年我开发过的博客 就挺有意思,域名,技术栈和平台的折腾史 2018年使用hexo搭建了个静态博客,部署在gith...
继续阅读 »

2023-08-15 13.21.03.gif


从构思到上线的全过程,开发中遇到一些未知问题,也都通过查阅资料和源码一一解决,小记一下望对正在使用或即将使用Nextjs开发的你们有所帮助。


那些年我开发过的博客


就挺有意思,域名,技术栈和平台的折腾史



  • 2018年使用hexo搭建了个静态博客,部署在github pages

  • 2020年重新写了博客,vuenodejsmongodb三件套,使用nginx部署在云服务器上

  • 2023年云服务器过期了,再一次重写了博客,nextjs为基础框架,部署在vercel


背景


因为日常开发离不开终端,正好也有重写博客的想法,打算开发一个不只是看的博客网站,所以模仿终端风格开发了Yucihent


技术栈


nextjs 更多技术栈


选用nextjs是因为next13更新且稳定了App Router和一些其他新特性。


设计


简约为主,首页为类终端风格,prompt样式参考了starship,也参考过ohmyzsh themes,选用starship因为觉得更好看。


交互


通过手动输入或点击列出的命令进行交互,目前可交互的命令有:



  • help 查看更多

  • listls 列出可用命令

  • clear 清空所有输出

  • posts 列出所有文章

  • about 关于我


后续会新增一些命令,增加交互的趣味。


暗黑模式



基于tailwinddark modenext-themes



首先将tailwinddark mode设置为class,目的是将暗黑模式的切换设置为手动,而不是跟随系统。


// tailwind.config.js

module.exports = {
darkMode: 'class'
}

新建ThemeProvider组件,用到next-themes提供的ThemeProvider,需要在文件顶部使用use client,因为createContext只在客户端组件使用。


'use client'

import { ThemeProvider as NextThemeProvider } from 'next-themes'
import type { ThemeProviderProps } from 'next-themes/dist/types'

export default function ThemeProvider({
children,
...props
}: ThemeProviderProps
) {
return <NextThemeProvider {...props}>{children}</NextThemeProvider>
}

app/layout.tsx中使用ThemeProvider,设置attributeclass,这是必要的。


<ThemeProvider attribute="class">{children}</ThemeProvider>

next-themes提供了useTheme,解构出themesetTheme用于手动设置主题。


综上基本实现暗黑模式切换,但你会在控制台看到此报错信息:Warning: Extra attributes from the server: class,style,虽然它并不影响功能,但终究是个报错。
作为第三方包,可能存在水合不匹配的问题,经查阅资料,禁用ThemeProvider组件预渲染消除报错。


资料:



const NoSSRThemeProvider =
dynamic(() => import('@/components/ThemeProvider'), {
ssr: false
})

<NoSSRThemeProvider attribute="class">{children}</NoSSRThemeProvider>

类终端



由输入和输出组件组成,输入的结果添加到输出list中



命令输入的打字效果


Alt Text

定义打字间隔100ms,对键入的命令for处理,定时器中根据遍历的索引延迟赋值。


const autoTyping = (cmd: string) => {
const interval = 100 // ms
for (let i = 0; i < cmd.length; i++) {
setTimeout(
() => {
setCmd((prev) => prev + cmd.charAt(i))
},
interval * (i + 1)
)
}
}

滚动到底部


定义外层容器refcontainerRef,键入命令后都自动滚动到页面底部,使用了scrollIntoViewapi,作用是让调用这个api的容器始终在页面可见,block参数设置为end表示垂直方向末端对其即最底端。


const containerRef = useRef<HTMLDivElement>(null)
useEffect(() => {
containerRef.current?.scrollIntoView({
behavior: 'smooth',
block: 'end'
})
}, [typedCmds])

MDX



何为mdx?即给md添加了jsx支持,功能更强大的md,在nextjs中通过@next/mdx解析.mdx文件,它会将mdreact components转成html



安装相关包,后两者作为@next/mdxpeerDependencies



  • @next/mdx

  • @mdx-js/loader

  • @mdx-js/react


next.config.js新增createMDX配置


// next.config.js

import createMDX from '@next/mdx'

const nextConfig = {}

const withMDX = createMDX()
export default withMDX(nextConfig)

接着在应用根目录下新建mdx-components.tsx


// mdx-components.tsx

import type { MDXComponents } from 'mdx/types'

export function useMDXComponents(components: MDXComponents): MDXComponents {
return {
...components
}
}

app目录下使用.mdx文件,useMDXComponents组件是必要的,


需要注意的是此文件命名上有一定规范只能命名为mdx-components,不能为其他名称,也不可为MdxComponents,从@next/mdx源码中可以看出会去应用根目录查找mdx-components


// @next/mdx部分源码

config.resolve.alias['next-mdx-import-source-file'] = [
'private-next-root-dir/src/mdx-components',
'private-next-root-dir/mdx-components',
'@mdx-js/react'
]

至此就可以在app中使用mdx


排版



为mdx解析成的html添加样式



解析mdx为html,但并没有样式,所以我们借助@tailwindcss/typography来为其添加样式,在tailwind.config.js使用该插件。


// tailwind.config.js

module.exports = {
plugins: [require('@tailwindcss/typography')]
}

在外层标签上添加prose的className,prose-invert用于暗黑模式。


<article className="prose dark:prose-invert">{mdx}</article>

综上我们实现了对mdx的样式支持,然而有一点是@tailwindcss/typography并不会对mdx代码块中代码进行高亮。


代码高亮



写文章或多或少都有代码,高亮是必不可少,那么react-syntax-highlighter该上场了



定义一个CodeHighligher组件


// CodeHighligher.tsx

import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
import {
oneDark,
oneLight
} from 'react-syntax-highlighter/dist/cjs/styles/prism'
import { useTheme } from 'next-themes'

export default function CodeHighligher({
lang,
code
}: {
lang: string
code: string
}
) {
const { theme } = useTheme()
return (
<SyntaxHighlighter
language={lang?.replace(/\language-/, '') || 'javascript'}
style={theme === 'light' ? oneLight : oneDark}
customStyle={{
padding: 20,
fontSize: 15,
fontFamily: 'var(--font-family)'
}}
>

{code}
</SyntaxHighlighter>

)
}

react-syntax-highlighter高亮代码可用hljsprism,我在这使用的prism,两者都有众多代码高亮主题可供选择,lang如果没标注则默认设置为javascript也可以简写为js,值得注意的是如果是使用hljs,则必须写javascript,不可简写为js,否则代码高亮失败,这一点prism更加友好。


同时可通过useTheme实现亮色,暗色模式下使用不同代码高亮主题。


组件写好了,该如何使用?上面讲到过mdx的解析,在useMDXComponents重新渲染pre标签。


// mdx-components.tsx

import type { MDXComponents } from 'mdx/types'
import CodeHighligher from '@/components/CodeHighligher'

export function useMDXComponents(components: MDXComponents): MDXComponents {
return {
pre: ({ children }) => {
const { className, children: code } = props
return <CodeHighligher lang={className} code={code} />
}
}
}

mdx文件中代码块会被解析成pre标签,可以对pre标签返回值作进一步处理,即返回高亮组件,这样可实现对代码高亮,当然高亮主题很多,选自己喜欢的。


文章


元数据



文章一些信息如标题,描述,日期,作者等都作为文章的元数据,使用yaml语法定义



---
title: '文章标题'
description: '文章描述'
date: '2020-01-01'
---

@next/mdx默认不会按照yaml语法解析,这会被解析成h2标签,然而我们并不希望元数据被解析成h2标签作为内容展示,更希望拿这类数据做其他处理,
为了正确解析yaml,需要借助remark-frontmatter来实现。


使用该插件,注意需要修改next配置文件名为next.config.mjs,因为remark-frontmatter只支持ESM规范。


// next.config.mjs

import createMDX from '@next/mdx'
import frontmatter from 'remark-frontmatter'

const nextConfig = {}

const withMDX = createMDX({
options: {
remarkPlugins: [frontmatter]
}
})
export default withMDX(nextConfig)

yaml被正确解析了那么我们可以使用gray-matter来获取文章元数据


列表


由于app目录是运行在nodejs runtime下,基本思路是用nodejs的fs模块去读取文章目录即mdxs/posts,读取该目录下的所有文章放在一个list中。


使用fs.readdirSync读取文章目录内容,但是这仅仅是拿到文章名称的集合。


const POST_PATH = path.join(process.cwd(), 'mdxs/posts')

// 文章名称集合
export function getPostList() {
return fs.readdirSync(POST_PATH).map((name) => name.replace(/\.mdx/, ''))
}

文章列表中展示的是标题而不是名称,标题作为文章的元数据,通过gray-matterreadapi读取文件可获取(也可以使用fs.readFileSync) read返回datacontent的对象,
data是元数据信息,content则是文章内容。


export function getPostMetaList() {
const posts = getPostList()

return posts.map((post) => {
const {
data: { title, description, date }
} = matter.read(path.join(POST_PATH, `${post}.mdx`))

// 使用fs.readFileSync
// const post = fs.readFileSync(path.join(POST_PATH, `${post}.mdx`), 'utf-8')
// const {
// data: { title, description, date }
// } = matter(post)

return {
slug: post,
title,
description,
date
}
})
}

上述方法中我们拿到了所有文章标题,描述信息,日期的list,根据list渲染文章列表。


详情


文章列表中使用Link跳转到详情,通过dynamic动态加载文章对应的mdx文件


export default function LoadMDX(props: Omit<PostMetaType, 'description'>) {
const { slug, title, date } = props

const DynamicMDX = dynamic(() => import(`@/mdxs/posts/${slug}.mdx`), {
loading: () => <p>loading...</p>
})

return (
<>
<div className="mb-12">
<h1 className="mb-5 font-[600]">{title}</h1>
<time className="my-0">{date}</time>
</div>
<DynamicMDX />
</>

)
}

generateStaticParams



优化文章列表跳转详情的速度



在文章详情组件导出generateStaticParams方法,这个方法在构建时静态生成路由,而不是在请求时按需生成路由,一定程度上提高了访问详情页速度


export async function generateStaticParams() {
const posts = await fetch('https://.../posts').then((res) => res.json())

return posts.map((post) => ({
slug: post.slug
}))
}

部署


项目是部署在vercel,使用github登录后我们新建一个项目,点进去后会看到Import Git Repository,导入对应仓库即可,也可使用vercel提供的模版新建一个,后续我们每次提交代码都会自动化部署。


Alt Text

有自己域名的可以在Domains中添加,然后去到你买域名的地方添加对应DNS解析即可。


总结


开发中遇到了一些坑:



  1. next-themes报错Warning: Extra attributes from the server: class,style,通过issues和看文档,最终找到了方案

  2. mdx-components组件的命名,经多次测试发现只能命名为mdx-components,阅读@next/mdx的源码也验证了

  3. 语法高亮,开始使用的hljs,mdx中的代码块写的js,部署到线上后发现代码并没有高亮,然后改用了prism正常高亮,
    又是阅读了react-syntax-highlighter源码发现hljs的语言集合中并没有js,所以无法正确解析,只能写成javascript,而prism两者写法都支持

  4. 首页的posts命令是运行在客户端组件中,fs无法使用,因此获取文章的方案使用fetch请求api

  5. 使用remark-frontmatter解析yaml无法和mdxRs: true同时使用,否则解析失败。添加此配置项表示使用基于rust的解析器来解析mdx,可能是还未支持的缘故


module.exports = withMDX({
experimental: {
mdxRs: true
}
})

后续更新:



  1. 会新增Weekly周刊模块,关注前端技术的更新

  2. 文章详情页添加上一篇和下一篇,更方便的阅读文章


作者:赫子子
来源:juejin.cn/post/7267408057163055139
收起阅读 »

一个鼠标滑过的样式~

web
🫰 demo 🫰🧐 思路分析 🧐这样看是不是一目了然呢~ 😏如上👆gif👆效果可以理解为👉 以鼠标位置为圆心,产生的背景圆,与box的间隙产生的交叉❓ 这么实现会不会有问题呢 ❓效果只在boxes区域出现,是不是需要判断鼠标位置来添加粉色背景圆呢 ❓...
继续阅读 »


hover.gif

🫰 demo 🫰

demo.gif

🧐 思路分析 🧐

原理.gif

这样看是不是一目了然呢~ 😏

如上👆gif👆效果可以理解为👉 以鼠标位置为圆心,产生的背景圆,与box的间隙产生的交叉

❓ 这么实现会不会有问题呢 ❓

  • 效果只在boxes区域出现,是不是需要判断鼠标位置来添加粉色背景圆呢 ❓
  • 而且这个只有在接触到 box 才会有 粉色背景圆box 以外的部分是没有颜色的,这个又如何解决呢 ❓
  • 或许也能实现,应该会麻烦些 :)

🧐 不妨换个思路 🧐

给每个box添加背景圆背景圆位置 根据鼠标位置变化,👇 如下所示 👇

image.png image.png

背景圆大小固定(比如200px),圆心位置如何确定呢?

👉 初始位置 (0,0) ,参照系则是参考box左上角

👉 动态变化的位置取(clientX - left, clientY - top)left  top  box 元素相对浏览器视口的位置,通过 getBoundingClientRect 方法获取

👉 取差值(clientX - left, clientY - top)也很好理解,因为伪元素位置是参照box左上的位置变化,这样就能在 差值(绝对值) < 半径 的时候出现在 box间隙

image.png

🌟 关键点 🌟

  • 盒子元素 box 添加伪元素 before,设置伪元素宽高均大于父元素,效果上类似于伪元素覆盖了box,同时设置偏移量 inset为负值,实现 “居中覆盖”(这样就能留出一个"空隙", 即👆gif👆粉色圆填充before  box 中间空白的部分)
  • 给伪元素背景设置背景色,demo中用的是 径向渐变,渐变的形状为200px 的圆形,圆心位置记为 --x  --y,通过css变量传入,颜色自定义即可(demo中采用的是rgba(245,158,11,.7)  transparent 的渐变)不用粉色了🤣

👀 关于--x  --y 的获取 👀

  • 记录鼠标位置 (mouseX, mouseY)
  • calBoxesPosition方法获取每个box 的位置 (left,top) 并记录差值 (mouseX - left, mouseY - top)
  • (mouseX, mouseY) 变动的时候重新触发 calBoxesPosition 方法即可

🚀 关于一些优化 🚀

  • 第一次页面加载调用 calBoxesPosition 后,在不滑动页面的情况下,每个box位置相对固定,可以缓存下来位置信息,避免该函数内部频繁调用 getBoundingClientRect 引发的性能问题造成卡顿
  • 滑动页面的时候,可以将记录box位置信息的字段重置为(0,0),再移动鼠标重新触发 calBoxesPosition 即可

👨‍💻代码(vue3实现)👨‍💻

PS: 不太会使用掘金的代码片段,不知道如何引入第三方库😅,如果验证代码, @vueuse/core 和 tailwindcss请自行安装🫠

(等我查一下怎么使用,再回来贴个代码片段~ ⏰@ 4-17 14:56 )

  1. template 结构

  1. css样式

  1. js 部分


作者:一只小於菟
来源:juejin.cn/post/7358622889681551372

收起阅读 »

threejs3D汽车换肤实战

web
06-汽车动态换肤的案列 课程内容 一、环境的搭建 (1)搭建项目 threejs的每个版本都有一些差异,在api和threejs项目文件夹下面,本案列使用的版本 npm i three@0.153.0 项目的目录结构如下: 03-fulldemo └───...
继续阅读 »

06-汽车动态换肤的案列


课程内容


一、环境的搭建


(1)搭建项目

threejs的每个版本都有一些差异,在api和threejs项目文件夹下面,本案列使用的版本


npm i three@0.153.0

项目的目录结构如下:


03-fulldemo
└───css
│───main.css

└───draco
│───gltf——存放Google Draco解码器插件

└───models——存放模型
│───ferrari.glb——模型文件,可以是glb也可以是gltf格式
│───ferrari_ao.png——模型贴图,这个图片是阴影效果

└───textures——纹理材质
│───venice_sunset_1k.hdr——将其用作场景的环境映射或者用来创建基于物理的材质


(2)代码基础结构搭建

创建对应的html文件并引入相应的环境


<!DOCTYPE html>
<html lang="en">
<head>
<title>three.js webgl - materials - car</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<link type="text/css" rel="stylesheet" href="./css/main.css">
<style>
body {
color: #bbbbbb;
background: #333333;
}
a {
color: #08f;
}
.colorPicker {
display: inline-block;
margin: 0 10px
}
</style>
</head>

<body>
<!--设置三个按钮,用于切换车身、轮毂、玻璃的颜色-->
<div id="info">
<span class="colorPicker"><input id="body-color" type="color" value="#ff0000"></input><br/>Body</span>
<span class="colorPicker"><input id="details-color" type="color" value="#ffffff"></input><br/>Details</span>
<span class="colorPicker"><input id="glass-color" type="color" value="#ffffff"></input><br/>Glass</span>
</div>
<!--要渲染3D的容器-->
<div id="container"></div>
<script type="importmap">
{
"imports": {
"three": "./node_modules/three/build/three.module.js",
"three/addons/": "./node_modules/three/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
//用于显示屏幕渲染帧率的面板
import Stats from 'three/addons/libs/stats.module.js';
//相机控件OrbitControls实现旋转缩放预览效果。
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
//加载GLTF文件格式的加载器,用于加载外部为gltf的文件
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
//Draco是一个用于压缩和解压缩 3D 网格和点云的开源库
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
//RGBELoader可以将HDR图像加载到Three.js应用程序中
import { RGBELoader } from 'three/addons/loaders/RGBELoader.js';

//下面的代码就是JS渲染逻辑代码
</script>
</body>
</html>

在css/main.css文件中我们的代码如下


body {
margin: 0;
background-color: #000;
color: #fff;
font-family: Monospace;
font-size: 13px;
line-height: 24px;
overscroll-behavior: none;
}

a {
color: #ff0;
text-decoration: none;
}

a:hover {
text-decoration: underline;
}

button {
cursor: pointer;
text-transform: uppercase;
}

#info {
position: absolute;
top: 0px;
width: 100%;
padding: 10px;
box-sizing: border-box;
text-align: center;
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
pointer-events: none;
z-index: 1; /* TODO Solve this in HTML */
}

a, button, input, select {
pointer-events: auto;
}

.lil-gui {
z-index: 2 !important; /* TODO Solve this in HTML */
}

@media all and ( max-width: 640px ) {
.lil-gui.root {
right: auto;
top: auto;
max-height: 50%;
max-width: 80%;
bottom: 0;
left: 0;
}
}

#overlay {
position: absolute;
font-size: 16px;
z-index: 2;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
background: rgba(0,0,0,0.7);
}

#overlay button {
background: transparent;
border: 0;
border: 1px solid rgb(255, 255, 255);
border-radius: 4px;
color: #ffffff;
padding: 12px 18px;
text-transform: uppercase;
cursor: pointer;
}

#notSupported {
width: 50%;
margin: auto;
background-color: #f00;
margin-top: 20px;
padding: 10px;
}


效果如下图:


image-20230627175428242


二、进行3D场景的渲染


(1)进行初始化函数设计

在项目中我们添加一个carInit函数进行动画的初始化


...省略之前代码
//下面的代码就是JS渲染逻辑代码
let scene, renderer, grid, camera;
function initCar(){
//里面就开始进行3D场景的搭建
}

//执行初始化函数
initCar()

上面的函数设计用于执行我们所有3d业务代码。


(2)创建场景

/**
* (1)获取要渲染的容器
*/

const container = document.getElementById('container');

/**
* (2)创建场景对象Scene
*/

//创建一个场景对象,用来模拟3d世界
scene = new THREE.Scene();
//设置一个场景的背景颜色
scene.background = new THREE.Color(0x333333);
//这个类中的参数定义了线性雾。也就是说,雾的密度是随着距离线性增大的
scene.fog = new THREE.Fog("red", 10, 15);

background:这个属性用于设置我们场景的背景颜色,0x333333默认采用深灰来作为我们初始颜色


fog:定义了线性雾,类似于在背景指定位置设置雾化的效果,让背景看起来更加模糊,凸显空旷效果。


(3)坐标格辅助对象

/**
* (3)坐标格辅助对象. 坐标格实际上是2维线数组.
*/

//创建网格对象,参数1:大小,参数2:网格细分次数,参数3:网格中线颜色,参数4:网格线条颜色
grid = new THREE.GridHelper(40, 40, 0xffffff, 0xffffff);
//网格透明度
grid.material.opacity = 1;
grid.material.depthWrite = false;
grid.material.transparent = true;
scene.add(grid);

坐标格辅助对象GridHelper可以在3D场景中定义坐标格出现。后续我们会在坐标格上面放我们的模型进行展示


代码编写完毕后,最终渲染出来的坐标格效果如下:


image-20230628155733478


(4) 创建相机对象

/**
* (4)创建透视相机
* 参数一:摄像机视锥体垂直视野角度
* 参数二:摄像机视锥体长宽比
* 参数三:摄像机视锥体近端面
* 参数四:摄像机视锥体远端面
*/

camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 0.3, 100);
camera.position.set(0, 1.4, - 4.5);

任何一个3D渲染效果都需要相机来成像


这一投影模式被用来模拟人眼所看到的景象,它是3D场景的渲染中使用得最普遍的投影模式


透视相机最大的特点就是满足近大远小的效果。


(5)创建一个渲染器

/**
* (5)创建一个渲染器
*/

renderer = new THREE.WebGLRenderer({ antialias: true });
renderer = new THREE.WebGLRenderer({ antialias: true });
//设置设备像素比。通常用于避免HiDPI设备上绘图模糊
renderer.setPixelRatio(window.devicePixelRatio);
//设置渲染出来的画布范围
renderer.setSize(window.innerWidth, window.innerHeight);
container.appendChild(renderer.domElement);
renderer.render(scene, camera);

有了场景、相机、坐标格辅助,我们想要让画面能够呈现出来,那就得有渲染器。


相当于你拍照需要将画面呈现到交卷上面。


其中renderer.render(scene, camera); 这段代码就是在进行渲染器的渲染。


如果render在指定频率内不断被调用,那就意味着可以不断拍照,不断渲染。可以实现动态切换效果


(6)效果渲染

当执行完上面的代码后,你需要确保调用了carInit这个函数,页面就可以渲染出对应的效果了


image-20230628161312404


说明:



  1. 场景的背景色为0x333333效果为深灰色。

  2. 我们设置的fog线性雾颜色为红色,所以你会发现在背景和网格之间会有一个过渡颜色。

  3. 网格的颜色采用的是0xffffff效果为灰色。


对应的各种参数,当你在学习的时候都都可以进行调整。一遍调整就能看懂参数和最终渲染的效果差异。


当你把fog的颜色调整为跟背景一样的时候,你会发现画面上就类似产生了迷雾效果,让3D背景更加立体


scene.fog = new THREE.Fog(0x333333, 10, 15);

效果如下:


image-20230628161707934


你也可以继续设置网格线条的透明度,让网格线不那么抢眼


grid.material.opacity = 0.3;

效果如下:


image-20230628161827094


是不是整个画面看起来3D立体效果会更强一些,背景看起来更深邃一些。


三、加载外部模型进行渲染


(1)添加轨道控制器

threejs官方给我们提供了一个类,OrbitControls(轨道控制器)可以使得相机围绕目标进行轨道运动。


换句话说,引入了OrbitControls后,我们可以操作鼠标来控制页面上动态效果。


比如:鼠标滚动、鼠标点击、鼠标左右滑动效果。


代码如下:


...省略了 【(5)创建一个渲染器】
/**
* (6)开启OrbitControls控件,可以支持鼠标操作图像
*/

controls = new OrbitControls(camera, container);
//你能够将相机向外移动多少(仅适用于PerspectiveCamera),其默认值为Infinity
controls.maxDistance = 9;
//你能够垂直旋转的角度的上限,范围是0到Math.PI,其默认值为Math.PI
controls.maxPolarAngle = THREE.MathUtils.degToRad(90);
controls.target.set(0, 0.5, 0);
controls.update();

加入上面代码后,我们还要继续优化代码


在carInit函数后面在添加一个render函数,用于执行渲染


function initCar(){

/**
* (5)创建一个渲染器
*/

renderer = new THREE.WebGLRenderer({ antialias: true });
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
container.appendChild(renderer.domElement);
//注释掉这句话
//renderer.render(scene, camera);
//调用一次render函数进行渲染
render()
}
function render(){
renderer.render(scene, camera);
requestAnimationFrame(render)
}

效果实现如下:



(2)加载汽车模型

既然要加载外部模型,那我们肯定需要通过模型软件来设计对应的模型。本案列不讲解如何设计模型,我使用threejs官方提供的模型来进行展示。


我们常用的模型格式如下:



  1. OBJ (Wavefront OBJ):


    OBJ 是一种常见的纯文本模型格式,支持存储模型的几何信息(顶点、面)和材质信息(纹理坐标、法线等)。可以通过OBJLoader来加载和解析OBJ格式的模型。


  2. FBX (Autodesk FBX):


    FBX 是由Autodesk开发的一种常用的二进制模型格式,支持存储模型的几何信息、材质、动画等。可以通过FBXLoader来加载和解析FBX格式的模型。


  3. GLTF (GL Transmission Format):


    GLTF 是一种基于JSON的开放标准,用于存储和传输三维模型和场景。GLTF格式支持几何信息、材质、骨骼动画、节点层次结构等,并且通常具有较小的文件大小。可以通过GLTFLoader来加载和解析GLTF格式的模型。


  4. STL (Stereolithography):


    STL 是一种常用的三维打印文件格式,用于存储模型的几何信息。STL 文件通常包含三角形面片的列表,用于定义模型的外观。可以通过STLLoader来加载和解析STL格式的模型。


  5. GLB:


    GLB是GL Transmission Format(gltf)的二进制版本,GLB格式将模型的几何信息、材质、骨骼动画、节点层次结构等存储在单个二进制文件中,通常具有较小的文件大小和更高的加载性能.



本案列采用glb格式来加载外部模型。


因为案列中使用glb模型数据采用了Draco来进行压缩,所以我们需要引入DRACOLoader来解析我们的模型


(1)引入DRACOLoader加载模型


/**
* (7)汽车模型相关的内容
* DRACOLoader 主要用于解析使用 Draco 压缩的 GLB 模型,而不是所有的 GLB 模型都使用了 Draco 压缩
*/

const dracoLoader = new DRACOLoader();
//配置加载器的位置,这个需要提前下载到项目中
dracoLoader.setDecoderPath('./draco/gltf/');
const loader = new GLTFLoader();
//设置GLTFLoader加载器使用DRACO来解析我们的模型数据
loader.setDRACOLoader(dracoLoader);


并不是所有的模型都需要Draco来进行加载,取决于你的模型在设计导出的时候是否用了Draco来进行压缩。



./draco/gltf/目录下面的文件如下:代码可以从gitee上面下载


image-20230629142933880


(3)加载glb模型数据

当你已经创建了`const loader = new GLTFLoader();这个类实例后,我们就可以加载模型了


/**
* (8)加载glb模型
*/

loader.load('./models/gltf/ferrari.glb', function (gltf) {
//获取到模型的数据
const carModel = gltf.scene.children[0];
//将模型添加到3D场景中
scene.add(carModel);
});
render()

加载的效果如下:


image-20230629143514305


模型已经加载成功了,但是你会发现他在整个背景中是黑色的。当然模型本身是有材质贴图的,车身默认是红色的。


之所以产生这个效果那是因为我们现在缺少一个非常重要的元素,那就是光照。


你试想一下,一个物体在没有任何光源的情况下,呈现出来的就是黑色的效果。如果你的场景背景也是黑色,那根本看不到效果。


(4)加载光影效果

我们设置光源的时候主要有两个部分



  1. 环境光:相当于天空的颜色,物体表面可以反射出对应的颜色。

  2. 点光源:相当于开启手电筒,照射到模型表面反射出来的颜色。


设置环境光


/**
* (9)添加光影效果
*/


//创建环境光
var ambient = new THREE.AmbientLight("blue");
scene.add(ambient);

环境光的颜色为blue,效果如下:


image-20230629145635074


环境光为blue的情况下,模型表面反射出来的颜色就是蓝色,一般金属材质和玻璃材质反射的效果更佳明显。所以轮毂和车辆挡风玻璃效果会更强烈一些。


设置点光源


/**
* (9)添加光影效果
*/


//创建环境光
var ambient = new THREE.AmbientLight("blue");
scene.add(ambient);

//创建点光源
var point = new THREE.PointLight("#fff");
//设置点光源位置
point.position.set(0, 300, 0);
//点光源添加到场景中
scene.add(point);

效果如下:


image-20230629145913328


此刻我们基本上完成了模型的渲染,环境光蓝色默认替换为黑色,这样车辆立体感会更强一些


//环境光
var ambient = new THREE.AmbientLight("#000");

效果如下:


image-20230629150241875


(5)加载hdr文件设置环境渲染

HDR(High Dynamic Range)文件是一种存储图像高动态范围信息的文件格式。


HDR可以理解成一张真实世界的图片或者设计者想要的灯光效果。


他的作用主要如下:



  1. HDR文件经常被用作环境贴图,用于模拟反射和光照环境。环境贴图是将场景的背景、反射和光照信息包装成一个纹理,然后将其应用到物体表面上。通过使用HDR文件作为环境贴图,可以更真实地模拟光线在场景中的反射和折射,增强渲染效果。

  2. HDR文件还可以用于模拟全局照明效果。全局照明是一种渲染技术,它考虑了场景中所有光源的组合对物体的影响,以获得更真实的照明效果。通过使用HDR文件提供的高动态范围和丰富的光照信息,可以在Three.js中实现更逼真的全局照明效果


也就说在本案列中如果我们想要获取更加真实的照明效果,我们可以使用设计师导出的hdr文件。将这个文件作为3D场景(Scene)的环境贴图


/**
* (2)创建场景对象Scene
*/

scene = new THREE.Scene();
scene.background = new THREE.Color(0x333333);
//通过RGBELoader加载hdr文件,它是一种图像格式,将其用作场景的环境映射或者用来创建基于物理的材质
scene.environment = new
RGBELoader().load('textures/equirectangular/venice_sunset_1k.hdr');
scene.environment.mapping = THREE.EquirectangularReflectionMapping;
scene.fog = new THREE.Fog(0x333333, 10, 15);

删除我们(9)添加光影效果中我们自己的光影效果


/**
* (9)添加光影效果
*/


//创建环境光
//var ambient = new THREE.AmbientLight("blue");
//scene.add(ambient);

//创建点光源
//var point = new THREE.PointLight("#fff");
//设置点光源位置
//point.position.set(0, 300, 0);
//点光源添加到场景中
//scene.add(point);

这样渲染下来我们物体在场景中显示的会更加自然


image-20230629152457227



不管你用hdr文件来作为环境贴图,还是采用光源设置来设计,我们都可以让模型在3D场景中更方便的显示出来。



四、汽车材质贴图


目前我们已经将模型渲染出来了,但是你会发现不管是车身、轮毂、还是玻璃材质跟我们想要的真实车辆材质是有区别的。比如你希望玻璃透明的、反光的。车身的漆面是可以反光的。模型在设计的时候使用默认材质。我们想要进行材质的替换。


(1)在步骤8中继续优化代码

/**
* (8)加载glb模型
* 并设置不同部位的材质。
*/

//物理网格材质(MeshPhysicalMaterial)
//车漆,碳纤,被水打湿的表面的材质需要在面上再增加一个透明的
const bodyMaterial = new THREE.MeshPhysicalMaterial({
color: 0xff0000, metalness: 1.0, roughness: 0.5, clearcoat: 1.0, clearcoatRoughness: 0.03
});

//汽车轮毂的材质,采用了标准网格材质,threejs解析gltf模型,会用两种材质PBR材质去解析
const detailsMaterial = new THREE.MeshStandardMaterial({
color: 0xffffff, metalness: 1.0, roughness: 0.5
});

//汽车玻璃的材质
const glassMaterial = new THREE.MeshPhysicalMaterial({
color: 0xffffff, metalness: 0.25, roughness: 0, transmission: 1.0
});

loader.load('./models/gltf/ferrari.glb', function (gltf) {
//获取到模型的数据
const carModel = gltf.scene.children[0];
//将模型添加到3D场景中
scene.add(carModel);
});

材质创建了过后,接下来我们就可以将材质加载了到模型中了。


loader.load('./models/gltf/ferrari.glb', function (gltf) {
//获取到模型的数据
const carModel = gltf.scene.children[0];

//获取模型中指定的模块,将默认材质替换为我们自定义材质
carModel.getObjectByName('body').material = bodyMaterial;
//轮毂的材质替换
carModel.getObjectByName('rim_fl').material = detailsMaterial;
carModel.getObjectByName('rim_fr').material = detailsMaterial;
carModel.getObjectByName('rim_rr').material = detailsMaterial;
carModel.getObjectByName('rim_rl').material = detailsMaterial;
////座椅的材质
carModel.getObjectByName('trim').material = detailsMaterial;
//玻璃的材质替换
carModel.getObjectByName('glass').material = glassMaterial;

scene.add(carModel);
});

上面的代码分别是获取模型中车身区域(body),获取轮毂区域(rim_fl、rim_fr、rim_rr、rim_rl)、座椅区域(trim)、玻璃区域(glass)


将我们自己创建的材质拿去替换默认材质实现加载渲染。


效果如下:


image-20230629164136907


替换过后的模型,更有金属质感和玻璃质感。材质对应的颜色你们都可以自己进行替换。


(2)给车底盘添加阴影效果


车底盘是没有阴影效果的,我们可以使用图片来进行模型贴图,让底盘有阴影效果会更加立体。


贴图的图片为png,图片由设计师出的


效果如下:


ferrari_ao


创建一个材质对象,并使用这张图片作为贴图


loader.load('./models/gltf/ferrari.glb', function (gltf) {
//获取到模型的数据
const carModel = gltf.scene.children[0];

//获取模型中指定的模块,将默认材质替换为我们自定义材质
carModel.getObjectByName('body').material = bodyMaterial;
//轮毂的材质替换
carModel.getObjectByName('rim_fl').material = detailsMaterial;
carModel.getObjectByName('rim_fr').material = detailsMaterial;
carModel.getObjectByName('rim_rr').material = detailsMaterial;
carModel.getObjectByName('rim_rl').material = detailsMaterial;
//座椅的材质
carModel.getObjectByName('trim').material = detailsMaterial;
//玻璃的材质替换
carModel.getObjectByName('glass').material = glassMaterial;

// shadow阴影效果图片
const shadow = new THREE.TextureLoader().load( './models/gltf/ferrari_ao.png' );
// 创建一个材质模型
const mesh = new THREE.Mesh(
new THREE.PlaneGeometry(0.655 * 4, 1.3 * 4),
new THREE.MeshBasicMaterial({
map: shadow, blending: THREE.MultiplyBlending, toneMapped: false, transparent: true
})
);
mesh.rotation.x = - Math.PI / 2;
mesh.renderOrder = 2;
carModel.add(mesh);

scene.add(carModel);
});

效果如下:


image-20230629175934463


通过效果图能看出,车辆底部是有阴影效果的,让整个3D效果渲染更加立体。


五、设置动画效果


(1)获取轮毂的材质对象

轮毂和网格地板我们都要动画加载


网格需要进行平移,按照z的反方向进行移动。


轮毂需要按照x轴的方向进行旋转


代码如下:


let wheels = []
function initCar(){
loader.load('./models/gltf/ferrari.glb', function (gltf) {
//获取到模型的数据
const carModel = gltf.scene.children[0];

...省略代码
//将车轮的模块保存到数组中,后面可以设置动画效果
wheels.push(
carModel.getObjectByName('wheel_fl'),
carModel.getObjectByName('wheel_fr'),
carModel.getObjectByName('wheel_rl'),
carModel.getObjectByName('wheel_rr')
);

scene.add(carModel);
});
}

上面的代码将轮毂模块获取到过后,放入到wheels数组中。


(2)设置轮毂的动画效果

接下来在render函数中进行动画控制


function render() {
controls.update();
//performance.now()是一个用于测量代码执行时间的方法。它返回一个高精度的时间戳,表示自页面加载以来的毫秒数
const time = - performance.now() / 1000;
//控制车轮的动画效果
for (let i = 0; i < wheels.length; i++) {
wheels[i].rotation.x = time * Math.PI * 2;
}
//控制网格的z轴移动
grid.position.z = - (time) % 1;

renderer.render(scene, camera);
requestAnimationFrame(render)
}

通过上面的代码我们已经能够实现轮毂和网格的动画效果了


六、切换颜色


实现颜色切换就必须绑定js的事件。


三个按钮,我们都绑定点击事件,并获取对应的颜色


function initCar(){
...省略代码
/**
* (10)切换车身颜色
* 获取到指定的按钮,得到你选中的颜色,并将颜色设置给我们自己的模型对象
*/

const bodyColorInput = document.getElementById('body-color');
bodyColorInput.addEventListener('input', function () {
bodyMaterial.color.set(this.value);
});

const detailsColorInput = document.getElementById('details-color');
detailsColorInput.addEventListener('input', function () {
detailsMaterial.color.set(this.value);
});

const glassColorInput = document.getElementById('glass-color');
glassColorInput.addEventListener('input', function () {
glassMaterial.color.set(this.value);
});
}

当我们将上面的代码实现后,切换颜色就完成分了。


只要修改bodyMaterial材质对象的颜色,页面刷新的时候就可以应用成功。


课程小结


作者:无处安放的波澜
来源:juejin.cn/post/7277787934848204835
收起阅读 »

JavaScript简介:从概念、特点、组成和用法全面带你快速了解JavaScript!

JavaScript,简称JS,是一种轻量级的解释型编程语言,它是网页开发中不可或缺的三剑客之一,与HTML和CSS并肩作战,共同构建起我们浏览的网页。今天我们就来了解一下JavaScript,看看它在我们的web前端开发中扮演着什么样的角色。一、JavaSc...
继续阅读 »

JavaScript,简称JS,是一种轻量级的解释型编程语言,它是网页开发中不可或缺的三剑客之一,与HTML和CSS并肩作战,共同构建起我们浏览的网页。

今天我们就来了解一下JavaScript,看看它在我们的web前端开发中扮演着什么样的角色。

一、JavaScript是什么?

JavaScript(简称“JS”)是一种具有函数优先的轻量级,解释型或即时编译型的编程语言。它以其作为开发Web页面的脚本语言而闻名,但也被广泛应用于非浏览器环境中。

JavaScript是一种基于原型编程、多范式的动态脚本语言,支持面向对象、命令式、声明式和函数式编程范式。


Description


JavaScript最初由Netscape公司的Brendan Eich于1995年为网景导航者浏览器设计并实现。由于Netscape与Sun的合作,Netscape管理层希望该语言在外观上看起来像Java,因此得名为JavaScript。

JavaScript的标准是ECMAScript。截至2012年,所有浏览器都完整地支持ECMAScript 5.1,旧版本的浏览器至少支持ECMAScript 3标准。

2015年6月17日,ECMA国际组织发布了ECMAScript的第六版,正式名称为ECMAScript 2015,但通常被称为ECMAScript 6或ES2015。


Description


JavaScript目前是互联网上最流行的脚本语言。这门语言不仅可用于HTML和Web开发,还可以广泛用于服务器、PC、笔记本电脑、平板电脑和智能手机等设备。

二、JavaScript能做什么?

动画效果:

让你的网页动起来,比如轮播图、下拉菜单等。


Description

表单验证:

在数据提交到服务器之前,进行即时的客户端验证。

异步请求:

通过AJAX技术,实现页面的局部更新,无需刷新整个页面。

交互式游戏:

创建复杂的网页游戏,或是简单的互动元素。

Web API:

利用浏览器提供的API,访问地理位置、摄像头、本地存储等。


Description

跨平台应用:

使用如React Native、Electron等框架,开发跨平台的移动应用和桌面应用。

后端开发:

Node.js的出现让JavaScript也能在服务器端大展拳脚。

三、JavaScript的组成

  • ECMAScript,描述了该语言的语法和基本对象。

  • 文档对象模型(DOM),描述处理网页内容的方法和接口。

  • 浏览器对象模型(BOM),描述与浏览器进行交互的方法和接口

Description


3.1 ECMAScript:

ECMAScript是一种由Ecma国际(前身为欧洲计算机制造商协会)在标准ECMA-262中定义的脚本语言规范。这种语言在万维网上应用广泛,它往往被称为JavaScript或JScript,但实际上后两者是ECMA-262标准的实现和扩展。

简单来说:

  • ECMAScript JavaScript是的核心,是规范标准。

  • 描述了语言的基本语法(var、for、if、array等)和数据类型(数字、字符串、布尔、函数、对象(obj、[]、{}、null)、未定义)。

3.2 DOM

文档对象模型 (DOM) 是HTML和XML文档的编程接口。它提供了对文档的结构化的表述,并定义了一种方式可以使从程序中对该结构进行访问,从而改变文档的结构,样式和内容。

DOM 将文档解析为一个由节点和对象(包含属性和方法的对象)组成的结构集合。

简言之,它会将web页面和脚本或程序语言连接起来。

可以理解为:

DOM Document Object Model文档对象模型,可以去操作网页。

Document(文档)

指的是XML和HTML的页面,当你创建一个页面并且加载到Web浏览器中,DOM就在幕后悄然而生,它会把你编写的网页文档转换成一个文档对象。

Object(对象)

js对象大致可以分为以下三种:

  • 用户定义对象,例如:var obj = {}

  • 内置对象,无需创建,可直接使用,例如:Array、Math和Data等

  • 宿主对象,浏览器提供的对象,例如:window、document

DOM中主要关注的就是document,document对象的主要功能就是处理网页内容。

Model(模型)

代表着加载到浏览器窗口的当前网页,可以利用JavaScript对它进行读取。

3.3 BOM

浏览器对象模型,操作浏览器。

Browser Object Model 浏览器对象模型提供了独立与内容的、可以与浏览器窗口进行互动的对象结构,BOM由多个对象构成,其中代表浏览器窗口的window对象是BOM的顶层对象,其他对象都是该对象的子对象。

四、JavaScript的特点

JavaScript是一种功能强大的编程语言,它的特点主要包括以下几点,在这里大家只需要了解一下就可以了。

  • 客户端脚本语言: JavaScript通常在用户的浏览器上运行,用于实现动态内容和用户界面的交互性。

  • 弱类型语言: JavaScript不要求开发者在编程时明确指定变量的类型,类型会在运行时自动转换。

  • 面向对象: JavaScript支持面向对象的编程模式,允许创建对象和定义它们之间的交互。

  • 事件驱动: JavaScript能够响应用户的操作(如点击、输入等),这使得它非常适合构建交互式的Web应用。

  • 跨平台: JavaScript代码可以在几乎所有的现代浏览器上运行,无论是Windows、macOS还是Linux操作系统。

  • 动态性: JavaScript是一种动态语言,可以在运行时改变其结构和行为。

  • 可扩展性: JavaScript可以通过添加新的函数和属性来扩展其内置对象的功能。

  • 宽松语法: JavaScript的语法相对宽松,使得编程更加灵活,但也可能导致错误。

  • 单线程与异步处理: JavaScript在浏览器中是单线程执行的,但它通过事件循环和回调函数等机制实现了异步处理。

  • 基于原型的继承: 不同于传统的类继承,JavaScript使用的是基于原型的继承方式。

  • 核心组成部分: JavaScript的核心由ECMAScript、DOM(文档对象模型)和BOM(浏览器对象模型)组成。

  • 多范式: JavaScript支持多种编程范式,包括过程式、面向对象和函数式编程。

总的来说,JavaScript的这些特点使其成为了Web开发中不可或缺的一部分,同时也适用于服务端编程(如Node.js)和其他非浏览器环境。


你是不是厌倦了一成不变的编程模式?想要突破自我,挑战新技术想要突破自我,挑战新技术?却迟迟找不到可以练手的项目实战?是不是梦想打造一个属于自己的支付系统?那么,恭喜你,云端源想免费实战直播——《VUE3+SpringBoot搭建移动支付功能(第1期)》即将开启,点击前往获取源码!

五、JavaScript的用法

1、页内样式:

在HTML文件中,可以在<head>或<body>标签中添加<script>标签,然后在<script>标签中编写JavaScript代码。这种方式适合较小的脚本或者是测试阶段的代码。

例如:

<!DOCTYPE html>
<html>
<head>
<title>页内样式示例</title>
<script>
// 在这里编写JavaScript代码
</script>
</head>
<body>
// 页面内容
</body>
</html>

2、页外样式:

步骤一:在js文件夹中创建一个Xxx.js文件。

步骤二:在Xxxx.js文件中编写JavaScript代码。

步骤三:在HTML文件的<head>标签中通过<script src="Xxxx.js"></script>进行引入。
例如:

<!DOCTYPE html>
<html>
<head>
<title>页外样式示例</title>
<script src="Xxxx.js"></script>
</head>
<body>
// 页面内容
</body>
</html>

需要注意的是,引入时路径要正确,如果是当前目录则直接写文件名,如果是上级目录则需要使用./来指定路径。

  • 页外样式写到<head>中,可以让它早点加载、早点完成。

  • 而页内样式写到<body>结束标签之前,可以让HTML代码先渲染内容,然后再执行JavaScript代码。

随着Web技术的发展,JavaScript也在不断进化。ES6引入了类、模块、箭头函数等新特性,未来的JavaScri
pt将更加强大、简洁。作为前端开发的基石,也是全栈开发的重要工具,JavaScript的重要性不言而喻。

现在,你是否已经迫不及待想要开启自己的JavaScript学习之旅了呢?记住,每一位大师都是从基础开始的,不要害怕犯错,因为每一个错误都是通往成功的阶梯。

拿起你的键盘,打开你的浏览器,让我们一起在JavaScript的海洋中遨游,发现编程的无穷魅力吧!

收起阅读 »

移动端安全区域适配方案

web
前言 什么是安全区域? 自从苹果推出了惊艳的iPhone X,智能手机界就正式步入了全面屏的新纪元。然而,这一革新也带来了一个特别的问题——那就是屏幕顶部的“刘海”和底部的“黑条”区域。这些区域犹如手机的“神秘面纱”,遮挡了一部分屏幕,给开发者带来了新的挑战。...
继续阅读 »

前言


什么是安全区域?


自从苹果推出了惊艳的iPhone X,智能手机界就正式步入了全面屏的新纪元。然而,这一革新也带来了一个特别的问题——那就是屏幕顶部的“刘海”和底部的“黑条”区域。这些区域犹如手机的“神秘面纱”,遮挡了一部分屏幕,给开发者带来了新的挑战。


Android似乎对iPhone的设计情有独钟,纷纷效仿这种全面屏的潮流。于是,越来越多的Android手机也开始有了这个安全区域的概念。


在这个背景下,移动端安全区域适配变得尤为重要。开发者们需要巧妙地调整应用的布局和界面,确保内容不会被这些特殊区域遮挡,同时保持应用的美观和易用性。


安全区域(safe area)



安全区域定义为视图中未被导航栏、选项卡栏、工具栏或视图控制器可能提供的其他视图覆盖的区域。



ios1.png


如上图所示,安全区域为中间蓝色部分,也就是说我们在页面布局时应该保证页面内容在蓝色安全区域内。


所以对于这类机型,你如果不特殊处理,那么它将会是这样的:


ios2.png

这样就会导致底部输入框的交互受影响


网页布局方式(viewport-fit)


在处理安全区域之前,我们需要先来了解viewport-fit属性,这是解决问题的关键。


iOS带来问题的同时也带来了解决问题的方法,为了适配 iPhoneX等全面屏机型 对现有 viewport meta 标签进行了扩展,用于设置视觉视口的大小来控制裁剪区域。


用法


<meta name="viewport" content="width=device-width,initial-scale=1, user-scalable=0, viewport-fit=cover">

属性值


该属性包含三个值:



  • auto:该值不会影响初始布局视口,并且整个网页都是可见的。 UA 在视口之外绘制的内容是未定义的。它可以是画布的背景颜色,或者 UA 认为合适的任何其他颜色。(默认值,与contain表现一致)

  • contain:初始布局视口和视觉视口设置为设备显示屏中内接的最大矩形。 UA 在视口之外绘制的内容是未定义的。它可以是画布的背景颜色,或者 UA 认为合适的任何其他颜色。

  • cover:初始布局视口和视觉视口设置为设备物理屏幕的外接矩形。


区别


在非矩形显示器上(比如手表)设置视口边界框的大小时,我们必须考虑以下因素:



  • 由于视口边界框的面积大于显示器的面积而导致的剪切区域

  • 视口边界框与显示区域之间的间隙


contain


ios3.png

当使用viewport-fit: contain时,初始视口将应用于显示器的最大内接矩形。


cover


ios4.png

当使用viewport-fit: cover时,初始视口将应用于显示器的外接矩形。


env


为了解决安全区域问题,iOS 11 新增了一个新的 CSS 函数env()和四个预定义的环境变量



  • safe-area-inset-left:安全区域距离左边边界距离

  • safe-area-inset-right:安全区域距离右边边界距离

  • safe-area-inset-top:安全区域距离顶部边界距离

  • safe-area-inset-bottom:安全区域距离底部边界距离



iOS 11 中提供的 env() 函数名为 constant()。从 Safari 技术预览版 41 和 iOS 11.2 beta 开始,constant() 已被删除并替换为 env()。如有必要,您可以使用 CSS 后备机制来支持这两个版本,但以后应该更喜欢使用 env()。 —— 来自webkit文档



上面的意思是从iOS12开始不再支持使用constant函数,所以为了兼容处理,我们应该这样写:


body {
padding-bottom: constant(safe-area-inset-bottom); /* 兼容 iOS < 11.2 */
padding-bottom: env(safe-area-inset-bottom); /* 兼容 iOS >= 11.2 */
}

使用该函数的前提是必须设置meta标签viewport-fit=cover ,并且对于不支持 env() 的浏览器,浏览器将会忽略它。


适配安全区域


第一步:


修改页面布局方式


<meta name="viewport" content="width=device-width,initial-scale=1, user-scalable=0, viewport-fit=cover">

第二步:


底部适配


.keyboard_foot {
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
}

ios5.png

这样安全区域问题就解决了!


作者:前端南玖
来源:juejin.cn/post/7357888522333225012
收起阅读 »

一边开飞机、一边修飞机,Node 官网的重新设计

web
《前端暴走团》,喜欢请抱走~大家好,我是团长林语冰。 当当当当~惊不惊喜,意不意外,相信大家已经注意到 Node 官网的偷偷变帅了! Node 最近可谓意气风发,不仅重新设计了新官网,还有新设计的吉祥物助阵。 今天,让我们一起来深度学习 Node 官方博客,...
继续阅读 »

《前端暴走团》,喜欢请抱走~大家好,我是团长林语冰。


00-wall.png


当当当当~惊不惊喜,意不意外,相信大家已经注意到 Node 官网的偷偷变帅了!


Node 最近可谓意气风发,不仅重新设计了新官网,还有新设计的吉祥物助阵。


今天,让我们一起来深度学习 Node 官方博客,携手 Node 团队一起回顾重新设计官网的这段旅程。


00-wall.png



免责声明


本文属于是语冰的直男翻译了属于是,略有删改,仅供粉丝参考。英文原味版请传送 Diving int0 the Node.js Website Redesign



规模和限制


Node 官方网站诞生已经超过 14 岁了。下载和文档主页的设计首次在 2011 年底崭露头角。这是 Node 0.6 的陈年旧事。


01-home.png


从那时起,Node 官网的规模随着项目需要与日俱增,包含了 1600 多页。在巅峰时期,它拥有大约 20 种国际化语言。Node 的域名(nodejs.org)每月处理 30 亿个请求,传输的数据量为 2 千兆字节。


错误的尝试


Node 官网首次尝试重新设计于 2019 年开始。工作从新的域名(nodejs.dev)和新存储库起步。蓦然回首,这可能从一开始就冥冥之中注定了该项目的失败。


简而言之,这个代码库不是社区或贡献者的常驻之地,也不存在已建立的贡献者工作流程。为生活奔走忙碌的人们自愿贡奉献自己的时间,但并不想学习第二套工具。该项目无法维持蒸蒸日上所需的领导力。


一边开飞机,一边修飞机


2022 年,团队回归现有的存储库,考虑如何重建站点。Node 的旧版代码库开始在各个维度上显示出它的老龄化。Node 旧版官网的设计已经 out 了。Node 旧版网站的内部结构很难扩展,而且文档也很少。


Node 团队仔细考虑了技术堆栈。正在进行的重新设计的第一阶段涉及 nextra,这是一个优秀的 Next 静态站点生成器。但随着网站的发展,我们发现自己经常“打破” nextra 的惯例,依赖于 nextra 抽象的底层 Next 模式和强大工具。


Next 是一个自然选择的进化过程,以其灵活性和强大功能而赫赫有名。举个栗子,Node 新网站仍然是为了终端用户速度和基础托管独立性而静态构建的,但利用 Next 的增量静态重新生成,来获取版本发布等动态内容。


我们与 Vercel 强强联手。当 Node 新官网的规模在静态导出时使 webpack 的内存管理紧张时,它们提供了直接支持。我们在公开发布之前对新版本进行了 Beta 测试,这是该框架的真实压力测试。


2023 年 4 月,我们进行了一次小型切换。拉取请求有 1600 个文件,将 GitHub UI 推向了渲染能力的极限。Node 新官网的基建会发生变化,但外观、内容和创作体验将保持不变。


这是一个重要的里程碑 —— 证明我们可以一边开飞机、一边修飞机。


重新设计


OpenJS 基金会慷慨解囊,全力资助 Node 团队与设计师一起进行重新设计。


设计师为 Node 新官网带来了现代化设计,其中包括用户体验流程、暗/亮模式、页面布局、移动视口注意事项和组件细分。


2-design.png


接下来是将设计实现为代码,重点放在基础设计元素和结构化组件层次结构的顺序构建上。我们从第一天起就构建了组件的变体,并从一开始就考虑了国际化。我们选择使用 Tailwind CSS,但重点是设计令牌和应用 CSS。


Orama 搜索将网站的所有内容让用户触手可及。它们对我们的静态内容进行索引,并以闪电般的速度提供 API 内容、学习材料、博客文章等结果。很难想象如果没有这个强大的搜索功能,Node 爱好者该如何方便的查阅文档。


Node 旧版官网已经国际化为近 20 种语言。虽然但是,一系列不幸的事件导致我们重置了所有翻译。


我们利用 Sentry 提供错误报告、监控和诊断工具。这对于识别问题和为我们的用户提供更好的体验大有助益。


Vercel 和 Cloudflare 支持可确保网站快速可靠。我们还通过 GitHub Actions 投资了 CI/CD 管道,为贡献者提供实时反馈。这包括使用 Chromatic、Lighthouse 结果进行视觉回归测试,确保网站质量保持较高水平。


03-ci.png


庆典开源日和黑客啤酒节


重新设计工作与 2023 年 9 月的庆典开源日以及下个月的黑客啤酒节不谋而合。我们通过将“良好的第一个 issue”作为离散的开发任务来为这些事件做好准备。就庆典开源日而言,我们还提供了现场指导,以便与会者能够以落地公关结束这一天。


仅在庆典开源日期间,就有 28 位作者提出了 40 个 PR(拉取请求)。黑客啤酒节又收到了 26 个 PR。


04-pr.png


文档


开源项目的好坏取决于它的文档。在此过程中,我们迭代或引入了:



  • 合作者指南

  • 贡献

  • README(自述文件)

  • 翻译

  • ......


新代码非常注重内联代码和配置注释、关注点分离,以及明确定义的常量。整个过程中使用 TS 可以辅助贡献者理解数据的形状和函数的预期行为。


未来规划


本次重新设计为 Node 官网的新时代奠定了基础。但工作还有待完成:



  • 将网站重新设计扩展到 API 文档。它们位于单独的代码库中,但计划将此处开发的样式移植到 API。

  • 探索网站和 API 文档的 monorepo(多库开发)。这应该可以改善重要的耦合,并减少管理两个独立代码库的开销。

  • 重新调整国际化努力。先前的翻译无法延续。我们的重量级 Markdown/MDX 方案提出了一个独特的挑战,我们正在与 Crowdin 合作解决。

  • 持续改进 CI/CD 流程。


致谢


许多人和组织为实现重新设计做出了大大小小的贡献。我们要感谢:



  • 首先也是最重要的是所有使这个项目成为可能的贡献者和合作者。

  • Chromatic 提供视觉测试平台,辅助我们审查 UI 更改,并捕获视觉回归。

  • Cloudflare 用于提供为 Node 网站、Node 的 CDN 等提供服务的基建。

  • Crowdin 提供了一个平台,使我们能够国际化 Node 官网并与译者合作。

  • Orama 提供了一个搜索平台,可以为我们的内容建立索引,并提供闪电般快速的结果。

  • Sentry 为其错误报告、监控和诊断工具提供开源许可证。

  • Vercel 提供为 Node 网站提供服务和支持的基建

  • 最后,感谢 OpenJS 基金会的支持和指导。


本期话题是 —— 你觉得 Node 的新官网颜值如何、体验如何?欢迎在本文下方自由言论,文明共享。


坚持阅读,自律打卡,每天一次,进步一点。


作者:前端暴走团
来源:juejin.cn/post/7357151301220335653
收起阅读 »

10分钟带你用RecyclerView+PagerSnapHelper实现一个等级指示器

web
老规矩:先上最终效果图 做前端的同学在平常的工作中,很多时候都会接触到各种指示器,这次我就接到一个等级指示器的需求: RecyclerView横向滚动,item之间有分割线,中间的item会被放大,边上的item会有一定的透明度进行淡化,滚动时要将等级变更回调...
继续阅读 »

老规矩:先上最终效果图


做前端的同学在平常的工作中,很多时候都会接触到各种指示器,这次我就接到一个等级指示器的需求:

RecyclerView横向滚动,item之间有分割线,中间的item会被放大,边上的item会有一定的透明度进行淡化,滚动时要将等级变更回调给外界进行处理,停止滚动后被选中的item需要居中。


效果图如下:


1.gif


实现流程



  1. 创建一个LevelRecyclerView继承RecyclerView,在内部init方法设置它的layoutManager,在外部提供数据源与adapter,然后最简单的RecyclerView就展示出来了

  2. 给每个item添加分割线

  3. 这时候RecyclerView可以随意滚动,给它添加一个PagerSnapHelper,让RecyclerView停下来时,item可以自动居中

  4. 这时又发现首尾的item,由于滚不到RecyclerView的中间,无法被选中,于是调整首尾item的分割线长度,使它们可以滚动到RecyclerView的中间

  5. 给RecyclerView注册滚动监听,在滚动过程中动态修改item的缩放与透明度,并将当前选中等级回调出去

  6. 重写smoothScrollToPosition,方法原实现是将某个item可见,但现在的需求是居中选中,所以要重写


初始化基本的RecyclerView


在LevelRecyclerView初始化时设置一个横向的LinearLayoutManager


    init {
layoutManager = LinearLayoutManager(context, HORIZONTAL, false)
}

设置基本的数据源与adapter


    private fun initRecycler() {
val list = mutableListOf<Int>()
list.add(R.drawable.icon_vip_level_0)
list.add(R.drawable.icon_vip_level_1)
list.add(R.drawable.icon_vip_level_2)
list.add(R.drawable.icon_vip_level_3)
list.add(R.drawable.icon_vip_level_4)
list.add(R.drawable.icon_vip_level_5)
list.add(R.drawable.icon_vip_level_6)
list.add(R.drawable.icon_vip_level_7)
list.add(R.drawable.icon_vip_level_8)
list.add(R.drawable.icon_vip_level_9)
list.add(R.drawable.icon_vip_level_10)

rv_level.adapter = object : CommonAdapter<Int>(this, R.layout.level_item, list) {
override fun convert(holder: ViewHolder, t: Int, position: Int) {
holder.setImageResource(R.id.iv_image, t)
holder.setOnClickListener(R.id.iv_image) {
rv_level.smoothScrollToPosition(position)
}
}
}
}

效果图:



给每个item添加分割线


创建一个LevelDividerItemDecoration类继承ItemDecoration,构造参数需要传入分割线的水平长度与高度,分割线的颜色为可选参数,重写getItemOffsets与onDraw方法,熟悉ItemDecoration的同学可能会觉得onDraw方法有点眼熟,因为我这个onDraw是在DividerItemDecoration上修改的


class LevelDividerItemDecoration @JvmOverloads constructor(
private val itemDividerHorizontalMargin : Int,
private val dividerHeight : Int,
dividerColor : Int = Color.parseColor("#3A3A3C")
) : ItemDecoration() {

//分割线Drawable
private val mDivider = ColorDrawable(dividerColor)
//分割线绘制区域
private val mBounds = Rect()

/**
* 计算item的分割线需要的尺寸,就是一个偏移量,可简单看成外边距
*/

override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
//上下不需要分割线设置为0,左右则是将构造时传入的itemDividerHorizontalMargin设置进去
outRect.set(itemDividerHorizontalMargin, 0, itemDividerHorizontalMargin, 0)
}

/**
* 绘制分割线
*/

override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
super.onDraw(canvas, parent, state)

canvas.save()
val top = (parent.height - dividerHeight) / 2
val bottom = top + dividerHeight
if (parent.clipToPadding) {
canvas.clipRect(parent.paddingLeft, top, parent.width - parent.paddingRight, bottom)
}

val childCount = parent.childCount
for (i in 0 until childCount) {
val item = parent.getChildAt(i)
//获取item的Rect,包含它的外边距(包含上面设置进去的偏移量)
parent.layoutManager!!.getDecoratedBoundsWithMargins(item, mBounds)

//左边分割线
mDivider.setBounds(mBounds.left, top, mBounds.left + itemDividerHorizontalMargin, bottom)
mDivider.draw(canvas)

//右边分割线
mDivider.setBounds(mBounds.right - itemDividerHorizontalMargin, top, mBounds.right, bottom)
mDivider.draw(canvas)
}
canvas.restore()
}
}

在init中添加到LevelRecyclerView


addItemDecoration(LevelDividerItemDecoration(
UIUtil.dip2px(context, 16.0),
UIUtil.dip2px(context, 4.0)))

效果图:


2.gif


这时候RecyclerView可以随意滚动,给它添加一个PagerSnapHelper,让RecyclerView停下来时,item可以自动居中


添加一个PagerSnapHelper


在内部添加一个PagerSnapHelper,并在init时设置依附于LevelRecyclerView


private val mSnapHelper = PagerSnapHelper()
init {
mSnapHelper.attachToRecyclerView(this)
layoutManager = mLayoutManager
}

效果图:


3.gif


这时又发现首尾的item,由于滚不到RecyclerView的中间,无法被选中,于是优化LevelDividerItemDecoration的计算与绘制,调整首尾item的分割线长度,使它们可以滚动到RecyclerView的中间


优化LevelDividerItemDecoration的计算与绘制


class LevelDividerItemDecoration @JvmOverloads constructor(
private val itemDividerHorizontalMargin : Int,
private val dividerHeight : Int,
dividerColor : Int = Color.parseColor("#3A3A3C")
) : ItemDecoration() {

//分割线Drawable
private val mDivider = ColorDrawable(dividerColor)
//分割线绘制区域
private val mBounds = Rect()

/**
* 计算item的分割线需要的尺寸,就是一个偏移量,可简单看成外边距
*/

override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
val parentWidth = parent.measuredWidth
val itemWidth = view.layoutParams.width
val lastPosition = parent.adapter?.itemCount?.minus(1) ?: 0
//针对首尾两个item计算它们的左右边距,用parentWidth - itemWidth再除2,可以使item刚好到达RecyclerView的中间
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.set(((parentWidth - itemWidth) * 0.5).toInt(), 0, itemDividerHorizontalMargin, 0)
}
lastPosition -> {
outRect.set(itemDividerHorizontalMargin, 0, ((parentWidth - itemWidth) * 0.5).toInt(), 0)
}
else -> outRect.set(itemDividerHorizontalMargin, 0, itemDividerHorizontalMargin, 0)
}
}

/**
* 绘制分割线
*/

override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
super.onDraw(canvas, parent, state)

canvas.save()
val top = (parent.height - dividerHeight) / 2
val bottom = top + dividerHeight
if (parent.clipToPadding) {
canvas.clipRect(parent.paddingLeft, top, parent.width - parent.paddingRight, bottom)
}

//RecyclerView宽度
val parentWidth = parent.measuredWidth
val childCount = parent.childCount
for (i in 0 until childCount) {
val item = parent.getChildAt(i)
//item宽度
val itemWidth = item.measuredWidth
//获取item的Rect,包含它的外边距(包含上面设置进去的偏移量)
parent.layoutManager!!.getDecoratedBoundsWithMargins(item, mBounds)

//左边分割线
if (i == 0 && mBounds.width() > itemWidth + itemDividerHorizontalMargin * 2) {
mDivider.setBounds(0, top, mBounds.right - itemWidth - itemDividerHorizontalMargin, bottom)
} else {
mDivider.setBounds(mBounds.left, top, mBounds.left + itemDividerHorizontalMargin, bottom)
}
mDivider.draw(canvas)

//右边分割线
if (i == childCount - 1 && mBounds.width() > itemWidth + itemDividerHorizontalMargin * 2) {
mDivider.setBounds(mBounds.left + itemWidth + itemDividerHorizontalMargin, top, parentWidth, bottom)
} else {
mDivider.setBounds(mBounds.right - itemDividerHorizontalMargin, top, mBounds.right, bottom)
}
mDivider.draw(canvas)
}
canvas.restore()
}
}

效果图:


4.gif


给RecyclerView注册滚动监听,在滚动过程中动态修改item的缩放与透明度,并将当前选中等级回调出去


定义一个等级回调接口


interface OnLevelChangeListener {
fun onLevelChange(position : Int)
}

添加OnScrollListener,在滚动过程中做了一些计算,每个方法都写了注释,具体看下面代码↓


addOnScrollListener(object : OnScrollListener() {
//系数最大值
private val maxFactor = .45F

/**
* RecyclerView滚动
*/

override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val first = mLayoutManager.findFirstVisibleItemPosition()
val last = mLayoutManager.findLastVisibleItemPosition()
val parentCenter = recyclerView.width / 2F
for (i in first..last) {
setItemTransform(i, parentCenter)
}
changeSnapView()
}

/**
* RecyclerView滚动状态改变
*/

override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
if (newState == SCROLL_STATE_IDLE) {
changeSnapView()
}
}

/**
* 对item进行各种变换
* 目前是缩放与透明度变换
*/

private fun setItemTransform(position : Int, parentCenter : Float) {
mLayoutManager.findViewByPosition(position)?.run {
val factor = calculationViewFactor(left.toFloat(), width.toFloat(), parentCenter)
val scale = 1 + factor
scaleX = scale
scaleY = scale
alpha = 1 - maxFactor + factor
}
}

/**
* 计算当前item的缩放与透明度系数
* item的中心离recyclerView的中心越远,系数越小(负相关)
*/

private fun calculationViewFactor(left: Float, width : Float, parentCenter : Float) : Float {
val viewCenter = left + width / 2
val distance = abs(viewCenter - parentCenter) / width
return max(0F, (1F - distance) * maxFactor)
}

/**
* 修改当前居中的item,把当前等级回调给外界
*/

private fun changeSnapView() {
mSnapHelper.findSnapView(mLayoutManager)?.let {
mLayoutManager.getPosition(it).let { position ->
if (lastPosition != position) {
lastPosition = position
levelListener?.onLevelChange(position)
}
}
}
}
})

给LevelRecyclerView设置等级回调监听


rv_level.levelListener = object : LevelRecyclerView.OnLevelChangeListener {
override fun onLevelChange(position: Int) {
Log.e("levelListener","levelListener $position")
tv_level.text = "等级:$position"
}
}

效果图:
1.gif


重写smoothScrollToPosition,方法原实现是将某个item可见,但现在的需求是居中选中,所以要重写


方法的原实现其实就是LinearLayoutManager内部创建了一个LinearSmoothScroller去进行滚动,现在我们创建一个CenterSmoothScroller类去继承LinearSmoothScroller,重写它的calculateDtToFit方法,calculateDtToFit用于计算滚动距离,而calculateSpeedPerPixel计算滚动速度


class CenterSmoothScroller(context: Context?) : LinearSmoothScroller(context) {

override fun calculateDtToFit(viewStart: Int, viewEnd: Int, boxStart: Int,
boxEnd: Int, snapPreference: Int)
: Int {
return boxStart + (boxEnd - boxStart) / 2 - (viewStart + (viewEnd - viewStart) / 2)
}

override fun calculateSpeedPerPixel(displayMetrics: DisplayMetrics?): Float {
return super.calculateSpeedPerPixel(displayMetrics) * 3F
}
}

override fun smoothScrollToPosition(position : Int) {
if (position == lastPosition) return
if (position < 0 || position >= (adapter?.itemCount ?: 0)) return

mLayoutManager.startSmoothScroll(
CenterSmoothScroller(context).apply {
targetPosition = position
}
)
}

到这里就完成了对整个LevelRecyclerView的开发了,实现了文章开头的动画效果
1.gif


总结


10分钟过去了,这个简单的LevelRecyclerView你拿下没有?

觉得不错的话,就不要吝啬你的点赞!

需要整份代码的话,下面链接自提。

代码链接 : github.MyCustomView


作者:小白白猪
来源:juejin.cn/post/7291474028744278016
收起阅读 »