注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

环信FAQ

环信FAQ

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

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
收起阅读 »

如何丝滑的实现首页看板拖拉拽功能?

web
需求简介 最近接了一个需求,需要实现不同登录人员可以自定义首页模块卡片。简单来说,就是实现首页看板模块的增添与拖拉拽,效果如下: 技术选型 原生js是支持拖拉拽的,只需要将拖拽的元素的 draggable 属性设置成 "true"即可,然后就是调用相应的函数...
继续阅读 »

需求简介


最近接了一个需求,需要实现不同登录人员可以自定义首页模块卡片。简单来说,就是实现首页看板模块的增添拖拉拽,效果如下:



技术选型


原生js是支持拖拉拽的,只需要将拖拽的元素的 draggable 属性设置成 "true"即可,然后就是调用相应的函数即可。


拖拽操作 - Web API 接口参考 | MDN


但是,原生js功能不够完善,使用起来需要改造的地方很多,因此,选用成熟的第三方插件比较好。


我们的主项目采用的是vue3,,经过一系列对比,最终选择了 vue-draggable-next这个插件。


vue-draggable-next


vue-draggable-next的周下载量月3万左右,可以看出是一个比较靠谱的插件。



它的使用方式npmj上也介绍的很详细:


vue-draggable-next


如果英文的使用Api看起来比较难受,网上还有中文的使用文档:


vue.draggable.next 中文文档 - itxst.com


这个插件也有vue2版本和纯js版本,其他框架也是也是可以完美使用的。


实现思路


需求与技术简析


根据我们的需求,我们应该实现的是分组拖拽,假设我们有三列,那我们要实现的就是这A、B、C三列数据相互拖拽。



我们看看中文官网给的示例:


vue.draggable.next group 例子


看起来很容易,我们只需要写多个draggable标签,每个draggable标签写入相同的组名即可。


实现方案


框架实现


回到代码中,要想实现一个三列可拖拉拽的模块列表,我们首先需要引入组件


<script lang="ts" setup>
import { VueDraggableNext } from 'vue-draggable-next'
// ....
</script>

然后定义一个数组储存数据:


<script lang="ts" setup>
import { VueDraggableNext } from 'vue-draggable-next'
const moduleList = ref([
{
"columnIndex": 1,
"moduleDetail": [
{ "moduleCode": "deviation", "moduleName": "控制失调空间",},
{ "moduleCode": "meeting_pending", "moduleName": "会议待办",},
{ "moduleCode": "abnormal_events", "moduleName": "异常事件", },
{ "moduleCode": "audit_matters", "moduleName": "事项审批",}
],
},
{
"columnIndex": 2,
"moduleDetail": [
{ "moduleCode": "air_conditioning_terminal", "moduleName": "空调末端", }
],
},
{
"columnIndex": 3,
"moduleDetail": [
{ "moduleCode": "run_broadcast", "moduleName": "运行播报",},
{"moduleCode": "my_schedule", "moduleName": "我的日程", },
{ "moduleCode": "cold_station", "moduleName": "冷站",}
],
}
])
</script>

最后,在代码中我们使用v-for循环渲染即可


<div v-for="moduleColumn in  moduleList " :key="moduleColumn.columnIndex" class="box">
<VueDraggableNext :list="moduleColumn.moduleDetail" group="column" >
<div v-for="(item, index) in moduleColumn.moduleDetail " :key="item.moduleCode" class="drag-item">
<!-- 模块内容 -->
</div>
</VueDraggableNext>

</div>

注意上面的html结构,我们循环渲染了三列VueDraggableNext标签,每个VueDraggableNext标签内部又通过v-for="(item, index) in moduleColumn.moduleDetail渲染了这个拖拽列内部的所有模块。我们通过group="column" 让每个VueDraggableNext组件的组名相同,实现了三个拖拽标签之间的模块互相拖拉拽。


拖拽点设置


正常情况小,我们肯定是希望在某个组件的固定位置才能拖动组件,因此我们需要使用到拖拽组件的handle属性。


vue.draggable.next属性说明:


handle:handle=".mover" 只有当鼠标在class为mover类的元素上才能触发拖到事件

根据属性说明,我们的代码实现起来也非常容易了。


  <div v-for="moduleColumn in  moduleList " :key="moduleColumn.columnIndex" class="box">
<VueDraggableNext :list="moduleColumn.moduleDetail" handle=".move" group="column">
<div v-for="(item, index) in moduleColumn.moduleDetail " :key="item.moduleCode" class="drag-item">
<div class="move">
拖拽区域
</div>
<!-- 模块内容 -->
</div>
</VueDraggableNext>

</div>

数据的增删改


实际开发中,我么一定会根据接口或者操作动态的更改列表,代码层也就是更改moduleList的值。非常幸运的是,如果你按照上面的方式写代码,当你拖拉拽完毕后,上面的moduleList值会自动更改,我们不用做任何处理!!!这么看,数据的增删改根本不是问题。


如何动态渲染组件


实际开发中,我们可能会遇到一个问题,就是如何动态的去渲染组件,如果你熟悉vue,使用动态组件component就可以实现。


首先,我们需要定义一个模块列表


import MeetingPending from '../components/meetingPending.vue'
import AbnormalEvents from '../components/abnormalEvents/index.vue'
import MySchedule from '../components/mySchedule.vue'
import TransactionApproval from '../components/transactionApproval.vue'
import RunningBroadcast from '../components/runningBroadcast.vue'
import CodeSite from '../components/codeSite/index.vue'
import MismatchSpace from '../components/mismatchSpace/index.vue'
import AirDevice from '../components/airDevice/index.vue'

// !全量模块选择列表
export const allModuleList = [
{ moduleCode: 'meeting_pending', label: '会议待办', component: MeetingPending },
{ moduleCode: 'my_schedule', label: '我的日程', component: MySchedule },
{ moduleCode: 'audit_matters', label: '事项审批', component: TransactionApproval },
{ moduleCode: 'abnormal_events', label: '异常事件', component: AbnormalEvents },
{ moduleCode: 'deviation', label: '控制失调空间', component: MismatchSpace },
{ moduleCode: 'run_broadcast', label: '运行播报', component: RunningBroadcast },
{ moduleCode: 'cold_station', label: '冷站', component: CodeSite },
{ moduleCode: 'air_conditioning_terminal', label: '空调末端', component: AirDevice }
]

然后根据moduleCode做匹配,动态渲染即可


  <div v-for="moduleColumn in  moduleList " :key="moduleColumn.columnIndex" class="box">
<VueDraggableNext :list="moduleColumn.moduleDetail" handle=".move" group="column">
<div v-for="(item, index) in moduleColumn.moduleDetail " :key="item.moduleCode" class="drag-item">
<div class="move">
拖拽区域
</div>
<component :is="getComponentsByCode(item.moduleCode)" ></component>
</div>
</VueDraggableNext>

</div>

更多定制化需求


如果上面的功能不满足你的需求,我们可以使用这个组件的其他属性,完成更多意想不到的效果


如果下面的属性说明未能完全看明,可以看左边的对应的菜单查看详细说明和例子。


属性名称说明
group如果一个页面有多个拖拽区域,通过设置group名称可以实现多个区域之间相互拖拽 或者 { name: "...", pull: [true, false, 'clone', array , function], put: [true, false, array , function] }
sort是否开启排序,如果设置为false,它所在组无法排序
delay鼠标按下多少秒之后可以拖拽元素
touchStartThreshold鼠标按下移动多少px才能拖动元素
disabled:disabled= "true",是否启用拖拽组件
animation拖动时的动画效果,如设置animation=1000表示1秒过渡动画效果
handle:handle=".mover" 只有当鼠标在class为mover类的元素上才能触发拖到事件
filter:filter=".unmover" 设置了unmover样式的元素不允许拖动
draggable:draggable=".item" 样式类为item的元素才能被拖动
ghost-class:ghost-class="ghostClass" 设置拖动元素的占位符类名,你的自定义样式可能需要加!important才能生效,并把forceFallback属性设置成true
chosen-class:ghost-class="hostClass" 被选中目标的样式,你的自定义样式可能需要加!important才能生效,并把forceFallback属性设置成true
drag-class:drag-class="dragClass"拖动元素的样式,你的自定义样式可能需要加!important才能生效,并把forceFallback属性设置成true
force-fallback默认false,忽略HTML5的拖拽行为,因为h5里有个属性也是可以拖动,你要自定义ghostClass chosenClass dragClass样式时,建议forceFallback设置为true
fallback-class默认false,克隆选中元素的样式到跟随鼠标的样式
fallback-on-body默认false,克隆的元素添加到文档的body中
fallback-tolerance按下鼠标移动多少个像素才能拖动元素,:fallback-tolerance="8"
scroll默认true,有滚动区域是否允许拖拽
scroll-fn滚动回调函数
scroll-fensitivity距离滚动区域多远时,滚动滚动条
scroll-speed滚动速度

传送门:vue.draggable.next 中文文档 - itxst.com


写在最后


关联文章:如何实现模块的锚点定位及闪烁提示:juejin.cn/post/734622…
240315 155754.gif


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

微信小程序主包过大终极解决方案

web
随着小程序项目的不断迭代升级,避免不了体积越来越大。微信限制主包最多2M,然而我们的项目引入了直播插件直接占了1.1M,导致必须采用一些手段去优化。下面介绍一下优化思路和终极解决方案。 1.分包 我相信几乎所有人都能想到的方案,基本上这个方案就能解决问题。具...
继续阅读 »

随着小程序项目的不断迭代升级,避免不了体积越来越大。微信限制主包最多2M,然而我们的项目引入了直播插件直接占了1.1M,导致必须采用一些手段去优化。下面介绍一下优化思路和终极解决方案。



1.分包


我相信几乎所有人都能想到的方案,基本上这个方案就能解决问题。具体如何实现可以参照官方文档这里不做过多说明。(基础能力 / 分包加载 / 使用分包 (qq.com)),但是有时候你会发现分包之后好像主包变化不是很大,这是为什么呢?



  • 痛点1:通过依赖分析,如果分包中引入了第三方依赖,那么依赖的js仍然会打包在主包中,例如echarts、wxparse、socket.io。这就导致我们即使做了分包处理,但是主包还是很大,因为相关的js都会在主包中的vendor.js

  • 痛点2:插件只能在主包中无法分包,例如直播插件直接占据1M
    image.png

  • 痛点3:tabbar页面无法分包,只能在主包内

  • 痛点4:公共组件/方法无法分包,只能在主包内

  • 痛点5:图片只能在主包内


2.图片优化


图片是最好解决的,除了tabbar用到的图标,其余都放在云上就好了,例如oss和obs。而且放在云上还有个好处就是背景图片无需担心引入不成功。


3.tabbar页面优化


这部分可以采用tabbar页面都在放在一个文件夹下,比如一共有4个tab,那么一个文件夹下就只存放这4个页面。其余tabbar的子页面一律采用分包。


4.独立分包


独立分包是小程序中一种特殊类型的分包,可以独立于主包和其他分包运行。从独立分包中页面进入小程序时,不需要下载主包。当用户进入普通分包或主包内页面时,主包才会被下载。
但是使用的时候需要注意:



  • 独立分包中不能依赖主包和其他分包中的内容,包括 js 文件、template、wxss、自定义组件、插件等(使用 分包异步化 时 js 文件、自定义组件、插件不受此条限制)

  • 主包中的 app.wxss 对独立分包无效,应避免在独立分包页面中使用 app.wxss 中的样式;

  • App 只能在主包内定义,独立分包中不能定义 App,会造成无法预期的行为;

  • 独立分包中暂时不支持使用插件。


5.终极方案we-script



我们自己写的代码就算再多,其实增加的kb并不大。大部分大文件主要源于第三方依赖,那么有没有办法像webpack中的externals一样,当进入这个页面的时候再去异步加载js文件而不被打包呢(说白了就是CDN)



其实解决方案就是we-script,他允许我们使用CDN方式加载js文件。这样就不会影响打包体积了。


使用步骤



  1. npm install --save we-script

  2. "packNpmRelationList": [{"packageJsonPath": "./package.json", "miniprogramNpmDistDir":"./dist/"}]

  3. 点击开发者工具中的菜单栏:工具 --> 构建 npm

  4. "usingComponents": {"we-script": "we-script"}

  5. <we-script src="url1" />


使用中存在的坑


构建后可能会出现依赖报错,解决的方式就是将编译好的文件手动拖入miniprogram_npm文件夹中,主要是三个文件夹:we-script,acorn,eval5


最后成功解决了主包文件过大的问题,只要是第三方依赖,都可以通过这个办法去加载。


感谢阅读,希望来个三连支持下,转载记得标注原文地址~


作者:前端小鱼26
来源:juejin.cn/post/7355057488351674378
收起阅读 »

面试官:假如有几十个请求,如何去控制并发?

web
面试官:看你简历上做过图片或文件批量下载,那么假如我一次性下载几十个,如何去控制并发请求的? 让我想想,额~, 选中ID,循环请求?,八嘎!肯定不是那么沙雕的做法,这样做服务器直接崩溃啦!突然灵光一现,请求池!!! 我:利用Promise模拟任务队列,从而实现...
继续阅读 »

面试官:看你简历上做过图片或文件批量下载,那么假如我一次性下载几十个,如何去控制并发请求的?

让我想想,额~, 选中ID,循环请求?,八嘎!肯定不是那么沙雕的做法,这样做服务器直接崩溃啦!突然灵光一现,请求池!!!

我:利用Promise模拟任务队列,从而实现请求池效果。

面试官:大佬!


废话不多说,正文开始:


众所周知,浏览器发起的请求最大并发数量一般都是6~8个,这是因为浏览器会限制同一域名下的并发请求数量,以避免对服务器造成过大的压力。


首先让我们来模拟大量请求的场景


const ids = new Array(100).fill('')

console.time()
for (let i = 0; i < ids.length; i++) {
console.log(i)
}
console.timeEnd()

image.png


一次性并发上百个请求,要是配置低一点,又或者带宽不够的服务器,直接宕机都有可能,所以我们前端这边是需要控制的并发数量去为服务器排忧解难。


什么是队列?


先进先出就是队列,push一个的同时就会有一个被shift。我们看下面的动图可能就会更加的理解:


e0a2696a2299a3692d030dc7b956089a.gif


我们接下来的操作就是要模拟上图的队列行为。


定义请求池主函数函数


export const handQueue = (  
reqs // 请求数量
) => {}

接受一个参数reqs,它是一个数组,包含需要发送的请求。函数的主要目的是对这些请求进行队列管理,确保并发请求的数量不会超过设定的上限。


定义dequeue函数


const dequeue = () => {  
while (current < concurrency && queue.length) {
current++;
const requestPromiseFactory = queue.shift() // 出列
requestPromiseFactory()
.then(() => { // 成功的请求逻辑
})
.catch(error => { // 失败
console.log(error)
})
.finally(() => {
current--
dequeue()
});
}
}

这个函数用于从请求池中取出请求并发送。它在一个循环中运行,直到当前并发请求数current达到最大并发数concurrency或请求池queue为空。对于每个出队的请求,它首先增加current的值,然后调用请求函数requestPromiseFactory来发送请求。当请求完成(无论成功还是失败)后,它会减少current的值并再次调用dequeue,以便处理下一个请求。


定义返回请求入队函数


return (requestPromiseFactory) => {  
queue.push(requestPromiseFactory) // 入队
dequeue()
}

函数返回一个函数,这个函数接受一个参数requestPromiseFactory,表示一个返回Promise的请求工厂函数。这个返回的函数将请求工厂函数加入请求池queue,并调用dequeue来尝试发送新的请求,当然也可以自定义axios,利用Promise.all统一处理返回后的结果。


实验


const enqueue = requestQueue(6) // 设置最大并发数
for (let i = 0; i < reqs.length; i++) { // 请求
enqueue(() => axios.get('/api/test' + i))
}

动画.gif


我们可以看到如上图所示,请求数确实被控制了,只有有请求响应成功的同时才会有新的请求进来,极大的降低里服务器的压力,后端的同学都只能喊6


整合代码


import axios from 'axios'

export const handQueue = (
reqs // 请求总数
) => {
reqs = reqs || []


const requestQueue = (concurrency) => {
concurrency = concurrency || 6 // 最大并发数
const queue = [] // 请求池
let current = 0

const dequeue = () => {
while (current < concurrency && queue.length) {
current++;
const requestPromiseFactory = queue.shift() // 出列
requestPromiseFactory()
.then(() => { // 成功的请求逻辑
})
.catch(error => { // 失败
console.log(error)
})
.finally(() => {
current--
dequeue()
});
}

}

return (requestPromiseFactory) => {
queue.push(requestPromiseFactory) // 入队
dequeue()
}

}

const enqueue = requestQueue(6)

for (let i = 0; i < reqs.length; i++) {

enqueue(() => axios.get('/api/test' + i))
}
}

作者:大码猴
来源:juejin.cn/post/7356534347509645375
收起阅读 »

别忘了前端是靠什么起家的😡😡😡

web
一、忘了最基础的东西 前端开发的核心构建在三大基石技术上:HTML、CSS和JavaScript。回想起多年前,前端开发者常被戏称为“切图仔”,但就是这样的角色,通过精湛的CSS技巧,能够实现各种复杂的交互和特效,展现出前所未有的网页魔法。这是那些专注于服务端...
继续阅读 »

一、忘了最基础的东西


前端开发的核心构建在三大基石技术上:HTML、CSS和JavaScript。回想起多年前,前端开发者常被戏称为“切图仔”,但就是这样的角色,通过精湛的CSS技巧,能够实现各种复杂的交互和特效,展现出前所未有的网页魔法。这是那些专注于服务端开发的工程师所难以企及的领域。因此,前端工程师这一职业逐渐崭露头角,早期的培训班甚至设立了专门的课程来传授这些技能。然而,随着时间的推移,UI组件库和框架变得越来越普及,HTML和JavaScript的重要性依旧被人们所认可,但CSS技能却逐渐被边缘化,甚至有所忽视。在一次代码走查中,发现一个拥有三四年前端开发经验的同事,连CSS最基本的类型选择器都掌握不熟练。这一现象令人感到忧虑。


二、令人无语的代码


在一次对 useState 的使用场景进行治理的过程中。发现了一段感觉很无语的代码。代码我简化一下如下所示:


import React, { useState } from 'react';
import { Input } from 'antd';
import type { FC } from 'react';
import styles from './index.less';

const Test: FC = () => {
const [isFocus, setIsFocus] = useState(false);

return (
<Input
className={isFocus ? styles['input-focus'] : styles.input}
onFocus={() =>
{
setIsFocus(true);
}}
onBlur={() => {
setIsFocus(false);
}}
/>

);
};

export default Test;

.input-focus{
background: #f2f3f;
}

三、询问缘由


这段代码的目的是根据输入框的焦点状态(聚焦或失去焦点)来改变其样式,逻辑上没有问题。


我找到编写这段代码的同事询问:“为什么需要定义一个isFocus状态呢?”


他看了代码良久,有些疑惑地解释说:“这是为了追踪输入框的聚焦状态,从而在聚焦时改变背景色。”


“这个状态还有其他用途吗?”我追问。


“没有,就这个作用。有问题吗?”他回答。


我继续探询:“不使用isFocus状态,我们还能达到同样的效果吗?”


他思考了一会儿:“如果不添加类名来标识输入框的聚焦状态,我们怎么区分呢?”


我提出了另一种方案:“我们能不能仅用CSS来实现这个效果?”


他迟疑了一下:“但是CSS怎么能识别输入框是否聚焦呢?”


我提醒他:“你有没有试过使用伪类选择器?”


“伪类?我通常只用类选择器。”他回答。


我解释道:“我们可以使用:focus伪类来实现这个效果。你可以先回去继续你的工作。”


四、审查他另外的代码


我继续审查了这位同事的其他代码,发现他对CSS的理解似乎并不深入。例如,为了实现列表的斑马纹效果,理应直接使用:nth-child(odd):nth-child(even)选择器,但他却通过在遍历过程中判断索引是奇数还是偶数来分别添加不同的类选择器实现这一效果。此外,他同时使用了float: leftposition: absolute,这在布局中是矛盾的组合。他还通过JavaScript动态添加类选择器来改变输入框提示文字的字体颜色,还一直重复定义colorfont-size而不懂这些可以继承。


我不确定这是否反映了他的态度问题或是能力问题,在现在只出不进,内部消化的环境下,我默默地记录下这些,以便将来作为评估的参考。


五、关键是理解而不是记忆


也许会有人觉得我要求的太苛刻,也许这位同事只是忘记了有这几个CSS选择器。的确,CSS选择器的种类众多,达到60多种,可能会让人难以记住每一个。然而,重点并不在于能否一一背诵每个选择器,而在于理解它们各自的功能和使用场景。这样,当面对特定的样式需求时,我们可以轻松地查找并应用最合适的选择器来实现目标效果。


最基本的元素选择器、类选择器、和ID选择器因其简洁直观而被频繁使用。但是,深入探索那些不那么显眼的选择器——如通配符选择器、组合选择器、属性选择器、伪类选择器、和伪元素选择器——同样至关重要。这些选择器赋予了我们更精细的控制权,使得我们能够创造出更加复杂和细腻的视觉效果。


总之,我们不必强迫自己记住所有CSS选择器。更为重要的是认识到CSS选择器的多样性和强大之处。这种认识使我们能够在遇到具体的样式挑战时,知道如何寻找解决方案,从而更高效地运用CSS优化我们的代码。


为了真正理解这些选择器,我们需要思考它们被设计出来的原因——它们是如何帮助我们更好地控制样式,应对各种布局和视觉挑战的。这种深入的理解方式,远比简单的记忆更为重要和有效。


六、为啥需要伪类选择器


伪类选择器在CSS中的存在有着重要的意义和作用。它们提供了一种方式来选择HTML文档中无法通过简单选择器(如元素选择器、类选择器或ID选择器)直接选择的元素。伪类选择器的设计初衷和主要用途包括以下几点:


1、表达元素的特定状态


伪类选择器允许开发者根据用户与页面的交互来改变元素的样式,而不需要改变HTML代码。例如,:hover伪类可以用来改变鼠标悬停在链接或按钮上时的样式,:focus伪类用于当元素获得焦点时(比如输入框被点击时),而:active伪类则用于元素被激活(通常是被点击)的瞬间。这些都是基于用户行为的动态变化,通过CSS直接实现,无需JavaScript介入,提高了网页的交互性和用户体验。


2、选择特定位置的元素


伪类选择器还可以用来选择处于特定位置的元素,例如第一个子元素、最后一个子元素或者是父元素的唯一子元素。这对于设计复杂的布局和样式非常有用,尤其是在处理列表、表格和导航菜单时。例如,:first-child:last-child:nth-child()等伪类选择器,它们提供了一种灵活的方式来选择和样式化这些特定位置的元素。


3、选择特定属性的元素


虽然属性选择器(如[attribute=value])可以用来基于元素的属性选择元素,但某些伪类选择器(如:checked)提供了更为简便的方式来选择具有特定属性的元素。例如,:checked伪类选择器可以选择所有选中的复选框和单选按钮,这对于创建自定义表单控件的样式非常有用。


4、增强可访问性


伪类选择器还可以增强网页的可访问性。例如,:focus伪类可以用来为获得焦点的元素定义明显的样式,这对于键盘导航用户来说非常重要。通过提供视觉反馈,用户可以更容易地识别当前交互的元素,从而提高网站的可访问性。


5、无需额外的HTML标记


使用伪类选择器,开发者可以在不增加额外HTML标记的情况下,实现复杂的样式和布局。这有助于保持HTML代码的简洁和语义化,同时还可以减少页面的大小和提高加载速度。


总之,伪类选择器为CSS提供了强大的功能,使得开发者能够以更细致和动态的方式控制网页的样式。它们是现代网页设计中不可或缺的工具,使得网页能够响应用户的交互,同时保持代码的整洁和高效。


七、为啥需要伪元素选择器


伪元素选择器在CSS中的引入,为网页设计和内容表现提供了更加丰富和灵活的手段。伪元素选择器允许开发者访问并样式化一个元素的特定部分,或者在文档树中虚拟地创建新的元素,而这些通常不能通过HTML直接实现。伪元素选择器的存在有几个重要的原因和用途:


1、访问和样式化文档的特定部分


伪元素选择器使得开发者能够访问并样式化元素的特定部分,比如第一行文本、第一个字母、或者元素之前和之后的内容。例如,::first-line::first-letter 伪元素分别允许开发者为元素的第一行文本和第一个字母设置特定的样式。这在打造具有吸引力的排版和阅读体验时非常有用。


2、在不改变HTML结构的情况下添加内容


通过使用 ::before::after 伪元素,开发者可以在元素的内容之前或之后插入新的内容或装饰,而不需要修改HTML代码。这种方法非常适合添加图标、装饰性元素或者是为元素添加特殊的前缀或后缀,同时保持HTML的清晰和语义化。


3、创建视觉效果


伪元素选择器也常被用于创建特殊的视觉效果,比如自定义的清除浮动方法(使用 ::after 清除浮动),或者是设计复杂的背景装饰和形状。这些都可以通过伪元素以及结合CSS的其他特性(如backgroundborderbox-shadow等)来实现。


4、提高网页性能


使用伪元素可以在不增加额外HTML元素的情况下实现复杂的设计,这有助于减少DOM的大小,从而提高网页的性能。通过减少页面加载时需要解析的HTML标签数量,可以加快页面的渲染速度。


5、保持HTML的语义化


通过使用伪元素来添加装饰性内容或样式,开发者可以避免在HTML中添加非语义化的标记。这有助于保持HTML文档的清晰和语义化,使得文档的结构更加明确,也更容易被搜索引擎优化(SEO)和屏幕阅读器理解。


总之,伪元素选择器为CSS提供了强大的功能,使得开发者能够以更细致和动态的方式控制网页的样式和内容。它们是现代网页设计中不可或缺的工具,允许开发者在不牺牲HTML语义化的前提下,实现复杂和创新的设计。


八、为啥需要属性选择器


属性选择器在CSS中的引入提供了一种强大的方式来根据元素的属性及其值来选择元素,从而应用特定的样式。这种选择器的存在和使用有几个关键的原因和优势:


1、精确选择和样式化元素


在复杂的网页设计中,开发者可能需要对具有特定属性或属性值的元素应用样式,而不是仅基于元素类型、类或ID。属性选择器使得这种精确选择成为可能。例如,可以选择所有设置了target="_blank"属性的<a>标签,并为它们应用特定的样式,以提示用户这些链接将在新窗口中打开。


2、提高CSS规则的灵活性


属性选择器增加了CSS规则的灵活性,允许开发者基于元素的属性和属性值来创建复杂的选择条件。这意味着开发者可以在不修改HTML结构的情况下,通过CSS实现更多的设计需求和响应式布局。


3、增强样式的可维护性


使用属性选择器,开发者可以避免在HTML中过度使用类或ID,从而简化HTML结构并提高样式的可维护性。当需要基于相同属性的元素应用统一的样式时,只需在CSS中定义一次相应的属性选择器规则,而不是在HTML中为每个元素重复添加类或ID。


4、促进更好的语义化和可访问性


属性选择器可以用来增强文档的语义化和可访问性。例如,通过选择具有特定role属性的元素并为它们应用样式,开发者可以帮助提高网页对于屏幕阅读器等辅助技术的可访问性。


5、实现条件样式


在某些情况下,开发者可能希望仅在元素具有特定属性或属性值时才应用样式。属性选择器使得这种条件样式化成为可能,无需额外的类或ID,也无需使用JavaScript。这种方式非常适合实现基于特定数据属性(data-*属性)的样式变化。


示例


假设我们想为所有含有特定属性data-tooltip的元素添加一个工具提示样式,我们可以使用如下CSS规则:


[data-tooltip] {
position: relative;
cursor: pointer;
}

[data-tooltip]:before {
content: attr(data-tooltip);
/* 更多的样式规则来定义工具提示的外观 */
}

这个示例展示了如何仅通过CSS和HTML属性来实现一个简单的工具提示功能,无需修改HTML结构或使用JavaScript。


总之,属性选择器为CSS提供了更多的选择和样式化能力,增加了样式表的灵活性和可维护性,同时促进了更好的文档结构和语义化。


九、为啥需要组合选择器


组合选择器在CSS中扮演着至关重要的角色,它们提供了一种强大的机制来选择具有特定关系的元素,从而允许开发者以更精细、更具体的方式应用样式。组合选择器的存在和使用主要基于以下几个原因:


1. 提高选择器的精确性


在复杂的网页布局中,仅使用简单选择器(如元素选择器、类选择器或ID选择器)往往难以精确地定位到特定的元素。组合选择器通过定义元素之间的关系(如父子关系、相邻关系等),使得开发者可以更精确地选择到目标元素。这种精确性对于实现特定的布局和样式效果至关重要。


2. 优化CSS的结构


使用组合选择器,可以避免在HTML中过度使用类或ID来达到样式目的,从而使得CSS的结构更加清晰和简洁。这种方法有助于提高代码的可维护性和可读性,同时减少了因重复定义样式而导致的冗余。


3. 实现更复杂的样式设计


组合选择器提供了一种方式来实现基于特定元素关系的复杂样式设计。例如,开发者可以使用子选择器(>)来仅为特定父元素的直接子元素应用样式,或使用相邻兄弟选择器(+)来为紧跟在特定元素后的兄弟元素应用样式。这种灵活性使得开发者能够创造出更加动态和富有层次感的页面布局和视觉效果。


4. 提升样式的可复用性


通过使用组合选择器,开发者可以为特定的元素关系定义样式,而不是针对特定的类或ID。这种做法增加了样式的可复用性,因为相同的组合选择器样式可以在不同的HTML结构中被复用,只要这些结构符合选择器定义的元素关系。


5. 保持HTML的语义化


组合选择器的使用有助于保持HTML代码的语义化,因为它们允许开发者基于元素之间的自然关系来应用样式,而不是强迫添加额外的类或ID。这样不仅使得HTML结构更加清晰,也有助于搜索引擎优化(SEO)和提高网站的可访问性。


示例


假设我们想为一个列表中的第一个项目添加特殊样式,我们可以使用子选择器和伪类选择器的组合来实现这一点:


ul > li:first-child {
color: red;
}

这个示例展示了如何使用组合选择器来精确选择并样式化特定的元素,而无需为该元素添加额外的类或ID。


总之,组合选择器是CSS中不可或缺的一部分,它们通过定义元素之间的关系增强了选择器的功能,使得开发者能够以更灵活、更高效的方式设计和实现网页样式。


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

前端部署发布项目后,如何解决缓存的老版本文件问题

web
针对这个问题有两个思路 方式一:纯前端 每次打包发版时都使用webpack构建一个version.json文件,文件里的内容是一个随机的字符串(我用的是时间戳),每次打包都会自动更新这个文件。 项目中,通过监听点击事件来请求version.json文件。使用本...
继续阅读 »

针对这个问题有两个思路


方式一:纯前端


每次打包发版时都使用webpack构建一个version.json文件,文件里的内容是一个随机的字符串(我用的是时间戳),每次打包都会自动更新这个文件。


项目中,通过监听点击事件来请求version.json文件。使用本地缓存将上一次生成的字符串存储起来,和本次请求过来的字符串进行对比;若字符串不一样,则说明有项目有新内容更新,提供用户刷新或清除缓存(我使用的)


方式二:前后端配合


在每个请求头加上发版的版本号,和保留在客户端的上一次版本号进行对比,如果不一致则强制刷新,刷新后保存当前版本号


实现:


1、webpack构建生成一个json文件,在项目目录下新建一个plugins的文件夹,新建version-webpack-plugin.js文件


webpack4****等高版本构建方式


/** Customized plug-in: Generate version number json file */const fs = require("fs");class VersionPlugin {  apply(compiler) {    // emit is an asynchronous hook, use tapAsync to touch it, you can also use tapPromise/tap (synchronous)    compiler.hooks.emit.tap("Version Plugin", (compilation) => {      const outputPath = compiler.path || compilation.options.output.path;      const versionFile = outputPath + "/version.json";      const timestamp = Date.now(); // timestamp as version number      const content = `{"version": "${timestamp}"}`;      /** Returns true if the path exists, false otherwise */      if (!fs.existsSync(outputPath)) {        // Create directories synchronously. Returns undefined or the path to the first directory created if recursive is true. This is the synchronous version of fs.mkdir().        fs.mkdirSync(outputPath, { recursive: true });      }      // Generate json file      fs.writeFileSync(versionFile, content, {        encoding: "utf8",        flag: "w",      });    });  }}module.exports = { VersionPlugin };

webpack3


低版本构建方式


/** Customized plug-in: Generate version number json file */const fs = require('fs')class VersionPlugin {  apply(compiler) {    compiler.plugin('done', function () {      // Copy the logic of the file, and the file has been compiled.      const outputPath = compiler.outputPath      const versionFile = outputPath + '/version.json'      const timestamp = Date.now() // 时间戳作为版本号      const content = `{"version": "${timestamp}"}`      /** Returns true if the path exists, false otherwise. */      if (!fs.existsSync(outputPath)) {        // Create directories synchronously. Returns undefined or the path to the first directory created if recursive is true. This is the synchronous version of fs.mkdir().        fs.mkdirSync(outputPath, { recursive: true })      }      // Generate json file      fs.writeFileSync(versionFile, content, {        encoding: 'utf8',        flag: 'w'      })    })  }}module.exports = { VersionPlugin }

2、在vue.config.js中使用这个plugin


const { VersionPlugin } = require('./src/plugin/version-webpack-plugin')

config.plugins.push(new VersionPlugin())


3、在每次执行webpack构建命令,都会在dist目录下生成一个version.json文件,里面有一个字段叫version,值是构建时的时间戳,每次构建都会生成一个新的时间戳。




4、发起ajax请求,请求version.json文件获取version时间戳,和本地保存的上一次的时间戳做比较,如果不一样,则进行对应的操作。/business/version.json,business是我项目的前缀,改成你自己的项目地址,能请求到version.json文件就行。


import axios from 'axios'import i18n from '@/i18n'import UpdateMessage from '@/components/common/UpdateProject/index.js'export function reloadVersion() {  axios.get(window.location.origin + '/mobile/version.json?v=' + Date.now()).then(rsp => {    let mobileVersion = localStorage.getItem('mobileVersion')    let onlineVersion = rsp.data.version    if (!mobileVersion) {      localStorage.setItem('mobileVersion', onlineVersion)      return    }    if (onlineVersion) {      if (mobileVersion !== onlineVersion) {        UpdateMessage.success({          title: i18n.t('bulk.pleaseWait'),          msg: i18n.t('common.updateRemind')        })        setTimeout(() => {          UpdateMessage.close()          localStorage.setItem('mobileVersion', onlineVersion)          window.location.reload();        }, 2000);      }    }  })}

5、请求发起的时机,可以使用定时器或者在切换页面的时候进行校验版本。根据自己的实际情况选择合适的调用时机。


async mounted() {  process.env.NODE_ENV !== 'development' && window.addEventListener('mousedown', this.handleonmousedown);},beforeDestroy() {  window.removeEventListener('mousedown', this.handleonmousedown)},

handleonmousedown() { reloadVersion()}

作者:jskai
来源:juejin.cn/post/7356049143955390518
收起阅读 »

怎么下载加密ts流的视频

web
以某网站如下的电影《2012》为例。 在这个网站上面,电影2012是以一系列几秒的ts格式来播放的,所以没办法直接复制视频地址来下载整部电影。看如下截图: 并且,每段ts还是加密的,单独下载ts文件是无法播放的,需要解密,如下图: 那要怎样才能下载完整的解...
继续阅读 »

以某网站如下的电影《2012》为例。


在这个网站上面,电影2012是以一系列几秒的ts格式来播放的,所以没办法直接复制视频地址来下载整部电影。看如下截图:


image.png


并且,每段ts还是加密的,单独下载ts文件是无法播放的,需要解密,如下图:


image.png


那要怎样才能下载完整的解密后的视频呢?下面分几步进行说明。


1、首先,获取该电影所有的ts列表,和加密方式及密钥:


要用chrome浏览器打开该网址,然后右击,点击检查,然后重新刷新页面,然后根据如下截图查看:


image.png


点击“index.m3u8”这个请求,然后根据如下截图:
image.png


能够得出该电影的所有ts列表,并且加密方式是“AES-128”,密钥是enc.key的请求中,iv是16字节长度的0 。 现查看enc.key请求如下:
image.png


发现是乱码(有些网站不是乱码,而是字符串)。乱码是因为该密钥是二进制的,需要用查看hex工具来获取16进制的密钥。


先下载该“enc.key”到本地,然后用hex工具查看16进制值。mac系统可以用如下查看:


image.png


可以得出该密钥的16进制为:7be5d74d56af87838c3b98f1a2febf8f


2、根据ts列表,用php来实现多进程快速下载


下载所有ts文件有很多方法,可以手动一个个下载,但是因为太多,所以这个方法会比较麻烦。可以用php脚本来快速下载。


创建个1.php文件,用来下载ts文件。写入如下内容:


<?php

function my_file_get_contents($url) {
$arrContextOptions = [
'ssl' => [
'verify_peer' => false,
'verify_peer_name' => false,
]
];
return file_get_contents($url, false, stream_context_create($arrContextOptions));
}

for ($i=$argv[1]; $i <= $argv[2]; $i++) {
echo $i.'...'.PHP_EOL;
$f = $i.'.ts';
if (file_exists($f)) {
continue;
}
// 下面的链接要改成“index.m3u8”这里面相对应的ts链接
$data = my_file_get_contents('https://hnts.ymuuy.com:65/hls/200/20240110/2077/plist'.$i.'.ts');
file_put_contents($f, $data);
}

然后,再创建个2.php文件,用来创建下载命令。写入如下内容:


<?php

// 882要改成改成“index.m3u8”这里面最大数字的ts链接后的数字
for ($i=1; $i<=882; $i+=20) {
$tmp = $i+20;
if ($tmp > 882) {
$tmp = 882;
}
echo 'php 1.php '.$i.' '.$tmp.' &'.PHP_EOL;
}

然后,运行如下命令:
image.png


生成了可以多进程下载ts文件的命令行,然后复制生成的命令,在终端运行如下:
image.png


可以看到,已经在快速下载了,分为了882/20=44个进程来同时快速下载。


可以用如下命令来查看下载进度:


while true
do
du -sh `pwd`; ls |wc -l;sleep 1;
done

显示如下:


image.png


会显示出当前下载的大小,和下载的总ts数。


注意,全部都下载完后,要查看下有没有大小为0的ts文件,这些是下载失败的文件,删除后,重新运行下下载命令即可。


3、所有文件都下载完后,要开始解密并合并了


同样也是用php脚本来解密,保存下面脚本为decrypt.php:


<?php

// 如果“enc.key”的密钥是二进制的话,就用下面这行
$key = hex2bin("7be5d74d56af87838c3b98f1a2febf8f");
// 如果“enc.key”的密钥是字符串的话,就用下面这行
// $key = 'Cibz2Dp3bCnzlmVx';

// 原样复制“index.m3u8”里面的IV的0x后面的部分
$iv = hex2bin("00000000000000000000000000000000");

$decrypted_file = 'output.ts'; // 最终要保存的文件

// 882改为ts总数
for ($i=1; $i<=882; $i++) {
echo $i,'...',PHP_EOL;
$encrypted_file = $i.'.ts';
$data = file_get_contents($encrypted_file);
$decrypted_data = openssl_decrypt($data, 'AES-128-CBC', $key, OPENSSL_RAW_DATA, $iv);
file_put_contents($decrypted_file, $decrypted_data, FILE_APPEND);
}

echo "解密成功,已保存为:".$decrypted_file;


运行如下命令:


image.png
image.png


这样,就成功的解密并合并为了output.ts文件,用支持ts的播放器就可以播放此电影了。


有问题这边留言探讨下~


作者:leptune
来源:juejin.cn/post/7356143704699519003
收起阅读 »

生产环境中的console.log语句会导致内存泄漏,一定不要用!!!

web
前言 如果要在 JS 中找一个用的最多的函数,那一定就是console.log,在前端进行调试时,大家都屡试不爽,都喜欢用的函数。但是在生产环境中使用console.log之类的打印日志,这就会造成内存的泄漏了,这是我们不可以忽视的一个点。 为什么会造成内存泄...
继续阅读 »

前言


如果要在 JS 中找一个用的最多的函数,那一定就是console.log,在前端进行调试时,大家都屡试不爽,都喜欢用的函数。但是在生产环境中使用console.log之类的打印日志,这就会造成内存的泄漏了,这是我们不可以忽视的一个点。


为什么会造成内存泄漏呢?接下来我们来分析分析。


先来这样的一个场景


<body>
<h1 id="app" @click="handleClick"> Hello, console.log</h1>

<script>
const h1 = document.getElementById('app');

h1.addEventListener('click', () => {
const arr = new Array(100000).fill(0);
console.log(arr);
})
</script>
</body>

每当我们点击一次<h1>元素时,就会创建了一个包含 100000 个元素的数组,并将其输出到控制台中。


GIF 2024-4-9 18-20-27.gif


我们知道打印在控制台上的数组,我们是可以将它展开来看见更加详细的内容的,所以造成内存泄漏的原因是什么呢?


按照过程,点击一下,触发一个事件处理函数,待这个函数执行完之后,里面的生成的数组按道理是要销毁掉的,但是因为经过了打印,控制台里面需要保持对这个数组的引用, 不然的话我们就不能展开数组,查看里面的内容了,所以它会一直保存,随着我们点击次数的增多,这样的数组引用次数越来越多,于是就造成了内存泄漏。


接下来我们借助Performance来具体的展示一下是不是这样的情况。


在进行前我们先进行一下垃圾回收(图片中小扫把就是垃圾回收),释放一下内存以便为了更好的观察console.log带来的内存泄漏,然后点击几次h1元素,打印数组,最后再进行一次垃圾回收


GIF 2024-4-9 18-40-21.gif


我们就可以看到,即使我们最后点了垃圾回收,还是存在一部分东西没有被回收,也是占用着内存的,这里指的就是我们打印在控制台的数组了。


0c065197df9c917bb3f467cb7c1ee77.png


我们来个不打印数组的情况看看(操作过程和前面一样,这里只展示最后的结果)


12762d42e5c30b6d6690d79179a1ac9.png


这时我们就可以观察到,内存的增长和下降都是很正常的,每当我们点击一次h1元素,就执行一次事件处理函数,导致内存的占用,可是执行完之后,内存就立马释放出来了。最后点击一次垃圾回收,内存的占用也就和刚刚开始时一样了。


那么说,我们不打开控制台不就不会造成内容泄漏了?那确实,在谷歌浏览器中会进行特殊的处理,并不会造成内存泄漏,但是在别的浏览器中,情况就不一样了。


结尾 🌸🌸🌸


看完这篇文章,我们一定要注意不要在生产环境中使用console.log!不要在生产环境中使用console.log!不要在生产环境中使用console.log!重要的事情说三遍。


但是在开发环境中我们要使用console.log来调试代码怎么办呢?那就需要在打包到生产环境时,把这个console.log给去掉,手动删的话又太麻烦了,这时就可以借助terser工具来帮助我们了。


好的,今日分享到此结束,最后感谢小伙伴的阅读。


作者:Ywis
来源:juejin.cn/post/7355763456081313832
收起阅读 »

用 VitePress 搭建电子书,绝了!

web
大家好,我是杨成功。 自从《前端开发实战派》出版以后,好多买过的小伙伴都联系我,问我有没有电子书?纸质书在公司看不方便,一些现成的代码没办法复制。 确实没有电子版,我也听大家的建议上微信读书,结果那边审核没通过。我想不行我自己搞一个电子书呗,给买了纸书的朋友免...
继续阅读 »

大家好,我是杨成功。


自从《前端开发实战派》出版以后,好多买过的小伙伴都联系我,问我有没有电子书?纸质书在公司看不方便,一些现成的代码没办法复制。


确实没有电子版,我也听大家的建议上微信读书,结果那边审核没通过。我想不行我自己搞一个电子书呗,给买了纸书的朋友免费阅读,方便他们随时查阅。


经过一番调研,VitePress 的 UI 我最喜欢,扩展性也非常好,所以就用它来搭建。


新建项目


在一个空文件夹下,使用命令生成项目:


$ npx vitepress init

全部使用默认选项,生成结构如下:


2024-04-07-16-55-42.png


图中的 .vitepress/config.mts 就是 VitePress 的配置文件。另外三个 .md 文件是 Markdown 内容,VitePress 会根据文件名自动生成路由,并将文件内容转换为 HTML 页面。


为了代码更优雅,一般会把 Markdown 文件放在 docs 目录下。只需要添加一个配置:


// config.mts
export default defineConfig({
srcDir: 'docs',
});

改造后的目录结构是这样:


2024-04-07-17-27-23.png


安装依赖并运行项目:


$ yarn add vitepress vue
$ yarn run docs:dev

前期设计的难点


电子书的内容不完全对外开放,只有买过纸书的人才能阅读。和掘金小册差不多,只能看部分内容,登录或购买后才能解锁全部章节。


而 VitePress 是一个静态站点生成器,默认只解析 Markdown。要想实现上述的功能,必须用到纯 Vue 组件,这需要通过扩展默认主题来实现。


扩展默认主题,也就是扩展 VitePress 的原始 Vue 组件,达到自定义的效果。


遵循这个思路,我们需要扩展的内容如下:



  • 添加登录页面,允许用户登录。

  • 添加用户中心页面,展示用户信息、退出登录。

  • 修改头部组件,展示登录入口。

  • 页面根组件,获取当前用户状态。

  • 修改内容组件,无权限时不展示内容。


当然了还需要接入几个接口:



  • 登录/注册接口。

  • 获取当前用户信息接口。

  • 验证当前用户权限的接口。


扩展默认主题


扩展默认主题,首先要创建一个 .vitepress/theme 文件夹,用来存放主题的组件、样式等代码。该文件夹下新建 index.ts 表示主题入口文件。


入口文件导出主题配置:


// index.ts
import Layout from './Layout.vue';

export default {
Layout,
enhanceApp({ app, router, siteData }) {
// ...
},
};

上面代码导入了一个 Layout.vue,这个组件是自定义布局组件:


<!-- Layout.vue -->
<script setup>
import DefaultTheme from 'vitepress/theme';

const { Layout } = DefaultTheme;
</script>

<template>
<Layout>
<template #nav-bar-content-after>
<button>登录</button>
</template>
</Layout>
</template>

为啥需要这个组件呢?因为该组件是项目根组件,可以从两个方面扩展:


(1)使用自定义插槽。


Layout 组件提供了许多插槽,允许我们在页面的多处位置插入内容。比如上面代码中的 nav-bar-content-after 插槽,会在头部组件右侧插入登录按钮。


具体有哪些插槽,详见这里


(2)做全局初始化。


当刷新页面时,需要做一些初始化操作,比如调用接口、监听某些状态等。


这个时候可以使用 Vue 的各种钩子函数,比如 onMounted:


// Layout.vue
<script setup>
import { onMounted } from 'vue';
onMounted(() => {
console.log('初始化、请求接口');
});
</script>

如何定制内容组件?


VitePress 的内容组件,会把所有 Markdown 内容渲染出来。但是如果用户没有登录,我们不允许展示内容,而是提示用户登录,就像掘金小册这样:


2024-04-07-08-50-00.png


定制内容组件,核心是在内容渲染的区域加一个判断:如果用户登录且验证通过,渲染内容即可;否则,展示类似上图的提示登录界面。


接下来我翻了 VitePress 的源码,找到了这个名为 VPDoc.vue 的组件:



github.com/vuejs/vitep…



在上方组件大概 46 行,我找到了内容渲染区域:


2024-04-07-09-09-20.png


就在这个位置,添加一个判断,就达到我们想要的效果了:


<main class="main">
<Content
class="vp-doc"
v-if="isLogin"
:class="[
pageName,
theme.externalLinkIcon && 'external-link-icon-enabled'
]"

/>

<div v-else>
<h4>登录后阅读全文</h4>
<button>去登录</button>
</div>

</main>

那怎么让这个修改生效呢?


VitePress 提供了一个 重写内部组件 的方案。将 VPDoc.vue 组件拷贝到本地,按照上述方法修改,重命名为 CusVPDoc.vue


在配置文件 .vitepress/config.ts 中添加重写逻辑:


// config.ts
export default defineConfig({
vite: {
resolve: {
alias: [
{
find: /^.*\/VPDoc\.vue$/,
replacement: fileURLToPath(new URL('./components/CusVPDoc.vue', import.meta.url)),
},
],
},
},
});

这样便实现了自定义内容组件,电子书截图如下:


2024-04-10-09-28-42.png


添加自定义页面


添加自定义页面,首先要创建一个自定义组件。


以登录页面为例,创建一个自定义组件 CusLogin.vue,编写登录页面和逻辑,然后将其注册为一个全局组件。在 Markdown 页面文件中,直接使用这个组件。


注册全局组件的方法,是在主题入口文件中添加以下配置:


// .vitepress/theme/index.ts
import CusLogin from './components/CusLogin.vue'

export default {
...
enhanceApp({ app}) {
app.component("CusLogin", CusLogin); // 注册全局组件
// ...
},
} satisfies Theme;

最后,新建 Markdown 文件 login.md,写入内容如下:


---
layout: page
---


<CusLogin />

现在访问路由 “/login” 就可以看到自定义登录页面了。


2024-04-10-09-30-28.png


全局状态管理


涉及到用户登录,那么必然会涉及在多个组件中共享登录信息。


如果要做完全的状态管理,不用说,安装 Pinia 并经过一系列配置,可以实现。但是我们的需求只是共享登录信息,完全没必要再装一套 Pinia,使用 组合式函数 就可以了。


具体怎么实现,在另一篇文章 Vue3 新项目,没必要再用 Pinia 了! 中有详细介绍。


接入 Bootstrap


自定义页面,总是需要一个 UI 框架。上面的登录页面中,我使用了 Bootstrap。


Vitepress 使用 UI 框架有一个限制:必须兼容 SSR。因为 Vitepress 本质上使用了 Vue 的服务端渲染功能,在构建期间生成多个 HTML 页面,并不是常见的单页面应用。


这意味着,Vue 组件只有在 beforeMountmounted 钩子中才能访问 DOM API。


而 Bootstrap 不需要打包构建就可以使用 UI,非常适合 Vitepress。


首先安装 Bootstrap:


$ yarn add bootstrap

然后在主题入口文件中引入 Sass 和 JS 文件:


import 'bootstrap/scss/bootstrap-cus.scss';
import 'bootstrap/dist/js/bootstrap.bundle.min.js';

按常理说,这样就可以了,但是实际运行会报错:找不到某个 DOM API。


还记得那个限制吗?必须兼容 SSR!因此不能直接引入 JS 文件。


解决方法是在自定义布局组件 Layout.vue 中通过异步的方式引入:


// .vitepress/theme/Layout.vue
onMounted(() => {
import('bootstrap/dist/js/bootstrap.bundle.min.js');
});

这样就大功告成了,你可以使用 Bootstrap 中丰富的 UI。


最终的电子书效果:《前端开发实战派》,欢迎点评。


最后留一个思考题:Vitepress 支持主题切换,Bootstrap 也分浅色和深色主题;切换 Vitepress 主题时,如何同步更改 Bootstrap 的主题呢?



公众号:程序员成功

作者微信:杨成功



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

HTML问题:如何实现分享URL预览?

web
前端功能问题系列文章,点击上方合集↑ 序言 大家好,我是大澈! 本文约2100+字,整篇阅读大约需要3分钟。 本文主要内容分三部分,如果您只需要解决问题,请阅读第一、二部分即可。如果您有更多时间,进一步学习问题相关知识点,请阅读至第三部分。 感谢关注微信公众号...
继续阅读 »

前端功能问题系列文章,点击上方合集↑


序言


大家好,我是大澈!


本文约2100+字,整篇阅读大约需要3分钟。


本文主要内容分三部分,如果您只需要解决问题,请阅读第一、二部分即可。如果您有更多时间,进一步学习问题相关知识点,请阅读至第三部分。


感谢关注微信公众号:“程序员大澈”,然后加入问答群,从此让解决问题的你不再孤单!


1. 需求分析


为了提高用户对页面链接分享的体验,需要对分享链接做一些处理。


以 Telegram(国外某一通讯软件) 为例,当在 Telegram 上分享已做过处理的链接时,它会自动尝试获取链接的预览信息,包括标题、描述和图片。


如此当接收者看到时,可以立即获取到分享链接的一些重要信息。这有助于接收者更好地了解链接的内容,决定是否点击查看详细内容。


图片


2. 实现步骤


2.1 实现前的说明


对于URL分享预览这个功能问题,在项目中挺常用的,只不过今天我们是以一些框架分享API的底层原理角度来讲的。


实现这种功能的关键,是在分享的链接中嵌入适当的元数据信息,应用软件会自动解析,请求分享链接的预览信息,并根据返回的元数据生成预览卡片。


对于国内的应用软件,目前我试过抖音,它可以实现分享和复制粘贴都自动解析,而微信、QQ等只能实现分享的自动解析。


对于国外的应用软件,我只实验过Telegram,它可以实现分享和复制粘贴都自动解析,但我想FacebookTwitterInstagram这些应用应该也都是可以的。


2.2 实现代码


实现URL链接的分享预览,你可以使用 Open Graph协议或 Twitter Cards,然后在 HTML 的 标签中,添加以下 meta 标签来定义链接预览的信息。


使用时,将所有meta全部复制过去,然后根据需求进行自定义即可。


还要注意两点,确保你页面的服务器正确配置了 SSL 证书,以及确保链接的URL有效(即:服务器没有做白名单限制)。


<head>
  
  <meta property="og:title" content="预览标题">
  <meta property="og:description" content="预览描述">
  <meta property="og:image:width" content="图片宽度">
  <meta property="og:image:height" content="图片高度">
  <meta property="og:image" content="预览图片的URL">
  <meta property="og:url" content="链接的URL">
  
  
  <meta name="twitter:card" content="summary">
  <meta name="twitter:title" content="预览标题">
  <meta name="twitter:description" content="预览描述">
  <meta property="twitter:image:width" content="图片宽度">
  <meta property="twitter:image:height" content="图片高度">
  <meta name="twitter:image" content="预览图片的URL">
  <meta name="twitter:url" content="链接的URL">
head>

下面我们做一些概念的整理、总结和学习。


3. 问题详解


3.1 什么是Open Graph协议?


Open Graph协议是一种用于在社交媒体平台上定义和传递网页元数据的协议。它由 Facebook 提出,并得到了其他社交媒体平台的支持和采纳。Open Graph 协议旨在标准化网页上的元数据,使网页在社交媒体上的分享和预览更加一致和可控。


通过在网页的 HTML  标签中添加特定的 meta 标签,使用 Open Graph 协议可以定义和传递与网页相关的元数据信息,如标题、描述、图片等。这些元数据信息可以被社交媒体平台解析和使用,用于生成链接预览、分享内容和提供更丰富的社交图谱。


使用 Open Graph 协议,网页的所有者可以控制链接在社交媒体上的预览内容,确保链接在分享时显示的标题、描述和图片等信息准确、有吸引力,并能够准确传达链接的主题和内容。这有助于提高链接的点击率、转化率和用户体验。


Open Graph 协议定义了一组标准的 meta 标签属性,如 og:titleog:descriptionog:image 等,用于提供链接预览所需的元数据信息。通过在网页中添加这些 meta 标签并设置相应的属性值,可以实现链接预览在社交媒体平台上的一致展示。


需要注意的是,Open Graph 协议是一种开放的标准,并不限于 Facebook 平台。其他社交媒体平台,如 Twitter、LinkedIn 等,也支持使用 Open Graph 协议定义和传递网页元数据,以实现链接预览的一致性。


图片


3.2 什么是Twitter Cards?


Twitter Cards 是一种由 Twitter 推出的功能,它允许网站所有者在他们的网页上定义和传递特定的元数据,以便在 Twitter 上分享链接时生成更丰富和吸引人的预览卡片。通过使用 Twitter Cards,网页链接在 Twitter 上的分享可以展示标题、描述、图片、链接和其他相关信息,以提供更具吸引力和信息丰富的链接预览。


Twitter Cards 提供了多种类型的卡片,以适应不同类型的内容和需求。以下是 Twitter Cards 的一些常见类型:



  • Summary CardSummary Card 类型的卡片包含一个标题、描述和可选的图片。它适用于分享文章、博客帖子等内容。

  • Summary Card with Large ImageSummary Card with Large Image 类型的卡片与 Summary Card 类型类似,但图片尺寸更大,更突出地展示在卡片上。

  • App CardApp Card 类型的卡片用于分享移动应用程序的信息。它包含应用的名称、图标、描述和下载按钮,以便用户可以直接从预览卡片中下载应用。

  • Player CardPlayer Card 类型的卡片用于分享包含媒体播放器的内容,如音频文件、视频等。它允许在预览卡片上直接播放媒体内容。


通过在网页的 HTML  标签中添加特定的 meta 标签,使用 Twitter Cards 可以定义和传递与链接预览相关的元数据信息,如标题、描述、图片、链接等。这些元数据信息将被 Twitter 解析和使用,用于生成链接预览卡片。


使用 Twitter Cards 可以使链接在 Twitter 上的分享更加吸引人和信息丰富,提高链接的点击率和用户参与度。它为网站所有者提供了更多控制链接在 Twitter 上展示的能力,并提供了一种更好的方式来呈现他们的内容。


图片


图片


结语


建立这个平台的初衷:



  • 打造一个仅包含前端问题的问答平台,让大家高效搜索处理同样问题。

  • 通过不断积累问题,一起练习逻辑思维,并顺便学习相关的知识点。

  • 遇到难题,遇到有共鸣的问题,一起讨论,一起沉淀,一起成长。

作者:程序员大澈
来源:juejin.cn/post/7310112330663231515
收起阅读 »

旋转、缩放、移动:掌握CSS Transform动画的终极指南!

在深入探讨CSS变形动画之前,让我们先探讨一下掌握它之后你可以实现哪些有趣的效果。学习了CSS变形动画之后,你将能够为你的网页添加引人注目的动态效果,例如创建一个立体的3D魔方,或者设计一个引人入胜的旋转菜单。这些仅仅是众多可能性中的一小部分,但或许可以勾起我...
继续阅读 »

在深入探讨CSS变形动画之前,让我们先探讨一下掌握它之后你可以实现哪些有趣的效果。

学习了CSS变形动画之后,你将能够为你的网页添加引人注目的动态效果,例如创建一个立体的3D魔方,或者设计一个引人入胜的旋转菜单。

Description
这些仅仅是众多可能性中的一小部分,但或许可以勾起我们的学习兴趣。

一、什么是CSS变形动画?

CSS变形动画是利用CSS3的transform属性创建的动画效果。它可以使元素旋转、缩放、倾斜甚至翻转,让静态的网页元素动起来,为用户带来更加丰富的交互体验。

坐标系统

首先我们要学习的变形动画,想达到在上图中出现的3D效果单纯的X与Y两个轴是实现不了的,还需要加入一条纵深轴,即Y轴的参与才有一个3D的视觉感受。

那么如何来理解X,Y,Z这三条轴的关系呢?可以看一下下面这张图。

Description

  • X轴代表水平轴

  • Y轴代表垂直轴

  • Z轴代表纵深轴

X和Y轴都非常好理解,怎么理解这个Z轴呢?

CSS的中文名称叫做层叠样式表,那么它肯定是一层一层的。之前学习过z-index就是用来设置层的优先级,优先级越高越在上面,也可以理解为离我们肉眼越近,它把优先级低的层给盖住了,所以Z轴可以理解为我们观察的视角与被观察物体之间的一条轴。

  • Z轴数值越大,说明观测距离越远。

  • Z轴的数值可以无限大,所以设置的时候一定要小心。

二、变形操作

使用 transform 来控制元素变形操作,包括控制移动、旋转、倾斜、3D转换等。

Description

下面我们通过一些例子来演示一下,比较常用的变形操作:

2.1 位移 translate()

translate()函数可以将元素向指定的方向移动,类似于position中的relative。或以简单的理解为,使用translate()函数,可以把元素从原来的位置移动,而不影响在X、Y轴上的任何Web组件。

想象一下,当你滚动页面时,一个元素平滑地从一个位置滑向另一个位置,这种流畅的过渡效果可以大大提升用户体验。

translate我们分为三种情况:

1)translate(x,y)水平方向和垂直方向同时移动(也就是X轴和Y轴同时移动)

2)translateX(x)仅水平方向移动(X轴移动)

3)translateY(Y)仅垂直方向移动(Y轴移动)

实例演示: 通过translate()函数将元素向Y轴下方移动50px,X轴右方移动100px。

HTML代码:

<div class="wrapper">
<div>我向右向下移动</div>
</div>

CSS代码:


.wrapper {
width: 200px;
height: 200px;
border: 2px dotted red;
margin: 20px auto;
}
.wrapper div {
width: 200px;
height: 200px;
line-height: 200px;
text-align: center;
background: orange;
color: #fff;
-webkit-transform: translate(50px,100px);
-moz-transform:translate(50px,100px);
transform: translate(50px,100px);
}

演示结果:

Description

2.2 旋转 rotate()

旋转rotate()函数通过指定的角度参数使元素相对原点进行旋转。旋转不仅可以是固定的度数,还可以是动态变化的,创造出无限的可能性。

它主要在二维空间内进行操作,设置一个角度值,用来指定旋转的幅度。如果这个值为正值,元素相对原点中心顺时针旋转;如果这个值为负值,元素相对原点中心逆时针旋转。如下图所示:

Description

HTML代码:

<div class="wrapper">
<div></div>
</div>

CSS代码:

.wrapper {
width: 200px;
height: 200px;
border: 1px dotted red;
margin: 100px auto;
}
.wrapper div {
width: 200px;
height: 200px;
background: orange;
-webkit-transform: rotate(45deg);
transform: rotate(45deg);
}

演示结果:
Description

2.3 扭曲 skew()

扭曲skew()函数能够让元素倾斜显示。这种效果常常用于模拟速度感或者倾斜的视觉效果。

它可以将一个对象以其中心位置围绕着X轴和Y轴按照一定的角度倾斜。这与rotate()函数的旋转不同,rotate()函数只是旋转,而不会改变元素的形状。skew()函数不会旋转,而只会改变元素的形状。

Skew()具有三种情况:
1)skew(x,y)使元素在水平和垂直方向同时扭曲(X轴和Y轴同时按一定的角度值进行扭曲变形);

Description

第一个参数对应X轴,第二个参数对应Y轴。如果第二个参数未提供,则值为0,也就是Y轴方向上无斜切。

2)skewX(x)仅使元素在水平方向扭曲变形(X轴扭曲变形);
Description

3)skewY(y)仅使元素在垂直方向扭曲变形(Y轴扭曲变形)。
Description

示例演示:通过skew()函数将长方形变成平行四边形。

HTML代码:

<div class="wrapper">
<div>我变成平形四边形</div>
</div>

CSS代码:

.wrapper {
width: 300px;
height: 100px;
border: 2px dotted red;
margin: 30px auto;
}
.wrapper div {
width: 300px;
height: 100px;
line-height: 100px;
text-align: center;
color: #fff;
background: orange;
-webkit-transform: skew(45deg);
-moz-transform:skew(45deg)
transform:skew(45deg);
}

演示结果:
Description

2.4 缩放 scale()

缩放 scale()函数 让元素根据中心原点对对象进行缩放。这不仅可以用来模拟放大镜效果,还可以创造出元素的进入和退出动画,比如一个图片慢慢缩小直至消失。

缩放 scale 具有三种情况:

1) scale(X,Y)使元素水平方向和垂直方向同时缩放(也就是X轴和Y轴同时缩放)。

Description
例如:

div:hover {
-webkit-transform: scale(1.5,0.5);
-moz-transform:scale(1.5,0.5)
transform: scale(1.5,0.5);
}

注意:Y是一个可选参数,如果没有设置Y值,则表示X,Y两个方向的缩放倍数是一样的。

2)scaleX(x)元素仅水平方向缩放(X轴缩放)
Description
3)scaleY(y)元素仅垂直方向缩放(Y轴缩放)
Description
HTML代码:

<div class="wrapper">
<div>我将放大1.5倍</div>
</div>

CSS代码:


.wrapper {
width: 200px;
height: 200px;
border:2px dashed red;
margin: 100px auto;
}
.wrapper div {
width: 200px;
height: 200px;
line-height: 200px;
background: orange;
text-align: center;
color: #fff;
}
.wrapper div:hover {
opacity: .5;
-webkit-transform: scale(1.5);
-moz-transform:scale(1.5)
transform: scale(1.5);
}

演示结果:
Description
注意: scale()的取值默认的值为1,当值设置为0.01到0.99之间的任何值,作用使一个元素缩小;而任何大于或等于1.01的值,作用是让元素放大。


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

2.5 矩阵 matrix()

matrix() 是一个含六个值的(a,b,c,d,e,f)变换矩阵,用来指定一个2D变换,相当于直接应用一个[a b c d e f]变换矩阵。就是基于水平方向(X轴)和垂直方向(Y轴)重新定位元素。

此属性值使用涉及到数学中的矩阵,我在这里只是简单的说一下CSS3中的transform有这么一个属性值,如果需要深入了解,需要对数学矩阵有一定的知识。

示例演示:通过matrix()函数来模拟transform中translate()位移的效果。
HTML代码:

<div class="wrapper">
<div></div>
</div>

CSS代码:

.wrapper {
width: 300px;
height: 200px;
border: 2px dotted red;
margin: 40px auto;
}
.wrapper div {
width:300px;
height: 200px;
background: orange;
-webkit-transform: matrix(1,0,0,1,50,50);
-moz-transform:matrix(1,0,0,1,50,50);
transform: matrix(1,0,0,1,50,50);
}

演示结果:

Description

2.6 原点 transform-origin

任何一个元素都有一个中心点,默认情况之下,其中心点是居于元素X轴和Y轴的50%处。如下图所示:

Description

在没有重置transform-origin改变元素原点位置的情况下,CSS变形进行的旋转、位移、缩放,扭曲等操作都是以元素自己中心位置进行变形。

但很多时候,我们可以通过transform-origin来对元素进行原点位置改变,使元素原点不在元素的中心位置,以达到需要的原点位置。

transform-origin取值和元素设置背景中的background-position取值类似,如下表所示:

Description

示例演示:

通过transform-origin改变元素原点到左上角,然后进行顺时旋转45度。

HTML代码:

<<div class="wrapper">
<div>原点在默认位置处</div>
</div>
<div class="wrapper transform-origin">
<div>原点重置到左上角</div>
</div>

CSS代码:

.wrapper {
width: 300px;
height: 300px;
float: left;
margin: 100px;
border: 2px dotted red;
line-height: 300px;
text-align: center;
}
.wrapper div {
background: orange;
-webkit-transform: rotate(45deg);
transform: rotate(45deg);
}
.transform-origin div {
-webkit-transform-origin: left top;
transform-origin: left top;
}

演示结果:
Description

以上就是css动画中几种基本的变形技巧了,掌握这些我们可以操控我们的网页元素实现我们想要的一些基本动画效果。

在这个充满创造力的时代,CSS变形动画是每个前端开发者必备的技能。它不仅能提升用户体验,更能激发设计师和开发者的创意火花。所以,不妨尝试一下,让你的网页动起来,给用户留下深刻的印象吧!

收起阅读 »

个人或个体户,如何免费使用微信小程序授权登录

web
需求 个人或个体户,如何免费使用微信小程序授权,快速登录进系统内部? 微信授权登录好处: 不用自己开发一个登录模块,节省开发和维护成本 安全性得到了保障,安全验证完全交由腾讯验证,超级可靠哇 可能有的人会问,为何不用微信公众号授权登录?原因很简单,因为一年...
继续阅读 »

需求


个人或个体户,如何免费使用微信小程序授权,快速登录进系统内部?


微信授权登录好处:



  1. 不用自己开发一个登录模块,节省开发和维护成本

  2. 安全性得到了保障,安全验证完全交由腾讯验证,超级可靠哇


可能有的人会问,为何不用微信公众号授权登录?原因很简单,因为一年要300元,小公司得省钱啊!


实现步骤说明


所有的步骤里包含四个对象,分别是本地后台本地微信小程序本地网页、以及第三方微信后台



  1. 本地后台调用微信后台https://api.weixin.qq.com/cgi-bin/token接口,get请求,拿到返回的access_token

  2. 本地后台根据拿到的access_token,调用微信后台https://api.weixin.qq.com/wxa/getwxacodeunlimit接口,得到二维码图片文件,将其输出传递给本地网页显示

  3. 本地微信小程序本地网页的二维码图片,跳转至小程序登录页面,通过wx.login方法,在success回调函数内得到code值,并将该值传递给本地后台

  4. 本地后台拿到code值后,调用微信后台https://api.weixin.qq.com/sns/jscode2session接口,get请求,得到用户登录的openid即可。



注意点:



  1. 上面三个微信接口/cgi-bin/token/getwxacodeunlimit/jscode2session必须由本地后台调用,微信小程序那边做了前端限制;

  2. 本地网页如何得知本地微信小程序已扫码呢?


本地微信小程序code,通过A接口,将值传给后台,后台拿到openid后,再将成功结果返回给本地微信小程序;同时,本地网页不断地轮询A接口,等待后台拿到openid后,便显示登录成功页面。



微信小程序核心代码


Page({
data: {
theme: wx.getSystemInfoSync().theme,
scene: "",
jsCode: "",
isLogin: false,
loginSuccess: false,
isChecked: false,
},
onLoad(options) {
const that = this;
wx.onThemeChange((result) => {
that.setData({
theme: result.theme,
});
});
if (options !== undefined) {
if (options.scene) {
wx.login({
success(res) {
if (res.code) {
that.setData({
scene: decodeURIComponent(options.scene),
jsCode: res.code,
});
}
},
});
}
}

},
handleChange(e) {
this.setData({
isChecked: Boolean(e.detail.value[0]),
});
},
formitForm() {
const that = this;
if (!this.data.jsCode) {
wx.showToast({
icon: "none",
title: "尚未微信登录",
});
return;
}
if (!this.data.isChecked) {
wx.showToast({
icon: "none",
title: "请先勾选同意用户协议",
});
return;
}
wx.showLoading({
title: "正在加载",
});
let currentTimestamp = Date.now();
let nonce = randomString();
wx.request({
url: `A接口?scene=${that.data.scene}&js_code=${that.data.jsCode}`,
header: {},
method: "POST",
success(res) {
wx.hideLoading();
that.setData({
isLogin: true,
});
if (res.statusCode == 200) {
that.setData({
loginSuccess: true,
});
} else {
if (res.statusCode == 400) {
wx.showToast({
icon: "none",
title: "无效请求",
});
} else if (res.statusCode == 500) {
wx.showToast({
icon: "none",
title: "服务内部错误",
});
}
that.setData({
loginSuccess: false,
});
}
},
fail: function (e) {
wx.hideLoading();
wx.showToast({
icon: "none",
title: e,
});
},
});
},
});


scene为随机生成的8位数字


本地网页核心代码


    let isInit = true
function loginWx() {
isInit = false
refreshQrcode()
}
function refreshQrcode() {
showQrLoading = true
showInfo = false
api.get('/qrcode').then(qRes => {
if (qRes.status == 200) {
imgSrc = `${BASE_URL}${qRes.data}`
pollingCount = 0
startPolling()
} else {
showToast = true
toastMsg = '二维码获取失败,请点击刷新重试'
showInfo = true
}
}).finally(() => {
showQrLoading = false
})
}

// 开始轮询
// 1000毫秒轮询一次
function startPolling() {
pollingInterval = setInterval(function () {
pollDatabase()
}, 1000)
}
function pollDatabase() {
if (pollingCount >= maxPollingCount) {
clearInterval(pollingInterval)
showToast = true
toastMsg = '二维码已失效,请刷新'
showInfo = true
return
}
pollingCount++
api.get('/result').then(res => {
if (res.status == 200) {
clearInterval(pollingInterval)
navigate('/os', { replace: true })
} else if (res.status == 408) {
clearInterval(pollingInterval)
showToast = true
toastMsg = '二维码已失效,请刷新'
showInfo = true
}
})
}



html的部分代码如下所示


     <button class="btn" on:click={loginWx}>微信登录</button>
<div id="qrcode" class="relative mt-10">
{#if imgSrc}
<img src={imgSrc} alt="二维码图片"/>
{/if}
{#if showQrLoading}
<div class="mask absolute top-0 left-0 w-full h-full z-10">
<Loading height="12" width="12"/>
</div>
{/if}
</div>

尾声


若需要完整代码,或想知道如何申请微信小程序,欢迎大家关注或私信我哦~~


作者:zwf193071
来源:juejin.cn/post/7351649413401493556
收起阅读 »

🚫为了防止狗上沙发,写了一个浏览器实时识别目标功能📷

web
背景 家里有一条狗🐶,很喜欢乘人不备睡沙发🛋️,恰好最近刚搬家 + 狗迎来了掉毛期 不想让沙发上很多毛。所以希望能识别到狗,然后播放“gun 下去”的音频📣。 需求分析 需要一个摄像头📷 利用 chrome 浏览器可以调用手机摄像头,获取权限,然后利用 ...
继续阅读 »

背景



家里有一条狗🐶,很喜欢乘人不备睡沙发🛋️,恰好最近刚搬家 + 狗迎来了掉毛期 不想让沙发上很多毛。所以希望能识别到狗,然后播放“gun 下去”的音频📣。


需求分析



  • 需要一个摄像头📷

    • 利用 chrome 浏览器可以调用手机摄像头,获取权限,然后利用 video 将摄像头的内容绘制到 video 上。



  • 通过摄像头实时识别画面中的狗🐶

    • 利用 tensorflow 和预训练的 COCO-SSD MobileNet V2 模型进行对象检测。

    • 将摄像头的视频流转化成视频帧图像传给模型进行识别



  • 录制一个音频

    • 识别到目标(狗)后播放音频📣



  • 需要部署在一个设备上

    • 找一个不用的旧手机📱,Android 系统

    • 安装 termux 来实现开启本地 http 服务🌐




技术要点



  1. 利用浏览器 API 调用手机摄像头,将视频流推给 video


    const stream = await navigator.mediaDevices.getUserMedia({
    // video: { facingMode: "environment" }, // 摄像头后置
    video: { facingMode: "user" },
    });

    const videoElement = document.getElementById("camera-stream");
    videoElement.srcObject = stream;


  2. 加载模型,实现识别


    let dogDetector;

    async function loadDogDetector() {
    // 加载预训练的SSD MobileNet V2模型
    const model = await cocoSsd.load();
    dogDetector = model; // 将加载好的模型赋值给dogDetector变量
    }


  3. 监听 video 的播放,将视频流转换成图像传入模型检测


    videoElement.addEventListener("play", async () => {
    requestAnimationFrame(processVideoFrame);
    });

    async function processVideoFrame() {
    if (!videoElement.paused && !videoElement.ended) {
    canvas.width = videoElement.videoWidth;
    canvas.height = videoElement.videoHeight;
    ctx.drawImage(videoElement, 0, 0, canvas.width, canvas.height);

    // 获取当前帧图像数据
    const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);

    // 对帧执行预测
    let predictionClasses = "";
    const predictions = await dogDetector.detect(imageData);
    // 处理预测结果,比如检查是否有狗被检测到
    for (const prediction of predictions) {
    predictionClasses += `${prediction.class}\n`; // 组装识别的物体名称
    if (prediction.class === "dog") {
    // 播放声音
    playDogBarkSound();
    }
    }
    nameContainer.innerText = predictionClasses.trim(); // 移除末尾的换行符

    requestAnimationFrame(processVideoFrame);
    }
    }


  4. 播放音频


    async function playDogBarkSound() {
    if (playing) return;
    playing = true;
    const audio = new Audio(dogBarkSound);
    audio.addEventListener("ended", () => {
    playing = false;
    });
    audio.volume = 0.5; // 调整音量大小
    await audio.play();
    }


  5. 手机开启本地 http 服务



    • 安装 termux

    • 安装 python3

    • 运行 python3 -m http.server 8000



  6. 将项目上传到 termux 的目录





项目代码(改为 html 文件后)


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Mobile Dog Detector</title>
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@4.17.0/dist/tf.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/coco-ssd@2.2.3/dist/coco-ssd.min.js"></script>
<style>
#camera-stream {
width: 200px;
height: auto;
}
#name {
height: 200px;
overflow-y: auto;
font-family: Arial, sans-serif;
}
</style>
</head>
<body>
<video id="camera-stream" autoplay playsinline></video>
<div id="name" style="height: 200px"></div>

<script>
let playing = false;
let dogDetector;

async function loadDogDetector() {
// 加载预训练的SSD MobileNet V2模型
const model = await cocoSsd.load();
dogDetector = model; // 将加载好的模型赋值给dogDetector变量
console.log("dogDetector", dogDetector);
startCamera();
}
// 调用函数加载模型
loadDogDetector();

async function startCamera() {
const stream = await navigator.mediaDevices.getUserMedia({
// video: { facingMode: "environment" }, // 摄像头后置
video: { facingMode: "user" },
});
const nameContainer = document.getElementById("name");
const videoElement = document.getElementById("camera-stream");
videoElement.srcObject = stream;

const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");

videoElement.addEventListener("play", async () => {
requestAnimationFrame(processVideoFrame);
});
async function processVideoFrame() {
if (!videoElement.paused && !videoElement.ended) {
canvas.width = videoElement.videoWidth;
canvas.height = videoElement.videoHeight;
ctx.drawImage(videoElement, 0, 0, canvas.width, canvas.height);

const imageData = ctx.getImageData(
0,
0,
canvas.width,
canvas.height
);

let predictionClasses = "";
const predictions = await dogDetector.detect(imageData);
for (const prediction of predictions) {
predictionClasses += `${prediction.class}\n`;
if (prediction.class === "dog") {
// 修改为检测到狗时播放声音
playDogBarkSound();
}
}
nameContainer.innerText = predictionClasses.trim();

requestAnimationFrame(processVideoFrame);
}
}

async function playDogBarkSound() {
if (playing) return;
playing = true;
const audio = new Audio("./getout.mp3");
audio.addEventListener("ended", () => {
playing = false;
});
audio.volume = 0.5; // 调整音量大小
await audio.play();
}
}
</script>
</body>
</html>

实现效果


效果很好👍,用旧手机开启摄像头后,检测到狗就播放声音了。


但是,家里夫人直接做了一个围栏晚上给狗圈起来了🚫



实现总结


该方案通过以下步骤实现了一个基于网页的实时物体检测系统,专门用于识别画面中的狗并播放特定音频以驱赶它离开沙发。具体实现过程包括以下几个核心部分:



  • 调用摄像头:


使用浏览器提供的 navigator.mediaDevices.getUserMedia API 获取用户授权后调用手机摄像头,并将视频流设置给 video 元素展示。



  • 加载物体检测模型:


使用 TensorFlow.js 和预训练的 COCO-SSD MobileNet V2 模型进行对象检测,加载模型后赋值给 dogDetector 变量。
处理视频流与图像识别:


监听 video 元素的播放事件,通过 requestAnimationFrame 循环逐帧处理视频。
将当前视频帧绘制到 canvas 上,然后从 canvas 中提取图像数据传入模型进行预测。
在模型返回的预测结果中,如果检测到“dog”,则触发播放音频函数。



  • 播放音频反馈:


定义一个异步函数 playDogBarkSound 来播放指定的音频文件,确保音频只在前一次播放结束后才开始新的播放。



  • 部署环境准备:


使用旧 Android 手机安装 Termux,创建本地 HTTP 服务器运行项目代码。
上传项目文件至 Termux 目录下并通过访问 localhost:8000 启动应用。


通过以上技术整合,最终实现了在旧手机上部署一个能够实时检测画面中狗的网页应用,并在检测到狗时播放指定音频。


作者:前端小蜗
来源:juejin.cn/post/7345672631323394098
收起阅读 »

前端在线预览播放视频方案,dpPlayer

web
华为云生成obs链接时,可以做配置。 视频是用来预览的 视频是用来下载的 一般我们播放本地视频都是使用vedio标签,但是vedio标签只支持三种视频格式:MP4、WebM、Ogg,对于在线视频直接使用vedio不支持播放。 故,上述 2 中的视频,在ve...
继续阅读 »

华为云生成obs链接时,可以做配置。



  1. 视频是用来预览

  2. 视频是用来下载


一般我们播放本地视频都是使用vedio标签,但是vedio标签只支持三种视频格式:MP4、WebM、Ogg,对于在线视频直接使用vedio不支持播放。
故,上述 2 中的视频,在vedio中不支持播放,浏览器访问链接,直接就下载了。


先介绍几个概念:


流协议: 流协议就是在两个通信系统之间传输多媒体文件的一套规则,它定义了视频文件将如何分解为小数据包以及它们在互联网上传输的顺序,RTMP与 RTSP 是比较常见的流媒体协议。


HLS: HLS (HTTP Live Streaming)是Apple的动态码率自适应技术。主要用于PC和Apple终端的音视频服务。包括一个m3u(8)的索引文件,TS媒体分片文件和key加密串文件。参考:HLS。简单来说,HLS是一种协议,如果你的视频源是http://xxxx.m3u8这种,就选择这种协议,.m3u8是个文本文件,直播时,他的内容实时变更,内部指向一个或多个.ts文件。


HTTP-FLV: HTTP-FLV 是将音视频数据以 FLV 文件格式进行封装,再将 FLV 格式数据封装在 HTTP 协议中进行传输的一种流媒体传输方式。HTTP-FLV 的实现原理: HTTP-FLV 利用 HTTP/1.1 分块传输机制发送 FLV 数据。虽然直播服务器无法知道直播流的长度,但是 HTTP/1.1 分块传输机制可以不填写 conten-length 字段而是携带 Transfer-Encoding: chunked 字段,这样客户端就会一直接受数据。参考:FLV 和 HTTP-FLV

简单来说就是你的视频源是直播且是xxxx.flv,就选择这种协议播放。还有个websocket-flv,是基于websocket的。


RTMP与RTSP: 什么是RTMP 和 RTSP?它们之间有什么区别?


H264(AVC)与H265(HEVC): 都是视频编码,是视频压缩格式,由于视频本身的码流太大,所以需要经过压缩然后再通过网络进行传输,其中H265是H264的升级版,很多播放器无法播放H265视频。




xgplayer


vue2的系统,本来用xgplayer 版本:2.32.5。无奈本地可以展示,测试环境不能用,报错不明显,粗略看了一下是插件底层,内部报错,故放弃xgpalyer插件。


ps.我在vue3的系统中,用过xgpalyer插件,挺好用的


优点如下:



  • 官网教程非常简单清晰,上手快

  • 使用起来体验感很好

  • 支持直播点播,支持hls、http+flv、dash、WebRTC直播,还有音乐播放器 。

  • 提供在线可调试demo


dpplayer


然后,我就换了 dppalyer插件来展示。点击查看中文文档


这个插件,我去github查了一下,15k星星,用的人还是挺多,但是,个人感觉不如 xgplayer好用。


安装npm install dplayer --save


在页面中引用


import DPlayer from 'dplayer';

const dp = new DPlayer(options);

dpplayer实现是通过生成iframe页面,将视频嵌套到其中。


刚开始给容器写了样式,宽100% 高100%,结果它不能自适应屏幕,很难受。后面我强行定宽420px。高度自动获取当前容器高度,定了一个最大高度。


但其实没有用,它会根据宽度,自己按比例缩放高度。
所以我在视频渲染出来后,自动调了一下全屏功能dpPlayer.fullScreen.request('web');
勉强解决了这个问题。


贴一下我的完整代码


<template>
<div class="vedio-wrapper" :style="{'max-height': winH}">
<el-empty v-if="!player" description="暂无数据"></el-empty>
<div :id="id" allowfullscreen="allowfullscreen" />
</div>

</template>


<script>
import DPlayer from 'dplayer';

import { getParam } from '@/utils/utils'
import {
getBucketObsFileUrl
} from '@/api/common'

export default {
name: 'previewMedia',
components:{},
data() {
return {
winH: '300px',
id: 'dpPlayerDom',
player: null
}

},
created() {
const winH = window.innerHeight
this.winH = winH + 'px'
},
mounted() {
this.getFileUrl()
},
methods: {

async getFileUrl() {
try {
const filePath = getParam('filePath')
const type = getParam('type') ? parseInt(getParam('type')) : 1
if (!filePath) return
const params = {
objectKey: filePath,
type
}
const data = await getBucketObsFileUrl(params);
this.setVedioplayerConfig(data)
} catch (e) {
console.error(e)
}
},

setVedioplayerConfig(url) {
if (!url) return

const tmpConfig = {
container: document.getElementById('dplayer'),
screenshot: false,
video: {
url: url,
thumbnails: 'thumbnails.jpg',
},
contextmenu: []

}

this.$nextTick(() => {
tmpConfig.container = document.getElementById(this.id)
const dpPlayer = new DPlayer(tmpConfig);
this.player = dpPlayer

dpPlayer.fullScreen.request('web');
})

}
}
}
</script>

<style scoped lang="scss">
.vedio-wrapper {
width: 400px;
height: 100%;
margin: 0 auto;
}
</style>



作者:山间板栗
来源:juejin.cn/post/7355456165244239912
收起阅读 »

布局升级秘籍:掌握CSS Grid网格布局,打造响应式网页设计

随着现代网页设计的不断演进,传统的布局方式已经逐渐不能满足设计师和开发者们对于高效、灵活且强大布局系统的追求。而CSS Grid网格布局,正是在这样的背景下应运而生的。今天,我们就来深入探讨CSS Grid布局的魅力所在,带你解锁这项强大的设计工具,让网页布局...
继续阅读 »

随着现代网页设计的不断演进,传统的布局方式已经逐渐不能满足设计师和开发者们对于高效、灵活且强大布局系统的追求。而CSS Grid网格布局,正是在这样的背景下应运而生的。

今天,我们就来深入探讨CSS Grid布局的魅力所在,带你解锁这项强大的设计工具,让网页布局变得更加简单和高效。

一、什么是CSS Grid布局?

CSS Grid布局,简称为Grid,是CSS的一个二维布局系统,它能够处理行和列,使得网页布局变得更加直观和强大。与传统的布局方式相比,Grid能够轻松实现复杂的页面结构,而无需繁琐的浮动、定位或是使用多个嵌套容器。

Grid网格布局是一种基于网格的布局系统,它允许我们通过定义行和列的大小、位置和排列方式来创建复杂的网页布局。

Description

这与之前讲到的flex一维布局不相同。

设置display:grid/inline-grid的元素就是网格布局容器,这样就能触发浏览器渲染引擎的网格布局算法。

<div>
<div class="item item-1">
<p></p >
</div>
<div class="item item-2"></div>
<div class="item item-3"></div>
</div>

上述代码实例中,.container元素就是网格布局容器,.item元素就是网格的项目,由于网格元素只能是容器的顶层子元素,所以p元素并不是网格元素。

二、Grid的基本概念

首先,我们来了解一下CSS Grid布局的核心概念:

容器(Container):

设置了display: grid;的元素成为容器。它是由一组水平线和垂直线交叉构成,就如同我们所在的地区是由小区和各个路构成。

项目(Item):

容器内的直接子元素,称为项目。

网格线(Grid Lines):

划分行和列的线条,可以想象成坐标轴。正常情况下n行会有n+1根横向网格线,m列有m+1根纵向网格线。比如田字就好像是一个三条水平线和三条垂直线构成的网格元素。

Description

上图是一个 2 x 3 的网格,共有3根水平网格线和4根垂直网格线。

行:

即两个水平网格线之间的空间,也就是水平轨道,就好比我们面朝北边东西方向横向排列的楼房称为行。

列:

即两个垂直网格线之间的空间,也就是垂直轨道,也就是南北方向排列的楼房。

单元格:

由水平线和垂直线交叉构成的每个区域称为单元格,网络单元格是CSS网格中的最小单元。也就是说东西和南北方向的路交叉后划分出来的土地区域。

网格轨道(Grid Tracks):

两条相邻网格线之间的空间。

网格区域(Grid Area):

四条网格线围成的空间,可以是行或列。本质上,网格区域一定是矩形的。例如,不可能创建T形或L形的网格区域。

三、Grid的主要属性

CSS Grid网格布局的主要属性包括:

  • display:设置元素为网格容器或网格项。

  • grid-template-columns 和 grid-template-rows:用于定义网格的列和行的大小。

  • grid-column-gap 和 grid-row-gap:用于定义网格的列和行的间距。

  • grid-template-areas:用于定义命名区域,以便在网格中引用。

  • grid-auto-flow:用于控制网格项的排列方式,可以是行(row)或列(column)。

  • grid-auto-columns 和 grid-auto-rows:用于定义自动生成的列和行的大小。

  • grid-column-start、grid-column-end、grid-row-start 和 grid-row-end:用于定义网格项的位置。

  • justify-items、align-items 和 place-items:用于对齐网格项。

  • grid-template:一个复合属性,用于一次性定义多个网格布局属性。

下面将详细介绍这些属性的概念及作用:

3.1 display

通过给元素设置:display:grid | inline-grid,可以让一个元素变成网格布局元素。

语法:

display: grid | inline-grid;

display: grid:表示把元素定义为块级网格元素,单独占一行;

display:inline-grid:表示把元素定义为行内块级网格元素,可以和其他块级元素在同一行。

3.2 grid-template-columns和grid-template-rows

grid-template-columns和grid-template-rows:用于定义网格的列和行的大小。

  • grid-template-columns 属性设置列宽

  • grid-template-rows 属性设置行高

.wrapper {
display: grid;
/* 声明了三列,宽度分别为 200px 200px 200px */
grid-template-columns: 200px 200px 200px;
grid-gap: 5px;
/* 声明了两行,行高分别为 50px 50px */
grid-template-rows: 50px 50px;
}

以上表示固定列宽为 200px 200px 200px,行高为 50px 50px。

上述代码可以看到重复写单元格宽高,我们也可以通过使用repeat()函数来简写重复的值。

  • 第一个参数是重复的次数

  • 第二个参数是重复的值

所以上述代码可以简写成:

.wrapper {
display: grid;
grid-template-columns: repeat(3,200px);
grid-gap: 5px;
grid-template-rows:repeat(2,50px);
}

除了上述的repeact关键字,还有:

auto-fill: 表示自动填充,让一行(或者一列)中尽可能的容纳更多的单元格。

grid-template-columns: repeat(auto-fill, 200px)

表示列宽是 200 px,但列的数量是不固定的,只要浏览器能够容纳得下,就可以放置元素。

fr: 片段,为了方便表示比例关系。

grid-template-columns: 200px 1fr 2fr

表示第一个列宽设置为 200px,后面剩余的宽度分为两部分,宽度分别为剩余宽度的 1/3 和 2/3。

minmax: 产生一个长度范围,表示长度就在这个范围之中都可以应用到网格项目中。第一个参数就是最小值,第二个参数就是最大值。

minmax(100px, 1fr)

表示列宽不小于100px,不大于1fr。

auto: 由浏览器自己决定长度。

grid-template-columns: 100px auto 100px

表示第一第三列为 100px,中间由浏览器决定长度。

3.3 grid-row-gap 属性, grid-column-gap 属性, grid-gap 属性

grid-column-gap和grid-row-gap,用于定义网格的列间距和行间距。grid-gap 属性是两者的简写形式。

  • grid-row-gap: 10px 表示行间距是 10px

  • grid-column-gap: 20px 表示列间距是 20px

  • grid-gap: 10px 20px 等同上述两个属性

3.4 grid-auto-flow 属性

grid-auto-flow,用于控制网格项的排列方式,可以是行(row)或列(column)。

  • 划分网格以后,容器的子元素会按照顺序,自动放置在每一个网格。

  • 顺序就是由grid-auto-flow决定,默认为行,代表"先行后列",即先填满第一行,再开始放入第二行。

Description

当修改成column后,放置变为如下:

Description

3.5 justify-items 属性, align-items 属性, place-items 属性

justify-items、align-items和place-items,用于定义网格项目的对齐方式。

  • justify-items 属性设置单元格内容的水平位置(左中右)

  • align-items 属性设置单元格的垂直位置(上中下)

.container {
justify-items: start | end | center | stretch;
align-items: start | end | center | stretch;
}

属性对应如下:

  • start:对齐单元格的起始边缘

  • end:对齐单元格的结束边缘

  • center:单元格内部居中

  • stretch:拉伸,占满单元格的整个宽度(默认值)

  • place-items属性是align-items属性和justify-items属性的合并简写形式。

3.6 justify-content 属性, align-content 属性, place-content 属性

  • justify-content属性是整个内容区域在容器里面的水平位置(左中右)

  • align-content属性是整个内容区域的垂直位置(上中下)

.container {
justify-content: start | end | center | stretch | space-around | space-between | space-evenly;
align-content: start | end | center | stretch | space-around | space-between | space-evenly;
}

两个属性的写法完全相同,都可以取下面这些值:

  • start - 对齐容器的起始边框

  • end - 对齐容器的结束边框

  • center - 容器内部居中

Description

  • space-around - 每个项目两侧的间隔相等。所以,项目之间的间隔比项目与容器边框的间隔大一倍。

  • space-between - 项目与项目的间隔相等,项目与容器边框之间没有间隔。

  • space-evenly - 项目与项目的间隔相等,项目与容器边框之间也是同样长度的间隔。

  • stretch - 项目大小没有指定时,拉伸占据整个网格容器。

Description

3.7 grid-auto-columns 属性和 grid-auto-rows 属性

有时候,一些项目的指定位置,在现有网格的外部,就会产生显示网格和隐式网格。

比如网格只有3列,但是某一个项目指定在第5行。这时,浏览器会自动生成多余的网格,以便放置项目。超出的部分就是隐式网格。

而grid-auto-rows与grid-auto-columns就是专门用于指定隐式网格的宽高。

3.8 grid-column-start 属性、grid-column-end 属性、grid-row-start 属性以及grid-row-end 属性

指定网格项目所在的四个边框,分别定位在哪根网格线,从而指定项目的位置。

  • grid-column-start 属性:左边框所在的垂直网格线

  • grid-column-end 属性:右边框所在的垂直网格线

  • grid-row-start 属性:上边框所在的水平网格线

  • grid-row-end 属性:下边框所在的水平网格线

<style>
#container{
display: grid;
grid-template-columns: 100px 100px 100px;
grid-template-rows: 100px 100px 100px;
}
.item-1 {
grid-column-start: 2;
grid-column-end: 4;
}
</style>

<div id="container">
<div class="item item-1">1</div>
<div class="item item-2">2</div>
<div class="item item-3">3</div>
</div>

通过设置grid-column属性,指定1号项目的左边框是第二根垂直网格线,右边框是第四根垂直网格线。

Description

3.9 grid-area 属性

grid-area 属性指定项目放在哪一个区域。

.item-1 {
grid-area: e;
}

意思为将1号项目位于e区域

grid-area属性一般与上述讲到的grid-template-areas搭配使用。

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

点这里前往学习哦!

3.10 justify-self 属性、align-self 属性以及 place-self 属性

  • justify-self属性设置单元格内容的水平位置(左中右),跟justify-items属性的用法完全一致,但只作用于单个项目。

  • align-self属性设置单元格内容的垂直位置(上中下),跟align-items属性的用法完全一致,也是只作用于单个项目。

.item {
justify-self: start | end | center | stretch;
align-self: start | end | center | stretch;
}

这两个属性都可以取下面四个值。

  • start:对齐单元格的起始边缘。

  • end:对齐单元格的结束边缘。

  • center:单元格内部居中。

  • stretch:拉伸,占满单元格的整个宽度(默认值)

四、Grid网格布局应用场景

CSS Grid网格布局的应用场景非常广泛,包括但不限于:

创建复杂的网页布局:

CSS Grid网格布局可以轻松创建出复杂的网页布局,如多列布局、不规则布局等。

创建响应式设计:

CSS Grid网格布局可以轻松实现响应式设计,通过调整网格的大小和间距,可以适应不同的屏幕尺寸。

创建复杂的组件布局:

CSS Grid网格布局也可以用于创建复杂的组件布局,如卡片布局、轮播图布局等。

总的来说,CSS Grid网格布局是一种强大的布局工具,可以帮助网页设计者轻松创建出各种复杂的网页布局。

CSS Grid布局为我们提供了一个全新的视角来思考页面布局,它让复杂布局的实现变得简单明了。随着浏览器支持度的提高,未来的网页设计将更加灵活和富有创意。

掌握了CSS Grid布局,你就已经迈出了成为前端设计高手的重要一步。不断实践,不断探索,你会发现更多Grid的神奇之处。

收起阅读 »

一次接手外包公司前端代码运行踩坑过程

web
背景 外包项目结束后,代码交给我公司需要存起来,因为后期还会有迭代开发任务,以后的事情肯定是我们公司内部来维护了,那就需要把代码运行起来,这过程中运行前端项目遇到的几个问题和处理过程简单记录下。 主要问题是,外包公司建有自己UI组件库,所有里面很多包是他们np...
继续阅读 »

背景


外包项目结束后,代码交给我公司需要存起来,因为后期还会有迭代开发任务,以后的事情肯定是我们公司内部来维护了,那就需要把代码运行起来,这过程中运行前端项目遇到的几个问题和处理过程简单记录下。


主要问题是,外包公司建有自己UI组件库,所有里面很多包是他们npm私有仓库的托管,我们无法访问到他们的私服仓库,思路是从 node_modules中 把私有包迁移到我们公司自己内网仓库


代码


我拿到的两个项目代码,共有两个项目代码,下面这是web的代码,处理思路是一样的


image.png


第一步运行看是否正常


因为观察到项目中有 node_modules ,因为外包公司是把整个项目文件都拷贝过来了,里面还包括 .git 目录,如果能直接运行起来,那万事大吉。


显示看下图,是运行报错的,缺少包和相关命令,所以我们还是得自己来重新安装 node_modules ,但是问题是私有包如何解决?


image.png


第二次尝试重新安装包


我们尝试重新直接安装包,安装失败,因为访问不到私有仓库域名


image.png


正式迁移包


我们公司也是用verdaccio搭建过私有仓库的,所以要把外包项目的私有包上传到我们公司内部



  • package.json中找到私有包

  • 拷贝私有包成独立项目

  • 推送到我们公司内部verdaccio仓库(没有私有仓库就传到npm上也一样,但是外包公司自己的包还是别外传)

  • 项目中配置.npmrc锁定包来源

  • 锁定项目中版本号


package.json中找到私有包


通过判定看到下图的包在 http://www.npmjs.com/ 中查找不到,所以下面这些 @iios前缀的包是需要迁移到包


image.png


拷贝私有包成独立项目



我们从 node_modules 中拷贝出来这些文件夹



image.png



观察到所有包都是完整的,都有package.json文件



image.png


推送到我们公司内部verdaccio仓库


这里这么多包,如果简化可以使用lerna或者shell脚本来统一处理版本问题,但是我们简化就按个包执行推送命令即可


image.png


后续所有包同理操作即可


image.png


后面就不一一列举了,检查verdaccio是否推送成功


image.png


项目中配置.npmrc锁定包来源


现在私有包都上传完成了,所以需要回到主项目,安装包就行了,但是因为有私有包,于是需要执行 .npmrc 规定各种包的安装路径


image.png


锁定项目中版本号


这一步是我习惯,在package.json中,版本号固定写死,而不是 ^前缀开头自动更新此版本


而且更重要的是,外包项目已经在线上运行,万一以后要三方包变化导致一些莫名其妙问题就很麻烦,锁定版本号是非常有必要的,才能以后很久之后打开发布代码也是没有问题的


image.png


image.png


删除node_modules 和 yarn.lock ,重新安装包


image.png


image.png


重新运行


一切都搞完了,重新运行成功


image.png


image.png


作者:一个不会重复的id
来源:juejin.cn/post/7348090716578824230
收起阅读 »

弱智吧成最好中文AI训练数据:大模型变聪明,有我一份贡献

web
在中文网络上流传着这样一段话:弱智吧里没有弱智。百度「弱智吧」是个神奇的地方,在这里人人都说自己是弱智,但大多聪明得有点过了头。最近几年,弱智吧的年度总结文章都可以顺手喜提百度贴吧热度第一名。所谓总结,其实就是给当年吧里的弱智发言排个名。各种高质量的段子在这里...
继续阅读 »


在中文网络上流传着这样一段话:弱智吧里没有弱智。

百度「弱智吧」是个神奇的地方,在这里人人都说自己是弱智,但大多聪明得有点过了头。最近几年,弱智吧的年度总结文章都可以顺手喜提百度贴吧热度第一名。所谓总结,其实就是给当年吧里的弱智发言排个名。

各种高质量的段子在这里传入传出,吸引了无数人的围观和转载,这个贴吧的关注量如今已接近 300 万。你网络上看到的最新流行词汇,说不定就是弱智吧老哥的杰作。

随着十几年的发展,越来越多的弱智文学也有了奇怪的风格,有心灵鸡汤,有现代诗,甚至有一些出现了哲学意义。

最近几天,一篇人工智能领域论文再次把弱智吧推上了风口浪尖。

引发 AI 革命的大模型因为缺乏数据,终于盯上了弱智吧里无穷无尽的「数据集」。有人把这些内容拿出来训练了 AI,认真评测对比一番,还别说,效果极好。

接下来,我们看看论文讲了什么。
最近,大型语言模型(LLM)取得了重大进展,特别是在英语方面。然而,LLM 在中文指令调优方面仍然存在明显差距。现有的数据集要么以英语为中心,要么不适合与现实世界的中国用户交互模式保持一致。 
为了弥补这一差距,一项由 10 家机构联合发布的研究提出了 COIG-CQIA(全称 Chinese Open Instruction Generalist - Quality Is All You Need),这是一个高质量的中文指令调优数据集。数据来源包括问答社区、维基百科、考试题目和现有的 NLP 数据集,并且经过严格过滤和处理。
此外,该研究在 CQIA 的不同子集上训练了不同尺度的模型,并进行了深入的评估和分析。本文发现,在 CQIA 子集上训练的模型在人类评估以及知识和安全基准方面取得了具有竞争力的结果。
研究者表示,他们旨在为社区建立一个多样化、广泛的指令调优数据集,以更好地使模型行为与人类交互保持一致。
本文的贡献可以总结如下:

提出了一个高质量的中文指令调优数据集,专门用于与人类交互保持一致,并通过严格的过滤程序实现;

探讨了各种数据源(包括社交媒体、百科全书和传统 NLP 任务)对模型性能的影响。为从中国互联网中选择训练数据提供了重要见解;

各种基准测试和人工评估证实,在 CQIA 数据集上微调的模型表现出卓越的性能,从而使 CQIA 成为中国 NLP 社区的宝贵资源。


  • 论文地址:https://arxiv.org/pdf/2403.18058.pdf
  • 数据地址:https://huggingface.co/datasets/m-a-p/COIG-CQIA
  • 论文标题:COIG-CQIA: Quality is All You Need for Chinese Instruction Fine-tuning


COIG-CQIA 数据集介绍

为了保证数据质量以及多样性,本文从中国互联网内的优质网站和数据资源中手动选择了数据源。这些来源包括社区问答论坛、、内容创作平台、考试试题等。此外,该数据集还纳入了高质量的中文 NLP 数据集,以丰富任务的多样性。具体来说,本文将数据源分为四种类型:社交媒体和论坛、世界知识、NLP 任务和考试试题。


社交媒体和论坛:包括知乎、SegmentFault 、豆瓣、小红书、弱智吧。

世界知识:百科全书、四个特定领域的数据(医学、经济管理、电子学和农业)。

NLP 数据集:COIG-PC 、COIG Human Value 等。

考试试题:中学和大学入学考试、研究生入学考试、逻辑推理测试、中国传统文化。
表 1 为数据集来源统计。研究者从中国互联网和社区的 22 个来源总共收集了 48,375 个实例,涵盖从常识、STEM 到人文等领域。

图 2 说明了各种任务类型,包括信息提取、问答、代码生成等。

图 3 演示了指令和响应的长度分布。

为了分析 COIG-CQIA 数据集的多样性,本文遵循先前的工作,使用 Hanlp 工具来解析指令。

实验结果

该研究在不同数据源的数据集上对 Yi 系列模型(Young et al., 2024)和 Qwen-72B(Bai et al., 2023)模型进行了微调,以分析数据源对模型跨领域知识能力的影响,并使用 Belle-Eval 上基于模型(即 GPT-4)的自动评估来评估每个模型在各种任务上的性能。
表 2、表 3 分别显示了基于 Yi-6B、Yi-34B 在不同数据集上进行微调得到的不同模型的性能。模型在头脑风暴、生成和总结等生成任务中表现出色,在数学和编码方面表现不佳。


下图 4 显示了 CQIA 和其他 5 个基线(即 Yi-6B-Chat、Baichuan2-7B-Chat、ChatGLM2-6B、Qwen-7B-Chat 和 InternLM-7B-Chat)的逐对比较人类评估结果。结果表明,与强基线相比,CQIA-Subset 实现了更高的人类偏好,至少超过 60% 的响应优于或与基线模型相当。这不仅归因于 CQIA 能够对人类问题或指令生成高质量的响应,还归因于其响应更符合现实世界的人类沟通模式,从而导致更高的人类偏好。

该研究还在 SafetyBench 上评估了模型的安全性,结果如下表 4 所示:

在 COIG Subset 数据上训练的模型性能如下表 5 所示:





作者:APPSO
来源:mp.weixin.qq.com/s/BN52IrDg-xNosxkJ6MbNvA
收起阅读 »

Geoserver:小程序巨丝滑渲染海量点位

web
文章最后有效果图 需求 在小程序上绘制 40000+ 的点位。 难点 众所周知小程序的 map 组件性能低下,同时渲染几百个 marker 就会卡顿,一旦加上 callout 弹窗,这个数量可能会降到几十个,如果使用了 自定义弹窗(custom-callou...
继续阅读 »

文章最后有效果图



需求


在小程序上绘制 40000+ 的点位。


难点


众所周知小程序的 map 组件性能低下,同时渲染几百个 marker 就会卡顿,一旦加上 callout 弹窗,这个数量可能会降到几十个,如果使用了 自定义弹窗(custom-callout) 会更卡,所以渲染 4w+ 的点,用常规方法是不可能实现的。


方案


按需加载


按需加载即按屏幕坐标加载,只显示视野范围内的点位,需要后端配合在接口中新增 bbox(Bounding box) 参数,再从数据库中查出范围内的点。


小程序端需要使用视野变化监听方法实时更新,虽然请求和渲染频繁,但是在缩放等级较大时,有很高的性能:


<map bindregionchange="regionChanged" markers="{{markers}}">

regionChanged(e){
this.data.bbox = [     [e.detail.region.southwest.longitude, e.detail.region.southwest.latitude],
    [e.detail.region.northeast.longitude, e.detail.region.northeast.latitude],
  ]
   // 执行获取点、渲染点的操作
}

需要注意的是,目前的微信版本(8.0.47),基础库3.3.4该方法不可用,见 微信开放社区


如果遇到 bindregionchange 不可用时,可以用 bind:touchend 方法代替,手动获取范围


    setBbox() {
     mapCtx = wx.createMapContext('map', this)
     mapCtx.getRegion({
       success: (res) => {
         let bbox = [
          [res.southwest.longitude, res.southwest.latitude],
          [res.northeast.longitude, res.northeast.latitude],
        ]
         // 执行获取点、渲染点的操作
        })
    })
  }

使用了按需渲染后,在缩放等级较大时,已经可以有很好的效果,移动屏幕时基本可以秒加载出新的点,同时清除掉屏幕范围外的点。


然而,在点位多的时候,我们收到了 setData 长度超出的报错,页面也异常卡顿。


优化渲染方式


小程序的 setData 方法最多只能更新 1M 的数据,超过这个数据会报错,并严重卡顿,即使不超过,在数据量较大时,也会非常卡顿,为了解决这个问题,我们不能再使用 setData 去渲染数据。


小程序提供了专门渲染点的方法: addMarkers


// 执行获取点、渲染点的操作处,使用该方法,并设置 clear: true 。这样就达到了上面说的,更新点时,旧的点会被清除。


然而,这并没有解决根本问题,我们现在可以做到渲染远远大于1M的数据,并渲染时不会报错,但是由于小程序 map 组件的渲染策略,我们的点会一个一个渲染上去,我们知道更新 canvas 代价是很大的,尤其是像 marker 这种携带很多必要信息的东西。


这里我们尝试将 marker 携带的参数压缩到极致,仅保留经纬度、颜色状态信息、id、callout,效果依然差强人意。


并且,由于小程序 marker 的 callout 不是互斥的,且没有给我们预留参数去设置这一点,所以在我们切换 marker 选中状态时,需要把 marker 数组完全遍历一遍,移除其他的 callout , 并添加新的 callout,这个开销也是巨大不可接受的。


优化选中策略


为了解决切换 marker 选中状态时的开销问题,我们想了一个绝妙的主意,就是将 marker 数组中的 callout 完全移除,只保留 id 等必要字段,在点击时,添加一个新的带 callout 点上去,盖住原来的点,这样看起来就是原有的点被选中了,这样既压缩了 marker 携带参数,又解决了切换选中时必须遍历 marker 数组的问题。


height: 20,
width: 17,
iconPath: this.data.markerIcons[this.getMarkerType(item)],
latitude: item.point[1],
longitude: item.point[0],
id: this.getUniqueNumber(item.uid), //id 必须是数字
storeCode: item.uid,
//callout:{...} // 不要此项
customCallout: {} //必须加,不然会有一个没有内容的弹窗,这个可以阻止默认弹窗弹出

优化海量点渲染策略


经过上面的优化,我们的小程序已经可以高性能的显示点位了,但是当缩放等级低时(12以下),点位多起来了,我们目前的方法就显得力不从心了。


如果点位无限多,我们又该如何优化呢?


聚合


聚合指的是将临近的点位聚合成一个大点,从而达到渲染点数变少、提高性能的方法。


此方法经过实测,发现当点达到一定量级的时候,用了反而比不用还卡,因为每当你缩放地图时,都需要计算聚合,当计算压力大于渲染压力时,聚合反而成了一种负担,而不是优化了。


所以我们不用聚合。


小程序个性化图层


小程序提供了付费功能:个性化图层,可以上传海量数据并生成一个小程序支持加载的图层。遗憾的是这种方法只适合静态数据,对于经常需要变动的数据,这种方法的实时性得不到保证,只能通过手动在后台更新数据。


所以此方案也不可用。


瓦片


小程序 map 是不支持瓦片(个性化图层除外)加载的,但是我们知道,瓦片就是一张图片而已,那么小程序可以在地图上放图片吗,答案是可以:addGroundOverlay


我们决定朝着此方案努力,请看下文。


搭建 geoserver


首先到 geoserver官网 下载geoserver本体,geoserver是为数不多几个推荐 windows 平台的大型工具软件,下载前注意,geoserver对 jdk 版本有要求,版本不一致会导致 geoserver 启动失败等问题。


image.png
我们的服务器是 linux ,所以下载了linux版本,到服务器找个位置 直接 unzip 就可以了。


安装完之后,需要先编辑 start.ini 调整一个合适的空闲端口,作为后面web端管理页面的地址端口。别忘了在防火墙开启此端口。


最后在 bin 中有一个 startup.sh , 使用 nohup 命令设置后台运行。


此时在浏览器输入服务器地址和你刚刚设置的端口号,最后加上 /geoserver,即可看到geoserver的管理页面。


image.png
初始用户名密码:admin geoserver


登录完成后可以看到全部功能


image.png
点击数据存储 -> 添加新的数据存储,即可添加数据并发布图层。


可以看到支持 PostGis,使用 PostGis 作为数据源,图层会实时更新,也就是说,当数据变化时,无需任何代码和人工干预。


当数据源添加完成后,需要新建一个图层,并指定为刚刚新建的数据源。


此时,在图层预览页面即可看到刚刚创建的图层了,当然此时的图层使用的是默认样式,需要编写SLD(xml格式)的样式文件去指定样式,这对于我们来说无疑是一种负担。


好在 geoserver 有 css 插件,安装此插件并重启geoserver,即可使用 css 编写图层样式。


* {
 mark-size:8px;
}
[control_sts == 1] {
 mark:url("https://entropy.xxx.cn/xx/dotgreen.png");
}
[control_sts == 0] {
 mark:url("https://entropy.xxx.cn/xx/dotgray.png");
}

可以看到,它与标准css还是有一些差异的,像mark、mark-size在标准css中是不存在的。


指定样式后,在图层预览页面,可以看到效果


image.png


打开控制台,可以看到网络请求中的地址长这样:


http://xxx:8089/geoserver/cite/wms?SERVICE=WMS&VERSION=1.1.1&REQUEST=GetMap&FORMAT=image%2Fpng&TRANSPARENT=true&STYLES&LAYERS=cite%3Axc_store_geo&exceptions=application%2Fvnd.ogc.se_inimage&SRS=EPSG%3A4326&WIDTH=670&HEIGHT=768&BBOX=114.4720458984375%2C37.7874755859375%2C118.1524658203125%2C42.0062255859375

放到浏览器窗口打开,发现是一张png图片,那么我们刚好可以使用小程序的 addGroundOverlay 添加到地图上。


SERVICE: WMS
VERSION: 1.1.1
REQUEST: GetMap
FORMAT: image/png
TRANSPARENT: true
STYLES:
LAYERS: xx:xxxx
exceptions: application/vnd.ogc.se_inimage
SRS: EPSG:4326
WIDTH: 670
HEIGHT: 768
BBOX: 114.4720458984375,37.7874755859375,118.1524658203125,42.0062255859375

看一下这些参数,出了 BBOX ,其他的写固定值就可以了。


这里注意,宽高值,需要设置为小程序中地图元素的大小,单位是 px。


在小程序中拼装WMS地址


比较简单,直接看代码:


    setTileImage(params: { LAYERS: string[], BBOX: string, SCREEN_WIDTH: number, SCREEN_HEIGHT: number, CQL_FILTER: string }) {
     mapCtx = wx.createMapContext('map', this)
     this.removeTileImage().then(() => {
       for (let index in params.LAYERS) {
         let id = +(9999 + index)
         !this.data.groundOverlayIds.includes(id) && this.data.groundOverlayIds.push(id)
         let data: any = {
           id: +(9999 + index),
           zIndex: 999,
           src: `http://xxx:8089/geoserver/cite/wms?SERVICE=WMS&VERSION=1.1.1&REQUEST=GetMap&LAYERS=${params.LAYERS[index]}&STYLES=&exceptions=application/vnd.ogc.se_inimage&FORMAT=image/png&TRANSPARENT=true&FORMAT_OPTIONS=antialias:full&SRS=EPSG:4326&BBOX=${params.BBOX}&WIDTH=${params.SCREEN_WIDTH * 2}&HEIGHT=${params.SCREEN_HEIGHT * 2}&CQL_FILTER=${params.CQL_FILTER}`,
           bounds: {
             southwest: {
               latitude: +params.BBOX.split(',')[1],
               longitude: +params.BBOX.split(',')[0]
            },
             northeast: {
               latitude: +params.BBOX.split(',')[3],
               longitude: +params.BBOX.split(',')[2]
            }
          }
        }
         mapCtx.addGroundOverlay({
           ...data,
        })
      }
    })
  },

我这里封装了一个可以接受多个图层的方法,这里值得注意的是,我没有使用 updateGroundOverlay 方法去更新图层,而是先使用 removeGroundOverlay 移除,再重新添加的,这是因为updateGroundOverlay有一个bug,我不说,你可以自己试试。


完成


f42f89c10e3c044f3b8e0200d7dfa52a.webp
至此已经完全实现了小程序的海量点的渲染,无论点有多少,我们都只需要渲染一张图片而已,性能好的一批。


作者:德莱厄斯
来源:juejin.cn/post/7348363874965028864
收起阅读 »

统一公司的项目规范

web
初始化项目 vscode 里下好插件:eslint,prettier,stylelint 官网模版创建项目:pnpm create vite react-starter --template react-swc-ts 安装依赖:pnpm i 后面有可能遇到 ...
继续阅读 »

初始化项目



  • vscode 里下好插件:eslint,prettier,stylelint

  • 官网模版创建项目:pnpm create vite react-starter --template react-swc-ts

  • 安装依赖:pnpm i

  • 后面有可能遇到 ts 类型错误,可以提前安装一个pnpm i @types/node -D


配置 npm 使用淘宝镜像



  • 配置npmrc


    registry = "https://registry.npmmirror.com/"



配置 node 版本限制提示



  • package.json 中配置


    "engines": {
    "node": ">=16.0.0"
    },



配置 eslint 检查代码规范



eslint 处理代码规范,prettier 处理代码风格
eslint 选择只检查错误不处理风格,这样 eslint 就不会和 prettier 冲突
react 官网有提供一个 hook 的 eslint (eslint-plugin-react-hooks),用处不大就不使用了




  • 安装:pnpm i eslint -D

  • 生成配置文件:eslint --init(如果没eslint,可以全局安装一个,然后使用npx eslint --init)


    - To check syntax and find problems  //这个选项是eslint默认选项,这样就不会和pretter起风格冲突
    - JavaScript modules (import/export)
    - React
    - YES
    - Browser
    - JSON
    - Yes
    - pnpm


  • 配置eslintrc.json->rules里配置不用手动引入 react,和配置不可以使用 any

  • 注意使用 React.FC 的时候如果报错说没有定义 props 类型,那需要引入一下 react


    "rules": {
    //不用手动引入react
    "react/react-in-jsx-scope": "off",
    //使用any报错
    "@typescript-eslint/no-explicit-any": "error",
    }


  • 工作区配置.vscode>settings.json,配置后 vscode 保存时自动格式化代码风格


    比如写了一个 var a = 100,会被自动格式化为 const a = 100


    {
    "editor.codeActionsOnSave": {
    // 每次保存的时候将代码按照 eslint 格式进行修复
    "source.fixAll.eslint": true,
    //自动格式化
    "editor.formatOnSave": true
    }
    }


  • 配置.eslintignore,eslint 会自动过滤 node_modules


    dist


  • 掌握eslint格式化命令,后面使用 lint-staged 提交代码的时候需要配置


    为什么上面有 vscode 自动 eslint 格式化,还需要命令行: 因为命令行能一次性爆出所有警告问题,便于找到位置修复


    npx eslint . --fix//用npx使用项目里的eslint,没有的话也会去使用全局的eslint
    eslint . --fix //全部类型文件
    eslint . --ext .ts,.tsx --fix //--ext可以指定文件后缀名s

    eslintrc.json 里配置



  • "env": {
    "browser": true,
    "es2021": true,
    "node": true // 因为比如配置vite的时候会使用到
    },



配置 prettier 检查代码风格



prettier 格式化风格,因为使用 tailwind,使用 tailwind 官方插件




  • 安装:pnpm i prettier prettier-plugin-tailwindcss -D

  • 配置.prettierrc.json


    注释要删掉,prettier 的配置文件 json 不支持注释


    {
    "singleQuote": true, // 单引号
    "semi": false, // 分号
    "trailingComma": "none", // 尾随逗号
    "tabWidth": 2, // 两个空格缩进
    "plugins": ["prettier-plugin-tailwindcss"] //tailwind插件
    }


  • 配置.prettierignore


    dist
    pnpm-lock.yaml


  • 配置.vscode>settings.json,配置后 vscode 保存时自动格式化代码风格


    {
    "editor.codeActionsOnSave": {
    // 每次保存的时候将代码按照 eslint 格式进行修复
    "source.fixAll.eslint": true
    },
    //自动格式化
    "editor.formatOnSave": true,
    //风格用prettier
    "editor.defaultFormatter": "esbenp.prettier-vscode"
    }


  • 掌握prettier命令行


    可以让之前没有格式化的错误一次性暴露出来


    npx prettier --write .//使用Prettier格式化所有文件



配置 husky 使用 git hook



记得要初始化一个 git 仓库,husky 能执行 git hook,在 commit 的时候对文件进行操作




  • 安装


    sudo pnpm dlx husky-init


    pnpm install


    npx husky add .husky/commit-msg 'npx --no -- commitlint --edit "$1"',commit-msg 使用 commitlint


    npx husky add .husky/pre-commit "npm run lint-staged",pre-commit 使用 lint-staged



配置 commitlint 检查提交信息



提交规范参考:http://www.conventionalcommits.org/en/v1.0.0/




  • 安装pnpm i @commitlint/cli @commitlint/config-conventional -D

  • 配置.commitlintrc.json


    { extends: ['@commitlint/config-conventional'] }



配置 lint-staged 增量式检查



  • 安装pnpm i -D lint-staged

  • 配置package.json


    "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "preview": "vite preview",
    "prepare": "husky install",
    "lint-staged": "npx lint-staged"//新增,对应上面的husky命令
    },


  • 配置.lintstagedrc.json


    {
    "*.{ts,tsx,json}": ["prettier --write", "eslint --fix"],
    "*.css": ["stylelint --fix", "prettier --write"]
    }



配置 vite(代理/别名/drop console 等)



如果有兼容性考虑,需要使用 legacy 插件,vite 也有 vscode 插件,也可以下载使用




  • 一些方便开发的配置


    import { defineConfig } from 'vite'
    import react from '@vitejs/plugin-react-swc'
    import path from 'path'

    // https://vitejs.dev/config/
    export default defineConfig({
    esbuild: {
    drop: ['console', 'debugger']
    },
    css: {
    // 开css sourcemap方便找css
    devSourcemap: true
    },
    plugins: [react()],
    server: {
    // 自动打开浏览器
    open: true
    proxy: {
    '/api': {
    target: 'https://xxxxxx',
    changeOrigin: true,
    rewrite: (path) => path.replace(/^\/api/, '')
    }
    }
    },
    resolve: {
    // 配置别名
    alias: { '@': path.resolve(__dirname, './src') }
    },
    //打包路径变为相对路径,用liveServer打开,便于本地测试打包后的文件
    base: './'
    })


  • 配置打包分析,用 legacy 处理兼容性


    pnpm i rollup-plugin-visualizer -D


    pnpm i @vitejs/plugin-legacy -D,实际遇到了再看官网用


    import { defineConfig } from 'vite'
    import react from '@vitejs/plugin-react-swc'
    import { visualizer } from 'rollup-plugin-visualizer'
    import legacy from '@vitejs/plugin-legacy'
    import path from 'path'
    // https://vitejs.dev/config/
    export default defineConfig({
    css: {
    // 开css sourcemap方便找css
    devSourcemap: true
    },
    plugins: [
    react(),
    visualizer({
    open: false // 打包完成后自动打开浏览器,显示产物体积报告
    }),
    //考虑兼容性,实际遇到了再看官网用
    legacy({
    targets: ['ie >= 11'],
    additionalLegacyPolyfills: ['regenerator-runtime/runtime']
    })
    ],
    server: {
    // 自动打开浏览器
    open: true
    },
    resolve: {
    // 配置别名
    alias: { '@': path.resolve(__dirname, './src') }
    },
    //打包路径变为相对路径,用liveServer打开,便于本地测试打包后的文件
    base: './'
    })


  • 如果想手机上看网页,可以pnpm dev --host

  • 如果想删除 console,可以按h去 help 帮助,再按c就可以 clear console


配置 tsconfig



  • tsconfig.json 需要支持别名


    {
    "compilerOptions": {
    "target": "ESNext",
    "useDefineForClassFields": true,
    "lib": ["DOM", "DOM.Iterable", "ESNext"],
    "allowJs": false,
    "skipLibCheck": true,
    "esModuleInterop": false,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "module": "ESNext",
    "moduleResolution": "Node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",
    "baseUrl": "./",
    "paths": {
    "@/*": ["src/*"]
    }
    },
    "include": ["src"],
    "references": [{ "path": "./tsconfig.node.json" }]
    }



配置 router



  • 安装:pnpm i react-router-dom

  • 配置router->index.ts


    import { lazy } from 'react'
    import { createBrowserRouter } from 'react-router-dom'
    const Home = lazy(() => import('@/pages/home'))
    const router = createBrowserRouter([
    {
    path: '/',
    element: <Home></Home>
    }
    ])
    export default router


  • 配置main.tsx


    import { RouterProvider } from 'react-router-dom'
    import ReactDOM from 'react-dom/client'
    import './global.css'
    import router from './router'

    ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
    <RouterProvider router={router} />
    )



配置 zustand 状态管理



  • 安装pnpm i zustand

  • store->index.ts


    import { create } from 'zustand'

    interface appsState {
    nums: number
    setNumber: (nums: number) => void
    }

    const useAppsStore = create<appsState>((set) => ({
    nums: 0,
    setNumber: (num) => {
    return set(() => ({
    nums: num
    }))
    }
    }))

    export default useAppsStore


  • 使用方法


    import Button from '@/comps/custom-button'
    import useAppsStore from '@/store/app'
    const ZustandDemo: React.FC = () => {
    const { nums, setNumber } = useAppsStore()
    const handleNum = () => {
    setNumber(nums + 1)
    }
    return (
    <div className="p-10">
    <h1 className="my-10">数据/更新</h1>
    <Button click={handleNum}>点击事件</Button>
    <h1 className="py-10">{nums}</h1>
    </div>

    )
    }

    export default ZustandDemo



配置 antd



  • 新版本的 antd,直接下载就可以用,如果用到它的图片再单独下载pnpm i antd

  • 注意 antd5 版本的 css 兼容性不好,如果项目有兼容性要求,需要去单独配置


配置 Tailwind css


pnpm i tailwindcss autoprefixer postcss


tailwind.config.cjs


// 打包后会有1kb的css用不到的,没有影响
// 用了antd组件关系也不大,antd5的样式是按需的
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: 'class',
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
theme: {
extend: {
// colors: {
// themeColor: '#ff4132',
// textColor: '#1a1a1a'
// },
// 如果写自适应布局,可以指定设计稿为1000px,然后只需要写/10的数值
// fontSize: {
// xs: '3.3vw',
// sm: '3.9vw'
// }
}
},
plugins: []
}

postcss.config.cjs


module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
}

我喜欢新建一个 apply.css 引入到全局


@tailwind base;
@tailwind components;
@tailwind utilities;

.margin-center {
@apply mx-auto my-0;
}

.flex-center {
@apply flex justify-center items-center;
}

.absolute-center {
@apply absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2;
}

封装 fetch 请求



这个封装仅供参考,TS 类型有点小问题



// 可以传入这些配置
interface BaseOptions {
method?: string
credentials?: RequestCredentials
headers?: HeadersInit
body?: string | null
}

// 请求方式
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD'

// 第一层出参
interface ResponseObject {
ok: boolean
error: boolean
status: number
contentType: string | null
bodyText: string
response: Response
}

// 请求头类型
type JSONHeader = {
Accept: string
'Content-Type': string
}

// 创建类
class Request {
private baseOptions: BaseOptions = {}

// 根据传入的 baseOptions 做为初始化参数
constructor(options?: BaseOptions) {
this.setBaseOptions(options || {})
}

public setBaseOptions(options: BaseOptions): BaseOptions {
this.baseOptions = options
return this.baseOptions
}

// 也提供获取 baseOption 的方法
public getBaseOptions(): BaseOptions {
return this.baseOptions
}

// 核心请求 T 为入参类型,ResponseObject 为出参类型
public request<T>(
method: HttpMethod,
url: string,
data?: T, //支持使用get的时候配置{key,value}的query参数
options?: BaseOptions //这里也有个 base 的 method
): Promise<ResponseObject> {
// 默认 baseOptions
const defaults: BaseOptions = {
method
// credentials: 'same-origin'
}

// 收集最后要传入的配置
const settings: BaseOptions = Object.assign(
{},
defaults,
this.baseOptions,
options
)

// 如果 method 格式错误
if (!settings.method || typeof settings.method !== 'string')
throw Error('[fetch-json] HTTP method missing or invalid.')

// 如果 url 格式错误
if (typeof url !== 'string')
throw Error('[fetch-json] URL must be a string.')

// 支持大小写
const httpMethod = settings.method.trim().toUpperCase()

// 如果是GET
const isGetRequest = httpMethod === 'GET'

// 请求头
const jsonHeaders: Partial<JSONHeader> = { Accept: 'application/json' }

// 如果不是 get 设置请求头
if (!isGetRequest && data) jsonHeaders['Content-Type'] = 'application/json'

// 收集最后的headers配置
settings.headers = Object.assign({}, jsonHeaders, settings.headers)

// 获取query参数的key
const paramKeys = isGetRequest && data ? Object.keys(data) : []

// 获取query参数的值
const getValue = (key: keyof T) => (data ? data[key] : '')

// 获取query key=value
const toPair = (key: string) =>
key + '=' + encodeURIComponent(getValue(key as keyof T) as string)

// 生成 key=value&key=value 的query参数
const params = () => paramKeys.map(toPair).join('&')

// 收集最后的 url 配置
const requestUrl = !paramKeys.length
? url
: url + (url.includes('?') ? '&' : '?') + params()

// get没有body
settings.body = !isGetRequest && data ? JSON.stringify(data) : null

// 做一层res.json()
const toJson = (value: Response): Promise<ResponseObject> => {
// 拿到第一次请求的值
const response = value

const contentType = response.headers.get('content-type')
const isJson = !!contentType && /json|javascript/.test(contentType)

const textToObj = (httpBody: string): ResponseObject => ({
ok: response.ok,
error: !response.ok,
status: response.status,
contentType: contentType,
bodyText: httpBody,
response: response
})

const errToObj = (error: Error): ResponseObject => ({
ok: false,
error: true,
status: 500,
contentType: contentType,
bodyText: 'Invalid JSON [' + error.toString() + ']',
response: response
})

return isJson
? // 如果是json,用json()
response.json().catch(errToObj)
: response.text().then(textToObj)
}

// settings做一下序列化
const settingsRequestInit: RequestInit = JSON.parse(
JSON.stringify(settings)
)

// 最终请求fetch,再通过then就能取到第二层res
return fetch(requestUrl, settingsRequestInit).then(toJson)
}

public get<T>(
url: string,
params?: T,
options?: BaseOptions
): Promise<ResponseObject> {
return this.request<T>('GET', url, params, options)
}

public post<T>(
url: string,
resource: T,
options?: BaseOptions
): Promise<ResponseObject> {
return this.request<T>('POST', url, resource, options)
}

public put<T>(
url: string,
resource: T,
options?: BaseOptions
): Promise<ResponseObject> {
return this.request<T>('PUT', url, resource, options)
}

public patch<T>(
url: string,
resource: T,
options?: BaseOptions
): Promise<ResponseObject> {
return this.request<T>('PATCH', url, resource, options)
}

public delete<T>(
url: string,
resource: T,
options?: BaseOptions
): Promise<ResponseObject> {
return this.request<T>('DELETE', url, resource, options)
}
}

const request = new Request()

export { request, Request }


如果用 axios 请求


request.ts


import axios from 'axios'
import { AxiosInstance } from 'axios'
import { errorHandle, processData, successHandle } from './resInterceptions'
import { defaultRequestInterception } from './reqInterceptions'
const TIMEOUT = 5 * 1000

class Request {
instance: AxiosInstance
constructor() {
this.instance = axios.create()
this.init()
}

private init() {
this.setDefaultConfig()
this.reqInterceptions()
this.resInterceptions()
}
private setDefaultConfig() {
this.instance.defaults.baseURL = import.meta.env.VITE_BASE_URL
this.instance.defaults.timeout = TIMEOUT
}
private reqInterceptions() {
this.instance.interceptors.request.use(defaultRequestInterception)
}
private resInterceptions() {
this.instance.interceptors.response.use(processData)
this.instance.interceptors.response.use(successHandle, errorHandle)
}
}

export default new Request().instance

reqInterceptions.ts


import type { InternalAxiosRequestConfig } from 'axios'

const defaultRequestInterception = (config: InternalAxiosRequestConfig) => {
// TODO: 全局请求拦截器: 添加token
return config
}

export { defaultRequestInterception }

resInterceptions.ts


import { AxiosError, AxiosResponse } from 'axios'
import { checkStatus } from './checkStatus'

const processData = (res: AxiosResponse) => {
// TODO:统一处理数据结构
return res.data
}

const successHandle = (res: AxiosResponse) => {
// TODO:处理一些成功回调,例如请求进度条
return res.data
}

const errorHandle = (err: AxiosError) => {
if (err.status) checkStatus(err.status)
else return Promise.reject(err)
}

export { processData, successHandle, errorHandle }

checkStatus.ts


export function checkStatus(status: number, msg?: string): void {
let errMessage = ''

switch (status) {
case 400:
errMessage = `${msg}`
break
case 401:
break
case 403:
errMessage = ''
break
// 404请求不存在
case 404:
errMessage = ''
break
case 405:
errMessage = ''
break
case 408:
errMessage = ''
break
case 500:
errMessage = ''
break
case 501:
errMessage = ''
break
case 502:
errMessage = ''
break
case 503:
errMessage = ''
break
case 504:
errMessage = ''
break
case 505:
errMessage = ''
break
default:
}
if (errMessage) {
// TODO:错误提示
// createErrorModal({title: errMessage})
}
}

api.ts


import request from '@/services/axios/request'
import { ReqTitle } from './type'

export const requestTitle = (): Promise<ReqTitle> => {
return request.get('/api/一个获取title的接口')
}

type.ts


export type ReqTitle = {
title: string
}

配置 mobx(可不用)



  • 安装pnpm i mobx mobx-react-lite

  • 配置model->index.ts


    import { makeAutoObservable } from 'mobx'

    const store = makeAutoObservable({
    count: 1,
    setCount: (count: number) => {
    store.count = count
    }
    })

    export default store


  • 使用方法举个 🌰


    import store from '@/model'
    import { Button } from 'antd'
    import { observer, useLocalObservable } from 'mobx-react-lite'
    const Home: React.FC = () => {
    const localStore = useLocalObservable(() => store)
    return (
    <div>
    <Button>Antd</Button>
    <h1>{localStore.count}</h1>
    </div>

    )
    }

    export default observer(Home)



配置 changelog(可不用)


pnpm i conventional-changelog-cli -D


第一次先执行conventional-changelog -**p** angular -**i** CHANGELOG.md -s -r 0全部生成之前的提交信息


配置个脚本,版本变化打 tag 的时候可以使用


"scripts": {
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s"
}

配置 editorConfig 统一编辑器(可不用)



editorConfig,可以同步编辑器差异,其实大部分工作 prettier 做了,需要下载 editorConfig vscode 插件
有编辑器差异的才配置一下,如果团队都是 vscode 就没必要了




  • 配置editorconfig


    #不再向上查找.editorconfig
    root = true
    # *表示全部文件
    [*]
    #编码
    charset = utf-8
    #缩进方式
    indent_style = space
    #缩进空格数
    indent_size = 2
    #换行符lf
    end_of_line = lf



配置 stylelint 检查 CSS 规范(可不用)



stylelint 处理 css 更专业,但是用了 tailwind 之后用处不大了




  • 安装:pnpm i -D stylelint stylelint-config-standard

  • 配置.stylelintrc.json


    {
    "extends": "stylelint-config-standard"
    }


  • 配置.vscode>settings.json,配置后 vscode 保存时自动格式化 css


    {
    "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true, // 每次保存的时候将代码按照 eslint 格式进行修复
    "source.fixAll.stylelint": true //自动格式化stylelint
    },
    "editor.formatOnSave": true, //自动格式化
    "editor.defaultFormatter": "esbenp.prettier-vscode" //风格用prettier
    }


  • 掌握stylelint命令行


    npx stylelint "**/*.css" --fix//格式化所有css,自动修复css



下面是 h5 项目(可不用)


配置vconsole(h5)



  • 安装pnpm i vconsole -D

  • main.tsx里新增


    import VConsole from 'vconsole'
    new VConsole({ theme: 'dark' })



antd 换成 mobile antd(h5)



  • pnpm remove antd

  • pnpm add antd-mobile


配置 postcss-px-to-viewport(废弃)



  • 把蓝湖设计稿尺寸固定为 1000px(100px我试过蓝湖直接白屏了),然后你点出来的值比如是 77px,那你只需要写 7.7vw 就实现了自适应布局,就不再需要这个插件了

  • 安装:pnpm i postcss-px-to-viewport -D

  • 配置postcss.config.cjs


    module.exports = {
    plugins: {
    'postcss-px-to-viewport': {
    landscape: false, // 是否添加根据 landscapeWidth 生成的媒体查询条件 @media (orientation: landscape)
    landscapeUnit: 'vw', // 横屏时使用的单位
    landscapeWidth: 568, // 横屏时使用的视口宽度
    unitToConvert: 'px', // 要转化的单位
    viewportWidth: 750, // UI设计稿的宽度
    unitPrecision: 5, // 转换后的精度,即小数点位数
    propList: ['*'], // 指定转换的css属性的单位,*代表全部css属性的单位都进行转换
    viewportUnit: 'vw', // 指定需要转换成的视窗单位,默认vw
    fontViewportUnit: 'vw', // 指定字体需要转换成的视窗单位,默认vw
    selectorBlackList: ['special'], // 指定不转换为视窗单位的类名,
    minPixelValue: 1, // 默认值1,小于或等于1px则不进行转换
    mediaQuery: true, // 是否在媒体查询的css代码中也进行转换,默认false
    replace: true, // 是否转换后直接更换属性值
    exclude: [/node_modules/] // 设置忽略文件,用正则做目录名匹配
    }
    }
    }



作者:imber
来源:juejin.cn/post/7241875166887444541
收起阅读 »

如何及时发现网页的隐形错误

web
在上一篇文章前端监控究竟有多重要?大家了解了前端监控系统的重要性以及前端监控的组成部分、常见的监控指标、埋点方式。 接下来这篇文章我们就来详细学习一下前端监控系统中的,异常监控。 想要进行异常监控之前,肯定先要了解有哪些异常才能进行监控。 异常的类型 一般来说...
继续阅读 »

在上一篇文章前端监控究竟有多重要?大家了解了前端监控系统的重要性以及前端监控的组成部分、常见的监控指标、埋点方式。


接下来这篇文章我们就来详细学习一下前端监控系统中的,异常监控


想要进行异常监控之前,肯定先要了解有哪些异常才能进行监控。


异常的类型


一般来说,浏览器端的异常分为两种类型:



  • JavaScript 错误,一般都是来自代码的原因。

  • 静态资源错误,一般都是来着资源加载的原因


而这里面我们又有各自的差异


JavaScript 错误


先来说说JavaScript的错误类型,ECMA-262 定义了 7 种错误类型,说明如下:



  • EvalError :eval() 函数的相关的错误

  • RangeError :使用了超出了 JavaScript 的限制或范围的值。

  • ReferenceError: 引用了未定义的变量或对象

  • TypeError: 类型错误

  • URIError: URI操作错误

  • SyntaxError: 语法错误 (这个错误WebIDL中故意省略,保留给ES解析器使用)

  • Error: 普通异常,通常与 throw 语句和 try/catch 语句一起使用,利用属性 name 可以声明或了解异常的类型,利用message 属性可以设置和读取异常的详细信息。


如果想更详细了解可以看详细错误罗列这篇文章


静态资源错误



  • 通过 XMLHttpRequest、Fetch() 的方式来请求的 http 资源时。

  • 利用
收起阅读 »

CSS弹性布局:Flex布局及属性完全指南,点击解锁新技能!

Flex 布局是一种新型的 CSS 布局模式,它主要用于弹性盒子布局。相比于传统的布局方式,它更加灵活,易于调整,也更加适应不同的设备和屏幕尺寸。下面我们就来详细解析 Flex 布局及其属性,帮助大家深入理解和运用 Flex 布局。一、什么是Flex布局?在介...
继续阅读 »

Flex 布局是一种新型的 CSS 布局模式,它主要用于弹性盒子布局。相比于传统的布局方式,它更加灵活,易于调整,也更加适应不同的设备和屏幕尺寸。

下面我们就来详细解析 Flex 布局及其属性,帮助大家深入理解和运用 Flex 布局。

一、什么是Flex布局?

在介绍Flex布局之前,我们不得不提到它的前辈——浮动和定位。它们曾是布局的主力军,但随着响应式设计的兴起,它们的局限性也愈发明显。

Flex布局的出现,正是为了解决这些局限性,它允许我们在一个容器内对子元素进行灵活的排列、对齐和空间分配。

Description

Flex全称为 “Flexible Box Layout”,即 “弹性盒布局”,旨在提供一种更有效的方式来布局、对齐和分配容器中项目之间的空间,即使它们的大小未知或动态变化。

声明定义

容器里面包含着项目元素,使用 display:flex 或 display:inline-flex 声明为弹性容器。

.container {
display: flex | inline-flex;
}

flex布局的作用

  • 在父内容里面垂直居中一个块内容。

  • 使容器的所有子项占用等量的可用宽度/高度,而不管有多少宽度 / 高度可用。

  • 使多列布局中的所有列采用相同的高度,即使它们包含的内容量不同。

二、Flex布局的核心概念

要理解Flex布局,我们必须先了解几个核心概念:

2.1 容器与项目

容器(Container):设置了display: flex;的元素成为Flex容器。容器内的子元素自动成为Flex项目。

.container{
display: flex;
}
<div class="container">
<div class="item"> </div>
<div class="item">
<p class="sub-item"> </p>
</div>
<div class="item"> </div>
</div>

上面代码中, 最外层的 div 就是容器,内层的三个 div 就是项目。

注意: 项目只能是容器的顶层子元素(直属子元素),不包含项目的子元素,比如上面代码的 p 元素就不是项目。flex布局只对项目生效。

2.2 主轴(Main Axis)和交叉轴(Cross Axis)

主轴是Flex项目的排列方向,交叉轴则是垂直于主轴的方向。

Description

主轴(main axis)

沿其布置子容器的从 main-start 开始到 main-end ,请注意,它不一定是水平的;这取决于 flex-direction 属性(见下文), main size 是它可放置的宽度,是容器的宽或高,取决于 flex-direction。

交叉轴(cross axis)

垂直于主轴的轴称为交叉轴,它的方向取决于主轴方向,是主轴写满一行后另起一行的方向,从 cross-start 到 cross-end , cross size 是它可放置的宽度,是容器的宽或高,取决于 flex-direction。

三、Flex布局的基本属性

3.1容器属性

Description

容器的属性主要包括:

  • flex-direction:定义了主轴的方向,可以是水平或垂直,以及其起始和结束的方向。

  • flex-wrap:决定了当容器空间不足时,项目是否换行。

  • flex-flow:这是flex-direction和flex-wrap的简写形式。

  • justify-content:设置项目在主轴上的对齐方式。

  • align-items:定义了项目在交叉轴上的对齐方式。

  • align-content:定义了多根轴线时,项目在交叉轴上的对齐方式。

  • gap row-gap、column-gap:设置容器内项目间的间距。

3.1.1 主轴方向 flex-direction

定义主轴的方向,也就是子项目元素排列的方向。

  • row (默认):从左到右 ltr ;从右到左 rtl

  • row-reverse :从右到左 ltr ;从左到右 rtl

  • column: 相同, row 但从上到下

  • column-reverse: 相同, row-reverse 但从下到上

.container {
flex-direction: row | row-reverse | column | column-reverse;
}

Description

Description

3.1.2 换行 flex-wrap

设置子容器的换行方式,默认情况下,子项目元素都将尝试适合一行nowrap。

  • nowrap (默认)不换行

  • wrap 一行放不下时换行

  • wrap-reverse 弹性项目将从下到上换行成多行

.container {
flex-wrap: nowrap | wrap | wrap-reverse;
}

Description

3.1.3 简写 flex-flow

flex-direction 和 flex-wrap 属性的简写,默认值为 row nowrap。

.container {
flex-flow: column wrap;
}

取值情况:

Description

3.1.4 项目群对齐 justify-content与align-items

justify-c ontent 决定子元素在主轴方向上的对齐方式,默认是 flex-start。

.container {
justify-content: flex-start | flex-end | center | space-between | space-around | space-evenly | start | end | left | right ... + safe | unsafe;
}

Description

align-items 决定子元素在交叉轴方向上的对齐方式,默认是 stretch。

.container {
align-items: stretch | flex-start | flex-end | center | baseline | first baseline | last baseline | start | end | self-start | self-end + ... safe | unsafe;
}

Description

3.1.5多行对齐 align-content

align-content属性定义了多根轴线的对齐方式。如果项目只有一根轴线,该属性不起作用。

  • flex-start:与交叉轴的起点对齐。

  • flex-end:与交叉轴的终点对齐。

  • center:与交叉轴的中点对齐。

  • space-between:与交叉轴两端对齐,轴线之间的间隔平均分布。

  • space-around:每根轴线两侧的间隔都相等。所以,轴线之间的间隔比轴线与边框的间隔大一倍。

  • stretch(默认值):轴线占满整个交叉轴。

.container {
align-content: flex-start | flex-end | center | space-between | space-around | space-evenly | stretch | start | end | baseline | first baseline | last baseline + ... safe | unsafe;
}

Description

3.1.6 间距 gap row-gap column-gap

设置容器内项目之间的间距,只控制项目与项目的间距,对项目与容器的间距不生效。

.container {
display: flex;
...
gap: 10px;
gap: 10px 20px; /* row-gap column gap */
row-gap: 10px;
column-gap: 20px;
}

Description
这设置的是最小间距,因为 just-content 导致的间距变大。

3.2项目属性

Description

项目item 的属性包括:

  • order:指定了项目的排列顺序。

  • flex-grow:定义了在有可用空间时的放大比例。

  • flex-shrink:定义了在空间不足时的缩小比例。

  • flex-basis:指定了项目在分配空间前的初始大小。

  • flex:这是flex-grow、flex-shrink和flex-basis的简写形式。

  • align-self:允许单个项目独立于其他项目在交叉轴上对齐。

3.2.1 排序位置 order

  • 每个子容器的order属性默认为0

  • 通过设置order属性值,改变子容器的排列顺序

  • 可以是负值,数值越小的话,排的越靠前

.item1 {
order: 3; /* default is 0 */
}

Description

3.2.2 弹性成长 flex-grow

在容器主轴上存在剩余空间时, flex-grow才有意义。

定义的是可放大的能力,0 (默认)禁止放大,大于 0 时按占的比重分放大,负数无效。

.container{
border-left:1.2px solid black;
border-top:1.2px solid black;
border-bottom: 1.2px solid black;
width: 100px;
height: 20px;
display: flex;
}
.item{
border-right:1.2px solid black;
width: 20px;height: 20px;
}
.item1{
/* 其他的都是0,这一个是1,1/1所以能所有剩下的空间都是item1的 */
flex-grow: 1; /* default 0 */
}
<div>
<div class="item item1" style="background-color: #7A42A8;"></div>
<div style="background-color: #8FAADC;"></div>
<div style="background-color: #DAE3F3;"></div>
</div>

Description

3.2.3 弹性收缩 flex-shrinik

当容器主轴 “空间不足” 且 “禁止换行” 时, flex-shrink才有意义。

定义的是可缩小的能力,1 (默认)等大于 0 的按比例权重收缩, 0 为禁止收缩,负数无效。

.container{
width: 100px;
height: 20px;
display: flex;
flex-wrap: nowrap;
}
.item{
width: 50px;height: 20px;
}
.item1{/*收缩权重1/3,总空间50,所以它占33.33,为原本的2/3*/
flex-shrink: 1; /* default 1 */
}
.item2{/*收缩权重2/3,总空间50,所以它占16.67,为原本的1/3*/
flex-shrink: 2; /* default 1 */
}
.item3{
flex-shrink: 0; /* default 1 */
}
<div>
<div class="item item1" style="background-color: #7A42A8;"></div>
<div class="item item2" style="background-color: #8FAADC;"></div>
<div class="item item3" style="background-color: #DAE3F3;"></div>
</div>

Description

3.2.4 弹性基值 flex-basis

flex-basis 指定了 flex 元素在主轴方向上的初始尺寸,它可以是长度(例如 20% 、 5rem 等)或关键字。felx-wrap根据它计算是否换行,默认值为 auto ,即项目的本来大小。它会覆盖原本的width 或 height。

.item {
flex-basis: <length> | auto; /* default auto */
}

3.2.5 弹性简写flex

flex-grow , flex-shrink 和 flex-basis 组合的简写,默认值为 0 1 auto。

.item {
flex: none | [ <'flex-grow'> <'flex-shrink'>? || <'flex-basis'> ]
}

取值情况:
Description

3.2.6自我对齐 align-self

这允许为单个弹性项目覆盖默认的交叉轴对齐方式 align-items。

.item {
align-self: auto | flex-start | flex-end | center | baseline | stretch;
}

Description

注意: flexbox布局和原来的布局是两个概念,部分css属性在flexbox盒子里面不起作用,eg:float , clear 和 vertical-align 等等。

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

点这里前往学习哦!

四、实战演练

让我们通过一个简单的例子来实践一下Flex布局的魅力。假设我们有6张图片,我们希望在不同的屏幕尺寸下,它们能够自适应排列。

1、设置容器属性:

对于包含图片的容器,首先将其display属性设置为flex,从而启用Flex布局。

2、确定排列方向:

根据设计需求,可以通过设置flex-direction属性来确定图片的排列方向。例如,如果希望图片在小屏幕上水平排列,可以设置flex-direction: row;如果希望图片垂直排列,则设置flex-direction: column。

3、调整对齐方式:

使用justify-content和align-items属性来调整图片的对齐方式。例如,如果想让图片在主轴上均匀分布,可以设置justify-content: space-around;如果想让图片在交叉轴上居中对齐,可以设置align-items: center。

4、允许换行显示:

如果需要图片在小屏幕上换行显示,可以添加flex-wrap: wrap属性。

5、优化空间分配:

通过调整flex-grow、flex-shrink和flex-basis属性来优化空间分配。例如,可以设置图片的flex-basis为calc(100% / 3 - 20px),这样每张图片会占据三分之一的宽度减去20像素的间距。

示例代码如下:

<!DOCTYPE html>
<html>
<head>
<style>
.image-grid {
display: flex;
flex-wrap: wrap;
justify-content: space-around;
}
.image-grid img {
flex-basis: calc(100% / 3 - 20px);
}
@media screen and (max-width: 800px) {
.image-grid img {
flex-basis: calc(100% / 2 - 20px);
}
}
@media screen and (max-width: 400px) {
.image-grid img {
flex-basis: calc(100% - 20px);
}
}
</style>
</head>
<body>
<div>
<img src="image1.jpg" alt="Image 1">
<img src="image2.jpg" alt="Image 2">
<img src="image3.jpg" alt="Image 3">
<img src="image4.jpg" alt="Image 4">
<img src="image5.jpg" alt="Image 5">
<img src="image6.jpg" alt="Image 6">
</div>
</body>
</html>

将上述代码保存为一个HTML文件,并将image1.jpg、image2.jpg等替换为你自己的图片路径。然后在浏览器中打开该HTML文件,你将看到一个响应式的图片网格布局,图片会根据屏幕尺寸自适应排列。

Flex布局以其简洁明了的属性和强大的适应性,已经成为现代网页设计不可或缺的工具。掌握了Flex布局,你将能够轻松应对各种复杂的页面布局需求,让你的设计更加灵活、美观。现在,就打开你的代码编辑器,开始你的Flex布局之旅吧!

收起阅读 »

前端可玩性UP项目:大屏布局和封装

web
前言 autofit.js 发布马上要一年了,也收获了一批力挺用户,截至目前它在github上有1k 的 star,npm 上有超过 13k 的下载量。 这篇文章主要讲从设计稿到落地开发大屏应用,大道至简,这篇文章能帮助各位潇洒自如的开发大屏。 分析设计稿 分...
继续阅读 »

前言


autofit.js 发布马上要一年了,也收获了一批力挺用户,截至目前它在github上有1k 的 star,npm 上有超过 13k 的下载量。


这篇文章主要讲从设计稿到落地开发大屏应用,大道至简,这篇文章能帮助各位潇洒自如的开发大屏。


分析设计稿


分析设计稿之前先吐槽一下大屏这种展现形式,这简直就是自欺欺人、面子工程的最直接的诠释,是吊用没有,只为了好看,如果设计的再不好看啊,这就纯纯是屎。在我的理解中,这就像把PPT放到了web端,仅此而已。



但是王哥告诉我:"你看似没有用的东西,其实都有用,很多想真正做出有用的产品的企业,没钱,就要先把面子工程做好,告诉别人他们要做一件什么事,这样投资人才会看到,后面才有机会发展。"



布局方案


image.png
上图展示了一个传统意义上且比较普遍的大屏形态,分为四个部分,分别是


头部


头部经常放标题、功能菜单、时间、天气


左右面板


左右面板承载了各种数字和报表,还有视频、轮播图等等


中间


中间部分一般放地图,这其中又分假地图(一张图片)、图表地图(如echarts)、地图引擎(如:leaflet、mapbox、高德、百度)。或者有的还会放3D场景,一般有专门的同事去做3D场景,然后导入到web端。


大屏的设计通常的分辨率是 1920*1080 的,这也是迄今为止应用最广泛的显示器配置,当然也有基于客户屏幕做的异形分辨率,这就五花八门了。


但是万变不离其宗,分辨率的变化不会影响它的基本结构,根据上面的图,我们可以快速构建结构代码


  <div class='Box'>
   <div class="header"></div>
   <div class="body">
     <div class="leftPanel"></div>
     <div class="mainMap"></div>
     <div class="rightPanel"></div>
   </div>
 </div>

上面的代码实现了最简单的上下(Box)+左右(body)的布局结构,完全不需要任何定位策略。


要实现上图的效果,只需最简单的CSS即可完成布局。


组件方案


大屏虽然是屎,但是是一种可玩性很强的项目,想的越复杂,做起来就越复杂,想的越简单,做起来就越简单。


可以疯狂封装炫技,因为大屏里面的可玩组件简直太多了,且涵盖的太全了,想怎么玩都可以,包括但不限于 各类图表库的封装(echarts、highCharts、vChart)、轮播图(swiper)、地图引擎、视频库(包括直播流)等等。


如果想简单,甚至可以不用封装,可以看到结构甚至简单到不用CSS几行就可以搭建出基本框架,只把header、leaftPanel、rightPanel、map封装一下就可以了。


这里还有一个误区,就是大家都喜欢把 大型的组件库 拉到大屏里来用,结果做完了发现好像只用了一个 toast 和一个下拉组件,项目打包后却增大了几十倍的体积,其实像这种简单的组件,完全可以手写,或者找小的独立包来用,一方面会减小体积,不至于让项目臃肿,另一方面可以锻炼自己的手写能力,这才是有必要的封装。


适配


目前主流的适配方案,依然是 rem 方案,其原理就是根据根元素的 font-size 自动计算大小,但是此方法需要手动计算 rem 值,或者使用第三方插件如postcss等,但是此方案还有一个弊端,就是无法向下兼容,因为浏览器中最小的文字大小是12px。


vh/vw方案就不再赘述了,原理基本和 rem/em 相似,都涉及到单位的转换。


autofit.js


主要讲一下使用 autofit.js 如何快速实现适配。


不支持的场景


首先 autofit.js 不支持 elementUI(plus)、ant-design等组件库,具体是不支持包含popper.js的组件,popper.js 在计算弹出层位置时,不会考虑 scale 后的元素的视觉大小,所以会造成弹出元素的位置偏移。


其次,不支持 百度地图,百度地图对窗口缩放事件没有任何处理,有同学反馈说,即使使用了resize属性,百度地图在和autofit.js共同使用时,也会有事件热区偏移的问题。而且百度地图使用 bd-09 坐标系,和其他图商不通用,引擎的性能方面也差点意思,个人不推荐在开发中使用百度地图。


然后一些拖拽库,如甘特图插件,可能也不支持,他们在计算鼠标位置时同样没有考虑 scale 后的元素的视觉大小。


用什么单位


不支持的单位:vh、vw、rem、em


让我诧异的是,老有人问我该用什么单位,主要徘徊在 px 和 % 之间,加群的同学多数是因为用了相对单位,导致留白了。不过人各有所长,跟着各位大佬我也学到了很多。


看下图


image.png
假如有两个宽度为1000的元素,他们内部都有一个子元素,第一个用百分比设置为 width:50%;left:1% , 第二个设置为 wdith:500px;left:10px 。此时,只要外部的1000px的容器宽度不变,这两个内部元素在视觉上是一模一样的,且在实际数值上也是一模一样的,他们宽度都为500px,距离左侧10px。


但是如果外部容器变大了,来看一下效果:


image.png
在样式不变的情况下,仅改变外部容器大小,差异就出来了,由上图可知,50%的元素依然占父元素的一半,实际宽度变成了 1000px,距离左侧的实际距离变成了 20px。


这当然不难理解,百分比单位是根据 最近的、有确定大小的父级元素计算的。


所以,应该用什么单位其实取决于想做什么,举个例子:在1920*1080基础上开发,中间的地图写成了宽度为 500px ,这在正常情况下,看起来没有任何问题,它大概占屏幕的 26%,当屏幕分辨率达到4096*2160时,它在屏幕上只占 12%,看起来就是缩在一角。而当你设置宽度为26%时,无论显示器如何变化,它始终占屏幕26%。


autofit.js 所干的事,就是把1000px 变成了 2000px或者把2000px变成了1000px,并给它设置了一个合适的缩放大小。


图表、图片拉伸


背景或各种图片按需设置 object-fit: cover;即可


图表如echarts一般推荐使用百分比,且监听窗口变化事件做resize()


结语


再次感慨,大道至简,事情往往没有那么复杂,祝各位前程似锦。


作者:德莱厄斯
来源:juejin.cn/post/7344625554530779176
收起阅读 »

js检测网页空闲状态(一定时间内无操作)

web
1. 背景 最近开发项目时,常碰到“用户在一定时间内无任何操作时,跳转到某个页面”的需求。 网上冲浪后,也没有找到一个比较好的js封装去解决这个问题,从而决定自己实现。 2. 如何判断页面是否空闲 首先,我们要知道什么是空闲?用户一定时间内,没有对网页进行任何...
继续阅读 »

1. 背景


最近开发项目时,常碰到“用户在一定时间内无任何操作时,跳转到某个页面”的需求。


网上冲浪后,也没有找到一个比较好的js封装去解决这个问题,从而决定自己实现。


2. 如何判断页面是否空闲


首先,我们要知道什么是空闲?用户一定时间内,没有对网页进行任何操作,则当前网页为空闲状态。


用户操作网页,无非就是通过鼠标键盘两个输入设备(暂不考虑手柄等设备)。因而我们可以监听相应的输入事件,来判断网页是否空闲(用户是否有操作网页)。



  1. 监听鼠标移动事件mousemove

  2. 监听键盘按下事件mousedown

  3. 在用户进入网页后,设置延时跳转,如果触发以上事件,则移除延时器,并重新开始。


3. 网页空闲检测实现


3.1 简易实现


以下代码,简单实现了一个判断网页空闲的方法:


const onIdleDetection = (callback, timeout = 15, immediate = false) => {
let pageTimer;

const onClearTimer = () => {
pageTimer && clearTimeout(pageTimer);
pageTimer = undefined;
};
const onStartTimer = () => {
onClearTimer();
pageTimer = setTimeout(() => {
callback();
}, timeout * 1000);
};

const startDetection = () => {
onStartTimer();
document.addEventListener('mousedown', onStartTimer);
document.addEventListener('mousemove', onStartTimer);
};
const stopDetection = () => {
onClearTimer();
document.removeEventListener('mousedown', onStartTimer);
document.removeEventListener('mousemove', onStartTimer);
};
const restartDetection = () => {
onClearTimer();
onStartTimer();
};

if (immediate) {
startDetection();
}

return {
startDetection,
stopDetection,
restartDetection
};
};

也许你注意到了,我并没有针对onStartTimer事件进行防抖,那这是不是会对性能有影响呢?


是的,肯定有那么点影响,那我为啥不添加防抖呢?


这是因为添加防抖后,形成了setTimeout嵌套,嵌套setTimeout会有精度问题(参考)。


或许你还会说,非活动标签页(网页被隐藏)的setTimeout的执行和精度会有问题(参考非活动标签的超时)。


确实存在以上问题,接下来我们就来一一解决吧!


3.2 处理频繁触发问题


我们可以通过添加一个变量记录开始执行时间,当下一次执行与当前的时间间隔小于某个值时直接退出函数,从而解决这个问题(节流思想应用)。


const onIdleDetection = (callback, timeout = 15, immediate = false) => {
let pageTimer;
// 记录开始时间
let beginTime = 0;
const onStartTimer = () => {
// 触发间隔小于100ms时,直接返回
const currentTime = Date.now();
if (pageTimer && currentTime - beginTime < 100) {
return;
}

onClearTimer();
// 更新开始时间
beginTime = currentTime;
pageTimer = setTimeout(() => {
callback();
}, timeout * 1000);
};
const onClearTimer = () => {
pageTimer && clearTimeout(pageTimer);
pageTimer = undefined;
};

const startDetection = () => {
onStartTimer();
document.addEventListener('mousedown', onStartTimer);
document.addEventListener('mousemove', onStartTimer);
};
const stopDetection = () => {
onClearTimer();
document.removeEventListener('mousedown', onStartTimer);
document.removeEventListener('mousemove', onStartTimer);
};
const restartDetection = () => {
onClearTimer();
onStartTimer();
};

if (immediate) {
startDetection();
}

return {
startDetection,
stopDetection,
restartDetection
};
};

3.3 处理页面被隐藏的情况(完整实现)


我们可以监听visibilitychange事件,在页面隐藏时移除延时器,然后页面显示时继续计时,从而解决这个问题。


/**
* 网页空闲检测
* @param {() => void} callback 空闲时执行,即一定时长无操作时触发
* @param {number} [timeout=15] 时长,默认15s,单位:秒
* @param {boolean} [immediate=false] 是否立即开始,默认 false
* @returns
*/

const onIdleDetection = (callback, timeout = 15, immediate = false) => {
let pageTimer;
let beginTime = 0;
const onClearTimer = () => {
pageTimer && clearTimeout(pageTimer);
pageTimer = undefined;
};
const onStartTimer = () => {
const currentTime = Date.now();
if (pageTimer && currentTime - beginTime < 100) {
return;
}

onClearTimer();
beginTime = currentTime;
pageTimer = setTimeout(() => {
callback();
}, timeout * 1000);
};

const onPageVisibility = () => {
// 页面显示状态改变时,移除延时器
onClearTimer();

if (document.visibilityState === 'visible') {
const currentTime = Date.now();
// 页面显示时,计算时间,如果超出限制时间则直接执行回调函数
if (currentTime - beginTime >= timeout * 1000) {
callback();
return;
}
// 继续计时
pageTimer = setTimeout(() => {
callback();
}, timeout * 1000 - (currentTime - beginTime));
}
};

const startDetection = () => {
onStartTimer();
document.addEventListener('mousedown', onStartTimer);
document.addEventListener('mousemove', onStartTimer);
document.addEventListener('visibilitychange', onPageVisibility);
};

const stopDetection = () => {
onClearTimer();
document.removeEventListener('mousedown', onStartTimer);
document.removeEventListener('mousemove', onStartTimer);
document.removeEventListener('visibilitychange', onPageVisibility);
};

const restartDetection = () => {
onClearTimer();
onStartTimer();
};

if (immediate) {
startDetection();
}

return {
startDetection,
stopDetection,
restartDetection
};
};

通过以上代码,我们就完整地实现了一个网页空闲状态检测的方法。


4. 扩展阅读


chrome浏览器其实提供了一个Idle DetectionAPI,来实现网页空闲状态的检测,但是这个API还是一个实验性特性,并且Firefox与Safari不支持。API参考


作者:求知若饥
来源:juejin.cn/post/7344670957405405223
收起阅读 »

灰度发布策略在前端无损升级中的应用

web
为了提升浏览器加载页面资源的性能,对于js、css、图片等静态资源,web服务器往往会通过Cache-Control、ETag/If--Match、Last-Modified/If-Modified-Since、Pragma、Expires、Date、Age等...
继续阅读 »

为了提升浏览器加载页面资源的性能,对于js、css、图片等静态资源,web服务器往往会通过Cache-Control、ETag/If--Match、Last-Modified/If-Modified-Since、Pragma、Expires、Date、Age等头部来控制、管理、检测这类资源对缓存机制的使用情况。同时,为了使新版本的js、css等资源立即生效,一种比较通行的做法是为js、css这些文件名添加一个hash值。这样当js、css内容发生变化时,浏览器获取的是不同的js、css文件。在这种情况下,旧版本的index.html文件可能是这样的:


<!DOCTYPE html>
<html>
<head>
<meta charset=utf-8>
<title>测试</title>
<meta http-equiv=X-UA-Compatible content="IE=Edge">
<meta name=viewport content="width=device-width,minimum-scale=1,maximum-scale=1">
<link href=/static/css/main.4656e35c1e8d4345f5bf.css rel=stylesheet>
</head>
<style>
html, body {
width: 100%;
}
</style>
<body>
<div id=newMain></div>
<script type=text/javascript src=/static/js/main-4656e35c1e8d4345f5bf.js></script>
</body>
</html>

当项目的js、css内容发生了变化时,新版本的index.html文件内容变成这样的(js和css文件名携带了新的hash值1711528240049):


<!DOCTYPE html>
<html>
<head>
<meta charset=utf-8>
<title>测试</title>
<meta http-equiv=X-UA-Compatible content="IE=Edge">
<meta name=viewport content="width=device-width,minimum-scale=1,maximum-scale=1">
<link href=/static/css/main.1711528240049.css rel=stylesheet>
</head>
<style>
html, body {
width: 100%;
}
</style>
<body>
<div id=newMain></div>
<script type=text/javascript src=/static/js/main-1711528240049.js></script>
</body>
</html>

因为index.html文件一般会设置为不缓存,这样用户每次访问首页时,都会从web服务器重新获取index.html,然后根据index.html中的资源文件是否变化,从而决定是否使用缓存的文件。这样既能让用户立即获取最新的js、css等静态资源文件,又能充分地使用缓存。index.html的响应头大概长这样:


847fa51ec16bbfa14a5865482b23f396.png


但是为了保证系统的高可用,web后端往往由多个实例提供服务,用户请求会在多个服务实例间进行负载均衡。而系统升级过程中,会存在多个版本共存的现象。这时,如果用户从旧版本实例上获取了index.html文件,然后再去获取旧版本的js、css文件(main-4656e35c1e8d4345f5bf.jsmain.4656e35c1e8d4345f5bf.css),但是请求却分发到了新版本服务实例上,这时因为新版本服务实例只有main-1711528240049.jsmain.1711528240049.css文件,就会导致访问失败。反过来,如果从新版本实例上获取了index.html文件,在请求相应的js、css文件时,也可能被分发到旧版本实例上,也导致访问失败。


解决方法:


1)首先,改造一下index.html文件中引用js、css等静态资源的路径,添加一个版本号,如v1、v2,这样index.html文件对js、css的引用变为:


<link href=/static/v1/css/main.1711528240049.css rel=stylesheet>
<script type=text/javascript src=/static/v1/js/main-1711528240049.js></script>

2)使用灰度发布策略升级系统,具体步骤如下(假设系统包含A、B两个服务实例)



  1. 升级前(稳态),在应用网关(代理)上配置路由策略V1,该路由策略的功能为:匹配路径前缀/static/v1的请求负载均衡分发到A、B两个服务实例

  2. 将待升级的服务实例A从路由策略V1中摘除掉,这时用户请求只会发送给实例B

  3. 待实例A上所有进行中的请求都处理完后,就可以安全的停掉旧的服务,替换为新的服务,这时还不会有请求分发到实例A

  4. 待实例B测试功能正常后,在应用网关(代理)上新增一条路由策略V2,该路由策略的功能为:匹配路径前缀/static/v2的请求分发到服务实例A。这时,从服务实例A上获取的index.html文件引发的后续js、css请求,都会分发到服务实例A,从服务实例B上获取的index.html文件引发的后续js、css请求,都会分发到服务实例B

  5. 继续将实例B从路由策略V1中摘掉,然后升级实例B,将实例B添加到路由策略V2中

  6. 所有的流量都切换到了路由策略V2中,下线路由策略V1。完成整个升级过程,实现了前端的无损升级


作者:movee
来源:juejin.cn/post/7353069220827856946
收起阅读 »

特效炸裂:小米 SU7 在线特效网站技术不完全揭秘!!!

web
哈喽,大家好 我是 xy👨🏻‍💻。用 Three.js 实现 小米 SU7 在线体验,特效相当炸裂!!! 前言 最近一位叫 @GameMCU的大佬用 Webgl、Three.js 等技术实现了一个 小米 SU7 在线体验网站:https://gamemcu....
继续阅读 »

哈喽,大家好 我是 xy👨🏻‍💻。用 Three.js 实现 小米 SU7 在线体验,特效相当炸裂!!!



前言


最近一位叫 @GameMCU的大佬用 WebglThree.js 等技术实现了一个 小米 SU7 在线体验网站:https://gamemcu.com/su7/被广大网友疯传,效果相当炸裂!


网站首发当天由于访问量过大导致奔溃, 后来可能获得了某里云官方支持!!! 这一波真的要给某里云点赞!



更有网友评论: 这效果和交互完全可以吊打官方和各种卖车的网站了啊



并且 @小米汽车官方:求求了,收编了吧,这能极大提升小米su7的逼格,再用到公司其他产品,能提升整体公司的逼格



废话不多说,直接上效果!!!


效果展示



  • 模拟在汽车在道路行驶特效,宛如身临其境




  • 流线型车身设计,彰显速度与激情的完美融合。每一处细节都经过精心打磨,只为给你带来最纯粹的驾驶体验。




  • 在高速行驶的过程中,风阻是影响车速的重要因素。我们的特效模拟器通过先进的算法,真实还原了风阻对车辆的影响。当你长按鼠标,感受那股扑面而来的气流,仿佛置身于真实的驾驶环境中。




  • 雷达实时探测功能可以帮你轻松掌握周围车辆的情况,让你在驾驶过程中更加安心



视频


是怎么实现的


在线体验完@GameMCU大佬的网站之后, 我很好奇大佬是使用什么技术去实现的, 身为前端开发的我, 第一步当然是 F12 打开控制台查看



发现使用的是 Three.js r150 版本开发, 并且还用了一个叫 xviewer.js 的插件,


于是乎我找到了@GameMCU大佬的 github 主页, 在主页中介绍了 xviewer.js:



xviewer.js是一个基于 three.js 的插件式渲染框架,它对 three.js 做了一层简洁优雅的封装,包含了大量实用的组件和插件,目标是让前端开发者能更简单地应用webgl技术。



比较遗憾的是 xviewer.js 目前还没有开源, 不过按照作者的意思是可能会在近期开源。


虽然目前 小米 SU7 在线体验网站没有开源, 但是作者主页开源了另外一个项目: three.js复刻原神启动, 也是一个基于 xviewer.js 开发的在线网站。


通过源码发现作者在项目中写了大量的 Shader, Shader 对于实现复杂的视觉效果和图形渲染技术至关重要,它们使得开发者能够创建出令人印象深刻的3D场景动画



Shader 是一种在计算机图形学中使用的程序,它运行在图形处理单元(GPU)上,用于处理渲染过程中的光照、颜色、纹理等视觉效果。


Shader 通常被用于 3D 图形渲染中,以增强视觉效果,使得图像更加逼真和吸引人。


在 Three.js 中, Shader 通常分为两类:顶点着色器(Vertex Shader)和片元着色器(Fragment Shader)。


顶点着色器负责处理顶点数据,如位置颜色纹理坐标等,而片元着色器则负责处理像素颜色,包括光照材质属性


总之,Shader 在 Three.js 中扮演着至关重要的角色,它们为开发者提供了强大的工具来创建丰富、动态和引人入胜的 3D 图形内容。通过学习和掌握 Shader 编程,开发者可以极大地扩展 Three.js 的应用范围和创作能力。


那么作为一名前端开发人员, 应该怎么快速入门 Shader, 并且用 Shader 创造令人惊叹的交互体验呢???


三个学习 Shader 网站推荐


1. The Book of Shaders



网址: https://thebookofshaders.com/?lan=ch


The Book of Shaders 是一个在线学习 Shader 的网站(电子书),它提供了一系列关于 Shader 的基础教程和示例代码,堪称入门级指南


2.Shadertoy



网址:https://www.shadertoy.com/


Shadertoy 是一个基于 WebGL 的在线实时渲染平台,主要用于编辑分享查看 shader 程序及其实现的效果。


在这个平台上,用户可以创作和分享自己的 3D 图形效果。它提供了一个简单方便的环境,让用户可以轻松编辑自己的片段着色器,并实时查看修改的效果。


同时,Shadertoy 上有许多大佬分享他们制作的酷炫效果的代码,这些代码是完全开源的,用户可以在这些代码的基础上进行修改和学习。


除此之外,Shadertoy 还允许用户选择声音频道,将当前帧的声音信息转变成纹理(Texture),传入 shader 当中,从而根据声音信息来控制图形。这使得 Shadertoy 在视觉和听觉的结合上有了更多的可能性。


3.glsl.app



网址:https://glsl.app/


glsl.app 是一个在线的 GLSL (OpenGL Shading Language) 编辑器。GLSL 是一种用于图形渲染的着色语言,特别是在 OpenGL 图形库中。这种语言允许开发者为图形硬件编写着色器程序,这些程序可以运行在 GPU 上,用于计算图像的各种视觉效果。


在 glsl.app 上,你可以:



  • 编写和编辑着色器代码:直接在网页上编写顶点着色器、片元着色器等。

  • 实时预览:当你编写或修改着色器代码时,可以立即在右侧的预览窗口中看到效果。

  • 分享你的作品:完成你的着色器后,你可以获得一个链接,通过这个链接与其他人分享你的作品。

  • 学习:如果你是初学者,该网站还提供了很多示例和教程,帮助你了解如何编写各种着色器效果。


参考连接:



作者:前端开发爱好者
来源:juejin.cn/post/7352797634556706831
收起阅读 »

提升你的CSS技能:深入理解伪类选择器和伪元素选择器!

在CSS的世界里,有些选择器并不像它们的名字那样直接。今天,我们要探索的是两种特殊的选择器:伪类选择器和伪元素选择器。它们虽然名字相似,但功能和用途却大有不同。下面就让我们一起来了解一下它们是如何在我们的页面布局中扮演着不可或缺的角色的吧。一、伪类选择器1、什...
继续阅读 »

在CSS的世界里,有些选择器并不像它们的名字那样直接。今天,我们要探索的是两种特殊的选择器:伪类选择器和伪元素选择器。它们虽然名字相似,但功能和用途却大有不同。

下面就让我们一起来了解一下它们是如何在我们的页面布局中扮演着不可或缺的角色的吧。

一、伪类选择器

1、什么是伪类选择器

伪类选择器,顾名思义,是一种特殊的选择器,它用来选择DOM元素在特定状态下的样式。这些特定状态并不是由文档结构决定的,而是由用户行为(如点击、悬停)或元素的状态(如被访问、被禁用)来定义的。

例如,我们可以用伪类选择器来改变链接在不同状态下的颜色,从而给用户以视觉反馈。

2、伪类选择器的语法

selector:pseudo-class {
property: value;
}

a:link {
color: #FF0000;
}

input:focus {
background-color: yellow;
}

注意:伪类名称对大小写不敏感。

3、常用的伪类选择器

下面分别介绍一下比较常用几类伪类选择器:

3.1 动态伪类选择器

这类选择器主要用于描述用户与元素的交互状态。例如:

1):hover: 当鼠标悬停在元素上时的样式。

代码示例:将链接的文本颜色改为红色

a:hover {
color: red;
}

2):active:当元素被用户激活(如点击)时的样式。

代码示例:将按钮的背景色改为蓝色

button:active {
background-color: blue;
}

3):focus: 当元素获得焦点(如输入框被点击)时的样式。

代码示例:将输入框的边框颜色改为绿色

input:focus {
border-color: green;
}

4):visited: 用于设置已访问链接的样式,通常与:link一起使用来区分未访问和已访问的链接。

代码示例:将已访问链接的颜色改为紫色

a:visited {
color: purple;
}

3.2 UI元素状态伪类选择器

这类选择器用于描述元素在用户界面中的状态。例如:

1):enabled和:disabled: 用于表单元素,表示元素是否可用。

示例:将禁用的输入框的边框颜色改为灰色

input:disabled {
border-color: gray;
}

2):checked: 用于单选框或复选框,表示元素是否被选中。

示例:将选中的单选框的背景色改为黄色

input[type="radio"]:checked {
background-color: yellow;
}

3):nth-child(n): 选取父元素中第n个子元素。

示例:将列表中的奇数位置的项目的背景色改为蓝色:

li:nth-child(odd) {
background-color: blue;
}

3.4 否定伪类选择器

这类选择器用于排除符合特定条件的元素。例如:

:not(selector): 选取不符合括号内选择器的所有元素。

示例:将不是段落的元素的背景色改为灰色:

*:not(p) {
background-color: gray;
}

4、常见应用

  • 设置鼠标悬停在元素上时的样式;

  • 为已访问和未访问链接设置不同的样式;

  • 设置元素获得焦点时的样式;


// 示例:a 标签的四种状态,分别对应 4 种伪类;

/* 未访问的链接 */
a:link {
color: blue;
}

/* 已访问的链接 */
a:visited {
color: red;
}

/* 鼠标悬停链接 */
a:hover {
color: orange;
}

/* 已选择的链接(鼠标点击但不放开时) */
a:active {
color: #0000FF;
}

注意:

  • a 标签的 4 个伪类(4种状态)必须按照一定顺序书写,否则将会失效;

  • a:hover 必须在 CSS 定义中的 a:link 和 a:visited 之后,才能生效;

  • a:active 必须在 CSS 定义中的 a:hover 之后才能生效;

  • 书写顺序为:a:link、a:visited、a:hover、a:active;

  • 记忆方法:love hate - “爱恨准则”;

二、伪元素选择器

1、什么是伪元素选择器

与伪类选择器不同,伪元素选择器是用来选择DOM元素的特定部分,而不是整个元素。它们通常用于处理那些不是由HTML标签直接表示的内容,比如首行文字、首字母或者生成的内容(如内容前面的编号)。

伪元素选择器允许我们对页面上的某些部分进行精确的样式控制,而这些部分在HTML结构中并不存在。

2、伪元素选择器语法

selector::pseudo-element {
property: value;
}

p::first-line {
color: #ff0000;
}

h1::before {
content: '♥';
}

3、常用伪元素选择器

伪元素选择器并不是针对真正的元素使用的选择器,而是针对CSS中已经定义好的伪元素使用的选择器,CSS中有如下四种常用伪元素选择器:first-line、 first-letter、 before、after。

3.1 ::first-line

::first-line表示第一行(第一行内容根据屏幕大小来决定显示多少字),例如:p::first-line{}。
代码示例:

    <style>
p::first-line{
color: blue;
}
</style>

Description

3.2 ::first-letter

::first-letter表示第一个字母,例如:p::first-letter{}。

代码示例:

<style>
p::first-letter{
font-size: 30px;
color: blueviolet;
}
</style>

Description

3.3 ::before和::after

::before表示元素的开始,::after表示元素的最后,before和after必须结合content属性来使用。

代码示例:

 <style>
p::after{
content: "hahaha";
color: red;
}
p::before{
content: "hehehe";
color: coral;
}
</style>

Description

注意:

  • before和after创建一个元素,但是属于行内元素。
  • 新创建的这个元素在文档中是找不到的,所以我们称为伪元素。
  • before在父元素内容的前面创建元素,after在父元素内容的后面插入元素。
  • 伪元素选择器和标签选择器一样,权重为1。

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

点这里前往学习哦!

三、伪类与伪元素选择器的区别

CSS中的伪类选择器和伪元素选择器都是用来选取DOM中特定元素的选择器。具体区别如下:

伪类的操作对象是文档树中已有的元素,而伪元素则创建了一个文档数外的元素。因此,伪类与伪元素的区别在于:有没有创建一个文档树之外的元素;

  • 伪类本质上是为了弥补常规CSS选择器的不足,以便获取到更多信息;

  • 伪元素本质上是创建了一个有内容的虚拟容器;

  • CSS3 中伪类和伪元素的语法不同;

  • 在 CSS3 中,已经明确规定了伪类用一个冒号来表示,而伪元素则用两个冒号来表示;

  • 可以同时使用多个伪类,而只能同时使用一个伪元素。

总的来说,伪类选择器关注的是元素在特定状态下的样式变化,而伪元素选择器则是通过创建新的元素来实现特定的样式效果。两者都是CSS中非常强大的工具,可以帮助开发者实现复杂的页面布局和动态效果。

伪类选择器和伪元素选择器虽然不是真正的元素,但它们在CSS中扮演着极其重要的角色。了解并熟练运用它们,可以让你的网页更加生动、互动性更强,同时也能更好地控制页面的布局和内容的表现。

收起阅读 »

JSON非常慢:这里有更快的替代方案!

web
是的,你没听错!JSON,这种在网络开发中普遍用于数据交换的格式,可能正在拖慢我们的应用程序。在速度和响应性至关重要的世界里,检查 JSON 的性能影响至关重要。在这篇博客中,深入探讨 JSON 可能成为应用程序瓶颈的原因,并探索更快的替代方法和优化技术,使您...
继续阅读 »

是的,你没听错!JSON,这种在网络开发中普遍用于数据交换的格式,可能正在拖慢我们的应用程序。在速度和响应性至关重要的世界里,检查 JSON 的性能影响至关重要。在这篇博客中,深入探讨 JSON 可能成为应用程序瓶颈的原因,并探索更快的替代方法和优化技术,使您的应用程序保持最佳运行状态。


JSON 是什么,为什么要关心?


image.png


JSON 是 JavaScript Object Notation 的缩写,一种轻量级数据交换格式,已成为应用程序中传输和存储数据的首选。它的简单性和可读格式使开发者和机器都能轻松使用。但是,为什么要在项目中关注 JSON 呢?


JSON 是应用程序中数据的粘合剂。它是服务器和客户端之间进行数据通信的语言,也是数据库和配置文件中存储数据的格式。从本质上讲,JSON 在现代网络开发中起着举足轻重的作用。


JSON 的流行以及人们使用它的原因...


主要有就下几点:



  1. 人类可读格式:JSON 采用简单明了、基于文本的结构,便于开发人员和非开发人员阅读和理解。这种人类可读格式增强了协作,简化了调试。

  2. 语言无关:JSON 与任何特定编程语言无关。它是一种通用的数据格式,几乎所有现代编程语言都能对其进行解析和生成,因此具有很强的通用性。

  3. 数据结构一致性:JSON 使用键值对、数组和嵌套对象来实现数据结构的一致性。这种一致性使其具有可预测性,便于在各种编程场景中使用。

  4. 浏览器支持:浏览器原生支持 JSON,允许应用程序与服务器进行无缝通信。这种本地支持极大地促进了 JSON 在开发中的应用。

  5. JSON API:许多服务和应用程序接口默认以 JSON 格式提供数据。这进一步巩固了 JSON 在网络开发中作为数据交换首选的地位。

  6. JSON 模式:开发人员可以使用 JSON 模式定义和验证 JSON 数据的结构,从而为其应用程序增加一层额外的清晰度和可靠性。


鉴于这些优势,难怪全球的开发人员都依赖 JSON 来满足他们的数据交换需求。不过,随着我们深入探讨,会发现与 JSON 相关的潜在性能问题以及如何有效解决这些挑战。


对速度的需求


应用速度和响应速度的重要性


在当今快节奏的数字环境中,应用程序的速度和响应能力是不容忽视的。用户希望在网络和移动应用中即时获取信息、快速交互和无缝体验。对速度的这种要求是由多种因素驱动的:



  1. 用户期望:用户已习惯于从数字互动中获得闪电般快速的响应。他们不想等待网页加载或应用程序响应。哪怕是几秒钟的延迟,都会导致用户产生挫败感并放弃使用。

  2. 竞争优势:速度可以成为重要的竞争优势。与反应慢的应用程序相比,反应迅速的应用程序往往能更有效地吸引和留住用户。

  3. 搜索引擎排名:谷歌等搜索引擎将页面速度视为排名因素。加载速度更快的网站往往在搜索结果中排名靠前,从而提高知名度和流量。

  4. 转换率:电子商务网站尤其清楚速度对转换率的影响。网站速度越快,转换率越高,收入也就越高。

  5. 移动性能:随着移动设备的普及,对速度的需求变得更加重要。移动用户的带宽和处理能力往往有限,因此,快速的应用程序性能必不可少。


JSON 会拖慢我们的应用程序吗?


在某些情况下,JSON 可能是导致应用程序运行速度减慢的罪魁祸首。解析 JSON 数据的过程,尤其是在处理大型或复杂结构时,可能会耗费宝贵的毫秒时间。此外,低效的序列化和反序列化也会影响应用程序的整体性能


JSON 为什么会变慢


1.解析开销


JSON 数据到达应用程序后,必须经过解析过程才能转换成可用的数据结构。解析过程可能相对较慢,尤其是在处理大量或深度嵌套的 JSON 数据时。


2.序列化和反序列化


JSON 要求在从客户端向服务器发送数据时进行序列化(将对象编码为字符串),并在接收数据时进行反序列化(将字符串转换回可用对象)。这些步骤会带来开销并影响应用程序的整体速度。


在微服务架构的世界里,JSON 通常用于在服务之间传递消息。但是,JSON 消息需要序列化和反序列化,这两个过程会带来巨大的开销。



在众多微服务不断通信的情况下,这种开销可能会累积起来,有可能会使应用程序减慢到影响用户体验的程度。



image.png


3.字符串操作


JSON 以文本为基础,主要依靠字符串操作来进行连接和解析等操作。与处理二进制数据相比,字符串处理速度较慢。


4.缺乏数据类型


JSON 的数据类型(如字符串、数字、布尔值)有限。复杂的数据结构可能需要效率较低的表示方法,从而导致内存使用量增加和处理速度减慢。


image.png


5.冗长性


JSON 的人机可读设计可能导致冗长。冗余键和重复结构会增加有效载荷的大小,导致数据传输时间延长。


6.不支持二进制


JSON 缺乏对二进制数据的本地支持。在处理二进制数据时,开发人员通常需要将其编码和解码为文本,这可能会降低效率。


7.深嵌套


在某些情况下,JSON 数据可能嵌套很深,需要进行递归解析和遍历。这种计算复杂性会降低应用程序的运行速度,尤其是在没有优化的情况下。


JSON 的替代品


虽然 JSON 是一种通用的数据交换格式,但由于其在某些情况下的性能限制,开发者开始探索更快的替代格式。我们来看呓2其中的一些替代方案。


1.协议缓冲区(protobuf)


协议缓冲区(通常称为 protobuf)是谷歌开发的一种二进制序列化格式。其设计宗旨是高效、紧凑和快速。Protobuf 的二进制特性使其在序列化和反序列化时比 JSON 快得多。


何时使用:当你需要高性能数据交换时,尤其是在微服务架构、物联网应用或网络带宽有限的情况下,请考虑使用 protobuf


2. MessagePack 信息包


MessagePack 是另一种二进制序列化格式,以速度快、结构紧凑而著称。其设计目的是在保持与各种编程语言兼容的同时,提高比 JSON 更高的效率。


何时使用:当你需要在速度和跨语言兼容性之间取得平衡时,MessagePack 是一个不错的选择。它适用于实时应用程序和对减少数据量有重要要求的情况。


3. BSON(二进制 JSON)


BSON 或二进制 JSON 是一种从 JSON 衍生出来的二进制编码格式。它保留了 JSON 的灵活性,同时通过二进制编码提高了性能。BSON 常用于 MongoDB 等数据库。


何时使用:如果你正在使用 MongoDB,或者需要一种能在 JSON 和二进制效率之间架起桥梁的格式,那么 BSON 就是一个很有价值的选择。


4. Apache Avro(阿帕奇 Avro)


Apache Avro 是一个数据序列化框架,专注于提供一种紧凑的二进制格式。它基于模式,可实现高效的数据编码和解码。


何时使用:Avro 适用于模式演进非常重要的情况,如数据存储,以及需要在速度和数据结构灵活性之间取得平衡的情况。


与 JSON 相比,这些替代方案在性能上有不同程度的提升,具体选择取决于您的具体使用情况。通过考虑这些替代方案,您可以优化应用程序的数据交换流程,确保将速度和效率放在开发工作的首位。


image.png


每个字节的重要性:优化数据格式


JSON 数据


下面是我们的 JSON 数据示例片段:


{
"id": 1, // 14 bytes
"name": "John Doe", // 20 bytes
"email": "johndoe@example.com", // 31 bytes
"age": 30, // 9 bytes
"isSubscribed": true, // 13 bytes
"orders": [ // 11 bytes
{ // 2 bytes
"orderId": "A123", // 18 bytes
"totalAmount": 100.50 // 20 bytes
}, // 1 byte
{ // 2 bytes
"orderId": "B456", // 18 bytes
"totalAmount": 75.25 // 19 bytes
} // 1 byte
] // 1 byte
}

JSON 总大小: ~139 字节


JSON 功能多样,易于使用,但也有缺点,那就是它的文本性质。每个字符、每个空格和每个引号都很重要。在数据大小和传输速度至关重要的情况下,这些看似微不足道的字符可能会产生重大影响。


效率挑战:使用二进制格式减少数据大小


现在,我们提供其他格式的数据表示并比较它们的大小:


协议缓冲区 (protobuf)


syntax = "proto3";

message User {
int32 id = 1;
string name = 2;
string email = 3;
int32 age = 4;
bool is_subscribed = 5;
repeated Order orders = 6;

message Order {
string order_id = 1;
float total_amount = 2;
}
}

0A 0E 4A 6F 68 6E 20 44 6F 65 0C 4A 6F 68 6E 20 44 6F 65 65 78 61 6D 70 6C 65 2E 63 6F 6D 04 21 00 00 00 05 01 12 41 31 32 33 03 42 DC CC CC 3F 05 30 31 31 32 34 34 35 36 25 02 9A 99 99 3F 0D 31 02 42 34 35 36 25 02 9A 99 99 3F

协议缓冲区总大小: ~38 字节


MessagePack


二进制表示法(十六进制):


a36a6964000000000a4a6f686e20446f650c6a6f686e646f65406578616d706c652e636f6d042100000005011241313302bdcccc3f0530112434353625029a99993f

信息包总大小: ~34 字节


Binary Representation (Hexadecimal):


3e0000001069640031000a4a6f686e20446f6502656d61696c006a6f686e646f65406578616d706c652e636f6d1000000022616765001f04370e4940

BSON 总大小: ~43 字节


Avro


二进制表示法(十六进制):


0e120a4a6f686e20446f650c6a6f686e646f65406578616d706c652e636f6d049a999940040a020b4108312e3525312e323538323539

Avro 总大小: ~32 字节


image.png


现在,你可能想知道,为什么这些格式中的某些会输出二进制数据,但它们的大小却各不相同。Avro、MessagePack 和 BSON 等二进制格式具有不同的内部结构和编码机制,这可能导致二进制表示法的差异,即使它们最终表示的是相同的数据。下面简要介绍一下这些差异是如何产生的:


1. Avro



  • Avro 使用模式对数据进行编码,这种模式通常包含在二进制表示法中。

  • Avro 基于模式的编码通过提前指定数据结构,实现了高效的数据序列化和反序列化。

  • Avro 的二进制格式设计为自描述格式,这意味着模式信息包含在编码数据中。这种自描述性使 Avro 能够保持不同版本数据模式之间的兼容性。


2. MessagePack



  • MessagePack 是一种二进制序列化格式,直接对数据进行编码,不包含模式信息。

  • 它使用长度可变的整数和长度可变的字符串的紧凑二进制表示法,以尽量减少空间使用。

  • MessagePack 不包含模式信息,因此更适用于模式已提前知晓并在发送方和接收方之间共享的情况。



3. BSON



  • BSON 是 JSON 数据的二进制编码,包括每个值的类型信息。

  • BSON 的设计与 JSON 紧密相连,但它增加了二进制数据类型,如 JSON 缺乏的日期和二进制数据。

  • 与 MessagePack 一样,BSON 不包括模式信息。


这些设计和编码上的差异导致了二进制表示法的不同:



  • Avro 包含模式信息并具有自描述性,因此二进制文件稍大,但与模式兼容。

  • MessagePack 的编码长度可变,因此非常紧凑,但缺乏模式信息,因此适用于已知模式的情况。

  • BSON 与 JSON 关系密切,并包含类型信息,与 MessagePack 等纯二进制格式相比,BSON 的大小会有所增加。


总之,这些差异源于每种格式的设计目标和特点。Avro 优先考虑模式兼容性,MessagePack 侧重于紧凑性,而 BSON 在保持类似 JSON 结构的同时增加了二进制类型。格式的选择取决于您的具体使用情况和要求,如模式兼容性、数据大小和易用性。


优化 JSON 性能


下面是一些优化 JSON 性能的实用技巧以及代码示例和最佳实践:


1.最小化数据大小



  • 使用简短的描述性键名:选择简洁但有意义的键名,以减少 JSON 对象的大小


// Inefficient
{
"customer_name_with_spaces": "John Doe"
}

// Efficient
{
"customerName": "John Doe"
}


  • 尽可能缩写:在不影响清晰度的情况下,考虑对键或值使用缩写。


// 效率低
{
"transaction_type": "purchase"
}

// 效率高
{
"txnType": "purchase"
}

2.明智使用数组



  • 尽量减少嵌套:避免深度嵌套数组,因为它们会增加解析和遍历 JSON 的复杂性。


// 效率低
{
"order": {
"items": {
"item1": "Product A",
"item2": "Product B"
}
}
}

// 效率高
{
"orderItems": ["Product A", "Product B"]
}

3.优化数字表示法


尽可能使用整数:如果数值可以用整数表示,就用整数代替浮点数。


// 效率低
{
"quantity": 1.0
}

// 效率高
{
"quantity": 1
}

4.删除冗余


避免重复数据:通过引用共享值来消除冗余数据。


// 效率低
{
"product1": {
"name": "Product A",
"price": 10
},
"product2": {
"name": "Product A",
"price": 10
}
}

// 效率高
{
"products": [
{
"name": "Product A",
"price": 10
},
{
"name": "Product B",
"price": 15
}
]
}

5.使用压缩


应用压缩算法:如果适用,在传输过程中使用 Gzipor Brotlito 等压缩算法来减小 JSON 有效负载的大小。


// 使用 zlib 进行 Gzip 压缩的 Node.js 示例
const zlib = require('zlib');

const jsonData = {
// 在这里填入你的 JSON 数据
};

zlib.gzip(JSON.stringify(jsonData), (err, compressedData) => {
if (!err) {
// 通过网络发送 compressedData
}
});


6.采用服务器端缓存:


缓存 JSON 响应:实施服务器端缓存,高效地存储和提供 JSON 响应,减少重复数据处理的需要。


7.配置文件和优化


剖析性能:使用剖析工具找出 JSON 处理代码中的瓶颈,然后优化这些部分。


实际优化:在实践中加快 JSON 的处理速度


在本节中,我们将探讨实际案例,这些案例在使用 JSON 时遇到性能瓶颈并成功克服。我们会看到诸如 LinkedIn、Auth0、Uber 等知名技术公司如何解决 JSON 的限制并改善他们应用的性能。这些案例为如何提升应用处理速度和响应性提供了实用的策略。


1.LinkedIn 的协议缓冲区集成:



  • 挑战:LinkedIn 面临的挑战是 JSON 的冗长以及由此导致的网络带宽使用量增加,从而导致延迟增加。

  • 解决方案:他们采用了 Protocol Buffers,这是一种二进制序列化格式,用以替换微服务通信中的 JSON。

  • 影响:这一优化将延迟降低了 60%,提高了 LinkedIn 服务的速度和响应能力。


2.Uber 的 H3 地理索引:


挑战:Uber 使用 JSON 来表示各种地理空间数据,但解析大型数据集的 JSON 会降低其算法速度。


解决方案:他们引入了 H3 Geo-Index,这是一种用于地理空间数据的高效六边形网格系统,可减少 JSON 解析开销。


影响:这一优化大大加快了地理空间业务的发展,增强了 Uber 的叫车和地图服务。


3.Slack 的信息格式优化:


挑战:Slack 需要在实时聊天中传输和呈现大量 JSON 格式的消息,这导致了性能瓶颈。


解决方案:他们优化了 JSON 结构,减少了不必要的数据,只在每条信息中包含必要的信息。


影响:这项优化使得消息展现更快,从而提高了 Slack 用户的整体聊天性能。


4.Auth0 的协议缓冲区实现:


挑战:Auth0 是一个流行的身份和访问管理平台,在处理身份验证和授权数据时面临着 JSON 的性能挑战。


解决方案:他们采用协议缓冲区(Protocol Buffers)来取代 JSON,以编码和解码与身份验证相关的数据。


影响:这一优化大大提高了数据序列化和反序列化的速度,从而加快了身份验证流程,并增强了 Auth0 服务的整体性能。


这些现实世界中的例子展示了通过优化策略解决 JSON 的性能挑战如何对应用程序的速度、响应速度和用户体验产生实质性的积极影响。它们强调了考虑替代数据格式和高效数据结构的重要性,以克服各种情况下与 JSON 相关的速度减慢问题。


结论


在不断变化的网络开发环境中,优化 JSON 性能是一项宝贵的技能,它能让你的项目与众不同,并确保你的应用程序在即时数字体验时代茁壮成长。


作者:王大冶
来源:juejin.cn/post/7303424117243297807
收起阅读 »

解锁前端难题:亲手实现一个图片标注工具

web
本文为稀土掘金技术社区首发签约文章,30 天内禁止转载,30 天后未获授权禁止转载,侵权必究! 业务中涉及图片的制作和审核功能,审核人员需要在图片中进行标注,并说明存在的问题,标注过程中需要支持放大缩小,移动等交互,将业务剥离,这个需求,可以定义为实现一个图...
继续阅读 »

本文为稀土掘金技术社区首发签约文章,30 天内禁止转载,30 天后未获授权禁止转载,侵权必究!



业务中涉及图片的制作和审核功能,审核人员需要在图片中进行标注,并说明存在的问题,标注过程中需要支持放大缩小,移动等交互,将业务剥离,这个需求,可以定义为实现一个图片标注功能。


实现这个功能并不容易,其涉及的前端知识点众多,本文带领大家从零到一,亲手实现一个,支持缩放,移动,编辑的图片标注功能,文字描述是抽象的,眼见为实,实现效果如下所示:


Kapture 2024-03-20 at 18.43.56.gif


技术方案


这里涉及两个关键功能,一个是绘制,包括缩放和旋转,一个是编辑,包括选取和修改尺寸,涉及到的技术包括,缩放,移动,和自定义形状的绘制(本文仅实现矩形),绘制形状的选取,改变尺寸和旋转角度等。


从大的技术选型来说,有两种实现思路,一种是 canvas,一种是 dom+svg,下面简单介绍下两种思路和优缺点。


canvas 可以方便实现绘制功能,但编辑功能就比较困难,当然这可以使用库来实现,这里我们考虑自己亲手实现功能。



  • 优点

    • 性能较好,尤其是在处理大型图片和复杂图形时。

    • 支持更复杂的图形绘制和像素级操作。

    • 一旦图形绘制在 Canvas 上,就不会受到 DOM 的影响,减少重绘和回流。



  • 缺点

    • 交互相对复杂,需要手动管理图形的状态和事件。

    • 对辅助技术(如屏幕阅读器)支持较差。



  • 可能遇到的困难

    • 实现复杂的交互逻辑(如选取、移动、修改尺寸等)可能比较繁琐。

    • 在缩放和平移时,需要手动管理坐标变换和图形重绘。




dom+svg 也可以实现功能,缩放和旋转可以借助 css3 的 transform。



  • 优点

    • 交互相对简单,可以利用 DOM 事件系统和 CSS。

    • 对辅助技术支持较好,有助于提高可访问性。



  • 缺点

    • 在处理大型图片和复杂图形时,性能可能不如 Canvas。

    • SVG 元素数量过多时,可能会影响页面性能。



  • 可能遇到的困难

    • 在实现复杂的图形和效果时,可能需要较多的 SVG 知识和技巧。

    • 管理大量的 SVG 元素和事件可能会使代码变得复杂。




总的来说,如果对性能有较高要求,或需要进行复杂的图形处理和像素操作,可以选择基于 Canvas 的方案。否则可以选择基于 DOM + SVG 的方案。在具体实现时,可以根据项目需求和技术栈进行选择。


下面我们选择基于 canvas 的方案,通过例子,一步一步实现完成功能,让我们先从最简单的开始。


渲染图片


本文我们不讲解 canvas 基础,如果你不了解 canvas,可以先在网上找资料,简单学习下,图片的渲染非常简单,只用到一个 API,这里我们直接给出代码,示例如下:


这里我们提前准备一个 canvas,宽高设定为 1000*700,这里唯一的一个知识点就是要在图片加载完成后再绘制,在实战中,需要注意绘制的图片不能跨域,否则会绘制失败。


<body>
<div>
<canvas id="canvas1" width="1000" height="700"></canvas>
</div>
<script>
const canvas1 = document.querySelector('#canvas1');
const ctx1 = canvas1.getContext('2d');
let width = 1000;
let height = 700;

let img = new Image();
img.src = './bg.png';
img.onload = function () {
draw();
};

function draw() {
console.log('draw');
ctx1.drawImage(img, 0, 0, width, height);
}
</script>
</body>

现在我们已经成功在页面中绘制了一张图片,这非常简单,让我们继续往下看吧。


缩放


实现图片缩放功能,我们需要了解两个关键的知识点:如何监听缩放事件和如何实现图片缩放。


先来看第一个,我用的是 Mac,在 Mac 上可以通过监听鼠标的滚轮事件来实现缩放的监听。当用户使用鼠标滚轮时,会触发 wheel 事件,我们可以通过这个事件的 deltaY 属性来判断用户是向上滚动(放大)还是向下滚动(缩小)。


可以看到在 wheel 事件中,我们修改了 scale 变量,这个变量会在下面用到。这里添加了对最小缩放是 1,最大缩放是 3 的限制。


document.addEventListener(
'wheel',
function (event) {
if (event.ctrlKey) {
// detect pinch
event.preventDefault(); // prevent zoom
if (event.deltaY < 0) {
console.log('Pinching in');
if (scale < 3) {
scale = Math.min(scale + 0.1, 3);
draw();
}
} else {
console.log('Pinching out');
if (scale > 1) {
scale = Math.max(scale - 0.1, 1);
draw();
}
}
}
},
{ passive: false }
);

图片缩放功能,用到了 canvas 的 scale 函数,其可以修改绘制上下文的缩放比例,示例代码如下:


我们添加了clearRect函数,这用来清除上一次绘制的图形,当需要重绘时,就需要使用clearRect函数。


这里需要注意开头和结尾的 save 和 restore 函数,因为我们会修改 scale,如果不恢复的话,其会影响下一次绘制,一般在修改上下文时,都是通过 save 和 restore 来复原的。


let scale = 1;

function draw() {
console.log('draw');
ctx1.clearRect(0, 0, width, height);
ctx1.save();
ctx1.scale(scale, scale);
ctx1.drawImage(img, 0, 0, width, height);
ctx1.restore();
}

这里稍微解释一下 scale 函数,初次接触,可能会不太好理解。在 Canvas 中使用 scale 函数时,重要的是要理解它实际上是在缩放绘图坐标系统,而不是直接缩放绘制的图形。当你调用 ctx.scale(scaleX, scaleY) 时,你是在告诉 Canvas 之后的所有绘图操作都应该在一个被缩放的坐标系统中进行。


这意味着,如果你将缩放比例设置为 2,那么在这个缩放的坐标系统中,绘制一个宽度为 50 像素的矩形,实际上会在画布上产生一个宽度为 100 像素的矩形。因为在缩放的坐标系统中,每个单位长度都变成了原来的两倍。


因此,当我们谈论 scale 函数时,重点是要记住它是在缩放整个绘图坐标系统,而不是单独的图形。这就是为什么在使用 scale 函数后,所有的绘图操作(包括位置、大小等)都会受到影响。


现在我们已经实现了图片的缩放功能,效果如下所示:


Kapture 2024-03-21 at 15.20.58.gif


鼠标缩放


细心的你可能发现上面的缩放效果是基于左上角的,基于鼠标点缩放意味着图片的缩放中心是用户鼠标所在的位置,而不是图片的左上角或其他固定点。这种缩放方式更符合用户的直觉,可以提供更好的交互体验。


为了实现这种效果,可以使用 tanslate 来移动原点,canvas 中默认的缩放原点是左上角,具体方法是,可以在缩放前,将缩放原点移动到鼠标点的位置,缩放后,再将其恢复,这样就不会影响后续的绘制,实现代码如下所示:


let scaleX = 0;
let scaleY = 0;

function draw() {
ctx1.clearRect(0, 0, width, height);
ctx1.save();
// 注意这行1
ctx1.translate(scaleX, scaleY);
ctx1.scale(scale, scale);
// 注意这行2
ctx1.translate(-scaleX, -scaleY);
ctx1.drawImage(img, 0, 0, width, height);
ctx1.restore();
}

scaleX 和 scaleY 的值,可以在缩放的时候设置即可,如下所示:


// zoom
document.addEventListener(
'wheel',
function (event) {
if (event.ctrlKey) {
if (event.deltaY < 0) {
if (scale < 3) {
// 注意这里两行
scaleX = event.offsetX;
scaleY = event.offsetY;
scale = Math.min(scale + 0.1, 3);
draw();
}
}
// 省略代码
}
},
{ passive: false }
);

现在我们已经实现了图片的鼠标缩放功能,效果如下所示:


3.gif


移动视口


先解释下放大时,可见区域的概念,好像叫视口吧
当处于放大状态时,会导致图像只能显示一部分,此时需要能过需要可以移动可见的图像,
这里选择通过触摸板的移动,也就是 wheel 来实现移动视口


通过 canvas 的 translate 来实现改变视口


在图片放大后,整个图像可能无法完全显示在 Canvas 上,此时只有图像的一部分(即可见区域)会显示在画布上。这个可见区域也被称为“视口”。为了查看图像的其他部分,我们需要能够移动这个视口,即实现图片的平移功能。


在放大状态下,视口的大小相对于整个图像是固定的,但是它可以在图像上移动以显示不同的部分。你可以将视口想象为一个固定大小的窗口,你通过这个窗口来观察一个更大的图像。当你移动视口时,窗口中显示的图像部分也会相应改变。


为了实现移动视口,我们可以通过监听触摸板的移动事件(也就是 wheel 事件)来改变视口的位置。当用户通过触摸板进行上下或左右滑动时,我们可以相应地移动视口,从而实现图像的平移效果。


我们可以使用 Canvas 的 translate 方法来改变视口的位置。translate 方法接受两个参数,分别表示沿 x 轴和 y 轴移动的距离。在移动视口时,我们需要更新图片的位置,并重新绘制图像以反映新的视口位置。


代码改动如下所示:


let translateX = 0;
let translateY = 0;

function draw() {
// 此处省略代码
// 改变视口
ctx1.translate(translateX, translateY);

ctx1.drawImage(img, 0, 0, width, height);
ctx1.restore();
}

// translate canvas
document.addEventListener(
"wheel",
function (event) {
if (!event.ctrlKey) {
// console.log("translate", event.deltaX, event.deltaY);
event.preventDefault();
translateX -= event.deltaX;
translateY -= event.deltaY;
draw();
}
},
{ passive: false }
);

在这个示例中,translateXtranslateY 表示视口的位置。当用户通过触摸板进行滑动时,我们根据滑动的方向和距离更新视口的位置,并重新绘制图像。通过这种方式,我们可以实现图像的平移功能,允许用户查看图像的不同部分。


现在我们已经实现了移动视口功能,效果如下所示:


4.gif


绘制标注


为了便于大家理解,这里我们仅实现矩形标注示例,实际业务中可能存在各种图形的标记,比如圆形,椭圆,直线,曲线,自定义图形等。


我们先考虑矩形标注的绘制问题,由于 canvas 是位图,我们需要在 js 中存储矩形的数据,矩形的存储需要支持坐标,尺寸,旋转角度和是否在编辑中等。因为可能存在多个标注,所以需要一个数组来存取标注数据,我们将标注存储在reacts中,示例如下:


let rects = [
{
x: 650,
y: 350,
width: 100,
height: 100,
isEditing: false,
rotatable: true,
rotateAngle: 30,
},
];

下面将 rects 渲染到 canvas 中,示例代码如下:


代码扩机并不复杂,比较容易理解,值得一提的rotateAngle的实现,我们通过旋转上下文来实现,其旋转中心是矩形的图形的中心点,因为操作上线文,所以在每个矩形绘制开始和结束后,要通过saverestore来恢复之前的上下文。


isEditing表示当前的标注是否处于编辑状态,在这里编辑中的矩形框,我们只需设置不同的颜色即可,在后面我们会实现编辑的逻辑。


function draw() {
// 此处省略代码
ctx1.drawImage(img, 0, 0, width, height);

rects.forEach((r) => {
ctx1.strokeStyle = r.isEditing ? 'rgba(255, 0, 0, 0.5)' : 'rgba(255, 0, 0)';

ctx1.save();
if (r.rotatable) {
ctx1.translate(r.x + r.width / 2, r.y + r.height / 2);
ctx1.rotate((r.rotateAngle * Math.PI) / 180);
ctx1.translate(-(r.x + r.width / 2), -(r.y + r.height / 2));
}
ctx1.strokeRect(r.x, r.y, r.width, r.height);
ctx1.restore();
});

ctx1.restore();
}

现在我们已经实现了标注绘制功能,效果如下所示:


5.png


添加标注


为了在图片上添加标注,我们需要实现鼠标按下、移动和抬起时的事件处理,以便在用户拖动鼠标时动态地绘制一个矩形标注。同时,由于视口可以放大和移动,我们还需要进行坐标的换算,确保标注的位置正确。


首先,我们需要定义一个变量 drawingRect 来存储正在添加中的标注数据。这个变量将包含标注的起始坐标、宽度和高度等信息:


let drawingRect = null;

接下来,我们需要实现鼠标按下、移动和抬起的事件处理函数:


mousedown中我们需要记录鼠标按下时,距离视口左上角的坐标,并将其记录到全局变量startXstartY中。


mousemove时,需要更新当前在绘制矩形的数据,并调用draw完成重绘。


mouseup时,需要处理添加操作,将矩形添加到rects中,在这里我做了一个判断,如果矩形的宽高小于 1,则不添加,这是为了避免在鼠标原地点击时,误添加图形的问题。


let startX = 0;
let startY = 0;
canvas1.addEventListener('mousedown', (e) => {
startX = e.offsetX;
startY = e.offsetY;
const { x, y } = computexy(e.offsetX, e.offsetY);

console.log('mousedown', e.offsetX, e.offsetY, x, y);

drawingRect = drawingRect || {};
});
canvas1.addEventListener('mousemove', (e) => {
// 绘制中
if (drawingRect) {
drawingRect = computeRect({
x: startX,
y: startY,
width: e.offsetX - startX,
height: e.offsetY - startY,
});
draw();
return;
}
});
canvas1.addEventListener('mouseup', (e) => {
if (drawingRect) {
drawingRect = null;
// 如果绘制的矩形太小,则不添加,防止原地点击时添加矩形
// 如果反向绘制,则调整为正向
const width = Math.abs(e.offsetX - startX);
const height = Math.abs(e.offsetY - startY);
if (width > 1 || height > 1) {
const newrect = computeRect({
x: Math.min(startX, e.offsetX),
y: Math.min(startY, e.offsetY),
width,
height,
});
rects.push(newrect);
draw();
}
return;
}
});

下面我们来重点讲讲上面的computexycomputeRect函数,由于视口可以放大和移动,我们需要将鼠标点击时的视口坐标换算为 Canvas 坐标系的坐标。


宽高的计算比较简单,只需要将视口坐标除以缩放比例即可得到。但坐标的计算并不简单,这里通过视口坐标,直接去推 canvas 坐标是比较困难的,我们可以求出 canvas 坐标计算视口坐标的公式,公式推导如下:


vx: 视口坐标
x: canvas坐标
scale: 缩放比例
scaleX: 缩放原点
translateX: 视口移动位置

我们x会在如下视口操作后进行渲染成vx:
1: ctx1.translate(scaleX, scaleY);
2: ctx1.scale(scale, scale);
3: ctx1.translate(-scaleX, -scaleY);
4: ctx1.translate(translateX, translateY);

根据上面的步骤,每一步vx的推演如下:
1: vx = x + scaleX
2: vx = x * scale + scaleX
3: vx = x * scale + scaleX - scaleX * scale
4: vx = x * scale + scaleX - scaleX * scale + translateX * scale

通过上面 vx 和 x 的公式,我们可以计算出来 x 和 vx 的关系如下,我在这里走了很多弯路,导致计算的坐标一直不对,不要试图通过 vx 直接推出 x,一定要通过上面的公式来推导:


x = (vx - scaleX * (1 - scale) - translateX * scale) / scale

理解了上面坐标和宽高的计算公式,下面的代码就好理解了:


function computexy(x, y) {
const xy = {
x: (x - scaleX * (1 - scale) - translateX * scale) / scale,
y: (y - scaleY * (1 - scale) - translateY * scale) / scale,
};
return xy;
}
function computewh(width, height) {
return {
width: width / scale,
height: height / scale,
};
}
function computeRect(rect) {
const cr = {
...computexy(rect.x, rect.y),
...computewh(rect.width, rect.height),
};
return cr;
}

最后,我们需要一个函数来绘制标注矩形:


function draw() {
// 此处省略代码
if (drawingRect) {
ctx1.strokeRect(
drawingRect.x,
drawingRect.y,
drawingRect.width,
drawingRect.height
);
}
ctx1.restore();
}

现在我们已经实现了添加标注功能,效果如下所示:


6.gif


选取标注


判断选中,将视口坐标,转换为 canvas 坐标,遍历矩形,判断点在矩形内部
同时需要考虑点击空白处,清空选中状态
选中其他元素时,清空上一个选中的元素
渲染选中状态,选中状态改变边的颜色,为了明显,红色变为绿色
要是先选取元素的功能,关键要实现的判断点在矩形内部,判断点在矩形内部的逻辑比较简单,我们可以抽象为如下函数:


function poInRect({ x, y }, rect) {
return (
x >= rect.x &&
x <= rect.x + rect.width &&
y >= rect.y &&
y <= rect.y + rect.height
);
}

在点击事件中,我们拿到的是视口坐标,首先将其转换为 canvas 坐标,然后遍历矩形数组,判断是否有中选的矩形,如果有的话将其存储下来。


还需要考虑点击新元素时,和点击空白时,重置上一个元素的选中态的逻辑,代码实现如下所示:


canvas1.addEventListener('mousedown', (e) => {
startX = e.offsetX;
startY = e.offsetY;
const { x, y } = computexy(e.offsetX, e.offsetY);

const pickRect = rects.find((r) => {
return poInRect({ x, y }, r);
});

if (pickRect) {
if (editRect && pickRect !== editRect) {
// 选择了其他矩形
editRect.isEditing = false;
editRect = null;
}
pickRect.isEditing = true;
editRect = pickRect;
draw();
} else {
if (editRect) {
editRect.isEditing = false;
editRect = null;
draw();
}
drawingRect = drawingRect || {};
}
});

现在我们已经实现了选取标注功能,效果如下所示:


7.gif


移动


接下来是移动,也就是通过拖拽来改变已有图形的位置
首先需要一个变量来存取当前被拖拽元素,在 down 和 up 时更新这个元素
要实现拖拽,需要一点小技巧,在点击时,计算点击点和图形左上角的坐标差,在每次 move 时,用当前坐标减去坐标差即可
不要忘了将视口坐标,换算为 canvas 坐标哦


接下来,我们将实现通过拖拽来改变已有标注的位置的功能。这需要跟踪当前被拖拽的标注,并在鼠标移动时更新其位置。


首先,我们需要一个变量来存储当前被拖拽的标注:


let draggingRect = null;

在鼠标按下时(mousedown 事件),我们需要判断是否点击了某个标注,并将其设置为被拖拽的标注,并在鼠标抬起时(mouseup 事件),将其置空。


要实现完美的拖拽效果,需要一点小技巧,在点击时,计算点击点和图形左上角的坐标差,将其记录到全局变量shiftXshiftY,关键代码如下所示。


let shiftX = 0;
let shiftY = 0;
canvas1.addEventListener('mousedown', (e) => {
const { x, y } = computexy(e.offsetX, e.offsetY);

if (pickRect) {
// 计算坐标差
shiftX = x - pickRect.x;
shiftY = y - pickRect.y;
// 标记当前拖拽元素
draggingRect = pickRect;
draw();
}
});
canvas1.addEventListener('mouseup', (e) => {
if (draggingRect) {
// 置空当前拖拽元素
draggingRect = null;
return;
}
});

在鼠标移动时(mousemove 事件),如果有标注被拖拽,则更新其位置,关键代码如下所示。


canvas1.addEventListener('mousemove', (e) => {
const { x, y } = computexy(e.offsetX, e.offsetY);

// 当前正在拖拽矩形
if (draggingRect) {
draggingRect.x = x - shiftX;
draggingRect.y = y - shiftY;
draw();
return;
}
});

现在我们已经实现了移动功能,效果如下所示:


8.gif


修改尺寸


为了实现标注尺寸的修改功能,我们可以在标注的四个角和四条边的中点处显示小方块作为编辑器,允许用户通过拖拽这些小方块来改变标注的大小。


首先,我们需要实现编辑器的渲染逻辑。我们可以在 drawEditor 函数中添加代码来绘制这些小方块。


在这里,我们使用 computeEditRect 函数来计算标注的八个编辑点的位置,并在 drawEditor 函数中绘制这些小方块,关键代码如下所示:


在这个例子中,我们只展示了上边中间编辑点的处理逻辑,其他编辑点的处理逻辑类似。


function computeEditRect(rect) {
let width = 10;
let linelen = 16;
return {
t: {
type: "t",
x: rect.x + rect.width / 2 - width / 2,
y: rect.y - width / 2,
width,
height: width,
},
b: {// 代码省略},
l: {// 代码省略},
r: {// 代码省略},
tl: {// 代码省略},
tr: {// 代码省略},
bl: {// 代码省略},
br: {// 代码省略},
};
}
function drawEditor(rect) {
ctx1.save();
const editor = computeEditRect(rect);
ctx1.fillStyle = "rgba(255, 150, 150)";

// 绘制矩形
for (const r of Object.values(editor)) {
ctx1.fillRect(r.x, r.y, r.width, r.height);
}
ctx1.restore();
}
function draw() {
rects.forEach((r) => {
// 添加如下代码
if (r.isEditing) {
drawEditor(r);
}
});
}

接下来,我们需要实现拖动这些编辑点来改变标注大小的功能。首先,我们需要在鼠标按下时判断是否点击了某个编辑点。


在这里,我们使用 poInEditor 函数来判断鼠标点击的位置是否接近某个编辑点。如果是,则设置 startEditRect, dragingEditor, editorShiftXY 来记录正在调整大小的标注和编辑点。


let startEditRect = null;
let dragingEditor = null;
let editorShiftX = 0;
let editorShiftY = 0;
function poInEditor(point, rect) {
const editor = computeEditRect(rect);
if (!editor) return;

for (const edit of Object.values(editor)) {
if (poInRect(point, edit)) {
return edit;
}
}
}
canvas1.addEventListener('mousedown', (e) => {
startX = e.offsetX;
startY = e.offsetY;
const { x, y } = computexy(e.offsetX, e.offsetY);

if (editRect) {
const editor = poInEditor({ x, y }, editRect);
if (editor) {
// 调整大小
startEditRect = { ...editRect };
dragingEditor = editor;
editorShiftX = x - editor.x;
editorShiftY = y - editor.y;
return;
}
}
});

然后,在鼠标移动时,我们需要根据拖动的编辑点来调整标注的大小。


在这个例子中,我们只展示了上边中间编辑点的处理逻辑,其他编辑点的处理逻辑类似。通过拖动不同的编辑点,我们可以实现标注的不同方向和维度的大小调整。


canvas1.addEventListener('mousemove', (e) => {
const { x, y } = computexy(e.offsetX, e.offsetY);

// 如果存在编辑中的元素
if (editRect) {
const editor = poInEditor({ x, y }, editRect);
// 调整大小中
if (dragingEditor) {
const moveX = (e.offsetX - startX) / scale;
const moveY = (e.offsetY - startY) / scale;

switch (dragingEditor.type) {
case 't':
editRect.y = startEditRect.y + moveY;
editRect.height = startEditRect.height - moveY;
break;
}
draw();
return;
}
}
});

现在我们已经实现了修改尺寸功能,效果如下所示:


9.gif


旋转


实现旋转编辑器的渲染按钮,在顶部增加一个小方块的方式来实现,


旋转图形会影响选中图形的逻辑,即点在旋转图形里的判断,这块的逻辑需要修改


接下来实现旋转逻辑,会涉及 mousedown 和 mousemove


接下来介绍旋转,这一部分会有一定难度,涉及一些数学计算,而且旋转逻辑会修改多出代码,下面我们依次介绍。


旋转涉及两大块功能,一个是旋转编辑器,一个是旋转逻辑,我们先来看旋转编辑器,我们可以在标注的顶部增加一个用于旋转的小方块作为旋转编辑器,如下图所示:


image.png


下面修改我们的drawEditorcomputeEditRect函数,增加渲染逻辑,涉及一个方块和一条线的渲染。


其中rotr就是顶部的方块,rotl是那条竖线。


function computeEditRect(rect) {
let width = 10;
let linelen = 16;
return {
...(rect.rotatable
? {
rotr: {
type: 'rotr',
x: rect.x + rect.width / 2 - width / 2,
y: rect.y - width / 2 - linelen - width,
width,
height: width,
},
rotl: {
type: 'rotl',
x1: rect.x + rect.width / 2,
y1: rect.y - linelen - width / 2,
x2: rect.x + rect.width / 2,
y2: rect.y - width / 2,
},
}
: null),
};
}
function drawEditor(rect) {
ctx1.save();
const editor = computeEditRect(rect);
ctx1.fillStyle = 'rgba(255, 150, 150)';
const { rotl, rotr, ...rects } = editor;

// 绘制旋转按钮
if (rect.rotatable) {
ctx1.fillRect(rotr.x, rotr.y, rotr.width, rotr.height);
ctx1.beginPath();
ctx1.moveTo(rotl.x1, rotl.y1);
ctx1.lineTo(rotl.x2, rotl.y2);
ctx1.stroke();
}

// 绘制矩形
// ...
}

在实现旋转逻辑之前,先来看一个问题,如下图所示,当我们在绿色圆圈区按下鼠标时,在我们之前的逻辑中,也会触发选中状态。


image.png


这是因为我们判断点在矩形内部的逻辑,并未考虑旋转的问题,我们的矩形数据存储了矩形旋转之前的坐标和旋转角度,如下所示。


let rects = [
{
x: 650,
y: 350,
width: 100,
height: 100,
isEditing: false,
rotatable: true,
rotateAngle: 30,
},
];

解决这个问题有两个思路,一个是将旋转后矩形的四个点坐标计算出来,这种方法比较麻烦。另一个思路是逆向的,将要判断的点,以矩形的中点为中心,做逆向旋转,计算出其在 canvas 中的坐标,这个坐标,可以继续参与我们之前点在矩形内的计算。


关键代码如下所示,其中rotatePoint是计算 canvas 中的坐标,poInRotRect判断给定点是否在旋转矩形内部。


// 将点绕 rotateCenter 旋转 rotateAngle 度
function rotatePoint(point, rotateCenter, rotateAngle) {
let dx = point.x - rotateCenter.x;
let dy = point.y - rotateCenter.y;

let rotatedX =
dx * Math.cos((-rotateAngle * Math.PI) / 180) -
dy * Math.sin((-rotateAngle * Math.PI) / 180) +
rotateCenter.x;
let rotatedY =
dy * Math.cos((-rotateAngle * Math.PI) / 180) +
dx * Math.sin((-rotateAngle * Math.PI) / 180) +
rotateCenter.y;

return { x: rotatedX, y: rotatedY };
}

function poInRotRect(
point,
rect,
rotateCenter = {
x: rect.x + rect.width / 2,
y: rect.y + rect.height / 2,
},
rotateAngle = rect.rotateAngle
) {
if (rotateAngle) {
const rotatedPoint = rotatePoint(point, rotateCenter, rotateAngle);
const res = poInRect(rotatedPoint, rect);
return res;
}
return poInRect(point, rect);
}

接下来实现旋转逻辑,这需要改在 mousedown 和 mousemove 事件,实现拖动时的实时旋转。


在 mousedown 时,判断如果点击的是旋转按钮,则将当前矩形记录到全局变量rotatingRect


canvas1.addEventListener('mousedown', (e) => {
startX = e.offsetX;
startY = e.offsetY;
const { x, y } = computexy(e.offsetX, e.offsetY);

if (editRect) {
const editor = poInEditor({ x, y }, editRect);
if (editor) {
// 调整旋转
if (editor.type === 'rotr') {
rotatingRect = editRect;
prevX = e.offsetX;
prevY = e.offsetY;
return;
}
// 调整大小
}
}
});

在 mousemove 时,判断如果是位于旋转按钮上,则计算旋转角度。


canvas1.addEventListener('mousemove', (e) => {
// 绘制中
const { x, y } = computexy(e.offsetX, e.offsetY);
// 当前正在拖拽矩形

// 如果存在编辑中的元素
if (editRect) {
const editor = poInEditor({ x, y }, editRect);
console.log('mousemove', editor);

// 旋转中
if (rotatingRect) {
const relativeAngle = getRelativeRotationAngle(
computexy(e.offsetX, e.offsetY),
computexy(prevX, prevY),
{
x: editRect.x + editRect.width / 2,
y: editRect.y + editRect.height / 2,
}
);
console.log('relativeAngle', relativeAngle);
editRect.rotateAngle += (relativeAngle * 180) / Math.PI;
prevX = e.offsetX;
prevY = e.offsetY;
draw();
return;
}

// 调整大小中
}
});

将拖拽移动的距离,转换为旋转的角度,涉及一些数学知识,其原理是通过上一次鼠标位置和本次鼠标位置,计算两个点和旋转中心(矩形的中心)三个点,形成的夹角,示例代码如下:


function getRelativeRotationAngle(point, prev, center) {
// 计算上一次鼠标位置和旋转中心的角度
let prevAngle = Math.atan2(prev.y - center.y, prev.x - center.x);

// 计算当前鼠标位置和旋转中心的角度
let curAngle = Math.atan2(point.y - center.y, point.x - center.x);

// 得到相对旋转角度
let relativeAngle = curAngle - prevAngle;

return relativeAngle;
}

现在我们已经实现了旋转功能,效果如下所示:


10.gif


总结


在本文中,我们一步一步地实现了一个功能丰富的图片标注工具。从最基本的图片渲染到复杂的标注编辑功能,包括缩放、移动、添加标注、选择标注、移动标注、修改标注尺寸、以及标注旋转等,涵盖了图片标注工具的核心功能。


通过这个实例,我们可以看到,实现一个前端图片标注工具需要综合运用多种前端技术和知识,包括但不限于:



  • Canvas API 的使用,如绘制图片、绘制形状、图形变换等。

  • 鼠标事件的处理,如点击、拖拽、滚轮缩放等。

  • 几何计算,如点是否在矩形内、旋转角度的计算等。


希望这个实例能够为你提供一些启发和帮助,让你在实现自己的图片标注工具时有一个参考和借鉴。


更进一步


站在文章的角度,到此为止,下面让我们站在更高的维度思考更进一步的可能,我们还能继续做些什么呢?


在抽象层面,我们可以考虑将图片标注工具的核心功能进行进一步的抽象和封装,将其打造成一个通用的开源库。这样,其他开发者可以直接使用这个库来快速实现自己的图片标注需求,而无需从零开始。为了实现这一目标,我们需要考虑以下几点:



  • 通用性:库应该支持多种常见的标注形状和编辑功能,以满足不同场景的需求。

  • 易用性:提供简洁明了的 API 和文档,使得开发者能够轻松集成和使用。

  • 可扩展性:设计上应该留有足够的灵活性,以便开发者可以根据自己的特定需求进行定制和扩展。

  • 性能优化:注重性能优化,确保库在处理大型图片或复杂标注时仍能保持良好的性能。


在产品层面,我们可以基于这个通用库,进一步开发成一个功能完备的图片标注工具,提供开箱即用的体验。这个工具可以包括以下功能:



  • 多种标注类型:支持矩形、圆形、多边形等多种标注类型。

  • 标注管理:提供标注的增加、删除、编辑、保存等管理功能。

  • 导出和分享:支持导出标注结果为各种格式,如 JSON、XML 等,以及分享给他人协作编辑。

  • 用户界面:提供友好的用户界面,支持快捷键操作,提高标注效率。

  • 集成与扩展:支持与其他系统或工具的集成,提供 API 接口和插件机制,以便进行功能扩展。


通过不断地迭代和优化,我们可以使这个图片标注工具成为业界的标杆,为用户提供高效便捷的标注体验。


感谢您的阅读和关注!希望这篇文章能够为您在前端开发中实现图像标注功能提供一些有价值的见解和启发。如果您有任何问题、建议或想要分享自己的经验,欢迎在评论区留言交流。让我们一起探索更多前端技术的可能性,不断提升我们的技能和创造力!


本文示例源码:github.com/yanhaijing/…


本文示例预览:yanhaijing.com/imagic/demo…


作者:颜海镜
来源:juejin.cn/post/7350954669742768147
收起阅读 »

Window.print() 实现浏览器打印

web
前言 由于在公司项目中有打印的具体业务场景,在查询相关资料后,找到了 Window.print() 是用来打印的方法,写下这篇文章供自己和大家查漏补缺。 语法 window.print(); 该方法没有参数和返回值,在页面中直接调用,将直接打印整个页面,具体...
继续阅读 »

前言


由于在公司项目中有打印的具体业务场景,在查询相关资料后,找到了 Window.print() 是用来打印的方法,写下这篇文章供自己和大家查漏补缺。


语法


window.print();

该方法没有参数和返回值,在页面中直接调用,将直接打印整个页面,具体使用如下面代码所示



在点击打印该页面按钮后,会触发浏览器的打印对话框,对话框里有一些配置项,可以设置打印的相关参数。


image.png


根据上面的方法,我们就可以实现在浏览器中打印页面。


但是实际的开发中,我们的业务场景比这更加复杂。例如:只打印某个 DOM 元素,需要根据用户需求调整纸张的大小和形状,调整打印的布局,字体大小,缩放比例等等。这些都是常见的情况,那我们应该怎么做呢?


使用 @media 媒体查询


媒体查询 MDN解释:
@media CSS @ 规则可用于基于一个或多个媒体查询的结果来应用样式表的一部分。使用它,你可以指定一个媒体查询和一个 CSS 块,当且仅当该媒体查询与正在使用其内容的设备匹配时,该 CSS 块才能应用于该文档。


简单来说就是使用媒体查询根据不同的条件来决定应用不同的样式。具体到我们的需求就是,在打印时,使用专门的打印样式,隐藏其他元素,实现只打印某个元素的效果



使用样式表


在你的 标签中添加 link 元素,倒入专门供打印使用的样式表。你可以在样式表中编写打印是的具体样式。


<link href="/media/css/print.css" media="print" rel="stylesheet" />

打印页面时常用的一些样式


利用 print 设置打印页面的样式,利用 page 设置打印文档的纸张配置


 /* print.css */
@media print {
* {
-webkit-print-color-adjust: exact !important; /* 保证打印出来颜色与页面一致 */
}
.no-break {
page-break-inside: avoid; /* 避免元素被剪切 */
}
.no-print {
display: none; /* 不想打印的元素设置隐藏 */
}

@page {
size: A4 prtrait; /* 设置打印纸张尺寸及打印方向:A4,纵向打印 */
margin-top: 3cm /* 利用 margin 设置页边距 */
}

}

使用 iframe 实现更加精细的打印


我们也可以将想要打印的内容在 iframe 中渲染出来并打印,通过创建一个隐藏的 iframe 元素,将想要打印的内容放入其中,然后触发 iframe 的打印功能,实现更加灵活的打印。伪代码如下所示:


<!doctype html>
<html lang="en-US">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>Use ifame print example</title>
<script>
function printPage() {
// 新建一个 iframe 元素并隐藏
const iframe = document.createElement("iframe");
iframe.style.position = "fixed";
iframe.style.right = "0";
iframe.style.bottom = "0";
iframe.style.width = "0";
iframe.style.height = "0";
iframe.style.border = "0";
// 将它添加到 document 中
document.body.appendChild(hideFrame);

// 拿到新创建的 iframe 的文档流
const iframeDocument = iframe.contentDocument;

// 在 iframe 中新建一个 link 标签,引入专门用于打印的样式表
const printCssLink = iframeDocument.createElement('link')
printCssLink.rel = 'stylesheet';
printCssLink.type = 'text/css';
printCssLink.media = 'print';
printCssLink.href = `medis/css/print.css`;
iframeDocument.head.appendChild(printCssLink);

// 在 iframe 中新建一个容器,用来存放你想要打印的元素
let printContainer = iframeDocument.createElement('div');
iframeDocument.body.appendChild(printContainer);

iframeDocument.close(); // 关闭 iframe 文档流

// 获取 iframe 的 window 对象
const iframeWindow = iframe.contentWindow;
// 当 iframe 内容加载完成后触发打印功能。
iframeWindow.onload = function() {
iframeWindow.print();
}
// 打印完后移除 iframe 元素
document.body.removeChild(iframe);
}
</script>
</head>
<body>
<p>
<span onclick="printPage();">
Print external page!
</span>
</p>
</body>
</html>

以上就是关于我在使用 Window.print() 打印页面的一些总结,欢迎大家有问题在评论区讨论学习。


作者:It_Samba
来源:juejin.cn/post/7267091417021628475
收起阅读 »

如何正确编写一个占满全部可视区域的组件(hero component)?

web
什么是 hero component hero component 或者 hero image 是一个网页设计术语,用于描述欢迎访问者访问网页的全屏视频、照片、插图或横幅。该图像始终位于网页顶部附近的显着位置,通常会在屏幕上延伸整个宽度。 我们经常见到这种...
继续阅读 »

什么是 hero component



hero component 或者 hero image 是一个网页设计术语,用于描述欢迎访问者访问网页的全屏视频、照片、插图或横幅。该图像始终位于网页顶部附近的显着位置,通常会在屏幕上延伸整个宽度。



我们经常见到这种 hero component,在视觉上占据了整个视口的宽度和高度。比如特斯拉的官网:


image.png


如何实现一个 hreo component?


很多人可能会不假思索的写出下面的 css 代码:


.hero {
width: 100vw;
height: 100vh;
}

你写完了这样的代码,满意地提测后。测试同学走过来告诉你:你的代码在苹果手机上不好使了!


image.png


如图所示,hreo component 被搜索栏挡住了。


又是 safari 的问题!我们可以先停止关于 Is Safari the new Internet Explorer? 的辩论,看看问题的成因和解决办法。


什么是视口单位


vh 单位最初存在时被定义为:等于初始包含块高度的 1%。



  • vw = 视口尺寸宽度的 1%。

  • vh = 视口大小高度的 1%。


将元素的宽度设置为 100vw,高度设置为 100vh,它就会完全覆盖视口。这就是上面 hero component 实现的基本原理:


image.png
这看起来非常完美,但在移动设备上。苹果手机的工程师觉得我们应该最大化利用手机浏览器的空间,于是在 Safari 上引入了动态工具栏,动态工具栏会随着用户的滑动而收起。


页面会表现为:高度为 100vh 的元素将从视口中溢出


image.png


当页面向下滚动时,动态工具栏会收起。在这种状态下,高度设为 100vh 的元素将覆盖整个视口。


image.png



图片来自于:大型、小型和动态视口单元



svh / lvh / dvh


为了解决上面提到的问题,2019 年,一个新的 CSS 提案诞生了。



上面的解释来自于 MDN,读起来有点拗口,其实顾名思义,再结合下面的动图就很好理解:
dvh.gif



在 tailwindcss 的文档中,对 dvh 有一个漂亮的动画演示:tailwindcss.com/blog/tailwi…



用以上的属性可以完美解决上面的 safari 中视口大小的问题,值得一提的是,有的人会建议始终使用 dvh 代替 vh。从上面的动图可以看到,dvh 在手机上其实会有个延迟和跳跃。所以是否使用dvh还是看业务的实际场景,就我而言,上面的 hero component 的例子使用 lvh 更合适。


兼容性


你可以在 can I use 中查看兼容性:


image.png


可以看到,这三个 css 属性还算是比较新的特性,如果为了兼容旧浏览器,最好是把 vh 也加上:


.hero {
width: 100vw;
height: 100vh;
height: 100dvh;
}

这样,即使在不支持新特性的浏览器中,也会降级到 vh 的效果。


感谢阅读本文~


作者:李章鱼
来源:juejin.cn/post/7352079427863592971
收起阅读 »

如何完成一个完全不依赖客户端时间的倒计时

web
前言 最近在做一个调查问卷系统,其中有一个需求就是倒计 40 分钟以后自动提交问卷。由于 UI 库使用的是 antd,所以我第一反应是使用 antd 的 CountDown 组件。 于是我就愉快的写出以下代码: import { Statistic } fro...
继续阅读 »

前言


最近在做一个调查问卷系统,其中有一个需求就是倒计 40 分钟以后自动提交问卷。由于 UI 库使用的是 antd,所以我第一反应是使用 antdCountDown 组件。
于是我就愉快的写出以下代码:


import { Statistic } from 'antd';
const { Countdown } = Statistic;

const TOTAL_TIME = 40;
const deadline = dayjs(startTime).add(TOTAL_TIME, 'minute').valueOf();


function TitleAndCountDown() {
useEffect(() => {
if (currentTime >= deadline) {
onFinish();
}
}, []);

return (
<Countdown
value={deadline}
onFinish={onFinish}
format="mm:ss"
prefix={<img src={clock} style={{ width: 25, height: 25 }} />
}
/>

);
}

其中 startTimecurrentTime 是服务端给我返回的开始答题时间以及现在的时间,onFinish 是提交问卷的函数。测试一切正常,并且看起来好像没有依赖客户端时间,于是我就愉快的提交了代码。


antd 的问题


上线后,有客户反映倒计时不正常,进入系统后直接显示 9000 多秒,导致业务直接进行不下去。这个时候我就懵了,我的代码中并没有依赖任何客户端时间,问题肯定是出现在 antdCountDown 组件上。于是我就去看了一下 antdCountDown 组件的源码,果不其然


 // 30帧
const REFRESH_INTERVAL= 1000 / 30;

const stopTimer = () => {
onFinish?.();
if (countdown.current) {
clearInterval(countdown.current);
countdown.current = null;
}
};

const syncTimer = () => {
const timestamp = getTime(value);
if (timestamp >= Date.now()) {
countdown.current = setInterval(() => {
forceUpdate();
onChange?.(timestamp - Date.now());
if (timestamp < Date.now()) {
stopTimer();
}
}, REFRESH_INTERVAL);
}
};

React.useEffect(() => {
syncTimer();
return () => {
if (countdown.current) {
clearInterval(countdown.current);
countdown.current = null;
}
};
}, [value]);

核心代码就是这段,本质 CountDown 并不是一个倒计时,而是根据客户端时间算出来的一个时间差值,这也能解释为啥这个倒计时相对比较准确。


但是依赖了客户端时间,就意味客户的本地时间会影响这个倒计时的准确性,甚至可以直接通过修改本地时间来绕过倒计时。一开始我的方案是加入 diff 值修正客户端时间,我也给 antd 官方提了一个 PR,但是被拒绝了。后来想了一下 CountDown 组件可以直接传入 diff 后的 value,确实没有必要新增 props


这个方案后来也是被否了,因为还是依赖了客户端时间。客户的机房条件比较复杂,可能一开始时间不对,但是做题途中时间会校正回来。因为我们这个调查系统短时间有几十万人参加调查,为了不给服务器过多的压力,查询服务器时间接口的频率是 1 分钟一次,所以会有很长时间的倒计时异常。


完全不依赖客户端时间的倒计时


倒计时的方案大致有 4 种, setTimeoutsetIntervalrequestAnimationFrameWeb WorkerrequestAnimationFrameWeb Worker 因为兼容性问题暂时放弃。


setInterval 实现倒计时是比较方便的,但是 setInterval 有两个缺点



  1. 使用 setInterval 时,某些间隔会被跳过;

  2. 可能多个定时器会连续执行;


每个 setTimeout 产生的任务会直接 push 到任务队列中;而 setInterval 在每次把任务push到任务队列前,都要进行一下判断(看上次的任务是否仍在队列中)。


可以看到,主线程的渲染都会对 setTimeoutsetInterval 的执行时间产生影响,但是 setTimeout 的影响小一点。所以我们可以使用 setTimeout 来实现倒计时.


const INTERVAL = 1000;

interface CountDownProps {
restTime: number;
format?: string;
onFinish: () => void;
key: number;
}
export const CountDown = ({ restTime, format = 'mm:ss', onFinish }: CountDownProps) => {
const timer = useRef<NodeJS.Timer | null>(null);
const [remainingTime, setRemainingTime] = useState(restTime);

useEffect(() => {
if (remainingTime < 0 && timer.current) {
onFinish?.();
clearTimeout(timer.current);
timer.current = null;
return;
}
timer.current = setTimeout(() => {
setRemainingTime((time) => time - INTERVAL);
}, INTERVAL);
return () => {
if (timer.current) {
clearTimeout(timer.current);
timer.current = null;
}
};
}, [remainingTime]);

return <span>{dayjs(remainingTime > 0 ? remainingTime : 0).format(format)}</span>;
};

为了修正 setTimeout 的时间误差,我们需要在 聚焦页面的时候 以及 定时一分钟请求一次服务器时间来修正误差。这里我们使用 swr 来轻松实现这个功能。


const REFRESH_INTERVAL = 60 * 1000;

export function useServerTime() {
const { data } = useSWR('/api/getCurrentTime', swrFetcher, {
// revalidateOnFocus 默认是开启的,但是我们项目中给关了,所以需要重新激活
revalidateOnFocus: true,
refreshInterval: REFRESH_INTERVAL,
});
return { currentTime: data?.currentTime };
}

最后我们把 CountDown 组件和 useServerTime 结合起来


function TitleAndCountDown() {
const { currentTime } = useServerTime();

return (
<Countdown
restTime={deadline - currentTime}
onFinish={onFinish}
key={deadline - currentTime}
/>

);
}

这样,就完成了一个完全不依赖客户端时间的倒计时组件。


总结



  • 上面方案中的 setTimeout 其实换成 requestAnimationFrame 计时会更加准确,也解决了 requestAnimationFrame未被激活的页面中 中不会执行的问题。

  • setIntervalsetTimeout 的时间误差是由于主线程的渲染时间造成的,所以如果我们的页面中有很多的动画,那么这个误差会更大。

  • 未激活的页面,setTimeout 的最小执行间隔是 1000ms


作者:xinglee
来源:juejin.cn/post/7229898205256417341
收起阅读 »

JavaScript作用域详解

web
作用域可分为词法作用域和动态作用域,JavaScript 使用词法作用域,也称为静态作用域。 词法作用域是指变量的作用域在代码写好的时候就确定了,而不是在运行时确定。函数在定义的时候就决定了其作用域,而不是在调用的时候。 JavaScript 的作用域(S...
继续阅读 »

作用域可分为词法作用域和动态作用域,JavaScript 使用词法作用域,也称为静态作用域。


词法作用域是指变量的作用域在代码写好的时候就确定了,而不是在运行时确定。函数在定义的时候就决定了其作用域,而不是在调用的时候。


JavaScript 的作用域(Scope)是指在代码中定义变量时,这些变量在哪里以及在哪些地方可以被访问。作用域控制着变量的可见性和生命周期。在 JavaScript 中,有全局作用域和局部作用域的概念,作用域的规则由函数定义和代码块定义来决定。


1. 全局作用域(Global Scope)


全局作用域是指在整个 JavaScript 程序中都可访问的范围。在全局作用域中定义的变量和函数可以被任何地方访问,包括代码文件、函数内部、循环块等。例如:


var globalVariable = "I am global";

function globalFunction({
  console.log(globalVariable);
}

globalFunction(); // 输出: I am global

2. 局部作用域(Local Scope)


局部作用域是指在函数内部或代码块内部定义的变量,其可见性仅限于该函数或代码块内部。这种作用域遵循 "变量提升" 的规则,即变量在声明之前就可以被访问,但其值为 undefined。例如:


function localScopeExample({
  var localVariable = "I am local";
  console.log(localVariable);
}

localScopeExample(); // 输出: I am local
console.log(localVariable); // 错误,localVariable 不在此处可见

3. 块级作用域(Block Scope)


在 ES6 引入块级作用域概念,可以通过 letconst 关键字在代码块内定义变量,这使得变量在块级范围内有效。在此之前,JavaScript 只有函数作用域,使用 var 关键字定义的变量在整个函数范围内有效。


if (true) {
  let blockVariable = "I am in a block";
  console.log(blockVariable);
}

console.log(blockVariable); // 错误,blockVariable 不在此处可见

总结


作用域是 JavaScript 中重要的概念,理解作用域有助于正确使用变量、避免命名冲突,提高代码的可维护性。


作者:MasterBao
来源:mdnice.com/writing/c771e23f7b014afbbe42499a1b32b0f7
收起阅读 »

【CSS定位属性】用CSS定位属性精确控制你的网页布局!

CSS定位属性是用于控制网页中元素位置的一种方式,它能够让元素在页面上精准地落在我们想要的位置。在CSS中,定位(Positioning)是控制元素在页面上如何定位和显示的一种机制。它主要包括四种属性:静态定位(static)、相对定位(relative)、绝...
继续阅读 »

CSS定位属性是用于控制网页中元素位置的一种方式,它能够让元素在页面上精准地落在我们想要的位置。

在CSS中,定位(Positioning)是控制元素在页面上如何定位和显示的一种机制。它主要包括四种属性:静态定位(static)、相对定位(relative)、绝对定位(absolute)、固定定位(fixed)。

每种定位方式都有其独特的特点和使用场景,下面将分别介绍这几种定位属性。

一、Static(静态定位)

静态定位是元素的默认定位方式,元素按照正常的文档流进行排列。在静态定位状态下,不能配合top、bottom、left、right来改变元素的位置。

  • 可以用于取消元素之前的定位设置。

代码示例:

<!DOCTYPE html>
<html>
<head>
<style>
.static {
background-color: lightblue;
padding: 100px;
}
</style>
</head>
<body>


<div>这是一个静态定位的元素。</div>


</body>
</html>

Description

二、Fixed(固定定位)

固定定位使元素相对于浏览器窗口进行定位,即使页面滚动,元素也会保持在固定的位置。

  • 固定定位的元素会脱离正常的文档流。

示例代码:

<!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>Document</title>
<style>
*{
margin: 0;
padding: 0;
}
body{
/* 给整个页面设置高度,出滚动条以便观察 */
height: 5000px;
}
div{
width: 100px;
height: 100px;
background-color: blue;
/* 固定定位 */
position: fixed;
right: 100px;
bottom: 100px;
}
</style>
</head>
<body>
<div></div>
</body>
</html>

运行结果:

移动前

Description

移动后

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>Document</title>
<style>
body {
position: relative;
}
.content {
/* 页面内容样式 */
}
#backToTop {
position: fixed;
bottom: 20px;
right: 20px;
background-color: #333;
color: #fff;
border: none;
padding: 10px;
cursor: pointer;
}
</style>
</head>
<body style="height: 5000px;">
<div>

</div>
<button id="backToTop" onclick="scrollToTop()">返回顶部</button>
<script>
function scrollToTop() {
window.scrollTo({top: 0, behavior: 'smooth'});
}
</script>
</body>
</html>

运行结果:

Description

三、Relative(相对定位)

相对定位是将元素对于它在标准流中的位置进行定位,通过设置边移属性top、bottom、left、right,使指定元素相对于其正常位置进行偏移。如果没有定位偏移量,对元素本身没有任何影响。

不使元素脱离文档流,空间会保留,不影响其他布局。

代码示例:


<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.box1{
width:200px;
height:100px;
background:skyblue;
margin:10px;
}
.box2{
width:200px;
height:100px;
background:pink;
margin:10px;
position:relative;/*相对定位*/
left:100px;/*向右偏移100px*/
top:-50px;/*向上偏移50px*/
}
.box3{
width:200px;
height:100px;
background:yellowgreen;
margin:10px;
}
</style>
</head>
<body>
<div>1</div>
<div>2</div>
<div>3</div>
</body>
</html>

运行结果:

没使用相对定位之前是这样的:

Description

使用相对定位后:相对于原来的位置向右偏移了100px,向上偏移50px。
Description

虽然它的位置发生了变化,但它在标准文档流中的原位置依然保留。

四、Absolute(绝对定位)

绝对定位使元素相对于最近的非 static 定位祖先元素进行定位。如果没有这样的元素,则相对于初始包含块(initial containing block)。绝对定位的元素会脱离正常的文档流。

  • 如果该元素为内联元素,则会变成块级元素,可直接设置其宽和高的值(让内联具备快特性);

  • 如果该元素为块级元素,使其宽度根据内容决定。(让块具备内联的特性)

<style>
.wrap{
width:500px;
height:400px;
border: 2px solid red;
}
.box1{
width:200px;
height:100px;
background:skyblue;
margin:10px;
}
.box2{
width:200px;
height:100px;
background:pink;
margin:10px;
position:absolute;/*绝对定位*/
left:100px;/*向右偏移100px*/
top:30px;/*向下偏移30px*/
}
.box3{
width:200px;
height:100px;
background:yellowgreen;
margin:10px;


}
</style>
<div>
<div>1</div>
<div>2</div>
<div>3</div>
</div>

将第二个设置为绝对定位后,它脱离了文档流可以定位到页面的任何地方,在标准文档流中的原有位置会空出来,所以第三个会排到第一个下面。

Description

第二个相对于它的父元素向右偏移100,向下偏移30。

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

点这里前往学习哦!

五、z-index(层级顺序的改变)

层叠顺序决定了元素之间的堆叠顺序。z-index 属性用于设置元素的层叠顺序。具有较高 z-index 值的元素会覆盖具有较低 z-index 值的元素。

注意:

  • 默认值是0
  • 数值越大层越靠上
  • 不带单位
  • 没有最大值和最小值
  • 可以给负数

代码示例:

<!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>Document</title>
<style>
div:nth-of-type(1){
width: 300px;
height: 300px;
background-color: skyblue;
position: absolute;
}
div:nth-of-type(2){
width: 200px;
height: 200px;
background-color: pink;
position: absolute;
}
div:nth-of-type(3){
width: 100px;
height: 100px;
background-color: yellowgreen;
position: absolute;
z-index: -1;
}
</style>
</head>
<body>

<div></div>
<div></div>
<div></div>


</body>
</html>

运行结果:

Description

可以看到,最后一个div依然存在,但是看不见了,原因就是我们改变了z-index属性值。

Description

以上就是CSS定位属性的介绍了,通过这些定位属性,可以灵活地控制网页中元素的位置和堆叠顺序。

在实际应用中,CSS定位属性的使用需要考虑到整体布局和用户体验。合理运用这些定位技巧,可以让你的网页不仅美观,而且易于使用和维护。记住,好的设计总是细节和功能的完美结合。

收起阅读 »

解锁 JSON.stringify() 5 个鲜为人知的功能

web
作为一名前端开发者,你可能熟悉JSON.stringify()方法,通常用于调试。但是很多只是简单使用一下接下来,让我们深入了解其实用性。 考虑一个对象如果想把她转成字符串打印出来: const obj = { name: 'San Shang Y...
继续阅读 »

u=142040142,590010156&fm=253&fmt=auto&app=138&f=JPEG.webp


作为一名前端开发者,你可能熟悉JSON.stringify()方法,通常用于调试。但是很多只是简单使用一下接下来,让我们深入了解其实用性。


考虑一个对象如果想把她转成字符串打印出来:


const obj = {  
name: 'San Shang You Ya',
age: 18
};
console.log(obj.toString()); // Result: [object Object]

如果你想这样打印你所看到的只能是 [object Object]


我们可以借助JSON.stringify()方法


const obj = {  
name: 'San Shang You Ya',
age: 18
};
console.log(JSON.stringify(obj));
// Result: {"name":"San Shang You Ya","age":18}

大多数开发者直接使用 JSON.stringify(),但我即将揭示一些隐藏的技巧。


1. 第二个参数(Array)


-JSON.stringify() 接受第二个参数,它是一个你想在控制台中显示的对象的键的数组。例如:


const obj = {  
name: 'San Shang You Ya',
age: 18
};
console.log(JSON.stringify(obj, ['name']));
// Result: {"name": "San Shang You Ya"}

这样而不是将整个 JSON 对象混乱地显示在控制台中,可以通过将所需的键作为数组传递给第二个参数来选择性地打印。


2. 第二个参数(Function)



  • 第二个参数也可以是一个函数,根据函数内的逻辑输出键值对。

  • 如果返回 undefined,则该键值对将不会被打印出来。


const obj = {  
name: 'San Shang You Ya',
age: 18
};

console.log(JSON.stringify(obj, (key, value) => (key === "age" ? value : undefined)));
// Result: {"age": 18}

3. 第三个参数作为数字



  • 第三个参数控制最终字符串中的间距。如果是一个数字,字符串化的每个级别将相应缩进。


const obj = {  
name: 'San Shang You Ya',
age: 18
};
console.log(JSON.stringify(obj, null, 2));

image.png


4. 第三个参数作为字符串


如果第三个参数是一个字符串,它将替换为空格字符


image.png


5. toJSON 方法


对象可以拥有一个 toJSON 方法。
JSON.stringify() 返回该方法的结果,并对其进行字符串化,而不是转换整个对象。


const superhero= {  
firstName: "San Shang",
lastName: "You Ya",
age: 21,
toJSON() {
return {
fullName: `${this.firstName} + ${this.lastName}`
};
}
};

console.log(JSON.stringify(superhero));
// Result: "{ "fullName" : "San Shang You Ya"}"

作者:StriveToY
来源:juejin.cn/post/7329164061390798883
收起阅读 »

拯救强迫症!前端统一代码规范

web
1. 代码格式化 1.1 工具介绍 ESLint 是一款用于查找并报告代码中问题的工具 Stylelint 是一个强大的现代 CSS 检测器 Prettier 是一款强大的代码格式化工具,支持多种语言 lint-staged 是一个在 git 暂存文件上运...
继续阅读 »

1. 代码格式化


1.1 工具介绍


Untitled 1.png



  • ESLint 是一款用于查找并报告代码中问题的工具

  • Stylelint 是一个强大的现代 CSS 检测器

  • Prettier 是一款强大的代码格式化工具,支持多种语言

  • lint-staged 是一个在 git 暂存文件上运行 linters 的工具

  • husky 是 Git Hook 工具,可以设置在 git 各个阶段触发设定的命令


1.2 配置说明


1.2.1 ESLint 配置


在项目根目录下增加 .eslintrc.js 文件进行配置,配置项详见官方文档,以下为参考配置:


npm i -D eslint eslint-plugin-vue eslint-plugin-import eslint-import-resolver-typescript @typescript-eslint/parser @typescript-eslint/eslint-plugin prettier eslint-plugin-prettier eslint-config-prettier

module.exports = {
// 此项是用来告诉 eslint 找当前配置文件不能往父级查找
root: true,
// 全局环境
env: {
browser: true,
node: true,
},
// 指定如何解析语法,eslint-plugin-vue 插件依赖vue-eslint-parser解析器
parser: "vue-eslint-parser",
// 优先级低于parse的语法解析配置
parserOptions: {
// 指定ESlint的解析器
parser: "@typescript-eslint/parser",
// 允许使用ES语法
ecmaVersion: 2020,
// 允许使用import
sourceType: "module",
// 允许解析JSX
ecmaFeatures: {
jsx: true,
},
},
extends: [
"eslint:recommended", // 引入 ESLint的核心功能并且报告一些常见的共同错误
"plugin:import/recommended", // import/export语法的校验
"plugin:import/typescript", // import/export 语法的校验(支持 TS)
// 'plugin:vue/essential' // vue2 版本使用
// 'plugin:vue/recommended', // vue2 版本使用
"plugin:vue/vue3-essential", // vue3 版本使用
"plugin:vue/vue3-recommended", // vue3 版本使用
"plugin:@typescript-eslint/recommended",
"prettier", // prettier 要放在最后!
],
plugins: ["prettier"],
rules: {
"prettier/prettier": "error",
"@typescript-eslint/explicit-module-boundary-types": "off",
"no-undef": "off",
// 更多规则详见:http://eslint.cn/docs/rules/
},
settings: {
"import/parsers": {
"@typescript-eslint/parser": [".ts", ".tsx"],
},
"import/resolver": {
typescript: {
alwaysTryTypes: true,
},
},
},
};

💡当 ESLint 同时使用 prettier 的时候,prettier 和 ESLint 可能存在一些规则冲突,我们需要借助 eslint-plugin-prettiereslint-config-prettier 进行解决,在安装完依赖包后在 .eslintrc.js 配置文件中进行添加如下内容:


module.exports = {
"extends": [
// 其他扩展内容...
"prettier" // prettier 要放在最后!
],
"plugins": ["prettier"],
"rules": {
"prettier/prettier": "error"
},
}

1.2.2 StyleLint 配置


在项目根目录下增加 .stylelintrc.js 文件进行配置,配置项详见官方文档,以下为参考配置:


npm i -D stylelint stylelint-config-standard stylelint-order stylelint-config-rational-order prettier stylelint-prettier stylelint-config-prettier postcss-html postcss-less stylelint-config-recommended-vue

module.exports = {
extends: [
'stylelint-config-standard', // 官方 stylelint 规则
'stylelint-config-rational-order', // 属性排列顺序规则
/*
* 通过安装 stylelint-prettier,设置 'stylelint-prettier/recommended',其包含了三个操作
plugins: ['.'],
extends: ['stylelint-config-prettier'], // 需要安装 stylelint-config-prettier
rules: {'prettier/prettier': true},
*/

'stylelint-prettier/recommended',
],
plugins: [
'stylelint-order', // CSS 属性排序
],
rules: {
// 更多规则详见:https://stylelint.io/user-guide/rules/list
},
};

💡当 StyleLint 同时使用 prettier 的时候,prettier 和 StyleLint 可能存在一些规则冲突,我们需要借助 stylelint-prettierstylelint-config-prettier 进行解决,在安装完依赖包后在 .stylelintrc.js 配置文件中进行添加如下内容:


module.exports = {
extends: [
/*
* 通过安装 stylelint-prettier,设置 'stylelint-prettier/recommended',其包含了三个操作
plugins: ['.'],
extends: ['stylelint-config-prettier'], // 需要安装 stylelint-config-prettier
rules: {'prettier/prettier': true},
*/

'stylelint-prettier/recommended',
],
};

1.2.3 Prettier 配置


在项目根目录下增加 .prettierrc.js 文件进行配置,配置项详见官方文档,以下为参考配置:


npm i -D prettier

module.exports = {
// 更多规则详见:https://prettier.io/docs/en/options.html
printWidth: 120, // 单行长度
tabWidth: 2, // 缩进长度
useTabs: false, // 使用空格代替tab缩进
semi: true, // 句末使用分号
singleQuote: true, // 使用单引号
bracketSpacing: true, // 在对象前后添加空格-eg: { foo: bar }
quoteProps: 'consistent', // 对象的key添加引号方式
trailingComma: 'all', // 多行时尽可能打印尾随逗号
jsxBracketSameLine: true, // 多属性html标签的‘>’折行放置
arrowParens: 'always', // 单参数箭头函数参数周围使用圆括号-eg: (x) => x
jsxSingleQuote: true, // jsx中使用单引号
proseWrap: 'preserve',
htmlWhitespaceSensitivity: 'ignore', // 对HTML全局空白不敏感
};

1.2.4 husky 和 lint-staged 配置


step1. 初始化 husky


npx husky-init && npm install

step2. 在 .husky/pre-commit 文件中进行修改(注意区别 husky@7 与 husky@4 的设置方式)


#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

npx lint-staged

step3. 安装 lint-statged 并在 package.json 中进行设置


npm i -D lint-staged

{
"lint-staged": {
"*.{js,ts,vue}": [
"eslint --fix",
"prettier --write",
"git add"
],
"*.{css,less,vue}": [
"stylelint --fix",
"prettier --write",
"git add"
]
}
}

1.3 使用参考



  1. 代码提交:根据上述工具配置,代码在提交仓库时进行检查和格式化,实现代码风格统一;

  2. 本地保存:在 VSCode 中进行配置,使得代码在保存的时候即按照相应的规则进行格式化;



如何在 VSCode 中进行配置使得能够自动按照相应的规则进行格式化呢?接下来进入第二章《编辑器配置》。



2. 编辑器配置


2.1 VSCode 配置


2.1.1 配置内容


Untitled.png


所有 VSCode 配置自定义的内容(包括插件部分)都在 setting.json 文件中,以下为参考配置:


{
"editor.tabSize": 2,
"window.zoomLevel": 0,
"editor.fontSize": 14,
"editor.formatOnPaste": true,
"editor.formatOnSave": true,
"editor.multiCursorModifier": "ctrlCmd",
"editor.snippetSuggestions": "top",
"eslint.codeAction.showDocumentation": {
"enable": true
},
"eslint.run": "onSave",
"eslint.format.enable": true,
"eslint.options": {
"extensions": [
".js",
".vue",
".ts",
".tsx"
]
},
"eslint.validate": [
"javascript",
"typescript",
"vue"
],
"stylelint.validate": [
"css",
"less",
"postcss",
"scss",
"sass",
"vue",
],
// 保存时按照哪个规则进行格式化
"editor.codeActionsOnSave": {
"source.fixAll": true,
"source.fixAll.eslint": true
},
"files.autoSave": "afterDelay", // 文件自动保存
"files.autoSaveDelay": 2000, // 2s 后文件自动保存
}

参考资料: VS Code 使用指南VS Code 中 Vetur 与 prettier、ESLint 联合使用


2.1.1 插件推荐



  1. Eslint: Integrates ESLint JavaScript int0 VS Code

  2. stylelint: Official Stylelint extension for Visual Studio Code

  3. Prettier: Code formatter using prettier

  4. EditorConfig: EditorConfig Support for Visual Studio Code

  5. Npm Intellisense: VS Code plugin that autocompletes npm modules in import statements

  6. Path Intellisense: VS Code plugin that autocompletes filenames

  7. Auto Rename Tag: Auto rename paired HTML/XML tag

  8. Auto Close Tag: Automatically add HTML/XML close tag

  9. Code Spelling Checker: Spelling checker for source code

  10. Volar / Vetur: Language support for Vue 3 / Vue tooling for VS Code


2.2 EditorConfig 配置


EditorConfig 的优先级高于编辑器自身的配置,因此可用于维护不同开发人员、不同编辑器的编码风格。在项目根目录下增加 .editorconfig 文件进行配置即可,以下为参考配置:


# Editor configuration, see http://editorconfig.org

# 表示是最顶层的 EditorConfig 配置文件
root = true

[*] # 表示所有文件适用
charset = utf-8 # 设置文件字符集为 utf-8
indent_style = space # 缩进风格(tab | space)
indent_size = 2 # 缩进大小
end_of_line = lf # 控制换行类型(lf | cr | crlf)
insert_final_newline = true # 始终在文件末尾插入一个新行
trim_trailing_whitespace = true # 去除行尾的任意空白字符

3. Commit Message 格式化


3.1 工具介绍


Conventional Commits 约定式提交规范是一种用于给提交信息增加人机可读含义的规范,可以通过以下工具来进行检查、统一和格式化:



  • commitlint:检查您的提交消息是否符合 conventional commit format

  • commitizen:帮助撰写规范 commit message 的工具

  • cz-customizable:自定义配置 commitizen 工具的终端操作

  • commitlint-config-cz:合并 cz-customizable 的配置和 commitlint 的配置


3.2 配置说明


3.2.1 格式化配置


step1. 安装 commitizen 和 cz-customizable


npm install -D commitizen cz-customizable

step2. 在 package.json 添加以下内容:


{
"config": {
"commitizen": {
"path": "node_modules/cz-customizable"
}
}
}

step3. 在项目根目录下增加 .cz-config.js 文件进行配置即可,以下为参考配置:


module.exports = {
// type 类型
types: [
{ value: 'feat', name: 'feat: 新增功能' },
{ value: 'fix', name: 'fix: 修复 bug' },
{ value: 'docs', name: 'docs: 文档变更' },
{ value: 'style', name: 'style: 代码格式改变(不影响功能)' },
{ value: 'refactor', name: 'refactor: 代码重构(不包括 bug 修复、功能新增)' },
{ value: 'perf', name: 'perf: 性能优化' },
{ value: 'test', name: 'test: 添加或修改测试用例' },
{ value: 'build', name: 'build: 构建流程或外部依赖变更' },
{ value: 'ci', name: 'ci: 修改 CI 配置或脚本' },
{ value: 'chore', name: 'chore: 对构建过程或辅助工具和库的更改(不影响源文件、测试用例)' },
{ value: 'revert', name: 'revert: 回滚 commit' },
],
// scope 类型
scopes: [
['components', '组件相关'],
['hooks', 'hook 相关'],
['utils', 'utils 相关'],
['styles', '样式相关'],
['deps', '项目依赖'],
// 如果选择 custom,后面会让你再输入一个自定义的 scope。也可以不设置此项,把后面的 allowCustomScopes 设置为 true
['custom', '以上都不是,我要自定义'],
].map(([value, description]) => {
return {
value,
name: `${value.padEnd(30)} (${description})`,
};
}),
// 交互提示信息
messages: {
type: '确保本次提交遵循 Angular 规范!\n选择你要提交的类型:',
scope: '选择一个 scope(可选):\n',
customScope: '请输入自定义的 scope:\n', // 选择 scope: custom 时会出现的提示
subject: '填写简短精炼的变更描述:\n',
body: '填写更加详细的变更描述(可选)。使用 "|" 换行:\n',
breaking: '列举非兼容性重大的变更(可选):\n',
footer: '列举出所有变更的 ISSUES CLOSED(可选):\n',
confirmCommit: '是否确认提交?',
},
// 设置只有 type 选择了 feat 或 fix,才询问 breaking message
allowBreakingChanges: ['feat', 'fix'],
// subject 限制长度
subjectLimit: 100,
};

step4. 新增 husky 配置,使得提交 commit message 时触发 commitizen,快捷命令如下:


npx husky add .husky/prepare-commit-msg "exec < /dev/tty && node_modules/.bin/cz --hook || true"

注意,commitizen 如果是全局安装,则使用下面的快捷命令:


npx husky add .husky/prepare-commit-msg "exec < /dev/tty && git cz --hook || true"

3.2.2 格式检查配置


step1. 安装 commitlint 和 commitlint-config-cz ****依赖:


npm install --save-dev @commitlint/{config-conventional,cli} commitlint-config-cz

step2. 在项目根目录下增加 commitlint.config.js 文件进行配置即可,以下为配置内容:


module.exports = {
extends: ['@commitlint/config-conventional', 'cz'],
rules: {},
};

step3. 新增 husky 配置,使得提交 commit message 时触发 commitlint 检验,配置内容如下:


npx husky add .husky/commit-msg 'npx --no -- commitlint --edit "$1"'

3.3 使用参考


在命令行输入 git commit,然后根据命令行提示输入相应的内容,完成之后则会自动生成符合规范的 commit message,从而实现提交信息的统一。


4. 代码规范参考


4.1 JS/TS 规范


社区类代码风格:



工具类代码风格:



4.2 CSS 规范


社区类代码风格:



工具类代码风格



4.3 VUE 规范


推荐阅读 Vue 官方风格指南:Vue2 版本Vue3 版本,其他可参考 eslint-plugin-vue


作者:植物系青年
来源:juejin.cn/post/7278575483909799947
收起阅读 »