注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

Android 放大镜窥视效果

前言 放大镜效果是一种常用的局部图片观察效果,其本质原理依然是将原图片放大之后,经过范围裁剪然后会知道指定区域的一种效果。实际上放大效果有2种常见的效果,比如在一些购物网站,鼠标移动到的位置被放大,然后展示在侧边区域,这两者代码几乎一样,主要区别如下: 侧边...
继续阅读 »

前言


放大镜效果是一种常用的局部图片观察效果,其本质原理依然是将原图片放大之后,经过范围裁剪然后会知道指定区域的一种效果。实际上放大效果有2种常见的效果,比如在一些购物网站,鼠标移动到的位置被放大,然后展示在侧边区域,这两者代码几乎一样,主要区别如下:



  • 侧边区域观测要移动Shader或者在指定位置裁剪图像

  • 本文效果是移动区域,但是为了保证图片能尽可能对齐,需要将放大的图片向左上角偏移。


本文和上一篇《手电筒照亮效果》一样,如果没看过的先看上一篇,方便你理解本篇,因为同样的原理不会在这篇重新提及或者过多提及,都是局部区域效果实现。


效果预览


滑动放大效果


fire_62.gif


窥视效果


fire_63.gif


方法镜滑动放大实现方法


使用Shader作为载体


首先要做的是将图片放大,放大之后,我们可以利用Path裁剪图片或者Shader向裁剪区域绘制,这里我们依然使用Shader,毕竟优点很多,这里我们主要要实现2个目的。



  • Shader载入Bitmap,放大1.2倍

  • Shader向左上角偏移,对齐图片中心


      if (shader == null) {
float ratio = 1.2f;
scaledBitmap = Bitmap.createScaledBitmap(mBitmap, (int) (mBitmap.getWidth() * ratio), (int) (mBitmap.getHeight() * ratio), true);
shader = new BitmapShader(scaledBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
// 做下偏移
matrix.setTranslate(-(scaledBitmap.getWidth() - mBitmap.getWidth())/2f ,-(scaledBitmap.getHeight() - mBitmap.getHeight())/2f);
shader.setLocalMatrix(matrix);
}

事件处理


其实处理事件有很多简便的方法,但是首先得拦截事件,Android种拦截事件的方法很多,clickable就是其中之一


setClickable(true); //触发hotspot

拦截按压移动事件,这里我们使用 HotSpot 机制,其实就是触点,西方人命名习惯使用HotSpot,通过下面就能处理事件,连onTouchEvent我们都不用搭理。


  @Override
public void dispatchDrawableHotspotChanged(float x, float y) {
super.dispatchDrawableHotspotChanged(x, y);
this.x = x;
this.y = y;
postInvalidate();
}

@Override
protected void dispatchSetPressed(boolean pressed) {
super.dispatchSetPressed(pressed);
postInvalidate();
}

裁剪Canvas区域为原图区域


为什么要裁剪Canvas区域内,主要是因为你的图片并不一定能完全填充整个View,但是你使用的TileMode肯定是CLAMP,这会使得放大镜中图像的边缘拉长,现象很奇怪,反正你可以去掉试试。另外说一下,Android中似乎新增加了一种TileMode,不过还没来得及试一下。


   int save = canvas.save();
canvas.clipRect(0,0,mBitmap.getWidth(),mBitmap.getHeight());
canvas.restoreToCount(save);

绘制核心逻辑


在核心逻辑中,我们有一步要绘制区域填充颜色,主要原因是非透明区域的绘制会导致出现透视效果。


    int save = canvas.save();
canvas.clipRect(0,0,mBitmap.getWidth(),mBitmap.getHeight());
//绘制原图
canvas.drawBitmap(mBitmap, 0, 0, null);
//区域用填充颜色,防止出现区域透视,上面的区域能看见下面的区域
mCommonPaint.setColor(Color.WHITE);
canvas.drawCircle( x , y,width/4f,mCommonPaint);
//绘制放大效果
mCommonPaint.setShader(shader);
canvas.drawCircle( x , y,width/4f,mCommonPaint);
mCommonPaint.setShader(null);

canvas.restoreToCount(save);

放大镜窥视效果


其实两者代码没有多大区别,滑动放大效果主要是移动镜子,而窥视效果镜子不动,使用移动图片的方式实现。


位置计算 & 绘制


固定镜子中心在右下角


//放大平移时需要偏移的距离
float offsetX = -(scaledBitmap.getWidth() - mBitmap.getWidth()) / 2f;
float offsetY = -(scaledBitmap.getHeight() - mBitmap.getHeight())/2f;
//窥视镜圆心
float mirrorCenterX = mBitmap.getWidth() - width / 4f;
float mirrorCenterY = mBitmap.getHeight() - width/4f;

图像平移距离


(mirrorCenterX - x) 
(mirrorCenterY - y)

矩阵变换,平移事件点位置图像到右下角圆的中心


//(mirrorCenterX - x) ,(mirrorCenterY-y) 是把当前中心点的图像平移到圆心哪里
matrix.setTranslate( offsetX + (mirrorCenterX - x) , offsetY + (mirrorCenterY-y));
shader.setLocalMatrix(matrix);

绘制镜子



int save = canvas.save();
canvas.clipRect(0,0,mBitmap.getWidth(),mBitmap.getHeight());
canvas.drawBitmap(mBitmap, 0, 0, null);
mCommonPaint.setColor(Color.DKGRAY);
canvas.drawCircle(mirrorCenterX , mirrorCenterY,width/4f,mCommonPaint);
mCommonPaint.setShader(shader);
canvas.drawCircle( mirrorCenterX , mirrorCenterY,width/4f,mCommonPaint);
mCommonPaint.setShader(null);

canvas.restoreToCount(save);

总结


本篇和之前的很多篇文章一样,都是实现Canvas图片绘制,很复杂的效果我们没有涉及到,但是在这些文章中,都会有各种各样的问题和思考。总之,我们要善于利用矩阵和设计思想,绘制我们的想象。


全部代码


按照惯例,提供全部代码


滑动放大代码


public class ScaleBigView extends View {
private final DisplayMetrics mDM;
private TextPaint mCommonPaint;
private Bitmap mBitmap;
private Shader shader = null;
private Matrix matrix = new Matrix();
private Bitmap scaledBitmap;

public ScaleBigView(Context context) {
this(context, null);
}

public ScaleBigView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ScaleBigView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mDM = getResources().getDisplayMetrics();
initPaint();
setClickable(true); //触发hotspot
}

private void initPaint() {
//否则提供给外部纹理绘制
mCommonPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
mCommonPaint.setAntiAlias(true);
mCommonPaint.setStyle(Paint.Style.FILL_AND_STROKE);
mCommonPaint.setStrokeCap(Paint.Cap.ROUND);
mCommonPaint.setFilterBitmap(true);
mCommonPaint.setDither(true);
mCommonPaint.setStrokeWidth(dp2px(20));
mBitmap = decodeBitmap(R.mipmap.mm_012);

}

private Bitmap decodeBitmap(int resId) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inMutable = true;
return BitmapFactory.decodeResource(getResources(), resId, options);
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
if (widthMode != MeasureSpec.EXACTLY) {
widthSize = mDM.widthPixels / 2;
}
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);

if (heightMode != MeasureSpec.EXACTLY) {
heightSize = widthSize / 2;
}
setMeasuredDimension(widthSize, heightSize);

}
private float x;
private float y;
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
if (width < 1 || height < 1) {
return;
}
if (shader == null) {
float ratio = 1.2f;
scaledBitmap = Bitmap.createScaledBitmap(mBitmap, (int) (mBitmap.getWidth() * ratio), (int) (mBitmap.getHeight() * ratio), true);
shader = new BitmapShader(scaledBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
matrix.setTranslate(-(scaledBitmap.getWidth() - mBitmap.getWidth())/2f ,-(scaledBitmap.getHeight() - mBitmap.getHeight())/2f);
shader.setLocalMatrix(matrix);
}


int save = canvas.save();
canvas.clipRect(0,0,mBitmap.getWidth(),mBitmap.getHeight());
//绘制原图
canvas.drawBitmap(mBitmap, 0, 0, null);
//区域用填充颜色,防止出现区域透视,上面的区域能看见下面的区域
mCommonPaint.setColor(Color.WHITE);
canvas.drawCircle( x , y,width/4f,mCommonPaint);
//绘制放大效果
mCommonPaint.setShader(shader);
canvas.drawCircle( x , y,width/4f,mCommonPaint);
mCommonPaint.setShader(null);

canvas.restoreToCount(save);
}

@Override
public void dispatchDrawableHotspotChanged(float x, float y) {
super.dispatchDrawableHotspotChanged(x, y);
this.x = x;
this.y = y;
postInvalidate();
}

@Override
protected void dispatchSetPressed(boolean pressed) {
super.dispatchSetPressed(pressed);
postInvalidate();
}

public float dp2px(float dp) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, mDM);
}

}

窥视镜效果


public class ScaleBigView extends View {
private final DisplayMetrics mDM;
private TextPaint mCommonPaint;
private Bitmap mBitmap;
private Shader shader = null;
private Matrix matrix = new Matrix();
private Bitmap scaledBitmap;

public ScaleBigView(Context context) {
this(context, null);
}

public ScaleBigView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ScaleBigView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mDM = getResources().getDisplayMetrics();
initPaint();
setClickable(true); //触发hotspot
}

private void initPaint() {
//否则提供给外部纹理绘制
mCommonPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
mCommonPaint.setAntiAlias(true);
mCommonPaint.setStyle(Paint.Style.FILL_AND_STROKE);
mCommonPaint.setStrokeCap(Paint.Cap.ROUND);
mCommonPaint.setFilterBitmap(true);
mCommonPaint.setDither(true);
mCommonPaint.setStrokeWidth(dp2px(20));
mBitmap = decodeBitmap(R.mipmap.mm_012);

}

private Bitmap decodeBitmap(int resId) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inMutable = true;
return BitmapFactory.decodeResource(getResources(), resId, options);
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
if (widthMode != MeasureSpec.EXACTLY) {
widthSize = mDM.widthPixels / 2;
}
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);

if (heightMode != MeasureSpec.EXACTLY) {
heightSize = widthSize / 2;
}
setMeasuredDimension(widthSize, heightSize);

}
private float x;
private float y;
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
if (width < 1 || height < 1) {
return;
}
if (shader == null) {
float ratio = 1.2f;
scaledBitmap = Bitmap.createScaledBitmap(mBitmap, (int) (mBitmap.getWidth() * ratio), (int) (mBitmap.getHeight() * ratio), true);
shader = new BitmapShader(scaledBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);

}
//放大平移
float offsetX = -(scaledBitmap.getWidth() - mBitmap.getWidth()) / 2f;
float offsetY = -(scaledBitmap.getHeight() - mBitmap.getHeight())/2f;

//窥视镜圆心
float mirrorCenterX = mBitmap.getWidth() - width / 4f;
float mirrorCenterY = mBitmap.getHeight() - width/4f;

//(mirrorCenterX - x) ,(mirrorCenterY-y) 是把当前中心点的图像平移到圆心哪里
matrix.setTranslate( offsetX + (mirrorCenterX - x) , offsetY + (mirrorCenterY-y));
shader.setLocalMatrix(matrix);

int save = canvas.save();
canvas.clipRect(0,0,mBitmap.getWidth(),mBitmap.getHeight());
canvas.drawBitmap(mBitmap, 0, 0, null);
mCommonPaint.setColor(Color.DKGRAY);
canvas.drawCircle(mirrorCenterX , mirrorCenterY,width/4f,mCommonPaint);
mCommonPaint.setShader(shader);
canvas.drawCircle( mirrorCenterX , mirrorCenterY,width/4f,mCommonPaint);
mCommonPaint.setShader(null);

canvas.restoreToCount(save);
}

@Override
public void dispatchDrawableHotspotChanged(float x, float y) {
super.dispatchDrawableHotspotChanged(x, y);
this.x = x;
this.y = y;
postInvalidate();
}

@Override
protected void dispatchSetPressed(boolean pressed) {
super.dispatchSetPressed(pressed);
postInvalidate();
}

public float dp2px(float dp) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, mDM);
}

}

作者:时光少年
来源:juejin.cn/post/7310124656996302874
收起阅读 »

Android 手电筒照亮效果

前言 经常在掘金博客上看到使用前端技术实现的手电筒照亮效果,但是搜了一下android相关的,发现少得很,实际上这种效果在Android上实现会更简单,当然,条条大路通罗马,有很多技术手段去实现这种效果,今天我们选择一种相对比较好的方法来实现。 实现方法梳理 ...
继续阅读 »

前言


经常在掘金博客上看到使用前端技术实现的手电筒照亮效果,但是搜了一下android相关的,发现少得很,实际上这种效果在Android上实现会更简单,当然,条条大路通罗马,有很多技术手段去实现这种效果,今天我们选择一种相对比较好的方法来实现。


实现方法梳理



  • 第一种方法就是利用Path路径进行 Clip Outline,然后绘制不同的渐变效果即可,这种方法其实很适合蒙版切图,不过也能用于实现这种特效。

  • 第二种方法是利用Xfermode 进行中间图层镂空。

  • 第三种方法就是Shader,效率高且无锯齿。


效果


fire_61.gif


实现原理


其实本篇的核心就是Shader了,这次我们也用RadialGradient来实现,本篇几乎没有任何难度,关键技术难点就是Shader 的移动,其实最经典的效果是Facebook实现的光影文案,本质上时Matrix + Shader.setLocalMatrix 实现。


155007_4C1U_2256215.gif


Matrix涉及一些数学问题,Matrix初始化本身就是单位矩阵,几乎每个操作都是乘以另一个矩阵,属于线性代数的基本知识,难度其实并不高。


matrix.setTranslation(1,2) 可以看作,矩阵的乘法无非是行乘列,繁琐事繁琐,但是很容易理解



1,0,0, 1,0,1,
0,1,0, X 0,1,2,
0,0,1 0,0,1


我们来看看经典的facebook 出品代码


public class GradientShaderTextView extends TextView {

private LinearGradient mLinearGradient;
private Matrix mGradientMatrix;
private Paint mPaint;
private int mViewWidth = 0;
private int mTranslate = 0;

private boolean mAnimating = true;
private int delta = 15;
public GradientShaderTextView(Context ctx)
{
this(ctx,null);
}

public GradientShaderTextView(Context context, AttributeSet attrs) {
super(context, attrs);
}

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (mViewWidth == 0) {
mViewWidth = getMeasuredWidth();
if (mViewWidth > 0) {
mPaint = getPaint();
String text = getText().toString();
// float textWidth = mPaint.measureText(text);
int size;
if(text.length()>0)
{
size = mViewWidth*2/text.length();
}else{
size = mViewWidth;
}
mLinearGradient = new LinearGradient(-size, 0, 0, 0,
new int[] { 0x33ffffff, 0xffffffff, 0x33ffffff },
new float[] { 0, 0.5f, 1 }, Shader.TileMode.CLAMP); //边缘融合
mPaint.setShader(mLinearGradient);
mGradientMatrix = new Matrix();
}
}
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);

int length = Math.max(length(), 1);
if (mAnimating && mGradientMatrix != null) {
float mTextWidth = getPaint().measureText(getText().toString());
mTranslate += delta;
if (mTranslate > mTextWidth+1 || mTranslate<1) {
delta = -delta;
}
mGradientMatrix.setTranslate(mTranslate, 0); //自动平移矩阵
mLinearGradient.setLocalMatrix(mGradientMatrix);
postInvalidateDelayed(30);
}
}

}

本文案例


本文要实现的效果其实也是一样的方法,只不过不是自动移动,而是添加了触摸事件,同时加了放大缩小效果。


坑点


Shader 不支持矩阵Scale,本身打算利用Scale缩放光圈,但事与愿违,不仅不支持,连动都动不了了,因此,本文采用了两种Shader,按压时使用较大半径的Shader,手放开时使用默认的Shader。


知识点


canvas.drawPaint(mCommonPaint);

这个绘制并不是告诉你可以这么绘制,而是想说,设置了Shader之后,这样调用,Shader半径之外的颜色时Shader最后一个颜色值,我们最后一个颜色值时黑色,那就是黑色,我们改成白色当然也是白色,下图是改成白色之后的效果,周围都是白色


企业微信20231207-230353@2x.png


关键代码段


super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
if (width < 1 || height < 1) {
return;
}
//大光圈shader
if (radialGradientLarge == null) {
radialGradientLarge = new RadialGradient(0, 0,
dp2px(100),
new int[]{Color.TRANSPARENT, 0x01ffffff, 0x33ffffff, 0x66000000, 0x77000000, 0xff000000},
new float[]{0.1f, 0.3f, 0.5f, 0.7f, 0.9f, 0.95f},
Shader.TileMode.CLAMP);
}
//默认光圈shader
if (radialGradientNormal == null) {
radialGradientNormal = new RadialGradient(0, 0,
dp2px(50),
new int[]{Color.TRANSPARENT, 0x01ffffff, 0x33ffffff, 0x66000000, 0x77000000, 0xff000000},
new float[]{0.1f, 0.3f, 0.5f, 0.7f, 0.9f, 0.95f},
Shader.TileMode.CLAMP);
}

//绘制地图
canvas.drawBitmap(mBitmap, 0, 0, null);

//移动shader中心点
matrix.setTranslate(x, y);
//设置到矩阵
radialGradientLarge.setLocalMatrix(matrix);
radialGradientNormal.setLocalMatrix(matrix);
if(isPressed()) {
//按压时
mCommonPaint.setShader(radialGradientLarge);
}else{
//松开时
mCommonPaint.setShader(radialGradientNormal);
}
//直接用画笔绘制,那么周围的颜色是Shader 最后的颜色
canvas.drawPaint(mCommonPaint);

好了,我们的效果基本实现了。


总结


本篇到这里就截止了,我们今天掌握的知识点是Shader相关的:



  • Shader 矩阵不能Scale

  • 设置完Shader 的画笔外围填充色为Ridial Shader最后的颜色

  • Canvas 可以直接drawPaint

  • Shader.setLocalMatrix是移动Shader中心点的方法


代码


按照惯例,给出全部代码


public class LightsView extends View {
private final DisplayMetrics mDM;
private TextPaint mCommonPaint;
private Bitmap mBitmap;
private RadialGradient radialGradientLarge = null;
private RadialGradient radialGradientNormal = null;
private float x;
private float y;
private boolean isPress = false;
private Matrix matrix = new Matrix();
public LightsView(Context context) {
this(context, null);
}

public LightsView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public LightsView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mDM = getResources().getDisplayMetrics();
initPaint();
setClickable(true); //触发hotspot
}

private void initPaint() {
//否则提供给外部纹理绘制
mCommonPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
mCommonPaint.setAntiAlias(true);
mCommonPaint.setStyle(Paint.Style.FILL_AND_STROKE);
mCommonPaint.setStrokeCap(Paint.Cap.ROUND);
mCommonPaint.setFilterBitmap(true);
mCommonPaint.setDither(true);
mBitmap = decodeBitmap(R.mipmap.mm_06);

}

private Bitmap decodeBitmap(int resId) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inMutable = true;
return BitmapFactory.decodeResource(getResources(), resId, options);
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
if (widthMode != MeasureSpec.EXACTLY) {
widthSize = mDM.widthPixels / 2;
}
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);

if (heightMode != MeasureSpec.EXACTLY) {
heightSize = widthSize / 2;
}
setMeasuredDimension(widthSize, heightSize);

}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
if (width < 1 || height < 1) {
return;
}
if (radialGradientLarge == null) {
radialGradientLarge = new RadialGradient(0, 0,
dp2px(100),
new int[]{Color.TRANSPARENT, 0x01ffffff, 0x33ffffff, 0x66000000, 0x77000000, 0xff000000},
new float[]{0.1f, 0.3f, 0.5f, 0.7f, 0.9f, 0.95f},
Shader.TileMode.CLAMP);
}
if (radialGradientNormal == null) {
radialGradientNormal = new RadialGradient(0, 0,
dp2px(50),
new int[]{Color.TRANSPARENT, 0x01ffffff, 0x33ffffff, 0x66000000, 0x77000000, 0xff000000},
new float[]{0.1f, 0.3f, 0.5f, 0.7f, 0.9f, 0.95f},
Shader.TileMode.CLAMP);
}

canvas.drawBitmap(mBitmap, 0, 0, null);

matrix.setTranslate(x, y);
radialGradientLarge.setLocalMatrix(matrix);
radialGradientNormal.setLocalMatrix(matrix);
if(isPressed()) {
mCommonPaint.setShader(radialGradientLarge);
}else{
mCommonPaint.setShader(radialGradientNormal);
}
canvas.drawPaint(mCommonPaint);
}

@Override
public void dispatchDrawableHotspotChanged(float x, float y) {
super.dispatchDrawableHotspotChanged(x, y);
this.x = x;
this.y = y;
postInvalidate();
}

@Override
protected void dispatchSetPressed(boolean pressed) {
super.dispatchSetPressed(pressed);
postInvalidate();
}

public float dp2px(float dp) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, mDM);
}

}

作者:时光少年
来源:juejin.cn/post/7309687967064817716
收起阅读 »

6G,它来了,真的666!

你好,这里是网络技术联盟站。2023年12月5日至6日,全球6G发展大会在重庆两江新区成功举行。会议汇集了中国两院院士和国内外权威专家,共同探讨了6G的发展愿景和技术产业路径。中国工程院院士张平提出,移动通信系统正处于一个重要的发展阶段,需要从容量、速率、服务...
继续阅读 »

你好,这里是网络技术联盟站。

2023年12月5日至6日,全球6G发展大会在重庆两江新区成功举行。会议汇集了中国两院院士和国内外权威专家,共同探讨了6G的发展愿景和技术产业路径。中国工程院院士张平提出,移动通信系统正处于一个重要的发展阶段,需要从容量、速率、服务和智能等多个维度上进行扩展。他强调,6G的发展应该着重于维度上和智能性上的提升。

张平院士指出,当前通信技术发展面临的挑战包括信息压缩的极限和数据吞吐量的过大,这些因素使得提升通信系统容量性能难以持续。因此,6G时代需要寻找可持续发展的路径,其中人工智能的融合被视为关键因素,有望实现通信技术和人工智能技术的共赢共生。

华为技术有限公司无线CTO童文在主题演讲中强调了人工智能在未来通信技术中的核心作用,预测在未来五年到十年内,大多数研发设计和文字工作将由AI取代。他认为,AI Agent将成为承载6G服务和应用的核心载体。

中国通信标准化协会理事长闻库则提出,6G的愿景不仅仅是提高网速,而是在此基础上实现手机性能比的提升、覆盖的提升和垂直行业的全面拓展。他强调,AI的引入为6G的发展提供了新的可能性,未来的通信将更加注重智能化的创新。

会议还讨论了6G距离落地的距离和后续关键行动,包括6G技术的梳理和验证、6G与5G的衔接、加强5G基础建设、推进5G-A迈向商用,以及加大技术创新力度和强化开放合作。

这次大会的讨论和发言反映了6G技术发展的最新动态和未来方向,展现了中国在6G技术研发方面的积极态度和领先地位。

对于我们从事网络,或者说从事IT行业的人来说,学习6G技术是迫在眉睫!本文瑞哥就带大家好好了解一下6G技术,相信看完本文,一定对您有所帮助!

让我们直接开始!

无线技术演进

无线通信技术的演进是一个持续的过程,每一代技术都在速度、容量、延迟和连接性方面带来了显著的改进。

1G无线网络

  • 时间:20世纪80年代
  • 特点:主要侧重于语音通信,网络速度缓慢。

2G无线网络

  • 时间:1990/91年
  • 特点:带来了一些无线数据服务,如WAP、MMS和SMS,但速度仍然很慢。2G的迭代版本2.5G和2.75G提高了数据速率。

3G无线网络

  • 时间:2004/2005年
  • 特点:增加了视频通话、移动电视、基于位置的服务,以及更快的数据传输速度(8-20Mbps)。随着需求的增加,出现了3.5G和3.75G,带来了移动电子邮件和个人对个人的游戏。

4G无线网络

  • 时间:2009年
  • 特点:能够更快地下载和上传文件,同时处理语音和数据呼叫。4G手机在信号充足的情况下可以快速下载文件。

5G无线网络

  • 时间:2019年
  • 特点:承诺更快的速度、更少的延迟和更多的连接,支持更多设备同时连接。5G网络使许多新产品和物联网传感器能够以极快的移动数据速度在全球范围内连接。

6G无线网络

  • 预计时间:2030年左右
  • 特点:预计将提升5G网络的功能,并提供增强的覆盖范围、改进的功能和超快的移动数据速度。支持全球数十亿个物联网设备,并使人们能够享受虚拟现实(VR)、增强现实(AR)和混合现实(MR)等应用。

每一代无线技术的演进都是为了满足不断增长的数据需求和改善用户体验。从1G到6G,我们见证了无线通信技术的巨大变革,这些变革不仅影响了我们的通信方式,还推动了社会和经济的发展。

下面我们先着重介绍一下4G和5G技术的发展。

4G技术的发展

4G,作为第四代移动通信技术,标志着移动互联网时代的到来。它在速度、容量和连接性方面相比于3G技术有了显著的提升,为用户提供了更快的上网体验和更丰富的移动服务。

4G技术的核心是长期演进(LTE)标准,它提供了更高的数据传输速率和更低的网络延迟。LTE的引入使得移动宽带服务质量得到了显著提升,支持了更高清晰度的视频通话和更快速的数据下载。

MIMO技术通过使用多个天线同时发送和接收数据,显著提高了信号的质量和传输速度。这一技术的应用使得4G网络能够支持更多用户的同时连接,同时提高了网络的稳定性和覆盖范围。

MIMO:多输入多输出。

OFDM技术通过将信号分散到多个频道上,减少了干扰并提高了频谱效率。这一技术的使用使得4G网络能够更有效地利用有限的频谱资源,提供更高的数据传输速率。

OFDM:正交频分复用。

4G特点

  • 高速数据传输:4G网络的数据速率通常在100Mbps到1Gbps之间,使得视频通话和在线游戏等应用变得流畅。这一速度的提升为移动互联网的普及和移动应用的发展提供了强大的动力。
  • 改善的网络覆盖:4G技术通过更高效的频谱利用和网络优化,提供了更广泛的网络覆盖和更好的服务质量。这一改进使得用户即使在移动中也能享受到稳定的网络连接。
  • 支持多种应用:4G网络支持了社交媒体、流媒体服务、云计算等多种新兴应用。这些应用的发展推动了移动互联网的创新和多样化,为用户提供了更丰富的选择和更便捷的服务。

5G技术的突破

5G,即第五代移动通信技术,是继4G之后的又一次重大技术革新。它不仅提供了更快的速度和更低的延迟,还开启了物联网和智能设备的新时代。

5G技术的一个重要特点是使用毫米波频段,这些频段通常在30GHz到300GHz之间。毫米波通信提供了更高的数据速率和更大的带宽,使得5G网络能够支持超高清视频流、虚拟现实和增强现实等带宽密集型应用。

5G网络采用了大规模多输入多输出(MIMO)技术,通过使用更多的天线,显著提高了网络容量和效率。这一技术的应用使得5G网络能够同时服务更多的用户,同时提高了信号的质量和覆盖范围。

5G技术引入了网络切片的概念,允许运营商为不同的服务和应用提供定制化的网络资源。这意味着网络可以根据应用的需求动态分配资源,从而提高效率和性能。

5G特点

  • 极高速度:5G网络的峰值速率可达20Gbps,这是4G网络速率的数倍。这一速度的提升为各种新兴应用提供了强大的支持,包括自动驾驶汽车、远程医疗和智能城市等。
  • 超低延迟:5G网络的延迟低至1毫秒,这对于需要即时响应的应用至关重要。例如,远程手术和工业自动化都需要极低的延迟来确保操作的准确性和安全性。
  • 广泛的连接性:5G技术支持海量的设备连接,这对于物联网的发展至关重要。从智能家居到智能工厂,5G网络能够支持数以亿计的设备同时在线,实现高效的数据交换和控制。

下面进入我们的主角:6G。

从马可尼的无线电信号传输到5G的高速移动通信,每一代技术都在推动社会进步和经济发展。6G预计将是一个跨越性的技术,不仅仅是速度的提升,而是在智能化、感知能力和计算能力上的全面革新。

那么什么是6G呢?

6G

6G是指第六代移动通信技术,是5G的后继者。它被设计为一种更高级、更先进的无线通信技术,旨在提供比5G更快的速度、更低的延迟和更大的网络容量。

6G会是什么样子?

6G有望实现每秒1太比特的极高速度,相较于当前大多数家庭互联网网络可用的最快速度1 Gbps,以及5G的最高速度10 Gbps,有了显著的提升。

6G可能会利用太赫兹波或亚毫米波的频段,这能够提供更高的频谱,支持更大的带宽,进一步增强网络性能。这也可能解决5G中毫米波距离短、需要视线的问题。

6G将更加依赖人工智能,实现协同工作,特别是在自动驾驶汽车、工厂自动化等方面。边缘计算的应用将使网络更本地化,减少响应时间,提高协同效率。

6G有望支持更高级别的沉浸式技术,包括虚拟现实、细胞表面、植入物和无线脑机接口。这将为用户提供更身临其境的体验,推动智能可穿戴设备和植入物的发展。

6G可能会实现物理生活与网络空间的完全融合,通过可穿戴设备和植入在人体上的微型设备,实时支持人类的思想和行动。

6G工作原理(可能)

6G的一个关键特征可能是利用超高频率传输数据。这包括在数百千兆赫(GHz)或甚至太赫兹(THz)范围内进行通信。这将提供更大的带宽和更高的数据传输速度。

6G可能会采用先进的技术,以提高频谱的利用效率。通过使用复杂的数学方法,6G网络可以在同一频率上实现同时发送和接收,从而提高频谱的传输效率。

6G可能会采用网状网络架构,将设备连接到彼此,形成一个分布式的网络。这样的架构可以提供更好的覆盖范围和更高的可靠性,同时支持设备之间的直接通信。

6G可能会引入新的互联网协议(New IP),以提高网络的效率和性能。这可能包括一种新型的IP数据包,具有更多导航和优先级信息,以支持更智能、自适应的网络通信。

6G可能会根据材料的原子和分子对特定波长的吸收和发射频率进行选择性的波长利用。这样的技术可以优化信号传输,并考虑到不同材料对电磁辐射的特定响应。

6G频段的使用

预计6G网络的最新先锋频谱将主要位于中频段,即7 GHz到20 GHz之间。这个频段的使用将通过极端的多输入多输出(MIMO)技术提供更大的容量。中频段的特点是提供相对较高的数据传输速率,并在城市室外小区中发挥重要作用。

对于广泛的覆盖范围,6G将继续利用低频段,预计在460 MHz到694 MHz之间。低频段通常用于提供更广泛的覆盖范围和更好的穿透能力,尤其是在城市和 室内环境中。

6G计划利用次太赫兹频谱,这是一个非常高的频段。这将实现超过100 Gbps的峰值数据速度,为未来对高速数据传输需求极高的应用提供支持。

6G将广泛使用新的光谱范围,包括高达太赫兹的频段。这将推动本地化技术到新的水平,提高定位的精确度,并为各种应用场景带来新的可能性。

6G将通过使用广泛的频谱范围,特别是高频段,显着提高定位精度,达到厘米级的水平。这将对各种应用,包括导航、位置服务和物联网设备的定位,产生积极影响。

6G特点

  1. 超高数据速率: 6G的一个主要目标是实现前所未有的数据速率。这意味着网络将能够提供比当前标准更高的下载和上传速度,以支持高清内容的无缝传输,同时满足未来对更大带宽需求的应用,如虚拟现实(VR)和增强现实(AR)。

5G的峰值数据吞吐量通常被设计为在20 Gbps左右,而6G将迈向更令人惊叹的1 Tbps(太比特每秒)的峰值数据速率。这是对比5G速度的显著提升,为未来的高度数据密集型应用提供了更大的带宽。

5G旨在提供用户体验数据速率为100 Mbps,而6G将将这一速率提高到1 Gbps(千兆比特每秒)。这将使用户能够更快地下载和上传数据,以支持更高质量的多媒体服务和应用。

由于更高的频谱效率,6G将比5G提高近一倍以上的速度。这种效率的提升对于满足未来对大容量、高速度连接的需求至关重要,尤其是在处理大规模视频、虚拟现实、增强现实等数据密集型应用时。

  1. 超低延迟: 6G致力于实现超低延迟,即数据从一个网络点传输到另一个点所需的时间。这对于需要即时响应的实时应用非常关键,例如自动驾驶汽车、远程手术和工业自动化。预计延迟将减少到毫秒甚至微秒的水平。

5G的设计目标是将时延降至1毫秒。这对于许多实时应用程序,如增强现实、虚拟现实和自动驾驶汽车等来说,已经是一个显著的提升。然而,6G将进一步将用户体验到的延迟降低到0.1毫秒以下,实现更加极致的超低延迟。

由于延迟的大幅减少,许多实时应用程序将获得更好的性能和功能。这对于需要即时响应的应用场景,如在线游戏、实时视频通话和远程协作等,将带来明显的改进。

超低延迟将使网络能够实现更迅速的紧急响应。这对于紧急情况下的通信和救援操作非常重要,例如在自然灾害发生时,网络可以更快速、更有效地协调救援活动。

  1. 海量连接: 6G旨在支持物联网(IoT)中预计将连接数十亿台设备的海量连接。为实现这一目标,网络需要进一步发展,以应对大规模设备之间的通信、数据传输和管理。

6G将更加专注于支持机器对机器的连接,强调物联网(IoT)和各种设备之间的通信。这对于未来智能城市、智能工厂、智能交通系统等应用来说至关重要,其中大量设备需要相互协调和通信。

  1. 能源效率: 可持续性是当前和未来网络发展的一个重要考虑因素。6G的设计将更加注重能源效率,通过优化网络基础设施和采用智能电源管理技术,以减少能源消耗,同时保持高性能连接。
  2. 人工智能集成: 6G将与人工智能(AI)密切结合,通过利用AI算法和机器学习技术来实现智能网络管理、资源分配和优化。这种集成有望提高网络的整体性能和效率,使其更具自适应性和智能性。

6G的优势

  1. 更快、更可靠的数据速度: 6G将实现更高的数据传输速度,为企业和消费者提供更快速、更可靠的互联网连接。这将促使新的应用和服务,如实时的3D全息视频流、超高清虚拟现实等。
  2. 更低的延迟: 6G将具有更低的延迟,即数据传输的时间将进一步缩短。这对于需要实时通信的应用非常关键,例如远程手术、虚拟会议和自动驾驶汽车等。
  3. 更广泛的设备和应用: 6G将支持更广泛范围的设备和应用,包括物联网中的大量连接设备、智能城市中的各种传感器和控制系统,以及新兴的技术领域,如增强现实和虚拟现实。
  4. 提高安全性和性能: 6G将利用人工智能和机器学习来提高网络的安全性和性能。这将增强网络的自我学习和自适应性,使其更具抵御网络攻击的能力,同时确保网络能够处理未来6G预计增加的大规模数据流量。
  5. 推动新兴技术和应用: 6G的引入将推动各种新兴技术和应用的发展,包括智能交通、医疗创新、工业自动化和元宇宙等。这将为社会带来更多创新和便利。

6G的潜在应用

  1. 实时全息视频会议: 6G的超高速度和低延迟将使实时全息视频会议成为可能。用户可以感觉到与对方面对面交流,这对于企业协作、在线教育和虚拟团队合作具有重要意义。

  1. 超高清虚拟现实(VR): 6G的高带宽和低延迟将推动超高清虚拟现实体验的发展。这将改善虚拟旅游、虚拟培训和虚拟游戏等领域的用户体验。
  2. 自动驾驶汽车: 6G的超低延迟对于自动驾驶汽车至关重要。实时通信将使汽车能够相互协作,共享实时交通和道路信息,提高自动驾驶汽车的安全性和效率。
  3. 远程手术: 6G的低延迟和高带宽将为远程手术提供支持。外科医生可以远程操控手术机器人进行手术,为无法到达医院的患者提供及时的医疗服务。
  4. 工业物联网(IIoT): 6G的大容量和广泛连接性将促进工业物联网的发展。在工业领域,各种传感器和设备可以实时通信,实现智能制造和工业自动化。
  5. 智能城市: 6G将为智能城市提供支持,实现各种城市基础设施的智能化管理,包括交通系统、能源管理、环境监测等。
  6. 医疗创新: 6G有望推动医疗领域的创新,包括远程医疗服务、医疗数据实时传输和医疗设备的互联互通,提高医疗保健的效率和可及性。

6G发展面临哪些挑战?

  1. 新频谱的需求: 为了实现更高的数据速率和容量,6G需要利用新的频谱范围。然而,目前可用的射频频段有限,而且这些频段通常由政府监管机构进行分配。因此,确保有足够的频谱来支持6G是一个重要的挑战。
  2. 新技术的发展: 6G的实现将依赖于一系列新技术的发展,包括太赫兹通信、新的无线电接入技术以及人工智能和机器学习在网络管理中的应用。这些技术目前仍在研发阶段,需要时间来完善和商业化。
  3. 提高安全性: 随着网络的发展,安全性变得尤为关键。6G需要更高水平的安全性,以应对日益复杂和普及的网络攻击。确保用户数据的隐私和网络的稳定性是一个必须解决的挑战。
  4. 部署成本: 6G的部署成本预计将比5G更高。引入新的频段和技术,以及更新现有的基础设施,都需要巨大的资金投入。这可能涉及到国家和企业层面的资金支持,以确保6G网络的建设和推广。
  5. 国际合作和标准制定: 6G的发展需要国际合作,以确保全球范围内的一致性和互操作性。同时,制定一系列统一的国际标准也是一个关键挑战,以便不同厂商的设备和网络可以无缝地协同工作。

5G与6G频谱比较

  1. 最大频率:
  • 5G:100 GHz
  • 6G:10太赫兹
  1. 最大带宽:
  • 5G:1 GHz
  • 6G:100 GHz
  1. 峰值数据速率:
  • 5G:10 Gbps(上传链路)至20 Gbps(下载链路)
  • 6G:100 Gbps至1 Tbps
  1. 平均用户体验数据速率:
  • 5G:100 Mbps
  • 6G:1 Gbps
  1. 峰值频谱效率:
  • 5G:30 b/s/Hz
  • 6G:60 b/s/Hz
  1. 用户体验的平均频谱效率:
  • 5G:0.03 b/s/Hz
  • 6G:3 b/s/Hz
  1. 移动支持:
  • 5G:最高500公里/小时
  • 6G:最高1000公里/小时
  1. 密度:
  • 5G:每平方米1台设备
  • 6G:每平方米100个设备
  1. 端到端延迟:
  • 5G:1至10毫秒
  • 6G:少于1毫秒
  1. 单频全双工传输:
  • 5G:没有
  • 6G:有
  1. 全球覆盖:
  • 5G:70多个国家已经推出5G,其中中国和美国在城市中处于领先地位
  • 6G:中国申请的6G专利最多,其次是美国

6G的商业化时间表

  • 标准制定:业内预计6G标准和规范的制定将从2025年开始¹。
  • 部署时间:预计6G系统将在2028年左右开始部署。
  • 商业化:预计6G的商业部署将在2030年左右实现³。

这些时间表是基于当前的技术发展和预测,可能会随着研究进展和行业动态而有所调整。6G技术的商业化还需要克服许多技术和政策上的挑战,包括频谱分配、网络架构设计、安全性和隐私保护等方面。

哪些公司正在领导6G技术研发?

全球有多家公司正在积极参与6G技术的研发,比如华为 (Huawei)、中兴通讯(ZTE)、中国移动、中国电信、中国联通、三星 (Samsung)、LG、NTT DOCOMO、高通 (Qualcomm)、AT&T、诺基亚 (Nokia)、爱立信 (Ericsson)等等。

中国针对6G做出的行动

中国政府在“第14次五年计划(2021-2025年)及2035年远景目标纲要”中明确提出了发展6G技术的目标²。

中国还成立了IMT-2030(6G)推进组,由主要通信运营商、基础设施供应商、IT公司和研究机构等约80家企业组成,致力于6G技术的研发和标准化工作。6G推进组已经发布了《6G网络架构展望》和《6G无线系统设计原则和典型特征》等技术方案,这些方案旨在为6G技术从万物互联向万物智联的转变提供技术路径。

中国工业和信息化部已宣布正在有序开展6G相关的技术试验,以推动6G创新发展。加快5G与XR、数字孪生、机器人等新产业新应用的融合发展,加速相关产业成熟,夯实6G应用基础。此外,推动信息通信企业与垂直行业企业密切沟通、协同合作,共同参与6G需求研究、技术研发、标准制定等全流程各环节,携手构建6G繁荣应用生态。

中国计划在2024年前完成6G相关主要技术的明确和概念机的测试验证,以提升技术能力。

预计到2026年,中国将开展典型应用场景和性能指标的确立,进行试制机的研发和基站功能性能的验证。

中国计划在2030年左右实现6G的商用化,而标准化制定的时间预计将在2025年。6G技术将引入新的应用场景,如通信与感知的结合、通信与人工智能的结合,以及泛在物联网等。这些技术不仅将连接人类,还将连接智能体,如机器人和元宇宙,进一步完善5G在行业中尚未解决的场景。

中国正在加强国际合作,与欧洲6G智慧网络和业务产业协会(6G-IA)、韩国6G论坛、印度通信标准开发协会(TSDSI)等签署合作备忘录,共同推进6G技术的发展。

此外,中国的通信巨头如华为和中国移动也在积极参与6G技术的研究和开发工作。华为是首家宣布开始研究6G的中国公司,随后与其他国内外企业和研究机构展开了多项合作。

据市场研究机构Market Research Future预计,到2040年,全球6G市场规模将超过3400亿美元,年复合增长率达58.1%。中国预计将成为全球最大的6G市场之一,全球近50%的6G专利申请来自中国。

总结

6G技术与5G相比,在速度上有显著的提升。根据研究和预测,6G的理论最高速度可达到1Tbps(即1000Gbps),这比5G的理论最高速度20Gbps快了50倍。此外,有报道称在中国的实验室环境中已经实现了206.25Gbps的速度。

6G将使用比5G更高的频率波段,操作在30GHz到300GHz的毫米波段,甚至可能达到300GHz到3000GHz的辐射波段。这些更高的频率波段将允许更快的数据传输速度和更大的带宽容量。

中国在6G技术的研发和创新方面正加速推进,预计在2030年左右实现商用。中国工业和信息化部已经指导成立了6G推进组,旨在为6G创新发展提供政策支持,并推动形成全球统一的6G标准。

6G技术不仅仅是速度的提升,它还将服务于社会管理和治理,以及智能体的应用。6G网络预计将是一个地面无线与卫星通信集成的全连接世界,不仅比5G更快、更可靠,还将推动移动通信与人工智能、感知、计算等技术的跨领域融合发展。

中国已经开始进行6G技术试验,并陆续开展了关于6G系统架构和技术方案的研究。最近,中国6G推进组发布了相关技术方案,为6G从万物互联走向万物智联提供了技术路径。

6G时代的基站将不仅支持通信信号的发送和接收,还将支持通信和感知,利用无线电波感知周边环境、物体形状和运动等,这不仅能提升通信性能,还将催生新业务。例如,基站可以进行升级改造,以支持低空经济和空域管理,或者用于交通管理。

6G将促进沉浸感更强的全息视频,实现物理世界、虚拟世界、人的世界三个世界的联动。今年6月,国际电信联盟完成了6G愿景需求建议书,明确了6G典型产品和关键能力指标,其中中国提出的5类6G典型场景和14个关键能力指标全部被采纳。

这些进展表明,中国在6G技术的发展上正处于全球领先地位,积极推进技术研发和创新,为未来的通信技术和应用开辟新的可能性。

朋友们,让我们一起期待中国在6G领域继续“雄霸全球”吧!


作者:wljslmz
来源:juejin.cn/post/7310143510102540297
收起阅读 »

Linus:批评 GitHub 代码合并【毫无用处的】

Linux 和 Git 的创建者 Linus Torvalds 批评 GitHub 创造了“毫无用处的代码合并”。 Torvalds 的评论可以在 Linux 开发邮件列表的存档中查看,该评论针对的是 Paragon Software 的创始人兼首席执行官 K...
继续阅读 »


Linux 和 Git 的创建者 Linus Torvalds 批评 GitHub 创造了“毫无用处的代码合并”。


Torvalds 的评论可以在 Linux 开发邮件列表的存档中查看,该评论针对的是 Paragon Software 的创始人兼首席执行官 Konstantin Komarov,关于为即将到来的 5.15 内核提交其读写 NTFS 驱动程序。


Torvalds 说,GitHub 创建了绝对无用的垃圾合并,你永远不应该使用 GitHub 接口来合并任何东西。


早在 2012 年,Torvalds 就对他为什么不使用 GitHub 进行拉取请求给出了更详细的解释:



GitHub 会丢弃所有相关信息,比如应该为要求我拉取的人提供一个有效的电子邮件地址。diffstat 也是有缺陷和无用的。




Git 附带了一个不错的拉取请求生成模块,但 github 决定用他们自己的完全劣质的版本替换它。因此,我认为 github 对此太无能了。托管很好,但拉取请求和在线提交编辑只是纯粹的垃圾。



Paragon Software 提交的驱动程序提高了与本机 Windows 文件系统 NTFS 的互操作性。提交过程在一年多前就开始了,但面临投诉,称其 27,000 行代码太大而无法审查。


提交了较小的块,但很明显,Paragon 一直在努力掌握 Linux 内核开发过程。最终 Torvalds 介入并在此过程中提供指导。


7 月,Torvalds 指出,与其将代码发布到 fsdevel 列表中,不如最终将其作为实际的拉取请求提交。


当时,Paragon 回应说:“也感谢您的澄清。直到现在,我们才真正清楚这个信息。我们刚刚发送了第 27 个补丁系列,它修复了针对当前 linux-next 的可构建性。在将拉取请求发送给您之前,我们需要几天时间来准备适当的拉取请求“。


这似乎比预期的要长一些,但 Paragon 于 2021 年 9 月 3 日星期五提交了拉取请求。该公司表示,“当前版本适用于普通/压缩/稀疏文件,并支持 acl、NTFS 日志重播。


除了建议不要使用 GitHub 的接口进行合并之外,Torvalds 还表示——虽然这次他会让它通过——拉取请求应该已经签署。


Torvalds 认为在一个完美的世界里,这将是一个 PGP 签名,可以通过信任链直接追溯到你。


最后拉取请求被合并,Torvalds 也作了最终评论。


Torvalds 认为最初的拉取往往有一些奇怪的地方,他现在会接受它们,为了继续发展,他需要正确地做事。


作者:ENG八戒
来源:juejin.cn/post/7312293783973675008
收起阅读 »

7个Js async/await高级用法

web
7个Js async/await高级用法 JavaScript的异步编程已经从回调(Callback)演进到Promise,再到如今广泛使用的async/await语法。后者不仅让异步代码更加简洁,而且更贴近同步代码的逻辑与结构,大大增强了代码的可读性与可维护...
继续阅读 »

7个Js async/await高级用法


JavaScript的异步编程已经从回调(Callback)演进到Promise,再到如今广泛使用的async/await语法。后者不仅让异步代码更加简洁,而且更贴近同步代码的逻辑与结构,大大增强了代码的可读性与可维护性。在掌握了基础用法之后,下面将介绍一些高级用法,以便充分利用async/await实现更复杂的异步流程控制。


1. async/await与高阶函数


当需要对数组中的元素执行异步操作时,可结合async/await与数组的高阶函数(如mapfilter等)。


// 异步过滤函数
async function asyncFilter(array, predicate) {
const results = await Promise.all(array.map(predicate));

return array.filter((_value, index) => results[index]);
}

// 示例
async function isOddNumber(n) {
await delay(100); // 模拟异步操作
return n % 2 !== 0;
}

async function filterOddNumbers(numbers) {
return asyncFilter(numbers, isOddNumber);
}

filterOddNumbers([1, 2, 3, 4, 5]).then(console.log); // 输出: [1, 3, 5]

2. 控制并发数


在处理诸如文件上传等场景时,可能需要限制同时进行的异步操作数量以避免系统资源耗尽。


async function asyncPool(poolLimit, array, iteratorFn) {
const result = [];
const executing = [];

for (const item of array) {
const p = Promise.resolve().then(() => iteratorFn(item, array));
result.push(p);

if (poolLimit <= array.length) {
const e = p.then(() => executing.splice(executing.indexOf(e), 1));
executing.push(e);
if (executing.length >= poolLimit) {
await Promise.race(executing);
}
}
}

return Promise.all(result);
}

// 示例
async function uploadFile(file) {
// 文件上传逻辑
}

async function limitedFileUpload(files) {
return asyncPool(3, files, uploadFile);
}

3. 使用async/await优化递归


递归函数是编程中的一种常用技术,async/await可以很容易地使递归函数进行异步操作。


// 异步递归函数
async function asyncRecursiveSearch(nodes) {
for (const node of nodes) {
await asyncProcess(node);
if (node.children) {
await asyncRecursiveSearch(node.children);
}
}
}

// 示例
async function asyncProcess(node) {
// 对节点进行异步处理逻辑
}

4. 异步初始化类实例


在JavaScript中,类的构造器(constructor)不能是异步的。但可以通过工厂函数模式来实现类实例的异步初始化。


class Example {
constructor(data) {
this.data = data;
}

static async create() {
const data = await fetchData(); // 异步获取数据
return new Example(data);
}
}

// 使用方式
Example.create().then((exampleInstance) => {
// 使用异步初始化的类实例
});

5. 在async函数中使用await链式调用


使用await可以直观地按顺序执行链式调用中的异步操作。


class ApiClient {
constructor() {
this.value = null;
}

async firstMethod() {
this.value = await fetch('/first-url').then(r => r.json());
return this;
}

async secondMethod() {
this.value = await fetch('/second-url').then(r => r.json());
return this;
}
}

// 使用方式
const client = new ApiClient();
const result = await client.firstMethod().then(c => c.secondMethod());

6. 结合async/await和事件循环


使用async/await可以更好地控制事件循环,像处理DOM事件或定时器等场合。


// 异步定时器函数
async function asyncSetTimeout(fn, ms) {
await new Promise(resolve => setTimeout(resolve, ms));
fn();
}

// 示例
asyncSetTimeout(() => console.log('Timeout after 2 seconds'), 2000);

7. 使用async/await简化错误处理


错误处理是异步编程中的重要部分。通过async/await,可以将错误处理的逻辑更自然地集成到同步代码中。


async function asyncOperation() {
try {
const result = await mightFailOperation();
return result;
} catch (error) {
handleAsyncError(error);
}
}

async function mightFailOperation() {
// 有可能失败的异步操作
}

function handleAsyncError(error) {
// 错误处理逻辑
}

通过以上七个async/await的高级用法,开发者可以在JavaScript中以更加声明式和直观的方式处理复杂的异步逻辑,同时保持代码整洁和可维护性。在实践中不断应用和掌握这些用法,能够有效地提升编程效率和项目的质量。


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

推送数据?也许你不需要 WebSocket

web
提到推送数据,大家可能会首先想到 WebSocket。 确实,WebSocket 能双向通信,自然也能做服务器到浏览器的消息推送。 但如果只是单向推送消息的话,HTTP 就有这种功能,它就是 Server Send Event。 WebSocket 的通信过程...
继续阅读 »

提到推送数据,大家可能会首先想到 WebSocket。


确实,WebSocket 能双向通信,自然也能做服务器到浏览器的消息推送。


但如果只是单向推送消息的话,HTTP 就有这种功能,它就是 Server Send Event。


WebSocket 的通信过程是这样的:



首先通过 http 切换协议,服务端返回 101 的状态码后,就代表协议切换成功。


之后就是 WebSocket 格式数据的通信了,一方可以随时向另一方推送消息。


而 HTTP 的 Server Send Event 是这样的:



服务端返回的 Content-Type 是 text/event-stream,这是一个流,可以多次返回内容。


Sever Send Event 就是通过这种消息来随时推送数据。


可能你是第一次听说 SSE,但你肯定用过基于它的应用。


比如你用的 CICD 平台,它的日志是实时打印的。


那它是如何实时传输构建日志的呢?


明显需要一段一段的传输,这种一般就是用 SSE 来推送数据。


再比如说 ChatGPT,它回答一个问题不是一次性给你全部的,而是一部分一部分的加载回答。


这也是基于 SSE。




知道了什么是 SSE 以及它的应用,我们来自己实现一下吧:


创建 nest 项目:


npx nest new sse-test


把它跑起来:


npm run start:dev


访问 http://localhost:3000 可以看到 hello world,代表服务器跑成功了:



然后在 AppController 添加一个 stream 接口:



这里不是通过 @Get、@Post 等装饰器标识,而是通过 @Sse 标识这是一个 event stream 类型的接口。


@Sse('stream')
stream() {
return new Observable((observer) => {
observer.next({ data: { msg: 'aaa'} });

setTimeout(() => {
observer.next({ data: { msg: 'bbb'} });
}, 2000);

setTimeout(() => {
observer.next({ data: { msg: 'ccc'} });
}, 5000);
});
}

返回的是一个 Observable 对象,然后内部用 observer.next 返回消息。


可以返回任意的 json 数据。


我们先返回了一个 aaa、过了 2s 返回了 bbb,过了 5s 返回了 ccc。


然后写个前端页面:


创建一个 react 项目:


npx create-react-app --template=typescript sse-test-frontend


在 App.tsx 里写如下代码:


import { useEffect } from 'react';

function App() {

useEffect(() => {
const eventSource = new EventSource('http://localhost:3000/stream');
eventSource.onmessage = ({ data }) => {
console.log('New message', JSON.parse(data));
};
}, []);

return (
<div>hello</div>
);
}

export default App;

这个 EventSource 是浏览器原生 api,就是用来获取 sse 接口的响应的,它会把每次消息传入 onmessage 的回调函数。


我们在 nest 服务开启跨域支持:



然后把 react 项目 index.tsx 里这几行代码删掉,它会导致额外的渲染:



执行 npm run start


因为 3000 端口被占用了,它会跑在 3001:



浏览器访问下:



看到一段段的响应了没?


这就是 Server Send Event。


在 devtools 里可以看到,响应的 Content-Type 是 text/event-stream:



然后在 EventStream 里可以看到每一次收到的消息:



这样,服务端就可以随时向网页推送消息了。


那它兼容性怎么样呢?


可以在 MDN 看到:



除了 ie、edge 外,其他浏览器都没任何兼容问题。


基本是可以放心用的。


那用在哪呢?


一些只需要服务端推送的场景就特别适合 Server Send Event。


比如这个站内信:



这种推送用 WebSocket 就没必要了,可以用 SSE 来做。


那连接断了怎么办呢?


不用担心,浏览器会自动重连。


这点和 WebSocket 不同,WebSocket 如果断开之后是需要手动重连的,而 SSE 不用。


再比如说日志的实时推送。


我们来测试下:


tail -f 命令可以实时看到文件的最新内容:



我们通过 child_process 模块的 exec 来执行这个命令,然后监听它的 stdout 输出:


const { exec } = require("child_process");

const childProcess = exec('tail -f ./log');

childProcess.stdout.on('data', (msg) => {
console.log(msg);
});

用 node 执行它:



然后添加一个 sse 的接口:


@Sse('stream2')
stream2() {
const childProcess = exec('tail -f ./log');

return new Observable((observer) => {
childProcess.stdout.on('data', (msg) => {
observer.next({ data: { msg: msg.toString() }});
})
});

监听到新的数据之后,把它返回给浏览器。


浏览器连接这个新接口:



测试下:



可以看到,浏览器收到了实时的日志。


很多构建日志都是通过 SSE 的方式实时推送的。


日志之类的只是文本,那如果是二进制数据呢?


二进制数据在 node 里是通过 Buffer 存储的。


const { readFileSync } = require("fs");

const buffer = readFileSync('./package.json');

console.log(buffer);


而 Buffer 有个 toJSON 方法:



这样不就可以通过 sse 的接口返回了么?


试一下:


@Sse('stream3')
stream3() {
return new Observable((observer) => {
const json = readFileSync('./package.json').toJSON();
observer.next({ data: { msg: json }});
});
}



确实可以。


也就是说,基于 sse,除了可以推送文本外,还可以推送任意二进制数据。


总结


服务端实时推送数据,除了用 WebSocket 外,还可以用 HTTP 的 Server Send Event。


只要 http 返回 Content-Type 为 text/event-stream 的 header,就可以通过 stream 的方式多次返回消息了。


它传输的是 json 格式的内容,可以用来传输文本或者二进制内容。


我们通过 Nest 实现了 sse 的接口,用 @Sse 装饰器标识方法,然后返回 Observe 对象就可以了。内部可以通过 observer.next 随时返回数据。


前端使用 EventSource 的 onmessage 来接收消息。


这个 api 的兼容性很好,除了 ie 外可以放心的用。


它的应用场景有很多,比如站内信、构建日志实时展示、chatgpt 的消息返回等。


再遇到需要消息推送的场景,不要直接 WebSocket 了,也许 Server Send Event 更合适呢?


作者:zxg_神说要有光
来源:juejin.cn/post/7272564663116759074
收起阅读 »

只会Vue的我,用两天学会了react,这个方法您也可以

web
背景 由于工作需要学习react框架;最开始看文档的时候感觉还挺难的。但当我看了半天文档以后才发现,原来react这样学才是最快的;前提是同学们会vue一类的框架哈。 该方法适用于会vue的同学们食用 我们在学习以前先去想一想,在vue中我们常用的方法是什么,...
继续阅读 »

背景


由于工作需要学习react框架;最开始看文档的时候感觉还挺难的。但当我看了半天文档以后才发现,原来react这样学才是最快的;前提是同学们会vue一类的框架哈。


该方法适用于会vue的同学们食用


我们在学习以前先去想一想,在vue中我们常用的方法是什么,我们遇到一些场景时在vue中是怎么做的。


当我们想到这儿的时候就会发现,对啊;既然vue是这样做的,那么react中是怎么做的呢?别急,我们一步一步对比着来。


这样岂不是更能理解哦!下面就让我们开始吧!


冲冲冲。。。


Vue梳理


在开始之前,我们先来梳理一下我们在vue中常用的API或者场景有哪些。


以下这几种就是我们常见的一些功能,主要是列表渲染、表单输入和一些计算属性等等;我们只需要根据原有的需要的功能去学习即可。



  • 组件传值

  • 获取DOM

  • 列表渲染

  • 条件渲染

  • class

  • 计算属性

  • 监听器

  • 表单输入

  • 模板


vue/react对比学习


组件传值


vue


// 父组件
<GoodsList v-if="!isGoodsIdShow" :goodsList="goodsList"/>
// 子组件 -- 通过props获取即可
props: {
goodsList:{
type:Array,
default:function(){
return []
}
}
}

react


// 父组件
export default function tab(props:any) {
const [serverUrl, setServerUrl] = useState<string | undefined>('https://');
console.log(props);
// 父组件接收子组件的值并修改
const changeMsg = (msg?:string) => {
setServerUrl(msg);
};

return(
<View className='tab'>
<View className='box'>
<TabName msg={serverUrl} changeMsg={changeMsg} />
</View>
</View>

)
}

// 子组件
function TabName(props){
console.log('props',props);
// 子传父
const handleClick = (msg:string) => {
props.changeMsg(msg);
};
return (
<View>
<Text>{props.msg}</Text>
<Button onClick={()=>{handleClick('77777')}}>测试</Button>
</View>

);
};

获取DOM


vue


this.$refs['ref']

react


// 声明ref    
const domRef = useRef<HTMLInputElement>(null);
// 通过点击事件选择input框
const handleBtnClick = ()=> {
domRef.current?.focus();
console.log(domRef,'domRef')
}

return(
<View className='home'>
<View className='box'>
<Input ref={domRef} type="text" />
<button onClick={handleBtnClick}>增加</button>
</View>
</View>

)

列表渲染


vue


<div v-for="(item, index) in mealList" :key="index">
{{item}}
</div>

react


//声明对象类型
type Coordinates = {
name:string,
age:number
};
// 对象
let [userState, setUserState] = useState<Coordinates>({ name: 'John', age: 30 });
// 数组
let [list, setList] = useState<Coordinates[]>([{ name: '李四', age: 30 }]);

// 如果你的 => 后面跟了一对花括号 { ,那你必须使用 return 来指定返回值!
const listItem = list.map((oi)=>{
return <View key={oi.age}>{oi.name}</View>
});

return (
{
list.map((oi)=>{
return <Text className='main-list-title' key={oi.age}>{oi.name}</Text>
})
}
<View>{ listItem }</View>
</View>
)

条件渲染


计算属性


vue


computed: {
userinfo() {
return this.$store.state.userinfo;
},
},

react


const [serverUrl, setServerUrl] = useState('https://localhost:1234');
let [age, setAge] = useState(2);

const name = useMemo(() => {
return serverUrl + " " + age;
}, [serverUrl]);
console.log(name) // https://localhost:1234 2

监听器


vue


watch: {
// 保证自定义菜单始终显示在页面中
customContextmenuTop(top) {
...相关操作
}
},

react


import { useEffect, useState } from 'react';

export default function home() {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
const [age, setAge] = useState(2);

/**
* useEffect第二个参数中所传递的值才会进行根据值的变化而出发;
* 如果没有穿值的话,就不会监听数据变化
*/

useEffect(()=>{
if (age !== 5) {
setAge(++age)
}
},[age])

useEffect(()=>{
if(serverUrl !== 'w3c') {
setServerUrl('w3c');
}
},[serverUrl])

return(78)
}

总结


从上面的方法示例我们可以得出一个结论:在其他框架(自己会的)中常用到的方法或者场景进行针对性的学习即可。


这样的好处是你能快速的上手开发,然后在实际开发场景中遇到解决不了的问题再去查文档或者百度。


这只是我的一点小小的发现,哈哈哈。。。


如果对你有感触的话,可以尝试一下这个方法;我觉得还是很不错的


注意:react推荐函数式组件开发,不推荐类组件开发,我在上面没有说明,大家也可以去文档看看,类组件和函数组件还是有很大差别的,如:函数组件没有生命周期,一般使用监听来完成的,监听的使用方法还是有所不同,大家可以具体的去试试,我在这儿也是告诉大家一些方法;具体去学了才是你的。


为了方便自己学习记录,以及给大家提供思路,我下期给大家带来 vite + ts + react的搭建


作者:雾恋
来源:juejin.cn/post/7268844150233219107
收起阅读 »

一个大专生工作总结

2020刚入大学,学的云计算方向专业,也成功当时班级的学委,从那时候开始各种学习计算机像方面技术,学的比较杂,感觉啥都学。运维类的redhat centos7 ,前端web,最基础的 HTML、CSS、JavaScript,后端就从比较感兴趣的JAVA开始学,...
继续阅读 »

2020刚入大学,学的云计算方向专业,也成功当时班级的学委,从那时候开始各种学习计算机像方面技术,学的比较杂,感觉啥都学。运维类的redhat centos7 ,前端web,最基础的 HTML、CSS、JavaScript,后端就从比较感兴趣的JAVA开始学,学到后面越学越学不动,然后转战Python,学会了爬虫,从这时候开始疯狂在网络上找资源学,疯狂阅览互联网走势,还有各种好玩的技术。刚开始学计算机的应该都会想过以后当一名黑客吧。学习大半个学期逐渐熟悉了互联网大致内容了,发现自己不适合。学不动根本学不动,觉得自己只能朝一个方向发展了。


大一


自己也很不错,也非常爱学,也拿到了人生第一个奖学金,老师都对我印象也挺不错的,大一学的专业课大部分我都会,老师提出的问题,我都能一一回答上来,还能讲一两个解决方式。我们老师还好奇还问我你学过吗,我就说基本都学过,底下同学都投来羡慕眼光,那是应该算在我在班级第一次高光时刻。


img_v2_2f1640ed-5668-4b27-a440-97259463424g.gif


大二上


大二开始迷失自我了,开始翘课和兄弟出去玩耍喝酒,那时候我也追上我喜欢的女孩,也成了男女朋友,就开始不对学习感兴趣,天天除了玩就是玩。接下来就是期末考试直线下滑,辅导员开始找我谈话了,讲不少人生大道理,虽然我没听进去多少,但是我还是知道不能再继续颓废下去了。


v2_0285f304-80f0-4eac-8f9d-e36b3cf6635g.gif


大二下


开始思考人生了,觉得上了大学应该不留遗憾,开始考各类计算机证书:网络工程师中级证书、HCIA、HCIP、云计算中级证书,等之类没有含金量证书。当时准备冲击红帽认证后来疫情原因,也不让出校,就没有冲击欲望了。就参加计算机比赛去了,我记得当时总共参加了三个比赛,院系一等、B类二等、我最期望的A类比赛我苦学了大半个学期,天天待在机房里学,因为这个比赛东道主在我们学校举行,懂了都懂,大差不差也能拿省一,省一可以免试专升本。然后可以去一本高校读本科,因为疫情取消了,什么都取消了。


心里虽然不是个滋味!人生还是要继续的。


v2_4720fea9-0838-4295-bbbb-8254eaa782bg.jpg


大三


专科生大学基本都是2+1,两年在学校,半年实习,才能拿到毕-业-证!就这样2022年10月开始思考是否专升本问题,思来想去两种方案:假如考上还要继续上俩年大学,第二种早点进入社会工作不断提高自己工作经验,也能搞到大钱。我还是选择了第二种方案,开始写自己简历,然后疯狂在BOSS 智联招聘 全程无忧等招聘平台疯狂投送简历,然后就有一家比较大的企业看上我了,应聘的是网络运维工程师,实习4k转正5k、双休、包吃包住、免费住人才公寓,不快不慢就实习了6个月,也拿到了毕-业-证书,最后转正签劳动合同的时候还是选择了离开。
原因还是:工作学不到东西,加上工作挺舒坦的,每天基本没事,基本都是活少聊天多,就是这样。人是有欲望的,身边的好多朋友转正之后薪资7K-9K的,感觉自己不能再继续荒废下去了。


v2_c9054330-c8f5-4f29-8ce2-0306f9c9903g.jpg


辞职后,也存了一点钱,也玩了一个多月,开始找工作,互联网工作真的难找,加上我现在不是实习生了,对自己薪资要求也比较高,我就开始疯狂的学,也要拿出自己能出的手东西,就自己做了个人网站博客,买了服务器,买了域名。可是呢还是找不到工作,我就开始不找网络工程师方面工作了,简历到处投,直到有家比较大公司桌面运维工作找到了我。经过一两轮面试,合格通过了,但是薪资也谈的不太理想只有6k。
三线城市6K确实足够生活的,不过我还是要继续努力。helpdesk只是我的暂时的工作,还是要更高方向发展。


加油 加油 加油 !!!!


v2_0cc05c0e-8aec-41f8-b500-32c49e76270g.jpg


作者:一码归亿码
来源:juejin.cn/post/7312352526706524201
收起阅读 »

京东一面:post为什么会发送两次请求?🤪🤪🤪

在前段时间的一次面试中,被问到了一个如标题这样的问题。要想好好地去回答这个问题,这里牵扯到的知识点也是比较多的。 那么接下来这篇文章我们就一点一点开始引出这个问题。 同源策略 在浏览器中,内容是很开放的,任何资源都可以接入其中,如 JavaScript 文件、...
继续阅读 »

在前段时间的一次面试中,被问到了一个如标题这样的问题。要想好好地去回答这个问题,这里牵扯到的知识点也是比较多的。


那么接下来这篇文章我们就一点一点开始引出这个问题。


同源策略


在浏览器中,内容是很开放的,任何资源都可以接入其中,如 JavaScript 文件、图片、音频、视频等资源,甚至可以下载其他站点的可执行文件。


但也不是说浏览器就是完全自由的,如果不加以控制,就会出现一些不可控的局面,例如会出现一些安全问题,如:



  • 跨站脚本攻击(XSS)

  • SQL 注入攻击

  • OS 命令注入攻击

  • HTTP 首部注入攻击

  • 跨站点请求伪造(CSRF)

  • 等等......


如果这些都没有限制的话,对于我们用户而言,是相对危险的,因此需要一些安全策略来保障我们的隐私和数据安全。


这就引出了最基础、最核心的安全策略:同源策略。


什么是同源策略


同源策略是一个重要的安全策略,它用于限制一个源的文档或者它加载的脚本如何能与另一个源的资源进行交互。


如果两个 URL 的协议、主机和端口都相同,我们就称这两个 URL 同源。



  • 协议:协议是定义了数据如何在计算机内和之间进行交换的规则的系统,例如 HTTP、HTTPS。

  • 主机:是已连接到一个计算机网络的一台电子计算机或其他设备。网络主机可以向网络上的用户或其他节点提供信息资源、服务和应用。使用 TCP/IP 协议族参与网络的计算机也可称为 IP 主机。

  • 端口:主机是计算机到计算机之间的通信,那么端口就是进程到进程之间的通信。


如下表给出了与 URL http://store.company.com:80/dir/page.html 的源进行对比的示例:


URL结果原因
http://store.company.com:80/dir2/page.html同源只有路径不同
http://store.company.com:80/dir/inner/another.html同源只有路径不同
https://store.company.com:443/secure.html不同源协议不同,HTTP 和 HTTPS
http://store.company.com:81/dir/etc.html不同源端口不同
http://news.company.com:80/dir/other.html不同源主机不同

同源策略主要表现在以下三个方面:DOM、Web 数据和网络。



  • DOM 访问限制:同源策略限制了网页脚本(如 JavaScript)访问其他源的 DOM。这意味着通过脚本无法直接访问跨源页面的 DOM 元素、属性或方法。这是为了防止恶意网站从其他网站窃取敏感信息。

  • Web 数据限制:同源策略也限制了从其他源加载的 Web 数据(例如 XMLHttpRequest 或 Fetch API)。在同源策略下,XMLHttpRequest 或 Fetch 请求只能发送到与当前网页具有相同源的目标。这有助于防止跨站点请求伪造(CSRF)等攻击。

  • 网络通信限制:同源策略还限制了跨源的网络通信。浏览器会阻止从一个源发出的请求获取来自其他源的响应。这样做是为了确保只有受信任的源能够与服务器进行通信,以避免恶意行为。


出于安全原因,浏览器限制从脚本内发起的跨源 HTTP 请求,XMLHttpRequest 和 Fetch API,只能从加载应用程序的同一个域请求 HTTP 资源,除非使用 CORS 头文件


CORS


对于浏览器限制这个词,要着重解释一下:不一定是浏览器限制了发起跨站请求,也可能是跨站请求可以正常发起,但是返回结果被浏览器拦截了。


浏览器将不同域的内容隔离在不同的进程中,网络进程负责下载资源并将其送到渲染进程中,但由于跨域限制,某些资源可能被阻止加载到渲染进程。如果浏览器发现一个跨域响应包含了敏感数据,它可能会阻止脚本访问这些数据,即使网络进程已经获得了这些数据。CORB 的目标是在渲染之前尽早阻止恶意代码获取跨域数据。



CORB 是一种安全机制,用于防止跨域请求恶意访问跨域响应的数据。渲染进程会在 CORB 机制的约束下,选择性地将哪些资源送入渲染进程供页面使用。



例如,一个网页可能通过 AJAX 请求从另一个域的服务器获取数据。虽然某些情况下这样的请求可能会成功,但如果浏览器检测到请求返回的数据可能包含恶意代码或与同源策略冲突,浏览器可能会阻止网页访问返回的数据,以确保用户的安全。


跨源资源共享(Cross-Origin Resource Sharing,CORS)是一种机制,允许在受控的条件下,不同源的网页能够请求和共享资源。由于浏览器的同源策略限制了跨域请求,CORS 提供了一种方式来解决在 Web 应用中进行跨域数据交换的问题。


CORS 的基本思想是,服务器在响应中提供一个标头(HTTP 头),指示哪些源被允许访问资源。浏览器在发起跨域请求时会先发送一个预检请求(OPTIONS 请求)到服务器,服务器通过设置适当的 CORS 标头来指定是否允许跨域请求,并指定允许的请求源、方法、标头等信息。


简单请求


不会触发 CORS 预检请求。这样的请求为 简单请求,。若请求满足所有下述条件,则该请求可视为 简单请求



  1. HTTP 方法限制:只能使用 GET、HEAD、POST 这三种 HTTP 方法之一。如果请求使用了其他 HTTP 方法,就不再被视为简单请求。

  2. 自定义标头限制:请求的 HTTP 标头只能是以下几种常见的标头:AcceptAccept-LanguageContent-LanguageLast-Event-IDContent-Type(仅限于 application/x-www-form-urlencodedmultipart/form-datatext/plain)。HTML 头部 header field 字段:DPR、Download、Save-Data、Viewport-Width、WIdth。如果请求使用了其他标头,同样不再被视为简单请求。

  3. 请求中没有使用 ReadableStream 对象。

  4. 不使用自定义请求标头:请求不能包含用户自定义的标头。

  5. 请求中的任意 XMLHttpRequestUpload 对象均没有注册任何事件监听器;XMLHttpRequestUpload 对象可以使用 XMLHttpRequest.upload 属性访问


预检请求


非简单请求的 CORS 请求,会在正式通信之前,增加一次 HTTP 查询请求,称为 预检请求


需预检的请求要求必须首先使用 OPTIONS 方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。预检请求 的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。


例如我们在掘金上删除一条沸点:


20230822094049


它首先会发起一个预检请求,预检请求的头信息包括两个特殊字段:



  • Access-Control-Request-Method:该字段是必须的,用来列出浏览器的 CORS 请求会用到哪些 HTTP 方法,上例是 POST。

  • Access-Control-Request-Headers:该字段是一个逗号分隔的字符串,指定浏览器 CORS 请求会额外发送的头信息字段,上例是 content-type,x-secsdk-csrf-token

  • access-control-allow-origin:在上述例子中,表示 https://juejin.cn 可以请求数据,也可以设置为* 符号,表示统一任意跨源请求。

  • access-control-max-age:该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是 1 天(86408 秒),即允许缓存该条回应 1 天(86408 秒),在此期间,不用发出另一条预检请求。


一旦服务器通过了 预检请求,以后每次浏览器正常的 CORS 请求,就都跟简单请求一样,会有一个 Origin 头信息字段。服务器的回应,也都会有一个 Access-Control-Allow-Origin 头信息字段。


20230822122441


上面头信息中,Access-Control-Allow-Origin 字段是每次回应都必定包含的。


附带身份凭证的请求与通配符


在响应附带身份凭证的请求时:



  • 为了避免恶意网站滥用 Access-Control-Allow-Origin 头部字段来获取用户敏感信息,服务器在设置时不能将其值设为通配符 *。相反,应该将其设置为特定的域,例如:Access-Control-Allow-Origin: https://juejin.cn。通过将 Access-Control-Allow-Origin 设置为特定的域,服务器只允许来自指定域的请求进行跨域访问。这样可以限制跨域请求的范围,避免不可信的域获取到用户敏感信息。

  • 为了避免潜在的安全风险,服务器不能将 Access-Control-Allow-Headers 的值设为通配符 *。这是因为不受限制的请求头可能被滥用。相反,应该将其设置为一个包含标头名称的列表,例如:Access-Control-Allow-Headers: X-PINGOTHER, Content-Type。通过将 Access-Control-Allow-Headers 设置为明确的标头名称列表,服务器可以限制哪些自定义请求头是允许的。只有在允许的标头列表中的头部字段才能在跨域请求中被接受。

  • 为了避免潜在的安全风险,服务器不能将 Access-Control-Allow-Methods 的值设为通配符 *。这样做将允许来自任意域的请求使用任意的 HTTP 方法,可能导致滥用行为的发生。相反,应该将其设置为一个特定的请求方法名称列表,例如:Access-Control-Allow-Methods: POST, GET。通过将 Access-Control-Allow-Methods 设置为明确的请求方法列表,服务器可以限制哪些方法是允许的。只有在允许的方法列表中的方法才能在跨域请求中被接受和处理。

  • 对于附带身份凭证的请求(通常是 Cookie),


这是因为请求的标头中携带了 Cookie 信息,如果 Access-Control-Allow-Origin 的值为 *,请求将会失败。而将 Access-Control-Allow-Origin 的值设置为 https://juejin。cn,则请求将成功执行。


另外,响应标头中也携带了 Set-Cookie 字段,尝试对 Cookie 进行修改。如果操作失败,将会抛出异常。


为什么本地使用 webpack 进行 dev 开发时,不需要服务器端配置 cors 的情况下访问到线上接口?


当你在本地通过 Ajax 或其他方式请求线上接口时,由于浏览器的同源策略,会出现跨域的问题。但是在服务器端并不会出现这个问题。


它是通过 Webpack Dev Server 来实现这个功能。当你在浏览器中发送请求时,请求会先被 Webpack Dev Server 捕获,然后根据你的代理规则将请求转发到目标服务器,目标服务器返回的数据再经由 Webpack Dev Server 转发回浏览器。这样就绕过了浏览器的同源策略限制,使你能够在本地开发环境中访问线上接口。


参考文章



总结


预检请求是在进行跨域资源共享 CORS 时,由浏览器自动发起的一种 OPTIONS 请求。它的存在是为了保障安全,并允许服务器决定是否允许跨域请求。


跨域请求是指在浏览器中向不同域名、不同端口或不同协议的资源发送请求。出于安全原因,浏览器默认禁止跨域请求,只允许同源策略。而当网页需要进行跨域请求时,浏览器会自动发送一个预检请求,以确定是否服务器允许实际的跨域请求。


预检请求中包含了一些额外的头部信息,如 Origin 和 Access-Control-Request-Method 等,用于告知服务器实际请求的方法和来源。服务器收到预检请求后,可以根据这些头部信息,进行验证和授权判断。如果服务器认可该跨域请求,将返回一个包含 Access-Control-Allow-Origin 等头部信息的响应,浏览器才会继续发送实际的跨域请求。


使用预检请求机制可以有效地防范跨域请求带来的安全风险,保护用户数据和隐私。


整个完整的请求流程有如下图所示:


20230822122544


最后分享两个我的两个开源项目,它们分别是:



这两个项目都会一直维护的,如果你也喜欢,欢迎 star 🥰🥰🥰


作者:Moment
来源:juejin.cn/post/7269952188927017015
收起阅读 »

技术总监写的十个方法,让我精通了lambda表达式

前公司的技术总监写了工具类,对Java Stream 进行二次封装,使用起来非常爽,全公司都在用。 我自己照着写了一遍,改了名字,分享给大家。 一共整理了10个工具方法,可以满足 Collection、List、Set、Map 之间各种类型转化。例如 将 C...
继续阅读 »

前公司的技术总监写了工具类,对Java Stream 进行二次封装,使用起来非常爽,全公司都在用。


我自己照着写了一遍,改了名字,分享给大家。


一共整理了10个工具方法,可以满足 Collection、List、Set、Map 之间各种类型转化。例如



  1. Collection<OrderItem> 转化为 List<OrderItem>

  2. Collection<OrderItem> 转化为 Set<OrderItem>

  3. List<OrderItem> 转化为 List<Long>

  4. Set<OrderItem> 转化为 Set<Long>

  5. Collection<OrderItem> 转化为 List<Long>

  6. Collection<OrderItem> 转化为 Set<Long>

  7. Collection<OrderItem>中提取 Key, Map 的 Value 就是类型 OrderItem

  8. Collection<OrderItem>中提取 Key, Map 的 Value 根据 OrderItem 类型进行转化。

  9. Map<Long, OrderItem> 中的value 转化为 Map<Long, Double>

  10. value 转化时,lamada表达式可以使用(v)->{}, 也可以使用 (k,v)->{ }


Collection 集合类型到 Map类型的转化。


Collection 转化为 Map


由于 List 和 Set 是 Collection 类型的子类,所以只需要实现Collection 类型转化为 Map 类型即可。
Collection转化为 Map 共分两个方法



  1. Collection<OrderItem> Map<Key, OrderItem>,提取 Key, Map 的 Value 就是类型 OrderItem

  2. Collection<OrderItem>Map<Key,Value> ,提取 Key, Map 的 Value 根据 OrderItem 类型进行转化。


使用样例


代码示例中把Set<OrderItem> 转化为 Map<Long, OrderItem>Map<Long ,Double>


@Test
public void testToMap() {
Collection<OrderItem> collection = coll;
Set<OrderItem> set = toSet(collection);

Map<Long, OrderItem> map = toMap(set, OrderItem::getOrderId);
}

@Test
public void testToMapV2() {
Collection<OrderItem> collection = coll;
Set<OrderItem> set = toSet(collection);

Map<Long, Double> map = toMap(set, OrderItem::getOrderId, OrderItem::getActPrice);
}

代码展示


public static <T, K> Map<K, T> toMap(Collection<T> collection, Function<? super T, ? extends K> keyMapper) {
return toMap(collection, keyMapper, Function.identity());
}

public static <T, K, V> Map<K, V> toMap(Collection<T> collection,
Function<? super T, ? extends K> keyFunction,
Function<? super T, ? extends V> valueFunction)
{
return toMap(collection, keyFunction, valueFunction, pickSecond());
}

public static <T, K, V> Map<K, V> toMap(Collection<T> collection,
Function<? super T, ? extends K> keyFunction,
Function<? super T, ? extends V> valueFunction,
BinaryOperator<V> mergeFunction)
{
if (CollectionUtils.isEmpty(collection)) {
return new HashMap<>(0);
}

return collection.stream().collect(Collectors.toMap(keyFunction, valueFunction, mergeFunction));
}

public static <T> BinaryOperator<T> pickFirst() {
return (k1, k2) -> k1;
}
public static <T> BinaryOperator<T> pickSecond() {
return (k1, k2) -> k2;
}

Map格式转换


转换 Map 的 Value



  1. 将 Map<Long, OrderItem> 中的value 转化为 Map<Long, Double>

  2. value 转化时,lamada表达式可以使用(v)->{}, 也可以使用 (k,v)->{ }。


测试样例


@Test
public void testConvertValue() {
Collection<OrderItem> collection = coll;
Set<OrderItem> set = toSet(collection);

Map<Long, OrderItem> map = toMap(set, OrderItem::getOrderId);

Map<Long, Double> orderId2Price = convertMapValue(map, item -> item.getActPrice());
Map<Long, String> orderId2Token = convertMapValue(map, (id, item) -> id + item.getName());

}

代码展示


public static <K, V, C> Map<K, C> convertMapValue(Map<K, V> map, 
BiFunction<K, V, C> valueFunction,
BinaryOperator<C> mergeFunction)
{
if (isEmpty(map)) {
return new HashMap<>();
}
return map.entrySet().stream().collect(Collectors.toMap(
e -> e.getKey(),
e -> valueFunction.apply(e.getKey(), e.getValue()),
mergeFunction
));
}

public static <K, V, C> Map<K, C> convertMapValue(Map<K, V> originMap, BiFunction<K, V, C> valueConverter) {
return convertMapValue(originMap, valueConverter, Lambdas.pickSecond());
}

public static <T> BinaryOperator<T> pickFirst() {
return (k1, k2) -> k1;
}
public static <T> BinaryOperator<T> pickSecond() {
return (k1, k2) -> k2;
}

集合类型转化


Collection 和 List、Set 的转化



  1. Collection<OrderItem> 转化为 List<OrderItem>

  2. Collection<OrderItem> 转化为 Set<OrderItem>


public static <T> List<T> toList(Collection<T> collection) {
if (collection == null) {
return new ArrayList<>();
}
if (collection instanceof List) {
return (List<T>) collection;
}
return collection.stream().collect(Collectors.toList());
}

public static <T> Set<T> toSet(Collection<T> collection) {
if (collection == null) {
return new HashSet<>();
}
if (collection instanceof Set) {
return (Set<T>) collection;
}
return collection.stream().collect(Collectors.toSet());
}

测试样例


@Test//将集合 Collection 转化为 List
public void testToList() {
Collection<OrderItem> collection = coll;
List<OrderItem> list = toList(coll);
}

@Test//将集合 Collection 转化为 Set
public void testToSet() {
Collection<OrderItem> collection = coll;
Set<OrderItem> set = toSet(collection);
}

List和 Set 是 Collection 集合类型的子类,所以无需再转化。


List、Set 类型之间的转换


业务中有时候需要将 List<A> 转化为 List<B>。如何实现工具类呢?


public static <T, R> List<R> map(List<T> collection, Function<T, R> mapper) {
return collection.stream().map(mapper).collect(Collectors.toList());
}

public static <T, R> Set<R> map(Set<T> collection, Function<T, R> mapper) {
return collection.stream().map(mapper).collect(Collectors.toSet());
}

public static <T, R> List<R> mapToList(Collection<T> collection, Function<T, R> mapper) {
return collection.stream().map(mapper).collect(Collectors.toList());
}

public static <T, R> Set<R> mapToSet(Collection<T> collection, Function<T, R> mapper) {
return collection.stream().map(mapper).collect(Collectors.toSet());
}

测试样例



  1. List<OrderItem> 转化为 List<Long>

  2. Set<OrderItem> 转化为 Set<Long>

  3. Collection<OrderItem> 转化为 List<Long>

  4. Collection<OrderItem> 转化为 Set<Long>


@Test
public void testMapToList() {
Collection<OrderItem> collection = coll;
List<OrderItem> list = toList(coll);

List<Long> orderIdList = map(list, (item) -> item.getOrderId());
}

@Test
public void testMapToSet() {
Collection<OrderItem> collection = coll;
Set<OrderItem> set = toSet(coll);

Set<Long> orderIdSet = map(set, (item) -> item.getOrderId());
}

@Test
public void testMapToList2() {
Collection<OrderItem> collection = coll;

List<Long> orderIdList = mapToList(collection, (item) -> item.getOrderId());
}

@Test
public void testMapToSetV2() {
Collection<OrderItem> collection = coll;

Set<Long> orderIdSet = mapToSet(collection, (item) -> item.getOrderId());

}

总结一下 以上样例包含了如下的映射场景



  1. Collection<OrderItem> 转化为 List<OrderItem>

  2. Collection<OrderItem> 转化为 Set<OrderItem>

  3. List<OrderItem> 转化为 List<Long>

  4. Set<OrderItem> 转化为 Set<Long>

  5. Collection<OrderItem> 转化为 List<Long>

  6. Collection<OrderItem> 转化为 Set<Long>

  7. Collection<OrderItem>中提取 Key, Map 的 Value 就是类型 OrderItem

  8. Collection<OrderItem>中提取 Key, Map 的 Value 根据 OrderItem 类型进行转化。

  9. Map<Long, OrderItem> 中的value 转化为 Map<Long, Double>

  10. value 转化时,lamada表达式可以使用(v)->{}, 也可以使用 (k,v)->{ }


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

看不了电视直播了?那就自己做一个(一)

web
事情的起因是这样的,前两天打开电视看直播,突然变成了下面这个画面。 开始以为是太久没更新了,想着重新安装一下就好了。结果上网一查才知道,电视直播软件都下架了。 电视直播只能安装有线,或者通过央视频手机收看了。有线暂时安装不了,用手机看又总是感觉很别扭。 虽...
继续阅读 »

事情的起因是这样的,前两天打开电视看直播,突然变成了下面这个画面。


j29zRYWy.jpeg


开始以为是太久没更新了,想着重新安装一下就好了。结果上网一查才知道,电视直播软件都下架了。


电视直播只能安装有线,或者通过央视频手机收看了。有线暂时安装不了,用手机看又总是感觉很别扭。


a71ea8d3fd1f4134205611b2199935ccd0c85e21.png


虽然也可以通过投屏的方式用电视播放,但切换频道时还得使用手机操作,非常的麻烦。


广电的这波操作出发点可能是好的,后续应该会提供其他收看方式,但是目前这个真空期着实有点尴尬。思来想去,干脆自己动手做一个吧


就是干.jpg


经过一个周末的折腾,过程虽然有一点点曲折,但总算是完成了第一个电视版本,感觉和以前相比,清晰度还不错,切换也更流畅。


111.gif


再来看一下卫视。


也是OK的


22 (1).gif


电视端有了,又突然想着把它放到手机上,虽然手机上可以直接使用央视频播放,但还是有点繁琐,于是又稍微做了一些调整,推出了一个手机端的版本,切换还是相当的丝滑。


手机.gif


最后我其实还改了一版在电脑上使用的,但是这个除了摸鱼好像也没别的用处,所以对于我来说意义不大。


实现篇


接下来说一下具体的实现,会涉及到一些编程相关的内容,如果不感兴趣可以直接跳到结尾。


客户端应用开发


这一篇先介绍客户端的应用的开发,主要就是安卓应用的开发。虽然以前没有这方面经验,但是想法有了,剩下的交给chatGpt就好了。


1. 播放器


首先是播放器的选择,一开始我采用了原生MediaPlayer,主要是考虑到跟各版本安卓系统的兼容性会好一点,而且它使用起来非常的简单,十几行代码就搞定了。


public class PlayActivity extends Activity {
ChannelService channelService;
VideoView videoView;

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
channelService = new HttpChannelService();
setContentView(R.layout.activity_play);
videoView = findViewById(R.id.video_view);
channelService.loadChannels((success, message) -> runOnUiThread(() -> {
Channel channel = channelService.getDefaultChannel();
videoView.setVideoURI(Uri.parse(channel.getSource()));
videoView.start();
}));
}
}

后来替换成了谷歌开源的ExoPlayer,因为只是简单使用,所以代码基本上也没什么差别。


public class PlayActivity extends Activity {
ChannelService channelService;
private StyledPlayerView videoView;
private ExoPlayer player;

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
initView();
initChannels();
}

private void initView() {
setContentView(R.layout.activity_play);
videoView = findViewById(R.id.video_view);
player = new ExoPlayer.Builder(this).build();
videoView.setPlayer(player);
}

private void initChannels() {
channelService = new HttpChannelService();
channelService.loadChannels((boolean success, String message) -> runOnUiThread(() -> {
ChannelService.Channel channel = channelService.getDefaultChannel();
play(channel);
}));
}

private void play(Channel channel){
player.setMediaItem(MediaItem.fromUri(channel.getSource()));
player.prepare();
player.setPlayWhenReady(true);
}
}

2. 监听器


接下来就是考虑对遥控器按键的监听处理,对于这个应用而言,只需要监控方向键以及退出键就好了,当然也可以根据需要对菜单键或者确定键进行响应。


videoView.setOnKeyListener((view, keyCode, event) -> {
switch (keyCode) {
// 向下操作处理 切换下一个频道
case KeyEvent.KEYCODE_DPAD_DOWN:
if (event.getAction() == KeyEvent.ACTION_DOWN) {
Channel channel = channelService.getNextChannel();
play(channel);
return true;
}
break;
}
return false;
});

3. 视频源管理


把ChannelService留到最后讲,是因为它的作用通过下面的接口定义就一目了然了。


这里之所以要定义成接口,比如常见的通过m3u获取,是因为考虑到视频源可能有不同实现,关于实现部分会在下一篇详细讲解。


public interface ChannelService {

/**
* 加载频道
*/

void loadChannels(LoadCallBack callBack);

/**
* 获取默认频道
*/

Channel getDefaultChannel();

/**
* 获取下一个频道
*/

Channel getNextChannel();

/**
* 获取前一个频道
*/

Channel getPrevChannel();

}

4. 手机版


手机版因为没有了遥控器,所以需要对触屏动作进行监听来对视频进行操控,主要就是左右滑动的切换,以及上下滑动的音量调节等。


结语


因为是即兴的创作,也没有打算能长久使用,所以很多细节我并没有考虑,比如内容的缓存,节目回看,网络监控等等这些。但是这几天使用下来体验还是挺不错的。后续我可以把整个源码开放出来,大家有兴趣可以自行去补充。


到这里客户端的实现就讲完了,下一篇再讲一下其他部分的实现。


作者:双子小匠
来源:juejin.cn/post/7311961893610995748
收起阅读 »

带圆角的虚线边框?CSS 不在话下

今天,我们来看这么一个非常常见的切图场景,我们需要一个带圆角的虚线边框,像是这样: 这个我们使用 CSS 还是可以轻松解决的,代码也很简单,核心代码: div { border-radius: 25px; border: 2px dashed...
继续阅读 »

今天,我们来看这么一个非常常见的切图场景,我们需要一个带圆角的虚线边框,像是这样:



这个我们使用 CSS 还是可以轻松解决的,代码也很简单,核心代码:


div {
border-radius: 25px;
border: 2px dashed #aaa;
}

但是,原生的 dashed 有一个问题,就是我们无法控制虚线的单段长度与间隙


假设,我们要这么一个效果呢虚线效果呢:



此时,由于无法控制 border: 2px dashed #aaa 产生的虚线的单段长度与线段之间的间隙,border 方案就不再适用了。


那么,在 CSS 中,我们还有其它方式能够实现带圆角,且虚线的单段长度与线段之间间隙可控的方式吗?


本文,我们就一起探讨探讨。


实现不带圆角的虚线效果


上面的场景,使用 CSS 实现起来比较麻烦的地方在于,图形有一个 border-radius


如果不带圆角,我们可以使用渐变,很容易的模拟虚线效果。


我们可以使用线性渐变,轻松的模拟虚线的效果:


div {
width: 150px;
height: 100px;
background: linear-gradient(90deg, #333 50%, transparent 0) repeat-x;
background-size: 4px 1px;
background-position: 0 0;
}

看看,使用渐变模拟的虚线如下:



解释一下上面的代码:



  1. linear-gradient(90deg, #333 50%, transparent 0),实现一段渐变内容,100% - 50% 的内容是 #333 颜色,剩下的一半 50% - 0 的颜色是透明色 transprent

  2. repeat-x 表示只在 x 方向重复

  3. background-size: 4px 1px 表示上述渐变内容的长宽分别是 4px\ 1px,这样配合 repeat-x就能实现只有 X 方向的重复

  4. 最后的 background-position: 0 0 控制渐变的定位


因此,我们只需要修改 background 的参数,就可以得到各种不一样的虚线效果:



完整的代码,你可以戳这里:CodePen Demo -- Linear-gradient Dashed Effect


并且,渐变是支持多重渐变的,因此,我们把容器的 4 个边都用渐变表示即可:


div {
background:
linear-gradient(90deg, #333 50%, transparent 0) repeat-x,
linear-gradient(90deg, #333 50%, transparent 0) repeat-x,
linear-gradient(0deg, #333 50%, transparent 0) repeat-y,
linear-gradient(0deg, #333 50%, transparent 0) repeat-y;
background-size: 4px 1px, 4px 1px, 1px 4px, 1px 4px;
background-position: 0 0, 0 100%, 0 0, 100% 0;
}

效果如下:



但是,如果要求的元素带 border-radius 圆角,这个方法就不好使了,整个效果就会穿帮。


因此,在有圆角的情况下,我们就需要另辟蹊径。


利用渐变实现带圆角的虚线效果


当然,本质上我们还是需要借助渐变效果,只是,我们需要转换一下思路。


譬如,我们可以使用角向渐变。


假设,我们有这么一个带圆角的元素:


<div>div>

div {
width: 300px;
height: 200px;
background: #eee;
border-radius: 20px;
}

效果如下:



如果我们修改内部的 background: #eee,把它替换成重复角向渐变的这么一个图形:


div {
//...
- background: #eee;
+ background: repeating-conic-gradient(#000, #000 3deg, transparent 3deg, transparent 6deg);
}

解释一下,这段代码创建了一个重复的角向渐变背景,从黑色(#000)开始,每 3deg 变为透明,然后再从透明到黑色,以此循环重复。


此时,这样的背景效果可用于创建一种渐变黑色到透明的重复纹理效果:



在这个基础上,我们只需要给这个图形上层,再利用伪元素,叠加一层颜色,就得到了我们想要的边框效果,并且,边框间隙和大小可以简单调整。


完整的代码:


div {
position: relative;
width: 300px;
height: 200px;
border-radius: 20px;
background: repeating-conic-gradient(#000, #000 3deg, transparent 3deg, transparent 6deg);

&::before {
content: "";
position: absolute;
inset: 1px;
background: #eee;
border-radius: 20px;
}
}

效果如下:



乍一看,效果还不错。但是如果仔细观察,会发现有一个致命问题:虚线线段的每一截长度不一致


只有当图形的高宽一致时,线段长度才会一致。高宽比越远离 1,差异则越大:



完整的代码,你可以戳这里:CodePen Demo -- BorderRadius Dashed Border


那有没有办法让虚线长度能够保持一样呢?


可以!我们再换一种渐变,我们改造一下底下的角向渐变,重新利用重复线性渐变:


div {
border-radius: 20px;
background:
repeating-linear-gradient(
-45deg,
#000 0,
#000 7px,
transparent 7px,
transparent 10px
);
}

此时,我们能得到这样一个斜 45° 的重复线性渐变图形:



与上面方法一类似,再通过在这个图形的基础上,在元素中心,叠加多一层纯色遮罩图形,只漏出最外围一圈的图形,带圆角的虚线边框就实现了:



此方法比上面第一种渐变方法更好之处在于,虚线每一条线段的长度是固定的!是不是非常的巧妙?


完整的代码,你可以戳这里:CodePen Demo -- BorderRadius Dashed Border


最佳解决方案:SVG


当然,上面使用 CSS 实现带圆角的虚线边框,还是需要一定的 CSS 功底。


并且,不管是哪个方法,都存在一定的瑕疵。譬如如果希望边框中间不是背景色,而是镂空的,上述两种 CSS 方式都将不再使用。


因此,对于带圆角的虚线边框场景,最佳方式一定是 SVG。(切图也算是吧,但是灵活度太低)


只是很多人看到 SVG 会天然的感到抗拒,或者认为 SVG 不太好掌握。


所以,本文再介绍一个非常有用的开源工具 -- Customize your CSS Border



通过这个开源工具,我们可以快速生成我们想要的虚线边框效果,并且一键复制可以嵌入到 CSS background 中的 SVG 代码图片格式。


图形的大小、边框的粗细、虚线的线宽与间距,圆角大小统统是可以可视化调整的。


通过一个动图,简单感受一下:



总结一下


本文介绍了 2 种在 CSS 中,不借助切图和 SVG 实现带圆角的虚线边框的方式:



  1. 重复角向渐变叠加遮罩层

  2. 重复线性渐变叠加遮罩层


当然,两种 CSS 方式都存在一定瑕疵,但是对于一些简单场景是能够 Cover 住的。


最后,介绍了借助 SVG 工具 Customize your CSS Border 快速生成带圆角的虚线边框的方式。将 SVG 生成的矢量图像数据直接嵌入到 background URL 中,能够应付几乎所有场景,相对而言是更好的选择。


最后


好了,本文到此结束,希望本文对你有所帮助 :)


作者:Chokcoco
来源:juejin.cn/post/7311681326712487999
收起阅读 »

前端打包后,静态文件的名字为什么是一串Hash值?

web
引言 前段时间公司需要招聘几个初级前端,面试过程中,问了这么一个问题:“项目打包后的dist文件夹中,比如js、css这些文件的名称为什么是hash值的,就是一串无规则的字符串”。基本上都不知道静态文件为什么需要这种无规则的hash值,今天就稍微说一下哈。 静...
继续阅读 »

引言


前段时间公司需要招聘几个初级前端,面试过程中,问了这么一个问题:“项目打包后的dist文件夹中,比如js、css这些文件的名称为什么是hash值的,就是一串无规则的字符串”。基本上都不知道静态文件为什么需要这种无规则的hash值,今天就稍微说一下哈。


静态文件何时被加载


拿常用的单页面应用举例,当我们访问一个网站的时候,最终会指向 index.html 这个文件,也就是打包后的 dist 文件夹中的 index.html


比如说 https://some-domain.com/home, 并点击回车键,我们的服务器中实际上没有这个路由,但我们不是返回 404,而是返回我们的 index.html。为什么地址中我们没有输入index.html这个路径,但还是指向到 index.html文件并加载它?因为现在大多数都用nginx去部署,一般在url中输入地址的时候末尾都会加个 “/” ,nginx中已经把“/”重定向到 index.html文件了


image.png


此时按下回车,这个 index.html 文件就被加载获取到了,然后开始自上而下的去加载里面的引用和代码,比如在html中引入的css、js、图片文件。


浏览器默认缓存


当用户按下回车键就向目标服务器去请求index.html文件,加载解析index.html文件的同时就会连带着加载里面的js、css文件。有没有想过,用户第一次已经从服务器请求下载静态文件到客户端了,第二次去浏览该网站该不会还让我去向服务器请求吧,不会吧不会吧,如果每次都请求下载,那用户的体验多不好,每次请求都需要时间,不说服务器的压力会增加,最重要的是用户的体验感,展现到用户眼前的时间会增加!


所以说浏览器已经想到这个了,当请求静态资源的时候,就会默认缓存请求过来的静态文件,这种浏览器自带的默认缓存就叫做 启发式缓存 。 除非手动设置不需要缓存no-store,此时请求静态文件的时候文件才不会缓存到本地!


浏览器默认缓存详情可见 MDN 启发式缓存。不管什么缓存都有缓存的时效性吧,如果想知道 启发式缓存 到底把静态文件缓存了多久,可以阅读笔者的这篇文章 浏览器的启发式缓存到底缓存了多久?


vue-cli里的默认配置,css和js的名字都加了哈希值,所以新版本css、js和就旧版本的名字是不同的,不会有缓存问题。


Hash值的作用


那既然知道了浏览器会有默认的缓存,当加载静态资源的时候会命中启发式缓存并缓存到本地。那如果我们重新部署前端包的时候,如何去请求新的静态资源呢,而不是缓存的静态资源?这时候就得用到hash值了


下面模拟了掘金网站的静态资源获取,当请求静态资源的时候,实际访问的是服务器中静态资源存放的位置
image.png


返回即是当前请求静态资源的具体内容
image.png


第一次进来的时候会去请求服务器的资源缓存到本地,比如 0dbcf63.js 这个文件就被缓存到本地了,后面再正常刷新就直接获取的是缓存中的资源了(disk cache 内存缓存)。


如果前端包重新部署后,试想一下如果 0dbcf63.js这个js文件不叫这个无规则的名字,而是固定的名字,那浏览器怎么知道去请求缓存中的还是服务器中的,所以浏览器的机制就是请求缓存中的,除非缓存中的过期了,才会去请求服务器中的静态资源。如果没有请求到最新的资源,页面也不会更新到最新的内容,影响用户的体验。


那浏览器这个机制是怎么判断的,那就是根据资源的名称,该资源的资源名称如果没变 并且 没有设置不缓存 并且 资源没过期,那就会请求缓存中的资源,否则就会请求服务器中的资源


image.png



当静态资源的名称发生变化的时候,请求路径就会发生变化,所以就会重新命中服务器新部署的静态资源!这就是为什么需要hash值的原因,为了区分之前部署的文件和这次部署文件的区别,好让浏览器去重新获取最新的资源。



第三方库


由于像 lodash 或 react 这样的第三方库很少像本地源代码一样频繁修改,因此通常推荐将第三方库提取到单独的 chunk-vendor 中


 const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
entry: './src/index.js',
plugins: [
new HtmlWebpackPlugin({
title: 'Caching',
}),
],
output: {
filename: '[name].[contenthash].js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
optimization: {
moduleIds: 'deterministic',
runtimeChunk: 'single',
splitChunks: {
cacheGr0ups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
},
},
},
},
};

这样依赖的静态文件就会打包到chunk-vendor中,并且多次打包不会改变文件的hash值,以上是webpack原生的配置,如果使用的vue脚手架,那么脚手架已经都配置好了。


image.png


作者:Lakeiedward
来源:juejin.cn/post/7311219067199881254
收起阅读 »

三行代码实现完美瀑布流

web
需求 最近准备做一个瀑布流的需求,这里每个卡片的高度是不固定的,有点类似下图这样的。 难点 如果绝对定位,如何定位每个卡片的位置。 因为可以发现需求里边的每个卡片的高度都是不固定的,所以如果想使用绝对定位的话,需要实时动态的计算每个卡片的left、top,...
继续阅读 »

需求


最近准备做一个瀑布流的需求,这里每个卡片的高度是不固定的,有点类似下图这样的。



难点



  1. 如果绝对定位,如何定位每个卡片的位置。
    因为可以发现需求里边的每个卡片的高度都是不固定的,所以如果想使用绝对定位的话,需要实时动态的计算每个卡片的left、top,这里会涉及大量的计算。


因为每个卡片的高度是不固定,所以如果想要计算left、top,必须首先获取卡片的高度,但是卡片里边不仅包含图片,还有文字,这个时候计算高度是比较困难的。




  1. 如何结合虚拟列表实现瀑布流


这个时候必须要根据scrollTop的位置,判断什么时候需要加载哪些数据,判断可视区域里边数据的起始索引以及结束索引,这里同样会涉及大量的计算,同时还因为每个卡片的高度不固定,甚至只有图片和文字加载到浏览器以后,才能得到真实的高度,这样会更困难。


解决方案


解决方案1



  1. 如果绝对定位,如何定位每个卡片的位置。


1.1 后端计算
后端可以先把每个图片的高度和宽度提前计算好,直接返回给前端进行处理,然后前端根据后端返回的图片高度和宽度,然后再动态的计算出每个卡片的高度(文字部分也可以固定高度,使用省略号实现)。


1.2 前端计算


前端计算还是比较麻烦的,需要先等卡片组件加载完成,才能得到宽度和高度,而且因为数据量比较大,每个卡片计算出来以后,还需要去根据计算出来的结果去更新left、top,会非常麻烦。
这里可以采用node作为中间层进行计算,还是使用类似后端计算的思路。


还有一种方法是使用observe api 动态观察每个卡片,当观察到卡片加载完成后,再动态根据卡片的宽度和高度计算,不过这样同样很麻烦。



  1. 如何结合虚拟列表实现瀑布流


这里因为卡片的高度是不固定的,同时也是瀑布流,所以不能使用react-window 来解决,不过可以使用react-window的类似思路,自己封装一个npm 包,根据scroll事件判断需要加载那些数据。


解决方案2


使用css3的columns来实现,该技术解决方案不需要计算高度,也不需要去定位,但是columns这个属性会把卡片高度给切开
如下图:



不过可以使用下面代码来解决,


js
复制代码

.test {
// color: red;
// height: 2000px;
background-color: red;
gap: 1rem;
columns: 5;
.no-break {
break-inside: avoid;
}
}

效果如下:



作者:邹小邹
链接:https://juejin.cn/post/7273890921506553890
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

推荐一个小而全的第三方登录开源组件

大家好,我是 Java陈序员。 我们在企业开发中,常常需要实现登录功能,而有时候为了方便,就需要集成第三方平台的授权登录。如常见的微信登录、微博登录等,免去了用户注册步骤,提高了用户体验。 为了业务考虑,我们有时候集成的不仅仅是一两个第三方平台,甚至更多。这就...
继续阅读 »

大家好,我是 Java陈序员


我们在企业开发中,常常需要实现登录功能,而有时候为了方便,就需要集成第三方平台的授权登录。如常见的微信登录、微博登录等,免去了用户注册步骤,提高了用户体验。


为了业务考虑,我们有时候集成的不仅仅是一两个第三方平台,甚至更多。这就会大大的提高了工作量,那么有没有开源框架来统一来集成这些第三方授权登录呢?


答案是有的,今天给大家介绍的项目提供了一个第三方授权登录的工具类库


项目介绍


JustAuth —— 一个第三方授权登录的工具类库,可以让你脱离繁琐的第三方登录 SDK,让登录变得So easy!


JustAuth


JustAuth 集成了诸如:Github、Gitee、微博、钉钉、百度、Coding、腾讯云开发者平台、OSChina、支付宝、QQ、微信、淘宝、Google、Facebook、抖音、领英、小米、微软、今日头条、Teambition、StackOverflow、Pinterest、人人、华为、企业微信、酷家乐、Gitlab、美团、饿了么、推特、飞书、京东、阿里云、喜马拉雅、Amazon、Slack和 Line 等第三方平台的授权登录。


功能特色:



  • 丰富的 OAuth 平台:支持国内外数十家知名的第三方平台的 OAuth 登录。

  • 自定义 state:支持自定义 State 和缓存方式,开发者可根据实际情况选择任意缓存插件。

  • 自定义 OAuth:提供统一接口,支持接入任意 OAuth 网站,快速实现 OAuth 登录功能。

  • 自定义 Http:接口 HTTP 工具,开发者可以根据自己项目的实际情况选择相对应的HTTP工具。

  • 自定义 Scope:支持自定义 scope,以适配更多的业务场景,而不仅仅是为了登录。

  • 代码规范·简单:JustAuth 代码严格遵守阿里巴巴编码规约,结构清晰、逻辑简单。


安装使用


回顾 OAuth 授权流程


参与的角色



  • Resource Owner 资源所有者,即代表授权客户端访问本身资源信息的用户(User),也就是应用场景中的“开发者A”

  • Resource Server 资源服务器,托管受保护的用户账号信息,比如 Github
    Authorization Server 授权服务器,验证用户身份然后为客户端派发资源访问令牌,比如 Github

  • Resource ServerAuthorization Server 可以是同一台服务器,也可以是不同的服务器,视具体的授权平台而有所差异

  • Client 客户端,即代表意图访问受限资源的第三方应用


授权流程


OAuth 授权流程


使用步骤


1、申请注册第三方平台的开发者账号


2、创建第三方平台的应用,获取配置信息(accessKey, secretKey, redirectUri)


3、使用 JustAuth 实现授权登陆


引入依赖


<dependency>
<groupId>me.zhyd.oauthgroupId>
<artifactId>JustAuthartifactId>
<version>{latest-version}version>
dependency>

调用 API


// 创建授权request
AuthRequest authRequest = new AuthGiteeRequest(AuthConfig.builder()
.clientId("clientId")
.clientSecret("clientSecret")
.redirectUri("redirectUri")
.build());
// 生成授权页面
authRequest.authorize("state");
// 授权登录后会返回code(auth_code(仅限支付宝))、state,1.8.0版本后,可以用AuthCallback类作为回调接口的参数
// 注:JustAuth默认保存state的时效为3分钟,3分钟内未使用则会自动清除过期的state
authRequest.login(callback);


说明:
JustAuth 的核心就是一个个的 request,每个平台都对应一个具体的 request 类。
所以在使用之前,需要就具体的授权平台创建响应的 request.如示例代码中对应的是 Gitee 平台。



集成国外平台



国外平台需要额外配置 httpConfig



AuthRequest authRequest = new AuthGoogleRequest(AuthConfig.builder()
.clientId("Client ID")
.clientSecret("Client Secret")
.redirectUri("应用回调地址")
// 针对国外平台配置代理
.httpConfig(HttpConfig.builder()
// Http 请求超时时间
.timeout(15000)
// host 和 port 请修改为开发环境的参数
.proxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress("127.0.0.1", 10080)))
.build())
.build());

SpringBoot 集成


引入依赖


<dependency>
<groupId>com.xkcoding.justauthgroupId>
<artifactId>justauth-spring-boot-starterartifactId>
<version>1.4.0version>
dependency>

配置文件


justauth:
enabled: true
type:
QQ:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/qq/callback
union-id: false
WEIBO:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/weibo/callback
GITEE:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/gitee/callback
DINGTALK:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/dingtalk/callback
BAIDU:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/baidu/callback
CSDN:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/csdn/callback
CODING:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/coding/callback
coding-group-name: xx
OSCHINA:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/oschina/callback
ALIPAY:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/alipay/callback
alipay-public-key: MIIB**************DAQAB
WECHAT_OPEN:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/wechat_open/callback
WECHAT_MP:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/wechat_mp/callback
WECHAT_ENTERPRISE:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/wechat_enterprise/callback
agent-id: 1000002
TAOBAO:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/taobao/callback
GOOGLE:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/google/callback
FACEBOOK:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/facebook/callback
DOUYIN:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/douyin/callback
LINKEDIN:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/linkedin/callback
MICROSOFT:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/microsoft/callback
MI:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/mi/callback
TOUTIAO:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/toutiao/callback
TEAMBITION:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/teambition/callback
RENREN:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/renren/callback
PINTEREST:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/pinterest/callback
STACK_OVERFLOW:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/stack_overflow/callback
stack-overflow-key: asd*********asd
HUAWEI:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/huawei/callback
KUJIALE:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/kujiale/callback
GITLAB:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/gitlab/callback
MEITUAN:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/meituan/callback
ELEME:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/eleme/callback
TWITTER:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/twitter/callback
XMLY:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/xmly/callback
# 设备唯一标识ID
device-id: xxxxxxxxxxxxxx
# 客户端操作系统类型,1-iOS系统,2-Android系统,3-Web
client-os-type: 3
# 客户端包名,如果 clientOsType 为12时必填。对Android客户端是包名,对IOS客户端是Bundle ID
#pack-id: xxxx
FEISHU:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/feishu/callback
JD:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/jd/callback
cache:
type: default

代码使用


@Slf4j
@RestController
@RequestMapping("/oauth")
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class TestController {
private final AuthRequestFactory factory;

@GetMapping
public List list() {
return factory.oauthList();
}

@GetMapping("/login/{type}")
public void login(@PathVariable String type, HttpServletResponse response) throws IOException {
AuthRequest authRequest = factory.get(type);
response.sendRedirect(authRequest.authorize(AuthStateUtils.createState()));
}

@RequestMapping("/{type}/callback")
public AuthResponse login(@PathVariable String type, AuthCallback callback) {
AuthRequest authRequest = factory.get(type);
AuthResponse response = authRequest.login(callback);
log.info("【response】= {}", JSONUtil.toJsonStr(response));
return response;
}

}

总结


JustAuth 集成的第三方授权登录平台,可以说是囊括了业界中大部分主流的应用系统。如国内的微信、微博、Gitee 等,还有国外的 Github、Google 等。可以满足我们日常的开发需求,开箱即用,可快速集成!


最后,贴上项目地址:


https://github.com/justauth/JustAuth

在线文档:


https://www.justauth.cn/

最后


推荐的开源项目已经收录到 GitHub 项目,欢迎 Star


https://github.com/chenyl8848/great-open-source-project

或者访问网站,进行在线浏览:


https://chencoding.top:8090/#/

作者:Java陈序员
来源:juejin.cn/post/7312060958175559743
收起阅读 »

token过期了怎么办?

token过期了怎么办?一般做法是重复第一次获取token的过程(比如登录,扫描授权等) ,这样做的缺点是用户体验不好,每一小时强制登录一次几乎是无法忍受的。那应该怎么办呢?其实这是一个老生常谈的问题,但是最近发现很多人并不清楚,所以今天就一次讲清这...
继续阅读 »

token过期了怎么办?一般做法是重复第一次获取token的过程(比如登录,扫描授权等) ,这样做的缺点是用户体验不好,每一小时强制登录一次几乎是无法忍受的。那应该怎么办呢?其实这是一个老生常谈的问题,但是最近发现很多人并不清楚,所以今天就一次讲清这个问题!

token 过期处理

没有绝对的安全, 所谓的安全处理, 就是提高攻击者攻击的难度, 对他造成了一定的麻烦, 我们这个网站就是安全的! 网站安全性就是高的! 所以: token 必须要有过期时间!

token 过期问题

目标: 了解token过期问题的存在, 学习token过期的解决思路

现象:

你登陆成功之后,接口会返回一个token值,这个值在后续请求时带上(就像是开门钥匙)。

但是,这个值一般会有有效期(具体是多长,是由后端决定),在我们的项目中,这个有效期是2小时。

如果,上午8点登陆成功,到了10:01分,则token就会失效,再去发请求时,就会报401错误。

思考:

  1. token需要过期时间吗 ?

    token即是获取受保护资源的凭证,当然必须有过期时间。否则一次登录便可永久使用,认证功能就失去了其意义。非但必须有个过期时间,而且过期时间还不能太长,

    参考各个主流网站的token过期时间,一般1小时左右

    token一旦过期, 一定要处理, 不处理, 用户没法进行一些需要授权页面的使用了

  2. token过期该怎么办?

    token过期,就要重新获取。

    那么重新获取有两种方式,一是重复第一次获取token的过程(比如登录,扫描授权等) ,这样做的缺点是用户体验不好,每一小时强制登录一次几乎是无法忍受的。

    那么还剩第二种方法,那就是主动去刷新token. 主动刷新token的凭证是refresh token,也是加密字符串,并且和token是相关联的。相比可以获取各种资源的token,refresh token的作用仅仅是获取新的token,因此其作用和安全性要求都大为降低,所以其过期时间也可以设置得长一些。

目标效果 - 保证每一小时, 都是一个不同的token

第一次请求 9:00 用的是 token1  第二次请求 12:00 用的是 token2

当用户登陆成功之后,返回的token中有两个值,说明如下:

image.png

  • token:

    • 作用:在访问一些接口时,需要传入token,就是它。
    • 有效期:2小时。
  • refresh_token

    • 作用: 当token的有效期过了之后,可以使用它去请求一个特殊接口(这个接口也是后端指定的,明确需要传入refresh_token),并返回一个新的token回来(有效期还是2小时),以替换过期的那个token。
    • 有效期:14天。(最理想的情况下,一次登陆可以持续14天。)

image.png

对于 某次请求A 的响应,如果是401错误

  • 有refresh_token,用refresh_token去请求回新的token

    • 新token请求成功

      • 更新本地token
      • 再发一次请求A
    • 新token请求失败

      • 清空vuex中的token
      • 携带请求地址,跳转到登陆页
  • 没有refresh_token

    • 清空vuex中的token
    • 携带请求地址,跳转到登陆页

对于一个请求的响应 401, 要这么处理, 对于十个请求的响应 401, 也要这么处理,

我们可以统一将这个token过期处理放在响应拦截器中

请求拦截器: 所有的请求, 在真正被发送出去之前, 都会先经过请求拦截器 (可以携带token)

响应拦截器: 所有的响应, 在真正被(.then.catch await)处理之前, 都会先经过响应拦截器, 可以在这个响应拦截器中统一对响应做判断

响应拦截器处理token

目标: 通过 axios 响应拦截器来处理 token 过期的问题

响应拦截器: http://www.kancloud.cn/yunye/axios…

  1. 没有 refresh_token 拦截到登录页, 清除无效的token

测试: {"token":"123.123.123"}

// 添加响应拦截器
http.interceptors.response.use(function (response) {
// 对响应数据做点什么 (成功响应) response 就是成功的响应 res
return response
}, function (error) {
// 对响应错误做点什么 (失败响应) 处理401错误
// console.dir(error)
if (error.response.status === 401) {
console.log('token过期了, 一小时过去了, 需要通过refresh_token去刷新token')
// 获取 refresh_token, 判断是否存在, 存在就去刷新token
const refreshToken = store.state.tokenInfo.refresh_token
if (refreshToken) {
console.log('存在refreshToken, 需要进行刷新token操作')
} else {
// 没有refreshToken, 直接去登录, 将来还能跳回来
// router.currentRoute 指向当前路由信息对象 === 等价于之前页面中用的 this.$route
// 清除本地token, 跳转登录 (无意义的本地token内容, 要清除)
store.commit('removeToken')
router.push({
path: '/login',
query: {
backto: router.currentRoute.fullPath
}
})
}
}
return Promise.reject(error)
})

提供清除token的mutation

// 移出tokenInfo的信息, 恢复成空对象
removeToken (state) {
state.tokenInfo = {}
// 更新到本地, 本地可以清掉token信息
removeToken()
},
  1. 有 refresh_token 发送请求, 刷新token

测试操作: 将 token 修改成 xyz, 模拟 token 过期, 而有 refresh_token 发现401, 会自动帮你刷新token

{"refresh_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MDYzNTcyODcsInVzZXJfaWQiOjExMDI0OTA1MjI4Mjk3MTc1MDQsInJlZnJlc2giOnRydWV9.2A81gpjxP_wWOjclv0fzSh1wzNm6lNy0iXM5G5l7TQ4","token":"xyz"}

const refreshToken = store.state.tokenInfo.refresh_token
if (refreshToken) {
console.log('存在refreshToken, 需要进行刷新token操作')
// (1) 发送请求, 进行刷新token操作, 获取新的token
// 注意: 这边发请求, 不用http实例, 用它会自动在请求前帮你携带token(会覆盖你的refresh_token)
// 这边, 直接用 axios 发送请求
const res = await axios({
method: 'put',
url: 'http://ttapi.research.itcast.cn/app/v1_0/authorizations',
// 请求头中携带refresh_token信息
headers: {
Authorization: `Bearer ${refreshToken}`
}
})
const newToken = res.data.data.token
// (2) 将新token更新到vuex中
store.commit('setTokenInfo', {
refresh_token: refreshToken,
token: newToken
})
}
  1. 刷新token后, 应该重新发送刚才的请求 (让用户刷新token无感知)
return http(error.config)
  1. 那万一 refresh_token 也过期了, 是真正的用户登录过期了 (一定要让用户重新登录的)

测试: {"refresh_token":"123.123","token":"123.123.123"} 修改后, 修改的是本地, 记得刷新一下

从哪拦走的, 就回到哪去

// 添加响应拦截器
http.interceptors.response.use(function (response) {
// 对响应数据做点什么 (成功响应) response 就是成功的响应 res
return response
}, async function (error) {
// 对响应错误做点什么 (失败响应) 处理401错误
// console.dir(error)
if (error.response.status === 401) {
console.log('token过期了, 一小时过去了, 需要通过refresh_token去刷新token')
// 获取 refresh_token, 判断是否存在, 存在就去刷新token
const refreshToken = store.state.tokenInfo.refresh_token
if (refreshToken) {
try {
console.log('存在refreshToken, 需要进行刷新token操作')
// (1) 发送请求, 进行刷新token操作, 获取新的token
// 注意: 这边发请求, 不用http实例, 用它会自动在请求前帮你携带token(会覆盖你的refresh_token)
// 这边, 直接用 axios 发送请求
const res = await axios({
method: 'put',
url: 'http://ttapi.research.itcast.cn/app/v1_0/authorizations',
// 请求头中携带refresh_token信息
headers: {
Authorization: `Bearer ${refreshToken}`
}
})
const newToken = res.data.data.token
// (2) 将新token更新到vuex中
store.commit('setTokenInfo', {
refresh_token: refreshToken,
token: newToken
})
// (3) 重新发送刚才的请求, http, 自动携带token (携带的是新token)
// error.config就是之前用于请求的配置对象, 可以直接给http使用
return http(error.config)
} catch {
// refresh_token 过期了, 跳转到登录页
// 清除过期的token对象
store.commit('removeToken')
// 跳转到登录页, 跳转完, 将来跳回来
router.push({
path: '/login',
query: {
backto: router.currentRoute.fullPath
}
})
}
} else {
// 没有refreshToken, 直接去登录, 将来还能跳回来
// router.currentRoute 指向当前路由信息对象 === 等价于之前页面中用的 this.$route
// 清除本地token, 跳转登录 (无意义的本地token内容, 要清除)
store.commit('removeToken')
router.push({
path: '/login',
query: {
backto: router.currentRoute.fullPath
}
})
}
}
return Promise.reject(error)
})

注意点:

  1. 响应拦截器要加在axios实例 http 上。
  2. 用refresh_token请求新token时,要用axios,不要用实例 http (需要: 手动用 refresh_token 请求)
  3. 得到新token之后,再发请求时,要用 http 实例 (用token请求)
  4. 过期的 token 可以用 refresh_token 再次更新获取新token, 但是过期的 refresh_token 就应该从清除了


作者:JoyZ
来源:juejin.cn/post/7308992811449172005
收起阅读 »

被中文输入法坑死了

web
PM:在PC端做一个@功能吧,就是那种...。 我:你不用解释🤔我知道那个功能,监听keydown事件,然后e.keycode === 50,那可太简单了。 那可太简单了,可太简单了,太简单了,简单了,单了,了......(掉进坑里的回声) 坑1:KeyB...
继续阅读 »

PM:在PC端做一个@功能吧,就是那种...。



我:你不用解释🤔我知道那个功能,监听keydown事件,然后e.keycode === 50,那可太简单了。



那可太简单了,可太简单了,太简单了,简单了,单了,了......(掉进坑里的回声)


坑1:KeyBoardEvent.keycode



废弃的属性你就坚持用吧,一用一个不吱声。以后线上跑得好好的代码突然报错了,你都不知道bug在哪儿。


现在的web标准里,要确定一个键盘事件是靠e.keye.codecode代表触发事件的物理按键,比如2的位置code='Digit2'key返回用户按下的物理按键的值,它还与 shiftKey 等调节性按键的状态、键盘的区域和布局有关。


所以对于@来说,直接判断e.key === "@"来做后续的操作就行了。


addEventListner('keydown', (e) => {
if (e.key === "@") {
e.preventDefalut(); // 这里去掉@字符是为了后续插入和监听方便处理
// 插入有@字符的,可监听输入的元素
// 唤起小窗....
}
});


仔细看上面的这几行代码和注释,要开始考(坑)了。


坑2:输入法的坑


起因


在我美滋滋地以为避过了坑1就没事了的时候,一个夜晚我的测试同学告诉我,在测试环境突然就体验不到这个功能了,无论输入多少个@都不行,白天还好好的🤯。


好一个「白天还好好的」。


我自己测试的时候又百分百能体验到🤔,所以最开始我还在怀疑他没有配上测试系统......


于是,让测试同学的windows电脑连到我的开发环境debug一看:


好家伙,真是好家伙😅他的电脑的e.key === "Process"????!!!


什么意思呢,就是正常我们理想中的@字符产生是shift+2按键的组合,监听keydown之后我们会按顺序收到两个回调:



  1. e.key === "Shift"e.code === "ShiftLeft"或者shiftRight

  2. e.key === "@"e.code === "Digit2"


但是实际在测试同学的电脑里,1是一样的,但是2变了,2变成了e.key === "Process"


虽然键盘事件有变化,但是在前端页面上的@字符是没有任何变化的。难怪他说他会突然失效了。我问他做了什么怎么会突然变了,他想了想说晚上从系统输入法换成了微信输入法.....


上网检索(chatGPT)了一番,明白了一个新的知识点:


输入法的全称叫Input Method Editor输入法编辑器(IME)。本质上它也是个编辑器。为了能输入各类字符(比如汉字和阿拉伯字等),IME会先处理用户的英文字母输入然后通过系统的底层调用传递给浏览器,浏览器再显示到用户的界面。这里的Process很大概率就是当时输入法给出的某个值表示那个时刻它还在处理中。


解决办法


既然KeyBoardEvent靠不住,那我们换一种监听方式。


我找到了一个非常适用于输入法的监听事件叫做CompositionEvent,它表示用户间接输入文本(如使用输入法)时发生的事件。此接口的常用事件有compositionstart, compositionupdatecompositionend。它们三个事件分别对应的动作,通俗一点说就是你用输入法开始打字、正在打字和结束打字。


于是乎,我监听compositionend不就行了!在输入法end的时候我再去看你end的字符是不是@不就行了!


// addEventListner('keydown', (e) => {
addEventListner('compositionend', (e) => {
// if (e.key === "@") {
if (e.data === "@") {
e.preventDefalut(); // 这里去掉@字符是为了后续插入和监听方便处理
// 插入有@字符并且可监听输入的元素
// 唤起小窗....
}
});

对于输入法来说,按键的up和down的key值就算不尽人意也没什么损失,毕竟用户毫无感知。但是,compositionend永远是不会错的,如果compositionende.data都不是@字符了,那么在用户的编辑器界面上的显示肯定也会跟着出错。


所以监听这个肯定就是万无一失的方法了,哈哈哈我真是个“天才”(蠢材)。
修改之后让测试同学尝试之后果然就可以了。


坑3:输入法继续坑


起因


时间过去了没一会,本天才就收到了另一个测试同学反馈的问题说为什么输入了一个@字符之后,会出现两个@在界面上?


我第一反应就是难道没有执行到e.preventDefalut()?既然后续功能能正常使用,没执行到也不应该啊🤔。然后在我电脑一通尝试,发现safari浏览器在输入法为中文的情况下也会触发这个问题。


于是,让测试同学的windows电脑连到我的开发环境debug一看(梅开二度):


执行到了,也没有报错什么的,但是@字符并没有被prevent掉🤯。


再加上我自己传入的@,所以界面上就出现了两个@字符。啊这这这,这很难评......


ceeb653ely8gzozgvjq1cg20j20hhgvb.gif


我是左思右想,百思不得其解,于是只能:



stack overflow上也有这个问题


上面大概的意思就是compositionend事件里使用 e.preventDefault() 在技术上可行的,但它可能不会产生你期望的效果。可能是因为 compositionend 事件标志着一次输入构成(composition)会话的结束,而在这个点上阻止默认行为可能没有意义,因为输入的流程已经完成了。


更推荐用keydown,compositionstartinput来处理这种情况。


keydown是不可能keydown了,已经被坑了。compositionstart也不行,因为刚开始输入那会才按下了shift键,@字符还没出来呢。那就只能input了。


解决办法


最开始我没有选择input就是因为它不能使用e.preventDefault()。我必须要对输入的字符串进行单独处理,去掉@,当时觉得很麻烦就没有选择这个方法。


额....好好好,行行行,现在还是必须得处理一下了。


// addEventListner('keydown', (e) => {
// addEventListner('compositionend', (e) => {
addEventListner('input', (e) => {
// if (e.key === "@") {
if (e.data === "@") {
?怎么去处理字符呢 // 这里去掉@字符是为了后续插入和监听方便处理
// 插入有@字符并且可监听输入的元素
// 唤起小窗....
}
});

对于这个处理字符的方法,也是一个新知识点了。起初我还想的是去处理编辑器里的content,然后再给它插入回去,这样子复杂度很高并且出错的概率极大。


这里的解决办法主要是使用CharacterData接口。CharacterData 接口是操作那些包含字符数据的节点的核心,特别是在需要动态地更改文本内容时。


例如,在一个文本节点上使用 deleteData() 方法可以从文本中移除一部分内容,而不必完全替换或重写整个节点


const selection = window.getSelection();
const { anchorNode, anchorOffset } = selection;
if (anchorNode.substringData(anchorOffset - 1, 1) === '@') {
selection.anchorNode.deleteData(anchorOffset - 1, 1);
}

写完这个之后,我用自己的safari浏览器测试发现果然没有问题了。


哈哈哈我真是个“天才”(蠢材)。


坑4:输入法深坑🕳️


我自信满满地让测试同学再重试一下😎,然后测试同学说:和之前一样啊,还是有两个@字符。


我:啊?啊啊??啊啊啊???


IMG_6547.jpg


于是,让测试同学的windows电脑连到我的开发环境debug一看(梅开三度):


发现测试同学电脑上的anchorOffset和正常的情况下是不一样的,会小一位,所以导致anchorOffset - 1拿到的前一个字符并不等于@,所以后续也没有把它处理掉🤯。


我是左思右想,百思不得其解,stack overflow上也没有相关的问题。不过,结合IME的概念,肯定还是输入法的问题。


结合之前keydown的e.key==="Processing",可能在input触发时输入法的编辑器其实还是没有完成工作(composition),导致在那个时候SelectionanchorOffset不一致。其实浏览器的Selection肯定不会错,那anchorOffset看起来像是错了,我觉得应该是输入法在转换的过程对我们的前端页面做了一些用户看不到的东西,而anchorOffset把它显化出来罢了。


解决办法


于是乎,我尝试性的对处理字符串的那串代码进行延时,目的是为了等待输入法彻底工作完毕。


// addEventListner('keydown', (e) => {
// addEventListner('compositionend', (e) => {
addEventListner('input', (e) => {
// if (e.key === "@") {
if (e.data === "@") {
setTimeout(() => {
const selection = window.getSelection();
const { anchorNode, anchorOffset } = selection;
if (anchorNode.substringData(anchorOffset - 1, 1) === '@') {
selection.anchorNode.deleteData(anchorOffset - 1, 1);
} // 这里去掉@字符是为了后续插入和监听方便处理
});

// 插入有@字符并且可监听输入的元素
// 唤起小窗....
}
});

然后,问题真的就彻底解决了。


这个功能做起来可太简单了......😅


作者:Liqiuyue
来源:juejin.cn/post/7307041255740981286
收起阅读 »

使用flex实现瀑布流

web
什么是瀑布流 瀑布流,又称瀑布流式布局。是比较流行的一种网站页面布局,视觉表现为参差不齐的多栏布局,随着页面滚动条向下滚动,这种布局还会不断加载数据块并附加至当前尾部。 特点: 固定宽度,高度不一 参差不齐的布局 使用flex实现瀑布流 实现的效果是分成两...
继续阅读 »

什么是瀑布流


瀑布流,又称瀑布流式布局。是比较流行的一种网站页面布局,视觉表现为参差不齐的多栏布局,随着页面滚动条向下滚动,这种布局还会不断加载数据块并附加至当前尾部。


特点:



  • 固定宽度,高度不一

  • 参差不齐的布局


使用flex实现瀑布流


实现的效果是分成两列的瀑布流,往下滑动会加载下一页的数据,并渲染到页面中!


微信图片_20230731231617.jpg


样式的实现


<view class="blessing-con">
<view class='blessing-con-half'>
<view id="leftHalf">
<view class="blessing-con-half-item" :class="bgColor[index % 3]"
v-for="(item, index) in newBlessingWordsList1" :key="index">
<view class="item-con">
</view>
</view>
</view>
</view>
<view class='blessing-con-half'>
<view id="rightHalf">
<view class="blessing-con-half-item" :class="bgColor[(index + 1) % 3]"
v-for="(item, index) in newBlessingWordsList2" :key="index">
<view class="item-con"></view>
</view>
</view>
</view>
</view>
<view class="blessing-more" @click="handlerMore">
<image v-if="hasWallNext" class="more-icon"
src="xx/blessingGame/arr-down.png">
</image>
<view class="blessing-more-text">{{ blessingStatus }}</view>
</view>

.blessing-con 定义外层容器为flex布局,同时设置主轴对齐方式为space-between


.blessing-con-half定义左右两侧的容器的样式


.blessing-con-half-item定义每一个小盒子的样式


.blessing-con {
padding: 32rpx 20rpx;
display: flex;
justify-content: space-between;
height: 1100rpx;
overflow-y: auto;
.blessing-con-half {
width: 320rpx;
height: 100%;
box-sizing: border-box;
.blessing-con-half-item {
width: 100%;
height: auto;
display: flex;
flex-direction: column;
box-sizing: border-box;
margin: 0 0 24rpx;
position: relative;
}
}
}

这里每个小盒子的背景色按蓝-黄-红的顺序,同时通过伪类给盒子顶部添加锯齿图片,实现锯齿效果


bgColor: ['blueCol', 'yellowCol', 'pinkCol'], //祝福墙背景

// 不同颜色
.blessing-con-half-item {
&.pinkCol {
&::before {
.setbg(320rpx, 16rpx, 'blessingGame/pink-bg.png');
}
.item-con {
background: #FFE7DF;
}
}

&.yellowCol {
&::before {
.setbg(320rpx, 16rpx, 'blessingGame/orange-bg.png');
}
.item-con {
background: #fff0e0;
}
}

&.blueCol {
&::before {
.setbg(320rpx, 16rpx, 'blessingGame/blue-bg.png');
}
.item-con {
background: #e0f7ff;
}
}
}
}

功能实现


在data中定义两个数组存储左右列表的数据


data(){
return{
blessingWordsList: [],// 祝福墙数据
newBlessingWordsList: [],//已添加的数据
newBlessingWordsList1: [],//左列表
newBlessingWordsList2: [],//右列表
isloading:false,//是否正在加载
hasWallNext:false,//是否有下一页
leftHeight: 0,//左高度
rightHeight: 0,//右高度
blessingWordsCount: 0,//计数器
isActive: 0, //tab初始化索引
timer:null,//定时器
}
}

调取接口请求列表数据



  • 第一次请求数据需要初始化列表数据和计数器

  • 每一次请求完都需要开启定时器


// 获取祝福墙列表(type=1则请求下一页)
async getBlessingWall(type = 0) {
try {
let res = await api.blessingWall({
activityId: this.activityId,
pageNum: this.pageWallNum,
pageSize: this.pageWallSize
})
this.isloading = false
if (res.code == 1 && res.rows) {
let list = res.rows
this.blessingWordsList = (type==0 ? list : [...this.blessingWordsList, ...list])
if (this.blessingWordsList.length > 0 && !this.timer && this.isActive == 1) {
if(this.pageWallNum == 1){
this.newBlessingWordsList = []
this.newBlessingWordsList1 = []
this.newBlessingWordsList2 = []
this.blessingWordsCount = 0
}
this.start()
}
// 处理请求下一页的情况
if (type == 1) {
this.start()
}
this.hasWallNext = res.hasNext
if (!this.hasWallNext) {
this.blessingStatus = "没有更多了哟"
} else {
this.blessingStatus = "点击加载更多"
}
}
} catch (error) {
console.log(error)
}
},
// 加载更多
async handlerMore() {
if (this.hasWallNext && !this.isloading) {
this.isloading = true
this.pageWallNum++
await this.getBlessingWall(1)
}
},

开启一个定时器,用于动态添加左右列表的数据


start() {
// 清除定时器
clearInterval(this.timer)
this.timer = null;

this.timer = setInterval(() => {
let len = this.blessingWordsList.length
if (this.blessingWordsCount < len) {
let isHave = false
// 在列表中获取一个元素
let item =this.blessingWordsList[this.blessingWordsCount]
// 判断新列表中是否已经存在相同元素,防止重复添加
this.newBlessingWordsList.forEach((tmp)=>{
if(tmp.id == item.id){
isHave = true
}
})
// 如果不存在
if (!isHave) {
this.newBlessingWordsList.push(item)//添加该元素
this.$nextTick(() => {
this.getHei(item)//添加元素到左右列表
})
}
} else {
// 遍历完列表中的数据,则清除定时器
clearInterval(this.timer)
this.timer = null;
}
}, 10)
}

计算当前左右容器的高度,判断数据要添加到哪一边



  • 使用uni-app的方法获取左右容器的dom对象,再获取他们当前的高度

  • 比较左右高度,向两个数组动态插入数据

  • 每插入一条数据,计数器+1


getHei(item) {
const query = uni.createSelectorQuery().in(this)
// 左边
query.select('#leftHalf').boundingClientRect(res => {
if (res) {
this.leftHeight = res.height
}
// 右边
const query1 = uni.createSelectorQuery().in(this)
query1.select('#rightHalf').boundingClientRect(dataRight => {
if (dataRight) {
this.rightHeight = dataRight.height != 0 ? dataRight.height : 0
if (this.leftHeight == this.rightHeight || this.leftHeight < this.rightHeight) {
// 相等 || 左边小
this.newBlessingWordsList1.push(item)
} else {
// 右边小
this.newBlessingWordsList2.push(item)
}
}
this.blessingWordsCount++
}).exec()
}).exec()
},

这里有一个注意点,调用start方法的时候,必须确保页面渲染了左右容器的元素,否则会拿不到容器的高度


比如我这个项目是有tab切换的!


微信图片_20230731231616.jpg
进入页面的时候会请求一次数据,这时候因为tab初始状态在0,所以并不会调用start方法,要到切换tab到1时,才会调用start方法开始计算高度。


data(){
return{
isActive: 0, //tab初始化索引
timer:null,//定时器
}
}
async onLoad(options) {
this.getBlessingWall()
}
// tab选项卡切换
tabClick(index) {
this.isActive = index
this.isLoaded = false;
if (this.blessingWordsList.length > 0 && !this.timer && this.isActive == 1) {
if(this.pageWallNum == 1){
this.newBlessingWordsList = []
this.newBlessingWordsList1 = []
this.newBlessingWordsList2 = []
this.blessingWordsCount = 0
}
this.start()
}
},

最后


这次选用了flex实现瀑布流,实现瀑布流的方式还有其他几种方法,后续有机会的话,我会补充其他几种方式,如果感兴趣的话,可以点点关注哦!


作者:藤原豆腐店
来源:juejin.cn/post/7260713996165021754
收起阅读 »

前端同事最讨厌的后端行为,看看你中了没有

web
前端同事最讨厌的后端行为,看看你中了没有 听说这是前端程序员最讨厌的后端行为,不知道你有没有碰到过,或者你的前端同事虽然没跟你说过,但是你已经被偷偷吐槽了。 前端吐槽:后端从不自测接口,等到前后端联调时,这个接口获取不到,那个接口提交不了,把前端当成自己...
继续阅读 »

前端同事最讨厌的后端行为,看看你中了没有



听说这是前端程序员最讨厌的后端行为,不知道你有没有碰到过,或者你的前端同事虽然没跟你说过,但是你已经被偷偷吐槽了。




前端吐槽:后端从不自测接口,等到前后端联调时,这个接口获取不到,那个接口提交不了,把前端当成自己的接口测试员,耽误前端的开发进度。



听到这个吐槽,仿佛看到曾经羞愧的自己。这个毛病以前我也有啊,有些接口,尤其是大表单提交接口,表单特别大,字段很多,有时候就偷懒了,直接编译过了,就发到测试环境了。前端同时联调的时候一调接口,异常了。


好在后来改了,毕竟让人发现自己接口写的有问题,也是一件丢脸的事儿。


但是我还真见过后端的同学,写完接口一个都不测,直接发测试环境的。


我就碰到过厉害的,编译都不过,就直接提代码。以前,有个新来的同事,分了任务就默默的干着,啥也不问,然后他做的功能测试就各种发现问题。说过之后,就改一下,但是基本上还是不测试,本想再给他机会的,所以后来他每次提代码,我都review一下。直到有一天,我发现忍不了了,他把一段全局配置给注释了,然后把代码提了,我过去问他是不是本地调试,忘了取消注释了。他的回答直接让我震惊了,他说:不是的,是因为不注释那段代码,我本地跑步起来,所以肯定是那段代码有问题,所以就注释了。


然后,当晚,他就离职了。


解决方式


对于这种大表单类似的问题,应该怎么处理呢?


好像没有别的方法,只能克服自己的懒惰,为自己写的代码负责。就想着,万一接口有问题,别人可能会怀疑你水平不行,你水平不行,就是你不行啊,程序员怎么能不行呢。


你可以找那么在线 Java BeanJSON的功能,直接帮你生成请求参数,或者现在更可以借助 ChatGPT ,帮你生成请求参数,而且生成的参数可能比你自己瞎填的看上去更合理。


或者,如果是小团队,不拘一格的话,可以让前端的同事把代码提了,你本地跑着自测一下,让前端同事先做别的功能,穿插进行也可以。



前端吐槽:后端修改了字段或返回结构不通知前端



这个就有点不讲武德了。


正常情况下,返回结构和字段都是事先约定好的,一般都是先写接口,做一些 Mock 数据,然后再实现真实的逻辑。


除了约定好返回字段和结构外,还包括接口地址、请求方法、头信息等等,而且一个项目都会有项目接口规范,同一类接口的返回字段可能有很多相同的部分。


后端如果改接口,必须要及时通知前端,这其实应该是正常的开发流程。后端改了接口,不告诉前端,到时候测试出问题了,一般都会先找前端,这不相当于让前端背锅了吗,确实不地道啊。


后端的同学们,谨记啊。



前端吐槽:为了获取一个信息,要先调用好几个接口,可能参数还是相同的



假设在一个详情页面,以前端的角度就是,我获取详情信息,就调用详情接口好了,为什么调用详情接口之前,要调用3、4个其他的接口,你详情里需要啥参数,我直接给你传过去不就好了吗。


在后端看来可能是这样的,我这几个接口之前就写好了,前端拿过去就能用,只不过就是多调几次罢了,没什么大不了的吧。


有些时候,可能确实是必须这么做的,比如页面内容太多,有的部分查询逻辑复杂,比较耗时,这时候需要异步加载,这样搞确实比较好。


但是更多时候其实就是后端犯懒了,不想再写个新接口。除了涉及到性能的问题,大多数逻辑都应该在后端处理,能用一个接口处理完,就不应该让前端多调用第二个接口。


有前端的朋友曾经问过我,他说,他们现在做的系统中有些接口是根据用户身份来展示数据的,但是前端调用登录接口登录系统后,在调用其他接口的时候,除了在 Header 中加入 token 外,还有传很多关于用户信息的很多参数,这样做是不是不合理的。


这肯定不合理,token 本来就是根据用户身份产生的,后端拿到 token 就能获取用户信息,这是常识问题,让前端在接口中再传一遍,既不合理也不安全。


类似的问题还有,比如后端接口返回一堆数据,然后有的部分有用、有的部分没有,有的部分还涉及到逻辑,不借助文档根本就看不明白怎么用,这其实并不合理。


接口应该尽量只包含有用的部分,并且尽可能结构清晰,配合简单的字段说明就能让人明白是怎么回事,是最好的效果。


如果前后端都感觉形势不对了,后端一个接口处理性能跟不上了,前端处理又太麻烦了。这时候就要向上看了,产品设计上可能需要改一改了。


后端的同学可以学一点前端,前端的同学也可以学一点后端,当你都懂一些的时候,就能两方面考虑了,这样做出来的东西可能会更好用一点。总之,前后端相互理解,毕竟都是为了生活嘛。


作者:古时的风筝
来源:juejin.cn/post/7254927062425829413
收起阅读 »

面试官:你能说说常见的前端加密方法吗?

web
前言 本篇文章略微介绍一下前端中常见的加密算法。前端中常见的加密算法主要形式包括——哈希函数,对称加密和非对称加密算法。 一、哈希函数 定义:哈希也叫散列,是指将任意长度的消息映射为固定长度的输出的算法,该输出一般叫做散列值或者哈希值,也叫做摘要(Dige...
继续阅读 »

前言


本篇文章略微介绍一下前端中常见的加密算法。前端中常见的加密算法主要形式包括——哈希函数,对称加密和非对称加密算法。


一、哈希函数


image.png



  • 定义:哈希也叫散列,是指将任意长度的消息映射为固定长度的输出的算法,该输出一般叫做散列值或者哈希值,也叫做摘要(Digest)。简单来说,这种映射就是一种数据压缩,而且散列是不可逆的,也就是无法通过输出还原输入。

  • 特点:不可逆性(单向性)、抗碰撞性(消息不同其散列值也不同)、长度固定

  • 常见应用场景:由于不可逆性,常用于密码存储、数字签名、电子邮件验证、验证下载等方面,更多的是用用在验证数据的完整性方面。



    • 密码存储:明文保存密码是危险的。通常我们把密码哈希加密之后保存,这样即使泄漏了密码,因为是散列后的值,也没有办法推导出密码明文(字典攻击难以破解)。验证的时候,只需要对密码(明文)做同样的散列,对比散列后的输出和保存的密码散列值,就可以验证同一性。

    • 可用于验证下载文件的完整性以及防篡改:比如网站提供安装包的时候,通常也同时提供md5值,这样用户下载之后,可以重算安装包的md5值,如果一致,则证明下载到本地的安装包跟网站提供的安装包是一致的,网络传输过程中没有出错。



  • 优势:不可逆,速度快、存储体积小,可以帮助保护数据的完整性和减轻篡改风险。

  • 缺点:安全性不高、容易受到暴力破解


image.png


常见类型:SHA-512、SHA-256、MD5(MD5生成的散列码是128位)等。



  • MD5(Message Digest Algorithm 5) :是RSA数据安全公司开发的一种单向散列算法,非可逆,相同的明文产生相同的密文。

  • SHA(Secure Hash Algorithm) :可以对任意长度的数据运算生成一个固定位数的数值。

  • SHA/MD5对比:SHA在安全性方面优于MD5,并且可以选择多种不同的密钥长度。 但是,由于内存需求更高,运行速度可能会更慢。 不过,MD5因其速度而得到广泛使用,但是由于存在碰撞攻击风险,因此不再推荐使用。


二、对称加密



  • 定义:指加密和解密使用同一种密钥的算法。


image.png



  • 特点:优点是速度快,通信效率高;缺点是安全性相对较低。信息传输使用一对一,需要共享相同的密码,密码的安全是保证信息安全的基础,服务器和N个客户端通信,需要维持N个密码记录且不能修改密码。

  • 优势:效率高,算法简单,系统开销小,速度快,适合大数量级的加解密,安全性中等

  • 缺点:秘钥管理比较难,密钥存在泄漏风险。

  • 常见应用场景:适用于需要高速加密/解密的场景,例如 HTTP 传输的 SSL/TLS 部分,适用于加密大量数据,如文件加密、网络通信加密、数据加密、电子邮件、Web 聊天等。



    • 文件加密:将文件用相同的密钥加密后传输或存储,只有拥有密钥的用户才能解密文件。

    • 数据库加密:对数据库中的敏感信息进行加密保护,防止未经授权的人员访问。

    • 通信加密:将网络数据通过对称加密算法进行加密,确保数据传输的机密性,比较适合大量短消息的加密和解密。

    • 个人硬盘加密:对称加密可以为硬盘加密提供较好的安全性和高处理速度,这对个人电脑而言可能是一个不错的选择。



  • 常见类型DES,3DES,AES 等:



    • DES(Data Encryption Standard):分组式加密算法,以64位为分组对数据加密,加解密使用同一个算法,速度较快,适用于加密大量数据的场合。

    • 3DES(Triple DES):三重数据加密算法,是基于DES,对每个数据块应用三次DES加密算法,强度更高。

    • AES(Advanced Encryption Standard):高级加密标准算法,速度快,安全级别高,目前已被广泛应用,适用于加密大量数据,如文件加密、网络通信加密等。




AES与DES区别

AES与DES之间的主要区别在于加密过程。在DES中,将明文分为两半,然后再进行进一步处理;而在AES中,整个块不进行除法,整个块一起处理以生成密文。相对而言,AES比DES快得多,与DES相比,AES能够在几秒钟内加密大型文件。



  • DES



    • 优点:DES算法具有极高安全性,到目前为止,除了用穷举搜索法对DES算法进行攻击外,还没有发现更有效的办法。

    • 缺点:分组比较短、密钥太短、密码生命周期短、运算速度较慢。



  • AES



    • 优点:运算速度快,对内存的需求非常低,适合于受限环境。分组长度和密钥长度设计灵活, AES标准支持可变分组长度;具有很好的抵抗差分密码分析及线性密码分析的能力。

    • 缺点:目前尚未存在对AES 算法完整版的成功攻击,但已经提出对其简化算法的攻击。




三、非对称加密


-定义:指加密和解密使用不同密钥的算法,通常情况下使用公共密钥进行加密,而私有密钥用于解密数据。公钥和私钥是成对存在,公钥是从私钥中提取产生公开给所有人的,如果使用公钥对数据进行加密,那么只有对应的私钥(不能公开)才能解密,反之亦然。


image.png



  • 特点:缺点是加密解密速度较慢,通信效率较低,优点是安全性高,需要两个不同密钥,信息一对多。因为它使用的是不同的密钥,所以需要耗费更多的计算资源。服务器只需要维持一个私钥就可以和多个客户端进行通信,但服务器发出的信息能够被所有的客户端解密,且该算法的计算复杂,加密的速度慢。

  • 优势:秘钥容易管理,不存在密钥的交换问题,安全性好,主要用在数字签名,更适用于区块链技术的点对点之间交易的安全性与可信性。

  • 缺点:加解密的计算量大,比对称加密算法计算复杂,性能消耗高,速度慢,适合小数据量或数据签名

  • 常见应用场景:在实际应用中,非对称加密通常用于需要确保数据完整性和安全性的场合,例如数字证书的颁发、SSL/TLS 协议的加密、数字签名、加密小文件、密钥交换、实现安全的远程通信等。



    • 数字签名:数字签名是为了保证数据的真实性和完整性,通常使用非对称加密实现。发送方使用自己的私钥对数据进行签名,接收方使用发送方的公钥对签名进行验证,如果验证通过,则可以确认数据的来源和完整性。常见的数字签名算法都基于非对称加密,如RSA、DSA等。

    • ** 身份认证**:Web浏览器和服务器使用SSL/TLS技术来进行安全通信,其中就使用了非对称加密技术。Web浏览器在与服务器建立连接时,会对服务器进行身份验证并请求其证书。服务器将其证书发送给浏览器,证书包含服务器的公钥。浏览器使用该公钥来加密随机生成的“对话密钥”,然后将其发送回服务器。服务器使用自己的私钥解密此“对话密钥”,以确保双方之间的会话是安全的。

    • 安全电子邮件:非对称加密可用于电子邮件中,确保邮件内容只能由预期的收件人看到。发件人使用收件人的公钥对邮件进行加密,收件人使用自己的私钥对其进行解密。这确保了只有目标收件人才能读取邮件。



  • 常见类型RSA,DSA,DSS,ECC 等



    • RSA:由 RSA 公司发明,是一个支持变长密钥的公共密钥算法,需要加密的文件块的长度也是可变的。RSA 是一种非对称加密算法,即加密和解密使用一对不同的密钥,分别称为公钥和私钥。公钥用于加密数据,私钥用于解密数据。RSA 算法的安全性基于大数分解问题,密钥长度通常选择 1024 位、2048 位或更长。RSA 算法用于保护数据的机密性、确保数据的完整性和实现数字签名等功能。

    • DSA(Digital Signature Algorithm) :数字签名算法,仅能用于签名,不能用于加解密。

    • ECC(Elliptic Curves Cryptography) :椭圆曲线密码编码学。

    • DSS:数字签名标准,可用于签名,也可以用于加解密。




总结


前端使用非对称加密原理很简单,平时用的比较多的也是非对称加密,前后端共用一套加密解密算法,前端使用公钥对数据加密,后端使用私钥将数据解密为明文。中间攻击人拿到密文,如果没有私钥的话是没办法破解的。


欢迎大佬继续评论区补充


作者:优秀稳妥的Zn
来源:juejin.cn/post/7280057907055919144
收起阅读 »

你的网站如何接入QQ,微信登录

web
主要实现步骤 对接第三方平台,获取第三方平台的用户信息。 利用该用户信息,完成本应用的注册。 qq登录接入 接入前的配置 qq互联 登录后,点击头像,进行开发者信息填写,等待审核。 邮箱验证后,等待审核。 审核通过后,然后就可以创建应用了。 然后填写...
继续阅读 »

主要实现步骤



  • 对接第三方平台,获取第三方平台的用户信息。

  • 利用该用户信息,完成本应用的注册。


qq登录接入


接入前的配置


qq互联


登录后,点击头像,进行开发者信息填写,等待审核。


image.png


邮箱验证后,等待审核。


image.png


审核通过后,然后就可以创建应用了。


image.png


然后填写一些网站信息,等待审核。审核通过后,即可使用。


开始接入



  1. 导入qq登录的sdk



<script type="text/javascript" charset="utf-8" src="https://connect.qq.com/qc_jssdk.js" data-appid="您应用的appid"
data-redirecturi="qq扫码后的回调地址(上面配置中可以查到)">
script>


  1. 点击qq登录,弹出扫码窗口。


// QQ 登录的 URL
const QQ_LOGIN_URL =
'https://graph.qq.com/oauth2.0/authorize?client_id=您的appid&response_type=token&scope=all&redirect_uri=您的扫码后的回调地址'
window.open(
QQ_LOGIN_URL,
'oauth2Login_10609',
'height=525,width=585, toolbar=no, menubar=no, scrollbars=no, status=no, location=yes, resizable=yes'
)


  1. 挂起qq登录。需要注意的是,扫码登录成功后,调试代码需要在线上环境。


"qqLoginBtn" v-show="false">

// QQ 登录挂起
onMounted(() => {
QC.Login(
{
btnId: 'qqLoginBtn' //插入按钮的节点id
},
// 登录成功之后的回调,但是需要注意,这个回调只会在《登录回调页面中被执行》
// 登录存在缓存,登录成功一次之后,下次进入会自动重新登录(即:触发该方法,所以我们应该在离开登录页面时,注销登录)
// data就是当前qq的详细信息
(data, opts) => {
console.log('QQ登录成功')
// 1. 注销登录,否则在后续登录中会直接触发该回调
QC.Login.signOut()
// 2. 获取当前用户唯一标识,作为判断用户是否已注册的依据。(来决定是否跳转到注册页面)
const accessToken = /access_token=((.*))&expires_in/.exec(
window.location.hash
)[1]
// 3. 拼接请求对象
const oauthObj = {
nickname: data.nickname,
figureurl_qq_2: data.figureurl_qq_2,
accessToken
}
// 4. 完成跨页面传输 (需要将数据传递到项目页面,而非qq登录弹框页面中进行操作)
brodacast.send(oauthObj)

// 针对于 移动端而言:通过移动端触发 QQ 登录会展示三个页面,原页面、QQ 吊起页面、回调页面。并且移动端一个页面展示整屏内容,且无法直接通过 window.close() 关闭,所以在移动端中,我们需要在当前页面继续进行后续操作。
oauthLogin(LOGIN_TYPE_QQ, oauthObj)
// 5. 在 PC 端下,关闭第三方窗口
window.close()
}
)
})


  1. 跨页面窗口通信


想要实现跨页面信息传输,通常有两种方式:



  • BroadcastChannel:允许 同源 的不同浏览器窗口,Tab页,frame或者 iframe 下的不同文档之间相互通信。但是会存在兼容性问题。

  • localStorage + window.onstorage:通过localStorage 进行 同源 的数据传输。用来处理 BroadcastChannel 不兼容的浏览器。以前写过一篇文章


// brodacast.js
// 频道名
const LOGIN_SUCCESS_CHANNEL = 'LOGIN_SUCCESS_CHANNEL'

// safari@15.3 不支持 BroadcastChannel,所以我们需要对其进行判定使用,在不支持 BroadcastChannel 的浏览器中,使用 localstorage
let broadcastChannel = null
if (window.BroadcastChannel) {
broadcastChannel = new BroadcastChannel(LOGIN_SUCCESS_CHANNEL)
}

/**
* 等待 QQ 登录成功
* 因为 QQ 登录会在一个新的窗口中进行,用户扫码登录成功之后会回调《新窗口的 QC.Login 第二参数 cb》,而不会回调到原页面。
* 所以我们需要在《新窗口中通知到原页面》,所以就需要涉及到 JS 的跨页面通讯,而跨页面通讯指的主要就是《同源页面的通讯》
* 同源页面的通讯方式有很多,我们这里主要介绍:
* 1. BroadcastChannel ->
https://developer.mozilla.org/zh-CN/docs/Web/API/BroadcastChannel

* 2. window.onstorage:注意:该事件不在导致数据变化的当前页面触发
*/

/**
* 等待回调,它将返回一个 promise,并携带对应的数据
*/

const wait = () => {
return new Promise((resolve, reject) => {
if (broadcastChannel) {
// 触发 message 事件时的回调函数
broadcastChannel.onmessage = async (event) => {
// 改变 promise 状态
resolve(event.data)
}
} else {
// 触发 localStorage 的 setItem 事件时回调函数
window.onstorage = (e) => {
// 判断当前的事件名
if (e.key === LOGIN_SUCCESS_CHANNEL) {
// 改变 promise 状态
resolve(JSON.parse(e.newValue))
}
}
}
})
}

/**
* 发送消息。
* broadcastChannel:触发 message
* localStorage:触发 setItem
*/

const send = (data) => {
if (broadcastChannel) {
broadcastChannel.postMessage(data)
} else {
localStorage.setItem(LOGIN_SUCCESS_CHANNEL, JSON.stringify(data))
}
}

/**
* 清除
*/

const clear = () => {
if (broadcastChannel) {
broadcastChannel.close()
broadcastChannel = null
}
localStorage.removeItem(LOGIN_SUCCESS_CHANNEL)
}

export default {
wait,
send,
clear
}


  1. 拿到数据后,进行登录(自己服务器登录接口)操作。



  • 传入对应参数(loginType, accessToken)等参数进行用户注册判断。

  • 通过accessToken判断用户已经注册,那么我们就直接在后台查出用户名和密码直接登录了。

  • 通过accessToken判断用户未注册,那么我们将跳转到注册页面,让其注册。


 // 打开视窗之后开始等待
brodacast.wait().then(async (oauthObj) => {
// 登录成功,关闭通知
brodacast.clear()
// TODO: 执行登录操作
oauthLogin("QQ", oauthObj)
})

// oauthLogin.js
import store from '@/store'
import router from '@/router'
import { message } from '@/libs'
import { LOGIN_TYPE_OAUTH_NO_REGISTER_CODE } from '@/constants'

/**
* 第三方登录统一处理方法
*
@param {*} oauthType 登录方式
*
@param {*} oauthData 第三方数据
*/

export const oauthLogin = async (oauthType, oauthData) => {
const code = await store.dispatch('user/login', {
loginType: oauthType,
...oauthData
})
// 返回 204 表示当前用户未注册,此时给用户一个提示,走注册页面
if (code === LOGIN_TYPE_OAUTH_NO_REGISTER_CODE) {
message('success', `欢迎您 ${oauthData.nickname},请创建您的账号`, 6000)
// 进入注册页面,同时携带当前的第三方数据和注册标记
router.push({
path: '/register',
query: {
reqType: oauthType,
...oauthData
}
})
return
}

// 否则表示用户已注册,直接进入首页
router.push('/')
}

微信扫码登录接入


微信开放平台


登录后,进行对应的应用注册,填写一大堆详细信息,然后进行交钱,就可以使用微信登录了。


image.png


开始接入


整个微信登录流程与QQ登录流程略有不同,分为以下几步:


1.通过 微信登录前置数据获取 接口,获取登录数据(比如 APP ID)。就是后台将一些敏感数据通过接口返回。


2.根据获取到的数据,拼接得到 open url 地址。打开该地址,展示微信登录二维码。移动端微信扫码确定登录。


// 2. 根据获取到的数据,拼接得到 `open url` 地址
window.open(
`https://open.weixin.qq.com/connect/qrconnect?appid=${appId}&redirect_uri=${redirectUri}&response_type=code&scope=${scope}&state=${state}#wechat_redirect`,
'',
'height=525,width=585, toolbar=no, menubar=no, scrollbars=no, status=no, location=yes, resizable=yes'
)

3.等待用户扫码后,从当前窗口中解析 window.location.search 得到用户的 code数据。 微信扫码后,会重定向到登录页面。


/**
* 微信登录成功之后的窗口数据解析
*/

if (window.location.search) {
const code = /code=((.*))&state/.exec(window.location.search)[1]
if (code) {
brodacast.send({
code
})
// 关闭回调网页
window.close()
}
}

4.根据 appId、appSecret、code 通过接口获取用户的 access_token


5.根据 access_token 获取用户信息


6.通过用户信息触发 oauthLogin 方法。


调用的接口,都是后端通过微信提供的api来获取到对应的数据,然后再通过接口返回给开发者。  以前也写过微信登录文章


// 等待扫码登录成功通知
brodacast.wait().then(async ({ code }) => {
console.log('微信扫码登录成功')
console.log(code)
// 微信登录成功,关闭通知
brodacast.clear()
// 获取 AccessToken 和 openid
const { access_token, openid } = await getWXLoginToken(
appId,
appSecret,
code
)
// 获取登录用户信息
const { nickname, headimgurl } = await getWXLoginUserInfo(
access_token,
openid
)
console.log(nickname, headimgurl)
// 执行登录操作
oauthLogin(LOGIN_TYPE_WX, {
openid,
nickname,
headimgurl
})
})

需要注意的是,在手机端,普通h5页面是不能使用微信扫码登录的。


总结


相同点



  • 接入前需要配置一些内容信息。

  • 都需要在线上环境进行调试。

  • 都是扫码后在三方窗口中获取对应的信息,发送到当前项目页面进行请求,判断用户是否已经注册,还是未注册。已经注册时,调用login接口时,password直接传递空字符串即可,后端可以通过唯一标识,获取到对应的用户名和密码,直接返回token进行登录。未注册,就跳转到注册页面,让其注册。


不同点



  • qq接入需要导入qc_sdk。

  • qq直接扫码后即可获取到用户信息,就可以直接调用login接口进行判断用户是否注册了。

  • 微信扫码后,获取code来换取access_token, openid,然后再通过access_token, openid来换取用户信息。然后再调用login接口进行判断用户是否注册了。


作者:Spirited_Away
来源:juejin.cn/post/7311343161363234866
收起阅读 »

工作6年了日期时间格式化还在写YYYY疯狂给队友埋雷

前言 哈喽小伙伴们好久不见,今天来个有意思的雷,看你有没有埋过。 正文 不多说废话,公司最近来了个外地回来的小伙伴,在广州工作过6年,也是一名挺有经验的开发。 他提交的代码被小组长发现有问题,给打回了,原因是里面日期格式化的用法有问题,用的Simpl...
继续阅读 »

前言



哈喽小伙伴们好久不见,今天来个有意思的雷,看你有没有埋过。



正文



不多说废话,公司最近来了个外地回来的小伙伴,在广州工作过6年,也是一名挺有经验的开发。




他提交的代码被小组长发现有问题,给打回了,原因是里面日期格式化的用法有问题,用的SimpleDateFormat,但不知道是手误还是什么原因,格式用了YYYY-MM-dd。




这种写法埋了一个不大不小的雷。




用一段测试代码就可以展示出来问题



1.jpg



打印结果如下:



2.jpg



很明显,使用YYYY时,2023年变成了2024年,在正常情况下可能没问题,但是在跨年的时候大概率就会有问题了。




原因比较简单,与小写的yyyy不同,大写的YYYY表示一个基于周的年份。它是根据周计算的年份,而不是基于日历的年份。通常情况下,两者的结果是相同的,但在跨年的第一周或最后一周可能会有差异。




比如我如果换成2023-12-30又不会有问题了



3.jpg



另外,Hutool工具类本身是对Java一些工具的封装,DateUtil里面也有用到SimpleDateFormat,因此也会存在类似的问题。



4.jpg



避免这个问题的方法也十分简单,要有公用的格式类,所有使用日期格式的地方都引用这个类,这个类中就定义好yyyy-MM-dd想给的格式即可,这样就不会出现有人手误给大家埋雷了。



总结




  1. 日期时间格式统一使用yyyy小写;

  2. 日期格式要规定大家都引用定义好的工具类,避免有人手误打错。




最后再回头想一想,这种小问题并不会马上暴露出来,倘若没有被发现,到了明年元旦,刚好跨年的时候,是不是就要坑死一堆人了。



作者:程序员济癫
来源:juejin.cn/post/7269013062677823528
收起阅读 »

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

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

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


序言


大家好,我是大澈!


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


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


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


1. 需求分析


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


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


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


图片


2. 实现步骤


2.1 实现前的说明


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


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


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


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


2.2 实现代码


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


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


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


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

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


3. 问题详解


3.1 什么是Open Graph协议?


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


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


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


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


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


图片


3.2 什么是Twitter Cards?


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


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



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

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

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

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


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


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


图片


图片


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

拼多多算法题,是清华考研真题!

写在前面 在 LeetCode 上有一道"备受争议"的题目。 该题长期作为 拼多多题库中的打榜题 : 据同学们反映,该题还是 清华大学 和 南京大学 考研专业课中的算法题。 其中南京大学的出题人,还真贴心地针对不同解法,划分不同分值: 细翻评论区。 不...
继续阅读 »

写在前面


在 LeetCode 上有一道"备受争议"的题目。


该题长期作为 拼多多题库中的打榜题


出现频率拉满


据同学们反映,该题还是 清华大学南京大学 考研专业课中的算法题。



其中南京大学的出题人,还真贴心地针对不同解法,划分不同分值:




细翻评论区。


不仅是拼多多,该题还在诸如 神州信息滴滴出行 这样的互联网大厂笔试中出现过:





但,这都不是这道题"备受争议"的原因。


这道题最魔幻的地方是:常见解法可做到 O(n)O(n) 时间,O(1)O(1) 空间,而进阶做法最快也只能做到 O(n)O(n) 时间,O(logn)O(\log{n}) 空间


称作"反向进阶"也不为过。


接下来,我将从常规解法的两种理解入手,逐步进阶到考研/笔面中分值更高的进阶做法,帮助大家在这题上做到尽善尽美。


毕竟在一道算法题上做到极致,比背一段大家都会"八股文",在笔面中更显价值。


题目描述


平台:LeetCode


题号:LCR 161 或 53


给你一个整数数组 nums,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。


子数组是数组中的一个连续部分。


示例 1:


输入:nums = [-2,1,-3,4,-1,2,1,-5,4]

输出:6

解释:连续子数组 [4,-1,2,1] 的和最大,为 6

示例 2:


输入:nums = [1]

输出:1

示例 3:


输入:nums = [5,4,-1,7,8]

输出:23

提示:



  • 1<=nums.length<=1051 <= nums.length <= 10^5

  • 104<=nums[i]<=104-10^4 <= nums[i] <= 10^4


进阶:如果你已经实现复杂度为 O(n)O(n) 的解法,尝试使用更为精妙的分治法求解。


前缀和 or 线性 DP


当要我们求「连续段」区域和的时候,要很自然的想到「前缀和」。


所谓前缀和,是指对原数组“累计和”的描述,通常是指一个与原数组等长的数组。


设前缀和数组为 sumsum 的每一位记录的是从「起始位置」到「当前位置」的元素和。


例如 sum[x]sum[x] 是指原数组中“起始位置”到“位置 x”这一连续段的元素和。


有了前缀和数组 sum,当我们求连续段 [i,j][i, j] 的区域和时,利用「容斥原理」,便可进行快速求解。


通用公式:ans = sum[j] - sum[i - 1]



由于涉及 -1 操作,为减少边界处理,我们可让前缀和数组下标从 11 开始。在进行快速求和时,再根据原数组下标是否从 11 开始,决定是否进行相应的下标偏移。


学习完一维前缀和后,回到本题。


先用 nums 预处理出前缀和数组 sum,然后在遍历子数组右端点 j 的过程中,通过变量 m 动态记录已访问的左端点 i 的前缀和最小值。最终,在所有 sum[j] - m 的取值中选取最大值作为答案。


代码实现上,我们无需明确计算前缀和数组 sum,而是使用变量 s 表示当前累计的前缀和(充当右端点),并利用变量 m 记录已访问的前缀和的最小值(充当左端点)即可。


本题除了将其看作为「前缀和裸题用有限变量进行空间优化」以外,还能以「线性 DP」角度进行理解。


定义 f[i]f[i] 为考虑前 ii 个元素,且第 nums[i]nums[i] 必选的情况下,形成子数组的最大和。


不难发现,仅考虑前 ii 个元素,且 nums[i]nums[i] 必然参与的子数组中。要么是 nums[i]nums[i] 自己一个成为子数组,要么与前面的元素共同组成子数组。


因此,状态转移方程:


f[i]=max(f[i1]+nums[i],nums[i])f[i] = \max(f[i - 1] + nums[i], nums[i])

由于 f[i]f[i] 仅依赖于 f[i1]f[i - 1] 进行转移,可使用有限变量进行优化,因此写出来的代码也是和上述前缀和角度分析的类似。


Java 代码:


class Solution {
public int maxSubArray(int[] nums) {
int s = 0, m = 0, ans = -10010;
for (int x : nums) {
s += x;
ans = Math.max(ans, s - m);
m = Math.min(m, s);
}
return ans;
}
}

C++ 代码:


class Solution {
public:
int maxSubArray(vector<int>& nums) {
int s = 0, m = 0, ans = -10010;
for (int x : nums) {
s += x;
ans = max(ans, s - m);
m = min(m, s);
}
return ans;
}
};

Python 代码:


class Solution:
def maxSubArray(self, nums: List[int]) -> int:
s, m, ans = 0, 0, -10010
for x in nums:
s += x
ans = max(ans, s - m)
m = min(m, s)
return ans

TypeScript 代码:


function maxSubArray(nums: number[]): number {
let s = 0, m = 0, ans = -10010;
for (let x of nums) {
s += x;
ans = Math.max(ans, s - m);
m = Math.min(m, s);
}
return ans;
};


  • 时间复杂度:O(n)O(n)

  • 空间复杂度:O(1)O(1)


分治


“分治法”的核心思路是将大问题拆分成更小且相似的子问题,通过递归解决这些子问题,最终合并子问题的解来得到原问题的解。


实现分治,关键在于对“递归函数”的设计(入参 & 返回值)。


在涉及数组的分治题中,左右下标 lr 必然会作为函数入参,因为它能用于表示当前所处理的区间,即小问题的范围。


对于本题,仅将最大子数组和(答案)作为返回值并不足够,因为单纯从小区间的解无法直接推导出大区间的解,我们需要一些额外信息来辅助求解。


具体的,我们可以将返回值设计成四元组,分别代表 区间和前缀最大值后缀最大值最大子数组和,用 [sum, lm, rm, max] 表示。


有了完整的函数签名 int[] dfs(int[] nums, int l, int r),考虑如何实现分治:



  1. 根据当前区间 [l,r][l, r] 的长度进行分情况讨论:

    1. l=rl = r,只有一个元素,区间和为 nums[l]nums[l],而 最大子数组和、前缀最大值 和 后缀最大值 由于允许“空数组”,因此均为 max(nums[l],0)\max(nums[l], 0)

    2. 否则,将当前问题划分为两个子问题,通常会划分为两个相同大小的子问题,划分为 [l,mid][l, mid][mid+1,r][mid + 1, r] 两份,递归求解,其中 mid=l+r2mid = \left \lfloor \frac{l + r}2{} \right \rfloor




随后考虑如何用“子问题”的解合并成“原问题”的解:



  1. 合并区间和 (sum): 当前问题的区间和等于左右两个子问题的区间和之和,即 sum = left[0] + right[0]

  2. 合并前缀最大值 (lm): 当前问题的前缀最大值可以是左子问题的前缀最大值,或者左子问题的区间和加上右子问题的前缀最大值。即 lm = max(left[1], left[0] + right[1])

  3. 合并后缀最大值 (rm): 当前问题的后缀最大值可以是右子问题的后缀最大值,或者右子问题的区间和加上左子问题的后缀最大值。即 rm = max(right[2], right[0] + left[2])

  4. 合并最大子数组和 (max): 当前问题的最大子数组和可能出现在左子问题、右子问题,或者跨越左右两个子问题的边界。因此,max 可以通过 max(left[3], right[3], left[2] + right[1]) 来得到。


一些细节:由于我们在计算 lmrmmax 的时候允许数组为空,而答案对子数组的要求是至少包含一个元素。因此对于 nums 全为负数的情况,我们会错误得出最大子数组和为 0 的答案。针对该情况,需特殊处理,遍历一遍 nums,若最大值为负数,直接返回最大值。


Java 代码:


class Solution {
// 返回值: [sum, lm, rm, max] = [区间和, 前缀最大值, 后缀最大值, 最大子数组和]
int[] dfs(int[] nums, int l, int r) {
if (l == r) {
int t = Math.max(nums[l], 0);
return new int[]{nums[l], t, t, t};
}
// 划分成两个子区间,分别求解
int mid = l + r >> 1;
int[] left = dfs(nums, l, mid), right = dfs(nums, mid + 1, r);
// 组合左右子区间的信息,得到当前区间的信息
int[] ans = new int[4];
ans[0] = left[0] + right[0]; // 当前区间和
ans[1] = Math.max(left[1], left[0] + right[1]); // 当前区间前缀最大值
ans[2] = Math.max(right[2], right[0] + left[2]); // 当前区间后缀最大值
ans[3] = Math.max(Math.max(left[3], right[3]), left[2] + right[1]); // 最大子数组和
return ans;
}
public int maxSubArray(int[] nums) {
int m = nums[0];
for (int x : nums) m = Math.max(m, x);
if (m <= 0) return m;
return dfs(nums, 0, nums.length - 1)[3];
}
}

C++ 代码:


class Solution {
public:
// 返回值: [sum, lm, rm, max] = [区间和, 前缀最大值, 后缀最大值, 最大子数组和]
vector<int> dfs(vector<int>& nums, int l, int r) {
if (l == r) {
int t = max(nums[l], 0);
return {nums[l], t, t, t};
}
// 划分成两个子区间,分别求解
int mid = l + r >> 1;
auto left = dfs(nums, l, mid), right = dfs(nums, mid + 1, r);
// 组合左右子区间的信息,得到当前区间的信息
vector<int> ans(4);
ans[0] = left[0] + right[0]; // 当前区间和
ans[1] = max(left[1], left[0] + right[1]); // 当前区间前缀最大值
ans[2] = max(right[2], right[0] + left[2]); // 当前区间后缀最大值
ans[3] = max({left[3], right[3], left[2] + right[1]}); // 最大子数组和
return ans;
}
int maxSubArray(vector<int>& nums) {
int m = nums[0];
for (int x : nums) m = max(m, x);
if (m <= 0) return m;
return dfs(nums, 0, nums.size() - 1)[3];
}
};

Python 代码:


class Solution:
def maxSubArray(self, nums: List[int]) -> int:
def dfs(l, r):
if l == r:
t = max(nums[l], 0)
return [nums[l], t, t, t]
# 划分成两个子区间,分别求解
mid = (l + r) // 2
left, right = dfs(l, mid), dfs(mid + 1, r)
# 组合左右子区间的信息,得到当前区间的信息
ans = [0] * 4
ans[0] = left[0] + right[0] # 当前区间和
ans[1] = max(left[1], left[0] + right[1]) # 当前区间前缀最大值
ans[2] = max(right[2], right[0] + left[2]) # 当前区间后缀最大值
ans[3] = max(left[3], right[3], left[2] + right[1]) # 最大子数组和
return ans

m = max(nums)
if m <= 0:
return m
return dfs(0, len(nums) - 1)[3]

TypeScript 代码:


function maxSubArray(nums: number[]): number {
const dfs = function (l: number, r: number): number[] {
if (l == r) {
const t = Math.max(nums[l], 0);
return [nums[l], t, t, t];
}
// 划分成两个子区间,分别求解
const mid = (l + r) >> 1;
const left = dfs(l, mid), right = dfs(mid + 1, r);
// 组合左右子区间的信息,得到当前区间的信息
const ans = Array(4).fill(0);
ans[0] = left[0] + right[0]; // 当前区间和
ans[1] = Math.max(left[1], left[0] + right[1]); // 当前区间前缀最大值
ans[2] = Math.max(right[2], right[0] + left[2]); // 当前区间后缀最大值
ans[3] = Math.max(left[3], right[3], left[2] + right[1]); // 最大子数组和
return ans;
}

const m = Math.max(...nums);
if (m <= 0) return m;
return dfs(0, nums.length - 1)[3];
};


  • 时间复杂度:O(n)O(n)

  • 空间复杂度:递归需要函数栈空间,算法每次将当前数组一分为二,进行递归处理,递归层数为 logn\log{n},即函数栈最多有 logn\log{n} 个函数栈帧,复杂度为 O(logn)O(\log{n})


总结


虽然,这道题的进阶做法相比常规做法,在时空复杂度上没有优势。


但进阶做法的分治法更具有 进一步拓展 的价值,容易展开为支持「区间修改,区间查询」的高级数据结构 - 线段树。


实际上,上述的进阶「分治法」就是线段树的"建树"过程。


这也是为什么「分治法」在名校考研课中分值更大,在大厂笔面中属于必选解法的原因,希望大家重点掌握。


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

大厂是怎么封装api层的?ts,axios 基于网易公开课

web
先看一下使用方法 先声明,这套写法是我在一次网易公开课学来的,说是大厂内部是怎么封装请求的,本人只是给它改成ts版本。 挺香的。 上核心代码 代码一:utils/request/getrequest.ts import axios, { type Axi...
继续阅读 »

先看一下使用方法
请求封装2.png


先声明,这套写法是我在一次网易公开课学来的,说是大厂内部是怎么封装请求的,本人只是给它改成ts版本。
挺香的。


上核心代码


WX20231124-143933@2x.png


代码一:utils/request/getrequest.ts



import axios, { type AxiosRequestConfig, type CancelTokenSource } from "axios";
import { manualStopProgram } from '@/utils/index';

import server from "./server";
import type { RequestConfig, ApiRouter, ServerRes } from './server.types'

class Requestextends keyof ApiRouter = keyof ApiRouter> {
requestRouter: ApiRouter = {} as ApiRouter
requestTimes = 0;
requestMap: Record<string, CancelTokenSource> = {};
toLogined = false

/**
*
@feat <注册请求路由>
*/

parseRouter(routerName: T, defaultAxiosConfigMap: Record) {
const apiModule = this.requestRouter[routerName] = {} as ApiRouter[T]

Object.entries(defaultAxiosConfigMap).forEach((item) => {
type ApiName = keyof ApiRouter[T]

const [apiName, defaultRequestConfig] = item as [ApiName, RequestConfig];

const request = this.sendMes.bind(
this,
routerName,
apiName,
defaultRequestConfig,
);

apiModule[apiName] = request as ApiRouter[T][ApiName]
apiModule[apiName].state = "ready";
});
}
async sendMes<ApiName extends keyof ApiRouter[T] = keyof ApiRouter[T]>(
routerName: T,
apiName: ApiName,
defaultRequestConfig: RequestConfig,
requestParams: Record<string, any>,
otherAxiosConfig?: RequestConfig
): Promise<ServerRes> {
this.requestTimes += 1;

return new Promise(async (resolve, reject) => {
try {
const selfMe = this.requestRouter[routerName][apiName];
const requstConfig: RequestConfig = {
...defaultRequestConfig,
...otherAxiosConfig,
data: requestParams,
};

/**
*
@feat <取消上一个同url请求>
*
@remarks [注:
* 个别页面需要同个api地址,多次请求,请传uniKey 作为区分,
* 不然有概率出现上一个请求被干掉
* ]
*/

if (selfMe.state === 'pending') this.cancelLastSameUrlRequest(requstConfig);

// 保险方案,传了 uniKey 才取消请求
// if (selfMe.state === 'pending' && requstConfig.uniKey) this.cancelLastSameUrlRequest(requstConfig);

const successCb = (res: ServerRes) => {
const ret = this.responseHandle(res, requstConfig)
resolve(ret);
};
const failCb = (error: unknown) => {
console.error("接口报错: " + requstConfig.url, error);
// 处理错误逻辑
throw error;
};
const complete = () => {
selfMe.state = "ready";
this.requestTimes -= 1;

if (this.requestTimes === 0) {
this.toLogined = false;
}
};

selfMe.state = "pending";
requstConfig.cancelToken = this.axiosSourceHandle(requstConfig).token;

await server(requstConfig).then(successCb).catch(failCb).finally(complete);

} catch (error) {
reject(error);
}
})
}

responseHandle(res: ServerRes, config: RequestConfig) {
const { code } = res;
console.warn(`请求返回: ${config.url}`, res);

if (code === 405) throw String("405 检查请求方式");
if (code === 401) this.toLogin();
if (code !== 200) throw String(res.message);

return res;
}

toLogin() {
if (this.toLogined) return;
throw String("请先登录");
}

generateReqKey(requestConfig: RequestConfig) {
return `${requestConfig.url}__${requestConfig.uniKey || ''}`
}
axiosSourceHandle(requestConfig: RequestConfig) {
const cancelToken = axios.CancelToken;
const source = cancelToken.source();

const reqKey = this.generateReqKey(requestConfig);
this.requestMap[reqKey] = source;

return source;
}
// 处理取消上一个请求
cancelLastSameUrlRequest(requestConfig: RequestConfig) {
const reqKey = this.generateReqKey(requestConfig);
const currentReqKey = this.requestMap[reqKey];

currentReqKey.cancel(`${manualStopProgram} reqKey: ${reqKey}`); // manualStopProgram 是一个标识,让外面的提示框忽略报错
}
}


export default new Request();



代码二:utils/request/server.ts


import axios from "axios";
import { UserInfo } from "@/utils/index";
import type { RequestConfig, ServerRes } from "./server.types";

export default async function server(
axiosRequestConfig: RequestConfig
): Promise<ServerRes> {
const token = UserInfo.getToken() || "";
const reqData = (() => {
const data = axiosRequestConfig.data;
const isFormData = data instanceof FormData;

if (isFormData) {
data.
append("token", token);
return data;
}
return {
...data,
token
};
}
)();

const { data: resBody, status } = await axios({
...axiosRequestConfig,
withCredentials: true,
data: reqData
}
).
catch((err) => {
const errMsg = err && typeof err === "object" && err !== null && "message" in err
if (errMsg) throw err.message;
throw err;
}
);

return resBody;
}

export {
server
}

import type { AxiosRequestConfig } from "axios";
import type { Api } from "@/apis/index";

export type RequestConfigany> = AxiosRequestConfig & {
uniKey?: string | number;
};

export type ApiConfig = {
params: T;
return: K;
};

export type List_pagiantion = {
page: number;
page_size: number;
};

// 这里有点绕,把各个api的参数和返回值 合成一个个特定的函数
export type ApiRouter = {
[K in keyof Api]: {
[T in keyof Api[K]]: Api[K][T] extends ApiConfig<any, any>
? {
(params: Api[K][T]["params"], otherRequestConfig?: RequestConfig): Promise<{
message: string;
code: number;
data: Api[K][T]["return"];
}>;
state?: 'pending' | 'ready'
}
: never;
};
}

export type ApiRouter__requestConfig = {
[K in keyof Api]: {
[T in keyof Api[K]]: RequestConfig;
};
}
export type ServerRes = {
code: number,
message: string,
data: any
};

接下来是apis文件夹(即开头的那个图片),在这里配置接口信息,日常业务代码在这里写


接口.png


代码一 写api配置 :src/apis/modules/admin-admin/index.ts


import type { ApiRouter__requestConfig } from "@/utils/modules/request/server.types.d";

const indexAdmin: ApiRouter__requestConfig["adminAdmin"] = {
getList: {
method: "post",
url: "/admin/admin/getList"
},
};

export default indexAdmin;

代码二 写接口类型声明 :src/apis/modules/admin-admin/index.types.d.ts


import type { ApiConfig } from "@/utils/modules/request/server.types.d";

export type AdminAdmin = {
getList: ApiConfig<
{
page: number,
page_size: number,
phone?: string,
status?: ManagerStatus,
groups_id?: number
},
{
count: number,
list: {
id: number,
phone: string,
groups_id: number,
create_at: string,
status: number,
status_txt: string,
groups_txt: string
}[]
}
>,
};


代码三 注册路由 :src/apis/index.ts


import { request } from "@/utils/index";
import indexAdmin from "./modules/index-admin/index";

request.parseRouter("indexAdmin", indexAdmin);

export type Api = {
indexAdmin: IndexAdmin;
}

// 这个是另一个作用,到处配置项,配合接口做权限控制,下面在说
export function getApiConfigMap() {
return {
indexAdmin,
};
}

下面说说这个封装方式好在哪里:


1. 解决的痛点:


以往我们看到最常见的封装方式,就这种

export function Api1() {
return axios.get('xx')
}

export function Api2() {
return axios.get('xx')
}

export function Api3() {
return axios.get('xx')
}

export function Api4() {
return axios.get('xx')
}

这种就非常麻木,一直写函数,每一个都要写配置项,没有数据结构结构=(无法复用)。
如果换成上面的 const indexAdmin: ApiRouter__requestConfig["adminAdmin"] = {},这种写法,就有数据结构了,有了结构之后就可以进行组合复用
比如上面提到的 getApiConfigMap 可以把数据结构直接导出,配合接口做按钮级权限控制,
接口会返回一份配置项{authen1: '/admin/admin/getList'}。
我们二者一比对,就能判断出是否有权限了。


比如看下面代码
PermissionWrapper 是一个权限容器组件 hasPermission=true就显示按钮
store.state.myPermission?.enterpriseAlarm?.edit 是用 getApiConfigMap 和结构权限表配合生成的


        <PermissionWrapper
hasPermission={store.
state.myPermission?.enterpriseAlarm?.edit}
>

<el-button type="primary" size="small" text onClick={openEditDialog}>
编辑
el-button>

PermissionWrapper>

ts直接提示,写起来很舒服,快准狠
ts提示.png


2. 请求函数封闭又开放


经过上面的 parseRouter 注册路由之后,sendMes 生成了N个请求函数,独一无二的函数,里面的fail success 可以做的事情很多,不如限制登录,取消上一个请求等等。大家有啥想法欢迎评论区写出来,我们一起优化它。


sendMes 最后一个参数,保持了开放性,在调用的时候我们传入uniKey就可以取消上一个请求了,还有一些特殊的参数,随便造。


3. 方便提取Api类型的参数和返回值类型(这是我额外拓展的)


我们经常会需要把参数和返回值的类型拿出来到页面上使用,这时候,就可以通过下面这个XX全局声明拿到。


declare namespace XX {
export type PromiseParams = T extends Promise ? T : never;

/**
*
@feat < 提取 api 参数 >
*
@describe <
type GetListParams = XX.ExtractApiParams

* >
*/

export type ExtractApiParams<Function_type> = Function_type extends (e: infer U) => any
? Excludeundefined>
: never;

/**
*
@feat < 提取 api 返回值 >
*
@describe <
type GetListReturn = XX.ExtractApiReturnType

* >
*/

export type ExtractApiReturnType<Function_type> = PromiseParams<
ReturnType<Function_type>
>["data"];

/**
*
@feat < 提取 api 为分页的 list item>
*
@describe <
type TableRow = XX.ExtractPromiseListItem

* >
*/

export type ExtractPromiseListItem<Function_type> =
ExtractApiReturnType<Function_type>["list"][number];
}

下面是一些使用方法举例
image.png


image.png


image.png


image.png


用上面的写法很方便就能在页面中把具体的类型拿出来。做到一次类型声明,到处使用。


api封装是一个长期的话题,axios很好用,但其实它就是一个请求方法而已。相信大家也见过很多乱七八糟的写法。特别是一些老项目,想新增api都不知道放在哪个文件夹。


很幸运无意中看到网易公开课的老师们讲解,那时候他们写的是js版本,看到这种由配置对象直接生成api函数的做法瞬间眼前一亮,这不就是我一直在找的封装方式,满足了我所有的想象。感谢感谢


后来我花了点时间,让它变成ts版本,还封装XX这个全局声明,让它彻底好用起来。希望这个封装能让大家受益。


细心的读者可能会发现上面的代码,一直抛错误,但是却没有拦截提示。 这是笔者推崇的报错终止程序,而不是用return的方式。(js终止程序,我常用throw 替代 return


如果您有什么好的建议或想法,欢迎评论区留言。有用请点点赞,还有更多经验总结在路上。
嘴下留情,骂我倒无所谓,重要的是别把评论区搞得乌烟瘴气


原课程链接:js es5版本,有兴趣的可以看看。但我觉得它那个取消请求,得再升级一下不然万一同个页面调用同个接口2次,就会取消第一个请求了。所以我加装了uniKey作为标识。
live.study.163.com/live/index.…


作者:闸蟹
来源:juejin.cn/post/7304594468157849640
收起阅读 »

前端学一点Docker,不信你学不会

虽然前端很少跟docker打交道,但随着工作流程的自动化现代化,docker正变得越来越重要。无论你是希望扩展技能到全栈领域,还是想要炫技,掌握Docker基本知识都是前端小伙伴重要的一步。 什么是Docker Docker 是一个开源的应用容器引擎,可以让...
继续阅读 »

虽然前端很少跟docker打交道,但随着工作流程的自动化现代化,docker正变得越来越重要。无论你是希望扩展技能到全栈领域,还是想要炫技,掌握Docker基本知识都是前端小伙伴重要的一步。


什么是Docker



Docker 是一个开源的应用容器引擎,可以让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中,然后发布到任何流行的 Linux 机器上,也可以实现虚拟化。



我们知道,软件的安装是要区分系统环境的;即使是运行环境相同,依赖的版本一旦有所出入,也容易出现“我这里明明好使,你那里为啥不行“的问题。容器技术解决了环境一致性依赖管理的问题。


因此,容器是我们的项目的运行环境,而docker是容器的运行环境与管理平台。


关键词


镜像 (Image)


镜像是构建容器的模板,可以简单理解为类似Js中的class类或构造函数。


镜像中详细记录了应用所需的运行环境、应用代码、如何操作系统文件等等信息;


容器 (Container)


容器是镜像的运行实例。可以简单理解为”new 镜像()“的实例,通过docker命令可以任意创建容器。


当前操作系统(宿主机)与容器之间的关系,可以参照浏览器页面与iframe之间的关系。容器可以拥有独立的IP地址,网络,文件系统以及指令等;容器与容器、容器与”宿主机“之间以隔离的方式运行,每个容器中通常运行着一个(或多个)应用。


仓库 (Registry)


仓库是集中管理镜像的地方。类似于npm平台与npm包之间的关系。


如果我们将搭建项目环境的详细过程以及具体的依赖记录进镜像中,每当需要部署新服务时,就可以很容易的通过镜像,创建出一个个完整的项目运行环境,完成部署。


示例——安装启动Mysql


1. 安装Docker


具体过程可参考菜鸟教程,下面以macOS系统作为例子进行演示。


启动docker客户端如下:



打开系统终端(下面是在vscode的终端中演示),输入命令:


docker -v

效果如下:



说明docker已经安装并启动。


2. 下载Mysql镜像


下载镜像有点类似于安装npm包:npm install <包名>,这里输入docker镜像的安装命令:docker pull mysql来下载安装mysql的镜像:



安装结束后,输入镜像列表的查看命令:docker images



当然,通过docker的客户端App也可以看到:



3. 创建mysql镜像的容器,启动Mysql


输入启动容器命令:


docker run -d -p 3308:3306 -e MYSQL_ROOT_PASSWORD=123456 mysql

先来看下启动结果,下面的一堆数字是完整的容器id:



输入的这一串命令是什么意思?



  • docker run: 这是启动新容器的命令。

  • -d--detach 是使mysql服务在后台运行,而不是占用当前终端界面。

  • -p 3308:3306: 这是端口映射参数:

    • 创建容器会默认创建一个子网,与宿主机所处的网络互相隔离;mysql服务默认端口3306,如果要通过宿主机所在网络访问容器所处的子网络中的服务,就需要进行端口映射(不熟悉网络的可以看下《如何跟小白解释网络》)。

    • 宿主机的端口在前(左边),容器的端口在后(右边)。



  • -e MYSQL_ROOT_PASSWORD=123456: 设置环境变量 MYSQL_ROOT_PASSWORD=123456;也就是将mysql服务的root用户的密码为123456

  • mysql: 这是上面刚刚pull的镜像的名称。


通过上面的命令,我们启动了一个mysql镜像的容器,并将主机的3308端口映射到了容器所在子网中ip的3306端口,这样我们就可以通过访问主机的localhost:3308来访问容器中的mysql服务了。


4.访问Mysql服务


下面写一段nodeJs代码:


// mysql.js
const mysql = require('mysql');
const connection = mysql.createConnection({
host: 'localhost',
port: '3308',
user: 'root',
password: '123456',
database: '',
});
connection.connect();
// 显示全部数据库
connection.query('show databases;', function (err, rows, fields) {
if (err) {
console.log('[SELECT ERROR] - ', err.message);
return;
}
console.log('--------------------------SELECT----------------------------');
console.log(rows);
console.log('------------------------------------------------------------');
});

这里调用了nodeJs的mysql包,访问localhost:3308,用户名为root,密码为123456,运行结果下:


$ node mysql.js;
[SELECT ERROR] - ER_NOT_SUPPORTED_AUTH_MODE: Client does not support authentication
protocol requested by server; consider upgrading MySQL client

这里报错了,原因是mysql服务的认证协议与我们代码中的不同导致的。这里我们需要对mysql服务进行一些修改。


为此,我们需要进入mysql容器,对mysql进行一些直接的命令行操作。


5.进入容器


首先,我们需要知道容器的id,输入容器查看命令:docker ps,展示容器列表如下:



其中55cbcc600353就是我们需要的容器的短id,然后执行命令:docker exec -it 55cbcc600353 bash,以下是命令的解析:



  • docker exec:用于向运行中的容器发布命令。

  • -it:分配一个终端(伪终端),允许用shell命令进行交互。也就是将容器中的终端界面映射到宿主机终端界面下,从而对容器进行直接的命令行操作。

  • 55cbcc600353:容器ID或容器名称。

  • bash:这是要在容器内执行的命令。这里是启动了容器的Bash shell程序。


运行结果如下:



我们看到bash-4.4#  后闪烁的光标。这就是容器的bash shell命令提示符,这里输入的shell命令将会在容器环境中执行。


我们输入mysql登录命令,以root用户身份登录:mysql -uroot -p123456



成功登录mysql后,在mysql>命令提示符下输入:ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password By '123456';


这条命令用来修改’root’用户的认证方式为mysql_native_password ,将密码设置为123456,并允许来自任何主机(‘%’)的连接。


输入exit;命令退出mysql>命令提示符:



再按下:ctl+D退出容器终端,回到宿主机系统终端下。再次运行上面的js代码,效果如下:



这样我们就完成了本地mysql服务的部署。


结束


通过上面的简介以及安装部署mysql服务的例子,相信不了解docker的前端小伙伴已经有了一些概念;感兴趣的小伙伴可以继续深入,学习相关的知识。


作者:硬毛巾
来源:juejin.cn/post/7304538094782808105
收起阅读 »

社会现实告诉我,00后整顿职场就是个笑话

00后整顿职场,也算是我之前的关键词吧。 我硬怼老板要加班费和提成,和他们辩论什么是我的财产,什么是公司的财产。 甚至还能在即将被开除的时候,反将一军把老板开除。 而正是因为这一次把老板开除,让我得到了机会。可以站在了相对于之前更高的位置,来俯瞰整个职场。 也...
继续阅读 »

00后整顿职场,也算是我之前的关键词吧。


我硬怼老板要加班费和提成,和他们辩论什么是我的财产,什么是公司的财产。


甚至还能在即将被开除的时候,反将一军把老板开除。


而正是因为这一次把老板开除,让我得到了机会。可以站在了相对于之前更高的位置,来俯瞰整个职场。


也真正意义上让我感受到了,00后整顿职场,就是一个互联网笑话罢了。


1、职场宫斗,成功上位


我之前在苏州工作,堪称工作中的宫斗,并且在这场宫斗大戏中胜出,将原有的项目负责人开除,成功上位。


而这个项目存在的问题非常多,我就在六月被派遣去项目的总部合肥进行学习,等到打通项目的全部链路后,再回到苏州。


届时我将以这个项目的负责人,重新搭建团队,开展这个项目。所以我在合肥那边,以员工的身份深入各个工作组进行学习。


在市场部,运营部的办公大厅工作过,也在各部门的独立办公室工作过。


我感觉自己像个间谍,一边在以平级的打工人身份和我的同事们相处,一边又以苏州负责人的身份,参与那些领导才能参与的内部会议。


2、内心变化的开端


我在合肥总部工作中,接触了很多躺平摆烂的同事,但这个“躺平摆烂“要加上双引号。


他们是00后,90后,甚至有85后。如果放在三个月前,我可以不假思索地说,他们全都是我最讨厌的人。他们如同牛羊一般任人宰割,上级让加班,他们就加班,有时候加班甚至超过四五个小时也没有怨言。


我甚至从来没听他们感慨过为什么没有加班费。亲眼看着他们被自己的上级用一些与工作无关的鸡毛蒜皮之事骂得狗血淋头,但他们也只会在被骂完之后,背地里吐槽那个领导估计是在家被老婆骂了,才来拿他们泄愤。


我打听了他们的工资,只能说中规中矩,起码不是能让人当牛做马的数字。偶尔我见到一两个有骨气的人,觉得拿这么点钱就应该干这么点事。干不爽就马上离职,但马上就会有下一个人替补他的位置,形成闭环。


我惊讶于怎么有人能惹到这个地步,但后来和他们日渐熟落,我们一起吃饭,一起打游戏,一起下班顺路回家,还参加了他们的生日聚会。我发现他们活得其实真的很洒脱。一切都是随遇而安,下班时间一到,他们就真的可以无忧无虑。


因为他们有一份工资还行的工作,养活自己。他们没有啃老,也没有用卑鄙的手段,去抢想要努力的人应该分到的蛋糕,也压根不去想要赚很多钱,因为没有什么需要太高消费的需求。


加上现在的环境,找到一份可观收入的工作确实很难。所以公司偶尔的加班,领导偶尔的泄愤,这些毕竟还是少数时候的偶尔,也都没有超过他们的心理承受阈值,那也就得过且过了。


所以我们其实都一样,只是个普通人罢了。而像我们这样的普通人,取之不尽,用之不竭。这到底是好事还是坏事呢?


3、复杂的职场生态环境


建立在这个基础上,视觉转换到高层领导们这里。他们当着我的面说,这样的人就是个底层打工仔,缺人就招,加班照旧,心情不好还要扣他们的全勤绩效。


压根就不怕这些底层打工仔闹事,纵使有一两个所谓的决心者辞职,也能在很快时间找到下一位。


两者形成互补,共同铸就了这样恶劣的职场生态环境。但我说职场无法改变,远不止这么一点原因。


在这个项目中,我说好听一些只能算是项目负责人,在此之上还有着项目股东,这还要细分成大股东和小股东。而我所在的项目属于互联网赛道,也就是说需要一些新鲜事物的眼光和思维来对待。


但这些股东们经常提出一些奇怪的意见,就如同用微商时代的卖货思维,来指点直播带货,并且他们是出钱的股东,他们提出的战略方针不容我驳回,因为在他们的光辉历史中,有大量的成功案例,来佐证他们的思路是对的。


我刚开始觉得也有道理。他们能有钱投资,肯定是有什么过人的本领能让他们赚到钱,但是随着相处下来,我发现不过是他们本身家里条件就优越,在九几年就能拿出一百万给他们创业。


他们把这一百万分散到二十个领域,每个投资五万总能撞上那么一两个风口,让他们实现钱生钱。


九几年的五万也算是一笔不少的投资。他们这样的发财经历,让我很难不产生质疑,这不是给我我也行吗?


毕竟他们如果真的有什么过人的本领和远见,也不至于在每次内部开会之前,都要组织喊这样的口号:“好,很好,非常好,越来越好“


甚至试图把这样的口号,带到每一次迎接客户的项目介绍会上。我以自曝式要挟制止他们这个行为,我说如果你们这么干,那我当天就辞职,内部都是自己人,我可以陪你们这样弄,但如果对外这么搞,被录下来说我们是传销,我都不知道怎么辩解。


4、职场中的背锅人


他们就是这样坚信着自己能成功,是因为自己有过人的才华。所以自我洗脑着自己提出的方向没有错。如果出错了,亏损了,那一定是负责人的问题。


但好巧不巧,我就是那个负责人。我已经无数次告诉他们,我们这个项目压根就不需要穿黑丝短裙跳舞的小姐姐。


我甚至写了一篇报告给他们,分析我们的项目为什么不能用擦边这种手段引流。但他们执意要,说这样来流量快,我都有点分不清到底是他们自己想看,还是深信这样做确实是可行。


但如果最后这样还是没成功,导致项目亏损,大概率还是在我身上找原因吧。


面对他们这样的大佬,我心里很清楚,这已经远远不是宫斗了,这也绝对不是靠几个心计,或者有实力撑腰就能取胜上位了。这场权力的游戏,不是我等草民玩得起的。


5、换个思路,创造属于自己的职场


一边是被提供资金,但是瞎指挥的股东们摧残,一边是在有限的预算下,我作为负责人,确实很难做到尊重打工人的内心挣扎,回到苏州我虽然能身居高位,但我终将成为我曾经最鄙视的人。


我不要当这个背锅侠,我也不想在这个环境中,去逐渐接受这样的价值观。


这样看来确实如此,00后整顿职场不过是一场互联网的狂欢罢了。


这个题材的故事,也永远只能发生在职场的最底层。由一群家境优越,体验生活的公子哥和我这种不知好歹的普通人共同出演。


大部分人只是在手机屏幕前把我们当个乐子,成了扣个666,然后一起吃胜利的果实。没成,那就确实是看了个乐子。


或许是因为他们心里也清楚,凭我们压根就做不到。


00后现在确实整顿不了职场,因为社会的资源和命脉还不掌握在00后手上。


但就止步于此了吗?我曾说过我想有一个自己的小工作室,遵守劳动法,双休,按时发工资,交纳五险一金。


是的,换个思路,也许00后不需要整顿职场,而是直接创造属于自己的职场,那么接下来我就要向着这个目标去努力了,毕竟二十年后我也还是00后,不如到时候再来说00后整顿职场吧。


作者:程序员Winn
来源:juejin.cn/post/7311603432929984552
收起阅读 »

我在酷家乐这 4 年,项目成败与反思

引言 2023-12-06 是我在酷家乐的最后一天,想把我在酷家乐这 4 年主导落地的项目做个总结,聊聊每个项目的创立背景、结果成败,以及反思。为了防止业务敏感信息泄漏,文中不会涉及到任何业务情况,项目结果数据,项目截图等内容。 19 相遇 时间来到 19 年...
继续阅读 »

引言


2023-12-06 是我在酷家乐的最后一天,想把我在酷家乐这 4 年主导落地的项目做个总结,聊聊每个项目的创立背景、结果成败,以及反思。
为了防止业务敏感信息泄漏,文中不会涉及到任何业务情况,项目结果数据,项目截图等内容。


19 相遇


时间来到 19 年 8 月,那是我加入酷家乐的日子。作为 IC 投入到酷家乐用户增长团队,当时团队主要在做激励体系、积分抽奖、酷签到、勋章等 To C 促活业务。


新业务


19 年 10 月,用户平台线成立 “设计圈” 新项目,是个 To B 的 SaaS 业务,目的是打通企业内部设计孤岛,让企业内部设计师共享、共建、共成长。后被大家戏称 “小主站”,即主站的子功能 SaaS 化。
过程中我统一所有前台页面启动逻辑,增加启动的中间件机制,中间件机制也是首次被引入到页面启动流程中,对于多页、统一的场景至关重要;对于管理后台,引入当时比较前沿的 UForm(即现在的 Formily),并进行业务定制的封装,目的是简化表单、表格等场景的开发工作,而此动作也提效明显。在此感谢阿里对开源的贡献。
反思:



  1. 多页应用,需要有入口做全局逻辑的管控。而落地做法很多:html 入口/JS 统一使用固定的 boot 逻辑等等

  2. 垂直领域能做一定的技术轮子。例如:表单表格的管理后台场景,需要有垂直领域的组件来做提效,基础组件还不够

  3. 过程中担任 SM ,反推自己以全局视角考虑问题,并且关注团队成员的任务与过程


20 回归


20 年 2 月一半精力回归用户增长团队,直到 6 月份完全回归。


小程序平台


20 年 1 月,公司内部小程序业务增多,需要做一定的基础设施,以提升整体的开发效率。前端团队老大,推动成立“小程序平台”专项虚拟小组,由几个小程序的业务团队同学(设计圈也有小程序业务),以及基架组转岗过来的同学组成。
我主动负责其中的 CI/CD 部分,接入 Def(公司前端统一 CLI 工具),完成套件、构建、部署等功能。当时微信小程序还不支持 CLI 部署,只能借助 “微信小程序开发者” 工具,在 Windows/Mac 上使用,而公司已有工程化 Linux 相关的基建,完全用不了,故在 Windows 虚拟机上安装“微信小程序开发者”工具,并且本地启动 proxy server 与开发者工具互通,CLI 再调用 Windows 本地的 proxy server 完成互通。
反思:



  1. 微信开发者工具客户端等以 UI 的形式提供给使用方,对于小团队很友好,对于想集成到大团队自有工作流的系统中,很差(好在微信现在已提供 SDK 跨平台发布,以便于集成到现有系统中)

  2. 我只参与了“小程序平台”不到半年,随后平台越发庞大:包括微信公众号管理、用户管理,甚至域名管理、人员管理、运维中心、营销工具等微信本身已经提供的能力包装。投入了非常多的人力,但是我个人认为过于超前。主要原因是:

    1. 酷家乐本身各业务小程序并没有太多增长

    2. 边缘功能太多,绝大多数场景根本用不到。我理解仅需要这些核心能力:模板分发多商家小程序、CI/CD、组件库、脚手架。



  3. 基建不应太过超前,优先满足最核心、提效最高的能力。


TMS


对于 To C 的产品,用户增长当时主要靠运营驱动,借助一些营销获客、促留存的手段,而产品需求绝大部分来自于运营同学。而面向运营同学的工具,有 2 款:



  1. TMS:仿淘宝店铺装修的页面搭建平台,主要可以完成产品介绍页、营销页等功能的搭建;

  2. 云台:运营平台,面向 To C 用户的营销推送(短信、公众号、邮箱、站内信等场景)、广告位管理等核心应用场景。JS 全栈开发,包括对 MySQL、Redis 等持久层的直接调用。


对于 TMS 页面搭建平台,有个极大地痛点:所有的模块(前端开发的定制模块)很难开发与发布,所有模块都杂糅在一个 NPM 包内,所以开发一个模块的流程是这样:



  1. TMS 管理后台创建一个模块,就是元信息了拿到模块 ID

  2. Mod Package 里开发一个模块,包含展示场景和编辑场景的组件

  3. 将 NPM 包 Link 到 TMS 管理后台的仓库

  4. 启动仓库本地 debug mode (体验很差)



  5. 开发阶段结束,开始发布阶段




  6. 发布 NPM 包

  7. 分别安装到对外渲染、对内后台管理的 Repo 上

  8. 分别发布对外、对内系统


整体流程很长,导致业务开发同学更愿意 0-1 写一个静态页,而不是开发一个 TMS 模块,进而造成了业务模块并不是很多,生态不丰富。
我在开发 TMS 模块时也深感痛苦,故过程中对 “新开发一个模块” 的流程进行了改进。
整体原则就是将模块的安装、加载从主体中剥离,从 NPM 包转变为浏览器端运行时注入的模块。当时已经有了 SEED,它比较基础,且全局都有安装,它是一个运行时模块加载、管理器,可以简单类比 SeaJS。通过维护一个 Alias ,模块 key 与 JS/CSS CDN 列表的映射关系,来决定如何加载模块,而这个 Alias 本身也是通过一个大 JSON 进行保存。
那么解决思路很直观了,只需要保存一个 TMS 模块 Key 与模块打包后的 JS/CSS 产物,即可做到将模块的安装由 buildtime -> runtime,进而能做到模块的调试、打包、发布与 TMS 主系统完全隔离。
优化后的流程是这样的:



  1. TMS 管理后台创建一个模块,就是元信息了拿到模块 ID

  2. 各自业务仓库开发一个符合一定 Interface 的模块

  3. 业务仓库本地 debug (打开 TMS 测试环境,直接把模块请求代理到本地即可)



  4. 开发阶段结束,开始发布阶段




  5. 各业务仓库构建模块,产出 JS/CSS,并自动上传 CDN,修改 Alias JSON 完成发布


本地调试由于仅需要构建当前模块,所以开发体验很棒。


SEED 与微应用


但是 SEED 也有它自己的问题,Alias JSON 独立于现有其他发布平台维护,且无灰度、无回滚,是个很大的稳定性隐患。
当时公司基建(Pub)已初步成型,比较超前,核心是以最小可发布粒度的一站式解决方案,而首推的就是页面单元,能将传统的以 Repo 多页为发布单元,转为以独立页面为发布单元,且秒级发布、秒级回滚。前端微应用当时也初步成型,主要目的是拆分酷家乐工具的巨石应用,提升开发效能。
当时主站还在继续使用 SEED,前端微应用和 SEED 其实目标非常类似,核心都是独立开发、独立发布。这时产生了一个想法 “能否让前端微应用支持浏览器端运行时加载,以替代掉 SEED 模块管理部分的能力”,达到 “All in micro” 的效果。
此时,前端微应用的输出模式是 html 片段,此片段可以注入到 page 中,最终输出完整的 page html 给到浏览器,即拼接形式。页面与微应用可独立发布,在统一的 Node.js Page Render 层进行服务端拼接,以组装成一个可以由多团队共建的完整应用。
那么,做法很清晰了,需要将仅支持在服务端 html 拼接形式使用的微应用,扩展为支持浏览器端运行时动态获取微应用 html 片段,并注入到 DOM 中去,并解决 Script 等标签无法执行与如何同步有序执行的问题,这就诞生了“Pub 微应用加载器”。
此时已存在 Single SPA 或乾坤等库,独立发布的功能是大家共有的,沙箱&路由联动等特性是不需要的,所以也没有参考这些开源库实现。
此阶段之后就顺势推动 SEED 历史模块全量迁移 Pub 微应用,相对的好处是:



  1. 拥抱同样的基建(CI/CD),灰发&回滚等机制

  2. 无需页面预置环境

  3. 去中心化,微应用加载器分布式安装在各个微应用 or 页面 bundle 内,不到 8K (未压缩)


而 TMS 的新模块开发方式也由 SEED 模块过渡到使用 Pub 微应用模块。


公共包


基建相对比较成熟了,但是主站业务的公共包却一直比较混乱,质量也不高。“磨刀不误砍柴工”,工具库、业务组件库的重要性不言而喻,这半年也开启了公共库的创建和规范:



  1. types:以业务域划分,定义业务通用的类型单元,例如方案等

  2. utils:工具函数

  3. rc:业务特定的组件库

  4. etc...


这部分内容大多数公司做的事情类似,不细讲了。


反思



  1. HTML 是组成页面的基本单元,以它为切入点,相对以 JS Entry 能做更多事;

  2. 跨团队协作,独立发布,低耦合是效能王道

  3. 开源产品能解决部分通用问题,工作流的串联,整体架构还需独立设计

  4. 秒级发布&回滚,能解决绝大多数稳定性问题

  5. 发布卡点 or 审批对于新手是保护,对于老手是枷锁


H2 开始,也带来一些新的挑战:



  1. 如何快速搭建新站点

  2. 类似的区块如何复用,是否复制是个更好的选择?


21 创新


Star


基于 20 下半年业务上各种新站点搭建带来的效率以及质量的综合挑战,21 年初我在思考“是否要造一个全司共建共享的物料共享平台”,以打破团队间信息壁垒。
在此阶段我已经是敏捷组 TO,并且有一定的影响力,所以大家愿意跟着我的想法一起干,包括隔壁组同学。此时恰好 UED 团队同学有“设计物料共享”的想法,所以一拍即合,前端 5 人 + 设计 2 人,自建组成虚拟小组,利用业余时间创建:Star 物料平台
平台设想大而全:



  1. 开发物料:Web 端、VSCode 插件、物料开发 CLI;分为 2 大类:区块、页面模板

  2. 设计物料:Web 端、Sketch 插件


这里主要讨论下开发物料,区块和页面模板都是参考自“飞冰”的设计,利用“复制”的手段,达到复用的目的。好处就是可以任意修改,不会因为 Interface 不满足而无法使用或扩展,相似的视觉效果都能直接拿来用。
而 VSCode 和 Sketch 插件的代码分别 Fork 自开源项目 IceWork、Kitchen(好像是),进行自有系统以及物料库的集成。
整个系统全栈 TS 开发,包括 Sketch 插件,服务端采用 NestJS+MySQL+Serverless 完成。
反思:



  1. 现在看来,区块的复用方式不如组件的形式,而且也没有用起来

  2. 页面模板倒是用来做初始化页面 or 微应用的规范了,也是一种将各业务线规范落地的平台

  3. 设计物料和 Sketch 插件使用量可观,相对于原始 File 下载分发,借助 Sketch 插件自动享受最新的设计物料比较高效


所以就区块来说,Star 是失败的,所以后来又逐步优化,新增了微应用文档的接入,因为微应用的使用方必然是需要阅读文档的,Star 就是一个比较好的集成微应用使用文档的平台,直接关联微应用的唯一 Key。


登录注册


在这之前我也兼账号体系(UIC)的前端负责人。酷家乐的账号体系也许是互联网行业最复杂的系统之一,它的复杂性来源于:



  1. 面向多种产品:To C、To B

  2. 面向多种身份:设计师、业主、从业主,在这之下又有很多细分行业


登录注册链路也有一定的复杂性:



  1. 注册链路极长,三方绑定 -> 手机验证 -> 选择身份 -> 推荐设计师/业主 -> 发放奖励

  2. 登录的形式:三方、扫码,弹窗登录、登录页面

  3. 登录过程中的风控拦截,图片验证,

  4. 登录过程中的 C & B 多账号绑定

  5. etc.. 还有很多没有列出来的


面临的挑战:



  1. 整体偏过程式的写法:你可以想想一个回调函数内部写了非常长的逻辑,且牵一发动全身

  2. 数据流与执行流的混乱:Promise 可能存在一直 Pending 的状态,例如某个 callback(resolve) 一直不执行,流程中的数据传递混乱,没有一条主线

  3. 以上带来的结果就是,涉及登录注册的任务估时 x2

  4. 美间、模袋等业务的加入,需要打通账号体系,并且复用同一套登录注册能力(但是不接受走同一个页面完成 SSO,这决定了后续的架构模式)


基于此,对登录注册组件进行了彻底的重构:



  1. 更合理的分层:基础包(通用 UIC 逻辑)、核心能力(支持配置化的形式确定外部需要何种登录注册方式)、酷家乐业务场景下的微应用 以及 其他业务场景下的页面

  2. 插件化的架构模式:借助 Tapable 完成异步串行的执行场景,增加登录注册前后等超 10 个 Hook,为后续扩展奠定了基础,并解决执行流问题

  3. 全局 Store:解决数据流问题

  4. 将原有非核心链路的逻辑拆分出接近 10 个插件,完成业务逻辑


结果:



  1. 扩展性:最初设想就是未来至少 3 年不需要重构登录注册模块,目前我认为至少 5 年是没有问题的

  2. 研发效率的提升:统一群核之下的几乎所有业务线的登录注册;后续几年的实战中,对于登录注册业务上的各种大需求,都没有对核心部分造成影响,通过插件都能满足需求

  3. 整体的 ROI 还是很高的


反思:



  1. 对核心业务的架构优化是值得投入的

  2. 插件化不仅用于工程化领域,也可用于业务,需要一定复用性、扩展性的场景都可考虑

  3. 架构是为了不让复杂度指数级爆炸


开发效率与规范


21 年的以上 2 个偏全司基建或特定业务,开发效率与规范也在持续进行:



  1. 为了多仓库共享代码,造了 Rocket CLI,定位是基于 subtree 的业务线级别的代码共享

  2. 规范了业务域为单位的 Owner 机制,并且不分端(PC、H5、小程序)

  3. 规范了 lint/babel/postcss/ts config,并且基于 Rocket 可以做到及时的共享更新

  4. 规范了所有 page 的启动方式,也基于 Rocket 进行共享

  5. 规范了全局弹窗的管理器,支持优先级队列机制

  6. etc...


基于 Rocket 的基建能力,做了到所有业务仓库共享同一套 xxx config,共享同一套业务启动逻辑。
但是也带来了一些棘手的问题:



  1. Git subtree 的机制,会让 Repo 历史记录混乱,掺杂很多不相干的 commit

  2. 高版本 Git subtree 提交时,部分同学总是无法 push 上去

  3. 随着时间推移,2 年后的今天,commit 已达 2k 多条(中间应是某些同学误操作带上去的),导致后期又增加了 reset 的机制,并且把 shared Repo 给重置了,进而又导致 shared Repo 与业务 Repo history 对不上...


这些问题只能通过比较懂的同学人肉操作下,以达到可以正常 push pull。所以后期会弃用 Rocket,改回 Npm Package,但是增加一些功能让他能保持定期更新。
反思:



  1. Subtree 有其局限性,最好的协作模式我认为一个业务线采用单一的 monorepo,通过基建去直面单 Repo 的构建性能问题,部署效率问题;

  2. 对于业务线的开发团队,最优先的是制定规范、落地规范到代码里、及时更新规范,以达到开发者同一套开发思路,对于协同开发效率是极好的;

  3. 不要分端,其他端的开发成本相对团队多人的沟通成本低很多;


22 再创新


22 年底有写过《2022 年终总结》,所以这里尽量谈的更宽泛一些, 有一定的相似处。
职位的变化,21 年中开始担任 Action Mgr,22 年初转为正式 Mgr。也会有一些管理思考,但是本文不会涉及。


客户端打包平台


没错,又开始造平台了。背景是酷家乐的主要用户在 PC 端,且绝大多数都使用客户端(基于 Electron),而且其他业务线也会开发自己的客户端(例如美间)。
所以除了一些基础 Electron 扩展能力的复用之外,长远来看最好能有个工程化平台,集成端侧的构建、打包、发布、分发等一系列的能力。这就是“客户端打包平台” 也可以称之为“客户端 DevOps 平台”。
做了如下事情:



  1. 首先需要一个打包环境,不仅要打包 Windows/Mac 上的 Electron 应用,后续还支持了 Android App 的打包

  2. 其次需要一个打包管理后台,包括:应用管理、构建管理、版本管理、发布管理以及权限

  3. 最后定义一套接入规范,以 Node.js 脚本形式接入,脚本接收一些入参,根据参数构建、打包、签名产出最终的安装包(固定目录),平台进行上传并回调更新 CDN URL、版本等信息


整体逻辑并不复杂,说一些它和 Web 页面发布的区别:



  1. 存在版本,线上版本碎片

  2. 存在复杂的更新机制,也有灰发机制

  3. 存在不同渠道分发不同安装包,便于后续的安装来源统计

  4. 多种打包目标:Windows/Mac/Android,不同目标会有提供不同的打包环境


对于平台,还有很多事没做,例如:数据看板,版本分布等,但是对于近几年足够了。
有些同学可能会不理解,和 Gitlab CI 有啥区别?
借助 CI 仅能完成任务的触发,而任务是需要特定的运行环境的,除此之外:版本的管理、灰发、渠道分发都是平台特有的能力。
反思:



  1. 针对核心业务做基建更不易出错


SSR


过去几年,随着基建升级,老的 FreeMarker(JAVA) + JQuery ,慢慢转变为 Nunjucks(Node.js)+ JQuery,再转变为 Nunjucks(Node.js)+ React。而到 React 阶段,服务端直出页面关键 HTML(SSR)已不存在。产生的结果就是来自搜索引擎的流量逐渐下滑,而 SEO 对于酷家乐来说至关重要,是个非常重要的流量窗口。为了拯救 SEO,22 年上半年开始了一些 SSR 的尝试。
但是,要做 SSR ,会和业界常用方案有所不同:不会采用 Next.js 类似全栈框架,因为此类全栈框架带来的问题是每个业务都需要独立的 Node.js 服务,还需要持续的观测稳定性,出问题对于业务开发者来说是非常棘手的,对开发者的要求极高。
所以 SSR 服务需要做到的效果:



  1. 每个 SSR 页面都可以独立发布,即使他们在一个 Repo

  2. 创建 SSR 容器服务,由 SSR 服务的开发者管理服务的稳定性,业务开发者无需关心

  3. 所有 SSR 页面都运行在这个容器内

  4. 所有 SSR 页面需要有沙箱,运行上下文隔离

  5. 需要有降级到 CSR 的策略


除了 SSR 服务本身之外,也需要有其周边的工具链:



  • 针对每个 SSR 页面构建打包为独立的 Server Entry

  • Server Entry 需要符合约定的 Interface,输入请求上下文,输出渲染结果

  • TS type 包,便于接入


方案详情页是第一个接入的页面,上线前借助 Apache Benchmark 工具做了一定的压测,上线结果也是很好的,此阶段 22 年中上线。
到此阶段,还有一些工程化问题需要联合基架组一起解决,深入集成到 Pub 系统内:



  1. 本地开发:支持 SSR 和 CSR 的同步开发,以及规范的本地开发调用链

  2. 构建:自动识别哪些页面需要走 SSR ,完成 Server Entry 构建

  3. 发布:发布后,对于页面信息的变更,秒级同步到 PR 与 SSR,完成应用的自动更新

  4. 运行:集成请求链路 浏览器 -> PR -> SSR,自动降级能力;以及 Pub 上配置包、语言包等能力的打通


此阶段在 22 年下半年完成,完成后对于业务开发者来说,开发一个 SSR 页面和 CSR 一样简单,不仅是 SEO 的提升,对于首屏优化也有效。
过程中也遇到了各种问题:



  1. 一些二方包实现时没有考虑 Node 端运行场景,例如使用了很多 window/navigator 等浏览器端全局变量,使用 jsdom 注入到每个页面上下文里解决(但也带了问预料之外问题,见 3)

  2. OOM 问题:随着 SSR 流量增多,有一天触发了一端代码的临界点,即运行几天后内存溢出,服务被 K8s Pod 自动重启,反复;排查下来是一个非常基础的监控模块,在并发的 HTTP 链接达到一定数量后进入另一个分支,这个分支对缓存的清理有问题,导致持续增长的内存没有被回收

  3. GC 频繁导致 CPU 增高:根因是有个页面使用到了 react-helmet 库管理 document head 信息,helmet 又使用了 react-side-effect 来处理 props 变化引发的副作用,问题就出现在我们 Mock 了 window/document 等信息,让库误认为当前运行环境在浏览器端,进而将本应无副作用的 string 处理,变成了 DOM 处理,让 Node.js 内 new space 的空间增多,进而引发频繁 GC。


可以看到目前的 SSR 方案也并不是完美的,虽然做了沙箱,但是本质他们还是运行在同一个线程之内的,共享同一个 CPU 资源和内存资源。当出现 GC 引发的 CPU 问题,或 OOM 引发的问题,就会导致整体的不可用。
所以解决这些问题的方案就只能做多进程,一个页面的 SSR 就启动一个独立进程,由主进程管控数据分发、应用更新,这样能充分利用多核 CPU,不至于一个页面 Bug 引发整体的雪崩。
反思:



  1. 之前做的平台更偏研发效率,SSR 能解决一定的业务问题

  2. 不一定一开始就要最完美的方案,保留一定扩展性,能解决当下问题就是最好的


其他


22 年也有一些效果不错的优化:



  • 帮助中心的核心 API 性能提升 70%,帮助中心 JS 全站开发,主要优化的是对 MySQL 调用相关的业务层的逻辑优化。同时也解决其稳定性问题,之前总会因为流量的翻倍导致服务短时间不可用;

  • Star 部分也在持续优化:新增了一些页面模板,微应用的文档是在这一年做的

  • 客户端的可观测行:本地日志优化,以及用户的一键上报

  • 重写富文本编辑器,并应用在多条业务线

  • etc


23 优化


23 年主要是对现有系统的优化,年中也由主站转岗到了国际站。


SSR & Star & 客户端



  • SSR 一些接入文档,最佳实践之类的文档编写

  • Star 权限管理;支持培训物料类目

  • 客户端监控体系的建设,接入现有监控平台

  • 客户端打包平台的持续优化等


国际站


国际站的技术沉淀基本等价于 3 年前的主站,所以还有很多问题需要解决,以及一些提效工具都没用上,感觉和大多数业务线有些脱节。
国际站有很大的特点:多语言、多货币、多集群,依赖的很多三方也不同,例如登录、支付场景。以上都和前端息息相关,其中和开发方式密切度非常高的,就是多语言。
而多语言,目前公司已有基建也比较完善:语言包 CDN 化 + 配置后台 + 项目粒度管理 + VSCode 插件提效。但是也由很多问题:治理困难,例如如何清理一些无用词条;验证困难,例如如何验证其他语种的有效性等。我目前还没有想到比较好的解决手段。


Magi 配置平台


除此之外,页面配置化的能力,对于运营可快速尝试各种增长手段也至关重要。目前运营会采用上文提到的 TMS 来搭建一些营销页、产品介绍页。但也有一些是不满足需求的:SEO、性能、多语言等。
除了 SEO 之外,另外 2 条通过优化 TMS 都还能解决。因为 TMS 的整体架构决定了,想要能支持 SSR 很难,更不必说内置的或二方的组件了。除了页面编排需求之外,还有这些诉求:



  1. 开发者编写的页面也需要有配置化的能力,而针对特定功能开发特定的后台,成本极高

  2. 页面配置化需要能根据国家、人群维度进行不同的展示

  3. 分站点,例如不同国家不同站点


为了满足以上需求,计划造一个低代码配置平台,以及低代码引擎。目前还处于非常早期的阶段,仅完成整体的架构设计和部分 Core & Editor 逻辑的编写。


总结


至此,酷家乐的旅程告一段落。
这段旅程里,做了很多针对研发效率、质量方面的工作,也为其他岗位角色(UED、运营、市场)带来了人效的提升。我相信每一份努力和效率的提升,都会让酷家乐进步一点点,让我们在这个竞争激烈的市场上赢得胜利的机会多一点点。在这里收获满满,未来祝愿群核科技越来越好!
再额外聊一下关于离职,我的看法。我们常看到某些同学因为个别同事的离职,而内心动摇,也决定离职,我曾经也这样。但是在加入酷家乐前,就告诉自己,直面自己内心,不要在乎他人的去留,只要能确定自己能有成长、有收获、与自己规划相符就足够了,共勉。



2023-12-08
于 良渚


作者:洋葱x
来源:juejin.cn/post/7310028335480619027
收起阅读 »

喊话各大流行UI库,你们的Select组件到底行不行啊?

web
各种 UI 库的 Select,你们能不能人性化一点! 最近在云效上合并代码,本想着懒的目的输入了非连续的关键字搜索分支,结果... 大概逻辑就是,搜索时必须输入连续的字母,比如,要找 “master-alpha”分支,非要输入 master-al 才能搜到...
继续阅读 »

各种 UI 库的 Select,你们能不能人性化一点!


最近在云效上合并代码,本想着懒的目的输入了非连续的关键字搜索分支,结果...


1.gif


大概逻辑就是,搜索时必须输入连续的字母,比如,要找 “master-alpha”分支,非要输入 master-al 才能搜到,像图中输入 “masal” 就完全搜索不到。这导致了很多场景下使用起来很不方便,例如我们只记得几个非连续的关键字,或者懒得打那么多连续的关键字来搜索,用户体验较差。


然后我又看了几个流行组件库的 Select。


Element-ui


2.gif


Antd


3.gif


Naive-ui


4.gif


全军覆没!


那我们来自己实现一个吧!先来两个实战图。


不带高亮的非连续搜索


6.gif


带高亮的非连续搜索


5.gif


实现不带高亮的非连续搜索


以vue3+ElementUI为例,在这里将会用到一个小小的js库叫sdm2来实现非连续的字符串匹配。


视图部分


<el-select
v-model="value"
size="large"
placeholder="Filter options"
filterable
:filter-method="q => (query = q)">

<el-option
v-for="item in optionsComputed"
:key="item.value"
:value="item.value"
:label="item.label">

el-option>
el-select>

没有什么特别的,就是加了个filterMethod函数将关键词赋值给query状态,然后optionsComputedquery值根据关键词进行筛选


import { match } from 'sdm2';

const options = [/* 选项列表 */];
const query = ref('');
const optionsComputed = computed(() => options.filter(({ label }) =>
// 使用sdm2的match函数筛选
match(label, query.value, {
// 忽略大小写匹配
ignoreCase: true,
}
)));

就这么简单完成了。


实现带高亮的非连续搜索


视图部分


高亮部分使用v-html动态解析html字符串。


<el-select
v-model="value"
size="large"
placeholder="Filter options"
filterable
:filter-method="q => (query = q)">

<el-option
v-for="item in optionsComputed"
:key="item.value"
:value="item.value"
:label="item.label">

<div v-html="item.highlight">div>
el-option>
el-select>

为了让匹配到的关键字高亮,我们需要将匹配到的关键字转换为html字符串,并将高亮部分用包裹,最后用v-html动态解析。


import { filterMap } from 'sdm2';

const options = [/* 选项列表 */];
const query = ref('');
const optionsComputed = computed(() =>
// 为了过滤出选项,并将到的转换为html字符串,此时我们要用sdm2的filterMap函数
filterMap(options, query.value, {
ignoreCase: true,

// 把matchStr返回的字符串作为被匹配项
matchStr: ({ label }) => label,

// 匹配到后转换为html高亮字符串
onMatched: (matchedStr) => `${matchedStr}`,

// 将匹配到的项转换转换为需要的格式,str为onMatched转换后的字符串,origin为数组的每项原始值
onMap: ({ str, origin }) => {
return {
highlight: str,
...origin
}
}
})
);

然后一个带高亮的非连续搜索就完成了。


总结


这样你的搜索库就又更智能点了吧,然后各位 UI 库作者,你们也可以考虑考虑这个方案,或者有哪位朋友愿意的话也可以去为他们提一个issue或PR。

作者:古韵
来源:juejin.cn/post/7310104657212178459
收起阅读 »

从一线城市回老家后的2023“躺平”生活

归家 22年的十月份,在上海工作了三年多的我回到了老家。 前端,20年二本毕业的,当时在上海看老家的招聘信息,感觉很棒,很心动。又因为公司在大裁员,刚刚好在最后一轮裁员的时候,被裁了,拿了赔偿金,因为房租没有到期,又最后玩了玩,回了老家。 现实的落差感    ...
继续阅读 »

归家


22年的十月份,在上海工作了三年多的我回到了老家。


前端,20年二本毕业的,当时在上海看老家的招聘信息,感觉很棒,很心动。又因为公司在大裁员,刚刚好在最后一轮裁员的时候,被裁了,拿了赔偿金,因为房租没有到期,又最后玩了玩,回了老家。


现实的落差感


    回到老家后,又休息了十几天吧,就开始看招聘的信息,之前在上海看着很心动的岗位,简历投了又投,要么回复你,岗位已招满,要么压根不理你(后来我才知道,是学历的问题,老家这边的国企,最低学历就是研究生了,压根不看你工作履历)。剩余那些搭理你的公司,大都是小公司,可能只有十个人左右,而且大多都是单休或者大小周,有些甚至五险一金都没有,工资也低的可怜,是之前的三分之一差不多。


    心里难免有很强烈的落差感,但是由于我们(们:我老公,那个时候我们还是情侣,从大学开始的,在一起快7年了)家都是在这边的,两边父母都在这,觉得我们之后就是要在这里发展的,我俩硬着头皮,每天划拉着招聘信息,投着简历,适当地去面试。


    中间有一家公司,我感觉还可以,然后想着去试试,干了两天半。


    刚去的第一天,技术团队是:一个后端和一个外包的后端,以及一个跟我一样刚入职的前端,一共就我们四个人,然后就是老板,只有我是女生。除此之外,还有保洁阿姨(中午会做饭,公司中午管饭)、人事小姐姐等一些非开发人员。下午开会,老板居然直接在会议室抽起了烟(熏得我不要不要的)!然后项目用的是ruoyi的架子,里面有些代码是那俩后端暂时写的,看起来有些乱。就这样干了两天,那俩后端,很爱抽烟,再加上老板也带头会议室开会还抽烟,整天感觉身边烟熏火燎的。


    到第三天的时候,中午开了个会,意思是,之前我们开发的好像需求都不行,并且又提了一堆新需求,还告诉我们说只有两三天的时间搞完。我就意识到不对劲,是逼着人加班,没死没活的干的那种。然后再加上被熏了三天,于是开完会,我就赶紧收拾着我的东西,跑路了,干不了,根本干不了。既不合理,而且办公环境很糟糕(每天烟熏火燎),还没有社保,据说年后才缴纳。下午他们打来电话,问我咋回事,还要给我加薪让我再回去,但我已决心不去那干了。后来的我一点都不后悔这样做,甚至觉得很明智。


    就这样继续在招聘软件上看着,有新岗位咯,就投,就面试。


     突然有一天的周日,我接到了一个电话,说我可以来上班,他们缴纳五险,是双休,还有餐补,并且薪资也比之前面试的也差不多(之前还有个公司给的薪资和他一样,但是他是大小周,我不想去),这种待遇的公司,对于目前的我来说,已经很可以了,然后我就同意了,并且两天后去入职,这家公司就是我现在的公司。相比之前那家“烟熏火燎”的公司,这家就正规了许多,可能因为总部在深圳吧。


我们订婚啦


既然工作稳定了,那就开始丰富生活。2023年02月05日,我们举办了订婚宴~




工作


    这边的前端工作不太是普通的传统前端,而是electron打包出来是个exe啊,或者是针对模型3d渲染引擎啊,依托于基于threeJs二次开发出来的一些第三方,之类的,总之跟之前做的不一样,之前的我做的都是h5、微信小程序、或者接入一些公众号之类的。所以与其说是在工作,不如说是一直在学习吧。公司也知道我不太会,于是乎就给我很长时间先学,先熟悉,然后再去一点点开发。并且我几乎没加过班。


我们结婚啦


后面一切按照计划进行,拍摄婚纱照、男方那边在忙着新房装修,我们这边在置办嫁妆、买车车等。


在2023年10月10日,我们举行了典礼。



安稳且平淡


现在的我们每天安安稳稳,我想着适当提升下自己的学历(因为我们这边的好单位,现在好像都要研究生毕业了),在看着咱们计算机考研408的一些科目(双11心血来潮,一下子买了六七百块的书,不看总觉得买书钱白瞎了TAT),但是每天下班回家,还是忍不住看一些电视剧啥的,佛系考研,阿弥陀佛,哈哈哈哈


我们从上海一直养到现在的猫猫~



这就是我跟大家分享的我的这2023年的一年的经历。说实话,回老家的确比在一线城市更真实,因为身边有父母,有家人,每个周末都可充实。一线城市是素质高、节奏快,人的整个思想境界感觉都跟老家这边的人不一样。但兜兜转转,回老家似乎也并不是“躺平”,有落差感,因为接触过好的了。反正无论怎样,感觉简单、安稳、快乐的过好每一天就挺好。我们一起加油吧~


作者:wenLi
来源:juejin.cn/post/7311206584205869096
收起阅读 »

年底事故频发,做前端会不会出大型事故?

炽天使-S-蛇女-甜甜果实 前言 一些乐子,一些思考,不喜勿喷,欢迎交流 最近崩的有点多,来看看都有哪些 语雀崩了... 阿里云崩了... 滴滴崩了... 腾讯视频崩了... ...... 刚看完《三体》三部曲,最后一部《三体3:死神永生》里面,宇宙因为...
继续阅读 »

炽天使-S-蛇女-甜甜果实



前言



一些乐子,一些思考,不喜勿喷,欢迎交流



最近崩的有点多,来看看都有哪些


语雀崩了...


阿里云崩了...


滴滴崩了...


腾讯视频崩了...


......


刚看完《三体》三部曲,最后一部《三体3:死神永生》里面,宇宙因为质量流失过大,也快崩溃死亡了,也不知道会不会出个后续,归零者把宇宙的宏观维度重新回到十维的情况...


......


似乎什么都在崩,哪哪都在崩,好在掘金没有崩,不然掘友都看不到文章了,给掘金点赞一波,服务稳住老狗



继续回到主题上,做个前端开发工程师会不会导致项目出现大型事故?


前端都包含的职责


关于前端这个岗位负责的部分,分三个大类来分析讨论



  1. 只负责开发页面的前端

  2. 负责页面开发,也负责基础架子搭建和组件开发的前端

  3. 负责开发、基础建设、服务部署等等相关工作的前端


1. 只负责开发页面的前端


项目架子是别人搭好的,大部分核心或共同组件是别人写好的,或者负责维护的


日常工作具体内容包括但不限于,使用 vue, react 等框架开发业务,实现各种与后端的数据交互,展示效果,以及用一些库或者手写一些组件库之类的实现某些特定的业务效果或场景


如果项目经过的必要的功能测试,并运行稳定的项目,如果出现突发大型事故,百分之九十九点九的问题,都是运维或后端服务方面的,需要背锅侠大概率也轮不到前端


咱该吃吃该喝喝,遇事别往心里搁



2. 负责页面开发,也负责基础架子搭建和组件开发的前端


在稍有规模的团队中,负责前端工程搭建,一些通用组件封装,以及打包相关的配置,包括一些基础的性能优化等工作的一般是前端老鸟,或者项目中的资深技术选手做的,这种角色一般是比较熟悉业务了或者是团队中的主力选手了


如果项目中遇到突发大型事故,一般是冲在一线,虽然问题百分之九十的可能不是前端范畴,剩下的那百分之十还是很有可能的,例如那一年的开源项目 Ant Design 圣诞节彩蛋事件,在某些公司直接彩蛋变炸弹💣


由于官方没有通知,也排查不出问题,谁知道这玩意儿竟然是个官方出的圣诞节彩蛋,如果需要背锅侠,很可能直接就是前端开发相关负责人背锅了



3. 负责开发、基础建设、服务部署等等相关工作的前端


这种前端一般多少涉及点全栈了,相关服务的操作权限可能也有。一般微小型公司这种情况比较常见,一人多职,稍微人多点的中大型公司服务器运维相关岗位,这种一般都有专门的部门或单独岗位的人负责统一管理,这种角色可能是一线负责人或者技术团队负责人等等


在中大型公司,这种什么都参与,也有相关权限的,可能资深大拿,或者技术部负责人之类的,如果是负责人一般也很少直接参与业务开发了


如果项目遇到突发大型事故,一线负责人一般有事儿都是第一时间知道,然后第一时间协调资源或者参与问题解决,小公司中可能老板都会直接参与协调解决,这种核心选手在小公司中一般不会受到什么处罚,如果是大公司中需要有人承担责任的,一般也可以甩锅出去,找个具体干活的开发背锅



分享身边发生过的一件事


小创业公司,一天去潜在客户那边演示项目,由于做的是 toB 的项目,目前还是自用阶段,没对外开放,日常都在公司是内网开发测试使用,然后老板有天安排去xx公司演示,然后团队中俩人就去了,俩人提前一个小时多到的,在他们连好投屏显示器后准备先点点看看,结果发现项目不能登录😅😅😅


赶紧排查原因,原来后端接口服务域名地址没有对外开放,外网不能访问


当时那俩人拿的是公司的mac演示本,没有装远程工具之类的,负责运维的那个人不在公司,在公司的只了解一点点运维,也就会点 Linux 了解 Nginx 那种,操作方面和专业运维差远了,运维电话联系指挥操作弄了约半小时也没好,后来放弃了现场演示了,改成只讲一下 PPT 了,最后不知道和那边是怎么交代的,反正后来没和那边的公司升级成商业合作伙伴


这时候背不背锅可能意义不大了,小创业公司如果是重要的演示直接搞砸了,公司的业务可能直接就没有了,公司活着都是个问题,员工工资能不能正常发都得看工资的家底支不支持了


怎么做能大概率不背锅


明确职责,积极沟通解决问题,多产出等等这些因素是一方面,人是社会动物,有人的地方就有江湖,有江湖的地方也可能需要下面的因素



  • 在团队中保持较强的竞争力,让领导觉的你性价比高

  • 大量参与核心业务开发,让领导觉的替换了你以后其他人上手代价高

  • 成为组织或团队的核心圈人员,和领导混熟了,凡事好商量(重要!!!)


写在最后


一般来说是做的越多,责任越大;认真对待工作,出问题也不用怕,兵来将挡,水来土掩...


这年头各路公司都在 “开猿劫留,减猿增笑”,日常中的我们也需要进行一些准备,以应对突如其来的事故或者变故,以不变应万变...


每个人的经历,认知都是不一样的,同样的人不同角度下的世界也是不一样的,有不同意见是非常正常的,欢迎探讨交流不一样的心得,互相学习,共同进步



如果喜欢本文章或感觉文章有用,用你那发财的小手点赞、收藏、关注再走呗 ^_^ 


微信公众号:草帽Lufei




作者:草帽lufei
来源:juejin.cn/post/7311602153783705627
收起阅读 »

Ant Design Mini 支持微信小程序啦!

web
Ant Design Mini 经过一段时间的开发与不断的努力,我们兴奋地宣布,Ant Desigin Mini 组件库中已有 16 个核心组件完成了微信小程序的适配工作!现在你不仅可以在支付宝小程序中使用 Ant Desigin Mini 组件库,也可以在微...
继续阅读 »

Ant Design Mini


经过一段时间的开发与不断的努力,我们兴奋地宣布,Ant Desigin Mini 组件库中已有 16 个核心组件完成了微信小程序的适配工作!现在你不仅可以在支付宝小程序中使用 Ant Desigin Mini 组件库,也可以在微信小程序中使用了!


目前这项适配正处于 Beta 阶段,我们诚挚地邀请大家前来体验。首批适配的组件包括:ButtonSliderContainerIconLoadingSwitchTagInputCalendarListResultPopoverMaskStepperPopupCheckbox


先来看看 Demo 效果:
image.png


我们的官网文档、Demo 都已同步更新双端切换能力:
2023-11-28 14.44.51.gif


你可以参考以下文档进行接入:



双平台无法对齐的特性


受制于各平台框架设计,以下这些特性存在差异:



  • 两个平台的事件 API 不同。支付宝小程序可以把实例上通过 props 传递给子组件,而微信需要在 data 里传递函数。视图层的写法也所有不同。

    • 下面是 Calendar 在两种平台的使用方式。

      • 微信小程序






Page({
data:{
handleFormat() {
}
}
})

<calendar onFormat="{{handleFormat}}" />

  - 支付宝小程序

Page({
handleFormat() {

}
})
<calendar onFormat="handleFormat" />


  • 微信小程序不支持在 slot 为空时显示默认值, 所以我们在微信平台取消了部分 slot,对应会损失一些定制能力。 比如说 Calendar 组件, 在支付宝可以通过 calendarTitle 这个 slot 定制标题,在微信端只能通过 css 来控制样式。


<View class="ant-calendar-title-container">
{/* #if ALIPAY */}
<Slot name="calendarTitle">
{/* #endif */}
<View class="ant-calendar-title">{currentMonth.title}</View>
{/* #if ALIPAY */}
</Slot>
{/* #endif */}
</View>


  • 微信小程序不支持循环渲染 slot , 所以部分组件无法迁移到微信, 比如说 IndexBar 组件, 使用了 Slot 递归渲染整个组件的内容。这种写法无法迁移到微信。


<view a:for="{{items}}">
<slot
value="{{item}}"
index="{{index}}"
name="labelPreview" />

</view>

双平台适配背后的工程技术


下面我们为大家介绍一下 Antd Mini 支持多平台背后的一些工程方案。


中立的模板语法: 使用 tsx 开发小程序


由于支付宝和微信的小程序的语法各有差异,为了解决让 Antd Mini 同时支持两个端,我们团队选择的 tsx 的一个子集作为小程序的模板语言。
使用 tsx 具有以下优势:



  • 可以直接使用 babel 解析代码,无需自己开发编译器。

  • 各个 IDE 原生支持 TSX 的类型推导与代码提示。

  • 视图层和逻辑层可以复用同一份 props 类型。

  • 可以直接通过 import 导入其他的小程序组件,使用 typescript 进行类型检查。

  • 视图层脚本也可以享受类型校验,无需依赖平台 IDE


由于小程序的视图语法比较受限,从 tsx 向跨平台视图语法转换是比较容易的。我们基于 babel 开发了一个简单的编译器,解析原先 tsx 的语法树以后,将 React 的语法平行转换为可读性比较强的小程序视图语法。
具体举例来看:



  • 条件判断 : 我们使用了 &&以及 ?: 三元表达式替代了之前的 :if 标签。

    • tsx: !!a && <Text>a</Text>

    • 微信小程序:<text wx:if="{{ !!a }}" />

    • 支付宝小程序:<text wx:if="{{ !!a }}" />



  • 循环: 我们使用了 map 代替之前的 :for 标签,从源码里自动分析出 :for-item :for-index :key 等标签。

    • tsx:




{todoList.map((task, taskIndex) => (
<Text
hidden={!mixin.value}
key={task.id}
data-item-id={taskIndex}
data-num={20}
>

{taskIndex} {task}
</Text>

))}


  • 微信小程序:


<block
wx:for="{{ todoList }}"
wx:for-index="taskIndex"
wx:for-item="task"
wx:key="{{ task.id }}">
<!-- display: inline -->
<text
hidden="{{ !mixin.value }}"
data-item-id="{{ taskIndex }}"
data-num="{{ 20 }}"
>
{{ taskIndex }}{{ task }}</text
>
</block>



  • 支付宝小程序


  <block
a:for="{{ todoList }}"
a:for-index="taskIndex"
a:for-item="task"
a:key="{{ task.id }}">
<!-- display: inline -->
<text
hidden="{{ !mixin.value }}"
data-item-id="{{ taskIndex }}"
data-num="{{ 20 }}"
>
{{ taskIndex }}{{ task }}</text
>
</block>



  • 事件绑定: 我们会按照配置,自动将直接转换为两个平台的格式。

    • tsx:<Text onClick="handleClick" />

    • 微信小程序: <text bind:click="handleClick" />

    • 支付宝小程序: <text onClick="handleClick" />



  • 视图层脚本

    • 我们还规定了以 sjs.ts 作为视图层脚本的文件格式。 在编译时候转换为对应平台的文件格式。

      • tsx: import helper from './helper.sjs.ts'

      • 微信小程序: <wxs src="./helper.wxs" module="helper" />

      • 支付宝小程序: <import-sjs src="./helper.sjs" module="helper" />





  • 类型方案

    • 为了让逻辑层类型与视图层关联,我们设计了一些工具类型。 比如说下面使用的 TSXMLProps,将 IProps 的 onClick 转换成了字符串。




// Calendar.axml.tsx
import { TSXMLProps, View } from 'tsxml';

interface IProps {
className?: string;
style?: string;
onClick: (e) => void;
}

interface InternalData {
size: number;
}

export default (
{ className, style }: TSXMLProps<IProps>,
{ size }: InternalData
) => (
<View class={`ant-calendar ${className ? className : ''}`} style={style}>
{size}
</View>

);

// Page.axml.tsx

import Calendar from './Calendar.axml.tsx'

export default () => (<Calendar onClick="handleClick" />)

目前使用 tsx 的这套方案还存在一些限制:



  • 和小程序相同,一个文件内只能定义一个组件。

  • 如果使用自定义组件,需要配置组件事件在各个平台的写法。


老组件语法转换?用 AI 就行了


在决定使用 tsx 语法之后,我们还面临一个很棘手的工作量问题:如何把历史组件库 axml 代码全量转换为最新的 tsx 语法?
这时候就该 ChatGPT 出场了,我们请 AI 来帮助我们完成这个一次性转换工作。
为了让转换结果更靠谱,我们使用了一些技巧:



  • 使用了 tsx 编译器等测试用例作为 prompt ,让 AI 可以更好的了解 tsx 的写法。

  • 除了 tsx 文件以外,我们还将组件的 props.ts 与 config.json 加到了 propmt 里,可以帮助 AI 生成更好的 import 导入。


在这里,你可以看到这份转换的完整 prompt。


确保 AI 产出的正确性?再用我们的编译器转回来


为了确保 AI 产出的代码是正确的,我们使用编译器将 AI 编写的 tsx 重新编译回 axml ,再用 git diff 对原始代码做比对,由此即可核查 AI 转换的正确性。


当然,这两次转换的过程不会完全等价,比如转换 map 的过程中会出现一层嵌套的 <block/>。好在这样的差异不多,一般肉眼看一遍就能确认正确性了。


跨平台通用的组件逻辑:小程序函数式组件(functional-mini)


除了视图,我们还需要确保组件逻辑适配到双端。这里我们使用了小程序函数式组件( functional-mini )的形式来编写,functional-mini 的源码及文档放置均在 ant-design/functional-mini


使用了函数式组件后,Antd Mini 用上了计算属性、useEffect 等特性,也能通过 hooks 来替换原有的大量 mixin 实现,让代码的可维护性提升了一个台阶。


以典型的 Popover 组件为例,逻辑部分适配完成后,它的代码完全变成了 React 风格,数据变更流程一目了然:


const Popover = (props: IPopoverProps) => {
const [value] = useMergedState(props.defaultVisible, {
value: props.visible,
});
const [popoverStyle, setPopoverStyle] = useState({
popoverContentStyle: '',
adjustedPlacement: '',
});

useEffect(() => {
setPopoverStyle({
popoverContentStyle: '',
adjustedPlacement: '',
});
}, [value, props.autoAdjustOverflow, props.placement]);

return {
adjustedPlacement: popoverStyle.adjustedPlacement,
popoverContentStyle: popoverStyle.popoverContentStyle,
};
};

关于小程序函数式组件的原理、特性介绍,我们将在后续的分享中另行展开。


写在最后


欢迎大家一起来尝试 Ant Design Mini 的跨平台能力,你可以在 issue 区提出宝贵的建议与 Bug 反馈。


官网: mini.ant.design/


国内镜像:ant-design-mini.antgroup.com/


作者:支付宝体验科技
来源:juejin.cn/post/7311603519570952246
收起阅读 »

最全面包交友养网站甜蜜定制APP 学生老总竞争激烈

婚恋网站交朋友APP里白领学生礼模信息经常更新 ,为双方约星巴克咖啡厅见面谈了解清楚满意后开始相处包养的网站,女士大多上班或上学见面聊天相处可以看出来 都可长或短期相处私人助理 看缘分和新鲜感,微信993153133 为双方搭建一个相识的平台保护双方权利,张家...
继续阅读 »

婚恋网站交朋友APP里白领学生礼模信息经常更新 ,为双方约星巴克咖啡厅见面谈了解清楚满意后开始相处包养的网站,女士大多上班或上学见面聊天相处可以看出来 都可长或短期相处私人助理 看缘分和新鲜感,微信993153133 为双方搭建一个相识的平台保护双方权利,张家口北京每个城市都可提供双方认识的机会”

收起阅读 »

大厂前端开发规定,你也能写成诗一样的代码(保姆级教程)

web
BEM 使用起来很多人不晓得BEM是什么东西 我来解释给你们听  BEM是一种前端开发的命名方法论,全称为Block-Element-Modifier,中文翻译为块、元素、修饰符。BEM的主要思想是将页面分解成多个可重用、独立的模块,每个模块由一个主块(Blo...
继续阅读 »

BEM 使用起来

很多人不晓得BEM是什么东西 我来解释给你们听

  BEM是一种前端开发的命名方法论,全称为Block-Element-Modifier,中文翻译为块、元素、修饰符。BEM的主要思想是将页面分解成多个可重用、独立的模块,每个模块由一个主块(Block)和零个或多个元素(Element)组成,可以使用修饰符(Modifier)来描述模块的不同状态和变化。

BEM的命名规则如下:

  • 块(Block):一个独立的、可重用的组件,通常由一个或多个元素组成,并且有一个命名空间作为标识符。通常以单个单词命名,使用连字符分割单词,例如:menu、button、header等。
  • 元素(Element):块的组成部分,不能独立存在,必须隶属于某个块。元素的命名使用两个连字符“__”与块名分隔,例如:menu__item、button__text、header__logo等。
  • 修饰符(Modifier):描述块或元素的某种状态或变化,以单个单词或多个单词组成,使用两个连字符“--”与块或元素名分隔。例如:menu--horizontal、button--disabled、header__logo--small等。

  通过使用BEM命名方法论,可以实现更好的代码复用性、可维护性和可扩展性。BEM的命名规则清晰明了,易于理解和使用,可以有效地提高团队开发效率和代码质量。

page+ hd/bd/ft 用起来

"page+ hd/bd/ft" 是一种简化的命名约定,常用于网页布局中。下面是对这些缩写的解释:

  • page:页面的整体容器,表示整个页面的最外层包裹元素。
  • hd:代表页头(header),用于放置页面的标题、导航栏等顶部内容。
  • bd:代表页体(body),用于放置页面的主要内容,如文章、图片、表格等。
  • ft:代表页脚(footer),用于放置页面的底部内容,如版权信息、联系方式等。

  这种命名约定的好处是简洁明了,可以快速理解页面的结构和布局。通过将页面划分为页头、页体和页脚,可以更好地组织和管理页面的各个部分,提高代码的可读性和可维护性。

更好的使用工具(stylus插件)

,Stylus 是一种 CSS 预处理器,它允许你使用更加简洁、优雅的语法编写 CSS。通过在命令行中运行 npm i -g stylus 命令,你可以在全局范围内安装 Stylus,并开始使用它来编写样式文件。 .styl 是 Stylus 文件的扩展名,你可以使用 Stylus 编写样式规则。然后,你可以将这些编写好的 Stylus 文件转换为普通的 CSS 文件,以便在网页中使用。

  具体地说,你可以创建一个名为 common.styl 的文件,并在其中编写 Stylus 样式规则。然后,通过运行 stylus -w common.styl -o common.css 命令,你可以让 Stylus 监听 common.styl 文件的变化,并自动将其编译为 common.css 文件。

  以下是一份示例代码来说明这个过程:

  1. 创建 common.styl 文件,并在其中编写样式规则:
// common.styl
$primary-color = #ff0000

body
font-family Arial, sans-serif
background-color $primary-color

h1
color white
  1. 打开终端,进入 common.styl 文件所在的目录,运行以下命令:
Copy Code
stylus -w common.styl -o common.css

  这将启动 Stylus 监听模式,并将 common.styl 文件编译为 common.css 文件。每当你在 common.styl 文件中进行更改时,Stylus 将自动重新编译 common.css 文件,以反映出最新的样式更改。 请注意,为了运行上述命令,你需要先在全局范围内安装 Stylus,可以使用 npm i -g stylus 命令进行安装。

stylus的优点

  Stylus 作为一种 CSS 预处理器,在实际开发中有以下几个优点:

  1. 更加简洁、优雅的语法:Stylus 的语法比原生 CSS 更加简洁,可以让我们更快地编写样式规则,同时保持代码的可读性和可维护性。
  2. 变量和函数支持:Stylus 支持变量和函数,可以提高样式表的重用性和可维护性。通过使用变量和函数,我们可以在整个样式表中轻松更改颜色、字体等属性,而无需手动修改每个样式规则。
  3. 混合(Mixins)支持:Stylus 的混合功能允许我们将一个样式规则集合包装在一个可重用的块中,并将其应用于多个元素。这可以大大简化样式表的编写,并减少重复代码。
  4. 自动前缀处理:Stylus 可以自动添加适当的浏览器前缀,以确保样式规则在不同的浏览器中得到正确的渲染。
  5. 非常灵活的配置:Stylus 提供了非常灵活的配置选项,可以根据项目的需要启用或禁用不同的功能,例如自动压缩、源映射等。

  总之,Stylus 通过提供更加简洁、灵活的语法和功能,可以使我们更加高效地编写 CSS 样式表,并提高代码的可重用性和可维护性。

最后一个大招阿里的适配神器 flexible.js

  flexible.js 是一款由阿里巴巴的前端团队开发的移动端适配解决方案。它通过对 Viewport 的缩放和 rem 单位的使用,实现了在不同设备上的自适应布局。

具体来说,flexible.js 主要包括以下几个步骤:

  1. 根据屏幕的宽度计算出一个缩放比例,并将该值设置到 Viewport 的 meta 标签中。
  2. 计算出 1rem 对应的像素值,并将其动态设置到 HTML 元素的 font-size 属性中。
  3. 在 CSS 中使用 rem 单位来定义样式规则。这些规则会自动根据 HTML 元素的 font-size 属性进行适配。

  通过这种方式,我们可以实现在不同设备上的自适应布局。具体来说,我们只需要在 CSS 中使用 rem 单位来定义样式规则,而不需要关注具体的像素值。当页面在不同设备上打开时,flexible.js 会自动根据屏幕宽度和像素密度等信息进行适配,从而保证页面的布局和样式在不同设备上都可以得到正确的显示。

  需要注意的是,flexible.js 并不能完全解决移动端适配的所有问题,还有一些特殊情况需要我们手动处理。例如,一些图片或者 Canvas 等元素可能需要根据不同设备的像素密度进行缩放,而这些操作需要我们手动实现。不过,flexible.js 可以帮助我们简化移动端适配的工作,提高开发效率。

  下期我来教大家手动写适配器 喜欢的来个关注 点赞 这个也是以后写文章的动力所在 谢谢大家能观看我的文章 咱下期在见 拜拜


作者:扯蛋438
来源:juejin.cn/post/7303126570323443722

收起阅读 »

JS问题:简单的console.log不要再用了!试试这个

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

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


序言


大家好,我是大澈!


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


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


1. 需求分析


一般情况下,我们在项目中进行代码调试时,往往只会在逻辑中使用console.log进行控制台打印调试。


这种方式虽然比较常规直接,但是如果打印数据多了,就会导致你的控制台消息变得异常混乱。


所以,我们有了更好的选择,那就是console对象提供的其它API,来让我们能够更清晰的区分打印信息。


图片


2. 实现步骤


2.1 console.warn


当我们需要区分一些比较重要的打印信息时,可以使用warn进行警告提示。


图片



2.2 console.error


当我们需要区分一些异常错误的打印信息时,可以使用error进行错误提示。


图片


2.3 console.time/timeEnd


想看看一段代码运行需要多长时间,可以使用time


这对于需要一些时间的CPU密集型应用程序非常有用,例如神经网络或 HTML Canvas读取。


下面执行这段代码:


console.time("Loop timer")
for(let i = 0; i < 10000; i++){
    // Some code here
}
console.timeEnd("Loop timer")


结果如下:图片


2.4 console.trace


想看看函数的调用顺序是怎样的吗?可以使用trace


下面执行这段代码:



  function trace(){
    console.trace()
  }
  function randomFunction(){
      trace();
  }
  randomFunction()


setup中,randomFunction 调用trace,然后又调用console.trace


因此,当您调用 randomFunction 时,您将得到类似的输出,结果如下:


图片


2.5 console.group/groupEnd


当我们需要将一类打印信息进行分组时,可以使用group


下面执行这段代码:


console.group("My message group");

console.log("Test2!");
console.log("Test2!");
console.log("Test2!");

console.groupEnd()

结果如下:


图片



2.6 console.table


在控制台中打印表格信息,可以使用table


对!你没听错,就是让我们以表格形式展示打印信息。


如果使用log打印:


var person1 = {name: "Weirdo", age : "-23", hobby: "singing"}
var person2 = {name: "SomeName", age : "Infinity", hobby: "programming"}

console.log(person1, person2);

结果如下:


这样做是不是让数据看起来很混乱。


图片


反之,如果我们使用table输出:


var person1 = {name: "Weirdo", age : "-23", hobby: "singing"}
var person2 = {name: "SomeName", age : "Infinity", hobby: "programming"}

console.table({person1, person2})

结果如下:


怎么样!从来不知道控制台可以看起来如此干净,对吧!


图片


2.7 console.clear


最后,使用clear把控制台清空吧!


图片


3. 问题详解


3.1 可以自定义log的样式吗?


答案当然是可以的,只需要借助%c这个占位符。


%c 是console的占位符,用于指定输出样式或应用 CSS 样式到特定的输出文本。


但请注意,%c 占位符只在部分浏览器中支持,如 Chrome、Firefox 等。


通过使用 %c 占位符,可以在 console.log 中为特定的文本应用自定义的 CSS 样式。这样可以改变输出文本的颜色、字体、背景等样式属性,以便在控制台中以不同的样式突出显示特定的信息。


以下是使用%c 占位符应用样式的示例:


console.log("%c Hello, World!", 
  "color: red; font-weight: bold;border1px solid red;");

结果如下:


图片


通过使用 %c 占位符和自定义的样式规则,可以在控制台输出中以不同的样式突出显示特定的文本,使得输出更加清晰和易于识别。


这在调试和日志记录过程中非常有用,特别是当需要突出显示特定类型的信息或错误时。


结语


建立这个平台的初衷:



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

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

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

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

2023 闲聊开猿节流 降本增笑

前言 2023年大环境的影响,互联网行业真是难啊,裁员风声四起,言论不一,说互联网增长不如预期,存量运营也不再需要多少人手。各大互联网公司,一直持续着开源节流,降本增效的策略,裁掉一批人,直接降低固定成本。这真是一剂猛药啊,效果也是杠杠的。最后变成了 开猿节...
继续阅读 »

前言


2023年大环境的影响,互联网行业真是难啊,裁员风声四起,言论不一,说互联网增长不如预期,存量运营也不再需要多少人手。各大互联网公司,一直持续着开源节流,降本增效的策略,裁掉一批人,直接降低固定成本。这真是一剂猛药啊,效果也是杠杠的。最后变成了



开猿节流 降本增笑



语雀宕机、阿里云几次宕机,滴滴宕机,最近腾讯也宕机,难道剩下的都是写ppt汇报的吗!

哎,当决策者不懂”岁月静好”是怎么得来的时候,就是”负重前行”的人越来越少的时候。

最后,雪崩的时候,没有一片雪花是无辜的。


经典的笑话



一群年薪百万的在加班讨论给年薪10w不到降本增效



image.png

说点经历


说说我以前的一家公司的降本增效案例


背景:好几年前了,那时候环境没这么差,但是公司的盈利点,增长不如预期,老板很焦虑,带着高管团队去了趟延安,主题:重走长征路,学习先辈创业的艰辛。回来之后,在公司发起,“赚一块钱活动”,就是开动你聪明的脑瓜子,出出金点子,能达到降本增效的目的,所有部门,所有员工都需要参与,那活动就轰轰烈烈的开始了,相对还好,没开猿节流,达到降本增效!


插曲


行政部门,怎么做的呢?为了达到开源节流的目的,他们也是花了心思的。


控制电灯,比如办公区10个电灯泡,他们拿掉5个灯泡,那电费不是可以少支出一半,他们还真是这样执行的,直接整个办公区的电灯泡,拿掉了一半。没过多少天,引起了整个公司对行政部门做法的不满,闹到老板那里去了,老板还是比较务实的,直接把行政的负责人臭骂一顿,让恢复原样。这是降本增效吗,这是牺牲员工的利益达到的,如果这样,还不如在家办公,是不是房租 水电费都省了呢。


技术贴近业务


我在的部门主要是负责供应链系统的开发,比如:订单履约系统、库存系统、商品系统......,做技术的,怎么才能做到降本增效,想想挺难的。我们搞开发的,不就通过代码实现产品,同时保障系统的稳定运行,就OK了。不管怎么样,做技术的,还是得从技术的角度想想,能不能完成公司给的任务?


优化系统


复盘部门负责的所有系统,找出系统的性能瓶颈点,通过技术手段、一定的策略进行优化,比如:以前需要三台机器才能支撑目前的流量,系统优化后,两台就行,同时系统比优化前前性能还好,支撑的流量更大。那减少了一台机器,变相的减少了硬件支出的固定成本。


贴近业务,提高人效


没事找业务的人聊聊天,喝喝咖啡,你会得到意想不到的收获。开发一般获取的需求如下:


image.png


产品对业务提出的需求,也不一定能准确描述,提供比较好的解决方案,需求经过产品理解然后再输出到开发,开发如果不深层次挖掘,只是按照产品的设计,进行开发,跟现实还是又一定的差距的。借用黄晓明的经典名言:



我不要你觉得,我要我觉得



在跟业务聊天中,谈到我们的订单履约系统



  1. 用户一个订单包含多个商品,商品不在同一个仓库,需要多仓发货;商品库存不足,有库存的先发。好多订单,都需要人工进行查看,进行手工拆单,系统自动化吗?减轻点我们的工作压力。有了,自动化拆单

  2. 业务还发现一个用户特点,同一个用户,在时间间隔不到30分钟,连续下两个或多个订单,用户、地址、收货人姓名、电话、信息都一样。有了,合单,节约物流成本


通过以上交谈,我得出了两个需求点:



  • 自菜单拆单 当收到订单信息,查看是否在同一个仓库,如果不在,自动拆分单个仓库进行发货。如果订单商品中,有库存不足的商品,拆分订单,有库存的先发货。注意:用户看到的还是一个订单,只是商品对应的发货单不一样而已

  • 订单自动化合并 根据用户的下单规律,我们在订单下发仓库进行发货的时候,我们先延迟半个小时,看看在这半小时,是否有用户,再次下单,并且满足(买家ID、收货人姓名、电话、地址信息都一样)的订单合并到一个发货单里发货,订单与发货单对应关系N:1


通过上面的策略,自动化拆单,提供了人效,订单自动化合并,降低了物流成本,真正达到了降本增效,不是降本增笑,得到了公司的一致好评,技术人不单单会写代码,也能搞产品


提高个人的技术能力


能力:技术+沟通


沟通能力强,才能准确把握需求


技术能力强,写出高质量的代码,提高系统性能、稳定性。


这个单纯的提高人效,不怎么好衡量,周期比较长



总结:上面几点,是我们部门,通过技术能力,赋能业务,达到降本增效的目的。



应对开源节流 降本增效


提高个人能力


技术人立命之本:技术,先精后广,比如:我是Java开发,那Java这门语言好好的研究,熟练掌握,源码读一读。各种框架的使用、原理,什么场景使用什么技术做为解决方案,起码你掌握了这些,面试能过吧。接下来,有时间、有精力学学其他语言,多门语言,多一种优势吧。再说了,现在貌似又回到了过去,全栈这次词,提的越来越多了。当年诺基亚很火的时候,一大堆搞塞班开发的,后面诺基亚哑火了,你如果还坚守塞班,不学学安卓、ios 是不是基本就GG了?


chatgpt,真心强大,完全可以替代初级工程师,你还有什么理由,不提高自己的技术力呢!


沟通


有的时候,沟通比技术更重要。有人的地方就有江湖,江湖是什么,江湖是人情世故,不是打打杀杀。如果说技术能力是智商,那语言艺术就是情商。会说话,对于程序员来说,真的是硬伤,大部分程序员的世界,都是机器的世界, 0 1 世界 除了 0 1 哪来的2啊?为什么说干技术的干不过写ppt的,因为人家把你的功劳抢了啊,会说话,会汇报啊,别看不起这些人,这也是一种软实力。


没事多去领导面前刷刷脸,混个脸熟,这个比起你做了多少个需求,重要很多。起码提起你的时候,领导知道这个人是谁。


不要认死理,程序员的世界 不应该只有 0 1,应该有更多可能性,2 3 4 5...都可以有。领导就算放个屁,你也要觉得是香的(有点跪舔的意思了,但事实就是这么残酷,虽然我也没做到)领导的面子一定要给,好处不知道有没有,起码没坏处,起码领导觉得你态度端正,执行力强。


跟你工作上接触的人员,多沟通,处理好关系。第一:从别人那里可能得到一些你不知道的有用信息,也有可能收获好基友吧 第二:让周边的人认可你,公司也发展壮大,你的部门大领导可能都没跟你沟通过,如果要了解你,你的信息来源可能是别人对你的评价,有好的有坏的。如果刚好有升职加薪的机会给到你,结果因为别人的几句话,你就被否决了,是不是很亏。所以搞好同事间的关系很重要。


贴近业务


技术都是为业务服务的,再牛逼的技术脱离了业务,只能等死。因为业务不盈利啊,持续亏损,你说老板还留着你过年吗?不要说,我们技术都是按要求按质量根据产品的需求,去做的,系统稳定,线上也没出现过问题,业务不行跟我们技术有什么关系。我在一家公司,业务不好,技术也得分担一部分责任,why?你们开发的东西,是不是没达到业务的目标,这是真实的存在的,产品想的不一定是业务想的,技术理解的也不一定是产品想的。


以前我也一直以为,只要技术好,在哪里不是干。其实真不是这样的,你再好的技术,如果没有一些场景的解决方案,真是纸上谈兵,理论跟落地,差距太大了。比如,阿里云经常提到的的是异地多活,还不几次宕机,造成的损失,真不是金钱能衡量的,理论说的头头是道,但落地的时候,难度远远超过我们的想象,所以要贴近业务,真正做到技术落地,服务好业务


好多大厂出来的人,在细分行业自己创业,其实就是在公司的时候,就很关注业务,技术赋能业务,业务反哺技术。
当你懂技术,懂业务,这样的人,能开源掉吗?


防御性编程


防御性编程,貌似今年技术人应对开猿节流提出的。说是代码不写注释、文档不写,代码能有多烂就多烂,最好写成屎山代码。离开你,换个人根本没法维护,要不重构,别无他法。还有就是不要尽力,能做到100分,我只做到60分就好,剩下的40分是你的保命符,留一点个人上升的空间。带新人,随便带带,教会徒弟饿死师傅不能全部教会他,不然你离走人也不远了。还有很多说法,我就不一一列举了


这种观点,我不支持也不反对,根据自身的实际情况来决定是否使用,过河拆桥的事情也不少。


发展副业


俗话说:猫有九条命,形容猫的生存能力很强,没那么容易死


发展副业,发展副业,发展副业


image.png

发展副业,真的很重要。不要一味只知道工作,拼死累活的给干。不要被公司轻松拿捏你,副业好处如下:



  1. 增加收入:通过副业,你可以获得额外的收入来源,增加财务稳定性,改善生活品质。

  2. 提升技能:副业需要学习和掌握新的技能,这对你的个人和职业发展都是有益的。你可以通过副业开拓新的领域,提高自己的专业能力。

  3. 备用职业选择:副业可以成为你的备用职业选择,当主业遇到困难或变故时,你有一个备选的收入来源和职业发展路径。

  4. 实现梦想和兴趣:副业可以让你追求自己的梦想和兴趣。你可以选择从事自己喜欢的工作,追求个人的创造力和热情。

  5. 社交机会和网络拓展:通过副业,你可以结识更多的人,与更多领域的专业人士交流和合作。这有助于扩大你的人脉和拓展人际关系。


以上观点中,我认为 备用职业选择,这个最重要,起码在公司在开展降本增效,开源节流的时候,你心不慌吧,没有这份工作,我也能活的好好的,起码能保证生活吧


怎么发展副业呢,可能有人看到这,会问,有什么副业途径呢?送外卖算副业吗?只要体力好,能干得了也算。开滴滴算吗?算啊。摆地摊算吗?算啊,除了主业,通过其他赚钱的途径,都是副业。


复盘自己,审视自己,找一个相对适合自己的。我个人也在找,也在尝试。


总结


公司发展到一定阶段,肯定会遇到瓶颈期,如果过不去,开源节流,降本增效,势在必行,公司也要活下去啊,如果公司不在了,全部一起手拉手走,还能怎么办?只是在执行的过程中,人为因素太大了,有能力的可能走了,写ppt、嘴活好的留下了,结果公司的线上服务宕机了,阿里、滴滴宕机事故损失的,比起裁员省的那点钱,简直没可比对性。



雪崩的时候,没有一片雪花是无辜的



作者:柯柏技术笔记
来源:juejin.cn/post/7310787455112495139
收起阅读 »

为啥IoT(物联网)选择了MQTT协议?

物联网设备要实现互相通信,须一套标准通信协议,MQTT(Message Queuing Telemetry Transport)专为物联网设备设计的一套标准消息队列通信协议。使用MQTT协议的IoT设备,可以连接到任何支持MQTT协议的消息队列上,进行通信。 ...
继续阅读 »

物联网设备要实现互相通信,须一套标准通信协议,MQTT(Message Queuing Telemetry Transport)专为物联网设备设计的一套标准消息队列通信协议。使用MQTT协议的IoT设备,可以连接到任何支持MQTT协议的消息队列上,进行通信。



  • 宏观,MQTT和其他MQ传输协议差不多。也是“发布-订阅”消息模型

  • 网络结构,也是C/S架构,IoT设备是客户端,Broker是服务端,客户端与Broker通信进行收发消息


但毕竟使用场景不同,所以,MQTT和普通MQ比,还有很多区别。


1 客户端都运行在IoT设备


1.1 IoT设备特点


① 便宜


最大特点,一个水杯才几十块钱,它上面智能模块成本十块钱最多,再贵就卖不出去。十块钱的智能设备内存都是按KB计算,可能都没有CPU,也不一定有os,整个设备就一个SoC(System on a Chip)。这样的设备就需要通信协议不能复杂,功能不能太多。


② 无线连接


IoT设备一般采用无线连接,很多设备经常移动,导致IoT设备网络连接不稳定,且是常态。


MQTT协议设计上充分考虑这些特点。协议的报文设计极简,惜字如金。协议功能也非常简单,基本就只有:



  • 发布订阅主题

  • 收发消息


这两个最核心功能。为应对网络连接不稳定问题,MQTT增加机制:



  • 心跳机制,可让客户端和服务端双方都能随时掌握当前连接状态,一旦发现连接中断,可尽快重连

  • 会话机制,在服务端来保存会话状态,客户端重连后就可恢复之前会话,继续收发消息。这样,把复杂度转移到服务端,客户端实现更简单


2 服务端高要求


MQTT面临的使用场景中,服务端需支撑海量IoT设备同时在线。


普通的消息队列集群,服务的客户端都运行在性能强大的服务器,所以客户端数量不会特别多。如京东的JMQ集群,日常在线客户端数量大概十万左右,就足够支撑全国人民在京东买买买。


而MQTT使用场景中,需支撑的客户端数量,远不止几万几十万。如北京交通委若要把全市车辆都接入进来,就是个几百万客户端的规模。路侧的摄像头,每家每户的电视、冰箱,每个人随身携带的各种穿戴设备,这些设备规模都是百万、千万级甚至上亿级。


3 不支持点对点通信


MQTT协议的设计目标是支持发布-订阅(Publish-Subscribe)模型,而不是点对点通信。


MQTT的主要特点之一是支持发布者(Publisher)将消息发布到一个主题(Topic),而订阅者(Subscriber)则可以通过订阅相关主题来接收这些消息。这种模型在大规模的分布式系统中具有很好的可扩展性和灵活性。因此,MQTT更适合用于多对多、多对一的通信场景,例如物联网(IoT)应用、消息中间件等。


虽然MQTT的设计目标不是点对点通信,但在实际使用中,你仍然可以通过一些设计来模拟点对点通信。例如,使用不同的主题来模拟点对点通信,或者在应用层进行一些额外的协议和逻辑以实现点对点通信的效果。


一般做法都是,每个客户端都创建一个以自己ID为名字的主题,然后客户端来订阅自己的专属主题,用于接收专门发给这个客户端的消息。即MQTT集群中,主题数量和客户端数量基本是同一量级。


4 MQTT产品选型


如何支持海量在线IoT设备和海量主题,是每个支持MQTT协议的MQ面临最大挑战。也是做MQTT服务端技术选型时,需重点考察技术点。


开源MQTT产品


有些是传统MQ,通过官方或非官方扩展,实现MQTT协议支持。也有一些专门的MQTT Server产品,这些MQTT Server在协议支持层面,大多没问题,性能和稳定性方面也都满足要求。但还没发现能很好支撑海量客户端和主题的开源产品。why?


传统MQ


虽可通过扩展来支持MQTT协议,但整体架构设计之初,并未考虑支撑海量客户端和主题。如RocketMQ元数据保存在NameServer的内存,Kafka是保存在zk,这些存储都不擅长保存大量数据,所以也支撑不了过多客户端和主题。


另外一些开源MQTT Server


很多就没集群功能或集群功能不完善。集群功能做的好的产品,大多都把集群功能放到企业版卖。


所以做MQTT Server技术选型,若你接入IoT设备数量在10w内,可选择开源产品,选型原则和选择普通消息队列一样,优先选择一个流行、熟悉的开源产品即可。


若客户端规模超过10w量级,需支撑这么大规模客户端数量,服务端只有单节点肯定不够,须用集群,并且这集群要支持水平扩容。这时就几乎没开源产品了,此时只能建议选择一些云平台厂商提供的MQTT云服务,价格相对较低,也可选择价格更高商业版MQTT Server。


另外一个选择就是,基于已有开源MQTT Server,通过一些集成和开发,自行构建MQTT集群。


5 构建一个支持海量客户端的MQTT集群


MQTT集群如何支持海量在线的IoT设备?
一般来说,一个MQTT集群它的架构应该是这样的:



从左向右看,首先接入的地址最好是一个域名,这样域名后面可配置多个IP地址做负载均衡,当然这域名不是必需。也可直接连负载均衡器。负载均衡可选F5这种专用的负载均衡硬件,也可Nginx这样软件,只要是四层或支持MQTT协议的七层负载均衡设备,都可。


负载均衡器后面要部署一个Proxy集群


Proxy集群作用



  • 承接海量IoT设备连接

  • 维护与客户端的会话

  • 作为代理,在客户端和Broker之间进行消息转发


在Proxy集群后是Broker集群,负责保存和收发消息。


有的MQTT Server集群架构:



架构中没Proxy。实际上,只是把Proxy和Broker功能集成到一个进程,这两种架构本质没有太大区别。可认为就是同一种架构来分析。


前置Proxy,易解决海量连接问题,由于Proxy可水平扩展,只要用足够多的Proxy节点,就可抗海量客户端同时连接。每个Proxy和每个Broker只用一个连接通信即可,这对每个Broker来说,其连接数量最多不会超过Proxy节点的数量。


Proxy对于会话的处理,可借鉴Tomcat处理会话的两种方式:



  • 将会话保存在Proxy本地,每个Proxy节点都只维护连接到自己的这些客户端的会话。但这要配合负载均衡来使用,负载均衡设备需支持sticky session,保证将相同会话的连接总是转发到同一Proxy节点

  • 将会话保存在一个外置存储集群,如Redis集群或MySQL集群。这样Proxy就可设计成完全无状态,对负载均衡设备也没特殊要求。但这要求外置存储集群具备存储千万级数据能力,同时具有很好性能


如何支持海量主题?


较可行的解决方案,在Proxy集群的后端,部署多组Broker小集群,如可以是多组Kafka小集群,每个小集群只负责存储一部分主题。这样对每个Broker小集群,主题数量就可控制在可接受范围内。由于消息是通过Proxy进行转发,可在Proxy中采用一些像一致性哈希等分片算法,根据主题名称找到对应Broker小集群。这就解决支持海量主题的问题。


UML


Proxy的UML图:


@startuml
package "MQTT Proxy Cluster" {
class MQTTProxy {
+handleIncomingMessage()
+handleOutgoingMessage()
+produceMessage()
+consumeMessage()
}

class Client {
+sendMessage()
+receiveMessage()
}

class Broker {
+publish()
+subscribe()
}

Client --> MQTTProxy
MQTTProxy --> Broker
}
@enduml

@startuml
actor Client
entity MQTTProxy
entity Broker

Client -> MQTTProxy : sendMessage()
activate MQTTProxy
MQTTProxy -> Broker : produceMessage()
deactivate MQTTProxy
@enduml

@startuml
entity MQTTProxy
entity Broker
actor Client

Broker -> MQTTProxy : publishMessage()
activate MQTTProxy
MQTTProxy -> Client : consumeMessage()
deactivate MQTTProxy
@enduml


Proxy收发消息的时序图:



Proxy生产消息流程的时序图:



Proxy消费消息流程的时序图:


image-20231208134111361

6 总结


MQTT是专门为物联网设备设计的一套标准的通信协议。这套协议在消息模型和功能上与普通的消息队列协议是差不多的,最大的区别在于应用场景不同。在物联网应用场景中,IoT设备性能差,网络连接不稳定。服务端面临的挑战主要是,需要支撑海量的客户端和主题。


已有的开源的MQTT产品,对于协议的支持都不错,在客户端数量小于十万级别的情况下,可以选择。对于海量客户端的场景,服务端必须使用集群来支撑,可以选择收费的云服务和企业版产品。也可以选择自行来构建MQTT集群。


自行构建集群,最关键技术点,就是通过前置Proxy集群解决海量连接、会话管理和海量主题:



  • 前置Proxy负责在Broker和客户端之间转发消息,通过这种方式,将海量客户端连接收敛为少量的Proxy与Broker之间的连接,解决了海量客户端连接数的问题

  • 维护会话的实现原理,和Tomcat维护HTTP会话一样

  • 海量主题,可在后端部署多组Broker小集群,每个小集群分担一部分主题这样的方式来解决


参考:



作者:JavaEdge在掘金
来源:juejin.cn/post/7310786611805929499
收起阅读 »

uniapp日常总结--uniapp页面传值

uniapp日常总结--uniapp页面传值在Uniapp中,不同页面之间传值可以通过以下几种方式实现:URL参数传递:可以通过在跳转链接中添加参数,然后在目标页面通过this.$route.params或this.$route.query来获取传递的参数。 ...
继续阅读 »

uniapp日常总结--uniapp页面传值

在Uniapp中,不同页面之间传值可以通过以下几种方式实现:

  1. URL参数传递:

    可以通过在跳转链接中添加参数,然后在目标页面通过this.$route.paramsthis.$route.query来获取传递的参数。


    <uni-link :url="'/pages/targetPage/targetPage?param1=' + value1 + '¶m2=' + value2">跳转到目标页面uni-link>
    // 在目标页面获取参数
    export default {
    mounted() {
    const param1 = this.$route.params.param1;
    const param2 = this.$route.params.param2;
    console.log(param1, param2);
    }
    }
  2. 使用页面参数(Query):

    1. 在触发页面跳转的地方,例如在一个按钮的点击事件中:
    // 在当前页面的某个事件触发时跳转到目标页面,并传递参数
    uni.navigateTo({
    url: '/pages/targetPage/targetPage',
    // 传递的参数,可以是一个对象
    success(res) {
    console.log(res);
    },
    fail(err) {
    console.error(err);
    },
    // 参数传递方式,query 表示通过 URL 参数传递
    // params 表示通过 path 参数传递
    // 一般情况下使用 query 就可以了
    // 使用 params 时,目标页面的路径需要定义成带参数的形式
    // 如 '/pages/targetPage/targetPage/:param1/:param2'
    method: 'query',
    // 要传递的参数
    query: {
    key1: 'value1',
    key2: 'value2'
    }
    });



    //简写 在当前页面的某个事件触发时跳转到目标页面,并传递参数
    uni.navigateTo({
    url: '/pages/targetPage/targetPage?key1=value1&key2=value2',
    });
    1. 在目标页面中,可以通过this.$route.query来获取传递的参数:
export default {
onLoad(query) {
// 获取传递的参数
const key1 = this.$route.query.key1;
const key2 = this.$route.query.key2;

console.log(key1, key2);
},
// 其他页面生命周期或方法等
};

在目标页面的onLoad生命周期中,this.$route.query可以获取到传递的参数。key1key2就是在跳转时传递的参数。如果使用uni.switchTab方法进行页面跳转,是无法直接传递参数的。因为uni.switchTab用于跳转到 tabBar 页面,而 tabBar 页面是在底部显示的固定页面,不支持传递参数。如果需要在 tabBar 页面之间传递参数,可以考虑使用全局变量、本地存储等方式进行参数传递。

  • Vuex状态管理:

    使用Vuex进行全局状态管理,可以在一个页面中修改状态,而在另一个页面中获取最新的状态。

    适用于需要在多个页面之间共享数据的情况。

    如果你的应用使用了Vuex,可以在一个页面的computed属性或methods中触发commit,然后在另一个页面通过this.$store.state获取值。

    在第一个页面:

    // 在页面中触发commit
    this.$store.commit('setValue', value);

    在第二个页面:

    // 在另一个页面获取值
    const value = this.$store.state.value;
    console.log(value);
  • 使用本地存储(Storage):

    使用本地存储(localStorage或uni提供的存储API)将数据存储到本地,然后在另一个页面中读取。适用于需要持久保存数据的情况。如果数据不大,你也可以将数据存储在本地存储中,然后在目标页面读取。

    其中根据使用情景可以使用同步StorageSync或者异步Storage来实现。

    两者存在一定的区别,简单介绍可以查看下方链接:

    uniapp日常总结--setStorageSync和setStorage区别

    同步:使用uni.setStorageSyncuni.getStorageSync等方法,将数据存储在本地,然后在另一个页面读取。

    // 在页面A中保存数据到本地存储
    uni.setStorageSync('key', value);
    // 在页面B中从本地存储中读取数据
    const value = uni.getStorageSync('key');
    console.log(value);

    异步:使用uni.setStorageuni.getStorage等方法,将数据存储在本地,然后在另一个页面读取。

    // 在页面A中保存数据到本地存储
    uni.setStorage({
    key: 'yourDataKey',
    data: yourData,
    });
    // 在页面B中从本地存储中读取数据
    uni.getStorage({
    key: 'yourDataKey',
    success: function (res) {
    const pageData = res.data;
    },
    });
  • 事件总线:

    使用uni提供的API进行页面传值,如uni.$emituni.$on

    通过事件触发和监听的方式在页面之间传递数据。

    使用Uniapp的事件总线来进行组件之间的通信。在发送组件中,使用uni.$emit触发一个自定义事件,并在接收组件中使用uni.$on监听这个事件。

    在发送组件:

    uni.$emit('customEvent', data);

    在接收组件:

    uni.$on('customEvent', (data) => {
    console.log(data);
    });
  • 应用全局对象:

    通过uni.$app访问应用全局对象,从而在不同页面之间共享数据。

    在发送页面:

    uni.$app.globalData.value = data;

    在接收页面:

    const value = uni.$app.globalData.value;
    console.log(value);
  • URL参数传递对于简单的场景比较方便。Vuex适用于较大的应用状态管理。本地存储适用于需要在页面刷新后仍然保持的简单数据。事件总线方法适用于简单的组件通信。页面参数相对常用于跳转。根据具体需求和应用场景,选择合适的方式进行数据传递。不同的场景可能需要不同的方法。


    作者:狐说狐有理
    来源:juejin.cn/post/7310786618390855717

    收起阅读 »

    TS中,到底用`type`还是`interface`呢?

    web
    结论 直接说结论,用type一把梭即可,除非你要发布到npm,接下来我一一解释 为什么定义对象都要使用type呢? 如图所示,我鼠标悬浮后,并不知道里面是什么东西 只能获取结果时调出代码提示,或者ctrl + 鼠标左键进入,查看类型定义 那么我用type呢? ...
    继续阅读 »

    结论


    直接说结论,用type一把梭即可,除非你要发布到npm,接下来我一一解释


    为什么定义对象都要使用type呢?


    如图所示,我鼠标悬浮后,并不知道里面是什么东西


    只能获取结果时调出代码提示,或者ctrl + 鼠标左键进入,查看类型定义


    那么我用type呢?


    image.png


    可以看到,现在鼠标悬浮能直接查看类型定义了


    这一点是让我最受不了的,所以直接选择type即可


    image.png


    区别


    1. 如何继承



    先看看interface,通过extends关键字



    image.png



    type,则通过交叉类型。不过我认为interface好看点



    image.png


    2. 其他特性



    interface重写时



    • 如果有不同的属性,则会添加;

    • 如果是相同的属性但是类型不同,则会报错;



    这点有好有坏,当你不小心名字重复了,那你就容易出问题


    但同时利于扩展,不过没有人会这么写吧?


    直接去原来的接口添加属性不行吗?


    唯一的场景,就是开发工具库后。别人使用你的工具时,可以为你扩展类型


    image.png


    3. type独有的优势


    除了上面的悬浮能查看具体类型外,type还提供了很多的关键字使用,这是interface不具备的


    比如in关键字,用来枚举类型


    这里我写个删除属性的泛型,和Omit一样的,但是interface不支持


    此外还有很多TS特有的关键字,都只能通过type使用,比如infer


    不过这也符合直觉,因为interface就是定义一个类型而已


    image.png


    经过以上探讨,可以得出一个结论


    平时开发可以都用type


    发布工具库给别人用时,用interface


    作者:寅时码
    来源:juejin.cn/post/7304867327752912906
    收起阅读 »

    个人代码优化技巧

    web
    背景 贴近目前公司的业务,做的增删改查比较多。基本上都是做一些表格的业务系统比较多,因此在写的过程中,都会遇到一些优化的细点,仅供参考,觉得好的可以采纳,不好的欢迎提出来。 一、import按需加载 很多小伙伴肯定不少看到过,性能优化路由要用import(...
    继续阅读 »

    背景



    贴近目前公司的业务,做的增删改查比较多。基本上都是做一些表格的业务系统比较多,因此在写的过程中,都会遇到一些优化的细点,仅供参考,觉得好的可以采纳,不好的欢迎提出来。



    一、import按需加载


    很多小伙伴肯定不少看到过,性能优化路由要用import('@/views/xxxx.vue')这样就可以按需加载了。
    本身的vue-cli自动创建出来的时候也会有这一条语句。除了给路由优化之外呢,还有别的场景优化空间呢?那肯定有的啦。那就是结合<component/>自带的组件去一起实现。


    场景呈现


    正常情况下,做一个业务模块,都会分为【基础表】、【业务表】,一般情况下,用户维护好了基础表信息了之后,剩下的就是信息交叉复用,有可能在某个业务页面,我需要点击某个按钮后根据某个值到某个基础表的页面进行搜索信息,并勾选行信息。


    <template>
    <div>
    <div class="count" @click="showDynamicComponent">按需加载页面</div>
    <Modal title="动态数据" :visible="visible" @ok="()=>dynamicComponent=null">
    <component :is="dynamicComponent" ref="dynamicRef"/>
    </Modal>
    </div>

    </template>

    <script>
    import { Modal } from 'ant-design-vue'
    export default {
    components: {
    Modal
    },
    data() {
    return {
    dynamicComponent: null,
    visible: false
    };
    },
    methods: {
    showDynamicComponent() {
    this.visible = true
    import('@/views/baseInfo/a.vue').then(res=>{
    this.dynamicComponent = res.module
    })
    },
    },
    };
    </script>


    最后通过this.$refs.dynamicRef这个方式来拿到组件的信息和方法。




    二、表格维护


    因为公司的做的系统报表比较多,这时候表头的数量和表单都是比较多的,恰好公司使用的UI框架是ant-design-vue,表头的数量达到40-50的时候,那么代码的占用函数就很大,而且在产品经常在开发阶段,定义的表头位置顺序变来变去,于是为了方便维护和开发,我封装成一个函数,我还没考虑过这个性能损耗问题,但是维护起来确实方便很多。


    业务场景


    举个例子,一个表头有用户姓名年龄,正常情况下,ant-design-vue表头是这么写的。


    const columns = [{
    dataIndex: 'username',
    title: '用户'
    }, {
    dataIndex: 'realname',
    title: '姓名'
    }, {
    dataIndex: 'age',
    title: '年龄'
    }]

    数据少的时候,维护没有什么问题,倒是表头数量很多的时候,可能40-50个,一百个?大概是这个数,看起来就很费劲。因为自己业务确实遇到过这个问题,维护起来要么单独创建一个文件大概一百多行一点点找,要么就放在业务代码里,但是无论如何阅读性都很差。所以我想了个办法,把它平铺变成数组形式。


    import { genTableColumnsUtil } from '@/utils/tableJs'
    const columns = genTableColumnsUtil([
    ['username', '用户'],
    ['realname', '姓名'],
    ['age', '年龄'],
    ])

    这时候是不是就好看多了?甚至这个可以做成二级表头,递归做嵌套。那额外的配置项拓展项怎么搞?


    const columns = genTableColumnsUtil([
    ['username', '用户'],
    ['realname', '姓名'],
    ['age', '年龄'],
    ],
    {username: { width: '20%' }})

    我的做法就是在函数里面在穿多一个对象,这样就可以填充上去了。毕竟大多数字段只是展示而已,没有做太多的单元格定制化,如果要定制化,搜索对应的dataIndex就好了。


    image.png


    image.png


    这时候调整顺序的时候,还有定制化的时候就阅读性就好很多。




    三、依赖包单独抽离


    性能优化不只是代码层面的优化,除了nginx配置http2,gzip...
    单独抽离chunk包也可以达到加快访问速度的目的。


    业务场景


    // 在vue.config.js加入这段代码
    module.exports = {
    configureWebpack: config => {
    // 分包,打包时将node_modules中的代码单独打包成一个chunk
    config.optimization.splitChunks = {
    maxInitialRequests: Infinity, // 一个入口最大的并行请求数,默认为3
    minSize: 0, // 一个入口最小的请求数,默认为0
    chunks: 'all', // async只针对异步chunk生效,all针对所有chunk生效,initial只针对初始chunk生效
    cacheGr0ups: { // 这里开始设置缓存的 chunks
    packVendor: { // key 为entry中定义的 入口名称
    test: /[\\/]node_modules[\\/]/, // 正则规则验证,如果符合就提取 chunk
    name(module) {
    const packageName = module.context.match(
    /[\\/]node_modules[\\/](.*?)([\\/]|$)/
    )[1]
    return `${packageName.replace('@', '')}`
    }
    }
    }
    }
    }
    }
    }

    最后在打包完了之后。可以查看一下。


    image.png




    四、thread-loader打包


    业务场景


    充分利用cpu核心数,进行快速打包、其实我也没感觉有多快。


     // 开启多线程打包
    config.module
    .rule('js')
    .test(/\.js$/)
    .use('thread-loader')
    .loader('thread-loader')
    .options({
    // worker使用cpu核心数减1
    workers: require('os').cpus().length - 1,
    // 设置cacheDirectory
    cacheDirectory: '.cache/thread-loader'
    })
    .end()



    五、ECharts按需使用


    业务场景


    数字化是趋势,图形可视化在所难免,但往往我们有时候没做那么复杂的图形,可能只用到了饼图和柱状图,或者别的,怎么样都用不完ECharts更多的图形,ECharts是大家常用的图形化之一,ECharts第一步教程都是告诉我们,在
    vue文件里


    import * as echarts from 'echarts'

    殊不知,我们用不到的图形都加载进来,打包的时候就可以看到,这玩意,3M多。
    所以,看情况来加载图形配置


    import * as echarts from 'echarts/core'

    import { BarChart, LineChart, PieChart } from 'echarts/charts'

    import {
    TitleComponent,
    TooltipComponent,
    GridComponent,
    LegendComponent,
    ToolboxComponent,
    } from 'echarts/components'

    import { CanvasRenderer } from 'echarts/renderers'

    echarts.use([
    TitleComponent,
    TooltipComponent,
    GridComponent,
    BarChart,
    LineChart,
    PieChart,
    CanvasRenderer,
    LegendComponent,
    ToolboxComponent
    ])

    export default echarts

    通过vscode的包插件,可以看到引入的模块大小


    image.png


    作者:hhope
    来源:juejin.cn/post/7309791510873784372
    收起阅读 »

    学会Grid之后,我觉得再也没有我搞不定的布局了

    web
    说到布局很多人的感觉应该都是恐惧,为此很多人都背过一些很经典的布局方案,例如:圣杯布局、双飞翼布局等非常耳熟的名词; 为了实现这些布局我们有很多种实现方案,例如:table布局、float布局、定位布局等,当然现在比较流行的肯定是flex布局; flex布局属...
    继续阅读 »

    说到布局很多人的感觉应该都是恐惧,为此很多人都背过一些很经典的布局方案,例如:圣杯布局双飞翼布局等非常耳熟的名词;


    为了实现这些布局我们有很多种实现方案,例如:table布局float布局定位布局等,当然现在比较流行的肯定是flex布局


    flex布局属于弹性布局,所谓弹性也可以理解为响应式布局,而同为响应式布局的还有Grid布局


    Grid布局是一种二维布局,可以理解为flex布局的升级版,它的出现让我们在布局方面有了更多的选择,废话不多说,下面开始全程高能;



    本篇不会过多介绍grid的基础内容,更多的是一些布局的实现方案和一些小技巧;



    常见布局


    所谓的常见布局只是我们在日常开发中经常会遇到的布局,例如:圣杯布局双飞翼布局这种名词我个人觉得不用太过于去在意;


    因为这类布局最后的解释都会变成几行几列,内容在哪一行哪一列,而这些就非常直观的对标了grid的特性;


    接下来我们来一起看看一些非常常见的布局,并且用grid来实现;


    1. 顶部 + 内容


    image.png


    html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>Titletitle>
    <style>
    html, body {
    margin: 0;
    padding: 0;
    }

    body {
    display: grid;
    grid-template-rows: 60px 1fr;
    height: 100vh;
    }

    .header {
    background-color: #039BE5;
    }

    .content {
    background-color: #4FC3F7;
    }

    .header,
    .content {
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 24px;
    }
    style>
    head>
    <body>
    <div class="header">Headerdiv>
    <div class="content">Contentdiv>
    body>
    html>


    2. 顶部 + 内容 + 底部


    image.png


    html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>Titletitle>
    <style>
    html, body {
    margin: 0;
    padding: 0;
    }

    body {
    display: grid;
    grid-template-rows: 60px 1fr 60px;
    height: 100vh;
    }

    .header {
    background-color: #039BE5;
    }

    .content {
    background-color: #4FC3F7;
    }

    .footer {
    background-color: #039BE5;
    }

    .header,
    .content,
    .footer {
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 24px;
    }
    style>
    head>
    <body>
    <div class="header">Headerdiv>
    <div class="content">Contentdiv>
    <div class="footer">Footerdiv>
    body>
    html>


    这里示例和上面的示例唯一的区别就是多了一个footer,但是我们可以看到代码并没有多少变化,这就是grid的强大之处;


    可以看码上掘金的效果,这里的内容区域是单独滚动的,从而实现了headerfooter固定,内容区域滚动的效果;


    实现这个效果也非常简单,只需要在content上加上overflow: auto即可;




    3. 左侧 + 内容


    image.png


    html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>Titletitle>
    <style>
    html, body {
    margin: 0;
    padding: 0;
    }

    body {
    display: grid;
    grid-template-columns: 240px 1fr;
    height: 100vh;
    }

    .left {
    background-color: #039BE5;
    }

    .content {
    background-color: #4FC3F7;
    }

    .left,
    .content {
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 24px;
    }


    style>
    head>
    <body>
    <div class="left">Leftdiv>
    <div class="content">Contentdiv>
    body>
    html>


    这个示例效果其实和第一个是类似的,只不过是把grid-template-rows换成了grid-template-columns,这里就不提供码上掘金的示例了;



    4. 顶部 + 左侧 + 内容


    image.png


    html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>Titletitle>
    <style>
    html, body {
    margin: 0;
    padding: 0;
    }

    body {
    display: grid;
    grid-template-rows: 60px 1fr;
    grid-template-columns: 240px 1fr;
    height: 100vh;
    }

    .header {
    grid-column: 1 / 3;
    background-color: #039BE5;
    }

    .left {
    background-color: #4FC3F7;
    }

    .content {
    background-color: #99CCFF;
    }

    .header,
    .left,
    .content {
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 24px;
    }

    style>
    head>
    <body>
    <div class="header">Headerdiv>
    <div class="left">Leftdiv>
    <div class="content">Contentdiv>
    body>
    html>


    这个示例不同点在于header占据了两列,这里我们可以使用grid-column来实现,grid-column的值是start / end,例如:1 / 3表示从第一列到第三列;


    如果确定这一列是占满整行的,那么我们可以使用1 / -1来表示,这样如果后续变成顶部 + 左侧 + 内容 + 右侧的布局,那么header就不需要修改了;



    5. 顶部 + 左侧 + 内容 + 底部


    image.png


    html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>Titletitle>
    <style>
    html, body {
    margin: 0;
    padding: 0;
    }

    body {
    display: grid;
    grid-template-areas:
    "header header"
    "left content"
    "left footer";
    grid-template-rows: 60px 1fr 60px;
    grid-template-columns: 240px 1fr;
    height: 100vh;
    }

    .header {
    grid-area: header;
    background-color: #039BE5;
    }

    .left {
    grid-area: left;
    background-color: #4FC3F7;
    }

    .content {
    grid-area: content;
    background-color: #99CCFF;
    }

    .footer {
    grid-area: footer;
    background-color: #6699CC;
    }

    .header,
    .left,
    .content,
    .footer {
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 24px;
    }
    style>
    head>
    <body>
    <div class="header">Headerdiv>
    <div class="left">Leftdiv>
    <div class="content">Contentdiv>
    <div class="footer">Footerdiv>
    body>
    html>


    这个示例的小技巧是使用了grid-template-areas,使用这个属性可以让我们通过代码来直观的看到布局的样式;


    这里的值是一个字符串,每一行代表一行,每个字符代表一列,例如:"header header"表示第一行的两列都是header,这里的header是我们自己定义的,可以是任意值;


    定义好了之后就可以在对应的元素上使用grid-area来指定对应的区域,这里的值就是我们在grid-template-areas中定义的值;





    码上掘金中的效果可以看到,左侧的菜单和内容区域都是单独滚动的,这里的实现方式和第二个示例是一样的,只需要需要滚动的元素上加上overflow: auto即可;



    响应式布局


    响应式布局指的是页面的布局会随着屏幕的大小而变化,这里的变化可以是内容区域大小可以自动调整,也可以是页面布局随着屏幕大小进行自动调整;


    这里我就用掘金的页面来举例,这里只提供一个思路,所以不会像上面那样提供那么多示例;


    1. 基础布局实现


    移动端布局


    image.png



    以移动端的效果开始,掘金的移动端的布局就是上面的效果,这里我简单的将页面分为了三个部分,分别是headernavigationcontent


    注:这里不是要100%还原掘金的页面,只是为了演示grid布局,具体页面结构和最后实现的效果会有非常大的差异,这里只会实现一些基础的布局;



    html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>Titletitle>
    <style>
    html, body {
    margin: 0;
    padding: 0;
    }

    body {
    display: grid;
    grid-template-areas:
    "header"
    "navigation"
    "content";
    grid-template-rows: 60px 48px 1fr;
    height: 100vh;
    }

    .header {
    grid-area: header;
    background-color: #039BE5;
    }

    .navigation {
    grid-area: navigation;
    background-color: #4FC3F7;
    }

    .content {
    grid-area: content;
    background-color: #99CCFF;
    }


    .header,
    .navigation,
    .content {
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 24px;
    }

    style>
    head>
    <body>
    <div class="header">Headerdiv>
    <div class="navigation">Navigationdiv>
    <div class="content">Contentdiv>
    body>
    html>

    iPad布局


    image.png



    这里是需要借助媒体查询来实现的,在媒体查询中只需要调整一下grid-template-rowsgrid-template-columns的值即可;


    由于这里的效果是上面一个的延伸,为了阅读体验会移除上面相关的css代码,只保留需要修改的代码;



    html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>Titletitle>
    <style>

    .right {
    display: none;
    background-color: #6699CC;
    }

    @media (min-width: 1000px) {
    body {
    grid-template-areas:
    "header header"
    "navigation navigation"
    "content right";
    grid-template-columns: 1fr 260px;
    }

    .right {
    grid-area: right;

    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 24px;
    }
    }
    style>
    head>
    <body>
    <div class="header">Headerdiv>
    <div class="navigation">Navigationdiv>
    <div class="content">Contentdiv>
    <div class="right">Rightdiv>
    body>
    html>

    PC端布局


    image.png



    和上面处理方式相同,由于Navigation移动到了左侧,所以还要额外的修改一下grid-template-areas的值;


    这里就可以体现grid的强大之处了,我们可以简单的修改grid-template-areas就可以实现一个完全不同的布局,而且代码量非常少;


    为了居中显示内容,我们需要在左右两侧加上一些空白区域,可以简单的使用.来实现,这里的.表示一个空白区域;


    由于内容的宽度基本上是固定的,所以留白区域简单的使用1fr进行占位即可,这样就可以平均的分配剩余的空间;



    html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>Titletitle>
    <style>
    @media (min-width: 1220px) {
    body {
    grid-template-areas:
    "header header header header header"
    ". navigation content right .";
    grid-template-columns: 1fr 180px minmax(0, 720px) 260px 1fr;
    grid-template-rows: 60px 1fr;
    }
    }
    style>
    head>
    <body>
    <div class="header">Headerdiv>
    <div class="navigation">Navigationdiv>
    <div class="content">Contentdiv>
    <div class="right">Rightdiv>
    body>
    html>

    完善一些细节


    QQ录屏20231210000552.gif



    最终的布局大概就是上图这样,这里主要处理的各个版块的间距和响应式内容区域的大小,这里的处理方式主要是使用column-gap和一个空的区域进行占位来实现的;


    这里的column-gap表示列与列之间的间距,值可以是pxemrem等基本的长度属性值,也可以使用计算函数,但是不能使用弹性值fr


    空区域进行占位留间距其实我并不推荐,这里只是演示grid布局可以实现的一些功能,具体的实现方式还是要根据实际情况来定,这里我更推荐使用margin来实现;


    完整代码如下:



    html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>Titletitle>
    <style>
    html, body {
    margin: 0;
    padding: 0;
    }

    body {
    display: grid;
    grid-template-areas:
    "header header header"
    "navigation navigation navigation"
    ". . ."
    ". content .";
    grid-template-columns: 1fr minmax(0, 720px) 1fr;
    grid-template-rows: 60px 48px 10px 1fr;
    column-gap: 10px;
    height: 100vh;
    }

    .header {
    grid-area: header;
    background-color: #039BE5;
    }

    .navigation {
    grid-area: navigation;
    background-color: #4FC3F7;
    }

    .content {
    grid-area: content;
    background-color: #99CCFF;
    }

    .right {
    display: none;
    background-color: #6699CC;
    }

    @media (min-width: 1000px) {
    body {
    grid-template-areas:
    "header header header header"
    "navigation navigation navigation navigation"
    ". . . ."
    ". content right .";
    grid-template-columns: 1fr minmax(0, 720px) 260px 1fr;
    }

    .right {
    grid-area: right;

    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 24px;
    }
    }

    @media (min-width: 1220px) {
    body {
    grid-template-areas:
    "header header header header header"
    ". . . . ."
    ". navigation content right .";
    grid-template-columns: 1fr 180px minmax(0, 720px) 260px 1fr;
    grid-template-rows: 60px 10px 1fr;
    }
    }

    .header,
    .navigation,
    .content {
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 24px;
    }

    style>
    head>
    <body>
    <div class="header">Headerdiv>
    <div class="navigation">Navigationdiv>
    <div class="content">Contentdiv>
    <div class="right">Rightdiv>
    body>
    html>

    简单复刻版




    码上掘金上的效果来说已经完成了大部分的布局和一些效果,目前来说就是还差一些交互,还有一些细节上的处理,感兴趣的同学可以自行完善;



    异型布局


    异性布局指的是页面中的元素不是按照常规的流式布局进行排版,又或者说不规则的布局,这里我简单的列出几个布局,来看看grid是如何实现的;


    1. 照片墙


    image.png


    html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>Titletitle>
    <style>
    html, body {
    margin: 0;
    padding: 0;
    background: #f2f3f5;
    overflow: auto;
    }

    body {
    display: grid;
    grid-template-columns: repeat(12, 100px);
    grid-auto-rows: 100px;
    place-content: center;
    gap: 6px;
    height: 100vh;
    }

    .photo-item {
    width: 200px;
    height: 200px;
    clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%);
    }

    style>
    head>
    <body>

    body>
    <script>
    function randomColor() {
    return '#' + Math.random().toString(16).substr(-6);
    }

    let row = 1;
    let col = 1;
    for (let i = 0; i < 28; i++) {
    const div = document.createElement('div');
    div.className = 'photo-item';
    div.style.backgroundColor = randomColor();
    div.style.gridRow = `${row} / ${row + 2}`;
    div.style.gridColumn = `${col} / ${col + 2}`;

    document.body.appendChild(div);
    col += 2;
    if (col > 11) {
    row += 1;
    col = row % 2 === 0 ? 2 : 1;
    }
    }
    script>
    html>


    这是一个非常简单的照片墙效果,如果不使用grid的话,我们大概率是会使用定位去实现这个效果,但是换成grid的话就非常简单了;


    而且代码量是非常少的,这里就不提供码上掘金的 demo 了,感兴趣的同学可以将代码复制下来自行查看效果;



    2. 漫画效果


    image.png




    在漫画中有很多类似这种不规则的漫画框,如果使用定位的话,那么代码量会非常大,而且还需要计算一些位置,使用grid的话就非常简单了;


    可以看到这里还有一个气泡文字显示的效果,按照页面书写顺序,气泡是不会显示的,这里我们可以使用z-index来实现,这里的z-index值越大,元素就越靠前;


    而且气泡文字效果也是通过grid来进行排版的,并没有使用其他的布局来实现,代码量也并不多,感兴趣的同学可以自行查看;



    3. 画报效果


    image.png




    在一个画报中,我们经常会看到文字和图片混合排版的效果,由于这里直接使用的是渐变的背景,而且我的文字都是随意进行排列的,没有什么规律,所以看起来会比较混乱;


    在画报效果中看似文字排版非常混乱不规则,但是实际上设计师在设计的时候也是会划分区域的,当然用定位也是没问题的,但是使用grid的话就会简单很多;


    我这里将页面划分为12 * 12区域的网格,然后依次对不同的元素进行单独排列和样式的设置;



    流式布局


    流式布局指的是页面的内容会随着屏幕的大小而变化,流式布局也可以理解为响应式布局;


    但是不同于响应式布局的是,流式布局的布局不会像响应式布局那样发生变化,只是内容会随着轴进行流动;


    通常这种指的是grid-template-columns: repeat(auto-fit, minmax(0, 1fr))这种;


    直接看效果:


    QQ录屏20231210222012.gif




    这里有两个关键字,一个是auto-fit,还有一个是auto-fill,在行为上它们是相同的,不同的是它们在网格创建的不同,



    image.png



    就像上面图中看到的一样,使用auto-fit会将空的网格进行折叠,可以看到他们的结束colum的数字都是6;


    像我们上面的实例中不会出现这个问题,因为我们使用了响应式单位fr,只有使用固定单位才会出现这个现象;


    感兴趣的同学可以将minmax(200px, 1fr)换成200px尝试;



    对比 Flex 布局


    在我上面介绍了这么多的布局场景和案例,其实可以很明显的发现一件事,那就是我使用grid进行的布局基本上都是大框架;


    当然上面也有一些布局使用flex也是可以实现的,但是我们再换个思路,除了flex可以做到上面的一些布局,float布局、table布局、定位布局其实也都能实现;


    不同的是float布局、table布局、定位布局基本上都是一些hack的方案,就拿table布局来说,table本身就是一个html标签,作用就是用来绘制表格,被拿来当做布局的一种方案也是迫不得已;


    web布局发展到现在的我们有了正儿八经可以布局的方案flex,为什么又要出一个grid呢?


    grid的出现绝对不是用来替代flex的,在我上面的实现的一些布局案例中,也可以看到我还会使用flex


    我个人理解的是使用grid进行主体的大框架的搭建,flex作为一些小组件的布局控制,两者搭配使用;


    flex能实现一些grid不好实现的布局,同样grid也可以实现flex实现困难的布局;


    本身它们的定位就不痛,flex作为一维布局的首选,grid定位就是比flex高一个维度,它的定位是二维布局,所以他们之间没有必要进行对比,合理使用就好;


    总结


    上面介绍的这么多基于grid布局实现的布局方案,足以看出grid布局的强大;


    grid布局的体系非常庞大,本文只是梳理出一些常见的布局场景,通过grid布局去实现这些布局,来体会grid带来的便利;


    可能需要完全理解我上面的全部示例需要对grid有一定的了解才可以,但是都看到这里了,不妨去深挖一下;


    grid布局作为一项强大的布局技术,有望在未来继续发展,除了我上面说到的布局,grid还有很多小技巧来实现非常多的布局场景;


    碍于我的见识和文笔的限制,我这次介绍grid肯定是有很多不足的,但是还是希望这篇文章能为你对于布局相关能有新的认识;


    作者:田八
    来源:juejin.cn/post/7310423470546354239
    收起阅读 »

    相比拼多多市值一路狂奔,阿里巴巴究竟输在哪里?

    相信最近在互联网界最热门的事情就是拼多多的市值超过了阿里。 这个事情为什么有这么大的轰动?想当年阿里可是电商的一个阿里巴巴在2017年的时候市值超过了亚马逊。成为了中国乃至世界互联网电商界不可小觑的力量。 然而短短在八年的时间里面万亿市值就被一个在2015年...
    继续阅读 »

    相信最近在互联网界最热门的事情就是拼多多的市值超过了阿里。



    这个事情为什么有这么大的轰动?想当年阿里可是电商的一个阿里巴巴在2017年的时候市值超过了亚马逊。成为了中国乃至世界互联网电商界不可小觑的力量。


    然而短短在八年的时间里面万亿市值就被一个在2015年9月份创建的拼多多给反超了,而且据公开数据,阿里现在员工有20万多同学,拼多多只有1万多。


    与此同时,阿里巴巴的国内外电商份额在急速的下降,而拼多多不仅在国内增速一骑绝尘,在全球范围已经开始输出拼多多的低价力量,据统计拼多多已经占据国内26%的市场份额,旗下的temu在欧美澳加如入无人之境,充分让这些外国人了解什么是“兄弟就砍我一刀”的消费降级的乐趣。


    与此同时,令人担忧的是,阿里巴巴目前没有任何能够快速绝地逢生的迹象。


    就如当年百度出了魏则西事件后,我们振聋发聩发馈的一问,谷歌退出中国后的百度到底怎么了?到底发生了什么,一个原来在中国互联网市值排名第一的公司,到底为什么在短短的几年间就到了道德沦丧不争气的地步?


    虽然百度和阿里面对问题性质截然不同,而今天我们相似的也可以问一句,为什么阿里巴巴到了目前这个境遇?


    网上已经有很多文章来谈论为什么阿里巴巴会被拼多多反超。理由有很多,比如说在战略上的决策失误,阿里坚持了新零售升级的消费主义,比如说拼多多非常聚焦收敛,而阿里巴巴投资收购了不少业务,业务分散,比如说拼多多比较低调,而阿里巴巴出了很多公关事件。比如说阿里巴巴的内部味道过重,而拼多多就是拿员工时间换钱不谈价值观。


    这些固然都是阿里巴巴为什么现在业绩下滑被拼多多反超的原因。但今天我想换一个思路来用拟人的方式,从情绪上所以说根本原因。


    我觉得根本原因就是阿里巴巴太过于傲慢。也就是傲慢这个本质上的原因,才导致了一系列战略决策的失误,用人的失误,情报的失误。


    为什么这么说?在原来一个庞然大物下居然还能存在一个拼多多能够釜底抽薪,难道阿里巴没有任何人能够觉察到拼多多从零开始的这种号召力和变革力吗?难道阿里巴巴没有牛逼的人物能够反制拼多多吗?难道阿里巴巴没有人才了吗?


    显然这些都是否定的,成立于1999年,历时已经24年,阿里巴巴能够从零做到全球目前的这个地步,意味着它就有一个强大的管理团队,强大的人才以及强大的组织力,那为什么依然没有阻止拼多多的起来呢?


    这个企业竞争形势变化在《创新者的窘境》里面说的非常的明确,这个就是所谓的小公司对大公司的颠覆式创新。也就是一个小公司,往往能够从新的维度,新的方向,形成快速的行动力,终究在不起眼的地方,再造一个大市场。往往小公司利用到了更新的一些理念和价值,使得小公司能够在短时间内在大公司的眼皮底下快速的形成大规模的创新力量,从小起步,犹如积蓄力量的蚂蚁,最终掀翻步履维艰的大象。


    而大公司往往在成功之后就会有自己的一个路径依赖。在路径依赖的情况下的话,就往往会主观上忽视掉最弱小的竞争对手,甚至完全不把竞争对手当回事儿。


    换一句话来说就是公司太大,大的极度傲慢,历史上已经有数见不鲜的例子,比如刚刚倒下的全球手机霸主诺基亚。


    在拼多多刚刚起来的时候,内部同学已经有很多人都感知到拼多多的竞争。但是阿里巴巴犹如一条非常大的一个航船,在让商家没有难做的生意愿景上,新零售升级,在双十一GMV增长方向上无法停止。于是慢慢导致高管乃至最底层的执行的人都有意无意忽略了拼多多的增长。


    我之前和一个天猫的研究生同学吐槽他的双十一优惠券计算复杂度之高。我这位同学骄傲的告诉我们,只是我不是天猫的目标客户,他说,其实你不知道有多少客户非常喜欢我们的搭楼游戏,喜欢我们的复杂的优惠计算,说完一脸傲娇。



    于是就在这种情况下,拼多多一路狂奔,简单粗暴的后续界面,简单粗暴的退款逻辑,一刀刀砍向了原来忠诚的淘宝用户。从开始抢走了淘宝的低端羊毛用户,到抢走了淘宝的中间用户,直到现在的强力补贴,连高端消费用户都抢走了。


    拼多多说秉承的客户第一理念,让所有消费者如沐春风,在被淘宝商家歧视的价值主张里,好像找到了另外一个发泄口。


    这个就是颠覆式创新的力量。也是无数大公司单纯的血的教训。只不过诸如淘宝这样的大公司还是依然没有躲过这样的故事。


    当然我依然相信阿里巴巴是一个有韧性,有希望的公司。毕竟阿里巴巴原来就从最艰难的路子里面杀出一条血路。此次确实是淘宝面对的最大危机,但我相信也是新希望的开始。


    正如微软CEO纳德拉所说,人们往往高估了短期的影响力而低估了长期的影响力。


    胜者坚持长期主义,鹿死谁手,犹未知之。


    作者:ali老蒋
    来源:juejin.cn/post/7308643782376570934
    收起阅读 »

    今天还要用 React 吗:利弊权衡

    web
    免责声明 本文属于是语冰的直男翻译了属于是,略有删改,仅供粉丝参考,英文原味版请临幸 The Pros and Cons of Using React Today。 在过去的十年中,React 因辅助整个行业的开发者构建顶尖 UI 的超能力,为大家所熟知。...
    继续阅读 »




    免责声明


    本文属于是语冰的直男翻译了属于是,略有删改,仅供粉丝参考,英文原味版请临幸 The Pros and Cons of Using React Today



    00-wall.jpg


    在过去的十年中,React 因辅助整个行业的开发者构建顶尖 UI 的超能力,为大家所熟知。


    本文在 2023 年底和 2024 年对 React 进行了深入而平衡的展望。我们将看看它值得称道的优势、明显的短板,以及对当今开发者的可靠性。


    让我们先从 React 与众不同的创新功能开始,然后再将注意力转向它给开发者带来的挑战。


    React JS 是什么鬼物?


    ReactJS 是一个组件筑基的 JS 库,最初由 Facebook 创建,并在十年前发布。该库简化了开发者创建交互式 UI,同时有效管理组件状态。它能够为复杂 App 编写多个组件,而不会丢失它们在浏览器的 DOM(文档对象模型)中的状态,这对一大坨开发者而言是一个明显的福利。


    虽然 React 主要是一个用于 Web App 的工具,但其多功能性通过 React Native 扩展到移动 App 开发。这个强大的开源库允许开发 Android、iOS 和 Windows App,展示了 React 跨平台开发的灵活性。


    React 生态系统


    React 最大的资源之一是其庞大的生态系统,其中充满了第三方库和工具,极大地扩展了其功能。这对于路线规划 App 等复杂项目尤其有利,这些项目通常依赖集成大量外部服务,比如地图 API 和路径算法。


    React 的灵活性和与各种第三方服务的兼容性简化了集成过程,允许开发者使用高级功能增强其 App,而不会产生过多的开销。


    其核心是基本的库和工具,比如 React Router,用于 SPA(单页应用程序)中的动态路由,确保无缝的 UX(用户体验)过渡。Redux 是一个关键的状态管理工具,它为状态创建了一个中心化 store,使不同的组件能够一致地访问和更新它,这在大型 App 中尤为重要。


    React.js:不仅仅是复杂性


    虽然 React 在 UI 创建方面表现出色,但在状态管理和 SEO 优化等领域存在不足。幸运的是,更广泛的 JS 生态系统提供了许多工具,这些工具好处多多,比如更简化的状态管理方案、通过 SSR(服务器端渲染)增强的 SEO 和数据库管理。让我们瞄一下 React 若干更突出的集成选项。


    对于那些寻求更简单替代方案的人而言,MobX 提供了一种直观的状态管理方案,并且样板更少。此外,Next.js 通过提供 SSR 和 SSG(静态站点生成)解决了客户端渲染 App 的当前 SEO 限制。在开发和测试方面,CRA(Create React App)简化了设置新前端构建管道的过程,使开发者能够立即开始运行,而不会受到配置的困扰。


    同时,Storybook 作为一个 UI 开发环境,开发者可以在其中独立可视化其 UI 组件的不同状态。Jest 在单元和快照测试中很受欢迎,它与 React 无缝集成。由 Airbnb 开发的 Enzyme 是一个测试工具,它简化了断言、操作和遍历 React 组件输出的过程。


    额外的库和工具进一步丰富了 React 生态系统;Material-UI 和 Ant Design 提供了全面的 UI 框架,可以满足美学和功能要求,而 Axios 则提供了一个 Promise 筑基的 HTTP 客户端来发送 HTTP 请求。React Query 简化了获取、缓存和更新异步数据的过程,React Helmet 有助于管理对文档头的更改,这对于 SPA 中的 SEO 至关重要。


    React 与其他技术的集成 —— 比如后端框架,包括 Node.js 和 Django;状态管理库,比如 Apollo for GraphQL,增强了其灵活性。如今,开发者甚至可以将 PDF 查看器嵌入到网站中,并大大优化 UX。


    然而,React 的不断发展要求开发者跟上最新的变化和进步,React 为试图制作高质量、可扩展和可维护的 Web App 的开发者提供的无数解决方案抵消了这一挑战。


    React 之利


    React 已经将自己确立为构建动态和响应式 Web App 的关键库,原因如下:


    组件筑基架构


    传统的 JS App 在扩展时经常会遇到状态管理问题。虽然但是,React 提供了复杂的、独立维护的可复用组件,允许开发者在不影响其他页面的情况下更新网页的局部 —— 确保松耦合和协作功能。


    当然,这个概念并不是 React 独有的;举个栗子,Angular 也使用组件作为基本构建块。尽管如此,React 庞大的社区、Meta 的支持和相对丝滑的学习曲线使其成为开发者的最爱。


    开发中的增强定制


    React 的多功能性在构建针对特定业务需求量身定制的 App 时大放异彩。尤其是其组件筑基架构允许在 App 中无缝组装复杂结构。


    举个栗子,在构建集成仪表板时,React 的生态系统有助于将各种模块(比如图表、小部件和实时数据源)集成到一个有凝聚力的 UI 中,使开发者能够打造不仅功能强大,而且直观且具有视觉吸引力的 UX。


    这种强大的适应性恰恰凸显了为什么 React 仍然是旨在创建多功能和健壮的 Web App 的开发者的首选。


    面向未来的开发者选项


    React 面向未来的特性是它为开发者提供的最引人注目的优势之一。React 灵活的架构迎合了当前的 Web 开发需求,同时也无缝地适应了将塑造行业近期的新兴技术。


    值得注意的是,机器学习正在向 Web 开发领域取得重大进展,2022 年全球 ML 市场价值已经达到 210 亿美元,这凸显了 React 面向未来的特性以及与此类进步相协调的能力的重要性。


    其中一个比较突出的例子是 TensorFlow.js,一个用于图像和模式识别的 ML 库。同样,React 允许集成 ML 驱动的聊天机器人甚至推荐功能。此外,WebAssembly 可以帮助允许用 Rust、Python 或 C++ 编码的 ML 应用程序存在于原生 App 中。


    用于状态管理的 Redux


    在 SPA 中,多个组件驻留在单个页面上,管理状态和组件间通信很快就会变得具有挑战性 —— 这正是 Redux for React 的亮点。


    作为 React 不可或缺的一部分,它充当“管理器”,确保组件之间的数据流一致且准确,集中状态管理并促进组件自治性,显着提高数据稳定性和 UX。


    React 之弊


    虽然 React 为不同技能水平的开发者提供了许多优势,但它并非没有各自的缺点,包括以下内容:



    • 复杂的概念和高级模式:React 引入了若干高级概念和模式,这些概念和模式一开始可能会让初学者不知所措。要了解 JSX、组件、props、状态管理、生命周期方法和钩子,需要扎实掌握 JS 基础知识。

    • 与其他技术的集成复杂性:React 经常与其他工具和技术结合使用——如 Redux、React Router 和各种中间件 —— 对于新手来说,了解如何将这些技术与 React 集成可能极具挑战。

    • 非 JS 开发者的障碍:React 对 JS 的严重依赖对于不精通 JS 的开发者而言可能是一个障碍。虽然 JS 是一种通用且广泛使用的语言,但来自不同编程背景的开发者可能会发现适应 JS 的范式和 React 的使用方式极具挑战。

    • 不是一个成熟的框架:React 主要处理 MVC 的“视图”部分,也称为模型视图控制器架构。对于“模型”和“控制器”,需要额外的库,与 Angular 等功能齐全的框架相比,这最终会导致结构化程度较低且可能更加混乱的代码。

    • 代码膨胀:React.js 的特点是其大量的库和依赖需求,因其臃肿的 App 而臭名昭著。这种膨胀通常表现为较长的加载时间,尤其是在复杂的项目中。该框架的结构严重依赖其虚拟 DOM,即使是次要功能也需要加载整个库,这大大增加了 App 的数字足迹并降低了其效率。

    • 在传统设备和弱网络上的性能下降:React.js App 的性能在较旧的硬件和互联网连接较差的地区往往会下降。这主要是由于框架的客户端渲染模型和密集的 JS 处理。这些因素可能会导致渲染交互式元素的延迟,这在计算能力有限的设备或带宽有限的环境中尤为明显,这会对 UX 产生不利影响。


    最终裁决


    随着 Web 开发领域的不断发展,React 的灵活性和强大的生态系统使其处于有利地位。它将继续使开发者能够将尖端功能无缝地整合到其 App 中。虽然但是,虽然 React 为开发者提供了很多好处,但它仍然有其缺点。


    React 的复杂性和对高级 JS 概念的依赖带来了曲折的学习曲线,尤其是对于新手或尚未精通 JS 的人。它还主要解决了 MVC 架构的“视图”方面,需要额外的工具来进行完整的 App 开发,这可能会导致更复杂和结构化更少的代码库。


    尽管存在这些挑战,但庞大而活跃的 React 社区在其持续发展中发挥着至关重要的作用。在可预见的未来,它将继续成为 Web 和移动 App 开发的关键库。


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

    一个30岁老前端的人生经历(学习+工作+婚姻+孩子),给迷茫的朋友一点激励。

    web
    前言 我93年的,还差几天就到30周岁生日。做前端开发大概也有6、7年,算是老前端了。 2023 写作 在掘金看文章已经很多年了,学到了很多东西。今年三月份在掘金写下了第一篇文章,开始的想法是把自己的开发笔记整理一下写成文章,来巩固一下自己的知识,没想到在写文...
    继续阅读 »

    前言


    我93年的,还差几天就到30周岁生日。做前端开发大概也有6、7年,算是老前端了。


    2023


    写作


    在掘金看文章已经很多年了,学到了很多东西。今年三月份在掘金写下了第一篇文章,开始的想法是把自己的开发笔记整理一下写成文章,来巩固一下自己的知识,没想到在写文章分享的过程中帮助了很多人,也得到了很多人认可,让我写文章分享的动力也越来越强,基本每周都会写一篇,闲在没事的时候基本都是在构思下一篇文章写什么。希望明年能写更多的文章,帮助更多的人。


    写作给我带来了什么



    1. 巩固知识。把自己学到的东西,分享出去,印象更深刻了。

    2. 更多的机会。写作过程中,有大厂私信我,给我一些面试机会,这个时候的面试机会还是很宝贵的,不过因为一些原因都拒绝了。

    3. 快乐。很多人私信我说很感谢我分享的东西,让他们学到了很多东西。看到这些感谢的话,自己得到认可,还是很开心的。

    4. 钱。写文章参与掘金的金石计划活动,陆陆续续差不多获得了接近1000的收入,每次拿到钱,带着老婆孩子去吃顿好的,还是不错的。


    降薪


    今年公司受大环境影响,裁了一部分人,留下来的人也都降薪了。开始有点接受不了,想跑,但是因为是在上海嘉定郊区,附近找不到好的工作,最近的也要1个小时的地铁,并且小孩刚上一年级,上海基本不可能跨区转校,所以打消了换工作的念头,只能在公司了干下去,相信公司会好起来的。


    家庭


    看了上面肯定有倔友怀疑,30岁小孩怎么能上一年级?不错我大四结的婚,还是奉子成婚,所以早早的有了孩子。


    因为自己小时候是留守儿童,不想让自己孩子过留守儿童的生活,所以孩子一直是和我们在一起,记得我刚出来工作的时候,一个月才3500,我老婆全职带孩子,这些薪资刚够花销,生活过的比较拮据,有时候还要靠我父母接济。


    现在收入稍微好了一些,但是我们还没有买房子,存款也不多,在别人看来压力可能会有点大,但是我心比较大,平时消费欲望也比较低,对钱不是那么渴望,一家人在一起也是开开心心的,不过还是想给老婆孩子一个自己的家,努力奋斗吧。


    孩子今年上一年级了,再上一年级后,作业明显变多了,每天都要写到很晚,看着孩子很累,也没办法,不写好作业,第二天老师就会在群里点名说。


    孩子比较调皮,经常在学校里和同学打架,最多的时候,一周被班主任叫了三次家长,有时候是他的错,有时候是别人的错。因为老婆全职在家带孩子,这些都是我老婆处理的,她最近有点焦虑,每天都担惊受怕的,害怕孩子在学校又闯祸,整的我也有点焦虑,工作状态有点差。


    关于孩子打架的事,我和老婆猜测可能是学习压力太大了,每天放学回来就开始写作业,一直写到睡觉,平时还有一些兴趣班要上,玩的时间太少了,积累了很多怨气没地方发泄,所以比较暴躁。现在每天放学后先让他玩半个小时再写作业,并且和他多次沟通,告诉他暴力解决问题是不对的,目前稍微好了一些。有这方面经验的兄弟,可以在评论区指点一下。


    健康


    今年五月份的时候,身体有点不舒服,平时熬夜比较多,人也比较胖,就想着去体检一下,体检结果肝功能有一项转氨酶比正常高三倍,然后到医院做了一次全身体检,抽了9管血,结果还好没啥大问题,可能是脂肪肝导致的转氨酶很高,医生建议要减肥。


    因为平时比较忙,没有时间健身,就搞了个自行车上下班骑,公司离家大概5公里左右,上下班每天骑10公里左右,从6月份买车到现在基本没断过,虽然体重没有降下来,但是精神状态和体力好了不少,以前稍微有点运动量,就气喘吁吁全身冒汗,现在好多了。


    希望倔友们多注意健康,少熬夜,身体才是最重要的。


    2024展望


    关于2024,立几个flag吧



    1. 最少分享40篇文章

    2. 完善fluxy-admin平台,把前后端低代码平台集成进来,做出一个企业级低代码平台开源出去。

    3. 看react源码,并做个专栏分享。每次面试,被面试官问react底层一些东西的时候,回答的都不是很好,就是因为没有彻底了解底层,所以回答的都很片面,明年一定要把react吃透。今年年初买了卡颂大佬的react设计原理书籍,现在在床头吃灰呢。

    4. 减肥


    个人真实经历分享


    给大家分享一下我的个人真实经历,与君共勉。


    我出生在一个很普通的农村家庭,有点小聪明,但是贪玩,高中三年基本都是看电子书度过,天天上课把手机放在书下面,装作看书,实际上都是在看小说,现在回想起来,想不通老师为啥重来没有发现过。


    开始决定好好学习是高三上学期,有次上课和同桌说话,被老师说你自己不学,不要影响别人学习,还说了一些很难听的话(当时我在班里大概倒数10几名的样子,同桌10几名左右。),虽然我贪玩,但是我自尊心比较强,我就不服气,然后上课开始好好听课,后面一次月考竟然考到了10几名,和同桌成绩差不多,然后就开始飘了,上课又开始看小说,下次月考又考的很差,然后难受,又开始好好听课,就这样成绩一会好一会坏,不过拿了几次进步奖,同学笑话我是不是为了拿进去奖,故意退步的。


    真正让我决定好好学习的是高三下学期开学的前一天晚上,我家庭条件不是特别好,而我当时因为中考考的很差,只能上一个学费比较贵的私立高中,高三下学期开学前一天晚上我爸还在为我筹学费(家里没有穷到付不起学费的地步,只是当前家里钱被其他地方占用了,拿不出来。),最终从亲戚那里借了点钱,然后我爸把钱交到我手里,让我明天交学费,看着我爸粗糙的手(我爸是干工地的),这一刻我决定好好学习,不然都对不起这学费。高三下学期上课就没看过手机了,由于底子太差,高考离二本线差了几分,最终上了个三本。


    高考结束,暑假期间迷上了英雄联盟。大学的时候,室友也玩,经常和室友一块包夜,第二天要么旷课在宿舍睡觉,要么在教室最后一排睡觉,导致第一学期就挂了三科,不过后面补考都过了。后面还是继续玩,大二下学期突然觉得不能这样浑浑噩噩了,还不如出去打工,给家里省点学费还能挣点钱(不知道当时为啥有这想法),然后就和父母说了一下,不上学了出去打工,当时是想退学的,还好我好朋友和我说先休学吧,以后后悔还有机会。


    在苏州找了一个工厂,干了一个星期干不下去了,身体上的劳累倒是其次,主要是看不到生活的希望,每天就像一个机器一样,后面就回去上学了,然后学习非常努力,后面还得了奖学金,毕业论文也被评上了优秀论文,也是优秀毕业生,但是毕业学校没有给学位证,只给了毕-业-证,因为挂科超过5门(补考过了也没用,只要挂科超过5门,就完了,我们那一届有不少没有学位证的。),这个政策最开始都不知道,没有学位证后问学校,学校才说的,也不能怨学校,算是自食其果吧。没有学位证对找工作还是有很大影响的,后面有几次面试通过大厂了,因为没有学位证而被拒。


    实习的时候,实习单位和学校是有合作的,学校知道我的事迹也知道我在实习单位表现的不错,所以就邀请我回去给学弟学妹们分享我的经历。当时分享完后,有几个学弟加我微信说,他们现在也是这个状态,我的经历让他们有了重新开始的信心。


    后面的工作之旅也是一路坎坷,不过最后的结果是好的,目前在公司里做前端负责人,收入还不错。工作之旅明年年终总结再和大家分享吧。


    和大家分享我的经历,就是想告诉大家永远不要放弃,只要坚持,就会有希望,同时也想告诉大家每个人都要为自己做过的事负责,因为贪玩我没考上好一点的大学,因为贪玩我没有学位证,但是我后面迷途知返,通过自己的努力,还是得到了一份不错的工作,一个美满的家庭。


    最后


    很喜欢deft在夺冠时说的一句话:我唯一会的仅剩英雄联盟,如果在这条路上我不能成功,那我的人生将没有任何意义。


    而我唯一会的就是写代码,我不一定能成功,但是我想努力做到更好。


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

    惊!27岁程序媛的一年竟然干了这些事

    hello铁铁们,这是继年中总结之后的又一篇年底回顾。 首先,真的很高兴我的文章能被很多很多的小伙伴看到,每一条评论我都有认真看,有一部分评论也让我对未来产生了新的感悟,也从未想到,会有这么多朋友同我一样有着类似的困惑,也很开心在文章发布之后收获了志同道合的伙...
    继续阅读 »

    hello铁铁们,这是继年中总结之后的又一篇年底回顾。

    首先,真的很高兴我的文章能被很多很多的小伙伴看到,每一条评论我都有认真看,有一部分评论也让我对未来产生了新的感悟,也从未想到,会有这么多朋友同我一样有着类似的困惑,也很开心在文章发布之后收获了志同道合的伙伴。

    这篇文章,我将为我上一篇文章中还未产生结果的问题画上一个句号,并且浅浅思考一下我即将到来的28岁的人生。


    f0c09162ef99abf035453ada2e742d5e.jpeg

    我好像一只温水中的青蛙


    我依然在北京这座城市漂泊,我没有勇气或者说没有足够的能力与底气回到老家扎根,我依然过着普普通通的周中上班周末摆烂的人生,两点一线的在舒适圈中挣扎,不愿逃脱。

    再加上现在经济依然低迷,对于无房无车的普通的我来说,我依然不敢潇洒的离开这个岗位,经常和朋友说“等什么时候裁了我给我n+1”,但是如果那一天真的到来,我依然不知道,我的未来在哪

    (虽然但是,不思考人生时候的自己还是蛮开朗的)


    给2023画上句号


    我很喜欢27岁的我。


    因为这一年没什么坏消息,平淡的生活很快乐,偶尔周六和朋友聚会,周日在家休息,偶尔和朋友去临近的城市旅游,和闺蜜吐槽公司发生的种种奇葩事件,和同事的关系还算融洽,家人也依然健康。
    唯一的遗憾就是持续性的鼻炎让我查出了猫子过敏,不得不把我养了三年的猫子送回了老家,好在猫子回家之后心情还算不错,家人也很喜欢它。

    附上一张在东北看雪的傻猫


    微信图片_20231208154352.png

    然后我不甘寂寞又养了鼠子,哈哈


    微信图片_20231208160621.png

    学习方面


    1.软考通过!


    这是今年中旬定好了目标的任务,所以一直都有很认真的在复习,虽然今年笔试改成了机试,通过率降低了很多,并且很认真复习的内容好多都没有考到,但努力依然得到了好的结果,紧张的成绩查询之后带来了好的消息,真的很令人开心!
    我依然相信,努力就会有结果,哪怕最后可能不尽人意,但一定会有所收获。
    由于明年几个计划的优先级较高所以还没有考高级的打算,另外考过的小伙伴透露一下是不是真的很难啊(听说高级上了好几个level)


    QQ图片20231208155133.jpg

    2.开始参与开源项目


    无聊的生活每天枯燥的工作,通过上一篇文章有一个大哥联系了我,邀请我参与他的开源项目,因为他的项目体系已经很完整,所以我毫不犹豫的参与到其中去,接触到了一些新的思想,对于很久没有长进的我来说,做做新东西的免费劳动力也是很开心的事(虽然也没贡献几行代码哈哈)。


    3.接触web3、区块链


    开源的大哥还带我接触了web3,区块链等领域,也有机会一起合作区块链开发的兼职项目(虽然目前还是启动阶段没我啥事)。


    4.开始学习java


    程序员这个行业太卷了,与其不如别人卷我不如我先发制人,于是我决定努力向全栈发展,哪怕不能全栈也总归比只会前端要好,所以在软考结束之后,我就开始了java学习计划,目前还处于基础语法学习阶段,本来这个任务是放在明年开始的,因为有了一定的空闲时间所以比计划有所提前。


    5.依然坚持每周1~2道LeetCode


    leetcode依然在刷,但是已经没什么精力去突破困难的题了,而且由于长时间不动脑子,导致好多中等难度的题也不会做了,所以这方面还是要维持住,不然退步的实在太快。


    最后附上每天学习进度表,做计划真的很有用,不然我每天真的什么都不想做,每当计划完成打上√的时候还是会有成就感。


    微信图片_20231208160823.png
    微信图片_20231208160917.png
    微信图片_20231208161127.png

    好多人问我这个日历工具是什么,其实就是excel找个模板,然后自己写内容,不够智能但是这种自己逐渐调整出来的时常能带来更多思考。


    6.天气转凉,骑行搁置


    由于天冷,我的车锁都被冻得梆硬,虽然是东北人但是我怕冷,后来就没有再骑行,等春暖花开时我一定继续践行我的骑行计划!


    2024目标


    28岁大龄女中年坚持在程序员团队中浑水摸鱼,LeetCode继续刷,计划表继续做,坚持学习,励志坚决不做家庭主妇!


    学习java,向全栈发展


    坚持每天学习英语


    持续参与开源,接触兼职工作


    先附上12月定的计划表,如果有新的的临时计划依然会做补充。


    微信图片_20231208165407.png

    flag还是少立,最好一段时间认真做好一件事,有计划的践行目标,完成的概率会提升许多。


    真想永远做温水中的青蛙不被煮烂


    好像已经很努力了,但是依然很普通,逐渐饱和的市场让我这种螺丝钉即使做点什么努力也不会改变任何结果,我知道,这个世界上有很多很多更加优秀的人。

    我好像有一些居安思危的改变,但骨子里依然没变,我不知道当危险来临的那天我该怎么办,我预测不到未来,比如每次面试时面试官问我“未来的规划”时,我还是不知道如何回答他,就好像在我很小的时候立过的那些flag一个都没有实现,但是我现在依然不温不火的过着还算不错的人生,也许这就是普通人的一生

    确实,永远有人更好,当下便是最好。

    加油吧,为了梦想,曾经立过的flag,哪怕他们不会实现,也给日子一点奔头,一起努力吧,加油!


    2648ff5ff16d83fcde8e1f6117c4f472.jpeg
    作者:毛毛裤
    来源:juejin.cn/post/7310560146623021090
    收起阅读 »

    客户要一次加载上千张轮播图如何丝滑地实现?不用第三方(没找到),十来行核心代码实现

    web
    引言 最近再做3D的大屏,然后客户发来1G的图片压缩包,让我全放上去当轮播图 这不得卡死啊,现成且现代化好用的第三方库没找到 于是又到了我最爱的实现源码环节,核心代码十多行即可 底部有源码 思路 压缩图片 轮播只需要两张,来回交换,用点障眼法就是无缝了 批...
    继续阅读 »

    引言


    最近再做3D的大屏,然后客户发来1G的图片压缩包,让我全放上去当轮播图


    这不得卡死啊,现成且现代化好用的第三方库没找到


    于是又到了我最爱的实现源码环节,核心代码十多行即可


    底部有源码


    思路



    • 压缩图片

    • 轮播只需要两张,来回交换,用点障眼法就是无缝了


    批量压缩


    这个用canvas就能实现,于是我写了个HTML来批量压缩


    canvas转存图片时,使用jpeg | webp的格式即可压缩,MDN上有


    使用canvas.toBlob可以压缩更多空间,这个不是重点,提一嘴而已


    image.png


    虚拟无缝轮播实现


    直接一张动图,清晰明了的解决问题


    t.gif


    是不是看起来很简单,加载前两张图,当动画结束时,改变移动那张图的src,同时位移


    再加个overflow: hidden不就行了吗


    e.gif


    编码实现


    HTMLCSS我就不贴了,这个没什么难度,容器固定大小,子元素排列一行


    然后给包装的容器添加个transform即可


    下面是用vue3写的,定义了一个imgIndexArr数组装当前要显示的索引


    _data为计算属性,根据imgIndexArr自动变化,里面放的就是图片


    我们只需要修改imgIndexArr即可实现数据切换


    image.png


    我们需要在动画完成时改变,即添加ontransitionend事件


    当触发next方法,图片滚动停止后,就要执行onTransitionEnd


    定义俩变量,一个代表最左边的图,一个为右边的图


    这里根据变量,决定谁会更新src,并且改变left值实现位移,不好描述啊


    transform会一直向右位移,left值也是,所以他们会形成永动机


    image.png


    HTML里写上他们位移的样式即可自动更新


    image.png


    Bug


    至此,看着已完成,似乎没有任何问题


    但是你把页面隐藏了过后,过一会图片全都不见了,我们打开控制台看看为什么


    可以看到,left停止更新了,也就是说,onTransitionEnd没有执行


    image.png


    transitionend在你浏览器隐藏页面时,就会停止执行


    这时需要在页面隐藏时,停止执行,执行如下代码即可


    /** 离开浏览器时 不会执行`transitionend` 所以要停止 */
    function bindEvent() {
    window.addEventListener('visibilitychange', () => {
    document.hidden
    ? stop()
    : play()
    })
    }

    这时一定有人会说,你这不能往左啊,没有控制组件啊


    如果要往左的话,只需要把两张图轮流交换改成4张图即可


    具体逻辑都是差不多的



    源码: gitee.com/cjl2385/dig…



    作者:寅时码
    来源:juejin.cn/post/7310111620368597011
    收起阅读 »

    前段时间面试了一些人,有这些槽点跟大家说说

    大家好,我是拭心。 前段时间组里有岗位招人,花了些时间面试,趁着周末把过程中的感悟和槽点总结成文和大家讲讲。 简历书写和自我介绍 今年的竞争很激烈:找工作的人数量比去年多、平均质量比去年高。裸辞的慎重,要做好和好学校、有大厂经历人竞争的准备 去年工作...
    继续阅读 »

    大家好,我是拭心。


    前段时间组里有岗位招人,花了些时间面试,趁着周末把过程中的感悟和槽点总结成文和大家讲讲。


    image.png


    简历书写和自我介绍



    1. 今年的竞争很激烈:找工作的人数量比去年多、平均质量比去年高。裸辞的慎重,要做好和好学校、有大厂经历人竞争的准备


    image.png



    1. 去年工作经历都是小公司的还有几个进了面试,今年基本没有,在 HR 第一关就被刷掉了

    2. 这种情况的,一定要走内推,让内推的人跟 HR 打个招呼:这人技术不错,让用人部门看看符不符合要求

    3. 用人部门筛简历也看学历经历,但更关注这几点:过去做了什么项目、项目经验和岗位对不对口、项目的复杂度怎么样、用到的技术栈如何、他在里面是什么角色

    4. 如果项目经历不太出彩,简历上可以补充些学习博客、GitHub,有这两点的简历我都会点开仔细查看,印象分会好很多

    5. 现在基本都视频面试,面试的时候一定要找个安静的环境、体态认真的回答。最好别用手机,否则会让人觉得不尊重!

    6. 我面过两个神人,一个在马路上边走边视频;另一个聊着聊着进了卫生间,坐在马桶上和我讲话(别问我怎么知道在卫生间的,他努力的声音太大了。。。)

    7. 自我介绍要自然一点,别像背课文一样好吗亲。面试官不是考你背诵,是想多了解你一点,就当普通聊天一样自然点

    8. 介绍的时候不要过于细节,讲重点、结果、数据,细节等问了再说

    9. 准备介绍语的时候问问自己,别人可以得到什么有用的信息、亮点能不能让对方快速 get 到

    10. 实在不知道怎么介绍,翻上去看第 4 点和第 5 点

    11. 出于各种原因,很多面试官在面试前没看过你的简历,在你做自我介绍时,他们也在一心二用 快速地浏览你的简历。所以你的自我介绍最好有吸引人的点,否则很容易被忽略

    12. 你可以这样审视自己的简历和自我介绍:


      a. 整体:是否能清晰的介绍你的学历、工作经历和技能擅长点


      b. 工作经历:是否有可以证明你有能力、有结果的案例,能否从中看出你的能力和思考


      c. 技能擅长点:是否有岗位需要的大部分技能,是否有匹配工作年限的复杂能力,是否有区别于其他人的突出点



    面试问题


    image.png



    1. 根据公司规模、岗位级别、面试轮数和面试官风格,面试的问题各有不同,我们可以把它们简单归类为:项目经历、技能知识点和软素质

    2. 一般公司至少有两轮技术面试 + HR 面试,第一轮面试官由比岗位略高一级的人担任,第二轮面试官由用人部门领导担任

    3. 不同轮数考察侧重点不同。第一轮面试主要确认简历真实性和基础技术能力,所以主要会围绕项目经历和技能知识点;第二轮面试则要确认这个人是否适合岗位、团队,所以更偏重过往经历和软素质


    项目经历


    项目经历就是我们过往做过的项目。


    项目经历是最能体现一个程序员能力的部分,因此面试里大部分时间都在聊这个。


    有朋友可能会说:胡说,为什么我的面试大部分时候都是八股文呢?


    大部分都是八股文有两种可能:要么是初级岗位、要么是你的经历没什么好问的。哦还有第三种可能,面试官不知道问什么,从网上搜的题。


    image.png


    在项目经历上,面试者常见的问题有这些:



    1. 不重要的经历占比过多(比如刚毕业的时候做的简单项目花了半页纸)

    2. 经历普通,没有什么亮点(比如都是不知名项目,项目周期短、复杂度低)

    3. 都是同质化的经历,看不出有成长和沉淀(比如都是 CRUD、if visible else gone)


    出现这种情况,是因为我们没有从面试官的角度思考,不知道面试的时候对方都关注什么。


    在看面试者的项目经历时,面试官主要关注这三点:


    1. 之前做的项目有没有难度


    2. 项目经验和当前岗位需要的是否匹配


    3. 经过这些项目,这个人的能力有哪些成长


    因此,我们在日常工作和准备面试时,可以这样做:



    1. 工作时有意识地选择更有复杂度的,虽然可能花的时间更多,但对自己的简历和以后发展都有好处

    2. 主动去解决项目里的问题,解决问题是能力提升的快车道,解决的问题越多、能力会越强

    3. 解决典型的问题后,及时思考问题的本质是什么、如何解决同一类问题、沉淀为文章、记录到简历,这些都是你的亮点

    4. 经常复盘,除了公司要求的复盘,更要做自己的复盘,复盘这段时间里有没有成长

    5. 简历上,要凸显自己在项目面试的挑战、解决的问题,写出自己如何解决的、用到什么技术方案

    6. 投简历时,根据对方业务类型和岗位要求,适当的调整项目经历里的重点,突出匹配的部分

    7. 面试时,要强调自己在项目里的取得的成果、在其中的角色、得到什么可复制的经验


    技能知识点


    技能知识点就是我们掌握的编程语言、技术框架和工具。


    相较于项目经历,技能知识点更关键,因为它决定了面试者是否能够胜任岗位。


    image.png


    在技能知识点方面,面试者常见的问题有这些:



    1. 不胜任岗位:基础不扎实,不熟悉常用库的原理

    2. 技术不对口:没有岗位需要的领域技术

    3. 技术过剩:能力远远超出岗位要求


    第一种情况就是我们常说的“技术不行”。很多人仅仅在工作里遇到不会的才学习,工作多年也没有自己的知识体系,在面试的时候很容易被基础知识点问倒,还给自己找理由说“我是高级开发还问这么细节的,面试官只会八股文”。框架也是浅尝辄止,会用就不再深入学了,这在面试的时候也很容易被问住。


    第二种情况,是岗位工作内容属于细分领域,但面试者不具备这方面的经验,比如音视频、跨端等。为了避免这种情况,我们需要打造自己的细分领域技能,最好有一个擅长的方向,越早越好。


    第三种情况简单的来说就是“太贵了”。有时候一些资深点的开发面试被挂掉,并不是因为你的能力有问题,而是因为岗位的预算有限。大部分业务需求都是增删改查和界面展示,并不需要多复杂的经验。这种情况下,要么再去看看更高级的岗位,要么降低预期。


    在我面试的人里,通过面试的都有这些特点:



    1. 技术扎实:不仅仅基础好,还有深度

    2. 解决过复杂的问题:项目经验里除了完成业务需求,也有做一些有挑战的事


    有些人的简历上只写项目经历不写技能知识点,对此我是反对的,这样做增加了面试官了解你的成本。问项目经历的目的还是想确认你有什么能力,为什么不直接明了的写清楚呢?


    软素质


    这里的「软素质」指面试时考察的、技术以外的点。


    程序员的日常工作里,除了写代码还需要做这些事:



    1. 理解业务的重点和不同需求的核心点,和其他同事协作完成

    2. 从技术角度,对需求提出自己的思考和建议,反馈给其他人

    3. 负责某个具体的业务/方向,成为这个方面所有问题的处理者


    image.png


    因此,面试官或者 HR 还会考察这些点,以确保面试者具备完成以上事情的能力:



    1. 理解能力和沟通表达能力

    2. 业务能力

    3. 稳定性


    第一点是指面试者理解问题和讲清楚答案的能力。遇到过一些面试者,面试的时候过于紧张,讲话都讲不清楚,这种就让人担心“会不会是个社恐”、“工作里该不会也这样说不清楚吧”;还有的人爱抢答,问题都没听明白就开始抢答,让人怀疑是不是性格太急躁太自大;还有的人过于能讲,但讲不到重点,东扯西扯,让人对他的经历和理解能力产生了怀疑。


    第二点是指在实现业务目标的过程中可以提供的能力。 业务发展是需要团队共同努力的,但有的人从来没这么想过,觉得自己上班的任务就是写代码,来什么活干什么活,和外包一样。


    业务发展中可能有各种问题。定方向的领导有时候会过于乐观、跨部门协作项目可能会迟迟推进不动、产品经理有时候也会脑子进水提无用需求、质量保障的测试同学可能会大意漏掉某个细节测试。这个时候,程序员是否能够主动站出来出把力,帮助事情向好的方向发展,就很重要了。


    遇到过一些面试者,在一家公司干了好几年,问起来业务发展情况语焉不详,让人感觉平时只知道写代码;还有的面试者,说起业务问题抱怨指责一大堆,“领导太傻逼”、“产品经理尽提蠢需求”,负能量满满😂。


    第三点是指面试者能不能在一家公司长久干下去。 对于级别越高的人,这点要求就越高,因为他的离开对业务的发展会有直接影响。即使级别不高,频繁换工作也会让人对你有担心:会不会抗压能力很差、会不会一不涨工资就要跑路。一般来说,五年三跳就算是临界线,比这个频繁就算是真的“跳的有点多”。


    针对以上这三点,我们可以这样做:



    1. 面试时调整心态,当作普通交流,就算不会也坦然说出,不必过于紧张

    2. 回答问题时有逻辑条理,可以采用类似总分总的策略

    3. 工作时多关注开发以外的事,多体验公司产品和竞品,在需求评审时不摸鱼、多听听为什么做、思考是否合理、提出自己的想法

    4. 定好自己的职业规划(三年小进步、五年大进步),在每次换工作时都认真问问自己:下一份工作能否帮助自己达到目标


    总结


    好了,这就是我前段时间面试的感悟和吐槽。


    总的来说,今年找工作的人不少,市面上的岗位没有往年那么多。如果你最近要换工作,最好做足准备。做好后面的规划再换、做好准备再投简历、经历整理清楚再面试。


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

    吐槽大会,来瞧瞧资深老前端写的代码

    web
    忍无可忍,不吐不快。 本期不写技术文章,单纯来吐槽下公司项目里的奇葩代码,还都是一些资深老前端写的,希望各位对号入座。 知道了什么是烂代码,才能写出好代码。 别说什么代码和人有一个能跑就行的话,玩笑归玩笑。 人都有菜的时候,写出垃圾代码无可厚非,但是工作几年...
    继续阅读 »

    忍无可忍,不吐不快。



    本期不写技术文章,单纯来吐槽下公司项目里的奇葩代码,还都是一些资深老前端写的,希望各位对号入座。


    知道了什么是烂代码,才能写出好代码。


    别说什么代码和人有一个能跑就行的话,玩笑归玩笑。


    人都有菜的时候,写出垃圾代码无可厚非,但是工作几年了还是写垃圾代码,有点说不过去。


    我认为的垃圾代码,不是你写不出深度优先、广度优先的算法,也不是你写不出发布订阅、策略模式的 if else


    我认为垃圾代码都有一个共性:看了反胃。就是你看了天然的不舒服,看了就头疼的代码,比如排版极丑,没有空格、没有换行、没有缩进。


    优秀的代码就是简洁清晰,不能用策略模式优化 if else 没关系,多几行代码影响不大,你只要清晰,就是好代码。


    有了差代码、好代码的基本标准,那我们也废话不多说,来盘盘这些资深前端的代码,到底差在哪里。


    -------------更新------------


    集中回答一下评论区的问题:


    1、项目是原来某大厂的项目,是当时的外包团队写的,整体的代码是还行的。eslintcommitlint 之类的也是有的,只不过这些都是可以禁用的,作用全靠个人自觉。


    2、后来项目被我司收购,原来的人也转为我司员工,去支撑其他项目了。这个项目代码也就换成我们这批新来的维护了。很多代码是三几年前留下的确实没错,谁年轻的时候没写过烂代码,这是可以理解的。只不过同为三年前的代码,为啥有人写的简单清晰,拆分清楚,有的就这么脏乱差呢?


    3、文中提到的很多代码其实是最近一年写的,写这些功能的前端都是六七年经验的,当时因为项目团队刚组建,没有前端,抽调这些前端过来支援,写的东西真的非常非常不规范,这是让人难以理解的,他们在原来的项目组(核心产品,前端基建和规范都很厉害)也是这么写代码的吗?


    4、关于团队规范,现在的团队是刚成立没多久的,都是些年轻人,领导也不是专业前端,基本属于无前端 leader 的状态,我倒是很想推规范,但是我不在其位,不好去推行这些东西,提了建议,领导是希望你简单写写业务就行(说白了我不是 leader 说啥都没用)。而且我们这些年轻前端大家都默认打开 eslint,自觉性都很高,所以暂时都安于现状,代码 review 做过几次,都没太大的毛病,也就没继续做了。


    5、评论区还有人说想看看我写的代码,我前面文章有,我有几个开源项目,感兴趣可以自己去看。项目上的代码因为涉及公司机密,没法展示。


    6、本文就是篇吐槽,也不针对任何人,要是有人看了不舒服,没错,说的就是你。要是没事,大家看了乐呵乐呵就行。轻喷~


    文件命名千奇百怪


    同一个功能,都是他写的,三个文件夹三种奇葩命名法,最重要的是第三个,有人能知道它这个文件夹是什么功能吗?这完全不知道写的什么玩意儿,每次我来看代码都得重新理一遍逻辑。


    image.png


    组件职责不清


    还是刚才那个组件,这个 components 文件夹应该是放各个组件的,但是他又在底下放一个 RecordDialog.jsx 作为 components 的根组件,那 components 作为文件名有什么意义呢?


    image.png


    条件渲染逻辑置于底层


    这里其实他写了几个渲染函数,根据客户和需求的不同条件性地渲染不同内容,但是判断条件应该放在函数外,和函数调用放在一起,根据不同条件调用不同渲染函数,而不是函数内就写条件,这里就导致逻辑内置,过于分散,不利于维护。违反了我们常说的高内聚、低耦合的编程理念。


    image.png


    滥用、乱用 TS


    项目是三四年前的项目了,主要是类组件,资深前端们是属于内部借调来支援维护的,发现项目连个 TS 都没有,不像话,赶紧把 TS 配上。配上了,写了个 tsx 后缀,然后全是无类型或者 anyscript。甚至于完全忽视 TSESlint 的代码检查,代码里留下一堆红色报错。忍无可忍,我自己加了类型。


    image.png


    留下大量无用注释代码和报错代码


    感受下这个注释代码量,我每次做需求都删,但是几十个工程,真的删不过来。


    image.png


    image.png


    丑陋的、隐患的、无效的、混乱的 css


    丑陋的:没有空格,没有换行,没有缩进


    隐患的:大量覆盖组件库原有样式的 css 代码,不写类名实现 namespace


    无效的:大量注释的 css 代码,各种写了类名不写样式的垃圾类名


    混乱的:从全局、局部、其他组件各种引入 css 文件,导致样式代码过于耦合


    image.png


    一个文件 6 个槽点


    槽点1:代码的空行格式混乱,影响代码阅读


    槽点2:空函数,写了函数名不写函数体,但是还调用了!


    槽点3:函数参数过多不优化


    槽点4:链式调用过长,属性可能存在 undefined 或者 null 的情况,代码容易崩,导致线上白屏


    槽点5:双循环嵌套,影响性能,可以替换为 reduce 处理


    槽点6:参数、变量、函数的命名随意而不语义化,完全不知道有何作用,且不使用小驼峰命名


    image.png


    变态的链式取值和赋值


    都懒得说了,各位观众自己看吧。


    image.png


    代码拆分不合理或者不拆分导致代码行数超标


    能写出这么多行数的代码的绝对是人才。


    尽管说后来维护的人加了一些代码,但是其初始代码最起码也有近 2000 行。


    image.png


    这是某个功能的数据流文件,使用 Dva 维护本身代码量就比 Redux 少很多了,还是能一个文件写出这么多。导致到现在根本不敢拆出去,没人敢动。


    image.png


    杂七杂八的无用 js、md、txt 文件


    在我治理之前,充斥着非常多的无用的、散乱的 md 文档,只有一个函数却没有调用的 js 文件,还有一堆测试用的 html 文件。


    实在受不了干脆建个文件夹放一块,看起来也要舒服多了。


    image.png


    less、scss 混用


    这是最奇葩的。


    image.png


    特殊变量重命名


    这是真大佬,整个项目基建都是他搭的。写的东西也非常牛,bug 很少。但是大佬有一点个人癖好,例如喜欢给 window 重命名为 G。这虽然算不上大缺点,但真心不建议大家这么做,window 是特殊意义变量,还请别重命名。


    const G = window;
    const doc = G.document;

    混乱的 import


    规范的 import 顺序,应该是框架、组件等第三方库优先,其次是内部的组件库、包等,然后是一些工具函数,辅助函数等文件,其次再是样式文件。乱七八糟的引入顺序看着都烦,还有这个奇葩的引入方式,直接去 lib 文件夹下引入组件,也是够奇葩了。


    image.png


    写在最后


    就先吐槽这么多吧,这些都是平时开发过程中容易犯的错误。


    要想保持一个好的编码习惯,写代码的时候就得时刻告诉自己,你的代码后面是会有人来看的,不想被骂就写干净点。


    我觉得什么算法、设计模式都是次要的,代码清晰,数据流向清晰,变量名起好,基本 80% 不会太差。


    写代码就像做人,现实总是千难万苦,各种妥协和无奈,但是这不意味着我们可以无底线的做事。给自己设个底线,不论做人还是做事。


    共勉。


    作者:北岛贰
    来源:juejin.cn/post/7265505732158472249
    收起阅读 »

    博客园又崩了,这个锅要不要阿里云背?

    昨天下午博客园又崩了,不过与其它大厂的崩溃不同,博客园出现崩溃的频率好像有点高。 这是怎么回事呢?和阿里云又有什么关系,这篇文章就带大家来一探究竟。 到底是谁的问题? 昨天下午(2023年12月8日)博客园官方发布了一个故障公告,官网截图如下: 博客园的故障...
    继续阅读 »

    昨天下午博客园又崩了,不过与其它大厂的崩溃不同,博客园出现崩溃的频率好像有点高。


    这是怎么回事呢?和阿里云又有什么关系,这篇文章就带大家来一探究竟。


    到底是谁的问题?


    昨天下午(2023年12月8日)博客园官方发布了一个故障公告,官网截图如下:



    博客园的故障是数据库CPU 100%,今年已经出现了7次,根据我这个不经常上博客园的人的观察,往年也有出现,好像频率没这么高。


    出现了7次都不能解决,这是个什么问题呢?


    根据我的技术经验,数据库CPU百分之百,一般是某些SQL写的质量不佳,在某些情况下可能出现了大数据量全表扫描的情况,迟迟不能执行完毕,长期霸占CPU资源导致的。


    按说这种问题只要定位到对应的SQL,改掉相关语句就可以了,但是就是这个问题把博客园难住了。


    参数嗅探问题?


    看看官方针对此次问题的说明:





    这里有两个重要的信息:博客园的数据库使用的是 SQL Server;博客园的主要查询使用的是存储过程。博客园是.NET技术体系的,使用SQL Server比较顺其自然;使用存储过程可以提高SQL执行的效率,博客园是08年创立的,这在十几年前也比较流行;看它使用的分页方法也是比较新的,这说明它也一直在优化。


    官方怀疑是参数嗅探问题造成 SQL Server 缓存了性能极差的执行计划,这句话中有两个名词:参数嗅探问题和执行计划,没接触过的同学可能会比较蒙,我先给大家普及一下。


    执行计划:每条SQL在数据库内部执行时都会有一个执行计划,主要就是先查询哪张表、表之间怎么关联、执行的时候使用哪些索引,等等。


    参数嗅探问题:存储过程在首次执行时会先进行编译,后续执行的时候都使用这个编译的结果,而不是每次都解释执行,因为编译相对比较耗时。编译时,数据库还会根据当前使用的存储过程参数确定一个最优的执行计划,并把这个执行计划也一并缓存起来,后续再执行的时候就会直接使用这个执行计划。


    问题主要就出现在这个缓存的执行计划,因为对于不同的参数来说,执行计划的效率可能差别很大,这主要是查询数据分布不均匀的问题造成的。


    我在公司的业务中也经常遇到这个问题,有的用户数据多,有的用户数据少,即使我们为用户Id字段设置了索引,数据库有时仍旧会认为不使用这个索引的效率更高,它会自己选择一个自认为更优的查询路径,比如全表扫描,实际执行时就出现了慢SQL的情况。


    到博客园这里,官方认为就是自己的某个存储过程因为参数嗅探问题导致某些慢SQL,慢SQL导致CPU使用过高,最后导致数据库崩溃。


    而官方一直没有定位到出现问题的SQL或者出现问题的存储过程,可能博客园的SQL太多了吧,出现问题的不止一个SQL。又或者是 SQL Server 的问题,或者阿里云的锅?


    SQL Server的问题?


    SQL Server 作为一款商业数据库,能活到现在,而且价格还不低,其产品能力是经过了残酷的市场考验的。虽然任何产品都不可避免的存在一些BUG,但是导致这种问题的BUG应该不会持续这么久。所以 SQL Server 本身的问题应该不大,或者说 SQL Server 的数据查询方式没有问题。


    还有很多同学提到 SQL Server 性能不行,单纯根据我的使用经验来说,类似的场景 SQL Server的查询性能往往比 MySQL 要好不少,其它很多用户也有类似的反馈:



    我也专门找了一些 SQL Server 和其它数据库的性能对比,截图如下:



    文章和数据来源:


    segmentfault.com/q/101000002…


    http://www.ijarcce.com/upload/2015…


    另外我们也可以从博客园分享的数据库的监控日志中略窥一二:



    从图上可以看出,出现问题的时间比较随机,也不是什么高峰期。博客园也提到过凌晨4-5点钟出现类似问题。看这个CPU使用率只有20%多一点,所以并非是遇到了性能瓶颈。



    阿里云的问题?


    阿里云为什么可能背锅?因为博客园部署在阿里云上,服务器和数据库都用的阿里云产品。


    记得之前出现这个问题时,博客园官方对阿里云颇多微词,后来双方可能进行了深入交流,博客园接受了参数嗅探问题,此后就一直在这块查找。


    那么阿里云能不能彻底撇清关系呢?


    正常情况下,阿里云上部署的 SQL Server 应该是从微软购买的,微软应该也要提供一些技术支持,包括安装和日常的运行维护支持。这个 SQL Server 可能和 Azure 上部署的有些差别,但微软也不会砸自己的招牌,数据库版本不应该有大问题。


    阿里云只是部署和运维 SQL Server,说白了阿里云只是搞了底层的存储、网络、操作系统等服务,上层的数据库应用完全是微软的,他插不上手,这种数据库程序的CPU百分百的故障很难和阿里云干的事挂上钩。


    再者阿里云自己也开发数据库,虽然 SQL Server 不开源,但是高手们对于一些底层的设计,或者可能存在问题的地方,应该也是门清的。阿里云上 SQL Server 服务使用者众多,如果很多企业都遇到这个问题,应该也早就爆出来并解决了。


    所以这个问题甩锅到阿里云身上的难度比较大。当然也没办法完全排除,毕竟总有些极端情况,阿里云最近也崩了很多次,会不会在某些方面有些幺蛾子?大家也不知道。


    怎么解决问题?


    换数据库?



    正如上文所说,问题出现在数据库自身上的可能性不大,而且换数据库要重写所有的SQL,还可能要修改表结构,这个工作量不是一星半点。


    如果真的是参数嗅探问题,换了数据库一样存在执行计划效率不一致的问题。


    换云?


    这基本是认为阿里云能力不行。


    如果真的怀疑是这方面的问题,倒是可以试试,不过不是直接迁移过去,而是把数据导出来一份,放到别的公有云上,或者本地部署一套SQL Server。


    然后采集SQL执行日志,在测试的数据库中进行重放执行,如果问题还会发生,那就不是云厂商的问题,如果跑了很久,问题都没有出现过,那才有根据说云服务的问题概率比较大一些。


    当然这个测试的成本比较高,也许可以通过精简样本或者提高SQL执行频率加速一下测试。


    作为技术人,甩锅时一定要有理有据。


    再或者就不讲理,博客园死磕阿里云,要么就是你的问题,要么就是你帮我找出问题来。有时候云厂商的技术团队也是可以上门或者以其他方式进行亲密沟通的。再不行花点钱找个高手呢?可能还是博客园太老实了?或者阿里云太傲慢了?又或者博客园太穷了?


    解决参数嗅探问题


    阿里云的问题只能是猜测,参数嗅探的问题确是能够实实在在抓住的,阿里云的数据库产品是提供了慢SQL日志查询的。


    只需要找出出现问题时的慢SQL,看博客园以往的故障公告也是曾经抓到过一些问题SQL的。


    但是问题为什么还会一直出现呢?


    有可能是问题SQL太多了。经过十几年的迭代,博客园的代码量可能十分庞大,再加上博客园这两年经营比较困难,没有人力和精力投入到这方面,只能问题出现了再去反查,然后改正。能活着就不错了,估计团队内部也没有技术牛人,精力都放到了活下来的事情上。


    具体为什么一直解决不了,咱们就说到这里。


    下面给大家聊聊怎么解决参数嗅探的问题,我想这个对于搞技术的同学来说才是最重要的.


    上面我们已经说过参数嗅探问题就是数据库使用了效率不高的执行计划,那么解决这个问题的核心思路就是让数据库不去使用这些低效计划。这里分享一些我了解的方法。


    暴力清理


    重启服务器、重启数据库,博客园采用的处理方法差不多都是这个。



    还有一个稍微优雅点的方案,清除所有的执行计划缓存:DBCC FREEPROCCACHE,不管这些执行计划是不是有问题。但是不确定这个指令能不能在阿里云的数据库服务上执行。


    这些都是强制重新创建执行计划的方法,坏处就是影响都比较大,很可能会影响用户使用服务,比较暴力。


    而且这些方法不能治本,只能短时间的缓解一下,说不定在某个时刻,执行计划又被重建了,或者SQL执行又超时了。


    优雅机制


    SQL Server本身也有一些优雅的方案来缓解这个问题。比如:



    • 不缓存执行计划,虽然缓存能带来一些效率上的提升,但相比参数嗅探问题带来的性能损失就是小巫见大巫了。可以在存储过程中使用WITH RECOMPILE,让查询每次都重新编译。

    • 强制使用某个查询计划,比如强制使用某个索引,这个索引对于所有的查询都不会太差;SQL Server中还可以强制使用某个条件的查询计划。不过找到这个索引或者条件的难度可能比较大,因为数据一直在变化,现在是好的并不代表一直好。

    • 只清除特定语句或存储过程的查询缓存,使用 DBCC FREEPROCCACHE(@plan_id) 指定执行计划,这样影响更小。

    • 另外表统计信息陈旧、索引碎片、缺少索引都可能导致参数嗅探问题,遇到问题时可以从这几个方面调查一下。


    详情可参考阿里的这篇文章: mysql.taobao.org/monthly/201…


    谨慎评估


    在我们设计表、编写SQL的时候,需要考虑数据会如何分布,查询有哪些条件,特别是数据可能分布不均匀的情况。


    比如有的用户的数据量可能是大部分用户的10倍、甚至百倍,排序的字段可能导致不使用包含条件字段的索引,查询可能在多个索引之间飘移。


    如果可能存在问题,就要考虑表如何设计、数据如何查询,普通关系数据库难以解决时,我们还可以考虑采用NoSQL、分布式数据库等方案,以稳定查询效率。




    以上就是本文的主要内容了,因本人才疏学浅,不免存在错漏,如有问题还望指正。


    关注微/信/公/众/号:萤火架构,技术提升不迷路。


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

    【Java集合】双列集合HashMap的概念、特点及使用

    HashMap是Java中的一个集合类,它实现了Map接口,提供了一种存储键值对的方式。你可以把它想象成一个没有固定大小和形状的储物柜,你可以随时往里面放东西,也可以随时取出东西。而且,这个储物柜还有一个神奇的功能,那就是无论你放进去的是什么,取出来的总是你放...
    继续阅读 »

    HashMap是Java中的一个集合类,它实现了Map接口,提供了一种存储键值对的方式。你可以把它想象成一个没有固定大小和形状的储物柜,你可以随时往里面放东西,也可以随时取出东西。而且,这个储物柜还有一个神奇的功能,那就是无论你放进去的是什么,取出来的总是你放进去的那个。

    上篇文章讲了Map接口的概念,以及Map接口中的常用方法和对Map集合的遍历,本篇文章我们将继续介绍另一个十分重要的双列集合—HashMap。


    HashMap 概念

    HashMap集合是Map接口的一个实现类,它用于存储键值映射关系,该集合的键和值允许为空,但键不能重复,且集合中的元素是无序的。

    特点

    HashMap底层是由哈希表结构组成的,其实就是“数组+链表”的组合体,数组是HashMap的主体结构,链表则主要是为了解决哈希值冲突而存在的分支结构。正因为这样特殊的存储结构,HashMap集合对于元素的增、删、改、查操作表现出的效率都比较高。

    结构

    在java1.8以后采用数组+链表+红黑树的形势来进行存储,通过散列映射来存储键值对,如下图:

    Description

    • 在初始化时将会给定默认容量为16

    • 对key的hashcode进行一个取模操作得到数组下标

    • 数组存储的是一个单链表

    • 数组下标相同将会被放在同一个链表中进行存储

    • 元素是无序排列的

    • 链表超过一定长度(TREEIFY_THRESHOLD=8)会转化为红黑树

    • 红黑树在满足一定条件会再次退回链表

    看到这个图,是不是挺熟悉!没错,这个就是我们在讲Set时,它的内存结构图,当时我们说 HashSet的底层就是 Map集合,只不过Set只使用了Map集合中的Key,没有使用Value而已。

    小练习

    在之前我们已经讲了不少Map的使用方法,本篇中就不做过多解释了,来上了个小练习,在体会下它的使用。

    每位学生(姓名,年龄)都有自己的家庭住址。那么,既然有对应关系,则将学生对象和家庭住址存储到map集合中。学生作为键, 家庭住址作为值。

    注意,学生姓名相同并且年龄相同视为同一名学生。

    编写学生类:

        public class Student {
    private String name;
    private int age;

    public Student() {
    }

    public Student(String name, int age) {
    this.name = name;
    this.age = age;
    }

    public String getName() {
    return name;
    }

    public void setName(String name) {
    this.name = name;
    }

    public int getAge() {
    return age;
    }

    public void setAge(int age) {
    this.age = age;
    }

    @Override
    public boolean equals(Object o) {
    if (this == o)
    return true;
    if (o == null || getClass() != o.getClass())
    return false;
    Student student = (Student) o;
    return age == student.age && Objects.equals(name, student.name);
    }

    @Override
    public int hashCode() {
    return Objects.hash(name, age);
    }
    }

    编写测试类:

        public class HashMapTest {
    public static void main(String[] args) {
    //1,创建Hashmap集合对象。
    Map<Student,String>map = new HashMap<Student,String>();
    //2,添加元素。
    map.put(newStudent("lisi",28), "上海");
    map.put(newStudent("wangwu",22), "北京");
    map.put(newStudent("zhaoliu",24), "成都");
    map.put(newStudent("zhouqi",25), "广州");
    map.put(newStudent("wangwu",22), "南京");

    //3,取出元素。键找值方式
    Set<Student>keySet = map.keySet();
    for(Student key: keySet){
    Stringvalue = map.get(key);
    System.out.println(key.toString()+"....."+value);
    }
    }
    }
    • 当给HashMap中存放自定义对象时,如果自定义对象作为key存在,这时要保证对象唯一,必须复写对象的hashCode和equals方法(如果忘记,请回顾HashSet存放自定义对象)。

    • 如果要保证map中存放的key和取出的顺序一致,可以使用java.util.LinkedHashMap集合来存放。

    你还在苦恼找不到真正免费的编程学习平台吗?可以试试云端源想!课程视频、在线书籍、在线编程、实验场景模拟、一对一咨询……你想要的全部学习资源这里都有,重点是统统免费!点这里即可查看

    LinkedHashMap

    我们知道HashMap保证成对元素唯一,并且查询速度很快,可是成对元素存放进去是没有顺序的,那么我们要保证有序,还要速度快怎么办呢?

    在HashMap下面有一个子类LinkedHashMap,它继承自HashMap。特别的是,LinkedHashMap在HashMap的基础上维护了一个双向链表,可以按照插入顺序或者访问顺序来迭代元素。此外,LinkedHashMap结合了HashMap的数据操作和LinkedList的插入顺序维护的特性,因此也可以被看做是HashMap与LinkedList的结合。它是链表和哈希表组合的一个数据存储结构。把上个练习使用LinkedHashMap的使用一下

        publicclass LinkedHashMapDemo {
    publicstaticvoid main(String[] args) {

    //Map<String, String> map = new HashMap<String, String>();

    LinkedHashMap<String, String> map = new LinkedHashMap<String, String>();
    map.put("马云", "阿里巴巴");
    map.put("马化腾", "腾讯");
    map.put("李彦宏", "百度");
    Set<Entry<String, String>> entrySet = map.entrySet();
    for (Entry<String, String> entry : entrySet) {
    System.out.println(entry.getKey() + " " + entry.getValue());
    }
    }
    }

    总结

    总的来说,HashMap是Java中的一个强大工具,它可以帮助我们高效地处理大量的数据。但是,我们也需要注意,虽然HashMap的性能很高,但如果不正确地使用它,可能会导致内存泄漏或者数据丢失的问题。因此,我们需要正确地理解和使用HashMap,才能充分发挥它的强大功能。

    本系列文章写到这里,为大家介绍集合家族的知识,基本上就可以告一段落了。

    在这个系列文章中,我们讲述了单列和双列集合的家族体系以及简单的使用。集合中不少的实现类,我们并未讲述,大家下来可以通过java的API文档,去学习使用。还是那句话,熟能生巧!只看不练,假把式!

    本系列以上内容,都是在实际项目中,会经常碰到这些概念的使用,当然了,文中的内容可能也不是尽善尽美的,如有错误,可以私信,探讨!
    happy ending!

    收起阅读 »