注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

环信FAQ

环信FAQ

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

双token和无感刷新token(简单写法,一文说明白,不墨迹)

为什么有这篇小作文?最近要给自己的项目加上token自动续期,但是在网上搜的写法五花八门,有的光前端部分就写了几百行代码,我看着费劲,摸了半天也没有实现,所以决定自己造轮子项目构成后端部分:使用golang的gin框架起的服务前端部分:vue+elementu...
继续阅读 »

为什么有这篇小作文?

最近要给自己的项目加上token自动续期,但是在网上搜的写法五花八门,有的光前端部分就写了几百行代码,我看着费劲,摸了半天也没有实现,所以决定自己造轮子

项目构成

  • 后端部分:使用golang的gin框架起的服务
  • 前端部分:vue+elementui

先说后端部分,后端逻辑相对前端简单点,关键两步

  1. 登陆接口生成双token
"github.com/dgrijalva/jwt-go"
func (this UserController) DoLogin(ctx *gin.Context) {
username := ctx.Request.FormValue("username")
passWord := ctx.Request.FormValue("password")
passMd5 := middlewares.CreateMD5(passWord)
expireTime := time.Now().Add(10 * time.Second).Unix() //token过期时间10秒,主要是测试方便
refreshTime := time.Now().Add(20 * time.Second).Unix() //刷新的时间限制,超过20秒重新登录
user := modules.User{}
err := modules.DB.Model(&modules.User{}).Where("username = ? AND password = ?", username, passMd5).Find(&user).Error
if err != nil {
ctx.JSON(400, gin.H{
"success": false,
"message": "用户名或密码错误",
})
} else {
println("expireTime", string(rune(expireTime)))
myClaims := MyClaims{
user.Id,
jwt.StandardClaims{
ExpiresAt: expireTime,
},
}
myClaimsRefrrsh := MyClaims{
user.Id,
jwt.StandardClaims{
ExpiresAt: refreshTime,
},
}
jwtKey := []byte("lyf123456")
tokenObj := jwt.NewWithClaims(jwt.SigningMethodHS256, myClaims)
tokenStr, err := tokenObj.SignedString(jwtKey)
tokenFresh := jwt.NewWithClaims(jwt.SigningMethodHS256, myClaimsRefrrsh)
tokenStrRefresh, err2 := tokenFresh.SignedString(jwtKey)
if err != nil && err2 != nil {
ctx.JSON(200, gin.H{
"message": "生成token失败",
"success": false,
})
} else {
ctx.JSON(200, gin.H{
"message": "登录成功",
"success": true,
"token": tokenStr,//数据请求的token
"refreshToken": tokenStrRefresh,//刷新token用的
})
}
}
}
  1. 刷新token的方法
func (this UserController) RefrshToken(ctx *gin.Context) {
tokenData := ctx.Request.Header.Get("Authorization") //这里是个关键点,刷新token时也要带上token,不过这里是前端传的refreshToken
if tokenData == "" {
ctx.JSON(401, gin.H{
"message": "token为空",
"success": false,
})
ctx.Abort()
return
}
tokenStr := strings.Split(tokenData, " ")[1]
_, claims, err := middlewares.ParseToken(tokenStr)
expireTime := time.Now().Add(10 * time.Second).Unix()
refreshTime := time.Now().Add(20 * time.Second).Unix()
if err != nil {
ctx.JSON(400, gin.H{
"success": false,
"message": "token传入错误",
})
} else {
myClaims := MyClaims{
claims.Uid,
jwt.StandardClaims{
ExpiresAt: expireTime,
},
}
myClaimsRefrrsh := MyClaims{
claims.Uid,
jwt.StandardClaims{
ExpiresAt: refreshTime,
},
}
jwtKey := []byte("lyf123456")
tokenObj := jwt.NewWithClaims(jwt.SigningMethodHS256, myClaims)
tokenStr, err := tokenObj.SignedString(jwtKey)
tokenFresh := jwt.NewWithClaims(jwt.SigningMethodHS256, myClaimsRefrrsh)
tokenStrRefresh, err2 := tokenFresh.SignedString(jwtKey)
if err != nil && err2 != nil {
ctx.JSON(400, gin.H{
"message": "生成token失败",
"success": false,
})
} else {
ctx.JSON(200, gin.H{
"message": "刷新token成功",
"success": true,
"token": tokenStr,
"refreshToken": tokenStrRefresh,
})
}
}
}
  1. 路由中间件里验证token
package middlewares

import (
"strings"

"github.com/dgrijalva/jwt-go"
"github.com/gin-gonic/gin"
)

type MyClaims struct {
Uid int
jwt.StandardClaims
}

func AuthMiddleWare(c *gin.Context) {
tokenData := c.Request.Header.Get("Authorization")
if tokenData == "" {
c.JSON(401, gin.H{
"message": "token为空",
"success": false,
})
c.Abort()
return
}
tokenStr := strings.Split(tokenData, " ")[1]
token, _, err := ParseToken(tokenStr)
if err != nil || !token.Valid {
// 这里我感觉觉是个关键点,我看别人写的,过期了返回401,但是前端的axios的响应拦截器里捕获不到,所以我用201状态码,
c.JSON(201, gin.H{
"message": "token已过期",
"success": false,
})
c.Abort()
return
} else {
c.Next()
}
}

func ParseToken(tokenStr string) (*jwt.Token, *MyClaims, error) {
jwtKey := []byte("lyf123456")
// 解析token
myClaims := &MyClaims{}
token, err := jwt.ParseWithClaims(tokenStr, myClaims, func(token *jwt.Token) (interface{}, error) {
return jwtKey, nil
})
return token, myClaims, err
}

总结一下:后端部分三步,1.登陆时生成双token,2,路由中间件里验证token,过期时返回201状态码(201是我私人定的,并不是行业标准)。3,刷新token的方法里也和登陆接口一样返回双token

前端部分

前端部分在axios封装时候加拦截器判断token是否过期,我这里跟别人写的最大的不同点是:我创建了两个axios对象,一个正常数据请求用(server),另一个专门刷新token用(serverRefreshToken),这样写的好处是省去了易错的判断逻辑

import axios from 'axios'
import { ElMessage } from 'element-plus'
import router from '../router'
//数据请求用
const server=axios.create({
baseURL:'/shopApi',
timeout:5000
})
// 刷新token专用
const serverRefreshToken=axios.create({
baseURL:'/shopApi',
timeout:5000
})
//获取新token的方法
async function getNewToken(){
let res=await serverRefreshToken.request({
url:`/admin/refresh`,
method:"post",
})
if(res.status==200){
sessionStorage.setItem("token",res.data.token)
sessionStorage.setItem("refreshToken",res.data.refreshToken)
return true
}else{
ElMessage.error(res.data.message)
router.push('/login')
return false
}
}
//这里是正常获取数据用的请求拦截器,主要作用是给所有请求的请求头里加上token
server.interceptors.request.use(config=>{
let token=""
token=sessionStorage.getItem("token")
if(token){
config.headers.Authorization="Bearer "+token
}
return config
},error=>{
Promise.reject(error)
})
//这里是正常获取数据用的响应拦截器,正常数据请求都是200状态码,当拦截到201状态码时,代表token过期了,
server.interceptors.response.use(async(res)=>{
if(res.status==201){
//获取新token
let bl=await getNewToken()
if(bl){
//获取成功新token之后,把刚才token过期拦截到的请求重新发一遍,获取到数据之后把res覆盖掉
//这里是个关键点,下边这行代码里的第二个res是token过期后被拦截的那个请求,config里是该请求的详细信息,重新请求后返回的是第一个res,把失败的res覆盖掉,这里有点绕,文字不好表达,
res=await server.request(res.config)
}
}
return res
},error=>{
if(error.response.status==500||error.response.status==401||error.response.status==400){
router.push('/login')
ElMessage.error(error.response.data.message)
Promise.reject(error)
}

})
//这里是刷新token专用的axios对象,他的作用是给请求加上刷新token专用的refreshToken
serverRefreshToken.interceptors.request.use(config=>{
let token=""
token=sessionStorage.getItem("refreshToken")
if(token){
config.headers.Authorization="Bearer "+token
}
return config
},error=>{
Promise.reject(error)
})
export default server

总结一下,前端部分:1,正常数据请求和刷新token用的请求分开了,各司其职。省去复杂的判断。2,获取新的token和refreshToken后更新原来旧的token和refreshToken。(完结)


作者:锋行天下
来源:juejin.cn/post/7337876697427148811

为什么有这篇小作文?

最近要给自己的项目加上token自动续期,但是在网上搜的写法五花八门,有的光前端部分就写了几百行代码,我看着费劲,摸了半天也没有实现,所以决定自己造轮子

项目构成

  • 后端部分:使用golang的gin框架起的服务
  • 前端部分:vue+elementui

先说后端部分,后端逻辑相对前端简单点,关键两步

  1. 登陆接口生成双token
"github.com/dgrijalva/jwt-go"
func (this UserController) DoLogin(ctx *gin.Context) {
username := ctx.Request.FormValue("username")
passWord := ctx.Request.FormValue("password")
passMd5 := middlewares.CreateMD5(passWord)
expireTime := time.Now().Add(10 * time.Second).Unix() //token过期时间10秒,主要是测试方便
refreshTime := time.Now().Add(20 * time.Second).Unix() //刷新的时间限制,超过20秒重新登录
user := modules.User{}
err := modules.DB.Model(&modules.User{}).Where("username = ? AND password = ?", username, passMd5).Find(&user).Error
if err != nil {
ctx.JSON(400, gin.H{
"success": false,
"message": "用户名或密码错误",
})
} else {
println("expireTime", string(rune(expireTime)))
myClaims := MyClaims{
user.Id,
jwt.StandardClaims{
ExpiresAt: expireTime,
},
}
myClaimsRefrrsh := MyClaims{
user.Id,
jwt.StandardClaims{
ExpiresAt: refreshTime,
},
}
jwtKey := []byte("lyf123456")
tokenObj := jwt.NewWithClaims(jwt.SigningMethodHS256, myClaims)
tokenStr, err := tokenObj.SignedString(jwtKey)
tokenFresh := jwt.NewWithClaims(jwt.SigningMethodHS256, myClaimsRefrrsh)
tokenStrRefresh, err2 := tokenFresh.SignedString(jwtKey)
if err != nil && err2 != nil {
ctx.JSON(200, gin.H{
"message": "生成token失败",
"success": false,
})
} else {
ctx.JSON(200, gin.H{
"message": "登录成功",
"success": true,
"token": tokenStr,//数据请求的token
"refreshToken": tokenStrRefresh,//刷新token用的
})
}
}
}
  1. 刷新token的方法
func (this UserController) RefrshToken(ctx *gin.Context) {
tokenData := ctx.Request.Header.Get("Authorization") //这里是个关键点,刷新token时也要带上token,不过这里是前端传的refreshToken
if tokenData == "" {
ctx.JSON(401, gin.H{
"message": "token为空",
"success": false,
})
ctx.Abort()
return
}
tokenStr := strings.Split(tokenData, " ")[1]
_, claims, err := middlewares.ParseToken(tokenStr)
expireTime := time.Now().Add(10 * time.Second).Unix()
refreshTime := time.Now().Add(20 * time.Second).Unix()
if err != nil {
ctx.JSON(400, gin.H{
"success": false,
"message": "token传入错误",
})
} else {
myClaims := MyClaims{
claims.Uid,
jwt.StandardClaims{
ExpiresAt: expireTime,
},
}
myClaimsRefrrsh := MyClaims{
claims.Uid,
jwt.StandardClaims{
ExpiresAt: refreshTime,
},
}
jwtKey := []byte("lyf123456")
tokenObj := jwt.NewWithClaims(jwt.SigningMethodHS256, myClaims)
tokenStr, err := tokenObj.SignedString(jwtKey)
tokenFresh := jwt.NewWithClaims(jwt.SigningMethodHS256, myClaimsRefrrsh)
tokenStrRefresh, err2 := tokenFresh.SignedString(jwtKey)
if err != nil && err2 != nil {
ctx.JSON(400, gin.H{
"message": "生成token失败",
"success": false,
})
} else {
ctx.JSON(200, gin.H{
"message": "刷新token成功",
"success": true,
"token": tokenStr,
"refreshToken": tokenStrRefresh,
})
}
}
}
  1. 路由中间件里验证token
package middlewares

import (
"strings"

"github.com/dgrijalva/jwt-go"
"github.com/gin-gonic/gin"
)

type MyClaims struct {
Uid int
jwt.StandardClaims
}

func AuthMiddleWare(c *gin.Context) {
tokenData := c.Request.Header.Get("Authorization")
if tokenData == "" {
c.JSON(401, gin.H{
"message": "token为空",
"success": false,
})
c.Abort()
return
}
tokenStr := strings.Split(tokenData, " ")[1]
token, _, err := ParseToken(tokenStr)
if err != nil || !token.Valid {
// 这里我感觉觉是个关键点,我看别人写的,过期了返回401,但是前端的axios的响应拦截器里捕获不到,所以我用201状态码,
c.JSON(201, gin.H{
"message": "token已过期",
"success": false,
})
c.Abort()
return
} else {
c.Next()
}
}

func ParseToken(tokenStr string) (*jwt.Token, *MyClaims, error) {
jwtKey := []byte("lyf123456")
// 解析token
myClaims := &MyClaims{}
token, err := jwt.ParseWithClaims(tokenStr, myClaims, func(token *jwt.Token) (interface{}, error) {
return jwtKey, nil
})
return token, myClaims, err
}

总结一下:后端部分三步,1.登陆时生成双token,2,路由中间件里验证token,过期时返回201状态码(201是我私人定的,并不是行业标准)。3,刷新token的方法里也和登陆接口一样返回双token

前端部分

前端部分在axios封装时候加拦截器判断token是否过期,我这里跟别人写的最大的不同点是:我创建了两个axios对象,一个正常数据请求用(server),另一个专门刷新token用(serverRefreshToken),这样写的好处是省去了易错的判断逻辑

import axios from 'axios'
import { ElMessage } from 'element-plus'
import router from '../router'
//数据请求用
const server=axios.create({
baseURL:'/shopApi',
timeout:5000
})
// 刷新token专用
const serverRefreshToken=axios.create({
baseURL:'/shopApi',
timeout:5000
})
//获取新token的方法
async function getNewToken(){
let res=await serverRefreshToken.request({
url:`/admin/refresh`,
method:"post",
})
if(res.status==200){
sessionStorage.setItem("token",res.data.token)
sessionStorage.setItem("refreshToken",res.data.refreshToken)
return true
}else{
ElMessage.error(res.data.message)
router.push('/login')
return false
}
}
//这里是正常获取数据用的请求拦截器,主要作用是给所有请求的请求头里加上token
server.interceptors.request.use(config=>{
let token=""
token=sessionStorage.getItem("token")
if(token){
config.headers.Authorization="Bearer "+token
}
return config
},error=>{
Promise.reject(error)
})
//这里是正常获取数据用的响应拦截器,正常数据请求都是200状态码,当拦截到201状态码时,代表token过期了,
server.interceptors.response.use(async(res)=>{
if(res.status==201){
//获取新token
let bl=await getNewToken()
if(bl){
//获取成功新token之后,把刚才token过期拦截到的请求重新发一遍,获取到数据之后把res覆盖掉
//这里是个关键点,下边这行代码里的第二个res是token过期后被拦截的那个请求,config里是该请求的详细信息,重新请求后返回的是第一个res,把失败的res覆盖掉,这里有点绕,文字不好表达,
res=await server.request(res.config)
}
}
return res
},error=>{
if(error.response.status==500||error.response.status==401||error.response.status==400){
router.push('/login')
ElMessage.error(error.response.data.message)
Promise.reject(error)
}

})
//这里是刷新token专用的axios对象,他的作用是给请求加上刷新token专用的refreshToken
serverRefreshToken.interceptors.request.use(config=>{
let token=""
token=sessionStorage.getItem("refreshToken")
if(token){
config.headers.Authorization="Bearer "+token
}
return config
},error=>{
Promise.reject(error)
})
export default server

总结一下,前端部分:1,正常数据请求和刷新token用的请求分开了,各司其职。省去复杂的判断。2,获取新的token和refreshToken后更新原来旧的token和refreshToken。(完结)


作者:锋行天下
来源:juejin.cn/post/7337876697427148811
收起阅读 »

掌握Redis核心:常用数据类型的高效运用秘籍!

在数据驱动的时代,高效地存储和处理数据成为了开发者们的重要任务。Redis,作为一个开源的高性能键值对(key-value)数据库,以其独特的数据结构和丰富的功能,成为了众多项目的首选。今天,我们就来揭开Redis的神秘面纱,看看它是如何通过不同的数据类型,为...
继续阅读 »

在数据驱动的时代,高效地存储和处理数据成为了开发者们的重要任务。Redis,作为一个开源的高性能键值对(key-value)数据库,以其独特的数据结构和丰富的功能,成为了众多项目的首选。

今天,我们就来揭开Redis的神秘面纱,看看它是如何通过不同的数据类型,为我们提供高效、灵活的数据存储和处理能力的。

一、字符串(String):数据的基石

String类型简介

字符串是Redis最基本的数据类型,它可以存储文本、数字或者二进制数据。

使用字符串类型,你可以执行原子性的操作,如追加(APPEND)、设置(SET)和获取(GET)。例如,你可以将用户信息作为字符串存储,并通过键快速检索。

  • 一个key对应一个value。

  • String类型是二进制安全的。只要内容可以使用字符串表示就可以存储到string中。比如jpg图片或者序列化的对象。

  • 一个Redis中字符串value最多可以是512M。

常用命令

set key value: 添加键值对。

Description

get key: 查询key对应的键值。

Description

注意:如果设置了两次相同的key,后设置的就会把之前的key覆盖掉。

append key value: 将给定的value追加到原值的末尾。

Description

strlen key: 获得值的长度。

Description

setnx key value: 只有在key 不存在时 ,才能设置 key 的值。

Description

incr key: 将 key 中储存的数字值增1(只能对数字值操作,如果为空,新增值为1)。

Description

decr key: 将 key 中储存的数字值减1(只能对数字值操作,如果为空,新增值为-1)。

Description

incrby / decrby key 步长: 通过自定义步长方式增减 key 中储存的数字值。

Description

mset key1 value1 key2 value2 …: 同时设置一个或多个键值对。

Description

mget key1 key2 key3 …: 同时获取一个或多个value。

Description

msetnx key1 value1 key2 value2 …: 所有给定 key 都不存在时,同时设置一个或多个 key-value 对。

注意:此操作有原子性,只要有一个不符合条件的key。其他的也都不能设置成功。如下图:
Description

getrange key 起始位置、结束位置: 获得值的范围,类似java中的substring。

Description

setrange key 起始位置 value: 用 value覆写key所储存的字符串值,从起始位置开始(索引从0开始)。

Description

setex key 过期时间 value: 可以在设置键值的同时,设置过期时间,单位秒(前面的expire是给已有的键值设置过期时间,注意区别)。

Description

getset key value: 以新换旧,设置了新值同时获得旧值。

Description

应用场景

存储用户信息: 将用户的姓名、年龄等信息作为字符串存储在Redis中,通过键值对的方式快速检索和更新。

计数器: 使用INCR命令实现访问量、点赞数等计数功能。

二、哈希(Hash):组织数据的框架

Hash类型简介

哈希类型允许你存储字段-值对的集合。这种结构非常适合于存储对象,如用户的个人信息。通过HSET和HGET命令,你可以设置和获取哈希中的字段和值。

哈希类型的优势在于它可以对字段进行原子性操作,而不需要读取整个对象。

常用命令

hset key field value: 给key集合中的 field 键赋值value。
Description

hget key1 field: 从key1集合field取出 value。

Description

hmset key1 field1 value1 field2 value2… : 批量设置hash的值(一次性设置多个数据值)。

Description

hexists key1 field: 查看哈希表 key 中,给定域 field 是否存在。

Description

hkeys key: 列出该hash集合的所有field。

Description

hvals key: 列出该hash集合的所有value。

Description

hincrby key field increment: 为哈希表 key 中的域 field 的值加上增量。

Description

hsetnx key field value: 将哈希表 key 中的域 field 的值设置为 value ,当且仅当域 field 不存在。

Description

应用场景

存储用户对象: 将用户对象的多个属性(如姓名、年龄、性别等)存储在一个哈希结构中,通过HGETALL命令获取整个对象,或使用HGET/HSET针对某个属性进行操作。

存储配置信息: 将应用程序的配置信息以字段-值对的形式存储在哈希中,方便集中管理和修改。

三、列表(List):有序数据的队列

List类型简介

列表类型提供了一种顺序存储数据的方式,它类似于Python中的列表或Java中的LinkedList。

你可以使用LPUSH和RPUSH命令在列表的头部或尾部添加元素。列表还支持范围查询和列表内元素的移除操作,非常适合于实现消息队列等场景。

  • List中单键多值,即一个key对应多个value,其中的多个value值使用List进行存储。

  • Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)。

  • 它的底层实际是个双向链表,对两端的操作性能很高,通过索引下标的操作中间的节点性能会较差。

常用命令

**lpush / rpush key value1 value2 value3 … :**从左边/右边插入一个或多个值(l代表left,r代表right)。

**lrange key start stop:**按照索引下标获得元素(从左到右)。

其中lrange k1 0 -1表示取出k1中全部value值(0表示左边第一个,-1代表右边第一个)。

Description

lpop / rpop key: 从左边/右边吐出一个值。值在键在,值光键亡。
pop表示把值拿出来。

Description
图片从左边取出k1的一个value值。如下图:

Description

从右边取出k2的一个value值。如下图:

Description

value值全部取完的时候,key就没有了。如下图:

Description

rpoplpush key1 key2: 从key1列表右边吐出一个值,插到key2列表左边。

Description

lindex key index: 按照索引下标获得元素(从左到右)。

Description

llen key: 获得列表长度。

Description

linsert key before value newvalue : 在value的前面插入newvalue插入值。

Description

lrem key n value: 从开始删除n个value(从左到右)图片。

Description

lset key index value: 将列表key下标为index的值替换成value。

Description

应用场景

消息队列: 使用LPUSH/RPUSH命令将待处理的消息添加到列表头部/尾部,使用LPOP/RPOP从列表中取出并处理消息。

关注列表: 存储用户关注的其他用户列表,使用LINSERT命令在列表中插入新关注的对象。

你还在苦恼找不到真正免费的编程学习平台吗?可以试试【云端源想】!课程视频、知识库、微实战、云实验室、一对一咨询……你想要的全部学习资源这里都有,重点是现在还是免费的!

点这里即可查看!

四、集合(Set):去重数据的集合

Set类型简介

集合类型用于存储无序且唯一的数据集合。当你需要存储不允许重复的元素时,集合是一个很好的选择。

SADD命令用于向集合中添加元素,而SMEMBERS可以获取集合中的所有元素。集合还支持交集、并集和差集等高级操作,非常适合于处理标签、好友关系等场景。

Redis的Set是string类型的无序,不可重复集合。Set底层其实是一个value为null的hash表,所以添加,删除,查找的复杂度都是0(1)。

常用命令

sadd key value1 value2 …: 将一个或多个 member 元素加入到key对应的集合中,已经存在的 member 元素将被忽略

smembers key: 取出key对应的集合中的所有值。

Description

sismember key value: 判断集合是否为含有该value值。1表示有,0表示没有。

Description

scard key: 返回key对应集合中的元素个数。

Description

srem key value1 value2…: 删除key对应的集合中的某些元素。

Description

spop key: 随机从key对应的集合中吐出一个值。

Description

srandmember key n: 随机从key对应的集合中取出n个值。不会从集合中删除 (rand即为random)。

Description

smove sourceKey destinationKey value: 把集合中一个值从一个集合移动到另一个集合。

Description

sinter key1 key2: 返回两个集合的交集元素

sunion key1 key2: 返回两个集合的并集元素。

sdiff key1 key2: 返回两个集合的差集元素(key1中的,不包含key2中的)。

Description

应用场景

好友关系: 将用户的好友列表存储在一个集合中,使用SADD命令添加好友,使用SISMEMBER判断某个用户是否为好友。

标签系统: 将文章或商品的标签存储在集合中,使用SUNION/SINTER等命令进行标签的交集、并集操作。

五、有序集合(Sorted Set)

Sorted Set类型简介

有序集合是Redis中的一个高级数据类型,它结合了集合的唯一性和列表的排序功能。

每个元素都关联一个分数(score),根据分数对元素进行排序。ZADD命令用于添加元素和分数,ZRANGE则可以获取排序后的元素列表。

注:zset是sorted set的缩写

常用命令

zadd key score1 value1 score2 value2…: 将一个或多个 member 元素及其 score 值加入到有序集 key 当中。

Description

zrange key start stop (withscores): 返回有序集 key 中,下标在start和stop之间的元素。(带withscores,可以让分数一起和值返回到结果集)。

zrange rank 0 -1 (withscores): 表示取出全部元素,从小到大排列。如下图:

Description

zrangebyscore key min max (withscores): 返回有序集 key 中所有 score 值介于 min 和 max 之间(包括等于 min 或 max )的成员。有序集成员按 score 值递增(从小到大)次序排列。

Description

zrevrangebyscore key max min (withscores): 同上,改为从大到小排列(其中rev表示reverse)。

Description

zincrby key increment value: 为元素的score加上增量。

Description

zrem key value: 删除该集合下,指定值的元素。

Description

zcount key min max: 统计该集合,分数区间内的元素个数。

Description

zrank key value: 返回该值在集合中的排名。(排在第一位的是0)

Description

应用场景

排行榜系统: 可以使用有序集合来存储用户的得分,并根据得分进行排序。

带权重的集合: 有序集合可以用来实现带权重的集合,即每个元素都有一个对应的权重值。

时间线排序: 有序集合可以用于实现时间线排序,即将事件或消息按照时间顺序进行排序。

六、总结

本篇文章我们探索了Redis的五种常用数据类型及常用命令和使用场景。每一种数据类型都有其独特的优势和适用情况,掌握它们将使你在数据处理的道路上更加得心应手。

无论是构建缓存系统,还是实现复杂的数据结构,Redis都能提供强有力的支持。希望这篇文章能帮助你更好地理解和运用Redis,让你的项目在数据的世界中脱颖而出。

收起阅读 »

听说你会架构设计?来,弄一个公交&地铁乘车系统

1. 引言 1.1 上班通勤的日常 “叮铃铃”,“叮铃铃”,早上七八点,你还在温暖的被窝里和闹钟“斗智斗勇”。 突然,你意识到已经快迟到了,于是像个闪电侠一样冲进卫生间,速洗漱,急穿衣,左手抄起手机,右手拿起面包,边穿衣边啃早餐。 这个时候,通勤的老难题又摆...
继续阅读 »

1. 引言


1.1 上班通勤的日常


“叮铃铃”,“叮铃铃”,早上七八点,你还在温暖的被窝里和闹钟“斗智斗勇”。



突然,你意识到已经快迟到了,于是像个闪电侠一样冲进卫生间,速洗漱,急穿衣,左手抄起手机,右手拿起面包,边穿衣边啃早餐。


这个时候,通勤的老难题又摆在了你面前:要不要吃完这口面包、刷牙和洗脸,还是先冲出门赶车?


好不容易做出了一个艰难的决定——放下面包,快步冲出门。你拿出手机,点开了熟悉的地铁乘车 App 或公交地铁乘车码小程序。


然后,一张二维码在屏幕上亮了起来,这可是你每天通勤的“敲门砖”。





你快步走到地铁站,将手机二维码扫描在闸机上,"嗖"的一声,闸机打开,你轻松通过,不再需要排队买票,不再被早高峰的拥挤闹心。


你走进地铁车厢,挤到了一个角落,拿出手机,开始计划一天的工作。


1.2 公交&地铁乘车系统


正如上文所说,人们只需要一台手机,一个二维码就可以完成上班通勤的所有事项。


那这个便捷的公交或地铁乘车系统是如何设计的呢?它背后的技术和架构是怎样支撑着你我每天的通勤生活呢?


今天让我们一起揭开这个现代都市打工人通勤小能手的面纱,深入探讨乘车系统的设计与实现


在这个文章中,小❤将带你走进乘车系统的世界,一探究竟,看看它是如何在短短几年内从科幻电影中走出来,成为我们日常生活不可或缺的一部分。


2. 需求设计


2.1 功能需求




  • 用户注册和登录: 用户可以通过手机应用或小程序注册账号,并使用账号登录系统。

  • 路线查询: 用户可以查询地铁的线路和站点信息,包括发车时间、车票价格等。

  • 获取乘车二维码: 系统根据用户的信息生成乘车二维码。

  • 获取地铁实时位置: 用户可以查询地铁的实时位置,并查看地铁离当前站台还有多久到达。

  • 乘车扫描和自动支付: 用户在入站和出站时通过扫描二维码来完成乘车,系统根据乘车里程自动计算费用并进行支付。

  • 交易记录查询: 用户可以查询自己的交易历史记录,包括乘车时间、金额、线路等信息。


2.2 乘车系统的非功能需求


乘车系统的用户量非常大,据《中国主要城市通勤检测报告-2023》数据显示,一线城市每天乘公交&地铁上班的的人数普遍超过千万,平均通勤时间在 45-60 分钟,并集中在早高峰和晚高峰时段。


所以,设计一个热点数据分布非均匀、人群分布非均匀的乘车系统时,需要考虑如下几点:



  • 用户分布不均匀,一线城市的乘车系统用户,超出普通城市几个数量级。

  • 时间分布不均匀,乘车系统的设计初衷是方便上下班通勤,所以早晚高峰的用户数会高出其它时间段几个数量级。

  • 高并发: 考虑到公交车/地铁系统可能同时有大量的用户在高峰时段使用,系统需要具备高并发处理能力。

  • 高性能: 为了提供快速的查询和支付服务,系统需要具备高性能,响应时间应尽可能短。

  • 可扩展性: 随着用户数量的增加,系统应该容易扩展,以满足未来的需求。

  • 可用性: 系统需要保证24/7的可用性,随时提供服务。

  • 安全和隐私保护: 系统需要确保用户数据的安全和隐私,包括支付信息和个人信息的保护。


3. 概要设计


3.1 核心组件




  • 前端应用: 开发手机 App 和小程序,提供用户注册、登录、查询等功能。

  • 后端服务: 设计后端服务,包括用户管理、路线查询、二维码管理、订单处理、支付系统等。

  • 数据库: 使用关系型数据库 MySQL 集群存储用户信息、路线信息、交易记录等数据。

  • 推送系统: 将乘车后的支付结果,通过在线和离线两种方式推送给用户手机上。

  • 负载均衡和消息队列: 考虑使用负载均衡和消息队列技术来提高系统性能。


3.2 乘车流程


1)用户手机与后台系统的交互


交互时序图如下:



1. 用户注册和登录: 用户首先需要在手机应用上注册并登录系统,提供个人信息,包括用户名、手机号码、支付方式等。


2. 查询乘车信息: 用户可以使用手机应用查询公交车/地铁的路线和票价信息,用户可以根据自己的出行需求选择合适的线路。


3. 生成乘车二维码: 用户登录后,系统会生成一个用于乘车的二维码,这个二维码可以在用户手机上随时查看。这个二维码是城市公交系统的通用乘车二维码,同时该码关联到用户的账户和付款方式,用户可以随时使用它乘坐任何一辆公交车或地铁。


2)用户手机与公交车的交互


交互 UML 状态图如下:




  1. 用户进站扫码: 当用户进入地铁站时,他们将手机上的乘车码扫描在进站设备上。这个设备将扫描到的乘车码发送给后台系统。

  2. 进站数据处理: 后台系统接收到进站信息后,会验证乘车码的有效性,检查用户是否有进站记录,并记录下进站的时间和地点。

  3. 用户出站扫码: 用户在乘车结束后,将手机上的乘车码扫描在出站设备上。

  4. 出站数据处理: 后台系统接收到出站信息后,会验证乘车码的有效性,检查用户是否有对应的进站记录,并记录下出站的时间和地点。


3)后台系统的处理



  1. 乘车费用计算: 基于用户的进站和出站地点以及乘车规则,后台系统计算乘车费用。这个费用可以根据不同的城市和运营商有所不同。

  2. 费用记录和扣款: 系统记录下乘车费用,并从用户的付款方式(例如,支付宝或微信钱包)中扣除费用。

  3. 乘车记录存储: 所有的乘车记录,包括进站、出站、费用等信息,被存储在乘车记录表中,以便用户查看和服务提供商进行结算。

  4. 通知用户: 如果有需要,系统可以向用户发送通知,告知他们的乘车费用已被扣除。

  5. 数据库交互: 在整个过程中,系统需要与数据库交互来存储和检索用户信息、乘车记录、费用信息等数据。


3. 详细设计


3.1 数据库设计



  • 用户信息表(User) ,包括用户ID、手机号、密码、支付方式、创建时间等。

  • 二维码表 (QRCode) ,包括二维码ID、用户ID、城市ID、生成时间、有效期及二维码数据等。

  • 车辆&地铁车次表 (Vehicle) ,包括车辆ID、车牌或地铁列车号、车型(公交、地铁)、扫描设备序列号等。

  • 乘车记录表 (TripRecord) ,包括记录ID、用户ID、车辆ID、上下车时间、起止站点等。

  • 支付记录表 (PaymentRecord) ,包括支付ID、乘车记录ID、交易时间、交易金额、支付方式、支付状态等。


以上是一些在公交车&地铁乘车系统中需要设计的数据库表及其字段的基本信息,后续可根据具体需求和系统规模,还可以进一步优化表结构和字段设计,以满足性能和扩展性要求。


详细设计除了要设计出表结构以外,我们还针对两个核心问题进行讨论:



  • 最短路线查询



  • 乘车二维码管理


3.2 最短路线查询


根据交通部门给的公交&地铁路线,我们可以绘制如下站点图:



假设图中的站点有 A-F,涉及到的交通工具有地铁 1 号线和 2 路公交,用户的起点和终点分别为 A、F 点。我们可以使用 Dijkstra 算法来求两点之间的最短路径,具体步骤为:


步骤已遍历集合未遍历集合
1选入A,此时最短路径 A->A = 0,再以 A 为中间点,开始寻找下一个邻近节点{B、C、D、E、F},其中与 A 相邻的节点有 B 和 C,AB=6,AC=3。接下来,选取较短的路径节点 C 开始遍历
2选取C,A->C=3,此时已遍历集合为{A、C},以 A 和 C 为中间点,开始寻找下一个邻近节点{B、D、E、F},其中与 A、C 相邻的节点有 B 和 D,AB=6,ACD=3+4=7。接下来,选取较短的路径节点 B 开始遍历
3选取B,A->B=6,此时已遍历集合为{A、C、B},A 相邻的节点已经遍历结束,开始寻找和 B、C 相近的节点{D、E、F},其中与 B、C 相邻的节点有 D,节点 D 在之前已经有了一个距离记录(7),现在新的可选路径是 ABD=6+5=11。显然第一个路径更短,于是将 D 的最近距离 7 加入到集合中
4选取D,A->D=7,此时已遍历集合为{A、C、B、D},寻找 D 相邻的节点{E、F},其中 DE=2,DF=3,选取最近路径的节点 E 加入集合
5选取 E,A->E=7+2=9,此时已遍历集合为{A、C、B、D、E},继续寻找 D 和 E 相近的节点{F},其中 DF=3,DEF=2+5=7,于是F的最近距离为7+3=10.
6选取F,A->F=10,此时遍历集合为{A、C、B、D、E、F}所有节点已遍历结束,从 A 点出发,它们的最近距离分别为{A=0,C=3,B=6,D=7,E=9,F=10}

在用户查询路线之前,交通部门会把公交 & 地铁的站点经纬度信息输入到路线管理系统,并根据二维的空间经纬度编码存储对应的站点信息。


我们设定西经为负,南纬为负,所以地球上的经度范围就是[-180, 180],纬度范围就是[-90,90]。如果以本初子午线、赤道为界,地球可以分成 4 个部分。



根据这个原理,我们可以先将二维的空间经纬度编码成一个字符串,来唯一标识用户或站点的位置信息。再通过 Redis 的 GeoHash 算法,来获取用户出发点附近的所有站点信息。


GeoHash 算法的原理是将一个位置的经纬度换算成地址编码字符串,表示在某个矩形区域,通过这个算法可以快速找到同一个区域的所有站点


一旦获得了起始地点的经纬度,系统就可以根据附近的站点信息,调用路线管理系统来查找最佳的公交或地铁路线。


一旦用户选择了一条路线,导航引擎启动并提供实时导航指引。导航引擎可能会使用地图数据和 GPS 定位来指导用户前往起止站点。


3.3 乘车二维码管理


乘车码是通过 QR 码(Quick Response Code)技术生成的,它比传统的 Bar Code 条形码能存更多的信息,也能表示更多的数据类型,如图所示:



二维码的生成非常简单,拿 Go 语言来举例,只需引入一个三方库:


import "github.com/skip2/go-qrcode"

func main() {
    qr,err:=qrcode.New("https://mp.weixin.qq.com",qrcode.Medium)
if err != nil {
    log.Fatal(err)
else {
    qr.BackgroundColor = color.RGBA{50,205,50,255//定义背景色
    qr.ForegroundColor = color.White //定义前景色
    qr.WriteFile(256,"./wechatgzh_qrcode.png"//转成图片保存
    }
}

以下是该功能用户和系统之间的交互、二维码信息存储、以及高并发请求处理的详细说明:



  1. 用户与系统交互: 用户首先在手机 App 上登录,系统会验证用户的身份和付款方式。一旦验证成功,系统根据用户的身份信息和付款方式,动态生成一个 QR 码,这个 QR 码包含了用户的标识信息和相关的乘车参数。

  2. 二维码信息存储: 生成的二维码信息需要在后台进行存储和关联。通常,这些信息会存储在一个专门的数据库表中,该表包含以下字段:



    • 二维码ID:主键ID,唯一标识一个二维码。

    • 用户ID:与乘车码关联的用户唯一标识。

    • 二维码数据:QR码的内容,包括用户信息和乘车参数。

    • 生成时间:二维码生成的时间戳,用于后续的验证和管理。

    • 有效期限:二维码的有效期,通常会设置一个时间限制,以保证安全性。



  3. 高并发请求处理: 在高并发情况下,大量的用户会同时生成和扫描二维码,因此需要一些策略来处理这些请求:



    • 负载均衡: 后台系统可以采用负载均衡技术,将请求分散到多个服务器上,以分担服务器的负载。

    • 缓存优化: 二维码的生成是相对耗时的操作,可以采用 Redis 来缓存已生成的二维码,避免重复生成。

    • 限制频率: 为了防止滥用,可以限制每个用户生成二维码的频率,例如,每分钟只允许生成 5  次,这可以通过限流的方式来实现。




总之,通过 QR 码技术生成乘车码,后台系统需要具备高并发处理的能力,包括负载均衡、缓存和频率限制等策略,以确保用户能够快速获得有效的乘车二维码。


同时,二维码信息需要被安全地存储和管理,比如:加密存储以保护用户的隐私和付款信息。



不清楚如何限流的,可以看我之前的这篇文章:若我问到高可用,阁下又该如何应对呢?



4. 乘车系统的发展


4.1 其它设计


除此之外,公交车或地铁的定位和到站时间计算可能还涉及定位设备、GPS 系统、NoSQL 数据库、用户 TCP 连接管理系统等核心组件,并通过实时数据采集、位置处理、到站时间计算和信息推送等流程来为用户提供准确的乘车信息。


同时,自动支付也是为了方便用户的重要功能,可以通过与第三方支付平台的集成来实现。


4.2 未来发展


公交车/地铁乘车系统的未来发展可以包括以下方向:



  • 智能化乘车: 引入智能设备,如人脸自动识别乘客、人脸扣款等。

  • 大数据分析: 利用大数据技术分析乘车数据,提供更好的服务。


在设计和发展过程中,也要不断考虑用户体验、性能和安全,确保系统能够满足不断增长的需求。


由于篇幅有限,文章就到此结束了。


希望读者们能对公交&地铁乘车系统的设计有更深入的了解,并和小❤一起期待未来更多的交通创新解决方案叭~


作者:xin猿意码
来源:juejin.cn/post/7287495466514055202
收起阅读 »

年,是团圆

大概是我刚上小学时候,村子里流行起来出门打工,每个家庭里面的壮年男人都会去湖北挖桩,组团去。他们过完年出发,先走路下山,坐车再坐船。 出门一段时间后,他们会寄钱回家,那时寄钱的方式我不清楚也还从没有问过,只记得家里女人们听到对门肉嗓子大喊“xxx,来拿汇票”时...
继续阅读 »

大概是我刚上小学时候,村子里流行起来出门打工,每个家庭里面的壮年男人都会去湖北挖桩,组团去。他们过完年出发,先走路下山,坐车再坐船。


出门一段时间后,他们会寄钱回家,那时寄钱的方式我不清楚也还从没有问过,只记得家里女人们听到对门肉嗓子大喊“xxx,来拿汇票”时,她们是很开心的。


快过年,男人们从外面回家,背着或挑着大件的行李,行李中是破旧的棉穗,小孩的新衣与不常见的水果糖。


挖桩需要两个人配合,一人在井里往下挖,一人在上面接桶转移土石,隔一段时间换人。有想法的男人认为他可以一直待在井里,女人提桶倒土是完全没有问题的,如果与自己的女人组团,工钱不用与别人分,挣钱的速度便会快上一倍。


于是,到我上初中时候,出门打工的规模更加扩大,家中的男人女人一起出门,小孩交给父母带着。


男人女人外出打工的时间是一样的,过完年出门,快过年时回家。


男人女人年龄渐长,他们的小孩长成大人,成人都外出打工,过完年出门,快过年时回家。


男人女人与小孩的目的地并不相同。于是过年,成为一家团圆的日子。


家家张灯结彩,人人喜笑颜开。


初一的早餐,是可以很简单的,汤圆或是包面;也可以是复杂的,蒸米饭,炸鱼、牛肉干、猪耳朵、凉拌鸡脚等许多凉菜,昨天的猪脚炖鸡汤,再配几个新鲜炒菜,满满一桌。复杂与简单的差异,来自于当天行程的安排,马上要走亲戚就简单些,家中要来客人便复杂点。


午饭,必然是丰盛的,一锅必不可少的鲜炖的猪脚炖鸡汤(是的,猪脚与鸡年三十只炖四分之一,留着客人来后能炖新的猪脚汤),一盆小孩儿最是欢迎的洋芋片,一锅人人喜爱的猪头汤清炖萝卜,两盘肥腻多汁男人们热衷的辣椒炒三线儿,一盘香肠、两碗扣肉,再来一盘炸豆腐……凉菜有猪肝、猪耳朵、鸡脚、炸鱼、牛肉干、萝卜干儿、折耳根……炒一个青菜,或是再加一碗青菜猪血汤。


男人们大都是会喝点酒的,他们互相劝着,你敬我一口酒,我回你两句话:“这酒好甜啊!”女人们偶尔也喝些酒,但更多是和小孩一起喝饮料。主妇是最后上桌吃饭的,大家吃饭时,她为桌上换热汤,为客人盛热饭。


吃过午饭,晒晒太阳烤烤火,嗑嗑瓜子吹吹牛。客人准备回家,临出发告诫主人:“你们明天一定要来啊,都要来。”


初二初三初四初五初六,是初一的重复。


隔得近,午饭的重复便是晚饭。吃过晚饭,今日不用再做饭,男人女人们围着炉子说过去说现在,说去年说当下,那“哈哈”大笑声,是时常能传到地坝的。


地坝里,孩子们的游戏一个接一个的切换。老鹰抓小鸡、三个字(一个人追众人跑,快被抓到时喊出随便的三个字,被抓者定住等待救援,抓人者切换目标;待所有人都被定住,便问“桃花开了没?”)、跳绳、打沙包……四岁五岁的,大多数游戏还玩不太明白,但他们的参与感该是最强的,因为哥哥姐姐叔叔阿姨都护着他们。


地坝里的叽叽喳喳,盖过屋子里哈哈哈哈。


年,是团圆!


作者:我要改名叫嘟嘟
来源:juejin.cn/post/7337957655190847522
收起阅读 »

年收入 100 万,不敢生孩子

今日热帖,有网友发帖称:互联网大头兵夫妇,两个人都 30+ 了,老公 xhs 后端年包 80+,我私企年薪 20 左右,老家三线城市有房。本来今年要孩子了,但我这边最近又在搞优化感觉很不稳定,老公这边还挺好,他们组的 Id 和业务线比我稳一些,不知道能不能算是...
继续阅读 »

今日热帖,有网友发帖称:互联网大头兵夫妇,两个人都 30+ 了,老公 xhs 后端年包 80+,我私企年薪 20 左右,老家三线城市有房。

本来今年要孩子了,但我这边最近又在搞优化感觉很不稳定,老公这边还挺好,他们组的 Id 和业务线比我稳一些,不知道能不能算是风险对冲。

主要焦虑的点还是怕有了孩子花销太大,不知道要不要回老家发展,还是继续在大城市坚守。

2023 年中国的出生人口是 902 万,比上一年少了 208 万,已经跌破一千万了。看到这个网友的担忧,有这个出生率也就不奇怪了。

这可是年收入超过 100 万的家庭啊,这个收入都不敢生,那还有人敢生吗?

不过话说回来,现在养娃也确实费钱。回想我小时候,哪穿过什么尿不湿,一个开裆裤就养大了。

现在呢,孩子一下生就得要个月嫂、尿不湿、吸奶棒、磨牙棒、奶粉、果泥、肉泥;长大了就开始各种兴趣班,钢琴、游泳、轮滑、英语、乐高,哪一个都得学。

怪不得都说孩子就是专业碎钞机,分分钟都在烧钱。

花钱是一方面,更重要的是会牵扯家长的精力。我听好多朋友说过,有了孩子之后,就很少睡过一个整觉。

再有一个就是教育,咱们都属于穷什么都不能穷教育,在教育上,无论多少钱都能花出去。

去年年底,好多小朋友生病,结果就是一边在医院打点滴,一边做作业,太卷了。

大人在公司卷,小孩在学校卷,卷在起跑线上。

再有一个原因就是缺乏安全感。

现在经济形势不好,公司随时都有可能裁员,一旦被裁员,就断了收入,就算有一定的积蓄,也会很焦虑。

大环境我们无法改变,只能从自身出发,调整好心态,积极乐观,从容面对。

1、明确自我定位,了解优劣势,规划未来职业道路

这也是了解自己的一个过程,了解自己的优势和劣势,目标是什么。只有目标明确了,才能走的更加坚定。

2、持续学习

这一点无需多言,活到老学到老,不管是哪一个行业,做什么类型的工作,持续学习都是必须的。

3、保持身体健康

身体是革命的本钱,特别是程序员,本来就加班很多,而且每天久坐,很多人身体都不是很好。一定要保持一定时间的运动,积极锻炼,有一个好的身体才能更好的工作。

熬过这个经济周期,就又是一条好汉。

之前看过一个说法,大概意思是说如果你现在不吃苦,不在大城市扎根,那将来就得你孩子来吃苦。

我之前感觉这句话太对了,所以一定得努力,留在大城市。但现在不这么想了,凭啥就得我吃苦?宁愿孩子吃苦,也不要自己吃苦。

过年回老家,和老家的同学和朋友聊天。我说我每天基本都是十点多下班,甚至十一点或者十二点,他们都觉得不可思议。

毕竟他们都是五点多就下班了,六点多吃完饭,就是老婆孩子热炕头了。

这可能就是世界的参差,我们总在一个环境下生活,就认为生活就是这样的,工作就是应该加班的,但实际上还有很多其他的生活方式。

就看我们有没有发现,有没有勇气去尝试。

我不想把自己陷入到大城市好,还是小城市好的争论中去,毕竟我原来是铁了心的想要在大城市,但现在已经动摇了。

还是那句话,没有哪一个选择是标准的,也没有哪一个选择是一层不变的。

但有一个原则是可以坚持的,那就是选择让自己舒服的。


作者:程序员贝塔
来源:mp.weixin.qq.com/s/aQF-FEwdLkvIPFMi3ZtKWQ

收起阅读 »

年后面试,最好不要有这几种心态

大家好,我是老三,大家新年好,我在朋友圈看到有朋友已经在大张旗鼓地“内卷”,为年后的面试做准备。 成功的面试常常是源于实力+运气,失败的面试可能会有各种各样的原因,知识点的盲区、和面试官不对眼、经验不匹配……很多东西我们是没法控制的,只能尽量做好自己能做的——...
继续阅读 »

大家好,我是老三,大家新年好,我在朋友圈看到有朋友已经在大张旗鼓地“内卷”,为年后的面试做准备。


成功的面试常常是源于实力+运气,失败的面试可能会有各种各样的原因,知识点的盲区、和面试官不对眼、经验不匹配……很多东西我们是没法控制的,只能尽量做好自己能做的——八股更熟一点、算法多刷一刷,硬技能相关的暂且不多聊,我们今天聊一聊,心态相关的,哪些心态,是求职面试中最好不要有的。


我得找个特别完美的工作


“钱多事少干的爽”,几乎是所有打工人的终极梦想,但是回归现实,真有这样的工作,凭什么流转到普通人身上呢?


有人可能会说,某某在XX公司,你看干的轻松,年终还发的贼多,真爽啊。这其实可以算幸存者偏差,我们有时候只能看到经过某种筛选而产生的结果,比如今年微信视频发了十几个月的年终,一度在各个公众号刷屏——我们可能觉得微信给的钱真多啊!但其实想想,哪怕同属WXG,除了微信视频,其它的部门也有这么“钱多”吗?


“钱多”、“事少”、“干的爽”,三者里面能满足一个就很不错了,钱多一般事也多,事少一般钱也少,”干的爽“更是一个非常主观的因素,自己不去干一干真不知道,干了也可以调整。


老三经历了几次跳槽,找工作的心理预期没那么高,为什么呢?我过去的工作经历基本上从低位到高位跳的一个过程,但是有的同学比较优秀,可能一出道就是在高位,刚工作就是大厂SSP。


这些有实力的同学,也许也会面临一个幸福的烦恼,就像谈恋爱,如果一个女生的初恋特别优秀,她后面可能会单身很久,因为她发现很难找到比初恋更优秀的男朋友。“曾经沧海难为水,除却巫山不是云”。


经济学上有一个“锚定效应”:



人们需要对某个事件做定量估测时,会将某些特定数值作为起始值,起始值像锚一样制约着估测值。在做决策的时候,会不自觉地给予最初获得的信息过多的重视。



有这么好的“初恋”,一般他们都不会轻易“分手”,但是假如遇到了一些波动,主观或者客观的原因,导致“分手”,那么在找下一份工作的时候,可能需要正视“锚定”的因素。


我们找工作的时候,也会去“锚定”,和现在的工作去比,和其它人的工作去比,但其实,相比评估价值,可能找准自己的需求更加重要,钱多发展职级大厂背书业务匹配稳定工作生活平衡 ……可以像排工作任务一样,排一下需求的优先级,哪些是必须的,哪些是可以妥协的。


找工作也和找对象类似,我有一个哥们,特别喜欢古力娜扎,可能很多人的女神也是迪丽热巴、古力娜扎、马尔扎哈……但是应该没有普通人会把自己找对象的目标定在这些女神头上。这就涉及到“吸引力”和“可得性”的平衡,很多时候,一份好的工作,对我们的“吸引力”是巨大的,但是我们也不得不承认它的“可得性”是极低的。


外包,是互联网软件行业一个比较普遍,而且越来越普遍的用工方式,有的朋友在外包——“你为什么要做外包呢?”,我觉得一般人也不会问这个问题,这就像之前有个主持人:“为什么不吃肉呢?因为肉不好吃吗?”生活所迫嘛,我们很多时候,只是在可选的选择里选一个相对最优的。


尤其是在现在整体行情不景气,如果真的没有办法,比如空窗了很久,妥协也是可以接受的,华为进不去OD也能凑合。


在经济学上,资源的配置常常达不到最优,找工作也是一样,能在一定可达性的基础上,通过努力获取次优的选择,已经很不错了。


“幻想不是理想”,这个世界上少有完美的工作,也少有完美的人生,我们很多时候,都要接受这些不完美,做一些适当的取舍。


留下还是跑路,好纠结


45度的人生最难受。


我去年经历了很纠结的时刻,当时的工作满足不了我的需求,但是我又不敢跑路,因为市场的寒气实在让人望而却步,就在这种纠结中,我错过了最好的面试备战和简历投递的时机,当时的第一目标快手,等我十月份觉得可以面一面的时候,HC已经锁了一个月了。


最怕的就是这种纠结的心态,工作投入不进去,面试也准备不好。还是那句话,找准现在的问题和需求。


第一,我现在的工作是不是已经非常让我难以接受了?有哪些点让我难以接受?是客观的原因还是主观的原因?我有没有尝试最大的努力去解决?我有没有寻求别人的帮助?


第二,现在这份工作的问题解决不了,我能通过跳槽解决吗?是不是换一个公司,换一个环境,就能解决或者缓解我的问题?什么样的公司能解决或者缓解我的问题?这样的公司对我的可得性大概有多少?


想清楚这两点,就能确认自己离开的意向,留下就好好干,努力去解决问题;要走就做好规划,挤出时间准备。


第三,工作和面试,不是绝对冲突的。只要有想离开的想法,再忙每天也能抽点时间出来准备的,把刷抖音,打游戏的时间拿一部分出来准备面试,做好规划,每天时间充足就拉短周期,每天时间少就拉长周期。


其实很多的时候的纠结,只是给自己找放纵的借口,“其实,我不是特别想走”,“外面现在行情很差,没什么机会”,这样一来,就可以心安理得地玩游戏、刷抖音,至于面试准备,后面再说吧。


我也经历了这样当鸵鸟的时期,直到一些关系比较好的同事因为各种原因离开,再加上一些裁员的传言,才彻底打醒了我。


打破纠结的最好办法就是行动,最怕的就是找个借口,让自己停在了原地。


这个面试没啥,随便面面


面试有自信是一件好事,比如老三,可能会觉得自己的八股比较熟;有的朋友,可能会觉得,自己的算法比较溜;有的觉得自己的项目比较亮眼。


自信,不是轻敌。我有个朋友吃了这样的亏,面飞猪的时候,随便约了个时间,当时那个时间是在旅行中,感觉的时候觉得很简单,结果挂了,事后一问,面试官说根本听不清;后面他面快手,也吃了同样的亏,因为没怎么准备,一面挂在了自己最擅长的算法上。


同样,面试,难和简单,也是一个很主观的感觉,别人感觉简单的,自己未必简单,还是那个朋友,他面飞猪,相对简单,我面飞猪,写了两道题,几乎问到了所有知识点的八股,足足面了九十多分钟。


认真对待每一场面试,现在这个环境,每一个机会都是非常难得的,很多面试机会,错过了就不再有。每一场面试,都尽量拿比较好的状态去面。


有的人说,我不咋想去,只是练手,练手也要认真一点,就像模拟考,也得好好考。而且现在不想去,可能只是了解不多,后面如果了解多了,未必不是完全不想去。


完全准备好了再面


面试,永远没有完全准备好的时候。很多时候,觉得准备个七八成的时候,就可以去面了。


第一,很多岗位的窗口期是很短的,在现在这种僧多粥少的局面下,一个岗位可能很短的时间就关闭了,如果不及时地去面,可能就会失去这个机会。


第二,面试永远会有答不上来的问题,面试本身就是个非对称的场景,以面试官之长,攻己之短,面试,也有一些运气的成分在里面。


第三,准备六个月,未必效果比三个月好,人的投入,是有边际递减的效应的,短时间高效率的准备,也许比拉长战线更有效果。


机会来了,估计着差不多就冲,我有个同事,很短的时间准备,但是因为年底的机会很好,也认真地准备了,一年多的经验,拿下了美团和京东的Offer。


我和面试官是对立的


面试是一场交流,一头是面试官,一头是候选人,有这么一个段子,面试是”三分天注定,七分靠打拼,九十分看面试官心情“,我和这样一个能一票否决我的人,我应该怎么打交道呢?


对立情绪是不可取的,在考试这个逻辑的下面,其实我们和面试官还有合作的逻辑。我以前作为面试官,面了几十个人,面到后面,我的感受是什么呢?赶紧招一个合适的人选吧,别让我面试了。


在这种关系里,面试官也是有需求的,面试官希望尽快招到合适的人,让自己早点摆脱面试对自己的干扰。面试官也是有投入的,毕竟面试的时间,基本都不会算进工作排期。


既然双方都有需求和投入,当然候选人的需求和投入会更多,那么这段关系就不是完全从上到下俯视的,我觉得最好的面对面试官的心态,就是萍水相逢的路人,尊重但不谄媚、存异但是和谐、热情但有边界,如果有缘份的话,我们可能会成为真正的朋友,如果没有缘分那就相忘江湖。


很多人说,程序员都不太擅长交流,但至少请保持礼貌,“您好”,“谢谢”,“不好意思”,“感谢你的时间”,我觉得这些礼貌用语,应该小孩子都会吧。


面试,不仅有硬实力的碰撞,也有人情世故的交融。


面试官让我不爽


我在之前,某个博主的文章里,看到他在某个公司的三面挂了,因为感觉和面试官不太对味。根据我的经验,其实三面很少挂人,这种,就是和面试官的”碰撞“激烈了一些。


我们应该明白一个道理:在面试的流程中,我们是没有挑选的权利的,只有拿到Offer后,我们才是真正有挑选的权利。


这个世界我们不认同的东西多了,要么改变它,要么适应它,这句话不太好听,既然选择了打工,那大概率是只能适应的。


面试里,面试官说的东西,我们可能不认同,面试官的态度甚至可能不友好,我觉得我们能做的,就是保持礼貌:


“对对,你是对的。”


我去年面西二旗某厂,三面面试官给我的感觉是无知且蛮横,她的很多问题都让我无言以对,但是我还是全程保持了微笑,努力保持了风度,没想到最后面试过了。


如果你真的看面试官不爽,在Offer后拒绝他们,不比在面试阶段,闹得不欢而散,最后只能心里默默骂着傻叉要痛快?


面的不好,放弃吧


面试永远有我们不会的问题,这是正常的。可能在某个问题卡壳之后,心态就慢慢崩掉,破罐子破摔。这时候,一定要给自己一些积极的心理暗示,面试直接发生在面试官和候选人之间,间接发生在候选人和候选人之间。


“我这种八股文职业选手都答不好,其他人更不行了。”


而且,答的不好,未必就是没戏,很多时候,面试官可能通过一些开放性的问题或者压力面的方式,考察候选人的广度深度,思维方式。


所以,哪怕偶有一个点答不好,也一定要绷住心态,我之前作为面试官,面试实习生,一个候选人,坦白说,她面的不好,同事的实习生,他觉得可以,那就通过吧,结果告知她面试通过后,那个候选人心态崩了,放弃了后面的面试。


最差不过挂了,最坏的结果都能接受,那就朝最好的方向尝试努力。


失败了好几次,没信心了


我们经常在网上,刷到一些“Offer收割机”“吊打面试官”,但是经过我本人的实践,反正我是做不到,比如美团,我面了四个岗位才拿到Offer。


面试失败,是一件很正常的事情,“面了不一定能过,过了不一定能Offer”,我们可能因为各种原因挂掉。但是,千万不能因为失败几次,就灰心丧气,面一次总结一次,查缺补漏,面的多了,自然会面了,就能面过了。


而且面试也有运气的成分,此路不通,未必别路也不通。


很多人恐惧面试,其实我也怕面试,每次面试前,都很忐忑,但是想一想,最差不过挂了,越淡定的面试发挥地越好,越紧张的面试发挥地越差。


在挫折里不断进步,保持心态,才有机会拿到好的结果。






工作是为了更好地生活,生活是为了快乐和幸福,每个人的快乐和幸福都是自己定义的,祝大家既能找到好的工作,也能享受生活的快乐和幸福。






参考:


[1].《长的好看,能当饭吃吗:提升认知的33个经济学常识》


[2].《经济学原理》






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

前端又又出新框架,这次没有打包了

web
最近,前端开发领域又迎来了一个新框架——ofa.js。它的独特之处在于,不依赖于现有的 nodes/npm/webpack 前端开发工作流程。与jQuery类似,只需引用一个脚本,您就能像使用React/Vue/Angular一样轻松地开发大型应用。 极易上...
继续阅读 »

最近,前端开发领域又迎来了一个新框架——ofa.js。它的独特之处在于,不依赖于现有的 nodes/npm/webpack 前端开发工作流程。与jQuery类似,只需引用一个脚本,您就能像使用React/Vue/Angular一样轻松地开发大型应用。


punch-logo.png


极易上手


如果您要开发简单的项目,想要用一个漂亮的按钮组件,例如 Ant Design 中的 Button组件,你需要学习Node.js、NPM和React等知识,才能开始使用该按钮组件。对于非前端开发者或初学者来说,这将是一个漫长的过程。


如果使用基于ofa.js 开发的组件,就不需要这么复杂了;你只需要了解HTML的基础知识(即使不看ofa.js的文档),也可以轻松使用基于ofa.js开发的组件。以下是使用官方的 punch-logo 代码示例:


<!-- 引入ofa.js到您的项目 -->
<script src="https://cdn.jsdelivr.net/gh/kirakiray/ofa.js/dist/ofa.min.js"></script>

<!-- 加载预先开发的punch-logo组件 -->
<l-m src="https://ofajs.github.io/ofa-v4-docs/docs/publics/comps/punch-logo.html"></l-m>

<!-- 使用punch-logo组件 -->
<punch-logo style="margin: 50px 0 0 100px">
<img
src="https://ofajs.github.io/ofa-v4-docs/docs/publics/logo.svg"
logo
height="90"
/>

<h2>不加班了</h2>
<p slot="fly">下班给我</p>
<p slot="fly">迟点下班</p>
<p slot="fly">周末加班</p>
</punch-logo>

punch-demo.gif


你可以最直接拷贝上面的代码,放到一个空白的html文件内运行试试;这使得ofa.js非常容易与传统的Web开发技术栈相融合。


一步封装组件


封装组件同样非常简单,只需一个HTML文件即可实现。以下是一个官方封装的开关(switch)组件示例:


<!-- my-switch.html -->
<template component>
<style>
.switch {
position: relative;
display: inline-block;
width: 60px;
height: 34px;
background-color: #ccc;
transition: background-color 0.4s;
border-radius: 34px;
cursor: pointer;
}

.slider {
position: absolute;
height: 26px;
width: 26px;
left: 4px;
bottom: 4px;
background-color: white;
transition: transform 0.4s;
border-radius: 50%;
}

.switch.checked {
background-color: #2196f3;
}

.switch.checked .slider {
transform: translateX(26px);
}
</style>
<div class="switch" class:checked="checked" on:click="checked = !checked">
<span class="slider"></span>
</div>
<script>
export default {
tag: "my-switch",
data: {
checked: true,
},
};
</script>
</template>

在使用时,只需使用 l-m 组件引用它:


<script src="https://cdn.jsdelivr.net/gh/kirakiray/ofa.js/dist/ofa.min.js"></script>
<l-m src="./my-switch.html"></l-m>
<my-switch></my-switch>

switch.gif


示例可以在官方网站下方查看。由于无需打包流程,只需将文件上传到静态服务器即可发布,还可以进行跨域引用,这极大降低了组件共享的成本。


多种模板语法糖


ofa.js与Vue和Angular一样提供了许多模板语法糖,主要包括:



  • 文本渲染

  • 属性绑定/双向绑定

  • 事件绑定

  • 条件渲染

  • 列表渲染

  • ...


具体案例可在官网向下滚动至“提供多样便捷的模板语法”处查看。


天生的状态同步高手


与其他框架不同,ofa.js 使用无感状态同步。这意味着数据不需要通过函数操作,只需设置数据对象即可实现状态同步。以下是一个共享黑夜模式的按钮示例:


// is-dark.js
const isDark = $.stanz({
value: false,
});

export default isDark;

<!-- my-button.html -->
<template component>
<style>
:host {
display: block;
}

.container {
display: inline-block;
padding: 0.5em 1em;
color: white;
border-radius: 6px;
background-color: blue;
cursor: pointer;
user-select: none;
}
.container.dark {
background-color: red;
}
</style>
<div class="container" class:dark="isDark.value">
<slot></slot>
</div>
<script>
import isDark from "./is-dark.js";
export default {
data: {
isDark: {},
},
attached() {
// 共享dark对象数据
this.isDark = isDark;
},
detached() {
// 清除内存记录
this.isDark = {};
},
};
</script>
</template>

sync-state.gif


您可以跳转到 状态同步案例 以查看效果。


最简单的表单操作


表单只需调用formData方法,就能生成自动同步数据的对象:


<form id="myForm">
<input type="text" name="username" value="John Doe" />
<div>
sex:
<label>
man
<input type="radio" name="sex" value="man" />
</label>
<label>
woman
<input type radio="radio" name="sex" value="woman" />
</label>
</div>
<textarea name="message">Hello World!</textarea>
</form>
<br />
<div id="logger"></div>
<script>
const data = $("#myForm").formData();

$("#logger").text = data;
data.watch(() => {
$("#logger").text = data;
});
</script>

form1.gif


您还可以轻松地反向设置表单数据:


<form id="myForm">
<input type="text" name="username" value="John Doe" />
<div>
sex:
<label>
man
<input type="radio" name="sex" value="man" />
</label>
<label>
woman
<input type="radio" name="sex" value="woman" />
</label>
</div>
<textarea name="message">Hello World!</textarea>
</form>
<br />
<div id="logger"></div>
<script>
const data = $("#myForm").formData();

setTimeout(() => {
// 反向设置数据
data.username = "Yao";
data.sex = "man";
data.message = "ofa.js is good!";
}, 1000);
</script>

form2.gif


制作自定义表单组件也没有其他框架那么复杂,只需为组件定义valuename属性即可。


具体效果可跳转至formData API查看。


开发应用


您还可以使用ofa.js开发Web应用,然后直接引用已开发的应用到您的网页上:


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>应用演示</title>
<script src="https://cdn.jsdelivr.net/gh/kirakiray/ofa.js/dist/ofa.min.js"></script>
</head>
<body>
<o-app src="https://xxxxx.com/app-config.mjs"> </o-app>
</body>
</html>

具体效果可跳转至使用o-app组件查看。


SCSR


官方提供了一种类似服务端渲染的解决方案,它不仅保证了用户体验,还使页面在静态状态下可被搜索引擎爬取。官网采用了SCSR的方案。



SCSR的全称是Static Client-Side Rendering,又称为静态客户端渲染。它是CSR(Client-Side Rendering)的一种变种,在保留了CSR用户体验的基础上,还能够让页面在静态状态下被搜索引擎爬取。



您可以点击SCSR方案以查看详细信息。


代码简洁


当前版本4.3.29的 ofa.min.js 文件仅有52KB,经过gzip压缩后仅有18KB。


其他


最近升级到了v4版本,目前可用的第三方库还比较有限,但以后将逐渐增加。作者正在准备开发基于ofa.js的UI库。


作者是一位中国开发者,快去给 ofa.js 加星吧!


作者:皮卡怪兽
来源:juejin.cn/post/7295576148364460071
收起阅读 »

全世界都想让程序员专心写业务代码

最近两年我不知道你是否和我拥有同样的感受,编程容易了许多,同时乐趣也少了许多 一 最近有两个契机让我写这篇文章,一是过去一年我陆续把我所有 side project 后端从 Azure App Service 迁移到 Digital Oce...
继续阅读 »

最近两年我不知道你是否和我拥有同样的感受,编程容易了许多,同时乐趣也少了许多



最近有两个契机让我写这篇文章,一是过去一年我陆续把我所有 side project 后端从 Azure App Service 迁移到 Digital Ocean(以下简称 DC) 的 Droplet(Virtual Machine),再迁移到 DC 的 K8S,目的是剥离应用内托管型(managed)资源;前端顺便也从 Azure Static Web Apps 迁移到 Vercel;二是最近读完《Competing in the Age of AI》,其中关于于 operation model 的论述与近期迁移应用的感受不谋而合


书中把公司价值划分为两部分,首先是商业模型(business model),即为消费者带来了何种价值;其次是运营模型(operation model),即如何将价值传递到消费者手中。运营的难点不在交付本身,而在于如何大规模(scale)、大范围(scope)交付,并且持续对交付方法改进和创新(learning)


同样,如果代码的价值在于上线(通俗说派上用场),无论是对自己还是客户还是用户,那么交付对我们来说同样重要,以上所说的大规模、大范围、以及保证持续改进的交付难点也同样成立。


先上线,剩下的以后再说



你们有没有想过 Netlify 和 Vercel 究竟是做什么生意的?如果你的答案依然是 JAM stack,只能说明你已经 out 了。不妨去看看 Netlify 官网的 slogan,甚至不用是 slogan,看看网站首页的 title 写的是什么:



Scale & Ship Faster with a Composable Web Architecture



Netlify 所做的正如上述所标榜的,帮助代码更快的交付上线。换而言之,它们售卖的的是一站式 web 应用的部署解决方案,除 host 和流水线外,还提供免费的用户行为分析、页面性能监控、日志等工具,以及 Redis/SQL/Blob 数据库。


Azure 不是也提供类似的服务吗?问题的关键在于不同平台间的开发者体验,Vercel 能够真正做到上述服务一键集成,你会明显感觉到 Vercel 在加速你的交付,让你更快得到对于产品的反馈。而在 Azure 下开发更像是赤裸裸的买卖,Azure 团队按照他们的理解提供一个最低限度可以使用产品,至于多好用,多大程度能和其它 Azure 服务契合则不在他们的考虑范围内。如下图所示在 Vercel 的 dashboard 上,你可以纵览所有从部署状态到线上监测一系列信息。试想一下如果在 Azure 上集成所有对应的功能并呈现类似的 dashboard 需要付出多少外的精力


vercel-screenshot.png



再简单聊聊 DevSpace。这里的 DevSpace,也可以是 tilt,可以是 skaffoldtelepresence


GitOps 无疑是伟大的,但这种伟大里依然充满了“一板一眼”——我的一次上线需要经过 push 代码、创建 release、推送镜像到GHCR、更新 yaml、通过 ArgoCD 部署。我理解它们是 necessay evil。


相比之下 DevSpace 看上去是反流程的,它可以帮助你在本地直接创建镜像并执行部署,还允许热更新容器内文件。即便是创建镜像,我发现它利用也非是既有的 pipeline,而是借助 buildkit 在我的集群上创建一个 local registry 并将镜像储存在此。


别搞糊涂了,生产环境和开发环境具有完全相反的气质,我们希望生产环境稳定、可追溯,这是 GitOps 的优势所在,但开发过程具有破坏性、需要不停试错,DevSpace 着重解决的则是开发体验问题,对于代码修改它可以带来高效的反馈。


compiling-joke.png


上图已经流传了很多年,图中的 compiling 可以替换成 uploading, deploying,running test 等等。过去十年它是事实,现在应该只是一个笑话



在 Vercel 和 DevSapce上我们看到了一类区别于传统的开发方式,这里没有 pipeline,没有 IaC,没有手动的统计埋点,无需申请第三方资源,从 GitHub 上导入 repo 的那一刻起,它自动为你匹配部署脚本,寻找项目入口。开发者所需要做的,就是专心编写他所需要的业务代码


模板化的项目是带来这些便利的主要原因,继续往上追溯,主流前端框架自带 CLI 工具的流行功不可没,从大到创建一个新项目小到执行编译,命令行一键即可完成。无论是 React,Vue 还是 Angular,你都不会在官方的教程里找到专门的章节教你为项目编写 webpack 或者 vite 的配置。也许是因为习惯了早年间凡事都需要自己动手的工作方式,我花了很长时间才想通并接受这件事:传统从0到1搭建项目的能力已不再重要。


越来越多的环节像一个个黑盒出现在开发流程中,在此之前如果我对 pipeline 感兴趣,我可以阅读 .github 文件夹中的代码看 action 是如何定义的,而现在我连机会都没有了。连同代码被一同干掉的还有程序员的编程乐趣。站在交付的角度上来说这种乐趣并没有什么道理:为什么我要花上几天的时间只为了研究如何写一段 yaml 把代码送上线运行,更何况这段代码带来不了任何产品收益。


最近另一个类似的体验是,Next.js 编写起来太无趣了,不是说它不够好,而是问题在于它太好到你只能这么写。说起来有些贱,我很怀念那个一种功能有十几个写法,然后这十几个写法还有鄙视链的“坏”时光


当我们再度审视代码的生命周期,你会发现所有人努力的目标,是想让程序员尽可能少的关心核心业务代码以外的事情。一切一切都在想方设法优化运营模型。很明显个性化配置和重复劳动在规模化前是不受欢迎的



也许换个思路会好受一些:当你加入一家公司之后,发现同等体量的应用利用市面上的工具可以做到随时上线;而你们的产品却只能选择在每个周六凌晨部署,并需要若干人值班守候,这听上去让人有些失望是不是。


cover.jpg


作者:李熠
来源:juejin.cn/post/7336538738750013440
收起阅读 »

我收到的最好的职业建议

Nicholas C. Zakas 是全世界最著名的 JavaScript 程序员之一。 今天突然看到大神在13年写的一篇长文,回顾自己的职业生涯,提到七个对他来说最重要的建议 个人觉得每一条都很受益;所以决定翻写出来;做一个学习记录。 一、Don’t be...
继续阅读 »

image.png
Nicholas C. Zakas 是全世界最著名的 JavaScript 程序员之一。


今天突然看到大神在13年写的一篇长文,回顾自己的职业生涯,提到七个对他来说最重要的建议


个人觉得每一条都很受益;所以决定翻写出来;做一个学习记录。




一、Don’t be a short-order cook 不要做一个点菜厨师



你要别人点什么;你就做什么。积极表达自己的见解



我的第一份工作持续了8个月,因为公司倒闭了。当我和我的经理谈论我下一步该怎么做时,他给了我这样的建议:


尼古拉斯,你的价值不仅仅在于你的代码。无论你的下一个工作是什么,一定要确保你不是一个按需制作的厨师。不要接受那种你被告知要构建什么以及如何构建的工作。你需要在一个既欣赏你对产品的见解,又欣赏你构建能力的地方工作。


这是我在整个职业生涯中一直牢记的事情。仅仅做一个实现者是不够的--你需要参与到实现的过程中。优秀的工程师不只是服从命令,他们会给产品负责人给予反馈,并与他们合作,使产品更好。幸运的是,我明智地选择了我的工作,从来没有在人们不尊重或不重视我的见解的情况下结束。


个人反思:在选择一份工作的时候,一定要有自主权;这个自主权可以是自己争取的;通过表达意见以及个人反馈来进行争取。侧面也说明需要自己对相关系统有更深的理解。


二、Self-promote 自我推销



推销自己



进入雅虎后;有一天,我在雅虎的团队经理把我拉到一边,给予我一些建议。他一直在看我的代码和产出内容,告诉我,觉得我有点缺失:


你做得很好。我是说真的很棒。我喜欢你的代码,它很少有bug。但是; 问题是别人看不到这一点。为了让你的工作得到认可,你必须让别人知道。你需要做一些自我推销来引起注意。


我花了一点时间来消化他说的话,但我终于弄明白了。如果你做了很好的工作,但没有人知道你做了很好的工作,那么它并没有真正帮助你。你的经理可以支持你,但不能为你辩护。组织内部的人需要了解你的价值,最好的方法就是告诉人们你做了什么。


个人反思:做一个角落里静静编码的工程师,并不可取。你的主管会支持你,但是他没法替你宣传。公司的其他人需要明白你的价值,最好的办法就是告诉别人你做了什么。一封简单的Email:"嗨,我完成了XXX,欢迎将你的想法告诉我",就很管用。


三、It’s about people 关注人员,学会团队相处



学会带领团队



在我职业生涯的早期,我非常注重头衔。我一直想知道我必须做些什么才能得到晋升。在雅虎主页上与我的新经理进行第一次一对一的交谈时,我问我需要什么才能获得晋升。他的话至今仍在我耳边回响:


当你的技术能力过关以后,就要考验你与他人相处的能力了。


从那时起,我不确定我是否对软件工程专业有了更好的了解。他是完全正确的。那时,没有人质疑我的技术能力。众所周知,我是一个写出优秀、高质量代码的人,很少有错误。我缺乏的是领导能力


从那时起,我看到无数工程师在他们的职业生涯中被困在一个层面上。聪明的人,优秀的代码,但无法与他人有效合作,使他们留在原地。每当有人觉得自己被困在软件工程生涯中时,我都会讲述这个建议,而且它总是在金钱上是正确的。


个人反思:在团队工作中,除了完成自己的工作后;更多需要学会与团队成员的相处,这决定了你是否能够走向管理;当然;领导能力也是自己需要认真培养和学习的。


四、 of this matters 这些都不重要



生活才是最重要的



我在雅虎度过了一段令人沮丧的时期。也许沮丧不是正确的词,更像是生气。我愤怒地爆发,经常与人争吵。事情出了问题,我不喜欢这样。在一个特别艰难的一天,我问我的一位导师,当很多事情出错时,他是如何保持冷静的。他的回答是:


这很简单。你看,这些都不重要。所以一些蹩脚的代码被签入了,所以网站宕机了。那又怎样?工作不可能是你的全部生活。这些不是真正的问题,而是工作问题。真正重要的是工作之外发生的事情。我回家,我的妻子在等我。这真是太好了


我从马萨诸塞州搬到加利福尼亚,很难交到朋友。工作是我的生命,它让我保持理智,所以当它不顺利时,这意味着我的生活不顺利。这次谈话让我意识到我的生活中必须有其他事情发生,我可以回去忘记我在工作中遇到的麻烦。


他是对的,一旦我改变了思维方式,将工作中烦人的事情重新归类为“工作事情”,我就能够更清楚地思考。我能够在工作中冷静下来,与人进行更愉快的互动。


个人反思:把工作和生活分开,工作不是你的全部;学会分清楚这个是工作问题;不要把工作问题带出来的情绪带入到生活中。


五、Authority, your way 自主,自己决策



找到自己道路



当我第一次被提升为雅虎的首席工程师时,我和我的主管坐下来,以更好地了解这个角色的含义。我知道我必须更像一个领导者,但我很难自主决策。我寻求帮助。他是这样说的:


以前都是我们告诉你做什么,从现在开始,你必须自己回答这个问题了,我期待你来告诉我,什么事情需要做。


很多工程师都没有完成这个转变,如果能够做到,可能就说明你成熟了,学会了取舍。你不可能把时间花在所有事情上面,必须找到一个重点。


个人反思:你需要慢慢的学会自主决策,成为专家。找到自己的风格;并显现出来。


六、Act like you’re in charge 把自己当成主人


我每天要开很多会,有些会议我根本无话可说。我对一个朋友说,我不知道自己为什么要参加这个会,也没有什么可以贡献,他说:


"不要再去开这样的会了。你参加一个会,那是因为你参与了某件事。如果不确定自己为什么要在场,就停下来问。如果这件事不需要你,就离开。不要从头到尾都静静地参加一个会,要把自己当成负责人,大家会相信你的。"


从那时起,我从没有一声不发地参加会议。我确保只参加那些需要我参加的会议。


七、找到水平更高的人


最后,让我从自己的经历出发,给我的读者一个建议。


"找到那些比你水平更高、更聪明的人,尽量和他们在一起,吃饭或者喝咖啡,向他们讨教,了解他们拥有的知识。你的职业,甚至你的生活,都会因此变得更好。"


作者:GleanWu
来源:juejin.cn/post/7337227407365423131
收起阅读 »

在上海做程序员这么多年,退休后我的工资是多少?

大家好,我是拭心。 最近看到一个很可惜的事:有个阿姨在深圳缴纳了 12 年社保,第 13 年家里突然有事不得不回老家,回去后没再缴纳社保,结果退休后无法领退休工资,还得出来打工赚钱。 之所以这样,是因为阿姨及其家人对退休的相关知识了解不多,痛失一笔收入。 吸取...
继续阅读 »

image.png


大家好,我是拭心。


最近看到一个很可惜的事:有个阿姨在深圳缴纳了 12 年社保,第 13 年家里突然有事不得不回老家,回去后没再缴纳社保,结果退休后无法领退休工资,还得出来打工赚钱。


之所以这样,是因为阿姨及其家人对退休的相关知识了解不多,痛失一笔收入。


吸取教训,作为在上海工作多年的打工人,为了老有所依,我花了些时间学习了养老金相基本知识,并且估算了一下退休后每月能拿到的钱,这篇文章来聊聊。


文章主要内容:



  1. 如何能在上海领退休工资

  2. 我退休后大概能领多少钱

  3. 退休工资的组成


如何能在上海领退休工资


image.png


上海作为全国 GDP 第一的城市,居民的收入也是很可观的。平均工资在 2023 年达到了 12183,平均退休工资在全国也位列前茅:


image.png


从上图可以看到,上海的平均退休工资居然有五千多!不用早起不用挤地铁,躺在家里每月就能领五千多,花不完,根本花不完啊!


那么问题来了,我们如何能领到上海的退休工资?


主要有 2 个条件:



  1. 达到退休年龄:男性满 60 周岁,女性满 50 周岁(灵活就业人员需要满 55 周岁)

  2. 退休前累计缴费社保 >= 180 个月(也就是 15 年),其中在上海至少缴满 120 个月(10 年)


第二点很关键:在上海需要缴满社保 10 年,上海 + 其他地方累计需要缴满 15 年。 比如小张在上海工作并缴纳社保满 10 年,然后去青岛缴纳 5 年,最后可以在上海领退休工资;但如果在青岛缴纳了 10 年,在上海缴纳了 5 年,就无法在上海领退休工资了。


需要强调的是,这里说的是「累计缴满」,即使中间有断开也没关系。


还有一个细节是,发工资不等于缴纳社保,个别不正规公司会漏缴社保,这需要我们打工人自己多关注。 身边有朋友遇到过:刚毕业加入的公司规模很小,人力资源不靠谱,干了半年多只缴纳了两个月社保。


怎么看公司有没有给自己缴社保呢?我们可以从随申办上查询:


image.png


OK,这就是在上海领退休工资的条件。


退休后大概能领多少钱


掐指一算,社保缴满 15 年的任务我已经完成了一半,但还有二十九年才能领钱,心里苦啊😭。


虽然拿不到,但我对能领多少钱还是非常好奇的,究竟比平均退休工资高还是被平均?🤔


经过一番搜索,我终于发现了退休工资的计算方法。


国家社会保险公共服务平台网站中,有一个「企业职工养老保险待遇测算」的功能:


image.png



国家社会保险公共服务平台 -> 养老保险 -> 企业职工养老保险待遇测算


链接:si.12333.gov.cn/157569.jhtm…



我们只需要输入年龄、预计退休年龄、当前缴纳年限、目前及之前平均工资、养老保险个人账户余额及未来大概工资即可测算退休工资。


如果不知道你的「养老保险个人账户余额」有多少,可以从随申办 app 搜索「养老金」查询余额:


image.png


在填完所有需要的信息后,我的预算结果是这样的:


image.png


我的天,个十百千万,居然有两万五?这还花的完??


几秒后冷静下来,我才发现算错了。。。


两万五应该是这样算的:按照当前收入,再连续缴纳 29 年。 😂


image.png


对于我们程序员,保持收入 29 年基本是不可能的,我还是重新调整参数再看看吧。


我现在社保缴纳了七年半,如果缴够 15 年社保,退休后我能领多少钱呢?


image.png


答案是一万四!看着还不错哈,每天能有 480 元左右,就是不知道 30 年后的物价怎么样了😂。


如果再悲观一点,社保缴纳到 35 岁(然后最低标准缴够 15 年),退休后大概能领多少呢?


image.png


答案是一万元!


看了下人民币的贬值率,30 年后的一万元不知道有没有今天三千块钱的购买力😷。


OK,这就是我退休后大概能领到的工资范围。


退休工资的组成


从上面的预算结果中我们可以看到,养老金由三部分组成:基础养老金、个人账户养老金和过渡性养老金,它们都是什么意思呢?


1.基础养老金 = 退休时平均工资 ×(1+平均缴费指数)÷ 2 × 累计缴费年限 × 1%。


退休时平均工资指的是退休时所在地区上年度的社会平均工资。也就是说经济发达地区的基础养老保险金,要高于欠发达地区。


平均缴费指数指的参保人选择的缴纳比例(一般在0.6-3之间)。每个月社保的缴费比例越高,相应的基础养老金越高。


例如,小张退休时,上年度的社会平均工资是 12000。虎虎的缴费指数平均值是 1,累计缴存了15年,他的基础养老金约为:12000*(1+1)/2 * 15 * 1% = 1800。


2.个人账户养老金 = 养老保险个人账户累计金额 ÷ 养老金计发月数。


我们缴纳社保时,一部分会进入个人社保账户,一部分会进入国家统筹账户。个人账户的部分,直接影响退休养老金的计算。


计发月数和我们退休的年龄有直接的关系,退休的越晚,计发月数越少;退休的时间越早,计发月数越多。一般来说,按照 60岁 退休,计发月数是 139 个月。



退休金的计发月数只是用来计算退休金,而不是说只能领这么久的退休金。



例如,小张社保的个人缴纳比例为 8%,社保的计算基数是 9339,他选择在 60 岁退休,那他的个人账户养老金约为:9339 * 8% * 12 * 15 / 139 = 967.49。


3.过渡性养老金 = 退休时平均工资 × 建立个人账户前的缴费年限 × 1.3% × 平均缴费指数


过渡性养老金,是指在养老保险制度发生变化(比如缴费标准提高、计算方法改变、退休年龄调整)的时候,给予受影响群体的资金补充。


这个奖金的计算规则说法不一,一种比较广泛的计算方法是:退休时所在地区的平均工资 x 缴费指数 x 缴费年限 x 过渡系数,其中过渡系数大概在 1% 到 1.4% 之间。


OK,这就是养老金三部分组成的含义。


总结


好了,这篇文章到这里就结束了,主要讲了:



  1. 如何能上海领退休工资:缴纳 10~15 年社保,到达退休年龄

  2. 我退休后大概能领多少钱:30 年后的一万左右

  3. 退休工资的三部分组成:基础养老金、个人账户养老金和过渡性养老金


通过写这篇文章,我对养老金的认识更多了一些,希望国家繁荣昌盛,让我退休的时候能多领点钱!


作者:张拭心
来源:juejin.cn/post/7327480122407141388
收起阅读 »

22点下班的地铁上闲看发现一个现象...求解

打工人写代码写到晚上十点😭 匆匆忙忙赶上地铁,累的手机也不想玩,因为眼睛累,也不想坐因为坐了一天了,闲的蛋疼,也不知道怎么的,脑子里就出了这么个想法😓 我发现了一个奇怪的现象,无论是大学生,还是普通的上班族,或者城市写字楼里的白领,现在很多人,包括年轻人,脚上...
继续阅读 »

打工人写代码写到晚上十点😭
匆匆忙忙赶上地铁,累的手机也不想玩,因为眼睛累,也不想坐因为坐了一天了,闲的蛋疼,也不知道怎么的,脑子里就出了这么个想法😓


我发现了一个奇怪的现象,无论是大学生,还是普通的上班族,或者城市写字楼里的白领,现在很多人,包括年轻人,脚上穿的大部分都是运动鞋,很少有人穿皮鞋了,年轻人好像只有在结婚的当天,才会去穿皮鞋。


我记得,以前穿皮鞋才叫时尚,出门的时候,一个阳光的年轻人,为了给人一个好的形象,都会把皮鞋擦的增明透亮,上面再配上西装、西裤,走起路来,咔咔地响,那才叫一个酷,女孩子穿皮鞋的更多,上身穿什么的都有,脚上穿的基本上都是高跟皮鞋,走起路来,左扭一下,右扭一下,在大街上会很吸人眼球的。


不知从何时起,皮鞋突然失宠了,人们都喜欢穿运动鞋了,商场里售卖的鞋子,大部分也都是运动鞋,基本上看不到皮鞋的影子了。


前几天,我就去了一趟卖鞋的商场,里面销售的鞋子,品牌很多,但几乎全是运动鞋,包括特步、鸿星尔克、李宁、安踏等等,还有一些叫不出名字洋品牌。


我自己也已经有十多年没有穿皮鞋的习惯了,穿的也都是运动鞋,这是为什么呢


作者:threerocks
来源:juejin.cn/post/7337867671096885286
收起阅读 »

前端要懂的Docker部分

前言 最近学习部署的时候,发现前端部署可以通过多种方式来进行部署,web服务器,Docker,静态部署:Github page,网站托管平台:Vercel,还有一些自动化部署的东西目前还没有学到... Docker开始 简介 Docker是一个应用打包,分发,...
继续阅读 »

前言


最近学习部署的时候,发现前端部署可以通过多种方式来进行部署,web服务器,Docker,静态部署:Github page,网站托管平台:Vercel,还有一些自动化部署的东西目前还没有学到...


Docker开始


简介


Docker是一个应用打包,分发,部署的工具,它就相当于一个容器,将你想要的一些依赖、第三方库、环境啥的和代码进行一块打包为镜像,并上传到镜像仓库里面,这样别人可以直接从镜像仓库里面直接拉去这个代码来进行运行,可以适应不同的电脑环境,打造了一个完全封闭的环境。



  • 打包:将软件运行所需要的依赖、第三方库、软件打包到一起,变成一个安装包

  • 分发:将你打包好的安装包上传到一个镜像 仓库,其他人可以很方便的获取安装

  • 部署:拿着安装包就可以一个命令运行起来你的应用,自动模拟出一摸一样的运行环境


为什么使用Docker



Docker 的出现主要是为了解决以下问题:“在我的机器上运行正常,但为什么到你的机器上就运行不正常了?”。



平常开发的时候,项目在本地跑的好好的,但是其他人想要在他的电脑上去跑你的应用程序,但是却跑不起来,他得配置数据库,Web 服务器,插件啥的,非常不方便。Docker的出现解决了这些问题。


基本术语


镜像:理解为软件安装包,可以方便的进行传播和安装,它包含了运行应用程序所需的所有元素,包括代码、运行时环境、库、环境变量和配置文件。



  • 镜像可以通过Dockerfile来进行创建

  • Dockerfile就相当于一个脚本,编写一些命令,他会识别这些命令并且执行


容器:软件安装后的状态,每个软件运行的环境都是独立的、隔离的,称之为容器。


仓库:仓库是存放 Docker 镜像的地方。仓库允许你分享你的镜像,你可以将你的镜像推送(push)到仓库,也可以从仓库拉取(pull)其他人分享的镜像。



  • Docker 提供了一个公共的仓库 Docker Hub,你可以在上面上传你的镜像,或者寻找你需要的镜像。


常见命令



  • docker run: 用于从 Docker 镜像启动一个容器。例如,docker run -p 8080:80 -d my-app 将从名为 "my-app" 的 Docker 镜像启动一个新的容器,并将容器的 8080 端口映射到主机的 80 端口。

  • docker build: 用于从 Dockerfile 构建 Docker 镜像。例如,docker build -t my-app . 将使用当前目录中的 Dockerfile 构建一个名为 "my-app" 的 Docker 镜像。

  • docker pull: 用于从 Docker Hub 或其他 Docker 注册服务器下载 Docker 镜像。例如,docker pull nginx 将从 Docker Hub 下载官方的 Nginx 镜像。

  • docker push: 用于将 Docker 镜像推送到 Docker Hub 或其他 Docker 注册服务器。例如,docker push my-app 将名为 "my-app" 的 Docker 镜像推送到你的 Docker Hub 账户。

  • docker ps: 用于列出正在运行的 Docker 容器。添加 -a 选项(docker ps -a)可以列出所有容器,包括已停止的。

  • docker stop: 用于停止正在运行的 Docker 容器。例如,docker stop my-container 将停止名为 "my-container" 的 Docker 容器。

  • docker rm: 用于删除 Docker 容器。例如,docker rm my-container 将删除名为 "my-container" 的 Docker 容器。

  • docker rmi: 用于删除 Docker 镜像。例如,docker rmi my-app 将删除名为 "my-app" 的 Docker 镜像。

  • docker logs: 用于查看 Docker 容器的日志。例如,docker logs my-container 将显示名为 "my-container" 的 Docker 容器的日志。

  • docker exec: 用于在正在运行的 Docker 容器中执行命令。例如,docker exec -it my-container bash 将在名为 "my-container" 的 Docker 容器中启动一个 bash shell。
    docker常见命令



ps:不会使用就:docker load --help



编写Dockerfile


FROM nginx:latest
# 定义作者
MAINTAINER Merikle

#删除目录下的default.conf文件
#RUN rm /etc/nginx/conf.d/default.conf
#设置时区
RUN cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo 'Asia/Shanghai' >/etc/timezone

#将本地nginx.conf配置覆盖nginx配置
COPY nginx.conf /etc/nginx/nginx.conf
# 将dist文件中的内容复制到 /usr/share/nginx/html/ 这个目录下面
COPY dist/ /usr/share/nginx/html/
#声名端口
EXPOSE 8748

RUN echo 'web project build success!!'

FROM:指定基础镜像


RUN:一般指安装的过程


COPY:拷贝本地文件到镜像的指定目录


ENV:环境变量


EXPOSE:指定容器运行时监听到的端口,是给镜像的使用者看的


ENTRYPOINT:镜像中应用的启动命令,容器运行时调用


编写完Dockerfile生成镜像:


docker build -t test:v1 .


运行镜像:


docker run -p 8080:8080 --name test-hello test:v1


意思:跑在8080端口将test:v1命名为text-hello


之后你可以发布到上面说的仓库里面




  • 命令行登录账号: docker login -u username

  • 新建一个tag,名字必须跟你注册账号一样 docker tag test:v1 username/test:v1

  • 推上去 docker push username/test:v1

  • 部署试下 docker run -dp 8080:8080 username/test:v1


实践一下Docker部署前端项目


核心思想:


将前端打包的dist放到nginx里面,然后添加一个nginx.conf文件。


docker的话就是多了一个Dockerfile文件来构建镜像而已。


nginx.conf文件的编写


#nginx.conf文件编写
#user nobody;
worker_processes 1;

#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;

#pid logs/nginx.pid;


events {
worker_connections 1024;
}


http {
include mime.types;
default_type application/octet-stream;

#log_format main '$remote_addr - $remote_user [$time_local] "$request" '
# '$status $body_bytes_sent "$http_referer" '
# '"$http_user_agent" "$http_x_forwarded_for"';

#access_log logs/access.log main;

sendfile on;
#tcp_nopush on;

#keepalive_timeout 0;
keepalive_timeout 65;

#gzip on;

server {
#监听的端口
listen 8748;
#请填写绑定证书的域名或者IP
server_name 121.199.29.3;

gzip on;
gzip_min_length 1k;
gzip_comp_level 9;
gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png;
gzip_vary on;
gzip_disable "MSIE [1-6]\.";

#charset koi8-r;

#access_log logs/host.access.log main;

location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}

#error_page 404 /404.html;

# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}

# proxy the PHP scripts to Apache listening on 127.0.0.1:80
#
#location ~ \.php$ {
# proxy_pass http://127.0.0.1;
#}

# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
#
#location ~ \.php$ {
# root html;
# fastcgi_pass 127.0.0.1:9000;
# fastcgi_index index.php;
# fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
# include fastcgi_params;
#}

# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
#location ~ /\.ht {
# deny all;
#}
}


# another virtual host using mix of IP-, name-, and port-based configuration
#
#server {
# listen 8000;
# listen somename:8080;
# server_name somename alias another.alias;

# location / {
# root html;
# index index.html index.htm;
# }
#}


# HTTPS server
#
#server {
# listen 443 ssl;
# server_name localhost;

# ssl_certificate cert.pem;
# ssl_certificate_key cert.key;

# ssl_session_cache shared:SSL:1m;
# ssl_session_timeout 5m;

# ssl_ciphers HIGH:!aNULL:!MD5;
# ssl_prefer_server_ciphers on;

# location / {
# root html;
# index index.html index.htm;
# }
#}

}


Dockerfile文件的编写


# Dockerfile文件

FROM nginx:latest
# 定义作者
MAINTAINER Merikle

#删除目录下的default.conf文件
#RUN rm /etc/nginx/conf.d/default.conf
#设置时区
RUN cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo 'Asia/Shanghai' >/etc/timezone

#将本地nginx.conf配置覆盖nginx配置
COPY nginx.conf /etc/nginx/nginx.conf
# 将dist文件中的内容复制到 /usr/share/nginx/html/ 这个目录下面
COPY dist/ /usr/share/nginx/html/
#声名端口
EXPOSE 8748

RUN echo 'web project build success!!'


然后将这些压缩成zip(或者本地打包为镜像,然后上传到镜像仓库里面,在服务器里面拉取镜像就可以了),然后传到服务器中,进行解压,构建镜像并且运行镜像来进行前端部署。


部署node项目


Dockerfile的编写


FROM node:11
MAINTAINER Merikle

#
复制代码
ADD . /mongo-server

#
设置容器启动后的默认运行目录
WORKDIR /mongo-server

#
运行命令,安装依赖
# RUN 命令可以有多个,但是可以用 && 连接多个命令来减少层级。
# 例如 RUN npm install && cd /app && mkdir logs
RUN npm install --registry=https://registry.npm.taobao.org

#
CMD 指令只能一个,是容器启动后执行的命令,算是程序的入口。
# 如果还需要运行其他命令可以用 && 连接,也可以写成一个shell脚本去执行。
# 例如 CMD cd /app && ./start.sh
CMD node app.js

然后构建镜像


发布镜像到hub上面


在服务器上面进行拉取镜像并且运行镜像。


部署mongodb


这个还没有部署,之后再说啦。


作者:Charlotten
来源:juejin.cn/post/7332290436345905187
收起阅读 »

千分位分隔?一个vue指令搞定

web
说在前面 🎈对数字进行千分位分隔应该是大部分同学都做过的功能了吧,常规的做法通常是编写一个工具函数来对数据进行转换,那么我们可不可以通过vue指令来实现这一功能呢? 效果展示 实现原理 非输入框 非输入框我们只需要对其展示进行处理,我们可以判断绑定元素...
继续阅读 »

说在前面



🎈对数字进行千分位分隔应该是大部分同学都做过的功能了吧,常规的做法通常是编写一个工具函数来对数据进行转换,那么我们可不可以通过vue指令来实现这一功能呢?



效果展示




实现原理


非输入框


非输入框我们只需要对其展示进行处理,我们可以判断绑定元素的innerHTML是否不为空,不为空的话则直接对其innerHTML内容进行格式化。


export default {
bind: function (el, binding) {
const separator = binding.value || ",";
if (el.innerHTML) {
el.innerHTML = addThousandSeparator(el.innerText, separator);
}
},
};

输入框



对于输入框,我们希望其有以下功能:



1、输入的时候去掉分隔符


这里我们只需要监听元素的聚焦(focus)事件即可,取到元素的值,将其分隔符去掉后重新赋值。


el.addEventListener("focus", (event) => {
const value = event.target.value;
event.target.value = deleteThousandSeparator(value, separator);
});

2、输入完成后添加分隔符


这里我们只需要监听元素的失焦(blur)事件即可,取到元素的值,对其进行添加分隔符处理后重新赋值。


el.addEventListener("blur", (event) => {
const value = event.target.value;
event.target.value = addThousandSeparator(value, separator);
});

千分位分隔函数


function addThousandSeparator(num, separator = ",") {
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, separator);
}


  • num.toString(): 将输入的数字 num 转换为字符串,以便后续处理。

  • .replace(/\B(?=(\d{3})+(?!\d))/g, separator): 这里使用了正则表达式进行替换操作。具体解释如下:



    • \B: 表示非单词边界,用于匹配不在单词边界处的位置。

    • (?=(\d{3})+(?!\d)): 使用正向预查来匹配每三位数字的位置,但不匹配末尾不足三位的数字。

    • (\d{3})+: 匹配连续的三位数字。

    • separator: 作为参数传入的分隔符,默认为 ,。

    • g: 表示全局匹配,即匹配所有满足条件的位置。




这样,通过正则表达式的替换功能,在数字字符串中的每三位数字之间插入指定的千位分隔符,从而实现千位分隔符的添加。


去掉千分位分隔


function deleteThousandSeparator(numberString, separator = ",") {
return numberString.replace(new RegExp(separator, "g"), "");
}

直接将字符串中的分隔符全部替换为空即可。


完整代码


function addThousandSeparator(num, separator = ",") {
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, separator);
}
function deleteThousandSeparator(numberString, separator = ",") {
return numberString.replace(new RegExp(separator, "g"), "");
}
export default {
bind: function (el, binding) {
const separator = binding.value || ",";
if (el.innerHTML) {
el.innerHTML = addThousandSeparator(el.innerText, separator);
}
el.addEventListener("focus", (event) => {
const value = event.target.value;
event.target.value = deleteThousandSeparator(value, separator);
});
el.addEventListener("blur", (event) => {
const value = event.target.value;
event.target.value = addThousandSeparator(value, separator);
});
},
};


组件库


组件文档


目前该组件也已经收录到我的组件库,组件文档地址如下:
jyeontu.xyz/jvuewheel/#…


组件内容


组件库中还有许多好玩有趣的组件,如:



  • 悬浮按钮

  • 评论组件

  • 词云

  • 瀑布流照片容器

  • 视频动态封面

  • 3D轮播图

  • web桌宠

  • 贡献度面板

  • 拖拽上传

  • 自动补全输入框

  • 图片滑块验证


等等……


组件库源码


组件库已开源到gitee,有兴趣的也可以到这里看看:gitee.com/zheng_yongt…


作者:JYeontu
来源:juejin.cn/post/7337125978802683956
收起阅读 »

SQL中为什么不要使用1=1

最近看几个老项目的SQL条件中使用了1=1,想想自己也曾经这样写过,略有感触,特别拿出来说道说道。 编写SQL语句就像炒菜,每一种调料的使用都会影响菜品的最终味道,每一个SQL条件的加入也会影响查询的执行效率。那么 1=1 存在什么样的问题呢?为什么又会使用呢...
继续阅读 »

最近看几个老项目的SQL条件中使用了1=1,想想自己也曾经这样写过,略有感触,特别拿出来说道说道。


编写SQL语句就像炒菜,每一种调料的使用都会影响菜品的最终味道,每一个SQL条件的加入也会影响查询的执行效率。那么 1=1 存在什么样的问题呢?为什么又会使用呢?


为什么会使用 1=1?


在动态构建SQL查询时,开发者可能会不确定最终需要哪些条件。这时候,他们就会使用“1=1”作为一个始终为真的条件,让接下来的所有条件都可以方便地用“AND”连接起来,就像是搭积木的时候先放一个基座,其他的积木块都可以在这个基座上叠加。


就像下边这样:


SELECT * FROM table WHERE 1=1
<if test="username != null">
AND username = #{username}
</if>
<if test="age > 0">
AND age = #{age}
</if>

这样就不用在增加每个条件之前先判断是否需要添加“AND”。


1=1 带来的问题


性能问题


我们先来了解一下数据库查询优化器的工作原理。查询优化器就像是一个聪明的图书管理员,它知道如何最快地找到你需要的书籍。当你告诉它所需书籍的特征时,它会根据这些信息选择最快的检索路径。比如你要查询作者是“谭浩强”的书籍,它就选择先通过作者索引找到书籍索引,再通过书籍索引找到对应的书籍,而不是费力的把所有的书籍遍历一遍。


但是,如果我们告诉它一些无关紧要的信息,比如“我要一本书,它是一本书”,这并不会帮助管理员更快地找到书,反而可能会让他觉得困惑。一个带有“1=1”的查询可能会让数据库去检查每一条记录是否满足这个始终为真的条件,这就像是图书管理员不得不检查每一本书来确认它们都是书一样,显然是一种浪费。


不过这实际上可能也不会产生问题,因为现代数据库的查询优化器已经非常智能,它们通常能够识别出像 1=1 这样的恒真条件,并在执行查询计划时优化掉它们。在许多情况下,即使查询中包含了1=1,数据库的性能也不会受到太大影响,优化器会在实际执行查询时将其忽略。


代码质量


不过,我们仍然需要避免在查询中包含 1=1,有以下几点考虑:



  1. 代码清晰性:即使数据库可以优化掉这样的条件,但对于阅读SQL代码的人来说,1=1可能会造成困惑。代码的可读性和清晰性非常重要,特别是在团队协作的环境中。

  2. 习惯养成:即使在当前的数据库系统中1=1不会带来性能问题,习惯了写不必要的代码可能会在其他情况下引入实际的性能问题。比如,更复杂的无用条件可能不会那么容易被优化掉。

  3. 优化器的限制:虽然现代优化器很强大,但它们并不是万能的。在某些复杂的查询场景中,即使是简单的 1=1 也可能对优化器的决策造成不必要的影响,比如索引的使用。

  4. 跨数据库兼容性:不同的数据库管理系统(DBMS)可能有不同的优化器能力。一个系统可能轻松优化掉1=1,而另一个系统则可能不那么高效。编写不依赖于特定优化器行为的SQL语句是一个好习惯。


编写尽可能高效、清晰和准确的SQL语句,不仅有助于保持代码的质量,也让代码具有更好的可维护性和可扩展性。


替代 1=1 的更佳做法


现在开发者普遍使用ORM框架来操作数据库了,还在完全手写拼SQL的同学可能需要反思下了,这里给两个不同ORM框架下替代1=1的方法。


假设我们有一个用户信息表 user,并希望根据传入的参数动态地过滤用户。


首先是Mybatis


<!-- MyBatis映射文件片段 -->
<select id="selectUsersByConditions" parameterType="map" resultType="com.example.User">
SELECT * FROM user
<where>
<!-- 使用if标签动态添加条件 -->
<if test="username != null and username != ''">
AND username = #{username}
</if>
<if test="age > 0">
AND age = #{age}
</if>
<!-- 更多条件... -->
</where>
</select>

在 MyBatis 中,避免使用 WHERE 1=1 的典型方法是利用动态SQL标签(如 )来构建条件查询。 标签会自动处理首条条件前的 AND 或 OR。当没有满足条件的 或其他条件标签时, 标签内部的所有内容将被忽略,从而不会生成多余的 AND 或 WHERE 子句。


再看看 Entity Framework 的方法:


var query = context.User.AsQueryable();
if (!string.IsNullOrEmpty(username))
{
query = query.Where(b => b.UserName.Contains(username));
}
if (age>0)
{
query = query.Where(b => b.Age = age);
}
var users = query.ToList();

这是一种函数式编程的写法,最终生成SQL时,框架会决定是否在条件前增加AND,而不需要人为的增加 1=1。


总结


“1=1”在SQL语句中可能看起来无害,但实际上它是一种不良的编程习惯,可能会导致性能下降。就像在做饭时不会无缘无故地多加调料一样,我们在编写SQL语句时也应该避免添加无意义的条件。


每一行代码都应该有它存在的理由,不要让你的数据库像一个困惑的图书管理员,浪费时间在不必要的事情上。


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

uniapp根据不同的环境配置不同的运行基础路径

web
前言当我们使用uniapp开发同一个项目发布不同的环境二级路径不同时,这时候我们就要根据环境来添加运行的基础路径product:xxx-product-api.xxx.com:9002/productConf…text:xxx-text-api.xxx.com...
继续阅读 »

前言

当我们使用uniapp开发同一个项目发布不同的环境二级路径不同时,这时候我们就要根据环境来添加运行的基础路径

product:xxx-product-api.xxx.com:9002/productConf…

text:xxx-text-api.xxx.com:9002/textConfig/

但是当我们使用HBuilderX开发时你会发现manifest.json手动配置Web配置时只能配置一个像这种情况

image.png

碰到这种情况你会怎么处理?

你是不是会在每次打包发布之前变更该环境对应基础路径?

这样也是一种方法,不过其过程太繁琐,废话不多说,上正文!!!

正文

当我们使用HX创建项目时项目中是没有package.json文件和vue.config.js文件的

  1. 在根目录下创建package.json文件,用于配置多个环境也可用于Hx自定义发行
{
"id": "sin-signature",
"name": "签名组件-兼容H5、小程序、APP",
"version": "1.0.0",
"description": "用于uni-app的签名组件,支持H5、小程序、APP,可导出svg矢量图片。",
"keywords": ["签名,签字,svg,canvas"],

"uni-app": {
"scripts": {
"h5-dev": {
"title": "H5-DEV",
"env": {
"NODE_ENV": "development",
"UNI_PLATFORM": "h5",
"VUE_APP_BASE_API": "http://192.168.3.3:8081"
},
"define": {
"H5": true
}
},
"h5-xx": {
"title": "H5-XX",
"env": {
"NODE_ENV": "production",
"UNI_PLATFORM": "h5",
"VUE_APP_BASE_API": "http://xxx.xx.xx.xxx:8092"
},
"define": {
"H5": true
}
},
"h5-test": {
"title": "H5-TEST",
"env": {
"NODE_ENV": "production",
"UNI_PLATFORM": "h5",
"VUE_APP_BASE_API": "https://beta-text-api.nextopen.cn"
},
"define": {
"H5": true
}
},
"h5-prod": {
"title": "H5-PROD",
"env": {
"NODE_ENV": "production",
"UNI_PLATFORM": "h5",
"VUE_APP_BASE_API": "https://product-api.nextopen.cn"
},
"define": {
"H5": true
}
},

}
}
  1. 在根目录下创建vue.config.js文件,用于处理不同环境配置不同的基础路径
const fs = require('fs')
//此处如果是用HBuilderX创建的项目manifest.json文件在项目跟目录,如果是 cli 创建的则在 src 下,这里要注意
//process.env.UNI_INPUT_DIR为项目所在的绝对路径,经测试,相对路径会找不到文件
const manifestPath = process.env.UNI_INPUT_DIR + '/manifest.json'
let Manifest = fs.readFileSync(manifestPath, { encoding: 'utf-8' })
function replaceManifest(path, value) {
const arr = path.split('.')
const len = arr.length
const lastItem = arr[len - 1]

let i = 0
let ManifestArr = Manifest.split(/\n/)

for (let index = 0; index < ManifestArr.length; index++) {
const item = ManifestArr[index]
if (new RegExp(`"${arr[i]}"`).test(item)) ++i;
if (i === len) {
const hasComma = /,/.test(item)
ManifestArr[index] = item.replace(new RegExp(`"${lastItem}"[\\s\\S]*:[\\s\\S]*`), `"${lastItem}": ${value}${hasComma ? ',' : ''}`)
break;
}
}

Manifest = ManifestArr.join('\n')
}

// 动态修改 h5 路由 base
if (process.UNI_SCRIPT_ENV?.NODE_ENV === 'text'){
//测试的 base
replaceManifest('h5.router.base', '"/textConfig/"')
}else if (process.UNI_SCRIPT_ENV?.NODE_ENV === 'product'){
//生产的 base
replaceManifest('h5.router.base', '"/productConfig/"')
}else {
/其他的 base
replaceManifest('h5.router.base', '""')
}

fs.writeFileSync(manifestPath, Manifest, {
"flag": "w"
})

参考uniapp官方文档:uniapp.dcloud.net.cn/collocation…


作者:快乐是Happy
来源:juejin.cn/post/7337208702201086002
收起阅读 »

程序员的副业发展

前言 之前总有小伙伴问我,现在没有工作,或者想在空闲时间做一些程序员兼职,怎么做,做什么,能赚点外快 因为我之前发别的文章的时候有捎带着说过一嘴我做一些副业,这里就说一下我是怎么做的,都做了什么 希望能对你有些帮助~ 正文 学生单 学生单是我接过最多的,已经写...
继续阅读 »

前言


之前总有小伙伴问我,现在没有工作,或者想在空闲时间做一些程序员兼职,怎么做,做什么,能赚点外快


因为我之前发别的文章的时候有捎带着说过一嘴我做一些副业,这里就说一下我是怎么做的,都做了什么


希望能对你有些帮助~


正文


学生单


学生单是我接过最多的,已经写了100多份毕设,上百份大作业了,这里给大家介绍一下


python这种的数据处理的大作业也很多,但是我个人不太会,所以没结过,我只说我做过的


我大致做过几种单子,最多的是学生的单子,分为大作业单子毕设单子


大作业单一般指一个小作业,比如:



  • 几个web界面(大多是html、css、js)

  • 一个全栈的小demo,大多是jsp+SSM或者vue+springboot,之所以不算是毕设是因为,页面的数目不多,数据库表少,而且后端也很简单


我不知道掘金这里能不能说价格,以防万一我就不说大致价格了,大家想参考价格可以去tb或者咸鱼之类的打听就行


然后最多的就是毕设单子,一般就是一个全栈的项目



  • 最多的是vue+springboot的项目,需求量特别大,这里说一下,之前基本都是vue2的项目,现在很多学校要求vue3了,但是大部分商家vue3的模板很少,所以tb上接vue3的项目要么少,要么很贵,所以我觉得能接vue3和springboot项目的可以打一定的价格战,vue2的市面上价格差不多,模板差不多,不好竞争的

  • 少数vue+node的全栈项目,一般是express或者koa,价格和springboot差不多,但是需求量特别少

  • uni+vue+springboot的项目,其实和vue+springboot项目差不多,因为单纯的vue+springboot项目太多了,所以现在很多人要求做个uni手机端,需求量适中

  • .net项目,信管专业的学生用.net的很多,需求量不少,有会的可以考虑一下


这是我接过的比较多的项目,数据库我没有单说,基本上都是MySQL,然后会要求几张表,以及主从表有几对,这就看客户具体要求了



需要注意的点:大部分你得给客户配环境,跑程序,还是就是毕设一般是要求论文的,有论文的会比单纯程序赚的多,但是一定要注意对方是否要求查重,如果要求查重,一般是不建议接的,一般都是要求维普和知网查重,会要了你的老命。还有需要注意的是,学生单子一般是需要答辩的,你可以选择是否包答辩,当然可以调整价格,但是你一旦包答辩,你的微信在答辩期间就不会停了。你永远不知道他们会有怎样的问题



商业单


商业单有大有小,小的跟毕设差不多,大的需要签合同


我接的单子大致就一种,小程序+后台管理+后端,也就是一个大型的全栈项目,要比学生单复杂,而且你还要打包、部署、上线,售后,有一个周期性,时间也比较长


72761aa2847097aa719f2c9728dc560.jpg


image.png


ff5d9aaae6207ab8cbbe847c73cbd36.jpg


9e157d5ddab294d3214fa1d8ece07dc.jpg


为了防止大家不信,稍微放几个聊天记录,是这两个月来找的,也没有给自己打广告,大家都是开发者,开发个小程序有什么打广告,可吹的(真的是被杠怕了)


技术栈有两种情况:自己定客户定


UI也有两种情况:有设计图的无设计图的(也就是自己设计)


基本上也就是两种客户:懂技术的客户,不懂技术的客户


指定技术栈的我就不说了,对于不指定技术栈的我大致分为两种



  • 小程序端:uni/小程序原生、后台:vue、后端:云开发

  • 小程序端:uni/小程序原生、后台:vue、后端:springboot


这取决于预算,预算高的就用springboot、预算不高的就云开发一把嗦,需要说的是并不是说云开发差,其实现在云开发已经满足绝大部分的需求,很完善了,而springboot则是应用广泛,客户后期找别人接手更方便


对于没有UI设计图的,我会选择去各种设计网站去找一些灵感


当项目达到一定金额,会签署合同,预付定金,这是对双方的一种保障


其实在整个项目中比较费劲的是沟通,不是单独说与客户的沟通,更多的是三方沟通,作为上线的程序,需要一些资料手续,这样就需要三方沟通,同时还有一定的周期,可能会被催


讲解单


当然,有的时候人家是有程序的,可能是别人代写的,可能是从开源扒下来的,这个时候客户有程序,但是看不懂,他们可能需要答辩,所以会花钱找人给他们梳理一下,讲一讲, 这种情况比较简单,因为不需要你去写代码,但是需要你能看懂别人的代码


这种情况不在少数,尤其是在小红书这种单子特别多,来钱快,我一般是按照小时收费


cb519bce3fedc451116b659f6cb7388.jpg


e4531c4d8d6527208a03e1dcc6ede32.jpg


aef2baeabe8859caac59fd7ae0b456c.jpg


知识付费这东西很有意思,有时候你回答别人的一些问题,对方也会象征性地给你个几十的红包


接单渠道


我觉得相对于什么单,大家更在意的是怎么接单,很多人都接不到单,这才是最难受的


其实对此我个人并没有太好的建议的方法,我认为最重要的,还是你的交际能力,你在现实中不善于交际,网络上也不善于交际,那就很难了


因为我之前是在学校,在校期间干过一些兼职,所以认识的同学比较多,同时自身能力还可以,所以会有很多人来找,然后做完之后,熟人之间会慢慢介绍,人就越来越多,所以我不太担心能否接单这件事,反而是单太多,自己甚至成立一个小型工作室去接单


如果你是学生的话,一定要在学校积累客户,这样会越来越多,哪怕是现在我还看到学校的各种群天天有毕业很多年以及社会人士来打广告呢,你为什么就不可以呢


当然但是很多人现在已经不是学生了,也不知道怎么接触学生,那么我给大家推荐另外的道路



  • 闲鱼接单

  • 小红书接单


大部分学生找的写手都会比较贵,这种情况下,很多学生都会选择去上面的两个平台去货比三家,那么你的机会就来了


有人说不行啊,这种平台发接单帖子就被删了,那么你就想,为什么那么多人没被删,我也没被删,为什么你被删除了


其次是我最不推荐的一种接单方式:tb写手


为什么不推荐呢,其实就是tb去接单,然后会在tb写手群外包给写手,也就是tb在赚你的差价


这种感觉很难受,而且赚的不多,但是如果你找不到别的渠道,也可以尝试一下


最后


我只是分享一下自己接单的方式,但是说实在的,接一个毕设单或者是商业单其实挺累的,不是说技术层面的,更多的是心累,大家自行体会吧,而且现在商场内卷严重,甚至有人200、300就一个小程序。。。


所以大家要想,走什么渠道,拿什么竞争


另外,像什么猪八戒这种的外包项目的网站,我只是见过,但是没实际用过,接过,所以不好评价


希望大家赚钱顺利,私单是一种赚钱的方式,但是是不稳定的,一定还是要以自己本身的工作为主,自行判断~


作者:Shaka
来源:juejin.cn/post/7297124052174848036
收起阅读 »

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

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

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



免责声明


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



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


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


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


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


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

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

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

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


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



  • 数据隐藏

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

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


数据隐藏


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


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

柯里化函数和偏函数:


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

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

increment(2) // 3

2. 纯函数是什么鬼物?


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



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

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


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



  • 随机数生成器

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

  • 可以改变状态的参数

  • 当前系统时间


副作用包括但不限于:



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

  • 打印到控制台

  • 写入屏幕、文件或网络

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

  • 触发任何外部进程


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


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


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


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

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

const h = compose(f, g)

h(20) // 42

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


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


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



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

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

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


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


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



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

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

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


5. Promise 是什么鬼物?


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


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



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

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

    • 已完成:操作成功完成

    • 拒绝:操作失败



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

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


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

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

6. TS 是什么鬼物?


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


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



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

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

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

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

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


interface User {
id: number
name: string
}

type GetUser = (userId: number) => User

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

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


7. Web Components 是什么鬼物?


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


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


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


8. React Hook 是什么鬼物?


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


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



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

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

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

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

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


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


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


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


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


import React, { useState } from 'react'

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

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

)
}

export default ClickCounter

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


10. TDD 是什么鬼物?


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


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


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



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

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

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

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


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



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

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

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


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


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


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


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


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


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


26-cat.gif


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

程序员必看的几大定律,你中招了吗?

一 晕轮效应 我们通常会从局部信息形成一个完整的印象,根据最少量的情况对别人或其他事物做出全面的结论。 举个简单的例子,当你看到一个陌生美女的时候,你通常会认为对方长得这么好看,笑起来这么甜,肯定哪哪都好! 当你看到一位发量稀疏的开发老哥,你通常会觉得这位...
继续阅读 »

一 晕轮效应



我们通常会从局部信息形成一个完整的印象,根据最少量的情况对别人或其他事物做出全面的结论。



举个简单的例子,当你看到一个陌生美女的时候,你通常会认为对方长得这么好看,笑起来这么甜,肯定哪哪都好!


当你看到一位发量稀疏的开发老哥,你通常会觉得这位老哥技术能力肯定非常强,做人肯定也很靠谱!



在晕轮效应影响下,一个人或事物的优点或缺点一旦变为光圈被扩大,其缺点或优点也就隐退到光圈的背后,被别人视而不见了。



对于程序员来说,有两点可以考虑:



  1. 打造自己的光晕:让自己成为专家或者像个专家,可以提升自己的话语权。


    比如在掘金写文章,当你拥有几千粉丝,几十万阅读量的时候,即使你什么都不说,别人看到这个账号都会觉得你是个厉害人物,与你交流的时候都会注意几分。


  2. 突破别人的光晕:在我使用npm上各种组件或者工具的时候,常常会感慨,好厉害!这么多star的项目,肯定没有Bug吧,如果有,那一定是我用的方式不对!打开源码看的时候更是惊呼,好厉害,完全看不懂,也不可能看的懂吧?


    但其实,褪去这些光环,你会发现,即使是成熟的项目,也会有bug,高深的代码,也都只是一个个基础的语句组合起来的,当你理解了作者的思想,也就能理解代码。



二 眼不见为净定律



看不见的,就是干净的。



看到这个定律,我的第一反应就是,屎山代码为什么会存在?


还不是因为管理人员看不到这坨屎山,他看到的是一个功能正常运行的系统,所以人家并不觉得这是屎山,而是美丽的风景线!


只有我们这些天天在这座屎山上添砖加瓦的程序员才能会感受到这种绝望!


所以面对屎山代码,不要抱怨,最好的方法就是找个机会把这座屎山丢给其他人,毕竟眼不见为净嘛!


当它不在你手上的时候,你会发现其实它也挺好的,毕竟眼不见为净嘛!


三 虚假同感偏差


你们是否会遇到这种情况:明明一件很重要的事情,催了某个人很久了,他却迟迟未做!


这里就涉及到虚假同感偏差,因为这件事对你来说很重要,所以通常会自我推断,觉得别人也会认为这件事情很重要,然而事实上,对你很重要的事,对他人来说可能回过头就给忘记了!


所以啊,要让别人重视一件你觉得很重要的事情,就是也让他感觉到重要,这样别人就不敢忘记了,比如可以补充一句:某某领导正在关注这件事,麻烦尽快,谢谢!


另外就是当我们非常确信自己观点或意见的时候,也很容易产生虚假同感偏差,这时候如果有人提出不同的观点,我们会下意识的反驳,并且觉得问题来自于他人。


比如我们自信满满地写完一段代码并且自测之后,提交给测试人员进行测试,当测试人员跟你反馈存在某BUG,我相信第一时间反应大多都是:我不信!!!


然后就有以下对话:



你:可能前端有缓存,你刷新一下再试试?


测试:行,我试一下。


过了十分钟......


测试:还是一样的问题啊,你看一下。


你:是不是测试数据有问题啊,我自己都测试过了,不应该有问题!


测试:行吧,我再看看。


过了十分钟......


测试:数据都排查过了,是正常的,你检查一下吧!


你:(还想再挣扎一下)你怎么操作的?


测试:就点击一下这个按钮,我还能玩出什么花吗?


排查了一会,哦~居然是空值的情况没有判断,我还能再白痴一点吗!


你:问题已经修复了,是某某复杂的场景我没考虑清楚,你再测试一下!



四 自我宽恕定律



人性有个根深蒂固的特点,就是容易发现别人的缺点和错误,却不容易看到自己的不足。


正所谓,见人之过易,见己之过难。




  • 当看到别人的代码存在一个空指针异常,心里想:这都能忘记判断,其他代码会不会也有问题!

  • 当发现自己的代码存在一个空指针异常,心里想:只是不小心忘记判断了嘛,加一下就好了!

  • 当接手别人项目的时候:卧槽,这代码写的啥啊,注释没几句,变量命名这么奇葩,逻辑这么混乱,我要咋接啊!!

  • 当项目给别人接手时候:我这代码虽然注释不多,但是很规范的,你看这变量命名不就能知道是什么含义了嘛,逻辑也非常顺,这个方法几百行你按顺序看下来就行了,我都给你写在一起,不需要跳来跳去地看,多方便!


五 补偿作用:



弱点也是一种力量源!



大家应该都听说过这个现象:瞎子的眼睛虽然看不见了,听力通常会变得非常灵敏!


这种生理上的现象吸引了很多有兴趣的心理学家,所谓补偿,就是发挥一个人的最大优势,激发其自信心,抵消其弱点。


看到补偿机制,我第一想到的就是在掘金看到的各种专科大佬。


虽然学历起点比其他人低一些,但有时候正是因为学历劣势,更加激发他们深耕技术的决心,反而达到其他更高学历人员都无法达到的高度。


这又让我想起了一句话:打不倒我的,会让我更强大!!


六 皮尔斯定理



意识到无知,才是知道的开始。



还有一句话,我觉得很适合接在这句话后面:知道的越多,才发现自己不懂的越多!


于是就形成了一个闭环: 意识到无知->开始知道->知道的越多->意识到无知


这句话我相信大部分人都听过很多遍了,不知道你们是从什么时候开始意识到自己的无知呢?


曾经,我还是小白的时候,在福州某公司上班,每天做的事情就是SpringBoot接口的开发,或者修改某些业务逻辑,我以为这差不多就是开发的全部了。


那时候对接的前端是使用Vue写的,我甚至不知道什么是Vue,只知道是某个挺流行的前端技术。


每次部署,我看前端文件里就只有一个index.html文件,我真的非常奇怪,为什么这么大的项目,只有一个html文件?


那时候我对前端认知还停留在html+js+css+jquery的时代,所以完全想不通。


本来还觉得自己前端也是有点基础的,直到接触Vue,我才惊呼,卧槽,前端怎么变成这样子了?什么nodejs,什么npm完全没听说过。


用过一段时间之后,我更是惊呼,卧槽,前端还能这样子?明明我HTML+CSS+JS只懂一点,都能做出这么好看的页面了。


有了各种开源前端组件,即使对原生HTML标签和CSS不太懂,也能算是个还不错的前端开发了。


还有这ES6语法用起来也太爽了吧,比JAVA可自由太多了。


所以很感慨,当我没进入前端圈子的时候,还以为自己懂一些,进入之后,才发现自己真的是一窍不通,啥都要学。


更感慨的是,当我第一次接触掘金,我惊呼,卧槽,这个社区分享的东西都好干啊,好多听都没听过的标题,原来我有这么多东西都不懂!原来前端是个这么卷的领域!


结语


感谢阅读,希望本篇文章对你有所帮助!


作者:林劭敏
来源:juejin.cn/post/7295623585363771443
收起阅读 »

对代码封装的一点思考

之前看过一篇文章讲代码封装的过程,下面从简述文章的内容开始梳理下自己对代码封装的一点思考. --- 以下是之前文章内容简述 --- 在业务迭代的初期,代码中有一段业务逻辑A,由于A逻辑在项目中使用比较广泛,于是对A逻辑进行了代码逻辑的封装,包括参数的预设等 ...
继续阅读 »

之前看过一篇文章讲代码封装的过程,下面从简述文章的内容开始梳理下自己对代码封装的一点思考.


--- 以下是之前文章内容简述 ---


在业务迭代的初期,代码中有一段业务逻辑A,由于A逻辑在项目中使用比较广泛,于是对A逻辑进行了代码逻辑的封装,包括参数的预设等

初始化封装

随着业务的发展,又出现了B模块,B模块中几乎可以直接使用A逻辑,但是需要对A逻辑进行一些小小的改动,比如入参的调整、逻辑分支的增加

再次封装

这种通过修改较小范围代码就能实现功能的方式在实际开发中是很常规的操作,它并没有不好,只是当这种修改变的次数多的时候,最开始封装的逻辑A就变成了‘四不像’,它似乎可以服务于A模块,但是确有些冗余,出现了‘我好像看不懂这段代码’,‘这段代码还是别改了’,‘你改吧 我是改不了’的场景。在多次封装之后,逻辑A容易出现以下的问题



  • 入参不可控

  • 逻辑分支复杂,不清晰,修改范围不确定,容易对其他模块产生影响

  • 复用度逐渐下降,解决方法



    1. 将就不再继续新增逻辑,变成'老大难'问题,总有爆发的一天

    2. 重写相关逻辑,功能回测范围大,影响范围不可控,容易出现漏测等




--- 以下是正文 ---


提前封装或者过度封装对实际业务的编写都有很大影响,最实际的感受就是这段代码不好写,这个逻辑看不懂。封装的代码逻辑是结果,封装的出发点是因,从出发点去看而不是面向结果的封装似乎更能编写出可维护性的代码。以下是自己对封装的一点思考。


降低复杂度


复杂度

通过对业务中分散的相似逻辑提取,就能一定程度上降低项目的复杂度和重复度的问题。在修改相应的业务逻辑时候就相对集中和可控。新增逻辑的修改也能做到应用范围的同步。


解耦逻辑


在设计模式中需要设计的模块尽量遵循单一职责,但是在实际编写代码的过程中很容易出现模块功能的耦合,比如常见的:



  • 视图层逻辑与数据层逻辑耦合

  • 非相关逻辑的入侵 比如视图的逻辑需要感知请求状态的改变做相应的逻辑处理


通过将相关的逻辑都聚合到模块封装的内部,就能较低其他模块对不相关逻辑的感知,也就做到了解耦。在下面的例子中,通过将A逻辑的关联逻辑都封装进A模块中,降低了功能模块对封装逻辑的感知。内部封装

相关例子:



  • ahooks中的useRequest中,就通过对请求状态和数据的操作就行封装,将数据相关操作做到了内聚,减轻了视图层的负担

  • Antd pro components的table


分层


分层封装

在例子中,通过将逻辑A中的相关逻辑提取到上层,分层的方式实现了模块功能的内聚与解耦。在前端的MVC中就通过将相关操作封装成Controller,来完成逻辑的解耦


复用(一致性,可控性)


从降低复杂度、逻辑结构、分层设计最终的目标是达到封装代码的复用。通过复用能控制代码的一致性,实现修改代码行为的可控。



-- 写在最后 --


代码的优化或者设计是应该在理解业务的基础上,从相对大的视角出发来看的。在实际做的时候容易出现缺少全局视角的设计缺失和不完善,也容易出现过度设计,需要仔细去把握这个度量。


作者:前端小板凳
来源:juejin.cn/post/7337354931479085096
收起阅读 »

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

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

浅聊一下


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


开始


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


虚拟DOM


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


image.png


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


html>
<html lang="en">

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

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

html>

在这里,template模板实际上是


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

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


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

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


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

diff算法


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


html>
<html lang="en">

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

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

html>

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


image.png


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


image.png


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


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



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

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

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

  4. 同上


image.png


别再写 :key = "index"


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


image.png


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


image.png


可以用随机数吗?


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

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


    结尾


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


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

    分类树,我从2s优化到0.1s

    前言 分类树查询功能,在各个业务系统中可以说随处可见,特别是在电商系统中。 但就是这样一个简单的分类树查询功能,我们却优化了5次。 到底是怎么回事呢? 背景 我们的网站使用了SpringBoot推荐的模板引擎:Thymeleaf,进行动态渲染。 它是一个XM...
    继续阅读 »

    前言


    分类树查询功能,在各个业务系统中可以说随处可见,特别是在电商系统中。



    但就是这样一个简单的分类树查询功能,我们却优化了5次。


    到底是怎么回事呢?


    背景


    我们的网站使用了SpringBoot推荐的模板引擎:Thymeleaf,进行动态渲染。


    它是一个XML/XHTML/HTML5模板引擎,可用于Web与非Web环境中的应用开发。


    它提供了一个用于整合SpringMVC的可选模块,在应用开发中,我们可以使用Thymeleaf来完全代替JSP或其他模板引擎,如Velocity\FreeMarker等。


    前端开发写好Thymeleaf的模板文件,调用后端接口获取数据,进行动态绑定,就能把想要的内容展示给用户。


    由于当时这个是从0-1的新项目,为了开快速开发功能,我们第一版接口,直接从数据库中查询分类数据,组装成分类树,然后返回给前端。


    通过这种方式,简化了数据流程,快速把整个页面功能调通了。


    第1次优化


    我们将该接口部署到dev环境,刚开始没啥问题。


    随着开发人员添加的分类越来越多,很快就暴露出性能瓶颈。


    我们不得不做优化了。


    我们第一个想到的是:加Redis缓存


    流程图如下:

    于是暂时这样优化了一下:



    1. 用户访问接口获取分类树时,先从Redis中查询数据。

    2. 如果Redis中有数据,则直接数据。

    3. 如果Redis中没有数据,则再从数据库中查询数据,拼接成分类树返回。

    4. 将从数据库中查到的分类树的数据,保存到Redis中,设置过期时间5分钟。

    5. 将分类树返回给用户。


    我们在Redis中定义一个了key,value是一个分类树的json格式转换成了字符串,使用简单的key/value形式保存数据。


    经过这样优化之后,dev环境的联调和自测顺利完成了。


    第2次优化


    我们将这个功能部署到st环境了。


    刚开始测试同学没有发现什么问题,但随着后面不断地深入测试,隔一段时间就出现一次首页访问很慢的情况。


    于是,我们马上进行了第2次优化。


    我们决定使用Job定期异步更新分类树到Redis中,在系统上线之前,会先生成一份数据。


    当然为了保险起见,防止Redis在哪条突然挂了,之前分类树同步写入Redis的逻辑还是保留。


    于是,流程图改成了这样:

    增加了一个job每隔5分钟执行一次,从数据库中查询分类数据,封装成分类树,更新到Redis缓存中。


    其他的流程保持不变。


    此外,Redis的过期时间之前设置的5分钟,现在要改成永久。


    通过这次优化之后,st环境就没有再出现过分类树查询的性能问题了。


    第3次优化


    测试了一段时间之后,整个网站的功能快要上线了。


    为了保险起见,我们需要对网站首页做一次压力测试。


    果然测出问题了,网站首页最大的qps是100多,最后发现是每次都从Redis获取分类树导致的网站首页的性能瓶颈。


    我们需要做第3次优化。


    该怎么优化呢?


    答:加内存缓存。


    如果加了内存缓存,就需要考虑数据一致性问题。


    内存缓存是保存在服务器节点上的,不同的服务器节点更新的频率可能有点差异,这样可能会导致数据的不一致性。


    但分类本身是更新频率比较低的数据,对于用户来说不太敏感,即使在短时间内,用户看到的分类树有些差异,也不会对用户造成太大的影响。


    因此,分类树这种业务场景,是可以使用内存缓存的。


    于是,我们使用了Spring推荐的caffine作为内存缓存。


    改造后的流程图如下:



    1. 用户访问接口时改成先从本地缓存分类数查询数据。

    2. 如果本地缓存有,则直接返回。

    3. 如果本地缓存没有,则从Redis中查询数据。

    4. 如果Redis中有数据,则将数据更新到本地缓存中,然后返回数据。

    5. 如果Redis中也没有数据(说明Redis挂了),则从数据库中查询数据,更新到Redis中(万一Redis恢复了呢),然后更新到本地缓存中,返回返回数据。



    需要注意的是,需要改本地缓存设置一个过期时间,这里设置的5分钟,不然的话,没办法获取新的数据。



    这样优化之后,再次做网站首页的压力测试,qps提升到了500多,满足上线要求。
    最近我建了新的技术交流群,打算将它打造成高质量的活跃群,欢迎小伙伴们加入。


    我以往的技术群里技术氛围非常不错,大佬很多。


    image.png


    加微信:su_san_java,备注:加群,即可加入该群。


    第4次优化


    之后,这个功能顺利上线了。


    使用了很长一段时间没有出现问题。


    两年后的某一天,有用户反馈说,网站首页有点慢。


    我们排查了一下原因发现,分类树的数据太多了,一次性返回了上万个分类。


    原来在系统上线的这两年多的时间内,运营同学在系统后台增加了很多分类。


    我们需要做第4次优化。


    这时要如何优化呢?


    限制分类树的数量?


    答:也不太现实,目前这个业务场景就是有这么多分类,不能让用户选择不到他想要的分类吧?


    这时我们想到最快的办法是开启nginxGZip功能。


    让数据在传输之前,先压缩一下,然后进行传输,在用户浏览器中,自动解压,将真实的分类树数据展示给用户。


    之前调用接口返回的分类树有1MB的大小,优化之后,接口返回的分类树的大小是100Kb,一下子缩小了10倍。


    这样简单的优化之后,性能提升了一些。


    第5次优化


    经过上面优化之后,用户很长一段时间都没有反馈性能问题。


    但有一天公司同事在排查Redis中大key的时候,揪出了分类树。之前的分类树使用key/value的结构保存数据的。


    我们不得不做第5次优化。


    为了优化在Redis中存储数据的大小,我们首先需要对数据进行瘦身。


    只保存需要用到的字段。


    例如:


    @AllArgsConstructor
    @Data
    public class Category {

    private Long id;
    private String name;
    private Long parentId;
    private Date inDate;
    private Long inUserId;
    private String inUserName;
    private List children;
    }

    像这个分类对象中inDate、inUserId和inUserName字段是可以不用保存的。


    修改自动名称。


    例如:


    @AllArgsConstructor
    @Data
    public class Category {
    /**
    * 分类编号
    */

    @JsonProperty("i")
    private Long id;

    /**
    * 分类层级
    */

    @JsonProperty("l")
    private Integer level;

    /**
    * 分类名称
    */

    @JsonProperty("n")
    private String name;

    /**
    * 父分类编号
    */

    @JsonProperty("p")
    private Long parentId;

    /**
    * 子分类列表
    */

    @JsonProperty("c")
    private List children;
    }

    由于在一万多条数据中,每条数据的字段名称是固定的,他们的重复率太高了。


    由此,可以在json序列化时,改成一个简短的名称,以便于返回更少的数据大小。


    这还不够,需要对存储的数据做压缩。


    之前在Redis中保存的key/value,其中的value是json格式的字符串。


    其实RedisTemplate支持,value保存byte数组


    先将json字符串数据用GZip工具类压缩成byte数组,然后保存到Redis中。


    再获取数据时,将byte数组转换成json字符串,然后再转换成分类树。


    这样优化之后,保存到Redis中的分类树的数据大小,一下子减少了10倍,Redis的大key问题被解决了。




    作者:苏三说技术
    来源:juejin.cn/post/7233012756315963452
    收起阅读 »

    宏观经济的一些判断

    估计谁都没想到2023年,疫情恢复的第一年竟然如此:大厂裁员、楼市继续下跌、年底股市暴跌等。 这篇文章说下笔者对宏观经济的一些理解和判断,以试图解释过去一年发生的一些事情。 宏观经济周期 笔者看来,由于美元具备全球货币属性,美元加息是全球的麻烦。所以总体而言,...
    继续阅读 »

    估计谁都没想到2023年,疫情恢复的第一年竟然如此:大厂裁员、楼市继续下跌、年底股市暴跌等。


    这篇文章说下笔者对宏观经济的一些理解和判断,以试图解释过去一年发生的一些事情。


    宏观经济周期


    笔者看来,由于美元具备全球货币属性,美元加息是全球的麻烦。所以总体而言,全球经济处于美元加息影响下的结果中,也就是处于美元回流,全球货币紧缩,全球经济处于下行周期中


    但每个主要经济体又有其自身的宏观经济周期,比如说我们国家,疫情三年为了人民的生命健康,我们国家没有像西方打开方便之门,大肆放水,而是选择严守死防。


    三年疫情,我们国家保护了人民的生命健康,但我们也封闭了三年,紧缩了三年。我们国家处于疫情恢复周期中,处于上行周期中。但始作俑者的美国因为超发货币,自身经济过热,所以需要降低过热经济,所以宏观经济也处于下行周期中。


    这里说一个题外话,宏观经济是通缩好还是通货膨胀好呢?实际上,过分的通缩和过分的通货膨胀都不好,适度的通胀最好,一般来说维持在2%左右是比较好的,这也就是美国一直想通过加息实现的通胀水平。


    不管是通缩还是通胀代表的都是一种趋势,通缩经济逐渐下行,规模逐渐减小,反之通胀经济处于上行,规模逐渐扩大,这样才能实现GDP的增长,实现经济的发展。


    回来继续说宏观经济周期,当下国家要做的就是让经济处于扩张区间,或者说实现经济的适度通胀。


    2023年CPI和PPI一直不温不火,其实有一点通缩的迹象,所以国家在出台各种手段改善这种迹象:



    1. 对楼市逐渐降低首付比例,逐渐放开楼市,逐渐降息,逐渐降低存款准备金等

    2. 对股市降低印花税,转融通暂停,加大逆回购力度,证监会新管理层上任等


    但改变趋势谈何容易,同时还要考虑美国的加息节奏。所以出台的手段并不是那么尽如人意,也因为受到美国加息节奏的影响,为了维持外汇的稳定性,出台的时机也并未尽如人意。


    笔者觉得正因为上述原因:美元加息周期、国内降息周期导致2023大厂裁员、楼市继续下跌、年底股市暴跌。


    2024年宏观经济


    目前美国已经暂停加息半年,加息周期已经到顶,预期5、6月份后降息。也正因为暂停加息,加息到顶我国央行今天进一步降低5年期贷款利率。


    2024年的国内的宏观经济依然是将经济恢复为适度通胀的水平。笔者的判断是在汇率稳定的前提下,稳定楼市,因为楼市涉及的面太大。稳定了楼市才能稳定经济稳定预期,才能稳定股市。


    完全有可能的是央行在美国降息后选择再一次降息贷款利息的方式,加强人们的信心,加强人们对国家进一步搞活经济的决心,给人们信心。


    稳定楼市而不是大力发展楼市。笔者认为未来国家的发展会逐渐向高科技产业倾斜,从依靠楼市带动GDP增长改为依靠科技带动。


    综上,一些配套的改革一定也会有:比如真正支持优秀公司发展的证券市场,比如政府职能转变为服务好企业,比如更加开放的区域市场(现在的浦东新区)等等。


    而笔者比较关心的股票市场,隶属于证券市场,笔者觉得未来中国的股市一定会越来越好,尤其优秀的科技公司。


    (本文完)


    作者:通往自由之路
    来源:juejin.cn/post/7337227407365963803
    收起阅读 »

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

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

    前言


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


    什么是BASE64


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


    BASE64的优缺点


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


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



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



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


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


    案例


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



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



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


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

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


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


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


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


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



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


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


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

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


    结束语


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


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

    听说你会架构设计?来,弄一个红包系统

    大家好,我是小❤,一个漂泊江湖多年的 985 非科班程序员,曾混迹于国企、互联网大厂和创业公司的后台开发攻城狮。 1. 引言 当我那天拿着手机,正在和朋友们的微信群里畅聊着八卦新闻和即将到来的周末计划时,忽然一条带着喜意的消息扑面而来,消息正中间赫然写着八个大...
    继续阅读 »

    大家好,我是小❤,一个漂泊江湖多年的 985 非科班程序员,曾混迹于国企、互联网大厂和创业公司的后台开发攻城狮。


    1. 引言


    当我那天拿着手机,正在和朋友们的微信群里畅聊着八卦新闻和即将到来的周末计划时,忽然一条带着喜意的消息扑面而来,消息正中间赫然写着八个大字:恭喜发财,大吉大利



    抢红包!!相信大部分人对此都不陌生,自 2015 年春节以来,微信就新增了各类型抢红包功能,吸引了数以亿万级的用户参与体验,今天,我们就来聊一聊这个奇妙有趣的红包系统。


    2. 概要设计


    2.1 系统特点



    抢红包系统从功能拆分,可以分为包红包、发红包、抢红包和拆红包 4 个功能。


    对于系统特性来说,抢红包系统和秒杀系统类似。



    每次发红包都是一次商品秒杀流程,包括商品准备,商品上架,查库存、减库存,以及秒杀开始,最终的用户转账就是红包到账的过程。


    2.2 难点


    相比秒杀活动,微信发红包系统的用户量更大,设计更加复杂,需要重视的点更多,主要包括以下几点。


    1、高并发


    海量并发请求,秒杀只有一次活动,但红包可能同一时刻有几十万个秒杀活动。


    比如 2017 鸡年除夕,微信红包抢红包用户数高达 3.42 亿,收发峰值 76 万/秒,发红包 37.77 亿 个。


    2、安全性要求


    红包业务涉及资金交易,所以一定不能出现超卖、少卖的情况。



    • 超卖:发了 10 块钱,结果抢到了 11 块钱,多的钱只能系统补上,如此为爱发电应用估计早就下架了;

    • 少卖:发了 10 块钱,只抢了 9 块,多的钱得原封不动地退还用户,否则第二天就接到法院传单了。


    3、严格事务


    参与用户越多,并发 DB 请求越大,数据越容易出现事务问题,所以系统得做好事务一致性


    这也是一般秒杀活动的难点所在,而且抢红包系统涉及金钱交易,所以事务级别要求更高,不能出现脏数据


    3. 概要设计


    3.1 功能说明


    抢红包功能允许用户在群聊中发送任意个数和金额的红包,群成员可以抢到随机金额的红包,但要保证每个用户的红包金额不小于 0.01 元



    抢红包的详细交互流程如下:



    1. 用户接收到抢红包通知,点击通知打开群聊页面;

    2. 用户点击抢红包,后台服务验证用户资格,确保用户尚未领取过此红包;

    3. 若用户资格验证通过,后台服务分配红包金额并存储领取记录;

    4. 用户在微信群中看到领取金额,红包状态更新为“已领取”;

    5. 异步调用支付接口,将红包金额更新到钱包里。


    3.2 数据库设计


    红包表 redpack 的字段如下:



    • id: 主键,红包ID

    • userId: 发红包的用户ID

    • totalAmount: 总金额

    • surplusAmount: 剩余金额

    • total: 红包总数

    • surplusTotal: 剩余红包总数


    该表用来记录用户发了多少红包,以及需要维护的剩余金额。


    红包记录表 redpack_record 如下:



    • id: 主键,记录ID

    • redpackId: 红包ID,外键

    • userId: 用户ID

    • amount: 抢到的金额


    记录表用来存放用户具体抢到的红包信息,也是红包表的副表。


    3.3 发红包



    1. 用户设置红包的总金额和个数后,在红包表中增加一条数据,开始发红包;

    2. 为了保证实时性和抢红包的效率,在 Redis 中增加一条记录,存储红包 ID 和总人数 n``;

    3. 抢红包消息推送给所有群成员。


    3.4 抢红包


    从 2015 年起,微信红包的抢红包和拆红包就分离了,用户点击抢红包后需要进行两次操作。


    这也是为什么明明有时候抢到了红包,点开后却发现该红包已经被领取完了



    抢红包的交互步骤如下:



    1. 抢红包:抢操作在 Redis 缓存层完成,通过原子递减的操作来更新红包个数,个数递减为 0 后就说明抢光了。

    2. 拆红包:拆红包时,首先会实时计算金额,一般是通过二倍均值法实现(即 0.01 到剩余平均值的 2 倍之间)。

    3. 红包记录:用户获取红包金额后,通过数据库的事务操作累加已经领取的个数和金额,并更新红包表和记录表。

    4. 转账:为了提升效率,最终的转账为异步操作,这也是为什么在春节期间,红包领取后不能立即在余额中看到的原因。


    上述流程,在一般的秒杀活动中随处可见,但是,红包系统真的有这么简单吗?


    当用户量过大时,高并发下的事务一致性怎么保证,数据分流如何处理,红包的数额分配又是怎么做的,接下来我们一一探讨。


    4. 详细设计


    由于是秒杀类设计,以及 money 分发,所以我们重点关注抢红包时的高并发解决方案和红包分配算法。


    4.1 高并发解决方案


    首先,抢红包系统的用户量很大,如果几千万甚至亿万用户同时在线发抢红包,请求直接打到数据库,必然会导致后端服务过载甚至崩溃。


    而在这种业务量下,简单地对数据库进行扩容不仅会让成本消耗剧增,另一方面由于存在磁盘的性能瓶颈,所以大概率解决不了问题。


    所以,我们将解决方案集中在 减轻系统压力、提升响应速度 上,接下来会从缓存、加锁、异步分治等方案来探讨可行性。


    1、缓存


    和大多数秒杀系统设计相似,由于抢红包时并发很高,如果直接操作 DB 里的数据表,可能触发 DB 锁的逻辑,导致响应不及时。



    所以,我们可以在 DB 落盘之前加一层缓存,先限制住流量,再处理红包订单的数据更新。


    这样做的优点是用缓存操作替代了磁盘操作,提升了并发性能,这在一般的小型秒杀活动中非常有效!


    但是,随着微信使用发&抢红包的用户量增多,系统压力增大,各种连锁反应产生后,数据一致性的问题逐渐暴露出来:



    • 假设库存减少的内存操作成功,但是 DB 持久化失败了,会出现红包少发的问题;

    • 如果库存操作失败,DB 持久化成功,又可能会出现红包超发的问题。


    而且在几十万的并发下,直接对业务加锁也是不现实的,即便是乐观锁。


    2、加锁


    在关系型 DB 里,有两种并发控制方法:分为乐观锁(又叫乐观并发控制,Optimistic Concurrency Control,缩写 “OCC”)和悲观锁(又叫悲观并发,Pessimistic Concurrency Control,缩写“PCC”)。



    悲观锁在操作数据时比较悲观,认为别的事务可能会同时修改数据,所以每次操作数据时会先把数据锁住,直到操作完成


    乐观锁正好相反,这种策略主打一个“信任”的思想,认为事务之间的数据竞争很小,所以在操作数据时不会加锁,直到所有操作都完成到提交时才去检查是否有事务更新(通常是通过版本号来判断),如果没有则提交,否则进行回滚。


    在高并发场景下,由于数据操作的请求很多,所以乐观锁的吞吐量更大一些。但是从业务来看,可能会带来一些额外的问题:



    1. 抢红包时大量用户涌入,但只有一个可以成功,其它的都会失败并给用户报错,导致用户体验极差;

    2. 抢红包时,如果第一时间有很多用户涌入,都失败回滚了。过一段时间并发减小后,反而让手慢的用户抢到了红包

    3. 大量无效的更新请求和事务回滚,可能给 DB 造成额外的压力,拖慢处理性能。


    总的来说,乐观锁适用于数据竞争小,冲突较少的业务场景,而悲观锁也不适用于高并发场景的数据更新。


    因此对于抢红包系统来说,加锁是非常不适合的。


    3、异步分治


    综上所述,抢红包时不仅要解决高并发问题、还得保障并发的顺序性,所以我们考虑从队列的角度来设计。


    我们知道,每次包红包、发红包、抢红包时,也有先后依赖关系,因此我们可以将红包 ID 作为一个唯一 Key,将发一次红包看作一个单独的 set,各个 set 相互独立处理



    这样,我们就把海量的抢红包系统分成一个个的小型秒杀系统,在调度处理中,通过对红包 ID 哈希取模,将一个个请求打到多台服务器上解耦处理。


    然后,为了保证每个用户抢红包的先后顺序,我们把一个红包相关的操作串行起来,放到一个队列里面,依次消费。


    从上述 set 分流我们可以看出,一台服务器可能会同时处理多个红包的操作,所以,为了保证消费者处理 DB 不被高并发打崩,我们还需要在消费队列时用缓存来限制并发消费数量


    抢红包业务消费时由于不存储数据,只是用缓存来控制并发。所以我们可以选用大数据量下性能更好的 Memcached。


    除此之外,在数据存储上,我们可以用红包 ID 进行哈希分表,用时间维度对 DB 进行冷热分离,以此来提升单 set 的处理性能。


    综上所述,抢红包系统在解决高并发问题上采用了 set 分治、串行化队列、双维度分库分表 等方案,使得单组 DB 的并发性能得到了有效提升,在应对数亿级用户请求时取得了良好的效果。


    4.2 红包分配算法


    抢红包后,我们需要进行拆红包,接下来我们讨论一下红包系统的红包分配算法。


    红包金额分配时,由于是随机分配,所以有两种实现方案:实时拆分和预先生成。


    1、实时拆分


    实时拆分,指的是在抢红包时实时计算每个红包的金额,以实现红包的拆分过程。


    这个对系统性能和拆分算法要求较高,例如拆分过程要一直保证后续待拆分红包的金额不能为空,不容易做到拆分的红包金额服从正态分布规律。


    2、预先生成


    预先生成,指的是在红包开抢之前已经完成了红包的金额拆分,抢红包时只是依次取出拆分好的红包金额。


    这种方式对拆分算法要求较低,可以拆分出随机性很好的红包金额,但通常需要结合队列使用。


    3、二倍均值法


    综合上述优缺点考虑,以及微信群聊中的人数不多(目前最高 500 人),所以我们采用实时拆分的方式,用二倍均值法来生成随机红包,只满足随机即可,不需要正态分布。



    故可能出现很大的红包差额,但这更刺激不是吗🐶



    使用二倍均值法生成的随机数,每次随机金额会在 0.01 ~ 剩余平均值*2 之间。


    假设当前红包剩余金额为 10 元,剩余个数为 5,10/5 = 2,则当前用户可以抢到的红包金额为:0.01 ~ 4 元之间。


    4、算法优化


    用二倍均值法生成的随机红包虽然接近平均值,但是在实际场景下:微信红包金额的随机性和领取的顺序有关系,尤其是金额不高的情况下


    于是,小❤耗费 “巨资” 在微信群发了多个红包,得出了这样一个结论:如果发出的 红包总额 = 红包数*0.01 + 0.01,比如:发了 4 个红包,总额为 0.05,则最后一个人领取的红包金额一定是 0.02



    无一例外:



    所以,红包金额算法大概率不是随机分配,而是在派发红包之前已经做了处理。比如在红包金额生成前,先生成一个不存在的红包,这个红包的总额为 0.01 * 红包总数


    而在红包金额分配的时候,会对每个红包的随机值基础上加上 0.01,以此来保证每个红包的最小值不为 0。


    所以,假设用户发了总额为 0.04 的个数为 3 的红包时,需要先提取 3*0.01 到 "第四个" 不存在的红包里面,于是第一个人抢到的红包随机值是 0 ~ (0.04-3*0.01)/3


    由于担心红包超额,所以除数的商是向下取二位小数,0 ~ (0.04-3*0.01)/3 ==> (0 ~ 0) = 0,再加上之前提取的保底值 0.01,于是前两个抢到的红包金额都是 0.01。最后一个红包的金额为红包余额,即 0.02


    算法逻辑用 Go 语言实现如下:


        import (
       "fmt"
       "math"
       "math/rand"
       "strconv"
    )

    type RedPack struct {
        SurplusAmount float64 // 剩余金额
         SurplusTotal int // 红包剩余个数
    }

    // 取两位小数
    func remainTwoDecimal(num float64) float64 {
        numStr := strconv.FormatFloat(num, 'f'264)
        num, _ = strconv.ParseFloat(numStr, 64)
        return num
    }

    // 获取随机金额的红包
    func getRandomRedPack(rp *RedPack) float64 {
        if rp.SurplusTotal <= 0 {
            // 该红包已经被抢完了
            return 0
        }

        if rp.SurplusTotal == 1 {
            return remainTwoDecimal(rp.SurplusAmount + 0.01)
        }

           // 向下取整
        avgAmount := math.Floor(100*(rp.SurplusAmount/float64(rp.SurplusTotal))) / float64(100)
        avgAmount = remainTwoDecimal(avgAmount)

           // 生成随机数种子
        rand.NewSource(time.Now().UnixNano())

        var max float64
        if avgAmount > 0 {
            max = 2*avgAmount - 0.01
        } else {
            max = 0
        }
        money := remainTwoDecimal(rand.Float64()*(max) + 0.01)

        rp.SurplusTotal -= 1
        rp.SurplusAmount = remainTwoDecimal(rp.SurplusAmount + 0.01 - money)

        return money
    }

    // 实现主函数
    func main() {
        rp := &RedPack{
            SurplusAmount: 0.06,
            SurplusTotal:  5,
        }
        rp.SurplusAmount -= 0.01 * float64(rp.SurplusTotal)
        total := rp.SurplusTotal
        for i := 0; i < total; i++ {
            fmt.Println(getRandomRedPack(rp))
        }
    }

    打印结果:



    0.01、0.01、0.01、0.01、0.02



    喜大普奔,符合预期!


    5. 总结


    设计一个红包系统不仅要考虑海量用户的并发体验和数据一致性,还得保障用户资金的安全


    这种技术难点,对于传统的 “秒杀系统” 有过之而无不及。


    本文主要探讨了高并发场景下的设计方案和红包分配的算法,覆盖了目前红包系统常见的几大难题。




    作者:xin猿意码
    来源:juejin.cn/post/7312352501406908452
    收起阅读 »

    代码要同时推送到 gitee 和 github 该怎么办?教你两招!

    前言 我们作为特色社会主义社会中的一份子,跟国外程序员不一样的地方在于,别人能轻松访问 github, 我们却要借梯子,或者用国产的 gitee。 作为有追求的程序员,作为成年人,当然是两个我都要! 那么如何优雅地把本地代码同时维护到 gitee 和 gith...
    继续阅读 »

    前言


    我们作为特色社会主义社会中的一份子,跟国外程序员不一样的地方在于,别人能轻松访问 github, 我们却要借梯子,或者用国产的 gitee


    作为有追求的程序员,作为成年人,当然是两个我都要!


    那么如何优雅地把本地代码同时维护到 giteegithub上呢?这里带给大家2个方法。


    push 2个 remote original


    假如我们在 github 创建了一个仓库,那么本地 clone 下来后,主分支(main)是默认跟github 仓库的主分支(main) 关联的。这样直接在 VSCode 里面点 同步的圈圈 就会自动同步,如下图。


    image.png


    现在我又要推到 gitee,怎么办,很简单,新增一个 remote 源,并命名为 gitee,默认的 origin 已经跟 github 关联了。


    git remote add gitee 

    这样我们就有2个源,在 VSCodeGit Graph 中可以直接 push 到2个源中,2个都勾选上。


    image.png


    点击 Yes, push 就可以推上去了。爽歪歪


    gitee 仓库镜像管理:gitee -> github


    本来上面的方法用得好好的,但是我的梯子质量不好,时不时推不上 github ( gitee 倒是轻轻松松,没有失败过),然后我又得重新 push,有的时候要重试好多次才行,非常浪费时间!!


    然后我就发现 gitee 原来是可以同步推到 github 的,根本就不用我们手动操作,点赞!


    image.png


    官方链接在这里 仓库镜像管理(Gitee<->Github 双向同步),我就不做搬运工了,给几个图:


    image.png


    image.png


    其中还需要到 github 获取 token,方法如下:


    image.png


    官方链接在这里 如何申请 GitHub 私人令牌?


    我设置成功如下图:


    image.png



    token生成的时候要设置过期时间,尽量设置长一点比如一年。



    image.png


    推到 gitee 的代码会自动同步到 github, 我们也可以手动点右侧的 更新 按钮,手动同步。


    经我检测,是成功的,刚刚推不上去 github 的代码,通过 gitee 同步过去了,本地也能看到代码是同步的。


    image.png



    有一个需要注意的是,你只能推到你自己的github仓库,不能推到你的github组织的仓库。



    以我的仓库为例:



    因为选推送的目标仓库时压根就不能选组织,只能选个人!但是源仓库可以是个人的或者组织的。


    总结


    本文介绍了2种同步 gitee 和 github 仓库的方法,视情况选择:



    • 当目标仓库是个人时,第二种会比较方便,推上到 gitee 后,会自动同步到 github

    • 当目标仓库是组织时,不能用第二种,只能用第一种,自己手动 push 到2个仓库,需要你的梯子质量好,不然可能推不上 github


    最后把我每天都看的美女分享给大家~养眼啊


    pretty-girl.png


    作者:菲鸽
    来源:juejin.cn/post/7327353620232339506
    收起阅读 »

    总结一下程序员的一些经历

    前言 大学毕业已经6年了,不知不觉已经到了30岁,不免会对中年危机产生焦虑心理。平常上掘金浏览文章,除了一些技术相关的,还有倔友工作经历相关的,以求得一丝共鸣。最近有感而发,仔细想想自己也没有写过总结的文章,所以想总结一下自己的经历,同时有什么想法大家也可以交...
    继续阅读 »

    前言


    大学毕业已经6年了,不知不觉已经到了30岁,不免会对中年危机产生焦虑心理。平常上掘金浏览文章,除了一些技术相关的,还有倔友工作经历相关的,以求得一丝共鸣。最近有感而发,仔细想想自己也没有写过总结的文章,所以想总结一下自己的经历,同时有什么想法大家也可以交流一下。


    大学


    我大学学的计算机专业,不知道大家的专业是自己深思熟虑后报的还是随便报的,反正我是随便报的。当时填志愿的时候家里人希望我去学医,其实我是有点不太想,但是我又不知道该报什么专业。由于我个人的性格是比较内向的,不太喜欢和人打交道,所以希望以后的工作也如此。在排除了一些专业后,发现计算机专业是和电脑打交道,然后就报了,后来家里人也同意了,最后在杭州上的。


    大学期间大一学的课程学的是基础课和java,c#等基础语言,大二学的是计算机组成和网络相关。期间我也没怎么规划未来方向,目标是拿了学分就行。


    到了大三,专业细分,我选择了软件工程,其他的数字媒体和计算机科学感觉不太适合我。大一大二的课程安排还算紧凑,大三课程少了很多,平均下来,一天两门课,也就是4课时。学校管的比较宽,那会儿有空闲的时间就在寝室里玩游戏。到了期末的时候,就临时抱佛脚,正所谓“一花一世界,一叶一菩提,一天一本书,一周一学期”,所以现在感觉那个时候还是挺没有目标的。


    不知道大家有没有这样的感受:大三的课程要求大多是软件设计,老师教的都是理论知识,但是实际需要完成一个项目。比方说,上课的时候教的是“类”,“设计模式”,到了期末就要做一个课程管理系统。那个时候感觉知识体系都挺零散的,也没有UI框架(或者是组件)的概念,甚至java里的swing也要学,基本使用的是原生的,也没有什么思路,一个页面也不知道怎么去做。但是最终为了完成期末作业,我不得不上网一步一步跟着做,没有加入自己的想法
    ,就这样搬运完成了。


    实习


    到了大四,基本没有课程,要求就是实习。我的技术总结起来就仅仅是学过,想着能有公司收留就行,投了几家,面了两家,都失败了。那会儿是G20,留在杭州还挺麻烦的,需要学校报备,然后还要和学校签免责协议。回老家又没有什么软件公司,正在为实习这个事情发愁的时候,好在有一个亲戚在老家有开公司,所以我就趁着这个机会被介绍进去了。


    介绍的时候亲戚说建议我去学习他的本行业,也就是制造行业;意思是他在软件这块投了挺多钱,不对软件抱太大期望,希望我转其他的。我考虑到我的实习是需要和开发相关的,所以想要毕业,必须做开发,最终我被安排在一个软件子公司下面。第二天报到之后,我的上级领导就了解我的背景,有没有学过数据库之类的,我就说有。还有刚来上几天班的两个小伙伴,他们是社招的。然后我们几个就一起学习公司的相关框架。


    公司实际上是二次开发,之前是花钱买的开发平台。页面也比较简单,就是一些单据。所以我们的工作内容也比较简单,就是拖拖控件,写写增删改查事件。简单的表单熟练了之后一下午可以搞三四个。很长时间都是比较清闲。当然一方面也是因为我是实习,优先安排学校的事,然而我也是写写实习总结。中间也碰到了一些困难,确实业务上复杂的情况也有,需要写存储过程;那时我以为做不出来是能力不行,想一个人硬着抗下所有。最后在一个小时没有解决用户反馈的问题之后,然而我的上级领导态度挺好的,说这个业务挺复杂,他来亲自上手。


    就这样过了一个月,等到了发工资的时候,人家都发了,我才发现我没有谈论这个问题,后来上级领导跟总经理反馈了,总经理说实习是没有工资的。这里说一下,我那个亲戚是总裁,总经理是他的侄子,执行权在总经理,况且我跟他也不认识。我想着实习就算了吧,毕业后再说。


    过了三个月,不知道是不是我上级领导提的,竟然和我签合同,说是总经理破例给我发工资。等到发工资那天,发了2K的实习工资,我还是挺开心的,毕竟是我自己的劳动所得。由于这几个月吃住都是家里的,我选择给了家里,然而家里给我留了一半当生活费。这几个月实习期间,有学到一些知识和经验,到了后期基本就是重复的工作,没有了更多的技术提升。


    毕业


    然后到了毕业前的上半年,由于要做毕业设计,需要和导师经常沟通,所以呆在老家不太方便,所以我得换公司实习。其实这个时候我留在学校做毕业设计就可以了,但是看到周围的童鞋都在公司上班,我也不想掉队。然后我又在手机招聘上投了几家公司,但又是石沉大海。后来突然收到杭州的两家面试,我很奇怪竟然不是我投的那几家,估计招聘软件可以看到,后面面了之后过了。然后我就跟领导说我要回学校了,明天就要走,当时我也不太清楚交接的概念,感觉做的内容也挺简单的。没想到领导竟然也同意了,然后我一天内交接完,之后就回学校了。


    毕业来了杭州的这家公司(简称公司A),一待就是4年半,你们可能会问为什么第一家公司可以待这么久,包括我和同事都这么说,其实也是无奈。公司规模还挺大,用的是.NET,之前写的是C#客户端,所以很多东西需要重新学。在这家公司实习,也没有安排太多活,主要是学习一些技术,写几个简单的页面。同时也搞一下学校的毕业设计。等到2017年6月份答辩完,我就正式签了合同。同时我也离开了学校,得知大学同学公司在同一个区,就和他们一起租了一个160平的大套房,几个人平均下来人均1300一月。那时公司A开的工资不高,只有几k,然后每个月都在精打细算,看看能存下多少钱。


    公司A不是外包公司,但是我所在的部门是做项目的,主要为甲方做定制,有时候采集数据和开发方便需要驻现场。没过多久,公司便派我出去出差。出差所做的内容基本上是数据综合展示页面,没有涉及核心业务,只需要从底层获取数据展示就好了。出差还有个好处就是有补贴,每日固定,由于甲方不提供住宿,我们就2人一组一起住酒店,有时还会跟老板说常住进行讨价还价。算下来,一个月的出差补贴能和工资差不多。所以就算每个月经常加班,也都是可以接受。这个项目上干完2个月可能又到其他项目上去干,然后根据项目时长又考虑是租房还是住酒店,如此往复。那时感觉和同学比起来,赚得还算可以。


    不知不觉这样子已经过了两年,公司A涨的工资并不多,同学跳槽获得了更高的工资,换了公司去了其他区,我们一起租的房子也退了,各自重新去租了单间。一方面那时我一个月的工资算上出差补贴,也不算高了,也就是正常水平,另一方面长期到处出差没有归宿感,长期下去找不到对象也不是办法,此时我已经萌生了跳槽的念头。但是回过头来发现,这两年一直是在用.NET做一些展示的界面,说技术没有用到很深的技术,说业务也不是懂业务,这才发现出去面试的时候也不是很有竞争力。看看市场行情java比C#好点,我把这两年的项目包装成java,但面了几家都杳无音信。最后有一家外包发了offer,我这次的目的有两个,一是不出差,2是可以学到技术,显然外包不满足这两个条件,所以我放弃了。考虑到一个月存下的钱还能接受,我就妥协了,继续干着,这次在平常工作之余,学习java相关的技术。


    迷茫


    然而到了第三年,公司组织架构变动,部门缩减预算,我负责维护产品,有什么问题远程支持。也就是意味着出差的频次减少了,必要的情况下有时出去一个礼拜,赚得出差补贴也少了。这样算下来,相比两年工作经验的人,我赚得是少的,而且进来的新人都比我们老人多,同事问起来,说真的,我都不好意思跟他讲。这个时候我想跳槽的心思再一次涌起,但是快到了年底,也没有什么机会,打算过年后再看看。不过令人惊喜的是,这次年终奖给了不少,估计是考虑到我们工资比较低,工资不涨,年终奖来凑,也算是补了一点内心的不平衡。


    原先部门的人员还一直挺稳定,到了20年中,老人也走了几个,新人进来了几个,这时我已经算是比较老了,平常同事问起来,意思说第一家公司待了3年有点久,平常一年或两年就跳槽了,我也是表面上呵呵一下,内心也想着逃离。然后我开始看起了行情,发现行情又不好了,加上我java没有项目实践,只是学了点基础的技术,业务也不精通。是的,我放弃了,我再一次陷入迷茫,难道我就只能一直这样子待下去了吗。


    没过多久,公司组织架构再一次变动,部门业务也有调整,我们的技术从.NET过渡到java,这一次对于我来说是一件好事,意味着我可以用java做项目了。而且这次公司的业务是做新的产品,不再维护老的项目,做开发的都知道,一直维护老的项目学不到太多的技术,所以这对于我来说是一种解脱。在大公司的好处是,可以及时跟进技术,然后我就接触了微服务和docker相关的技术,k8s是其他同事在研究。


    不知不觉又干了两年,这两年算是学到了一些技术,工资目前还偏低,这个时候我也思考过,我以后的方向,是在大厂当凤尾,还是去小公司当鸡头。加上之前家里催着相亲,前前后后见了不少个都没有结果。有些是因为在老家,异地不太方便,也就算了。加上家里的条件不太好,这几年也没存到什么钱,在杭州买房有很大的压力。然后我就考虑回老家,老家的软件行情也不太好,毕竟是二线城市(现在已经是三线了),机会不是很多。刚好碰到校友群里有老乡在银行工作,发了招聘,然后我联系他,获得了面试机会。在这次面试中,我自认为技术上还是比较自信的,然而银行的流程比较复杂,有5轮面试,由总行的和分行的。然而又失败了,这次失败是因为用词表达不够准确,被分行的领导们否决了。但我不知道是不是有其他因素考虑在内,比如外貌、言行举止,需要体现银行人的面貌,不过都不重要了。


    回家


    到了2022年初,在某聘发现老家有公司(公司B)在招架构师,我就去试试看。业务刚起步,我心想如果能够能有发展的话,以后在技术部门还是有话语权的。公司B工资给的一般,但考虑到可以住在家里,生活成本会少很多,有什么事情家里沟通也很方便。于是我就进了这家公司。说真的公司A待了4年半,还有些不舍。公司还是原来的公司,但是部门调整,人员流动、调整、拆分,部门是否还是原来那个部门。


    在公司B的经历只能用两个字概括:闲着。情况是公司原先有老系统的经验,现在BOSS他们在进行业务架构,我们就做一些技术架构的学习,在我看来前期还会是项目级别的开发,不会一下子上升到K8S集群管理。一来是真正成熟的产品还需从项目开始落地,二来是管理成本会很大,人员有限。


    公司实习生来了几个,又走了几个。到现在持续了快两年,如果不是工资能够正常发并且人员还在招,我都怀疑公司是不是在正常运作。好在也是在回家的这两年,经过家里人介绍,我完成了另一件终身大事。


    总结


    不知不觉写了很多,有些细节还没写出来。这几年铺天盖地的技术,我们是学了又学,卷了又卷。当软件技术不能够有效提高生产力的时候,意味着软件开发的红利已经过去。想想以前,如果我毕业时考研读了人工智能的专业,现在会不会好很多;如果大学的时候努努力,进互联网的大厂,现在又会不会不一样。但是我不后悔,我把原因归结于没有规划,大学如此,工作的时候也是如此,等行情不好的时候或者是中年危机出现的时候,再去准备就已经迟了。一切的焦虑来源于没有充足的准备。


    要为最坏的打算做好准备,如果公司不行了,难道还回杭州去背井离乡吗。所以未来我打算发展一下副业,不需要依赖于公司也可以自己生计。


    作者:哥伦比亚拆迁队
    来源:juejin.cn/post/7337112282575323146
    收起阅读 »

    研发误删的库,凭什么要 DBA 承担责任

    镇楼图 三个角色 删库以及更宽泛的数据库变更场景中有三个角色,业务研发,DBA 以及使用的数据库变更工具: 业务研发通常指的是后端研发。国内最主流的技术栈还是 Java,此外 Go 也有一部分,另有全栈的则使用 Node。这些语言通常会配备对应的 ORM ...
    继续阅读 »

    镇楼图


    file


    三个角色


    删库以及更宽泛的数据库变更场景中有三个角色,业务研发,DBA 以及使用的数据库变更工具:



    • 业务研发通常指的是后端研发。国内最主流的技术栈还是 Java,此外 Go 也有一部分,另有全栈的则使用 Node。这些语言通常会配备对应的 ORM 和数据库打交道,Java 的 MyBatis,Go 的 GORM,Node 的 TypeORM 等。

    • DBA 就是数据库管理员。有些公司即使没有全职 DBA,也会有看着数据库的那个人。

    • 数据库变更工具。公司业务稍微上了规模,一般会选择在专门的数据库变更工具上执行操作,开源的产品里比较主流的有 Archery, Yearning, Bytebase。


    生命周期


    交代完出场角色,我们再来说一下,数据库变更的整个生命周期:



    • 研发在数据库变更工具上提交了一个变更工单。

    • 工具可能进行一些自动化检测,修改字段会提示锁表,删库,删表会警告破坏应用代码的兼容性。

    • DBA 进行审核。

    • 审核通过后,进行发布。

    • 告警铺天盖地/客诉蜂拥而来,业务一排查,怀疑可能是之前的数据库变更引起的,拉上 DBA,实锤。于是再一起制定补救方案。

    • 经过几天的奋战,最终修复了问题。

    • 开始进行复盘。


    上一期回顾的 Linear 删库故障就是这样一套流程。到了复盘阶段,国外普遍采用 blameless postmortem,对事不对人,但国内通常会给故障定主责和次责。安全生产的角度来说,定责任人的威慑力肯定更强。但对于数据库变更,往往一出就是大故障,承担主责往往意味着全年绩效最低档。所以每当业务研发和 DBA 还有他们的主管走进会议室,都会很认真地想着怎样一起复盘。


    场景模拟


    我们先来模拟一个场景:


    业务研发想在 MySQL 8.0 上把一个 VARCHAR 列的长度从 20 改成 100:


    ALTER TABLE person MODIFY name VARCHAR (100); 

    研发在数据库变更工具上提交了这个语句,因为 DBA 也读了 MySQL 8.0 官方文档,知道 MySQL 8.0 修改列不会锁表导致不可用,所以审批通过,然后 DBA 执行这条语句。业务居然挂了!原来 MySQL 8.0 里当把 VARCHAR 长度修改为超过 64 时,还是会锁表的!


    切回复盘室里认真讨论的气氛。业务团队主张 DBA 主责,业务研发次责,因为



    • 工具没有自动检测出这个风险,进行提示。

    • DBA 作为专业人员在审核阶段没有发现锁表问题。

    • 最后是由 DBA 去执行了变更操作。

    • 业务研发提交了导致问题的 SQL,但要求业务研发了解这个 MySQL 的执行细节有点强人所难。


    DBA 团队主张业务团队主责,DBA 次责,因为:



    • 业务研发应该为自己的业务负责,包括数据库在内。

    • 问题 SQL 是由业务发起的。业务研发并没有提示业务风险,比如业务的重要性,业务的高峰期等。

    • DBA 没有识别到这个问题,督查失职。


    好了,到了这里两边其实都有各自的道理,小伙伴们可以停下来思考一下,更倾向于如何定责。下面说一下我的观点。


    我认为这个情况应该是**「业务团队主责,DBA 次责」**。


    针对业务团队的几个主张:



    工具没有自动检测出这个风险,进行提示。



    那好,既然 DBA 引入带自动检测的工具反而留下把柄,那大家还是提交在文档上吧,完全没有自动化检测。



    DBA 作为专业人员在审核阶段没有发现锁表问题。



    双方这点都有共识,但光这条属于失查,次责。



    最后是由 DBA 去执行了变更操作。



    是 DBA 执行还是研发执行,这是流程的设计。谁去点执行按钮都改变不了这次故障的发生。



    业务研发提交了导致问题的 SQL,但要求业务研发了解这个 MySQL 的执行细节有点强人所难。



    没有错,但我们来换一个角度。研发写代码调用了一个 API,让 API 的维护者审核一下。结果 API 维护者也没有审核出来一个 bug,上线后导致故障。研发确实可以说让他了解 API 的实现细节有点难,但是研发为了完成自己的任务,使用了一个自己不熟悉的 API,那是不是应该由自己承担风险?


    但在实际 PK 中由 DBA 承担主责的情况时有发生,往往是因为第 3 点,最后是由 DBA 点了发布按钮。但就像上面说的,我认为这点是站不住脚的。流程设计让 DBA 点那下,是因为由 DBA 来点效率是更高的,但如果因为 DBA 点了而要背锅,那 DBA 就会拒绝这个流程,甩给研发自己去发布,这样流程只会更加低效。这里也要澄清一下,也不是 DBA 点就一定是更高效的流程,但只想表达定责应该和谁最后点发布那一下无关。


    场景泛化


    我们再进一步把这个场景泛化一下。在垂直业务团队和横向平台团队的协作中,因为业务开发造成的故障,通常应该都由业务团队承担主责,否则的话,就会造成权责不对等,把平台团队推向消极合作的情况。


    这个就像老师带着小朋友和家长们集体活动,照看小朋友的第一责任人始终是家长,如果要让老师承担主责,大概率就不会组织活动了。当然这也有例外,比如假设老师罔顾家长的嘱托,携带小朋友参加不适合的活动,造成意外,那就是老师的责任了。


    回到模拟的场景,业务研发应该做的,是给 DBA 更多的业务风险提示,比如说哪些高峰时段不能做变更。如果研发说了这些,但是 DBA 依然置若罔闻,发布造成事故,那 DBA 也自然难辞其咎。


    故障定责是一个激烈的话题,关乎到大家的奖金,加薪,升职。本文尝试给出一个判定的原则,也欢迎大家留言探讨。回忆起那些年参加过的复盘会议,就像抄起刚一同肝完的啤酒瓶,朝对方劈头盖脸扔了过去。我心中不禁泛起丝丝寒意,想到了鲁迅的呐喊:


    「 从来如此,便对么?」


    作者:Bytebase
    来源:juejin.cn/post/7332346676165574707
    收起阅读 »

    对于软件开发者来说什么证书含金量最高

    1. 引言 在快速变化的软件行业,持续学习和证书认证成为了许多开发者职业发展的关键。不仅因为这些证书能够提升个人技能,而且在求职过程中,这些证书也是向雇主展示专业技能和承诺的重要方式。 2. 市场上主流的软件开发相关证书 2.1 Oracle Certifie...
    继续阅读 »

    1. 引言


    在快速变化的软件行业,持续学习和证书认证成为了许多开发者职业发展的关键。不仅因为这些证书能够提升个人技能,而且在求职过程中,这些证书也是向雇主展示专业技能和承诺的重要方式。


    2. 市场上主流的软件开发相关证书


    2.1 Oracle Certified Professional, Java SE Programmer (OCPJP)



    • 相关技能:Java 编程、面向对象的设计原理、Java SE API。

    • 为何重要:Java 是世界上最受欢迎的编程语言之一,Oracle 的 Java 认证能够显著提升一个开发者在Java领域的专业性。


    ocpjp-8-certificate.jpg


    2.2 Microsoft Certified: Azure Solutions Architect Expert



    • 相关技能:云计算、Azure服务设计和管理、安全和合规性。

    • 为何重要:随着云计算的普及,熟练掌握微软Azure平台的专家需求量大增。


    2.3 AWS Certified Solutions Architect – Associate



    • 相关技能:云计算、AWS服务设计和管理、网络技术。

    • 为何重要:AWS 是市场上最大的云服务提供商,此认证证明了在AWS平台上设计高可用、成本效益高的系统的能力。


    2.4 Certified ScrumMaster (CSM)



    • 相关技能:敏捷和Scrum实践、团队合作、项目管理。

    • 为何重要:Scrum 是最流行的敏捷开发方法之一,CSM证书证明了在使用Scrum方法管理项目方面的专业知识。


    1625704928523.png


    2.5 PMP (Project Management Professional)



    • 相关技能:项目管理、领导和团队管理、风险管理。

    • 为何重要:对于希望在项目管理方面发展的软件开发人员,PMP是一个国际认可的高级证书。


    3. 选择正确的证书



    • 自我评估:首先,理解自己的职业目标和兴趣所在是非常重要的。例如,如果我们对云计算充满热情,那么AWS或Azure相关的证书可能更适合我们,在国内阿里云和腾讯云的认证也可以。

    • 行业需求:研究市场需求和趋势,选择对应的证书。例如,随着云计算和数据科学的兴起,相关的证书需求量也在增长。

    • 长期价值:考虑证书对我们长期职业发展的帮助。一些证书可能需要持续的教育和更新。


    4. Certified ScrumMaster(CSM)推荐


    在当今快速变化的软件开发领域,敏捷方法论已成为推动项目成功的关键因素。作为最受欢迎的敏捷实践之一,Scrum 方法论在全球范围内得到了广泛应用。Certified ScrumMaster(CSM)认证对软件开发者和项目管理者来说是一项宝贵的资格认证。


    CSM-Banner.png


    4.1. CSM 认证概述


    Certified ScrumMaster(CSM)是由 Scrum Alliance 提供的一项认证,旨在确认个人在理解和实践 Scrum 方法论方面的能力。CSM 认证不仅涉及 Scrum 的基本原则和实践,还包括团队合作、项目管理和敏捷思维。


    4.2. CSM 认证的重要性



    • 提升职业素养:CSM 认证可以显著提高个人在敏捷项目管理领域的专业知识和技能。

    • 增强市场竞争力:随着敏捷方法的普及,CSM 认证成为了求职者在简历中的一个亮点,增加了他们在市场上的竞争力。

    • 促进团队合作:CSM 认证的持有者能更有效地领导和促进敏捷团队的合作与沟通。

    • 优化项目管理:CSM 认证有助于改进项目的交付周期,提高项目成功率。


    4.3. 获取 CSM 认证的流程



    • 参加培训:首先需要参加为期两天的认证课程,由 Scrum Alliance 认证的培训师(CST)授课。

    • 通过考试:完成课程后,参加在线考试,考试内容涵盖 Scrum 的基本原则、框架和实践。

    • 维持认证:CSM 认证需要每两年续期一次,通过积累 Scrum 教育单位(SEUs)和缴纳续费来实现。


    4.4. 为何选择 CSM 认证



    • 适用范围广:无论是软件开发者、项目经理还是希望提高项目管理能力的专业人士,CSM 认证都是一个极佳的选择。

    • 实践导向:CSM 认证不仅仅是理论知识的学习,更重要的是它强调实践和实际应用。

    • 全球认可:CSM 是一项国际认可的认证,对于追求国际职业发展的专业人士来说具有重要意义。


    Certified ScrumMaster 认证是一项对软件开发者和项目管理者极具价值的资格认证。它不仅提供了敏捷方法论的深入理解,还有助于在职业生涯中获得更多的机会和挑战。无论我们是刚刚开始职业生涯,还是希望在当前领域有更深入的发展,CSM 认证都是一个值得考虑的优秀选择。


    5. 总结


    选择正确的证书对于软件开发者的职业发展至关重要。在选择时,应当考虑个人的职业规划、市场需求以及证书的长期价值。同时,持续学习和保持技能的最新性是每个软件开发者成功的关键。


    作者:王义杰
    来源:juejin.cn/post/7329573732597776410
    收起阅读 »

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

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

    写在开头


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


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


    14DBA96E.gif


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


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


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


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


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


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


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


    14F03F73.jpg

    第一种(推荐)


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


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

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

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

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


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


    image.png


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


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


    我😤:嗯嗯。


    第二种


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


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

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


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



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


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


    它原理是通过了 CORS


    image.png


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










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


    image.png


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

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


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

    年终总结:工作三年,满腔热血已然消散

    毕业到现在也已经有三年多的工作经验了,最开始坚信努力就会有结果,到被社会毒打后开始迷茫,一直在思考生活和工作之间的关系和意义。目前是彻底的技术躺平,如今供需关系变化了,单纯的提升技术也不见得能找到一份好的工作,所以有点学不动技术了,感觉学习技术的价值不如以前大...
    继续阅读 »

    毕业到现在也已经有三年多的工作经验了,最开始坚信努力就会有结果,到被社会毒打后开始迷茫,一直在思考生活和工作之间的关系和意义。目前是彻底的技术躺平,如今供需关系变化了,单纯的提升技术也不见得能找到一份好的工作,所以有点学不动技术了,感觉学习技术的价值不如以前大了,还不如学点软技能,或者看看书拓宽视野也好。好在人没有躺平,一直在尝试寻找一个破局的契机。既然改变不了环境,那就先改变自己吧。



    说说以前


    第一份工作


    刚毕业的第一份工作工资很低而且996,花了很多钱去报名学习编程技术的课程,当时觉得只有技术上去了薪资才会随之上涨,果不其然,最开始先投资自己才能更快提高身价的方式很对,来年跳槽工资是第一份工作的2.X倍吧,一方面当时环境还没这么糟糕,另一方面HR问期望薪资的时候自己也比较敢开价,主要是当时学了不少技术课程,面试的时候也是一顿Vue源码输出,所以当时底气比较足敢报价,现在回想起来确实有点狮子大开口的感觉[笑哭]。


    第二份工作


    第二份工作虽然比第一份工作的薪资高了很多,不过好景不长,待遇上去了,伴随而来的是更多的挑战和更大的压力,我倒是不抵触压力和挑战,适当的压力会让人更快的成长,我抵触的是那家公司的“坐牢”文化,明面上写着5点半下班,暗地里大搞工时考核,工作日至少需要待够11.5个小时,工时不够的就会被谈话了,就算没啥事也得待在公司刷工时,后面11.5都没法满足了。


    休养


    人到底能有多坏?一直以为大家同是打工人,矛盾是一致对外的,直到碰到了上家公司领导无底线的压榨、转正临近人事威胁劝退的恶心事之后,才体验到真正的险恶和残酷。没了工作后,开始怀疑自我,变得迷茫,每天都要出去走走,不让自己闷在屋子里,或许是阅历不足、亦或是承受能力不够吧,能熬过去的都会有成长。


    休息了近半年才重新开始找工作,这半年时间基本上是边做个低代码开源,边外出散心的状态,见见老友,吃饭叙旧,也尝试了很多新奇的体验。


    新工作入职前去了趟南通,在大学室友那住几天,算起来我俩都是无业游民,他住的附近有一个很大的批发市场,海鲜还算便宜种类也多,每天都让他骑着小电驴载着我去,我俩连续吃了很多顿海鲜,吃饱后骑着小电驴去看山山水水、逛逛公园,晚上回来一起开黑。对比一线城市的物价,感觉那的东西还挺实惠,可能我们是工作日去逛的,车少人也少,没有大城市的拥挤和喧嚣,也没有匆忙的旅人,突然间感觉生活节奏变得很慢,原来很多事情都可以慢慢来。


    不过小城市的工作收入确实很低很多,没有收入来源的话,这种生活不会持续太久,活在当下尽情享受当前的每分每秒。


    回到现在


    现在这份工作


    当时休息完开始找工作的时候,知道环境不好,心里已经有一个预期了,不过这难找的程度还是超过了我的预期,找的时间越长人越焦虑,虽然焦虑并没有什么正向作用,反而让人睡不着吃不好,但很难控制得住不去焦虑,当时想着随便有个外包都去了,好在最终结果是好的,拿到了唯一一份offer,也就是现在这份工作,没有太多的考虑的余地就买机票去了。


    这份工作虽然降薪了,但是双休不加班,工作上也没什么太大的压力,下班后晚上就去跑步,很喜欢跑完大汗淋漓的感觉。


    不过最近公司好像是收益不太行了,少了很多人,收益不行的时候就开始抓考勤、算考核绩效,几个人的活压到一个人身上,见怪不怪了,一般公司开始走下坡路的时候就出现这种操作。总感觉码农这份工作抗风险能力还是很差的,要资源没资源,要人脉没人脉,多数劳动合同上还限制了不能干副业,风险来临前要早做打算才是。


    谈谈未来


    2023年完成的目标


    1、看完5本书(趁着地铁通勤时看的,对扩宽视野很有帮助)


    2、坚持跑步(每个工作周至少去跑一次)


    3、打卡背单词(一直想提升英语能力,不过感觉每天只是花点时间背单词的话,还是提升太慢了)


    4、附近城市旅游(珠海跨年、清远漂流、桂林竹筏、江门海岛沙滩等,基本都是周末去的,玩得也很开心,节假日的时候人多不想去挤)


    5、交朋友(总感觉这一年交到的朋友太少了,想玩个桌游都凑不够人)


    2024年我想要做什么


    1、雅思,目标定在5.5以上吧(我的英语一直很差很差,以前没怎么学,考试基本靠蒙,想着年后报个班提升一下效率,在英语上欠下的债总归要还的)


    2、看书(闲的时候多看几本书)


    3、旅游(旅游是一个享受世界的过程,我喜欢慢慢悠悠的,总想着没工作的时候去一趟新疆或者内蒙,估摸着能住一个月吧,就想静静的在一望无际的草原上躺这么几天)


    4、出去外面看看(护-照最近也办下来了,想体验更多的风土人情,见识更宽阔的世界)


    5、运动(养生和跑步不能停,目标累计到一千公里以上吧,23年已累计到八百公里了,剩下两百公里应该洒洒水)


    6、交朋友(交到更多的朋友,偶尔想去玩一些东西,总是没有合适的朋友一起)


    寻破局之路


    什么局


    总觉得程序员这个职业稳定性差,没什么安全感,要么是三十五岁槛,要么是身体扛不住,不乏有些大咖支招转管理岗,可现实就是管理坑位就这么些,绝多大数人是坐不上的,再者拼命去卷竞争管理岗,到时身体垮了,ICU住一住,抵不住可能会人财两空。到时候年龄大了失业了,找不到工作,即没人脉和资源,也不会别的专业技能,这种可预见性的危机可以提前做些准备,避免走到这一步。


    如何破


    感觉自己总是走不出眼前的一亩三分地,不停的在这小圈子里兜兜转转,世界规则的条条例例已经把人框在一个范围里了,人生应该是旷野而不是轨道,如果自己再给自己设限,那么这个框将更小。很多时候出于对未知的恐惧,没勇气踏出第一步。我给自己找了两个破土而生的方向:1、换个赛道;2、换个环境,若当下环境找不到破局的希望,那就朝外面看看,试着换个环境,让变数更大。


    两个方向的结果未定,好在变数够大,不至于一眼就看到头。


    作者:Daw
    来源:juejin.cn/post/7330521390510866495
    收起阅读 »

    热爱前端,也没能逃过七年之痒

    大家好,我是杨成功。 从参加工作到今年十月底,我做前端已经整整七年了。都说婚姻有七年之痒,我觉得工作也同样如此。所谓工作的“七年之痒”,即职业倦怠期。我觉得我倒没有倦怠,但感受不一样了。 以前我一直想前端可以干到退休,这是我的理想职业。现在我虽然还是一名前端工...
    继续阅读 »

    大家好,我是杨成功。


    从参加工作到今年十月底,我做前端已经整整七年了。都说婚姻有七年之痒,我觉得工作也同样如此。所谓工作的“七年之痒”,即职业倦怠期。我觉得我倒没有倦怠,但感受不一样了。


    以前我一直想前端可以干到退休,这是我的理想职业。现在我虽然还是一名前端工程师,但是工作内容已经离前端越来越远了。


    以前我觉得做一个骨灰级程序员、掌握各种牛逼的技术是毕生目标;现在我会想人生精彩多样,多尝试一些不一样的事情不也同样有趣?


    1-3 年:热爱、探索


    我参加工作很早,二十出头。那时候啥也不懂,但是精力旺盛啥也想学,经常写代码到凌晨 2 点也不觉得累。有一部分人选择前端是因为简单,我就是纯粹的喜欢前端。


    前端中有很多好玩的东西,比如各种动画、特效,我都非常感兴趣。在工作中常常因为研究出一种“高级”的写法、实现了某个“牛逼”的功能而沾沾自喜。虽然现在看起来很小儿科,但想起来真让人怀念。


    我的第一份工作工资很低(<3k),应该比 95% 的前端都低。当时没有经验,心里想着只要能学到东西就成。在那家公司干了一年多,公司用到的技术基本都学了一遍,进步飞快。“又穷又爱”的状态估计以后再也不会有了。


    3-5 年:积累、挑战


    工作三年多的时候,我换了家公司,带一个前端小团队,每天都扎在项目里。以前总是追求新技术,怎么花哨怎么来。可负责项目后才发现,解决问题和快速产出才是第一位。


    当时的前端非常火热,全社会都是跳槽的机会,跳槽等于涨薪。于是面试变得千奇百怪,大家在卷各种原理、源码、八股文,不管面不面试刷题成了必修课。很多开发者们非常讨厌这些东西,但是又不得不去做。


    当然也有好处,就是各种新技术层出不穷。虽然很多都是轮子,但确实有不少突破性的技术,帮助传统前端接触到更广的技术面,能做更多的事情。


    我没有花大量时间刷面试题,新技术倒是跟了不少,而且很多都用在了项目中。像 JS 原理题、算法题、某些框架的源码之类,我基本没怎么看过;但是像 Node.js、Android、Linux、跨端开发这些,我花了很多的时间研究,因为确实可以解决项目中的问题。


    我一直认为我属于“外卷”类型的:Title 一直是前端,但从不认为自己只是一个前端。什么技术都想试试。所以后来我承担过很多攻坚的角色,像服务器、原生 App、音视频等。我发现能让我上头的可能并不是前端,而是搞定一个难题的快感。


    得益于这种心态吧,五年内我积累了很多,但我认为收获最大的是习惯了面对挑战。


    5-7 年:瓶颈、迷茫


    工作五年以上,年龄直逼 30 岁,好像一瞬间就老了,可我总觉得自己还是个孩子。这个时候总会问自己:我的工作有什么意义?我要一直这样下去吗?我想要什么样的生活?


    我是在第 6 年的时候感受到了瓶颈。技术方面一直在进步,但对项目的帮助越来越小———项目进入了稳定期。稳定期意味着没有了涨薪的机会,工作重点逐渐从“怎么实现”变成了“怎么汇报”。以前写日报是“汇总成果”,现在变成了“显得有事可做”。


    可能任何一家产品成熟的公司都是这样吧,我不习惯,我还在适应阶段。


    从今年开始,我最大的迷茫是工作与生活如何平衡。我在北京这几年,大部分精力都扑在了工作上,家人离的很远,每年见个一两次,也没把谈女朋友当回事。想和家人朋友在一块,可工作又不能放弃。成年人说自己不做选择全都要,而我好像只能二选一。


    以前一门心思地想靠技术跳槽、进大厂,今年突然觉得没意思。看到很多人被裁员、加班、互卷,我突然想也许现在挺好的呢?双休不加班、领导也 Nice、没有绩效考核、办公室关系也简单。是不是以前自己太浮躁了,没有好好享受当下呢?


    所以,要不要继续写代码?还是回老家做别的事?工作上要不要再卷一点?努力攒钱还是趁年轻消费?要不要参加相亲考虑结婚?一连串的问题汹涌而来。


    有些问题能想明白,有些问题还是不明白,但更多的是想明白了也做不到。人的成长流失最快的是勇气,可能某天一件意料之外的事情,会让你一下子做出决定。


    写了一本书


    工作五年之后,我常常会思考一个问题:如果有一天不做程序员了,我还能干什么?


    程序员大概都不喜欢社交吧,或者不擅长社交。我特别羡慕大圣老师,他可以把自己的知识通过视频很生动的表达出来。但我就不行,我好像对镜头恐惧,尝试过好多次全身的不自在。


    录视频有难度,不过写文章还行。正好积累了很多知识经验,一边总结一边练笔,于是开始写掘金。后来又碰到个机会写书,我就觉得这个更好,可以把这么多年的经验总结浓缩到一本书里。或许可以帮助一些前端朋友快速进阶,或许还能赚点稿费。


    这本书名叫 《前端开发实战派》


    之后怎么走


    七年之前觉得我会写代码到 70 岁,直到写不动了为止。七年之后,我最喜欢的工作依然是程序员,但我不再执着于能不能干到 35 岁了。世界还有很多不一样的精彩,我不能把自己困在程序里。


    与那些大厂大佬们相比,我赚的不多,心气也不高。没有想过一定要留在大城市,也不觉得以后有了小孩,就一定要奔着“好的教育”和“名校”去卷,太累了。其实只要没有大城市和名校的执念,生活压力也不会那么大。


    这样来看,如果有一天我被裁了,其实也没什么可担心的。选择一个离家近的地方,没有大都市的物欲和诱惑,过一些简单轻松的生活,或许并不糟糕。只是身在大城市,面对万千繁华仿佛难以自拔,但你心里好像知道这不是你追求的,却又停不下来。


    我有一个预感,可能 30 岁后不再做程序员了,至少不会只埋头钻研技术。做前端这几年让我在各方面成长迅速,不过做久了也有弊端,比如表达能力、社交能力退化,不擅长处理人际关系,不直接接触商业,而这些往往是人生下半场,决定幸福和事业的关键。


    但我依然喜欢技术。无论做什么,技术都会是我自己的优势。


    我们大老板是技术出身,孩子都上小学了,还经常熬夜帮我们处理技术难题。有次聚会我问他,公司那么多事情要忙,怎么还有精力写代码呢?他说写代码就是我最放松的时候。我不由得一阵佩服,或许这就是技术人的魅力吧。


    但在 30 岁之前,我会继续站在技术一线,做一个什么都搞的前端人。


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

    360周鸿祎为什么说大模型已成茶叶蛋?

    大模型炒了一年,为什么没有特别火的应用? 最近几天360创始人周鸿祎称,去年感觉大模型是原子弹,今年感觉是茶叶蛋。 什么意思?我想大概就是说大模型谁都能玩了,现在国内的大模型没有一千,也有几百个了,大模型没什么稀奇的了。但是另一方面也反映了大家都是为了大模型而...
    继续阅读 »

    大模型炒了一年,为什么没有特别火的应用?


    最近几天360创始人周鸿祎称,去年感觉大模型是原子弹,今年感觉是茶叶蛋。


    什么意思?我想大概就是说大模型谁都能玩了,现在国内的大模型没有一千,也有几百个了,大模型没什么稀奇的了。但是另一方面也反映了大家都是为了大模型而大模型,但是大模型没能解决什么实际问题,或者说解决的问题太小,有点让人失望了。



    邓宁-克鲁格效应


    我认为这种感觉是很正常的,也符合事物的一般发展规律,一个新事物出现的时候,大家都抱着很大的期望,期待它去解决各种各样的问题,但是毕竟是新东西,和整个世界的磨合、整合还不够,还需要各种去适配,所以新鲜劲儿过去之后,很多问题还是没解决,大家就感觉失望了。然后这个新事物还要默默的发展一段时间,才有机会重回梦想之巅。


    这种情况有一个名词:邓宁-克鲁格效应(Dunning-Kruger Effect),也简称达克效应(D-K Effect),可以用下边这条曲线来理解它。达克效应本来说的是人的认知过程,但也经常被用来表示事物的发展过程。



    AI大模型的下一步


    AI大模型下一步会怎么发展?我认为首先还是要紧盯OpenAI,作为大模型的引爆者和引领者,OpenAI的发展方向至关重要。


    去年底OpenAI推出了GPTs,也就是大模型的应用商店,为什么干这件事?我认为是因为AGI发展遇阻,技术和资金都有点跟不上,这一点可以从最近OpenAI投资AI芯片、大规模融资,以及OpenAI CEO奥特曼让大家耐心等待AGI等等事件中略窥一二。为了提振信心,探寻更多机会,OpenAI不得不搞出这个应用商店,借助外部的更多力量来促进AI的发展。


    另外预计OpenAI今年就会发布GPT-5,大模型的能力进一步增强。据预测,GPT-5将是一个原生的多模态大模型,不仅能处理文本和图像,还能处理音视频内容,GPT-5甚至将会具备自主的AI模型开发能力,这将使其能够生成各种多模态的AI模型,从而学习和完成新的任务,这将大大扩展GPT-5的应用能力,有力推动通用机器人的发展,给人很多的想象空间。


    GPT-5是更好吃的茶叶蛋,还是更厉害的氢弹?让我们拭目以待!



    大模型和世界的磨合


    另外上边我提到大模型需要和世界进行磨合,怎么磨合?


    我认为第一步就是将AI能力融入到企业的产品或者服务中去。我们现在可以看到很多工具都集成了AI大模型,比如钉钉魔法棒、WPS AI助手、Photoshop AI绘画功能等等,现在也有了一些AI商用产品,比如AI客服、AI培训、AI教育等等方面,还有很多看起来不起眼的AI写作、AI绘画、AI编程等等,他们都在慢慢的渗透到各行各业,这些已经在潜移默化的发生,慢慢的改变工作方式,提升效率。


    虽然还没看到可以持续爆火的应用,也许只是磨合的不够,是黎明前的黑暗。


    对于大家特别期待的AI原生应用,或许可以小小的期待下GPT-5。


    不过我认为不管是AI+应用还是AI原生应用,最重要的是要解决确定性的问题,解决可能产生的错误或不准确的预测结果,否则大家只能把它当做一个玩具,或者只用在某些比较小的场景,无法做到各行各业遍地开花,也就无法推动整个世界的变革与发展。




    以上就是本文的主要内容,欢迎留言一起讨论。


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

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

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

    1. 前言


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


    2. 概念


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


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


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


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


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


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


    2.2 为什么需要控制反转


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


    后端系统中有多个对象:



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

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

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


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



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

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

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


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


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

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


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



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



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


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


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


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

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

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

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



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


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

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

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

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


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


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

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

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

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

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

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



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


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


    2.3 控制反转思想


    2.3.1 获取资源的传统方式



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

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


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



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

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


    2.4 如何实现控制反转


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


    技术描述


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


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


    实现方法


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


    细说


    1.依赖注入:



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

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

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

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


    2.依赖查找


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


    2.4.1 工厂例子依赖注入改造


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


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

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

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

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



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



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

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

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


    3. NestJS 依赖注入


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


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



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

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

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


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


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


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


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


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


    在 Module 的 providers 里声明:


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


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


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


    构造函数或者属性注入


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


    异步的注入对象


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


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


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


    4.实践


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


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


    在 DeptModule  模块中的 propviders 中引入 DeptService


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


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


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


    小结


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


    参考资料



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

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

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

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


    方案一:硬编码


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


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


    image.png


    image.png


    image.png


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


    方案二:sass变量配置


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


    image.png



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


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



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


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

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

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


    • 全局引入


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


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


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

    image.png


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


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


    代码结构如下:


    image.png



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


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

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

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

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


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

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

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


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


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

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


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

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



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


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


    • App.vue指定默认主题色


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

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


    image.png


    实现效果如下:


    image.png


    image.png


    image.png


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


    方案四:滤镜filter


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


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


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


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


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


    方案五:特殊时期变灰



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


    body {
    filter: grayscale(1);
    }

    image.png


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


    总结


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


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

    接手了一个外包开发的项目,我感觉我的头快要裂开了~

    嗨,大家好,我是飘渺。 最近,我和小伙伴一起接手了一个由外包团队开发的微服务项目,这个项目采用了当前流行的Spring Cloud Alibaba微服务架构,并且是基于一个“大名鼎鼎”的微服务开源脚手架(附带着模块代码截图,相信很多同学一看就能认出来)。然而,...
    继续阅读 »

    嗨,大家好,我是飘渺。


    最近,我和小伙伴一起接手了一个由外包团队开发的微服务项目,这个项目采用了当前流行的Spring Cloud Alibaba微服务架构,并且是基于一个“大名鼎鼎”的微服务开源脚手架(附带着模块代码截图,相信很多同学一看就能认出来)。然而,在这段时间里,我受到了来自"外包"和"微服务"这双重debuff的折磨。


    image-20231016162237399


    今天,我想和大家分享一下我在这几天中遇到的问题。希望这几个问题能引起大家的共鸣,以便在未来的微服务开发中避免再次陷入相似的困境。


    1、服务模块拆分不合理


    绝大部分网上的微服务开源框架都是基于后台管理进行模块拆分的。然而在实际业务开发中,应该以领域建模为基础来划分子服务。


    目前的服务拆分方式往往是按照团队或功能来拆分,这种不合理的拆分方式导致了服务调用的混乱,同时增加了分布式事务的风险。


    2、微服务拆分后数据库并没拆分


    所有服务都共用同一个数据库,这在物理层面无法对数据进行隔离,也导致一些团队为了赶进度,直接读取其他服务的数据表。


    这里不禁要问:如果不拆分数据库,那拆分微服务还有何意义?


    3、功能复制,不是双倍快乐


    在项目中存在一个基础设施模块,其中包括文件上传、数据字典、日志等基础功能。然而,文件上传功能居然在其他模块中重复实现了一遍。就像这样:


    image-20231017185809403


    4、到处都是无用组件堆彻


    在项目的基础模块中,自定义了许多公共的Starter,并且这些组件在各个微服务中被全都引入。比如第三方登录组件、微信支付组件、不明所以的流程引擎组件、验证码组件等等……


    image.png


    拜托,我们已经有自己的SSO登录,不需要微信支付,还有自己的流程引擎。那些根本用不到的东西,干嘛要引入呢?


    5、明显的错误没人解决


    这个问题是由上面的问题所导致的,由于引入了一个根本不需要的消息中间件,项目运行时不断出现如下所示的连接异常。


    image-20231013223714103


    项目开发了这么久,出错了这么久,居然没有一个人去解决,真的让人不得不佩服他们的忍受力。


    6、配置文件一团乱麻


    你看到服务中这一堆配置文件,是不是心里咯噔了一下?


    image-20231017190214587


    或许有人会说:"没什么问题呀,按照不同环境划分不同的配置文件”。可是在微服务架构下,已经有了配置中心,为什么还要这么做呢?这不是画蛇添足吗?


    7、乱用配置中心


    项目一开始就明确要使用Apollo配置中心,一个微服务对应一个appid,appid一般与application.name一致。


    但实际上,多个服务却使用了相同的appid,多个服务的配置文件还塞在了同一个appid下。


    更让人费解的是,有些微服务又不使用配置中心。


    8、Nacos注册中心混乱


    由于项目有众多参与的团队,为了联调代码,开发人员在启动服务时不得不修改配置文件中Nacos的spring.cloud.nacos.discovery.group属性,同时需要启动所有相关服务。


    这导致了两个问题:一是某个用户提交了自己的配置文件,导致其他人的服务注册到了别的group,影响他人的联调;二是Nacos注册中心会存在一大堆不同的Gr0up,查找服务变得相当麻烦。


    其实要解决这个问题只需要重写一下网关的负载均衡策略,让流量调度到指定的服务即可。据我所知,他们使用的开源框架应该支持这个功能,只是他们不知道怎么使用。


    9、接口协议混乱


    使用的开源脚手架支持Dubbo协议和OpenFeign调用,然而在我们的项目中并不会使用Dubbo协议,微服务之间只使用OpenFeign进行调用。然而,在对外提供接口时,却暴露了一堆支持Dubbo协议的接口。


    10、部署方式混乱


    项目部署到Kubernetes云环境,一般来说,服务部署到云上的内部服务应该使用ClusterIP的方式进行部署,只有网关服务需要对外访问,网关可以通过NodePort或Ingress进行访问。


    这样做可以避免其他人或服务绕过网关直接访问后端微服务。


    然而,他们的部署方式是所有服务都开启了NodePort访问,然后在云主机上还要部署一套Nginx来反向代理网关服务的NodePort端口。


    image-20231016162150035


    结语


    网络上涌现着众多微服务开源脚手架,它们吸引用户的方式是将各种功能一股脑地集成进去。然而,它们往往只是告诉你“如何集成”却忽略了“为什么要集成”。


    尽管这些开源项目能够在学习微服务方面事半功倍,但在实际微服务项目中,我们不能盲目照搬,而应该根据项目的实际情况来有选择地裁剪或扩展功能。这样,我们才能更好地应对项目的需求,避免陷入不必要的复杂性,从而更加成功地实施微服务架构。


    最后,这个开源项目你们认识吗?


    image-20231017190633190


    作者:飘渺Jam
    来源:juejin.cn/post/7291480666087964732
    收起阅读 »

    8年前端,那就聊聊被裁的感悟吧!!

    前端开发,8年工作经验,一共呆了2家公司,一个是做积分兑换的广告公司。这是一个让我成长并快乐的公司,并不是因为公司多好,而是遇到了一群快乐的朋友,直到现在还依旧联系着。 另一家是做电子签名的独角兽,我人生的至暗时刻就是在这里这里并不是说公司特别不好,而是自己的...
    继续阅读 »

    前端开发,8年工作经验,一共呆了2家公司,一个是做积分兑换的广告公司。这是一个让我成长并快乐的公司,并不是因为公司多好,而是遇到了一群快乐的朋友,直到现在还依旧联系着。
    另一家是做电子签名的独角兽,我人生的至暗时刻就是在这里这里并不是说公司特别不好,而是自己的际遇


    我的经历


    第一家公司


    第一家公司说来也巧,本来是准备入职一家外包的,在杭州和同学吃个饭,接到了面试通知,一看地址就在楼上,上去一共就3轮面试,不到2个小时直接给了offer。有些东西真的就是命中注定


    第二家公司


    第二家公司我入职以后挖了上家公司诸多墙角,我一共挖了6个前端,2个后端。拯救朋友们于水深火热之中。





    我本以为我能开启美好的新生活,结果第二年就传来我父亲重病的噩耗 肺癌晚期,我学习了大量的肺癌知识什么小细胞,非小细胞,基因检测呀等等。。。





    可是最后还是没有挽留住他的生命,我记得我俩在最后一次去武汉的时候,睡在一起,他给我说了很多。


    他说:治不好就算了,只是没能看到自己的孙子有些可惜罢了。

    他说:我这一辈碌碌无为,没给你带来多么优越的条件,结婚、买房、工作都没给到任何帮助,唯一让我感到欣慰的是你那么努力,比我强多了,家里邻居很多都眼馋你呢。
    他说:你小孩的名字想好了吗?你媳妇真是个孝顺的孩子,性格也好,心地善良,你要好好对待她。

    他说了很多。。。我都快忘了他说了啥了,我不想忘来着,可是可是,想起来就又好难过。


    这只是我人生历程的一部分,我把这些讲出来,是为了让大家明白,你现在所经历的困苦其实没有那么严重,人在逆境之中会放大自己的困难,以博得同情。所以现在很多人给我倒苦水的时候,我总有点不屑一顾的感觉,并不是我有多强,我只是觉得都能过去。


    在灰暗的时候,工作总是心不在焉,情绪莫名冲动,我和领导吵过架,和ui妹妹撕破脸,导致人家天天投诉我。我leader说我态度极其嚣张,我说你再多说一句,我干死你所以不裁我裁谁


    我的人生感悟


    我时常以我爸的角度换位思考,我在得知这个消息后我该咋办?是积极面对,还是放弃治疗?可是所有的都是在假设的前提之下,一切不可为真。只有在其中的才最能明白其中的感受。
    那一年我看着他积极想活着的毅力,也看到了他身体日渐消瘦的无奈,无奈之余还要应付各种亲戚的嘘寒问暖


    我现在很能明白《天道》中那段,丁元英说的如果为了孝顺的名声,让父亲痛苦没有尊严地活着,还不如让父亲走了。 的意思了。在他昏迷不醒的时候,大小便失禁的时候,真不如有尊严的走了。


    我其实已经预感到自己要被裁,我原本是挺担心的,可是后来想想父亲的话,我总结成一句话圆滑对事,诚以待人。 这句话看上去前后矛盾,无外乎俩个观点。


    圆滑对事的意思是:就是要学会嘴甜,事嘛能少干就少干,能干几分是几分,累的是别人,爽的是自己,在规则中寻求最大的自我利益化。


    诚以待人的意思是:圆滑归圆滑,不能对谁都圆滑,你得有把事情办的很好的能力,你需要给真正需要的人创造价值,而不是为了给压榨者提供以自我健康为代价的价值。



    用现在最流行的词来说就是「佛系」。


    什么叫活明白了,通常被理解为不争不抢,得之淡然、失之泰然、顺其自然的一种心理状态。


    活明白的人一般知道自己要什么样的生活,他们不世故、不圆滑,坦荡的、磊落的做自己应该做的事儿。他们与社会上潜规则里的不良之风格格不入,却不相互抵触,甚至受到局中人的青睐与欣赏。


    活明白的人看着更为洒脱,得不张扬,失不气馁,心态随和、随遇而安。


    不过,还有一种活明白的人,不被多数人所接受。他们玩世不恭、好吃懒做,把所有一切交给命运去背锅。这种人极度自我,没有什么可以超越他自己的利益,无法想象这种活法,简直就是在浪费六道轮回的名额。


    总之,有的人活明白了,是调整自己的心态,维护社会的稳定和安宁。有的人活明白了,是以自我为中心,一边依赖着社会救济,一边责备社会龌蹉。


    所以,活明白的人也分善与恶,同样是一种积极向善,另一种是消极向恶,二者同出而异名。



    我对生活的态度


    离职的第一个月,便独自一人去了南京,杭州,长沙,武汉,孝感。我见了很多老朋友,听听他们发发牢骚,然后找一些小众的景点完成探险。


    在南京看了看中医,在杭州露营看了看日落,在长沙夜爬了岳麓山,在武汉坐了超级大摆锤,在孝感去了无名矿坑并在一个奶奶家蹭了中午饭。


    我的感受极其良好,我体验了前所未有生活态度,我热情待人,嘻嘻笑笑,我站在山顶敞怀吹风,在无尽的树林中悠然自得,治愈我不少的失落情绪。我将继续为生活的不易奔波,也将继续热爱生活,还会心怀感恩对待他人,也会圆滑处事 事事佛系。


    背景1.png


    图层 1.png


    IMG_6214.JPG


    IMG_6198.JPG


    IMG_6279.JPG


    可能能解决你的问题


    要不要和家里人说


    我屏蔽了家里人,把负面情绪隐藏,避免波及母亲本就脆弱的内心世界,我还骗她说公司今年不挣钱,提前让我们放假,只给基础工资。如果你家境殷实,家庭和睦,我建议大方的说,这样你和父母又多了一个可以聊的话题,不妨和他们多多交流,耐心一些。


    裁员,真不是你的问题


    请记住,你没有任何问题,你被裁员是公司的损失,你不需要为此担责,你需要做的是让自己更强,不管是心理、身体还是技术,你得让自己变得精彩,别虚度了这如花般的时光。可能你懒,可能也没什么规划,那就想到啥就做啥好了,可能前几次需要鼓足干劲,后面就会发现轻而易举。


    如何度过很丧的阶段


    沮丧需要一个发泄的出口,可以保持运动习惯,比如日常爬楼梯、跑步等,一场大汗淋漓后,又是一个打满鸡血积极向上的你。

    不要总在家待着,要想办法出门,多建立与社会的联系,多和朋友吹吹牛逼,别把脸面看的那么重要,死皮赖脸反而是一种讨人喜欢的性格。



    不管环境怎样,希望你始终向前,披荆斩棘

    如果你也正在经历这个阶段,希望你放平心态,积极应对

    如果你也在人生的至暗时刻,也请不要彷徨,时间总会治愈一切

    不妨试试大胆一点,生活给的惊喜也同样不少

    我在一个冬天的夜晚写着文字,希望能对你有些帮助


    作者:顾昂_
    来源:juejin.cn/post/7331657679012380722
    收起阅读 »

    vue3的宏到底是什么东西?

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

    前言


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


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


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


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


    progress.png


    vue3的宏是什么?


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



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



    宏是在哪个阶段运行?


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


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


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


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


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


    一个defineProps宏的例子


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


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

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

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


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


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

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

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

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

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


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


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


    convert.png


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


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

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

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


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


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

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

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


    总结


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



    • vue中的宏到底是什么?


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


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


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


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


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



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


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

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

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

    引言

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

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


    注释篇

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

      image.png

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

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

      comment.gif


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

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


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

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

      image.png


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

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

      image.png

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

      image.png

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

      image.png

    2. 文档注释指令

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

      image.png

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

      image.png

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

      image.png

    3. MarkDown 文档注释

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

      image.png

    4. 结合 TS

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

      image.png

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

      image.png

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

      image.png

    5. 总结

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

    减少条件分支语句

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

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

      image.png

    2. 提前返回

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

      image.png

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

      image.png

    代码七宗罪

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

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

    image.png

    语义化

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

    image.png

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

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

    image.png

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

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

    函数二义性

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

    image.png

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

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

    image.png

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

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

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

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

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

    image.png

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

    image.png

    var 的坏处

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

      image.png

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

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

      image.png

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

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

      image.png

      现在用 let 就行了

      image.png

      所以我求求你别用 var 了

    格式化

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

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

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

    我的风格如下

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

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

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

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

    image.png

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

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

    image.png

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

    image.png

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

    image.png

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

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

    image.png

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

    image.png

    三目运算格式化

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

    image.png

    字符串以及对象格式化

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

    image.png

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

    image.png

    分支语句

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

    image.png

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


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

    收起阅读 »

    前端项目如何准确预估个人工时

    补充 看来很多小伙伴对这个问题感兴趣,大家不要忽视了压工时这个事。 领导为什么会压工时? 使他的KPI更好看 不清楚做这个东西实际要多长时间 因为第2点的原因,他也无法去争取合理时间 部分人看着下属加班,有种大权在握,言出法随的畅快感 码农为什么不要轻易答...
    继续阅读 »

    补充


    看来很多小伙伴对这个问题感兴趣,大家不要忽视了压工时这个事。


    领导为什么会压工时?



    1. 使他的KPI更好看

    2. 不清楚做这个东西实际要多长时间

    3. 因为第2点的原因,他也无法去争取合理时间

    4. 部分人看着下属加班,有种大权在握,言出法随的畅快感


    码农为什么不要轻易答应压工时?



    • 无形中会打击你的自信心,当自信心全无的时候,要么是职业生涯结束,要么是变成人人都跑来拿捏一手的角色

    • 轻易妥协,会让你的说的话,可信度降低。毕竟,别人随便说一下,激一下,你就妥协了,那很容易就让人觉得,你就是随意乱说一个时间

    • 这会妨碍你对自己真实能力的认知和评估


    被压工时了怎么办?



    • 偶尔有少量任务,被压了少量工时,个人认为是可以接受的,毕竟不可能一切都能按规划走

    • 大量工作被压工时,那就告知延期风险,你的工作速度应该保持不变,完不成,就让项目延期。如何解决延期问题?那是领导的事情,不是一个小码农应该操心的。


    没怎么压工时,但把工作时间延长了?



    • 首先,工作该是什么速度,就是什么速度,不要替领导着急着赶进度

    • 其次,反馈这有延期风险,建议领导增派人手。(记得先和其他成员通个气)

    • 该提加班就提加班,调休或加班工资是你应得的,累了就调休,你是人,不是机器


    为什么要给自己留缓冲时间?加缓冲时间是工作不饱和?



    • 加缓冲时间不是工作不饱和

    • 8小时工作日,你不可能每分每秒都在写代码,谁也做不到。

    • 你不可能熟悉每个API,总有要你查资料的时候,而查资料,可能你查了4-5个地方,才总结出正确用法,这需要额外的时间

    • 你的工作随时可能被人打断,比如:开会,喝水,同事问你问题等等,这都会耗费你的时间

    • 你拉取代码,提交代码,思考实现方式,和业务进一步确认需求细节,和UI沟通交互细节,自测,造mock数据,这都需要时间

    • 如果没有缓冲时间,一个任务延期,可能会导致,后续N个任务都延期。

    • 即使从项目角度分析,足够的缓冲时间,有利于降低项目延期风险


    工作总是被人打断怎么办?



    • 比如:开会,比如插入了一个紧急工作任务,这种较长时间的打断,那就将这些被占用的时间,写进工作日志,即时向项目组反馈,要求原本的工作任务加工时或延迟开始

    • 被同事问问题。几句话能说清楚的,那不妨和他直说。几句话说不清楚的,那只能等你有空的时候,再给他解答。要优先考虑完成自己的工作任务。


    大方的承认自己的不足,能力多大,就做多少事,明确自己的定位


    可能有的小伙伴,可能被别人激一下,被人以质疑的语句问一下,后续就被人牵着鼻子走了。有很大因素是因为不敢承认某方面的工作能力暂有欠缺。其实大方的承认即可,有问题,那就暴露问题,如果项目组其他成员会,那就让他来教你,这也属于沟通协作。如果没人会,那说明这是一个需要集思广益的公共问题。


    可能有同学觉得自己就是个小码农甚至因为自己是外包,不敢发表自己的想法和见解,其实大可不必,只要你就事论事,有理有据,完全可以大方说出来,你不说出来,你永远只能从自己的角度看这个问题,你无法确认自己是对的还是错的。错了咱改,对了继续保持。既不贬低别人,也不看轻自己,以平常心讨论即可。


    明确自己的定位,就是个普通码农,普通干活的,项目延期了,天塌了也是领导想办法解决。自己不会的就反馈,别人不会自己会的,那就友好分享。不会的,不要羞于请教。干不过来了,及时告知领导,让其协调解决。坦坦荡荡,不卑不亢。


    前提



    1. 此方法是在没有技术阻碍的前提条件下预估,如果有技术障碍,请先解决技术阻碍

    2. 此方法需要根据个人实际情况调整

    3. 这里以普通的以vue,element-plus,axios为基础技术的管理系统为例

    4. 这些都是个人见解,欢迎在评论区提出不同观点

    5. 请先以一个普通的CRUD界面,测算自己的基本编码速度


    为啥评估会不准确


    自我评估时





    领导给你评估时

    功能领导认为的领导忘记的领导认为的时间实际时间
    加个字段加个显示字段而已,实际只要3分钟吧码农要找到对应代码,查看代码上下文,或许还涉及样式的修改,后端接口可能还没有这个字段, 还要自测20分钟2小时
    做个纯列表页面前端只要把后端的字段显示出来就好了吧,肯定会很快可能没有直接可用的组件,即使有,前端可能需要查组件文档,看具体用法, 还得处理loading状态,空状态,然后还得查看后端接口文档,看哪些字段需要额外处理,最后还得自测,甚至可能在真正对接前,需要自己造mock接口2小时8小时
    编辑/新增界面就写个表单,前端把数据提交给后端就完事了前端需要理解业务逻辑,需要做数据校验,对于类似下拉数据,图片上传,可能还要和后端沟通,数据从哪里取,分别表示什么意思,怎么上传图片,提交数据后,成功后要怎么做,以及失败的异常处理,用户填了一半数据之后,刷新了界面,应该如何处理,后端接口没出来前,需要自己mock接口,用来自测4小时3天
    一个响应式界面就一个普通界面应该不至于有什么难度吧忽略了这是一个响应式界面,前端需要与UI设计师沟通,确认在不同情况,界面如何响应,以及思考如何实现,如果业务数据还会对界面的响应式产生影响,那还得进一步深入分析8小时3天
    实现多语言功能多语言,不就是用编码代替原本的文字嘛,根本不需要额外的时间处理吧前端需要考虑多语言数据从哪里来,多语言切换之后对原本布局的影响,多语言切换之后,表单错误提示的处理方式不给额外时间3-4天
    做个3/4环直接使用图表插件,调下API就出来了前端可能需要进行数据转换,需要查看图表插件的文档,图表插件可能没有现成的,需要通过搜索引擎找类似的示例,然后模仿实现,甚至图表插件根本无法实现这种效果,需要用其他技术实现3小时4天
    前期一个连续的类似界面上一个界面和这个类似,把上个界面的代码复制过来,改改字段和接口,应该能很快完成很多界面看着一样,但实际业务逻辑差别很大,只是界面表现形式类似,有些字段是动态显示/隐藏的,有些可以固定写,表单字段的验证逻辑,可能也不一样。并且上一个界面的代码都还没写,还没测试,这里还有很多不确定因素,直接复制还可能导致,同一个错误,在多个界面同时出现2-3小时前一个界面花了多久,这个界面可能还是花了差不多的时间
    仿照xx官网的效果,做个静态界面好多网站都是这个效果,这应该是烂大街的效果吧某个效果可能是知识盲区,需要查资料2天1周,甚至可能做不了
    参考公司内部其他系统界面,实现类似界面现成的东西,这系统也上线好久了,应该把代码复制过来,稍微改改就OK了吧当前这个人从未接触过这个系统,对这个系统一点都不了解,了解需要时间,可能另外的项目有自己的框架,和当前系统的框架不同,无法直接使用, 另外一个项目无法直接给项目代码给你,只能让人给你讲解,但讲解人没时间或不是随时都有时间,或就是随意讲讲,另一个项目的这个界面,可能是经过多人集思广益,多轮讨论与重构才最终得到了这个效果5小时3-5天
    用低代码平台实现个界面就是拖拖组件的事情,代码都不用写,应该很快组件可能没有,有组件可能某些业务逻辑在这个低代码平台难以实现,需要咨询低代码平台的提供方,但低代码提供方,几个人需要服务几十个人,无法优先给你解答,即使解答了,可能给出的方案不通用(或者他们还需要继续问他们内部团队的人),遇到下个类似的情况,原来的解决方案又无效了。难以调试或无法调试,前端原本的知识储备,在低代码平台,仅剩原始的js语法有效2天3周

    总原则



    • 不要duang的一下,对整个界面/模块进行评估,应该对行列,表单项,逻辑点,进行评估,然后将总的时间加起来,就是这个界面的预估工时

    • 要至少多估20%的时间,一个是因为你很难持续性的投入(比如:有人突然问你问题,上厕所,喝水,或有事请假)

    • 请将一天的工作时间最多算6.5小时(因为你可能需要开会,可能被其他事情打断,可能有时不在状态,同时也算是给自己留点思考时间)

    • 尽量不要在过了一遍需求之后,立马评估工时(不要被项目经理或业务的节奏带偏),而是要自己再思考一遍需求,想想大概的实现逻辑,重难点等等,尽量不要当天给出工时评估

    • 如果是给别人评估工时,那尽可能给别人多评点工时

    • 工期紧的时候,加人有必要,但1个人干7天的活,7个人未必能1天干完

    • 有公共组件和没有公共组件完成同样的功能,所需要的时间可能天差地别, 因此,请确保先完成公共组件的开发

    • 请先将业务逻辑理顺,把工作进行拆分,直至自己能正确预估的范围内


    前端有哪些地方需要耗费工时



    • 思考实现方式

    • 静态UI界面还原与响应式

    • 业务逻辑实现

    • 动态UI交互

    • 后端接口mock

    • 后端接口对接

    • 自测


    前端项目应该分成几步实现



    1. 整体项目搭建以及规范与约束确认

    2. 整体页面结构,路由结构搭建

    3. 统一UI风格,以及公共组件实现

    4. 具体的界面实现


    1,2点应该由项目组长完成
    3点应该由项目组长以及技术较强的组员共同完成


    常见的公共组件工时

    组件工时
    查询按钮60 分钟
    提交按钮60 分钟
    confirm按钮60 分钟
    下拉按钮60 分钟
    分页表格360 分钟
    JSON配置分页表格240 分钟
    动态表单360 分钟
    JSON动态表单360 分钟
    模态框90 分钟
    抽屉组件90 分钟
    select组件90 分钟
    tree组件120 分钟
    cascade组件90 分钟
    日期选择组件60 分钟
    日期范围选择组件120 分钟
    axios封装360 分钟
    卡片组件60 分钟
    面包屑组件60 分钟

    列表页拆分与编码工时预估




    首先做总体拆分,分成3大部分



    1. 头部的搜索表单

    每个表单项30分钟左右,每个功能按钮40分钟左右


    因此这里是1个表单项(30分钟),2个功能按钮(80分钟),总计110分钟



    1. 中间的工具栏




    P.S. 这里没算右侧工具条,只算了左侧功能按钮


    因为是列表页,添加角色这个按钮,只考虑是个简单按钮加个点击事件,至于点击按钮之后的角色添加界面的工时不放在列表页评估,而是在添加角色界面单独评估,因此添加角色按钮算30分钟


    批量操作按钮,应该使用公共组件的下拉按钮组件,以及与分页表格组件配合实现,因此算40-60分钟


    因此这里整体应该总计在70分钟内



    1. 主体的分页表格




    应该使用公共组件的分页表格组件实现



    • 普通列(直接显示字段值的列,和简单转换的列)每列算20分钟

    • 操作列按每个操作按钮另算

    • 复杂转换列按40-60分钟算

    • 排序列按40-60分钟算

    • 分页表格组件调用30分钟


    从界面看,这里有6列,checkbox列和序号列,是分页表格组件实现的,无需再算工时,除操作列和创建时间外,其他都属于普通列算20分钟每列,创建时间列算40分钟,因此总共100分钟


    操作列角色成员,角色权限和修改,都需要打开一个抽屉界面(抽屉界面里的东西另算,不算在列表页中),删除需要调后端接口以及确认,因此



    • 角色成员按钮: 20分钟

    • 角色权限按钮: 20分钟

    • 修改按钮: 20分钟

    • 删除按钮: 30分钟


    总计: 100 + 20*3 + 30 = 190分钟


    因此整个列表页工时


    列表页需要mock 1个接口,列表接口,算20分钟


    110 + 70 + 190 + 20 = 390 分钟 = 6.5小时


    再在390分钟的基础上再多加20% = 390*1.2 = 468 分钟 = 7.8 小时


    P.S.



    1. 添加角色/角色成员/角色权限这是独立界面,需要单独计算时间。计算方式也与上面的类似

    2. 没有单独计算自测时间,个人认为理想情况应该对1个界面,加2-3小时自测时间

    3. 没有计算联调时间,联调时间应该另算

    4. 没有计算UI还原时间,对于复杂UI界面或UI还原度要求高的界面,应该单独计算UI还原时间

    5. 对于复杂的业务逻辑,可以将业务逻辑拆解为一条条的业务逻辑项,每个业务逻辑项以40分钟左右每条作为参考实现时间

    6. 没有考虑思考时间,对于复杂的业务逻辑,或者没做过的界面形态,或者复杂的界面形态等,必须将思考时间计算进来,或者说,在已经基本想明白怎么去实现的基础上,再去评估工时


    被误解的敏捷开发模式

    错误的敏捷开发




    • 敏捷开发就是强调一个快字

    • 敏捷开发就是不断的压榨工时

    • 敏捷开发就是不停的加班


    正确的敏捷开发



    • 测试在项目之初就介入,编写完测试用例之后,共享给开发,方便开发自测

    • 将一个完整的项目进行合理拆分,拆分为若干独立小迭代

    • 每个小迭代完成之后,进行提测以及收集用户试用反馈,尽早反馈,以及尽早发现问题

    • 在小迭代提测期间,应该让开发略作修整(改bug或修整)和总结(总结共性问题,避免下阶段,再重复出现这些共性问题),而非让开发立马进入下阶段开发,否则容易造成,开发一边赶下阶段需求,一边赶上阶段bug

    • 个人认为敏捷开发,重点在于敏捷,灵巧好掉头,分阶段交付,及早发现问题,拥抱需求变化。而非简单的抽着鞭子让程序员加班赶工996或007


    相关文章


    作者:悟空和大王
    链接:https://juejin.cn/post/7330071686489636904
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    收起阅读 »

    情人节即分手,FreeNginx 来了

    时间线 2019 年 3 月 11 日,F5 Networks 宣布以 6.7 亿美元收购 Nginx。 2022.01.18, NGINX 创始人 Igor Sysoev 从 NGINX 和 F5 离职:“Igor Sysoev 选择离开 F5,以便将...
    继续阅读 »

    38d432781be446559f99d3d139ef6be7.png


    时间线



    2019 年 3 月 11 日,F5 Networks 宣布以 6.7 亿美元收购 Nginx。




    2022.01.18, NGINX 创始人 Igor Sysoev 从 NGINX 和 F5 离职:“Igor Sysoev 选择离开 F5,以便将更多的时间留给陪伴家人和朋友,以及个人项目。感谢他所做的一切让全世界的网站变得更好。”




    在 2024.2.14 情人节这天,作为 nginx 的长期核心开发者之一,马克西姆-杜宁(Maxim Dounin)宣布创建一个名为 Freenginx 的新分叉项目。



    他在宣布 Freenginx 时说道


    "你们可能知道,F5 于 2022 年关闭了莫斯科办事处,从那时起我就不再为 F5 工作了。 不过,我们已经达成协议,我将继续作为志愿者参与nginx开发。在近两年的时间里,我一直致力于改进nginx,免费为大家提供更好的nginx。
    不幸的是,F5一些新的非技术管理人员最近决定干涉nginx多年来使用的安全策略,无视策略和开发者的立场。


    这很好理解:他们拥有这个项目,可以对其做任何事情,包括以市场为导向的行为,无视开发者和社区的立场。 不过,这还是与我们的协议相矛盾。 更重要的是,我无法再控制F5内部对nginx的修改,也不再将nginx视为一个为公众利益开发和维护的自由开源项目。


    因此,从今天起,我将不再参与F5运营的nginx开发。 取而代之的是,我将启动另一个项目,由开发者而非公司实体运行。


    目标是使nginx开发不受公司任意行为的影响。 欢迎提供帮助和贡献。 希望大家都能从中受益。


    freenginx.org上的简短声明



    freenginx.org的目标是使nginx的开发不受任意公司行为的影响。



    image.png


    开源和商业


    利益与目标不同决定了开源项目的不同发展方向,这不好评说好坏对错。



    作为商业公司,F5毕竟真金白银花了那么多钱拥有了nginx,全职人员的成本付出,这肯定需要往商业化方向考量,希望能找到商业与开源的平衡。




    Maxim Dounin 有着开发者的自由理想园,站在开发者和开源使用者的角度看开源项目的发展,nginx 能更开放更自由,方向由社区掌控。也希望 freenginx 能发展顺利。



    oracle-jdk vs openjdk, mysql vs mariadb, 现在有了 nginx vs freenginx, 我们现在可以开始关注 Freenginx 的未来发展,看未来有多少其他开发者会专注于这个新的分叉。


    Nginx 擦边广告,使用 HertzBeat 快速监控 Nginx



    HertzBeat 是一款我们开源的实时监控系统,无需Agent,性能集群,兼容Prometheus,自定义监控和状态页构建能力。github.com/dromara/her…




    它支持对应用服务,应用程序,数据库,缓存,操作系统,大数据,中间件,Web服务器,云原生,网络,自定义等监控。下面广告演示下如果使用 HertzBeat 快速监控 Nginx 服务状态。



    1. 部署 HertzBeat


    docker run -d -p 1157:1157 -p 1158:1158 --name hertzbeat tancloud/hertzbeat


    2. 部署 Nginx


    本地部署启动 Nginx, 默认监控 Nginx 可用性,若监控更多指标,则需启用 Nginx 的 ngx_http_stub_status_modulengx_http_reqstat_module 监控模块


    参考文档:hertzbeat.com/zh-cn/docs/…


    3. 在 HertzBeat 添加 Nginx 监控


    访问 HertzBeat 控制页面,在 应用服务监控 -> Nginx服务器 添加对端 Nginx 监控,配置对端IP端口等参数。



    确认添加后就OK啦,后续我们就可以看到 Nginx 的相关指标数据状态,还可以设置告警阈值通知等,当 Nginx 挂了或者某个指标异常过高时,通过邮件钉钉微信等通知我们。





    10分钟搞定,快来使用 HertzBeat 24小时自动观测你的 Nginx 状态


    在 Github Star 我们!


    github.com/dromara/her…


    gitee.com/dromara/her…



    部分内容来源于 http://www.msn.com/zh-cn/chann…



    作者:Dromara开源社区
    来源:juejin.cn/post/7335089578321854498
    收起阅读 »

    localhost和127.0.0.1的区别是什么?

    今天在网上逛的时候看到一个问题,没想到大家讨论的很热烈,就是标题中这个: localhost和127.0.0.1的区别是什么? 前端同学本地调试的时候,应该没少和localhost打交道吧,只需要执行 npm run 就能在浏览器中打开你的页面窗口,地址栏显...
    继续阅读 »

    今天在网上逛的时候看到一个问题,没想到大家讨论的很热烈,就是标题中这个:


    localhost和127.0.0.1的区别是什么?



    前端同学本地调试的时候,应该没少和localhost打交道吧,只需要执行 npm run 就能在浏览器中打开你的页面窗口,地址栏显示的就是这个 http://localhost:xxx/index.html


    可能大家只是用,也没有去想过这个问题。


    联想到我之前合作过的一些开发同学对它们俩的区别也没什么概念,所以我觉得有必要普及下。


    localhost是什么呢?


    localhost是一个域名,和大家上网使用的域名没有什么本质区别,就是方便记忆。


    只是这个localhost的有效范围只有本机,看名字也能知道:local就是本地的意思。


    张三和李四都可以在各自的机器上使用localhost,但获取到的也是各自的页面内容,不会相互打架。


    从域名到程序


    要想真正的认清楚localhost,我们还得从用户是如何通过域名访问到程序说起。


    以访问百度为例。


    1、当我们在浏览器输入 baidu.com 之后,浏览器首先去DNS中查询 baidu.com 的IP地址。


    为什么需要IP地址呢?打个比方,有个人要寄快递到你的公司,快递单上会填写:公司的通讯地址、公司名称、收件人等信息,实际运输时快递会根据通信地址进行层层转发,最终送到收件人的手中。网络通讯也是类似的,其中域名就像公司名称,IP地址就像通信地址,在网络的世界中只有通过IP地址才能找到对应的程序。


    DNS就像一个公司黄页,其中记录着每个域名对应的IP地址,当然也有一些域名可能没做登记,就找不到对应的IP地址,还有一些域名可能会对应多个IP地址,DNS会按照规则自动返回一个。我们购买了域名之后,一般域名服务商会提供一个域名解析的功能,就是把域名和对应的IP地址登记到DNS中。


    这里的IP地址从哪里获取呢?每台上网的电脑都会有1个IP地址,但是个人电脑的IP地址一般是不行的,个人电脑的IP地址只适合内网定位,就像你公司内部的第几栋第几层,公司内部人明白,但是直接发给别人,别人是找不到你的。如果你要对外部提供服务,比如百度这种,你就得有公网的IP地址,这个IP地址一般由网络服务运营商提供,比如你们公司使用联通上网,那就可以让联通给你分配一个公网IP地址,绑定到你们公司的网关服务器上,网关服务器就像电话总机,公司内部的所有网络通信都要通过它,然后再在网关上设置转发规则,将网络请求转发到提供网络服务的机器上。


    2、有了IP地址之后,浏览器就会向这个IP地址发起请求,通过操作系统打包成IP请求包,然后发送到网络上。网络传输有一套完整的路由协议,它会根据你提供的IP地址,经过路由器的层层转发,最终抵达绑定该IP的计算机。


    3、计算机上可能部署了多个网络应用程序,这个请求应该发给哪个程序呢?这里有一个端口的概念,每个网络应用程序启动的时候可以绑定一个或多个端口,不同的网络应用程序绑定的端口不能重复,再次绑定时会提示端口被占用。通过在请求中指定端口,就可以将消息发送到正确的网络处理程序。


    但是我们访问百度的时候没有输入端口啊?这是因为默认不输入就使用80和443端口,http使用80,https使用443。我们在启动网络程序的时候一定要绑定一个端口的,当然有些框架会自动选择一个计算机上未使用的端口。



    localhost和127.0.0.1的区别是什么?


    有了上边的知识储备,我们就可以很轻松的搞懂这个问题了。


    localhost是域名,上文已经说过了。


    127.0.0.1 呢?是IP地址,当前机器的本地IP地址,且只能在本机使用,你的计算机不联网也可以用这个IP地址,就是为了方便开发测试网络程序的。我们调试时启动的程序就是绑定到这个IP地址的。


    这里简单说下,我们经常看到的IP地址一般都是类似 X.X.X.X 的格式,用"."分成四段。其实它是一个32位的二进制数,分成四段后,每一段是8位,然后每一段再转换为10进制的数进行显示。


    那localhost是怎么解析到127.0.0.1的呢?经过DNS了吗?没有。每台计算机都可以使用localhost和127.0.0.1,这没办法让DNS来做解析。


    那就让每台计算机自己解决了。每台计算机上都有一个host文件,其中写死了一些DNS解析规则,就包括 localhost 到 127.0.0.1 的解析规则,这是一个约定俗成的规则。


    如果你不想用localhost,那也可以,随便起个名字,比如 wodehost,也解析到 127.0.0.1 就行了。


    甚至你想使用 baidu.com 也完全可以,只是只能自己自嗨,对别人完全没有影响。


    域名的等级划分


    localhost不太像我们平常使用的域名,比如 http://www.juejin.cn 、baidu.com、csdn.net, 这里边的 www、cn、com、net都是什么意思?localhost为什么不需要?


    域名其实是分等级的,按照等级可以划分为顶级域名、二级域名和三级域名...


    顶级域名(TLD):顶级域名是域名系统中最高级别的域名。它位于域名的最右边,通常由几个字母组成。顶级域名分为两种类型:通用顶级域名和国家顶级域名。常见的通用顶级域名包括表示工商企业的.com、表示网络提供商的.net、表示非盈利组织的.org等,而国家顶级域名则代表特定的国家或地区,如.cn代表中国、.uk代表英国等。


    二级域名(SLD):二级域名是在顶级域名之下的一级域名。它是由注册人自行选择和注册的,可以是个性化的、易于记忆的名称。例如,juejin.cn 就是二级域名。我们平常能够申请到的也是这种。目前来说申请 xxx.com、xxx.net、xxx.cn等等域名,其实大家不太关心其顶级域名com\net\cn代表的含义,看着简短好记是主要诉求。


    三级域名(3LD):三级域名是在二级域名之下的一级域名。它通常用于指向特定的服务器或子网。例如,在blog.example.com中,blog就是三级域名。www是最常见的三级域名,用于代表网站的主页或主站点,不过这只是某种流行习惯,目前很多网站都推荐直接使用二级域名访问了。


    域名级别还可以进一步细分,大家可以看看企业微信开放平台这个域名:developer.work.weixin.qq.com,com代表商业,qq代表腾讯,weixin代表微信,work代表企业微信,developer代表开发者。这种逐层递进的方式有利于域名的分配管理。


    按照上边的等级定义,我们可以说localhost是一个顶级域名,只不过它是保留的顶级域,其唯一目的是用于访问当前计算机。


    多网站共用一个IP和端口


    上边我们说不同的网络程序不能使用相同的端口,其实是有办法突破的。


    以前个人博客比较火的时候,大家都喜欢买个虚拟主机,然后部署个开源的博客程序,抒发一下自己的感情。为了挣钱,虚拟主机的服务商会在一台计算机上分配N多个虚拟主机,大家使用各自的域名和默认的80端口进行访问,也都相安无事。这是怎么做到的呢?


    如果你有使用Nginx、Apache或者IIS等Web服务器的相关经验,你可能会接触到主机头这个概念。主机头其实就是一个域名,通过设置主机头,我们的程序就可以共用1个网络端口。


    首先在Nginx等Web程序中部署网站时,我们会进行一些配置,此时在主机头中写入网站要使用的域名。


    然后Nginx等Web服务器启动的时候,会把80端口占为己有。


    然后当某个网站的请求到达Nginx的80端口时,它会根据请求中携带的域名找到配置了对应主机头的网络程序。


    然后再转发到这个网络程序,如果网络程序还没有启动,Nginx会把它拉起来。


    私有IP地址


    除了127.0.0.1,其实还有很多私有IP地址,比如常见的 192.168.x.x。这些私有IP地址大部分都是为了在局域网内使用而预留的,因为给每台计算机都分配一个独立的IP不太够用,所以只要局域网内不冲突,大家就可劲的用吧。你公司可以用 192.168.1.1,我公司也可以用192.168.1.1,但是如果你要访问我,就得通过公网IP进行转发。


    大家常用的IPv4私有IP地址段分为三类:


    A类:从10.0.0.0至10.255.255.255


    B类:从172.16.0.0至172.31.255.255


    C类:从192.168.0.0至192.168.255.255。


    这些私有IP地址仅供局域网内部使用,不能在公网上使用。


    --


    除了上述三个私有的IPv4地址段外,还有一些保留的IPv4地址段:


    用于本地回环测试的127.0.0.0至127.255.255.255地址段,其中就包括题目中的127.0.0.1,如果你喜欢也可以给自己分配一个127.0.0.2的IP地址,效果和127.0.0.1一样。


    用于局域网内部的169.254.0.0至169.254.255.255地址段,这个很少接触到,如果你的电脑连局域网都上不去,可能会看到这个IP地址,它是临时分配的一个局域网地址。


    这些地址段也都不能在公网上使用。


    --


    近年来,还有一个现象,就是你家里或者公司里上网时,光猫或者路由器对外的IPv4地址也不是公网IP了,这时候获得的可能是一个类似 100.64.x.x 的地址,这是因为随着宽带的普及,运营商手里的公网IP也不够了,所以运营商又加了一层局域网,而100.64.0.0 这个网段是专门分给运营商做局域网用的。如果你使用阿里云等公有云,一些云产品的IP地址也可能是这个,这是为了将客户的私有网段和公有云厂商的私有网段进行有效的区分。


    --


    其实还有一些不常见的专用IPv4地址段,完整的IP地址段定义可以看这里:http://www.iana.org/assignments…



    IPv6


    你可能也听说过IPv6,因为IPv4可分配的地址太少了,不够用,使用IPv6甚至可以为地球上的每一粒沙子分配一个IP。只是喊了很多年,大家还是喜欢用IPv4,这里边原因很多,这里就不多谈了。


    IPv6地址类似:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX

    它是128位的,用":"分成8段,每个X是一个16进制数(取值范围:0-F),IPv6地址空间相对于IPv4地址有了极大的扩充。比如:2001:0db8:3c4d:0015:0000:0000:1a2f:1a2b 就是一个有效的IPv6地址。


    关于IPv6这里就不多说了,有兴趣的可以再去研究下。


    关注萤火架构,加速技术提升!


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

    前端实现excel_xlsx文件预览

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

    使用的框架: React


    要使用的库: exceljs、handsontable



    1. 概述


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


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


    2. 实现步骤


    2.1 安装库


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


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


    直接贴代码了:


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

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

    // 注册模块
    registerAllModules();

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

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

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

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

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

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


    </div>
    </>

    )
    }

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


    image.png


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


    3. 其它的杂七杂八


    3.1 单元格样式


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


    image.png


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


    image.png


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


    第一步


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


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

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


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



    第二步


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


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


    下面是获取颜色的函数:


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

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


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


    3.2 合并单元格


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


    image.png


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


    image.png


    3. 总结(附全代码)


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



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



    附上全部代码:


    /**
    * exceljs + handsontable
    */

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

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

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

    // 注册模块
    registerAllModules();

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

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

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

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

    });

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

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

    const worksheet = workbook.getWorksheet(1);

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

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

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

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

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

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

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

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

    </div>
    </>

    )
    }

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

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

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

    嗨,我们又见面了!


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


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


    alovajs:轻量级请求策略库


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


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

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

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


    对比axios,alovajs的优势


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


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



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

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

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

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


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


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


    indexOf 基本用法


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


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


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

    console.log(index); // 2

    indexOf 的第二个参数


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


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


    我们来看一个例子:


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

    console.log(index); // 0

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

    console.log(index2); // 3

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


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


    优秀库源码里的使用


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



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



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


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


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

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

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

    return result;
    }

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


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

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


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

    let result = "";

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

    return result;
    }

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


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

    let result = "";

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

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

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

    return result;
    }

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


    res1.png


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


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


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

    let result = "";

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

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

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

    return result;
    }

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


    res2.png


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


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

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


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


    总结


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


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

    春晚刘谦魔术的模拟程序

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

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


    什么是约瑟夫环问题?


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


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


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


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


    1、考虑最简单的情况


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


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


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


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


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


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


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


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


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


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


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


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



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

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

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

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

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

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

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

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


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


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

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

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

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

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

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

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

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

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

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


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


    image-20240210161329783


    image-20240210161339317


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


    import random

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

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

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

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

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

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

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

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

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

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


    java


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

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

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

           Random rand = new Random();

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

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

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

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

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

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

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

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

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


    以及c++代码


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

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

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

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

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

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

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

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

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

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

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

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

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

       return 0;
    }

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

    rpc比http好吗,缪论?

    是什么,如何理解 RPC(Remote Procedure Call) 直译就是远程过程调用 HTTP(HyperText Transfer Protorl) 直译就是超文本传输协议 RPC和HTTP都是 请求-响应协议,但是因为出现的时机、设计理念、约定协议...
    继续阅读 »

    是什么,如何理解


    RPC(Remote Procedure Call) 直译就是远程过程调用


    HTTP(HyperText Transfer Protorl) 直译就是超文本传输协议


    RPC和HTTP都是 请求-响应协议,但是因为出现的时机、设计理念、约定协议、效率、应用范围、使用规则等不同,所以是不同的名字,本质都是为了分布式系统间的通信而生,是一种应用层通信(请求-响应)协议(从OSI网络模型来看)。



    • RPC是 Bruce Jay Nelson 在1981年创造的术语,HTTP是在1990年左右产生的(可以参看维基百科)


    RPC协议 和 RPC,到底叫什么?RPC协议=RPC


    HTTP协议、HTTP,到底叫什么?HTTP协议=HTTP


    RPC|HTTP只是大家的简称



    • HTTP协议不仅仅只有协议,还有超文本,传输,以及很多功能(比如编解码、面试经常背的各种参数的作用)

    • RPC协议也不仅仅只有协议,还有 编解码,服务注册发现,负载均衡等


    RPC协议本质上定义了一种通信的流程,而具体的实现技术是没有约束的,每一种RPC框架都有自己的实现方式,我认为HTTP也是RPC的一种实现方式


    协议直白来讲是一种约定,rpc和http都是为了服务器间的通信而生,都需要制定一套标准协议来进行通信。不过HTTP比较火,是一个全世界的统一约定,使用比较广泛。但通用也意味着冗余,所以后来又产生了很多RPC框架(自定义协议,具备优秀的性能等)


    我们可以自定义RPC请求/响应 包含的消息头和消息体结构,自定义编解码方式,自定义网络通信方式,只要clientserver消息的发送和解析能对应即可,这些问题确认下来,一个RPC框架就设计出来了


    下面先从请求过程看一下RPC和HTTP都会经历哪些阶段,然后再分阶段去做对比


    一次请求的过程



    阶段阶段分层RPCHTTP
    client: 业务逻辑xx业务逻辑层
    client: 客户端构造请求,发起调用编解码thrift|json|protobuf等json|图片等
    client: 根据传输协议构造数据流协议层thrift|gRPC|Kitex|dubbo等HTTP1 |HTTP1.1|HTTP2|QUIC等
    client: 服务发现服务发现自定义内部服务发现组件DNS
    client: 网络通信:传输数据流网络通信层接口层:netty|netpool,根据OS的API做了一些封装本质:TCP|UDP|HTTP系列接口层:HTTP内部自己实现,目前不清楚咋做的本质:TCP|UDP
    server: 把数据流解析为协议结构协议层略,同上略,同上
    server: 解析协议中的请求体编解码略,同上略,同上
    server: 执行业务逻辑xx业务逻辑层略,同上略,同上

    从请求链路可以看到,最核心的只有三层:编解码、协议、网络通信


    下面会从这3个角度去对比HTTP和RPC


    HTTP VS RPC自定义协议


    HTTP和RPC 2个关键词不具备可比较性,因为RPC包含了HTTP。


    但是RPC自定义协议(thrift, protobuf, dubbo, kitex-thrift等) 是RPC的具体实现,HTTP也是RPC的具体实现,它们是具备可比较性的


    编解码(序列化)



    • 序列化: 指将程序运行过程中的动态内存数据(java的class、go的struct)转化为硬盘中静态二进制数据的过程,以方便网络传输。

    • 反序列化:指将硬盘中静态二进制数据转化为程序运行过程中的动态内存数据的过程,以方便程序计算。


    HTTP/1.1 一般用json


    自定义RPC协议 一般用 thrift、protobuf


    kitex序列化协议


    维度json(HTTP/1.1)protobuf(gRPC)
    优点1. 可读性好、使用简单,学习成本低1. 序列化后的体积比json小 => 传输效率高
    2. 序列化/反序列化速度快 => 性能损耗小
    缺点1. JSON 进行序列化的额外空间开销比较大
    2. JSON 没有类型,比如无法区分整数和浮点
    像 Java 、Go这种强类型语言,不是很友好,解析速度比较慢(需要通过反射解决)
    1. 不可读,都是二进制
    适用场景适用于服务提供者与服务调用者之间传输的数据量要相对较小的情况,否则会严重影响性能追求高性能的场景

    协议层


    编码之后,数据转换成字节流,但是RPC通信时,每次请求发送的数据大小不是固定的,那么为了区分消息的边界,避免粘包、半包等现象,我们需要定义一种协议,来使得接收方能够正确地读出不定长的内容。简单点说,通信协议就是约定客户端和服务器端传输什么数据,以及如何解析数据。


    维度HTTP/1.1kitex-TTHeader
    优点1. 灵活,可以自定义很多字段
    2. 几乎所有设备都可以支持HTTP协议
    1. 灵活,通用,可以自定义
  • 自定义必要字段即可 => 减小报文体积,提高传输效率
    2. 性能优秀
  • 缺点1. 包含许多为了适应浏览器的冗余字段,这些是内部服务用不到的,性能差1. 部分设备存在不能支持,通用性欠佳

    可参考



    可以思考一下 序列化、传输协议、网络通信的关系,下面以kitex为例进行分析


    kitex codec 接口定义kitex thrift 序列化实现kitex ttheader协议,kitex 发送请求核心代码



    可以发现 Encode中,先根据message构造出header,写入out,然后再把data(实际的业务数据)写到out。


    encode函数完全遵守 ttheader协议去构造数据。


    最后再把out通过网络库发送出去



    网络通信层


    网络通信层主要提供一个易用的网络库,封装了操作系统提供的socket api。


    维度HTTP/1.1kitex框架
    实现方式一般采用短连接需要3次握手(可以配置长链接添加请求头Keep-Alive: timeout=20)- 长连接,指在一个连接上可以连续发送多个数据包,在连接保持期间,如果没有数据包发送,需要双方发链路检测包rpc框架维护一个tcp连接池,每次用完不断开连接,通过心跳检测断开连接(探测服务是否有问题)- 支持短连接、长连接池、连接多路复用以及连接池状态监控。
    优点1. 几乎所有设备都可以支持HTTP协议1. 不用每次请求都经历三次握手&四次挥手,减少延时
    缺点1. 每次请求都要新建连接,性能差1. 部分设备存在不能支持,通用性欠佳

    HTTP的长连接和TCP长连接不是一个东西,需要注意下,TCP Keepalive是操作系统实现的功能,并不是TCP协议的一部分,需要在操作系统下进行相关配置(只能保证网络没问题,不能代表服务没问题)


    其中 HTTP2 拥有多路复用、优先级控制、头部压缩等优势


    可以参考


    kitex:连接类型


    RPC自定义协议 和 HTTP的使用场景


    公司内部的微服务,对客户端提供的服务 适合用RPC,更好的性能


    对外服务、单体服务、为前端提供的服务适合用HTTP


    我的思考


    rpc在编解码、协议层、网络通信 都比HTTP有更大的优势,那为啥不把HTTP换成RPC呢



    1. 人的认知,HTTP已经深入人心(或者说生态好,通用性强),几乎所有的机器、浏览器和语言默认都会支持。但是自定义RPC协议 可能很多人都没听过(比如kitex、dubbo等),还让别人支持,根本不可能。

      • 需要建设全局的DNS等等,HTTP链路中的组件都需要换成 自定义的那一套,成本极高。

      • 但是公司内部可以搞成一套,可以极大提高性能,何乐而不为。

      • 我见过的案例是很多时候并没有深入思考为什么用,而是大家都这么用,我也这么用。



    2. 浏览器只支持 http协议。而且浏览器不支持自定义编解码的解析

      • 为啥大家面向浏览器/前端 不用自定义编解码?

        • 举例:protobuf不支持前端语言,但是支持java

        • 就是自定义编解码框架支持语言有限,很多语言没有工具可以做,并且浏览器也不支持。对于问题排查比较困难。

        • github.com/protocolbuf…



      • http不仅可以传输json、还可以传输二进制、图片等。所以协议层可以用http,编解码用protobuf/thrift也是可行的。

        • 公司内部实际案例:服务端和客户端交互时,为了提高性能,采用protobuf编解码数据,使用http协议传输数据。

          • 但是每次请求/响应数据都是不可读的。服务端会把protobuf编码前的数据转为json,用于打印log/存储,方便排查问题。





      • 参考 丨隋堤倦客丨的评论





    • RPC框架 可以自定义负载均衡,重试机制,高可用,流量控制等策略。这些是HTTP不能支持的

      • 我理解是协议层用的http,但是内部的运行机制还是自定义的。http只是定义了传输数据的格式。举个例子:http的流量控制其实用的是 tcp的滑动窗口,http协议本身不具备这些功能。但是rpc是可以自己加这些功能的。这些功能必然有数据传输,这个传输协议用的http。

      • 参考 leewp同学的评论




    参考


    如何保活主流RPC框架长连接,Dubbo的心跳机制,值得学习_牛客博客


    3.8 既然有 HTTP 协议,为什么还要有 RPC?


    4.15 TCP Keepalive 和 HTTP Keep-Alive 是一个东西吗?


    RPC 漫谈: 连接问题


    聊一聊Go网络编程(一)--TCP连接通信 - 掘金


    Kitex前传:RPC框架那些你不得不知的故事


    kitex 传输协议


    dubbo RPC 协议


    作者:cli
    来源:juejin.cn/post/7264454873588449336
    收起阅读 »

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

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

    前言


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


    正文


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


    第一版


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


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

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

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

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

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


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


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


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


    第二版


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

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

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

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

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


    对比测试


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

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

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

    result


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


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

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

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

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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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

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

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

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


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


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


    一.自卑


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


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


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


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


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


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


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


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


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


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


    二.面子


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


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


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


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


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


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


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


    三.认知


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


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


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

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


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


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


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


    ————


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


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


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


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


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


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


    绝非留恋原地!


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

    压缩炸弹,Java怎么防止

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

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


    1.1 什么是压缩炸弹


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


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


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

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

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


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


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


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



    1.2 压缩炸弹会有什么危害


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

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

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

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



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

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

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

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

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



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



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


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


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


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

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

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


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

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

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


    2.2 Java怎么防止压缩炸弹


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


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

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

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


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

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

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

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

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

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


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


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


    先来看看我们实现的思路


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

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

    实现流程说明如下:



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

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

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

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

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

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

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

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

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

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

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

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


    实现代码工具类


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

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

    public class FileBombUtil {

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

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

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

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

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

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

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

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

    }

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

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

    }

    测试类


    import java.io.File;

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

    public class Test {

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

    }

    三、总结


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


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


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


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




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

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

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



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