注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

检测图片是否cmyk

web
引入 最近业务上有要求,要求如果是 Jpeg 格式文件, 前端在上传的时候要求判断一下这个文件是否 CMYK 颜色模式(color mode/ color space)。 这个颜色模式是打印行业需要的。如果不是则禁止上传,并提示用户。 一开始我以为这个应该存储...
继续阅读 »

引入


最近业务上有要求,要求如果是 Jpeg 格式文件, 前端在上传的时候要求判断一下这个文件是否 CMYK 颜色模式(color mode/ color space)。 这个颜色模式是打印行业需要的。如果不是则禁止上传,并提示用户。


一开始我以为这个应该存储在 exif 文件信息中, 去拿一下就好了, 但是简单测试发现两个问题:



  1. 文件是否携带 exif 信息是不确定的, 即便出自设计师导出文件, 有可能也是不携带颜色模式信息的。

  2. 除此之外, 依靠 exif 信息去判断,严格来说,即便携带,也是不准确的, 因为这个信息是可以被人为修改的。


经过一番研究, 我暂时发现可能有两种方式,去达成目的。 但是这篇文章实际不是以解决问题为导向,而是期望尽可能的深入一丢丢。 如果急于找到解决方案, 直接翻到文章底部查看具体 编码实现 即可。


什么是 CMYK 颜色模式?



了解 Photoshop 颜色模式 (adobe.com)



CMYK 是一种颜色模式,它表示四种颜色通道:青色(Cyan)、品红色(Magenta)、黄色(Yellow)和黑色(Key,通常表示黑色)。这种颜色模式主要用于印刷和彩色印刷工作中。


以下是 CMYK 颜色模式中各颜色通道的简要介绍:



  1. 青色 (Cyan): 表示蓝绿色。在印刷中,它用于调整蓝色和绿色的浓度。

  2. 品红色 (Magenta): 表示品红或洋红色。在印刷中,它用于调整红色和蓝色的浓度。

  3. 黄色 (Yellow): 表示黄色。在印刷中,它用于调整红色和绿色的浓度。

  4. 黑色 (Key): 通常表示黑色。在印刷中,黑色是通过使用黑色油墨单独添加的,以增加图像的深度和对比度。在 CMYK 模式中,K 代表 Key,以避免与蓝色 (B) 冲突。


这四个颜色通道可以叠加在一起以创建各种颜色。通过调整每个通道的浓度,可以实现广泛的颜色表达。CMYK 被广泛用于印刷领域,因为它能够准确地模拟很多颜色,并提供了在印刷过程中需要的色彩控制。


与 RGB(红绿蓝)颜色模式不同,CMYK 是一种适合印刷的颜色模式,因为它更好地反映了油墨混合的方式,并考虑到印刷物质上的光的特性


怎么在web判断一个 jpeg/jpg 文件 颜色模式是否 cmyk ?


简单说一下这两种方法, 实际上是同一种原理, 因为对于一张图片而言, 它除了携带有 exif 文件元信息之外, 还有文件头信息。


既然不能通过 exif 元信息去判断, 那么我们可以通过文件头信息去做判断。


首先,简单测试可以发现, 即便一个 cmyk 图片没有 exif 描述元信息标识这是一个 cmyk 颜色模式的图片, 但是 各种设计类软件都能够标记出来。 以ps为例:


image-20231128163932682.png
但是 exif 信息中是没有的:


image-20231128164033843.png


甚至一些解析库,就连最基本携带的元信息都没读出来:



stackblitz.com/edit/exif-j…



image-20231128164214625.png


为什么设计软件可以标记出这个图片是否是 cmyk 颜色模式?


这个问题, 我在网上翻了很久,确实是找不到相关文章有阐述设计软件的原理。 不过Ai 的回答是这样的, 具备一定的参考性:



有朋友找到了记得踢我一脚,这里提前感谢啦~



image-20231128174834089.png


用 ImageMagic 解析图片文件



什么是 imageMagic ?


ImageMagick 主要由大量的命令行程序组成,而不提供像 Adobe Photoshop、GIMP 这样的图形界面。它还为很多程序语言提供了 API 库。


ImageMagick 的功能包括:



  • 查看、编辑位图文件

  • 进行图像格式转换

  • 图像特效处理

  • 图像合成

  • 图像批处理


ImageMagick 广泛用于图像处理、图形设计、Web 开发等领域。它是许多开源软件项目的重要组成部分,例如 GIMP、Inkscape、Linux 系统中的图像工具等。


ImageMagick 的优势包括:



  • 功能强大,支持多种图像格式和图像处理功能

  • 开放源代码,免费使用

  • 、、可移植性强,支持多种操作系统



@jayce: imageMagic 类似于 ffmpeg, 只不过它专注图像处理




我们可以利用 ImageMagic 的 identify 工具命令 去解析图片以查看一些信息:


image-20231128180008082.png


加上 -verbose 选项可以查看更多详细信息:


$ ./magick identify -verbose ./CMYK.jpg
$ ./magick identify -verbose ./RGB.jpg

image-20231129092244504.png


这些数据是什么? 从哪里解析出来的呢? 这个需要看一下 jpeg 文件的一些标准文件结构


ISO/IEC 10918-1 和 ISO/IEC 10918-5


这两个文件都是 JPEG 的标准文档,只是不同的部分,wiki 上对二者描述大致是 5 是 对 1 的很多细节的展开和补充。是补充规范


JPEG File Interchange Format (JFIF) 和 Exif


JFIF(JPEG File Interchange Format)和 EXIF(Exchangeable image file format)是两种与 JPEG 图像相关的标准,但它们具有不同的目的和功能。


JFIF 是一个图片文件格式标准, 它被发布于 10918-5, 是对 10918-1 的细节补充。



  1. JFIF (JPEG File Interchange Format):

    • 目的: JFIF 是一种用于在不同设备和平台之间交换 JPEG 图像的简单格式。它定义了 JPEG 文件的基本结构,以确保文件在不同系统中的一致性和可互操作性。

    • 特点: JFIF 文件通常包含了基本的图像数据,但不一定包含元数据信息。它主要关注图像的编码和解码,而不太关心图像的其他详细信息。JFIF 文件通常使用 .jpg 或 .jpeg 扩展名。



  2. EXIF (Exchangeable image file format):

    • 目的: EXIF 是一种在数字摄影中广泛使用的标准,用于嵌入图像文件中的元数据信息。这些元数据可以包括拍摄日期、相机型号、曝光时间、光圈值等。EXIF 提供了更丰富的信息,有助于记录和存储与拍摄有关的详细数据。

    • 特点: EXIF 数据以二进制格式嵌入在 JPEG 图像中,提供了关于图像和拍摄条件的详细信息。这对于数字相机和其他支持 EXIF 的设备非常有用。EXIF 文件通常使用 .jpg 或 .jpeg 扩展名。




JPEG 文件标准结构语法


jpeg 作为压缩数据结构, 是一个非常复杂的数据组织, 我们的关注点只在关系到我们想要解决的问题。 标准文档 ISO/IEC 10918-1 : 1993(E).中有部分相关说明。


概要:


结构上来说, jpeg 的数据格式由以下几个部分,有序组成: parameters, markers, 以及 entropy-coded data segments, 其中 parameters 和 markers 部分通常被组织到 marker segments, 因为它们都是用字节对齐的代码表, 都是由8位字节的有序序列组成。


Parameters


这部分携带有参数编码关键信息, 是图片成功被解析的关键。


Markers


Markers 标记用于标识压缩数据格式的各种结构部分。大多数标记开始包含一组相关参数的标记段;有些标记是单独存在的。所有标记都被分配了两个字节的代码


例如 SOI : 从 0xFF,0xD8这两个字节开始,标记为图片文件的文件头开始, SOF0: 从 0xFF, 0xD8这两个字节开始,标记了 ”帧“ 的开始,它实际上会携带有图片的一些基本信息, 例如宽高,以及颜色通道等。 这个颜色通道其实也是我们主要需要关注的地方。


下表是完整的标记代码:


image-20231129095415255.png



@refer:


http://www.digicamsoft.com/itu/itu-t81…
http://www.digicamsoft.com/itu/itu-t81…



wiki 上也有相关的帧头部字段说明:


Short nameBytesPayloadNameComments
SOI0xFF, 0xD8noneStart Of Image
SOF00xFF, 0xC0variable sizeStart Of Frame (baseline DCT)Indicates that this is a baseline DCT-based JPEG, and specifies the width, height, number of components, and component subsampling (e.g., 4:2:0).
SOF20xFF, 0xC2variable sizeStart Of Frame (progressive DCT)Indicates that this is a progressive DCT-based JPEG, and specifies the width, height, number of components, and component subsampling (e.g., 4:2:0).
DHT0xFF, 0xC4variable sizeDefine Huffman Table(s)Specifies one or more Huffman tables.
DQT0xFF, 0xDBvariable sizeDefine Quantization Table(s)Specifies one or more quantization tables.
DRI0xFF, 0xDD4 bytesDefine Restart IntervalSpecifies the interval between RSTn markers, in Minimum Coded Units (MCUs). This marker is followed by two bytes indicating the fixed size so it can be treated like any other variable size segment.
SOS0xFF, 0xDAvariable sizeStart Of ScanBegins a top-to-bottom scan of the image. In baseline DCT JPEG images, there is generally a single scan. Progressive DCT JPEG images usually contain multiple scans. This marker specifies which slice of data it will contain, and is immediately followed by entropy-coded data.
RSTn0xFF, 0xDn (n=0..7)noneRestartInserted every r macroblocks, where r is the restart interval set by a DRI marker. Not used if there was no DRI marker. The low three bits of the marker code cycle in value from 0 to 7.
APPn0xFF, 0xEnvariable sizeApplication-specificFor example, an Exif JPEG file uses an APP1 marker to store metadata, laid out in a structure based closely on TIFF.
COM0xFF, 0xFEvariable sizeCommentContains a text comment.
EOI0xFF, 0xD9noneEnd Of Image


Syntax and structure



整体结构


image-20231129111546427.png



@refer: http://www.digicamsoft.com/itu/itu-t81…



Frame Header


image-20231129111645470.png


image-20231129112439294.png


image-20231129112306831.png



@refer: http://www.digicamsoft.com/itu/itu-t81…



SOFn : 帧开始标记标记帧参数的开始。下标n标识编码过程是基线顺序、扩展顺序、渐进还是无损,以及使用哪种熵编码过程。


在其标准文档中,我们有找到 SOFn 的子字段说明,不过在其他地方,倒是看到了不少描述:


特别是在这里 JPEG File Layout and Format


image-20231129141121729.png


可以看到,在 SOFn 这个标记中, 有一个字段为会指明 components 的数量,它代表的实际上颜色通道, 如果是 1,那么就是灰度图, 如果是3,那就是RGB, 如果是 4 就是 CMYK.


到这里我们就知道了, 我们可以读取到这个对应的字节段,从而判断一个图片的颜色模式了。


怎么读取呢?


这篇资料说了明了 Jpeg 文件格式中字节和上述字段的关联关系: Anatomy of a JPEG


注意这篇资料中有一段描述,会影响到我们后续的逻辑判断:


image-20231129142053598.png



就是 SOF0 是必须的,但是可以被 SOFn>=1 替换。 所以在做逻辑判断的时候,后续的也要判断。



我们可以先大概看看一个图片文件的字节流数据长什么样子:(因为所有的字段都是 FF 字节位开头,所以高亮了)


1701248911601.png



以上页面可以在这里访问: jaycethanks.github.io/demos/DemoP…



但样太不便于阅读了, 而且实在太长了。 这里有个网站 here,可以将关键的字节段截取出来:


image-20231129171534152.png


我们主要看这里:


image-20231129171621345.png
可以看到 components 为 4.


如果是 RGB:


image-20231129171722071.png


这里就是 3,


如果是灰度图,components 就会是1


image-20231129172500605.png


EXIF 在哪里?


一个额外的小问题, 我们常见的 exif 元信息存储在哪里呢?


其实上面的 Markers 部分给出的表格中也说明了 ,在 Appn 中可以找到 exif 信息, 但是wiki 上说的是 App0, 在这个解析网站中,我们可以看到:


image-20231201113938640.png


编码实现


有了上述具体的分析, 我们就能有大致思路, 这里直接给出相关代码:



代码参考 github.com/zengming00/node-jpg-is-cmyk




/**
*
@refer https://github.com/zengming00/node-jpg-is-cmyk/blob/master/src/index.ts

*
@refer https://cyber.meme.tips/jpdump/#
*
@refer https://mykb.cipindanci.com/archive/SuperKB/1294/JPEG%20File%20Layout%20and%20Format.htm
*
@refer https://www.ccoderun.ca/programming/2017-01-31_jpeg/
*
* 通过 jpg 文件头判断是否是 CMYK 颜色模式
*
@param { Uint8Array } data
*/

function checkCmyk(data: Uint8Array) {
let pos = 0;
while (pos < data.length) {
pos++;
switch (data[pos]) {
case 0xd8: {// SOI - Start of Image
pos++;
break;
}
case 0xd9: {// EOI - End of Image
pos++;
break;
}
case 0xc0: // SOF0 - Start of Frame, Baseline DCT
case 0xc1: // SOF1 - Start of Frame, Extended Sequential DCT
case 0xc2: { // SOF2 - Start of Frame, Progressive DCT
pos++;
const len = (data[pos] << 8) | data[pos + 1];
const compoNum = data[pos + 7];
if (compoNum === 4) {
// 如果components 数量为4, 那么就认为是 cmyk
return true;
}
pos += len;
break;
}
case 0xc4: { // DHT - Define Huffman Table
pos++;
const len = (data[pos] << 8) | data[pos + 1];
pos += len;
break;
}
case 0xda: { // SOS - Start of Scan
pos++;
const len = (data[pos] << 8) | data[pos + 1];
pos += len;
break;
}
case 0xdb: { // DQT - Define Quantization Table
pos++;
const len = (data[pos] << 8) | data[pos + 1];
pos += len;
break;
}
case 0xdd: { // DRI - Define Restart Interval
pos++;
const len = (data[pos] << 8) | data[pos + 1];
pos += len;
break;
}
case 0xe0: { // APP0 - Application-specific marker
pos++;
const len = (data[pos] << 8) | data[pos + 1];
pos += len;
break;
}
case 0xfe: { // COM - Comment
pos++;
const len = (data[pos] << 8) | data[pos + 1];
pos += len;
break;
}
default: {
pos++;
const len = (data[pos] << 8) | data[pos + 1];
pos += len;
}
}
}
return false;
}

有没有其他的方法?


既然 imageMagic 这么成熟且强大, 我们有办法利用它来做判断吗?


我们可以通过 wasm, 在web中去利用这些工具, 我找到了 WASM-ImageMagick 这个, 但是他的打包好像有些问题 vite 引入的时候会报错,看着好像也没有要修复的意思, issue 里面有老哥自己修改了打包配置进行了修复在这里: image-magick


我们就写的demo测试函数:


import * as Magick from '@xn-sakina/image-magick'

export default function (file: File) {
if (!file) return;
// 创建FileReader对象
var reader = new FileReader();
// 当读取完成时的回调函数
reader.onload = async function (e) {
// 获取ArrayBuffer
var arrayBuffer = e.target?.result as ArrayBuffer;
if (arrayBuffer) {
// 将 ArrayBuffer 转换为 Uint8Array
const sourceBytes = new Uint8Array(arrayBuffer);
const inputFiles = [{ name: 'srcFile.png', content: sourceBytes }]
let commands: string[] = ["identify srcFile.png"]
const { stdout } = await Magick.execute({inputFiles, commands});

// 这里打印一下结果
console.log('stdout:',stdout[0])

}
};
// 读取文件为ArrayBuffer
reader.readAsArrayBuffer(file);
}

import isCmyk from '../utils/isCmyk.ts'
const handleFileChange = (e: Event) => {
const file = (e.target as HTMLInputElement)?.files?.[0]
isCmyk(file) // 这里文件上传调用一下
......

测试几个文件


image-20231130104941592.png


可以看到, Gray, RGB, CMYK 检测都可以正常输出, 说明可以这么干。



但是这个库, 文档写的太乱了。 - -



这个库的大小有 5 m之大 - -, npm 上找了下, 目前相关的包,也没有比这个更小的好像。


作者:sun_zy
来源:jaycethanks.github.io/blog_11ty/posts/Others/%E6%A3%80%E6%B5%8B%E5%9B%BE%E7%89%87%E6%98%AF%E5%90%A6cmyk/
收起阅读 »

我是如何使用Flow+Retrofit封装网络请求的

各位好,本人是练习kt时长一年的准练习生,接下来我将用一种我认为还行的方式封装网络请求,如有雷点,请各位佬轻喷 首先,定义一个请求结果类 sealed class RequestResult<out T> { data object INI...
继续阅读 »

各位好,本人是练习kt时长一年的准练习生,接下来我将用一种我认为还行的方式封装网络请求,如有雷点,请各位佬轻喷
首先,定义一个请求结果类


sealed class RequestResult<out T> {
data object INIT : RequestResult<Nothing>()
data object LOADING : RequestResult<Nothing>()
data class Success<out T>(val data: T) : RequestResult<T>()
data class Error(val errorCode: Int = -1, val errorMsg: String? = "") : RequestResult<Nothing>()
}

接下来,定义Retrofit的service,由于我个人的极简主义,特别讨厌复制粘贴,所以我做了一个非常大胆的决定


interface SimpleService {
//目前我们只关注这两方法
@GET
suspend fun commonGet(@Url url: String, @QueryMap param: HashMap<String, Any>): ApiResponse<Any>
//目前我们只关注这两方法
@POST
suspend fun commonPost(@Url url: String, @Body param: HashMap<String, Any>): ApiResponse<Any>

@GET
suspend fun commonGetList(@Url url: String, @QueryMap param: HashMap<String, Any>): ApiListData<Any>

@POST
suspend fun commonPostList(@Url url: String, @Body param: HashMap<String, Any>): ApiListData<Any>

@GET
suspend fun commonGetPageList(@Url url: String, @QueryMap param: HashMap<String, Any>): ApiPageData<Any>

@POST
suspend fun commonPostPageList(@Url url: String, @Body param: HashMap<String, Any>): ApiPageData<Any>
}

and在apiManager中生成这个service


object BaseApiManager {
val simpleService by lazy<SimpleService> {
getService()
}

接下来我定义了一个RequestParam类来帮助收敛请求需要的参数


@Keep
data class RequestParam<T>(
val clazz: Class<T>? = null,
val url: String,
val isGet: Boolean = true,
val paramBuilder: (HashMap<String, Any>.() -> Unit)? = null
){
val param: HashMap<String, Any>
get() {
val value = hashMapOf<String, Any>()
paramBuilder?.invoke(value)
return value
}
}

再然后便是请求真正发出的地方


internal fun <T> commonRequest(
param: RequestParam<T>,
builder: ((T) -> Unit)? = null
)
= flow {
emit(RequestResult.LOADING)
Timber.d(param.param.toString())
runCatching {
if (param.isGet) {
BaseApiManager.simpleService.commonGet(param.url, param.param)
} else {
BaseApiManager.simpleService.commonPost(param.url, param.param)
}
}.onSuccess {
if (it.code != StatusCode.REQUEST_SUCCESS) {
emit(RequestResult.Error(it.code, it.message))
} else {
val gson = Gson()
val data = gson.fromJson(gson.toJson(it.data), param.clazz)
builder?.invoke(data)
emit(RequestResult.Success(data))
}
}.onFailure {
emit(RequestResult.Error(StatusCode.REQUEST_FAILED, it.message))
}
}.flowOn(Dispatchers.IO)

在经过上述封装后,此时我在vm中发出一个网络请求就变成


viewModelScope.launch {
commonRequest(
RequestParam(
XXXBean::class.java, //数据类class
"/xxx/xxx/xxx", //地址
false //是否get
) {
put("xxx", 11)
put("xxxx", "25")
}
).collect {
when(it) {
RequestResult.INIT -> {
假设这边是Init弹窗
}
RequestResult.LOADING -> {
关闭Init弹窗
假设这边是Loading弹窗
}
is RequestResult.Error -> {
关闭Loading弹窗
toast(it.errorMsg)
}

is RequestResult.Success -> {
关闭Loading弹窗
发送成功事件或者改变UI状态
}
}
}

那么这边会遇到一个有点烦人的事情,实际上


RequestResult.INIT -> {
假设这边是Init弹窗
}
RequestResult.LOADING -> {
关闭Init弹窗
假设这边是Loading弹窗
}
is RequestResult.Error -> {
关闭Loading弹窗
toast(it.errorMsg)
}

这三兄弟中,我们经常会做一些重复的操作,于是我略施小计,将这几个行为定义成CommonEffect


sealed class MVICommonEffect {
data object ShowLoading: MVICommonEffect()
data object DismissLoading: MVICommonEffect()
data class ShowToast(val msg: String?): MVICommonEffect()
}

同时将Flow<RequestResult>的订阅步骤拆开,由于kt中两个隐式this对象写起来很繁琐,所以我是把这一串代码放到baseiewModel中的


fun <T> Flow<RequestResult<T>>.onInit(initBlock: suspend () -> Unit): Flow<RequestResult<T>> {
return onEach {
if (it == RequestResult.INIT) {
initBlock.invoke()
}
}
}

fun <T> Flow<RequestResult<T>>.onLoading(
showLoading: Boolean = true,
loadingBlock: suspend () -> Unit
)
: Flow<RequestResult<T>> {
return onEach {
if (it == RequestResult.LOADING) {
if (showLoading) {
emitLoadingEffect()
}
loadingBlock.invoke()
}
}
}

fun <T> Flow<RequestResult<T>>.onSuccess(
dismissLoading: Boolean = true,
successBlock: suspend ((data: T) -> Unit)
)
: Flow<RequestResult<T>> {
return onEach {
if (it is RequestResult.Success) {
if (dismissLoading) {
emitDismissLoadingEffect()
}
successBlock.invoke(it.data)
}
}
}

fun <T> Flow<RequestResult<T>>.onError(
dismissLoading: Boolean = true,
showToast: Boolean = true,
errorBlock: suspend (code: Int, msg: String?) -> Unit
)
: Flow<RequestResult<T>> {
return onEach {
if (it is RequestResult.Error) {
if (dismissLoading) {
emitDismissLoadingEffect()
}
if (showToast) {
emitToastEffect(it.errorMsg)
}
errorBlock.invoke(it.errorCode, it.errorMsg)
}
}
}

fun <T> Flow<RequestResult<T>>.onCommonSuccess(
loadingInvoke: Boolean,
showToast: Boolean,
successBlock: suspend ((data: T) -> Unit)
)
= this.onInit().onLoading(loadingInvoke)
.onError(
dismissLoading = loadingInvoke,
showToast = showToast
).onSuccess(
dismissLoading = loadingInvoke
) {
successBlock.invoke(it)
}

private val _commonEffect = MutableSharedFlow<MVICommonEffect>()
override val commonEffect: SharedFlow<MVICommonEffect> by lazy {
_commonEffect.asSharedFlow()
}

override suspend fun emitLoadingEffect() {
_commonEffect.emit(MVICommonEffect.ShowLoading)
}

override suspend fun emitDismissLoadingEffect() {
_commonEffect.emit(MVICommonEffect.DismissLoading)
}

override suspend fun emitToastEffect(msg: String?) {
_commonEffect.emit(MVICommonEffect.ShowToast(msg))
}

那么接下来,vm中网络请求就可以用一种很赏心悦目的方式出现了


private fun requestTestData(): Flow<RequestResult<XXXBean>> {
return commonRequest(
RequestParam(
XXXBean::class.java,
"xxx"
)
)
}

private fun updateTestData() {
requestData().onInit().onLoading().onError().onSuccess {
Timber.d(it.toString)
}.launchIn(viewModelScope)
}

接下来,只需要在基类View中订阅上述的MVICommonEffect,就可以handle大部分情况下的loading,toast.


由于本人能力有限,不足之处还望大佬指正.


作者:伟大的小炮队长
来源:juejin.cn/post/7368758932154843188
收起阅读 »

GPT-4o,遥遥领先,作为前端人的一些思考

大家好,我是LV。 我早上一般起的比较早~ 大概6点左右就起来刷各种AI资讯。 但是今天,5点左右就起来了,迫不及待想看 OpenAI 发布的内容~ 也顺便写篇文章跟大家分享一下最新的资讯~ 以及作为前端人的一些思考~ 希望对你有所帮助~ 欢迎加入最懂AI的...
继续阅读 »

大家好,我是LV。


我早上一般起的比较早~ 大概6点左右就起来刷各种AI资讯。


但是今天,5点左右就起来了,迫不及待想看 OpenAI 发布的内容~



也顺便写篇文章跟大家分享一下最新的资讯~


以及作为前端人的一些思考~


希望对你有所帮助~


欢迎加入最懂AI的前端伙伴们~群,一起探讨AI赋能前端研发。


GPT-4o



  • 结合文本、图像、视频、语音的全能模型

  • 可以通过语音交互以及具备识别物体和基于视觉信息进行快速回答的功能

  • 性能上,GPT-4o达到了GPT-4 Turbo水平

  • 成本相比GPT-4-turbo砍一半,速度快一倍,响应时间最低232毫秒,平均320毫秒。遥遥领先!

  • 将为 macOS 操作系统设计桌面ChatGPT应用程序,无缝集成到 macOs 中,可以使用键盘快捷键查询问题并与 ChatGPT 进行截图讨论或直接开展声音/视频对话。


以上详见:openai.com/index/hello…


前端人的思考


成本砍半,速度加倍


做应用层的前端er,可以换新的 API Model 了,虽然价格没有 3.5 那么便宜,也算是GPT4自由了(我也赶紧给LV0给换上)。


音视频支持



  • 通过视频连线ChatGPT,实时辅助修bug


之前只能够通过将bug转换为文字或者图片再给到AI,有了音视频功能,直接可以连线 ChatGPT,让GPT实时给你debug。



  • 通过视频连线ChatGPT,辅助编码,相当于请了一个24在线的编程导师~

  • 通过视频的形式给AI一些UI交互上的信息,从截图生代码 ==> 原型交互生代码(离AGI Code又近了一步)


跟macOS的结合


在vscode、在网页、在控制台、在Codding的任何地方,有问题,就会有答案。(作为mac粉,着实期待了~)


其他思考


作为AI应用研发的创业者角色,有几点思考~


OpenAI的这一波更新带来了新的机遇:


例如在教育领域、情感陪伴服务以及同声传译服务:



  • 语音增加了情绪理解和有感情的回复,老人或者残疾人士陪伴

  • 手机能够实时解析摄像头捕获的视频画面,并提供指导,这种能力有潜力取代家庭教师的角色

  • 同时进行翻译(即同传)的工作可以由此技术执行,从而有可能替代专业的同声传译人员


不过,这波更新也破灭了多少创业者正在做的事情~ 比如:


智能眼镜,给视疾人士提供出行便捷(我前几天还看到有人在花大力气自研这项技术,现在升级一下模型或许就能很低门槛接入了~)


Sam Altman 很早在斯坦福大学举办的一个演讲中预示:GPT-5和GPT-6将极大超越GPT-4,警示创业者考虑AI未来发展,创业不要要专注于解决当前AI的局限性问题。


简单来说:别做跟官方做技术竞争,比如:花大量时间通过各种布丁来拓展AI的上下文能力,降低迷惑性。


至于要做啥,从稳健的角度来看,不要轻易涉足一个未知的领域,建议基于熟悉的业务场景聚焦来做AI赋能。把现有你熟悉的业务场景梳理出来,尝试用AI结合进去,AI赋能现有的业务流程,让现有的业务跑起来效能更高或者门槛更低。


比如:我很熟悉前端研发领域,那我会深度聚焦AI赋能前端研发,拆解研发中的各个环节步骤,不断尝试AI赋能各个步骤,提升现有的研发效能,降低研发门槛,再把这些经验抽象产品化。


聚焦细分业务,保持敏锐度,将最新的AI技术快速结合到业务中去。


作者:LV技术派
来源:juejin.cn/post/7368421137917788198
收起阅读 »

在线人数统计功能怎么实现?

一、前言 大家好!我是sum墨,一个一线的底层码农,平时喜欢研究和思考一些技术相关的问题并整理成文,限于本人水平,如果文章和代码有表述不当之处,还请不吝赐教。 在线人数统计这个功能相信大家一眼就明白是啥,这个功能不难做,实现的方式也很多,这里说一下我常使用的方...
继续阅读 »

一、前言


大家好!我是sum墨,一个一线的底层码农,平时喜欢研究和思考一些技术相关的问题并整理成文,限于本人水平,如果文章和代码有表述不当之处,还请不吝赐教。


在线人数统计这个功能相信大家一眼就明白是啥,这个功能不难做,实现的方式也很多,这里说一下我常使用的方式:使用Redis的有序集合(zset)实现。
核心方法是这四个:zaddzrangeByScorezremrangeByScorezrem


二、实现步骤


1. 如何认定用户是否在线?


认定用户在线的条件一般跟网站有关,如果网站需要登录才能进入,那么这种网站就是根据用户的token令牌有效性判断是否在线;
如果网站是公开的,是那种不需要登录就可以浏览的,那么这种网站一般就需要自定一个规则来识别用户,也有很多方式实现如IPdeviceId浏览器指纹,推荐使用浏览器指纹的方式实现。


浏览器指纹可能包括以下信息的组合:用户代理字符串 (User-Agent string)、HTTP请求头信息、屏幕分辨率和颜色深度、时区和语言设置、浏览器插件详情等。现成的JavaScript库,像 FingerprintJSClientJS,可以帮助简化这个过程,因为它们已经实现了收集上述信息并生成唯一标识的算法。


使用起来也很简单,如下:


// 安装:npm install @fingerprintjs/fingerprintjs

// 使用示例:
import FingerprintJS from '@fingerprintjs/fingerprintjs';

// 初始化指纹JS Library
FingerprintJS.load().then(fp => {
// 获取访客ID
fp.get().then(result => {
const visitorId = result.visitorId;
console.log(visitorId);
});
});


这样就可以获取一个访问公开网站的用户的唯一ID了,当用户访问网站的时候,将这个ID放到访问链接的Cookie或者header中传到后台,后端服务根据这个ID标示用户。


2. zadd命令添加在线用户


(1)zadd命令介绍
zadd命令有三个参数



key:有序集合的名称。
score1、score2 等:分数值,可以是整数值或双精度浮点数。
member1、member2 等:要添加到有序集合的成员。
例子:向名为 myzset 的有序集合中添加一个成员:ZADD myzset 1 "one"



(2)添加在线用户标识到有序集合中


// expireTime给用户令牌设置了一个过期时间
LocalDateTime expireTime = LocalDateTime.now().plusSeconds(expireTimeout);
String expireTimeStr = DateUtil.formatFullTime(expireTime);
// 添加用户token到有序集合中
redisService.zadd("user.active", Double.parseDouble(expireTimeStr), userToken);


由于一个用户可能户会重复登录,这就导致userToken也会重复,但为了不重复计算这个用户的访问次数,zadd命令的第二个参数很好的解决了这个问题。
我这里的逻辑是:每次添加一个在线用户时,利用当前时间加上过期时间计算出一个分数,可以有效保证当前用户只会存在一个最新的登录态。



3. zrangeByScore命令查询在线人数


(1)zrangeByScore命令介绍



key:指定的有序集合的名字。
min 和 max:定义了查询的分数范围,也可以是 -inf 和 +inf(分别表示“负无穷大”和“正无穷大”)。
例子:查询分数在 1 到 3之间的所有成员:ZRANGEBYSCORE myzset 1 3



(2)查询当前所有的在线用户


// 获取当前的日期
String now = DateUtil.formatFullTime(LocalDateTime.now());
// 查询当前日期到"+inf"之间所有的用户
Set userOnlineStringSet = redisService.zrangeByScore("user.active", now, "+inf");


利用zrangeByScore方法可以查询这个有序集合指定范围内的用户,这个userOnlineStringSet也就是在线用户集,它的size就是在线人数了。



4. zremrangeByScore命令定时清除在线用户


(1)zremrangeByScore命令介绍



key:指定的有序集合的名字。
min 和 max:定义了查询的分数范围,也可以是 -inf 和 +inf(分别表示“负无穷大”和“正无穷大”)。
例子:删除分数在 1 到 3之间的所有成员:ZREMRANGEBYSCORE myzset 1 3



(2)定时清除在线用户


// 获取当前的日期
String now = DateUtil.formatFullTime(LocalDateTime.now());
// 清除当前日期到"-inf"之间所有的用户
redisService.zremrangeByScore(""user.active"","-inf", now);


由于有序集合不会自动清理下线的用户,所以这里我们需要写一个定时任务去定时删除下线的用户。



5. zrem命令用户退出登录时删除成员


(1)zrem命令介绍



key:指定的有序集合的名字。
members:需要删除的成员
例子:删除名为xxx的成员:ZREM myzset "xxx"



(2)定时清除在线用户


// 删除名为xxx的成员
redisService.zrem("user.active", "xxx");


删除 zset中的记录,确保主动退出的用户下线。



三、小结一下


这种方案的核心逻辑就是,创建一个在线用户身份集合为key,利用用户身份为member,利用过期时间为score,然后对这个集合进行增删改查,实现起来还是比较巧妙和简单的,大家有兴趣可以试试看。


作者:summo
来源:juejin.cn/post/7356065093060427816
收起阅读 »

汉文帝刘恒:权谋高手,带你看中式管理

前言 这里我打算先讲两个例子,让大家感受一下中式管理以及里面的运作规律。日常生活中,我们接触的都是表象,也就是最外层的具象,而里面的结构以及组成大部分人是没有太多去深入理解的。 汉文帝刘恒 谈到这个大家都会想起汉朝的文景之治,采用无为而治,少插手百姓生活,通...
继续阅读 »

efb27f6fa8439656d5ba22473411d951.jpeg


前言




这里我打算先讲两个例子,让大家感受一下中式管理以及里面的运作规律。日常生活中,我们接触的都是表象,也就是最外层的具象,而里面的结构以及组成大部分人是没有太多去深入理解的。


汉文帝刘恒


谈到这个大家都会想起汉朝的文景之治,采用无为而治,少插手百姓生活,通过这种市场经济的方式恢复民生。然而大部分人不了解的是他还是一个权谋高手,这还得从刘邦说起。


刘邦的势力是由功臣集团,比如说英布、彭越、韩信..,以及吕氏集团,吕后、樊哙、吕产..,刘氏集团,也就是刘邦的家族体系构成,刘邦称帝之后扶持吕氏势力来清洗异姓诸侯,当他发现吕氏力量非常强大的时候,就扶持戚夫人来平衡吕氏,没想到刘邦已经比较年迈了,无法像汉武帝扶持卫青、霍去病一样来壮大自己的力量,所以当吕后掌权之后对戚夫人干掉,扶持自家的吕氏上来,这个时候功臣集团跟他们有冲突,这里面的陈平、周勃以及刘氏集团依据刘邦最后留下的白马之盟,联手干掉吕氏,这时需要另立领导者。


因为这些功臣集团目的是为了稳固权力,又不能背锅,所以需要找一个势力比较弱小的,好拿捏的上来话事,所以刘恒上场了。


那么他做的几件事:


1、通过旧部收回御林军的权限,去清理异己,功臣集团不想背锅,只能乖乖的交出军权


2、安抚平反的人,论功行赏


3、分化内部,提了陈平,压了周勃


我们可以看到刘恒是一个权谋高手,本来是作为一个被控制人的角色出场的,通过几个关键动作最后稳住了自己的脚跟。


汉代丞相陈平


陈平是刘邦那会儿跟着他的,六处谋划为刘邦出了很大力,比如说真假汉王,为刘邦脱险;离间范增,使得项羽失去一个重要的谋士。在汉文帝的时候就被任命为丞相,他就问陈平你知道丞相是干嘛的吗?


陈平:“丞相向上是调理天子的气息,向下管理百官,对外监视诸侯,对内管好百姓”,看懂了吗,这就是位置决定职责,反观周勃他是军事人才,所以在此次之后自己辞职了。


《年会不能停》领导要领


当大鹏的来历被揭穿之后,有个领导跟他支招,想要当领导也不能就记住几点:第一不要明确自己的意思,第二会用感情牌,第三懂得分化。


上面的三个例子,大家是否对中式管理有了一个初步的认识呢?下面我将讲讲我对中式管理的认识。


power


定义




权力跟资本是同一个代名词,就是资源分配权,因为资源有限,那么就需要对它进行一个合理的分配。而权力跟资本也有不同。


权力是需要大量的铺垫,比如说乡里老人组,有很高的威望,它需要前期大量的文化铺垫的,它是一个长期有效的方式。


资本是需要权衡利弊的方式去谈判,因为资本是趋利的,跟人性一样,所以前期沟通成本很大,但是一旦达成很快执行很顺利。


构成




以古代皇权为例,下面有文官、外戚、太监,一直讲的集中中心力量,不是说皇权特别强,而是它需要支持者来巩固位置,所以一般是结合刚刚三方中的某一方来强化。


比如说刘邦,一开始借助吕氏势力干掉诸侯,然后扶持自己的外戚势力戚夫人,最后还定了白马之盟,给了功臣集团和刘氏集团正当的理由来处理过于强大的吕氏。


在公司里面也一样,ceo下面肯定需要掌握自己的核心部门的权限,来支持他执行自己的目标,并不是说这个公司是他的他想怎么弄就怎么弄,因为里面还有很多带资入组的大佬。


动作




中心力量 + 支持一方势力 => 打压其他势力


具体方式:洗牌、分化,在《年会不能停》这个电影里面讲到了公司效益不行,要进行裁员广进计划,人力部的权限就很高,从而干掉其他势力的人。至于分化,在前言的刘恒那里用到了,某个体系内部也不是说大家都是一致的,一旦有利益冲突,或者分配不均就会分化。


从上面的构成很容易理解这个动作的产生意义,这就是我们常说的内斗,我们平时很难理解为什么大家不干点正事,天天在那里斗哈哈,这就要谈到归属。


归属




之前写过一篇文章介绍过,就是管理权限是有归属的,如果说这个公司非常大,但是不是你的,跟你没有一点关系对不对。


这就是上面讲的内斗存在的意义,争取资源、机会,最终实现权力的扩大。你说资源重要吧,其实并不是,而是这种资源分配权更重要。


中式管理




上层管理


我们从陈平对宰相的理解可以知道,就是管理队伍、定好方向、管理资源(收益、风险),他还漏了一个权力斗争,这个肯定不能直说哈哈哈。


我们再回头看看《年会不能停》领导要领


1、不要明确自己的意思


这就很传统了,为什么规矩都是模棱两可的呢,如果规矩说的很清楚,还需要你干什么?这是第一点,第二点他可以再次被解读,而不是明确拍板,这样有锅也是下面干活的人出错。


2、分化,转移矛盾


这个典型的手段,当大家干的天昏地暗的时候,那么你的位置就很稳固,大家不会把矛盾指向你头上。


所以这一部分的管理核心技能是管理好团队,文化建设(权力形式、资本形式),管理好目标,管理好资源。


中下层管理


我认为这些是核心的业务主力,也就是攻城略地的大头兵。这一层他是上一层的弱化,应该更加偏向业务那块,比如说跨部门资源调用,团队工作计划制定,合理利用资源。


比如说韩信的十面埋伏,在打大战的时候你对团队的了解有多少,你对整个战场了解有多少,你的计划是怎样的。


管理的认知


1、看定位


每个等级它的要求不一样的,底层的大头兵更多是做事的技能、态度,因为要攻城略地,对于中下层管理,对局部的战况要有自己的把控。


2、职责


基于上面的定位,我们可以得出这个定位下面的职责。


3、管理:权力、资源、目标、文化(情绪)


这里涉及的知识面太多了,就不再展开了。


总结




有时我们看不懂为啥公司内部一直内斗,还有业务干的一团糟,看了它的定义、归属就会有一个大致的认知,正是因为我们对内部的构造没有比较深入的了解,以底层的大头兵角度就会觉得这是内耗的情况。


这种管理很大程度跟文化有关系,也就是几千年来演变的规律形成的习惯在影响我们现代管理模式,它很简单,就是管理资源、方向,它也很复杂,单纯一个方面拎出来都是一个很大的知识面。


作者:大鸡腿同学
来源:juejin.cn/post/7329100659877494796
收起阅读 »

刘邦-中年痞子到霸道总裁的一生

前言 最近花了9块钱开通会员,就为了读下《汉高祖刘邦》这本书,一直以来我认为社会阅历是人跟人之间的差距,这种属于后天的积累,当你在社会实践的时候过于单一,或者说接触面更少的时候,应该读读别人的传记。 刘邦有几个比较有意思的点,首先他有天选之子的面相、骨相,当...
继续阅读 »

前言




最近花了9块钱开通会员,就为了读下《汉高祖刘邦》这本书,一直以来我认为社会阅历是人跟人之间的差距,这种属于后天的积累,当你在社会实践的时候过于单一,或者说接触面更少的时候,应该读读别人的传记。


刘邦有几个比较有意思的点,首先他有天选之子的面相、骨相,当然我认识这些真正目的有几个,一个是人需要借助名头、声望来发展自身实力一样道理,另一个是权力正统性,能够说服别人,这个相当重要。其次我觉得很奇怪,他的能力可以跟他的岗位匹配上,从一个庭长到一个君主,这里面要求的能力是不一样的,他为啥能具备这方面的能力的转变呢?


下面我们就一一展开刘邦的一生历程,以及我读后的感受。


各个派系对比




背景


在秦朝末期,因为律法严苛,导致民愤,各个势力崛起,非常经典的说法;其中就有刘邦、项羽、还有很出名的陈胜吴广。


ps:当你看秦朝发家的时候你就会清楚,它是如何变成一个战争机器的,工作的细化,把种田的锁死在种田上,打战的打战,定下军功授于的规则。就像《共产宣言》里面讲的,当工作细分化之后,人会更加专业化,效率更高,同时工作量更大,更加劳累。当秦统一六国之后,推行同样的机制,而且推行郡县制,势必遭到各个传统门阀的抵抗的,也不一定适用其他地方。


你觉得的律法严苛,实际上是人家的发家史,只不过无法推广,以及郡县制的影响太大。


派系发展历程


1、陈胜吴广


相比另外两股力量,显得不太起眼,因为结束的比较早,我觉得跟资源有关系,刘邦自己一开始就是亭长,吕氏家底,项羽门阀势力,反观这个陈吴资源是利益临时凑在一起,另外能力上军事、管理都不突出。


2、刘邦


他是在40岁之后才开始走大运,他之前更像一个痞子的作风,为人豪爽,喜欢喝酒结交朋友,然后做了亭长,认识樊哙、曹参、夏侯婴这些铁子,跟当地的大户吕氏有结交,期间认识萧何,后来在沛县反叛,在发展过程中认识张良,通过张良结识其他人,比如项伯,也就是鸿门宴上解围的那哥们;本身刘邦势力比较薄弱,需要借助多方势力,比如敌方将领,项羽部下策反了英布,韩信原本也是项羽帐前持戟郎中,另外借助彭越对抗项羽;最后在韩信十面埋伏,还有张良的四面楚歌下将项羽击败。


这是一统之前的历程,后面开始削韩信,平异姓王,白马之盟,完成了从一个痞子到霸道总裁的转变。


军事:


家底是吕氏 + 后面加入带资异姓王


权谋:


a、朋友多多,敌人少少


这对于只会武力的人来讲是降维打击,用了敌方的英布、韩信,助力了彭越其他势力,从而发展自己的势力。


b、驭将


舍得利益,比如封韩信齐王,后面多次封赏,当一统之后对各个出力的人给予套现机会。


c、权力纵横术


一统之后,韩信成为刘邦心中的刺,使用了狡猾的手段去除了韩信的兵权,然后软禁,平定各个异姓王,最后因为吕氏权力太大了,定下了白马之盟。


这个是陈胜吴广所无法具备的能力,以及项羽,对比起来项羽更像莽夫。


扩展一下:首先权力的正统性是很重要的,另外维持权力的力量必不可少,最后是权力纵横术。因为本身资源就是有限的,一定会内斗,其次只有在互相制衡的基础上,可以保证这种头头的稳固。


3、项羽


对比其他势力,他有很多优势,是一个门阀家族,家族里面有项伯、项梁,门下还有很多人追随,比如说英布、季布等悍将,韩信也是在下面干过一段时间,再加上项羽本身天生神力,所向披靡。打出了非常有名的巨鹿之战,破釜沉舟,大破秦军,因为跟刘邦的约定先进城先为王,听取范增意见设置了鸿门宴,后面刘邦封为汉王,同时也为压制刘邦势力让他去汉中地方,派出以前投降的秦降将去守,所以后来被韩信的暗渡陈仓偷袭了,还有后面背水一战,围攻刘邦于荥阳,差点把刘邦嘎了,但是后面的事上面也有提到被韩信、刘邦等围功,中了计谋,被反间了范增,还有十面埋伏这些,最终失败了。


军事:


项羽的家族很雄厚的,而且跟随的人才大有人在


权谋:


a、大力出奇迹


本身项羽的条件、资源确实比较叼,有点偏科感觉哈哈


b、不会管理,妇人之仁


首先他不注重谋士,比如韩信经常给他提意见,没有给下属发展空间。然后对于鸿门宴的时候,没有对刘邦下手,没有霸道总裁的雷厉风行的手段。


说白了,不是很好的管理者,从英布、韩信的出走看出,因为人家在你下面没有施展空间,其次你没有一个奖励机制来推进大家为你卖力。


idea




很重要一点,我们能从这些历史故事中学到点什么?



  • 做事


1、人脉、资源


我们看刘邦以前虽然说是个痞子,但是人家人脉真的广,一个是亭长的位置,认识一群铁子,结识了当地的大户吕氏,如果他后面没有壮大其实在沛县也是一个小霸王,做什么事都方便。


这就是打工人不具备的东西,圈子小,能力还不强。


2、德要配位


作为一个管理者,你是否具备业务、人才规划能力,以及利益合理分配,还有激励体系建设。就像韩信点兵,每个人都能来个2w人马,点个10几个副将,东南西北布阵,你的能力跟位置匹配的。


3、博弈能力


这个是非常难的,以前我们会陷入非黑即白,就是不是朋友就是敌人,我个人也很难逃脱这个认知。但是纵观优秀的战略家,可以权衡利害,这就是很难的。


比如说刘邦就很听劝,可能人家很生气,但是只要你足够说服力,他可以听你的。他可以接受敌方的英雄,可以接受跟匈奴联姻,他可以把他儿子踹下来躲避楚军追击,理智的逻辑胜于情绪。


如果你回头再看冯唐老师讲的,不要脸,不着急,不害怕,一个痞子更容易做出点成就,“前途光明,道路曲折”,每个有成就的人都会经历各种曲折,下面的乐观精神也是一种不害怕的表现。



  • 乐观的态度


他一直很乐观,从发家没有项羽叼,到多次差点被嘎,被项羽胖揍,刘邦挺乐观的。


这个非常重要,纵观现代年轻人,他们会觉得当前环境对他们比较难发展,就是没有资源、没有比较强的赚钱能力,所以采取收缩以求得自在生存,也会产生悲观的心态,这个本身是人性、本能的选择。但是当你有乐观精神,才有面对困难的勇气,也有了捕捉机会的欲望。



  • 陈胜吴广的失败


我们可以比喻成如何办理一场活动,首先需要人才对吧,布置活动现场,策划,干活,相比之下他们就是乌合之众,没有得力干将,没有杰出的管理人才,然后办活动需要资金对吧,有支持的粮食、金钱,他们并不代表某一方的势力可以稳定的输入资金来源,当你有了资金之后,队伍庞大之后,就有派系斗争,领导者有没有对应的权力纵横术、权衡利害能力。


作者:大鸡腿同学
来源:juejin.cn/post/7322723692745031690
收起阅读 »

需求小能手——拦截浏览器窗口关闭

web
前言 最近碰到一个需求,网页端页面有评价功能,要求用户点击关闭浏览器时强制弹出评价对话框让用户评价。刚听到这个需求我大意了没有闪,以为很简单,没想到很难实现,很多需求果然不能想当然啊,接下来我们来看一下该功能实现的一些思路。 窗口关闭 要想实现该功能最简单的想...
继续阅读 »

前言


最近碰到一个需求,网页端页面有评价功能,要求用户点击关闭浏览器时强制弹出评价对话框让用户评价。刚听到这个需求我大意了没有闪,以为很简单,没想到很难实现,很多需求果然不能想当然啊,接下来我们来看一下该功能实现的一些思路。


窗口关闭


要想实现该功能最简单的想法就是监听浏览器关闭事件,然后阻止默认事件,执行自定义的事件。整个思路核心就是监听事件,搜索一番果然有浏览器关闭触发的事件。


事件



  • onunload:资源被卸载时触发,浏览器窗口关闭时卸载资源就会触发,我们监听一下该事件看能不能阻止窗口关闭。


    window.addEventListener('unload', function (e) {
console.log(e);
e.preventDefault()
});

打开页面再关闭,会发现控制台打印出了e然后就关闭了,看来在onunload事件中并不能阻止窗口关闭,得另找方法,刚好在onunload事件介绍中还链接了一个事件——beforeonunlaod。



  • beforeunload :当窗口关闭或刷新时触发,该事件在onunload之前触发。并且在该事件中可以弹出对话框,询问用户是否确认离开或者重新加载,这不是正是我们想要的效果。根据mdn上的介绍,要想出现弹出对话看需要用preventDefault()事件,并且为了兼容性我们最好再加上以下方法中的一个:

    1.将e.renturenValue赋一个字符串。

    2.事件函数返回一个字符串。
    接下来让我们试一试:


    window.addEventListener('beforeunload', function (e) {
e.preventDefault()
e.returnValue = ''
});

打开关闭未生效,再检查下代码没问题呀,这是因为浏览器本身安全机制导致的,在ie浏览器中没有任何限制,但是在chrome、edge等浏览器中用户必须在短时间操作过页面才能触发。打开页面点几个文字在关闭窗口,这次就能出现弹窗了。

2(W_WV8AVWRT3(4R1HWBRR7.png

当我们点击离开页面就会关闭,点击取消继续停留,上面提到过刷线也能触发,我们再点下刷新。

T456)ZI7MJ2XK1X3M%BE7SN.png

出现的提示有所改变,我们知道浏览器的刷新有好几种方式,我们可以都尝试一下:



  • ctrl+R:本身就是浏览器刷新按钮的快捷键,能够触发。

  • f5:能否触发。

  • 前进、后退:能够触发。

    这三种方式提示内容跟点击刷新按钮一样。回到我们的需求,虽然已经能够阻止窗口关闭,但是刷新依旧能阻止,我们需求是用户关闭,所以我们要区分用户操作是刷新还是关闭。


区分


要想区分就要找到以下两者之间的区别,两者都会执行onbeforeunload与onunload两个事件,不能直接通过某个事件区分。但是两个事件之间的时间差是不同的。刷新时两者时间差在10毫秒左右,而关闭时在3毫秒左右,判断以下时间差就能区分出来。


       var time = null;
window.addEventListener('beforeunload', function (e) {
time = new Date().getTime();
});
window.addEventListener('unload', function (e) {
const nowTime = new Date().getTime();
if (nowTime - time < 5) {
console.log('窗口关闭');
}
});

用此方法就能区分出来,但是此判断是在onunload事件中的,而窗口弹出是在beforeunlaod,这方法只适用于在关闭时执行某个函数,但不能满足我们的需求。除此之外还有一个问题就是刷新默认弹出对话框的内容是不能修改的,所以如果我们想要弹出自定义的对话框是不可能的。经过分析操作能够做到的就是,在用户刷新或关闭时出现系统自带对话框,同时在下方弹出自定义对话框,然后用户点击取消再去操作自定义对话框。


总结


总的来说要想拦截浏览器窗口关闭并且弹出自定义对话框,目前我还没有完美的实现方案,只能带有众多缺陷的去实现。如果我们只是想在关闭窗口前执行函数那就使用时间差区分即可。


作者:躺平使者
来源:juejin.cn/post/7281912738862481448
收起阅读 »

工作两年以来,被磨圆滑了,心智有所成长……

刚毕业时候年轻气盛,和邻居组的老板吵了几句。后来我晋升时,发现他是评委…… 曾经的我多么嚣张,现在的我就多么低调。 一路走来,磕磕绊绊,几年来,我总结了工作上的思考…… 工作思考 有效控制情绪,在沟通时使用适当的表情包以传达善意。无论线上还是线下,都应避免争...
继续阅读 »

刚毕业时候年轻气盛,和邻居组的老板吵了几句。后来我晋升时,发现他是评委…… 曾经的我多么嚣张,现在的我就多么低调。


一路走来,磕磕绊绊,几年来,我总结了工作上的思考……


工作思考



  1. 有效控制情绪,在沟通时使用适当的表情包以传达善意。无论线上还是线下,都应避免争吵。只有和气相处,我们才能推动工作的进展。

  2. 在讨论具体问题之前,先进行一些预备性的交流。情绪应放在第一位,工作讨论放在第二位。如果对方情绪不好,最好选择另一个时间再进行讨论。

  3. 在与他人交流时要保持初学者的态度和需求,不要用技术去怼人。

  4. 进入新团队先提升自己在团队的业务能力,对整个系统有足够的了解,不要怕问问题和学习。不要新入职就想毁天灭地,指手画脚 ”这里的设计不合理,那里有性能瓶颈“。

  5. 在各个事情上,都要比别人多了解一点。对于关键的事情要精通,对于其他事情也要多花一点时间去投入。

  6. 遇到困难时,先自己思考和尝试解决,然后再请教他人。不要机械地提问,也不要埋头一直搞而不主动提问。但如果是新入职,可以例外,多提问总没有坏处,但要在思考的基础上提问。

  7. 当向他人求助时,首先要清晰地阐述自己正在面临的问题、目标、已尝试的方法以及所需要的帮助和紧迫程度。所有的方面都要有所涉及。在提问之前,最好加上一句是否可以帮忙,这样对解决问题是否有帮助更加明确。因为别

  8. 一定有时间来帮助你,即使有时间,你也不一定找对了人。

  9. 在明确软件产品要解决的业务问题之前,先了解自己负责的那部分与业务的对应关系。

  10. 主要核心问题一定要提前叙述清楚,不要等别人问

  11. 要始终坚持追踪事情的进展,与与自己有交互的队友讨论接口,并关注他们的进度,以确保协调一致。

  12. 要主动向队友述说自己的困难,在项目延期或遇到困难时,要主动求助同事或领导,是否能分配部分工作给其他人,不要全部自己承担。

  13. 如果预计任务需要延期,要提前告知领导。如果有进展,也要及时向领导汇报。

  14. 如果无法参加会议但是自己是会议的重要参与者,一定要提前告知领导自己的进度、计划和想法,最好以书面形式或电话告知。如果可以远程参加,可以选择电话参加。除非有极其重要的事情,务必参加会议。不要假设别人都知道你的进度和想法。

  15. 要少说话,多做事。在开会时,不要凭借想当然的想法,可以询问其他小组的细节,但不要妄自揣测别人的细节,以为自己是对的。否则会被批评。

  16. 程序员如果经验丰富,很容易产生自我感觉良好的情绪。要避免这种情况,我们必须使用自己没有使用过的东西,并进行充分的测试,这样才能减少问题的出现。要提前考虑好所有细节,不要认为没有问题就不加考虑。要给自己留出处理问题的时间,并及时反馈并寻求帮助。

  17. 当与他人交流时,要始终保持有始有终的态度,特别是当寻求他人帮助时,最后一定要确认OK。要胆大心细,不要害怕犯错,要有成果,要快速并提高效率,不择手段地追求快速,并对结果负责。工作一定要完成闭环,要记事情要好,记住重要的事情并使用备忘录记录待办事项。

  18. 每完成一个项目后,应该回顾一下使用了什么知识、技能和工具。要总结并记录下这些,并与之前积累的知识和技能进行关联。如果发生了错误,也要记录下来,并将经验进行总结。

  19. 每天早上先思考今天要做什么,列出1、2、3,然后每天晚上下班时回顾已完成的任务、未完成的任务以及遇到的问题。

  20. 如果有待办事项没有立即处理,一定要用工具记录下来,不要心存侥幸以为自己能记住。


代码编写和技术问题



  1. 在代码编写过程中要认真对待,对于代码审核之前,要自己好好检查,给人一种可靠的感觉。

  2. 对于代码审核,不要过于苛刻,要容忍个人的发挥。

  3. 在提交代码给测试之前,应该先自行进行测试验证通过。

  4. 如果接口没有做到幂等性,那就会给未来的人工运维增加困难。当数据存在多份副本时,例如容量信息和上下游同时存在的资源,需要评估数据不一致的可能性以及解决方法。可以考虑通过数据校准或严格的代码编写来保证最终的一致性,或者考虑只在一方保存数据或以一方的数据为准。一旦出现数据不一致,则以其中一方的数据为准,无需人为干预即可自动达到数据再次一致。

  5. 要学会横向和纵向分割隔离系统,明确系统的边界,这样可以更好地进行并发合作开发和运维,提高效率。各个子系统应该独立变化,新的设计要考虑向后兼容性和上下游兼容性问题,包括上线期间的新老版本兼容。在设计评审阶段就应该重视这些问题。

  6. 如果在代码审查中无法发现业务问题或代码风格问题,不妨重点关注日志的打印是否合理和是否存在bug。

  7. 在依赖某个服务或与其他服务共享时,要确认该服务是否要废弃、是否是系统的瓶颈,以及是否可以自己进行改造或寻找更优的提供者。

  8. 使用缓存时注意预热,以防止开始使用时大量的缓存未命中导致数据库负载过高。

  9. 在使用rpc和mq、共享数据库、轮询、进程间通信和服务间通信时,要根据情况做出选择,并注意不要产生依赖倒置。

  10. 在接口有任何变动时,务必通过书面和口头确认。在这方面,要多沟通,尽量详细,以避免出现严重问题!毕竟,软件系统非常复杂,上下游之间的理解难以保持一致。

  11. 尽可能使用批量接口,并考虑是否需要完全批量查询。当批量接口性能较差时,设置适当的最大数量,并考虑客户端支持将批量接口聚合查询。批量接口往往是tp99最高的接口。

  12. 对于系统重要设计和功能,要考虑降级预案,并加入一些开关来满足安全性和性能需求。

  13. 如果数据不一致,可以考虑对比两方的不一致数据并打印错误日志,例如es/db等。

  14. 在系统设计之前,要充分调研其他人的设计,了解背景和现状。

  15. 废弃的代码应立即删除,如果以后需要,可以从git中找回。如果实在不想删除,也要注释掉!特别是对外的rpc、http接口,不使用的要立即删除,保持代码简洁。接手项目的人不熟悉背景情况,很难判断这段废弃代码的意义,容易造成混乱和浪费时间。要努力将其和其他有效代码联系起来,但这很困难。

  16. 在代码中要有详尽的日志记录!但是必须有条理和规范,只打印关键部分。对于执行的定时任务,应该打印足够详细的统计结果。最好使用简洁明了的日志,只记录最少量但最详细的信息,反馈程序的执行路径。

  17. 如果接口调用失败或超时,应该如何处理?幂等和重试如何处理?


当你写下一行代码前



  1. 要明确这行代码可能出现的异常情况以及如何处理,是将异常隔离、忽略还是单独处理,以防遗漏某些异常。

  2. 需要确保该行代码的输入是否已进行校验,并考虑校验可能引发的异常。

  3. 需要思考由谁调用该代码,会涉及哪些上游调用,并确定向调用者提供什么样的预期结果。

  4. 需要确定是否调用了一个方法或接口,以及该调用是否会阻塞或是异步的,并考虑对性能的影响。

  5. 需要评估该行代码是否可以进行优化,是否可以复用。

  6. 如果该行代码是控制语句,考虑是否能简化控制流程是否扁平。

  7. 对于日志打印或与主要逻辑无关的输出或报警,是否需要多加关注,因为它们可能还是很重要的。

  8. 如果代码是set等方法,也要仔细检查,避免赋错属性。IDE可能会有误提示,因为属性名前缀类似,set方法容易赋值错误。


当你设计一个接口时



  1. 接口的语义应该足够明确,避免出现过于综合的上帝接口

  2. 如果语义不明确,需要明确上下游的期望和需求。有些需求可以选择不提供给上游调用。

  3. 对于接口超时的处理,可以考虑重试和幂等性。在创建和删除接口时要确定是否具有幂等性,同时,幂等后返回的数据是否和首次请求一致也需要考虑。

  4. 接口是否需要防止并发,以及是否成为性能瓶颈也需要考虑。

  5. 设计接口时要确保调用方能够完全理解,如果他对接口的理解有问题,就需要重新设计接口。这一点非常关键,可以通过邮件确认或者面对面交流来确保调用方理解得清楚。

  6. 在开发过程中,需要定期关注队友的开发进度,了解他们是否已经使用了接口以及是否遇到了问题。这个原则适用于所有的上下游和相关方,包括产品和测试人员。要想清楚如何对接口进行测试,并与测试人员明确交流。

  7. 最好自己整理好测试用例,不要盲目地指望测试人员能发现所有的bug。

  8. 需要考虑是否需要批量处理这个接口,以减少rpc请求的次数。但即使是批量处理,也要注意一次批处理最多处理多少条记录,不要一次性处理全部记录,避免由于网络阻塞或批量处理时间过长导致上游调用超时,需要适度控制批量处理的规模。


作者:五阳
来源:juejin.cn/post/7306025036656787475
收起阅读 »

前端实现文件预览img、docx、xlsx、ppt、pdf、md、txt、audio、video

web
前言 最近有接到一个需求,要求前端支持上传制定后缀文件,且支持页面预览,上传简单,那么预览该怎么实现呢,尤其是不同类型的文件预览方案,那么下面就我这个需求的实现,分不同情况来讲解一下👇 具体的预览需求: 预览需要支持的文件类型有: png、jpg、jpeg...
继续阅读 »

前言



最近有接到一个需求,要求前端支持上传制定后缀文件,且支持页面预览,上传简单,那么预览该怎么实现呢,尤其是不同类型的文件预览方案,那么下面就我这个需求的实现,分不同情况来讲解一下👇



具体的预览需求:
预览需要支持的文件类型有: png、jpg、jpeg、docx、xlsx、ppt、pdf、md、txt、audio、video,另外对于不同文档还需要有定位的功能。例如:pdf 定位到页码,txtmarkdown定位到文字并滚动到指定的位置,音视频定位到具体的时间等等。




⚠️ 补充: 我的需求是需要先将文件上传到后台,然后我拿到url地址去展示,对于markdowntxt的文件需要先用fetch获取,其他的展示则直接使用url链接就可以。


不同文件的实现方式不同,下面分类讲解,总共分为以下几类:



  1. 自有标签文件:png、jpg、jpeg、audio、video

  2. 纯文字的文件: markdown & txt

  3. office 类型的文件: docx、xlsx、ppt

  4. embed 引入文件:pdf

  5. iframe:引入外部完整的网站




自有标签文件:png、jpg、jpeg、audio、video



对于图片、音视频的预览,直接使用对应的标签即可,如下:



图片:png、jpg、jpeg


示例代码:


 <img src={url} key={docId} alt={name} width="100%" />;

预览效果如下:


截屏2024-04-30 11.18.01.png


音频:audio


示例代码:


<audio ref={audioRef} controls controlsList="nodownload" style={{ width: '100%' }}>
<track kind="captions" />
<source src={url} type="audio/mpeg" />
</audio>

预览效果如下:


截屏2024-04-30 11.18.45.png


视频:video


示例代码:


<video ref={videoRef} controls muted controlsList="nodownload" style={{ width: '100%' }}>
<track kind="captions" />
<source src={url} type="video/mp4" />
</video>

预览效果如下:


截屏2024-05-13 18.21.13.png


关于音视频的定位的完整代码:


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

interface IProps {
type: 'audio' | 'video';
url: string;
timeInSeconds: number;
}

function AudioAndVideo(props: IProps) {
const { type, url, timeInSeconds } = props;
const videoRef = useRef<HTMLVideoElement>(null);
const audioRef = useRef<HTMLAudioElement>(null);

useEffect(() => {
// 音视频定位
const secondsTime = timeInSeconds / 1000;
if (type === 'audio' && audioRef.current) {
audioRef.current.currentTime = secondsTime;
}
if (type === 'video' && videoRef.current) {
videoRef.current.currentTime = secondsTime;
}
}, [type, timeInSeconds]);

return (
<div>
{type === 'audio' ? (
<audio ref={audioRef} controls controlsList="nodownload" style={{ width: '100%' }}>
<track kind="captions" />
<source src={url} type="audio/mpeg" />
</audio>
) : (
<video ref={videoRef} controls muted controlsList="nodownload" style={{ width: '100%' }}>
<track kind="captions" />
<source src={url} type="video/mp4" />
</video>
)}
</div>
);
}

export default AudioAndVideo;



纯文字的文件: markdown & txt



对于markdown、txt类型的文件,如果拿到的是文件的url的话,则无法直接显示,需要请求到内容,再进行展示。



markdown 文件



在展示markdown文件时,需要满足字体高亮、代码高亮、如果有字体高亮,需要滚动到字体所在位置、如果有外部链接,需要新开tab页面再打开。



需要引入两个库:


marked:它的作用是将markdown文本转换(解析)为HTML


highlight: 它允许开发者在网页上高亮显示代码。


字体高亮的代码实现:



高亮的样式,可以在行间样式定义



  const highlightAndMarkFirst = (text: string, highlightText: string) => {
let firstMatchDone = false;
const regex = new RegExp(`(${highlightText})`, 'gi');
return text.replace(regex, (match) => {
if (!firstMatchDone) {
firstMatchDone = true;
return `<span id='first-match' style="color: red;">${match}</span>`;
}
return `<span style="color: red;">${match}</span>`;
});
};

代码高亮的代码实现:



需要借助hljs这个库进行转换



marked.use({
renderer: {
code(code, infostring) {
const validLang = !!(infostring && hljs.getLanguage(infostring));
const highlighted = validLang
? hljs.highlight(code, { language: infostring, ignoreIllegals: true }).value
: code;
return `<pre><code class="hljs ${infostring}">${highlighted}</code></pre>`;
}
},
});

链接跳转新tab页的代码实现:


marked.use({
renderer: {
// 链接跳转
link(href, title, text) {
const isExternal = !href.startsWith('/') && !href.startsWith('#');
if (isExternal) {
return `<a href="${href}" title="${title}" target="_blank" rel="noopener noreferrer">${text}</a>`;
}
return `<a href="${href}" title="${title}">${text}</a>`;
},
},
});

滚动到高亮的位置的代码实现:



需要配合上面的代码高亮的方法



const firstMatchElement = document.getElementById('first-match');
if (firstMatchElement) {
firstMatchElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
}

完整的代码如下:



入参的docUrlmarkdown文件的线上url地址,searchText 是需要高亮的内容。



import React, { useEffect, useState, useRef } from 'react';
import { marked } from 'marked';
import hljs from 'highlight.js';

const preStyle = {
width: '100%',
maxHeight: '64vh',
minHeight: '64vh',
overflow: 'auto',
};

// Markdown展示组件
function MarkdownViewer({ docUrl, searchText }: { docUrl: string; searchText: string }) {
const [markdown, setMarkdown] = useState('');
const markdownRef = useRef<HTMLDivElement | null>(null);

const highlightAndMarkFirst = (text: string, highlightText: string) => {
let firstMatchDone = false;
const regex = new RegExp(`(${highlightText})`, 'gi');
return text.replace(regex, (match) => {
if (!firstMatchDone) {
firstMatchDone = true;
return `<span id='first-match' style="color: red;">${match}</span>`;
}
return `<span style="color: red;">${match}</span>`;
});
};

useEffect(() => {
// 如果没有搜索内容,直接加载原始Markdown文本
fetch(docUrl)
.then((response) => response.text())
.then((text) => {
const highlightedText = searchText ? highlightAndMarkFirst(text, searchText) : text;
setMarkdown(highlightedText);
})
.catch((error) => console.error('加载Markdown文件失败:', error));
}, [searchText, docUrl]);

useEffect(() => {
if (markdownRef.current) {
// 支持代码高亮
marked.use({
renderer: {
code(code, infostring) {
const validLang = !!(infostring && hljs.getLanguage(infostring));
const highlighted = validLang
? hljs.highlight(code, { language: infostring, ignoreIllegals: true }).value
: code;
return `<pre><code class="hljs ${infostring}">${highlighted}</code></pre>`;
},
// 链接跳转
link(href, title, text) {
const isExternal = !href.startsWith('/') && !href.startsWith('#');
if (isExternal) {
return `<a href="${href}" title="${title}" target="_blank" rel="noopener noreferrer">${text}</a>`;
}
return `<a href="${href}" title="${title}">${text}</a>`;
},
},
});
const htmlContent = marked.parse(markdown);
markdownRef.current!.innerHTML = htmlContent as string;
// 当markdown更新后,检查是否需要滚动到高亮位置
const firstMatchElement = document.getElementById('first-match');
if (firstMatchElement) {
firstMatchElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}
}, [markdown]);

return (
<div style={preStyle}>
<div ref={markdownRef} />
</div>

);
}

export default MarkdownViewer;

预览效果如下:


截屏2024-05-13 17.59.04.png


txt 文件预览展示



支持高亮和滚动到指定位置



支持高亮的代码:


  function highlightText(text: string) {
if (!searchText.trim()) return text;
const regex = new RegExp(`(${searchText})`, 'gi');
return text.replace(regex, `<span style="color: red">$1</span>`);
}

完整代码:


import React, { useEffect, useState, useRef } from 'react';
import { preStyle } from './config';

function TextFileViewer({ docurl, searchText }: { docurl: string; searchText: string }) {
const [paragraphs, setParagraphs] = useState<string[]>([]);
const targetRef = useRef<HTMLDivElement | null>(null);

function highlightText(text: string) {
if (!searchText.trim()) return text;
const regex = new RegExp(`(${searchText})`, 'gi');
return text.replace(regex, `<span style="color: red">$1</span>`);
}

useEffect(() => {
fetch(docurl)
.then((response) => response.text())
.then((text) => {
const highlightedText = highlightText(text);
const paras = highlightedText
.split('\n')
.map((para) => para.trim())
.filter((para) => para);
setParagraphs(paras);
})
.catch((error) => {
console.error('加载文本文件出错:', error);
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [docurl, searchText]);

useEffect(() => {
// 处理高亮段落的滚动逻辑
const timer = setTimeout(() => {
if (targetRef.current) {
targetRef.current.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}, 100);

return () => clearTimeout(timer);
}, [paragraphs]);

return (
<div style={preStyle}>
{paragraphs.map((para: string, index: number) => {
const paraKey = para + index;

// 确定这个段落是否包含高亮文本
const isTarget = para.includes(`>${searchText}<`);
return (
<p key={paraKey} ref={isTarget && !targetRef.current ? targetRef : null}>
<div dangerouslySetInnerHTML={{ __html: para }} />
</p>
);
})}
</div>

);
}

export default TextFileViewer;

预览效果如下:


截屏2024-05-13 18.34.27.png




office 类型的文件: docx、xlsx、ppt



docx、xlsx、ppt 文件的预览,用的是office的线上预览链接 + 我们文件的线上url即可。




关于定位:用这种方法我暂时尝试是无法定位页码的,所以定位的功能我采取的是后端将office 文件转成pdf,再进行定位,如果只是纯展示,忽略这个问题即可。



示例代码:


<iframe
src={`https://view.officeapps.live.com/op/view.aspx?src=${url}`}
width="100%"
height="500px"
frameBorder="0"
></iframe>

预览效果如下:


截屏2024-05-07 17.58.45.png




embed 引入文件:pdf



pdf文档预览时,可以采用embed的方式,这个httpsUrl就是你的pdf文档的链接地址



示例代码:


 <embed src={`${httpsUrl}`} style={preStyle} key={`${httpsUrl}`} />;

关于定位,其实是地址上拼接的页码sourcePage,如下:


 const httpsUrl = sourcePage
? `${doc.url}#page=${sourcePage}`
: doc.url;

<embed src={`${httpsUrl}`} style={preStyle} key={`${httpsUrl}`} />;


预览效果如下:


截屏2024-05-07 17.50.07.png




iframe:引入外部完整的网站



除了上面的各种文件,我们还需要预览一些外部的网址,那就要用到iframe的方式



示例代码:


 <iframe
title="网址"
width="100%"
height="100%"
src={doc.url}
allow="microphone;camera;midi;encrypted-media;"/>


预览效果如下:


截屏2024-05-07 17.51.26.png




总结: 到这里我们支持的所有文件都讲述完了,有什么问题,欢迎评论区留言!


作者:玖月晴空
来源:juejin.cn/post/7366432628440924170
收起阅读 »

人走茶凉?勾心斗角?职场无友谊?

你和同事之间存在竞争关系 要不要把工作关系维护成伙伴关系 明枪暗箭防不胜防 背后捅刀子往往最不设防 大家是否在职场上交友是有也遇到过以上困扰呢? 不要在职场上交“朋友”,而是要寻找“盟友”。 这两者的区别在于应对策略: 我们会愿意为“朋友”牺牲自己的利益,像是...
继续阅读 »

你和同事之间存在竞争关系


要不要把工作关系维护成伙伴关系


明枪暗箭防不胜防


背后捅刀子往往最不设防


大家是否在职场上交友是有也遇到过以上困扰呢?


不要在职场上交“朋友”,而是要寻找“盟友”。


这两者的区别在于应对策略:


我们会愿意为“朋友”牺牲自己的利益,像是一张年卡。


而结交“盟友”就是为了一起争取更多利益,《孔乙己》说得好:“这次是现钱,酒要好。”


所以,在职场上的“受欢迎”和社交场、朋友圈上的“受欢迎”之间有着本质的区别:


你和你的同事未必真心喜欢彼此,但在日常相处当中能够客气、友善地交往。


大家需要寻找盟友时会第一个想到你,在争斗冲突时会尽量绕开你,这就是一种非常理想的“受欢迎”状态。 不要在职场上寻求友谊和爱,这件事是不对的。


在这里给大家列出一个在职场上受欢迎的清单。


1.实力在及格线以上


这是一切的前提。职场新人要“先活下来,再做兄弟”,稳住了工作能力这个基本面,才有资格和同事谈交情。


实力不够的人会拖累整个团队、增加所有人的工作量,大家恨都来不及,绝对不会和他称兄道弟。


实力强可以表现为实力本身,在初级职位上,也可以表现为潜力。


极少数特别强大的人可能从一开始就能很好地完成工作,但是大部分人在新加入一个团队时都需要经过一段时间的磨合,在这个过程中有欠缺和不足都是正常的,你所表现出来的敬业精神、学习能力和进步的速度才是大家对你进行评价的关键。


刚入职的新人,对于要做的事情完全没有概念,但是为人极勤奋又上进,给他布置的任务会完成得特别扎实,每一天都在飞快地进步。这样的人在职场上永远都能收获一大把来自他人的橄榄枝。


2.比较高的自尊水平


高自尊的人对自己评价高,要求也高,又能够带着欣赏的眼光去看周围的人,他们不光是很好的父母、伴侣和朋友,同时也是职场上最好的结盟对象。


高自尊的人往往拥有很多优秀的品质,同时他们也能够理解“大局”,和他们合作不用在鸡毛蒜皮的细节上纠缠推诿,可以把精力全部用来开疆拓土,极大地降低团队的内耗。


如果你是一个高自尊的人,在日常生活中表现出了自律和很好的品行,就会收获高自尊同类的赞赏。有些低自尊的人可能会认为你的言行是在“装X”,别犹豫,把他们从你的结交名单当中划掉,高自尊会帮你筛掉一批最糟糕的潜在合作者。


如果你是一个部门的领导者,记得要维护高自尊的下属,他们都是潜在的优秀带队者,给他们一个位子就可以坐上来自己动,给他们一点精神鼓励和支持,他们就会变得无所不能。


即使高自尊的手下可能某些地方让你感到嫉妒或者冒犯(这是常见的,嫉妒是每个人都一定会有的情感),也绝对不要默许或者纵容低自尊的妄人跑去伤害他们,否则会伤了大家的心,事业就难以成功了。


“朕可以敲打丞相,但你算什么东西”就是对这种低自尊妄人最好的态度。


3.嘴严,可靠


在任何一个群体当中,多嘴多舌的人都不会受到尊重,而在职场上,嘴不严尤其危险。


如果你是一个爱说是非的人,围绕在你周围的只会是一帮同样没正事、低级趣味的家伙。你会被打上“不可靠”的标记,愿意和你交流的人越来越少,大家等着看你什么时候因为多嘴闯祸,而强者根本不会和你为伍。


有些同学曾经给我留言说,自己很内向,不知道如何跟同事拉近关系。内向的人最适合强调自己的“嘴严”和“可靠”,在职场上,这两项品质远比“能说会道”更让人喜欢。


4.随和,有分寸


体面的人不传闲话,也不会轻易对旁人发表议论。


“思想可以特立独行,生活方式最好随大流”,这是对自己的要求,而他人的生活方式是不是合理,不是我们能评价的。


哪怕是最亲近的人,都未必能知晓对方的全部经历和心里藏着的每一件小事。在职场上大家保持着客气有礼的距离,就更不可能了解每个人做事的出发点和逻辑,“看不懂”是正常的,但是完全没有必要“看不惯”。如果还要大发议论,把自己的“看不惯”到处传播,你的伙伴就只会越来越少。


有人说在北上广深这样的大城市,人和人之间距离遥远,缺人情味,太冷漠。


这不是冷漠,而是对“和自己不一样”的宽容,这份宽容就是我们在向文明社会靠拢的标志。


5.懂得如何打扮


还记得斯大林的故事吗?在他离开校园之后,从头到脚都经过精心设计,不是为了精神好看,而是要让自己看起来就像一位投身革命事业的进步青年。


有句老话叫做“先敬罗衣后敬人”,本意是讽刺那些根据衣饰打扮来评价一个人的现象。我们自己在做判断的时候要尽量避免受到这类偏见的影响,但是对他人可能存在的偏见一定要心中有数。人是视觉动物,穿着打扮是“人设(人物设定)”的一部分,在我们开口说话之前,衣饰鞋袜就已经传达了无数信息。


想要成为职场当中受欢迎的人,穿着打扮的风格就要和公司的调性保持一致,最安全的做法是向你的同事靠拢。


在一个风格统一的群体当中,“与众不同”这件事自带攻击性。如果在事业单位之类的上年纪同事比较多的地方上班,马卡龙色的衣服和颜色夸张的口红,最好等到下班时间再上身。


这不是压抑天性,而是自我保护和职业精神。


6.和优秀的人站在一起


在职场上,优秀的人品质都是相似的:勤奋,自律,不断精进。如果发现了这样的同事,就要尽量和他们保持良好关系。


但是,单纯的日常沟通并不足以让你们成为盟友,正式结盟往往是通过利益交换和分享:当你遇到棘手的工作任务,就可以主动邀请对方共同跟进,同时将一部分利益让出去。愉快的合作是关系飞跃的最好契机。


优秀的人能认可的,通常也都是自己的同类。如果你能获得他们的称许和背书,在同事当中的地位自然会有所提升。


7.知道如何求助


前两天有一位关系户同学留言说,自己即将去实习,因为家人的关系可以得到一些行业资深专家的指点,问自己应该如何表现,是不是不懂就要问,像“好奇宝宝”一样,对方就会觉得自己好学上进。


我告诉她说,不要上去就问,有任何疑惑都先用搜索引擎找一下答案,如果找不出来,再带着你搜到的细节去询问那些资深前辈。


互联网时代有个很大的变化,就是人们获取信息的成本大大降低。善用搜索引擎寻找答案,就能更快、更精准、更全面地找到自己想要的东西,这种方式比跑到对方工位边用嘴问效率高得多。


凡事都问,只会让人觉得你的文字阅读能力有限,同时既不把自己的时间当回事,也不尊重别人的时间。尤其对方还是行业中的专家,他们的时间一定比实习生的宝贵多了。如果网上找不到答案,再带着细节去仔细咨询,这样的请教才是高效的,才能证明你是一个“好学上进”的人。


职场不是校园,不会再有一群老师专门负责手把手地教你,不轻易占用其他同事的时间会让你成为一个自立、有分寸、受尊重的人。毕业之后,你取得进步的速度、最终的上升空间,都和使用搜索引擎寻找答案的能力呈正相关。


8.技巧地送出小恩小惠


小恩小惠带两个“小”字,并不意味着这是一种微末小技。事实上,即使是最普通的零食,只要讲究得法,都可以送到人心里。


你的同事当中有没有因为宗教信仰而忌口的情况?


甲和乙爱吃辣,丙和丁爱吃甜,是不是两种口味都来上一点?


要留心同事的自我暴露,最好是用一个小本本记下来,关键时刻可能派上大用场。大家都是成年人,不会像孩子一样轻易被小恩小惠打动,打动我们的往往是“你把我放在心上”的温暖。


9.良好的情绪管理能力


很多时候这是个隐藏特征,但是自带“一票否决”属性:平时表现得沉着稳重,周围同事们不会有特别明显的感觉,然而歇斯底里和失控只要有一次,之前苦心经营的人设就会全面崩塌。情绪不稳定的人一般没人敢惹,但是也没人会在意了:你会被视为一个“病人”,很难再有大的发展。


已经发泄出去的情绪不能收回来,这个时候不要反复陷入纠结和悔恨,待在情绪里不出来,钱花出去了就不要去想,不要去比价。


如果情绪失控了,应该立刻做到的是原谅自己,然后考虑如何不再有下一次失控。要知道大多数人一辈子都至少会换三四次工作,了不起是换个地方,重新再来。


有的人特别幸运,天生长得好看,容易被人喜欢。


如果不是让人眼前一亮的高颜值人士,就不要太心急了。


成为一个自律、行为可以预期的人,也能慢慢地被别人喜欢。


人生很长,被人喜欢这件事,我们不用赶时间。


作者:程序员小高
来源:juejin.cn/post/7255589558996992059
收起阅读 »

假如互联网人都很懂冒犯

大家好,我是老三,最近沉迷于听脱口秀,并且疯狂安利同事。 脱口秀演员常常说的一句话是:“脱口秀是冒犯的艺术”。最近我发现,同事们好像有点不一样了。 阳光灿烂的早上,趿拉着我的宝马拖鞋,跨上包浆的小黄车,屁股感受着阳光积累的炙热,往公司飞驰而去。 一步跨进电梯...
继续阅读 »

大家好,我是老三,最近沉迷于听脱口秀,并且疯狂安利同事。


脱口秀演员常常说的一句话是:“脱口秀是冒犯的艺术”。最近我发现,同事们好像有点不一样了。




阳光灿烂的早上,趿拉着我的宝马拖鞋,跨上包浆的小黄车,屁股感受着阳光积累的炙热,往公司飞驰而去。


一步跨进电梯间,我擦汗的动作凝固住了,挂上了矜持的微笑:“老板,早上好。”


老板:“早,你还在呢?又来带薪划水了?”


我:“嗨,我这再努力,最后不也就让你给我们多换几个嫂子嘛。”


老板:“没有哈哈,我开玩笑。”


我:“我也是,哈哈哈。”


今天的电梯似乎比往常慢了很多。


我:“老板最近在忙什么?”


老板:“昨天参加了一个峰会,马xx知道吧?他就坐我前边。”


我:“卧槽,真能装。没有,哈哈。”


老板:“哈哈哈”。


电梯到了,我俩都步履匆匆地进了公司。


小组内每天早上都有一个晨会,汇报工作进度和计划。


开了一会,转着椅子,划着朋友圈的我停了下来——到我了。


我:“昨天主要……今天计划……”


Leader:“你这不能说没有一点产出,也可以说一点产出都没有。其实,我对你是有一些失望的,原本今年绩效考评给你一个……”


我:“影响你合周报了是吗?不是哈哈。”


Leader、小组同事:“哈哈哈“。


Leader:“好了,我们这次顺便来对齐一下双月OKR,你们OKR都写的太保守了,一看就是能完成的,往大里吹啊。开玩笑哈哈。”。


我:”我以前就耕一亩田,现在把整个河北平原都给犁了。不是,哈哈。”


同事:“我要带公司打上月球,把你踢下来,我来当话事人。唉,哈哈”


Leader、同事、我:“哈哈哈“。


晨会开完,开始工作,产品经理拉我和和前端对需求。


产品经理:“你们程序员懂Java语言、Python语言、Go语言,就是不懂汉语言,真不想跟你们对需求。开个玩笑,哈哈。”


我:“没啥,你吹牛皮像狼,催进度像狗,做需求像羊,就这需求文档,还没擦屁股纸字多,没啥好对的。不是哈哈。”


产品经理、前端、我:“哈哈哈”。


产品经理:“那我们就对到这了,你们接着聊技术实现。”


前端:“没啥好聊的,后端大哥看着写吧,反正你们那破接口,套的比裹脚布还厚,没事还老出BUG。没有哈哈。”


我:“还不是为了兼容你们,一点动脑子的逻辑都不写,天天切图当然不出错。不是哈哈。”


前端、我:“哈哈哈”。


经过一番拉扯之后,我终于开始写代码了。


看到一段代码,我皱起了眉头,同事写的,我顺手写下了这样一段注释:


/**
* 写这段代码的人,建议在脑袋开个口,把水倒掉。不是哈哈,开个玩笑。
**/


代码写完了,准备上线,找同事给我Review,同事看了一会,给出了评论。



又在背着我们偷偷写烂代码了,建议改行。没有哈哈。



同事、我:“哈哈哈”。


终于下班了,路过门口,HR小姐姐还在加班。


我:“小姐姐怎么还没下班?别装了,老板都走了。开玩笑哈哈。”


HR小姐姐:“这不是看看怎么优化你们嘛,任务比较重。不是,哈哈。”


HR小姐姐、我:“哈哈哈”。


我感觉到一种不一样的氛围在公司慢慢弥散开来,我不知道怎么形容,但我想到了一句话——


“既分高下,也决生死”。




写这篇的时候,想到两年前,有个叫码农小说家的作者横空出世,写了一些生动活泼、灵气十足的段子,我也跟风写了两篇,这就是“荒腔走板”系列的来源。


后来,他结婚了。


看(抄)不到的我只能自己想,想破头也写不不来像样的段子,这个系列就不了了之,今天又偶尔来了灵感,写下一篇,也顺带缅怀一下光哥带来的快乐。


作者:三分恶
来源:juejin.cn/post/7259036373579350077
收起阅读 »

为什么网站要使用HTTPS?

现在HTTPS基本上已经是网站的标配了,很少会遇到单纯使用HTTP的网站。但是十年前这还是另一番景象,当时只有几家大型互联网公司的网站会使用HTTPS,大部分使用的都还是简单的HTTP,这一切是怎么发生的呢?为什么要把网站升级到HTTPS?若干年前,公司开发了...
继续阅读 »

现在HTTPS基本上已经是网站的标配了,很少会遇到单纯使用HTTP的网站。但是十年前这还是另一番景象,当时只有几家大型互联网公司的网站会使用HTTPS,大部分使用的都还是简单的HTTP,这一切是怎么发生的呢?

为什么要把网站升级到HTTPS?

若干年前,公司开发了一款APP,其中的某些页面是用H5实现的,有一天用户向我们反馈,页面中弹出了一个广告窗口,这让当时身为开发小白的我感觉很懵逼,后来经过经验丰富的老程序员点拨,才知道这是被电信运营商劫持了,运营商拦截了服务器对用户的HTTP响应,并在中间夹带了一些私货。

一些网龄比较大的同学可能还有这样的记忆:网站页面找不到的时候,浏览器会跳转到一个运营商或者路由器厂商的网址导航页面;家里的宽带到期的时候,浏览器网页右下角会弹出续费通知。

这都是HTTP响应被劫持的表现,HTTP本身没什么安全机制,HTTP传输的数据很容易被窃取和篡改,这也是我们将网站升级到HTTPS的根本动机。

使用HTTPS有很多好处,这里稍微展开介绍一下:

  • 数据加密:HTTPS通过SSL/TLS协议为数据传输过程提供了加密,即便数据在传输过程中被截获,没有密钥也无法解读数据内容。这就像是特工使用密文发送电报,即使电报内容被别人截获,没有密码表也无法解读其中的内容。
  • 身份验证:使用HTTPS的网站会获得权威认证机构颁发的证书,这就像是一个“身-份-证”,让访问者能够确认自己访问的是官方合法的网站,有效防止钓鱼网站的风险。
  • 数据完整性:因为数据传输的中间人接触不到密钥,不仅不能解密,而且也无法对数据进行加密,这就保证了数据在传输过程中不被篡改、伪造。
  • 增强用户信任:由于浏览器会对HTTPS网站显示锁标志,这有助于增强访问者对网站的信任。就像是看到家门口安装了高级安全锁,人们会自然而然地觉得这家人对安全非常重视,从而更加放心。
  • SEO优势:谷歌等搜索引擎已经明确表示,HTTPS是搜索排名算法的一个信号。这意味着使用HTTPS的网站在搜索结果中可能会获得更高的排名,具备更大的竞争优势。

HTTPS的发展趋势

大约从2010年开始,大型网站和安全专家开始倡导使用HTTPS,也就是在HTTP上加上SSL/TLS协议进行加密。

根据互联网安全研究机构的报告,目前超过80%的网站已经使用HTTPS。特别是那些大型电商平台和社交媒体网站,几乎100%都已经完成了从HTTP到HTTPS的升级。

不仅是企业和网站管理员在推动HTTPS的普及,各国政府和互联网安全组织也在积极推荐使用HTTPS。例如,各种浏览器都会对那些仍然使用HTTP的网站标记为“不安全”。

随着人们对网络安全意识的增强,大家也更加偏好那些使用HTTPS的网站。就像是在选择酒店的时候,你可能会更倾向于选择那些看起来保卫严密的酒店。

HTTPS的技术原理

加密技术

HTTPS 安全通信的核心在于加密技术。这里面主要涉及两种加密方式:对称加密和非对称加密。

  • 对称加密:就像是你和朋友使用同一把钥匙来锁和解一个箱子。信息的发送方和接收方使用同一个密钥进行数据的加密和解密。这种方式的优点是加解密速度快,通信成本低,但缺点在于如果密钥被中间截获或者泄漏,通信就不安全了。
  • 非对称加密:就像是用一个钥匙锁箱子(公钥),另一个钥匙来开箱子(私钥)。发送方使用接收方的公钥进行加密,而只有接收方的私钥能解开。这样即便公钥被公开,没有私钥也无法解密信息,从而保证了传输数据的安全。

在实际应用中,HTTPS 通常采用混合加密机制。在连接建立初期使用非对称加密交换对称加密的密钥,一旦密钥交换完成,之后的通信就切换到效率更高的对称加密。就像是先通过一个安全的箱子(非对称加密)把家门钥匙(对称加密的密钥)安全送到朋友手中,之后就可以放心地使用这把钥匙进行通信了。

SSL/TLS协议

HTTPS 实际上是 HTTP 协议跑在 TLS 协议之上,TLS的全称是 Transport Layer Security,从字面上理解就是传输层安全,保护数据传输的安全。有时候我们还会看到 SSL 这个词,SSL 其实是 TLS 的前身,它的全称是 Secure Sockets Layer,Socket 就是是TCP/UDP编程中经常接触的套接字概念,也是传输层的一个组件。

可以理解为,SSL/TLS就像是一个提供安全保护的信封,确保了信件(数据)在寄送过程中的安全。

让我们来详细探查下 HTTPS 的工作流程:

1、开始握手:当浏览器尝试与服务器建立HTTPS连接时,它首先会发送一个“Hello”消息给服务器,这个消息里包含了浏览器支持的加密方法(包括对称加密和非对称加密等)等信息。

2、服务器回应:服务器收到客户端的“Hello”之后,会选择一组客户端和服务器都支持的加密方法,然后用自己的私钥对信息进行签名,把这个签名连同服务器的SSL证书一起发送到客户端,SSL证书里包含了服务器的公钥。

3、验证证书:客户端收到服务器发过来的证书后,会首先验证证书的合法性,确保证书是可信任的CA颁发,且未被篡改。这个验证会使用浏览器或者操作系统内置的安全根证书,验证从服务器证书到根证书的所有认证链上的签名都是可信任的。

4、生成临时密钥:一旦证书验证通过,客户端就会生成一串随机密钥(也就是对称密钥)。然后,客户端会用服务器的公钥对这串随机密钥进行加密,再发送给服务器。

5、服务器解密获取对称密钥:服务器收到加密后的数据,会用自己的私钥对其解密,获取到其中的对称密钥。到这里,客户端和服务器双方就都拥有了这个对称密钥,后续的通信就可以使用这个对称密钥进行加密了。

这里我们介绍的密钥交换方式是RSA,其实TLS支持多种密钥交换机制,除了RSA,还包括Diffie-Hellman密钥交换(简称DH)、椭圆曲线Diffie-Hellman(简称ECDH)密钥交换等,或者RSA和DH的结合。DH密钥交换不需要在通信双方之间直接发送对称密钥,同时即使证书的私钥被泄露,之前的会话密钥也不能被推导出来,之前的通信也就无法被解密,这样更加安全。有兴趣的同学可以去搜索了解一下。

证书和认证机构(CA)

为了保证网站的身份真实性,HTTPS还涉及到了证书(SSL证书)的使用。这个证书由认证机构(CA)颁发,包含了网站公钥、网站身份信息等。浏览器或操作系统内置了这些认证机构的信任列表,能自动验证证书的真实性。

证书认证机构会在颁发证书前确认网站的身份,这有点像买火车票之前,需要先通过身份认证来确认你的身份。根据验证的深度和范围,证书可以分为以下几种类型:

  1. 域名验证(DV)证书

这种证书只验证网站拥有者对域名的控制权。CA会通过Url文件验证或DNS记录验证等方式来确认申请者是否控制该域名。DV证书的发放速度快,成本低,但它只证明域名的控制权,不会验证组织的真实身份。

  1. 组织验证(OV)证书

OV证书不仅验证域名的控制权,还要验证申请证书的组织是真实、合法且正式注册的。这就像提交某些申请时,除了要上传身-份-证,还要上传企业的营业执照,确认你是某个公司的员工。OV证书提供了更高级别的信任,适用于商业网站。

  1. 扩展验证(EV)证书

EV证书提供了最高级别的验证。在这个过程中,CA会进行更为严格和全面的审查,包括确认申请组织的法律、运营和物理存在。这就像不仅检查身-份-证和营业执照,还要确认你的实际居住地址、实际办公地点等信息。EV证书为用户提供了最高水平的信任,但它的发放流程最为复杂,成本也最高。

配置HTTPS的步骤

1. 获取SSL/TLS证书

可以从阿里云、腾讯云等这些大的云计算平台申请你需要的证书,也可以从专门的证书颁发机构获取。

证书可以只针对单个域名,比如www.juejin.cn,那只能 http://www.juejin.cn 使用这个证书,www2.juejin.cn 不能使用这个证书;也可以配置为泛域名,比如 *.juejin.cn,那么 http://www.juejin.cn 和 www2.juejin.cn 都可以使用这个证书。

申请证书时会验证你的身份,比如对于DV证书,需要你在DNS中配置一个特殊TXT解析、或者在网站中放置一个特别的验证文件,证书颁发机构能够通过网络进行验证。

验证通过后,证书颁发结构会给你发放证书,包括公钥和私钥。

证书有免费版和收费版。免费版一般只针对单个域名,仅颁发DV证书,证书的有效期一般是3-12个月。普通用户为了节约成本,可以使用免费版本,通过一些程序脚本实现证书的到期自动更新。

2. 配置Web服务器

拿到证书后,需要在你的Web服务器上配置它,具体步骤取决于你使用的服务器软件(如Apache、Nginx等)。

注意HTTPS默认的监听端口是443,使用这个端口,用户访问时可以不输入端口号。

3. 强制使用HTTPS

为了确保所有数据都是安全传输的,我们可以使用重定向让用户始终访问HTTPS地址。

在Web服务器上设置,将所有HTTP请求重定向到HTTPS,用户使用HTTP时都会自动跳转到HTTPS,比如访问 juejin.cn 会自动跳转到 juejin.cn。

4. 维护和更新

证书都是有保质期的,需要在证书到期前进行续期。有时候我们还需要根据安全威胁报告,及时更新SSL/TLS的加密设置,确保它们符合最新的安全标准。

HTTPS的安全问题

HTTPS虽然大大提高了网站的安全性,但它也不是万无一失的。

1、弱加密算法

如果使用过时或不安全的加密算法,加密的数据可能会被破解。

在Web服务器配置中禁用已知不安全的SSL/TLS版本(如TLS 1.0和1.1)和弱加密套件,选择使用强加密算法,如AES_GCM或ChaCha20。

2、钓鱼网站

即使是使用HTTPS的网站,也可能是钓鱼网站,比如DV证书只验证网站的域名归属,不确认网站具体是干什么的。这就像强盗穿上快递员的制服,你很难一眼识破。

对于关键的服务,比如在线购物、上传个人信息,用户需要提高警惕,检查网站的URL,确保是访问的正确网站。

我们也可以使用浏览器提供的安全插件或服务来识别和阻止访问已知的恶意网站。

3、中间人攻击

即使使用了HTTPS,如果攻击者能够在通信双方之间插入自己,就能够监听、修改传输的数据。如果你使用过Fiddler 这种抓包程序做过前端通信调试,就很容易理解这个问题。这就像快递途中有个假冒的收发室,所有包裹都得先经过它。

要防范这个问题比较困难,用户尽量不要在公共的WiFi网络进行敏感操作,不随便下载安装可疑的文件或程序,网站运营者要确保网站的TLS配置是安全的,使用强加密算法和协议。

4、审核不严的证书

证书颁发机构审核不严或者胡乱颁发证书,比如别有用心的人通过特殊手段就能申请到google.com的证书。而且历史上也确实发生过。

2011年,荷兰证书颁发机构(CA)DigiNotar因被黑客入侵并滥发了大量伪造的SSL/TLS证书,包括对Google域名的证书,最终导致DigiNotar破产。

2016年,中国CA机构WoSign及旗下子公司StartCom被曝出多种违规操作,导致主流浏览器厂商逐步撤销对这两家CA的信任。

解决这个问题主要依赖证书颁发机构和监管机构的安全机制,浏览器和操作系统厂商也可以在问题发生后通过紧急更新来避免风险的进一步扩大,使用证书的用户如果有能力,可以通过监控CA机构发布的证书颁发日志来探查是否有未经授权的证书颁发给你的域名。


以上就是本文的主要内容,希望此文能让你对Https有了一个系统全面的了解,更好的保护Http通信安全。


作者:萤火架构
来源:juejin.cn/post/7366053684154777626
收起阅读 »

关于我裁员在家没事接了个私单这件事...

起因 2024年3月31日,我被公司裁员了。 2024年4月1日,果断踏上了回家的路,决定先休息一个星期。晚上回到了郑州,先跟一起被裁的同事在郑州小聚一下,聊聊后面的打算。第二天下午回家。 2024年4月8日,知道现在的大环境不好,不敢错过“金三银四”,赶忙回...
继续阅读 »

起因


2024年3月31日,我被公司裁员了。


2024年4月1日,果断踏上了回家的路,决定先休息一个星期。晚上回到了郑州,先跟一起被裁的同事在郑州小聚一下,聊聊后面的打算。第二天下午回家。


2024年4月8日,知道现在的大环境不好,不敢错过“金三银四”,赶忙回上海开始找工作。结果环境比预想的还要差啊,以前简历放开就有人找,现在每天投个几十封都是石沉大海。。。


2024年4月15日,有个好朋友找我,想让我给他们公司开发一个“拨号APP”(主要原因其实是这个好哥们想让我多个赚钱门路😌),主要的功能就是在他们的系统上点击一个“拨号”按钮,然后员工的工作手机上就自动拨打这个号码。


可行性分析


涉及到的修改:



  • 系统前后端

  • 拨号功能的APP


拿到这个需求之后,我并没有直接拒绝或者同意,而是先让他把他公司那边的的源代码发我了一份,大致看了一下使用的框架,然后找一些后端的朋友看有没人有人一起接这个单子;而我自己则是要先看下能否实现APP的功能(因为我以前从来没有做过APP!!!)。


我们各自看过自己的东西,然后又沟通了一番简单的实现过程后达成了一致,搞!


因为我这边之前的技术栈一直是VUE,所以决定使用uni-app实现,主要还是因为它的上手难度会低很多。


第一版


需求分析


虽说主体的功能是拨号,但其实是隐含很多辅助性需求的,比如拨号日志、通时通次统计、通话录音、录音上传、后台运行,另外除了这些外还有额外的例如权限校验、权限引导、获取手机号、获取拨号状态等功能需要实现。


但是第一次预算给的并不高,要把这些全部实现显然不可能。因此只能简化实现功能实现。



  • 拨号APP

    • 权限校验

      • 实现部分(拨号、录音、文件读写)



    • ❌权限引导

    • 查询当前手机号

      • 直接使用input表单,由用户输入



    • 查询当前手机号的拨号任务

      • 因为后端没有socket,使用setTimeout模拟轮询实现。



    • 拨号、录音、监测拨号状态

      • 根据官网API和一些安卓原生实现



    • 更新任务状态

      • 告诉后端拨号完成



    • ❌通话录音上传

    • ❌通话日志上传

    • ❌本地通时通次统计

    • 程序运行日志

    • 其他

      • 增加开始工作、开启录音的状态切换

      • 兼容性,只兼容安卓手机即可






基础设计


一个input框来输入用户手机号,一个开始工作的switch,一个开启录音的切换。用户输入手机号,点击开始工作后开启轮询,轮询到拨号任务后就拨号同时录音,同时监听拨号状态,当挂断后结束录音、更新任务状态,并开启新一轮的轮询。


开干


虽然本人从未开发过APP,但本着撸起袖子就是干的原则,直接打开了uni-app的官网就准备开怼。


1、下载 HbuilderX。


2、新建项目,直接选择了默认模板。


3、清空 Hello页面,修改文件名,配置路由。


4、在vue文件里写主要的功能实现,并增加 Http.jsRecord.jsPhoneCall.jsPower.js来实现对应的模块功能。


⚠️关于测试和打包


运行测试


在 HbuilderX 中点击“运行-运行到手机或模拟器-运行到Android APP基座”会打开一个界面,让你选择运行到那个设备。这是你有两种选择:



  • 把你手机通过USB与电脑连接,然后刷新列表就可以直接运行了。

    • 很遗憾,可能是苹果电脑与安卓手机的原因,插上后检测不出设备😭。。。



  • 安装Android Studio,然后通过运行内置的模拟器来供代码运行测试。

    • 这种很麻烦,要下载很久,且感觉测试效果并不好,最好还是用windows电脑连接手机的方法测试。




关于自定义基座和标准基座的差别,如果你没有买插件的话,直接使用基准插座就好。如果你要使用自定义基座,就首先要点击上图中的制作自定义基座,然后再切换到自定义基座执行。


但是不知道为什么,我这里一直显示安装自定义基座失败。。。


打包测试


除了以上运行测试的方法外,你还有一种更粗暴的测试方法,那就是打包成APP直接在手机上安装测试。


点击“发行-原生APP 云打包”,会生成一个APK文件,然后就可以发送到手机上安装测试。不过每天打包的次数有限,超过次数需要购买额外的打包服务或者等第二天打包。


我最终就是这样搞得,真的我哭死,我可能就是盲调的命,好多项目都是盲调的。


另外,在打包之前我们首先要配置manifest.json,里面包含了APP的很多信息。比较重要的一个是AppId,一个是App权限配置。参考uni-app 权限配置Android官方权限常量文档。以下是拨号所需的一些权限:



// 录制音频
<uses-permission android:name="android.permission.RECORD_AUDIO" />
// 修改音频设置
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />

// 照相机
<uses-permission android:name="android.permission.CAMERA" />
// 写入外部存储
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
// 读取外部存储
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

// 读取电话号码
<uses-permission android:name="android.permission.READ_PHONE_NUMBERS" />
// 拨打电话
<uses-permission android:name="android.permission.CALL_PHONE" />
// 呼叫特权
<uses-permission android:name="android.permission.CALL_PRIVILEGED" />
// 通话状态
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
// 读取拨号日志
<uses-permission android:name="android.permission.READ_CALL_LOG" />
// 写入拨号日志
<uses-permission android:name="android.permission.WRITE_CALL_LOG" />
// 读取联系人
<uses-permission android:name="android.permission.READ_CONTACTS" />
// 写入联系人
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
// 读取SMS?
<uses-permission android:name="android.permission.READ_SMS" />

// 写入设置
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
// 唤醒锁定?
<uses-permission android:name="android.permission.WAKE_LOCK" />
// 系统告警窗口?
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
// 接受完整的引导?
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

⚠️权限配置这个搞了很长时间,即便现在有些权限还是不太清楚,也不知道是不是有哪些权限没有配置上。。。


⚠️权限校验


1、安卓 1


好像除了这样的写法还可以写"scope.record"或者permission.CALL_PHONE


permision.requestAndroidPermission("android.permission.CALL_PHONE").then(res => {
// 1 获得权限 2 本次拒绝 -1 永久拒绝
});

2、安卓 2


plus.android.requestPermissions(["android.permission.CALL_PHONE"], e => {
// e.granted 获得权限
// e.deniedPresent 本次拒绝
// e.deniedAlways 永久拒绝
});

3、uni-app


这个我没测试,AI给的,我没有用这种方法。有一说一,百度AI做的不咋地。


// 检查权限
uni.hasPermission({
permission: 'makePhoneCall',
success() {
console.log('已经获得拨号权限');
},
fail() {
// 示例:请求权限
uni.authorize({
scope: 'scope.makePhoneCall',
success() {
console.log('已经获得授权');
},
fail() {
console.log('用户拒绝授权');
// 引导用户到设置中开启权限
uni.showModal({
title: '提示',
content: '请在系统设置中打开拨号权限',
success: function(res) {
if (res.confirm) {
// 引导用户到设置页
uni.openSetting();
}
}
});
}
});
}
});

✅拨号


三种方法都可以实现拨号功能,只要有权限,之所以找了三种是为了实现APP在后台的情况下拨号的目的,做了N多测试,甚至到后面搞了一份原生插件的代码不过插件加载当时没搞懂就放弃了,不过到后面才发现原来后台拨号出现问题的原因不在这里,,具体原因看后面。


另外获取当前设备平台可以使用let platform = uni.getSystemInfoSync().platform;,我这里只需要兼容固定机型。


1、uni-app API


uni.makePhoneCall({
phoneNumber: phone,
success: () => {
log(`成功拨打电话${phone}`);
},
fail: (err) => {
log(`拨打电话失败! ${err}`);
}
});

2、Android


plus.device.dial(phone, false);

3、Android 原生


写这个的时候有个小插曲,当时已经凌晨了,再加上我没有复制,是一个个单词敲的,结果竟然敲错了一个单词,测了好几遍都没有成功。。。还在想到底哪里错了,后来核对一遍才发现😭,control cv才是王道啊。


// Android
function PhoneCallAndroid(phone) {
if (!plus || !plus.android) return;
// 导入Activity、Intent类
var Intent = plus.android.importClass("android.content.Intent");
var Uri = plus.android.importClass("android.net.Uri");
// 获取主Activity对象的实例
var main = plus.android.runtimeMainActivity();
// 创建Intent
var uri = Uri.parse("tel:" + phone); // 这里可修改电话号码
var call = new Intent("android.intent.action.CALL", uri);
// 调用startActivity方法拨打电话
main.startActivity(call);
}

✅拨号状态查询


第一版用的就是这个获取状态的代码,有三种状态。第二版的时候又换了一种,因为要增加呼入、呼出、挂断、未接等状态的判断。


export function getCallStatus(callback) {
if (!plus || !plus.android) return;
let maintest = plus.android.runtimeMainActivity();
let Contexttest = plus.android.importClass("android.content.Context");
let telephonyManager = plus.android.importClass("android.telephony.TelephonyManager");
let telManager = plus.android.runtimeMainActivity().getSystemService(Contexttest.TELEPHONY_SERVICE);
let receiver = plus.android.implements('io.dcloud.android.content.BroadcastReceiver', {
onReceive: (Contexttest, intent) => {
plus.android.importClass(intent);
let phoneStatus = telManager.getCallState();
callback && callback(phoneStatus);
//电话状态 0->空闲状态 1->振铃状态 2->通话存在
}
});
let IntentFilter = plus.android.importClass("android.content.IntentFilter");
let filter = new IntentFilter();
filter.addAction(telephonyManager.ACTION_PHONE_STATE_CHANGED);
maintest.registerReceiver(receiver, filter);
}

⚠️录音


录音功能这个其实没啥,都是官网的API,无非是简单的处理一些东西。但是这里有一个大坑!


一坑


就是像通话录音这种涉及到的隐私权限很高,正常的这种录音是在通话过程中是不被允许的。


二坑


后来一次偶然的想法,在接通之后再开启录音,发现就可以录音了。


但随之而来的是第二个坑,那就是虽然录音了,但是当播放的时候发现没有任何的声音,还是因为保护隐私的原因,我当时还脱离代码专门试了试手机自带的录音器来在通话时录音,发现也不行。由此也发现uni的录音本身也是用的手机录音器的功能。


三坑


虽然没有声音,但是我还是试了下保存,然后就发现了第三个坑,那就是虽然获取了文件权限,但是现在手机给的读写权限都是在限定内的,录音所在的文件夹是无权访问的。。。


另辟蹊径


其实除了自己手动录音外还可以通过手机自带的通话录音来实现,然后只要手机去读取录音文件并找到对应的那个就可以了。思路是没啥问题,不过因为设置通话录音指引、获取录音文件都有问题,这一版本就没实现。


// 录音

var log = console.log,
recorder = null,
// innerAudioContext = null,
isRecording = false;

export function startRecording(logFun = console.log) {
if (!uni.getRecorderManager || !uni.getRecorderManager()) return logFun('不支持录音!');
log = logFun;
recorder = uni.getRecorderManager();
// innerAudioContext = uni.createInnerAudioContext();
// innerAudioContext.autoplay = true;
recorder.onStart(() => {
isRecording = true;
log(`录音已开始 ${new Date()}`);
});
recorder.onError((err) => {
log(`录音出错:${err}`);
console.log("录音出错:", err);
});
recorder.onInterruptionBegin(() => {
log(`检测到录音被来电中断...`);
});
recorder.onPause(() => {
log(`检测到录音被来电中断后尝试启动录音..`);
recorder.start({
duration: 10 * 60 * 1000,
});
});
recorder.start({
duration: 10 * 60 * 1000,
});
}

export function stopRecording() {
if (!recorder) return
recorder.onStop((res) => {
isRecording = false;
log(`录音已停止! ${new Date()}`); // :${res.tempFilePath}
// 处理录制的音频文件(例如,保存或上传)
// powerCheckSaveRecord(res.tempFilePath);
saveRecording(res.tempFilePath);
});
recorder.stop();
}

export function saveRecording(filePath) {
// 使用uni.saveFile API保存录音文件
log('开始保存录音文件');
uni.saveFile({
tempFilePath: filePath,
success(res) {
// 保存成功后,res.savedFilePath 为保存后的文件路径
log(`录音保存成功:${res.savedFilePath}`);
// 可以将res.savedFilePath保存到你的数据中,或者执行其他保存相关的操作
},
fail(err) {
log(`录音保存失败! ${err}`);
console.error("录音保存失败:", err);
},
});
}

运行日志


为了更好的测试,也为了能实时的看到执行的过程,需要一个日志,我这里就直接渲染了一个倒序的数组,数组中的每一项就是各个函数push的字符串输出。简单处理。。。。嘛。


联调、测试、交工


搞到最后,大概就交了个这么玩意,不过也没有办法,一是自己确实不熟悉APP开发,二是满共就给了两天的工时,中间做了大量的测试代码的工作,时间确实有点紧了。所幸最起码的功能没啥问题,也算是交付了。


image.png


第二版


2024年05月7日,老哥又找上我了,想让我们把他们的这套东西再给友商部署一套,顺便把这个APP再改一改,增加上通时通次的统计功能。同时也是谈合作,如果后面有其他的友商想用这套系统,他来谈,我们来实施,达成一个长期合作关系。


我仔细想了想,觉得这是个机会,这块东西的市场需求也一直有,且自己现在失业在家也有时间,就想着把这个简单的功能打磨成一个像样的产品。也算是做一次尝试。


需求分析



  • ✅拨号APP

    • 登录

      • uni-id实现



    • 权限校验

      • 拨号权限、文件权限、自带通话录音配置



    • 权限引导

      • 文件权限引导

      • 通话录音配置引导

      • 获取手机号权限配置引导

      • 后台运行权限配置引导

      • 当前兼容机型说明



    • 拨号

      • 获取手机号

        • 是否双卡校验

        • 直接读取手机卡槽中的手机号码

        • 如果用户不会设置权限兼容直接input框输入



      • 拨号

      • 全局拨号状态监控注册、取消

        • 支持呼入、呼出、通话中、来电未接或挂断、去电未接或挂断





    • 录音

      • 读取录音文件列表

        • 支持全部或按时间查询



      • 播放录音

      • ❌上传录音文件到云端



    • 通时通次统计

      • 云端数据根据上面状态监控获取并上传

        • 云端另写一套页面



      • 本地数据读取本机的通话日志并整理统计

        • 支持按时间查询

        • 支持呼入、呼出、总计的通话次数、通话时间、接通率、有效率等





    • 其他

      • 优化日志显示形式

        • 封装了一个类似聊天框的组件,支持字符串、Html、插槽三种显示模式

        • 在上个组件的基础上实现权限校验和权限引导

        • 在上两个组件的基础上实现主页面逻辑功能



      • 增加了拨号测试、远端连接测试

      • 修改了APP名称和图标

      • 打包时增加了自有证书






中间遇到并解决的一些问题


关于框架模板


这次重构我使用了uni中uni-starter + uni-admin 项目模板。整体倒还没啥,这俩配合还挺好的,就只是刚开始不知道还要配置东西一直没有启动起来。


建立完项目之后还要进uniCloud/cloudfunctions/common/uni-config-center/uni-id配置一个JSON文件来约定用户系统的一些配置。


打包的时候也要在manifest.json将部分APP模块配置进去。


还搞了挺久的,半天才查出来。。


类聊天组件实现



  • 设计

    • 每个对话为一个无状态组件

    • 一个图标、一个名称、一个白底的展示区域、一个白色三角

    • 内容区域通过类型判断如何渲染

    • 根据前后两条数据时间差判断是否显示灰色时间



  • 参数

    • ID、名称、图标、时间、内容、内容类型等



  • 样式

    • 根据左边右边区分发送接收方,给与不同的类名

    • flex布局实现




样式实现这里,我才知道原来APP和H5的展示效果是完全不同的,个别地方需要写两套样式。


关于后台运行


这个是除了录音最让我头疼的问题了,我想了很多实现方案,也查询了很多相关的知识,但依旧没效果。总体来说有以下几种思路。



  • 通过寻找某个权限和引导(试图寻找到底是哪个权限控制的)

  • 通过不停的访问位置信息

  • 通过查找相应的插件、询问GPT、百度查询

  • 通过程序切入后台之后,在屏幕上留个悬浮框(参考游戏脚本的做法)

  • 通过切入后台后,发送消息实现(没测试)


测试了不知道多少遍,最终在一次无意中,终于发现了如何实现后台拨号,并且在之后看到后台情况下拨号状态异常,然后又查询了应用权限申请记录,也终于知道,归根到底能否后台运行还是权限的问题。


关于通话状态、通话记录中的类型


这个倒还好,就是测试的问题,知道了上面为啥异常的情况下,多做几次测试,就能知道对应的都是什么状态了。


通话状态:呼入振铃、通话中(呼入呼出)、通话挂断(呼入呼出)、来电未接或拒绝、去电未接或拒接。


通话日志:呼入、呼出、未接、语音邮件、拒接


交付


总体上来说还过得去,相比于上次简陋的东西,最起码有了一点APP的样子,基本上该有的功能也基本都已经实现了,美中不足的一点是下面的图标没有找到合适的替换,然后录音上传的功能暂未实现,不过这个也好实现了。


image.png


后面的计划



  • 把图标改好

  • 把录音文件是否已上传、录音上传功能做好

  • 把APP的关于页面加上,对接方法、使用方法和视频、问题咨询等等

  • 原本通话任务、通时通次这些是放在一个PHP后端的,对接较麻烦。要用云函数再实现一遍,然后对外暴露几个接口,这样任何一个系统都可以对接这个APP,而我也可以通过控制云空间的跨域配置来开放权限

  • 把数据留在这边之后,就可以再把uni-admin上加几个页面,并且绑定到阿里云的云函数前端网页托管上去

  • 如果有可能的话,上架应用商店,增加上一些广告或者换量联盟之类的东西

  • 后台运行时,屏幕上加个悬浮图标,来电时能显示个振铃啥的


大致的想法就这些了,如果这个产品能继续卖下去,我就会不断的完善它。


最后


现在的行情真的是不好啊,不知道有没有大哥给个内推的机会,本人大专计算专业、6.5年Vue经验(专精后台管理、监控大屏方向,其他新方向愿意尝试),多个0-1-2项目经验,跨多个领域如人员管理、项目管理、产品设计、软件测试、数据爬虫、NodeJS、流程规范等等方面均有了解,工作稳定不经常跳,求路过的大哥给个内推机会把!



作者:前端湫
来源:juejin.cn/post/7368421971384860684
收起阅读 »

最近得了一场病 差点要了我的命

最近得了一场病,前后持续了有十多天,时至今日感觉脑袋还是昏昏沉沉的不在状态,感觉像是药吃的,毕竟连着吃了十多天西药,可能人也吃傻了吧,中间还挂了五天水,算是补充了能量。 起因是和老婆去吃饭,可能是吃得太饱晚上着凉了,结果第二天下班回来就发烧,最开始是去社康看的...
继续阅读 »

最近得了一场病,前后持续了有十多天,时至今日感觉脑袋还是昏昏沉沉的不在状态,感觉像是药吃的,毕竟连着吃了十多天西药,可能人也吃傻了吧,中间还挂了五天水,算是补充了能量。


起因是和老婆去吃饭,可能是吃得太饱晚上着凉了,结果第二天下班回来就发烧,最开始是去社康看的,想着缴纳的社保也一直没用过,不知道怎么用,刚好借这个机会去试试。


图片


不知道西安的社保是什么样,深圳这边的分一二三档,一档可以去任意社康和医院看病买药,有独立账户;二档没有独立账户,且只能去绑定的社康看病,去医院的话需要先去社康开转诊,最新消息说现在不用了,而且也不能单独买药。


去了之后,先是几项检查,抽血化验,鼻腔测试,加起来一百多块吧,当时就纳闷,一个感冒发烧要搞得这么复杂吗,问医生说流程是这样的,要检查是什么原因引起的,看完报告说有点轻微病毒感染,再就是细菌感染引起的发烧,然后就开了点药。


图片


说下这里的报销比例吧,理论上最高能报到75%,个人能承担25%,二百块钱的医药费,报销完自己付了有五十多块钱吧。听说这是深圳这边今年十月份重新调整后的,之前是二档每年报销一千额度,单次报销比例高,个人承担费用少,调整后每人每年有两千多的免费额度,但是单次报销比例也降低了,意味着个人每次承担的也多了,当然如果你是长期去医院,也是比较划算的。


图片


再来看一下在社康开的药吧,一大盒口服液,里面是多支小瓶装;一盒头孢,之前总是听说,也是头一次吃,说是消炎的;还有一盒粉末状的东西,说是用温水冲服,后来发现是盐水,怎么说呢,总感觉有点开玩笑的意思吧。


图片


两天的药吃完之后呢,中间身体短暂好转了半天,再之后到晚上就又是继续发烧,最高烧到接近四十度,第二天量的时候已经达到39.5,且中间一直伴随着头疼,实在忍不了,整宿睡不着,早上六点多给领导发了消息请了假。


图片


这次是去楼下的私人诊所医治,女医生看了就说连着发烧这么多天现在吃药肯定来不及了,必须要打针,然后就开始输液了。小时候感觉输液是一件大动干戈的事,现在看来却是那么的平淡无奇,四小瓶水挂完之后浑身冒汗,温度也降了下来,头也没有那么疼了,中途老婆一直陪着我,还给我带了吃的,挂完之后医生同时又开了几天吃的药。


图片


第二天感觉轻松多了,为了巩固又去挂了一天,然后两天药也吃完了,接下来又产生了新的问题,不知道咋回事一直打嗝,连着一整天不停歇的那种打,刚开始以为是吃东西噎着了,可是无论怎么喝水憋气都无济于事,上班时坐在工位上自己嗝的都有点不好意思了,一直到晚上回家,没办法又去问了医生,说可能是胃痉挛,胃部引起的,开了两顿吃的药,很神奇的是刚吃下去没一会打嗝就停了,这一夜算是到这了,也睡了个好觉。


图片


接下来又是继续发烧,头疼,头晕,浑身无力,中午休息头疼的睡不着觉,下午手脚发麻,实在有些扛不住了,出去外面商场找了个沙发窝着睡了会,一直扛到下班,又去看医生,说是没好彻底,继续挂水,吃药,又是三天。这一路下来,真的是折磨人,让老婆也跟着前前后后来回折腾。其实在整个过程当中,发烧这些我感觉都可以忍,最难受的是头疼,偏头痛,那种神经痛,一刻不停的那种疼,真的很折磨人,让人崩溃。


图片


再往后又连着挂了三天水,吃了三天药,折腾了十多天好觉差不多了,接下来又残留着一点小问题,就是一直咳嗽停不下来,不是喉咙咳是从肺里面的那种咳,好觉应该问题不太大,过几天就好了,可是持续了几天还是一直在咳嗽,然后就又去看了医生,买了三天的药一百多块钱,问题是和前面开的药也一模一样没啥变化,感觉这边还是挺黑的,普普通通两三天药就是一百多,放在老家可能就几十块最多了,再加上前面打针输液的,总共花了一千多块吧,就是个普通的感冒发烧,稍微严重点。


图片


经过这次事件,有以下几点感触吧。无论什么时候,身体健康是第一位,所谓的工作,都是建立在你有个好的身体的前提下,身体状态良好,你才能更好的投入工作。第二,平时除了多加锻炼身体,还要注重身体按摩,长期坐在办公室不运动,颈椎难免有影响,没事多按摩活动下,也利用颈部头部血液循环,有助于偏头痛的缓解;再就是第三点,要好好吃饭,注重身体包养,好好对待自己的胃,很多东西都是吃出来的。


作者:编程迪
来源:juejin.cn/post/7306018817687765044
收起阅读 »

28个令人惊艳的JavaScript单行代码

web
JavaScript作为一种强大而灵活的脚本语言,充满了许多令人惊艳的特性。本文将带你探索28个令人惊艳的JavaScript单行代码,展示它们的神奇魅力。 1. 阶乘计算 使用递归函数计算给定数字的阶乘。 const factorial = n => ...
继续阅读 »

JavaScript作为一种强大而灵活的脚本语言,充满了许多令人惊艳的特性。本文将带你探索28个令人惊艳的JavaScript单行代码,展示它们的神奇魅力。


1. 阶乘计算


使用递归函数计算给定数字的阶乘。


const factorial = n => n === 0 ? 1 : n * factorial(n - 1);
console.log(factorial(5)); // 输出 120

2. 判断一个变量是否为对象类型


const isObject = variable === Object(variable);

3. 数组去重


利用Set数据结构的特性,去除数组中的重复元素。


const uniqueArray = [...new Set(array)];

4. 数组合并


合并多个数组,创建一个新的数组。


const mergedArray = [].concat(...arrays);

5. 快速最大值和最小值


获取数组中的最大值和最小值。


const max = Math.max(...array);
const min = Math.min(...array);

6. 数组求和


快速计算数组中所有元素的和。


const sum = array.reduce((acc, cur) => acc + cur, 0);

7. 获取随机整数


生成一个指定范围内的随机整数。


const randomInt = Math.floor(Math.random() * (max - min + 1)) + min;

8. 反转字符串


将字符串反转。


const reversedString = string.split('').reverse().join('');

9. 检查回文字符串


判断一个字符串是否为回文字符串。


const isPalindrome = string === string.split('').reverse().join('');

10. 扁平化数组


将多维数组转换为一维数组。


const flattenedArray = array.flat(Infinity);

11. 取随机数组元素


从数组中随机取出一个元素。


const randomElement = array[Math.floor(Math.random() * array.length)];

12. 判断数组元素唯一


检查数组中的元素是否唯一。


const isUnique = array.length === new Set(array).size;

13. 字符串压缩


将字符串中重复的字符进行压缩。


const compressedString = string.replace(/(.)\1+/g, match => match[0] + match.length);

14. 生成斐波那契数列


生成斐波那契数列的前n项。


const fibonacci = Array(n).fill().map((_, i, arr) => i <= 1 ? i : arr[i - 1] + arr[i - 2]);

15. 数组求交集


获取多个数组的交集。


const intersection = arrays.reduce((acc, cur) => acc.filter(value => cur.includes(value)));

16. 验证邮箱格式


检查字符串是否符合邮箱格式。


const isValidEmail = /^[\w-]+(\.[\w-]+)*@([\w-]+\.)+[a-zA-Z]{2,7}$/.test(email);

17. 数组去除假值


移除数组中的所有假值,如falsenull0""undefined


const truthyArray = array.filter(Boolean);

18. 求阶乘


计算一个数的阶乘。


const factorial = n => n <= 1 ? 1 : n * factorial(n - 1);

19. 判断质数


检查一个数是否为质数。


const isPrime = n => ![...Array(n).keys()].slice(2).some(i => n % i === 0);

20. 检查对象是空对象


判断对象是否为空对象。


const isEmptyObject = Object.keys(object).length === 0 && object.constructor === Object;

21. 判断回调函数为真


检查数组中的每个元素是否满足特定条件。


const allTrue = array.every(condition);

22. 检查回调函数为假


检查数组中是否有元素满足特定条件。


const anyFalse = array.some(condition);

23. 数组排序


对数组进行排序。


const sortedArray = array.sort((a, b) => a - b);

24. 日期格式化


将日期对象格式化为指定格式的字符串。


const formattedDate = new Date().toISOString().slice(0, 10);

25. 将字符串转为整数类型


const intValue = +str;

26. 计算数组中元素出现的次数


统计数组中各元素的出现次数。


const countOccurrences = array.reduce((acc, cur) => (acc[cur] ? acc[cur]++ : acc[cur] = 1, acc), {});

27. 交换两个变量的值


[a, b] = [b, a];

28. 利用逗号运算符分隔多个表达式


const result = (expression1, expression2, ..., expressionN);

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

困扰我 1 小时的 404 错误 别人 1 分钟解决了

上周五遇到了一个 Bug,没有任何异常,困扰了我一个小时,差点连周末都过不好了。最后没办法,请教了组内的同学,没想到竟被他 1 分钟解决了! 事情的起因,只是因为我把一个接口从 @GetMapping 改成了 @PostMapping ,然后接口就报以下的错误...
继续阅读 »

上周五遇到了一个 Bug,没有任何异常,困扰了我一个小时,差点连周末都过不好了。最后没办法,请教了组内的同学,没想到竟被他 1 分钟解决了!


事情的起因,只是因为我把一个接口从 @GetMapping 改成了 @PostMapping ,然后接口就报以下的错误:


image.png
没有任何的 WARN 或者 ERROR 日志!

网上搜了一下,也没有什么有效的信息,万能的 AI 给出了下面这样的回答:


image.png
404 的错误太常见了,有很多原因造成这一结果。


但可以确定的是,我的请求路径和控制器配置都是没有问题的,因为只要要把 @PostMapping 改回 @GetMapping ,一切都运行正常。


在这种情况下,搜索引擎和AI,除了给我造成干扰误导排查方向外,不能起到什么实质性的作用。


无奈,我只能硬着头皮打开 DEBUG 日志,尝试对照源码,去解决问题了。


不幸的是,DEBUG 日志实在太多了,里面也没有任何异常。我刚学 SpringBoot 不久,这些碎片化的日志,不能引起我的任何联想,因此,实质上也起不到辅助排查的作用。


折腾了一个多小时,还是没有什么头绪,明天就周末了,带着这个 Bug,周末恐怕都休息不好。于是,我就硬着头皮找了组内一个比较有经验的同学帮忙看一下。


他过来翻了翻日志,查看了一下配置类,淡淡地说到,你打开了 Csrf 验证,但是请求却没带 Token。说罢,指导我加上了一行代码:


.csrf().disable()

然后,再次访问,竟然就真的可以了!整个过程也就 1 分钟左右!


我这个小弱鸡的心灵着实有些触动。于是追问到,大神你是怎么看出来的呀。


“没什么,就是经验多了。日志里面有些信息,比如 token 相关的, 其实已经提示了你答案。不过,需要你对框架比较了解,才能 get 到这些信息。新手遇到这种没有明显异常的问题,确实会比较费劲。”


“那有没有什么办法,可以快速搞懂这种框架问题啊,每次遇到都挺烦躁的,不仅影响研发进度,也影响心情” , 上进的我还是想从大神这里获取更多的经验。


“额….我想想”,大神迟疑了一会儿,“你可以试试这个 XCodeMap 插件“,”它可以提供更丰富的信息,图形化的形式,可以较为容易看出可能存在的问题。实在看不出,你也可以基于这些信息再去问搜索引擎或者AI”。


“感谢大神,我去试一下”。


试用了一下,这个工具画出了下面的序列图:


image.png
我虽然不懂什么 Csrf 的原理,但是这个图已经可以清晰地表达出问题了,在 SpringBoot 的FilterChain 中,走到 CsrfFilter 就终止了,并且调用了一个 AccessDeniedHandler。


看起来,这个序列图是实时动态采集的,而且做了很多剪枝,把一些关键调用给标记了出来。对于 SpringBoot 系列,其会把各种 Filter 的调用情况展示出来,可以让人一眼看出来是哪个 Filter 出了问题。


点击 CsrfFilter 的 doFilter 方法,可以看到以下代码:


image.png
这个代码可以看出来,Csrf 的原理(以CookieCsrfToken为例)就是取两个token进行比对。其中一个从请求的 Header 或者 Parameter 中读出。另外一个,从 Cookie 中读出。


image.png
由于浏览器的同源策略,攻击网站无法获取本网站的Cookie,也即其无法完成下面这样的JS操作:


image.png
但是本网站可以通过上面的操作,把 Cookie 中的token设置到 Header 中,这样就达到了避免 CSRF 攻击的效果。


不过,这里还有一个小插曲,csrf 验证失败,本意应该是报 403 错误码,然后转发到 “/403” 页面,只是因为我没有配置 “/403” 页面,最终才报了404 错误。


image.png


这次由 Csrf 引起的 404 错误,就到此为止了。


我独自完成了后面的排查,还是很开心的。我没有大神那样丰富的经验,可以凭借只言片语的日志信息,就可以推断出问题所在。但我借助 XCodeMap 绘制的动态序列图,按图索骥,搞清楚了问题的来龙去脉。这让我想起了下面的一句话:


人类有了弓箭,拳头就不再是绝对硬实力了。好的工具,可以削平人与人的差距!


感觉自己与大神接近了不少!


参考资料:



作者:摸鱼总工
来源:juejin.cn/post/7362722064069427237
收起阅读 »

工作七年后,我不太关注是否升职加薪了,你很优秀,别因世俗的目标限制自己

前言 Hi 你好,我是东东拿铁,一个正在探索个人IP&副业的后端程序员。 工作七年后,我不太关注是否升职加薪了,你很优秀,别因世俗的目标限制自己。 这篇文章我只想用我半年的经历告诉你一件事:探索无限可能,注重个人成长。 为什么别着眼于晋升or加薪 毕业...
继续阅读 »

前言


Hi 你好,我是东东拿铁,一个正在探索个人IP&副业的后端程序员。


工作七年后,我不太关注是否升职加薪了,你很优秀,别因世俗的目标限制自己。


这篇文章我只想用我半年的经历告诉你一件事:探索无限可能,注重个人成长


为什么别着眼于晋升or加薪


毕业刚到北京的前三年,面对自己在的小公司的尴尬局面,看着Boss上琳琅满目的招聘信息,我的脑海里只有四个字,跳槽涨薪


技术,什么不会我学什么,面经写的什么我就去背什么,算法更是赶鸭子上架,哪怕先从背代码开始。


由于咱们程序员行业特殊性,在北京的时候,下班都在晚上10点左右。当然,上班时间也比较晚,十点到就可以。


刚到北京那段时间,我早上起床后,都在看一些SpringBoot的专栏,因为18、19年那会,面试要求就是这样的,只用Spring MVC落后了。


我觉着那时候和高三的那段时间特别相似,上学期间,宿舍、食堂、教室的三点一线。工作之后变成了小出租屋、办公室两点一线,连食堂这一步都给省了,订外卖嘛。


那几年的时间,我从没和非程序员的朋友们,吃过一次晚饭。


工作累的时候,我时常会去楼道里站一会放松一下,记得那时候办公室的楼道里的墙壁,是洞洞形状的,望着外面车水马龙,我第一次有一种深处困境的感觉。(这个印象让我非常深刻,直接就翻出了老照片)



就这样,我一路摸索前行,直到进入字节,因为工作范围变化,我才去接触了更多的朋友,产品、运营、策划、销售。


很多同事,虽然职责不同,但是工作产出高,既能把控方向,也能处理风险。之前我总觉着技术才是那个扶大厦于将倾的角色,慢慢发现我们才是金字塔的最底层,我看到了需求是一步步怎么从市场到运营、从运营到产品,再从产品到研发。


接触的越多,越感觉自己的见识与认知的狭窄。


后来我了解到一个词,“信息茧房”。



“信息茧房”是指人们倾向于只关注与自己兴趣相符的信息,久而久之,会限制自己的视野和认知范围。


“信息茧房”会导致个人的认知和价值观固化,失去批判性思维和多元化思考的能力。它还会加剧社会分化和对立,因为不同的信息茧房之间缺乏有效沟通和交流,容易引发群体极化和冲突。



我接触的都是程序员,我的同学也都是程序员,所以晋升、加薪、进入大厂,变成了我工作之路上唯一的目标。


慢慢的我就掉进这个陷阱了,为了升职加薪,我上班、下班都在学技术,总以为技术都学会了,自然就能升职加薪了。可最后发现,技术好像并不是最重要的那个。


目标单一是如何影响程序员的


技术


在上一篇文章里,有一个读者给我留言:最讽刺的是大部分程序员竟然觉得c端高并发高可用才叫技术。



上一篇文章里面我也讲了我对于技术的本质的四个阶段,其中三个阶段都是对于技术的追求。


这就是技术人的执念,我们想在技术上分一个高低,想去追求高并发,追求更新的技术。


但事实上,做不完的需求,写不完的CRUD才是常态,能少和产品撕一撕,保持一个良好的心情都挺难的。



回到之前文章聊到的,技术的本质是工具。当前你的产品有什么问题,技术是不是能够发挥作用,就已经产生业务价值了,技术含量绝不是由高并发、大流量来衡量的。


追求确定性


做技术久了,习惯了程序的输入与输出,习惯按照某种规律、某个流程、某个框架、某个计划去做事情,我们写的每一行代码,都是确定性的,我们不大喜欢“变化”,喜欢确定性的东西。


记得有一段时间,已经带着团队半年,却迟迟没有晋升,心里很着急。


我对着wiki上的职级能力要求表,一条条看,一条条对比,我觉着我的能力都足够了,为什么还不让我晋升。


可晋升是这样的吗,我满足了能力要求表,就能够立刻轮到我吗?



晋升是企业的一次人才选拔,选拔那些对于公司未来发展更有价值,能承担更大责任的人。



晋升需要你拿到成绩、具备能力、还要具备一定的影响力。


但还有一件最重要的,就是只有在企业不断发展,业务不断发展,团队快速扩张的时候,才会有充足的机会,提供给我们。


但你说,企业能不能高速发展,这是一件确定性的事情吗,可能老板们都不能给出一个确定性的答复。


同样的,涨薪也一样是不确定的,行业、企业发展不好的时候,拖欠工资都有可能,那如何希望能够涨薪呢?


后来我想明白了这件事之后,回想自己自己当时死磕级别要求的样子,感觉挺有意思的。


社交关系


我是山东人,人情社会从小就感受到了许多,也见识了靠社交关系真的能解决很多问题。但是我总觉着,只要靠自己的努力,哪有什么是花钱解决不了的问题,如果有,那就加钱。


后来,在宝宝出生前,很突然的去医院住院,我们先被安排到了一个三人病房。一个病床,一张小桌子,一个沙发床,就是全部空间。孩子的东西很多,我必须把行李箱打开铺在地上,才能及时拿到需要的东西,护士来的时候,我要不就需要把沙发床收起来,要不就得收起行李箱,特别狼狈。


最重要的是普通病房只能有一个陪护,我陪着老婆情况下就不能再请月嫂了。我又是个新奶爸,照顾孩子和还得照顾老婆,忙的不亦乐乎。


其实,我们早早就预约了独立病房,但是资源有限,需要的时候却住满了,无论我怎么去问,人家都说安排不了。独立病房一晚800多元,但你想花钱都花不出去。


最后家人给某个朋友打了电话,然后又联系了医院,我当晚就搬到独立病房了。


是的,医院一般预留着几间独立病房,就是为了方便一些领导临时安排。


Enmmmm,毕业几年都在北京,这几年来一人吃饱全家不饿,可是在有了孩子的第一天,我就被这个社会深深的毒打了。



无效社交确实没用,有效社交都是资源。



高薪


毕业半年,我有勇气裸辞去北京闯荡。


19年时比毕业薪资饭了两倍,给了我高位上车买房的勇气,觉着明天会更好,房贷嘛,只会越来越不值钱。


毕业五年,薪资翻5倍,但你现在让我我裸辞去闯荡,想想房贷,想想娃,反而觉着被限制住了。


环境变了,市场增速放缓,内卷又严重。在这种环境下,想跳槽,发现机会少,或者有机会也不一定能能接的住你的package。你身边有没有这样的“动弹不得”的朋友呢。


其实高薪,更多的是平台、行业红利带来的,毕竟互联网更容易形成规模。


但我们如果因为高薪,被高薪限制住了自己而畏手畏脚,舍不得放下眼前的利益,放弃更多的可能性,那我们自然会因为高薪,限制自己更长远的发展。


去探索那些不确定的东西


你可能想说,程序员不注重升职加薪,那注重什么呢?我是这么做的。


爱表达的人,先影响世界


第一点我想说的是,去找到自己喜欢或者擅长的事情,并坚持下去。


我探索的方向,是写技术博客,扩大个人影响力,做个人IP。


高中的时候买硬皮本子写,大学了买手帐写,工作了从印象笔记写,写日记、写感悟,开心了写,难过了写。


这是一件从不需要人督促我,但我缺断断续续坚持好多年的事情。


后来看了很多书,看了很多文章,有些文字真的很有力量,能让人感同身受,又能激励我去前进。


我也被一个个优秀的博主,不断的激励着,直到我自己迈出了这一步


我在低谷中,为了缓解焦虑,报名参加了技术人写作训练营,很快里面的内容就不满足我的输出,我又买来粥左罗老师的《学会写作2.0》,读了三遍


不知不觉间,写文章好像并不难了。一直困扰我的没人看怎么办,写的不好怎么办,写什么,怎么排版,怎么起标题,怎么写开头,怎么收尾,一点点的都被解决了。


半年前,我第一次认真的写了一篇文章,并发到掘金上。



半年后,我的文章,竟然上了掘金综合榜榜一,我到现在也觉着挺让人激动的。



我朋友常说,你写这些有什么用,赚到钱了吗?Enmmmm,我写下这句话就发给了他,刚刚发给他,他依然这么说。



借用明白老师文章里的一段话,来回答我为什么坚持。



当一个人能持续成长,包含了知识、思维、能力、心态、情绪、赚钱、关系、健康、感情等,并且他能把自己的成长过程,不断真实的分享出来,大家看到后,就会慢慢对他有信任感,他也就会慢慢拥有影响力。





还有一位朋友在一篇文章中提到我



保持真诚,保持利他,这个世界的规律是,当你在做一件帮助很多人成功的事情时,很多人会希望并帮助你成功,利他终利己。


见识更多人,试着了解可能


做技术的人,都有一个习惯,就是遇到技术难题,自己会苦苦钻研,查阅资料、阅读源码,对于技术的攻关、学习来讲,确实是对的。


但你我很容易就会把这个习惯,迁移到面对的人生其他问题上,小到买房买车,大到职业发展、人生选择,自己钻研很有可能会走很多弯路。


我在字节最累、最迷茫的时候,每次和我的mentor、leader聊完天,我都会有豁然开朗的感觉,因为他们走过你走过的路,对你的问题就是降维打击。


最近半年,在互联网上,了解、认识了好多大佬、朋友。


有做程序员副业社群的刘卡卡,看到了他一路做过来的经历,也在认识他之后,见识了他飞速成长、快速发展的一段时间。


还有已经作出成绩、完成转型的大佬托尼学长。


有和我一样在努力在公众号、掘金输出的朋友猿java、江天飞鸟、Goland猫、IT男的一人企业,每当想到有人在结伴前行,心中便不再孤单。


还有毕业三年,就靠小红书、闲鱼月变现1w+的读者朋友。


还有更多我认识他他不认识我的大佬,亦仁、芷蓝、靠谱、明白、雪梅。


我见识了太多种可能性,你可以做社群,可以做闲鱼电商、可以做面试辅导,可以做自媒体教练。你或许一个月可以通过互联网增加一万元甚至几万的收入,也可能通过互联网实现财务自由。


最重要的,我发现这些人可以自由选择喜欢的事情去做,而且做的很好。这不比只跟随市场需求走,逼迫自己做些不喜欢的事情,强太多了吗?


所以,认识更多人,学习他们的经验,同时这些人也是你的资源。在你苦恼、迷茫的时候去请教一下,聊聊天,你就能从不一样的角度看问题,甚至直接解决问题。就像你看到我,你可以认识我,有程序员方面的问题,也可以直接联系我。


保持头脑开放


赚钱的机会往往是开始于我们第一眼看不上、瞧不起的信息差。


比如说 大学期间,我曾经对微商嗤之以鼻。卖假货、朋友圈刷屏,太low了!
几年之后,当年做微商的,做得好的赚到第一桶金,做得不好,也积累了项目经验、私域用户。


工作后,我觉着那些整理面经的,没什么意思,不就是罗列了知识,照着书本上的内容,那可差的太远了,有这时间我看看书不好吗?


但整理面经的,在技术平台持续输出的那些人,不但积累了第一波粉丝,在那个快速发展的时间,很多人靠公众号赚到了第一桶金。


现在我开始接受一些知识付费,加入了一些社群,我让自己沉浸在一个有着各类机会的环境里,我尝试去看那些曾经我嗤之以鼻的小项目。


只有保持头脑极度开放,才能让各种信息流入。特别是对于一个想赚钱的人,开放的头脑意味着我们允许赚钱机会向我们靠近。


我之前一直不是一个头脑开放的人,所以现在我可能没有太好的经验和大家分享,但今年我一定会有所尝试,通过加入的社群去开阔眼界,也会把过程、收获分享给大家。


相信时间的力量


最后一点我想说,很多事情要慢慢来。


你不必因为别人的成绩而感到焦虑,也不要用当下进展的快慢,去定义以后是否能到达远方。


更不要忽视时间带来的力量,所有的积累都会在未来某一时刻回报给你。


说在最后


好了,文章到这里就要结束啦,很感谢你能看到最后。


当然,每个人的阶段不同,如果你工作5年内,还是把更多的精力放在晋升、加薪,因为你的空间还很大,未来一定不可限量,但不要让他成为你唯一的目标。


但工作5年以后,当职业生涯遇到瓶颈,你的人生还很长,不妨试着去探索更多的可能。




作者:东东拿铁
来源:juejin.cn/post/7367701663170592818
收起阅读 »

如何快速实现多行文本擦除效果

web
今天来实现一个多行文本擦除的效果,有种经典咏流传节目中表演开始前阅读诗句的一些既视感,在工作中其实也遇到过这样的需求当时是用的其他方法来实现的,现在发现了更简单的一种方法并且里面也涵盖了不少的知识点。 以上就是最终要实现的效果,比较敏感的同学呢应该能看到文本...
继续阅读 »

今天来实现一个多行文本擦除的效果,有种经典咏流传节目中表演开始前阅读诗句的一些既视感,在工作中其实也遇到过这样的需求当时是用的其他方法来实现的,现在发现了更简单的一种方法并且里面也涵盖了不少的知识点。


img1.gif


以上就是最终要实现的效果,比较敏感的同学呢应该能看到文本是由歌词组成的哈哈,没错今天是我偶像发新歌的一天,就用歌词来致敬一下吧!


思路


首先先来捋一下思路,乍一看效果好像只有一段文本,但其实是由两段相同文本组成的。



  1. 两段相同文本组成,这是为了让它们实现重合,第二段文本会覆盖在第一段文本上。

  2. 修改第二段文本背景色为渐变色。

  3. 最后对渐变颜色的背景色添加动画效果。


先来搭建一下结构部分:


<body>
<div class="container">
<p>
失去你以来 万物在摇摆 你指的山海 像玩具一块一块 我是你缔造又提防的AI 如果我存在 是某种伤害
不被你所爱 也不能具象出来 我想拥有你说失去过谁的 那种痛感 失去你以来 万物在摇摆 你指的山海 像玩具一块一块我是你缔造又提防的AI 如果我存在 只对你无害 想做你所爱 再造你要的时代 执行你最初设计我的大概
成为主宰 失去你以来 万物在摇摆 你指的山海 像玩具一块一块 也许我本来 就是种伤害 我终于明白 我根本就不存在 谁不在造物主设置的循环 活去死来
</p>
<p class="eraser">
<span class="text">
失去你以来 万物在摇摆 你指的山海 像玩具一块一块 我是你缔造又提防的AI 如果我存在 是某种伤害
不被你所爱 也不能具象出来 我想拥有你说失去过谁的 那种痛感 失去你以来 万物在摇摆 你指的山海 像玩具一块一块我是你缔造又提防的AI 如果我存在 只对你无害 想做你所爱 再造你要的时代
执行你最初设计我的大概
成为主宰 失去你以来 万物在摇摆 你指的山海 像玩具一块一块 也许我本来 就是种伤害 我终于明白 我根本就不存在 谁不在造物主设置的循环 活去死来
</span>
</p>
</div>
</body>

代码中两段文本都是由p标签包裹,第二段中加入了一个span标签是因为后面修改背景色的时候凸显出行的效果,这个下面加上样式后就看到了。


添加样式:


* {
margin: 0;
padding: 0;
box-sizing: border-box;
}

body {
background: #000;
color: #fff;
}

.container {
width: 60%;
text-indent: 20px;
line-height: 2;
font-size: 18px;
margin: 30px auto;
}

img2.png


现在只需要给第二段增加一个定位效果即可实现文本的覆盖:


* {
margin: 0;
padding: 0;
box-sizing: border-box;
}

body {
background: #000;
color: #fff;
}

.container {
width: 60%;
/* 直接加在父元素中即可对所有块级元素的子元素进行首行缩进 */
text-indent: 20px;
line-height: 2;
font-size: 18px;
margin: 30px auto;
position: relative;
}

.eraser {
position: absolute;
/* 这里等同于top:0 right:0 bottom:0 left:0 */
inset: 0;
/*
这里解释一下inset属性,inset属性用作定位元素的top、right、bottom 、left这些属性的简写
依照的也是上右下左的顺序。
例如:inset:1px 2px 等同于 top:1px right:2px bottom:1px left:2px
*/

}

image.png


那接下来就应该修改背景颜色了。


以上重复代码省略......

.text {
background: #fff;
}

这时候给span标签加上背景颜色后会看到:


image.png


而不是这样的效果,这就是为什么需要加一个span标签的原因了。


image.png


以上重复代码省略......

.text {
background: linear-gradient(to right, #0000 10%, #000 10%);
color:transparent;
}

image.png


下面要调整的就是将渐变里面的百分比变为动态的,我们可以声明一个变量:


以上重复代码省略......

.text {
--p:0%;
background: linear-gradient(to right, #0000 var(--p), #000 calc( var(--p) + 30px)); // 加上30px显示一个默认的渐变区域
color:transparent;
}

image.png


下面就该加上动画效果了,在设置动画时改变--p变量的值为100%


以上重复代码省略......

.text {
--p:0%;
background: linear-gradient(to right, #0000 var(--p), #000 calc( var(--p) + 30px));
color:transparent;
animation: erase 8s linear;
}

@keyframes erase{
to{
--p:100%;
}
}

但是这样写完之后发现并没有出现动画的效果,这是因为css动画中只有数值类的css属性才会生效,这里已经是一个数值了但--p还不是一个属性,所以我们要把他变成一个css属性,可以利用@property规则来帮助我们生成一个-xxx的自定义,它的结构:


@property 属性名称 {
syntax: '<类型>'; // 必须
initial-value: 默认值; // 必须
inherits: false; // 是否可继承 非必须
}

以上重复代码省略......

.text {
--p:0%;
background: linear-gradient(to right, #0000 var(--p), #000 calc( var(--p) + 30px));
color:transparent;
animation: erase 8s linear;
}

@property --p {
syntax: '<percentage>';
initial-value: 0%;
inherits: false;
}

@keyframes erase{
to{
--p:100%;
}
}

到此为止也就实现开头的效果了!!!


作者:孤独的根号_
来源:juejin.cn/post/7333761832472838144
收起阅读 »

超级离谱的前端需求:搜索图片里的文字!!难倒我了!

web
前言 大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心~ 背景 是这样的,我们公司有一个平台,这个平台上面有一个页面,是一个我们公司内部存放一些字幕图片的,图片很多,差不多每一页有100张的样子,类似于下面这样的图片 ...
继续阅读 »

前言


大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心~


背景


是这样的,我们公司有一个平台,这个平台上面有一个页面,是一个我们公司内部存放一些字幕图片的,图片很多,差不多每一页有100张的样子,类似于下面这样的图片



前几天上面大佬们说想要更加方便快捷地找到某一张图片,怎么个快捷法呢?就是通过搜索文字,能搜索到包含这些文字的图片。。。我一想,这需求简直逆天啊!!!!平时只做过搜索文字的,没做过根据文字搜索出图片的。。。。



思路


其实思路很清晰,分析出每一张图片上的文字,并存在对象的keyword中,搜搜的时候去过滤出keyword包含搜索文字的图片即可。


但是难就难在,我要怎么分析出图片上的文字并存起来呢?


tesseract.js


于是我就去网上找找有哪些库可以实现这个功能,你还真别说,还真有!!这个库就是tesseract.js



tesseract.js 是一个可以分析出图片上文字的一个库,我们通过一个小例子来看看他的使用方式


首先需要安装这个库


npm i tesseract.js

接着引入并使用它解析图片文字,它识别后会返回一个 Promise,成功的话会走 then



可以看出他直接能把图片上的结果解析出来!!!真的牛逼!!!有了这个,那我轻轻松松就可以完成上面交代的任务了!!!



实现功能


我们需要解析每一张图片的文字,并存入 keyword属性中,以供过滤筛选



可以看到每一张图片都解析得到keyword



那么搜索效果自然可以完成



搜索中文呢?


上面只能解析英文,可以看到有 eng 这个参数,那怎么才能解析中文呢?只需要改成chi_sim即可




如果你想要中文和英文一起解析,可以这么写eng+chi_sim





作者:Sunshine_Lin
来源:juejin.cn/post/7355554711167369268
收起阅读 »

大专生两年经验的年度总结

  🍉写在前面,00后程序猿(身高183!),今天是12.11,已经可以算的上是23年的末尾了。从21年8月开始入行,已经一坤年的时间了。 🍋年度目标 目标的话,基本是重复上年的操作,没有几条是达成的。 薪资目标        算是勉强到达年前的目标了吧 手...
继续阅读 »

  🍉写在前面,00后程序猿(身高183!),今天是12.11,已经可以算的上是23年的末尾了。从21年8月开始入行,已经一坤年的时间了。


🍋年度目标


目标的话,基本是重复上年的操作,没有几条是达成的。



  1. 薪资目标        算是勉强到达年前的目标了吧

  2. 手写promise全部规范   总体流程是可以的,个别规范还未实现,算成功85%

  3. react          不好估计,是学完了,但是一直没有完整的demo项目,算完成70%

  4. java          只会java基础,算完成15%左右

  5. 《vue的设计与实现》  仅把书买了,进度0%

  6. 个人博客系统      前端完成七七八八了,由于java的原因后端没写,前端完成度:80%,后端:0%

  7. 自己组装个台式电脑   完成完成,这种目标肯定是第一时间完成的

    看了一下上面的目标和完成度的对比,今年真的蛮失败的,尤其是工作上,这个等文章后面再提。


先来说一下组装台式机的过程


  为什么首先写这个呢?肯定不是因为我想炫耀。电脑是四月份开始决定组装的,依据贴吧老哥和自己的要求在JD上选了一些配置(3060ti g6x+12600kf),机箱选的罗宾3,总体体验还不错,尤其这9个棱镜风扇,风扇当初考虑了半天,因为已经买了水冷,只打算再买三个风扇,毕竟我是第一次装机,风扇太多我不一定装的好,后面一想(风扇肯定是越多越帅啊),就多入手了6个风扇。后面又多加了一个发光的显卡支架,最终成了下面的样子(手机像素不行,电脑实物还是很帅的)。


86DE025CB6BFCC2B2E116C511C5EBAD7.jpg


2023这一年我是如何实现上面的目标的?


  工资这方面,没什么好说的,肯定是正常的面试吹嘘。上面列出的几条技术要求,一般都是工作闲的时候写一写,没有刻意要求自己(可能正是这种心态,导致了那么多目标没有完成)。下面的图片便是我今年的所有收获。其实还有一个小目标完成了,上面没有提到,那就是锻炼身体,毕竟钱再多,技术再好都不如身体重要,晚上都会去小区广场那边进行跳绳,从起初的一天500已经变成一天3000个^_^


image.png


然后就是这两年就职的公司情况


  一坤年的时间,我已经入职了三家公司,离职的原因都是一些不可抗力因素。



第一家:南京,公司是做自研项目的,开发团队有10个人左右,前端最多的时候是4个前端,单休,每周三下午固定时间有下午茶,工作很辛苦,但是公司氛围很不错,每个人都很好沟通,都很照顾我。后面离职是因为老板认为南京这边人力成本有点高,把公司搬回老家了。




第二家:南京,公司算是做自研项目的,为什么说算是呢?这边的主要业务是做自己的项目,然后把这个项目的核心内容卖出去(嵌套到甲方的项目中),当时入职时就我一个前端,4个后端,老板本身也是后端,一个测试,大小周,一般是同时进展两个项目(老板和领导能力比较强,他俩负责一个项目,我和另外两个后端负责另一个项目),每周三下午是固定的羽毛球运动,小零食管饱,公司氛围同第一家一样很好,每个人都很好沟通,第一天入职时,老板会请吃饭,一般是一个项目结束后会团建聚餐一次。后面离职是因为当时公司暂时没项目,老板和我们讨论:他想降低整体的薪资(是讨论,并不是那种直接通知),我和几个同事都能理解老板,但是都表示不能接受(这是很现实的事情),老板最终给予了我们三个n+1(已经很好了)。`




第三家:依旧在南京,也就是现在在职的一家公司,仍然是自研,工作内容十分轻松,一共是三个开发,年终奖、午餐、双休这些都有,并且项目已经很成熟了,基本不会有什么大改动(至少目前是,来这边一年了都是一些小问题的修改)。并且公司基本不会存在倒闭的问题(老板的其它业务需要这个系统,并且其它业务十分赚钱,老板是身价很高的那种人)。从上面的几条内容来看,这个公司应该是大多数人心中不错的公司了,但是可能有人会发现我没有提到公司氛围这个内容:

  接下来我要提的便是氛围了,我只能说差,并且不是一般的差。为什么这么说呢?首先我们是只有三个开发,按常理来说:人越少,氛围会越好,但是我们办公的地方很独特,我们是同公司其它业务的人一同办公的,都是在同一个场地办公,问题的一小部分就出现在这,其它业务的人员都是官职很高的老领导(你可以这么理解:和你一起办公的都是清华大学、北京大学各大高校的退休校长、退休教授),如果是同样业务,那倒很好,说不定咱还能攀攀别人的关系的,可惜不是,并且我是基本不敢进行激情讨论的,我这边的领导怕激情讨论影响他人。并且在这个公司让我学会了语言的艺术,勾心斗角的人是真TiMi的多,到处都是人情世故、阿谀奉承(排除个别不是的),这个时候可能又会有人说,小团队氛围干嘛要受别人的影响呢,做好自己就可以了。

  对于上面的问题,我只能说,哥们,真不能怪我们,我本人还是蛮开朗的,在我之前的两位同事还未离职的时候,我们三个的氛围也算还不错吧,后面他俩因为种种原因离职了。后面就入职了另一个同事,可能是性格问题,我们之间的沟通很少,除了对接口的时候会说几句话。可能又有人会说了,领导主持一次聚餐,大家互相熟悉一下不就好了吗。看到这种,只能微微一笑,我可以这么给你解释我的领导,我之所以能学会人情世故、阿谀奉承都得去谢谢他。对外他是唯唯诺诺,对内是重拳出击。变脸比翻书还快,上级对他有好脸色,他对我们不一定有好脸色,上级对他没好脸色,他对我们必定是重拳出击(这个哥们让我见识到了心态可以决定年纪,因为他真会装孙子)。比活火山还离谱,事情多的时候嫌我们做的不行、做的慢,事情少的时候,嫌我太闲,怎么看怎么不顺眼,我是真提莫的无语了。并且这个人十分喜欢讲冷笑话(至少我是这么认为的),十分冷的那种。



image.png


image.png


  上面两张截图,是我周末在外面玩,然后领导叫我来加班的乌龙事情,我只能说:罕见,这种罕见的极品是怎么混上领导的(我刚入职的时候只有他一个光杆司令)。工作中的甩锅事情我就不想说了,因为根本说不过来(我不知道这个甩锅在其它公司多不多),在我之前的公司出了问题基本都是领导自己揽下来(无论是不是他的问题),并且这种人我们都很听他的,也很佩服这种人。但是这个极品就不一样了只要是对外演示的时候,无论是谁的问题,永远是甩锅给我们,绝对不能因为这个问题影响他装逼,是真极品。日常中还有更极品的事情我就不写了(只能说比三国杀还恶心)。


🍍日后的计划


  换工作,年后必须先把工作换了,哪怕是裸辞,这种极品领导很难相处。其次是java,java已经学了两年了,去年的计划就已经有java了,一直没有学,2024一定要学会。然后就是爬武功山,是真想去那看看,另一个目标的话就是要攒一部分钱出来,要把买车提上日程。


作者:外围前端吴彦祖
来源:juejin.cn/post/7310964581052121100
收起阅读 »

设计呀,你是真会给前端找事呀!!!

web
背景 设计:我想要的你听明白了吗,你做出来的和我想要的差距很大,你怎么没有一点审美(你个臭男人,你怎么不按我画的做)! 我:啊?这样自适应不是很好吗,适配了大部分机型呀,而且不会有啥显示的兼容性,避免不必要的客户咨询和客户投诉。 设计: 你上一家公司就是因为...
继续阅读 »

背景



  • 设计:我想要的你听明白了吗,你做出来的和我想要的差距很大,你怎么没有一点审美(你个臭男人,你怎么不按我画的做)!

  • :啊?这样自适应不是很好吗,适配了大部分机型呀,而且不会有啥显示的兼容性,避免不必要的客户咨询和客户投诉。

  • 设计: 你上一家公司就是因为有你这样的优秀员工才倒闭的吧?!

  • :啊?ntm和产品是一家的是吗?





我该如何应对


先看我实现的


b0nh2-9h1qy.gif


在看看设计想要的


9e2b0572-aff4-4644-9eeb-33a9ea76265c.gif
总结一下:



  • 1.一个的时候宽度固定,不管屏幕多大都占屏幕的一半。

  • 2.俩个的时候,各占屏幕的一半,当屏幕过小的时候两个并排展示换行。

  • 3.三个的时候,上面俩,下面一个,且宽度要一样。

  • 4.大于三个的时候,以此类推。



有句话叫做什么,乍一看很合理,细想一下,这不是扯淡么。



所以我又和设计进行了亲切的对话



  • :两个的时候你能考虑到小屏的问题,那一个和三个的时候你为啥不考虑,难道你脑袋有泡,在想一个和三个的时候泡刚好堵住了?

  • 设计: 你天天屌不拉几的,我就要这样,这样好看,你懂个毛的设计,你知道什么是美感和人体工学设计,视觉效果拉满吗?

  • :啊?我的姑奶奶耶,你是不是和产品一个学校毕业的,咋就一根筋呢?

  • 产品:ui说的对,我听ui的。汪汪汪(🐶)


当时那个画面就像是,就像是:





而我就像是
1b761c13b4439463a77ac8abf563677d.png


那咋办,写呗,我能咋办?



我月黑风夜,
黑衣傍我身,
潜入尔等房,
打你小屁屁?



代码实现


   class={[
'group-even-number' : this.evenNumber,
'group-odd-number' : this.oddNumber,
'themeSelectBtnBg'
]}
value={this.currentValue}
onInput={(value: any) => {
this.click(value)
}}
>
...


   .themeSelectBtnBg {
display: flex;
&:nth-child(2n - 1) {
margin-left: 0;
margin-right: 10px;
}
&:nth-child(2n) {
margin-left: 0;
margin-right: 0;
}

}
// 奇数的情况,宽度动态计算,将元素挤下去
.group-odd-number {
// 需要减去padding的宽度
width: calc(50% - 7.5px);
}

.group-even-number {
justify-content: space-between;
@media screen and (max-width:360px) {
justify-content: unset;
margin-right: unset;
flex: 1;
flex-wrap: wrap;
}
}

行吧,咱就这样吧




作者:顾昂_
来源:juejin.cn/post/7304268647101939731
收起阅读 »

完蛋,我失忆了,记一次团建的翻车之旅😵

0x0. 背景介绍 本文首发于我的同名公众号,「野生的码农」。 转眼间,距离上篇文章的发表已经1个多月了,正当我纠结于再写点什么的时候,一场突如其来的事故,让我短暂地失忆了。不久后,流感来袭,发烧咳嗽了许久,完全打断了我的计划。现已基本康复,就分享下团建而引发...
继续阅读 »

0x0. 背景介绍


本文首发于我的同名公众号,「野生的码农」。


转眼间,距离上篇文章的发表已经1个多月了,正当我纠结于再写点什么的时候,一场突如其来的事故,让我短暂地失忆了。不久后,流感来袭,发烧咳嗽了许久,完全打断了我的计划。现已基本康复,就分享下团建而引发的「血案」,顺便聊聊一些倒霉事,博君一笑😁。


2015年6月,刚到腾讯不久,在张北草原团建,有个斗鸡的热身环目,就像这样:


斗鸡


不同的是,我们是在水泥地上开斗的。组里有个身高1米85的大长腿同事,可能因为重心太高或者忌惮对手是领导,被斗输,摔倒在地。看上去摔的并不重,但他脸色不太好,走路一瘸一拐的。


次日,返回北京,积水潭医院,诊断为股骨骨折。先后做了两次大手术,需要卧床休息很久,印象中休了近1年的假。屋漏偏逢连夜雨,他受伤时老婆快生了,算是陪他老婆休了个产假。


因为是在团建中受伤的,走的工伤保险报销医疗费和申请假期。万万没想到,码农有朝一日也能用上工伤保险。


说到保险,当初我老婆生娃,我问 HR 能否用我的生育保险申请北京的什么津贴,她说刚取消了。好吧,那我的生育保险能干啥?答曰:



可以报销男性的输精管结扎术的费用



啊?这。。。此时无声胜有声。。。再见。。。


自「斗鸡」团建之后,我拒绝参加可能有风险的活动,只参加爬山、散步、扯淡之类的「老年团」了,我可不想再休个产假。


10月底,组里在筹划「鬼屋探险」主题的剧本杀团建,在看了宣传片后,我怂了,立刻放弃。


虽然我本科是学医的,多次接触过大体老师,是坚定的无神论者,但我不敢看恐怖片。初中,不小心和老表们一起看了《山村老尸》,连续多个晚上,刚闭上眼就开始放电影,很久才能睡着。某天,房间突然出现一滩水,和电影里的情节一样,吓的我以为女鬼要来杀我了。。。


在我退出之后,陆续又有几位老哥跑路,组织的同学又贴心地攒了个「泡温泉」的老年团,而且还有按摩😍。鹅妹子嘤!这个不戳,零风险,带条泳裤即可,冲!


美中不足的是,稍微有点远,接近70多公里。出发前一天,统计拼车信息,顺路的几位老哥,都跑去捉鬼了,我只能单飞了。温泉在隔壁的巢湖市,不走高速的话,沿途会经过巢湖,应该会有些不错的风景,遂决定骑摩托车去,也就1个多小时。


巢湖


同事问我是否确定骑车去,必须确定啊,小 case。去年7月,历时两天,先后经历暴晒和暴雨的「冰火两重天」,把我的125踏板摩托车从北京骑回合肥,接近1200公里,我每年骑车都超过7000公里,区区70公里,何足挂齿?


然鹅,自认为老司机的我,终究还是翻车了,温泉团建之后,我就基本没碰过摩托车了。有意思的是,至今我都不知道车祸是怎么发生的,那段记忆完全丢失了😵‍💫。。。


0x1. 案发经过


10月27日,周五,阴,20℃,上午10:50,我拿着手机,一脸懵逼地站在一个陌生城市的某大道的辅路上。旁边躺着我的小踏板,后视镜和转向灯摔烂了,手机支架掉在地上,外套、护膝、鞋子都有损坏,手臂、胳膊、膝盖等多处擦伤,正在流血,身上也有点疼,但我完全想不起来发生什么了。


扶起踏板后,发现旁边工地有个保安一直在朝我这边看,遂走上前咨询刚才发生了什么事。他说我自己摔倒了,没有人撞我,我也没撞到人,提醒我满脸是血,让我赶紧去医院。打开前置摄像头看了下,嘴巴肿了,下巴紫了,脸上有几个出血点,皮肉伤,破相而已,不碍事,大不了去挂个「丑科」。


丑科


比起伤痛来说,此刻,我更想知道的是:



我是谁?我在哪?我要去干啥?



走到路边,找块空地坐了下来,试图想起点什么。发现当天早上的部分记忆有问题,时常出现「该内存不能为 Read」,而之前的记忆都可任意读取。


打开微信看了看,有个团建群在活跃着,依稀想起来今天是去泡温泉,但记忆非常模糊,像是在回忆几十年前的事情,又非常像是在做梦。打电话给同事确认了团建的事,我在原地待命,他开车过来接我。


等待的间隙,打电话给老婆,告诉她我因为车祸失忆了,今天早上的事记不清了,问她我是否来泡温泉的。我特意强调了我没在开玩笑,这略带搞笑的提问,似乎把她给吓到了,问我是不是脑子摔坏了。我随后补充道问题不大,我还记得她跟儿子,也记得我父母,同事在过来的路上了,一会就去医院。在确认了我的安全后,她问我工资卡密码,啊,我失忆了,不记得了,再见👋🏻👋🏻


同事到了后,跟刚才的保安又简单聊了下,得知工地上没有摄像头。如果报警的话,应该能调取附近的监控,我嫌麻烦,毕竟除了失忆,也没啥大事,就不麻烦警察蜀黍了。


前往附近医院,急诊,CT,万幸戴了头盔,没有脑出血。急诊医生说他看片不专业,是放射科医生出具的报告。遗憾的是,忘了拿 CT 的报告单了,也不知道上面写的啥,现在只有病历能证明我曾经「脑子坏了」:


病历


完事后,同事帮忙买了碘伏和湿巾,往伤口喷了喷,嘶~,嘶~~,我靠,这么疼😭。消毒时,才发现腹股沟和肚子也有多处血肉模糊的,虽然有点疼,但也都是皮肉伤,并无大碍。


幸好当天气温不高,穿的比较厚,戴了手套,穿了护膝,身上只有一些表面的擦伤。最重要的还是戴了头盔,否则团建就要改吃席了。我记得最后一次看导航是10:30左右,联系同事是10:50,所以我很可能倒地后昏迷了一段时间。


去年从北京骑回合肥,头盔是唯一的装备,连手套都没有,还在高速上跑了300多公里,差点被大车撞到,现在想想都后怕。刚到家时,问我老婆我是否很牛逼,她回复道:



我觉得你是傻逼



彼时,我觉得她不懂我的爱好,话不投机半句多。现在,我认可她的说法。两天,骑行1200公里,确实很牛逼,但也是一种不负责任,毕竟上有老下有小的,没资格去冒险做这种「装逼」的事了。


继续说回团建,虽然下巴和嘴唇受伤了,应该不影响吃饭吧,而且中午有大餐,正好补补身子。出发,干饭🍚。


0x2. 参观温泉


干饭完毕,前往温泉度假酒店。我这一身伤,肯定是不能下水了,本着来都来了的原则,土鳖的我决定进去涨个见识,至少能看看温泉是啥样的。


好么,原来温泉就是公共澡堂子啊,还不区分男女。有意思的是,这澡堂还是露天的:


露天温泉


可能是工作日的缘故,人很少。实话说,我觉得所谓温泉,其实是电加热的(泉)水,不过我没有证据,如果我错了,就当我胡说吧。


澡堂的种类还挺多的,区别就是添加剂不同,有红酒澡堂、牛奶澡堂、玫瑰澡堂等等,建议老板可以再搞个酱香澡堂,说不定会门庭若市。室内有个澡堂养了些小鱼,可去除腿部和脚上的死皮,就是不知道小鱼是否会觉得恶心🤮


同事们泡完澡,去二楼按摩,问我是否一起。拒绝,我当时浑身都在疼,别给我送走了。后来,听他们描述按摩的过程,还挺有意思的,此处省略18万字,付费后可见🐶。


17:00,约了辆货拉拉,正好有同事要提前走,搭他的车去了事故地点。其他同事留下来,晚上又搓了一顿(饭)。


19:00,我和踏板到家了。


货拉拉


就这样,我参加了团建,但又没有完全参加。


啊!这是一次多么难忘的团建啊!!虽然我失忆了,但这次刻骨铭心的团建将永远在我的脑海中「阴魂不散」!!!这一刻,感觉脖子上的工牌都更加鲜艳了,唯一不好的是,工牌是被我的鲜血染红的😎。


写到这,想起了另一件悲催的事:


7月底,带孩子去海边玩,买了条泳裤。傍晚到达目的地后,去附近熟悉下环境,然后,我就摔倒在海边的礁石上了。胳膊破了一大块,碰到海水就生疼。次日,只敢在浅水区站着,泳裤几乎没碰到水。温泉团建,泳裤一直躺在踏板的坐垫下,都没能出来看一眼温泉。。。


0x3. 第二滴血


因为后视镜摔碎了,上路非常危险,手上的伤口尚未愈合,气温也在逐渐走低,今年基本是告别摩托车了,只能被迫开着「仅摇号一年就中标的京牌油车」上下班了。


团建后的第1个工作日晚上,开车前往合肥的「华强北」--「大钟楼」,把车停在城泊画的停车位上。拿到送修的 iPad 后,在开出车位时,看到左侧有个电动车飞奔而来,立刻踩下了刹车。在我静止了3秒后,他还是直接撞了上来,摔倒在地。实话说,我怕他讹我,没有立刻下车。


对方是个年轻小伙,让我帮忙扶一把,说起不来,应该不是碰瓷的,遂下车把他的电动车扶了起来。随后,他自己站起来了,膝盖破了一大块,还在流着血。小伙看着学生模样,戴着个耳环,抽烟,看着还算实诚。我说我停着在啊,你咋还撞上来了?他回答说「我知道,我知道,接电话没注意到」。


我问他伤势怎么样,是否要去医院看看。他说没事没事,不需要去医院,他去找朋友有急事,让我直接走。我不放心,我不知道这算谁的责任,万一他后面有啥事,我这算是肇事逃逸吗?我跟他说先别走,还是让专业的人来处理吧,遂报警并打了保险公司电话。


等待的间隙,看了下车损情况,前保险杠被撞变形了,松松垮垮的,有个雷达被撞脱落了:


撞车现场


很快,交警就来了,出示正件,拍照,如实反映情况,小伙说他是在打电话没注意到汽车。流程走完后,交警说情况复杂,他定不了责,需要我和小伙一起前往某指定的地点定责。


啥??还有交警定不了责的?难道是我全责?我已经停车了啊,交警说那肯定不是。我又问道,如果我自认全责,保险公司是否会认可?交警笑道,那当然可以,保险公司是以交警的结论来赔付的,但是没必要,我不是全责,会影响保费。


交警说我们自己协商也行,但是估计协商不出来,还是一起去定责吧。确实,我不知道责任如何划分,也不知道修车费用,完全没法协商。


小伙一脸诚恳地说肯定会承担修车费用,我们互留了联系方式,后面再约时间一起去定责。交警特意提醒了我们,任何一方都有义务协助对方完成定责,否则可能会被追究法律责任。


搞定,各回各家。然后,就没有然后了。


消失的他


次日上午,微信问他是否需要去医院,傍晚回复我没啥事;傍晚约定责时间,久未回复,电话之,不接,凌晨回复我待定。


考虑到定责还要请假,我那破车也不值钱,就近找了个便宜的修车店,150元就搞定了。我没指望他全付,随便给多少都行,微信之,让他看着给。整整一天,没有回复,难道他没看懂「看着给」的意思?


修车后的第2天,打了两个电话给他,都被挂断,微信回复我说要等发工资。罢了,我自认倒霉,不想为这点钱再浪费时间了。原打算说他两句的,想想还是算了,不值得。


本来,这里是放了聊天记录截图的,最后整理文章内容时删除了,没必要。


很久之后,我想到一件事,他骑的是美团共享电动车,应该自带保险?很可能根本不需要他掏钱,我懒得去操心了,就酱。


果然,不要考验人性,男人都是大猪蹄子🐷。


0x4. 第一滴血


现在回想起来,那段时间一直在倒霉,交通工具陆续因为意外而罢工,只能靠「11路公交车」了。但是,很不幸,最先遭殃的,恰恰就是我的大长腿。


在团建的前两周,周一早上,送孩子上学,电梯死活不来,马上要迟到了,只能走楼梯了。因为走的很着急,没注意到一楼大堂里的平板车,发生了下面悲剧的一幕:




  • 地上有辆平板车,平板车前面有个30多岁的女人摔倒了,面部朝下

  • 平板车后面有个30多岁的男人摔倒了,面部朝上

  • 平板车附近有一些液体的药渍

  • 平板车右侧是一扇玻璃门,玻璃门外站着一个小孩,一脸茫然地望着里面的男女。



我让 AI 帮我画出上面的场景,很快啊,它画了4张图:


AI 画的图


呃,它应该是理解错了平板车,我补充说明「平板车」是用来临时拉货的小推车,它又重新画了两幅图:


AI 画的图2


得,叫狗不如自走,这画的都是啥啊,驴头不对马嘴的,只能老夫亲自出马了,上述惨案现场的真实还原图如下:


惨案现场


惨案的复现步骤如下:




  1. 我踩到了平板车尾部,摔倒,右腿撞到了车尾

  2. 平板车动起来了,撞到走在前面的老婆,摔倒

  3. 老婆手里端着一杯液体的药,摔倒时洒落一地



我骂骂咧咧地站了起来,忍着右腿的疼痛,一脚踹向了平板车。只见平板车不急不慢地驶向了玻璃门外,不偏不倚地撞到我儿子腿上了,尼玛。。。


送完孩子后,返回家中,看了下伤口,右侧大腿,有10cm的划痕,在出血,估计是碰到了平板车的拐角了。在微信上跟物业说了此事,回复了个捂脸的表情;问我是啥车,回复道挺大的一辆铁车,继续捂脸。他这是觉得我是傻逼?我本来是想调监控看下当时发生了啥,为啥会没看见那么大的车,算了,自讨没趣,喷了点酒精和紫汞就上班去了。


当晚,划痕的「线」就演化成了「面」,大腿内侧接近1/5的皮肤都是瘀伤的红色,挺吓人的,就不放照片了。应该没啥大事,肯定是没伤到骨头,就没去医院了。


几天后,物业主动联系我,说他前几天在休息,今天刚回来,现在找到了当初放平板车的人,让我把伤势说的重一点,可以多索要一些赔偿。我回复道,不用了,我不交物业费就是了,你也别再找我了,然后我就挂断了电话。


后来我才知道,是我老婆跟物业总公司交涉后,物业才找我的。特么的,我缺那点赔偿?物业这工作态度,感觉是我为他服务似的。原来,我都是提前预存一年的物业费,看来我脑子早就坏了。


好在,老婆孩子的伤势较轻,没多久就好了。我到现在也没完全恢复,腿上的大片红色已经褪去,平时完全没感知,但是按上去还有点疼,只能等着时间来磨平伤痛了。


0x5. 总结 & 体会


半个月前,我又去了趟巢湖,喝喜酒。高速入口,取卡,发现左侧胳膊不能完全抬起来,稍微用点力就巨疼无比。其实,自从团建骑车摔倒后,左侧肩膀一直在疼,因为不影响日常活动,我就没在意。


原来那次事故,除了「失忆」外,还摔出了隐藏 bug,只是需要特定的姿势才能复现:



坐着,手臂外展,向上抬起



拍了个核磁共振,问题不大,肩袖损伤,保守治疗,开药,外敷内用。一周后,依然疼痛,复查,医生说等它自己慢慢恢复吧,也不需要再来复查了。「伤筋动骨一百天」,古人总结的经验数据,多少还是有点道理的。


前几天,骑车去修后视镜和转向灯,因为路太烂,手机从支架上掉了下来,可能是情景再现,依稀想起了一点事。当初貌似是为了躲避路上突然出现的大坑或什么东西,双手捏死了刹车,因为车速较快,又没有 abs,然后就翻车了。但是记忆非常模糊,我也不知道是真实发生还是自己的心理暗示。


简单算了下,因为骑摩托车参加团建,我多花了2000余元,也不知道踏板能否卖这么多钱:




  1. 外套:300元

  2. 货拉拉:150元

  3. 摩托车后视镜 + 手机支架 + 转向灯:200元

  4. 汽车保险杠:150元

  5. 头部 CT:260元

  6. 核磁共振 + 医药费:1000元



整个团建,我只参加了吃饭,然后看人泡澡,听人描述按摩。我这算是响应刺激消费的号召,创造了2000元的 GDP 吗?


你很机车耶


在我「失忆」后不久,我的台式机也「失忆」了,这个机器是为组内提供 Crash 堆栈在线解析服务的,服务部署在 WSL 里。某天,WSL 的文件系统突然变成只读的了,不能创建和修改任何文件,也即无法产生新的记忆了。


我怀疑是所谓的安全软件搞的鬼,联系 IT,回复说要等更高权限的人来处理。我等不及了,尝试了微软官网提供的修复方法Read-only fallback error


so easy,只需3个命令,一顿操作猛如虎后,WSL 彻底凉凉了,完全启动不了了,之前好歹还能进入系统。。。


其实,Crash 除了表示「崩溃」,还有「车祸」之意,也就是说:



Crash 后的我,让解析 Crash 堆栈的机器 Crash 了



宕机后的几天,我可能中招了甲流或支原体,39℃,周末连续烧了3天,忽冷忽热的,又体验了一把「冰火两重天」。


周一,还在发着烧,但感觉好了不少,就去公司了。发烧时精神状态不好,代码肯定是写不了了,但来都来了,索性乘着一股热劲,花了一天时间,把之前的坑又踩了一遍,重新安装了 WSL 并搭建了环境,有种失而复得的感觉。这次,我把坑记录下来了,防止哪天又 Crash 了。


看了下安全软件的日志,某天强制重启了电脑,估计是突然断电导致没有正确关闭 WSL,把 WSL 的文件系统搞坏了,变成只读了。我猜测,车祸后的失忆也是类似的,因为摔倒后大脑突然断电,导致Cache在大脑RAM里的记忆没能及时FlushDisk上,那段记忆就这么永远丢失了。


不同的是,电脑挂了可以重装,人要是挂了,再也没有机会重来了,直接少走几十年弯路。


这次「失忆」的经历,既是不幸,也是万幸。万幸在没有酿成大祸前,让我深刻地理解了随处可见的标语:



道路千万条,安全第一条



最后,也请读者朋友们时刻牢记「安全第一」,包括但不限于走路、骑车、开车,祝大家在人生的旅途中一路平安。


道路千万条,安全第一条


作者:野生的码农
来源:juejin.cn/post/7310423310167916594
收起阅读 »

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

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

站会


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


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


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


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


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


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


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


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


排查


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


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

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


image.png

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



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



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


image.png


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


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


image.png


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


定位


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


image.png

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


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


解决


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


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

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


image.png


反思


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


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


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


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


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


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


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


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

被裁员半年了,谈谈感想

后端开发,22年9月,跳槽到某新能源生态企业,23年3月中旬的某个周一下午,被HR通知到会议室做个沟通,两周前收到转正答辩PPT模板让我填写,原本以为是做转正答辩的相关沟通,结果是沟通解除劳动合同,赔偿N+1,第二天就是lastday。 进入公司后经历了几次组...
继续阅读 »

后端开发,22年9月,跳槽到某新能源生态企业,23年3月中旬的某个周一下午,被HR通知到会议室做个沟通,两周前收到转正答辩PPT模板让我填写,原本以为是做转正答辩的相关沟通,结果是沟通解除劳动合同,赔偿N+1,第二天就是lastday。

进入公司后经历了几次组织架构调整,也不断变化着业务形态,但本着拥抱变化的心态,想着会越来越好,所以对于这个突发状况毫无准备。


心路历程


首月


刚刚经历裁员,下个月会有工资、奖金和赔偿金入账,赔偿金不扣税,同时对于市场环境没有了解,比较乐观。首月的想法就是写简历,并开始投递,先投不想去的公司找面试经验;找学习资料、刷题;期望薪资是不需要涨薪,大概平薪就行。

首月面了三家公司,发现了自己的诸多漏洞,项目比较垂类,讲解过程混乱;基础知识复习不足,很多新出来的延展概念了解不够。


第二个月


上个月期盼的奖金到账了,有些庆幸,又有些失落。庆幸的是收到一笔不菲的补偿金,失落的是下月开始就没有收入了。

发现面试机会变少了,整月才面了三四家,这个月发现的问题,更多的是从架构角度来的,诸如幂等、一致性hash等场景,个人了解的相对简单了。


第三个月


广深的工作机会实在是少,开始同时投递其他城市的岗位试水。月初一家公司现场面了4轮都很顺利,第二天最后一轮CTO面,被嘲讽之前业务简单,比较受打击。月底面其他城市的岗位,一面过后第二天晚上10点又被拉上线做一面的补充面。

开始焦虑了,一想到还没找到工作,补偿金估计一两个月也会花完,可能要动用积蓄了,心跳就加速,越想越加速。努力想让自己变得不去想,只去想没有掌握的知识点,算是熬过了这个月。


第四个月


这个月,感觉蛮顺利,月初面一家大厂,技术面、主管面、HR面、提交资料都很顺利,感觉稳了,每天都看看公众号的面试状态,希望能快点沟通offer;月中也走完了一家中厂的4轮面试流程;月底又走完了另一家新能源车企的面试流程。

整个月过完,自己感觉飘了,感觉同时手握3个offer机会,晚几天随便一家给offer call就去了。个人心态一下子就变了,月内简历几乎没怎么投了,看知识点好像也没那么认真了。


第五、第六个月


好吧,上个月的3个机会,全都没有等来,继续面试。心态有点躺平,焦虑感少了,颓废感来了,BOSS直聘岗位几乎能投的都投过了,没有面试的日子,会过得略显浑浑噩噩,不知道要做什么。
陆续来了几个offer,也终于决定下来了,降薪差不多40%,但好在稳定性应该有保障。


心态的转变



  • 从渴望周末,到期盼工作日


    工作时渴望周末的休息 ,没找到工作时,每一个周末的到来,都意味着本周没有结果,而过完周末,意味着过完了1/4月。感觉日子过得好快,以前按天过,现在按周过,半年时间感觉也只是弹指一挥间。

    每一个周一的到来,意味着拥抱新的机会。每周的面试频率比较高时,会感到更充实;面试频率低下来时,焦虑感会时不时的涌上心头,具体表现是狂刷招聘软件,尝试多投递几个职位。


  • 肯定 -> 否定 -> 肯定


    找工作初期,信心满满。定制计划,每天刷多少题,每天看什么知识点,应该按照什么节奏投递简历,自己全都规划好了

    中期,备受打击,总有答不上来的问题,有些之前看过的知识点,临场也会突然忘记,感觉太糟糕了。

    后期,受的打击多了,自己不会的越来越少,信心又回来了



可能能解决你的问题


要不要和家里人说


自己这半年下来,没有和家里人说,每周还是固定时间给家里打电话,为了模拟之前在路边遛弯打电话,每次电话都会坐在阳台。

个人情况是家在北方,本人在南方,和爸妈说了只能徒增他们的焦虑,所以我先瞒着了。


被裁员,是你的问题吗?


在找工作的初期,总会这样问自己,是不是自己选错了行业,是不是自己不该跳槽,会陷入一种自责的懊恼情绪。请记住,你没有任何问题,你被裁员是公司的损失,你不需要为此担责,你需要做的是让自己更强,不管是心理、身体还是技术。


用什么招聘软件


我用了BOSS直聘和猎聘两个,建议准备好了的话,可以再多搞几个平台海投。另外需要注意几点:



  1. 招聘者很久没上线,对应岗位应该是不招的

  2. 猎聘官方会不定期打电话推荐岗位,个人感觉像是完成打电话KPI,打完电话或加完微信后就没有后续跟进消息了

  3. 你看岗位信息,招聘者能看到你的查看记录,如果对某个岗位感兴趣,怕忘记JD要求,可以截图保存,避免暴露特别感兴趣的想法被压价


在哪复习


除非你已经有在家里持续专注学习的习惯,否则不管你有没有自己的书房,建议还是去找一个自习室图书馆,在安静的氛围中,你会更加高效、更加专注。

如果只能在家里复习,那么远离你的手机,把手机放到其他房间,并确保有电话你能听到,玩手机会耗费你的专注力和执行力。

(你在深圳的话,可以试试 南山书房 ,在公众号可以预约免费自习室,一次两小时)


如何度过很丧的阶段


多多少少都会有非常沮丧的阶段,可能是心仪的offer最终没有拿到手,可能是某些知识点掌握不牢的自我批判。

沮丧需要一个发泄的出口,可以保持运动习惯,比如日常爬楼梯、跑步等,一场大汗淋漓后,又是一个打满鸡血积极向上的你。

不要总在家待着,要想办法出门,多建立与社会的联系,社会在一直进步,你也不能落下。


一些建议


1. 项目经历


讲清楚几点:



  • 项目背景


    让人明白项目解决了什么问题,大概是怎么流转的,如果做的比较垂类,还需要用通俗易懂的话表达项目中的各个模块。


  • 你在其中参与的角色


    除了开发之外,是否还承担了运维、项目管理等职责,分别做了什么


  • 取得的成果


    你的高光时刻,比如解决了线上内存泄漏问题、消息堆积问题、提升了多少QPS等,通常这些亮点会被拿出来单独问,所以成果相关的延展问题也需要提前想好



还比较重要的是,通过项目介绍,引导面试官的问题走向,面试只通过几十分钟的时间来对你做出评价,其实不够客观,你需要做的是在这几十分钟的时间内尽可能的放大你的优势



除此之外,还需要做项目的延展思考



比如我自己,刚工作时做客户端开发,负责客户端埋点模块的重构,面试时被问到,“如果让你设计一个埋点服务端系统,你会考虑哪些方面”? 对于这类问题,个人感觉需要在场景设计类题目下功夫,需要了解诸如秒杀抢购等场景的架构实现方案,以及方案解决的痛点问题,这类问题往往需要先提炼痛点问题,再针对痛点问题做优化。


2. 知识点建议


推荐两个知识点网站,基本能涵盖80%的面试知识点,通读后基本能实现知识点体系化

常用八股 -- JavaGuide

操作系统、网络、MYSQL、Redis -- 小林coding


知识成体系,做思维导图进行知识记忆

那么多知识点,你是不可能全都记全的,部分知识点即使滚瓜烂熟了,半个月后基本也就忘光了。让自己的知识点成框架、成体系,比如Redis的哨兵模式是怎么做的,就需要了解到因为要确保更高的可用性,引入了主备模式,而主备模式不能自动进行故障切换,所以引入了哨兵模式做故障切换。

不要主观认为某个知识点不会被问到

不要跳过任何一个知识点,不能一味的把认为不重要的知识点往后放,因为放着放着可能就不会去看了。建议对于此类知识点,先做一个略读,做到心中大概有数,细节不必了解很清楚,之后有空再对细节查漏补缺。

之前看到SPI章节,本能认为不太重要,于是直接略过,面试中果然被问到(打破双亲委派模型的方式之一),回过头再去看,感觉其实不难,别畏惧任何一个知识点。

理论结合实践

不能只背理论,需要结合实践,能实践的实践,不能实践的最好也看看别人的实现过程。

比如线程顺序打印,看知识点你能知道可以使用join、wait/notify、condition、单线程池等方式完成,但如果面试突然让你写,对于api不熟可能还是写不出。

又比如一些大型系统的搭建,假如是K8S,你自己的机器或者云服务器没有足够的资源支撑一整套系统的搭建,那么建议找一篇别人操作的博客细品。

不要强关联知识点

被面试官问到一些具体问题,不要强行回答知识点,可能考察的是一个线上维护经验,此时答知识点可能给面试官带来一个理论帝,实操经验弱的感觉。

举两个例子,被问过线上环境出问题了,第一时间要如何处理?,本能的想到去看告警、基于链路排查工具排查是哪个环节出了问题,但实际面试官想得到的答案是版本回滚,第一时间排查出问题前做了什么更新动作,并做相应动作的回滚;又被问过你调用第三方服务失败了,如何本地排查问题?,面试官想考察的是telnet命令,因为之前出现过网络环境切换使用不同hosts配置,自己回答的是查看DNS等问题,这个问题问的并不漂亮,但是也反映出强关联知识点的问题。

建立自己的博客,并长期更新

养成写博客的习惯,记录自己日常遇到的问题,日常的感受,对于某些知识点的深度解析等。面试的几十分钟,你的耐心,你解决问题的能力,没办法完全展示,这时候甩出一个持续更新的博客,一定是很好的加分项。同时当你回顾时,也是你留下的积累和痕迹。



半年很长,但放在一生来看却又很短

不管环境怎样,希望你始终向前,披荆斩棘

如果你也正在经历这个阶段,希望你早日上岸



作者:雪梨酒色托帕石
来源:juejin.cn/post/7274229908314308666
收起阅读 »

泰国游记:声色张扬的奇妙之旅

囧途开始 四月初的时候,阿宇问我:“老三,五一去泰国不?” 其实那一阵状态“水深火热”,没什么太多玩的心思,但是一想到小卡拉米一把年纪了,还没出过国,这次不去,下次又不知道什么时候了。 “去他妈的,整!” 订好机票,简单做个攻略。四月三十号,上海下着雨,浦东机...
继续阅读 »

囧途开始


四月初的时候,阿宇问我:“老三,五一去泰国不?”


其实那一阵状态“水深火热”,没什么太多玩的心思,但是一想到小卡拉米一把年纪了,还没出过国,这次不去,下次又不知道什么时候了。


“去他妈的,整!”


订好机票,简单做个攻略。四月三十号,上海下着雨,浦东机场,还有几分凉意,但是此时的我,已经开始畅想那头的曼谷有多么火热。


万万没想到,飞机延误!延了三次!原本五点开的飞机,到了差不多八点,还没动静,而且最后一次航司没有给出任何官方通知和说明。我心想:“今天不会走不了,要打道回府吧?”


飞机延误


正忐忑间,通知飞机起飞!哦吼,晚了三个多小时,但是终于飞了!望着上海的的灯光渐渐模糊,这下终于要曼谷见了。


上海出发


当天的气候不太友好,航程中,经历了三次颠簸。到了凌晨,终于抵达了曼谷,按照原定计划,找到接机司机,直奔芭提雅。


飞机到达曼谷


在去的路上,我们讨论了一下要不要订酒店,合计了一下,到了那先整点夜市小烧烤,再马杀鸡一下解解乏,酒店这事,不着急。


经历了两个多小时的路程,终于到达了芭提雅,这时候发现,我们去的那片地方,怎么都是灯火灰暗,说好的夜市小烧烤呢?哥俩累了,要不先把酒店顶了,携程再一看,明明曼谷看还有很多房间,这回附近的酒店都满了。要不先找个马杀鸡店?哥俩茫然地转在街上,芭提雅的热浪、下水道的恶臭,冲面而来,转了一圈,发现马杀鸡店也都打烊了,哥俩大眼瞪小眼,一时相对无言。


携程是拉了,要不试试美团吧?抱着试一试的态度,用美团搜了一下,哎,发现几百米远的一个酒店有房间,订!顺着高德,一路导航了过去。进了一个疑似前台的大厅,结果一看,没有人,什么情况?


疑似的酒店大堂


赶紧打电话问问,电话打通,用我多年来一直稳定磕碜的英语,开始磕磕巴巴地咨询:


“We we… book en… your room on meituan er website,we en are in the the the…hotel, but there is no body.”


对面也回复了一堆话,英语也不是很好,当然主要是我听力也差,只听懂了一点点:


“Do you in the hotel?”


"Ah yes."


……


又鸡同鸭讲了一阵,阿宇又接过去掰扯了一会,还是没掰扯明白。


我接过来,只能说,一时无言,“We need help.”,她也想帮我,但她回复的我都听不懂,唉,挂了吧,哥俩这个酒店看样子是住不上了。


我看了眼大堂的沙发,心想,要不哥俩就在这凑合一宿得了。结果,接电话的那个前台大姐找过来了!


Madam,你就是我们的光!


原来酒店的前台就在对面,我还埋怨高德又缺德了,导错了地方,结果领完房卡,发现住房还真差不多真就在高德导航到的位置——我也知道为什么前台大姐会知道要出来找我们,看起来不是第一次了。


此时,已经是凌晨三点了,洗漱洗漱,躺在床上,凌晨三点半。


再规划规划第二天的行程,订订酒店,凌晨四点,奔波了大半天的三某,终于睡了。


住的房间


第二天不太早,老三在一阵饿意中醒来,得出去找点吃的,不知道吃什么,找个711吧,去的途中,发现有一条小路,看着是通向海边,先去海边看看吧,结果路边的小棚子里,跳出了三只狗子,一阵狂吠,奔着我而来,没感受到芭提雅人的热情,先感受到了芭提雅狗的”热情“,我随手抄起一根棍子,今天非得让你们尝点中国的颜色。


——狗主人出来了,我扔掉棍子,算了,今天先放你们一马,以后好好做狗。


小路


随便吃点东西,回去准备换酒店,我突然反应过来,我们看了半天地图,要住海边,要离今天去的真理寺近,选来选去,阿宇重新订的,不就是现在这家吗?我俩真是热昏了头,这还不如直接续呢,瞅瞅这破酒店,哪有海景?


换房间吧,先住着,放下东西,到了前台,正在办手续,我往外面瞟一眼,大海!这个酒店还真的是海景酒店!酒店的楼下,泳池,海滩,在往侧前方一看,真理寺也赫然在目——芭提雅的热辣假期要开始了!


酒店泳池


沙滩和真理寺


芭提雅


海滩午餐


一天没怎么正经吃饭的老三和阿宇,决定中午得找个餐厅好好吃一顿,刷了会大众点评,本来想去附近的一个海鲜市场,但是阿宇不能吃海鲜,找了另外一家评分不错的餐厅Surf & Turf Beach Club & Restaurant,这家餐厅在海边,吹着海风,看着大海和沙滩,用餐心情非常美妙。


海边餐厅


服务员阿姨不太会英文,对着菜单,像两个看图学字的小学生一样,我们点了饮料和餐,整体上还是没让人失望的。


菠萝炒饭色泽金黄,味道清甜,混杂着坚果、果干、肉松,口感非常丰富,我们一致认为,这是目前为止,吃过的最好吃的菠萝饭。


点的餐


冬阴功汤,一口下去——嗯,泰国味儿!酸辣鲜三种口味汇聚,咖喱和辣椒的辣,柠檬的酸,在口腔里交相奏响,互相融合。


点之前,服务员阿姨特意提示了我“辣”,我还忐忑了一下,结果发现这种辣和我想象的辣不一样。怎么形容呢,如果说国内的菜,比如川菜的辣是一种干燥的辣,这里的辣是一种湿润的辣。


路边马杀鸡


来了泰国,怎么能不体验一下“马杀鸡”呢?吃过午饭,天气正热,下午的行程还早,穿过烫脚的街头,找到了路边的一家按摩店。


价格看起来很合算,按!我选了肩背按摩。


路边按摩店


先洗个脚——咱这也算两只脚踏进洗脚界了。换好衣服,趴在塌板上,来自中国的面团,要被泰国的师傅揉搓了。


洗脚


先刷油,精油滴在背上,有一点辛辣的感觉,让我想起了以前尝试用过的泰王油。上好油,开始反复揉搓,师傅的手、肘、膝都是这次“白案面点制作”的工具。


和中式按摩相比,泰式按摩要轻柔很多,这种轻柔是恰到好处的轻柔,既能感受到力量的渗透,又不会感觉到很痛。最后,师傅拉扯一下手指,就像是把面团给拉长,结束了这次下厨。打开店门,走进芭提雅滚烫的天气离,中国的面团”下锅“了。


真理寺


下午去了一片海滩之隔的真理寺,直线距离很近,但是真要去的话,得绕很长一段路。


真理寺外景


进入真理寺之前,工作人员给我们发了顶安全帽,去过很多景点,真理寺是第一个需要戴安全帽参观的。为什么呢?因为真理寺还在施工。


真理寺始建于1981年,历经多年建设,至今仍然没有完工。它是纯木制结构,所有的构造都采用卡榫结构,雕塑都是纯手工打造,虽然它被称之为寺,但其实不是一座寺庙,而是一件艺术品。


真理寺正在施工中的雕刻师傅


真理寺的修建耗时弥久,似乎不知道它到底会什么时候结束,但是和真正的真理相比,又不一样,毕竟真理寺总有一天修完,但是真理是无穷无尽的。


真理寺的小奶猫


环绕真理寺的时候,在一个台阶上看到两只稚嫩的小奶猫正在打闹,真理是有生命力的,这两只小猫比我们离”真理“更近。


真理寺和花与海


真理寺在一座宁静的海滩,从一汪孔洞看过去,一枝白花,开在绿的大海旁,远处的天是蓝的,光洒进来,滴在老的木头上。


真理寺内的雕塑风格各异,能看出来一些佛教和印度教的风格,我们参观的时候,刚好有一个旅游团,蹭到了一点导游的解说,导游说到:”……观世音菩萨……孔子“,这两个熟悉的名字引起了我的注意,仔细一看,还真有这样的雕塑,真理寺真是一个多国家、多宗教的文化汇聚地。


真理寺内景


在真理寺还看到了大象,不过这大象驮着人,看起来很温顺的样子,希望以后有机会能看看野生的大象吧。


真理寺驼人的大象


芭提雅落日


在真理寺结束地比较早,天气又比较热,回到了我们的海景酒店休息,下了水,在蓝色的泳池里休息扑腾了一会。


天色渐晚,太阳没那么晒人,躺在沙滩的躺椅上,喝口冰凉的啤酒,吃点水果,吹着海风,看着太阳慢慢变成橘色。


躺在海边


没忍住下海泡了一会,在温热的海水里,看着太阳慢慢落到海平面之下,这一刻,感到格外松弛。


日落


日落真理寺


蒂芬妮人妖秀


日落不是芭提雅一天生活的结束,夜晚才刚刚开始。


来了芭提雅,怎么能不看人妖秀,芭提雅知名度最高的是蒂芬妮人妖秀。蒂芬妮的场馆,白色的礼堂风格,喷泉闪着不同的颜色。


芬妮人妖秀场馆


蒂芬妮秀禁止摄影,所以只拍了开幕和谢幕。抛开猎奇的成分,单纯从表演的角度来看,我觉得蒂芬妮秀的艺术水平很高。


蒂芬妮秀开场


演员都称得上是端庄典雅,尤其是每场得领舞,我不知道这是不是就是所谓得”人妖皇后“,身材笔挺、面容姣好,超模的风范、专业的歌舞水平。


演出的节目,风格形式也非常多样,歌舞剧、独舞、现代舞、民族舞……全场大概有三十个节目,每个节目都有不一样的特点。很多节目看得出来,是迎合了游客的审美,包含了不同国家的歌舞形式,泰国、美国、中国、印度、越南、朝鲜……


我印象里特别深刻的节目有两个。


一个是“菊花台”,当《菊花台》的音乐响起,演员表演起了中国风的舞蹈,整个剧院充满了中国观众的欢呼,剧院是懂怎么撩拨观众的。


另外一个,也是全场印象最深刻的一个,是“对唱”,“男演员”戴礼帽,穿礼服,声音清澈,灯光熄灭,“女演员”迅速登场,声音妩媚。


我最开始以为这是两个演员,切换的时候,一个演员钻到幕布后,另外一个演员钻出来。瞪大眼睛,我想看看到底怎么换的,不对——这好像是一个演员!


灯光打开,谜底揭晓,果然是一个演员,左边身子穿着男礼服,右边身子穿着女礼服,左面示人,男声开唱,右边示人,女声开唱。


演员快速左右转身,男女声切换自如,全场充满了欢呼。


这位表演者唱什么我没听懂,但听出来了情绪非常激亢,甚至挺出来了愤怒。“人妖”这个群体,对自己的认同到底是男还是女呢?是向左,还认为自己是男,还是向右,认为自己是女?或者正面对人,雌雄莫辨?


来泰国的路上,我看了一些书,我以前以为人妖在泰国有悠久的历史,后来发现,”人妖“是现代的产物。


芭提雅曾经只是个小渔村,为什么后来逐渐能逐渐成为度假胜地呢?因为美国来过,确切说美国海军陆战队来过。二战后,泰国倒向美国,成为美国反共的桥头堡,芭提雅就曾经是美国海军陆战队的驻地,它也因此而兴。


人妖同样也因此而来,驻扎的美国大兵给芭提雅的市场带来了消费,当然也包括成人市场,因为汇率物价等等方面的差异,美国大兵指甲缝里漏出的一点油,都可能会改变一个泰国的贫穷家庭。有些家庭,家里只有儿子,穷得实在受不了了怎么办呢?如果儿子长的还算清秀,那就牙一咬,送去做变性手术,去挣美国大兵的钱,没想到有些美国大兵还挺喜欢,因为不用担心怀孕。


富可能使人变坏,穷可能使人变态。人妖的来源,算是穷的没办法的办法。


蒂芬妮秀落幕


蒂芬妮秀落幕后,演员会站在小广场上,招徕游客合影,一次100泰铢。歌舞秀中的艺术感,终究还是变成生活的真实感,这不是艺术,这是生意。


等待合影的演员


这是一条不能回头的路,演员得在年轻的时候尽可能挣到养老的钱——如果有老可养的话,因为注射激素过多,通常很难长寿。


到了三十岁以后,雄性激素的分泌难以抑制,TA们的男性特征会越来越明显,难以遮掩,TA们也会被老板无情地抛弃。我在蒂芬妮的前台,看到售货员,化妆、穿裙子、踩高跟,但是男性特征已经无法遮掩,检票员也是如此,剧场的一个内场管理员,留了美式的油头,乍一看是一个“小哥”,仔细一看也发现步态和声音不太对劲。


整个蒂芬妮秀才几个工作人员,其他人都去哪了呢?而且,蒂芬妮的演员,绝对都是百里挑一的,那剩下的九十九呢?曲终人落幕,台上永远会有人,谁会知道离开台子的人呢。


风俗步行街


芭提雅的夜生活,还有一个地方,是一定要去看一看的,那就是风月步行街。坐着双条车,穿行在中滩天海滩旁的长街,风月步行街到了。


坐双条车


刚到街口,五色的灯光,喧闹的人声,嘈杂的音乐铺面而来。


风月步行街的街口


街边的夜店灯红酒绿,街边的女郎穿着清凉性感,招徕客人的皮条客穿梭其间。


风月步行街


皮条客和街边女郎


路上的行人来来往往,有的走着走着,脚就不听使唤地就拐进了街边的一家店,成为喝着、唱着、跳着的人群中的一员。


夜店里的人群


——欲望在这条街上流淌。


真是万恶的资本主义啊!唉,怎么这就到头了,阿宇,你拉我回去干什么?


格兰岛


如果想在芭提雅体验一下热带小岛风情,那么格兰岛是个不错的选择。


从芭提雅的码头登船,大概过了半个小时,在海船的摇晃中,一座小岛渐渐映入眼帘,青的山下,红瓦的房子,和蓝的海界限分明。


抵达格兰岛


在燥热的风里,登上Tawaen观景点,浅蓝色的天空下,白色的沙滩渐渐变得湿润,绿色的海,渐渐变成深深的墨绿。


Tawaen观景点看海


下到Tawaen海滩,满是戏水的游客,男男女女,各个国家。


Tawaen海滩的游客


Tawaen的水上项目很多,我选择了4个项目,摩托艇+香蕉船+浮潜+滑翔伞。


摩托艇,之前在秦皇岛试过,这里再试一次,感觉是海水更清,浪更大,不到一会,全身上下都湿透了。


看到了香蕉船,那必须尝试一下,为什么呢?因为想起了NBA的”香蕉船兄弟“,对了,最后带完香蕉船的小哥,一定会推荐玩一下”翻船“,拉着香蕉船的快艇,最后会控制让香蕉船翻掉,所有人都落到水里。


NBA香蕉船兄弟


可惜坐船的时候只有我一个人,如果“支付四狗”组合能齐聚,那大可Cosplay一下香蕉船兄弟,对了,我们也要翻船的。


香蕉船和湿透的我


试了下浮潜,体验一般。作为一个旱鸭子,只能在水里乱扑腾,一不小心,就是原地打陀螺,头埋进水里,满眼都是绿色,海水往耳朵里,顺着缝隙往口鼻里流。当然,如果会游泳,应该体验会很不错,同船会游泳的一些伙计,开心地都流连忘返。在会游泳之前,我应该是不会再玩这个项目了。


浮潜


最期待的的是滑翔伞,远远地看着,人像风筝一样在天上飘着。


远看滑翔伞


上了快艇,两位师傅都很皮,一个师傅话多、活泼,我们一上船,“中国人?”肯定之后,这位师傅放起了音乐《我们不一样》,放着放着他也跟着在船头唱了起来“我们不一样,每个人有不同的境遇…”唱着唱着还跳了起来。


唱跳的皮师傅


另一位师傅看起来稳重一点,安静地掌着舵。但是快艇开起来之后,就不是那么回事了,这位师傅喜欢开“快船”,快艇在海上速度拉满,还玩起了漂移。


很快,开始滑翔,第一位勇士上了天,小船上充满了欢呼声。第二位,第三位,第四…快乐戛然而止,船抛锚了。


两个师傅尝试了半天,都没能打响快艇,抛锚的小船,就像一块木板,在海浪中飘摇晃荡,我终于体会到什么叫“海上孤舟”,我们只是在海滩上,想想如果远洋抛锚,那该多么绝望。最后另外一艘快艇过来救了我们,拖着抛锚的快艇回了码头。


船拖船


又想起了皮师傅之前唱的歌:“我们不一样,我们的船会Done”……下次有机会再玩吧。


伴随着半斜的太阳,从码头乘船离开了格兰岛。看着身后的格兰岛越来越远,又回到了暮色中的芭提雅。


离开格兰岛


暮色中的芭提雅


芭提雅码头远眺


曼谷


大皇宫


来到曼谷的第一站,直奔大皇宫,大皇宫在泰国的地位,就相当于中国的故宫,去了曼谷不去大皇宫,就像是去了北京不去故宫。


大皇宫外景


但是去的时候不赶巧,大皇宫似乎在接待什么团体,暂时不开放。第二天再去大皇宫,终于开放了,而且免费。


大皇宫的殿宇


一座大皇宫,一部泰国近代史。


泰国的历史不算悠久,它有四个王朝,素可泰王朝、阿瑜陀耶王朝、吞武里王朝、曼谷王朝,大皇宫就是曼谷王朝的皇宫,也称作拉玛王朝,在现在的泰国仍然延续,目前在位的是拉玛十世。


现任泰王-拉玛十世


拉玛王朝创立于公元1782年,此时的中国处于清朝乾隆年间,拉玛一世初建大皇宫,后来历代泰王逐渐完善。


大皇宫舍利塔


大皇宫的建筑风格很混搭,既有传统的东南亚风格,白墙红瓦,也有宗教元素,金灿灿的舍利塔,华丽的佛堂。西洋风格同样也非常浓厚,很多建筑都被珐琅点缀,有些建筑完全是欧式风格。


这是因为大皇宫后来的完善,同样伴随着拉玛王朝的动荡与变革,资本主义殖民浪潮兴起,泰国虽然没有沦为殖民地,但是却沦为了半殖民地。和宗主国清国被人用坚船利炮打开国门不一样,从拉玛四世开始,泰国主动打开了国门。


大皇宫一角


拉玛四世主动派遣留学生到欧洲学习军事政治法律,并对国内的政治经济进行了改革。拉玛四世的继任者,也就是著名的朱拉隆功大帝,是个更激进的改革者,少年即位的朱拉隆功,组织了一些王室和贵族青年,自称为“少年暹罗”,在“进步”和“改革”的主题下争辩和前进。


“少年暹罗”的意思,是把他们的对手暗喻为“老年暹罗”, 立志要把他们扫进故纸堆。朱拉隆功的改革是成功的,所以他成为泰国五大帝之一。


这跟大皇宫的建筑风格有什么关系呢?《泰国史》里写了这么一段:



在19世纪70年代,朱拉隆功建造了一座新的宫殿,采用意大利风格的设计,但是在顶部采用了暹罗式的屋顶,以取悦传统主义者。1907年,这种妥协就被抛弃了,国王修建了带有强烈古典主义风格的阿南达沙玛空御座厅(Ananta Samakhom throne hall),采用了卡雷拉大理石、米兰的花岗岩、德国的铜和维也纳的陶瓷。整座建筑成为通往旧城区北部的新的王室宫邸区的入口,那是一片欧式风格的宫殿群,以及为其他王室家族成员建造的宅邸。国王和贵族们进口了许多欧洲的小摆设来装点这些新房子。



泰国宫廷和欧洲联系越来越紧密,体现在建筑风格,就是有很多欧化的地方。对了,从《泰国通史》看到比较有意思的一个点,拉玛王朝的前四位国王都有中文名:郑华、郑佛、郑福、郑明,拉玛五世也就是朱拉隆功开始,没有中文名,朱拉隆功即位于1868年,此时的中国属于清朝的同治年间。


大皇宫一角


大皇宫也有血色,年轻的拉玛八世在大皇宫饮弹身亡,泰王是自杀还是他杀?行刺者是谁?已经成了历史谜案。拉玛八世的弟弟普密蓬·阿杜德即位,命运的改变总是这么突然,年轻的普密蓬应该是个热爱自由的人,在瑞士留学的时候,他因为飙车失去了一只眼睛。


街头供奉的普密蓬


如果没有兄长的遇刺,他也许会成为一个风流贵族,也许泰国会是另外一个样子。命运把他推到他该在的位置,他做的非常好,正如他的名字泰文的意思,“无与伦比的能力”,可以说是名副其实。


他影响了几乎整个泰国的现代史,军政府、民选政府……在每次政治危机的时候,普密蓬都会在恰当的时候出手,来保证政局的基本稳定。他亲近民众,是泰国民众非常热爱的”父亲“。


也正是由于拉玛八世的遇刺,王室认为大皇宫不详,搬到了其它地方,所以我们今天才有机会参观大皇宫。


郑王庙


郑王庙和大皇宫大概一水之隔,它纪念的是一个传奇国王——郑信大帝。


湄南河岸的郑王庙


郑信大帝的一生,是一个传奇,也如同一部跌宕起伏的戏剧。


他的前半生比起爽文不遑多让,郑信的父亲郑镛是潮州人,因为谋生,孤身来到泰国。郑镛从贩水果的小贩做起,逐渐发达,承包赌税,也籍着这个关系,郑信被过继给当时的财政大臣昭披耶却克里。从贫民到大亨,两代完成阶层晋升,郑镛已经足够励志,但是他的儿子更加传奇。


郑王庙塔


青年郑信,成为北方的一个城主。泰缅战争爆发,阿瑜陀耶王朝灭亡,属于郑信的舞台却搭建好了。郑信率兵赴京勤王,参与了出城出击,但是遭遇了失败,在撤退到城下的时候,发现已经被阻拦在外,进退无据之中,郑信毅然带兵突围,没想到阿瑜陀耶城亡,郑信生。


太阳照耀下的郑王庙


王朝乱世之中,郑信逐渐崛起,他先是在泰国南部站稳脚跟,又团结各方力量,挥师北上,收复阿瑜陀耶城。


1768年1月4日,郑信加冕为吞武里王。


郑信和吞武里王朝的落幕也很突然,阿瑜陀耶城发生骚动,郑信派遣大臣披耶讪镇压,披耶讪却被叛军说服,加入了叛军。此时吞武里空虚,叛军长驱直入,王宫卫队不支,郑信不得已退位,出家为僧。


郑王庙塔侧面


此时听闻国内政变的远征军主帅,郑信的女婿通銮,回国平乱,平乱之后的第二天,郑信身亡,死于紫檀木棍的击打之下——这是帝王的死法。


通銮,也就是拉玛一世,他让我想起了另外一个历史人物——司马懿,郑信出家为僧按照惯例可以免死,司马懿同样指洛水为誓,一定会放过曹爽——政治也许有时候不能讲道德吧。


对了,这位拉玛一世的中文名叫郑华,在对宗主国清国的国书中,他自称郑信的儿子。


郑信大帝的一生,在绝境中奋起,在最辉煌时戛然而止,让人不得不感概历史和命运的无常。


拳赛


来到泰国,我最大的愿望就是看一场泰拳赛,泰国有两大知名泰拳场,伦披尼和迦南隆,可以说是泰拳圣地,几乎所有泰拳手都梦想披上这两个拳场的金腰带。


伦披尼经历了一次搬迁,目前在曼谷郊区,迦南隆,在曼谷市区,在某些app上,被翻译成那差达慕,我一开始还吃惊,点评拳赛,竟然没有迦南隆,原来是这个翻译闹了乌龙。


这次我选择是更远的伦披尼,是因为周五,伦披尼和One冠军赛合作,有ONE周五格斗夜比赛。ONE冠军赛是是目前世界排名第二、东南亚排名第一的综合格斗赛事,当然,地处东南亚,ONE冠军赛自然也免不了泰拳比赛,甚至目前ONE的踢拳赛事同样开始迅猛发力。


能在泰拳两大圣地之一的伦披尼,看一场顶级的现代格斗赛事,实在是一件无比快乐的事情。


ONE格斗夜


我已经有六七年没到现场看比赛了,经历了两个小时的拥堵路程,我们终于赶到了伦披尼。


领票,进场,坐到座位上,音乐、灯光,现场的欢呼声,我感觉开始兴奋起来了———It's Time!


拳赛准备开场


开局的第一场比赛,日本选手VS白俄罗斯选手,日本选手有很浓重的空手道风格的意味,站立更强一些,几次打晃白俄罗斯选手,但是白俄罗斯选手的摔柔更胜一筹,在第三回后半段,裸绞终结了日本选手。第一场比赛就出现了终结,太兴奋了!


日本选手被终结


还有一场日本vs泰国的比赛,日本选手身高臂展更有优势,一度压制了泰国选手,肘法给泰国选手的脸开了一个大口子,泰国选手血流满面,苦苦支撑,我以为这场比赛就这么结束了,没想到泰国选手抓住一个漏洞,前手摆拳命中,日本选手重重倒地不起,赛事方赶紧用担架把日本选手抬走。我觉得这场是整个比赛最残酷的一场,赢的人血流满面,输的人担架抬走。


输赢皆惨烈


联合主赛同样精彩,一个年轻的黎巴嫩选手,挑战老牌的泰拳王,这个泰拳王应该是个明星选手,出场的时候全场欢呼,还现场表演了一段拜师舞,没想到场上局势风云突变,年轻选手,一开始就凶猛发力,老拳王显然有些慢热,没有进入节奏,第一回合就被击倒了两次,虽然最后依靠老道的经验,撑到了最后,但还是一致判定负。


现场还有很多精彩的比赛,现场看比赛和隔着屏幕看比赛,完全是不同的感觉,现场的欢呼声,选手的打击声,再好的摄影设备都制造不出来这样的临场感,去了现场,才能真正感受到现场的热烈!看比赛的时候,我完全兴奋起来了,呐喊,嘶吼,赛事结束后嗓子都是哑的。


只能说:过瘾,下次还看!


ONE巅峰赛


ONE格斗夜的赛事结束之后,我突然发现周六的早上还有一场比赛,ONE巅峰赛,这场比赛有一场金腰带的卫冕战,而且最重要的是,这一场有三位中国选手:胡勇、魏锐、张立鹏,两场MMA赛事,一场踢拳赛事。


尤其是魏锐VS秋元皓贵的这场比赛,让我想起了之前我非常喜欢的选手邱建良,带着前世界第一的势头,打入了ONE冠军赛,结果第一场被秋元皓贵阻击,至今还没有再次复出比赛。这场,毫无疑问,是一场恩怨局,这样的机会可遇不可求,我一定要来现场,一定要亲眼看到魏锐击败秋元皓贵。


晚上回到酒店已经差不多两点,我完全兴奋地睡不着,一早五点,我又起床到了伦披尼现场,这次现场摆放了一条金腰带,金腰带在前,谁能拒绝合影呢?


和金腰带的合影


开局两场速杀


首局终结获胜,在拳赛里是很少见的,终结一般发生在选手体能下降和伤害累积之后,但是今天的比赛,开场的两局都是首回合终结速胜。


今天比赛的第一场是泰拳比赛,在第一回合的后段,先是一个摆拳,打晃,接着一个击腹的直拳,选手痛苦倒地,这一拳应该是岔气了。


第二场比赛是无道服的巴西柔术比赛,日本选手上场之后,很快就被巴西选手压制,拿背裸绞一气呵成,几秒,裁判试了一下,日本选手胳膊已经软了,赶紧终止比赛。巴西选手在赛后抑制不住地哭泣,巴西柔术比赛,需要一点欣赏的门槛,所以纯巴西柔术选手很难接到商业比赛,很难赚到钱,平时生活很艰难,所以也能理解为什么巴西选手忍不住哭泣。


为了三位中国选手的比赛而来,加上状态不佳,中间的几场比赛,都没有上一场那么高的兴致。


胡勇被克制


终于来了,第一位出场的中国选手是胡勇,伴随着中文的出场音乐,全场响起了巨大的欢呼声,能在异国他乡,为本国的选手加油助威,这是一件多么令人激动的事!


现场的声音非常整齐,一位大哥自发担当了”领喊“的角色,他喊”胡勇“,现场的中国观众跟着一起喊”加油“,”胡勇……加油“的声音响彻整个场馆。


胡勇的脚下移动比较灵活,能看出,他以前应该是有散打的背景,他的站立比对手出色,开局几次重击对手。


对手随后调整了策略,坚决要和胡勇打地面,对手的摔柔水平在胡勇之上,每一次防守,胡勇都得拼尽全力,几次被拖入地面,被拿到了很深的把位,现场的中国观众心都揪了起来。


胡勇


最终三局战罢,胡勇点数落败。胡勇还很年轻,还有时间能继续完善和提高自己,加油!


魏锐稳健取胜


这是我最期待的比赛,魏锐是我特别佩服的一个选手,已过而立之年,家庭圆满,带的徒弟都成了冠军,他的职业生涯,武林风金腰带、勇士的荣耀金腰带、还有最具含金量的K1金腰带,功成名就,他已经不需要证明什么了。但是在职业生涯暮年,他还是选择了走出国门,挑战自己,继续冲击ONE的金腰带,这就是真正的武者意气!


魏锐出场


这场也是一场复仇局,对手秋元皓贵,曾经击败过邱建良,这次魏锐前来,也有为好兄弟复仇的意味。魏锐是是非常有名字的选手,他出场的时候,全场的欢呼像海啸一样。


魏锐迎击拳命中


伴随着同样整齐的”魏锐……加油“的声音,比赛开始。魏锐赛前说,针对秋元的技术特点,他已经找到了应对的办法,果然所言非虚。魏锐灵活的脚下移动,让秋元皓贵根本打不出他习惯的快速组合,到了第二回合,秋元皓贵甚至顶着伤害,硬往前压,给魏锐造成了一些麻烦,但是魏锐经验丰富、拳商很高,进行了调整,加强移动和迎击,整场比赛,基本上没见到秋元像往常一样的高速组合。


第三回合开始


就这样,三局战罢,在异国他乡的赛场上,魏锐一致判定击败了秋元皓贵,向着金腰带又迈进了一步。


获胜的魏锐身披国旗


裁判举起魏锐的手的时候,我满脑子和嘶吼的只有四个字:”魏锐牛逼!“


张立鹏憾负


张立鹏是老牌的MMA选手,曾经也打进过UFC,这次他遇到了不小的麻烦,他的对手赛前超重,协议之下,张立鹏选择了接下比赛。


果然,临场,对手的维度比张立鹏要大一号,在地面的缠斗中,张立鹏在力量上劣势太大,根本压不住对手,而且体能消耗地过快,到了第三回合,完全是强撑着打完,中间有一些好机会,因为体能不足都没有抓住。


张立鹏拿到把位但压制不住


最终,对手一致判定获胜。


张立鹏因为体能消耗过度,被轮椅推离现场,希望他能尽快恢复,早日拿下下一场胜利。


这场有个彩蛋,目前在UFC排名最高的中国男子选手宋亚东也来到了现场,作为张立鹏的边角,希望中国MMA的未来越来越好!


宋亚东来到现场


射击


来泰国,有第二件必做的事情,就是体验射击,因为没有提前预订,去不了最推荐的海军射击俱乐部,我选择了陆军俱乐部。


带着兴奋过后的眩晕,我来到了陆军射击俱乐部,下午两点开门,经历了一阵等待之后,领到了我的子弹,黄橙橙,亮晶晶。


领到了子弹


等待的时间,看着里面正在射击的游客,耳边传来了鞭炮一样的声音,震得耳朵嗡嗡的。


下午的射击场


终于轮到我了,我选择的套餐是三款手枪,教练帮忙上子弹上膛,我接枪瞄准,想起之前看的一点教程,身子往前压——后来看视频,嗯,怎么是勾着头的,好丑,以后有机会动作练漂亮一点。


设计中


开枪,手枪的后坐力不大,但是射击的时候,枪会往上跳,一开始,空靶了若干次,后来强打精神,努力瞄准,终于上靶了,甚至还蒙中了一发靶心。


调整射击姿势


这次射击体验呢,整体感觉是隔靴搔痒,瞄准、扣动扳机,多少感觉有点麻木,当然也可能和我此时头脑已经昏沉有关。


我买了五十发子弹,本来以为不少,打完却觉得意犹未尽。最后离开的时候,旁边的步枪靶场,一声巨响,我的耳朵瞬间轰鸣,回头一看,原来是有个大哥在打霰弹枪。


步枪靶场


没有哪个男人不爱枪,希望以后有机会能更深度地体验甚至学习射击。


感受


人生意义就是体验,我们回忆的时候,永远只会回忆自己体验过的东西,看一万个短视频,看一百本书,不如一次滚烫的沙滩来的记忆深刻。


泰国这个国家给我的感受,就是混杂,各种东西混杂,有寺庙的肃穆,有海岛的宁静、有拳赛的刺激,有红灯区的放荡…就像冬阴功汤,一下子把各种味道混杂在了一起。


在芭提雅和曼谷的街头,也能感受到泰国的贫富差距,大街时不时能看到几辆超跑,但更多的是坐在路边的乞丐,那天阿宇查了一下泰国王室的财富,他惊讶了一下,我也是——殿堂之下有杂草。


泰国很自由,但我觉得太过自由不是一件好事,大麻店、红灯区…这类声色刺激的东西,其实就像是表面能看到的纱布,底下不知道隐藏着多少溃烂的伤口,感谢我们的国家,社会主义铁拳,压得这些东西抬不了头。


最后,世界这么大,还是要去看看!




参考



  1. 《泰国常识》

  2. 《泰国通史》

  3. 《爱上泰国:你的色彩惊艳了我的时光》

  4. 《泰国史》

  5. 《泰国攻略》


作者:三分恶
来源:juejin.cn/post/7364785775345762356
收起阅读 »

怎么用一句话证明你在游戏公司里的最底层?

引言 今天在知乎看到一个有趣的帖子:如何一句话证明你在公司最底层?我们把范围缩小到游戏公司。 关于这个问题,身边80%的朋友描述了自己在公司底层的难忘回忆,还有几位朋友甚至因为这不堪的回忆破防了。 刚进入游戏公司的新人,迷茫是常态。和大家一样,笔者也曾是公司的...
继续阅读 »

如何一句话证明你在公司最底层?


引言


今天在知乎看到一个有趣的帖子:如何一句话证明你在公司最底层?我们把范围缩小到游戏公司。


关于这个问题,身边80%的朋友描述了自己在公司底层的难忘回忆,还有几位朋友甚至因为这不堪的回忆破防了。


刚进入游戏公司的新人,迷茫是常态。和大家一样,笔者也曾是公司的最底层,总觉得每天一睁眼就是各种困难的事等着我:



担心工作内容不会做,担心与同事沟通不好,担心自己考核不过关......



今天的这篇文章,大家一起来看看一位位于游戏公司底层的游戏开发者的最底层体验。


最底层体验


图片源于网络


1.介绍一下你自己


大家好,我是XXX,来自XXX。虽然我是一个新人,但我对游戏充满了热情,这种热情已经伴随我多年。小时候,我就沉迷于各种游戏,从那时起,我就梦想着有一天能够为创造令人陶醉的游戏世界做出贡献。我加入这个行业的目标是成为一个出色的游戏开发者,并参与创造令人惊叹的游戏体验。我相信,通过与这个行业的优秀人才一起工作,我可以不断成长,并为我们的团队和项目做出贡献。谢谢大家,请多多指教。


此处应有一阵热烈的掌声,那是对一位懵懂的游戏行业新人的勇敢表示敬畏。他或许不知道他的棱角将在这里被磨平。


熟悉又让人崩溃的弹窗


2.熟悉项目,体验游戏。


游戏行业新人刚进到游戏公司,可能第一件事就是登陆公司内部使用的通讯工具。你的直属上司可能早早的在网线那头等候着你的上线。


你好,XXX。你先接收一下这份文档,仔细阅读一下里面的内容。检出一下公司的游戏项目,然后根据文档把游戏跑起来。体验一下游戏,熟悉一下游戏的每个系统。有问题可以请教你旁边的那位大神,他负责带你。


好的,谢谢。由于在来公司之前做足了准备,检出项目、运行项目这种小问题肯定难不倒你。这时候你会惊讶,原来这就是大型的商业化游戏项目,看起来有那么点高大上,但是最多的是还是看不懂。不过这游戏玩着好无聊,不是我喜欢的类型。想到未来的日子里,需要不停地重复地在这个游戏里面遨游,"真的会谢"。



3.分配任务



  • 修改禅道bug序号XXX的问题。

  • 修改活动XXX文本显示异常问题。

  • 修改XXX报错问题,完成禅道单子序号1、2、3、4、5......。


游戏行业新人的入门任务往往就是这些看起来微不足道,但是却非常细节的问题。正所谓不积跬步无以至千里,通过慢慢处理这些小小的bug和显示异常的问题,无疑是熟悉项目的最好方式。虽然这些都是比较基本的内容,修改bug、调整UI、修复报错。但是能够体现一个新人的基本功:阅读问题描述、理解问题描述、定位问题所在系统、定位系统所在代码、读懂代码原有逻辑、修改错误代码、验证问题是否修复、思考会不会对其他内容造成影响。


这对于管理者来说是非常合理的,但对于新人来说,未免太过于简单了。


支线任务


4.支线任务


游戏行业新人入门有可能并不能第一时间接触到游戏项目主分支的代码,往往是参与其他的一些分支版本,例如审核服(专门为了应对平台审核员的审核搭建的游戏服)、版署服(用于申请版号专门搭建的游戏服)、海外服(主要负责多语言版本的语言提取、翻译替换、本地化处理)等等。


安排新人去处理这些支线任务,为的就是让新人从另外一个相对安全的分支去熟悉游戏项目,避免因新人的处理不当造成线上版本出问题,从而造成公司的经济损失。支线任务通常就是枯燥单一的体力劳动,不需要过多的技巧,只需要耐得住寂寞的心。


图片源于网络


5.几点下班


一位有着远大抱负的新人,往往在刚进入公司的日子里,不知道几点下班。领导分配给我的任务,实在太简单了,三两下就完成了,还不到规定的时间。为了能够更加快速地熟悉项目,参与游戏功能的开发,继续研究代码。


HR说19点下班,但是18点的时候大家都跑去吃饭,不解,跟着。等到19点的时候,果然没有人下班。继续奋笔疾书。20点的时候终于有人下班了,可是领导还是没动静,算了,再看看代码吧。21点,领导好像发现了这个新人,让他早点回去休息。(没有人告诉他,这将是常态。) "没事,我再看会代码,马上就回去了。"


手机先吃


6.福利


同事: “公司发月饼了,你没去领吗?”,“不知道啊,没人通知。我刚来几天。”


同事: ”我看大家都去领了,现在。“,”我不知道自己是否算正式员工“


同事: ”你先去看看吧,反正大家都在领。“,兴致冲冲地跑到发月饼的地方。


发月饼的: “叫什么名字?”,”XXX“


发月饼的: ”名单上没这个人,不能领!“


刚加入公司的时候,可能由于没转正或者名字还没有进入公司的名册,往往会导致有些福利不能享受。例如公司发月饼的时候,人人有份,唯独你。又或者公司发奖金,你拿200慰问金。公司发年终奖,你还是拿慰问金。 但是如果你想请假,领导秒批。甚至说你想离职,领导也是轻描淡写,“好的”。没有丝毫的牵挂留恋。这是前所未有的福利。


结语


不管怎样,虽然你是公司的最底层,但你是公司中最坚实的基石,因为你在每一颗砖石上都留下了你的汗水和努力,为了让整座大厦能够稳固地矗立在成功的巅峰。加油,请认真工作,积极向上。




作者:亿元程序员
来源:juejin.cn/post/7281589318329925689
收起阅读 »

从事程序媛工作的我都经历了什么?

第一段工作经历     大学一直就是学的Java,学校在湖南后面安排去浙江嘉兴实习做毕业设计项目的时候发现更加喜欢前端, 就想往这个方向去发展,2020年6月毕业就开始了我的前端求职之路,好在那个时候互联网对这块需求还是挺大的,以应届生的身份成功入职了一家小型...
继续阅读 »

第一段工作经历


    大学一直就是学的Java,学校在湖南后面安排去浙江嘉兴实习做毕业设计项目的时候发现更加喜欢前端,
就想往这个方向去发展,2020年6月毕业就开始了我的前端求职之路,好在那个时候互联网对这块需求还是挺大的,以应届生的身份成功入职了一家小型公司,面试也比较简单,更多的还是了解性格和学习方式以及自己对未来发展想法,也很顺利就入职了。


离职原因


    在那家公司大概做了半年,到了发展的瓶颈期,就两个前端,来来往往离职了三批人,就我还在原地,很多时候感觉学不到什么实际性的东西就果断离职了......


   我发现入职的第一家公司对自己的职业规划还是挺重要的,有人带你给予你学习的方向这点很重要,但可惜我没有这样的运气,基本都是靠自己在工作中摸索和自学试错成本也高,学校学的大部分东西在工作中基本都派不上用场,这也让我很苦恼,不过感觉大部分的人基本都是这样吧......


第二段工作经历


    第二家公司是一个大型的厂做晶导体和手机电脑电子产品,幸运的是,遇上了急需人手的时候,面试基本都能对答如流,他们对我也挺满意的,就破格让大专学历的我入职了,收到offer的时候还是很开心的,毕竟这样的机会少之又少......


    实际开发的时候整个IT开发团队都是分不同的组,我所在的组做的项目更多的是偏向公司内部的考勤、人事、薪资这类型的后台管理系统。项目用的vue2是基于vue-element-template后台模板进行二次开发,之前的老大会进行项目搭建给我们顺流程和定开发规则,让我根据规则来进行模块开发,这在某种程度上做了统一性也省了很多的麻烦更方便后期维护。


    熟悉了之后有时候也自己尝试从0开始搭建项目自己也学到了很多。其他的项目组更多的是跟流水线上的产品打交道,基本每天都在加班改需求事情太多流水线也不稳定,有bug的时候都要在公司守株待兔去解决这种突发情况,但是我几乎都是早上8.30上班到下午5.30就下班,加班的情况很少。


    上班有时候活多的时候就忙的键盘真的冒火,不忙的时候时间都属于自己,通过刷刷前端的视频,看看知乎和掘金之类的补充能量来度过这普通的一天......


嘉兴生活


    工作坐标嘉兴,是个很悠闲慢节奏适合养老的城市,不内卷,生活也没什么压力,消费也不高,过的很轻松,平时周末约上小姐妹出去玩玩逛逛街啥的,后面搬家换了个离公司近的地方感觉晚上阴森森的有点害怕,就养了一只蓝猫,现在已经快4岁了,性格超级好,每天等着我回家跟家人一样很温暖......搬家后通勤时间基本骑个共享电动车10分钟左右就能到,也没有啥特别大的变化,除了不包吃住,其他方面真的挺好的,同事也很好相处,后面还内推了一个姐妹来这边上班,刚好也是大学同学,我们就开始了合租的生活,她也有一只猫猫,就这样过上了两人两猫的生活,这样的生活持续了一年半左右...


离开浙江去深圳


    后面找男朋友了,算是大学同学,实习的时候分到一个班级做毕业设计项目,变成了同桌。那个时候我们还不熟,很少讲话。我比较高冷,我俩对话仅限于问问作业以及项目上的一些问题,直到毕业设计答辩完我们两个加起来的对话也不超过20句,加了个QQ也是为了方便传递老师布置的作业,偷懒直接抄他的作业罢了......


    2020年疫情居家期间,无聊就开始玩王者,突然看到他在线,就随便点了一下邀请,结果他同意了,这在我意料之外,然后就开始带游戏,慢慢熟悉起来了,这样的生活持续了差不多一年左右,算是暧昧期吧。2021的某一天,突然在QQ上跟我表白,我没准备好拒绝了!后面我们状态依旧持续这样,大概过了一两个月之后,我感觉我们之间状态还是没变化,后面就同意了在一起了。就这样开始了异地恋,我们除了五一和国庆放假会去到双方城市见面到处去玩,其他时候都在自己的城市做着忙碌着自己的工作,大部分也只能通过手机微信聊天去了解和关心对方。


    他跟我一样都是做前端,一开始他在中山工作,工作一段时间他感觉工资低就去深圳发展,在那边上班时间越来越久,通勤时间是一个小时左右,我们只有下班才有机会聊聊天也让我们格外珍惜,我每天都等他下班洗漱完就一块休息,久而久之习惯了,基本每天晚上都会开着语音一整晚不挂电话。后面下班越来越晚,异地了一年多,很多矛盾开始出现。就不太想继续异地恋,他就想让我去深圳发展,他觉得我们老家都在湖南,离深圳比较近,我想让他来浙江,他说离家太远了,家里有爷爷奶奶回去一趟不方便,后面拗不过,争执了半年,最后我妥协去了深圳......


深圳工作后续


    2022年7月份来的深圳,提前把猫从浙江托运到深圳,花了我800大洋。行李太多,打包了一堆包裹寄过去让他给我拿。跨越1600公里坐了8个小时的高铁,不远千里只为你而来!我当时想:“如果有个人能为我这样,我辜负全世界也绝对不能辜负他”。


    我男朋友提前一个半小时坐公交来高铁站接我,傻傻的在那等了我一个多小时。到站下车就快要见面的那段时间我心里好紧张,有点不知所措,他在出口等我,看到对方后,一时间双方都有点尴尬,不知道该说些什么......


    每次很久没见之后都会这样,但我还挺喜欢这种感觉,我称之为属于两人的“新鲜感”。他先开的口:"坐车辛苦了,累不累,饿不饿?”,我回了个还好,接过我手里的密码箱,知道我中午没吃饭,提前从车站下面的商圈打包了一份鱼粉。给我找了个能坐着的地方,递给我让我先吃,不知道怎么描述当时的场景又好笑又温馨,异地了一年多,终于奔现的感觉(~ ̄(OO) ̄)ブ。让我开心又有点陌生,之前都是短暂的相聚,所以这次跟以往的感觉都不太一样,打怪终于打到大BOSS了......


   他一直看着我吃,我有些不好意思,坐了一天车顾不上形象有些许狼狈。我叫他背对着我别老看,他就时不时偷瞄,一碗粉都能吃半个小时挺不可思议的~ 我偶尔也关注看着他,只是不敢直视他只是偷瞄,可能是害羞吧(✿◡‿◡)


   他穿着一个白衬衫牛仔裤白白净净的,那天的印象一直留存在我的脑海中,都说情人眼里出西施,不外乎这种吧,人活几个瞬间,喜欢也是,就是在某个时间里面在某种环境的衬托下刚好对上了眼刚好他的行为让你有喜欢和心动的感觉....


   吃了准备回去了,走着走着突然停下:“把手给我”,我挺惊讶的也挺开心的,感觉他的手大能直接把我两个手都包住,天气太热了,牵手热的出汗了也没放开过......后面不太熟悉高铁站,他带着我转了大半个小时,后面终于找到出口了,打了个出租车花了70多,真的颠覆了我的想象力,深圳消费确实是挺高的! 一路上看着深圳的风景对这个地方充满了好奇......


          持续更新中......


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

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

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

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


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


1.png


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


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


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


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



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

  2. 运行 package.json 中的脚本

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

  4. 内置 WebSocket 客户端

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

  6. 文件模式匹配功能


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



  1. V8 引擎升级至 12.4 版本

  2. Maglev 编译器默认启用

  3. 改进 AbortSignal 的创建性能


接下来开始介绍。


支持通过 require() 导入 ESM


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


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


// CommonJS

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

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

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


// ESM

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

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

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


// Node 22

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

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

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


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



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

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

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


运行package.json中的脚本


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


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

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


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


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


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


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


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


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


node --watch app.js

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


内置 WebSocket 客户端


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


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


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


用法示例:


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

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

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


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


用法如下:


const fs = require('fs');

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

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

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

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


文件模式匹配——glob 和 globSync


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


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


glob 函数(异步)


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


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

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

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



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

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


globSync 函数(同步)


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


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

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

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


使用场景


这两个函数适用于:



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

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

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


V8 引擎升级至 12.4 版本


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



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

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

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


Maglev 编译器默认启用


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


改进AbortSignal的创建性能


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


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


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

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

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

总结


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


3.jpeg


关于我


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


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




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

2万块钱买平板:苹果新一代iPad Pro直接上M4芯片,最强也最贵

万众瞩目的苹果 M4 芯片,刚刚在新一代的 iPad Pro 上亮相了。 北京时间 5 月 7 日晚 10 点,苹果举行了春季新品发布特别活动。这次活动发布了新款 iPad(Air 和 Pro)以及新一代 Apple Pencil 和妙控键盘配件等新品。 ...
继续阅读 »

万众瞩目的苹果 M4 芯片,刚刚在新一代的 iPad Pro 上亮相了。



图片


北京时间 5 月 7 日晚 10 点,苹果举行了春季新品发布特别活动。这次活动发布了新款 iPad(Air 和 Pro)以及新一代 Apple Pencil 和妙控键盘配件等新品。


当然,新一代 iPad Pro 是此次发布会上大家关注的重点,尤其是它所搭载的芯片。


苹果没有让大家失望,为 iPad Pro 装上了自家新一代的 M4 芯片,这也是该芯片的首次亮相。


「你可能会认为我们会使用极其强大的 M3 芯片,但我们跨越到下一代 ——M4 芯片。」


也就是说全新 iPad Pro 搭载的芯片直接从 M2 跳到了 M4。


下面这张图,可以说是用一图总结了 M4 芯片的强大性能。


图片


展开来说,全新 M4 芯片由 280 亿晶体管组成,基于第二代 3nm 技术打造,并在 CPU、GPU 和 NPU 方面迎来一系列提升。


全新 10 核心 CPU


在 CPU 方面,M4 拥有 10 核心 CPU,包含 4 个性能核心和 6 个能效核心。下一代核心改进了分支预测功能,为性能核心提供更广泛的解码和执行引擎,为能效核心提供更深层次执行引擎。


此外,性能核心和能效核心还具有增强的下一代机器学习(ML)加速器。


图片


与前代 iPad Pro 搭载的 M2 相比,M4 的 CPU 性能提升了 50%


图片


因此,无论是在 Logic Pro 中处理复杂的管弦乐文件,还是在 LumaFusion 中向 4K 视频添加高要求的效果,M4 都能提高整个专业工作流程的性能。


图片


10 核心 GPU


其次,GPU 部分。M4 的全新 10 核心 GPU 建立在 M3 系列芯片的新一代图形架构之上。它具有动态缓存功能,这是苹果的一项创新,可以在硬件中实时动态分配本地内存,从而显著提高 GPU 的平均利用率。


图片


此次,M4 芯片提高了专业应用程序以及游戏方面的性能。苹果表示,这是硬件加速光线追踪首次登陆 iPad,在游戏等体验中实现更真实的阴影和反射。


图片


《暗黑破坏神:不朽》游戏。


硬件加速的网格着色也内置于 GPU 中,可以提供更强大的几何处理能力和效率。相比之下,M4 芯片专业渲染性能得到了巨大提升,是 M2 芯片速度的四倍


图片


图片专业渲染软件 Octane。


在能耗方面,M4 只需一半的功耗即可提供与 M2 相同的性能。即使与轻薄笔记本电脑中最新的 PC 芯片相比,M4 只需四分之一的功耗即可提供相同的性能。


全新显示引擎


图片


M4 采用了开创性技术加持的全新显示引擎,实现了 Ultra Retina XDR 的精度、色彩准确度和亮度均匀性,这是一种结合两个 OLED 面板的光线创建的最先进的显示屏。


图片


最强大的神经引擎


M4 的神经引擎采用 16 核心设计,使得芯片更快、性能更强。


苹果表示,M4 拥有苹果有史以来最强大的神经引擎,每秒能够执行惊人的 38 万亿次操作,是 A11 Bionic 中的第一代神经引擎速度的 60 倍


图片


图片


神经引擎与 CPU 中的下一代机器学习加速器、高性能 GPU 和更高带宽的统一内存一起,使 M4 成为一款极其强大的 AI 芯片。


借助 iPadOS 中的 AI 功能(例如用于实时音频字幕的 Live Captions 以及识别视频和照片中目标的 Visual Look Up),新款 iPad Pro 允许用户在设备上快速完成令人惊叹的 AI 任务。


在苹果的展示中,配备 M4 的 iPad Pro 只需轻按一下,即可轻松将 Final Cut Pro 中 4K 视频的主题与背景分离。


图片


苹果表示,M4 中的神经引擎比当今任何 AI PC 中的神经处理单元都更强大


自 2017 年以来,苹果所有的芯片都包含了某种版本的神经引擎,尽管到目前为止,这些芯片主要用于增强和分类照片、光学字符识别、离线听写和其他事情。但苹果可能需要更快的东西来支持端侧以大型语言模型为核心的生成式人工智能,苹果预计将在下个月的 WWDC 上在 iOS 和 iPadOS 18 上推出这种人工智能。


从往年来看,M1 和 M2 之间的等待以及 M2 和 M3 之间的等待期都是一年半左右。由于苹果公布的技术细节很少,很难知道 M3 和 M4 之间更快的转变是什么原因。可能是 M3 落后于计划,而 M4 准时或提前;也有可能 M4 只是对 M3 进行了相对温和的架构更新。这需要拿到后续测试结果才能判断。


M4 版 iPad Pro:8999 元起


除了所搭载的芯片,苹果发布会上还介绍了新款 iPad 的其他细节。


新款 iPad Air 分为 11 英寸和 13 英寸两个版本,搭载 M2 芯片,支持 Wi-Fi 6E(可以选择支持 5G 的型号),最大存储空间 1TB,比搭载 M1 芯片的 iPad Air 快 50%,但显示屏仍然是 LED 显示屏。售价方面:11 英寸机型 4799 元起,13 英寸 6499 元起。


图片


新款 iPad Pro 同样分为 11 英寸和 13 英寸两个版本,采用了双层串联 OLED 屏幕,亮度更高,色彩显示更精准。其全屏亮度可以达到 1000 尼特,峰值亮度达到 1600 尼特,苹果称其为「超精视网膜 XDR 显示屏」。


售价方面,11 英寸机型 8999 元起,13 英寸 11499 元起。但如果想要更高的配置,预算会一路飙升。13 英寸 2TB 顶配达到了 19999 元。如果选择纳米纹理玻璃,售价将达到 20799 元。这可能是 iPad 史上最贵的机型。


图片


这款新品 5 月 9 日上午 9 点接受订购,5 月 15 日发售。你准备入手吗? 


参考链接


http://www.apple.com/newsroom/20…


arstechnica.com/apple/2024/…




作者:机器之心
来源:juejin.cn/post/7366149991159250954
收起阅读 »

房车用了两年多,这个油耗我是没有想到的

2021年12月买了一台基于大同V80改装的B型小房车,转眼已过两年半的时间,在这两年半的时间里,总行程达四万多公里,带我们走了许许多多的地方,看了祖国的山川湖海,秀美风光,去跑山、去露营、去看海、去旅行,留下了太多太多美好的回忆 有许多小伙伴对房车感兴趣,...
继续阅读 »

2021年12月买了一台基于大同V80改装的B型小房车,转眼已过两年半的时间,在这两年半的时间里,总行程达四万多公里,带我们走了许许多多的地方,看了祖国的山川湖海,秀美风光,去跑山、去露营、去看海、去旅行,留下了太多太多美好的回忆



有许多小伙伴对房车感兴趣,会咨询关于房车各种各样的问题,所以我准备写几篇文章来详细介绍下房车使用过程中的各种细节问题。今天这篇文章主要来分享下关于这台房车的加油和油耗相关的问题,主要包含柴油、尿素和油耗三部分,看完之后相信你对房车加油及油耗会有更加清晰的认识


柴油


我买的这台房车是烧柴油的,有小伙伴对柴油不了解,有问柴油在城市里好不好加的问题,目前我去过的所有加油站,无论是城市还是乡村,都有柴油供应,柴油相比汽油来说有很多优势,例如:


1.柴油更便宜:柴油往往比最便宜的92号汽油还要便宜一点点


2.燃烧效率高:柴油引擎通常具有更高的压缩比,这意味着在燃烧燃料时能够更高效地释放能量。这使得柴油引擎在燃料效率上表现更优秀,相对于同等排量的汽油引擎,柴油引擎的燃油消耗通常更低,也就是更省油


3.扭矩输出大:由于柴油燃料的化学性质,柴油引擎在高压下能够产生更多的扭矩。这使得柴油引擎在牵引重载或需要大扭矩输出的应用中表现优异,比如卡车、拖拉机等


4.长途行驶经济性好:上边说了柴油引擎的燃油效率高和扭矩输出大,同时因为其价格本身就比汽油低,所以柴油车辆通常在长途行驶中具有更好的经济性


5.动力输出稳定:柴油燃料的燃烧过程相对稳定,这意味着柴油引擎在低转速和高负荷下能够提供更加稳定的动力输出,这对于需要长时间持续工作的应用,如发电机和工程机械,尤其重要


鉴于以上几点,除了家用轿车外的很多商用车辆很多使用柴油,尤其是那些跑长途的大货车、客车和大巴,以及对动力要求比较高的农用和工程机械等,城市内的商用车辆也很多,所以加油根本不是问题



说回房车,加柴油的优势除了以上几点外,更为关键的是加油基本不用等,尤其是在节假日的高速服务区,之前开汽油车节假日出行,在高速服务区加油站排队加油等待半小时一小时的都是常有的事,但自从开了这个柴油车,就再也没有等过了


尿素


上边说了那么多柴油相比汽油等优势,那为什么家用轿车基本上都是烧汽油而非柴油。那这就要说说柴油车相比于汽油车的劣势了


1.低温启动问题:在寒冷的气候条件下,柴油引擎的启动可能会更困难,尤其是在没有预热系统的情况下。相比之下,汽油引擎在低温下启动更为容易,家用车要的是更易用


2.振动和噪音:柴油引擎通常比汽油引擎产生更多的振动和噪音,尤其是在低速行驶时。这可能会降低驾驶舒适度,特别是对于家庭轿车这样的日常驾驶,汽油车的舒适性更好


3.排放标准:柴油引擎在排放方面通常比汽油引擎更具挑战性


家用车更看重的是易用性和舒适性,所以这也是为什么大多数的家用车都采用汽油的主要原因。同时柴油车为了达到目前最新的国六B排放标准,除了常规的技术优化升级外,往往还要通过添加尿素溶液,减少氮氧化物的排放来达标国六B标准



那有小伙伴问添加尿素频繁吗?麻烦吗?加尿素就跟加柴油差不多,加到尿素箱里即可,尿素在每个加油站里都有卖,一桶10KG的价格大概是30元,我的车子一次加一桶多不到两桶,加一次的总花费大概也就60块,我没有具体计算过加满一箱能跑多远,预估大约有五千公里左右的样子,不是很频繁,一年如果行驶两万公里的话,也就是说需要添加3至4次尿素


油耗


最后来聊聊油耗,我的房车是基于大通V80短轴中顶底盘改装的B型房车,原车重应该在2吨左右,再加上上装家具,还有水箱电器之类的,保守估计重3吨,经过两年多的实际行驶,现在整体油耗在8.5L/百公里左右


其中最高油耗10个多一点,10个多油是在非常非常堵的情况下拿到的数据,所以我姑且认为全是最高油耗吧,最低油耗大概也在8个油左右,平时开车有关注,全程高速也有接近8个油,所以算是最低油耗吧,这个车一般都是周末节假日出去玩儿开的多,上班通勤开的少,整体路况都很好,不常遇到堵车的情况,所以综合油耗也是比较低



这个油耗我是非常满意了,毕竟这么大的车,我之前上下班通勤开的CRV,综合油耗都在百公里9升多。买之前以为这么大的车油耗怎么着都要十几个,确实没想到实际会这么低,网上发了帖子,许多同样的车主都表示与我的数据相差不大,可见这是真实表现了


最后


曾经跟多个加油站的一线加油员聊过,比起汽油车,他们更愿意给柴油车加油,主要是汽油车一般都是家用小轿车为主,而柴油车都是运输/工具车为主,家用轿车油箱一般都比较小,50升左右,而柴油车油箱普遍比较大,上百升甚至几百升很正常,一次能加更多的油。更为重要的是,汽油车大多都是自己的,部分汽油车的车主能买个车就觉得自己高人一等,到加油站之类的地方就会对服务人员颐指气使,而柴油车大多都司机或者一线体力劳动者居多,跟加油站员工属于类似工种,更能理解,相对会好说话,不计较,更平和


作者:37丫37
来源:juejin.cn/post/7367275063873470502
收起阅读 »

程序媛28岁前畅游中国是什么体验?

本人计算机硕士毕业,先后在三家厂工作,工作节奏虽说不是 007 吧,但偶尔 996 是有的,勤勤恳恳搬砖是常态,也偶尔累了就划划水摸鱼。在这行业不焦虑是假的,35 岁危机时刻提醒着每一位年轻的程序员,这行主打一个精神内耗。 前几年互联网飞速发展高薪招人时,大家...
继续阅读 »

本人计算机硕士毕业,先后在三家厂工作,工作节奏虽说不是 007 吧,但偶尔 996 是有的,勤勤恳恳搬砖是常态,也偶尔累了就划划水摸鱼。在这行业不焦虑是假的,35 岁危机时刻提醒着每一位年轻的程序员,这行主打一个精神内耗


前几年互联网飞速发展高薪招人时,大家都有肉吃,现在遇到互联网寒冬了,有汤喝就不错了,尤其对晚入行的95 后社畜,现在回过头看,已经是互联网红利退潮的末期了。对于 80 后早一批入行的程序员, 肯定钱也挣够了,房子也早就翻几倍了,早就有抗御风险的能力了,即使裁员了也能拿着分手费找个差不多的厂子继续苟着。但是对于 95 后来说,惨不忍睹,行业内卷及其严重,刚有点工作经验就遭遇大规模裁员,重点买房都是踩在最高点接盘,现在房价跌了,车子打价格战,直接把前几年辛辛苦苦挣的首付跌没了,这几年白干了,说起来,心就抽搐的疼。不像人家00 后,直接看开了,不破三个 dai,房贷,车贷和传宗接代,直接卷老家公务员躺平,享受人生,逃离大城市的拥挤,拒绝被房子的套牢。


金融危机,经济下行,行业越来越卷,精神内耗极其严重,身体健康堪忧。我突然顿悟了,我决定,为自己而活。想看世界的心也越来越强烈,最后我坐不住了,做了个大胆的决定,畅游中国。刚好疫情快结束时,航空公司推出了自己的产品,畅游中国随心飞,我立刻入手了,入手价是三千多点,全国飞不限次数。我一边安排好自己的时间订机票,一边计划旅行路线,一个女生独自环游中国之旅开始了。没有队友,不给生活中任何糟心事打断我的计划,一人吃饱,全家不饿。下面给大家说说我去了哪些地方。


贵州-贵阳


我看好时间后立刻定机票,从上海飞到了贵阳,准备打卡黄果树瀑布。我定的酒店就在黄果树景点附近不远,一大清早7点我就起床了,呼吸着让人神清气爽的空气,吃了一些自己带的进口苹果作为早餐,特别甘甜,饱腹感足足的。8点进山了,那一刻,我别提多开心了。


回想起当社畜时,每次都是8.30起床,9.30左右到公司,每天上班心情比上坟都沉重,永远干不完的KPI,OCR,不是被PUA就是吃老板画的大饼,再丰盛的早餐一想到一堆任务要做,吃着也如同嚼蜡,更别提神清气爽,心境开拓了。


在进入黄果树后,我欢快的脚步往前走,因为我是一个女孩子独行,所以不太愿意跟陌生人说话,一路上虽然很沉默,但看到这些壮观的自然景色,闻着草木花果香,内心激动不已。爬了一个钟左右的山,终于看到了大瀑布。


下面是我实拍的景点图:


WechatIMG110.jpg
WechatIMG114.jpg
WechatIMG111.jpg
WechatIMG115.jpg

有句古诗,疑似银河落九天,一路好山好水,逛完黄果树后我出来就去吃了贵州的特色菜,价格美丽,味道很不错,超级喜欢,
当时就在感慨,上海要是能吃到这么好吃又鲜美的酸汤鱼就好了。


WechatIMG109.jpg

重庆


本来下一步去梵净山再顺路去成都自驾318路线的,但时间紧迫,我弟弟在重庆读书,说要跟我一起去自驾318,我就先去重庆跟他汇合了。


1191711609983_.pic.jpg
1211711610320_.pic.jpg
1241711610640_.pic.jpg

最后那个火锅要适度吃啊,吃两顿辣的我的陈年胃病都犯了,好几天没缓过来,哭晕在厕所,我弟跟个没事人一样,这是我深刻认识到当了多年的社畜的后果就是,经常熬夜加班点外卖,把好好的身体给造坏了。重庆的洪崖洞,解放碑也去了很多次了,这里给个图


1201711609995_.pic.jpg

成都


抵达成都,在春熙路逛了逛,宽窄巷子之前逛过就没去了,


1221711610329_.pic.jpg
1461711611961_.pic.jpg

本来想租自驾神车-坦克300的,价格是普通suv的2倍,结果路上纠结一会的功夫就被抢先租走了(自我反思:以后看准就下手吧,人生有几次这种机会,有啥好犹豫的),租了一辆1.5T的大众SUV,跟我弟一起直奔车行,然后去超市采购路上的食物,大包小包买了一堆,放车后备箱,深夜就起航了


都江堰


教科书上的都江堰,真正去看了,才深深佩服古人治水的智慧,我不是文盲,所以不用一句:卧槽,发表感叹。之前也去过洛阳的黄河小浪底水库,武汉的长江大桥,这些水利工程的智慧。


1261711610658_.pic.jpg
1251711610651_.pic.jpg

青城山


1451711611634_.pic.jpg

这里是青城山下白素贞的故事发源地。爬山是个体力活,当时穿着拖鞋就上山了,下山就傻眼了,不好意思,这里我偷懒了,坐缆车下车,嘿嘿。


泸定桥


1281711611190_.pic.jpg
1271711610783_.pic.jpg

打卡泸定桥,走上面摇摇晃晃确实需要一些勇气,特别怕手机掉下去。


海螺沟


一鼓作气,一路直行,抵达海螺沟。来之前,我觉得新能源车咋自驾318,路上看到同样是特斯拉车主,我感觉自己有点狭隘。人啊,果然要多出去看看,不能活在自己的局限认知中。


1301711611238_.pic.jpg
1481711613272_.pic.jpg
1311711611256_.pic.jpg

不过开车还是要小心,路上遇到有车盘山时发生侧翻的。还有山上偶尔会有落石下来,要当心了。


木格措


一路景色壮观,蓝天白云,川西一定要必去。到了康定情歌的原地。打个卡。


1331711611313_.pic.jpg
1471711612939_.pic.jpg

不过我路上听的歌一直都是朴树的《平凡之路》,一路循环:


我曾经跨过山和大海 也穿过人山人海

我曾经拥有着的一切 转眼都飘散如烟

我曾经失落失望 失掉所有方向

直到看见平凡 才是唯一的答案
....


不正是正值青春的我受伤了,但又奋力前行寻找答案吗。


四姑娘山


一路直行。。。抵达四姑娘山,四姑娘山有四座雪山组成,远看景色很壮观,雪已经化了很多。


1291711611214_.pic.jpg
1441711611506_.pic.jpg
1421711611464_.pic.jpg

当地信仰


遇到了一群一动不动的牦牛,还有一匹热情好客的长脸马。拿出来一个饼给它,它吃的还很香。本来开心的事现在记录起来突然感觉在暗示自己在公司当牛做马,不说了,emo了。据说那白色塔这是当地的信仰,表示尊重。


1361711611362_.pic.jpg
1351711611349_.pic.jpg
1391711611410_.pic.jpg

雅拉山口


盘山路,1.5T的车开着有点吃力,油门上不去。终于爬上山了,下车拍照时,激动过头了,开始缺氧,头疼,吸氧。。。。。。。。


1371711611374_.pic.jpg
1381711611398_.pic.jpg
1561711614555_.pic.jpg

后面走着走着身体扛不住了,我去当地买了高反的药,吃了没啥用,氧气越吸头越疼,我弟要回去上课,我身体不抗造,遗憾的半途而归了。再次强调一下,好风景要趁年轻,体力好,等老了走不动了,确实再好的风景,都没那心情和体力去欣赏了。


1551711614521_.pic.jpg
1571711614582_.pic.jpg

乐山


跟我弟散伙后,我自己开车去了乐山大佛,保佑我顺风顺水吧。还去看了东方佛群,卧佛,药师佛,看了各种佛,记不清楚了。。。


1531711614452_.pic.jpg
1601711617453_.pic.jpg
1611711617471_.pic.jpg

峨眉山


接着我自己又自驾去了峨眉山,两个地方相差不是很远,看到了峨眉山的云海,云雾缭绕,超级刺眼!


1521711614435_.pic.jpg
1511711614416_.pic.jpg

下山后当晚接着又踩着点返回成都还车。休息一晚后,又顺路打卡了锦里。感受人世间的烟火和繁华


1591711614637_.pic.jpg
1581711614623_.pic.jpg

又吃了一顿火锅后,回上海。这时,胃没有不舒服,看来,这一圈下来,肠胃好很多了。


1621711617506_.pic.jpg

又回到了我熟悉的大上海。


安徽


经过一段时间的调养后,我觉得的身体状态老好了,爬山那不是小意思,走,爬山去,什么黄山,三清山,庐山,武功山,离沪这么近,爬起来不费劲!我到了安徽省,黄山市,休息一晚准备去爬山。当晚被出租车司机拉到了老街逛逛。


1631711617723_.pic.jpg

就一个小型的徽派建筑青砖白瓦的特色,跟顾村差不多。逛完后突然下起了大雨,我猝不及防没带伞,
就记得那晚的雨,比情深深雨蒙蒙中依萍找她爸要钱被鞭子抽回去时遇到的那场大雨还大。。。。。。


黄山


不凑巧,上山时遇到了大雾,但来都来了,那就爬山下去吧。到了光明顶也啥都看不见,但幸运的时,下山时,守得云开见月明,气喘吁吁的开心拍照。


1641711617970_.pic.jpg
1651711617990_.pic.jpg
1661711618010_.pic.jpg

江西


黄山结束后,顺路就来到了江西,江西景色比较集中,一定要去上饶啊,那就先去望仙谷看看吧。


上饶-望仙谷


人工打造的经典,现实版的仙侠世界。小雨朦胧,青山傍水,景色秀丽。


1681711618049_.pic.jpg
1671711618030_.pic.jpg
1691711618070_.pic.jpg

上饶-三清山


谁说黄山归来不看山,我觉得三清山值得一去,至少我是不后悔的。每座山都有每座山的特色,爬到这时,腿开始抖了,但我可不是那么轻易就能认输的人啊,继续爬,专挑难爬的道:一线天!!!!!


1721711618536_.pic.jpg
1701711618509_.pic.jpg
1711711618524_.pic.jpg

哈哈,说这个像蟒蛇,像吗?


1731711618548_.pic.jpg

下山时腿疼的不行,扛不住了,嘴不硬了,不去庐山了,武功山了。。。。


南昌


对了,不明白为啥江西彩礼那么高?


1741711619525_.pic.jpg

广东


广州


从南昌飞到广州了,看了小蛮腰,在附近喝喝茶,遛遛弯,吃点茶点


1751711619548_.pic.jpg
1761711619562_.pic.jpg

深圳


到深圳后租了个车溜达到海边吃海鲜,还去华强北也溜一溜,吃了很多粤菜


2121711623487_.pic.jpg
2131711623507_.pic.jpg
2141711623518_.pic.jpg

香港


从深圳坐高铁到香港也就十几分钟,跟快的。香港巴士,香港茶餐厅,路过金店,想买项链的,但又怕弄丢了就没买,现在金价那么高,有点损失。


2171711623568_.pic.jpg
2181711623590_.pic.jpg
2161711623554_.pic.jpg
2151711623538_.pic.jpg

新疆


从上海飞新疆要4个多小时,一路太无聊了,下飞机后,心情就好很多


1781711619752_.pic.jpg

乌鲁木齐


去了大巴扎,吃了羊肉串和切糕,还有新疆大盘鸡


1911711619947_.pic.jpg
1921711619959_.pic.jpg
1931711619971_.pic.jpg
1951711620020_.pic.jpg

无人区


没信号,没水,荒漠一片。。。


1901711619929_.pic.jpg
1891711619916_.pic.jpg

伊犁


到了伊犁市区后,去了小吃街,吃了羊肉


1961711620038_.pic.jpg

赛里木湖


高原湖泊,非常适合自驾游玩,我这里是跟人拼车去的。看着真舒服,可惜我把单反带来,也背不动,这是人家的


1851711619856_.pic.jpg
1841711619835_.pic.jpg
1801711619776_.pic.jpg
1831711619820_.pic.jpg
1811711619793_.pic.jpg
1821711619807_.pic.jpg

边境-国门,果子沟大桥, 薰衣草


1881711619895_.pic.jpg
1871711619882_.pic.jpg
1861711619867_.pic.jpg

新疆白天长,夜里段,到了晚上9点多,天才慢慢开始变黑。


北京


这次我飞到了老北京,看了天安门,看了老城墙


1971711620074_.pic.jpg
1981711620099_.pic.jpg

内蒙古


从北京顺路来了内蒙古呼和浩特,先填饱肚了,去那个什么街买了一堆牛肉干


呼和浩特


1991711623013_.pic.jpg
2011711623052_.pic.jpg
2021711623101_.pic.jpg

青甘环线


说到去青甘,想起有个在学生时期就在玩的狐朋狗友,听说我打算去自驾就想跟我一起去。因为我的车是新能源,自驾充电比较麻烦,他打算提混动车方便些,他说让我等他提车带他一起去自驾,本来约定好了时间,到快出发时,一会又说不打算提车了,又说等他面试换好工作后,最后他自己又各种理由怂了,这种又想出去玩,又想挣钱,又不舍得花钱,这种拧巴的状态,我很无语,当然,这也是现实中大部分人的写实吧,这里我想说,做好权衡利弊和取舍就好,既然决定去追求诗和远方,就不要再去跟钱分文必争了,不可否认,旅行确实需要花钱,我们能做的就是按照自己能承担的最低的成本去看世界。人家说勇敢的人先享受世界,让他纠结犹豫去吧,我就先溜了,毕竟老祖宗给的经验是:欲买桂花同载酒,终不似,少年游。再后来,他说他提车了,问我还去不去,我说我早就已经打卡过了。我问他新工作找好了?他说还没有。。。所以他白拧巴了,车还是要提,想去的地方最终还是要去,挣不了的钱最终还是没到口袋里去。毕竟能随时说走就走的同行者只有自己。


我是从内蒙飞到了青海的西宁。


西宁市


填饱肚子先,然后出发去青海湖,远看蓝色,近看青色,全靠天气


2031711623308_.pic.jpg
2351711690717_.pic.jpg

青海湖


2091711623426_.pic.jpg
2051711623359_.pic.jpg

茶卡盐湖


天空之境,名不虚传。


2061711623379_.pic.jpg
2081711623412_.pic.jpg
2361711690772_.pic.jpg

丹霞地貌,策马奔腾


2111711623453_.pic.jpg
2101711623438_.pic.jpg

策马奔腾很潇洒,归来草原上都是马粪,有点臭。。。
仙气飘飘的牦牛,跟川西的大黑牛不一样
2231711624048_.pic.jpg
2041711623341_.pic.jpg


后面的敦煌,莫高窟去不了了,青海也是有3000多海拔的,玩嗨了,又又又高反了,不得已要回去了,哎,当了这么多年生产驴,身体熬废了。回去后多锻炼身体吧。毕竟身体是革命的成本。


武汉


于是,先飞回了武汉玩几天。回家转转,熟悉的感觉。喜欢武汉的大江大湖和历史文化。黄鹤一去不复返,白云千载空悠悠。
然后又从武汉飞到上海狗着。


2241711624590_.pic.jpg
2261711624823_.pic.jpg
2251711624807_.pic.jpg

上海市


这个城市充满了魅力。只要你有钱,就可以纸醉金迷,去和平饭店享受,去挥霍。没钱,只能继续搬砖。


2291711625200_.pic.jpg
2301711625213_.pic.jpg
2271711624846_.pic.jpg


回去后改善饮食,一边努力干活学习,一边下定决心锻炼,都有马甲线了,五公里so easy ,哈哈哈哈。每次回到上海这个繁华的国际大都市,我都深深感受到,这座城市虽然压力大,但终究是自由的,没人关心和打扰你的私人生活,你可以为自己而活,安排自己的一生,不必循规蹈矩,不必顾及世俗的眼光,这个城市包容能力很强,不妨大胆一些,追求自己的人生。去不同的城市体验不一样的生活和文化。




在买随心飞之前我也去过很多城市,比如:湖北的荆州,湖南的岳阳,张家界,广东的东莞,广西的桂林和北海,海南的三亚,云南的昆明大理丽江,江浙沪包邮一带的杭州,南京,无锡,湖州,台州,宁波,福建的厦门,河南的洛阳,开封,郑州,信阳,山东青岛,陕西西安,安徽合肥等城市。时间有限,码字不易,很抱歉这里我就不全部列出了。尤其在学生时代,那是真的快乐,没有一丝丝杂念,单纯的快乐。后面打算环游世界了,已经去了东南亚的一些国家,这里我想说我本来就是为了WLB努力的,工作生活两不误,我的旅途未完待续~


回顾这么多年,走过的国内大大小小的城市,也没具体统计过,开始逐渐让自己的眼界开阔起来,不让自己的眼光那么狭隘了,看待任何事物更具包容性吧,以前不理解的东西,现在慢慢理解了。也许人生就是这样,思想和观念一直变化。还是那句话,勇敢的人先享受人生吧,不要辜负努力写代码的自己。


作者:为了WLB努力
来源:juejin.cn/post/7351301965034586152
收起阅读 »

携手15年,语雀创始人玉伯从蚂蚁离职,选择一个人远行

转载好文:雷锋网 本文作者:何思思 2023年4月28日,即4月28日凌晨,玉伯发朋圈称将要离开蚂蚁,今天也是玉伯在蚂蚁的最后一天。 他写道:“再见,山峰下的园区。一个人选择远行,并不一定是马云说的钱给少了或者心委屈了,也可以是为了远方有西湖般的美景。”下面...
继续阅读 »

转载好文:雷锋网 本文作者:何思思


image.png


2023年4月28日,即4月28日凌晨,玉伯发朋圈称将要离开蚂蚁,今天也是玉伯在蚂蚁的最后一天。


他写道:“再见,山峰下的园区。一个人选择远行,并不一定是马云说的钱给少了或者心委屈了,也可以是为了远方有西湖般的美景。”下面的配图是园区风景,还有眺望远方的景色。


不愿做技术大佬,要做为产品服务的技术


“前端大牛、技术大佬”是业界给玉伯贴的标签,2008年加入淘宝后,玉伯先后做出了前端领域很火的框架 SeaJS、KISSY,之后带领团队通过开源做了很多技术产品。


但玉伯始终认为,技术只是工具,最终还是要为产品服务。所以当时在淘宝内部,玉伯一直是“折腾”的状态,加入淘宝那年,玉伯就参加了内部的赛马机制,跟团队做了几个月的创新产品,最后以失败告终,又回到了Java 团队做技术。


但这并有改变他要做创新产品的初心,于是2010 年到2011年,他一边做技术研发,一边继续摸索创新产品,但一直没做出能拿的出手的产品。直到2016年,在蚂蚁体验技术部的创新产品孵化机制策马扬鞭项目中,玉伯团队主导的语雀问世,并于2018年正式对公网提供服务。


也有内部人士称:玉伯当时和老板提了条件说,光做前端没意思,你要想留住我,就得给我一款产品做。所以当时玉伯自己要了一个团队,专门做一个闭环产品。


其实,从语雀诞生到现在经历了两次生死局:第一次是2018年,腾讯文档、钉钉文档、飞书文档相继亮相,文档产品迎来爆发期,当时阿里也想抓住这个风口,语雀最终把三分之二的人输送给了钉钉,作为钉钉文档的初始团队。在团队仅剩七八个人的时候,玉伯再次招人将团队扩充到二十人左右。


第二次是2020年,彼时,钉钉文档做了很久但并没达到预期效果,而语雀正值上升期,阿里云为了尽快把文档做起来,想把语雀、钉钉文档、阿里云笔记等内部各种文档团队聚集起来,成立一个独立的阿里文档事业部,由玉伯牵头,但却被无招反对,这也间接帮助了玉伯。


直到2021年,蚂蚁成立了智能协同事业部,其中语雀作为重点产品,以独立BU运作。


创业中的理想派,为了做好一件事而做


从2016年到现在,为了做好语雀,玉伯做了大量的工作。


玉伯曾回忆道,做语雀最大的一个感触是,啥都得做。最开始是半个PD,很快变成了客服,同时兼做运营,还需要去承担BD的工作,因为没有BD,只能逼着自己去做,一切为了产品往前跑。


也有用户在即刻分享道,自己曾经在语雀的付费用户群中提了一个文档的排序问题,当时玉伯就在群里,很快的响应了这个需求并做了优化。


image.png


此外,玉伯也背负了巨大的营收压力,尤其是近两年在阿里集团整体缩紧的状态下。雷峰网通过其他渠道了解到,集团也给语雀定了目标——“盈亏平衡”。


迫于压力,近两年语雀也调整了收费策略,2019年语雀开始尝试简单的商业化模式,即初级的团队语雀空间和语雀个人的收费版本;紧接着又重新设计了个人版价格策略,分为99元会员、199元超级会员、299元至尊会员三个档次,团队和空间版的收费则更高。


这对一个小团队来说并不容易,首先,较钉钉、飞书、腾讯文档而言,语雀强调的是知识管理的逻辑,其次,语雀服务的对象偏小众聚焦在侧重知识管理的用户,且这些目标对象比较分散,很难第一时间发掘到,这就意味着需要花很长时间去培养,没办法快速完成转化;再就是,虽然语雀团队不大,只有五六十人左右,但这部分人大都是互联网人才,成本也是一笔不小的支出。


雷峰网在之前拜访玉伯时听闻,目前语雀主要服务蚂蚁和阿里内部,在阿里内的日活已经达到了11万左右,商业化方面还比较单一,主要是通过发布会的方式宣传。由此可见,语雀的商业化路径还没完全打开。


无论选择出去创业还是集团内部创业,背负营收压力都是不可避免的。但抛开这个不谈,仅玉伯的个人角度出发,他曾谈过自己做语雀的初心,就是想把自己内心想做的事情做完,且这件事还能帮助到别人,就做了。


正是这种简单纯粹的心态,让玉伯在做语雀时只专注事情的本身以及这件事情创造的价值,而并非拼命地追求变现。


雷峰网(公众号:雷峰网)曾发表文章《留给飞书的时间》,他如此评论:



“现实主义者关注的是钱,理想主义者关注的是时间,当代这个社会,钱很重要。但更重要的,对个体来说,是如何提高时间的质量,对人类来说,不仅关注时间的质量,还关注整个人类时间的长短,是否可延续下去。赚钱是为了花钱,花钱是为了提升时间的品质甚至长度。围绕钱的现实主义者,最终会为围绕时间的理想主义者服务。”



从玉伯最新的朋友圈内容,不难看出,他的离开或许和钱权没有太大的关系,而是为了追求心目中的诗和远方。他也曾经说过自己有三个梦:“技术梦、产品梦、自由梦。”离开蚂蚁,或许是为了去实现他的“自由梦。”


作者:狗头大军之江苏分军
来源:juejin.cn/post/7299035378589040667
收起阅读 »

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

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

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

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

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

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

组合式函数

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

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

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

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

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

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

实际上,区别大了去了。

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

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

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

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

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

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

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

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


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

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

模块化的使用方法

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

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

import { ref } from 'vue'

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

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


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

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

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

import { ref, computed } from 'vue'

export const num1 = ref(3)

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

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

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

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

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


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

当遇到需要在Activity间传递大量的数据怎么办?

在Activity间传递的数据一般比较简单,但是有时候实际开发中也会传一些比较复杂的数据,尤其是面试问道当遇到需要在Activity间传递大量的数据怎么办? Intent 传递数据的大小是有限制的,它大概能传的数据是1M-8K,原因是Binder锁映射的内存大...
继续阅读 »

在Activity间传递的数据一般比较简单,但是有时候实际开发中也会传一些比较复杂的数据,尤其是面试问道当遇到需要在Activity间传递大量的数据怎么办?


Intent 传递数据的大小是有限制的,它大概能传的数据是1M-8K,原因是Binder锁映射的内存大小就是1M-8K.一般activity间传递数据会要使用到binder,因此这个就成为了数据传递的大小的限制。那么当activity间要传递大数据采用什么方式呢?其实方式很多,我们就举几个例子给大家说明一下,但是无非就是使用数据持久化,或者内存共享方案。一般大数据的存储不适宜使用SP, MMKV,DataStore。


Activity之间传递大量数据主要有如下几种方式实现:


  • LruCache

  • 持久化(sqlite、file等)

  • 匿名共享内存


使用LruCache

LruCache是一种缓存策略,可以帮助我们管理缓存,想具体了解的同学可以去Glide章节中具体先了解下。在当前的问题下,我们可以利用LruCache存储我们数据作为一个中转,好比我们需要Activity A向Activity B传递大量数据,我们可以Activity A先向LruCache先写入数据,之后Activity B从LruCache读取。


首先我们定义好写入读出规则:


public interface IOHandler {
   //保存数据
   void put(String key, String value);
   void put(String key, int value);
   void put(String key, double value);
   void put(String key, float value);
   void put(String key, boolean value);
   void put(String key, Object value);

   //读取数据
   String getString(String key);
   double getDouble(String key);
   boolean getBoolean(String key);
   float getFloat(String key);
   int getInt(String key);
   Object getObject(String key);
}

我们可以根据规则也就是接口,写出具体的实现类。实现类中我们保存数据使用到LruCache,这里面我们一定要设置一个大小,因为内存中数据的最大值是确定,我们保存数据的大小最好不要超过最大值的1/8.


LruCache mCache = new LruCache<>( 10 * 1024*1024);

写入数据我们使用比较简单:


@Override
public void put(String key, String value) {
   mCache.put(key, value);
}

好比上面写入String类型的数据,只需要接收到的数据全部put到mCache中去。


读取数据也是比较简单方便:


@Override
public String getString(String key) {
   return String.valueOf(mCache.get(key));
}

持久化数据

那就是sqlite、file等方式。将需要传递的数据写在临时文件或者数据库中,再跳转到另外一个组件的时候再去读取这些数据信息,这种处理方式会由于读写文件较为耗时导致程序运行效率较低。这种方式特点如下:


优势:


(1)应用中全部地方均可以访问


(2)即便应用被强杀也不是问题了


缺点:


(1)操做麻烦


(2)效率低下


匿名共享内存

在跨进程传递大数据的时候,我们一般会采用binder传递数据,但是Binder只能传递1M一下的数据,所以我们需要采用其他方式完成数据的传递,这个方式就是匿名共享内存。


Anonymous Shared Memory 匿名共享内存」是 Android 特有的内存共享机制,它可以将指定的物理内存分别映射到各个进程自己的虚拟地址空间中,从而便捷的实现进程间内存共享。


Android 上层提供了一些内存共享工具类,就是基于 Ashmem 来实现的,比如 MemoryFile、 SharedMemory。


作者:派大星不吃蟹
来源:juejin.cn/post/7264503091116965940
收起阅读 »

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

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

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

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

一、什么是数组

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

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

Description

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

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

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

var firstElement = myArray[0];

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

二、数组的作用

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

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

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

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

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

三、创建数组的方法

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

1)字面量方式:

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

var arr = [];

2)使用Array构造函数:

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

var arr = new Array();

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

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

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

四、使用数组的方法

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

1、访问和修改数组元素

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

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

2、向数组末尾添加元素

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

arr.push(5);

3、从数组末尾移除元素

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

arr.pop();

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

arr.unshift(0);

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

arr.shift();

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

console.log(arr.length);

7、遍历数组

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

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

8、数组排序

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

arr.sort();

9、数组反转

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

arr.reverse();

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

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

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

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

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

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

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

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

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

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

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

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

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

收起阅读 »

Chat Gpt详细教程:手把手带你Open AI 的API对接

AI
今年4月最大的一个瓜,就是Open AI全面免费开放了,所以很多人想白漂API但却不知该如何去获取Open AI的API,甚至好多小伙伴都碰壁在注册Open AI的半路上了,甚至是如何开通海外付费都变成一个难题~所以针对以上的问题,我将出一份教程为大家一一解决...
继续阅读 »

今年4月最大的一个瓜,就是Open AI全面免费开放了,所以很多人想白漂API但却不知该如何去获取Open AI的API,甚至好多小伙伴都碰壁在注册Open AI的半路上了,甚至是如何开通海外付费都变成一个难题~

所以针对以上的问题,我将出一份教程为大家一一解决。当然,本次教程全程是由本人跑过一遍的,本人亲测不封号、不踩雷、不墨迹。

Description

在整个的注册、激活Open AI账户、升级Open AI使用级别、对接API等等都会借助其他工具而产生一些费用,请各位老板慎重考虑并尝试。

在教程学习中产生的其他平台的工具费用与任何问题都与本教程无关,请各位老板悉知。

注意:所有工具的昵称全部统称为“XXX国际旅游卡”担心被优化了~ 不懂的可以问。

话不多说,开始进入正题。

一、第一步,注册谷歌邮箱

大家去下载一个谷歌浏览器,然后安装到电脑上,再去寻一个稳定“梯子”。准备工作就算完了。接下来给大家演示注册流程哈~

咳咳~ 有一些小伙伴可能不理解什么是“梯子”请自行百度搜索哈。或者可以来咨询我给你推荐一个好用的。

1、在谷歌网页输入Goolgle的地址:www.google.com,点击右上角的登录页面。
Description
2、跳转到登录页面后,点击“创建账号”,选择个人用途。下一步。
Description
3、填写基本信息,我一般会选择把名字写成英文字母,因为这样会显得很高级。Hhh~
Description
4、填写生日,随便写也可以,完全不影响后面的使用。
Description
5、创建你自己的Gmail地址,也可以使用默认生成的Gmail地址。看个人喜好了。
Description
6、设置密码,这一步重要的不是设置密码,而是保存好密码。省的到时候记不得密码了。我一般会写在我的便签里,方便后面查找使用。
Description
7、这里填写一个你的QQ邮箱即可,方便后续重要操作。多一个保障么~如果你不打算长期用的话,就直接跳过即可。
Description
8、确认信息,下一步,阅读协议,同意,注册成功!恭喜恭喜~
Description
Description
9、修改谷歌个人资料,不想改的跳过即可。
Description
10、首次登录后会出现验证登录的情况。重新验证登录一下就OK了
Description
11、手机号绑定验证,这个时候写国内的手机号码即可,短信都是秒到。输入验证码,就OK了

验证码是G- 开头的,输入后面的纯数字就OK。
Description
Description
成功登录!记得自己切换成中文模式哦~

二、第二步,注册Open AI

1、访问Open Ai 官网,点击登录。
Description
2、登录验证,正常跟着指示操作即可。
Description
配合验证就OK,
3、关键的时刻到了,使用谷歌账号登录!省去一切繁琐步骤。
Description
4、选择当前已登录的谷歌账号即可。没什么技术含量了~ 跟着步骤走就准没错。
Description
5、点击继续,(要是英文看着难受,就点下面的切换语言即可,不切换的话点击的位置都是一样的。)
Description
6、创建Open AI的基本账户信息,最好是英文,你写中文也行,就是后面会显示的很奇怪。
Description
示范的模板~ 按着下面的格式去写就OK了。
Description
7、点击前往获取API,这点毋庸置疑了。先把API拿到手,再去体验GPT吧!
Description
8、到达了主页面!点击侧边的菜单栏,选择API Keys——创建新的API,看图吧!
Description
Description
9、这一步很重要了!非常的重要。在首次获取API的时候,Open AI会要求你验证手机号的!国内手机暂时不大行,所以这个时候你需要一个海外的旅游卡,这是重点来咯!!!

三、第三步,国际SIM旅游卡

1、现在我们需要借助一个工具!国际SIM旅游卡租赁~ 我们去访问s ms-activate。短期出国、酷爱外服游戏的的朋友们应该清楚这玩意儿的好处。此处不做过多讲解。
Description
2、这里的注册流程与前面一样,选择用谷歌账号登录即可,这里不用过多的废话,里面都是中文介绍的方便很多。但是记得先充值点余额进去,方便后面使用它的短信接受功能。
Description
3、充值的话国际旅游卡支持多重支付方式,也包括了咱们国内的支付宝。充值的时候选择一个最低档就可以,够你使用了。
Description
4、充值成功后,就不用管它了。我们去租用一个国际SIM旅游卡。在首页选择租用——Whatsapp——选择一个国家的旅游卡
Description
5、但你租赁成功后,在右边会显示你的旅游卡的SIM卡 号,这个时候你就可以去激活Open ai 的API 了。(回到第二步的第7点位置,应该不需要再截图给大家看了~ )

提示:它的最低租用时效是4个小时,4个小时够你随便玩耍了。

四、第四步,创建API

1、回到openai 界面后点击创建秘钥——填写项目昵称——选择项目类——全部权限——创建秘钥。
Description
Description
2、这一步非常的关键!一定要点复制,然后保存好,API秘钥仅在首次能看见全部秘钥,等你退出这个界面的时候,就再也看不到全部秘钥了。切记切记!!!
Description
好了,这一步你已经成功的获取秘钥了!!!接下来是升级Open AI的账户级别了。

五、第五步,升级Open AI使用级别

首先点击绑定国际旅游卡信息~
Description
我们的账户默认级别是0,我们需要升级到级别1。
Description
下图为未升级级别是对接API的报错信息,不信的可以去试试~ 白漂还是有点门槛的。
Description
但是我们没有国际卡可怎么办呢?别急,教都教了怎么可能教一半呢?各位老板继续往下看!


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


六、第六步,注册+绑定

我们需要通过国际旅游卡去升级Open AI的使用级别,这一步是需要付费的哦。

首先我们要去访问bewildcard。

1、点击登录,直接用国内的号码即可登录。这里无需多说什么咯~
Description
2、我的卡片——选择一年——支付。后面的步骤就不详细展示了,基本按着提示去操作就OK了。大家选择ZFB认证会比较顺畅一些。
Description
在这个界面里按着图片步骤走即可,中英文的显示都一样。
Description
3、当你将卡片注册成功后,在“我的卡片”里会显示你的卡片信息了,如下图:
Description
4、此时你的卡片中余额是0,Wild Card 最低起步是10美元~ 所以各位老爷都懂的~ 含泪付吧…舍不得余额套不着API……
Description
5、返回Open AI官网,绑定Open AI的账户,升级使用级别。
Description
6、选择个人个体账户。
Description
7、填写详细信息。在这里要注意这个CVC,是你卡片的安全码,记得填上,是需要验证的。按着旅游卡的卡片信息去填写下面的即可。
Description
OKK,一切都大功告成!!!当你将账户“使用级别”升级成功后,可以开开心心的去对接API进行使用了。一下是使用级别1的权限了,各位老板要详细阅读哦~
Description
Description
以下是我做测试时用的真实数据,如果大家想长期使用的话,建议每个月定期给国际旅游卡(WildCaard)充上10美元,避免断粮导致的各种报错~

往往我们会最容易忽略这一点~ 然后开始在程序里疯狂找报错原因,哈哈哈~
Description
好了,本次的教程到此结束了。如果在这过程中还有什么不懂的,可以来与我交流哦。

最后祝各位老板,身体健康,工作顺利!拜拜咯~

收起阅读 »

关于“明显没有bug的代码”的一些拙见

以前听过过一个有趣的说法:不要编写没有明显bug的代码,要去编写明显没有bug的代码。这里提到的两个概念:“没有明显bug的代码”和“明显没有bug的代码”。同样的文字,只是调换了下顺序,表达的就是完全不同的概念了。前者“没有明显bug的代码”大概是最常见的代...
继续阅读 »

以前听过过一个有趣的说法:不要编写没有明显bug的代码,要去编写明显没有bug的代码。这里提到的两个概念:“没有明显bug的代码”和“明显没有bug的代码”。同样的文字,只是调换了下顺序,表达的就是完全不同的概念了。前者“没有明显bug的代码”大概是最常见的代码了,特征就是:



  1. 每段程序看起来合理,但结果就是不对

  2. 程序看起来复杂、奇怪,但就是可以正常运行

  3. 天书一般的程序

  4. 待补充


平时工作中到处缝缝补补的代码大概就是这种代码吧。背后的原因一般比较复杂,有时还不可追溯,项目工期紧,人员交接等等都有可能。因此,与其思考“如何避免没有明显bug的代码”,还不如思考“如何写出明显没有bug的代码”。本文就何为“明显没有bug的代码”总结一些个人的思的胡思乱想,阐述这类代码的几个特征。


特征1:代码简短


“明显没有xx”意味着一眼能看出来,而“一眼”这个条件就有很大的限制。如果给我一个函数,包含1000多行代码,我鼠标滚轮要滚好久,才能过完一遍代码,那么这种代码一定不是“明显没有bug的代码”。那么,反过来说,“明显没有bug的代码”一定是短小的代码。比如,Java中的Objects.equals方法:


public static boolean equals(Object a, Object b) {
return (a == b) || (a != null && a.equals(b));
}

这一段代码简短到,代码跟功能定义的文字篇幅差不多,连写文档注释的必要都没有了。还有一个更极端的例子是Java的Objects.isNull方法:


public static boolean isNull(Object obj) {
return obj == null;
}

简直就是一段“废话”。


特征2:功能完整且连贯


“一眼能看出”还意味着功能不能太分散,如果一个功能,分散在十多个函数或文件中,那么看这段功能就得在很多代码片段中跳来跳去,这个就需要开发者来阅读代码时充当一个“人肉解释器”的角色,在大脑中把各个代码片段组合起来才能明白整个流程和细节,这无疑降低阅读代码的效率,bug也容易隐藏在各个代码片段的“缝隙”中。举个常见的例子:在图形界面应用中,用户登录后,弹出登录成功的提示,然后关闭登录页面。一种普通的实现是:


//1. 在登录按钮触发登录操作
loginButton.setOnClickListener(v -> controller.login(username, poassword))

//2. 在登录成功的回调中展示弹窗
public void onLoginSuccess(User user){
LoginSuccessDialog.show("login success")
}

//3. 在失败的回调中展示错误信息
public void onLoginFailed(String errorMsg){
MessageDialog.showMessage(errorMsg);
}

//4. 在LoginSuccessDialog确认后关闭页面
public void onLoginDialogConfirmed(){
loginPage.close();
}

看上去好像解耦很不错,但功能都变得七零八落。要拼凑出完整的功能大概得仔细阅读整段代码,更别提“一眼看出”了。那么一眼能看出的代码大概长啥样呢?我想,大概是这样:


loginButton.setOnClickListener(v -> {
controller.login(username, poassword)
.onSuccess(user ->
LoginSuccessDialog.总而言之,解耦需谨慎,不要因过度解耦而牺牲了内聚性和连贯性。show("login success")
.onConfirmed(() -> loginPage.close()))
.onFailed(errorMsg ->
MessageDialog.showMessage(errorMsg));
});

这是对该视图流程的一个连贯的描述,而且篇幅更短。至于获取到用户数据存储到本地数据库、通知其他页面更新等操作,跟当前视图没有关系,也就不需要放在这段代码里。总而言之,解耦需谨慎,不要因过度解耦而牺牲了内聚性和连贯性。


特征3:良好的表达


代码的篇幅得到控制后,要让人一眼看懂,还需要容易理解才行。设计心理学提出“设计传达所有必要的信息,创造一个良好的系统概念模型,引导用户理解系统状态,带来掌控感”。程序设计也是如此,代码是程序功能的文本表达,需要传达对应信息来让人产生该功能正确的概念模型。


以一个常见的上传图片的弹窗为例,思考一个菜单弹窗,包含取消和两个功能按钮:从相册选择和拍照上传,例如下图这样。


在这里插入图片描述


那么对应的代码可以表达为:


MenuDialog.create()
.withAction("拍照上传", dialog -> {
takePhoto();
})
.withAction("本地上传", dialog -> {
chooseFromGallery();
})
.onCancel(() -> {
//do something
})
.show();

或者


MenuDialog.create()
.withAction("拍照上传", controller::takePhoto)
.withAction("本地上传", controller::chooseFromGallery)
.onCancel(() -> {
//do something
})
.show();

没有多余的代码,该有的信息都表达到位,而且和实际功能有良好的对应关系。


4. 特征4:可验证正确性


代码可以让人一眼看懂之后,那么判断其有没有bug,还有一个重要前提:这个代码是有正确性可言的,可以被验证。


例如,来看下面这段随意的代码:


int type;
boolean isClosed;

void doSomething(String text) {
if (type == 0) {
if (isClosed) {
println(text);
} else {
error("something wrong");
}
} else if (type == 1) {
//do something
}
}

这段程序简短、易读,但是doSomething函数的行为依赖两个外部变量,而这两个外部变量又容易被其他地方随意改动。比如,type的定义域为1、2、3,但如果type新增类型4的时候或者被错误地赋值为-1的时候,这个doSomething函数的行为还是正确的吗?doSomething函数的正确性依赖于type变量的正确性,那么又依赖于读写type变量的程序的正确性,这样的程序是难以验证的。而且,对上下文依赖越多的程序,越难以产生明确的定义,因为这个定义也依赖上下文的定义。定义不明确,更难以验证内容的正确性。


相比之下,Objects.equalsObjects.isNull方法有着明确的定义,而且不受上下文影响,可以一眼就看出对错。而下面这段代码:


MenuDialog.create()
.withAction("拍照上传", controller::takePhoto)
.withAction("本地上传", controller::chooseFromGallery)
.onCancel(() -> {
//do something
})
.show();

表达明确,可以快速判断出程序行为是否正确、符合期望。即便MenuDialog出现异常,或者takePhotochooseFromGallery出了什么问题,也不需要来修改这段程序。


不过,程序验证是一个有点高深的科研方向,要严格验证一个程序的正确性是很困难的一件事,不过我们仍然可以试着去编写一些“看起来”正确的程序。(利用函数式编程思想写出来的代码通常容易验证一些)


作者:乐征skyline
来源:juejin.cn/post/7236010330051887164
收起阅读 »

程序员黑话之故障专辑(中英文对照版)

正好最近业内接连发生了几起影响比较大的故障,那我们就专门做一期「故障专辑」吧。 故障 故障有好几种叫法,比较正式的 故障 - Outage 事故 - Incident 不怎么严重的,时间很短的 抖了一下 - Jitter(多用于网络) Hiccup (中...
继续阅读 »

正好最近业内接连发生了几起影响比较大的故障,那我们就专门做一期「故障专辑」吧。


故障


故障有好几种叫法,比较正式的



  • 故障 - Outage

  • 事故 - Incident


不怎么严重的,时间很短的



  • 抖了一下 - Jitter(多用于网络)

  • Hiccup (中文翻译是打了个嗝,不过中文里貌似没有这个讲法)


通俗点的说法



  • 挂了/崩了- Down
    file


500


当在请求某个网络资源时,服务器内部发生错误时,返回的错误编号。扩展为系统发生内部故障。


file


变更


虽然突然的流量暴涨,或者光缆被挖断,数据中心着火,被雷劈都有可能,但绝大多数时候,故障都是变更导致的。


file


变更分为三大类:



  • 代码变更 - Code Change

  • 配置变更 - Config Change

  • 数据库变更 - Database Change


左移 (shift-left)


降低变更风险的一个方法,就是做变更前检查,问题越早发现越好。因为变更的流水线是从左往右画的,起点在左边。所以左移就是把检查尽量靠近起点。


金丝雀 (Canary)


以前矿工下井,会带一只金丝雀,如果井下空气出现状况,更敏感的金丝雀会先有异常。这个概念也带到了软件研发里。会循序渐进地做变更。另外一种叫法是灰度 (Grayscale)。


file


单元化/区域化 (Regionalization)


在互联网公司逐渐普及的架构,主要由 AWS 发扬光大,把服务进行隔离。


爆炸半径 (Blast Radius)


金丝雀和单元化都是为了降低爆炸半径,减少故障的影响面。


file


值班 (On-call)


也叫 Carry the pager。以前带着的传呼机叫做 Pager。现在传呼机被手机/软件取代了,但 Pager 这个名字沿用了下来。


file


复盘 (Postmortem)


原义是尸检报告。在软件研发领域,指详细的故障分析报告。


惊群 (Thundering Herd)


file


打雷后,动物一下子被惊醒了,到处乱窜,造成混乱。在故障恢复阶段要小心的问题,很容易刚拉起一个服务,立马又被积压的请求打挂。


结语


船停在港口是最安全的,但那不是造船的目的。软件需要持续的变更迭代,变更就有风险。但研发团队可以通过引入工具,来降低风险,针对一开始变更的三种类型,市面上也有成熟的开源方案:
代码变更 - 老牌的有 Jenkins,新兴的有 Drone CI 和 Zadig




作者:Bytebase
来源:juejin.cn/post/7301244964297670693
收起阅读 »

室友打一把王者就学会了Java多线程

大家好,我是二哥呀。 对于 Java 初学者来说,多线程的很多概念听起来就很难理解。比方说: 进程,是对运行时程序的封装,是系统进行资源调度和分配的基本单位,实现了操作系统的并发。 线程,是进程的子任务,是 CPU 调度和分派的基本单位,实现了进程内部的并发...
继续阅读 »


大家好,我是二哥呀。


对于 Java 初学者来说,多线程的很多概念听起来就很难理解。比方说:



  • 进程,是对运行时程序的封装,是系统进行资源调度和分配的基本单位,实现了操作系统的并发。

  • 线程,是进程的子任务,是 CPU 调度和分派的基本单位,实现了进程内部的并发。


很抽象,对不对?打个比喻,你在打一把王者(其实我不会玩哈 doge):



  • 进程可以比作是你开的这一把游戏

  • 线程可以比作是你所选的英雄或者是游戏中的水晶野怪等之类的。


带着这个比喻来理解进程和线程的一些关系,一个进程可以有多个线程就叫多线程。是不是感觉非常好理解了?


进程和线程


❤1、线程在进程下进行


(单独的英雄角色、野怪、小兵肯定不能运行)


❤2、进程之间不会相互影响,主线程结束将会导致整个进程结束


(两把游戏之间不会有联系和影响。你的水晶被推掉,你这把游戏就结束了)


❤3、不同的进程数据很难共享


(两把游戏之间很难有联系,有联系的情况比如上把的敌人这把又匹配到了)


❤4、同进程下的不同线程之间数据很容易共享


(你开的那一把游戏,你可以看到每个玩家的状态——生死,也可以看到每个玩家的出装等等)


❤5、进程使用内存地址可以限定使用量


(开的房间模式,决定了你可以设置有多少人进,当房间满了后,其他人就进不去了,除非有人退出房间,其他人才能进)


创建线程的三种方式


搞清楚上面这些概念之后,我们来看一下多线程创建的三种方式:


继承 Thread 类


♠①:创建一个类继承 Thread 类,并重写 run 方法。


public class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName() + ":打了" + i + "个小兵");
}
}
}

我们来写个测试方法验证下:


//创建MyThread对象
MyThread t1=new MyThread();
MyThread t2=new MyThread();
MyThread t3=new MyThread();
//设置线程的名字
t1.setName("鲁班");
t2.setName("刘备");
t3.setName("亚瑟");
//启动线程
t1.start();
t2.start();
t3.start();

来看一下执行后的结果:



实现 Runnable 接口


♠②:创建一个类实现 Runnable 接口,并重写 run 方法。


public class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {//sleep会发生异常要显示处理
Thread.sleep(20);//暂停20毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "打了:" + i + "个小兵");
}
}
}

我们来写个测试方法验证下:


//创建MyRunnable类
MyRunnable mr = new MyRunnable();
//创建Thread类的有参构造,并设置线程名
Thread t1 = new Thread(mr, "张飞");
Thread t2 = new Thread(mr, "貂蝉");
Thread t3 = new Thread(mr, "吕布");
//启动线程
t1.start();
t2.start();
t3.start();

来看一下执行后的结果:



实现 Callable 接口


♠③:实现 Callable 接口,重写 call 方法,这种方式可以通过 FutureTask 获取任务执行的返回值。


public class CallerTask implements Callable<String> {
public String call() throws Exception {
return "Hello,i am running!";
}

public static void main(String[] args) {
//创建异步任务
FutureTask<String> task=new FutureTask<String>(new CallerTask());
//启动线程
new Thread(task).start();
try {
//等待执行完成,并获取返回结果
String result=task.get();
System.out.println(result);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}

关于线程的一些疑问


❤1、为什么要重写 run 方法?


这是因为默认的run()方法不会做任何事情。


为了让线程执行一些实际的任务,我们需要提供自己的run()方法实现,这就需要重写run()方法。


public class MyThread extends Thread {
public void run() {
System.out.println("MyThread running");
}
}

在这个例子中,我们重写了run()方法,使其打印出一条消息。当我们创建并启动这个线程的实例时,它就会打印出这条消息。


❤2、run 方法和 start 方法有什么区别?



  • run():封装线程执行的代码,直接调用相当于调用普通方法。

  • start():启动线程,然后由 JVM 调用此线程的 run() 方法。


❤3、通过继承 Thread 的方法和实现 Runnable 接口的方式创建多线程,哪个好?


实现 Runable 接口好,原因有两个:



  • ♠①、避免了 Java 单继承的局限性,Java 不支持多重继承,因此如果我们的类已经继承了另一个类,就不能再继承 Thread 类了。

  • ♠②、适合多个相同的程序代码去处理同一资源的情况,把线程、代码和数据有效的分离,更符合面向对象的设计思想。Callable 接口与 Runnable 非常相似,但可以返回一个结果。


控制线程的其他方法


针对线程控制,大家还会遇到 3 个常见的方法,我们来一一介绍下。


1)sleep()


使当前正在执行的线程暂停指定的毫秒数,也就是进入休眠的状态。


需要注意的是,sleep 的时候要对异常进行处理。


try {//sleep会发生异常要显示处理
Thread.sleep(20);//暂停20毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}

2)join()


等待这个线程执行完才会轮到后续线程得到 cpu 的执行权,使用这个也要捕获异常。


//创建MyRunnable类
MyRunnable mr = new MyRunnable();
//创建Thread类的有参构造,并设置线程名
Thread t1 = new Thread(mr, "张飞");
Thread t2 = new Thread(mr, "貂蝉");
Thread t3 = new Thread(mr, "吕布");
//启动线程
t1.start();
try {
t1.join(); //等待t1执行完才会轮到t2,t3抢
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
t3.start();

来看一下执行后的结果:



3)setDaemon()


将此线程标记为守护线程,准确来说,就是服务其他的线程,像 Java 中的垃圾回收线程,就是典型的守护线程。


//创建MyRunnable类
MyRunnable mr = new MyRunnable();
//创建Thread类的有参构造,并设置线程名
Thread t1 = new Thread(mr, "张飞");
Thread t2 = new Thread(mr, "貂蝉");
Thread t3 = new Thread(mr, "吕布");

t1.setDaemon(true);
t2.setDaemon(true);

//启动线程
t1.start();
t2.start();
t3.start();

如果其他线程都执行完毕,main 方法(主线程)也执行完毕,JVM 就会退出,也就是停止运行。如果 JVM 都停止运行了,守护线程自然也就停止了。


小结


本文主要介绍了 Java 多线程的创建方式,以及线程的一些常用方法。最后再来看一下线程的生命周期吧,一图胜千言。



好了,如果你想学好 Java,GitHub 上标星 10000+ 的《二哥的 Java 进阶之路》不容错过,据说每一个优秀的 Java 程序员都喜欢她,风趣幽默、通俗易懂。内容包括 Java 基础、Java 并发编程、Java 虚拟机、Java 企业级开发(Git、Nginx、Maven、Intellij IDEA、Spring、Spring Boot、Redis、MySql 等等)、Java 面试等核心知识点。学 Java,就认准二哥的 Java 进阶之路😄。


Github 仓库:github.com/itwanger/to…


码云仓库(国内访问更快):gitee.com/itwanger/to…


star 了这个仓库就等于你拥有了成为了一名优秀 Java 工程师的潜力。



把二哥的座右铭送给你:没有什么使我停留——除了目的,纵然岸旁有玫瑰、有绿荫、有宁静的港湾,我是不系之舟


作者:沉默王二
来源:juejin.cn/post/7329413905028186124
收起阅读 »

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

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

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

  1. 使用场景

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

  2. 功能描述

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

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

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

    第三步,保存文件

editfile.gif

  1. 实现方式概述

    直接看代码:

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

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

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

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


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

眼看他搭中台,眼看他又拆了

曾几何时,中台一度被当做“变革灵药”,嫁接在“前台作战单元”和“后台资源部门”之间,实现企业各业务线的“打通”和全域业务能力集成,提高开发和服务效率。但在中台如火如荼之际,我们可以发现各大企业又在反其道而行,纷纷不断进行“拆中台”,那么中台对于企业而言,究竟发...
继续阅读 »

曾几何时,中台一度被当做“变革灵药”,嫁接在“前台作战单元”和“后台资源部门”之间,实现企业各业务线的“打通”和全域业务能力集成,提高开发和服务效率。但在中台如火如荼之际,我们可以发现各大企业又在反其道而行,纷纷不断进行“拆中台”,那么中台对于企业而言,究竟发挥了哪些作用,当前又出现了哪些问题?今天,我们特邀了高级研发管理专家、腾讯云 TVP 程超老师,他将从搭中台到拆中台的风向转变,探讨企业软件架构的底层逻辑。



中台都在忽悠吗?都被忽悠瘸了?我们都在悄悄淘汰中台,你们还在建?最近网上充斥大量文章和观点,都在说中台过时。为什么会这样说?是因为成本与复杂性?技术限制与业务变化?还是因为组织变化?为什么会这样呢?且听我一一分析。


众所周知,中台是指企业内部的中间层平台,负责连接上下游系统,提供数据和功能服务。而在过去几年中台概念曾经风靡一时,甚至被认为是企业数字化转型的关键。然而,近年来,一些企业确实出现了对中台战略的重新评估,不再像之前那样盲目地追求中台建设。其实,中台的概念兴起于企业数字化转型的浪潮中,企业开始意识到传统的前台系统(如客户端应用)与后台系统(如企业资源规划系统)之间的断层,而中台则被认为是弥合这种断层的理想方式。


值得一提的是,关于中台的定义,业内大佬也曾经发表过一些观点:


提炼各个业务条线的共性需求,并将这些打造成组件化的资源/能力包,然后以接口的形式提供给前台各业务部门使用,这样就可以最大限度地避免“重复造轮子”的问题,也让每一个新的前台业务创新能够真正意义上“站在巨人的肩膀上”,而不用每次开辟一个新业务都像新建一家创业公司那么艰难,甚或更为艰难。——某企业资深架构师 钟华


总结而言,中台的核心点主要有以下三个:



  • 中台是为前台而生。

  • 提炼各业务条线的共性需求。

  • 减少“重复造轮子”的时间与资源浪费。


01四大层面解读中台备受追捧原因


2015年,业界首次提出“大中台、小前台”战略,是想打造统一技术架构、产品支撑体系、数据共享平台、安全体系等等,把整个组织“横”过来,支撑多种多样的业务形态。中台似乎已经成为行业标配,稍有规模的公司都建设了自己的中台,掀起了一股强劲的中台风。


中台能够解决哪些问题呢?在我看来,主要有以下四种:



  • 项目重复造轮子严重,无法形成抽象共用


中台提供了一种在企业内部建立统一的技术平台或者服务平台的模式。这个平台可以被不同部门或者项目共享和复用,从而减少了重复开发的情况。随着新业务的不断接入,共享服务也从仅能提供单一的业务功能,不断的自我进化成更健壮更强大的服务,不断适应各种业务线的新需求。同时在数据积累方面,通过数据中台将各业务的数据都沉淀下来,不断地积累数据,发挥数据的最大威力。



  • 业务变化快,缓慢的研发流程难以迅速响应


很多企业开发响应慢,其实大部分都是因为数据问题,没有做到实时、准确和统一。比如一家公司的订单,分为 C 端订单,B 端订单,共享单车订单等等,这些订单分管在不同部门中,想要做订单统计、预测等就比较困难,各类型订单彼此割裂,而如果企业只有一个订单中心的话,数据就能够在不同场景下感知到业务的变化和联动。



  • 提高资源利用率和研发效率



说起如何提高资源利用率和研发效率,我总结为中台建设五步法:插件化、服务化、配置化、异步化和数据化。这五步环环相扣,其中插件化就是提高研发效率的关键点,我们将对核心交易流程进行抽象建模设计,并通过流程引擎的改造,实现增加多个插件和扩展点。这样,不同的业务场景可以根据需求自定义其个性化逻辑,将整个交易环节抽象为一个流程框架,并在其基础上引入一系列业务扩展。这种设计使得各业务间互不干扰,更灵活地满足各自需求。


提高资源利用率,这也是必然的,服务、数据、组件等形成统一复用,各资源也不再分散,只需通过一套服务来做支撑,并且可以通过各业务线的忙闲情况,做资源的调控、比如某个业务线使用交易中台服务,高峰时期是在早上8点到晚上12点,凌晨以后基本没有业务量,则可以考虑把针对这个业务线的资源配置降低,从而实现降本增效。



  • 提高系统稳定性和可靠性


一般来说系统的故障由三个方面引起,系统 bug、变更配置、并发流量变化。而技术中台避免了各个部门为解决自身技术问题而随意修改系统设置和配置的情况,这样做有助于防止整个系统因为随意修改而出现不稳定和安全问题。


02拆分中台并非全盘否定中台


前面我主要介绍了中台能解决哪些问题,但其实很多企业在实际引入中台的过程中,也遇到了很多问题:



  • 中台与前台的边界模糊


很多前台的业务让中台接管开发,到底是接还是不接?中台的角色和范围缺乏明确界定,导致中台与业务之间的责任划分模糊不清,引发了重复建设、资源浪费和沟通成本等问题。



  • 稳定性与灵活性的冲突


稳定与灵活一直是个矛盾体,中台接入的业务线非常多,一旦出问题影响面巨大,代码质量如何把控、上线流程如何稳定、业务如何做好隔离,都需要考虑清楚。



  • 沟通障碍与目标差异


协调中台团队和业务团队之间的沟通和合作,平衡双方的需求和利益,以及处理中台和业务之间的依赖和变更,都是一项复杂的管理任务。



  • 中台规划与业务需求之间的平衡


中台的服务需求和响应之间存在不匹配,这导致中台无法满足业务的多样化和个性化需求。有时中台过度迎合业务的短期需求,却牺牲了其长期规划和可持续发展。



  • 利益分配


距离业务近的地方,比距离业务远的地方更能得到公司增长的成果,中台看似业务,其实只是沉淀,追求的是稳定和灵活。还有业务下沉的时候,会涉及到与中台的业务交接,前台业务必定会减少。如果是部门划到中台,是否会有人员变动?当中台的服务价值和收益缺乏清晰界定,将难以有效衡量自身的贡献和影响。


综上,中台看似很美好,但很多企业在实际落地的时候却因为遇到这些问题,导致陷入困境,中台建设越建越复杂,甚至有些企业对中台也逐渐失去了信心,反而成了阻碍企业发展的瓶颈。


近两年业界开始风行“拆中台”策略——将中台变“薄”,拆分到多个独立的业务单元。这使得很多企业又开始认为中台已成明日黄花,引进中台并不是一个好选择,甚至有些企业将自身发展不顺的原因也归在了中台上面,一时间中台被全盘否定了。


我个人则认为拆分中台并非全盘否定中台,而是基于自身发展阶段和市场环境的变化进行战略调整和优化。“天下大事,合久必分,分久必合”,这就意味着在中台的管理和战略中,必须根据具体情况来做出分合的决策。有时候,将中台进行分散管理或者分解成更小的部分可能更为合适,因为这样有助于更好地满足各个业务单位的需求,提高灵活性和适应性。互联网大厂们将庞大而僵化的共享中台重新组织为灵活的业务域中台,可以更好适应具体业务场景和用户需求,既能保留中台提供通用能力和协同效率的优势,又能增加中台的灵活性和个性化。


03企业应该因地制宜选择是否需要中台


首先,我想强调的是,“中台”本身并不是一个新的架构思想,这个架构思想早在若干年以前就已经有了,很多企业已经是这么做了,就像面向对象编程语言中(Java)高内聚,低耦合,便是这种思想。


当企业处在初创期,随着业务发展产生多条业务线或产品线的时候,就会面临协同方面的挑战,如果每条业务线都要自己成立技术、运维、数据等部门,这样显然是非常浪费人力和资源的。为了适应快速发展的业务,就需要成立中台部门,来抽取、复用共性的东西,形成统一,这样既能满足“小前台,大中台”策略,让业务快跑抢占市场,中台提供稳定的炮火支援,又能提高协同和研发效率。参考示意图如下:



当企业已经渡过初创期,发展已经具有较大规模时,各条业务线人员和业务场景也比初创时更加庞大和复杂,企业了将面临更加多样化的市场,以及强大的响应能力,甚至每条业务线都要独立去创新,这样统一的中台部门就会变成瓶颈,人员、响应时间、需求变化和沟通等都会成为阻碍多样化需求的绊脚石。这时候企业就需要根据市场需要,将庞大而僵化的大共享中台,拆分到各业务单元中,将中台下沉到各业务单元中,这样既能保留中台的通用和协同能力,又能针对具体业务和场景不断增加灵活性和定制性。参考示意图如下:



总而言之,中台不是一直不变的,它需要根据市场需求不断进化,演变成能够满足当前企业市场需要的形态。中台不是万能的,它只是企业数字化转型的一种重要实现路径,我们不能对中台有过高的期望,而是应该理性地回归到企业数字化转型的价值上来。


作者简介


程超,腾讯云 TVP,高级研发管理专家,14年 Java 研发经验,8年技术管理和架构经验,曾任京东架构师,易宝支付和松果出行架构技术负责人,熟悉支付和电商领域,擅长微服务生态建设和运维监控,对 Dubbo、Spring Cloud 和 gRPC 等微服务框架有深入研究,并应用于项目,帮助过多家公司进行过微服务建设和改造,目前正在建设业务中台。 合著作品《深入分布式缓存》和《高可用可伸缩微服务架构》,极客时间每日一课讲师和出品人,CSDN 博主专家。


作者:腾讯云开发者
来源:juejin.cn/post/7366175769602932755
收起阅读 »

如果按代码量算工资,也许应该这样写

前言 假如有一天我们要按代码量来算工资,那怎样才能写出一手漂亮的代码,同时兼顾代码行数和实际意义呢? 要在增加代码量的同时提高代码质量和可维护性,能否做到呢? 答案当然是可以,这可难不倒我们这种摸鱼高手。 耐心看完,你一定有所收获。 正文 1. 实现更多的...
继续阅读 »

前言


假如有一天我们要按代码量来算工资,那怎样才能写出一手漂亮的代码,同时兼顾代码行数和实际意义呢?


要在增加代码量的同时提高代码质量和可维护性,能否做到呢?


答案当然是可以,这可难不倒我们这种摸鱼高手。


耐心看完,你一定有所收获。


giphy.gif


正文


1. 实现更多的接口:


给每一个方法都实现各种“无关痛痒”的接口,比如SerializableCloneable等,真正做到不影响使用的同时增加了相当数量的代码。


为了这些代码量,其中带来的性能损耗当然是可以忽略的。


public class ExampleClass implements Serializable, Comparable<ExampleClass>, Cloneable, AutoCloseable {

@Override
public int compareTo(ExampleClass other) {
// 比较逻辑
return 0;
}

// 实现 Serializable 接口的方法
private void writeObject(ObjectOutputStream out) throws IOException {
// 序列化逻辑
}

private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
// 反序列化逻辑
}

// 实现 Cloneable 接口的方法
@Override
public ExampleClass clone() throws CloneNotSupportedException {
// 复制对象逻辑
return (ExampleClass) super.clone();
}

// 实现 AutoCloseable 接口的方法
@Override
public void close() throws Exception {
// 关闭资源逻辑
}

}


除了示例中的SerializableComparableCloneableAutoCloseable,还有Iterable


2. 重写 equals 和 hashcode 方法


重写 equalshashCode 方法绝对是上上策,不仅增加了代码量,还为了让对象在相等性判断和散列存储时能更完美的工作,确保代码在处理对象相等性时更准确、更符合业务逻辑。


public class ExampleClass {
private String name;
private int age;

// 重写 equals 方法
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}

if (obj == null || getClass() != obj.getClass()) {
return false;
}

ExampleClass other = (ExampleClass) obj;
return this.age == other.age && Objects.equals(this.name, other.name);
}

// 重写 hashCode 方法
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}


giphy (2).gif


3. 增加配置项和参数:


不要管能不能用上,梭哈就完了,问就是为了健壮性和拓展性。


public class AppConfig {
private int maxConnections;
private String serverUrl;
private boolean enableFeatureX;

// 新增配置项
private String emailTemplate;
private int maxRetries;
private boolean enableFeatureY;

// 写上构造函数和getter/setter
}

4. 增加监听回调:


给业务代码增加监听回调,比如执行前、执行中、执行后等各种Event,这里举个完整的例子。


比如创建个 EventListener ,负责监听特定类型的事件,事件源则是产生事件的对象。通过EventListener 在代码中增加执行前、执行中和执行后的事件。


首先,我们定义一个简单的事件类 Event


public class Event {
private String name;

public Event(String name) {
this.name = name;
}

public String getName() {
return name;
}
}

然后,我们定义一个监听器接口 EventListener


public interface EventListener {
void onEventStart(Event event);

void onEventInProgress(Event event);

void onEventEnd(Event event);
}

接下来,我们定义一个事件源类 EventSource,在执行某个业务方法时,触发事件通知:


public class EventSource {
private List<EventListener> listeners = new ArrayList<>();

public void addEventListener(EventListener listener) {
listeners.add(listener);
}

public void removeEventListener(EventListener listener) {
listeners.remove(listener);
}

public void businessMethod() {
Event event = new Event("BusinessEvent");

// 通知监听器:执行前事件
for (EventListener listener : listeners) {
listener.onEventStart(event);
}

// 模拟执行业务逻辑
System.out.println("Executing business method...");

// 通知监听器:执行中事件
for (EventListener listener : listeners) {
listener.onEventInProgress(event);
}

// 模拟执行业务逻辑
System.out.println("Continuing business method...");

// 通知监听器:执行后事件
for (EventListener listener : listeners) {
listener.onEventEnd(event);
}
}
}

现在,我们可以实现具体的监听器类,比如 BusinessEventListener,并在其中定义事件处理逻辑:


public class BusinessEventListener implements EventListener {
@Override
public void onEventStart(Event event) {
System.out.println("Event Start: " + event.getName());
}

@Override
public void onEventInProgress(Event event) {
System.out.println("Event In Progress: " + event.getName());
}

@Override
public void onEventEnd(Event event) {
System.out.println("Event End: " + event.getName());
}
}

最后,我们写个main函数来演示监听事件:


public class Main {
public static void main(String[] args) {
EventSource eventSource = new EventSource();
eventSource.addEventListener(new BusinessEventListener());

// 执行业务代码,并触发事件通知
eventSource.businessMethod();

// 移除监听器
eventSource.removeEventListener(businessEventListener);
}
}

如此这般那般,代码量猛增,还顺带实现了业务代码的流程监听。当然这只是最简陋的实现,真实环境肯定要比这个复杂的多。


5. 构建通用工具类:


同样的,甭管用不用的上,定义更多的方法,都是为了健壮性。


比如下面这个StringUtils,可以从ApacheCommons、SpringBoot的StringUtil或HuTool的StrUtil中拷贝更多的代码过来,美其名曰内部工具类。


public class StringUtils {
public static boolean isEmpty(String str) {
return str == null || str.trim().isEmpty();
}

public static boolean isBlank(String str) {
return str == null || str.trim().isEmpty();
}

// 新增方法:将字符串反转
public static String reverse(String str) {
if (str == null) {
return null;
}
return new StringBuilder(str).reverse().toString();
}

// 新增方法:判断字符串是否为整数
public static boolean isInteger(String str) {
try {
Integer.parseInt(str);
return true;
} catch (NumberFormatException e) {
return false;
}
}
}

6. 添加新的异常类型:


添加更多异常类型,对不同的业务抛出不同的异常,每种异常都要单独去处理


public class CustomException extends RuntimeException {
// 构造函数
public CustomException(String message) {
super(message);
}

// 新增异常类型
public static class NotFoundException extends CustomException {
public NotFoundException(String message) {
super(message);
}
}

public static class ValidationException extends CustomException {
public ValidationException(String message) {
super(message);
}
}
}

// 示例:添加不同类型的异常处理
public class ExceptionHandling {
public void process(int value) {
try {
if (value < 0) {
throw new IllegalArgumentException("Value cannot be negative");
} else if (value == 0) {
throw new ArithmeticException("Value cannot be zero");
} else {
// 正常处理逻辑
}
} catch (IllegalArgumentException e) {
// 异常处理逻辑
} catch (ArithmeticException e) {
// 异常处理逻辑
}
}
}


7. 实现更多设计模式:


在项目中运用更多设计模式,也不失为一种合理的方式,比如单例模式、工厂模式、策略模式、适配器模式等各种常用的设计模式。


比如下面这个单例,大大节省了内存空间,虽然它存在线程不安全等问题。


public class SingletonPattern {
// 单例模式
private static SingletonPattern instance;

private SingletonPattern() {
// 私有构造函数
}

public static SingletonPattern getInstance() {
if (instance == null) {
instance = new SingletonPattern();
}
return instance;
}

}

还有下面这个策略模式,能避免过多的if-else条件判断,降低代码的耦合性,代码的扩展和维护也变得更加容易。


// 策略接口
interface Strategy {
void doOperation(int num1, int num2);
}

// 具体策略实现类
class AdditionStrategy implements Strategy {
@Override
public void doOperation(int num1, int num2) {
int result = num1 + num2;
System.out.println("Addition result: " + result);
}
}

class SubtractionStrategy implements Strategy {
@Override
public void doOperation(int num1, int num2) {
int result = num1 - num2;
System.out.println("Subtraction result: " + result);
}
}

// 上下文类
class Context {
private Strategy strategy;

public Context(Strategy strategy) {
this.strategy = strategy;
}

public void executeStrategy(int num1, int num2) {
strategy.doOperation(num1, num2);
}
}

// 测试类
public class StrategyPattern {
public static void main(String[] args) {
int num1 = 10;
int num2 = 5;

// 使用加法策略
Context context = new Context(new AdditionStrategy());
context.executeStrategy(num1, num2);

// 使用减法策略
context = new Context(new SubtractionStrategy());
context.executeStrategy(num1, num2);
}
}

对比下面这段条件判断,高下立判。


public class Calculator {
public static void main(String[] args) {
int num1 = 10;
int num2 = 5;
String operation = "addition"; // 可以根据业务需求动态设置运算方式

if (operation.equals("addition")) {
int result = num1 + num2;
System.out.println("Addition result: " + result);
} else if (operation.equals("subtraction")) {
int result = num1 - num2;
System.out.println("Subtraction result: " + result);
} else if (operation.equals("multiplication")) {
int result = num1 * num2;
System.out.println("Multiplication result: " + result);
} else if (operation.equals("division")) {
int result = num1 / num2;
System.out.println("Division result: " + result);
} else {
System.out.println("Invalid operation");
}
}
}


8. 扩展注释和文档:


如果要增加代码量,写更多更全面的注释也不失为一种方式。


/**
* 这是一个示例类,用于展示增加代码数量的技巧和示例。
* 该类包含一个示例变量 value 和示例构造函数 ExampleClass(int value)。
* 通过示例方法 getValue() 和 setValue(int newValue),可以获取和设置 value 的值。
* 这些方法用于展示如何增加代码数量,但实际上并未实现实际的业务逻辑。
*/

public class ExampleClass {

// 示例变量
private int value;

/**
* 构造函数
*/

public ExampleClass(int value) {
this.value = value;
}

/**
* 获取示例变量 value 的值。
* @return 示范变量 value 的值
*/

public int getValue() {
return value;
}

/**
* 设置示例变量 value 的值。
* @param newValue 新的值,用于设置 value 的值。
*/

public void setValue(int newValue) {
this.value = newValue;
}
}

结语


哪怕是以代码量算工资,咱也得写出高质量的代码,合理合法合情的赚票子。


giphy (1).gif


作者:一只叫煤球的猫
来源:juejin.cn/post/7263760831052906552
收起阅读 »

一个30岁前端老社畜的人生经历

前言 在掘金多年,我一直是一个读者,从事前端快8年了,每天都在看一些视频和资料以及别人的日记,零零碎碎我也做过一些笔记,但是都不成体系。这些笔记至今留存在各种应用上,写了就再也没打开过,还是没有养成习惯,我希望能坚持下去,为自己的人生添加一点历史,等以后老了,...
继续阅读 »

前言


在掘金多年,我一直是一个读者,从事前端快8年了,每天都在看一些视频和资料以及别人的日记,零零碎碎我也做过一些笔记,但是都不成体系。这些笔记至今留存在各种应用上,写了就再也没打开过,还是没有养成习惯,我希望能坚持下去,为自己的人生添加一点历史,等以后老了,我还能证明我的青春有过一些记录,偶尔回味也会是一件比较幸福的事情。


近些年,感觉社会戾气挺重的,特别是疫情的时候,抖音里面的那些评论很让人糟心,现在年轻人也逐渐选择躺平,也是对社会的卷系妥协,随着经济的下滑,一般学校的研究生可能都很难找到一个比较ok的工作,更别提本科或者大专,作为学历真的拿不出手的我,更加焦虑。


从业前端快8年了,做过很多类型的项目,小到一般的H5展示网页,大到区块链应用、智能能源项目;其实回过头来看,没有什么大的成就感,我的从业经验只获得过一次奖杯,就是吃苦耐劳奖一个镀金的大手指,那还是我4年前在一家外包公司连续工作48小时做一个小程序上线后,老板看我确实辛苦,于是发了一个这个奖杯给我,后面被我娃摔坏了,就啥也没有了。


2023


2023年其实回头来看,收获并不是很大,归纳下来也没有几条:


  1. 今年非全研究生在读了

  2. 今年提交了入党申请

  3. 第二套房子装修完成

  4. 小孩来到了身边读书(之前在农村读幼儿园)

  5. 工作中学会了Vue3,能用Java做开发,同时更了解了业务方面的知识

  6. 开始了写作的习惯

  7. 跑了5场马拉松


2024 展望


2024年还有几天就到了,我希望每一年都能有点收获吧,立几个flag:


  1. 带妈妈旅游一次

  2. 成为党员积极分子

  3. 提高Java方面的基础知识,以及three 3D方面的能力

  4. 看不低于5本技术书籍,至少写30篇技术文章

  5. 还清自己的个人债务,当然不包含房贷

  6. 跑5场马拉松


行业展望


目前行业有些自媒体在唱衰,说前端已死,但是我觉得没这么悲观,国家多次强调往智能方向发展,各行业的智能得依托计算机才能智能,像什么智慧制造,智慧能源,智慧农村等等都需要计算机技术来运算和展示。前端只是比以前的要求会高一些了,在5,6年前对前端技术要求没有那么高,大家0基础都可以参与,但如今可能不行了,我觉得这是一个好事,要求高一点,薪资也会高一些。淘汰的,就是一开始就不适合这个行业的人。我目前在一家大型央企工作,还是算较为稳定,但同时也需要不断学习,因为或许某一天的淘汰人选就会是我,社会是残酷的,混日子终究不是一个好的方式。


个人真实经历分享


给大家分享一下我的个人真实经历,与君共勉。


我出生在一个普通的农村家庭,初中开始接触老虎机,高中接触网吧,17岁前没有出过县城,是个十足的井底之蛙,父亲几十年一直在外地工作,一年就回家两三次,从小就我妈妈一个人带我,她做装修的,每天早上7点就骑车外出上班了,下午5点回家,我在家做好饭菜等她回家吃了后,她就马上去田里土里种庄稼,喂了很多牲畜,高中毕业前一直都是这样(不过高中我住校,就我妈妈和我妹在家),我妈妈非常节约,从我记事起,每年只有过年她才会舍得给自己买一两件衣服,因为她觉得过年要穿新的衣服,寓意者新的一年有好的开始,从来没给自己买过首饰,也从来没有烫染过头发,也从来没有赌博过,但同时我父亲其实并不是有责任心的人,基本从不过问家里,以及我的学习。


我的学习打小从小就不好,学习生涯当了两个月的劳动委员,这就是我的荣耀,因为我觉得我小时就是sb,在干啥完全不知道,在学校就是为了吃那一顿饭,和同学天天玩,初中要毕业就被学校各种“”“好言相劝” 去中专学技术,学会拿高薪,实际上是为了赚中专学校的回扣。中专后面又把我们送去富士康,天天12个小时流水线,学校也是为了赚富士康的回扣,我的学业就是这样被卖来卖去,突然觉得有些可悲。这也是普遍读书不行的农村孩子的现状。


我的第一份工作是从2013年开始的,到现在已经差不多10年了,那就做一个时间线看看我的悲催往事吧,这也是我第一次对外讲


2013-2014.02


毕业季,和同学们坐着学校包的几辆大巴车,开到了成都郫县的富士康厂区,哪个时候富士康才在这里建厂,每天的工作就是搬东西,从另外一个地方往厂区里面搬,后面正式开工就开始了每天12个小时的白夜班交替,本来身体从小就弱,经常生病,在富士康就是上班,生病,加上富士康十三跳以及厂区经常出事,我和同学晚上提着东西,连夜翻墙走的,对,真是翻墙走的,后面线长给我打电话,我说我已经不在成都了。不过线长是我老乡,还是跟我没算旷工,算正常离职。 这里不得不说一句,在厂里,一个芝麻官都的官威都不得了,我实在看不惯,加上没前途,才走的。那时候天天12小时到手工资3500,自己上班赚钱还挺潇洒的,下班就去麻辣烫,一人吃饱全家不饿,和同学们啤酒小菜吃着,真是潇洒,厂区还有来自五湖四海的同龄妹子,都是中专生,还是挺快乐的,因为大家年龄相同,就是吃喝玩乐。自然半年才没存什么钱,灰溜溜回了老家,被我妈骂了一顿。


总结:富士康收获: 吃喝玩乐,此刻我的人生规划一无所知


2014.2-2015.07


回了老家,每天早上我妈6点就起来做饭、洗衣服、扫地等等,我起来烧柴,跟猪熬糠羹,喂猪,经常都是公鸡还没叫,我们都忙活一阵了,坚持了十天我就受不了了,因为我得承认,我出去工作后我变懒了,但是每天晚上很晚才睡,因为我在成都买了一个山寨的洛基亚手机,我开始在QQ聊天了,枯燥的生活我受不了,我要出去上班,我就去了重庆。就我妈和我妹两个人在家,这里我的说一下,我去重庆了,我妹才读幼儿园,我妈每天已经非常忙了,我妹从小就是邻居照看,她是位留守的老人,她每天给我妹妹吃好喝好的,比我奶奶好了太多,因为我妈性格很强势,和奶奶性格不合,我奶奶从来没照顾过我和我妹,都在伯伯家带他们的孩子,我妈妈经常晚上8.9点才从田里回家,我妹都是在邻居婆婆那儿吃饭睡觉,前几年她去世了,我妈妈哭了好久,因为她是我家的大贵人,现在每次走到她的坟前,我们都会去跪拜她。现在想起来,我妈太伟大了,她一生都是这么勤劳,吃苦。


到重庆了,上了一年多的厂,其实也是浑浑噩噩的,没有学历,只有在厂里做检验员,一个月2400的工资,入不敷出,因为当时听说主管也是中专学习,干了10多年才当主管,主管才5200一月工资,我觉得没前途,加上厂里玩的好的同事也走了,我也就走了


收获:C1驾-照, 成人高考专科录取通知书, iSO9O001,iSO14001 两个体系证书


2015.7-2016.7


这一年我就像做梦一样,2015年3月去学校报到,认识了班花我老婆,然后就开始交往,然后10月的时候,检查到怀孕了,过年就去了她的老家,因为怀孕了,也就准备结婚的事情了,同学们简直惊叹,纷纷问我怎么办到的?我才23岁,当父亲完全没概念,不过这也满足了我家人的愿望,穷人家里早当家,就在这一年,我妈妈存了一辈子的钱就被我花完了。10月检查怀孕,11月孩子她妈跟我父母說了要了买房买车的事情,我妈妈非常反对,后面我外婆对我妈说:你就这一个儿子,你都不帮他,以后他不恨你吗? 我妈妈想了几晚她咬咬牙还是同意了,过年去了女方家,她父母挺喜欢我的,我妈妈第二年年初付了房子首付26万,后面装修8万,买车8万,结婚7万,对没听错,全是我妈出的,她平时在农村做装修,有的时候包工,一个月7,8000有的时候包工一万多一个月工资,省吃俭用,全部存下的,都被我全部榨干了,好在岳父岳母没有要我一分彩礼,还给我2万块钱装修,他们也是农村人,也是吃了很多苦,2万得他们在工地干很久了,他们在老家为我们办了十多桌,请了一村的人来吃饭。


我妈后面才跟我说,这么多钱,我爸只出了一万块钱,我现在都不可思议,他在外面这么多年的钱去哪儿了? 但我也不恨他,毕竟每个人想法不同,他没有义务要给我出这些费用,不过好在之前房子一个平方8000,算是重庆比较贵的房子了,现在26000一个平方,算是赚了一些,有了一个家庭的财产保障,之前还要贵一点,现在房地产不行降了一些。


2016年7月后我出来也是误打误撞的进入了计算机这个行业,我之前压根就不了解这个行业,是看的招聘网站,招聘信息写的5000的工资,我那时候才3000多,在做销售,简直是高薪了,结果去了才知道,原来是计算机培训学校,耐不住那个美女姐姐各种软磨硬泡,我还是去学了计算机,当然,钱还是我妈跟我出的,因为也是孩子她妈跟我妈说这个行业好,比上厂强,我妈才听了她的,要是我说,那根本不管用。


收获:买房,结婚,买车,装修,好像所有的大事这段时间都基本完成了,虽然都是我妈出的钱


2016.7-2018.7


2016年10月孩子出生了,我也从培训学校出来工作了一段时间,培训机构学了4个月,时间都忙家里的事情去了,所以一毕业面试了20多家公司,都被打击了,每次都想放弃,但是回到家,看到家人,我都心里说不出的滋味,为此也哭了好多次,孩子她妈跟着我这些年,没买过一件超过300的衣服,全是淘宝的几十块一件的,我妈妈为了我在农村不管工作有多远,天气有多冷,都要去工作,我觉得我就是累赘,那时我24岁,我压力可能已经超出了我的极限,房贷3000,孩子每个月2000,车子和物业1000,还有生活费,每个月花销都要8000,有的时候孩子一生病就可能要一万以上,我后面找到一份工作4500,是切图仔,每天就jQuery,才稍微帮家里分担了一下,其实压力全部都在我妈妈哪儿,我妈妈为了我,操碎了不少心。


2018年我拿到了大专学历,然后随即开始了报名成人高考本科,孩子她妈就没有报名了,她觉得女孩子大专就够了,加上家里也没钱


收获:
1.当父亲了,压力更大了。我必须得成熟一点了,在前端行业算是正式入行了,通过自己每天工作之外,在各种QQ群里聊天拉业务,我的外快收入也逐渐多了起来,虽然很多时候工作到2点,但是总算是跟家庭减少一点压力,虽然期间换了3家工作,但是我的工资也高了一些,月薪到手9000了,加上外快时多时不多,一个月平均有个1.3的收入了;我也有一点点经济带家人去自驾游了(不过只有两次)
2.成人高考本科录取通知书


2018.7-2020.7


这一年通过我经常在QQ群聊天的好友介绍,我到了一家外包公司(他当时也拿了回扣,但是我也很感激他。因为他教我怎么面试,跟我出面试题),因为通过了客户的面试,我厚着脸皮开到了1.6一个月的工资。到手14k,我当时说我拿这么多,家里人都以为骗他们,不过等发第一个月工资的时候,他们觉得我以前选择计算机是对的,我妈妈也多了很多笑容,这个时候小孩也是大了,妈妈一个人带着孩子读幼儿园,我和孩子他妈在重庆上班,我妈也在上班,家庭算是好了起来,大家笑容也多了起来。当时加上的我的外快业务,一年也能赚个6,7万,因为大家知道我在做这块,后面一些朋友陆续的给我介绍,我也会给他们相对满足的回扣。平均一个月收入已经超过2万了,不过有点不厚道的是,我上班没事也在做外包。


收获:自己随着年龄的增加,人的心态也在发生变化,随着收入多了起来,脸上的笑容也多了,家庭矛盾也少了,日子也越来越有奔头了


2020.7-2023.10


2021年因为公司被客户从人力资源池给移除了,我们没有资格做客户的业务了,我随即也面临着失业,我28岁了,其实我还是很恐慌的,因为家庭开支这么大,加上我长期做外包,技术底子很薄弱,可能失业找不到这么高的工资,所以我很担忧,工作随便都是全日制本科起,我一个半罐子学历,能干点啥,但是后面客户对我的技术能力还有做事能力还算比较认可的,给我推荐了另外还在资源池的外包公司,但是我都不去,我觉得外包没有前途,同时他们也开不起16k的工资(虽然技术不咋的,但是现在是这工资,让我转到其他外包公司才13k,我也心有不甘啊),最后客户他们把我转进了客户内部,于是我一个中专生进入了体制内,不过在进入之前,各部长对我的学历还是有一定质疑,不过我的直系领导以自身名誉担保,我还是通过他们的几轮面试,最终成功进入。进入到体制内,身边的同事都是985的博士,研究生,还有都是留学回来的,也有一些清华北大北航的研究生,其实还是很自卑的,大家学历这么高,有的时候不得不承认,他们的专业素养,学术知识,脑回路都比较灵活,他们的英语都非常厉害,有的同事28岁都上中央台了,太强了,我妈妈非常担心我的学历,怕我一在公司一犯错就被开除,其实有的时候她还是多虑了,我也在尽力 的追赶他们,希望差距尽量小一点点。所以在2023年我拿到了非全的研究生录取通知书,继续读软件工程。在公司也申请入了党,因为他们全是党员,我在公司负责两个部门的前端管理工作,也在带一些校招的研究生,同时我也在2022年5月买了第二套房,首付42万,其他非要加上差不多47万,为此把车卖了一万五,凑点首付钱,我妈妈又出了26万,再一次吧我妈给榨干了,这次我爸也没有出一分钱。不过还在我是组合贷,每个月只出商业贷,每个月出差不多2000的房贷,第一套房贷也还有20多万就还完了,2023年我孩子读小学了,我妈妈也来重庆带孩子了,为此她没有继续在做装修了,每天接送小孩子上学放学,中间有两个小时去一家店里打扫卫生,每个月2500的收入。我在每个月给他2000多生活费,虽然她不太适应城市的生活,觉得城市的人会看不起她是农村人,走哪儿都不会用导航,但是她慢慢的还是习惯了,城市的人并没有觉得自己高人一等,她还算是过得比较快乐,现在我的收入在重庆来说还算OK,外快也有,但是我也不想太累了,我想把时间利用在学习上,因为同事们都很强,我尽量向他们看齐,


收获:本科毕-业-证,非全研究生录取通知书,稳定的工作,第二套房子首付+装修(因为旁边学校更好一点)


最后


今年30了,孩子已经7岁了,我已经开始享受十天的年假了。其实我已经算是走了很多路,深夜哭了很多次,第二天依旧怀揣着斗志,我数次回想我这30年的发展,其实都过得不是很灿烂,或许平凡的就是这样,一无所有的农村人,只能靠父母,如果父母靠不住,那自己也开心点,随着父母的年龄越来越大,我的压力也变大了,他们很多时候会征求我的意见,我也要拿钱支持他们了,也有一些感悟:


1.每年还是得有一个目标,细分到每个月,每一周去完成它,如果没有目标,那就认真的把每一件事情尽量做好,贵人每个人都会遇到,只是看能否抓住,可能会是工作中,生活中的某一个,他愿意提携你一下,真的能少走很多弯路,我的经验告诉我,我有两三次都有贵人帮助我,只是我没有把握住,就像之前一起做区块链,一起做电商的公司老板就很喜欢我,因为我比较踏实,没攻击性,人老实。但是我还是太年轻,很多时候做事不够成熟,就这样和机会擦肩而过,他们现在已经是财富自由了


2.与人交流,说话适可而止,充分尊重他人,聊天中尽量带点幽默,学习一下话题的扩展


3.没事多扩展一下人脉,我才开始培训机构出来,基本没学,全靠在QQ群聊天的人带的我,怎么学,我每次遇到问题我都会问他们,他们在远程帮我改bug,这样我才能保住工作


4.多学习,我看了下我现在的同事,他们没事不会在网上划水,而是都在学习,最敬佩的是旁边那位,一年了从小白,到一名技术骨干,技术成长太快了,他除了学习,每天还不断在看书,我只能说佩服,我很多时候都在刷抖音,我自愧不如,我有罪


5.想办法融入更好的圈子,我之前待得公司都不大,都是外包公司,大家学历都很低,没有一个是985或者211的本科生,大家上班都在聊吃喝嫖赌,主要是聊女人那点事。但是现在我发现身边的同事几年了,没有一个人说过一句脏话,说话总会特别舒服,因为你能感受到他非常尊重你,说话也非常温柔,绝不会听到SEX,tm 这种言词。


6.接受自己的平凡。我以前有很多想法,内心很浮躁,后面发现读书越少的人想法越多,到最后越来越差,债务缠身,本来都是资本的牟利工具,平凡开心更好,把家里的事情处理好,生活上逐渐改善品质,就已经很不错了,在平凡的生活多点浪漫,对未来有一点期待,但不浮夸,我觉得就已经很不错了


7.多多提高自己的综合素质吧,我是一个比较随心的人,但是后面发现,穿的邋遢,说话幼稚,身形不行,走在外面都没自信,何况别人会怎么看你呢,这一点我也在慢慢提高


8.最后我的技术其实很一般,node,vue,react,java,python,php,微信小程序,three.js 这些都有做过,有的都是为了外包业务减少点成本才去学的,但是要说哪一个比较深入,可能就前端的这几个框架,因为天天都在做,偶尔看看掘金的技术文档,但是要说特别深入的,抱歉没有,因为我从误打误撞开始进入这个行业,我的目的不是因为喜欢,而是因为工资高一点,我没有想给要为这个行业带来些什么,我只想活着,我现在觉得我没有特别喜欢做的行业,我不清楚我能在这个行业做多少年,但是只要做,我就把它做好,因为做工作的态度跟自己的喜欢没有关系,做事是做人,自己的工作做好了,下个同事才会很轻松。同时也在尽可能的弥补一些自己的软实力。希望在某一天,有更好的机会,自己能抓得住,自己不会为了自己的能力而自卑!


9.2023-12-21 17:28:53 下班了


作者:程序员xx1
来源:juejin.cn/post/7314877697996947482
收起阅读 »

为什么老家的黄瓜比北京的便宜?普通人应该去经济发达地区谋生

有时候会跟着家人去逛菜市场,有次我岳母说北京的菜真贵,原本在农村老家2、3块的黄瓜到了北京要5、6块一斤,感觉真是土气。 出于好奇,我就开始琢磨这个问题。 我问了财务专业的老婆大人,老婆大人的答案很直接,大概意思是北京的供应链比老家贵的多,比如运输成本、用于卖...
继续阅读 »

有时候会跟着家人去逛菜市场,有次我岳母说北京的菜真贵,原本在农村老家2、3块的黄瓜到了北京要5、6块一斤,感觉真是土气。


出于好奇,我就开始琢磨这个问题。


我问了财务专业的老婆大人,老婆大人的答案很直接,大概意思是北京的供应链比老家贵的多,比如运输成本、用于卖菜的房租成本、还有销售蔬菜的人力成本……然后这些成本都加到蔬菜价格上,自然就高了。


我哈哈一笑,老婆大人的回答自然是不敢出声反驳的,但我可以在心里揣测。我觉得老婆大人说的没问题,但并不深刻,于是我开始浮想联翩……


一 自产自销


在北京生活久了,我发现就蔬菜来说并非不可以自产的。我之前生活的小区,就有销售自产蔬菜的大爷大妈。老婆说是因为供应链比老家的贵,可大爷大妈自产自销不需要供应链啊,老婆的回答就没法解释了。


在我的印象里面,大爷大妈自产的蔬菜也是要比老家的贵的,为什么会如此?


我思考的结果是,大爷大妈是询价定价的。简单来说大爷大妈一开始是不知道定价多少合适的,定的太低心里不得劲,定的太高没人买,所以大爷大妈会去附近菜市场询价,然后定一个比菜市场低的价格,这样既解决了心里不得劲问题,也解决了卖不出去问题。


但这本质上还是因为北京市场上的蔬菜价格比老家的贵。


二 供应链


如果不是自产自销,那就是依靠供应链了。在老家市场销售的黄瓜和北京市场销售的黄瓜,假设有着相同的货源,但背后供应链体系肯定是不同的。


在我们老家有那种大集,就是固定一个时间周期在一个固定地点开展买卖。


大集上的菜农销售蔬菜基本可以忽略掉租房成本,然后因为菜农本身就是销售人员,也省下销售成本,这样整体的供应链成本就下来了,就能够低价销售蔬菜了。


这也就是老婆说的供应链成本不同。


三 边际


边际是经济学核心概念之一,是一种思维方式,就是永远看市场中最后一个人的行为或者最后一个产品的情况。


比如在劳动力市场上,工资不是市场中的平均水平的劳动者决定的,而是最后一个参加劳动的人决定的,要看给他多高的工资他才愿意去做这份工作,同时也要看他有多大的贡献工厂才雇用他,两者相等的时候才是市场里的均衡工资


拉长时间看蔬菜市场中的黄瓜价格会受边际影响。简单来说,蔬菜超市会因为追求最大经济利益调整蔬菜价格,直到找到均衡价格。


结果因为北京的购买者更能容忍价格的波动,导致北京的黄瓜比农村的贵。


四 可替代性


北京的购买者之所以更能容忍价格波动,原因之一是北京生活的人相对老家农村生活的人,在蔬菜这件事上,可替代性弱。


北京生活的人,少有自产自销者;而农村老家的人,多可以自产自销。这虽然不是价格上涨的直接原因,但无形当中推高了均衡价格。


五 购买力


北京的购买者之所以更能容忍价格波动,原因之二是北京的购买力强。


我之前在小区周边的永辉超市看到有机蔬菜,标记35.98/kg,我当时想什么人会买这种蔬菜呢?


蔬菜.jpg


从结果来说,北京购买者购买力强是老家的黄瓜比北京便宜的直接原因


尾声


黄瓜的价格只是一个小的不能再小的缩影,老家的黄瓜之所以比北京便宜,根本原因是老家农村和北京有着根本不同的经济结构


北京这种大城市有更高效的资源利用率,更强的生产能力和更高效的生产效率,结果就是北京创造了更多财富,即使普通人在这里也会有较高收入,高收入下基于边际思维演化的黄瓜价格,形成高的均衡价格。


多年前我曾极力劝我一个朋友来北京,他在四线城市很努力,但始终没能赚大钱。当初我说不出更具说服力的让他来北京的理由,但我认为我现在找到了:他赚钱少可能不是因为他不够努力,而是因为他本来就不在一个发达的经济体里,也就难以享受到比较高的红利。


所以我给普通奋斗者的建议是去经济发达地区谋生吧!


作者:通往自由之路pro
来源:juejin.cn/post/7353233940545323045
收起阅读 »

Activity界面路由的一种简单实现

1. 引言 平时Android开发中,启动Activity是非常常见的操作,而打开一个新Activity可以直接使用Intent,也可以每个Activity提供一个静态的启动方法。但是有些时候使用这些方法并不那么方便,比如:一个应用内的网页需要打开一个原生Ac...
继续阅读 »

1. 引言


平时Android开发中,启动Activity是非常常见的操作,而打开一个新Activity可以直接使用Intent,也可以每个Activity提供一个静态的启动方法。但是有些时候使用这些方法并不那么方便,比如:一个应用内的网页需要打开一个原生Activity页面时。这种情况下,网页的调用代码可能是app.openPage("/testPage")这样,或者是用app.openPage("local://myapp.com/loginPage")这样的方式,我们需要用一种方式把路径和页面关联起来。Android可以允许我们在Manifest文件中配置<data>标签来达到类似效果,也可以使用ARouter框架来实现这样的功能。本文就用200行左右的代码实现一个类似ARouter的简易界面路由。


2. 示例


2.1 初始化


这个操作建议放在Application的onCreate方法中,在第一次调用Router来打开页面之前。


public class AppContext extends Application {
@Override
public void onCreate() {
super.onCreate();
Router.init(this);
}
}

2.2 启动无参数Activity


这是最简单的情况,只需要提供一个路径,适合“关于我们”、“隐私协议”这种简单无参数页面。


Activity配置:


@Router.Path("/testPage")
public class TestActivity extends Activity {
//......
}

启动代码:


Router.from(mActivity).toPath("/testPage").start();
//或
Router.from(mActivity).to("local://my.app/testPage").start();

2.3 启动带参数Activity


这是比较常见的情况,需要在注解中声明需要的参数名称,这些参数都是必要参数,如果启动的时候没有提供对应参数,则发出异常。


Activity配置:


@Router.Path(value = "/testPage",args = {"id", "type"})
public class TestActivity extends Activity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//加载布局...

String id = getIntent().getStringExtra("id"); //获取参数
int type = getIntent().getIntExtra("type", 0);//获取参数
}
}

启动代码:


Router.from(mActivity).toPath("/testPage").with("id", "t_123").with("type", 1).start();

2.4 启动带有静态启动方法的Activity


有一些Activity需要通过它提供的静态方法启动,就可以使用Path中的method属性和Entry注解来声明入口,可以提供参数。在提供了method属性时,需要用Entryargs来声明参数。


Activity配置:


@Router.Path(value = "/testPage", method = "open")
public class TestActivity extends Activity {

@Router.Entry(args = {"id", "type"})
public static void open(Activity activity, Bundle args) {
Intent intent = new Intent(activity, NestWebActivity.class);
intent.putExtras(args);
activity.startActivity(intent);
}

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//加载布局...

String id = getIntent().getStringExtra("id"); //获取参数
int type = getIntent().getIntExtra("type", 0);//获取参数
}
}

启动代码:


Router.from(mActivity).toPath("/testPage").with("id", "t_123").with("type", 1).start();

3. API介绍


3.1 Path注解


这个注解只能用于Activity的子类,表示这个Activity需要页面路由的功能。这个类有三个属性:



  • value:表示这个Activity的相对路径。

  • args:表示这个Activity需要的参数,都是必要参数,如果打开页面时缺少指定参数,就会发出异常。

  • method:如果这个Activity需要静态方法做为入口,就将这个属性指定为方法名,并给对应方法添加Entry注解。(注意:这个属性值不为空时,忽略这个注解中的args属性内容)


3.1 Entry注解


这个注解只能用于Activity的静态方法,表示这个方法作为打开Activity的入口。仅包含一个属性:



  • args:表示这个方法需要的参数。


3.2 Router.init方法



  • 方法签名:public static void init(Context context)

  • 方法说明:这个方法用于初始化页面路由表,必须在第一次用Router打开页面之前完成初始化。建议在Application的onCreate方法中完成初始化。


3.3 Rouater.from方法



  • 方法签名:public static Router from(Activity activity)

  • 方法说明:这个方法用于创建Router实例,传入的参数通常为当前Activity。例如,要从AActivity打开BActivity,那么传入参数为AActivity的实例。


3.4 Rouater.to和Rouater.toPath方法



  • 方法签名:




  1. public RouterBuilder to(String urlString)

  2. public RouterBuilder toPath(String path)




  • 方法说明:这个方法用于指定目标的路径,to需要执行绝对路径,而toPath需要指定相对路径。返回的RouterBuilder用于接收打开页面需要的参数。


3.4 RouterBuilder.with方法



  • 方法签名:




  1. public RouterBuilder with(String key, String value)

  2. public RouterBuilder with(String key, int value)




  • 方法说明:这个方法用于添加参数,对应Bundle的各个put方法。目前只有常用的Stringint两个类型。如有需要可自行在RouterBuilder中添加对应的方法。


3.4 RouterBuilder.start方法



  • 方法签名:public void start()

  • 方法说明:这个方法用于打开页面。如果存在路径错误、参数错误等异常情况,会发出对应运行时异常。


4. 实现


import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;

import androidx.annotation.Keep;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;

@Keep
public class Router {

public static final String SCHEME = "local";
public static final String HOST = "my.app";
public static final String URL_PREFIX = SCHEME + "://" + HOST;

private static final Map<String, ActivityStarter> activityPathMap = new ConcurrentHashMap<>();

public static void init(Context context) {
try {
PackageManager packageManager = context.getPackageManager();
PackageInfo packageInfo = packageManager.getPackageInfo(
context.getPackageName(), PackageManager.GET_ACTIVITIES);

for (ActivityInfo activityInfo : packageInfo.activities) {
Class<?> aClass = Class.forName(activityInfo.name);
Path annotation = aClass.getAnnotation(Path.class);
if (annotation != null && !TextUtils.isEmpty(annotation.value())) {
activityPathMap.put(annotation.value(), (Activity activity, Bundle bundle) -> {
if (TextUtils.isEmpty(annotation.method())) {
for (String arg : annotation.args()) {
if (!bundle.containsKey(arg)) {
throw new IllegalArgumentException(String.format("Bundle does not contains argument[%s]", arg));
}
}
Intent intent = new Intent(activity, aClass);
intent.putExtras(bundle);
activity.startActivity(intent);
} else {
try {
Method method = aClass.getMethod(annotation.method(), Activity.class, Bundle.class);
Entry entry = method.getAnnotation(Entry.class);
if (entry != null) {
for (String arg : entry.args()) {
if (!bundle.containsKey(arg)) {
throw new IllegalArgumentException(String.format("Bundle does not contains argument[%s]", arg));
}
}
method.invoke(null, activity, bundle);
} else {
throw new IllegalStateException("can not find a method with [Entry] annotation!");
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
});
}
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}

public static Router from(Activity activity) {
return new Router(activity);
}

private final Activity activity;

private Router(Activity activity) {
this.activity = activity;
}

public RouterBuilder to(String urlString) {
if (TextUtils.isEmpty(urlString)) {
return new ErrorRouter(new IllegalArgumentException("argument [urlString] must not be null"));
} else {
return to(Uri.parse(urlString));
}
}

public RouterBuilder toPath(String path) {
return to(Uri.parse(URL_PREFIX + path));
}

public RouterBuilder to(Uri uri) {
try {
if (SCHEME.equals(uri.getScheme())) {
if (HOST.equals(uri.getHost())) {
String path = uri.getPath();//note: 二级路径暂不考虑
ActivityStarter starter = activityPathMap.get(path);
if (starter == null) {
throw new IllegalStateException(String.format("path [%s] is not support", path));
} else {
NormalRouter router = new NormalRouter(activity, starter);
for (String key : uri.getQueryParameterNames()) {
if (!TextUtils.isEmpty(key)) {
router.with(key, uri.getQueryParameter(key));
}
}
return router;
}
} else {
throw new IllegalArgumentException(String.format("invalid host : %s", uri.getHost()));
}
} else {
throw new IllegalArgumentException(String.format("invalid scheme : %s", uri.getScheme()));
}
} catch (RuntimeException e) {
return new ErrorRouter(e);
}
}

public static abstract class RouterBuilder {
public abstract RouterBuilder with(String key, String value);

public abstract RouterBuilder with(String key, int value);

public abstract void start();
}


private static class ErrorRouter extends RouterBuilder {
private final RuntimeException exception;

private ErrorRouter(RuntimeException exception) {
this.exception = exception;
}

@Override
public RouterBuilder with(String key, String value) {
return this;
}

@Override
public RouterBuilder with(String key, int value) {
return this;
}

@Override
public void start() {
throw exception;
}
}

private static class NormalRouter extends RouterBuilder {
final Activity activity;
final Bundle bundle = new Bundle();
final ActivityStarter starter;

private NormalRouter(Activity activity, ActivityStarter starter) {
this.activity = Objects.requireNonNull(activity);
this.starter = Objects.requireNonNull(starter);
}

@Override
public RouterBuilder with(String key, String value) {
bundle.putString(key, value);
return this;
}

@Override
public RouterBuilder with(String key, int value) {
bundle.putInt(key, value);
return this;
}

@Override
public void start() {
starter.start(activity, bundle);
}
}

@FunctionalInterface
private interface ActivityStarter {
void start(Activity activity, Bundle bundle);
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Path {
String value();

String method() default "";

String[] args() default {};
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Entry {
String[] args() default {};
}
}

5. 注意



  1. 这个工具的一些功能与ARouter类似,实际项目中建议使用ARouter。如果有特殊需求,例如,页面参数的检查或定制具体打开行为,可以考虑基于这个工具进行修改。

  2. 使用了Pathmethod属性时注意添加对应的混淆设置,避免因混淆而导致找不到对应方法。


作者:乐征skyline
来源:juejin.cn/post/7235639979882463292
收起阅读 »

基于SSE的实时消息推送

背景 小盟 AI 助手项目中需要服务端把 AI 模型回调回来内容,实时推送到客户端,展示给用户;整个流程需要一个能够快速支持上线的服务端推送方案。经过对现有的一些服务端推送方案进行调研,并结合项目的周期、实现成本、用户体验等多方综合考量,最终选择了 ...
继续阅读 »



背景


小盟 AI 助手项目中需要服务端把 AI 模型回调回来内容,实时推送到客户端,展示给用户;整个流程需要一个能够快速支持上线的服务端推送方案。经过对现有的一些服务端推送方案进行调研,并结合项目的周期、实现成本、用户体验等多方综合考量,最终选择了 Server-Sent Events(SSE)方案进行实践。


首先服务端推送,是一种允许应用服务器主动将信息发送到客户端的能力,为客户端提供了实时的信息更新和通知,增强了用户体验。


服务端推送主要基于以下几个诉求:


(1)实时通知:在很多情况下,用户期望实时接收到应用的通知,如新消息提醒、活动提醒等。


(2)节省资源:如果没有服务端推送,客户端需要通过轮询的方式来获取新信息,会造成客户端、服务端的资源损耗。通过服务端推送,客户端只需要在收到通知时做出响应,大大减少了资源的消耗。


(3)增强用户体验:通过服务端推送,应用可以针对特定用户或用户群发送有针对性的内容,如优惠活动、个性化推荐等。这有助于提高用户对应用的满意度和黏性。


方案对比



轮询: 是一种较为传统的方式,客户端定时地向服务端发送请求,询问是否有新数据。服务端只需要检查数据状态,然后将结果返回给客户端。轮询的优点是实现简单,兼容性好;缺点是可能产生较大的延迟,且对服务端资源消耗较高。


长轮询(Long Polling): 轮询的改进版。客户端向服务器发送请求,服务器收到请求后,如果有新的数据,立即返回给客户端;如果没有新数据,服务器会等待一定时间(比如30秒超时时间),在这段时间内,如果有新数据,就返回给客户端,否则返回空数据。客户端处理完服务器返回的响应后,再次发起新的请求,如此反复。长轮询相较于传统的轮询方式减少了请求次数,但仍然存在一定的延迟。   


WebSocket: 一种双向通信协议,同时支持服务端和客户端之间的实时交互。WebSocket 是基于 TCP 的长连接,和HTTP 协议相比,它能实现轻量级的、低延迟的数据传输,非常适合实时通信场景,主要用于交互性强的双向通信。


SSE: 是一种基于 HTTP 协议的推送技术。服务端可以使用 SSE 来向客户端推送数据,但客户端不能通过 SSE 向服务端发送数据。相较于 WebSocket,SSE 更简单、更轻量级,但只能实现单向通信。


图片


图片


小盟 AI 助手项目需要快速上线且保证要用户较好的使用体验。鉴于 SSE 技术的轻量、实现简单、不增加额外的资源成本;当前业务场景也只需要服务端到用户端的单向的字符推送。非常适合项目需要。所以决定使用 SSE 来实现内容推送。****


深入 SSE



SSE 服务端推送,它基于 HTTP 协议,易于实现和部署,特别适合那些需要服务器主动推送信息、客户端只需接收数据的场景。有以下特点:


1、简单:基于 HTTP,无须额外的协议或者类库支持。主流浏览器都支持。


2、事件流:使用"事件流"(Event Stream)将数据从服务器发送到客户端。每个事件都可以包含一个事件标识符、事件类型和数据字段。客户端可以根据这些信息来解析和处理接收到的数据。


3、自动重连:意外断开时会自动尝试重新连接。可以确保了在网络故障或连接中断后能够及时恢复通信,为用户提供连续的数据流。重连时会在 HTTP 头中的Last_Event_ID 带上上一次的数据 ID,便于服务端返回后续数据。


4、单向推送:只能从服务端推送数据到客户端。


图片


SSE 消息体介绍:


图片


SSE消息体示例:


图片


服务端主要使用 Spring,其对 SSE 主要提供了两种支持:



  • Spring WebMVC:传统的基于 Servlet 的同步阻塞编程模型,即 同步模型 Web 框架。

  • Spring WebFlux:异步非阻塞的响应式编程模型,即 异步模型 Web 框架。          


项目基于springboot,所以选择使用前者实现。SseEmitter emitter = new SseEmitter(); 一句代码就可以建立一个 SSE 连接。


实践



后端实现


建立一个SseEmitterManger,统一管理当前服务 SSE 连接的创建、释放以及数据推送。结合 Redis 缓存可实现集群环境 SSE 连接的管理。


核心逻辑如下:



  • 连接池维护,设定一个上限,避免过大,导致内存问题。


static final Map<String, SseEmitter> sseCache =     new ConcurrentHashMap<>(300)          


  • 建立SSE连接,为每个连接建立一个唯一的MsgId,用来维护SSE连接与客户端的关系了;在 Redis 缓存中存入MsgId和当前机器节点的IP和Port,这样可以找到SSE 连接所在的服务结点,然后通过 HTTP 请求转发需要发送的数据到对应的服务节点上进行处理。


sse = new SseEmitter()sseId = "sse_xxx";redisKey= "aisse:" + bosId + "_" + wid ipPort = "10.10.10.10:8080"redis.hset(redisKey, msgId, ipPort)sseCache.put(msgId, sseEmitter);


  • 获取持有连接的 pod ipPort;根据 IP 发起请求。


ipPort = redisUtil.hashGet(redisKey, msgId)


  • 获取当前服务结点的SSE连接,发送数据。


sseEmitter = sseCache.get(msgId)sseEmitter.send(msgJson)          


  • 释放SSE连接


SseEmitter sseEmitter = sseCache.get(msgId);sseEmitter.complete();sseCache.remove(msgId);redisUtil.hashDel(redisKey, msgId);

**核心流程图如下: **  


图片


需要注意的是开启 SSE 连接接口的整个链路都要支持长连接。例如使用 Nginx 则要开启长连接的配置:



  • keepalive 用于控制可连接整个 upstream servers 的 HTTP 长连接的个数,即控制总数。

  • proxy_http_verion 用于控制代理后端链接时使用的 HTTP 版本,默认为 1.0。要想使用长连接,必须配置为 1.1。

  • proxy_set_header 需要设置为 Connection "",否则则发往 upstream servers 的请求中,Connection header 的值将为close,导致无法建立长连接。   


http {        upstream keepAliveService {            server 10.10.131.149:8080;            keepalive 20;        }            server {            listen 80;            server_name keepAliveService;            location /keep-alive/hello {                proxy_http_version 1.1;                proxy_set_header Connection "";                proxy_pass http://keepAliveService;            }        }}

**前端实现 **  


前端可以使用组件 @microsoft/fetch-event-source 来实现。


npm i @microsoft/fetch-event-source
import { fetchEventSource } from '@microsoft/fetch-event-source';let controller = new AbortController(); let eventSource = fetchEventSource('apiUrl', { method: 'POST', headers: { 'Content-Type': 'application/json', 'token': '....' }, signal: controller.signal, body: JSON.stringify({ ... // 传参 }), onopen() { // 建立连接 }, onmessage(event) { // 接收信息 // 成功之后满足某些条件可以使用AbortController关闭连接 controller.abort() eventSource?.close && eventSource.close(); }, onerror() { // 服务异常 controller.abort() eventSource?.close && eventSource.close(); }, onclose() { // 服务关闭 },})

总结



SSE 轻量级的服务端单向推送技术;具有支持跨域、使用简单、支持自动重连等特点。相对于 WebSocket 更加轻量级,如果需求场景客户端和服务端单向通信,那么 SSE 是一个不错的选择。


作者:微盟技术中心
来源:juejin.cn/post/7317325043541032970
收起阅读 »

社会现实告诉我,00后整顿职场就是个笑话

00后整顿职场,也算是我之前的关键词吧。 我硬怼老板要加班费和提成,和他们辩论什么是我的财产,什么是公司的财产。 甚至还能在即将被开除的时候,反将一军把老板开除。 而正是因为这一次把老板开除,让我得到了机会。可以站在了相对于之前更高的位置,来俯瞰整个职场。 也...
继续阅读 »

00后整顿职场,也算是我之前的关键词吧。


我硬怼老板要加班费和提成,和他们辩论什么是我的财产,什么是公司的财产。


甚至还能在即将被开除的时候,反将一军把老板开除。


而正是因为这一次把老板开除,让我得到了机会。可以站在了相对于之前更高的位置,来俯瞰整个职场。


也真正意义上让我感受到了,00后整顿职场,就是一个互联网笑话罢了。


1、职场宫斗,成功上位


我之前在苏州工作,堪称工作中的宫斗,并且在这场宫斗大戏中胜出,将原有的项目负责人开除,成功上位。


而这个项目存在的问题非常多,我就在六月被派遣去项目的总部合肥进行学习,等到打通项目的全部链路后,再回到苏州。


届时我将以这个项目的负责人,重新搭建团队,开展这个项目。所以我在合肥那边,以员工的身份深入各个工作组进行学习。


在市场部,运营部的办公大厅工作过,也在各部门的独立办公室工作过。


我感觉自己像个间谍,一边在以平级的打工人身份和我的同事们相处,一边又以苏州负责人的身份,参与那些领导才能参与的内部会议。


2、内心变化的开端


我在合肥总部工作中,接触了很多躺平摆烂的同事,但这个“躺平摆烂“要加上双引号。


他们是00后,90后,甚至有85后。如果放在三个月前,我可以不假思索地说,他们全都是我最讨厌的人。他们如同牛羊一般任人宰割,上级让加班,他们就加班,有时候加班甚至超过四五个小时也没有怨言。


我甚至从来没听他们感慨过为什么没有加班费。亲眼看着他们被自己的上级用一些与工作无关的鸡毛蒜皮之事骂得狗血淋头,但他们也只会在被骂完之后,背地里吐槽那个领导估计是在家被老婆骂了,才来拿他们泄愤。


我打听了他们的工资,只能说中规中矩,起码不是能让人当牛做马的数字。偶尔我见到一两个有骨气的人,觉得拿这么点钱就应该干这么点事。干不爽就马上离职,但马上就会有下一个人替补他的位置,形成闭环。


我惊讶于怎么有人能惹到这个地步,但后来和他们日渐熟落,我们一起吃饭,一起打游戏,一起下班顺路回家,还参加了他们的生日聚会。我发现他们活得其实真的很洒脱。一切都是随遇而安,下班时间一到,他们就真的可以无忧无虑。


因为他们有一份工资还行的工作,养活自己。他们没有啃老,也没有用卑鄙的手段,去抢想要努力的人应该分到的蛋糕,也压根不去想要赚很多钱,因为没有什么需要太高消费的需求。


加上现在的环境,找到一份可观收入的工作确实很难。所以公司偶尔的加班,领导偶尔的泄愤,这些毕竟还是少数时候的偶尔,也都没有超过他们的心理承受阈值,那也就得过且过了。


所以我们其实都一样,只是个普通人罢了。而像我们这样的普通人,取之不尽,用之不竭。这到底是好事还是坏事呢?


3、复杂的职场生态环境


建立在这个基础上,视觉转换到高层领导们这里。他们当着我的面说,这样的人就是个底层打工仔,缺人就招,加班照旧,心情不好还要扣他们的全勤绩效。


压根就不怕这些底层打工仔闹事,纵使有一两个所谓的决心者辞职,也能在很快时间找到下一位。


两者形成互补,共同铸就了这样恶劣的职场生态环境。但我说职场无法改变,远不止这么一点原因。


在这个项目中,我说好听一些只能算是项目负责人,在此之上还有着项目股东,这还要细分成大股东和小股东。而我所在的项目属于互联网赛道,也就是说需要一些新鲜事物的眼光和思维来对待。


但这些股东们经常提出一些奇怪的意见,就如同用微商时代的卖货思维,来指点直播带货,并且他们是出钱的股东,他们提出的战略方针不容我驳回,因为在他们的光辉历史中,有大量的成功案例,来佐证他们的思路是对的。


我刚开始觉得也有道理。他们能有钱投资,肯定是有什么过人的本领能让他们赚到钱,但是随着相处下来,我发现不过是他们本身家里条件就优越,在九几年就能拿出一百万给他们创业。


他们把这一百万分散到二十个领域,每个投资五万总能撞上那么一两个风口,让他们实现钱生钱。


九几年的五万也算是一笔不少的投资。他们这样的发财经历,让我很难不产生质疑,这不是给我我也行吗?


毕竟他们如果真的有什么过人的本领和远见,也不至于在每次内部开会之前,都要组织喊这样的口号:“好,很好,非常好,越来越好“


甚至试图把这样的口号,带到每一次迎接客户的项目介绍会上。我以自曝式要挟制止他们这个行为,我说如果你们这么干,那我当天就辞职,内部都是自己人,我可以陪你们这样弄,但如果对外这么搞,被录下来说我们是传销,我都不知道怎么辩解。


4、职场中的背锅人


他们就是这样坚信着自己能成功,是因为自己有过人的才华。所以自我洗脑着自己提出的方向没有错。如果出错了,亏损了,那一定是负责人的问题。


但好巧不巧,我就是那个负责人。我已经无数次告诉他们,我们这个项目压根就不需要穿黑丝短裙跳舞的小姐姐。


我甚至写了一篇报告给他们,分析我们的项目为什么不能用擦边这种手段引流。但他们执意要,说这样来流量快,我都有点分不清到底是他们自己想看,还是深信这样做确实是可行。


但如果最后这样还是没成功,导致项目亏损,大概率还是在我身上找原因吧。


面对他们这样的大佬,我心里很清楚,这已经远远不是宫斗了,这也绝对不是靠几个心计,或者有实力撑腰就能取胜上位了。这场权力的游戏,不是我等草民玩得起的。


5、换个思路,创造属于自己的职场


一边是被提供资金,但是瞎指挥的股东们摧残,一边是在有限的预算下,我作为负责人,确实很难做到尊重打工人的内心挣扎,回到苏州我虽然能身居高位,但我终将成为我曾经最鄙视的人。


我不要当这个背锅侠,我也不想在这个环境中,去逐渐接受这样的价值观。


这样看来确实如此,00后整顿职场不过是一场互联网的狂欢罢了。


这个题材的故事,也永远只能发生在职场的最底层。由一群家境优越,体验生活的公子哥和我这种不知好歹的普通人共同出演。


大部分人只是在手机屏幕前把我们当个乐子,成了扣个666,然后一起吃胜利的果实。没成,那就确实是看了个乐子。


或许是因为他们心里也清楚,凭我们压根就做不到。


00后现在确实整顿不了职场,因为社会的资源和命脉还不掌握在00后手上。


但就止步于此了吗?我曾说过我想有一个自己的小工作室,遵守劳动法,双休,按时发工资,交纳五险一金。


是的,换个思路,也许00后不需要整顿职场,而是直接创造属于自己的职场,那么接下来我就要向着这个目标去努力了,毕竟二十年后我也还是00后,不如到时候再来说00后整顿职场吧。


作者:程序员Winn
来源:juejin.cn/post/7311603432929984552
收起阅读 »

RecyclerView还能这样滚动对齐?

前言 RecyclerView要想滚动到指定position,一般有scrollToPosition()和smoothScrollToPosition()两种方式。滚动到指定position后,通常还会要求itemView对齐RecyclerView起始点、中...
继续阅读 »

前言


RecyclerView要想滚动到指定position,一般有scrollToPosition()smoothScrollToPosition()两种方式。滚动到指定position后,通常还会要求itemView对齐RecyclerView起始点、中心点或结束点


熟悉RecyclerView的人应该知道,使用自定义SmoothScroller可以实现平滑滚动到指定position的同时,让itemView和RecyclerView的对齐;而scrollToPosition()方法只能滚动到指定position。那有办法让scrollToPosition()也做到对齐吗?


拆解行为


分析对齐的行为后,可以分为几步



  1. 让目标itemView可见

  2. 计算itemView和目的位置的偏移量

  3. 将itemView移动到目的位置


第一步scrollToPosition()就已经可以实现了,最后一步就是调用scrollBy(),那其实只需要实现第二步计算偏移量,而这可以参考SmoothScroller的实现


平滑滚动


来看下SmoothScroller是怎么做的。通常做法都是自定义LinearSmoothScroller


RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
int preference = LinearSmoothScroller.SNAP_TO_START;// 对齐方式
LinearSmoothScroller smoothScroller = new LinearSmoothScroller(context){
@Override
protected int getHorizontalSnapPreference() {
return preference;
}

@Override
protected int getVerticalSnapPreference() {
return preference;
}
};
smoothScroller.setTargetPosition(targetPosition);
layoutManager.startSmoothScroll(smoothScroller);

简单介绍下几种对齐方式



  • SNAP_TO_START:对齐RecyclerView起始位置

  • SNAP_TO_END:对齐RecyclerView结束位置

  • SNAP_TO_ANY:对齐RecyclerView任意位置,确保itemView在RecyclerView内


接下来看下getVerticalSnapPreference()或者getHorizontalSnapPreference()的返回值是怎么影响到itemView的对齐的。查看LinearSmoothScroller源码发现这两个方法会在onTargetFound()里调用


protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
final int dx = calculateDxToMakeVisible(targetView, getHorizontalSnapPreference());
final int dy = calculateDyToMakeVisible(targetView, getVerticalSnapPreference());
final int distance = (int) Math.sqrt(dx * dx + dy * dy);
final int time = calculateTimeForDeceleration(distance);
if (time > 0) {
action.update(-dx, -dy, time, mDecelerateInterpolator);
}
}

不难看出,该方法是计算targetView当前要滚动的偏移量和时长,并设置给action。而calculateDxToMakeVisible()calculateDyToMakeVisible()正是我们要找的计算偏移量的方法


由于这两个方法只依赖LayoutManager,所以我们可以将这些代码逻辑复制出来,创建一个Rangefinder类,用于计算偏移量


public class Rangefinder {
private final RecyclerView.LayoutManager mLayoutManager;

public Rangefinder(RecyclerView.LayoutManager layoutManager) {
mLayoutManager = layoutManager;
}

@Nullable
public RecyclerView.LayoutManager getLayoutManager() {
return mLayoutManager;
}

// 计算view在RecyclerView中完全可见所需的垂直偏移量
public int calculateDyToMakeVisible(View view, int snapPreference) {
final RecyclerView.LayoutManager layoutManager = getLayoutManager();
if (layoutManager == null || !layoutManager.canScrollVertically()) {
return 0;
}
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
view.getLayoutParams();
final int top = layoutManager.getDecoratedTop(view) - params.topMargin;
final int bottom = layoutManager.getDecoratedBottom(view) + params.bottomMargin;
final int start = layoutManager.getPaddingTop();
final int end = layoutManager.getHeight() - layoutManager.getPaddingBottom();
return calculateDtToFit(top, bottom, start, end, snapPreference);
}

// 计算view在RecyclerView中完全可见所需的水平偏移量
public int calculateDxToMakeVisible(View view, int snapPreference) {
final RecyclerView.LayoutManager layoutManager = getLayoutManager();
if (layoutManager == null || !layoutManager.canScrollHorizontally()) {
return 0;
}
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
view.getLayoutParams();
final int left = layoutManager.getDecoratedLeft(view) - params.leftMargin;
final int right = layoutManager.getDecoratedRight(view) + params.rightMargin;
final int start = layoutManager.getPaddingLeft();
final int end = layoutManager.getWidth() - layoutManager.getPaddingRight();
return calculateDtToFit(left, right, start, end, snapPreference);
}

public int calculateDtToFit(int viewStart, int viewEnd, int boxStart, int boxEnd,
@SnapPreference int snapPreference)
{
switch (snapPreference) {
case LinearSmoothScroller.SNAP_TO_START:
return boxStart - viewStart;
case LinearSmoothScroller.SNAP_TO_END:
return boxEnd - viewEnd;
case LinearSmoothScroller.SNAP_TO_ANY:
final int dtStart = boxStart - viewStart;
if (dtStart > 0) {
return dtStart;
}
final int dtEnd = boxEnd - viewEnd;
if (dtEnd < 0) {
return dtEnd;
}
break;
}
return 0;
}
}

有了计算偏移量的方法,接下来就是实现itemView的对齐了


即时滚动


根据上面的拆解步骤,再分析下每一步要做的事情



  1. 调用scrollToPosition()使目标itemView可见。因为该方法最终会requestLayout(),所以要在layout后,才能通过获取到itemView。那么可以post()后调用LayoutManagerfindViewByPosition()方法获取itemView

  2. 参考LinearSmoothScrolleronTargetFound()方法,使用上面的Rangefinder计算itemView和目的位置的偏移量

  3. 调用scrollBy()将itemView移动到目的位置


RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
recyclerView.scrollToPosition(targetPosition);
recyclerView.post(new Runnable() {
@Override
public void run() {
View targetView = layoutManager.findViewByPosition(targetPosition);
if (targetView != null) {
Rangefinder rangefinder = new Rangefinder(layoutManager);
final int dx = rangefinder.calculateDxToMakeVisible(targetView, preference);
final int dy = rangefinder.calculateDyToMakeVisible(targetView, preference);
if (dx != 0 || dy != 0) {
recyclerView.scrollBy(-dx, -dy);
}
}
}
});

至此,我们就实现了即时滚动到position的同时,让itemView和RecyclerView对齐的功能。当然,这也只是测试代码,实际使用还会对上面的逻辑进行封装


测试代码 recyclerView-scroll-demo


参考


作者:benio
来源:juejin.cn/post/7364740313284444186
收起阅读 »

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

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

动画.gif


实现过程


我们先创建下 vue 项目


npm init vite-app vue3-vite-animation

进入文件夹中


cd vue3-vite-animation

安装下依赖


npm install

启动


npm run dev

image-20240503171537954.png


重新修改 App.vue


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

</template>

<script setup lang="ts">

</script>



<style>

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

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


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


image-20240503175456039.png


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


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

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


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


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

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

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


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


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


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

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

image-20240503180914393.png


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


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


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

image-20240503181138545.png


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


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


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


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

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


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


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


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

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

动画-1714752074132-6.gif


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


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


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

动画-1714752309164-8.gif


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


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


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


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

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

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

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

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

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


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


修改下样式、与模板


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

</template>

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

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


动画-1714753474905-10.gif


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


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


image-20240504002759638.png


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

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

动画-1714754131456-15.gif


如果是右上角呢


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

动画-1714754468881-23.gif


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


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


y 也是同理


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

动画-1714754788538-25.gif


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


完整代码


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

</template>

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

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

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

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

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


<style>

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

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

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

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

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

换肤动画源码


小结


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


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


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


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

从密码到无密码:账号安全进化史(科普向)

本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究! 本文是一篇科普文,五一结束了,大家看点轻松的~ 不知道大家在过去半年有没有发现 Github 强制开启了 2FA,而且还不可以关闭的,每次你打开 github 都会提...
继续阅读 »

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




本文是一篇科普文,五一结束了,大家看点轻松的~


不知道大家在过去半年有没有发现 Github 强制开启了 2FA,而且还不可以关闭的,每次你打开 github 都会提醒你的验证:


Image.png


简单的说,就是打开 Github 进行验证时,只依靠密码验证已经不被允许,你必须打开你手机上的验证软件,把里面随机码输入到 Github 才能完成身份验证,类似于十年前国内 QQ 安全中心的验证。


这是一种双重验证的手段,用于更好的保证我们的账号安全,今天就以此为引,给大家讲讲账号安全相关发展的历史。


第一幕:密码的独角戏 - 脆弱的防线


在互联网的蛮荒时代,密码就像原始人手中的木棍和石头,是守护账号安全的唯一屏障。然而,这道防线却是脆弱不堪的,面对黑客的攻击,如同纸糊的老虎,一戳就破。暴-力-破-解、字-典-攻-击、社会-工程-学手段,都足以让密码这道防线形同虚设。


1. 暴-力-破-解:暴-力-破-解就是通过遍历的方式尝试出你的密码组合,比如银行的六位取款密码实际上只有 46656种组合,利用现在任何一台电脑或者手机的算力都能瞬间算出来,为了对应这种情况,现在几乎所有网站都有密码输入次数限制。


2. 字-典-攻-击:字典-攻击就是利用常用密码来攻破你的密码,比暴力破解效率更好,比如 123456 这个密码就有很多人使用。


3. 社会-工程-学:社会-工程-学说人话就是套你的话,或者调查你的信息,比如在和你沟通的过程中知道了你的手机号、身-份-证号码、生日信息等,因为有大量的人用手机号后六位、身-份-证号后六位或者生日当作自己的密码,所以这种手段的成功率一般会更高。


在当前这个时代,由于互联网各种 App 的涌入,每个人都拥有大量的账号,如何记忆他们成为了一个难题,大量的人选择对所有网站使用同一个密码,这就又造成了账号安全问题。


重复使用密码就像是使用同一把钥匙开启不同的门,一旦一把钥匙被复制,所有的门都将面临危险。


每年,全球都会发生无数起数据泄露事件,大量的用户名和密码被公开曝光。这些泄露的密码成为了黑客攻击的利器,他们可以利用这些密码进行撞库攻击,尝试登录其他网站。


会在不同的网站使用相同的密码将会导致“一损俱损”的局面。一旦一个网站发生数据泄露,黑客便可以利用泄露的密码尝试登录其他网站,从而获取更多个人信息,造成更大的损失。


第二幕:多因素认证 (MFA) 登场 - 多重关卡,层层设防


所以,为了弥补密码的不足,MFA 应运而生,为账号安全加装了多重门锁。除了密码这把“钥匙”,你还需要其他的“通关密语”才能进入:



  • 验证码: 这是国内最常用的方式,甚至几乎所有 App 都已经不需要你记忆账号密码,只需要一个手机验证码即可,国外使用手机验证码的很少,因为他们更多使用邮箱来注册账号,比如我现在在使用的编辑软件Craft 在登录时就要求你提供邮箱验证码,它甚至不能设置密码。

  • 指纹识别: 你的指纹独一无二,所以它就像是你的专属的“魔法印记”,轻轻一按,就能保证你是你。

  • 面部识别: 对着摄像头眨眨眼,你的面容信息也是你的专属印记,苹果手机上甚至使用了虹膜识别来检测你是你,而不是别人。

  • 安全令牌: 一个小巧的硬件设备,可以生成一次性密码,就像古代的“虎符”一样,只有拥有它才能调兵遣将。令牌可以有软件和硬件两种方式,软件就是 Google Auth 这种软件,而硬件则是我们早古互联网时代网上购物常用的网银 U 盾形式。


在开头的时候,我曾提到了 2FA,它和本节的 MFA 听名字非常相似,实际上说的也几乎是一个东西。


2FA 是指:需要两种验证,才能完成整个验证,一般是密码和动态安全令牌。


MFA 是指:需要两种或以上验证方式,才能完成整个验证,一般也是密码和动态安全令牌。


所以在大多数语境下,这俩说的其实是一个东西,有些验证方式将两个验证方式合而为一,比如手机/邮箱验证码。


因为多因子验证的核心是:一个你知道的凭证 和 一个你刚刚才知道的凭证。


我们一开始就知道的凭证往往是邮箱 + 密码,一个刚刚才会知道的凭证往往就是动态安全令牌码了,所以手机验证码登录的方式也是 2FA,还是属于比较方便的那种。


注:我这里说的手机验证码登录是真的发给你验证码,而不是国内的那种手机号一键登录。


第三幕:单点登录 (SSO) 崛起 - 统一管理的钥匙


其实随着 MFA 的出现,安全问题已经不需要太担心了,所以接下来账号安全开始朝着:安全 + 高效的方向开始发展,所以开始出现了 SSO。


SSO 的第一个阶段是内部互信,它的概念最早可以追溯到 1990 年代,随着企业内部网络的发展而兴起。


后来随着互联网的发展,一个公司往往同时拥有多个业务,比如十年前还是百度的天下的时候,我们会同时使用百度知道、百度贴吧、百度网盘这些产品。


你只需要在某一个百度旗下的产品登录一次,打开其他产品的时候往往也会自动识别到你的账号。


比如你在百度贴吧登录了,此时你打开网页版的百度网盘你自动就是已登录状态。


不要以为这是一个 So easy 的操作,它的原理其实是使用你存储在同一个主域名下的 cookie 实现的。


比如百度贴吧的域名是:tieba.baidu.com/,而百度网盘的域名是:pan.baidu.com/,它俩都属于主域名 baidu.com,所以通过携带同域名下cookie 的方式,让同域名下的其他服务也能正确识别当前账号。


具体识别方案一般有两种:



  1. 通过共享 session + cookie 的方式做验证。

  2. 通过获取 cookie 内部跳转到 SSO 做验证。


无论使用哪种方案,携带 cookie 这个操作必不可少,所以这一阶段的 SSO 是基于 Cookie 的。


可能还有一个词大家比较常见:SAML,SAML标准也是用于内部系统互信,做的事和基于 Cookie 的 SSO 都是一样的,所以这里我不再赘述。


第四幕:OAuth 协议的诞生 - 授权管理的桥梁


经历完 SSO 的第一个阶段之后,我们就来到了 SSO 的第二阶段:外部互信


由于 Web 互联网的兴起,这一阶段也被称为基于 Web 的 SSO,这一阶段的代表是OAuth。


你有没有想过,如果我们在所有平台都使用同一个账号多好,就不用在记忆那么多的应用账号密码,减少心智负担。


在国内互联圈地的情况下,这种情况并没有实现,也可以说通过手机号实现了。


但是在国外,Google 账号体系几乎就是事实上的一号通行,你注册一个 Google 账号之后,几乎可以通过这个账号登录所有的网站,这就是 OAtuh 的作用。


想象一下,你拥有许多宝藏,分别存放于不同的宝库中。比如,你在 Facebook 上存储着你的社交关系,在 Google 上存储着你的邮件和文件,在 Spotify 上存储着你的音乐喜好。


现在,你想要使用一个新的游戏应用,而这个应用需要访问你在 Facebook 上的好友列表,以便你能够邀请好友一起玩游戏。


这时,你面临一个两难的选择:



  • 分享密码: 将你的 Facebook 密码告诉游戏应用,让它直接访问你的好友列表。但这存在着巨大的安全风险,一旦游戏应用泄露你的密码,你的所有 Facebook 数据都将暴露无遗。

  • 放弃使用: 由于担心安全问题,你放弃使用这个游戏应用,从而错过了与好友一起游戏的乐趣。


为了解决上面这种问题,Google 等公司在 2010 年发布了 OAuth1.0,由于它存在许多问题,所以又在 2012 年发布了 OAuth2.0。


所以在现如今,几乎所有公司都接入了 Google 的 OAuth 登录,当你在第三方平台想使用 Google 账号登录时,OAuth 协议会引导你到 Google 进行授权。


平台会询问你是否同意授权第三方应用访问你的部分数据 (例如好友列表),如果你同意,平台就会发放一个临时的“通行证”给第三方应用,让它可以访问你的数据,但不会泄露你的密码。


所以 OAuth 的核心是授权而非共享。


第五幕:无密码时代的曙光 - 告别繁琐的密码


我相信当大家看到第四节的时候,大家就会觉得应该就这些了,没有别的新意了,恰恰相反,为了彻底摆脱密码的束缚,世界巨头们正在探索新的“魔法”,那就是无密码


在 2019 年,WebAuthn 标准被 W3C 以建议的形式发布,它是 FIDO 联盟下 FIDO2 的核心组件,旨在减少人们对于密码的依赖。


它带了以下三个好处:



  • 消除密码依赖: 通过使用更加安全的认证方式,例如生物识别技术 (指纹、面部识别) 或安全密钥,消除用户对密码的依赖,降低密码泄露和网络钓鱼攻击的风险。

  • 提升用户体验: 简化登录流程,无需记忆和输入复杂的密码,只需轻触指纹或插入安全密钥,即可完成身份验证。

  • 增强安全性: 使用公钥加密技术,确保用户的认证信息不会被窃取或伪造,有效抵御网络攻击。


如果大家有在 Mac 上的 Safari 浏览器登录苹果账号的经历,就会发现它不需要你输入密码,只需要一次简单的指纹验证:


Image.png


这时你通过验证你的指纹就可以顺利登录成功,这就是基于 WebAuthn 标准的 Passkeys。


目前苹果、谷歌、微软等几乎所有大厂都支持了Passkeys,,由于它也是一个 W3C 标准,所以你可以通过这个网站查看支持列表。


看起来指纹验证就像开头我们说过的 MFA,但是它比 MFA 多了一个东西就是设备,通过生物信息 + 受信设备的方式完成了它的整个认证流程,它拥有两个比较大的特点:



  • 提供了一套标准化的用户界面和用户体验,简化了无密码登录的操作流程。

  • 将用户的登录凭证 (私钥) 存储在用户的设备 (例如手机、电脑) 中,并通过云端服务进行同步,方便用户在不同设备上登录。


说回我们开头的 Github 的 2FA,其实 Github 也接入了它,如果你完成 2FA 之后,之后就可以在浏览器中通过指纹验证登录。


身份认证的未来已来,无密码的出现,为我们在登录授权流程中带来了许多方便~




好了,以上就是本篇文章的全部内容了,希望大家多多点赞支持,我将更快提供更好更优质的内容。


注:本文小标题是借助 AI 能力起的,部分描述也借助了 AI 美化,AI 美化生成内容不会超过 300 字(本文 4000 字),请大家放心食用。


作者:和耳朵
来源:juejin.cn/post/7364764922339065890
收起阅读 »