注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

为什么要使用Kotlin 对比 Java,Kotlin简介

什么是Kotlin 打开Kotlin编程语言的官网,里面大大的写着, A modern programming languagethat makes developers happier. 是一门让程序员写代码时更有幸福感的现代语言 Kotlin语法...
继续阅读 »

什么是Kotlin


打开Kotlin编程语言的官网,里面大大的写着,



A modern programming languagethat makes developers happier.




是一门让程序员写代码时更有幸福感的现代语言




  • Kotlin语法糖非常多,可以写出更为简洁的代码,便于阅读。

  • Kotlin提供了空安全的支持,可以的让程序更为稳定。

  • Kotlin提供了协程支持,让异步任务处理起来更为方便。

  • Google:Kotlin-first,优先支持kotlin,使用kotlin可以使用更多轮子


接下来对比Java举一些例子。


简洁


当定义一个网络请求的数据类时


Java

public class JPerson {
private String name;
private int age;
//getter
//setter
//hashcode
//copy
//equals
//toString
}

Kotlin

data class KPerson(val name: String,val age: Int)

这里用的是Kotlin 的data class 在class 前面加上data 修饰后,kotlin会自动为我们生成上述Java类注释掉的部分


当我们想从List中筛掉某些我们不想要的元素时


Java

List<Integer> list = new ArrayList<>();  

List<Integer> result = new ArrayList<>();
for (Integer integer : list) {
if (integer > 0) { //只要值>0的
result.add(integer);
}
}

System.out.println(result);

Kotlin

val list: List<Int> = ArrayList()

println(list.filter { it > 0 })

如上代码,都能达到筛选List中 值>0 的元素的效果。


这里的filter是Kotlin提供的一个拓展函数,拓展函数顾名思义就是拓展原来类中没有的函数,当然我们也可以自定义自己的拓展函数。


当我们想写一个单例类时


Java

public class PersonInJava {
public static String name = "Jayce";
public static int age = 10;

private PersonInJava() {
}
private static PersonInJava instance;
static {
instance = new PersonInJava();
}
public static PersonInJava getInstance() {
return instance;
}
}

Kotlin

object PersonInKotlin {
val name: String = "Jayce"
val age: Int = 10
}

是的,只需要把class换成object就可以了,两者的效果一样。


还有很多很多,就不一一举例了,接下来看看安全。


安全


空安全

var name: String = "Jayce" //name的定义是一个非空的String
name = null //将name赋值为null,IDE会报错,编译不能通过,因为name是非空的String

var name: String? = "Jayce" //String后面接"?"说明是一个可空的String
name.length //直接使用会报错,需要提前判空
//(当然,Kotlin为我们提供了很多语法糖,我们可以很方便的进行判空)

类型转换安全

fun gotoSleep(obj: Any) {
if (obj is PersonInKotlin) {//判断obj是不是PersonInKotlin
obj.sleep() // 在if的obj已经被认为是PersonInKotlin类型,所以可以直接调用他的函数,调用前不需要类型转换
}
}

协程


这里只是简单的举个例子


Kotlin的协程不是传统意义上那个可以提高并发性能的协程序


官方的对其定义是这样的



  • 协程是一种并发设计模式,您可以在Android平台上使用它来简化异步执行的代码

  • 程序的逻辑可以在协程中顺序地表达,而底层库会为我们解决其异步性。


当我们用Java请求网络数据时,一般是这么写的。

 getPerson(new Callback<Person>() {//这里有一个回调            
@Override
public void success(Person person) {
runOnUiThread(new Runnable() { //切换线程
@Override
public void run() {
updateUi(person)
}
})
}

@Override
public void failure(Exception e) {
...
}
});

Kotlin协程后我们只需要这么写

 CoroutineScope(Dispatchers.Main).launch { //启动一个协程
val person = withContext(Dispatchers.IO) {//切换IO线程
getPerson() //请求网络
}
updateUi(person)//主线程更新UI
}

他们两个都干的同一件事,最明显的区别就是,代码更为简洁了,如果在回调里面套回调的话回更加明显,用Java的传统写法就会造成人们所说的CallBack Hell。


除此之外协程还有如下优点



  • 轻量

  • 更少的内存泄漏

  • 内置取消操作

  • 集成了Jatpack


这里就不继续深入了,有兴趣的同学可以参考其他文章。


Kotlin-first


在Google I/O 2019的时候,谷歌已经宣布Kotlin-first,建议Android开发将Kotlin作为第一开发语言。


为什么呢,总结就是因为Kotlin简洁、安全、兼容Java、还有协程。


至于有没有其他原因,我也不知道。(手动狗头)


Google将为更多的投入到Kotlin中来,比如




  • 为Kotlin提供特定的APIs (KTX, 携程, 等)




  • 提供Kotlin的线上练习




  • 示例代码优先支持Kotlin




  • Jetpack Compose,这个是用Kotlin开发的,没得选。。。。。




  • 跨平台开发,用Kotlin实现跨平台开发。






好的Kotlin就先介绍到这里,感兴趣的同学就快学起来吧~
接下来在其他文章会对Kotlin和携程进行详细的介绍。


df7da83ea7648fdeb9963e9552ba2425.gif


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

Android 文件上传(包括大文件上传)

1.简介: android 文件上传可以分为两类:一个是小文件,直接上传文件;一个是大文件,这个需要分块上传。Okhttp+Retrofit实现文件上传。 2. 需要的依赖和权限: implementation 'com.squareup.retrofi...
继续阅读 »

1.简介:


android 文件上传可以分为两类:一个是小文件,直接上传文件;一个是大文件,这个需要分块上传。Okhttp+Retrofit实现文件上传。


2. 需要的依赖和权限:

    implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.squareup.retrofit2:adapter-rxjava2:2.5.0'
implementation 'io.reactivex.rxjava2:rxandroid:2.0.2'
implementation 'io.reactivex.rxjava2:rxjava:2.2.5'
implementation 'com.squareup.retrofit2:converter-scalars:2.9.0'
implementation 'com.squareup.okhttp3:logging-interceptor:3.9.1'
    <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

3.示例:


3.1.小文件上传:直接上传文件(图片上传为例)

public class UpLoadImageUtils {
private static final String TAG = "UpLoadImageUtils";
//需要上传的图片数量
private static int imgSum;
//上传成功的图片数量
private static int uploadSuccessNum;
private static String enRttId;
//失败数量
private static int errorNum;

private static TestService apiService;


public static void getService(){
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(BuildConfig.BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build();
apiService = retrofit.create(TestService.class);
}

@SuppressLint("CheckResult")
public static void uploadImage(String url, File file) {
MultipartBody.Builder builder = new MultipartBody.Builder()
.setType(MultipartBody.FORM);//表单类型
Map<String, RequestBody> map = new HashMap<>();

//"image/png" 是内容类型,后台设置的类型

RequestBody requestBody = RequestBody.create(MediaType.parse("multipart/form-data"), file);

builder.addFormDataPart("name", file.getName());
builder.addFormDataPart("size", "" + file.length());
/*
* 这里重点注意:
* com_img[]里面的值为服务器端需要key 只有这个key 才可以得到对应的文件
* filename是文件的名字,包含后缀名的 比如:abc.png
*/

builder.addFormDataPart("file", file.getName(), requestBody);
MultipartBody body = builder.build();
Observable<BaseResponse> meSetIconObservable = apiService.imgUpload(url, body);

meSetIconObservable.subscribeOn(Schedulers.io())
.unsubscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(baseResponse -> {
Log.i(TAG, JsonUtil.jsonToString(baseResponse));
if ("000000".equalsIgnoreCase(baseResponse.getCode())) {
Log.i(TAG, "onComplete: ---" + uploadSuccessNum);
errorNum = 0;
uploadSuccessNum++;
if (imgSum == uploadSuccessNum) {
finishUpload(true);
uploadSuccessNum = 0;
}
} else {
if (errorNum < 4) {
uploadImage(url, file);
errorNum++;
}else {
finishUpload(false);
}
}
}, throwable -> {
Log.i(TAG, "onComplete: ---" + throwable.getMessage());
});
}


/**
* 上传
*
* @param compressFile 需要上传的文件
* @param urls 需要上传的文件地址
*/
public static void uploadList(List<String> urls, List<File> compressFile) {
getService();
//多张图片
imgSum = urls.size();
for (int i = 0; i < compressFile.size(); i++) {
uploadImage(urls.get(i), compressFile.get(i));
}
}


public interface TestService {
@POST()
Observable<BaseResponse> imgUpload(@Url String url, @Body MultipartBody multipartBody);
}
}

public class UpLoadImageUtils {
private static final String TAG = "UpLoadImageUtils";
//需要上传的图片数量
private static int imgSum;
//上传成功的图片数量
private static int uploadSuccessNum;
private static String enRttId;
//失败数量
private static int errorNum;

private static TestService apiService;


public static void getService(){
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(BuildConfig.BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build();
apiService = retrofit.create(TestService.class);
}

@SuppressLint("CheckResult")
public static void uploadImage(String url, File file) {
MultipartBody.Builder builder = new MultipartBody.Builder()
.setType(MultipartBody.FORM);//表单类型
Map<String, RequestBody> map = new HashMap<>();

//"image/png" 是内容类型,后台设置的类型

RequestBody requestBody = RequestBody.create(MediaType.parse("multipart/form-data"), file);

builder.addFormDataPart("name", file.getName());
builder.addFormDataPart("size", "" + file.length());
/*
* 这里重点注意:
* com_img[]里面的值为服务器端需要key 只有这个key 才可以得到对应的文件
* filename是文件的名字,包含后缀名的 比如:abc.png
*/

builder.addFormDataPart("file", file.getName(), requestBody);
MultipartBody body = builder.build();
Observable<BaseResponse> meSetIconObservable = apiService.imgUpload(url, body);

meSetIconObservable.subscribeOn(Schedulers.io())
.unsubscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(baseResponse -> {
Log.i(TAG, JsonUtil.jsonToString(baseResponse));
if ("000000".equalsIgnoreCase(baseResponse.getCode())) {
Log.i(TAG, "onComplete: ---" + uploadSuccessNum);
errorNum = 0;
uploadSuccessNum++;
if (imgSum == uploadSuccessNum) {
finishUpload(true);
uploadSuccessNum = 0;
}
} else {
if (errorNum < 4) {
uploadImage(url, file);
errorNum++;
}else {
finishUpload(false);
}
}
}, throwable -> {
Log.i(TAG, "onComplete: ---" + throwable.getMessage());
});
}


/**
* 上传
*
* @param compressFile 需要上传的文件
* @param urls 需要上传的文件地址
*/
public static void uploadList(List<String> urls, List<File> compressFile) {
getService();
//多张图片
imgSum = urls.size();
for (int i = 0; i < compressFile.size(); i++) {
uploadImage(urls.get(i), compressFile.get(i));
}
}


public interface TestService {
@POST()
Observable<BaseResponse> imgUpload(@Url String url, @Body MultipartBody multipartBody);
}
}


3.2.大文件分块上传(视频上传为例)同步

public class UploadMediaFileUtils {

private static final String TAG = "UploadMediaFileUtils";
private static UploadService uploadService;
//基础的裁剪大小20m
private static final long baseCuttingSize = 20 * 1024 * 1024;
//总的块数
private static int sumBlock;
//取消上传
private static boolean isCancel = false;
//是否在上传中
private static boolean isUploadCenter=false;

public static void uploadMediaFile(String url, String uploadName, File file, String appInfo, IOUploadAudioListener ioResultListener) {
if (file.exists()) {
getService();
//总的分块数
sumBlock = (int) (file.length() / baseCuttingSize);
if (file.length() % baseCuttingSize != 0) {
sumBlock = sumBlock + 1;
}
isCancel = false;
isUploadCenter = true;
uploadMedia(url, uploadName, file, appInfo, 1, ioResultListener);
} else {
Log.i(TAG, "文件不存在");
ioResultListener.errorResult("-1", "文件不存在");
}
}

@SuppressLint("CheckResult")
public static void uploadMedia(String url, String uploadName, File file, String appInfo, int currentBlock, IOUploadAudioListener ioResultListener) {
if (isCancel){
Log.i(TAG, "取消上传");
return;
}

byte[] fileStream = cutFile(file, currentBlock - 1, ioResultListener);
if (fileStream == null) {
Log.i(TAG, "uploadMedia: getBlock error");
ioResultListener.errorResult("-1", "fileStream为空");
return;
}

MultipartBody.Builder builder = new MultipartBody.Builder()
.setType(MultipartBody.FORM);//表单类型
RequestBody requestBody = RequestBody.create(MultipartBody.FORM, fileStream);
builder.addFormDataPart("name", uploadName);
builder.addFormDataPart("size", "" + fileStream.length);
Log.i(TAG, "size" + fileStream.length);
builder.addFormDataPart("num", "" + currentBlock);
/*
* 这里重点注意:
* com_img[]里面的值为服务器端需要key 只有这个key 才可以得到对应的文件
* filename是文件的名字,包含后缀名的 比如:abc.png
*/

builder.addFormDataPart("file", file.getName(), requestBody);
MultipartBody body = builder.build();
Observable<BaseResponse> meSetIconObservable = uploadService.mediaUpload("huizhan", appInfo, url, body);

meSetIconObservable.subscribeOn(Schedulers.io())
.unsubscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(baseResponse -> {
if ("000000".equalsIgnoreCase(baseResponse.getCode())) {
double progress = 100 * div(currentBlock, sumBlock, 2);
ioResultListener.progress("" + progress);
if (currentBlock < sumBlock) {
uploadMedia(url, uploadName, file, appInfo, currentBlock + 1, ioResultListener);
return;
}
ioResultListener.successResult(baseResponse);
} else {
ioResultListener.errorResult(baseResponse.getCode(), baseResponse.getDesc());
}
}, throwable -> {
ioResultListener.errorResult("-1", "上传失败");
});
}

public static void getService() {
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build();
uploadService = retrofit.create(UploadService.class);
}


public interface UploadService {
@POST()
Observable<BaseResponse> mediaUpload(@Header("X-Biz-Id") String bizId,
@Header("X-App-Info") String AppInfo,
@Url String url,
@Body MultipartBody multipartBody);
}



/**
* 写入本地(测试用)
*
* @param list
*/
public static void writeFile(List<byte[]> list) {
FileWriter file1 = new FileWriter();
String path = Environment.getExternalStorageDirectory() + File.separator + "12345.wav";
try {
file1.open(path);
for (int i = 0; i < list.size(); i++) {
Log.i(TAG, "writeFile: " + list.get(i).length);
file1.writeBytes(list.get(i), 0, list.get(i).length);
}
file1.close();
LogUtils.i(TAG, "writeFile: ");
} catch (Exception e) {
e.printStackTrace();
}
}


public static byte[] cutFile(File file, int currentBlock, IOUploadAudioListener ioResultListener) {
Log.i(TAG, "getBlockThree:000000---" + currentBlock);
int size = 20 * 1024 * 1024;
byte[] endResult = null;
byte[] result = new byte[size];
ByteArrayOutputStream out = new ByteArrayOutputStream();
try {
RandomAccessFile accessFile = new RandomAccessFile(file, "rw");
accessFile.seek(currentBlock * size);
//判断是否整除
if (file.length() % baseCuttingSize != 0) {
//当前的位数和总数是否相等(是不是最后一段)
if ((currentBlock + 1) != sumBlock) {
int len = accessFile.read(result);
out.write(result, 0, len);
endResult = out.toByteArray();
} else {
//当有余数时
//当前位置2147483647-20971520
byte[] bytes = new byte[(int) (file.length() % baseCuttingSize)];
int len = accessFile.read(bytes);
out.write(bytes, 0, len);
endResult = out.toByteArray();
}
} else {
int len = accessFile.read(result);
out.write(result, 0, len);
endResult = out.toByteArray();
}
accessFile.close();
out.close();
} catch (IOException e) {
// e.printStackTrace();
ioResultListener.errorResult("-1", "cutFile失败");
}
return endResult;
}

/**
* 提供(相对)精确的除法运算。当发生除不尽的情况时,由scale参数指
* 定精度,以后的数字四舍五入。
*
* @param v1 被除数
* @param v2 除数
* @param scale 表示表示需要精确到小数点以后几位。
* @return 两个参数的商
*/
public static double div(double v1, double v2, int scale) {
if (scale < 0) {
throw new IllegalArgumentException(
"The scale must be a positive integer or zero");
}
BigDecimal b1 = new BigDecimal(Double.toString(v1));
BigDecimal b2 = new BigDecimal(Double.toString(v2));
return b1.divide(b2, scale, BigDecimal.ROUND_HALF_UP).doubleValue();
}

/**
* 取消上传
*/
public static void cancelUpload() {
if (isUploadCenter) {
isCancel = true;
}
}

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

关于 Android 稳定性优化你应该了解的知识点

前言 Android 稳定性优化是一个需要长期投入,持续运营和维护的一个过程,不仅深入探讨了 Java Crash、Native Crash 和 ANR 的解决流程及方案,还分析了其内部实现原理和监控流程。本文对稳定性优化方面的知识做了一个全面总结,主要内容如...
继续阅读 »

前言


Android 稳定性优化是一个需要长期投入,持续运营和维护的一个过程,不仅深入探讨了 Java Crash、Native Crash 和 ANR 的解决流程及方案,还分析了其内部实现原理和监控流程。本文对稳定性优化方面的知识做了一个全面总结,主要内容如下:


image.png


如何提升App的稳定性


一般性的App能接触到稳定性的需求其实并不多,只有大型的处于稳定运营期的App才会重视App的稳定性,稳定性实际上是一个大问题,一个稳定的产品才能够保证用户的留存率,所以稳定性是质量体系中最基本也是最关键的一环:



  • 稳定性是大问题,Crash是P0优先级:对于用户来说很难容忍你的应用发生崩溃

  • 稳定性可优化的面很广:不仅仅是指崩溃,像卡顿、耗电等也属于稳定性优化的范畴,对于移动端高可用这个标准来说,性能优化只是高可用的一部分,还有一部分就是应用业务流程功能上的可用


稳定性维度



  • Crash维度:一般会将Crash单独作为一项重要指标进行突破,最常见的统计指标就是Crash率,后面会说到

  • 性能维度:启动速度、内存、卡顿、流量、电量等等,在解决应用的Crash之后,就应该着手保障性能体系的稳定

  • 业务高可用维度:业务层面的高可用是相当关键的一步,需要使用多种手段去保障App业务的主流程及核心路径的可用性


稳定性优化概述


如果App到了线上才发现异常,其实已经造成了损失,所以稳定性优化重点在于预防



  • 重在预防、监控必不可少:从开发到测试到发布上线运维这些各个阶段都需要预防异常的发生,或者说要将发生异常造成的损失降到最低,用最小的代价暴露最多的问题,同时监控也是必不可少的一步,需要拥有一定的监控手段来更加灵敏的发现问题

  • 思考更深一层、重视隐含信息:比如你发现了一个崩溃,但是你不能简单的只看这一个崩溃,要考虑这个崩溃是不是在其他地方也有同样或者类似的,如果有就考虑是否统一处理,今后该如何预防,总结经验

  • 长效保持需要科学流程:在项目的每一个阶段建立完善的相关规范,保证长效的优化效果


如何有效降低应用崩溃率


Crash相关指标


1.UV、PV Crash率



  • UV Crash率:等于Crash UV/DAU:主要针对于用户使用量的统计,它统计一段时间内所有用户中发生过崩溃的用户占比,和UV强相关,UV是指Unique Visitor一天内访问网站的人数(是以cookie为依据),一天内同一访客的多次访问只计算为1,一台电脑不同的浏览器的cookie值不同。

  • PV Crash率:针对用户使用频率的统计,统计一段时间内所有用户启动次数中发生崩溃的占比,和PV强相关,PV是指PageView也就是页面点击量,每次刷新就算一次浏览,多次打开同一页面会累加。

  • UV Crash方便评估用户影响范围,PV Crash方便评估相关Crash的影响严重程度

  • 注意:沿用同一种衡量方式:不管你是使用UVCrash还是PVCrash作为主要指标,你应该一直使用它,因为和Crash率相关的会有一些经验值,这些经验值需要对应一个衡量指标


2.Java、Native Crash率



  • Java Crash:在Java代码中,出现了未捕获的异常,导致程序异常退出

  • Native Crash:一般都是因为在Native代码中访问非法地址,也可能是地址对齐出现了问题,或者发生了程序主动abort,这些都会产生相应的signal信号,导致程序异常退出。目前Native崩溃中最成熟的方案是BreakPad


3.启动、重点流程Crash率



  • 启动Crash率:在启动阶段用户还没有完全打开就发生了崩溃的占比,只要是用户打开App10s之内发生的崩溃都被视为启动Crash,它是Crash分类中最严重的一类,我们需要重点关注这个指标,而且降低的越低越好,并且我们应该结合客户端的容灾策略进行自主修复


4.增量、存量Crash率



  • 增量Crash:指新增Crash,它是新版本Crash率变动的原因,如果没有新增的Crash,那么新版本的Crash率应该是和老版本Crash率保持一致,所以增量Crash是新版本中需要重点解决的问题

  • 存量Crash:指老版本中已经存在的Crash,这些Crash一般都是难以解决或者是需要在特定场景下才会出现的难以复现的问题,这类问题需要长期投入精力持续解决

  • 优先解决增量、持续跟进存量


5.Crash率评价指标



  • 务必在千分之二以下:Java和Native的崩溃率加起来需要在千分之二以下才能算是合格的

  • Crash率处于万分位视为优秀的标准


Crash关键问题


1.尽可能还原Crash现场


一旦发生崩溃,我们需要尽可能保留崩溃现场信息,这样有利于还原崩溃发生时的各种场景信息,从而推断出可能导致崩溃的原因,对于采集环节可以参考以下采集点:



  • 采集堆栈、用户设备、OS版本、发生崩溃的进程、线程名、崩溃前后的Logcat

  • 前后台、使用时长、App版本、小版本、渠道

  • CPU架构、内存信息、线程数、资源包信息、行为日志


image.png


上面是一张Bugly后台的截图,对于成熟的性能监控平台不仅有Crash的单独信息,同时会对各种Crash进行聚合以及报警。


2.APM后台聚合展示



  • Crash现场信息:包括Crash具体堆栈信息及其它额外信息

  • Crash Top机型、OS版本、分布版本、发生地域:有了这些Top Crash信息之后就能够知道哪些Crash的影响范围比较大需要重点关注

  • Crash起始版本、上报趋势、是否新增、持续版本、发生量级等等:可以从多个视角判断Crash发生的可能原因以及决定是否需要修复,在哪些版本上进行修复


3.Crash相关的整体架构


image.png


4.非技术相关的关键问题


建立规范的流程保证开发人员能够及时处理线上发生的问题:



  • 专项小组轮值:成立专门小组来跟踪每个版本周期之内线上产生的Crash,保证一定有人跟进处理

  • 自动分配匹配:可以自定义某些业务模块名称,自动分配给相应人员处理

  • 处理流程全纪录:记录相应人员处理Crash的每一步,后期出现问题追究相关责任人也是有据可查的


单个Crash处理方案



  • 根据堆栈及现场信息找答案:一般来说堆栈信息可以帮助我们解决90%的问题

  • 找共性比如机型、OS、实验开关、资源包等:有些Crash信息通过堆栈找不到有用的帮助,不能直接解决,这种情况下可以通过Crash发生时的各种现场信息作辅助判断,分析这些Crash用户拥有哪些共性

  • 线下复现、远程调试:有了共性之后尝试在线下进行复现,或者尝试能否进行远程调试


Crash率治理方案



  • 解决线上常规Crash:抽出一定时间来专门解决所有常规的Crash,这些Crash一般相对来说比较容易解决

  • 系统级Crash尝试Hook绕过:当然Android系统版本一直在不断的升级,随着新系统的覆盖率越来越高,老版本的系统Bug可能会逐渐减少

  • 疑难Crash重点突破、更换方案:做到长期跟踪,团队合作重点突破,实在不行可以考虑更换实现方案


通过以上几点应该可以解决大部分的存量Crash,同时再控制好新增Crash,这样一来整体的Crash率一般都能够得到有效降低。


这一部分的内容有点杂而多,一般也是需要多端配合,所以不太好做具体演示,大家可以在网上多查找相关资料进行巩固学习。


如何选择合适的崩溃服务



  1. 腾讯Bugly: 除了有crash数据还有运营数据

  2. UC 啄木鸟:可以捕获Java、Native异常,被系统强杀的异常,ANR,Low Memory Killer、killProcess。技术深度以及捕获能力强

  3. 网易云捕:继承便捷,访问快,捕获以及上报速度及时,支持实时报警,提供多种报警选项,可以自定义参数。

  4. Google的Firebase

  5. crashlytics:服务器在国外,访问速度慢,会丢掉数据

  6. 友盟:crash之后会在再次启动的时候上报数据,所以不能立即获得这部分信息


移动端业务高可用方案


移动端高可用方案不仅仅是指性能方面的高可用,业务方面的高可用也是尤为重要的,如果业务都走不通,试问你性能做的再好又有何用呢?



  • 高可用:性能+业务

  • 业务高可用侧重于用户功能完整可用

  • 业务高可用影响公司实际收入:比如支付流程不通


对于业务是否可用不像Crash一样,如果发生Crash我们可以收到系统的回调,业务不可用实际上我们是无从知道的,所以针对建设移动端业务高可用的方案总结以下几点:


1.数据采集



  • 梳理项目主流程、核心路径、关键节点:一般需要对项目主流程和核心路径做埋点监控,比如用户下单需要从列表页到详情页再到下单页,这就是一个核心路径,我们可以监控具体每个页面的到达率和下单成功率

  • AOP自动采集、统一上报:数据采集的时候可以采用AOP的方式,减少接入成本,上报的时候可以采取统一的上报减少流量和电量消耗,上传到后台之后再做详细的分析,得出所有业务流程的转化率,以及相应界面的异常率


2.报警策略



  • 阈值报警:比如某个业务失败的次数超过了阈值就报警通知

  • 趋势报警:对比前一天的异常情况,如果增加的趋势超过了一定的比例即便是未达阈值也要触发报警

  • 特定指标报警、直接上报:比如支付SDK调用失败,这种错误无需跟着统一的数据上报,出现立即上报


3.异常监控



  • Catch代码块:实际开发中我们为了避免程序崩溃,经常会写一些try{}catch(){}来捕获相关异常,但是这样操作完成之后,程序确实不崩溃了,相应的功能也是无法使用的,所以这些被Catch住的异常也要上报,有利于分析功能不可用的原因

  • 异常逻辑:比如我们需要对结果为true的调用方法进行处理,结果为false时不执行任务,但是我们也需要上报异常,统计一下出现这种情况的用户的占比情况,以便针对性的优化


这里简单的举个栗子,表明意思:

        try {
//业务处理
LogUtils.i("...");
}catch (Exception e){
//如果未加上统计,就无法知道具体是什么原因导致的功能不可用
ExceptionMonitor.monitor(Log.getStackTraceString(e));
}

boolean flag = true;
if (flag){
//正常,继续执行相关任务
}else {
//异常,不执行任务,这种情况产生的异常也应该进行上报
ExceptionMonitor.monitor("自定义业务失败标识");
}

4.单点追查



  • 需要针对性分析的特定问题:这些问题相对小众,很可能是和特定用户的操作习惯、账户体系相关,所以要尽可能获取多的数据重点分析

  • 全量日志回捞,专项分析:很多日志信息默认都是只记录不上传,比如用户全部的行为日志,这些日志只有在需要的时候才有用,平时没必要上传,没啥用还浪费流量,如果需要排查特定用户的详细信息,也可以通过服务端下发指令客户端接收指令后上传


5.兜底策略


当你通过监控了解到业务不正常之后,请问该如何修复?这里就要用到兜底策略了,就是到了最后一步各种措施都做了,用户还是出现了异常,这种情况仍然还是要有相关联的配置手段来达到高可用。对于业务上的异常除了热修复的手段之外,还可以通过建立配置中心,将功能开关关闭。



  • 配置中心,功能开关:实际项目中很多数据都是通过服务端动态下发配置的,将这些功能集合起来的处理平台就是配置中心。举个栗子:比如新版本上线了一个新功能,加了一个入口,上线之后发现功能不稳定,此时就可以通过服务端配置的方式将此功能开关关闭,这样即使用户无法使用新功能,但是至少不会发现业务的异常

  • 跳转分发中心:熟悉组件化开发的朋友都知道做组件化module的拆分必不可少的就是要有一个路由,它的作用就是跳转分发中心,所有的跳转都是通过路由来做,如果匹配到需要跳转到有Bug的功能界面时可以统一跳转到一个异常处理的页面


移动端容灾方案


移动端容灾必要性


说到容灾,首先来看一下需要防范的灾是什么?主要分为两部分:性能异常和业务异常,只要是对用户的实际体验产生了很大的影响,都是需要防范的App线上灾害。



  • 灾:性能、业务异常


传统的流程是如何处理线上出现的紧急问题的呢?传统的处理流程首先需要用户反馈出现的不正常情况,接着开发人员进行紧急的BUG修复,然后重新打包上传渠道进行更新,可见传统的流程比较繁琐,灵敏度较低,如果日活量较高,随着Bug在线上存活的时间延长对用户基数的影响是巨大的,势必是无法接受的



  • 传统流程:用户反馈、重新打包、渠道更新,不可接受


移动端容灾最佳实践


1.功能开关



  • 配置中心,服务端下发配置控制:首先对任何新上线的功能加上功能开关,可以通过配置中心的方式下发开关决定是否显示新功能的入口,出现异常情况可以随时关闭入口,这样可以保证上线的新功能处于可控状态

  • 针对场景,功能新加或代码改动:一是新增了功能,二是出现了代码改动,比如重构代码,最好保留之前的老方案,在新方案上线之后如果有问题,可以切回之前的老方案


这里简单的做个演示:

public class ConfigManager {

public static boolean mOpenClick = true; //默认值为true

}

mAdapter.setOnItemClickListener((view, position) -> {
//控制点击事件是否开启
if (ConfigManager.mOpenClick){ //mOpenClick的值从接口获取,由服务端控制
//处理具体业务
}
});

2.统跳中心


组件化之后的项目的页面跳转都是通过路由来做的,如果发现线上产生了异常,可以在路由跳转这一步拦截有Bug的跳转,重定向到备用方案,或者统一的错误处理中界面,更多的情况是为了对线上用户产生的影响降到最低,如果有Bug不能进行热修复,也没有合适的开关可用,会做一个临时的H5页面,让用户点击之后跳转到临时的H5页面,这样用户还是可以操作,只是在体验上稍微差一点,总归来说比不能用强的多



  • 界面切换通过路由,路由决定是否重定向

  • Native Bug不能热修则跳转到临时H5


3.动态化修复


目前为止,国内市场安卓的热修复方案已经比较成熟了,对于大型项目来说,一般都会支持热修复的能力,热修复技术就是用户不需要重新安装一个Apk,就可以实现比原有Apk有较大更新的能力,比如微信的Tinker和美团的Robust都是非常好的热修复实现方案。需要注意的是,热修复也只是一个功能,对于热修复也需要加上各种完善的统计,需要知道热修方案是否真正有效果,没有用造成更大的损失



  • 热修复能力,可监控、灰度、回滚、清除

  • 推拉结合、多场景调用保证到达率

  • Weex、RN增量更新


4.安全模式


安全模式侧重于移动端发生严重Crash时的自动恢复,做的好的安全模式往往会有几级不同的策略,比如App多次启动失败,那就重置整个App到安装的状态,避免因为一些脏数据导致的App持续闪退,同时如果有Bug并且非常严重到了最严重的等级,可以采用阻塞性热修来解决,即:必须等热修成功之后才可进入主页面。需要注意的是,安全模式不仅仅可以针对App Crash,也可以针对一些组件,比如网络请求多次失败后也可以进入安全模式,暂时拒绝用户的网络请求,避免给服务端造成的额外压力



  • 根据Crash信息自动恢复,多次启动失败重置App

  • 严重Bug可阻塞性热修复

  • 异常熔断:多次请求失败则主动拒绝


容灾方案总结:


image.png


这几种方式是由简单到复杂的递进,为了保障线上的稳定性,最好在应用中多加入几个稳定性保障方案。


稳定性长效治理


对于稳定性优化来说是一个细活,需要打持久战,不能一个版本优化了,后面又恶化了,因此需要在项目开发的整个周期内的不同阶段都加上相应的方案。


1.开发阶段


在开发阶段组内每个开发人员的编码实力都是不一样的,因此需要统一编码规范,然后结合一些手段增强人员的编码功底,尽可能的将问题消灭在编码阶段,尽可能的写出高质量的代码,同时要结合开发阶段的技术评审,以及每天的互相CodeReview机制,坚持几个月编码水平肯定会有明显的提升,开发阶段明显的问题应该就不会再有了,而且代码风格结构也会大体一致。同时开发阶段还需要做的事情就是架构优化,项目的架构应该根据项目的不同发展阶段来不断优化,这里说两点,第一能力收敛比如界面切换的能力用路由来实现,对网络请求要统一网络库统一使用方式,这样可以避免不正当的使用带来的Bug,第二统一容错,比如对于网络请求来说可以在网络请求回来的时候加上预先校验,判断回来的数据是否合法,如果不合法就不需要再把数据转给上层业务了



  • 统一编码规范、增强编码功底、技术评审、CodeReview机制

  • 架构优化:能力收敛、统一容错


2.测试阶段



  • 功能测试、自动化测试、回归测试、覆盖安装

  • 特殊场景、机型等边界测试

  • 云测平台:辅助测试,满足对特殊机型的测试需求


3.合码阶段


开发时肯定是在自己的分支进行开发,测试通过之后才会往主干分支合入,合入之前首先需要进行代码的编译检查和静态扫描发现可能存在的问题,经过校验之后也不能直接合入,应该将自己的分支首先合入到一个和主干分支一样的分支中进行预编译,编译通过之后最好加上主流程的回归测试



  • 编译检测,静态扫描

  • 预编译流程、主流程自动回归


4.发布阶段


到了发布阶段一般来说App都是经过了开发自测、QA测试、内部测试等测试环节,相对来说比较稳定了,但是需要注意的是,很多问题你不可能全部测出来,所以必须谨慎对待



  • 多轮灰度:灰度的量级要由小变多,争取以最小的代价暴露最多的问题

  • 分场景、维度全面覆盖


5.运维阶段


任何一个小问题在海量用户面前都会影响巨大,因此这个阶段必须要依靠APM的灵敏监控



  • APM灵敏监控

  • 回滚、降级策略

  • 热修复、容灾方案

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

SharedPreferences的一种极简优雅且安全的用法

针对Android平台键值对的持久化存储,虽然Jetpack出了新的DataStore,但实际项目中SharedPreferences还是有大量使用,本文结合以前的使用经验给出一种极简且优雅且安全的实践。(示例项目见 gitee.com/spectre1225...
继续阅读 »

针对Android平台键值对的持久化存储,虽然Jetpack出了新的DataStore,但实际项目中SharedPreferences还是有大量使用,本文结合以前的使用经验给出一种极简且优雅且安全的实践。(示例项目见 gitee.com/spectre1225…


1. SharedPreferences的使用与改进


SharedPreferences的基本读写代码如下:

preferences.edit().putInt("intKey", 1).apply();//写
preferences.getInt("intKey", 0);//读,0为默认值

代码中直接这么用的话,键会很不好管理,不清楚一个键值对到底有多少地方使用,当键发生改变需要修改的时候,也容易遗漏。于是就有了以下改进:

public interface XXXConfig{
String KEY_PROPERTY_AA = "key_aa";
String KEY_PROPERTY_BB = "key_bb";
String KEY_PROPERTY_CC = "key_cc";
//more key.......
}

//使用的地方
preferences.edit().putInt(XXXConfig.KEY_PROPERTY_AA, 1).apply();//写
preferences.getInt(XXXConfig.KEY_PROPERTY_AA, 0);//读,0为默认值

但这样写仍然有问题,就是缺少值类型的约束:一个key对应的value,可能有很多种类型。这种情况下,需要额外的注释或文档来记录每一个key对应的value的类型信息。于是,有人想到可以像JavaBean一样,采用getter和setter方法的形式:

public class XXXConfig {
private SharedPreferences preferences;
private String KEY_PROPERTY_AA = "key_aa";
private String KEY_PROPERTY_BB = "key_bb";

//中间省略初始化......

public int getPropertyAA() {
return preferences.getInt(KEY_PROPERTY_AA, 0);
}

public void setPropertyAA(int value) {
preferences.edit().putInt(KEY_PROPERTY_AA, value);
}

public int getPropertyBB() {
return preferences.getInt(KEY_PROPERTY_BB, 0);
}

public void setPropertyBB(int value) {
preferences.edit().putInt(KEY_PROPERTY_BB, value);
}
}

这种写法改进了类型安全,但每次新增就需要写一个属性和两个方法,过程比较繁琐。理想情况,我还是希望像写文档一样只需要写下面这样的信息:

属性1:类型 int
属性2:类型 String

然后使用的地方可以直接取值。因此,就有了下面介绍的新的封装方法:暂且称为NeoPreference。


2. NeoPreference简单使用


首先,我们需要需要创建一个inferface来继承Config接口,这个新的接口对应一个SharedPreferences,默认接口名即为SharedPreferences的名称,例如:

public interface DemoConfig extends Config {

}

这里的DemoConfig即为SharedPreferences的名称。有时候我们想要自己另外指定名称,则可以使用Config.Name注解:

@Config.Name("my_demo_config")
public interface DemoConfig extends Config {

}

这个时候SharedPreferences名称就是my_demo_config


然后我们就可以通过ConfigManager来获取DemoConfig的实例:

DemoConfig config = ConfigManager.getInstance().getConfig(DemoConfig.class);

到目前为止还没有什么新鲜的,接下来我们往里面添加新的配置项/属性:

@Config.Name("my_demo_config")
public interface DemoConfig extends Config {
Property<Integer> versionCode();
}

在上述基础上,只需要添加一行代码,就添加了新的键值对:key的值为versionCode,value的类型为Integer。然后我们的读写代码可以这么写:

DemoConfig config = ConfigManager.getInstance().getConfig(DemoConfig.class);
Integer versionCode = config.versionCode().get();//读
config.versionCode().set(versionCode + 1);//写

如果我们想要单独定key的名字,我们可以使用对应属性的注解:

@Config.Name("my_demo_config")
public interface DemoConfig extends Config {
@IntItem(key = "my_version_code")
Property<Integer> versionCode();
}

我们还可以指定值的范围和默认值:


@Config.Name("my_demo_config")
public interface DemoConfig extends Config {
@IntItem(key = "my_version_code", start = 1, to = 10000, defaultValue = 1)
Property<Integer> versionCode();
}

这样,在值不符合规范的时候会抛出异常:

DemoConfig config = ConfigManager.getInstance().getConfig(DemoConfig.class);
config.versionCode().set(-1);//throw exeception

3. NeoPreference API说明


这个工具的API除了ConfigManager类以外主要分两部分:Property类以及类型对应的注解。


3.1 ConfigManager接口说明


ConfigManager是单例实现,维护一个SharedPreferencesConfig的注册表,提供getConfigaddListener两个方法。


以下是getConfig方法签名:

public <P extends Config> P getConfig(Class<P> pClass);
public <P extends Config> P getConfig(Class<P> pClass, int mode);

参数pClass是继承Config类的接口class,可选参数mode对应SharedPreferences的mode。


addListener的方法监听指定preferenceName中内容的变化,签名如下:

public void addListener(String preferenceName, WeakReference<Listener> listenerRef);
public void addListener(LifecycleOwner lifecycleOwner, String preferenceName, Listener listener);

第一个方法接受一个Listener的弱引用,需要调用者自己持有监听器的引用,自己管理生命周期,否则可能被回收。第二个方法不采用弱引用参数,而是额外添加LifecycleOwner,这个监听器的声明周期采用LifecycleOwner对应的生命周期。


3.2 Property类接口说明


Property接口包括:

public final String getKey();//获取属性对应的key
public T get(T defValue); //获取属性值,defValue为默认值
public T get(); //获取属性值,采用缺省默认值
public void set(T value); //设置属性值
public Optional<T> opt(); //以Optional的形式返回属性值
public final void addListener(WeakReference<Listener<T>> listenerRef) //类似ConfigManager,不过只监听该属性的值变化
public final void addListener(LifecycleOwner owner, Listener<T> listener)//类似ConfigManager,不过只监听该属性的值变化

泛型参数支持LongIntegerFloatBooleanStringSet<String>SharedPreferences支持的几种类型,以及额外的Serializable


3.3 类型相关注解介绍


这些注解对应SharedPreferences支持的几种类型(其中description字段暂时不用)。

@interface StringItem {
String key() default "";
boolean supportEmpty() default true;
String[] valueOf() default {};
String defaultValue() default "";
String description() default "";
}

@interface BooleanItem {
String key() default "";
boolean defaultValue() default false;
String description() default "";
}

@interface IntItem {
String key() default "";
int defaultValue() default 0;
int start() default Integer.MIN_VALUE;
int to() default Integer.MAX_VALUE;
int[] valueOf() default {};
String description() default "";
}

@interface LongItem {
String key() default "";
long defaultValue() default 0;
long start() default Long.MIN_VALUE;
long to() default Long.MAX_VALUE;
long[] valueOf() default {};
String description() default "";
}

@interface FloatItem {
String key() default "";
float defaultValue() default 0;
float start() default -Float.MIN_VALUE;
float to() default Float.MAX_VALUE;
float[] valueOf() default {};
String description() default "";
}

@interface StringSetItem {
String key() default "";
String[] valueOf() default {};
String description() default "";
}

@interface SerializableItem {
String key() default "";
Class<?> type() default Object.class;
String description() default "";
}

4. 完整实现


见:gitee.com/spectre1225…


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

年轻人疯狂逃离的东方小巴黎|出路在哪里

不知不觉回到哈尔滨工作三年了,经历了两家公司,最近又燃起了换工作的心思。都说人挪活、树挪死,所以,我还想要活着,甚至获得更好一点,只能选择挪一挪,说句不好听的,这就有种矮子里拔大个的感觉。 概述 稍微熟悉哈尔滨的人都应该知道,哈尔滨是座没有互联网的城市。回...
继续阅读 »

不知不觉回到哈尔滨工作三年了,经历了两家公司,最近又燃起了换工作的心思。都说人挪活、树挪死,所以,我还想要活着,甚至获得更好一点,只能选择挪一挪,说句不好听的,这就有种矮子里拔大个的感觉。



概述


微信图片_20220607092204.jpg
稍微熟悉哈尔滨的人都应该知道,哈尔滨是座没有互联网的城市。回溯过去的十几甚至二十年,哈尔滨整体的软件行业基本处于停滞的状态。每年的各种互联网公司排行,独角兽公司排行甚至从没有出现过哈尔滨这三个字眼。被人们所熟知的BJTA等互联网巨头,他们甚至没有在哈尔滨这座城市扎根的丝毫想法。当然,我指的是研发团队,毕竟研发才是整个互联网公司的核心,他们支撑着公司各条业务线的运转。


你可能会说,没有大厂的城市多了去了。确实如此,但是哈尔滨作为最北方的省会城市,有着美丽的称号东方小巴黎,作为二线城市中排名靠前的城市,软件行业的发展确实是没有进步。最近几年,同为二线城市的成都武汉西安等城市的发展可谓是突飞猛进,一跃成为新一线城市,工作机会与工资水平大大提升。然而作为共和国长子中的省会城市,却在原地踏步,夸张点说,甚至是在退步!


城市表现


微信图片_20220607093006.jpg


随着2022年全国城市商业魅力排名的发布,我们清晰的看到,全国共有4个一线城市,15个新一线城市,而哈尔滨不出意外的出现在二线城市当中,且排名二线城市的第9位。更夸张的是,东北竟然无一城市入选新一线,去年杀入榜单的沈阳今年也铩羽而归了。


2020年时,全国千万级人口城市有18个,其中就有哈尔滨,到了2021年,哈尔滨居然跌出了这份榜单,全是常驻人口为988.5万,且全是人口增长率为-2%。那么你不禁要问,人口去哪了?为何会越来越少?


我愿意用恶劣这两个字来形容如今哈尔滨年轻人的工作环境。2022年哈尔滨工资全国排名第38位7160元,乍一看,这个水平不低了,确实,但是你真正了解就会发现,工资区间在3k~5k的人大有人在,他们同样是大学毕业,活力满满的年轻人,但是却没有机会获得一份高薪水的工作,甚至五险一金都是奢望。高收入人群虽然是少数,但是他们的工资却达到普通人的几倍,甚至十几倍。你会说,低收入的人还是没有能力,然而真的如此吗?


对比其他新一线城市,哈尔滨还是差了太多,我指的工作的机会。人确实有优秀的人,也有相对差一些的人,但是他们的层次也不至于相差十几倍这么夸张。但是在哈尔滨恰恰就是这样一种体现,年轻人甚至没有机会去展示自己的能力。


绝大多数的企业,高层都是在公司深耕多年的老人,他们的存在注定年轻人没有取代他们的机会,这并不是一个完全靠能力就能上位的城市。公司内部没有明确的晋升机制,没有规范的涨薪条例,更没有给你预留以后的位置。


软件行业的工资水平,远远低于行业的平均值。3~5年的程序员工资水平大约在7k-9k,5-10年的大约水平在11-15k左右。这种水平在一线甚至其他新一线城市已经是低薪了。毫不夸张的说,这还是不错的那些程序员能够拿到的薪水。


技术水平


微信图片_20220607092151.jpg
既然咱们是程序员,那就来聊聊哈尔滨的程序员水平如何。


2019年我刚回到哈尔滨,面试过几家公司,对于技术层面的面试基本都是皮毛,仅限于是否使用过哪些工具?用过哪些方法?如何写一个sql?


是否觉得现在面试还有如此不卷的公司?有,而且在哈尔滨90%都是这样的。虽说整体面试难度看起来不大,也很好通过,但是你觉得仅凭如此,能获得不菲的薪水?当然,基于这种环境,你就不可能获得高薪水。因为你的面试官根本也不懂更深层面的内容。如果一个饱受面试摧残的程序员来到哈尔滨面试,我相信你会发现一个新大陆。当你面试入职后,你甚至会不屑于与他们讨论技术原理性问题,因为他们根本不懂。


我作为一个后端开发,竟然遇到这样一个面试题:会用div画三角形吗?你能相信这是一个哈工大博士生,且工作10余年的java程序员能问出来的问题?


在目前的微服务发展形势下,当然也有不少公司开始使用这些技术,然而他们不是基于业务去拆分,而是基于功能,你能想象到的是,原本没有多少功能的系统,生生拆出十几甚至二十个服务,上线后每天的用户量,用一个手指就可以数的过来。当然,有部分程序员还是有理智的,不会认同你们leader的这种恶劣行为,但是你的反对不会有效果。


有一句话,我听得时候很不屑,现在想想,这就是现实!你学这些干啥?学了你也用不上。确实,很多东西学了你也用不上。说个简单的方面,哈尔滨企业的容器化,或者云化的程度微乎其微。且大部分功能在领导看来能用就可以。


长期在哈尔滨的程序员,经过这种环境熏陶,相当于浪费生命,真的好比坐井观天,他们如果出去其他新一线城市,相信必然会感叹程序员居然是这样的


新的机遇


微信图片_20220607092208.jpg
那么,哈尔滨软件行业到底有没有新的机遇?前一段时间相信哈尔滨人都知道的一个新闻,六大巨头宣布与哈尔滨达成深度合作,其中包含华为,百度,京东,腾旭,中兴,中科。初一听,这好像确实是对哈尔滨的好消息,这几个公司基本都是行业的领头羊,如果他们到来必定能够带来不少的就业岗位,提升一定的经济增长。


但是细细观察你会发现,最高兴的人无异于房地产行业,似乎踩到他们的尾巴一般,各种营销号,短视频不断的四处宣扬。但也不得不说,疫情导致哈尔滨的商业难以开展,所有的行业都停滞了,他们也像找到了救命稻草。


但是我却并不很看好这次机会,从本质来说,这六个企业能来,还是黑龙江政府给到了足够的吸引力,提供了强大的政策支持,不然我不认为会有企业发善心一样的去带动你你们的经济发展。


另外,本次六个企业的到来,至少我不认为是软件从业人员的机遇,研发团队无论从质量,还是数量,相比于其他城市,哈尔滨还是有很大的不足的。即使有,我觉得也只是外包团队,而不会是核心研发团队。


在从目前公布的几个公司的业务方先来看:华为在算力中心、人工智能、人才培养,百度在“一基地、三中心、七平台”,京东在“一店、一基地、一底座、一战略、三平台”,腾讯在数字转型、生物医药、创意设计,中兴在5G产业链,中科在咨询智库、数智制造业等方面提出落地诉求。在我看来特别像是老板在给我们画大饼。


如何找工作


微信图片_20220607093003.jpg
不管以后如何发展,现在还是要找一分稳定的工作。作为程序员,我还是总结一下几个重要的方面:


1、技术方面,即使处于哈尔滨这种躺平的环境,还是要不断的学习的。深度与广度同样重要,可以让你在工作当中得心应手,有更多的解决方案,也就意味着有更多被领导关注的机会。平时的沟通中,能更多的体现你的技术能力,将会使你在领导的印象里增加重量。同时在面试时,你技术能力的体现,还是有助于获得相对高一些的薪水。


2、工作环境。目前很多企业克扣员工较为严重,名义上双休,实际单休;加班、出差严重,且没有加班费,调休等机制;五险一金按照最低工资标准缴纳,设置有些工资没有一金;领导PUA严重,公司氛围沉闷,逢场作戏,拍领导马屁层出不穷。选择的时候慎重考虑。


3、工作距离。目前哈尔滨的地铁建设、覆盖程度不高,虽然公交线路较多,但是堵车较严重。大部分公司聚集在香坊开发区,松北等较远地带,没有地铁,但是部分公司带有通勤车,考虑目前的油价和你的薪水,建议慎重考虑出行方式。


4、薪水提升。在哈尔滨,想要薪水double,基本没有可能,涨个三五千就算不错的,如果有这种机会,且考虑我前面分析的三个方向,基本符合你的设想,别犹豫。虽然有可能是坑,但也是能拿到更多钱的坑。


总结


工作之余,闲聊而已。希望给在哈尔滨的同仁们,和即将回来的同仁们一点参考。虽然咱们哈尔滨环境不好,但是在吃这方面还是顶呱呱的。如果您有幸进入国企,或者拿到公务员的铁饭碗,作为养老城市还是不错的选择。也希望咱们哈尔滨能够越来越好吧,不要让年轻人无奈的离开。


微信图片_20220607092142.jpg

收起阅读 »

Springboot如何优雅的进行数据校验

基于 Spring Boot ,如何“优雅”的进行数据校验呢? 引入依赖 首先只需要给项目添加上 spring-boot-starter-web 依赖就够了,它的子依赖包含了我们所需要的东西。 注意: Spring Boot 2.3 1 之后,spring-...
继续阅读 »

基于 Spring Boot ,如何“优雅”的进行数据校验呢?


引入依赖


首先只需要给项目添加上 spring-boot-starter-web 依赖就够了,它的子依赖包含了我们所需要的东西。


image.png


注意:
Spring Boot 2.3 1 之后,spring-boot-starter-validation 已经不包括在了 spring-boot-starter-web 中,需要我们手动加上!


<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

验证 Controller 的输入


一定一定不要忘记在类上加上 @ Validated 注解了,这个参数可以告诉 Spring 去校验方法参数。


验证请求体


验证请求体即使验证被 @RequestBody 注解标记的方法参数。


PersonController


我们在需要验证的参数上加上了@Valid注解,如果验证失败,它将抛出MethodArgumentNotValidException。默认情况下,Spring 会将此异常转换为 HTTP Status 400(错误请求)。


@RestController
@RequestMapping("/api/person")
@Validated
public class PersonController {

@PostMapping
public ResponseEntity<PersonRequest> save(@RequestBody @Valid PersonRequest personRequest) {
return ResponseEntity.ok().body(personRequest);
}
}

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class PersonRequest {

@NotNull(message = "classId 不能为空")
private String classId;

@Size(max = 33)
@NotNull(message = "name 不能为空")
private String name;

@Pattern(regexp = "(^Man$|^Woman$|^UGM$)", message = "sex 值不在可选范围")
@NotNull(message = "sex 不能为空")
private String sex;

}

使用 Postman 验证


image.png


验证请求参数


验证请求参数(Path Variables 和 Request Parameters)即是验证被 @PathVariable 以及 @RequestParam 标记的方法参数。


PersonController


@RestController
@RequestMapping("/api/persons")
@Validated
public class PersonController {

@GetMapping("/{id}")
public ResponseEntity<Integer> getPersonByID(@Valid @PathVariable("id") @Max(value = 5, message = "超过 id 的范围了") Integer id) {
return ResponseEntity.ok().body(id);
}

@PutMapping
public ResponseEntity<String> getPersonByName(@Valid @RequestParam("name") @Size(max = 6, message = "超过 name 的范围了") String name) {
return ResponseEntity.ok().body(name);
}
}

使用 Postman 验证


image.png


image.png


嵌套校验


在一个校验A对象里另一个B对象里的参数


需要在B对象上加上@Valid注解


image.png


image.png


常用校验注解总结


JSR303 定义了 Bean Validation(校验)的标准 validation-api,并没有提供实现。Hibernate Validation是对这个规范/规范的实现 hibernate-validator,并且增加了 @Email、@Length、@Range 等注解。Spring Validation 底层依赖的就是Hibernate Validation。


JSR 提供的校验注解:



  • @Null 被注释的元素必须为 null

  • @NotNull 被注释的元素必须不为 null

  • @AssertTrue 被注释的元素必须为 true

  • @AssertFalse 被注释的元素必须为 false

  • @Min(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值

  • @Max(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值

  • @DecimalMin(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值

  • @DecimalMax(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值

  • @Size(max=, min=) 被注释的元素的大小必须在指定的范围内

  • @Digits (integer, fraction) 被注释的元素必须是一个数字,其值必须在可接受的范围内

  • @Past 被注释的元素必须是一个过去的日期

  • @Future 被注释的元素必须是一个将来的日期

  • @Pattern(regex=,flag=) 被注释的元素必须符合指定的正则表达式


Hibernate Validator 提供的校验注解



  • @NotBlank(message =) 验证字符串非 null,且长度必须大于 0

  • @Email 被注释的元素必须是电子邮箱地址

  • @Length(min=,max=) 被注释的字符串的大小必须在指定的范围内

  • @NotEmpty 被注释的字符串的必须非空

  • @Range(min=,max=,message=) 被注释的元素必须在合适的范围内


image.png


@JsonFormat与@DateTimeFormat注解的使用


@JsonFormat用于后端传给前端的时间格式转换,@DateTimeFormat用于前端传给后端的时间格式转换


JsonFormat


1、使用maven引入@JsonFormat所需要的jar包


        <dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.8.8</version>
</dependency>

<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.8.8</version>
</dependency>

<dependency>
<groupId>org.codehaus.jackson</groupId>
<artifactId>jackson-mapper-asl</artifactId>
<version>1.9.13</version>
</dependency>

2、在需要查询时间的数据库字段对应的实体类的属性上添加@JsonFormat


   @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateDate;

注: timezone:是时间设置为东八区,避免时间在转换中有误差,pattern:是时间转换格式


DataTimeFormat


1、添加依赖


       <dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>2.3</version>
</dependency>

2、我们在对应的接收前台数据的对象的属性上加@DateTimeFormat


@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime acquireDate;

3.这样我们就可以将前端获取的时间转换为一个符合自定义格式的时间格式存储到数据库了
全局异常统一处理:拦截并处理校验出错的返回数据
写一个全局异常处理类


@ControllerAdvice

public class GlobalExceptionHandler{
/**
* 处理参数校验异常
*/

@ExceptionHandler({MethodArgumentNotValidException.class})
@ResponseBody
public ErrorResponseData validateException(MethodArgumentNotValidException e) {
log.error("参数异常"+e.getBindingResult().getFieldError().getDefaultMessage(),e);
return new ErrorResponseData(10001,e.getBindingResult().getFieldError().getDefaultMessage());
}

/**
* 处理json转换异常(比如 @DateTimeFormat注解转换日期格式时)
*/

@ExceptionHandler({HttpMessageNotReadableException.class})
@ResponseBody
public ErrorResponseData jsonParseException(HttpMessageNotReadableException e) {
log.error("参数异常"+e.getLocalizedMessage(),e);
return new ErrorResponseData(10001,e.getCause().getMessage());
}

}

作者:Hypnosis
来源:juejin.cn/post/7241114001324228663
收起阅读 »

让弹窗更易于使用~

web
标题又名:简单弹窗、多弹窗、复杂弹窗怎么做代码和状态的解耦。 关键字:react / modal 问题 实际业务中,不乏弹窗组件中包含大量复杂的业务逻辑。如: function Order() { // 省略上百行方法状态    const [vis...
继续阅读 »

标题又名:简单弹窗、多弹窗、复杂弹窗怎么做代码和状态的解耦。


关键字:react / modal


问题


实际业务中,不乏弹窗组件中包含大量复杂的业务逻辑。如:


function Order() {
// 省略上百行方法状态
   const [visible,setVisible] = useState(false)
   const withModalState = useState<any>()
   
   return (
  <Modal>
      <Input/>
           <Input/>
<Select/>
<Checkbox/>
       </Modal>

  )
}

甚至还有多弹窗的情形。如:


function Order() {
// 省略上百行方法状态
   const [visible1,setVisible1] = useState(false)
   const [visible2,setVisible2] = useState(false)
   const [visible3,setVisible3] = useState(false)
   
   const withModalState1 = useState<any>()
   const withModalState2 = useState<any>()
   const withModalState3 = useState<any>()
   
   // 省略 不懂多少 handlexx
   return (
       <main>
        ...
           <Modal1></* 省略很多代码 */></Modal1>

           <Modal2></* 省略很多代码 */></Modal3>
           <Modal3></* 省略很多代码 */></Modal3>
       </main>
  )
}


非常的痛:



  1. 如果弹窗在处理完内部流程后,又还有返回值,这有又会有一大通的处理函数。

  2. 如果这些代码都写在一个文件中还是不太容易维护的。


而且随着业务的断增长,每次都这么写还是很烦的。


期望的使用方式


因此有没有一种更贴近业务实际的方案。


让弹窗只专注于自己的内部事务,同时又能将控制权交给调用方呢。


或者说我就是不喜欢 Modal 代码堆在一起……


如下:


// Modal1.tsx
export function Modal1(props) return <Modal></* ... */></Modal>

// 甚至可以将MOdal中的逻辑做的更聚合。通过2次封再导出给各方使用
export const function Check_XXX_Window() {/* */ return open(Modal1)}
export const function Check_XXX_By_Id_Window() { return open(Modal1)}
export const function Check_XXX_Of_Ohter_Window() { return open(Modal1)}

// Order.tsx
function Order() {

function xxHndanle {
const expect = Check_XXX_Window(Modal1,args) // 调用方法,传入参数,获得预期值 ,完成1个流程
}
return (
<main>
...
// 不实际挂载 Modal 组件
</main>

)
}

像这样分离两者的代码,这样处理起来肯定是清爽很多的。


实现思路


实现的思路都是一致的。



  1. 创建占位符组件,放到应用的某处。

  2. 将相关状态集管理,并与Modal做好关联。

  3. 暴露控制权。


因此基于不同的状态管理方案,实现是多种多样的。相信很多大佬都自己内部封装过不少了。


但是在此基础上,我还想要3点。



  1. API使用简单,但也允许一定配置。

  2. 返回值类型推导,除了帮我管理 close 和 open 还要让我用起来带提示的。

  3. 无入侵性,不依赖框架以外的东西。


ez-modal-react


emm......苦于市面上找不到这类产品(感觉同类并不多……讨论的也不多?)


于是我自己开源了一个。回应上文实现思路,可以点击看代码,几百行而已。



并不是突发奇想而来,其实相关特性早就在企业内部是使用多年了。我也是受前辈和社区启发。



基本特性



  1. 基于Promise封装

  2. 返回值类型推导

  3. 没有入侵性,体积小。


使用画面


import EasyModal, { InnerModalProps } from 'ez-modal-react';

+ interface IProps extends InnerModalProps<'fybe?'> /*传入返回值类型*/ {
+ age: number;
+ name: string;
+ }

export const InfoModal = EasyModal.create(
+ (props: Props) => {
return (
<Modal
title="Hello"
open={props.visible}
onOk={() => {
+ props.hide(); // warn 应有 1 个参数,但获得 0 个。 (property) hide: (result: "fybe?") => void ts(2554)
}}
onCancel={() => {
props.hide(null); //safe hide 接受 null 作为参数。它兼具 hide resolve 两种功能。
}}
>
<h1>{props.age}</h1>
</Modal>
);
});

+ // warn 类型 "{ name: string; }" 中缺少属性 "age",但类型 "ModalProps<Props, "fybe?">" 中需要该属性。
EasyModal.show(InfoModal, { name: 'foo' }).then((resolve) => {
console.log(resolve);
+ //输出 "fybe?"
});

也支持用 hook


import EasyModal, { useModal, InnerModalProps } from 'ez-modal-react';

interface IProps extends InnerModalProps<'苏振华'>/* 指定返回值类型 */ {
age: number;
name: string;
}

export const Info = EasyModal.create((props: Props) => {
const modal = useModal<Props>();

function handleOk(){
modal.hide(); // ts(2554) (property) hide: (result: "苏振华") => void ts(2554)
}

return <Modal open={modal.visible} onOk={handleOk} onCancel={modal.hide}></Modal>
});


EasyModal.show(Info,{age:18,}) // 缺少属性 "age"

还有一些特性如支持配置 hide 弹窗时的默认行为。(我认为大多数情况下可能用不上)



export const Info = EasyModal.create((props: Props) => {
const modal = useModal<Props>();

function handleOk(){
modal.hide();
+ modal.resolve('苏振华') // 需要手动抛出成功
+ modal.remove() // 需要手动注销组件。可用于反复打开弹窗,但是不希望状态被清除的场景。
}

return <Modal open={modal.visible} onOk={handleOk} onCancel={modal.hide}></Modal>

// Ohter.tsx
EasyModal.open(Info, {},
+ config:{
+ resolveOnHide:false, // 默认为 true
+ removeOnHide:false, // 默认为 true
+ }
);


当然以上是针对弹窗内有复杂业务场景的状况。


大部分场景都是调用方只在乎 open或close ,仅仅解耦代码这一项好处,就可以让代码变得清爽。


如常用的有展示类,设置类的弹窗,TS类型都用不上。


仓库


其他就不一一介绍了,主要的已经说完了,想了解更多,可以看看仓库。github


🎮 Codesandbox Demo


Codesandbox是一个线上集成环境,可以直接打开玩玩。点击 Demo Link 前往


初衷


让弹窗使用更轻松。



包名 ez 开头,是因为我DOTA在东南亚打多了,觉得该词特别贴切。



授之于鱼叉


GitHub仓库地址
ez-modal-react


觉得有用帮我点个星~~~ 感恩,有啥问题可以提出,看到会回复。如果有需要会持续维护该项目。


下列诸神,望您不吝赐教

作者:胖东蓝
来源:juejin.cn/post/7238917620849246263

收起阅读 »

为什么需要PNPM ?

web
PNPM是什么? 在日常工作里面,总是有同事让我将npm替换成pnpm,理由是这玩意速度更快,效率更高。那到底pnpm是什么呢?他为什么比npm/yarn有着更大的优势?首先,毫不疑问,pnpm是作为一个前端包管理器工具被提出的,作者的初衷是为了能够开发出一款...
继续阅读 »


PNPM是什么?


在日常工作里面,总是有同事让我将npm替换成pnpm,理由是这玩意速度更快,效率更高。那到底pnpm是什么呢?他为什么比npm/yarn有着更大的优势?
首先,毫不疑问,pnpm是作为一个前端包管理器工具被提出的,作者的初衷是为了能够开发出一款能够有效节省磁盘空间,提高安装速度的包管理工具。
其次,npm和yarn存在的诸多问题,也让开发者诟病已久,pnpm也是在这样的背景下被开发出来并广受欢迎的。
image.png
image.png
image.png


PNPM解决了什么样的问题?


在讨论pnpm解决了什么样的问题之前,我们可以先看看npm和yarn这些传统包管理工具到底存在什么样的问题。


NPM 2


在Npm2.x里面,当你观察node_modules时,你会发现对于不同包之间的依赖关系,npm2.x采用的是层层嵌套的方式去管理这些依赖包。
image.png
对于依赖关系来说,嵌套的管理方式虽然让不同包之间的依赖关系一目了然,但是却存在着诸多的问题。



  • 公共的依赖无法重复利用。不同的包里难免会存在相同的依赖,但是在npm2.x里面,这些公共依赖会被复制很多次,存在于不同的包的嵌套node_modules里,这样会导致下载速度的下降以及浪费了许多磁盘空间。

  • **嵌套关系过深时,路径名过长。**在window下,有很多程序无法处理超过260个字符的文件路径名,如果嵌套关系过深时,就有可能会超出这个限制,导致windows下存在无法解析的现象。



NPM 3 & YARN


针对上述Npm 2存在的两个典型的问题,Yarn和Npm3采用了扁平化的依赖管理方式,所有的依赖不再层层嵌套,而是全部都在同一层,这样就同时解决了重复依赖多次复制和路径名过长的问题了。
image.png
可以看到使用npm3安装的express使,大部分的包都不会有二层的node_modules的,当然,如果同时存在多个版本的包时,则还是会出现部分嵌套node_modules的情况。
另外,Yarn和Npm 3都采用了lock文件,借此来保证每次拉取同一个项目的依赖时,使用的是同一个版本,避免不同版本之间的差异性导致的项目Bug。
但是,问题又来了,难道使用了扁平化的依赖管理方式就是完美的吗,这种管理方式会不会产生新的问题?答案否定的,扁平化的管理方式会导致两个最主要的问题



  • 幽灵依赖

  • 磁盘空间问题没有完全解决


幽灵依赖


幽灵依赖是指你的项目明明没有在package.json文件里声明的依赖,但是在代码里面却可以使用到。导致这个问题的最主要的原因就是,依赖扁平化了,当你的项目寻找依赖的时候,可以找到所有node_modules里的最外层依赖。
显然,幽灵依赖是会带来隐患的,当你依赖的包A有一天不再需要包B了, 你对B的幽灵依赖就会导致错误,从而发生问题。


磁盘问题


当同一个项目依赖了某个包的多个版本时,Npm3和Yarn只会提升其中的一个,而其余版本的包还是不能避免复制多次的问题 。


PNPM是如何解决这些问题的?


在讨论Pnpm的机制之前,我们需要先学习下一些操作系统相关的知识。


链接


链接实际上是一种文件共享的方式。在Linux的文件系统中,除了文件名和文件内容,还有一个很重要的概念,就是inode,inode类似于C语言的指针,它指向了物理硬盘的一个区块,只有有文件指向这个区块,他就不会从硬盘中消失。而硬链接和软连接最大的区别,则是inode的与原文件的关系。


硬链接


一般来说,inode与文件名、文件数据是一对一的关系,但我们可以通过shell命令让多个文件名指向同一个inode,这种就是硬链接(hard link)。由于硬链接文件和源文件使用同一个inode,并指向同一块文件数据,除文件名之外的所有信息都是一样的。所以这两个文件是等价的,可以说是互为硬链接文件。修改任意一个文件,可以看到另外一个文件的内容也会同步变化。
也正是因为有多个相同的inode指向同一个区块,所以硬链接的源文件即使被删除,也不会对链接文件有任何影响。


软链接


软连接又称符号链接,与硬链接共用一个inode不同的是,软链接会创建一个新的inode,存放着源文件的绝对路径信息,并指向源文件。当用户访问软链接时,系统会自动将其替换为该软链接所指向的源文件的文件路径,然后访问源文件。
而PNPM则是利用了以上链接的机制,来解决Npm和Yarn存在的问题。
当你使用Pnpm安装依赖时,Pnpm会在全局的仓库里保存一份npm包的内容,然后再利用链接的方式,从全局仓库里链接到你项目里的虚拟仓库,也就是node_modules里的是.pnpm。


image.png


你所安装的依赖,都会单独存放在node_modules下,而依赖所依赖的npm包都会通过软链接的方式,链接.pnpm里的虚拟仓库里的包,而.pnpm里的包,则是通过硬链接从全局Store里链接过来的。
image.pngimage.png
通过软硬链接结合的方式,Pnpm可以很有效的解决了Npm和yarn遗留的问题:
1、抛弃了扁平化的管理方式,避免了幽灵依赖。
2、Npm包的存储方式都是放在全局,使用到的时候只会建立一个硬链接,硬链接和源文件共享同一份内存空间,不会造成重复依赖的空间浪费。另外,当依赖了同一个包的不同版本时,只对变更的文件进行更新,不需要重复下载没有变更的部分。
3、下载速度,当存在已经使用过的npm包时,只会建立链接,而不会重新下载,大大提升包安装的速度。


PNPM天生支持Monorepo?


monorepo 是在一个项目中管理多个包的项目组织形式。
它能解决很多问题:工程化配置重复、link 麻烦、执行命令麻烦、版本更新麻烦等。
而利用Pnpm的workSpace配合changesets,就可以很简单的完成一个Monorepo项目的搭建,所以说Pnpm天生就是Mo

作者:Gamble_
来源:juejin.cn/post/7240662396020916282
norepo的利器。

收起阅读 »

前端自动部署:从手动到自动的进化

web
在现代 Web 开发中,前端自动化已经成为了必不可少的一部分。随着项目规模的增加和开发人员的增多,手动部署已经无法满足需求,因为手动部署容易出错,而且需要大量的时间和精力。因此,自动化部署已经成为了前端开发的趋势。在本文中,我们将介绍前端自动化部署的基本原理和...
继续阅读 »

在现代 Web 开发中,前端自动化已经成为了必不可少的一部分。随着项目规模的增加和开发人员的增多,手动部署已经无法满足需求,因为手动部署容易出错,而且需要大量的时间和精力。因此,自动化部署已经成为了前端开发的趋势。在本文中,我们将介绍前端自动化部署的基本原理和实现方式,并提供一些示例代码来说明。


前端自动化部署的基本原理


前端自动化部署的基本原理是将人工操作转换为自动化脚本。这些脚本可以执行一系列操作,例如构建、测试和部署应用程序。自动化部署可以帮助开发人员节省时间和精力,并提高应用程序的质量和可靠性。


自动化部署通常包括以下步骤:



  1. 构建应用程序:使用构建工具(例如 webpack、gulp 或 grunt)构建应用程序的代码和资源文件。

  2. 运行测试:使用测试工具(例如 Jest、Mocha 或 Karma)运行应用程序的单元测试、集成测试和端到端测试。

  3. 部署应用程序:使用部署工具(例如 Jenkins、Travis CI 或 CircleCI)将应用程序部署到生产服务器或云平台上。


实现前端自动化部署的方式


实现前端自动化部署的方式有很多种,以下是其中的一些:


1. 使用自动化部署工具


自动化部署工具可以帮助我们自动化构建、测试和部署应用程序。这些工具通常具有以下功能:



  • 与版本控制系统集成,例如 Git 或 SVN。

  • 与构建工具集成,例如 webpack、gulp 或 grunt。

  • 与测试工具集成,例如 Jest、Mocha 或 Karma。

  • 与部署平台集成,例如 AWS、Azure 或 Heroku。


自动化部署工具可以帮助我们节省时间和精力,并提高应用程序的质量和可靠性。


以下是一个使用 Jenkins 自动化部署前端应用程序的示例:


pipeline {
agent any
stages {
stage('Build') {
steps {
sh 'npm install'
sh 'npm run build'
}
}
stage('Test') {
steps {
sh 'npm run test'
}
}
stage('Deploy') {
steps {
sh 'npm run deploy'
}
}
}
}

在这个示例中,我们使用 Jenkins 构建、测试和部署前端应用程序。我们将应用程序的代码和资源文件打包到一个 Docker 容器中,并将容器部署到生产服务器上。


2. 使用自动化构建工具


自动化构建工具可以帮助我们自动化构建应用程序的代码和资源文件。这些工具通常具有以下功能:



  • 支持多种语言和框架,例如 JavaScript、React 和 Vue。

  • 支持多种模块化方案,例如 CommonJS 和 ES6 模块。

  • 支持多种打包方式,例如单文件和多文件打包。

  • 支持多种优化方式,例如代码压缩和文件合并。


以下是一个使用 webpack 自动化构建前端应用程序的示例:


const path = require('path');

module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{
test: /.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
},
},
{
test: /.css$/,
use: ['style-loader', 'css-loader'],
},
],
},
};

在这个示例中,我们使用 webpack 构建前端应用程序的代码和资源文件。我们将应用程序的入口文件设置为 src/index.js,将输出文件设置为 dist/bundle.js,并使用 Babel 转换 JavaScript 代码和使用 CSS Loader 加载 CSS 文件。


3. 使用自动化测试工具


自动化测试工具可以帮助我们自动化运行应用程序的单元测试、集成测试和端到端测试。这些工具通常具有以下功能:



  • 支持多种测试框架,例如 Jest、Mocha 和 Jasmine。

  • 支持多种测试类型,例如单元测试、集成测试和端到端测试。

  • 支持多种测试覆盖率工具,例如 Istanbul 和 nyc。

  • 支持多种测试报告工具,例如 JUnit 和 HTML。


以下是一个使用 Jest 自动化测试前端应用程序的示例:


test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3);
});

在这个示例中,我们使用 Jest 运行一个简单的单元测试。我们将 sum 函数的输入设置为 1 和 2,并将期望输出设置为 3。如果测试通过,Jest 将输出 PASS,否则将输出 FAIL


结论


前端自动化部署已经成为了现代 Web 开发的趋势。通过使用自动化部署工具、自动化构建工具和自动化测试工具,我们可以节省时间和精力,并提高应用程序的质量和可靠性。在未来,前端自动化部署将会变得更加普遍和重要,因此我们需要不断学习和掌

作者:_大脑斧
来源:juejin.cn/post/7240636320761921594
握相关的技术和工具。

收起阅读 »

程序员增强自控力的方法

作为一名程序员,我们经常会面临工作压力和时间紧迫的情况,因此有一个好的自控力对于我们的工作和生活都是至关重要的。以下是一些可以帮助程序员增强自控力的方法: 1. 设定明确的目标和计划 制定明确的目标和计划可以帮助我们更好地管理时间和精力。我们可以使用日程表、任...
继续阅读 »

作为一名程序员,我们经常会面临工作压力和时间紧迫的情况,因此有一个好的自控力对于我们的工作和生活都是至关重要的。以下是一些可以帮助程序员增强自控力的方法:


1. 设定明确的目标和计划


制定明确的目标和计划可以帮助我们更好地管理时间和精力。我们可以使用日程表、任务清单、时间追踪工具等,来帮助我们控制时间并更有效地完成任务。


2. 掌控情绪


作为程序员,我们需要面对很多挑战和压力,容易受到情绪的影响。因此,掌握情绪是一个非常重要的技能。可以通过冥想、呼吸练习、运动等方法,来帮助我们保持冷静、积极和乐观的心态。


3. 管理焦虑和压力


焦虑和压力是我们常常遇到的问题之一,所以我们需要学会如何管理它们。我们可以使用放松技巧、适度锻炼、交流沟通等方法,来减轻我们的焦虑和压力。


4. 培养自律习惯


自律是一个非常重要的品质。我们可以通过设定目标、建立规律和强化自我控制等方式,来培养自律习惯。


5. 自我反思和反馈


经常进行自我反思和反馈可以帮助我们更好地了解自己的优缺点和行为模式。我们可以使用反馈工具或与他人交流,来帮助我们成长和改进。


6. 持续学习和自我发展


程序员需要不断学习和自我发展,以保持竞争力和提升自己的技能。通过阅读书籍、参加培训、探究新技术等方式,可以帮助我们持续成长,增强自我控制力。


结论


自控力是我们工作和生活中重要的的品质之一,可以帮助我们更好地应对各种挑战和压力。通过设定目标、掌控情绪、管理焦虑和压力、培养自律习惯、自我反思和反馈、持续学习和自我发展等方法,我们可以帮助自己增强自我控制

作者:郝学胜
来源:juejin.cn/post/7241015051661312061
能力并提高工作效率。

收起阅读 »

哎,今天在公司的最后一天了

“啊!” 我今天居然被通知裁员了!!! 虽然之前一直盛传我们这边要裁员,但是我想着,应该一时半会轮不到我这边,我感觉我们项目还是相对挣点钱的。但是呢,心里也是挺忐忑的。 今天,我正在那全心全意的敲代码,敲的正起劲呢。我突然看到我们项目经理被叫走了。啊?我一想啥...
继续阅读 »

“啊!” 我今天居然被通知裁员了!!!


虽然之前一直盛传我们这边要裁员,但是我想着,应该一时半会轮不到我这边,我感觉我们项目还是相对挣点钱的。但是呢,心里也是挺忐忑的。


今天,我正在那全心全意的敲代码,敲的正起劲呢。我突然看到我们项目经理被叫走了。啊?我一想啥情况?要开会的话怎么只叫我们项目经理,怎么不叫我呀。


难道是要裁员?!难道真的是要裁员?!然后我就看着我们项目经理和我们的上级领导他们一起坐在小屋里聊了半天,啊,我的小心脏呀,我心里就祈祷呀:“千万不要裁员啊!千万不要裁员呀!千万不要裁员呀!!!”


等我们项目经理出来之后,他走到了我这边,然后 “啪” 拍了一下我的肩膀,然后 “哎” 叹了口气。他说:“我们这个项目要被裁掉了。”


我说心里特别失落,但还故作镇定的说:“为啥?我们的项目不是还挣钱呢吗?”


项目经理说:“哎,挣钱也不行。我们现在不需要这么多人了。我们现在的项目,没有一个大的发展了啊!你先等一会吧,等一会他们还得找你谈。”。他走的时候,又拍了拍我肩膀。


哎,当时我就感觉我心里呀那种失落感呀,没法说的那种感觉。果然,没一会,我们经理就来了。他过来之后跟我说:“走,请你到小屋里喝点水。”


我苦笑着跟他说:“经理,我能不去吗?我现在不渴。”


然后我们经理说:“哎,不行呀,我都已经给你倒好了,走吧走吧,歇会去。”


然后我就默默的跟他去了。进去之后呢,我们俩都坐下了。经理跟我笑着说:“恭喜你呀,脱离苦海了。”


哎,我当时心情比较低落,我说:“是呀,脱离苦海了,但又上了刀山了呀。哈哈哈。。。”


然后他说:“哎,确实是,没办法,现在,哎,公司也不容易。现在有一些项目确实得收缩。”


我说:“哎,这也没啥,这都很正常。咱公司还算不错的,最起码还让过了个节。很多公司什么都不管,就这样让走了呀。哎!”


后面我们就谈了一些所谓的那种离职补偿啊,等等一些列的东西**。**


反正当时感觉着吧,就是,嗯,聊完之后呢就准备出去嘛。然后走路的时候呀,就感觉这个腿上啊就跟绑了铅块一样。


当时我感觉,哎,裁员这玩意怎么说呢,都没法回去和亲人说呀,弄的一下午这个心里慌慌的。怎么跟家人交代呢?人至中年居然混成这样,哎!!!


郑重声明,本文不是为了制造焦虑,发文的原因有两个:



  1. 我今年 33 了,一方面给大家展现下一个普通程序员 35 岁后能咋样?是送外卖还是跑滴滴?难道真的就找不到工作了吗?

  2. 感觉我并没有走好自己的人生路,把自己的经历写出来发到网上,让年轻人以我为鉴,能更好的走好自己的人
    作者:程序员黑黑
    来源:juejin.cn/post/7110887208953282590
    生路。

收起阅读 »

我的前端开发学习之路,从懵懂到自信

前端开发,刚开始学的时候,我感觉自己就像个孩子,一脸懵懂。当时,我非常迷茫,不知道该从何开始学习。但是,我并没有放弃,因为我对前端开发充满了热情和兴趣。 刚开始学习时,我觉得 HTML、CSS 和 JavaScript 这些基础知识就已经够难了,但是当我开始接...
继续阅读 »

前端开发,刚开始学的时候,我感觉自己就像个孩子,一脸懵懂。当时,我非常迷茫,不知道该从何开始学习。但是,我并没有放弃,因为我对前端开发充满了热情和兴趣。


刚开始学习时,我觉得 HTML、CSS 和 JavaScript 这些基础知识就已经够难了,但是当我开始接触一些流行的框架和库时,我才发现自己真正的水平有多菜。当时,我就像一只踩在滑板上的小猪,不断地摔倒,但是我并没有放弃,我一直在努力地学习和实践。


在学习的过程中,我遇到了许多困难和挑战,但是也有很多有趣的体验和经历。有一次,我在编写一个简单的网页时,花了一整天的时间,结果发现自己的代码有一个很小的错误,导致整个网页无法正常显示。当时我感觉自己就像一个猴子在敲打键盘,非常无助和懊恼。但是,通过不断地调试和修改,我最终找到了错误,并且成功地将网页显示出来。当时,我感觉自己就像一只成功攀爬上树的小猴子,非常自豪和兴奋。


除了遇到困难和挑战,我在学习前端开发过程中也经历了许多有趣的体验。有一次,我在编写一个小型的应用程序时,发现我的代码出现了一个非常有趣的小 bug。当用户在页面上进行操作时,页面上的一些元素会突然出现在屏幕的右侧,然后又突然消失不见。当时我还担心这个 bug 会影响用户的正常使用,但是后来发现这个 bug 其实很有趣,而且还能给用户带来一些意外的乐趣。于是我就把这个 bug 留了下来,并且在用户操作时添加了一些特效,让这个小 bug 变成了一个有趣的亮点。


12.jpg
总结一波:
第一点,学习前端开发需要有耐心。前端开发不是一个短时间内可以学会的技能,它需要大量的时间和精力。尤其是在学习的早期,你可能会觉得有些技术和概念非常难以理解。但是,只要你有耐心,坚持不懈地学习,最终你一定会掌握这些技能。


第二点,建立一个良好的学习计划非常重要。前端开发有很多不同的技术和概念,你需要有一个清晰的学习计划来帮助你系统地学习和掌握这些知识。首先,你需要了解 HTML、CSS 和 JavaScript 这三大基本技术。其次,你可以学习一些流行的框架和库,如 React、Vue、jQuery 等,这些技术可以帮助你更快捷地构建网站和应用程序。


第三点,实践是学习前端开发的关键。你可以通过练习编写代码来更好地理解前端开发的技术和概念。在学习的过程中,你可以尝试编写一些小项目,比如一个简单的网页或者一个小型的应用程序。通过实践,你可以更深入地了解前端开发的各个方面,并且提高你的编程技能。


第四点,不要害怕向他人寻求帮助。前端开发是一个非常开放和社交的领域,你可以通过参加社区活动、参与在线讨论、向他人寻求帮助等方式来更好地学习和成长。有时候,你可能会遇到一些困难,或者对某些概念不是很理解,这时候向他人寻求帮助是非常重要的。你可以参加一些线上或线下的前端开发社区,与其他开发者交流经验和技巧,也可以在 GitHub 等平台上查看其他人的代码,从中学习和借鉴。


第五点,不断更新自己的知识和技能。前端开发是一个不断发展和变化的领域,新技术和新概念层出不穷。因此,你需要不断地更新自己的知识和技能,跟上前端开发的最新动态。你可以通过阅读博客、参加培训课程、观看技术视频等方式来学习新的技术和概念。


总之,学习前端开发需要有耐心、建立一个良好的学习计划、实践、寻求帮助和不断更新自己的知识和技能。这些都是非常重要的,也是我在学习前端开发过程中得到的宝贵经验。通过不断地学习和实践,相信你我可以成为一名优

作者:梦想橡皮擦丶
来源:juejin.cn/post/7239363820875513916
秀的前端开发工程师。

收起阅读 »

基于环信Web Vue3 Demo使用electron快速打包生成桌面端应用

前言一直以来都有听说利用yarn install安装项目相关 npm 依赖。在此项目目录下打开终端请敲下wait-on以及wait-on 是一个 Node.js 包,它可以用于等待多个指定的资源(如 HTTP 资源、TCP 端口或文件)变得可用。它通...
继续阅读 »

前言

一直以来都有听说利用electron可以非常便捷的将网页应用快速打包生成为桌面级应用,并且可以利用 electron 提供的 API 调用原生桌面 API 一些高级功能,于是这次借着论证环信 Web 端 SDK 是否可以在 electron 生成的桌面端正常稳定使用,我决定把官方新推出的 webim-vue3-demo,打包到桌面端,并记录一下这次验证的过程以及所遇到的问题以及解决方式。

前置技能

  • 拥有良好的情绪自我管理,能够在遇到棘手问题时不一拳给到键盘。
  • 拥有较为熟练的水群能力,能够在遇到问题时,主动向技术群内参差不齐的群友们抛出自己的问题。
  • 【重要】拥有较为熟练的搜索引擎使用能力。
  • 能够看到这篇文章,那说明以上能力你已完全具备。

测试流程记录

第一步、准备工作

  • 克隆 vue3 Demo 项目到本地 环信 vue3-demo 源码地址
  • 在编辑器内打开此项目并执行yarn install安装项目相关 npm 依赖。
  • 在此项目目录下打开终端请敲下yarn add electron,从而在该项目中安装 electron。
  • 安装一些依赖工具wait-on以及cross-env

wait-on 是一个 Node.js 包,它可以用于等待多个指定的资源(如 HTTP 资源、TCP 端口或文件)变得可用。它通常用于等待应用程序的依赖项准备好后再启动应用程序。例如,您可以使用 wait-on 等待数据库连接、消息队列和其他服务就绪后再启动您的应用程序。这样可以确保您的应用程序在尝试使用这些资源之前不会崩溃。

cross-env 是一个 npm 包,它的作用是在不同平台上设置环境变量。在不同操作系统中,设置环境变量的方式是不同的。例如,在 Windows 中使用命令 set NODE_ENV=production 设置环境变量,而在 Unix/Linux/Mac 上则需要使用 export NODE_ENV=production 命令。

此时可能会进入到漫长的等待阶段,第一、这个包本身就比较大,第二、相信大家都懂由于网络原因导致,并且有可能进行会经历几次TIMOUT安装失败。此时就需要心平气和,且有耐心的进行改变镜像地址科学进行上网WIFI切换为移动流量多去重试几次,相信道友你总会成功过的。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2xyqXdwF-1685947556715)(https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/69f46811946344499047553af9abccd9~tplv-k3u1fbpfcp-watermark.image?)]
有如下输出则应该为安装成功。

第二步、项目目录增加 electron 文件

在项目增加 electron 文件时我们需要扩展一部分知识从而了解为什么创建创建这个目录,并在该目录下增加main.js文件的作用。当然如果觉得不需要可以直接略过。

主进程与渲染进程的概念

在 Electron 中,主进程和渲染进程是两个不同的概念。主进程是 Electron 应用程序的核心,它运行在一个 Node.js 实例中,并管理应用程序的生命周期、窗口创建和销毁、与底层操作系统进行交互等。主进程还可以通过 IPC(进程间通信)机制与渲染进程进行通信。
渲染进程则是应用程序的 UI 界面所在的进程。每个 Electron 窗口都有其自己的渲染进程。渲染进程是一个 Chromium 渲染引擎实例,它运行在一个仅包含 Web API 的环境中。渲染进程负责渲染 HTML、CSS 和 JavaScript,并处理来自用户的输入事件,同时通过 IPC 机制与主进程进行通信。
由于渲染进程只能访问 Web API 而不能直接访问 Node.js API,因此如果需要在渲染进程中使用 Node.js API,就需要通过 IPC 机制向主进程发出请求,由主进程代为执行并将结果返回给渲染进程。

主进程与渲染进程分别应该写在哪?

在 Electron 应用程序中,主进程通常写在名为 main.js 或者 index.js 的 JavaScript 文件中,这个文件是应用程序的入口点。而渲染进程则通常写在 HTML 文件和其引入的 JavaScript 文件中。在一个 Electron 窗口中,可以通过调用 webContents 对象的 loadURL 方法来加载一个 HTML 文件,其中包含了渲染进程所需的代码和资源。该 HTML 文件中的 JavaScript 代码将运行在对应的渲染进程中,可以通过 Electron 提供的一些 API 和 Web API 来进行与用户界面相关的操作
需要注意的是,在 Electron 中,由于主进程和渲染进程是不同的 Node.js 实例,因此它们之间并不能直接共享变量或者调用函数。如果想要实现主进程和渲染进程之间的通信,必须使用 Electron 提供的 IPC 机制,通过发送消息的方式来进行进程间通信。

有些 electron 文件目录下 preload.js 的作用

在 Electron 中,preload.js 文件是一个可选的 JavaScript 文件,用于在渲染进程创建之前加载一些额外的脚本或者模块,从而扩展渲染进程的能力。preload.js 文件通常存放在与主进程代码相同的目录下。

preload.js 的实际运用主要有以下几个方面:

  1. 托管 Node.js API:preload.js 中可以引入 Node.js 模块,并将其暴露到 window 对象中,从而使得在渲染进程中也能够使用 Node.js API,避免了直接在渲染进程中调用 Node.js API 带来的安全风险。
  2. 扩展 Web API:preload.js 中还可以定义一些自定义的函数或者对象,然后将它们注入到 window 对象中,这样在渲染进程中就可以直接使用它们了,而无需再进行额外的导入操作。
  3. 进行一些初始化操作:preload.js 文件中的代码会在每个渲染进程的上下文中都运行一遍,在这里可以进行一些初始化操作,比如为页面添加一些必要的 DOM 元素、为页面注册事件处理程序等。

需要注意的是,preload.js 文件中的代码运行在渲染进程的上下文中,因此如果 preload.js 中包含一些恶意代码,那么它很可能会危及整个渲染进程的安全性。因此,在编写 preload.js 文件时,一定要格外小心,并且仅引入那些你信任的模块和对象。

1、 添加 electron 文件

  • 此时项目目录

2、 electron 下新建main.js示例代码如下:

const { app, BrowserWindow } = require('electron');
const path = require('path');
const NODE_ENV = process.env.NODE_ENV;
app.commandLine.appendSwitch('allow-file-access-from-files');
function createWindow() {
// Create the browser window.
const mainWindow = new BrowserWindow({
width: 980,
height: 680,
fullscreen: true,
skipTaskbar: true,
webPreferences: {
nodeIntegration: true,
preload: path.join(__dirname, 'preload.js'),
},
});

if (NODE_ENV === 'development') {
mainWindow.loadURL('http://localhost:9001/');
mainWindow.webContents.openDevTools();
} else {
mainWindow.loadURL(`file://${path.join(__dirname, '../dist/index.html')}`);
}
}

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.

app.whenReady().then(() => {
createWindow();
});

// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') app.quit();
});

3、 electron 下新建preload.js,示例代码如下:

此文件为可选文件

//允许vue项目使用 ipcRenderer 接口, 演示项目中没有使用此功能
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('ipcRender', ipcRenderer);

4、修改package.json,当前示例代码如下:

  • 修改"main"配置,将其指向为"main": "electron/main.js"
  • 增加一个针对 electron 启动的"scripts""electron:dev": "wait-on tcp:3000 && cross-env NODE_ENV=development electron ./"

当前项目配置如下所示

{
"name": "webim-vue3-demo",
"version": "0.1.0",
"private": true,
"main": "electron/main.js",
"scripts": {
"dev": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"electron:dev": "wait-on tcp:9001 && cross-env NODE_ENV=development electron ./"
},
"dependencies": {
"@vueuse/core": "^8.4.2",
"agora-rtc-sdk-ng": "^4.14.0",
"axios": "^0.27.2",
"benz-amr-recorder": "^1.1.3",
"core-js": "^3.8.3",
"easemob-websdk": "^4.1.6",
"element-plus": "^2.2.5",
"nprogress": "^0.2.0",
"pinyin-pro": "^3.10.2",
"vue": "^3.2.13",
"vue-router": "^4.0.3",
"vuex": "^4.0.0"
},
"devDependencies": {
"@babel/core": "^7.12.16",
"@babel/eslint-parser": "^7.12.16",
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-eslint": "~5.0.0",
"@vue/cli-plugin-router": "~5.0.0",
"@vue/cli-plugin-vuex": "~5.0.0",
"@vue/cli-service": "~5.0.0",
"cross-env": "^7.0.3",
"electron": "^24.3.1",
"eslint": "^7.32.0",
"eslint-plugin-vue": "^8.0.3",
"sass": "^1.51.0",
"sass-loader": "^12.6.0",
"wait-on": "^7.0.1"
}
}

第三步、本地启动起来验证一下

  1. 启动运行原 vue 项目

这里启动项目至端口号 9001,跟上面 electron/main.jsmainWindow.loadURL(' http://localhost:9001/')是可以对应上的,也就是 electron 运行起来将会加载此服务地址。

yarn run dev
  1. 新开一个终端执行,输入下方命令启动 electron

执行下面命令

yarn run electron:dev

可以看到自动开启了一个 electron 页面



并且经过测试验证登录没有什么问题。

第四步、尝试打包并验证打包出来的安装包是否可用。

1、安装electron-builder

该工具为 electron 打包工具库

终端执行下面命令安装 electron-builder

yarn add electron-builder --dev

2、package.json 配置打包脚本命令以及设置打包个性化配置项

参考配置如下

具体配置项作用请参考官网文档,下面有些配置也是 CV 大发过来的,没有具体深入研究。

{
"name": "webim-vue3-demo",
"version": "0.1.0",
"private": true,
"main": "electron/main.js",
"scripts": {
"dev": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"electron:dev": "wait-on tcp:9001 && cross-env NODE_ENV=development electron ./",
"electron:build": "rimraf dist && vue-cli-service build && electron-builder",
"electron:build2": "electron-builder"
},
"dependencies": {
"@vueuse/core": "^8.4.2",
"agora-rtc-sdk-ng": "^4.14.0",
"axios": "^0.27.2",
"benz-amr-recorder": "^1.1.3",
"core-js": "^3.8.3",
"easemob-websdk": "^4.1.6",
"element-plus": "^2.2.5",
"nprogress": "^0.2.0",
"pinyin-pro": "^3.10.2",
"vue": "^3.2.13",
"vue-router": "^4.0.3",
"vuex": "^4.0.0"
},
"devDependencies": {
"@babel/core": "^7.12.16",
"@babel/eslint-parser": "^7.12.16",
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-eslint": "~5.0.0",
"@vue/cli-plugin-router": "~5.0.0",
"@vue/cli-plugin-vuex": "~5.0.0",
"@vue/cli-service": "~5.0.0",
"cross-env": "^7.0.3",
"electron": "^24.3.1",
"electron-builder": "^23.6.0",
"eslint": "^7.32.0",
"eslint-plugin-vue": "^8.0.3",
"sass": "^1.51.0",
"sass-loader": "^12.6.0",
"wait-on": "^7.0.1"
},
"build": {
"productName": "webim-electron",
"appId": "com.lvais",
"copyright": "2023@easemob",
"directories": {
"output": "output"
},
"extraResources": [
{
"from": "./src/assets",
"to": "./assets"
}
],
"files": ["dist/**/*", "electron/**/*"],
"mac": {
"artifactName": "${productName}_${version}.${ext}",
"target": ["dmg"]
},
"win": {
"target": [
{
"target": "nsis",
"arch": ["x64"]
}
],
"artifactName": "${productName}_${version}.${ext}"
},
"nsis": {
"oneClick": false,
"allowElevation": true,
"allowToChangeInstallationDirectory": true,
"createDesktopShortcut": true
},
"linux": {}
}
}

3、开始 build

  • 先这样

build 原始 vue 项目

yarn run build
  • 再那样

build electron 项目

yarn run electron:build

可能会进入漫长的等待,但是不要慌,可能与网络关系比较大,需要耐心等待。


打包成功之后可以看到有一个 output 文件夹的生成,打开之后可以选择双击打开软件验证看下是否可以正常开启应用

正常开启页面的话,证明没有问题,如果遇到了问题,下方会有一些我遇到的问题,可以作为参考。

令人痛苦的问题汇总

问题一、打包后页面空白,并且出现类似(Failed to load resource: net::ERR_FILE_NOT_FOUND)报错

问题简述:发现只有在打包之后的 electron 应用,启动后存在页面空白,dev 情况下正常。

解决手段之一:

经排查,更改vue.config.jspublicPath的配置为’./’

const { defineConfig } = require('@vue/cli-service');
module.exports = defineConfig({
transpileDependencies: true,
lintOnSave: false,
devServer: {
host: 'localhost',
port: 9001,
// https:true
},
publicPath: './',
chainWebpack: (config) => {
//最小化代码
config.optimization.minimize(true);
//分割代码
config.optimization.splitChunks({
chunks: 'all',
});
},
});

原因打包后的应用 electron 会从相对路径开始找资源,所以经过此配置可以所有资源则开始从相对路径寻找。

    默认情况下,Vue CLI 会假设你的应用是被部署在一个域名的根路径上,例如 `https://www.my-app.com/`。如果应用被部署在一个子路径上,你就需要用这个选项指定这个子路径。例如,如果你的应用被部署在 `https://www.my-app.com/my-app/`,则设置 `publicPath``/my-app/`

这个值也可以被设置为空字符串 (`''`) 或是相对路径 (`'./'`),这样所有的资源都会被链接为相对路径,这样打出来的包可以被部署在任意路径,也可以用在类似 Cordova hybrid 应用的文件系统中。

解决手段之二:

经过一顿操作之后发现仍然还是空白,并且打开控制台看到页面可以正常加载资源文件,但是 index.html 返回此类错误:We're sorry but XXX doesn't work properly without JavaScript,经过查找发现可以通过修改路由模式来解决,经过测试确实有效。

修改后的代码示例:

const router = createRouter({
//改为#则可以直接变更路由模式
history: createWebHistory('#'),
routes,
});

问题二、

问题简述:页面展示正常后,调用登录发现出现下图报错

解决方式:经发现原来是发起 axios 请求环信置换连接 token 接口的时候,协议的获取是通过window.location.protocol来获取的,那么打包之后的协议为file:那么这时发起的请求就会变更为以 file 协议发起的请求,那么修改这里的逻辑,判断如果为 file 协议则默认走 http 协议发起请求,示例代码如下:

import axios from 'axios';
const defaultBaseUrl = '//a1.easemob.com';
console.log('window.location.protocol', window.location.protocol);
// create an axios instance
const service = axios.create({
withCredentials: false,
// baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
baseURL: `${
window
.location.protocol === 'file:' ? 'https:' : window.location.protocol
}
${defaultBaseUrl}`
,
// withCredentials: true, // send cookies when cross-domain requests
timeout: 30000, // request timeout
headers: { 'Content-Type': 'application/json' },
});
// request interceptor
service.interceptors.request.use(
(config) => {
// do something before request is sent
return config;
},
(error) => {
// do something with request error
console.log('request error', error); // for debug
return Promise.reject(error);
}
);

// response interceptor
service.interceptors.response.use(
/**
* If you want to get http information such as headers or status
* Please return response => response
*/


/**
* Determine the request status by custom code
* Here is just an example
* You can also judge the status by HTTP Status Code
*/

(response) => {
const res = response.data;
const code = response.status;
// if the custom code is not 20000, it is judged as an error.
if (code >= 400) {
return Promise.reject(new Error(res.desc || 'Error'));
} else {
return res;
}
},
(error) => {
if (error.response) {
const res = error.response.data; // for debug
if (error.response.status === 401 && res.code !== '001') {
console.log('>>>>>无权限');
}
if (error.response.status === 403) {
res.desc = '您没有权限进行查询和操作!';
}
return Promise.reject(res.desc || error);
}
return Promise.reject(error);
}
);

export default service;

参考资料

特别鸣谢两位道友文章非常有用,可以作为参考:

收起阅读 »

vue3项目打包时We're sorry but XXX doesn't work properly without JavaScript

vue
题引: 这周末公司突然分配了一个任务,让我搞一个混合代码的平板项目:vue3+安卓原生 来配合实现。看了一眼,问题不大,那边只要求把做好的页面打包成 dist 文件发给组长即可。开干。 正文: 当界面做完之后且打包完成,就打开了 dist 文件夹里的 inde...
继续阅读 »

题引:


这周末公司突然分配了一个任务,让我搞一个混合代码的平板项目:vue3+安卓原生 来配合实现。看了一眼,问题不大,那边只要求把做好的页面打包成 dist 文件发给组长即可。开干。


正文:


当界面做完之后且打包完成,就打开了 dist 文件夹里的 index.html 。突然发现页面是空白的,打开调试器之后突然发现了一个报错:

<strong>We’re sorry but XXX doesn’t work properly without JavaScript enabled</strong>



看了一下vue-router、pinia没有什么问题,调用顺序也没错。于是就往打包的文件夹查看,才发现了引用的路径是以 / 绝对路径开头的,以至于资源无法加载而导致页面空白。


发现了这个问题,直接定位到 vue.config.js 文件,如果是vite创建的话应该是 vite.config.js

//vue.config.js
export default = {
publicPath: './', //打包文件的路径
... // 其他配置
}

//vite.config.js
import {defineConfig} from 'vite';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
publicPath: './', //打包文件的路径
...
})

当然,上网查了一下前端的路由也会导致这个问题的出现。可以把mode值从 history 改成 hash

import {createRouter,createWebHashHistory} from 'vue-router';

const routes = [];
const router = createRouter({
router,
history:createWebHashHistory()
})

结尾:


以上就是处理打包上线时遇到 项目在没有启用JavaScript的情况下无法正常工作 的情况。


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

安卓与串口通信-数据分包的处理

前言 本文是安卓串口通信的第 5 篇文章。本来这篇文章不在计划内,但是最近在项目中遇到了这个问题,正好借此机会写一篇文章,在加深自己理解的同时也让大伙对串口通信时接收数据可能会出现分包的情况有所了解。 其实关于串口通信会可能会出现分包早有耳闻,但是我自己实际使...
继续阅读 »

前言


本文是安卓串口通信的第 5 篇文章。本来这篇文章不在计划内,但是最近在项目中遇到了这个问题,正好借此机会写一篇文章,在加深自己理解的同时也让大伙对串口通信时接收数据可能会出现分包的情况有所了解。


其实关于串口通信会可能会出现分包早有耳闻,但是我自己实际使用时一直没有遇到过,或者准确的说,虽然遇到过,但是并没有特意的去处理:


分包?不就是传过来的数据不完整嘛,那我把这个数据丢了,等一个完整的数据不就得了。


亦或者,之前使用的都是极少量的数据,一次读取的数据只有 1 byte ,所以很少出现数据包不完整的情况。


何为分包?


严格意义上来说,其实并不存在分包的概念。


因为由于串口通信的特性,它并不知道不知道也无法知道所谓的 “包” 是什么,它只知道你给了数据给它,他就尽可能的把数据发出去。


因为串口通信时使用的是流式传输,也就是说,所有数据都是以流的形式进行发送、读取,也不存在所谓的“包”的概念。


所谓的“包”只是我们在应用层人为的规定了多少长度的数据或者满足什么样格式的数据为一个“包”。


而为了最大程度的减少通信时的请求次数,在处理数据流时,通常会尽可能多的读取数据,然后缓存起来(即所谓的缓冲数据),直至达到设置的某个大小或超过某个时间没有读取到新的数据。


例如,我们人为的规定了一个数据包为 10 字节,PLC 或 其他串口设备发送时将这 10 个字节的数据连续的发送出来。但是安卓设备或其他主机在接收时,由于上面所说的原因,可能会先读到 4 字节的数据,再读到 6 字节的数据。也就是说,我们需要的完整数据不会在一次读取中读到,而是被拆分成了不同的“数据包”,此即所谓的 “分包”:


1.gif


怎么处理分包?


其实谜底就在谜面上,通过上面对分包出现的原因进行简单的解释之后,相信大伙对于怎么解决分包问题已经有了自己的答案。


解决分包的核心原理说起来非常简单,无非就是把我们需要的完整的数据包从多次读取到的数据中取出来,再拼成我们需要的完整数据包即可。


问题在于,我们应该怎么才能知晓读取到数据属于哪个数据包呢?我们又该怎么知道数据包是否已经完整了呢?


这就取决于我们在使用串口通信时定义的协议了。


一般来说,为了解决分包问题,我们常用的定义协议的方法有以下几种:



  1. 规定所有数据为固定长度。

  2. 为一个完整的数据规定一个终止字符,读到这个字符表示本次数据包已完整。

  3. 在每个数据包之前增加一个字符,用于表示后续发送的数据包长度。


固定数据包长度


固定数据长度指我们规定每次通信时发送的数据包长度都是固定的长度,如果实际长度不足规定的长度则使用某些特殊字符如 \0 填充剩余的长度。


对于这种情况,非常好处理,只要我们每次读取数据时都判断读取到的数据长度,如果数据长度没有达到符合的固定长度,则认为读取数据不完整,就接着读取,直至数据长度符合:

val resultByte = mutableListOf<Byte>()
private fun getFullData(count: Int = 0, dataSize: Int = 20): ByteArray {
val buffer = ByteArray(1024)
val readLen = usbSerialPort.read(buffer, 2000)
for (i in 0 until readLen) {
resultByte.add(buffer[i])
}

// 判断数据长度是否符合
return if (resultByte.size == dataSize) {
resultByte.toByteArray()
} else {
if (count < 10) {
getFullData(count + 1, dataSize)
}
else {
// 超时
return ByteArray(0)
}
}
}

但是这种方式也有一个明显的缺点,那就是使用场景局限性特别强,只适合于主机发送请求,从机器回应的这种场景,因为如果是在从机不停的发送数据,而主机可能在某个时间段读取,也可能一直轮询读取的情况下,光靠数据长度判断是不可靠的,因为我们无法确保我们读到的指定长度的数据一定就是同一个完整数据,有可能参杂了上一次的数据或者下一次的数据,而一旦读取错一次,就意味着以后每次读取的数据都是错的。


增加结束符


为了解决上述方式导致的局限性,我们可以给每一帧数据增加一个结束符号,通常来说我们会规定 \r\n 即 CRLF (0x0D 0x0A)为结束符号。


所以,我们在读取数据时会循环读取,直至读取到结束符号,则我们认为本次读取结束,已经获得了一个完整的数据包:

val resultByte = mutableListOf<Byte>()
private fun getFullData(): ByteArray {
var isFindEnd = false

while (!isFindEnd) {
val buffer = ByteArray(1024)
val readLen = usbSerialPort.read(buffer, 2000)
if (readLen != 0) {
for (i in 0 until readLen) {
resultByte.add(buffer[i])
}
if (buffer[readLen - 1] == 0x0A.toByte() && buffer.getOrNull(readLen - 2) == 0x0D.toByte()) {
isFindEnd = true
}
}
}

return resultByte.toByteArray()
}

但是这个方法显然也有一个缺陷,那就是如果是单次间隔读取或者轮询时第一次读取数据有可能也是不完整的数据。


因为我们虽然读取到了结束符号,但是并不意味着这次读取的就是完整的数据,或许前面还有数据我们并没有读到。


不过这种方式可以确保轮询时只有第一次读取数据有可能不完整,但是后续的数据都是完整的。


只是单次间隔读取的话就无法保证读取到的是完整数据了。


在开头增加数据包长度


和增加结束符类似,我们也可以在数据包开头增加一个特殊字符,然后在后面紧跟着一个指定长度(1byte)字符指定接下来的数据包长度有多长。


这样,我们就可以在解析时首先查找这个开始符号,查找到之后则认为一个新的数据包开始了,然后读取之后 1byte 的字符,获取到这个数据包的长度,接下里按照这个这个指定长度,循环读取直到长度符合即可。


具体读取方式其实就是上面两种方式的结合,所以这里我就不贴代码了。


最好的情况


最方便的解决数据分包的方法当然是在数据中既包括固定数据头、固定数据尾、甚至连数据长度都是固定的。


例如某款温度传感器,发送的是数据格包为固定 10 位长度,且有结束符 CRLF,并且数据包开头有且只有 -+ (0x2B 0x2D 0x20)三种情况,那么我们在接收数据时就可以这么写:

val resultByte = mutableListOf<Byte>()
val READ_WAIT_MILLIS = 2000
private fun getFullData(count: Int = 0, dataSize: Int = 14): ByteArray {
var isFindStar = false
var isFindEnd = false
while (!isFindStar) { // 查找帧头
val buffer = ByteArray(1024)
val readLen = usbSerialPort.read(buffer, READ_WAIT_MILLIS)
if (readLen != 0) {
if (buffer.first() == 0x2B.toByte() || buffer.first() == 0x2D.toByte() || buffer.first() == 0x20.toByte()) {
isFindStar = true
for (i in 0 until readLen) { // 有帧头,把这次结果存入
resultByte.add(buffer[i])
}
}
}
}

while (!isFindEnd) { // 查找帧尾
val buffer = ByteArray(1024)
val readLen = usbSerialPort.read(buffer, READ_WAIT_MILLIS)
if (readLen != 0) {
for (i in 0 until readLen) { // 先把结果存入
resultByte.add(buffer[i])
}
if (buffer[readLen - 1] == 0x0A.toByte() && buffer.getOrNull(readLen - 2) == 0x0D.toByte()) { // 是帧尾, 结束查找
isFindEnd = true
}
}
}


// 判断数据长度是否符合
return if (resultByte.size == dataSize) {
resultByte.toByteArray()
} else {
if (count < 10) {
getFullData(count + 1, dataSize)
}
else {
return ByteArray(0)
}
}

粘包呢?


上面我们只说了分包情况,但是在实际使用过程中,还有可能会出现粘包的现象。


粘包,顾名思义就是不同的数据包在一次读取中混合到了一块。


如果想要解决粘包的问题也很简单,类似于解决分包,也是需要我们在定义协议时给出能够区分不同数据包的方式,这样我们按照协议解析即可。


总结


其实串口通信中的分包或者粘包解决起来并不难,问题主要在于串口通信一般都是每个硬件设备厂商或者传感器厂商自己定义一套通信协议,而有的厂商定义的协议比较“不考虑”实际,没有给出任何能够区分不同数据包的标志,这就会导致我们在接入这些设备时无法正常的解析出数据包。


但是也并不是说就没有办法去解析,而是需要我们具体情况具体分析,比如温度传感器,虽然通信协议中没有给出数据头、数据尾、数据长度等信息,但是其实它返回的数据格式几乎都是固定的,我们只要按照这个固定格式去解析即可。


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

Android 如何统一处理登录后携带数据跳转到目标页面

需求场景 我们在开发应用的时候经常会遇到先登录,登录成功后再跳转到目标页面。比如商品详情页面我们点击购买必须要先登录,登录完成才能去下单支付。针对这种场景,我们一般有两种做法: 点击购买跳转到登录,登录完成需要用户再次点击购买才能去下单支付页面,这种用户体验...
继续阅读 »

需求场景


我们在开发应用的时候经常会遇到先登录,登录成功后再跳转到目标页面。比如商品详情页面我们点击购买必须要先登录,登录完成才能去下单支付。针对这种场景,我们一般有两种做法:



  1. 点击购买跳转到登录,登录完成需要用户再次点击购买才能去下单支付页面,这种用户体验不是很好。

  2. 点击购买跳转到登录,登录完成直接跳转到下单支付页面。


第一种我们就不谈了产品经理不同意🐶。第二种我们一般是在 onActivityResult 里面获取到登录成功,然后根据 code 跳转到目标页面。这种方式缺点就是我们要在每个页面都处理相同的逻辑还有定义各种 code,如果应用里面很多这种场景也太繁琐了。那有没有统一的方式去处理这种场景就是我们今天的主题了。


封装方式


我们的应用是组件化的,APP 的页面跳转使用了 Arouter。所以我们统一处理使用 Arouter 封装。直接上代码

fun checkLoginToTarget(postcard: Postcard) {//Postcard 是 Arouter 的类
if (User.isLogin()) {
postcard.navigation()
} else {
//不能使用 postcard 切换 path 直接跳转,因为 group 可能不同,所以重新 build
ARouter.getInstance().build(Constant.LOGIN)
.with(postcard.extras)//获取携带的参数重新转入
.withString(Constant.TAGACTIVIFY, postcard.path)//添加目标路由
.navigation()
}
}
//登录成功后在登录页面执行这个方法
fun loginSuccess() {
val intent= intent
val target = intent.getStringExtra(Constant.TAGACTIVIFY)//获取目标路由
target?.apply {
if (isNotEmpty()){
val build = ARouter.getInstance().build(this)
val extras = intent.extras//获取携带的参数
if (extras != null) {
build.with(extras)
}
build.navigation()
}
}
finish()
}

代码加了注释,使用 Kotlin 封装了顶层函数,登录页面在登录成功后跳转到目标页面,针对上面的场景直接调用 checkLoginToTarget 方法。

checkLoginToTarget(ARouter.getInstance().build(Constant.PAY_PAGE).withInt(Constant.GOOD_ID,id))

通过 Arouter 传入下单支付的路由地址,并且携带了商品的 ID,生成了 Postcard 参数。登录成功后能带着商品 ID
直接下单支付了。


最后


如果项目里没有使用路由库可以使用 Intent 封装实现,或者别的路由库也可以用上面的方式去做统一处理。


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

揭开Android视图绘制的神秘面纱

揭开Android视图绘制的神秘面纱 在Android的UI中,View是至关重要的一个组件,它是用户界面的基本构建块。在View的绘制过程中,涉及到很多重要的概念和技术。本文将详细介绍Android View的绘制过程,让你能够更好地理解和掌握Android...
继续阅读 »

揭开Android视图绘制的神秘面纱


在Android的UI中,View是至关重要的一个组件,它是用户界面的基本构建块。在View的绘制过程中,涉及到很多重要的概念和技术。本文将详细介绍Android View的绘制过程,让你能够更好地理解和掌握Android的UI开发。


什么是View?


View是Android系统中的一个基本组件,它是用户界面上的一个矩形区域,可以用来展示文本、图片、按钮等等。View可以响应用户的交互事件,比如点击、滑动等等。在Android中,所有的UI组件都是继承自View类。


View的绘制过程


View的绘制过程可以分为三个阶段:测量、布局和绘制。下面我们将逐一介绍这三个阶段。


测量阶段(Measure)


测量阶段是View绘制过程的第一个重要阶段。在测量阶段,系统会调用View的onMeasure方法,测量View的宽度和高度。在这个过程中,系统会根据View的LayoutParams和父容器的大小来计算出View的大小。


例:下面代码是一个自定义View的onMeasure方法例程。在测量过程中,我们设定了View的大小。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 获取宽度的Size和Mode
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
// 如果Mode是精确的,直接返回
if (widthMode == MeasureSpec.EXACTLY) {
setMeasuredDimension(widthSize, heightMeasureSpec);
return;
}

// 计算View的宽度
int desiredWidth = getPaddingLeft() + getPaddingRight() + defaultWidth;
int measuredWidth;
if (desiredWidth < widthSize) {
measuredWidth = desiredWidth;
} else {
measuredWidth = widthSize;
}

// 设置宽度和高度的Size和Mode
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int measuredHeight = defaultHeight;
if (heightMode == MeasureSpec.EXACTLY) {
measuredHeight = heightSize;
} else if (heightMode == MeasureSpec.AT_MOST) {
measuredHeight = Math.min(defaultHeight, heightSize);
}
setMeasuredDimension(measuredWidth, measuredHeight);
}

在测量阶段结束后,系统会将计算好的宽度和高度传递给布局阶段。


布局阶段(Layout)


布局阶段是View绘制过程的第二个重要阶段。在布局阶段,系统会调用View的onLayout方法,将View放置在父容器中的正确位置。在这个过程中,系统会根据View的LayoutParams和父容器的位置来确定View的位置。


例:下面代码是一个自定义ViewGroup的onLayout方法例程。在布局过程中,我们遍历子View,并根据LayoutParams确定子View的位置和大小。

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int count = getChildCount();
int left = getPaddingLeft();
int top = getPaddingTop();
int right = getMeasuredWidth() - getPaddingRight();
int bottom = getMeasuredHeight() - getPaddingBottom();

for (int i = 0; i < count; i++) {
View child = getChildAt(i);
if (child.getVisibility() == GONE) {
continue;
}

LayoutParams lp = (LayoutParams) child.getLayoutParams();
int childLeft = left + lp.leftMargin;
int childTop = top + lp.topMargin;
int childRight = right - lp.rightMargin;
int childBottom = bottom - lp.bottomMargin;
child.layout(childLeft, childTop, childRight, childBottom);
}
}

绘制阶段(Draw)


绘制阶段是View绘制过程的最后一个重要阶段。在绘制阶段,系统会调用View的onDraw方法,绘制View的内容。在这个过程中,我们可以使用Canvas对象来绘制各种形状、文本和图片等等。


例:下面代码是一个自定义View的onDraw方法例程。在绘制过程中,我们使用Paint对象绘制了一段文本。

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

//绘制文本
String text = "Hello World";
Paint paint = new Paint();
paint.setTextSize(50);
paint.setColor(Color.RED);
paint.setAntiAlias(true);
canvas.drawText(text, 0, getHeight() / 2, paint);
}

除了绘制内容,我们还可以在绘制阶段绘制View的背景和前景。系统会调用drawBackgrounddrawForeground方法来绘制背景和前景。值得注意的是,View的绘制顺序是:先绘制背景,再绘制内容,最后绘制前景。


View的绘制流程


View的绘制流程可以看作是一个递归调用的过程,下面我们将具体介绍这个过程。


Step 1:创建View


在View绘制过程的开始阶段,我们需要创建一个View对象,并将它添加到父容器中。在这个过程中,系统会调用View的构造函数,并将View的LayoutParams传递给它。


Step 2:测量View


接下来,系统会调用View的measure方法,测量View的宽度和高度。在这个过程中,View会根据自身的LayoutParams和父容器的大小来计算出自己的宽度和高度。


Step 3:布局View


在测量完成后,系统会调用View的layout方法,将View放置在父容器中的正确位置。在这个过程中,View会根据自身的LayoutParams和父容器的位置来确定自己的位置。


Step 4:绘制背景


在布局完成后,系统会调用View的drawBackground方法,绘制View的背景。在这个过程中,我们可以使用Canvas对象来绘制各种形状、文本和图片等等。


Step 5:绘制内容


接下来,系统会调用View的onDraw方法,绘制View的内容。在这个过程中,我们可以使用Canvas对象来绘制各种形状、文本和图片等等。


Step 6:绘制前景


在绘制内容完成后,系统会调用View的drawForeground方法,绘制View的前景。在这个过程中,我们同样可以使用Canvas对象来绘制各种形状、文本和图片等等。


Step 7:绘制子View


接着,系统会递归调用ViewGroup的dispatchDraw方法,绘制所有子View的内容。在这个过程中,我们可以使用Canvas对象来绘制各种形状、文本和图片等等。


Step 8:完成绘制


最后,所有的View绘制完成,整个View树也就绘制完成。


例:下面代码是一个自定义ViewGroup的绘制流程例程。在绘制过程中,我们先画背景,再绘制每个子View的内容。

public class MyViewGroup extends ViewGroup {

public MyViewGroup(Context context) {
super(context);
}

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

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 测量子View的宽高
measureChildren(widthMeasureSpec, heightMeasureSpec);

// 获取ViewGroup的宽高大小
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);

// 设置ViewGroup的宽高
setMeasuredDimension(widthSize, heightSize);
}

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// 遍历所有子View,设置它们的位置和大小
int childCount = getChildCount();
int left, top, right, bottom;
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
left = childView.getLeft();
top = childView.getTop();
right = childView.getRight();
bottom = childView.getBottom();
childView.layout(left, top, right, bottom);
}
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 画背景
canvas.drawColor(Color.WHITE);
}

@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
// 绘制每个子View的内容
for (int i = 0; i < getChildCount(); i++) {
View childView = getChildAt(i);
childView.draw(canvas);
}
}
}

在ViewGroup的绘制流程中,系统会先调用ViewGroup的draw方法,然后依次调用dispatchDraw方法和绘制每个子View的draw方法。ViewGroup的绘制顺序是先绘制自己的背景,再绘制每个子View的内容和背景,最后绘制自己的前景。


总结


本文详细介绍了Android View的绘制过程,包括测量阶段、布局阶段和绘制阶段。同时,我们还在代码实现的角度,详细说明了Android ViewGroup的绘制流程,帮助你更好地理解和掌握Android的UI开发。


推荐


android_startup: 提供一种在应用启动时能够更加简单、高效的方式来初始化组件,优化启动速度。不仅支持Jetpack App Startup的全部功能,还提供额外的同步与异步等待、线程控制与多进程支持等功能。


AwesomeGithub: 基于Github的客户端,纯练习项目,支持组件化开发,支持账户密码与认证登陆。使用Kotlin语言进行开发,项目架构是基于JetPack&DataBinding的MVVM;项目中使用了Arouter、Retrofit、Coroutine、Glide、Dagger与Hilt等流行开源技术。


flutter_github: 基于Flutter的跨平台版本Github客户端,与AwesomeGithub相对应。


android-api-analysis: 结合详细的Demo来全面解析Android相关的知识点, 帮助读者能够更快的掌握与理解所阐述的要点。


daily_algorithm: 每日一算法,由浅入深,欢迎加入一起共勉。


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

那些拿20k💰的大佬在职场都是怎么沟通的❓

☀️ 前言 大家好我是小卢,职场沟通是每个职场人必备的技能,但是如何提高职场沟通能力却是需要不断学习和实践。 下面就给大家带来四点方法能很大程度提升你在职场的沟通能力。 ⌨️ 了解每个人的沟通方式 每个人的个性、经验和教育背景都不同,这些因素都会影响到个...
继续阅读 »

☀️ 前言



  • 大家好我是小卢,职场沟通是每个职场人必备的技能,但是如何提高职场沟通能力却是需要不断学习和实践。

  • 下面就给大家带来四点方法能很大程度提升你在职场的沟通能力。


⌨️ 了解每个人的沟通方式



  • 每个人的个性、经验和教育背景都不同,这些因素都会影响到个人的沟通方式。有些人可能会喜欢直接表达自己的想法和意见,而有些人可能更倾向于暗示询问

  • 在实际职场生活中,我们不仅需要了解自己的沟通方式,还需要了解对方的沟通方式。简单举个例子吧:

  • 假设你是一个刚入公司不久的产品经理,你每周或者每月都需要给上司做一个工作汇报。

    • 你知道你的上司更喜欢使用图表和数据进行沟通,你可以准备好相关的数据和图表,并在开会过程中使用它们。这可以帮助你的上司更好地理解你的意思,并更快地进入到讨论的核心问题。

    • 你知道你的上司比较喜欢使用直接表达的方式来沟通,在这种情况下,直接表达你的想法和意见可能更为有效。你可以以明确的方式表达你对项目的看法,并解释你的看法背后的原因。



  • 在与他人交流时,我们需要时刻注意自己的语言、态度和非语言信号,并根据对方的反应进行调整。这需要一定的敏感度和经验,但是通过不断地练习和反思,我们可以逐渐提高自己的职场沟通能力,并取得更好的效果。


🤗 注意语速和语调



  • 职场沟通中,语速和语调是非常重要的因素,它们往往可以决定对方对你的印象和理解,如果你的语速太快或者语调不合适,很容易让对方感到困惑或者不舒服。

  • 除了注意自己的语速和语调,我们还需要注意对方的语速和语调。如果在某次交谈中你发现对方特别激动,说话特别快导致你不能全部理解,你可以说:“你说的内容非常重要,我来总结一下刚刚你分享的信息,看看是否符合预期,以便我能更好地理解你的意思?”

  • 这样的话语不仅能够有效地表达自己的需求,也能够尊重对方的沟通方式,让双方都能够更好地理解彼此。


👂 学会倾听



  • 职场沟通不仅仅是说话,更重要的是倾听。倾听意味着不仅是听别人说话,还包括尊重对方的意见和观点,关注对方的情感和态度,以及在适当的时候提出问题和反馈,以达到更好的沟通效果。

  • 要成为一个好的倾听者,我们需要全神贯注地聆听对方说话。这意味着不要分心,不要中途打断对方,而是要给对方充足的时间和空间来表达自己的想法和意见。如果你有不同的看法或者意见,可以先把它们记在脑海里,等对方表达完后再进行回应。

  • 在倾听的过程中,我们还需要注意对方的情绪和表情。通过观察对方的肢体语言和面部表情,我们可以更好地了解对方的真实意图和情感状态,从而更好地回应和理解对方的想法和需求,建立更好的信任和合作关系。

  • 举个例子吧:假设你是一个团队的领导,正在讨论下一步的项目计划。你发现其中一个成员很少发表意见,似乎对讨论不太感兴趣。你可以采取主动倾听的方式,问他对当前的计划有何看法,或者给他更具体的问题,以激发他的参与度。这样可以让他感受到自己的意见被认真听取,也有助于整个团队更好地理解和解决问题


👺 简明扼要



  • 简明扼要是职场中非常重要的一个点。当你需要向同事或客户提出需求时,最好提前思考好问题的前提条件、现状和问题的分支情况,一次性把问题说明白,尽量减少来回问答的次数,这样可以更有效地利用大家的时间和精力。

  • 为了让自己的观点更清晰地传达给别人,你可以先说出结论和重点,然后再说明为什么这么认为,并提供相关的事实依据。在接受问题或错误的指责时,也应该直接说明问题并找到解决办法,而不是遮掩或解释,以保证工作的顺利完成。

  • 我有一个同事在公司寻求大佬帮助的时候把前置说了很久,导致一直进入不到重点,别人根本不知道你想表达什么,这不仅浪费了别人的时间,还会让人对你产生厌恶。

  • 你可以简单干脆一点:这个问题导致了 xxx,影响了 xxx 的用户,他的原因是 xxx,我的想法是 xxx ,所以想问一下有没有更好的方案?


wallhaven-8o6rmo.jpeg


👋 写在最后



  • 如果您觉得这篇文章有帮助到您的的话不妨🍉🍉关注+点赞+收藏+评论+转发🍉🍉支持一下哟~~😛您的支持就是我更新的最大动力。

  • 如果想跟我一起讨论和学习更多的前端知识可以加入我的前端交流学习群,大家一起畅谈天下~~~

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

怎么算是在工作中负责?

作为打工人,受人之禄,忠人之事。但就像呼兰说的,躺有躺的价格,卷有卷的价格。身为程序员,我们在平时工作中要做到怎样才能算是“负责”了呢? 我们可以把工作边界和范围分为三部分: 个人基本能力 工作内容 工作时间 对自己的基本能力负责 基本能力包括两部分:1)...
继续阅读 »

作为打工人,受人之禄,忠人之事。但就像呼兰说的,躺有躺的价格,卷有卷的价格。身为程序员,我们在平时工作中要做到怎样才能算是“负责”了呢?


我们可以把工作边界和范围分为三部分:



  • 个人基本能力

  • 工作内容

  • 工作时间


对自己的基本能力负责


基本能力包括两部分:1)技术能力,2)熟悉公司系统的能力。


程序员是一个非常需要持续学习的职业,我们在实际工作中,遇到自己不会的问题在所难免,这时可以向别人请教,但是千万不要觉得请教完就没事儿了,我们需要思考复盘自己为什么不会,要想办法补齐自己的知识和技能短板。


我们学的东西一定要在实际工作中使用,这样才能够激发学习的积极性,同时验证自己的学习成果。当公司准备技术升级或者技术转型时,这也是我们为自己的技能升级的好机会。


很多公司都会有自己的内部系统,熟练掌握和使用这些系统,也是我们需要做到的,它的价值在于,内部系统一般都是和公司的整个监控系统集成好的,例如公司内部的SOA框架或者微服务框架,都是和公司内部的监控系统有集成的,即使这个框架再“不好”,公司内部的项目还是会使用,我们需要让自己站得高一些,去看待内部系统在整个公司级别的作用和地位,这样才能更好地发挥自己的技术能力。


对安排的工作负责


程序员职业的特殊性在于,工作本身的具体内容和难度,会随着被安排的工作内容的改变而改变。从对工作负责的角度来说,我们大部分人会付出比当初预想的更多的时间,才能让自己按时完成工作。


如果一件事情的复杂度远远超过之前的预估,在规定的时间内,自己确实无法完成,这时正确的态度不是硬着头皮上,而是将情况理清楚,早点找经理或者负责人,让他们知道事情的进度和之前没有预想到的难度,把事情重新安排一下。


从管理者的角度来看,一件事情安排的不合理,就应该早发现,早计划,重新安排资源。


对工作时间负责


对工作时间负责,是说最好在“实际上班”时间之前到,避免有人找你却找不到的情况。


这不只是为了保证工作时间,而是想强调程序员的工作不止是写代码,还有很多沟通交流的事情,要保证基本的工作时间,才能更有效的和团队交流,确保我们的工作的价值。


对于项目和团队安排的各种会议,要准时参加,如果不能参加,需要提前告知经理或者会议组织者,避免浪费大家的事情。


总之,我们工作中的责任是一点点增加的,负责任的态度和习惯,也是从平时工作中一件件事情中养成的。形成这样的习惯,成为一个受人信任的人,是我们在职场中要培养的重要品质。



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

何不食肉糜?

21年的时候,微博上有过一番口诛笔伐,就是就是管清友建议刚开始工作的年轻人就近租房不要把时间浪费在上班的路上,要把时间利用起来投资自己,远比省下的房租划算。 视频见这里:http://www.bilibili.com/video/BV1Bb… 当时我印象非常...
继续阅读 »

21年的时候,微博上有过一番口诛笔伐,就是就是管清友建议刚开始工作的年轻人就近租房不要把时间浪费在上班的路上,要把时间利用起来投资自己,远比省下的房租划算。


视频见这里:http://www.bilibili.com/video/BV1Bb…



当时我印象非常深刻,微博评论是清一色的 “何不食肉糜”,或者说“房租你付?”


可能是因为这件事情的刺激,管清友后来才就有了“我特别知道年轻人建议专家不要建议”的言论。


对还是错?


在我看来,管清友的这个建议可以说是掏心掏肺,非常真诚,他在视频里也说了,他是基于很多实际案例才说的这些话,不是说教。


为什么我这么肯定呢?


很简单,我就是代表案例。


我第一家公司在浦东陆家嘴,四号线浦东大道地铁站旁边,我当时来上海的时候身无分文,借的家里的钱过来的,我是贫困家庭。


但,为了节约时间,我就在公司附近居住,步行五分钟,洗头洗澡都是中午回住的地方完成,晚上几乎都是11:00之后回去,倒头就睡,因为时间可以充分利用。


节约的时间,我就来研究前端技术,写代码,写文章,做项目,做业务,之前的小册(免费章节,可直接访问)我也提过,有兴趣的可以去看看。


现在回过头来看那段岁月,那是充满了感激和庆幸,自己绝对做了一个非常正确的决定,让自己的职业发展后劲十足。


所以,当看到管清友建议就近租房的建议,我是非常有共鸣的,可惜世界是参差的,管清友忽略了一个事实,那就是优秀的人毕竟是少数,知道如何主动投资自己的人也是凤毛麟角,他们根本就无法理解。


又或者,有些人知道应该要投资自己,但是就是做不到,毕竟辛苦劳累,何苦呢,做人,不就是应该开心快乐吗?


说句不好听的,有些人的时间注定就是不值钱的。


工作积极,时间长是种优势?


一周前,我写了篇文章,谈对“前端已死”的看法,其中提到了“团队下班最晚,工作最积极”可以作为亮点写在简历里。


结果有人笑出了声。



好巧的是,管清友的租房建议也有人笑了,出没出声并不知道。



也有人回复“何不食肉糜”。


这有些出乎我的意料,我只是陈述一个简单的事实,却触动了很多人的敏感神经。


我突然意识到,有些人可能有一个巨大的认知误区,就是认为工作时长和工作效率是负相关的,也就是那些按时下班的是效率高,下班晚的反而是能力不足,因为代码不熟,bug太多。



雷军说你说的很有道理,我称为“劳模”是因为我工作能力不行。


你的leader也认为你说的对,之前就是因为我每天准时下班,证明了自己的能力,所以自己才晋升的。


另外一个认知误区在于,把事实陈述当作目标指引。


如果你工作积极,是那种为自己而工作的人,你就在简历中体现,多么正常的建议,就好比,如果你是北大毕业的,那你就在简历中体现,没任何问题吧。


我可没有说让你去拼工作时长,装作工作积极,就好比我没有让你考北大一样。


你就不是这种类型的人,对吧,你连感同身受都做不到,激动个什么呢,还一大波人跟着喊666。


当然,我也理解大家的情绪,我还没毕业的时候,也在黑心企业待过,钱少事多尽煞笔,区别在于,我相对自驱力和自学能力强一些,通过自己的努力跳出了这个循环。


但大多数人还是被工作和生活推着走,所以对加班和内卷深恶痛绝,让本就辛苦的人生愈发艰难,而这种加班和内卷并没有带来收入的提升。


那问题来了,有人通过努力奋斗蒸蒸日上,有人的辛苦努力原地踏步,同样的,有的人看到建议觉得非常有用,有的人看到建议觉得何不食肉糜,区别在哪里呢?


究竟是资本作恶呢?还是自己能力不足呢?


那还要建议吗?


管清友不再打算给年轻人建议了,我觉得没必要。


虽然,大多数时候,那些听得进去建议的人大多不需要建议,而真正需要建议的又听不进,但是,那么多年轻人,总有一部分潜力股,有一些真正需要帮助的人。


他们可能因为环境等原因,有短暂的迷茫与不安,但是,来自前人发自真心的建议或许可以让他们坚定自己前进方向,从而走出不一样的人生。


就像当年我被乔布斯的那些话语激励过那般。


所以,嘲笑之人任其笑之,只要能帮助到部分人,那就有了价值。


因此,我不会停止给出那些我认为对于成长非常有帮助的建议。


(完)


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

聊聊我在阿里第一年375&晋升的心得

前言 思来想去,觉得这个事情除了领导赏识大佬抬爱之外,还是挺不容易的,主观认为有一定的参考价值,然后也是复盘一下,继续完善自己。 绩效 首先晋升的条件就是要有个好绩效,那么我们就先基于绩效这个维度简单聊一下。 很多同学都知道阿里的绩效是361制度,也就是30%...
继续阅读 »

前言


思来想去,觉得这个事情除了领导赏识大佬抬爱之外,还是挺不容易的,主观认为有一定的参考价值,然后也是复盘一下,继续完善自己。


绩效


首先晋升的条件就是要有个好绩效,那么我们就先基于绩效这个维度简单聊一下。


很多同学都知道阿里的绩效是361制度,也就是30%的人拿A,60%的人拿B,10%的人拿C,不过在阿里,我们一般不用ABC来表示,除去价值观考核,我们常用的表达是3.75、3.5、3.25,初略的对应关系如下:

361通用阿里
30%A3.75
60%B3.5 ±
10%C3.25

那么,了解了阿里的绩效制度,再来看看绩效里面的门道。


首先,讲一个职场潜规则,「团队的新人是用来背绩效的」,也就是会把差绩效(325)指标分配给新人,因为如果把325给老人,容易产生团队不稳定的因素,而且不得不承认的是,少一个老人比少一个新人的成本更大,另一方面,你作为新人,业务不熟,根基不稳,也不见得能产出多大的价值,所以对于多数新人来说,也只能接受。据我所知,只有极少的公司里面会有「绩效保护」这种政策,而且一般还是针对的应届生,社招来说,还是要看能力的。


其次,基于潜规则,大部分新人都在为保住3.5做努力,只有少数优秀的人可以拿到更好的绩效。而新人拿375在阿里是很少的存在,即使是老人,拿375都是非常不容易的,何况是一个新人。


最后,就是晋升,晋升的前提条件就是满一年且是好绩效,加上现在降本增效的大环境,有的要求连续两年375才行,甚至都不一定有名额,当然,晋升也和团队绩效和级别有关系,但总之,男上加男,凤毛麟角。


个人背景


我是21年8月份入职阿里的,2022年是在阿里的第一个整财年。


之前一直是在小厂摸爬滚打,没有大厂经历,这对于我来说,是劣势,写代码不注重安全性防护,没有code review,没有ab test,没有自动化测试,没有文档沉淀,没有技术分享,纯纯小作坊。更重要的是,小厂和大厂的做事风格流程什么的,真的是千差万别,所以当时的landing对我来说,还是很难受的。但是,我有没有自带的优势呢,也有,写作能力,但是光有写作能力还是不够的,你没东西可写也不行啊。


其实试用期结束之后就差不多进入新的财年了,对于刚刚进入状态的我,也迎来了更大的挑战。过去的一整年有较多的精力都投入在研发效能和安全生产方面,这对于以前纯业务开发的我来说,完全是一个新的领域,好在不负重托,也略有成果。


其实回想过去一年的经历来看,今天的成绩是多维度的结合,比如硬实力和软实力、个人和团队、内部和外部等多个维度,下面将分别介绍一些我个人的心得经验,仅供参考。


沟通能力


这也用说?不就是说话吗,谁不会说?


我看过很多的简历,如果有「自我评价」,几乎都会提到「具备良好的沟通能力」。
可是沟通能力看起来真的有那么简单吗?我认为不是的,实际上我甚至认为它有点难。


在职场经常会有这些问题:



  1. 这个点我没太理解,你能在解释一下吗?

  2. 为什么要这么做?为什么不这么做?

  3. 现在有什么问题?

  4. 你的诉求是什么?

  5. 讲话的时候经常被打断等等...


这些问题你是不是被问到过,或者这么问别人呢。


而这些问题的背后,则反映了沟通的不完整和不够清晰。面对他人的挑战,或向跨部门的同学讲述目标价值时,会沟通的同学会显的游刃有余,而不会沟通的同学则显得捉襟见肘。


沟通方面,其实也包含很多场景。


首先是逻辑要清晰。


面对用户的一线同事,比如销售和客服,他们都是有话术的,话术就是沟通的技巧。


为什么脸皮薄/不懂拒绝的人容易被销售忽悠?


因为销售在跟客户沟通的时候,就是有一套完整的话术,他先讲一,看你反应再决定讲二还是三;当你拒绝了A方案,他还有B方案C方案。一套完整的话术逻辑把你都囊括在里面,最后只能跟着他的思维走,这就是话术的意义。


同样的,在职场,你跟人沟通的时候,不能直说怎么做,解决方案是什么,而背景和问题同样重要,同时讲述问题的时候要尽可能的拆解清楚,避免遗漏,这样不只是让别人更理解你的意思,其实有时候换个视角,解决方案可能有更优的。


逻辑清晰,方案完善,对方就会处于一个比较舒服的状态,有时候能起到事半功倍的效果。你可能会觉得有些麻烦,但如果因为没有表达清楚而导致最后的结果不符合预期,孰轻孰重,应该拎得清的吧?


其次是要分人。


我在之前的面经中提到,自我介绍要分人,技术面试官和HR的关注点一定是不一样的,面对这种不同的出发点,你讲的东西肯定不能是同一套,要有侧重点,你跟HR讲你用了什么牛逼的技术、原理是什么,那不是瞎扯嘛。


这个逻辑在职场也是一样的,你和同事讨论技术、向领导汇报、回答领导的领导问题、跟产品、运营、跨部门沟通,甚至出现故障的时候给客满提供的话术,面对不同的角色、不同的场合,表达出来的意思一定是要经过「翻译」的,多换位思考。


即要把自己的意思传达到,也要让对方get到,这才是有效沟通。


所谓沟通能力,不只是有表达,还要有倾听。


倾听方面,比如当别人给你讲一个事情的时候,你能不能快速理解,能不能get到对方的点,这也很重要。
快速且高效,这是一个双向的过程。这里面会涉及到理解能力,而能理解的基础是基于现有的认知,也就是过往的工作经验、项目经历和知识面,这是一个积累的过程。


当然,也有可能是对方表达的不够清楚,这时候就要不耻下问,把事情搞清楚,搞不清楚就会有不确定性,就是有风险,如果最终的结果不符合预期,那么复盘的时候,委屈你一下,不过分吧😅。


最后是沟通媒介。


我们工作沟通的场景一般都是基于钉钉、微信、飞书之类的即时通讯办公平台,文字表达的好处是,它可以留档记录,方便后期翻阅查看,但是也一定程度在沟通表达的传递上,存在不高效的情况。


那这时候,语音、电话就上场了,如果你发现文字沟通比较费劲的时候,那一定不如直接讲来的更高效。


但是语音、电话就一定能聊的清楚吗,也不见得。


“聊不清楚就当面聊”,为什么当面聊就能比语音、电话聊的清楚?因为当面聊,不只是可以听到语气看到表情肢体动作,更重要的是当面聊的时候,双方一定是专注在这个事情上的,不像语音的时候还可以干其他的事,文字甚至会长时间已读不回,所以讲不清楚的时候,当面聊的效果是最好的。为了弥补留档的缺陷,完事再来个文字版的纪要同步出来,就ok了。


其他。


上面提到逻辑要清晰,要分人,还有倾听能力,和善用沟通媒介。


其实沟通里还包括反应能力,当你被突然问到一个问题的时候,能不能快速流畅的表达清楚,这个也很关键,如果你支支吾吾,反反复复的都没有说清楚,设想一下,其他人会怎么看你,“这人是不是不行啊?”,长此以往,这个信任度就会降低,而一旦打上标签,你就需要很多努力才能证明回来。


还包括争辩能力,比如在故障复盘的时候,能不能有效脱身不被拉下水,能不能大事化小小事化了,也都是沟通的技巧,限于篇幅,不展开了。


学会复盘


复盘是什么?


复盘是棋类术语,指对局完毕后,复演该盘棋的记录,以检查对局中招法的优劣与得失关键。在工作中,复盘是通过外在的表象、客观的结果找出本质,形成成功经验或失败教训,并应用到其他类似事件中,提升面向未来的能力。


所以,复盘不是流水账的记录经过和简单总结,也不是为了表彰罗列成绩,更不是为了甩锅而相互扯皮。找出本质的同时一定要有思考,这个思考可以体现在后续的一些执行事项和未来规划上,总之,就是要让「复盘」变的有意义。


什么是有意义的复盘?


就是你通过这次复盘,能知道哪些错误是不能再犯的,哪些正确的事是可以坚持去做的。会有一种「再来一次结果肯定不一样」的想法,通过有意义的复盘让「不完美」趋向「完美」。


我个人复盘的三个阶段:



  • 回顾:回顾目标、经过、结果的整个流程;

  • 分析:通过主观、客观的视角分析,失败的原因是什么,成功的原因是什么;

  • 转化:把成功经验和失败教训形成方法论,应用到类似事件中;


Owner意识


什么是owner意识?


简单讲就是主人翁精神。认真负责是工作的底线,积极主动是「owner意识」更高一级的要求。


如果你就是怀着只干活的心态,或者躺平的心态,换我是领导,也不认为你能把活做好。因为这种心态就是「做了就行,能用就行」,但有owner意识不一样,这种人做事的时候就会多思考一点、多做一点,这里面最主要的就是主动性,举个栗子,好比写代码,当你想让程序运行的更好的时候,你就会多关注代码编写的鲁棒性,而不是随便写写完成功能就行。


但人性自私,责任感也不是人人都有,更别提主动性了,所以这两种心态的人其实差别很大,有owner意识的人除了本职工作能做好之外,在涉及到跨团队的情况,也能主动打破边界去push,有责任心有担当,往往能受到团队内外的一致好评。


在其位谋其职,我其实并没有特意去固化自己的owner意识,就是怀着一个要把事情做好的心态,跟我个人性格也有关系,现在回想起来,不过是水到渠成而已。



卷不一定就有owner意识,不卷也不代表没有。



向上管理


这个其实我一开始做的并不好,甚至可以说是很差,小公司出身哪需要什么向上管理,活干好就行。


但是现在不一样了,刚入职比较大的一个感受就是,我老板(领导)其实并不太过问我的工作内容,只是偶尔问一下进度。


然而这个「问」,其实也能反应出一些过往不太在意的问题:



  1. 没有及时汇报,等到老板来问的时候其实处于一个被动的局面了,虽然也不会有什么太大的影响,可能多数人也都是这样,但是这不正说明我不够出众吗?

  2. 不确定性,什么进度?有没有遇到问题?这些都是不确定性,老板不喜欢“惊喜”,有困难要说,有风险要提前暴,该有结果的时候没有,老板也很被动,你会留下一个什么印象?


当然,向上管理也不只是向上汇报,也是一个体现个人能力和学习的渠道。不要只提问题找老板要解决方案,我会简述问题,评估影响面,还会给出解决方案的Plan A和Plan B,这样老板就是做选择题了,即使方案不够完美,老板指点一下不正是学习的好机会吗。


学会写TODO


为什么写todo?


写todo的习惯其实是在上家公司养成的,因为要写周报,如果不记录每天做了什么,写周报的时候就会时不时的出现遗漏的情况。除了记录当天所做的事情之外,我还列了第二天要做的事情。虽然一直有给自己定规划的习惯,但是颗粒度都没有这么细过。彼时的todo习惯,不仅知道当天做了什么,还规划了第二天做什么,时刻有目标,也不觉得迷茫。


进入阿里之后,虽然没有周报月报这种东西,但是这个习惯也一直保持着,在此之上,我还做了一些改良。



  1. 优先级,公司体量一旦大起来之后,业务线就很多,随之而来的事情就很多,我个人对处理工作是非常反感多线程并发的,特别是需要思考的工作,虽然能并行完成,但完成度不如专注在一件事情上的好,但是有些事情又确实得优先处理,所以就只能根据事情的紧急程度排一下优先级,基本很少有一件事需要从早到晚都投入在里面的,所以抽出身来就可以处理下一件事,所以也不会出现耽误其他事情的情况,当然线上故障除外。

  2. 消失的时间,因为真的是事情又多又杂,时常在下班的时候会有一种瞎忙的感觉,就是「忙了一天,好像什么都没干」,但又确实很忙很累,仿佛陷入一个怪圈。所以后来我就把颗粒度拆的更细,精确到小时,也不是几点钟该做什么,就是把做的事情记录下来,并备注一下投入的时间,比如上午排障答疑投入了两小时,这样到中午吃饭的时候也不至于上午就这样虚度了。让“消失的时间”有迹可循,治愈精神内耗。

  3. 归纳总结,我现在是在语雀以月度为单位记录每天的todo,这样就可以进行月度的总结和复盘,在半年度的OKR总结的时候,还有了归纳总结的来源,有经过、有结果、还有时间线,一目了然,再也不用为写总结发愁了。


总之,写todo的好处除了记录做了什么、要做什么,它还能辅助你把工作安排的更好。


有规划有目标,也不会陷入一个迷茫虚度的状态,属于一个成本非常低而收益却很高的好习惯,不止工作,学习也是如此,生活亦然。


其他方面


适应能力


于我个人来说,工作强度比以前要大很多,慢慢习惯了就行,在大厂里面阿里还不是最卷的,但钱也不是最多的;工作流程方面只是有些不清楚而已,并没有什么门槛,熟悉了就行;还有阿里味儿,确实要学很多新词儿、缩写、甚至潜台词,这没啥说的,还没见到有能独善其身的😅。


适应能力也不是随便说说,有太多的人入职新公司干的怀疑人生、浑身难受而跑路的,抛开公司的问题不说,难道就没有自己的问题吗?🐶


我把适应分为两个阶段,第一个阶段就是适应工作环境,熟悉公司、同事、产品、项目;第二个阶段就是适应之后,要想想如何迎接没有新手光环的挑战,如何去展示自己、提升自己等。


技术能力


夯实自己的技术能力尤为重要,毕竟是吃饭的家伙,是做事拿结果的重要工具和手段。


在大家技术能力的基本面都不会太差的情况下,如何在技术上建立团队影响力,就是需要思考的问题。


要找准自己在团队中的定位,并在这一领域深耕,做到一提这个技术就能想到你的效果,建立技术壁垒。


其实也不只是技术,要学会举一反三,找到自己在团队的差异性,虽然不可替代性在公司离了谁都可以转的说法下有些牵强,但是可以提高自己在团队的重要性和信任度。


信息渠道


要学会拓宽自己的信息渠道,有句话叫,掌握的信息越多,决策就越正确



  • 多看,看别人在做什么,输出什么,规划什么;

  • 多聊,找合作方,相同目标的同事,甚至其他公司的朋友,互通有无;


看完聊完要把对自己有用的信息提炼出来哦。


影响力


内部的话,主要是建立同事间的信任,技术的占比相对要多一些;


外部的话,主要是在合作方那里建立一个靠谱的口碑,如果配合超预期那就更好了,我就是被几个大佬抬了一手,虽然不起决定性作用,但是也很重要。


摆脱固化


跳脱出程序员的固化思维


程序的世界非0即1,程序员的思维都是非常严谨的,这个严谨有时候可能会显得有些“死板”,在商业化的公司里面,很多事情不是能不能的问题,而是要不要的问题。而在这里面,技术大多数都不是第一要素,出发点得是业务视角、用户视角,很多技术难点、卡点,有时候甚至不用技术也能解决。


小结



  • 沟通能力:逻辑要清晰,对象要分人,还有倾听能力,和善用沟通媒介;

  • owner意识:认真负责是工作的底线,积极主动是「owner意识」更高一级的要求;

  • 向上管理:向上管理也不只是向上汇报,也是一个体现个人能力和学习的渠道;

  • 写TODO:辅助工作,治愈内耗,一个成本低而收益高的好习惯;

  • 其他方面:拓宽信息渠道,找到技术方向,简历内部外部的影响力等;



实际上不止这些,今天就到这吧。



最后


当下的市场环境无论是求职还是晋升,都挺难的,都在降本增效,寒气逼人,我能拿到晋升的门票,诚然是实力的体现,但也有运气的成分。没晋升也不一定是你的问题,放平心态,当下保住工作才是最重要的。


哔哔了这么多,可能很多同学道理也都懂,估计就难在知行合一吧...


最后送给大家一句罗翔老师的经典名言:



人生最大的痛苦,就是你知道什么是对的,但却永远做出错误的选择,知道和做到这个巨大的鸿沟,你永远无法跨越。


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

协程和协程作用域

理清子协程,父协程,协程作用域,协程生命周期,协程作用域的生命周期等的关系。 1、协程是在协程作用域内执行的轻量级并发单元。当协程的代码块执行完成时,它会挂起并返回到其父协程或顶层协程中。 2、父协程通过调用协程构建器(如 launch、async)来启动子...
继续阅读 »

理清子协程,父协程,协程作用域,协程生命周期,协程作用域的生命周期等的关系。


1、协程是在协程作用域内执行的轻量级并发单元。当协程的代码块执行完成时,它会挂起并返回到其父协程或顶层协程中。




2、父协程通过调用协程构建器(如 launchasync)来启动子协程。在启动子协程时,子协程会继承父协程的上下文(包括调度器、异常处理器等)。这意味着子协程会以与父协程相同的上下文执行。父协程可以通过 join() 方法等待子协程执行完成,以确保子协程的结果可用。


父协程可以通过取消操作来取消子协程。当父协程被取消时,它会递归地取消所有的子协程。子协程会接收到取消事件,并根据取消策略来决定如何处理取消。


父协程和子协程之间的关系可以帮助管理协程的层次结构和生命周期。通过父协程启动和取消子协程,可以有效地组织和控制协程的执行流程,实现更灵活和可靠的协程编程。

fun main() {
runBlocking {
val parentJob = launch {

val childJob = launch {
printMsg("childJob start")
delay(500)
printMsg("childJob complete")
}

childJob.join()
printMsg("parentJob complete")

}

parentJob.join()
parentJob.cancel()
printMsg("parentJob cancel")
}
}

//日志
main @coroutine#3 childJob start
main @coroutine#3 childJob complete
main @coroutine#2 parentJob complete
main @coroutine#1 parentJob cancel
Process finished with exit code 0
fun main() {
runBlocking {
val parentJob = launch {

val childJob = launch {
printMsg("childJob start")
delay(500)
printMsg("childJob complete")
}

childJob.join()
printMsg("parentJob complete")

}

//parentJob.join() <----------变化在这里
parentJob.cancel()
printMsg("parentJob cancel")
}
}

//日志
main @coroutine#1 parentJob cancel
Process finished with exit code 0



3、协程作用域(CoroutineScope)是用于协程的上下文环境,它提供了协程的启动和取消操作的上下文。协程作用域定义了协程的生命周期,并决定了协程在何时启动、在何时取消。


协程作用域是一个接口,定义了两个主要方法:




  • launch:用于启动一个新的协程。launch 方法会创建一个新的协程,并将其添加到当前协程作用域中。启动的协程将继承父协程的上下文,并在协程作用域内执行。




  • cancel:用于取消协程作用域中的所有协程。cancel 方法会发送一个取消事件到协程作用域中的所有协程,使它们退出执行。




协程作用域与协程之间的关系是协程在协程作用域内执行的。协程作用域为协程提供了上下文环境,使得协程可以访问到必要的上下文信息,例如调度器(Dispatcher)和异常处理器(ExceptionHandler)。通过在协程作用域中启动协程,可以确保协程的生命周期受到协程作用域的管理,并且在协程作用域取消时,所有协程都会被取消。

fun main() = runBlocking {
coroutineScope {
launch {
delay(1000)
printMsg("Coroutine 1 completed")
}

launch(Job()) { <---------协程2不使用协程作用域的上下文,会脱离协程作用域的控制
delay(2000)
printMsg("Coroutine 2 completed")
}
}
printMsg("Coroutine scope completed")
}

//日志
main @coroutine#2 Coroutine 1 completed 1685615335423
main @coroutine#1 Coroutine scope completed 1685615335424 <-------协程1执行完,协程作用域就执行完
Process finished with exit code 0 <---------程序退出



4、如果使用 GlobalScope.launch 创建协程,则协程会成为全局协程,它的生命周期独立于程序的其他部分。当协程的代码执行完毕后,全局协程不会自动退出,除非应用程序本身退出。因此,全局协程可以在整个应用程序的生命周期内持续执行,直到应用程序终止。


如果使用协程作用域(例如 runBlockingcoroutineScope 等)创建协程,则协程的生命周期受协程作用域的限制。当协程的代码执行完毕后,它会返回到作用域的父协程或顶级协程中,而不会自动退出。




5、当协程作用域内的所有协程执行完成后,协程作用域仍然存在,但其中的协程会被标记为完成状态。这意味着协程作用域仍然可以用于启动新的协程,但之前的协程不会再执行。


协程作用域的生命周期不仅仅依赖于其中的协程执行状态,还取决于其父协程或顶级协程的生命周期。如果协程作用域的父协程或顶级协程被取消或完成,那么协程作用域也将被取消

fun main() = runBlocking {
coroutineScope {
launch {
delay(1000)
printMsg("Coroutine 1 completed")
}

launch {
delay(2000)
printMsg("Coroutine 2 completed")
}
}
printMsg("Coroutine scope completed")
}

//日志
main @coroutine#2 Coroutine 1 completed
main @coroutine#3 Coroutine 2 completed
main @coroutine#1 Coroutine scope completed
Process finished with exit code 0 <---------程序退出

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

一个96年前端的2022年中总结 (落户,看房,还贷,被裁)

人到中年中年总是觉得很累,每天忙不完的事,操不完的心,曾今那些无忧无虑的日子似乎消失的无影无踪, 工作三年一点存款没有 落户 关于落户这个事, 就是一个很突然的想法,很突然, 得知天津有个"海河英才计划",只要是本科生, 就比较容易落户,所以就想着试一试, ...
继续阅读 »


人到中年中年总是觉得很累,每天忙不完的事,操不完的心,曾今那些无忧无虑的日子似乎消失的无影无踪, 工作三年一点存款没有



落户


关于落户这个事, 就是一个很突然的想法,很突然, 得知天津有个"海河英才计划",只要是本科生, 就比较容易落户,所以就想着试一试, 心想起码这个是一个阶级的跨越(农村-->城市), 然后就在网上各种搜索,问一些了解的朋友,看看怎么操作, 最后发现好多人说有北京社保,在天津落户会被查出来, 然后就被拉入黑名单了. 最后为了稳妥还是找了一个中介帮忙操作了一下,花了1w, 等我办完,不久就有我的朋友自己办的三无人员落户, 一分钱没花. 唉😌!!!!!


看房


一开始在安居客上看, 因为穷, 基本也没啥可选择性, 一筛选也就那么几个, 然后就联系了一个销售去看房, 不得不说这个销售真的可以, 不知道他们能赚多少钱哈, 但是服务是真的不错, 一到了高铁站他们就去接你,还请你吃饭, 带着你看,看完之后还把你送到高铁站. 想着一开始就看西青和北辰的,但是西青的都好贵, 北辰好像会好点, 然后就去北辰看, 销售说他觉得武清也不错, 就带着去武清也看了, 看完之后, 心里比较了一下, 感觉被武清的哪个样板间和户型深深吸引了,并且价格也比较合适些, 然后我回到北京一周左右吧, 就跟那个销售说,打算买武清哪个, 然后然后我父母给我拿了20w, 剩下的就是我自己的,还借了朋友一些, 首付了40多, 然后就打印征信, 一顿签字就买了. 感觉很随意, 感觉买房就跟买菜似的, 不过还是有一点区别的, 买菜之后不会让我身无分文, 买房会😭. 现在就是每个月5000多贷款"真爽".


image.png


学习



  1. 缺失了刚毕业那会的激情,刚毕业那会,每天下班还会去学习, 刷视频, 看文章, 现在下班回家已经不想再打开电脑了. 刷视频(此视频非彼视频)

  2. 今年也学习了一些新的东西摸鱼之间,刷了一些课程,<破解JavaScript高级玩法,成为精通JS的原生专家> <Vue3全家桶>

  3. React技术栈是我工作一直使用的,也会持续性的学习一些,每天刷刷Medium和掘金

  4. 深度学习Nginx,进行了一半了,以前对nginx只停留在使用的层面

  5. 上半年在公司分享了一下架手架的原理以及实践

  6. 英语的学习说实话真的有点三天打鱼两天晒网了, 好在现在有一半了


img_1.png


工作


21年年底, 老东家北京这边合作的项目,终止了,然后面临了裁员, 不过当初也确实有了想跳槽的想法了,本来想,等到年终奖发了,就提离职. 没想到提前到来了, 给了正常的赔偿(n+1) 拿了三个月的赔偿, 正好月底, 算上本月的薪资,还有一些调休啥的 加起来一共是4个月, 感觉还挺爽的, 因为大概还有个20天左右吧, 就快过年了, 然后在回家和找工作抉择了一下, 决定先找找工作,然后就开始学习在掘金上查看面经, 感觉都是各种源码, 给我搞的有点懵, 毕竟缺钱嘛. 先后面试了一些公司: 金山, 58, 携程, 欢聚, 等等; 说实话,这段时间招人的还挺多的. 所以我很快就入职了, hr问我年前能入职吗? 其实那段时间疫情严重了, 老家那边也不让北京的回去, 所以在过年的前一周我入职了, (没钱的人不配拥有假期😭) . 唉, 第一次自己在外地过年.


关于兼职:



  • 今年和朋友一块干了一个公司的官网,本来也没打算要钱的,最后老板一人给了一张京东e卡

  • 还干了一个审核ppt的工作,一个ppt给150,不用改, 就说哪里写的不合适, 不过这个活有点恶心, 每个人理解不一样, 每次我这边审核后, 拿去交付,还是很多问题


生活



  1. 上半年感觉一直有疫情断断续续,大家都比较封闭,也没出去玩过, 偶尔和朋友去爬个山. 5, 6月还居家办公了好久, 记不太清了,应该得有一个月

  2. 从去年十月一到现在一直没有回过家, 有些想回去看看, 但是最近每个周末都有事😞

  3. 和女朋友去看过两次脱口秀, 感觉现场的感觉还是很棒的, 比电视看好太多

  4. 因为对象住他们单位的宿舍, 所以我自己平常下班也懒得收拾屋子, 只有礼拜天,才会大扫除一下, 或者对象来的时候😁

  5. 前段时间迷上了王者,以前从来不怎么玩游戏的, 熬夜打游戏, 导致生活节奏有点乱, 每天的精神状态也不如以前,正在积极调整. 但是吧,我告诉你们我的云中君玩的贼6的 不服来战哦

  6. 养了一只鹦鹉, 刚来还不是很好看, 现在尾巴长长的了


image.png


下半年flag



  • 完成Nginx的深度学习

  • 希望能出去转一圈, 看看外面的世界

  • 继续背单词学习

  • 看看车车, 目前感觉Crv和宋大妈还不错

  • 能再进行一次有价值的分享


作者:nanfeiyan
来源:juejin.cn/post/7124511406099005471
收起阅读 »

箭头函数函数是否有原型

web
问题:箭头函数是否有原型 今天在博客重构之余,看到某个前端群有群友这样问: 面试被问到了一个题,箭头函数是否有原型 大家觉得有吗? 首先不说它是不是,我们来回顾一下 箭头函数 是什么,原型 又是什么。 箭头函数是什么 箭头函数表达式的语法比函数表达式更简...
继续阅读 »

问题:箭头函数是否有原型


今天在博客重构之余,看到某个前端群有群友这样问:



面试被问到了一个题,箭头函数是否有原型
大家觉得有吗?



首先不说它是不是,我们来回顾一下 箭头函数 是什么,原型 又是什么。


箭头函数是什么



箭头函数表达式的语法比函数表达式更简洁,并且没有自己的this,arguments,super或new.target。箭头函数表达式更适用于那些本来需要匿名函数的地方,并且它不能用作构造函数。



developer.mozilla.org/zh-CN/docs/…



引入箭头函数有两个方面的作用:更简短的函数并且不绑定this。



一般来说如果问题是函数是否有原型时,那么可以好不犹豫的回答说是,但因为箭头函数的特殊性导致了答案的不确定性。


原型是什么



当谈到继承时,JavaScript 只有一种结构:对象。
每个对象(object)都有一个私有属性指向另一个名为原型(prototype)的对象。
原型对象也有一个自己的原型,层层向上直到一个对象的原型为 null。
根据定义,null 没有原型,并作为这个原型链(prototype chain)中的最后一个环节。



developer.mozilla.org/zh-CN/docs/…



一个 Function 对象在使用 new 运算符来作为构造函数时,会用到它的 prototype 属性。它将成为新对象的原型。



developer.mozilla.org/zh-CN/docs/…


什么是原型在 mdn 中已经讲得很清楚了,也就是对象的prototype


试验


所以按理来说对象都有原型,那么试试就知道了。


首先看看箭头函数的原型:


const a = () => {};

console.log(a.prototype); // undefined

在浏览器控制台可以把上述代码执行一遍,可以发现结果是 undefined。


那么是否就说明箭头函数没有原型了呢?别急继续往下看。


const a = () => {};

console.log(a.prototype); // undefined

console.log(a.__proto__); // ƒ () { [native code] }

我们可以看到 a.__proto__ 是一个 native function


那么 __proto__ 又是什么呢?


__proto__是什么



Object.prototype (en-US) 的 __proto__ 属性是一个访问器属性(一个 getter 函数和一个 setter 函数), 暴露了通过它访问的对象的内部[[Prototype]] (一个对象或 null)。




__proto__ 的读取器 (getter) 暴露了一个对象的内部 [[Prototype]] 。对于使用对象字面量创建的对象,这个值是 Object.prototype (en-US)。对于使用数组字面量创建的对象,这个值是 Array.prototype。对于 functions,这个值是 Function.prototype。对于使用 new fun 创建的对象,其中 fun 是由 js 提供的内建构造器函数之一 (Array, Boolean, Date, Number, Object, String 等等),这个值总是 fun.prototype。对于用 JS 定义的其他 JS 构造器函数创建的对象,这个值就是该构造器函数的 prototype 属性。




__proto__ 属性是 Object.prototype (en-US) 一个简单的访问器属性,其中包含了 get(获取)和 set(设置)的方法,任何一个 __proto__ 的存取属性都继承于 Object.prototype (en-US),但一个访问属性如果不是来源于 Object.prototype (en-US) 就不拥有 __proto__ 属性,譬如一个元素设置了其他的 __proto__ 属性在 Object.prototype (en-US) 之前,将会覆盖原有的 Object.prototype (en-US)。



developer.mozilla.org/zh-CN/docs/…


看解释可能有人不大理解,举个例子 🌰:


function F() {}
const f = new F();

console.log(f.prototype); // undefined
console.log(f.__proto__ === F.prototype); // true
console.log(f.__proto__.constructor); // F(){}

new F 也即是 F 的实例 ff__proto__ 属性指向 Fprototype


由此可以得出实例与原型的关系:new function得到实例,实例的 __proto__ 又指向function的原型,原型的构造器指向原函数。


结论


好了,理解了什么是 __proto__ 后,我们回到原来的问题上:箭头函数函数是否有原型?


通过上述的代码我们可以知道箭头函数就是Function的实例,如果你觉得不是,那么请看下面的例子:


const a = () => {};

console.log(a instanceof Function); // true
console.log(a.__proto__ === Function.prototype); // true
console.log(a.prototype); // undefined

所以最终得出两种结论:如果按中文语意那么箭头函数是Function的实例,而依据实例与原型的关系,它是有原型的;如果原型仅仅只说的是prototype,那么结论就是没有



注:以上代码结果都是在chrome113下运行得出



如有错误,欢迎指正~


参考


箭头函数
developer.mozilla.org/zh-CN/docs/…


构造器
developer.mozilla.org/zh-CN/docs/…


原型链
developer.mozilla.org/zh-CN/docs/…


Function.prototype
developer.mozilla.org/zh-CN/docs/…


__proto__
developer.mozilla.org/zh-CN/

docs/…

收起阅读 »

关于晋升的一点思考

晋升是一个极好的自我review的机会,不,应该是最好,而且没有之一。 晋升是最好的自我review的一次机会,不管有没有晋升成功,只要参加了晋升,认真准备过PPT,就已经包赚不赔了。 总的来说,晋升的准备工作充分体现出了——功夫在平时。平时要是没有两把刷子,...
继续阅读 »

晋升是一个极好的自我review的机会,不,应该是最好,而且没有之一。

晋升是最好的自我review的一次机会,不管有没有晋升成功,只要参加了晋升,认真准备过PPT,就已经包赚不赔了。


总的来说,晋升的准备工作充分体现出了——功夫在平时。平时要是没有两把刷子,光靠答辩准备的一两个月,是绝无可能把自己“包装”成一个合格的候选人的。

下面整体剖析一下自己在整个准备过程中的观察、思考、判断、以及做的事情和拿到的结果。


准备工作


我做的第一件事情并不是动手写PPT,而是搜集信息,花了几天在家把网上能找到的所有关于晋升答辩的文章和资料全撸了一遍,做了梳理和总结。


明确了以下几点:



  • 晋升是在做什么

  • 评委在看什么

  • 候选人要准备什么

  • 评判的标准是什么

  • 常见的坑有哪些


首先要建立起来的,是自己对整个晋升的理解,形成自己的判断。后面才好开展正式的工作。



写PPT


然后开始进入漫长而又煎熬的PPT准备期,PPT的准备又分为四个子过程,并且会不断迭代进行。写成伪代码就是下面这个样子。


do {
确认思路框架;
填充内容细节;
模拟答辩;
获取意见并判断是否还需要修改;
} while(你觉得还没定稿);

我的PPT迭代了n版,来来回回折腾了很多次,思路骨架改了4次,其中后面三次均是在准备的后半段完成的,而最后一次结构大改是在最后一周完成的。这让我深深的觉得前面准备的1个月很多都是无用功。


迭代,迭代,还是迭代


在筹备的过程中,有一个理念是我坚持和期望达到的,这个原则就是OODA loop ( Boyd cycle)


OODA循环是美军在空战中发展出来的对敌理论,以美军空军上校 John Boyd 为首的飞行员在空战中驾驶速度慢火力差的F-86军刀,以1:10的击落比完胜性能火力俱佳的苏联米格-15。而Boyd上校总结的结论就是,不是要绝对速度快,而是要比对手更快的完成OODA循环


而所谓的OODA循环,就是指 observe(观察)–orient(定位)–decide(决策)–act(执行) 的循环,是不是很熟悉,这不就是互联网的快速迭代的思想雏形嘛。


相关阅读 what is OODA loop

wiki.mbalib.com/wiki/包以德循环 (from 智库百科)

en.wikipedia.org/wiki/OODA_l… (from Wikipedia)


看看下图,PPT应该像第二排那样迭代,先把框架确定下来,然后找老板或其他有经验的人对焦,框架确定了以后再填充细节。如果一开始填充细节(像第一排那样),那么很有可能越改越乱,最后一刻还在改PPT。


btw,这套理论对日常工作生活中的大部分事情都适用。


一个信息论的最新研究成果


我发现,程序员(也有可能是大部分人)有一个倾向,就是show肌肉。明明很简单明了的事情,非要搞得搞深莫测,明明清晰简洁的架构,非要画成“豆腐宴”。


晋升述职核心就在做一件事,把我牛逼的经历告诉评委,并让他们相信我牛逼。


所以,我应该把各种牛逼的东西都堆到PPT里,甚至把那些其实一般的东西包装的很牛逼,没错吧?


错。


这里面起到关键作用的是 “让他们相信我牛逼” ,而不是“把我牛逼的故事告诉评委”。简单的增大的输出功率是不够的,我要确保评委能听进去并且听懂我说的东西,先保证听众能有效接收,再在此基础上,让听众听的爽。


How?


公式:喜欢 = 熟悉 + 意外


从信息论的角度来看,上面的公式说的就是旧信息和新信息之间要搭配起来。那么这个搭配的配比应该是多少呢?


这个配比是15.87% ——《科学美国人》


也就是说,你的内容要有85%是别人熟悉的,另外15%是能让别人意外的,这样就能达到最佳的学习/理解效果。这同样适用于心流、游戏设计、神经网络训练。所以,拿捏好这个度,别把你的PPT弄的太高端(不知所云),也别搞的太土味(不过尔尔)。


能够否定自己,是一种能力


我审视自己的时候发现,很多时候,我还保留一张PPT或是还持续的花心思做一件事情,仅仅是因为——舍不得。你有没有发现,我们的大脑很容易陷入“逻辑自洽”中,然后越想越对,越想越兴奋。


千万记得,沉没成本不是成本,经济学里成本的定义是放弃了的最大价值,它是一个面向未来的概念,不面向过去。


能够否定和推翻自己,不留恋于过去的“成就” ,可以帮助你做出更明智的决策。


我一开始对好几页PPT依依不舍,觉得自己做的特牛逼。但是后来,这些PPT全被我删了,因为它们只顾着自己牛逼,但是对整体的价值却不大,甚至拖沓。


Punchline


Punchline很重要,这点我觉得做的好的人都自觉或不自觉的做到了。想想,当你吧啦吧啦讲的时候,评委很容易掉线的,如果你没有一些点睛之笔来高亮你的成果和亮点的话,别人可能就糊里糊涂的听完了。然后呢,他只能通过不断的问问题来挖掘你的亮点了。


练习演讲


经过几番迭代以后,PPT可以基本定稿,这个时候就进入下一个步骤,试讲。


可以说,演讲几乎是所有一线程序员的短板,很多码农兄弟们陪电脑睡的多了,连“人话”有时候都讲不利索了。我想这都要怪Linus Torvalds的那句


Talk is cheap. Show me the code.


我个人的经验看来,虽然成为演讲大师长路漫漫不可及,但初级的演讲技巧其实是一个可以快速习得的技能,找到几个关键点,花几天时间好好练几遍就可以了,演讲要注意的点主要就是三方面:



  • 形象(肢体语言、着装等)

  • 声音(语速、语调、音量等)

  • 文字(逻辑、关键点等)



演讲这块,我其实也不算擅长,我把仅有的库存拿出来分享。


牢记表达的初衷


我们演讲表达,本质上是一个一对多的通信过程,核心的目标是让评委或听众能尽可能多的接受到我们传达的信息


很多程序员同学不善于表达,最明显的表现就是,我们只管吧啦吧啦的把自己想说的话说完,而完全不关心听众是否听进去了。


讲内容太多


述职汇报是一个提炼的过程,你可能做了很多事情,但是最终只会挑选一两件最有代表性的事情来展现你的能力。有些同学,生怕不能体现自己的又快又猛又持久,在PPT里塞了太多东西,然后又讲不完,所以只能提高语速,或者囫囵吞枣、草草了事。


如果能牢记表达的初衷,就不应该讲太多东西,因为听众接收信息的带宽是有限的,超出接收能力的部分,只会转化成噪声,反而为你的表达减分。


过度粉饰或浮夸


为了彰显自己的过人之处,有时候会自觉或不自觉的把不是你的工作也表达出来,并没有表明哪些是自己做的,哪些是别人做的。一旦被评委识破(他本身了解,或问问题给问出来了),那将会让你陈述的可信度大打折扣。


此外,也表达的时候也不要过分的浮夸或张扬,一定的抑扬顿挫是加分的,但过度浮夸会让人反感。


注意衔接


作为一个演讲者,演讲的逻辑一定要非常非常清晰,让别人能很清晰明了的get到你的核心思路。所以在演讲的时候要注意上下文之间的衔接,给听众建设心理预期:我大概会讲什么,围绕着什么展开,分为几个部分等等。为什么我要强调这个点呢,因为我们在演讲的时候,很容易忽略听众的感受,我自己心里有清楚的逻辑,但是表达的时候却很混乱,让人一脸懵逼。


热情


在讲述功能或亮点的时候,需要拿出自己的热情和兴奋,只有激动人心的演讲,才能抓住听众。还记得上面那个分布图吗?形象和声音的占比达到93%,也就是说,你自信满满、热情洋溢的说“吃葡萄不吐葡萄皮”,也能打动听众。


第一印象


这个大家都知道,就是人在最初形成的印象会对以后的评价产生影响 。

这是人脑在百万年进化后的机制,可以帮助大脑快速判断风险和节省能耗——《思考,快与慢》

评委会刻意避免,但是人是拗不过基因的,前五分钟至关重要,有经验的评委听5分钟就能判断候选人的水平,一定要想办法show出你的与众不同。可以靠你精心排版的PPT,也可以靠你清晰的演讲,甚至可以靠一些小 trick(切勿生搬硬套)。


准备问题


当PPT准备完,演讲也练好了以后,不出意外的话,应该没几天了。这个时候要进入最核心关键的环节,准备问题。


关于Q&A环节,我的判断是,PPT和演讲大家都会精心准备,发挥正常的话都不会太差。这就好像高考里的语文,拉不开差距,顶多也就十几分吧。而Q&A环节,则是理综,优秀的和糟糕的能拉开50分的差距,直接决定总分。千万千万不可掉以轻心。


问题准备我包含了这几个模块:



  • 业务:业务方向,业务规划,核心业务的理解,你做的事情和业务的关系,B类C类的差异等

  • 技术:技术难点,技术亮点,技术选型,技术方案的细节,技术规划,代码等

  • 数据:核心的业务数据,核心的技术指标,数据反映了什么等等

  • 团队:项目管理经验,团队管理经验

  • 个人:个人特色,个人规划,自己的反思等等


其中业务、技术和数据这三块是最重要的,需要花80%的精力去准备。我问题准备大概花了3天时间,整体还是比较紧张的。准备问题的时候,明显的感觉到自己平时的知识储备还不太够,对大业务方向的思考还不透彻,对某些技术细节的把控也还不够到位。老话怎么说的来着,书到用时方恨少,事非经过不知难。


准备问题需要全面,不能有系统性的遗漏。比如缺少了业务理解或竞品分析等。


在回答问题上,也有一些要点需要注意:


听清楚再回答


问题回答的环节,很多人会紧张,特别是一两道问题回答的不够好,或气氛比较尴尬的时候,容易大脑短路。这个时候,评委反复问你一个问题或不断追问,而自己却觉得“我说的很清楚了呀,他还没明白吗”。我见过或听说过很多这样的案例,所以这应该是时有发生的。


为了避免自己也踩坑,我给自己定下了要求,一定要听清楚问题,特别是问题背后的问题。如果觉得不清楚,就反问评委进行doubel check。并且在回答的过程中,要关注评委的反映,去确认自己是否答到点子上了。


问题背后的问题


评委的问题不是天马行空瞎问的,问题的背后是在考察候选人的某项素质,通过问题来验证或挖掘候选人的亮点。这些考察的点都是公开的,在Job Model上都有。


我认为一个优秀的候选人,应当能识别出评委想考察你的点。找到问题背后的问题,再展开回答,效果会比单纯的挤牙膏来的好。


逻辑自洽、简洁明了


一个好的回答应该是逻辑自洽的。这里我用逻辑自洽,其实想说的是你的答案不一定要完全“正确”(其实往往也没有标准答案),但是一定不能自相矛盾,不能有明显的逻辑漏洞。大部分时候,评委不是在追求正确答案,而是在考察你有没有自己的思考和见解。当然,这种思考和见解几乎都是靠平时积累出来的,很难临时抱佛脚。


此外,当你把逻辑捋顺了以后,简洁明了的讲出来就好了,我个人是非常喜欢能把复杂问题变简单的人的。一个问题的本质是什么,核心在那里,关键的几点是什么,前置条件和依赖是什么,需要用什么手段和资源去解决。当你把这些东西条分缕析的讲明白以后,不用再多啰嗦一句,任何人都能看出你的牛逼了。


其他


心态调整


我的心态经历过过山车般的起伏,可以看到



在最痛苦最难受的时候,如果身边有个人能理解你陪伴你,即使他们帮不上什么忙,也是莫大的宽慰。如果没有这样的人,那只能学会自己拥抱自己,自己激励自己了。


所以,平时对自己的亲人好一点,对朋友们好一点,他们绝对是你人生里最大的财富。


关于评委


我从一开始就一直觉得评委是对手,是来挑战你的,对你的汇报进行证伪。我一直把晋升答辩当作一场battle来看待,直到进入考场的那一刻,我还在心理暗示,go and fight with ths giants。


但真实的经历以后,感觉评委更多的时候并不是站在你的对立面。评委试图通过面试找到你的一些闪光点,从而论证你有能力晋升到下一个level。从这个角度来讲,评委不但不是“敌人”,更像是友军一般,给你输送弹药(话题)。


一些教训




  • 一定要给自己设置deadline,并严格执行它。如果自我push的能力不强,就把你的deadline公开出来,让老板帮你监督。




  • 自己先有思考和判断,再广开言路,不要让自己的头脑成为别人思想的跑马场。




  • 坚持OODA,前期千万不要扣细节。这个时候老板和同事是你的资源,尽管去打扰他们吧,后面也就是一两顿饭的事情。




附件


前期调研



参考文章


知乎


作者:酒红
来源:juejin.cn/post/7240805459288162360
收起阅读 »

程序员创业:从技术到商业的转变

作为一名程序员,我们通常会聚焦于编程技能和技术能力的提升,这也是我们日常工作的主要职责。但是,随着技术的不断发展和市场的变化,仅仅依靠技术能力已经不足以支撑我们在职场上的发展和求职竞争力了。所以,作为一名有远大理想的程序员,我们应该考虑创业的可能性。 为什么程...
继续阅读 »

作为一名程序员,我们通常会聚焦于编程技能和技术能力的提升,这也是我们日常工作的主要职责。但是,随着技术的不断发展和市场的变化,仅仅依靠技术能力已经不足以支撑我们在职场上的发展和求职竞争力了。所以,作为一名有远大理想的程序员,我们应该考虑创业的可能性。


为什么程序员要创业?


创业其实并非只适用于商学院的毕业生或者有创新理念的企业家。程序员在业内有着相当高的技术储备和市场先知,因此更容易从技术角度前瞻和切入新兴市场,更好地利用技术储备来实现创业梦想。


此外,创业可以释放我们的潜力,同时也可以让我们找到自己的定位和方向。在创业的过程中,我们可能会遇到各种挑战和困难,但这些挑战也将锻炼我们的意志力和决策能力,让我们更好地发挥自己的潜力。


创业需要具备的技能


作为一名技术人员,创业需要具备更多的技能。首先是商业和运营的技能:包括市场分析、用户研究、产品策划、项目管理等。其次是团队管理和沟通能力,在创业的过程中,人才的招聘和管理是核心问题。


另外,还需要具备跨界合作的能力,通过开放性的合作与交流,借助不同团队的技术和资源,完成创业项目。所以我们应该将跨界合作看作是创业过程中的重要选择,选择和加强自己的跨界交流和合作能力,也能为我们的企业注入活力和创新精神。


如何创业?


从技术到商业的转变,从最初想法的诞生到成熟的企业的创立,都需要一个创业的路线图。以下是一些需要注意的事项:




  1. 研究市场:了解市场趋势,分析需求,制定产品策略。可以去参加行业论坛,争取到专业意见和帮助。




  2. 制定商业计划:包括产品方案、市场营销、项目管理、团队建设等。制定一个系统的商业计划是投资者和团队成员对创业企业的认可。




  3. 招募团队:由于我们一般不是经验丰富的企业家,团队的选择尤为重要。要找的不仅要是技能和经验匹配的团队,更要找能一起携手完成创业项目的合作者。




  4. 行动计划:从实现规划步入到实战行动是创业项目的关键。按部就班地完成阶段性任务,控制实施进度和途中变化,在完成一个阶段后可以重新评估计划。




  5. 完成任务并分析:最后,团队成员需要根据企业进展,完整阶段性的目标,做自己的工作。及时完成考核任务并一起分享数据分析、事件解决和项目总结等信息,为项目下一阶段做出准确预测。




结语


创业是一条充满挑战性和机遇的路线,也是在我们的技术和业务的进一步升级中一条非常良好的通道。越来越多的技术人员意识到了自己的潜力,开始考虑自己创业的可能性。只要学会逐步掌握创业所需的技能和知识,并制订出详细的创业路线图,大可放手去尝试,才能最终实现

作者:郝学胜
来源:juejin.cn/post/7240465997002047547
自己心中的创业梦想。

收起阅读 »

App高级感营造之 高斯模糊

效果 类似毛玻璃,或者马赛克的效果。我们可以用它来提升app背景的整体质感,或者给关键信息打码。 源代码 import 'dart:ui'; import 'package:flutter/material.dart'; void main() { ...
继续阅读 »

效果


类似毛玻璃,或者马赛克的效果。我们可以用它来提升app背景的整体质感,或者给关键信息打码。


高斯模糊1.gif


高斯模糊2.gif


源代码


import 'dart:ui';

import 'package:flutter/material.dart';

void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {
const MyApp({super.key});

// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}

class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});

final String title;

@override
State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
// 高斯模糊的第一种写法 ImageFiltered 包裹要模糊的组件

/// 将子组件进行高斯模糊
/// [child] 要模糊的子组件
Widget _imageFilteredWidget1({required Widget child, double sigmaValue = 1}) {
return ImageFiltered(
imageFilter: ImageFilter.blur(sigmaX: sigmaValue, sigmaY: sigmaValue),
child: child,
);
}

/// 使用第一种模糊方式的案例
Widget _demo1() {
return Container(
padding: const EdgeInsets.all(50),
color: Colors.blue.shade100,
width: double.infinity,
child: Column(
children: [
_imageFilteredWidget1(
child: SizedBox(
width: 150,
child: Image.asset(
"assets/images/bz1.jpg",
fit: BoxFit.fitHeight,
),
),
),
const SizedBox(height: 100),
_imageFilteredWidget1(
child: const Text(
"测试高斯模糊",
style: TextStyle(fontSize: 30, color: Colors.blueAccent),
),
sigmaValue: 2)
],
),
);
}

/// 利用 BackdropFilter 做高斯模糊
_backdropFilterWidget2({
required Widget child,
double sigmaValueX = 1,
double sigmaValueY = 1,
}) {
return ClipRect(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: sigmaValueX, sigmaY: sigmaValueY),
child: child,
),
);
}

///
Widget _demo2() {
return SizedBox(
width: double.infinity,
height: double.infinity,
child: Stack(
alignment: Alignment.center,
children: [
Positioned.fill(
child: Image.asset(
"assets/images/bz1.jpg",
fit: BoxFit.fill,
),
),
Positioned(
child: _backdropFilterWidget2(
sigmaValueX: _sigmaValueX,
sigmaValueY: _sigmaValueY,
child: Container(
width: MediaQuery.of(context).size.width - 100,
height: MediaQuery.of(context).size.height / 2,
alignment: Alignment.center,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: const Color(0x90ffffff),
),
child: const Text(
"高斯模糊",
style: TextStyle(fontSize: 30, color: Colors.white),
),
)),
top: 20,
),
_slider(
bottomMargin: 200,
themeColors: Colors.yellow,
title: '横向模糊度',
valueAttr: _sigmaValueX,
onChange: (double value) {
setState(() {
_sigmaValueX = value;
});
},
),
_slider(
bottomMargin: 160,
themeColors: Colors.blue,
title: '纵向模糊度',
valueAttr: _sigmaValueY,
onChange: (double value) {
setState(() {
_sigmaValueY = value;
});
},
),
_slider(
bottomMargin: 120,
themeColors: Colors.green,
title: '同时调整:',
valueAttr: _sigmaValue,
onChange: (double value) {
setState(() {
_sigmaValue = value;
_sigmaValueX = value;
_sigmaValueY = value;
});
},
),
],
),
);
}

Widget _slider({
required String title,
required double bottomMargin,
required Color themeColors,
required double valueAttr,
required ValueChanged<double>? onChange,
}) {
return Positioned(
bottom: bottomMargin,
child: Row(
children: [
Text(title, style: TextStyle(color: themeColors, fontSize: 18)),
SliderTheme(
data: SliderThemeData(
trackHeight: 20,
activeTrackColor: themeColors.withOpacity(.7),
thumbColor: themeColors,
inactiveTrackColor: themeColors.withOpacity(.4)
),
child: SizedBox(
width: MediaQuery.of(context).size.width * 0.5,
child: Slider(
value: valueAttr,
min: 0,
max: 10,
onChanged: onChange,
),
),
),
SizedBox(
width: 50,
child: Text('${valueAttr.round()}',
style: TextStyle(color: themeColors, fontSize: 18)),
),
],
),
);
}

double _sigmaValueX = 10;
double _sigmaValueY = 10;

double _sigmaValue = 10;

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: _demo2(),
);
}
}

实现原理


实现高斯模糊,在flutter中有两种方式:


ImageFiltered


它可以对其包裹的子组件施加高斯模糊,需要传入 ImageFilter 控制模糊程度,分为X Y两个方向的模糊,实际上就是对图片进行拉伸,数字越大,模糊效果越大。


/// 将子组件进行高斯模糊
/// [child] 要模糊的子组件
Widget _imageFilteredWidget1({required Widget child, double sigmaValue = 1}) {
return ImageFiltered(
imageFilter: ImageFilter.blur(sigmaX: sigmaValue, sigmaY: sigmaValue),
child: child,
);
}

BackDropFilter


同样需要一个 ImageFilter参数控制模糊度,与 ImageFilter的区别是,它会对它覆盖的组件整体模糊。
所以如果我们需要对指定的子组件进行模糊的话,需要再包裹一个ClipRect裁切。


/// 利用  BackdropFilter 做高斯模糊
_backdropFilterWidget2({required Widget child, double sigmaValue = 1}) {
return ClipRect(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaY: sigmaValue, sigmaX: sigmaValue),
child: child,
),
);
}

由于 BackdropFilter 会对其子组件进行图形处理,所以其子组件可能会变得更加消耗性能。因此,需要谨慎使用 BackdropFilter 组件。


作者:拳布离手
来源:juejin.cn/post/7239631010429108280
收起阅读 »

不用递归也能实现深拷贝

web
前言 在现代化的 Web 开发中,深拷贝是一个常见的数据处理需求,它允许我们复制并操作数据,而不影响原始数据。然而,使用递归实现深拷贝的方法可能对性能产生负面影响,特别是在处理大规模数据时。因此,越来越多的前端开发者开始关注另一种不用递归的方式实现深拷贝。 深...
继续阅读 »

前言


在现代化的 Web 开发中,深拷贝是一个常见的数据处理需求,它允许我们复制并操作数据,而不影响原始数据。然而,使用递归实现深拷贝的方法可能对性能产生负面影响,特别是在处理大规模数据时。因此,越来越多的前端开发者开始关注另一种不用递归的方式实现深拷贝。


深拷贝的实现方式


我们先来看看常用的深拷贝的实现方式


JSON.parse(JSON.stringify())


利用 JSON.stringify 将对象转成 JSON 字符串,再用 JSON.parse 把字符串解析成新的对象实现深拷贝。


这种方式代码简单,常用于深拷贝简单类型的对象。


在复杂类型的对象上会有问题:



  1. undefined、function、symbol 会被忽略或者转为 null(数组中)

  2. 时间对象变成了字符串

  3. RegExp、Error 对象序列化的结果将只得到空对象

  4. NaN、Infinity 和-Infinity,则序列化的结果会变成 null

  5. 对象中存在循环引用的情况也无法正确实现深拷贝


函数库 lodash 的 cloneDeep 方法


这种方式使用简单,而且 cloneDeep 内部是使用递归方式实现深拷贝,因此不会有 JSON 转换方式的问题;但是需要引入函数库 js,为了一个函数而引入一个库总感觉不划算。


递归方法


声明一个函数,函数中变量对象或数组,值为基本数据类型赋值到新对象中,值为对象或数组就调用自身函数。


// 手写深拷贝
function deepCopy(data) {
const map = {
"[object Number]": "number",
"[object Boolean]": "boolean",
"[object String]": "string",
"[object Function]": "function",
"[object Array]": "array",
"[object Object]": "object",
"[object Null]": "null",
"[object Undefined]": "undefined",
"[object Date]": "date",
"[object RegExp]": "regexp",
};
var copyData;
var type = map[Object.prototype.toString.call(data)];
if (type === "array") {
copyData = [];
data.forEach((item) => copyData.push(deepCopy(item)));
} else if (type === "object") {
copyData = {};
for (var key in data) {
copyData[key] = deepCopy(data[key]);
}
} else {
copyData = data;
}
return copyData;
}

递归方式结构清晰将任务拆分成多个简单的小任务执行,可读性强,但是效率低,调用栈可能会溢出,函数每次调用都会在内存栈中分配空间,而每个进程的容量是有限的,当调用的层次太多时,就会超出栈的容量,从而导致溢出。


深拷贝其实是对树的遍历过程


嵌套对象很像下面图中的树。


Untitled.png


递归的思路是遍历 1 对象的属性判断是否是对象,发现属性 2 是一个对象在调用函数本身来遍历 2 对象的属性是否是对象如此反复知道变量 9 对象。


Untitled 1.png


9 对象的属性中没有对象然后返回 5 对象去遍历其他属性是否是对象,没有再返回 2 对象,最后返回到 1 对象发现其 3 属性是一个对象。


Untitled 2.png


Untitled 3.png


最后在找 4 对象。


Untitled 4.png


可以看到递归其实是对树的深度优先遍历。


那么不用递归可以实现树的深度优先遍历么?


答案是肯定的。


不用递归实现深度优先遍历深拷贝


观察递归算法可以发现实现深度优先遍历主要是两个点



  1. 利用栈来实现深度优先遍历的节点顺序

  2. 记录哪些节点已经走过了


第一点可以用数组来实现栈


const stack = [source]
while (stack.length) {
const data = stack.pop()
for (let key in data) {
if (typeof source[key] === "object") {
stack.push(data[key])
}
}
}

这样就能把所有的嵌套对象都放入栈中,就可以遍历所有的嵌套子对象。


第二点因为发现对象属性值是对象时会中断当前对象的属性遍历改去遍历子对象,因此要记录对象的遍历的状态。由于 for in 的遍历是无序的即使用一个变量存 key 也没办法知道哪些 key 已经遍历过了,需要一个数组记录所有遍历过的属性。


这里还有另一种简单的方法就是用 Object.keys 来获取对象的 key 数组放到 stack 栈中。


const stack = [...Object.keys(source).map(key => ({ key, source: source }))]
while (stack.length) {
const { key, data } = stack.pop()
if (typeof data[key] === "object") {
stack.push(...Object.keys(data[key]).map(k => ({ key: k, data: data[key] })))
}
}

这样 stack 中深度优先遍历的遍历的对象顺序也记录其中。


这里将代码优化下, 把 Object.keys 换成 Object.entries 更为精简


const stack = [...Object.entries(source)]
while (stack.length) {
const [ key, value ] = stack.pop()
if (typeof value === "object") {
stack.push(...Object.entries(value))
}
}

遍历完成下一步就是创建一个新的对象进行赋值。


const stack = [...Object.entries(source)]
const result = {}
const cacheMap = {}
let id = 0
let cache
while (stack.length) {
const [key, value, id] = stack.pop()
if (id != undefined && cacheMap[id]) {
cache = cacheMap[id]
} else {
cache = result
}
if (typeof value === "object") {
cacheMap[id] = cache[key] = {}
stack.push(...Object.entries(value).map(item => [...item, id++]))
} else {
cache[key] = value
}
}
return result

因为对象时引用类型,因此可以通过 cacheMap[id] 来快速访问 result 的嵌套对象。


代码还可以优化:


cacheMap 可以用 WeakMap 来声明减少 id 的声明:


const stack = [...Object.entries(source)]
const result = {}
const cacheMap = new WeakMap()
let cache
while (stack.length) {
const [key, value, parent] = stack.pop()
if (cacheMap.has(parent)) {
cache = cacheMap.get(parent)
} else {
cache = result
}
if (typeof value === "object") {
cache[key] = {}
cacheMap.set(value, cache[key])
stack.push(...Object.entries(value).map(item => [...item, value]))
} else {
cache[key] = value
}
}
return result

stack 中的数组项中的 parent 可以换成目标对象:


const result = {}
const stack = [...Object.entries(source).map(item => [...item, result])]
while (stack.length) {
const [key, value, target] = stack.pop()
if (typeof value === "object") {
target[key] = {}
stack.push(...Object.entries(value).map(item => [...item, target[key]]))
} else {
target[key] = value
}
}
return result

加上数组的判断最终代码为:


function cloneDeep(source) {
const map = {
"[object Number]": "number",
"[object Boolean]": "boolean",
"[object String]": "string",
"[object Function]": "function",
"[object Array]": "array",
"[object Object]": "object",
"[object Null]": "null",
"[object Undefined]": "undefined",
"[object Date]": "date",
"[object RegExp]": "regexp"
}
const result = Array.isArray(source) ? [] : {}
const stack = [...Object.entries(source).map(item => [...item, result])]
const toString = Object.prototype.toString
while (stack.length) {
const [key, value, target] = stack.pop()
if (map[toString.call(value)] === 'object' || map[toString.call(value)] === 'array') {
target[key] = Array.isArray(value) ? [] : {}
stack.push(...Object.entries(value).map(item => [...item, target[key]]))
} else {
target[key] = value
}
}
return result
}

console.log(cloneDeep({ a: 1, b: '12' }))
//{ a: 1, b: '12' }
console.log(cloneDeep([{ a: 1, b: '12' }, { a: 2, b: '12' }, { a: 3, b: '12' }]))
//[{ a: 1, b: '12' }, { a: 2, b: '12' }, { a: 3, b: '12' }]

广度优先遍历实现深拷贝


同样的思路,实现深拷贝的最终代码为:


function cloneDeep(source) {
const map = {
"[object Number]": "number",
"[object Boolean]": "boolean",
"[object String]": "string",
"[object Function]": "function",
"[object Array]": "array",
"[object Object]": "object",
"[object Null]": "null",
"[object Undefined]": "undefined",
"[object Date]": "date",
"[object RegExp]": "regexp"
}
const result = {}
const stack = [{ data: source, target: result }]
const toString = Object.prototype.toString
while (stack.length) {
let { target, data } = stack.unshift()
for (let key in data) {
if (map[toString.call(data[key])] === 'object' || map[toString.call(data[key])] === 'array') {
target[key] = Array.isArray(data[key]) ? [] : {}
stack.push({ data: data[key], target: target[key] })
} else {
target[key] = data[key]
}
}
}
return result
}

作者:千空
来源:juejin.cn/post/7238978371689136185
收起阅读 »

慢慢的喜欢上泛型 之前确实冷落了

前言 下图 CSDN 水印 为自身博客 什么泛型 通俗意义上来说泛型将接口的概念进一步延伸,”泛型”字面意思就是广泛的类型,类、接口和方法代码可以应用于非常广泛的类型,代码与它们能够操作的数据类型不再绑定在一起,同一套代码,可以用于多种数据类型,这样,不仅可...
继续阅读 »

前言


下图 CSDN 水印 为自身博客


什么泛型



通俗意义上来说泛型将接口的概念进一步延伸,”泛型”字面意思就是广泛的类型,类、接口和方法代码可以应用于非常广泛的类型,代码与它们能够操作的数据类型不再绑定在一起,同一套代码,可以用于多种数据类型,这样,不仅可以复用代码,降低耦合,同时,还可以提高代码的可读性和安全性。



泛型带来的好处



在没有泛型的情况的下,通过对类型 Object 的引用来实现参数的“任意化”,“任意化”带来的缺点是要做显式的强制类型转换,而这种转换是要求开发者对实际参数类型可以预知的情况下进行的。对于强制类型转换错误的情况,编译器可能不提示错误,在运行的时候才出现异常,这是本身就是一个安全隐患。
那么泛型的好处就是在编译的时候能够检查类型安全,并且所有的强制转换都是自动和隐式的



public class GlmapperGeneric<T> {
private T t;
public void set(T t) { this.t = t; }
public T get() { return t; }

public static void main(String[] args) {
// do nothing
}

/**
* 不指定类型
*/

public void noSpecifyType(){
GlmapperGeneric glmapperGeneric = new GlmapperGeneric();
glmapperGeneric.set("test");
// 需要强制类型转换
String test = (String) glmapperGeneric.get();
System.out.println(test);
}

/**
* 指定类型
*/

public void specifyType(){
GlmapperGeneric<String> glmapperGeneric = new GlmapperGeneric();
glmapperGeneric.set("test");
// 不需要强制类型转换
String test = glmapperGeneric.get();
System.out.println(test);
}
}



上面这段代码中的 specifyType 方法中 省去了强制转换,可以在编译时候检查类型安全,可以用在类,方法,接口上。



泛型中通配符



我们在定义泛型类,泛型方法,泛型接口的时候经常会碰见很多不同的通配符,比如 T,E,K,V 等等,这些通配符又都是什么意思呢?



常用的 T,E,K,V,?



本质上这些个都是通配符,没啥区别,只不过是编码时的一种约定俗成的东西。比如上述代码中的 T ,我们可以换成 A-Z 之间的任何一个 字母都可以,并不会影响程序的正常运行,但是如果换成其他的字母代替 T ,在可读性上可能会弱一些。通常情况下,T,E,K,V,?是这样约定的:




  • ?表示不确定的 java 类型

  • T (type) 表示具体的一个java类型

  • K V (key value) 分别代表java键值中的Key Value

  • E (element) 代表Element

  • < T > 等同于 < T extends Object>

  • < ? > 等同于 < ? extends Object>


?无界通配符



先从一个小例子看起:



// 范围较广
static int countLegs (List<? extends Animal > animals ) {
int retVal = 0;
for ( Animal animal : animals )
{
retVal += animal.countLegs();
}
return retVal;
}
// 范围定死
static int countLegs1 (List< Animal > animals ){
int retVal = 0;
for ( Animal animal : animals )
{
retVal += animal.countLegs();
}
return retVal;
}

public static void main(String[] args) {
List<Dog> dogs = new ArrayList<>();
// 不会报错
countLegs( dogs );
// 报错
countLegs1(dogs);
}



对于不确定或者不关心实际要操作的类型,可以使用无限制通配符(尖括号里一个问号,即 <?> ),表示可以持有任何类型。像 countLegs 方法中,限定了上届,但是不关心具体类型是什么,所以对于传入的 Animal 的所有子类都可以支持,并且不会报错。而 countLegs1 就不行。



上界通配符 < ? extends E>



上届:用 extends 关键字声明,表示参数化的类型可能是所指定的类型,或者是此类型的子类。
在类型参数中使用 extends 表示这个泛型中的参数必须是 E 或者 E 的子类,这样有两个好处:



1.如果传入的类型不是 E 或者 E 的子类,编译不成功
2. 泛型中可以使用 E 的方法,要不然还得强转成 E 才能使用


List<? extends Number> eList = null;
eList = new ArrayList<Integer>();
//语句1取出Number(或者Number子类)对象直接赋值给Number类型的变量是符合java规范的。
Number numObject = eList.get(0); //语句1,正确

//语句2取出Number(或者Number子类)对象直接赋值给Integer类型(Number子类)的变量是不符合java规范的。
Integer intObject = eList.get(0); //语句2,错误

//List<? extends Number>eList不能够确定实例化对象的具体类型,因此无法add具体对象至列表中,可能的实例化对象如下。
eList.add(new Integer(1)); //语句3,错误

下界通配符 < ? super E>



下界: 用 super 进行声明,表示参数化的类型可能是所指定的类型,或者是此类型的父类型,直至 Object



在类型参数中使用 super 表示这个泛型中的参数必须是 E 或者 E 的父类。


List<? super Integer> sList = null;
sList = new ArrayList<Number>();

//List<? super Integer> 无法确定sList中存放的对象的具体类型,因此sList.get获取的值存在不确定性
//,子类对象的引用无法赋值给兄弟类的引用,父类对象的引用无法赋值给子类的引用,因此语句错误
Number numObj = sList.get(0); //语句1,错误

//Type mismatch: cannot convert from capture#6-of ? super Integer to Integer
Integer intObj = sList.get(0); //语句2,错误
//子类对象的引用可以赋值给父类对象的引用,因此语句正确。
sList.add(new Integer(1)); //语句3,正确

1. 限定通配符总是包括自己
2. 上界类型通配符:add方法受限
3. 下界类型通配符:get方法受限
4. 如果你想从一个数据类型里获取数据,使用 ? extends 通配符
5. 如果你想把对象写入一个数据结构里,
6. 使用 ? super 通配符 如果你既想存,又想取,那就别用通配符
7. 不能同时声明泛型通配符上界和下界


?和 T 的区别


// 指定集合元素只能是T类型
List<T> list = new ArrayList<T>();
// 集合元素可以是任意类型的,这种是 没有意义的 一般是方法中只是为了说明用法
List<?> list = new Arraylist<?>();


?和 T 都表示不确定的类型,区别在于我们可以对 T 进行操作,但是对 ?不行,比如如下这种 :



// 可以
T t = operate();

// 不可以
?car = operate();


T 是一个 确定的 类型,通常用于泛型类和泛型方法的定义,?是一个 不确定 的类型,通常用于泛型方法的调用代码和形参,不能用于定义类和泛型方法。



区别1 通过 T 来 确保 泛型参数的一致性


   public <T extends Number> void test1(List<T> dest, List<T> src) {
System.out.println();
}

public static void main(String[] args) {
test test = new test();
// integer 是number 的子类 所以是正确的
List<Integer> list = new ArrayList<Integer>();
List<Integer> list1 = new ArrayList<Integer>();
test.test1(list,list1);
}

在这里插入图片描述



通配符是 不确定的,所以这个方法不能保证两个 List 具有相同的元素类型



public void
test(List<? extends Number> dest, List<? extends Number> src)

GlmapperGeneric<String> glmapperGeneric = new GlmapperGeneric<>();
List<String> dest = new ArrayList<>();
List<Number> src = new ArrayList<>();
glmapperGeneric.testNon(dest,src);
//上面的代码在编译器并不会报错,但是当进入到 testNon 方法内部操作时(比如赋值),对于 dest 和 src 而言,就还是需要进行类型转换

区别2:类型参数可以多重限定而通配符不行



使用 & 符号设定多重边界(Multi Bounds),指定泛型类型 T 必须是 MultiLimitInterfaceA 和 MultiLimitInterfaceB 的共有子类型,此时变量 t 就具有了所有限定的方法和属性。对于通配符来说,因为它不是一个确定的类型,所以不能进行多重限定



区别3:通配符可以使用超类限定而类型参数不行



类型参数 T 只具有 一种 类型限定方式



T extends A



但是通配符 ? 可以进行 两种限定



? extends A
? super A

Class和 Class<?>区别


Class<"T"> (默认没有双引号 系统会自动把T给我换成特殊字符才加的引号) 在实例化的时候,T 要替换成具体类。Class<?>它是个通配泛型,? 可以代表任何类型,所以主要用于声明时的限制情况


作者:进阶的派大星
来源:juejin.cn/post/7140472064577634341
收起阅读 »

Vue 为什么要禁用 undefined?

web
Halo Word!大家好,我是大家的林语冰(挨踢版)~ 今天我们来伪科普一下——Vue 等开源项目为什么要禁用/限用 undefined? 敏感话题 我们会讨论几个敏感话题,包括但不限于—— 测不准的 undefined 如何引发复合 BUG? 薛定谔的...
继续阅读 »

Halo Word!大家好,我是大家的林语冰(挨踢版)~


今天我们来伪科普一下——Vue 等开源项目为什么要禁用/限用 undefined




敏感话题


我们会讨论几个敏感话题,包括但不限于——



  1. 测不准的 undefined 如何引发复合 BUG?

  2. 薛定谔的 undefined 如何造成二义性?

  3. 未定义的 undefined 为何语义不明?


懂得都懂,不懂关注,日后再说~




1. 测不准的 undefined 如何引发复合 BUG?


一般而言,开源项目对 undefined 的使用有两种保守方案:



  • 禁欲系——能且仅能节制地使用 undefined

  • 绝育系——禁用 undefined


举个粒子,Vue 源码就选择用魔法打败魔法——安排黑科技 void 0 重构 undefined


vue-void.png


事实上,直接使用 undefined 也问题不大,毕竟 undefined 表面上还是比较有安全感的。


readonly-desc.gif


猫眼可见,undefined 是一个鲁棒只读的属性,表面上相当靠谱。


虽然 undefined 自己问题不大,但最大的问题在于使用不慎可能会出 BUG。undefined 到底可能整出什么幺蛾子呢?


你知道的,不同于 null 字面量,undefined 并不恒等于 undefined 原始值,比如说祂可以被“作用域链截胡”。


举个粒子,当 undefined 变身成为 bilibili,同事的内心是崩溃的。


bilibili.png


猫眼可见,写做 undefined 变量,读做 'bilbili' 字符串,这样的代码十分反人类。


这里稍微有点违和感。机智如你可能会灵魂拷问,我们前面不是已经证明了 undefined 是不可赋值的只读属性吗?怎么祂喵地一言不合说变就变,又可以赋值了呢?来骗,来偷袭,不讲码德!


这种灵异现象主要跟变量查找的作用域链机制有关。读写变量会遵循“就近原则”优先匹配,先找到谁就匹配谁,就跟同城约会一样,和樱花妹异地恋的优先级肯定不会太高,所以当前局部作用域的优先级高于全局作用域,于是乎 JS 会优先使用当前非全局同名变量 undefined


换而言之,局部的同名变量 undefined 屏蔽(shadow,AKA“遮蔽”)了全局变量 globalThis.undefined


关于作用域链这种“远亲不如近邻”的机制,吾愿赐名为“作用域链截胡”。倘若你不会搓麻将,你也可以命名为“作用域链抢断”。倘若你不会打篮球,那就叫“作用域链拦截”吧。


globalThis.undefined 确实是只读属性。虽然但是,你们重写非全局的 undefined,跟我 globalThis.undefined 有什么关系?


周树人.gif


我们总以为 undefined 短小精悍,但其实 globalThis.undefined 才能扬长避短。


当我们重新定义了 undefinedundefined 就名不副实——名为 undefined,值为任意值。这可能会在团队协作中引发复合 BUG。


所谓“复合 BUG”指的是,单独的代码可以正常工作,但是多人代码集成就出现问题。


举个粒子,常见的复合 BUG 包括但不限于:



  • 命名冲突,比如说 Vue2 的 Mixin 就有这个瑕疵,所以 Vue3 就引入更加灵活的组合式 API

  • 作用域污染,ESM 模块之前也有全局作用域污染的老毛病,所以社区有 CJS 等模块化的轮子,也有 IIFE 等最佳实践

  • 团队协作,Git 等代码版本管理工具的开发冲突


举个粒子,undefined 也可能造成类似的问题。


complex-bug.png


猫眼可见,双方的代码都问题不大,但放在一起就像水遇见钠一般干柴烈火瞬间爆炸。


这里分享一个小众的冷知识,这样的代码被称为“Jenga Code”(积木代码)。


Jenga 是一种派对益智积木玩具,它的规则是,先把那些小木条堆成一个规则的塔,玩家轮流从下面抽出一块来放在最上面,谁放上之后木塔垮掉了,谁就 GG 了。


jenga.gif


积木代码指的是一点点的代码带来了亿点点的 BUG,一行代码搞崩整个项目,码农一句,可怜焦土。


换而言之,这样的代码对于 JS 运行时是“程序正义”的,对于开发者却并非“结果正义”,违和感拉满,可读性和可为维护性十分“赶人”,同事读完欲哭无泪。


所谓“程序正义”指的是——JS 运行时没有“阳”,不会抛出异常,直接挂掉,浏览器承认你的代码 Bug free,问题不大。


祂敢报错吗?祂不敢。虽然但是,无症状感染也是感染。你敢这么写吗?你不敢。除非忍不住,或者想跑路。


举个粒子,“离离原上谱”的“饭圈倒牛奶”事件——



  • 有人鞠躬尽瘁粮食安全

  • 有人精神饥荒疯狂倒奶


这种行为未必违法,但是背德,每次看到只能无视,毕竟语冰有“傻叉恐惧症”。


“程序正义”不代表“结果正义”,代码能 run 不代表符合“甲方肝虚”,不讲码德可能造成业务上的技术负债,将来要重构优化来还债。所谓“前猫拉屎,后人铲屎”大抵也是如此。


综上所述,要警惕测不准的 undefined 在团队开发中造成复合 BUG。




2. 薛定谔的 undefined 如何造成二义性?


除了复合 BUG,undefined 还可能让代码产生二义性。


代码二义性指的是,同一行代码,可能有不同的语义。


举个粒子,JS 的一些代码解读就可能有歧义。


mistake.png


undefined 也可能造成代码二义性,除了上文的变量名不副实之外,还很可能产生精神分裂的割裂感。


举个粒子,代码中存在两个一龙一猪的 undefined


default.png


猫眼可见,undefined 的值并不相同,我只觉得祂们双标。


undefined 变量之所以是 'bilibili' 字符串,是因为作用域链就近屏蔽,cat 变量之所以是 undefined 原始值,是因为已声明未赋值的变量默认使用 undefined 原始值作为缺省值,所以没有使用局部的 undefined 变量。


倘若上述二义性强度还不够,那我们还可以写出可读性更加逆天的代码。


destruct.png


猫眼可见,undefined 有没有精神分裂我不知道,但我快精神分裂了。


代码二义性还可能与代码的执行环境有关,譬如说一猫一样的代码,在不同的运行时,可能有一龙一猪的结果。


strict-mode.png


猫眼可见,我写你猜,谁都不爱。


大家大约会理直气壮地反驳,我们必不可能写出这样不当人的代码,var 是不可能 var 的,这辈子都不可能 var


问题在于,墨菲定律告诉我们,只要可能有 BUG,就有可能有 BUG。说不定你的猪队友下一秒就给你来个神助攻,毕竟不是每个人都像你如此好学,既关注了我,还给我打 call。


语冰以前也不相信倒牛奶这么“离离原上谱”的事件,但是写做“impossible”,读做“I M possible”。


事实上,大多数教程一般不会刻意教你去写错误的代码,这其实恰恰剥夺了我们犯错的权利。不犯错我们就不会去探究为什么,而对知识点的掌握只停留在表面是什么,很多人知错就改,下次还敢就是因为缺少了试错的成就感和多巴胺,不知道 BUG 的 G 点在哪里,没有形成稳固的情绪记忆。


请相信我,永远写正确的代码本身就是一件不正确的事情,你会看到这期内容就是因为语冰被坑了气不过,才给祂载入日记。


语冰很喜欢的一部神作《七龙珠》里的赛亚人,每次从濒死体验中绝处逢生战斗力就会增量更新,这个设定其实蛮科学的,譬如说我们身边一些“量变到质变”的粒子,包括但不限于:



  • 骨折之后骨头更加坚硬了

  • 健身也是肌肉轻度撕裂后增生

  • 记忆也是不断复习巩固


语冰并不是让大家在物理层面去骨折,而是鼓励大家从 BUG 中学习。私以为大神从来不是没有 BUG,而是 fix 了足够多的 BUG。正如爱迪生所说,我没有失败 999 次,而是成功了 999 次,我成功证明了那些方法完全达咩。


综上所述,undefined 的二义性在于可能产生局部的副作用,一猫一样的代码在不同运行时也可以有一龙一猪的结果,最终导致一千个麻瓜眼中有一千个哈利波特,读码人集体精神分裂。




3. 未定义的 undefined 为何语义不明?


除了可维护性感人的复合 BUG 和可读性感人的代码二义性,undefined 自身的语义也很难把握。


举个粒子,因为太麻烦就全写 undefined 了。


init.png


猫眼可见,原则上允许我们可以无脑地使用 undefined 初始化任何变量,万物皆可 undefined


虽然但是,绝对的光明等于绝对的黑暗,绝对的权力导致绝对的腐败。undefined 的无能恰恰在于祂无所不能,语冰有幸百度了一本书叫《选择的悖论》,这大约也是 undefined 的悖论。


代码是写给人看的,代码的信息越具体明确越好,偏偏 undefined 既模糊又抽象。你知道的,我们接触的大多数资料会告诉我们 undefined 的意义是“未定义/无值”。


虽然但是,准确而无用的观念,终究还是无用的。undefined 的正确打开方式就是无为,使用 undefined 的最佳方式是不使用祂。




免责声明



本文示例代码默认均为 ESM(ECMAScript Module)筑基测评,因为现代化前端开发相对推荐集成 ESM,其他开发环境下的示例会额外注释说明,edge cases 的解释权归大家所有。



今天的《ES6 混合理论》就讲到这里啦,我们将在本合集中深度学习若干奇奇怪怪的前端面试题/冷知识,感兴趣的前端爱好者可以关注订阅,也欢迎大家自由言论和留言许愿,共享 BUG,共同内卷。


吾乃前端的虔信徒,传播 BUG 的福音。


我是大家的林语冰,我们一期一会,不散不见,掰掰~


作者:大家的林语冰
来源:juejin.cn/post/7240483867123220540
收起阅读 »

由阿里裁员引发的一些思考

前言 从阿里淘系离开差不多2年多了,最近阿里又来到风口浪尖上,也是打出一套眼花缭乱的组合拳, 先是马老板回国; 3.28日 阿里开启成立24年来最大组织架构变革,逍遥子张勇,宣布启动“1+6+N”组织变革,各个大业务线实行自负盈亏,有独立融资和上市的可能性;...
继续阅读 »


前言


从阿里淘系离开差不多2年多了,最近阿里又来到风口浪尖上,也是打出一套眼花缭乱的组合拳



  1. 先是马老板回国;

  2. 3.28日 阿里开启成立24年来最大组织架构变革,逍遥子张勇,宣布启动“1+6+N”组织变革,各个大业务线实行自负盈亏,有独立融资和上市的可能性;

  3. 最近又开始爆出阿里大裁员,各种小道消息什么 阿里云7%,天猫淘宝25%,然后阿里开始辟谣,且不论真假,一时间,给整个互联网圈传递一股寒气,今天就在茶话会上聊聊这个事情。


理性的看待阿里裁员


阿里裁员其实是有个心理预期的,个人觉得主要有以下原因吧



  1. 阿里 361制度末位淘汰10%,连续2年3.25就会被淘汰,每年本身就有一批人要淘汰

  2. 组织架构拆解需要自负盈亏,一些子业务之前可以吃大锅饭,现在分田到户了,就需要人员进行优化提高组织效率,达到降本增效

  3. 核心业务一直被蚕食,人才盘点降本增效,淘汰贵的产出一般的 换一拨 校招生既能补充新鲜血液又可以降低成本


目前在互联网下行这个大环境下,加之之前一直宣传的35岁危机更加放大了裁员带来的恐慌。互联网正在慢慢回归理性这是个不争的事实,甚至连老美的硅谷互联网大厂都裁了一波,看看马斯克接手推特后"大杀四方"的狠劲。


互联网的退潮期


大潮正在退去


行业也是要顺势而为,风口来了猪都上天,不过目前国内互联网已经过了之前的高速增长期,监管也在收紧,大家都在拼存量市场,都卷到到菜市场了(各种买菜 多多买菜、橙心优选等等)从蓝海杀到了红海,增长上不去了,要么开源,要么节流。开源的话寻找新赛道何其难,元宇宙的尸体还热乎着,前几年大家都选择出海,但是除了字节还算可以(也难),小米在印度被阿三罚了好几十亿,整体看来开源难度过大。 image.png 大厂纷纷启用了节流大招,字节去年就在喊去肥增瘦各个项目开始review roi要求打正,肯定打不正的有的就地正法了,教育部门、游戏部门都是重灾区; 腾讯去年也是整合内部资源,PCG(破产G)去年基本上干掉一半,其实阿里这一波跟去年腾讯一样半斤八两。 而且这几年互联网大厂之间好像有点默契,年终之后都在裁员,降低员工流动性,不仅对业务稳定带来好处也能减少薪资开支。不过跳槽涨薪确实香,之前基本2年一跳,早些时候行情好能double,最不济也有40%的涨幅,不过现在在字节已经2年多了,确实没有任何跳槽打算了


非理性招聘慢慢在回归理性


大厂员工的招聘本身就是非理性的,还记得前年微信出了一个爆炸的表情,当时看到一篇文章作者自嘲自己是清华毕业在微信研究"炸屎"表情。。。非理性主要有以下几个方面造成的:



  1. 赛马机制导致团队重复


早期在业务遇到增长瓶颈和重大的课题时候,往往采用加人的方法,《人月神话》早就证伪了技术在这方面的不可靠,可能反而会让协作效率降低。这也间接导致了大厂的山头主义,由于领导需要使用团队规模来确立地位,毕竟更多的HC,就意味着揽到更多的事情,获得更高的地位。这在早期也是被更高层的领导所默许的,腾讯大名鼎鼎的赛马机制,就是使用多支团队来做同样的事情,微信当年就是这么诞生的,观察大部门的大厂对团队的分工有时候是可以模糊的,而且资源都还不错,如果一个团队不行,就让另一个上,这也不可避免的引起了内耗,这种机制效率上有有提升,但是代价是巨大资金开销,内部组织臃肿。



  1. 人才储备过盛


前些年,大厂业务增长太强劲了,各个赛道都要投入人力,人员分工更加细化,大厂的app可能一个按钮就是一个业务线,这也经常自嘲为拧螺丝的,因此招聘规模也是空前的,先招进来再内部淘汰,挑选了最优秀的,而且也是变相的打击竞争对手,不由得想起来了华为,华为之前财大气粗,连续狂招几年,直接把中兴干的人才断档。 在当前这个战略收缩的过冬阶段,这样的裁员可能还是结构性的、长期的,只要业务不行公司可能就及时止损了,带来的就是裁员,得有个清醒的认识。



  1. 大厂员工真的不便宜


互联网作为行业天花板,经历了资本的无序扩张,资金充备,花的都是投资人的钱,互相竞争着加价招人,大厂校招生的白菜价也是其他行业所无法企及的,几年下来每年的普调、跳槽的几轮加价,大家都来到了一个薪资高位。这些都是建立在你做的业务能给公司转来更多的钱,当增长停止时候,你还能给公司赚这么多吗?从经济学的供需关系来看很简单:公司年薪100w招你来,你真的能持续给公司多赚150w吗?业务增长时候,公司开掉你,人力成本节省了100w,公司的业务会降低100w吗?如果没有那裁掉的你是个理性的选择,因为你实在是太贵了。这些年大厂不光是干掉大头兵,甚至连一些高P开始受到波及了,因为他们更贵。所以薪资来到高位的找个好业务能苟着尽量苟着,风口没了,猪是飞不上天的。加上最近几年大学生找工作难,裁员换血一波,对公司也是极好的。 再看看老美那边,大厂基本也都是正式员工(贵)+外包(便宜),国内感觉也会慢慢往这个方向发展,看看国内华为这几年,外包招的飞起,之前在阿里 QA、前端、UI都是正式工+外包搭配。(除了移动端+服务端好像没见过)


大趋势下的互联网人该如何做


黄金时代一起不复返了,看清行业的大趋势,做好心理预期:



  1. 降低自己的经济杠杆,适当降低消费欲望,毕竟手里有粮心不慌。我就把房贷提前还了大头,留点尾巴抵抵税

  2. 尽量找个稳一点的业务线,延长职业生涯,认真做事,苟住就好,边缘业务可能隔三差五就要一波拥抱变化,我是活水到了字节一个比较赚钱的业务

  3. 有余力的可以探索一些副业,可以是老本行,或者家里人脉广的也可以涉足其他行业,比如水果店啥的,看自己人脉关系了,试试看副业能不能养起来。可以是一种商业模式,我看就有很多网红收割校招生搞星球,搞培训啥的忙的不亦乐乎;也可以是发现一个痛点,上次就听说 一个还没交付的楼盘一个业主在业主群搞了个公众号,直播楼盘进度,小的私域流量也是能赚点小钱的。培养一些产品思维,注意观察生活吧,不过副业确实比较难要有耐心,最近我就在googleplay上架一个游戏app,不过自然流量太低了,国内的话现在对个人开发者太不又好了,不仅应用商店很多都需要企业资质,穿山甲、优量汇这些广告平台也是需要企业资质,基本把个人开发者路封死了,注册个企业比较麻烦成本也高,这块有兴趣后面可以展开说说,还有灰黑产的话还是要慎重,来钱快可能进去也快

  4. 要是还想在业内混的话,专业能力还是不能丢,提高个人竞争力,保持技术关注度,加强 技术的深度以及广度,做一个T型人才,尽量成为一个全栈吧。其实现在服务端go的gin框架,java的spring学起来也很快,前端搞个小程序基本上就齐活了,不仅是个人竞争力,也是副业的基础;工作中也要注意提高自己的软实力,比如 稳定的情绪、有效的沟通、适当的向上管理,做一个大家都认可的靠谱的合作伙伴


最后尽人事 听天命吧,心态还是要稳住,积极乐观的工作和生活。好了,茶也喝完了,本期的茶话会就到这里吧,祝大家工作顺利,欢迎留言讨论


作者:Android茶话会
来源:juejin.cn/post/7237489935901032504
收起阅读 »

技术人创业是怎么被自己短板KO的

这几天搜索我的聊天记录,无意中看到了一个两年前的聊天消息。那是和我讨论出海业务的独立开发网友,后来又去创业的业界精英。顺手一搜,当初他在我的一个开发者群也是高度活跃的人群之一,还经常私下给我提建议,人能力很强,做出海矩阵方向的项目做的很拿手。但是在后来忽然就没...
继续阅读 »

这几天搜索我的聊天记录,无意中看到了一个两年前的聊天消息。那是和我讨论出海业务的独立开发网友,后来又去创业的业界精英。顺手一搜,当初他在我的一个开发者群也是高度活跃的人群之一,还经常私下给我提建议,人能力很强,做出海矩阵方向的项目做的很拿手。但是在后来忽然就没消息了,虽然人还留在群里。


我好奇点开了他的朋友圈,才知道他已经不做独立开发了,而且也(暂时)不在 IT 圈里玩了,去帮亲戚家的服装批发业务打打下手,说是下手,应该也是二当家级别了,钱不少,也相对安稳。朋友圈的画风以前是IT行业动态,出海资讯现在是销售文案和二维码。


和他私下聊了几句,他跟我说他现在过的也还好,人生路还长着呢,谈起了自己在现在这行做事情的经历,碎碎念说了不少有趣的事情,最后还和我感慨说:“转行后感觉脑子灵活了很多”,我说那你写程序的时候脑子不灵活吗,他发了个尴尬而不失礼貌的表情,“我以前技术搞多了,有时候死脑筋。”


这种话我没少听过,但是从一个认识(虽然是网友)而且大跨度转行的朋友这里说出来,就显得特别有说服力。尤其了解了他的经历后,想写篇文章唠叨下关于程序员短板的问题,还有这种短板不去补强,会怎么一步步让路越走越窄的。


现在离职(或者被离职)的程序员越来越多了,程序员群体,尤其是客户端程序员这个群体,只要能力过得去,都有全栈化和业务全面化的潜力。尤其是客户端程序员,就算是在公司上班时,业余时间写写个人项目,发到网上,每个月赚个四到五位数的副业收入也是可以的。


再加上在公司里遇到的各种各样的窝囊事,受了无数次“煞笔领导”的窝囊气,这会让一些程序员产生一种想法,我要不是业余时间不够,不然全职做个项目不就起飞了?


知道缺陷在哪儿,才能扬长避短,所以我想复盘一下,程序员创业,在主观问题上存在哪些短板。(因为说的是总体情况,也请别对号入座)


第一,认死理。


和代码,协议,文档打交道多了,不管自己情愿不情愿,人多多少少就有很强的“契约概念”,代码的世界条理清晰,因果分明,1就是1,0就是0,在这样的世界里呆多了,你要说思维方式不被改变,那是不可能的 --- 而且总的来说,这种塑造其实是好事情。要不然也不会有那么多家长想孩子从小学编程了。(当然了,家长只是想孩子学编程,不是做程序员。)


常年埋头程序的结果,很容易让技术人对于社会上很多问题的复杂性本质认识不到位,恐惧,轻视,或者视而不见,总之,喜欢用自己常年打磨的逻辑能力做一个推理,然后下一个简单的结论。用毛爷爷的话说,是犯了形而上的毛病。


例如,在处理iOS产品上架合规性一类问题时,这种毛病暴露的就特别明显。


比如说相信一个功能别的产品也是这么做的,也能通过审核,那自己照着做也能通过。但是他忽略了这种判断背后的条件是,你的账号和别的账号在苹果眼里分量也许不同的,而苹果是不会把这件事写在文档上的。


如果只是说一说不要紧,最怕的是“倔”,要不怎么说是“认死理”呢。


第二,喜欢拿技术套市场。


​这个怎么理解呢,就是有追求的技术人喜欢研究一些很强的技术,但是研究出来后怎么用,也就是落实到具体的应用场景,就很缺点想象力了。


举个身边有意思的例子,有个技术朋友花了三年时间业余时间断断续续的写,用 OpenGL 写了一套动画效果很棒的 UI 引擎,可以套一个 View 进去后定制各种酷炫的动画效果。做出来后也不知道用来干嘛好,后来认识了一个创业老板,老板一看你这个效果真不错啊,你这引擎多少钱我买了,朋友也没什么概念,说那要不五万卖你。老板直接钱就打过去了。后来老板拿给手下的程序员维护,用这套东西做了好几个“小而美”定位的效率工具,简单配置下就有酷炫的按钮动画效果,配合高级的视觉设计逼格拉满,收入怎么样我没问,但是苹果在好几个国家都上过推荐。


可能有人要说,那这个程序员哥哥没有UI帮忙啊,对,是这个理,但是最根本的问题是,做小而美工具这条路线,他想都没想到,连意识都意识不到的赚钱机会,怎么可能把握呢?有没有UI帮忙那是实现层的门槛而已。


第三,不擅长合作。


为什么很多创业赚到小钱(马化腾,李彦宏这些赚大钱就不说了,对我们大部分人没有参考价值)而且稳定活下来的都是跑商务,做营销出身的老板。


他们会搞钱。


他们会搞钱,是​因为他们会搞定人,投资人,合伙人,还有各种七七八八的资源渠道。


大部分人,在创业路上直接卡死在这条路线上了。


投资人需要跑,合作渠道需要拉,包括当地的税务减免优惠,创业公司激励奖金,都需要和各种人打交道才能拿下来。


那我出海总行了吧,出海就不用那么麻烦了吧。不好意思,出海的合作优势也是领先的,找海外的自媒体渠道合作,给产品提曝光。坚持给苹果写推荐信,让自家产品多上推荐。你要擅长做这些,就不说比同行强一大截,起码做出好产品后创业活下来的希望要高出不少,还有很多信息差方法论,需要进圈子才知道。



--- 


我说的这些,不是贬损也不是中伤,说白了,任何职业都有自己的短板,也就是我们说的职业病,本来也不是什么大不了的事情。只是我们在大公司拧螺丝的时候,被保护的太好了。


只是创业会让一个人的短处不断放大,那是因为你必须为自己的选择负责了,没人帮你擦屁股了背锅了。所以短板才显得那么刺眼。


最后说一下,不是说有短板就会失败,谁没点短处呢。写出来只是让自己和朋友有更好的自我认知,明白自己的长处在哪,短处在哪。


最后补一个,左耳朵耗子的事情告诉我们,程序员真的要保养身子,拼到最后其实还是拼身

作者:风海铜锣
来源:juejin.cn/post/7238443713873199159
体,活下来才有输出。

收起阅读 »

python计算质数的几种方法

因为要学着写渗透工具,这几天都在上python编程基础课,听得我打瞌睡,毕竟以前学过嘛。 最后sherry老师留了作业,其中一道题是这样的: 题目:编写python程序找出10-30之间的质数。 太简单了,我直接给出答案: Prime = [11, 13, 1...
继续阅读 »

因为要学着写渗透工具,这几天都在上python编程基础课,听得我打瞌睡,毕竟以前学过嘛。
最后sherry老师留了作业,其中一道题是这样的:


题目:编写python程序找出10-30之间的质数。


太简单了,我直接给出答案:


Prime = [11, 13, 17, 19, 23, 29]
print(Prime)

输出结果:


[11, 13, 17, 19, 23, 29]

当然,这样做肯定会在下节课被sherry老师公开处刑的,所以说还是要根据上课时学的知识写个算法。


1.穷举法


回想一下上课时学了变量、列表、循环语句之类的东西,sherry老师还亲自演示了多重死循环是怎么搞出来的(老师是手滑了还是业务不熟练啊),所以我们还是要仔细思考一下不要重蹈覆辙。


思路:首先要构造一个循环,遍历所有符合条件的自然数,然后一个一个验证是否为质数,最后把符合条件的质数列出来。


# 最开始编的穷举法,简单粗暴,就是性能拉跨。
# P=因数,N=自然数
import time

t0 = time.time() # 开始时间
Min = 10 # 范围最小值
Max = 30 # 范围最大值
Result = [] # 结果

for N in range(Min, Max): # 给自然数来个遍历
for P in range(2, N):
if (N % P == 0): # 判断是否有因数
break # 有因数那就不是质数,跳出循环
else:
Result.append(N)

print('计算', Min, '到', Max, '之间的质数')
print(Min, '到', Max, '之间的质数序列:', Result)
print(Min, '到', Max, '之间的质数个数:', len(Result))
print('计算耗时:', time.time() - t0, '秒')

执行结果(计算耗时是最后加上去的):


2023-05-28-22-35-46.png


到这里作业就搞定了。然后把其他几道题也做完了,发现很无聊,就又切回来想搞点事。这么点计算量,0秒真的有点少,不如趁这个机会烤一烤笔记本的性能,所以直接在Min和Max的值后面加几个0。试试100000-200000。


2023-05-28-23-02-03.png


很尴尬,直接卡住了,这代码有点拉跨啊,完全不符合我的风格。
倒了杯咖啡,终于跑完了。


2023-05-28-23-01-07.png


这个也太夸张,一定是哪里出了问题,很久以前用C写的代码我记得也没那么慢啊。反正周末挺闲的,不如仔细研究一下。


2.函数(CV)大法


为了拓宽一下思路,我决定借鉴一下大佬的代码。听说函数是个好东西,所以就CV了两个函数。


一个函数判断质数,另一个求范围内的所有质数,把它们拼一起,是这个样子:


# 网上学来的,自定义两个函数,但是数值稍微大点就卡死了。
import time

t0 = time.time()
Min = 100000 # 范围最小值
Max = 200000 # 范围最大值


def is_prime(n): return 0 not in [n % i for i in range(2, n//2+1)] # 判断是否为质数


def gen_prime(a, b): return [n for n in range(
a, b+1) if 0 not in [n % i for i in range(2, n//2+1)]] # 输出范围内的质数


print('计算', Min, '到', Max, '之间的质数')
print(Min, '到', Max, '之间的质数序列:', gen_prime(Min, Max))
print('计算耗时:', time.time() - t0, '秒')

稍微改动了一下,还是100000-200000,我们试试看。


2023-05-28-23-08-35.png


嗯,一运行风扇就开始啸叫,CPU都快烤炸了。看来CV大法也不行啊。
经过漫长的烤机,这次结果比上次还惨,300多秒,这两个函数本质上还是穷举法,看来这条路也走不通。


3.穷举法改


我们可以分析一下穷举法的代码,看看有没有什么改进的方法。
首先,通过九年义务教育掌握的数学知识,我们知道,质数中只有2是偶数,所以计算中可以把偶数忽略掉,只计算奇数,工作量立马减半!
其次,在用因数P判断N是否为质数时,如果P足够大的话,比如说PxP>=N的时候,那么后面的循环其实是重复无意义的。因为假设PxQ>=N,那么P和Q必然有一个小于sqrt(N),只需要计算P<=sqrt(N)的情况就行了。


因为2作为唯一一个偶数,夹在循环里面处理起来很麻烦,所以放在开头处理掉。最终的代码如下:


# 优化后的代码,减少了一些无意义的循环,比以前快多了。
import time

t0 = time.time()
Min = 100000 # 范围最小值
Max = 200000 # 范围最大值
Prime = [2, 3] # 质数列表
Result = [] # 结果
Loop = 0 # 计算循环次数

if Min <= 2:
Result.append(2)
if Min <= 3:
Result.append(3) # 先把2这个麻烦的偶数处理掉
for N in range(5, Max, 2):
for P in range(3, int(N**0.5)+2, 2): # 只计算到根号N
Loop += 1
if (N % P == 0):
break
else:
Prime.append(N)
if N > Min:
Result.append(N)

print('计算', Min, '到', Max, '之间的质数')
print(Min, '到', Max, '之间的质数序列:', Result)
print(Min, '到', Max, '之间的质数个数:', len(Result))
print('循环次数:', Loop)
print('计算耗时:', time.time() - t0, '秒')

2023-05-28-23-09-54.png


代码量虽然多了,但是效果还是很明显,100000-200000才0.4秒,快了不知道多少,看来我们的思路是对的。
我决定再加到1000000-5000000,看看能不能撑住。因为输出太多了控制台会卡死,所以改一下,只输出最后一个质数。


2023-05-28-23-19-12.png


总共花了64秒,看来还是有点费劲。


4.穷举法魔改


我们再来分析一下,如果我们用于判断的因数,不是用奇数列表,而是用生成的Prime列表里面的质数,因为质数的个数远远少于奇数,所以第二个循环会少一些工作量呢?可以试试看。但是因为这个改动,需要加一些判断语句进去,所以节省的时间比较有限。


# 别看这个代码比较长,但是跑到1000万也不会卡死,而且还很快。
import time

t0 = time.time()
Min = 1000000 # 范围最小值
Max = 5000000 # 范围最大值
Prime = [2, 3] # 质数列表
Result = [] # 结果
Loop = 0 # 计算循环次数

if Min <= 2:
Result.append(2)
if Min <= 3:
Result.append(3)
for N in range(5, Max, 2):
M = int(N**0.5) # 上限为根号N
for P in range(len(Prime)): # 在质数列表Prime中遍历
Loop += 1
L = Prime[P+1]
if (N % L == 0):
break
elif L >= M: # 上限大于根号N,判断为质数并跳出循环
Prime.append(N)
if N > Min:
Result.append(N)
break

print('计算', Min, '到', Max, '之间的质数')
print('最后一个质数:', Result[-1])
print(Min, '到', Max, '之间的质数个数:', len(Result))
print('循环次数:', Loop)
print('计算耗时:', time.time() - t0, '秒')

还是1000000-5000000再试试看


2023-05-28-23-25-29.png


这次耗时22秒,时间又缩短了一大半,但是好像已经没多少改进的空间了,感觉穷举法已经到头了,需要新的思路。


5.埃氏筛法


其实初中数学我们就学过埃氏筛法:
如果P是质数,那么大于P的N的倍数一定不是质数。把所有的合数排除掉,那么剩下的就都是质数了。
我们可以生成一个列表用来储存数字是否是质数,初始阶段都是质数,每次得出一个质数就将它的倍数全部标记为合数。


# 速度已经起飞了。
import time

t0 = time.time()
Min = 1000000 # 范围最小值
Max = 2000000 # 范围最大值
Loop = 0 # 计算循环次数
Result = [] # 结果

Natural = [True for P in range(Max)] # 自然数列表标记为True
for P in range(2, Max):
if Natural[P]: # 标记如果为True,就是质数
if P >= Min:
Result.append(P) # 添加范围之内的质数
for N in range(P*2, Max, P): # 将质数的倍数的标记改为False
Loop += 1
Natural[N] = False

print('计算', Min, '到', Max, '之间的质数')
print('最后一个质数:', Result[-1])
print(Min, '到', Max, '之间的质数个数:', len(Result))
print('循环次数:', Loop)
print('计算耗时:', time.time() - t0, '秒')

2023-05-29-00-11-23.png


1.6秒,比最高级的穷举法还要快上10多倍,这是数学的胜利。
再试试1-50000000。


1.png


很不错,只需要20秒。因为筛法的特性,忽略内存的影响,数值越大,后面的速度反而越快了。


6.欧拉筛法


我们可以仔细分析一下,上面的埃氏筛法在最后标记的时候,还是多算了一些东西,N会重复标记False,比如77,既是7的倍数又是11的倍数,这样会被标记两次,后面的大合数会重复标记多次,浪费了算力,所以标记的时候要排除合数。另外就是P*N大于Max时,后面的计算已经无意义了,也要跳出来。把这些重复的动作排除掉,就是欧拉筛法,也叫线性筛。


# 最终版,优化了很多细节。
import time

t0 = time.time()
Min = 1 # 范围最小值
Max = 50000000 # 范围最大值
Loop = 0 # 计算循环次数
Prime = [2]
Result = [] # 结果

if Min <= 2:
Result.append(2)
Limit = int(Max/3)+1
Natural = [True for P in range(Max+1)] # 自然数列表标记为True
for P in range(3, Max+1, 2):
if Natural[P]: # 标记如果为True,就是质数
Prime.append(P)
if P >= Min:
Result.append(P)
if P > Limit: # 超过Limit不需要再筛了,直接continue
continue
for N in Prime: # 将质数的倍数的标记改为False
Loop += 1
if P*N > Max: # 超过Max就无意义了,直接break
break
Natural[P * N] = False
if P % N == 0: # 判断是否为合数
break

print('计算', Min, '到', Max, '之间的质数')
print('最后一个质数:', Result[-1])
print(Min, '到', Max, '之间的质数个数:', len(Result))
print('循环次数:', Loop)
print('计算耗时:', time.time() - t0, '秒')

(因为之前的版本缩进错了,所以更新了这段代码)


2.png


同样的条件下耗时11.46秒。这是因为多了一个列表和几行判断语句,加上python的解释型特性,所以实际上并不会快好几倍,但是总体效率还是有50%左右的提升。


好了,这次把老师课堂上讲的变量、列表、循环语句什么的都用上了,算是现买现卖、活学活用吧。我觉得这次的作业怎么说也能拿满分吧,sherry老师记得下次上课夸夸我。


作者:ReisenSS
来源:juejin.cn/post/7238199999732695097
收起阅读 »

Vue3 除了keep-alive,还有哪些页面缓存的实现方案

web
引言 有这么一个需求:列表页进入详情页后,切换回列表页,需要对列表页进行缓存,如果从首页进入列表页,就要重新加载列表页。 对于这个需求,我的第一个想法就是使用keep-alive来缓存列表页,列表和详情页切换时,列表页会被缓存;从首页进入列表页时,就重置列表页...
继续阅读 »

引言


有这么一个需求:列表页进入详情页后,切换回列表页,需要对列表页进行缓存,如果从首页进入列表页,就要重新加载列表页。


对于这个需求,我的第一个想法就是使用keep-alive来缓存列表页,列表和详情页切换时,列表页会被缓存;从首页进入列表页时,就重置列表页数据并重新获取新数据来达到列表页重新加载的效果。


但是,这个方案有个很不好的地方就是:如果列表页足够复杂,有下拉刷新、下拉加载、有弹窗、有轮播等,在清除缓存时,就需要重置很多数据和状态,而且还可能要手动去销毁和重新加载某些组件,这样做既增加了复杂度,也容易出bug。


接下来说说我的想到的新实现方案(代码基于Vue3)。


省流


demo: xiaocheng555.github.io/page-cache/…


代码: github.com/xiaocheng55…


keep-alive 缓存和清除



keep-alive 缓存原理:进入页面时,页面组件渲染完成,keep-alive 会缓存页面组件的实例;离开页面后,组件实例由于已经缓存就不会进行销毁;当再次进入页面时,就会将缓存的组件实例拿出来渲染,因为组件实例保存着原来页面的数据和Dom的状态,那么直接渲染组件实例就能得到原来的页面。



keep-alive 最大的难题就是缓存的清理,如果能有简单的缓存清理方法,那么keep-alive 组件用起来就很爽。


但是,keep-alive 组件没有提供清除缓存的API,那有没有其他清除缓存的办法呢?答案是有的。我们先看看 keep-alive 组件的props:


include - string | RegExp | Array。只有名称匹配的组件会被缓存。
exclude - string | RegExp | Array。任何名称匹配的组件都不会被缓存。
max - number | string。最多可以缓存多少组件实例。

从include描述来看,我发现include是可以用来清除缓存,做法是:将组件名称添加到include里,组件会被缓存;移除组件名称,组件缓存会被清除。根据这个原理,用hook简单封装一下代码:


import { ref, nextTick } from 'vue'

const caches = ref<string[]>([])

export default function useRouteCache () {
// 添加缓存的路由组件
function addCache (componentName: string | string []) {
if (Array.isArray(componentName)) {
componentName.forEach(addCache)
return
}

if (!componentName || caches.value.includes(componentName)) return

caches.value.push(componentName)
}

// 移除缓存的路由组件
function removeCache (componentName: string) {
const index = caches.value.indexOf(componentName)
if (index > -1) {
return caches.value.splice(index, 1)
}
}

// 移除缓存的路由组件的实例
async function removeCacheEntry (componentName: string) {
if (removeCache(componentName)) {
await nextTick()
addCache(componentName)
}
}

return {
caches,
addCache,
removeCache,
removeCacheEntry
}
}

hook的用法如下:


<router-view v-slot="{ Component }">
<keep-alive :include="caches">
<component :is="Component" />
</keep-alive>
</router-view>

<script setup lang="ts">
import useRouteCache from './hooks/useRouteCache'
const { caches, addCache } = useRouteCache()

<!-- 将列表页组件名称添加到需要缓存名单中 -->
addCache(['List'])
</script>

清除列表页缓存如下:


import useRouteCache from '@/hooks/useRouteCache'

const { removeCacheEntry } = useRouteCache()
removeCacheEntry('List')


此处removeCacheEntry方法清除的是列表组件的实例,'List' 值仍然在 组件的include里,下次重新进入列表页会重新加载列表组件,并且之后会继续列表组件进行缓存。



列表页清除缓存的时机


进入列表页后清除缓存


在列表页路由组件的beforeRouteEnter勾子中判断是否是从其他页面(Home)进入的,是则清除缓存,不是则使用缓存。


defineOptions({
name: 'List1',
beforeRouteEnter (to: RouteRecordNormalized, from: RouteRecordNormalized) {
if (from.name === 'Home') {
const { removeCacheEntry } = useRouteCache()
removeCacheEntry('List1')
}
}
})

这种缓存方式有个不太友好的地方:当从首页进入列表页,列表页和详情页来回切换,列表页是缓存的;但是在首页和列表页间用浏览器的前进后退来切换时,我们更多的是希望列表页能保留缓存,就像在多页面中浏览器前进后退会缓存原页面一样的效果。但实际上,列表页重新刷新了,这就需要使用另一种解决办法,点击链接时清除缓存清除缓存


点击链接跳转前清除缓存


在首页点击跳转列表页前,在点击事件的时候去清除列表页缓存,这样的话在首页和列表页用浏览器的前进后退来回切换,列表页都是缓存状态,只要当重新点击跳转链接的时候,才重新加载列表页,满足预期。


// 首页 Home.vue

<li>
<router-link to="/list" @click="removeCacheBeforeEnter">列表页</router-link>
</li>


<script setup lang="ts">
import useRouteCache from '@/hooks/useRouteCache'

defineOptions({
name: 'Home'
})

const { removeCacheEntry } = useRouteCache()

// 进入页面前,先清除缓存实例
function removeCacheBeforeEnter () {
removeCacheEntry('List')
}
</script>

状态管理实现缓存


通过状态管理库存储页面的状态和数据也能实现页面缓存。此处状态管理使用的是pinia。


首先使用pinia创建列表页store:


import { defineStore } from 'pinia'

interface Item {
id?: number,
content?: string
}

const useListStore = defineStore('list', {
// 推荐使用 完整类型推断的箭头函数
state: () => {
return {
isRefresh: true,
pageSize: 30,
currentPage: 1,
list: [] as Item[],
curRow: null as Item | null
}
},
actions: {
setList (data: Item []) {
this.list = data
},
setCurRow (data: Item) {
this.curRow = data
},
setIsRefresh (data: boolean) {
this.isRefresh = data
}
}
})

export default useListStore

然后在列表页中使用store:


<div>
<el-page-header @back="goBack">
<template #content>状态管理实现列表页缓存</template>
</el-page-header>
<el-table v-loading="loading" :data="tableData" border style="width: 100%; margin-top: 30px;">
<el-table-column prop="id" label="id" />
<el-table-column prop="content" label="内容"/>
<el-table-column label="操作">
<template v-slot="{ row }">
<el-link type="primary" @click="gotoDetail(row)">进入详情</el-link>
<el-tag type="success" v-if="row.id === listStore.curRow?.id">刚点击</el-tag>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:currentPage="listStore.currentPage"
:page-size="listStore.pageSize"
layout="total, prev, pager, next"
:total="listStore.list.length"
/>
</div>

<script setup lang="ts">
import useListStore from '@/store/listStore'
const listStore = useListStore()

...
</script>

通过beforeRouteEnter钩子判断是否从首页进来,是则通过 listStore.$reset() 来重置数据,否则使用缓存的数据状态;之后根据 listStore.isRefresh 标示判断是否重新获取列表数据。


defineOptions({
beforeRouteEnter (to: RouteLocationNormalized, from: RouteLocationNormalized) {
if (from.name === 'Home') {
const listStore = useListStore()
listStore.$reset()
}
}
})

onBeforeMount(() => {
if (!listStore.useCache) {
loading.value = true
setTimeout(() => {
listStore.setList(getData())
loading.value = false
}, 1000)
listStore.useCache = true
}
})

缺点


通过状态管理去做缓存的话,需要将状态数据都存在stroe里,状态多起来的话,会有点繁琐,而且状态写在store里肯定没有写在列表组件里来的直观;状态管理由于只做列表页数据的缓存,对于一些非受控组件来说,组件内部状态改变是缓存不了的,这就导致页面渲染后跟原来有差别,需要额外代码操作。


页面弹窗实现缓存


将详情页做成全屏弹窗,那么从列表页进入详情页,就只是简单地打开详情页弹窗,将列表页覆盖,从而达到列表页 “缓存”的效果,而非真正的缓存。


这里还有一个问题,打开详情页之后,如果点后退,会返回到首页,实际上我们希望是返回列表页,这就需要给详情弹窗加个历史记录,如列表页地址为 '/list',打开详情页变为 '/list?id=1'。


弹窗组件实现:


// PopupPage.vue

<template>
<div class="popup-page" :class="[!dialogVisible && 'hidden']">
<slot v-if="dialogVisible"></slot>
</div>
</template>

<script setup lang="ts">
import { useLockscreen } from 'element-plus'
import { computed, defineProps, defineEmits } from 'vue'
import useHistoryPopup from './useHistoryPopup'

const props = defineProps({
modelValue: {
type: Boolean,
default: false
},
// 路由记录
history: {
type: Object
},
// 配置了history后,初次渲染时,如果有url上有history参数,则自动打开弹窗
auto: {
type: Boolean,
default: true
},
size: {
type: String,
default: '50%'
},
full: {
type: Boolean,
default: false
}
})
const emit = defineEmits(
['update:modelValue', 'autoOpen', 'autoClose']
)

const dialogVisible = computed<boolean>({ // 控制弹窗显示
get () {
return props.modelValue
},
set (val) {
emit('update:modelValue', val)
}
})

useLockscreen(dialogVisible)

useHistoryPopup({
history: computed(() => props.history),
auto: props.auto,
dialogVisible: dialogVisible,
onAutoOpen: () => emit('autoOpen'),
onAutoClose: () => emit('autoClose')
})
</script>

<style lang='less'>
.popup-page {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
z-index: 100;
overflow: auto;
padding: 10px;
background: #fff;

&.hidden {
display: none;
}
}
</style>

弹窗组件调用:


<popup-page 
v-model="visible"
full
:history="{ id: id }">
<Detail></Detail>
</popup-page>


hook:useHistoryPopup 参考文章:juejin.cn/post/713994…



缺点


弹窗实现页面缓存,局限比较大,只能在列表页和详情页中才有效,离开列表页之后,缓存就会失效,比较合适一些简单缓存的场景。


父子路由实现缓存


该方案原理其实就是页面弹窗,列表页为父路由,详情页为子路由,从列表页跳转到详情页时,显示详情页字路由,且详情页全屏显示,覆盖住列表页。


声明父子路由:


{
path: '/list',
name: 'list',
component: () => import('./views/List.vue'),
children: [
{
path: '/detail',
name: 'detail',
component: () => import('./views/Detail.vue'),
}
]
}

列表页代码:


// 列表页
<template>
<el-table v-loading="loading" :data="tableData" border style="width: 100%; margin-top: 30px;">
<el-table-column prop="id" label="id" />
<el-table-column prop="content" label="内容"/>
<el-table-column label="操作">
<template v-slot="{ row }">
<el-link type="primary" @click="gotoDetail(row)">进入详情</el-link>
<el-tag type="success" v-if="row.id === curRow?.id">刚点击</el-tag>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:currentPage="currentPage"
:page-size="pageSize"
layout="total, prev, pager, next"
:total="list.length"
/>


<!-- 详情页 -->
<router-view class="popyp-page"></router-view>
</template>

<style lang='less' scoped>
.popyp-page {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 100;
background: #fff;
overflow: auto;
}
</style>

结尾


地址:


demo: xiaocheng555.github.io/page-cache/…


代码: github.com/xiaoch

eng55…

收起阅读 »

用proxy改造你的console

web
前言 在前端平常的开发中,最长使用的调试手段应该就是console大法。console很好用,但有时候打印变量多了,看起来就比较懵。 let name1 = 'kk'; name2 = 'kkk'; name3 = 'kk1k'; name4= 'k1kk';...
继续阅读 »

前言


在前端平常的开发中,最长使用的调试手段应该就是console大法。console很好用,但有时候打印变量多了,看起来就比较懵。


let name1 = 'kk'; name2 = 'kkk'; name3 = 'kk1k'; name4= 'k1kk';
console.log(name1)
console.log(name2)
console.log(name3)
console.log(name4)

打印如下
image.png


那个变量 对应那个 就比较难分辨。我又不想在写代码来分辨(懒😀),那个打印对应的变量是多少。


解决方案


方案一,通过ast解析console 将变量名放在console后面,奈何esbuild不支持ast操作(不是我不会 哈哈哈哈), 故放弃。


方案二,既然vue能代理对象,那么console是不是也能被代理。


实践


第一步代理console,将原始的console,用全局变量originConsole保存起来,以便后续使用
withLogging 函数拦截console.log 重写log参数


const originConsole = window.console; 
var console = new Proxy(window.console, {
get(target, property) {
if(property === 'log') {
return withLogging(target[property])
}
return target[property] },
})

遇到问题,js中 无法获取获取变量的名称的字符串。就是说无法打印变量名。


解决方案,通过vite中的钩子函数transform,将console.log(name.x) 转化成 console.log(name.x, ['isPlugin', 'name.x'])


      transform(src, id) {
if(id.includes('src')) { // 只解析src 下的console
const matchs = src.matchAll(/console.log\((.*)\);?/g);
[...matchs].forEach((item) => {
const [matchStr, args] = item;
let replaceMatch = ''
const haveSemicolon = matchStr.endsWith(";");
const sliceIndex = haveSemicolon ? -2 : -1;
const temp = matchStr.slice(0,sliceIndex);
const tempArgs = args.split(",").map(item => {
if(item.endsWith('"')) {
return item
}
return `"${item}"`
}).join(",")
replaceMatch = `${temp},['isPlugin',${tempArgs}]);`
src = src.replace(matchStr, replaceMatch)
});
}
return {
code: src,
id,
}
},

这样最终就实现了类型于这样的输出代码


  originConsole.log('name.x=', name.x)

这样也就最终实现了通过变量输出变量名跟变量值的一一对应


最后


我将其写成了一个vite插件,vite-plugin-consoles 感兴趣的可以试试,有bug记得跟我说(●'◡'●)


源码地址:
github.com/ALiangTech/…


作者:平平无奇的阿良
来源:juejin.cn/post/7238508573667344441
收起阅读 »

前端小白的几个坏习惯

web
最近在教授前端小白学员编写一些简单的网页。在这个过程中发现了小白们比较容易遇到的一些问题或者坏习惯,在这里对它们进行一一解释。 文件名命名 有些学员的文件命名是这样的: 除了网页的内容外,所有的东西都应该用英文,而不是拼音。 原因有如下几点: 编程不是一个...
继续阅读 »

最近在教授前端小白学员编写一些简单的网页。在这个过程中发现了小白们比较容易遇到的一些问题或者坏习惯,在这里对它们进行一一解释。


文件名命名


有些学员的文件命名是这样的:



除了网页的内容外,所有的东西都应该用英文,而不是拼音。


原因有如下几点:



  1. 编程不是一个人的活动,是群体活动。我们使用的编程语言、框架和库,几乎全部都是英文。使用中文,你的协作者会难以理解你的代码。而且中英混搭会让代码阅读困难。

  2. 使用拼音和使用汉字基本上没有什么区别,甚至还不如汉字直观。

  3. 拼音很难加音标,而且即使能加音标,也很难表达真正的意思,因为同音词太多,它存在多义性,比如 heshui,你不知道它到底是在表达喝水还是河水。

  4. 使用拼音会让你显得非常不专业。

  5. 坚持使用英文编程,有利于提高英语水平。


如果英语不好,刚开始可能会难以忍受,但是一旦熬过去开始这段时间,坚持下来,将会是长期的回报。


如果你英语实在是非常差劲,可以借助一些翻译软件。比如世界上最好的翻译网站:translate.google.com/,虽然是 Google 的域名,但是大陆并没有墙。


不止是文件名,变量、函数等事物都应该使用英文命名。


使用英语,越早越好。


文件类型命名


有些同学的文件命名是这样的:



文件命名的问题上面已经解释了,这里主要来看文件后缀名的问题。


应该使用小写 .htm/.html 结尾。


原因有如下几点:



  1. 不同的操作系统处理大小写是不一样的。Windows/Mac 系统大小写不敏感,Linux 系统大小写敏感。统一命名方式会具有更好的移植性。


比如我们有如下目录结构:


├── cat.html
├── dog.html

下面的代码在 Mac/Windows 系统上正常。


<a href="./Dog.html">跳转到狗的页面</a>

但是在 Linux 系统上会出现 404。


我们开发时通常是在 Mac/Windows 系统,这时问题很难暴露,但是部署时通常是在 Linux 系统,就容易导致开发时正常,部署时异常的不一致性。



  1. 易读性,事实证明小写的单词更易于阅读。

  2. 便捷性,文件名和后缀名都保持小写,不需要额外按下 Shift 键了。

  3. htm 和 html 的区别是,在老的 DOS 系统上,文件后缀名最多只支持 3 位。所以很多语言都会把文件后缀名限制成 3 位以内。现在的操作系统已经没有这个问题了,所以 htm 和 html 的作用是完全一致的。如果你追求简洁一点,那么使用 htm 时完全没问题的。


代码格式化


有些同学的代码是这样的:



VSCode 提供了 prettier 插件,我们可以使用它对代码格式化。


代码格式化有以下优点:



  1. 代码格式化后更易于阅读和修改。比如它会自动帮你添加空格、对齐、换行等。

  2. 不需要去刻意学习代码样式了,代码格式化工具会帮你做好,并且在这个过程中你会潜移默化的学会怎么样调整代码样式。

  3. 使用统一的代码格式化,可以帮助大家在协作时保持一致,不会有比必要的争议。

  4. 新人加入项目时也可以更容易地融入到项目,不会看到风格迥异的代码。

  5. 在代码合并的时候也可以减少冲突的发生。


建议一定要开启代码格式化。


补充说明


这部分和文章内容无关,是针对评论区进行补充。


掘金没有评论置顶功能,我没办法逐一回复评论区。所以只能在文末进行统一解释。


很多人在评论区说本文水,或者在拿拼音的事情抬杠。本来我不想解释,但是负面评论的人确实不少。


我说两点。


第一,文章标题开头四字明确表明目标群体是前端小白,小白是什么概念能明白吗?一定是「xxx源码解读」才是干货硬货?


第二,关于中文好还是英文好,我不想继续争论。我从业多年,看过无数项目源码,从后端 Java JDBC、Spring、JVM、Go 到前端 React、Redux、Webpack、Babel,无一例外全是英文。或者你随便找个初具规模的互联网中大厂或者外企的程序员,看看他们公司是不是有不让用拼音和汉字的规范。


程序员群体普遍的毛病就是固执己见。永远只是站在自己的视角去观察世界,看到的永远都是自己想看到的。然后去贸然指责,这样真的会显得自己很没有修养,而且很无知。


哪怕做不到感同身受,也应该给予应有的尊重,哪怕难以理解,也不要随意贬低。这是做人的基本修养。


掘金是技术分享平台,不是贴吧/知乎。我写文章的本心只是分享内容,没有收各位一分钱。


文章内容对你有价值的话,非常感谢你的点赞支持。


文章内容对你无用的话,退出去就好了。


言尽于此。


能看懂就看,再看不懂就直接屏蔽我吧,谢谢配合。


最后希望掘金能推出文章评论置顶功能,或者文章禁止评论功能。这对创作者来说绝对是刚需。


作者:代码与野兽
来源:juejin.cn/post/7142368724144619556
收起阅读 »

都JDK17了,你还在用JDK8

Spring Boot 3.1.0-M1 已经发布一段时间了,不知道各位小伙伴是否关注了。随着Spring 6.0以及SpringBoot 3.0的发布,整个开发界也逐步进入到jdk17的时代。大有当年从jdk6 到jdk8升级过程,痛苦并快乐着。 为了不被时...
继续阅读 »

Spring Boot 3.1.0-M1 已经发布一段时间了,不知道各位小伙伴是否关注了。随着Spring 6.0以及SpringBoot 3.0的发布,整个开发界也逐步进入到jdk17的时代。大有当年从jdk6 到jdk8升级过程,痛苦并快乐着。


为了不被时代抛弃,开发者应追逐新的技术发展,拥抱变化,不要固步自封。


0x01 纵观发展




  • Pre-alpha(Dev)指在软件项目进行正式测试之前执行的所有活动




  • LTS(Long-Term Support)版本指的是长期支持版本




  • Alpha 软件发布生命周期的alpha阶段是软件测试的第一阶段




  • Beta阶段是紧随alpha阶段之后的软件开发阶段,以希腊字母第二个字母命名




  • Release candidate 发行候选版(RC),也被称为“银色版本”,是具备成为稳定产品的潜力的 beta 版本,除非出现重大错误,否则准备好发布




  • Stable release 稳定版又称为生产版本,是通过所有验证和测试阶段的最后一个发行候选版(RC)




  • Release 一旦发布,软件通常被称为“稳定版”




下面我们来看下JDK9~JDK17的发展:


版本发布时间版本类型支持时间新特性
JDK 92017年9月长期支持版(LTS)5年- 模块化系统(Jigsaw)
- JShell
- 接口的私有方法
- 改进的 try-with-resources
- 集合工厂方法
- 改进的 Stream API
JDK 102018年3月短期支持版(non-LTS)6个月- 局部变量类型推断
- G1 垃圾回收器并行全阶段
- 应用级别的 Java 类数据共享
JDK 112018年9月长期支持版(LTS)8年- HTTP 客户端 API
- ZGC 垃圾回收器
- 移除 Java EE 和 CORBA 模块
JDK 122019年3月短期支持版(non-LTS)6个月- switch 表达式
- JVM 原生 HTTP 客户端
- 微基准测试套件
JDK 132019年9月短期支持版(non-LTS)6个月- switch 表达式增强
- 文本块
- ZGC 垃圾回收器增强
JDK 142020年3月短期支持版(non-LTS)6个月- switch 表达式增强
- 记录类型
- Pattern Matching for instanceof
JDK 152020年9月短期支持版(non-LTS)6个月- 移除 Nashorn JavaScript 引擎
- ZGC 垃圾回收器增强
- 隐藏类和动态类文件
JDK 162021年3月短期支持版(non-LTS)6个月- 位操作符增强
- Records 类型的完整性
- Vector API
JDK 172021年9月长期支持版(LTS)8年- 垃圾回收器改进
- Sealed 类和接口
- kafka客户端更新
- 全新的安全存储机制

需要注意的是,LTS 版本的支持时间可能会受到 Oracle 官方政策变化的影响,因此表格中的支持时间仅供参考。


0x02 详细解读


JDK9 新特性


JDK 9 是 Java 平台的一个重大版本,于2017年9月发布。它引入了多项新特性,其中最重要的是模块化系统。以下是 JDK 9 新增内容的详细解释:



  1. 模块化系统(Jigsaw):


Jigsaw 是 JDK 9 引入的一个模块化系统,它将 JDK 拆分为约 90 个模块。这些模块相互独立,可以更好地管理依赖关系和可见性,从而提高了代码的可维护性和可重用性。模块化系统还提供了一些新的工具和命令,如 jmod 命令和 jlink 命令,用于构建和组装模块化应用程序。



  1. JShell:


JShell 是一个交互式的 Java 命令行工具,可以在命令行中执行 Java 代码片段。它可以非常方便地进行代码测试和调试,并且可以快速地查看和修改代码。JShell 还提供了一些有用的功能,如自动补全、实时反馈和历史记录等。



  1. 接口的私有方法:


JDK 9 允许在接口中定义 private 和 private static 方法。这些方法可以被接口中的其他方法调用,但不能被实现该接口的类使用。这样可以避免在接口中重复编写相同的代码,提高了代码的重用性和可读性。



  1. 改进的 try-with-resources:


在 JDK 9 中,可以在 try-with-resources 语句块中使用 final 或 effectively final 的变量。这样可以避免在 finally 语句块中手动关闭资源,提高了代码的可读性和可维护性。



  1. 集合工厂方法:


JDK 9 提供了一系列工厂方法,用于创建 List、Set 和 Map 等集合对象。这些方法可以使代码更加简洁和易读,而且还可以为集合对象指定初始容量和类型参数。



  1. 改进的 Stream API:


JDK 9 引入了一些新的 Stream API 方法,如 takeWhile、dropWhile 和 ofNullable 等。takeWhile 和 dropWhile 方法可以根据指定的条件从流中选择元素,而 ofNullable 方法可以创建一个包含一个非空元素或空元素的 Stream 对象。


除了以上几个新特性,JDK 9 还引入了一些其他的改进和优化,如改进的 Stack-Walking API、改进的 CompletableFuture API、Java 应用程序启动时优化(Application Class-Data Sharing)等等。这些新特性和改进都为 Java 应用程序的开发和运行提供了更好的支持。


JDK10 新特性


JDK10是JDK的一个短期支持版本,于2018年3月发布。它的主要特性如下:




  1. 局部变量类型推断:Java 10中引入了一种新的语法——var关键字,可以用于推断局部变量的类型,使代码更加简洁。例如,可以这样定义一个字符串类型的局部变量:var str = "hello"




  2. G1 垃圾回收器并行全阶段:JDK10中对G1垃圾回收器进行了改进,使其可以在并行全阶段进行垃圾回收,从而提高了GC效率。




  3. 应用级别的 Java 类数据共享:Java 10中引入了一项新的特性,即应用级别的 Java 类数据共享(AppCDS),可以在多个JVM进程之间共享Java类元数据,从而加速JVM的启动时间。




  4. 线程局部握手协议:Java 10中引入了线程局部握手协议(Thread-Local Handshakes),可以在不影响整个JVM性能的情况下,暂停所有线程执行特定的操作。




  5. 其他改进:Java 10还包含一些其他的改进,例如对Unicode 10.0的支持,对时间API的改进,以及对容器API的改进等等。




总的来说,JDK10主要关注于提高Java应用程序的性能和可维护性,通过引入一些新的特性和改进对JDK进行优化。


JDK11 新特性


JDK11是JDK的一个长期支持版本,于2018年9月发布。它的主要特性如下:




  1. HTTP 客户端 API:Java 11中引入了一个全新的HTTP客户端API,可以用于发送HTTP请求和接收HTTP响应,而不需要依赖第三方库。




  2. ZGC 垃圾回收器:Java 11中引入了ZGC垃圾回收器(Z Garbage Collector),它是一种可伸缩且低延迟的垃圾回收器,可以在数百GB的堆上运行,且最大停顿时间不超过10ms。




  3. 移除Java EE和CORBA模块:Java 11中移除了Java EE和CORBA模块,这些模块在Java 9中已被标记为“过时”,并在Java 11中被完全移除。




  4. Epsilon垃圾回收器:Java 11中引入了一种新的垃圾回收器,称为Epsilon垃圾回收器,它是一种无操作的垃圾回收器,可以在不进行垃圾回收的情况下运行应用程序,适用于测试和基准测试等场景。




  5. 其他改进:Java 11还包含一些其他的改进,例如对Lambda参数的本地变量类型推断,对字符串API的改进,以及对嵌套的访问控制的改进等等。




总的来说,JDK11主要关注于提高Java应用程序的性能和安全性,通过引入一些新的特性和改进对JDK进行优化。其中,HTTP客户端API和ZGC垃圾回收器是最值得关注的特性之一。


JDK12 新特性


JDK12是JDK的一个短期支持版本,于2019年3月发布。它的主要特性如下:




  1. Switch 表达式:Java 12中引入了一种新的Switch表达式,可以使用Lambda表达式风格来简化代码。此外,Switch表达式也支持返回值,从而可以更方便地进行流程控制。




  2. Microbenchmark Suite:Java 12中引入了一个Microbenchmark Suite,可以用于进行微基准测试,从而更好地评估Java程序的性能。




  3. JVM Constants API:Java 12中引入了JVM Constants API,可以用于在运行时获取常量池中的常量,从而更好地支持动态语言和元编程。




  4. Shenandoah 垃圾回收器:Java 12中引入了Shenandoah垃圾回收器,它是一种低暂停时间的垃圾回收器,可以在非常大的堆上运行,且最大暂停时间不超过几毫秒。




  5. 其他改进:Java 12还包含一些其他的改进,例如对Unicode 11.0的支持,对预览功能的改进,以及对集合API的改进等等。




总的来说,JDK12主要关注于提高Java应用程序的性能和可维护性,通过引入一些新的特性和改进对JDK进行优化。其中,Switch表达式和Shenandoah垃圾回收器是最值得关注的特性之一。


JDK13 新特性


JDK13是JDK的一个短期支持版本,于2019年9月发布。它的主要特性如下:




  1. Text Blocks:Java 13中引入了一种新的语法,称为Text Blocks,可以用于在代码中编写多行字符串,从而简化代码编写的复杂度。




  2. Switch 表达式增强:Java 13中对Switch表达式进行了增强,支持多个表达式和Lambda表达式。




  3. ZGC 并行处理引用操作:Java 13中对ZGC垃圾回收器进行了改进,支持并行处理引用操作,从而提高了GC效率。




  4. Reimplement the Legacy Socket API:Java 13中重新实现了Legacy Socket API,从而提高了网络编程的性能和可维护性。




  5. 其他改进:Java 13还包含一些其他的改进,例如对预览功能的改进,对嵌套访问控制的改进,以及对集合API的改进等等。




总的来说,JDK13主要关注于提高Java应用程序的性能和可维护性,通过引入一些新的特性和改进对JDK进行优化。其中,Text Blocks和Switch表达式增强是最值得关注的特性之一。


JDK14 新特性


JDK14是JDK的一个短期支持版本,于2020年3月发布。它的主要特性如下:




  1. Records:Java 14中引入了一种新的语法,称为Records,可以用于定义不可变的数据类,从而简化代码编写的复杂度。




  2. Switch 表达式增强:Java 14中对Switch表达式进行了增强,支持使用关键字 yield 返回值,从而可以更方便地进行流程控制。




  3. Text Blocks增强:Java 14中对Text Blocks进行了增强,支持在Text Blocks中嵌入表达式,从而可以更方便地生成动态字符串。




  4. Pattern Matching for instanceof:Java 14中引入了一种新的语法,称为Pattern Matching for instanceof,可以用于在判断对象类型时,同时对对象进行转换和赋值。




  5. 其他改进:Java 14还包含一些其他的改进,例如对垃圾回收器和JVM的改进,对预览功能的改进,以及对JFR的改进等等。




总的来说,JDK14主要关注于提高Java应用程序的可维护性和易用性,通过引入一些新的特性和改进对JDK进行优化。其中,Records和Pattern Matching for instanceof是最值得关注的特性之一。


JDK15 新特性


JDK15是JDK的一个短期支持版本,于2020年9月发布。它的主要特性如下:




  1. Sealed Classes:Java 15中引入了一种新的语法,称为Sealed Classes,可以用于限制某个类的子类的数量,从而提高代码的可维护性。




  2. Text Blocks增强:Java 15中对Text Blocks进行了增强,支持在Text Blocks中使用反斜杠和$符号来表示特殊字符,从而可以更方便地生成动态字符串。




  3. Hidden Classes:Java 15中引入了一种新的类,称为Hidden Classes,可以在运行时动态创建和卸载类,从而提高代码的灵活性和安全性。




  4. ZGC并发垃圾回收器增强:Java 15中对ZGC垃圾回收器进行了增强,支持在启动时指定内存大小,从而提高了GC效率。




  5. 其他改进:Java 15还包含一些其他的改进,例如对预览功能的改进,对JVM和垃圾回收器的改进,以及对集合API和I/O API的改进等等。




总的来说,JDK15主要关注于提高Java应用程序的可维护性和性能,通过引入一些新的特性和改进对JDK进行优化。其中,Sealed Classes和Hidden Classes是最值得关注的特性之一。


JDK16 新特性


JDK16是JDK的一个短期支持版本,于2021年3月发布。它的主要特性如下:




  1. Records增强:Java 16中对Records进行了增强,支持在Records中定义静态方法和构造方法,从而可以更方便地进行对象的创建和操作。




  2. Pattern Matching for instanceof增强:Java 16中对Pattern Matching for instanceof进行了增强,支持在判断对象类型时,同时对对象进行转换和赋值,并支持对switch语句进行模式匹配。




  3. Vector API:Java 16中引入了一种新的API,称为Vector API,可以用于进行SIMD(Single Instruction Multiple Data)向量计算,从而提高计算效率。




  4. JEP 388:Java 16中引入了一个新的JEP(JDK Enhancement Proposal),称为JEP 388,可以用于提高Java应用程序的性能和可维护性。




  5. 其他改进:Java 16还包含一些其他的改进,例如对垃圾回收器、JVM和JFR的改进,对预览功能的改进,以及对集合API和I/O API的改进等等。




总的来说,JDK16主要关注于提高Java应用程序的性能和可维护性,通过引入一些新的特性和改进对JDK进行优化。其中,Records增强和Pattern Matching for instanceof增强是最值得关注的特性之一。


JDK17 新特性


JDK17是JDK的一个长期支持版本,于2021年9月发布。它的主要特性如下:




  1. Sealed Classes增强:Java 17中对Sealed Classes进行了增强,支持在Sealed Classes中定义接口和枚举类型,从而提高代码的灵活性。




  2. Pattern Matching for switch增强:Java 17中对Pattern Matching for switch进行了增强,支持在switch语句中使用嵌套模式和or运算符,从而提高代码的可读性和灵活性。




  3. Foreign Function and Memory API:Java 17中引入了一种新的API,称为Foreign Function and Memory API,可以用于在Java中调用C和C++的函数和库,从而提高代码的可扩展性和互操作性。




  4. JEP 391:Java 17中引入了一个新的JEP(JDK Enhancement Proposal),称为JEP 391,可以用于提高Java应用程序的性能和可维护性。




  5. 其他改进:Java 17还包含一些其他的改进,例如对垃圾回收器、JVM和JFR的改进,对预览功能的改进,以及对集合API和I/O API的改进等等。




总的来说,JDK17主要关注于提高Java应用程序的灵活性、可扩展性和性能,通过引入一些新的特性和改进对JDK进行优化。其中,Sealed Classes增强和Foreign Function and Memory API是最值得关注的特性之一。


总结




  • JDK9:引入了模块化系统、JShell、私有接口方法、多版本兼容性等新特性




  • JDK10:引入了局部变量类型推断、垃圾回收器接口、并行全垃圾回收器等新特性




  • JDK11:引入了ZGC垃圾回收器、HTTP客户端API、VarHandles API等新特性




  • JDK12:引入了Switch表达式、新的字符串方法、HTTP/2客户端API等新特性




  • JDK13:引入了Text Blocks、Switch表达式增强、改进的ZGC性能等新特性




  • JDK14:引入了Records、Switch表达式增强、Pattern Matching for instanceof等新特性




  • JDK15:引入了Sealed Classes、Text Blocks增强、Hidden Classes等新特性




  • JDK16:引入了Records增强、Pattern Matching for instanceof增强、Vector API等新特性




  • JDK17:引入了Sealed Classes增强、Pattern Matching for switch增强、Foreign Function and Memory API等新特性




总的来说,JDK9到JDK17的更新涵盖了Java应用程序开发的各个方面,包括模块化、垃圾回收、性能优化、API增强等等,为Java开发者提供了更多的选择和工具,以提高代码的质量和效率


小记


Java作为一门长盛不衰的编程语言,未来的发展仍然有许多潜力和机会。




  • 云计算和大数据:随着云计算和大数据的发展,Java在这些领域的应用也越来越广泛。Java已经成为了许多云计算平台和大数据处理框架的首选语言之一。




  • 移动端和IoT:Java也逐渐开始在移动端和物联网领域崭露头角。Java的跨平台特性和安全性,使得它成为了许多移动应用和物联网设备的首选开发语言。




  • 前沿技术的应用:Java社区一直在积极探索和应用前沿技术,例如人工智能、机器学习、区块链等。Java在这些领域的应用和发展也将会是未来的趋势。




  • 开源社区的发展:Java开源社区的发展也将会对Java的未来产生重要影响。Java社区的开源项目和社区贡献者数量不断增加,将会为Java的发展提供更多的动力和资源。




  • 新的Java版本:Oracle已经宣布将在未来两年内发布两个新的Java版本,其中一个是短期支持版本,另一个是长期支持版本。这将会为Java开发者提供更多的新特性和改进,以满足不断变化的需求。




总的来说,Java作为一门优秀的编程语言,具有广泛的应用和发展前景。随着技术的不断创新和社区的不断发展,Java的未来将会更加光明。


更多内容:


image.png


作者:QIANGLU
来源:juejin.cn/post/7238795712569802809
收起阅读 »

卸下if-else 侠的皮衣!

web
🤭当我是if-else侠的时候 😶怕出错 给我一个功能,我总是要写很多if-else,虽然能跑,但是维护起来确实很难受,每次都要在一个方法里面增加逻辑,生怕搞错,要是涉及到支付功能,分分钟炸锅 😑难调试 我总是不知道之前写的逻辑在哪里,一个方法几百行逻辑,而且...
继续阅读 »

🤭当我是if-else侠的时候


😶怕出错


给我一个功能,我总是要写很多if-else,虽然能跑,但是维护起来确实很难受,每次都要在一个方法里面增加逻辑,生怕搞错,要是涉及到支付功能,分分钟炸锅


😑难调试


我总是不知道之前写的逻辑在哪里,一个方法几百行逻辑,而且是不同功能点冗余在一起!这可能让我牺牲大量时间在这查找调试中


🤨交接容易挨打


当你交接给新同事的时候,这个要做好新同事的白眼和嘲讽,这代码简直是坨翔!这代码简直是个易碎的玻璃,一碰就碎!这代码简直是个世界十大奇迹!


🤔脱下if-else侠的皮衣


先学习下开发的设计原则


单一职责原则(SRP)



就一个类而言,应该仅有一个引起他变化的原因



开放封闭原则(ASD)



类、模块、函数等等应该是可以扩展的,但是不可以修改的



里氏替换原则(LSP)



所有引用基类的地方必须透明地使用其子类的对象



依赖倒置原则(DIP)



高层模块不应该依赖底层模块



迪米特原则(LOD)



一个软件实体应当尽可能的少与其他实体发生相互作用



接口隔离原则(ISP)



一个类对另一个类的依赖应该建立在最小的接口上



在学习下设计模式


大致可以分三大类:创建型结构型行为型

创建型:工厂模式 ,单例模式,原型模式

结构型:装饰器模式,适配器模式,代理模式

行为型:策略模式,状态模式,观察者模式


为了尽快脱掉这个if-else的皮衣,我们就先学习一种比较容易接受的设计模型:策略模式


策略模式


举个例子



  • 当购物类型为“苹果”时,满 100 - 20,不满 100 打 9 折

  • 当购物类型为“香蕉”时,满 100 - 30,不满 100 打 8 折

  • 当购物类型为“葡萄”时,满 200 - 50,不叠加

  • 当购物类型为“梨”时,直接打 5 折

    然后你根据传入的类型和金额,写一个通用逻辑出来,
    当我是if-else侠的时候,我估计会这样写:


funcion getPrice(type,money)
//处理苹果
if(type == 'apple'){
if(money >= 100){
return money - 20
}
return money * 0.9
}
//处理香蕉
if(type == 'banana'){
if(money >= 100){
return money - 30
}
return money * 0.8
}
//处理葡萄
if(type == 'grape'){
if(money >= 200){
return money - 50
}
return money
}
//处理梨
if(type == 'pear'){
return money * 0.5
}
}

然后我们开始来分析问题:\



  1. 违反了单一职责原则(SRP)

    一个方法里面处理了四个逻辑,要是里面的哪个代码块出事了,调试起来也麻烦

  2. 违反了开放封闭原则(ASD)

    假如我们要增加多一种水果的逻辑,就又要在这个方法中修改,然后你修改完这个方法,就跟测试说,我在这个方法增加了多一个种水果,可能要重新回归这个方法,那你看测试增加了多少工作量



改造考虑:消灭if-else, 支持扩展但是不影响原本功能!



const fruitsPrice = {
apple(money){
if(money >= 100){
return money - 20
}
return money * 0.9
},
banana(money){
if(money >= 100){
return money - 30
}
return money * 0.8
},
grape(money){
if(money >= 200){
return money - 50
}
return money
},
pear(money){
return money * 0.5
}
}

首先定义一个fruitPrice对象,里面都是各种水果价格的映射关系

然后我们调用的时候可以这样


function getPrice(type,money){
return fruitsPrice[type](money)
}

当我们要扩展新水果的时候


fruitsPrice.orange = function(money){
return money*0.4
}

综上所述:
用策略模式重构这个原本的逻辑,方便扩展,调试,清晰简明,当然这只是一个模式重构的情况,可能还有更优的情况,靠大家摸索


结尾


遵守设计规则,脱掉if-else的皮衣,善用设计模式,加油,骚年们!

作者:向乾看
来源:juejin.cn/post/7239267216805871671
给我点点赞,关注下!

收起阅读 »

环信十周年趴——我的程序人生

我是一名网瘾少年...记得上小学那会,每天中午我都会去学校的机房打传奇,那时候跟着老师一起弄了一个私服,感觉很牛,可能正是因为这个,才开始我的计算机编码启蒙。然而,也仅仅是启蒙了,初中和高中沉迷在了游戏中,不可自拔;也正因为如此,考了一个普通的大学,还得服从调...
继续阅读 »

我是一名网瘾少年...

记得上小学那会,每天中午我都会去学校的机房打传奇,那时候跟着老师一起弄了一个私服,感觉很牛,可能正是因为这个,才开始我的计算机编码启蒙。

然而,也仅仅是启蒙了,初中和高中沉迷在了游戏中,不可自拔;也正因为如此,考了一个普通的大学,还得服从调剂,但万幸的是专业是计算机科学与技术,我可以早一年在寝室配电脑。

整个大学依然沉迷游戏,颓废度过,到了大四,由于别的同学已经有找到实习工作的了,我才开始出现焦虑;为了未来,我踏上了去北京的火车,去学习iOS开发,当时是2015年,已经过了最火爆的时候,学成之后,我在北京四处碰壁,有些是因为我没有工作经验,有些则是因为我是培训出身,苦熬半个月,马上过年了,没办法只能打道回府。

回到老家本想着过完年再战北京,但阴差阳错,我在老家找了一份iOS开发工作,工作稳定,挣得钱够花,也就渐渐放弃了北京梦。

如今,我已在iOS开发这个领域做了6年多,在不断学习中,有很多收获,同时也用业余的时间学习python和MySQL,安卓也有涉猎,并且微信小程序可以接私活,挣外快;我坚信,继续坚持自我的修行之路,不断的提高自己的技能,一定能成为更加优秀的程序员。

生活虽然平淡如水,但总能在不经意间有一些小收获,我想,这也算一种幸福的生活。

最后,环信真的是一款优秀的产品,文档通俗易懂,接口功能丰富,在这个环信十周年之际,我祝愿环信越办越好,发展壮大,奋勇向前。

收起阅读 »

如何让安卓应用有两个入口

在使用鼎鼎大名的 leakcanary 检测内存泄漏时,我们发现,添加了 leakcanary 依赖后,再次运行 app 时,桌面上会多一个应用图标。 打开这个 Leaks 应用就能看到自己的 app 中存在的内存泄漏。 让桌面上多一个应用图标,这是怎么做到...
继续阅读 »

在使用鼎鼎大名的 leakcanary 检测内存泄漏时,我们发现,添加了 leakcanary 依赖后,再次运行 app 时,桌面上会多一个应用图标。


leakcanary


打开这个 Leaks 应用就能看到自己的 app 中存在的内存泄漏。


让桌面上多一个应用图标,这是怎么做到的呢?


答案是使用 activity-alias


一、为应用设置两个入口,分别启动两个 Activity


举个例子,通过 activity-alias 为应用程序指定另一个入口:

<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".SecondActivity"
android:exported="false"
android:taskAffinity="second.affinity"/>
<activity-alias
android:name="SecondActivity"
android:exported="true"
android:icon="@mipmap/ic_launcher"
android:label="SecondActivity"
android:targetActivity=".SecondActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>

可以看到,应用入口是 MainActivity,但我们通过 activity-alias 给 SecondActivity 也设置了应用入口的 intent-filter,安装后,桌面就会有两个入口:


activity-alias


点击两个图标就会启动两个不同的 Activity。这里还给 SecondActivity 设置了 taskAffinity,目的是让 SecondActivity 启动时,被放在一个新的栈中。


二、为应用设置两个入口,启动同一个 Activity


activity-alias 添加入口时,是不是一定要启动不同的 Activity 呢?


答案是不一定。activity-alias 也允许我们为同一个 Activity 定义多个别名,从而实现一个应用程序拥有多个图标或多个启动入口的效果。

<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity-alias
android:name=".Alias"
android:icon="@mipmap/ic_chrome"
android:label="Fake Chrome"
android:targetActivity=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>

可以看到,应用入口是 MainActivity,但我们通过 activity-alias 给 MainActivity 又设置了一个别名,安装后,桌面就会有两个入口:


activity-alias


点击两个图标都会启动同一个 Activity。


三、activity-alias 还能做什么?


如果我们需要设置一个 Activity 支持打开网页,通常会采用这样的做法:

<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="http" />
</intent-filter>
</activity>

这里给 MainActivity 添加了支持打开网页的 intent-filter。运行后,当遇到打开链接的请求时,就会弹出这样的对话框:


open with


除了这种方式,activity-alias 也可以实现同样的功能。

<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity-alias
android:name=".browser"
android:targetActivity=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="http" />
</intent-filter>
</activity-alias>

另外,activity-alias 还可以给我们的应用再加一个 label 说明。

<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity-alias
android:name=".browser"
android:label="My Browser"
android:targetActivity=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="http" />
</intent-filter>
</activity-alias>

此时再打开链接,就会在 My Application 底部展示我们新增的 label: My Browser:


My Browser


四、总结


activity-alias 为应用程序提供了更多的灵活性和可定制性。使用activity-alias,我们可以为一个Activity定义多个入口,从而增强应用程序的用户体验。


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

五年后端外包仔的回顾

前言 背景:普通二本 专业:软件工程 第一次写文章,之前都是看别人写文章,觉得很厉害,自己也想尝试写写,以一个普通人的角度分享一下自己的经历,抒发一下emo情绪,顺便做个总结。 转眼间已经工作了快五年,从一个一窍不通的小白变成略知一二的老白,看着隔壁面试一个实...
继续阅读 »

前言


背景:普通二本


专业:软件工程


第一次写文章,之前都是看别人写文章,觉得很厉害,自己也想尝试写写,以一个普通人的角度分享一下自己的经历,抒发一下emo情绪,顺便做个总结。
转眼间已经工作了快五年,从一个一窍不通的小白变成略知一二的老白,看着隔壁面试一个实习生,又想起了以前。


实习


一开始我也没想到自己会进这个行业,本来是想选择数学专业,因为我对数学比较感兴趣,但是我妈觉得数学专业不太好找工作,而且那时候IT还是算比较火的行业,也感谢我妈当年选择了一条还可以的赛道= =


大四找实习的时候,遭到了面试官的毒打,我才发现自己这四年来只顾着打游戏了,问啥啥不会,才真正接触八股文。当年的八股文比现在还算简单不少,都是基础题。回想起来之前是真的有点离谱,学了ssm,背了背八股文就冲了,面试官还说我是不是经常打游戏...后面每天都投一堆简历,参加面试,最后入职了一家外包公司,开启了外包仔生活。


第一家公司算是帮助我入了这个行业吧。公司是上市公司,收了很多实习生,当时面试也没问什么,可能是把我们当做一张白纸。实习培训了公司的框架,后面转正了就开始投入项目。项目中用的是公司的框架,后端ssh,前端angularjs,做的是内部系统。化身CRUD工程师天天加班赶需求,做了两年多感觉没啥提升,除了业务基本没有成长,而且还要驻点出差。那时候驻点佛山,我坐顺风车去佛山的时候,司机问怎么从广州去到佛山工作,是不是工资很高?我:一言难尽...


跳槽


因为薪资和职业发展原因选择裸辞了,本来前公司的经理想着外包我回去一个项目工作,价格也谈好了,后面又没下文了。在这段空档期放纵了一会儿,去了旅游,吃了大餐,逛了公园。


鸣沙山.jpg
摩打.jpg
五羊.jpg

快乐了一段时间后,就跳槽到这家公司呆到了现在。这家公司其实还挺好的,走路上班而且不怎么加班,唯一的缺点就是薪资方面,总结就是钱少事少离家近。公司业务是做互联网电视的,在这里我也接触到很多以前公司没有的场景,比如高并发和大数据量的处理方案,感觉自己的CRUD能力变得成熟一点了。


精神内耗


在这个内卷、焦虑的时代中,有的人开开心心摸鱼躺平,有的人努力考公上岸,有的人努力学习保持进步,而我就是现实躺平,又想着自己要加油的状态。很喜欢黄执中的一句话

半吊子得不到幸福,你在此岸望彼岸,你两头不到岸

因为疫情原因去年年终奖没了,又想跳槽了,看了很多文章更焦虑了。老生常谈的35岁失业、大厂裁员、寒冬将至......躺平真的好快乐,打打游戏,摸摸鱼,刷刷抖音一天又过去了,大哥几年前送我的算法就只看了目录,感觉工作后静下心来看书挺难的。但是这样子温水煮青蛙感觉35岁就要去当保安了,所以还是加入内卷队伍吧。


我是一个极其懒惰的人,可能想着一出是一出,今天想看书,明天想写博客,但是只是停留在脑子里,没有付诸于行动。其实还是要动起手来,就好像我写这篇文章一样,写得不好也没啥关系,也是一种进步。所以说想到什么就做吧!


写在最后


定几个小目标吧:



  1. 每周花时间看看书

  2. 搭建一个自己的项目

  3. 抽空刷leetcode

  4. 找到一个更好的公司平台


摸鱼躺平也好,拼搏奋斗也罢,还是相信当下的决定是自己认为最好的选择,最后祝大家活成理想的样子。


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

小米:阳了,被裁了

随着防疫政策的放开,小阳人越来越多了,身边很多小伙伴都在朋友圈晒自己阳了之后的各种状态,基本上都处于一边发烧,一边坚持工作的状态,症状严重的小伙伴忍着疼痛还要处理公司的任务,把自己奉献给公司,然后收到了却是公司无情的裁员的消息。 年末将至,知乎和小米也登上了热...
继续阅读 »

随着防疫政策的放开,小阳人越来越多了,身边很多小伙伴都在朋友圈晒自己阳了之后的各种状态,基本上都处于一边发烧,一边坚持工作的状态,症状严重的小伙伴忍着疼痛还要处理公司的任务,把自己奉献给公司,然后收到了却是公司无情的裁员的消息。


年末将至,知乎和小米也登上了热搜。


裁员


我之前在小米的同事陆陆续续收到了通知,阳着还在工作,然后收到了裁员的消息。国内公司裁员的吃相都不怎么好看,基本上在发年终奖前会进行大比例的裁员。


2021 年的时候小米有 32000 名员工,据传 2022 年底小米要裁 6000 名员工,裁员幅度接近 20%,无论消息是否真实,但是这次裁员规模影响范围应该不小。



小米为什么要裁员


小米连续 3 个季度开始下滑,前 3 个季度,每个季度利润 20 亿,相比于去年同期的 50 亿下跌了很多,那为什么利润下跌这么多呢,主要有以下原因:



  1. 公司不赚钱,意味着主营业务开始萎缩,小米的主营业务,手机前 3 个季度卖了 4020 万部,销售额大概 425 亿,平均每部手机 1000 元,原本指望华为被制裁之后,小米能拿下这部分用户,但是最后也放弃了,这部分用户基本上都归苹果了

  2. 据调查中国的手机市场已经处于饱和状态,每年换手机的发烧友越来越少了

  3. 小米赌上全部身价大踏步地进入汽车领域,汽车是个周期长、投资大的业务,没有上百个亿,基本上不可能会有结果的

  4. 小米的股价也跌了很多,投资人很失望,我也买了很多小米的股票,基本上都是血亏


所以不得不开始降本增效,在老板的眼里,业务上升期的时候,开始疯狂砸钱招人,到达了瓶颈,业务不再增长的时候,老板就会冷静下来盘算,到底需不需要这么多人,然后开始降本增效,而裁员就是最有效的控制成本的手段。


曾经有小伙伴问过,小米的年终奖能拿多少


我在这里也只是顺口一说,大家当做饭后余兴看看就好了,小米的年终奖是 2 个月,而个人绩效是跟部门和所在事业部挂钩的,如果部门的绩效好的话,大部分人都能拿满,但是如果部门绩效不好的话,只有少数人能拿满,平均下来一个部门能拿满 2 个月的人数非常少,如果你非常的优秀,拿 3~4 个月也是有的,但是这个比例极其少,如果你和领导关系好的话,那么就另当别论了。


小米这次裁员赔偿虽然给了 N+2,但是这次裁员的吃相也比较难看,引来了小米员工的吐槽。以下图片来自网络。





而每次裁员,应届生都是最惨的,在大裁员的环境下,能不能找到工作是最大的问题,应届生和有工作经验的社招生是不一样的,无论是赔偿还是找工作的机会,相比于应届生更愿意招社招生,当然特别优秀的除外。




我之前很多在小米的同事,赔偿都给了 N + 2,但是年底被裁员时间点非常的不好,短时间内,想找到工作是非常困难的,但是先不要着急,如果你的身体还没恢复,建议先等身体恢复,在恢复期间,整理一下你的工作项目,网上搜索一下面试题,整理和回顾这些面试题,记住一定要多花时间刷算法题。


等到年后找工作会容易些,面试的成功的率也会很高,你的溢价空间也会很大,在选择公司的时候,这个阶段还是以稳为主,避开那些风险高的公司和部门。


文章的最后


遍地小阳人的冬天比以往更冷,在公司非常艰难,业务不再增长的时候,都会断臂求生,我们都要去面对被裁的风险。


站在打工者的角度,当一个人在某个环境待久了,会被表象的舒适所蒙蔽,时间久了会变得很迷茫,所以我们要想办法跳出舒适圈,保持学习的热情。



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

入坑两个月自研创业公司

一、拿offer 其实入职前,我就感觉到有点不对劲,居然要自带电脑。而且人事是周六打电话发的offer!自己多年的工作经验,讲道理不应该入这种坑,还是因为手里没粮心中慌,工作时间长的社会人,还是不要脱产考研、考公,疫情期间更是如此,本来预定2月公务员面试,结果...
继续阅读 »

一、拿offer


其实入职前,我就感觉到有点不对劲,居然要自带电脑。而且人事是周六打电话发的offer!自己多年的工作经验,讲道理不应该入这种坑,还是因为手里没粮心中慌,工作时间长的社会人,还是不要脱产考研、考公,疫情期间更是如此,本来预定2月公务员面试,结果一直拖到7月。


二、入职工作


刚入职工作时,一是有些抗拒,二呢是有些欣喜。抗拒是因为长时间呆家的惯性,以及人的惰性,我这只是呆家五个月,那些呆家一年两年的,再进入社会,真的很难,首先心理上他们就要克服自己的惰性和惯性,平时生活习惯也要发生改变


三、人言可畏


刚入职工作时,有工作几个月的老员工和我说,前公司的种种恶心人的操作,后面呢我也确实见识到了:无故扣绩效,让员工重新签署劳动协议,但是,也有很多不符实的,比如公司在搞幺蛾子的时候,居然传出来我被劝退了……


四、为什么离开


最主要的原因肯定还是因为发不出工资,打工是为了赚钱,你想白嫖我?现在公司规模也不算小了,想要缓过来,很难。即便缓过来,以后就不会出现这样的状况了?公司之前也出现过类似的状况,挺过来的老员工们我也没看到有什么优待,所以这家公司不值得我去熬。技术方面我也基本掌握了微信和支付宝小程序开发,后面不过是需求迭代。个人成长方面,虽然我现在是前端部门经理,但前端组跑的最快,可以预料后面我将面临无人可用的局面,我离职的第二天,又一名前端离职了,约等于光杆司令,没意义。


五、收获


1.不要脱产,不要脱产
2.使用uniapp进行微信和支付宝小程序开发
3.工作离家近真的很爽
4.作为技术人员,只要你的上司技术还行,你的工期他是能正常估算,有什么难点说出来,只要不是借口,他也能理解,同时,是借口他也能一下识别出来,比如,一个前端和我说:“后端需求不停调整,所以没做好。”问他具体哪些调整要两个星期?他又说不出来。这个借口就不要用了,但是我也要走了,我也没必要去得罪他。
5.进公司前,搞清楚公司目前是盈利还是靠融资活,靠融资活的创业公司有风险…


六、未来规划


关于下一份工作:
南京真是外包之城,找了两周只有外包能满足我目前18k的薪资,还有一家还降价了500…
目前offer有
vivo外包,20k
美的外包,17.5k
自研中小企业,18.5k


虽然美的外包薪资最低,但我可能还是偏向于美的外包。原因有以下几点:
1.全球手机出货量下降,南京的华为外包被裁了不少,很难说以后vivo会不会也裁。
2.美的目前是中国家电行业的龙头老大,遥遥领先第二名,目前在大力发展b2c业务,我进去做的也是和商场相关。
3.美的的办公地点离我家更近些
4.自研中小企业有上网限制,有过类似经验的开发人,懂得都懂,很难受。


关于考公:
每年10月到12月准备下,能进就进,不能再在考公上花费太多时间了。


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

不想写代码的程序员可以尝试的几种职位

标题不够严谨,应该是不想写业务代码的程序员可以做什么。 这里主要覆盖大家可能平时没关注,或者是国内少见的工作;所以像 technical product manager, project manager 这种就不再赘述了。 这里也主要分享 IT 行业内的岗位,...
继续阅读 »

标题不够严谨,应该是不想写业务代码的程序员可以做什么。


这里主要覆盖大家可能平时没关注,或者是国内少见的工作;所以像 technical product manager, project manager 这种就不再赘述了。


这里也主要分享 IT 行业内的岗位,要是除开行业限制,范围就太大了。


Developer Relation/Advocate


国外有很多面向开发者的技术创新公司,比如 Vercel ,PlanetScale ,Prisma ,Algolia 等。


这类公司的用户就是开发者,所以他们的市场活动也都是围绕着开发者;他们需要让更多的开发者可以更容易地把他们的技术用到他们的技术栈里,所以就有了这种岗位。用中文来表达,可能有点类似是布道师的意思?


国内更多是将技术应用起来,而不是创造一些新的技术,所以这种岗位在国内就非常少见了。当然近几年也还是有一些技术驱动型公司的,像 PingCAP ,Agora 等。


希望国内有更多像这样的公司出来。


Technical Recruiter


这个工作从 title 上就大概知道是做什么的了。


这个岗位有深有浅,深的可能是比较完整的招聘职能,浅的可能就是 HR 部门里面试和筛选技术候选人的。


Technical Writer


这个听着像是产品经理的工作,确实会和产品的职责有小部分重叠。


这是个面向内部的岗位,不喜欢对外对用户 /客户的朋友会非常喜欢。通常是一些比较大型的企业要做软件迁移或者什么系统、流程升级之类的时候,因为会牵扯到非常多的 moving parts ,所以通常都需要一个独立岗位来负责 documentation 的工作。


工作内容包括采访以及记录各部门的现有流程和业务需求,然后是新流程 /系统 /软件的手册、图表等等。


这里的“technical”不是我们研发中的技术,更多是“业务”层面的意思。同样这个岗位对技术要求不高,但是有研发背景是非常加分的。


Technical Support


通常这个岗位归属客服部门,高于普通 customer service rep 。普通的 customer support 是客户遇到问题时的第一层支持 - 基本会讲话、了解产品就能干的工作;如果第一层解决不了客户的问题,就会升级到后面 technical support 了。


这个岗位范围会更广一点,几乎任何 IT 公司都会有这种支持岗;对技术要求根据不同公司而不同,比如 Vercel 对这个岗位的技术要求肯定比 HelpScout (一个客服软件)要高。


但整体来说都不如研发要求高,但对应的薪酬待遇也没有研发那么好。


结语


其实说了这么多总结下来就是国外技术生态、开源氛围好很多,并且对技术足够的重视,促使很多技术公司的出现,然后催生了这些工作。


如果觉得本帖有启发,欢迎留言支持鼓励后续的创作。


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

深入学习 Kotlin 枚举的进阶用法:简洁又高效~

Kotlin 作为现代的、强大的编程语言,可以给开发者提供诸多特性和工具,得以帮助我们编写更加高效、更具可读性的代码。 其中一个重要的特性便是 Enum 枚举,其本质上是一种数据类型:允许你定义一组用名称区分的常量。 本篇文章将通过代码案例带你探索 Kotli...
继续阅读 »

Kotlin 作为现代的、强大的编程语言,可以给开发者提供诸多特性和工具,得以帮助我们编写更加高效、更具可读性的代码。


其中一个重要的特性便是 Enum 枚举,其本质上是一种数据类型:允许你定义一组用名称区分的常量


本篇文章将通过代码案例带你探索 Kotlin 枚举的进阶用法,进而帮助大家理解如何将 Enum 更好地应用到项目当中。


1. 枚举类


可以说 Enum Classes 是 Kotlin 中展示一组常量的绝佳方式。


具体来说,它允许你定义一组有限数量的成员来限定数据类型,并且你可以在代码的各处便捷使用这些枚举类型。


如下,我们用 enum 关键字定义一周内各天的枚举类型。

 enum class DayOfWeek {
     MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
 }

然后在代码中自由使用该枚举,比如:

 fun getWeekendDays(): List<DayOfWeek> {
     return listOf(DayOfWeek.SATURDAY, DayOfWeek.SUNDAY)
 }

2. 枚举属性


除了展示类型,Kotlin Enum 还可以拥有属性 property,这意味着开发者可以给枚举成员添加额外的信息。


比如下面,我们给 DayOfWeek 枚举增加各天在周内的序号属性。

 enum class DayOfWeek(val number: Int) {
     MONDAY(1),
     TUESDAY(2),
     WEDNESDAY(3),
     THURSDAY(4),
     FRIDAY(5),
     SATURDAY(6),
     SUNDAY(7)
 }

然后便可以获得该天的序号信息。

 fun getDayNumber(day: DayOfWeek): Int {
     return day.number
 }

3. 枚举函数


Kotlin Enum 也支持定义函数,所以可以在枚举内部定义功能性方法、供外部使用。


如下在 DayOfWeek 枚举里增加一个用来判断该天是否属于周末的 isWeekend() 函数。

 enum class DayOfWeek(val number: Int) {
     MONDAY(1),
     TUESDAY(2),
     WEDNESDAY(3),
     THURSDAY(4),
     FRIDAY(5),
     SATURDAY(6),
     SUNDAY(7);
 
     fun isWeekend(): Boolean {
         return this == SATURDAY || this == SUNDAY
    }
 }

在使用该枚举的地方,便可以直接使用该函数进行判断。

 fun printDayType(day: DayOfWeek) {
     if (day.isWeekend()) {
         println("$day is a weekend day.")
    } else {
         println("$day is a weekday.")
    }
 }

4. 枚举构造函数


既然 Enum 可以拥有属性,那么自然支持构造函数,所以开发者可以在实例构造的时候,增加充分多的信息。


比如,我们在 DayOfWeek 枚举的构造函数里,在序号以外增加该天的名称信息。

 enum class DayOfWeek(val number: Int, val displayName: String) {
     MONDAY(1, "Monday"),
     TUESDAY(2, "Tuesday"),
     WEDNESDAY(3, "Wednesday"),
     THURSDAY(4, "Thursday"),
     FRIDAY(5, "Friday"),
     SATURDAY(6, "Saturday"),
     SUNDAY(7, "Sunday");
 
     override fun toString(): String {
         return displayName
    }
 }

这样便可以获得该枚举携带的名称数据。

 fun printDayName(day: DayOfWeek) { 
     println("The day of the week is ${day.displayName}")
 }

5. 枚举扩展函数


和普通类一样,也可以针对 Enum Class 添加扩展函数。我们可以在枚举类外部,按需添加额外的功能函数。


比如这里给 DayOfWeek 枚举扩展一个获取下一天的函数。

 fun DayOfWeek.nextDay(): DayOfWeek {
     return when (this) {
         MONDAY -> TUESDAY
         TUESDAY -> WEDNESDAY
         WEDNESDAY -> THURSDAY
         THURSDAY -> FRIDAY
         FRIDAY -> SATURDAY
         SATURDAY -> SUNDAY
         SUNDAY -> MONDAY
    }
 }

像调用枚举本身定义的函数一样,自由使用该扩展函数。

 fun printNextDay(day: DayOfWeek) {
     println("The next day is ${day.nextDay()}")
 }

结语


可以看到 Kotlin Enum 可以帮助开发者定义好一组类型的常量:大大简化代码、具备更好的可读性以及提供额外的功能函数。


通过上述的进阶用法,相信大家可以使用 Enum 创造出更加健壮和高效的代码,同时也更容易理解和维护。


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

【Android】Kotlin 中特别的关键字

前言 Kotlin 是一种现代化的静态类型编程语言,被广泛应用于 Android 开发、Web 开发等领域。与其他编程语言相比,Kotlin 中具有一些独特的关键字,本文将介绍这些关键字及其使用。 1. data data 是 Kotlin 中的一个关键字,用...
继续阅读 »

前言


Kotlin 是一种现代化的静态类型编程语言,被广泛应用于 Android 开发、Web 开发等领域。与其他编程语言相比,Kotlin 中具有一些独特的关键字,本文将介绍这些关键字及其使用。


1. data


data 是 Kotlin 中的一个关键字,用于定义数据类。数据类是一种特殊的类,用于封装数据,通常不包含任何业务逻辑。定义数据类时,只需要列出需要存储的属性即可,Kotlin 会自动生成一些常用的方法,如 toString()equals()hashCode() 等。

kotlinCopy code
data class User(val id: Int, val name: String, val age: Int)

2. companion object


companion object 是 Kotlin 中的一个关键字,用于定义伴生对象。伴生对象是一个类内部的单例对象,可以访问该类的私有成员。与 Java 中的静态方法类似,Kotlin 中的伴生对象可以定义静态方法和静态属性。

class Utils {
companion object {
fun add(a: Int, b: Int): Int {
return a + b
}
}
}

val result = Utils.add(1, 2)

3. lateinit


lateinit 是 Kotlin 中的一个关键字,用于定义延迟初始化的变量。延迟初始化的变量必须是非空类型的,并且不能使用 val 关键字定义。在变量被访问之前,它必须被初始化,否则会抛出 UninitializedPropertyAccessException 异常。

class Person {
lateinit var name: String

fun initName() {
name = "John"
}

fun printName() {
if (::name.isInitialized) {
println(name)
} else {
println("Name has not been initialized yet")
}
}
}

val person = Person()
person.initName()
person.printName()

4. by


by 是 Kotlin 中的一个关键字,用于实现委托。委托是一种将对象的某些职责交给另一个对象处理的机制,可以简化代码的编写。Kotlin 中的委托分为接口委托、类委托和属性委托三种,通过 by 关键字实现。

interface Car {
fun drive()
}

class RealCar : Car {
override fun drive() {
println("Real car is driving")
}
}

class FakeCar(private val realCar: RealCar) : Car by realCar {
override fun drive() {
println("Fake car is driving")
}
}

val realCar = RealCar()
val fakeCar = FakeCar(realCar)
fakeCar.drive()

5. when


when 是 Kotlin 中的一个关键字,用于实现类似于 switch-case 的语句。与 switch-case 不同的是,when 可以匹配更加复杂的条件表达式,并且支持任意类型的值作为条件。另外,when 的分支可以是表达式,可以方便地处理多种情况。

fun getScore(level: String): Int = when (level) {
"A" -> 90
"B" -> 80
"C" -> 70
else -> 0
}

val scoreA = getScore("A")
val scoreB = getScore("B")

6. object


object 是 Kotlin 中的一个关键字,用于定义匿名对象或单例对象。匿名对象是一种不需要定义类名的对象,可以在需要的时候创建并使用。单例对象是一种只有一个实例的对象,可以在整个应用程序中共享使用。

fun main() {
val person = object {
val name = "John"
val age = 20
}
println("${person.name} is ${person.age} years old")
}

object Config {
val host = "localhost"
val port = 8080
}

val host = Config.host
val port = Config.port

7. reified


reified 是 Kotlin 中的一个关键字,用于实现泛型类型的具体化。在 Java 中,泛型类型在运行时会被擦除,导致无法获取泛型类型的具体信息。而在 Kotlin 中,通过 reified 关键字可以将泛型类型具体化,可以在运行时获取泛型类型的信息。

inline fun <reified T> getType(): String = T::class.java.simpleName

val typeName = getType<Int>()

总结


Kotlin 中的关键字具有一些独特的特性,如数据类、伴生对象、延迟初始化、委托、when、对象表达式和具体化的泛型类型等。这些特性可以让开发者更加方便地编写代码,提高代码的可读性和可维护性。


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

2023移动端技术探索

1. 行业背景 过去的2022年对大家来说都是困难的一年,难在疫情影响,难在宏观环境的增长放缓。没有增长带来的就是痛苦的体验,对于互联网行业,21年的主题是“反996”,到了22年风向就变成了“降本增效”、“业务搜索”以及“裁员”。再细化动移动端,经过十年的发...
继续阅读 »

1. 行业背景


过去的2022年对大家来说都是困难的一年,难在疫情影响,难在宏观环境的增长放缓。没有增长带来的就是痛苦的体验,对于互联网行业,21年的主题是“反996”,到了22年风向就变成了“降本增效”、“业务搜索”以及“裁员”。再细化动移动端,经过十年的发展,它已经步入“成熟期”,各行各业都被改造差不多了,技术上该有的轮子都有了,基础的服务也搭建差不多了,似乎真正到达瓶颈了,存量时代的小修小补对人力的需求已经是对半砍了。脉脉《抢滩数字时代·人才迁徙报告2023》报告显示:2022年企业招聘总职位数量同比减少21.67%,纯互联网职位量同比减少50.4%。


2023-01-30-23-04-03-image.png
又到了制定新一年OKR的时候了,大家都在发愁技术项目的规划,不知道在技术上去做哪些探索和突破。InfoQ发布的《中国软件技术发展洞察和趋势预测研究报告2023》第三条核心结论显示:2022年技术服务理念转变,从技术先进到业务赋能,IT部门公司定位逐渐由成本部门转向业务赋能部门,技术也更被边缘化了,个人职业发展屏障出现,这个时候我们不禁对前途迷茫甚至产生质疑。再细化到移动端,《中国软件技术发展洞察和趋势预测研究报告2023》展示的“中国技术成熟度评估曲线”中前沿和早期推广项目貌似都与移动端没有太大关系。


2023-01-31-15-59-41-image.png


本文尝试从各个方面探索移动端可以发展的方向,最大程度的“压榨”可能的技术方向(有些只是抛出问题,而不是最终答案)。


2. 近两年大厂探索方向与成果


在挖掘之前先看看大厂(可能是某个领域有所建树)这些年在做什么,看看有没有直接可以抄的作业。


2.1 21年出调研结果


21年初写OKR时对几个大厂做了调研,下面分别看看阿里、美团、京东做了什么,准备做什么:


阿里移动端技术全景图


2023-01-30-15-31-22-image.png


阿里移动端发展趋势


2023-01-30-15-31-56-image.png


美团移动端技术全景图


2023-01-30-15-45-52-image.png


京东移动端技术全景图


2023-01-30-15-32-53-image.png


京东移动端未来远景图


2023-01-30-15-43-34-image.png


看起来都大同小异,可能各个规模的公司都在建设或者建设完成。


2.2 22年产出


在看看22年大厂的输出,这里主要来自于企业技术公众号输出内容。


2.2.1 阿里


阿里推出的《2022技术人的百宝黑皮书》总结了2022年阿里年度精选终端技术栈的内容:


2023-01-31-17-21-38-image.png


2.2.2 美团


下面内容摘自美团技术发布的《2022年美团技术年货-合辑》:


2023-01-31-17-35-46-image.png


2.2.3 百度


百度App技术公众号发布2022精选文章:


2023-01-31-17-39-09-image.png


2.2.4 分析


从上面三家企业对外输出的文章看,在移动端的动作不外乎几个方向:



  1. 跨端/低代码

  2. 性能优化

  3. 自动化测试

  4. 开发平台/平台化能力

  5. AI


3. 移动端主要方向分析


结合上面整理出来的,我们看看移动端“可以”有哪些方向。


3.1 业务开发


业务开发还是主流的市场需求,这块会占大部分的比例,IT部门从成本部门转为赋能部门后,主要的工作量就是支持业务。


3.2 跨端/低代码


在降本增效的背景下,跨端还会持续搞,但是也不是新东西了。H5、React、Flutter、小程序,这些都各有利弊,不同场景用不同技术,像小程序这种更适合平台化的超级APP,规模不够大的话,性价比不高。


3.3 性能优化


同样的,性能优化也是需要持续做的事情,但是也不是新东西了,一些技术手段都比较成熟了,没有太多可挖掘的空间了。


3.4 架构方向


架构管理方向随着规模的收缩,很难出现机会了。


3.5 开发平台建设


在公司内部,类似于蚂蚁的mPaas开发平台在业务快速成长期对提效会有很大的帮助,这个时候随着业务的裂变,推出各种APP,开发平台可以避免很多重复的工作,助力应用快速上线和运营,但是在收缩期再去建设就有点不划算了。


单点的平台能力,比如监控、埋点之类的或者用第三方或者也自建完成了,对缺失的个别能力,可以根据业务需求点滴建设。


3.6 系统应用/Framework/驱动开发


随着AI、Iot、新能源的发展与兴起,释放出一些系统开发的诉求,相对于之前,嵌入式驱动开发的薪资也有所增长,也算是一个方向,但是也要记住,比起手机,电视、汽车毕竟是少数,如果纯转嵌入式的话可能沾Iot的光规模更大些,不过比起芯片,这也是比较成熟的技术,可挖掘方向不大,只是多了个写业务的战场。


3.7 XR


目前比较成熟的是VR,但是VR在端上展示主要基于H5,采集会有单独硬件,有些也支持了手机采集,但是还是那句话,市场需求不大。至于AR、元宇宙更多的是AI的综合应用了,我们也不讨论了。


3.8 音视频


音视频一直是移动端比较大和前沿的一块方向,但是现在也已趋于成熟,下面看看主要的几个方向:



  • 点播:播放器的事情,主要涉及多解码期、预加载秒开,剩下的交给系统播放器都可以完成的很好了;

  • 录制:系统录制工具,或者基于系统采集、编码、封装封装一套;

  • 视频编辑和特效处理:编辑主要是解复用--->解码--->逐帧处理--->编码--->复用的过程,逐帧处理用到视频上主要设计合成、滤镜等;音频主要是变声、声音融合,都是通用的技术,稍微体现差异的就是特效处理中与AI的结合,比如美颜、带眼镜等会用到图像检测,但是都不是门槛,也谈不上前沿探讨;

  • 直播:直播也同样有成熟的结局方案:采集推流,开源服务端以及成熟CDN,播放ijk,秒开之类的都是参数优化了;

  • 实时音视频:实时音视频开发成本比较高,主要的挑战是弱网对抗,3A处理等,由于不是通用协议,没有CDN,自己搭建机房成本高,而且不见得效果比第三方好,所以也是一件性价比比较低的事情。

  • 编解码:目前主流的还是H264,VP8,H264甚至都没有推开,限制编解码算法的主要是推广和兼容性,所以编解码器都是一些组织去搞,一个公司贸然去开发,风险很大。


3.9 AI


人类一直在追求更智能的机器,AI是未来,所以即使现在不够好,并且没有找到太多的落地场景,但是很多公司还在搞,尤其是ChatGPT的能力让大家惊讶,但是它仍然不是真正的“像人类一样”的智能。目前通用的AI主要有一下几个方向:




  1. 语音方向



    1. 前端信号处理

    2. 唤醒

    3. 语音识别

    4. 声纹

    5. TTS

    6. 作曲:抖音之前分享有过这方面实践和应用

    7. 基于特征的语音编码:比如谷歌推出的的lyra和SoundStream,Lyra的设计工作速率为3kbps,听力测试表明,在该比特率下,Lyra的性能优于任何其他编解码器,并优于Opus的8kbps,因此实现了60%以上的带宽削减。但是正如上面说的,编解码器的瓶颈主要还是在于标准的推广。




  2. 图像方向



    1. 检测

    2. 识别

    3. 图像比较(应用于UI自动化测试)




  3. 自然语言处理



    1. 智能问答

    2. 意图识别

    3. 文档纠错




  4. 风控




  5. 推荐




  6. 用户画像




  7. 元宇宙/数字人:数字人更像是一个AI的综合应用。




还有些特殊的特殊业务场景的特殊用户,比如房产领域:



  1. 户型解读(基于图像的特殊特征)

  2. 训练场


对于AI,移动端可以做哪些探索?回答这个问题先要搞明白哪些场景适配放在端上来做。Android官方给了一个决策的标准:


2023-01-31-23-57-36-image.png


上面提到的特别需要在端上应用的主要有:



  1. 唤醒

  2. 图像检测

  3. 语音编解码


可以放在端上应用的:



  1. ASR

  2. TTS

  3. 图像标签


基于这些场景端上的主要工作量是什么呢?模型训练大部分还是放在云端,端上就是加载模型,输入数据,展示输出结果,还有可能就是对引擎,框架做些优化:


2023-01-30-15-44-47-image.png


4. 总结


整体来看,整个移动端技术的发展可以说到了“山穷水尽”的地步,可挖掘的创新型内容不是很多了,大部分都是在现有体系维护和迭代。整体来看业务支撑还是主要的需求来源,车机、Iot也释放出一些机会,跨端、开发平台、性能优化、VR已趋于成熟,端智能落地的还是语音、图像这些通用的方向,深度结合业务的还有待挖掘。目前的AI解决的还是“决策”问题,从现在生成式到未来创造式通用的、”人类水平“的“智能”还有很长的路要走,谁也不能打包票说雷·库兹韦尔提出的奇点理论的“奇点”能不能到来,什么时候到来,智能的进化不止是算法层面的,还会收到算力的影响,像《流浪地球》系列中的MOSS机器人是因为量子计算机算力快的加成。


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

环信十周年趴——我的程序人生之RTC

        作为一名程序员,我的编程之路始于13年,当时我对计算机和编程非常感兴趣。我开始学习Java,这是我接触到的第一种编程语言。逐渐地,我发现编程可以让我创造出自己的东西...
继续阅读 »

        作为一名程序员,我的编程之路始于13年,当时我对计算机和编程非常感兴趣。我开始学习Java,这是我接触到的第一种编程语言。逐渐地,我发现编程可以让我创造出自己的东西并解决实际问题,这让我感到非常有成就感。

        随着时间的推移,我开始学习其他编程语言,包括Python和Go语言。这些语言各有优劣,但都有助于我更好地理解编程思想和实践。

        我的职业生涯中,我曾经涉足央企、CDN、RTC等多个产品线。在每个产品线中,我都学习到了新的知识和技能。我注意到,不同的产品线需要不同的技术和能力,这让我不断地学习和成长。

        我的程序人生充满了挑战和机遇。在这个过程中,我遇到了许多有趣的问题和难题,并通过不断的学习和实践找到了解决方法。我也遇到了一些非常有才华和热情的同事,他们对我的职业生涯产生了深远的影响。

        我相信,我的程序人生还有很长的路要走。未来,我将继续学习新的技术和探索新的领域,以便更好地服务于社会和行业。我坚信,编程是一项有趣而有价值的事业,它可以让我们创造出更好的世界。

本文参与环信十周年活动

收起阅读 »

《环信十周年趴——我的程序人生之iOS- Flutter》

    我是一名iOS开发工程师,已经在这个行业里摸爬滚打了14年。在这14年中,我经历了从Objective-C到Swift再到Flutter的演进,也经历了从少年时期的满怀抱负到中年危机的现实冲击。现在,我想分享一下...
继续阅读 »


    我是一名iOS开发工程师,已经在这个行业里摸爬滚打了14年。在这14年中,我经历了从Objective-C到Swift再到Flutter的演进,也经历了从少年时期的满怀抱负到中年危机的现实冲击。现在,我想分享一下我的程序人生,希望能给那些像我曾经一样,心中有梦的年轻人一些启示。

    在我刚开始从事iOS开发的时候, Objective-C还是主流语言。那时候,我用C语言的基本语法,学习Objective-C的面向对象编程,一步步攻克了这个语言的第一道难关。但是,随着时间的推移,Objective-C的局限性越来越明显。它语法笨重、效率不高,越来越难以满足我们开发的需求。于是,Swift语言应运而生。
Swift语言的出现,让我看到了iOS开发的新希望。它快速、安全、简洁,有着现代编程语言的特性,如可选类型、函数式编程等。在Swift语言的基础上,我能够编写出更加高效、更加优美的代码,也能够更加深入地理解iOS框架的本质。同时,Swift语言也为我后续学习其他语言提供了很大的帮助。

    随着移动开发的不断发展,跨平台开发成为了一个越来越重要的话题。Flutter逐渐成为了时下移动开发领域的一个热门话题。我开始探索这个新的开发框架,并迅速被它的快速高效、易于学习所吸引。Flutter允许开发者使用Dart语言编写应用程序,并能够在多个平台上运行,包括iOS和Android。这对于需要跨平台开发的人来说,是一个巨大的优势。通过学习Flutter,我不仅拓宽了自己的技能树,还为未来的发展奠定了更加坚实的基础。

    在多年的开发工作中,我不仅积累了丰富的开发经验,还面临了各种挑战。在这个过程中,我从一个满怀梦想和追求的少年成长为一个中年大叔,在职业道路上走过了漫长的历程。我曾在北京这样的城市追逐梦想,也曾为了安逸的生活而选择了哈尔滨这样的城市。我的职业生涯也因此经历了起起伏伏,有过了许多成就和贡献,也面临过中年危机和职业瓶颈。

然而,尽管我的职业生涯充满了挑战和不确定性,我始终坚信技术发展带来的机遇和变革。我相信,只要我们保持学习、适应和发展的态度,不断面对挑战并克服困难,我们终将在程序人生的道路上找到自己的位置,实现自己的价值。

    未来,我希望继续探索新的技术和框架,不断拓宽自己的技能树和学习领域。我相信,只有不断尝试新事物、不断挑战自己,才能在程序人生的道路上走得更远、更高。同时,我也希望把自己的经验和所学所悟传递给更多有志于从事程序员这个行业的年轻人,帮助他们少走弯路,实现自己的梦想。

    场面话说完了,说一下这几年要感谢的人吧。 QQ好友洪江Objective-C学习道路上的伙伴,白嫖了他一套iOS开发的课程,经常一起半夜互相改代码的兄弟,QQ好友阿林同学97年的开发者,白嫖一套游戏源码。最最需要感谢是我死皮赖脸认的师傅,凌晨三点起来给我改代码被媳妇大骂的声音至今还在我的脑海中。还有就是自己了。没想到上课坐不住板凳的自己,遇到代码后我可以变得这样的安静。

    我的程序人生,充满了挑战和机遇,也充满了不确定性和可能性。我期待着在这个道路上继续前行,不断探索、学习和成长,迎接未来的挑战和机遇。

本文参与环信十周年活动,活动链接:https://www.imgeek.net/question/474026”

收起阅读 »

《环信十周年趴——程序之路也有得失,不必介怀》

我的程序生涯可谓是充满了曲折和成长的旅程。从一开始的业余爱好,到如今的职业发展,我经历了许多挑战和机遇,也积累了不少宝贵的经验。回顾起来,我最初接触编程是在大学期间。当时,我被计算机的神秘和无限可能性所吸引,开始学习编写简单的代码。起初,我对编程还不太熟悉,但...
继续阅读 »


我的程序生涯可谓是充满了曲折和成长的旅程。从一开始的业余爱好,到如今的职业发展,我经历了许多挑战和机遇,也积累了不少宝贵的经验。

回顾起来,我最初接触编程是在大学期间。当时,我被计算机的神秘和无限可能性所吸引,开始学习编写简单的代码。起初,我对编程还不太熟悉,但是通过不断的学习和实践,我渐渐掌握了一些基本的编程语言和技巧。

毕业后,我决定将编程作为我的职业。我投身于软件开发行业,从一名初级程序员开始。在职场中,我面对了各种项目和团队合作的挑战。通过不断学习和与同事的交流,我的编程能力得到了提升,我也逐渐熟悉了软件开发的流程和方法。

随着时间的推移,我逐渐担任更高级的职位,并开始负责一些重要项目的开发。同时,我也积极追求自我提升,不断学习新的编程技术和工具。我学习了机器学习和数据科学的知识,掌握了一些流行的开发框架和库。这些新技能不仅提升了我的职业竞争力,还让我能够解决更加复杂的问题。

在职场上,我也遇到了一些奇葩和不愉快的经历。有一次,我加入了一家初创公司,他们开发了一款虚拟现实游戏。我被聘为首席程序员,负责游戏的核心功能开发。开始时,我对这个机会充满了期待,希望能够在这个新兴行业有所突破。

然而,不久之后,我发现这家公司的管理层存在着一些奇葩的决策和不合理的要求。他们对于开发进度的期望过高,要求我们在短时间内完成大量的工作。同时,他们也没有给予足够的资源和支持,导致我们在技术上遇到了很多困难。

更糟糕的是,公司的管理层对于员工的待遇也非常吝啬。工资低于行业平均水平,福利待遇简直可以说是微乎其微。而且,他们还经常加班,但却不提供加班补偿。这让我感到非常不满和失望。

在与同事的交流中,我发现大家都对公司的管理方式感到不满。许多人都在考虑离职,寻找更好的机会。尽管我对这个项目充满了热情,但最终我还是做出了离职的决定。

离开这家公司后,我感到一种解脱和自由。我决定重新评估自己的职业规划,并寻找更好的工作环境。我参加了一些技术研讨会和行业活动,扩展了人脉和知识面。

通过努力和坚持,我最终找到了一家知名游戏开发公司的工作机会。这家公司有着良好的声誉和优秀的团队氛围。在这里,我得到了更好的薪资待遇和职业发展机会。与同事们的合作也非常愉快,他们互相支持和激励,共同追求技术的进步和项目的成功。

在新的公司,我不仅继续提升自己的技术能力,还积极参与项目的管理和领导工作。我逐渐晋升为高级程序员,并负责指导和培养新人。我享受着这种成长和发展的过程,同时也在职业道路上收获了不断增长的薪资。

除了职业发展,我还热衷于扩展自己的知识领域。我广泛阅读与编程相关的书籍,不仅加深了对技术的理解,还开拓了思维的广度。这些书籍不仅拓宽了我的知识面,也为我在工作中遇到的问题提供了解决思路。

在编程道路上,我结识了许多优秀的同行和导师。他们在我职业发展中起到了关键的作用。他们与我分享自己的经验和知识,给予了我很多指导和支持。有时候,在解决问题的过程中,他们的帮助让我事半功倍。

总结而言,我的程序生涯经历了起伏和挑战,但也收获了许多成长和成功。通过不断学习和努力,我掌握了新的技能,取得了薪资的增长,结识了良师益友。我学会了在职场中勇敢面对困难,果断做出改变并寻找更好的机会。这些经历让我明白了职业选择的重要性,一个良好的工作环境和管理团队对于个人的成长和幸福感至关重要。

在我的职业规划中,我也意识到了不断学习和适应新技术的重要性。随着科技的迅猛发展,编程领域也在不断演进。我持续关注行业的趋势,并主动学习新的编程语言、框架和工具。这使我能够跟上潮流,提升自己的竞争力,并为公司的发展做出贡献。

此外,我也始终注重个人的硬件装备。一台高效的电脑和适合编程需求的工具是提高工作效率的关键。我不断更新我的硬件设备,并保持其良好状态,以确保在工作中能够高效地完成任务。

在这个职业生涯中,我经历了职场的起伏和挑战,但我始终坚持不懈地追求自己的梦想和目标。通过遇到的困难和不愉快的经历,我学会了坚持和勇敢面对挑战,也学会了在逆境中寻找机会和改变。

通过不断学习、拓展技能、寻找良师益友和适应职业发展的机会,我在程序生涯中取得了成长和进步。我不仅拥有了稳定的职业发展和增长的薪资,还培养了自己的领导能力和团队合作精神。

总的来说,程序生涯是一段充满挑战和机遇的旅程。通过坚持不懈的努力和持续学习,我在职业道路上取得了一定的成就。我相信,只要保持对技术的热情和对自我提升的追求,我将继续在编程的世界中不断成长和创造出更多的价值。

自己总结了一句话。

对于命运,不必抱怨什么。因为,你就是你的上帝。

                                                              ---- 致自己

收起阅读 »