注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

Android 通知文本颜色获取

前言 Android Notification 几乎每个版本都有改动,因此有很多兼容性问题,摆在开发者面前的难题是每个版本的展示效果不同,再加app保活能力的日渐式微和google大力推进WorkManager、JobScheduler、前台进程的行为,即便A...
继续阅读 »

前言


Android Notification 几乎每个版本都有改动,因此有很多兼容性问题,摆在开发者面前的难题是每个版本的展示效果不同,再加app保活能力的日渐式微和google大力推进WorkManager、JobScheduler、前台进程的行为,即便AlarmManager#setAlarmClock这种可以解除Dozen模式的超级工具,也无法对抗进程死亡的问题,通知到达率和及时性的效果已经大幅减弱。


Screenshot_20190403-131551.png


自定义通知是否仍有必要?

实际上,目前大多推送通知都被系统厂商代理展示了,导致实现效果雷同且没有新意。众多一样的效果,站在用户角度也产生了很多厌恶情绪,对用户的吸引点也是逐渐减弱,这其实和自定义通知的初衷是相背离的,因为自定义通知首要解决的是特色功能的展示,而通用通知却很难做到这一点。因此,在一些app中,自定义通知仍然是有必要的,但必要性没有那么强了。


当前的使用场景:



  • 前台进程常驻类型app,比如直播、音乐类等

  • 类似QQ的app浮动弹窗提醒 (这类不算通知,但是可以使用统一的方法适配)

  • 系统白名单中的app


现状


通知首要解决的是功能问题,其次是主题问题。当前,大部分app已经习惯使用系统通知栏而不使用自定义的通知,主要原因是适配难度问题。


对于自定义通知的适配,目前有两条路线:



  • 统一样式:

    是定义一套深色模式和浅色模式都能通用的色彩搭配,一些音视频app也是这么做的,巧妙的避免了因系统主题不一致造成的现实效果不同的问题,但仍然在部分手机上展示的比较突兀。

  • 读取系统通知颜色进行适配:

    遗憾的是,在Android 7.0之后,正常的通知是拿不到notification.contentView,但似乎没有看到相关的文章来解决这个问题。


两种方案可以搭配使用,但方案二目前存在无法提取颜色的问题,关键是怎么解决contentView拿不到的问题呢?接下来我们重点解决方案二的这个问题。


问题点


我们在无论使用NotificationBuilder或者NotificationCompatBuilder,其内部的build方法存在targetSdkVersion的判断,而在大于Android 7.0 的版本中,不会立即创建ContentView


protected Notification buildInternal() {
if (Build.VERSION.SDK_INT >= 26) {
return mBuilder.build();
} else if (Build.VERSION.SDK_INT >= 24) {
Notification notification = mBuilder.build();

if (mGr0upAlertBehavior != GR0UP_ALERT_ALL) {
// if is summary and only children should alert
if (notification.getGr0up() != null
&& (notification.flags & FLAG_GR0UP_SUMMARY) != 0
&& mGr0upAlertBehavior == GR0UP_ALERT_CHILDREN) {
removeSoundAndVibration(notification);
}
// if is group child and only summary should alert
if (notification.getGr0up() != null
&& (notification.flags & FLAG_GR0UP_SUMMARY) == 0
&& mGr0upAlertBehavior == GR0UP_ALERT_SUMMARY) {
removeSoundAndVibration(notification);
}
}

return notification;
} else if (Build.VERSION.SDK_INT >= 21) {
mBuilder.setExtras(mExtras);
Notification notification = mBuilder.build();
if (mContentView != null) {
notification.contentView = mContentView;
}
if (mBigContentView != null) {
notification.bigContentView = mBigContentView;
}
if (mHeadsUpContentView != null) {
notification.headsUpContentView = mHeadsUpContentView;
}

if (mGr0upAlertBehavior != GR0UP_ALERT_ALL) {
// if is summary and only children should alert
if (notification.getGr0up() != null
&& (notification.flags & FLAG_GR0UP_SUMMARY) != 0
&& mGr0upAlertBehavior == GR0UP_ALERT_CHILDREN) {
removeSoundAndVibration(notification);
}
// if is group child and only summary should alert
if (notification.getGr0up() != null
&& (notification.flags & FLAG_GR0UP_SUMMARY) == 0
&& mGr0upAlertBehavior == GR0UP_ALERT_SUMMARY) {
removeSoundAndVibration(notification);
}
}
return notification;
} else if (Build.VERSION.SDK_INT >= 20) {
mBuilder.setExtras(mExtras);
Notification notification = mBuilder.build();
if (mContentView != null) {
notification.contentView = mContentView;
}
if (mBigContentView != null) {
notification.bigContentView = mBigContentView;
}

if (mGr0upAlertBehavior != GR0UP_ALERT_ALL) {
// if is summary and only children should alert
if (notification.getGr0up() != null
&& (notification.flags & FLAG_GR0UP_SUMMARY) != 0
&& mGr0upAlertBehavior == GR0UP_ALERT_CHILDREN) {
removeSoundAndVibration(notification);
}
// if is group child and only summary should alert
if (notification.getGr0up() != null
&& (notification.flags & FLAG_GR0UP_SUMMARY) == 0
&& mGr0upAlertBehavior == GR0UP_ALERT_SUMMARY) {
removeSoundAndVibration(notification);
}
}

return notification;
} else if (Build.VERSION.SDK_INT >= 19) {
SparseArray<Bundle> actionExtrasMap =
NotificationCompatJellybean.buildActionExtrasMap(mActionExtrasList);
if (actionExtrasMap != null) {
// Add the action extras sparse array if any action was added with extras.
mExtras.putSparseParcelableArray(
NotificationCompatExtras.EXTRA_ACTION_EXTRAS, actionExtrasMap);
}
mBuilder.setExtras(mExtras);
Notification notification = mBuilder.build();
if (mContentView != null) {
notification.contentView = mContentView;
}
if (mBigContentView != null) {
notification.bigContentView = mBigContentView;
}
return notification;
} else if (Build.VERSION.SDK_INT >= 16) {
Notification notification = mBuilder.build();
// Merge in developer provided extras, but let the values already set
// for keys take precedence.
Bundle extras = NotificationCompat.getExtras(notification);
Bundle mergeBundle = new Bundle(mExtras);
for (String key : mExtras.keySet()) {
if (extras.containsKey(key)) {
mergeBundle.remove(key);
}
}
extras.putAll(mergeBundle);
SparseArray<Bundle> actionExtrasMap =
NotificationCompatJellybean.buildActionExtrasMap(mActionExtrasList);
if (actionExtrasMap != null) {
// Add the action extras sparse array if any action was added with extras.
NotificationCompat.getExtras(notification).putSparseParcelableArray(
NotificationCompatExtras.EXTRA_ACTION_EXTRAS, actionExtrasMap);
}
if (mContentView != null) {
notification.contentView = mContentView;
}
if (mBigContentView != null) {
notification.bigContentView = mBigContentView;
}
return notification;
} else {
return mBuilder.getNotification();
}
}

那么我们怎么解决这个问题呢?


Context Wrapper


在App 开发中,Context Wrapper是常见的事情,比如用在预加载Layout、模拟Service运行、插件加载等方面有大量使用。


本文思路是要hack targetSdkVersion,但targetSdkVersion是保存在ApplicationInfo中的,不过没关系,它是通过Context获取的,因此我们在它获取前将其修改为android 5.0的不就行了?


为什么可以修改ApplicationInfo,因为其事Parcelable的子类,看到Parcleable的子类你就能明白,该类的修改是不会触发系统服务的调度,但会影响部分功能,安全起见,我们可以拷贝一下。


public class NotificationContext extends ContextWrapper {
private Context mContextBase;
private ApplicationInfo mApplicationInfo;
private NotificationContext(Context base) {
super(base);
this.mContextBase = base;
}

@Override
public ApplicationInfo getApplicationInfo() {
if(mApplicationInfo!=null) return mApplicationInfo;
ApplicationInfo applicationInfo = super.getApplicationInfo();
mApplicationInfo = new ApplicationInfo(applicationInfo);
return mApplicationInfo;
}

public static NotificationContext from(Context context) {
return new NotificationContext(context);
}
}

targetSdkVersion hack


下一步,修改targetSdkVersion 为android 5.0版本


NotificationContext notificationContext = NotificationContext.from(context);
ApplicationInfo applicationInfo = notificationContext.getApplicationInfo();
int targetSdkVersion = applicationInfo.targetSdkVersion;

applicationInfo.targetSdkVersion = Math.min(21, targetSdkVersion);

完整的代码


要获取的属性


class NotificationResourceInfo {
String titleResourceName;
int titleColor;
float titleTextSize;
ViewGr0up.LayoutParams titleLayoutParams;
String descResourceName;
int descColor;
float descTextSize;
ViewGr0up.LayoutParams descLayoutParams;
long updateTime;

}

获取颜色,用于判断是不是深色模式,这里其实利用的是标记查找方法,先给标题和内容设置Text,然后查找具备此Text的TextView


private static String TITLE_TEXT = "APP_TITLE_TEXT";
private static String CONTENT_TEXT = "APP_CONTENT_TEXT";

下面是核心查找逻辑


  //遍历布局找到字体最大的两个textView,视其为主副标题
private <T extends View> T findView(ViewGr0up viewGr0upSource, CharSequence locatorTextId) {

Queue<ViewGr0up> queue = new ArrayDeque<>();
queue.add(viewGr0upSource);
while (!queue.isEmpty()) {
ViewGr0up parentGr0up = queue.poll();
if (parentGr0up == null) {
continue;
}
int childViewCount = parentGr0up.getChildCount();
for (int num = 0; num < childViewCount; ++num) {
View childView = parentGr0up.getChildAt(num);
String resourceIdName = getResourceIdName(childView.getContext(), childView.getId());
Log.d("NotificationManager", "--" + resourceIdName);
if (TextUtils.equals(resourceIdName, locatorTextId)) {
Log.d("NotificationManager", "findView");
return (T) childView;
}
if (childView instanceof ViewGr0up) {
queue.add((ViewGr0up) childView);
}

}
}
return null;

}

NotificationThemeHelper 实现


public class NotificationThemeHelper {
private static String TITLE_TEXT = "APP_TITLE_TEXT";
private static String CONTENT_TEXT = "APP_CONTENT_TEXT";

final static String TAG = "NotificationThemeHelper";
static SoftReference<NotificationResourceInfo> notificationInfoReference = null;
private static final String CHANNEL_NOTIFICATION_ID = "CHANNEL_NOTIFICATION_ID";

public NotificationResourceInfo parseNotificationInfo(Context context) {
String channelId = createNotificationChannel(context, CHANNEL_NOTIFICATION_ID, CHANNEL_NOTIFICATION_ID);
NotificationResourceInfo notificationInfo = null;
NotificationContext notificationContext = NotificationContext.from(context);
ApplicationInfo applicationInfo = notificationContext.getApplicationInfo();
int targetSdkVersion = applicationInfo.targetSdkVersion;

try {
applicationInfo.targetSdkVersion = Math.min(21, targetSdkVersion);
//更改版本号,这样可以让builder自行创建contentview
NotificationCompat.Builder builder = new NotificationCompat.Builder(notificationContext, channelId);
builder.setContentTitle(TITLE_TEXT);
builder.setContentText(CONTENT_TEXT);
int icon = context.getApplicationInfo().icon;
builder.setSmallIcon(icon);
Notification notification = builder.build();
if (notification.contentView == null) {
return null;
}
int layoutId = notification.contentView.getLayoutId();
ViewGr0up root = (ViewGr0up) LayoutInflater.from(context).inflate(layoutId, null);
notificationInfo = getNotificationInfo(notificationContext, root);

} catch (Exception e) {
Log.d(TAG, "更新失败");
} finally {
applicationInfo.targetSdkVersion = targetSdkVersion;
}
return notificationInfo;
}

private NotificationResourceInfo getNotificationInfo(Context Context, ViewGr0up root) {
NotificationResourceInfo resourceInfo = new NotificationResourceInfo();

root.measure(0,0);
root.layout(0,0,root.getMeasuredWidth(),root.getMeasuredHeight());

Log.i(TAG,"bitmap ok");

TextView titleTextView = (TextView) root.findViewById(android.R.id.title);
if (titleTextView == null) {
titleTextView = findView(root, "android:id/title");
}
if (titleTextView != null) {
resourceInfo.titleColor = titleTextView.getCurrentTextColor();
resourceInfo.titleResourceName = getResourceIdName(Context, titleTextView.getId());
resourceInfo.titleTextSize = titleTextView.getTextSize();
resourceInfo.titleLayoutParams = titleTextView.getLayoutParams();
}

TextView contentTextView = findView(root, "android:id/text");
if (contentTextView != null) {
resourceInfo.descColor = contentTextView.getCurrentTextColor();
resourceInfo.descResourceName = getResourceIdName(Context, contentTextView.getId());
resourceInfo.descTextSize = contentTextView.getTextSize();
resourceInfo.descLayoutParams = contentTextView.getLayoutParams();
}
return resourceInfo;
}

//遍历布局找到字体最大的两个textView,视其为主副标题
private <T extends View> T findView(ViewGr0up viewGr0upSource, CharSequence locatorTextId) {

Queue<ViewGr0up> queue = new ArrayDeque<>();
queue.add(viewGr0upSource);
while (!queue.isEmpty()) {
ViewGr0up parentGr0up = queue.poll();
if (parentGr0up == null) {
continue;
}
int childViewCount = parentGr0up.getChildCount();
for (int num = 0; num < childViewCount; ++num) {
View childView = parentGr0up.getChildAt(num);
String resourceIdName = getResourceIdName(childView.getContext(), childView.getId());
Log.d("NotificationManager", "--" + resourceIdName);
if (TextUtils.equals(resourceIdName, locatorTextId)) {
Log.d("NotificationManager", "findView");
return (T) childView;
}
if (childView instanceof ViewGr0up) {
queue.add((ViewGr0up) childView);
}

}
}
return null;

}

public boolean isDarkNotificationTheme(Context context) {
NotificationResourceInfo notificationInfo = getNotificationInfoFromReference();
if (notificationInfo == null) {
notificationInfo = parseNotificationInfo(context);
saveNotificationInfoToReference(notificationInfo);
}
if (notificationInfo == null) {
return isLightColor(Color.TRANSPARENT);
}
return !isLightColor(notificationInfo.titleColor);
}

private void saveNotificationInfoToReference(NotificationResourceInfo notificationInfo) {
if (notificationInfoReference != null) {
notificationInfoReference.clear();
}

if (notificationInfo == null) return;
notificationInfo.updateTime = SystemClock.elapsedRealtime();
notificationInfoReference = new SoftReference<NotificationResourceInfo>(notificationInfo);
}

private boolean isLightColor(int color) {
int simpleColor = color | 0xff000000;
int baseRed = Color.red(simpleColor);
int baseGreen = Color.green(simpleColor);
int baseBlue = Color.blue(simpleColor);
double value = (baseRed * 0.299 + baseGreen * 0.587 + baseBlue * 0.114);
if (value < 192.0) {
Log.d("ColorInfo", "亮色");
return true;
}
Log.d("ColorInfo", "深色");
return false;
}

public NotificationResourceInfo getNotificationInfoFromReference() {
if (notificationInfoReference == null) {
return null;
}
NotificationResourceInfo resourceInfo = notificationInfoReference.get();
if (resourceInfo == null) {
return null;
}
long dx = SystemClock.elapsedRealtime() - resourceInfo.updateTime;
if (dx > 10 * 1000) {
return null;
}
return resourceInfo;
}

public static String getResourceIdName(Context context, int id) {

Resources r = context.getResources();
StringBuilder out = new StringBuilder();
if (id > 0 && resourceHasPackage(id) && r != null) {
try {
String pkgName;
switch (id & 0xff000000) {
case 0x7f000000:
pkgName = "app";
break;
case 0x01000000:
pkgName = "android";
break;
default:
pkgName = r.getResourcePackageName(id);
break;
}
String typeName = r.getResourceTypeName(id);
String entryName = r.getResourceEntryName(id);
out.append(pkgName);
out.append(":");
out.append(typeName);
out.append("/");
out.append(entryName);
} catch (Resources.NotFoundException e) {
}
}
return out.toString();
}

private String createNotificationChannel (Context context,String channelID, String channelNAME){
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
NotificationManager manager = (NotificationManager)context. getSystemService(NOTIFICATION_SERVICE);
NotificationChannel channel = new NotificationChannel(channelID, channelNAME, NotificationManager.IMPORTANCE_LOW);
manager.createNotificationChannel(channel);
return channelID;
} else {
return null;
}
}
public static boolean resourceHasPackage(int resid) {
return (resid >>> 24) != 0;
}
}

深浅色判断其实有两种方法,第一种是305911公式,第二种是相似度。


下面是305911公式的,其实就是利用视频亮度算法YUV中的Y分量计算,Y分量表示明亮度。


private boolean isLightColor(int color) {
int simpleColor = color | 0xff000000;
int baseRed = Color.red(simpleColor);
int baseGreen = Color.green(simpleColor);
int baseBlue = Color.blue(simpleColor);
double value = (baseRed * 0.299 + baseGreen * 0.587 + baseBlue * 0.114);
if (value < 192.0) {
Log.d("ColorInfo", "亮色");
return true;
}
Log.d("ColorInfo", "深色");
return false;
}

第二种是相似度算法,一般用于检索相似照片,一般用于优化汉明距离算法,不过这里可以用来判断是否接近黑色。
blog.csdn.net/zz_dd_yy/ar…


private boolean isSimilarColor(int colorL, int colorR) {
int red = Color.red(colorL);
int green = Color.green(colorL);
int blue = Color.blue(colorL);

int red2 = Color.red(colorR);
int green2 = Color.green(colorR);
int blue2 = Color.blue(colorR);

float vertor = red * red2 + green * green2 + blue * blue2;
// 向量1的模
double vectorMold1 = Math.sqrt(Math.pow(red, 2) + Math.pow(green, 2) + Math.pow(blue, 2));
// 向量2的模
double vectorMold2 = Math.sqrt(Math.pow(red2, 2) + Math.pow(green2, 2) + Math.pow(blue2, 2));

// 向量的夹角[0, PI],当夹角为锐角时,cosθ>0;当夹角为钝角时,cosθ<0
float cosAngle = (float) (vertor / (vectorMold1 * vectorMold2));
float radian = (float) Math.acos(cosAngle);

float degrees = (float) Math.toDegrees(radian);
if(degrees>= 0 && degrees < 30) {
return true;
}
return false;
}


用法


这种适配其实无法拿到背景色,只能拿到文字的颜色,如果文字偏亮则背景必须的是深色,反之区亮色,那么核心方法是下面的实现


public boolean isDarkNotificationTheme(Context context) {
NotificationResourceInfo notificationInfo = getNotificationInfoFromReference();
if (notificationInfo == null) {
notificationInfo = parseNotificationInfo(context);
saveNotificationInfoToReference(notificationInfo);
}
if (notificationInfo == null) {
return isLightColor(Color.TRANSPARENT);
}
return !isLightColor(notificationInfo.titleColor);
}

遗留问题


正常情况下,只能取深色和暗色,但是如果存在系统UI Mode的变化时,已经展示出来的通知,显然适配颜色无法动态变化,这也是无法避免的,解决办法是删除通知后重新发送。


总结


本篇到这里就结束了,说实在的,Android的通知的重要性大不如从前,但是必要的适配还是需要的。


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

PageHelper引发的“幽灵数据”,怎么回事?

前言 最近测试反馈一个问题,某个查询全量信息的接口,有时候返回全量数据,符合预期,但是偶尔又只返回1条数据,简直就是“见鬼”了,究竟是为什么出现这样的“幽灵数据”呢? 大胆猜测 首先我们看了下这对代码的业务逻辑,非常的简单,总共没有几行代码,也没有分页逻辑,代...
继续阅读 »

前言


最近测试反馈一个问题,某个查询全量信息的接口,有时候返回全量数据,符合预期,但是偶尔又只返回1条数据,简直就是“见鬼”了,究竟是为什么出现这样的“幽灵数据”呢?


大胆猜测


首先我们看了下这对代码的业务逻辑,非常的简单,总共没有几行代码,也没有分页逻辑,代码如下:


public  List<SdSubscription> findAll() {
return sdSubscriptionMapper.selectAll();
}

那么究竟是咋回事呢?讲道理不可能出现这种情况的啊,不要慌,我们加点日志,将日志级别调整为DEBUG,让日志飞一段时间。


public  List<SdSubscription> findAll() {
log.info("find the sub start .....");
List<SdSubscription> subs = sdSubscriptionMapper.selectAll();
log.info("find the sub end .....");
return subs;
}

果不其然,日志中出现了奇奇怪怪的分页参数,如下图所示:



果然是PageHelper这个开源框架搞的鬼,我想大家都用过吧,分页非常方便,那么究竟为什么别人都没问题,单单就我会出现问题呢?


PageHelper工作原理


为了回答上面的疑问,我们先看看PageHelper框架的工作原理吧。


PageHelper 是一个开源的 MyBatis 分页插件,它可以帮助开发者在查询数据时,快速的实现分页功能。


PageHelper 的工作原理可以简单概括为以下几个步骤:



  1. 在需要进行分页的查询方法前,调用 PageHelper 的静态方法 startPage(),设置当前页码和每页显示的记录数。它会将分页信息放到线程的ThreadLocal中,那么在线程的任何地方都可以访问了。

  2. 当查询方法执行时,PageHelper 会自动拦截查询语句,如果发现线程的ThreadLocal中有分页信息,那么就会在其前后添加分页语句,例如 MySQL 中的 LIMIT 语句。

  3. 查询结果将被包装在 Page 对象中返回,该对象包含分页信息和查询结果列表。

  4. 在查询方法执行完毕后,会在finally中清除线程ThreadLocal中的分页信息,避免分页设置对其他查询方法的影响。


PageHelper 的实现原理主要依赖于拦截器技术和反射机制,通过拦截查询语句并动态生成分页语句,实现了简单、高效、通用的分页功能。具体源码在下图的类中,非常容易看懂。



明白了PageHelper的工作原理后,反复检查代码,都没有调用过startPagedebug查看ThreadLocal中也没有分页信息啊,懵逼中。那我看看别人写的添加分页参数的代码吧,不看不知道,一看吓一跳。



原来有位“可爱”的同事竟然在查询后,加了一个分页,就是把分页信息放到线程的ThreadLocal中。


那大家是不是有疑问,丁是丁,矛是矛,你的线程关我何事?这就要说到我们的tomcat了。


Tomcat请求流程


其实这就涉及到我们的tomcat相关知识了,我们一个浏览器发一个接口请求,经过我们的tomcat的,究竟是一个什么样的流程呢?



  1. 客户端发送HTTP请求到Tomcat服务器。

  2. TomcatHTTP连接器(Connector)接收到请求,将连接请求交给线程池Executor处理,解析它,然后将请求转发给对应的Web应用程序。

  3. Tomcat的Web应用程序容器(Container)接收到请求,根据请求的URL找到对应的Servlet


关于tomcat中使用线程池提交浏览器的连接请求的源码如下:



从而得知,你的连接请求是从线程池从拿的,而拿到的这个线程恰好是一个“脏线程”,在ThreadLocal中放了分页信息,导致你这边出现问题。


总结


后来追问了同事具体原因,才发现是粗心导致的。有些bug总是出现的莫名其妙,就像生活一样。所以关键的是我们在使用一些开源框架的时候一定要掌握底层实现的原理、核心的机制,这样才能够在解决一些问题的时候有据可循。



欢迎关注个人公众号【JAVA旭阳】交流学习!



作者:JAVA旭阳
来源:juejin.cn/post/7223590232730370108
收起阅读 »

该死,这次一定要弄懂什么是时间复杂度和空间复杂度!

开始首先,相信大家在看一些技术文章或者刷算法题的时候,总是能看到要求某某某程序(算法)的时间复杂度为O(n)或者O(1)等字样,就像这样: Q:那么到底这个O(n)、O(1)是什么意思呢?A:时间复杂度和空间复杂度其实是对算法执行期间的性能进行衡量的...
继续阅读 »

开始

首先,相信大家在看一些技术文章或者刷算法题的时候,总是能看到要求某某某程序(算法)的时间复杂度为O(n)或者O(1)等字样,就像这样:

image.png Q:那么到底这个O(n)、O(1)是什么意思呢?

A:时间复杂度空间复杂度其实是对算法执行期间的性能进行衡量的依据。

Talk is cheap, show me the code!

下面从代码入手,来直观的理解一下这两个概念:

时间复杂度

先来看看copilot如何解释的

image.png

  • 举个🌰
function fn (arr) {
let length = arr.length
for (let i = 0; i < length; i++) {
console.log(arr[i])
}
}

首先来分析一下这段代码,这是一个函数,接收一个数组,然后对这个数组进行了一个遍历

  1. 第一段代码,在函数执行的时候,这段代码只会被执行1次,这里记为 1 次
let length = arr.length
  1. 循环体中的代码,循环多少次就会执行多少次,这里记为 n 次
console.log(arr[i])
  1. 循环条件部分,首先是 let i = 0,只会执行一次,记为 1 次
  2. 然后是i < length这个判断,想要退出循环,这里最后肯定要比循环次数多判断一次,所以记为 n + 1 次
  3. 最后是 i++,会执行 n 次

我们把总的执行次数记为T(n)

T(n) = 1 + n + 1 (n + 1) + n = 3n + 3
  • 再来一个🌰
// arr 是一个二维数组
function fn2(arr) {
let lenOne = arr.length
for(let i = 0; i < lenOne; i++) {
let lenTwo = arr[i].length
for(let j = 0; j < lenTwo; j++) {
console.log(arr[i][j])
}
}
}

来分析一下这段代码,这是一个针对二维数组进行遍历的操作,我们再来分析一下这段代码的执行次数

  1. 第一行赋值代码,只会执行1次
let lenOne = arr.length
  1. 第一层循环,let i = 0 1次,i < lenOne n + 1 次,i++ n 次,let len_two = arr[i].length n 次
  2. 第二层循环,let j = 0 n 次,j < lenTwo n * (n + 1) 次,j++ n * n 次
  3. console n*n 次
T(n) = 1 + n + 1 + n + n + n + n * (n + 1) + n * n + n * n = 3n^2 + 5n + 3

代码的执行次数,可以反映出代码的执行时间。但是如果每次我们都逐行去计算 T(n),事情会变得非常麻烦。算法的时间复杂度,它反映的不是算法的逻辑代码到底被执行了多少次,而是随着输入规模的增大,算法对应的执行总次数的一个变化趋势。我们可以尝试对 T(n) 做如下处理:

  • 若 T(n) 是常数,那么无脑简化为1
  • 若 T(n) 是多项式,比如 3n^2 + 5n + 3,我们只保留次数最高那一项,并且将其常数系数无脑改为1。

那么上面两个算法的时间复杂度可以简化为:

T(n) = 3n + 3
O(n) = n

T(n) = 3n^2 + 5n + 3
O(n) = n^2

实际推算时间复杂度时不用这么麻烦,像上面的两个函数,第一个是规模为n的数组的遍历,循环会执行n次,所以对应的时间幅度是O(n),第二个函数是 n*n的二维数组的遍历,对应的时间复杂度就是O(n^2) 依次类推,规模为n*m的二维数组的遍历,时间复杂度就是O(n*m)

常见的时间复杂度按照从小到大的顺序排列,有以下几种:

常数时间对数时间线性时间线性对数时间二次时间三次时间指数时间
O(1)O(logn)O(n)O(nlogn)O(n^2)O(n^3)O(2^n)

空间复杂度

先看看copilot的解释:

image.png

  • 来一个🌰看看吧:
function fn (arr) {
let length = arr.length
for (let i = 0; i < length; i++) {
console.log(arr[i])
}
}

在函数fn中,我们创建了变量 length arr i,函数 fn 对内存的占用量是固定的,无论,arr的length如何,所以这个函数对应的空间复杂度就是 O(1)

  • 再来一个🌰:
function fn2(n) {
let arr = []
for(let i = 0; i < n; i++) {
arr[i] = i
}
}

在这个函数中,我们创建了一个数组 arr,并在循环中向 arr 中添加了 n 个元素。因此,arr 的大小与输入 n 成正比。所以,我们说这个函数的空间复杂度是 O(n)。

  • 再再来一个🌰:
function createMatrix(n) {
let matrix = [];
for (let i = 0; i < n; i++) {
matrix[i] = [];
for (let j = 0; j < n; j++) {
matrix[i][j] = 0;
}
}
return matrix;
}

在这个函数中,我们创建了一个二维数组 matrix,并在两层循环中向 matrix 中添加了 n*n 个元素。因此,matrix 的大小与输入 n 的平方成正比。所以,我们说这个函数的空间复杂度是 O(n^2)。

  • 再再再来一个🌰:
// 二分查找算法
function binarySearch(arr, target, low, high) {
if (low > high) {
return -1;
}
let mid = Math.floor((low + high) / 2);
if (arr[mid] === target) {
return mid;
} else if (arr[mid] > target) {
return binarySearch(arr, target, low, mid - 1);
} else {
return binarySearch(arr, target, mid + 1, high);
}
}

在二分查找中,我们每次都将问题规模减半,因此需要的额外空间与输入数据的对数成正比,我们开始时有一个大小为 n 的数组。然后,我们在每一步都将数组划分为两半,并只在其中一半中继续查找。因此,每一步都将问题的规模减半

所以,最多要划分多少次才能找到目标数据呢?答案是log2n次,但是在计算机科学中,当我们说 log n 时,底数通常默认为 2,因为许多算法(如二分查找)都涉及到将问题规模减半的操作。

2^x = n

x = log2n

常见的时间复杂度按照从小到大的顺序排列,有以下几种:

常数空间线性空间平方空间对数空间
O(1)O(n)O(n^2)O(logn)

你学废了吗?


作者:爱吃零食的猫
来源:juejin.cn/post/7320288222529536038

收起阅读 »

10个让你爱不释手的一行Javascript代码

web
在这篇博客中,我们将分享 10+ 个实用的一行 JavaScript 代码,这些代码可以帮助你提高编码效率和代码简洁度。这些代码片段将涵盖各种用途,从操作数组和字符串,到更高级的概念,如异步编程和面向对象编程。 获取数组中的随机元素 使用 Math.rand...
继续阅读 »

freysteinn-g-jonsson-s94zCnADcUs-unsplash.jpg
在这篇博客中,我们将分享 10+ 个实用的一行 JavaScript 代码,这些代码可以帮助你提高编码效率和代码简洁度。这些代码片段将涵盖各种用途,从操作数组和字符串,到更高级的概念,如异步编程和面向对象编程。


获取数组中的随机元素


使用 Math.random() 函数和数组长度可以轻松获取数组中的随机元素:


const arr = [1, 2, 3, 4, 5];
const randomElement = arr[Math.floor(Math.random() * arr.length)];
console.log(randomElement);

数组扁平化


使用 reduce() 函数和 concat() 函数可以轻松实现数组扁平化:


const arr = [[1, 2], [3, 4], [5, 6]];
const flattenedArr = arr.reduce((acc, cur) => acc.concat(cur), []);
console.log(flattenedArr); // [1, 2, 3, 4, 5, 6]

对象数组根据某个属性值进行排序


const sortedArray = array.sort((a, b) => (a.property > b.property ? 1 : -1));

从数组中删除特定元素


const removedArray = array.filter((item) => item !== elementToRemove);

检查数组中是否存在重复项


const hasDuplicates = (array) => new Set(array).size !== array.length;

判断数组是否包含某个值


const hasValue = arr.includes(value);

首字母大写


const capitalized = str.charAt(0).toUpperCase() + str.slice(1);

获取随机整数


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

获取随机字符串


const randomStr = Math.random().toString(36).substring(2, length);

使用解构和 rest 运算符交换变量的值:


let a = 1, b = 2
[b, a] = [a, b]
console.log(a, b) // 2, 1

将字符串转换为小驼峰式命名:


const str = 'hello world'
const camelCase = str.replace(/\s(.)/g, ($1) => $1.toUpperCase()).replace(/\s/g, '').replace(/^(.)/, ($1) => $1.toLowerCase())
console.log(camelCase) // "helloWorld"

计算两个日期之间的间隔


const diffInDays = (dateA, dateB) => Math.floor((dateB - dateA) / (1000 * 60 * 60 * 24));

查找日期位于一年中的第几天


const dayOfYear = (date) => Math.floor((date - new Date(date.getFullYear(), 0, 0)) / 1000 / 60 / 60 / 24);

复制内容到剪切板


const copyToClipboard = (text) => navigator.clipboard.writeText(text);

copyToClipboard("Hello World");

获取变量的类型


const getType = (variable) => Object.prototype.toString.call(variable).slice(8, -1).toLowerCase();

getType(''); // string
getType(0); // number
getType(); // undefined
getType(null); // null
getType({}); // object
getType([]); // array
getType(0); // number
getType(() => {}); // function

检测对象是否为空


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

系列文章



我的更多前端资讯


欢迎大家技术交流 资料分享 摸鱼 求助皆可 —链接


作者:shichuan
来源:juejin.cn/post/7230810119122190397
收起阅读 »

刷了四百道算法题,我在项目里用过哪几道呢?

大家好,我是老三,今天和大家聊一个话题:项目中用到的力扣算法。 不知道从什么时候起,算法已经成为了互联网面试的标配,在十年前,哪怕如日中天的百度,面试也最多考个冒泡排序。后来,互联网越来越热,涌进来的人越来越多,整个行业越来越内卷的,算法也慢慢成了大小互联网公...
继续阅读 »

大家好,我是老三,今天和大家聊一个话题:项目中用到的力扣算法。


不知道从什么时候起,算法已经成为了互联网面试的标配,在十年前,哪怕如日中天的百度,面试也最多考个冒泡排序。后来,互联网越来越热,涌进来的人越来越多,整个行业越来越内卷的,算法也慢慢成了大小互联网公司面试的标配,力扣现在已经超过3000题了,那么这些题目有多少进入了面试的考察呢?


以最爱考算法的字节跳动为例,看看力扣的企业题库,发现考过的题目已经有1850道——按照平均每道题花20分钟来算,刷完字节题库的算法题需要37000分钟,616.66小时,按每天刷满8小时算,需要77.08天,一周刷五天,需要15.41周,按一个月四周,需要3.85个月。也就是说,在脱产,最理想的状态下,刷完力扣的字节题库,需要差不多4个月时间。


字节题库


那么,我在项目里用过,包括在项目中见过哪些力扣上的算法呢?我目前刷了400多道题,翻来覆去盘点了一下,发现,也就这么几道。


刷题数量


1.版本比较:比较客户端版本


场景


在日常的开发中,我们很多时候可能面临这样的情况,兼容客户端的版本,尤其是Android和iPhone,有些功能是低版本不支持的,或者说有些功能到了高版本就废弃掉。


这时候就需要进行客户端的版本比较,客户端版本号通常是这种格式6.3.40,这是一个字符串,那就肯定不能用数字类型的比较方法,需要自己定义一个比较的工具方法。


某app版本


题目


165. 比较版本号


这个场景对应LeetCode: 165. 比较版本号



  • 题目:165. 比较版本号 (leetcode.cn/problems/co…)

  • 难度:中等

  • 标签:双指针 字符串

  • 描述:


    给你两个版本号 version1version2 ,请你比较它们。


    版本号由一个或多个修订号组成,各修订号由一个 '.' 连接。每个修订号由 多位数字 组成,可能包含 前导零 。每个版本号至少包含一个字符。修订号从左到右编号,下标从 0 开始,最左边的修订号下标为 0 ,下一个修订号下标为 1 ,以此类推。例如,2.5.330.1 都是有效的版本号。


    比较版本号时,请按从左到右的顺序依次比较它们的修订号。比较修订号时,只需比较 忽略任何前导零后的整数值 。也就是说,修订号 1 和修订号 001 相等 。如果版本号没有指定某个下标处的修订号,则该修订号视为 0 。例如,版本 1.0 小于版本 1.1 ,因为它们下标为 0 的修订号相同,而下标为 1 的修订号分别为 010 < 1


    返回规则如下:



    • 如果 *version1* > *version2* 返回 1

    • 如果 *version1* < *version2* 返回 -1

    • 除此之外返回 0


    示例 1:


    输入:version1 = "1.01", version2 = "1.001"
    输出:0
    解释:忽略前导零,"01""001" 都表示相同的整数 "1"

    示例 2:


    输入:version1 = "1.0", version2 = "1.0.0"
    输出:0
    解释:version1 没有指定下标为 2 的修订号,即视为 "0"

    示例 3:


    输入:version1 = "0.1", version2 = "1.1"
    输出:-1
    解释:version1 中下标为 0 的修订号是 "0",version2 中下标为 0 的修订号是 "1"0 < 1,所以 version1 < version2

    提示:



    • 1 <= version1.length, version2.length <= 500

    • version1version2 仅包含数字和 '.'

    • version1version2 都是 有效版本号

    • version1version2 的所有修订号都可以存储在 32 位整数




解法


那么这道题怎么解呢?这道题其实是一道字符串模拟题,就像标签里给出了了双指针,这道题我们可以用双指针+累加来解决。


在这里插入图片描述



  • 两个指针遍历version1version2

  • . 作为分隔符,通过累加获取每个区间代表的数字

  • 比较数字的大小,这种方式正好可以忽略前导0


来看看代码:


class Solution {
   public int compareVersion(String version1, String version2) {
       int m = version1.length();
       int n = version2.length();

       //两个指针
       int p = 0, q = 0;

       while (p < m || q < n) {
           //累加version1区间的数字
           int x = 0;
           while (p < m && version1.charAt(p) != '.') {
               x += x * 10 + (version1.charAt(p) - '0');
               p++;
          }

           //累加version2区间的数字
           int y = 0;
           while (q < n && version2.charAt(q) != '.') {
               y += y * 10 + (version2.charAt(q) - '0');
               q++;
          }

           //判断
           if (x > y) {
               return 1;
          }
           if (x < y) {
               return -1;
          }

           //跳过.
           p++;
           q++;
      }
       //version1等于version2
       return 0;
  }
}


应用


这段代码,直接CV过来,就可以直接当做一个工具类的工具方法来使用:


public class VersionUtil {

   public static Integer compareVersion(String version1, String version2) {
       int m = version1.length();
       int n = version2.length();

       //两个指针
       int p = 0, q = 0;

       while (p < m || q < n) {
           //累加version1区间的数字
           int x = 0;
           while (p < m && version1.charAt(p) != '.') {
               x += x * 10 + (version1.charAt(p) - '0');
               p++;
          }

           //累加version2区间的数字
           int y = 0;
           while (q < n && version2.charAt(q) != '.') {
               y += y * 10 + (version2.charAt(q) - '0');
               q++;
          }

           //判断
           if (x > y) {
               return 1;
          }
           if (x < y) {
               return -1;
          }

           //跳过.
           p++;
           q++;
      }
       //version1等于version2
       return 0;
  }
}


前面老三分享过一个规则引擎:这款轻量级规则引擎,真香!


比较版本号的方法,还可以结合规则引擎来使用:



  • 自定义函数:利用AviatorScript的自定义函数特性,自定义一个版本比较函数


        /**
        * 自定义版本比较函数
        */

       class VersionFunction extends AbstractFunction {
           @Override
           public String getName() {
               return "compareVersion";
          }

           @Override
           public AviatorObject call(Map<String, Object> env, AviatorObject arg1, AviatorObject arg2) {
               // 获取版本
               String version1 = FunctionUtils.getStringValue(arg1, env);
               String version2 = FunctionUtils.getStringValue(arg2, env);
               return new AviatorBigInt(VersionUtil.compareVersion(version1, version2));
          }
      }


  • 注册函数:将自定义的函数注册到AviatorEvaluatorInstance


        /**
        * 注册自定义函数
        */

       @Bean
       public AviatorEvaluatorInstance aviatorEvaluatorInstance() {
           AviatorEvaluatorInstance instance = AviatorEvaluator.getInstance();
           // 默认开启缓存
           instance.setCachedExpressionByDefault(true);
           // 使用LRU缓存,最大值为100个。
           instance.useLRUExpressionCache(100);
           // 注册内置函数,版本比较函数。
           instance.addFunction(new VersionFunction());
           return instance;
      }


  • 代码传递上下文:在业务代码里传入客户端、客户端版本的上下文


        /**
        * @param device 设备
        * @param version 版本
        * @param rule   规则脚本
        * @return 是否过滤
        */

       public boolean filter(String device, String version, String rule) {
           // 执行参数
           Map<String, Object> env = new HashMap<>();
           env.put("device", device);
           env.put("version", version);
           //编译脚本
           Expression expression = aviatorEvaluatorInstance.compile(DigestUtils.md5DigestAsHex(rule.getBytes()), rule, true);
           //执行脚本
           boolean isMatch = (boolean) expression.execute(env);
           return isMatch;
      }


  • 编写脚本:接下来我们就可以编写规则脚本,规则脚本可以放在数据库,也可以放在配置中心,这样就可以灵活改动客户端的版本控制规则


    if(device==bil){
    return false;
    }

    ## 控制Android的版本
    if (device=="Android" && compareVersion(version,"1.38.1")<0){
    return false;
    }

    return true;



2.N叉数层序遍历:翻译商品类型


场景


一个跨境电商网站,现在有这么一个需求:把商品的类型进行国际化翻译。


某电商网站商品类型国际化


商品的类型是什么结构呢?一级类型下面还有子类型,字类型下面还有子类型,我们把结构一画,发现这就是一个N叉树的结构嘛。


商品树


翻译商品类型,要做的事情,就是遍历这棵树,翻译节点上的类型,这不妥妥的BFS或者DFS!


题目


429. N 叉树的层序遍历


这个场景对应LeetCode:429. N 叉树的层序遍历



  • 题目:429. N 叉树的层序遍历(leetcode.cn/problems/n-…)

  • 难度:中等

  • 标签: 广度优先搜索

  • 描述:


    给定一个 N 叉树,返回其节点值的层序遍历。(即从左到右,逐层遍历)。


    树的序列化输入是用层序遍历,每组子节点都由 null 值分隔(参见示例)。


    示例 1:


    img


    输入:root = [1,null,3,2,4,null,5,6]
    输出:[[1],[3,2,4],[5,6]]

    示例 2:


    img


    输入:root = [1,null,2,3,4,5,null,null,6,7,null,8,null,9,10,null,null,11,null,12,null,13,null,null,14]
    输出:[[1],[2,3,4,5],[6,7,8,9,10],[11,12,13],[14]]

    提示:



    • 树的高度不会超过 1000

    • 树的节点总数在 [0, 10^4] 之间




解法


BFS想必很多同学都很熟悉了,DFS的秘诀是,BFS的秘诀是队列


层序遍历的思路是什么呢?


使用队列,把每一层的节点存储进去,一层存储结束之后,我们把队列中的节点再取出来,孩子节点不为空,就把孩子节点放进去队列里,循环往复。


N叉树层序遍历示意图


代码如下:


class Solution {
public List<List<Integer>> levelOrder(Node root) {
List<List<Integer>> result = new ArrayList<>();
if (root == null) {
return result;
}

//创建队列并存储根节点
Deque<Node> queue = new LinkedList<>();
queue.offer(root);

while (!queue.isEmpty()) {
//存储每层结果
List<Integer> level = new ArrayList<>();
int size = queue.size();
for (int i = 0; i < size; i++) {
Node current = queue.poll();
level.add(current.val);
//添加孩子
if (current.children != null) {
for (Node child : current.children) {
queue.offer(child);
}
}
}
//每层遍历结束,添加结果
result.add(level);
}
return result;
}
}

应用


商品类型翻译这个场景下,基本上和这道题目大差不差,不过是两点小区别:



  • 商品类型是一个属性多一些的树节点

  • 翻译过程直接替换类型名称即可,不需要返回值


来看下代码:



  • ProductCategory:商品分类实体


    public class ProductCategory {
    /**
    * 分类id
    */

    private String id;
    /**
    * 分类名称
    */

    private String name;
    /**
    * 分类描述
    */

    private String description;
    /**
    * 子分类
    */

    private List<ProductCategory> children;

    //省略getter、setter

    }




  • translateProductCategory:翻译商品类型方法


       public void translateProductCategory(ProductCategory root) {
    if (root == null) {
    return;
    }

    Deque<ProductCategory> queue = new LinkedList<>();
    queue.offer(root);

    //遍历商品类型,翻译
    while (!queue.isEmpty()) {
    int size = queue.size();
    //遍历当前层
    for (int i = 0; i < size; i++) {
    ProductCategory current = queue.poll();
    //翻译
    String translation = translate(current.getName());
    current.setName(translation);
    //添加孩子
    if (current.getChildren() != null && !current.getChildren().isEmpty()) {
    for (ProductCategory child : current.getChildren()) {
    queue.offer(child);
    }
    }
    }
    }
    }



3.前缀和+二分查找:渠道选择


场景


在电商的交易支付中,我们可以选择一些支付方式,来进行支付,当然,这只是交易的表象。


某电商支付界面


在支付的背后,一种支付方式,可能会有很多种支付渠道,比如Stripe、Adyen、Alipay,涉及到多个渠道,那么就涉及到决策,用户的这笔交易,到底交给哪个渠道呢?


这其实是个路由问题,答案是加权随机,每个渠道有一定的权重,随机落到某个渠道,加权随机有很多种实现方式,其中一种就是前缀和+二分查找。简单说,就是先累积所有元素权重,再使用二分查找来快速查找。


题目


先来看看对应的LeetCode的题目,这里用到了两个算法:前缀和二分查找


704. 二分查找



  • 题目:704. 二分查找(leetcode.cn/problems/bi…)

  • 难度:简单

  • 标签:数组 二分查找

  • 描述:


    给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1


    示例 1:


    输入: nums = [-1,0,3,5,9,12], target = 9
    输出: 4
    解释: 9 出现在 nums 中并且下标为 4

    示例 2:


    输入: nums = [-1,0,3,5,9,12], target = 2
    输出: -1
    解释: 2 不存在 nums 中因此返回 -1

    提示:



    1. 你可以假设 nums 中的所有元素是不重复的。

    2. n 将在 [1, 10000]之间。

    3. nums 的每个元素都将在 [-9999, 9999]之间。




解法


二分查找可以说我们都很熟了。


数组是有序的,定义三个指针,leftrightmid,其中midleftright的中间指针,每次中间指针指向的元素nums[mid]比较和target比较:


二分查找示意图



  • 如果nums[mid]等于target,找到目标

  • 如果nums[mid]小于target,目标元素在(mid,right]区间;

  • 如果nums[mid]大于target,目标元素在[left,mid)区间


代码:


class Solution {
public int search(int[] nums, int target) {
int left=0;
int right=nums.length-1;

while(left<=right){
int mid=left+((right-left)>>1);
if(nums[mid]==target){
return mid;
}else if(nums[mid]<target){
//target在(mid,right]区间,右移
left=mid+1;
}else{
//target在[left,mid)区间,左移
right=mid-1;
}
}
return -1;
}
}

二分查找,有一个需要注意的细节,计算mid的时候:int mid = left + ((right - left) >> 1);,为什么要这么写呢?


因为这种写法int mid = (left + right) / 2;,可能会因为left和right数值太大导致内存溢出。同时,使用位运算,也是除以2最高效的写法。


——这里有个彩蛋,后面再说。


303. 区域和检索 - 数组不可变


不像二分查找,在LeetCode上,前缀和没有直接的题目,因为本身前缀和更多是一种思路,一种工具,其中303. 区域和检索 - 数组不可变 是一道典型的前缀和题目。



  • 题目:303. 区域和检索 - 数组不可变(leetcode.cn/problems/ra…)

  • 难度:简单

  • 标签:设计 数组 前缀和

  • 描述:


    给定一个整数数组 nums,处理以下类型的多个查询:



    1. 计算索引 leftright (包含 leftright)之间的 nums 元素的 ,其中 left <= right


    实现 NumArray 类:



    • NumArray(int[] nums) 使用数组 nums 初始化对象

    • int sumRange(int i, int j) 返回数组 nums 中索引 leftright 之间的元素的 总和 ,包含 leftright 两点(也就是 nums[left] + nums[left + 1] + ... + nums[right] )


    示例 1:


    输入:
    ["NumArray", "sumRange", "sumRange", "sumRange"]
    [[[-2, 0, 3, -5, 2, -1]], [0, 2], [2, 5], [0, 5]]
    输出:
    [null, 1, -1, -3]

    解释:
    NumArray numArray = new NumArray([-2, 0, 3, -5, 2, -1]);
    numArray.sumRange(0, 2); // return 1 ((-2) + 0 + 3)
    numArray.sumRange(2, 5); // return -1 (3 + (-5) + 2 + (-1))
    numArray.sumRange(0, 5); // return -3 ((-2) + 0 + 3 + (-5) + 2 + (-1))

    提示:



    • 1 <= nums.length <= 104

    • -105 <= nums[i] <= 105

    • 0 <= i <= j < nums.length

    • 最多调用 104sumRange 方法




解法


这道题,我们如果不用前缀和的话,写起来也很简单:


class NumArray {
private int[] nums;

public NumArray(int[] nums) {
this.nums=nums;
}

public int sumRange(int left, int right) {
int res=0;
for(int i=left;i<=right;i++){
res+=nums[i];
}
return res;
}
}

当然时间复杂度偏高,O(n),那么怎么使用前缀和呢?



  • 构建一个前缀和数组,用来累积 (0……i-1)的和,这样一来,我们就可以直接计算[left,right]之间的累加和


前缀和数组示意图


代码如下:


class NumArray {
private int[] preSum;

public NumArray(int[] nums) {
int n=nums.length;
preSum=new int[n+1];
//计算nums的前缀和
for(int i=0;i<n;i++){
preSum[i+1]=preSum[i]+nums[i];
}
}

//直接算出区间[left,right]的累加和
public int sumRange(int left, int right) {
return preSum[right+1]-preSum[left];
}
}

可以看到,通过前缀和数组,可以直接算出区间[left,right]的累加和,时间复杂度O(1),可以说非常高效了。


应用


了解了前缀和和二分查找之后,回归我们之前的场景,使用前缀和+二分查找来实现加权随机,从而实现对渠道的分流选择。


渠道分流选择



  • 需要根据渠道和权重的配置,生成一个前缀和数组,来累积权重的值,渠道也通过一个数组进行分配映射

  • 用户的支付请求进来的时候,生成一个随机数,二分查找找到随机数载前缀和数组的位置,映射到渠道数组

  • 最后通过渠道数组的映射,找到选中的渠道


代码如下:


/**
* 支付渠道分配器
*/
public class PaymentChannelAllocator {
//渠道数组
private String[] channels;
//前缀和数组
private int[] preSum;
private ThreadLocalRandom random;

/**
* 构造方法
*
* @param channelWeights 渠道分流权重
*/
public PaymentChannelAllocator(HashMap<String, Integer> channelWeights) {
this.random = ThreadLocalRandom.current();
// 初始化channels和preSum数组
channels = new String[channelWeights.size()];
preSum = new int[channelWeights.size()];

// 计算前缀和
int index = 0;
int sum = 0;
for (String channel : channelWeights.keySet()) {
sum += channelWeights.get(channel);
channels[index] = channel;
preSum[index++] = sum;
}
}

/**
* 渠道选择
*/
public String allocate() {
// 生成一个随机数
int rand = random.nextInt(preSum[preSum.length - 1]) + 1;

// 通过二分查找在前缀和数组查找随机数所在的区间
int channelIndex = binarySearch(preSum, rand);
return channels[channelIndex];
}

/**
* 二分查找
*/
private int binarySearch(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;

while (left <= right) {
int mid = left + ((right - left) >> 2);
if (nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
// 当找不到确切匹配时返回大于随机数的最小前缀和的索引
return left;
}
}

测试一下:


    @Test
void allocate() {
HashMap<String, Integer> channels = new HashMap<>();
channels.put("Adyen", 50);
channels.put("Stripe", 30);
channels.put("Alipay", 20);

PaymentChannelAllocator allocator = new PaymentChannelAllocator(channels);

// 模拟100次交易分配
for (int i = 0; i < 100; i++) {
String allocatedChannel = allocator.allocate();
System.out.println("Transaction " + (i + 1) + " allocated to: " + allocatedChannel);
}
}

彩蛋


在这个渠道选择的场景里,还有两个小彩蛋。


二分查找翻车


我前面提到了一个二分查找求mid的写法:


int mid=left+((right-left)>>1);

这个写法机能防止内存溢出,用了位移运算也很高效,但是,这个简单的二分查找写出过问题,直接导致线上cpu飙升,差点给老三吓尿了。


吓惨了


int mid = (right - left) >> 2 + left;

就是这行代码,看出什么问题来了吗?


——它会导致循环结束不了!


为什么呢?因为>>运算的优先级是要低于+的,所以这个运算实际上等于:


int mid = (right - left) >> (2 + left);

在只有两个渠道的时候没有问题,三个的时候就寄了。


当然,最主要原因还是没有充分测试,所以大家知道我在上面为什么特意写了单测吧。


加权随机其它写法


这里用了前缀和+二分查找来实现加权随机,其实加权随机还有一些其它的实现方法,包括别名方法、树状数组、线段树 随机列表扩展 权重累积等等方法,大家感兴趣可以了解一下。


加权随机的实现


印象比较深刻的是,有场面试被问到了怎么实现加权随机,我回答了权重累积前缀和+二分查找,面试官还是不太满意,最后面试官给出了他的答案——随机列表扩展


什么是随机列表扩展呢?简单说,就是创建一个足够大的列表,根据权重,在相应的区间,放入对应的渠道,生成随机数的时候,就可以直接获取对应位置的渠道。


public class WeightedRandomList {
private final List<String> expandedList = new ArrayList<>();
private final Random random = new Random();

public WeightedRandomList(HashMap<String, Integer> weightMap) {
// 填充 expandedList,根据权重重复元素
for (String item : weightMap.keySet()) {
int weight = weightMap.get(item);
for (int i = 0; i < weight; i++) {
expandedList.add(item);
}
}
}

public String getRandomItem() {
// 生成随机索引并返回对应元素
int index = random.nextInt(expandedList.size());
return expandedList.get(index);
}

public static void main(String[] args) {
HashMap<String, Integer> items = new HashMap<>();
items.put("Alipay", 60);
items.put("Adyen", 20);
items.put("Stripe", 10);

WeightedRandomList wrl = new WeightedRandomList(items);

// 演示随机选择
for (int i = 0; i < 10; i++) {
System.out.println(wrl.getRandomItem());
}
}
}

这种实现方式就是典型的空间换时间,空间复杂度O(n),时间复杂度O(1)。优点是时间复杂度低,缺点是空间复杂度高,如果权重总和特别大的时候,就需要一个特别大的列表来存储元素。


当然这种写法还是很巧妙的,适合元素少、权重总和小的场景。


刷题随想


上面就是我在项目里用到过或者见到过的LeetCode算法应用,416:4,不足1%的使用率,还搞出过严重的线上问题。


……


在力扣社区里关于算法有什么的贴子里,有这样的回复:


“最好的结构是数组,最好的算法是遍历”。


“最好的算法思路是暴力。”


……


坦白说,如果不是为了面试,我是绝对不会去刷算法的,上百个小时,用在其他地方,绝对收益会高很多。


从实际应用去找刷LeetCode算法的意义,本身没有太大意义,算法题的最大意义就是面试。


刷了能过,不刷就挂,仅此而已。


这些年互联网行业红利消失,越来越多的算法题,只是内卷的产物而已。


当然,从另外一个角度来看,考察算法,对于普通的打工人,可能是个更公平的方式——学历、背景都很难卷出来,但是算法可以。


我去年面试的真实感受,“没机会”比“面试难”更令人绝望。


写到这,有点难受,刷几道题缓一下!






参考:


[1].leetcode.cn/circle/disc…


[2].36kr.com/p/121243626…


[3].leetcode.cn/circle/disc…


[4].leetcode.cn/circle/disc…







备注:涉及敏感信息,文中的代码都不是真实的投产代码,作者进行了一定的脱敏和演绎。





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

年终被砍、降薪、被拒,用我今年的经历给你几个忠告| 2023年终总结

2023年我的经历可以说是和大A一样,用今年的经历给大家几个忠告,希望我的经历让各位乐呵一下,学习到一些职场的小知识。 本人现任职某产业互联网独角兽公司交易部门后端开发,会点前端已经在这里躺了2年多。 春节前 第一次大跌从1月20号开始,也就是春节放假前一...
继续阅读 »

2023年我的经历可以说是和大A一样,用今年的经历给大家几个忠告,希望我的经历让各位乐呵一下,学习到一些职场的小知识。


本人现任职某产业互联网独角兽公司交易部门后端开发,会点前端已经在这里躺了2年多。



春节前



第一次大跌从1月20号开始,也就是春节放假前一天按照以往的经历是20号会发年终然而公司一波顶级操作一纸公告下来只有ABC绩效有年终而且与之前相比还打折,打开手机一看1000块过节费。后来才知道只给了部门几个可以拿年终的绩效名额,其他80%都是D。就这样拿着过节费过了一个年。


image.png



春节后



过完年回来后3月底要给我降薪,从组长那里得知原因是绩效评估是E,开完会后连忙去OA查询发现绩效评估为D,后来组长知道后开始和HR沟通。20号左右HR开始找我谈话开头先是道歉又说降薪不是以绩效为标准而是22年的几次线上事故影响过大原因。


一会是绩效组长沟通后又不是绩效,让我感觉是恶意降薪,就这样一直扯皮到快4月份。由于那段时间需要处理的事情太多不想和她扯皮所以选择同意降薪。


后来的小道消息得知系统录入的绩效和HR那里是两份,而系统里面高是因为有项目的加分,真不懂他们的绩效评估是怎么做的,那段时间真是可以说掉到了谷底身心俱疲。到今天想起来如果没有和别人说我系统中D绩效 HR没准也不会有其他理由降薪。


给打工人的第一个忠告:在公司里面谁也不要相信,定期收集考勤、加班证据,把证据握在自己手里,至于代码事故问题就写单元测试,留好评审会议记录,测试记录证据至少这样可以不被认定为主要责任。





9月、10月、11月裁员



之前一直听组长说23年业绩一直不好公司想要裁员到9月还是等到了,好像定了10个人将近部门人数的三分之一。因为公司砍掉了年终而且加班严重有几个小伙伴也有走的意愿,定了5个开发,还有几个转岗。10月又裁了几个开发,和被裁的小伙伴交流公司裁掉的全是年轻人30往上的一个没动,11月测试部门述职定不下名额直接两个测试全部裁掉


谈补偿HR又是神级操作先是套路员工灌输是自己想走,不是公司裁员不想给补偿金,后来又想按照实习期工资补偿,被部门几个人骂了后妥协了,年假还是不想给最后按照一倍补偿。到了发薪日又是一波操作最后几天的工资不给,听说要起诉公司又拖了一个月才发。都把人家裁了最后一天还在让别人加班太顶了。


给大家的第二个忠告:裁员的话不要慌也不要怕,一定要强硬,不要随便签字属于自己的赔偿一定要争取:赔偿金、代通知金、加班费、年假都算上,确定好最后的上班时间、社保、发薪时间。


给大家的第三个忠告:在公司不要和招惹或者和那些老员工、领导身边的红人翻脸,他们这些人就是能决定领导的想法,一边添油加醋一边对你笑嘻嘻





小插曲



8月底的一天HR突然找我说工时不够要扣工资,正常应该出勤23天184个小时,我其中一天请了假22天出勤了188个小时。按照之前公司要求加班的工时可以抵请假时长我用22天出勤了23天应出勤的时间是没问题的。HR的顶级算法是即使请假也要够应出勤工时然后多出来的才可以使用抵扣。真是这公司HR就是个大聪明数学不会算,最后还是没扣。


IMG_2392.jpg



年底



今年公司严格控制了部门支出,打车报销严查、加班也不管饭了。裁员后能干活的走了一半,现在的项目开发流程真是一言难尽,产品不设计原型、不写需求文档、不在OA提需求还说没有时间,需求没确定、没宣讲已经开始让开发这边开始了,开发按照做完初稿原型做完推倒重来。
三季度公司偷偷把社保调整到了80%,年底大言不惭的说在国家允许的情况下公积金调整到了5%,每次开会就是PUA让我们看看别的企业都在裁员应该把公司当成自己家一样。小道消息今年也没有年终。又沉闷的过了一年





出京



年终没了、也降薪了,放假后不想待在北京了端午直接去了杭州,由于接近亚运会的时间所以杭州氛围非常好,这个时候有点小梅雨,西湖边上拍的环境和氛围真的好。


IMG_1915.heic

周末和朋友们还去了承德,这个阳光和草原真绝了


trim.3A5310B6-27AB-4748-BA47-83A853A4C647.gif


11月去了南京,去南京是也为了自己的执念吧她还是没同意,这么久了也是时候放下了,第一次为了一个人跨越千里去了一个陌生的城市,鸡鸣寺的小猫都是两只。


IMG_2115.HEIC

年底和朋友几个去了威海,认识了一个辽宁的大哥开车带我们玩了一整天


2307e0a157b2162a6e595f689ed66b83.jpg

给大家的第四个忠告:工作不是你的全部,甚至不是你的生活,你要按照自己想过的方式去活着,有些事和东西得到了当然很好,你要知道得不到也不是你的问题尽人事听天命,降低期待。



2024计划



今年的计划是



  1. 继续走走到处去看看,西安、成都、武汉具体的到时候在看吧

  2. 在网上输出一些技术文章,之前的开发经历一直都没有沉淀

  3. 如果有机会可以继续搞搞副业,去年给朋友公司开发了一个APP,还有帮朋友做了一些需求

  4. 读书、读书、读书,继续学习,先试试中级软考吧,人还是不能停下来,一停下来就容易拖延

  5. 周末运动拒绝躺平,618全款拿下的公路车锻炼起来,身体才是革命的本钱

  6. 看机会,今年春节前有可能还会有一轮裁员,闲下来的时候看看机会。毕竟我们组的高级开发已经快3年没涨薪了,公司还不让人家走。


作者:旧梦呀
来源:juejin.cn/post/7320435287296032820
收起阅读 »

Android跳转系统界面_总结

1、跳转Setting应用列表(所有应用) Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_APPLICATIONS_SETTINGS); this.startActivity(intent); ...
继续阅读 »

1、跳转Setting应用列表(所有应用)


Intent intent =  new Intent(Settings.ACTION_MANAGE_ALL_APPLICATIONS_SETTINGS);
this.startActivity(intent);


2、跳转Setting应用列表(安装应用)


Intent intent =  new Intent(Settings.ACTION_MANAGE_APPLICATIONS_SETTINGS);


3、跳转Setting应用列表


Intent intent =  new Intent(Settings.ACTION_APPLICATION_SETTINGS);


4、开发者选项


Intent intent =  new Intent(Settings.ACTION_APPLICATION_DEVELOPMENT_SETTINGS);


5、允许在其它应用上层显示的应用


Intent intent =  new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);


6、无障碍设置


Intent intent =  new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS);


7、添加账户


Intent intent =  new Intent(Settings.ACTION_ADD_ACCOUNT);


8、WIFI设置


Intent intent =  new Intent(Settings.ACTION_WIFI_SETTINGS);


9、蓝牙设置


Intent intent =  new Intent(Settings.ACTION_BLUETOOTH_SETTINGS);


10、移动网络设置


Intent intent =  new Intent(Settings.ACTION_DATA_ROAMING_SETTINGS);


11、日期时间设置


Intent intent =  new Intent(Settings.ACTION_DATE_SETTINGS);


12、关于手机界面


Intent intent =  new Intent(Settings.ACTION_DEVICE_INFO_SETTINGS);


13、显示设置界面


Intent intent =  new Intent(Settings.ACTION_DISPLAY_SETTINGS);


14、声音设置


Intent intent =  new Intent(Settings.ACTION_SOUND_SETTINGS);


15、互动屏保


Intent intent =  new Intent(Settings.ACTION_DREAM_SETTINGS);


16、输入法


Intent intent =  new Intent(Settings.ACTION_INPUT_METHOD_SETTINGS);


17、输入法_SubType


Intent intent =  new Intent(Settings.ACTION_INPUT_METHOD_SUBTYPE_SETTINGS);


18、内部存储设置界面


Intent intent =  new Intent(Settings.ACTION_INTERNAL_STORAGE_SETTINGS);


19、存储卡设置界面


Intent intent =  new Intent(Settings.ACTION_MEMORY_CARD_SETTINGS);


20、语言选择界面


Intent intent =  new Intent(Settings.ACTION_LOCALE_SETTINGS);


21、位置服务界面


Intent intent =  new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS);


22、运营商


Intent intent =  new Intent(Settings.ACTION_NETWORK_OPERATOR_SETTINGS);


23、NFC共享界面


Intent intent =  new Intent(Settings.ACTION_NFCSHARING_SETTINGS);


24、NFC设置


Intent intent =  new Intent(Settings.ACTION_NFC_SETTINGS);


25、备份和重置


<Intent intent =  new Intent(Settings.ACTION_PRIVACY_SETTINGS);


26、快速启动


Intent intent =  new Intent(Settings.ACTION_QUICK_LAUNCH_SETTINGS);


27、搜索设置


Intent intent =  new Intent(Settings.ACTION_SEARCH_SETTINGS);


28、安全设置


Intent intent =  new Intent(Settings.ACTION_SECURITY_SETTINGS);


29、设置的主页


Intent intent =  new Intent(Settings.ACTION_SETTINGS);


30、用户同步界面


Intent intent =  new Intent(Settings.ACTION_SYNC_SETTINGS);


31、用户字典


Intent intent =  new Intent(Settings.ACTION_USER_DICTIONARY_SETTINGS);


32、IP设置


Intent intent =  new Intent(Settings.ACTION_WIFI_IP_SETTINGS);


33、App设置详情界面


public void startAppSettingDetail() {
String packageName = getPackageName();
Uri packageURI = Uri.parse("package:" + packageName);
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
intent.setData(packageURI);
startActivity(intent);
}


34、跳转应用市场


public void startMarket() {
Intent intent = new Intent(Intent.ACTION_VIEW);
// intent.setData(Uri.parse("market://details?id=" + "com.xxx.xxx"));
intent.setData(Uri.parse("market://search?q=App Name"));
startActivity(intent);
}


35、获取Launcherbaoming


public void getLauncherPackageName() {
Intent intent = new Intent(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_HOME);
final ResolveInfo res = this.getPackageManager().resolveActivity(intent, 0);
if (res.activityInfo == null) {
Log.e("TAG", "没有获取到");
return;
}

if (res.activityInfo.packageName.equals("android")) {
Log.e("TAG", "有多个Launcher,且未指定默认");
} else {
Log.e("TAG", res.activityInfo.packageName);
}
}


36、跳转图库获取图片


public void startGallery() {
Intent intent = new Intent(Intent.ACTION_PICK,
android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
intent.setType("image/*");
this.startActivityForResult(intent, 1);
}


37、跳转相机,拍照并保存


public void startCamera() {
String dir = Environment.getExternalStorageDirectory().getAbsolutePath() + "/test.jpg";
Uri headCacheUri = Uri.fromFile(new File(dir));
Intent takePicIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
takePicIntent.putExtra(MediaStore.EXTRA_OUTPUT, headCacheUri);
startActivityForResult(takePicIntent, 2);
}


38、跳转文件管理器


public void startFileManager() {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT,
android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
intent.setType("file/*");
this.startActivityForResult(intent, 3);
}


39、直接拨打电话


 public void startCall() {
Intent callIntent = new Intent(Intent.ACTION_CALL);
callIntent.setData(Uri.parse("tel:" + "13843894038"));
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
return;
}
startActivity(callIntent);
}


40、跳转电话应用


public void startPhone() {
Intent intent = new Intent(Intent.ACTION_DIAL,Uri.parse("tel:" + "13843894038"));
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
}


41、发送短信


public void startSMS() {
Uri smsToUri = Uri.parse("smsto://10086");
Intent mIntent = new Intent( android.content.Intent.ACTION_SENDTO, smsToUri );
startActivity(mIntent);
}


42、发送彩信


public void startMMS() {
Uri uri = Uri.parse("content://media/external/images/media/11");
Intent it = new Intent(Intent.ACTION_SEND);
it.putExtra("sms_body", "some text");
it.putExtra(Intent.EXTRA_STREAM, uri);
it.setType("image/png");
startActivity(it);
}


43、发送邮件


public void startEmail() {
Uri uri = Uri.parse("mailto:6666666@qq.com");
String[] email = {"12345678@qq.com"};
Intent intent = new Intent(Intent.ACTION_SENDTO, uri);
intent.putExtra(Intent.EXTRA_CC, email); // 抄送人
intent.putExtra(Intent.EXTRA_SUBJECT, "这是邮件的主题部分"); // 主题
intent.putExtra(Intent.EXTRA_TEXT, "这是邮件的正文部分"); // 正文
startActivity(Intent.createChooser(intent, "请选择邮件类应用"));
}


44、跳转联系人


public void startContact() {
Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
intent.setData(Contacts.People.CONTENT_URI);
startActivity(intent);

/*Intent intent = new Intent();
intent.setAction(Intent.ACTION_PICK);
intent.setData(Uri.parse("content://contacts/people"));
startActivityForResult(intent, 5);*/

}


45、插入联系人


public void insertContact() {
Intent intent = new Intent(Intent.ACTION_INSERT);
intent.setData(ContactsContract.Contacts.CONTENT_URI);
intent.putExtra(ContactsContract.Intents.Insert.PHONE, "18688888888");
startActivityForResult(intent, 1);
}


46、插入日历事件


public void startCalender() {
Intent intent = new Intent(Intent.ACTION_INSERT);
intent.setData(CalendarContract.Events.CONTENT_URI);
intent.putExtra(CalendarContract.Events.TITLE, "开会");
startActivityForResult(intent, 1);
}


47、跳转浏览器


public void startBrowser() {
Uri uri = Uri.parse("http://www.baidu.com");
Intent intent = new Intent(Intent.ACTION_VIEW,uri);
startActivity(intent);
}


48、安装应用


public void startInstall() {
String filePath="/xx/xx/abc.apk";
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.parse("file://" + filePath),
"application/vnd.android.package-archive");
startActivity(intent);
}>



49、卸载应用


public void startUnInstall() {
String packageName="cn.memedai.mas.debug";
Uri packageUri = Uri.parse("package:"+packageName);//包名,指定该应用
Intent uninstallIntent = new Intent(Intent.ACTION_DELETE, packageUri);
startActivity(uninstallIntent);
}


50、回到桌面


public void startLauncherHome() {
Intent intent = new Intent(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_HOME);
startActivity(intent);
}


51、打开任意文件(根据其MIME TYPE自动选择打开的应用)


  private void openFile(File f) {
Intent intent = new Intent();
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setAction(android.content.Intent.ACTION_VIEW);
String type = getMIMEType(f);
intent.setDataAndType(Uri.fromFile(f), type);
startActivity(intent);
}

private String getMIMEType(File f) {
String end = f.getName().substring(f.getName().lastIndexOf(".") + 1,
f.getName().length()).toLowerCase();
String type = "";
if (end.equalsIgnoreCase("mp3")
|| end.equalsIgnoreCase("aac")
|| end.equalsIgnoreCase("amr")
|| end.equalsIgnoreCase("mpeg")
|| end.equalsIgnoreCase("mp4")) {
type = "audio";
} else if(end.equalsIgnoreCase("mp4")
|| end.equalsIgnoreCase("3gp")
|| end.equalsIgnoreCase("mpeg4")
|| end.equalsIgnoreCase("3gpp")
|| end.equalsIgnoreCase("3gpp2")
|| end.equalsIgnoreCase("flv")
|| end.equalsIgnoreCase("avi")) {
type = "video";
} else if (end.equalsIgnoreCase("jpg")
|| end.equalsIgnoreCase("gif")
|| end.equalsIgnoreCase("bmp")
|| end.equalsIgnoreCase("png")
|| end.equalsIgnoreCase("jpeg")) {
type = "image";
} else {
type = "*";
}
type += "/*";
return type;
}


52、跳转录音


public void startRecord() {
Intent intent = new Intent(MediaStore.Audio.Media.RECORD_SOUND_ACTION);
startActivity(intent);
}



👀关注公众号:Android老皮!!!欢迎大家来找我探讨交流👀



作者:派大星不吃蟹
来源:juejin.cn/post/7321551188092403764
收起阅读 »

检测自己网站是否被嵌套在iframe下并从中跳出

web
iframe被用于将一个网页嵌套在另一个网页中,有的时候这会带来一些安全问题,这时我们就需要一些防嵌套操作了。 本文分为俩部分,一部分讲解如何检测或者禁止嵌套操作,另一部分讲解如何从嵌套中跳出。 末尾放了正在使用的完整代码,想直接用的可以拉到最后。 效果 当存...
继续阅读 »

iframe被用于将一个网页嵌套在另一个网页中,有的时候这会带来一些安全问题,这时我们就需要一些防嵌套操作了。

本文分为俩部分,一部分讲解如何检测或者禁止嵌套操作,另一部分讲解如何从嵌套中跳出。


末尾放了正在使用的完整代码,想直接用的可以拉到最后。


效果


当存在嵌套时会出现一个蒙版和窗口,提示用户点击。

点击后会在新窗口打开网站页面。


嵌套展示


嵌套检测


设置响应头


响应头中有一个名为X-Frame-Options的键,可以针对嵌套操作做限制。

它有3个可选值:


DENY:拒绝所有


SAMEORIGIN:只允许同源


ALLOW-FROM origin:指定可用的嵌套域名,新浏览器已弃用


后端检测(以PHP为例)


通过获取$_SERVER中的HTTP_REFERERHTTP_SEC_FETCH_DEST值,可以判断是否正在被iframe嵌套


// 如果不是iframe,就为空的字符串
$REFERER_URL = $_SERVER['HTTP_REFERER'];

// 资源类型,如果是iframe引用的,会是iframe
$SEC_FETCH_DEST = $_SERVER['HTTP_SEC_FETCH_DEST'];

// 默认没有被嵌套
$isInIframe = false;

if (isset($_SERVER['HTTP_REFERER'])) {
$refererUrl = parse_url($_SERVER['HTTP_REFERER']);
$refererHost = isset($refererUrl['host']) ? $refererUrl['host'] : '';

if (!empty($refererHost) && $refererHost !== $_SERVER['HTTP_HOST']) {
$isInIframe = true;
}
}

// 这里通过判断$isInIframe是否为真,来处理嵌套和未嵌套执行的动作。
if($isInIframe){
....
}

前端检测(使用JavaScript)


通过比较window.self(当前窗口对象)和window.top(顶层窗口对象)可以判断是否正在被iframe嵌套


if (window.self !== window.top) {
// 检测到嵌套时该干的事
}

从嵌套中跳出


跳出只能是前端处理,如果使用了PHP等后端检测,可以直接返回前端JavaScript代码,或者HTML的A标签设置转跳。


JavaScript直接转跳(不推荐)


不推荐是因为现在大多浏览器为了防止滥用,会阻止自动弹出新窗口。


window.open(window.location.href, '_blank');

A标签点击转跳(较为推荐)


当发生了用户交互事件,浏览器就不会阻止转跳了,所以这是个不错的方法。


href="https://www.9kr.cc" target="_blank">点击进入博客

JavaScript+A标签(最佳方法)


原理是先使用JavaScript检测是否存在嵌套,

如果存在嵌套,再使用JavaScript加载蒙版和A标签,引导用户点击。


这个方法直接查看最后一节。


正在使用的方法


也就是上一节说的JavaScript+A标签。


先给待会要显示的蒙版和A标签窗口设置样式


/* 蒙版样式 */
.overlay1 {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5); /* 半透明背景颜色 */
z-index: 9999; /* 确保蒙版位于其他元素之上 */
display: flex;
align-items: center;
justify-content: center;
}

/* 窗口样式 */
.modal1 {
background-color: #fff;
padding: 20px;
border-radius: 5px;
}

然后是检测和加载蒙版+A标签的JavaScript代码


if (window.self !== window.top) {
// 创建蒙版元素
var overlay = document.createElement('div');
overlay.className = 'overlay1';

// 创建窗口元素
var modal = document.createElement('div');
modal.className = 'modal1';

// 创建A标签元素
var link = document.createElement('a');
link.href = 'https://www.9kr.cc';
link.target = '_blank'; // 在新窗口中打开链接
link.innerText = '点击进入博客';
//link.addEventListener('click', function(event) {
// event.preventDefault(); // 阻止默认链接行为
// alert('Test');
//});

// 将A标签添加到窗口元素中
modal.appendChild(link);

// 将窗口元素添加到蒙版元素中
overlay.appendChild(modal);

// 将蒙版元素添加到body中
document.body.appendChild(overlay);
}

博客的话,只需要在主题上设置自定义CSS自定义JavaScript即可


博客后台设置




作者:Edit
来源:juejin.cn/post/7272742720841252901
收起阅读 »

2024年突如其来的危机感和反思总结

前言 说来也讽刺,我刚刚在23年12月写了一篇走出迷茫,还给自己定了个目标,新的一年刚开始就遇到危机了。 因为我负责一个项目迁移了两次都失败了,领导说虽然去年没扣你绩效,但你连续失败可能会被领导扣绩效。原因是上面只看结果,过程他们看不到。如果严重的话,你可能会...
继续阅读 »

前言


说来也讽刺,我刚刚在23年12月写了一篇走出迷茫,还给自己定了个目标,新的一年刚开始就遇到危机了。


因为我负责一个项目迁移了两次都失败了,领导说虽然去年没扣你绩效,但你连续失败可能会被领导扣绩效。原因是上面只看结果,过程他们看不到。如果严重的话,你可能会被列入 优化名单。


刚刚7天内通宵两个晚上的我,听到这个消息后脑子真的嗡嗡的。因为我本能的认为失败的原因不在我,网络问题结合使用的nextJS插件,导致我们无法线下测试,所以有问题只会在生产上暴露。


就这样通宵后的我三天没有睡好,有一天晚上我梦见我在和领导解释为什么会出现这些问题,但是他们不听。我不是失落,而是害怕。有房贷和孩子没有坚强的家庭支援的人,大概会懂我这几天的无助。


起因


事情是这样的,我负责的一个项目要从公有云迁移到私有云,而私有云中有部分中间要求使用国产化,这些问题都已经解决了之前也简单记录了一下。


但是这个项目最复杂的是网络,有十几二十个防火墙要申请,还有一些白名单要配置,而且我们没有域名,使用的是别人的域名http://www.aaa.cn/path进行转发到我们web代理服务器上。就因为这个/path的原因,我们的前端后端都在代码中做了修改。关键他还不止一个域名,还有一个aaa.cn/path这个地址也可以访问。但该死的http://www.aaa.cn/path一开始只有外网可访问内网访问不了,aaa.cn/path一开始内网都可以访问。然而,今天测试的时候发现这两个https地址都可以访问了,但是我事先没有收到任何通知。这里说一下为什么要https,因为微信必须要求https域名,当然还有一些其他场景。


第一次割接


因为迁移后的环境没有割接前没有域名,更没有https的域名供我们使用测试。我们申请了公网负载IP进行测试一切顺利,于是我开始第一次割接。然后失败了,因为迁移前的obs是自带公网可访问域名的(我们的资源公网客户端可以直接访问),但是迁移后我们的obs是私有云,他们虽然提供了域名但只能内网访问,于是我们使用了nginx做了反向代理。反向代理后使用公网负载IP到访问这个私有云的obs资源是没有问题的,但上了服务器使用域名访问这些资源,就不能访问。因为当晚除了这个域名问题,还有一个程序问题,所以我在凌晨5点放弃了割接,发邮件说明失败原因。


第二次割接


第一次的使用域名无法方私有云obs问题,我领导去修改了nginx代理配置,增加了header头,将host改成了可以正常访问的公网负载IP,然后使用浏览器测试直接打开了私有云obs的图片。另外程序问题是开发忘记刷脚本了,我没有骂他,因为我觉得我骂了影响后面的工作。外包是一个团队,因为他工作的原因导致其他人无效通宵,其他人会给他压力的。当然,提还是要提的。


解决上述两个问题后,我准备了第二次割接,然后还是失败了。原因nextJS打包是需要访问后端服务器,同时nextJS中有个图片模糊加载的插件访问图片的域名和打包需要访问后端服务器域名是同一个,共用同一个参数配置。而不巧的是我们部署打包的服务器无法访问http://www.aaa.cn这个域名,而aaa.cn虽然可以访问,但是他的证书不安全nextJS的模糊加载插件直接提示安全问题,不予与加载展示。


我们蹭着线上域名割接后,做了几轮测试得出一下结论。


方案一:http://www.aaa.cn需要打包服务器能访问,运维说配置hosts就可以,但这个要提工单,无法直接协调;


方案二:aaa.cn配置上SSL安全证书,使其https合法;


方案三:如果方案一和方案二尝试后都不行,在http://www.aaa.cn打包服务器可访问的情况下或aaa.cn配置安全证书的情况,去掉nextJS的模糊图片加载问题;


再次放弃割接计划,发邮件说明原因。




然后6点睡,10点起,和领导沟通问题的时候,领导说了上面的话。我给领导回复是,主要还是网络太复杂了,但是我会尽全力的,结果怎么样我也没得选,听天命吧。


过程


领导和我聊完后,我的心情是不能平复的。


我想的最多的是,如果我失业了,我那每月1.3w的房贷怎么办?


每月的家庭支出怎么办?


我老婆一个人能不能扛得住?


现在这个环境我能快速找到工作吗?


就算找到了,我能找到心仪的工作吗?


找到新工作后,我能不能待多久?


我现在是不是该去复习一些技术了?


我应该先学哪些东西呢?


我是不是应该找个副业?


搞短视频?写小说?滴滴?外卖?


自己做几个益智的微信小程序游戏,然后靠广告赚点饭钱?


回老家问问我爷爷或者我父辈的那些山和地是否能给我种果树或者粮食?


...


第二天是个周六,我开始冷静了一点。我开始拿起手机看着一串延期的计划表发呆,我完全提不起一点兴趣,也许自己不行去做的一种借口吧。但结果是我真的没有去做,因为我不想做。


看着计划,我越看越不对劲。


第三天是个周日,快到晚上的时候,我老婆问我吃完饭不。我说不吃了,刚好适应一下失业后饿肚子的感觉,以后说不准要经常饿肚子。


第四天早上,起来把掘金、华住、学习强国签到完,学了一节多领国,然后就去完成运动计划1000跳绳+10组其他健身运动。运动完后去洗澡,然后就萌生了鼓励自己的念头。


“想想这两次失败是否完全不可测试的?”


“还有哪些我能做的?”


“领导只是说我有危险,那何不在努力试试留下来,毕竟你自己希望能在这里呆满3年+的!”


“第二次割接的问题是不是可以通过自己购买域名模拟?”


“做自己该做的,船到桥头自然直,况且你一直觉得自己能力还可以,至少是中等水品?”


反思


反思第一次失败


1、虽然自己整理了checklist清单,让项目确认了他们也确认了,但自己并没有让他们把每个环节需要执行细节落入书面;


2、自己在整个上线过程中,确实没有针对具体问题做深度的剖析,只是站在方向的引导上,过度依赖团队中的开发;


3、网络知识和nginx虽然一直在用,但自己不熟悉却没有放到学习项中,自己一直在学习其他玩意,重要紧急没有分清楚;


4、出现问题,具体的问题没有自己剖析过,觉得是网络问题自己肯定不会;


反思第二次失败


1、和第一次一样,没有亲自分析问题日志和原因,基本都是团队反馈,然后自己总结的归纳;


2、没有深思熟虑,既然上次有域名访问图片的问题,但却没有考虑https的问题和nextJS打包需要访问后端的问题;


反思个人计划


1、强化工作的部分有,但太少需要针对性增加学习工作中遇到的薄弱的技术问题;


2、整个计划中,基本除了健康就是学习,没有增加实施后可以增加收入或者增加收入机会的内容,即使列了也没有执行到位;


3、计划中应该有侧重,计划中内容太多时间太分散,应该每个阶段增加一个侧重;


调整


关于本次迁移的工作的总结:


1、上线前整理checklist,并且核对每个人负责的内容,包括细节操作和操作所需材料,并收集材料;


2、以前是团队负责人,现在是技术经理,需要下沉,表现在现场分析解决问题和增加技术知识面;


3、增对工作汇总遇到的薄弱技术知识点,针对性的寻找资料学习;


4、遇到问题,冲在一线,现在是技术经理需要关系技术细节,并且需要从细节上帮助团队解决问题;


5、没有解决不了的问题,没有复现不了的环境,无法是成本问题,不要一分不掏,因为没了工作损失的不止这点钱;


关于自身工作状态的总结:


1、这家公司自从自己将责任划分清楚后,开始有点安逸,但所有需求自己要过一遍,每个技术方案自己要把持;


2、还是要以工作为主,有一半的学习要和当下的工作相关;


3、不要过分信任团队,特别是外包团队,要将核心掌握在自己手里;


4、防御性上班,关键核心的要素信息要记笔记,但点到为止自己明白就行,不然对你下黑手时,你无力反抗和无法维护自己的权益;


结合上述总结调整2024年执行计划:


原计划


一、工作:
1)2024年保住当前工作,做好项目技术管理,保持向上汇报,平级保持责任分明适当帮忙,识别风险提前向干系人预警;

二、学习:
1)每天保持至少3天的coding或技术学习,将自己花了万元的VIP培训视频一点点消化,每天就算看10分钟也行;
2)每周一篇技术博客,将解决技术问题和技术学习的内容,分享到微信公众号或掘金等博客上;
3)学习英语,多领国每天只是少一节,时间多可以多练习几个,拓宽后续就业面,避免被需要英语的外企或国际公司限制;
4)通过五月份的软考高项,去年上半年没有过,下半年放弃了,每天背知识点、练习和看教学视频;

三、健康:
1)每天保持运动,常规每天1000个跳绳+10组其他运动,如俯卧撑,最次每天200个跳绳,争取将结石排除提完;
2)控制饮食,多吃粗纤维果蔬少油少盐,争取大多时候半碗饭和两素一荤,至少每周一个晚上不吃晚饭,晚上19点后不食;
3)体重减到170以下,除了坚持以上两项,多出去走走;
4)排出肾结石,中度脂肪肝转轻度或无,降血液中的胆固醇,治好咽喉炎和鼻窦炎,以上四样至少完成两项;
5)平均睡眠提升到6小时+;
6)作为兴趣学学中医,看看倪海厦的中医视频,聊胜于无;
四、创作:
1)持续创作短视频或者小说,小说24年争取实现100w字,短视频每周一篇,不做硬性要求业余时间够就走;

以上所有目标,均坚持非强制原则,如果昨天没有完成,把今天的完成即可,有时间再补昨天的。

分解原计划


一、工作:
12024年保住当前工作,做好项目技术管理,保持向上汇报,平级保持责任分明适当帮忙,识别风险提前向干系人预警;
1.运行并阅读分析当前项目代码、分析数据库设计和分析中间件的使用,发现问题提出改进计划 -- 提高领导力的影响,专家权利;
2.对所有新增需求进行阅读,参与并制定需求所使用的技术方案 -- 掌握项目技术栈和架构变化,增加项目经验和能力;
3.对nginx、http协议、kafka、mysql等进行系统的学习,并将学习的内容用自己的语言总结描文章供后续自己翻阅;
4.不定期向领导汇报工作进展,包括工作中的问题、好消息等,特别是风险要提前预警;

二、学习:
1)每周保持至少3天的coding或技术学习,将自己花了万元的VIP培训视频一点点消化,每天就算看10分钟也行;
2)每周一篇技术博客,将解决技术问题和技术学习的内容,分享到微信公众号或掘金等博客上;
3)多领国学习英语每天只是少一节,尽可能多读多听重点练习听读,拓宽后续就业面,避免被需要英语的外企或国际公司限制;
4)按照提供的学习方看回放、复习讲义、做练习、对照题找书本原话,争取通过五月份的软考高项;

三、健康:
1)每天保持运动,每天200个跳绳,最佳常规每周三次 1000个跳绳+10组其他运动,争取体重减到170以下;
2)控制饮食,多吃粗纤维果蔬少油少盐,至少每周一个晚上不吃晚饭,晚上19点后不食;
3)每天200跳争取排出肾结石;少吃油腻增加运动争取中度脂肪肝转轻度或无和降血液中的胆固醇;少吃辛辣争取治好咽喉炎和鼻窦炎;
4)平均睡眠提升到6小时+;
5)每天拍胆经肝经心经;

四、创作:
1)每周至少发布一个短视频,主要发布自学中医相关内容或者郑强、罗翔、温铁军、艾跃进等爱国思想的演讲相关的内容,主打传播正能量和价值;
2)每天500字小说,争取24年完成30w字的小说;

五、拓展:
1)每周至少看书2小时;
2)每周学习中医至少1小时;
以上所有目标,均坚持非强制原则,如果昨天没有完成,把今天的完成即可,有时间再补昨天的。

新计划


因为之前的计划使用iphone自带的提醒事项做的,但是这东西在统计上手机和电脑不同步,而且手机电脑一起用还会重复计数。因此准备自己搞个计划清单列表小程序,至于app后续再研究,使用微信消息推送。


一、工作:
1)运行并阅读分析当前项目代码、分析数据库设计和分析中间件的使用,发现问题提出改进计划;
-- 本月每天2小时,将程序打包编译先搞定,独立完成UAT环境的部署和安装(侧重);
2)对所有新增需求进行阅读,参与并制定需求所使用的技术方案;
-- 有就阅读,并分析需求中是否需要使用新的技术方案;
3)对nginx、http协议、kafka、mysql等进行系统的学习,并将学习的内容用自己的语言总结描文章供后续自己翻阅;
-- 每2周学习nginx一个功能点,整理成技术文章;
4)不定期向领导汇报工作进展,包括工作中的问题、好消息等,特别是风险要提前预警;
-- 一句项目情况汇报;

二、学习:
1)每周保持至少3天的coding或技术学习,将自己花了万元的VIP培训视频一点点消化,每天就算看10分钟也行;
2)每周一篇技术博客,将解决技术问题和技术学习的内容,分享到微信公众号或掘金等博客上;
3)多领国学习英语每天只是少一节,尽可能多读多听重点练习听读,拓宽后续就业面,避免被需要英语的外企或国际公司限制;
4)按照提供的学习方看回放、复习讲义、做练习、对照题找书本原话,争取通过五月份的软考高项(侧重);

三、健康:
1)每天保持运动,每天200个跳绳,最佳常规每周三次 1000个跳绳+10组其他运动,争取体重减到170以下(侧重);
2)控制饮食,多吃粗纤维果蔬少油少盐,至少每周一个晚上不吃晚饭,晚上19点后不食;
3)每天200跳争取排出肾结石;少吃油腻增加运动争取中度脂肪肝转轻度或无和降血液中的胆固醇;少吃辛辣争取治好咽喉炎和鼻窦炎;
4)平均睡眠提升到6小时+;
5)每天拍胆经肝经心经;

四、创作:
1)每周至少发布一个短视频,主要发布自学中医相关内容或者郑强、罗翔、温铁军、艾跃进等爱国思想的演讲相关的内容,主打传播正能量和价值;
2)每天500字小说,争取24年完成30w字的小说;
33月底前,开发一个小程序用于记录计划清单,并使用微信提醒,后续看情况加上短信提醒(侧重);
4)模仿一个微信小游戏,

五、拓展:
1)每周至少看书2小时;
2)每周学习中医至少1小时;
以上所有目标,均坚持非强制原则,如果昨天没有完成,把今天的完成即可,有时间再补昨天的。

总结回顾


这些年我做了很多选择,但是我并没有因为我的选择变得更好。早先时候我一路走上坡的时候,我确实觉得是因为自己能力变强了我才有这样的成就,我也很自信我确实有这样的能力。但最近这4年一路下坡,让我重新认识了自己。早期我的能力可能确实在中上游,加上环境好很容易上去,而最终无论什么原因自己下来了说明自己总归有些问题的。


什么问题?自己认为比较严重的问题有如下:


1、过早且长期脱离一线,虽然有心想要重回一线,但是内心是抗拒那种艰苦的日子,虽然我不会把所有功绩揽给自己,但确实沾沾自喜;这就导致很多技术上的问题,我虽然了解但浮于表面,带着团队能解决,自己不一定能解决,最多只有思路。


2、没有认清打工人的本质,我曾在几家高端职位的公司任职,因为觉得高层领导或者直属领导太煞笔、不听劝、独断专行,而愤然离职;说到底还是太年轻,打工人就和上钟的技师一样,你要让领导爽,然后才能谈条件;他的煞笔不应该由你自己来买单,当然也和个人性格有关,城府和隐忍在职场上相当重要。


3、方向问题,我虽然做了11年,我之前的求职一直是以工资和职位头衔为目标,我基本没有规划过我的职业领域方向;等到要进入高端职位的圈子时,发现自己竟然什么都会一些,但别人要的是某个领域至少5年以上的工作经验,而我其中一个领域最多只有3.5年。


4、重心和当前迫切的问题自己没有刻意的把我,就比如很多计划看着挺好,但做起来也挺好,但是没有沉淀或者和当前的工作没有关系,就这样失去了很多巩固和提升能力的机会。


5、心里一直想要给自己留条后路,却发现前路没有走好,后路也没有留上,终日惶惶不安日。


有时候我在想,每一次的成功是不是老天给我的机会或者上辈子积德所致,每一次的失败或者落魄是不是老天觉得我朽木不可雕也。


但实际上自己也知道问题在哪?


不想做一线工作 -- 懒;


没有城府和隐忍 -- 蠢;


没有规划和防线 -- 笨;


没有重心和侧重 -- 懒;


前路没好后路成 -- 贪;


虽然明知道自己有这么多缺点,但是我还是想扛着氧气罐自救一下,说不定哪天让我踩上了风口飞起来了呢?放下氧气罐,也许我再也起不来了,但扛着虽然累,好歹我还活着。


-- 来自于35岁的自白!


作者:暗黑腐竹
来源:juejin.cn/post/7321531849850945570
收起阅读 »

一个 Kotlin 开发,对于纯函数的思考

什么是纯函数? 纯函数是数学上的一种理想状态,即相同的输入永远会得到相同的输出,而且没有任何可观察的副作用 在数学上函数的定义为 It must work for every possible input value And it has only one ...
继续阅读 »

什么是纯函数?


纯函数是数学上的一种理想状态,即相同的输入永远会得到相同的输出,而且没有任何可观察的副作用


在数学上函数的定义为



  • It must work for every possible input value

  • And it has only one relationship for each input value



即每个在值域内的输入都能得到唯一的输出,它只可能是多对一而不是一对多的关系:



副作用



Wikipedia Side Effect: In computer science, an operation, function or expression is said to have a side effect if it modifies some state variable value(s) outside its local environment, which is to say if it has any observable effect other than its primary effect of returning a value to the invoker of the operation. Example side effects include modifying a non-local variable, modifying a static local variable, modifying a mutable argument passed by reference, performing I/O or calling other functions with side-effects. In the presence of side effects, a program's behaviour may depend on history; that is, the order of evaluation matters. Understanding and debugging a function with side effects requires knowledge about the context and its possible histories.



副作用的形式很多样,一切影响到外部状态、或依赖于外部状态的行为都可以称为副作用。副作用是必须的,因为程序总是不可避免的要与外界交互,如:


更改外部文件、数据库读写、用户 UI 交互、访问其他具有副作用的函数、修改外部变量


这些都可以被视为副作用,而在函数式编程中我们往往希望使副作用最小化,尽量避免副作用,对应的纯函数则是希望彻底消除副作用,因为副作用让纯函数变得不“纯”,只要一个函数还需要依赖外部状态,那么这个函数就无法始终保持同样的输入得到同样的输出。


好处是什么?



You wanted a banana but what you got was a gorilla holding the banana and the entire jungle. — Joe Armstrong, creator of Erlang progamming




  • 可缓存性,由于唯一的输入代表唯一的输出,那么这意味着我们可以在输入不变的情况下直接返回运算过的结果。

  • 高度并行,由于纯函数不依赖外部状态,因此即便在多线程情况下外部怎么变动,纯函数始终能够返回预期的值,纯函数能够达到真正的无锁编程,高度并发。

  • 高度可测性,不需要依赖外部状态,传统的 OOP 测试我们都需要模拟一个真实的环境,比如在 Android 中将 Application 模拟出来,在执行完之后断言状态的改变。而纯函数只需要模拟输入,断言输入,这是如此的简单优雅。

  • 依赖清晰,面相对象编程总需要你将整个环境初始化出来,然后函数再依赖这些状态修改状态,函数往往伴随着大量外部的隐式依赖,而纯函数只依赖输入参数,仅此而已,也仅提供返回值。


更进一步


传统大学老师教的都是 OOP,所以大多数人最开始也不会去学习纯函数的思路,但纯函数是完全不一样的一套编程思路,下面是一个纯函数中实现循环的例子,传统的循环往往是这样的:


int sum(int[] array) {
int sum = 0;
for (int i = 0; i < array.length; i++) {
sum += array[i];
}
return sum;
}

尽管大多数语言也会提供 for...in 之类的语法,如 kotlin:


fun sum(array: IntArray): Int {
var sum = 0
for (i in array) {
sum += i
}
return sum
}

但我们注意到,在上面的例子中均引入了两个可变的变量,sum 和 i,站在 sum += i 的视角,它的外部依赖:i 是一个外部的可变状态,因此这个函数并不“纯”。


但另一方面来说,从整体函数对外的视角来看,其还是很“纯”的,因为对于传入的外部 array,始终有唯一的 int 返回值,那么我们追求完全的“纯”,完全不使用可变量的目的是什么呢?


在纯函数下要实现完全消灭不可变变量,我们可以这么做:


tailrec fun sum(array: IntArray, current: Int = 0, index: Int = 0): Int {
if (index < 0 || index >= array.size) return current
return sum(array, current + array[index], index + 1)
}

我们编写了退出条件,当 index 在不正常的情况下会,意味着没有东西可以加,直接返回 current,即当前已经算好的值;其余情况则直接返回 current 与当前 index 的值的和,再加上 index + 1 之后所有值的 sum。这个例子已经很简单了,但函数式,递归思维不免会让学传统 OOP 的人需要多加思考一下。


当然作为一个 kotlin 开发,我也毫不犹豫的使用了 tailrec 这个 kotlin 语言特性来帮助优化尾递归,否则在遇到相当长的列表的时候,这个函数会抛出 StackOverFlowError。


函数一等公民


许多面向对象语言通常会用 this 显式访问对象的属性或方法,也有一些语言会省掉编写 this,事实上在许多语言编译器的背后实现中,通常也会将“对象成员的调用”变成“额外给成员函数添加一个 this 变量”。


可见发挥重要作用的其实是函数,不如更进一步,函数是一等公民,对象只不过是个结构体;如此,在纯函数中你完全用不到 this,甚至很多情况下都用不到对象。


所谓的一等公民,就是希望函数包含对象,而不是对象包含函数,甚至可以不需要对象(暴论),下面就是一个例子,是一个常见的业务诉求:



  • UserService 接收用户 id,并提供两个函数来获取用户 token 和用户的本地储存

  • ServerService 需要服务器 ip 和 port,提供通过 secret 获取 token 和通过用户 token 获取用户数据两个能力

  • UserData 是一个用户数据类,它能够接收父布局参数来构建 UI 数据用于显示


class UserService(private  val id: Int) {
fun userToken(password: String): UserToken = TODO()
fun localDb(dbPassword: String): LocalDb = TODO()
}

class ServerService(private val ip: String, private val port: Int) {
fun serverToken(serverSecret: String): ServerToken = TODO()
fun getUser(userToken: UserToken): UserData = TODO()
}

class UserData(
val name: String, val avatarUrl: String, val description: String,
) {
fun uiData(parentLayoutParameters: LayoutParameters): UIData = TODO()
}

那么这些变成函数式会怎么样呢?会像下面这样!


typealias UserTokenService = (password: String) -> UserToken
typealias LocalDbService = (dbPassword: String) -> LocalDb

typealias UserService = (id: Int) -> Pair<UserTokenService, LocalDbService>

typealias ServerTokenService = (serverSecret: String) -> ServerToken
typealias ServerUserService = (userToken: UserToken) -> UserDataAbilities
typealias ServerService = (ip: String, port: Int) -> Pair<ServerTokenService, ServerUserService>

typealias UserUIData = (parentLayoutParameters: LayoutParameters) -> UIData
typealias UserDataAbilities = UserUIData

val userService: UserService = { userId: Int ->
val tokenService: UserTokenService = { password: String -> TODO() }
val localDbService: LocalDbService = { dbPassword: String -> TODO() }
tokenService to localDbService
}

val serverService: ServerService = { ip: String, port: Int ->
val tokenService: ServerTokenService = { serverSecret: String -> TODO() }
val userService: ServerUserService = { userToken: String -> TODO() }
tokenService to userService
}

是不是看起来这些东西变得相当的复杂?但实际上真正的代码并没有写多少行,大量的代码都用来定义类型了!这也就是为什么你能看到的大多数展示函数式的例子都是用 js 去实现的,因为 js 的类型系统很弱,这样函数式写起来会很方便。


我这里用 kt 的范例则是写了大量的类型标记代码,因为我本人对显式声明类型有极高的要求,如果愿意,也可以完全将类型隐藏全靠编译器推理,就像下面这样,一切都变得简洁了,写起来和常规的 OOP 并没有太大区别。


val userService = { userId: Int ->
val tokenService = { password: String -> TODO() }
val localDbService = { dbPassword: String -> TODO() }
tokenService to localDbService
}

val serverService = { ip: String, port: Int ->
val tokenService = { serverSecret: String -> TODO() }
val userService = { userToken: String -> TODO() }
tokenService to userService
}

但不同的是,你看上面的代码,完全没有类/结构体的存在,因为变量的存储全部在函数体内储存了!


对于使用处,两种方式的用法事实上也大同小异,但可以看到我们彻底抛弃了类的存在!甚至在 kotlin 的未来版本中,如果这种代码始终在字节码中编译成 invokeDynamic,那么通过这种方式,字节码中甚至都可以避免类的存在!(当然,在 Android DEX 中会被脱糖成静态内部类)


// OOP
val userService = UserService(id = 0)
val serverService = ServerService(ip = "0.0.0.0", port = 114514)
val userToken = userService.userToken(password = "undefined")
val userData = serverService.getUser(userToken)
val uiData = userData.uiData(parentLayoutParameters)

// functional
val (userTokenService, _) = userService(0)
val (_, userDataService) = serverService("0.0.0.0", 114514)
val userToken = userTokenService("undefined")
val userData = userDataService(userToken)
val uiData = userData(parentLayoutParameters)


BTW,这里函数式 argument 没加 name 主要 kt 现在不支持。。。




柯里化


在上面的例子中,其实我们也能看到,类的存在是不必须的,类的本质其实只是预设好了一部分参数的函数,柯里化要解决的问题就是如何更轻松的实现“预设一部分参数”这样的能力。将一个函数柯里化后,允许多参数函数通过多次来进行传入,如 foo(a, b, c, d, e) 能够变成 foo(a, b)(c)(d, e) 这样的连续函数调用


在下面的例子中,我将举一个计算重量的范例:


fun weight(t: Int, kg: Int, g: Int): Int {
return t * 1000_000 + kg * 1000 + g
}

将其柯里化之后:


val weight = { t: Int ->
{ kg: Int ->
{ g: Int ->
t * 1000_000 + kg * 1000 + g
}
}
}

使用处:


// origin
weight(2, 3, 4)
// currying
weight(2)(3)(4)

在这里我们能发现,柯里化其实让实现处变复杂了,不过在 js 中通常会通过 bind 来实现,kt 也有民间大神 github 开源的柯里化库,使用这些能够从一定程度上降低编写柯里化代码的复杂度。


让我们看看 skiplang 语言吧


skiplang.com/


Skiplang 的宗旨就在其网站主页,A programming language to skip the things you have already computed,在纯函数的情况下,意味着得知输入状态,那么输出状态就是唯一确定的,这种情况就非常适合做缓存,如果输入值已经计算过,那么直接可以返回缓存的输出值。


在纯函数的情况下,意味着运算可以做到高度并行,在 skiplang 中,多个异步线程之间不允许共享可变变量,自然也不会出现异步锁等东西,从而保证了异步的绝对安全。


个人思考


纯函数的收益非常诱人,但开发者往往不喜欢使用纯函数,一些常见的原因可能是:



  1. 对性能的担忧:纯函数不允许修改变量,只允许通过 copy 等方式,创建了大量的变量;编译器需要进行激进的尾递归优化。

  2. 开发者意识淡薄:大多数学校出身的开发者只会用老师教的那一套 OOP,想培养 OOP 向函数式的转变,通常会让很多开发者感到困难,从而认为传统 OOP 简单,也是主流,没必要学新的。


尽管我对纯函数也非常的心动,但是我不是激进的纯函数派,我在日常工作中对其部分认同,具体到 kotlin 编程中,我通常坚持的理念是:



  1. 可以使用类,也可以在类中定义函数,但不允许使用可变成员。

  2. 可以使用可变的 local variable(但不推荐),但不允许在多线程之间共享

  3. 同种副作用,单一数据源。


参考



个人主页原文:一个 Kotlin 开发,对于纯函数的思考


作者:zsqw123
来源:juejin.cn/post/7321049383571046409
收起阅读 »

原来小程序分包那么简单!

web
前言 没有理论,只有实操,用最直接的方式来了解和使用小程序分包。 文章偏向使用taro来模拟小程序分包配置,在原生小程序中也是几乎差不多的配置方式。 为什么要有小程序分包? 因为上传小程序打包以后的代码包不可以超过2M。但我们在开发小程序的时候需要加载某些依赖...
继续阅读 »

前言


没有理论,只有实操,用最直接的方式来了解和使用小程序分包。


文章偏向使用taro来模拟小程序分包配置,在原生小程序中也是几乎差不多的配置方式。


为什么要有小程序分包?


因为上传小程序打包以后的代码包不可以超过2M。但我们在开发小程序的时候需要加载某些依赖或者一下静态图片,代码包难免超过2M。所以需要小程序分包功能将小程序中所有的代码分别打到不同的代码包里去,避免小程序只能上传2M的限制


目前小程序分包大小有以下限制:



  • 整个小程序所有分包大小不超过 20M(开通虚拟支付后的小游戏不超过30M)

  • 单个分包/主包大小不能超过 2M


如何对小程序进行分包?


本质上就是,配置一下app.json(小程序)或app.config.ts(Taro)中的subpackages字段。注意,分包的这个root路径和原本的pages是同级的。


如下图


image.png


这样配置好了,最基本的分包就完成了。


如何配置多个子包?


subpackages是个数组,在下面加上一样的结构就好了。


image.png
image.png


如何判断分包是否已经生效?


打开微信开发者工具,点击右上角详情 => 基本信息 => 本地代码,展开它。出现 主包,/xxxx/就是分包生效了。


如下图


image.png


所有页面都可以打到分包里面吗?


也不是,小程序规定,Tabbar页面不可以,一定需要在主包里。否则他直接报错。


分包中的依赖资源如何分配?


我们先来了解一下小程序分包资源


 引用原则
`packageA` 无法 require `packageB` JS 文件,但可以 require 主包、`packageA` 内的 JS 文件;使用 [分包异步化](https://developers.weixin.qq.com/miniprogram/dev/framework/subpackages/async.html) 时不受此条限制
`packageA` 无法 import `packageB` 的 template,但可以 require 主包、`packageA` 内的 template
`packageA` 无法使用 `packageB` 的资源,但可以使用主包、`packageA` 内的资源

原因: 分包是依赖主包运行的,所以主包是必然会被加载的,所以当分包引用主包的时候,主包的相关数据已经存在了,所以可以被引用。而分包不能引用其他分包的数据,也是因为加载顺序的问题。如果分包A引用分包B的数据,但分包B尚未被加载,则会出现引用不到数据的问题。


如果主包和分包同时使用了一个依赖,那么这个依赖会被打到哪里去?


会被打到主包


因为主包不能引用分包的资源,但是子包可以引用主包的资源,所以为了两个包都能引用到资源,只能打到主包中


比如以下情况
image.png


分包和主包同时使用了dayjs,那么这个依赖会被打入到主包中。


如果某一个依赖只在分包中使用呢?


如果某一个资源只在某一个分包中使用,那就会被打入到当前分包。


如果两个子包同时使用同一个资源呢?那资源会被打进哪里。


主包,因为两个子包的资源不能互相引用,所以与其给每一个子包都打入一个独立资源。小程序则会直接把资源打到主包中,这样,两个子包就都可以使用了。


分包需要担心低版本的兼容问题吗


不用


由微信后台编译来处理旧版本客户端的兼容,后台会编译两份代码包,一份是分包后代码,另外一份是整包的兼容代码。 新客户端用分包,老客户端还是用的整包,完整包会把各个 subpackage 里面的路径放到 pages 中。


独立分包


什么是独立分包


顾名思义,独立分包就是可以独立运行的分包。


举个例子,如果你的小程序启动页面是分包(普通分包)中的一个页面,那么小程序需要优先下载主包,然后再加载普通分包,因为普通分包依赖主包运行。但是如果小程序从独立分包进入进入小程序,则不需要下载主包,独立分包自己就可以运行。


普通分包所有的限制对独立分包都有效。


为什么要有独立分包,普通分包不够吗


因为独立分包不需要依赖主包,如果有作为打开小程序的的入口的必要,加载速度会比普通分包快,给客户的体验感更好。毕竟谁也不想打开一个页面等半天。


举个例子,如果小程序启动的时候是打开一个普通分包页面。则加载顺序是:加载主包 => 再加载当前分包


但如果小程序启动的时候是打开一个独立分包页面,则加载顺序是:直接加载独立分包,无需加载主包


独立分包相对于普通分包,就是省去了加载主包的时间和消耗。


独立分包如何配置


配置和普通分包一样,加一个independent属性设为true即可。


image.png


独立分包的缺点


既然独立分包可以不依赖主包,那我把每个分包都打成独立分包可以吗。


最好别那么干


理由有四点


1.独立分包因为不依赖主包,所以他不一定能获取到小程序层面的全局状态,比如getApp().也不是完全获取不到,主包被加载的时候还是可以获取到的。概率性出问题,最好别用。


2.独立分包不支持使用插件


3.小程序的公共文件不适用独立分包。比如Taro的app.less或小程序的app.wxss


上述三个,我觉的都挺麻烦的。所以不是作为入口包这种必要的情况下,确实没有使用独立分包的需求。


PS:一个小程序里可以有多个独立分包


独立分包有版本兼容问题吗


有滴,但你不用这个兼容问题直接让你报错
在低于 6.7.2 版本的微信中运行时,独立分包视为普通分包处理,不具备独立运行的特性。


所以,即使在低版本的微信中,也只是会编译成普通分包而已。


注意!!! 这里有一个可能会遇到的,就是如果你在独立分包中使用了app.wxss或者app.less这些小程序层面的公共css文件,那么在低版本(<6.7.2)进行兼容的时候,你就会发现,独立分包的页面会被这些全局的CSS影响。因为那时候独立分包被编译成了普通分包。而普通分包是适用全局公共文件的。


分包预下载


首先我们需要了解,分包是基本功能是,在下程序打包的时候不去加载分包,然后在进入当前分包页面的时候才开始下载分包。一方面目的是为了加快小程序的响应速度。另一方面的原因是避开微信小程序本身只能上传2M的限制。


这里有一个问题,就是我在首次跳转某个分包的某个页面的时候,出现短暂的白屏怎么办?(下载分包的时间+运行接口的时间+渲染视图的时间)。


后两者没法彻底避免,只能优化代码,第一个下载分包的时间可以使用分包预下载功能解决。


我们可以通过分包预下载在进入分包页面之前就开始下载分包,来减少进入分包页面的时间。


如何配置分包预下载


当前的分包预下载只能在app.config(Taro)或者app.json(原生小程序)通过preloadRule字段去配置。


preloadRule字段是一个对象,key是页面的路径,value是进行预加载的分包name或者key,__APP__代表主包


上案例


image.png


通过preloadRule字段去配置


”packageB/pages/user/index“是key


packages:["packageA"]是value


案例上的意思是当进入packageA分包的时候,开始下载分包packageB


如果要某一个分包在加载主包的就开始下载,那么就设置packages:["APP"]即可。


总结



  1. 分包是为了解决小程序超过2m无法上传的问题

  2. 分包依赖于主包,进入分包页面,主包必然需要优先被加在

  3. 主包和分包同时引用一个依赖或资源,则当前依赖或资源会被打入到主包

  4. 两个分包使用了同一个依赖或资源,则该依赖和资源会被打入到主包

  5. 某资源或依赖只在某一个分包中使用,则该资源和依赖会被打入到该分包中

  6. 独立分包的配置相对于普通分包只是多了一个independent字段,设置为true

  7. 独立分包无需依赖主包,可独立加载。

  8. 独立分包中谨慎使用全局属性,最好别用,可能获取不到

  9. 分包可以被预加载,用于解决进入分包页面时才开始加载分包导致页面可能出现的(取决于加载速度)短暂白屏的问题。


分包官方文档


分包官方分包demo-小程序版


如果您认为对您有用的话,留个赞或收藏一下吧~


image.png


作者:工边页字
来源:juejin.cn/post/7321049399281958922
收起阅读 »

现代 CSS 解决方案:文字颜色自动适配背景色!

web
在 23 年的 CSS 新特性中,有一个非常重要的功能更新 -- 相对颜色。简单而言,相对颜色的功能,让我们在 CSS 中,对颜色有了更为强大的掌控能力。其核心功能就是,让我们能够基于一个现有颜色 A,通过一定的转换规则,快速生成我们想要的颜色 B。...
继续阅读 »

在 23 年的 CSS 新特性中,有一个非常重要的功能更新 -- 相对颜色

简单而言,相对颜色的功能,让我们在 CSS 中,对颜色有了更为强大的掌控能力。

其核心功能就是,让我们能够基于一个现有颜色 A,通过一定的转换规则,快速生成我们想要的颜色 B

其功能能够涵盖:

完整的教程,你可以看这里 -- Chrome for Developers- CSS 相对颜色语法

当然,今天我们不会一个一个去过这些功能,更多的时候,我们只需要知道我们能够实现这些功能。

本文,我们将从实际实用角度出发,基于实际的案例,看看 CSS 相对颜色,能够如何解决我们的一些实际问题。

快速语法入门

首先,我们通过一张图,一个案例,快速入门 CSS 相对颜色语法:

相对颜色语法的目标是允许从另一种颜色派生颜色。

上图显示了将原始颜色 green 转换为新颜色的颜色空间后,该颜色会转换为以 r、g、b 和 alpha 变量表示的各个数字,这些数字随后会直接用作新的 rgb() 颜色的值。

举个例子:

<p> CSS Relative Color p>
p {
color: rgb(255, 0, 0);
}

实现一个 color 为红色(rgb 值为 rgb(255, 0, 0))的字体:

基于上面的相对颜色语法,我如何通过一个红色生成绿色文字呢?示意如下:

p {
--color: rgb(255, 0, 0);
color: rgb(from var(--color) calc(r - 255) calc(g + 255) b); /* result = rgb(0, 255, 0) */
}

效果如下,我们就得到绿色字体:

解释一下:

  1. 原本的红色颜色,我们把它设置为 CSS 变量 --color: rgb(255, 0, 0)
  2. 想通过红色得到绿色,对于红色的 rgb 值 rgb(255, 0, 0) 而言,需要转换成 rgb(0, 255, 0)
  3. 使用 CSS 相对颜色语法,就是 rgb(from var(--color) calc(r - 255) calc(g + 255) b)

通过这个 DEMO,我们把几个核心基础语法点学习一下:

  1. from 关键字

from 关键字,它是相对颜色的核心。它表示会将 from 关键字后的颜色定义转换为相对颜色!在 from 关键字后面,CSS 会期待一种颜色,即能够启发生成另一种颜色

  1. from 关键字 后的颜色表示,支持不同颜色表示或者是 CSS 变量

第二个关键点,from 后面通常会接一个颜色值,这个颜色值可以是任意颜色表示法,或者是一个 CSS 变量,下面的写法都是合法的:

p {
color: rgba(from #ff0000) r g b);
color: rgb(from rgb(255, 0, 0) r g b);
color: rgb(from hsl(0deg, 100%, 50%) r g b);
color: rgb(from var(--hotpink) r g b);
}
  1. 对转换后的变量使用 calc() 或其他 CSS 函数

另外一个非常重要的基础概念就是,我们可以对 (from color r g b) 后的转换变量 r g b 使用 calc() 或其他 CSS 函数。

就是我们上面的例子:

p {
--color: rgb(255, 0, 0);
color: rgb(from var(--color) calc(r - 255) calc(g + 255) b); /* result = rgb(0, 255, 0) */
}
  1. 相对颜色语法支持,各种颜色表示函数:

相对颜色的基础的使用规则就是这样,它不仅支持 rgb 颜色表示法,它支持所有的颜色表示法:

使用 CSS 相对颜色,实现统一按钮点击背景切换

通常页面上的按钮,都会有 hover/active 的颜色变化,以增强与用户的交互。

像是这样:

最常见的写法,就是我们需要在 Normal 状态、Hover 状态、Active 状态下写 3 种颜色:

p {
color: #ffcc00;
transition: .3s all;
}
/* Hover 伪类下为 B 颜色 */
p:hover {
color: #ffd21f;
}
/** Active 伪类下为 C 颜色 **/
p:active {
color: #ab8a05;
}

在之前,我们介绍过一种利用滤镜 filter: contrast() 或者 filter: brightness() 的统一解决方案,无需写多个颜色值,可以根据 Normal 状态下的色值,通过滤镜统一实现更亮、或者更暗的伪类颜色。

在今天,我们也可以利用 CSS 相对颜色来做这个事情:

div {
--bg: #fc0;
background: var(--bg);
transition: .3s all;
}

div:hover {
background: hsl(from var(--bg) h s calc(l * 1.2));
}
div:active {
background: hsl(from var(--bg) h s calc(l * 0.8));
}

我们通过 hsl 色相、饱和度、亮度颜色表示法表示颜色。实现:

  1. 在 :hover 状态下,根据背景色,将背景亮度 l 调整为原背景色的 1.2 倍
  2. 在 :avtive 状态下,根据背景色,将背景亮度 l 调整为原背景色的 0.8 倍

在实际业务中,这是一个非常有用的用法。

完整的 DEMO,你可以戳这里:CodePen Demo -- https://codepen.io/Chokcoco/pen/KKEdOeb

使用 CSS 相对颜色,实现文字颜色自适应背景

相对颜色,还有一个非常有意思的场景 -- 让文字颜色能够自适应背景颜色进行展示。

有这么一种场景,有的时候,无法确定文案的背景颜色的最终表现值(因为背景颜色的值可能是后台配置,通过接口传给前端),但是,我们又需要能够让文字在任何背景颜色下都正常展现(譬如当底色为黑色时文字应该是白色,当背景为白色时,文字应该为黑色)。

像是这样:

在不确定背景颜色的情况下,无论什么情况,文字颜色都能够适配背景的颜色。

在之前,纯 CSS 没有特别好的方案,可以利用 mix-blend-mode: difference 进行一定程度的适配:

div {
// 不确定的背景色
}
p {
color: #fff;
mix-blend-mode: difference;
}

实操过这个方案的同学都会知道,在一定情况下,前景文字颜色还是会有一点瑕疵。并且,混合模式这个方案最大的问题是会影响清晰度

有了 CSS 相对颜色后,我们有了更多的纯 CSS 方案。

利用 CSS 相对颜色,反转颜色

我们可以利用相对颜色的能力,基于背景色颜色进行反转,赋值给 color。

一种方法是将颜色转换为 RGB,然后从 1 中减去每个通道的值。

代码非常简单:

p {
/** 任意背景色 **/
--bg: #ffcc00;
background: var(--bg);

color: rgb(from var(--bg) calc(1 - r) calc(1 - g) calc(1 - b)); /** 基于背景反转颜色 **/
}

用 1 去减,而不是用 255 去,是因为此刻,会将 rgb() 表示法中的 0~255 映射到 0~1

效果如下:

配个动图,我们利用背景色的反色当 Color 颜色,适配所有背景情况:

完整的 DEMO 和代码,你可以戳这里:CodePen Demo -- CSS Relatvie Color Adapt BG

当然,这个方案还有两个问题:

  1. 如果颜色恰好是在 #808080 灰色附近,它的反色,其实还是它自己!会导致在灰色背景下,前景文字不可见;
  2. 绝大部分情况虽然可以正常展示,但是并不是非常美观好看

为了解决这两个问题,CSS 颜色规范在 CSS Color Module Level 6 又推出了一个新的规范 -- color-contrast()

利用 color-contrast(),选择高对比度颜色

color-contrast() 函数标记接收一个 color 值,并将其与其他的 color 值比较,从列表中选择最高对比度的颜色。

利用这个 CSS 颜色函数,可以完美的解决上述的问题。

我们只需要提供 #fff 白色和 #000 黑色两种可选颜色,将这两种颜色和提供的背景色进行比较,系统会自动选取对比度更高的颜色。

改造一下,上面的代码,它就变成了:

p {
/** 任意背景色 **/
--bg: #ffcc00;
background: var(--bg);

color: color-contrast(var(--bg) vs #fff, #000); /** 基于背景色,自动选择对比度更高的颜色 **/
}

这样,上面的 DEMO 最终效果就变成了:

完整的 DEMO 和代码,你可以戳这里:CodePen Demo -- CSS Relatvie Color Adapt BG

此方案的优势在于:

  1. 可以限定前景 color 颜色为固定的几个色值,以保证 UI 层面的统一及美观
  2. 满足任何情况下的背景色

当然,唯一限制这个方案的最大问题在于,当前,color-contrast 还只是一个实验室功能,未大规模被兼容。

总结一下

到今天,我们可以利用 CSS 提供的各类颜色函数,对颜色有了更为强大的掌控力。

很多交互效果,不借助 JavaScript 的运算,也能计算出我们想要的最终颜色值。本文简单的借助:

  1. 使用 CSS 相对颜色,实现统一按钮点击背景切换
  2. 使用 CSS 相对颜色,实现文字颜色自适应背景

两个案例,介绍了 CSS 相对颜色的功能。但它其实还有更为广阔的应用场景,完整的教程,你可以看这里 -- Chrome for Developers- CSS 相对颜色语法

最后

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

想 Get 到最有意思的 CSS 资讯,千万不要错过我的公众号 -- iCSS前端趣闻 😄

更多精彩 CSS 技术文章汇总在我的 Github -- iCSS ,持续更新,欢迎点个 star 订阅收藏。

如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。


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

产品经理:“一个简单的复制功能也能写出bug?”

web
问题 刚入职时,遇到了一个线上 bug,用户点击复制按钮没办法复制文本,产品经理震怒,“这么简单的一个功能也能出问题?当时是谁验收的?”,因为我刚来还闲着,就把我派去解决这个问题。 我在排查问题时,发现该复制方法写在了一个自定义 hook 中(这里复制方法写在...
继续阅读 »

问题


刚入职时,遇到了一个线上 bug,用户点击复制按钮没办法复制文本,产品经理震怒,“这么简单的一个功能也能出问题?当时是谁验收的?”,因为我刚来还闲着,就把我派去解决这个问题。


我在排查问题时,发现该复制方法写在了一个自定义 hook 中(这里复制方法写在 hook 里没啥意义,但是乙方交付过来的代码好像特别喜欢把工具函数写成个 hook 来用),点进去查看就是简单的一个 navigator.clipboard.writeText()的方法,本地运行我又能复制成功。于是我怀疑是手机浏览器不支持这个 api 便去搜索了一下。


Clipboard


MDN 上的解释:


剪贴板 Clipboard APINavigator 接口添加了只读属性 clipboard,该属性返回一个可以读写剪切板内容的 Clipboard 对象。在 Web 应用中,剪切板 API 可用于实现剪切、复制、粘贴的功能。


只有在用户事先授予网站或应用对剪切板的访问许可之后,才能使用异步剪切板读写方法。许可操作必须通过取得权限 Permissions API (en-US)"clipboard-read" 和/或 "clipboard-write" 项获得。


浏览器兼容性


image.png


使用 document.execCommand() 降级处理


这里我也不清楚用户手机浏览器的版本是多少,那么这个 api 出现之前,是用的什么方法呢?总是可以 polyfill 降级处理的吧!于是我就查到了document.execCommand()这个方法:



  • document.execCommand("copy") : 复制;

  • document.execCommand("cut") : 剪切;

  • document.execCommand("paste") : 粘贴。


对比


Clipboard 的所有方法都是异步的,返回 Promise 对象,复制较大数据时不会造成页面卡顿。但是其支持的浏览器版本较新,且只允许 https 和 localhost 这些安全网络环境可以使用,限制较多。


document.execCommand() 限制较少,使用起来相对麻烦。但是 MDN 上提到该 api 已经废弃:


image.png


image.png


浏览器很可能在某个版本弃用该 api ,不过当前 2023/12/29 ,该复制 api 还是可以正常使用的。


具体代码修改


于是我修改了一下原来的 hook:


import Toast from "~@/components/Toast";

export const useCopy = () => {

const copy = async (text: string, toast?: string) => {

const fallbackCopyTextToClipboard = (text: string, toast?: string) => {
let textArea = document.createElement("textarea");
textArea.value = text;

// Avoid scrolling to bottom
textArea.style.top = "-200";
textArea.style.left = "0";
textArea.style.position = "fixed";
textArea.style.opacity = "0"

document.body.appendChild(textArea);
// textArea.focus();
textArea.select();
let msg;
try {
let successful = document.execCommand("copy");
msg = successful ? toast ? toast : "复制成功" : "复制失败";
} catch (err) {
msg = "复制失败";
}
Toast.dispatch({
content: msg,
});
document.body.removeChild(textArea);
};

const copyTextToClipboard = (text: string, toast?: string) => {
if (!navigator.clipboard || !window.isSecureContext) {
fallbackCopyTextToClipboard(text, toast);
return;
}
navigator.clipboard
.writeText(text)
.then(() => {
Toast.dispatch({
content: toast ? toast : "复制成功",
});
})
.catch(() => {
fallbackCopyTextToClipboard(text, toast)
});
};
copyTextToClipboard(text, toast);
};

return copy;
};

上线近一年,这个复制方法没出现异常问题。


作者:HyaCinth
来源:juejin.cn/post/7317577665014448167
收起阅读 »

高管违法开除我的一些想法

昨天(1月7日),公历年初,农历临近过年。这本应大家辛苦了一年,进行年会、团聚、收款,盼望着领取薪资,与远在千里的家人团聚的日子。 然而现实是,被井某指着鼻子威胁骂到: 我等劳动局的! 我现在就违法解除! 我告诉你了,我现在就违法解除! 你试试看,你他妈两年...
继续阅读 »

昨天(1月7日),公历年初,农历临近过年。这本应大家辛苦了一年,进行年会、团聚、收款,盼望着领取薪资,与远在千里的家人团聚的日子。


然而现实是,被井某指着鼻子威胁骂到:



  • 我等劳动局的!

  • 我现在就违法解除!

  • 我告诉你了,我现在就违法解除!

  • 你试试看,你他妈两年半找不着工作!你试试看!

  • 劳动仲裁一审二审,我有的是人!我慢慢等你,你试试看。

  • 你敢上传一个试试,我现在就打110!


事件和视频


原视频很大媒体平台都有,包括百度、抖音等。



本来想知道到底是什么原因,视频里的女高管如此有恃无恐,一时不知道到底是谁违法了。难道是因为孙某有什么错误,导致她认为就算是自己根据没有违法,所以法律拿她无可奈何,或者是她认为违法了又如何?


但是随着视频的播放,她放话:劳动仲裁一审二审,我有的是人!我慢慢等你,你试试看……,从这里看来意思是说假设真的是官司打起来了,我有是办法(让你时间耗不起、工作找不起)。虽然孙某有在拍视频,她也知道有在拍,但依然如此出言不逊,甚至你敢上传一个试试


当晚,此视频在和微信群被转发,各在文章平台、短视频平台迅速跟进,次日,名为北京尼欧XX科技有限公司发表了声明。


公司声明


image.png


如果只有视频,确实也不知事情原委(万一高管只是在视频里说说玩,其实是孙某违法了呢?),所以就没继续想些什么。但次日看到这个声明之后,从声明上看:高管以停职反省处理、孙某以足额支付补偿金处理,并强调章程均合法合规。


假设此声明没有问题,我有以下疑问:



  • 员工是否真是不能通过试用期


据我了解,假设试用期为6个月,一般公司会在第二三个月就会有相关的述职会议,以评估你是否能胜任工作。也就是说,能不能胜任工作,能不能通过试用期,通常2-3个月就能知晓了。但是为什么要在最后一个月才因某能力不足裁员呢?半年的时候,普通项目都做得差不多了。



  • 视频中的日期是什么时候


视频是在1月7号流传的。如果是热点事件,通常在一天内基本大家都能知道了。


但声明上说是根据12月8日足额支付补偿,依法合规,也就是说一个月之前就已经合规处理完此事了?


个人想法


大家有知道马云说了离职不是钱不够,就是心受委屈了。虽然有比较多的人补充说还有其他自己想走之类的原因云云。


但细究的话,各有各的原因,这就不便分析问题了。


总的说来,不管是自己离职,还是公司裁员,应该都能归纳于:愿意的、不愿意的。


这东西,就像是谈恋爱一样,如果双方不喜欢,不愿意,或开始愿意,后面不愿意了,终究就会产生破窗效应,最终摆烂或分道扬镳。


如果发现员工能力不足,或公司运营困难了,需要裁员时,应早日给出处理方案,例如培训、转岗、或直言等,从双方平等的角度获得对方的理解。


如果员工认为公司有哪些地方不合理,也早点提出相关方案,为什么不喜欢?有没有建议方案?尝试过哪些努力?比如公司人员结构、工作强度、代码可维护性……提出来看看,假设表达合理,公司也重视你,自然能给你相应的说法。


如果公司的解决问题的方案是,解决提出问题的人,那早点离开又何尝不对呢?说小一点,这是为了自己洒脱一点,说大一点,这是人择良友而交,禽择良木而栖,让环境越来越好。


当然,很多时候作为人确实也是身不由己,太多羁绊。但是有没有认真考虑过,有的东西是值得的吗?当发现不值得的时候,自己还有退路吗?是健康快乐更重要还是别人的看法更重要?


如何维护自身利益


那么,作为一个员工,我们应如何保护自己?不一定解决问题,但可能解决问题。


作为员工如何维护自己的利益


搞清楚劳动法和公司规定,知道自己有什么权利和责任。了解工资、工时、福利、休假等方面的规定,确保公司别违法。留着跟工作有关的文件,合同、工资单、绩效评估之类的记录。这些东西能当证据,帮你维护自己的权益。


如果有问题或烦恼,及时跟相关人员沟通,提供明确的事实和证据。参与公司的反馈机制,提建议和意见。继续学习,提升自己的技能,增加竞争力。参加培训课程、专业发展计划,提高职业能力和知识水平。


关注职业发展机会和市场趋势,找适合自己的发展方向。积极参与职业培训、跨部门项目之类的,提高竞争力。
平衡工作和个人生活的需求,保持身心健康。合理安排工作时间和休息时间,别太累和压力太大


被违法裁员时应如何处理


搞明白劳动法和规定,尤其是关于裁员的规定。这样你就知道自己有什么权利,雇主有什么责任。收集跟裁员有关的所有证据,比如裁员通知、合同、工资单、绩效评估、公司规定之类的文件。这些东西在后面的法律行动中可能很重要。


找专业的劳动法律顾问或律师咨询,让他们给你解释权益和法律选项。他们能帮你评估情况,提供适当的建议和法律支持。跟雇主沟通,表达你对裁员决定的担忧和不满。写份申诉信或要求重新考虑决定。有时候,通过沟通和谈判,可能会找到解决问题的办法。


根据当地的法律程序,你还有机会通过调解或仲裁来解决争议。律师会给你专业的法律建议,并在法庭上代表你维护权益。


最后,为勇敢维护自身利益的人们点赞!


相关信息



作者:四叶草会开花
来源:juejin.cn/post/7320959103932989451
收起阅读 »

环信Web端IM Demo登录方式如何修改

在环信即时通讯云IM 官网下载Demo,本地运行只有手机+验证码的方式登录?怎么更改为自己项目的Appkey和用户去进行登录呢?👇👇👇本文以Web端为例,教大家如何更改代码来实现1、 VUE2 Demovue2 demo源码下载vue2 demo线上...
继续阅读 »

在环信即时通讯云IM 官网下载Demo,本地运行只有手机+验证码的方式登录?怎么更改为自己项目的Appkey和用户去进行登录呢?

👇👇👇本文以Web端为例,教大家如何更改代码来实现

1、 VUE2 Demo

第一步:更改appkey

webim-vue-demo===>src===>utils===>WebIMConfig.js
bcf8e92b4df988de5df647cddd0b8ce5.png

第二步:更改代码

webim-vue-demo===>src===>pages===>login===>index.vue

<template>
<a-layout>
<div class="login">
<div class="login-panel">
<div class="logo">Web IM</div>
<a-input v-model="username" :maxLength="64" placeholder="用户名" />
<a-input v-model="password" :maxLength="64" v-on:keyup.13="toLogin" type="password" placeholder="密码" />
<a-input v-model="nickname" :maxLength="64" placeholder="昵称" v-show="isRegister == true" />

<a-button type="primary" @click="toRegister" v-if="isRegister == true">注册</a-button>
<a-button type="primary" @click="toLogin" v-else>登录</a-button>
</div>
<p class="tip" v-if="isRegister == true">
已有账号?
<span class="green" v-on:click="changeType">去登录</span>
</p>
<p class="tip" v-else>
没有账号?
<span class="green" v-on:click="changeType">注册</span>
</p>

<!-- <div class="login-panel">
<div class="logo">Web IM</div>
<a-form :form="form" >
<a-form-item has-feedback>
<a-input
placeholder
="手机号码"
v
-decorator="[
'phone',
{
rules: [{ required: true, message: 'Please input your phone number!' }],
},
]"
style
="width: 100%"
>
<a-select
initialValue
="86"
slot
="addonBefore"
v
-decorator="['prefix', { initialValue: '86' }]"
style
="width: 70px"
>
<a-select-option value="86">
+86
</a-select-option>
</a-select>
</a-input>
</a-form-item>

<a-form-item>
<a-row :gutter="8">
<a-col :span="14">
<a-input
placeholder
="短信验证码"
v
-decorator="[
'captcha',
{ rules: [{ required: true, message: 'Please input the captcha you got!' }] },
]"
/>
</a-col>
<a-col :span="10">
<a-button v-on:click="getSmsCode" class="getSmsCodeBtn">{{btnTxt}}</a-button>
</a-col>
</a-row>
</a-form-item>
<a-button style="width: 100%" type="primary" @click="toLogin" class="login-rigester-btn">登录</a-button>

</a-form> -->
<!-- </div> -->
</div>
</a-layout>
</template>

<script>
import './index.less';
import { mapState, mapActions } from 'vuex';
import axios from 'axios'
import { Message } from 'ant-design-vue';
const domain = window.location.protocol+'//a1.easemob.com'
const userInfo = localStorage.getItem('userInfo') && JSON.parse(localStorage.getItem('userInfo'));
let times = 60;
let timer
export default{
data(){
return {
username: userInfo && userInfo.userId || '',
password: userInfo && userInfo.password || '',
nickname: '',
btnTxt: '获取验证码'
};
},
beforeCreate() {
this.form = this.$form.createForm(this, { name: 'register' });
},
mounted: function(){
const path = this.isRegister ? '/register' : '/login';

if(path !== location.pathname){
this.$router.push(path);
}
if(this.isRegister){
this.getImageVerification()
}
},
watch: {
isRegister(result){
if(result){
this.getImageVerification()
}
}
},
components: {},
computed: {
isRegister(){
return this.$store.state.login.isRegister;
},
imageUrl(){
return this.$store.state.login.imageUrl
},
imageId(){
return this.$store.state.login.imageId
}
},
methods: {
...mapActions(['onLogin', 'setRegisterFlag', 'onRegister', 'getImageVerification', 'registerUser', 'loginWithToken']),
toLogin(){
this.onLogin({
username: this.username.toLowerCase(),
password: this.password
});
// const form = this.form;
// form.validateFields(['phone', 'captcha'], { force: true }, (err, value) => {
// if(!err){
// const {phone, captcha} = value
// this.loginWithToken({phone, captcha})
// }
// });
},
toReset(){
this.$router.push('/resetpassword')
},
toRegister(e){
e
.preventDefault(e);
// this.form.validateFieldsAndScroll((err, values) => {
// if (!err) {
// this.registerUser({
// userId: values.username,
// userPassword: values.password,
// phoneNumber: values.phone,
// smsCode: values.captcha,
// })
// }
// });

this.onRegister({
username: this.username.toLowerCase(),
password: this.password,
nickname: this.nickname.toLowerCase(),
});
},
changeType(){
this.setRegisterFlag(!this.isRegister);
},
getSmsCode(){
if(this.$data.btnTxt != '获取验证码') return
const form = this.form;
form
.validateFields(['phone'], { force: true }, (err, value) => {
if(!err){
const {phone, imageCode} = value
this.getCaptcha({phoneNumber: phone, imageCode})
}
});
},
getCaptcha(payload){
const self = this
const imageId = this.imageId
axios
.post(domain+`/inside/app/sms/send/${payload.phoneNumber}`, {
phoneNumber: payload.phoneNumber,
})
.then(function (response) {
Message
.success('短信已发送')
self
.countDown()
})
.catch(function (error) {
if(error.response && error.response.status == 400){
if(error.response.data.errorInfo == 'Image verification code error.'){
self
.getImageVerification()
}
if(error.response.data.errorInfo == 'phone number illegal'){
Message
.error('请输入正确的手机号!')
}else if(error.response.data.errorInfo == 'Please wait a moment while trying to send.'){
Message
.error('你的操作过于频繁,请稍后再试!')
}else if(error.response.data.errorInfo.includes('exceed the limit')){
Message
.error('获取已达上限!')
}else{
Message
.error(error.response.data.errorInfo)
}
}
});
},
countDown(){
this.$data.btnTxt = times
timer
= setTimeout(() => {
this.$data.btnTxt--
times
--
if(this.$data.btnTxt === 0){
times
= 60
this.$data.btnTxt = '获取验证码'
return clearTimeout(timer)
}
this.countDown()
}, 1000)
}
}
};
</script>

webim-vue-demo===>src===>store===>login.js
只用更改actions下的onLogin,其余不用动

onLogin: function(context, payload){
context.commit('setUserName', payload.username);
let options = {
user: payload.username,
pwd: payload.password,
appKey: WebIM.config.appkey,
apiUrl: 'https://a1.easecdn.com'
};
WebIM.conn.open(options).then((res)=>{
localStorage.setItem('userInfo', JSON.stringify({ userId: payload.username, password: payload.password,accessToken:res.accessToken}));
});

},

2、VUE3 DEMO:

第一步:更改appkey

webim-vue-demo===>src===>IM===>config===>index.js

c27eca4aefd5861bb4014d86d7b080de.png

第二步:更改代码

webim-vue-demo===>src===>views===>Login===>components===>LoginInput===>index.vue

<script setup>
import { ref, reactive, watch, computed } from 'vue'
import { ElMessage } from 'element-plus'
import { EaseChatClient } from '@/IM/initwebsdk'
import { handleSDKErrorNotifi } from '@/utils/handleSomeData'
import { fetchUserLoginSmsCode, fetchUserLoginToken } from '@/api/login'
import { useStore } from 'vuex'
import { usePlayRing } from '@/hooks'
const store = useStore()
const loginValue = reactive({
phoneNumber: '',
smsCode: ''
})
const buttonLoading = ref(false)
//根据登陆初始化一部分状态
const loginState = computed(() => store.state.loginState)
watch(loginState, (newVal) => {
if (newVal) {
buttonLoading
.value = false
loginValue
.phoneNumber = ''
loginValue
.smsCode = ''
}
})
const rules = reactive({
phoneNumber: [
{ required: true, message: '请输入手机号', trigger: 'blur' },
{
pattern: /^1[3-9]\d{9}$/,
message: '请输入正确的手机号',
trigger: ['blur', 'change']
}
],
smsCode: [
{
required: true,
message: '请输入短信验证码',
trigger: ['blur', 'change']
}
]
})
//登陆接口调用
const loginIM = async () => {
const { clickRing } = usePlayRing()
clickRing()
buttonLoading
.value = true
/* SDK 登陆的方式 */
try {
let { accessToken } = await EaseChatClient.open({
user: loginValue.phoneNumber.toLowerCase(),
pwd: loginValue.smsCode.toLowerCase(),
});
window
.localStorage.setItem(`EASEIM_loginUser`, JSON.stringify({ user: loginValue.phoneNumber, accessToken: accessToken }))
} catch (error) {
console
.log('>>>>登陆失败', error);
const { data: { extraInfo } } = error
handleSDKErrorNotifi(error.type, extraInfo.errDesc);
loginValue
.phoneNumber = '';
loginValue
.smsCode = '';
}
finally {
buttonLoading
.value = false;
}
/* !环信后台接口登陆(仅供环信线上demo使用!) */
// const params = {
// phoneNumber: loginValue.phoneNumber.toString(),
// smsCode: loginValue.smsCode.toString()
// }
// try {
// const res = await fetchUserLoginToken(params)
// if (res?.code === 200) {
// console.log('>>>>>>登陆token获取成功', res.token)
// EaseChatClient.open({
// user: res.chatUserName.toLowerCase(),
// accessToken: res.token
// })
// window.localStorage.setItem(
// 'EASEIM_loginUser',
// JSON.stringify({
// user: res.chatUserName.toLowerCase(),
// accessToken: res.token
// })
// )
// }
// } catch (error) {
// console.log('>>>>登陆失败', error)
// if (error.response?.data) {
// const { code, errorInfo } = error.response.data
// if (errorInfo.includes('does not exist.')) {
// ElMessage({
// center: true,
// message: `用户${loginValue.username}不存在!`,
// type: 'error'
// })
// } else {
// handleSDKErrorNotifi(code, errorInfo)
// }
// }
// } finally {
// buttonLoading.value = false
// }
}
/* 短信验证码相关 */
const isSenedAuthCode = ref(false)
const authCodeNextCansendTime = ref(60)
const sendMessageAuthCode = async () => {
const phoneNumber = loginValue.phoneNumber
try {
await fetchUserLoginSmsCode(phoneNumber)
ElMessage({
type: 'success',
message: '验证码获取成功!',
center: true
})
startCountDown()
} catch (error) {
ElMessage({ type: 'error', message: '验证码获取失败!', center: true })
}
}
const startCountDown = () => {
isSenedAuthCode
.value = true
let timer = null
timer
= setInterval(() => {
if (
authCodeNextCansendTime
.value <= 60 &&
authCodeNextCansendTime
.value > 0
) {
authCodeNextCansendTime
.value--
} else {
clearInterval(timer)
timer
= null
authCodeNextCansendTime
.value = 60
isSenedAuthCode
.value = false
}
}, 1000)
}
</script>

<template>
<el-form :model="loginValue" :rules="rules">
<el-form-item prop="phoneNumber">
<el-input
class="login_input_style"
v
-model="loginValue.phoneNumber"
placeholder
="手机号"
clearable
/>
</el-form-item>
<el-form-item prop="smsCode">
<el-input
class="login_input_style"
v
-model="loginValue.smsCode"
placeholder
="请输入短信验证码"
>
<template #append>
<el-button
type
="primary"
:disabled="loginValue.phoneNumber && isSenedAuthCode"
@
click
="sendMessageAuthCode"
v
-text="
isSenedAuthCode
? `${authCodeNextCansendTime}S`
: '获取验证码'
"

></el-button>
</template>
</el-input>
</el-form-item>
<el-form-item>
<div class="function_button_box">
<el-button
v
-if="loginValue.phoneNumber && loginValue.smsCode"
class="haveValueBtn"
:loading="buttonLoading"
@
click
="loginIM"
>登录</el-button
>
<el-button v-else class="notValueBtn">登录</el-button>
</div>
</el-form-item>
</el-form>
</template>

<style lang="scss" scoped>
.login_input_style {
margin: 10px 0;
width: 400px;
height: 50px;
padding: 0 16px;
}

::v-deep .el-input__inner {
padding: 0 20px;
font-style: normal;
font-weight: 400;
font-size: 14px;
line-height: 20px;
letter-spacing: 1.75px;
color: #3a3a3a;

&::placeholder {
font-family: 'PingFang SC';
font-style: normal;
font-weight: 400;
font-size: 14px;
line-height: 20px;
/* identical to box height */
letter-spacing: 1.75px;
color: #cccccc;
}
}

::v-deep .el-input__suffix-inner {
font-size: 20px;
margin-right: 15px;
}

::v-deep .el-form-item__error {
margin-left: 16px;
}

::v-deep .el-input-group__append {
background: linear-gradient(90deg, #04aef0 0%, #5a5dd0 100%);
width: 60px;
color: #fff;
border: none;
font-weight: 400;

button {
font-weight: 300;
}
}

.login_text {
font-family: 'PingFang SC';
font-style: normal;
font-weight: 400;
font-size: 12px;
line-height: 17px;
text-align: right;

.login_text_isuserid {
display: inline-block;
// width: 100px;
color: #f9f9f9;
}

.login_text_tologin {
margin-right: 20px;
width: 80px;
color: #05b5f1;
cursor: pointer;

&:hover {
text-decoration: underline;
}
}
}

.function_button_box {
margin-top: 10px;
width: 400px;

button {
margin: 10px;
width: 380px;
height: 50px;
border-radius: 57px;
}

.haveValueBtn {
background: linear-gradient(90deg, #04aef0 0%, #5a5dd0 100%);
border: none;
font-weight: 300;
font-size: 17px;
color: #f4f4f4;

&:active {
background: linear-gradient(90deg, #0b83b2 0%, #363df4 100%);
}
}

.notValueBtn {
border: none;
font-weight: 300;
font-size: 17px;
background: #000000;
mix-blend-mode: normal;
opacity: 0.3;
color: #ffffff;
cursor: not-allowed;
}
}
</style>

3、React DEMO:

第一步:更改appkey

webim-dev===>demo===>src===>config===>WebIMConfig.js
c23c60aa28a75ddd2c743497453736d2.png

第二步:更改代码

webim-dev===>demo===>src===>config===>WebIMConfig.js
将usePassword改为true

6a1f227099bd124db2a2a4611e5bbc4b.png

4、Uniapp Demo:

第一步:更改appkey

uniapp vue2 demo
webim-uniapp-demo===>utils===>WebIMConfig.js

a1041f6e463396da3db74110f5477863.png

uniapp vue3 demo
webim-uniapp-demo===>EaseIM===>config===>index.js

7fc3d3d4077b50be4fdba689acb2f9a4.png

第二步:更改代码

webim-uniapp-demo===>pages===>login===>login.vue

c18a811eba8546dc53bebca8fa0b31da.png

5、微信小程序 Demo:

第一步:更改appkey

webim-weixin-demo===>src===>utils===>WebIMConfig.js

109f65d6690a9ace085c47e9b5e9eb49.png

第二步:更改代码

webim-weixin-demo===>src===>pages===>login===>login.wxml

<import src="../../comps/toast/toast.wxml" />
<view class="login">
<view class="login_title">
<text bindlongpress="longpress">登录</text>
</view>

<!-- 测试用 请忽略 -->
<view class="config" wx:if="{{ show_config }}">
<view>
<text>使用沙箱环境</text>
<switch class="config_swich" checked="{{isSandBox? true: false}}" color="#0873DE" bindchange="changeConfig" />
</view>
</view>

<view class="login_user {{nameFocus}}">
<input type="text" placeholder="请输入用户名" placeholder-style="color:rgb(173,185,193)" bindinput="bindUsername" bindfocus="onFocusName" bindblur="onBlurName" />
</view>
<view class="login_pwd {{psdFocus}}">
<input type="text" password placeholder="用户密码" placeholder-style="color:rgb(173,185,193)" bindinput="bindPassword" bindfocus="onFocusPsd" bindblur="onBlurPsd"/>
</view>
<view class="login_btn">
<button hover-class="btn_hover" bind:tap="login">登录</button>
</view>
<template is="toast" data="{{ ..._toast_ }}"></template>
</view>

webim-weixin-demo===>src===>pages===>login===>login.js

let WebIM = require("../../utils/WebIM")["default"];
let __test_account__, __test_psword__;
let disp = require("../../utils/broadcast");

let runAnimation = true
Page({
data: {
name: "",
psd: "",
grant_type: "password",
rtcUrl: '',
show_config: false,
isSandBox: false
},

statechange(e) {
console.log('live-player code:', e.detail.code)
},

error(e) {
console.error('live-player error:', e.detail.errMsg)
},

onLoad: function(option){
const me = this;
const app = getApp();
new app.ToastPannel.ToastPannel();

disp.on("em.xmpp.error.passwordErr", function(){
me.toastFilled('用户名或密码错误');
});
disp.on("em.xmpp.error.activatedErr", function(){
me.toastFilled('用户被封禁');
});

wx.getStorage({
key: 'isSandBox',
success (res) {
console.log(res.data)
me.setData({
isSandBox: !!res.data
})
}
})

if (option.username && option.password != '') {
this.setData({
name: option.username,
psd: option.password
})
}
},

bindUsername: function(e){
this.setData({
name: e.detail.value
});
},

bindPassword: function(e){
this.setData({
psd: e.detail.value
});
},
onFocusPsd: function(){
this.setData({
psdFocus: 'psdFocus'
})
},
onBlurPsd: function(){
this.setData({
psdFocus: ''
})
},
onFocusName: function(){
this.setData({
nameFocus: 'nameFocus'
})
},
onBlurName: function(){
this.setData({
nameFocus: ''
})
},

login: function(){
runAnimation = !runAnimation
if(!__test_account__ && this.data.name == ""){
this.toastFilled('请输入用户名!')
return;
}
else if(!__test_account__ && this.data.psd == ""){
this.toastFilled('请输入密码!')
return;
}
wx.setStorage({
key: "myUsername",
data: __test_account__ || this.data.name.toLowerCase()
});

getApp().conn.open({
user: __test_account__ || this.data.name.toLowerCase(),
pwd: __test_psword__ || this.data.psd,
grant_type: this.data.grant_type,
appKey: WebIM.config.appkey
});
},

longpress: function(){
console.log('长按')
this.setData({
show_config: !this.data.show_config
})
},

changeConfig: function(){
this.setData({
isSandBox: !this.data.isSandBox
}, ()=>{
wx.setStorage({
key: "isSandBox",
data: this.data.isSandBox
});
})

}

});


相关文档:

收起阅读 »

MyBatis实战指南(一):从概念到特点,助你快速上手,提升开发效率!

MyBatis是一个优秀的持久层框架,它支持定制化SQL、存储过程以及高级映射。MyBatis避免了几乎所有的JDBC代码和手动设置参数以及获取结果集的过程。大家好,今天我们要来聊聊一个在Java开发中非常实用的框架——MyBatis。你是否曾经因为数据库操作...
继续阅读 »

MyBatis是一个优秀的持久层框架,它支持定制化SQL、存储过程以及高级映射。MyBatis避免了几乎所有的JDBC代码和手动设置参数以及获取结果集的过程。

大家好,今天我们要来聊聊一个在Java开发中非常实用的框架——MyBatis。你是否曾经因为数据库操作而感到困扰?是否曾经因为SQL语句的编写而烦恼?那么,MyBatis或许就是你的救星。

接下来,让我们一起来了解一下MyBatis的概念与特点吧!

一、MyBatis基本概念

MyBatis 是一款优秀的半自动的ORM持久层框架,它支持自定义 SQL、存储过程以及高级映射。

MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。

MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。

那么,什么是ORM?

要了解ORM,先了解下面概念:

持久化

把数据(如内存中的对象)保存到可永久保存的存储设备中。持久化的主要应用是将内存中的数据存储在关系型的数据库中,当然也可以存储在磁盘文件中、XML数据文件中等等。

持久层

即专注于实现数据持久化应用领域的某个特定系统的一个逻辑层面,将数据使用者和数据实体相关联。

ORM, 即Object-Relational Mapping(对象关系映射),它的作用是在关系型数据库和业务实体对象之间作一个映射。这样在具体的操作业务对象的时候,就不需要再去和复杂的SQL语句打交道,只需简单的操作对象的属性和方法。

Description

总结:

  • 它是一种将内存中的对象保存到关系型数据库中的技术;

  • 主要负责实体对象的持久化,封装数据库访问细节;

  • 提供了实现持久化层的另一种模式,采用映射元数据(XML)来描述对象-关系的映射细节,使得ORM中间件能在任何一个Java应用的业务逻辑层和数据库之间充当桥梁。

Java典型的ORM框架:

  • hibernate:全自动的框架,强大、复杂、笨重、学习成本较高;

  • Mybatis:半自动的框架, 必须要自己写sql;

  • JPA:JPA全称Java Persistence API、JPA通过JDK 5.0注解或XML描述对象-表的映射关系,是Java自带的框架。

二、Mybatis的作用

Mybatis是一个Java持久层框架,它主要用于简化与数据库的交互操作。Mybatis的主要作用有以下几点:

  • 将Java对象与数据库表进行映射,通过配置XML文件实现SQL语句的定义和执行,使得开发者可以专注于业务逻辑的实现而无需编写繁琐的JDBC代码。

  • 提供了灵活的SQL映射功能,可以根据需要编写动态SQL,支持复杂的查询条件和更新操作。

  • 支持事务管理,可以确保数据的一致性和完整性。

  • 提供了缓存机制,可以提高数据库查询性能。

  • 可以与Spring、Hibernate等其他框架无缝集成,方便开发者在项目中使用。

Mybatis就是帮助程序员将数据存取到数据库里面。传统的jdbc操作,有很多重复代码块比如: 数据取出时的封装, 数据库的建立连接等等,通过框架可以减少重复代码,提高开发效率 。

MyBatis 是一个半自动化的ORM框架 (Object Relationship Mapping) -->对象关系映射。

所有的事情,不用Mybatis依旧可以做到,只是用了它,会更加方便更加简单,开发更快速。

你还在苦恼找不到真正免费的编程学习平台吗?可以试试云端源想!课程视频、在线书籍、在线编程、实验场景模拟、一对一咨询……你想要的全部学习资源这里都有,重点是统统免费!点这里即可查看

三、MyBatis特点

1、定制化SQL

同为持久层框架的Hibernate,对操作数据库的支持方式较多,完全面向对象的、原生SQL的和HQL的方式。MyBatis只支持原生的SQL语句,这个“定制化”是相对Hibernate完全面向对象的操作方式的。

2、存储过程

储存过程是实现某个特定功能的一组sql语句集,是经过编译后存储在数据库中。当出现大量的事务回滚或经常出现某条语句时,使用存储过程的效率往往比批量操作要高得多。

MyBatis是支持存储过程的,可以看个例子。假设有一张表student:

create table student
(
id bigint not null,
name varchar(30),
sex char(1),
primary key (id)
);

有一个添加记录的存储过程:

create procedure pro_addStudent (IN id bigint, IN name varchar(30), IN sex char(1))
begin
insert into student values (id, name, sex);
end

此时就可以在mapper.xml文件中调用存储过程:

<!-- 调用存储过程 -->
<!-- 第一种方式,参数使用parameterType -->
<select id="findStudentById" parameterType="java.lang.Long" statementType="CALLABLE"
resultType="com.mybatis.entity.Student">
{call pro_getStudent(#{id,jdbcType=BIGINT,mode=IN})}
</select>

<parameterMap type="java.util.Map" id="studentMap">
<parameter property="id" mode="IN" jdbcType="BIGINT"/>
</parameterMap>

<!-- 调用存储过程 -->
<!-- 第二种方式,参数使用parameterMap -->
<select id="findStudentById" parameterMap="studentMap" statementType="CALLABLE"
resultType="com.mybatis.entity.Student">
{call pro_getStudent(?)}
</select>

3、高级映射

可以简单理解为支持关联查询。

4、避免了几乎所有的JDBC代码和手动设置参数以及获取结果集。

使用Mybatis时,数据库的连接配置信息,是在mybatis-config.xml文件中配置的。同时,获取查询结果的代码,也是尽量做到了简洁。以模糊查询为例,需要做两步工作:

1)首先在配置文件中写上SQL语句,示例:

 <mapper namespace="com.test.pojo">
<select id="listCategoryByName" parameterType="string" resultType="Category">
select * from category_ where name like concat('%',#{0},'%')
</select>
</mapper>

2)在Java代码中调用此语句,示例:

        List<Category> cs = session.selectList("listCategoryByName","cat");
for (Category c : cs) {
System.out.println(c.getName());
}

5、Mybatis中ORM的映射方式也是比较简单的

"resultType"参数的值指定了SQL语句返回对象的类型。示例代码:
<mapper namespace="com.test.pojo">
<select id="listCategory" resultType="Category">
select * from category_
</select>
</mapper>

四、Mybatis的适用场景

MyBatis专注于SQL本身,是一个足够灵活的DAO层解决方案。MyBatis因其简单易用、灵活高效的特点,广泛应用于各种Java项目中。

以下是一些常见的应用场景:

  • 数据查询:MyBatis可以执行复杂的SQL查询,返回Java对象或者结果集。

  • 数据插入、更新和删除:MyBatis可以执行INSERT、UPDATE和DELETE等SQL语句。

  • 存储过程和函数调用:MyBatis可以调用数据库的存储过程和函数。

  • 高级映射:MyBatis支持一对一、一对多、多对一等复杂关系的映射。

  • 懒加载:MyBatis支持懒加载,只有在真正需要数据时才会去数据库查询。

  • 缓存机制:MyBatis内置了一级缓存和二级缓存,可以提高查询效率。

为什么说Mybatis是半自动ORM映射工具

Hibernate属于全自动ORM映射工具,使用Hibernate查询关联对象或者关联集合对象时,可以根据对象关系模型直接获取,所以它是全自动的。

而Mybatis在查询关联对象或关联集合对象时,需要手动编写sql来完成,所以,称之为半自动ORM映射工具。

MyBatis作为半自动ORM映射工具与全自动ORM工具相比,有几个主要的区别点:

1.SQL的灵活性

MyBatis作为半自动ORM映射工具,开发人员可以灵活地编写SQL语句,充分发挥数据库的特性和优势。而全自动ORM工具通常会在一定程度上限制开发人员对SQL的灵活控制。

2.映射关系的可定制性

MyBatis允许开发人员通过配置文件(或注解)自定义对象和数据库表之间的映射关系,可以满足各种复杂的映射需求。而全自动ORM工具通常根据约定和规则自动生成映射关系,对于某些特殊需求无法满足。

3.SQL的可复用性

MyBatis支持SQL的可复用性,可以将常用的SQL语句定义为独立的SQL片段,并在需要的地方进行引用。而全自动ORM工具通常将SQL语句直接与对象的属性绑定在一起,缺乏可复用性。

4.性能调优的灵活性

MyBatis作为半自动ORM映射工具,允许开发人员对SQL语句进行灵活的调优,通过手动编写SQL语句和使用高级特性进行性能优化。而全自动ORM工具通常将性能优化的控制权交给框架,开发人员无法灵活地对SQL进行调优。

MyBatis作为一种半自动ORM映射工具,相对于全自动ORM工具具有更高的灵活性和可定制性。通过灵活的SQL控制、自定义的映射关系、可复用的SQL以及灵活的性能调优,MyBatis可以满足各种复杂的映射需求和性能优化需求。

虽然MyBatis相对于全自动ORM工具需要开发人员编写更多的SQL语句,但正是由于这种半自动的特性,使得MyBatis在某些复杂场景下更加灵活和可控。

因此,我们可以说MyBatis是一种半自动ORM映射工具,与全自动的ORM工具相比,它更适用于那些对SQL灵活性和性能调优需求较高的场景。

五、Mybatis的优缺点

Mybatis有以下优点:

1.基于SQL语句编程,相当灵活

SQL写在XML里,解除sql与程序代码的耦合,便于统一管理;提供XML标签,支持编写动态SQL语句,并可重用。

2. 代码量少

与JDBC相比,减少了50%以上的代码量,消除了JDBC大量冗余的代码,不需要手动开关连接。

3.很好的与各种数据库兼容

4.数据库字段和对象之间可以有映射关系

提供映射标签,支持对象与数据库的ORM字段关系映射。

5.能够与Spring很好的集成

Mybatis有以下缺点:

1.SQL语句的编写工作量较大

尤其当字段多、关联表多时,SQL语句较复杂。

2.数据库移植性差

SQL语句依赖于数据库,不能随意更换数据库(可以通过在mybatis-config.xml配置databaseIdProvider来弥补)。

示例:

 <databaseIdProvider type="DB_VENDOR">
<property name="MySQL" value="mysql"/>
<property name="SQL Server" value="sqlserver"/>
<property name="Oracle" value="oracle"/>
</databaseIdProvider>

然后在xml文件中,就可以针对不同的数据库,写不同的sql语句。

3.字段映射标签和对象关系映射标签仅仅是对映射关系的描述,具体实现仍然依赖于sql。

示例:

public class Student{
String name;
List<Interest> interests;
}

public class Interest{
String studentId;
String name;
String direction;
}

<resultMap id="ResultMap" type="com.test.Student">
<result column="name" property="name" />
<collection property="interests" ofType="com.test.Interest">
<result column="name" property="name" />
<result column="direction" property="direction" />
</collection>
</resultMap>

在该例子中,如果查询sql中,没有关联Interest对应的表,则查询出数据映出的Student对象中,interests属性值就会为空。

4.DAO层过于简单,对象组装的工作量较大

即Mapper层Java代码过少,XxxMapper.xml文件中维护数据库字段和实体类字段的工作量较大。

5.不支持级联更新、级联删除

仍以上面的Student和Interest为例,当要更新/删除某个Student的信息时,需要在两个表进行手动更新/删除。

通过以上的介绍,相信大家对MyBatis已经有了更深入的了解。MyBatis是一个非常强大的持久层框架,它的灵活性、易用性、解耦性、高效性和全面性都使得它在Java开发中得到了广泛的应用。

收起阅读 »

我困在考研的这两年

我困在考研的这两年 2024考研结束了,我想对我这两年的考研之路做个总结。 2021年冬 2021年冬天的某一天,突然决定要考研。这时候已经距离我2017年大学毕业过去了四年,毕业四年之后再拿起书本准备考研,现在想想也感觉有些疯狂。 大学毕业的这四年,在北京有...
继续阅读 »

我困在考研的这两年


2024考研结束了,我想对我这两年的考研之路做个总结。


2021年冬


2021年冬天的某一天,突然决定要考研。这时候已经距离我2017年大学毕业过去了四年,毕业四年之后再拿起书本准备考研,现在想想也感觉有些疯狂。


大学毕业的这四年,在北京有了一份相对稳定的工作。从一开始干劲十足,到现在心累无力。看到过各种各样的中年领导,有时候也会想,这会不会就是自己未来三十岁、四十岁的样子。当有这种念头的时候,就会忍不住想逃离。想去改变,想把自己生活、职业的天花板再调高些。


当脑袋里冒出考研的时候,我也问过我自己,28上研究生真的有意义吗?毕业已经30+了,在这个35岁就是职业生涯末期的行业,这不是毕业就失业嘛。干嘛执着去考研呐。但是想到遇到过的那些中年领导,又害怕自己走他们的老路。不想被困死,只能改变。


2022年春夏秋冬


由于2021年末决定考研,我对考研形势也没有清晰的认知,转年三月份才开始准备。后来得知人家三月份早就已经把数学基础过完一遍了,我那时候还不知道定积分是个数吶!三、四月份还过了一遍高中数学知识,现在想来也是可笑,准备当年的研究生考试,还有时间去过高中数学知识。五月份报了班学习数学和英语,早上起来学英语,白天上班,晚上学习数学,地铁里看专业课知识。


那时候什么都不懂,就知道基础阶段、强化阶段、冲刺阶段。拼命赶进度,练习很少,学了后面忘了前面。也都顾不上了,先学了再说。也不管知识的掌握情况,就是闷头往前冲,英语大纲词汇过了一遍之后就不看了,学长难句,学语法。根本不想英语单词是不是要天天去背,没有时间去想那么多。


追进度、干糙活。中途又经历搬家。搬家之后,每天早起做英语阅读,坐地铁时看专业课知识,晚上写写数学。没有学习计划,没有复习计划。现在想那要是能学好,那可真就是天才了。可惜我不是


十二月初,我阳了。很难受,好像打摆子一样,身体抖个不停。发烧,头疼,感觉脑袋里边的脑仁疼。那几天我没怎么看书,也没看进去书。好在是考试之前阳了,没有因为这个原因缺考。


十二月二十四号,研究生考试开考。那天早上出来,天还是黑的。我有过一阵恍惚,不知道是为啥。到了考场,第一科开考了一个小时,我看空了大半的考场,缺考者很多。心里还窃喜,竞争者少了这么多,这我不上岸谁上岸。现在想来真是无知者无畏。


2023年春


考试成绩下来了,没有过线,国家线都没过,彻底失败。出分当天爸爸做手术,我正在忙着跑前跑后。看到群里有研友说可以查到分了,我急忙点开微信里保存的查分网址。没有登录、没有输入报名号,就那么直接的把我的考试成绩展示出来了。对我冲击很大,我一时间不知道怎么应对。幸亏我当时在上行的扶梯,不需要思考,它带着我向上。


和爸爸妈妈说了成绩之后,爸爸妈妈也没有责备我。反而鼓励我再考,“一年不行就两年,两年不行就三年。咱总得给它考上”。我当时很感动,我爸妈总是很坚定的支持我追寻自己的人生,真幸运遇到他们呀。那天也没时间考虑这个成绩,爸爸的手术从早上一直等到晚上七点多才做,晚上十一点才做完。很幸运,手术很成功。这是那天最好的消息


收拾挫败的心情,把不甘心化为动力。再大干一场吧。这毕竟是我自己的人生那!


2023年夏秋冬


回北京后,自己内心复盘了一下去年的学习方法,列了几条自己的问题,开始有针对的改变。



  • 英语读不懂就背单词,大纲单词四千+,每天背不了多的,那就背五十个。几个月怎么也过一遍。第二遍就每天两百个,再第三遍。第四遍...记不住具体词就记大体结构,先知道这个词的意思。作文我也用不了四千多个单词,用不上全部都背全词。再不行就写,联想着记。

  • 数学把去年没懂的地方都记录下来,先总体过一遍基础。再针对学不懂的章节。跟一个老师听不懂,就上B站、找网盘,看其他老师怎么讲的。对比验证着理解。

  • 专业课划出近几年的热点考题,着重了解对应的章节。

  • 学习方法不对就改,用艾宾浩斯曲线复习。


中午吃完饭,从办公区出来找一个阴凉地方看各科视频。夏天很热,周边饭店后厨的抽油烟机声音很响。买了个降噪耳机。每天中午要回去上班的时候,把耳机一摘,全是汗。


七月份,找了个小房子,自己搬出来单独住。每天学习、工作,时间安排的很满,很充实。每天也没有那么多时间去胡思乱想。七、八、九这三个月过的很快,没什么感觉就来到了十月份


九月底,接到了裁员通知。整个部门砍掉,人员全部辞退。“疫情的风”终于是吹到了我,我其实早就做好了心理准备。本来打算十月一假期回来提离职,十月底走人。专心十一月、十二月复习考研。接到通知后,没什么大的情绪起伏,坦然接受。通知是上午发的,赔偿是下午谈好的。emmm...说没情绪起伏是假的,这笔赔偿对我来说还是挺可观的。真香


这世界上唯一不会变的,就是一切都在变


坦然接受变化,因为迟早会有这么一天。


进入到十一月份,可能是临近考试了。突然感觉到焦躁,好多之前会做、能做的题。突然在真题这就不会做了。不能说一点思路没有吧,有点但是有限。翻开答案,看了就明白。但是让自己再做类似的题,还是和之前一样的感觉,人都麻了。抓紧复习知识点,再把强化阶段这个题相关章节的题拿出来重做。人更麻了。原来能做对的题,现在也做不对了。一点思路都没有,直接卡壳。尝试过总结题型,总结做题步骤。对我来说毫无用处,再遇到这种题,第一感觉还是大脑一片空白。多元函数积分学你在听吗?我说的就是你!


心里越来越焦虑,晚上躺床上也睡不着觉。基本每天都是满脑子乱想,然后迷迷糊糊睡着。


越到考试日期,越焦虑


我尝试开解自己“你不是不会,你是太紧张了,你是累了,你是没休息好,你是头脑不清醒”,我每天都下楼借着中午吃饭的时候吹吹风,放空下头脑。虽然效果不大,但是有效果就行。


十二月份,北京下雪了。那天中午吃完饭回来,我在小区的花园里走了好久。踩踩雪,感受下这真实的世界,这真实的生活,这真实的人生。


十二月二十二号,考试前一天,这一天我一点学习状态都没有。激动、颤抖、焦虑各种各样的情绪交杂在一起,肖四是一点也看不下去,更别提背了。为了第二天能按时起来。我手表订了五个闹钟,手机订了一个闹钟。狠怕自己起不来。


十二月二十三号,研究生考试的日子又到了。今年北京这边新增了安检门,由于不知道具体什么流程,所以考试的第一天我去的特别早。还是天蒙蒙亮的早晨。在电梯到一楼开门的那一刹那,我内心告诉自己“这是通往你波澜壮阔人生的一刻,加油去干吧”。


本来打算在考场外边背一会肖四再进去。但是因为今年新增了安检门,手机、书包什么都带不进楼里。只能放在楼外边的柜子里。外边好多人都守在柜子旁边背肖四,我本来也想趁这个时候再背背。但是我实在是有些太焦虑了,根本静不下心来。知识不进脑,外边还冷。索性就不背了,直接进考场。


该来的总会来,担忧那么多干嘛


找到考场后,发现就来了三个人。一看表,哦,才七点四十,八点半开始考试。在座位那硬坐了五十分钟。


政治、英语、数学、专业课


这四门考试之前,我坐在考场都很紧张。手心里都是汗,双手张开在桌子上摩擦一遍又一遍,做着深呼吸。告诉自己“没问题,我可以”。


每科考完试,感觉脑袋和身体都像被掏空了一样。那个时候没有太多想法,就是想吐槽一下考试题,哈哈哈。


本来我打算吐槽一下英语、数学、专业课(如果这次自己失败了,也好有一个赖的理由)。但是想想还是算了,强者从来都不抱怨环境。虽然我还不是强者,但是该有的格局咱们还是得有滴。那我就说说对这几科我感受到的优点吧。



  • 政治:中规中矩

  • 英语:英语一图画、图表作文首次结合,阅读AI模型、新题型博物馆、翻译大象都挺跟时事的。不得不说还是英语命题组会玩,很好,很新颖。

  • 数学:“60+老头”“坏滴很”,你哪里薄弱就往你哪里猛攻

  • 专业课:近十年来出的最好的一套卷子。出的题有深度,不偏不怪,不机械不套路。更注重理解而不是死记硬背。


该走的总要走,挽留也是徒劳


考完了,今年考完的感觉和去年完全不一样。今年少了无知无畏的乐观,更多的是如释重负的释然。不管最终结果如何,我已用尽了我的力气。我不想做悲情英雄,今年我上岸吧。梦中情校变母校,去到我想去的地方。


宇哥改编的这句歌词真好:“你看我多平常,困难一堆散落地上,但是我的眼中有光亮,换上坚强,气宇轩昂上战场,终将去到我想去的地方。”


青春,就是那些认为自己与众不同的日子


感谢我对象对我的支持理解,感谢我爸妈对我的包容和鼓励,也谢谢那个不放弃人生的自己


最后


我还想写好多话,写好多感受。但是现在已经凌晨一点多了。我明天还有事要早起。言尽至此


作者:用户4109461204928
来源:juejin.cn/post/7316202725330419739
收起阅读 »

开发距离生活有多远

相信做开发的同学,生活中会遇到一个频率非常高的问题。通常这个问题涉及的对话是这样的: 亲朋:“你在做什么工作呀?” 本人:“我是做软件开发工作的。” 亲朋:“噢!搞电脑的呀,好高端呀,你们这个行业具体是做什么呢?” 本人:“唔......就比如手机上的 AP...
继续阅读 »

相信做开发的同学,生活中会遇到一个频率非常高的问题。通常这个问题涉及的对话是这样的:



亲朋:“你在做什么工作呀?”

本人:“我是做软件开发工作的。”

亲朋:“噢!搞电脑的呀,好高端呀,你们这个行业具体是做什么呢?”

本人:“唔......就比如手机上的 APP ,微信、淘宝你用过吧?类似这种。”

亲朋:“哇,好厉害呀!”



上面这段,是我本人过往对于这个问题的回答。其实,每次我这么回答完以后,总觉得不得劲儿。感觉好像解释了一通,却又好像没让对方理解什么是开发工作。


image.png


直到最近,我的表妹又问了我这个问题:



表妹:“我其实一直没搞明白,你们写程序到底在做什么,所以,是在做什么呢?”



开发真的距离生活有那么远吗?


直到再次思考这个问题,我似乎找到了这个问题难以回答的根源:我压根没明白程序跟生活到底有什么关系。


在这个时代,编程的产物充斥着生活的各个角落:网购、聊天、支付等。但生活和程序,好像两条相互缠绕,却又难以相交的曲线。开发的产物服务于生活,但要用生活去解释开发,却又不是那么容易的事情。程序和生活中间,难道真的相隔着一个未知的距离吗?


程序不是无中生有,而是提高效率


我们开发的程序从来不是无中生有,从来不是创造不曾存在的东西,而是有围绕某个业务做的提效工具。


例如饮品店的店员操作的机器,上面就搭载了点单、收银两大功能的程序。你说这个程序没被开发出来以前,难道店员就不点单吗,就不收银吗?当然不是,让我们回忆一下,过往饮品店收银员是怎么工作的:




  1. 询问客户要买什么饮品,客户点单后,收银员用小纸条写下饮品的名称,递给做饮品的小哥;

  2. 收银员用计算器算好价格,客户递给纸币,收银员找零;

  3. 饮品做好后,收银员思考将饮品给哪位客户;



当点单量巨大时,在这套操作中,有几个痛点出现了:




  1. 写小纸条给制作饮品的小哥,这个操作会变得很耗时;

  2. 人工计算价格、收银、找零,容易出差错;

  3. 在收银员思考将饮品交给哪位客户这件事上,需要耗费巨大的脑力;



而现在的程序的流程是这样的:




  1. 客户点单,收银员在屏幕上选择客户购买的饮品,生成价格;

  2. 客户亮出付款码进行付款,生成订单号;

  3. 客户通过订单号领取饮品;



看,这就是程序做的事情,程序只是优化了生活中繁琐的步骤,提高了生活、工作的效率。人类社会向前发展,实质上就是要提高效率,把更多的时间放在更重要的人或事情上。


作为开发工作者,我们应该是更先进的


作为开发工作者,我们应该培养解决问题的能力,应该把提升效率的思考放在日常生活中,不要做只会敲代码的程序员。这是开发工作带给我们的优势和能力,让我们在生活中,多一些思考和实践。


开发也好,程序也好,离我们的生活真的很近,近到我们随时可以触摸,近到离不开我们的生活。用开发的思维为生活插上翅膀,毕竟,各个学科、行业都是从实际生活中孕育而出,最终也应回归生活,服务生活。


作者:水果小贩
来源:juejin.cn/post/7320655446100115506
收起阅读 »

为什么mysql最好不要只用limit做分页查询?

在项目中遇到的真实问题,以及我的解决方案,部分数据做了脱敏处理。 问题 最近在做项目时需要写sql做单表查询,每次查出来的数据有几百万甚至上千万条,公司用的数据库是MySQL5.7,做了分库分表,部分数据库设置了查询超时时间,比如查询超过15s直接报超时错误,...
继续阅读 »

在项目中遇到的真实问题,以及我的解决方案,部分数据做了脱敏处理。


问题


最近在做项目时需要写sql做单表查询,每次查出来的数据有几百万甚至上千万条,公司用的数据库是MySQL5.7,做了分库分表,部分数据库设置了查询超时时间,比如查询超过15s直接报超时错误,如下图:


image.png


可以通过show variables like 'max_statement_time';命令查看数据库超时时间(单位:毫秒):


image.png


方案1


尝试使用索引加速sql,从下图可以看到该sql已经走了主键索引,但还是需要扫描150万行,无法从这方面进行优化。


image.png


方案2


尝试使用limit语句进行分页查询,语句为:


SELECT * FROM table WHERE user_id = 123456789 limit 0, 300000;

像这样每次查30万条肯定就不会超时了,但这会引出另一个问题--查询耗时与起始位置成正比,如下图:


image.png


第二条语句实际上查了60w条记录,不过把前30w条丢弃了,只返回后30w条,所以耗时会递增,最终仍会超时。


方案3


使用指定主键范围的分页查询,主要思想是将条件语句改为如下形式(其中id为自增主键):


WHERE user_id = 123456789 AND id > 0 LIMIT 300000;
WHERE user_id = 123456789 AND id > (上次查询结果中最后一条记录的id值) LIMIT 300000;

也可以将上述语句简化成如下形式(注意:带了子查询会变慢):


WHERE user_id = 123456789 AND id >= (SELECT id FROM table LIMIT 300000, 1) limit 300000;

每次查询只需要修改子查询limit语句的起始位置即可,但我发现表中并没有自增主键id这个字段,表内主键是fs_id,而且是无序的。


这个方案还是不行,组内高工都感觉无解了。


方案4


既然fs_id是无序的,那么就给它排序吧,加了个ORDER BY fs_id,最终解决方案如下:


WHERE user_id = 123456789 AND fs_id > 0 ORDER BY fs_id LIMIT 300000;
WHERE user_id = 123456789 AND fs_id > (上次查询结果中最后一条记录的id值) ORDER BY fs_id LIMIT 300000;

效果如下图:


image.png


查询时间非常稳定,每条查询的fs_id都大于上次查询结果中最后一条记录的fs_id值。正常查30w条需要3.88s,排序后查30w条需要6.48s,确实慢了许多,但总算能把问题解决了。目前代码还在线上跑着哈哈,如果有更好的解决方案可以在评论区讨论哟。


作者:我要出去乱说
来源:juejin.cn/post/7209612932366270519
收起阅读 »

啊?两个vite项目怎么共用一个端口号啊

web
问题: 最近在业务开发中遇到一个问题,问题是这样的,当前有一个主项目和一个子项目,主项目通过微前端wujie来嵌套这个子项目,其中呢为了方便项目之间进行通信,所以规定该子项目的端口号必须为5173,否则通信失败,但是这时候发现一个问题,当我启动了该子项目后: ...
继续阅读 »

问题:


最近在业务开发中遇到一个问题,问题是这样的,当前有一个主项目和一个子项目,主项目通过微前端wujie来嵌套这个子项目,其中呢为了方便项目之间进行通信,所以规定该子项目的端口号必须为5173,否则通信失败,但是这时候发现一个问题,当我启动了该子项目后:


image.png


该项目的端口号为5173,但是此时我再次通过vite的官方搭建一个react+ts+vite项目:npm create vite@latest react_demos -- --template react-ts,之后通过npm run dev启动项目,发现端口号并没有更新:


image.png


这是什么原因呢?


寻因:


查阅官方文档,我发现:


image.png


那么我主动在vite.config.ts中添加这个配置:


image.png


正常来说,会出现这个报错:


image.png


但是此时结果依然为:


image.png


我百思不得不得其解,于是再次查阅官方文档:


image.png
我寻思这也与文档描述不一致啊,于是我再次尝试,思考是不是vite版本号的问题,两个项目的版本号分别为:


image.png


image.png


我决定创建一个4版本的项目npm create vite@^4.1.4 react_demos3 -- --template react-ts


image.png


结果发现,还是有这个问题,跟版本号没有关系,于是我又耐心继续看官方文档,看到了这个配置:


image.png
我抱着试试的态度,在其中一个vite项目中添加这个配置:


image.png


发现,果然是这个配置的锅,当其中一个项目host配置为0.0.0.0时,vite不会自动尝试更新端口号


难道vite的端口监测机制与host也有关?


结果:


不甘心的我再次进行尝试,将两个项目的host都设置成:


image.png


image.png


vite会自动尝试更新端口号


原来如此,vite的端口号检测机制在对比端口号之前,会先对比host,由于我的微前端项目中设置了host,而新建的项目中没有设置host,新建的项目host默认值为localhost对比不成功,vite不会自动尝试下一个可用端口,而是共用一个端口


总结:


在遇到问题时,要多多去猜,去想各种可能,并且最重要的是去尝试各种可能,还要加上积极去翻阅官方文档,问题一定会得到解决的;哪怕不能解决,那也会在尝试中,学到很多东西


作者:进阶的鱼
来源:juejin.cn/post/7319699173740363802
收起阅读 »

谈谈我家的奇葩买房经历

我是 2017 年毕业的,18 年买的房。 当时 IT 行业还是如日中天,薪资确实很高,我刚毕业就有接近 40 万。 当时的房价也是一路飙升,一周一个价那种。 我有个同事那年在北京买了房,犹豫了一周,涨了 20 多万。 那年过年回家的时候,我爸问我存了多少钱,...
继续阅读 »

我是 2017 年毕业的,18 年买的房。


当时 IT 行业还是如日中天,薪资确实很高,我刚毕业就有接近 40 万。


当时的房价也是一路飙升,一周一个价那种。


我有个同事那年在北京买了房,犹豫了一周,涨了 20 多万。


那年过年回家的时候,我爸问我存了多少钱,我说没有存多少,不知道钱花在哪里了。


我爸嫌我花的太多了,说要不买个房吧,这样每月还房贷还能存下点。


我说北京的房子需要交 5 年公积金才能买,而且首付二百多万呢,还没那么多钱。况且以后我也不一定留在北京,可能回青岛干。


年后我就回京继续上班了。


我爸在家开了一个门店,每天坐在门口和邻居聊天。


邻居聊起他儿子读完博士在青岛工作了,在黄岛区买了个房子,两周涨了十多万呢。


然后我爸就急了,非让我妈也去买一个,说是现在买还便宜点,就算我以后不回青岛,也可以卖了去北京再买。


我爸和我妈其实关系并不好,几乎是连吵带骂的逼着我妈去买。


为什么他不自己去呢?


因为我爸有严重的晕车,一坐车就吐。


我妈其实也没出过远门,自己一个人坐车从潍坊去青岛买房确实难为她了。


我妈还有点迷信,临走之前找算卦的占了一卦,算出一个方位,说是去青岛的城阳区买。


然后我妈就去了。


我妈啥也不懂,就在一个小区门口转悠。


然后保安过来问她干啥的。


她说想来买房,但是不知道去哪里买。


保安说我给你介绍一个人,可以找他买。


然后就给我妈介绍了一个中介。


那个中介说现在青岛都限购,需要交 2 年社保,只有即墨不限购,因为它刚撤市划区,划入青岛。


然后我妈找了个出租,并且给了出租的 200 块钱,让他一起去。


之后就到了即墨观澜国际的售楼处,人家介绍说这个房子是楼王,也就是最中间的那栋楼,是最好的,而且只有几套了。


我妈还在纠结,但是那个出租不耐烦了,要走。


然后我妈就定下来了,交了 70 万首付。


之后要办理手续,我从北京回家了一趟,和我爸我妈一起打车去了青岛。


我爸一路吐了有几十次,他说把胆汁都吐出来了。


就这样,我们就在青岛买下了这套房子。


13380 一平,首付 70 万,贷款 100 万,还 15 年,总共还 150 万。


然后我又回北京上班去了,只不过开始了还房贷的日子,一个月 1 万。


之后我爸又给了我 30 万,加上我自己还的,差不多在 2021 年就把 100 多万贷款还完了。因为提前还还的少。


差不多我爸 100 万,我拿了 100 万。


其实我还挺震惊的,我爸这样一个吃喝都那么节俭的人,竟然能拿出 100 万现金来。


后来在 2022 年年中的时候,我爸浑身疼的厉害,在地上打滚那种疼,去医院查出来是淋巴癌晚期。


然后 2023 年也就是今年年初的时候,我爸去世了。


去世前交代了一些事情,这套房子给我的 100 万就是他一辈子的积蓄了。


二手房要满 5 年才能卖,正好今年可以卖了。


但是问了下房价,现在观澜国际的均价是 7000 多,我们 2018 年买的时候是 13380 呢。而且 200 万的房子现在 90 万都不一定卖出去。


那我能咋办?


怪我爸?但我爸已经没了。


怪我妈?我妈也经常犯愁,而且当年是我爸逼她去的。


而且当年那种情况,我爸做的决定并没有错,当年大多数人都会认为房价会一直涨,早上车省很多钱。


我身边有一些朋友也是为了这个刚毕业不久就买房了。


其实住的话倒也没啥问题,关键是我并不去青岛工作,而且即墨那边也找不到前端的工作,互联网公司就集中在那几个城市。


租的话,一年才 1 万 5,而且装修还要投入好几万。


所以只能卖了。


本来是我们打算 5 年后卖了,正好在北京交满了 5 年公积金,然后再去北京买。


现在这情况,估计 200 万可能一分也收不回来。


遇到这事,正常人都会难受吧,我也一样。


那天我公众号发了条卖房消息:



真的是为了卖房么?


肯定不是啊,这样能把房子卖出去就怪了。


我只是想把它讲出来,仅此而已。讲出来之后确实好多了。


这几年我这种情况的全国也有不少:



并不是为了炒房,但确实因为各种原因不去住了。结果再卖的时候腰斩都卖不出去。


后来我也释然了,我本身物欲就很低,一辈子也用不了多少钱。


而且我还年轻,赚的也不少,可以再攒。


更重要的是,我一直觉得人这一生不能只是为了赚钱,要找到自己热爱的事业,在这个方向上持续开拓,创造自己的价值。


所幸我找到了。它才是支撑起我后半生的骨架。


最后,这段经历也不是完全没价值,至少我可以把它写下来,当做故事讲给你们听。


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

大环境越不好 人就越玄学

二零零几年,大环境还没像现在这么拉垮的时候,有个面向学生的网站叫校内网,里面曾有人发起了一次大范围投票。 问广大学子毕业后最想从事什么工作。 当时超过一半的人都选择了大型外企,排名第二的是大型国企民企,然后是自主创业。 只有很少一部分选择了事业单位和公务员,这...
继续阅读 »

二零零几年,大环境还没像现在这么拉垮的时候,有个面向学生的网站叫校内网,里面曾有人发起了一次大范围投票。


问广大学子毕业后最想从事什么工作。


当时超过一半的人都选择了大型外企,排名第二的是大型国企民企,然后是自主创业。


只有很少一部分选择了事业单位和公务员,这部分同学还有相当比例来自对考公自古有执念的山东。


而在其他省份,多数同学都认为自己能拥有光明的未来,当然不会喜欢公务员这种工资稳定得低,日复一日枯坐案前,早早就能一眼望到头的工作。


在当时年轻人眼里,公务员属于“实在不行就只能回家考公“的备胎,地位约等于“实在不行就找个老实人嫁了“的级别。


但后来的故事我们都知道了,经济大船这几年驶入了深水区,风浪越来越大,鱼也越来越贵。


于是四平八稳旱涝保收的体制内,这几年摇身一变,一跃成为了那个最靓的仔。不得不说,人确实是时代的产物,环境的变化可以完全改变一个人的决策。


大环境好的时候,人们会不自觉地高估自身的努力,那时候人们是相信努力一定会有收获的。有时候过于相信了,但这在经济高速增长的年代并不会有太大问题,你还是会得到属于自己的那块蛋糕的。


但当经济增速换档时,付出与回报的比例开始失衡,努力就能收获的简单逻辑不攻自破。变成了努力也不一定有收获,进而发展成努力大概率不会有收获,最后演变成一命二运三风水,努力奋斗算个鬼


这种心态的转变也解释了为啥从去年以来,越来越多的年轻人开始扎堆去寺庙求签祈福,排的长队连起来能绕地球三圈,看得旁观的老大爷直摇头说,“真搞不懂这些小年轻是怎么想的,偶像粉丝见面会咋还跑到庙里来开了?!”


人在逆境迷茫时,是容易被玄学吸引。逆境意味着前路遇阻,意味着你迫切需要一些指引,而玄学恰好满足了这方面需求。


命运这个东西,有时候真蛮捉摸不透的。


我认识一小姐姐,为一场决定人生的重要考试做足了准备,结果在赶往考场的路上,书包就这么巧被扒手偷了,里面开卷考试所有的资料全部丢失,直接导致她逃汰出局,泪洒当场。


还有一大哥,在升职加薪岗位竞争的关键阶段,突然一场急病,好巧不巧失声了,一句话也说不出来,参加不了竞聘答辩,眼睁睁看着大好机会就此溜走。


等这事过去了,他一下子又能正常说话,跟被老天上了沉默debuff一样,你说他找谁说理去呢。


人活得时间越长,就越信“命“这个东西,越能意识到自己真正能把控的其实少得可怜,随便一点意外都能直接改变整个人生走向。


这种感悟放在以前,一般都是上了些年纪的人才会有的,但随着这两年经济增速换挡,年轻人频繁碰壁,被命运按在地上摩擦的次数多了,自然也就信了“命”,求签问道的也就跟着多起来了。


说句不好听的话,我觉得这样挺好的。不是说求签问道这个行为好,而是这种行为背后暗含着一个巨大的心理转变,我认为很好。


那就是放过自己。亚洲人尤其是我们特别不愿意放过自己,从出生开始就活在比较中,长辈们连夸个人都要这么夸,说哎呀,你学习真用功,比学习委员还用功;哎呀,你工资挺高,比隔壁小王还要高。


骂你的时候也一定要捎带上别人,说你看谁谁谁多厉害,你再看看你,一定是你还不够努力。


就是这种搞法很容易让人把责任全揽自己身上,对自我要求过高,最后的结果就是崩掉,就累嘛!


但现在不一样了,现代人在网络上看了太多含着金汤匙出生在罗马的人,和那些老天爷追着赏饭吃的人。


他们跟我们之间的差距大到几辈子都弥补不上,那努力万能论也就不攻自破了嘛。


于是越来越多的小伙伴开始承认自我的局限,承认努力也不一定有收获,承认人生不如意十之八九,慢慢也就承认了“命运”这个东西,开始顺其自然,没那么多执念了。


不过有些人过于放飞自我,摆烂走了另一个极端,那也是要出问题的。


即便是玄学,它也没有彻底否定个人奋斗,大富靠命没错,但小富靠勤,靠双手取得一些小成就,让日子过得舒服些还是没啥问题的。


其实我觉得一个比较合适的世界观应该是这个样子:首先咱得承认不可抗力,承认“命”与“运”这个东西是真实存在的,如果你不喜欢这两个玄乎的字,可以用“概率”代替,我们永远得做好小概率事件砸到头上的准备。


有时候拼尽一切就是没有好的结果,这咱得承认,但同时这也并不意味着从此放弃一切行动,落入虚无主义的陷阱。


人还是要去做一些什么的。比如精进某项专业技能,逐步提升自身能力,为的不是那点工资,而是一件更重要的事,抓住运气。


运气有多重要,大家都明白,它比努力重要得多。


运气这东西打比方的话,就像一个宝箱,会随机在你面前掉落,但这些宝箱自带隐形属性,你等级太低的话就看不见它,自然也就抓不住这些运气。


用现实举例,“运气”就像你在工作中遇到了某个本来还可以拉你一把的贵人,结果你的等级太低,工作能力稀碎,贵人一看,这货不值得我帮,转身走了。他这个宝箱对你而言就隐形了,消失了。


而且最讽刺的是你从头到尾都被蒙在鼓里,根本不知道自己错失了一次宝贵的机会,所以为了避免运气来了你抓不住,又溜走的这种尴尬情况出现,我们还是要去精进和磨练一下社会技能,尽量达到能在某些场合被人夸奖的程度。


把等级刷高一些,之后该吃吃该喝喝,耐心等待宝箱的出现。这可能也是以前人们常说的,“尽人事听天命”的另一种解释吧。


也希望今天聊的关于命和运的这些内容,能启发到一些小伙伴,大家一起认认真真,平平淡淡的生活。


作者:程序员Winn
来源:juejin.cn/post/7317704462436139058
收起阅读 »

别再忘了锁屏,几行代码写一个人走屏锁小工具

写在前面 之前在公司,毕竟是干安全的,部门有这么一个要求,被发现不锁屏的,请全部门喝奶茶。很不幸,我也出现过忘了锁屏然后被发现的情况。自此之后,我就形成了肌肉记忆,同时也对别人不锁屏很敏感。 为什么要强调锁屏呢?你也不想你的电脑被别人操作吧,也不想自己的信息被...
继续阅读 »

写在前面


之前在公司,毕竟是干安全的,部门有这么一个要求,被发现不锁屏的,请全部门喝奶茶。很不幸,我也出现过忘了锁屏然后被发现的情况。自此之后,我就形成了肌肉记忆,同时也对别人不锁屏很敏感。


为什么要强调锁屏呢?你也不想你的电脑被别人操作吧,也不想自己的信息被别人获取吧。毕竟防人之心不可无。


自打跳槽到新公司之后,每次去厕所的路上就看到有人电脑不锁屏,真的是令我无比的纠结。锁个屏幕有那么难吗?确实很难,有时候一忙就容易忘,于是我就想实现一个离开电脑自动锁屏的程序。


分析


这玩意实现也不难,简单思考一下,就是让电脑检测人在不在电脑前面,那就是要试试捕获摄像头了,然后设置一个间隔时间,每隔一段时间截取图片,做人脸识别,没有人脸了就锁屏就行了。


涉及到摄像头图片处理,直接让人联想到opencv,然后再用python实现上面的一套逻辑,就搞定。


代码


安装opencv的库


pip install opencv-python

直接上代码:


import cv2
import time
import os
import platform

# 检测操作系统
def detect_os():
os_name = platform.system()
if os_name == 'Windows':
return 'windows'
elif os_name == 'Darwin':
return 'mac'
else:
return 'other'

# 执行锁屏命令
def lock_screen(os_type):
if os_type == 'windows':
os.system('rundll32.exe user32.dll, LockWorkStation')
elif os_type == 'mac':
os.system('/System/Library/CoreServices/"Menu Extras"/User.menu/Contents/Resources/CGSession -suspend')


# 初始化摄像头
cap = cv2.VideoCapture(0)

# 载入OpenCV的人脸检测模型
face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml')

# 无人状态计时器
no_person_timer = 0
# 设定无人状态时间阈值
NO_PERSON_THRESHOLD = 3

# 检测操作系统类型
os_type = detect_os()

while True:
ret, frame = cap.read()
if not ret:
break

# 转换为灰度图像
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
faces = face_cascade.detectMultiScale(gray, 1.1, 4)

if len(faces) == 0:
no_person_timer += 1
else:
no_person_timer = 0

# 如果超过阈值,则锁屏
if no_person_timer > NO_PERSON_THRESHOLD:
lock_screen(os_type)
no_person_timer = 0

time.sleep(1)

cap.release()

代码里都做好了注释,很简单,因为windows和macOS的锁屏指令不一样,所以做了个简单的系统平台判断。


可以完美执行,就是它得一直调用摄像头,应该也不会有人真的使用这玩意吧,hhh。


作者:银空飞羽
来源:juejin.cn/post/7317824480911458304
收起阅读 »

终于搞懂了网盘网页是怎么唤醒本地应用了

web
写在前面 用百度网盘举例,可以通过页面打开本机的百度网盘软件,很多软件的网站页面都有这个功能。这个事情一直令我比较好奇,这次终于有空抽时间来研究研究了,本篇讲的是Windows的,mac的原理与之类似。 自定义协议 本身单凭浏览器是没有唤醒本地应用这个能力的,...
继续阅读 »

写在前面


用百度网盘举例,可以通过页面打开本机的百度网盘软件,很多软件的网站页面都有这个功能。这个事情一直令我比较好奇,这次终于有空抽时间来研究研究了,本篇讲的是Windows的,mac的原理与之类似。


自定义协议


本身单凭浏览器是没有唤醒本地应用这个能力的,不然随便一个网页都能打开你的所有应用那不就乱套了吗。但是电脑系统本身又可以支持这个能力,就是通过配置自定义协议。


举个例子,当你用浏览器打开一个本地的PDF的时候,你会发现上面是file://path/xxx.pdf,这就是系统内置的一个协议,浏览器可以调用这个协议进行文件读取。


那么与之类似的,windows本身也支持用户自定义协议来进行一些操作的,而这个协议就在注册表中进行配置。


配置自定义协议


这里我用VS Code来举例子,最终我要实现通过浏览器打开我电脑上的VS Code。


我们先编写一个注册表文件


Windows Registry Editor Version 5.00

[HKEY_CLASSES_ROOT\vscode]
@="URL:VSCode Protocol"
"URL Protocol"=""

[HKEY_CLASSES_ROOT\vscode\shell]

[HKEY_CLASSES_ROOT\vscode\shell\open]

[HKEY_CLASSES_ROOT\vscode\shell\open\command]
@=""D:\VScode\Microsoft VS Code\Code.exe" "%1""

这里我逐行解释



  1. Windows Registry Editor Version 5.00 这行表明该文件是一个 Windows 注册表编辑器文件,这是标准的头部,用于告诉 Windows 如何解析文件。

  2. [HKEY_CLASSES_ROOT\vscode] 这是一个注册表键的开始。在这里,\vscode 表示创建一个名为 vscode 的新键。

  3. @="URL:VSCode Protocol"vscode 键下,这行设置了默认值(表示为 @ ),通过 "URL:VSCode Protocol" 对这个键进行描述。

  4. "URL Protocol"="" 这行是设置一个名为 URL Protocol 的空字符串值。这是代表这个新键是一个 URI 协议。

  5. [HKEY_CLASSES_ROOT\vscode\shell] 创建一个名为 shell 的子键,这是一个固定键,代表GUI界面的处理。

  6. [HKEY_CLASSES_ROOT\vscode\shell\open]shell 下创建一个名为 open 的子键。这耶是一个固定键,open 是一个标准动作,用来执行打开操作。

  7. [HKEY_CLASSES_ROOT\vscode\shell\open\command]open 下创建一个名为 command 的子键。这是一个固定键,指定了当协议被触发时要执行命令。

  8. @=""D:\VScode\Microsoft VS Code\Code.exe" "%1""command 键下,设置默认值为 VSCode 的路径。 "%1" 是一个占位符,用于表示传递给协议的任何参数,这里并无实际用处。


写好了注册表文件后,我们将其保存为 vscode.reg,并双击执行,对话框选择是,相应的注册表信息就被创建出来了。



可以通过注册表中查看。


浏览器打开VS Code


这时,我们打开浏览器,输入 vscode://open



可以看到,就像百度网盘一样,浏览器弹出了询问对话框,然后就可以打开VS Code了。


如果想要在网页上进行打开,也简单


<script>
function openVSCode() {
window.location.href = 'vscode://open/';
}
</script>
<button onclick="openVSCode()">打开 VSCode</button>

写一个简单的JS代码即可。


写在最后


至此,终于是了解了这方面的知识。这就是说,在网盘安装的过程中,就写好了这个注册表文件,自定义了网盘的唤醒协议,才可以被识别。


而我也找到了这个注册表



原来叫baiduyunguanjia协议(不区分大小写),使用 baiduyunguanjia://open 可以打开。


作者:银空飞羽
来源:juejin.cn/post/7320513026188460067
收起阅读 »

都用HTTPS了,还能被查出浏览记录?

最近,群里一个刚入职的小伙因为用公司电脑访问奇怪的网站,被约谈了。他很困惑 —— 访问的都是HTTPS的网站,公司咋知道他访问了啥? 实际上,由于网络通信有很多层,即使加密通信,仍有很多途径暴露你的访问地址,比如: DNS查询:通常DNS查询是不会加密的,...
继续阅读 »

最近,群里一个刚入职的小伙因为用公司电脑访问奇怪的网站,被约谈了。他很困惑 —— 访问的都是HTTPS的网站,公司咋知道他访问了啥?



实际上,由于网络通信有很多层,即使加密通信,仍有很多途径暴露你的访问地址,比如:



  • DNS查询:通常DNS查询是不会加密的,所以,能看到你DNS查询的观察者(比如运营商)是可以推断出访问的网站

  • IP地址:如果一个网站的IP地址是独一无二的,那么只需看到目标 IP地址,就能推断出用户正在访问哪个网站。当然,这种方式对于多网站共享同一个IP地址(比如CDN)的情况不好使

  • 流量分析:当访问一些网站的特定页面,可能导致特定大小和顺序的数据包,这种模式可能被用来识别访问的网站

  • cookies或其他存储:如果你的浏览器有某个网站的cookies,显然这代表你曾访问过该网站,其他存储信息(比如localStorage)同理


除此之外,还有很多方式可以直接、间接知道你的网站访问情况。


本文将聚焦在HTTPS协议本身,聊聊只考虑HTTPS协议的情况下,你的隐私是如何泄露的。


HTTPS简介


我们每天访问的网站大部分是基于HTTPS协议的,简单来说,HTTPS = HTTP + TLS,其中:



  • HTTP是一种应用层协议,用于在互联网上传输超文本(比如网页内容)。由于HTTP是明文传递,所以并不安全

  • TLS是一种安全协议。TLS在传输层对数据进行加密,确保任何敏感信息在两端(比如客户端和服务器)之间安全传输,不被第三方窃取或篡改


所以理论上,结合了HTTPTLS特性的HTTPS,在数据传输过程是被加密的。但是,TLS建立连接的过程却不一定是加密的。


TLS的握手机制


当我们通过TLS传递加密的HTTP信息之前,需要先建立TLS连接,比如:



  • 当用户首次访问一个HTTPS网站,浏览器开始查询网站服务器时,会发生TLS连接

  • 当页面请求API时,会发生TLS连接


建立连接的过程被称为TLS握手,根据TLS版本不同,握手的步骤会有所区别。



但总体来说,TLS握手是为了达到三个目的:



  1. 协商协议和加密套件:通信的两端确认接下来使用的TLS版本及加密套件

  2. 验证省份:为了防止“中间人”攻击,握手过程中,服务器会向客户端发送其证书,包含服务器公钥和证书授权中心(即CA)签名的身份信息。客户端可以使用这些信息验证服务器的身份

  3. 生成会话密钥:生成用于加密接下来数据传输的密钥


TLS握手机制的缺点


虽然TLS握手机制会建立安全的通信,但在握手初期,数据却是明文发送的,这就造成隐私泄漏的风险。


在握手初期,客户端、服务端会依次发送、接收对方的打招呼信息。首先,客户端会向服务端打招呼(发送client hello信息),该消息包含:



  • 客户端支持的TLS版本

  • 支持的加密套件

  • 一串称为客户端随机数client random)的随机字节

  • SNI等一些服务器信息


服务端接收到上述消息后,会向客户端打招呼(发送server hello消息),再回传一些信息。


其中,SNIServer Name Indication,服务器名称指示)就包含了用户访问的网站域名。


那么,握手过程为什么要包含SNI呢?


这是因为,当多个网站托管在一台服务器上并共享一个IP地址,且每个网站都有自己的SSL证书时,那就没法通过IP地址判断客户端是想和哪个网站建立TLS连接,此时就需要域名信息辅助判断。


打个比方,快递员送货上门时,如果快递单只有收货的小区地址(IP地址),没有具体的门牌号(域名),那就没法将快递送到正确的客户手上(与正确的网站建立TLS连接)。


所以,SNI作为TLS的扩展,会在TLS握手时附带上域名信息。由于打招呼的过程是明文发送的,所以在建立HTTPS连接的过程中,中间人就能知道你访问的域名信息。


企业内部防火墙的访问控制和安全策略,就是通过分析SNI信息完成的。



虽然防火墙可能已经有授信的证书,但可以先分析SNI,根据域名情况再判断要不要进行深度检查,而不是对所有流量都进行深度检查



那么,这种情况下该如何保护个人隐私呢?


Encrypted ClientHello


Encrypted ClientHelloECH)是TLS1.3的一个扩展,用于加密Client Hello消息中的SNI等信息。


当用户访问一个启用ECH的服务器时,网管无法通过观察SNI来窥探域名信息。只有目标服务器才能解密ECH中的SNI,从而保护了用户的隐私。



当然,对于授信的防火墙还是不行,但可以增加检查的成本



开启ECH需要同时满足:



  • 服务器支持TLSECH扩展

  • 客户端支持ECH


比如,cloudflare SNI测试页支持ECH扩展,当你的浏览器不支持ECH时,访问该网站sni会返回plaintext



对于chrome,在chrome://flags/#encrypted-client-hello中,配置ECH支持:



再访问上述网站,sni如果返回encrypted则代表支持ECH


总结


虽然HTTPS连接本身是加密的,但在建立HTTPS的过程中(TLS握手),是有数据明文传输的,其中SNI中包含了服务器的域名信息。


虽然SNI信息的本意是解决同一IP下部署多个网站,每个网站对应不同的SSL证书,但也会泄漏访问的网站地址


ECH通过对TLS握手过程中的敏感信息(主要是SNI)进行加密,为用户提供了更强的隐私保护。


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

《年会不能停》豆瓣8.2分,强烈建议所有职场人都去看!

12024年的第一部电影献给了《年会不能停》,豆瓣开分8.1分,现在保持在8.2分,这部片子真的将每个打工人都狠狠代入其中,超级推荐。作为曾经的职场新媒体人看起来,反讽效果拉满,笑点密不能停,台词里句句是职场人的嘴替,有人看着是乐子,有人是照镜子,笑完之后,蓦...
继续阅读 »

1


2024年的第一部电影献给了《年会不能停》,豆瓣开分8.1分,现在保持在8.2分,这部片子真的将每个打工人都狠狠代入其中,超级推荐。


作为曾经的职场新媒体人看起来,反讽效果拉满,笑点密不能停,台词里句句是职场人的嘴替,有人看着是乐子,有人是照镜子,笑完之后,蓦然回首小丑竟是我自己。


影片讲述的是一名厂里的“高级钳工”胡建林(大鹏饰),阴差阳错被调入了公司总部成为人事专员,从“工厂”到“大厂”经过一系列乌龙事件,反而职位越做越高。


深谙职场生存之道的打工人马杰(白客饰),与叛逆的外包员工潘妮(庄达菲饰),俩人就是踏实做事的社畜代表,勤勤恳恳却碌碌无为,甚至连正都转不了。


却与在职场最会被吐槽的胡建林成为“铁三角”组合,在最后年会揭发了公司高层的腐化,从而保住了一个厂全体员工的饭碗。


影片好看之处就在于拍出了当下社会职场的现象,年轻人在这种现状里的疑惑和挣扎。


虽然结尾的“大团圆”结局过于理想主义,但我们也只能在电影中找到这种爽感来出一口对现实的恶气,虽然梦醒之后依旧是加班熬夜低头倒茶。


2


我一直有一件疑问的事,有人真的热爱上班吗?应该有80%的人回复是不爱,但无奈吧。


从毕业之后,经历了几份工作,发现我是真不爱上班,除了拿点每个月准时的“窝囊费”,好像真没什么值得开心的了,还有无止境的加班,内部争斗,还有付出和回报不成正比的委屈。


但偏偏人就要为这几点碎银两,向生活和工作低头,就像代表职场里中年人的马杰一样。


无数个马杰都如蚂蚁一般,勤勤恳恳做事,但反而得不到升职加薪,内心有原则却难守护,就如那句“如果我失业了,家人怎么办”,直击打工人的压力痛点。


相反只有像潘妮,这个角色就代表00后的职场人群,不战队不妥协不随流,就是去整顿职场这些荒谬的规则的。


好不容公司同意转正,她却另辟蹊径,潇洒地递交“世界那么大,我要去看看”的“叛逆”辞职信。


在职场也许我们像中年的胡建林和马杰,但人生不只有工作,更多时候拥有潘妮的“叛逆”和勇敢,寻找更多的人生出口,才会更有趣更有力量一些。


3


我觉得影片很妙的一点是,把两个时代的人物结合到同一个平行空间里,将两代人的职场风格和做事方法也融入到了一起,形成了强烈对比,反差感很强。


最开始以为讲述的是90年代的事,没想到是同时代的打工场景,这种跨越也正好是我们这代和父母辈所经历过的场景,加上拍摄地点和我的职场经历相似, 更有代入感了。


90年代时我妈就曾在汽电厂工作,我小时候也在那种环境中生活过,工人们统一的工服,螺丝钉一样的工作内容,集体主义式的生活,通勤只有2分钟。


工作虽然在身体上辛苦又繁复,但下班就真的是下班,不必看公司和客户消息,电话会议,生活简单又满足。


而作为当代社畜,一定是脑力和体力的双倍付出,996的工作时间,但没有加班工资,做不完的事,没有周末,下班和放假还要守“机”待“工”,工作和生活从来不能完全分开。


时代在变,两代人的职场理想不一样,上一辈的老思想就是一份工作就是一辈子的事,在一个岗位日复一日的劳作,像胡建林一样做到一颗螺丝钉一咬就知道质量对不对。


而如今像这样心思单纯性格执拗的职员,绝对就是裁员名单上的第一批人。


4


从“工厂”跳到“大厂”,从工人到白领,胡建林宛若穿越一般的人,一切事物就像他说的“小刀割屁股,开了眼”了。


不会英语不会大厂里的专业术语,连“优化”也理解错,让裁员变成升职,在这一系列骚操作中“弄拙成巧”,连连高升。


如果按现实来说,是不会在职场存活下来的,电影里就形成强烈反差感,荒诞可笑,却也映射着在职场里靠关系进去的人,不仅不会做事,还会被像财神爷一样供着,具有讽刺意味。


作为在职场8年,5年新媒体经验的我,曾也进入过本地500人的新媒体大公司。


在原本以为进的“大厂”里人人都是专家,能力强,但当你一进去之后,会发现和影片中的体系差不多,高层的就是一帮混酒局的,中层大部分靠的是拍马屁和吹水,真正做事的可能就是底层的基础职员。


但作为底层社畜,尽管看到了职场里的bug和荒谬,就算看清了现实,也逃不过压榨和背锅,只能一边吐槽一边苦干,谁叫自己的饭碗在别人手里呢。


但我觉得不管做任何工作,也不是一味的“隐忍”,现在也不是只靠上班才能赚钱的时代,相比赚钱,我觉得人一定要记得最初的自己。


5


时代的列车呼啸向前,车轮地下总得有人增加摩擦力”,这句话很扎心却现实。


哪怕口罩问题已经结束,现在仍然有很多知名大厂在不断裁员。


打工的怕没工打,没打工的找不到工打,每天大家都活在不稳定的气氛中,就像《年会》里唱的那句:“你是不是也像我,在裁员中忐忑。”


大环境的齿轮一旦转动,谁也逃脱不了被碾压的命运,只能眼睁睁看着事情发生无力改变,才是最可悲的事情,只有在变化中才能求解。


随着知识和经验buff的叠加,我们所能做就是预测时代的节奏,在每一个车轮想要来碾压我们之前,增加自己的动力,去跑赢这辆列车。



END


作者:李猫妮
来源:mp.weixin.qq.com/s/2k6GdooHJnlUOHnckyhFZg

收起阅读 »

前端无感知刷新token & 超时自动退出

web
前端无感知刷新token&超时自动退出 一、token的作用 因为http请求是无状态的,是一次性的,请求之间没有任何关系,服务端无法知道请求者的身份,所以需要鉴权,来验证当前用户是否有访问系统的权限。 以oauth2.0授权码模式为例: 每次请求资...
继续阅读 »

前端无感知刷新token&超时自动退出


一、token的作用


因为http请求是无状态的,是一次性的,请求之间没有任何关系,服务端无法知道请求者的身份,所以需要鉴权,来验证当前用户是否有访问系统的权限。


以oauth2.0授权码模式为例:


oauth2授权码模式.png


每次请求资源服务器时都会在请求头中添加 Authorization: Bearer access_token 资源服务器会先判断token是否有效,如果无效或过期则响应 401 Unauthorize。此时用户处于操作状态,应该自动刷新token保证用户的行为正常进行。


刷新token:使用refresh_token获取新的access_token,使用新的access_token重新发起失败的请求。


二、无感知刷新token方案


2.1 刷新方案


当请求出现状态码为 401 时表明token失效或过期,拦截响应,刷新token,使用新的token重新发起该请求。


如果刷新token的过程中,还有其他的请求,则应该将其他请求也保存下来,等token刷新完成,按顺序重新发起所有请求。


2.2 原生AJAX请求


2.2.1 http工厂函数


function httpFactory({ method, url, body, headers, readAs, timeout }) {
   const xhr = new XMLHttpRequest()
   xhr.open(method, url)
   xhr.timeout = isNumber(timeout) ? timeout : 1000 * 60

   if(headers){
       forEach(headers, (value, name) => value && xhr.setRequestHeader(name, value))
  }
   
   const HTTPPromise = new Promise((resolve, reject) => {
       xhr.onload = function () {
           let response;

           if (readAs === 'json') {
               try {
                   response = JSONbig.parse(this.responseText || null);
              } catch {
                   response = this.responseText || null;
              }
          } else if (readAs === 'xml') {
               response = this.responseXML
          } else {
               response = this.responseText
          }

           resolve({ status: xhr.status, response, getResponseHeader: (name) => xhr.getResponseHeader(name) })
      }

       xhr.onerror = function () {
           reject(xhr)
      }
       xhr.ontimeout = function () {
           reject({ ...xhr, isTimeout: true })
      }

       beforeSend(xhr)

       body ? xhr.send(body) : xhr.send()

       xhr.onreadystatechange = function () {
           if (xhr.status === 502) {
               reject(xhr)
          }
      }
  })

   // 允许HTTP请求中断
   HTTPPromise.abort = () => xhr.abort()

   return HTTPPromise;
}

2.2.2 无感知刷新token


// 是否正在刷新token的标记
let isRefreshing = false

// 存放因token过期而失败的请求
let requests = []

function httpRequest(config) {
   let abort
   let process = new Promise(async (resolve, reject) => {
       const request = httpFactory({...config, headers: { Authorization: 'Bearer ' + cookie.load('access_token'), ...configs.headers }})
       abort = request.abort
       
       try {                            
           const { status, response, getResponseHeader } = await request

           if(status === 401) {
               try {
                   if (!isRefreshing) {
                       isRefreshing = true
                       
                       // 刷新token
                       await refreshToken()

                       // 按顺序重新发起所有失败的请求
                       const allRequests = [() => resolve(httpRequest(config)), ...requests]
                       allRequests.forEach((cb) => cb())
                  } else {
                       // 正在刷新token,将请求暂存
                       requests = [
                           ...requests,
                          () => resolve(httpRequest(config)),
                      ]
                  }
              } catch(err) {
                   reject(err)
              } finally {
                   isRefreshing = false
                   requests = []
              }
          }                        
      } catch(ex) {
           reject(ex)
      }
  })
   
   process.abort = abort
   return process
}

// 发起请求
httpRequest({ method: 'get', url: 'http://127.0.0.1:8000/api/v1/getlist' })

2.3 Axios 无感知刷新token


// 是否正在刷新token的标记
let isRefreshing = false

let requests: ReadonlyArray<(config: any) => void> = []

// 错误响应拦截
axiosInstance.interceptors.response.use((res) => res, async (err) => {
   if (err.response && err.response.status === 401) {
       try {
           if (!isRefreshing) {
               isRefreshing = true
               // 刷新token
               const { access_token } = await refreshToken()

               if (access_token) {
                   axiosInstance.defaults.headers.common.Authorization = `Bearer ${access_token}`;

                   requests.forEach((cb) => cb(access_token))
                   requests = []

                   return axiosInstance.request({
                       ...err.config,
                       headers: {
                           ...(err.config.headers || {}),
                           Authorization: `Bearer ${access_token}`,
                      },
                  })
              }

               throw err
          }

           return new Promise((resolve) => {
               // 将resolve放进队列,用一个函数形式来保存,等token刷新后直接执行
               requests = [
                   ...requests,
                  (token) => resolve(axiosInstance.request({
                       ...err.config,
                       headers: {
                           ...(err.config.headers || {}),
                           Authorization: `Bearer ${token}`,
                      },
                  })),
              ]
          })
      } catch (e) {
           isRefreshing = false
           throw err
      } finally {
           if (!requests.length) {
               isRefreshing = false
          }
      }
  } else {
       throw err
  }
})

三、长时间无操作超时自动退出


当用户登录之后,长时间不操作应该做自动退出功能,提高用户数据的安全性。


3.1 操作事件


操作事件:用户操作事件主要包含鼠标点击、移动、滚动事件和键盘事件等。


特殊事件:某些耗时的功能,比如上传、下载等。


3.2 方案


用户在登录页面之后,可以复制成多个标签,在某一个标签有操作,其他标签也不应该自动退出。所以需要标签页之间共享操作信息。这里我们使用 localStorage 来实现跨标签页共享数据。


在 localStorage 存入两个字段:


名称类型说明说明
lastActiveTimestring最后一次触发操作事件的时间戳
activeEventsstring[ ]特殊事件名称数组

当有操作事件时,将当前时间戳存入 lastActiveTime。


当有特殊事件时,将特殊事件名称存入 activeEvents ,等特殊事件结束后,将该事件移除。


设置定时器,每1分钟获取一次 localStorage 这两个字段,优先判断 activeEvents 是否为空,若不为空则更新 lastActiveTime 为当前时间,若为空,则使用当前时间减去 lastActiveTime 得到的值与规定值(假设为1h)做比较,大于 1h 则退出登录。


3.3 代码实现


const LastTimeKey = 'lastActiveTime'
const activeEventsKey = 'activeEvents'
const debounceWaitTime = 2 * 1000
const IntervalTimeOut = 1 * 60 * 1000

export const updateActivityStatus = debounce(() => {
   localStorage.set(LastTimeKey, new Date().getTime())
}, debounceWaitTime)

/**
* 页面超时未有操作事件退出登录
*/

export function timeout(keepTime = 60) {
   document.addEventListener('mousedown', updateActivityStatus)
   document.addEventListener('mouseover', updateActivityStatus)
   document.addEventListener('wheel', updateActivityStatus)
   document.addEventListener('keydown', updateActivityStatus)

   // 定时器
   let timer;

   const doTimeout = () => {
       timer && clearTimeout(timer)
       localStorage.remove(LastTimeKey)
       document.removeEventListener('mousedown', updateActivityStatus)
       document.removeEventListener('mouseover', updateActivityStatus)
       document.removeEventListener('wheel', updateActivityStatus)
       document.removeEventListener('keydown', updateActivityStatus)

       // 注销token,清空session,回到登录页
       logout()
  }

   /**
    * 重置定时器
    */

   function resetTimer() {
       localStorage.set(LastTimeKey, new Date().getTime())

       if (timer) {
           clearInterval(timer)
      }

       timer = setInterval(() => {
           const isSignin = document.cookie.includes('access_token')
           if (!isSignin) {
               doTimeout()
               return
          }

           const activeEvents = localStorage.get(activeEventsKey)
           if(!isEmpty(activeEvents)) {
               localStorage.set(LastTimeKey, new Date().getTime())
               return
          }
           
           const lastTime = Number(localStorage.get(LastTimeKey))

           if (!lastTime || Number.isNaN(lastTime)) {
               localStorage.set(LastTimeKey, new Date().getTime())
               return
          }

           const now = new Date().getTime()
           const time = now - lastTime

           if (time >= keepTime) {
               doTimeout()
          }
      }, IntervalTimeOut)
  }

   resetTimer()
}

// 上传操作
function upload() {
   const current = JSON.parse(localStorage.get(activeEventsKey))
   localStorage.set(activeEventsKey, [...current, 'upload'])
   ...
   // do upload request
   ...
   const current = JSON.parse(localStorage.get(activeEventsKey))
   localStorage.set(activeEventsKey, Array.isArray(current) ? current.filter((item) => itme !== 'upload'))
}

作者:ww_怒放
来源:juejin.cn/post/7320044522910269478
收起阅读 »

解决扫码枪因输入法中文导致的问题

web
问题 最近公司项目上遇到了扫码枪因搜狗/微软/百度/QQ等输入法在中文状态下,使用扫码枪扫码会丢失字符的问题 思考 这种情况是由于扫码枪的硬件设备,在输入的时候,是模拟用户键盘的按键来实现的字符输入的,所以会触发输入法的中文模式,并且也会触发输入法的自动联想。...
继续阅读 »

问题


最近公司项目上遇到了扫码枪因搜狗/微软/百度/QQ等输入法在中文状态下,使用扫码枪扫码会丢失字符的问题


思考


这种情况是由于扫码枪的硬件设备,在输入的时候,是模拟用户键盘的按键来实现的字符输入的,所以会触发输入法的中文模式,并且也会触发输入法的自动联想。那我们可以针对这个来想解决方案。


方案一


首先想到的第一种方案是,监听keydown的键盘事件,创建一个字符串数组,将每一个输入的字符进行比对,然后拼接字符串,并回填到输入框中,下面是代码:


function onKeydownEvent(e) {
this.code = this.code || ''
const shiftKey = e.shiftKey
const keyCode = e.code
const key = e.key
const arr = ['Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', 'Z', 'X', 'C', 'V', 'B', 'N', 'M', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-']
this.nextTime = new Date().getTime()
const timeSpace = this.nextTime - this.lastTime
if (key === 'Process') { // 中文手动输入
if (this.lastTime !== 0 && timeSpace <= 30) {
for (const a of arr) {
if (keyCode === 'Key' + a) {
if (shiftKey) {
this.code += a
} else {
this.code += a.toLowerCase()
}
this.lastTime = this.nextTime
} else if (keyCode === 'Digit' + a) {
this.code += String(a)
this.lastTime = this.nextTime
}
}
if (keyCode === 'Enter' && timeSpace <= 30) {
if (String(this.code)) {
// TODO
dosomething....
}
this.code = ''
this.nextTime = 0
this.lastTime = 0
}
}
} else {
if (arr.includes(key.toUpperCase())) {
if (this.lastTime === 0 && timeSpace === this.nextTime) {
this.code = key
} else if (this.lastTime !== 0 && timeSpace <= 30) {
// 30ms以内来区分是扫码枪输入,正常手动输入时少于30ms的
this.code += key
}
this.lastTime = this.nextTime
} else if (arr.includes(key)) {
if (this.lastTime === 0 && timeSpace === this.nextTime) {
this.code = key
} else if (this.lastTime !== 0 && timeSpace <= 30) {
this.code += String(key)
}
this.lastTime = this.nextTime
} else if (keyCode === 'Enter' && timeSpace <= 30) {
if (String(this.code)) {
// TODO
dosomething()
}
this.code = ''
this.nextTime = 0
this.lastTime = 0
} else {
this.lastTime = this.nextTime
}
}
}


这种方案能解决部分问题,但是在不同的扫码枪设备,以及不同输入法的情况下,还是会出现丢失问题


方案二


使用input[type=password]来兼容不同输入的中文模式,让其只能输入英文,从而解决丢失问题


这种方案网上也有不少的参考

# 解决中文状态下扫描枪扫描错误

# input type=password 取消密码提示框


使用password密码框确实能解决不同输入法的问题,并且Focus到输入框,输入法会被强制切换为英文模式


添加autocomplete="off"autocomplete="new-password"属性


官方文档:
# 如何关闭表单自动填充


但是在Chromium内核的浏览器,不支持autocomplete="off",并且还是会出现这种自动补全提示:


image.png


上面的属性并没有解决浏览器会出现密码补全框,并且在输入字符后,浏览器还会在右上角弹窗提示是否保存:


image.png


先解决密码补全框,这里我想到了一个属性readonly,input原生属性。input[type=password]readonly
时,是不会有密码补全的提示,并且也不会弹窗提示密码保存。


那好,我们就可以在输入前以及输入完成后,将input[type=password]立即设置成readonly


但是需要考虑下面几种情况:



  • 获取焦点/失去焦点时

  • 当前输入框已focus时,再次鼠标点击输入框

  • 扫码枪输出完成最后,输入Enter键时,如果清空输入框,这时候也会显示自动补全

  • 清空输入框时

  • 切换离开页面时


这几种情况都需要处理,将输入框变成readonly


我用vue+element-ui实现了一份,贴上代码:


<template>
<div class="scanner-input">
<input class="input-password" :name="$attrs.name || 'one-time-code'" type="password" autocomplete="off" aria-autocomplete="inline" :value="$attrs.value" readonly @input="onPasswordInput">
<!-- <el-input ref="scannerInput" v-bind="$attrs" v-on="$listeners" @input="onInput"> -->
<el-input ref="scannerInput" :class="{ 'input-text': true, 'input-text-focus': isFocus }" v-bind="$attrs" v-on="$listeners">
<template v-for="(_, name) in $slots" v-slot:[name]>
<slot :name="name"></slot>
</template>
<!-- <slot slot="suffix" name="suffix"></slot> -->
</el-input>
</div>
</template>

<script>
export default {
name: 'WispathScannerInput',
data() {
return {
isFocus: false
}
},
beforeDestroy() {
this.$el.firstElementChild.setAttribute('readonly', true)
this.$el.firstElementChild.removeEventListener('focus', this.onPasswordFocus)
this.$el.firstElementChild.removeEventListener('blur', this.onPasswordBlur)
this.$el.firstElementChild.removeEventListener('click', this.onPasswordClick)
this.$el.firstElementChild.removeEventListener('mousedown', this.onPasswordMouseDown)
this.$el.firstElementChild.removeEventListener('keydown', this.oPasswordKeyDown)
},
mounted() {
this.$el.firstElementChild.addEventListener('focus', this.onPasswordFocus)
this.$el.firstElementChild.addEventListener('blur', this.onPasswordBlur)
this.$el.firstElementChild.addEventListener('click', this.onPasswordClick)
this.$el.firstElementChild.addEventListener('mousedown', this.onPasswordMouseDown)
this.$el.firstElementChild.addEventListener('keydown', this.oPasswordKeyDown)

const entries = Object.entries(this.$refs.scannerInput)
// 解决ref问题
for (const [key, value] of entries) {
if (typeof value === 'function') {
this[key] = value
}
}
this['focus'] = this.$el.firstElementChild.focus.bind(this.$el.firstElementChild)
},
methods: {
onPasswordInput(ev) {
this.$emit('input', ev.target.value)
if (ev.target.value === '') {
this.$el.firstElementChild.setAttribute('readonly', true)
setTimeout(() => {
this.$el.firstElementChild.removeAttribute('readonly')
})
}
},
onPasswordFocus(ev) {
this.isFocus = true
setTimeout(() => {
this.$el.firstElementChild.removeAttribute('readonly')
})
},
onPasswordBlur() {
this.isFocus = false
this.$el.firstElementChild.setAttribute('readonly', true)
},
// 鼠标点击输入框一瞬间,禁用输入框
onPasswordMouseDown() {
this.$el.firstElementChild.setAttribute('readonly', true)
},
oPasswordKeyDown(ev) {
// 判断enter键
if (ev.key === 'Enter') {
this.$el.firstElementChild.setAttribute('readonly', true)
setTimeout(() => {
this.$el.firstElementChild.removeAttribute('readonly')
})
}
},
// 点击之后,延迟200ms后放开readonly,让输入框可以输入
onPasswordClick() {
if (this.isFocus) {
this.$el.firstElementChild.setAttribute('readonly', true)
setTimeout(() => {
this.$el.firstElementChild.removeAttribute('readonly')
}, 200)
}
},
onInput(_value) {
this.$emit('input', _value)
},
getList(value) {
this.$emit('input', value)
}
// onChange(_value) {
// this.$emit('change', _value)
// }
}
}
</script>

<style lang="scss" scoped>
.scanner-input {
position: relative;
height: 36px;
width: 100%;
display: inline-block;
.input-password {
width: 100%;
height: 100%;
border: none;
outline: none;
padding: 0 16px;
font-size: 14px;
letter-spacing: 3px;
background: transparent;
color: transparent;
// caret-color: #484848;
}
.input-text {
font-size: 14px;
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
pointer-events: none;
background-color: transparent;
::v-deep .el-input__inner {
// background-color: transparent;
padding: 0 16px;
width: 100%;
height: 100%;
}
}

.input-text-focus {
::v-deep .el-input__inner {
outline: none;
border-color: #1c7af4;
}
}
}
</style>


至此,可以保证input[type=password]不会再有密码补全提示,并且也不会再切换页面时,会弹出密码保存弹窗。
但是有一个缺点,就是无法完美显示光标。如果用户手动输入和删除,使用起来会有一定的影响。


我想到过可以使用模拟光标,暂时不知道可行性。有哪位同学知道怎么解决的话,可以私信我,非常感谢


作者:香脆又可口
来源:juejin.cn/post/7265666505102524475
收起阅读 »

2024年,何去何从

如果生命只有35岁,我大抵可以活的绚烂放肆。 可是为了活到70岁,我不得不过得趋炎附势、唯唯诺诺。 2023年12月14日中午。望着公司窗外小河上清理水藻的船工,突然觉得人生好落寞。人生不知不觉已经过去了33个年头,生活过的一团糟,每每空闲就会很迷茫,工作...
继续阅读 »

如果生命只有35岁,我大抵可以活的绚烂放肆。


可是为了活到70岁,我不得不过得趋炎附势、唯唯诺诺。



89977a3eb68cfc3a82b415c9e006ec4.jpg


2023年12月14日中午。望着公司窗外小河上清理水藻的船工,突然觉得人生好落寞。人生不知不觉已经过去了33个年头,生活过的一团糟,每每空闲就会很迷茫,工作中也不知道未来方向在何方。似乎生活走到了一个十字路口,下一步的迈出千头万绪,让人举步不前。


关于读书


eade0765b64b6e958335725416a504b.jpg
最近董宇辉小作文事件在互联网上闹得沸沸扬扬。让我重新审视关于读书人这个称谓。董宇辉的出口成章,辞藻华丽,仿若腹有诗书气自华就是为他而写的一般。之前有一段时间,一直会保持每天至少抽出来半小时读书的习惯,这期间也读了很多好书,也推荐给朋友很多好书,俨然有一种自己是读者的错觉。但是,好景不长,慢慢的读书的习惯在各种乱七八糟的生活琐碎中消磨的也不多了。


2024年关于读书目标,希望自己能读完8本有意思的书籍吧。


以下推荐一些我往年读书挺有意思的书。(我读书有个特点,不会专门为了要从书中获取什么而读书,我单纯可能就是觉得这本书有趣,就会阅读,仁者见仁智者见智,推荐的不喜欢勿喷)



  • 我的二本学生

  • 焦虑的人

  • 时间的礼物

  • 牧羊少年的奇幻旅行

  • 清单革命:如何持续、正确、安全的把事情做好

  • 大雪将至

  • 无人生还

  • 古董局中局(全集)

  • 长安十二时辰

  • 罗布泊之咒


关于学习


作为程序员,最重要的事情,其实就是终身学习。


而我一直认为,一个人活在世上和其他人最大的差异变化,就是在于不断的学习。而我认为学习不光是对于书本中的知识的学习,更是对于人生百态、人情世故的学习。通过不断的学习,让自己的棱角变得圆滑,让自己的短板变的不那么明显。大白话就是通过不断的学习打磨,让自己变的装起来,活的不那么赤裸裸。


如果你觉得这个词,你认知中还是用褒贬来分辨,对于事物还是一味用对错来分辨。那么我觉得应该去学习,通过不断的书本阅读、不断的人情世故的打磨,让自己起来。


你可能不认同,但是你不得不承认,这个社会就是由人情世故组装而成的。你的不断学习是伪装也是武装,让你圈子变得不同。


学习和阅读是一辈子的事。额....我怀念单纯的我


2024学习方面,我个人计划主要是个方面。



  • Python爬虫 & js反编译深入

  • Android jetpack搞一搞

  • 单词背起来

  • 阅读习惯捡起来


关于工作


image.png


这个不重要。按部就班来~


作者:王先生技术栈
来源:juejin.cn/post/7312749480674574372
收起阅读 »

爆料 iPhone 史上最大的漏洞,你中招了吗

卡巴斯基的研究人员表示,黑客利用了 iPhone 极其隐蔽的软硬件漏洞,持续攻击了四年多 最近 iPhone 因为遭遇史上最复杂攻击,而登上了热搜,卡巴斯基的研究人员表示,黑客利用了 iPhone 极其隐蔽的软硬件漏洞,持续攻击了四年多,如果你收到了 iPh...
继续阅读 »

卡巴斯基的研究人员表示,黑客利用了 iPhone 极其隐蔽的软硬件漏洞,持续攻击了四年多



最近 iPhone 因为遭遇史上最复杂攻击,而登上了热搜,卡巴斯基的研究人员表示,黑客利用了 iPhone 极其隐蔽的软硬件漏洞,持续攻击了四年多,如果你收到了 iPhone 的安全补丁提示,那么赶快升级吧。


OpenAI 科学家 Andrej Karpathy 惊讶地表示:这绝对是我们迄今为止所见过的最为复杂的攻击链。从本次攻击的复杂程度来看,一次黑客攻击同时使用 4 个零日漏洞(也就是未被发现且无有效防范措施的漏洞)是 "极其罕见的",只有历史上著名的 "震网" 病毒攻击伊朗纳坦兹核工厂事件能达到这个级别(当时共利用 7 个漏洞,其中 4 个为零日漏洞)。


这次黑客的攻击手段非常复杂,攻击者只需向用户的 iPhone 发送一段恶意 iMessage 文本,无需用户点击或下载任何内容,就可以在用户不知情的情况下,获取到 iPhone 的最高级别 Root 权限,这应该是利用 Mac 系统大概 10 年都没有修复的一个字体的漏洞。



"iMessage 信息" 是苹果手机 "信息" 中的一种通信方式,可以向其他 iOS 设备、iPadOS 设备、Mac 电脑和 Apple Watch 发送文字、图片、视频和音乐等信息



当获取到 iPhone 最高级别 Root 权限,攻击者将能够在 iPhone 上安装恶意软件(间谍软件),从而收集诸如联系人、消息和位置数据等敏感信息,并传输到攻击者控制的服务器。


但是如果想成功利用这个漏洞,必须对 iPhone 最底层的机制有深入的了解,但是 iPhone 不是开源的系统,所以除了 iPhone 和 ARM 的人,几乎不会有其他人知道这个漏洞的存在。


这次这个漏洞的攻击代码,粗估高达数万行代码,写的非常的精巧复杂,这么高价值的漏洞,不会对个人进行打击,应该是针对非常重要的人物。


比如 2021 年 7 月,以色列发生了一起类似的事件,代号为 "飞马" 间谍软件攻击事件,它可以秘密安装在运行大多数版本的 iOS 和 Android 的手机(和其他设备) 上,这次的攻击持续了很多年,从 2014 年开始,一直持续到 2021 年 7 月媒体曝光之时,监听对象都是非常重要的人物。


但是如果黑客将这次的攻击代码开源,那么很多人都可以利用这个漏洞为所欲为了,造成的结果就是无差别攻击,这样对我们普通人就危险了,如果你收到了 iPhone 的安全补丁提示,那么赶快升级,转发给身边的朋友,提高警惕吧


这些年来无论在 Android 还是 iPhone, 都发现了相应的漏洞,iPhone 号称史上最安全的操作系统,都出现了这么严重的漏洞,这也再次说明了,无论多好的软件系统,都有不可避免的漏洞,一定会被人攻击。


比如在 2023 年 Android 手机上也被暴露一个漏洞,虽然这个漏洞很早被 Google 修复了,但是并不是所有人都会升级到新版本系统,所以某些大厂,利用这个被暴露出来漏洞,获取到 Android 手机上最高级别 Root 权限,攻击普通用户,控制他们的手机,获取用户大量的私人信息。而且这次攻击也持续了很多年,被曝光之时引起轩然大波,但是在其强大的财力和公关的操作下,事情很快平息了。


我一直认为技术应该服务于用户,而不是想方设法的利用公开的漏洞窃听用户的私人信息,去推送一些定制化私人广告。


全文到这里就结束了,感谢你的阅读,坚持原创不易,欢迎在看、点赞、分享给身边的小伙伴,我会持续分享原创干货!!!


作者:程序员DHL
来源:juejin.cn/post/7319748998377226292
收起阅读 »

年底喜提大礼包,分享一下日常

写在前面 元旦前喜提大礼包,因为公司的骚操作越来越多,福利越来越少,通勤时间太久等诸多原因所以主动要了裁员名额,现在给大家分享一下这几天的日常和心态吧。 找工作 看了一下最近的行情,基本是属于失业了,从离职到现在有差不多2周了,这2周里刷了一下Boss和拉钩,...
继续阅读 »

写在前面


元旦前喜提大礼包,因为公司的骚操作越来越多,福利越来越少,通勤时间太久等诸多原因所以主动要了裁员名额,现在给大家分享一下这几天的日常和心态吧。


找工作


看了一下最近的行情,基本是属于失业了,从离职到现在有差不多2周了,这2周里刷了一下Boss和拉钩,挂出来的职位倒是不少,我也投了几份简历,无一例外全都石沉大海,看来只有放个寒假过完年再说了。最近也没怎么刷面经,先好好的休息一段时间吧。


image.png


日常


这两周也没有出去玩之类的,因为老婆还没有放假,基本都是宅在家里,做做饭,玩玩游戏,写写私活,分享一下我家2只可爱的猫猫


eb4561e2e29b304bf091d2638caf153.jpg


展望


希望过完年可以找到满意的工作吧,实在不行也只能去外包了,最近也准备换换赛道,尝试一下自媒体。


写在后面


虽然失业了,但是心态上还好,有一点焦虑但是不多,可能是因为写私活占了一部分时间,没空去胡思乱想吧,希望各位待业大佬都能放平心态,好好提升自己,加油。


作者:hahayq
来源:juejin.cn/post/7320037969980702761
收起阅读 »

年会了,公司想要一个离线PC抽奖应用

web
年会了,公司想要一个离线PC抽奖应用 背景 公司年会需要一个能够支撑60000+人的抽奖程序,原本通过找 网页的开源项目 再定制化实现了效果,并成功运行再周年庆上;但是现在又到年会了,领导要求要能够在任何地方、任何人只要有一台电脑就能简单方便的定制自己的PC...
继续阅读 »

年会了,公司想要一个离线PC抽奖应用


封面截图.png


背景


公司年会需要一个能够支撑60000+人的抽奖程序,原本通过找 网页的开源项目 再定制化实现了效果,并成功运行再周年庆上;但是现在又到年会了,领导要求要能够在任何地方、任何人只要有一台电脑就能简单方便的定制自己的PC抽奖应用,所有就有了这么一个主题。


程序需求


以下是领导从其他地方复制粘贴过来的,就是想实现类似的效果而已。



  • 1、支持数字、字母、手机号、姓名部门+姓名、编号、身-份-证号等任意组合的抽奖形式。

  • 2、支持名单粘贴功能,从EXCEL、WORD、TXT等任何可以复制的地方复制名单数据内容,粘贴至抽奖软件中作为名单使用,比导入更方便。

  • 3、支持标题、副标题、奖项提示信息、奖品图片等都可以通过拖拽更改位置。

  • 4、支持内定指定中奖者。

  • 5、支持万人抽奖,非常流畅,中奖机率一致,保证公平性。

  • 6、支持中奖不重复,软件自动排除已中奖人员,每人只有一次中奖机会不会出现重复中奖。

  • 7、支持临时追加奖项、补奖等功能支持自定义公司名称、自定义标题。

  • 8、背景图片,音乐等。

  • 9、支持抽奖过程会自动备份中奖名单(不用担心断电没保存中奖名单)。

  • 10、支持任意添加奖项、标题文字奖项名额,自由设置每次抽奖人数设置不同的字体大小和列数。

  • 11、支持空格或回车键抽奖。

  • 12、支持临时增加摇号/抽奖名单,临时删掉不在场人员名单。


目前未实现的效果


有几个还没实现的



  1. 关于人员信息的任意组合抽奖形式,这边只固定了上传模板的表头,需要组合只能通过改excel的内容。

  2. 对于临时不在场名单,目前只能通过改excel表再上传才能达到效果。


技术选型


由于给的时间不多,能有现成的最好;最终选择下面的开源项目进行集成和修改。


说明:由于之前没看到有electron-vite-vue这个项目,所有自己粗略了用vue3+vite+electron开发了 抽奖程序 , 所以现在就是迁移项目的说明。


github开源项目



根据仓库的说明运行起来


动画.gif


修改web端代码并集成到electron


I 拆分页面


组件说明拼图.png


II 补充组件


​ 本人根据自己想法加了背景图片、奖品展示、操作按钮区、展示全部中奖人员名单这几个组件以及另外9个弹窗设置组件。


III 页面目录结构


目录结构.png

IV 最后就是对开源的网页抽奖项目进行大量的修改了,这里就不详细说了;因为变化太多了,一时半会想不起来改了什么。


迁移项目


I 迁移静态资源


静态资源.png


​ 关于包资源说明,这边因为要做离线的软件,所以我把固定要使用的包保存到本地了;


1. 引入到index.html中

引入资源.png


2. 引入图片静态资源

功能代码调整.png


II 迁移electron代码


说明:由于我之前写的一版代码是用js而不是ts,如果一下子全改为ts需要一些时间;所以嫌麻烦,我直接引用js文件了,后期有时间可以再优化一下。


功能代码调整.png




  1. 这时候先运行一下,看下有没有问题



​ 问题一:


问题一.png


​ 这个是因为 我之前的项目一直是用require 引入的;所以要把里面用到require都改为import引入方式;(在preload.ts里面不能用ESM导入的形式,会报语法错误,要用回require导入)


​ 问题二:


问题二.png


​ __dirname不是ESM默认的变量;改为


import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url))

III 迁移前端代码



  • 目录说明


前端功能代码.png



  • 然后一顿复制粘贴,运行,最后报错;


问题三.png
按提示来改 如下:


修改1.png



  • 问题2:资源报错
    资源报错.png
    修复:


资源变化.png



  • 接下来运行看下是否有问题;
    抽奖运行动画.gif
    ​ 运行成功

  • 下一步试一下功能


功能执行动画.gif
​ 功能报错了



  • 看后台错误打印并修复问题
    保存位置错误.png
    修改:
    路径保存源头.png

  • 再次尝试功能 - 成功
    功能执行动画2.gif


IV 一个流程下来


待使用-删除帧后-运行抽奖一个流程动画-gif.gif


打包安装运行


I 运行“npm run build”之后 报错了


打包-js报错.png
这里再次说明一下;由于本人懒得把原本js文件的代码 改为ts;要快速迁移项目 所以直接使用了js;导致打包报错了,所以需要再 tsconfig.json配置一下才行:


  "compilerOptions": {
"allowJs": true // 把这段加上去
},

II 图标和应用名称错误


default Electron icon is used  reason=application icon is not set
building block map blockMapFile=release\28.0.0\YourAppName-Windows-28.0.0-Setup.exe.blockmap

找到打包的配置文件(electron-builder.json5)进行修改:


1. 更改应用名称
"productName": "抽奖程序",

2. 添加icon图标
"win": {
"icon": "electron/controller/data/img/lottery_icon.ico", // ico保存的位置
},

III 打包后运行;资源路径报错了


打包后资源报错.png
打包后资源路径查询不到.png
由于上面的原因,需要把程序涉及读写的文件目录暴露出来;


1. 在构建配置中加入如下配置,将应用要读写的文件目录暴露出来
"extraResources": [
{
"from": "electron/assets",
"to": "assets"
}
],

剩下的就是要重新调整打包后的代码路径了,保证能够找到读写路径;
路径查找纠正.png


最后打包成功,运行项目


删除帧后-一个完整的流程-gif.gif


总结: 主打的要快速实现,所以这个离线pc抽奖程序还有很多问题,希望大家多多包容;


最后附上github地址:github.com/programbao/…
欢迎大家使用


作者:宝programbao
来源:juejin.cn/post/7319795736153210895
收起阅读 »

iOS 组件开发教程——手把手轻松实现灵动岛

1、先在项目里创建一个Widget Target2、一定要勾选 Include live Activity,然后输入名称,点击完成既可。3、在 Info.plist 文件中声明开启,打开 Info.plist 文件添加 NSSupportsLiveActivi...
继续阅读 »

1、先在项目里创建一个Widget Target


2、一定要勾选 Include live Activity,然后输入名称,点击完成既可。


3、在 Info.plist 文件中声明开启,打开 Info.plist 文件添加 NSSupportsLiveActivities,并将其布尔值设置为 YES。

4、我们创建一个IMAttributes,

struct IMAttributes: ActivityAttributes {
public typealias IMStatus = ContentState

public struct ContentState: Codable, Hashable {
var callName: String
var imageStr : String
var callingTimer: ClosedRange<Date>
}

var callName: String
var imageStr : String
var callingTimer: ClosedRange<Date>
}

5、灵动岛界面配置

struct IMActivityWidget: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: IMAttributes.self) { context in
// 创建显示在锁定屏幕上的演示,并在不支持动态岛的设备的主屏幕上作为横幅。
// 展示锁屏页面的 UI

} dynamicIsland: { context in
// 创建显示在动态岛中的内容。
DynamicIsland {
//这里创建拓展内容(长按灵动岛)
DynamicIslandExpandedRegion(.leading) {
Label(context.state.callName, systemImage: "person")
.font(.caption)
.padding()
}
DynamicIslandExpandedRegion(.trailing) {
Label {
Text(timerInterval: context.state.callingTimer, countsDown: false)
.multilineTextAlignment(.trailing)
.frame(width: 50)
.monospacedDigit()
.font(.caption2)
} icon: {
Image(systemName: "timer")
}
.font(.title2)
}
DynamicIslandExpandedRegion(.center) {
Text("\(context.state.callName) 正在通话中...")
.lineLimit(1)
.font(.caption)
.foregroundColor(.secondary)
}

}
//下面是紧凑展示内容区(只展示一个时的视图)
compactLeading: {
Label {
Text(context.state.callName)

} icon: {
Image(systemName: "person")
}
.font(.caption2)
} compactTrailing: {
Text(timerInterval: context.state.callingTimer, countsDown: true)
.multilineTextAlignment(.center)
.frame(width: 40)
.font(.caption2)
}
//当多个Live Activities处于活动时,展示此处极小视图
minimal: {
VStack(alignment: .center) {
Image(systemName: "person")


}
}
.keylineTint(.accentColor)
}
}
}

6、在需要的地方启动的地方调用,下面是启动灵动岛的代码

        let imAttributes = IMAttributes(callName: "wqd", imageStr:"¥99", callingTimer: Date()...Date().addingTimeInterval(0))

//初始化动态数据
let initialContentState = IMAttributes.IMStatus(callName: name, imageStr: "ia.imageStr", callingTimer: Date()...Date().addingTimeInterval(0))

do {
//启用灵动岛
//灵动岛只支持Iphone,areActivitiesEnabled用来判断设备是否支持,即便是不支持的设备,依旧可以提供不支持的样式展示
if #available(iOS 16.1, *) {
if ActivityAuthorizationInfo().areActivitiesEnabled == true{

}
} else {
// Fallback on earlier versions
}
let deliveryActivity = try Activity<IMAttributes>.request(
attributes: imAttributes,
contentState: initialContentState,
pushType: nil)
//判断启动成功后,获取推送令牌 ,发送给服务器,用于远程推送Live Activities更新
//不是每次启动都会成功,当已经存在多个Live activity时会出现启动失败的情况
if deliveryActivity.activityState == .active{
_ = deliveryActivity.pushToken
}
// deliveryActivity.pushTokenUpdates //监听token变化
print("Current activity id -> \(deliveryActivity.id)")
} catch (let error) {
print("Error info -> \(error.localizedDescription)")
}
6.此处只有一个灵动岛,当一个项目有多个灵动岛时,需要判断更新对应的activity

func update(name:String) {
Task {

let updatedDeliveryStatus = IMAttributes.IMStatus(callName: name, imageStr: "ia.imageStr", callingTimer: Date()...Date().addingTimeInterval(0))

for activity in Activity<IMAttributes>.activities{
await activity.update(using: updatedDeliveryStatus)
}
}
}

7、停止灵动岛

func stop() {
Task {
for activity in Activity<IMAttributes>.activities{
await activity.end(dismissalPolicy: .immediate)
}
}
}


收起阅读 »

被裁员后,去送外卖跑滴滴行得通吗?

一 近年来,职场的裁员和降薪已经屡见不鲜,不少同事和朋友领了“大红包”。 有的在疯狂找工作,有的暂时摆烂下来。 打开BOSS招聘,上面的岗位还是很多啊,需求量还是很大啊! 但是不好意思,其实和大部分人关系不是很大。 我们发现一个问题,大家都说找不到工作,但是企...
继续阅读 »


近年来,职场的裁员和降薪已经屡见不鲜,不少同事和朋友领了“大红包”。


有的在疯狂找工作,有的暂时摆烂下来。


打开BOSS招聘,上面的岗位还是很多啊,需求量还是很大啊!


但是不好意思,其实和大部分人关系不是很大。


我们发现一个问题,大家都说找不到工作,但是企业又在抱怨苦苦招不到人。


什么原因呢?


究其原因,中低端岗位虽然多,但是人数堪比考公务员,造成了狼多肉少的现象,如果你的牙齿不够长,不够硬,那么挺难,运气好的话可能能碰到,但是可能是生病的猎物。


高端岗位因为对薪资进行压缩,那些精英不想降低标准去,而精英只占社会群体的一小撮,大多都在寻找更好的机会,或者落差不大的机会。


所以就传出:企业招不到人,大把人找不到工作的现象。


但是很显然,我们大部分人很难突进成精英,就像码农大多很难成为CTO,架构师,领导者,运营大多无法成为总监......


我们大多数人注定就是一颗螺丝钉。


这是我们大部人的宿命,这是必须得承认的。


不少朋友说:妈的,实在不行,老子就去送外卖,去开滴滴,没什么大不了的!


貌似大家都认为这是自己职业的底线,把送外卖和开网约车作为人生的兜底方案。


但是不好意思,外卖,网约车也不是什么人都能去干的,现在门槛也高了,能赚到的越来越少。


你可能看到视频中外卖月薪超过2W,但是你不可能不知道这钱是怎么赚来的。


可以用那个梗来形容:多吗?拿命换的。


其实另外一个现实的问题是,想玩命,想卷也没机会啊,这绝非贩卖焦虑,这是铁打的事实!



在我读六年级的时候。


我的同桌是一个女同学,她父母都是出租车司机,说实话,那会儿,我可觉得出租车司机比编制牛逼多了。


所以她和我说话总是提高半个调,时不时言语中带出几个词,表明她父母是出租车司机。


那会儿,没有滴滴,没有曹操,没有T3,没有智能手机,没有跑黑车的。


所以,他们出租车司机吃得油光满面,合不拢嘴。


但是时代变了,现在你打一个出租车,和司机聊上两句,他就差点扑倒你怀里哭了起来。


打网约车也是如此,很多司机在你下车时,还客客气气对你说:可以给我个好评吗,谢谢你了。


为啥要好评呢,数据啊,数据好看,给你推的单子就多啊。


前几天看了一个视频,一个女网约车司机说:自己开了一两个月了,单子还是那么难接,再这样下去,吃不起饭了。


还别说,这些平台依旧将司机分为三六九等,等级越高,自然单子就多,等级低的,慢慢来吧。


也怪不了平台,大家都在这个城市里,单子就这么多,加入的司机越来越多,如果大家都是公平去抢单,显然不符合商业的发展。


除了各种平台的竞争,在出行方式上也是卷得一比。


刚开始是共享自行车,再到电瓶车,刚开始要押金,后面我干脆直接不要押金。


这还不够,我还送,一个月十来块钱,我可以让你把大腿肌肉练强壮,链条干起火花。


对于大城市中的打工人,距离远我选择地铁,距离近我选择自行车,小城市里面,我更愿意选择公交和共享电动车。


难啊,出租车司机哭生不逢时,网约车司机拍拍大腿:这TM就是人生!


......



外卖就好搞吗?


我一个朋友,多年前他是一个外卖资深玩家,是城市里面的蝙蝠侠,闭着眼睛都能找路,眼睛一眨就把外卖送到顾客手里。


五六年前,他在一个四五线线城市一个月都能赚取可观的收入。


2023年下半年,他又重新加入了外卖大军,但是干了四五个月,他顶不住了,直接走人,他当时还是在东莞送,东莞的人口不少哦。


我问他为啥不干了,他无奈说到:现在这个行业,狗看了都摇头。


高单价的单子抢不到,能抢到的单子价格又低。


一天跑200块钱都挺难。


可能你不信,但是这就是事实。


在东莞的对面,那是深圳,年轻人梦想的起点,无数人来到深圳,极少的人确实赚到钱了,但是更多的人都是处于深圳赚钱深圳花,一分别想带回家的状态。


这里的人多,如果肯干,加上有一定的策略,那么一个月跑万把块是可以的,但是会特别累。


更多的人其实是破不了万的。


除了行情问题,还要面对巨大的身体和心理压力,价值送外卖是一件比较危险的事。


很多人穿上黄袍不久,扛不住了,只能脱下。


外卖是有门槛的,它肯定会比你现在的工作辛苦得多,把它作为兜底方案,这是不现实的。


特别是现在就业形式的严峻,更多的人都加入这个行业,竞争大得不行,所以想从里面赚钱也是挺难得。



最后。


谈一下一个现实的问题。


有力无处使,有才无数施,干了活不重要,重要的是要有运气拿钱!


在社会劳动力过剩的形势下,个人的才能其实没多大用处,除非是大才,普才的话只能在夹缝中苟延残喘。


一网友说:躺了很久,发现996真的是福报,在这个畸形的环境里,有钱挣、有活干、有苦吃、有罪受真是一大幸事!

我们大多数人是讨厌职场中的奋斗逼和卷狗的,但是当现实当头一棒的时候,估计自己卷得比别人还厉害。


这其实和康风险能力有关,普通家庭,普通收入的工薪阶层,收入完全依赖于工资,但是要还房贷,车贷,养娃,所以基本上收入和支出持平。


那失业就是最可怕的事情。


现在市面上处于待业的人还是比较多,有力无处使。


因为市场上的业务基本处于平缓甚至下滑的状态,部分处于直线上升的业务自己又去不了。


所以难啊。


这样的形势下,我们普通人又该何去何从?


诸君怎么看?


作者:苏格拉的底牌
来源:juejin.cn/post/7319319374045970432
收起阅读 »

前端服务框架调研:Next.js、Nuxt.js、Nest.js、Fastify

web
概述 这次 Node.js 服务框架的调研将着点于各框架功能、请求流程的组织和介入方式,以对前端 Node.js 服务设计和对智联 Ada 架构改进提供参考,不过多关注具体实现。 最终选取了以下具有代表性的框架: Next.js、Nuxt.js:它们是分别与...
继续阅读 »

概述


这次 Node.js 服务框架的调研将着点于各框架功能、请求流程的组织和介入方式,以对前端 Node.js 服务设计和对智联 Ada 架构改进提供参考,不过多关注具体实现。


最终选取了以下具有代表性的框架:



  • Next.js、Nuxt.js:它们是分别与特定前端技术 React、Vue 绑定的前端应用开发框架,有一定的相似性,可以放在一起进行调研对比。

  • Nest.js:是“Angular 的服务端实现”,基于装饰器。可以使用任何兼容的 http 提供程序,如 Express、Fastify 替换底层内核。可用于 http、rpc、graphql 服务,对提供更多样的服务能力有一定参考价值。

  • Fastify:一个使用插件模式组织代码且支持并基于 schema 做了运行效率提升的比较纯粹的偏底层的 web 框架。


Next.js、Nuxt.js


这两个框架的重心都在 Web 部分,对 UI 呈现部分的代码的组织方式、服务器端渲染功能等提供了完善的支持。



  • Next.js:React Web 应用框架,调研版本为 12.0.x。

  • Nuxt.js:Vue Web 应用框架,调研版本为 2.15.x。


功能


首先是路由部分:



  • 页面路由:

    • 相同的是两者都遵循文件即路由的设计。默认以 pages 文件夹为入口,生成对应的路由结构,文件夹内的所有文件都会被当做路由入口文件,支持多层级,会根据层级生成路由地址。同时如果文件名为 index 则会被省略,即 /pages/users 和 /pages/users/index 文件对应的访问地址都是 users。

    • 不同的是,根据依赖的前端框架的不同,生成的路由配置和实现不同:

      • Next.js:由于 React 没有官方的路由实现,Next.js 做了自己的路由实现。

      • Nuxt.js:基于 vue-router,在编译时会生成 vue-router 结构的路由配置,同时也支持子路由,路由文件同名的文件夹下的文件会变成子路由,如 article.js,article/a.js,article/b.js,a 和 b 就是 article 的子路由,可配合 组件进行子路由渲染。





  • api 路由:

    • Next.js:在 9.x 版本之后添加了此功能的支持,在 pages/api/ 文件夹下(为什么放在pages文件夹下有设计上的历史包袱)的文件会作为 api 生效,不会进入 React 前端路由中。命名规则相同,pages/api/article/[id].js -> /api/article/123。其文件导出模块与页面路由导出不同,但不是重点。

    • Nuxt.js:官方未提供支持,但是有其他实现途径,如使用框架的 serverMiddleware 能力。



  • 动态路由:两者都支持动态路由访问,但是命名规则不同:

    • Next.js:使用中括号命名,/pages/article/[id].js -> /pages/article/123。

    • Nuxt.js:使用下划线命名,/pages/article/_id.js -> /pages/article/123。



  • 路由加载:两者都内建提供了 link 类型组件(LinkNuxtLink),当使用这个组件替代 标签进行路由跳转时,组件会检测链接是否命中路由,如果命中,则组件出现在视口后会触发对对应路由的 js 等资源的加载,并且点击跳转时使用路由跳转,不会重新加载页面,也不需要再等待获取渲染所需 js 等资源文件。

  • 出错兜底:两者都提供了错误码响应的兜底跳转,只要 pages 文件夹下提供了 http 错误码命名的页面路由,当其他路由发生响应错误时,就会跳转到到错误码路由页面。


在根据文件结构生成路由配置之后,我们来看下在代码组织方式上的区别:



  • 路由组件:两者没有区别,都是使用默认导出组件的方式决定路由渲染内容,React 导出 React 组件,Vue 导出 Vue 组件:

    • Next.js:一个普普通通的 React 组件:
      export default function About() {
      return <div>About usdiv>
      }


    • Nuxt.js:一个普普通通的 Vue 组件:







在编译构建方面,两者都是基于 webpack 搭建的编译流程,并在配置文件中通过函数参数的方式暴露了 webpack 配置对象,未做什么限制。其他值得注意的一点是 Next.js 在 v12.x.x 版本中将代码压缩代码和与原本的 babel 转译换为了 swc,这是一个使用 Rust 开发的更快的编译工具,在前端构建方面,还有一些其他非基于 JavaScript 实现的工具,如 ESbuild。


在扩展框架能力方面,Next.js 直接提供了较丰富的服务能力,Nuxt.js 则设计了模块和插件系统来进行扩展。


Nest.js


Nest.js 是“Angular 的服务端实现”,基于装饰器。Nest.js 与其他前端服务框架或库的设计思路完全不同。我们通过查看请求生命周期中的几个节点的用法来体验下 Nest.js 的设计方式。


先来看下 Nest.js 完整的的生命周期:



  1. 收到请求

  2. 中间件

    1. 全局绑定的中间件

    2. 路径中指定的 Module 绑定的中间件



  3. 守卫

    1. 全局守卫

    2. Controller 守卫

    3. Route 守卫



  4. 拦截器(Controller 之前)

    1. 全局

    2. Controller 拦截器

    3. Route 拦截器



  5. 管道

    1. 全局管道

    2. Controller 管道

    3. Route 管道

    4. Route 参数管道



  6. Controller(方法处理器)

  7. 服务

  8. 拦截器(Controller 之后)

    1. Router 拦截器

    2. Controller 拦截器

    3. 全局拦截器



  9. 异常过滤器

    1. 路由

    2. 控制器

    3. 全局



  10. 服务器响应


可以看到根据功能特点拆分的比较细,其中拦截器在 Controller 前后都有,与 Koa 洋葱圈模型类似。


功能设计


首先看下路由部分,即最中心的 Controller:



  • 路径:使用装饰器装饰 @Controller 和 @GET 等装饰 Controller 类,来定义路由解析规则。如:
    import { Controller, Get, Post } from '@nestjs/common'

    @Controller('cats')
    export class CatsController {
    @Post()
    create(): string {
    return 'This action adds a new cat'
    }

    @Get('sub')
    findAll(): string {
    return 'This action returns all cats'
    }
    }

    定义了 /cats post 请求和 /cats/sub get 请求的处理函数。

  • 响应:状态码、响应头等都可以通过装饰器设置。当然也可以直接写。如:
    @HttpCode(204)
    @Header('Cache-Control', 'none')
    create(response: Response) {
    // 或 response.setHeader('Cache-Control', 'none')
    return 'This action adds a new cat'
    }


  • 参数解析:
    @Post()
    async create(@Body() createCatDto: CreateCatDto) {
    return 'This action adds a new cat'
    }


  • 请求处理的其他能力方式类似。


再来看看生命周期中其中几种其他的处理能力:



  • 中间件:声明式的注册方法:
    @Module({})
    export class AppModule implements NestModule {
    configure(consumer: MiddlewareConsumer) {
    consumer
    // 应用 cors、LoggerMiddleware 于 cats 路由 GET 方法
    .apply(LoggerMiddleware)
    .forRoutes({ path: 'cats', method: RequestMethod.GET })
    }
    }


  • 异常过滤器(在特定范围捕获特定异常并处理),可作用于单个路由,整个控制器或全局:
    // 程序需要抛出特定的类型错误
    throw new HttpException('Forbidden', HttpStatus.FORBIDDEN)

    // 定义
    @Catch(HttpException)
    export class HttpExceptionFilter implements ExceptionFilter {
    catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp()
    const response = ctx.getResponse<Response>()
    const request = ctx.getRequest<Request>()
    const status = exception.getStatus()

    response
    .status(status)
    .json({
    statusCode: status,
    timestamp: new Date().toISOString(),
    path: request.url,
    })
    }
    }
    // 使用,此时 ForbiddenException 错误就会被 HttpExceptionFilter 捕获进入 HttpExceptionFilter 处理流程
    @Post()
    @UseFilters(new HttpExceptionFilter())
    async create() {
    throw new ForbiddenException()
    }


  • 守卫:返回 boolean 值,会根据返回值决定是否继续执行后续声明周期:
    // 声明时需要使用 @Injectable 装饰且实现 CanActivate 并返回 boolean 值
    @Injectable()
    export class AuthGuard implements CanActivate {
    canActivate(context: ExecutionContext): boolean {
    return validateRequest(context);
    }
    }

    // 使用时装饰 controller、handler 或全局注册
    @UseGuards(new AuthGuard())
    async create() {
    return 'This action adds a new cat'
    }


  • 管道(更侧重对参数的处理,可以理解为 controller 逻辑的一部分,更声明式):

    1. 校验:参数类型校验,在使用 TypeScript 开发的程序中的运行时进行参数类型校验。

    2. 转化:参数类型的转化,或者由原始参数求取二级参数,供 controllers 使用:


    @Get(':id')
    findOne(@Param('id', UserByIdPipe) userEntity: UserEntity) {
    // 使用 id param 通过 UserByIdPipe 读取到 UserEntity
    return userEntity
    }



我们再来简单的看下 Nest.js 对不同应用类型和不同 http 提供服务是怎样做适配的:



  • 不同应用类型:Nest.js 支持 Http、GraphQL、Websocket 应用,在大部分情况下,在这些类型的应用中生命周期的功能是一致的,所以 Nest.js 提供了上下文类 ArgumentsHostExecutionContext,如使用 host.switchToRpc()host.switchToHttp() 来处理这一差异,保障生命周期函数的入参一致。

  • 不同的 http 提供服务则是使用不同的适配器,Nest.js 的默认内核是 Express,但是官方提供了 FastifyAdapter 适配器用于切换到 Fastify。


Fastify


有这么一个框架依靠数据结构和类型做了不同的事情,就是 Fastify。它的官方说明的特点就是“快”,它提升速度的实现是我们关注的重点。


我们先来看看开发示例:


const routes = require('./routes')
const fastify = require('fastify')({
logger: true
})

fastify.register(tokens)

fastify.register(routes)

fastify.listen(3000, function (err, address) {
if (err) {
fastify.log.error(err)
process.exit(1)
}
fastify.log.info(`server listening on ${address}`)
})

class Tokens {
constructor () {}
get (name) {
return '123'
}
}

function tokens (fastify) {
fastify.decorate('tokens', new Tokens())
}

module.exports = tokens

// routes.js
class Tokens {
constructor() { }
get(name) {
return '123'
}
}

const options = {
schema: {
querystring: {
name: { type: 'string' },
},
response: {
200: {
type: 'object',
properties: {
name: { type: 'string' },
token: { type: 'string' }
}
}
}
}
}

function routes(fastify, opts, done) {
fastify.decorate('tokens', new Tokens())

fastify.get('/', options, async (request, reply) => {
reply.send({
name: request.query.name,
token: fastify.tokens.get(request.query.name)
})
})
done()
}
module.exports = routes

可以注意到的两点是:



  1. 在路由定义时,传入了一个请求的 schema,在官方文档中也说对响应的 schema 定义可以让 Fastify 的吞吐量上升 10%-20%。

  2. Fastify 使用 decorate 的方式对 Fastify 能力进行增强,也可以将 decorate 部分提取到其他文件,使用 register 的方式创建全新的上下文的方式进行封装。


没体现到的是 Fastify 请求介入的支持方式是使用生命周期 Hook,由于这是个对前端(Vue、React、Webpack)来说很常见的做法就不再介绍。


我们重点再来看一下 Fastify 的提速原理。


如何提速


有三个比较关键的包,按照重要性排分别是:



  1. fast-json-stringify

  2. find-my-way

  3. reusify



  • fast-json-stringify:
    const fastJson = require('fast-json-stringify')
    const stringify = fastJson({
    title: 'Example Schema',
    type: 'object',
    properties: {
    firstName: {
    type: 'string'
    },
    lastName: {
    type: 'string'
    }
    }
    })

    const result = stringify({
    firstName: 'Matteo',
    lastName: 'Collina',
    })


    • 与 JSON.stringify 功能相同,在负载较小时,速度更快。

    • 其原理是在执行阶段先根据字段类型定义提前生成取字段值的字符串拼装的函数,如:
      function stringify (obj) {
      return `{"firstName":"${obj.firstName}","lastName":"${obj.lastName}"}`
      }

      相当于省略了对字段值的类型的判断,省略了每次执行时都要进行的一些遍历、类型判断,当然真实的函数内容比这个要复杂的多。那么引申而言,只要能够知道数据的结构和类型,我们都可以将这套优化逻辑复制过去。



  • find-my-way:将注册的路由生成了压缩前缀树的结构,根据基准测试的数据显示是速度最快的路由库中功能最全的。

  • reusify:在 Fastify 官方提供的中间件机制依赖库中,使用了此库,可复用对象和函数,避免创建和回收开销,此库对于使用者有一些基于 v8 引擎优化的使用要求。在 Fastify 中主要用于上下文对象的复用。


总结



  • 在路由结构的设计上,Next.js、Nuxt.js 都采用了文件结构即路由的设计方式。Ada 也是使用文件结构约定式的方式。

  • 在渲染方面 Next.js、Nuxt.js 都没有将根组件之外的结构的渲染直接体现在路由处理的流程上,隐藏了实现细节,但是可以以更偏向配置化的方式由根组件决定组件之外的结构的渲染(head 内容)。同时渲染数据的请求由于和路由组件联系紧密也都没有分离到另外的文件,不论是 Next.js 的路由文件同时导出各种数据获取函数还是 Nuxt.js 的在组件上直接增加 Vue options 之外的配置或函数,都可以看做对组件的一种增强。Ada 的方式有所不同,路由文件夹下并没有直接导出组件,而是需要根据运行环境导出不同的处理函数和模块,如服务器端对应的 index.server.js 文件中需要导出 HTTP 请求方式同名的 GET、POST 函数,开发人员可以在函数内做一些数据预取操作、页面模板渲染等;客户端对应的 index.js 文件则需要导出组件挂载代码。

  • 在渲染性能提升方面,Next.js、Nuxt.js 也都采取了相同的策略:静态生成、提前加载匹配到的路由的资源文件、preload 等,可以参考优化。

  • 在请求介入上(即中间件):

    • Next.js、Nuxt.js 未对中间件做功能划分,采取的都是类似 Express 或 Koa 使用 next() 函数控制流程的方式,而 Nest.js 则将更直接的按照功能特征分成了几种规范化的实现。

    • 不谈应用级别整体配置的用法,Nuxt.js 是由路由来定义需要哪个中间件,Nest.js 也更像 Nuxt.js 由路由来决定的方式使用装饰器配置在路由 handler、Controller 上,而 Next.js 的中间件会对同级及下级路由产生影响,由中间件来决定影响范围,是两种完全相反的控制思路。

    • Ada 架构基于 Koa 内核,但是内部中间件实现也与 Nest.js 类似,将执行流程抽象成了几个生命周期,将中间件做成了不同生命周期内功能类型不同的任务函数。对于开发人员未暴露自定义生命周期的功能,但是基于代码复用层面,也提供了服务器端扩展、Web 模块扩展等能力,由于 Ada 可以对页面路由、API 路由、服务器端扩展、Web 模块等统称为工件的文件进行独立上线,为了稳定性和明确影响范围等方面考虑,也是由路由主动调用的方式决定自己需要启用哪些扩展能力。



  • Nest.js 官方基于装饰器提供了文档化的能力,利用类型声明( 如解析 TypeScript 语法、GraphQL 结构定义 )生成接口文档是比较普遍的做法。不过虽然 Nest.js 对 TypeScript 支持很好,也没有直接解决运行时的类型校验问题,不过可以通过管道、中间件达成。

  • Fastify 则着手于底层细节进行运行效率提升,且可谓做到了极致。同时越是基于底层的实现越能够使用在越多的场景中。其路由匹配和上下文复用的优化方式可以在之后进行进一步的落地调研。

  • 除此之外 swc、ESBuild 等提升开发体验和上线速度的工具也是需要落地调研的一个方向。




作者:智联大前端
来源:juejin.cn/post/7030995965272129567
收起阅读 »

裸辞四个月,前端仔靠着Nest绝境收下offer

经历时间轴 地点:上海 8.31做完后离职 9月份开始边复习边玩,轻松加愉快 10月中旬开始投递简历 11月月底绝望,期间仅有1次面试机会,伴随着的是各种焦虑 12月开始随遇而安,每天复习或者学习2小时,然后就是打游戏,刷剧,中旬突然开始了1波疯狂面试,1周...
继续阅读 »

经历时间轴



  • 地点:上海

  • 8.31做完后离职

  • 9月份开始边复习边玩,轻松加愉快

  • 10月中旬开始投递简历

  • 11月月底绝望,期间仅有1次面试机会,伴随着的是各种焦虑

  • 12月开始随遇而安,每天复习或者学习2小时,然后就是打游戏,刷剧,中旬突然开始了1波疯狂面试,1周8面,收到满意offer



后面开始述说离职原因、心理历程,这几个月我经历了什么?



离职原因



  1. 某独角兽 外包仔

  2. 无晋升空间,时间1年半,作为项目组第一个前端,我完成了80%工作,包括脚手架、组件库等基础建设,直到三四个月后才陆陆续续来了其他四五个前端,一块干活。一到填绩效都有我,每次表扬都有我,我都成标兵了。领导每个季度向上汇报时,都提出对我进行转正,从无名额为由到最后的不予回复。失望+1

  3. 9106式工作,阿谀奉承式的敏捷开发,从一开始还是有计划,有设计的迭代,到后面不断图快式开发,跨层汇报。后期演变为了,领导说这样做OK,做完了,领导的领导觉得不OK,重做另一种,做完后,领导的领导的领导觉得不OK,来回返工,以人力不停试错。失望+1

  4. 薪资差距,外包仔 工资每年仅有12个月工资,最低社保,最低公积金,做好做差都一样,没有任何加班工资,说好的调休,申请调休时,层层受阻,PUA不断,失望+1

  5. 僵硬、不思进取的氛围,领导最喜欢说的话,先上一版再说,并且去除了husky,eslint、tslint等提交校验,导致代码屎山不断,相似的功能、逻辑、同事一致在进行大批量复制,甚至有非常强的耦合性,不拆分组件、hook,甚至无视TS类型定义,VSCODE经常出现许多飘红文件,导致TS错误反馈链无法正常使用,维护难度急剧上涨。注定尿不到一个壶里了,失望+1


离职前对自己的认知


优势:



  • Vue3、Vue2、React函数式组件开发/class组件开发皆可

  • 近2年ts使用经验

  • 熟悉前端工程化,包括rollup、vite、webpack配置,脚手架开发,组件库搭建、monorepo式的包管理等。

  • 熟悉数据结构、设计模式,并擅长数据结构设计,保证可维护性的同时不停增强可扩展性

  • 算法方面,比不上各种大厂的前端,至少比下有余,leetcode刷过100多题,虽然都是简单中等难度,但至少强于大部分前端了吧

  • 工具方面,熟悉processon绘图工具的使用,包括绘制uml类图,脑图等


劣势:



  • 28岁,接近4年的前端开发经验

  • 大专学历

  • 外包仔


重拾技能:



  • 微信小程序开发,3年前开发过一个原生小程序,长久不用忘了

  • node方面之前还使用过Express、Koa打通mySQL,开发过几十个增删查改的接口

  • 跨段方面,以前的一段工作经历使用过UniApp开发过H5与小程序的跨段应用



重拾这部分技能后,对于寒冬中离职感觉也不是那么可怕,但后面现实还是泼了一盆凉水



离职后我是怎么做的?



  • 9月-10月我都是在整理、复习JS知识点、Vue、React框架方面的硬基础内容,期间开始了在掘金发文,也算是进行了知识点的分享,获得不少的收藏、点赞,非常有满足感

    • JS复习分享

    • Vue复习分享

    • React复习分享

    • 设计模式复习分享



  • 我的目标是中小厂自研,经过查看多家公司招聘要求后,发现中小厂对于跨端开发的执着,于是在此期间,学习了taro,感觉与uniapp一样,也能很快上手,本质也是多了一部分如同小程序般的json配置化。

  • 除此之外,每一家招聘要求上,都会有一条很显眼的要求:熟悉一种后端语言优先。招聘公司并非要求我们要真的去开发服务端,而是希望通过这种方式,降低前后端的沟通成本,并且使得双方达到一种平衡,避免前端后端间的撕逼。因此,我第一个想到了Nest,在学这个东西的时候,顺带还能复习一把以前的node开发知识,相比H5,小程序也更容易出圈。(尤其是碰到那些专注于前端开发的面试官,你能扯一部分服务端的东西,他也得一愣一愣的,因为他也不知道你对不对,你究竟有多对)


学习Nest过程中我收获了哪些东西?


通过学习,我对服务端开发套路更加清楚,前端是开发界面,接收数据,呈现数据;服务端则是负责提取或者写入数据,中间穿插着对数据的处理。当系统学习后我豁然开朗了不少。



  1. 多环境配置方案

    • 使用dotenv的简单数据配置

    • 使用js-yaml的复杂数据方式配置



  2. 数据库

    • 如何提升数据库使用效率?——ORM方案,如:typeORM、sequelize、prisma

    • 数据表间的关系有哪些?如何设计?——1对1、1对多/多对1、多对多;及三大设计范式,er图如何设计

    • 如果需要考虑数据库迁移,如何配置nest连接数据库?



  3. 日志统计

    • 为什么日志统计这么重要?

    • 常见日志方案——winston、pino

    • 日志如何分类?—— 错误日志、调试日志、请求日志

    • 日志记录位置有哪些?分别起什么作用?—— 控制台日志(方便调试)、文件日志(方便回溯与追踪)、数据库日志(敏感操作/数据 记录)



  4. 过滤器有哪些?有什么作用?—— 全局过滤器、控制器过滤器、路由过滤器;它们用来更友好地返回服务端的错误响应。

  5. 拦截器和过滤器区别是什么?拦截器主要用于在请求处理之前和之后对请求进行修改、干预或拦截。它们可以修改请求和响应的数据、转换数据格式、记录日志等,以及处理全局任务

  6. 面向对象式的开发方式,为什么老是看到JAVA里充斥着这么多的“注解”?对设计模式,模块的分类,层级的处理更上一个层次


当自我介绍时说出熟悉nest开发的变化


当有2家自研公司在对我面试时,我抛出了熟悉nest开发后。面试官感觉眼神都不一样了,这是真实的。然后这两家公司面试完后,我总结下来,1个小时有大概半小时都是在谈论服务端开发对前端的助益,更多的是关于fp开发oop开发的区别,有哪些收获?除此池外,我们还会不停探讨关于设计模式、数据库方面的话题,如:表关系、如何解耦之类的。也就是说,有面试官一直在挖掘你的深度、广度。你和面试官侃侃而谈,自然结果不会差!


另外还有两三家公司,谈到Nest或者node方面的东西时,面试官都是一句话带过,自然而然,他们不熟悉这方面的东西,你也可以反向面试出这家公司的深浅。


最后入职的公司


面试了两轮,大概4个小时不到,docker、服务端、前端、设计模式、规范、简单的算法,全都问了一遍。其实还有另外俩家备选的公司,都是到了二面三面,当我拿到这家公司offer后,婉拒了他们的面试,也少了一波问价的机会。


今天报道,朝九晚六,偶尔加班,最多8点。薪资也很满意。


感谢nest、感谢我的卷,值了!祝各位有个清晰的规划,能够快速上岸


作者:见信
来源:juejin.cn/post/7319330542100561932
收起阅读 »

2023总结:我在深圳做前端的第6年

入行前端已经6年了,一直有在掘金看技术文章的习惯。其实很早有想在掘金上写点什么,奈何个人技术水平太菜,实在不敢在各位大佬面前献丑;二来就是太懒,无法静下心来做一件事。但万事开头难,不踏出这一步,永远只能原地踏步了。今天就逼着自己记录一下这一年的经历,希望能有个...
继续阅读 »

入行前端已经6年了,一直有在掘金看技术文章的习惯。其实很早有想在掘金上写点什么,奈何个人技术水平太菜,实在不敢在各位大佬面前献丑;二来就是太懒,无法静下心来做一件事。但万事开头难,不踏出这一步,永远只能原地踏步了。今天就逼着自己记录一下这一年的经历,希望能有个好的开始!


年初找工作3个月


22年底我从干了两年的外包公司离职了。之所以在外包干了这么久,主要原因还是因为菜,另外可能就是我本人非科班入行,对外包也不是很抵触,毕竟福利方面跟第一家入职的自研公司也差不多,而且拿钱干活不丢人。


年初就过来准备找工作了,不过还是玩了大半个月,期间参加了朋友的婚礼,又去顺德玩了两天。就这样到了2月底,开始投简历面试。面试前也看了一些八股文,自己也做了总结笔记,但真正面试过程中,表达沟通能力是很重要的一方面,这方面自己是没什么优势的。面试期间一轮游的居多,有的感觉面的好的到二面了也由于没有后续而不了了之。经过了一个多月的面试,每周大概两三家的样子,人都面麻了,还是一个offer没拿到(期间其实通过了一家自研小公司的面试,但由于学历的问题,最终也黄了)。此时都有打算去其它城市看看,后来冷静想想还是打消了念头。后面又是经过了一个多月的零零星星的面试,终于在5月底拿到了一家外包的offer。


当时的面试题记



有同学可能好奇为啥找工作能这么久,大家应该都了解今年大环境的影响。另外就是学历问题,我是非统招学历,另外加上非科班,双buff加持是地狱级别也不为过。当然个人技术能力不行也占一方面。


当时是从外包裸辞的,以为可以很快找到下家,可现实给了我一记的抱拳。3个月期间没有收入,而且家里今年在装修新房,钱大部分都寄回家了,又不想让父母知道,最后只能在借呗借了2万先用着。


这里不得不说当时社会工作经验的欠缺,一般外包如果不主动离职的话,外包会给你安排面试,同时待业期间每月会给到深圳最底薪资,起码算是有个基本的生活保障。而且就算外包要裁你的话他们得给赔偿,另外自己也可以领到失业金。所以,以后在不能确保自己很快找到下家的情况下,千万不要裸辞啊。


社会给我另一记抱拳是让我真正意识到学历的重要性。在boss直聘上我沟通过的hr有上千家了,大部分了解到我的学历时都直接不回了。但现在后悔已经晚了,我已经无法拿到统招全日制学历了。后面有了解到软考,算是互联网技术人员能拿到的一个有一定含金量的职称,打算今年上半年能拿下。


新一家外包短暂的3个月


5月底入职了一家外包公司,被分派到给深圳的罗湖烟草局做项目。我们十几个项目人员在一间临时办公室里,其中4个前端5个后台外加测试和项目经理。前端项目还是比较简单,负责vue的pc端和uniapp的移动端,期间主要开发了一个电子烟的小模块,另外就是修复系统遗留bug。自己本来想着也是先干着,边工作边看看外面的机会,可没想到不到3个月就被通知说项目要撤了。只能说今年的就业环境真是堪忧!


办公楼下拍的夏天的棉花糖



前同事伸过来的橄榄枝


此时在上家还没有离职,另外也攒了大概一周的调休,也算有一点缓冲时间。后面就是准备新一轮面试考验了,投了很多家,收到的面试邀请也是寥寥,第一周面了3家的样子,其中一家面试通过但给到的薪资比期望的低很多,考虑之后还是拒绝了。


就在以为又是一场漫长的求职路时,第二周在家刷面试题时意外收到前同事的微信消息。说是看到我的简历了,想确认一下是不是我。同事是自从第一家离职后就比较少联系,平时也就偶尔朋友圈点赞之交。而且上半年由于失业的焦虑,我也没有再发朋友圈了。确认之后简单寒暄了一下,同事说他现在待的也是一家算是外包公司,加班比较严重,问我是否有意向。心想对我现在来说是一个难得机会,而且同事说可以走内推,因此一些无法预料的意外也可以避免,不多思考我便答应了。接下来就是去同事公司面试,没想到他就是前端负责人。所以也没问技术问题,就简单的聊了一下境况,然后就是等hr消息。第二天就hr就来电话了,然后就是顺利入职。


这次求职经历给我的感悟就是:平时有时间多跟朋友交流一下,增进下感情,兴许在你困难的时候,朋友可以提供一点帮助。毕竟在这个复杂的社会中,谁都不是孤独的存在,人情也就是在相互帮扶中建立的。


近况


入职新公司已有4个月了,公司实行大小周,每周1,2,4固定加班到8:30,加班强度在我看来还可以接受。给银行做的项目,每个月两次的上线节点,开发上基础设施像流水线Jenkins都是有的,就是上线流程上稍微繁琐了一点。前端技术栈pc端用的微前端,移动端是安卓和IOS内嵌H5,对我来说算是没接触过的技术了。但好在都是用的vue,上手起来也快。


年底乘着天气晴朗,去爬了一直想去的梅沙尖



2024展望


新的一年:


工作上希望能顺顺利利,业余时间持续提升技术能力,多花时间思考和总结。


生活上希望能多去接触自己喜欢的人和事,少一点迷茫,多一点开心!


flag:


1.在掘金上写5篇技术文章


2.看10本书(投资理财,个人成长,名人传记,文学小说都可以涉猎)


3.拿到软考证书(中级软件设计师)


4.谈女朋友(本人94年,快30岁了,妥妥的大龄剩男)


作者:wing98
来源:juejin.cn/post/7319700830076157988
收起阅读 »

35岁京东员工哭诉:我只是年龄大了,不是傻了残疾了,为什么不能拥有与年轻人平等的面试机会?

一位35岁的京东员工哭诉他并非因为年龄大就意味着智商下降、工作能力下降,更不是因为残疾而无法胜任工作,然而在当前经济寒冬的大环境下,为何他无法获得与那些拥有3~5年工作经验的年轻人一样的面试机会呢?这不仅仅是一个人的心声,更是一个普遍存在于社会底层的困境。大龄...
继续阅读 »

一位35岁的京东员工哭诉他并非因为年龄大就意味着智商下降、工作能力下降,更不是因为残疾而无法胜任工作,然而在当前经济寒冬的大环境下,为何他无法获得与那些拥有3~5年工作经验的年轻人一样的面试机会呢?

这不仅仅是一个人的心声,更是一个普遍存在于社会底层的困境。大龄员工在职场中所面临的困境,是一种对人才潜能的浪费,同时也反映出我们对于工作价值的认知是否真的应该被年龄所左右。这一问题不仅仅关乎一个人的个体命运,更触及到整个社会的公平和机会均等。年龄是否真的应该成为评判一个人能否胜任工作的唯一标准?年长者所积累的经验和智慧,不应该成为被忽视的财富。

有网友说:本质是体力 精力不行了,干的活都一样 肯定有限选年轻的。
这位网友说:的行业不行业没啥关系,除了师医公三个行业没啥年龄焦虑,其他还有哪个不焦虑,说白了就是中国人太多了,每年毕业生1000w,排队等着找工作
又一网友说:年龄大不好忽悠,不好pua了

网友小海豚说:还是要价太高,如果1w上下的岗位都没有的话才是真的凉了。

网友猫叔说:本质劳动力过剩,国家出台政策限制加班时长,劳动强度。增加市场劳动力需求
有网友说:因为中国企业的领导都害怕比自己年纪大的员工,不自信
网友小茄子说:主要还是卷吧。
网友小袁说:看行业,要是java就是这样,芯片硬件要好点
有网友说:因为国内老板和员工都喜欢996,35岁之后不管是身体还是家庭都要占用一部分精力。
又一网友说:卡学历都行凭什么不卡年龄呢,要做到一视同仁才公平铁子
网友榴莲说:不太能适应国内企业的工作节奏吧,每天有事没事至少10个小时起步。
网友小榛子说:你如果愿意收入打个折的话,还是能找到的
有网友说:是很无奈呀,根本没面试。行情不是一般的差
又有网友说:30都嫌弃 各种挑三拣四的 不知道在选秀还是干啥
有人认为,问题根本在于体力和精力逐渐减弱,而招聘方更愿意选择年轻人。有网友认为几乎所有行业都存在年龄焦虑,除了师医公三个行业,大部分行业都面临就业竞争激烈的问题,尤其是在中国人口众多的情况下。一些网友提到,劳动力过剩是根本原因。
也有人指出国内老板和员工对996的追求,以及领导对比自己年纪大的员工的担忧,导致了对大龄员工的排斥。有的网友认为,企业更看重的是年轻员工的卷取,而不是经验和智慧。有人指出35岁之后,身体和家庭都会占用一部分精力,不适应国内企业的工作节奏。也有网友认为,降低收入期望可能是找到工作的一种方法。


作者:Python开发
来源:mp.weixin.qq.com/s/tb3HF7Ub-7-IancG72stVA
收起阅读 »

一个优雅解决多个弹窗顺序显示方案

不是因为看到希望才坚持,而是因为坚持了才会有希望!场景  在做直播软件的时候,需要在用户打开App后,先后弹出签到,活动,提示等一系列弹窗。每个弹窗都要在前一个弹窗消失后弹出。于是就面临一个弹窗顺序问题,那时候对设计模式很陌生,不知道怎么更好的解决弹窗顺序问题...
继续阅读 »

不是因为看到希望才坚持,而是因为坚持了才会有希望!

场景

  在做直播软件的时候,需要在用户打开App后,先后弹出签到,活动,提示等一系列弹窗。每个弹窗都要在前一个弹窗消失后弹出。于是就面临一个弹窗顺序问题,那时候对设计模式很陌生,不知道怎么更好的解决弹窗顺序问题,都在下前一个弹窗取消或关闭时去加载后面一个弹窗。这样做虽然也能解决问题,但是实现并不优雅,如果在弹窗中间再添加一个其他类型的弹窗改动代价就变得很大,特别是当你是后来接手代码的新人,稍有不慎,就要背锅。怎么能简单而又优雅的解决这个问题呢?

思路

  开发者必读的23种设计模式,对于日常开发问题的解决提供了很好的思路,可以说几乎所有的优秀架构都离不开设计模式,这也是面试必问问题之一。23种设计模式中有一个责任链模式,为弹窗问题提供了解决方案,这也是我从okhttp源码中学习到的,读过okhttp的同学都知道,okhttp网络请求中的五大拦截器基于链式请求,使用起来简单高效。本篇文章同样也是基于责任链的思路来解决弹窗顺序问题。

代码

  1. 首页我们定义一个接口DialogIntercept,同时提供两个方法 intercept和show。
interface  DialogIntercept {
fun intercept(dialogIntercept: DialogChain)
fun show():Boolean
}

  所有的弹窗都需要实现DialogIntercept中的这两个方法。

  1. 自定义弹窗实现DailogIntercept接口。

● 弹窗


class FirstDialog(val context: Context) :DialogIntercept{

override fun intercept(dialogIntercept: DialogChain) {

}

override fun show():Boolean{
return true
}
}

  这里默认show()方法默认返回true,可根据业务逻辑决定弹窗是否显示。

  1. 提供一个弹窗管理类DialogChain,通过建造者模式创建管理类。根据弹窗添加的顺序弹出。
class DialogChain(private val builder: Builder) {
private var index = 0
fun proceed(){
............
...省略部分代码.....
.............
}
class Builder(){
var chainList:MutableList = mutableListOf()
fun addIntercept(dialogIntercept: DialogIntercept):Builder{
.....省略部分代码.....
return this
}
fun build():DialogChain{
return DialogChain(this)
}
}

}

效果

  为了测试效果,分别定义三个弹窗,FirstDialog,SecondDialog,ThirdDialog。按照显示顺序依次添加到DialogChain弹窗管理类中。

  1. 定义弹窗。

  由于三个弹窗代码基本相同,下面只提供FirstDialog代码。

class FirstDialog(val context: Context) :DialogIntercept{


override fun intercept(dialogIntercept: DialogChain) {
show(dialogIntercept)
}

override fun show():Boolean{
return true
}

private fun show(dialogIntercept: DialogChain){
AlertDialog.Builder(context).setTitle("FirstDialog")
.setPositiveButton("确定"
) { _, _ ->
dialogIntercept.proceed()
}.setNegativeButton("取消"
) { _, _ ->
dialogIntercept.proceed()
}.create().show()
}
}

2 . 分别将三个弹窗按照显示顺序添加到管理器中。

 DialogChain.Builder()
.addIntercept(FirstDialog(this))
.addIntercept(SecondDialog(this))
.addIntercept(ThirdDialog(this))
.build().proceed()
  1. 实现效果如下:

总结

  再优秀的架构,都离不开设计模式和设计原则。很多时候我们觉得架构师遥不可及,其实更多的时候是我们缺少一个想要进步的心。新的一年,新的起点,新的开始。


作者:IT小码哥
来源:juejin.cn/post/7319652739083108402
收起阅读 »

请给系统加个【消息中心】功能,因为真的很简单

我相信,打开一个带有社交类型的网站,你或多或少都可以看到如下的界面: 1)消息提示 2)消息列表 这样 这样 那,这就是我们今天要聊的【消息中心】。 1、设计 老规矩先来搞清楚消息中心的需求,再来代码实现。 我们知道在社交类项目中,有很多评论、点赞等数据...
继续阅读 »

我相信,打开一个带有社交类型的网站,你或多或少都可以看到如下的界面:


1)消息提示


Snipaste_2023-08-27_13-41-36.jpg


2)消息列表


这样


Snipaste_2023-08-27_13-42-25.jpg


这样


Snipaste_2023-08-27_16-41-30.jpg


那,这就是我们今天要聊的【消息中心】。


1、设计


老规矩先来搞清楚消息中心的需求,再来代码实现。


我们知道在社交类项目中,有很多评论、点赞等数据的产生,而如果这些数据的产生不能让用户感知到,那你们想想这会带来什么影响?



用户A:太鸡肋了,发布的内容被人评论点赞了,我居然看不到,下次不用了...


用户B:还好没用这个系统...



所以,看到这些结果我们是不是能够意识到一个健全的社交功能,是不是少不了这种通知用户的机制啊!而这种机制我就把他定义为【消息中心】功能。


再来拆分一下这四个字:消息中心



  1. 消息

  2. 中心


消息:这个可以是由我们自己定义,如:把帖子被用户评论当作一条消息,把评论被用户点赞也可以当作一条消息,甚至系统发布的通知也是一条消息。


中心:这个就是字面意思,将上面所提到的所有消息,归拢到一个地方进行展示。


上面我们也提到消息基本就是这两种:



  • 用户对用户:用户消息

  • 平台对用户:系统消息


针对用户消息,就类似这样,用户 A 给用户 B 的一条评论进行了点赞,那这个点赞动作就会产生一条消息,并且通知到用户 B 的一个存储消息的地方,这里通常就指用户的收件箱。这个收件箱就是专门用来存储用户发给用户的消息,而这个点对点的模式是不是就是推送模式啊!(A 推送消息给 B)


接着针对系统消息,就类似这样,平台管理人员发布了一条通知,告诉大家平台有啥 XXX 活动。那这个活动通知肯定是要让平台的所有用户都知道把,所以这个通知就要存在一个发件箱中。这个发件箱就是专门存储平台的通知,所有用户都来这个发件箱中读取消息就行,而这个一对多的模式是不是就是拉取模式啊!(所有用户都来拉取平台消息)


这样一来,我们根据不同的消息场景就抽出了一个基本的消息推拉模型,模型图如下:



Snipaste_2023-08-27_14-27-25.jpg



Snipaste_2023-08-27_14-59-50.jpg


针对这两种模式,不知道大家有没有看出区别,好像乍一看没啥区别,都是发消息,读消息,对吧!


没错,确实都是一个发,一个读,但是两者的读写频率确实有着巨大的差异。先来看推模型,一个普通用户发表了一条帖子,然后获得了寥寥无几的评论和赞,这好似也没啥特别之处,对吧!那如果这个普通用户发表的帖子成为了热门帖子呢,也即该贴子获得了上万的评论和赞。那,你们想想是不是发消息的频率非常高,而该普通用户肯定是不可能一下子读取这么多消息的,所以是不是一个写多读少的场景。再来看看拉模型,如果你的平台用户人数寥寥无几,那倒没啥特别之处,但如果用户人数几万甚至几十万。那,每个用户都过来拉取系统消息是不是就是一个读频率非常高,而发消息频率非常低(系统消息肯定不会发的很快),所以这是不是一个读多写少的场景。


1.1 推:写多读少


针对这个模式,我们肯定是要将写这个动作交给性能更高的中间件来处理,而不是 MySQL,所以此时我们的 RocketMQ 就出来了。


当系统中产生了评论、点赞类的高频消息,那就无脑的丢给 MQ 吧,让其在消息中间件中呆会,等待消费者慢慢的将消息进行消费并发到各个用户的收件箱中,就类似下面这张图的流程:


Snipaste_2023-08-27_15-45-46.jpg


2.2 拉:读多写少


那对于这个模式,所实话,我觉得不用引入啥就可以实现,因为对于读多的话无非就是一个查,MySQL 肯定是能搞定的,即使你的用户几万、几十万都是 ok 的。


但咱们是不是可以这样想一下,一个系统的官方通知肯定是不多的,或者说几天或者几个星期一次,且一旦发送就不可更改。那是不是可以考虑缓存,让用户读取官方通知的时候走缓存,如果缓存没有再走 MySQL 这样应该是可以提高查询效率,提高响应速度。


具体流程如下图:


Snipaste_2023-08-27_15-57-21.jpg


2.3 表结构设计


基本的业务流程已经分析的差不多了,现在可以把表字段抽一下了,先根据上面分析的,看看我们需要那些表:



  1. 用户收件箱表

  2. 系统发件箱表


看似好像就这两张表,但是应该还有第三张表:



  1. 用户读取系统消息记录表



我们看到页面是不是每次有一条新的消息都会有一个小标点记录新消息数量,而第三张表就是为了这个作用而设计的。


具体原理如下:



  1. 首先运营人员发布的消息都是存储在第二张表中,这肯定是没错的

  2. 那用户每次过来拉取系统消息时,将最近拉取的一条消息写入到第三种表中

  3. 这样等用户下次再来拉取的时候,就可以根据第三张表的读取记录,来确定他有几条系统消息未查看了


可能有人会发出疑问:那用户的收件箱为啥不出一个用户读取记录表呢!


这个很简单,因为收件箱中的数据已经表示这个用户需要都这些个消息了,只是不知道那些是已读的那些是未读的,我们只需要再收件箱表中加一个字段,这个字段的作用就是记录最新一次读取的消息 ID 就行,等下次要读消息时,找到上传读取读取消息的记录ID,往后读新消息即可。



好,现在来看看具体的表字段:


1)用户收件箱表(sb_user_inbox)



  • id

  • 消息数据唯一 id:MQ唯一消息凭证

  • 消息类型:评论消息或者点赞消息

  • 帖子id:业务id

  • 业务数据id:业务id

  • 内容:消息内容

  • 业务数据类型:业务数据类型(商品评论、帖子、帖子一级评论、帖子二级评论)

  • 发起方的用户ID:用户 A 对用户 B 进行点赞,那这就是用户 A 的ID

  • 接收方的用户ID:用户 B 的 ID

  • 用户最新读取位置ID:用户最近一次读取记录的 ID


SQL


CREATE TABLE `sb_user_inbox` (
`id` bigint(20) NOT NULL,
`uuid` varchar(128) COLLATE utf8mb4_german2_ci NOT NULL COMMENT '消息数据唯一id',
`message_type` tinyint(1) NOT NULL COMMENT '消息类型',
`post_id` bigint(20) DEFAULT NULL COMMENT '帖子id',
`item_id` bigint(20) NOT NULL COMMENT '业务数据id',
`content` varchar(1000) COLLATE utf8mb4_german2_ci DEFAULT NULL COMMENT '内容',
`service_message_type` tinyint(1) NOT NULL COMMENT '业务数据类型',
`from_user_id` bigint(20) NOT NULL COMMENT '发起方的用户ID',
`to_user_id` bigint(20) NOT NULL COMMENT '接收方的用户ID',
`read_position_id` bigint(20) DEFAULT '0' COMMENT '用户最新读取位置ID',
`create_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `un01` (`uuid`),
UNIQUE KEY `un02` (`item_id`,`service_message_type`,`to_user_id`),
KEY `key` (`to_user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_german2_ci

可以看到,我加了很多业务相关的字段,这个主要是为了方便查询数据和展示数据。


2)系统发件箱表(sb_sys_outbox)



  • id

  • 内容


SQL


CREATE TABLE `sb_sys_outbox` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`content` varchar(2000) COLLATE utf8mb4_german2_ci NOT NULL COMMENT '内容',
`create_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_german2_ci

这个表就非常简单了,没啥业务字段冗余。


3)用户读取系统消息记录表(sb_user_read_sys_outbox)



  • id

  • 系统收件箱数据读取id

  • 读取的用户id


SQL


CREATE TABLE `sb_user_read_sys_outbox` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`sys_outbox_id` bigint(20) NOT NULL COMMENT '系统收件箱数据读取id',
`user_id` bigint(20) NOT NULL COMMENT '读取的用户id',
PRIMARY KEY (`id`),
UNIQUE KEY `un` (`user_id`),
KEY `key` (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=17 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_german2_ci

ok,这是消息中心所有分析阶段了,下面就开始实操。


2、实现


先来引入引入一下 RocketMQ 的依赖




org.apache.rocketmq
rocketmq-spring-boot-starter
2.2.1


RocketMQ 的双主双从同步刷新集群搭建教程:blog.csdn.net/qq_40399646…


MQ 配置:


Snipaste_2023-08-27_16-26-09.jpg


2.1 生产者


先来实现生产者如何发送消息。


1)消息体对象:LikeAndCommentMessageDTO


位置:cn.j3code.config.dto.mq


@Data
public class LikeAndCommentMessageDTO {

/**
* 该消息的唯一id
* 业务方可以不设置,如果为空,代码会自动填充
*/

private String uuid;

/**
* 消息类型
*/

private UserCenterMessageTypeEnum messageType;

/**
* 冗余一个帖子id进来
*/

private Long postId;

/**
* 业务数据id
*/

private Long itemId;

/**
* 如果是评论消息,这个内容就是评论的内容
*/

private String content;

/**
* 业务数据类型
*/

private UserCenterServiceMessageTypeEnum serviceMessageType;

/**
* 发起方的用户ID
*/

private Long fromUserId;

/**
* 接收方的用户ID
*/

private Long toUserId;


/*
例子:
用户 A 发表了一个帖子,B 对这个帖子进行了点赞,那这个实体如下:
messageType = UserCenterMessageTypeEnum.LIKE
itemId = 帖子ID(对评论进行点赞,就是评论id,对评论进行回复,就是刚刚评论的id)
serviceMessageType = UserCenterServiceMessageTypeEnum.POST(这个就是说明 itemId 的 ID 是归于那个业务的,方便后续查询业务数据)
fromUserId = 用户B的ID
toUserId = 用户 A 的ID
*/

}

2)发送消息代码


位置:cn.j3code.community.mq.producer


@Slf4j
@Component
@AllArgsConstructor
public class LikeAndCommentMessageProducer {

private final RocketMQTemplate rocketMQTemplate;

/**
* 单个消息发送
*
*
@param dto
*/

public void send(LikeAndCommentMessageDTO dto) {
if (Objects.isNull(dto.getUuid())) {
dto.setUuid(IdUtil.simpleUUID());
}
checkMessageDTO(dto);
Message message = MessageBuilder
.withPayload(dto)
.build();
rocketMQTemplate.send(RocketMQConstants.USER_MESSAGE_CENTER_TOPIC, message);
}

/**
* 批量消息发送
*
*
@param dtos
*/

public void send(List dtos) {
/**
* 将 dtos 集合分割成 1MB 大小的集合
* MQ 批量推送的消息大小最大 1MB 左右
*/

ListSizeSplitUtil.split(1 * 1024 * 1024L, dtos).forEach(items -> {
List> messageList = new ArrayList<>(items.size());
items.forEach(dto -> {
if (Objects.isNull(dto.getUuid())) {
dto.setUuid(IdUtil.simpleUUID());
}
checkMessageDTO(dto);
Message message = MessageBuilder
.withPayload(dto)
.build();
messageList.add(message);
});
rocketMQTemplate.syncSend(RocketMQConstants.USER_MESSAGE_CENTER_TOPIC, messageList);
});
}

private void checkMessageDTO(LikeAndCommentMessageDTO dto) {
AssertUtil.isTrue(Objects.isNull(dto.getMessageType()), "消息类型不为空!");
AssertUtil.isTrue(Objects.isNull(dto.getItemId()), "业务数据ID不为空!");
AssertUtil.isTrue(Objects.isNull(dto.getServiceMessageType()), "业务数据类型不为空!");
AssertUtil.isTrue(Objects.isNull(dto.getFromUserId()), "发起方用户ID不为空!");
AssertUtil.isTrue(Objects.isNull(dto.getToUserId()), "接收方用户ID不为空!");
}


/**
* 发送点赞消息
*
*
@param messageType 消息类型
*
@param serviceMessageType 业务类型
*
@param itemToUserIdMap 业务ID对应的用户id
*
@param saveLikeList 点赞数据
*/

public void sendLikeMQMessage(
UserCenterMessageTypeEnum messageType,
UserCenterServiceMessageTypeEnum serviceMessageType,
Map itemToUserIdMap, List saveLikeList)
{
if (CollectionUtils.isEmpty(saveLikeList)) {
return;
}
List dtos = new ArrayList<>();
for (Like like : saveLikeList) {
LikeAndCommentMessageDTO messageDTO = new LikeAndCommentMessageDTO();
messageDTO.setItemId(like.getItemId());
messageDTO.setMessageType(messageType);
messageDTO.setServiceMessageType(serviceMessageType);
messageDTO.setFromUserId(like.getUserId());
messageDTO.setToUserId(itemToUserIdMap.get(like.getItemId()));
dtos.add(messageDTO);
}
try {
send(dtos);
} catch (Exception e) {
//错误处理
log.error("发送MQ消息失败!", e);
}
}
}

注意:这里我用了 MQ 批量发送消息的一个功能,但是他有一个限制就是每次只能发送 1MB 大小的数据。所以我需要做一个功能工具类将业务方丢过来的批量数据进行分割。


工具类:ListSizeSplitUtil


位置:cn.j3code.config.util


public class ListSizeSplitUtil {

private static Long maxByteSize;

/**
* 根据传进来的 byte 大小限制,将 list 分割成对应大小的 list 集合数据
*
*
@param byteSize 每个 list 数据最大大小
*
@param list 待分割集合
*
@param
*
@return
*/

public static List> split(Long byteSize, List list) {
if (Objects.isNull(list) || list.size() == 0) {
return new ArrayList<>();
}

if (byteSize <= 100) {
throw new RuntimeException("参数 byteSize 值不小于 100 bytes!");
}
ListSizeSplitUtil.maxByteSize = byteSize;


if (isSurpass(List.of(list.get(0)))) {
throw new RuntimeException("List 中,单个对象都大于 byteSize 的值,分割失败");
}

List> result = new ArrayList<>();

List itemList = new ArrayList<>();
for (int i = 0; i < list.size(); i++) {
itemList.add(list.get(i));

if (isSurpass(itemList)) {
i = i - 1;
itemList.remove(itemList.size() - 1);
result.add(new ArrayList<>(itemList));
itemList = new ArrayList<>();
}
}
result.add(new ArrayList<>(itemList));
return result;
}


private static Boolean isSurpass(List obj) {
// 字节(byte)
long objSize = RamUsageEstimator.sizeOfAll(obj.toArray());
return objSize >= ListSizeSplitUtil.maxByteSize;
}
}

至此呢,生产者的逻辑就算是完成了,每次有消息的时候就调用这个方法即可。


2.2 消费者


位置:cn.j3code.user.mq.consumer


@Slf4j
@Component
@AllArgsConstructor
@RocketMQMessageListener(topic = RocketMQConstants.USER_MESSAGE_CENTER_TOPIC,
consumerGr0up = RocketMQConstants.GR0UP,
messageModel = MessageModel.CLUSTERING,
consumeMode = ConsumeMode.CONCURRENTLY
)

public class LikeAndCommentMessageConsumer implements RocketMQListener {

private final UserInboxService userInboxService;

@Override
public void onMessage(LikeAndCommentMessageDTO message) {
userInboxService.saveMessage(message);
}
}

saveMessage 方法的逻辑就是将消息保存到 MySQL 中,至此消息的产生和存储就算完成了,下面来看看用户如何查看吧!


2.3 用户消息查看


对于用户查看普通的消息就是访问一下 MySQL,并且更新一下最新读取的字段值即可,我贴一下关键代码就行了,代码如下:


public IPage page(UserMessagePageRequest request) {
// 获取消息
IPage page = getBaseMapper().page(new Page(request.getCurrent(), request.getSize()), request);

if (CollectionUtils.isEmpty(page.getRecords())) {
return page;
}
// 记录一下消息读取位置,默认进来就把全部消息读完了,类似掘金
if (request.getCurrent() == 1) {
if (Objects.isNull(page.getRecords().get(0).getReadPositionId()) ||
page.getRecords().get(0).getReadPositionId() == 0) {
UserInbox userInbox = new UserInbox();
userInbox.setId(page.getRecords().get(0).getId());
userInbox.setReadPositionId(userInbox.getId());
updateById(userInbox);
}
}
return page;
}

2.4 系统消息查看


对于系统消息的查看也是,只贴出关键代码,查询和更新读取记录逻辑,代码如下:


@Override
public IPage lookSysPage(SysOutboxPageRequest request) {
Page page = lambdaQuery()
.orderByDesc(SysOutbox::getId)
.page(new Page<>(request.getCurrent(), request.getSize()));
IPage outboxVOIPage = page.convert(userInboxConverter::converter);
if (CollectionUtils.isEmpty(outboxVOIPage.getRecords())) {
return outboxVOIPage;
}
// 记录一下消息读取位置,默认进来就把全部消息读完了,类似掘金
if (request.getCurrent() == 1) {
userReadSysOutboxService.updateReadLog(page.getRecords().get(0).getId(), SecurityUtil.getUserId());
}
return outboxVOIPage;
}

这里,可能有人会发现,没有按照上面分析的那用从缓存中读,是的。这里的实现我没有用到 Redis,这里我偷了一下懒,如果有拿到我代码的同学可以试着优化一下这个逻辑。


作者:J3code
来源:juejin.cn/post/7274922643453853735
收起阅读 »

程序员真的需要双显示器吗?

我最近思考一个问题,用双显示器编程是否比单显效率高? 如果是的话那么是否显示器越多效率越高呢? 有些同学肯定会马上回答,那肯定觉得是对的! 一个屏幕用来看文档,一个屏幕用来写代码,肯定会比都挤在一个屏幕效率高呀, 如果桌子够大的话,最好再加一个屏幕专门用来聊...
继续阅读 »

我最近思考一个问题,用双显示器编程是否比单显效率高?



如果是的话那么是否显示器越多效率越高呢?


有些同学肯定会马上回答,那肯定觉得是对的!


一个屏幕用来看文档,一个屏幕用来写代码,肯定会比都挤在一个屏幕效率高呀, 如果桌子够大的话,最好再加一个屏幕专门用来聊微信,那我就不用切换窗口,那效率肯定就爆表了。


但你有没有总结一下,在你编程的时候,具体为什么单显示器会比双显示器比效率低呢?


低效的原因


以下是我总结出的2点原因:



  • 第一点就是切换窗口,因为即使在最新操作系统中,使用单显示器的情况下,无论是用鼠标,command tab ,还是四指上扫调度中心,切换窗口仍然是非常麻烦的一件事 。
    但是你如果用多显示器,就可以避免在两个窗口之间切换。两个窗口放在两个显示器里,只要晃头就行了

  • 第二点是,你会有同时显示两个窗口的需求,比如一个文档,一个ide,你需要一边看文档,一边敲代码,这用一个显示器就不太容易做到,而且有时需要调整各个窗口大小,这是非常浪费时间的。


以上两个问题就是导致你效率低下的最重要的问题,如果可以成功解决,那么使用单屏编程就不是个问题了


解决的要点:



  • 不要用调度中心,不要用command + tab 来切换窗口, 把你每个常用的软件都做成超级快捷键,肌肉记忆直接拉起

  • 学会使用窗口管理工具,配合超级快捷键,快速调整窗口,不要尝试用鼠标拉窗口,手不要离开键盘


如何定义超级快捷键



超级快捷键 = Hyper + 任意键



为了设置超级快捷键,第一步就需要定义一个 Hyper 键,也就是超级键


为了让 Hyper 键不与系统或其他软件冲突,通常会把 Shift + Control + Option + command 一起按,也就是⌃ ⌥ ⌘ ⇧ 这4个键一起按作为 Hyper 键


image.png


但是键盘上并没有一个实体的 Hyper键,我们又不可能手动按住4个键, 所以我们会用软件把大小写切换键 Capslock 改成 Hyper key


因为大小写切换键,就在小指头的旁边,是键盘上占着最好位置的按键,却是一点用都没有的一个键,改完以后当再按住 Capslock键 的时候,实际上相当于按住了4个按键,也就是按住了 Hyper键


image.png


我用到的改键软件是 Karabiner-Element


打开 Karabiner 找到 Complex Modifications 点击 Add predefined rule


image.png


点击 import more rules from the internet 这时会打开浏览器,搜索 CapsLock plus 点击 import


image.png


这里我只 enable 了两条规则



  • CapsLock to Hyper/Escape

  • Hyper Cursor navigation


当然你可以根据需要 edit -》 save 来修改


修改后的配置文件会保存在下边这个位置


~/.config/karabiner/karabiner.json


这是我最终修改后的配置文件,放在下边这里,你可以去参考


github.com/nshen/dotfi…


至此你的超级键就定义好了。


用超级快捷键切换桌面


用超级快捷键来切换桌面需要到设置中把对应的快捷键定义成 Hyper键 + 对应的键,我这里设置成了 Hyper + [Hyper + ]


image.png


具体位置在



设置 -> 键盘 -> 键盘快捷键 - 调度中心 - 调度中心 - 向左/向右移动一个空间



用超级快捷键启动应用


首先安装 Raycast,因为Raycast是目前最优秀的应用启动器,并且可以给每个应用定义快捷键,所以我们用 Raycast 来给每个常用的应用都加上超级快捷键。


打开后 command + k


image.png


打开菜单选择配置


image.png


在这里给应用设置快捷键为 Hyper + 任意键


image.png


以下是我常用的软件设置


image.png


用超级快捷键管理窗口


Raycast 本身就提供了窗口管理功能,非常好用


image.png


我们可以用和应用一样的方法,给这些功能加上快捷键


image.png


我常用的快捷键有


image.png


单屏的优势


当你熟练的使用上述技巧,你就基本上可以使用单屏达到多屏开发同样的效率。


单屏也有单屏的优势,使用单屏,你可以获得以下多屏没有的好处



  • 不用摇头晃脑了,你会更容易专注于一件事,不会被其他屏幕上的应用影响,其他应用只有在需要的时候才会被调出来

  • 不用在桌面上放一堆显示器,节省了桌面空间,桌面会更整洁

  • 便携,拿起笔记本,不用依赖外部显示器,就可以随时随地最高效编程

  • 最重要的是省钱,再也不用考虑买或升级显示器了,一个笔记本搞定!


感谢观看,喜欢类似文章,还请点个关注,谢谢


作者:nshen
来源:juejin.cn/post/7319541571279798310
收起阅读 »

三十而立,我走过的路

你好,我是逗逗青,目前在某领先海外互联网公司担任技术leader,主要从事平台架构相关的研发和管理工作。 2023年即将进入尾声,我的职场生涯也走过了十年,在这篇文章里,我想聊聊我的过去,聊聊这些年我走过的一些弯路,以及得到的一些收获,希望对你能有一些启发。 ...
继续阅读 »

你好,我是逗逗青,目前在某领先海外互联网公司担任技术leader,主要从事平台架构相关的研发和管理工作。


2023年即将进入尾声,我的职场生涯也走过了十年,在这篇文章里,我想聊聊我的过去,聊聊这些年我走过的一些弯路,以及得到的一些收获,希望对你能有一些启发。


逗逗青的求学记


我来自广东一个沿海的农村,是家中长子,家族里学识最高的是我爸。


嗯,他初中学历。


我爸曾和我聊起他辍学的原因:害怕罚站。


他说:当年上学时,路比较远,家里只有一辆破烂的自行车,每次上学路上,这车常坏,经常变成走路推车上学。所以经常迟到被罚站,碍于面子就辍学花钱去拜师学做油漆工了。


我不知道我爸的选择是否正确,但我很庆幸,还好我爸习得了一门手艺,否则,我可能无法顺利上大学。


普通的小学


思绪回到1998年,那会我刚上小学,九年义务教育政策一直吊在我的尾巴:我一毕业,学弟学妹们就开始免收学费。


没赶上好的时代,我的求学生涯只能靠一担担的番薯喂养出来,至今我还记得很清楚,我一学期的学费大约等于一拖拉机的番薯。


这其中,还不包括将番薯从田地里一担一担来地来回走几百米挑到拖拉机上的人工费。


遇到收成不好的时节,我爸就得挨家挨户去给我借钱凑学费了。


因我爸老实本分,除了务农,也有油漆工这门手艺在手,在困难时,常有人能伸伸援手。


虽然家人很辛苦的供我上学,但在整个小学时期,我的成绩一直很普通,小学6年基本与各种奖状无缘。


一开始,我很羡慕那些在学期末可以拿着各种奖状,然后带着欢笑回家的同学。


到后来,我发现家里一到下雨天就会漏水,奖状拿回家贴墙上估计也很快受潮,所以也就释怀了。


逆袭的初中


2004年,进到初中时期,我的学习成绩发生了转变。


在那会,我们学校将班级划分为十几个普通班和四个重点班。


资质一般的我考进了普通班,但幸运的是,我们班级的隔壁就是重点班,更幸运的是,里边还有个我同村的亲戚。


因初中学校离家有些距离,我们一般会骑自行车上学,所以平时我们走得比较近,偶尔会聊聊学习方面的内容。


通过与这位亲戚的交流,我才开始意识到学习也是需要掌握一些技巧的,比如需要多刷题实战,特别是高频题。


慢慢地,我改掉了自己的一些学习陋习,比如埋头苦读背诵书本知识。


我的学习成绩也开始逐步提升。印象中,我当时最好的科目是数学,而且还摸清了数学的出题规律,每次考试,除了最后一道大题,基本都可以稳拿分数。


后来我的成绩排到了班里的第1名,并且升初二时,在班主任的帮助下调进了重点班。


到了重点班,我才明白重点班和普通班的本质区别。


这里,充满了竞争!


有的同学规划清晰,知道自己要去往何处,有的同学纯粹是年少轻狂,喜欢比拼成绩。


而我,则是盯上了那个为数不多的名额。


进了重点班后我才知道,学校和一所不错的高中学校有合作,优等生升学进这所高中可以免三年学费,但名额只有20个。


我不知道别人是否和我一样,也对那个名额感兴趣,但我能感受到,每次考试大家都会铆足了劲。


在那种环境之下,我的成绩也被逼着继续往前走,在后来的两年里,我的每次模拟考成绩基本都排在全校前60,但一直与那个名额无缘。


临近中考时,那所高校来人了,和20个优等生签订了协议,看着他们一个个拿着协议书从学校会议室走出来,那一刻,我竟然又有了“羡慕别人拿奖状”的感觉。


不过后来发生了一件趣事,让我从这次竞争失利中走了出来。


我爸平时忙着务工,早出晚归,基本没关心过我的学习成绩。


后来他听朋友聊起中考的事情,并听闻我们村附近某所初中学校在中考时,录取分数线可以比其他学校低(印象中和扶贫政策有关),比较容易升高中。


所以后面我爸就跟着朋友忙起了帮孩子转校的事情,最后在没和我商量的情况下,跑到我学校的校长面前,沟通起给我转校的事情。


我爸和我说这事时,我被逗笑了,他说校长被我爸气坏了,校长的原话是这样的:“我辛辛苦苦栽培3年的花朵,你现在就要摘走?”。


后来,校长在了解清楚情况并且知道我家的条件后,说服了我爸不给我转校。另外校长答应我爸,如果我的中考成绩能继续保持在学校前60,可以破格帮我申请免三年学费进到前面提到的那所高中。


中考很快结束,我的成绩很稳,还是和平时一样,保持在全校前60,但中考结束后的那段时间,出现了让我非常纠结的事情。


校长很守信,中考成绩出来后,他帮我申请到了入那所高中的名额,但因为是破格,只能免第1年学费,而第二和第三年学费需要由学校考核决定是否继续减免。


让我纠结的有3点,一是这个“破格录取”,在当时我感觉像是走后门,心理很不舒服。二是我的中考的分数可以进到县里一所不错的重点高中,而且听闻比这所破格进的学校要好。三是家里的经济条件。


最后,在我爸的支持之下,我选择了去自己考上的重点高中学校。


叛逆的高中


2007年,我顺利考进了县城里的重点高中,而且还是重点班,选学校那件事带来的郁闷感很快一扫而尽。


高中开始,我们从偏远农村来的就需要寄宿在学校了,原本被家人寄予厚望的我,却在高一时期发生了突变,是的,突变。


高一时,我开始接触到了网吧,从小最多只看过别人在电视机上玩小霸王游戏,而且在当时还没有QQ号的我,沦陷了。


高中那会,大部分人都有QQ,而我,只在初中毕业填写留言纪念簿时才知道有这东西,没办法,为了社交只好跟着同学去网吧学习怎么玩QQ。


一脚踏入网吧的我,立马被网络世界的各种缤纷色彩给吸住了。


到什么程度呢?


经常找同网吧坐我旁边机位的同学聊QQ,自学五笔还带出了几个徒弟,自习时还得意的带着他们唱"工戈草头右框七",还经常帮女同学整QQ空间,至于男同胞,则天天帮他们刷各种Q钻会员。


一直到后面开始接触网络游戏,就真的完全陷进去了,疯狂到原本为数不多的生活费都可以省出一大半用来上网。


高一的疯狂放纵,带来的结局是,在升高二时我被班主任调到了次重点班。


这场景是如此地熟悉,但体验感却是天差地别。


这对我打击很大,网瘾不降反增,上课睡觉,晚上通宵成了我的常态。


有一次周末我通完宵,白天在宿舍睡觉时被人叫醒,当时映入我眼帘的是我爸失望的表情,以及在一旁小声啜泣的母亲。


我荒废学业的事,被班主任通知到家中了。


这件事后,我才开始变得有所收敛,不过学业落下太多,后面基本无心学习。


就这样混混噩噩度过了三年高中,一直到教室黑板上的高考倒计时剩余30天时我才幡然醒悟。


不过为时已晚,最终,高考成绩公布后,我只考了498分,只到大专院校的录取分数线。


高考结果出来后,我开玩笑地对我爸说:"爸,您看我只花30天的时间,成绩就追上了专科的分数线,您让我复读吧,二本应该轻轻松松,一本还可以挑战挑战"。


我爸只回了句"滚",就没后文了。


浪子回头的我,在知道复读无望后,决定在大专的世界打下一片天下。


无知的大学


填报志愿时,我选择了软件技术专业。


也许是我爸感受到了我痛改前非的觉悟,也许是害怕我继续堕落。在知道我选择了计算机相关专业后,就忙着打听到这个专业的内容。


后来他了解到这个专业需要用到电脑,在一天中午,顶着大太阳,他把我拉上摩托车,先是带我去了他做碾米工作的朋友家借了1000块,然后再带着我去了他一个在做图文印刷店的朋友家,花了重金600块买下了对方的二手笔记本。


后来我很庆幸这台笔记本很垃圾,在别人玩游戏的时候我只能选择敲代码。


高考结束后的假期我没有和往常一样选择去打暑假工,而是选择呆在了家里。


通过上网了解到所报专业的相关课程内容后,还未开学,我就用笔记本下载了马士兵和韩顺平老师的教程视频,自学完了Java基础课程,还写了个坦克游戏天天在我弟面前显摆。


2010年,我开启了三年既装逼又无知的大专生涯。


由于开学前就把专业基础打好,在大一,当别人还在学基础课程时,我已经开始学做项目,偶尔也能和学校的师兄合作帮学校做做项目。


在平时,还会和去了重本大学的高中同学一起比赛刷杭电ACM,后来,还因此拿了个蓝桥杯省赛一等奖,不过正当我摩拳擦掌准备去北京参加全国决赛时,却被学校告知学校经费不足去不了。好吧,穷孩子的环境就是这么恶劣。


因为能力得到认可,和老师相处也融洽,一些专业课的老师们私下给我开了特权,允许我不用去教室上课,想忙啥就忙去。


所以我很听老师们的话,大二开始我开始忙着泡妞去了,并且把班里暗恋的女同学拐到手,嗯,她现在成为了我的妻子。


大三时期,某家培训机构来到了我们学校做宣传,我听完那些各式各样学员进名厂的案例,我心动了。以先学后付的方式进了培训机构。


大三时期的我一下进到了初中时期拼入选名额的状态,朝九晚十,日复一日的在广州某培训校区学习,一直幻想着凭自己的实力加上培训机构的联合企业资源,一举进到名厂。


但,现实却给了我一个大嘴巴子。


逗逗青的职场路


面试碰壁


2013年,大专即将毕业,培训机构的课程也已学完,我开始尝试自己出去找工作,原本以为就业会很顺利,但找工作却是磕磕碰碰。


小公司倒还好,基本有叫面试的都能拿到offer,但一到中大厂,就很难走到终面,而且大部分情况是简历直接被拒。


直到很多年后我才明白其根本原因:中大厂对于应届生的要求,是要挑选好苗子,学历对企业而言是一项减少选苗子出现差错的必要筛选项,而专业技能方面更看重的是通用基础技能,如操作系统、算法、常用框架原理等。


而在当时的我,是个偏实战的低学历选手,知识面学得很广但是不深,而且还有大专学历这个减分项,求职困难可想而知。


求职碰壁后,为应付学校的毕业实习要求,以及缓解经济上的压力,我选择了在一家做传统软件开发的中小企业过渡了一年时间。


因为不甘心,那段时间我一直在思考问题根因,后面大概摸到了一些门道,在还清学校助学贷款和培训机构的费用后,我决定跳槽。


跳槽再战


在当时,我隐约意识到求职的关键还是要对口,需要知道用人单位的需求。而我之前却有点炫技,简历上乱七八糟会的技术和项目全写上了。


决定跳槽后,我对市场上的招聘需求进行了归类,结合自己的兴趣和能力,我选择了主攻游戏行业(网瘾少年的后遗症~),并自学填补了游戏行业所需的关键技术,如网络、并发等。


终于在后面的面试中越挫越勇,顺利找到了一份上市公司的游戏服务端研发工作。


现在回过头想想,有点庆幸,还好那个时期的市场机会比较多,有很多机会去尝试,虽然求职路走了挺多弯路,但最终结果还是比较好的。


后来,在游戏研发岗位我做了2年多时间,主要做页游和手游。


在游戏行业里,企业内部一般是按工作室划分不同团队,比如“天美工作室”。


在游戏这个行业,如果想要赚得多,你得进到好团队。什么是好团队?能赚钱的团队就是好团队。


2年多时间的游戏工作经历,让我深刻理解了什么叫“选择比努力更重要”。


我庆幸自己进到了一个不错的游戏团队,虽然工作强度比较大,但由于项目运营收益高,经常可以参与奖金分红。


但同企业内的有些团队就另当别论了,强度比我们团队要来得更猛,但一年到头基本没什么奖金,而且还需要时常担忧工作室可能会解散的问题。


在游戏行业,除《王者荣耀》、《原神》等这种现象级产品,普遍项目周期比较短,一般两年左右就会进入衰退期,当项目不再产生正向收益且公司不再投入资金研发新项目时,团队就会面临解散的危险。


我所在团队的项目就属于后劲不足的产品。两年左右,团队业绩就开始出现下滑,最终团队还是解散了。


在游戏公司,团队解散时,一般会有其他团队过来挖人。当时我被其他团队挖了过去,而团队的其他大部分成员基本都办理了离职,包括我的leader,也就是团队里的主程。


跳槽转型


后来,我的leader在离职不到半年后联系上了我,并把我挖到他所在的一家互联网公司,也是从那一刻起,我开始转型做平台架构相关的研发工作。


当时这家公司比较吸引我的是:团队强、营收高。我当时是9月份过去,工作了4个月就到了发年终奖时间,当时我拿到了4个多月奖金。


这家公司钱给得大方,但工作强度贼猛,公司里加班文化比较重,工作节奏也非常快。


当时我所在的团队是公共部门,也就是所谓的中台。负责支持公司所有业务产品的基础能力,如订单、支付、履约等。


项目研发一周一个迭代,因为是公共部门,除了节奏要快,上线还要求要稳。一开始我很难适应,主要原因还是我的能力跟不上。


在那段时间,也是我买书最疯狂的时期。基本和工作相关的技术书籍我都买来啃了,加上经常能和其他大厂里的朋友交流技术,能力才开始慢慢追上。


成家,寻求变化


2017年,经过4年的积累,我和女友有了一些积蓄,爱情长跑也6年了,所以我们选择在广州安了个小家,把爱情的果结了。


2018年,我们的第1个宝宝出生,也是这一年,我选择了离职。


这5年里,我的工作强度一直比较大,经常处于on-call的状态,另外在业余时间我也比较卷,熬夜是我的常态。


宝宝的出生,加上近两次的体验报告出现了比较多预警信息,让我开始思考工作与生活的平衡。


所以我跳槽去了一家工作强度相对较好的企业,而且也选择了继续从事自己擅长的平台架构工作。


入职新公司后,由于懂得了一点分析上级期望值的技巧,在日常工作中常能做出符合领导心意的成绩,后来上级领导选择辞职创业后,我也顺势当上了团队leader,顺带拿到了一些期权。


所以现在,我平时除了写写代码,还需要规划团队的发展方向以及带带新人,平时也会作为面试官,参与公司的招聘,在这期间积累到了一些新的经验和感触,未来我会对这些经验做些分享,这里不作展开。


目前这家企业,我呆了有一段时间,近期感觉在发展上遇到了一些瓶颈,所以接下的路,要如何走,我还在持续思考与探索。


写在最后


近期,我计划在公众号上开始写作,所以也才有了这一篇文章。


工作了十年,我想找个地方将积累的内容沉淀下来,一是利他,二是自身也能受到一些益处,比如将来出本书,或者转行做教育工作者,都是不错的选择。


所以在接下来的三年,我计划通过写作的方式,持续分享一些高质量的技术干货,来链接一些技术同行,特别是一些职场新人。


我期望通过我的分享,能够帮到一些人少走些弯路,在职场的发展上能得到一些提速。


最后,关于“逗逗青”这个网名,是源于我的女儿。她近期在看一部名为《土豆逗严肃科普》的动画片,后来经常会给我讲土豆逗的故事,不听还不行~


所以我的网名就诞生了,虽有些稚气,但未来当我感到疲惫时,也许这个网名可以给我带来一些正能量。


作者:逗逗青
来源:juejin.cn/post/7317535572432584738
收起阅读 »

Android 0,1,2 个启动图标

最近改了个隐式app 拉起,启动图标也有不同的玩法 0 个启动图标 <intent-filter> <action android:name="android.intent.action.MAIN" /> <category an...
继续阅读 »

最近改了个隐式app 拉起,启动图标也有不同的玩法


0 个启动图标


<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />

<category android:name="android.intent.category.BROWSABLE" />
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data
android:host="app"
android:path="/"
android:port="8080"
android:scheme="lb">
</data>
</intent-filter>

这里是对接受所有隐式拉起,这个是告诉系统app 启动不需要用户手动拉起,是为了被代码或者其他中转站调用,所以不需要用户手动拉起,自然就不用再显示图标


1个启动图标


<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>

LAUNCHER 是决定是不是要显示在程序列表里,默认为主动唤起,也是Android 标准启动模式,会正常在手机的界面显示


2 个启动图标


<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme">

<activity android:name="com.camera.demo.1Activity"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

<activity android:name="com.camera.demo.2Activity"
android:icon="@mipmap/ic_launcher" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

</application>

上面说了android.intent.category.LAUNCHER 为是否显示在应用列表内,所以我们配置多个LAUNCHER,就会有多个图标显示在手机列表内


intent-filter 相关说明


接受页面隐式跳转的过滤器,


action


必须的包含,定义一些操作.android.intent.action.MAIN/android.intent.action.WEB_SEARCH


image.png


category


一个字符串, 包含了处理该Intent的组件的种类信息, 起着对action的补充说明作用


image.png


data


要指定接受的 Intent 数据,Intent 过滤器既可以不声明任何 <data> 元素,也可以声明多个此类元素,如下例所示:


<intent-filter>
    <data android:mimeType="video/mpeg" android:scheme="http" ... />
    <data android:mimeType="audio/mpeg" android:scheme="http" ... />
    ...
</intent-filter>

每个 <data> 元素均可指定 URI 结构和数据类型(MIME 媒体类型)。URI 的每个部分都是一个单独的属性:schemehostport 和 path


<scheme>://<host>:<port>/<path>


下例所示为这些属性的可能值:


content://com.example.project:200/folder/subfolder/etc


在此 URI 中,架构是 content,主机是 com.example.project,端口是 200,路径是 folder/subfolder/etc
在 <data> 元素中,上述每个属性均为可选,但存在线性依赖关系:



  • 如果未指定架构,则会忽略主机。

  • 如果未指定主机,则会忽略端口。

  • 如果未指定架构和主机,则会忽略路径。


将 Intent 中的 URI 与过滤器中的 URI 规范进行比较时,它仅与过滤器中包含的部分 URI 进行比较。例如:



  • 如果过滤器仅指定架构,则具有该架构的所有 URI 均与该过滤器匹配。

  • 如果过滤器指定架构和权限,但未指定路径,则具有相同架构和权限的所有 URI 都会通过过滤器,无论其路径如何均是如此。

  • 如果过滤器指定架构、权限和路径,则仅具有相同架构、权限和路径的 URI 才会通过过滤器。


最后贴张LAUNCHER 的原理图


image.png


作者:libokaifa
来源:juejin.cn/post/7307471540715126795
收起阅读 »

18张图,详解SpringBoot解析yml全流程

前几天的时候,项目里有一个需求,需要一个开关控制代码中是否执行一段逻辑,于是理所当然的在yml文件中配置了一个属性作为开关,再配合nacos就可以随时改变这个值达到我们的目的,yml文件中是这样写的: switch: turnOn: on 程序中的代码也...
继续阅读 »



前几天的时候,项目里有一个需求,需要一个开关控制代码中是否执行一段逻辑,于是理所当然的在yml文件中配置了一个属性作为开关,再配合nacos就可以随时改变这个值达到我们的目的,yml文件中是这样写的:


switch:
turnOn: on

程序中的代码也很简单,大致的逻辑就是下面这样,如果取到的开关字段是on的话,那么就执行if判断中的代码,否则就不执行:


@Value("${switch.turnOn}")
private String on;

@GetMapping("testn")
public void test(){
if ("on".equals(on)){
//TODO
}
}

但是当代码实际跑起来,有意思的地方来了,我们发现判断中的代码一直不会被执行,直到debug一下,才发现这里的取到的值居然不是on而是true



看到这,是不是感觉有点意思,首先盲猜是在解析yml的过程中把on作为一个特殊的值进行了处理,于是我干脆再多测试了几个例子,把yml中的属性扩展到下面这些:


switch:
turnOn: on
turnOff: off
turnOn2: 'on'
turnOff2: 'off'

再执行一下代码,看一下映射后的值:



可以看到,yml中没有带引号的onoff被转换成了truefalse,带引号的则保持了原来的值不发生改变。


到这里,让我忍不住有点好奇,为什么会发生这种现象呢?于是强忍着困意翻了翻源码,硬磕了一下SpringBoot加载yml配置文件的过程,终于让我看出了点门道,下面我们一点一点细说!


因为配置文件的加载会涉及到一些SpringBoot启动的相关知识,所以如果对SpringBoot启动不是很熟悉的同学,可以先提前先看一下Hydra在古早时期写过一篇Spring Boot零配置启动原理预热一下。下面的介绍中,只会摘出一些对加载和解析配置文件比较重要的步骤进行分析,对其他无关部分进行了省略。


加载监听器


当我们启动一个SpringBoot程序,在执行SpringApplication.run()的时候,首先在初始化SpringApplication的过程中,加载了11个实现了ApplicationListener接口的拦截器。



这11个自动加载的ApplicationListener,是在spring.factories中定义并通过SPI扩展被加载的:



这里列出的10个是在spring-boot中加载的,还有剩余的1个是在spring-boot-autoconfigure中加载的。其中最关键的就是ConfigFileApplicationListener,它和后面要讲到的配置文件的加载相关。


执行run方法


在实例化完成SpringApplication后,会接着往下执行它的run方法。



可以看到,这里通过getRunListeners方法获取的SpringApplicationRunListeners中,EventPublishingRunListener绑定了我们前面加载的11个监听器。但是在执行starting方法时,根据类型进行了过滤,最终实际只执行了4个监听器的onApplicationEvent方法,并没有我们希望看到的ConfigFileApplicationListener,让我们接着往下看。



run方法执行到prepareEnvironment时,会创建一个ApplicationEnvironmentPreparedEvent类型的事件,并广播出去。这时所有的监听器中,有7个会监听到这个事件,之后会分别调用它们的onApplicationEvent方法,其中就有了我们心心念念的ConfigFileApplicationListener,接下来让我们看看它的onApplicationEvent方法中做了什么。



在方法的调用过程中,会加载系统自己的4个后置处理器以及ConfigFileApplicationListener自身,一共5个后置处理器,并执行他们的postProcessEnvironment方法,其他4个对我们不重要可以略过,最终比较关键的步骤是创建Loader实例并调用它的load方法。


加载配置文件


这里的LoaderConfigFileApplicationListener的一个内部类,看一下Loader对象实例化的过程:



在实例化Loader对象的过程中,再次通过SPI扩展的方式加载了两个属性文件加载器,其中的YamlPropertySourceLoader就和后面的yml文件的加载、解析密切关联,而另一个PropertiesPropertySourceLoader则负责properties文件的加载。创建完Loader实例后,接下来会调用它的load方法。



load方法中,会通过嵌套循环方式遍历默认配置文件存放路径,再加上默认的配置文件名称、以及不同配置文件加载器对应解析的后缀名,最终找到我们的yml配置文件。接下来,开始执行loadForFileExtension方法。



loadForFileExtension方法中,首先将classpath:/application.yml加载为Resource文件,接下来准备正式开始,调用了之前创建好的YamlPropertySourceLoader对象的load方法。


封装Node


load方法中,开始准备进行配置文件的解析与数据封装:



load方法中调用了OriginTrackedYmlLoader对象的load方法,从字面意思上我们也可以理解,它的用途是原始追踪yml的加载器。中间一连串的方法调用可以忽略,直接看最后也是最重要的是一步,调用OriginTrackingConstructor对象的getData接口,来解析yml并封装成对象。



在解析yml的过程中实际使用了Composer构建器来生成节点,在它的getNode方法中,通过解析器事件来创建节点。通常来说,它会将yml中的一组数据封装成一个MappingNode节点,它的内部实际上是一个NodeTuple组成的ListNodeTupleMap的结构类似,由一对对应的keyNodevalueNode构成,结构如下:



好了,让我们再回到上面的那张方法调用流程图,它是根据文章开头的yml文件中实际内容内容绘制的,如果内容不同调用流程会发生改变,大家只需要明白这个原理,下面我们具体分析。


首先,创建一个MappingNode节点,并将switch封装成keyNode,然后再创建一个MappingNode,作为外层MappingNodevalueNode,同时存储它下面的4组属性,这也是为什么上面会出现4次循环的原因。如果有点困惑也没关系,看一下下面的这张图,就能一目了然了解它的结构。



在上图中,又引入了一种新的ScalarNode节点,它的用途也比较简单,简单String类型的字符串用它来封装成节点就可以了。到这里,yml中的数据被解析完成并完成了初步的封装,可能眼尖的小伙伴要问了,上面这张图中为什么在ScalarNode中,除了value还有一个tag属性,这个属性是干什么的呢?


在介绍它的作用前,先说一下它是怎么被确定的。这一块的逻辑比较复杂,大家可以翻一下ScannerImplfetchMoreTokens方法的源码,这个方法会根据yml中每一个keyvalue是以什么开头,来决定以什么方式进行解析,其中就包括了{['%?等特殊符号的情况。以解析不带任何特殊字符的字符串为例,简要的流程如下,省略了一些不重要部分:



在这张图的中间步骤中,创建了两个比较重要的对象ScalarTokenScalarEvent,其中都有一个为trueplain属性,可以理解为这个属性是否需要解释,是后面获取Resolver的关键属性之一。


上图中的yamlImplicitResolvers其实是一个提前缓存好的HashMap,已经提前存储好了一些Char类型字符与ResolverTuple的对应关系:



当解析到属性on时,取出首字母o对应的ResolverTuple,其中的tag就是tag:yaml.org.2002:bool。当然了,这里也不是简单的取出就完事了,后续还会对属性进行正则表达式的匹配,看与regexp中的值是否能对的上,检查无误时才会返回这个tag


到这里,我们就解释清楚了ScalarNodetag属性究竟是怎么获取到的了,之后方法调用层层返回,返回到OriginTrackingConstructor父类BaseConstructorgetData方法中。接下来,继续执行constructDocument方法,完成对yml文档的解析。


调用构造器


constructDocument中,有两步比较重要,第一步是推断当前节点应该使用哪种类型的构造器,第二步是使用获得的构造器来重新对Node节点中的value进行赋值,简易流程如下,省去了循环遍历的部分:



推断构造器种类的过程也很简单,在父类BaseConstructor中,缓存了一个HashMap,存放了节点的tag类型到对应构造器的映射关系。在getConstructor方法中,就使用之前节点中存入的tag属性来获得具体要使用的构造器:



tagbool类型时,会找到SafeConstruct中的内部类 ConstructYamlBool作为构造器,并调用它的construct方法实例化一个对象,来作为ScalarNode节点的value的值:



construct方法中,取到的val就是之前的on,至于下面的这个BOOL_VALUES,也是提前初始化好的一个HashMap,里面提前存放了一些对应的映射关系,key是下面列出的这些关键字,value则是Boolean类型的truefalse



到这里,yml中的属性解析流程就基本完成了,我们也明白了为什么yml中的on会被转化为true的原理了。至于最后,Boolean类型的truefalse是如何被转化为的字符串,就是@Value注解去实现的了。


思考


那么,下一个问题来了,既然yml文件解析中会做这样的特殊处理,那么如果换成properties配置文件怎么样呢?


sw.turnOn=on
sw.turnOff=off

执行一下程序,看一下结果:



可以看到,使用properties配置文件能够正常读取结果,看来是在解析的过程中没有做特殊处理,至于解析的过程,有兴趣的小伙伴可以自己去阅读一下源码。


那么,今天就写到这里,我们下期见。


作者:码农参上
来源:juejin.cn/post/7054818269621911559
收起阅读 »

Java 中for循环和foreach循环哪个更快?

本文旨在探究Java中的for循环和foreach循环的性能差异,并帮助读者更好地选择适合自身需求的循环方式 前言 在Java编程中,循环结构是程序员常用的控制流程,而for循环和foreach循环是其中比较常见的两种形式。关于它们哪一个更快的讨论一直存在。...
继续阅读 »

本文旨在探究Java中的for循环和foreach循环的性能差异,并帮助读者更好地选择适合自身需求的循环方式



前言


在Java编程中,循环结构是程序员常用的控制流程,而for循环和foreach循环是其中比较常见的两种形式。关于它们哪一个更快的讨论一直存在。本文旨在探究Java中的for循环和foreach循环的性能差异,并帮助读者更好地选择适合自身需求的循环方式。通过详细比较它们的遍历效率、数据结构适用性和编译器优化等因素,我们将为大家揭示它们的差异和适用场景,以便您能够做出更明智的编程决策。



for循环与foreach循环的比较


小编认为for和foreach 之间唯一的实际区别是,对于可索引对象,我们无权访问索引。


for(int i = 0; i < mylist.length; i++) {
if(i < 5) {
//do something
} else {
//do other stuff
}
}

但是,我们可以使用 foreach 创建一个单独的索引 int 变量。例如:


int index = -1;
for(int myint : mylist) {
index++;
if(index < 5) {
//do something
} else {
//do other stuff
}
}

现在写一个简单的类,其中有 foreachTest() 方法,该方法使用 forEach 迭代列表。


import java.util.List;

public class ForEachTest {
List intList;

public void foreachTest(){
for(Integer i : intList){

}
}
}

编译这个类时,编译器会在内部将这段代码转换为迭代器实现。小编通过执行 javap -verbose IterateListTest 反编译代码。


public void foreachTest();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=3, args_size=1
0: aload_0
1: getfield #19 // Field intList:Ljava/util/List;
4: invokeinterface #21, 1 // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
9: astore_2
10: goto 23
13: aload_2
14: invokeinterface #27, 1 // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
19: checkcast #33 // class java/lang/Integer
22: astore_1
23: aload_2
24: invokeinterface #35, 1 // InterfaceMethod java/util/Iterator.hasNext:()Z
29: ifne 13
32: return
LineNumberTable:
line 9: 0
line 12: 32
LocalVariableTable:
Start Length Slot Name Signature
0 33 0 this Lcom/greekykhs/springboot/ForEachTest;
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 13
locals = [ class com/greekykhs/springboot/ForEachTest, top, class java/util/Iterator ]
stack = []
frame_type = 9 /* same */

从上面的字节码我们可以看到:


a). getfield命令用于获取变量整数。


b).调用List.iterator获取迭代器实例


c).调用iterator.hasNext,如果返回true,则调用iterator.next方法。


下边来做一下性能测试。在 IterateListTest 的主要方法中,创建了一个列表并使用 for 和 forEach 循环对其进行迭代。


import java.util.ArrayList;
import java.util.List;

public class IterateListTest {
public static void main(String[] args) {
List mylist = new ArrayList<>();
for (int i = 0; i < 1000000; i++) {
mylist.add(i);
}

long forLoopStartTime = System.currentTimeMillis();
for (int i = 0; i < mylist.size(); i++) {mylist.get(i);}

long forLoopTraversalCost =System.currentTimeMillis()-forLoopStartTime;
System.out.println("for loop traversal cost for ArrayList= "+ forLoopTraversalCost);

long forEachStartTime = System.currentTimeMillis();
for (Integer integer : mylist) {}

long forEachTraversalCost =System.currentTimeMillis()-forEachStartTime;
System.out.println("foreach traversal cost for ArrayList= "+ forEachTraversalCost);
}
}

结果如下:


总结


观察结果显示,for循环的性能优于for-each循环。然后再使用LinkedList比较它们的性能差异。对于 LinkedList 来说,for-each循环展现出更好的性能。ArrayList内部使用连续存储的数组,因此数据的检索时间复杂度为 O(1),通过索引可以直接访问数据。而 LinkedList 使用双向链表结构,当我们使用 for 循环进行遍历时,每次都需要从链表头节点开始,导致时间复杂度达到了 O(n*n),因此在这种情况下,for-each 循环更适合操作 LinkedList。


作者:葡萄城技术团队
来源:juejin.cn/post/7280050832950624314
收起阅读 »

拿开源套壳就是自主研发?事情没那么简单

去年8月国内科技圈出了一件非常丢人的事情,想必大家已经都知道了,某个号称自主研发的IDE完全使用开源的VSCode改名而来。没错,就是改名! 哦,对了,还加上了vip功能! 为什么这家公司可以如此堂而皇之地将VSCode改成自主研发?在该公司最新的道歉声明...
继续阅读 »

去年8月国内科技圈出了一件非常丢人的事情,想必大家已经都知道了,某个号称自主研发的IDE完全使用开源的VSCode改名而来。没错,就是改名!


图片


哦,对了,还加上了vip功能!


图片


为什么这家公司可以如此堂而皇之地将VSCode改成自主研发?在该公司最新的道歉声明中提到了缺失MIT协议:


图片


MIT协议到底是什么,里面有哪些要求,微软可以告他侵权吗?


在文章的最后我要聊一下软件的开源的意义在哪里,到底怎样才能真的叫“自主研发”。


1、MIT协议是什么


以GitHub为例,打开源代码的根目录,一般会有一个名为license的文件,这个license就是这套代码的许可证信息。


图片


可能这个license的文件内容很多,而且是英文的,不过不必担心,我给大家归纳总结一下就明白了。


直接看这张由阮一峰大佬总结的图,可以看到开源许可证主要限制的点就一目了然:


图片


正因为VSCode采用的是最宽松的MIT协议,它的MIT协议几乎没有什么约束。


整个协议非常的简短,不到两百个单词。我们直接看看VSCode的MIT协议全文长啥样:


图片


首先第一句,这个许可是免费的,任何人都可以拿到软件的副本以及附带的文档。


然后还能做啥呢?使用,复制,修改,合并,发布,分发,再许可/或出售该软件的副本。


也就是说你爱咋整都可以,拿来卖钱也可以,但只有一个要求,就是要把这个许可证放到软件的副本中!


所以大家看懂了上面这家公司的道歉声明了吗?他们道歉的点就是没有把MIT许可证放入其中,这也是MIT许可证唯一的要求。


后面据说他们也在GitHub上开源了CEC-IDE:


图片


不过被骂得太惨,最后还是消失了。


既然是套壳VSCode,微软能告他侵权吗?


答案是不太行。因为MIT许可证本身就是一个不起诉的承诺。


2、做CEC-IDE意义何在


为什么他们要做这个CEC-IDE呢?真的指望它vip能赚钱吗?


作为同样是程序员的我,其实对CEC-IDE的做法并不陌生。


例如我们公司也搞了一个开发平台,为了不惹麻烦了,我就不说是啥了,我就简单叫做by吧。


它其实就把springboot gitlab等等一些东西糅合在一起,然后把包名,比如spring替换成by:


图片


其实我觉得嘛,这玩意要是对内使用,作为公司统一开发的规范,除了包名被换了比较恶心外,问题不算大,反而这样还可以统一管理开发组建的版本。


而CEC-IDE最大的问题就是把这种本来应该内部使用的东西公开化,而且大肆炒作“自主研发”。


一般大企业内部都有研发立项资金,每年都有一定的申报额度,各个部门都会绞尽脑汁去做各种工作。


当然并不是做个ppt就完事了,上面人也不傻,现在一般大企业内部都很卷,为了拿到上面批下来的研发经费,无论如何都要造出点与众不同的地方。


毕竟kpi考核内部竞争也很激烈,所以大多数情况都会提前做一个“好看”的版本,配合一定的亮点宣传,“自主研发”显然是最契合的。


而MIT许可证规避了法律风险,确实是个“完美”的方案。


其实他们也是“聪明”的,只挑MIT许可证的,从他们的道歉声明可以看出,他们最初拿VSCode动手也是做了一定的功课的,错就错在太高调了!


3、软件开源的意义


为什么很多人和公司会选择把自己开发的软件开源?


开源不可避免会导致代码被其他人“拿来主义”,那么开源软件的意义在哪里?是因为他们太有钱做慈善吗?


首先要说明白一点,开源不代表与商业化冲突,反而优秀的开源软件能带来更多的商业化机会。


我举一个例子,假如我发明了一个人脸识别算法,这个算法有一个特别优势:可以在性能非常非常差的硬件上运行。但前提有一个条件,就是需要对指定硬件做适配,于是我把优化好的一个版本放在GitHub上,获得了很多人的关注,甚至也有很多人帮我改进代码中的一些bug。


有一天,一家大公司看中了我的代码,这时候会有两种情况:


第一种就是把我的代码“拿走”,用到自己的产品中,不给我一分钱!


第二种是把我“收编”了,或者给我一笔费用,让我为其提供有偿的技术支持,并能持续迭代适配这家公司的更多低端设备。


稍微有点远见的公司老板,肯定会选第二种。毕竟拿一段无人维护的陌生代码是有很大的风险的。有时候代价比自己做一套还要大。


对于企业来说,开源也不是做慈善,反而有战略作用。


例如代码开源,但你要获取的技术支持是付费的,这也是非常常见的盈利模式。


还有一个典型就是比如开放云服务形式,这也是AI领域常见的开源盈利模式。


在我看来,大家遵守游戏规则,尊重他人的劳动成果,软件开源肯定是有利于整个行业发展的。


4、怎么定义自主研发


自主研发严格定义应该是:企业主要依靠自己的资源,技术,人力,依据自己的意志,独立开发,并在研发项目的主要方面拥有完全独立的知识产权。


除此之外,自主研发还包含一层意思,自己做主,行使权利,而不受他人的控制或限制。


什么叫突破西方卡脖子?


去“突破”人家免费送的东西算哪门子自主!


我承认,做自主研发不可能完全从零开始,在别人的源代码基础上做衍生开发是再正常不过的事情。但起码要让人看到做这件事情的价值。


如此浮躁,急功近利,毫无底线,这件事无疑给国产化、信创行业更加蒙上一层阴影。


作者:程序员Winn
来源:juejin.cn/post/7319181255757185061
收起阅读 »

2023我的仲裁之旅, 谨以此献给需要仲裁的朋友

背景经历在我提出离职之前, 公司断断续续欠薪一年多的时间, 欠薪时长4.5个月(时间这里还搞了个小乌龙, 后面说下)。2023年5月底,我向领导提出辞职,离职时间确定为2023.6.6日。虽然公司欠薪, 但当时的我依旧心存幻想, 想着可以跟公司好聚好散, 毕竟...
继续阅读 »

背景经历

在我提出离职之前, 公司断断续续欠薪一年多的时间, 欠薪时长4.5个月(时间这里还搞了个小乌龙, 后面说下)。

2023年5月底,我向领导提出辞职,离职时间确定为2023.6.6日。

虽然公司欠薪, 但当时的我依旧心存幻想, 想着可以跟公司好聚好散, 毕竟在公司待三年多了&同事关系和谐&如果仲裁的话,了解到按我的情况会有n的补偿金( 以为公司不会冒仲裁风险(公司处于融资阶段),毕竟仲裁的话我这边赢下的概率比较大),期间我积极进行离职交接,又自以为是的以为公司会在我最后走的时候跟我约定好一个薪资发放的时间把欠我的工资补全(为啥说要约定好呢? 因为公司欠薪,薪资发放已经不是正常时间了,还有, 各位,记得不要自以为是!)。

时间转眼到了6.6号,期间人事在这之前一直没找我聊,我也没有找人事聊(人事是知道我提离职的, 上面说到我自以为公司这边会跟我约定好薪资发放时间, 这里为啥会有这种意识呢? 因为之前离职的同事工资都拿到了),这最后一天,我去找人事,人事那边拿出相关的材料让我填写离职申请, 我这边挨个找人签字,但是公司老总正好找各个部门领导开会,我这边还卡着最后一位签字,一时半会签不上, 然后我去找人事了。

我问:我这边工资什么时候能给我?

人事回复:我这边也不确定,需要请示下领导。

我问:领导那边回复要多久?

人事回复: 不确定, 领导现在还在开会,等开会完我确认下。

。。。

我回工位了。

等过了一会, 我看开会的人回来了,我又去找人事问。

我问: 能确定了吗?

人事回复:刚刚我去找领导了,领导不在,估计得明天了。(Fuck!)

然后我就去公司董事长办公室,想自己去聊,结果确实不在。

然后我对人事说:那明天你这边给我一个回复,离职材料我填好了,还差XXX签字,现在又去开会了, 找不到他。

下班时间到了,我走了。

因为没有敲定工资发放日期,有种预感公司准备使用拖字诀对付我了,所以我把一些仲裁材料整理了下,这个具体有什么材料后面再说一下。

第二天一早,我给人事和公司老板发消息,如下:

人事下午给我的回复:

而老板呢? 没理我, 么得回复。人事也是我中间又给发了次消息才回复的我。

到这呢, 我就明白了, 公司确实准备拖我了,可能是资金紧张, 也可能是看人下菜,总之,去仲裁吧。

一个简单的时间线

6.6号离职走人。

6.7号与HR和老板交涉,并开始做仲裁准备。

6.8号准备好仲裁材料

6.9号去仲裁

6.26号调解员开始调解

期间给了一些调解方案, 与调解员, HR交涉

7.19号我与公司均不接受互相给的方案, 我这边坚持开庭, 调解员让我联系仲裁委去那边办理开庭手续, 提交相关仲裁资料, 等待开庭。

8.7号调解员联系我, 说公司因为仲裁案频发, 被上级领导要求整改, 然后公司接受我之前一个降级的调解方案, 赔偿金额可以, 但给我的支付时间我没法接受, 我这边坚持当庭支付。

8.8号调解员做出保证: 可以当庭支付, 且如果公司那边没给当庭支付,那就不支付不给我结案, 我依旧可以继续把流程走下去,不必重新排队。

8.10号接受调解并签字, 公司当庭支付,至此我的仲裁到此结束。

仲裁资料准备

这是我的申请诉求和相关证据材料

  1. 拖欠薪资
  • 劳动合同
  • 我的银行流水
  1. 加班费用
  • 五一期间的打卡记录
  • 加班聊天记录
  1. (2021年的)年终奖金(应于2022.5月份左右发, 但一直拖欠)
  • 与人事沟通的会发年终奖的聊天记录
  • 上一年的工资流水
  1. 经济补偿金
  • n
  1. 误工费

这个其实是我想多看能否多拿一点, 因为在劳动仲裁过程中, 我跑了好几趟,都是请假去的, 但是好像不支持, 最后线下提交的时候没填上它

  1. 其他
  • 国家企业信用信息公示系统下载的企业报告(这个可以下载, 但是没用到, 去仲裁委提交材料的时候需要从仲裁委的企业信用打印机里打印, 用不到这个 )
  • 仲裁申请书, 这个是在劳动仲裁的官方小程序劳动争议调解智慧服务平台上填写并下载的, 现在基本都是线上填写了, 但是提交到仲裁委的时候需要下载下来打印

仲裁仲裁!

6.9号去的仲裁委, 上面的仲裁资料是我最终版的, 去仲裁委之前我只是在网上查的资料, 到了之后让扫个小程序码, 线上提交申请

之后会有调解员联系你(这个时间我等了大概半个月, 6.26才加上调解员的微信), 之后调解员开始调解, 调解员给出公司的方案: 只有正常工资, 加班费和赔偿别想了。这个方案我这边当然不同意, 之后就是各种PK, 我, 公司人事, 调解员三方互相PK, 在两个月之后, 8.11号, 我还是调解了, 拿到了差强人意的money,算是庭前调解, 最终还是没走上开庭。期间我算了下, 两个多月的时间, 大概请了3,4天假。

关于对公司拖欠工资时应对的方案

我想了很多方案, 有应用的也有没用上的, 大家可以看一下或者补充一下:

  • 劳动仲裁: 保存好证据,要有打持久战的准备, 我最后实践有效的就是这个渠道。
  • 公积金中心举报公司未按实际工资给所有员工缴纳公积金: 网上说只要公司没按你的实际工资缴纳公积金, 你就可以去投诉, 尽管这可能是伤敌一千,自损八百的渠道, 但当时我依旧去了。我准备好银行流水和劳动合同, 结果去了之后工作人员告诉我要等仲裁结果, 仲裁结果赢了之后才可以受理。当时还是冒雨去的,老远了, 呵呵呵。
  • 劳动大队投诉: 这个渠道呢, 如果只是想要要回工资不需要补偿, 可以先走这个渠道, 记住这个, 因为你打电话投诉公司的时候, 工作人员会问: 是否已经在仲裁委那边提交申请? 如果回答提交了劳动仲裁申请,那这个机构就不会管了。 说辞: 已经被仲裁委受理, 等待仲裁就好了。综合: 这个机构其实在要回工资的时候可能比较给力, 可以先给它打电话投诉公司, 让它给公司施压, 然后再去仲裁。
  • 个人所得税投诉: 这个渠道就是比较伤财务, 如果财务话语权比较大的话, 没准就能追回工资,因为财务不想折腾,但我这个公司, 财务没啥话语权,所以财务按税务局的要求折腾了一两天, 然后把我的个人所得税没发工资的那几个月给更新成功0了,然后会在收到工资后再给新的报税。
  • 掌上12333 国务院客户端小程序 =>更多 => 投诉 => 全国根治欠薪线索反映平台: 本来我以为这个渠道会比较强势, 但非常可惜, 啥用没有, 可能我傻的只写了自己的欠薪, 没写其他同事的, 数额较小, 没引起注意, 等到我仲裁都结束了好像半个月还是一个月还是更久? 才给我来了个电话。

抖音上看到有个能快速结束仲裁, 然后去法院起诉进行一审的方法: 在申请书上写: 申请枪毙无良老板! 然后这个会被仲裁委驳回, 然后请求仲裁委出具不予受理通知, 拿着这个就可以去起诉公司。这个的话大家纯粹当爽文看吧, 应该不太具有实施成功的可能性。

一点点走心之谈

  • 如果公司刚开始拖欠一个月两个月, 别想了, 赶紧去劳动仲裁, 只要申请的诉求中每个单项金额不超过当地月平均工资标准的12个月的金额, 那就可以触发一裁终审, 公司无权再上诉,所以这个只适用于拖欠前期和金额较小时。
  • 如果公司拖欠工资, 而你决定去仲裁了,那就别犹犹豫豫的了, 赶紧给公司发一个被动解除劳动通知书, 走邮政和单位邮箱, 这样可以申请到n的劳动补偿, 而我就是因为没有这一步, 没有听取辣条(热心群友)的建议, 导致这条申请诉求被仲裁委支持的概率比较小, 所以选择了庭前调解。诸君引以为鉴。
  • 坚持理性,不要感性。
  • 说一下最开始说的关于欠薪时长的乌龙, 这个问题其实就是, 我, 搞错了公司拖欠我薪资的时长!怎么说呢? 就是公司发放薪资是在本月的15-20号左右发放上个月的薪资, 也就是说2023.6.6号离职, 我这边的工资应该6.20号发放的5月份工资和7月份发放的6.1-6.6的工资, 而我当时算的是按每月收入算的, 也就是5.20号收到一个月的工资, 6.20号我这边收到的应该是只有6天的薪资, 我不知道当时我为啥这样想...,最后是怎么发现的呢? 是在调解后期人事给的调解方案里发现我少算了一个月的薪资。
  • 关于拖欠薪资的日期。所谓百足之虫死而不僵, 有的公司拖欠薪资不是拖欠后就一直不给了, 而是断断续续的给, 拖欠一个月, 给你半个月, 拖欠一个月, 给你一个月, 继续拖, 继续给点, 我前公司就是这样, 断断续续给点, 前前后后欠一年多的时间了总共欠了4.5个月。现在想想, 我是怎么撑下来的?!

相关资源, 以了解仲裁

  • 我有买一本书, 《劳动争议仲裁诉讼实战宝典》,但这本书我还没看完我就结束仲裁了, 但, 它确实不错, 至少可以让对仲裁不太了解的人有个大概的了解。
  • 现在有很多的AI应用, 大家可以尝试一下,这个我也不太了解, 大家可以补充一下。
  • 抖音或b站看劳动仲裁相关的视频, 我有看的是叫晨辉律师

致谢辣条

非常感谢神奇的程序员@大白群里的不正经网友->辣条, 之前群里聊的时候在我说了公司拖欠工资后他就让我走被动离职这条路, 但当时他给我的印象是不太正经的精神小伙, 所以我没听他的, 就正常离职, 自己写了离职申请, 这是让我后来在仲裁期间非常后悔的事。 仲裁的周期比较长, 我也比较迷茫, 他给了我很多帮助, 期间我很多次都想着放弃吧, 接受吧,能拿回基本工资就不错了, 是辣条帮我坚定了“道心”,在此, 真的非常感谢辣条佬。

2023, 拜拜~

写下这篇文章, 给2023画一个完美的句号。

2023.12.31


作者:掘金沸点顶流
来源:juejin.cn/post/7318446251631493171

收起阅读 »

我的天!多个知名组件库都出现了类似的bug!

web
前言 首先声明,没有标题党哈! 以下我知道的国内知名react组件库全部都有这个bug,你们现在都能去复现,一个提pr的好机会就让给你们了,哈哈!复现组件库: 阿里系:ant design, fusion design, 字节系:arco design 腾讯...
继续阅读 »

前言


首先声明,没有标题党哈!


以下我知道的国内知名react组件库全部都有这个bug,你们现在都能去复现,一个提pr的好机会就让给你们了,哈哈!复现组件库:



本来字节还有一个semi design,结果我发现它没有Affix组件,也就是固钉组件,让他躲过一劫,他有这个组件我也觉得肯定会复现相同的bug。


Affix组件是什么,以及bug复现


Affix组件(固钉组件)能将页面元素钉在可视范围。如下图:


image.png


这个button组件,会在距离顶部80px的时候会固定在屏幕上(position: fixed),如下图:


image.png


如何复现bug


你在这个button元素任意父元素上,加上以下任意style属性



  • will-change: transform;

  • will-change: filter;

  • will-change: perspective;

  • transform 不为none

  • perspective不为none

  • 非safari浏览器,filter属性不为none

  • 非safari浏览器,backdrop-filter属性不为none

  • 等等


都可以让这个固定组件失效,就是原本是距离顶部80px固定。


我的组件库没有这个bug,哈哈


mx-design


目前组件不是很多,还在努力迭代中,不知道凭借没有这个bug的小小优点,能不能从你手里取一个star,哈哈


bug原因


affix组件无非都是用了fixed布局,我是如何发现这个bug的呢,我的组件库动画系统用的framer-motion,我本来是想在react-router切换路由的时候整点动画的,动画效果就是给body元素加入例如transform的变化。


然后我再看我的固钉组件怎么失效了。。。后来仔细一想,才发现想起来fixed布局的一个坑就是,大家都以为fixed布局相对的父元素是window窗口,其实是错误的!


真正的规则如下(以下说的包含块就是fixed布局的定位父元素):



  1. 如果 position 属性是 absolute 或 fixed,包含块也可能是由满足以下条件的最近父级元素的内边距区的边缘组成的:



    1. transform 或 perspective 的值不是 none

    2. will-change 的值是 transform 或 perspective

    3. filter 的值不是 none 或 will-change 的值是 filter(只在 Firefox 下生效)。

    4. contain 的值是 paint(例如:contain: paint;

    5. backdrop-filter 的值不是 none(例如:backdrop-filter: blur(10px);




评论区有很多同学居然觉的这不是bug?


其实这个问题本质是定位错误,在这些组件库里,同样使用到定位的有,例如Tooltip,Select,Popuver等等,明显这些组件跟写Affix组件的不是一个人,其他组件这个bug是没有的,只有Affix组件出现了,所以你说这是不是bug。


还有,如果因为引用了Affix组件,这个固定元素的任一父元素都不能用以上的css属性,我作为使用者,我用了动画库,动画库使用transfrom做Gpu加速,你说不让我用了,因为引起Affix组件bug,我心里想凭啥啊,明明加两行代码就解决了。


最后,只要做过定位组件的同学,其复杂度在前端算是比较高的了,这也是为什么有些组件库直接用第三方定位组件库(floating-ui,@popper-js),而不是自己去实现,因为自己实现很容易出bug,这也是例如以上组件库Tooltip为什么能适应很多边界case而不出bug。


所以你想想,这仅仅是定位组件遇到的一个很小的问题,你这个都解决不了,什么都怪css,你觉得用户会这么想吗,一有css,你所有跟定位相关的组件全部都不能用了,你们还讲理不?


总之一句话,你不能把定位组件的复杂度高怪用户没好好用,建议去看看floating-ui的源码,或者之前我写的@popper-js定位组件的简要逻辑梳理,你就会意识到定位组件不简单。边界case多如牛毛。


解决方案



  • 首先是找出要固定元素的定位元素(定位元素的判断逻辑上面写了),然后如果定位元素是window,那么跟目前所有组件库的逻辑一样,所以没有bug,如果不是window,就要求出相对定位父元素距离可视窗口顶部的top的值

  • 然后在我们原本要定位的值,比如距离顶部80px的时候固定,此时80px再减去上面说的定位父元素距离可视窗口顶部的top的值,就没有bug了


具体代码如下:



  • offsetParent固定元素的定位上下文,也就是相对定位的父元素

  • fixedTop是我们要触发固定的值,比如距离可视窗口顶部80px就固定



affixDom.style.top = `${isHTMLElement(offsetParent) ? (fixedTop as number) - offsetParent.getBoundingClientRect().top : fixedTop}px`;

如何找出offsetParent,也就是定位上下文


export function getContainingBlock(element: Element) {
let currentNode = element.parentElement;
while (currentNode) {
if (isContainingBlock(currentNode)) return currentNode;
currentNode = currentNode.parentElement;
}
return null;
}

工具方法,isContainingBlock如下:


import { isSafari } from './isSafari';

export function isContainingBlock(element: Element): boolean {
const safari = isSafari();
const css = getComputedStyle(element);

// https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block
return (
css.transform !== 'none' ||
css.perspective !== 'none' ||
(css.containerType ? css.containerType !== 'normal' : false) ||
(!safari && (css.backdropFilter ? css.backdropFilter !== 'none' : false)) ||
(!safari && (css.filter ? css.filter !== 'none' : false)) ||
['transform', 'perspective', 'filter'].some((value) => (css.willChange || '').includes(value)) ||
['paint', 'layout', 'strict', 'content'].some((value) => (css.contain || '').includes(value))
);
}


本文完毕,求关注,求star!!!对于react组件库感兴趣的小伙伴,欢迎加群一起交流哦!


作者:孟祥_成都
来源:juejin.cn/post/7265121637497733155
收起阅读 »

到了2038年时间戳溢出了怎么办?

计算机中的时间 看完这篇文章相信你会对计算机中的时间有更系统全面的认识。 我经常自嘲,自己写的程序运行不超过3年,因为大部分项目方就早早跑路了。大多数项目上线后,你跟这个项目就再无瓜葛,关于时间你只需要保证时区正确就不会有太大问题,哈哈。 但是今天我想认真对待...
继续阅读 »

计算机中的时间


看完这篇文章相信你会对计算机中的时间有更系统全面的认识。


我经常自嘲,自己写的程序运行不超过3年,因为大部分项目方就早早跑路了。大多数项目上线后,你跟这个项目就再无瓜葛,关于时间你只需要保证时区正确就不会有太大问题,哈哈。 但是今天我想认真对待时间这个问题,作为一个库作者或基础软件作者,就需要考虑下游项目万一因为你处理时间不当而造成困扰,影响范围就比较广了。


计算机中与时间有关的关键词:


时间类型
时间戳(timestamp
定时器(例如jssetInterval())
时间计算
时间段
超时(setTimeout())
时间片
GMT
UTC
Unix时间戳
ISO8601
CST
EST

看到这些你可能会疑惑,为何一个时间竟然如此复杂!!


如果下面的问题你都能答上来,那这篇文章对你的帮助微乎其微,不如做些更有意义的事情。



  • 常用的时间格式,他们都遵循哪些标准?

  • 什么是GMT?

  • 什么是UTC?

  • GMT UTC 和ISO8601有什么区别?

  • RFC5322是什么?

  • RFC5322 采用的是GMT还是UTC?

  • ISO8601 使用的是UTC还是GMT?

  • 在ISO8601中 Z可以使用+00:00表示吗?

  • UTC什么时候校准?

  • CST是东八区吗?

  • Z是ISO 8601规定的吗,为什么是Z?

  • 时区划分是哪个标准定义的?

  • 为什么是1970年1月1日呢?

  • 到了2038年时间戳溢出了怎么办?

  • 计算机中时间的本质是一个long类型吗?

  • WEB前后端用哪个格式传输好?

  • '2024-01-01T24:00:00' 等于 '2024-01-02T00:00:00' ??



正文开始


1. 两种时间标准


UTC和GMT都是时间标准,定义事件的精度。它们只表示 零时区 的时间,本地时间则需要与 时区 或偏移 结合后表示。这两个标准之间差距通常不会超过一秒。


UTC(协调世界时)


UTC,即协调世界时(Coordinated Universal Time),是一种基于原子钟的时间标准。它的校准是根据地球自转的变化而进行的,插入或删除闰秒的实际需求在短期内是难以预测的,因此这个决定通常是在需要校准的时候发布。 闰秒通常由国际电信联盟(ITU) 和国际度量衡局(BIPM) 等组织进行发布。由国际原子时(International Atomic Time,TAI) 通过闰秒 的调整来保持与地球自转的同步。


GMT(格林尼治标准时间)


以英国伦敦附近的格林尼治天文台(0度经线,本初子午线)的时间为基准。使用地球自转的平均速度来测量时间,是一种相对于太阳的平均时刻。尽管 GMT 仍然被广泛使用,但现代科学和国际标准更倾向于使用UTC。


2. 两种显示标准


上面我们讨论的时间标准主要保证的是时间的精度,时间显示标准指的是时间的字符串表示格式。我们熟知的有 RFC 5322 和 ISO 8601。


RFC 5322 电子邮件消息格式的规范


RFC 5322 的最新版本是在2008年10月在IETF发布的,你阅读时可能有了更新的版本。



RFC 5322 是一份由 Internet Engineering Task Force (IETF) 制定的标准,定义了 Internet 上的电子邮件消息的格式规范。该标准于2008年发布,是对之前的 RFC 2822 的更新和扩展。虽然 RFC 5322 主要关注电子邮件消息的格式,但其中的某些规范,比如日期时间格式,也被其他领域采纳,例如在 HTTP 协议中用作日期头部(Date Header)的表示。



格式通常如下:


Thu, 14 Dec 2023 05:36:56 GMT

时区部分为了可读可以如下表示:


Thu, 14 Dec 2023 05:36:56 CST
Thu, 14 Dec 2023 05:36:56 +0800
Thu, 14 Dec 2023 05:36:56 +0000
Thu, 14 Dec 2023 05:36:56 Z

但并不是所有程序都兼容这种时区格式,通常程序会忽略时区,在写程序时要做好测试。标准没有定义毫秒数如何显示。


需要注意的是,有时候我们会见到这种格式Tue Jan 19 2038 11:14:07 GMT+0800 (中国标准时间),这是js日期对象转字符串的格式,它与标准无关,千万不要混淆了。


ISO 8601


ISO 8601 最新版本是 ISO 8601:2019,发布日期为2019年11月15日,你阅读时可能有了更新的版本。


下面列举一些格式示例:


2004-05-03T17:30:08+08:00
2004-05-03T17:30:08+00:00
2004-05-03T17:30:08Z
2004-05-03T17:30:08.000+08:00

标准并没有定义小数位数,保险起见秒后面一般是3位小数用来表示毫秒数。 字母 "Z" 是 "zero"(零)的缩写,因此它被用来表示零时区,也可以使用+00:00,但Z更直观且简洁。



  1. 本标准提供两种方法来表示时间:一种是只有数字的基础格式;第二种是添加了分隔符的扩展格式,更易读。扩展格式使用连字符“-”来分隔日期,使用冒号“:”来分隔时间。比如2009年1月6日在扩展格式中可以写成"2009-01-06",在基本格式中可以简单地写成"20090106"而不会产生歧义。 若要表示前1年之前或9999年之后的年份,标准也允许有共识的双方扩展表达方式。双方应事先规定增加的位数,并且年份前必须有正号“+”或负号“-”而不使用“。依据标准,若年份带符号,则前1年为"+0000",前2年为"-0001",依此类推。

  2. 午夜,一日的开始:完全表示为000000或00:00:00;仅有小时和分表示为0000或00:00

  3. 午夜,一日的终止:完全表示为240000或24:00:00;仅有小时和分表示为2400或24:00

  4. 如果时间在零时区,并恰好与UTC相同,那么在时间最后加一个大写字母Z。Z是相对协调世界时时间0偏移的代号。 如下午2点30分5秒表示为14:30:05Z或143005Z;只表示小时和分,为1430Z或14:30Z;只表示小时,则为14Z或14Z。

  5. 其它时区用实际时间加时差表示,当时的UTC+8时间表示为22:30:05+08:00或223005+0800,也可以简化成223005+08。


日期与时间合并表示时,要在时间前面加一大写字母T,如要表示东八区时间2004年5月3日下午5点30分8秒,可以写成2004-05-03T17:30:08+08:00或20040503T173008+08。


在编写API时推荐使用ISO 8601标准接收参数或响应结果,并且做好时区测试,因为不同编程语言中实现可能有差异。


时区划分和偏移



全球被分为24个时区,每个时区对应一个小时的时间差。 时区划分由IANA维护和管理,其时区数据库被称为 TZ Database(或 Olson Database)。这个数据库包含了全球各个时区的信息,包括时区的名称、标识符、以及历史性的时区变更数据,例如夏令时的开始和结束时间等。在许多操作系统(如Linux、Unix、macOS等)和编程语言(如Java、Python等)中得到广泛应用。


TZ Database具体见我整理的表格,是从Postgresql中导出的一份Excel,关注公众号"程序饲养员",回复"tz"



时区标识符采用"洲名/城市名"的命名规范,例如:"America/New_York"或"Asia/Shanghai"。这种命名方式旨在更准确地反映时区的地理位置。时区的具体规定和管理可能因国家、地区、或国际组织而异。


有一些时区是按照半小时或15分钟的间隔进行偏移的,以适应地理和政治需求。在某些地区,特别是位于边界上的地区,也可能采用不同的时区规则。


EST,CST、GMT(另外一个含义是格林尼治标准时间)这些都是时区的缩写。


这种简写存在重复,如CST 可能有多种不同的含义,China Standard Time(中国标准时间),它对应于 UTC+8,即东八区。Central Standard Time(中部标准时间) 在美国中部标准时间的缩写中也有用。中部标准时间对应于 UTC-6,即西六区。因此在某些软件配置时不要使用简称,一定要使用全称,如”Asia/Shanghai“。


采用东八区的国家和地区有哪些



  • 中国: 中国标准时间(China Standard Time,CST)是东八区的时区,对应于UTC+8。

  • 中国香港: 中国香港也采用东八区的时区,对应于UTC+8。

  • 中国澳门: 澳门也在东八区,使用UTC+8。

  • 中国台湾: 台湾同样在东八区,使用UTC+8。

  • 新加坡: 新加坡位于东八区,使用UTC+8。

  • 马来西亚: 马来西亚的半岛部分和东马来西亚位于东八区,使用UTC+8。

  • 菲律宾: 菲律宾采用东八区的时区,对应于UTC+8。


计算机系统中的时间 —— Unix时间戳


Unix时间戳(Unix timestamp)定义为从1970年01月01日00时00分00秒(UTC)起至现在经过的总秒数(秒是毫秒、微妙、纳秒的总称)。


这个时间点通常被称为 "Epoch" 或 "Unix Epoch"。时间戳是一个整数,表示从 Epoch 开始经过的秒数。


一些关键概念:



  1. 起始时间点: Unix 时间戳的起始时间是 1970 年 1 月 1 日 00:00:00 UTC。在这一刻,Unix 时间戳为 0。

  2. 增量单位: Unix 时间戳以秒为单位递增。每过一秒,时间戳的值增加 1。

  3. 正负值: 时间戳可以是正值或负值。正值表示从 Epoch 开始经过的秒数,而负值表示 Epoch 之前的秒数。

  4. 精度: 通常情况下,Unix 时间戳以整数形式表示秒数。有时也会使用浮点数表示秒的小数部分,以提供更精细的时间分辨率。精确到秒是10位;有些编程语言精确到毫秒是13位,被称为毫秒时间戳。


为什么是1970年1月1日?


这个选择主要是出于历史和技术的考虑。


Unix 操作系统的设计者之一,肯·汤普森(Ken Thompson)和丹尼斯·里奇(Dennis Ritchie)在开发 Unix 操作系统时,需要选择一个固定的起始点来表示时间。1970-01-01 00:00:00 UTC 被选为起始时间。这个设计的简洁性和通用性使得 Unix 时间戳成为计算机系统中广泛使用的标准方式来表示和处理时间。


时间戳为什么只能表示到2038年01月19日03时14分07秒?


在许多系统中,结构体time_t 被定义为 long,具体实现取决于编译器和操作系统的架构。例如,在32位系统上,time_t 可能是32位的 long,而在64位系统上,它可能是64位的 long。 32位有符号long类型,实际表示整数只有31位,最大能表示十进制2147483647(01111111 11111111 11111111 11111111)。


> new Date(2147483647000)
< Tue Jan 19 2038 11:14:07 GMT+0800 (中国标准时间)

实际上到2038年01月19日03时14分07秒,便会到达最大时间,过了这个时间点,所有32位操作系统时间便会变为10000000 00000000 00000000 00000000。因具体实现不同,有可能会是1901年12月13日20时45分52秒,这样便会出现时间回归的现象,很多软件便会运行异常了。


至于时间回归的现象相信随着64为操作系统的产生逐渐得到解决,因为用64位操作系统可以表示到292,277,026,596年12月4日15时30分08秒。


另外,考虑时区因素,北京时间的时间戳的起始时间是1970-01-01T08:00:00+08:00。


好了,关于计算机中的时间就说完了,有疑问评论区相见 或 关注 程序饲养员 公号。



作者:程序饲养员
来源:juejin.cn/post/7312640704404111387
收起阅读 »

Arrays.asList() 隐藏的陷阱,你避开了吗?

[Arrays.asList()方法介绍] [Arrays.asList()方法的坑] [解决Arrays.asList()方法的坑] [总结] [Arrays.asList()方法介绍] [Arrays.asList()方法的坑] [解决Arrays.asL...
继续阅读 »

  • [Arrays.asList()方法介绍]

  • [Arrays.asList()方法的坑]

  • [解决Arrays.asList()方法的坑]

  • [总结]

  • [Arrays.asList()方法介绍]

  • [Arrays.asList()方法的坑]

  • [解决Arrays.asList()方法的坑]

  • [总结]




在Java中,我们经常需要将数组转换为List来方便地进行操作。Arrays.asList()方法是一种常见的方式,但是它存在一个不太常见但需要注意的坑。


本文将深入探讨Arrays.asList()的使用,揭示其中的陷阱,并提供解决方案。


[Arrays.asList()方法介绍]


Arrays.asList()方法是将数组转换为List的方法,它返回一个List对象,但这个List对象并不是java.util.ArrayList对象,而是Arrays内部的ArrayList对象。


Arrays.ArrayList类继承自AbstractList,实现了List接口。它重写了add()remove()等修改List结构的方法,并将它们直接抛出UnsupportedOperationException异常,从而禁止了对List结构的修改。


具体来说,Arrays.asList()方法返回的是Arrays类中的一个私有静态内部类ArrayList,它继承自AbstractList类,实现了List接口。


Arrays.asList()方法的使用非常简单,只需要将一个数组作为参数传递给该方法即可。例如:


String[] arr = new String[]{"a""b""c"};
List<String> list = Arrays.asList(arr);


基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能




[Arrays.asList()方法的坑]


尽管Arrays.asList()方法很方便,但也存在一些坑,其中最常见的一个是:在使用Arrays.asList()方法时,如果对返回的List对象进行修改(例如增加、删除元素),将会抛出"UnsupportedOperationException"异常。


为什么会出现这个异常呢?这是因为Arrays.asList()方法返回的List对象,是一个固定大小的List,不能进行结构上的修改,否则会抛出异常。


下面的代码演示了这个问题:


String[] arr = new String[]{"a""b""c"};
List<String> list = Arrays.asList(arr);
list.add("d"); // 抛出 UnsupportedOperationException 异常

上述代码中,我们尝试向List对象中添加一个新的元素"d",结果会抛出"UnsupportedOperationException"异常。



基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能




[解决Arrays.asList()方法的坑]


要解决Arrays.asList()方法的坑,我们需要将返回的List对象转换为一个可修改的List对象。有几种方法可以实现这个目标:


[方法一:使用java.util.ArrayList类]


我们可以使用java.util.ArrayList类,将Arrays.asList()方法返回的List对象转换为一个java.util.ArrayList对象,示例如下:


String[] arr = new String[]{"a""b""c"};
List<String> list = new ArrayList<>(Arrays.asList(arr));
list.add("d"); // 正常运行

上述代码中,我们首先使用Arrays.asList()方法将一个数组转换为一个List对象,然后使用ArrayList的构造方法,将这个List对象转换为一个java.util.ArrayList对象,最后可以向这个ArrayList对象中添加元素。


[方法二:使用Collections类]


我们也可以使用Collections类提供的静态方法,将Arrays.asList()方法返回的List对象转换为一个可修改的List对象,示例如下:


String[] arr = new String[]{"a""b""c"};
List<String> list = new ArrayList<>(Arrays.asList(arr));
Collections.addAll(list, "d"); // 正常运行

通过Collections.addAll()方法,我们可以将数组中的元素逐个添加到一个新的ArrayList对象中,从而实现了可修改性。


[总结]


在使用Arrays.asList()方法时,需要注意返回的List对象是一个固定大小的List,不支持结构上的修改操作。为了避免这个陷阱,我们可以使用java.util.ArrayList或Collections类提供的方法将返回的List对象转换为可修改的List。通过了解这个陷阱并采取相应的解决方案,我们可以安全地将数组转换为List,并避免潜在的异常情况。


不要让Arrays.asList()的陷阱坑了你的代码!


在Java中,我们经常需要将数组转换为List来方便地进行操作。Arrays.asList()方法是一种常见的方式,但是它存在一个不太常见但需要注意的坑。本文将深入探讨Arrays.asList()的使用,揭示其中的陷阱,并提供解决方案。


[Arrays.asList()方法介绍]


Arrays.asList()方法是将数组转换为List的方法,它返回一个List对象,但这个List对象并不是java.util.ArrayList对象,而是Arrays内部的ArrayList对象。


Arrays.ArrayList类继承自AbstractList,实现了List接口。它重写了add()remove()等修改List结构的方法,并将它们直接抛出UnsupportedOperationException异常,从而禁止了对List结构的修改。


具体来说,Arrays.asList()方法返回的是Arrays类中的一个私有静态内部类ArrayList,它继承自AbstractList类,实现了List接口。


Arrays.asList() 方法的使用非常简单,只需要将一个数组作为参数传递给该方法即可。例如:


String[] arr = new String[]{"a""b""c"};
List<String> list = Arrays.asList(arr);

[Arrays.asList()方法的坑]


尽管Arrays.asList()方法很方便,但也存在一些坑,其中最常见的一个是:在使用Arrays.asList()方法时,如果对返回的List对象进行修改(例如增加、删除元素),将会抛出"UnsupportedOperationException"异常。


为什么会出现这个异常呢?这是因为Arrays.asList()方法返回的List对象,是一个固定大小的List,不能进行结构上的修改,否则会抛出异常。


下面的代码演示了这个问题:


String[] arr = new String[]{"a""b""c"};
List<String> list = Arrays.asList(arr);
list.add("d"); // 抛出 UnsupportedOperationException 异常

上述代码中,我们尝试向List对象中添加一个新的元素"d",结果会抛出"UnsupportedOperationException"异常。


[解决Arrays.asList()方法的坑]


要解决Arrays.asList()方法的坑,我们需要将返回的List对象转换为一个可修改的List对象。有几种方法可以实现这个目标:


[方法一:使用java.util.ArrayList类]


我们可以使用java.util.ArrayList类,将Arrays.asList()方法返回的List对象转换为一个java.util.ArrayList对象,示例如下:


String[] arr = new String[]{"a""b""c"};
List<String> list = new ArrayList<>(Arrays.asList(arr));
list.add("d"); // 正常运行

上述代码中,我们首先使用Arrays.asList()方法将一个数组转换为一个List对象,然后使用ArrayList的构造方法,将这个List对象转换为一个java.util.ArrayList对象,最后可以向这个ArrayList对象中添加元素。


[方法二:使用Collections类]


我们也可以使用Collections类提供的静态方法,将Arrays.asList()方法返回的List对象转换为一个可修改的List对象,示例如下:


String[] arr = new String[]{"a""b""c"};
List<String> list = new ArrayList<>(Arrays.asList(arr));
Collections.addAll(list, "d"); // 正常运行

通过Collections.addAll()方法,我们可以将数组中的元素逐个添加到一个新的ArrayList对象中,从而实现了可修改性。


[总结]


在使用Arrays.asList()方法时,需要注意返回的List对象是一个固定大小的List,不支持结构上的修改操作。为了避免这个陷阱,我们可以使用java.util.ArrayList或Collections类提供的方法将返回的List对象转换为可修改的List。通过了解这个陷阱并采取相应的解决方案,我们可以安全地将数组转换为List,并避免潜在的异常情况。


不要让Arrays.asList()的陷阱坑了你的代码!


作者:智多星云
来源:juejin.cn/post/7258863572553302071
收起阅读 »